From 824c9b3534b9be82d677c16f20107f821ffddebe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:36:35 +0000 Subject: [PATCH] Deploy to GitHub pages --- 404.html | 47 + CNAME | 1 + about-the-author/index.html | 47 + .../personal-life/2024-07-24.html | 47 + ...\350\277\252\346\226\257\345\260\274.html" | 47 + about-the-author/personal-life/index.html | 47 + about-the-author/personal-life/wewe.html | 47 + about-the-author/works/index.html | 47 + ...\344\272\272\344\275\234\345\223\201.html" | 47 + ads.txt | 1 + article/index.html | 59 + ...\346\236\266\346\236\204.html-B9tl8qmz.js" | 1 + ...\346\224\271\347\211\210.html-BVYNR54w.js" | 72 + assets/1mysql.html-CW43wjS_.js | 1 + ...\344\275\277\347\224\250.html-9FjHGi9W.js" | 226 + assets/2017.html-DzgcI9cv.js | 1 + assets/2018.html-UHfvoYE6.js | 1 + assets/2019.html-Df2iUZPh.js | 1 + assets/2024-07-24.html-9GUkpnJj.js | 1 + ...\346\226\257\345\260\274.html-CU80Bk9D.js" | 1 + ...\344\273\273\345\212\241.html-NSgzVWXM.js" | 61 + ...\347\263\273\347\273\237.html-BH3nB-v9.js" | 206 + assets/404.html-BMHKR2ag.js | 1 + ...\351\203\250\347\275\262.html-BN5IQG4o.js" | 96 + ...\345\244\207\344\273\275.html-Ca5VIWc1.js" | 61 + ...\346\217\222\344\273\266.html-Br7NfWE4.js" | 13 + ...\345\210\206\346\236\220.html-DopYDuB6.js" | 1 + ...\344\274\230\345\214\226.html-8ms_Vznn.js" | 1 + ...HTTPS\343\200\201HTTP2.0.html-DvomVRAD.js" | 1 + ...\345\217\202\346\225\260.html-BtZgPFK4.js" | 24 + ...\346\250\241\345\236\213.html-Bis3Hglq.js" | 150 + ...\347\254\224\350\256\260.html--8cnUWGp.js" | 16 + ...\346\224\266\351\233\206.html-SN4rN7dm.js" | 161 + ...\345\210\206\346\236\220.html-DBlO1MVO.js" | 199 + ...\344\275\277\347\224\250.html-CvPEGSZS.js" | 49 + assets/aop.html-C7ud1qex.js | 51 + assets/app-ftEjETWs.js | 330 + assets/chatgpt.html-BgeRWiYw.js | 101 + assets/cms.html-Dfw5I__q.js | 1 + assets/devops-ping-tai.html-CjTXGrBr.js | 237 + assets/elastic-spark.html-Cz2jVT52.js | 27 + ...272\220\347\240\201debug.html-Bo6HcrbI.js" | 1 + assets/feed.html-BcapapFL.js | 1 + assets/g1.html-CcgAHdWl.js | 1 + assets/giscus-BZxmVUME.js | 66 + assets/icon/apple-icon-152.png | Bin 0 -> 3086 bytes assets/icon/chrome-192.png | Bin 0 -> 3760 bytes assets/icon/chrome-512.png | Bin 0 -> 9230 bytes assets/icon/chrome-mask-192.png | Bin 0 -> 4079 bytes assets/icon/chrome-mask-512.png | Bin 0 -> 9208 bytes assets/icon/guide-maskable.png | Bin 0 -> 1691 bytes assets/icon/guide-monochrome.png | Bin 0 -> 1465 bytes assets/icon/ms-icon-144.png | Bin 0 -> 3445 bytes ...\350\256\276\347\275\256.html-BThIkzaw.js" | 17 + assets/index-AN989yVn.js | 61 + assets/index.html-0EEVTum5.js | 1 + assets/index.html-5ZTxzR_p.js | 1 + assets/index.html-7tsgb-dC.js | 1 + assets/index.html-B1Akg086.js | 1 + assets/index.html-B2fh3Mku.js | 1 + assets/index.html-B8Qu0Pmr.js | 1 + assets/index.html-B9smc87Y.js | 1 + assets/index.html-BAbeWXfD.js | 1 + assets/index.html-BEv74XKv.js | 1 + assets/index.html-BKAyyte1.js | 1 + assets/index.html-BTDM2Wpl.js | 1 + assets/index.html-BXiz75su.js | 1 + assets/index.html-BcZZ4qCt.js | 1 + assets/index.html-BwVnwmVL.js | 1 + assets/index.html-By-pQ7Wl.js | 1 + assets/index.html-C9P5ouBn.js | 1 + assets/index.html-CD6aFyf5.js | 1 + assets/index.html-CMe--mJk.js | 1 + assets/index.html-CY6pLx2d.js | 1 + assets/index.html-CdxL9VtY.js | 1 + assets/index.html-CpCz4LWW.js | 1 + assets/index.html-D1R9zQcp.js | 1 + assets/index.html-D4WG7yPk.js | 1 + assets/index.html-DGy09Ap4.js | 1 + assets/index.html-DJHdkCFZ.js | 1 + assets/index.html-DJZmVM8a.js | 1 + assets/index.html-DQFfT5fQ.js | 1 + assets/index.html-DRyhNgG6.js | 1 + assets/index.html-DWXiwN5o.js | 1 + assets/index.html-DYmvtxA9.js | 1 + assets/index.html-DaAoZU-K.js | 1 + assets/index.html-DlwW5Vje.js | 1 + assets/index.html-DqVhfNm4.js | 1 + assets/index.html-DtDo2qGn.js | 1 + assets/index.html-KwQlr7R4.js | 1 + assets/index.html-SdCcjUz-.js | 1 + assets/index.html-TDa4J3OS.js | 1 + assets/index.html-Uqis1PLJ.js | 1 + assets/index.html-jGjDZXTI.js | 1 + assets/index.html-muK9iEFv.js | 1 + assets/index.html-nZcQpO1z.js | 1 + assets/index.html-wU0YCmBl.js | 1 + ...346\213\237\346\234\272).html-CpFuSPpy.js" | 7 + assets/kafka.html-tMeeu2BM.js | 1 + ...\344\270\273\346\234\272.html-BbWWw_2k.js" | 41 + assets/main.html-B2OQipck.js | 1 + assets/nio.html-DVcQhX_V.js | 1 + assets/photoswipe.esm-GXRgw7eJ.js | 4 + ...\347\274\223\345\255\230.html-H1xCPktw.js" | 87 + assets/request_limit.html-CBNYeI2L.js | 7 + assets/serverlog.html-bICZU0Nm.js | 2 + assets/spark on k8s operator.html-D9Tpznzk.js | 67 + assets/springcloud_sentinel.html-CFCKUB71.js | 1 + assets/starcraft-ai.html-C018rwGF.js | 1 + assets/style-BjEFg8CQ.css | 1 + assets/synchronized.html-B2MTfSju.js | 59 + assets/tesla.html-K0riacrL.js | 23 + assets/tiktok2023.html-BxK5gJWF.js | 1 + assets/volatile.html-C-UWQAjc.js | 1 + assets/webflux.html-KjGFiVkN.js | 1 + assets/wewe.html-BLY5_jfL.js | 1 + assets/zgc.html-Cj33F57G.js | 4 + assets/zookeeper.html-Dd8HToeq.js | 1 + ...\350\257\246\350\247\243.html-Dg8HV8OO.js" | 275 + ...\345\221\275\344\273\244.html-BtEABYaE.js" | 1 + ...\350\277\207\347\250\213.html-zaM0dK5_.js" | 4 + ...\345\270\214\346\247\275.html-a1P2YWrj.js" | 1 + ...\344\275\234\345\223\201.html-DDpX9I3v.js" | 1 + ...\345\260\217\350\256\260.html-Bb5S9Lg5.js" | 4 + ...\344\270\273\346\234\272.html-DMCiuYMs.js" | 56 + ...\344\275\277\347\224\250.html-DfOcEEjE.js" | 1 + ...\347\272\277\347\250\213.html-DsVzFk6c.js" | 54 + ...\347\233\221\346\216\247.html-BNdB--VB.js" | 1 + ...\347\210\254\350\231\253.html-D6Y4vXXb.js" | 179 + ...\351\231\220\346\265\201.html-B1ZIsZdu.js" | 49 + ...\345\237\272\347\241\200.html-Bsofr9G8.js" | 1 + ...\347\274\226\350\257\221.html-BNnmGgbc.js" | 8 + ...\346\224\266\345\231\250.html-Br8UVQp8.js" | 5 + ...\346\212\223\345\217\226.html-CPwBtyqR.js" | 108 + ...\346\225\264\347\220\206.html-BKoUISo4.js" | 1 + ...\347\256\227\346\263\225.html-BVqGPH4F.js" | 1 + ...\346\246\202\350\277\260.html-CyDH_f4g.js" | 1 + ...\345\205\245\351\227\250.html-DF2q_M-N.js" | 106 + ...\347\274\223\345\255\230.html-DNuD8_hU.js" | 1 + ...\226\346\216\222LiteFlow.html-CvCVGQll.js" | 124 + ...\347\224\265\345\225\206.html-CwtUep3k.js" | 1 + ...\345\220\221\351\224\201.html-DwU2qcUM.js" | 1 + ...\346\200\273\347\273\223.html-Zcj8usmP.js" | 1 + ...\346\200\235\350\267\257.html-DzuQiit4.js" | 1 + ...\345\212\233\346\226\257.html-BrY99iGU.js" | 1 + ...\345\217\257\347\224\250.html-DjOPo7tP.js" | 1 + ...\345\271\266\345\217\221.html-B4hi1h21.js" | 1 + ...\346\200\247\350\203\275.html-CN7rmRWu.js" | 1 + ...\346\200\235\350\200\203.html-DLPHuyVJ.js" | 1 + atom.xml | 7493 +++++++++++++++++ atom.xsl | 534 ++ bdunion.txt | 1 + bigdata/index.html | 47 + bigdata/spark/elastic-spark.html | 73 + bigdata/spark/index.html | 47 + category/index.html | 47 + cloudnative/index.html | 47 + ...csearch\346\272\220\347\240\201debug.html" | 47 + database/elasticsearch/index.html | 47 + ...\347\250\213\350\257\246\350\247\243.html" | 321 + .../\345\237\272\347\241\200.html" | 47 + database/index.html | 47 + database/mysql/1mysql.html | 47 + database/mysql/index.html | 47 + ...\345\272\223\347\274\223\345\255\230.html" | 47 + ...\346\204\217\345\220\221\351\224\201.html" | 47 + ...\347\240\201\345\210\206\346\236\220.html" | 245 + database/redis/index.html | 47 + .../redis/redis\347\274\223\345\255\230.html" | 133 + ...\345\223\210\345\270\214\346\247\275.html" | 47 + donate/index.html | 47 + favicon.ico | Bin 0 -> 21722 bytes feed.json | 1158 +++ index.html | 59 + interesting/chatgpt.html | 147 + .../idea\350\256\276\347\275\256.html" | 63 + interesting/index.html | 47 + .../mini\344\270\273\346\234\272/index.html" | 47 + ...\346\216\245\344\270\273\346\234\272.html" | 87 + ...\270\252mini\344\270\273\346\234\272.html" | 102 + interesting/starcraft-ai.html | 47 + interesting/tesla.html | 69 + ...\347\232\204\345\221\275\344\273\244.html" | 47 + ...\344\270\216\346\236\266\346\236\204.html" | 47 + ...\347\273\210\346\224\271\347\211\210.html" | 118 + ...\347\232\204\344\275\277\347\224\250.html" | 272 + ...\346\227\266\344\273\273\345\212\241.html" | 107 + ...\345\277\227\347\263\273\347\273\237.html" | 252 + ...\347\276\244\351\203\250\347\275\262.html" | 142 + ...\345\272\223\345\244\207\344\273\275.html" | 107 + ...\347\232\204\346\217\222\344\273\266.html" | 59 + ...\346\204\237\345\210\206\346\236\220.html" | 47 + ...\350\203\275\344\274\230\345\214\226.html" | 47 + .../index.html" | 47 + ...\345\212\241\347\233\221\346\216\247.html" | 47 + ...\345\217\215\347\274\226\350\257\221.html" | 54 + ...\351\230\205\346\212\223\345\217\226.html" | 154 + ...\345\205\267\346\225\264\347\220\206.html" | 47 + ...\344\271\213\347\224\265\345\225\206.html" | 47 + interview/index.html | 47 + interview/tiktok2023.html | 47 + ...\344\274\230\345\217\202\346\225\260.html" | 70 + ...\345\255\230\346\250\241\345\236\213.html" | 196 + java/JVM/cms.html | 47 + java/JVM/g1.html | 47 + java/JVM/index.html | 47 + ...350\231\232\346\213\237\346\234\272).html" | 53 + java/JVM/zgc.html | 50 + ...\344\274\230\350\277\207\347\250\213.html" | 50 + ...\345\233\236\346\224\266\345\231\250.html" | 51 + ...\350\276\276\347\256\227\346\263\225.html" | 47 + ...\344\274\230\346\200\235\350\267\257.html" | 47 + ...t Prometheus\344\275\277\347\224\250.html" | 95 + java/SpringBoot/aop.html | 97 + java/SpringBoot/index.html | 47 + java/SpringBoot/webflux.html | 47 + java/index.html | 47 + java/io/index.html | 47 + java/io/nio.html | 47 + java/serverlog.html | 48 + ...\346\213\237\347\272\277\347\250\213.html" | 100 + ...\345\274\217\351\231\220\346\265\201.html" | 95 + ...\213\347\274\226\346\216\222LiteFlow.html" | 170 + "java/\347\272\277\347\250\213/index.html" | 47 + .../synchronized.html" | 105 + "java/\347\272\277\347\250\213/volatile.html" | 47 + ...\343\200\201HTTPS\343\200\201HTTP2.0.html" | 47 + "java/\347\275\221\347\273\234/index.html" | 47 + ...\351\253\230\345\217\257\347\224\250.html" | 47 + ...\351\253\230\345\271\266\345\217\221.html" | 47 + ...\351\253\230\346\200\247\350\203\275.html" | 47 + ...\344\272\233\346\200\235\350\200\203.html" | 47 + ...\344\272\233\347\254\224\350\256\260.html" | 62 + ...\345\277\227\346\224\266\351\233\206.html" | 207 + kubernetes/devops/devops-ping-tai.html | 283 + kubernetes/devops/index.html | 47 + kubernetes/index.html | 47 + kubernetes/request_limit.html | 53 + kubernetes/spark on k8s operator.html | 113 + life/2017.html | 47 + life/2018.html | 47 + life/2019.html | 47 + life/index.html | 47 + link/index.html | 47 + link/main.html | 47 + logo.png | Bin 0 -> 21722 bytes logo.svg | 101 + middleware/canal/index.html | 47 + ...200\224canal\345\260\217\350\256\260.html" | 50 + middleware/index.html | 47 + middleware/kafka/index.html | 47 + middleware/kafka/kafka.html | 47 + middleware/sentinel/index.html | 47 + middleware/sentinel/springcloud_sentinel.html | 47 + ...\351\227\250\344\275\277\347\224\250.html" | 47 + middleware/zookeeper/index.html | 47 + middleware/zookeeper/zookeeper.html | 47 + ...\345\210\227\347\210\254\350\231\253.html" | 225 + open-source-project/index.html | 47 + robots.txt | 5 + rss.xml | 7048 ++++++++++++++++ rss.xsl | 506 ++ sitemap.xml | 3 + sitemap.xsl | 207 + slide/index.html | 47 + star/index.html | 47 + stock/index.html | 47 + ...\350\265\233\345\212\233\346\226\257.html" | 47 + system-design/feed.html | 47 + system-design/index.html | 47 + .../index.html" | 47 + ...347\250\213-\346\246\202\350\277\260.html" | 47 + ...\347\273\237\345\205\245\351\227\250.html" | 152 + .../index.html" | 47 + ...\351\227\250\346\200\273\347\273\223.html" | 47 + tag/index.html | 47 + timeline/index.html | 47 + 277 files changed, 29538 insertions(+) create mode 100644 404.html create mode 100644 CNAME create mode 100644 about-the-author/index.html create mode 100644 about-the-author/personal-life/2024-07-24.html create mode 100644 "about-the-author/personal-life/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html" create mode 100644 about-the-author/personal-life/index.html create mode 100644 about-the-author/personal-life/wewe.html create mode 100644 about-the-author/works/index.html create mode 100644 "about-the-author/works/\344\270\252\344\272\272\344\275\234\345\223\201.html" create mode 100644 ads.txt create mode 100644 article/index.html create mode 100644 "assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-B9tl8qmz.js" create mode 100644 "assets/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html-BVYNR54w.js" create mode 100644 assets/1mysql.html-CW43wjS_.js create mode 100644 "assets/2.Lucene\347\232\204\344\275\277\347\224\250.html-9FjHGi9W.js" create mode 100644 assets/2017.html-DzgcI9cv.js create mode 100644 assets/2018.html-UHfvoYE6.js create mode 100644 assets/2019.html-Df2iUZPh.js create mode 100644 assets/2024-07-24.html-9GUkpnJj.js create mode 100644 "assets/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html-CU80Bk9D.js" create mode 100644 "assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-NSgzVWXM.js" create mode 100644 "assets/4.\346\227\245\345\277\227\347\263\273\347\273\237.html-BH3nB-v9.js" create mode 100644 assets/404.html-BMHKR2ag.js create mode 100644 "assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-BN5IQG4o.js" create mode 100644 "assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-Ca5VIWc1.js" create mode 100644 "assets/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html-Br7NfWE4.js" create mode 100644 "assets/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html-DopYDuB6.js" create mode 100644 "assets/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html-8ms_Vznn.js" create mode 100644 "assets/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html-DvomVRAD.js" create mode 100644 "assets/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html-BtZgPFK4.js" create mode 100644 "assets/Java\345\206\205\345\255\230\346\250\241\345\236\213.html-Bis3Hglq.js" create mode 100644 "assets/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html--8cnUWGp.js" create mode 100644 "assets/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html-SN4rN7dm.js" create mode 100644 "assets/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html-DBlO1MVO.js" create mode 100644 "assets/Spring Boot Prometheus\344\275\277\347\224\250.html-CvPEGSZS.js" create mode 100644 assets/aop.html-C7ud1qex.js create mode 100644 assets/app-ftEjETWs.js create mode 100644 assets/chatgpt.html-BgeRWiYw.js create mode 100644 assets/cms.html-Dfw5I__q.js create mode 100644 assets/devops-ping-tai.html-CjTXGrBr.js create mode 100644 assets/elastic-spark.html-Cz2jVT52.js create mode 100644 "assets/elasticsearch\346\272\220\347\240\201debug.html-Bo6HcrbI.js" create mode 100644 assets/feed.html-BcapapFL.js create mode 100644 assets/g1.html-CcgAHdWl.js create mode 100644 assets/giscus-BZxmVUME.js create mode 100644 assets/icon/apple-icon-152.png create mode 100644 assets/icon/chrome-192.png create mode 100644 assets/icon/chrome-512.png create mode 100644 assets/icon/chrome-mask-192.png create mode 100644 assets/icon/chrome-mask-512.png create mode 100644 assets/icon/guide-maskable.png create mode 100644 assets/icon/guide-monochrome.png create mode 100644 assets/icon/ms-icon-144.png create mode 100644 "assets/idea\350\256\276\347\275\256.html-BThIkzaw.js" create mode 100644 assets/index-AN989yVn.js create mode 100644 assets/index.html-0EEVTum5.js create mode 100644 assets/index.html-5ZTxzR_p.js create mode 100644 assets/index.html-7tsgb-dC.js create mode 100644 assets/index.html-B1Akg086.js create mode 100644 assets/index.html-B2fh3Mku.js create mode 100644 assets/index.html-B8Qu0Pmr.js create mode 100644 assets/index.html-B9smc87Y.js create mode 100644 assets/index.html-BAbeWXfD.js create mode 100644 assets/index.html-BEv74XKv.js create mode 100644 assets/index.html-BKAyyte1.js create mode 100644 assets/index.html-BTDM2Wpl.js create mode 100644 assets/index.html-BXiz75su.js create mode 100644 assets/index.html-BcZZ4qCt.js create mode 100644 assets/index.html-BwVnwmVL.js create mode 100644 assets/index.html-By-pQ7Wl.js create mode 100644 assets/index.html-C9P5ouBn.js create mode 100644 assets/index.html-CD6aFyf5.js create mode 100644 assets/index.html-CMe--mJk.js create mode 100644 assets/index.html-CY6pLx2d.js create mode 100644 assets/index.html-CdxL9VtY.js create mode 100644 assets/index.html-CpCz4LWW.js create mode 100644 assets/index.html-D1R9zQcp.js create mode 100644 assets/index.html-D4WG7yPk.js create mode 100644 assets/index.html-DGy09Ap4.js create mode 100644 assets/index.html-DJHdkCFZ.js create mode 100644 assets/index.html-DJZmVM8a.js create mode 100644 assets/index.html-DQFfT5fQ.js create mode 100644 assets/index.html-DRyhNgG6.js create mode 100644 assets/index.html-DWXiwN5o.js create mode 100644 assets/index.html-DYmvtxA9.js create mode 100644 assets/index.html-DaAoZU-K.js create mode 100644 assets/index.html-DlwW5Vje.js create mode 100644 assets/index.html-DqVhfNm4.js create mode 100644 assets/index.html-DtDo2qGn.js create mode 100644 assets/index.html-KwQlr7R4.js create mode 100644 assets/index.html-SdCcjUz-.js create mode 100644 assets/index.html-TDa4J3OS.js create mode 100644 assets/index.html-Uqis1PLJ.js create mode 100644 assets/index.html-jGjDZXTI.js create mode 100644 assets/index.html-muK9iEFv.js create mode 100644 assets/index.html-nZcQpO1z.js create mode 100644 assets/index.html-wU0YCmBl.js create mode 100644 "assets/jvm(java\350\231\232\346\213\237\346\234\272).html-CpFuSPpy.js" create mode 100644 assets/kafka.html-tMeeu2BM.js create mode 100644 "assets/mac\350\277\236\346\216\245\344\270\273\346\234\272.html-BbWWw_2k.js" create mode 100644 assets/main.html-B2OQipck.js create mode 100644 assets/nio.html-DVcQhX_V.js create mode 100644 assets/photoswipe.esm-GXRgw7eJ.js create mode 100644 "assets/redis\347\274\223\345\255\230.html-H1xCPktw.js" create mode 100644 assets/request_limit.html-CBNYeI2L.js create mode 100644 assets/serverlog.html-bICZU0Nm.js create mode 100644 assets/spark on k8s operator.html-D9Tpznzk.js create mode 100644 assets/springcloud_sentinel.html-CFCKUB71.js create mode 100644 assets/starcraft-ai.html-C018rwGF.js create mode 100644 assets/style-BjEFg8CQ.css create mode 100644 assets/synchronized.html-B2MTfSju.js create mode 100644 assets/tesla.html-K0riacrL.js create mode 100644 assets/tiktok2023.html-BxK5gJWF.js create mode 100644 assets/volatile.html-C-UWQAjc.js create mode 100644 assets/webflux.html-KjGFiVkN.js create mode 100644 assets/wewe.html-BLY5_jfL.js create mode 100644 assets/zgc.html-Cj33F57G.js create mode 100644 assets/zookeeper.html-Dd8HToeq.js create mode 100644 "assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-Dg8HV8OO.js" create mode 100644 "assets/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html-BtEABYaE.js" create mode 100644 "assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-zaM0dK5_.js" create mode 100644 "assets/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html-a1P2YWrj.js" create mode 100644 "assets/\344\270\252\344\272\272\344\275\234\345\223\201.html-DDpX9I3v.js" create mode 100644 "assets/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html-Bb5S9Lg5.js" create mode 100644 "assets/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html-DMCiuYMs.js" create mode 100644 "assets/\345\205\245\351\227\250\344\275\277\347\224\250.html-DfOcEEjE.js" create mode 100644 "assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-DsVzFk6c.js" create mode 100644 "assets/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html-BNdB--VB.js" create mode 100644 "assets/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html-D6Y4vXXb.js" create mode 100644 "assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-B1ZIsZdu.js" create mode 100644 "assets/\345\237\272\347\241\200.html-Bsofr9G8.js" create mode 100644 "assets/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html-BNnmGgbc.js" create mode 100644 "assets/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html-Br8UVQp8.js" create mode 100644 "assets/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html-CPwBtyqR.js" create mode 100644 "assets/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html-BKoUISo4.js" create mode 100644 "assets/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html-BVqGPH4F.js" create mode 100644 "assets/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html-CyDH_f4g.js" create mode 100644 "assets/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html-DF2q_M-N.js" create mode 100644 "assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-DNuD8_hU.js" create mode 100644 "assets/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html-CvCVGQll.js" create mode 100644 "assets/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html-CwtUep3k.js" create mode 100644 "assets/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html-DwU2qcUM.js" create mode 100644 "assets/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html-Zcj8usmP.js" create mode 100644 "assets/\350\260\203\344\274\230\346\200\235\350\267\257.html-DzuQiit4.js" create mode 100644 "assets/\350\265\233\345\212\233\346\226\257.html-BrY99iGU.js" create mode 100644 "assets/\351\253\230\345\217\257\347\224\250.html-DjOPo7tP.js" create mode 100644 "assets/\351\253\230\345\271\266\345\217\221.html-B4hi1h21.js" create mode 100644 "assets/\351\253\230\346\200\247\350\203\275.html-CN7rmRWu.js" create mode 100644 "assets/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html-DLPHuyVJ.js" create mode 100644 atom.xml create mode 100644 atom.xsl create mode 100644 bdunion.txt create mode 100644 bigdata/index.html create mode 100644 bigdata/spark/elastic-spark.html create mode 100644 bigdata/spark/index.html create mode 100644 category/index.html create mode 100644 cloudnative/index.html create mode 100644 "database/elasticsearch/elasticsearch\346\272\220\347\240\201debug.html" create mode 100644 database/elasticsearch/index.html create mode 100644 "database/elasticsearch/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html" create mode 100644 "database/elasticsearch/\345\237\272\347\241\200.html" create mode 100644 database/index.html create mode 100644 database/mysql/1mysql.html create mode 100644 database/mysql/index.html create mode 100644 "database/mysql/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html" create mode 100644 "database/mysql/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html" create mode 100644 "database/redis/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html" create mode 100644 database/redis/index.html create mode 100644 "database/redis/redis\347\274\223\345\255\230.html" create mode 100644 "database/redis/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html" create mode 100644 donate/index.html create mode 100644 favicon.ico create mode 100644 feed.json create mode 100644 index.html create mode 100644 interesting/chatgpt.html create mode 100644 "interesting/idea\350\256\276\347\275\256.html" create mode 100644 interesting/index.html create mode 100644 "interesting/mini\344\270\273\346\234\272/index.html" create mode 100644 "interesting/mini\344\270\273\346\234\272/mac\350\277\236\346\216\245\344\270\273\346\234\272.html" create mode 100644 "interesting/mini\344\270\273\346\234\272/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html" create mode 100644 interesting/starcraft-ai.html create mode 100644 interesting/tesla.html create mode 100644 "interesting/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/2.Lucene\347\232\204\344\275\277\347\224\250.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/3.\345\256\232\346\227\266\344\273\273\345\212\241.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/4.\346\227\245\345\277\227\347\263\273\347\273\237.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html" create mode 100644 "interesting/\344\270\252\344\272\272\347\275\221\347\253\231/index.html" create mode 100644 "interesting/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html" create mode 100644 "interesting/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html" create mode 100644 "interesting/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html" create mode 100644 "interesting/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html" create mode 100644 "interesting/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html" create mode 100644 interview/index.html create mode 100644 interview/tiktok2023.html create mode 100644 "java/JVM/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html" create mode 100644 "java/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213.html" create mode 100644 java/JVM/cms.html create mode 100644 java/JVM/g1.html create mode 100644 java/JVM/index.html create mode 100644 "java/JVM/jvm(java\350\231\232\346\213\237\346\234\272).html" create mode 100644 java/JVM/zgc.html create mode 100644 "java/JVM/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html" create mode 100644 "java/JVM/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html" create mode 100644 "java/JVM/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html" create mode 100644 "java/JVM/\350\260\203\344\274\230\346\200\235\350\267\257.html" create mode 100644 "java/SpringBoot/Spring Boot Prometheus\344\275\277\347\224\250.html" create mode 100644 java/SpringBoot/aop.html create mode 100644 java/SpringBoot/index.html create mode 100644 java/SpringBoot/webflux.html create mode 100644 java/index.html create mode 100644 java/io/index.html create mode 100644 java/io/nio.html create mode 100644 java/serverlog.html create mode 100644 "java/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html" create mode 100644 "java/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html" create mode 100644 "java/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html" create mode 100644 "java/\347\272\277\347\250\213/index.html" create mode 100644 "java/\347\272\277\347\250\213/synchronized.html" create mode 100644 "java/\347\272\277\347\250\213/volatile.html" create mode 100644 "java/\347\275\221\347\273\234/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html" create mode 100644 "java/\347\275\221\347\273\234/index.html" create mode 100644 "java/\351\253\230\345\217\257\347\224\250.html" create mode 100644 "java/\351\253\230\345\271\266\345\217\221.html" create mode 100644 "java/\351\253\230\346\200\247\350\203\275.html" create mode 100644 "java/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html" create mode 100644 "kubernetes/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html" create mode 100644 "kubernetes/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html" create mode 100644 kubernetes/devops/devops-ping-tai.html create mode 100644 kubernetes/devops/index.html create mode 100644 kubernetes/index.html create mode 100644 kubernetes/request_limit.html create mode 100644 kubernetes/spark on k8s operator.html create mode 100644 life/2017.html create mode 100644 life/2018.html create mode 100644 life/2019.html create mode 100644 life/index.html create mode 100644 link/index.html create mode 100644 link/main.html create mode 100644 logo.png create mode 100644 logo.svg create mode 100644 middleware/canal/index.html create mode 100644 "middleware/canal/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html" create mode 100644 middleware/index.html create mode 100644 middleware/kafka/index.html create mode 100644 middleware/kafka/kafka.html create mode 100644 middleware/sentinel/index.html create mode 100644 middleware/sentinel/springcloud_sentinel.html create mode 100644 "middleware/sentinel/\345\205\245\351\227\250\344\275\277\347\224\250.html" create mode 100644 middleware/zookeeper/index.html create mode 100644 middleware/zookeeper/zookeeper.html create mode 100644 "middleware/zookeeper/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html" create mode 100644 open-source-project/index.html create mode 100644 robots.txt create mode 100644 rss.xml create mode 100644 rss.xsl create mode 100644 sitemap.xml create mode 100644 sitemap.xsl create mode 100644 slide/index.html create mode 100644 star/index.html create mode 100644 stock/index.html create mode 100644 "stock/\350\265\233\345\212\233\346\226\257.html" create mode 100644 system-design/feed.html create mode 100644 system-design/index.html create mode 100644 "system-design/\346\216\250\350\215\220\347\263\273\347\273\237/index.html" create mode 100644 "system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html" create mode 100644 "system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html" create mode 100644 "system-design/\350\256\241\347\256\227\345\271\277\345\221\212/index.html" create mode 100644 "system-design/\350\256\241\347\256\227\345\271\277\345\221\212/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html" create mode 100644 tag/index.html create mode 100644 timeline/index.html diff --git a/404.html b/404.html new file mode 100644 index 00000000..e2fcb701 --- /dev/null +++ b/404.html @@ -0,0 +1,47 @@ + + + + + + + + + + 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..2f77233b --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +wenzhihuai.com \ No newline at end of file diff --git a/about-the-author/index.html b/about-the-author/index.html new file mode 100644 index 00000000..dd7aa150 --- /dev/null +++ b/about-the-author/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 关于我 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/about-the-author/personal-life/2024-07-24.html b/about-the-author/personal-life/2024-07-24.html new file mode 100644 index 00000000..df3e682e --- /dev/null +++ b/about-the-author/personal-life/2024-07-24.html @@ -0,0 +1,47 @@ + + + + + + + + + + 2024-07-24 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/about-the-author/personal-life/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html" "b/about-the-author/personal-life/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html" new file mode 100644 index 00000000..f30f932f --- /dev/null +++ "b/about-the-author/personal-life/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 2024-11-09上海迪斯尼 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/about-the-author/personal-life/index.html b/about-the-author/personal-life/index.html new file mode 100644 index 00000000..4f2d11f4 --- /dev/null +++ b/about-the-author/personal-life/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Personal Life | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/about-the-author/personal-life/wewe.html b/about-the-author/personal-life/wewe.html new file mode 100644 index 00000000..f88427a3 --- /dev/null +++ b/about-the-author/personal-life/wewe.html @@ -0,0 +1,47 @@ + + + + + + + + + + 自我介绍 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/about-the-author/works/index.html b/about-the-author/works/index.html new file mode 100644 index 00000000..86dfd915 --- /dev/null +++ b/about-the-author/works/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Works | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/about-the-author/works/\344\270\252\344\272\272\344\275\234\345\223\201.html" "b/about-the-author/works/\344\270\252\344\272\272\344\275\234\345\223\201.html" new file mode 100644 index 00000000..5a2ccee9 --- /dev/null +++ "b/about-the-author/works/\344\270\252\344\272\272\344\275\234\345\223\201.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 小程序 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/ads.txt b/ads.txt new file mode 100644 index 00000000..be5a5540 --- /dev/null +++ b/ads.txt @@ -0,0 +1 @@ +google.com, pub-9037099208128116, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/article/index.html b/article/index.html new file mode 100644 index 00000000..e3902b25 --- /dev/null +++ b/article/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + 文章 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-B9tl8qmz.js" "b/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-B9tl8qmz.js" new file mode 100644 index 00000000..e44a73f9 --- /dev/null +++ "b/assets/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html-B9tl8qmz.js" @@ -0,0 +1 @@ +import{_ as t,c as r,d as a,o as i}from"./app-ftEjETWs.js";const n={};function l(o,e){return i(),r("div",null,e[0]||(e[0]=[a('

1.历史与架构

各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点有点意思这个页面。
本文从下面这几个方面来讲讲网站的建立:

  1. 建站故事与网站架构
  2. lucene搜索的使用
  3. 使用quartz来定时备份数据库
  4. 使用百度统计api做日志系统
  5. Nginx小集群的搭建
  6. [数据库]
  7. 使用机器学习对微博进行分析
  8. 网站性能优化
  9. SEO优化

1.建站故事与网站架构

1.1建站过程

起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是杨青的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。

第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。

最终版本在考虑时,也找了很多模板,影响深刻的是tale欲思这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。
页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。
已经部署在腾讯云服务器上,存储使用了七牛云的cdn。

1.2 网站整体技术架构

最终版的技术架构图如下:

网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了张开涛的博客。

运行流程分析

  1. 浏览器发送http请求。/blogdetail.html?blogid=1。
  2. tomcat容器初始化,顺序为context-param>listener>filter>servlet,此时,spring中的bean还没有被注入的,不建议在此处加载bean,网站声明了两个类(IPFilter和CacheControlFilter),IPFilter用来拦截IP,CacheControlFilter用来缓存。
  3. 初始化Spring。
  4. DispatcherServlet——>HandlerMapping进行请求到处理的映射,HandlerMapping将“/blogdetail”路径直接映射到名字为“/blogdetail”的Bean进行处理,即BlogController。
  5. 自定义拦截器,其中BaseIntercepter实现了HandleInterceptor的接口,用来记录每次访问的链接以及后台响应的时间。
  6. DispatcherServlet——> SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter将HandlerExecutionChain中的处理器适配为BlogController。
  7. BlogController执行查询,取得结果集返回数据。
  8. blogdetail(ModelAndView的逻辑视图名)——>InternalResourceViewResolver, InternalResourceViewResolver使用JstlView,具体视图页面在/blogdetail.jsp。
  9. JstlView(/blogdetail.jsp)——>渲染,将在处理器传入的模型数据(blog=Blog!)在视图中展示出来。
  10. 返回响应。

1.3 日志系统

日志系统架构如下:

日志系统曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。
出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。

1.4 【有点意思】自然语言处理

本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击这里尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。

总结

断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:GitHub上求给个Star,以后面试能讲讲这个网站。

',27)]))}const s=t(n,[["render",l],["__file","1.历史与架构.html.vue"]]),p=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html","title":"1.历史与架构","lang":"zh-CN","frontmatter":{"description":"1.历史与架构 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。 由原本的ssh变成ssm,再变成ssm+shi...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"1.历史与架构"}],["meta",{"property":"og:description","content":"1.历史与架构 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。 由原本的ssh变成ssm,再变成ssm+shi..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/600-20240126113210252.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"1.历史与架构\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/600-20240126113210252.png\\",\\"https://github-images.wenzhihuai.com/images/500.png\\",\\"https://github-images.wenzhihuai.com/images/awfawefwefwef.png\\",\\"https://github-images.wenzhihuai.com/images/awefaweagregrgbwerbwer.png\\",\\"https://github-images.wenzhihuai.com/images/awfawefwefawefwef.png\\",\\"https://github-images.wenzhihuai.com/images/QQ%E6%88%AA%E5%9B%BE20170825141127.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1.建站故事与网站架构","slug":"_1-建站故事与网站架构","link":"#_1-建站故事与网站架构","children":[{"level":3,"title":"1.1建站过程","slug":"_1-1建站过程","link":"#_1-1建站过程","children":[]},{"level":3,"title":"1.2 网站整体技术架构","slug":"_1-2-网站整体技术架构","link":"#_1-2-网站整体技术架构","children":[]},{"level":3,"title":"1.3 日志系统","slug":"_1-3-日志系统","link":"#_1-3-日志系统","children":[]},{"level":3,"title":"1.4 【有点意思】自然语言处理","slug":"_1-4-【有点意思】自然语言处理","link":"#_1-4-【有点意思】自然语言处理","children":[]}]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":6.79,"words":2038},"filePathRelative":"interesting/个人网站/1.历史与架构.md","localizedDate":"2020年1月21日","excerpt":"\\n

各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
\\n大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

","autoDesc":true}');export{s as comp,p as data}; diff --git "a/assets/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html-BVYNR54w.js" "b/assets/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html-BVYNR54w.js" new file mode 100644 index 00000000..01e28e78 --- /dev/null +++ "b/assets/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html-BVYNR54w.js" @@ -0,0 +1,72 @@ +import{_ as i,c as a,d as e,o as n}from"./app-ftEjETWs.js";const t={};function l(h,s){return n(),a("div",null,s[0]||(s[0]=[e(`

10.历时8年最终改版

不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

早年的网站截图
早年的网站截图

总体而言,自建站对学习知识,了解整个建站的原理能够起到非常重要的作用,但是维护成本实在是太高了,每个月要支付服务器的费用,而且一旦想拿服务器来做点什么,都得提防一下会不会造成破坏。最终还是选择采用vuepress2来重构一下自建站,毕竟把markdown放到github,把图片放到cos里减少了不少的维护量。下面是使用vuepress2建站的代码地址

一、博客的安装

具体vuepress2官网讲解的很详细了,不用再处理什么,按照步骤创建一个项目即可,为了网站的美观,个人使用了theme hope这款主题。

二、配置

导航栏、侧边栏官网也有详细的讲解,也不再阐述,需要注意的是自动目录,之前看JavaGuide的样式,他那边的每篇文章都需要写一次ts文件(children),后来发现官网可以把children设置为structure,即可实现根据md文件生成侧边栏目录。注意的是,这里不是根据markdown的文件名来目录名,而是取markdown文件的标题。

    {
+        text: "Redis",
+        prefix: "redis/",
+        icon: "redis",
+        collapsible: false,
+        children: "structure"
+    },

三、为文章增加评论

vuepress-plugin-comment2,使用了Giscus,Giscus绑定了github账号,所以可以从一定程度上防止被别人刷广告,需要再个人的项目Settings->General把Discussions这个选项给勾选上。

一定要开启discussions
一定要开启discussions

然后去config.ts配置插件。

        commentPlugin({
+            provider: "Giscus",
+            comment: true, //启用评论功能
+            repo: "Zephery/MyWebsite", //远程仓库
+            repoId: "MDEwOlJlcG9zaXRvcnkyMDM2MDIyMDQ=", //对应自己的仓库Id
+            category: "General",
+            categoryId: "DIC_kwDODCK5HM4Ccp32" //对应自己的分类Id
+        }),

即可在页面上看到效果

颜色可自定义
颜色可自定义

四、博客的部署

官网也有讲解部署的情况,具体可以看官网Github Pages,整体上看速度还是挺慢的,可以尝试去gitee上部署看一下,之后就可以在pages通过域名访问了。需要在项目下创建.github/workflows/docs.yml文件,具体配置参考官网,不需做任何改动。

五、Github pages自定义域名

github自带的io域名zephery.github.io,做为一名开发,肯定是用自己的域名是比较好的。需要注意下中间的红色框,前面的是分支,后面的是你项目的路径。一般默认即可,不用修改。

pages的配置
pages的配置

购买域名->域名解析,即把我的个人域名wenzhihuai.com指向zephery.github.io(通过cname)即可,然后开启强制https。如果DNS一直没有校验通过,那么可能是CAA的原因。通过DNS诊断工具来判断。

DNS诊断工具
DNS诊断工具

上面的custom domain配置好了之后,但DNS一直没有校验正确,原因是CAA没有正确解析,需要加上即可。

0 issue "trust-provider.com"
+0 issuewild "trust-provider.com"
CAA记录解析
CAA记录解析

之后就可以看到Github Pages的DNS校验成功,并且可以强制开启https了。

六、Typora图床

之前的图床使用的是七牛云和又拍云,都有免费的额度吧,不过看情况未来前景似乎经营不太好,目前改用了腾讯云。存储容量50GB,每个月外网访问流量10GB,满足个人网站使用。具体的配置过程比较简单,就不再阐述了,可以直接看uPic的官方介绍。

七、为自己的内容增加收入

有钱才有写作的动力,之前的网站开启了几年的捐赠,总共都没有收到过50块钱,只能从广告这一处想想办法,百度、腾讯广告似乎都不支持个人网站,谷歌可以。配置谷歌广告,网上的教程不少,例如: vuepress配置谷歌广告-通过vue-google-adsense库,缺点是,大部分的文章都是需要在自己的markdown文件中新增特定的标识符。比如:

# js 模板引擎 mustache 用法
+
+<ArticleTopAd></ArticleTopAd>
+
+## 一. 使用步骤

每一篇文章都要新增显然不符合懒惰的人,下面是个人尝试的解决办法,用的是vuepress2提供的slots插槽。

谷歌广告adsense
谷歌广告adsense

上面的目的是为了获取data-ad-client和data-ad-slot,其中,data-ad-slot为广告单元,不一样。并且,配置完之后可能需要等一个小时才会生效,不要着急。

docs/.vuepress/config.ts

head: [
+    [
+        "script",
+        {
+            "data-ad-client": "ca-pub-9037099208128116",
+            async: true,
+            src: "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
+        }
+    ],
+]
+
+...
+alias: {
+    "@theme-hope/components/NormalPage": path.resolve(
+        __dirname,
+        "./components/NormalPage.vue",
+    ),
+},

docs/.vuepress/components/NormalPage.vue

<template>
+  <normal-page>
+    <template #contentBefore>
+      <ins class="adsbygoogle"
+           style="display:block; text-align:center;width: 90%;margin: 0 auto;"
+           data-ad-layout="in-article"
+           data-ad-format="fluid"
+           data-ad-client="ca-pub-9037099208128116"
+           data-ad-slot="8206550629"></ins>
+    </template>
+  </normal-page>
+</template>
+<script>
+import NormalPage from "vuepress-theme-hope/components/NormalPage.js";
+
+export default {
+  name: "adsense-inline",
+  components: {
+    'normal-page': NormalPage,
+  },
+  mounted() {
+    this.adsenseAddLoad();
+  },
+  methods: {
+    adsenseAddLoad() {
+      let inlineScript = document.createElement("script");
+      inlineScript.type = "text/javascript";
+      inlineScript.text = '(adsbygoogle = window.adsbygoogle || []).push({});'
+      document.getElementsByTagName('body')[0].appendChild(inlineScript);
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+</style>

本地是没办法进行调试的,可以从官网插槽演示的文章中用div进行调试,等修改完毕发布之后,即可在自己的网站上看到相关的广告和收入(浏览器要把封禁广告的插件关闭)。

出现的广告
出现的广告
谷歌广告收入
谷歌广告收入

收入虽然低,但是基本上个人没有成本,只需要域名的85块钱。

常见问题

为什么广告不能正常显示?

"建议多写原创高质量的文章出来,AdSense才会匹配出合适的广告,用户感兴趣了才会浏览量增加,你才会有更多的广告收入。"

还是得多写一写优质的文章。

最后,多帮忙点一下个人网站的广告吧,感恩

网站地址:https://www.wenzhihuai.com

源码地址:https://github.com/Zephery/MyWebsite

`,50)]))}const r=i(t,[["render",l],["__file","10.历时8年最终改版.html.vue"]]),k=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html","title":"10.历时8年最终改版","lang":"zh-CN","frontmatter":{"description":"10.历时8年最终改版 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"10.历时8年最终改版"}],["meta",{"property":"og:description","content":"10.历时8年最终改版 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/600-20240126113210252-20240131213935629.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"10.历时8年最终改版\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/600-20240126113210252-20240131213935629.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201153605099.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240201205909384.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201144459067.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201151253084.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201151243938.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201150802070.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201151218077.png\\",\\"https://github-images.wenzhihuai.com/github/image-20240201151334064.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、博客的安装","slug":"一、博客的安装","link":"#一、博客的安装","children":[]},{"level":2,"title":"二、配置","slug":"二、配置","link":"#二、配置","children":[]},{"level":2,"title":"三、为文章增加评论","slug":"三、为文章增加评论","link":"#三、为文章增加评论","children":[]},{"level":2,"title":"四、博客的部署","slug":"四、博客的部署","link":"#四、博客的部署","children":[]},{"level":2,"title":"五、Github pages自定义域名","slug":"五、github-pages自定义域名","link":"#五、github-pages自定义域名","children":[]},{"level":2,"title":"六、Typora图床","slug":"六、typora图床","link":"#六、typora图床","children":[]},{"level":2,"title":"七、为自己的内容增加收入","slug":"七、为自己的内容增加收入","link":"#七、为自己的内容增加收入","children":[]},{"level":2,"title":"常见问题","slug":"常见问题","link":"#常见问题","children":[]}],"git":{"createdTime":1706165044000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":5.78,"words":1733},"filePathRelative":"interesting/个人网站/10.历时8年最终改版.md","localizedDate":"2024年1月25日","excerpt":"\\n

不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

","autoDesc":true}');export{r as comp,k as data}; diff --git a/assets/1mysql.html-CW43wjS_.js b/assets/1mysql.html-CW43wjS_.js new file mode 100644 index 00000000..114bea80 --- /dev/null +++ b/assets/1mysql.html-CW43wjS_.js @@ -0,0 +1 @@ +import{_ as i,c as a,a as e,o as m}from"./app-ftEjETWs.js";const n={};function o(s,t){return m(),a("div",null,t[0]||(t[0]=[e("h1",{id:"mysql",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#mysql"},[e("span",null,"Mysql")])],-1),e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/img.png",alt:"img",tabindex:"0",loading:"lazy"}),e("figcaption",null,"img")],-1)]))}const l=i(n,[["render",o],["__file","1mysql.html.vue"]]),c=JSON.parse('{"path":"/database/mysql/1mysql.html","title":"Mysql","lang":"zh-CN","frontmatter":{"description":"Mysql imgimg","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/1mysql.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Mysql"}],["meta",{"property":"og:description","content":"Mysql imgimg"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/img.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:53:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:53:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Mysql\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/img.png\\"],\\"dateModified\\":\\"2024-02-06T07:53:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1679834663000,"updatedTime":1707206019000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":0.02,"words":5},"filePathRelative":"database/mysql/1mysql.md","localizedDate":"2023年3月26日","excerpt":"\\n
\\"img\\"
img
","autoDesc":true}');export{l as comp,c as data}; diff --git "a/assets/2.Lucene\347\232\204\344\275\277\347\224\250.html-9FjHGi9W.js" "b/assets/2.Lucene\347\232\204\344\275\277\347\224\250.html-9FjHGi9W.js" new file mode 100644 index 00000000..7a4cdfc3 --- /dev/null +++ "b/assets/2.Lucene\347\232\204\344\275\277\347\224\250.html-9FjHGi9W.js" @@ -0,0 +1,226 @@ +import{_ as s,c as a,d as n,o as h}from"./app-ftEjETWs.js";const l={};function k(t,i){return h(),a("div",null,i[0]||(i[0]=[n(`

2.Lucene的使用

首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

Lucene的整体架构

image
image

搜索引擎的几个重要概念:

  1. 倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。

  2. 停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。

  3. 排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。

Lucene中的几个概念

  1. 文档(Document):文档是一系列域的组合,文档的域则代表一系列域文档相关的内容。
  2. 域(Field):每个文档可以包含一个或者多个不同名称的域。
  3. 词(Term):Term是搜索的基本单元,与Field相对应,包含了搜索的域的名称和关键词。
  4. 查询(Query):一系列Term的条件组合,成为TermQuery,但也有可能是短语查询等。
  5. 分词器(Analyzer):主要是用来做分词以及去除停用词的处理。

索引的建立

索引的搜索

lucene在本网站的使用:

  1. 搜索 2. 自动分词

一、搜索

注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:

<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-core</artifactId><!--lucene核心-->
+    <version>\${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
+    <version>\${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
+    <version>\${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-queryparser</artifactId><!--格式化-->
+    <version>\${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
+    <version>\${lucene.version}</version>
+</dependency>
  1. 构建索引
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
+SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
+IndexWriterConfig config = new IndexWriterConfig(analyzer);
+IndexWriter writer = new IndexWriter(dir, config);
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
+writer.addDocument(doc);
+writer.close();
  1. 更新与删除
IndexWriter writer = getWriter();
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
+writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc);   //更新索引
+writer.close();
  1. 查询
private static void search_index(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径,也就是索引所在的位置
+        IndexReader reader = DirectoryReader.open(dir);
+        IndexSearcher searcher = new IndexSearcher(reader);
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        QueryParser parser = new QueryParser("content", analyzer); //查询解析器
+        Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象
+        TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,
+        for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+            System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
+        }
+        reader.close();
+    } catch (IOException | ParseException e) {
+        logger.error(e.toString());
+    }
+}
  1. 高亮
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
+SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
+Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
+highlighter.setTextFragmenter(fragmenter);
+for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+    Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+    String title = doc.get("title");
+    TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
+    String hTitle = highlighter.getBestFragment(tokenStream, title);
+    System.out.println(hTitle);
+}

结果

<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
  1. 分页
    目前lucene分页的方式主要有两种:
    (1). 每次都全部查询,然后通过截取获得所需要的记录。由于采用了分词与倒排索引,所有速度是足够快的,但是在数据量过大的时候,占用内存过大,容易造成内存溢出
    (2). 使用searchAfter把数据保存在缓存里面,然后再去取。这种方式对大量的数据友好,但是当数据量比较小的时候,速度会相对慢。
    lucene中使用searchafter来筛选顺序
ScoreDoc lastBottom = null;//相当于pageSize
+BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
+QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
+Query query1 = parser1.parse(q);
+booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
+TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits);  //lastBottom(pageSize),pagehits(pagenum)
  1. 使用效果
    全部代码放在这里,代码写的不太好,光从代码规范上就不咋地。在网页上的使用效果如下:

二、lucene自动补全

百度、谷歌等在输入文字的时候会弹出补全框,如下图:

在搭建lucene自动补全的时候,也有考虑过使用SQL语句中使用like来进行,主要还是like对数据库压力会大,而且相关度没有lucene的高。主要使用了官方suggest库以及autocompelte.js这个插件。
suggest的原理看这,以及索引结构看这

使用:

  1. 导入maven包
<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-suggest</artifactId>
+    <version>6.6.0</version>
+</dependency>
  1. 如果想将结果反序列化,声明实体类的时候要加上:
public class Blog implements Serializable {
  1. 实现InputIterator接口
InputIterator的几个方法:
+long weight():返回的权重值,大小会影响排序,默认是1L
+BytesRef payload():对某个对象进行序列化
+boolean hasPayloads():是否有设置payload信息
+Set<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤
+boolean hasContexts():判断是否有下一个,默认为false
public class BlogIterator implements InputIterator {
+    /**
+     * logger
+     */
+    private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
+    private Iterator<Blog> blogIterator;
+    private Blog currentBlog;
+
+    public BlogIterator(Iterator<Blog> blogIterator) {
+        this.blogIterator = blogIterator;
+    }
+
+    @Override
+    public boolean hasContexts() {
+        return true;
+    }
+
+    @Override
+    public boolean hasPayloads() {
+        return true;
+    }
+
+    public Comparator<BytesRef> getComparator() {
+        return null;
+    }
+
+    @Override
+    public BytesRef next() {
+        if (blogIterator.hasNext()) {
+            currentBlog = blogIterator.next();
+            try {
+                //返回当前Project的name值,把blog类的name属性值作为key
+                return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 将Blog对象序列化存入payload
+     * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法
+     */
+    @Override
+    public BytesRef payload() {
+        try {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream out = new ObjectOutputStream(bos);
+            out.writeObject(currentBlog);
+            out.close();
+            BytesRef bytesRef = new BytesRef(bos.toByteArray());
+            return bytesRef;
+        } catch (IOException e) {
+            logger.error("", e);
+            return null;
+        }
+    }
+
+    /**
+     * 文章标题
+     */
+    @Override
+    public Set<BytesRef> contexts() {
+        try {
+            Set<BytesRef> regions = new HashSet<BytesRef>();
+            regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
+            return regions;
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Couldn't convert to UTF-8");
+        }
+    }
+
+    /**
+     * 返回权重值,这个值会影响排序
+     * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值
+     */
+    @Override
+    public long weight() {
+        return currentBlog.getHits();   //change to hits
+    }
+}
  1. ajax 建立索引
/**
+ * ajax建立索引
+ */
+@Override
+public void ajaxbuild() {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        //创建Blog测试数据
+        List<Blog> blogs = blogMapper.getAllBlog();
+        suggester.build(new BlogIterator(blogs.iterator()));
+    } catch (IOException e) {
+        System.err.println("Error!");
+    }
+}
  1. 查找
    因为有些文章的标题是一样的,先对list排序,将标题短的放前面,长的放后面,然后使用LinkHashSet来存储。
@Override
+public Set<String> ajaxsearch(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        List<String> list = lookup(suggester, keyword);
+        list.sort(new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                if (o1.length() > o2.length()) {
+                    return 1;
+                } else {
+                    return -1;
+                }
+            }
+        });
+        Set<String> set = new LinkedHashSet<>();
+        for (String string : list) {
+            set.add(string);
+        }
+        ssubSet(set, 7);
+        return set;
+    } catch (IOException e) {
+        System.err.println("Error!");
+        return null;
+    }
+}
  1. controller层
@RequestMapping("ajaxsearch")
+public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    String keyword = request.getParameter("keyword");
+    if (StringUtils.isEmpty(keyword)) {
+        return;
+    }
+    Set<String> set = blogService.ajaxsearch(keyword);
+    Gson gson = new Gson();
+    response.getWriter().write(gson.toJson(set));//返回的数据使用json
+}
  1. ajax来提交请求
    autocomplete.js源代码与介绍:https://github.com/xdan/autocomplete
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
+<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
+<script type="text/javascript">
+    /******************** remote start **********************/
+    $('#remote_input').autocomplete({
+        source: [
+            {
+                url: "ajaxsearch.html?keyword=%QUERY%",
+                type: 'remote'
+            }
+        ]
+    });
+    /********************* remote end ********************/
+</script>
  1. 效果:

欢迎访问我的个人网站

参考:
https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/

http://iamyida.iteye.com/blog/2205114

`,55)]))}const p=s(l,[["render",k],["__file","2.Lucene的使用.html.vue"]]),r=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html","title":"2.Lucene的使用","lang":"zh-CN","frontmatter":{"description":"2.Lucene的使用 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。 Lucene的整体架构 imageimage 搜索引擎的几个重要概念: 倒排索引:将...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2.Lucene的使用"}],["meta",{"property":"og:description","content":"2.Lucene的使用 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。 Lucene的整体架构 imageimage 搜索引擎的几个重要概念: 倒排索引:将..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/lucenejoijfoaiwheifuwhifu.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2.Lucene的使用\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/lucenejoijfoaiwheifuwhifu.png\\",\\"https://github-images.wenzhihuai.com/images/fewfwaefawe20170903183124.png\\",\\"https://github-images.wenzhihuai.com/images/fweafwe170903175148.png\\",\\"https://github-images.wenzhihuai.com/images/fwefawfagergwerg20170903184512.png\\",\\"https://github-images.wenzhihuai.com/images/bervererfwaefewf20170903200915.png\\",\\"https://github-images.wenzhihuai.com/images/QQ%E6%88%AA%E5%9B%BE20170728102929.png\\",\\"https://github-images.wenzhihuai.com/images/QQ%E6%88%AA%E5%9B%BE20170728105628.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"Lucene的整体架构","slug":"lucene的整体架构","link":"#lucene的整体架构","children":[]},{"level":3,"title":"搜索引擎的几个重要概念:","slug":"搜索引擎的几个重要概念","link":"#搜索引擎的几个重要概念","children":[]},{"level":3,"title":"Lucene中的几个概念","slug":"lucene中的几个概念","link":"#lucene中的几个概念","children":[]},{"level":3,"title":"lucene在本网站的使用:","slug":"lucene在本网站的使用","link":"#lucene在本网站的使用","children":[]},{"level":2,"title":"一、搜索","slug":"一、搜索","link":"#一、搜索","children":[]},{"level":2,"title":"二、lucene自动补全","slug":"二、lucene自动补全","link":"#二、lucene自动补全","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":6.82,"words":2046},"filePathRelative":"interesting/个人网站/2.Lucene的使用.md","localizedDate":"2020年1月21日","excerpt":"\\n

首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

","autoDesc":true}');export{p as comp,r as data}; diff --git a/assets/2017.html-DzgcI9cv.js b/assets/2017.html-DzgcI9cv.js new file mode 100644 index 00000000..52f2bb24 --- /dev/null +++ b/assets/2017.html-DzgcI9cv.js @@ -0,0 +1 @@ +import{_ as a,c as i,d as t,o as n}from"./app-ftEjETWs.js";const p={};function r(o,e){return n(),i("div",null,e[0]||(e[0]=[t('

2017

2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...

春节期间,把自己的网站整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~

疲惫的面试历程

2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。

**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?

然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。

有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。

没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?

这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。

突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。

还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。
  4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。

感情、我们毕业了

5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。
  5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。
  6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。

准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。

996的工作

七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。

没有环境,就自己创造,下班有时间就改改自己的网站http://www.wenzhihuai.com,GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。

决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?

双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。

孤独

珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!!
远看:

近看:

题外话:
有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
《企业IT架构转型之道》是今年最好的架构图书,没有之一。

期望

想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗

',35)]))}const h=a(p,[["render",r],["__file","2017.html.vue"]]),m=JSON.parse('{"path":"/life/2017.html","title":"2017","lang":"zh-CN","frontmatter":{"description":"2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/2017.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2017"}],["meta",{"property":"og:description","content":"2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"http://image.wenzhihuai.com/images/20171231044153.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2017\\",\\"image\\":[\\"http://image.wenzhihuai.com/images/20171231044153.png\\",\\"http://image.wenzhihuai.com/images/20171231050705.png\\",\\"http://image.wenzhihuai.com/images/20171231073414.png\\",\\"http://image.wenzhihuai.com/images/20171231080129.png\\",\\"http://image.wenzhihuai.com/images/20171231075917.png\\",\\"http://image.wenzhihuai.com/images/20171231082716.png\\",\\"http://image.wenzhihuai.com/images/20171231082725.png\\",\\"http://image.wenzhihuai.com/images/20171231085744.png\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1579958290000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":11.22,"words":3366},"filePathRelative":"life/2017.md","localizedDate":"2020年1月25日","excerpt":"\\n

2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
\\n17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

","autoDesc":true}');export{h as comp,m as data}; diff --git a/assets/2018.html-UHfvoYE6.js b/assets/2018.html-UHfvoYE6.js new file mode 100644 index 00000000..623d7497 --- /dev/null +++ b/assets/2018.html-UHfvoYE6.js @@ -0,0 +1 @@ +import{_ as t,c as a,d as i,o as n}from"./app-ftEjETWs.js";const o={};function r(p,e){return n(),a("div",null,e[0]||(e[0]=[i('

2018

年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。

心惊胆战的裸辞经历

其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。
去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。

and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好

工作工作

入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。
公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。
去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。
这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。

跑步跑步

平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。

Others

  1. 拿到了深圳户口,全程只花了15天,感谢学妹and家人
  2. 买了第一台MacBook Pro,对于Java的我真心不好用
  3. 盼了好几年的老姐结婚,18年终于了解了这心事
  4. 养了只猫,在经历了好几个月的早起之后,晚上睡觉锁厨房,终于安分了,对不起了哈哈哈

2019

想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱

',18)]))}const h=t(o,[["render",r],["__file","2018.html.vue"]]),c=JSON.parse('{"path":"/life/2018.html","title":"2018","lang":"zh-CN","frontmatter":{"description":"2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历 其实校招过...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/2018.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2018"}],["meta",{"property":"og:description","content":"2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历 其实校招过..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"http://image.wenzhihuai.com/images/2019011011294921712062.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2018\\",\\"image\\":[\\"http://image.wenzhihuai.com/images/2019011011294921712062.png\\",\\"http://image.wenzhihuai.com/images/201901130418261145369442.png\\",\\"https://upyuncdn.wenzhihuai.com/201901130414532037038633.png\\",\\"https://upyuncdn.wenzhihuai.com/20190113103109865107434.png\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"心惊胆战的裸辞经历","slug":"心惊胆战的裸辞经历","link":"#心惊胆战的裸辞经历","children":[]},{"level":2,"title":"工作工作","slug":"工作工作","link":"#工作工作","children":[]},{"level":2,"title":"跑步跑步","slug":"跑步跑步","link":"#跑步跑步","children":[]},{"level":2,"title":"Others","slug":"others","link":"#others","children":[]},{"level":2,"title":"2019","slug":"_2019","link":"#_2019","children":[]}],"git":{"createdTime":1579957849000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":4.35,"words":1306},"filePathRelative":"life/2018.md","localizedDate":"2020年1月25日","excerpt":"\\n

年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

","autoDesc":true}');export{h as comp,c as data}; diff --git a/assets/2019.html-Df2iUZPh.js b/assets/2019.html-Df2iUZPh.js new file mode 100644 index 00000000..05b32be2 --- /dev/null +++ b/assets/2019.html-Df2iUZPh.js @@ -0,0 +1 @@ +import{_ as t,c as a,d as i,o as n}from"./app-ftEjETWs.js";const p={};function r(h,e){return n(),a("div",null,e[0]||(e[0]=[i('

2019

2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

工作

从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。

之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。

工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈

学习

项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。
过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。
操作系统感觉还是要好好学学了,加油

Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。

感情

吵架了,没心思写

运动

过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。

还爬了两次南山

其他

看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。
过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。

2020展望

每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!

当然,如果有可能的话,我想去CSIG

',23)]))}const c=t(p,[["render",r],["__file","2019.html.vue"]]),l=JSON.parse('{"path":"/life/2019.html","title":"2019","lang":"zh-CN","frontmatter":{"description":"2019 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/2019.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2019"}],["meta",{"property":"og:description","content":"2019 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://upyuncdn.wenzhihuai.com/202001010544131765044792.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T04:53:47.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T04:53:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2019\\",\\"image\\":[\\"https://upyuncdn.wenzhihuai.com/202001010544131765044792.png\\",\\"https://upyuncdn.wenzhihuai.com/202001010516141216483720.png\\",\\"http://image.wenzhihuai.com/images/20200101052650730023760.png\\",\\"http://image.wenzhihuai.com/images/202001010535231861948911.png\\"],\\"dateModified\\":\\"2024-01-26T04:53:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"工作","slug":"工作","link":"#工作","children":[]},{"level":2,"title":"学习","slug":"学习","link":"#学习","children":[]},{"level":2,"title":"感情","slug":"感情","link":"#感情","children":[]},{"level":2,"title":"运动","slug":"运动","link":"#运动","children":[]},{"level":2,"title":"其他","slug":"其他","link":"#其他","children":[]},{"level":2,"title":"2020展望","slug":"_2020展望","link":"#_2020展望","children":[]}],"git":{"createdTime":1579958290000,"updatedTime":1706244827000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":4.43,"words":1329},"filePathRelative":"life/2019.md","localizedDate":"2020年1月25日","excerpt":"\\n

2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
\\n19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

","autoDesc":true}');export{c as comp,l as data}; diff --git a/assets/2024-07-24.html-9GUkpnJj.js b/assets/2024-07-24.html-9GUkpnJj.js new file mode 100644 index 00000000..66b78d95 --- /dev/null +++ b/assets/2024-07-24.html-9GUkpnJj.js @@ -0,0 +1 @@ +import{_ as t,c as a,d as o,o as i}from"./app-ftEjETWs.js";const r={};function c(p,e){return i(),a("div",null,e[0]||(e[0]=[o('

2024-07-24

新买了台air m3,午夜色的,心心念念了好久

IMG_20240723_235443

读写速度

c6c117ff1f5f6371b3ab682621369c1f

和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧

096e891a0278263d0c790e74f32ef911_720

非果粉,但好多苹果设备

0742e82ab0ba499dea8b267f958cdada',9)]))}const n=t(r,[["render",c],["__file","2024-07-24.html.vue"]]),s=JSON.parse('{"path":"/about-the-author/personal-life/2024-07-24.html","title":"2024-07-24","lang":"zh-CN","frontmatter":{"description":"2024-07-24 新买了台air m3,午夜色的,心心念念了好久 IMG_20240723_235443 读写速度 c6c117ff1f5f6371b3ab682621369c1f 和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧 096e891a0278263d0c790e74f32ef911_720 非果粉,但好多苹果设备 0742...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2024-07-24"}],["meta",{"property":"og:description","content":"2024-07-24 新买了台air m3,午夜色的,心心念念了好久 IMG_20240723_235443 读写速度 c6c117ff1f5f6371b3ab682621369c1f 和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧 096e891a0278263d0c790e74f32ef911_720 非果粉,但好多苹果设备 0742..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-07-24T15:52:12.000Z"}],["meta",{"property":"article:modified_time","content":"2024-07-24T15:52:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2024-07-24\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-07-24T15:52:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1721836332000,"updatedTime":1721836332000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":0.34,"words":102},"filePathRelative":"about-the-author/personal-life/2024-07-24.md","localizedDate":"2024年7月24日","excerpt":"\\n

新买了台air m3,午夜色的,心心念念了好久

\\n\\"IMG_20240723_235443\\"","autoDesc":true}');export{n as comp,s as data}; diff --git "a/assets/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html-CU80Bk9D.js" "b/assets/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html-CU80Bk9D.js" new file mode 100644 index 00000000..44ffcc45 --- /dev/null +++ "b/assets/2024-11-09\344\270\212\346\265\267\350\277\252\346\226\257\345\260\274.html-CU80Bk9D.js" @@ -0,0 +1 @@ +import{_ as a,c as r,a as e,b as i,o as n,r as c}from"./app-ftEjETWs.js";const p={};function l(m,t){const o=c("BiliBili");return n(),r("div",null,[t[0]||(t[0]=e("h1",{id:"_2024-11-09上海迪斯尼",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_2024-11-09上海迪斯尼"},[e("span",null,"2024-11-09上海迪斯尼")])],-1)),i(o,{bvid:"BV1LSmrYqEDw"})])}const h=a(p,[["render",l],["__file","2024-11-09上海迪斯尼.html.vue"]]),d=JSON.parse('{"path":"/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html","title":"2024-11-09上海迪斯尼","lang":"zh-CN","frontmatter":{"description":"2024-11-09上海迪斯尼","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"2024-11-09上海迪斯尼"}],["meta",{"property":"og:description","content":"2024-11-09上海迪斯尼"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-11T16:10:11.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-11T16:10:11.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"2024-11-09上海迪斯尼\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-11-11T16:10:11.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1731254136000,"updatedTime":1731341411000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":2}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"about-the-author/personal-life/2024-11-09上海迪斯尼.md","localizedDate":"2024年11月10日","excerpt":"\\n","autoDesc":true}');export{h as comp,d as data}; diff --git "a/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-NSgzVWXM.js" "b/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-NSgzVWXM.js" new file mode 100644 index 00000000..09a2fb62 --- /dev/null +++ "b/assets/3.\345\256\232\346\227\266\344\273\273\345\212\241.html-NSgzVWXM.js" @@ -0,0 +1,61 @@ +import{_ as s,c as a,d as n,o as t}from"./app-ftEjETWs.js";const l={};function e(h,i){return t(),a("div",null,i[0]||(i[0]=[n(`

3.定时任务

先看一下Quartz的架构图:

一.特点:

  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  3. 分布式和集群能力。

二.主要组成部分

  1. JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种信息。
  2. JobDetail:QUartz的执行任务的类,通过newInstance的反射机制实例化Job。
  3. Trigger: Job的时间触发规则。主要有SimpleTriggerImpl和CronTriggerImpl两个实现类。
  4. Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。
  5. Scheduler:由上图可以看出,Scheduler是Quartz独立运行的容器。其中,Trigger和JobDetail可以注册到Scheduler中。
  6. ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

三、Quartz设计

  1. properties file
    官网中表明:quartz中使用了quartz.properties来对quartz进行配置,并保留在其jar包中,如果没有定义则默认使用改文件。

四、使用

  1. hello world!代码在这
  2. 本网站中使用quartz来对数据库进行备份,与Spring结合
    (1)导入spring的拓展包,其协助spring集成第三方库:邮件服务、定时任务、缓存等。。。
<dependency>
+    <groupId>org.springframework</groupId>
+    <artifactId>spring-context-support</artifactId>
+    <version>4.2.6.RELEASE</version>
+</dependency>

(2)导入quartz包

<dependency>
+    <groupId>org.quartz-scheduler</groupId>
+    <artifactId>quartz</artifactId>
+    <version>2.3.0</version>
+</dependency>

(3)mysql远程备份
使用命令行工具仅仅需要一行:

mysqldump -u [username] -p[password] -h [hostip] database > file

但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed

<dependency>
+    <groupId>ch.ethz.ganymed</groupId>
+    <artifactId>ganymed-ssh2</artifactId>
+    <version>262</version>
+</dependency>

完整代码如下:

@Component("mysqlService")//在spring中注册一个mysqlService的Bean
+public class MysqlUtil {
+    ...
+    StringBuffer sb = new StringBuffer();
+    sb.append("mysqldump -u " + username + " -p" + password + " -h " + host + " " +
+            database + " >" + file);
+    String sql = sb.toString();
+    Connection connection = new Connection(s_host);
+    connection.connect();
+    boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证
+    if (!isAuth) {
+        logger.error("server login error");
+    }
+    Session session = connection.openSession();
+    session.execCommand(sql);//执行linux语句
+    InputStream stdout = new StreamGobbler(session.getStdout());
+    BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
+    ...
+}

(4)spring中配置quartz

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
+    <property name="targetObject" ref="mysqlService"/>
+    <property name="targetMethod" value="exportDataBase"/>
+</bean>
+<!--定义触发时间  -->
+<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
+    <property name="jobDetail" ref="jobDetail"/>
+    <!-- cron表达式,每周五2点59分运行-->
+    <property name="cronExpression" value="0 59 2 ? * FRI"/>
+</bean>
+<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
+<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
+    <property name="triggers">
+        <list>
+            <ref bean="myTrigger"/>
+        </list>
+    </property>
+</bean>

(5)java完整文件在这

Spring的高级特性之定时任务

java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。

(1)spring配置文件中配置:

<task:annotation-driven/>

(2)最简单的例子,在所需要的函数上添加定时任务即可运行

    @Scheduled(fixedRate = 5000)
+    public void reportCurrentTime() {
+        System.out.println("每隔5秒运行一次" + sdf.format(new Date()));
+    }

(3)运行的时候会报错:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)
+	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)

参考:
1.http://blog.csdn.net/oarsman/article/details/52801877
2.http://stackoverflow.com/questions/31199888/spring-task-scheduler-no-qualifying-bean-of-type-org-springframework-scheduli
Spring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:

1.尝试从配置中找到一个TaskScheduler Bean

2.寻找ScheduledExecutorService Bean

3.使用默认的scheduler
修改log4j.properties即可:
log4j.logger.org.springframework.scheduling=INFO
其实这个功能不影响定时器的功能。
(4)结果:

每隔5秒运行一次14:44:34
+每隔5秒运行一次14:44:39
+每隔5秒运行一次14:44:44
`,36)]))}const p=s(l,[["render",e],["__file","3.定时任务.html.vue"]]),r=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html","title":"3.定时任务","lang":"zh-CN","frontmatter":{"description":"3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。 二.主要组成部分 JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"3.定时任务"}],["meta",{"property":"og:description","content":"3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。 二.主要组成部分 JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/11627468_1438829844Zbz8.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"3.定时任务\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/11627468_1438829844Zbz8.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"一.特点:","slug":"一-特点","link":"#一-特点","children":[]},{"level":3,"title":"二.主要组成部分","slug":"二-主要组成部分","link":"#二-主要组成部分","children":[]},{"level":3,"title":"三、Quartz设计","slug":"三、quartz设计","link":"#三、quartz设计","children":[]},{"level":3,"title":"四、使用","slug":"四、使用","link":"#四、使用","children":[]},{"level":2,"title":"Spring的高级特性之定时任务","slug":"spring的高级特性之定时任务","link":"#spring的高级特性之定时任务","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.55,"words":1064},"filePathRelative":"interesting/个人网站/3.定时任务.md","localizedDate":"2020年1月21日","excerpt":"\\n

先看一下Quartz的架构图:

\\n
\\"\\"
","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/4.\346\227\245\345\277\227\347\263\273\347\273\237.html-BH3nB-v9.js" "b/assets/4.\346\227\245\345\277\227\347\263\273\347\273\237.html-BH3nB-v9.js" new file mode 100644 index 00000000..eeb64c4d --- /dev/null +++ "b/assets/4.\346\227\245\345\277\227\347\263\273\347\273\237.html-BH3nB-v9.js" @@ -0,0 +1,206 @@ +import{_ as s,c as a,d as n,o as t}from"./app-ftEjETWs.js";const h={};function l(e,i){return t(),a("div",null,i[0]||(i[0]=[n(`

4.日志系统.md

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:

企业级的网站日志不能公开,但是我的是个人网站,用来跟大家一起学习的,所以,需要将百度的统计页面展示出来,但是,百度并不提供日志的图像,只提供API给开发者调用,而且还限制访问次数,一天不能超过2000次,这个对于实时统计来说,确实不够,所以只能展示前几天的访问统计。这里的日志系统分为三个步骤:1.API获取数据;2.存储数据;3.展示数据。页面效果如下,也可以点开我的网站的日志系统:

百度统计提供了Tongji API的Java和Python版本,这两个版本及其复杂,可用性极低,所以,本人用Python写了个及其简单的通用版本,整体只有28行,代码在这,https://github.com/Zephery/baidutongji。下面是具体过程

1.网站代码安装

先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。

<script>
+var _hmt = _hmt || [];
+(function() {
+  var hm = document.createElement("script");
+  hm.src = "https://hm.baidu.com/hm.js?code";
+  var s = document.getElementsByTagName("script")[0];
+  s.parentNode.insertBefore(hm, s);
+})();
+</script>

同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。

至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。

2.根据API获取数据

官网的API详细的记录了接口的参数以及解释,
链接:https://api.baidu.com/json/tongji/v1/ReportService/getData,详细的官方报告请访问官网TongjiApi
所需参数(必须):

参数名称参数类型描述
methodstring要查询的报告
start_datestring查询起始时间
end_datestring查询结束时间
metricsstring自定义指标

其中,参数start_date和end_date的规定为:yyyyMMdd,这里我们使用python的原生库,datetime、time,获取昨天的时间以及前七天的日期。

today = datetime.date.today()   # 获取今天的日期
+yesterday = today - datetime.timedelta(days=1) # 获取昨天的日期
+fifteenago = today - datetime.timedelta(days=7) # 获取前七天的日期
+end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "")  # 格式化成yyyyMMdd格式

3.构建请求

说明:siteId可以根据个人百度统计的链接获取,也可以使用Tongji API的第一个接口列表获取用户的站点列表。首先,我们构建一个类,由于username、password、token都是通用的,所以我们将它设置为构造方法的参数。

class Baidu(object):
+    def __init__(self, siteId, username, password, token):
+        self.siteId = siteId
+        self.username = username
+        self.password = password
+        self.token = token

然后构建一个共同的方法,用来获取提交数据之后返回的结果,其中提供了4个可变参数,分别是(start_date:起始日期,end_date:结束日期,method:方法,metrics:指标),返回的是字节,最后需要decode("utf-8")一下变成字符:

def getresult(self, start_date, end_date, method, metrics):
+    body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+                       "username": self.username},
+            "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+                     "end_date": end_date,
+                     "metrics": metrics}}
+    data = bytes(json.dumps(body), 'utf8')
+    req = urllib.request.Request(base_url, data)
+    response = urllib.request.urlopen(req)
+    the_page = response.read()
+    return the_page.decode("utf-8")

至此,python获取百度统计的过程基本就没了,没错,就是那么简简单单的几行,完整代码见https://github.com/Zephery/baidutongji/blob/master/baidu.py,但是,想要实现获取各种数据,仍需要做很多工作。

4.实际运用

(1)需要使用其他参数怎么办

python中提供了个可变参数来解决这一烦恼,详细请看http://www.jianshu.com/p/98f7e34845b5,可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple,而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

def getresult(self, start_date, end_date, method, metrics, **kw):
+    base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+    body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+                       "username": self.username},
+            "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+                     "end_date": end_date, "metrics": metrics}}
+    for key in kw:  #对可变参数进行遍历,如果有的话就往body中加入
+        body['body'][key] = kw[key]

使用方式:

result = self.getresult(start, end, "source/all/a",
+                        "pv_count,visitor_count,avg_visit_time", viewType='visitor')  #其中viewTYpe便是可变参数

(2)获取的数据如何解析
百度统计返回的结果比较简洁而又反人类,以获取概览中的pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time为例子:

result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+                      "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")

返回的结果是:

[[['2017/09/12'], ['2017/09/13'], ['2017/09/14'], ['2017/09/15'], ['2017/09/16'], ['2017/09/17'], ['2017/09/18']],   
+[[422, 76, 76, 41.94, 221],  
+ [284, 67, 65, 50.63, 215],   
+ [67, 23, 22, 52.17, 153],   
+ [104, 13, 13, 36.36, 243],   
+ [13, 4, 4, 33.33, 66],   
+ [73, 7, 6, 37.5, 652],  
+ [63, 11, 11, 33.33, 385]  
+ ], [], []]

即:翻译成人话就是:

[[[date1,date2,...]], 
+ [[date1的pv_count, date1的visitor_count, date1的ip_count, date1的bounce_ratio, date1的avg_visit_time],
+ [date2的pv_count, date2的visitor_count, date2的ip_count, date2的bounce_ratio, date2的avg_visit_time],
+  ...,[]
+ ],[],[]]

极其反人类的设计。还好用的python,python数组的特性实在太强了。出了可以运用[x for x in range]这类语法之外,还能与三元符(x if y else x+1,如果y成立,那么结果是x,如果y不成立,那么结果是x+1)一起使用,这里注意:如果当天访问量为0,其返回的json结果是'--',所以要判断是不是为'--',归0化,才能在折线图等各种图上显示。下面是pv_count的例子:

pv_count = [x[0] if x[0] != '--' else 0 for x in result[1]]

(3)每周限制2000次
在开通数据导出服务的时候,不知道大家有没有注意到它的说明,即我们是不能实时监控的,只能将它放在临时数据库中,这里我们选择了Redis,并在centos里定义一个定时任务,每天全部更新一次即可。

python中redis的使用方法很简单,连接跟mysql类似:

# 字符串
+pool = redis.ConnectionPool(host='your host ip', port=port, password='your auth')  # TODO redis地址
+r = redis.Redis(connection_pool=pool)

本网站使用redis的数据结构只有set,方法也很简单,就是定义一个key,然后value是数组的字符串获取json。

ip_count = [x[2] if x[2] != '--' else 0 for x in result[1]]
+r.set("ip_count", ip_count)
+# json
+name = [item[0]['name'] for item in data[0]]
+count = 0
+tojson = []
+for item in data[1]:
+    temp = {}
+    temp["name"] = name[count]
+    temp["pv_count"] = item[0]
+    temp["visitor_count"] = item[1]
+    temp["average_stay_time"] = item[2]
+    tojson.append(temp)
+    count = count + 1
+r.set("rukouyemian", json.dumps(tojson[:5]))

5.基本代码

下面是基本的使用代码,完整的使用代码就不贴了,有兴趣可以去我的github上看看,完整代码,希望能给个star哈哈哈,感谢

import json
+import time
+import datetime
+import urllib.parse
+import urllib.request
+
+base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+
+class Baidu(object):
+    def __init__(self, siteId, username, password, token):
+        self.siteId = siteId
+        self.username = username
+        self.password = password
+        self.token = token
+
+    def getresult(self, start_date, end_date, method, metrics, **kw):
+        base_url = "https://api.baidu.com/json/tongji/v1/ReportService/getData"
+        body = {"header": {"account_type": 1, "password": self.password, "token": self.token,
+                           "username": self.username},
+                "body": {"siteId": self.siteId, "method": method, "start_date": start_date,
+                         "end_date": end_date, "metrics": metrics}}
+        for key in kw:
+            body['body'][key] = kw[key]
+        data = bytes(json.dumps(body), 'utf8')
+        req = urllib.request.Request(base_url, data)
+        response = urllib.request.urlopen(req)
+        the_page = response.read()
+        return the_page.decode("utf-8")
+
+if __name__ == '__main__':
+    # 日期开始
+    today = datetime.date.today()
+    yesterday = today - datetime.timedelta(days=1)
+    fifteenago = today - datetime.timedelta(days=7)
+    end, start = str(yesterday).replace("-", ""), str(fifteenago).replace("-", "")
+    # 日期结束
+    bd = Baidu(yoursiteid, "username", "password", "token")
+    result = bd.getresult(start, end, "overview/getTimeTrendRpt",
+                          "pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time")
+    result = json.loads(result)
+    base = result["body"]["data"][0]["result"]["items"]
+    print(base)

6.展示数据

在将数据存进redis中之后,我们需要在博客中使用这些数据来制作图表。在newblog中使用方式也很简单,大概就是使用jedis读取数据,然后使用echarts或者highcharts展示。其中折线图以及线型图我都使用了highcharts,确实比echarts好看的多,但是地域图还是选择了echarts,毕竟中国的产品还是对中国的支持较好。
(1)PV、UV折线图
以图表PV、UV为例,由于存储进redis的是一个数组,所以,可以直接从redis中读取然后放到一个attribute里即可:

String pv_count = jedis.get("pv_count");
+String visitor_count = jedis.get("visitor_count");
+mv.addObject("pv_count", pv_count);
+mv.addObject("visitor_count", visitor_count);

jsp中的使用如下:

<div class="panel-heading" style="background-color: rgba(187,255,255,0.7)">
+    <div class="card-title">
+        <strong>PV和UV折线图</strong>
+    
+    
+    <div class="panel-body">
+        <div id="linecontainer" style="width: auto;height: 330px">
+        <script>
+            var chart = new Highcharts.Chart('linecontainer', {
+                title: {
+                    text: null
+                },
+                credits: {
+                    enabled: false
+                },
+                xAxis: {
+                    categories: ${daterange}
+                },
+                yAxis: {
+                    title: {
+                        text: '次数'
+                    },
+                    plotLines: [{
+                        value: 0,
+                        width: 1,
+                        color: '#808080'
+                    }]
+                },
+                tooltip: {
+                    valueSuffix: '次'
+                },
+                legend: {
+                    borderWidth: 0,
+                    align: "center", //程度标的目标地位
+                    verticalAlign: "top", //垂直标的目标地位
+                    x: 0, //间隔x轴的间隔
+                    y: 0 //间隔Y轴的间隔
+                },
+                series: [{
+                    name: 'pv',
+                    data:${pv_count}
+                }, {
+                    name: 'uv',
+                    data:${visitor_count}
+                }]
+            })
+        </script>

效果如下:

(2)地域访问量
在python代码中先获取地域的数据,其结果如下,百度统计跟echarts都是百度的,果然,自家人对自己人的支持真是特别友好的。

[{'pv_count': 649, 'pv_ratio': 7, 'visitor_count': 2684, 'name': '广东'}, {'pv_count': 2, 'pv_ratio': 2, 'visitor_count': 76, 'name': '四川'}, {'pv_count': 1, 'pv_ratio': 1, 'visitor_count': 3, 'name': '江苏'}]

地域图目前支持最好的还是百度的echarts,使用方法见echarts的官网吧,这里不再阐述,展示地域图的时候需要获取下载两个文件,china.js(其提供了js和json,这里使用的js),echarts.js
部分代码:

<script type="text/javascript">
+    var myChart = echarts.init(document.getElementById('diyu'));
+    option = {
+        tooltip: {
+            trigger: 'item'
+        },
+        legend: {
+            orient: 'vertical',
+            left: 'left'
+        },
+        visualMap: {
+            min: 0,
+            max:\${diyumax},
+            left: 'left',
+            top: 'bottom',
+            text: ['高', '低'],           // 文本,默认为数值文本
+            calculable: true
+        },
+        toolbox: {
+            show: true,
+            orient: 'vertical',
+            left: 'right',
+            top: 'center',
+            feature: {
+                dataView: {readOnly: false},
+                restore: {},
+                saveAsImage: {}
+            }
+        },
+        series: [
+            {
+                name: '访问量',
+                type: 'map',
+                mapType: 'china',
+                roam: false,
+                label: {
+                    normal: {
+                        show: true
+                    },
+                    emphasis: {
+                        show: true
+                    }
+                },
+                data: [
+                    <c:forEach var="diyu" items="\${diyu}">
+                    {name: '\${diyu.name}', value: \${to.pv_count}},
+                    </c:forEach>
+                ]
+            }
+        ]
+    };
+    myChart.setOption(option);
+</script>

结果如下:

结语

网上关于日志系统的几乎都是ELK,对于小网站的,隐私不是很重要的还是可以用用百度统计的,这套系统也折磨了我挺久的,特别是它那反人类的返回数据。期初本来是想使用百度统计的,后来考虑了一下ELK,尝试之后发现,服务器配置跑不起来,还是安安稳稳的使用了百度统计,于此做成了这个系统,美观度还是不高,颜色需要优化一下。最后,希望能在GitHub上给我个star吧。
日志系统地址:http://www.wenzhihuai.com/log.html
个人网站网址:http://www.wenzhihuai.com
个人网站代码地址:https://github.com/Zephery/newblog
百度统计python代码地址:https://github.com/Zephery/baidutongji
万分感谢

`,64)]))}const p=s(h,[["render",l],["__file","4.日志系统.html.vue"]]),r=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html","title":"4.日志系统.md","lang":"zh-CN","frontmatter":{"description":"4.日志系统.md 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"4.日志系统.md"}],["meta",{"property":"og:description","content":"4.日志系统.md 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/300.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"4.日志系统.md\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/300.png\\",\\"https://github-images.wenzhihuai.com/images/20170918090524.png\\",\\"https://github-images.wenzhihuai.com/images/20170918090534.png\\",\\"https://github-images.wenzhihuai.com/images/20170918090546.png\\",\\"https://github-images.wenzhihuai.com/images/20170919090557.png\\",\\"https://github-images.wenzhihuai.com/images/20170919091911.png\\",\\"https://github-images.wenzhihuai.com/images/20170919092505.png\\",\\"https://github-images.wenzhihuai.com/images/20170919012925.png\\",\\"https://github-images.wenzhihuai.com/images/echartsfawe.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1.网站代码安装","slug":"_1-网站代码安装","link":"#_1-网站代码安装","children":[]},{"level":2,"title":"2.根据API获取数据","slug":"_2-根据api获取数据","link":"#_2-根据api获取数据","children":[]},{"level":2,"title":"3.构建请求","slug":"_3-构建请求","link":"#_3-构建请求","children":[]},{"level":2,"title":"4.实际运用","slug":"_4-实际运用","link":"#_4-实际运用","children":[]},{"level":2,"title":"5.基本代码","slug":"_5-基本代码","link":"#_5-基本代码","children":[]},{"level":2,"title":"6.展示数据","slug":"_6-展示数据","link":"#_6-展示数据","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":9.64,"words":2891},"filePathRelative":"interesting/个人网站/4.日志系统.md","localizedDate":"2020年1月21日","excerpt":"\\n

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

","autoDesc":true}');export{p as comp,r as data}; diff --git a/assets/404.html-BMHKR2ag.js b/assets/404.html-BMHKR2ag.js new file mode 100644 index 00000000..b9e45dea --- /dev/null +++ b/assets/404.html-BMHKR2ag.js @@ -0,0 +1 @@ +import{_ as e,c as o,a as n,o as r}from"./app-ftEjETWs.js";const a={};function p(c,t){return r(),o("div",null,t[0]||(t[0]=[n("p",null,"404 Not Found",-1)]))}const i=e(a,[["render",p],["__file","404.html.vue"]]),l=JSON.parse('{"path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound","description":"404 Not Found","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/404.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:description","content":"404 Not Found"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\",\\"description\\":\\"404 Not Found\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":null,"excerpt":"

404 Not Found

\\n","autoDesc":true}');export{i as comp,l as data}; diff --git "a/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-BN5IQG4o.js" "b/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-BN5IQG4o.js" new file mode 100644 index 00000000..1aa6f734 --- /dev/null +++ "b/assets/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html-BN5IQG4o.js" @@ -0,0 +1,96 @@ +import{_ as s,c as a,d as n,o as e}from"./app-ftEjETWs.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

5.小集群部署.md

欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

nginx负载均衡

一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。
通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自张开涛的《亿级流量网站架构核心技术》

本站并没有那么多的服务器,目前只有两台,搭建不了那么大型的架构,就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构:

其中服务器A(119.23.46.71)为深圳节点,服务器B(47.95.10.139)为北京节点,搭建Nginx之后流量是这么走的:user->A->B-A->user或者user->A->user,第一条中A将请求转发给B,然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习,所以请不要考虑因为地域导致延时的问题。。。。下面是过程。

1.1 Nginx的安装

可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从https://pkgs.org/download/nginx可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。

启动后页面如下:

记一下常用命令

启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。
+[root@beijingali ~]# nginx          #启动nginx
+[root@beijingali ~]# nginx -s reload         #重启nginx
+[root@beijingali ~]# nginx -t           #校验nginx配置文件
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful

1.2 Nginx的配置

1.2.1 负载均衡算法

Nginx常用的算法有:
(1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。
(2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。
(3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。
使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是tomcat session的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。

http{
+    ...
+    upstream backend {
+        hash $uri;
+        # 北京节点
+        server 47.95.10.139:8080;
+        # 深圳节点
+        server 119.23.46.71:8080;
+    }
+
+    server {
+        ...
+        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            ...
+        }
+    ...

1.2.2 日志格式

之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧

    ...
+    log_format main '{"@timestamp":"$time_iso8601",'
+                    '"host":"$server_addr",'
+                    '"clientip":"$remote_addr",'
+                    '"size":$body_bytes_sent,'
+                    '"responsetime":$request_time,'
+                    '"upstreamtime":"$upstream_response_time",'
+                    '"upstreamhost":"$upstream_addr",'
+                    '"http_host":"$host",'
+                    '"url":"$uri",'
+                    '"xff":"$http_x_forwarded_for",'
+                    '"referer":"$http_referer",'
+                    '"agent":"$http_user_agent",'
+                    '"status":"$status"}';
+    access_log  logs/access.log  main;
+    ...

1.2.3 HTTP反向代理

配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考nginx 配置。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:

        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Host $host;
+            proxy_set_header REMOTE-HOST $remote_addr;
+        }

(1)proxy_set_header X-real-ip $remote_addr;
其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取:
request.getAttribute("X-real-ip")

(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。

1.2.4 HTTPS

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:

首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:

        ssl_certificate 1_www.wenzhihuai.com_bundle.crt;    #主要文件路径
+        ssl_certificate_key 2_www.wenzhihuai.com.key;
+        ssl_session_timeout 5m;         # 超时时间
+        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
+        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
+        ssl_prefer_server_ciphers on;

至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可

1.2.5 失败重试

配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试

upstream backend_server {
+    server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
+    server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
+}

session共享

分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自tomcat 集群中 session 共
1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。)
2.使用 tomcat sessionmanager 方法存储。(直接配置即可)
3.使用 terracotta 服务器共享。(不知道,不了解)
4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)

本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考Spring-Session官网。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。

首先,添加相关依赖

        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-data-redis</artifactId>
+            <version>1.3.1.RELEASE</version>
+            <type>pom</type>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>\${jedis.version}</version>
+        </dependency>

新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:

    <!-- redis -->
+    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
+    </bean>
+
+    <bean id="jedisConnectionFactory"
+          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+        <property name="hostName" value="\${host}" />
+        <property name="port" value="\${port}" />
+        <property name="password" value="\${password}" />
+        <property name="timeout" value="\${timeout}" />
+        <property name="poolConfig" ref="jedisPoolConfig" />
+        <property name="usePool" value="true" />
+    </bean>
+
+    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+        <property name="connectionFactory" ref="jedisConnectionFactory" />
+    </bean>
+
+    <!-- 将session放入redis -->
+    <bean id="redisHttpSessionConfiguration"
+          class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
+        <property name="maxInactiveIntervalInSeconds" value="1800" />
+    </bean>

然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截

<filter>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+</filter>
+<filter-mapping>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+    <dispatcher>REQUEST</dispatcher>
+    <dispatcher>ERROR</dispatcher>
+</filter-mapping>

配置完成,使用RedisDesktopManager查看结果:

测试:

访问http://www.wenzhihuai.com
tail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录

访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。

点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。

值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。
欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star
个人网站网址:http://www.wenzhihuai.com
个人网站代码地址:https://github.com/Zephery/newblog

`,56)]))}const r=s(t,[["render",l],["__file","5.小集群部署.html.vue"]]),k=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html","title":"5.小集群部署.md","lang":"zh-CN","frontmatter":{"description":"5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"5.小集群部署.md"}],["meta",{"property":"og:description","content":"5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171018044732.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"5.小集群部署.md\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171018044732.png\\",\\"https://github-images.wenzhihuai.com/images/20171018051437.png\\",\\"https://github-images.wenzhihuai.com/images/20171103062512.png\\",\\"https://github-images.wenzhihuai.com/images/20171103063022.png\\",\\"https://github-images.wenzhihuai.com/images/20171105024159.png\\",\\"https://github-images.wenzhihuai.com/images/20171105045003.png\\",\\"https://github-images.wenzhihuai.com/images/20171105050714.png\\",\\"https://github-images.wenzhihuai.com/images/20171105050757.png\\",\\"https://github-images.wenzhihuai.com/images/20171105050849.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1.1 Nginx的安装","slug":"_1-1-nginx的安装","link":"#_1-1-nginx的安装","children":[]},{"level":2,"title":"1.2 Nginx的配置","slug":"_1-2-nginx的配置","link":"#_1-2-nginx的配置","children":[]},{"level":2,"title":"测试:","slug":"测试","link":"#测试","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":9.5,"words":2849},"filePathRelative":"interesting/个人网站/5.小集群部署.md","localizedDate":"2020年1月21日","excerpt":"\\n

欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
\\n洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

","autoDesc":true}');export{r as comp,k as data}; diff --git "a/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-Ca5VIWc1.js" "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-Ca5VIWc1.js" new file mode 100644 index 00000000..b070755c --- /dev/null +++ "b/assets/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html-Ca5VIWc1.js" @@ -0,0 +1,61 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const l={};function t(p,s){return e(),a("div",null,s[0]||(s[0]=[n(`

6.数据库备份

先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:

果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:

大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:

一、MySql的复制

数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
(2)slave将master的binary log events拷贝到它的中继日志(relay log);
(3)slave重做中继日志中的事件,将改变反映它自己的数据。

该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。
下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。
SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。
此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。
MySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。

二、配置过程

2.1 创建所用的复制账号

由于是个自己的小网站,就不做过多的操作了,直接使用root账号

2.2 配置master

接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。

[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# master
+log-bin=mysql-bin
+# 设为基于行的复制
+binlog-format=ROW
+# 设置server的唯一id
+server-id=2
+# 忽略的数据库,不使用备份
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 要进行备份的数据库
+binlog-do-db=myblog

重启Mysql之后,查看主库状态,show master status。

其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。

2.3 配置slave

从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。


+[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# slave
+log-bin=mysql-bin
+# 服务器唯一id
+server-id=3
+# 不备份的数据库
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 需要备份的数据库
+replicate-do-db=myblog
+# 其他相关信息
+slave-skip-errors=all
+slave-net-timeout=60
+# 开启中继日志
+relay_log         = mysql-relay-bin
+# 
+log_slave_updates = 1
+# 防止改变数据
+read_only         = 1

重启slave,同时启动复制,还需要调整一下命令。

mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;

可以看见slave已经开始进行同步了。我们使用show slave status\\G来查看slave的状态。

其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。

某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:

原理相对比较简单:

1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
3.canal解析binary log对象(原始为byte流)

其中,配置过程如下:https://github.com/alibaba/canal,可以搭配Zookeeper使用。在ZKUI中能够查看到节点:

一般情况下,还要配合阿里的另一个开源产品使用otter,相关文档还是找找GitHub吧,个人搭建完了之后,用起来还是不如直接使用mysql的主主复制,而且异地机房同步这种大企业才有的业务。

公司又要996了,实在是忙不过来,感觉自己写的还是急躁了点,困==

`,38)]))}const h=i(l,[["render",t],["__file","6.数据库备份.html.vue"]]),d=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html","title":"6.数据库备份","lang":"zh-CN","frontmatter":{"description":"6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下: 果然,...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"6.数据库备份"}],["meta",{"property":"og:description","content":"6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下: 果然,..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171018051437-20240126113709561.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"6.数据库备份\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171018051437-20240126113709561.png\\",\\"https://github-images.wenzhihuai.com/images/20171118033130.png\\",\\"https://github-images.wenzhihuai.com/images/20171118034129.png\\",\\"https://github-images.wenzhihuai.com/images/20171118035150.png\\",\\"https://github-images.wenzhihuai.com/images/20171118040843.png\\",\\"https://github-images.wenzhihuai.com/images/20171118050128.png\\",\\"https://github-images.wenzhihuai.com/images/20171118051031.png\\",\\"https://github-images.wenzhihuai.com/images/20171120094405.png\\",\\"https://github-images.wenzhihuai.com/images/20171120100237.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"2.1 创建所用的复制账号","slug":"_2-1-创建所用的复制账号","link":"#_2-1-创建所用的复制账号","children":[]},{"level":3,"title":"2.2 配置master","slug":"_2-2-配置master","link":"#_2-2-配置master","children":[]},{"level":3,"title":"2.3 配置slave","slug":"_2-3-配置slave","link":"#_2-3-配置slave","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":5.77,"words":1732},"filePathRelative":"interesting/个人网站/6.数据库备份.md","localizedDate":"2020年1月21日","excerpt":"\\n

先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

","autoDesc":true}');export{h as comp,d as data}; diff --git "a/assets/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html-Br7NfWE4.js" "b/assets/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html-Br7NfWE4.js" new file mode 100644 index 00000000..7d7c0fbf --- /dev/null +++ "b/assets/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html-Br7NfWE4.js" @@ -0,0 +1,13 @@ +import{_ as e,c as a,d as s,o as t}from"./app-ftEjETWs.js";const n={};function r(h,i){return t(),a("div",null,i[0]||(i[0]=[s(`

7.那些牛逼的插件

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
本站主要用到的插件有:
1.wowslider
2.畅言
3.Editor.md
4.highchart、echart
5.百度分享
6.waterfall.js
7.心知天气
8.标签云

wowslider

可能是我这网站中最炫的东西了,图片能够自动像幻灯片一样自动滚动,让网站的首页一看起来就高大上,简直就是建站必备的东西,而且安装也及其简单,有兴趣可以点击官网看看。GitHub里也开放了源代码。安装过程:自己选择“幻灯片”切换效果,保存为html就行了,WORDPREESS中好像有集成这个插件的,做的还更好。感兴趣可以点击我的博客首页看一看。

不过还有个值得注意的问题,就是wowslider里面带有一个googleapis的服务,即https://fonts.googleapis.com/css?family=Arimo&subset=latin,cyrillic,latin-ext,由于一般用户不能访问谷歌,会导致网页加载速度及其缓慢,所以,去掉为妙

畅言

作为社交评论的工具,虽然说表示还是想念以前的多说,但是畅言现在做得还是好了,有评论审核,评论导出导入等功能,如果浏览量大的话,还能提供广告服务,让站长也能拿到一丢丢的广告费。本博客中使用了畅言的基本评论、获取某篇文章评论数的功能。可以到我这里留言

Editor.md

一款能将markdown解析为html的插件,国人的作品,博客的文章编辑器一开始想使用的是markdown,想法是:写文章、保存数据库都是markdown格式的,保存在数据库中,读取时有需要解析markdown,这个过程是有点耗时的,但是相比把html式的网页保存在数据库中友好点吧,因为如果一篇文章比较长的话,转成html的格式,光是大小估计也得超过几十kb?所以,还是本人选择的是一切都用源markdown。
editor.md,是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。页面看起来还是美观的,相比WORDPRESS的那些牛逼插件还差那么一点点,不过从普通人的眼光来看,应该是可以的了。此处,我用它作为解析网页的利器,还有就是后台编辑也是用的这个。

代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。

图表

目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)

不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。

百度分享

作为一个以博客为主的网站,免不了使用一些社会化分享的工具,目前主要是jiathis和百度分享,这两者的ui都是相似的(丑爆了)。凭我个人的感觉,jiathis加载实在是太过于缓慢,这点是无法让人忍受的,只好投靠百度。百度分享类似于jiathis,安装也很简单,具体见官网http://share.baidu.com/code/advance#tools。一直点点点,配置完之后,就是下图这种,丑爆了是不是?

好在对它的美观改变不是很难,此处参考了别人的UI设计,原作者我忘记怎么联系他了。其原理主要是使用图片来替换掉原本的东西。完整的源码可以点击此处

#share a {
+    width: 34px;
+    height: 34px;
+    padding: 0;
+    margin: 6px;
+    border-radius: 25px;
+    transition: all .4s;
+}
+/*主要的是替换掉backgound的背景图片*/
+#share a.bds_qzone {
+    background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;
+    background-size: 34px 34px;
+}

改完之后的效果。

瀑布流

有段时间,瀑布流特别流行?还有段时间,瀑布流开始遭到各种抵制。。。看看知乎的人怎么说,大部分人不喜欢的原因是让人觉得视觉疲劳,不过瀑布流最大的好处还是有的:提高发现好图的效率以及图片列表页极强的视觉感染力。没错,我还是想把自己的网站弄得铉一些==,所以采用了瀑布流(不过效果不是很好,某些浏览器甚至加载出错),这个大bug有时间再改,毕竟花了很多时间做的这个,效果确实不咋地。目前主要的瀑布流有waterfall.jsmasory.js。这一块目前还不是很完善,希望能得到各位大佬的指点。

天气插件

此类咨询服务还是网上还是挺多的,这一块不知道是不是所谓的“画蛇添足”这部分,主要是我觉得网站右边这部分老是少了点什么,所以加上了天气插件。目前常用的天气插件有中国天气网心知天气等。安装方式见各自的官网,这里不再阐述,我使用的是心知天气。注意:心知天气限制流量的,一个小时内只能使用400次,如果超出了会失效,当然也可以付费使用。

标签云

标签云,弄得好的话应该说是一个网站的点缀。现在好像比较流行3D的标签云?像下面这种。

从个人的网站风格来看,比较适应PHP形式的,有点颜色而又不绚丽的即可,之前用的跟分类的一样的样式,即双纵列的样式,美观度还行,虽然老是感觉有点怪怪的,如果某个标签的字数过长怎么办,岂不是要顶出div了。所以还是选择换另一种风格,最终偶然一次找到了下面这种,能够自适应宽度,颜色虽然鲜艳了点(以后有空再调一下吧),源码见style.css。 下图为目前的标签页。

总结

作为一个后端人员,调css和js真是痛苦==,好在坚持下来了,虽然还是很多不足,以后有时间慢慢改。说了那么多,感觉自己还是菜的抠脚。
题外话,搭建一个博客,对于一个新近程序员来说真的是锻炼自己的一个好机会,能够认识到从前端、java后台、linux、jvm等等知识,只是真的有点耗时间(还不如把时间耗在Spring源码),如果不采用别人的框架的话,付出的代价还是蛮大的(所以不要鄙视我啦)。没有什么能够一举两得,看自己的取舍吧。加油💪(ง •_•)ง

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

`,40)]))}const p=e(n,[["render",r],["__file","7.那些牛逼的插件.html.vue"]]),g=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html","title":"7.那些牛逼的插件","lang":"zh-CN","frontmatter":{"description":"7.那些牛逼的插件 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Ja...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"7.那些牛逼的插件"}],["meta",{"property":"og:description","content":"7.那些牛逼的插件 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Ja..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171121023427.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"7.那些牛逼的插件\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171121023427.png\\",\\"https://github-images.wenzhihuai.com/images/20171121024358.png\\",\\"https://github-images.wenzhihuai.com/images/20171122030819.png\\",\\"https://github-images.wenzhihuai.com/images/20171122034349.png\\",\\"https://github-images.wenzhihuai.com/images/20171121023342.png\\",\\"https://github-images.wenzhihuai.com/images/20171125122526.png\\",\\"https://github-images.wenzhihuai.com/images/20171121022116.png\\",\\"https://github-images.wenzhihuai.com/images/20171127034920.png\\",\\"https://github-images.wenzhihuai.com/images/20171125103716.png\\",\\"https://github-images.wenzhihuai.com/images/20171125105127.png\\",\\"https://github-images.wenzhihuai.com/images/20171127033202.png\\",\\"https://github-images.wenzhihuai.com/images/20171127033945.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"wowslider","slug":"wowslider","link":"#wowslider","children":[]},{"level":2,"title":"畅言","slug":"畅言","link":"#畅言","children":[]},{"level":2,"title":"Editor.md","slug":"editor-md","link":"#editor-md","children":[]},{"level":2,"title":"图表","slug":"图表","link":"#图表","children":[]},{"level":2,"title":"百度分享","slug":"百度分享","link":"#百度分享","children":[]},{"level":2,"title":"瀑布流","slug":"瀑布流","link":"#瀑布流","children":[]},{"level":2,"title":"天气插件","slug":"天气插件","link":"#天气插件","children":[]},{"level":2,"title":"标签云","slug":"标签云","link":"#标签云","children":[]}],"git":{"createdTime":1579574285000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":7.24,"words":2173},"filePathRelative":"interesting/个人网站/7.那些牛逼的插件.md","localizedDate":"2020年1月21日","excerpt":"\\n

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
\\n建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
\\n本站主要用到的插件有:
\\n1.wowslider
\\n2.畅言
\\n3.Editor.md
\\n4.highchart、echart
\\n5.百度分享
\\n6.waterfall.js
\\n7.心知天气
\\n8.标签云

","autoDesc":true}');export{p as comp,g as data}; diff --git "a/assets/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html-DopYDuB6.js" "b/assets/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html-DopYDuB6.js" new file mode 100644 index 00000000..9b468a32 --- /dev/null +++ "b/assets/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html-DopYDuB6.js" @@ -0,0 +1 @@ +import{_ as n,c as a,a as e,o}from"./app-ftEjETWs.js";const r={};function i(c,t){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"_8-基于贝叶斯的情感分析",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_8-基于贝叶斯的情感分析"},[e("span",null,"8.基于贝叶斯的情感分析")])],-1)]))}const m=n(r,[["render",i],["__file","8.基于贝叶斯的情感分析.html.vue"]]),s=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html","title":"8.基于贝叶斯的情感分析","lang":"zh-CN","frontmatter":{"description":"8.基于贝叶斯的情感分析","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"8.基于贝叶斯的情感分析"}],["meta",{"property":"og:description","content":"8.基于贝叶斯的情感分析"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"8.基于贝叶斯的情感分析\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1566721640000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":11},"filePathRelative":"interesting/个人网站/8.基于贝叶斯的情感分析.md","localizedDate":"2019年8月25日","excerpt":"\\n","autoDesc":true}');export{m as comp,s as data}; diff --git "a/assets/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html-8ms_Vznn.js" "b/assets/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html-8ms_Vznn.js" new file mode 100644 index 00000000..aaa67903 --- /dev/null +++ "b/assets/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html-8ms_Vznn.js" @@ -0,0 +1 @@ +import{_ as n,c as a,a as e,o}from"./app-ftEjETWs.js";const r={};function i(c,t){return o(),a("div",null,t[0]||(t[0]=[e("h1",{id:"_9-网站性能优化",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_9-网站性能优化"},[e("span",null,"9.网站性能优化")])],-1)]))}const m=n(r,[["render",i],["__file","9.网站性能优化.html.vue"]]),s=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html","title":"9.网站性能优化","lang":"zh-CN","frontmatter":{"description":"9.网站性能优化","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"9.网站性能优化"}],["meta",{"property":"og:description","content":"9.网站性能优化"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"9.网站性能优化\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1566721640000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.02,"words":7},"filePathRelative":"interesting/个人网站/9.网站性能优化.md","localizedDate":"2019年8月25日","excerpt":"\\n","autoDesc":true}');export{m as comp,s as data}; diff --git "a/assets/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html-DvomVRAD.js" "b/assets/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html-DvomVRAD.js" new file mode 100644 index 00000000..c5239d9d --- /dev/null +++ "b/assets/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html-DvomVRAD.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as i,o as r}from"./app-ftEjETWs.js";const n={};function h(p,e){return r(),a("div",null,e[0]||(e[0]=[i('

TCP/IP、HTTP、HTTPS、HTTP2.0

HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

HTTPS,即加密后的HTTP。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。目前TLS的版本是1.2,定义在RFC 5246中,暂时还没有被广泛的使用。

HTTP2.0,下一代的HTTP协议。相比于HTTP1.x,大幅度的提升了web性能,进一步减少了网络延时和拥塞。

各自的RFC相关文档自己去搜吧,https://www.rfc-editor.org/

一、TCP/IP

为了了解HTTP,有必要先理解一下TCP/IP。目前,存在两种划分模型的方法,OSI七层模型和TCP/IP模型,具体的区别不在阐述。HTTP是建立在TCP协议之上,所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性,例如tcp建立连接的3次握手和断开连接的4次挥手以及每次建立连接带来的RTT延迟时间。

TCP三次握手四次挥手的原理,由于篇幅关系,具体请看TCP协议的三次握手和四次挥手

二、HTTP

超文本传输协议(HyperText Transfer Protocol) 是伴随着计算机网络和浏览器而诞生,在浏览器出现之前,人们是怎么使用网络的?,不管怎么说,那个时代对于现在的我们,有点难以想象。。。之后,网景发布了Netscape Navigator浏览器,才慢慢打开了互联网的幕布。如果根据OSI来划分的话,HTML属于表示层,而HTTP属于应用层。HTTP发展至今,经过了HTTP0.9、HTTP1.0、HTTP1.1、HTTP2.0的时代,虽然2.0很久之前就正式提出标准,大多浏览器也支持了,但是网络支持HTTP2.0的却很少。

2.1 HTTP报文分析

报文,是网络中交换和传输的基本单元,即一次性发送的数据块。HTTP的报文是由一行一行组成的,纯文本,而且是明文,即:如果能监听你的网络,那么你发送的所有账号密码都是可以看见的,为了保障数据隐秘性,HTTPS随之而生。

2.1.1 请求报文:

为了形象点,我们把报文标准和实际的结合起来看。

下面是实际报文,以访问自己的网站(http://www.wenzhihuai.com)中的一个链接为例。

请求行

请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT,这里我们使用的是GET方法,访问的是/biaoqianyun.do,协议使用的是HTTP/1.1。
GET:当客户端要从服务器中读取某个资源时,使用GET 方法。如果需要加传参数的话,需要在URL之后加个"?",然后把参数名字和值用=连接起来,传递参数长度受限制,通常IE8的为4076,Chrome的为7675。例如,/index.jsp?id=100&op=bind。
POST:当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,以名称/值的形式出现,可以传输大量数据;

请求头部

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
User-Agent:产生请求的浏览器类型;
Accept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ / ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
Accept-Language:客户端可接受的自然语言;
Accept-Encoding:客户端可接受的编码压缩格式;
Accept-Charset:可接受的应答的字符集;
Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
connection:连接方式(close 或 keepalive),如果是close的话就需要进行TCP四次挥手关闭连接,如果是keepalive,表明还能继续使用,这是HTTP1.1对1.0的新增,加快了网络传输,默认是keepalive;
Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;

空行

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头;

请求包体

请求包体不在 GET 方法中使用,而是在POST 方法中使用。POST 方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型 Content-Type 和包体长度 Content-Length;

2.1.2 响应报文

同样,先粘贴报文标准。

抓包,以访问(http://www.wenzhihuai.com)为例。

状态行

状态行由 HTTP 协议版本字段、状态码和状态码的描述文本 3 个部分组成,他们之间使用空格隔开,描述文本一般不显示;
状态码:由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:
1xx:服务器已接收,但客户端可能仍要继续发送;
2xx:成功;
3xx:重定向;
4xx:请求非法,或者请求不可达;
5xx:服务器内部错误;

响应头部:响应头可能包括:

Location:Location响应报头域用于重定向接受者到一个新的位置。例如:客户端所请求的页面已不存在原先的位置,为了让客户端重定向到这个页面新的位置,服务器端可以发回Location响应报头后使用重定向语句,让客户端去访问新的域名所对应的服务器上的资源;
Server:Server 响应报头域包含了服务器用来处理请求的软件信息及其版本。它和 User-Agent 请求报头域是相对应的,前者发送服务器端软件的信息,后者发送客户端软件(浏览器)和操作系统的信息。
Vary:指示不可缓存的请求头列表;
Connection:连接方式,这个跟rquest的类似。
空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
响应包体:服务器返回给客户端的文本信息;

2.2 HTTP特性

HTTP的主要特点主要能概括如下:

2.2.1 无状态性

即,当客户端访问完一次服务器再次访问的时候,服务器是无法知道这个客户端之前是否已经访问过了。优点是不需要先前的信息,能够更快的应答,缺点是每次连接传送的数据量增大。这种做法不利于信息的交互,随后,Cookie和Session就应运而生,至于它俩有什么区别,可以看看COOKIE和SESSION有什么区别?

2.2.2 持久连接

HTTP1.1 使用持久连接keepalive,所谓持久连接,就是服务器在发送响应后仍然在一段时间内保持这条连接,允许在同一个连接中存在多次数据请求和响应,即在持久连接情况下,服务器在发送完响应后并不关闭TCP连接,客户端可以通过这个连接继续请求其他对象。

2.2.3 其他

支持客户/服务器模式、简单快速(请求方法简单Get和POST)、灵活(数据对象任意)

2.3 影响HTTP的因素

影响HTTP请求的因素:

  1. 带宽
    好像只要上网这个因素是一直都有的。。。即使再快的网络,也会有偶尔网络慢的时候。。。
  2. 延迟
    (1) 浏览器阻塞
    一个浏览器对于同一个域名,同时只能有4个链接(根据不同浏览器),如果超了后面的会被阻塞。
    常用浏览器阻塞数量看下图。

(2) DNS查询
浏览器建立连接是需要知道服务器的IP的,DNS用来将域名解析为IP地址,这个可以通过刷新DNS缓存来加快速度。
(3) 建立连接
由之前第一章的就可以看出,HTTP是基于TCP协议的,即使网络、浏览器再快也要进行TCP的三次握手,在高延迟的场景下影响比较明显,慢启动则对文件请求影响较大。

2.4 缺陷

  1. 耗时:传输数据每次都要建立连接;
  2. 不安全:HTTP是明文传输的,只要在路由器或者交换机上截取,所有东西(账号密码)都是可见的;
  3. Header内容过大:通常,客户端的请求header变化较小,但是每次都要携带大量的header信息,导致传输成本增大;
  4. keepalive压力过大:持久连接虽然有一点的优点,但同时也会给服务器造成大量的性能压力,特别是传输图片的时候。

BTW:明文传输有多危险,可以去试试,下面是某个政府网站,采用wireshark抓包,身份证、电话号码、住址什么的全暴露出来,所以,,,只要在路由器做点小动作,你的信息是全部能拿得到的,毕竟政府。

由于涉及的隐私太多,打了马赛克

三、HTTPS

由于HTTP报文的不安全性,网景在1994年就创建了HTTPS,并用在浏览器中。最初HTTPS是和SSL一起使用,然后演化为TLS。SSL/TLS在OSI模型中都是表示层的协议。SSL使 用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。

3.1 SSL/TLS

SSL(Secure Sockets Layer),简称安全套接入层,最初由上世纪90年代由网景公司设计。开启 SSL 会增加内存、CPU、网络带宽的开销,后二者跟你使用的 cipher suite 密切相关,其中参数很多,很难一概而论。开启 SSL 的前提是你的 cert 和 key 必须放在 TCP endpoint,你是否信得过那台设备。
TLS(Transport Layer Security),简称安全传输层协议,该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面,与具体的应用无关,所以,一般把TLS协议归为传输层安全协议。
由于本人在加密算法上面知识匮乏,就不误人子弟了,有兴趣可以看看百度百科里的资料,SSL,TLS

3.2 SPDY

2012年google提出了SPDY的方案,大家才开始从正面看待和解决老版本HTTP协议本身的问题,SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:
降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。SPDY构成图。

3.3 HTTPS报文分析

跟之前的报文分析一样,我们使用wireshark来抓包分析,以在百度上搜索点东西为例。

192.168.1.103为本地电脑的ip地址,14.215.177.39为百度服务器地址。下面是步骤:

  1. 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
  2. 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。之后服务器发送 Certificate 报文。报文中包含公开密钥证书。最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
  3. SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。
  4. 服务器同样发送 Change Cipher Spec 报文。 服务器同样发送 Finished 报文。
  5. 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。 应用层协议通信,即发送 HTTP 响应。
    当然,用一张图更容易解释
    简单地说就是下面。

当我们追踪流的数据的时候,可以看到,基本上都是乱码,经过加密,数据是看不到,如果需要在wireshark上看到,则需要在wireshark中配置ssl。

3.4 HTTPS全站化

现今,感觉只要和商业利益有关的,就不得不涉及到加密这类东西。淘宝、京东、唯品会这些电商可谓是最早推行全站https的,这类电商是离用户金钱最近的企业。截止今年底,基本所有商业网站也基本实现了HTTPS。。。。至于小站点,比如个人网站,玩玩还是可以的。如果一个网站需要由HTTP全部变为HTTPS,那么需要关注下面几点:

  1. CA证书,大部分证书都是需要收费的,当然,自己在服务器上用openssl也可以,不过浏览器会提示当前私密连接不安全这个警告,普通人看到这种信息是不会继续浏览的,所以,想使用HTTPS,可以使用Let's Encrypt,由谷歌等公司推行。
  2. HTTPS性能优化,SSL握手,HTTPS 对速度会有一定程度的降低,但是只要经过合理优化和部署,HTTPS 对速度的影响完全可以接受。
  3. CPU计算压力,HTTPS中大量的秘钥算法计算,对CPU的压力可想而知。
    至于我自己的个人网站,之前实现了https,用的免费证书,但是由于HTTPS下的网站,所有子链都要使用HTTPS,使用了七牛云的CDN,如果要使用HTTPS加速,是要收费的,所以只能放弃。。。

四、HTTP2.0

HTTP2.0,相较于HTTP1.x,大幅度的提升了web性能。在与HTTP/1.1完全语义兼容的基础上,进一步减少了网络延迟和传输的安全性。HTTP2.0可以说是SPDY的升级版(基于SPDY设计的),但是依然存在一些不同点:HTTP2.0支持明文传输,而SPDY强制使用HTTPS;HTTP2.0消息头的压缩算法采用HPACK,而非SPDY采用的DEFLATE。

4.1 历史

HTTP 2.0在2013年8月进行首次合作共事性测试。在开放互联网上HTTP 2.0将只用于https://网址,而 http://网址将继续使用HTTP/1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。HTTP 2.0是在SPDY(An experimental protocol for a faster web, The Chromium Projects)基础上形成的下一代互联网通信协议。HTTP/2 的目的是通过支持请求与响应的多路复用来较少延迟,通过压缩HTTPS首部字段将协议开销降低,同时增加请求优先级和服务器端推送的支持。

4.2 HTTP2.0新特性

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

4.2.1 二进制帧

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的Request Body则封装到DATA frame里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

4.2.2 多路复用

所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完。

4.2.3 流量控制

TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

4.2.4 服务器端推送

服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

4.2.5 首部压缩

HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。

4.3 HTTP1.1与HTTP2.0的对比

以访问https://http2.akamai.com/demo为例。

4.4 报文

访问https://http2.akamai.com/demo,谷歌浏览器的报文没有显示出协议,此处使用火狐浏览器。
响应头部分如下。

请求头如下。

采用淘宝网站为例,淘宝目前采用主站使用HTTP1.1,资源使用HTTP2.0,少些使用SPDY协议。目前也是业界比较流行的做法。

参考

  1. HTTPS那些事
  2. 如何搭建一个HTTP2.0的网站
  3. HTTP/2.0 相比1.0有哪些重大改进?
  4. HTTP2.0 demo
  5. Http、Https、Http2前身
  6. HTTP报文
  7. HTTP、HTTP2.0、SPDY、HTTPS 你应该知道的一些事
  8. HTTPS权威指南
  9. HTTP2.0的奇妙日常
  10. curl 支持 HTTP2
  11. 淘宝HTTPS探索
  12. HTTPS完全协议详解

欢迎访问我的个人网站。https://wenzhihuai.com

',103)]))}const o=t(n,[["render",h],["__file","IP、HTTP、HTTPS、HTTP2.0.html.vue"]]),T=JSON.parse('{"path":"/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html","title":"TCP/IP、HTTP、HTTPS、HTTP2.0","lang":"zh-CN","frontmatter":{"description":"TCP/IP、HTTP、HTTPS、HTTP2.0 HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"TCP/IP、HTTP、HTTPS、HTTP2.0"}],["meta",{"property":"og:description","content":"TCP/IP、HTTP、HTTPS、HTTP2.0 HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171224034237.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-14T17:59:53.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-14T17:59:53.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"TCP/IP、HTTP、HTTPS、HTTP2.0\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171224034237.png\\",\\"https://github-images.wenzhihuai.com/images/20171221101751.png\\",\\"https://github-images.wenzhihuai.com/images/20171221111615.png\\",\\"https://github-images.wenzhihuai.com/images/20171221043103.png\\",\\"https://github-images.wenzhihuai.com/images/20171224035755.png\\",\\"https://github-images.wenzhihuai.com/images/20171224035850.png\\",\\"https://github-images.wenzhihuai.com/images/20171226081508.png\\",\\"https://github-images.wenzhihuai.com/images/20171224044825.png\\",\\"https://github-images.wenzhihuai.com/images/20171226042016.png\\",\\"https://github-images.wenzhihuai.com/images/20171226044521.png\\",\\"https://github-images.wenzhihuai.com/images/20171226044137.png\\",\\"https://github-images.wenzhihuai.com/images/20171226045845.png\\",\\"https://github-images.wenzhihuai.com/images/20171226103043.png\\",\\"https://github-images.wenzhihuai.com/images/20171226105514.png\\",\\"https://github-images.wenzhihuai.com/images/20171226051221.png\\",\\"https://github-images.wenzhihuai.com/images/20171226071845.png\\",\\"https://github-images.wenzhihuai.com/images/20171226075106.png\\",\\"https://github-images.wenzhihuai.com/images/20171226065216.png\\"],\\"dateModified\\":\\"2024-02-14T17:59:53.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、TCP/IP","slug":"一、tcp-ip","link":"#一、tcp-ip","children":[]},{"level":2,"title":"二、HTTP","slug":"二、http","link":"#二、http","children":[{"level":3,"title":"2.1 HTTP报文分析","slug":"_2-1-http报文分析","link":"#_2-1-http报文分析","children":[]},{"level":3,"title":"2.2 HTTP特性","slug":"_2-2-http特性","link":"#_2-2-http特性","children":[]},{"level":3,"title":"2.3 影响HTTP的因素","slug":"_2-3-影响http的因素","link":"#_2-3-影响http的因素","children":[]}]},{"level":2,"title":"三、HTTPS","slug":"三、https","link":"#三、https","children":[{"level":3,"title":"3.1 SSL/TLS","slug":"_3-1-ssl-tls","link":"#_3-1-ssl-tls","children":[]},{"level":3,"title":"3.2 SPDY","slug":"_3-2-spdy","link":"#_3-2-spdy","children":[]},{"level":3,"title":"3.3 HTTPS报文分析","slug":"_3-3-https报文分析","link":"#_3-3-https报文分析","children":[]},{"level":3,"title":"3.4 HTTPS全站化","slug":"_3-4-https全站化","link":"#_3-4-https全站化","children":[]}]},{"level":2,"title":"四、HTTP2.0","slug":"四、http2-0","link":"#四、http2-0","children":[{"level":3,"title":"4.1 历史","slug":"_4-1-历史","link":"#_4-1-历史","children":[]},{"level":3,"title":"4.2 HTTP2.0新特性","slug":"_4-2-http2-0新特性","link":"#_4-2-http2-0新特性","children":[]},{"level":3,"title":"4.3 HTTP1.1与HTTP2.0的对比","slug":"_4-3-http1-1与http2-0的对比","link":"#_4-3-http1-1与http2-0的对比","children":[]},{"level":3,"title":"4.4 报文","slug":"_4-4-报文","link":"#_4-4-报文","children":[]}]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1707203159000,"updatedTime":1707933593000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":19.51,"words":5852},"filePathRelative":"java/网络/IP、HTTP、HTTPS、HTTP2.0.md","localizedDate":"2024年2月6日","excerpt":"\\n

HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

","autoDesc":true}');export{o as comp,T as data}; diff --git "a/assets/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html-BtZgPFK4.js" "b/assets/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html-BtZgPFK4.js" new file mode 100644 index 00000000..8bfbfe3e --- /dev/null +++ "b/assets/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html-BtZgPFK4.js" @@ -0,0 +1,24 @@ +import{_ as i,c as a,d as e,o as n}from"./app-ftEjETWs.js";const l={};function t(r,s){return n(),a("div",null,s[0]||(s[0]=[e(`

JVM调优参数

一、堆大小设置

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

典型设置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

二、回收器选择

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

2.1 吞吐量优先的并行收集器

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

2.2 响应时间优先的并发收集器

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
典型配置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

三、辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-XX:+PrintGC
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
                [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
输出形式:Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
输出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
输出形式:

34.702: [GC {Heap before gc invocations=7:
+ def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
+from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
+  to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
+ tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
+ def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
+  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
+  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
+ tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+}
+, 0.0757599 secs]

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
常见配置汇总

3.1 堆设置

-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

3.2 收集器设置

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器

3.3 垃圾回收统计信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

3.4 并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

3.5 并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结

4.1 年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

4.2 年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息
持久代并发收集次数
传统GC信息
花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

4.3 较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

`,49)]))}const d=i(l,[["render",t],["__file","JVM调优参数.html.vue"]]),p=JSON.parse('{"path":"/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html","title":"JVM调优参数","lang":"zh-CN","frontmatter":{"description":"JVM调优参数 一、堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"JVM调优参数"}],["meta",{"property":"og:description","content":"JVM调优参数 一、堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T04:00:26.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T04:00:26.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"JVM调优参数\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T04:00:26.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、堆大小设置","slug":"一、堆大小设置","link":"#一、堆大小设置","children":[{"level":3,"title":"典型设置:","slug":"典型设置","link":"#典型设置","children":[]}]},{"level":2,"title":"二、回收器选择","slug":"二、回收器选择","link":"#二、回收器选择","children":[{"level":3,"title":"2.1 吞吐量优先的并行收集器","slug":"_2-1-吞吐量优先的并行收集器","link":"#_2-1-吞吐量优先的并行收集器","children":[]},{"level":3,"title":"2.2 响应时间优先的并发收集器","slug":"_2-2-响应时间优先的并发收集器","link":"#_2-2-响应时间优先的并发收集器","children":[]}]},{"level":2,"title":"三、辅助信息","slug":"三、辅助信息","link":"#三、辅助信息","children":[{"level":3,"title":"3.1 堆设置","slug":"_3-1-堆设置","link":"#_3-1-堆设置","children":[]},{"level":3,"title":"3.2 收集器设置","slug":"_3-2-收集器设置","link":"#_3-2-收集器设置","children":[]},{"level":3,"title":"3.3 垃圾回收统计信息","slug":"_3-3-垃圾回收统计信息","link":"#_3-3-垃圾回收统计信息","children":[]},{"level":3,"title":"3.4 并行收集器设置","slug":"_3-4-并行收集器设置","link":"#_3-4-并行收集器设置","children":[]},{"level":3,"title":"3.5 并发收集器设置","slug":"_3-5-并发收集器设置","link":"#_3-5-并发收集器设置","children":[]}]},{"level":2,"title":"四、调优总结","slug":"四、调优总结","link":"#四、调优总结","children":[{"level":3,"title":"4.1 年轻代大小选择","slug":"_4-1-年轻代大小选择","link":"#_4-1-年轻代大小选择","children":[]},{"level":3,"title":"4.2 年老代大小选择","slug":"_4-2-年老代大小选择","link":"#_4-2-年老代大小选择","children":[]},{"level":3,"title":"4.3 较小堆引起的碎片问题","slug":"_4-3-较小堆引起的碎片问题","link":"#_4-3-较小堆引起的碎片问题","children":[]}]}],"git":{"createdTime":1579957849000,"updatedTime":1708056026000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":9.13,"words":2739},"filePathRelative":"java/JVM/JVM调优参数.md","localizedDate":"2020年1月25日","excerpt":"\\n

一、堆大小设置

\\n

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

","autoDesc":true}');export{d as comp,p as data}; diff --git "a/assets/Java\345\206\205\345\255\230\346\250\241\345\236\213.html-Bis3Hglq.js" "b/assets/Java\345\206\205\345\255\230\346\250\241\345\236\213.html-Bis3Hglq.js" new file mode 100644 index 00000000..afadc5fc --- /dev/null +++ "b/assets/Java\345\206\205\345\255\230\346\250\241\345\236\213.html-Bis3Hglq.js" @@ -0,0 +1,150 @@ +import{_ as s,c as a,d as e,o as n}from"./app-ftEjETWs.js";const l={};function t(h,i){return n(),a("div",null,i[0]||(i[0]=[e(`

Java内存模型(JMM)

本文转载自深入理解JVM-内存模型(jmm)和GC

1 CPU和内存的交互

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型


在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。

但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况。现在cpu和内存的交互大致如下。

cpu、缓存、内存
cpu、缓存、内存

cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性

在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。
以我的pc为例,因为cpu成本高,缓存区一般也很小。

image-20240216172615735
image-20240216172615735
CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。

如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性?

为了解决这个问题,各个处理器需遵循一些协议保证一致性。【如MSI,MESI啥啥的协议。。】

大概如下

cpu与内存.png
cpu与内存.png

在CPU层面,内存屏障提供了个充分必要条件

1.1.1 内存屏障(Memory Barrier)

CPU中,每个CPU又有多级缓存【上图统一定义为高速缓存】,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

为什么需要内存屏障
由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.
+简单来说:
+1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。
+2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。
+对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。
内存屏障的作用
cpu执行指令可能是无序的,它有两个比较重要的作用
+1.阻止屏障两侧指令重排序
+2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

volatile型变量

当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。

用代码可以这么理解

//相当于读写时加锁,保证及时可见性,并发时不被随意修改。
+public class SynchronizedInteger {
+  private long value;
+
+  public synchronized int get() {
+    return value;
+  }
+
+  public synchronized void set(long value) {
+    this.value = value;
+  }
+}

volatile型变量拥有如下特性

可见性,对于一个该变量的读,一定能看到读之前最后的写入。
+防止指令重排序,执行代码时,为了提高执行效率,会在不影响最后结果的前提下对指令进行重新排序,使用volatile可以防止,比如单例模式双重校验锁的创建中有使用到,如(https://www.jianshu.com/p/b30a4d568be4)
+
+注意的是volatile不具有原子性,如volatile++这样的复合操作,这里感谢大家的指正。

至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。简单画了个图。

img
img

2. Java内存区域

前提:本文讲的基本都是以Sun HotSpot虚拟机为基础的,Oracle收购了Sun后目前得到了两个【Sun的HotSpot和JRockit(以后可能合并这两个),还有一个是IBM的IBMJVM】

之所以扯了那么多计算机内存模型,是因为java内存模型的设定符合了计算机的规范。

Java程序内存的分配是在JVM虚拟机内存分配机制下完成

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

从下面这张图可以看出来,Java数据区域分为五大数据区域。这些区域各有各的用途,创建及销毁时间。

其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的。

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。

jmm
jmm

2.1 五大内存区域

2.1.1 程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器

我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

2.1.2 Java栈(虚拟机栈)

同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】

对于栈帧的解释参考 Java虚拟机运行时栈帧结构

栈帧: 是用来存储数据和部分过程结果的数据结构。
+栈帧的位置:  内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
+栈帧大小确定时间: 编译期确定,不受运行期数据影响。

通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址【深入理解Java虚拟机】怎么理解returnAddress

栈帧
栈帧

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

2.1.3 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

2.1.4 堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

参考逃逸分析

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

2.1.5 方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池

是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk1.7开始逐步去永久代。从String.interns()方法可以看出来
+String.interns()
+native方法:作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象,在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小。
public class StringIntern {
+    //运行如下代码探究运行时常量池的位置
+    public static void main(String[] args) throws Throwable {
+        //用list保持着引用 防止full gc回收常量池
+        List<String> list = new ArrayList<String>();
+        int i = 0;
+        while(true){
+            list.add(String.valueOf(i++).intern());
+        }
+    }
+}
+//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代
+//如果是jdk1.7或1.8环境下运行 同时限制堆的大小  将报heap space 即常量池在堆中

idea设置相关内存大小设置

这边不用全局的方式,设置main方法的vm参数。

做相关设置,比如说这边设定堆大小。(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)

这边如果不设置UseGCOverheadLimit将报java.lang.OutOfMemoryError: GC overhead limit exceeded,
+这个错是因为GC占用了多余98%(默认值)的CPU时间却只回收了少于2%(默认值)的堆空间。目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。虽然加大内存可以暂时解决这个问题,但是还是强烈建议去优化代码,后者更加有效,也可通过UseGCOverheadLimit避免[不推荐,这里是因为测试用,并不能解决根本问题]
img
img
img
img

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

2.2 对象的内存布局

在HotSpot虚拟机中。对象在内存中存储的布局分为

1.对象头
+2.实例数据
+3.对齐填充
2.2.1 对象头【markword】

在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。
+为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息,
假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

不同状态下存放数据

img
img

这其中锁标识位需要特别关注下。锁标志位与是否为偏向锁对应到唯一的锁状态

锁的状态分为四种无锁状态偏向锁轻量级锁重量级锁

不同状态时对象头的区间含义,如图所示。

对象头.jpg
对象头.jpg

HotSpot底层通过markOop实现Mark Word,具体实现位于markOop.hpp文件。

markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。

关于对象头和锁之间的转换,网上大神总结

偏向锁轻量级锁重量级锁.png
偏向锁轻量级锁重量级锁.png
2.2.2 实例数据
存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。
+分配策略:相同宽度的字段总是放在一起,比如double和long
2.2.3 对齐填充

这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。

由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。

2.3 对象的访问定位

java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
+1.句柄访问对象
+2.直接指针访问对象。(Sun HotSpot使用这种方式)

参考Java对象访问定位

2.3.1 句柄访问

简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。

优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。
访问方式2.jpg
访问方式2.jpg
2.3.2 直接指针

与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。

优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】

访问方式1.jpg
访问方式1.jpg

3.内存溢出

两种内存溢出异常[注意内存溢出是error级别的]
+1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度
+2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

java -verbose:class -version 可以查看刚开始加载的类,可以发现这两个类并不是异常出现的时候才去加载,而是jvm启动的时候就已经加载。这么做的原因是在vm启动过程中我们把类加载起来,并创建几个没有堆栈的对象缓存起来,只需要设置下不同的提示信息即可,当需要抛出特定类型的OutOfMemoryError异常的时候,就直接拿出缓存里的这几个对象就可以了。

比如说OutOfMemoryError对象,jvm预留出4个对象【固定常量】,这就为什么最多出现4次有堆栈的OutOfMemoryError异常及大部分情况下都将看到没有堆栈的OutOfMemoryError对象的原因。

参考OutOfMemoryError解读

Snip20180904_8.png
Snip20180904_8.png

两个基本的例子

public class MemErrorTest {
+    public static void main(String[] args) {
+        try {
+            List<Object> list = new ArrayList<Object>();
+            for(;;) {
+                list.add(new Object()); //创建对象速度可能高于jvm回收速度
+            }
+        } catch (OutOfMemoryError e) {
+            e.printStackTrace();
+        }
+
+        try {
+            hi();//递归造成StackOverflowError 这边因为每运行一个方法将创建一个栈帧,栈帧创建太多无法继续申请到内存扩展
+        } catch (StackOverflowError e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    public static void hi() {
+        hi();
+    }
+}
img
img

4.GC简介

GC(Garbage Collection):即垃圾回收器,诞生于1960年MIT的Lisp语言,主要是用来回收,释放垃圾占用的空间。


java GC泛指java的垃圾回收机制,该机制是java与C/C++的主要区别之一,我们在日常写java代码的时候,一般都不需要编写内存回收或者垃圾清理的代码,也不需要像C/C++那样做类似delete/free的操作。

4.1.为什么需要学习GC

对象的内存分配在java虚拟机的自动内存分配机制下,一般不容易出现内存泄漏问题。但是写代码难免会遇到一些特殊情况,比如OOM神马的。。尽管虚拟机内存的动态分配与内存回收技术很成熟,可万一出现了这样那样的内存溢出问题,那么将难以定位错误的原因所在。

对于本人来说,由于水平有限,而且作为小开发,并没必要深入到GC的底层实现,但至少想要说学会看懂gc及定位一些内存泄漏问题。

从三个角度切入来学习GC

1.哪些内存要回收

2.什么时候回收

3.怎么回收

哪些内存要回收

java内存模型中分为五大区域已经有所了解。我们知道程序计数器虚拟机栈本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

Java堆方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,gc进行回收前,第一件事就是确认哪些对象存活,哪些死去[即不可能再被引用]

4.2 堆的回收区域

为了高效的回收,jvm将堆分为三个区域
+1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
+2.老年代(Old Generation)
+3.永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】

GC为什么要分代-R大的回答

关于元空间

5 判断对象是否存活算法

1.引用计数算法
+早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
+优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
+缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
+
+2.可达性分析算法
+目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
+它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
gc.png
gc.png

可作为GC Roots的对象有四种

①虚拟机栈(栈桢中的本地变量表)中的引用的对象。
+②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
+③方法区中的常量引用的对象,
+④本地方法栈中JNI(native方法)引用的对象

即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。网上例子很多,基本上和深入理解JVM一书讲的一样对象的生存还是死亡

要真正宣告对象死亡需经过两个过程。
+1.可达性分析后没有发现引用链
+2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]

HotSpot虚拟机如何实现可达性算法

5 垃圾收集算法

jvm中,可达性分析算法帮我们解决了哪些对象可以回收的问题,垃圾收集算法则关心怎么回收。

5.1 三大垃圾收集算法

1.标记/清除算法最基础
+2.复制算法
+3.标记/整理算法
+jvm采用\`分代收集算法\`对不同区域采用不同的回收算法

参考GC算法深度解析

新生代采用复制算法

新生代中因为对象都是"朝生夕死的",【深入理解JVM虚拟机上说98%的对象,不知道是不是这么多,总之就是存活率很低】,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。

老年代采用标记/清除算法标记/整理算法

由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

5.2 枚举根节点算法

GC Roots 被虚拟机用来判断对象是否存活

可作为GC Roos的节点主要是在一些全局引用【如常量或静态属性】、执行上下文【如栈帧中本地变量表】中。那么如何在这么多全局变量和本地变量表找到【枚举】根节点将是个问题。

可达性分析算法需考虑

1.如果方法区几百兆,一个个检查里面的引用,将耗费大量资源。

2.在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。【因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程】

解决办法:实际上当系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。

虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】

OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。

这个时候有个safepoint(安全点)的概念。

HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。

safepoint不能太少,否则GC等待的时间会很久

safepoint不能太多,否则将增加运行GC的负担

安全点主要存放的位置

1:循环的末尾 
+2:方法临返回前/调用方法的call指令后 
+3:可能抛异常的位置

参考:关于安全点safepoint

6.垃圾收集器

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。
年轻代收集器
+Serial、ParNew、Parallel Scavenge
+老年代收集器
+Serial Old、Parallel Old、CMS收集器
+特殊收集器
+G1收集器[新型,不在年轻、老年代范畴内]
img
img

收集器,连线代表可结合使用

新生代收集器

6.1 Serial

最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。

虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器[简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式]

JVM的Client模式与Server模式

优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。

6.2 ParNew收集器

可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个,超线程cpu的话就不清楚了 - -】,如果cpu核数很多不想用那么多,可以通过*-XX:ParallelGCThreads*来控制垃圾收集线程的数量。

优点:
+1.支持多线程,多核CPU下可以充分的利用CPU资源
+2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
+
+缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。

6.3 Parallel Scavenge

采用复制算法的收集器,和ParNew一样支持多线程。

但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】

对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。

对于后台

高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算

Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。

老年代收集器

6.4 Serial Old

和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。

如果是Server模式有两大用途

1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。

2.作为CMS收集器的后备。

6.5 Parallel Old

支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】

在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【根据图,没有这个的话只剩Serial Old,而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old为单线程Server模式下会拖后腿【多核cpu下无法充分利用】,这种结合并不能让应用的吞吐量最大化。

Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。

6.6 CMS

CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】

启用CMS:-XX:+UseConcMarkSweepGC

正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发(Concurrent)的

它的运作分为4个阶段

1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
+2.并发标记:GC Roots Tarcing过程,即可达性分析
+3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
+4.并发清除

以上初始标记和重新标记需要stw(停掉其它运行java线程)

之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。

总体上CMS是款优秀的收集器,但是它也有些缺点。

1.cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。

2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC

3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。

6.7 G1收集器

G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)

g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

用到的算法为标记-清理、复制算法

jdk1.7,1.8的都是默认关闭的,更高版本的还不知道
+开启选项 -XX:+UseG1GC 
+比如在tomcat的catania.sh启动参数加上

g1是区域化的,它将java堆内存划分为若干个大小相同的区域【region】,jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。

jdk7中计算region的源码,这边博主看了下也看不怎么懂,也翻了下openjdk8的看了下关于region的处理似乎不太一样。。

g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。

g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。

g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。

几个重要的默认值,更多的查看官方文档oracle官方g1中文文档

g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作
+-XX:G1HeapRegionSize=n  设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域
+-XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒

7 Minor GC、Major GC、FULL GC、mixed gc

7.1 Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.

7.2 Major GC

Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。

7.3 Full GC

full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】

7.4 mixed GC【g1特有】

混合GC

收集整个young gen以及部分old gen的GC。只有G1有这个模式

8 查看GC日志

8.1 简单日志查看

要看得懂并理解GC,需要看懂GC日志。

这边我在idea上试了个小例子,需要在idea配置参数(-XX:+PrintGCDetails)。

img
img
public class GCtest {
+    public static void main(String[] args) {
+        for(int i = 0; i < 10000; i++) {
+            List<String> list = new ArrayList<>();
+            list.add("aaaaaaaaaaaaa");
+        }
+        System.gc();
+    }
+}
[GC (System.gc()) [PSYoungGen: 3998K->688K(38400K)] 3998K->696K(125952K), 0.0016551 secs[本次回收时间]] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+[Full GC (System.gc()) [PSYoungGen: 688K->0K(38400K)] [ParOldGen: 8K->603K(87552K)] 696K->603K(125952K), [Metaspace: 3210K->3210K(1056768K)], 0.0121034 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
+Heap
+ PSYoungGen[年轻代]      total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
+  eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000)
+  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
+  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
+ ParOldGen[老年代]       total 87552K, used 603K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
+  object space 87552K, 0% used [0x0000000740000000,0x0000000740096fe8,0x0000000745580000)
+ Metaspace[元空间]      used 3217K, capacity 4496K, committed 4864K, reserved 1056768K
+  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

8.2 离线工具查看

比如sun的gchistogcviewer离线分析工具,做个笔记先了解下还没用过,可视化好像很好用的样子。

8.3 自带的jconsole工具、jstat命令

终端输入jconsole就会出现jdk自带的gui监控工具

jconsole
jconsole

可以根据内存使用情况间接了解内存使用和gc情况

jconsole
jconsole

jstat命令

比如jstat -gcutil pid查看对应java进程gc情况

jstat
jstat
s0: 新生代survivor space0简称 就是准备复制的那块 单位为%
+s1:指新生代s1已使用百分比,为0的话说明没有存活对象到这边
+e:新生代eden(伊甸园)区域(%)
+o:老年代(%)
+ygc:新生代  次数
+ygct:minor gc耗时
+fgct:full gc耗时(秒)
+GCT: ygct+fgct 耗时

几个疑问

1.GC是怎么判断对象是被标记的

通过枚举根节点的方式,通过jvm提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存里的东西,而是通过OOPMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上面引用的信息并记录下来。

总结:通过OOPMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮HotSpot实现准确式GC【这边的准确关键就是类型,可以根据给定位置的某块数据知道它的准确类型,HotSpot是通过oopMap外部记录下这些信息,存成映射表一样的东西】。

2.什么时候触发GC

简单来说,触发的条件就是GC算法区域满了或将满了。

minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高]
+
+full GC:
+①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
+②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
+③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
+④CMS GC时出现Promotion Faield[pf]
+⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
+这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。
+比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)
Promotion Faield:minor GC时 survivor space放不下[满了或对象太大],对象只能放到老年代,而老年代也放不下会导致这个错误。
+Concurrent Model Failure:cms时特有的错误,因为cms时垃圾清理和用户线程可以是并发执行的,如果在清理的过程中
+可能原因:
+1 cms触发太晚,可以把XX:CMSInitiatingOccupancyFraction调小[比如-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)]
+2 垃圾产生速度大于清理速度,可能是晋升阈值设置过小,Survivor空间小导致跑到老年代,eden区太小,存在大对象、数组对象等情况
+3.空间碎片过多,可以开启空间碎片整理并合理设置周期时间

full gc导致了concurrent mode failure,而不是因为concurrent mode failure错误导致触发full gc,真正触发full gc的原因可能是ygc时发生的promotion failure。

3.cms收集器是否会扫描年轻代

会,在初始标记的时候会扫描新生代。

虽然cms是老年代收集器,但是我们知道年轻代的对象是可以晋升为老年代的,为了空间分配担保,还是有必要去扫描年轻代。

4.什么是空间分配担保

在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的,

如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。

如果允许:继续检查老年代最大可用可用的连续空间是否大于之前晋升的平均大小,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。

如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】

这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。

这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC,

5.为什么复制算法要分两个Survivor,而不直接移到老年代

这样做的话效率可能会更高,但是old区一般都是熬过多次可达性分析算法过后的存活的对象,要求比较苛刻且空间有限,而不能直接移过去,这将导致一系列问题(比如老年代容易被撑爆)

分两个Survivor(from/to),自然是为了保证复制算法运行以提高效率。

6.各个版本的JVM使用的垃圾收集器是怎么样的

准确来说,垃圾收集器的使用跟当前jvm也有很大的关系,比如说g1是jdk7以后的版本才开始出现。

并不是所有的垃圾收集器都是默认开启的,有些得通过设置相应的开关参数才会使用。比如说cms,需设置(XX:+UseConcMarkSweepGC)

这边有几个实用的命令,比如说server模式下

#UnlockExperimentalVMOptions UnlockDiagnosticVMOptions解锁获取jvm参数,PrintFlagsFinal用于输出xx相关参数,以Benchmark类测试,这边会有很多结果 大都看不懂- - 在这边查(usexxxxxxgc会看到jvm不同收集器的开关情况)
+java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark
+
+#后面跟| grep ":"获取已赋值的参数[加:代表被赋值过]
+java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark| grep ":"
+
+#获得用户自定义的设置或者jvm设置的详细的xx参数和值
+java -server -XX:+PrintCommandLineFlags Benchmark
img
img

本人用的jdk8,这边UseParallelGC为true,参考深入理解jvm那本书说这个是Parallel Scavenge+Serial old搭配组合的开关,但是网上又说8默认是Parallel Scavenge+Parallel Old,我还是信书的吧 - -。

更多相关参数来源

img
img

常用参数

据说更高版本的jvm默认使用g1

7 stop the world具体是什么,有没有办法避免

stop the world简单来说就是gc的时候,停掉除gc外的java线程。

无论什么gc都难以避免停顿,即使是g1也会在初始标记阶段发生,stw并不可怕,可以尽可能的减少停顿时间。

8 新生代什么样的情况会晋升为老年代

对象优先分配在eden区,eden区满时会触发一次minor GC

对象晋升规则
1 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。
2 对象太大新生代无法容纳则会分配到老年代
3 eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。
4 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代

8.怎么理解g1,适用于什么场景

G1 GC 是区域化、并行-并发、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可预测的暂停。增量的特性使 G1 GC 适用于更大的堆,在最坏的情况下仍能提供不错的响应。G1 GC 的自适应特性使 JVM 命令行只需要软实时暂停时间目标的最大值以及 Java 堆大小的最大值和最小值,即可开始工作。

g1不再区分老年代、年轻代这样的内存空间,这是较以往收集器很大的差异,所有的内存空间就是一块划分为不同子区域,每个区域大小为1m-32m,最多支持的内存为64g左右,且由于它为了的特性适用于大内存机器。

g1回收时堆内存情况
g1回收时堆内存情况

适用场景:

1.像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。

2.面向服务端,大内存,高cpu的应用机器。【网上说差不多是6g或更大】

3.应用在运行过程中经常会产生大量内存碎片,需要压缩空间【比cms好的地方之一,g1具备压缩功能】。

参考

深入理解Java虚拟机

JVM内存模型、指令重排、内存屏障概念解析

Java对象头

GC收集器

Major GC和Full GC的区别

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

关于准确式GC、保守式GC

关于CMS垃圾收集算法的一些疑惑

图解cms

G1垃圾收集器介绍

详解cms回收机制

总结

JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题,而且写java代码的时候难免会经常和内存打交道,遇到各种内存溢出问题,有时候又难以定位问题,因此是一定要学习jmm以及GC的。

`,308)]))}const r=s(l,[["render",t],["__file","Java内存模型.html.vue"]]),k=JSON.parse('{"path":"/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html","title":"Java内存模型(JMM)","lang":"zh-CN","frontmatter":{"description":"Java内存模型(JMM) 本文转载自深入理解JVM-内存模型(jmm)和GC 1 CPU和内存的交互 了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】 有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型 在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Java内存模型(JMM)"}],["meta",{"property":"og:description","content":"Java内存模型(JMM) 本文转载自深入理解JVM-内存模型(jmm)和GC 1 CPU和内存的交互 了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】 有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型 在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/10006199-d3fc8462f127a2c7-20240216171550523.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:55:09.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:55:09.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Java内存模型(JMM)\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/10006199-d3fc8462f127a2c7-20240216171550523.jpg\\",\\"https://github-images.wenzhihuai.com/images/image-20240216172615735.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-0a3299bca20f13ad.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-61e3c01d0b265732.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-a4108d8fb7810a71.jpeg\\",\\"https://github-images.wenzhihuai.com/images/10006199-728567b81e7abff5.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-b55cc68293d1807d.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-76054110706ff110.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-b0fd456c33c09fce.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-9b3fe05daab42136.jpg\\",\\"https://github-images.wenzhihuai.com/images/10006199-318ad80ccb29abe4.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-27ef5c978077ed1c.jpg\\",\\"https://github-images.wenzhihuai.com/images/10006199-6cefc46d23c2d549.jpg\\",\\"https://github-images.wenzhihuai.com/images/10006199-07496d628d676815.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-04a7ea1247b98809.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-854e1de91f66764b.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-975ca350889de014.jpg\\",\\"https://github-images.wenzhihuai.com/images/10006199-239c6e3a1d84a447.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-d409f452f8364937.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-c0eb6418cf4bade9.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-6ed3ee78469592e8.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-a3524986c654e356.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-324780351133d59a.png\\",\\"https://github-images.wenzhihuai.com/images/10006199-8c124d281e0c6dd1.png\\"],\\"dateModified\\":\\"2024-02-16T09:55:09.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1 CPU和内存的交互","slug":"_1-cpu和内存的交互","link":"#_1-cpu和内存的交互","children":[{"level":3,"title":"1.1.1 内存屏障(Memory Barrier)","slug":"_1-1-1-内存屏障-memory-barrier","link":"#_1-1-1-内存屏障-memory-barrier","children":[]}]},{"level":2,"title":"2. Java内存区域","slug":"_2-java内存区域","link":"#_2-java内存区域","children":[{"level":3,"title":"2.1 五大内存区域","slug":"_2-1-五大内存区域","link":"#_2-1-五大内存区域","children":[]}]},{"level":2,"title":"3.内存溢出","slug":"_3-内存溢出","link":"#_3-内存溢出","children":[]},{"level":2,"title":"4.GC简介","slug":"_4-gc简介","link":"#_4-gc简介","children":[]},{"level":2,"title":"4.1.为什么需要学习GC","slug":"_4-1-为什么需要学习gc","link":"#_4-1-为什么需要学习gc","children":[]},{"level":2,"title":"4.2 堆的回收区域","slug":"_4-2-堆的回收区域","link":"#_4-2-堆的回收区域","children":[]},{"level":2,"title":"5 判断对象是否存活算法","slug":"_5-判断对象是否存活算法","link":"#_5-判断对象是否存活算法","children":[]},{"level":2,"title":"5 垃圾收集算法","slug":"_5-垃圾收集算法","link":"#_5-垃圾收集算法","children":[{"level":3,"title":"5.1 三大垃圾收集算法","slug":"_5-1-三大垃圾收集算法","link":"#_5-1-三大垃圾收集算法","children":[]},{"level":3,"title":"5.2 枚举根节点算法","slug":"_5-2-枚举根节点算法","link":"#_5-2-枚举根节点算法","children":[]}]},{"level":2,"title":"6.垃圾收集器","slug":"_6-垃圾收集器","link":"#_6-垃圾收集器","children":[{"level":3,"title":"新生代收集器","slug":"新生代收集器","link":"#新生代收集器","children":[]},{"level":3,"title":"6.1 Serial","slug":"_6-1-serial","link":"#_6-1-serial","children":[]},{"level":3,"title":"6.2 ParNew收集器","slug":"_6-2-parnew收集器","link":"#_6-2-parnew收集器","children":[]},{"level":3,"title":"6.3 Parallel Scavenge","slug":"_6-3-parallel-scavenge","link":"#_6-3-parallel-scavenge","children":[]},{"level":3,"title":"老年代收集器","slug":"老年代收集器","link":"#老年代收集器","children":[]}]},{"level":2,"title":"6.4 Serial Old","slug":"_6-4-serial-old","link":"#_6-4-serial-old","children":[]},{"level":2,"title":"6.5 Parallel Old","slug":"_6-5-parallel-old","link":"#_6-5-parallel-old","children":[{"level":3,"title":"6.6 CMS","slug":"_6-6-cms","link":"#_6-6-cms","children":[]},{"level":3,"title":"6.7 G1收集器","slug":"_6-7-g1收集器","link":"#_6-7-g1收集器","children":[]}]},{"level":2,"title":"7 Minor GC、Major GC、FULL GC、mixed gc","slug":"_7-minor-gc、major-gc、full-gc、mixed-gc","link":"#_7-minor-gc、major-gc、full-gc、mixed-gc","children":[{"level":3,"title":"7.1 Minor GC","slug":"_7-1-minor-gc","link":"#_7-1-minor-gc","children":[]},{"level":3,"title":"7.2 Major GC","slug":"_7-2-major-gc","link":"#_7-2-major-gc","children":[]},{"level":3,"title":"7.3 Full GC","slug":"_7-3-full-gc","link":"#_7-3-full-gc","children":[]},{"level":3,"title":"7.4 mixed GC【g1特有】","slug":"_7-4-mixed-gc【g1特有】","link":"#_7-4-mixed-gc【g1特有】","children":[]}]},{"level":2,"title":"8 查看GC日志","slug":"_8-查看gc日志","link":"#_8-查看gc日志","children":[{"level":3,"title":"8.3 自带的jconsole工具、jstat命令","slug":"_8-3-自带的jconsole工具、jstat命令","link":"#_8-3-自带的jconsole工具、jstat命令","children":[]}]},{"level":2,"title":"几个疑问","slug":"几个疑问","link":"#几个疑问","children":[{"level":3,"title":"1.GC是怎么判断对象是被标记的","slug":"_1-gc是怎么判断对象是被标记的","link":"#_1-gc是怎么判断对象是被标记的","children":[]},{"level":3,"title":"2.什么时候触发GC","slug":"_2-什么时候触发gc","link":"#_2-什么时候触发gc","children":[]},{"level":3,"title":"3.cms收集器是否会扫描年轻代","slug":"_3-cms收集器是否会扫描年轻代","link":"#_3-cms收集器是否会扫描年轻代","children":[]},{"level":3,"title":"4.什么是空间分配担保","slug":"_4-什么是空间分配担保","link":"#_4-什么是空间分配担保","children":[]},{"level":3,"title":"5.为什么复制算法要分两个Survivor,而不直接移到老年代","slug":"_5-为什么复制算法要分两个survivor-而不直接移到老年代","link":"#_5-为什么复制算法要分两个survivor-而不直接移到老年代","children":[]},{"level":3,"title":"6.各个版本的JVM使用的垃圾收集器是怎么样的","slug":"_6-各个版本的jvm使用的垃圾收集器是怎么样的","link":"#_6-各个版本的jvm使用的垃圾收集器是怎么样的","children":[]},{"level":3,"title":"7 stop the world具体是什么,有没有办法避免","slug":"_7-stop-the-world具体是什么-有没有办法避免","link":"#_7-stop-the-world具体是什么-有没有办法避免","children":[]},{"level":3,"title":"8 新生代什么样的情况会晋升为老年代","slug":"_8-新生代什么样的情况会晋升为老年代","link":"#_8-新生代什么样的情况会晋升为老年代","children":[]},{"level":3,"title":"8.怎么理解g1,适用于什么场景","slug":"_8-怎么理解g1-适用于什么场景","link":"#_8-怎么理解g1-适用于什么场景","children":[]}]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1708075355000,"updatedTime":1708077309000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":46.11,"words":13833},"filePathRelative":"java/JVM/Java内存模型.md","localizedDate":"2024年2月16日","excerpt":"\\n

本文转载自深入理解JVM-内存模型(jmm)和GC

","autoDesc":true}');export{r as comp,k as data}; diff --git "a/assets/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html--8cnUWGp.js" "b/assets/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html--8cnUWGp.js" new file mode 100644 index 00000000..cc0397d1 --- /dev/null +++ "b/assets/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html--8cnUWGp.js" @@ -0,0 +1,16 @@ +import{_ as s,c as a,d as n,o as e}from"./app-ftEjETWs.js";const t={};function h(l,i){return e(),a("div",null,i[0]||(i[0]=[n(`

Jenkins的一些笔记

公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

一、在全局安全配置中

1.1 启用安全

如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直没有勾选安全域,然后授权策略为登录用户可以做任何事,之后权限这一块就彻底崩溃了,重装了又重装,才知道,需要勾选安全域。

1.2 跨域

同时开启跨站请求伪造保护,Jenkins的一些API需要用到的。

二、获取TOKEN

2.1 TOKEN

Jenkins的用户token可以在用户的设置下面获得,但是这种方式如果需要重装Jenkins的话,就得重新修改一次配置文件

经过对Jenkins-client的抓包分析,token可以由username+":"+password,然后进行base64加密组成,之后在token前面加上"Basic "即可,代码如下:

三、获取Jenkins-Crumb

在远程API调用的时候,Jenkins对于某些接口的要求不仅限于Authorization,还必须要有Jenkins-Crumb,这个东西之前在进行获取的时候,有时候会变来变去,比如用curl命令和f12查看的时候发现不一致,实在受不了,感觉毫无规律可言,之后才发现上面的Authorization来直接调用接口获取的才是正确的,再然后想想,可能是之前调用api的时候,没有开启启用安全,再或者是有没有勾选上使用碎片算法。

另,附上curl查询Jenkins-Crumb的命令:

curl -s 'http://admin:yourtoken@jenkins-url/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'

替换掉yourtoken和jenkins-url即可。

四、值得注意的事

4.1 API设计

Jenkins的API设计可谓是独领风骚,能把一个提交设计成这样真实佩服测试之后才发现只要提交个表单,key为json,value为值即可,其他的都不需要,这个设计我也不知道怎么来的,感觉超级坑。

4.2 生成构建job

由于我们是将Jenkins集成在我们自己的平台里面,并不暴露Jenkins给用户,所以,创建一个job的时候,必须由我们平台的参数往Jenkins里面提交,这一提交,发现的问题不少。
一是Jenkins的整个job的提交是由两步组成的,先是创建job,再提交配置。即:/createItem?name=xxx接口。
二是提交的配置参数,提交的是整个xml,而不是由一个一个参数组成的。对于java来说,就得使用xstream或者其他来转化,甚是折腾,如图这种转化。

4.3 构建的队列

在点击立即构建的时候,Jenkins是没有返回任何信息,但是在Jenkins的内部,它是通过放到队列里等待的,如果有空闲,就开始构建,否则等待,这个队列是可以获取得到的,我们从里面可以获取上一次构建的信息,是成功还是失败。这种情况下,假设我们多个人同时点击,这下子就有点慌了,如何获取到具体某个人的构建结果,有点虐心。想了半天,最终得出的事:代码相同,意味着每次构建的结果相同,为什么要允许多个人同时点击?就这么解决了:从一个job的构建队列中获取最后一次构建的信息,如果是正在构建,那么不允许构建了,直到构建结果出来。

4.4 构建进度的查看

需要将Jenkins中的构建进度移植到我们自有的平台,Jenkins的构建进度时通过ajax轮询实现的,获取文本的规则主要从response header里面的两个字段获取
(1)X-More-Data:是否有更多的数据
(2)X-Text-Size:从开始到该次调用的文本大小
我们是通过websocket来将文本内容推送到前端,使用的stomp协议,部分代码如下:

        while (true) {
+            ...
+            String string = response.body().string();
+            String header = response.header("X-More-Data");
+            if (!Strings.isNullOrEmpty(header) || start == 0) {
+                template.convertAndSend("/topic/" + uuid, string);
+                String textSize = response.header("X-Text-Size");
+                if (!Strings.isNullOrEmpty(textSize)) {
+                    start = Integer.parseInt(textSize);
+                }
+                TimeUnit.SECONDS.sleep(5);
+            } else {
+                template.convertAndSend("/topic/" + uuid, string);
+                return;
+            }
+        }

参考:
1.通过jenkins API去build一个job
2.Jenkins Remote API

`,34)]))}const k=s(t,[["render",h],["__file","Jenkins的一些笔记.html.vue"]]),r=JSON.parse('{"path":"/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html","title":"Jenkins的一些笔记","lang":"zh-CN","frontmatter":{"description":"Jenkins的一些笔记 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。 一、在全局安全配置中 1.1 启用安全 如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Jenkins的一些笔记"}],["meta",{"property":"og:description","content":"Jenkins的一些笔记 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。 一、在全局安全配置中 1.1 启用安全 如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20181027023132911846860.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-24T05:12:56.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-24T05:12:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Jenkins的一些笔记\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20181027023132911846860.png\\",\\"https://github-images.wenzhihuai.com/images/201810270231471528136147.png\\",\\"https://github-images.wenzhihuai.com/images/20181027023526539361439.png\\",\\"https://github-images.wenzhihuai.com/images/20181027024305570167743.png\\",\\"https://github-images.wenzhihuai.com/images/201810270251031329488332.png\\",\\"https://github-images.wenzhihuai.com/images/20181027030059348201424.png\\",\\"https://github-images.wenzhihuai.com/images/20181029112422597829257.png\\"],\\"dateModified\\":\\"2024-02-24T05:12:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、在全局安全配置中","slug":"一、在全局安全配置中","link":"#一、在全局安全配置中","children":[]},{"level":2,"title":"二、获取TOKEN","slug":"二、获取token","link":"#二、获取token","children":[]},{"level":2,"title":"三、获取Jenkins-Crumb","slug":"三、获取jenkins-crumb","link":"#三、获取jenkins-crumb","children":[]},{"level":2,"title":"四、值得注意的事","slug":"四、值得注意的事","link":"#四、值得注意的事","children":[]}],"git":{"createdTime":1708751576000,"updatedTime":1708751576000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.98,"words":1195},"filePathRelative":"kubernetes/Jenkins的一些笔记.md","localizedDate":"2024年2月24日","excerpt":"\\n

公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

","autoDesc":true}');export{k as comp,r as data}; diff --git "a/assets/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html-SN4rN7dm.js" "b/assets/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html-SN4rN7dm.js" new file mode 100644 index 00000000..ea30bd8c --- /dev/null +++ "b/assets/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html-SN4rN7dm.js" @@ -0,0 +1,161 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

Kubernetes容器日志收集

日志采集方式

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

1.原生方式 使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。
2.DaemonSet方式 在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。
3.Sidecar方式 一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。
三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。

一、原生方式


简单的说,原生方式就是直接使用kubectl logs来查看日志,或者将docker的日志通过日志驱动来打到syslog、journal等去,然后再通过命令来排查,这种方式最好的优势就是简单、资源占用率低等,但是,在多容器、弹性伸缩情况下,日志的排查会十分困难,仅仅适用于刚开始研究Kubernetes的公司吧。不过,原生方式确实其他两种方式的基础,因为它的两种最基础的理念,daemonset和sidecar模式都是基于这两种方式而来的。

1.1 控制台stdout方式

这种方式是daemonset方式的基础。将日志全部输出到控制台,然后docker开启journal,然后就能在/var/log/journal下面看到二进制的journal日志,如果要查看二进制的日志的话,可以使用journalctl来查看日志:journalctl -u docker.service -n 1 --no-pager -o json -o json-pretty

{
+        "__CURSOR" : "s=113d7df2f5ff4d0985b08222b365c27a;i=1a5744e3;b=05e0fdf6d1814557939e52c0ac7ea76c;m=5cffae4cd4;t=58a452ca82da8;x=29bef852bcd70ae2",
+        "__REALTIME_TIMESTAMP" : "1559404590149032",
+        "__MONOTONIC_TIMESTAMP" : "399426604244",
+        "_BOOT_ID" : "05e0fdf6d1814557939e52c0ac7ea76c",
+        "PRIORITY" : "6",
+        "CONTAINER_ID_FULL" : "f2108df841b1f72684713998c976db72665f353a3b4ea17cd06b5fc5f0b8ae27",
+        "CONTAINER_NAME" : "k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1",
+        "CONTAINER_TAG" : "f2108df841b1",
+        "CONTAINER_ID" : "f2108df841b1",
+        "_TRANSPORT" : "journal",
+        "_PID" : "6418",
+        "_UID" : "0",
+        "_GID" : "0",
+        "_COMM" : "dockerd-current",
+        "_EXE" : "/usr/bin/dockerd-current",
+        "_CMDLINE" : "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry hub.gcloud.lab --insecure-registry 172.30.0.0/16 --log-level=warn --signature-verification=false --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "_CAP_EFFECTIVE" : "1fffffffff",
+        "_SYSTEMD_CGROUP" : "/system.slice/docker.service",
+        "_SYSTEMD_UNIT" : "docker.service",
+        "_SYSTEMD_SLICE" : "system.slice",
+        "_MACHINE_ID" : "225adcce13bd233a56ab481df7413e0b",
+        "_HOSTNAME" : "dev4.gcloud.set",
+        "MESSAGE" : "I0601 23:56:30.148153       1 event.go:221] Event(v1.ObjectReference{Kind:\\"DaemonSet\\", Namespace:\\"openshift-monitoring\\", Name:\\"node-exporter\\", UID:\\"f6d2bdc1-6658-11e9-aca2-fa163e938959\\", APIVersion:\\"apps/v1\\", ResourceVersion:\\"15378688\\", FieldPath:\\"\\"}): type: 'Normal' reason: 'SuccessfulCreate' Created pod: node-exporter-hvrpf",
+        "_SOURCE_REALTIME_TIMESTAMP" : "1559404590148488"
+}

在上面的json中,_CMDLINE以及其他字段占用量比较大,而且这些没有什么意义,会导致一条简短的日志却被封装成多了几十倍的量,所以的在日志量特别大的情况下,最好进行一下字段的定制,能够减少就减少。
我们一般需要的字段是CONTAINER_NAME以及MESSAGE,通过CONTAINER_NAME可以获取到Kubernetes的namespace和podName,比如CONTAINER_NAME为k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1的时候
container name in pod: controllers
**pod name: **master-controllers-dev4.gcloud.set
namespace: kube-system
**pod uid: **dcab37be702c9ab6c2b17122c867c74a_1

1.2 新版本的subPathExpr

journal方式算是比较标准的方式,如果采用hostPath方式,能够直接将日志输出这里。这种方式唯一的缺点就是在旧Kubernetes中无法获取到podName,但是最新版的Kubernetes1.14的一些特性subPathExpr,就是可以将目录挂载的时候同时将podName写进目录里,但是这个特性仍旧是alpha版本,谨慎使用。
简单说下实现原理:容器中填写的日志目录,挂载到宿主机的/data/logs/namespace/service_name/$(PodName)/xxx.log里面,如果是sidecar模式,则将改目录挂载到sidecar的收集目录里面进行推送。如果是宿主机安装fluentd模式,则需要匹配编写代码实现识别namespace、service_name、PodName等,然后发送到日志系统。

可参考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md
日志落盘参考细节:

    env:
+    - name: POD_NAME
+      valueFrom:
+        fieldRef:
+          apiVersion: v1
+          fieldPath: metadata.name
+   ...
+    volumeMounts:
+    - name: workdir1
+      mountPath: /logs
+      subPathExpr: $(POD_NAME)

我们主要使用了在Pod里的主容器挂载了一个fluent-agent的收集器,来将日志进行收集,其中我们修改了Kubernetes-Client的源码使之支持subPathExpr,然后发送到日志系统的kafka。这种方式能够处理多种日志的收集,比如业务方的日志打到控制台了,但是jvm的日志不能同时打到控制台,否则会发生错乱,所以,如果能够将业务日志挂载到宿主机上,同时将一些其他的日志比如jvm的日志挂载到容器上,就可以使用该种方式。

{
+    "_fileName":"/data/work/logs/epaas_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"rongqiyun-dev",
+    "_podName":"aofjweojo-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}

二、Daemonset方式

daemonset方式也是基于journal,日志使用journal的log-driver,变成二进制的日志,然后在每个node节点上部署一个日志收集的agent,挂载/var/log/journal的日志进行解析,然后发送到kafka或者es,如果节点或者日志量比较大的话,对es的压力实在太大,所以,我们选择将日志推送到kafka。容器日志收集普遍使用fluentd,资源要求较少,性能高,是目前最成熟的日志收集方案,可惜是使用了ruby来写的,普通人根本没时间去话时间学习这个然后进行定制,好在openshift中提供了origin-aggregated-logging方案。
我们可以通过fluent.conf来看origin-aggregated-logging做了哪些工作,把注释,空白的一些东西去掉,然后我稍微根据自己的情况修改了下,结果如下:

@include configs.d/openshift/system.conf
+设置fluent的日志级别
+@include configs.d/openshift/input-pre-*.conf
+最主要的地方,读取journal的日志
+@include configs.d/dynamic/input-syslog-*.conf
+读取syslog,即操作日志
+<label @INGRESS>
+  @include configs.d/openshift/filter-retag-journal.conf
+  进行匹配
+  @include configs.d/openshift/filter-k8s-meta.conf
+  获取Kubernetes的相关信息  
+  @include configs.d/openshift/filter-viaq-data-model.conf
+  进行模型的定义
+  @include configs.d/openshift/filter-post-*.conf
+  生成es的索引id
+  @include configs.d/openshift/filter-k8s-record-transform.conf
+  修改日志记录,我们在这里进行了字段的定制,移除了不需要的字段
+  @include configs.d/openshift/output-applications.conf
+  输出,默认是es,如果想使用其他的比如kafka,需要自己定制
+</label>

当然,细节上并没有那么好理解,换成一步步理解如下:

1. 解析journal日志
origin-aggregated-logging会将二进制的journal日志中的CONTAINER_NAME进行解析,根据匹配规则将字段进行拆解

    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    }

2. es封装
主要用的是elasticsearch_genid_ext插件,写在了filter-post-genid.conf上。

3. 日志分类
通过origin-aggregated-logging来收集journal的日志,然后推送至es,origin-aggregated-logging在推送过程中做了不少优化,即适应高ops的、带有等待队列的、推送重试等,详情可以具体查看一下。

还有就是对日志进行了分类,分为三种:
(1).操作日志(在es中以.operations匹配的),记录了对Kubernetes的操作
(2).项目日志(在es中以project
匹配的),业务日志,日志收集中最重要的
(3).孤儿日志(在es中以.orphaned.*匹配的),没有namespace的日志都会打到这里

4. 日志字段定制
经过origin-aggregated-logging推送至后采集的一条日志如下:

{
+    "CONTAINER_TAG": "4ad125bb7558",
+    "docker": {
+      "container_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c"
+    },
+    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    },
+    "systemd": {
+      "t": {
+        "BOOT_ID": "6246327d7ea441339d6d14b44498b177",
+        "CAP_EFFECTIVE": "1fffffffff",
+        "CMDLINE": "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry 10.77.0.0/16 --log-level=warn --signature-verification=false --bridge=none --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "COMM": "dockerd-current",
+        "EXE": "/usr/bin/dockerd-current",
+        "GID": "0",
+        "MACHINE_ID": "0096083eb4204215a24efd202176f3ec",
+        "PID": "17181",
+        "SYSTEMD_CGROUP": "/system.slice/docker.service",
+        "SYSTEMD_SLICE": "system.slice",
+        "SYSTEMD_UNIT": "docker.service",
+        "TRANSPORT": "journal",
+        "UID": "0"
+      }
+    },
+    "level": "info",
+    "message": "\\tat com.sun.proxy.$Proxy242.execute(Unknown Source)",
+    "hostname": "host11.rqy.kx",
+    "pipeline_metadata": {
+      "collector": {
+        "ipaddr4": "10.76.232.16",
+        "ipaddr6": "fe80::a813:abff:fe66:3b0c",
+        "inputname": "fluent-plugin-systemd",
+        "name": "fluentd",
+        "received_at": "2019-05-15T09:22:39.297151+00:00",
+        "version": "0.12.43 1.6.0"
+      }
+    },
+    "@timestamp": "2019-05-06T01:41:01.960000+00:00",
+    "viaq_msg_id": "NjllNmI1ZWQtZGUyMi00NDdkLWEyNzEtMTY3MDQ0ZjEyZjZh"
+  }

可以看出,跟原生的journal日志类似,增加了几个字段为了写进es中而已,总体而言,其他字段并没有那么重要,所以我们对其中的字段进行了定制,以减少日志的大小,定制化字段之后,一段日志的输出变为(不是同一段,只是举个例子):

{
+    "hostname":"dev18.gcloud.set",
+    "@timestamp":"2019-05-17T04:22:33.139608+00:00",
+    "pod_name":"istio-pilot-8588fcb99f-rqtkd",
+    "appName":"discovery",
+    "container_name":"epaas-discovery",
+    "domain":"istio-system",
+    "sortedId":"NjA3ODVhODMtZDMyYy00ZWMyLWE4NjktZjcwZDMwMjNkYjQ3",
+    "log":"spiffluster.local/ns/istio-system/sa/istio-galley-service-account"
+}

5.部署
最后,在node节点上添加logging-infra-fluentd: "true"的标签,就可以在namespace为openshift-logging中看到节点的收集器了。

logging-fluentd-29p8z              1/1       Running   0          6d
+logging-fluentd-bpkjt              1/1       Running   0          6d
+logging-fluentd-br9z5              1/1       Running   0          6d
+logging-fluentd-dkb24              1/1       Running   1          5d
+logging-fluentd-lbvbw              1/1       Running   0          6d
+logging-fluentd-nxmk9              1/1       Running   1          5d

6.关于ip
业务方不仅仅想要podName,同时还有对ip的需求,控制台方式正常上是没有记录ip的,所以这算是一个难点中的难点,我们在kubernetes_metadata_common.rb的kubernetes_metadata中添加了 'pod_ip' => pod_object['status']['podIP'],最终是有些有ip,有些没有ip,这个问题我们继续排查。

三、Sidecar模式


这种方式的好处是能够获取日志的文件名、容器的ip地址等,并且配置性比较高,能够很好的进行一系列定制化的操作,比如使用log-pilot或者filebeat或者其他的收集器,还能定制一些特定的字段,比如文件名、ip地址等。
sidecar模式用来解决日志收集的问题的话,需要将日志目录挂载到宿主机的目录上,然后再mount到收集agent的目录里面,以达到文件共享的目的,默认情况下,使用emptydir来实现文件共享的目的,这里简单介绍下emptyDir的作用。
EmptyDir类型的volume创建于pod被调度到某个宿主机上的时候,而同一个pod内的容器都能读写EmptyDir中的同一个文件。一旦这个pod离开了这个宿主机,EmptyDir中的数据就会被永久删除。所以目前EmptyDir类型的volume主要用作临时空间,比如Web服务器写日志或者tmp文件需要的临时目录。
日志如果丢失的话,会对业务造成的影响不可估量,所以,我们使用了尚未成熟的subPathExpr来实现,即挂载到宿主的固定目录/data/logs下,然后是namespace,deploymentName,podName,再然后是日志文件,合成一块便是/data/logs/\${namespace}/\${deploymentName}/\${podName}/xxx.log。
具体的做法就不在演示了,这里只贴一下yaml文件。

apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: xxxx
+  namespace: element-dev
+spec:
+  template:
+    spec:
+      volumes:
+        - name: host-log-path-0
+          hostPath:
+            path: /data/logs/element-dev/xxxx
+            type: DirectoryOrCreate
+      containers:
+        - name: xxxx
+          image: 'xxxxxxx'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)
+        - name: xxxx-elog-agent
+          image: 'agent'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)

fluent.conf的配置文件由于保密关系就不贴了,收集后的一条数据如下:

{
+    "_fileName":"/data/work/logs/xxx_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"namespace",
+    "_ip":"10.128.93.31",
+    "_podName":"xxxx-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}

四、总结

总的来说,daemonset方式比较简单,而且适合更加适合微服务化,当然,不是完美的,比如业务方想把业务日志打到控制台上,但是同时也想知道jvm的日志,这种情况下或许sidecar模式更好。但是sidecar也有不完美的地方,每个pod里都要存在一个日志收集的agent实在是太消耗资源了,而且很多问题也难以解决,比如:主容器挂了,agent还没收集完,就把它给kill掉,这个时候日志怎么处理,业务会不会受到要杀掉才能启动新的这一短暂过程的影响等。所以,我们实际使用中首选daemonset方式,但是提供了sidecar模式让用户选择。

参考:
1.Kubernetes日志官方文档
2.Kubernetes日志采集Sidecar模式介绍
3.Docker日志收集最佳实践

`,41)]))}const k=i(l,[["render",t],["__file","Kubernetes容器日志收集.html.vue"]]),d=JSON.parse('{"path":"/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html","title":"Kubernetes容器日志收集","lang":"zh-CN","frontmatter":{"description":"Kubernetes容器日志收集 日志采集方式 日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。 1.原生方式 使用 kubectl ...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Kubernetes容器日志收集"}],["meta",{"property":"og:description","content":"Kubernetes容器日志收集 日志采集方式 日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。 1.原生方式 使用 kubectl ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://upyuncdn.wenzhihuai.com/201906020557591917511053.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-24T05:35:53.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-24T05:35:53.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Kubernetes容器日志收集\\",\\"image\\":[\\"https://upyuncdn.wenzhihuai.com/201906020557591917511053.png\\",\\"https://upyuncdn.wenzhihuai.com/201906020558431636899655.png\\",\\"https://upyuncdn.wenzhihuai.com/201906020559061972119021.png\\"],\\"dateModified\\":\\"2024-02-24T05:35:53.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"日志采集方式","slug":"日志采集方式","link":"#日志采集方式","children":[]},{"level":2,"title":"一、原生方式","slug":"一、原生方式","link":"#一、原生方式","children":[{"level":3,"title":"1.1 控制台stdout方式","slug":"_1-1-控制台stdout方式","link":"#_1-1-控制台stdout方式","children":[]},{"level":3,"title":"1.2 新版本的subPathExpr","slug":"_1-2-新版本的subpathexpr","link":"#_1-2-新版本的subpathexpr","children":[]}]},{"level":2,"title":"二、Daemonset方式","slug":"二、daemonset方式","link":"#二、daemonset方式","children":[]},{"level":2,"title":"三、Sidecar模式","slug":"三、sidecar模式","link":"#三、sidecar模式","children":[]},{"level":2,"title":"四、总结","slug":"四、总结","link":"#四、总结","children":[]}],"git":{"createdTime":1708751576000,"updatedTime":1708752953000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":10.62,"words":3187},"filePathRelative":"kubernetes/Kubernetes容器日志收集.md","localizedDate":"2024年2月24日","excerpt":"\\n

日志采集方式

\\n

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

","autoDesc":true}');export{k as comp,d as data}; diff --git "a/assets/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html-DBlO1MVO.js" "b/assets/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html-DBlO1MVO.js" new file mode 100644 index 00000000..f21b831c --- /dev/null +++ "b/assets/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html-DBlO1MVO.js" @@ -0,0 +1,199 @@ +import{_ as i,c as a,d as n,o as l}from"./app-ftEjETWs.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

RedissonLock分布式锁源码分析

最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

一、实现分布式锁的要求

  1. 互斥性。在任何时候,当且仅有一个客户端能够持有锁。
  2. 不能有死锁。持有锁的客户端崩溃后,后续客户端能够加锁。
  3. 容错性。大部分Redis或者ZooKeeper节点能够正常运行。
  4. 加锁解锁相同。加锁的客户端和解锁的客户端必须为同一个客户端,不能让其他的解锁了。

二、Redis实现分布式锁的常用命令

1.SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
2.expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
3.delete key
删除key,此处用来解锁使用。
4.HEXISTS key field
当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。
5.HINCRBY key field increment
将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。

三、常见写法

由上面三个命令,我们可以很快的写一个分布式锁出来:

if (conn.setnx("lock","1").equals(1L)) { 
+    //do something
+    return true; 
+} 
+return false;

但是这样也会存在问题,如果获取该锁的客户端挂掉了怎么办?一般而言,我们可以通过设置expire的过期时间来防止客户端挂掉所带来的影响,可以降低应用挂掉所带来的影响,不过当时间失效的时候,要保证其他客户端只有一台能够获取。

四、Redisson

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。——摘自百度百科

4.1 测试例子

先在pom引入Redssion

<dependency>
+    <groupId>org.redisson</groupId>
+    <artifactId>redisson</artifactId>
+    <version>3.6.1</version>
+</dependency>

起100个线程,同时对count进行操作,每次操作减1,加锁的时候能够保持顺序输出,不加的话为随机。

public class RedissonTest implements Runnable {
+    private static RedissonClient redisson;
+    private static int count = 10000;
+
+    private static void init() {
+        Config config = new Config();
+        config.useSingleServer()
+                .setAddress("redis://119.23.46.71:6340")
+                .setPassword("root")
+                .setDatabase(10);
+        redisson = Redisson.create(config);
+    }
+
+    @Override
+    public void run() {
+        RLock lock = redisson.getLock("anyLock");
+        lock.lock();
+        count--;
+        System.out.println(count);
+        lock.unlock();
+    }
+
+    public static void main(String[] args) {
+        init();
+        for (int i = 0; i < 100; i++) {
+            new Thread(new RedissonTest()).start();
+        }
+    }
+}

输出结果(部分结果):

...
+9930
+9929
+9928
+9927
+9926
+9925
+9924
+9923
+9922
+9921
+
+...

去掉lock.lock()和lock.unlock()之后(部分结果):

...
+9930
+9931
+9933
+9935
+9938
+9937
+9940
+9941
+9942
+9944
+9947
+9946
+9914
+...

五、RedissonLock源码分析

最新版的Redisson要求redis能够支持eval的命令,否则无法实现,即Redis要求2.6版本以上。在lua脚本中可以调用大部分的Redis命令,使用脚本的好处如下:
(1)减少网络开销:在Redis操作需求需要向Redis发送5次请求,而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
(2)原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
(3)复用:客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。

5.1 使用到的全局变量

全局变量:
expirationRenewalMap:存储entryName和其过期时间,底层用的netty的PlatformDependent.newConcurrentHashMap()
internalLockLeaseTime:锁默认释放的时间:30 * 1000,即30秒
id:UUID,用作客户端的唯一标识
PUBSUB:订阅者模式,当释放锁的时候,其他客户端能够知道锁已经被释放的消息,并让队列中的第一个消费者获取锁。使用PUB/SUB消息机制的优点:减少申请锁时的等待时间、安全、 锁带有超时时间、锁的标识唯一,防止死锁 锁设计为可重入,避免死锁。
commandExecutor:命令执行器,异步执行器

5.2 加锁

以lock.lock()为例,调用lock之后,底层使用的是lockInterruptibly,之后调用lockInterruptibly(-1, null);

(1)我们来看一下lockInterruptibly的源码,如果别的客户端没有加锁,则当前客户端进行加锁并且订阅,其他客户端尝试加锁,并且获取ttl,然后等待已经加了锁的客户端解锁。

//leaseTime默认为-1
+public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
+    long threadId = Thread.currentThread().getId();//获取当前线程ID
+    Long ttl = tryAcquire(leaseTime, unit, threadId);//尝试加锁
+    // 如果为空,当前线程获取锁成功,否则已经被其他客户端加锁
+    if (ttl == null) {
+        return;
+    }
+    //等待释放,并订阅锁
+    RFuture<RedissonLockEntry> future = subscribe(threadId);
+    commandExecutor.syncSubscription(future);
+    try {
+        while (true) {
+            // 重新尝试获取锁
+            ttl = tryAcquire(leaseTime, unit, threadId);
+            // 成功获取锁
+            if (ttl == null) {
+                break;
+            }
+            // 等待锁释放
+            if (ttl >= 0) {
+                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
+            } else {
+                getEntry(threadId).getLatch().acquire();
+            }
+        }
+    } finally {
+        // 取消订阅
+        unsubscribe(future, threadId);
+    }
+}

(2)下面是tryAcquire的实现,调用的是tryAcquireAsync

    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
+        return get(tryAcquireAsync(leaseTime, unit, threadId));
+    }

(3)下面是tryAcquireAsync的实现,异步尝试进行加锁,尝试加锁的时候leaseTime为-1。通常如果客户端没有加锁成功,则会进行阻塞,leaseTime为锁释放的时间。

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
+    if (leaseTime != -1) {   //在lock.lock()的时候,已经声明了leaseTime为-1,尝试加锁
+        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
+    }
+    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
+    //监听事件,订阅消息
+    ttlRemainingFuture.addListener(new FutureListener<Long>() {
+        @Override
+        public void operationComplete(Future<Long> future) throws Exception {
+            if (!future.isSuccess()) {
+                return;
+            }
+            Long ttlRemaining = future.getNow();
+            // lock acquired
+            if (ttlRemaining == null) {
+                //获取新的超时时间
+                scheduleExpirationRenewal(threadId);
+            }
+        }
+    });
+    return ttlRemainingFuture;  //返回ttl时间
+}

(4)下面是tryLockInnerAsyncy异步加锁,使用lua能够保证操作是原子性的

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
+    internalLockLeaseTime = unit.toMillis(leaseTime);
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
+              "if (redis.call('exists', KEYS[1]) == 0) then " +
+                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
+                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "return redis.call('pttl', KEYS[1]);",
+                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
+}

参数
KEYS[1](getName()) :需要加锁的key,这里需要是字符串类型。
ARGV[1](internalLockLeaseTime) :锁的超时时间,防止死锁
ARGV[2](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId
lua脚本解释

--检查key是否被占用了,如果没有则设置超时时间和唯一标识,初始化value=1
+if (redis.call('exists', KEYS[1]) == 0) then
+  redis.call('hset', KEYS[1], ARGV[2], 1);
+  redis.call('pexpire', KEYS[1], ARGV[1]);
+  return nil; 
+end; 
+--如果锁重入,需要判断锁的key field 都一致情况下 value 加一 
+if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
+  redis.call('hincrby', KEYS[1], ARGV[2], 1);
+  --锁重入重新设置超时时间  
+  redis.call('pexpire', KEYS[1], ARGV[1]); 
+  return nil; 
+end;
+--返回剩余的过期时间
+return redis.call('pttl', KEYS[1]);

(5)流程图

5.3 解锁

解锁的代码很简单,大意是将该节点删除,并发布消息。
(1)unlock源码

    public void unlock() {
+        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
+        if (opStatus == null) {
+            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+                    + id + " thread-id: " + Thread.currentThread().getId());
+        }
+        if (opStatus) {
+            cancelExpirationRenewal();
+        }

(2)异步解锁,并返回是否成功

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
+            "if (redis.call('exists', KEYS[1]) == 0) then " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; " +
+            "end;" +
+            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
+                "return nil;" +
+            "end; " +
+            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
+            "if (counter > 0) then " +
+                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
+                "return 0; " +
+            "else " +
+                "redis.call('del', KEYS[1]); " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; "+
+            "end; " +
+            "return nil;",
+            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
+
+    }

输入的参数有:
参数:
KEYS[1](getName()):需要加锁的key,这里需要是字符串类型。
KEYS[2](getChannelName()):redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName:“redisson_lock__channel__{” + getName() + “}”
ARGV[1](LockPubSub.unlockMessage):redis消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。
ARGV[2](internalLockLeaseTime):锁的超时时间,防止死锁
ARGV[3](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

此处lua脚本的作用:

--如果keys[1]不存在,则发布消息,说明已经被解锁了
+if (redis.call('exists', KEYS[1]) == 0) then
+    redis.call('publish', KEYS[2], ARGV[1]);
+    return 1;
+end;
+--key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。
+if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
+    return nil;
+end;
+--将value减1,这里主要用在重入锁
+local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
+if (counter > 0) then 
+    redis.call('pexpire', KEYS[1], ARGV[2]); 
+    return 0; 
+else 
+--删除key并消息
+    redis.call('del', KEYS[1]); 
+    redis.call('publish', KEYS[2], ARGV[1]); 
+    return 1;
+end; 
+return nil;

(3)删除过期信息

void cancelExpirationRenewal() {
+    Timeout task = expirationRenewalMap.remove(getEntryName());
+    if (task != null) {
+        task.cancel();
+    }
+}

总结

Redis2.6版本之后引入了eval,能够支持lua脚本,更好的保证了redis的原子性,而且redisson采用了大量异步的写法来避免性能所带来的影响。本文只是讲解了下redisson的重入锁,其还有公平锁、联锁、红锁、读写锁等,有兴趣的可以看下。感觉这篇文章写得也不是很好,毕竟netty还没开始学,有些api也不太清楚,希望各位大佬能够建议建议~~

参考:
1.redisson
2.Redis分布式锁的正确实现方式
3.分布式锁的多种实现方式
4.用Redis构建分布式锁
5.基于Redis的分布式锁实现
6.基于Redis实现分布式锁,Redisson使用及源码分析

`,53)]))}const p=i(h,[["render",k],["__file","RedissonLock分布式锁源码分析.html.vue"]]),d=JSON.parse('{"path":"/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html","title":"RedissonLock分布式锁源码分析","lang":"zh-CN","frontmatter":{"description":"RedissonLock分布式锁源码分析 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。 目前分布式锁常...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"RedissonLock分布式锁源码分析"}],["meta",{"property":"og:description","content":"RedissonLock分布式锁源码分析 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。 目前分布式锁常..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://upyuncdn.wenzhihuai.com/20180316081203383746214.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:22:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:22:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"RedissonLock分布式锁源码分析\\",\\"image\\":[\\"https://upyuncdn.wenzhihuai.com/20180316081203383746214.png\\",\\"https://upyuncdn.wenzhihuai.com/20180320010140455516380.png\\"],\\"dateModified\\":\\"2024-02-06T07:22:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、实现分布式锁的要求","slug":"一、实现分布式锁的要求","link":"#一、实现分布式锁的要求","children":[]},{"level":2,"title":"二、Redis实现分布式锁的常用命令","slug":"二、redis实现分布式锁的常用命令","link":"#二、redis实现分布式锁的常用命令","children":[]},{"level":2,"title":"三、常见写法","slug":"三、常见写法","link":"#三、常见写法","children":[]},{"level":2,"title":"四、Redisson","slug":"四、redisson","link":"#四、redisson","children":[{"level":3,"title":"4.1 测试例子","slug":"_4-1-测试例子","link":"#_4-1-测试例子","children":[]}]},{"level":2,"title":"五、RedissonLock源码分析","slug":"五、redissonlock源码分析","link":"#五、redissonlock源码分析","children":[{"level":3,"title":"5.1 使用到的全局变量","slug":"_5-1-使用到的全局变量","link":"#_5-1-使用到的全局变量","children":[]},{"level":3,"title":"5.2 加锁","slug":"_5-2-加锁","link":"#_5-2-加锁","children":[]},{"level":3,"title":"5.3 解锁","slug":"_5-3-解锁","link":"#_5-3-解锁","children":[]}]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1707204155000,"updatedTime":1707204155000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":9.66,"words":2897},"filePathRelative":"database/redis/RedissonLock分布式锁源码分析.md","localizedDate":"2024年2月6日","excerpt":"\\n

最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
\\n目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

","autoDesc":true}');export{p as comp,d as data}; diff --git "a/assets/Spring Boot Prometheus\344\275\277\347\224\250.html-CvPEGSZS.js" "b/assets/Spring Boot Prometheus\344\275\277\347\224\250.html-CvPEGSZS.js" new file mode 100644 index 00000000..a2d9a54c --- /dev/null +++ "b/assets/Spring Boot Prometheus\344\275\277\347\224\250.html-CvPEGSZS.js" @@ -0,0 +1,49 @@ +import{_ as s,c as a,d as e,o as n}from"./app-ftEjETWs.js";const t={};function l(h,i){return n(),a("div",null,i[0]||(i[0]=[e(`

Spring Boot Prometheus使用

一、基本原理

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

image-20240127202704612
image-20240127202704612

二、具体过程

三、pull模式(prometheus主动拉取)

<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-actuator</artifactId>
+</dependency>
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-registry-prometheus</artifactId>
+</dependency>
management:
+  endpoints:
+    web:
+      exposure:
+        include: prometheus
+  metrics:
+    tags:
+      application: \${spring.application.name}

之后查看/actuator/prometheus就可以看到

image-20240127202722776
image-20240127202722776

腾讯云上面有个prometheus的服务,接入云原生监控还要配置一个Servicemonitor、PodMonitor等,详细的可以访问腾讯云的官方文档(https://cloud.tencent.com/document/product/1416/56031)

四、主动上报(pushgateway)

大部分现实场景中,如果每增加一个服务,都需要开发去配置的话,不仅沟通成本高,也导致因配错而起的运维成本很高,采用主动上报方式比较简单,方便规避一些问题。

(1)引入库

主动上报除了需要引入spring-boot-starter-actuator、micrometer-registry-prometheus,还要引入simpleclient_pushgateway,三个都是必须的,少一个都不行。

        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.prometheus</groupId>
+            <artifactId>simpleclient_pushgateway</artifactId>
+        </dependency>

(2)修改配置文件

之后,为了安全考虑,需要把endpoints的所有信息都关闭掉,免得被泄露出去。

management:
+  endpoints:
+    #一定要设为false,防止不小心暴露/actuator相关接口触发安全工单
+    enabled-by-default: false
+  metrics:
+    tags:
+      #要设置应用的名称,否则会聚合到别的项目里面去
+      application: \${spring.application.name}
+    export:
+      prometheus:
+        pushgateway:
+          base-url: http://xxxxx
+          push-rate: 20s
+          job: \${spring.application.name}
+          enabled: true
+          username: xxxx
+          password: xxx
+          grouping-key:
+            instance: \${HOSTNAME}

最好多看一眼/actuator,确认一下是否已经关闭endpoints

image-20240127202830908
image-20240127202830908

(3)抓包看一下metrics(可跳过)

大致看了下 simpleclient_pushgateway和spring-boot-starter-actuator的源码,并没有对http的请求发起日志,调试的时候都不知道是不是正常上报过去了,只能采取抓包来研究下。

image-20240127202853218
image-20240127202853218
image-20240127202944264
image-20240127202944264

平均间隔5s左右,符合配置文件里的设置。

image-20240127203001776
image-20240127203001776

五、Grafana

对于java来说,常用的dashboard是https://grafana.com/grafana/dashboards/4701,也可以用spring boot的https://grafana.com/grafana/dashboards/6756

image-20240127203024477
image-20240127203024477

六、自定义监控上报

除了通用的封装好的指标之外,也可以自定义prometheus的监控。对于Spring Boot来说,只要如下代码即可实现:

    @Resource
+    private MeterRegistry meterRegistry;
+
+    public void report() {
+        meterRegistry.counter("指标", "tag的名称", "tag的值").increment();
+    }

也可以通过抓包来查看,以及在push-gateway上看到。

`,35)]))}const r=s(t,[["render",l],["__file","Spring Boot Prometheus使用.html.vue"]]),k=JSON.parse('{"path":"/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html","title":"Spring Boot Prometheus使用","lang":"zh-CN","frontmatter":{"description":"Spring Boot Prometheus使用 一、基本原理 Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exp...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Spring Boot Prometheus使用"}],["meta",{"property":"og:description","content":"Spring Boot Prometheus使用 一、基本原理 Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exp..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20240127202704612.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-27T13:52:01.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-27T13:52:01.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Spring Boot Prometheus使用\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20240127202704612.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127202722776.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127202830908.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127202853218.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127202944264.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127203001776.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240127203024477.png\\"],\\"dateModified\\":\\"2024-01-27T13:52:01.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"(1)引入库","slug":"_1-引入库","link":"#_1-引入库","children":[]},{"level":2,"title":"(2)修改配置文件","slug":"_2-修改配置文件","link":"#_2-修改配置文件","children":[]},{"level":2,"title":"(3)抓包看一下metrics(可跳过)","slug":"_3-抓包看一下metrics-可跳过","link":"#_3-抓包看一下metrics-可跳过","children":[]}],"git":{"createdTime":1706363521000,"updatedTime":1706363521000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.49,"words":1046},"filePathRelative":"java/SpringBoot/Spring Boot Prometheus使用.md","localizedDate":"2024年1月27日","excerpt":"\\n

一、基本原理

\\n

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

","autoDesc":true}');export{r as comp,k as data}; diff --git a/assets/aop.html-C7ud1qex.js b/assets/aop.html-C7ud1qex.js new file mode 100644 index 00000000..72fd0dc5 --- /dev/null +++ b/assets/aop.html-C7ud1qex.js @@ -0,0 +1,51 @@ +import{_ as e,c as s,d as a,o as n}from"./app-ftEjETWs.js";const l={};function t(r,i){return n(),s("div",null,i[0]||(i[0]=[a(`

AOP

一、概述

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

Spring自2.0版本开始采用@AspectJ注解非常容易的定义一个切面。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。

1.1 特点

AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。AOP是OOP的补充,是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP可以分为静态织入与动态织入,静态织入即在编译前将需织入内容写入目标模块中,这样成本非常高。动态织入则不需要改变目标模块。Spring框架实现了AOP,使用注解配置完成AOP比使用XML配置要更加方便与直观。

1.2 AOP概述

Aspect:一个模块用来关注多个类的切面。在JAVA EE的应用中,事务是AOP的典型例子。
Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.
Target(目标对象):代理的目标对象
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.spring采用动态代理织入,而AspectJ采用编译期织入和类装在期织入.
Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类Aspect(切面): 是切入点和通知(引介)的结合

二、Spring中的AOP

Spring实现AOP主要是由IOC容器来负责生成、管理的。其创建的方式有两种:

  1. 默认使用Java动态代理来创建AOP代理;
  2. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。高版本的Spring会自动选择是使用动态代理还是CGLIB生成代理内容,当然我们也可以强制使用CGLIB生成代理,那就是aop:config里面有一个"proxy-target-class"属性,这个属性值如果被设置为true,那么基于类的代理将起作用。

2.1 AspectJ支持5种类型的通知注解:

[1] Before:前置通知,在方法执行之前执行
[2] After:后置通知,在方法执行之后执行
[3] AfterRunning:返回通知,在方法返回结果之后执行
[4] AfterThrowing:异常通知,在方法抛出异常之后执行
[5] Around:环绕通知,围绕着方法执行
其中,环绕通知是最常见的一种通知注解,特别是在缓存的使用中,例如:Spring-Cache中的使用,在service的方法中添加一个cache的注解,通过AOP来拦截,如果缓存中已经存在,则直接返回结果,如果没有,再进行service的访问。

2.2 Spring提供了4种实现AOP的方式:

  1. 经典的基于代理的AOP
  2. @AspectJ注解驱动的切面
  3. 纯POJO切面
  4. 注入式AspectJ切面

三、原理概述

Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现。Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入,这点了解一下即可。

具体的原理请看Spring AOP

四、使用

网上看别人写了很多入门的例子,自己就不再阐述了,毕竟自己还是菜,下面是关于AOP入门的资料:
我们为什么要使用AOP?
Spring中AOP的实现
关于AOP

下面是自己在个人网站中的使用,主要是用来统计一个方法的执行消耗了多少时间,需要引入aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar的包。

4.1 在Spring MVC中开启AOP

    <!--自动扫描自定义切面-->
+    <aop:aspectj-autoproxy/>

4.2 定义一个切面

/**
+ * 可以使用 @Order 注解指定切面的优先级, 值越小优先级越高
+ */
+@Order(2)
+@Aspect
+@Component
+public class TimeInterceptor {
+}

4.3 声明一个切入点

    @Pointcut("execution(* com.myblog.service.impl.BlogServiceImpl.*(..))")
+    public void pointcut() {
+    }

4.4 声明一个前置切点

    @Before("pointcut()")
+    public void before(JoinPoint jp) {
+        logger.info(jp.getSignature().getName());
+        logger.info("----------前置通知----------");
+    }

4.5 声明一个后置切点

    @After("pointcut()")
+    public void after(JoinPoint jp) {
+        logger.info("----------最终通知----------");
+    }

4.6 环绕通知

这里,特别要注意的是要抛出Throwable异常,否则方法执行报错的时候无法处理也无法查看

    @Around("execution(* (com.myblog.service.impl.*+&&!com.myblog.service.impl.AsyncServiceImpl).*(..))")
+    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        Object obj = null;
+        Object[] args = joinPoint.getArgs();
+        long startTime = System.currentTimeMillis();
+        obj = joinPoint.proceed(args);
+        // 获取执行的方法名
+        long endTime = System.currentTimeMillis();
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
+        // 打印耗时的信息
+        this.printExecTime(methodName, startTime, endTime);
+        return obj;
+    }

4.7 返回结果通知

    @AfterReturning(pointcut = "execution(* com.myblog.service.impl.BlogServiceImpl.*(..))", returning = "result")
+    public void afterReturning(JoinPoint jp, Object result) {
+        logger.info(jp.getSignature().getName());
+        logger.info("结果是:" + result);
+        logger.info("----------返回结果----------");
+    }

4.8 异常后通知

    @AfterThrowing(pointcut = "execution(* com.myblog.service.impl.BlogServiceImpl.*(..))", throwing = "exp")
+    public void afterThrowing(JoinPoint jp, Exception exp) {
+        logger.info(jp.getSignature().getName());
+        logger.info("异常消息:" + exp.getMessage());
+        logger.info("----------异常通知----------");
+    }

4.9 结果

2018-02-04  17:22:46.287 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - getAllBlog
+2018-02-04  17:22:46.288 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - ----------前置通知----------
+2018-02-04  17:22:46.288 [http-nio-9090-exec-3] DEBUG com.myblog.dao.BlogMapper - Cache Hit Ratio [com.myblog.dao.BlogMapper]: 0.6
+2018-02-04  17:22:46.288 [http-nio-9090-exec-3] DEBUG com.myblog.dao.BlogMapper - Cache Hit Ratio [com.myblog.dao.BlogMapper]: 0.6666666666666666
+2018-02-04  17:22:46.289 [http-nio-9090-exec-3] INFO  com.myblog.cache.EhRedisCache - ===========================Cache L1 (ehcache) 
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - com.myblog.service.IBlogService.getAllBlog method take time: **5 ms**
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - ----------最终通知----------
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - getAllBlog
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - 结果是:Page{count=true, pageNum=1, pageSize=15, startRow=0, endRow=15, total=462, pages=31, countSignal=false, orderBy='null', orderByOnly=false, reasonable=true, pageSizeZero=true}
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.aspect.TimeInterceptor - ----------返回结果----------
+2018-02-04  17:22:46.292 [http-nio-9090-exec-3] INFO  com.myblog.cache.EhRedisCache - ===========================Cache L1 (ehcache) :{myCache}{com.myblog.service.impl.BlogServiceImpl.getBanner}={[ key = com.myblog.service.impl.BlogServiceImpl.getBanner, value=[com.myblog.model.Blog@2a5de6bc, com.myblog.model.Blog@544159b3, com.myblog.model.Blog@1de1421c, com.myblog.model.Blog@6dbb79bb, com.myblog.model.Blog@28160ab6], version=1, hitCount=2, CreationTime = 1517736161430, LastAccessTime = 1517736166292 ]}

由结果可以看到,整个方法的执行耗时5ms,算是客观吧,如果太大则要对其进行优化。

主要的源码在这:

TimeInterceptor

也可以下载我的博客源码参考参考:
newblog

参考

  1. Spring学习总结——Spring实现AOP的多种方式
  2. Spring AOP基础入门总结一
  3. Spring AOP官方
`,48)]))}const h=e(l,[["render",t],["__file","aop.html.vue"]]),o=JSON.parse('{"path":"/java/SpringBoot/aop.html","title":"AOP","lang":"zh-CN","frontmatter":{"description":"AOP 一、概述 在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/SpringBoot/aop.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"AOP"}],["meta",{"property":"og:description","content":"AOP 一、概述 在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20180118085015.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-30T06:53:42.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-30T06:53:42.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"AOP\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20180118085015.png\\",\\"https://github-images.wenzhihuai.com/images/20180204060518.png\\"],\\"dateModified\\":\\"2024-01-30T06:53:42.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、概述","slug":"一、概述","link":"#一、概述","children":[{"level":3,"title":"1.1 特点","slug":"_1-1-特点","link":"#_1-1-特点","children":[]},{"level":3,"title":"1.2 AOP概述","slug":"_1-2-aop概述","link":"#_1-2-aop概述","children":[]}]},{"level":2,"title":"二、Spring中的AOP","slug":"二、spring中的aop","link":"#二、spring中的aop","children":[{"level":3,"title":"2.1 AspectJ支持5种类型的通知注解:","slug":"_2-1-aspectj支持5种类型的通知注解","link":"#_2-1-aspectj支持5种类型的通知注解","children":[]},{"level":3,"title":"2.2 Spring提供了4种实现AOP的方式:","slug":"_2-2-spring提供了4种实现aop的方式","link":"#_2-2-spring提供了4种实现aop的方式","children":[]}]},{"level":2,"title":"三、原理概述","slug":"三、原理概述","link":"#三、原理概述","children":[]},{"level":2,"title":"四、使用","slug":"四、使用","link":"#四、使用","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1706596625000,"updatedTime":1706597622000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":7.17,"words":2150},"filePathRelative":"java/SpringBoot/aop.md","localizedDate":"2024年1月30日","excerpt":"\\n

一、概述

\\n

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

","autoDesc":true}');export{h as comp,o as data}; diff --git a/assets/app-ftEjETWs.js b/assets/app-ftEjETWs.js new file mode 100644 index 00000000..f2025531 --- /dev/null +++ b/assets/app-ftEjETWs.js @@ -0,0 +1,330 @@ +var Xh=Object.defineProperty;var ep=(e,t,n)=>t in e?Xh(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var ui=(e,t,n)=>ep(e,typeof t!="symbol"?t+"":t,n);/** +* @vue/shared v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function wa(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const xe={},Un=[],$t=()=>{},tp=()=>!1,ql=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Ba=e=>e.startsWith("onUpdate:"),Me=Object.assign,Ca=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},np=Object.prototype.hasOwnProperty,_e=(e,t)=>np.call(e,t),re=Array.isArray,Al=e=>Ur(e)==="[object Map]",lp=e=>Ur(e)==="[object Set]",ie=e=>typeof e=="function",ze=e=>typeof e=="string",ol=e=>typeof e=="symbol",De=e=>e!==null&&typeof e=="object",iu=e=>(De(e)||ie(e))&&ie(e.then)&&ie(e.catch),rp=Object.prototype.toString,Ur=e=>rp.call(e),ip=e=>Ur(e).slice(8,-1),ap=e=>Ur(e)==="[object Object]",xa=e=>ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Gn=wa(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Gr=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},op=/-(\w)/g,at=Gr(e=>e.replace(op,(t,n)=>n?n.toUpperCase():"")),sp=/\B([A-Z])/g,Pn=Gr(e=>e.replace(sp,"-$1").toLowerCase()),Wl=Gr(e=>e.charAt(0).toUpperCase()+e.slice(1)),di=Gr(e=>e?`on${Wl(e)}`:""),dn=(e,t)=>!Object.is(e,t),fi=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:l,value:n})},cp=e=>{const t=parseFloat(e);return isNaN(t)?e:t},up=e=>{const t=ze(e)?Number(e):NaN;return isNaN(t)?e:t};let Po;const Kr=()=>Po||(Po=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Sa(e){if(re(e)){const t={};for(let n=0;n{if(n){const l=n.split(fp);l.length>1&&(t[l[0].trim()]=l[1].trim())}}),t}function Ta(e){let t="";if(ze(e))t=e;else if(re(e))for(let n=0;n0)return;if(wl){let t=wl;for(wl=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;kl;){let t=kl;for(kl=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(l){e||(e=l)}t=n}}if(e)throw e}function fu(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function hu(e){let t,n=e.depsTail,l=n;for(;l;){const r=l.prevDep;l.version===-1?(l===n&&(n=r),Da(l),_p(l)):t=l,l.dep.activeLink=l.prevActiveLink,l.prevActiveLink=void 0,l=r}e.deps=t,e.depsTail=n}function Ni(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(pu(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function pu(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Pl))return;e.globalVersion=Pl;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&e.deps&&!Ni(e)){e.flags&=-3;return}const n=Ce,l=Tt;Ce=e,Tt=!0;try{fu(e);const r=e.fn(e._value);(t.version===0||dn(r,e._value))&&(e._value=r,t.version++)}catch(r){throw t.version++,r}finally{Ce=n,Tt=l,hu(e),e.flags&=-3}}function Da(e,t=!1){const{dep:n,prevSub:l,nextSub:r}=e;if(l&&(l.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=l,e.nextSub=void 0),n.subs===e&&(n.subs=l,!l&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Da(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function _p(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Tt=!0;const vu=[];function vn(){vu.push(Tt),Tt=!1}function mn(){const e=vu.pop();Tt=e===void 0?!0:e}function Io(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Ce;Ce=void 0;try{t()}finally{Ce=n}}}let Pl=0;class bp{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class qr{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0}track(t){if(!Ce||!Tt||Ce===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Ce)n=this.activeLink=new bp(Ce,this),Ce.deps?(n.prevDep=Ce.depsTail,Ce.depsTail.nextDep=n,Ce.depsTail=n):Ce.deps=Ce.depsTail=n,mu(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const l=n.nextDep;l.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=l),n.prevDep=Ce.depsTail,n.nextDep=void 0,Ce.depsTail.nextDep=n,Ce.depsTail=n,Ce.deps===n&&(Ce.deps=l)}return n}trigger(t){this.version++,Pl++,this.notify(t)}notify(t){La();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Oa()}}}function mu(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let l=t.deps;l;l=l.nextDep)mu(l)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Dr=new WeakMap,Sn=Symbol(""),ji=Symbol(""),Il=Symbol("");function Ye(e,t,n){if(Tt&&Ce){let l=Dr.get(e);l||Dr.set(e,l=new Map);let r=l.get(n);r||(l.set(n,r=new qr),r.map=l,r.key=n),r.track()}}function Yt(e,t,n,l,r,i){const a=Dr.get(e);if(!a){Pl++;return}const o=c=>{c&&c.trigger()};if(La(),t==="clear")a.forEach(o);else{const c=re(e),u=c&&xa(n);if(c&&n==="length"){const d=Number(l);a.forEach((f,h)=>{(h==="length"||h===Il||!ol(h)&&h>=d)&&o(f)})}else switch((n!==void 0||a.has(void 0))&&o(a.get(n)),u&&o(a.get(Il)),t){case"add":c?u&&o(a.get("length")):(o(a.get(Sn)),Al(e)&&o(a.get(ji)));break;case"delete":c||(o(a.get(Sn)),Al(e)&&o(a.get(ji)));break;case"set":Al(e)&&o(a.get(Sn));break}}Oa()}function yp(e,t){const n=Dr.get(e);return n&&n.get(t)}function Mn(e){const t=de(e);return t===e?t:(Ye(t,"iterate",Il),Lt(e)?t:t.map(tt))}function Pa(e){return Ye(e=de(e),"iterate",Il),e}const Ap={__proto__:null,[Symbol.iterator](){return pi(this,Symbol.iterator,tt)},concat(...e){return Mn(this).concat(...e.map(t=>re(t)?Mn(t):t))},entries(){return pi(this,"entries",e=>(e[1]=tt(e[1]),e))},every(e,t){return Gt(this,"every",e,t,void 0,arguments)},filter(e,t){return Gt(this,"filter",e,t,n=>n.map(tt),arguments)},find(e,t){return Gt(this,"find",e,t,tt,arguments)},findIndex(e,t){return Gt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Gt(this,"findLast",e,t,tt,arguments)},findLastIndex(e,t){return Gt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Gt(this,"forEach",e,t,void 0,arguments)},includes(...e){return vi(this,"includes",e)},indexOf(...e){return vi(this,"indexOf",e)},join(e){return Mn(this).join(e)},lastIndexOf(...e){return vi(this,"lastIndexOf",e)},map(e,t){return Gt(this,"map",e,t,void 0,arguments)},pop(){return vl(this,"pop")},push(...e){return vl(this,"push",e)},reduce(e,...t){return Fo(this,"reduce",e,t)},reduceRight(e,...t){return Fo(this,"reduceRight",e,t)},shift(){return vl(this,"shift")},some(e,t){return Gt(this,"some",e,t,void 0,arguments)},splice(...e){return vl(this,"splice",e)},toReversed(){return Mn(this).toReversed()},toSorted(e){return Mn(this).toSorted(e)},toSpliced(...e){return Mn(this).toSpliced(...e)},unshift(...e){return vl(this,"unshift",e)},values(){return pi(this,"values",tt)}};function pi(e,t,n){const l=Pa(e),r=l[t]();return l!==e&&!Lt(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const kp=Array.prototype;function Gt(e,t,n,l,r,i){const a=Pa(e),o=a!==e&&!Lt(e),c=a[t];if(c!==kp[t]){const f=c.apply(e,i);return o?tt(f):f}let u=n;a!==e&&(o?u=function(f,h){return n.call(this,tt(f),h,e)}:n.length>2&&(u=function(f,h){return n.call(this,f,h,e)}));const d=c.call(a,u,l);return o&&r?r(d):d}function Fo(e,t,n,l){const r=Pa(e);let i=n;return r!==e&&(Lt(e)?n.length>3&&(i=function(a,o,c){return n.call(this,a,o,c,e)}):i=function(a,o,c){return n.call(this,a,tt(o),c,e)}),r[t](i,...l)}function vi(e,t,n){const l=de(e);Ye(l,"iterate",Il);const r=l[t](...n);return(r===-1||r===!1)&&Ra(n[0])?(n[0]=de(n[0]),l[t](...n)):r}function vl(e,t,n=[]){vn(),La();const l=de(e)[t].apply(e,n);return Oa(),mn(),l}const wp=wa("__proto__,__v_isRef,__isVue"),gu=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ol));function Bp(e){ol(e)||(e=String(e));const t=de(this);return Ye(t,"has",e),t.hasOwnProperty(e)}class Eu{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,l){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return l===(r?i?Fp:Au:i?yu:bu).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(l)?t:void 0;const a=re(t);if(!r){let c;if(a&&(c=Ap[n]))return c;if(n==="hasOwnProperty")return Bp}const o=Reflect.get(t,n,Re(t)?t:l);return(ol(n)?gu.has(n):wp(n))||(r||Ye(t,"get",n),i)?o:Re(o)?a&&xa(n)?o:o.value:De(o)?r?In(o):Jl(o):o}}class _u extends Eu{constructor(t=!1){super(!1,t)}set(t,n,l,r){let i=t[n];if(!this._isShallow){const c=Ln(i);if(!Lt(l)&&!Ln(l)&&(i=de(i),l=de(l)),!re(t)&&Re(i)&&!Re(l))return c?!1:(i.value=l,!0)}const a=re(t)&&xa(n)?Number(n)e,mr=e=>Reflect.getPrototypeOf(e);function Lp(e,t,n){return function(...l){const r=this.__v_raw,i=de(r),a=Al(i),o=e==="entries"||e===Symbol.iterator&&a,c=e==="keys"&&a,u=r[e](...l),d=n?Hi:t?zi:tt;return!t&&Ye(i,"iterate",c?ji:Sn),{next(){const{value:f,done:h}=u.next();return h?{value:f,done:h}:{value:o?[d(f[0]),d(f[1])]:d(f),done:h}},[Symbol.iterator](){return this}}}}function gr(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Op(e,t){const n={get(r){const i=this.__v_raw,a=de(i),o=de(r);e||(dn(r,o)&&Ye(a,"get",r),Ye(a,"get",o));const{has:c}=mr(a),u=t?Hi:e?zi:tt;if(c.call(a,r))return u(i.get(r));if(c.call(a,o))return u(i.get(o));i!==a&&i.get(r)},get size(){const r=this.__v_raw;return!e&&Ye(de(r),"iterate",Sn),Reflect.get(r,"size",r)},has(r){const i=this.__v_raw,a=de(i),o=de(r);return e||(dn(r,o)&&Ye(a,"has",r),Ye(a,"has",o)),r===o?i.has(r):i.has(r)||i.has(o)},forEach(r,i){const a=this,o=a.__v_raw,c=de(o),u=t?Hi:e?zi:tt;return!e&&Ye(c,"iterate",Sn),o.forEach((d,f)=>r.call(i,u(d),u(f),a))}};return Me(n,e?{add:gr("add"),set:gr("set"),delete:gr("delete"),clear:gr("clear")}:{add(r){!t&&!Lt(r)&&!Ln(r)&&(r=de(r));const i=de(this);return mr(i).has.call(i,r)||(i.add(r),Yt(i,"add",r,r)),this},set(r,i){!t&&!Lt(i)&&!Ln(i)&&(i=de(i));const a=de(this),{has:o,get:c}=mr(a);let u=o.call(a,r);u||(r=de(r),u=o.call(a,r));const d=c.call(a,r);return a.set(r,i),u?dn(i,d)&&Yt(a,"set",r,i):Yt(a,"add",r,i),this},delete(r){const i=de(this),{has:a,get:o}=mr(i);let c=a.call(i,r);c||(r=de(r),c=a.call(i,r)),o&&o.call(i,r);const u=i.delete(r);return c&&Yt(i,"delete",r,void 0),u},clear(){const r=de(this),i=r.size!==0,a=r.clear();return i&&Yt(r,"clear",void 0,void 0),a}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Lp(r,e,t)}),n}function Ia(e,t){const n=Op(e,t);return(l,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?l:Reflect.get(_e(n,r)&&r in l?n:l,r,i)}const Dp={get:Ia(!1,!1)},Pp={get:Ia(!1,!0)},Ip={get:Ia(!0,!1)};const bu=new WeakMap,yu=new WeakMap,Au=new WeakMap,Fp=new WeakMap;function Rp(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Mp(e){return e.__v_skip||!Object.isExtensible(e)?0:Rp(ip(e))}function Jl(e){return Ln(e)?e:Fa(e,!1,xp,Dp,bu)}function ku(e){return Fa(e,!1,Tp,Pp,yu)}function In(e){return Fa(e,!0,Sp,Ip,Au)}function Fa(e,t,n,l,r){if(!De(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const a=Mp(e);if(a===0)return e;const o=new Proxy(e,a===2?l:n);return r.set(e,o),o}function Kn(e){return Ln(e)?Kn(e.__v_raw):!!(e&&e.__v_isReactive)}function Ln(e){return!!(e&&e.__v_isReadonly)}function Lt(e){return!!(e&&e.__v_isShallow)}function Ra(e){return e?!!e.__v_raw:!1}function de(e){const t=e&&e.__v_raw;return t?de(t):e}function Vp(e){return!_e(e,"__v_skip")&&Object.isExtensible(e)&&au(e,"__v_skip",!0),e}const tt=e=>De(e)?Jl(e):e,zi=e=>De(e)?In(e):e;function Re(e){return e?e.__v_isRef===!0:!1}function ee(e){return wu(e,!1)}function $e(e){return wu(e,!0)}function wu(e,t){return Re(e)?e:new Np(e,t)}class Np{constructor(t,n){this.dep=new qr,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:de(t),this._value=n?t:tt(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,l=this.__v_isShallow||Lt(t)||Ln(t);t=l?t:de(t),dn(t,n)&&(this._rawValue=t,this._value=l?t:tt(t),this.dep.trigger())}}function St(e){return Re(e)?e.value:e}const jp={get:(e,t,n)=>t==="__v_raw"?e:St(Reflect.get(e,t,n)),set:(e,t,n,l)=>{const r=e[t];return Re(r)&&!Re(n)?(r.value=n,!0):Reflect.set(e,t,n,l)}};function Bu(e){return Kn(e)?e:new Proxy(e,jp)}class Hp{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new qr,{get:l,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=l,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Cu(e){return new Hp(e)}function zp(e){const t=re(e)?new Array(e.length):{};for(const n in e)t[n]=xu(e,n);return t}class $p{constructor(t,n,l){this._object=t,this._key=n,this._defaultValue=l,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return yp(de(this._object),this._key)}}class Up{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function sl(e,t,n){return Re(e)?e:ie(e)?new Up(e):De(e)&&arguments.length>1?xu(e,t,n):ee(e)}function xu(e,t,n){const l=e[t];return Re(l)?l:new $p(e,t,n)}class Gp{constructor(t,n,l){this.fn=t,this.setter=n,this._value=void 0,this.dep=new qr(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Pl-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=l}notify(){if(this.flags|=16,!(this.flags&8)&&Ce!==this)return du(this,!0),!0}get value(){const t=this.dep.track();return pu(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Kp(e,t,n=!1){let l,r;return ie(e)?l=e:(l=e.get,r=e.set),new Gp(l,r,n)}const Er={},Pr=new WeakMap;let wn;function qp(e,t=!1,n=wn){if(n){let l=Pr.get(n);l||Pr.set(n,l=[]),l.push(e)}}function Wp(e,t,n=xe){const{immediate:l,deep:r,once:i,scheduler:a,augmentJob:o,call:c}=n,u=w=>r?w:Lt(w)||r===!1||r===0?un(w,1):un(w);let d,f,h,p,g=!1,v=!1;if(Re(e)?(f=()=>e.value,g=Lt(e)):Kn(e)?(f=()=>u(e),g=!0):re(e)?(v=!0,g=e.some(w=>Kn(w)||Lt(w)),f=()=>e.map(w=>{if(Re(w))return w.value;if(Kn(w))return u(w);if(ie(w))return c?c(w,2):w()})):ie(e)?t?f=c?()=>c(e,2):e:f=()=>{if(h){vn();try{h()}finally{mn()}}const w=wn;wn=d;try{return c?c(e,3,[p]):e(p)}finally{wn=w}}:f=$t,t&&r){const w=f,F=r===!0?1/0:r;f=()=>un(w(),F)}const y=su(),_=()=>{d.stop(),y&&y.active&&Ca(y.effects,d)};if(i&&t){const w=t;t=(...F)=>{w(...F),_()}}let A=v?new Array(e.length).fill(Er):Er;const m=w=>{if(!(!(d.flags&1)||!d.dirty&&!w))if(t){const F=d.run();if(r||g||(v?F.some((V,R)=>dn(V,A[R])):dn(F,A))){h&&h();const V=wn;wn=d;try{const R=[F,A===Er?void 0:v&&A[0]===Er?[]:A,p];c?c(t,3,R):t(...R),A=F}finally{wn=V}}}else d.run()};return o&&o(m),d=new cu(f),d.scheduler=a?()=>a(m,!1):m,p=w=>qp(w,!1,d),h=d.onStop=()=>{const w=Pr.get(d);if(w){if(c)c(w,4);else for(const F of w)F();Pr.delete(d)}},t?l?m(!0):A=d.run():a?a(m.bind(null,!0),!0):d.run(),_.pause=d.pause.bind(d),_.resume=d.resume.bind(d),_.stop=_,_}function un(e,t=1/0,n){if(t<=0||!De(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,Re(e))un(e.value,t,n);else if(re(e))for(let l=0;l{un(l,t,n)});else if(ap(e)){for(const l in e)un(e[l],t,n);for(const l of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,l)&&un(e[l],t,n)}return e}/** +* @vue/runtime-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Yl(e,t,n,l){try{return l?e(...l):e()}catch(r){Zl(r,t,n)}}function Dt(e,t,n,l){if(ie(e)){const r=Yl(e,t,n,l);return r&&iu(r)&&r.catch(i=>{Zl(i,t,n)}),r}if(re(e)){const r=[];for(let i=0;i>>1,r=nt[l],i=Fl(r);i=Fl(n)?nt.push(e):nt.splice(Yp(t),0,e),e.flags|=1,Tu()}}function Tu(){Ir||(Ir=Su.then(Lu))}function Zp(e){re(e)?qn.push(...e):an&&e.id===-1?an.splice(Nn+1,0,e):e.flags&1||(qn.push(e),e.flags|=1),Tu()}function Ro(e,t,n=Ht+1){for(;nFl(n)-Fl(l));if(qn.length=0,an){an.push(...t);return}for(an=t,Nn=0;Nne.id==null?e.flags&2?-1:1/0:e.id;function Lu(e){try{for(Ht=0;Ht{l._d&&Wo(-1);const i=Rr(t);let a;try{a=e(...r)}finally{Rr(i),l._d&&Wo(1)}return a};return l._n=!0,l._c=!0,l._d=!0,l}function zt(e,t,n,l){const r=e.dirs,i=t&&t.dirs;for(let a=0;ae.__isTeleport,on=Symbol("_leaveCb"),_r=Symbol("_enterCb");function Pu(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return we(()=>{e.isMounted=!0}),Jr(()=>{e.isUnmounting=!0}),e}const Et=[Function,Array],Iu={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Et,onEnter:Et,onAfterEnter:Et,onEnterCancelled:Et,onBeforeLeave:Et,onLeave:Et,onAfterLeave:Et,onLeaveCancelled:Et,onBeforeAppear:Et,onAppear:Et,onAfterAppear:Et,onAppearCancelled:Et},Fu=e=>{const t=e.subTree;return t.component?Fu(t.component):t},Xp={name:"BaseTransition",props:Iu,setup(e,{slots:t}){const n=Xl(),l=Pu();return()=>{const r=t.default&&Va(t.default(),!0);if(!r||!r.length)return;const i=Ru(r),a=de(e),{mode:o}=a;if(l.isLeaving)return mi(i);const c=Mo(i);if(!c)return mi(i);let u=Rl(c,a,l,n,f=>u=f);c.type!==pt&&On(c,u);let d=n.subTree&&Mo(n.subTree);if(d&&d.type!==pt&&!Bn(c,d)&&Fu(n).type!==pt){let f=Rl(d,a,l,n);if(On(d,f),o==="out-in"&&c.type!==pt)return l.isLeaving=!0,f.afterLeave=()=>{l.isLeaving=!1,n.job.flags&8||n.update(),delete f.afterLeave,d=void 0},mi(i);o==="in-out"&&c.type!==pt?f.delayLeave=(h,p,g)=>{const v=Mu(l,d);v[String(d.key)]=d,h[on]=()=>{p(),h[on]=void 0,delete u.delayedLeave,d=void 0},u.delayedLeave=()=>{g(),delete u.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return i}}};function Ru(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==pt){t=n;break}}return t}const e1=Xp;function Mu(e,t){const{leavingVNodes:n}=e;let l=n.get(t.type);return l||(l=Object.create(null),n.set(t.type,l)),l}function Rl(e,t,n,l,r){const{appear:i,mode:a,persisted:o=!1,onBeforeEnter:c,onEnter:u,onAfterEnter:d,onEnterCancelled:f,onBeforeLeave:h,onLeave:p,onAfterLeave:g,onLeaveCancelled:v,onBeforeAppear:y,onAppear:_,onAfterAppear:A,onAppearCancelled:m}=t,w=String(e.key),F=Mu(n,e),V=(M,T)=>{M&&Dt(M,l,9,T)},R=(M,T)=>{const $=T[1];V(M,T),re(M)?M.every(O=>O.length<=1)&&$():M.length<=1&&$()},Z={mode:a,persisted:o,beforeEnter(M){let T=c;if(!n.isMounted)if(i)T=y||c;else return;M[on]&&M[on](!0);const $=F[w];$&&Bn(e,$)&&$.el[on]&&$.el[on](),V(T,[M])},enter(M){let T=u,$=d,O=f;if(!n.isMounted)if(i)T=_||u,$=A||d,O=m||f;else return;let X=!1;const se=M[_r]=he=>{X||(X=!0,he?V(O,[M]):V($,[M]),Z.delayedLeave&&Z.delayedLeave(),M[_r]=void 0)};T?R(T,[M,se]):se()},leave(M,T){const $=String(e.key);if(M[_r]&&M[_r](!0),n.isUnmounting)return T();V(h,[M]);let O=!1;const X=M[on]=se=>{O||(O=!0,T(),se?V(v,[M]):V(g,[M]),M[on]=void 0,F[$]===e&&delete F[$])};F[$]=e,p?R(p,[M,X]):X()},clone(M){const T=Rl(M,t,n,l,r);return r&&r(T),T}};return Z}function mi(e){if(Ql(e))return e=fn(e),e.children=null,e}function Mo(e){if(!Ql(e))return Du(e.type)&&e.children?Ru(e.children):e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&ie(n.default))return n.default()}}function On(e,t){e.shapeFlag&6&&e.component?(e.transition=t,On(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Va(e,t=!1,n){let l=[],r=0;for(let i=0;i1)for(let i=0;iMl(g,t&&(re(t)?t[v]:t),n,l,r));return}if(Wn(l)&&!r){l.shapeFlag&512&&l.type.__asyncResolved&&l.component.subTree.component&&Ml(e,t,n,l.component.subTree);return}const i=l.shapeFlag&4?Ga(l.component):l.el,a=r?null:i,{i:o,r:c}=e,u=t&&t.r,d=o.refs===xe?o.refs={}:o.refs,f=o.setupState,h=de(f),p=f===xe?()=>!1:g=>_e(h,g);if(u!=null&&u!==c&&(ze(u)?(d[u]=null,p(u)&&(f[u]=null)):Re(u)&&(u.value=null)),ie(c))Yl(c,o,12,[a,d]);else{const g=ze(c),v=Re(c);if(g||v){const y=()=>{if(e.f){const _=g?p(c)?f[c]:d[c]:c.value;r?re(_)&&Ca(_,i):re(_)?_.includes(i)||_.push(i):g?(d[c]=[i],p(c)&&(f[c]=d[c])):(c.value=[i],e.k&&(d[e.k]=c.value))}else g?(d[c]=a,p(c)&&(f[c]=a)):v&&(c.value=a,e.k&&(d[e.k]=a))};a?(y.id=-1,dt(y,n)):y()}}}let Vo=!1;const Vn=()=>{Vo||(console.error("Hydration completed but contains mismatches."),Vo=!0)},t1=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",n1=e=>e.namespaceURI.includes("MathML"),br=e=>{if(e.nodeType===1){if(t1(e))return"svg";if(n1(e))return"mathml"}},Hn=e=>e.nodeType===8;function l1(e){const{mt:t,p:n,o:{patchProp:l,createText:r,nextSibling:i,parentNode:a,remove:o,insert:c,createComment:u}}=e,d=(m,w)=>{if(!w.hasChildNodes()){n(null,m,w),Fr(),w._vnode=m;return}f(w.firstChild,m,null,null,null),Fr(),w._vnode=m},f=(m,w,F,V,R,Z=!1)=>{Z=Z||!!w.dynamicChildren;const M=Hn(m)&&m.data==="[",T=()=>v(m,w,F,V,R,M),{type:$,ref:O,shapeFlag:X,patchFlag:se}=w;let he=m.nodeType;w.el=m,se===-2&&(Z=!1,w.dynamicChildren=null);let K=null;switch($){case Tn:he!==3?w.children===""?(c(w.el=r(""),a(m),m),K=m):K=T():(m.data!==w.children&&(Vn(),m.data=w.children),K=i(m));break;case pt:A(m)?(K=i(m),_(w.el=m.content.firstChild,m,F)):he!==8||M?K=T():K=i(m);break;case Cl:if(M&&(m=i(m),he=m.nodeType),he===1||he===3){K=m;const N=!w.children.length;for(let J=0;J{Z=Z||!!w.dynamicChildren;const{type:M,props:T,patchFlag:$,shapeFlag:O,dirs:X,transition:se}=w,he=M==="input"||M==="option";if(he||$!==-1){X&&zt(w,null,F,"created");let K=!1;if(A(m)){K=Zu(null,se)&&F&&F.vnode.props&&F.vnode.props.appear;const J=m.content.firstChild;K&&se.beforeEnter(J),_(J,m,F),w.el=m=J}if(O&16&&!(T&&(T.innerHTML||T.textContent))){let J=p(m.firstChild,w,m,F,V,R,Z);for(;J;){yr(m,1)||Vn();const ae=J;J=J.nextSibling,o(ae)}}else if(O&8){let J=w.children;J[0]===` +`&&(m.tagName==="PRE"||m.tagName==="TEXTAREA")&&(J=J.slice(1)),m.textContent!==J&&(yr(m,0)||Vn(),m.textContent=w.children)}if(T){if(he||!Z||$&48){const J=m.tagName.includes("-");for(const ae in T)(he&&(ae.endsWith("value")||ae==="indeterminate")||ql(ae)&&!Gn(ae)||ae[0]==="."||J)&&l(m,ae,null,T[ae],void 0,F)}else if(T.onClick)l(m,"onClick",null,T.onClick,void 0,F);else if($&4&&Kn(T.style))for(const J in T.style)T.style[J]}let N;(N=T&&T.onVnodeBeforeMount)&&_t(N,F,w),X&&zt(w,null,F,"beforeMount"),((N=T&&T.onVnodeMounted)||X||K)&&rd(()=>{N&&_t(N,F,w),K&&se.enter(m),X&&zt(w,null,F,"mounted")},V)}return m.nextSibling},p=(m,w,F,V,R,Z,M)=>{M=M||!!w.dynamicChildren;const T=w.children,$=T.length;for(let O=0;O<$;O++){const X=M?T[O]:T[O]=yt(T[O]),se=X.type===Tn;m?(se&&!M&&O+1<$&&yt(T[O+1]).type===Tn&&(c(r(m.data.slice(X.children.length)),F,i(m)),m.data=X.children),m=f(m,X,V,R,Z,M)):se&&!X.children?c(X.el=r(""),F):(yr(F,1)||Vn(),n(null,X,F,null,V,R,br(F),Z))}return m},g=(m,w,F,V,R,Z)=>{const{slotScopeIds:M}=w;M&&(R=R?R.concat(M):M);const T=a(m),$=p(i(m),w,T,F,V,R,Z);return $&&Hn($)&&$.data==="]"?i(w.anchor=$):(Vn(),c(w.anchor=u("]"),T,$),$)},v=(m,w,F,V,R,Z)=>{if(yr(m.parentElement,1)||Vn(),w.el=null,Z){const $=y(m);for(;;){const O=i(m);if(O&&O!==$)o(O);else break}}const M=i(m),T=a(m);return o(m),n(null,w,T,M,F,V,br(T),R),F&&(F.vnode.el=w.el,nd(F,w.el)),M},y=(m,w="[",F="]")=>{let V=0;for(;m;)if(m=i(m),m&&Hn(m)&&(m.data===w&&V++,m.data===F)){if(V===0)return i(m);V--}return m},_=(m,w,F)=>{const V=w.parentNode;V&&V.replaceChild(m,w);let R=F;for(;R;)R.vnode.el===w&&(R.vnode.el=R.subTree.el=m),R=R.parent},A=m=>m.nodeType===1&&m.tagName==="TEMPLATE";return[d,f]}const No="data-allow-mismatch",r1={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function yr(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(No);)e=e.parentElement;const n=e&&e.getAttribute(No);if(n==null)return!1;if(n==="")return!0;{const l=n.split(",");return t===0&&l.includes("children")?!0:n.split(",").includes(r1[t])}}Kr().requestIdleCallback;Kr().cancelIdleCallback;function i1(e,t){if(Hn(e)&&e.data==="["){let n=1,l=e.nextSibling;for(;l;){if(l.nodeType===1){if(t(l)===!1)break}else if(Hn(l))if(l.data==="]"){if(--n===0)break}else l.data==="["&&n++;l=l.nextSibling}}else t(e)}const Wn=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function a1(e){ie(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:l,delay:r=200,hydrate:i,timeout:a,suspensible:o=!0,onError:c}=e;let u=null,d,f=0;const h=()=>(f++,u=null,p()),p=()=>{let g;return u||(g=u=t().catch(v=>{if(v=v instanceof Error?v:new Error(String(v)),c)return new Promise((y,_)=>{c(v,()=>y(h()),()=>_(v),f+1)});throw v}).then(v=>g!==u&&u?u:(v&&(v.__esModule||v[Symbol.toStringTag]==="Module")&&(v=v.default),d=v,v)))};return H({name:"AsyncComponentWrapper",__asyncLoader:p,__asyncHydrate(g,v,y){const _=i?()=>{const A=i(y,m=>i1(g,m));A&&(v.bum||(v.bum=[])).push(A)}:y;d?_():p().then(()=>!v.isUnmounted&&_())},get __asyncResolved(){return d},setup(){const g=Ue;if(Na(g),d)return()=>gi(d,g);const v=m=>{u=null,Zl(m,g,13,!l)};if(o&&g.suspense||Qn)return p().then(m=>()=>gi(m,g)).catch(m=>(v(m),()=>l?Ve(l,{error:m}):null));const y=ee(!1),_=ee(),A=ee(!!r);return r&&setTimeout(()=>{A.value=!1},r),a!=null&&setTimeout(()=>{if(!y.value&&!_.value){const m=new Error(`Async component timed out after ${a}ms.`);v(m),_.value=m}},a),p().then(()=>{y.value=!0,g.parent&&Ql(g.parent.vnode)&&g.parent.update()}).catch(m=>{v(m),_.value=m}),()=>{if(y.value&&d)return gi(d,g);if(_.value&&l)return Ve(l,{error:_.value});if(n&&!A.value)return Ve(n)}}})}function gi(e,t){const{ref:n,props:l,children:r,ce:i}=t.vnode,a=Ve(e,l,r);return a.ref=n,a.ce=i,delete t.vnode.ce,a}const Ql=e=>e.type.__isKeepAlive;function o1(e,t){Vu(e,"a",t)}function s1(e,t){Vu(e,"da",t)}function Vu(e,t,n=Ue){const l=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Wr(t,l,n),n){let r=n.parent;for(;r&&r.parent;)Ql(r.parent.vnode)&&c1(l,t,n,r),r=r.parent}}function c1(e,t,n,l){const r=Wr(t,e,l,!0);cl(()=>{Ca(l[t],r)},n)}function Wr(e,t,n=Ue,l=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...a)=>{vn();const o=er(n),c=Dt(t,n,e,a);return o(),mn(),c});return l?r.unshift(i):r.push(i),i}}const Xt=e=>(t,n=Ue)=>{(!Qn||e==="sp")&&Wr(e,(...l)=>t(...l),n)},u1=Xt("bm"),we=Xt("m"),d1=Xt("bu"),Nu=Xt("u"),Jr=Xt("bum"),cl=Xt("um"),f1=Xt("sp"),h1=Xt("rtg"),p1=Xt("rtc");function v1(e,t=Ue){Wr("ec",e,t)}const m1="components";function kt(e,t){return E1(m1,e,!0,t)||e}const g1=Symbol.for("v-ndc");function E1(e,t,n=!0,l=!1){const r=At||Ue;if(r){const i=r.type;{const o=l0(i,!1);if(o&&(o===t||o===at(t)||o===Wl(at(t))))return i}const a=jo(r[e]||i[e],t)||jo(r.appContext[e],t);return!a&&l?i:a}}function jo(e,t){return e&&(e[t]||e[at(t)]||e[Wl(at(t))])}const Ui=e=>e?sd(e)?Ga(e):Ui(e.parent):null,Bl=Me(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Ui(e.parent),$root:e=>Ui(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ja(e),$forceUpdate:e=>e.f||(e.f=()=>{Ma(e.update)}),$nextTick:e=>e.n||(e.n=Qt.bind(e.proxy)),$watch:e=>V1.bind(e)}),Ei=(e,t)=>e!==xe&&!e.__isScriptSetup&&_e(e,t),_1={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:l,data:r,props:i,accessCache:a,type:o,appContext:c}=e;let u;if(t[0]!=="$"){const p=a[t];if(p!==void 0)switch(p){case 1:return l[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Ei(l,t))return a[t]=1,l[t];if(r!==xe&&_e(r,t))return a[t]=2,r[t];if((u=e.propsOptions[0])&&_e(u,t))return a[t]=3,i[t];if(n!==xe&&_e(n,t))return a[t]=4,n[t];Gi&&(a[t]=0)}}const d=Bl[t];let f,h;if(d)return t==="$attrs"&&Ye(e.attrs,"get",""),d(e);if((f=o.__cssModules)&&(f=f[t]))return f;if(n!==xe&&_e(n,t))return a[t]=4,n[t];if(h=c.config.globalProperties,_e(h,t))return h[t]},set({_:e},t,n){const{data:l,setupState:r,ctx:i}=e;return Ei(r,t)?(r[t]=n,!0):l!==xe&&_e(l,t)?(l[t]=n,!0):_e(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:l,appContext:r,propsOptions:i}},a){let o;return!!n[a]||e!==xe&&_e(e,a)||Ei(t,a)||(o=i[0])&&_e(o,a)||_e(l,a)||_e(Bl,a)||_e(r.config.globalProperties,a)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:_e(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ho(e){return re(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Gi=!0;function b1(e){const t=ja(e),n=e.proxy,l=e.ctx;Gi=!1,t.beforeCreate&&zo(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:a,watch:o,provide:c,inject:u,created:d,beforeMount:f,mounted:h,beforeUpdate:p,updated:g,activated:v,deactivated:y,beforeDestroy:_,beforeUnmount:A,destroyed:m,unmounted:w,render:F,renderTracked:V,renderTriggered:R,errorCaptured:Z,serverPrefetch:M,expose:T,inheritAttrs:$,components:O,directives:X,filters:se}=t;if(u&&y1(u,l,null),a)for(const N in a){const J=a[N];ie(J)&&(l[N]=J.bind(n))}if(r){const N=r.call(n,n);De(N)&&(e.data=Jl(N))}if(Gi=!0,i)for(const N in i){const J=i[N],ae=ie(J)?J.bind(n,n):ie(J.get)?J.get.bind(n,n):$t,Ge=!ie(J)&&ie(J.set)?J.set.bind(n):$t,Fe=k({get:ae,set:Ge});Object.defineProperty(l,N,{enumerable:!0,configurable:!0,get:()=>Fe.value,set:ge=>Fe.value=ge})}if(o)for(const N in o)ju(o[N],l,n,N);if(c){const N=ie(c)?c.call(n):c;Reflect.ownKeys(N).forEach(J=>{Ot(J,N[J])})}d&&zo(d,e,"c");function K(N,J){re(J)?J.forEach(ae=>N(ae.bind(n))):J&&N(J.bind(n))}if(K(u1,f),K(we,h),K(d1,p),K(Nu,g),K(o1,v),K(s1,y),K(v1,Z),K(p1,V),K(h1,R),K(Jr,A),K(cl,w),K(f1,M),re(T))if(T.length){const N=e.exposed||(e.exposed={});T.forEach(J=>{Object.defineProperty(N,J,{get:()=>n[J],set:ae=>n[J]=ae})})}else e.exposed||(e.exposed={});F&&e.render===$t&&(e.render=F),$!=null&&(e.inheritAttrs=$),O&&(e.components=O),X&&(e.directives=X),M&&Na(e)}function y1(e,t,n=$t){re(e)&&(e=Ki(e));for(const l in e){const r=e[l];let i;De(r)?"default"in r?i=Se(r.from||l,r.default,!0):i=Se(r.from||l):i=Se(r),Re(i)?Object.defineProperty(t,l,{enumerable:!0,configurable:!0,get:()=>i.value,set:a=>i.value=a}):t[l]=i}}function zo(e,t,n){Dt(re(e)?e.map(l=>l.bind(t.proxy)):e.bind(t.proxy),t,n)}function ju(e,t,n,l){let r=l.includes(".")?ed(n,l):()=>n[l];if(ze(e)){const i=t[e];ie(i)&&pe(r,i)}else if(ie(e))pe(r,e.bind(n));else if(De(e))if(re(e))e.forEach(i=>ju(i,t,n,l));else{const i=ie(e.handler)?e.handler.bind(n):t[e.handler];ie(i)&&pe(r,i,e)}}function ja(e){const t=e.type,{mixins:n,extends:l}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:a}}=e.appContext,o=i.get(t);let c;return o?c=o:!r.length&&!n&&!l?c=t:(c={},r.length&&r.forEach(u=>Mr(c,u,a,!0)),Mr(c,t,a)),De(t)&&i.set(t,c),c}function Mr(e,t,n,l=!1){const{mixins:r,extends:i}=t;i&&Mr(e,i,n,!0),r&&r.forEach(a=>Mr(e,a,n,!0));for(const a in t)if(!(l&&a==="expose")){const o=A1[a]||n&&n[a];e[a]=o?o(e[a],t[a]):t[a]}return e}const A1={data:$o,props:Uo,emits:Uo,methods:_l,computed:_l,beforeCreate:Xe,created:Xe,beforeMount:Xe,mounted:Xe,beforeUpdate:Xe,updated:Xe,beforeDestroy:Xe,beforeUnmount:Xe,destroyed:Xe,unmounted:Xe,activated:Xe,deactivated:Xe,errorCaptured:Xe,serverPrefetch:Xe,components:_l,directives:_l,watch:w1,provide:$o,inject:k1};function $o(e,t){return t?e?function(){return Me(ie(e)?e.call(this,this):e,ie(t)?t.call(this,this):t)}:t:e}function k1(e,t){return _l(Ki(e),Ki(t))}function Ki(e){if(re(e)){const t={};for(let n=0;n1)return n&&ie(t)?t.call(l&&l.proxy):t}}const zu={},$u=()=>Object.create(zu),Uu=e=>Object.getPrototypeOf(e)===zu;function x1(e,t,n,l=!1){const r={},i=$u();e.propsDefaults=Object.create(null),Gu(e,t,r,i);for(const a in e.propsOptions[0])a in r||(r[a]=void 0);n?e.props=l?r:ku(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function S1(e,t,n,l){const{props:r,attrs:i,vnode:{patchFlag:a}}=e,o=de(r),[c]=e.propsOptions;let u=!1;if((l||a>0)&&!(a&16)){if(a&8){const d=e.vnode.dynamicProps;for(let f=0;f{c=!0;const[h,p]=Ku(f,t,!0);Me(a,h),p&&o.push(...p)};!n&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!c)return De(e)&&l.set(e,Un),Un;if(re(i))for(let d=0;de[0]==="_"||e==="$stable",Ha=e=>re(e)?e.map(yt):[yt(e)],L1=(e,t,n)=>{if(t._n)return t;const l=$i((...r)=>Ha(t(...r)),n);return l._c=!1,l},Wu=(e,t,n)=>{const l=e._ctx;for(const r in e){if(qu(r))continue;const i=e[r];if(ie(i))t[r]=L1(r,i,l);else if(i!=null){const a=Ha(i);t[r]=()=>a}}},Ju=(e,t)=>{const n=Ha(t);e.slots.default=()=>n},Yu=(e,t,n)=>{for(const l in t)(n||l!=="_")&&(e[l]=t[l])},O1=(e,t,n)=>{const l=e.slots=$u();if(e.vnode.shapeFlag&32){const r=t._;r?(Yu(l,t,n),n&&au(l,"_",r,!0)):Wu(t,l)}else t&&Ju(e,t)},D1=(e,t,n)=>{const{vnode:l,slots:r}=e;let i=!0,a=xe;if(l.shapeFlag&32){const o=t._;o?n&&o===1?i=!1:Yu(r,t,n):(i=!t.$stable,Wu(t,r)),a=t}else t&&(Ju(e,t),a={default:1});if(i)for(const o in r)!qu(o)&&a[o]==null&&delete r[o]},dt=rd;function P1(e){return I1(e,l1)}function I1(e,t){const n=Kr();n.__VUE__=!0;const{insert:l,remove:r,patchProp:i,createElement:a,createText:o,createComment:c,setText:u,setElementText:d,parentNode:f,nextSibling:h,setScopeId:p=$t,insertStaticContent:g}=e,v=(E,b,B,D=null,S=null,P=null,G=void 0,z=null,j=!!b.dynamicChildren)=>{if(E===b)return;E&&!Bn(E,b)&&(D=x(E),ge(E,S,P,!0),E=null),b.patchFlag===-2&&(j=!1,b.dynamicChildren=null);const{type:I,ref:ne,shapeFlag:W}=b;switch(I){case Tn:y(E,b,B,D);break;case pt:_(E,b,B,D);break;case Cl:E==null&&A(b,B,D,G);break;case ht:O(E,b,B,D,S,P,G,z,j);break;default:W&1?F(E,b,B,D,S,P,G,z,j):W&6?X(E,b,B,D,S,P,G,z,j):(W&64||W&128)&&I.process(E,b,B,D,S,P,G,z,j,Q)}ne!=null&&S&&Ml(ne,E&&E.ref,P,b||E,!b)},y=(E,b,B,D)=>{if(E==null)l(b.el=o(b.children),B,D);else{const S=b.el=E.el;b.children!==E.children&&u(S,b.children)}},_=(E,b,B,D)=>{E==null?l(b.el=c(b.children||""),B,D):b.el=E.el},A=(E,b,B,D)=>{[E.el,E.anchor]=g(E.children,b,B,D,E.el,E.anchor)},m=({el:E,anchor:b},B,D)=>{let S;for(;E&&E!==b;)S=h(E),l(E,B,D),E=S;l(b,B,D)},w=({el:E,anchor:b})=>{let B;for(;E&&E!==b;)B=h(E),r(E),E=B;r(b)},F=(E,b,B,D,S,P,G,z,j)=>{b.type==="svg"?G="svg":b.type==="math"&&(G="mathml"),E==null?V(b,B,D,S,P,G,z,j):M(E,b,S,P,G,z,j)},V=(E,b,B,D,S,P,G,z)=>{let j,I;const{props:ne,shapeFlag:W,transition:te,dirs:le}=E;if(j=E.el=a(E.type,P,ne&&ne.is,ne),W&8?d(j,E.children):W&16&&Z(E.children,j,null,D,S,_i(E,P),G,z),le&&zt(E,null,D,"created"),R(j,E,E.scopeId,G,D),ne){for(const Be in ne)Be!=="value"&&!Gn(Be)&&i(j,Be,null,ne[Be],P,D);"value"in ne&&i(j,"value",null,ne.value,P),(I=ne.onVnodeBeforeMount)&&_t(I,D,E)}le&&zt(E,null,D,"beforeMount");const ue=Zu(S,te);ue&&te.beforeEnter(j),l(j,b,B),((I=ne&&ne.onVnodeMounted)||ue||le)&&dt(()=>{I&&_t(I,D,E),ue&&te.enter(j),le&&zt(E,null,D,"mounted")},S)},R=(E,b,B,D,S)=>{if(B&&p(E,B),D)for(let P=0;P{for(let I=j;I{const z=b.el=E.el;let{patchFlag:j,dynamicChildren:I,dirs:ne}=b;j|=E.patchFlag&16;const W=E.props||xe,te=b.props||xe;let le;if(B&&yn(B,!1),(le=te.onVnodeBeforeUpdate)&&_t(le,B,b,E),ne&&zt(b,E,B,"beforeUpdate"),B&&yn(B,!0),(W.innerHTML&&te.innerHTML==null||W.textContent&&te.textContent==null)&&d(z,""),I?T(E.dynamicChildren,I,z,B,D,_i(b,S),P):G||J(E,b,z,null,B,D,_i(b,S),P,!1),j>0){if(j&16)$(z,W,te,B,S);else if(j&2&&W.class!==te.class&&i(z,"class",null,te.class,S),j&4&&i(z,"style",W.style,te.style,S),j&8){const ue=b.dynamicProps;for(let Be=0;Be{le&&_t(le,B,b,E),ne&&zt(b,E,B,"updated")},D)},T=(E,b,B,D,S,P,G)=>{for(let z=0;z{if(b!==B){if(b!==xe)for(const P in b)!Gn(P)&&!(P in B)&&i(E,P,b[P],null,S,D);for(const P in B){if(Gn(P))continue;const G=B[P],z=b[P];G!==z&&P!=="value"&&i(E,P,z,G,S,D)}"value"in B&&i(E,"value",b.value,B.value,S)}},O=(E,b,B,D,S,P,G,z,j)=>{const I=b.el=E?E.el:o(""),ne=b.anchor=E?E.anchor:o("");let{patchFlag:W,dynamicChildren:te,slotScopeIds:le}=b;le&&(z=z?z.concat(le):le),E==null?(l(I,B,D),l(ne,B,D),Z(b.children||[],B,ne,S,P,G,z,j)):W>0&&W&64&&te&&E.dynamicChildren?(T(E.dynamicChildren,te,B,S,P,G,z),(b.key!=null||S&&b===S.subTree)&&Qu(E,b,!0)):J(E,b,B,ne,S,P,G,z,j)},X=(E,b,B,D,S,P,G,z,j)=>{b.slotScopeIds=z,E==null?b.shapeFlag&512?S.ctx.activate(b,B,D,G,j):se(b,B,D,S,P,G,j):he(E,b,j)},se=(E,b,B,D,S,P,G)=>{const z=E.component=Q1(E,D,S);if(Ql(E)&&(z.ctx.renderer=Q),X1(z,!1,G),z.asyncDep){if(S&&S.registerDep(z,K,G),!E.el){const j=z.subTree=Ve(pt);_(null,j,b,B)}}else K(z,E,b,B,S,P,G)},he=(E,b,B)=>{const D=b.component=E.component;if($1(E,b,B))if(D.asyncDep&&!D.asyncResolved){N(D,b,B);return}else D.next=b,D.update();else b.el=E.el,D.vnode=b},K=(E,b,B,D,S,P,G)=>{const z=()=>{if(E.isMounted){let{next:W,bu:te,u:le,parent:ue,vnode:Be}=E;{const ct=Xu(E);if(ct){W&&(W.el=Be.el,N(E,W,G)),ct.asyncDep.then(()=>{E.isUnmounted||z()});return}}let be=W,st;yn(E,!1),W?(W.el=Be.el,N(E,W,G)):W=Be,te&&fi(te),(st=W.props&&W.props.onVnodeBeforeUpdate)&&_t(st,ue,W,Be),yn(E,!0);const Je=bi(E),Ct=E.subTree;E.subTree=Je,v(Ct,Je,f(Ct.el),x(Ct),E,S,P),W.el=Je.el,be===null&&nd(E,Je.el),le&&dt(le,S),(st=W.props&&W.props.onVnodeUpdated)&&dt(()=>_t(st,ue,W,Be),S)}else{let W;const{el:te,props:le}=b,{bm:ue,m:Be,parent:be,root:st,type:Je}=E,Ct=Wn(b);if(yn(E,!1),ue&&fi(ue),!Ct&&(W=le&&le.onVnodeBeforeMount)&&_t(W,be,b),yn(E,!0),te&&ye){const ct=()=>{E.subTree=bi(E),ye(te,E.subTree,E,S,null)};Ct&&Je.__asyncHydrate?Je.__asyncHydrate(te,E,ct):ct()}else{st.ce&&st.ce._injectChildStyle(Je);const ct=E.subTree=bi(E);v(null,ct,B,D,E,S,P),b.el=ct.el}if(Be&&dt(Be,S),!Ct&&(W=le&&le.onVnodeMounted)){const ct=b;dt(()=>_t(W,be,ct),S)}(b.shapeFlag&256||be&&Wn(be.vnode)&&be.vnode.shapeFlag&256)&&E.a&&dt(E.a,S),E.isMounted=!0,b=B=D=null}};E.scope.on();const j=E.effect=new cu(z);E.scope.off();const I=E.update=j.run.bind(j),ne=E.job=j.runIfDirty.bind(j);ne.i=E,ne.id=E.uid,j.scheduler=()=>Ma(ne),yn(E,!0),I()},N=(E,b,B)=>{b.component=E;const D=E.vnode.props;E.vnode=b,E.next=null,S1(E,b.props,D,B),D1(E,b.children,B),vn(),Ro(E),mn()},J=(E,b,B,D,S,P,G,z,j=!1)=>{const I=E&&E.children,ne=E?E.shapeFlag:0,W=b.children,{patchFlag:te,shapeFlag:le}=b;if(te>0){if(te&128){Ge(I,W,B,D,S,P,G,z,j);return}else if(te&256){ae(I,W,B,D,S,P,G,z,j);return}}le&8?(ne&16&&We(I,S,P),W!==I&&d(B,W)):ne&16?le&16?Ge(I,W,B,D,S,P,G,z,j):We(I,S,P,!0):(ne&8&&d(B,""),le&16&&Z(W,B,D,S,P,G,z,j))},ae=(E,b,B,D,S,P,G,z,j)=>{E=E||Un,b=b||Un;const I=E.length,ne=b.length,W=Math.min(I,ne);let te;for(te=0;tene?We(E,S,P,!0,!1,W):Z(b,B,D,S,P,G,z,j,W)},Ge=(E,b,B,D,S,P,G,z,j)=>{let I=0;const ne=b.length;let W=E.length-1,te=ne-1;for(;I<=W&&I<=te;){const le=E[I],ue=b[I]=j?sn(b[I]):yt(b[I]);if(Bn(le,ue))v(le,ue,B,null,S,P,G,z,j);else break;I++}for(;I<=W&&I<=te;){const le=E[W],ue=b[te]=j?sn(b[te]):yt(b[te]);if(Bn(le,ue))v(le,ue,B,null,S,P,G,z,j);else break;W--,te--}if(I>W){if(I<=te){const le=te+1,ue=lete)for(;I<=W;)ge(E[I],S,P,!0),I++;else{const le=I,ue=I,Be=new Map;for(I=ue;I<=te;I++){const ut=b[I]=j?sn(b[I]):yt(b[I]);ut.key!=null&&Be.set(ut.key,I)}let be,st=0;const Je=te-ue+1;let Ct=!1,ct=0;const pl=new Array(Je);for(I=0;I=Je){ge(ut,S,P,!0);continue}let Vt;if(ut.key!=null)Vt=Be.get(ut.key);else for(be=ue;be<=te;be++)if(pl[be-ue]===0&&Bn(ut,b[be])){Vt=be;break}Vt===void 0?ge(ut,S,P,!0):(pl[Vt-ue]=I+1,Vt>=ct?ct=Vt:Ct=!0,v(ut,b[Vt],B,null,S,P,G,z,j),st++)}const Oo=Ct?F1(pl):Un;for(be=Oo.length-1,I=Je-1;I>=0;I--){const ut=ue+I,Vt=b[ut],Do=ut+1{const{el:P,type:G,transition:z,children:j,shapeFlag:I}=E;if(I&6){Fe(E.component.subTree,b,B,D);return}if(I&128){E.suspense.move(b,B,D);return}if(I&64){G.move(E,b,B,Q);return}if(G===ht){l(P,b,B);for(let W=0;Wz.enter(P),S);else{const{leave:W,delayLeave:te,afterLeave:le}=z,ue=()=>l(P,b,B),Be=()=>{W(P,()=>{ue(),le&&le()})};te?te(P,ue,Be):Be()}else l(P,b,B)},ge=(E,b,B,D=!1,S=!1)=>{const{type:P,props:G,ref:z,children:j,dynamicChildren:I,shapeFlag:ne,patchFlag:W,dirs:te,cacheIndex:le}=E;if(W===-2&&(S=!1),z!=null&&Ml(z,null,B,E,!0),le!=null&&(b.renderCache[le]=void 0),ne&256){b.ctx.deactivate(E);return}const ue=ne&1&&te,Be=!Wn(E);let be;if(Be&&(be=G&&G.onVnodeBeforeUnmount)&&_t(be,b,E),ne&6)rt(E.component,B,D);else{if(ne&128){E.suspense.unmount(B,D);return}ue&&zt(E,null,b,"beforeUnmount"),ne&64?E.type.remove(E,b,B,Q,D):I&&!I.hasOnce&&(P!==ht||W>0&&W&64)?We(I,b,B,!1,!0):(P===ht&&W&384||!S&&ne&16)&&We(j,b,B),D&&Ne(E)}(Be&&(be=G&&G.onVnodeUnmounted)||ue)&&dt(()=>{be&&_t(be,b,E),ue&&zt(E,null,b,"unmounted")},B)},Ne=E=>{const{type:b,el:B,anchor:D,transition:S}=E;if(b===ht){gt(B,D);return}if(b===Cl){w(E);return}const P=()=>{r(B),S&&!S.persisted&&S.afterLeave&&S.afterLeave()};if(E.shapeFlag&1&&S&&!S.persisted){const{leave:G,delayLeave:z}=S,j=()=>G(B,P);z?z(E.el,P,j):j()}else P()},gt=(E,b)=>{let B;for(;E!==b;)B=h(E),r(E),E=B;r(b)},rt=(E,b,B)=>{const{bum:D,scope:S,job:P,subTree:G,um:z,m:j,a:I}=E;Ko(j),Ko(I),D&&fi(D),S.stop(),P&&(P.flags|=8,ge(G,E,b,B)),z&&dt(z,b),dt(()=>{E.isUnmounted=!0},b),b&&b.pendingBranch&&!b.isUnmounted&&E.asyncDep&&!E.asyncResolved&&E.suspenseId===b.pendingId&&(b.deps--,b.deps===0&&b.resolve())},We=(E,b,B,D=!1,S=!1,P=0)=>{for(let G=P;G{if(E.shapeFlag&6)return x(E.component.subTree);if(E.shapeFlag&128)return E.suspense.next();const b=h(E.anchor||E.el),B=b&&b[Qp];return B?h(B):b};let q=!1;const U=(E,b,B)=>{E==null?b._vnode&&ge(b._vnode,null,null,!0):v(b._vnode||null,E,b,null,null,null,B),b._vnode=E,q||(q=!0,Ro(),Fr(),q=!1)},Q={p:v,um:ge,m:Fe,r:Ne,mt:se,mc:Z,pc:J,pbc:T,n:x,o:e};let fe,ye;return t&&([fe,ye]=t(Q)),{render:U,hydrate:fe,createApp:C1(U,fe)}}function _i({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function yn({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Zu(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Qu(e,t,n=!1){const l=e.children,r=t.children;if(re(l)&&re(r))for(let i=0;i>1,e[n[o]]0&&(t[l]=n[i-1]),n[i]=l)}}for(i=n.length,a=n[i-1];i-- >0;)n[i]=a,a=t[a];return n}function Xu(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Xu(t)}function Ko(e){if(e)for(let t=0;tSe(R1);function za(e,t){return $a(e,null,t)}function pe(e,t,n){return $a(e,t,n)}function $a(e,t,n=xe){const{immediate:l,deep:r,flush:i,once:a}=n,o=Me({},n),c=t&&l||!t&&i!=="post";let u;if(Qn){if(i==="sync"){const p=M1();u=p.__watcherHandles||(p.__watcherHandles=[])}else if(!c){const p=()=>{};return p.stop=$t,p.resume=$t,p.pause=$t,p}}const d=Ue;o.call=(p,g,v)=>Dt(p,d,g,v);let f=!1;i==="post"?o.scheduler=p=>{dt(p,d&&d.suspense)}:i!=="sync"&&(f=!0,o.scheduler=(p,g)=>{g?p():Ma(p)}),o.augmentJob=p=>{t&&(p.flags|=4),f&&(p.flags|=2,d&&(p.id=d.uid,p.i=d))};const h=Wp(e,t,o);return Qn&&(u?u.push(h):c&&h()),h}function V1(e,t,n){const l=this.proxy,r=ze(e)?e.includes(".")?ed(l,e):()=>l[e]:e.bind(l,l);let i;ie(t)?i=t:(i=t.handler,n=t);const a=er(this),o=$a(r,i.bind(l),n);return a(),o}function ed(e,t){const n=t.split(".");return()=>{let l=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${at(t)}Modifiers`]||e[`${Pn(t)}Modifiers`];function j1(e,t,...n){if(e.isUnmounted)return;const l=e.vnode.props||xe;let r=n;const i=t.startsWith("update:"),a=i&&N1(l,t.slice(7));a&&(a.trim&&(r=n.map(d=>ze(d)?d.trim():d)),a.number&&(r=n.map(cp)));let o,c=l[o=di(t)]||l[o=di(at(t))];!c&&i&&(c=l[o=di(Pn(t))]),c&&Dt(c,e,6,r);const u=l[o+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[o])return;e.emitted[o]=!0,Dt(u,e,6,r)}}function td(e,t,n=!1){const l=t.emitsCache,r=l.get(e);if(r!==void 0)return r;const i=e.emits;let a={},o=!1;if(!ie(e)){const c=u=>{const d=td(u,t,!0);d&&(o=!0,Me(a,d))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!o?(De(e)&&l.set(e,null),null):(re(i)?i.forEach(c=>a[c]=null):Me(a,i),De(e)&&l.set(e,a),a)}function Yr(e,t){return!e||!ql(t)?!1:(t=t.slice(2).replace(/Once$/,""),_e(e,t[0].toLowerCase()+t.slice(1))||_e(e,Pn(t))||_e(e,t))}function bi(e){const{type:t,vnode:n,proxy:l,withProxy:r,propsOptions:[i],slots:a,attrs:o,emit:c,render:u,renderCache:d,props:f,data:h,setupState:p,ctx:g,inheritAttrs:v}=e,y=Rr(e);let _,A;try{if(n.shapeFlag&4){const w=r||l,F=w;_=yt(u.call(F,w,d,f,p,h,g)),A=o}else{const w=t;_=yt(w.length>1?w(f,{attrs:o,slots:a,emit:c}):w(f,null)),A=t.props?o:H1(o)}}catch(w){xl.length=0,Zl(w,e,1),_=Ve(pt)}let m=_;if(A&&v!==!1){const w=Object.keys(A),{shapeFlag:F}=m;w.length&&F&7&&(i&&w.some(Ba)&&(A=z1(A,i)),m=fn(m,A,!1,!0))}return n.dirs&&(m=fn(m,null,!1,!0),m.dirs=m.dirs?m.dirs.concat(n.dirs):n.dirs),n.transition&&On(m,n.transition),_=m,Rr(y),_}const H1=e=>{let t;for(const n in e)(n==="class"||n==="style"||ql(n))&&((t||(t={}))[n]=e[n]);return t},z1=(e,t)=>{const n={};for(const l in e)(!Ba(l)||!(l.slice(9)in t))&&(n[l]=e[l]);return n};function $1(e,t,n){const{props:l,children:r,component:i}=e,{props:a,children:o,patchFlag:c}=t,u=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return l?qo(l,a,u):!!a;if(c&8){const d=t.dynamicProps;for(let f=0;fe.__isSuspense;function rd(e,t){t&&t.pendingBranch?re(e)?t.effects.push(...e):t.effects.push(e):Zp(e)}const ht=Symbol.for("v-fgt"),Tn=Symbol.for("v-txt"),pt=Symbol.for("v-cmt"),Cl=Symbol.for("v-stc"),xl=[];let mt=null;function U1(e=!1){xl.push(mt=e?null:[])}function G1(){xl.pop(),mt=xl[xl.length-1]||null}let Vl=1;function Wo(e,t=!1){Vl+=e,e<0&&mt&&t&&(mt.hasOnce=!0)}function id(e){return e.dynamicChildren=Vl>0?mt||Un:null,G1(),Vl>0&&mt&&mt.push(e),e}function AE(e,t,n,l,r,i){return id(Sl(e,t,n,l,r,i,!0))}function K1(e,t,n,l,r){return id(Ve(e,t,n,l,r,!0))}function Vr(e){return e?e.__v_isVNode===!0:!1}function Bn(e,t){return e.type===t.type&&e.key===t.key}const ad=({key:e})=>e??null,xr=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ze(e)||Re(e)||ie(e)?{i:At,r:e,k:t,f:!!n}:e:null);function Sl(e,t=null,n=null,l=0,r=null,i=e===ht?0:1,a=!1,o=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ad(t),ref:t&&xr(t),scopeId:Ou,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:l,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:At};return o?(Ua(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=ze(n)?8:16),Vl>0&&!a&&mt&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&mt.push(c),c}const Ve=q1;function q1(e,t=null,n=null,l=0,r=null,i=!1){if((!e||e===g1)&&(e=pt),Vr(e)){const o=fn(e,t,!0);return n&&Ua(o,n),Vl>0&&!i&&mt&&(o.shapeFlag&6?mt[mt.indexOf(e)]=o:mt.push(o)),o.patchFlag=-2,o}if(r0(e)&&(e=e.__vccOpts),t){t=W1(t);let{class:o,style:c}=t;o&&!ze(o)&&(t.class=Ta(o)),De(c)&&(Ra(c)&&!re(c)&&(c=Me({},c)),t.style=Sa(c))}const a=ze(e)?1:ld(e)?128:Du(e)?64:De(e)?4:ie(e)?2:0;return Sl(e,t,n,l,r,a,i,!0)}function W1(e){return e?Ra(e)||Uu(e)?Me({},e):e:null}function fn(e,t,n=!1,l=!1){const{props:r,ref:i,patchFlag:a,children:o,transition:c}=e,u=t?J1(r||{},t):r,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&ad(u),ref:t&&t.ref?n&&i?re(i)?i.concat(xr(t)):[i,xr(t)]:xr(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ht?a===-1?16:a|16:a,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&fn(e.ssContent),ssFallback:e.ssFallback&&fn(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&l&&On(d,c.clone(d)),d}function od(e=" ",t=0){return Ve(Tn,null,e,t)}function kE(e,t){const n=Ve(Cl,null,e);return n.staticCount=t,n}function yt(e){return e==null||typeof e=="boolean"?Ve(pt):re(e)?Ve(ht,null,e.slice()):Vr(e)?sn(e):Ve(Tn,null,String(e))}function sn(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:fn(e)}function Ua(e,t){let n=0;const{shapeFlag:l}=e;if(t==null)t=null;else if(re(t))n=16;else if(typeof t=="object")if(l&65){const r=t.default;r&&(r._c&&(r._d=!1),Ua(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Uu(t)?t._ctx=At:r===3&&At&&(At.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else ie(t)?(t={default:t,_ctx:At},n=32):(t=String(t),l&64?(n=16,t=[od(t)]):n=8);e.children=t,e.shapeFlag|=n}function J1(...e){const t={};for(let n=0;nUe||At;let Nr,Wi;{const e=Kr(),t=(n,l)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(l),i=>{r.length>1?r.forEach(a=>a(i)):r[0](i)}};Nr=t("__VUE_INSTANCE_SETTERS__",n=>Ue=n),Wi=t("__VUE_SSR_SETTERS__",n=>Qn=n)}const er=e=>{const t=Ue;return Nr(e),e.scope.on(),()=>{e.scope.off(),Nr(t)}},Jo=()=>{Ue&&Ue.scope.off(),Nr(null)};function sd(e){return e.vnode.shapeFlag&4}let Qn=!1;function X1(e,t=!1,n=!1){t&&Wi(t);const{props:l,children:r}=e.vnode,i=sd(e);x1(e,l,i,t),O1(e,r,n);const a=i?e0(e,t):void 0;return t&&Wi(!1),a}function e0(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,_1);const{setup:l}=n;if(l){vn();const r=e.setupContext=l.length>1?n0(e):null,i=er(e),a=Yl(l,e,0,[e.props,r]),o=iu(a);if(mn(),i(),(o||e.sp)&&!Wn(e)&&Na(e),o){if(a.then(Jo,Jo),t)return a.then(c=>{Yo(e,c,t)}).catch(c=>{Zl(c,e,0)});e.asyncDep=a}else Yo(e,a,t)}else cd(e,t)}function Yo(e,t,n){ie(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:De(t)&&(e.setupState=Bu(t)),cd(e,n)}let Zo;function cd(e,t,n){const l=e.type;if(!e.render){if(!t&&Zo&&!l.render){const r=l.template||ja(e).template;if(r){const{isCustomElement:i,compilerOptions:a}=e.appContext.config,{delimiters:o,compilerOptions:c}=l,u=Me(Me({isCustomElement:i,delimiters:o},a),c);l.render=Zo(r,u)}}e.render=l.render||$t}{const r=er(e);vn();try{b1(e)}finally{mn(),r()}}}const t0={get(e,t){return Ye(e,"get",""),e[t]}};function n0(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,t0),slots:e.slots,emit:e.emit,expose:t}}function Ga(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Bu(Vp(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Bl)return Bl[n](e)},has(t,n){return n in t||n in Bl}})):e.proxy}function l0(e,t=!0){return ie(e)?e.displayName||e.name:e.name||t&&e.__name}function r0(e){return ie(e)&&"__vccOpts"in e}const k=(e,t)=>Kp(e,t,Qn);function s(e,t,n){const l=arguments.length;return l===2?De(t)&&!re(t)?Vr(t)?Ve(e,null,[t]):Ve(e,t):Ve(e,null,t):(l>3?n=Array.prototype.slice.call(arguments,2):l===3&&Vr(n)&&(n=[n]),Ve(e,t,n))}const i0="3.5.13";/** +* @vue/runtime-dom v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ji;const Qo=typeof window<"u"&&window.trustedTypes;if(Qo)try{Ji=Qo.createPolicy("vue",{createHTML:e=>e})}catch{}const ud=Ji?e=>Ji.createHTML(e):e=>e,a0="http://www.w3.org/2000/svg",o0="http://www.w3.org/1998/Math/MathML",Jt=typeof document<"u"?document:null,Xo=Jt&&Jt.createElement("template"),s0={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,l)=>{const r=t==="svg"?Jt.createElementNS(a0,e):t==="mathml"?Jt.createElementNS(o0,e):n?Jt.createElement(e,{is:n}):Jt.createElement(e);return e==="select"&&l&&l.multiple!=null&&r.setAttribute("multiple",l.multiple),r},createText:e=>Jt.createTextNode(e),createComment:e=>Jt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Jt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,l,r,i){const a=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Xo.innerHTML=ud(l==="svg"?`${e}`:l==="mathml"?`${e}`:e);const o=Xo.content;if(l==="svg"||l==="mathml"){const c=o.firstChild;for(;c.firstChild;)o.appendChild(c.firstChild);o.removeChild(c)}t.insertBefore(o,n)}return[a?a.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},nn="transition",ml="animation",Xn=Symbol("_vtc"),dd={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},fd=Me({},Iu,dd),c0=e=>(e.displayName="Transition",e.props=fd,e),el=c0((e,{slots:t})=>s(e1,hd(e),t)),An=(e,t=[])=>{re(e)?e.forEach(n=>n(...t)):e&&e(...t)},es=e=>e?re(e)?e.some(t=>t.length>1):e.length>1:!1;function hd(e){const t={};for(const O in e)O in dd||(t[O]=e[O]);if(e.css===!1)return t;const{name:n="v",type:l,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:a=`${n}-enter-active`,enterToClass:o=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:u=a,appearToClass:d=o,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:h=`${n}-leave-active`,leaveToClass:p=`${n}-leave-to`}=e,g=u0(r),v=g&&g[0],y=g&&g[1],{onBeforeEnter:_,onEnter:A,onEnterCancelled:m,onLeave:w,onLeaveCancelled:F,onBeforeAppear:V=_,onAppear:R=A,onAppearCancelled:Z=m}=t,M=(O,X,se,he)=>{O._enterCancelled=he,rn(O,X?d:o),rn(O,X?u:a),se&&se()},T=(O,X)=>{O._isLeaving=!1,rn(O,f),rn(O,p),rn(O,h),X&&X()},$=O=>(X,se)=>{const he=O?R:A,K=()=>M(X,O,se);An(he,[X,K]),ts(()=>{rn(X,O?c:i),jt(X,O?d:o),es(he)||ns(X,l,v,K)})};return Me(t,{onBeforeEnter(O){An(_,[O]),jt(O,i),jt(O,a)},onBeforeAppear(O){An(V,[O]),jt(O,c),jt(O,u)},onEnter:$(!1),onAppear:$(!0),onLeave(O,X){O._isLeaving=!0;const se=()=>T(O,X);jt(O,f),O._enterCancelled?(jt(O,h),Yi()):(Yi(),jt(O,h)),ts(()=>{O._isLeaving&&(rn(O,f),jt(O,p),es(w)||ns(O,l,y,se))}),An(w,[O,se])},onEnterCancelled(O){M(O,!1,void 0,!0),An(m,[O])},onAppearCancelled(O){M(O,!0,void 0,!0),An(Z,[O])},onLeaveCancelled(O){T(O),An(F,[O])}})}function u0(e){if(e==null)return null;if(De(e))return[yi(e.enter),yi(e.leave)];{const t=yi(e);return[t,t]}}function yi(e){return up(e)}function jt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Xn]||(e[Xn]=new Set)).add(t)}function rn(e,t){t.split(/\s+/).forEach(l=>l&&e.classList.remove(l));const n=e[Xn];n&&(n.delete(t),n.size||(e[Xn]=void 0))}function ts(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let d0=0;function ns(e,t,n,l){const r=e._endId=++d0,i=()=>{r===e._endId&&l()};if(n!=null)return setTimeout(i,n);const{type:a,timeout:o,propCount:c}=pd(e,t);if(!a)return l();const u=a+"end";let d=0;const f=()=>{e.removeEventListener(u,h),i()},h=p=>{p.target===e&&++d>=c&&f()};setTimeout(()=>{d(n[g]||"").split(", "),r=l(`${nn}Delay`),i=l(`${nn}Duration`),a=ls(r,i),o=l(`${ml}Delay`),c=l(`${ml}Duration`),u=ls(o,c);let d=null,f=0,h=0;t===nn?a>0&&(d=nn,f=a,h=i.length):t===ml?u>0&&(d=ml,f=u,h=c.length):(f=Math.max(a,u),d=f>0?a>u?nn:ml:null,h=d?d===nn?i.length:c.length:0);const p=d===nn&&/\b(transform|all)(,|$)/.test(l(`${nn}Property`).toString());return{type:d,timeout:f,propCount:h,hasTransform:p}}function ls(e,t){for(;e.lengthrs(n)+rs(e[l])))}function rs(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Yi(){return document.body.offsetHeight}function f0(e,t,n){const l=e[Xn];l&&(t=(t?[t,...l]:[...l]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const is=Symbol("_vod"),h0=Symbol("_vsh"),p0=Symbol(""),v0=/(^|;)\s*display\s*:/;function m0(e,t,n){const l=e.style,r=ze(n);let i=!1;if(n&&!r){if(t)if(ze(t))for(const a of t.split(";")){const o=a.slice(0,a.indexOf(":")).trim();n[o]==null&&Sr(l,o,"")}else for(const a in t)n[a]==null&&Sr(l,a,"");for(const a in n)a==="display"&&(i=!0),Sr(l,a,n[a])}else if(r){if(t!==n){const a=l[p0];a&&(n+=";"+a),l.cssText=n,i=v0.test(n)}}else t&&e.removeAttribute("style");is in e&&(e[is]=i?l.display:"",e[h0]&&(l.display="none"))}const as=/\s*!important$/;function Sr(e,t,n){if(re(n))n.forEach(l=>Sr(e,t,l));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const l=g0(e,t);as.test(n)?e.setProperty(Pn(l),n.replace(as,""),"important"):e[l]=n}}const os=["Webkit","Moz","ms"],Ai={};function g0(e,t){const n=Ai[t];if(n)return n;let l=at(t);if(l!=="filter"&&l in e)return Ai[t]=l;l=Wl(l);for(let r=0;rki||(A0.then(()=>ki=0),ki=Date.now());function w0(e,t){const n=l=>{if(!l._vts)l._vts=Date.now();else if(l._vts<=n.attached)return;Dt(B0(l,n.value),t,5,[l])};return n.value=e,n.attached=k0(),n}function B0(e,t){if(re(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(l=>r=>!r._stopped&&l&&l(r))}else return t}const hs=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,C0=(e,t,n,l,r,i)=>{const a=r==="svg";t==="class"?f0(e,l,a):t==="style"?m0(e,n,l):ql(t)?Ba(t)||b0(e,t,n,l,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):x0(e,t,l,a))?(us(e,t,l),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&cs(e,t,l,a,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!ze(l))?us(e,at(t),l,i,t):(t==="true-value"?e._trueValue=l:t==="false-value"&&(e._falseValue=l),cs(e,t,l,a))};function x0(e,t,n,l){if(l)return!!(t==="innerHTML"||t==="textContent"||t in e&&hs(t)&&ie(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return hs(t)&&ze(n)?!1:t in e}const vd=new WeakMap,md=new WeakMap,jr=Symbol("_moveCb"),ps=Symbol("_enterCb"),S0=e=>(delete e.props.mode,e),T0=S0({name:"TransitionGroup",props:Me({},fd,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=Xl(),l=Pu();let r,i;return Nu(()=>{if(!r.length)return;const a=e.moveClass||`${e.name||"v"}-move`;if(!P0(r[0].el,n.vnode.el,a))return;r.forEach(L0),r.forEach(O0);const o=r.filter(D0);Yi(),o.forEach(c=>{const u=c.el,d=u.style;jt(u,a),d.transform=d.webkitTransform=d.transitionDuration="";const f=u[jr]=h=>{h&&h.target!==u||(!h||/transform$/.test(h.propertyName))&&(u.removeEventListener("transitionend",f),u[jr]=null,rn(u,a))};u.addEventListener("transitionend",f)})}),()=>{const a=de(e),o=hd(a);let c=a.tag||ht;if(r=[],i)for(let u=0;u{o.split(/\s+/).forEach(c=>c&&l.classList.remove(c))}),n.split(/\s+/).forEach(o=>o&&l.classList.add(o)),l.style.display="none";const i=t.nodeType===1?t:t.parentNode;i.appendChild(l);const{hasTransform:a}=pd(l);return i.removeChild(l),a}const I0=Me({patchProp:C0},s0);let wi,vs=!1;function F0(){return wi=vs?wi:P1(I0),vs=!0,wi}const R0=(...e)=>{const t=F0().createApp(...e),{mount:n}=t;return t.mount=l=>{const r=V0(l);if(r)return n(r,!0,M0(r))},t};function M0(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function V0(e){return ze(e)?document.querySelector(e):e}var tr=e=>/^[a-z][a-z0-9+.-]*:/.test(e)||e.startsWith("//"),N0=/.md((\?|#).*)?$/,Ka=(e,t="/")=>tr(e)||e.startsWith("/")&&!e.startsWith(t)&&!N0.test(e),gn=e=>/^(https?:)?\/\//.test(e),ms=e=>{if(!e||e.endsWith("/"))return e;let t=e.replace(/(^|\/)README.md$/i,"$1index.html");return t.endsWith(".md")?t=t.substring(0,t.length-3)+".html":t.endsWith(".html")||(t=t+".html"),t.endsWith("/index.html")&&(t=t.substring(0,t.length-10)),t},j0="http://.",H0=(e,t)=>{if(!e.startsWith("/")&&t){const n=t.slice(0,t.lastIndexOf("/"));return ms(new URL(`${n}/${e}`,j0).pathname)}return ms(e)},z0=(e,t)=>{const n=Object.keys(e).sort((l,r)=>{const i=r.split("/").length-l.split("/").length;return i!==0?i:r.length-l.length});for(const l of n)if(t.startsWith(l))return l;return"/"},$0=/(#|\?)/,Ed=e=>{const[t,...n]=e.split($0);return{pathname:t,hashAndQueries:n.join("")}},U0=["link","meta","script","style","noscript","template"],G0=["title","base"],K0=([e,t,n])=>G0.includes(e)?e:U0.includes(e)?e==="meta"&&t.name?`${e}.${t.name}`:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,Object.entries(t).map(([l,r])=>typeof r=="boolean"?r?[l,""]:null:[l,r]).filter(l=>l!=null).sort(([l],[r])=>l.localeCompare(r)),n]):null,q0=e=>{const t=new Set,n=[];return e.forEach(l=>{const r=K0(l);r&&!t.has(r)&&(t.add(r),n.push(l))}),n},W0=e=>e[0]==="/"?e:`/${e}`,_d=e=>e[e.length-1]==="/"||e.endsWith(".html")?e:`${e}/`,Zr=e=>e[e.length-1]==="/"?e.slice(0,-1):e,bd=e=>e[0]==="/"?e.slice(1):e,nr=e=>Object.prototype.toString.call(e)==="[object Object]",ke=e=>typeof e=="string";const J0="modulepreload",Y0=function(e){return"/"+e},gs={},C=function(t,n,l){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute("nonce"));r=Promise.all(n.map(o=>{if(o=Y0(o),o in gs)return;gs[o]=!0;const c=o.endsWith(".css"),u=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${o}"]${u}`))return;const d=document.createElement("link");if(d.rel=c?"stylesheet":J0,c||(d.as="script",d.crossOrigin=""),d.href=o,a&&d.setAttribute("nonce",a),document.head.appendChild(d),c)return new Promise((f,h)=>{d.addEventListener("load",f),d.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${o}`)))})}))}return r.then(()=>t()).catch(i=>{const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=i,window.dispatchEvent(a),!a.defaultPrevented)throw i})},Z0=JSON.parse("{}"),Q0=Object.fromEntries([["/",{loader:()=>C(()=>import("./index.html-By-pQ7Wl.js"),[]),meta:{t:"首页",i:"home"}}],["/about-the-author/",{loader:()=>C(()=>import("./index.html-DqVhfNm4.js"),[]),meta:{d:1706204085e3,e:` +

留空

+`,r:{minutes:.02,words:5},t:"关于我",y:"a"}}],["/bigdata/",{loader:()=>C(()=>import("./index.html-D1R9zQcp.js"),[]),meta:{d:156672164e4,e:` +`,r:{minutes:0,words:1},t:"Spark",y:"a"}}],["/cloudnative/",{loader:()=>C(()=>import("./index.html-DlwW5Vje.js"),[]),meta:{d:156672164e4,t:"",y:"a"}}],["/database/",{loader:()=>C(()=>import("./index.html-DWXiwN5o.js"),[]),meta:{d:1706182936e3,e:` +

目录留空

+`,r:{minutes:.02,words:5},t:"Database",y:"a"}}],["/donate/",{loader:()=>C(()=>import("./index.html-D4WG7yPk.js"),[]),meta:{d:1706169851e3,e:` +
微信支付
微信支付
`,r:{minutes:.04,words:12},t:"微信支付",y:"a"}}],["/interesting/",{loader:()=>C(()=>import("./index.html-B1Akg086.js"),[]),meta:{d:171606205e4,e:` +`,r:{minutes:.02,words:6},t:"好玩的",y:"a"}}],["/interesting/chatgpt.html",{loader:()=>C(()=>import("./chatgpt.html-BgeRWiYw.js"),[]),meta:{d:1706186538e3,e:` +

想体验的可以去微信上搜索【旅行的树】公众号。

+

一、ChatGPT注册

`,r:{minutes:9.96,words:2987},t:"微信公众号-chatgpt智能客服搭建",y:"a"}}],["/interesting/idea%E8%AE%BE%E7%BD%AE.html",{loader:()=>C(()=>import("./idea设置.html-BThIkzaw.js"),[]),meta:{d:1715445447e3,e:` +

一、类注释

+

想要在生成类的时候,自动带上author和时间,效果如下:

`,r:{minutes:1.33,words:399},t:"Jetbrains Idea设置",y:"a"}}],["/interesting/starcraft-ai.html",{loader:()=>C(()=>import("./starcraft-ai.html-C018rwGF.js"),[]),meta:{d:1706288023e3,e:` +

非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

`,r:{minutes:2.72,words:817},t:"StarCraft Ⅱ 人工智能教程",y:"a"}}],["/interesting/tesla.html",{loader:()=>C(()=>import("./tesla.html-K0riacrL.js"),[]),meta:{d:1706186538e3,e:` +

一、特斯拉应用申请

+

1.1 创建 Tesla 账户

+

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

`,r:{minutes:4.5,words:1351},t:"Tesla api",y:"a"}}],["/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html",{loader:()=>C(()=>import("./一些不常用但很实用的命令.html-BtEABYaE.js"),[]),meta:{d:1708752707e3,e:` +

https连接耗时检测

+
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\\n" -so /dev/null https://zhls.qq.com/test-nginx
+
`,r:{minutes:.16,words:47},t:"一些不常用但很实用的命令",y:"a"}}],["/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html",{loader:()=>C(()=>import("./基于OLAP做业务监控.html-BNdB--VB.js"),[]),meta:{d:1715617566e3,e:` +

在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

`,r:{minutes:1.95,words:586},t:"基于OLAP做业务监控",y:"a"}}],["/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html",{loader:()=>C(()=>import("./小程序反编译.html-BNnmGgbc.js"),[]),meta:{d:171008157e4,e:` +

第一步:电脑端提取

+

先找到小程序保存的地址,一般先找到微信的文件管理下

`,r:{minutes:.82,words:246},t:"小程序反编译",y:"a"}}],["/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html",{loader:()=>C(()=>import("./广州图书馆借阅抓取.html-CPwBtyqR.js"),[]),meta:{d:1706596625e3,e:` +

欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

`,r:{minutes:6.83,words:2049},t:"广州图书馆借阅抓取",y:"a"}}],["/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html",{loader:()=>C(()=>import("./开发工具整理.html-BKoUISo4.js"),[]),meta:{d:171008157e4,e:` +

ShadowsocksX

+

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
+win端:https://github.com/shadowsocks/shadowsocks-windows
+安卓:https://github.com/shadowsocks/shadowsocks-android

`,r:{minutes:.12,words:36},t:"开发工具整理",y:"a"}}],["/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html",{loader:()=>C(()=>import("./程序员副业探索之电商.html-CwtUep3k.js"),[]),meta:{d:1722783109e3,e:` +

在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

`,r:{minutes:10.01,words:3002},t:"程序员副业探索之电商",y:"a"}}],["/interview/tiktok2023.html",{loader:()=>C(()=>import("./tiktok2023.html-BxK5gJWF.js"),[]),meta:{d:1680437706e3,e:`

tt一面:
+1.全程项目
+2.lc3,最长无重复子串,滑动窗口解决

+

tt二面:
+全程基础,一直追问
+1.java内存模型介绍一下
+2.volatile原理
+3.内存屏障,使用场景?(我提了在gc中有使用)
+4.gc中具体是怎么使用内存屏障的,详细介绍
+5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
+6.线程内存分配方式,tlab相关
+7.happens-before介绍一下
+8.总线风暴是什么,怎么解决
+9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
+10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
+11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

`,r:{minutes:1.15,words:344},t:"",y:"a"}}],["/java/",{loader:()=>C(()=>import("./index.html-jGjDZXTI.js"),[]),meta:{d:156672164e4,e:` +`,r:{minutes:.01,words:4},t:"Java",y:"a"}}],["/java/serverlog.html",{loader:()=>C(()=>import("./serverlog.html-bICZU0Nm.js"),[]),meta:{d:1579957849e3,e:` +

服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
+
+stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

`,r:{minutes:.57,words:172},t:"被挖矿攻击",y:"a"}}],["/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html",{loader:()=>C(()=>import("./在 Spring 6 中使用虚拟线程.html-DsVzFk6c.js"),[]),meta:{d:1706186538e3,e:` +

一、简介

+

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

`,r:{minutes:4.83,words:1448},t:"在 Spring 6 中使用虚拟线程",y:"a"}}],["/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html",{loader:()=>C(()=>import("./基于kubernetes的分布式限流.html-B1ZIsZdu.js"),[]),meta:{d:1649482551e3,e:` +

做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

`,r:{minutes:7.18,words:2155},t:"基于kubernetes的分布式限流",y:"a"}}],["/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html",{loader:()=>C(()=>import("./流程编排LiteFlow.html-CvCVGQll.js"),[]),meta:{d:1725814553e3,e:` +

LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

`,r:{minutes:7.11,words:2134},t:"流程编排LiteFlow",y:"a"}}],["/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html",{loader:()=>C(()=>import("./高可用.html-DjOPo7tP.js"),[]),meta:{d:171008157e4,e:` +

4个9(99.99)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SentinelHystrix(维护状态)Resilience4j(Spring推荐)
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口滑动窗口Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性支持多种数据源支持多种数据源有限支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统的自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统
`,r:{minutes:4.27,words:1281},t:"高可用",y:"a"}}],["/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html",{loader:()=>C(()=>import("./高并发.html-B4hi1h21.js"),[]),meta:{d:171008157e4,e:` +

QPS

+

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

`,r:{minutes:4.28,words:1285},t:"高并发",y:"a"}}],["/java/%E9%AB%98%E6%80%A7%E8%83%BD.html",{loader:()=>C(()=>import("./高性能.html-CN7rmRWu.js"),[]),meta:{d:171008157e4,e:` +

响应时间

+

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

`,r:{minutes:3.03,words:909},t:"高性能",y:"a"}}],["/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html",{loader:()=>C(()=>import("./高性能高并发高可用的一些思考.html-DLPHuyVJ.js"),[]),meta:{d:1706983034e3,e:` +

TODO待补充

+

异步

+

Jmeter

`,r:{minutes:.36,words:109},t:"高性能高并发高可用的一些思考",y:"a"}}],["/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html",{loader:()=>C(()=>import("./Jenkins的一些笔记.html--8cnUWGp.js"),[]),meta:{d:1708751576e3,e:` +

公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

`,r:{minutes:3.98,words:1195},t:"Jenkins的一些笔记",y:"a"}}],["/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html",{loader:()=>C(()=>import("./Kubernetes容器日志收集.html-SN4rN7dm.js"),[]),meta:{d:1708751576e3,e:` +

日志采集方式

+

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

`,r:{minutes:10.62,words:3187},t:"Kubernetes容器日志收集",y:"a"}}],["/kubernetes/",{loader:()=>C(()=>import("./index.html-BEv74XKv.js"),[]),meta:{d:156672164e4,e:` +

包含CICD、Kubernetes

+`,r:{minutes:.03,words:8},t:"目录",y:"a"}}],["/kubernetes/request_limit.html",{loader:()=>C(()=>import("./request_limit.html-CBNYeI2L.js"),[]),meta:{d:1708083988e3,e:` +

我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

`,r:{minutes:2.21,words:663},t:"Kubernetes之request 和 limit详解",y:"a"}}],["/kubernetes/spark%20on%20k8s%20operator.html",{loader:()=>C(()=>import("./spark on k8s operator.html-D9Tpznzk.js"),[]),meta:{d:1655036363e3,e:` +

Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

`,r:{minutes:3.51,words:1054},t:"spark on k8s operator",y:"a"}}],["/life/2017.html",{loader:()=>C(()=>import("./2017.html-DzgcI9cv.js"),[]),meta:{d:157995829e4,e:` +

2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
+17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

`,r:{minutes:11.22,words:3366},t:"2017",y:"a"}}],["/life/2018.html",{loader:()=>C(()=>import("./2018.html-UHfvoYE6.js"),[]),meta:{d:1579957849e3,e:` +

年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

`,r:{minutes:4.35,words:1306},t:"2018",y:"a"}}],["/life/2019.html",{loader:()=>C(()=>import("./2019.html-Df2iUZPh.js"),[]),meta:{d:157995829e4,e:` +

2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
+19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

`,r:{minutes:4.43,words:1329},t:"2019",y:"a"}}],["/link/main.html",{loader:()=>C(()=>import("./main.html-B2OQipck.js"),[]),meta:{d:1706846339e3,e:` +`,r:{minutes:.28,words:83},t:"友链地址",y:"a"}}],["/middleware/",{loader:()=>C(()=>import("./index.html-0EEVTum5.js"),[]),meta:{d:170606734e4,e:` +`,r:{minutes:.01,words:3},t:"中间件",y:"a"}}],["/open-source-project/",{loader:()=>C(()=>import("./index.html-BcZZ4qCt.js"),[]),meta:{d:1706092222e3,e:`

jfaowejfoewj

+`,r:{minutes:0,words:1},t:"",y:"a"}}],["/stock/",{loader:()=>C(()=>import("./index.html-nZcQpO1z.js"),[]),meta:{d:1719930789e3,e:` +`,r:{minutes:3.15,words:946},t:"股票预测",y:"a"}}],["/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html",{loader:()=>C(()=>import("./赛力斯.html-BrY99iGU.js"),[]),meta:{d:1719930789e3,e:` +

update

+`,r:{minutes:.01,words:4},t:"赛力斯",y:"a"}}],["/system-design/feed.html",{loader:()=>C(()=>import("./feed.html-BcapapFL.js"),[]),meta:{d:171008157e4,e:` +

https://blog.csdn.net/weixin_45583158/article/details/128195940

`,r:{minutes:.02,words:7},t:"Feed系统设计",y:"a"}}],["/about-the-author/personal-life/2024-07-24.html",{loader:()=>C(()=>import("./2024-07-24.html-9GUkpnJj.js"),[]),meta:{d:1721836332e3,e:` +

新买了台air m3,午夜色的,心心念念了好久

+IMG_20240723_235443`,r:{minutes:.34,words:102},t:"2024-07-24",y:"a"}}],["/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html",{loader:()=>C(()=>import("./2024-11-09上海迪斯尼.html-CU80Bk9D.js"),[]),meta:{d:1731254136e3,e:` +`,r:{minutes:.04,words:12},t:"2024-11-09上海迪斯尼",y:"a"}}],["/about-the-author/personal-life/wewe.html",{loader:()=>C(()=>import("./wewe.html-BLY5_jfL.js"),[]),meta:{d:1706244827e3,e:` +

安安静静的开发,搞点好玩的。

+
硅谷
硅谷
`,r:{minutes:.07,words:21},t:"自我介绍",y:"a"}}],["/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html",{loader:()=>C(()=>import("./个人作品.html-DDpX9I3v.js"),[]),meta:{d:1706349103e3,e:` +
助眠风扇
助眠风扇
`,r:{minutes:.05,words:14},t:"小程序",y:"a"}}],["/bigdata/spark/elastic-spark.html",{loader:()=>C(()=>import("./elastic-spark.html-Cz2jVT52.js"),[]),meta:{d:165157815e4,e:` +

Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
+(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
+(2)数据RDBMS:mysql、oracle、mssql
+(3)NOSQL数据库:HBase、ES、Redis
+(4)消息对象:Redis

`,r:{minutes:1.71,words:514},t:"elastic spark",y:"a"}}],["/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html",{loader:()=>C(()=>import("./elasticsearch源码debug.html-Bo6HcrbI.js"),[]),meta:{d:1706186538e3,e:` +

一、下载源代码

+

直接用idea下载代码https://github.com/elastic/elasticsearch.git
+image

`,r:{minutes:.52,words:157},t:"【elasticsearch】源码debug",y:"a"}}],["/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html",{loader:()=>C(()=>import("./【elasticsearch】搜索过程详解.html-Dg8HV8OO.js"),[]),meta:{d:164778928e4,e:` +

本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

`,r:{minutes:8.92,words:2677},t:"【elasticsearch】搜索过程详解",y:"a"}}],["/database/elasticsearch/%E5%9F%BA%E7%A1%80.html",{loader:()=>C(()=>import("./基础.html-Bsofr9G8.js"),[]),meta:{d:1708786978e3,e:` +

一、ElasticSearch基础

+

1、什么是Elasticsearch:
+Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
+全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

`,r:{minutes:14.53,words:4360},t:"基础",O:100,y:"a"}}],["/database/mysql/1mysql.html",{loader:()=>C(()=>import("./1mysql.html-CW43wjS_.js"),[]),meta:{d:1679834663e3,e:` +
img
img
`,r:{minutes:.02,words:5},t:"Mysql",y:"a"}}],["/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html",{loader:()=>C(()=>import("./数据库缓存.html-DNuD8_hU.js"),[]),meta:{d:1679834663e3,e:` +

在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

`,r:{minutes:.34,words:101},t:"数据库缓存",y:"a"}}],["/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html",{loader:()=>C(()=>import("./行锁,表锁,意向锁.html-DwU2qcUM.js"),[]),meta:{d:171008157e4,e:` +`,r:{minutes:.03,words:9},t:"行锁,表锁,意向锁",I:!1,y:"a"}}],["/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html",{loader:()=>C(()=>import("./RedissonLock分布式锁源码分析.html-DBlO1MVO.js"),[]),meta:{d:1707204155e3,e:` +

最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
+目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

`,r:{minutes:9.66,words:2897},t:"RedissonLock分布式锁源码分析",y:"a"}}],["/database/redis/redis%E7%BC%93%E5%AD%98.html",{loader:()=>C(()=>import("./redis缓存.html-H1xCPktw.js"),[]),meta:{d:1579957849e3,e:` +

一、概述

+

1.1 缓存介绍

+

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
+缓存常用语:
+数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
+可查看Redis实战(一) 使用缓存合理性

`,r:{minutes:12.08,words:3623},t:"Redis缓存",y:"a"}}],["/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html",{loader:()=>C(()=>import("./一致性hash算法和哈希槽.html-a1P2YWrj.js"),[]),meta:{d:171008157e4,e:` +`,r:{minutes:.04,words:12},t:"一致性hash算法和哈希槽",I:!1,y:"a"}}],["/interesting/mini%E4%B8%BB%E6%9C%BA/",{loader:()=>C(()=>import("./index.html-wU0YCmBl.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:13},t:"mini主机",i:"pen",O:2,y:"a"}}],["/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html",{loader:()=>C(()=>import("./mac连接主机.html-BbWWw_2k.js"),[]),meta:{d:173290591e4,e:` +

一、mac端

+

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

`,r:{minutes:1.72,words:515},t:"mac通过网线连接主机(fnOS)",y:"a"}}],["/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html",{loader:()=>C(()=>import("./买了个mini主机.html-DMCiuYMs.js"),[]),meta:{d:1730035985e3,e:` +

虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

`,r:{minutes:5.88,words:1763},t:"买了个mini主机",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html",{loader:()=>C(()=>import("./1.历史与架构.html-B9tl8qmz.js"),[]),meta:{d:1579574285e3,e:` +

各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
+大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

`,r:{minutes:6.79,words:2038},t:"1.历史与架构",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html",{loader:()=>C(()=>import("./10.历时8年最终改版.html-BVYNR54w.js"),[]),meta:{d:1706165044e3,e:` +

不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

`,r:{minutes:5.78,words:1733},t:"10.历时8年最终改版",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html",{loader:()=>C(()=>import("./2.Lucene的使用.html-9FjHGi9W.js"),[]),meta:{d:1579574285e3,e:` +

首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

`,r:{minutes:6.82,words:2046},t:"2.Lucene的使用",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html",{loader:()=>C(()=>import("./3.定时任务.html-NSgzVWXM.js"),[]),meta:{d:1579574285e3,e:` +

先看一下Quartz的架构图:

+
`,r:{minutes:3.55,words:1064},t:"3.定时任务",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html",{loader:()=>C(()=>import("./4.日志系统.html-BH3nB-v9.js"),[]),meta:{d:1579574285e3,e:` +

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

`,r:{minutes:9.64,words:2891},t:"4.日志系统.md",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html",{loader:()=>C(()=>import("./5.小集群部署.html-BN5IQG4o.js"),[]),meta:{d:1579574285e3,e:` +

欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
+洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

`,r:{minutes:9.5,words:2849},t:"5.小集群部署.md",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html",{loader:()=>C(()=>import("./6.数据库备份.html-Ca5VIWc1.js"),[]),meta:{d:1579574285e3,e:` +

先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

`,r:{minutes:5.77,words:1732},t:"6.数据库备份",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html",{loader:()=>C(()=>import("./7.那些牛逼的插件.html-Br7NfWE4.js"),[]),meta:{d:1579574285e3,e:` +

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
+建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
+本站主要用到的插件有:
+1.wowslider
+2.畅言
+3.Editor.md
+4.highchart、echart
+5.百度分享
+6.waterfall.js
+7.心知天气
+8.标签云

`,r:{minutes:7.24,words:2173},t:"7.那些牛逼的插件",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html",{loader:()=>C(()=>import("./8.基于贝叶斯的情感分析.html-DopYDuB6.js"),[]),meta:{d:156672164e4,e:` +`,r:{minutes:.04,words:11},t:"8.基于贝叶斯的情感分析",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html",{loader:()=>C(()=>import("./9.网站性能优化.html-8ms_Vznn.js"),[]),meta:{d:156672164e4,e:` +`,r:{minutes:.02,words:7},t:"9.网站性能优化",y:"a"}}],["/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/",{loader:()=>C(()=>import("./index.html-BXiz75su.js"),[]),meta:{d:1708056026e3,e:`

jofjweoiaejof

+`,r:{minutes:.05,words:15},t:"个人网站",i:"pen",O:1,y:"a"}}],["/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html",{loader:()=>C(()=>import("./JVM调优参数.html-BtZgPFK4.js"),[]),meta:{d:1579957849e3,e:` +

一、堆大小设置

+

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

`,r:{minutes:9.13,words:2739},t:"JVM调优参数",y:"a"}}],["/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html",{loader:()=>C(()=>import("./Java内存模型.html-Bis3Hglq.js"),[]),meta:{d:1708075355e3,e:` +

本文转载自深入理解JVM-内存模型(jmm)和GC

`,r:{minutes:46.11,words:13833},t:"Java内存模型(JMM)",y:"a"}}],["/java/JVM/",{loader:()=>C(()=>import("./index.html-B9smc87Y.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:11},t:"JVM",i:"pen",O:100,y:"a"}}],["/java/JVM/cms.html",{loader:()=>C(()=>import("./cms.html-Dfw5I__q.js"),[]),meta:{d:1708056026e3,e:` +

JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

+

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

`,r:{minutes:.73,words:218},t:"CMS",y:"a"}}],["/java/JVM/g1.html",{loader:()=>C(()=>import("./g1.html-CcgAHdWl.js"),[]),meta:{d:1708056026e3,e:` +

本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

`,r:{minutes:39.5,words:11851},t:"G1",y:"a"}}],["/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html",{loader:()=>C(()=>import("./jvm(java虚拟机).html-CpFuSPpy.js"),[]),meta:{d:171008157e4,e:`

一、了解JVM

+

1、什么是JVM

+

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

`,r:{minutes:12.39,words:3717},t:"JVM(java虚拟机)",i:"pen",I:!1,y:"a"}}],["/java/JVM/zgc.html",{loader:()=>C(()=>import("./zgc.html-Cj33F57G.js"),[]),meta:{d:1708056026e3,e:` +

本文转载自12 张图带你彻底理解 ZGC

`,r:{minutes:11.65,words:3494},t:"ZGC",y:"a"}}],["/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html",{loader:()=>C(()=>import("./一次jvm调优过程.html-zaM0dK5_.js"),[]),meta:{d:1579573736e3,e:` +

前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
+该程序task主要分为三个模块:
+console进行一些cron的配置(表达式、任务名称、任务组等);
+schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
+client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
+整体架构跟github上开源的xxl-job类似,也可以参考一下。

`,r:{minutes:6.28,words:1884},t:"一次jvm调优过程",y:"a"}}],["/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html",{loader:()=>C(()=>import("./常用GC回收器.html-Br8UVQp8.js"),[]),meta:{d:1708077309e3,e:` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
垃圾回收器采用的GC算法代次
Serial复制新生代
Parallel复制新生代
ParNew复制新生代
CMS (Concurrent Mark-Sweep)标记-清除老年代
G1 (Garbage-First)标记-整理老年代
ZGC (Z Garbage Collector)标记-整理老年代
Shenandoah标记-复制(独立的全局复制阶段)老年代
`,r:{minutes:2.95,words:886},t:"主流GC收集器采用的算法",y:"a"}}],["/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html",{loader:()=>C(()=>import("./引用计数和根可达算法.html-BVqGPH4F.js"),[]),meta:{d:1708075355e3,e:` +

本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

`,r:{minutes:11.61,words:3483},t:"引用计数和根可达算法",y:"a"}}],["/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html",{loader:()=>C(()=>import("./调优思路.html-DzuQiit4.js"),[]),meta:{d:1708083988e3,e:` +

在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

`,r:{minutes:.7,words:210},t:"JVM调优思路",I:!1,y:"a"}}],["/java/SpringBoot/",{loader:()=>C(()=>import("./index.html-DGy09Ap4.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:11},t:"SpringBoot",i:"pen",O:100,y:"a"}}],["/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html",{loader:()=>C(()=>import("./Spring Boot Prometheus使用.html-CvPEGSZS.js"),[]),meta:{d:1706363521e3,e:` +

一、基本原理

+

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

`,r:{minutes:3.49,words:1046},t:"Spring Boot Prometheus使用",y:"a"}}],["/java/SpringBoot/aop.html",{loader:()=>C(()=>import("./aop.html-C7ud1qex.js"),[]),meta:{d:1706596625e3,e:` +

一、概述

+

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

`,r:{minutes:7.17,words:2150},t:"AOP",y:"a"}}],["/java/SpringBoot/webflux.html",{loader:()=>C(()=>import("./webflux.html-KjGFiVkN.js"),[]),meta:{d:1706349103e3,e:` +

1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?

+

ChatGpt:
+Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

`,r:{minutes:3.07,words:921},t:"Webflux",y:"a"}}],["/java/io/",{loader:()=>C(()=>import("./index.html-CpCz4LWW.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:11},t:"I/O",i:"pen",O:100,y:"a"}}],["/java/io/nio.html",{loader:()=>C(()=>import("./nio.html-DVcQhX_V.js"),[]),meta:{d:171008157e4,e:` +

一、简介

+

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

`,r:{minutes:1.96,words:587},t:"JAVA NIO",y:"a"}}],["/java/%E7%BA%BF%E7%A8%8B/",{loader:()=>C(()=>import("./index.html-CMe--mJk.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:12},t:"线程",i:"pen",O:100,y:"a"}}],["/java/%E7%BA%BF%E7%A8%8B/synchronized.html",{loader:()=>C(()=>import("./synchronized.html-B2MTfSju.js"),[]),meta:{d:1708075355e3,e:` +

偏向锁在JDK 15后已经废弃

+

一、什么是synchronized

`,r:{minutes:16.38,words:4915},t:"synchronized",y:"a"}}],["/java/%E7%BA%BF%E7%A8%8B/volatile.html",{loader:()=>C(()=>import("./volatile.html-C-UWQAjc.js"),[]),meta:{d:1708056026e3,e:` +

Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

`,r:{minutes:.54,words:161},t:"volatile",y:"a"}}],["/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html",{loader:()=>C(()=>import("./IP、HTTP、HTTPS、HTTP2.0.html-DvomVRAD.js"),[]),meta:{d:1707203159e3,e:` +

HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

`,r:{minutes:19.51,words:5852},t:"TCP/IP、HTTP、HTTPS、HTTP2.0",y:"a"}}],["/java/%E7%BD%91%E7%BB%9C/",{loader:()=>C(()=>import("./index.html-DaAoZU-K.js"),[]),meta:{d:1708056026e3,r:{minutes:.04,words:12},t:"网络",i:"pen",O:100,y:"a"}}],["/kubernetes/devops/",{loader:()=>C(()=>import("./index.html-muK9iEFv.js"),[]),meta:{d:1708056026e3,e:`

jofjweoiaejof

+`,r:{minutes:.04,words:12},t:"DevOps",i:"pen",O:1,y:"a"}}],["/kubernetes/devops/devops-ping-tai.html",{loader:()=>C(()=>import("./devops-ping-tai.html-CjTXGrBr.js"),[]),meta:{d:1566437746e3,e:` +

DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

`,r:{minutes:11.96,words:3589},t:"DevOps平台.md",y:"a"}}],["/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html",{loader:()=>C(()=>import("./中间件——canal小记.html-Bb5S9Lg5.js"),[]),meta:{d:1707204155e3,e:` +

接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

`,r:{minutes:5.37,words:1610},t:"canal小记",y:"a"}}],["/middleware/kafka/kafka.html",{loader:()=>C(()=>import("./kafka.html-tMeeu2BM.js"),[]),meta:{d:164778928e4,e:` +

1、请说明什么是Apache Kafka?
+Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

`,r:{minutes:17.01,words:5103},t:"kafka面试题",y:"a"}}],["/middleware/sentinel/springcloud_sentinel.html",{loader:()=>C(()=>import("./springcloud_sentinel.html-CFCKUB71.js"),[]),meta:{d:171008157e4,e:` +

参考

+

1.超详细springcloud sentinel教程~

`,r:{minutes:.06,words:17},t:"Spring Cloud Sentinel",I:!1,y:"a"}}],["/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html",{loader:()=>C(()=>import("./入门使用.html-DfOcEEjE.js"),[]),meta:{d:171008157e4,e:` +

参考

+

1.Sentinel入门(干货版)

`,r:{minutes:.06,words:17},t:"入门使用",I:!1,y:"a"}}],["/middleware/zookeeper/zookeeper.html",{loader:()=>C(()=>import("./zookeeper.html-Dd8HToeq.js"),[]),meta:{d:1706204085e3,e:` +

留空

+`,r:{minutes:.01,words:3},t:"Zookeeper",y:"a"}}],["/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html",{loader:()=>C(()=>import("./基于ZooKeeper的队列爬虫.html-D6Y4vXXb.js"),[]),meta:{d:1707204155e3,e:` +

一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
+简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
+基本的知识就不过多介绍了,可以参考参考下面这些人的:
+ZooKeeper官网
+http://www.cnblogs.com/wuxl360/p/5817471.html

`,r:{minutes:9.55,words:2864},t:"基于ZooKeeper的队列爬虫",y:"a"}}],["/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html",{loader:()=>C(()=>import("./推荐工程-概述.html-CyDH_f4g.js"),[]),meta:{d:173290591e4,e:` +

随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

`,r:{minutes:6.19,words:1856},t:"推荐工程-概述",O:1,y:"a"}}],["/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html",{loader:()=>C(()=>import("./推荐系统入门.html-DF2q_M-N.js"),[]),meta:{d:1725198884e3,e:` +

转载自:https://www.cnblogs.com/cgli/p/17225189.html

`,r:{minutes:16.79,words:5036},t:"推荐系统入门",y:"a"}}],["/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html",{loader:()=>C(()=>import("./计算广告基本概念入门总结.html-Zcj8usmP.js"),[]),meta:{d:1725198884e3,e:` +

广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

`,r:{minutes:12.31,words:3692},t:"计算广告基本概念入门总结",y:"a"}}],["/404.html",{loader:()=>C(()=>import("./404.html-BMHKR2ag.js"),[]),meta:{t:""}}],["/interview/",{loader:()=>C(()=>import("./index.html-DQFfT5fQ.js"),[]),meta:{t:"Interview"}}],["/life/",{loader:()=>C(()=>import("./index.html-DJHdkCFZ.js"),[]),meta:{t:"Life"}}],["/link/",{loader:()=>C(()=>import("./index.html-7tsgb-dC.js"),[]),meta:{t:"Link"}}],["/system-design/",{loader:()=>C(()=>import("./index.html-KwQlr7R4.js"),[]),meta:{t:"System Design"}}],["/about-the-author/personal-life/",{loader:()=>C(()=>import("./index.html-DtDo2qGn.js"),[]),meta:{t:"Personal Life"}}],["/about-the-author/works/",{loader:()=>C(()=>import("./index.html-TDa4J3OS.js"),[]),meta:{t:"Works"}}],["/bigdata/spark/",{loader:()=>C(()=>import("./index.html-BAbeWXfD.js"),[]),meta:{t:"Spark"}}],["/database/elasticsearch/",{loader:()=>C(()=>import("./index.html-BTDM2Wpl.js"),[]),meta:{t:"Elasticsearch"}}],["/database/mysql/",{loader:()=>C(()=>import("./index.html-Uqis1PLJ.js"),[]),meta:{t:"Mysql"}}],["/database/redis/",{loader:()=>C(()=>import("./index.html-B2fh3Mku.js"),[]),meta:{t:"Redis"}}],["/middleware/canal/",{loader:()=>C(()=>import("./index.html-DRyhNgG6.js"),[]),meta:{t:"Canal"}}],["/middleware/kafka/",{loader:()=>C(()=>import("./index.html-CY6pLx2d.js"),[]),meta:{t:"Kafka"}}],["/middleware/sentinel/",{loader:()=>C(()=>import("./index.html-BwVnwmVL.js"),[]),meta:{t:"Sentinel"}}],["/middleware/zookeeper/",{loader:()=>C(()=>import("./index.html-5ZTxzR_p.js"),[]),meta:{t:"Zookeeper"}}],["/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/",{loader:()=>C(()=>import("./index.html-C9P5ouBn.js"),[]),meta:{t:"推荐系统"}}],["/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/",{loader:()=>C(()=>import("./index.html-SdCcjUz-.js"),[]),meta:{t:"计算广告"}}],["/category/",{loader:()=>C(()=>import("./index.html-DYmvtxA9.js"),[]),meta:{t:"分类",I:!1}}],["/tag/",{loader:()=>C(()=>import("./index.html-BKAyyte1.js"),[]),meta:{t:"标签",I:!1}}],["/article/",{loader:()=>C(()=>import("./index.html-CD6aFyf5.js"),[]),meta:{t:"文章",I:!1}}],["/star/",{loader:()=>C(()=>import("./index.html-B8Qu0Pmr.js"),[]),meta:{t:"星标",I:!1}}],["/timeline/",{loader:()=>C(()=>import("./index.html-CdxL9VtY.js"),[]),meta:{t:"时间轴",I:!1}}],["/slide/",{loader:()=>C(()=>import("./index.html-DJZmVM8a.js"),[]),meta:{t:""}}]]);/*! + * vue-router v4.5.0 + * (c) 2024 Eduardo San Martin Morote + * @license MIT + */const jn=typeof document<"u";function yd(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function X0(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&yd(e.default)}const Ee=Object.assign;function Bi(e,t){const n={};for(const l in t){const r=t[l];n[l]=Pt(r)?r.map(e):e(r)}return n}const Tl=()=>{},Pt=Array.isArray,Ad=/#/g,e2=/&/g,t2=/\//g,n2=/=/g,l2=/\?/g,kd=/\+/g,r2=/%5B/g,i2=/%5D/g,wd=/%5E/g,a2=/%60/g,Bd=/%7B/g,o2=/%7C/g,Cd=/%7D/g,s2=/%20/g;function qa(e){return encodeURI(""+e).replace(o2,"|").replace(r2,"[").replace(i2,"]")}function c2(e){return qa(e).replace(Bd,"{").replace(Cd,"}").replace(wd,"^")}function Zi(e){return qa(e).replace(kd,"%2B").replace(s2,"+").replace(Ad,"%23").replace(e2,"%26").replace(a2,"`").replace(Bd,"{").replace(Cd,"}").replace(wd,"^")}function u2(e){return Zi(e).replace(n2,"%3D")}function d2(e){return qa(e).replace(Ad,"%23").replace(l2,"%3F")}function f2(e){return e==null?"":d2(e).replace(t2,"%2F")}function Nl(e){try{return decodeURIComponent(""+e)}catch{}return""+e}const h2=/\/$/,p2=e=>e.replace(h2,"");function Ci(e,t,n="/"){let l,r={},i="",a="";const o=t.indexOf("#");let c=t.indexOf("?");return o=0&&(c=-1),c>-1&&(l=t.slice(0,c),i=t.slice(c+1,o>-1?o:t.length),r=e(i)),o>-1&&(l=l||t.slice(0,o),a=t.slice(o,t.length)),l=E2(l??t,n),{fullPath:l+(i&&"?")+i+a,path:l,query:r,hash:Nl(a)}}function v2(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Es(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function m2(e,t,n){const l=t.matched.length-1,r=n.matched.length-1;return l>-1&&l===r&&tl(t.matched[l],n.matched[r])&&xd(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function tl(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function xd(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!g2(e[n],t[n]))return!1;return!0}function g2(e,t){return Pt(e)?_s(e,t):Pt(t)?_s(t,e):e===t}function _s(e,t){return Pt(t)?e.length===t.length&&e.every((n,l)=>n===t[l]):e.length===1&&e[0]===t}function E2(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),l=e.split("/"),r=l[l.length-1];(r===".."||r===".")&&l.push("");let i=n.length-1,a,o;for(a=0;a1&&i--;else break;return n.slice(0,i).join("/")+"/"+l.slice(a).join("/")}const Wt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};var jl;(function(e){e.pop="pop",e.push="push"})(jl||(jl={}));var Ll;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Ll||(Ll={}));function _2(e){if(!e)if(jn){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),p2(e)}const b2=/^[^#]+#/;function y2(e,t){return e.replace(b2,"#")+t}function A2(e,t){const n=document.documentElement.getBoundingClientRect(),l=e.getBoundingClientRect();return{behavior:t.behavior,left:l.left-n.left-(t.left||0),top:l.top-n.top-(t.top||0)}}const Qr=()=>({left:window.scrollX,top:window.scrollY});function k2(e){let t;if("el"in e){const n=e.el,l=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?l?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=A2(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function bs(e,t){return(history.state?history.state.position-t:-1)+e}const Qi=new Map;function w2(e,t){Qi.set(e,t)}function B2(e){const t=Qi.get(e);return Qi.delete(e),t}let C2=()=>location.protocol+"//"+location.host;function Sd(e,t){const{pathname:n,search:l,hash:r}=t,i=e.indexOf("#");if(i>-1){let o=r.includes(e.slice(i))?e.slice(i).length:1,c=r.slice(o);return c[0]!=="/"&&(c="/"+c),Es(c,"")}return Es(n,e)+l+r}function x2(e,t,n,l){let r=[],i=[],a=null;const o=({state:h})=>{const p=Sd(e,location),g=n.value,v=t.value;let y=0;if(h){if(n.value=p,t.value=h,a&&a===g){a=null;return}y=v?h.position-v.position:0}else l(p);r.forEach(_=>{_(n.value,g,{delta:y,type:jl.pop,direction:y?y>0?Ll.forward:Ll.back:Ll.unknown})})};function c(){a=n.value}function u(h){r.push(h);const p=()=>{const g=r.indexOf(h);g>-1&&r.splice(g,1)};return i.push(p),p}function d(){const{history:h}=window;h.state&&h.replaceState(Ee({},h.state,{scroll:Qr()}),"")}function f(){for(const h of i)h();i=[],window.removeEventListener("popstate",o),window.removeEventListener("beforeunload",d)}return window.addEventListener("popstate",o),window.addEventListener("beforeunload",d,{passive:!0}),{pauseListeners:c,listen:u,destroy:f}}function ys(e,t,n,l=!1,r=!1){return{back:e,current:t,forward:n,replaced:l,position:window.history.length,scroll:r?Qr():null}}function S2(e){const{history:t,location:n}=window,l={value:Sd(e,n)},r={value:t.state};r.value||i(l.value,{back:null,current:l.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(c,u,d){const f=e.indexOf("#"),h=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+c:C2()+e+c;try{t[d?"replaceState":"pushState"](u,"",h),r.value=u}catch(p){console.error(p),n[d?"replace":"assign"](h)}}function a(c,u){const d=Ee({},t.state,ys(r.value.back,c,r.value.forward,!0),u,{position:r.value.position});i(c,d,!0),l.value=c}function o(c,u){const d=Ee({},r.value,t.state,{forward:c,scroll:Qr()});i(d.current,d,!0);const f=Ee({},ys(l.value,c,null),{position:d.position+1},u);i(c,f,!1),l.value=c}return{location:l,state:r,push:o,replace:a}}function T2(e){e=_2(e);const t=S2(e),n=x2(e,t.state,t.location,t.replace);function l(i,a=!0){a||n.pauseListeners(),history.go(i)}const r=Ee({location:"",base:e,go:l,createHref:y2.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}function L2(e){return typeof e=="string"||e&&typeof e=="object"}function Td(e){return typeof e=="string"||typeof e=="symbol"}const Ld=Symbol("");var As;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(As||(As={}));function nl(e,t){return Ee(new Error,{type:e,[Ld]:!0},t)}function Kt(e,t){return e instanceof Error&&Ld in e&&(t==null||!!(e.type&t))}const ks="[^/]+?",O2={sensitive:!1,strict:!1,start:!0,end:!0},D2=/[.+*?^${}()[\]/\\]/g;function P2(e,t){const n=Ee({},O2,t),l=[];let r=n.start?"^":"";const i=[];for(const u of e){const d=u.length?[]:[90];n.strict&&!u.length&&(r+="/");for(let f=0;ft.length?t.length===1&&t[0]===80?1:-1:0}function Od(e,t){let n=0;const l=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const F2={type:0,value:""},R2=/[a-zA-Z0-9_]/;function M2(e){if(!e)return[[]];if(e==="/")return[[F2]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(p){throw new Error(`ERR (${n})/"${u}": ${p}`)}let n=0,l=n;const r=[];let i;function a(){i&&r.push(i),i=[]}let o=0,c,u="",d="";function f(){u&&(n===0?i.push({type:0,value:u}):n===1||n===2||n===3?(i.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),i.push({type:1,value:u,regexp:d,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),u="")}function h(){u+=c}for(;o{a(m)}:Tl}function a(f){if(Td(f)){const h=l.get(f);h&&(l.delete(f),n.splice(n.indexOf(h),1),h.children.forEach(a),h.alias.forEach(a))}else{const h=n.indexOf(f);h>-1&&(n.splice(h,1),f.record.name&&l.delete(f.record.name),f.children.forEach(a),f.alias.forEach(a))}}function o(){return n}function c(f){const h=z2(f,n);n.splice(h,0,f),f.record.name&&!xs(f)&&l.set(f.record.name,f)}function u(f,h){let p,g={},v,y;if("name"in f&&f.name){if(p=l.get(f.name),!p)throw nl(1,{location:f});y=p.record.name,g=Ee(Bs(h.params,p.keys.filter(m=>!m.optional).concat(p.parent?p.parent.keys.filter(m=>m.optional):[]).map(m=>m.name)),f.params&&Bs(f.params,p.keys.map(m=>m.name))),v=p.stringify(g)}else if(f.path!=null)v=f.path,p=n.find(m=>m.re.test(v)),p&&(g=p.parse(v),y=p.record.name);else{if(p=h.name?l.get(h.name):n.find(m=>m.re.test(h.path)),!p)throw nl(1,{location:f,currentLocation:h});y=p.record.name,g=Ee({},h.params,f.params),v=p.stringify(g)}const _=[];let A=p;for(;A;)_.unshift(A.record),A=A.parent;return{name:y,path:v,params:g,matched:_,meta:H2(_)}}e.forEach(f=>i(f));function d(){n.length=0,l.clear()}return{addRoute:i,resolve:u,removeRoute:a,clearRoutes:d,getRoutes:o,getRecordMatcher:r}}function Bs(e,t){const n={};for(const l of t)l in e&&(n[l]=e[l]);return n}function Cs(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:j2(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function j2(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const l in e.components)t[l]=typeof n=="object"?n[l]:n;return t}function xs(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function H2(e){return e.reduce((t,n)=>Ee(t,n.meta),{})}function Ss(e,t){const n={};for(const l in e)n[l]=l in t?t[l]:e[l];return n}function z2(e,t){let n=0,l=t.length;for(;n!==l;){const i=n+l>>1;Od(e,t[i])<0?l=i:n=i+1}const r=$2(e);return r&&(l=t.lastIndexOf(r,l-1)),l}function $2(e){let t=e;for(;t=t.parent;)if(Dd(t)&&Od(e,t)===0)return t}function Dd({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function U2(e){const t={};if(e===""||e==="?")return t;const l=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;ri&&Zi(i)):[l&&Zi(l)]).forEach(i=>{i!==void 0&&(t+=(t.length?"&":"")+n,i!=null&&(t+="="+i))})}return t}function G2(e){const t={};for(const n in e){const l=e[n];l!==void 0&&(t[n]=Pt(l)?l.map(r=>r==null?null:""+r):l==null?l:""+l)}return t}const K2=Symbol(""),Ls=Symbol(""),Xr=Symbol(""),Wa=Symbol(""),Xi=Symbol("");function gl(){let e=[];function t(l){return e.push(l),()=>{const r=e.indexOf(l);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function cn(e,t,n,l,r,i=a=>a()){const a=l&&(l.enterCallbacks[r]=l.enterCallbacks[r]||[]);return()=>new Promise((o,c)=>{const u=h=>{h===!1?c(nl(4,{from:n,to:t})):h instanceof Error?c(h):L2(h)?c(nl(2,{from:t,to:h})):(a&&l.enterCallbacks[r]===a&&typeof h=="function"&&a.push(h),o())},d=i(()=>e.call(l&&l.instances[r],t,n,u));let f=Promise.resolve(d);e.length<3&&(f=f.then(u)),f.catch(h=>c(h))})}function xi(e,t,n,l,r=i=>i()){const i=[];for(const a of e)for(const o in a.components){let c=a.components[o];if(!(t!=="beforeRouteEnter"&&!a.instances[o]))if(yd(c)){const d=(c.__vccOpts||c)[t];d&&i.push(cn(d,n,l,a,o,r))}else{let u=c();i.push(()=>u.then(d=>{if(!d)throw new Error(`Couldn't resolve component "${o}" at "${a.path}"`);const f=X0(d)?d.default:d;a.mods[o]=d,a.components[o]=f;const p=(f.__vccOpts||f)[t];return p&&cn(p,n,l,a,o,r)()}))}}return i}function Os(e){const t=Se(Xr),n=Se(Wa),l=k(()=>{const c=St(e.to);return t.resolve(c)}),r=k(()=>{const{matched:c}=l.value,{length:u}=c,d=c[u-1],f=n.matched;if(!d||!f.length)return-1;const h=f.findIndex(tl.bind(null,d));if(h>-1)return h;const p=Ds(c[u-2]);return u>1&&Ds(d)===p&&f[f.length-1].path!==p?f.findIndex(tl.bind(null,c[u-2])):h}),i=k(()=>r.value>-1&&Z2(n.params,l.value.params)),a=k(()=>r.value>-1&&r.value===n.matched.length-1&&xd(n.params,l.value.params));function o(c={}){if(Y2(c)){const u=t[St(e.replace)?"replace":"push"](St(e.to)).catch(Tl);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>u),u}return Promise.resolve()}return{route:l,href:k(()=>l.value.href),isActive:i,isExactActive:a,navigate:o}}function q2(e){return e.length===1?e[0]:e}const W2=H({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Os,setup(e,{slots:t}){const n=Jl(Os(e)),{options:l}=Se(Xr),r=k(()=>({[Ps(e.activeClass,l.linkActiveClass,"router-link-active")]:n.isActive,[Ps(e.exactActiveClass,l.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&q2(t.default(n));return e.custom?i:s("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),J2=W2;function Y2(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Z2(e,t){for(const n in t){const l=t[n],r=e[n];if(typeof l=="string"){if(l!==r)return!1}else if(!Pt(r)||r.length!==l.length||l.some((i,a)=>i!==r[a]))return!1}return!0}function Ds(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Ps=(e,t,n)=>e??t??n,Q2=H({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const l=Se(Xi),r=k(()=>e.route||l.value),i=Se(Ls,0),a=k(()=>{let u=St(i);const{matched:d}=r.value;let f;for(;(f=d[u])&&!f.components;)u++;return u}),o=k(()=>r.value.matched[a.value]);Ot(Ls,k(()=>a.value+1)),Ot(K2,o),Ot(Xi,r);const c=ee();return pe(()=>[c.value,o.value,e.name],([u,d,f],[h,p,g])=>{d&&(d.instances[f]=u,p&&p!==d&&u&&u===h&&(d.leaveGuards.size||(d.leaveGuards=p.leaveGuards),d.updateGuards.size||(d.updateGuards=p.updateGuards))),u&&d&&(!p||!tl(d,p)||!h)&&(d.enterCallbacks[f]||[]).forEach(v=>v(u))},{flush:"post"}),()=>{const u=r.value,d=e.name,f=o.value,h=f&&f.components[d];if(!h)return Is(n.default,{Component:h,route:u});const p=f.props[d],g=p?p===!0?u.params:typeof p=="function"?p(u):p:null,y=s(h,Ee({},g,t,{onVnodeUnmounted:_=>{_.component.isUnmounted&&(f.instances[d]=null)},ref:c}));return Is(n.default,{Component:y,route:u})||y}}});function Is(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const X2=Q2;function ev(e){const t=N2(e.routes,e),n=e.parseQuery||U2,l=e.stringifyQuery||Ts,r=e.history,i=gl(),a=gl(),o=gl(),c=$e(Wt);let u=Wt;jn&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const d=Bi.bind(null,x=>""+x),f=Bi.bind(null,f2),h=Bi.bind(null,Nl);function p(x,q){let U,Q;return Td(x)?(U=t.getRecordMatcher(x),Q=q):Q=x,t.addRoute(Q,U)}function g(x){const q=t.getRecordMatcher(x);q&&t.removeRoute(q)}function v(){return t.getRoutes().map(x=>x.record)}function y(x){return!!t.getRecordMatcher(x)}function _(x,q){if(q=Ee({},q||c.value),typeof x=="string"){const b=Ci(n,x,q.path),B=t.resolve({path:b.path},q),D=r.createHref(b.fullPath);return Ee(b,B,{params:h(B.params),hash:Nl(b.hash),redirectedFrom:void 0,href:D})}let U;if(x.path!=null)U=Ee({},x,{path:Ci(n,x.path,q.path).path});else{const b=Ee({},x.params);for(const B in b)b[B]==null&&delete b[B];U=Ee({},x,{params:f(b)}),q.params=f(q.params)}const Q=t.resolve(U,q),fe=x.hash||"";Q.params=d(h(Q.params));const ye=v2(l,Ee({},x,{hash:c2(fe),path:Q.path})),E=r.createHref(ye);return Ee({fullPath:ye,hash:fe,query:l===Ts?G2(x.query):x.query||{}},Q,{redirectedFrom:void 0,href:E})}function A(x){return typeof x=="string"?Ci(n,x,c.value.path):Ee({},x)}function m(x,q){if(u!==x)return nl(8,{from:q,to:x})}function w(x){return R(x)}function F(x){return w(Ee(A(x),{replace:!0}))}function V(x){const q=x.matched[x.matched.length-1];if(q&&q.redirect){const{redirect:U}=q;let Q=typeof U=="function"?U(x):U;return typeof Q=="string"&&(Q=Q.includes("?")||Q.includes("#")?Q=A(Q):{path:Q},Q.params={}),Ee({query:x.query,hash:x.hash,params:Q.path!=null?{}:x.params},Q)}}function R(x,q){const U=u=_(x),Q=c.value,fe=x.state,ye=x.force,E=x.replace===!0,b=V(U);if(b)return R(Ee(A(b),{state:typeof b=="object"?Ee({},fe,b.state):fe,force:ye,replace:E}),q||U);const B=U;B.redirectedFrom=q;let D;return!ye&&m2(l,Q,U)&&(D=nl(16,{to:B,from:Q}),Fe(Q,Q,!0,!1)),(D?Promise.resolve(D):T(B,Q)).catch(S=>Kt(S)?Kt(S,2)?S:Ge(S):J(S,B,Q)).then(S=>{if(S){if(Kt(S,2))return R(Ee({replace:E},A(S.to),{state:typeof S.to=="object"?Ee({},fe,S.to.state):fe,force:ye}),q||B)}else S=O(B,Q,!0,E,fe);return $(B,Q,S),S})}function Z(x,q){const U=m(x,q);return U?Promise.reject(U):Promise.resolve()}function M(x){const q=gt.values().next().value;return q&&typeof q.runWithContext=="function"?q.runWithContext(x):x()}function T(x,q){let U;const[Q,fe,ye]=tv(x,q);U=xi(Q.reverse(),"beforeRouteLeave",x,q);for(const b of Q)b.leaveGuards.forEach(B=>{U.push(cn(B,x,q))});const E=Z.bind(null,x,q);return U.push(E),We(U).then(()=>{U=[];for(const b of i.list())U.push(cn(b,x,q));return U.push(E),We(U)}).then(()=>{U=xi(fe,"beforeRouteUpdate",x,q);for(const b of fe)b.updateGuards.forEach(B=>{U.push(cn(B,x,q))});return U.push(E),We(U)}).then(()=>{U=[];for(const b of ye)if(b.beforeEnter)if(Pt(b.beforeEnter))for(const B of b.beforeEnter)U.push(cn(B,x,q));else U.push(cn(b.beforeEnter,x,q));return U.push(E),We(U)}).then(()=>(x.matched.forEach(b=>b.enterCallbacks={}),U=xi(ye,"beforeRouteEnter",x,q,M),U.push(E),We(U))).then(()=>{U=[];for(const b of a.list())U.push(cn(b,x,q));return U.push(E),We(U)}).catch(b=>Kt(b,8)?b:Promise.reject(b))}function $(x,q,U){o.list().forEach(Q=>M(()=>Q(x,q,U)))}function O(x,q,U,Q,fe){const ye=m(x,q);if(ye)return ye;const E=q===Wt,b=jn?history.state:{};U&&(Q||E?r.replace(x.fullPath,Ee({scroll:E&&b&&b.scroll},fe)):r.push(x.fullPath,fe)),c.value=x,Fe(x,q,U,E),Ge()}let X;function se(){X||(X=r.listen((x,q,U)=>{if(!rt.listening)return;const Q=_(x),fe=V(Q);if(fe){R(Ee(fe,{replace:!0,force:!0}),Q).catch(Tl);return}u=Q;const ye=c.value;jn&&w2(bs(ye.fullPath,U.delta),Qr()),T(Q,ye).catch(E=>Kt(E,12)?E:Kt(E,2)?(R(Ee(A(E.to),{force:!0}),Q).then(b=>{Kt(b,20)&&!U.delta&&U.type===jl.pop&&r.go(-1,!1)}).catch(Tl),Promise.reject()):(U.delta&&r.go(-U.delta,!1),J(E,Q,ye))).then(E=>{E=E||O(Q,ye,!1),E&&(U.delta&&!Kt(E,8)?r.go(-U.delta,!1):U.type===jl.pop&&Kt(E,20)&&r.go(-1,!1)),$(Q,ye,E)}).catch(Tl)}))}let he=gl(),K=gl(),N;function J(x,q,U){Ge(x);const Q=K.list();return Q.length?Q.forEach(fe=>fe(x,q,U)):console.error(x),Promise.reject(x)}function ae(){return N&&c.value!==Wt?Promise.resolve():new Promise((x,q)=>{he.add([x,q])})}function Ge(x){return N||(N=!x,se(),he.list().forEach(([q,U])=>x?U(x):q()),he.reset()),x}function Fe(x,q,U,Q){const{scrollBehavior:fe}=e;if(!jn||!fe)return Promise.resolve();const ye=!U&&B2(bs(x.fullPath,0))||(Q||!U)&&history.state&&history.state.scroll||null;return Qt().then(()=>fe(x,q,ye)).then(E=>E&&k2(E)).catch(E=>J(E,x,q))}const ge=x=>r.go(x);let Ne;const gt=new Set,rt={currentRoute:c,listening:!0,addRoute:p,removeRoute:g,clearRoutes:t.clearRoutes,hasRoute:y,getRoutes:v,resolve:_,options:e,push:w,replace:F,go:ge,back:()=>ge(-1),forward:()=>ge(1),beforeEach:i.add,beforeResolve:a.add,afterEach:o.add,onError:K.add,isReady:ae,install(x){const q=this;x.component("RouterLink",J2),x.component("RouterView",X2),x.config.globalProperties.$router=q,Object.defineProperty(x.config.globalProperties,"$route",{enumerable:!0,get:()=>St(c)}),jn&&!Ne&&c.value===Wt&&(Ne=!0,w(r.location).catch(fe=>{}));const U={};for(const fe in Wt)Object.defineProperty(U,fe,{get:()=>c.value[fe],enumerable:!0});x.provide(Xr,q),x.provide(Wa,ku(U)),x.provide(Xi,c);const Q=x.unmount;gt.add(x),x.unmount=function(){gt.delete(x),gt.size<1&&(u=Wt,X&&X(),X=null,c.value=Wt,Ne=!1,N=!1),Q()}}};function We(x){return x.reduce((q,U)=>q.then(()=>M(U)),Promise.resolve())}return rt}function tv(e,t){const n=[],l=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;atl(u,o))?l.push(o):n.push(o));const c=e.matched[a];c&&(t.matched.find(u=>tl(u,c))||r.push(c))}return[n,l,r]}function en(){return Se(Xr)}function Ut(e){return Se(Wa)}var Ja=Symbol(""),Rt=()=>{const e=Se(Ja);if(!e)throw new Error("useClientData() is called without provider.");return e},nv=()=>Rt().pageComponent,Oe=()=>Rt().pageData,ve=()=>Rt().pageFrontmatter,lv=()=>Rt().pageHead,ei=()=>Rt().pageLang,rv=()=>Rt().pageLayout,Mt=()=>Rt().routeLocale,Pd=()=>Rt().routePath,iv=()=>Rt().routes,Id=()=>Rt().siteData,lr=()=>Rt().siteLocaleData,av=Symbol(""),ea=$e(Z0),Yn=$e(Q0),Fd=(e,t)=>{const n=H0(e,t);if(Yn.value[n])return n;const l=encodeURI(n);if(Yn.value[l])return l;const r=ea.value[n]||ea.value[l];return r||n},wt=(e,t)=>{const{pathname:n,hashAndQueries:l}=Ed(e),r=Fd(n,t),i=r+l;return Yn.value[r]?{...Yn.value[r],path:i,notFound:!1}:{...Yn.value["/404.html"],path:i,notFound:!0}},ov=(e,t)=>{const{pathname:n,hashAndQueries:l}=Ed(e);return Fd(n,t)+l},sv=e=>{if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget){const t=e.currentTarget.getAttribute("target");if(t!=null&&t.match(/\b_blank\b/i))return}return e.preventDefault(),!0}},He=H({name:"RouteLink",props:{to:{type:String,required:!0},active:Boolean,activeClass:{type:String,default:"route-link-active"}},slots:Object,setup(e,{slots:t}){const n=en(),l=Ut(),r=k(()=>e.to.startsWith("#")||e.to.startsWith("?")?e.to:`/${ov(e.to,l.path).substring(1)}`);return()=>{var i;return s("a",{class:["route-link",{[e.activeClass]:e.active}],href:r.value,onClick:(a={})=>{sv(a)&&n.push(e.to).catch()}},(i=t.default)==null?void 0:i.call(t))}}}),cv=H({name:"AutoLink",props:{config:{type:Object,required:!0}},slots:Object,setup(e,{slots:t}){const n=sl(e,"config"),l=Ut(),r=Id(),i=k(()=>tr(n.value.link)),a=k(()=>n.value.target||(i.value?"_blank":void 0)),o=k(()=>a.value==="_blank"),c=k(()=>!i.value&&!o.value),u=k(()=>n.value.rel||(o.value?"noopener noreferrer":null)),d=k(()=>n.value.ariaLabel??n.value.text),f=k(()=>{if(n.value.exact)return!1;const p=Object.keys(r.value.locales);return p.length?p.every(g=>g!==n.value.link):n.value.link!=="/"}),h=k(()=>c.value?n.value.activeMatch?(n.value.activeMatch instanceof RegExp?n.value.activeMatch:new RegExp(n.value.activeMatch,"u")).test(l.path):f.value?l.path.startsWith(n.value.link):l.path===n.value.link:!1);return()=>{const{before:p,after:g,default:v}=t,y=(v==null?void 0:v(n.value))||[p==null?void 0:p(n.value),n.value.text,g==null?void 0:g(n.value)];return c.value?s(He,{class:"auto-link",to:n.value.link,active:h.value,"aria-label":d.value},()=>y):s("a",{class:"auto-link external-link",href:n.value.link,"aria-label":d.value,rel:u.value,target:a.value},y)}}}),Rd=H({name:"ClientOnly",setup(e,t){const n=ee(!1);return we(()=>{n.value=!0}),()=>{var l,r;return n.value?(r=(l=t.slots).default)==null?void 0:r.call(l):null}}}),Md=H({name:"Content",props:{path:{type:String,required:!1,default:""}},setup(e){const t=nv(),n=k(()=>{if(!e.path)return t.value;const l=wt(e.path);return a1(()=>l.loader().then(({comp:r})=>r))});return()=>s(n.value)}}),uv="Layout",dv="en-US",kn=Jl({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageHead:(e,t,n)=>{const l=ke(t.description)?t.description:n.description,r=[...Array.isArray(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:l}]];return q0(r)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:(e,t)=>e.lang||t.lang||dv,resolvePageLayout:(e,t)=>{const n=ke(e.frontmatter.layout)?e.frontmatter.layout:uv;if(!t[n])throw new Error(`[vuepress] Cannot resolve layout: ${n}`);return t[n]},resolveRouteLocale:(e,t)=>z0(e,decodeURI(t)),resolveSiteLocaleData:({base:e,locales:t,...n},l)=>{var r;return{...n,...t[l],head:[...((r=t[l])==null?void 0:r.head)??[],...n.head??[]]}}}),Bt=(e={})=>e,Ae=e=>gn(e)?e:`/${bd(e)}`;const fv=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"})),hv=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"}));function ul(e){return su()?(Ep(e),!0):!1}function lt(e){return typeof e=="function"?e():St(e)}const rr=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const pv=Object.prototype.toString,vv=e=>pv.call(e)==="[object Object]",ll=()=>{},Fs=mv();function mv(){var e,t;return rr&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function Ya(e,t){function n(...l){return new Promise((r,i)=>{Promise.resolve(e(()=>t.apply(this,l),{fn:t,thisArg:this,args:l})).then(r).catch(i)})}return n}const Vd=e=>e();function gv(e,t={}){let n,l,r=ll;const i=o=>{clearTimeout(o),r(),r=ll};return o=>{const c=lt(e),u=lt(t.maxWait);return n&&i(n),c<=0||u!==void 0&&u<=0?(l&&(i(l),l=null),Promise.resolve(o())):new Promise((d,f)=>{r=t.rejectOnCancel?f:d,u&&!l&&(l=setTimeout(()=>{n&&i(n),l=null,d(o())},u)),n=setTimeout(()=>{l&&i(l),l=null,d(o())},c)})}}function Ev(...e){let t=0,n,l=!0,r=ll,i,a,o,c,u;!Re(e[0])&&typeof e[0]=="object"?{delay:a,trailing:o=!0,leading:c=!0,rejectOnCancel:u=!1}=e[0]:[a,o=!0,c=!0,u=!1]=e;const d=()=>{n&&(clearTimeout(n),n=void 0,r(),r=ll)};return h=>{const p=lt(a),g=Date.now()-t,v=()=>i=h();return d(),p<=0?(t=Date.now(),v()):(g>p&&(c||!l)?(t=Date.now(),v()):o&&(i=new Promise((y,_)=>{r=u?_:y,n=setTimeout(()=>{t=Date.now(),l=!0,y(v()),d()},Math.max(0,p-g))})),!c&&!n&&(n=setTimeout(()=>l=!0,p)),l=!1,i)}}function _v(e=Vd){const t=ee(!0);function n(){t.value=!1}function l(){t.value=!0}const r=(...i)=>{t.value&&e(...i)};return{isActive:In(t),pause:n,resume:l,eventFilter:r}}function bv(e){let t;function n(){return t||(t=e()),t}return n.reset=async()=>{const l=t;t=void 0,l&&await l},n}function yv(e){return Xl()}function Av(...e){if(e.length!==1)return sl(...e);const t=e[0];return typeof t=="function"?In(Cu(()=>({get:t,set:ll}))):ee(t)}function Nd(e,t=200,n={}){return Ya(gv(t,n),e)}function kv(e,t=200,n=!1,l=!0,r=!1){return Ya(Ev(t,n,l,r),e)}function wv(e,t,n={}){const{eventFilter:l=Vd,...r}=n;return pe(e,Ya(l,t),r)}function Bv(e,t,n={}){const{eventFilter:l,...r}=n,{eventFilter:i,pause:a,resume:o,isActive:c}=_v(l);return{stop:wv(e,t,{...r,eventFilter:i}),pause:a,resume:o,isActive:c}}function Za(e,t=!0,n){yv()?we(e,n):t?e():Qt(e)}function Cv(e,t,n={}){const{immediate:l=!0}=n,r=ee(!1);let i=null;function a(){i&&(clearTimeout(i),i=null)}function o(){r.value=!1,a()}function c(...u){a(),r.value=!0,i=setTimeout(()=>{r.value=!1,i=null,e(...u)},lt(t))}return l&&(r.value=!0,rr&&c()),ul(o),{isPending:In(r),start:c,stop:o}}function ta(e=!1,t={}){const{truthyValue:n=!0,falsyValue:l=!1}=t,r=Re(e),i=ee(e);function a(o){if(arguments.length)return i.value=o,i.value;{const c=lt(n);return i.value=i.value===c?lt(l):c,i.value}}return r?a:[i,a]}const It=rr?window:void 0,xv=rr?window.document:void 0,jd=rr?window.navigator:void 0;function Zt(e){var t;const n=lt(e);return(t=n==null?void 0:n.$el)!=null?t:n}function Ie(...e){let t,n,l,r;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,l,r]=e,t=It):[t,n,l,r]=e,!t)return ll;Array.isArray(n)||(n=[n]),Array.isArray(l)||(l=[l]);const i=[],a=()=>{i.forEach(d=>d()),i.length=0},o=(d,f,h,p)=>(d.addEventListener(f,h,p),()=>d.removeEventListener(f,h,p)),c=pe(()=>[Zt(t),lt(r)],([d,f])=>{if(a(),!d)return;const h=vv(f)?{...f}:f;i.push(...n.flatMap(p=>l.map(g=>o(d,p,g,h))))},{immediate:!0,flush:"post"}),u=()=>{c(),a()};return ul(u),u}function Sv(){const e=ee(!1),t=Xl();return t&&we(()=>{e.value=!0},t),e}function ir(e){const t=Sv();return k(()=>(t.value,!!e()))}function Qa(e,t={}){const{window:n=It}=t,l=ir(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let r;const i=ee(!1),a=u=>{i.value=u.matches},o=()=>{r&&("removeEventListener"in r?r.removeEventListener("change",a):r.removeListener(a))},c=za(()=>{l.value&&(o(),r=n.matchMedia(lt(e)),"addEventListener"in r?r.addEventListener("change",a):r.addListener(a),i.value=r.matches)});return ul(()=>{c(),o(),r=void 0}),i}function Rs(e,t={}){const{controls:n=!1,navigator:l=jd}=t,r=ir(()=>l&&"permissions"in l),i=$e(),a=typeof e=="string"?{name:e}:e,o=$e(),c=()=>{var d,f;o.value=(f=(d=i.value)==null?void 0:d.state)!=null?f:"prompt"};Ie(i,"change",c);const u=bv(async()=>{if(r.value){if(!i.value)try{i.value=await l.permissions.query(a)}catch{i.value=void 0}finally{c()}if(n)return de(i.value)}});return u(),n?{state:o,isSupported:r,query:u}:o}function Tv(e={}){const{navigator:t=jd,read:n=!1,source:l,copiedDuring:r=1500,legacy:i=!1}=e,a=ir(()=>t&&"clipboard"in t),o=Rs("clipboard-read"),c=Rs("clipboard-write"),u=k(()=>a.value||i),d=ee(""),f=ee(!1),h=Cv(()=>f.value=!1,r);function p(){a.value&&_(o.value)?t.clipboard.readText().then(A=>{d.value=A}):d.value=y()}u.value&&n&&Ie(["copy","cut"],p);async function g(A=lt(l)){u.value&&A!=null&&(a.value&&_(c.value)?await t.clipboard.writeText(A):v(A),d.value=A,f.value=!0,h.start())}function v(A){const m=document.createElement("textarea");m.value=A??"",m.style.position="absolute",m.style.opacity="0",document.body.appendChild(m),m.select(),document.execCommand("copy"),m.remove()}function y(){var A,m,w;return(w=(m=(A=document==null?void 0:document.getSelection)==null?void 0:A.call(document))==null?void 0:m.toString())!=null?w:""}function _(A){return A==="granted"||A==="prompt"}return{isSupported:u,text:d,copied:f,copy:g}}const Ar=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},kr="__vueuse_ssr_handlers__",Lv=Ov();function Ov(){return kr in Ar||(Ar[kr]=Ar[kr]||{}),Ar[kr]}function Dv(e,t){return Lv[e]||t}function Pv(e){return Qa("(prefers-color-scheme: dark)",e)}function Iv(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Fv={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Ms="vueuse-storage";function Xa(e,t,n,l={}){var r;const{flush:i="pre",deep:a=!0,listenToStorageChanges:o=!0,writeDefaults:c=!0,mergeDefaults:u=!1,shallow:d,window:f=It,eventFilter:h,onError:p=T=>{console.error(T)},initOnMounted:g}=l,v=(d?$e:ee)(typeof t=="function"?t():t);if(!n)try{n=Dv("getDefaultStorage",()=>{var T;return(T=It)==null?void 0:T.localStorage})()}catch(T){p(T)}if(!n)return v;const y=lt(t),_=Iv(y),A=(r=l.serializer)!=null?r:Fv[_],{pause:m,resume:w}=Bv(v,()=>V(v.value),{flush:i,deep:a,eventFilter:h});f&&o&&Za(()=>{n instanceof Storage?Ie(f,"storage",Z):Ie(f,Ms,M),g&&Z()}),g||Z();function F(T,$){if(f){const O={key:e,oldValue:T,newValue:$,storageArea:n};f.dispatchEvent(n instanceof Storage?new StorageEvent("storage",O):new CustomEvent(Ms,{detail:O}))}}function V(T){try{const $=n.getItem(e);if(T==null)F($,null),n.removeItem(e);else{const O=A.write(T);$!==O&&(n.setItem(e,O),F($,O))}}catch($){p($)}}function R(T){const $=T?T.newValue:n.getItem(e);if($==null)return c&&y!=null&&n.setItem(e,A.write(y)),y;if(!T&&u){const O=A.read($);return typeof u=="function"?u(O,y):_==="object"&&!Array.isArray(O)?{...y,...O}:O}else return typeof $!="string"?$:A.read($)}function Z(T){if(!(T&&T.storageArea!==n)){if(T&&T.key==null){v.value=y;return}if(!(T&&T.key!==e)){m();try{(T==null?void 0:T.newValue)!==A.write(v.value)&&(v.value=R(T))}catch($){p($)}finally{T?Qt(w):w()}}}}function M(T){Z(T.detail)}return v}function Rv(e,t,n={}){const{window:l=It,...r}=n;let i;const a=ir(()=>l&&"ResizeObserver"in l),o=()=>{i&&(i.disconnect(),i=void 0)},c=k(()=>{const f=lt(e);return Array.isArray(f)?f.map(h=>Zt(h)):[Zt(f)]}),u=pe(c,f=>{if(o(),a.value&&l){i=new ResizeObserver(t);for(const h of f)h&&i.observe(h,r)}},{immediate:!0,flush:"post"}),d=()=>{o(),u()};return ul(d),{isSupported:a,stop:d}}function Mv(e,t={width:0,height:0},n={}){const{window:l=It,box:r="content-box"}=n,i=k(()=>{var f,h;return(h=(f=Zt(e))==null?void 0:f.namespaceURI)==null?void 0:h.includes("svg")}),a=ee(t.width),o=ee(t.height),{stop:c}=Rv(e,([f])=>{const h=r==="border-box"?f.borderBoxSize:r==="content-box"?f.contentBoxSize:f.devicePixelContentBoxSize;if(l&&i.value){const p=Zt(e);if(p){const g=p.getBoundingClientRect();a.value=g.width,o.value=g.height}}else if(h){const p=Array.isArray(h)?h:[h];a.value=p.reduce((g,{inlineSize:v})=>g+v,0),o.value=p.reduce((g,{blockSize:v})=>g+v,0)}else a.value=f.contentRect.width,o.value=f.contentRect.height},n);Za(()=>{const f=Zt(e);f&&(a.value="offsetWidth"in f?f.offsetWidth:t.width,o.value="offsetHeight"in f?f.offsetHeight:t.height)});const u=pe(()=>Zt(e),f=>{a.value=f?t.width:0,o.value=f?t.height:0});function d(){c(),u()}return{width:a,height:o,stop:d}}const Vs=["fullscreenchange","webkitfullscreenchange","webkitendfullscreen","mozfullscreenchange","MSFullscreenChange"];function ti(e,t={}){const{document:n=xv,autoExit:l=!1}=t,r=k(()=>{var _;return(_=Zt(e))!=null?_:n==null?void 0:n.querySelector("html")}),i=ee(!1),a=k(()=>["requestFullscreen","webkitRequestFullscreen","webkitEnterFullscreen","webkitEnterFullScreen","webkitRequestFullScreen","mozRequestFullScreen","msRequestFullscreen"].find(_=>n&&_ in n||r.value&&_ in r.value)),o=k(()=>["exitFullscreen","webkitExitFullscreen","webkitExitFullScreen","webkitCancelFullScreen","mozCancelFullScreen","msExitFullscreen"].find(_=>n&&_ in n||r.value&&_ in r.value)),c=k(()=>["fullScreen","webkitIsFullScreen","webkitDisplayingFullscreen","mozFullScreen","msFullscreenElement"].find(_=>n&&_ in n||r.value&&_ in r.value)),u=["fullscreenElement","webkitFullscreenElement","mozFullScreenElement","msFullscreenElement"].find(_=>n&&_ in n),d=ir(()=>r.value&&n&&a.value!==void 0&&o.value!==void 0&&c.value!==void 0),f=()=>u?(n==null?void 0:n[u])===r.value:!1,h=()=>{if(c.value){if(n&&n[c.value]!=null)return n[c.value];{const _=r.value;if((_==null?void 0:_[c.value])!=null)return!!_[c.value]}}return!1};async function p(){if(!(!d.value||!i.value)){if(o.value)if((n==null?void 0:n[o.value])!=null)await n[o.value]();else{const _=r.value;(_==null?void 0:_[o.value])!=null&&await _[o.value]()}i.value=!1}}async function g(){if(!d.value||i.value)return;h()&&await p();const _=r.value;a.value&&(_==null?void 0:_[a.value])!=null&&(await _[a.value](),i.value=!0)}async function v(){await(i.value?p():g())}const y=()=>{const _=h();(!_||_&&f())&&(i.value=_)};return Ie(n,Vs,y,!1),Ie(()=>Zt(r),Vs,y,!1),l&&ul(p),{isSupported:d,isFullscreen:i,enter:g,exit:p,toggle:v}}function Si(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function Vv(e,t,n={}){const{window:l=It}=n;return Xa(e,t,l==null?void 0:l.localStorage,n)}function Nv(e={}){const{window:t=It}=e;if(!t)return ee(["en"]);const n=t.navigator,l=ee(n.languages);return Ie(t,"languagechange",()=>{l.value=n.languages}),l}function Hd(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const Ti=new WeakMap;function eo(e,t=!1){const n=ee(t);let l=null,r="";pe(Av(e),o=>{const c=Si(lt(o));if(c){const u=c;if(Ti.get(u)||Ti.set(u,u.style.overflow),u.style.overflow!=="hidden"&&(r=u.style.overflow),u.style.overflow==="hidden")return n.value=!0;if(n.value)return u.style.overflow="hidden"}},{immediate:!0});const i=()=>{const o=Si(lt(e));!o||n.value||(Fs&&(l=Ie(o,"touchmove",c=>{jv(c)},{passive:!1})),o.style.overflow="hidden",n.value=!0)},a=()=>{const o=Si(lt(e));!o||!n.value||(Fs&&(l==null||l()),o.style.overflow=r,Ti.delete(o),n.value=!1)};return ul(a),k({get(){return n.value},set(o){o?i():a()}})}function Hv(e,t,n={}){const{window:l=It}=n;return Xa(e,t,l==null?void 0:l.sessionStorage,n)}function zv(e={}){const{window:t=It,behavior:n="auto"}=e;if(!t)return{x:ee(0),y:ee(0)};const l=ee(t.scrollX),r=ee(t.scrollY),i=k({get(){return l.value},set(o){scrollTo({left:o,behavior:n})}}),a=k({get(){return r.value},set(o){scrollTo({top:o,behavior:n})}});return Ie(t,"scroll",()=>{l.value=t.scrollX,r.value=t.scrollY},{capture:!1,passive:!0}),{x:i,y:a}}function $v(e={}){const{window:t=It,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:l=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:i=!0,type:a="inner"}=e,o=ee(n),c=ee(l),u=()=>{t&&(a==="outer"?(o.value=t.outerWidth,c.value=t.outerHeight):i?(o.value=t.innerWidth,c.value=t.innerHeight):(o.value=t.document.documentElement.clientWidth,c.value=t.document.documentElement.clientHeight))};if(u(),Za(u),Ie("resize",u,{passive:!0}),r){const d=Qa("(orientation: portrait)");pe(d,()=>u())}return{width:o,height:c}}const Ns=async(e,t)=>{const{path:n,query:l}=e.currentRoute.value,{scrollBehavior:r}=e.options;e.options.scrollBehavior=void 0,await e.replace({path:n,query:l,hash:t}),e.options.scrollBehavior=r},Uv=({headerLinkSelector:e,headerAnchorSelector:t,delay:n,offset:l=5})=>{const r=en();Ie("scroll",Nd(()=>{var g,v;const a=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(a-0)f.some(_=>_.hash===y.hash));for(let y=0;y=(((g=_.parentElement)==null?void 0:g.offsetTop)??0)-l,w=!A||a<(((v=A.parentElement)==null?void 0:v.offsetTop)??0)-l;if(!(m&&w))continue;const V=decodeURIComponent(r.currentRoute.value.hash),R=decodeURIComponent(_.hash);if(V===R)return;if(d){for(let Z=y+1;Z{const r=s("svg",{xmlns:"http://www.w3.org/2000/svg",width:e,height:e,preserveAspectRatio:"xMidYMid",viewBox:"25 25 50 50"},[s("animateTransform",{attributeName:"transform",type:"rotate",dur:"2s",keyTimes:"0;1",repeatCount:"indefinite",values:"0;360"}),s("circle",{cx:"50",cy:"50",r:"20",fill:"none",stroke:"currentColor","stroke-width":t,"stroke-linecap":"round"},[s("animate",{attributeName:"stroke-dasharray",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"1,200;90,200;1,200"}),s("animate",{attributeName:"stroke-dashoffset",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"0;-35px;-125px"})])]);return n?s("div",{class:"loading-icon-wrapper",style:`display:flex;align-items:center;justify-content:center;height:${l}px`},r):r};to.displayName="LoadingIcon";var vt=Uint8Array,zn=Uint16Array,Zv=Int32Array,zd=new vt([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),$d=new vt([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Qv=new vt([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Ud=function(e,t){for(var n=new zn(31),l=0;l<31;++l)n[l]=t+=1<>1|(Le&21845)<<1;ln=(ln&52428)>>2|(ln&13107)<<2,ln=(ln&61680)>>4|(ln&3855)<<4,na[Le]=((ln&65280)>>8|(ln&255)<<8)>>1}var Ol=function(e,t,n){for(var l=e.length,r=0,i=new zn(t);r>c]=u}else for(o=new zn(l),r=0;r>15-e[r]);return o},ar=new vt(288);for(var Le=0;Le<144;++Le)ar[Le]=8;for(var Le=144;Le<256;++Le)ar[Le]=9;for(var Le=256;Le<280;++Le)ar[Le]=7;for(var Le=280;Le<288;++Le)ar[Le]=8;var qd=new vt(32);for(var Le=0;Le<32;++Le)qd[Le]=5;var nm=Ol(ar,9,1),lm=Ol(qd,5,1),Li=function(e){for(var t=e[0],n=1;nt&&(t=e[n]);return t},xt=function(e,t,n){var l=t/8|0;return(e[l]|e[l+1]<<8)>>(t&7)&n},Oi=function(e,t){var n=t/8|0;return(e[n]|e[n+1]<<8|e[n+2]<<16)>>(t&7)},rm=function(e){return(e+7)/8|0},Wd=function(e,t,n){return(t==null||t<0)&&(t=0),(n==null||n>e.length)&&(n=e.length),new vt(e.subarray(t,n))},im=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],bt=function(e,t,n){var l=new Error(t||im[e]);if(l.code=e,Error.captureStackTrace&&Error.captureStackTrace(l,bt),!n)throw l;return l},am=function(e,t,n,l){var r=e.length,i=0;if(!r||t.f&&!t.l)return n||new vt(0);var a=!n,o=a||t.i!=2,c=t.i;a&&(n=new vt(r*3));var u=function(fe){var ye=n.length;if(fe>ye){var E=new vt(Math.max(ye*2,fe));E.set(n),n=E}},d=t.f||0,f=t.p||0,h=t.b||0,p=t.l,g=t.d,v=t.m,y=t.n,_=r*8;do{if(!p){d=xt(e,f,1);var A=xt(e,f+1,3);if(f+=3,A)if(A==1)p=nm,g=lm,v=9,y=5;else if(A==2){var V=xt(e,f,31)+257,R=xt(e,f+10,15)+4,Z=V+xt(e,f+5,31)+1;f+=14;for(var M=new vt(Z),T=new vt(19),$=0;$>4;if(m<16)M[$++]=m;else{var K=0,N=0;for(m==16?(N=3+xt(e,f,3),f+=2,K=M[$-1]):m==17?(N=3+xt(e,f,7),f+=3):m==18&&(N=11+xt(e,f,127),f+=7);N--;)M[$++]=K}}var J=M.subarray(0,V),ae=M.subarray(V);v=Li(J),y=Li(ae),p=Ol(J,v,1),g=Ol(ae,y,1)}else bt(1);else{var m=rm(f)+4,w=e[m-4]|e[m-3]<<8,F=m+w;if(F>r){c&&bt(0);break}o&&u(h+w),n.set(e.subarray(m,F),h),t.b=h+=w,t.p=f=F*8,t.f=d;continue}if(f>_){c&&bt(0);break}}o&&u(h+131072);for(var Ge=(1<>4;if(f+=K&15,f>_){c&&bt(0);break}if(K||bt(2),Ne<256)n[h++]=Ne;else if(Ne==256){ge=f,p=null;break}else{var gt=Ne-254;if(Ne>264){var $=Ne-257,rt=zd[$];gt=xt(e,f,(1<>4;We||bt(3),f+=We&15;var ae=tm[x];if(x>3){var rt=$d[x];ae+=Oi(e,f)&(1<_){c&&bt(0);break}o&&u(h+131072);var q=h+gt;if(h>4>7||(e[0]<<8|e[1])%31)&&bt(6,"invalid zlib data"),(e[1]>>5&1)==+!t&&bt(6,"invalid zlib data: "+(e[1]&32?"need":"unexpected")+" dictionary"),(e[1]>>3&4)+2};function cm(e,t){return am(e.subarray(sm(e,t),-4),{i:2},t,t)}var la=typeof TextDecoder<"u"&&new TextDecoder,um=0;try{la.decode(om,{stream:!0}),um=1}catch{}var dm=function(e){for(var t="",n=0;;){var l=e[n++],r=(l>127)+(l>223)+(l>239);if(n+r>e.length)return{s:t,r:Wd(e,n-1)};r?r==3?(l=((l&15)<<18|(e[n++]&63)<<12|(e[n++]&63)<<6|e[n++]&63)-65536,t+=String.fromCharCode(55296|l>>10,56320|l&1023)):r&1?t+=String.fromCharCode((l&31)<<6|e[n++]&63):t+=String.fromCharCode((l&15)<<12|(e[n++]&63)<<6|e[n++]&63):t+=String.fromCharCode(l)}};function fm(e,t){{for(var n=new vt(e.length),l=0;l{const t=atob(e);return hm(cm(fm(t)))},Ft=(e,t)=>{var l;const n=(l=(t==null?void 0:t._instance)??Xl())==null?void 0:l.appContext.components;return n?e in n||at(e)in n||Wl(at(e))in n:!1},no=e=>new Promise(t=>{setTimeout(t,e)}),Fn=e=>{const t=Mt();return k(()=>e[t.value]??{})},Jd=e=>typeof e<"u",Di=e=>typeof e=="number",{isArray:ra}=Array,Hr=(e,t)=>ke(e)&&e.startsWith(t),vm=(e,t)=>ke(e)&&e.endsWith(t),{entries:En}=Object,{fromEntries:mm}=Object,{keys:hn}=Object,lo=e=>{if(e){if(typeof e=="number")return new Date(e);const t=Date.parse(e.toString());if(!Number.isNaN(t))return new Date(t)}return null},ni=e=>Hr(e,"/");let Yd=e=>ke(e.title)?{title:e.title}:null;const Zd=Symbol(""),gm=e=>{Yd=e},Em=()=>Se(Zd),_m=e=>{e.provide(Zd,Yd)};var bm={"/":{title:"目录",empty:"暂无目录"}},ym=H({name:"Catalog",props:{base:{type:String,default:""},level:{type:Number,default:3},index:Boolean,hideHeading:Boolean},setup(e){const t=Em(),n=Fn(bm),l=Oe(),r=iv(),i=Id(),a=$e(En(r.value).map(([c,{meta:u}])=>{const d=t(u);if(!d)return null;const f=c.split("/").length;return{level:vm(c,"/")?f-2:f-1,base:c.replace(/\/[^/]+\/?$/,"/"),path:c,...d}}).filter(c=>nr(c)&&ke(c.title))),o=k(()=>{const c=e.base?W0(_d(e.base)):l.value.path.replace(/\/[^/]+$/,"/"),u=c.split("/").length-2,d=[];return a.value.filter(({level:f,path:h})=>{if(!Hr(h,c)||h===c)return!1;if(c==="/"){const p=hn(i.value.locales).filter(g=>g!=="/");if(h==="/404.html"||p.some(g=>Hr(h,g)))return!1}return f-u<=e.level}).sort(({title:f,level:h,order:p},{title:g,level:v,order:y})=>h-v||(Di(p)?Di(y)?p>0?y>0?p-y:-1:y<0?p-y:1:p:Di(y)?y:f.localeCompare(g))).forEach(f=>{var g;const{base:h,level:p}=f;switch(p-u){case 1:{d.push(f);break}case 2:{const v=d.find(y=>y.path===h);v&&(v.children??(v.children=[])).push(f);break}default:{const v=d.find(y=>y.path===h.replace(/\/[^/]+\/$/,"/"));if(v){const y=(g=v.children)==null?void 0:g.find(_=>_.path===h);y&&(y.children??(y.children=[])).push(f)}}}}),d});return()=>{const c=o.value.some(u=>u.children);return s("div",{class:["vp-catalog",{index:e.index}]},[e.hideHeading?null:s("h2",{class:"vp-catalog-main-title"},n.value.title),o.value.length?s(e.index?"ol":"ul",{class:["vp-catalog-list",{deep:c}]},o.value.map(({children:u=[],title:d,path:f,content:h})=>{const p=s(He,{class:"vp-catalog-title",to:f},()=>h?s(h):d);return s("li",{class:"vp-catalog-item"},c?[s("h3",{id:d,class:["vp-catalog-child-title",{"has-children":u.length}]},[s("a",{href:`#${d}`,class:"vp-catalog-header-anchor","aria-hidden":!0},"#"),p]),u.length?s(e.index?"ol":"ul",{class:"vp-child-catalogs"},u.map(({children:g=[],content:v,path:y,title:_})=>s("li",{class:"vp-child-catalog"},[s("div",{class:["vp-catalog-sub-title",{"has-children":g.length}]},[s("a",{href:`#${_}`,class:"vp-catalog-header-anchor"},"#"),s(He,{class:"vp-catalog-title",to:y},()=>v?s(v):_)]),g.length?s(e.index?"ol":"div",{class:e.index?"vp-sub-catalogs":"vp-sub-catalogs-wrapper"},g.map(({content:A,path:m,title:w})=>e.index?s("li",{class:"vp-sub-catalog"},s(He,{to:m},()=>A?s(A):w)):s(He,{class:"vp-sub-catalog-link",to:m},()=>A?s(A):w))):null]))):null]:s("div",{class:"vp-catalog-child-title"},p))})):s("p",{class:"vp-empty-catalog"},n.value.empty)])}}}),Am=Bt({enhance:({app:e})=>{_m(e),Ft("Catalog",e)||e.component("Catalog",ym)}});const km=Object.freeze(Object.defineProperty({__proto__:null,default:Am},Symbol.toStringTag,{value:"Module"}));var wm={"/":{backToTop:"返回顶部"}};const Bm=H({name:"BackToTop",setup(){const e=ve(),t=Fn(wm),n=$e(),{height:l}=Mv(n),{height:r}=$v(),{y:i}=zv(),a=k(()=>e.value.backToTop!==!1&&i.value>100),o=k(()=>i.value/(l.value-r.value)*100);return we(()=>{n.value=document.body}),()=>s(el,{name:"back-to-top"},()=>a.value?s("button",{type:"button",class:"vp-back-to-top-button","aria-label":t.value.backToTop,onClick:()=>{window.scrollTo({top:0,behavior:"smooth"})}},[s("span",{class:"vp-scroll-progress",role:"progressbar","aria-labelledby":"loadinglabel","aria-valuenow":o.value},s("svg",s("circle",{cx:"26",cy:"26",r:"24",fill:"none",stroke:"currentColor","stroke-width":"4","stroke-dasharray":`${Math.PI*o.value*.48} ${Math.PI*(100-o.value)*.48}`}))),s("div",{class:"back-to-top-icon"})]):null)}}),Cm=Bt({rootComponents:[Bm]}),xm=Object.freeze(Object.defineProperty({__proto__:null,default:Cm},Symbol.toStringTag,{value:"Module"}));/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const js=(e,t)=>{e.classList.add(t)},Hs=(e,t)=>{e.classList.remove(t)},Sm=e=>{var t;(t=e==null?void 0:e.parentNode)==null||t.removeChild(e)},Pi=(e,t,n)=>en?n:e,zs=e=>(-1+e)*100,Tm=(()=>{const e=[],t=()=>{const n=e.shift();n&&n(t)};return n=>{e.push(n),e.length===1&&t()}})(),Lm=e=>e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(t,n)=>n.toUpperCase()),wr=(()=>{const e=["Webkit","O","Moz","ms"],t={},n=i=>{const{style:a}=document.body;if(i in a)return i;const o=i.charAt(0).toUpperCase()+i.slice(1);let c=e.length;for(;c--;){const u=`${e[c]}${o}`;if(u in a)return u}return i},l=i=>{const a=Lm(i);return t[a]??(t[a]=n(a))},r=(i,a,o)=>{i.style[l(a)]=o};return(i,a)=>{for(const o in a){const c=a[o];Object.hasOwn(a,o)&&Jd(c)&&r(i,o,c)}}})(),qt={minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},Pe={percent:null,isRendered:()=>!!document.getElementById("nprogress"),set:e=>{const{speed:t,easing:n}=qt,l=Pe.isStarted(),r=Pi(e,qt.minimum,1);Pe.percent=r===1?null:r;const i=Pe.render(!l),a=i.querySelector(qt.barSelector);return i.offsetWidth,Tm(o=>{wr(a,{transform:`translate3d(${zs(r)}%,0,0)`,transition:`all ${t}ms ${n}`}),r===1?(wr(i,{transition:"none",opacity:"1"}),i.offsetWidth,setTimeout(()=>{wr(i,{transition:`all ${t}ms linear`,opacity:"0"}),setTimeout(()=>{Pe.remove(),o()},t)},t)):setTimeout(()=>{o()},t)}),Pe},isStarted:()=>typeof Pe.percent=="number",start:()=>{Pe.percent||Pe.set(0);const e=()=>{setTimeout(()=>{Pe.percent&&(Pe.trickle(),e())},qt.trickleSpeed)};return e(),Pe},done:e=>!e&&!Pe.percent?Pe:Pe.increase(.3+.5*Math.random()).set(1),increase:e=>{let{percent:t}=Pe;return t?(t=Pi(t+(typeof e=="number"?e:(1-t)*Pi(Math.random()*t,.1,.95)),0,.994),Pe.set(t)):Pe.start()},trickle:()=>Pe.increase(Math.random()*qt.trickleRate),render:e=>{if(Pe.isRendered())return document.getElementById("nprogress");js(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=qt.template;const n=t.querySelector(qt.barSelector),l=document.querySelector(qt.parent),r=e?"-100":zs(Pe.percent??0);return wr(n,{transition:"all 0 linear",transform:`translate3d(${r}%,0,0)`}),l&&(l!==document.body&&js(l,"nprogress-custom-parent"),l.appendChild(t)),t},remove:()=>{Hs(document.documentElement,"nprogress-busy"),Hs(document.querySelector(qt.parent),"nprogress-custom-parent"),Sm(document.getElementById("nprogress"))}},Om=()=>{we(()=>{const e=en(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||Pe.start()}),e.afterEach(n=>{t.add(n.path),Pe.done()})})},Dm=Bt({setup(){Om()}}),Pm=Object.freeze(Object.defineProperty({__proto__:null,default:Dm},Symbol.toStringTag,{value:"Module"}));var Im=Object.create,Qd=Object.defineProperty,Fm=Object.getOwnPropertyDescriptor,ro=Object.getOwnPropertyNames,Rm=Object.getPrototypeOf,Mm=Object.prototype.hasOwnProperty,Vm=(e,t)=>function(){return e&&(t=(0,e[ro(e)[0]])(e=0)),t},Nm=(e,t)=>function(){return t||(0,e[ro(e)[0]])((t={exports:{}}).exports,t),t.exports},jm=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of ro(t))!Mm.call(e,r)&&r!==n&&Qd(e,r,{get:()=>t[r],enumerable:!(l=Fm(t,r))||l.enumerable});return e},Hm=(e,t,n)=>(n=e!=null?Im(Rm(e)):{},jm(Qd(n,"default",{value:e,enumerable:!0}),e)),or=Vm({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),zm=Nm({"../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(e,t){or(),t.exports=l;function n(i){return i instanceof Buffer?Buffer.from(i):new i.constructor(i.buffer.slice(),i.byteOffset,i.length)}function l(i){if(i=i||{},i.circles)return r(i);const a=new Map;if(a.set(Date,f=>new Date(f)),a.set(Map,(f,h)=>new Map(c(Array.from(f),h))),a.set(Set,(f,h)=>new Set(c(Array.from(f),h))),i.constructorHandlers)for(const f of i.constructorHandlers)a.set(f[0],f[1]);let o=null;return i.proto?d:u;function c(f,h){const p=Object.keys(f),g=new Array(p.length);for(let v=0;vnew Date(p)),c.set(Map,(p,g)=>new Map(d(Array.from(p),g))),c.set(Set,(p,g)=>new Set(d(Array.from(p),g))),i.constructorHandlers)for(const p of i.constructorHandlers)c.set(p[0],p[1]);let u=null;return i.proto?h:f;function d(p,g){const v=Object.keys(p),y=new Array(v.length);for(let _=0;_(a=Jm(e,u,d),a.finally(()=>{if(a=null,n.trailing&&o&&!r){const f=c(u,o);return o=null,f}}),a);return function(...u){return a?(n.trailing&&(o=u),a):new Promise(d=>{const f=!r&&n.leading;clearTimeout(r),r=setTimeout(()=>{r=null;const h=n.leading?l:c(this,u);for(const p of i)p(h);i=[]},t),f?(l=c(this,u),d(l)):i.push(d)})}}async function Jm(e,t,n){return await e.apply(t,n)}function ia(e,t={},n){for(const l in e){const r=e[l],i=n?`${n}:${l}`:l;typeof r=="object"&&r!==null?ia(r,t,i):typeof r=="function"&&(t[i]=r)}return t}const Ym={run:e=>e()},Zm=()=>Ym,ef=typeof console.createTask<"u"?console.createTask:Zm;function Qm(e,t){const n=t.shift(),l=ef(n);return e.reduce((r,i)=>r.then(()=>l.run(()=>i(...t))),Promise.resolve())}function Xm(e,t){const n=t.shift(),l=ef(n);return Promise.all(e.map(r=>l.run(()=>r(...t))))}function Ii(e,t){for(const n of[...e])n(t)}class eg{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(t,n,l={}){if(!t||typeof n!="function")return()=>{};const r=t;let i;for(;this._deprecatedHooks[t];)i=this._deprecatedHooks[t],t=i.to;if(i&&!l.allowDeprecated){let a=i.message;a||(a=`${r} hook has been deprecated`+(i.to?`, please use ${i.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(a)||(console.warn(a),this._deprecatedMessages.add(a))}if(!n.name)try{Object.defineProperty(n,"name",{get:()=>"_"+t.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[t]=this._hooks[t]||[],this._hooks[t].push(n),()=>{n&&(this.removeHook(t,n),n=void 0)}}hookOnce(t,n){let l,r=(...i)=>(typeof l=="function"&&l(),l=void 0,r=void 0,n(...i));return l=this.hook(t,r),l}removeHook(t,n){if(this._hooks[t]){const l=this._hooks[t].indexOf(n);l!==-1&&this._hooks[t].splice(l,1),this._hooks[t].length===0&&delete this._hooks[t]}}deprecateHook(t,n){this._deprecatedHooks[t]=typeof n=="string"?{to:n}:n;const l=this._hooks[t]||[];delete this._hooks[t];for(const r of l)this.hook(t,r)}deprecateHooks(t){Object.assign(this._deprecatedHooks,t);for(const n in t)this.deprecateHook(n,t[n])}addHooks(t){const n=ia(t),l=Object.keys(n).map(r=>this.hook(r,n[r]));return()=>{for(const r of l.splice(0,l.length))r()}}removeHooks(t){const n=ia(t);for(const l in n)this.removeHook(l,n[l])}removeAllHooks(){for(const t in this._hooks)delete this._hooks[t]}callHook(t,...n){return n.unshift(t),this.callHookWith(Qm,t,...n)}callHookParallel(t,...n){return n.unshift(t),this.callHookWith(Xm,t,...n)}callHookWith(t,n,...l){const r=this._before||this._after?{name:n,args:l,context:{}}:void 0;this._before&&Ii(this._before,r);const i=t(n in this._hooks?[...this._hooks[n]]:[],l);return i instanceof Promise?i.finally(()=>{this._after&&r&&Ii(this._after,r)}):(this._after&&r&&Ii(this._after,r),i)}beforeEach(t){return this._before=this._before||[],this._before.push(t),()=>{if(this._before!==void 0){const n=this._before.indexOf(t);n!==-1&&this._before.splice(n,1)}}}afterEach(t){return this._after=this._after||[],this._after.push(t),()=>{if(this._after!==void 0){const n=this._after.indexOf(t);n!==-1&&this._after.splice(n,1)}}}}function tf(){return new eg}var tg=Object.create,nf=Object.defineProperty,ng=Object.getOwnPropertyDescriptor,io=Object.getOwnPropertyNames,lg=Object.getPrototypeOf,rg=Object.prototype.hasOwnProperty,ig=(e,t)=>function(){return e&&(t=(0,e[io(e)[0]])(e=0)),t},lf=(e,t)=>function(){return t||(0,e[io(e)[0]])((t={exports:{}}).exports,t),t.exports},ag=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of io(t))!rg.call(e,r)&&r!==n&&nf(e,r,{get:()=>t[r],enumerable:!(l=ng(t,r))||l.enumerable});return e},og=(e,t,n)=>(n=e!=null?tg(lg(e)):{},ag(nf(n,"default",{value:e,enumerable:!0}),e)),L=ig({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),sg=lf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js"(e,t){L(),function(n){var l={À:"A",Á:"A",Â:"A",Ã:"A",Ä:"Ae",Å:"A",Æ:"AE",Ç:"C",È:"E",É:"E",Ê:"E",Ë:"E",Ì:"I",Í:"I",Î:"I",Ï:"I",Ð:"D",Ñ:"N",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"Oe",Ő:"O",Ø:"O",Ù:"U",Ú:"U",Û:"U",Ü:"Ue",Ű:"U",Ý:"Y",Þ:"TH",ß:"ss",à:"a",á:"a",â:"a",ã:"a",ä:"ae",å:"a",æ:"ae",ç:"c",è:"e",é:"e",ê:"e",ë:"e",ì:"i",í:"i",î:"i",ï:"i",ð:"d",ñ:"n",ò:"o",ó:"o",ô:"o",õ:"o",ö:"oe",ő:"o",ø:"o",ù:"u",ú:"u",û:"u",ü:"ue",ű:"u",ý:"y",þ:"th",ÿ:"y","ẞ":"SS",ا:"a",أ:"a",إ:"i",آ:"aa",ؤ:"u",ئ:"e",ء:"a",ب:"b",ت:"t",ث:"th",ج:"j",ح:"h",خ:"kh",د:"d",ذ:"th",ر:"r",ز:"z",س:"s",ش:"sh",ص:"s",ض:"dh",ط:"t",ظ:"z",ع:"a",غ:"gh",ف:"f",ق:"q",ك:"k",ل:"l",م:"m",ن:"n",ه:"h",و:"w",ي:"y",ى:"a",ة:"h",ﻻ:"la",ﻷ:"laa",ﻹ:"lai",ﻵ:"laa",گ:"g",چ:"ch",پ:"p",ژ:"zh",ک:"k",ی:"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9",က:"k",ခ:"kh",ဂ:"g",ဃ:"ga",င:"ng",စ:"s",ဆ:"sa",ဇ:"z","စျ":"za",ည:"ny",ဋ:"t",ဌ:"ta",ဍ:"d",ဎ:"da",ဏ:"na",တ:"t",ထ:"ta",ဒ:"d",ဓ:"da",န:"n",ပ:"p",ဖ:"pa",ဗ:"b",ဘ:"ba",မ:"m",ယ:"y",ရ:"ya",လ:"l",ဝ:"w",သ:"th",ဟ:"h",ဠ:"la",အ:"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h",ဧ:"e","၏":"-e",ဣ:"i",ဤ:"-i",ဉ:"u",ဦ:"-u",ဩ:"aw","သြော":"aw",ဪ:"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"",č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z",ހ:"h",ށ:"sh",ނ:"n",ރ:"r",ބ:"b",ޅ:"lh",ކ:"k",އ:"a",ވ:"v",މ:"m",ފ:"f",ދ:"dh",ތ:"th",ލ:"l",ގ:"g",ޏ:"gn",ސ:"s",ޑ:"d",ޒ:"z",ޓ:"t",ޔ:"y",ޕ:"p",ޖ:"j",ޗ:"ch",ޘ:"tt",ޙ:"hh",ޚ:"kh",ޛ:"th",ޜ:"z",ޝ:"sh",ޞ:"s",ޟ:"d",ޠ:"t",ޡ:"z",ޢ:"a",ޣ:"gh",ޤ:"q",ޥ:"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"",ა:"a",ბ:"b",გ:"g",დ:"d",ე:"e",ვ:"v",ზ:"z",თ:"t",ი:"i",კ:"k",ლ:"l",მ:"m",ნ:"n",ო:"o",პ:"p",ჟ:"zh",რ:"r",ს:"s",ტ:"t",უ:"u",ფ:"p",ქ:"k",ღ:"gh",ყ:"q",შ:"sh",ჩ:"ch",ც:"ts",ძ:"dz",წ:"ts",ჭ:"ch",ხ:"kh",ჯ:"j",ჰ:"h",α:"a",β:"v",γ:"g",δ:"d",ε:"e",ζ:"z",η:"i",θ:"th",ι:"i",κ:"k",λ:"l",μ:"m",ν:"n",ξ:"ks",ο:"o",π:"p",ρ:"r",σ:"s",τ:"t",υ:"y",φ:"f",χ:"x",ψ:"ps",ω:"o",ά:"a",έ:"e",ί:"i",ό:"o",ύ:"y",ή:"i",ώ:"o",ς:"s",ϊ:"i",ΰ:"y",ϋ:"y",ΐ:"i",Α:"A",Β:"B",Γ:"G",Δ:"D",Ε:"E",Ζ:"Z",Η:"I",Θ:"TH",Ι:"I",Κ:"K",Λ:"L",Μ:"M",Ν:"N",Ξ:"KS",Ο:"O",Π:"P",Ρ:"R",Σ:"S",Τ:"T",Υ:"Y",Φ:"F",Χ:"X",Ψ:"PS",Ω:"O",Ά:"A",Έ:"E",Ί:"I",Ό:"O",Ύ:"Y",Ή:"I",Ώ:"O",Ϊ:"I",Ϋ:"Y",ā:"a",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",ū:"u",Ā:"A",Ē:"E",Ģ:"G",Ī:"I",Ķ:"k",Ļ:"L",Ņ:"N",Ū:"U",Ќ:"Kj",ќ:"kj",Љ:"Lj",љ:"lj",Њ:"Nj",њ:"nj",Тс:"Ts",тс:"ts",ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"E",Ł:"L",Ń:"N",Ś:"S",Ź:"Z",Ż:"Z",Є:"Ye",І:"I",Ї:"Yi",Ґ:"G",є:"ye",і:"i",ї:"yi",ґ:"g",ă:"a",Ă:"A",ș:"s",Ș:"S",ț:"t",Ț:"T",ţ:"t",Ţ:"T",а:"a",б:"b",в:"v",г:"g",д:"d",е:"e",ё:"yo",ж:"zh",з:"z",и:"i",й:"i",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",х:"kh",ц:"c",ч:"ch",ш:"sh",щ:"sh",ъ:"",ы:"y",ь:"",э:"e",ю:"yu",я:"ya",А:"A",Б:"B",В:"V",Г:"G",Д:"D",Е:"E",Ё:"Yo",Ж:"Zh",З:"Z",И:"I",Й:"I",К:"K",Л:"L",М:"M",Н:"N",О:"O",П:"P",Р:"R",С:"S",Т:"T",У:"U",Ф:"F",Х:"Kh",Ц:"C",Ч:"Ch",Ш:"Sh",Щ:"Sh",Ъ:"",Ы:"Y",Ь:"",Э:"E",Ю:"Yu",Я:"Ya",ђ:"dj",ј:"j",ћ:"c",џ:"dz",Ђ:"Dj",Ј:"j",Ћ:"C",Џ:"Dz",ľ:"l",ĺ:"l",ŕ:"r",Ľ:"L",Ĺ:"L",Ŕ:"R",ş:"s",Ş:"S",ı:"i",İ:"I",ğ:"g",Ğ:"G",ả:"a",Ả:"A",ẳ:"a",Ẳ:"A",ẩ:"a",Ẩ:"A",đ:"d",Đ:"D",ẹ:"e",Ẹ:"E",ẽ:"e",Ẽ:"E",ẻ:"e",Ẻ:"E",ế:"e",Ế:"E",ề:"e",Ề:"E",ệ:"e",Ệ:"E",ễ:"e",Ễ:"E",ể:"e",Ể:"E",ỏ:"o",ọ:"o",Ọ:"o",ố:"o",Ố:"O",ồ:"o",Ồ:"O",ổ:"o",Ổ:"O",ộ:"o",Ộ:"O",ỗ:"o",Ỗ:"O",ơ:"o",Ơ:"O",ớ:"o",Ớ:"O",ờ:"o",Ờ:"O",ợ:"o",Ợ:"O",ỡ:"o",Ỡ:"O",Ở:"o",ở:"o",ị:"i",Ị:"I",ĩ:"i",Ĩ:"I",ỉ:"i",Ỉ:"i",ủ:"u",Ủ:"U",ụ:"u",Ụ:"U",ũ:"u",Ũ:"U",ư:"u",Ư:"U",ứ:"u",Ứ:"U",ừ:"u",Ừ:"U",ự:"u",Ự:"U",ữ:"u",Ữ:"U",ử:"u",Ử:"ư",ỷ:"y",Ỷ:"y",ỳ:"y",Ỳ:"Y",ỵ:"y",Ỵ:"Y",ỹ:"y",Ỹ:"Y",ạ:"a",Ạ:"A",ấ:"a",Ấ:"A",ầ:"a",Ầ:"A",ậ:"a",Ậ:"A",ẫ:"a",Ẫ:"A",ắ:"a",Ắ:"A",ằ:"a",Ằ:"A",ặ:"a",Ặ:"A",ẵ:"a",Ẵ:"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d",ƒ:"f","™":"(TM)","©":"(C)",œ:"oe",Œ:"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o",º:"o",ª:"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY",元:"CNY",円:"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN",лв:"BGN","៛":"KHR","₡":"CRC","₸":"KZT",ден:"MKD",zł:"PLN","₽":"RUB","₾":"GEL"},r=["်","ް"],i={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},a={en:{},az:{ç:"c",ə:"e",ğ:"g",ı:"i",ö:"o",ş:"s",ü:"u",Ç:"C",Ə:"E",Ğ:"G",İ:"I",Ö:"O",Ş:"S",Ü:"U"},cs:{č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z"},fi:{ä:"a",Ä:"A",ö:"o",Ö:"O"},hu:{ä:"a",Ä:"A",ö:"o",Ö:"O",ü:"u",Ü:"U",ű:"u",Ű:"U"},lt:{ą:"a",č:"c",ę:"e",ė:"e",į:"i",š:"s",ų:"u",ū:"u",ž:"z",Ą:"A",Č:"C",Ę:"E",Ė:"E",Į:"I",Š:"S",Ų:"U",Ū:"U"},lv:{ā:"a",č:"c",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",š:"s",ū:"u",ž:"z",Ā:"A",Č:"C",Ē:"E",Ģ:"G",Ī:"i",Ķ:"k",Ļ:"L",Ņ:"N",Š:"S",Ū:"u",Ž:"Z"},pl:{ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ó:"o",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"e",Ł:"L",Ń:"N",Ó:"O",Ś:"S",Ź:"Z",Ż:"Z"},sv:{ä:"a",Ä:"A",ö:"o",Ö:"O"},sk:{ä:"a",Ä:"A"},sr:{љ:"lj",њ:"nj",Љ:"Lj",Њ:"Nj",đ:"dj",Đ:"Dj"},tr:{Ü:"U",Ö:"O",ü:"u",ö:"o"}},o={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},c=[";","?",":","@","&","=","+","$",",","/"].join(""),u=[";","?",":","@","&","=","+","$",","].join(""),d=[".","!","~","*","'","(",")"].join(""),f=function(y,_){var A="-",m="",w="",F=!0,V={},R,Z,M,T,$,O,X,se,he,K,N,J,ae,Ge,Fe="";if(typeof y!="string")return"";if(typeof _=="string"&&(A=_),X=o.en,se=a.en,typeof _=="object"){R=_.maintainCase||!1,V=_.custom&&typeof _.custom=="object"?_.custom:V,M=+_.truncate>1&&_.truncate||!1,T=_.uric||!1,$=_.uricNoSlash||!1,O=_.mark||!1,F=!(_.symbols===!1||_.lang===!1),A=_.separator||A,T&&(Fe+=c),$&&(Fe+=u),O&&(Fe+=d),X=_.lang&&o[_.lang]&&F?o[_.lang]:F?o.en:{},se=_.lang&&a[_.lang]?a[_.lang]:_.lang===!1||_.lang===!0?{}:a.en,_.titleCase&&typeof _.titleCase.length=="number"&&Array.prototype.toString.call(_.titleCase)?(_.titleCase.forEach(function(ge){V[ge+""]=ge+""}),Z=!0):Z=!!_.titleCase,_.custom&&typeof _.custom.length=="number"&&Array.prototype.toString.call(_.custom)&&_.custom.forEach(function(ge){V[ge+""]=ge+""}),Object.keys(V).forEach(function(ge){var Ne;ge.length>1?Ne=new RegExp("\\b"+p(ge)+"\\b","gi"):Ne=new RegExp(p(ge),"gi"),y=y.replace(Ne,V[ge])});for(N in V)Fe+=N}for(Fe+=A,Fe=p(Fe),y=y.replace(/(^\s+|\s+$)/g,""),ae=!1,Ge=!1,K=0,J=y.length;K=0?(w+=N,N=""):Ge===!0?(N=i[w]+l[N],w=""):N=ae&&l[N].match(/[A-Za-z0-9]/)?" "+l[N]:l[N],ae=!1,Ge=!1):N in i?(w+=N,N="",K===J-1&&(N=i[w]),Ge=!0):X[N]&&!(T&&c.indexOf(N)!==-1)&&!($&&u.indexOf(N)!==-1)?(N=ae||m.substr(-1).match(/[A-Za-z0-9]/)?A+X[N]:X[N],N+=y[K+1]!==void 0&&y[K+1].match(/[A-Za-z0-9]/)?A:"",ae=!0):(Ge===!0?(N=i[w]+N,w="",Ge=!1):ae&&(/[A-Za-z0-9]/.test(N)||m.substr(-1).match(/A-Za-z0-9]/))&&(N=" "+N),ae=!1),m+=N.replace(new RegExp("[^\\w\\s"+Fe+"_-]","g"),A);return Z&&(m=m.replace(/(\w)(\S*)/g,function(ge,Ne,gt){var rt=Ne.toUpperCase()+(gt!==null?gt:"");return Object.keys(V).indexOf(rt.toLowerCase())<0?rt:rt.toLowerCase()})),m=m.replace(/\s+/g,A).replace(new RegExp("\\"+A+"+","g"),A).replace(new RegExp("(^\\"+A+"+|\\"+A+"+$)","g"),""),M&&m.length>M&&(he=m.charAt(M)===A,m=m.slice(0,M),he||(m=m.slice(0,m.lastIndexOf(A)))),!R&&!Z&&(m=m.toLowerCase()),m},h=function(y){return function(A){return f(A,y)}},p=function(y){return y.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},g=function(v,y){for(var _ in y)if(y[_]===v)return!0};if(typeof t<"u"&&t.exports)t.exports=f,t.exports.createSlug=h;else if(typeof define<"u"&&define.amd)define([],function(){return f});else try{if(n.getSlug||n.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";n.getSlug=f,n.createSlug=h}catch{}}(e)}}),cg=lf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js"(e,t){L(),t.exports=sg()}});L();L();L();L();L();L();L();L();function ug(e){var t;const n=e.name||e._componentTag||e.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__||e.__name;return n==="index"&&((t=e.__file)!=null&&t.endsWith("index.vue"))?"":n}function dg(e){const t=e.__file;if(t)return Km(qm(t,".vue"))}function Gs(e,t){return e.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__=t,t}function li(e){if(e.__VUE_DEVTOOLS_NEXT_APP_RECORD__)return e.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(e.root)return e.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__}async function fg(e){const{app:t,uid:n,instance:l}=e;try{if(l.__VUE_DEVTOOLS_NEXT_UID__)return l.__VUE_DEVTOOLS_NEXT_UID__;const r=await li(t);if(!r)return null;const i=r.rootInstance===l;return`${r.id}:${i?"root":n}`}catch{}}function rf(e){var t,n;const l=(t=e.subTree)==null?void 0:t.type,r=li(e);return r?((n=r==null?void 0:r.types)==null?void 0:n.Fragment)===l:!1}function ri(e){var t,n,l;const r=ug((e==null?void 0:e.type)||{});if(r)return r;if((e==null?void 0:e.root)===e)return"Root";for(const a in(n=(t=e.parent)==null?void 0:t.type)==null?void 0:n.components)if(e.parent.type.components[a]===(e==null?void 0:e.type))return Gs(e,a);for(const a in(l=e.appContext)==null?void 0:l.components)if(e.appContext.components[a]===(e==null?void 0:e.type))return Gs(e,a);const i=dg((e==null?void 0:e.type)||{});return i||"Anonymous Component"}function aa(e,t){return t=t||`${e.id}:root`,e.instanceMap.get(t)||e.instanceMap.get(":root")}function hg(){const e={top:0,bottom:0,left:0,right:0,get width(){return e.right-e.left},get height(){return e.bottom-e.top}};return e}var Br;function pg(e){return Br||(Br=document.createRange()),Br.selectNode(e),Br.getBoundingClientRect()}function vg(e){const t=hg();if(!e.children)return t;for(let n=0,l=e.children.length;ne.bottom)&&(e.bottom=t.bottom),(!e.left||t.lefte.right)&&(e.right=t.right),e}var Ks={top:0,left:0,right:0,bottom:0,width:0,height:0};function Dn(e){const t=e.subTree.el;return typeof window>"u"?Ks:rf(e)?vg(e.subTree):(t==null?void 0:t.nodeType)===1?t==null?void 0:t.getBoundingClientRect():e.subTree.component?Dn(e.subTree.component):Ks}L();function ao(e){return rf(e)?gg(e.subTree):e.subTree?[e.subTree.el]:[]}function gg(e){if(!e.children)return[];const t=[];return e.children.forEach(n=>{n.component?t.push(...ao(n.component)):n!=null&&n.el&&t.push(n.el)}),t}var af="__vue-devtools-component-inspector__",of="__vue-devtools-component-inspector__card__",sf="__vue-devtools-component-inspector__name__",cf="__vue-devtools-component-inspector__indicator__",uf={display:"block",zIndex:2147483640,position:"fixed",backgroundColor:"#42b88325",border:"1px solid #42b88350",borderRadius:"5px",transition:"all 0.1s ease-in",pointerEvents:"none"},Eg={fontFamily:"Arial, Helvetica, sans-serif",padding:"5px 8px",borderRadius:"4px",textAlign:"left",position:"absolute",left:0,color:"#e9e9e9",fontSize:"14px",fontWeight:600,lineHeight:"24px",backgroundColor:"#42b883",boxShadow:"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"},_g={display:"inline-block",fontWeight:400,fontStyle:"normal",fontSize:"12px",opacity:.7};function dl(){return document.getElementById(af)}function bg(){return document.getElementById(of)}function yg(){return document.getElementById(cf)}function Ag(){return document.getElementById(sf)}function oo(e){return{left:`${Math.round(e.left*100)/100}px`,top:`${Math.round(e.top*100)/100}px`,width:`${Math.round(e.width*100)/100}px`,height:`${Math.round(e.height*100)/100}px`}}function so(e){var t;const n=document.createElement("div");n.id=(t=e.elementId)!=null?t:af,Object.assign(n.style,{...uf,...oo(e.bounds),...e.style});const l=document.createElement("span");l.id=of,Object.assign(l.style,{...Eg,top:e.bounds.top<35?0:"-35px"});const r=document.createElement("span");r.id=sf,r.innerHTML=`<${e.name}>  `;const i=document.createElement("i");return i.id=cf,i.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`,Object.assign(i.style,_g),l.appendChild(r),l.appendChild(i),n.appendChild(l),document.body.appendChild(n),n}function co(e){const t=dl(),n=bg(),l=Ag(),r=yg();t&&(Object.assign(t.style,{...uf,...oo(e.bounds)}),Object.assign(n.style,{top:e.bounds.top<35?0:"-35px"}),l.innerHTML=`<${e.name}>  `,r.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`)}function kg(e){const t=Dn(e);if(!t.width&&!t.height)return;const n=ri(e);dl()?co({bounds:t,name:n}):so({bounds:t,name:n})}function df(){const e=dl();e&&(e.style.display="none")}var oa=null;function sa(e){const t=e.target;if(t){const n=t.__vueParentComponent;if(n&&(oa=n,n.vnode.el)){const r=Dn(n),i=ri(n);dl()?co({bounds:r,name:i}):so({bounds:r,name:i})}}}function wg(e,t){var n;if(e.preventDefault(),e.stopPropagation(),oa){const l=(n=Ze.value)==null?void 0:n.app;fg({app:l,uid:l.uid,instance:oa}).then(r=>{t(r)})}}var zr=null;function Bg(){df(),window.removeEventListener("mouseover",sa),window.removeEventListener("click",zr,!0),zr=null}function Cg(){return window.addEventListener("mouseover",sa),new Promise(e=>{function t(n){n.preventDefault(),n.stopPropagation(),wg(n,l=>{window.removeEventListener("click",t,!0),zr=null,window.removeEventListener("mouseover",sa);const r=dl();r&&(r.style.display="none"),e(JSON.stringify({id:l}))})}zr=t,window.addEventListener("click",t,!0)})}function xg(e){const t=aa(Ze.value,e.id);if(t){const[n]=ao(t);if(typeof n.scrollIntoView=="function")n.scrollIntoView({behavior:"smooth"});else{const l=Dn(t),r=document.createElement("div"),i={...oo(l),position:"absolute"};Object.assign(r.style,i),document.body.appendChild(r),r.scrollIntoView({behavior:"smooth"}),setTimeout(()=>{document.body.removeChild(r)},2e3)}setTimeout(()=>{const l=Dn(t);if(l.width||l.height){const r=ri(t),i=dl();i?co({...e,name:r,bounds:l}):so({...e,name:r,bounds:l}),setTimeout(()=>{i&&(i.style.display="none")},1500)}},1200)}}L();var qs,Ws;(Ws=(qs=Y).__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__)!=null||(qs.__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__=!0);function Sg(e){let t=0;const n=setInterval(()=>{Y.__VUE_INSPECTOR__&&(clearInterval(n),t+=30,e()),t>=5e3&&clearInterval(n)},30)}function Tg(){const e=Y.__VUE_INSPECTOR__,t=e.openInEditor;e.openInEditor=async(...n)=>{e.disable(),t(...n)}}function Lg(){return new Promise(e=>{function t(){Tg(),e(Y.__VUE_INSPECTOR__)}Y.__VUE_INSPECTOR__?t():Sg(()=>{t()})})}L();L();function Og(e){return!!(e&&e.__v_isReadonly)}function ff(e){return Og(e)?ff(e.__v_raw):!!(e&&e.__v_isReactive)}function Fi(e){return!!(e&&e.__v_isRef===!0)}function bl(e){const t=e&&e.__v_raw;return t?bl(t):e}var Dg=class{constructor(){this.refEditor=new Pg}set(e,t,n,l){const r=Array.isArray(t)?t:t.split(".");for(;r.length>1;){const o=r.shift();e instanceof Map&&(e=e.get(o)),e instanceof Set?e=Array.from(e.values())[o]:e=e[o],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}const i=r[0],a=this.refEditor.get(e)[i];l?l(e,i,n):this.refEditor.isRef(a)?this.refEditor.set(a,n):e[i]=n}get(e,t){const n=Array.isArray(t)?t:t.split(".");for(let l=0;l"u")return!1;const l=Array.isArray(t)?t.slice():t.split("."),r=n?2:1;for(;e&&l.length>r;){const i=l.shift();e=e[i],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}return e!=null&&Object.prototype.hasOwnProperty.call(e,l[0])}createDefaultSetCallback(e){return(t,n,l)=>{if((e.remove||e.newKey)&&(Array.isArray(t)?t.splice(n,1):bl(t)instanceof Map?t.delete(n):bl(t)instanceof Set?t.delete(Array.from(t.values())[n]):Reflect.deleteProperty(t,n)),!e.remove){const r=t[e.newKey||n];this.refEditor.isRef(r)?this.refEditor.set(r,l):bl(t)instanceof Map?t.set(e.newKey||n,l):bl(t)instanceof Set?t.add(l):t[e.newKey||n]=l}}}},Pg=class{set(e,t){if(Fi(e))e.value=t;else{if(e instanceof Set&&Array.isArray(t)){e.clear(),t.forEach(r=>e.add(r));return}const n=Object.keys(t);if(e instanceof Map){const r=new Set(e.keys());n.forEach(i=>{e.set(i,Reflect.get(t,i)),r.delete(i)}),r.forEach(i=>e.delete(i));return}const l=new Set(Object.keys(e));n.forEach(r=>{Reflect.set(e,r,Reflect.get(t,r)),l.delete(r)}),l.forEach(r=>Reflect.deleteProperty(e,r))}}get(e){return Fi(e)?e.value:e}isRef(e){return Fi(e)||ff(e)}};L();L();L();var Ig="__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__";function Fg(){if(!Xd||typeof localStorage>"u"||localStorage===null)return{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""};const e=localStorage.getItem(Ig);return e?JSON.parse(e):{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""}}L();L();L();var Js,Ys;(Ys=(Js=Y).__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS)!=null||(Js.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS=[]);var Rg=new Proxy(Y.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS,{get(e,t,n){return Reflect.get(e,t,n)}});function Mg(e,t){je.timelineLayersState[t.id]=!1,Rg.push({...e,descriptorId:t.id,appRecord:li(t.app)})}var Zs,Qs;(Qs=(Zs=Y).__VUE_DEVTOOLS_KIT_INSPECTOR__)!=null||(Zs.__VUE_DEVTOOLS_KIT_INSPECTOR__=[]);var uo=new Proxy(Y.__VUE_DEVTOOLS_KIT_INSPECTOR__,{get(e,t,n){return Reflect.get(e,t,n)}}),hf=rl(()=>{fl.hooks.callHook("sendInspectorToClient",pf())});function Vg(e,t){var n,l;uo.push({options:e,descriptor:t,treeFilterPlaceholder:(n=e.treeFilterPlaceholder)!=null?n:"Search tree...",stateFilterPlaceholder:(l=e.stateFilterPlaceholder)!=null?l:"Search state...",treeFilter:"",selectedNodeId:"",appRecord:li(t.app)}),hf()}function pf(){return uo.filter(e=>e.descriptor.app===Ze.value.app).filter(e=>e.descriptor.id!=="components").map(e=>{var t;const n=e.descriptor,l=e.options;return{id:l.id,label:l.label,logo:n.logo,icon:`custom-ic-baseline-${(t=l==null?void 0:l.icon)==null?void 0:t.replace(/_/g,"-")}`,packageName:n.packageName,homepage:n.homepage,pluginId:n.id}})}function Tr(e,t){return uo.find(n=>n.options.id===e&&(t?n.descriptor.app===t:!0))}function Ng(){const e=tf();e.hook("addInspector",({inspector:l,plugin:r})=>{Vg(l,r.descriptor)});const t=rl(async({inspectorId:l,plugin:r})=>{var i;if(!l||!((i=r==null?void 0:r.descriptor)!=null&&i.app)||je.highPerfModeEnabled)return;const a=Tr(l,r.descriptor.app),o={app:r.descriptor.app,inspectorId:l,filter:(a==null?void 0:a.treeFilter)||"",rootNodes:[]};await new Promise(c=>{e.callHookWith(async u=>{await Promise.all(u.map(d=>d(o))),c()},"getInspectorTree")}),e.callHookWith(async c=>{await Promise.all(c.map(u=>u({inspectorId:l,rootNodes:o.rootNodes})))},"sendInspectorTreeToClient")},120);e.hook("sendInspectorTree",t);const n=rl(async({inspectorId:l,plugin:r})=>{var i;if(!l||!((i=r==null?void 0:r.descriptor)!=null&&i.app)||je.highPerfModeEnabled)return;const a=Tr(l,r.descriptor.app),o={app:r.descriptor.app,inspectorId:l,nodeId:(a==null?void 0:a.selectedNodeId)||"",state:null},c={currentTab:`custom-inspector:${l}`};o.nodeId&&await new Promise(u=>{e.callHookWith(async d=>{await Promise.all(d.map(f=>f(o,c))),u()},"getInspectorState")}),e.callHookWith(async u=>{await Promise.all(u.map(d=>d({inspectorId:l,nodeId:o.nodeId,state:o.state})))},"sendInspectorStateToClient")},120);return e.hook("sendInspectorState",n),e.hook("customInspectorSelectNode",({inspectorId:l,nodeId:r,plugin:i})=>{const a=Tr(l,i.descriptor.app);a&&(a.selectedNodeId=r)}),e.hook("timelineLayerAdded",({options:l,plugin:r})=>{Mg(l,r.descriptor)}),e.hook("timelineEventAdded",({options:l,plugin:r})=>{var i;const a=["performance","component-event","keyboard","mouse"];je.highPerfModeEnabled||!((i=je.timelineLayersState)!=null&&i[r.descriptor.id])&&!a.includes(l.layerId)||e.callHookWith(async o=>{await Promise.all(o.map(c=>c(l)))},"sendTimelineEventToClient")}),e.hook("getComponentInstances",async({app:l})=>{const r=l.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(!r)return null;const i=r.id.toString();return[...r.instanceMap].filter(([o])=>o.split(":")[0]===i).map(([,o])=>o)}),e.hook("getComponentBounds",async({instance:l})=>Dn(l)),e.hook("getComponentName",({instance:l})=>ri(l)),e.hook("componentHighlight",({uid:l})=>{const r=Ze.value.instanceMap.get(l);r&&kg(r)}),e.hook("componentUnhighlight",()=>{df()}),e}var Xs,ec;(ec=(Xs=Y).__VUE_DEVTOOLS_KIT_APP_RECORDS__)!=null||(Xs.__VUE_DEVTOOLS_KIT_APP_RECORDS__=[]);var tc,nc;(nc=(tc=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__)!=null||(tc.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__={});var lc,rc;(rc=(lc=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__)!=null||(lc.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__="");var ic,ac;(ac=(ic=Y).__VUE_DEVTOOLS_KIT_CUSTOM_TABS__)!=null||(ic.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__=[]);var oc,sc;(sc=(oc=Y).__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__)!=null||(oc.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__=[]);var Cn="__VUE_DEVTOOLS_KIT_GLOBAL_STATE__";function jg(){return{connected:!1,clientConnected:!1,vitePluginDetected:!0,appRecords:[],activeAppRecordId:"",tabs:[],commands:[],highPerfModeEnabled:!0,devtoolsClientDetected:{},perfUniqueGroupId:0,timelineLayersState:Fg()}}var cc,uc;(uc=(cc=Y)[Cn])!=null||(cc[Cn]=jg());var Hg=rl(e=>{fl.hooks.callHook("devtoolsStateUpdated",{state:e})});rl((e,t)=>{fl.hooks.callHook("devtoolsConnectedUpdated",{state:e,oldState:t})});var ii=new Proxy(Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__,{get(e,t,n){return t==="value"?Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__:Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__[t]}}),Ze=new Proxy(Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__,{get(e,t,n){return t==="value"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__:t==="id"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__:Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__[t]}});function vf(){Hg({...Y[Cn],appRecords:ii.value,activeAppRecordId:Ze.id,tabs:Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__,commands:Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__})}function zg(e){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__=e,vf()}function $g(e){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__=e,vf()}var je=new Proxy(Y[Cn],{get(e,t){return t==="appRecords"?ii:t==="activeAppRecordId"?Ze.id:t==="tabs"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__:t==="commands"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__:Y[Cn][t]},deleteProperty(e,t){return delete e[t],!0},set(e,t,n){return{...Y[Cn]},e[t]=n,Y[Cn][t]=n,!0}});function Ug(e={}){var t,n,l;const{file:r,host:i,baseUrl:a=window.location.origin,line:o=0,column:c=0}=e;if(r){if(i==="chrome-extension"){const u=r.replace(/\\/g,"\\\\"),d=(n=(t=window.VUE_DEVTOOLS_CONFIG)==null?void 0:t.openInEditorHost)!=null?n:"/";fetch(`${d}__open-in-editor?file=${encodeURI(r)}`).then(f=>{if(!f.ok){const h=`Opening component ${u} failed`;console.log(`%c${h}`,"color:red")}})}else if(je.vitePluginDetected){const u=(l=Y.__VUE_DEVTOOLS_OPEN_IN_EDITOR_BASE_URL__)!=null?l:a;Y.__VUE_INSPECTOR__.openInEditor(u,r,o,c)}}}L();L();L();L();L();var dc,fc;(fc=(dc=Y).__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__)!=null||(dc.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__=[]);var fo=new Proxy(Y.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__,{get(e,t,n){return Reflect.get(e,t,n)}});function ca(e){const t={};return Object.keys(e).forEach(n=>{t[n]=e[n].defaultValue}),t}function ho(e){return`__VUE_DEVTOOLS_NEXT_PLUGIN_SETTINGS__${e}__`}function Gg(e){var t,n,l;const r=(n=(t=fo.find(i=>{var a;return i[0].id===e&&!!((a=i[0])!=null&&a.settings)}))==null?void 0:t[0])!=null?n:null;return(l=r==null?void 0:r.settings)!=null?l:null}function mf(e,t){var n,l,r;const i=ho(e);if(i){const a=localStorage.getItem(i);if(a)return JSON.parse(a)}if(e){const a=(l=(n=fo.find(o=>o[0].id===e))==null?void 0:n[0])!=null?l:null;return ca((r=a==null?void 0:a.settings)!=null?r:{})}return ca(t)}function Kg(e,t){const n=ho(e);localStorage.getItem(n)||localStorage.setItem(n,JSON.stringify(ca(t)))}function qg(e,t,n){const l=ho(e),r=localStorage.getItem(l),i=JSON.parse(r||"{}"),a={...i,[t]:n};localStorage.setItem(l,JSON.stringify(a)),fl.hooks.callHookWith(o=>{o.forEach(c=>c({pluginId:e,key:t,oldValue:i[t],newValue:n,settings:a}))},"setPluginSettings")}L();L();L();L();L();L();L();L();L();L();L();var hc,pc,ft=(pc=(hc=Y).__VUE_DEVTOOLS_HOOK)!=null?pc:hc.__VUE_DEVTOOLS_HOOK=tf(),Wg={vueAppInit(e){ft.hook("app:init",e)},vueAppUnmount(e){ft.hook("app:unmount",e)},vueAppConnected(e){ft.hook("app:connected",e)},componentAdded(e){return ft.hook("component:added",e)},componentEmit(e){return ft.hook("component:emit",e)},componentUpdated(e){return ft.hook("component:updated",e)},componentRemoved(e){return ft.hook("component:removed",e)},setupDevtoolsPlugin(e){ft.hook("devtools-plugin:setup",e)},perfStart(e){return ft.hook("perf:start",e)},perfEnd(e){return ft.hook("perf:end",e)}},Jg={on:Wg,setupDevToolsPlugin(e,t){return ft.callHook("devtools-plugin:setup",e,t)}},Yg=class{constructor({plugin:e,ctx:t}){this.hooks=t.hooks,this.plugin=e}get on(){return{visitComponentTree:e=>{this.hooks.hook("visitComponentTree",e)},inspectComponent:e=>{this.hooks.hook("inspectComponent",e)},editComponentState:e=>{this.hooks.hook("editComponentState",e)},getInspectorTree:e=>{this.hooks.hook("getInspectorTree",e)},getInspectorState:e=>{this.hooks.hook("getInspectorState",e)},editInspectorState:e=>{this.hooks.hook("editInspectorState",e)},inspectTimelineEvent:e=>{this.hooks.hook("inspectTimelineEvent",e)},timelineCleared:e=>{this.hooks.hook("timelineCleared",e)},setPluginSettings:e=>{this.hooks.hook("setPluginSettings",e)}}}notifyComponentUpdate(e){var t;if(je.highPerfModeEnabled)return;const n=pf().find(l=>l.packageName===this.plugin.descriptor.packageName);if(n!=null&&n.id){if(e){const l=[e.appContext.app,e.uid,(t=e.parent)==null?void 0:t.uid,e];ft.callHook("component:updated",...l)}else ft.callHook("component:updated");this.hooks.callHook("sendInspectorState",{inspectorId:n.id,plugin:this.plugin})}}addInspector(e){this.hooks.callHook("addInspector",{inspector:e,plugin:this.plugin}),this.plugin.descriptor.settings&&Kg(e.id,this.plugin.descriptor.settings)}sendInspectorTree(e){je.highPerfModeEnabled||this.hooks.callHook("sendInspectorTree",{inspectorId:e,plugin:this.plugin})}sendInspectorState(e){je.highPerfModeEnabled||this.hooks.callHook("sendInspectorState",{inspectorId:e,plugin:this.plugin})}selectInspectorNode(e,t){this.hooks.callHook("customInspectorSelectNode",{inspectorId:e,nodeId:t,plugin:this.plugin})}visitComponentTree(e){return this.hooks.callHook("visitComponentTree",e)}now(){return je.highPerfModeEnabled?0:Date.now()}addTimelineLayer(e){this.hooks.callHook("timelineLayerAdded",{options:e,plugin:this.plugin})}addTimelineEvent(e){je.highPerfModeEnabled||this.hooks.callHook("timelineEventAdded",{options:e,plugin:this.plugin})}getSettings(e){return mf(e??this.plugin.descriptor.id,this.plugin.descriptor.settings)}getComponentInstances(e){return this.hooks.callHook("getComponentInstances",{app:e})}getComponentBounds(e){return this.hooks.callHook("getComponentBounds",{instance:e})}getComponentName(e){return this.hooks.callHook("getComponentName",{instance:e})}highlightElement(e){const t=e.__VUE_DEVTOOLS_NEXT_UID__;return this.hooks.callHook("componentHighlight",{uid:t})}unhighlightElement(){return this.hooks.callHook("componentUnhighlight")}},Zg=Yg;L();L();L();L();var Qg="__vue_devtool_undefined__",Xg="__vue_devtool_infinity__",e4="__vue_devtool_negative_infinity__",t4="__vue_devtool_nan__";L();L();var n4={[Qg]:"undefined",[t4]:"NaN",[Xg]:"Infinity",[e4]:"-Infinity"};Object.entries(n4).reduce((e,[t,n])=>(e[n]=t,e),{});L();L();L();L();L();var vc,mc;(mc=(vc=Y).__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__)!=null||(vc.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__=new Set);function l4(e,t){const[n,l]=e;if(n.app!==t)return;const r=new Zg({plugin:{setupFn:l,descriptor:n},ctx:fl});n.packageName==="vuex"&&r.on.editInspectorState(i=>{r.sendInspectorState(i.inspectorId)}),l(r)}function gf(e){Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.has(e)||je.highPerfModeEnabled||(Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.add(e),fo.forEach(t=>{l4(t,e)}))}L();L();var Hl="__VUE_DEVTOOLS_ROUTER__",il="__VUE_DEVTOOLS_ROUTER_INFO__",gc,Ec;(Ec=(gc=Y)[il])!=null||(gc[il]={currentRoute:null,routes:[]});var _c,bc;(bc=(_c=Y)[Hl])!=null||(_c[Hl]={});new Proxy(Y[il],{get(e,t){return Y[il][t]}});new Proxy(Y[Hl],{get(e,t){if(t==="value")return Y[Hl]}});function r4(e){const t=new Map;return((e==null?void 0:e.getRoutes())||[]).filter(n=>!t.has(n.path)&&t.set(n.path,1))}function po(e){return e.map(t=>{let{path:n,name:l,children:r,meta:i}=t;return r!=null&&r.length&&(r=po(r)),{path:n,name:l,children:r,meta:i}})}function i4(e){if(e){const{fullPath:t,hash:n,href:l,path:r,name:i,matched:a,params:o,query:c}=e;return{fullPath:t,hash:n,href:l,path:r,name:i,params:o,query:c,matched:po(a)}}return e}function a4(e,t){function n(){var l;const r=(l=e.app)==null?void 0:l.config.globalProperties.$router,i=i4(r==null?void 0:r.currentRoute.value),a=po(r4(r)),o=console.warn;console.warn=()=>{},Y[il]={currentRoute:i?Us(i):{},routes:Us(a)},Y[Hl]=r,console.warn=o}n(),Jg.on.componentUpdated(rl(()=>{var l;((l=t.value)==null?void 0:l.app)===e.app&&(n(),!je.highPerfModeEnabled&&fl.hooks.callHook("routerInfoUpdated",{state:Y[il]}))},200))}function o4(e){return{async getInspectorTree(t){const n={...t,app:Ze.value.app,rootNodes:[]};return await new Promise(l=>{e.callHookWith(async r=>{await Promise.all(r.map(i=>i(n))),l()},"getInspectorTree")}),n.rootNodes},async getInspectorState(t){const n={...t,app:Ze.value.app,state:null},l={currentTab:`custom-inspector:${t.inspectorId}`};return await new Promise(r=>{e.callHookWith(async i=>{await Promise.all(i.map(a=>a(n,l))),r()},"getInspectorState")}),n.state},editInspectorState(t){const n=new Dg,l={...t,app:Ze.value.app,set:(r,i=t.path,a=t.state.value,o)=>{n.set(r,i,a,o||n.createDefaultSetCallback(t.state))}};e.callHookWith(r=>{r.forEach(i=>i(l))},"editInspectorState")},sendInspectorState(t){const n=Tr(t);e.callHook("sendInspectorState",{inspectorId:t,plugin:{descriptor:n.descriptor,setupFn:()=>({})}})},inspectComponentInspector(){return Cg()},cancelInspectComponentInspector(){return Bg()},getComponentRenderCode(t){const n=aa(Ze.value,t);if(n)return(n==null?void 0:n.type)instanceof Function?n.type.toString():n.render.toString()},scrollToComponent(t){return xg({id:t})},openInEditor:Ug,getVueInspector:Lg,toggleApp(t){const n=ii.value.find(l=>l.id===t);n&&($g(t),zg(n),a4(n,Ze),hf(),gf(n.app))},inspectDOM(t){const n=aa(Ze.value,t);if(n){const[l]=ao(n);l&&(Y.__VUE_DEVTOOLS_INSPECT_DOM_TARGET__=l)}},updatePluginSettings(t,n,l){qg(t,n,l)},getPluginSettings(t){return{options:Gg(t),values:mf(t)}}}}L();var yc,Ac;(Ac=(yc=Y).__VUE_DEVTOOLS_ENV__)!=null||(yc.__VUE_DEVTOOLS_ENV__={vitePluginDetected:!1});var kc=Ng(),wc,Bc;(Bc=(wc=Y).__VUE_DEVTOOLS_KIT_CONTEXT__)!=null||(wc.__VUE_DEVTOOLS_KIT_CONTEXT__={hooks:kc,get state(){return{...je,activeAppRecordId:Ze.id,activeAppRecord:Ze.value,appRecords:ii.value}},api:o4(kc)});var fl=Y.__VUE_DEVTOOLS_KIT_CONTEXT__;L();og(cg());var Cc,xc;(xc=(Cc=Y).__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__)!=null||(Cc.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__={id:0,appIds:new Set});L();function s4(e){je.highPerfModeEnabled=e??!je.highPerfModeEnabled,!e&&Ze.value&&gf(Ze.value.app)}L();L();L();function c4(e){je.devtoolsClientDetected={...je.devtoolsClientDetected,...e};const t=Object.values(je.devtoolsClientDetected).some(Boolean);s4(!t)}var Sc,Tc;(Tc=(Sc=Y).__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__)!=null||(Sc.__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__=c4);L();L();L();L();L();L();L();var u4=class{constructor(){this.keyToValue=new Map,this.valueToKey=new Map}set(e,t){this.keyToValue.set(e,t),this.valueToKey.set(t,e)}getByKey(e){return this.keyToValue.get(e)}getByValue(e){return this.valueToKey.get(e)}clear(){this.keyToValue.clear(),this.valueToKey.clear()}},Ef=class{constructor(e){this.generateIdentifier=e,this.kv=new u4}register(e,t){this.kv.getByValue(e)||(t||(t=this.generateIdentifier(e)),this.kv.set(t,e))}clear(){this.kv.clear()}getIdentifier(e){return this.kv.getByValue(e)}getValue(e){return this.kv.getByKey(e)}},d4=class extends Ef{constructor(){super(e=>e.name),this.classToAllowedProps=new Map}register(e,t){typeof t=="object"?(t.allowProps&&this.classToAllowedProps.set(e,t.allowProps),super.register(e,t.identifier)):super.register(e,t)}getAllowedProps(e){return this.classToAllowedProps.get(e)}};L();L();function f4(e){if("values"in Object)return Object.values(e);const t=[];for(const n in e)e.hasOwnProperty(n)&&t.push(e[n]);return t}function h4(e,t){const n=f4(e);if("find"in n)return n.find(t);const l=n;for(let r=0;rt(l,n))}function Lr(e,t){return e.indexOf(t)!==-1}function Lc(e,t){for(let n=0;nt.isApplicable(e))}findByName(e){return this.transfomers[e]}};L();L();var v4=e=>Object.prototype.toString.call(e).slice(8,-1),_f=e=>typeof e>"u",m4=e=>e===null,zl=e=>typeof e!="object"||e===null||e===Object.prototype?!1:Object.getPrototypeOf(e)===null?!0:Object.getPrototypeOf(e)===Object.prototype,ua=e=>zl(e)&&Object.keys(e).length===0,pn=e=>Array.isArray(e),g4=e=>typeof e=="string",E4=e=>typeof e=="number"&&!isNaN(e),_4=e=>typeof e=="boolean",b4=e=>e instanceof RegExp,$l=e=>e instanceof Map,Ul=e=>e instanceof Set,bf=e=>v4(e)==="Symbol",y4=e=>e instanceof Date&&!isNaN(e.valueOf()),A4=e=>e instanceof Error,Oc=e=>typeof e=="number"&&isNaN(e),k4=e=>_4(e)||m4(e)||_f(e)||E4(e)||g4(e)||bf(e),w4=e=>typeof e=="bigint",B4=e=>e===1/0||e===-1/0,C4=e=>ArrayBuffer.isView(e)&&!(e instanceof DataView),x4=e=>e instanceof URL;L();var yf=e=>e.replace(/\./g,"\\."),Ri=e=>e.map(String).map(yf).join("."),Dl=e=>{const t=[];let n="";for(let r=0;rnull,()=>{}),Nt(w4,"bigint",e=>e.toString(),e=>typeof BigInt<"u"?BigInt(e):(console.error("Please add a BigInt polyfill."),e)),Nt(y4,"Date",e=>e.toISOString(),e=>new Date(e)),Nt(A4,"Error",(e,t)=>{const n={name:e.name,message:e.message};return t.allowedErrorProps.forEach(l=>{n[l]=e[l]}),n},(e,t)=>{const n=new Error(e.message);return n.name=e.name,n.stack=e.stack,t.allowedErrorProps.forEach(l=>{n[l]=e[l]}),n}),Nt(b4,"regexp",e=>""+e,e=>{const t=e.slice(1,e.lastIndexOf("/")),n=e.slice(e.lastIndexOf("/")+1);return new RegExp(t,n)}),Nt(Ul,"set",e=>[...e.values()],e=>new Set(e)),Nt($l,"map",e=>[...e.entries()],e=>new Map(e)),Nt(e=>Oc(e)||B4(e),"number",e=>Oc(e)?"NaN":e>0?"Infinity":"-Infinity",Number),Nt(e=>e===0&&1/e===-1/0,"number",()=>"-0",Number),Nt(x4,"URL",e=>e.toString(),e=>new URL(e))];function ai(e,t,n,l){return{isApplicable:e,annotation:t,transform:n,untransform:l}}var kf=ai((e,t)=>bf(e)?!!t.symbolRegistry.getIdentifier(e):!1,(e,t)=>["symbol",t.symbolRegistry.getIdentifier(e)],e=>e.description,(e,t,n)=>{const l=n.symbolRegistry.getValue(t[1]);if(!l)throw new Error("Trying to deserialize unknown symbol");return l}),S4=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,Uint8ClampedArray].reduce((e,t)=>(e[t.name]=t,e),{}),wf=ai(C4,e=>["typed-array",e.constructor.name],e=>[...e],(e,t)=>{const n=S4[t[1]];if(!n)throw new Error("Trying to deserialize unknown typed array");return new n(e)});function Bf(e,t){return e!=null&&e.constructor?!!t.classRegistry.getIdentifier(e.constructor):!1}var Cf=ai(Bf,(e,t)=>["class",t.classRegistry.getIdentifier(e.constructor)],(e,t)=>{const n=t.classRegistry.getAllowedProps(e.constructor);if(!n)return{...e};const l={};return n.forEach(r=>{l[r]=e[r]}),l},(e,t,n)=>{const l=n.classRegistry.getValue(t[1]);if(!l)throw new Error("Trying to deserialize unknown class - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564");return Object.assign(Object.create(l.prototype),e)}),xf=ai((e,t)=>!!t.customTransformerRegistry.findApplicable(e),(e,t)=>["custom",t.customTransformerRegistry.findApplicable(e).name],(e,t)=>t.customTransformerRegistry.findApplicable(e).serialize(e),(e,t,n)=>{const l=n.customTransformerRegistry.findByName(t[1]);if(!l)throw new Error("Trying to deserialize unknown custom value");return l.deserialize(e)}),T4=[Cf,kf,xf,wf],Dc=(e,t)=>{const n=Lc(T4,r=>r.isApplicable(e,t));if(n)return{value:n.transform(e,t),type:n.annotation(e,t)};const l=Lc(Af,r=>r.isApplicable(e,t));if(l)return{value:l.transform(e,t),type:l.annotation}},Sf={};Af.forEach(e=>{Sf[e.annotation]=e});var L4=(e,t,n)=>{if(pn(t))switch(t[0]){case"symbol":return kf.untransform(e,t,n);case"class":return Cf.untransform(e,t,n);case"custom":return xf.untransform(e,t,n);case"typed-array":return wf.untransform(e,t,n);default:throw new Error("Unknown transformation: "+t)}else{const l=Sf[t];if(!l)throw new Error("Unknown transformation: "+t);return l.untransform(e,n)}};L();var $n=(e,t)=>{const n=e.keys();for(;t>0;)n.next(),t--;return n.next().value};function Tf(e){if(Lr(e,"__proto__"))throw new Error("__proto__ is not allowed as a property");if(Lr(e,"prototype"))throw new Error("prototype is not allowed as a property");if(Lr(e,"constructor"))throw new Error("constructor is not allowed as a property")}var O4=(e,t)=>{Tf(t);for(let n=0;n{if(Tf(t),t.length===0)return n(e);let l=e;for(let i=0;ifa(i,t,[...n,...Dl(a)]));return}const[l,r]=e;r&&al(r,(i,a)=>{fa(i,t,[...n,...Dl(a)])}),t(l,n)}function D4(e,t,n){return fa(t,(l,r)=>{e=da(e,r,i=>L4(i,l,n))}),e}function P4(e,t){function n(l,r){const i=O4(e,Dl(r));l.map(Dl).forEach(a=>{e=da(e,a,()=>i)})}if(pn(t)){const[l,r]=t;l.forEach(i=>{e=da(e,Dl(i),()=>e)}),r&&al(r,n)}else al(t,n);return e}var I4=(e,t)=>zl(e)||pn(e)||$l(e)||Ul(e)||Bf(e,t);function F4(e,t,n){const l=n.get(e);l?l.push(t):n.set(e,[t])}function R4(e,t){const n={};let l;return e.forEach(r=>{if(r.length<=1)return;t||(r=r.map(o=>o.map(String)).sort((o,c)=>o.length-c.length));const[i,...a]=r;i.length===0?l=a.map(Ri):n[Ri(i)]=a.map(Ri)}),l?ua(n)?[l]:[l,n]:ua(n)?void 0:n}var Lf=(e,t,n,l,r=[],i=[],a=new Map)=>{var o;const c=k4(e);if(!c){F4(e,r,t);const g=a.get(e);if(g)return l?{transformedValue:null}:g}if(!I4(e,n)){const g=Dc(e,n),v=g?{transformedValue:g.value,annotations:[g.type]}:{transformedValue:e};return c||a.set(e,v),v}if(Lr(i,e))return{transformedValue:null};const u=Dc(e,n),d=(o=u==null?void 0:u.value)!=null?o:e,f=pn(d)?[]:{},h={};al(d,(g,v)=>{if(v==="__proto__"||v==="constructor"||v==="prototype")throw new Error(`Detected property ${v}. This is a prototype pollution risk, please remove it from your object.`);const y=Lf(g,t,n,l,[...r,v],[...i,e],a);f[v]=y.transformedValue,pn(y.annotations)?h[v]=y.annotations:zl(y.annotations)&&al(y.annotations,(_,A)=>{h[yf(v)+"."+A]=_})});const p=ua(h)?{transformedValue:f,annotations:u?[u.type]:void 0}:{transformedValue:f,annotations:u?[u.type,h]:h};return c||a.set(e,p),p};L();L();function Of(e){return Object.prototype.toString.call(e).slice(8,-1)}function Pc(e){return Of(e)==="Array"}function M4(e){if(Of(e)!=="Object")return!1;const t=Object.getPrototypeOf(e);return!!t&&t.constructor===Object&&t===Object.prototype}function V4(e,t,n,l,r){const i={}.propertyIsEnumerable.call(l,t)?"enumerable":"nonenumerable";i==="enumerable"&&(e[t]=n),r&&i==="nonenumerable"&&Object.defineProperty(e,t,{value:n,enumerable:!1,writable:!0,configurable:!0})}function ha(e,t={}){if(Pc(e))return e.map(r=>ha(r,t));if(!M4(e))return e;const n=Object.getOwnPropertyNames(e),l=Object.getOwnPropertySymbols(e);return[...n,...l].reduce((r,i)=>{if(Pc(t.props)&&!t.props.includes(i))return r;const a=e[i],o=ha(a,t);return V4(r,i,o,e,t.nonenumerable),r},{})}var Te=class{constructor({dedupe:e=!1}={}){this.classRegistry=new d4,this.symbolRegistry=new Ef(t=>{var n;return(n=t.description)!=null?n:""}),this.customTransformerRegistry=new p4,this.allowedErrorProps=[],this.dedupe=e}serialize(e){const t=new Map,n=Lf(e,t,this,this.dedupe),l={json:n.transformedValue};n.annotations&&(l.meta={...l.meta,values:n.annotations});const r=R4(t,this.dedupe);return r&&(l.meta={...l.meta,referentialEqualities:r}),l}deserialize(e){const{json:t,meta:n}=e;let l=ha(t);return n!=null&&n.values&&(l=D4(l,n.values,this)),n!=null&&n.referentialEqualities&&(l=P4(l,n.referentialEqualities)),l}stringify(e){return JSON.stringify(this.serialize(e))}parse(e){return this.deserialize(JSON.parse(e))}registerClass(e,t){this.classRegistry.register(e,t)}registerSymbol(e,t){this.symbolRegistry.register(e,t)}registerCustom(e,t){this.customTransformerRegistry.register({name:t,...e})}allowErrorProps(...e){this.allowedErrorProps.push(...e)}};Te.defaultInstance=new Te;Te.serialize=Te.defaultInstance.serialize.bind(Te.defaultInstance);Te.deserialize=Te.defaultInstance.deserialize.bind(Te.defaultInstance);Te.stringify=Te.defaultInstance.stringify.bind(Te.defaultInstance);Te.parse=Te.defaultInstance.parse.bind(Te.defaultInstance);Te.registerClass=Te.defaultInstance.registerClass.bind(Te.defaultInstance);Te.registerSymbol=Te.defaultInstance.registerSymbol.bind(Te.defaultInstance);Te.registerCustom=Te.defaultInstance.registerCustom.bind(Te.defaultInstance);Te.allowErrorProps=Te.defaultInstance.allowErrorProps.bind(Te.defaultInstance);L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();L();var Ic,Fc;(Fc=(Ic=Y).__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__)!=null||(Ic.__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__=[]);var Rc,Mc;(Mc=(Rc=Y).__VUE_DEVTOOLS_KIT_RPC_CLIENT__)!=null||(Rc.__VUE_DEVTOOLS_KIT_RPC_CLIENT__=null);var Vc,Nc;(Nc=(Vc=Y).__VUE_DEVTOOLS_KIT_RPC_SERVER__)!=null||(Vc.__VUE_DEVTOOLS_KIT_RPC_SERVER__=null);var jc,Hc;(Hc=(jc=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__)!=null||(jc.__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__=null);var zc,$c;($c=(zc=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__)!=null||(zc.__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__=null);var Uc,Gc;(Gc=(Uc=Y).__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__)!=null||(Uc.__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__=null);L();L();L();L();L();L();L();const N4=JSON.parse('{"encrypt":{},"logo":"https://github-images.wenzhihuai.com/images/logo.png","author":{"name":"Zephery","url":"https://wenzhihuai.com/article/"},"repo":"https://github.com/Zephery/MyWebsite","docsDir":"docs","pure":true,"breadcrumb":false,"footer":"粤ICP备17092242号-1","displayFooter":true,"pageInfo":["Author","Category","Tag","Original","Word","ReadingTime"],"blog":{"intro":"/about-the-author/","sidebarDisplay":"mobile","medias":{"Zhihu":"https://www.zhihu.com/people/wen-zhi-huai-83","Github":"https://github.com/Zephery","Gitee":"https://gitee.com/zephery.com.cn","BiliBili":"https://space.bilibili.com/3118581","WechatPay":"https://wenzhihuai.com/donate"}},"locales":{"/":{"lang":"zh-CN","navbarLocales":{"langName":"简体中文","selectLangAriaLabel":"选择语言"},"metaLocales":{"author":"作者","date":"写作日期","origin":"原创","views":"访问量","category":"分类","tag":"标签","readingTime":"阅读时间","words":"字数","toc":"此页内容","prev":"上一页","next":"下一页","lastUpdated":"上次编辑于","contributors":"贡献者","editLink":"编辑此页","print":"打印"},"blogLocales":{"article":"文章","articleList":"文章列表","category":"分类","tag":"标签","timeline":"时间轴","timelineTitle":"昨日不在","all":"全部","intro":"个人介绍","star":"星标","empty":"$text 为空"},"paginationLocales":{"prev":"上一页","next":"下一页","navigate":"跳转到","action":"前往","errorText":"请输入 1 到 $page 之前的页码!"},"outlookLocales":{"themeColor":"主题色","darkmode":"外观","fullscreen":"全屏"},"routeLocales":{"skipToContent":"跳至主要內容","notFoundTitle":"页面不存在","notFoundMsg":["这里什么也没有","我们是怎么来到这儿的?","这 是 四 零 四 !","看起来你访问了一个失效的链接"],"back":"返回上一页","home":"带我回家"},"navbar":[{"text":"主页","icon":"home","link":"/"},{"text":"好玩的","icon":"interesting","link":"/interesting/"},{"text":"Java","icon":"java","link":"/java/"},{"text":"中间件","icon":"middleware","link":"/middleware/"},{"text":"数据库","icon":"database","link":"/database/"},{"text":"大数据","icon":"bigdata","link":"/bigdata/"},{"text":"Kubernetes","icon":"Kubernetes","link":"/kubernetes/"},{"text":"系统设计","icon":"system-design","link":"/system-design/"},{"text":"股票预测","icon":"stock","link":"/stock/"},{"text":"捐赠","icon":"donate","link":"/donate/"},{"text":"网站相关","icon":"about","children":[{"text":"关于作者","icon":"zuozhe","link":"/about-the-author/personal-life/wewe.md"},{"text":"友链","icon":"link","link":"/link/main.md"}]}],"sidebar":{"/":[{"text":"首页","icon":"home","collapsible":false,"children":"structure"}],"/interesting/":[{"text":"好玩的","icon":"interesting","collapsible":false,"children":"structure"}],"/java/":[{"text":"Java","icon":"java","collapsible":false,"children":"structure"}],"/database/":[{"text":"MySQL","prefix":"mysql/","icon":"mysql","collapsible":false,"children":"structure"},{"text":"Redis","prefix":"redis/","icon":"redis","collapsible":false,"children":"structure"},{"text":"Elasticsearch","prefix":"elasticsearch/","icon":"elasticsearch","collapsible":false,"children":"structure"},{"text":"MongoDB","prefix":"mongodb/","icon":"mongodb","collapsible":false,"children":"structure"}],"/link/":[{"text":"友链","icon":"link","link":"main","collapsible":false}],"/bigdata/":[{"text":"Spark","prefix":"spark/","icon":"bigdata","collapsible":false,"children":"structure"}],"/middleware/":[{"text":"Kafka","prefix":"kafka/","icon":"kafka","collapsible":false,"children":"structure"},{"text":"Zookeeper","prefix":"zookeeper/","icon":"zookeeper","collapsible":false,"children":"structure"},{"text":"Canal","prefix":"canal/","icon":"canal","collapsible":false,"children":"structure"}],"/kubernetes/":[{"text":"Kubernetes","icon":"kubernetes","collapsible":false,"children":"structure"}],"/books/":[{"text":"计算机基础","link":"cs-basics","icon":"computer"},{"text":"数据库","link":"database","icon":"database"},{"text":"搜索引擎","link":"search-engine","icon":"search"},{"text":"Java","link":"java","icon":"java"},{"text":"软件质量","link":"software-quality","icon":"highavailable"},{"text":"分布式","link":"distributed-system","icon":"distributed-network"}],"/stock/":[{"text":"股票预测","link":"README","icon":"README"}],"/system-design/":[{"text":"系统设计","icon":"system-design","collapsible":false,"children":"structure"}],"/donate/":[{"text":"微信支付","link":"README","icon":"README"}],"/life/":[{"text":"生活","icon":"life","collapsible":false,"children":"structure"}],"/about-the-author/":[{"text":"个人生活","icon":"experience","prefix":"personal-life/","collapsible":false,"children":"structure"},{"text":"作品","icon":"works","prefix":"works/","collapsible":false,"children":"structure"},{"text":"杂谈","icon":"chat","prefix":"talking/","collapsible":false,"children":"structure"}],"/high-quality-technical-articles/":[{"text":"练级攻略","icon":"et-performance","prefix":"advanced-programmer/","collapsible":false,"children":["programmer-quickly-learn-new-technology","the-growth-strategy-of-the-technological-giant","ten-years-of-dachang-growth-road","meituan-three-year-summary-lesson-10","seven-tips-for-becoming-an-advanced-programmer","20-bad-habits-of-bad-programmers","thinking-about-technology-and-business-after-five-years-of-work"]},{"text":"个人经历","icon":"experience","prefix":"personal-experience/","collapsible":false,"children":["four-year-work-in-tencent-summary","two-years-of-back-end-develop--experience-in-didi-and-toutiao","8-years-programmer-work-summary","huawei-od-275-days"]},{"text":"程序员","icon":"code","prefix":"programmer/","collapsible":false,"children":["how-do-programmers-publish-a-technical-book","efficient-book-publishing-and-practice-Zephery"]},{"text":"面试","icon":"interview","prefix":"interview/","collapsible":true,"children":["the-experience-of-get-offer-from-over-20-big-companies","the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer","technical-preliminary-preparation","screen-candidates-for-packaging","summary-of-spring-recruitment","my-personal-experience-in-2021","how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology","some-secrets-about-alibaba-interview"]},{"text":"工作","icon":"work","prefix":"work/","collapsible":true,"children":["get-into-work-mode-quickly-when-you-join-a-company","32-tips-improving-career","employee-performance"]}],"/zhuanlan/":["java-mian-shi-zhi-bei","back-end-interview-high-frequency-system-design-and-scenario-questions","handwritten-rpc-framework","source-code-reading"]}}}}'),j4=ee(N4),Df=()=>j4,Pf=Symbol(""),H4=()=>{const e=Se(Pf);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},z4=(e,t)=>{const{locales:n,...l}=e;return{...l,...n==null?void 0:n[t]}},$4=Bt({enhance({app:e}){const t=Df(),n=e._context.provides[Ja],l=k(()=>z4(t.value,n.routeLocale.value));e.provide(Pf,l),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return l.value}}})}}),U4=Object.freeze(Object.defineProperty({__proto__:null,default:$4},Symbol.toStringTag,{value:"Module"})),G4=/language-(shellscript|shell|bash|sh|zsh)/,K4=({delay:e=500,duration:t=2e3,locales:n,selector:l,showInMobile:r,ignoreSelector:i=[],transform:a})=>{const o=Qa("(max-width: 419px)"),c=k(()=>!o.value||r),u=Fn(n),d=Oe(),f=y=>{var A;if(y.hasAttribute("copy-code"))return;const _=document.createElement("button");_.type="button",_.classList.add("vp-copy-code-button"),_.setAttribute("aria-label",u.value.copy),_.setAttribute("data-copied",u.value.copied),(A=y.parentElement)==null||A.insertBefore(_,y),y.setAttribute("copy-code","")};pe(()=>[d.value.path,c.value],async()=>{document.body.classList.toggle("no-copy-code",!c.value),c.value&&(await Qt(),await no(e),document.querySelectorAll(l.join(",")).forEach(f))},{immediate:!0});const{copy:p}=Tv({legacy:!0}),g=new WeakMap,v=async(y,_,A)=>{const m=_.cloneNode(!0);i.length&&m.querySelectorAll(i.join(",")).forEach(V=>{V.remove()}),a&&a(m);let w=m.textContent||"";if(G4.test(y.className)&&(w=w.replace(/^ *(\$|>) /gm,"")),await p(w),t<=0)return;A.classList.add("copied"),clearTimeout(g.get(A));const F=setTimeout(()=>{A.classList.remove("copied"),A.blur(),g.delete(A)},t);g.set(A,F)};Ie("click",y=>{const _=y.target;if(c.value&&_.matches('div[class*="language-"] > button.vp-copy-code-button')){const A=_.parentElement,m=_.nextElementSibling;if(!A||!m)return;v(A,m,_)}})};var q4=[],W4={"/":{copy:"复制代码",copied:"已复制"}},J4=['[vp-content] div[class*="language-"] pre'];const Y4=Bt({setup:()=>{K4({selector:J4,ignoreSelector:q4,locales:W4,duration:2e3,delay:500,showInMobile:!1})}}),Z4=Object.freeze(Object.defineProperty({__proto__:null,default:Y4},Symbol.toStringTag,{value:"Module"})),Q4=(e,t)=>{const n=Fn(t),l=ve(),r=Oe(),i=k(()=>!!l.value.copy||l.value.copy!==!1&&e.global),a=k(()=>nr(l.value.copy)?l.value.copy:null),o=k(()=>{var v;return((v=a.value)==null?void 0:v.disableCopy)??e.disableCopy??!1}),c=k(()=>{var v;return i.value?((v=a.value)==null?void 0:v.disableSelection)??e.disableSelection??!1:!1}),u=k(()=>{var v;return i.value?((v=a.value)==null?void 0:v.maxLength)??e.maxLength??0:0}),d=k(()=>{var v;return((v=a.value)==null?void 0:v.triggerLength)??e.triggerLength??100}),f=v=>v?`${Zr(gn(v)?v:`https://${v}`)}${r.value.path}`:window.location.href,h=(v,y)=>{const{author:_,license:A,link:m}=n.value;return[v?_.replace(":author",v):"",y?A.replace(":license",y):"",m.replace(":link",f(e.canonical))].filter(w=>w).join(` +`)},p=()=>{if(ke(r.value.copyright))return r.value.copyright.replace(":link",f());const{author:v,license:y}=r.value.copyright??{};return h(v??e.author,y??e.license)},g=v=>{const y=getSelection();if(y){const _=y.getRangeAt(0);if(i.value){const A=_.toString().length;if(o.value||u.value&&A>u.value){v.preventDefault();return}if(A>=d.value){v.preventDefault();const m=p(),w=document.createElement("div");w.appendChild(y.getRangeAt(0).cloneContents()),v.clipboardData&&(v.clipboardData.setData("text/html",`${w.innerHTML}
`),v.clipboardData.setData("text/plain",`${y.getRangeAt(0).cloneContents().textContent||""} +------ +${m}`))}}}};we(()=>{const v=document.querySelector("#app");Ie(v,"copy",g),za(()=>{v.style.userSelect=c.value?"none":"auto"})})};var X4={"/":{author:"著作权归:author所有",license:"基于:license协议",link:"原文链接::link"}},e3={canonical:"https://wenzhihuai.com/",author:"wenzhihuai.com",license:"MIT",global:!0,disableCopy:!1,disableSelection:!1,triggerLength:100,maxLength:700},t3=Bt({setup:()=>{Q4(e3,X4)}});const n3=Object.freeze(Object.defineProperty({__proto__:null,default:t3},Symbol.toStringTag,{value:"Module"})),l3=Bt({setup(){Ie("beforeprint",()=>{document.querySelectorAll("details").forEach(e=>{e.open=!0})})}}),r3=Object.freeze(Object.defineProperty({__proto__:null,default:l3},Symbol.toStringTag,{value:"Module"})),i3=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"}));let a3={};const If=Symbol(""),o3=()=>Se(If),s3=e=>{e.provide(If,a3)},c3=(async()=>{}).constructor,u3=(e,t,n)=>t==="js"?c3("myChart",`let width,height,option,__echarts_config__; +{ +${e} +__echarts_config__={width,height,option}; +} +return __echarts_config__; +`)(n):Promise.resolve({option:JSON.parse(e)});var d3=H({name:"ECharts",props:{config:{type:String,required:!0},id:{type:String,required:!0},title:{type:String,default:""},type:{type:String,default:"json"}},setup(e){const t=o3(),n=ee(!0),l=$e();let r;return Ie("resize",Nd(()=>r==null?void 0:r.resize(),100)),we(()=>{Promise.all([C(()=>import("./index-AN989yVn.js"),[]),no(800)]).then(async([i])=>{var c;await((c=t.setup)==null?void 0:c.call(t)),r=i.init(l.value);const{option:a,...o}=await u3(pm(e.config),e.type,r);r.resize(o),r.setOption({...t.option,...a}),n.value=!1})}),cl(()=>{r==null||r.dispose()}),()=>[e.title?s("div",{class:"echarts-title"},decodeURIComponent(e.title)):null,s("div",{class:"echarts-wrapper"},[s("div",{ref:l,class:"echarts-container",id:e.id}),n.value?s(to,{class:"echarts-loading",height:360}):null])]}});const f3={enhance:({app:e})=>{e.component("ECharts",d3),s3(e)}},h3=Object.freeze(Object.defineProperty({__proto__:null,default:f3},Symbol.toStringTag,{value:"Module"})),p3=ee({}),Ff=Symbol(""),v3=()=>Se(Ff),m3=e=>{e.provide(Ff,p3)},g3='
',E3=e=>ke(e)?Array.from(document.querySelectorAll(e)):e.map(t=>Array.from(document.querySelectorAll(t))).flat(),Rf=e=>new Promise((t,n)=>{e.complete?t({type:"image",element:e,src:e.src,width:e.naturalWidth,height:e.naturalHeight,alt:e.alt,msrc:e.src}):(e.onload=()=>{t(Rf(e))},e.onerror=()=>{n()})}),_3=(e,{download:t=!0,fullscreen:n=!0}={})=>{e.on("uiRegister",()=>{if(e.ui.registerElement({name:"bulletsIndicator",className:"photo-swipe-bullets-indicator",appendTo:"wrapper",onInit:l=>{const r=[];let i=-1;for(let a=0;a{e.goTo(r.indexOf(c.target))},r.push(o),l.appendChild(o)}e.on("change",()=>{i>=0&&r[i].classList.remove("active"),r[e.currIndex].classList.add("active"),i=e.currIndex})}}),n){const{isSupported:l,toggle:r}=ti();l.value&&e.ui.registerElement({name:"fullscreen",order:7,isButton:!0,html:'',onClick:()=>{r()}})}t&&e.ui.registerElement({name:"download",order:8,isButton:!0,tagName:"a",html:{isCustomSVG:!0,inner:'',outlineID:"pswp__icn-download"},onInit:l=>{l.setAttribute("download",""),l.setAttribute("target","_blank"),l.setAttribute("rel","noopener"),e.on("change",()=>{l.setAttribute("href",e.currSlide.data.src)})}})})},b3=(e,{scrollToClose:t=!0,download:n=!0,fullscreen:l=!0,...r})=>C(async()=>{const{default:i}=await import("./photoswipe.esm-GXRgw7eJ.js");return{default:i}},[]).then(({default:i})=>{let a=null;const o=e.map(c=>({html:g3,element:c,msrc:c.src}));return e.forEach((c,u)=>{const d=()=>{a==null||a.destroy(),a=new i({preloaderDelay:0,showHideAnimationType:"zoom",...r,dataSource:o,index:u,...t?{closeOnVerticalDrag:!0,wheelToZoom:!1}:{}}),_3(a,{download:n,fullscreen:l}),a.addFilter("thumbEl",()=>c),a.addFilter("placeholderSrc",()=>c.src),a.init()};c.getAttribute("photo-swipe")||(c.style.cursor="zoom-in",c.addEventListener("click",()=>{d()}),c.addEventListener("keypress",({key:f})=>{f==="Enter"&&d()}),c.setAttribute("photo-swipe","")),Rf(c).then(f=>{o.splice(u,1,f),a==null||a.refreshSlideContent(u)})}),t?Ie("wheel",()=>{a==null||a.close()}):()=>{}}),y3=({selector:e,locales:t,delay:n=500,download:l=!0,fullscreen:r=!0,scrollToClose:i=!0})=>{const a=v3(),o=Fn(t),c=Oe(),u=ve();let d=null;const f=()=>{const{photoSwipe:h}=u.value;h!==!1&&Qt().then(()=>no(n)).then(async()=>{const p=ke(h)?h:e;d=await b3(E3(p),{...a.value,...o.value,download:l,fullscreen:r,scrollToClose:i})})};we(()=>{f(),pe(()=>[c.value.path,a.value],()=>{d==null||d(),f()})}),cl(()=>{d==null||d()})};var A3={"/":{closeTitle:"关闭",downloadTitle:"下载图片",fullscreenTitle:"切换全屏",zoomTitle:"缩放",arrowPrevTitle:"上一个 (左箭头)",arrowNextTitle:"下一个 (右箭头)"}};const k3="[vp-content] :not(a) > img:not([no-view])",w3=A3,B3=800,C3=!0,x3=!0,S3=!0;var T3=Bt({enhance:({app:e})=>{m3(e)},setup:()=>{y3({selector:k3,delay:B3,locales:w3,download:C3,fullscreen:x3,scrollToClose:S3})}});const L3=Object.freeze(Object.defineProperty({__proto__:null,default:T3},Symbol.toStringTag,{value:"Module"})),O3=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,D3=(e,t)=>t.some(n=>{if(ke(n))return n===e.key;const{key:l,ctrl:r=!1,shift:i=!1,alt:a=!1}=n;return l===e.key&&r===e.ctrlKey&&i===e.shiftKey&&a===e.altKey}),P3=/[^\x00-\x7F]/,I3=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),Kc=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),qc=(e,t)=>{const n=t.join(" "),l=I3(e);if(P3.test(e))return l.some(a=>n.toLowerCase().indexOf(a)>-1);const r=e.endsWith(" ");return new RegExp(l.map((a,o)=>l.length===o+1&&!r?`(?=.*\\b${Kc(a)})`:`(?=.*\\b${Kc(a)}\\b)`).join("")+".+","gi").test(n)},F3=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=l=>{e.value&&D3(l,t.value)&&!O3(l.target)&&(l.preventDefault(),e.value.focus())};we(()=>{document.addEventListener("keydown",n)}),Jr(()=>{document.removeEventListener("keydown",n)})},R3=[{title:"关于我",headers:[],path:"/about-the-author/",pathLocale:"/",extraFields:[]},{title:"Spark",headers:[],path:"/bigdata/",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/cloudnative/",pathLocale:"/",extraFields:[]},{title:"Database",headers:[],path:"/database/",pathLocale:"/",extraFields:[]},{title:"微信支付",headers:[],path:"/donate/",pathLocale:"/",extraFields:[]},{title:"好玩的",headers:[],path:"/interesting/",pathLocale:"/",extraFields:[]},{title:"微信公众号-chatgpt智能客服搭建",headers:[{level:2,title:"一、ChatGPT注册",slug:"一、chatgpt注册",link:"#一、chatgpt注册",children:[{level:3,title:"1.1 短信手机号申请",slug:"_1-1-短信手机号申请",link:"#_1-1-短信手机号申请",children:[]},{level:3,title:"1.2 云服务器申请",slug:"_1-2-云服务器申请",link:"#_1-2-云服务器申请",children:[]},{level:3,title:"1.3 ChatGPT注册",slug:"_1-3-chatgpt注册",link:"#_1-3-chatgpt注册",children:[]}]},{level:2,title:"二、搭建nginx服务器",slug:"二、搭建nginx服务器",link:"#二、搭建nginx服务器",children:[]},{level:2,title:"三、公众号开发",slug:"三、公众号开发",link:"#三、公众号开发",children:[{level:3,title:"3.1 微信云托管",slug:"_3-1-微信云托管",link:"#_3-1-微信云托管",children:[]},{level:3,title:"3.2 一个简单ChatGPT简单回复",slug:"_3-2-一个简单chatgpt简单回复",link:"#_3-2-一个简单chatgpt简单回复",children:[]},{level:3,title:"3.3 服务部署",slug:"_3-3-服务部署",link:"#_3-3-服务部署",children:[]}]},{level:2,title:"四、没有回复(超时回复问题)",slug:"四、没有回复-超时回复问题",link:"#四、没有回复-超时回复问题",children:[]},{level:2,title:"五、会话保存",slug:"五、会话保存",link:"#五、会话保存",children:[]},{level:2,title:"六、其他问题",slug:"六、其他问题",link:"#六、其他问题",children:[{level:3,title:"6.1 限频",slug:"_6-1-限频",link:"#_6-1-限频",children:[]},{level:3,title:"6.2 秘钥key被更新",slug:"_6-2-秘钥key被更新",link:"#_6-2-秘钥key被更新",children:[]},{level:3,title:"6.3 为啥和官网的回复不一样",slug:"_6-3-为啥和官网的回复不一样",link:"#_6-3-为啥和官网的回复不一样",children:[]},{level:3,title:"6.4 玄学挂掉",slug:"_6-4-玄学挂掉",link:"#_6-4-玄学挂掉",children:[]}]},{level:2,title:"最后",slug:"最后",link:"#最后",children:[]}],path:"/interesting/chatgpt.html",pathLocale:"/",extraFields:[]},{title:"Jetbrains Idea设置",headers:[{level:2,title:"一、类注释",slug:"一、类注释",link:"#一、类注释",children:[]},{level:2,title:"二、方法注释",slug:"二、方法注释",link:"#二、方法注释",children:[]},{level:2,title:"三、关闭Annotate With Git Blame",slug:"三、关闭annotate-with-git-blame",link:"#三、关闭annotate-with-git-blame",children:[]}],path:"/interesting/idea%E8%AE%BE%E7%BD%AE.html",pathLocale:"/",extraFields:[]},{title:"StarCraft Ⅱ 人工智能教程",headers:[],path:"/interesting/starcraft-ai.html",pathLocale:"/",extraFields:[]},{title:"Tesla api",headers:[{level:2,title:"一、特斯拉应用申请",slug:"一、特斯拉应用申请",link:"#一、特斯拉应用申请",children:[{level:3,title:"1.1 创建 Tesla 账户",slug:"_1-1-创建-tesla-账户",link:"#_1-1-创建-tesla-账户",children:[]},{level:3,title:"1.2 提交访问请求",slug:"_1-2-提交访问请求",link:"#_1-2-提交访问请求",children:[]},{level:3,title:"1.3 访问应用程序凭据",slug:"_1-3-访问应用程序凭据",link:"#_1-3-访问应用程序凭据",children:[]},{level:3,title:"1.4 开始 API 集成",slug:"_1-4-开始-api-集成",link:"#_1-4-开始-api-集成",children:[]},{level:3,title:"2.1 认识token",slug:"_2-1-认识token",link:"#_2-1-认识token",children:[]},{level:3,title:"2.2 获取第三方应用token",slug:"_2-2-获取第三方应用token",link:"#_2-2-获取第三方应用token",children:[]},{level:3,title:"2.3 验证域名归属",slug:"_2-3-验证域名归属",link:"#_2-3-验证域名归属",children:[]}]}],path:"/interesting/tesla.html",pathLocale:"/",extraFields:[]},{title:"一些不常用但很实用的命令",headers:[],path:"/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html",pathLocale:"/",extraFields:[]},{title:"基于OLAP做业务监控",headers:[{level:2,title:"一、两者的比较",slug:"一、两者的比较",link:"#一、两者的比较",children:[]},{level:2,title:"二、为什么要用OLAP数据库来做监控",slug:"二、为什么要用olap数据库来做监控",link:"#二、为什么要用olap数据库来做监控",children:[]}],path:"/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html",pathLocale:"/",extraFields:[]},{title:"小程序反编译",headers:[{level:3,title:"第一步:电脑端提取",slug:"第一步-电脑端提取",link:"#第一步-电脑端提取",children:[]},{level:3,title:"第二步:解密wxapkg包",slug:"第二步-解密wxapkg包",link:"#第二步-解密wxapkg包",children:[]},{level:3,title:"第三步:解包",slug:"第三步-解包",link:"#第三步-解包",children:[]}],path:"/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html",pathLocale:"/",extraFields:[]},{title:"广州图书馆借阅抓取",headers:[{level:3,title:"1.页面跳转过程",slug:"_1-页面跳转过程",link:"#_1-页面跳转过程",children:[]},{level:3,title:"2.处理方法",slug:"_2-处理方法",link:"#_2-处理方法",children:[]},{level:3,title:"3.代码",slug:"_3-代码",link:"#_3-代码",children:[]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html",pathLocale:"/",extraFields:[]},{title:"开发工具整理",headers:[{level:2,title:"ShadowsocksX",slug:"shadowsocksx",link:"#shadowsocksx",children:[]},{level:2,title:"IDEA激活",slug:"idea激活",link:"#idea激活",children:[]},{level:2,title:"",slug:"",link:"#",children:[]}],path:"/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html",pathLocale:"/",extraFields:[]},{title:"程序员副业探索之电商",headers:[{level:2,title:"一、小程序化妆品",slug:"一、小程序化妆品",link:"#一、小程序化妆品",children:[{level:3,title:"1.1 小程序准备(营业执照&微信支付&小程序appId)",slug:"_1-1-小程序准备-营业执照-微信支付-小程序appid",link:"#_1-1-小程序准备-营业执照-微信支付-小程序appid",children:[]},{level:3,title:"1.2 小程序开发",slug:"_1-2-小程序开发",link:"#_1-2-小程序开发",children:[]}]},{level:2,title:"二、拼多多电商",slug:"二、拼多多电商",link:"#二、拼多多电商",children:[]},{level:2,title:"三、跨境电商",slug:"三、跨境电商",link:"#三、跨境电商",children:[]},{level:2,title:"四、总结",slug:"四、总结",link:"#四、总结",children:[]}],path:"/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/interview/tiktok2023.html",pathLocale:"/",extraFields:[]},{title:"Java",headers:[],path:"/java/",pathLocale:"/",extraFields:[]},{title:"被挖矿攻击",headers:[],path:"/java/serverlog.html",pathLocale:"/",extraFields:[]},{title:"在 Spring 6 中使用虚拟线程",headers:[{level:2,title:"一、简介",slug:"一、简介",link:"#一、简介",children:[]},{level:2,title:"二、 虚拟线程与平台线程",slug:"二、-虚拟线程与平台线程",link:"#二、-虚拟线程与平台线程",children:[]},{level:2,title:"三、在Spring 6中使用虚拟线程",slug:"三、在spring-6中使用虚拟线程",link:"#三、在spring-6中使用虚拟线程",children:[]},{level:2,title:"四、性能比较",slug:"四、性能比较",link:"#四、性能比较",children:[]}],path:"/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html",pathLocale:"/",extraFields:[]},{title:"基于kubernetes的分布式限流",headers:[{level:2,title:"一、概念",slug:"一、概念",link:"#一、概念",children:[{level:3,title:"1.1 使用场景",slug:"_1-1-使用场景",link:"#_1-1-使用场景",children:[]},{level:3,title:"1.2 维度",slug:"_1-2-维度",link:"#_1-2-维度",children:[]},{level:3,title:"1.3 分布式限流",slug:"_1-3-分布式限流",link:"#_1-3-分布式限流",children:[]}]},{level:2,title:"二、分布式限流常用方案",slug:"二、分布式限流常用方案",link:"#二、分布式限流常用方案",children:[]},{level:2,title:"三、基于kubernetes的分布式限流",slug:"三、基于kubernetes的分布式限流",link:"#三、基于kubernetes的分布式限流",children:[{level:3,title:"3.1 kubernetes中的副本数",slug:"_3-1-kubernetes中的副本数",link:"#_3-1-kubernetes中的副本数",children:[]},{level:3,title:"3.2 rateLimiter的创建",slug:"_3-2-ratelimiter的创建",link:"#_3-2-ratelimiter的创建",children:[]},{level:3,title:"3.3 rateLimiter的获取",slug:"_3-3-ratelimiter的获取",link:"#_3-3-ratelimiter的获取",children:[]},{level:3,title:"3.4 filter里的判断",slug:"_3-4-filter里的判断",link:"#_3-4-filter里的判断",children:[]}]},{level:2,title:"四、性能压测",slug:"四、性能压测",link:"#四、性能压测",children:[]},{level:2,title:"五、其他问题",slug:"五、其他问题",link:"#五、其他问题",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html",pathLocale:"/",extraFields:[]},{title:"流程编排LiteFlow",headers:[{level:2,title:"背景",slug:"背景",link:"#背景",children:[]},{level:2,title:"一、为什么需要流程编排",slug:"一、为什么需要流程编排",link:"#一、为什么需要流程编排",children:[]},{level:2,title:"二、它可以解决什么问题",slug:"二、它可以解决什么问题",link:"#二、它可以解决什么问题",children:[]},{level:2,title:"三、LiteFlow改造之后",slug:"三、liteflow改造之后",link:"#三、liteflow改造之后",children:[]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]},{level:2,title:"文档&参考",slug:"文档-参考",link:"#文档-参考",children:[]}],path:"/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html",pathLocale:"/",extraFields:[]},{title:"高可用",headers:[{level:2,title:"流量控制",slug:"流量控制",link:"#流量控制",children:[]},{level:2,title:"熔断降级",slug:"熔断降级",link:"#熔断降级",children:[]},{level:2,title:"异地多活",slug:"异地多活",link:"#异地多活",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html",pathLocale:"/",extraFields:[]},{title:"高并发",headers:[{level:2,title:"负载均衡",slug:"负载均衡",link:"#负载均衡",children:[]},{level:2,title:"池化技术",slug:"池化技术",link:"#池化技术",children:[]},{level:2,title:"流量漏斗",slug:"流量漏斗",link:"#流量漏斗",children:[]}],path:"/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html",pathLocale:"/",extraFields:[]},{title:"高性能",headers:[{level:2,title:"缓存",slug:"缓存",link:"#缓存",children:[]},{level:2,title:"异步",slug:"异步",link:"#异步",children:[]},{level:2,title:"I/O(网络、数据、文件)",slug:"i-o-网络、数据、文件",link:"#i-o-网络、数据、文件",children:[]},{level:2,title:"分库分表",slug:"分库分表",link:"#分库分表",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E9%AB%98%E6%80%A7%E8%83%BD.html",pathLocale:"/",extraFields:[]},{title:"高性能高并发高可用的一些思考",headers:[{level:2,title:"异步",slug:"异步",link:"#异步",children:[]},{level:2,title:"Jmeter",slug:"jmeter",link:"#jmeter",children:[]},{level:2,title:"JProfiler",slug:"jprofiler",link:"#jprofiler",children:[]},{level:2,title:"火焰图",slug:"火焰图",link:"#火焰图",children:[]},{level:2,title:"缓存",slug:"缓存",link:"#缓存",children:[]},{level:2,title:"限流熔断",slug:"限流熔断",link:"#限流熔断",children:[]},{level:2,title:"Kafka",slug:"kafka",link:"#kafka",children:[]},{level:2,title:"序列化",slug:"序列化",link:"#序列化",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html",pathLocale:"/",extraFields:[]},{title:"Jenkins的一些笔记",headers:[{level:2,title:"一、在全局安全配置中",slug:"一、在全局安全配置中",link:"#一、在全局安全配置中",children:[]},{level:2,title:"二、获取TOKEN",slug:"二、获取token",link:"#二、获取token",children:[]},{level:2,title:"三、获取Jenkins-Crumb",slug:"三、获取jenkins-crumb",link:"#三、获取jenkins-crumb",children:[]},{level:2,title:"四、值得注意的事",slug:"四、值得注意的事",link:"#四、值得注意的事",children:[]}],path:"/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html",pathLocale:"/",extraFields:[]},{title:"Kubernetes容器日志收集",headers:[{level:2,title:"日志采集方式",slug:"日志采集方式",link:"#日志采集方式",children:[]},{level:2,title:"一、原生方式",slug:"一、原生方式",link:"#一、原生方式",children:[{level:3,title:"1.1 控制台stdout方式",slug:"_1-1-控制台stdout方式",link:"#_1-1-控制台stdout方式",children:[]},{level:3,title:"1.2 新版本的subPathExpr",slug:"_1-2-新版本的subpathexpr",link:"#_1-2-新版本的subpathexpr",children:[]}]},{level:2,title:"二、Daemonset方式",slug:"二、daemonset方式",link:"#二、daemonset方式",children:[]},{level:2,title:"三、Sidecar模式",slug:"三、sidecar模式",link:"#三、sidecar模式",children:[]},{level:2,title:"四、总结",slug:"四、总结",link:"#四、总结",children:[]}],path:"/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html",pathLocale:"/",extraFields:[]},{title:"目录",headers:[],path:"/kubernetes/",pathLocale:"/",extraFields:[]},{title:"Kubernetes之request 和 limit详解",headers:[],path:"/kubernetes/request_limit.html",pathLocale:"/",extraFields:[]},{title:"spark on k8s operator",headers:[],path:"/kubernetes/spark%20on%20k8s%20operator.html",pathLocale:"/",extraFields:[]},{title:"2017",headers:[],path:"/life/2017.html",pathLocale:"/",extraFields:[]},{title:"2018",headers:[{level:2,title:"心惊胆战的裸辞经历",slug:"心惊胆战的裸辞经历",link:"#心惊胆战的裸辞经历",children:[]},{level:2,title:"工作工作",slug:"工作工作",link:"#工作工作",children:[]},{level:2,title:"跑步跑步",slug:"跑步跑步",link:"#跑步跑步",children:[]},{level:2,title:"Others",slug:"others",link:"#others",children:[]},{level:2,title:"2019",slug:"_2019",link:"#_2019",children:[]}],path:"/life/2018.html",pathLocale:"/",extraFields:[]},{title:"2019",headers:[{level:2,title:"工作",slug:"工作",link:"#工作",children:[]},{level:2,title:"学习",slug:"学习",link:"#学习",children:[]},{level:2,title:"感情",slug:"感情",link:"#感情",children:[]},{level:2,title:"运动",slug:"运动",link:"#运动",children:[]},{level:2,title:"其他",slug:"其他",link:"#其他",children:[]},{level:2,title:"2020展望",slug:"_2020展望",link:"#_2020展望",children:[]}],path:"/life/2019.html",pathLocale:"/",extraFields:[]},{title:"友链地址",headers:[],path:"/link/main.html",pathLocale:"/",extraFields:[]},{title:"中间件",headers:[],path:"/middleware/",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/open-source-project/",pathLocale:"/",extraFields:[]},{title:"股票预测",headers:[],path:"/stock/",pathLocale:"/",extraFields:[]},{title:"赛力斯",headers:[],path:"/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html",pathLocale:"/",extraFields:[]},{title:"Feed系统设计",headers:[],path:"/system-design/feed.html",pathLocale:"/",extraFields:[]},{title:"2024-07-24",headers:[],path:"/about-the-author/personal-life/2024-07-24.html",pathLocale:"/",extraFields:[]},{title:"2024-11-09上海迪斯尼",headers:[],path:"/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html",pathLocale:"/",extraFields:[]},{title:"自我介绍",headers:[],path:"/about-the-author/personal-life/wewe.html",pathLocale:"/",extraFields:[]},{title:"小程序",headers:[],path:"/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html",pathLocale:"/",extraFields:[]},{title:"elastic spark",headers:[{level:2,title:"一、原生RDD支持",slug:"一、原生rdd支持",link:"#一、原生rdd支持",children:[{level:3,title:"1.1 基础配置",slug:"_1-1-基础配置",link:"#_1-1-基础配置",children:[]},{level:3,title:"1.2 读取es数据",slug:"_1-2-读取es数据",link:"#_1-2-读取es数据",children:[]},{level:3,title:"1.3 写数据",slug:"_1-3-写数据",link:"#_1-3-写数据",children:[]}]},{level:2,title:"二、Spark Streaming",slug:"二、spark-streaming",link:"#二、spark-streaming",children:[]},{level:2,title:"三、Spark SQL",slug:"三、spark-sql",link:"#三、spark-sql",children:[]},{level:2,title:"四、Spark Structure Streaming",slug:"四、spark-structure-streaming",link:"#四、spark-structure-streaming",children:[]},{level:2,title:"五、Spark on kubernetes Operator",slug:"五、spark-on-kubernetes-operator",link:"#五、spark-on-kubernetes-operator",children:[]}],path:"/bigdata/spark/elastic-spark.html",pathLocale:"/",extraFields:[]},{title:"【elasticsearch】源码debug",headers:[],path:"/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html",pathLocale:"/",extraFields:[]},{title:"【elasticsearch】搜索过程详解",headers:[{level:3,title:"SearchType",slug:"searchtype",link:"#searchtype",children:[]},{level:2,title:"一、es的分布式搜索过程",slug:"一、es的分布式搜索过程",link:"#一、es的分布式搜索过程",children:[{level:3,title:"1.1 搜索入口",slug:"_1-1-搜索入口",link:"#_1-1-搜索入口",children:[]}]},{level:2,title:"二、初步调用流程",slug:"二、初步调用流程",link:"#二、初步调用流程",children:[]},{level:2,title:"三、协调节点",slug:"三、协调节点",link:"#三、协调节点",children:[{level:3,title:"3.1 query阶段",slug:"_3-1-query阶段",link:"#_3-1-query阶段",children:[]},{level:3,title:"3.2 Fetch阶段",slug:"_3-2-fetch阶段",link:"#_3-2-fetch阶段",children:[]}]},{level:2,title:"四、数据节点",slug:"四、数据节点",link:"#四、数据节点",children:[{level:3,title:"4.1 执行query、fetch流程",slug:"_4-1-执行query、fetch流程",link:"#_4-1-执行query、fetch流程",children:[]}]},{level:2,title:"五、数据返回",slug:"五、数据返回",link:"#五、数据返回",children:[]},{level:2,title:"六、总结",slug:"六、总结",link:"#六、总结",children:[]},{level:2,title:"本文参考",slug:"本文参考",link:"#本文参考",children:[]}],path:"/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html",pathLocale:"/",extraFields:[]},{title:"基础",headers:[{level:2,title:"一、ElasticSearch基础",slug:"一、elasticsearch基础",link:"#一、elasticsearch基础",children:[]},{level:2,title:"二、ES的写入流程",slug:"二、es的写入流程",link:"#二、es的写入流程",children:[]},{level:2,title:"三、ES的更新和删除流程",slug:"三、es的更新和删除流程",link:"#三、es的更新和删除流程",children:[]},{level:2,title:"四、ES的搜索流程",slug:"四、es的搜索流程",link:"#四、es的搜索流程",children:[]},{level:2,title:"五、ES在高并发下如何保证读写一致性?",slug:"五、es在高并发下如何保证读写一致性",link:"#五、es在高并发下如何保证读写一致性",children:[]},{level:2,title:"六、ES集群如何选举Master节点",slug:"六、es集群如何选举master节点",link:"#六、es集群如何选举master节点",children:[]},{level:2,title:"七、建立索引阶段性能提升方法",slug:"七、建立索引阶段性能提升方法",link:"#七、建立索引阶段性能提升方法",children:[]},{level:2,title:"八、ES的深度分页与滚动搜索scroll",slug:"八、es的深度分页与滚动搜索scroll",link:"#八、es的深度分页与滚动搜索scroll",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/database/elasticsearch/%E5%9F%BA%E7%A1%80.html",pathLocale:"/",extraFields:[]},{title:"Mysql",headers:[],path:"/database/mysql/1mysql.html",pathLocale:"/",extraFields:[]},{title:"数据库缓存",headers:[],path:"/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html",pathLocale:"/",extraFields:[]},{title:"行锁,表锁,意向锁",headers:[],path:"/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html",pathLocale:"/",extraFields:[]},{title:"RedissonLock分布式锁源码分析",headers:[{level:2,title:"一、实现分布式锁的要求",slug:"一、实现分布式锁的要求",link:"#一、实现分布式锁的要求",children:[]},{level:2,title:"二、Redis实现分布式锁的常用命令",slug:"二、redis实现分布式锁的常用命令",link:"#二、redis实现分布式锁的常用命令",children:[]},{level:2,title:"三、常见写法",slug:"三、常见写法",link:"#三、常见写法",children:[]},{level:2,title:"四、Redisson",slug:"四、redisson",link:"#四、redisson",children:[{level:3,title:"4.1 测试例子",slug:"_4-1-测试例子",link:"#_4-1-测试例子",children:[]}]},{level:2,title:"五、RedissonLock源码分析",slug:"五、redissonlock源码分析",link:"#五、redissonlock源码分析",children:[{level:3,title:"5.1 使用到的全局变量",slug:"_5-1-使用到的全局变量",link:"#_5-1-使用到的全局变量",children:[]},{level:3,title:"5.2 加锁",slug:"_5-2-加锁",link:"#_5-2-加锁",children:[]},{level:3,title:"5.3 解锁",slug:"_5-3-解锁",link:"#_5-3-解锁",children:[]}]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html",pathLocale:"/",extraFields:[]},{title:"Redis缓存",headers:[{level:2,title:"一、概述",slug:"一、概述",link:"#一、概述",children:[{level:3,title:"1.1 缓存介绍",slug:"_1-1-缓存介绍",link:"#_1-1-缓存介绍",children:[]},{level:3,title:"1.2 本站缓存架构",slug:"_1-2-本站缓存架构",link:"#_1-2-本站缓存架构",children:[]}]},{level:2,title:"二、Mybatis缓存",slug:"二、mybatis缓存",link:"#二、mybatis缓存",children:[{level:3,title:"2.1 mybatis一级缓存",slug:"_2-1-mybatis一级缓存",link:"#_2-1-mybatis一级缓存",children:[]},{level:3,title:"2.2 mybatis二级缓存",slug:"_2-2-mybatis二级缓存",link:"#_2-2-mybatis二级缓存",children:[]},{level:3,title:"2.3 Mybatis在分布式环境下脏读问题",slug:"_2-3-mybatis在分布式环境下脏读问题",link:"#_2-3-mybatis在分布式环境下脏读问题",children:[]}]},{level:2,title:"三、Redis缓存",slug:"三、redis缓存",link:"#三、redis缓存",children:[{level:3,title:"3.1 Spring Cache",slug:"_3-1-spring-cache",link:"#_3-1-spring-cache",children:[]},{level:3,title:"3.2 引入包",slug:"_3-2-引入包",link:"#_3-2-引入包",children:[]},{level:3,title:"3.3 ApplicationContext.xml",slug:"_3-3-applicationcontext-xml",link:"#_3-3-applicationcontext-xml",children:[]},{level:3,title:"3.5 自定义KeyGenerator",slug:"_3-5-自定义keygenerator",link:"#_3-5-自定义keygenerator",children:[]},{level:3,title:"3.4 添加注解",slug:"_3-4-添加注解",link:"#_3-4-添加注解",children:[]},{level:3,title:"3.5 测试",slug:"_3-5-测试",link:"#_3-5-测试",children:[]},{level:3,title:"3.6 实验结果",slug:"_3-6-实验结果",link:"#_3-6-实验结果",children:[]},{level:3,title:"3.7 分页的数据怎么办",slug:"_3-7-分页的数据怎么办",link:"#_3-7-分页的数据怎么办",children:[]}]},{level:2,title:"四、如何解决脏读?",slug:"四、如何解决脏读",link:"#四、如何解决脏读",children:[]},{level:2,title:"五、题外话",slug:"五、题外话",link:"#五、题外话",children:[]}],path:"/database/redis/redis%E7%BC%93%E5%AD%98.html",pathLocale:"/",extraFields:[]},{title:"一致性hash算法和哈希槽",headers:[],path:"/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html",pathLocale:"/",extraFields:[]},{title:"mini主机",headers:[],path:"/interesting/mini%E4%B8%BB%E6%9C%BA/",pathLocale:"/",extraFields:[]},{title:"mac通过网线连接主机(fnOS)",headers:[{level:2,title:"一、mac端",slug:"一、mac端",link:"#一、mac端",children:[]},{level:2,title:"二、主机端",slug:"二、主机端",link:"#二、主机端",children:[]},{level:2,title:"三、最后",slug:"三、最后",link:"#三、最后",children:[]}],path:"/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html",pathLocale:"/",extraFields:[]},{title:"买了个mini主机",headers:[{level:2,title:"一、系统的安装",slug:"一、系统的安装",link:"#一、系统的安装",children:[]},{level:2,title:"二、连接wifi",slug:"二、连接wifi",link:"#二、连接wifi",children:[{level:3,title:"步骤 1: 检查网络接口",slug:"步骤-1-检查网络接口",link:"#步骤-1-检查网络接口",children:[]},{level:3,title:"步骤 2: 启用 Wi-Fi 网卡",slug:"步骤-2-启用-wi-fi-网卡",link:"#步骤-2-启用-wi-fi-网卡",children:[]},{level:3,title:"步骤 3: 扫描可用的 Wi-Fi 网络",slug:"步骤-3-扫描可用的-wi-fi-网络",link:"#步骤-3-扫描可用的-wi-fi-网络",children:[]},{level:3,title:"步骤 4: 连接到 Wi-Fi 网络",slug:"步骤-4-连接到-wi-fi-网络",link:"#步骤-4-连接到-wi-fi-网络",children:[]},{level:3,title:"步骤 5: 验证连接状态",slug:"步骤-5-验证连接状态",link:"#步骤-5-验证连接状态",children:[]}]},{level:2,title:"三、VNC远程连接",slug:"三、vnc远程连接",link:"#三、vnc远程连接",children:[]},{level:2,title:"四、docker 配置",slug:"四、docker-配置",link:"#四、docker-配置",children:[]},{level:2,title:"五、文件共享",slug:"五、文件共享",link:"#五、文件共享",children:[]}],path:"/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html",pathLocale:"/",extraFields:[]},{title:"1.历史与架构",headers:[{level:2,title:"1.建站故事与网站架构",slug:"_1-建站故事与网站架构",link:"#_1-建站故事与网站架构",children:[{level:3,title:"1.1建站过程",slug:"_1-1建站过程",link:"#_1-1建站过程",children:[]},{level:3,title:"1.2 网站整体技术架构",slug:"_1-2-网站整体技术架构",link:"#_1-2-网站整体技术架构",children:[]},{level:3,title:"1.3 日志系统",slug:"_1-3-日志系统",link:"#_1-3-日志系统",children:[]},{level:3,title:"1.4 【有点意思】自然语言处理",slug:"_1-4-【有点意思】自然语言处理",link:"#_1-4-【有点意思】自然语言处理",children:[]}]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html",pathLocale:"/",extraFields:[]},{title:"10.历时8年最终改版",headers:[{level:2,title:"一、博客的安装",slug:"一、博客的安装",link:"#一、博客的安装",children:[]},{level:2,title:"二、配置",slug:"二、配置",link:"#二、配置",children:[]},{level:2,title:"三、为文章增加评论",slug:"三、为文章增加评论",link:"#三、为文章增加评论",children:[]},{level:2,title:"四、博客的部署",slug:"四、博客的部署",link:"#四、博客的部署",children:[]},{level:2,title:"五、Github pages自定义域名",slug:"五、github-pages自定义域名",link:"#五、github-pages自定义域名",children:[]},{level:2,title:"六、Typora图床",slug:"六、typora图床",link:"#六、typora图床",children:[]},{level:2,title:"七、为自己的内容增加收入",slug:"七、为自己的内容增加收入",link:"#七、为自己的内容增加收入",children:[]},{level:2,title:"常见问题",slug:"常见问题",link:"#常见问题",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html",pathLocale:"/",extraFields:[]},{title:"2.Lucene的使用",headers:[{level:3,title:"Lucene的整体架构",slug:"lucene的整体架构",link:"#lucene的整体架构",children:[]},{level:3,title:"搜索引擎的几个重要概念:",slug:"搜索引擎的几个重要概念",link:"#搜索引擎的几个重要概念",children:[]},{level:3,title:"Lucene中的几个概念",slug:"lucene中的几个概念",link:"#lucene中的几个概念",children:[]},{level:3,title:"lucene在本网站的使用:",slug:"lucene在本网站的使用",link:"#lucene在本网站的使用",children:[]},{level:2,title:"一、搜索",slug:"一、搜索",link:"#一、搜索",children:[]},{level:2,title:"二、lucene自动补全",slug:"二、lucene自动补全",link:"#二、lucene自动补全",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html",pathLocale:"/",extraFields:[]},{title:"3.定时任务",headers:[{level:3,title:"一.特点:",slug:"一-特点",link:"#一-特点",children:[]},{level:3,title:"二.主要组成部分",slug:"二-主要组成部分",link:"#二-主要组成部分",children:[]},{level:3,title:"三、Quartz设计",slug:"三、quartz设计",link:"#三、quartz设计",children:[]},{level:3,title:"四、使用",slug:"四、使用",link:"#四、使用",children:[]},{level:2,title:"Spring的高级特性之定时任务",slug:"spring的高级特性之定时任务",link:"#spring的高级特性之定时任务",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html",pathLocale:"/",extraFields:[]},{title:"4.日志系统.md",headers:[{level:2,title:"1.网站代码安装",slug:"_1-网站代码安装",link:"#_1-网站代码安装",children:[]},{level:2,title:"2.根据API获取数据",slug:"_2-根据api获取数据",link:"#_2-根据api获取数据",children:[]},{level:2,title:"3.构建请求",slug:"_3-构建请求",link:"#_3-构建请求",children:[]},{level:2,title:"4.实际运用",slug:"_4-实际运用",link:"#_4-实际运用",children:[]},{level:2,title:"5.基本代码",slug:"_5-基本代码",link:"#_5-基本代码",children:[]},{level:2,title:"6.展示数据",slug:"_6-展示数据",link:"#_6-展示数据",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html",pathLocale:"/",extraFields:[]},{title:"5.小集群部署.md",headers:[{level:2,title:"1.1 Nginx的安装",slug:"_1-1-nginx的安装",link:"#_1-1-nginx的安装",children:[]},{level:2,title:"1.2 Nginx的配置",slug:"_1-2-nginx的配置",link:"#_1-2-nginx的配置",children:[]},{level:2,title:"测试:",slug:"测试",link:"#测试",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html",pathLocale:"/",extraFields:[]},{title:"6.数据库备份",headers:[{level:3,title:"2.1 创建所用的复制账号",slug:"_2-1-创建所用的复制账号",link:"#_2-1-创建所用的复制账号",children:[]},{level:3,title:"2.2 配置master",slug:"_2-2-配置master",link:"#_2-2-配置master",children:[]},{level:3,title:"2.3 配置slave",slug:"_2-3-配置slave",link:"#_2-3-配置slave",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html",pathLocale:"/",extraFields:[]},{title:"7.那些牛逼的插件",headers:[{level:2,title:"wowslider",slug:"wowslider",link:"#wowslider",children:[]},{level:2,title:"畅言",slug:"畅言",link:"#畅言",children:[]},{level:2,title:"Editor.md",slug:"editor-md",link:"#editor-md",children:[]},{level:2,title:"图表",slug:"图表",link:"#图表",children:[]},{level:2,title:"百度分享",slug:"百度分享",link:"#百度分享",children:[]},{level:2,title:"瀑布流",slug:"瀑布流",link:"#瀑布流",children:[]},{level:2,title:"天气插件",slug:"天气插件",link:"#天气插件",children:[]},{level:2,title:"标签云",slug:"标签云",link:"#标签云",children:[]}],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html",pathLocale:"/",extraFields:[]},{title:"8.基于贝叶斯的情感分析",headers:[],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html",pathLocale:"/",extraFields:[]},{title:"9.网站性能优化",headers:[],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html",pathLocale:"/",extraFields:[]},{title:"个人网站",headers:[],path:"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/",pathLocale:"/",extraFields:[]},{title:"JVM调优参数",headers:[{level:2,title:"一、堆大小设置",slug:"一、堆大小设置",link:"#一、堆大小设置",children:[{level:3,title:"典型设置:",slug:"典型设置",link:"#典型设置",children:[]}]},{level:2,title:"二、回收器选择",slug:"二、回收器选择",link:"#二、回收器选择",children:[{level:3,title:"2.1 吞吐量优先的并行收集器",slug:"_2-1-吞吐量优先的并行收集器",link:"#_2-1-吞吐量优先的并行收集器",children:[]},{level:3,title:"2.2 响应时间优先的并发收集器",slug:"_2-2-响应时间优先的并发收集器",link:"#_2-2-响应时间优先的并发收集器",children:[]}]},{level:2,title:"三、辅助信息",slug:"三、辅助信息",link:"#三、辅助信息",children:[{level:3,title:"3.1 堆设置",slug:"_3-1-堆设置",link:"#_3-1-堆设置",children:[]},{level:3,title:"3.2 收集器设置",slug:"_3-2-收集器设置",link:"#_3-2-收集器设置",children:[]},{level:3,title:"3.3 垃圾回收统计信息",slug:"_3-3-垃圾回收统计信息",link:"#_3-3-垃圾回收统计信息",children:[]},{level:3,title:"3.4 并行收集器设置",slug:"_3-4-并行收集器设置",link:"#_3-4-并行收集器设置",children:[]},{level:3,title:"3.5 并发收集器设置",slug:"_3-5-并发收集器设置",link:"#_3-5-并发收集器设置",children:[]}]},{level:2,title:"四、调优总结",slug:"四、调优总结",link:"#四、调优总结",children:[{level:3,title:"4.1 年轻代大小选择",slug:"_4-1-年轻代大小选择",link:"#_4-1-年轻代大小选择",children:[]},{level:3,title:"4.2 年老代大小选择",slug:"_4-2-年老代大小选择",link:"#_4-2-年老代大小选择",children:[]},{level:3,title:"4.3 较小堆引起的碎片问题",slug:"_4-3-较小堆引起的碎片问题",link:"#_4-3-较小堆引起的碎片问题",children:[]}]}],path:"/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html",pathLocale:"/",extraFields:[]},{title:"Java内存模型(JMM)",headers:[{level:2,title:"1 CPU和内存的交互",slug:"_1-cpu和内存的交互",link:"#_1-cpu和内存的交互",children:[{level:3,title:"1.1.1 内存屏障(Memory Barrier)",slug:"_1-1-1-内存屏障-memory-barrier",link:"#_1-1-1-内存屏障-memory-barrier",children:[]}]},{level:2,title:"2. Java内存区域",slug:"_2-java内存区域",link:"#_2-java内存区域",children:[{level:3,title:"2.1 五大内存区域",slug:"_2-1-五大内存区域",link:"#_2-1-五大内存区域",children:[]}]},{level:2,title:"3.内存溢出",slug:"_3-内存溢出",link:"#_3-内存溢出",children:[]},{level:2,title:"4.GC简介",slug:"_4-gc简介",link:"#_4-gc简介",children:[]},{level:2,title:"4.1.为什么需要学习GC",slug:"_4-1-为什么需要学习gc",link:"#_4-1-为什么需要学习gc",children:[]},{level:2,title:"4.2 堆的回收区域",slug:"_4-2-堆的回收区域",link:"#_4-2-堆的回收区域",children:[]},{level:2,title:"5 判断对象是否存活算法",slug:"_5-判断对象是否存活算法",link:"#_5-判断对象是否存活算法",children:[]},{level:2,title:"5 垃圾收集算法",slug:"_5-垃圾收集算法",link:"#_5-垃圾收集算法",children:[{level:3,title:"5.1 三大垃圾收集算法",slug:"_5-1-三大垃圾收集算法",link:"#_5-1-三大垃圾收集算法",children:[]},{level:3,title:"5.2 枚举根节点算法",slug:"_5-2-枚举根节点算法",link:"#_5-2-枚举根节点算法",children:[]}]},{level:2,title:"6.垃圾收集器",slug:"_6-垃圾收集器",link:"#_6-垃圾收集器",children:[{level:3,title:"新生代收集器",slug:"新生代收集器",link:"#新生代收集器",children:[]},{level:3,title:"6.1 Serial",slug:"_6-1-serial",link:"#_6-1-serial",children:[]},{level:3,title:"6.2 ParNew收集器",slug:"_6-2-parnew收集器",link:"#_6-2-parnew收集器",children:[]},{level:3,title:"6.3 Parallel Scavenge",slug:"_6-3-parallel-scavenge",link:"#_6-3-parallel-scavenge",children:[]},{level:3,title:"老年代收集器",slug:"老年代收集器",link:"#老年代收集器",children:[]}]},{level:2,title:"6.4 Serial Old",slug:"_6-4-serial-old",link:"#_6-4-serial-old",children:[]},{level:2,title:"6.5 Parallel Old",slug:"_6-5-parallel-old",link:"#_6-5-parallel-old",children:[{level:3,title:"6.6 CMS",slug:"_6-6-cms",link:"#_6-6-cms",children:[]},{level:3,title:"6.7 G1收集器",slug:"_6-7-g1收集器",link:"#_6-7-g1收集器",children:[]}]},{level:2,title:"7 Minor GC、Major GC、FULL GC、mixed gc",slug:"_7-minor-gc、major-gc、full-gc、mixed-gc",link:"#_7-minor-gc、major-gc、full-gc、mixed-gc",children:[{level:3,title:"7.1 Minor GC",slug:"_7-1-minor-gc",link:"#_7-1-minor-gc",children:[]},{level:3,title:"7.2 Major GC",slug:"_7-2-major-gc",link:"#_7-2-major-gc",children:[]},{level:3,title:"7.3 Full GC",slug:"_7-3-full-gc",link:"#_7-3-full-gc",children:[]},{level:3,title:"7.4 mixed GC【g1特有】",slug:"_7-4-mixed-gc【g1特有】",link:"#_7-4-mixed-gc【g1特有】",children:[]}]},{level:2,title:"8 查看GC日志",slug:"_8-查看gc日志",link:"#_8-查看gc日志",children:[{level:3,title:"8.3 自带的jconsole工具、jstat命令",slug:"_8-3-自带的jconsole工具、jstat命令",link:"#_8-3-自带的jconsole工具、jstat命令",children:[]}]},{level:2,title:"几个疑问",slug:"几个疑问",link:"#几个疑问",children:[{level:3,title:"1.GC是怎么判断对象是被标记的",slug:"_1-gc是怎么判断对象是被标记的",link:"#_1-gc是怎么判断对象是被标记的",children:[]},{level:3,title:"2.什么时候触发GC",slug:"_2-什么时候触发gc",link:"#_2-什么时候触发gc",children:[]},{level:3,title:"3.cms收集器是否会扫描年轻代",slug:"_3-cms收集器是否会扫描年轻代",link:"#_3-cms收集器是否会扫描年轻代",children:[]},{level:3,title:"4.什么是空间分配担保",slug:"_4-什么是空间分配担保",link:"#_4-什么是空间分配担保",children:[]},{level:3,title:"5.为什么复制算法要分两个Survivor,而不直接移到老年代",slug:"_5-为什么复制算法要分两个survivor-而不直接移到老年代",link:"#_5-为什么复制算法要分两个survivor-而不直接移到老年代",children:[]},{level:3,title:"6.各个版本的JVM使用的垃圾收集器是怎么样的",slug:"_6-各个版本的jvm使用的垃圾收集器是怎么样的",link:"#_6-各个版本的jvm使用的垃圾收集器是怎么样的",children:[]},{level:3,title:"7 stop the world具体是什么,有没有办法避免",slug:"_7-stop-the-world具体是什么-有没有办法避免",link:"#_7-stop-the-world具体是什么-有没有办法避免",children:[]},{level:3,title:"8 新生代什么样的情况会晋升为老年代",slug:"_8-新生代什么样的情况会晋升为老年代",link:"#_8-新生代什么样的情况会晋升为老年代",children:[]},{level:3,title:"8.怎么理解g1,适用于什么场景",slug:"_8-怎么理解g1-适用于什么场景",link:"#_8-怎么理解g1-适用于什么场景",children:[]}]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html",pathLocale:"/",extraFields:[]},{title:"JVM",headers:[],path:"/java/JVM/",pathLocale:"/",extraFields:[]},{title:"CMS",headers:[],path:"/java/JVM/cms.html",pathLocale:"/",extraFields:[]},{title:"G1",headers:[{level:2,title:"G1的内存模型和分代策略",slug:"g1的内存模型和分代策略",link:"#g1的内存模型和分代策略",children:[{level:3,title:"G1收集器相关参数",slug:"g1收集器相关参数",link:"#g1收集器相关参数",children:[]},{level:3,title:"G1分区(Region)机制和分代策略",slug:"g1分区-region-机制和分代策略",link:"#g1分区-region-机制和分代策略",children:[]},{level:3,title:"G1 卡片(Card)标记机制",slug:"g1-卡片-card-标记机制",link:"#g1-卡片-card-标记机制",children:[]},{level:3,title:"G1 堆(Heap)空间调整机制",slug:"g1-堆-heap-空间调整机制",link:"#g1-堆-heap-空间调整机制",children:[]},{level:3,title:"本地分配缓冲 Local allocation buffer (Lab)",slug:"本地分配缓冲-local-allocation-buffer-lab",link:"#本地分配缓冲-local-allocation-buffer-lab",children:[]}]},{level:2,title:"Remember Set(RSet)和Collection Set(CSet)",slug:"remember-set-rset-和collection-set-cset",link:"#remember-set-rset-和collection-set-cset",children:[{level:3,title:"为什么需要RSet?",slug:"为什么需要rset",link:"#为什么需要rset",children:[]},{level:3,title:"RSet的实现机制",slug:"rset的实现机制",link:"#rset的实现机制",children:[]},{level:3,title:"Per Region Table(PRT)机制",slug:"per-region-table-prt-机制",link:"#per-region-table-prt-机制",children:[]},{level:3,title:"RSet记录的引用类型",slug:"rset记录的引用类型",link:"#rset记录的引用类型",children:[]},{level:3,title:"CSet(收集集合,Collection Set)",slug:"cset-收集集合-collection-set",link:"#cset-收集集合-collection-set",children:[]},{level:3,title:"RSet的维护",slug:"rset的维护",link:"#rset的维护",children:[]},{level:3,title:"屏障(Barrier)",slug:"屏障-barrier",link:"#屏障-barrier",children:[]},{level:3,title:"始快照算法(Snapshot at the Beginning,SATB)",slug:"始快照算法-snapshot-at-the-beginning-satb",link:"#始快照算法-snapshot-at-the-beginning-satb",children:[]},{level:3,title:"并发优化线程 (Concurrence Refinement Threads)",slug:"并发优化线程-concurrence-refinement-threads",link:"#并发优化线程-concurrence-refinement-threads",children:[]}]},{level:2,title:"G1的垃圾回收流程",slug:"g1的垃圾回收流程",link:"#g1的垃圾回收流程",children:[{level:3,title:"G1的GC模式",slug:"g1的gc模式",link:"#g1的gc模式",children:[]},{level:3,title:"GC 工作线程数(-XX:ParallelGCThreads)",slug:"gc-工作线程数-xx-parallelgcthreads",link:"#gc-工作线程数-xx-parallelgcthreads",children:[]},{level:3,title:"Young GC核心流程",slug:"young-gc核心流程",link:"#young-gc核心流程",children:[]},{level:3,title:"Mixed GC核心流程——混合收集周期(Mixed GC Cycle)",slug:"mixed-gc核心流程——混合收集周期-mixed-gc-cycle",link:"#mixed-gc核心流程——混合收集周期-mixed-gc-cycle",children:[]},{level:3,title:"并发标记周期 (Concurrent Marking Cycle)",slug:"并发标记周期-concurrent-marking-cycle",link:"#并发标记周期-concurrent-marking-cycle",children:[]},{level:3,title:"初始标记(Initial Mark,STW)",slug:"初始标记-initial-mark-stw",link:"#初始标记-initial-mark-stw",children:[]},{level:3,title:"并发标记(Concurrent Mark)",slug:"并发标记-concurrent-mark",link:"#并发标记-concurrent-mark",children:[]},{level:3,title:"最终标记(Final Mark)",slug:"最终标记-final-mark",link:"#最终标记-final-mark",children:[]},{level:3,title:"筛选回收(Clean Up)",slug:"筛选回收-clean-up",link:"#筛选回收-clean-up",children:[]},{level:3,title:"混合回收周期(Mixed GC Cycle)",slug:"混合回收周期-mixed-gc-cycle",link:"#混合回收周期-mixed-gc-cycle",children:[]},{level:3,title:"Young GC、Mixed GC 和 Full GC的触发时机",slug:"young-gc、mixed-gc-和-full-gc的触发时机",link:"#young-gc、mixed-gc-和-full-gc的触发时机",children:[]}]},{level:2,title:"JDK的G1垃圾回收器和CMS有什么区别",slug:"jdk的g1垃圾回收器和cms有什么区别",link:"#jdk的g1垃圾回收器和cms有什么区别",children:[]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/java/JVM/g1.html",pathLocale:"/",extraFields:[]},{title:"JVM(java虚拟机)",headers:[{level:2,title:"一、了解JVM",slug:"一、了解jvm",link:"#一、了解jvm",children:[]},{level:2,title:"二、运行时数据区",slug:"二、运行时数据区",link:"#二、运行时数据区",children:[]},{level:2,title:"三、类加载机制",slug:"三、类加载机制",link:"#三、类加载机制",children:[{level:3,title:"1、类的生命周期(7个)",slug:"_1、类的生命周期-7个",link:"#_1、类的生命周期-7个",children:[]},{level:3,title:"2、类加载的五个过程",slug:"_2、类加载的五个过程",link:"#_2、类加载的五个过程",children:[]},{level:3,title:"3、类加载器",slug:"_3、类加载器",link:"#_3、类加载器",children:[]},{level:3,title:"4、类加载机制(双亲委派)",slug:"_4、类加载机制-双亲委派",link:"#_4、类加载机制-双亲委派",children:[]}]},{level:2,title:"四、垃圾回收",slug:"四、垃圾回收",link:"#四、垃圾回收",children:[{level:3,title:"1、判断对象已死",slug:"_1、判断对象已死",link:"#_1、判断对象已死",children:[]},{level:3,title:"2、垃圾收集算法",slug:"_2、垃圾收集算法",link:"#_2、垃圾收集算法",children:[]},{level:3,title:"3、垃圾收集器",slug:"_3、垃圾收集器",link:"#_3、垃圾收集器",children:[]},{level:3,title:"4、JVM内存分代机制",slug:"_4、jvm内存分代机制",link:"#_4、jvm内存分代机制",children:[]},{level:3,title:"5、Minor GC、Major GC、Full GC之间的区别",slug:"_5、minor-gc、major-gc、full-gc之间的区别",link:"#_5、minor-gc、major-gc、full-gc之间的区别",children:[]},{level:3,title:"6、Minor GC、Major GC、Full GC触发条件",slug:"_6、minor-gc、major-gc、full-gc触发条件",link:"#_6、minor-gc、major-gc、full-gc触发条件",children:[]}]}],path:"/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html",pathLocale:"/",extraFields:[]},{title:"ZGC",headers:[{level:2,title:"1 内存多重映射",slug:"_1-内存多重映射",link:"#_1-内存多重映射",children:[]},{level:2,title:"2 染色指针",slug:"_2-染色指针",link:"#_2-染色指针",children:[{level:3,title:"2.1 三色标记回顾",slug:"_2-1-三色标记回顾",link:"#_2-1-三色标记回顾",children:[]},{level:3,title:"2.2 染色指针",slug:"_2-2-染色指针",link:"#_2-2-染色指针",children:[]}]},{level:2,title:"3 内存布局",slug:"_3-内存布局",link:"#_3-内存布局",children:[]},{level:2,title:"4 读屏障",slug:"_4-读屏障",link:"#_4-读屏障",children:[]},{level:2,title:"5 GC 过程",slug:"_5-gc-过程",link:"#_5-gc-过程",children:[{level:3,title:"5.1 初始标记",slug:"_5-1-初始标记",link:"#_5-1-初始标记",children:[]},{level:3,title:"5.2 并发标记",slug:"_5-2-并发标记",link:"#_5-2-并发标记",children:[]},{level:3,title:"5.3 再标记",slug:"_5-3-再标记",link:"#_5-3-再标记",children:[]},{level:3,title:"5.4 初始转移",slug:"_5-4-初始转移",link:"#_5-4-初始转移",children:[]},{level:3,title:"5.5 并发转移",slug:"_5-5-并发转移",link:"#_5-5-并发转移",children:[]},{level:3,title:"5.6 重定位",slug:"_5-6-重定位",link:"#_5-6-重定位",children:[]}]},{level:2,title:"6 垃圾收集算法",slug:"_6-垃圾收集算法",link:"#_6-垃圾收集算法",children:[{level:3,title:"6.1 JDK 16 之前",slug:"_6-1-jdk-16-之前",link:"#_6-1-jdk-16-之前",children:[]},{level:3,title:"6.2 JDK 16 改进",slug:"_6-2-jdk-16-改进",link:"#_6-2-jdk-16-改进",children:[]}]},{level:2,title:"7 总结",slug:"_7-总结",link:"#_7-总结",children:[]}],path:"/java/JVM/zgc.html",pathLocale:"/",extraFields:[]},{title:"一次jvm调优过程",headers:[{level:2,title:"1. 启用jmx和远程debug模式",slug:"_1-启用jmx和远程debug模式",link:"#_1-启用jmx和远程debug模式",children:[]},{level:2,title:"2. memory analyzer、jprofiler进行堆内存分析",slug:"_2-memory-analyzer、jprofiler进行堆内存分析",link:"#_2-memory-analyzer、jprofiler进行堆内存分析",children:[]},{level:2,title:"3. netty的方面的考虑",slug:"_3-netty的方面的考虑",link:"#_3-netty的方面的考虑",children:[]},{level:2,title:"4. 直接内存排查",slug:"_4-直接内存排查",link:"#_4-直接内存排查",children:[]},{level:2,title:"5. 推荐的直接内存排查方法",slug:"_5-推荐的直接内存排查方法",link:"#_5-推荐的直接内存排查方法",children:[]},{level:2,title:"6. 意外的结果",slug:"_6-意外的结果",link:"#_6-意外的结果",children:[]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html",pathLocale:"/",extraFields:[]},{title:"主流GC收集器采用的算法",headers:[{level:2,title:"是否可以使用两种或多种垃圾回收器",slug:"是否可以使用两种或多种垃圾回收器",link:"#是否可以使用两种或多种垃圾回收器",children:[]},{level:2,title:"查看java进程用的是哪种垃圾回收器",slug:"查看java进程用的是哪种垃圾回收器",link:"#查看java进程用的是哪种垃圾回收器",children:[]},{level:2,title:"怎么查看java进程eden、survivor等各个区的内存占用",slug:"怎么查看java进程eden、survivor等各个区的内存占用",link:"#怎么查看java进程eden、survivor等各个区的内存占用",children:[]},{level:2,title:"参考:",slug:"参考",link:"#参考",children:[]}],path:"/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html",pathLocale:"/",extraFields:[]},{title:"引用计数和根可达算法",headers:[{level:3,title:"硬件角度来看什么叫做垃圾?",slug:"硬件角度来看什么叫做垃圾",link:"#硬件角度来看什么叫做垃圾",children:[]},{level:3,title:"Java中的垃圾",slug:"java中的垃圾",link:"#java中的垃圾",children:[]},{level:2,title:"如何寻找垃圾?",slug:"如何寻找垃圾",link:"#如何寻找垃圾",children:[{level:3,title:"引用计数(Reference Count)",slug:"引用计数-reference-count",link:"#引用计数-reference-count",children:[]},{level:3,title:"根可达算法(Root Search)",slug:"根可达算法-root-search",link:"#根可达算法-root-search",children:[]},{level:3,title:"哪些是GC Root?",slug:"哪些是gc-root",link:"#哪些是gc-root",children:[]},{level:3,title:"线程栈 (Thread Stacks)",slug:"线程栈-thread-stacks",link:"#线程栈-thread-stacks",children:[]},{level:3,title:"静态变量 (Static Variables)",slug:"静态变量-static-variables",link:"#静态变量-static-variables",children:[]},{level:3,title:"常量池 (Constant Pool)",slug:"常量池-constant-pool",link:"#常量池-constant-pool",children:[]},{level:3,title:"JNI 引用 (JNI References)",slug:"jni-引用-jni-references",link:"#jni-引用-jni-references",children:[]},{level:3,title:"根可达算法原理",slug:"根可达算法原理",link:"#根可达算法原理",children:[]}]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html",pathLocale:"/",extraFields:[]},{title:"JVM调优思路",headers:[{level:2,title:"一、监控发现问题",slug:"一、监控发现问题",link:"#一、监控发现问题",children:[]},{level:2,title:"二、工具分析问题",slug:"二、工具分析问题",link:"#二、工具分析问题",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html",pathLocale:"/",extraFields:[]},{title:"SpringBoot",headers:[],path:"/java/SpringBoot/",pathLocale:"/",extraFields:[]},{title:"Spring Boot Prometheus使用",headers:[{level:2,title:"(1)引入库",slug:"_1-引入库",link:"#_1-引入库",children:[]},{level:2,title:"(2)修改配置文件",slug:"_2-修改配置文件",link:"#_2-修改配置文件",children:[]},{level:2,title:"(3)抓包看一下metrics(可跳过)",slug:"_3-抓包看一下metrics-可跳过",link:"#_3-抓包看一下metrics-可跳过",children:[]}],path:"/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html",pathLocale:"/",extraFields:[]},{title:"AOP",headers:[{level:2,title:"一、概述",slug:"一、概述",link:"#一、概述",children:[{level:3,title:"1.1 特点",slug:"_1-1-特点",link:"#_1-1-特点",children:[]},{level:3,title:"1.2 AOP概述",slug:"_1-2-aop概述",link:"#_1-2-aop概述",children:[]}]},{level:2,title:"二、Spring中的AOP",slug:"二、spring中的aop",link:"#二、spring中的aop",children:[{level:3,title:"2.1 AspectJ支持5种类型的通知注解:",slug:"_2-1-aspectj支持5种类型的通知注解",link:"#_2-1-aspectj支持5种类型的通知注解",children:[]},{level:3,title:"2.2 Spring提供了4种实现AOP的方式:",slug:"_2-2-spring提供了4种实现aop的方式",link:"#_2-2-spring提供了4种实现aop的方式",children:[]}]},{level:2,title:"三、原理概述",slug:"三、原理概述",link:"#三、原理概述",children:[]},{level:2,title:"四、使用",slug:"四、使用",link:"#四、使用",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/SpringBoot/aop.html",pathLocale:"/",extraFields:[]},{title:"Webflux",headers:[{level:3,title:"1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?",slug:"_1-servlet-3-1就已经支持异步非阻塞-为什么要用webflux",link:"#_1-servlet-3-1就已经支持异步非阻塞-为什么要用webflux",children:[]},{level:3,title:"webflux和springboot虚拟线程对比",slug:"webflux和springboot虚拟线程对比",link:"#webflux和springboot虚拟线程对比",children:[]}],path:"/java/SpringBoot/webflux.html",pathLocale:"/",extraFields:[]},{title:"I/O",headers:[],path:"/java/io/",pathLocale:"/",extraFields:[]},{title:"JAVA NIO",headers:[{level:2,title:"一、简介",slug:"一、简介",link:"#一、简介",children:[{level:3,title:"1.1典型的多路复用IO实现",slug:"_1-1典型的多路复用io实现",link:"#_1-1典型的多路复用io实现",children:[]}]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/io/nio.html",pathLocale:"/",extraFields:[]},{title:"线程",headers:[],path:"/java/%E7%BA%BF%E7%A8%8B/",pathLocale:"/",extraFields:[]},{title:"synchronized",headers:[{level:2,title:"一、什么是synchronized",slug:"一、什么是synchronized",link:"#一、什么是synchronized",children:[]},{level:2,title:"二、Synchronized 原理",slug:"二、synchronized-原理",link:"#二、synchronized-原理",children:[]},{level:2,title:"三、Synchronized 优化",slug:"三、synchronized-优化",link:"#三、synchronized-优化",children:[{level:3,title:"3.1 偏向锁",slug:"_3-1-偏向锁",link:"#_3-1-偏向锁",children:[]},{level:3,title:"3.2 轻量锁",slug:"_3-2-轻量锁",link:"#_3-2-轻量锁",children:[]},{level:3,title:"3.3 其他优化",slug:"_3-3-其他优化",link:"#_3-3-其他优化",children:[]}]},{level:2,title:"四、扩展",slug:"四、扩展",link:"#四、扩展",children:[{level:3,title:"4.1 Synchronized 和 ReenTrantLock 的对比",slug:"_4-1-synchronized-和-reentrantlock-的对比",link:"#_4-1-synchronized-和-reentrantlock-的对比",children:[]},{level:3,title:"4.2 Synchronized 与 ThreadLocal 的对比",slug:"_4-2-synchronized-与-threadlocal-的对比",link:"#_4-2-synchronized-与-threadlocal-的对比",children:[]},{level:3,title:"4.3 synchronized与volatile区别",slug:"_4-3-synchronized与volatile区别",link:"#_4-3-synchronized与volatile区别",children:[]}]},{level:2,title:"五、各种锁",slug:"五、各种锁",link:"#五、各种锁",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E7%BA%BF%E7%A8%8B/synchronized.html",pathLocale:"/",extraFields:[]},{title:"volatile",headers:[{level:2,title:"可见性",slug:"可见性",link:"#可见性",children:[]},{level:2,title:"有序性",slug:"有序性",link:"#有序性",children:[]}],path:"/java/%E7%BA%BF%E7%A8%8B/volatile.html",pathLocale:"/",extraFields:[]},{title:"TCP/IP、HTTP、HTTPS、HTTP2.0",headers:[{level:2,title:"一、TCP/IP",slug:"一、tcp-ip",link:"#一、tcp-ip",children:[]},{level:2,title:"二、HTTP",slug:"二、http",link:"#二、http",children:[{level:3,title:"2.1 HTTP报文分析",slug:"_2-1-http报文分析",link:"#_2-1-http报文分析",children:[]},{level:3,title:"2.2 HTTP特性",slug:"_2-2-http特性",link:"#_2-2-http特性",children:[]},{level:3,title:"2.3 影响HTTP的因素",slug:"_2-3-影响http的因素",link:"#_2-3-影响http的因素",children:[]}]},{level:2,title:"三、HTTPS",slug:"三、https",link:"#三、https",children:[{level:3,title:"3.1 SSL/TLS",slug:"_3-1-ssl-tls",link:"#_3-1-ssl-tls",children:[]},{level:3,title:"3.2 SPDY",slug:"_3-2-spdy",link:"#_3-2-spdy",children:[]},{level:3,title:"3.3 HTTPS报文分析",slug:"_3-3-https报文分析",link:"#_3-3-https报文分析",children:[]},{level:3,title:"3.4 HTTPS全站化",slug:"_3-4-https全站化",link:"#_3-4-https全站化",children:[]}]},{level:2,title:"四、HTTP2.0",slug:"四、http2-0",link:"#四、http2-0",children:[{level:3,title:"4.1 历史",slug:"_4-1-历史",link:"#_4-1-历史",children:[]},{level:3,title:"4.2 HTTP2.0新特性",slug:"_4-2-http2-0新特性",link:"#_4-2-http2-0新特性",children:[]},{level:3,title:"4.3 HTTP1.1与HTTP2.0的对比",slug:"_4-3-http1-1与http2-0的对比",link:"#_4-3-http1-1与http2-0的对比",children:[]},{level:3,title:"4.4 报文",slug:"_4-4-报文",link:"#_4-4-报文",children:[]}]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html",pathLocale:"/",extraFields:[]},{title:"网络",headers:[],path:"/java/%E7%BD%91%E7%BB%9C/",pathLocale:"/",extraFields:[]},{title:"DevOps",headers:[],path:"/kubernetes/devops/",pathLocale:"/",extraFields:[]},{title:"DevOps平台.md",headers:[{level:2,title:"一、自由风格的软件项目",slug:"一、自由风格的软件项目",link:"#一、自由风格的软件项目",children:[]},{level:2,title:"二、优化之后的CICD",slug:"二、优化之后的cicd",link:"#二、优化之后的cicd",children:[]},{level:2,title:"三、调研期",slug:"三、调研期",link:"#三、调研期",children:[]},{level:2,title:"四、产品化后的DevOps平台",slug:"四、产品化后的devops平台",link:"#四、产品化后的devops平台",children:[{level:3,title:"4.1 Java代码扫描",slug:"_4-1-java代码扫描",link:"#_4-1-java代码扫描",children:[]},{level:3,title:"4.2 Java单元测试",slug:"_4-2-java单元测试",link:"#_4-2-java单元测试",children:[]},{level:3,title:"4.3 Java构建并上传镜像",slug:"_4-3-java构建并上传镜像",link:"#_4-3-java构建并上传镜像",children:[]},{level:3,title:"4.4 部署到阿里云k8s",slug:"_4-4-部署到阿里云k8s",link:"#_4-4-部署到阿里云k8s",children:[]},{level:3,title:"4.5 整体流程",slug:"_4-5-整体流程",link:"#_4-5-整体流程",children:[]},{level:3,title:"4.4 日志",slug:"_4-4-日志",link:"#_4-4-日志",children:[]},{level:3,title:"4.5 定时触发",slug:"_4-5-定时触发",link:"#_4-5-定时触发",children:[]}]},{level:2,title:"五、其他",slug:"五、其他",link:"#五、其他",children:[{level:3,title:"5.1 Gitlab触发",slug:"_5-1-gitlab触发",link:"#_5-1-gitlab触发",children:[]}]},{level:2,title:"六、总结",slug:"六、总结",link:"#六、总结",children:[]}],path:"/kubernetes/devops/devops-ping-tai.html",pathLocale:"/",extraFields:[]},{title:"canal小记",headers:[{level:2,title:"问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么",slug:"问题一-测试环境一切正常-但是正式环境中-这几个字段全为0-不知道为什么",link:"#问题一-测试环境一切正常-但是正式环境中-这几个字段全为0-不知道为什么",children:[]},{level:2,title:"问题二:canal.properties中四种模式的差别",slug:"问题二-canal-properties中四种模式的差别",link:"#问题二-canal-properties中四种模式的差别",children:[]},{level:2,title:"问题三:如果要订阅的是mysql的从库改怎么做?",slug:"问题三-如果要订阅的是mysql的从库改怎么做",link:"#问题三-如果要订阅的是mysql的从库改怎么做",children:[]},{level:2,title:"问题四:部分字段没有更新",slug:"问题四-部分字段没有更新",link:"#问题四-部分字段没有更新",children:[]}],path:"/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html",pathLocale:"/",extraFields:[]},{title:"kafka面试题",headers:[],path:"/middleware/kafka/kafka.html",pathLocale:"/",extraFields:[]},{title:"Spring Cloud Sentinel",headers:[{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/middleware/sentinel/springcloud_sentinel.html",pathLocale:"/",extraFields:[]},{title:"入门使用",headers:[{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html",pathLocale:"/",extraFields:[]},{title:"Zookeeper",headers:[],path:"/middleware/zookeeper/zookeeper.html",pathLocale:"/",extraFields:[]},{title:"基于ZooKeeper的队列爬虫",headers:[{level:2,title:"一、整体架构",slug:"一、整体架构",link:"#一、整体架构",children:[]},{level:2,title:"二、ZooKeeper队列原理",slug:"二、zookeeper队列原理",link:"#二、zookeeper队列原理",children:[{level:3,title:"2.1 介绍",slug:"_2-1-介绍",link:"#_2-1-介绍",children:[]},{level:3,title:"2.2 Watcher介绍",slug:"_2-2-watcher介绍",link:"#_2-2-watcher介绍",children:[]},{level:3,title:"2.3 源码",slug:"_2-3-源码",link:"#_2-3-源码",children:[]}]},{level:2,title:"三、多线程并发",slug:"三、多线程并发",link:"#三、多线程并发",children:[]},{level:2,title:"四、使用",slug:"四、使用",link:"#四、使用",children:[{level:3,title:"实验结果",slug:"实验结果",link:"#实验结果",children:[]},{level:3,title:"实验二",slug:"实验二",link:"#实验二",children:[]}]},{level:2,title:"总结",slug:"总结",link:"#总结",children:[]}],path:"/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html",pathLocale:"/",extraFields:[]},{title:"推荐工程-概述",headers:[{level:2,title:"一、淘宝和拼多多的电商模式差异",slug:"一、淘宝和拼多多的电商模式差异",link:"#一、淘宝和拼多多的电商模式差异",children:[]},{level:2,title:"二、搜索、推荐、广告三者的异同",slug:"二、搜索、推荐、广告三者的异同",link:"#二、搜索、推荐、广告三者的异同",children:[]},{level:2,title:"三、推荐整体架构",slug:"三、推荐整体架构",link:"#三、推荐整体架构",children:[]},{level:2,title:"参考",slug:"参考",link:"#参考",children:[]}],path:"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html",pathLocale:"/",extraFields:[]},{title:"推荐系统入门",headers:[{level:2,title:"一、背景",slug:"一、背景",link:"#一、背景",children:[]},{level:2,title:"二、推荐系统介绍",slug:"二、推荐系统介绍",link:"#二、推荐系统介绍",children:[{level:3,title:"2.1、为什么需要推荐系统?",slug:"_2-1、为什么需要推荐系统",link:"#_2-1、为什么需要推荐系统",children:[]},{level:3,title:"2.2、推荐系统解决什么问题?",slug:"_2-2、推荐系统解决什么问题",link:"#_2-2、推荐系统解决什么问题",children:[]},{level:3,title:"2.3、推荐系统应用场景",slug:"_2-3、推荐系统应用场景",link:"#_2-3、推荐系统应用场景",children:[]},{level:3,title:"2.4、搜索、推荐、广告三者的异同",slug:"_2-4、搜索、推荐、广告三者的异同",link:"#_2-4、搜索、推荐、广告三者的异同",children:[]}]},{level:2,title:"三、推荐系统通用框架",slug:"三、推荐系统通用框架",link:"#三、推荐系统通用框架",children:[]},{level:2,title:"四、经典算法",slug:"四、经典算法",link:"#四、经典算法",children:[{level:3,title:"4.1、协同过滤是什么?",slug:"_4-1、协同过滤是什么",link:"#_4-1、协同过滤是什么",children:[]},{level:3,title:"4.2、如何找相似?",slug:"_4-2、如何找相似",link:"#_4-2、如何找相似",children:[]},{level:3,title:"4.3、Item-CF 的算法流程",slug:"_4-3、item-cf-的算法流程",link:"#_4-3、item-cf-的算法流程",children:[]}]},{level:2,title:"五、如何实现推荐系统",slug:"五、如何实现推荐系统",link:"#五、如何实现推荐系统",children:[{level:3,title:"5.1、选择数据集",slug:"_5-1、选择数据集",link:"#_5-1、选择数据集",children:[]},{level:3,title:"5.2、读取原始数据",slug:"_5-2、读取原始数据",link:"#_5-2、读取原始数据",children:[]},{level:3,title:"5.3、构造物品的相似度矩阵",slug:"_5-3、构造物品的相似度矩阵",link:"#_5-3、构造物品的相似度矩阵",children:[]},{level:3,title:"5.4、基于相似度矩阵推荐物品",slug:"_5-4、基于相似度矩阵推荐物品",link:"#_5-4、基于相似度矩阵推荐物品",children:[]},{level:3,title:"5.5、调用推荐系统",slug:"_5-5、调用推荐系统",link:"#_5-5、调用推荐系统",children:[]}]},{level:2,title:"六、问题与展望",slug:"六、问题与展望",link:"#六、问题与展望",children:[]}],path:"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html",pathLocale:"/",extraFields:[]},{title:"计算广告基本概念入门总结",headers:[],path:"/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]},{title:"Interview",headers:[],path:"/interview/",pathLocale:"/",extraFields:[]},{title:"Life",headers:[],path:"/life/",pathLocale:"/",extraFields:[]},{title:"Link",headers:[],path:"/link/",pathLocale:"/",extraFields:[]},{title:"System Design",headers:[],path:"/system-design/",pathLocale:"/",extraFields:[]},{title:"Personal Life",headers:[],path:"/about-the-author/personal-life/",pathLocale:"/",extraFields:[]},{title:"Works",headers:[],path:"/about-the-author/works/",pathLocale:"/",extraFields:[]},{title:"Spark",headers:[],path:"/bigdata/spark/",pathLocale:"/",extraFields:[]},{title:"Elasticsearch",headers:[],path:"/database/elasticsearch/",pathLocale:"/",extraFields:[]},{title:"Mysql",headers:[],path:"/database/mysql/",pathLocale:"/",extraFields:[]},{title:"Redis",headers:[],path:"/database/redis/",pathLocale:"/",extraFields:[]},{title:"Canal",headers:[],path:"/middleware/canal/",pathLocale:"/",extraFields:[]},{title:"Kafka",headers:[],path:"/middleware/kafka/",pathLocale:"/",extraFields:[]},{title:"Sentinel",headers:[],path:"/middleware/sentinel/",pathLocale:"/",extraFields:[]},{title:"Zookeeper",headers:[],path:"/middleware/zookeeper/",pathLocale:"/",extraFields:[]},{title:"推荐系统",headers:[],path:"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/",pathLocale:"/",extraFields:[]},{title:"计算广告",headers:[],path:"/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/",pathLocale:"/",extraFields:[]},{title:"分类",headers:[],path:"/category/",pathLocale:"/",extraFields:[]},{title:"标签",headers:[],path:"/tag/",pathLocale:"/",extraFields:[]},{title:"文章",headers:[],path:"/article/",pathLocale:"/",extraFields:[]},{title:"星标",headers:[],path:"/star/",pathLocale:"/",extraFields:[]},{title:"时间轴",headers:[],path:"/timeline/",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/slide/",pathLocale:"/",extraFields:[]}],M3=ee(R3),V3=()=>M3,N3=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:l})=>{const r=k(()=>e.value.filter(i=>i.pathLocale===t.value));return k(()=>{const i=n.value.trim().toLowerCase();if(!i)return[];const a=[],o=(c,u)=>{qc(i,[u.title])&&a.push({link:`${c.path}#${u.slug}`,title:c.title,header:u.title});for(const d of u.children){if(a.length>=l.value)return;o(c,d)}};for(const c of r.value){if(a.length>=l.value)break;if(qc(i,[c.title,...c.extraFields])){a.push({link:c.path,title:c.title});continue}for(const u of c.headers){if(a.length>=l.value)break;o(c,u)}}return a})},j3=e=>{const t=ee(0);return{focusIndex:t,focusNext:()=>{t.value{t.value>0?t.value-=1:t.value=e.value.length-1}}},H3=H({name:"SearchBox",props:{locales:{type:Object,required:!1,default:()=>({})},hotKeys:{type:Array,required:!1,default:()=>[]},maxSuggestions:{type:Number,required:!1,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:l}=zp(e),r=en(),i=Mt(),a=V3(),o=ee(null),c=ee(!1),u=ee(""),d=k(()=>t.value[i.value]??{}),f=N3({searchIndex:a,routeLocale:i,query:u,maxSuggestions:l}),{focusIndex:h,focusNext:p,focusPrev:g}=j3(f);F3({input:o,hotKeys:n});const v=k(()=>c.value&&!!f.value.length),y=()=>{v.value&&g()},_=()=>{v.value&&p()},A=m=>{if(!v.value)return;const w=f.value[m];w&&r.push(w.link).then(()=>{u.value="",h.value=0})};return()=>s("form",{class:"search-box",role:"search"},[s("input",{ref:o,type:"search",placeholder:d.value.placeholder,autocomplete:"off",spellcheck:!1,value:u.value,onFocus:()=>c.value=!0,onBlur:()=>c.value=!1,onInput:m=>u.value=m.target.value,onKeydown:m=>{switch(m.key){case"ArrowUp":{y();break}case"ArrowDown":{_();break}case"Enter":{m.preventDefault(),A(h.value);break}}}}),v.value&&s("ul",{class:"suggestions",onMouseleave:()=>h.value=-1},f.value.map(({link:m,title:w,header:F},V)=>s("li",{class:["suggestion",{focus:h.value===V}],onMouseenter:()=>h.value=V,onMousedown:()=>A(V)},s("a",{href:m,onClick:R=>R.preventDefault()},[s("span",{class:"page-title"},w),F&&s("span",{class:"page-header"},`> ${F}`)]))))])}});var z3=["s","/"],$3={"/":{placeholder:"搜索"}};const U3=$3,G3=z3,K3=10,q3=Bt({enhance({app:e}){e.component("SearchBox",t=>s(H3,{locales:U3,hotKeys:G3,maxSuggestions:K3,...t}))}}),W3=Object.freeze(Object.defineProperty({__proto__:null,default:q3},Symbol.toStringTag,{value:"Module"})),Mf="VUEPRESS_REDIRECT_STATUS",Wc=Vv(Mf,{}),Jc=Hv(Mf,{}),J3=e=>{const t=Nv(),n=Mt(),l=En(e.config);return k(()=>{if(l.some(([r])=>n.value===r)){for(const r of t.value)for(const[i,a]of l)if(a.includes(r))return i===n.value?null:{lang:r,localePath:i}}return null})};var Y3=H({name:"RedirectModal",props:{config:{type:Object,required:!0},locales:{type:Object,required:!0}},setup(e){const t=en(),n=Pd(),l=Mt(),r=J3(e.config),i=ee(),a=eo(i),o=ee(!1),c=k(()=>{if(!r.value)return null;const{lang:d,localePath:f}=r.value,h=[e.locales[f],e.locales[l.value]];return{hint:h.map(({hint:p})=>p.replace("$1",d)),switch:h.map(({switch:p})=>p.replace("$1",d)).join(" / "),cancel:h.map(({cancel:p})=>p).join(" / "),remember:h.map(({remember:p})=>p).join(" / ")}}),u=()=>{Jc.value[l.value]=!0,o.value&&(Wc.value[l.value]=!0),a.value=!1};return pe(n,()=>{a.value=!1}),we(async()=>{i.value=document.body,await Qt(),r.value&&!Jc.value[l.value]&&!Wc.value[l.value]&&(a.value=!0)}),Jr(()=>{a.value=!1}),()=>s(gd,{name:"redirect-modal-fade"},()=>{var d,f,h,p;return a.value?s("div",{key:"mask",class:"redirect-modal-mask"},s("div",{key:"popup",class:"redirect-modal-wrapper"},[s("div",{class:"redirect-modal-content"},(d=c.value)==null?void 0:d.hint.map(g=>s("p",g))),s("div",{class:"redirect-modal-hint"},[s("input",{id:"remember-redirect",type:"checkbox",value:o.value,onChange:()=>{o.value=!o.value}}),s("label",{for:"remember-redirect"},(f=c.value)==null?void 0:f.remember)]),s("button",{type:"button",class:"redirect-modal-action primary",onClick:()=>{u(),t.replace(n.value.replace(l.value,r.value.localePath))}},(h=c.value)==null?void 0:h.switch),s("button",{type:"button",class:"redirect-modal-action",onClick:()=>{u()}},(p=c.value)==null?void 0:p.cancel)])):null})}}),Z3={config:{},autoLocale:!1,localeFallback:!0,defaultBehavior:"defaultLocale"},Q3={"/":{name:"简体中文",hint:"您的首选语言是 $1,是否切换到该语言?",switch:"切换到 $1",cancel:"取消",remember:"记住我的选择"}};const Vf=Z3;var X3=Bt({setup(){},rootComponents:[()=>s(Y3,{config:Vf,locales:Q3})]});const e8=Object.freeze(Object.defineProperty({__proto__:null,config:Vf,default:X3},Symbol.toStringTag,{value:"Module"})),qe=e=>{const{icon:t="",color:n,size:l}=e,r=n||l?{}:null;return n&&(r.color=n),l&&(r.height=Number.isNaN(Number(l))?l:`${l}px`),gn(t)?s("img",{class:"icon",src:t,alt:"","no-view":"",style:r}):ni(t)?s("img",{class:"icon",src:Ae(t),alt:"","aria-hidden":"","no-view":"",style:r}):s(kt("FontIcon"),e)};qe.displayName="HopeIcon";const Nf=()=>{const e=ce();return k(()=>e.value.author)},t8="http://.",Rn=()=>{const e=en(),t=Ut();return n=>{if(n)if(ni(n))t.path!==n&&e.push(n);else if(tr(n))window&&window.open(n);else{const l=t.path.slice(0,t.path.lastIndexOf("/"));e.push(new URL(`${l}/${encodeURI(n)}`,t8).pathname)}}};var Yc={"/":{word:"约 $word 字",less1Minute:"小于 1 分钟",time:"大约 $time 分钟"}};const jf=()=>{const e=Oe();return k(()=>e.value.readingTime??null)},Hf=(e,t)=>{const{minutes:n,words:l}=e,{less1Minute:r,word:i,time:a}=t;return{time:n<1?r:a.replace("$time",Math.round(n).toString()),words:i.replace("$word",l.toString())}},Zc={words:"",time:""},pa=typeof Yc>"u"?null:Yc,zf=()=>pa?Fn(pa):k(()=>null),n8=()=>{if(typeof pa>"u")return k(()=>Zc);const e=jf(),t=zf();return k(()=>e.value&&t.value?Hf(e.value,t.value):Zc)},me=({name:e="",color:t="currentColor",ariaLabel:n},{attrs:l,slots:r})=>{var i;return s("svg",{xmlns:"http://www.w3.org/2000/svg",class:["icon",`${e}-icon`],viewBox:"0 0 1024 1024",fill:t,"aria-label":n??`${e} icon`,...l},(i=r.default)==null?void 0:i.call(r))};me.displayName="IconBase";const vo=(e,{slots:t})=>{var n;return(n=t.default)==null?void 0:n.call(t)},l8=e=>gn(e)?e:`https://github.com/${e}`,mo=(e="")=>!gn(e)||e.includes("github.com")?"GitHub":e.includes("bitbucket.org")?"Bitbucket":e.includes("gitlab.com")?"GitLab":e.includes("gitee.com")?"Gitee":null,$f=()=>s(me,{name:"github"},()=>s("path",{d:"M511.957 21.333C241.024 21.333 21.333 240.981 21.333 512c0 216.832 140.544 400.725 335.574 465.664 24.49 4.395 32.256-10.07 32.256-23.083 0-11.69.256-44.245 0-85.205-136.448 29.61-164.736-64.64-164.736-64.64-22.315-56.704-54.4-71.765-54.4-71.765-44.587-30.464 3.285-29.824 3.285-29.824 49.195 3.413 75.179 50.517 75.179 50.517 43.776 75.008 114.816 53.333 142.762 40.79 4.523-31.66 17.152-53.377 31.19-65.537-108.971-12.458-223.488-54.485-223.488-242.602 0-53.547 19.114-97.323 50.517-131.67-5.035-12.33-21.93-62.293 4.779-129.834 0 0 41.258-13.184 134.912 50.346a469.803 469.803 0 0 1 122.88-16.554c41.642.213 83.626 5.632 122.88 16.554 93.653-63.488 134.784-50.346 134.784-50.346 26.752 67.541 9.898 117.504 4.864 129.834 31.402 34.347 50.474 78.123 50.474 131.67 0 188.586-114.73 230.016-224.042 242.09 17.578 15.232 33.578 44.672 33.578 90.454v135.85c0 13.142 7.936 27.606 32.854 22.87C862.25 912.597 1002.667 728.747 1002.667 512c0-271.019-219.648-490.667-490.71-490.667z"}));$f.displayName="GitHubIcon";const Uf=()=>s(me,{name:"gitee"},()=>s("path",{d:"M512 992C246.92 992 32 777.08 32 512S246.92 32 512 32s480 214.92 480 480-214.92 480-480 480zm242.97-533.34H482.39a23.7 23.7 0 0 0-23.7 23.7l-.03 59.28c0 13.08 10.59 23.7 23.7 23.7h165.96a23.7 23.7 0 0 1 23.7 23.7v11.85a71.1 71.1 0 0 1-71.1 71.1H375.71a23.7 23.7 0 0 1-23.7-23.7V423.11a71.1 71.1 0 0 1 71.1-71.1h331.8a23.7 23.7 0 0 0 23.7-23.7l.06-59.25a23.73 23.73 0 0 0-23.7-23.73H423.11a177.78 177.78 0 0 0-177.78 177.75v331.83c0 13.08 10.62 23.7 23.7 23.7h349.62a159.99 159.99 0 0 0 159.99-159.99V482.33a23.7 23.7 0 0 0-23.7-23.7z"}));Uf.displayName="GiteeIcon";const Gf=()=>s(me,{name:"bitbucket"},()=>s("path",{d:"M575.256 490.862c6.29 47.981-52.005 85.723-92.563 61.147-45.714-20.004-45.714-92.562-1.133-113.152 38.29-23.442 93.696 7.424 93.696 52.005zm63.451-11.996c-10.276-81.152-102.29-134.839-177.152-101.156-47.433 21.138-79.433 71.424-77.129 124.562 2.853 69.705 69.157 126.866 138.862 120.576S647.3 548.571 638.708 478.83zm136.558-309.723c-25.161-33.134-67.986-38.839-105.728-45.13-106.862-17.151-216.576-17.7-323.438 1.134-35.438 5.706-75.447 11.996-97.719 43.996 36.572 34.304 88.576 39.424 135.424 45.129 84.553 10.862 171.447 11.447 256 .585 47.433-5.705 99.987-10.276 135.424-45.714zm32.585 591.433c-16.018 55.99-6.839 131.438-66.304 163.986-102.29 56.576-226.304 62.867-338.87 42.862-59.43-10.862-129.135-29.696-161.72-85.723-14.3-54.858-23.442-110.848-32.585-166.84l3.438-9.142 10.276-5.157c170.277 112.567 408.576 112.567 579.438 0 26.844 8.01 6.84 40.558 6.29 60.014zm103.424-549.157c-19.42 125.148-41.728 249.71-63.415 374.272-6.29 36.572-41.728 57.162-71.424 72.558-106.862 53.724-231.424 62.866-348.562 50.286-79.433-8.558-160.585-29.696-225.134-79.433-30.28-23.443-30.28-63.415-35.986-97.134-20.005-117.138-42.862-234.277-57.161-352.585 6.839-51.42 64.585-73.728 107.447-89.71 57.16-21.138 118.272-30.866 178.87-36.571 129.134-12.58 261.157-8.01 386.304 28.562 44.581 13.13 92.563 31.415 122.844 69.705 13.714 17.7 9.143 40.01 6.29 60.014z"}));Gf.displayName="BitbucketIcon";const Kf=()=>s(me,{name:"source"},()=>s("path",{d:"M601.92 475.2c0 76.428-8.91 83.754-28.512 99.594-14.652 11.88-43.956 14.058-78.012 16.434-18.81 1.386-40.392 2.97-62.172 6.534-18.612 2.97-36.432 9.306-53.064 17.424V299.772c37.818-21.978 63.36-62.766 63.36-109.692 0-69.894-56.826-126.72-126.72-126.72S190.08 120.186 190.08 190.08c0 46.926 25.542 87.714 63.36 109.692v414.216c-37.818 21.978-63.36 62.766-63.36 109.692 0 69.894 56.826 126.72 126.72 126.72s126.72-56.826 126.72-126.72c0-31.086-11.286-59.598-29.7-81.576 13.266-9.504 27.522-17.226 39.996-19.206 16.038-2.574 32.868-3.762 50.688-5.148 48.312-3.366 103.158-7.326 148.896-44.55 61.182-49.698 74.25-103.158 75.24-187.902V475.2h-126.72zM316.8 126.72c34.848 0 63.36 28.512 63.36 63.36s-28.512 63.36-63.36 63.36-63.36-28.512-63.36-63.36 28.512-63.36 63.36-63.36zm0 760.32c-34.848 0-63.36-28.512-63.36-63.36s28.512-63.36 63.36-63.36 63.36 28.512 63.36 63.36-28.512 63.36-63.36 63.36zM823.68 158.4h-95.04V63.36h-126.72v95.04h-95.04v126.72h95.04v95.04h126.72v-95.04h95.04z"}));Kf.displayName="SourceIcon";const qf=({link:e,type:t=mo(e??"")})=>{if(!t)return null;const n=t.toLowerCase();return s(n==="bitbucket"?Gf:n==="github"?$f:n==="gitlab"?"GitLab":n==="gitee"?Uf:Kf)},r8=(e,t=0)=>{let n=3735928559^t,l=1103547991^t;for(let r=0,i;r>>16,2246822507),n^=Math.imul(l^l>>>13,3266489909),l=Math.imul(l^l>>>16,2246822507),l^=Math.imul(n^n>>>13,3266489909),4294967296*(2097151&l)+(n>>>0)},sr=(e,t)=>r8(e)%t;class i8{constructor(){ui(this,"containerElement");ui(this,"messageElements",{});const t="message-container",n=document.getElementById(t);n?this.containerElement=n:(this.containerElement=document.createElement("div"),this.containerElement.id=t,document.body.appendChild(this.containerElement))}pop(t,n=2e3){const l=document.createElement("div"),r=Date.now();return l.className="message move-in",l.innerHTML=t,this.containerElement.appendChild(l),this.messageElements[r]=l,n>0&&setTimeout(()=>{this.close(r)},n),r}close(t){if(t){const n=this.messageElements[t];n.classList.remove("move-in"),n.classList.add("move-out"),n.addEventListener("animationend",()=>{n.remove(),delete this.messageElements[t]})}else hn(this.messageElements).forEach(n=>this.close(Number(n)))}destroy(){document.body.removeChild(this.containerElement)}}const Wf=/#.*$/u,a8=e=>{const t=Wf.exec(e);return t?t[0]:""},Qc=e=>decodeURI(e).replace(Wf,"").replace(/\/index\.html$/iu,"/").replace(/\/(README|index)\.md$/iu,"/").replace(/\.(?:html|md)$/iu,""),Jf=(e,t)=>{if(!Jd(t))return!1;const n=Qc(e.path),l=Qc(t),r=a8(t);return r?r===e.hash&&(!l||n===l):n===l};var o8=e=>Object.prototype.toString.call(e)==="[object Object]",Gl=e=>typeof e=="string";const{isArray:Yf}=Array,Xc=e=>o8(e)&&Gl(e.name),Kl=(e,t=!1)=>e?Yf(e)?e.map(n=>Gl(n)?{name:n}:Xc(n)?n:null).filter(n=>n!==null):Gl(e)?[{name:e}]:Xc(e)?[e]:(console.error(`Expect "author" to be \`AuthorInfo[] | AuthorInfo | string[] | string ${t?"":"| false"} | undefined\`, but got`,e),[]):[],Zf=(e,t)=>{if(e){if(Yf(e)&&e.every(Gl))return e;if(Gl(e))return[e];console.error(`Expect ${t} to be \`string[] | string | undefined\`, but got`,e)}return[]},Qf=e=>Zf(e,"category"),Xf=e=>Zf(e,"tag"),tn=()=>Df(),ce=()=>H4(),eh=()=>{const e=ve(),t=Nf();return k(()=>{const{author:n}=e.value;return n?Kl(n):n===!1?[]:Kl(t.value,!1)})},s8=()=>{const e=ve(),t=Se(Symbol.for("categoryMap"));return k(()=>Qf(e.value.category??e.value.categories).map(n=>{var l;return{name:n,path:((l=t==null?void 0:t.value.map[n])==null?void 0:l.path)??""}}))},c8=()=>{const e=ve(),t=Se(Symbol.for("tagMap"));return k(()=>Xf(e.value.tag??e.value.tags).map(n=>{var l;return{name:n,path:((l=t==null?void 0:t.value.map[n])==null?void 0:l.path)??""}}))},u8=()=>{const e=ve(),t=Oe();return k(()=>{const n=lo(e.value.date);if(n)return n;const{createdTime:l}=t.value.git??{};return l?new Date(l):null})},d8=()=>{const e=ce(),t=Oe(),n=ve(),l=eh(),r=s8(),i=c8(),a=u8(),o=jf(),c=n8(),u=k(()=>({author:l.value,category:r.value,date:a.value,localizedDate:t.value.localizedDate,tag:i.value,isOriginal:n.value.isOriginal??!1,readingTime:o.value,readingTimeLocale:c.value,pageview:n.value.pageview??!0})),d=k(()=>n.value.pageInfo??e.value.pageInfo??null);return{info:u,items:d}},ot=()=>{const e=tn();return k(()=>!!e.value.pure)},go=(e,t)=>"activeMatch"in t?new RegExp(t.activeMatch,"u").test(e.path):Jf(e,t.link),Eo=(e,t)=>"children"in t?!!t.prefix&&Jf(e,t.prefix)||t.children.some(n=>Eo(e,n)):go(e,t),_o={"/":["",{text:"",prefix:"cloudnative/",collapsible:!0,children:[""]},{text:"",prefix:"open-source-project/",collapsible:!0,children:[""]},{text:"Database",prefix:"database/",collapsible:!0,children:["",{text:"Elasticsearch",prefix:"elasticsearch/",collapsible:!0,children:["基础","【elasticsearch】搜索过程详解","elasticsearch源码debug"]},{text:"Mysql",prefix:"mysql/",collapsible:!0,children:["1mysql","数据库缓存"]},{text:"Redis",prefix:"redis/",collapsible:!0,children:["RedissonLock分布式锁源码分析","redis缓存"]}]},{text:"Interview",prefix:"interview/",collapsible:!0,children:["tiktok2023"]},{text:"Java",prefix:"java/",collapsible:!0,children:[{text:"I/O",prefix:"io/",collapsible:!0,icon:"pen",children:["nio"]},{text:"JVM",prefix:"JVM/",collapsible:!0,icon:"pen",children:["cms","g1","Java内存模型","JVM调优参数","zgc","一次jvm调优过程","常用GC回收器","引用计数和根可达算法"]},{text:"SpringBoot",prefix:"SpringBoot/",collapsible:!0,icon:"pen",children:["aop","Spring Boot Prometheus使用","webflux"]},{text:"线程",prefix:"线程/",collapsible:!0,icon:"pen",children:["synchronized","volatile"]},{text:"网络",prefix:"网络/",collapsible:!0,icon:"pen",children:["IP、HTTP、HTTPS、HTTP2.0"]},"在 Spring 6 中使用虚拟线程","基于kubernetes的分布式限流","流程编排LiteFlow","serverlog","高可用","高并发","高性能","高性能高并发高可用的一些思考"]},{text:"Life",prefix:"life/",collapsible:!0,children:["2017","2018","2019"]},{text:"Link",prefix:"link/",collapsible:!0,children:["main"]},{text:"Spark",prefix:"bigdata/",collapsible:!0,children:["",{text:"Spark",prefix:"spark/",collapsible:!0,children:["elastic-spark"]}]},{text:"System Design",prefix:"system-design/",collapsible:!0,children:["feed",{text:"推荐系统",prefix:"推荐系统/",collapsible:!0,children:["推荐工程-概述","推荐系统入门"]},{text:"计算广告",prefix:"计算广告/",collapsible:!0,children:["计算广告基本概念入门总结"]}]},{text:"中间件",prefix:"middleware/",collapsible:!0,children:["",{text:"Canal",prefix:"canal/",collapsible:!0,children:["中间件——canal小记"]},{text:"Kafka",prefix:"kafka/",collapsible:!0,children:["kafka"]},{text:"Sentinel",prefix:"sentinel/",collapsible:!0,children:[]},{text:"Zookeeper",prefix:"zookeeper/",collapsible:!0,children:["zookeeper","基于ZooKeeper的队列爬虫"]}]},{text:"关于我",prefix:"about-the-author/",collapsible:!0,children:["",{text:"Personal Life",prefix:"personal-life/",collapsible:!0,children:["2024-07-24","2024-11-09上海迪斯尼","wewe"]},{text:"Works",prefix:"works/",collapsible:!0,children:["个人作品"]}]},{text:"好玩的",prefix:"interesting/",collapsible:!0,children:[{text:"个人网站",prefix:"个人网站/",collapsible:!0,icon:"pen",children:["1.历史与架构","2.Lucene的使用","3.定时任务","4.日志系统","5.小集群部署","6.数据库备份","7.那些牛逼的插件","8.基于贝叶斯的情感分析","9.网站性能优化","10.历时8年最终改版"]},{text:"mini主机",prefix:"mini主机/",collapsible:!0,icon:"pen",children:["mac连接主机","买了个mini主机"]},"idea设置","starcraft-ai","tesla","一些不常用但很实用的命令","基于OLAP做业务监控","小程序反编译","广州图书馆借阅抓取","开发工具整理","chatgpt","程序员副业探索之电商"]},{text:"微信支付",prefix:"donate/",collapsible:!0,children:[""]},{text:"目录",prefix:"kubernetes/",collapsible:!0,children:[{text:"DevOps",prefix:"devops/",collapsible:!0,icon:"pen",children:["devops-ping-tai"]},"Jenkins的一些笔记","request_limit","Kubernetes容器日志收集","spark on k8s operator"]},{text:"股票预测",prefix:"stock/",collapsible:!0,children:["","赛力斯"]}],"/interesting/":[{text:"个人网站",prefix:"个人网站/",collapsible:!0,icon:"pen",children:["1.历史与架构","2.Lucene的使用","3.定时任务","4.日志系统","5.小集群部署","6.数据库备份","7.那些牛逼的插件","8.基于贝叶斯的情感分析","9.网站性能优化","10.历时8年最终改版"]},{text:"mini主机",prefix:"mini主机/",collapsible:!0,icon:"pen",children:["mac连接主机","买了个mini主机"]},"idea设置","starcraft-ai","tesla","一些不常用但很实用的命令","基于OLAP做业务监控","小程序反编译","广州图书馆借阅抓取","开发工具整理","chatgpt","程序员副业探索之电商"],"/java/":[{text:"I/O",prefix:"io/",collapsible:!0,icon:"pen",children:["nio"]},{text:"JVM",prefix:"JVM/",collapsible:!0,icon:"pen",children:["cms","g1","Java内存模型","JVM调优参数","zgc","一次jvm调优过程","常用GC回收器","引用计数和根可达算法"]},{text:"SpringBoot",prefix:"SpringBoot/",collapsible:!0,icon:"pen",children:["aop","Spring Boot Prometheus使用","webflux"]},{text:"线程",prefix:"线程/",collapsible:!0,icon:"pen",children:["synchronized","volatile"]},{text:"网络",prefix:"网络/",collapsible:!0,icon:"pen",children:["IP、HTTP、HTTPS、HTTP2.0"]},"在 Spring 6 中使用虚拟线程","基于kubernetes的分布式限流","流程编排LiteFlow","serverlog","高可用","高并发","高性能","高性能高并发高可用的一些思考"],"/database/mysql/":["1mysql","数据库缓存"],"/database/redis/":["RedissonLock分布式锁源码分析","redis缓存"],"/database/elasticsearch/":["基础","【elasticsearch】搜索过程详解","elasticsearch源码debug"],"/database/mongodb/":[],"/bigdata/spark/":["elastic-spark"],"/middleware/kafka/":["kafka"],"/middleware/zookeeper/":["zookeeper","基于ZooKeeper的队列爬虫"],"/middleware/canal/":["中间件——canal小记"],"/kubernetes/":[{text:"DevOps",prefix:"devops/",collapsible:!0,icon:"pen",children:["devops-ping-tai"]},"Jenkins的一些笔记","request_limit","Kubernetes容器日志收集","spark on k8s operator"],"/system-design/":["feed",{text:"推荐系统",prefix:"推荐系统/",collapsible:!0,children:["推荐工程-概述","推荐系统入门"]},{text:"计算广告",prefix:"计算广告/",collapsible:!0,children:["计算广告基本概念入门总结"]}],"/life/":["2017","2018","2019"],"/about-the-author/personal-life/":["2024-07-24","2024-11-09上海迪斯尼","wewe"],"/about-the-author/works/":["个人作品"],"/about-the-author/talking/":[]},f8=(e,t)=>{const n=e.replace(t,"/").split("/"),l=[];let r=Zr(t);return n.forEach((i,a)=>{a!==n.length-1?(r+=`${i}/`,l.push({link:r,name:i||"Home"})):i!==""&&(r+=i,l.push({link:r,name:i}))}),l},va=e=>!Ka(e)&&!tr(e);let Mi=null,El=null;const ma={wait:()=>Mi,pending:()=>{Mi=new Promise(e=>{El=e})},resolve:()=>{El==null||El(),Mi=null,El=null}};var Qe;(function(e){e.title="t",e.shortTitle="s",e.icon="i",e.index="I",e.order="O",e.breadcrumbExclude="b"})(Qe||(Qe={}));var Ke;(function(e){e.type="y",e.author="a",e.date="d",e.localizedDate="l",e.category="c",e.tag="g",e.isEncrypted="n",e.isOriginal="o",e.readingTime="r",e.excerpt="e",e.sticky="u",e.cover="v"})(Ke||(Ke={}));var ga;(function(e){e.article="a",e.home="h",e.slide="s",e.page="p"})(ga||(ga={}));const $r=(e,t=!1,n)=>{const{meta:l,path:r,notFound:i}=wt(e,n);return i?{text:r,link:r}:{text:!t&&l[Qe.shortTitle]?l[Qe.shortTitle]:l[Qe.title]||r,link:r,...l[Qe.icon]?{icon:l[Qe.icon]}:{}}},xn=(e="",t="")=>ni(t)||tr(t)?t:`${_d(e)}${t}`,th=(e,t)=>{const n=ke(e)?$r(xn(t,e)):e.link?{...e,link:va(e.link)?wt(xn(t,e.link)).path:e.link}:e;if("children"in n){const l=xn(t,n.prefix),r=n.children==="structure"?_o[l]:n.children;return{...n,prefix:l,children:r.map(i=>th(i,l))}}return{...n}},Ea=({config:e,prefix:t=""})=>e.map(n=>th(n,t)),h8=({config:e,routePath:t,headerDepth:n})=>{const l=hn(e).sort((r,i)=>i.length-r.length);for(const r of l)if(Hr(decodeURI(t),r)){const i=e[r];return Ea({config:i==="structure"?_o[r]:i||[],headerDepth:n,prefix:r})}return console.warn(`${decodeURI(t)} is missing sidebar config.`),[]},p8=({config:e,headerDepth:t,routeLocale:n,routePath:l})=>e==="structure"?Ea({config:_o[n],headerDepth:t,prefix:n}):ra(e)?Ea({config:e,headerDepth:t}):nr(e)?h8({config:e,routePath:l,headerDepth:t}):[],nh=Symbol(""),v8=()=>{const e=ve(),t=ce(),n=Mt(),l=Pd(),r=k(()=>e.value.home?!1:e.value.sidebar??t.value.sidebar??"structure"),i=k(()=>e.value.headerDepth??t.value.headerDepth??2),a=k(()=>p8({config:r.value,headerDepth:i.value,routeLocale:n.value,routePath:l.value}));Ot(nh,a)},bo=()=>{const e=Se(nh);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},eu=(e,t)=>e===!1?e:nr(e)?{...e,link:$r(e.link,!0,t).link}:ke(e)?$r(e,!0,t):null,_a=(e,t,n)=>{const l=e.findIndex(i=>i.link===t);if(l!==-1){const i=e[l+n];return i?i.link?i:"prefix"in i&&!wt(i.prefix).notFound?{...i,link:i.prefix}:null:null}for(const i of e)if("children"in i){const a=_a(i.children,t,n);if(a)return a}const r=e.findIndex(i=>"prefix"in i&&i.prefix===t);if(r!==-1){const i=e[r+n];return i?i.link?i:"prefix"in i&&!wt(i.prefix).notFound?{...i,link:i.prefix}:null:null}return null},m8=()=>{const e=ve(),t=ce(),n=bo(),l=Ut(),r=k(()=>{const a=eu(e.value.prev,l.path);return a===!1?null:a??(t.value.prevLink===!1?null:_a(n.value,l.path,-1))}),i=k(()=>{const a=eu(e.value.next,l.path);return a===!1?null:a??(t.value.nextLink===!1?null:_a(n.value,l.path,1))});return{prevLink:r,nextLink:i}},g8="719px",E8="1440px",_8="true",b8="9",_n={mobileBreakPoint:g8,pcBreakPoint:E8,enableThemeColor:_8,"theme-1":"#2196f3","theme-2":"#f26d6d","theme-3":"#3eaf7c","theme-4":"#fb9b5f",colorNumber:b8},{mobileBreakPoint:y8,pcBreakPoint:A8}=_n,tu=e=>e.endsWith("px")?Number(e.slice(0,-2)):null,cr=()=>{const e=ee(!1),t=ee(!1),n=()=>{e.value=window.innerWidth<=(tu(y8)??719),t.value=window.innerWidth>=(tu(A8)??1440)};return Ie("resize",n,!1),Ie("orientationchange",n,!1),we(()=>{n()}),{isMobile:e,isPC:t}},lh=Symbol(""),ur=()=>{const e=Se(lh);if(!e)throw new Error("useDarkmode() is called without provider.");return e},k8=e=>{const t=tn(),n=Pv(),l=k(()=>t.value.darkmode??"switch"),r=Xa("vuepress-theme-hope-scheme","auto"),i=k(()=>{const o=l.value;return o==="disable"?!1:o==="enable"?!0:o==="auto"?n.value:o==="toggle"?r.value==="dark":r.value==="dark"||r.value==="auto"&&n.value}),a=k(()=>{const o=l.value;return o==="switch"||o==="toggle"});e.provide(lh,{canToggle:a,config:l,isDarkmode:i,status:r}),Object.defineProperties(e.config.globalProperties,{$isDarkmode:{get:()=>i.value}})},w8=()=>{const{config:e,isDarkmode:t,status:n}=ur();za(()=>{e.value==="disable"?n.value="light":e.value==="enable"?n.value="dark":e.value==="toggle"&&n.value==="auto"&&(n.value="light")}),we(()=>{pe(t,l=>document.documentElement.setAttribute("data-theme",l?"dark":"light"),{immediate:!0})})},B8=H({name:"PageFooter",setup(){const e=tn(),t=ce(),n=ve(),l=eh(),r=k(()=>{const{copyright:u,footer:d}=n.value;return d!==!1&&!!(u||d||t.value.displayFooter)}),i=k(()=>{const{footer:u}=n.value;return u===!1?!1:ke(u)?u:t.value.footer??""}),a=k(()=>l.value.map(({name:u})=>u).join(", ")),o=u=>`Copyright © ${new Date().getFullYear()} ${a.value} ${u?`${u} Licensed`:""}`,c=k(()=>{const{copyright:u,license:d=""}=n.value,{license:f}=e.value,{copyright:h}=t.value;return u??(d?o(d):h??(a.value||f?o(f):!1))});return()=>r.value?s("footer",{class:"vp-footer-wrapper","vp-footer":""},[i.value?s("div",{class:"vp-footer",innerHTML:i.value}):null,c.value?s("div",{class:"vp-copyright",innerHTML:c.value}):null]):null}}),Or=()=>null,it=H({name:"AutoLink",props:{config:{type:Object,required:!0}},emits:["focusout"],slots:Object,setup(e,{emit:t,slots:n}){return()=>{const{icon:l}=e.config;return s(cv,{...e,onFocusout:()=>t("focusout")},{default:n.default?()=>n.default():null,before:n.before?()=>n.before():l?()=>s(qe,{icon:l}):null,after:n.after?()=>n.after():null})}}}),C8=H({name:"NavbarDropdown",props:{config:{type:Object,required:!0}},slots:Object,setup(e,{slots:t}){const n=Oe(),l=sl(e,"config"),r=k(()=>l.value.ariaLabel??l.value.text),i=ee(!1);pe(()=>n.value.path,()=>{i.value=!1});const a=o=>{o.detail===0&&(i.value=!i.value)};return()=>{var o;return s("div",{class:["vp-dropdown-wrapper",{open:i.value}]},[s("button",{type:"button",class:"vp-dropdown-title","aria-label":r.value,onClick:a},[((o=t.title)==null?void 0:o.call(t))||[s(qe,{icon:l.value.icon}),e.config.text],s("span",{class:"arrow"}),s("ul",{class:"vp-dropdown"},l.value.children.map((c,u)=>{const d=u===l.value.children.length-1;return s("li",{class:"vp-dropdown-item"},"children"in c?[s("h4",{class:"vp-dropdown-subtitle"},c.link?s(it,{config:c,onFocusout:()=>{c.children.length===0&&d&&(i.value=!1)}}):c.text),s("ul",{class:"vp-dropdown-subitems"},c.children.map((f,h)=>s("li",{class:"vp-dropdown-subitem"},s(it,{config:f,onFocusout:()=>{h===c.children.length-1&&d&&(i.value=!1)}}))))]:s(it,{config:c,onFocusout:()=>{d&&(i.value=!1)}}))}))])])}}}),rh=(e,t="")=>ke(e)?$r(xn(t,e)):"children"in e?{...e,...e.link&&va(e.link)?{link:wt(xn(t,e.link)).path}:{},children:e.children.map(n=>rh(n,xn(t,e.prefix)))}:{...e,link:va(e.link)?wt(xn(t,e.link)).path:e.link},ih=()=>{const e=ce();return k(()=>(e.value.navbar||[]).map(t=>rh(t)))},x8=()=>{const e=ce(),t=k(()=>e.value.repo),n=k(()=>t.value?l8(t.value):null),l=k(()=>t.value?mo(t.value):null),r=k(()=>n.value?e.value.repoLabel??l.value??"Source":null);return k(()=>!n.value||!r.value||e.value.repoDisplay===!1?null:{type:l.value??"Source",label:r.value,link:n.value})},S8=H({name:"NavScreenMenu",props:{config:{type:Object,required:!0}},setup(e){const t=Oe(),n=sl(e,"config"),l=k(()=>n.value.ariaLabel??n.value.text),r=ee(!1);pe(()=>t.value.path,()=>{r.value=!1});const i=(a,o)=>o[o.length-1]===a;return()=>[s("button",{type:"button",class:["vp-nav-screen-menu-title",{active:r.value}],"aria-label":l.value,onClick:()=>{r.value=!r.value}},[s("span",{class:"text"},[s(qe,{icon:n.value.icon}),e.config.text]),s("span",{class:["arrow",r.value?"down":"end"]})]),s("ul",{class:["vp-nav-screen-menu",{hide:!r.value}]},n.value.children.map(a=>s("li",{class:"vp-nav-screen-menu-item"},"children"in a?[s("h4",{class:"vp-nav-screen-menu-subtitle"},a.link?s(it,{config:a,onFocusout:()=>{i(a,n.value.children)&&a.children.length===0&&(r.value=!1)}}):a.text),s("ul",{class:"vp-nav-screen-menu-subitems"},a.children.map(o=>s("li",{class:"vp-nav-screen-menu-subitem"},s(it,{config:o,onFocusout:()=>{i(o,a.children)&&i(a,n.value.children)&&(r.value=!1)}}))))]:s(it,{config:a,onFocusout:()=>{i(a,n.value.children)&&(r.value=!1)}}))))]}}),T8=H({name:"NavScreenLinks",setup(){const e=ih();return()=>e.value.length?s("nav",{class:"nav-screen-links"},e.value.map(t=>s("div",{class:"navbar-links-item"},"children"in t?s(S8,{config:t}):s(it,{config:t})))):null}}),ah=()=>s(me,{name:"dark"},()=>s("path",{d:"M524.8 938.667h-4.267a439.893 439.893 0 0 1-313.173-134.4 446.293 446.293 0 0 1-11.093-597.334A432.213 432.213 0 0 1 366.933 90.027a42.667 42.667 0 0 1 45.227 9.386 42.667 42.667 0 0 1 10.24 42.667 358.4 358.4 0 0 0 82.773 375.893 361.387 361.387 0 0 0 376.747 82.774 42.667 42.667 0 0 1 54.187 55.04 433.493 433.493 0 0 1-99.84 154.88 438.613 438.613 0 0 1-311.467 128z"}));ah.displayName="DarkIcon";const oh=()=>s(me,{name:"light"},()=>s("path",{d:"M952 552h-80a40 40 0 0 1 0-80h80a40 40 0 0 1 0 80zM801.88 280.08a41 41 0 0 1-57.96-57.96l57.96-58a41.04 41.04 0 0 1 58 58l-58 57.96zM512 752a240 240 0 1 1 0-480 240 240 0 0 1 0 480zm0-560a40 40 0 0 1-40-40V72a40 40 0 0 1 80 0v80a40 40 0 0 1-40 40zm-289.88 88.08-58-57.96a41.04 41.04 0 0 1 58-58l57.96 58a41 41 0 0 1-57.96 57.96zM192 512a40 40 0 0 1-40 40H72a40 40 0 0 1 0-80h80a40 40 0 0 1 40 40zm30.12 231.92a41 41 0 0 1 57.96 57.96l-57.96 58a41.04 41.04 0 0 1-58-58l58-57.96zM512 832a40 40 0 0 1 40 40v80a40 40 0 0 1-80 0v-80a40 40 0 0 1 40-40zm289.88-88.08 58 57.96a41.04 41.04 0 0 1-58 58l-57.96-58a41 41 0 0 1 57.96-57.96z"}));oh.displayName="LightIcon";const sh=()=>s(me,{name:"auto"},()=>s("path",{d:"M512 992C246.92 992 32 777.08 32 512S246.92 32 512 32s480 214.92 480 480-214.92 480-480 480zm0-840c-198.78 0-360 161.22-360 360 0 198.84 161.22 360 360 360s360-161.16 360-360c0-198.78-161.22-360-360-360zm0 660V212c165.72 0 300 134.34 300 300 0 165.72-134.28 300-300 300z"}));sh.displayName="AutoIcon";const ch=()=>s(me,{name:"enter-fullscreen"},()=>s("path",{d:"M762.773 90.24h-497.28c-96.106 0-174.4 78.293-174.4 174.4v497.28c0 96.107 78.294 174.4 174.4 174.4h497.28c96.107 0 175.04-78.293 174.4-174.4V264.64c0-96.213-78.186-174.4-174.4-174.4zm-387.2 761.173H215.04c-21.867 0-40.427-17.92-41.067-41.066V649.92c0-22.507 17.92-40.427 40.427-40.427 11.307 0 21.227 4.694 28.48 11.947 7.253 7.253 11.947 17.92 11.947 28.48v62.293l145.28-145.28c15.893-15.893 41.813-15.893 57.706 0 15.894 15.894 15.894 41.814 0 57.707l-145.28 145.28h62.294c22.506 0 40.426 17.92 40.426 40.427s-17.173 41.066-39.68 41.066zM650.24 165.76h160.427c21.866 0 40.426 17.92 41.066 41.067v160.426c0 22.507-17.92 40.427-40.426 40.427-11.307 0-21.227-4.693-28.48-11.947-7.254-7.253-11.947-17.92-11.947-28.48v-62.186L625.6 450.347c-15.893 15.893-41.813 15.893-57.707 0-15.893-15.894-15.893-41.814 0-57.707l145.28-145.28H650.88c-22.507 0-40.427-17.92-40.427-40.427s17.174-41.173 39.787-41.173z"}));ch.displayName="EnterFullScreenIcon";const uh=()=>s(me,{name:"cancel-fullscreen"},()=>s("path",{d:"M778.468 78.62H247.922c-102.514 0-186.027 83.513-186.027 186.027V795.08c0 102.514 83.513 186.027 186.027 186.027h530.432c102.514 0 186.71-83.513 186.026-186.027V264.647C964.494 162.02 880.981 78.62 778.468 78.62zM250.88 574.35h171.122c23.324 0 43.122 19.115 43.804 43.805v171.121c0 24.008-19.114 43.122-43.122 43.122-12.06 0-22.641-5.006-30.378-12.743s-12.743-19.115-12.743-30.379V722.83L224.597 877.91c-16.953 16.952-44.6 16.952-61.553 0-16.953-16.954-16.953-44.602 0-61.554L318.009 661.39h-66.446c-24.007 0-43.122-19.114-43.122-43.122 0-24.12 18.432-43.918 42.439-43.918zm521.899-98.873H601.657c-23.325 0-43.122-19.114-43.805-43.804V260.55c0-24.007 19.115-43.122 43.122-43.122 12.06 0 22.642 5.007 30.379 12.743s12.743 19.115 12.743 30.38v66.445l154.965-154.965c16.953-16.953 44.601-16.953 61.554 0 16.953 16.953 16.953 44.6 0 61.554L705.536 388.55h66.446c24.007 0 43.122 19.115 43.122 43.122.114 24.007-18.318 43.804-42.325 43.804z"}));uh.displayName="CancelFullScreenIcon";const dh=()=>s(me,{name:"outlook"},()=>[s("path",{d:"M224 800c0 9.6 3.2 44.8 6.4 54.4 6.4 48-48 76.8-48 76.8s80 41.6 147.2 0 134.4-134.4 38.4-195.2c-22.4-12.8-41.6-19.2-57.6-19.2C259.2 716.8 227.2 761.6 224 800zM560 675.2l-32 51.2c-51.2 51.2-83.2 32-83.2 32 25.6 67.2 0 112-12.8 128 25.6 6.4 51.2 9.6 80 9.6 54.4 0 102.4-9.6 150.4-32l0 0c3.2 0 3.2-3.2 3.2-3.2 22.4-16 12.8-35.2 6.4-44.8-9.6-12.8-12.8-25.6-12.8-41.6 0-54.4 60.8-99.2 137.6-99.2 6.4 0 12.8 0 22.4 0 12.8 0 38.4 9.6 48-25.6 0-3.2 0-3.2 3.2-6.4 0-3.2 3.2-6.4 3.2-6.4 6.4-16 6.4-16 6.4-19.2 9.6-35.2 16-73.6 16-115.2 0-105.6-41.6-198.4-108.8-268.8C704 396.8 560 675.2 560 675.2zM224 419.2c0-28.8 22.4-51.2 51.2-51.2 28.8 0 51.2 22.4 51.2 51.2 0 28.8-22.4 51.2-51.2 51.2C246.4 470.4 224 448 224 419.2zM320 284.8c0-22.4 19.2-41.6 41.6-41.6 22.4 0 41.6 19.2 41.6 41.6 0 22.4-19.2 41.6-41.6 41.6C339.2 326.4 320 307.2 320 284.8zM457.6 208c0-12.8 12.8-25.6 25.6-25.6 12.8 0 25.6 12.8 25.6 25.6 0 12.8-12.8 25.6-25.6 25.6C470.4 233.6 457.6 220.8 457.6 208zM128 505.6C128 592 153.6 672 201.6 736c28.8-60.8 112-60.8 124.8-60.8-16-51.2 16-99.2 16-99.2l316.8-422.4c-48-19.2-99.2-32-150.4-32C297.6 118.4 128 291.2 128 505.6zM764.8 86.4c-22.4 19.2-390.4 518.4-390.4 518.4-22.4 28.8-12.8 76.8 22.4 99.2l9.6 6.4c35.2 22.4 80 12.8 99.2-25.6 0 0 6.4-12.8 9.6-19.2 54.4-105.6 275.2-524.8 288-553.6 6.4-19.2-3.2-32-19.2-32C777.6 76.8 771.2 80 764.8 86.4z"})]);dh.displayName="OutlookIcon";const fh=H({name:"ColorModeSwitch",setup(){const{config:e,isDarkmode:t,status:n}=ur(),l=ot(),r=()=>{e.value==="switch"?n.value={light:"dark",dark:"auto",auto:"light"}[n.value]:n.value=n.value==="light"?"dark":"light"},i=async a=>{if(!(document.startViewTransition&&!window.matchMedia("(prefers-reduced-motion: reduce)").matches&&!l.value)||!a){r();return}const c=a.clientX,u=a.clientY,d=Math.hypot(Math.max(c,innerWidth-c),Math.max(u,innerHeight-u)),f=t.value;await document.startViewTransition(async()=>{r(),await Qt()}).ready,t.value!==f&&document.documentElement.animate({clipPath:t.value?[`circle(${d}px at ${c}px ${u}px)`,`circle(0px at ${c}px ${u}px)`]:[`circle(0px at ${c}px ${u}px)`,`circle(${d}px at ${c}px ${u}px)`]},{duration:400,pseudoElement:t.value?"::view-transition-old(root)":"::view-transition-new(root)"})};return()=>s("button",{type:"button",class:"vp-color-mode-switch",id:"color-mode-switch",onClick:i},[s(sh,{style:{display:n.value==="auto"?"block":"none"}}),s(ah,{style:{display:n.value==="dark"?"block":"none"}}),s(oh,{style:{display:n.value==="light"?"block":"none"}})])}}),L8=H({name:"ColorMode",setup(){const e=ce(),{canToggle:t}=ur(),n=k(()=>e.value.outlookLocales.darkmode);return()=>t.value?s("div",{class:"vp-color-mode"},[s("label",{class:"vp-color-mode-title",for:"color-mode-switch"},n.value),s(fh)]):null}}),Vi="VUEPRESS_THEME_COLOR",O8=H({name:"ThemeColorPicker",props:{themeColor:{type:Object,required:!0}},setup(e){const t=(n="")=>{const l=document.documentElement.classList,r=hn(e.themeColor);if(!n){localStorage.removeItem(Vi),l.remove(...r);return}l.remove(...r.filter(i=>i!==n)),l.add(n),localStorage.setItem(Vi,n)};return we(()=>{const n=localStorage.getItem(Vi);n&&t(n)}),()=>s("ul",{class:"vp-theme-color-picker",id:"theme-color-picker"},[s("li",s("span",{class:"theme-color",onClick:()=>t()})),En(e.themeColor).map(([n,l])=>s("li",s("span",{style:{background:l},onClick:()=>t(n)})))])}}),Zn=_n.enableThemeColor==="true",D8=Zn?mm(En(_n).filter(([e])=>e.startsWith("theme-"))):{},P8=H({name:"ThemeColor",setup(){const e=ce(),t=k(()=>e.value.outlookLocales.themeColor);return()=>Zn?s("div",{class:"vp-theme-color"},[s("label",{class:"vp-theme-color-title",for:"theme-color-picker"},t.value),s(O8,{themeColor:D8})]):null}}),I8=H({name:"ToggleFullScreenButton",setup(){const{isSupported:e,isFullscreen:t,toggle:n}=ti();return()=>e?s("button",{type:"button",id:"full-screen-switch",class:"full-screen",ariaPressed:t.value,onClick:()=>n()},t.value?s(uh):s(ch)):null}}),hh=H({name:"ToggleFullScreenButton",setup(){const e=ce(),{isSupported:t}=ti(),n=k(()=>e.value.outlookLocales.fullscreen);return()=>t?s("div",{class:"full-screen-wrapper"},[s("label",{class:"full-screen-title",for:"full-screen-switch"},n.value),s(I8)]):null}}),ph=H({name:"OutlookSettings",setup(){const e=tn(),t=ot(),n=k(()=>!t.value&&e.value.fullscreen);return()=>s(Rd,()=>[Zn?s(P8):null,s(L8),n.value?s(hh):null])}}),F8=H({name:"NavScreen",props:{show:Boolean},emits:["close"],slots:Object,setup(e,{emit:t,slots:n}){const l=Oe(),{isMobile:r}=cr(),i=$e(),a=eo(i);return we(()=>{i.value=document.body,pe(r,o=>{!o&&e.show&&(a.value=!1,t("close"))}),pe(()=>l.value.path,()=>{a.value=!1,t("close")})}),cl(()=>{a.value=!1}),()=>s(el,{name:"fade",onEnter:()=>{a.value=!0},onAfterLeave:()=>{a.value=!1}},()=>{var o,c;return e.show?s("div",{id:"nav-screen",class:"vp-nav-screen"},s("div",{class:"vp-nav-screen-container"},[(o=n.before)==null?void 0:o.call(n),s(T8),s("div",{class:"vp-outlook-wrapper"},s(ph)),(c=n.after)==null?void 0:c.call(n)])):null})}}),R8=H({name:"NavbarBrand",setup(){const e=Mt(),t=lr(),n=ce(),l=k(()=>n.value.home??e.value),r=k(()=>t.value.title),i=k(()=>n.value.navbarTitle??r.value),a=k(()=>n.value.logo?Ae(n.value.logo):null),o=k(()=>n.value.logoDark?Ae(n.value.logoDark):null);return()=>s(He,{to:l.value,class:"vp-brand","aria-label":n.value.routeLocales.home},()=>[a.value?s("img",{class:["vp-nav-logo",{light:!!o.value}],src:a.value,alt:""}):null,o.value?s("img",{class:["vp-nav-logo dark"],src:o.value,alt:""}):null,i.value?s("span",{class:["vp-site-name",{"hide-in-pad":a.value&&(n.value.hideSiteNameOnMobile??!0)}]},i.value):null])}}),M8=H({name:"NavbarLinks",setup(){const e=ih();return()=>e.value.length?s("nav",{class:"vp-nav-links"},e.value.map(t=>s("div",{class:"vp-nav-item hide-in-mobile"},"children"in t?s(C8,{config:t}):s(it,{config:t})))):null}}),V8=H({name:"RepoLink",setup(){const e=x8();return()=>e.value?s("div",{class:"vp-nav-item vp-action"},s("a",{class:"vp-action-link",href:e.value.link,target:"_blank",rel:"noopener noreferrer","aria-label":e.value.label},s(qf,{type:e.value.type,style:{width:"1.25rem",height:"1.25rem",verticalAlign:"middle"}}))):null}}),vh=({active:e=!1},{emit:t})=>s("button",{type:"button",class:["vp-toggle-navbar-button",{"is-active":e}],"aria-label":"Toggle Navbar","aria-expanded":e,"aria-controls":"nav-screen",onClick:()=>t("toggle")},s("span",[s("span",{class:"vp-top"}),s("span",{class:"vp-middle"}),s("span",{class:"vp-bottom"})]));vh.displayName="ToggleNavbarButton";const yo=(e,{emit:t})=>s("button",{type:"button",class:"vp-toggle-sidebar-button",title:"Toggle Sidebar",onClick:()=>t("toggle")},s("span",{class:"icon"}));yo.displayName="ToggleSidebarButton";yo.emits=["toggle"];const N8=H({name:"OutlookButton",setup(){const e=tn(),t=Oe(),{canToggle:n}=ur(),{isSupported:l}=ti(),r=ot(),i=ee(!1),a=k(()=>!r.value&&e.value.fullscreen&&l),o=k(()=>Zn||n.value||a.value);return pe(()=>t.value.path,()=>{i.value=!1}),()=>o.value?s("div",{class:"vp-nav-item hide-in-mobile"},n.value&&!a.value&&!Zn?s(fh):a.value&&!n.value&&!Zn?s(hh):s("button",{type:"button",class:["vp-outlook-button",{open:i.value}],tabindex:"-1","aria-hidden":!0},[s(dh),s("div",{class:"vp-outlook-dropdown"},s(ph))])):null}}),j8=H({name:"NavBar",emits:["toggleSidebar"],slots:Object,setup(e,{emit:t,slots:n}){const l=ce(),{isMobile:r}=cr(),i=ee(!1),a=k(()=>{const{navbarAutoHide:d="mobile"}=l.value;return d!=="none"&&(d==="always"||r.value)}),o=k(()=>l.value.navbarLayout??{start:["Brand"],center:["Links"],end:["Language","Repo","Outlook","Search"]}),c={Brand:R8,Language:Or,Links:M8,Repo:V8,Outlook:N8,Search:Ft("SearchBox")?kt("SearchBox"):Or},u=d=>c[d]??(Ft(d)?kt(d):Or);return()=>{var d,f,h,p,g,v,y,_,A;return[s("header",{key:"navbar",id:"navbar",class:["vp-navbar",{"auto-hide":a.value}],"vp-navbar":""},[s("div",{class:"vp-navbar-start"},[s(yo,{onToggle:()=>{i.value&&(i.value=!1),t("toggleSidebar")}}),(d=n.startBefore)==null?void 0:d.call(n),(f=o.value.start)==null?void 0:f.map(m=>s(u(m))),(h=n.startAfter)==null?void 0:h.call(n)]),s("div",{class:"vp-navbar-center"},[(p=n.centerBefore)==null?void 0:p.call(n),(g=o.value.center)==null?void 0:g.map(m=>s(u(m))),(v=n.centerAfter)==null?void 0:v.call(n)]),s("div",{class:"vp-navbar-end"},[(y=n.endBefore)==null?void 0:y.call(n),(_=o.value.end)==null?void 0:_.map(m=>s(u(m))),(A=n.endAfter)==null?void 0:A.call(n),s(vh,{active:i.value,onToggle:()=>{i.value=!i.value}})])]),s(F8,{show:i.value,onClose:()=>{i.value=!1}},{before:n.screenTop?()=>n.screenTop():null,after:n.screenBottom?()=>n.screenBottom():null})]}}}),H8=H({name:"SidebarChild",props:{config:{type:Object,required:!0}},setup(e){const t=Ut();return()=>ke(e.config.link)?s(it,{class:["vp-sidebar-link",{active:go(t,e.config)}],config:{...e.config,exact:!0}}):s("p",e,[s(qe,{icon:e.config.icon}),e.config.text])}}),z8=H({name:"SidebarGroup",props:{config:{type:Object,required:!0},open:{type:Boolean,required:!0}},emits:["toggle"],setup(e,{emit:t}){const n=Ut(),l=ee(!1),r=k(()=>Eo(n,e.config)),i=k(()=>go(n,e.config)),a=k(()=>e.open||e.config.expanded&&!l.value);return()=>{const{collapsible:o,children:c=[],icon:u,prefix:d,link:f,text:h}=e.config;return s("section",{class:"vp-sidebar-group"},[s(o?"button":"p",{class:["vp-sidebar-header",{clickable:o||f,exact:i.value,active:r.value}],...o?{type:"button",onClick:()=>{l.value=!0,t("toggle")}}:{}},[s(qe,{icon:u}),f?s(it,{class:"vp-sidebar-title no-external-link-icon",config:{text:h,link:f}}):s("span",{class:"vp-sidebar-title"},h),o?s("span",{class:["vp-arrow",a.value?"down":"end"]}):null]),a.value||!o?s(mh,{key:d,config:c}):null])}}}),mh=H({name:"SidebarLinks",props:{config:{type:Array,required:!0}},setup(e){const t=Ut(),n=ee(-1),l=r=>{n.value=r===n.value?-1:r};return pe(()=>t.path,()=>{const r=e.config.findIndex(i=>Eo(t,i));n.value=r},{immediate:!0,flush:"post"}),()=>s("ul",{class:"vp-sidebar-links"},e.config.map((r,i)=>s("li","children"in r?s(z8,{config:r,open:i===n.value,onToggle:()=>l(i)}):s(H8,{config:r}))))}}),$8=H({name:"SideBar",slots:Object,setup(e,{slots:t}){const n=Ut(),l=bo(),r=$e();return we(()=>{pe(()=>n.hash,i=>{const a=document.querySelector(`.vp-sidebar a.vp-sidebar-link[href="${n.path}${i}"]`);if(!a)return;const{top:o,height:c}=r.value.getBoundingClientRect(),{top:u,height:d}=a.getBoundingClientRect();uo+c&&a.scrollIntoView(!1)},{immediate:!0})}),()=>{var i,a,o;return s("aside",{ref:r,key:"sidebar",id:"sidebar",class:"vp-sidebar","vp-sidebar":""},[(i=t.top)==null?void 0:i.call(t),((a=t.default)==null?void 0:a.call(t))??s(mh,{config:l.value}),(o=t.bottom)==null?void 0:o.call(t)])}}}),Ao=H({name:"CommonWrapper",props:{containerClass:{type:String,default:""},noNavbar:Boolean,noSidebar:Boolean,noToc:Boolean},slots:Object,setup(e,{slots:t}){const n=en(),l=ve(),r=tn(),i=ce(),{isMobile:a,isPC:o}=cr(),c=ot(),[u,d]=ta(!1),[f,h]=ta(!1),p=bo(),g=ee(!1),v=k(()=>e.noNavbar||l.value.navbar===!1||i.value.navbar===!1?!1:!!(i.value.logo??i.value.repo??i.value.navbar)),y=k(()=>e.noSidebar?!1:l.value.sidebar!==!1&&p.value.length!==0&&!l.value.home),_=k(()=>l.value.externalLinkIcon??r.value.externalLinkIcon??!0),A=k(()=>!e.noToc&&!l.value.home&&(l.value.toc??i.value.toc??!0)),m={x:0,y:0},w=R=>{m.x=R.changedTouches[0].clientX,m.y=R.changedTouches[0].clientY},F=R=>{const Z=R.changedTouches[0].clientX-m.x,M=R.changedTouches[0].clientY-m.y;Math.abs(Z)>Math.abs(M)*1.5&&Math.abs(Z)>40&&(Z>0&&m.x<=80?d(!0):d(!1))};let V=0;return Ie("scroll",kv(()=>{const R=window.scrollY;R<=58||R{R||d(!1)}),we(()=>{const R=eo(document.body);pe(u,M=>{R.value=M});const Z=n.afterEach(()=>{d(!1)});cl(()=>{R.value=!1,Z()})}),()=>s(Ft("GlobalEncrypt")?kt("GlobalEncrypt"):vo,()=>s("div",{class:["theme-container",{"hide-navbar":g.value,"no-navbar":!v.value,"sidebar-collapsed":!a.value&&!o.value&&f.value,"sidebar-open":a.value&&u.value,"no-sidebar":!y.value&&!t.sidebar&&!t.sidebarTop&&!t.sidebarBottom,"external-link-icon":_.value,pure:c.value,"has-toc":A.value},e.containerClass,l.value.containerClass??""],"vp-container":"",onTouchStart:w,onTouchEnd:F},[v.value?s(j8,{onToggleSidebar:()=>d()},{startBefore:t.navbarStartBefore?()=>t.navbarStartBefore():null,startAfter:t.navbarStartAfter?()=>t.navbarStartAfter():null,centerBefore:t.navbarCenterBefore?()=>t.navbarCenterBefore():null,centerAfter:t.navbarCenterAfter?()=>t.navbarCenterAfter():null,endBefore:t.navbarEndBefore?()=>t.navbarEndBefore():null,endAfter:t.navbarEndAfter?()=>t.navbarEndAfter():null,screenTop:t.navScreenTop?()=>t.navScreenTop():null,screenBottom:t.navScreenBottom?()=>t.navScreenBottom():null}):null,s(el,{name:"fade"},()=>u.value?s("div",{class:"vp-sidebar-mask",onClick:()=>d(!1)}):null),s(el,{name:"fade"},()=>a.value?null:s("div",{class:"toggle-sidebar-wrapper",onClick:()=>h()},s("span",{class:["arrow",f.value?"end":"start"]}))),s($8,{},{default:t.sidebar?()=>t.sidebar():null,top:t.sidebarTop?()=>t.sidebarTop():null,bottom:t.sidebarBottom?()=>t.sidebarBottom():null}),t.default(),s(B8)]))}}),ba=(e,{slots:t})=>{var f,h;const{bgImage:n,bgImageDark:l,bgImageStyle:r,color:i,description:a,image:o,imageDark:c,header:u,features:d=[]}=e;return s("div",{class:"vp-feature-wrapper"},[n?s("div",{class:["vp-feature-bg",{light:l}],style:[{"background-image":`url(${n})`},r]}):null,l?s("div",{class:"vp-feature-bg dark",style:[{"background-image":`url(${l})`},r]}):null,s("div",{class:"vp-feature",style:i?{color:i}:{}},[((f=t.image)==null?void 0:f.call(t,e))??[o?s("img",{class:["vp-feature-image",{light:c}],src:Ae(o),alt:""}):null,c?s("img",{class:"vp-feature-image dark",src:Ae(c),alt:""}):null],((h=t.info)==null?void 0:h.call(t,e))??[u?s("h2",{class:"vp-feature-header"},u):null,a?s("p",{class:"vp-feature-description",innerHTML:a}):null],d.length?s("div",{class:"vp-features"},d.map(({icon:p,title:g,details:v,link:y})=>{const _=[s("h3",{class:"vp-feature-title"},[s(qe,{icon:p}),s("span",{innerHTML:g})]),s("p",{class:"vp-feature-details",innerHTML:v})];return y?Ka(y)?s("a",{class:"vp-feature-item link",href:y,"aria-label":g,target:"_blank"},_):s(He,{class:"vp-feature-item link",to:y,"aria-label":g},()=>_):s("div",{class:"vp-feature-item"},_)})):null])])};ba.displayName="FeaturePanel";const oe=H({name:"DropTransition",props:{type:{type:String,default:"single"},delay:{type:Number,default:0},duration:{type:Number,default:.25},appear:Boolean},slots:Object,setup(e,{slots:t}){const n=r=>{r.style.transition=`transform ${e.duration}s ease-in-out ${e.delay}s, opacity ${e.duration}s ease-in-out ${e.delay}s`,r.style.transform="translateY(-20px)",r.style.opacity="0"},l=r=>{r.style.transform="translateY(0)",r.style.opacity="1"};return()=>{const r={name:"drop",appear:e.appear,onAppear:n,onAfterAppear:l,onEnter:n,onAfterEnter:l,onBeforeLeave:n},i=()=>t.default();return e.type==="group"?s(gd,r,i):s(el,r,i)}}}),gh=(e,{slots:t})=>s(el,{name:"fade-slide-y",mode:"out-in",onBeforeEnter:ma.resolve,onBeforeLeave:ma.pending},()=>t.default());gh.displayName="FadeSlideY";const U8=H({name:"HeroInfo",slots:Object,setup(e,{slots:t}){const n=ve(),l=lr(),r=k(()=>n.value.heroFullScreen??!1),i=k(()=>{const{heroText:u,tagline:d}=n.value;return{text:u??l.value.title??"Hello",tagline:d??l.value.description??"",isFullScreen:r.value}}),a=k(()=>{const{heroText:u,heroImage:d,heroImageDark:f,heroAlt:h,heroImageStyle:p}=n.value;return{image:d?Ae(d):null,imageDark:f?Ae(f):null,imageStyle:p,alt:h??u??"",isFullScreen:r.value}}),o=k(()=>{const{bgImage:u,bgImageDark:d,bgImageStyle:f}=n.value;return{image:ke(u)?Ae(u):null,imageDark:ke(d)?Ae(d):null,bgStyle:f,isFullScreen:r.value}}),c=k(()=>n.value.actions??[]);return()=>{var u,d,f;return s("header",{class:["vp-hero-info-wrapper",{fullscreen:r.value}]},[((u=t.bg)==null?void 0:u.call(t,o.value))??[o.value.image?s("div",{class:["vp-hero-mask",{light:o.value.imageDark}],style:[{"background-image":`url(${o.value.image})`},o.value.bgStyle]}):null,o.value.imageDark?s("div",{class:"vp-hero-mask dark",style:[{"background-image":`url(${o.value.imageDark})`},o.value.bgStyle]}):null],s("div",{class:"vp-hero-info"},[((d=t.logo)==null?void 0:d.call(t,a.value))??s(oe,{appear:!0,type:"group"},()=>{const{image:h,imageDark:p,imageStyle:g,alt:v}=a.value;return[h?s("img",{key:"light",class:["vp-hero-image",{light:p}],style:g,src:h,alt:v}):null,p?s("img",{key:"dark",class:"vp-hero-image dark",style:g,src:p,alt:v}):null]}),((f=t.info)==null?void 0:f.call(t,i.value))??s("div",{class:"vp-hero-infos"},[i.value.text?s(oe,{appear:!0,delay:.04},()=>s("h1",{id:"main-title",class:"vp-hero-title"},i.value.text)):null,i.value.tagline?s(oe,{appear:!0,delay:.08},()=>s("p",{id:"main-description",innerHTML:i.value.tagline})):null,c.value.length?s(oe,{appear:!0,delay:.12},()=>s("p",{class:"vp-hero-actions"},c.value.map(h=>s(it,{class:["vp-hero-action",h.type??"default","no-external-link-icon"],config:h},h.icon?{before:()=>s(qe,{icon:h.icon})}:{})))):null])])])}}}),Eh=(e,{slots:t})=>{var h,p,g;const{bgImage:n,bgImageDark:l,bgImageStyle:r,color:i,description:a,image:o,imageDark:c,header:u,highlights:d=[],type:f="un-order"}=e;return s("div",{class:"vp-highlight-wrapper",style:i?{color:i}:{}},[n?s("div",{class:["vp-highlight-bg",{light:l}],style:[{"background-image":`url(${n})`},r]}):null,l?s("div",{class:"vp-highlight-bg dark",style:[{"background-image":`url(${l})`},r]}):null,s("div",{class:"vp-highlight"},[((h=t.image)==null?void 0:h.call(t,e))??[o?s("img",{class:["vp-highlight-image",{light:c}],src:Ae(o),alt:""}):null,c?s("img",{class:"vp-highlight-image dark",src:Ae(c),alt:""}):null],((p=t.info)==null?void 0:p.call(t,e))??[s("div",{class:"vp-highlight-info-wrapper"},s("div",{class:"vp-highlight-info"},[u?s("h2",{class:"vp-highlight-header",innerHTML:u}):null,a?s("p",{class:"vp-highlight-description",innerHTML:a}):null,((g=t.highlights)==null?void 0:g.call(t,d))??s(f==="order"?"ol":f==="no-order"?"dl":"ul",{class:"vp-highlights"},d.map(({icon:v,title:y,details:_,link:A})=>{const m=[s(f==="no-order"?"dt":"h3",{class:"vp-highlight-title"},[v?s(qe,{class:"vp-highlight-icon",icon:v}):null,s("span",{innerHTML:y})]),_?s(f==="no-order"?"dd":"p",{class:"vp-highlight-details",innerHTML:_}):null];return s(f==="no-order"?"div":"li",{class:["vp-highlight-item-wrapper",{link:A}]},A?Ka(A)?s("a",{class:"vp-highlight-item link",href:A,"aria-label":y,target:"_blank"},m):s(He,{class:"vp-highlight-item link",to:A,"aria-label":y},()=>m):s("div",{class:"vp-highlight-item"},m))}))]))]])])};Eh.displayName="HighlightPanel";const hl=({custom:e})=>s(Md,{class:["theme-hope-content",{custom:e}],"vp-content":""});hl.displayName="MarkdownContent";hl.props={custom:Boolean};const G8=H({name:"HomePage",slots:Object,setup(e,{slots:t}){const n=ve(),l=k(()=>{const{features:i}=n.value;return ra(i)?i:null}),r=k(()=>{const{highlights:i}=n.value;return ra(i)?i:null});return()=>{var i,a,o,c;return s("main",{id:"main-content",class:"vp-page vp-project-home","aria-labelledby":n.value.heroText===null?"":"main-title"},[(i=t.top)==null?void 0:i.call(t),s(U8),((a=r.value)==null?void 0:a.map(u=>"features"in u?s(ba,u):s(Eh,u)))??(l.value?s(oe,{appear:!0,delay:.24},()=>s(ba,{features:l.value})):null),(o=t.center)==null?void 0:o.call(t),s(oe,{appear:!0,delay:.32},()=>s(hl)),(c=t.bottom)==null?void 0:c.call(t)])}}}),K8=H({name:"BreadCrumb",setup(){const e=Oe(),t=Mt(),n=ve(),l=ce(),r=$e([]),i=k(()=>(n.value.breadcrumb??l.value.breadcrumb??!0)&&r.value.length>1),a=k(()=>n.value.breadcrumbIcon??l.value.breadcrumbIcon??!0),o=()=>{const c=f8(e.value.path,t.value).map(({link:u,name:d})=>{const{path:f,meta:h,notFound:p}=wt(u);return p||h[Qe.breadcrumbExclude]?null:{title:h[Qe.shortTitle]||h[Qe.title]||d,icon:h[Qe.icon],path:f}}).filter(u=>u!==null);c.length>1&&(r.value=c)};return we(()=>{pe(()=>e.value.path,o,{immediate:!0})}),()=>s("nav",{class:["vp-breadcrumb",{disable:!i.value}]},i.value?s("ol",{vocab:"https://schema.org/",typeof:"BreadcrumbList"},r.value.map((c,u)=>s("li",{class:{"is-active":r.value.length-1===u},property:"itemListElement",typeof:"ListItem"},[s(He,{to:c.path,property:"item",typeof:"WebPage"},()=>[a.value?s(qe,{icon:c.icon}):null,s("span",{property:"name"},c.title||"Unknown")]),s("meta",{property:"position",content:u+1})]))):[])}}),q8=H({name:"PageNav",setup(){const e=ce(),t=Rn(),{prevLink:n,nextLink:l}=m8();return Ie("keydown",r=>{r.altKey&&(r.key==="ArrowRight"?l.value&&(t(l.value.link),r.preventDefault()):r.key==="ArrowLeft"&&n.value&&(t(n.value.link),r.preventDefault()))}),()=>n.value||l.value?s("nav",{class:"vp-page-nav"},[n.value?s(it,{class:"prev",config:n.value},()=>{var r,i;return[s("div",{class:"hint"},[s("span",{class:"arrow start"}),e.value.metaLocales.prev]),s("div",{class:"link"},[s(qe,{icon:(r=n.value)==null?void 0:r.icon}),(i=n.value)==null?void 0:i.text])]}):null,l.value?s(it,{class:"next",config:l.value},()=>{var r,i;return[s("div",{class:"hint"},[e.value.metaLocales.next,s("span",{class:"arrow end"})]),s("div",{class:"link"},[(r=l.value)==null?void 0:r.text,s(qe,{icon:(i=l.value)==null?void 0:i.icon})])]}):null]):null}}),_h=()=>s(me,{name:"author"},()=>s("path",{d:"M649.6 633.6c86.4-48 147.2-144 147.2-249.6 0-160-128-288-288-288s-288 128-288 288c0 108.8 57.6 201.6 147.2 249.6-121.6 48-214.4 153.6-240 288-3.2 9.6 0 19.2 6.4 25.6 3.2 9.6 12.8 12.8 22.4 12.8h704c9.6 0 19.2-3.2 25.6-12.8 6.4-6.4 9.6-16 6.4-25.6-25.6-134.4-121.6-240-243.2-288z"}));_h.displayName="AuthorIcon";const bh=()=>s(me,{name:"calendar"},()=>s("path",{d:"M716.4 110.137c0-18.753-14.72-33.473-33.472-33.473-18.753 0-33.473 14.72-33.473 33.473v33.473h66.993v-33.473zm-334.87 0c0-18.753-14.72-33.473-33.473-33.473s-33.52 14.72-33.52 33.473v33.473h66.993v-33.473zm468.81 33.52H716.4v100.465c0 18.753-14.72 33.473-33.472 33.473a33.145 33.145 0 01-33.473-33.473V143.657H381.53v100.465c0 18.753-14.72 33.473-33.473 33.473a33.145 33.145 0 01-33.473-33.473V143.657H180.6A134.314 134.314 0 0046.66 277.595v535.756A134.314 134.314 0 00180.6 947.289h669.74a134.36 134.36 0 00133.94-133.938V277.595a134.314 134.314 0 00-133.94-133.938zm33.473 267.877H147.126a33.145 33.145 0 01-33.473-33.473c0-18.752 14.72-33.473 33.473-33.473h736.687c18.752 0 33.472 14.72 33.472 33.473a33.145 33.145 0 01-33.472 33.473z"}));bh.displayName="CalendarIcon";const yh=()=>s(me,{name:"category"},()=>s("path",{d:"M148.41 106.992h282.176c22.263 0 40.31 18.048 40.31 40.31V429.48c0 22.263-18.047 40.31-40.31 40.31H148.41c-22.263 0-40.311-18.047-40.311-40.31V147.302c0-22.263 18.048-40.31 40.311-40.31zM147.556 553.478H429.73c22.263 0 40.311 18.048 40.311 40.31v282.176c0 22.263-18.048 40.312-40.31 40.312H147.555c-22.263 0-40.311-18.049-40.311-40.312V593.79c0-22.263 18.048-40.311 40.31-40.311zM593.927 106.992h282.176c22.263 0 40.31 18.048 40.31 40.31V429.48c0 22.263-18.047 40.31-40.31 40.31H593.927c-22.263 0-40.311-18.047-40.311-40.31V147.302c0-22.263 18.048-40.31 40.31-40.31zM730.22 920.502H623.926c-40.925 0-74.22-33.388-74.22-74.425V623.992c0-41.038 33.387-74.424 74.425-74.424h222.085c41.038 0 74.424 33.226 74.424 74.067v114.233c0 10.244-8.304 18.548-18.547 18.548s-18.548-8.304-18.548-18.548V623.635c0-20.388-16.746-36.974-37.33-36.974H624.13c-20.585 0-37.331 16.747-37.331 37.33v222.086c0 20.585 16.654 37.331 37.126 37.331H730.22c10.243 0 18.547 8.304 18.547 18.547 0 10.244-8.304 18.547-18.547 18.547z"}));yh.displayName="CategoryIcon";const Ah=()=>s(me,{name:"print"},()=>s("path",{d:"M819.2 364.8h-44.8V128c0-17.067-14.933-32-32-32H281.6c-17.067 0-32 14.933-32 32v236.8h-44.8C145.067 364.8 96 413.867 96 473.6v192c0 59.733 49.067 108.8 108.8 108.8h44.8V896c0 17.067 14.933 32 32 32h460.8c17.067 0 32-14.933 32-32V774.4h44.8c59.733 0 108.8-49.067 108.8-108.8v-192c0-59.733-49.067-108.8-108.8-108.8zM313.6 160h396.8v204.8H313.6V160zm396.8 704H313.6V620.8h396.8V864zM864 665.6c0 25.6-19.2 44.8-44.8 44.8h-44.8V588.8c0-17.067-14.933-32-32-32H281.6c-17.067 0-32 14.933-32 32v121.6h-44.8c-25.6 0-44.8-19.2-44.8-44.8v-192c0-25.6 19.2-44.8 44.8-44.8h614.4c25.6 0 44.8 19.2 44.8 44.8v192z"}));Ah.displayName="PrintIcon";const kh=()=>s(me,{name:"tag"},()=>s("path",{d:"M939.902 458.563L910.17 144.567c-1.507-16.272-14.465-29.13-30.737-30.737L565.438 84.098h-.402c-3.215 0-5.726 1.005-7.634 2.913l-470.39 470.39a10.004 10.004 0 000 14.164l365.423 365.424c1.909 1.908 4.42 2.913 7.132 2.913s5.223-1.005 7.132-2.913l470.39-470.39c2.01-2.11 3.014-5.023 2.813-8.036zm-240.067-72.121c-35.458 0-64.286-28.828-64.286-64.286s28.828-64.285 64.286-64.285 64.286 28.828 64.286 64.285-28.829 64.286-64.286 64.286z"}));kh.displayName="TagIcon";const wh=()=>s(me,{name:"timer"},()=>s("path",{d:"M799.387 122.15c4.402-2.978 7.38-7.897 7.38-13.463v-1.165c0-8.933-7.38-16.312-16.312-16.312H256.33c-8.933 0-16.311 7.38-16.311 16.312v1.165c0 5.825 2.977 10.874 7.637 13.592 4.143 194.44 97.22 354.963 220.201 392.763-122.204 37.542-214.893 196.511-220.2 389.397-4.661 5.049-7.638 11.651-7.638 19.03v5.825h566.49v-5.825c0-7.379-2.849-13.981-7.509-18.9-5.049-193.016-97.867-351.985-220.2-389.527 123.24-37.67 216.446-198.453 220.588-392.892zM531.16 450.445v352.632c117.674 1.553 211.787 40.778 211.787 88.676H304.097c0-48.286 95.149-87.382 213.728-88.676V450.445c-93.077-3.107-167.901-81.297-167.901-177.093 0-8.803 6.99-15.793 15.793-15.793 8.803 0 15.794 6.99 15.794 15.793 0 80.261 63.69 145.635 142.01 145.635s142.011-65.374 142.011-145.635c0-8.803 6.99-15.793 15.794-15.793s15.793 6.99 15.793 15.793c0 95.019-73.789 172.82-165.96 177.093z"}));wh.displayName="TimerIcon";const Bh=()=>s(me,{name:"word"},()=>[s("path",{d:"M518.217 432.64V73.143A73.143 73.143 0 01603.43 1.097a512 512 0 01419.474 419.474 73.143 73.143 0 01-72.046 85.212H591.36a73.143 73.143 0 01-73.143-73.143z"}),s("path",{d:"M493.714 566.857h340.297a73.143 73.143 0 0173.143 85.577A457.143 457.143 0 11371.566 117.76a73.143 73.143 0 0185.577 73.143v339.383a36.571 36.571 0 0036.571 36.571z"})]);Bh.displayName="WordIcon";const W8=()=>{const e=ce(),t=Oe(),n=ve();return k(()=>{var r;return n.value.contributors??e.value.contributors??!0?((r=t.value.git)==null?void 0:r.contributors)??null:null})},J8={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},Y8=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:l,editLinkPattern:r})=>{if(!l)return null;const i=mo(e);let a;return r?a=r:i!==null&&(a=J8[i]),a?a.replace(/:repo/u,gn(e)?e:`https://github.com/${e}`).replace(/:branch/u,t).replace(/:path/u,bd(`${Zr(n)}/${l}`)):null},Z8=()=>{const e=ce(),t=Oe(),n=ve();return k(()=>{const{repo:l,docsRepo:r=l,docsBranch:i="main",docsDir:a="",editLink:o,editLinkPattern:c=""}=e.value;if(!(n.value.editLink??o??!0)||!r)return null;const d=Y8({docsRepo:r,docsBranch:i,docsDir:a,editLinkPattern:c,filePathRelative:t.value.filePathRelative});return d?{text:e.value.metaLocales.editLink,link:d}:null})},bn=()=>{const e=ce();return k(()=>e.value.metaLocales)},Q8=()=>{const e=lr(),t=ce(),n=Oe(),l=ve();return k(()=>{var a,o;return!(l.value.lastUpdated??t.value.lastUpdated??!0)||!((a=n.value.git)!=null&&a.updatedTime)?null:new Date((o=n.value.git)==null?void 0:o.updatedTime).toLocaleString(e.value.lang)})},X8=H({name:"AuthorInfo",inheritAttrs:!1,props:{author:{type:Array,required:!0}},setup(e){const t=bn(),n=ot();return()=>e.author.length?s("span",{class:"page-author-info","aria-label":`${t.value.author}${n.value?"":"🖊"}`,...n.value?{}:{"data-balloon-pos":"up"}},[s(_h),s("span",e.author.map(l=>l.url?s("a",{class:"page-author-item",href:l.url,target:"_blank",rel:"noopener noreferrer"},l.name):s("span",{class:"page-author-item"},l.name))),s("span",{property:"author",content:e.author.map(l=>l.name).join(", ")})]):null}}),e6=H({name:"CategoryInfo",inheritAttrs:!1,props:{category:{type:Array,required:!0}},setup(e){const t=bn(),n=Rn(),l=ot();return()=>e.category.length?s("span",{class:"page-category-info","aria-label":`${t.value.category}${l.value?"":"🌈"}`,...l.value?{}:{"data-balloon-pos":"up"}},[s(yh),e.category.map(({name:r,path:i})=>s("span",{class:["page-category-item",{[`color${sr(r,Number(_n.colorNumber))}`]:!l.value,clickable:i}],role:i?"navigation":"",onClick:()=>{i&&n(i)}},r)),s("meta",{property:"articleSection",content:e.category.map(({name:r})=>r).join(",")})]):null}}),t6=H({name:"DateInfo",inheritAttrs:!1,props:{date:{type:Object,default:null},localizedDate:{type:String,default:""}},setup(e){const t=ei(),n=bn(),l=ot();return()=>e.date?s("span",{class:"page-date-info","aria-label":`${n.value.date}${l.value?"":"📅"}`,...l.value?{}:{"data-balloon-pos":"up"}},[s(bh),s("span",{"data-allow-mismatch":"text"},e.localizedDate||e.date.toLocaleDateString(t.value)),s("meta",{property:"datePublished",content:e.date.toISOString()||""})]):null}}),n6=H({name:"OriginalInfo",inheritAttrs:!1,props:{isOriginal:Boolean},setup(e){const t=bn();return()=>e.isOriginal?s("span",{class:"page-original-info"},t.value.origin):null}}),l6=H({name:"ReadingTimeInfo",inheritAttrs:!1,props:{readingTime:{type:Object,default:()=>null},readingTimeLocale:{type:Object,default:()=>null}},setup(e){const t=bn(),n=ot(),l=k(()=>{if(!e.readingTime)return null;const{minutes:r}=e.readingTime;return r<1?"PT1M":`PT${Math.round(r)}M`});return()=>{var r,i;return(r=e.readingTimeLocale)!=null&&r.time?s("span",{class:"page-reading-time-info","aria-label":`${t.value.readingTime}${n.value?"":"⌛"}`,...n.value?{}:{"data-balloon-pos":"up"}},[s(wh),s("span",(i=e.readingTimeLocale)==null?void 0:i.time),s("meta",{property:"timeRequired",content:l.value})]):null}}}),r6=H({name:"TagInfo",inheritAttrs:!1,props:{tag:{type:Array,default:()=>[]}},setup(e){const t=bn(),n=Rn(),l=ot();return()=>e.tag.length?s("span",{class:"page-tag-info","aria-label":`${t.value.tag}${l.value?"":"🏷"}`,...l.value?{}:{"data-balloon-pos":"up"}},[s(kh),e.tag.map(({name:r,path:i})=>s("span",{class:["page-tag-item",{[`color${sr(r,Number(_n.colorNumber))}`]:!l.value,clickable:i}],role:i?"navigation":"",onClick:()=>{i&&n(i)}},r)),s("meta",{property:"keywords",content:e.tag.map(({name:r})=>r).join(",")})]):null}}),i6=H({name:"ReadTimeInfo",inheritAttrs:!1,props:{readingTime:{type:Object,default:()=>null},readingTimeLocale:{type:Object,default:()=>null}},setup(e){const t=bn(),n=ot();return()=>{var l,r,i;return(l=e.readingTimeLocale)!=null&&l.words?s("span",{class:"page-word-info","aria-label":`${t.value.words}${n.value?"":"🔠"}`,...n.value?{}:{"data-balloon-pos":"up"}},[s(Bh),s("span",(r=e.readingTimeLocale)==null?void 0:r.words),s("meta",{property:"wordCount",content:(i=e.readingTime)==null?void 0:i.words})]):null}}}),Ch=H({name:"PageInfo",components:{AuthorInfo:X8,CategoryInfo:e6,DateInfo:t6,OriginalInfo:n6,PageViewInfo:Or,ReadingTimeInfo:l6,TagInfo:r6,WordInfo:i6},props:{items:{type:[Array,Boolean],default:()=>["Author","Original","Date","PageView","ReadingTime","Category","Tag"]},info:{type:Object,required:!0}},setup(e){const t=ot();return()=>e.items?s("div",{class:"page-info"},e.items.map(n=>s(kt(`${n}Info`),{...e.info,isPure:t.value}))):null}}),a6=H({name:"PageTitle",setup(){const e=Oe(),t=ve(),n=ce(),{info:l,items:r}=d8();return()=>s("div",{class:"vp-page-title"},[s("h1",[n.value.titleIcon===!1?null:s(qe,{icon:t.value.icon}),e.value.title]),s(Ch,{info:l.value,...r.value===null?{}:{items:r.value}}),s("hr")])}}),xh=()=>s(me,{name:"edit"},()=>[s("path",{d:"M430.818 653.65a60.46 60.46 0 0 1-50.96-93.281l71.69-114.012 7.773-10.365L816.038 80.138A60.46 60.46 0 0 1 859.225 62a60.46 60.46 0 0 1 43.186 18.138l43.186 43.186a60.46 60.46 0 0 1 0 86.373L588.879 565.55l-8.637 8.637-117.466 68.234a60.46 60.46 0 0 1-31.958 11.229z"}),s("path",{d:"M728.802 962H252.891A190.883 190.883 0 0 1 62.008 771.98V296.934a190.883 190.883 0 0 1 190.883-192.61h267.754a60.46 60.46 0 0 1 0 120.92H252.891a69.962 69.962 0 0 0-69.098 69.099V771.98a69.962 69.962 0 0 0 69.098 69.098h475.911A69.962 69.962 0 0 0 797.9 771.98V503.363a60.46 60.46 0 1 1 120.922 0V771.98A190.883 190.883 0 0 1 728.802 962z"})]);xh.displayName="EditIcon";const o6=H({name:"PageMeta",setup(){const e=ce(),t=Z8(),n=Q8(),l=W8();return()=>{var i;const{metaLocales:r}=e.value;return s("footer",{class:"vp-page-meta"},[t.value?s("div",{class:"vp-meta-item edit-link"},s(it,{class:"vp-meta-label",config:t.value},{before:()=>s(xh)})):null,s("div",{class:"vp-meta-item git-info"},[n.value?s("div",{class:"update-time"},[s("span",{class:"vp-meta-label"},`${r.lastUpdated}: `),s("span",{class:"vp-meta-info","data-allow-mismatch":"text"},n.value)]):null,(i=l.value)!=null&&i.length?s("div",{class:"contributors"},[s("span",{class:"vp-meta-label"},`${r.contributors}: `),l.value.map(({email:a,name:o},c)=>[s("span",{class:"vp-meta-info",title:`email: ${a}`},o),c!==l.value.length-1?",":""])]):null])])}}}),s6=H({name:"PrintButton",setup(){const e=tn(),t=ce();return()=>e.value.print===!1?null:s("button",{type:"button",class:"print-button",title:t.value.metaLocales.print,onClick:()=>{window.print()}},s(Ah))}}),Sh=H({name:"TOC",props:{items:{type:Array,default:()=>[]},headerDepth:{type:Number,default:2}},slots:Object,setup(e,{slots:t}){const n=Ut(),l=Oe(),r=bn(),[i,a]=ta(),o=$e(),c=ee("-1.7rem"),u=f=>{var h;(h=o.value)==null||h.scrollTo({top:f,behavior:"smooth"})},d=()=>{if(o.value){const f=document.querySelector(".vp-toc-item.active");f?c.value=`${f.getBoundingClientRect().top-o.value.getBoundingClientRect().top+o.value.scrollTop}px`:c.value="-1.7rem"}else c.value="-1.7rem"};return we(()=>{pe(()=>n.hash,f=>{if(o.value){const h=document.querySelector(`#toc a.vp-toc-link[href$="${f}"]`);if(!h)return;const{top:p,height:g}=o.value.getBoundingClientRect(),{top:v,height:y}=h.getBoundingClientRect();vp+g&&u(o.value.scrollTop+v+y-p-g)}}),pe(()=>n.fullPath,d,{flush:"post",immediate:!0})}),()=>{var g,v;const f=({title:y,level:_,slug:A})=>s(He,{to:`#${A}`,class:["vp-toc-link",`level${_}`],onClick:()=>{a()}},()=>y),h=(y,_)=>y.length&&_>0?s("ul",{class:"vp-toc-list"},y.map(A=>{const m=h(A.children,_-1);return[s("li",{class:["vp-toc-item",{active:n.hash===`#${A.slug}`}]},f(A)),m?s("li",m):null]})):null,p=e.items.length?h(e.items,e.headerDepth):l.value.headers?h(l.value.headers,e.headerDepth):null;return p?s("div",{class:"vp-toc-placeholder"},[s("aside",{id:"toc","vp-toc":""},[(g=t.before)==null?void 0:g.call(t),s("div",{class:"vp-toc-header",onClick:()=>{a()}},[r.value.toc,s(s6),s("div",{class:["arrow",i.value?"down":"end"]})]),s("div",{class:["vp-toc-wrapper",i.value?"open":""],ref:o},[p,s("div",{class:"vp-toc-marker",style:{top:c.value}})]),(v=t.after)==null?void 0:v.call(t)])]):null}}}),c6=H({name:"NormalPage",slots:Object,setup(e,{slots:t}){const n=ve(),{isDarkmode:l}=ur(),r=ce(),i=k(()=>n.value.toc??r.value.toc??!0),a=k(()=>n.value.headerDepth??r.value.headerDepth??2);return()=>s("main",{id:"main-content",class:"vp-page"},s(Ft("LocalEncrypt")?kt("LocalEncrypt"):vo,()=>{var o,c,u,d;return[(o=t.top)==null?void 0:o.call(t),n.value.cover?s("div",{class:"page-cover"},s("img",{src:Ae(n.value.cover),alt:"","no-view":""})):null,s(K8),s(a6),i.value?s(Sh,{headerDepth:a.value},{before:t.tocBefore?()=>t.tocBefore():null,after:t.tocAfter?()=>t.tocAfter():null}):null,(c=t.contentBefore)==null?void 0:c.call(t),s(hl),(u=t.contentAfter)==null?void 0:u.call(t),s(o6),s(q8),Ft("CommentService")?s(kt("CommentService"),{darkmode:l.value}):null,(d=t.bottom)==null?void 0:d.call(t)]}))}}),u6=(e,t)=>{const n=e.__vccOpts||e;for(const[l,r]of t)n[l]=r;return n},d6={name:"adsense-inline",components:{"normal-page":c6},mounted(){this.adsenseAddLoad()},methods:{adsenseAddLoad(){let e=document.createElement("script");e.type="text/javascript",e.text="(adsbygoogle = window.adsbygoogle || []).push({});",document.getElementsByTagName("body")[0].appendChild(e)},loadBaiduAdAsync(){(window.slotbydup=window.slotbydup||[]).push({id:"u6953633",container:"_vyps37o7ou",async:!0})}}};function f6(e,t,n,l,r,i){const a=kt("normal-page",!0);return U1(),K1(a,null,{contentBefore:$i(()=>t[0]||(t[0]=[Sl("ins",{class:"adsbygoogle",style:{display:"block","text-align":"center",width:"90%",margin:"0 auto"},"data-ad-layout":"in-article","data-ad-format":"fluid","data-ad-client":"ca-pub-9037099208128116","data-ad-slot":"8206550629"},null,-1)])),tocAfter:$i(()=>t[1]||(t[1]=[Sl("div",{class:"slot-demo-block"},"小站收益甚微,请帮忙点击下文章上面的广告(需关闭广告屏蔽插件)也可微信支付打赏鼓励作者 ",-1),Sl("img",{class:"slot-img",src:"https://github-images.wenzhihuai.com/test/image-20240901210440204.png",alt:""},null,-1)])),_:1})}const h6=u6(d6,[["render",f6],["__scopeId","data-v-826d2cba"],["__file","NormalPage.vue"]]),p6=H({name:"PortfolioHero",slots:Object,setup(e,{slots:t}){const n=Nf(),l=ve(),r=ee(0),i=k(()=>{var h;return((h=l.value.titles)==null?void 0:h[r.value])??""}),a=ee(""),o=k(()=>{const{name:h,avatar:p,avatarDark:g,avatarAlt:v,avatarStyle:y}=l.value;return{name:h??n.value.name,avatar:p?Ae(p):null,avatarDark:g?Ae(g):null,avatarStyle:y,alt:(v||h)??""}}),c=k(()=>{const{bgImage:h,bgImageDark:p,bgImageStyle:g}=l.value;return{image:ke(h)?Ae(h):null,imageDark:ke(p)?Ae(p):null,bgStyle:g}}),u=k(()=>{const{welcome:h,name:p,titles:g,medias:v}=l.value;return{name:p??n.value.name,welcome:h??"👋 Hi There, I'm",title:a.value,titles:g??[],medias:v??[]}}),d=()=>{a.value="";let h=0,p=!1;const g=async()=>{if(!p)if(a.value+=i.value[h],h+=1,await Qt(),h{g()},150);else{const v=u.value.titles.length;setTimeout(()=>{r.value=v<=1||r.value===u.value.titles.length-1?0:r.value+1},1e3)}};return g(),()=>{p=!0}};let f;return we(()=>{pe(i,()=>{f==null||f(),f=d()},{immediate:!0})}),()=>{var h,p,g;return s("section",{id:"portfolio",class:["vp-portfolio",{bg:c.value.image}]},[((h=t.bg)==null?void 0:h.call(t,c.value))??[c.value.image?s("div",{class:["vp-portfolio-mask",{light:c.value.imageDark}],style:[{background:`url(${c.value.image}) center/cover no-repeat`},c.value.bgStyle]}):null,c.value.imageDark?s("div",{class:"vp-portfolio-mask dark",style:[{background:`url(${c.value.imageDark}) center/cover no-repeat`},c.value.bgStyle]}):null],((p=t.avatar)==null?void 0:p.call(t,o.value))??s("div",{class:"vp-portfolio-avatar"},[s(oe,{delay:.04},()=>{const{avatarDark:v,name:y,alt:_,avatarStyle:A}=o.value;return[s("img",{key:"light",class:{light:v},src:o.value.avatar,title:y,alt:_,style:A}),v?s("img",{key:"dark",class:"dark",src:v,title:y,alt:_,style:A}):null]})]),s("div",{class:"vp-portfolio-container"},((g=t.info)==null?void 0:g.call(t,u.value))??s("div",{class:"vp-portfolio-info"},[s(oe,{appear:!0,delay:.08},()=>s("h6",{class:"vp-portfolio-welcome"},u.value.welcome)),s(oe,{appear:!0,delay:.12},()=>s("h1",{class:"vp-portfolio-name",id:"main-title"},u.value.name)),s(oe,{appear:!0,delay:.16},()=>s("h2",{class:"vp-portfolio-title"},a.value)),s(oe,{appear:!0,delay:.2},()=>u.value.medias.length?s("div",{class:"vp-portfolio-medias"},u.value.medias.map(({name:v,url:y,icon:_})=>s("a",{class:"vp-portfolio-media",href:y,rel:"noopener noreferrer",target:"_blank",title:v},s(qe,{icon:_})))):Ft("SocialMedias")?s(kt("SocialMedias")):null)]))])}}}),v6=H({name:"PortfolioHome",setup(){const e=ve(),t=k(()=>e.value.content??"portfolio");return()=>s("main",{id:"main-content",class:"vp-page vp-portfolio-home","aria-labelledby":"main-title"},[s(p6),t.value==="none"?null:s("div",{},s(oe,{appear:!0,delay:.24},()=>s(hl,{class:{"vp-portfolio-content":t.value==="portfolio"}})))])}}),ko=H({name:"SkipLink",props:{content:{type:String,default:"main-content"}},setup(e){const t=Oe(),n=ce(),l=$e(),r=({target:i})=>{const a=document.querySelector(i.hash);if(a){const o=()=>{a.removeAttribute("tabindex"),a.removeEventListener("blur",o)};a.setAttribute("tabindex","-1"),a.addEventListener("blur",o),a.focus(),window.scrollTo(0,0)}};return we(()=>{pe(()=>t.value.path,()=>l.value.focus())}),()=>[s("span",{ref:l,tabindex:"-1"}),s("a",{href:`#${e.content}`,class:"vp-skip-link sr-only",onClick:r},n.value.routeLocales.skipToContent)]}}),m6=H({name:"Layout",slots:Object,setup(e,{slots:t}){const n=tn(),l=ce(),r=Oe(),i=ve(),a=ot(),{isMobile:o}=cr(),c=k(()=>{var u,d;return((u=l.value.blog)==null?void 0:u.sidebarDisplay)??((d=n.value.blog)==null?void 0:d.sidebarDisplay)??"mobile"});return()=>[s(ko),s(Ao,{},{default:()=>{var u;return((u=t.default)==null?void 0:u.call(t))??(i.value.portfolio?s(v6):i.value.home?s(G8):s(a.value?vo:gh,()=>s(h6,{key:r.value.path},{top:t.top?()=>t.top():null,bottom:t.bottom?()=>t.bottom():null,contentBefore:t.contentBefore?()=>t.contentBefore():null,contentAfter:t.contentAfter?()=>t.contentAfter():null,tocBefore:t.tocBefore?()=>t.tocBefore():null,tocAfter:t.tocAfter?()=>t.tocAfter():null})))},navScreenBottom:c.value==="none"&&Ft("BloggerInfo")?()=>s(kt("BloggerInfo")):null,sidebar:!o.value&&c.value==="always"&&Ft("BloggerInfo")?()=>s(kt("BloggerInfo")):null})]}}),g6=H({name:"NotFoundHint",setup(){const e=ce(),t=()=>{const n=e.value.routeLocales.notFoundMsg;return n[Math.floor(Math.random()*n.length)]};return()=>s("div",{class:"not-found-hint"},[s("p",{class:"error-code"},"404"),s("h1",{class:"error-title"},e.value.routeLocales.notFoundTitle),s("p",{class:"error-hint"},t())])}}),E6=H({name:"NotFound",slots:Object,setup(e,{slots:t}){const n=en(),l=Mt(),r=ce();return()=>[s(ko),s(Ao,{noSidebar:!0},()=>{var i;return s("main",{id:"main-content",class:"vp-page not-found"},((i=t.default)==null?void 0:i.call(t))??[s(g6),s("div",{class:"actions"},[s("button",{type:"button",class:"action-button",onClick:()=>{window.history.go(-1)}},r.value.routeLocales.back),s("button",{type:"button",class:"action-button",onClick:()=>{n.push(r.value.home??l.value)}},r.value.routeLocales.home)])])})]}}),_6={Zhihu:'',Github:'',Gitee:'',BiliBili:'',WechatPay:''},b6={category:{"/":{path:"/category/",map:{}}},tag:{"/":{path:"/tag/",map:{}}}},Th=["/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html","/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html","/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html","/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html","/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html","/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html","/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html","/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html","/about-the-author/personal-life/2024-07-24.html","/stock/","/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html","/interesting/","/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html","/interesting/idea%E8%AE%BE%E7%BD%AE.html","/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html","/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html","/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html","/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html","/java/%E9%AB%98%E6%80%A7%E8%83%BD.html","/system-design/feed.html","/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html","/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html","/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html","/java/io/nio.html","/middleware/sentinel/springcloud_sentinel.html","/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html","/database/elasticsearch/%E5%9F%BA%E7%A1%80.html","/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html","/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html","/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html","/kubernetes/request_limit.html","/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html","/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html","/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html","/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html","/java/%E7%BA%BF%E7%A8%8B/synchronized.html","/interesting/mini%E4%B8%BB%E6%9C%BA/","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/","/java/JVM/","/java/JVM/cms.html","/java/JVM/g1.html","/java/JVM/zgc.html","/java/SpringBoot/","/java/io/","/java/%E7%BA%BF%E7%A8%8B/","/java/%E7%BA%BF%E7%A8%8B/volatile.html","/java/%E7%BD%91%E7%BB%9C/","/kubernetes/devops/","/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html","/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html","/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html","/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html","/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html","/link/main.html","/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html","/java/SpringBoot/aop.html","/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html","/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html","/java/SpringBoot/webflux.html","/interesting/starcraft-ai.html","/about-the-author/personal-life/wewe.html","/about-the-author/","/middleware/zookeeper/zookeeper.html","/interesting/chatgpt.html","/interesting/tesla.html","/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html","/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html","/database/","/donate/","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html","/open-source-project/","/middleware/","/interview/tiktok2023.html","/database/mysql/1mysql.html","/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html","/kubernetes/spark%20on%20k8s%20operator.html","/bigdata/spark/elastic-spark.html","/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html","/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html","/middleware/kafka/kafka.html","/life/2017.html","/life/2019.html","/java/serverlog.html","/life/2018.html","/database/redis/redis%E7%BC%93%E5%AD%98.html","/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html","/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html","/bigdata/","/cloudnative/","/java/","/kubernetes/","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html","/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html","/kubernetes/devops/devops-ping-tai.html"],y6={article:{"/":{path:"/article/",indexes:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100]}},star:{"/":{path:"/star/",indexes:[]}},timeline:{"/":{path:"/timeline/",indexes:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100]}},slide:{"/":{path:"/slide/",indexes:[]}}},ya=$e(b6);In(ya);const Lh=e=>{const t=Oe(),n=ve(),l=Mt();return k(()=>{var o;const r=e??((o=n.value.blog)==null?void 0:o.key)??"";if(!r)return console.warn("useBlogCategory: key not found"),{path:"/",map:{}};if(!(r in ya.value))throw new Error(`useBlogCategory: key ${r} is invalid`);const i=ya.value[r][l.value],a={path:i.path,map:{}};for(const c in i.map){const u=i.map[c];a.map[c]={path:u.path,items:[]};for(const d of u.indexes){const{path:f,meta:h}=wt(Th[d]);a.map[c].items.push({path:f,info:h})}t.value.path===u.path&&(a.currentItems=a.map[c].items)}return a})},Aa=$e(y6);In(Aa);const oi=e=>{const t=ve(),n=Mt();return k(()=>{var a;const l=e??((a=t.value.blog)==null?void 0:a.key)??"";if(!l)return console.warn("useBlogType: key not found"),{path:"/",items:[]};if(!(l in Aa.value))throw new Error(`useBlogType: key ${e} is invalid`);const r=Aa.value[l][n.value],i={path:r.path,items:[]};for(const o of r.indexes){const{path:c,meta:u}=wt(Th[o]);i.items.push({path:c,info:u})}return i})},Oh=Symbol(""),wo=()=>{const e=Se(Oh);if(!e)throw new Error("useTimeline() is called without provider.");return e},A6=()=>{const e=oi("timeline"),t=ei(),n=k(()=>{const l=[];return e.value.items.forEach(({info:r,path:i})=>{const a=lo(r[Ke.date]);if(a){const o=a.getFullYear();(!l[0]||l[0].year!==o)&&l.unshift({year:o,items:[]}),l[0].items.push({date:a.toLocaleDateString(t.value,{month:"numeric",day:"numeric"}),info:r,path:i})}}),{...e.value,config:l.reverse()}});Ot(Oh,n)},Dh=Symbol(""),dr=()=>{const e=Se(Dh);if(!e)throw new Error("useArticles() is called without provider.");return e},k6=()=>{const e=oi("article");Ot(Dh,e)},Ph=Symbol.for("categoryMap"),fr=()=>{const e=Se(Ph);if(!e)throw new Error("useCategoryMap() is called without provider.");return e},w6=()=>{const e=Lh("category");Ot(Ph,e)},Ih=Symbol(""),Bo=()=>{const e=Se(Ih);if(!e)throw new Error("useStars() is called without provider.");return e},B6=()=>{const e=oi("star");Ot(Ih,e)},Fh=Symbol.for("tagMap"),hr=()=>{const e=Se(Fh);if(!e)throw new Error("useTagMap() is called without provider.");return e},C6=()=>{const e=Lh("tag");Ot(Fh,e)},x6=()=>{k6(),w6(),B6(),C6(),A6()},pr=()=>{const e=tn(),t=ce();return k(()=>({...e.value.blog,...t.value.blog}))},S6=e=>{const t=ce();return k(()=>{const{[Ke.author]:n}=e.value;return n?Kl(n):n===!1?[]:Kl(t.value.author,!1)})},T6=e=>{const t=fr();return k(()=>Qf(e.value[Ke.category]).map(n=>({name:n,path:t.value.map[n].path})))},L6=e=>{const t=hr();return k(()=>Xf(e.value[Ke.tag]).map(n=>({name:n,path:t.value.map[n].path})))},O6=e=>k(()=>{const{[Ke.date]:t}=e.value;return lo(t)}),D6=e=>{const t=sl(e,"info"),n=pr(),l=S6(t),r=T6(t),i=L6(t),a=O6(t),o=zf(),c=k(()=>({author:l.value,category:r.value,date:a.value,localizedDate:t.value[Ke.localizedDate]??"",tag:i.value,isOriginal:t.value[Ke.isOriginal]??!1,readingTime:t.value[Ke.readingTime]??null,readingTimeLocale:t.value[Ke.readingTime]&&o.value?Hf(t.value[Ke.readingTime],o.value):null,pageview:e.path})),u=k(()=>n.value.articleInfo);return{info:c,items:u}},Rh=H({name:"SocialMedias",setup(){const e=pr(),t=ot(),n=k(()=>En(e.value.medias??{}).map(([l,r])=>typeof r=="string"?{name:l,icon:_6[l],link:r}:{name:l,...r}));return()=>n.value.length?s("div",{class:"vp-social-medias"},n.value.map(({name:l,icon:r,link:i})=>s("a",{class:"vp-social-media",href:i,rel:"noopener noreferrer",target:"_blank","aria-label":l||"",...t.value?{}:{"data-balloon-pos":"up"},innerHTML:gn(r)?``:r}))):null}}),Co=H({name:"BloggerInfo",setup(){const e=pr(),t=lr(),n=ce(),l=dr(),r=fr(),i=hr(),a=wo(),o=Rn(),c=k(()=>{var h;return e.value.name??((h=Kl(n.value.author)[0])==null?void 0:h.name)??t.value.title}),u=k(()=>e.value.avatar??n.value.logo),d=k(()=>n.value.blogLocales),f=k(()=>e.value.intro);return()=>{const{article:h,category:p,tag:g,timeline:v}=d.value,y=[[l.value.path,l.value.items.length,h],[r.value.path,hn(r.value.map).length,p],[i.value.path,hn(i.value.map).length,g],[a.value.path,a.value.items.length,v]];return s("div",{class:"vp-blogger-info",vocab:"https://schema.org/",typeof:"Person"},[s("div",{class:"vp-blogger",...f.value?{"aria-label":d.value.intro,"data-balloon-pos":"down",role:"link",onClick:()=>o(f.value)}:{}},[u.value?s("img",{class:"vp-blogger-avatar",src:Ae(u.value),property:"image",alt:"Blogger Avatar",loading:"lazy"}):null,c.value?s("div",{class:"vp-blogger-name",property:"name"},c.value):null,e.value.description?s("div",{class:"vp-blogger-description",innerHTML:e.value.description}):null,f.value?s("meta",{property:"url",content:Ae(f.value)}):null]),s("div",{class:"vp-blog-counts"},y.map(([_,A,m])=>s(He,{class:"vp-blog-count",to:_},()=>[s("div",{class:"count"},A),s("div",m)]))),s(Rh)])}}}),xo=()=>s(me,{name:"category"},()=>s("path",{d:"M148.41 106.992h282.176c22.263 0 40.31 18.048 40.31 40.31V429.48c0 22.263-18.047 40.31-40.31 40.31H148.41c-22.263 0-40.311-18.047-40.311-40.31V147.302c0-22.263 18.048-40.31 40.311-40.31zM147.556 553.478H429.73c22.263 0 40.311 18.048 40.311 40.31v282.176c0 22.263-18.048 40.312-40.31 40.312H147.555c-22.263 0-40.311-18.049-40.311-40.312V593.79c0-22.263 18.048-40.311 40.31-40.311zM593.927 106.992h282.176c22.263 0 40.31 18.048 40.31 40.31V429.48c0 22.263-18.047 40.31-40.31 40.31H593.927c-22.263 0-40.311-18.047-40.311-40.31V147.302c0-22.263 18.048-40.31 40.31-40.31zM730.22 920.502H623.926c-40.925 0-74.22-33.388-74.22-74.425V623.992c0-41.038 33.387-74.424 74.425-74.424h222.085c41.038 0 74.424 33.226 74.424 74.067v114.233c0 10.244-8.304 18.548-18.547 18.548s-18.548-8.304-18.548-18.548V623.635c0-20.388-16.746-36.974-37.33-36.974H624.13c-20.585 0-37.331 16.747-37.331 37.33v222.086c0 20.585 16.654 37.331 37.126 37.331H730.22c10.243 0 18.547 8.304 18.547 18.547 0 10.244-8.304 18.547-18.547 18.547z"}));xo.displayName="CategoryIcon";const So=()=>s(me,{name:"tag"},()=>s("path",{d:"M939.902 458.563L910.17 144.567c-1.507-16.272-14.465-29.13-30.737-30.737L565.438 84.098h-.402c-3.215 0-5.726 1.005-7.634 2.913l-470.39 470.39a10.004 10.004 0 000 14.164l365.423 365.424c1.909 1.908 4.42 2.913 7.132 2.913s5.223-1.005 7.132-2.913l470.39-470.39c2.01-2.11 3.014-5.023 2.813-8.036zm-240.067-72.121c-35.458 0-64.286-28.828-64.286-64.286s28.828-64.285 64.286-64.285 64.286 28.828 64.286 64.285-28.829 64.286-64.286 64.286z"}));So.displayName="TagIcon";const To=()=>s(me,{name:"timeline"},()=>s("path",{d:"M511.997 70.568c-243.797 0-441.429 197.633-441.429 441.435 0 243.797 197.632 441.429 441.43 441.429S953.431 755.8 953.431 512.002c0-243.796-197.637-441.434-441.435-441.434zm150.158 609.093-15.605 15.61c-8.621 8.615-22.596 8.615-31.215 0L472.197 552.126c-4.95-4.944-4.34-14.888-4.34-24.677V247.14c0-12.19 9.882-22.07 22.07-22.07h22.07c12.19 0 22.07 9.882 22.07 22.07v273.218l128.088 128.088c8.62 8.62 8.62 22.595 0 31.215zm0 0"}));To.displayName="TimelineIcon";const Mh=()=>s(me,{name:"slides"},()=>s("path",{d:"M896 170.667v426.666a85.333 85.333 0 0 1-85.333 85.334h-256v61.184l192.597 115.584-43.861 73.13-148.736-89.173v95.275h-85.334v-95.318l-148.736 89.216-43.861-73.13 192.597-115.627v-61.141h-256A85.333 85.333 0 0 1 128 597.333V170.667H85.333V85.333h853.334v85.334H896zm-682.667 0v426.666h597.334V170.667H213.333zM426.667 512h-85.334V341.333h85.334V512zm128 0h-85.334V256h85.334v256zm128 0h-85.334V384h85.334v128z"}));Mh.displayName="SlideIcon";const Vh=()=>s(me,{name:"sticky"},()=>[s("path",{d:"m381.3 733.8l-161.9 118c-5.9 4.5-13.2 6.6-20.1 6.6-8.7 0-17.7-3.4-24.3-10-12.2-12.2-13.9-31.3-3.5-45.2l144.5-195.5-113.6-112.9c-11.1-11.1-13.2-28.4-5.5-42 5.5-8.7 52.1-76.4 155.5-51 1.8 0.3 3.5 0.3 5.6 0.7 4.2 0.3 9 0.7 14.2 1.7 21.9 3.5 60.8-13.9 94.5-42.7 32.3-27.5 53.1-59.4 53.1-81.6 0-5.2 0-10.8-0.3-16-0.7-20.8-2.1-52.8 21.5-76.4 28.1-28.1 72.9-30.6 103.9-5.2 0.6 0.3 1 1 1.7 1.7 16.7 16.3 187.5 187.2 189.3 188.9 14.5 14.6 22.9 34.4 22.9 55.3 0 20.8-8 40.2-22.9 54.8-23.7 23.6-56 22.6-77.1 21.6-4.9 0-10.5-0.4-15.7-0.4-20.8 0-45.8 14.6-70.5 41.3-34.3 37.5-55.5 85.8-53.8 107.7 0.7 6.9 2.1 19.1 2.4 20.8 25 101.4-42.7 147.6-50.7 152.8-13.9 8.4-31.6 6.3-42.7-4.8l-112.1-112.2z"})]);Vh.displayName="StickyIcon";const si=()=>s(me,{name:"article"},()=>s("path",{d:"M853.333 938.667H170.667A42.667 42.667 0 0 1 128 896V128a42.667 42.667 0 0 1 42.667-42.667h682.666A42.667 42.667 0 0 1 896 128v768a42.667 42.667 0 0 1-42.667 42.667zm-42.666-85.334V170.667H213.333v682.666h597.334zM298.667 256h170.666v170.667H298.667V256zm0 256h426.666v85.333H298.667V512zm0 170.667h426.666V768H298.667v-85.333zm256-384h170.666V384H554.667v-85.333z"}));si.displayName="ArticleIcon";const Nh=()=>s(me,{name:"book"},()=>s("path",{d:"M256 853.333h426.667A85.333 85.333 0 0 0 768 768V256a85.333 85.333 0 0 0-85.333-85.333H469.333a42.667 42.667 0 0 1 0-85.334h213.334A170.667 170.667 0 0 1 853.333 256v512a170.667 170.667 0 0 1-170.666 170.667H213.333A42.667 42.667 0 0 1 170.667 896V128a42.667 42.667 0 0 1 42.666-42.667h128A42.667 42.667 0 0 1 384 128v304.256l61.653-41.088a42.667 42.667 0 0 1 47.36 0l61.654 41.045V256A42.667 42.667 0 0 1 640 256v256a42.667 42.667 0 0 1-66.347 35.499l-104.32-69.547-104.32 69.547A42.667 42.667 0 0 1 298.667 512V170.667H256v682.666z"}));Nh.displayName="BookIcon";const jh=()=>s(me,{name:"link"},()=>s("path",{d:"M460.8 584.533c17.067 17.067 17.067 42.667 0 59.734-17.067 17.066-42.667 17.066-59.733 0-85.334-85.334-85.334-217.6 0-302.934L554.667 192C640 110.933 776.533 110.933 857.6 196.267c81.067 81.066 81.067 213.333 0 294.4l-68.267 64c0-34.134-4.266-68.267-17.066-102.4l21.333-21.334c51.2-46.933 55.467-128 4.267-179.2s-128-55.466-179.2-4.266c-4.267 0-4.267 4.266-4.267 4.266L465.067 401.067c-51.2 51.2-51.2 132.266-4.267 183.466m123.733-183.466C601.6 384 627.2 384 644.267 401.067c85.333 85.333 85.333 217.6 0 302.933l-153.6 149.333C405.333 934.4 268.8 934.4 187.733 849.067c-81.066-81.067-81.066-213.334 0-294.4l68.267-64c0 34.133 4.267 72.533 17.067 102.4L251.733 614.4C204.8 665.6 204.8 746.667 256 793.6c51.2 46.933 123.733 46.933 174.933 0l149.334-149.333c51.2-51.2 51.2-128 0-179.2-12.8-17.067-17.067-46.934 4.266-64z"}));jh.displayName="LinkIcon";const Hh=()=>s(me,{name:"project"},()=>s("path",{d:"M987.456 425.152H864V295.296a36.48 36.48 0 0 0-36.544-36.544h-360l-134.08-128.256A9.344 9.344 0 0 0 327.04 128H36.48A36.48 36.48 0 0 0 0 164.544v676.608a36.48 36.48 0 0 0 36.544 36.544h797.76a36.672 36.672 0 0 0 33.92-22.848L1021.44 475.52a36.48 36.48 0 0 0-33.92-50.304zM82.304 210.304h215.424l136.64 130.752h347.328v84.096H198.848A36.672 36.672 0 0 0 164.928 448L82.304 652.8V210.304zM808.32 795.456H108.544l118.08-292.608h699.904L808.32 795.52z"}));Hh.displayName="ProjectIcon";const zh=()=>s(me,{name:"friend"},()=>s("path",{d:"M860.16 213.333A268.373 268.373 0 0 0 512 186.027a267.52 267.52 0 0 0-348.16 404.48L428.8 855.893a118.613 118.613 0 0 0 166.4 0l264.96-265.386a267.52 267.52 0 0 0 0-377.174zM800 531.627l-264.96 264.96a32.427 32.427 0 0 1-46.08 0L224 530.347a183.04 183.04 0 0 1 0-256 182.187 182.187 0 0 1 256 0 42.667 42.667 0 0 0 60.587 0 182.187 182.187 0 0 1 256 0 183.04 183.04 0 0 1 3.413 256z"}));zh.displayName="FriendIcon";const ka=()=>s(me,{name:"slide-down"},()=>s("path",{d:"M108.775 312.23c13.553 0 27.106 3.734 39.153 11.806l375.205 250.338 363.641-252.808c32.587-21.624 76.499-12.83 98.123 19.757 21.685 32.467 12.95 76.56-19.576 98.184l-402.854 278.89c-23.733 15.901-54.694 15.962-78.547.12L69.501 442.097c-32.647-21.685-41.441-65.777-19.817-98.304 13.734-20.54 36.201-31.563 59.09-31.563Z"}));ka.displayName="SlideDownIcon";const $h=()=>s("svg",{xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",class:"empty-icon",viewBox:"0 0 1024 1024",innerHTML:''});$h.displayName="EmptyIcon";const Uh=()=>s(me,{name:"lock"},()=>s("path",{d:"M787.168 952.268H236.832c-30.395 0-55.033-24.638-55.033-55.033V429.45c0-30.395 24.638-55.034 55.033-55.034h82.55V264.35c0-106.38 86.238-192.618 192.618-192.618S704.618 157.97 704.618 264.35v110.066h82.55c30.395 0 55.033 24.639 55.033 55.034v467.785c0 30.395-24.639 55.033-55.033 55.033zM484.483 672.046v115.122h55.034V672.046c31.99-11.373 55.033-41.605 55.033-77.496 0-45.592-36.958-82.55-82.55-82.55s-82.55 36.958-82.55 82.55c0 35.89 23.042 66.123 55.033 77.496zM622.067 264.35c0-60.788-49.28-110.067-110.067-110.067s-110.067 49.28-110.067 110.067v110.066h220.135V264.35z"}));Uh.displayName="LockIcon";const P6=H({name:"ArticleItem",props:{info:{type:Object,required:!0},path:{type:String,required:!0}},slots:Object,setup(e,{slots:t}){const n=sl(e,"info"),{info:l,items:r}=D6(e);return()=>{var h,p,g;const{[Qe.title]:i,[Ke.type]:a,[Ke.isEncrypted]:o=!1,[Ke.cover]:c,[Ke.excerpt]:u,[Ke.sticky]:d}=n.value,f=l.value;return s("div",{class:"vp-article-wrapper"},s("article",{class:"vp-article-item",vocab:"https://schema.org/",typeof:"Article"},[((h=t.cover)==null?void 0:h.call(t,{cover:c}))??(c?[s("img",{class:"vp-article-cover",src:Ae(c),alt:"",loading:"lazy"}),s("meta",{property:"image",content:Ae(c)})]:[]),d?s(Vh):null,s(He,{to:e.path},()=>{var v;return((v=t.title)==null?void 0:v.call(t,{title:i,isEncrypted:o,type:a}))??s("header",{class:"vp-article-title"},[o?s(Uh):null,a===ga.slide?s(Mh):null,s("span",{property:"headline"},i)])}),((p=t.excerpt)==null?void 0:p.call(t,{excerpt:u}))??(u?s("div",{class:"vp-article-excerpt",innerHTML:u}):null),s("hr",{class:"vp-article-hr"}),((g=t.info)==null?void 0:g.call(t,{info:f}))??s(Ch,{info:f,...r.value?{items:r.value}:{}})]))}}}),I6=H({name:"Pagination",props:{total:{type:Number,default:10},perPage:{type:Number,default:10},current:{type:Number,default:1}},emits:["updateCurrentPage"],setup(e,{emit:t}){let n;const l=ce(),r=ee(""),i=k(()=>l.value.paginationLocales),a=k(()=>Math.ceil(e.total/e.perPage)),o=k(()=>!!a.value&&a.value!==1),c=k(()=>a.value<7?!1:e.current>4),u=k(()=>a.value<7?!1:e.current{const{current:p}=e;let g=1,v=a.value;const y=[];a.value>=7&&(p<=4&&p4&&p>=a.value-3?(v=a.value,g=a.value-4):a.value>7&&(g=p-2,v=p+2));for(let _=g;_<=v;_++)y.push(_);return y}),f=p=>t("updateCurrentPage",p),h=p=>{const g=parseInt(p,10);g<=a.value&&g>0?f(g):n.pop(`${i.value.errorText.replace(/\$page/gu,a.value.toString())}`)};return we(()=>{n=new i8}),()=>s("div",{class:"vp-pagination"},o.value?s("nav",{class:"vp-pagination-list"},[s("div",{class:"vp-pagination-number "},[e.current>1?s("div",{class:"prev",role:"navigation",unselectable:"on",onClick:()=>f(e.current-1)},i.value.prev):null,c.value?[s("div",{role:"navigation",onClick:()=>f(1)},1),s("div",{class:"ellipsis"},"...")]:null,d.value.map(p=>s("div",{key:p,class:{active:e.current===p},role:"navigation",onClick:()=>f(p)},p)),u.value?[s("div",{class:"ellipsis"},"..."),s("div",{role:"navigation",onClick:()=>f(a.value)},a.value)]:null,e.currentf(e.current+1)},i.value.next):null]),s("div",{class:"vp-pagination-nav"},[s("label",{for:"navigation-text"},`${i.value.navigate}: `),s("input",{id:"navigation-text",value:r.value,onInput:({target:p})=>{r.value=p.value},onKeydown:p=>{p.key==="Enter"&&(p.preventDefault(),h(r.value))}}),s("button",{class:"vp-pagination-button",type:"button",role:"navigation",title:i.value.action,onClick:()=>h(r.value)},i.value.action)])]):[])}}),Lo=H({name:"ArticleList",props:{items:{type:Array,default:()=>[]}},setup(e){const t=Ut(),n=en(),l=pr(),r=ee(1),i=k(()=>l.value.articlePerPage??10),a=k(()=>e.items.slice((r.value-1)*i.value,r.value*i.value)),o=async c=>{r.value=c;const u={...t.query};!(u.page===c.toString()||c===1&&!u.page)&&(c===1?delete u.page:u.page=c.toString(),await n.push({path:t.path,query:u}))};return we(()=>{const{page:c}=t.query;o(c?Number(c):1),pe(r,()=>{const u=document.querySelector("#article-list").getBoundingClientRect().top+window.scrollY;setTimeout(()=>{window.scrollTo(0,u)},100)})}),()=>s("div",{id:"article-list",class:"vp-article-list",role:"feed"},a.value.length?[...a.value.map(({info:c,path:u},d)=>s(oe,{appear:!0,delay:d*.04},()=>s(P6,{key:u,info:c,path:u}))),s(I6,{current:r.value,perPage:i.value,total:e.items.length,onUpdateCurrentPage:o})]:s($h))}}),Gh=H({name:"CategoryList",setup(){const e=Oe(),t=fr();return()=>s("ul",{class:"vp-category-list"},En(t.value.map).sort(([,n],[,l])=>l.items.length-n.items.length).map(([n,{path:l,items:r}])=>s("li",{class:["vp-category",`color${sr(n,Number(_n.colorNumber))}`,{active:l===e.value.path}]},s(He,{to:l},()=>[n,s("span",{class:"vp-category-count"},r.length)]))))}}),Kh=H({name:"TagList",setup(){const e=ve(),t=hr(),n=l=>{var r;return l===((r=e.value.blog)==null?void 0:r.name)};return()=>s("ul",{class:"vp-tag-list"},En(t.value.map).sort(([,l],[,r])=>r.items.length-l.items.length).map(([l,{path:r,items:i}])=>s("li",{class:["vp-tag",`color${sr(l,Number(_n.colorNumber))}`,{active:n(l)}]},s(He,{to:r},()=>[l,s("span",{class:"vp-tag-count"},i.length)]))))}}),F6=H({name:"TimelineList",setup(){const e=ce(),t=wo(),n=Rn(),l=k(()=>e.value.blogLocales.timeline);return()=>s("div",{class:"timeline-list-wrapper"},[s("div",{class:"timeline-list-title",onClick:()=>n(t.value.path)},[s(To),s("span",{class:"num"},t.value.items.length),l.value]),s("hr"),s("div",{class:"timeline-content"},s("ul",{class:"timeline-list"},t.value.config.map(({year:r,items:i},a)=>s(oe,{appear:!0,delay:.08*(a+1)},()=>s("li",[s("h3",{class:"timeline-year"},r),s("ul",{class:"timeline-year-wrapper"},i.map(({date:o,info:c,path:u})=>s("li",{class:"timeline-item"},[s("span",{class:"timeline-date"},o),s(He,{class:"timeline-title",to:u},()=>c[Qe.title])])))])))))])}}),R6={article:si,category:xo,tag:So,timeline:To},qh=H({name:"InfoList",setup(){const e=ce(),t=dr(),n=fr(),l=k(()=>hn(n.value.map).length),r=Bo(),i=hr(),a=k(()=>hn(i.value.map).length),o=Rn(),c=ee("article"),u=k(()=>e.value.blogLocales);return()=>s("div",{class:"vp-blog-infos"},[s("div",{class:"vp-blog-type-switcher"},En(R6).map(([d,f])=>s("button",{type:"button",class:"vp-blog-type-button",onClick:()=>{c.value=d}},s("div",{class:["vp-blog-type-icon-wrapper",{active:c.value===d}],"aria-label":u.value[d],"data-balloon-pos":"up"},s(f))))),s(oe,()=>c.value==="article"?s("div",{class:"vp-star-article-wrapper"},[s("div",{class:"title",onClick:()=>o(t.value.path)},[s(si),s("span",{class:"num"},t.value.items.length),u.value.article]),s("hr"),r.value.items.length?s("ul",{class:"vp-star-articles"},r.value.items.map(({info:d,path:f},h)=>s(oe,{appear:!0,delay:.08*(h+1)},()=>s("li",{class:"vp-star-article"},s(He,{to:f},()=>d[Qe.title]))))):s("div",{class:"vp-star-article-empty"},u.value.empty.replace("$text",u.value.star))]):c.value==="category"?s("div",{class:"vp-category-wrapper"},[l.value?[s("div",{class:"title",onClick:()=>o(n.value.path)},[s(xo),s("span",{class:"num"},l.value),u.value.category]),s("hr"),s(oe,{delay:.04},()=>s(Gh))]:s("div",{class:"vp-category-empty"},u.value.empty.replace("$text",u.value.category))]):c.value==="tag"?s("div",{class:"vp-tag-wrapper"},[a.value?[s("div",{class:"title",onClick:()=>o(i.value.path)},[s(So),s("span",{class:"num"},a.value),u.value.tag]),s("hr"),s(oe,{delay:.04},()=>s(Kh))]:s("div",{class:"vp-tag-empty"},u.value.empty.replace("$text",u.value.tag))]):s(oe,()=>s(F6)))])}}),ci=H({name:"BlogWrapper",slots:Object,setup(e,{slots:t}){const{isMobile:n}=cr();return()=>[s(ko),s(Ao,{noSidebar:!0,noToc:!0},{default:()=>t.default(),navScreenBottom:()=>s(Co),sidebar:n.value?()=>s(qh):null})]}}),vr=()=>s("aside",{class:"vp-blog-info-wrapper"},[s(oe,()=>s(Co)),s(oe,{delay:.04},()=>s(qh))]);vr.displayName="InfoPanel";const M6=H({name:"BlogPage",setup(){const e=Oe(),t=ve(),n=fr(),l=hr();return()=>{const{key:r="",name:i=""}=t.value.blog||{},a=i?r==="category"?n.value.map[i].items:r==="tag"?l.value.map[i].items:[]:[];return s(ci,()=>s("div",{class:"vp-page vp-blog"},s("div",{class:"blog-page-wrapper"},[s("main",{id:"main-content",class:"vp-blog-main"},[s(oe,()=>r==="category"?s(Gh):r==="tag"?s(Kh):null),i?s(oe,{appear:!0,delay:.24},()=>s(Lo,{key:e.value.path,items:a})):null]),s(oe,{delay:.16},()=>s(vr,{key:"blog"}))])))}}}),V6="//theme-hope-assets.vuejs.press/hero/default.jpg",N6=H({name:"BlogHero",slots:Object,setup(e,{slots:t}){const n=ve(),l=lr(),r=$e(),i=k(()=>n.value.heroFullScreen??!1),a=k(()=>{const{heroText:c,heroImage:u,heroImageDark:d,heroAlt:f,heroImageStyle:h,tagline:p}=n.value;return{text:c??l.value.title??"Hello",tagline:p??"",image:u?Ae(u):null,imageDark:d?Ae(d):null,alt:f??c??"",imageStyle:h,isFullScreen:i.value}}),o=k(()=>{const{bgImage:c,bgImageDark:u,bgImageStyle:d}=n.value;return{image:ke(c)?Ae(c):c===!1?null:V6,imageDark:ke(u)?Ae(u):null,bgStyle:d,isFullScreen:i.value}});return()=>{var c,u;return n.value.hero===!1?null:s("div",{ref:r,class:["vp-blog-hero",{fullscreen:i.value,"no-bg":!o.value.image}]},[((c=t.bg)==null?void 0:c.call(t,o.value))??[o.value.image?s("div",{class:["vp-blog-mask",{light:o.value.imageDark}],style:[{background:`url(${o.value.image}) center/cover no-repeat`},o.value.bgStyle]}):null,o.value.imageDark?s("div",{class:"vp-blog-mask dark",style:[{background:`url(${o.value.imageDark}) center/cover no-repeat`},o.value.bgStyle]}):null],((u=t.info)==null?void 0:u.call(t,a.value))??[s(oe,{appear:!0,type:"group",delay:.04},()=>{const{image:d,imageDark:f,imageStyle:h,alt:p}=a.value;return[d?s("img",{key:"light",class:["vp-blog-hero-image",{light:f}],style:h,src:d,alt:p}):null,f?s("img",{key:"dark",class:"vp-blog-hero-image dark",style:h,src:f,alt:p}):null]}),s(oe,{appear:!0,delay:.08},()=>a.value.text?s("h1",{class:"vp-blog-hero-title"},a.value.text):null),s(oe,{appear:!0,delay:.12},()=>a.value.tagline?s("p",{class:"vp-blog-hero-description",innerHTML:a.value.tagline}):null)],a.value.isFullScreen?s("button",{type:"button",class:"slide-down-button",onClick:()=>{window.scrollTo({top:r.value.clientHeight,behavior:"smooth"})}},[s(ka),s(ka)]):null])}}}),j6=["link","article","book","project","friend"],H6=H({name:"ProjectPanel",components:{ArticleIcon:si,BookIcon:Nh,FriendIcon:zh,LinkIcon:jh,ProjectIcon:Hh},props:{items:{type:Array,required:!0}},setup(e){const t=ot(),n=Rn(),l=(r="",i="icon")=>j6.includes(r)?s(kt(`${r}-icon`)):gn(r)?s("img",{class:"vp-project-image",src:r,alt:i}):ni(r)?s("img",{class:"vp-project-image",src:Ae(r),alt:i}):s(qe,{icon:r});return()=>s("div",{class:"vp-project-panel"},e.items.map(({icon:r,link:i,name:a,desc:o,background:c})=>s("div",{class:["vp-project-card",{[`color${sr(a,Number(_n.colorNumber))}`]:!t.value&&!c}],...c?{style:c}:{},onClick:()=>n(i)},[l(r,a),s("div",{class:"vp-project-name"},a),s("div",{class:"vp-project-desc"},o)])))}}),z6=H({name:"BlogHomePage",setup(){const e=dr(),t=ve(),n=k(()=>t.value.projects??[]);return()=>s("div",{class:"vp-page vp-blog-home"},[s(N6),s("div",{class:"blog-page-wrapper"},[s("main",{id:"main-content",class:"vp-blog-main"},[n.value.length?s(oe,{appear:!0,delay:.16},()=>s(H6,{items:n.value})):null,s(oe,{appear:!0,delay:.24},()=>s(Lo,{items:e.value.items}))]),s(oe,{appear:!0,delay:.16},()=>s(vr,{key:"blog"}))]),s(oe,{appear:!0,delay:.28},()=>s(hl))])}}),Wh=()=>s(ci,()=>s(z6));Wh.displayName="BlogHome";var $6=[{key:"slide",path:"/slide/"}];const U6=H({name:"ArticleType",setup(){const e=Oe(),t=Mt(),n=ce(),l=dr(),r=Bo(),i=k(()=>{const a=n.value.blogLocales;return[{text:a.all,path:l.value.path},{text:a.star,path:r.value.path},...$6.map(({key:o,path:c})=>{const u=c.replace(/^\//,t.value);return{text:a[o]??wt(u).meta[Qe.title]??o,path:u}})]});return()=>s("ul",{class:"vp-article-type-wrapper"},i.value.map(a=>s("li",{class:["vp-article-type",{active:a.path===e.value.path}]},s(He,{to:a.path},()=>a.text))))}}),G6=H({name:"BlogPage",setup(){const e=oi(),t=ve(),n=Oe(),l=dr(),r=Bo(),i=k(()=>{const{key:a="",type:o}=t.value.blog||{};return a==="star"?r.value.items:o==="type"&&a?e.value.items:l.value.items});return()=>s(ci,()=>s("div",{class:"vp-page vp-blog"},s("div",{class:"blog-page-wrapper"},[s("main",{id:"main-content",class:"vp-blog-main"},[s(oe,()=>s(U6)),s(oe,{appear:!0,delay:.24},()=>s(Lo,{key:n.value.path,items:i.value}))]),s(oe,{delay:.16},()=>s(vr,{key:"blog"}))])))}}),K6=H({name:"TimelineItems",setup(){const e=pr(),t=ce(),n=wo(),l=k(()=>e.value.timeline??t.value.blogLocales.timelineTitle),r=k(()=>n.value.config.map(({year:i})=>({title:i.toString(),level:2,slug:i.toString(),children:[]})));return()=>s("div",{class:"timeline-wrapper"},s("ul",{class:"timeline-content"},[s(oe,()=>s("li",{class:"motto"},l.value)),s(Sh,{items:r.value}),n.value.config.map(({year:i,items:a},o)=>s(oe,{appear:!0,delay:.08*(o+1),type:"group"},()=>[s("h3",{key:"title",id:i,class:"timeline-year-title"},s("span",i)),s("li",{key:"content",class:"timeline-year-list"},[s("ul",{class:"timeline-year-wrapper"},a.map(({date:c,info:u,path:d})=>s("li",{class:"timeline-item"},[s("span",{class:"timeline-date"},c),s(He,{class:"timeline-title",to:d},()=>u[Qe.title])])))])]))]))}}),Jh=()=>s(ci,()=>s("div",{class:"vp-page vp-blog"},s("div",{class:"blog-page-wrapper"},[s("main",{id:"main-content",class:"vp-blog-main"},[s(oe,{appear:!0,delay:.24},()=>s(K6))]),s(oe,{delay:.16},()=>s(vr,{key:"blog"}))])));Jh.displayName="Timeline";gm(e=>{const t=e.t,n=e.I!==!1,l=e.i;return n?{title:t,content:l?()=>[s(qe,{icon:l}),t]:null,order:e.O,index:e.I}:null});const q6={enhance:({app:e,router:t})=>{const{scrollBehavior:n}=t.options;t.options.scrollBehavior=async(...l)=>(await ma.wait(),n(...l)),k8(e),e.component("HopeIcon",qe),e.component("BloggerInfo",Co),e.component("SocialMedias",Rh)},setup:()=>{w8(),v8(),x6()},layouts:{Layout:m6,NotFound:E6,BlogCategory:M6,BlogHome:Wh,BlogType:G6,Timeline:Jh}},W6=Object.freeze(Object.defineProperty({__proto__:null,default:q6},Symbol.toStringTag,{value:"Module"}));var J6={provider:"Giscus",comment:!0,repo:"Zephery/MyWebsite",repoId:"MDEwOlJlcG9zaXRvcnkyMDM2MDIyMDQ=",category:"General",categoryId:"DIC_kwDODCK5HM4Ccp32"};const Y6=J6,Z6=ee(Y6),Yh=Symbol(""),Zh=()=>Se(Yh),Q6=Zh,X6=e=>{e.provide(Yh,Z6)},Qh=()=>s("svg",{xmlns:"http://www.w3.org/2000/svg",width:"32",height:"32",preserveAspectRatio:"xMidYMid",viewBox:"0 0 100 100"},[s("circle",{cx:"28",cy:"75",r:"11",fill:"currentColor"},s("animate",{attributeName:"fill-opacity",begin:"0s",dur:"1s",keyTimes:"0;0.2;1",repeatCount:"indefinite",values:"0;1;1"})),s("path",{fill:"none",stroke:"#88baf0","stroke-width":"10",d:"M28 47a28 28 0 0 1 28 28"},s("animate",{attributeName:"stroke-opacity",begin:"0.1s",dur:"1s",keyTimes:"0;0.2;1",repeatCount:"indefinite",values:"0;1;1"})),s("path",{fill:"none",stroke:"#88baf0","stroke-width":"10",d:"M28 25a50 50 0 0 1 50 50"},s("animate",{attributeName:"stroke-opacity",begin:"0.2s",dur:"1s",keyTimes:"0;0.2;1",repeatCount:"indefinite",values:"0;1;1"}))]);Qh.displayName="LoadingIcon";const nu=["ar","ca","da","de","en","eo","es","fa","fr","he","id","it","ja","ko","nl","pl","pt","ro","ru","th","tr","uk","uz","vi","zh-CN","zh-TW"],eE=H({name:"GiscusComment",props:{identifier:{type:String,required:!0},darkmode:Boolean},setup(e){const t=Q6(),n=ei(),l=k(()=>!!(t.value.repo&&t.value.repoId&&t.value.category&&t.value.categoryId)),r=ee(!1),i=k(()=>{if(nu.includes(n.value))return n.value;const o=n.value.split("-")[0];return nu.includes(o)?o:"en"}),a=k(()=>({repo:t.value.repo,repoId:t.value.repoId,category:t.value.category,categoryId:t.value.categoryId,lang:i.value,theme:e.darkmode?t.value.darkTheme||"dark":t.value.lightTheme||"light",mapping:t.value.mapping||"pathname",term:e.identifier,inputPosition:t.value.inputPosition||"top",reactionsEnabled:t.value.reactionsEnabled===!1?"0":"1",strict:t.value.strict===!1?"0":"1",loading:t.value.lazyLoading===!1?"eager":"lazy",emitMetadata:"0"}));return we(async()=>{await C(()=>import("./giscus-BZxmVUME.js"),[]),r.value=!0}),()=>l.value?s("div",{id:"comment",class:["giscus-wrapper",{"input-top":t.value.inputPosition!=="bottom"}]},r.value?s("giscus-widget",a.value):s(Qh)):null}}),tE=H({name:"CommentService",props:{darkmode:Boolean},setup(e){const t=Zh(),n=Oe(),l=ve(),r=k(()=>l.value.comment??t.value.comment!==!1);return()=>s(eE,{class:"vp-comment","vp-comment":"",identifier:l.value.commentID??n.value.path,darkmode:e.darkmode,style:{display:r.value?"block":"none"}})}}),nE=Bt({enhance:({app:e})=>{X6(e),e.component("CommentService",tE)}}),lE=Object.freeze(Object.defineProperty({__proto__:null,default:nE},Symbol.toStringTag,{value:"Module"}));var rE={"/":{source:"源代码"}},iE=H({name:"SiteInfo",props:{name:{type:String,required:!0},desc:{type:String,default:""},logo:{type:String,default:""},url:{type:String,required:!0},preview:{type:String,required:!0},repo:{type:String,default:""}},setup(e){const t=Fn(rE);return()=>s("div",{class:"vp-site-info","data-name":e.name},[s("a",{class:["vp-site-info-navigator","no-external-link-icon"],title:e.name,href:e.url,target:"_blank"}),s("div",{class:"vp-site-info-preview",style:{background:`url(${Ae(e.preview)}) center/cover no-repeat`}}),s("div",{class:"vp-site-info-detail"},[e.logo?s("img",{class:"vp-site-info-logo",src:e.logo,alt:"",loading:"lazy","no-view":""}):null,s("div",{class:"vp-site-info-name"},e.name),s("div",{class:"vp-site-info-desc"},e.desc)]),e.repo?s("div",{class:"vp-site-info-source-wrapper"},s("a",{class:"vp-site-info-source",href:e.repo,"aria-label":t.value.source,"data-balloon-pos":"left",title:t.value.source,target:"_blank"},s(qf,{link:e.repo}))):null])}});const aE="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture",lu=e=>ke(e)?e:`${e}px`,oE=(e,t=0)=>{const n=$e(),l=k(()=>lu(St(e.width)??"100%")),r=ee("auto"),i=c=>{if(ke(c)){const[u,d]=c.split(":"),f=Number(u)/Number(d);if(!Number.isNaN(f))return f}return typeof c=="number"?c:16/9},a=c=>{const u=St(e.height),d=i(St(e.ratio));return u?lu(u):`${Number(c)/d+St(t)}px`},o=()=>{n.value&&(r.value=a(n.value.clientWidth))};return we(()=>{o(),Re(t)&&pe(t,o),Ie("orientationchange",o),Ie("resize",o)}),{el:n,width:l,height:r,resize:o}},ru="https://player.bilibili.com/player.html";var sE=H({name:"BiliBili",props:{bvid:{type:String,default:""},aid:{type:String,default:""},cid:{type:String,default:""},title:{type:String,default:"A BiliBili video"},page:{type:[String,Number],default:1},width:{type:[String,Number],default:"100%"},height:{type:[String,Number],default:void 0},ratio:{type:[String,Number],default:16/9},time:{type:[String,Number],default:0},autoplay:Boolean},setup(e){const{el:t,width:n,height:l,resize:r}=oE(e),i=ee(!1),a=k(()=>{const{aid:o,bvid:c,cid:u,autoplay:d,time:f,page:h}=e;return o&&u?`${ru}?aid=${o}&cid=${u}&t=${f}&autoplay=${d?1:0}&p=${h}`:c?`${ru}?bvid=${c}&t=${f}&autoplay=${d?1:0}`:null});return()=>a.value?[s("div",{class:"bilibili-desc"},s("a",{class:"sr-only",href:a.value},e.title)),s("iframe",{ref:t,src:a.value,title:e.title,class:"bilibili-iframe",allow:aE,style:{width:n.value,height:i.value?l.value:0},onLoad:()=>{i.value=!0,r()}}),i.value?null:s(to)]:[]}});const cE={enhance:({app:e})=>{Ft("SiteInfo")||e.component("SiteInfo",iE),Ft("BiliBili")||e.component("BiliBili",sE)},setup:()=>{},rootComponents:[]},uE=Object.freeze(Object.defineProperty({__proto__:null,default:cE},Symbol.toStringTag,{value:"Module"})),Cr=[fv,hv,Yv,km,xm,Pm,U4,Z4,n3,r3,i3,h3,L3,W3,e8,W6,lE,uE].map(e=>e.default).filter(Boolean),dE=JSON.parse('{"base":"/","lang":"zh-CN","title":"个人博客","description":"个人博客","head":[["script",{"data-ad-client":"ca-pub-9037099208128116","async":true,"src":"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"}],["meta",{"name":"robots","content":"all"}],["meta",{"name":"author","content":"个人博客"}],["meta",{"http-equiv":"Cache-Control","content":"no-cache, no-store, must-revalidate"}],["meta",{"http-equiv":"Pragma","content":"no-cache"}],["meta",{"http-equiv":"Expires","content":"0"}],["meta",{"name":"keywords","content":"Java基础, 多线程, JVM, 虚拟机, 数据库, MySQL, Spring, Redis, MyBatis, 系统设计, 分布式, RPC, 高可用, 高并发"}],["meta",{"name":"description","content":"Java基础, 多线程, JVM, 虚拟机, 数据库, MySQL, Spring, Redis, MyBatis, 系统设计, 分布式, RPC, 高可用, 高并发"}],["meta",{"name":"apple-mobile-web-app-capable","content":"yes"}],["script",{},"var _hmt = _hmt || [];\\n (function() {\\n var hm = document.createElement(\\"script\\");\\n hm.src = \\"https://hm.baidu.com/hm.js?e580b8db831811a4aaf4a8f3e30034dc\\";\\n var s = document.getElementsByTagName(\\"script\\")[0]; \\n s.parentNode.insertBefore(hm, s);\\n })();"],["link",{"rel":"alternate","type":"application/atom+xml","href":"http://www.wenzhihuai.com/atom.xml","title":"个人博客 Atom Feed"}],["link",{"rel":"alternate","type":"application/json","href":"http://www.wenzhihuai.com/feed.json","title":"个人博客 JSON Feed"}],["link",{"rel":"alternate","type":"application/rss+xml","href":"http://www.wenzhihuai.com/rss.xml","title":"个人博客 RSS Feed"}],["link",{"rel":"icon","href":"/favicon.ico"}]],"locales":{}}');var yl=$e(dE),fE=T2,hE=()=>{const e=ev({history:fE(Zr("/")),routes:[{name:"vuepress-route",path:"/:catchAll(.*)",components:{}}],scrollBehavior:(t,n,l)=>l||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{if(t.path!==n.path||n===Wt){const l=wt(t.fullPath);if(l.path!==t.fullPath)return l.path;const r=await l.loader();t.meta={...l.meta,_pageChunk:r}}else t.path===n.path&&(t.meta=n.meta)}),e},pE=e=>{e.component("ClientOnly",Rd),e.component("Content",Md),e.component("RouteLink",He)},vE=(e,t,n)=>{const l=k(()=>t.currentRoute.value.path),r=Cu((y,_)=>({get(){return y(),t.currentRoute.value.meta._pageChunk},set(A){t.currentRoute.value.meta._pageChunk=A,_()}})),i=k(()=>kn.resolveLayouts(n)),a=k(()=>kn.resolveRouteLocale(yl.value.locales,l.value)),o=k(()=>kn.resolveSiteLocaleData(yl.value,a.value)),c=k(()=>r.value.comp),u=k(()=>r.value.data),d=k(()=>u.value.frontmatter),f=k(()=>kn.resolvePageHeadTitle(u.value,o.value)),h=k(()=>kn.resolvePageHead(f.value,d.value,o.value)),p=k(()=>kn.resolvePageLang(u.value,o.value)),g=k(()=>kn.resolvePageLayout(u.value,i.value)),v={layouts:i,pageData:u,pageComponent:c,pageFrontmatter:d,pageHead:h,pageHeadTitle:f,pageLang:p,pageLayout:g,redirects:ea,routeLocale:a,routePath:l,routes:Yn,siteData:yl,siteLocaleData:o};return e.provide(Ja,v),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>d.value},$head:{get:()=>h.value},$headTitle:{get:()=>f.value},$lang:{get:()=>p.value},$page:{get:()=>u.value},$routeLocale:{get:()=>a.value},$site:{get:()=>yl.value},$siteLocale:{get:()=>o.value},$withBase:{get:()=>Ae}}),v},mE=()=>{const e=lv(),t=ei();let n=[];const l=()=>{e.value.forEach(a=>{const o=gE(a);o&&n.push(o)})},r=()=>{const a=[];return e.value.forEach(o=>{const c=EE(o);c&&a.push(c)}),a},i=()=>{document.documentElement.lang=t.value;const a=r();n.forEach((o,c)=>{const u=a.findIndex(d=>o.isEqualNode(d));u===-1?(o.remove(),delete n[c]):a.splice(u,1)}),a.forEach(o=>document.head.appendChild(o)),n=[...n.filter(o=>!!o),...a]};Ot(av,i),we(()=>{l(),pe(e,i,{immediate:!1})})},gE=([e,t,n=""])=>{const l=Object.entries(t).map(([o,c])=>ke(c)?`[${o}=${JSON.stringify(c)}]`:c===!0?`[${o}]`:"").join(""),r=`head > ${e}${l}`;return Array.from(document.querySelectorAll(r)).find(o=>o.innerText===n)||null},EE=([e,t,n])=>{if(!ke(e))return null;const l=document.createElement(e);return nr(t)&&Object.entries(t).forEach(([r,i])=>{ke(i)?l.setAttribute(r,i):i===!0&&l.setAttribute(r,"")}),ke(n)&&l.appendChild(document.createTextNode(n)),l},_E=R0,bE=async()=>{var n;const e=_E({name:"Vuepress",setup(){var i;mE();for(const a of Cr)(i=a.setup)==null||i.call(a);const l=Cr.flatMap(({rootComponents:a=[]})=>a.map(o=>s(o))),r=rv();return()=>[s(r.value),l]}}),t=hE();pE(e),vE(e,t,Cr);for(const l of Cr)await((n=l.enhance)==null?void 0:n.call(l,{app:e,router:t,siteData:yl}));return e.use(t),{app:e,router:t}};bE().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{u6 as _,Sl as a,Ve as b,AE as c,bE as createVueApp,kE as d,U1 as o,kt as r}; diff --git a/assets/chatgpt.html-BgeRWiYw.js b/assets/chatgpt.html-BgeRWiYw.js new file mode 100644 index 00000000..14ee856a --- /dev/null +++ b/assets/chatgpt.html-BgeRWiYw.js @@ -0,0 +1,101 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const h={};function l(t,s){return e(),a("div",null,s[0]||(s[0]=[n(`

微信公众号-chatgpt智能客服搭建

想体验的可以去微信上搜索【旅行的树】公众号。

一、ChatGPT注册

1.1 短信手机号申请

openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。

国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。

国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。

image-20230220172107296

之后再搜索框填openai进行下单购买即可。

img

1.2 云服务器申请

openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。

image-20230221193455384

1.3 ChatGPT注册

购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码

image-20230222104357230

然后回sms查看验证码。

image-20230222104225722

注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。

image-20230220173335691

通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。

image-20230222104316514

二、搭建nginx服务器

跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。

Do not share your API key with interesting, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.

windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。

    location /v1/completions {
+      proxy_pass https://api.openai.com/v1/completions;
+    }

然后使用下面的方法进行调试即可:

POST http://YOUR IP/v1/completions
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+
+{
+  "model": "text-davinci-003",
+  "prompt": "Say this is a test",
+  "max_tokens": 7,
+  "temperature": 0
+}

三、公众号开发

网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。

3.1 微信云托管

微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。

image-20230220192603751

提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。

image-20230220201004069

3.2 一个简单ChatGPT简单回复

微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。

一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。

const configuration = new Configuration({
+    apiKey: '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',
+    basePath: 'http://43.153.15.174/v1'
+});
+const openai = new OpenAIApi(configuration);
+
+async function simpleResponse(prompt) {
+    const completion = await openai.createCompletion({
+        model: 'text-davinci-003',
+        prompt,
+        max_tokens: 1024,
+        temperature: 0.1,
+    });
+    const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();
+    return strip(response, ['\\n', 'A: ']);
+}
+
+app.post("/message/simple", async (req, res) => {
+    console.log('消息推送', req.body)
+    // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用
+    const appid = req.headers['x-wx-from-appid'] || ''
+    const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body
+    console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
+    if (MsgType === 'text') {
+        message = await simpleResponse(Content)
+        res.send({
+            ToUserName: FromUserName,
+            FromUserName: ToUserName,
+            CreateTime: CreateTime,
+            MsgType: 'text',
+            Content: message,
+        })
+    } else {
+        res.send('success')
+    }
+})

本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。

3.3 服务部署

提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。

image-20230220203313790

部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。

image-20230220203711436

这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:

(1)服务有没有正常启动,看日志

(2)端口有没有设置错误,这个很多次没有注意到

image-20230220203445297

保存成功之后,就可以在微信公众号里测试了。

image-20230221134250689

体验还可以

四、没有回复(超时回复问题)

很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。

订阅号的消息推送分几种:

  1. 被动消息回复:指用户给公众号发一条消息,系统接收到后,可以回复一条消息。
  2. 主动回复/客服消息:可以脱离被动消息的5秒超时权限,在48小时内可以主动回复。但需要公众号完成微信认证。

根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html

image-20230221100251825

对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。

1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。

  // 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。
+  await Message.create({
+    fromUser: FromUserName,
+    response: '',
+    request: Content,
+    aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画
+  });

2.抽象一个 chatGPT 请求方法 getAIMessage,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。

 // 成功后,更新记录
+  await Message.update(
+    {
+      response: response,
+      status: MESSAGE_STATUS_ANSWERED,
+    },
+    {
+      where: {
+        fromUser: FromUserName,
+        request: Content,
+      },
+    },
+  );

3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。

  // 找一下,是否已有记录
+  const message = await Message.findOne({
+    where: {
+      fromUser: FromUserName,
+      request: Content,
+    },
+  });
+
+  // 已回答,直接返回消息
+  if (message?.status === MESSAGE_STATUS_ANSWERED) {
+    return \`[GPT]: \${message?.response}\`;
+  }
+
+  // 在回答中
+  if (message?.status === MESSAGE_STATUS_THINKING) {
+    return AI_THINKING_MESSAGE;
+  }

4.最后就是一个 Promise.race

  const message = await Promise.race([
+    // 3秒微信服务器就会超时,超过2.9秒要提示用户重试
+    sleep(2900).then(() => AI_THINKING_MESSAGE),
+    getAIMessage({ Content, FromUserName }),
+  ]);

这样子大概就能实现超时之前返回了。

五、会话保存

掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。

async function buildCtxPrompt({ FromUserName }) {
+  // 获取最近10条对话
+  const messages = await Message.findAll({
+    where: {
+      fromUser: FromUserName,
+      aiType: AI_TYPE_TEXT,
+    },
+    limit: 10,
+    order: [['updatedAt', 'ASC']],
+  });
+  // 只有一条的时候,就不用封装上下文了
+  return messages.length === 1
+    ? messages[0].request
+    : messages
+        .map(({ response, request }) => \`Q: \${request}\\n A: \${response}\`)
+        .join('\\n');
+}

之后就可以实现会话之间的保存通信了。

image-20230218203309437

六、其他问题

6.1 限频

chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。

对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。

6.2 秘钥key被更新

没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。

6.3 为啥和官网的回复不一样

我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧

image-20230221192417900

从官方文档来看,官方服务版的 ChatGPT 的模型并非基础版的text-davinci-003,而是经过了「微调:fine-tunes」。文档地址在这:platform.openai.com/docs/guides…

6.4 玄学挂掉

有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。

image-20230221175150025

参考:https://juejin.cn/post/7200769439335546935

最后

记得去微信关注【旅行的树】公众号体验
代码地址:https://github.com/Zephery/wechat-gpt

`,90)]))}const k=i(h,[["render",l],["__file","chatgpt.html.vue"]]),r=JSON.parse('{"path":"/interesting/chatgpt.html","title":"微信公众号-chatgpt智能客服搭建","lang":"zh-CN","frontmatter":{"description":"微信公众号-chatgpt智能客服搭建 想体验的可以去微信上搜索【旅行的树】公众号。 一、ChatGPT注册 1.1 短信手机号申请 openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。 国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。 国外手机号,没有的话也可以去https:/...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/chatgpt.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"微信公众号-chatgpt智能客服搭建"}],["meta",{"property":"og:description","content":"微信公众号-chatgpt智能客服搭建 想体验的可以去微信上搜索【旅行的树】公众号。 一、ChatGPT注册 1.1 短信手机号申请 openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。 国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。 国外手机号,没有的话也可以去https:/..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"微信公众号-chatgpt智能客服搭建\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、ChatGPT注册","slug":"一、chatgpt注册","link":"#一、chatgpt注册","children":[{"level":3,"title":"1.1 短信手机号申请","slug":"_1-1-短信手机号申请","link":"#_1-1-短信手机号申请","children":[]},{"level":3,"title":"1.2 云服务器申请","slug":"_1-2-云服务器申请","link":"#_1-2-云服务器申请","children":[]},{"level":3,"title":"1.3 ChatGPT注册","slug":"_1-3-chatgpt注册","link":"#_1-3-chatgpt注册","children":[]}]},{"level":2,"title":"二、搭建nginx服务器","slug":"二、搭建nginx服务器","link":"#二、搭建nginx服务器","children":[]},{"level":2,"title":"三、公众号开发","slug":"三、公众号开发","link":"#三、公众号开发","children":[{"level":3,"title":"3.1 微信云托管","slug":"_3-1-微信云托管","link":"#_3-1-微信云托管","children":[]},{"level":3,"title":"3.2 一个简单ChatGPT简单回复","slug":"_3-2-一个简单chatgpt简单回复","link":"#_3-2-一个简单chatgpt简单回复","children":[]},{"level":3,"title":"3.3 服务部署","slug":"_3-3-服务部署","link":"#_3-3-服务部署","children":[]}]},{"level":2,"title":"四、没有回复(超时回复问题)","slug":"四、没有回复-超时回复问题","link":"#四、没有回复-超时回复问题","children":[]},{"level":2,"title":"五、会话保存","slug":"五、会话保存","link":"#五、会话保存","children":[]},{"level":2,"title":"六、其他问题","slug":"六、其他问题","link":"#六、其他问题","children":[{"level":3,"title":"6.1 限频","slug":"_6-1-限频","link":"#_6-1-限频","children":[]},{"level":3,"title":"6.2 秘钥key被更新","slug":"_6-2-秘钥key被更新","link":"#_6-2-秘钥key被更新","children":[]},{"level":3,"title":"6.3 为啥和官网的回复不一样","slug":"_6-3-为啥和官网的回复不一样","link":"#_6-3-为啥和官网的回复不一样","children":[]},{"level":3,"title":"6.4 玄学挂掉","slug":"_6-4-玄学挂掉","link":"#_6-4-玄学挂掉","children":[]}]},{"level":2,"title":"最后","slug":"最后","link":"#最后","children":[]}],"git":{"createdTime":1706186538000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":9.96,"words":2987},"filePathRelative":"interesting/chatgpt.md","localizedDate":"2024年1月25日","excerpt":"\\n

想体验的可以去微信上搜索【旅行的树】公众号。

\\n

一、ChatGPT注册

","autoDesc":true}');export{k as comp,r as data}; diff --git a/assets/cms.html-Dfw5I__q.js b/assets/cms.html-Dfw5I__q.js new file mode 100644 index 00000000..cf7da1d9 --- /dev/null +++ b/assets/cms.html-Dfw5I__q.js @@ -0,0 +1 @@ +import{_ as e,c as o,d as a,o as n}from"./app-ftEjETWs.js";const r={};function p(c,t){return n(),o("div",null,t[0]||(t[0]=[a('

CMS

JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提前触发Full GC。

预测性差:CMS垃圾回收器的暂停时间和CPU资源消耗都很难预测,这可能会对系统的性能造成影响。

维护复杂:CMS垃圾回收器的代码相对复杂,需要更多的维护工作。

',6)]))}const s=e(r,[["render",p],["__file","cms.html.vue"]]),m=JSON.parse('{"path":"/java/JVM/cms.html","title":"CMS","lang":"zh-CN","frontmatter":{"description":"CMS JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。 CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。 内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/cms.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"CMS"}],["meta",{"property":"og:description","content":"CMS JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。 CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。 内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:22:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:22:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"CMS\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T09:22:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1708075355000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.73,"words":218},"filePathRelative":"java/JVM/cms.md","localizedDate":"2024年2月16日","excerpt":"\\n

JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

\\n

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

","autoDesc":true}');export{s as comp,m as data}; diff --git a/assets/devops-ping-tai.html-CjTXGrBr.js b/assets/devops-ping-tai.html-CjTXGrBr.js new file mode 100644 index 00000000..62407a11 --- /dev/null +++ b/assets/devops-ping-tai.html-CjTXGrBr.js @@ -0,0 +1,237 @@ +import{_ as n,c as a,d as e,o as i}from"./app-ftEjETWs.js";const l={};function p(t,s){return i(),a("div",null,s[0]||(s[0]=[e(`

DevOps平台.md

DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。

一、自由风格的软件项目

主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。

完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。

<project>
+    <actions/>
+    <description>xxx</description>
+    <properties>
+        <hudson.model.ParametersDefinitionProperty>
+            <parameterDefinitions>
+                <hudson.model.TextParameterDefinition>
+                    <name>buildParam</name>
+                    <defaultValue>v1</defaultValue>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>codeBranch</name>
+                    <defaultValue>master</defaultValue>
+                </hudson.model.TextParameterDefinition>
+            </parameterDefinitions>
+        </hudson.model.ParametersDefinitionProperty>
+    </properties>
+    <scm class="hudson.plugins.git.GitSCM">
+        <configVersion>2</configVersion>
+        <userRemoteConfigs>
+            <hudson.plugins.git.UserRemoteConfig>
+                <url>http://xxxxx.git</url>
+                <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>
+            </hudson.plugins.git.UserRemoteConfig>
+        </userRemoteConfigs>
+        <branches>
+            <hudson.plugins.git.BranchSpec>
+                <name>\${codeBranch}</name>
+            </hudson.plugins.git.BranchSpec>
+        </branches>
+        <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+        <extensions/>
+    </scm>
+    <builders>
+        <hudson.tasks.Shell>
+            <command>ls</command>
+        </hudson.tasks.Shell>
+        <hudson.tasks.Maven>
+            <targets>clean package install -Dmaven.test.skip=true</targets>
+            <mavenName>mvn3.5.4</mavenName>
+        </hudson.tasks.Maven>
+        <com.cloudbees.dockerpublish.DockerBuilder>
+            <server>
+                <uri>unix:///var/run/docker.sock</uri>
+            </server>
+            <registry>
+                <url>http://xxxx</url>
+            </registry>
+            <repoName>xxx/xx</repoName>
+            <forcePull>true</forcePull>
+            <dockerfilePath>Dockerfile</dockerfilePath>
+            <repoTag>\${buildParam}</repoTag>
+            <skipTagLatest>true</skipTagLatest>
+        </com.cloudbees.dockerpublish.DockerBuilder>
+    </builders>
+    <publishers>
+        <com.xxxx.notifications.Notifier/>
+    </publishers>
+</project>

二、优化之后的CICD

上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。

目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。

没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。

三、调研期

由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。

分布式构建jenkins x 首先要解决的是多个构建同时运行的问题,很久之前就调研过jenkins x,它必须要使用在kubernetes上,由于当时官方文档不全,而且我们的DevOps项目处于初始期,所有没有使用。jenkins的master slave结构就不多说了。jenkins x应该说是个全家桶,包含了helm仓库、nexus仓库、docker registry等,代码是jenkins-x-image

serverless jenkins 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。

阿里云云效 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。

微软Pipeline 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。

谷歌tekton kubernetes的官方cicd,目前已用于kubernetes的release发版过程,目前也仅仅是与GitHub相结合,gitlab无法使用,全过程可使用yaml文件来创建,跑起来就是类似kubernetes的job一样,用完即销毁,可惜目前比较新,依旧处于alpha版本,无法用于生产。有兴趣可以参考下:Knative 初体验:CICD 极速入门

四、产品化后的DevOps平台

在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:

4.1 Java代码扫描

PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。

stage('Clone') {
+    steps{
+        git branch: 'master', credentialsId: 'xxxx', url: "xxx"
+    }
+}
+stage('check') {
+    steps{
+        container('maven') {
+            echo "mvn pmd:pmd"
+        }
+    }
+}

4.2 Java单元测试

Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用"mvn test"命令来代替。


+stage('Clone') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+    }
+}
+stage('test') {
+    steps{
+        container('maven') {
+            sh "mvn test"
+        }
+    }
+}

4.3 Java构建并上传镜像

镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。

为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。


+stage('下载代码') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+        script {
+            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+        }
+    }
+}
+stage('打包并构建镜像') {
+    steps{
+        container('maven') {
+            echo "3.Build Docker Image Stage"
+            sh "mvn clean install -Dmaven.test.skip=true"
+            sh "docker build -f xxx/Dockerfile -t xxxxxx:\${build_tag} ."
+            sh "docker push xxxxxx:\${build_tag}"
+        }
+    }
+}

4.4 部署到阿里云k8s

CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。

def tmp = sh (
+    returnStdout: true,
+    script: "kubectl get deployment -n \${namespace} | grep \${JOB_NAME} | awk '{print \\$1}'"
+)
+//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+if(tmp.equals('')){
+    sh "helm init --client-only"
+    sh """helm repo add mychartmuseum http://xxxxxx \\
+                       --username myuser \\
+                       --password=mypass"""
+    sh """helm install --set name=\${JOB_NAME} \\
+                       --set namespace=\${namespace} \\
+                       --set deployment.image=\${image} \\
+                       --set deployment.imagePullSecrets=\${harborProject} \\
+                       --name \${namespace}-\${JOB_NAME} \\
+                       mychartmuseum/soa-template"""
+}else{
+    println "已经存在,替换镜像"
+    //epaas中一个pod的容器名称需要带上"-0"来区分
+    sh "kubectl set image deployment/\${JOB_NAME} \${JOB_NAME}-0=\${image} -n \${namespace}"
+}

4.5 整体流程

代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署

pipeline:

pipeline {
+    agent {
+        label "jenkins-maven"
+    }
+    stages{
+        stage('代码扫描,单元测试,镜像构建'){
+            parallel {
+                stage('并行任务一') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java代码扫描') {
+                        stage('Clone') {
+                            steps{
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('check') {
+                            steps{
+                                container('maven') {
+                                    echo "$BUILD_NUMBER"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务二') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java单元测试') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('test') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn -v"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务三') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('java构建镜像') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                                script {
+                                    build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+                                }
+                            }
+                        }
+                        stage('Build') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn clean install -Dmaven.test.skip=true"
+                                    sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:\${build_tag} ."
+                                    sh "docker push hub.gcloud.lab/rongqiyun/epaas:\${build_tag}"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        stage('部署'){
+            stages('部署到容器云') {
+                stage('check') {
+                    steps{
+                        container('maven') {
+                            script{
+                                if (deploy_app == "true"){
+                                    def tmp = sh (
+                                        returnStdout: true,
+                                        script: "kubectl get deployment -n \${namespace} | grep \${JOB_NAME} | awk '{print \\$1}'"
+                                    )
+                                    //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+                                    if(tmp.equals('')){
+                                        sh "helm init --client-only"
+                                        sh """helm repo add mychartmuseum http://xxxxxx \\
+                                                           --username myuser \\
+                                                           --password=mypass"""
+                                        sh """helm install --set name=\${JOB_NAME} \\
+                                                           --set namespace=\${namespace} \\
+                                                           --set deployment.image=\${image} \\
+                                                           --set deployment.imagePullSecrets=\${harborProject} \\
+                                                           --name \${namespace}-\${JOB_NAME} \\
+                                                           mychartmuseum/soa-template"""
+                                    }else{
+                                        println "已经存在,替换镜像"
+                                        //epaas中一个pod的容器名称需要带上"-0"来区分
+                                        sh "kubectl set image deployment/\${JOB_NAME} \${JOB_NAME}-0=\${image} -n \${namespace}"
+                                    }
+                                }else{
+                                    println "用户选择不部署代码"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

在jenkins x中查看:

4.4 日志

jenkins blue ocean步骤日志:

云效中的日志:

4.5 定时触发

    triggers {
+        cron('H H * * *') //每天
+    }

五、其他

5.1 Gitlab触发

pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。

六、总结

功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。

DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。

最后,DevOps是云原生的必经之路!!!

文章同步:
博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai

`,61)]))}const c=n(l,[["render",p],["__file","devops-ping-tai.html.vue"]]),r=JSON.parse('{"path":"/kubernetes/devops/devops-ping-tai.html","title":"DevOps平台.md","lang":"zh-CN","frontmatter":{"description":"DevOps平台.md DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。 公司技术部目前几百人左右吧...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"DevOps平台.md"}],["meta",{"property":"og:description","content":"DevOps平台.md DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。 公司技术部目前几百人左右吧..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/201908111044201770944400.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:53:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:53:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"DevOps平台.md\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/201908111044201770944400.png\\",\\"https://github-images.wenzhihuai.com/images/201908170612301464716259.png\\",\\"https://github-images.wenzhihuai.com/images/201908100346011648784131.png\\",\\"https://github-images.wenzhihuai.com/images/201908100429061448084424.png\\",\\"https://github-images.wenzhihuai.com/images/20190915082632234251996.png\\",\\"https://github-images.wenzhihuai.com/images/201908100428501805517052.png\\",\\"https://github-images.wenzhihuai.com/images/20190810043129475121819.png\\",\\"https://github-images.wenzhihuai.com/images/201908100431461365428934.png\\",\\"https://github-images.wenzhihuai.com/images/2019081004491578636427.png\\",\\"https://github-images.wenzhihuai.com/images/201908100524291184838290.png\\"],\\"dateModified\\":\\"2024-02-06T07:53:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、自由风格的软件项目","slug":"一、自由风格的软件项目","link":"#一、自由风格的软件项目","children":[]},{"level":2,"title":"二、优化之后的CICD","slug":"二、优化之后的cicd","link":"#二、优化之后的cicd","children":[]},{"level":2,"title":"三、调研期","slug":"三、调研期","link":"#三、调研期","children":[]},{"level":2,"title":"四、产品化后的DevOps平台","slug":"四、产品化后的devops平台","link":"#四、产品化后的devops平台","children":[{"level":3,"title":"4.1 Java代码扫描","slug":"_4-1-java代码扫描","link":"#_4-1-java代码扫描","children":[]},{"level":3,"title":"4.2 Java单元测试","slug":"_4-2-java单元测试","link":"#_4-2-java单元测试","children":[]},{"level":3,"title":"4.3 Java构建并上传镜像","slug":"_4-3-java构建并上传镜像","link":"#_4-3-java构建并上传镜像","children":[]},{"level":3,"title":"4.4 部署到阿里云k8s","slug":"_4-4-部署到阿里云k8s","link":"#_4-4-部署到阿里云k8s","children":[]},{"level":3,"title":"4.5 整体流程","slug":"_4-5-整体流程","link":"#_4-5-整体流程","children":[]},{"level":3,"title":"4.4 日志","slug":"_4-4-日志","link":"#_4-4-日志","children":[]},{"level":3,"title":"4.5 定时触发","slug":"_4-5-定时触发","link":"#_4-5-定时触发","children":[]}]},{"level":2,"title":"五、其他","slug":"五、其他","link":"#五、其他","children":[{"level":3,"title":"5.1 Gitlab触发","slug":"_5-1-gitlab触发","link":"#_5-1-gitlab触发","children":[]}]},{"level":2,"title":"六、总结","slug":"六、总结","link":"#六、总结","children":[]}],"git":{"createdTime":1566437746000,"updatedTime":1707206019000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":11.96,"words":3589},"filePathRelative":"kubernetes/devops/devops-ping-tai.md","localizedDate":"2019年8月22日","excerpt":"\\n

DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

","autoDesc":true}');export{c as comp,r as data}; diff --git a/assets/elastic-spark.html-Cz2jVT52.js b/assets/elastic-spark.html-Cz2jVT52.js new file mode 100644 index 00000000..ff777585 --- /dev/null +++ b/assets/elastic-spark.html-Cz2jVT52.js @@ -0,0 +1,27 @@ +import{_ as i,c as a,d as n,o as t}from"./app-ftEjETWs.js";const e={};function h(l,s){return t(),a("div",null,s[0]||(s[0]=[n(`

elastic spark

Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
(2)数据RDBMS:mysql、oracle、mssql
(3)NOSQL数据库:HBase、ES、Redis
(4)消息对象:Redis

elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。

Spark - Apache Spark
Spark - Apache Spark

一、原生RDD支持

1.1 基础配置

相关库引入:

        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>elasticsearch-spark-30_2.13</artifactId>
+            <version>8.1.3</version>
+        </dependency>

SparkConf配置,更多详细的请点击这里或者源码ConfigurationOptions

public static SparkConf getSparkConf() {
+    SparkConf sparkConf = new SparkConf().setAppName("elasticsearch-spark-demo");
+    sparkConf.set("es.nodes", "host")
+            .set("es.port", "xxxxxx")
+            .set("es.nodes.wan.only", "true")
+            .set("es.net.http.auth.user", "elxxxxastic")
+            .set("es.net.http.auth.pass", "xxxx")
+            .setMaster("local[*]");
+    return sparkConf;
+}

1.2 读取es数据

这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。

public static void main(String[] args) {
+    SparkConf conf = getSparkConf();
+    try (JavaSparkContext jsc = new JavaSparkContext(conf)) {
+
+        JavaPairRDD<String, Map<String, Object>> esRDD =
+                JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce");
+        esRDD.collect().forEach(System.out::println);
+    }
+}

esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。

1.3 写数据

支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。

public static void jsonWrite(){
+    String json1 = "{\\"reason\\" : \\"business\\",\\"airport\\" : \\"SFO\\"}";
+    String json2 = "{\\"participants\\" : 5,\\"airport\\" : \\"OTP\\"}";
+    JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));
+    JavaEsSpark.saveJsonToEs(stringRDD, "spark-json");
+}

比较常用的读写也就这些,更多可以看下官网相关介绍。

二、Spark Streaming

spark的实时处理,es5.0的时候开始支持,目前

三、Spark SQL

四、Spark Structure Streaming

五、Spark on kubernetes Operator

参考:

1.Apache Spark support

2.elasticsearch-hadoop

3.使用SparkSQL操作Elasticsearch - Spark入门教程

`,27)]))}const p=i(e,[["render",h],["__file","elastic-spark.html.vue"]]),r=JSON.parse('{"path":"/bigdata/spark/elastic-spark.html","title":"elastic spark","lang":"zh-CN","frontmatter":{"description":"elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、t...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"elastic spark"}],["meta",{"property":"og:description","content":"elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、t..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://databricks.com/wp-content/uploads/2019/02/largest-open-source-apache-spark.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"elastic spark\\",\\"image\\":[\\"https://databricks.com/wp-content/uploads/2019/02/largest-open-source-apache-spark.png\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、原生RDD支持","slug":"一、原生rdd支持","link":"#一、原生rdd支持","children":[{"level":3,"title":"1.1 基础配置","slug":"_1-1-基础配置","link":"#_1-1-基础配置","children":[]},{"level":3,"title":"1.2 读取es数据","slug":"_1-2-读取es数据","link":"#_1-2-读取es数据","children":[]},{"level":3,"title":"1.3 写数据","slug":"_1-3-写数据","link":"#_1-3-写数据","children":[]}]},{"level":2,"title":"二、Spark Streaming","slug":"二、spark-streaming","link":"#二、spark-streaming","children":[]},{"level":2,"title":"三、Spark SQL","slug":"三、spark-sql","link":"#三、spark-sql","children":[]},{"level":2,"title":"四、Spark Structure Streaming","slug":"四、spark-structure-streaming","link":"#四、spark-structure-streaming","children":[]},{"level":2,"title":"五、Spark on kubernetes Operator","slug":"五、spark-on-kubernetes-operator","link":"#五、spark-on-kubernetes-operator","children":[]}],"git":{"createdTime":1651578150000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":1.71,"words":514},"filePathRelative":"bigdata/spark/elastic-spark.md","localizedDate":"2022年5月3日","excerpt":"\\n

Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
\\n(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
\\n(2)数据RDBMS:mysql、oracle、mssql
\\n(3)NOSQL数据库:HBase、ES、Redis
\\n(4)消息对象:Redis

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/elasticsearch\346\272\220\347\240\201debug.html-Bo6HcrbI.js" "b/assets/elasticsearch\346\272\220\347\240\201debug.html-Bo6HcrbI.js" new file mode 100644 index 00000000..901d4b75 --- /dev/null +++ "b/assets/elasticsearch\346\272\220\347\240\201debug.html-Bo6HcrbI.js" @@ -0,0 +1 @@ +import{_ as a,c as t,d as i,o as s}from"./app-ftEjETWs.js";const r={};function c(h,e){return s(),t("div",null,e[0]||(e[0]=[i('

【elasticsearch】源码debug

一、下载源代码

直接用idea下载代码https://github.com/elastic/elasticsearch.git
image

切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
image

二、修改设置(可选)

为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:

setting 'xpack.security.enabled', 'false'

或者使用其中的用户名密码:

user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'

三、启动

先启动上面的 remote debug, 然后用 gradlew 启动项目:

./gradlew :run --debug-jvm
打开浏览器http://localhost:9200即可看到es相关信息了
image

',12)]))}const g=a(r,[["render",c],["__file","elasticsearch源码debug.html.vue"]]),p=JSON.parse('{"path":"/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html","title":"【elasticsearch】源码debug","lang":"zh-CN","frontmatter":{"description":"【elasticsearch】源码debug 一、下载源代码 直接用idea下载代码https://github.com/elastic/elasticsearch.git image 切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好 image 二、修改设置(可选) ...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"【elasticsearch】源码debug"}],["meta",{"property":"og:description","content":"【elasticsearch】源码debug 一、下载源代码 直接用idea下载代码https://github.com/elastic/elasticsearch.git image 切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好 image 二、修改设置(可选) ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/755525-20220124160719006-851383635.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"【elasticsearch】源码debug\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/755525-20220124160719006-851383635.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20220124160709513-361605195.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20220124160657219-1269826381.png\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706186538000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":0.52,"words":157},"filePathRelative":"database/elasticsearch/elasticsearch源码debug.md","localizedDate":"2024年1月25日","excerpt":"\\n

一、下载源代码

\\n

直接用idea下载代码https://github.com/elastic/elasticsearch.git
\\n\\"image\\"

","autoDesc":true}');export{g as comp,p as data}; diff --git a/assets/feed.html-BcapapFL.js b/assets/feed.html-BcapapFL.js new file mode 100644 index 00000000..c91498bd --- /dev/null +++ b/assets/feed.html-BcapapFL.js @@ -0,0 +1 @@ +import{_ as n,c as a,a as e,o as r}from"./app-ftEjETWs.js";const i={};function o(s,t){return r(),a("div",null,t[0]||(t[0]=[e("h1",{id:"feed系统设计",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#feed系统设计"},[e("span",null,"Feed系统设计")])],-1),e("p",null,[e("a",{href:"https://blog.csdn.net/weixin_45583158/article/details/128195940",target:"_blank",rel:"noopener noreferrer"},"https://blog.csdn.net/weixin_45583158/article/details/128195940")],-1)]))}const l=n(i,[["render",o],["__file","feed.html.vue"]]),c=JSON.parse('{"path":"/system-design/feed.html","title":"Feed系统设计","lang":"zh-CN","frontmatter":{"description":"Feed系统设计 https://blog.csdn.net/weixin_45583158/article/details/128195940","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/feed.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Feed系统设计"}],["meta",{"property":"og:description","content":"Feed系统设计 https://blog.csdn.net/weixin_45583158/article/details/128195940"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Feed系统设计\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.02,"words":7},"filePathRelative":"system-design/feed.md","localizedDate":"2024年3月10日","excerpt":"\\n

https://blog.csdn.net/weixin_45583158/article/details/128195940

","autoDesc":true}');export{l as comp,c as data}; diff --git a/assets/g1.html-CcgAHdWl.js b/assets/g1.html-CcgAHdWl.js new file mode 100644 index 00000000..d64cb372 --- /dev/null +++ b/assets/g1.html-CcgAHdWl.js @@ -0,0 +1 @@ +import{_ as t,c as i,d as a,o as n}from"./app-ftEjETWs.js";const r={};function l(o,e){return n(),i("div",null,e[0]||(e[0]=[a('

G1

本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点:

  • 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并发的方式,充分利用多核处理器的优势,提高了垃圾回收的效率。
  • 分代回收: G1垃圾回收器依旧采用分代回收的思想,但是和CMS等分代回收算法不同,G1不是将整个堆内存划分为年轻代、老年代和元空间。而是将堆内存划分为一个个固定大小的region,每个region可以属于年轻代或老年代。垃圾回收的基本单位是region,而不是整个堆,这使得垃圾回收更加灵活。
  • 增量回收: G1采用增量回收的方式,将整个垃圾回收过程分解为多个阶段进行,这样更有利于分散垃圾回收的压力,减小每次暂停的时间,提高系统的响应性。
  • Compacting回收: 与CMS回收器不同,G1是一种compacting回收器,其回收的内存空间是连续的。这样就可以避免CMS收集器由于不连续内存空间造成的所需堆空间更大和浮动垃圾的问题。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • 软实时: G1回收器具有软实时(soft real-time)的特性,用户可以指定垃圾回收时间的限时。虽然G1会努力在限定时间内完成垃圾回收,但并不保证每次都能在时限内完成。通过设定合理的目标,可以使大部分垃圾回收时间都在规定的时限内完成。

G1垃圾回收器以其创新性的设计和优越的性能特点,逐渐成为Java应用程序中首选的垃圾回收器之一。通过分代、增量、并行、并发等多种技术手段的结合,G1回收器在处理大内存和多核处理器的环境下表现出色,为Java应用程序提供了更好的性能和响应能力。

G1的内存模型和分代策略

G1收集器相关参数

参数默认值描述
-XX:G1NewSizePercent整堆的5%年轻代的初始空间百分比。
-XX:G1MaxNewSizePercent60%年轻代的最大空间百分比
-XX:MaxGCPauseMillis200ms指定的最大垃圾收集暂停时间
-XX:NewRatio-与-Xmn一同设置,年轻代和老年代的比例
-Xmn-设置年轻代的固定大小
-XX:G1HeapRegionSize1MB~32MB分区大小,每个分区是G1的基本单位
-XX:GCTimeRatio9GC与应用程序的耗时比例
-XX:G1MaxNewSizePercent99GC与应用程序的耗时比例,CMS默认为99,G1默认为9

G1分区(Region)机制和分代策略

img
img

如上面的图所示,G1垃圾回收器采用了内存分区(Region)思路,将整个堆空间分成若干个大小相等的内存区域,这些内存区域的大小一般大小在1MB~32MB之间,可以通过设置启动参数 -XX:G1HeapRegionSize=n 来指定分区的大小。

每次垃圾回收的时候,G1会逐段地回收这些内存区域,而且不要求对象的存储在物理上是连续的,只要在逻辑上是连续的即可。这样的好处在于,分代垃圾收集将关注点聚焦在最近分配的对象上,无需全堆扫描,从而避免了对长生命周期对象的频繁拷贝。并且年轻代和老年代的垃圾回收过程相互独立,有助于降低系统整体的响应时间。

虽然G1内存分区不要求内存分配是非连续的,但它依然逻辑上将内存划分为年轻代和老年代。然而,和CMS等分代的垃圾回收器不同,G1垃圾回收器不是将一大块连续的内存区域划分成年轻代和老年代,而是将每一个分区(Region)划分成Eden区、Survivor区、Huge区和Old区等。

并且和CMS等分代垃圾回收器不同的是,对于G1收集器来说,当现有年轻代分区被占满时,JVM会为年轻代动态分配新的空闲分区。

年轻代的整体内存大小会在初始空间(通过参数-XX:G1NewSizePercent设置,默认整堆的5%)和最大空间(通过参数-XX:G1MaxNewSizePercent设置,默认60%)之间动态变化。这个动态变化的过程由目标暂停时间(通过参数-XX:MaxGCPauseMillis设置,默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)共同决定。总结下上面说的,G1垃圾回收器的分区和分代机制主要有下面3个特点:

  • 分区(Region)大小可调: G1垃圾回收器可以设置分区(Region)大小范围为1MB~32MB,且必须是2的幂,默认情况下,整个堆被划分为2048个分区,但可以根据应用程序的需求进行调整。
  • 逻辑连续性: G1垃圾回收器并不强制要求对象在物理上的存储是连续的,而是以逻辑上的连续性为目标,这样一来可以更灵活的分配内存,使对象的存储不受物理位置的限制。
  • 动态分代: 每个分区并不固定地为某个代服务,分区(Region)可以被划分成Eden区、Survivor区、Huge区和Old区等。并且可以按需在年轻代和老年代之间切换,G1可以根据实际情况调整内存分区的使用。

G1垃圾回收器通过引入分区设计和分代策略,以逻辑上的连续性为基础,克服了传统垃圾回收器对物理连续性的依赖。这种设计使得对象的存储更加灵活,同时允许动态调整分区的使用,为应对不同应用场景提供了更好的适应性。

G1 卡片(Card)标记机制

img
img

如上图所示,在G1垃圾回收器中,每个分区内部进一步被划分为若干大小为512 Byte的卡片(Card)。这些卡片用于标识堆内存的最小可用粒度。所有分区的卡片记录在全局卡片表中(Global Card Table),而分配的对象将占用物理上连续的若干个卡片,总结起来主要有以下几点:

  • 卡片(Card): G1将每个分区(Region)进一步划分为大小为512 Byte的卡片,这是最小的标记单位,对象的分配和回收都以卡片为基本单位进行,这个卡片的大小是可以进行设置和调整的。
  • 全局卡表(Global Card Table): 所有分区的卡片状态会被记录在全局卡片表中,这个表维护了对每个卡片的引用状态,用于在垃圾回收过程中准确地追踪对象引用关系。
  • 对象分配物理连续性: 分配的对象会占用物理上连续的若干个卡片,这种设计有助于提高内存的利用率,并简化对内存的管理。
  • 引用查找: 对分区(Region)内对象引用的查找通过记录卡片(Card)的方式实现,G1维护了一个叫做RSet(Remembered Set)的数据结构,用于记录在分区内对象的引用关系。
  • 回收处理: 每次对内存的回收都涉及对指定分区的卡片进行处理。通过检查RSet中的信息,G1能够确定哪些对象是不再被引用的,从而进行相应的回收操作。

G1垃圾回收器利用卡片标记机制,将堆内存划分为最小的可标记单位,实现了对对象引用关系的准确追踪。这种分区和卡片的设计有助于提高垃圾回收的效率,同时为更精细的内存管理提供了基础。

G1 堆(Heap)空间调整机制

G1垃圾回收器提供了一套灵活的堆空间调整机制,通过自动调整堆空间大小来适应不同的应用场景,可以指定堆空间的大小,也可以设置堆空间的比例来调整GC的频率。

  • 指定堆空间大小: 通过标准的JVM参数 -Xms(初始堆大小)和 -Xmx(最大堆大小)可以手动指定堆空间的大小。
  • 自动调整: G1在发生年轻代收集或混合收集时,会根据GC与应用的耗费时间比进行自动调整堆空间大小。这个比例由目标参数 -XX:GCTimeRatio 决定,默认值为9。G1根据应用和GC的相对耗时,动态调整堆空间,以减少GC的频率和相应的时间开销。
  • GC频率控制: 如果GC频率过高,G1可以通过增加堆的尺寸来减少GC的频率,从而降低GC占用的时间。这是通过自动调整堆空间大小实现的。
  • 空间不足处理: 当发生对象空间分配或转移失败时,G1会首先尝试增加堆空间。如果扩容成功,问题得以解决;如果扩容失败,G1会触发一次担保的Full GC,Full GC后,G1重新计算堆尺寸,调整堆空间大小。

通过这套机制,G1垃圾回收器在运行时可以动态适应不同的工作负载,提高垃圾回收的效率,同时降低GC对应用程序的影响。

本地分配缓冲 Local allocation buffer (Lab)

img
img

在JVM中一个对象被创建之后,JVM首先要做的事就是给这个对象分配一块儿内存空间。但是,执行这个内存分配工作

的也是一个线程,那么只要涉及到多线程就可能会出现线程安全问题,于是就有了LAB(本地分配缓冲)的概念。

LAB(本地分配缓冲,Local allocation buffer)的意思是每个线程都有自己独立的一个分区(Region)来进行内存分配,这样线程之前的内存互不打扰,就保障了线程的安全。

结合G1垃圾回收器来说,由于采用了分区(Region)的思想,所以每个线程都可以“认领”某个分区用于线程本地的内存分配,而无需顾忌内存空间是否连续。每个应用线程和GC线程都会独立的使用某个分区,从而减少同步所耗费的时间,提升GC效率,这个分区被称为”本地分配缓冲区“(Lab),除了Lab之外还有TLAB、PLAB和GCLAB等概念,如下:

  • TLAB(Threads Local Allocation Buffer,线程本地分配缓冲区): 每一个应用线程(App thread)都可以独占一个本地缓冲区(即Region)来创建对象,TLAB通常属于 Eden 空间,因为大部分对象会首先在 Eden 区被分配(大型对象除外)。
  • *GCLab(***GC Local Allocation Buffer,GC 本地缓冲区): 每个GC线程在进行垃圾收集时,同样可以独占一个本地缓冲区(GCLAB,即Region)用于转移对象,在每次回收过程中,对象会被复制到 Survivor 空间或老年代空间。
  • *PLAB(***Promotion Local allocation buffer,晋升本地缓冲区): 对于从 Eden 或 Survivor 空间晋升到 Survivor 或老年代空间的对象,即Old对象,同样有GC线程独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

这种分区和本地缓冲区的设计使得每个线程有了自己独立的Region,有助于提高并发性和减少同步开销,从而提升垃圾收集的效率。

Remember Set(RSet)和Collection Set(CSet)

前面说过,对于G1收集器而言内存区域会被划分成Eden Region、Survivor Region、Old Region和Humongous Region等不同分代的区域,而这些区域内部又会按照512 Bytes的大小为一个Card,进一步被划成为多个Card。

G1收集器在分配对象的时候,是按照完整Card的来进行分配的,也就是说一个对象可以占用1个、2个甚至多个cards,即使这些Cards没有被完全占满,也会分配整数个Cards给这个对象使用。那么这样一来,当G1垃圾回收器需要回收某一个对象的时候就只需要回收对象对应的Cards就可以了,这样既高效也不会影响到其它对象的正常使用。

为什么需要RSet?

img
img

G1回收器在对某个Region进行垃圾回收的时候,首先会使用的**“根可达算法”和“三色标记**”算法从GC Roots开始进行垃圾标记。在垃圾标记的过程中,由于晋升等原因,某些Region当中对象可能会被移动到其它Region,但是G1收集器中对对象的引用是到Region中的Cards级别的。

在G1 垃圾收集器中的对象是可以晋升的,和其它垃圾收集器类似当年轻代的对象存活超过一定时间之后,就可以从年轻代晋升为老年代。如上图所示,假设GC Roots分别引用了Eden Region 中的A对象和Survivor Region中的B对象,而在GC过程中这两个对象由于晋升等原因被移动到了Old Region。而此时,对象A和B依旧还是引用了处于Eden和Survivor中的C、D对象,这也就发生了跨区域(Region)的引用。

除了上面所说的从老年代到新生代的引用之外,根据G1的设计思路,还存在以下几种引用情况:

  1. 分区内部有引用关系
  2. 新生代分区到新生代分区之间有引用关系
  3. 新生代分区到老生代分区之间有引用关系
  4. 老生代分区到新生代分区之间有引用关系
  5. 老生代分区到老生代分区之间有引用关系

另外,G1垃圾回收器也是分代垃圾回收器,也存在着新生代垃圾回收(Young GC,回收Eden区、Survivor区)、混合回收(Mixed GC,Eden区、Survivor 区、Old 区)和Full GC(Eden区、Survivor区、Old区、Huge区,Meta Space等)。

那么,假设Young GC发生的时候,就按照上图的例子,如果我们发现处于Eden区或者Survivor区的对象被Old区或者其它区域的对象所引用,这种时候如何才能将当前区域的对象成功标记为存活对象呢?

最简单的办法当然是整体扫描所有区域来得到其它区域对当前区域的引用,那就相当于每次Young GC都要进行一次Full GC才能找到所有引用关系,保证不发生漏标。但是,这种方法无异于高射炮打蚊子,得不偿失,会导致G1垃圾回收器效率极低。

所以,如果对于当前Region中的某个对象,我们知道它被什么对象所引用,或者换句话说有一个当前对象的**反向指针。这样以来,**我们就能够更快速的判断当前区域中的对象是否被GC Root所引用,是否应该被标记成存活对象。此时我们的主角就呼之欲出了——RSet。

RSet作为用于记录引用关系的数据结构,在有效地追踪这些引用。通过精确地维护RSet,G1能够更有效地执行垃圾回收,从而最大程度地减少对可用空间的影响。这种详细的引用关系分析有助于优化垃圾回收算法,提高Java应用程序的性能和资源利用率。

RSet的实现机制

img
img

由于G1垃圾回收器的回收粒度是按照区域(Region进行的),为了记录Region之间对象引用关系,当Region初始化的时候,JVM会拿出这个Region的一部分区域用来初始化一个RSet(Remembered Set,已记忆集合),这个集合的作用是记录和跟踪其它Region指向该Region中对象的Card的引用。在进行垃圾标记的时候,除了从GC Roots开始遍历,还会从RSet开始遍历,确保该区域中所有存活的对象都会被标记到。

RSet是一种典型的空间换时间的策略,对于Old->Young、Old-Old的跨代对象引用,只需要扫描RSet即可。RSet中记录了其它Region钟对象引用本Region中的对象,属于Point-Into(谁引用了我),而Card Table则属于Point-Out(我引用了谁)。但是,由于RSet是属于每一个Region的,如果单个Region中被引用的对象过多,对内存的开销就会太大,因此引入了我们接着需要介绍的节省RSet空间的机制——PRT机制。

Per Region Table(PRT)机制

**RSet内部采用Per Region Table(PRT)**来记录分区的引用情况。由于RSet的记录会占用分区的空间,当一个分区变得非常“热门”时,RSet占用的空间会增加,从而降低了分区的可用空间。为了应对这个问题,G1采用了改变RSet密度的策略,即在PRT中使用三种不同的模式来记录引用:

RSet储存状态描述实现方式描述
稀疏模式(Sparse)对于不那么热门的Region,直接记录引用对象的Card的索引,可以直接通过RSet找到对应对象,空间耗费最大,效率最高。通过哈希表方式实现。数组的Key是当前Region的地址,而值是Card地址数组。
细粒度模式(Fine-grained)记录引用对象的Region索引,可以通过RSet找到对应的Region.通过Region地址链表实现,维护当前Region中所有Card的BitMap集合。当Card被引用时,对应的Bit被设置为1。同时,维护一个对应Region的索引数量,用于跟踪引用情况。
粗粒度模式(Coarse-grained)只记录引用情况,每个分区对应一个比特位,只记录了引用的存在与否,需要通过整堆扫描才能找出所有引用,因此扫描速度是最慢的。通过BitMap来表示所有Region。如果有其他Region对当前Region有指针引用,就设置其对应的Bit为1,否则标记为0。

这种灵活的模式选择允许G1根据分区的特性和使用情况动态调整RSet的记录密度,以平衡空间占用和引用追踪的性能。这种策略的实施有助于在分区受欢迎的情况下最大限度地减少对可用空间的影响。

RSet记录的引用类型

同时,由于G1垃圾回收器在YGC的时候会对所有的Eden区和Survivor区域都进行扫描和回收,因此,从年轻代到年轻代的引用是无需记录的。更多的引用关系如下:

引用关系是否记录在RSet中说明
分区内部有引用关系不记录回收是针对一个分区进行的,回收时会遍历整个分区,无需记录分区内部引用关系。
新生代分区->新生代分区不记录G1回收器在Young GC的时候会全量处理所有新生代分区,无需额外记录这一引用关系。
新生代分区->老生代分区不记录YGC针对的是新生代分区,混合GC使用新生代分区作为根,FGC处理所有分区,无需额外记录这一引用关系。
老生代分区->新生代分区记录YGC时有两种根,一是栈空间/全局空间变量的引用,另一是老生代分区到新生代分区的引用,需要记录。
老生代分区->老生代分区记录在混合GC时可能只有部分分区被回收,必须记录引用关系以快速找到活跃对象。

如上表所示,由于G1回收器不同类型的GC所处理的区域不同,并不是所有其它区域对本区域的引用关系都需要被记录的,简单来说只有“老年代分区到新生代分区”的引用、“老年代分区到老年代”分区的引用需要被RSet记录下来。

CSet(收集集合,Collection Set)

img
img

G1收集器中的CSet(收集集合,Collection Set)是一组被选中进行垃圾回收的Region的集合。G1收集器是以Region为单位的,并且分为YGC和Mixed GC,这两种GC方式都是需要STW的,但是G1收集器的一个优势就是它是增量式的垃圾回收器,可以根据设置的回收停顿时间来动态调整每次的回收量。CSet就可以很好的支持增量回收的特性,其中存储的就每次YGC或者Mixed GC时需要回收清理的所有Region。当回收的时,CSet中的所有Region都将被释放,区域存活的对象将被移动到分配的空闲分区(Free Region)中。

由于G1收集器分为Young GC和Mixed GC两种模式,因此存在着两种不同的CSet分别在这两种模式下使用:

  • Young GC中的CSet: 在Young GC中,CSet包含所有Young区域,即Eden和Survivor分区。当Eden区域满时,触发Young GC,CSet中的对象将被移动或复制到Survivor分区或老年代。这个过程确保Young GC时只处理一小部分内存,减小了每次垃圾回收的暂停时间。
  • **Mixed GC中的CSet:**在Mixed GC中,CSet包括一部分老年代区域,这些区域被选中进行部分垃圾回收。与Young GC不同,Mixed GC关注的是整个堆的内存回收,而不仅仅是年轻代的部分。G1在Mixed GC过程中会优先选择收益高的老年代区域,以提高垃圾回收的效率。

通过使用CSet,G1收集器可以更灵活地控制垃圾回收的范围,同时通过优先处理收益高的区域,提高了内存回收的效率。这种策略使得G1可以在更可控的停顿时间内实现高效的垃圾回收。

RSet的维护

由于不能整堆扫描,又需要计算分区确切的活跃度,因此,G1需要一个增量式的完全标记并发算法,通过维护RSet,得到准确的分区引用信息。在G1中,RSet的维护主要来源两个方面:写栅栏(Write Barrier)和并发优化线程(Concurrence Refinement Threads)

屏障(Barrier)

img
img

屏障(Barrier)是指在原生代码片段中,当某些语句被执行时,屏障代码也会被执行。而G1主要在赋值语句中,使用写前屏障(Pre-Write Barrier)和写后屏障(Post-Write Barrier),如下:

  • 写前屏障 (Pre-Write Barrier):即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象。那么等式左侧对象原先引用的对象所在分区将因此丧失一个引用。此时,JVM需要在赋值语句生效之前,记录丧失引用的对象。JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新(见SATB)。
  • 写后屏障 (Post-Write Barrier):当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用。那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,写后屏障发生后,RSet也不会立即更新,同样只是记录此次更新日志,在将来批量处理(见Concurrence Refinement Threads)。

事实上,写屏障的指令序列开销非常昂贵,应用吞吐量也会根据屏障复杂度而降低。

始快照算法(Snapshot at the Beginning,SATB)

SATB是由Taiichi Tuasa 提出的增量式完全并发标记算法,主要应用于标记-清除垃圾收集器的并发标记阶段。SATB 针对 G1 的分区块堆结构设计,同时解决了 CMS(Concurrent Mark-Sweep)的一个主要问题,即重新标记暂停时间长导致的潜在风险。

SATB 的核心思想是创建一个对象图,类似于堆的逻辑快照,以确保在并发标记阶段能够鉴别出所有的垃圾对象。当赋值语句发生时,应用会改变其对象图,因此 JVM 需要记录被覆盖的对象。为此,写前屏障会在引用变更前,将值记录在 SATB 日志或缓冲区中。每个线程都有一个独占的 SATB 缓冲区,初始时有 256 条记录空间。当空间用尽时,线程会分配新的 SATB 缓冲区继续使用,而旧的缓冲区则加入全局列表。

在并发标记阶段,标记线程会定期检查和处理全局缓冲区列表的记录。然后,根据标记位图分片的标记位,扫描引用字段来更新 RSet(Remembered Set)。这个过程也被称为并发标记/SATB写前屏障。

并发优化线程 (Concurrence Refinement Threads)

G1 中使用基于 Urs Holzle 的快速写屏障,将屏障开销缩减到 2 个额外的指令。屏障将会更新一个 card table type 的结构来跟踪代间引用。当赋值语句发生后,写后屏障会先通过 G1 的过滤技术判断是否是跨分区的引用更新,并将跨分区更新对象的卡片加入缓冲区序列,即更新日志缓冲区或脏卡片队列。与 SATB 类似,一旦日志缓冲区用尽,则分配一个新的日志缓冲区,并将原来的缓冲区加入全局列表中。

并发优化线程 (Concurrence Refinement Threads) 专注于扫描日志缓冲区记录的卡片,以维护更新 RSet。可以通过设置参数 -XX:G1ConcRefinementThreads 来控制并发优化线程的最大数量,默认等于 -XX:ParallelGCThreads

并发优化线程一直保持活跃,一旦发现全局列表中有记录存在,就开始并发处理。如果记录增长迅速或者处理不及时,G1 会使用分层的方式调度,以使更多的线程处理全局列表。如果并发优化线程无法跟上缓冲区数量,Mutator 线程 (Java 应用线程) 可能会挂起应用并被加入来协助处理,直到全部处理完成。因此,需要尽量避免出现这种情况。

G1的垃圾回收流程

G1的GC模式

G1提供了两种GC模式,分别是Young GC(即YGC)和Mixed GC,它们都是完全停顿(”Stop The World“)的垃圾回收方式,需要暂停用户线程。而Full GC实际上是采用了Serial Old的GC方式,不是G1提供的GC模式,如下:

  • Young GC(年轻代GC):选择所有年轻代中的Region进行回收,通过控制年轻代的Region数量,即年轻代内存大小,来控制Young GC的时间开销。
  • Mixed GC(混合GC):同样选择所有年轻代中的Region,加上根据全局并发标记(global concurrent marking)统计的若干老年代Region进行回收。在用户指定的开销目标范围内,尽可能选择收益高的老年代Region。
  • **Full GC:**Mixed GC并非完全回收堆内存的Full GC,它只回收部分老年代的Region。如果Mixed GC无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,系统会使用Serial Old GC(Full GC)来收集整个GC heap。

以上3种GC模式中,Young GC和Mixed GC,是G1回收空间的主要活动。当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发Young GC进行垃圾回收;随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1就会根据一定的条件(参考后面内容)来触发Mixed GC和Full GC。

GC 工作线程数(-XX:ParallelGCThreads)

JVM可以通过参数-XX:ParallelGCThreads来指定GC工作的线程数量。该参数的默认值并非固定,而是根据当前的CPU资源进行计算。如果用户没有指定,并且CPU核数小于等于8,则默认与CPU核数相等;如果CPU核数大于8,则JVM会经过计算得到一个小于CPU核数的线程数。用户也可以手动指定线程数,例如与CPU核数相等。

Young GC核心流程

Young GC的核心流程就是采用STW的方式对所有的Young Region进行回收,将需要晋升的对象复制到老年代,这个过程不仅需要遍历GC Roots 还需要对CSet进行扫描。

当应用程序刚开始运行的时候,堆内的可用空间较多,当Young Region满的时候,就会触发Young GC。这个过程中,G1收集器会将所有的Young Region,即Eden区,放到CSet当中。Eden分区中存活的对象将被复制到Survivor分区。原有的Survivor分区中存活的对象将根据存活次数(年龄)的任期阈值分别晋升到PLAB(Parallel Live Allocation Buffer)、新的Survivor区,或Old区,原有的Eden分区将被整体回收。

具体来说,当JVM无法将新对象分配到Eden区域时,会触发Young GC,也称为"evacuation pause",这是一个用户线程完全暂停的过程,虽然其中部分步骤是并行执行的,但整个过程会导致STW,如下:

  • **1. 选择收集集合(Choose CSet):**G1会在遵循用户设置的GC暂停时间上限的基础上,选择一个最大Eden区域数,将这个数量的所有Eden区域作为收集集合。
  • **2. 根处理(Root Scanning):**从GC Roots开始遍历,查找从roots引用直达到CSet的对象(存活对象),并将它们移动到Survivor区域。同时将存活对象引用的对象加入标记栈(Mark Stack),这一阶段确保CSet中存活的对象被正确复制。
  • **3.RSet扫描(Scan RS):**在RSet扫描之前会先更新RSet(Update RS),因为RSet是先写日志,再通过一个Refine线程进行处理日志来维护RSet数据的。这里的更新RSet就是为了保证RSet日志被处理完成,RSet数据完整才可以进行扫描。然后,遍历RSet,将可直达到CSet的对象(存活对象)移动到Survivor区,同时将存活对象引用的对象加入标记栈。
  • **4. 移动(Evacuation/Object Copy):**遍历标记栈,将栈内的所有对象(存活对象)复制到Survivor区域。这一步是整个"evacuation pause"的核心,确保所有需要移动的对象都被正确复制。这个过程的目标是将年轻代的存活对象转移到Survivor区域,并确保相关引用关系得以更新。整个"evacuation pause"是一个短暂的停顿,但会保证内存中的对象关系得以正确维护。
  • **5. 收尾步骤:**年龄超过晋升阈值的对象会直接移动到老年代区域。执行一些收尾工作,如Redirty(与并发标记配合使用)、Clear CT(清理Card Table)、Free CSet(清理回收集合)等,这些操作通常耗时很短。

Young GC还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。Young GC会将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。

Mixed GC核心流程——混合收集周期(Mixed GC Cycle)

Young GC和Mixed GC是G1回收内存的主要活动。当应用程序启动时,堆内存可用空间相对较大,此时只有在年轻代填满时才会触发Young GC。随着应用程序的运行,老年代内存逐渐增长。当老年代占整个堆的比例达到初始化堆占用百分比阈值(InitiatingHeapOccupancyPercent,默认为45%)时,G1开始准备进行老年代的收集。

但是G1收集器不会直接进行Mixed GC的收集阶段,而是先通过“并发标记周期”(Concurrent Marking Cycle)来标记出高收益的老年代分区,确定下次Mixed GC的CSet(Choose CSet),之后再执行混合收集周期(Mixed GC Cycle)。

并发标记周期 (Concurrent Marking Cycle)

img
img

当不断的Young GC达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,就会触发并发标记周期,它的执行过程类似于CMS(Concurrent Mark-Sweep),执行过程包括以下四个步骤:

  1. 初始标记(Initial Mark,STW):和CMS一样,这个阶段是需要STW的,只标记从GC Root开始直接可达的对象。
  2. 并发标记(Concurrent Marking):并行标记线程与应用程序线程一起执行,标记整个heap中的对象,并收集各个Region的存活对象信息。
  3. 最终标记(Remark,STW):标记在并发标记阶段发生变化的对象,准备进行回收,这个阶段主要是重新标记在并发标记阶段产生的新垃圾,需要STW。
    另外,由于G1采用的**三色标记算法,在并发标记阶段可能会出现“多标”和“漏标”的情况。相对来说“多标”是可以容忍的,只需要等到下次GC的时候进行处理即可;但是“漏标”会导致正常使用的对象被释放,因此是不能容忍的。在CMS中会采用“写屏障 + 原始快照”** 的方法来处理漏标问题,而这个方法需要将发生变化的对象进行重新标记。
  4. 筛选回收(Cleanup):筛选回收环节会对Old Region的回收价值和成本进行排序,根据用户指定的停顿时间等来确定回收性价比最高的Region。这个阶段和CMS不同,是需要STW的。

初始标记(Initial Mark,STW)

初始标记(Initial Mark)的任务是标记所有直接可达的根对象,如原生栈对象、全局对象和JNI对象。由于根是对象图的起点,这个阶段需要暂停Mutator线程(Java应用线程),即需要一个STW的时间段。

实际上,当达到IHOP阈值时,G1并不会立即启动并发标记周期,而是等待下一次年轻代收集。这时,它利用年轻代收集的STW时间段来完成初始标记,这种方式称为借道(Piggybacking)。在初始标记暂停期间,分区的NTAMS都被设置到分区顶部Top。初始标记是并发执行的,直到所有分区都得到处理。

初始标记暂停结束后,年轻代收集已完成对象复制到Survivor的工作,应用线程开始活跃。为了保证标记算法的正确性,所有新复制到Survivor分区的对象都需要进行扫描并标记为根。这个过程被称为根分区扫描(Root Region Scanning),同时扫描的Survivor分区也被称为根分区(Root Region)。

根分区扫描必须在下一次年轻代垃圾收集启动之前完成,因为在并发标记的过程中可能会被若干次年轻代垃圾收集打断。这确保了每次GC都有准确的存活对象集合。

并发标记(Concurrent Mark)

在并发标记阶段,并发标记线程与应用线程并发执行,并发标记阶段通过参数-XX:ConcGCThreads(默认为GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量。

每个线程负责扫描一个分区,从而标记出存活对象图。在这个过程中,处理Previous/Next标记位图,扫描标记对象的引用字段。并发标记线程定期检查和处理STAB全局缓冲区列表的记录,更新对象引用信息。若开启参数-XX:+ClassUnloadingWithConcurrentMark,当一个类如果在Final Mark阶段不可达,将直接卸载。

标记任务必须在堆满之前完成扫描,否则会触发担保机制,经历一次长时间的串行Full GC。

存活数据计算(Live Data Accounting)是标记操作的附加产物,一旦对象被标记,将计算其字节数并记录到分区空间。只有NTAMS以下的对象会被标记和计算,标记周期结束时,Next位图将被清空,为下次标记周期做准备。

最终标记(Final Mark)

重新标记(Remark)是标记阶段的最后一步。在这个阶段,G1需要进行一次短时间的停顿,来处理剩余的SATB(并发标记产生的存活对象记录)日志缓冲区和所有的更新。

其目标是找出所有未被访问的存活对象,并安全地完成存活数据计算。这个阶段也是并行执行的,可以通过参数-XX:ParallelGCThread来设置在GC暂停时可用的并行GC线程数。

与此同时,引用处理也是重新标记阶段的一部分,对于所有强引用、软引用、弱引用、虚引用以及最终引用的对象,都会在引用处理中产生一些额外的开销。

筛选回收(Clean Up)

筛选回收的阶段,同样是一个STW(Stop-The-World)的过程,并且是并发执行的,主要会进行以下主要操作:

  • RSet梳理: 启发式算法会根据活跃度和RSet尺寸对分区进行不同等级的定义。同时,RSet的梳理也有助于发现无用的引用。可以通过参数-XX:+PrintAdaptiveSizePolicy来开启打印启发式算法决策的详细信息。
  • 整理堆分区: 为Mixed GC Cycle识别回收效益高(基于释放空间和暂停目标)的Old Region集合。
  • 识别所有空闲分区: 即发现无存活对象的分区,这些分区在筛选回收阶段可以直接回收,无需等待下次Mixed GC Cycle。

筛选回收阶段结束之后,就可能会启动Mixed GC Cycle,Mixed GC Cycle会根据该过程中整理出的回收效益高的分区来决定是否要将这些Region加入CSet并进行回收。

混合回收周期(Mixed GC Cycle)

当G1发起并发标记周期之后,并不会马上开始Mixed GC Cycle,G1会先等待下一次Young GC,然后在该收集阶段中,确定下次混合收集的CSet(Choose CSet),之后才会决定是否开启混合回收周期。

单次的Mixed GC与Young GC在操作上别无二致,可以参考前面的Young GC的流程。但是Mixed GC为了减小垃圾回收的停顿时间,Old Region可能无法在一次Mixed GC中被完全处理。

因此,G1会启动连续多次的Mixed GC,这一系列的Mixed GC被称为混合收集周期(Mixed Collection Cycle)。在Mixed GC Cycle中,G1会进行一系列的计算和决策,来确定单次收集的分区数量以及是否结束Mixed GC:

  • 计算每次加入到CSet中的分区数量;
  • 确定混合收集进行的次数;
  • 在上一次Young GC以及接下来的Mixed GC中,确定下一次加入CSet的分区(Choose CSet)的Region;
  • 确定是否结束Mixed GC Cycle,以及是否启动Full GC;

这些计算和决策是为了优化整个混合收集周期,使得在每次垃圾回收中都能有效地处理老年代的分区,同时控制停顿时间,以提高应用程序的响应性。

Young GC、Mixed GC 和 Full GC的触发时机

  • Young GC触发时机:
    Young GC的触发时机是当年轻代空间逐渐填满,无法再Eden区域分配新的对象时。
  • *Mixed GC***触发时机:
    Mixed GC的触发取决于一些参数的设定,这些参数包括:
参数含义
G1HeapWastePercent在全局并发标记结束后,系统检查Old Regions中即将被回收的垃圾百分比,只有达到此参数设定的百分比时,下次才会触发Mixed GC
G1MixedGCLiveThresholdPercentOld Region中存活对象的占比,只有在此参数设定的阈值以下,Region才会被选入CSet(Collection Set,回收集)
G1MixedGCCountTarget一次全局并发标记后,最多执行Mixed GC的次数
G1OldCSetRegionThresholdPercent一次Mixed GC中可选入CSet的最多老年代Region数量的百分比
  • *Full GC***触发时机:
    在G1垃圾收集器中,当堆空间无法分配新的分区时,即无法满足对象的分配需求时,G1会启动担保机制,执行一次完全暂停(STW)的、Serial Old单线程的Full GC。Full GC的过程涉及整个堆的标记、清理和压缩,最终留下只包含存活对象的堆。Full GC发生时,G1会在日志中记录 "to-space-exhausted" 和 "Evacuation Failure",以下是触发Full GC的一些场景,:
    • 从Young Region拷贝存活对象时:当G1尝试将Young Region的存活对象拷贝到其他分区时,如果找不到可用的Free Region,就会触发Full GC。
    • 从Old Region转移存活对象时:在尝试将Old Region的存活对象转移到其他分区时,如果找不到可用的Free Region,同样会触发Full GC。
    • 分配巨型对象时:当G1需要为巨型对象分配内存空间时,如果在Old Region找不到足够的连续分区,就会触发Full GC。

这些情况下,Full GC是G1的一种应对措施,但由于Full GC的代价昂贵,会导致暂停时间较长,因此应该尽量避免触发Full GC。

JDK的G1垃圾回收器和CMS有什么区别

1.内存划分:CMS垃圾回收器将Java堆划分为年轻代和老年代,而G1垃圾回收器则将Java堆划分为多个大小相等的区域(region),每个区域都可能是Eden区、Survivor区或Old区。

2.并发阶段:CMS垃圾回收器在标记和清除阶段都有并发的操作,而G1垃圾回收器只在标记阶段有并发的操作。

3.内存碎片处理:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。而G1垃圾回收器在垃圾回收过程中可以对对象进行压缩和整理,从而减少内存碎片。

4.可预测性:G1垃圾回收器引入了暂停时间目标(Pause Time Goal),可以让用户指定期望的垃圾回收暂停时间,从而提供更好的可预测性。

5.垃圾回收策略:CMS垃圾回收器主要关注的是降低老年代的垃圾回收暂停时间,而G1垃圾回收器则试图在整个Java堆上都实现低暂停时间。

总结

G1是一款卓越的垃圾收集器,适用于大型堆内存应用,并简化了性能调优的工作。通过主要参数设置初始堆、最大堆以及最大允许的GC暂停目标,可以获得良好的性能。尽管G1对内存空间的浪费较高,但通过"首先收集尽可能多的垃圾"的设计原则,能够及时发现过期对象,保持内存占用在合理水平。

G1的垃圾收集过程包括初始标记、并发标记、重新标记、清除、转移回收等步骤,同时以一个串行收集器做担保机制。然而,与其他垃圾收集器的过程描述不同,G1的设计原则是"Garbage First",不等待内存耗尽开始垃圾收集,而是在内部采用启发式算法,在老年代找出高收集收益的分区进行收集。

G1还能根据用户设置的暂停时间目标自动调整年轻代和总堆大小,使得暂停目标越短,年轻代空间越小、总空间越大。采用内存分区的思路,G1将内存划分为相等大小的内存分区,在回收时以分区为单位进行操作,将存活的对象复制到另一个空闲分区中。由于操作单位相等,G1天然是一种压缩方案(局部压缩)。

尽管G1是分代收集器,但内存分区不存在物理上的年轻代与老年代的区别,也无需独立的survivor(to space)堆。G1只有逻辑上的分代概念,每个分区都可能在不同代之间前后切换。

G1的收集过程是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集)。这种方式即使在堆内存很大时,也可以限制收集范围,从而降低停顿。

',142)]))}const d=t(r,[["render",l],["__file","g1.html.vue"]]),s=JSON.parse('{"path":"/java/JVM/g1.html","title":"G1","lang":"zh-CN","frontmatter":{"description":"G1 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器 G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点: 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/g1.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"G1"}],["meta",{"property":"og:description","content":"G1 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器 G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点: 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/v2-ddd707766e13fb354940a1a2e8ab5055_1440w.webp"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:55:09.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:55:09.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"G1\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/v2-ddd707766e13fb354940a1a2e8ab5055_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-e2aee61fa6f90ea5ee2471f9b260617e_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-73c5aa7da8361788952fbecac20caad2_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-27e44887ebaf83e0951f452714b8b311_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-819a31c0caf633685200ef331d027b70_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-37b4072e5746748e329d07c8f00b5f5b_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-557807dd7a05a5bb13b0fdb0d5a16b1d_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-1351bbae5aad68fcd85ebb0c1d2bc9ac_1440w.webp\\"],\\"dateModified\\":\\"2024-02-16T09:55:09.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"G1的内存模型和分代策略","slug":"g1的内存模型和分代策略","link":"#g1的内存模型和分代策略","children":[{"level":3,"title":"G1收集器相关参数","slug":"g1收集器相关参数","link":"#g1收集器相关参数","children":[]},{"level":3,"title":"G1分区(Region)机制和分代策略","slug":"g1分区-region-机制和分代策略","link":"#g1分区-region-机制和分代策略","children":[]},{"level":3,"title":"G1 卡片(Card)标记机制","slug":"g1-卡片-card-标记机制","link":"#g1-卡片-card-标记机制","children":[]},{"level":3,"title":"G1 堆(Heap)空间调整机制","slug":"g1-堆-heap-空间调整机制","link":"#g1-堆-heap-空间调整机制","children":[]},{"level":3,"title":"本地分配缓冲 Local allocation buffer (Lab)","slug":"本地分配缓冲-local-allocation-buffer-lab","link":"#本地分配缓冲-local-allocation-buffer-lab","children":[]}]},{"level":2,"title":"Remember Set(RSet)和Collection Set(CSet)","slug":"remember-set-rset-和collection-set-cset","link":"#remember-set-rset-和collection-set-cset","children":[{"level":3,"title":"为什么需要RSet?","slug":"为什么需要rset","link":"#为什么需要rset","children":[]},{"level":3,"title":"RSet的实现机制","slug":"rset的实现机制","link":"#rset的实现机制","children":[]},{"level":3,"title":"Per Region Table(PRT)机制","slug":"per-region-table-prt-机制","link":"#per-region-table-prt-机制","children":[]},{"level":3,"title":"RSet记录的引用类型","slug":"rset记录的引用类型","link":"#rset记录的引用类型","children":[]},{"level":3,"title":"CSet(收集集合,Collection Set)","slug":"cset-收集集合-collection-set","link":"#cset-收集集合-collection-set","children":[]},{"level":3,"title":"RSet的维护","slug":"rset的维护","link":"#rset的维护","children":[]},{"level":3,"title":"屏障(Barrier)","slug":"屏障-barrier","link":"#屏障-barrier","children":[]},{"level":3,"title":"始快照算法(Snapshot at the Beginning,SATB)","slug":"始快照算法-snapshot-at-the-beginning-satb","link":"#始快照算法-snapshot-at-the-beginning-satb","children":[]},{"level":3,"title":"并发优化线程 (Concurrence Refinement Threads)","slug":"并发优化线程-concurrence-refinement-threads","link":"#并发优化线程-concurrence-refinement-threads","children":[]}]},{"level":2,"title":"G1的垃圾回收流程","slug":"g1的垃圾回收流程","link":"#g1的垃圾回收流程","children":[{"level":3,"title":"G1的GC模式","slug":"g1的gc模式","link":"#g1的gc模式","children":[]},{"level":3,"title":"GC 工作线程数(-XX:ParallelGCThreads)","slug":"gc-工作线程数-xx-parallelgcthreads","link":"#gc-工作线程数-xx-parallelgcthreads","children":[]},{"level":3,"title":"Young GC核心流程","slug":"young-gc核心流程","link":"#young-gc核心流程","children":[]},{"level":3,"title":"Mixed GC核心流程——混合收集周期(Mixed GC Cycle)","slug":"mixed-gc核心流程——混合收集周期-mixed-gc-cycle","link":"#mixed-gc核心流程——混合收集周期-mixed-gc-cycle","children":[]},{"level":3,"title":"并发标记周期 (Concurrent Marking Cycle)","slug":"并发标记周期-concurrent-marking-cycle","link":"#并发标记周期-concurrent-marking-cycle","children":[]},{"level":3,"title":"初始标记(Initial Mark,STW)","slug":"初始标记-initial-mark-stw","link":"#初始标记-initial-mark-stw","children":[]},{"level":3,"title":"并发标记(Concurrent Mark)","slug":"并发标记-concurrent-mark","link":"#并发标记-concurrent-mark","children":[]},{"level":3,"title":"最终标记(Final Mark)","slug":"最终标记-final-mark","link":"#最终标记-final-mark","children":[]},{"level":3,"title":"筛选回收(Clean Up)","slug":"筛选回收-clean-up","link":"#筛选回收-clean-up","children":[]},{"level":3,"title":"混合回收周期(Mixed GC Cycle)","slug":"混合回收周期-mixed-gc-cycle","link":"#混合回收周期-mixed-gc-cycle","children":[]},{"level":3,"title":"Young GC、Mixed GC 和 Full GC的触发时机","slug":"young-gc、mixed-gc-和-full-gc的触发时机","link":"#young-gc、mixed-gc-和-full-gc的触发时机","children":[]}]},{"level":2,"title":"JDK的G1垃圾回收器和CMS有什么区别","slug":"jdk的g1垃圾回收器和cms有什么区别","link":"#jdk的g1垃圾回收器和cms有什么区别","children":[]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1708056026000,"updatedTime":1708077309000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":39.5,"words":11851},"filePathRelative":"java/JVM/g1.md","localizedDate":"2024年2月16日","excerpt":"\\n

本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

","autoDesc":true}');export{d as comp,s as data}; diff --git a/assets/giscus-BZxmVUME.js b/assets/giscus-BZxmVUME.js new file mode 100644 index 00000000..9d2a50ab --- /dev/null +++ b/assets/giscus-BZxmVUME.js @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const H=globalThis,V=H.ShadowRoot&&(H.ShadyCSS===void 0||H.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,q=Symbol(),J=new WeakMap;let ht=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==q)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(V&&t===void 0){const s=e!==void 0&&e.length===1;s&&(t=J.get(e)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&J.set(e,t))}return t}toString(){return this.cssText}};const ut=i=>new ht(typeof i=="string"?i:i+"",void 0,q),$t=(i,...t)=>{const e=i.length===1?i[0]:t.reduce((s,r,o)=>s+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+i[o+1],i[0]);return new ht(e,i,q)},_t=(i,t)=>{if(V)i.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const e of t){const s=document.createElement("style"),r=H.litNonce;r!==void 0&&s.setAttribute("nonce",r),s.textContent=e.cssText,i.appendChild(s)}},Z=V?i=>i:i=>i instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return ut(e)})(i):i;/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const{is:gt,defineProperty:ft,getOwnPropertyDescriptor:mt,getOwnPropertyNames:At,getOwnPropertySymbols:yt,getPrototypeOf:St}=Object,A=globalThis,Q=A.trustedTypes,vt=Q?Q.emptyScript:"",Y=A.reactiveElementPolyfillSupport,U=(i,t)=>i,L={toAttribute(i,t){switch(t){case Boolean:i=i?vt:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,t){let e=i;switch(t){case Boolean:e=i!==null;break;case Number:e=i===null?null:Number(i);break;case Object:case Array:try{e=JSON.parse(i)}catch{e=null}}return e}},K=(i,t)=>!gt(i,t),X={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:K};Symbol.metadata??(Symbol.metadata=Symbol("metadata")),A.litPropertyMetadata??(A.litPropertyMetadata=new WeakMap);class E extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??(this.l=[])).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=X){if(e.state&&(e.attribute=!1),this._$Ei(),this.elementProperties.set(t,e),!e.noAccessor){const s=Symbol(),r=this.getPropertyDescriptor(t,s,e);r!==void 0&&ft(this.prototype,t,r)}}static getPropertyDescriptor(t,e,s){const{get:r,set:o}=mt(this.prototype,t)??{get(){return this[e]},set(n){this[e]=n}};return{get(){return r==null?void 0:r.call(this)},set(n){const a=r==null?void 0:r.call(this);o.call(this,n),this.requestUpdate(t,a,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??X}static _$Ei(){if(this.hasOwnProperty(U("elementProperties")))return;const t=St(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(U("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(U("properties"))){const e=this.properties,s=[...At(e),...yt(e)];for(const r of s)this.createProperty(r,e[r])}const t=this[Symbol.metadata];if(t!==null){const e=litPropertyMetadata.get(t);if(e!==void 0)for(const[s,r]of e)this.elementProperties.set(s,r)}this._$Eh=new Map;for(const[e,s]of this.elementProperties){const r=this._$Eu(e,s);r!==void 0&&this._$Eh.set(r,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const r of s)e.unshift(Z(r))}else t!==void 0&&e.push(Z(t));return e}static _$Eu(t,e){const s=e.attribute;return s===!1?void 0:typeof s=="string"?s:typeof t=="string"?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){var t;this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),(t=this.constructor.l)==null||t.forEach(e=>e(this))}addController(t){var e;(this._$EO??(this._$EO=new Set)).add(t),this.renderRoot!==void 0&&this.isConnected&&((e=t.hostConnected)==null||e.call(t))}removeController(t){var e;(e=this._$EO)==null||e.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const s of e.keys())this.hasOwnProperty(s)&&(t.set(s,this[s]),delete this[s]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return _t(t,this.constructor.elementStyles),t}connectedCallback(){var t;this.renderRoot??(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),(t=this._$EO)==null||t.forEach(e=>{var s;return(s=e.hostConnected)==null?void 0:s.call(e)})}enableUpdating(t){}disconnectedCallback(){var t;(t=this._$EO)==null||t.forEach(e=>{var s;return(s=e.hostDisconnected)==null?void 0:s.call(e)})}attributeChangedCallback(t,e,s){this._$AK(t,s)}_$EC(t,e){var o;const s=this.constructor.elementProperties.get(t),r=this.constructor._$Eu(t,s);if(r!==void 0&&s.reflect===!0){const n=(((o=s.converter)==null?void 0:o.toAttribute)!==void 0?s.converter:L).toAttribute(e,s.type);this._$Em=t,n==null?this.removeAttribute(r):this.setAttribute(r,n),this._$Em=null}}_$AK(t,e){var o;const s=this.constructor,r=s._$Eh.get(t);if(r!==void 0&&this._$Em!==r){const n=s.getPropertyOptions(r),a=typeof n.converter=="function"?{fromAttribute:n.converter}:((o=n.converter)==null?void 0:o.fromAttribute)!==void 0?n.converter:L;this._$Em=r,this[r]=a.fromAttribute(e,n.type),this._$Em=null}}requestUpdate(t,e,s){if(t!==void 0){if(s??(s=this.constructor.getPropertyOptions(t)),!(s.hasChanged??K)(this[t],e))return;this.P(t,e,s)}this.isUpdatePending===!1&&(this._$ES=this._$ET())}P(t,e,s){this._$AL.has(t)||this._$AL.set(t,e),s.reflect===!0&&this._$Em!==t&&(this._$Ej??(this._$Ej=new Set)).add(t)}async _$ET(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const t=this.scheduleUpdate();return t!=null&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var s;if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??(this.renderRoot=this.createRenderRoot()),this._$Ep){for(const[o,n]of this._$Ep)this[o]=n;this._$Ep=void 0}const r=this.constructor.elementProperties;if(r.size>0)for(const[o,n]of r)n.wrapped!==!0||this._$AL.has(o)||this[o]===void 0||this.P(o,this[o],n)}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),(s=this._$EO)==null||s.forEach(r=>{var o;return(o=r.hostUpdate)==null?void 0:o.call(r)}),this.update(e)):this._$EU()}catch(r){throw t=!1,this._$EU(),r}t&&this._$AE(e)}willUpdate(t){}_$AE(t){var e;(e=this._$EO)==null||e.forEach(s=>{var r;return(r=s.hostUpdated)==null?void 0:r.call(s)}),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Ej&&(this._$Ej=this._$Ej.forEach(e=>this._$EC(e,this[e]))),this._$EU()}updated(t){}firstUpdated(t){}}E.elementStyles=[],E.shadowRootOptions={mode:"open"},E[U("elementProperties")]=new Map,E[U("finalized")]=new Map,Y==null||Y({ReactiveElement:E}),(A.reactiveElementVersions??(A.reactiveElementVersions=[])).push("2.0.4");/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const P=globalThis,k=P.trustedTypes,tt=k?k.createPolicy("lit-html",{createHTML:i=>i}):void 0,at="$lit$",m=`lit$${Math.random().toFixed(9).slice(2)}$`,ct="?"+m,Et=`<${ct}>`,v=document,M=()=>v.createComment(""),N=i=>i===null||typeof i!="object"&&typeof i!="function",F=Array.isArray,bt=i=>F(i)||typeof(i==null?void 0:i[Symbol.iterator])=="function",B=`[ +\f\r]`,w=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,et=/-->/g,st=/>/g,y=RegExp(`>|${B}(?:([^\\s"'>=/]+)(${B}*=${B}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),it=/'/g,rt=/"/g,lt=/^(?:script|style|textarea|title)$/i,Ct=i=>(t,...e)=>({_$litType$:i,strings:t,values:e}),wt=Ct(1),b=Symbol.for("lit-noChange"),l=Symbol.for("lit-nothing"),nt=new WeakMap,S=v.createTreeWalker(v,129);function dt(i,t){if(!F(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return tt!==void 0?tt.createHTML(t):t}const Ut=(i,t)=>{const e=i.length-1,s=[];let r,o=t===2?"":t===3?"":"",n=w;for(let a=0;a"?(n=r??w,c=-1):u[1]===void 0?c=-2:(c=n.lastIndex-u[2].length,d=u[1],n=u[3]===void 0?y:u[3]==='"'?rt:it):n===rt||n===it?n=y:n===et||n===st?n=w:(n=y,r=void 0);const f=n===y&&i[a+1].startsWith("/>")?" ":"";o+=n===w?h+Et:c>=0?(s.push(d),h.slice(0,c)+at+h.slice(c)+m+f):h+m+(c===-2?a:f)}return[dt(i,o+(i[e]||"")+(t===2?"":t===3?"":"")),s]};class R{constructor({strings:t,_$litType$:e},s){let r;this.parts=[];let o=0,n=0;const a=t.length-1,h=this.parts,[d,u]=Ut(t,e);if(this.el=R.createElement(d,s),S.currentNode=this.el.content,e===2||e===3){const c=this.el.content.firstChild;c.replaceWith(...c.childNodes)}for(;(r=S.nextNode())!==null&&h.length0){r.textContent=k?k.emptyScript:"";for(let f=0;f2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=l}_$AI(t,e=this,s,r){const o=this.strings;let n=!1;if(o===void 0)t=C(this,t,e,0),n=!N(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else{const a=t;let h,d;for(t=o[0],h=0;h{const s=(e==null?void 0:e.renderBefore)??t;let r=s._$litPart$;if(r===void 0){const o=(e==null?void 0:e.renderBefore)??null;s._$litPart$=r=new I(t.insertBefore(M(),o),o,void 0,e??{})}return r._$AI(i),r};/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */let O=class extends E{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){var e;const t=super.createRenderRoot();return(e=this.renderOptions).renderBefore??(e.renderBefore=t.firstChild),t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=Rt(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),(t=this._$Do)==null||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),(t=this._$Do)==null||t.setConnected(!1)}render(){return b}};var ot;O._$litElement$=!0,O.finalized=!0,(ot=globalThis.litElementHydrateSupport)==null||ot.call(globalThis,{LitElement:O});const G=globalThis.litElementPolyfillSupport;G==null||G({LitElement:O});(globalThis.litElementVersions??(globalThis.litElementVersions=[])).push("4.1.1");/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const It=i=>(t,e)=>{e!==void 0?e.addInitializer(()=>{customElements.define(i,t)}):customElements.define(i,t)};/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const xt={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:K},Ht=(i=xt,t,e)=>{const{kind:s,metadata:r}=e;let o=globalThis.litPropertyMetadata.get(r);if(o===void 0&&globalThis.litPropertyMetadata.set(r,o=new Map),o.set(e.name,i),s==="accessor"){const{name:n}=e;return{set(a){const h=t.get.call(this);t.set.call(this,a),this.requestUpdate(n,h,i)},init(a){return a!==void 0&&this.P(n,void 0,i),a}}}if(s==="setter"){const{name:n}=e;return function(a){const h=this[n];t.call(this,a),this.requestUpdate(n,h,i)}}throw Error("Unsupported decorator location: "+s)};function _(i){return(t,e)=>typeof e=="object"?Ht(i,t,e):((s,r,o)=>{const n=r.hasOwnProperty(o);return r.constructor.createProperty(o,n?{...s,wrapped:!0}:s),n?Object.getOwnPropertyDescriptor(r,o):void 0})(i,t,e)}/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const Lt=i=>i.strings===void 0;/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const kt={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},Dt=i=>(...t)=>({_$litDirective$:i,values:t});let zt=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,s){this._$Ct=t,this._$AM=e,this._$Ci=s}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}};/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const T=(i,t)=>{var s;const e=i._$AN;if(e===void 0)return!1;for(const r of e)(s=r._$AO)==null||s.call(r,t,!1),T(r,t);return!0},D=i=>{let t,e;do{if((t=i._$AM)===void 0)break;e=t._$AN,e.delete(i),i=t}while((e==null?void 0:e.size)===0)},pt=i=>{for(let t;t=i._$AM;i=t){let e=t._$AN;if(e===void 0)t._$AN=e=new Set;else if(e.has(i))break;e.add(i),jt(t)}};function Yt(i){this._$AN!==void 0?(D(this),this._$AM=i,pt(this)):this._$AM=i}function Bt(i,t=!1,e=0){const s=this._$AH,r=this._$AN;if(r!==void 0&&r.size!==0)if(t)if(Array.isArray(s))for(let o=e;o{i.type==kt.CHILD&&(i._$AP??(i._$AP=Bt),i._$AQ??(i._$AQ=Yt))};class Gt extends zt{constructor(){super(...arguments),this._$AN=void 0}_$AT(t,e,s){super._$AT(t,e,s),pt(this),this.isConnected=t._$AU}_$AO(t,e=!0){var s,r;t!==this.isConnected&&(this.isConnected=t,t?(s=this.reconnected)==null||s.call(this):(r=this.disconnected)==null||r.call(this)),e&&(T(this,t),D(this))}setValue(t){if(Lt(this._$Ct))this._$Ct._$AI(t,this);else{const e=[...this._$Ct._$AH];e[this._$Ci]=t,this._$Ct._$AI(e,this,0)}}disconnected(){}reconnected(){}}/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const Wt=()=>new Vt;class Vt{}const W=new WeakMap,qt=Dt(class extends Gt{render(i){return l}update(i,[t]){var s;const e=t!==this.Y;return e&&this.Y!==void 0&&this.rt(void 0),(e||this.lt!==this.ct)&&(this.Y=t,this.ht=(s=i.options)==null?void 0:s.host,this.rt(this.ct=i.element)),l}rt(i){if(this.isConnected||(i=void 0),typeof this.Y=="function"){const t=this.ht??globalThis;let e=W.get(t);e===void 0&&(e=new WeakMap,W.set(t,e)),e.get(this.Y)!==void 0&&this.Y.call(this.ht,void 0),e.set(this.Y,i),i!==void 0&&this.Y.call(this.ht,i)}else this.Y.value=i}get lt(){var i,t;return typeof this.Y=="function"?(i=W.get(this.ht??globalThis))==null?void 0:i.get(this.Y):(t=this.Y)==null?void 0:t.value}disconnected(){this.lt===this.ct&&this.rt(void 0)}reconnected(){this.rt(this.ct)}});var Kt=Object.defineProperty,Ft=Object.getOwnPropertyDescriptor,$=(i,t,e,s)=>{for(var r=s>1?void 0:s?Ft(t,e):t,o=i.length-1,n;o>=0;o--)(n=i[o])&&(r=(s?n(t,e,r):n(r))||r);return s&&r&&Kt(t,e,r),r};function Jt(i){return customElements.get(i)?t=>t:It(i)}let p=class extends O{constructor(){super(),this.GISCUS_SESSION_KEY="giscus-session",this.GISCUS_DEFAULT_HOST="https://giscus.app",this.ERROR_SUGGESTION="Please consider reporting this error at https://github.com/giscus/giscus/issues/new.",this.__session="",this._iframeRef=Wt(),this.messageEventHandler=this.handleMessageEvent.bind(this),this.hasLoaded=!1,this.host=this.GISCUS_DEFAULT_HOST,this.strict="0",this.reactionsEnabled="1",this.emitMetadata="0",this.inputPosition="bottom",this.theme="light",this.lang="en",this.loading="eager",this.setupSession(),window.addEventListener("message",this.messageEventHandler)}get iframeRef(){var i;return(i=this._iframeRef)==null?void 0:i.value}get _host(){try{return new URL(this.host),this.host}catch{return this.GISCUS_DEFAULT_HOST}}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("message",this.messageEventHandler)}_formatError(i){return`[giscus] An error occurred. Error message: "${i}".`}setupSession(){const i=location.href,t=new URL(i),e=localStorage.getItem(this.GISCUS_SESSION_KEY),s=t.searchParams.get("giscus")??"";if(this.__session="",s){localStorage.setItem(this.GISCUS_SESSION_KEY,JSON.stringify(s)),this.__session=s,t.searchParams.delete("giscus"),t.hash="",history.replaceState(void 0,document.title,t.toString());return}if(e)try{this.__session=JSON.parse(e)}catch(r){localStorage.removeItem(this.GISCUS_SESSION_KEY),console.warn(`${this._formatError(r==null?void 0:r.message)} Session has been cleared.`)}}signOut(){localStorage.removeItem(this.GISCUS_SESSION_KEY),this.__session="",this.update(new Map)}handleMessageEvent(i){if(i.origin!==this._host)return;const{data:t}=i;if(!(typeof t=="object"&&t.giscus))return;if(this.iframeRef&&t.giscus.resizeHeight&&(this.iframeRef.style.height=`${t.giscus.resizeHeight}px`),t.giscus.signOut){console.info("[giscus] User has logged out. Session has been cleared."),this.signOut();return}if(!t.giscus.error)return;const e=t.giscus.error;if(e.includes("Bad credentials")||e.includes("Invalid state value")||e.includes("State has expired")){if(localStorage.getItem(this.GISCUS_SESSION_KEY)!==null){console.warn(`${this._formatError(e)} Session has been cleared.`),this.signOut();return}console.error(`${this._formatError(e)} No session is stored initially. ${this.ERROR_SUGGESTION}`)}if(e.includes("Discussion not found")){console.warn(`[giscus] ${e}. A new discussion will be created if a comment/reaction is submitted.`);return}console.error(`${this._formatError(e)} ${this.ERROR_SUGGESTION}`)}sendMessage(i){var t;!((t=this.iframeRef)!=null&&t.contentWindow)||!this.hasLoaded||this.iframeRef.contentWindow.postMessage({giscus:i},this._host)}updateConfig(){const i={setConfig:{repo:this.repo,repoId:this.repoId,category:this.category,categoryId:this.categoryId,term:this.getTerm(),number:+this.getNumber(),strict:this.strict==="1",reactionsEnabled:this.reactionsEnabled==="1",emitMetadata:this.emitMetadata==="1",inputPosition:this.inputPosition,theme:this.theme,lang:this.lang}};this.sendMessage(i)}firstUpdated(){var i;(i=this.iframeRef)==null||i.addEventListener("load",()=>{var t;(t=this.iframeRef)==null||t.classList.remove("loading"),this.hasLoaded=!0,this.updateConfig()})}requestUpdate(i,t,e){if(!this.hasUpdated||i==="host"){super.requestUpdate(i,t,e);return}this.updateConfig()}getMetaContent(i,t=!1){const e=t?`meta[property='og:${i}'],`:"",s=document.querySelector(e+`meta[name='${i}']`);return s?s.content:""}_getCleanedUrl(){const i=new URL(location.href);return i.searchParams.delete("giscus"),i.hash="",i}getTerm(){switch(this.mapping){case"url":return this._getCleanedUrl().toString();case"title":return document.title;case"og:title":return this.getMetaContent("title",!0);case"specific":return this.term??"";case"number":return"";case"pathname":default:return location.pathname.length<2?"index":location.pathname.substring(1).replace(/\.\w+$/,"")}}getNumber(){return this.mapping==="number"?this.term??"":""}getIframeSrc(){const i=this._getCleanedUrl().toString(),t=`${i}${this.id?"#"+this.id:""}`,e=this.getMetaContent("description",!0),s=this.getMetaContent("giscus:backlink")||i,r={origin:t,session:this.__session,repo:this.repo,repoId:this.repoId??"",category:this.category??"",categoryId:this.categoryId??"",term:this.getTerm(),number:this.getNumber(),strict:this.strict,reactionsEnabled:this.reactionsEnabled,emitMetadata:this.emitMetadata,inputPosition:this.inputPosition,theme:this.theme,description:e,backLink:s},o=this._host,n=this.lang?`/${this.lang}`:"",a=new URLSearchParams(r);return`${o}${n}/widget?${a.toString()}`}render(){return wt` + + `}};p.styles=$t` + :host, + iframe { + width: 100%; + border: none; + min-height: 150px; + color-scheme: light dark; + } + + iframe.loading { + opacity: 0; + } + `;$([_({reflect:!0})],p.prototype,"host",2);$([_({reflect:!0})],p.prototype,"repo",2);$([_({reflect:!0})],p.prototype,"repoId",2);$([_({reflect:!0})],p.prototype,"category",2);$([_({reflect:!0})],p.prototype,"categoryId",2);$([_({reflect:!0})],p.prototype,"mapping",2);$([_({reflect:!0})],p.prototype,"term",2);$([_({reflect:!0})],p.prototype,"strict",2);$([_({reflect:!0})],p.prototype,"reactionsEnabled",2);$([_({reflect:!0})],p.prototype,"emitMetadata",2);$([_({reflect:!0})],p.prototype,"inputPosition",2);$([_({reflect:!0})],p.prototype,"theme",2);$([_({reflect:!0})],p.prototype,"lang",2);$([_({reflect:!0})],p.prototype,"loading",2);p=$([Jt("giscus-widget")],p);export{p as GiscusWidget}; diff --git a/assets/icon/apple-icon-152.png b/assets/icon/apple-icon-152.png new file mode 100644 index 0000000000000000000000000000000000000000..f53c6c55b049ea917c283acd84bd5a5f62a289c3 GIT binary patch literal 3086 zcmV+p4Ds`cP)+9>&D=XlHh1f?&_4V~(q3G{ZP#lKk++ktm<>hUn#^3k(Du?9nR#qj5 z;z)+89b&#edZg#(=D*kJ6J)^W=H}bm+vn!y9BrLDc%(^!s^Q_`WRTEblCooywQQTZ zjJeJrZk{=Lr0!y1;o{=s=f~w==;{ars4%2_WSH(+S2=m4D{-GYd8A8)t97u)^Yim2aGw@vngfR6>1AR=fT$B}qWAjx zEN;azaKz(WRa1zq+f)HK8&p6k&T0S!|PX6)GW=yGrik%JFtn*aa**h4quUMMhtDfsjOaK4>`T6Hv=FJ$J{Eje-DEyv51UWRTfOuB6yeQRaJrYKykoHy4&yM1GypJ9Wi8kL8Y%iu(Wm zSeoYVgM{qE!=ZtBJdWi6lIvBBu=&r=0CB7T|Nqo3Fz`}O_xJbyS5)0FFx@yf0Bofo zk@WHL@gbA=8*R?wFfagT#sG`$0&Ag$XQJ9I0sv;65pAILODvyqtoBk*0d}$|miy5v zEA3HE**7-c-rfdd!X0g%CUMUrX~PD4xe;=x8gZleSytIAEci=D*&zlEYRn;t>NBmN z**!pKjLVd9uEb+q+B%850001#bW%=J00aI6{`yC|{D}GXlwBtY`u?ni`oUBW`egn6 z_|uz|c4J#N9qziXjZU@bux%CAm61|`a!Ns|ntMnXw5FbybxGgMhIw%+9{ua3bjMmR zANJacfMrT4`{bdEfNDTFI1v8#@7T%1wv2ODMfv#j+RKY)R{Z(p+s415p8oy&!MwG! zj{W!T#=)zXc5!R?*vPQ0D3lPG000QxNklE6R(Uw-G@vvY?kDk>@}Dk}b;Amm>K#5LaYK|*+9#3=yucaGB1XMk zSwe_D7>wj{xo5v;_)GXCOV&hDjL32%qF-CzdeF<)RgF<+L>41OYwN4J%?GWBFSUe9 zxUis$pW&;Ft!=Vcnh0eE74vhEN~p|5 zo&ti0M5Ghy;y$j5sI1(j3Bl?vPgAj2#?l(<;x5h-ViXu63%dP;t1!%55cWYvqi;99 zS-4W-yDn%g#mUW#!#D7H3%nliIrzAv5m3$5bEQT`ChqT<7#X1&Mn}We#v4ixwqS5S zCYL{#NG4YblF5?OJ26GtJ6gj!BNTDxbeXdS)JaobYTI*{aYp#I>E!aEc4B!62pQ{P zp3yja>QL6Z3TxOKOPNyXM&=ohL9%*Gri52woe`iTyOlDTm=86;W`DEYZf|b3n{5Vo zFS743o6Xh0V6fS2)g2Rw3)^p5FFhDJH=Cxqnmn>@ielJs4Z48O4F!dTSa;x;Tq_lzu;8wl80+m4=g3HV^1u|(QRBm_uy2vz$=V8P9C>){>B0Vyyselg-`nD%`(xjH{rD2R zM1rT2YwaV?4(x(2AHBSH5e@w4n!GrCdRk{F}fP$foK97vxELtRGAr6rT~+@B#ZZ^pJul!q&k zis{HtoO%=+$H&KJsgVq+<#Mr6Nr3PR#DKC>O^!q&7@9=ls)(Ho0`X0w8nB#+9mSyeCN52hJyZA|pjgV!m8DX?j3W&ZJg|Oo+=M0T?xhJFe3u*P zseCC1aVm4aR4C=A!b6KAKn9Oui=Gp8Ijyh| zpgMHLXa(m?riDMtwo8m}P)Z{Za!ah?fUK3LEhgjVGr&P_ba08epn};aaZ3}^hN#BR zHqJe6iBL^t+lek;zZ=zM$o?9|%4X4WT%j^FsCIqvI=&Y4OmKk92U}v=Ooc7MQ88^` zrvAE)szV)a2?t8dnNE8aBBH-aOmx=R_~A?k)(yNqF{WSC=wXg z>nw^wTu}^1bj(rF$8k>!YBVY=2-y-dV$DIQADwaP9$AnRxU}*nJ5j=UWzDRgB^>SRQ-4~hYi-flm#!|DhT~_CvZyFCF3|k^x>FLVstjU(F zzDP4AY?7V%y+8feGAU(BgDZsXtBWYwcic@}s|$~AuDZl1)hiVm0f6Z$9>UZ{w^o;w zj*iCbPGlIX3lYn2Gr-@u*JvUV%o)_Q*Jyj3I)|%p5U;DbMGEwIk%OHu>#fJ54Z%MuFQxY|}l6IJSd(?{5xd|2(Vg)QwjyM{>g(%Eg|Ts?$L5TT z^7i%=WWeq1?cw3!X`H+7@9*d6=#RkI*-lO%Zk0`VqfFn;9r-t z0c4orb#~?A;sAf#-i3lSiR12CSVMlN3}%_);^H}jr}0}@+8h8hZ^PF^LP2!I;o;yu zcE#>mSLc0x?2(Zxa-cYM#`E;_>}z1`et!4%_U&h7=we~xYi{alVpWUN{QUhTZ=OVb z$TxJLnpi{tsGI_mIWQiiLJkArqbp{r>A@WYve7#$RnIq?lia$kjgwtM zitYhxqyTZO0B4@vGc)y3Ox-#<yC_W*6E7;vQ4C>$D# z>`rjH0Ccd*MNkZJre=Q15Nw|mY|iIZQXz1l_*YcoL_-i}#QRfFDQ?KoHb4V;wpE1L z`AJ4Blld&Orsg|41dV>iMq~g&x&TwYY=(L#f45yT0kQx90GD)9PE!B{{RsRE`${Y9 z{yOtL`(XTaBl_~_;>p>k8RkQ!ZT)C3CK3_*G1*l>IuWV%uM8jC8=U#86LGtu0n7;&j<; zuAYLS>GzM(@2#zKz@qA2Z8Q>zMztNS$-HB+wa^$${2f31+gZAaH$;Z{wFIP-SD~>csW#_06=`eFWrCc&*h~n&m`k<3Zcl_z%(e(cYQT ziqdQq6%~zy!YK8VM{+%SbmW`l05)@Vqv65glRy&{6kc7*wZ6W-kb>>(oy&qGZXPTI zdZ?gy`LK+uDY-jFOcFjA7R<#!4HcXySz6eGuq4QvOu=ycB2c7%^1wfvyp`k-!n}zO z$NPaIO$Yy3VN;>xSbw(=-GA~$1UY833V-Xb#=FI-#$~< za%l=XykI^_Pf66!<_&mL0&X{M$Or_|t>l5ve>uV?in=?2wte&a_H2Is$xjDWorFNu z+vf|t_ujx<%Dg1eeD>;Yl|{FW#bPlwJP%SfkEm4X zKn4u+>e;{m21psWcekp+pq820!#p3OY0%n9U4t{X5ohvRoz7STvI>Xeym+of(S4$( z3)!x0bK)c_+@Yv=&@9Bnv)>|m3eta|wPc&t6tO+xxFa!(ONJvR#A3Wj9D6CDf})b8 z<$OkBD4Z6URgw}uDnD}WZnb-z=4&Fso|EzKEUE%CH{& zi&HM300E~q!g)APq_eNRT8^6Rj)G!HM#UVf_C7VRjDQV?TyFJg4={~L4{m+-f*Q+p z)F!FQIFy#yY-_`#EJl1>`H_7kM9DIm|0Za4%3lg!OE)Tf( z!0X355P0vg%MWtEP{fFW2pE5SqxYtPzt5Qf~`jJ0klh(ZYptU)ZxB?C5zyT^*b?`(n?7)9e=1K zpUeFM-~Dq=;_tZ|l!d)BBafHqVxl#YiFHCBPX9wUFPMQv-EfVK*+t1MJa)HJ0-B~M zno<=UK-LY%*fjJRDp`A$-#DlSQ$XAR60q(NAQ9(Tgc^CmbI#o$a2sCH?ID;4v;NZDqFT|2uT+qCg zbv-9+h7{S!Ksw|k+ddaZPR}bnIMTER?NkI_xU3~`;v|48okbM31;GM1vF=@}YCW7p zg3YVhRA@BU?)q~QiPOn~i4h{%Y>Xx!(b&E{u`xp9Xf^wVLIxjve~|eTt0fr0<&l1o zZw9T215p5eK>{XW5%x5MBoOIAkO&PT5tn(5XkWwU9x1@;!Nc*h|74~*7wJztnJ6jtq zh=s^&7-&Xh4A5CENRcBe&d(NT(Rm<=1qQ{?18_r!4K7nFKi`WBl3(^x?$ZK!R|HUD zo`-jOR1AQTq$n@QY1MhixX2|;+HXb#r1V6}$)qGlMJ+o3Q+hIZwtEh-0Ah#W$z(w? zMLjZ+n9=~Qkpnt_3ZKkneTYrId5y^LX>)~&Fn&%jKPQ3LjL7Cl=ay~vLQ2(~i5Lp; zMRsY_1?z;-y=}WDRfWh4#rIG#7BCT9Ba#F}HC@~8jHoI^(kjeKB|*Ol5hKDuTV&QO zg$N9=!amRx@&L(_A`LixDMSLwzanVQ9~pBGc6A~#QK!K1f>MnP=qVT(qT)h&+%zFF zC62+L@En7O_$ozB#cYsQ*oKxp06qvKAriideEV4xS@ztn712=^N-$>gvW(D312bb` z1j#7d={FZ?!>TYP-iqv%;v-+>-3Sj`3Cad72=Pt^&KN9Ol@^l}=ub~S=IXLWBs0N1 zmDZnK&17NZIT>}w-&rYsZ}}`mw-=F@MS6K+Z$UczeN&R&M$LO5jfe>qhLbUo4Zdpj z7aX;vA$>2>Ax^InftWaLPr;MNiivu(Zu)0J^8X^@A&d~+B_O{OX+Nv^>#v(M;3B(P zk;O%1N)#qS)#6+T2t8YH6GVhDFIMEis2E(AmgwwX?VQhxqc9Z5y9`224~3pWaRld7 z5QUb(TZ^zL>uyoX2=%sQ?)?K6|Ccm(JuD)4o4;*eUeuO7E*^&EedwVHn(&hM;~PPx zUN!$O*Mc4?11N~Pir|ib{{_&%Us&MmQGzhe$<)L zAjZ~|-BP7lNWVHLYn^^^-Kz+lY)roqreDCWnGEpgWbFKwLAscKm!_@!b(u2BhxFSp zZs}>5RO$CBLbSwnR?Z}K#5h!p?=R&cVa*;5Yh=yXPt_8Q9m8A>f zu)@;I%1aq~s@BcXwj?YszH0NaAXdzLhKt(1AVxPmP4<=f$JrEEua!XRXBT%Os+E>V zlth6T(WK#c`sgLfRT+XTd0XKSm0++Vc#*)iXG(sTC=K>iCPzbzeXS+S$Lmzdcj2e4 z5MBW8pg=YE8+73|sy_xp?w?@XWFQ)wv^;kp8Z|F(nTbc;h~KVr93k+aoozrg8V{l| z5yJenfHS++rh%G}Ovt532 zb;qtKU_>Ve8Gp)m!&8VNw!T2;X+2K!{<+@U|Ihd|Z-@{CK@bE%5ClOG1VIo4K@bG- aFZ=--2|INIa z_u+kbzYk~bse5AX+?jiBjGBrp_Diyt00016UQS8_06+x;04S+o;IqWWndRTJAW}_9 zOZu5z+rK=!xW2u=-`LnVxVc@@)ZF~=Va>+o=aDjr$=^XCtlOCD3~#`ez6jF6C!f60-y=GNBM>Dk#c#k9{; z&)(SH+OkQh%jsU&4+vOaUl&f^mJY9sZkWjEUP2}$e3{trsT{;{&PTP*%IezK+S+~w zvwHz=*cYRWR`-(LLl5`oC@%TkhH+z>F;xtKWpwTZ`X8#9W7au+izQ>K@9*zPJ&Pi$ zMoNcPq(aLTKQ;z`9egfo_d}daQGLSyw29eJ%ClFcf|l#VA%`nEH3wc z-K=(yb(U3jt43noa=pYMKXz%#qAtGTGA zcIfZBUs_z`Y+{?c@>f%K+|n@5N=NQn5nIeA(#DMQ?R zk8v-WHMByp`3Z^(N>+MH?)vkeo;F433P#M5=)CVabI~vGAX;)5YRl ztXvS?C$n5rq3zplU)xk}x<`b5=GVPnk$Q7=J|o{wjyoPUX9XUt$)_5V#^XuOgxBmu zuA=q|_us5kWjh>qYBd%PbeARHrM33ew=YasN|06ZobPYfme~Iq2tDg)%u+$l`ddG) z%nlW6?QPBtg-+L6sq8Ngq+Lvox8;T2BmW)j+?6k^FEJ4@aRC6(BzY;gmhaNR%98`W z8^z%1crNd#IbIq(`eBCtmU}lN_O`@?3;~IU!^N@9ACh($++(A!dujPRzXi$L#i_HI zGd<9IG)s{>jQ*&pzj*RQDvK$;TNQNu*D)`dxwUY=H;fd^PxKjiZqNT4p-k{|WQ~gU z#hc7XaV>^wO8;@u9RCHMOGDj^+%WkMsvjy7RT>UA9~KuYhKKv*`?W&&fgX5{CfWq} znu{*tSq6rNh9+Mh_0}>az@=Pbh40@OU~kM}GXR|}tj?%_9=jCO+CmFrZ*56WaC9`l z-Mn>prD=H1vpxkdaH*TzDo!EN(Fh&ft#v>Bo5nEj>oK4VjmJ$_(w9b$s*2w;bog_O zp_(KKc8%~-Xmv+z1Yvq&pMJtME)`zANj+ZQqF28ROWKlJo(^wC2NjwWr8jQkqDZ&t zN!u(RpS8!`S9@?r)s?vRyLa#Qr>zw*HxNUiv~c%Ml9<%Ew>vg;REcCgag#eG63R>U z_Z}Xa7#pW%X!JMX5Z$G;)$Sd4cJ_0N`|EU#9=6pWdU+*(x;>dTROgEA`?sgT=_v3= z%i_#An+jfjUAN#&YgocAHB0G*Pr=Ywq(u`|7{yw2H;z^eNji34^hVtzj+PleP=S8Y zssLD(`j4le*O`Zx$~6rKR|N-wh4#U#OB$aEFBz6 z`h3#k&~pKjBq-wNfN&T7M~ZGFd&2|@fAk(L#^=}7WjH_bfLP$9&&i&eR;%##13bPo zxumwLR|7x}@^~&NEVgk5g_z137D{MW0p#Es3I#AvX`mZM#iO8T7|NoPKz36${e`+H z!bF%?zXB1aFEs*PC@rnnsO6K|I7BH;sl$Yr$?QsiIKpwf(Rp2YLPG{_NOBB`p?AEi zC>j0x$zU$S=M|E|1{PsBw}`@_R5(gD@f(Fho9@uGZowa&N6fH5Y9QPF3#FyadsjhW z${Ne7hvELNKwxDQjEYFy%gH=4Q^GX zW|zaxG^jwSuKFts=NJ40H<6*}wB!Jp`=ew?&fRgr$?PNqzg3mYXLIIvqxgPXZinww zdBEG16;V;|UvI9G+pl)A{x&aPtn55mZv+(8FRvZmwm8hMiOw4aEUx+7o4b^SPLGH+ zf3r@yiORVx&sf~czN}Mw`0Ky)q-zrOh4QXJNXua|T zM)CY{r0#J?@V|fiFVGIXFBi&`exKry{X%wr?fBd2L0%il7f_$RNZb}7b8doGMY-Q? zxnTm1Eby#u4NH7z$66Zv%#UdBE*PUY5^L!$aFWWUsrv%t5I?zP=}>P9GTz~K8WgtW zW-=5SDNw#$t@k<|5}vTQ;1x62;1BS*TjIqVL=nm0ldx}rxOzfuWe0N{SsY)aUBs&q zex=2s`CMcn&8h^irrq=;pl+xrAcVWEt3}09GDfsCy~wXuqh2tMeiSw4c*mJlKUTH+ z`5}lyG24?hBDmQwrImt}No$}c42}af5*$L2xs1viSE{`b)J235$xFy$qc1EFDttF* zb+5}zhA_D7Vuly6yju@M<>?q}nG^Ch5bn<(GB5b{wwNilMa_}{odo{XEt(F~NIx(v zS3yOW(p}bcHF3Y!Woa7XCey7ZB%l^4@(zaqTQ<#=utQSk^^KZ_j}`{|gz;Au4ONwT z&2=r0w50`$-OCgVuOtKuQS?Frjz9O0e4utksr;li3kDK@;cWP`bVJq}9XNdZ)Q?^s zZ|Df$Qr#Orhn_rfSoTEh@8OG>t4S7=pH5ME4s3<*UDx#CTqrP#?^B(Z_ve^TwH9i8gNPN!zO_%`ub;)?W7}# zaTfwl>lfnw*VT*1^_MqsYzU92kSbq!uC>0u6G}Q#>~oIJ^NgD@H8}Ay_)6sNHJELFN+)ZzY@so;b*ky3&|D+rFDznO>FA1 zFqaN8*}=`d6sBmy^-C_|)O=h}`D*kHXx*ztxcM*RnrYc@YBwpdt2aW-3Gw*)ld%Us zI@=%ktvwo`6yb_V2L-mr+aNyq^c^0%2lTAA%4tm1HzQ$`VoTZn`&5hSWFR{HbpUIi z>vct(rECf>SW=ndI`T;jY;g&#b6+QZyn}v_z8|(AGOpv;Q|q~`VIhc}9)83Y#?oH-;@@#w#JR#p9j4544Rtf>hqtuZ46XNw#_}G; z(HdU?iV(}=1k>UT4WOdj$Nnw72{yg0{f@BJjqc%?vIQy9U*g#mZPUMnC+4eLYJ~MP zd@OwEUSUxYTM7EpxE$9mpw#xWzuty?UNNG_ir6tsP%x^Mn3$y`WXSvA#m(x5Z|L5#-Rm09!JRxY+LX z>MYb++I7g)AgUd5eUMRry9qQFu8O~HTm9v`i>YbBJg-O~atw+gvrm@ZGqln5E0hS8 zI#DfaD@RK~wP!)L5#A>IQJRaa-oU4nBU?;UmtTx*C_D-|(&r;Z2$Ye03c!nE6FEMW z;|1(&Tsi7!N!AZjM?S`CqZNKyahk){WD2t7M6Zk^T;Ts)j=9Yzsp_pG^iCpea%#yS zN)et?GRoFj6LbzgM+=bmf(HS5sq{Cl43)kcm_UkfIRvDCF_?2$YL6s+{Ds7l)TR7j zSxjIM>j;|Snb!Q{D5hDLlosT`CANdT!o;BKTg)0O6FN1U%-D3!LZ0`JH{y;pW0=H# z13+GQp?Ngg9ntP;s7K$Zv~y1EZo1By;A)>mqNt)a61>5lLfpZfbPWj@h z?A>K;#?00~Vd^pxDCO9hl7=?71bI{K2O5)G;rM$M5t<_(Zu+*$TpV|gJABkLcJdQcX=9tO<>X}9`58x)QQ*||vQmFO#QXm32cJ zA+Fc2D$NC)q9`tJM=FLFja%S@R}0kjSH?NR9s}RVUEH?sL@ffj2U&4C|$)uhbuk#*fk$z z7-;(n)TO=yi^Bqb32?QkJ$+oW{rIE18o)!?yss+yk6^wQ4e7Gh@$EhamH3TW*7uo? z$hjtitChQRG2V$3DGG0+zA29U1GtZ<-OCKy__;_5lQY6nGV`V)zZV?n!ya4`Sd9TV zK^LHX)lTTZG$soDM1ae;EU+YBOlnpOQ(vTjT)Ajm4A7y9$zGLzRAX3=ncPZel#`rL zci-#2M-kGA`qm_iJFp}_t7Kx0-%C#5>}p1mX6`ZBnXG;btr-|nA4@FA#%jYw-r3+< z9$BlG6xsHDA&k0l^SgO-{O(%m+N;C@g5Io!PEYkA*u3J7#bUE?b}G1A*Ryf0+SI<8 z?*qGsw-8BTC|}lJ87`Y+5?PLJi&g<)D@F!8V6E2orB-X2Mkt=ZUk9dj{a%tS=(V(I zLo%NXQ586+B#OzBo5!JfJWfxbA-5m9Po`*EP*BiN=rW&bw&J3oW@vu#1F1n?BeiIU zeoH%sSj~kguokB>*?IWKP4V}t-6F#!A#X$liSgv!{a=ksFXQWp;jMYj!P?{vQmP-^IEEt<=HC$(U z7K71@R&@*f6RVbfC?J?ziS~ibeU_Xj%=EzcQ<%FYkWt;e1m}nfJoZOoB&b5YWyxREyZNBR;5olk^kjFSoCg_Vcts=994!xLLm6`uCkqG-p# zQoBOI8<9F%vBBX1M`!&BY-7Lf8w`7Y&aDKTZyH_pYp@poaZ!Pzg$wiTSss=d=uQ@e zBrbi#!;A(VEx|(BM$)ItbyG6!K!nzDj6j&=(I;nh7s()rGSs!k&V-kmy>^5 z^fM0GbM&^McSBjeLepw6VUqoU8pq>LhUCFy0*biogbih%1n?;WG6cM3qhRao4@=TC zIs~#3HR_{Fe}7n}r1dUNqvC~J|4lUF!yl94j-_L+V@;eigkXknzQ>>yCS!A^ zb(9>ddG8#q|LbWi06WqSod|_~a*>3q4cPR>fHnq6X0FtNM3pO;u6R#kd?mIVFC3G~ zVcxuYy-6rR<3`2HUN=Cl{OmL8b9rQ3Z$|R6w|Yx$5SvC4-HUmZ%t*=w?s>dCi)Rt; zD8)>AKtp2}g_YZ;>uEn}u>CeEPC?`~DFe=RK-EUS*pT?^RAhkgss_U!lz*5Rms6-2 zSKmu3(r-pB0;+_IHNS>ufuh$mn-)vz;5Vo<$q9mit#B>)}jR=dkkmNJ<)GN z@!Cw?YO2$n(U%;NnUJ<7C#_GOl(ruYVlcxw%_{gXCUNR|e&RnW)qZ8ua!}!AMjHfo zV0(0Y=~Hl~Q%KdbLGk4u&d^pGChJ`M&)D&Qu-0=oKlYP=O6%WmU+=5metnHx4}5u| zA^GLJt-K)|JGx%#vv)V+9?Nx@slK#`V<9>& zl|R!1Pcvhus~02L`J}Vco_VdGGtV%>$C_#_OUVgSlhD;3mgydLd`SC2+BZv!`N>0o z!u=xU&MAqr*=^^__2;o2=UqZGT+m9dqFWC^ET4;+?(|#dvG}nkXsW4m95cmya@CZ` zuOy{2WTdCj&vaM1l}Rg0578NI>MXzjDe%7fW~bla*Cm5~@go)@ke99K@EZ2_P@=**9sH>lT$TcHoi484vAd@E%w_`% z6`+$+0-etBF^`C#7%S#LJ~N_zHO@>8CJLL!w?PcWxbk@6OSlC~Jtj(Dqtn9FgKs-_ zaHcEleimQThhbyf2*ahiqP%6_Q5wURRtW3@%5sxq!{D**FV##{xiBu3K;KbfeYP=3 z5Hyn{Z-9Uyfzvl#s`$d{!v>pHkR(Ro)2(uavkzc3vbtBwvrF85yTd>XR5z(u;!Lbf zehsw{%X^E4%p^=NYZIQX`sAvQ20>aW=uUubMrIBMSm<7?G=HqZtBx}#+1K5_dK=TW z&67d81}So0CIu^D%K@2st>q`77)Oxs)0CRsa4yGJb!b zipm(!;G1$K zJq0?!wge%rYzt$CAP5~3YLBLuxW&7`h>1a!CA~9G7LoI5jbXgRT^Ivaw>$MtKsqZb zTgAV;Knx02Q~@zMTd&zbMwWcAlVqdhaT|&3C?XULKE55})A_RD+Qbr&j&X@9%#Qie zPEZ+0>;_*$c^B zQ<}tH&qr(s&AElodjQJyuBb%7+#BY5r%T6axQvL&k3w3KQKK|tye#GW;kl{R>b6jlwyARzOmS}L4=YvGNzYW!2d`p`{G!7 zFT*N`meBLopZI#jrBxRv8i3`_f3-W)#hZJp;(W)(n=Rtu`}_ygE3LPAa{M-+@nQ${ zTn4gim5=-9!gJ*^8Y+lUIQ>#2X!xit9>*Ah|8KOW6V_e((6&R=ZS;FlqUVk(nI0RI zZGduL@YTFi7U5$F;^p|5Q=bF__ysYc{xYsUn#@F}*G!&uLyV<8R(ptb+V!eYB*y#O z!lJ38n9w;=7EoAVX-smE(LO@c6sDxf3Ob3;29W(Qc?$pC!YKPErK(dCfiuT_5~@Yc zL%`pwy07CkTY=U2`6gdOVJoN+n8Y<~SKhD*VTy!dSUj7Qu|ggDA{6G-Osa&c0*ukhy8fK4>Z9u}yRxl)6`KYGntauPn*47`3*uJY^Q zc#etdmwulv_M(0lEJXZml6x4ZCzHXxARR2p$vl$|_90{P$hJ5Xz= z5S+AEcZzuUJUosw!n(aPl(KxeG8v%{)7bX**c+uKTl)M=P^`R-*J1JR-=wh8p~@jK z0cFzleVHgs)aT~YC`YYGVA158m#Bv)P;7^Om6q2Kys4cY>d+*XS@TMHDQ{}K2vx-8 z8Ea_^hN@4p=qDODO4w5@P*GMHoxn?K{kRp{#>sw@fV+&gbIb>h&nI1)pv6OqpC@SJu8!!?B+MxeYbsv*zvs2cmHw-b z0#}S~8!!}>55@)+4H8J5K+KxZ5m^LVv05%WXUTe%iJ^aQd~^EmrB#|9%v4V*ZZW9G zWyeXt1_VO6aVzB9LcN6}MYX4GrQt9k(LZraJKw`xU(y^eVJXs=q;GAxfx4(%r3WDx z;e4OY!kjMzW!7-rUX%HGN275I_UX7dCA&t;U7->pMD7!s!Uc~>_nuZ>Ux6?gzy&ia z0|cCT_sjYSYM*L;Jws-03Xqe?@^S2LIu@79KF{28c#eq3H>F( z?w%zU5H6MO!-rW(eK z*62DPk|d$-@)JR?C=mbjeYPJh5k6wXwW7Z)isiqvUy)hH z4qK!+s$w-HMnK!N^aMzR2_&=S@hoE>>b;}_MmOw-^u?#!D*x$UIdLZG>y9+6HzdlU zz1b;(COPVX8E4@CI&T1@U5%QJ=TU+nkUilxoOG1m^UK62cnVGrt|sq__bMrg%ju*> zj98|;ahkBL2rL_$!76!NK-QXR;zZ-xe(YOZ#^ zerQ?Mq_in4ON5pOV$;e!bVtmfLpjId7)@5C}6`M`C1(aA%`2Kq;~Bs_O4uRzyY zxT#-U6c|3t_0BpA36q+{EyxH$PF-|N5U;ckC!~G=_yfOAyo+SrnyvhImgazG6h1kY zN+NtD3P`Aar%yD(^~2h_OJX)pIEq*b z8Z+?rd{&P1+(bYvq53SavLcH(;ibQGzv9eVo6Fo63pcM~>Fc#>CG#z^K%FrEYN z)xUnv;3NtJH3OPhB@$#-d- zsW~*Xax^kR;%DYB^Yklf6KC?;pZz3jo=bA7DijSyBd1u+cTJGyWORGE=_SqcQ1L*e z4+gz3(KCOc+KHyDyqv2UO^KYm43W+iT#3%W7|Qo(TJ=@H(m5gO)Y(9gcG|J=uvQu5 zRYQTEnk-0dKimJwbm=zzX)%Vmc#286Q6Xr2+JgwB7Cd7=pW5+8F@76`gOX0DCOxtt zm*Cv2y?Qxh8A$mp{RM%NzOz&FzM308tt1?Jr0L;OA3yuUNum@uvrlSf{*nM%Fg33G zbEl;uYZSuz(+g8WMC_MTlZ5-U-m85^;Lnr7ADn)5s31mo5}EH`4Z#r25s$Y{*r<{X z?HX+l6Ap!Tf59Do^kp_aKbOnusT3?S1}x4mCA$sZeIkxKPaRS-FgY3)E0W*iuozH@214LT#REoOe9Nn15tF38|^HFanI%`TX79u1Yx z7C6+vymwVn;vaD1vW?Q8&v&I7myOrPZt7nU7pdF6ul?PB7WZ-;_TO*hp{II?miGTf r+U0uEe^juEr}?UCl>fH+Kgk<#{j{G0n?j7pf7J5QDpFMvrs4ku6VP0E literal 0 HcmV?d00001 diff --git a/assets/icon/chrome-mask-192.png b/assets/icon/chrome-mask-192.png new file mode 100644 index 0000000000000000000000000000000000000000..77c39a2a828560cdfa70eec967cf4e5892ed767d GIT binary patch literal 4079 zcmVm=KlHl)IB`_b>h$f z0PgPYOqg@-+zD78XDPDQ~-kH`1}10h2_4~@&Hhf?^aX!hSdA>+v{k!thj4);c=!_W1xsjP&{Z`uh5=$nOAQm(d>|`u+X_ zWSIa}lG89T0BgknSd#!tyP#o@07;DiJBZRcLE0t&*Wv2C&FEf@u*=!xu*KB?LW|Vk z=>Ul60Epz8w#${W$naiVzR=zPU6s3OlI`^OqPx!0D=Yu}{HnpzbDFsASzX)Y>%7g{ zC27I#I5WuC;`dA{g{Hs&T)qHXlmIe@004gOM?>D{?!(jGm#e~gox1>OoA6my@J&n4 z007Y4<^Wc{exkhrY@XjhKj!W9pRvY@s>4Knrzvxx5p12d%Gm%yxoMNM8fu#W8-nm- zU_Wug08qSRk+T3MgYGaY&)V&|#L;Y)woHVpM|h$Hq?qC7?U1gCFKogZWWedPv^#sIA9SJx zaiIWPl>h*B0F3VY?Cd0q=B~ctxXIb~+1RbT&5NPCk#M&3wzO1+tn{p_?TCl~41n!; zb?pEE{`2xbjpdlK;Fze@g`m?xd&^XV%J;y)0ARoCyuCPeqU@8B?`~|_Vqn-wOZ@Ne z?BU+~nl)($eLcnkJ>009lnb zkb~cOdEISi(Ge8cAOP9|08NwT0CLSfY`&&skpPEy0DNlFI6wGJEcQw!g*FY^E&ziK z0D!IO)4|f4hr<$T!;;3ox3{UTn`4~!_>yop4vnxiq) z$Z}fd%Q+|fz;=X#@csJx|J9>HNl8gbNl8gbNy&c-;duHWl}Zta5CYD0b`dr}AR8OQ z5PerTG_%JZzCL|?c{SoI@H0O*fkUDxV0V$3{Fa{uE{bIdb&lw*GxA#$9+7Nf z3?LNoW%5s3YCCg56axT2&=GKvS&zSX_Aobsn&F5qE5BlXMM@<3xds}a-N)w7P*}vW z12~9nFmm0(s^GB4rFC_CF<}n?{dzzMPF%cNchzR$R4lLaj9!4O{0M}8g{BK#`Sj)5 zwQFA=Etq$p267Qj2Lz^t4Oz_Qtr&SjIE8Th4N{U6x2`JYqPYs*RJ1} zrebgZev0vWy-j|{U~-8t8Z<(*tLw(xY(2ZD^kknHxYfck(2Q4`RI~MY*9behYP;$S zqf)J49Gpbl(2|(wUnYPVAb@;WDLx> z5MIl;Js_9-Be`;?mfHhDcq!uwNVun#!j((PtNZ=5P@aVV5YEC1hf~n1b;J`xO9U{0 z;u1s$7dw^slQ2#08W{f(ZvGxf~v8MtN0iMyuMGEDCxzON}U3~`r~s;>39)UR-8 za~tye{mghP@SGpRrrt^Ssy5_wQ@bLcfx-Tm2@t>en*lIQ$I(#e(3_39++La2?Wd+j z02~325C)J6`NHAw;lTZ^*&f_~@cG;7d>&ozJ_3{!_=FI2hMW;S>9YE)n`f|YKE5ii zdu{hT$DkSbgaA55B5o_2o-`6OIc-;$=V$SJPw`%0k6v$eS;=rDVAs#!+`jaWj`RJ6 z0R|lbr`2e++YcR0>2lEgk*<^H{0L51+5BXrJ5}@VK?LwKMg*a-l_2Z~i(AJCVf^OD zK%+MrN&B$OBr8H0@C}250T=1)K6zd)`FHX;fPhyBYDSzS;oQ4RQeFhfEWQggLrPDO z`tI3M3a2O>$GN}sp#T&`@&5b2AMKm_ixN>3$Ir#FqBp&$5T-^bhfrhEfq@wi9R*EM za#0ddbJa=|SGUjtjoheh+pT`7peI2Tz56XHD*B}e3ZftSANnskceJ%PS(XL!VRv@# zInK=edd|6X?>KIQGso7g+kO=OSGK|Z1MB8)e_&m{;Fp*KLx#Z3?f6jTgI9|d{tAnR zUc?5tt_k2*EdCW%EQcYZ)xf6hH^D%F^vlpM!kU27+c&M-c#HfiAGP&YLx2s9S$p7t$0wy}AG7%iU}GXa4Rj|G*a_Xb#^y^ZxDH{(18F4Ezi85CX`ymA}A}e_%@Y zd$)JUzxWs?`KA&1C+=zy@@so_`HmC*Dx~inn4jJk{l45Z4-EVvtg*vZApHl8u^c{p zxMKQdk1ad_(rc=h#3C5s4-wyYG12GI8I&DQo(oAAC-YanQg49lBskd46}J^VE%vr27r$rJQ%D%VmeM7@#&zJ8Bn}>m3H+o^;y6(Hj4eNN~yXDj2I9M zl$KnstE#o5nRv#6X3N!SUXyJZJ$Ud+41!t>U^{!|;K5M~x&|&_Gg$pKE|ZZaA~TtD zCh%V>_^2wY`;yz<@!e+K{^q5o75IlC!PR*?8G29Jv>BqUHx(e7fa)c;+2_4I3Rnu! z8)7D-JUnuQ+vCGiRH_ecnM~@2>HucY0w6qMOOIcB?o3OK5O$fLl>GoE7>jhNvxROy1HD0{5=6t-|wbwKUgAm3;vG4V!ZA+KY>d zSVaYc6wpY3>4+luEsTjzij5QsFbC2TJP=1onXy92b}?dMjM=1koE@=3M6gAnL)aCI z#auk7A_{tWsBZ(29*z+k71^7eq-fod!u{E4i-XpM=`&@v)jY9T;Twvoxfm?V+a)W+&P(TMcRTR4v>&4K7ZI>|NIqS1 ze4Gm@g)CoA30@n;D+^rxx4$GrdeZ`>;4`=P%|@V2BftQ5sxc*(OgNG%{;mLfejf^O z6>0s^5OiWFAoQET= zW*CeF?R;6JDP(}``OtCE0|ZL)gdG?{O_NIA4&V`lP!JGmNWvHA;g0PBQ1?*hbLlB8y56pEEa9ElNARdN=5Sk!_C289t^1@A<> zV)5itQaOevNh;2$9UvT#8Y!)a4Nxq2_tIE7+OnW(KD-WS0!(>MVCz@1mRNLxJ!%W@%sl|^R| z444TsHrvElZrm$5wFuCXtz%*vXjauY?~SqCc}3C$Fz7mRF(unVLdwet$2lw?h={CA z4;N~P_3nktKEjIl8qOA)5^yR3zDcj_9?O+mnVJO{_+16zWFf?IlG9xTLa3r-0VgWl zRCQus4F%9DPy+xA{jrqx*fO_m50maoO%yyTBN_j&H07NOxoGt=R@acX`Bo+3v0S$k#rGd}boX zp3P{SU9=ITBeS~#V`zKuD>8bbubVyNk9|GcXYKChJFod~KY-TJ(b3V-(b3V-(b3V- h(b3V-(b4%Ud;oR_3fSphSH1uM002ovPDHLkV1h;|2|@q> literal 0 HcmV?d00001 diff --git a/assets/icon/chrome-mask-512.png b/assets/icon/chrome-mask-512.png new file mode 100644 index 0000000000000000000000000000000000000000..b8349f4ea1d00b8bdc5a2dc905376803ffaa317c GIT binary patch literal 9208 zcmbVyRZyH=(B?bC;1XPd4H_V52*E-aoB+XsdyqhIhoFN)aJQhr-7U!A!Civ81ef3p zf4;rit=fyNJvS{+Rd-dNKHaCfI!swn1{do&761Ua?`0)b0Ra4u1w~D-mhA1%&(8t9 z#FLW~Kt1;M0SU55KUMy)y}b<>bO7G#`)5~S6?=d|;v_9C$R-PrivohSucoI@{@!2Q zK5QRf0>)kIPEM1XN7r|cSN;7RQ){!kr+!6Upl=7v1!*a+>3?tU<2w(<;&#YGwly<0 zCx36O9bU*g{X9@nVt2|}+B=P|Ul(!fZ=Bl5A6RUk+cNX%Z!~pV5a2(*x=pGd0z8WW z=e*;~n~chyU%$5wE^pokmj~C6Mpyp=lw&BYGfouV13GQ{^6&qFhS4?M3p)=-hmRK* zHu+sRCWvDhiNV#~ih%{~ga!t?O#PJRc*G$rZU^}0)i}2J=lJ~i$B!c&P4(oK?zxlD zihf1EVqD8~0A4n+x?eTAdh||cWc^@fWhcLD_M)S`q<1d0bwa{3Uo&9verM;6dmbPW zzOi?Bx3TfPYc{#G^T^I7x_R8atUv4LRzuHZPW!}hV8D@w`>GhsHoZkFyiz5s0+f?@ z_?im{*v@Pi^2qOW&TeD%DLyj&3TSsfcf zaqQpdMi@0$1;vJb@EM?YABgJhNdM}PXQ?|}UFTA=w>dWxR9n^<MWaJVRMK_o_n zcFL2j9Qe&BNzLRXeU1wFFM$g|IM=r#Xis`!UI^~8p0PZsow^6N5iGQa#**vHM=^^M z;z{{Ss7e@(Dr#b%%uJDu_olAWD09O{!fD0qm%=_BU5ZyTL)WXR1INMt-2OkG2clA% z0|PFG;SC$VsHms_KbR3EXd+T6QpHX)Ca%T1KiV*hWy>S2gBUC58X|dpmt$**IbpNx zP&58xh;P@iE)@!GnPZrfmX_|5L&ax#JvO-#1;$^!;nM-;M0BEj7Tf4>KPQYv z|4sT#`c3y+>D2Q%;%=!Eu zzua8}&;^=&sd&()g_=Bv0#;=+o$;b3r5y^0!!ndYwTcId0lt5Hg38EHy66Fz9-_cm zc4qJt!Qsb=xa2Iw8-1WCZlIZh#eWP|KYpOl?FQHoOPpB__Pb+C18(aE@a6$e>ZM!W z3NH*)BinOrGRjw!0-n}&3Mt5aL9l7pPg#`2pr2xnY2Ajtt`jcNI~P*A>0p?BMXjglnNm z^SK;vb|i=)bHgGmX}(mfKK(f()_S`>(X+&cNW@wf;RemzBq|T?+%0bV_9s>A@;s5n z8$a8&d6Rq@a=E`3Z5W8nzNEeyISEB}=d=VwixXh}{nKDzBBE^!Q-2sV?{b>S6cMIx{c672q5%;R7Iix((#Gdl` zUQ#_IqLGf`?Z(#JVqKfw$B&iqLyu#czF*HbcEWyoCkIE_{)Qoxt>Qk6t^_^x-8$Js zPIyz}9>u3}C?s{Zvq1Oe1L+EmBVDUD|H$VJZQE~#4 z{PUA{)NsVFtSj&_injy{NMhPXZ+8o0B=o`M-Rj65Gx(y?mQ@Bso+P^MWVlcljCcrl z!~q3wE7dpKok*aPLkA~N$YVsOT+e!{%ON|9I#Jr$1h>4$eT%vn!#|TWM^{!>nC0vW zCX{M5=Komd$ItiIcy}yrJse)CXE*vUkS6||oD(-%slPnBtvmP?t1siaKcrumzgN=! z#UtpAHJSb&T;a5%lukt!PWh(_=kY?K#7t39`?cIfeFK!pWNr-d>7! z_-KH;kY8L~FS?eYK5b}E5r#&&jwG>HE{zwJq*7Q&(p8>?9G7Y}IyNa*;;ejIow}Gs zsk{EQGI3|yp8rpDRN5l+zw|6Lmqu1gqdj+JsRBYz$!wVre}R_q%wol1kAZZsVk0$FdKV=9oCYkez4~^u zej8*`h>t(8@o_Ua*|-iLe{kbcD;*CWGET*qGE+`AEdldXkwh?Ik&V|F`+oy|plt@| z$2M?}lE^V^LMfOhf_ z4Lp-v+4`l_Fa$20AoMZx(DUhRQU(HvQUGftCm~srR|THMTM0>J-ha76krgjIOW=s-n%S^utJW5 zbpYKwYB<>>ZlooKrI;{alE)N76hiY{4annX7(emFgHM@fbFCF3cYz}PfEC#F#3m@O z10EsVR_rP7lB zEl9H3XQU#YKeLX=;G3(>wHQQ8SJXR^q2SpeqBCG0Ea|C?kO@0!rk6I5_x8m-B1eP0=tu0Z=c+{ z>+lap1Y5mWL-w<{7dAgw05?2Qvp9}IkGM8?G6d9mez2ca9{(^dR{{B?j4fPhc%BYCa|4fUWKY@=jUz{(oM1$*?a-O-c z6=xWRpzf59{G+I^uKrLss&uB{@N2l9;a{FFvLt@gI_mP37CAXnNZn7}xZPATPZzvc zVS;Fd%N(GJ#7|VCxMp4p>?4svVC&wCxzg_UBqn%MtDC9byoFFi6yXr*@!mY^t_WVb zHxH#GJVQQ+xbLB$V;9+bfDat@)S{{$IzgXSPtIU?r_U&g$=I3#rgl5j(SAu#z+=~B z-;dd6B42(^!{8=-ms_eQ`XsC|9Fhdp$Q44;{Jq}4H?Oc?di-tGjZsCwVQx>$r%gfA z{_*|sgUw7m&Jy-+Hsn|EtH~^+WkD|4u(gac@Cof4gFEh}3CZG~N69pIEe`~OUwS=< zEM?krBj_6^uud*ZsTNRck|N+VYkQ;LyF2QqRq{D26C-y}bx~_`)F;1N~3NDbK zN;q|_Cj*0a%A*Pa)9WmzPvT*GszS#TGoI0;qkpt7=Z13?f>@2n2hG)%eiX<>dlT=6 zfG5XL(ORia32KeF1%x~4JRS^d9um^Gp`lX$!V!_Mj|1y?;eY{#=j329zsfJH>7m5{ zf@``{WYgN=(ZV1Wl&W~%Vk#KNTM?dU3G5tF#ITDl_4qJd??$7mPe;ek6Q8p9+4W@6 zS(o$Va;I5--xItmt_&rm>o}M8pN$R3r;geHTh`CRI3i$(5Io34gS(nH1a34e?970F z3Bumx#01)OLJVLNJ(MJ>=z+h-O4#Gr9S>|6kDPw7t?$y9f>CF#{ zL8&5awX#AFy*EE)fwcJzr;HP_ywNp;Q3wSDqO|$>m(r>f-@=UE!tHA=t;7bbtuLmz z#C=z%-}7ekho(O0d&nx^79ZCBlHcmE3EQfbR50Hi#rZQw%yiS?4B8Q4YH|B9`#i-*s++-*Ih!ZK*@ zi@{bjtY?W+dP8}6!E5eh0va|MQ*5vM_;htw`HdEc!eHgL{4Z_ut9zORNUF+4 zWJJ_a)7|6A9y>KA!D(1q?d_MW2$UO>$t_H6n#hdjrC|lOZS3`u4W>@^^S#nZ5!7hu zIP(fWw5a>` zZ*x0K?>xuXlKW|!1m7ap0=()cU8XGY+Ryf*4Mpx2L)=!W-rfH(Lq42$3_NDFo}EQU zFHsrnbi<#7`2Q?dhjy>q3sB*O{O?3KXYKAMj1?~MBtt)QIM&wICTfR9u=^y-cvZrV z;cuQz`?$In3r~L&#QgVC7U$;R#DIn@n>Xm(4WaaC;2dx|hMV#F`RkfKX^vwF-$ca2 zmRqC>7g;t#^--e;jnZdp&OlqNiOIX4k>0qawhXTti)^Tobp|-d{6*2Wnq7x@vTj1M zY@#!=C4YGDz#1+qh`zaUs6W*pn%$TkNGZxxuyZtd?eP~wzq%fWG}IjN!U1F(1guQ& zHP*=VhzsZHj%prDXl}Kiagfy1^UQUlHj#jbZCJjIgB;bI_0{1_oCq7jY`Zi2G@~38 zdYUW4daM+k5hJnJCBO<=6DoMGo+B3vhrNY2)gVBjkAau9XUjjZA~=C1%IwvKbPzw- z&JVcbepmSr#)fRUyHz@Q{%|?9pUK=scAo6CH<;Ib(k@K3wTwvTf^WSHm&`VzyWd%G zRCqQqcE3YPSvhN7{qOfsg!_(gUIZM->?-L;w<#Rdz1{hLGF*gCr zOfX|4+(_frQ&FBPXrW`2qxH&+ICUND91rA7F!+rxWIYJ!oeFF0IANLwV~>9TUvlxE z__p$UHK%3%C*Fjo!3bn5eVkeU8Dm5&6|e*3b6$IK_Mi%F_~rEA9WO$TolVTQtm?d? z`me13B(kR1d{25H$?)UBVR-xOS}AdP#ni`gE`@voDt)hD+nrIj6j~K+6#GrbeOi`w z>=!9Ybm+P4hgx7d&6hiGz-J_+lX84}N9+KER(O?nC2^?tJL;)syMeDt{3rI&A2!LW zv@=YvFH5T}{v56NdE!zN@*LS{iPclfXgmzv*qCrNA132VVMVkXzU#6jpN@|&JTTN$ zWpQEdp3uQm@wfz0NFZP8{>T|hBJb78PkIVC0Fn(56eNPa-=&!O7nc1JD4hO=v8AUKE z(In2wK`xpi8?bC_@8gw7^;@ENWzA1y2;HR&rX#Ybpw7G3wwTa;N@p)0 z-|BwVC)@564xjSWdcX5b*a9ka>hHex5+Nd=mMu{6iv7iSJ$6i*fU-4jlvb(_Z}=i) zod3IjmVPOdGXE!{Y`aeid2SpIwfh46pZF_6gOUJQ9lymS7TjgCw6Stae>y-gOf~)v z{dH{Bgop=Qbi{sGjckMK4gefXSZu#ruR9WLnzT0yM2-ZZ9TG! zj;WB4$g>dVS^U;s%(*K)X#wezHR!3?UqFNI{+r&i%R=#Ze6Dt~9dpQ>{2-v4A z>vgT^eEnmfLqDy!s%8z&>qnPz-Dy7{h1O(OcbVVK_>qtFwill-WW#O1mme(Xaec04 zBDH8Cm0WLD)U>|ja5$^TSABpR84v?snO7bRTUC{cp5#B&VZysAKjG*%`Zub#Pm1Rl z$al_;zdKm6+7yaQfwHKBJIcT}N$ACL10-B6pFt&yuS(A^#NNlv&op12QTs1K>^W-I z!p?1a7pJ_QEwtqmSWwj}D$W^^AUb05QvH;MaUCn|zVz4-L0j03Wry=&R$|}QR0ft)xWB!l!IF#8RF!q1~AuFrxPFBPXaf|sP_Amb?PrpIQE$5^x9AJ zD0LyHSG1EN%L8T?I$N^q3Iud&HuN)m-k?$-2(2vyU1_0Y6UOo0J{>^X$2HuvQ%DgH zh~k)_{26tJ5L!7X1vBb#6vR->>7fvX1AfBVH!u-Sdf}Y;l|>37J73DDA#h+Yo`o#> z%l!sg)-GFSkg9wD`wyjD4eCMzgj!C$*4#s>M>w#OpT|ai%x}$5a~EdJ3IZOb(wt1& zD^u$W$K@Pp`@ZlyXwuc`UH^Dv{Ilc-Afx^Qv{7o?8F=4Y+`$e%k-|@~AZqBIym8WnB^}Xez~4Q9K>t_1p#XGf2-PV0`p3rj4ZnmZpU&i!-tF(>$K(BZPFl8}G{v zr$%9O+B14jOOsu~wKUXFg(ES}QeIS3hP!+tLa2VARj+uBsa4{QCb77AIX1yGbqXp* zuZG!PbfSxCH+p`qI$-z2*f%mrdH7X~u1YIlVQEnK2C+6~htfv#z>gZ5X_hm0t^*Rp zU!M6D1pJV37TBEA0t+S^j^EFyxEYZ`y=556BI+3O`S~UZ;RNY>*Ok5v>2~CV64_tD2 z>%FbTbC}{??%#qE55y_jp)^xT;RW{jk}RU0CSn$R$o)4OrW>lBuq!Z=c+ca?j7|JQ1Ug-LM-Z0}n3~RIP;6!OLt(Bv#cqdhFZnnY zAsi1`qOxbv*lUAF-5IQNR8upkk1a_+#0Ew=N*`97fd<{jT6XU#?3-t4a(=1oJR_)( z97jJIlm2F)Ak2dfvBxjj^so0FK2Q|bS!YAOVQ@|bHV zAmC8;;%5-2HYK5NwLXw<(eb*$x|E7rtBbeBEbPls4m*lTGt^9lvMOF z+dK|YIr)nT(D?zqL)haK_}TI;pz7rUAm@S&KJ3N@I#dTrzE2#-*9c;MEo#@nO@-j& z`HwOoGRm}p;=vXVV{aZ8K@gVtMj~ocW8OF$qWOumn5M1-{*(w*(Ok4bLT$Tk*tzzj z!0a}~t{|^^_HrWig?OSjD4dW>5|a5wnrjC1%UbPPK*SFeyOL46y2bed=37gaZCFl# z0X9|AnVnC1#RHnG-%)0|MX|4b<^qayOL5)i;qXS`t)sj#!~boR;qwz%86=hMR(7+z z{25LTyxx<{38O>{q6i{b*f+ov*fzV- z>|}%4cXvRuEs!8Zk1^%_wM@d95(qZ3NaJg z?rI@jj2UBA#8h!CqRVO6RS7uhSOhG8juXE8f#bFcmu1Fv_0O{bWgPh22OdgM%->PT zUtz)>S4RoSBekoJs90XkYyaz)Rn5mxQ>E{lv(`{@IsLhvpLrQFsa9)Vh?z_szN8Pf zu9~v@Afc@I@1b=+ui5Db#5-yE{PGz2*4 zC@c0Htyst(jCFtV6myLBdN;1I!}%NgzTI{=k!iD9dki0djyWdk%WoFJe@Gd91z8nW z!|1Wwr9R{oSB&t-oBCk?0UWGZ{L9ovxkrF{2gs~RF16{EArsLx<@*(6J&Dlx= zubjY|40g_8HRA6#t+e~MfwlA1QY!~2yR9V5S#LLnL*~=oCc3rgF4HHckXO2=LSHzS z9%nwVDC&UWGA^-bqDxHCC>~B+EcEP0S>I`K-S=Pk6<<@b5AY}&3@^wt-5HDcKMQ~N zkucKMvSf_~z+VmKTDa)26^}g8=GW#E+!d=}7Hrr+9;+l6n>zt}qs0<5>$WhaG{( zr9iJ-TNopRvGFlGnDP4qB&|uHe9`ItX6V{NuLJtEuJSXtdch(TGCPOJ1{Hz=d)wFNI z^V8W}qMCO|!?q}wS*+5dcJi#(uqwx}jN@!Cs{0)pCSw(7w?Ea1wBLl$Z5fN9Fy|`U z2`EPWkzd6%j!n5z!+R4z^)LVVZSACF=IY_`b3SBmleb3&u>176Gn{Ha{0Q%} zIcjiRM#b*#ygxn;Sbz>S`3k)-($Ef7s7aXEdIU8EbLU{n{=Ieo;U{)e1#~wSkd8}# zvL`#WmaIv*AKnSZIb~<RQ`>E?$Rnrl@9e|;i}EH zQ)GFtx)_#}%&*AxfXjDerWG!}5z+-p$It$=lJIzho_NmCL@`Os7;w=M882S!_RTHs z6(c##fTjz>&0^$Svgae~;DMkJ{WL`f^6tQT%t9ykgHXe5)YKy<&{2wE{+i4Qm zf}{bJ7j)i25KEz7+a`<6b>$HghK+hE-)y8M3Q}xxU*ul?ng2afs5Cz~a| z008^@`|gxFT`0noRP*6|}4GrAf+>nrv*Vor`b93Y4 z*_RLPA0n6&3UI^X28`)6>&uXJ_Bv-<_SE zZfb8t5C-7xW?wY|YTWl-qsjZf+ezX{24ypfFjUQVtyTR)4~{bc z2!bF8f*=TjAP9mW2!bF8f*|CJB*}ydo5=6uTAiS(+%|r)M^I7b0>2$4s3yb4Z@~nr zsiESxbsefn1o$Hn;98?s@c>pEzwL)mP1JSpi(J{+g!Q&Y>?{qaEJf@3UR8gGg>sQ% zHAdcHx7q9t-e_Vee9WndZu=MG>eKCq+QI)i>gp&TTn~-^gbdQip9L5r z7*91Tt2nCrOaD24fk@wfRpCpJf*PAy6BE6TyRHc-l#Pmr>W(0P4g1^r)?%@rBnoAJNk~?_Th5U8#$F>16tgekW>iGu5FcuE} z_ePLA-N7HTAzUNv#lhQ+Ju`X)TKH=Y`7_%1OQy)-SMg7RsR5U0&W?aRfhW*b_@ixI z4e(YU|HwJHR|ELOTFfJOhWhyT2pN~x?l#_Gr4fWk@A#js5j@A_glEUWMf=-$m!3{B zt4P=$5j@MV<>05L4h(sFypwt`fzi&xXy(EbS#p55-6f)%YZM+ z0uwAv$Wp*X$k72=pdGW)t}KrV+o2wwe${RBnz0-9y%&eAV0N($R&uNiIUnwHG%@^sU0*N z>lEDyZ@Z~I)P*9A)@dE2J3W?wZDr2WddTshNQRx#L1qn6)#2k!=y#>~`=Wjy!-;4Qlw1 ztpz0;j`0C27fRH^$0nY##Jjw3 zF#^7LeN4g0)^-}>1MjeJ053?sYY21nr21VwbO3%BHvXB)!7L)YSe>E#<>+)irYe6+ zDT|J&%HLATGV0)8>A({=z%a+2y-sei&|{k zy8>I=X_Q*^*3~afKC18-eHu^28#Q^@n2Dl8aAW;*jusWeNblhdf(@tj1JG#ZV3xMW zZT+9D`3h6nK3{X72paB=JqKes>dU;r9#XK%es2F(+^SGaRMKarx$Ms6ruo-0t5nG;E3FR%- zl~*2N>r&S&k2|K-_#`5aJnHI~`}y2|;GWMp=lkRN?aaVYHmH{)E~7YxM)gNE_D;3>s8rFSNGSQr23E$-&M$0fG|2!cV=lxXwL~!`6QnipfZ5ql(>g+Tg z4k5L4EmkY$Mi8)T2|5(aqF*AdU9dSGArsI;x8o@CSIU|!hVd+Fzg5&!%x*B)l4C%u zh#-7{)dALn4B$`)$7U0HN0#e|^pz^yf(;#U6uYIJA|g@t zW=n!>REbtT(a!XgBC{NH&+(bQnvQ06)wgd&&NVpoo`{tiS%_lFOobkbdS-)Gp*5--jGm#XX3(aAzqn@|8Q|Ya>yrne0Bzz4=8shw$_b z3}ZYtd~QWr7gC-ild&tQ(D0{g%{(vf#BPWD6ajo2^-T^G8* z`CfQ7#se;ezl_!CDhg2=-yl#6>I|gsvlpyB6q0^xAhJba81vJQZqD`&)#+>2Mbi`t zsuB4EL{?5RXxx3?%D2lxtj+-d7S_NmC}nd+<&Jw6 zA^9|f&Gv3t*@O1vObL^33F7NpDgK$CowsDxkFLysRARfmltN~Jf>7s{=Mh6R*tt*4 z3x@Zj$DRSTRG5j0j;({F?e$T9J_EjS`PVyk;A*B~hA76sF#uU~REvu&SLT_eJI-ZB z`eelUm1zemg<=ly^3ZYauJ=mZeheQN{HVHnj53i}H6}XPCPR4%Jej*ngl+p*#fk$J zE}5-r*bv@VstqJ1vsRCJO%VGC3T!HXLvbhkt@&(@ZYHEvCAb+sv>52P$utsYfL+=p z#vf5dB%yMP@+n}bFCAbl8Sf(|htz$e0g$r04FOVYg(y1Uu&w&&ydm1w)gs9>}YhNHRZWg6raiBJ+4}PrxqGUsj_7};r53m2?O?&EcR}@S|cLzRO9q^#|UrZ8-6vXwAnp$a~9WJWPSot qU2^DKjYST552^IhSo%NSa=!}P!zUyR&axMO9KMr+&@7khus&7 z>*(j_)+QI^q<>B=90AgR;^N~Rai$+{qAPTyFnOs;gQ{JRwCi+p_f0YYitTBex98{QK7X%0 ze5gHrsTFLV=j7vKnbDxZ)9LByCUc}Lc&bW+s@&Y%>FMd^<>gzCvQLMs<>~GwbEf9z z=1hpMEQ;py^Yb=%rA~;i=jiC^=H(b{peu>!6l1{dQ&2T?$KvAR;^X5%fUiV?txSZg z+#diNaHTSQs~&EjJAJApbfsyP&fkZK04k35T2~EY!6$dC9BrOJfUF8?p=On~r#6o0;^N{ibfn~FWEyLoQ;e_d>*_gusQ?$jEpW*A`uhI< z{_ybd?eFhUgUSPF$@oY`3T4FvWt%y4$Ms)b^kQE@e8=!)Vh3lO>{wLqX=gBbr|R$T z>1%59^YQjwTKoI^L3qgZTUXmhM)g})Gjzw_Qc^T_qe_Iy?R$IUb8;4#)81`uF^}ys zucMZagaEFi=_Mx>ai|KFi2zW%?>RTCbg2LT|D%en?`>}Q{QTzR~Ht&3c#5?5C#W!oZ?vTIomt0B4;#i`-3+*xXoI?M?vNUt$21=>TrU(=9FY z^YigjP~b8$@9*y|k@4*8>>`u*197Vabg&t7s`^@1foG=wT2}h{`tOV&~#ExQvd=4{sIRC{&4X#{0aSH1Nmk0 zH~!o|Iw=zn1^)iN9v1oA&bMU^4F3N2+r?i}PyYQJ?zgX}KQb^H{^ihb`SR&DEg9}i zAoAtFdt5F3`1bbQ)61HWRQ&L>gn?&H9}oKZ*`SGUNGBuy{{H^>_Qa=BQAj{P9`NJ0 zkAipp{rvpm-m;{ll!JVFX`&&x)aqyPX3V@X6oRA}Dp*hy>> zQ5XmCZ*V%O!-!hkaKRx8o~+Bo+Jl;K@Bk6ons|cB#kJ88jkpvWYmG*6H`TgEZL6tk z+;@$M#vQQI15Rn`g$zhjo4TERuR~?9jp^kvCh}u4^XARvpYOdl^8)1L$B!Ss1u9S&1K5o#wI@%WJbhaE zKn%D%19u(1ai!tJi4#9guTgpuF|eyDB19q%M{M)v%|9QGQ8*9-1BDpR^IR;(aa=-o zZ#(Kz5Cex7hxkw^#37-Cx;&8Z01e7vV4L6wr6dA@K>3$LN|uO*qg8P}od?nFcPUsR z3XblJap^qRpd$(fqNPBbERSxG3$&md&4DvT{ zUcs{!$vI4}p{}c|>w8^8g}k$-##uN9Kn3LG!H{FkX`Np$PhXB*ZthI?wu_?ZEnYYp zDDp+hgBQ)ioqBrT$MPiZYyO}hav170K>|#w822Bfw zQez4F8I2`ZOO0|RSShl4^`z1XSKI9E)sP3IX~9<1(i9A`K{gr=VohwYM>L8K1Yg-p zXT*c%Y*c90o&o*PGT_@8v3LggrLTqj^--2(n^-oO0d*m)1T~_#LKCSh1hUNx7ZruW zMW5%$eV&5)F#Hgo?t1OXYuWguQKq$4t99r35qnxPhG_kUjXV8*Yd8N+>)NM2dpr48 zqijm(^ZlF7U$3vs!&Tg;tE+amLC@>eM)Yh&mD_hsCc1D@1Y|O;jzm1++ zEw;I{P#_f)K3On7Wt9ZAyj%7nTaaRjH<@&!w-8OZJ~VBnUv@5N&2+4FMMp6w<_@r~=nC$2c?1hRQ36lZY3z;kt<{_wF6k19rL2=C8OoPw>p zBSL)I9~Lbd--kw^RX`FAN4G_U5YIb!F2fhKTGxyE)FBN6Br$NYO5hzaL5O4s4ei?? z4-gGRgR4q#aKeEdo`MXANA=AGL`nZn#TGYG&`+e-I}|MDxtK?@bm>wXHY+7xf}1w< z%z&ZGScV3YqyoinZZghGnVB+If}2vmbej)aAc=+}`$F*p*Q3EO+UB62B#iFRfl1fk z+>+IkCZWx%jc_0aezj|UFG@sF96xtz`)SNWG-v~5Olmr#529l~)Ca{PEg{NebEU%l zfe%UBZbB495cHFxqM)~e9xCdkhyH*l=&>IV<3z=PJJYfSOY{eH?o6gqkh?X#aLG3%_Zx7yj@?m+4m*45OKdd^AoIuvvp3_Ta z4&HgT;j_h?EgM>F!EJl9rDeZ)PxqBs_Vi8Y8JiU?WKN@^nS-bAF6oJj4%<&fsOaOa6(2u+dEEVM$^&4aRKPveB%X(wKttr%&)ca9)kNjIt z8pzL@e#s(>P8UvYiy-iCVcPHAkk(sqIQ2JF1xr;Ry~uG*o-du}eBD~YLuqWS5qrNT z|NQPJSs0lf1LpVJ$|uEUqoMhw&L}XD{93@)>W`?NzaR)AehwHQ2w>Yv3Ud|9X*zt- zrY;T-kH@Dx=pa7?Id5=4%#j$OX)1_N{z#C@wU!{=Nzrx^koA0SGxoM6T|j=FUzQ+i z67z?GFwo9e$^o%Dzinb0UC}j>2Gi+-SZn}_O~Zr1W2}YhF`ua_J;8@@9L=wKtO_|Z zLQsv>T>#{d1c5V}S2Q6PsCcJF1a!of9E{Lj=C@>@Pn=;zI(s<1GCx0W)v_ zJQ_yGE7a1w7;tm`C=hfe6^TVbo2Vc@a(l25F$FHMketqilmaBDsFQ#*pJ2mcvj_|=vI9ZC3}R=5&bw=Zi!skb(;-e3 z&rpW*7*b}=iWq#c`_yU>1u=B(!3q|2H69lO9103r&HyI0tVF+GJ43Tkj8GT_F)cy2 z1usL$0G9pOSo4%(V#L#P6hy%7Lp>-W<}NGo1)0hc$D z0Jy3tNLD9N9=PKHbIU|OXPOB$SHUG#&GNGSJxM*? literal 0 HcmV?d00001 diff --git "a/assets/idea\350\256\276\347\275\256.html-BThIkzaw.js" "b/assets/idea\350\256\276\347\275\256.html-BThIkzaw.js" new file mode 100644 index 00000000..dc1d414f --- /dev/null +++ "b/assets/idea\350\256\276\347\275\256.html-BThIkzaw.js" @@ -0,0 +1,17 @@ +import{_ as s,c as a,d as e,o as t}from"./app-ftEjETWs.js";const n={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[e(`

Jetbrains Idea设置

一、类注释

想要在生成类的时候,自动带上author和时间,效果如下:

/**
+ * @author zhihuaiwen
+ * @since 2024/5/12 00:22
+ */
+public class EFE {
+}

放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他:

image-20240512002500812
image-20240512002500812

二、方法注释

想让自动给方法生成注释,以及参数的默认注释:

  /**
+   * 获取首页商品列表
+   *
+   * @param type             类型 【1 精品推荐 2 热门榜单 3首发新品 4促销单品】
+   * @param pageParamRequest 分页参数
+   * @return List
+   */
+  @Override
+  public CommonPage<IndexProductResponse> findIndexProductList(Integer type, PageParamRequest pageParamRequest) {

去settings里面找到Live Templates,新建一个Template Group叫做user,然后再新建一个Live Template。

image-20240512001636936
image-20240512001636936

Abbreviation填*,Description随便填,然后再Template text填下:

*
+$VAR1$
+ * @return $returns$
+ */

编辑变量,Edit Variables:

image-20240512001711420.png
image-20240512001711420.png

VAR1:

groovyScript("def result=''; def params=\\"\${_1}\\".replaceAll('[\\\\\\\\[|\\\\\\\\]|\\\\\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+=' * @param ' + params[i] + ' ' + params[i] + ((i < params.size() - 1) ? '\\\\n' : '')}; return result", methodParameters())

returns:

methodReturnType()

三、关闭Annotate With Git Blame

Idea这个太烦了,总是不小心误点,想把右侧的两个都给去掉,网上查了有些比较旧,无法解决,下面特别声明下:

image-20240512001050312
image-20240512001050312

位置:settings直接搜Inlay Hints,把右侧的Usages、Code author给取消勾选即可

image-20240512001010797
image-20240512001010797

上面的设置放到百度网盘里了,可以下载后导入到Idea

链接: https://pan.baidu.com/s/1-3uhY1jssyMWyRbvBLQ2FQ?pwd=epw2 提取码: epw2 复制这段内容后打开百度网盘手机App,操作更方便哦

image-20240512115716124`,27)]))}const r=s(n,[["render",l],["__file","idea设置.html.vue"]]),k=JSON.parse('{"path":"/interesting/idea%E8%AE%BE%E7%BD%AE.html","title":"Jetbrains Idea设置","lang":"zh-CN","frontmatter":{"description":"Jetbrains Idea设置 一、类注释 想要在生成类的时候,自动带上author和时间,效果如下: 放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他: image-20240512002500812image-20240512002500812 二、方法注释 想让自动给方法生成注释,以及参数的默认...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Jetbrains Idea设置"}],["meta",{"property":"og:description","content":"Jetbrains Idea设置 一、类注释 想要在生成类的时候,自动带上author和时间,效果如下: 放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他: image-20240512002500812image-20240512002500812 二、方法注释 想让自动给方法生成注释,以及参数的默认..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20240512002500812.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-12T03:58:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-12T03:58:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Jetbrains Idea设置\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20240512002500812.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240512001636936.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240512001711420.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240512001050312.png\\",\\"https://github-images.wenzhihuai.com/images/image-20240512001010797.png\\"],\\"dateModified\\":\\"2024-05-12T03:58:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、类注释","slug":"一、类注释","link":"#一、类注释","children":[]},{"level":2,"title":"二、方法注释","slug":"二、方法注释","link":"#二、方法注释","children":[]},{"level":2,"title":"三、关闭Annotate With Git Blame","slug":"三、关闭annotate-with-git-blame","link":"#三、关闭annotate-with-git-blame","children":[]}],"git":{"createdTime":1715445447000,"updatedTime":1715486319000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":1.33,"words":399},"filePathRelative":"interesting/idea设置.md","localizedDate":"2024年5月11日","excerpt":"\\n

一、类注释

\\n

想要在生成类的时候,自动带上author和时间,效果如下:

","autoDesc":true}');export{r as comp,k as data}; diff --git a/assets/index-AN989yVn.js b/assets/index-AN989yVn.js new file mode 100644 index 00000000..a0b535fa --- /dev/null +++ b/assets/index-AN989yVn.js @@ -0,0 +1,61 @@ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var _p=function(r,e){return _p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,a){t.__proto__=a}||function(t,a){for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(t[n]=a[n])},_p(r,e)};function k(r,e){if(typeof e!="function"&&e!==null)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");_p(r,e);function t(){this.constructor=r}r.prototype=e===null?Object.create(e):(t.prototype=e.prototype,new t)}var oL=function(){function r(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1}return r}(),sL=function(){function r(){this.browser=new oL,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow=typeof window<"u"}return r}(),_t=new sL;typeof wx=="object"&&typeof wx.getSystemInfoSync=="function"?(_t.wxa=!0,_t.touchEventsSupported=!0):typeof document>"u"&&typeof self<"u"?_t.worker=!0:typeof navigator>"u"||navigator.userAgent.indexOf("Node.js")===0?(_t.node=!0,_t.svgSupported=!0):lL(navigator.userAgent,_t);function lL(r,e){var t=e.browser,a=r.match(/Firefox\/([\d.]+)/),n=r.match(/MSIE\s([\d.]+)/)||r.match(/Trident\/.+?rv:(([\d.]+))/),i=r.match(/Edge?\/([\d.]+)/),o=/micromessenger/i.test(r);a&&(t.firefox=!0,t.version=a[1]),n&&(t.ie=!0,t.version=n[1]),i&&(t.edge=!0,t.version=i[1],t.newEdge=+i[1].split(".")[0]>18),o&&(t.weChat=!0),e.svgSupported=typeof SVGRect<"u",e.touchEventsSupported="ontouchstart"in window&&!t.ie&&!t.edge,e.pointerEventsSupported="onpointerdown"in window&&(t.edge||t.ie&&+t.version>=11),e.domSupported=typeof document<"u";var s=document.documentElement.style;e.transform3dSupported=(t.ie&&"transition"in s||t.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||t.ie&&+t.version>=9}var sg=12,Sw="sans-serif",Ka=sg+"px "+Sw,uL=20,fL=100,hL="007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\WQb\\0FWLg\\bWb\\WQ\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\FFF5.5N";function vL(r){var e={};if(typeof JSON>"u")return e;for(var t=0;t=0)s=o*t.length;else for(var l=0;l>1)%2;s.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",a[l]+":0",n[u]+":0",a[1-l]+":auto",n[1-u]+":auto",""].join("!important;"),r.appendChild(o),t.push(o)}return t}function EL(r,e,t){for(var a=t?"invTrans":"trans",n=e[a],i=e.srcCoords,o=[],s=[],l=!0,u=0;u<4;u++){var f=r[u].getBoundingClientRect(),h=2*u,v=f.left,c=f.top;o.push(v,c),l=l&&i&&v===i[h]&&c===i[h+1],s.push(r[u].offsetLeft,r[u].offsetTop)}return l&&n?n:(e.srcCoords=o,e[a]=t?Tm(s,o):Tm(o,s))}function Pw(r){return r.nodeName.toUpperCase()==="CANVAS"}var kL=/([&<>"'])/g,OL={"&":"&","<":"<",">":">",'"':""","'":"'"};function Me(r){return r==null?"":(r+"").replace(kL,function(e,t){return OL[t]})}var NL=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Jh=[],BL=_t.browser.firefox&&+_t.browser.version.split(".")[0]<39;function Tp(r,e,t,a){return t=t||{},a?Cm(r,e,t):BL&&e.layerX!=null&&e.layerX!==e.offsetX?(t.zrX=e.layerX,t.zrY=e.layerY):e.offsetX!=null?(t.zrX=e.offsetX,t.zrY=e.offsetY):Cm(r,e,t),t}function Cm(r,e,t){if(_t.domSupported&&r.getBoundingClientRect){var a=e.clientX,n=e.clientY;if(Pw(r)){var i=r.getBoundingClientRect();t.zrX=a-i.left,t.zrY=n-i.top;return}else if(wp(Jh,r,a,n)){t.zrX=Jh[0],t.zrY=Jh[1];return}}t.zrX=t.zrY=0}function pg(r){return r||window.event}function Je(r,e,t){if(e=pg(e),e.zrX!=null)return e;var a=e.type,n=a&&a.indexOf("touch")>=0;if(n){var o=a!=="touchend"?e.targetTouches[0]:e.changedTouches[0];o&&Tp(r,o,e,t)}else{Tp(r,e,e,t);var i=VL(e);e.zrDelta=i?i/120:-(e.detail||0)/3}var s=e.button;return e.which==null&&s!==void 0&&NL.test(e.type)&&(e.which=s&1?1:s&2?3:s&4?2:0),e}function VL(r){var e=r.wheelDelta;if(e)return e;var t=r.deltaX,a=r.deltaY;if(t==null||a==null)return e;var n=Math.abs(a!==0?a:t),i=a>0?-1:a<0?1:t>0?-1:1;return 3*n*i}function Ap(r,e,t,a){r.addEventListener(e,t,a)}function zL(r,e,t,a){r.removeEventListener(e,t,a)}var pa=function(r){r.preventDefault(),r.stopPropagation(),r.cancelBubble=!0};function Dm(r){return r.which===2||r.which===3}var GL=function(){function r(){this._track=[]}return r.prototype.recognize=function(e,t,a){return this._doTrack(e,t,a),this._recognize(e)},r.prototype.clear=function(){return this._track.length=0,this},r.prototype._doTrack=function(e,t,a){var n=e.touches;if(n){for(var i={points:[],touches:[],target:t,event:e},o=0,s=n.length;o1&&a&&a.length>1){var i=Mm(a)/Mm(n);!isFinite(i)&&(i=1),e.pinchScale=i;var o=FL(a);return e.pinchX=o[0],e.pinchY=o[1],{type:"pinch",target:r[0].target,event:e}}}}};function Ge(){return[1,0,0,1,0,0]}function Sl(r){return r[0]=1,r[1]=0,r[2]=0,r[3]=1,r[4]=0,r[5]=0,r}function ph(r,e){return r[0]=e[0],r[1]=e[1],r[2]=e[2],r[3]=e[3],r[4]=e[4],r[5]=e[5],r}function Yr(r,e,t){var a=e[0]*t[0]+e[2]*t[1],n=e[1]*t[0]+e[3]*t[1],i=e[0]*t[2]+e[2]*t[3],o=e[1]*t[2]+e[3]*t[3],s=e[0]*t[4]+e[2]*t[5]+e[4],l=e[1]*t[4]+e[3]*t[5]+e[5];return r[0]=a,r[1]=n,r[2]=i,r[3]=o,r[4]=s,r[5]=l,r}function Dr(r,e,t){return r[0]=e[0],r[1]=e[1],r[2]=e[2],r[3]=e[3],r[4]=e[4]+t[0],r[5]=e[5]+t[1],r}function an(r,e,t,a){a===void 0&&(a=[0,0]);var n=e[0],i=e[2],o=e[4],s=e[1],l=e[3],u=e[5],f=Math.sin(t),h=Math.cos(t);return r[0]=n*h+s*f,r[1]=-n*f+s*h,r[2]=i*h+l*f,r[3]=-i*f+h*l,r[4]=h*(o-a[0])+f*(u-a[1])+a[0],r[5]=h*(u-a[1])-f*(o-a[0])+a[1],r}function dh(r,e,t){var a=t[0],n=t[1];return r[0]=e[0]*a,r[1]=e[1]*n,r[2]=e[2]*a,r[3]=e[3]*n,r[4]=e[4]*a,r[5]=e[5]*n,r}function vi(r,e){var t=e[0],a=e[2],n=e[4],i=e[1],o=e[3],s=e[5],l=t*o-i*a;return l?(l=1/l,r[0]=o*l,r[1]=-i*l,r[2]=-a*l,r[3]=t*l,r[4]=(a*s-o*n)*l,r[5]=(i*n-t*s)*l,r):null}function Rw(r){var e=Ge();return ph(e,r),e}const a7=Object.freeze(Object.defineProperty({__proto__:null,clone:Rw,copy:ph,create:Ge,identity:Sl,invert:vi,mul:Yr,rotate:an,scale:dh,translate:Dr},Symbol.toStringTag,{value:"Module"}));var ft=function(){function r(e,t){this.x=e||0,this.y=t||0}return r.prototype.copy=function(e){return this.x=e.x,this.y=e.y,this},r.prototype.clone=function(){return new r(this.x,this.y)},r.prototype.set=function(e,t){return this.x=e,this.y=t,this},r.prototype.equal=function(e){return e.x===this.x&&e.y===this.y},r.prototype.add=function(e){return this.x+=e.x,this.y+=e.y,this},r.prototype.scale=function(e){this.x*=e,this.y*=e},r.prototype.scaleAndAdd=function(e,t){this.x+=e.x*t,this.y+=e.y*t},r.prototype.sub=function(e){return this.x-=e.x,this.y-=e.y,this},r.prototype.dot=function(e){return this.x*e.x+this.y*e.y},r.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},r.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},r.prototype.normalize=function(){var e=this.len();return this.x/=e,this.y/=e,this},r.prototype.distance=function(e){var t=this.x-e.x,a=this.y-e.y;return Math.sqrt(t*t+a*a)},r.prototype.distanceSquare=function(e){var t=this.x-e.x,a=this.y-e.y;return t*t+a*a},r.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},r.prototype.transform=function(e){if(e){var t=this.x,a=this.y;return this.x=e[0]*t+e[2]*a+e[4],this.y=e[1]*t+e[3]*a+e[5],this}},r.prototype.toArray=function(e){return e[0]=this.x,e[1]=this.y,e},r.prototype.fromArray=function(e){this.x=e[0],this.y=e[1]},r.set=function(e,t,a){e.x=t,e.y=a},r.copy=function(e,t){e.x=t.x,e.y=t.y},r.len=function(e){return Math.sqrt(e.x*e.x+e.y*e.y)},r.lenSquare=function(e){return e.x*e.x+e.y*e.y},r.dot=function(e,t){return e.x*t.x+e.y*t.y},r.add=function(e,t,a){e.x=t.x+a.x,e.y=t.y+a.y},r.sub=function(e,t,a){e.x=t.x-a.x,e.y=t.y-a.y},r.scale=function(e,t,a){e.x=t.x*a,e.y=t.y*a},r.scaleAndAdd=function(e,t,a,n){e.x=t.x+a.x*n,e.y=t.y+a.y*n},r.lerp=function(e,t,a,n){var i=1-n;e.x=i*t.x+n*a.x,e.y=i*t.y+n*a.y},r}(),Wl=Math.min,Ul=Math.max,sn=new ft,ln=new ft,un=new ft,fn=new ft,Bo=new ft,Vo=new ft,ht=function(){function r(e,t,a,n){a<0&&(e=e+a,a=-a),n<0&&(t=t+n,n=-n),this.x=e,this.y=t,this.width=a,this.height=n}return r.prototype.union=function(e){var t=Wl(e.x,this.x),a=Wl(e.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=Ul(e.x+e.width,this.x+this.width)-t:this.width=e.width,isFinite(this.y)&&isFinite(this.height)?this.height=Ul(e.y+e.height,this.y+this.height)-a:this.height=e.height,this.x=t,this.y=a},r.prototype.applyTransform=function(e){r.applyTransform(this,this,e)},r.prototype.calculateTransform=function(e){var t=this,a=e.width/t.width,n=e.height/t.height,i=Ge();return Dr(i,i,[-t.x,-t.y]),dh(i,i,[a,n]),Dr(i,i,[e.x,e.y]),i},r.prototype.intersect=function(e,t){if(!e)return!1;e instanceof r||(e=r.create(e));var a=this,n=a.x,i=a.x+a.width,o=a.y,s=a.y+a.height,l=e.x,u=e.x+e.width,f=e.y,h=e.y+e.height,v=!(ip&&(p=_,dp&&(p=S,y=a.x&&e<=a.x+a.width&&t>=a.y&&t<=a.y+a.height},r.prototype.clone=function(){return new r(this.x,this.y,this.width,this.height)},r.prototype.copy=function(e){r.copy(this,e)},r.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},r.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},r.prototype.isZero=function(){return this.width===0||this.height===0},r.create=function(e){return new r(e.x,e.y,e.width,e.height)},r.copy=function(e,t){e.x=t.x,e.y=t.y,e.width=t.width,e.height=t.height},r.applyTransform=function(e,t,a){if(!a){e!==t&&r.copy(e,t);return}if(a[1]<1e-5&&a[1]>-1e-5&&a[2]<1e-5&&a[2]>-1e-5){var n=a[0],i=a[3],o=a[4],s=a[5];e.x=t.x*n+o,e.y=t.y*i+s,e.width=t.width*n,e.height=t.height*i,e.width<0&&(e.x+=e.width,e.width=-e.width),e.height<0&&(e.y+=e.height,e.height=-e.height);return}sn.x=un.x=t.x,sn.y=fn.y=t.y,ln.x=fn.x=t.x+t.width,ln.y=un.y=t.y+t.height,sn.transform(a),fn.transform(a),ln.transform(a),un.transform(a),e.x=Wl(sn.x,ln.x,un.x,fn.x),e.y=Wl(sn.y,ln.y,un.y,fn.y);var l=Ul(sn.x,ln.x,un.x,fn.x),u=Ul(sn.y,ln.y,un.y,fn.y);e.width=l-e.x,e.height=u-e.y},r}(),Ew="silent";function HL(r,e,t){return{type:r,event:t,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:t.zrX,offsetY:t.zrY,gestureEvent:t.gestureEvent,pinchX:t.pinchX,pinchY:t.pinchY,pinchScale:t.pinchScale,wheelDelta:t.zrDelta,zrByTouch:t.zrByTouch,which:t.which,stop:WL}}function WL(){pa(this.event)}var UL=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.handler=null,t}return e.prototype.dispose=function(){},e.prototype.setCursor=function(){},e}(fr),zo=function(){function r(e,t){this.x=e,this.y=t}return r}(),YL=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],ev=new ht(0,0,0,0),kw=function(r){k(e,r);function e(t,a,n,i,o){var s=r.call(this)||this;return s._hovered=new zo(0,0),s.storage=t,s.painter=a,s.painterRoot=i,s._pointerSize=o,n=n||new UL,s.proxy=null,s.setHandlerProxy(n),s._draggingMgr=new IL(s),s}return e.prototype.setHandlerProxy=function(t){this.proxy&&this.proxy.dispose(),t&&(C(YL,function(a){t.on&&t.on(a,this[a],this)},this),t.handler=this),this.proxy=t},e.prototype.mousemove=function(t){var a=t.zrX,n=t.zrY,i=Ow(this,a,n),o=this._hovered,s=o.target;s&&!s.__zr&&(o=this.findHover(o.x,o.y),s=o.target);var l=this._hovered=i?new zo(a,n):this.findHover(a,n),u=l.target,f=this.proxy;f.setCursor&&f.setCursor(u?u.cursor:"default"),s&&u!==s&&this.dispatchToElement(o,"mouseout",t),this.dispatchToElement(l,"mousemove",t),u&&u!==s&&this.dispatchToElement(l,"mouseover",t)},e.prototype.mouseout=function(t){var a=t.zrEventControl;a!=="only_globalout"&&this.dispatchToElement(this._hovered,"mouseout",t),a!=="no_globalout"&&this.trigger("globalout",{type:"globalout",event:t})},e.prototype.resize=function(){this._hovered=new zo(0,0)},e.prototype.dispatch=function(t,a){var n=this[t];n&&n.call(this,a)},e.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},e.prototype.setCursorStyle=function(t){var a=this.proxy;a.setCursor&&a.setCursor(t)},e.prototype.dispatchToElement=function(t,a,n){t=t||{};var i=t.target;if(!(i&&i.silent)){for(var o="on"+a,s=HL(a,t,n);i&&(i[o]&&(s.cancelBubble=!!i[o].call(i,s)),i.trigger(a,s),i=i.__hostTarget?i.__hostTarget:i.parent,!s.cancelBubble););s.cancelBubble||(this.trigger(a,s),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer(function(l){typeof l[o]=="function"&&l[o].call(l,s),l.trigger&&l.trigger(a,s)}))}},e.prototype.findHover=function(t,a,n){var i=this.storage.getDisplayList(),o=new zo(t,a);if(Im(i,o,t,a,n),this._pointerSize&&!o.target){for(var s=[],l=this._pointerSize,u=l/2,f=new ht(t-u,a-u,l,l),h=i.length-1;h>=0;h--){var v=i[h];v!==n&&!v.ignore&&!v.ignoreCoarsePointer&&(!v.parent||!v.parent.ignoreCoarsePointer)&&(ev.copy(v.getBoundingRect()),v.transform&&ev.applyTransform(v.transform),ev.intersect(f)&&s.push(v))}if(s.length)for(var c=4,p=Math.PI/12,d=Math.PI*2,g=0;g4)return;this._downPoint=null}this.dispatchToElement(i,r,e)}});function XL(r,e,t){if(r[r.rectHover?"rectContain":"contain"](e,t)){for(var a=r,n=void 0,i=!1;a;){if(a.ignoreClip&&(i=!0),!i){var o=a.getClipPath();if(o&&!o.contain(e,t))return!1}a.silent&&(n=!0);var s=a.__hostTarget;a=s||a.parent}return n?Ew:!0}return!1}function Im(r,e,t,a,n){for(var i=r.length-1;i>=0;i--){var o=r[i],s=void 0;if(o!==n&&!o.ignore&&(s=XL(o,t,a))&&(!e.topTarget&&(e.topTarget=o),s!==Ew)){e.target=o;break}}}function Ow(r,e,t){var a=r.painter;return e<0||e>a.getWidth()||t<0||t>a.getHeight()}var Nw=32,Go=7;function $L(r){for(var e=0;r>=Nw;)e|=r&1,r>>=1;return r+e}function Lm(r,e,t,a){var n=e+1;if(n===t)return 1;if(a(r[n++],r[e])<0){for(;n=0;)n++;return n-e}function ZL(r,e,t){for(t--;e>>1,n(i,r[l])<0?s=l:o=l+1;var u=a-o;switch(u){case 3:r[o+3]=r[o+2];case 2:r[o+2]=r[o+1];case 1:r[o+1]=r[o];break;default:for(;u>0;)r[o+u]=r[o+u-1],u--}r[o]=i}}function rv(r,e,t,a,n,i){var o=0,s=0,l=1;if(i(r,e[t+n])>0){for(s=a-n;l0;)o=l,l=(l<<1)+1,l<=0&&(l=s);l>s&&(l=s),o+=n,l+=n}else{for(s=n+1;ls&&(l=s);var u=o;o=n-l,l=n-u}for(o++;o>>1);i(r,e[t+f])>0?o=f+1:l=f}return l}function av(r,e,t,a,n,i){var o=0,s=0,l=1;if(i(r,e[t+n])<0){for(s=n+1;ls&&(l=s);var u=o;o=n-l,l=n-u}else{for(s=a-n;l=0;)o=l,l=(l<<1)+1,l<=0&&(l=s);l>s&&(l=s),o+=n,l+=n}for(o++;o>>1);i(r,e[t+f])<0?l=f:o=f+1}return l}function qL(r,e){var t=Go,a,n,i=0,o=[];a=[],n=[];function s(c,p){a[i]=c,n[i]=p,i+=1}function l(){for(;i>1;){var c=i-2;if(c>=1&&n[c-1]<=n[c]+n[c+1]||c>=2&&n[c-2]<=n[c]+n[c-1])n[c-1]n[c+1])break;f(c)}}function u(){for(;i>1;){var c=i-2;c>0&&n[c-1]=Go||w>=Go);if(T)break;b<0&&(b=0),b+=2}if(t=b,t<1&&(t=1),p===1){for(y=0;y=0;y--)r[x+y]=r[b+y];r[S]=o[_];return}for(var w=t;;){var T=0,A=0,D=!1;do if(e(o[_],r[m])<0){if(r[S--]=r[m--],T++,A=0,--p===0){D=!0;break}}else if(r[S--]=o[_--],A++,T=0,--g===1){D=!0;break}while((T|A)=0;y--)r[x+y]=r[b+y];if(p===0){D=!0;break}}if(r[S--]=o[_--],--g===1){D=!0;break}if(A=g-rv(r[m],o,0,g,g-1,e),A!==0){for(S-=A,_-=A,g-=A,x=S+1,b=_+1,y=0;y=Go||A>=Go);if(D)break;w<0&&(w=0),w+=2}if(t=w,t<1&&(t=1),g===1){for(S-=p,m-=p,x=S+1,b=m+1,y=p-1;y>=0;y--)r[x+y]=r[b+y];r[S]=o[_]}else{if(g===0)throw new Error;for(b=S-(g-1),y=0;ys&&(l=s),Pm(r,t,t+l,t+i,e),i=l}o.pushRun(t,i),o.mergeRuns(),n-=i,t+=i}while(n!==0);o.forceMergeRuns()}}var He=1,vs=2,Fi=4,Rm=!1;function nv(){Rm||(Rm=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function Em(r,e){return r.zlevel===e.zlevel?r.z===e.z?r.z2-e.z2:r.z-e.z:r.zlevel-e.zlevel}var KL=function(){function r(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=Em}return r.prototype.traverse=function(e,t){for(var a=0;a0&&(f.__clipPaths=[]),isNaN(f.z)&&(nv(),f.z=0),isNaN(f.z2)&&(nv(),f.z2=0),isNaN(f.zlevel)&&(nv(),f.zlevel=0),this._displayList[this._displayListLen++]=f}var h=e.getDecalElement&&e.getDecalElement();h&&this._updateAndAddDisplayable(h,t,a);var v=e.getTextGuideLine();v&&this._updateAndAddDisplayable(v,t,a);var c=e.getTextContent();c&&this._updateAndAddDisplayable(c,t,a)}},r.prototype.addRoot=function(e){e.__zr&&e.__zr.storage===this||this._roots.push(e)},r.prototype.delRoot=function(e){if(e instanceof Array){for(var t=0,a=e.length;t=0&&this._roots.splice(n,1)},r.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},r.prototype.getRoots=function(){return this._roots},r.prototype.dispose=function(){this._displayList=null,this._roots=null},r}(),pf;pf=_t.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(r){return setTimeout(r,16)};var Ds={linear:function(r){return r},quadraticIn:function(r){return r*r},quadraticOut:function(r){return r*(2-r)},quadraticInOut:function(r){return(r*=2)<1?.5*r*r:-.5*(--r*(r-2)-1)},cubicIn:function(r){return r*r*r},cubicOut:function(r){return--r*r*r+1},cubicInOut:function(r){return(r*=2)<1?.5*r*r*r:.5*((r-=2)*r*r+2)},quarticIn:function(r){return r*r*r*r},quarticOut:function(r){return 1- --r*r*r*r},quarticInOut:function(r){return(r*=2)<1?.5*r*r*r*r:-.5*((r-=2)*r*r*r-2)},quinticIn:function(r){return r*r*r*r*r},quinticOut:function(r){return--r*r*r*r*r+1},quinticInOut:function(r){return(r*=2)<1?.5*r*r*r*r*r:.5*((r-=2)*r*r*r*r+2)},sinusoidalIn:function(r){return 1-Math.cos(r*Math.PI/2)},sinusoidalOut:function(r){return Math.sin(r*Math.PI/2)},sinusoidalInOut:function(r){return .5*(1-Math.cos(Math.PI*r))},exponentialIn:function(r){return r===0?0:Math.pow(1024,r-1)},exponentialOut:function(r){return r===1?1:1-Math.pow(2,-10*r)},exponentialInOut:function(r){return r===0?0:r===1?1:(r*=2)<1?.5*Math.pow(1024,r-1):.5*(-Math.pow(2,-10*(r-1))+2)},circularIn:function(r){return 1-Math.sqrt(1-r*r)},circularOut:function(r){return Math.sqrt(1- --r*r)},circularInOut:function(r){return(r*=2)<1?-.5*(Math.sqrt(1-r*r)-1):.5*(Math.sqrt(1-(r-=2)*r)+1)},elasticIn:function(r){var e,t=.1,a=.4;return r===0?0:r===1?1:(!t||t<1?(t=1,e=a/4):e=a*Math.asin(1/t)/(2*Math.PI),-(t*Math.pow(2,10*(r-=1))*Math.sin((r-e)*(2*Math.PI)/a)))},elasticOut:function(r){var e,t=.1,a=.4;return r===0?0:r===1?1:(!t||t<1?(t=1,e=a/4):e=a*Math.asin(1/t)/(2*Math.PI),t*Math.pow(2,-10*r)*Math.sin((r-e)*(2*Math.PI)/a)+1)},elasticInOut:function(r){var e,t=.1,a=.4;return r===0?0:r===1?1:(!t||t<1?(t=1,e=a/4):e=a*Math.asin(1/t)/(2*Math.PI),(r*=2)<1?-.5*(t*Math.pow(2,10*(r-=1))*Math.sin((r-e)*(2*Math.PI)/a)):t*Math.pow(2,-10*(r-=1))*Math.sin((r-e)*(2*Math.PI)/a)*.5+1)},backIn:function(r){var e=1.70158;return r*r*((e+1)*r-e)},backOut:function(r){var e=1.70158;return--r*r*((e+1)*r+e)+1},backInOut:function(r){var e=2.5949095;return(r*=2)<1?.5*(r*r*((e+1)*r-e)):.5*((r-=2)*r*((e+1)*r+e)+2)},bounceIn:function(r){return 1-Ds.bounceOut(1-r)},bounceOut:function(r){return r<1/2.75?7.5625*r*r:r<2/2.75?7.5625*(r-=1.5/2.75)*r+.75:r<2.5/2.75?7.5625*(r-=2.25/2.75)*r+.9375:7.5625*(r-=2.625/2.75)*r+.984375},bounceInOut:function(r){return r<.5?Ds.bounceIn(r*2)*.5:Ds.bounceOut(r*2-1)*.5+.5}},Yl=Math.pow,Ua=Math.sqrt,df=1e-8,Bw=1e-4,km=Ua(3),Xl=1/3,zr=rn(),ar=rn(),ji=rn();function Ba(r){return r>-df&&rdf||r<-df}function te(r,e,t,a,n){var i=1-n;return i*i*(i*r+3*n*e)+n*n*(n*a+3*i*t)}function Om(r,e,t,a,n){var i=1-n;return 3*(((e-r)*i+2*(t-e)*n)*i+(a-t)*n*n)}function gf(r,e,t,a,n,i){var o=a+3*(e-t)-r,s=3*(t-e*2+r),l=3*(e-r),u=r-n,f=s*s-3*o*l,h=s*l-9*o*u,v=l*l-3*s*u,c=0;if(Ba(f)&&Ba(h))if(Ba(s))i[0]=0;else{var p=-l/s;p>=0&&p<=1&&(i[c++]=p)}else{var d=h*h-4*f*v;if(Ba(d)){var g=h/f,p=-s/o+g,y=-g/2;p>=0&&p<=1&&(i[c++]=p),y>=0&&y<=1&&(i[c++]=y)}else if(d>0){var m=Ua(d),_=f*s+1.5*o*(-h+m),S=f*s+1.5*o*(-h-m);_<0?_=-Yl(-_,Xl):_=Yl(_,Xl),S<0?S=-Yl(-S,Xl):S=Yl(S,Xl);var p=(-s-(_+S))/(3*o);p>=0&&p<=1&&(i[c++]=p)}else{var b=(2*f*s-3*o*h)/(2*Ua(f*f*f)),x=Math.acos(b)/3,w=Ua(f),T=Math.cos(x),p=(-s-2*w*T)/(3*o),y=(-s+w*(T+km*Math.sin(x)))/(3*o),A=(-s+w*(T-km*Math.sin(x)))/(3*o);p>=0&&p<=1&&(i[c++]=p),y>=0&&y<=1&&(i[c++]=y),A>=0&&A<=1&&(i[c++]=A)}}return c}function zw(r,e,t,a,n){var i=6*t-12*e+6*r,o=9*e+3*a-3*r-9*t,s=3*e-3*r,l=0;if(Ba(o)){if(Vw(i)){var u=-s/i;u>=0&&u<=1&&(n[l++]=u)}}else{var f=i*i-4*o*s;if(Ba(f))n[0]=-i/(2*o);else if(f>0){var h=Ua(f),u=(-i+h)/(2*o),v=(-i-h)/(2*o);u>=0&&u<=1&&(n[l++]=u),v>=0&&v<=1&&(n[l++]=v)}}return l}function ja(r,e,t,a,n,i){var o=(e-r)*n+r,s=(t-e)*n+e,l=(a-t)*n+t,u=(s-o)*n+o,f=(l-s)*n+s,h=(f-u)*n+u;i[0]=r,i[1]=o,i[2]=u,i[3]=h,i[4]=h,i[5]=f,i[6]=l,i[7]=a}function Gw(r,e,t,a,n,i,o,s,l,u,f){var h,v=.005,c=1/0,p,d,g,y;zr[0]=l,zr[1]=u;for(var m=0;m<1;m+=.05)ar[0]=te(r,t,n,o,m),ar[1]=te(e,a,i,s,m),g=Wa(zr,ar),g=0&&g=0&&u<=1&&(n[l++]=u)}}else{var f=o*o-4*i*s;if(Ba(f)){var u=-o/(2*i);u>=0&&u<=1&&(n[l++]=u)}else if(f>0){var h=Ua(f),u=(-o+h)/(2*i),v=(-o-h)/(2*i);u>=0&&u<=1&&(n[l++]=u),v>=0&&v<=1&&(n[l++]=v)}}return l}function Fw(r,e,t){var a=r+t-2*e;return a===0?.5:(r-e)/a}function Us(r,e,t,a,n){var i=(e-r)*a+r,o=(t-e)*a+e,s=(o-i)*a+i;n[0]=r,n[1]=i,n[2]=s,n[3]=s,n[4]=o,n[5]=t}function Hw(r,e,t,a,n,i,o,s,l){var u,f=.005,h=1/0;zr[0]=o,zr[1]=s;for(var v=0;v<1;v+=.05){ar[0]=se(r,t,n,v),ar[1]=se(e,a,i,v);var c=Wa(zr,ar);c=0&&c=1?1:gf(0,a,i,1,l,s)&&te(0,n,o,1,s[0])}}}var e2=function(){function r(e){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=e.life||1e3,this._delay=e.delay||0,this.loop=e.loop||!1,this.onframe=e.onframe||Yt,this.ondestroy=e.ondestroy||Yt,this.onrestart=e.onrestart||Yt,e.easing&&this.setEasing(e.easing)}return r.prototype.step=function(e,t){if(this._inited||(this._startTime=e+this._delay,this._inited=!0),this._paused){this._pausedTime+=t;return}var a=this._life,n=e-this._startTime-this._pausedTime,i=n/a;i<0&&(i=0),i=Math.min(i,1);var o=this.easingFunc,s=o?o(i):i;if(this.onframe(s),i===1)if(this.loop){var l=n%a;this._startTime=e-l,this._pausedTime=0,this.onrestart()}else return!0;return!1},r.prototype.pause=function(){this._paused=!0},r.prototype.resume=function(){this._paused=!1},r.prototype.setEasing=function(e){this.easing=e,this.easingFunc=K(e)?e:Ds[e]||dg(e)},r}(),Ww=function(){function r(e){this.value=e}return r}(),r2=function(){function r(){this._len=0}return r.prototype.insert=function(e){var t=new Ww(e);return this.insertEntry(t),t},r.prototype.insertEntry=function(e){this.head?(this.tail.next=e,e.prev=this.tail,e.next=null,this.tail=e):this.head=this.tail=e,this._len++},r.prototype.remove=function(e){var t=e.prev,a=e.next;t?t.next=a:this.head=a,a?a.prev=t:this.tail=t,e.next=e.prev=null,this._len--},r.prototype.len=function(){return this._len},r.prototype.clear=function(){this.head=this.tail=null,this._len=0},r}(),xl=function(){function r(e){this._list=new r2,this._maxSize=10,this._map={},this._maxSize=e}return r.prototype.put=function(e,t){var a=this._list,n=this._map,i=null;if(n[e]==null){var o=a.len(),s=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var l=a.head;a.remove(l),delete n[l.key],i=l.value,this._lastRemovedEntry=l}s?s.value=t:s=new Ww(t),s.key=e,a.insertEntry(s),n[e]=s}return i},r.prototype.get=function(e){var t=this._map[e],a=this._list;if(t!=null)return t!==a.tail&&(a.remove(t),a.insertEntry(t)),t.value},r.prototype.clear=function(){this._list.clear(),this._map={}},r.prototype.len=function(){return this._list.len()},r}(),Nm={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function wr(r){return r=Math.round(r),r<0?0:r>255?255:r}function a2(r){return r=Math.round(r),r<0?0:r>360?360:r}function Ys(r){return r<0?0:r>1?1:r}function iv(r){var e=r;return e.length&&e.charAt(e.length-1)==="%"?wr(parseFloat(e)/100*255):wr(parseInt(e,10))}function qn(r){var e=r;return e.length&&e.charAt(e.length-1)==="%"?Ys(parseFloat(e)/100):Ys(parseFloat(e))}function ov(r,e,t){return t<0?t+=1:t>1&&(t-=1),t*6<1?r+(e-r)*t*6:t*2<1?e:t*3<2?r+(e-r)*(2/3-t)*6:r}function Va(r,e,t){return r+(e-r)*t}function Qe(r,e,t,a,n){return r[0]=e,r[1]=t,r[2]=a,r[3]=n,r}function Dp(r,e){return r[0]=e[0],r[1]=e[1],r[2]=e[2],r[3]=e[3],r}var Uw=new xl(20),$l=null;function xi(r,e){$l&&Dp($l,e),$l=Uw.put(r,$l||e.slice())}function Ie(r,e){if(r){e=e||[];var t=Uw.get(r);if(t)return Dp(e,t);r=r+"";var a=r.replace(/ /g,"").toLowerCase();if(a in Nm)return Dp(e,Nm[a]),xi(r,e),e;var n=a.length;if(a.charAt(0)==="#"){if(n===4||n===5){var i=parseInt(a.slice(1,4),16);if(!(i>=0&&i<=4095)){Qe(e,0,0,0,1);return}return Qe(e,(i&3840)>>4|(i&3840)>>8,i&240|(i&240)>>4,i&15|(i&15)<<4,n===5?parseInt(a.slice(4),16)/15:1),xi(r,e),e}else if(n===7||n===9){var i=parseInt(a.slice(1,7),16);if(!(i>=0&&i<=16777215)){Qe(e,0,0,0,1);return}return Qe(e,(i&16711680)>>16,(i&65280)>>8,i&255,n===9?parseInt(a.slice(7),16)/255:1),xi(r,e),e}return}var o=a.indexOf("("),s=a.indexOf(")");if(o!==-1&&s+1===n){var l=a.substr(0,o),u=a.substr(o+1,s-(o+1)).split(","),f=1;switch(l){case"rgba":if(u.length!==4)return u.length===3?Qe(e,+u[0],+u[1],+u[2],1):Qe(e,0,0,0,1);f=qn(u.pop());case"rgb":if(u.length>=3)return Qe(e,iv(u[0]),iv(u[1]),iv(u[2]),u.length===3?f:qn(u[3])),xi(r,e),e;Qe(e,0,0,0,1);return;case"hsla":if(u.length!==4){Qe(e,0,0,0,1);return}return u[3]=qn(u[3]),Mp(u,e),xi(r,e),e;case"hsl":if(u.length!==3){Qe(e,0,0,0,1);return}return Mp(u,e),xi(r,e),e;default:return}}Qe(e,0,0,0,1)}}function Mp(r,e){var t=(parseFloat(r[0])%360+360)%360/360,a=qn(r[1]),n=qn(r[2]),i=n<=.5?n*(a+1):n+a-n*a,o=n*2-i;return e=e||[],Qe(e,wr(ov(o,i,t+1/3)*255),wr(ov(o,i,t)*255),wr(ov(o,i,t-1/3)*255),1),r.length===4&&(e[3]=r[3]),e}function n2(r){if(r){var e=r[0]/255,t=r[1]/255,a=r[2]/255,n=Math.min(e,t,a),i=Math.max(e,t,a),o=i-n,s=(i+n)/2,l,u;if(o===0)l=0,u=0;else{s<.5?u=o/(i+n):u=o/(2-i-n);var f=((i-e)/6+o/2)/o,h=((i-t)/6+o/2)/o,v=((i-a)/6+o/2)/o;e===i?l=v-h:t===i?l=1/3+f-v:a===i&&(l=2/3+h-f),l<0&&(l+=1),l>1&&(l-=1)}var c=[l*360,u,s];return r[3]!=null&&c.push(r[3]),c}}function yf(r,e){var t=Ie(r);if(t){for(var a=0;a<3;a++)e<0?t[a]=t[a]*(1-e)|0:t[a]=(255-t[a])*e+t[a]|0,t[a]>255?t[a]=255:t[a]<0&&(t[a]=0);return Tr(t,t.length===4?"rgba":"rgb")}}function i2(r){var e=Ie(r);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)}function Ms(r,e,t){if(!(!(e&&e.length)||!(r>=0&&r<=1))){t=t||[];var a=r*(e.length-1),n=Math.floor(a),i=Math.ceil(a),o=e[n],s=e[i],l=a-n;return t[0]=wr(Va(o[0],s[0],l)),t[1]=wr(Va(o[1],s[1],l)),t[2]=wr(Va(o[2],s[2],l)),t[3]=Ys(Va(o[3],s[3],l)),t}}var o2=Ms;function gg(r,e,t){if(!(!(e&&e.length)||!(r>=0&&r<=1))){var a=r*(e.length-1),n=Math.floor(a),i=Math.ceil(a),o=Ie(e[n]),s=Ie(e[i]),l=a-n,u=Tr([wr(Va(o[0],s[0],l)),wr(Va(o[1],s[1],l)),wr(Va(o[2],s[2],l)),Ys(Va(o[3],s[3],l))],"rgba");return t?{color:u,leftIndex:n,rightIndex:i,value:a}:u}}var s2=gg;function Qi(r,e,t,a){var n=Ie(r);if(r)return n=n2(n),e!=null&&(n[0]=a2(e)),t!=null&&(n[1]=qn(t)),a!=null&&(n[2]=qn(a)),Tr(Mp(n),"rgba")}function Xs(r,e){var t=Ie(r);if(t&&e!=null)return t[3]=Ys(e),Tr(t,"rgba")}function Tr(r,e){if(!(!r||!r.length)){var t=r[0]+","+r[1]+","+r[2];return(e==="rgba"||e==="hsva"||e==="hsla")&&(t+=","+r[3]),e+"("+t+")"}}function $s(r,e){var t=Ie(r);return t?(.299*t[0]+.587*t[1]+.114*t[2])*t[3]/255+(1-t[3])*e:0}function l2(){return Tr([Math.round(Math.random()*255),Math.round(Math.random()*255),Math.round(Math.random()*255)],"rgb")}var Bm=new xl(100);function mf(r){if(U(r)){var e=Bm.get(r);return e||(e=yf(r,-.1),Bm.put(r,e)),e}else if(ml(r)){var t=V({},r);return t.colorStops=G(r.colorStops,function(a){return{offset:a.offset,color:yf(a.color,-.1)}}),t}return r}const n7=Object.freeze(Object.defineProperty({__proto__:null,fastLerp:Ms,fastMapToColor:o2,lerp:gg,lift:yf,liftColor:mf,lum:$s,mapToColor:s2,modifyAlpha:Xs,modifyHSL:Qi,parse:Ie,random:l2,stringify:Tr,toHex:i2},Symbol.toStringTag,{value:"Module"}));var _f=Math.round;function Zs(r){var e;if(!r||r==="transparent")r="none";else if(typeof r=="string"&&r.indexOf("rgba")>-1){var t=Ie(r);t&&(r="rgb("+t[0]+","+t[1]+","+t[2]+")",e=t[3])}return{color:r,opacity:e??1}}var Vm=1e-4;function za(r){return r-Vm}function Zl(r){return _f(r*1e3)/1e3}function Ip(r){return _f(r*1e4)/1e4}function u2(r){return"matrix("+Zl(r[0])+","+Zl(r[1])+","+Zl(r[2])+","+Zl(r[3])+","+Ip(r[4])+","+Ip(r[5])+")"}var f2={left:"start",right:"end",center:"middle",middle:"middle"};function h2(r,e,t){return t==="top"?r+=e/2:t==="bottom"&&(r-=e/2),r}function v2(r){return r&&(r.shadowBlur||r.shadowOffsetX||r.shadowOffsetY)}function c2(r){var e=r.style,t=r.getGlobalScale();return[e.shadowColor,(e.shadowBlur||0).toFixed(2),(e.shadowOffsetX||0).toFixed(2),(e.shadowOffsetY||0).toFixed(2),t[0],t[1]].join(",")}function Yw(r){return r&&!!r.image}function p2(r){return r&&!!r.svgElement}function yg(r){return Yw(r)||p2(r)}function Xw(r){return r.type==="linear"}function $w(r){return r.type==="radial"}function Zw(r){return r&&(r.type==="linear"||r.type==="radial")}function gh(r){return"url(#"+r+")"}function qw(r){var e=r.getGlobalScale(),t=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(t)/Math.log(10)),1)}function Kw(r){var e=r.x||0,t=r.y||0,a=(r.rotation||0)*Ts,n=ot(r.scaleX,1),i=ot(r.scaleY,1),o=r.skewX||0,s=r.skewY||0,l=[];return(e||t)&&l.push("translate("+e+"px,"+t+"px)"),a&&l.push("rotate("+a+")"),(n!==1||i!==1)&&l.push("scale("+n+","+i+")"),(o||s)&&l.push("skew("+_f(o*Ts)+"deg, "+_f(s*Ts)+"deg)"),l.join(" ")}var d2=function(){return _t.hasGlobalWindow&&K(window.btoa)?function(r){return window.btoa(unescape(encodeURIComponent(r)))}:typeof Buffer<"u"?function(r){return Buffer.from(r).toString("base64")}:function(r){return null}}(),Lp=Array.prototype.slice;function ia(r,e,t){return(e-r)*t+r}function sv(r,e,t,a){for(var n=e.length,i=0;ia?e:r,i=Math.min(t,a),o=n[i-1]||{color:[0,0,0,0],offset:0},s=i;so;if(s)a.length=o;else for(var l=i;l=1},r.prototype.getAdditiveTrack=function(){return this._additiveTrack},r.prototype.addKeyframe=function(e,t,a){this._needsSort=!0;var n=this.keyframes,i=n.length,o=!1,s=Gm,l=t;if(fe(t)){var u=_2(t);s=u,(u===1&&!Ct(t[0])||u===2&&!Ct(t[0][0]))&&(o=!0)}else if(Ct(t)&&!no(t))s=Kl;else if(U(t))if(!isNaN(+t))s=Kl;else{var f=Ie(t);f&&(l=f,s=cs)}else if(ml(t)){var h=V({},l);h.colorStops=G(t.colorStops,function(c){return{offset:c.offset,color:Ie(c.color)}}),Xw(t)?s=Pp:$w(t)&&(s=Rp),l=h}i===0?this.valType=s:(s!==this.valType||s===Gm)&&(o=!0),this.discrete=this.discrete||o;var v={time:e,value:l,rawValue:t,percent:0};return a&&(v.easing=a,v.easingFunc=K(a)?a:Ds[a]||dg(a)),n.push(v),v},r.prototype.prepare=function(e,t){var a=this.keyframes;this._needsSort&&a.sort(function(d,g){return d.time-g.time});for(var n=this.valType,i=a.length,o=a[i-1],s=this.discrete,l=jl(n),u=Fm(n),f=0;f=0&&!(o[f].percent<=t);f--);f=v(f,s-2)}else{for(f=h;ft);f++);f=v(f-1,s-2)}p=o[f+1],c=o[f]}if(c&&p){this._lastFr=f,this._lastFrP=t;var g=p.percent-c.percent,y=g===0?1:v((t-c.percent)/g,1);p.easingFunc&&(y=p.easingFunc(y));var m=a?this._additiveValue:u?Fo:e[l];if((jl(i)||u)&&!m&&(m=this._additiveValue=[]),this.discrete)e[l]=y<1?c.rawValue:p.rawValue;else if(jl(i))i===$u?sv(m,c[n],p[n],y):g2(m,c[n],p[n],y);else if(Fm(i)){var _=c[n],S=p[n],b=i===Pp;e[l]={type:b?"linear":"radial",x:ia(_.x,S.x,y),y:ia(_.y,S.y,y),colorStops:G(_.colorStops,function(w,T){var A=S.colorStops[T];return{offset:ia(w.offset,A.offset,y),color:Xu(sv([],w.color,A.color,y))}}),global:S.global},b?(e[l].x2=ia(_.x2,S.x2,y),e[l].y2=ia(_.y2,S.y2,y)):e[l].r=ia(_.r,S.r,y)}else if(u)sv(m,c[n],p[n],y),a||(e[l]=Xu(m));else{var x=ia(c[n],p[n],y);a?this._additiveValue=x:e[l]=x}a&&this._addToTarget(e)}}},r.prototype._addToTarget=function(e){var t=this.valType,a=this.propName,n=this._additiveValue;t===Kl?e[a]=e[a]+n:t===cs?(Ie(e[a],Fo),ql(Fo,Fo,n,1),e[a]=Xu(Fo)):t===$u?ql(e[a],e[a],n,1):t===jw&&zm(e[a],e[a],n,1)},r}(),mg=function(){function r(e,t,a,n){if(this._tracks={},this._trackKeys=[],this._maxTime=0,this._started=0,this._clip=null,this._target=e,this._loop=t,t&&n){fh("Can' use additive animation on looped animation.");return}this._additiveAnimators=n,this._allowDiscrete=a}return r.prototype.getMaxTime=function(){return this._maxTime},r.prototype.getDelay=function(){return this._delay},r.prototype.getLoop=function(){return this._loop},r.prototype.getTarget=function(){return this._target},r.prototype.changeTarget=function(e){this._target=e},r.prototype.when=function(e,t,a){return this.whenWithKeys(e,t,St(t),a)},r.prototype.whenWithKeys=function(e,t,a,n){for(var i=this._tracks,o=0;o0&&l.addKeyframe(0,Is(u),n),this._trackKeys.push(s)}l.addKeyframe(e,Is(t[s]),n)}return this._maxTime=Math.max(this._maxTime,e),this},r.prototype.pause=function(){this._clip.pause(),this._paused=!0},r.prototype.resume=function(){this._clip.resume(),this._paused=!1},r.prototype.isPaused=function(){return!!this._paused},r.prototype.duration=function(e){return this._maxTime=e,this._force=!0,this},r.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var e=this._doneCbs;if(e)for(var t=e.length,a=0;a0)){this._started=1;for(var t=this,a=[],n=this._maxTime||0,i=0;i1){var s=o.pop();i.addKeyframe(s.time,e[n]),i.prepare(this._maxTime,i.getAdditiveTrack())}}}},r}();function Xi(){return new Date().getTime()}var x2=function(r){k(e,r);function e(t){var a=r.call(this)||this;return a._running=!1,a._time=0,a._pausedTime=0,a._pauseStart=0,a._paused=!1,t=t||{},a.stage=t.stage||{},a}return e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var a=t.getClip();a&&this.addClip(a)},e.prototype.removeClip=function(t){if(t.animation){var a=t.prev,n=t.next;a?a.next=n:this._head=n,n?n.prev=a:this._tail=a,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var a=t.getClip();a&&this.removeClip(a),t.animation=null},e.prototype.update=function(t){for(var a=Xi()-this._pausedTime,n=a-this._time,i=this._head;i;){var o=i.next,s=i.step(a,n);s&&(i.ondestroy(),this.removeClip(i)),i=o}this._time=a,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0;function a(){t._running&&(pf(a),!t._paused&&t.update())}pf(a)},e.prototype.start=function(){this._running||(this._time=Xi(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=Xi(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=Xi()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var a=t.next;t.prev=t.next=t.animation=null,t=a}this._head=this._tail=null},e.prototype.isFinished=function(){return this._head==null},e.prototype.animate=function(t,a){a=a||{},this.start();var n=new mg(t,a.loop);return this.addAnimator(n),n},e}(fr),b2=300,lv=_t.domSupported,uv=function(){var r=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],e=["touchstart","touchend","touchmove"],t={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},a=G(r,function(n){var i=n.replace("mouse","pointer");return t.hasOwnProperty(i)?i:n});return{mouse:r,touch:e,pointer:a}}(),Hm={mouse:["mousemove","mouseup"],pointer:["pointermove","pointerup"]},Wm=!1;function Ep(r){var e=r.pointerType;return e==="pen"||e==="touch"}function w2(r){r.touching=!0,r.touchTimer!=null&&(clearTimeout(r.touchTimer),r.touchTimer=null),r.touchTimer=setTimeout(function(){r.touching=!1,r.touchTimer=null},700)}function fv(r){r&&(r.zrByTouch=!0)}function T2(r,e){return Je(r.dom,new A2(r,e),!0)}function Qw(r,e){for(var t=e,a=!1;t&&t.nodeType!==9&&!(a=t.domBelongToZr||t!==e&&t===r.painterRoot);)t=t.parentNode;return a}var A2=function(){function r(e,t){this.stopPropagation=Yt,this.stopImmediatePropagation=Yt,this.preventDefault=Yt,this.type=t.type,this.target=this.currentTarget=e.dom,this.pointerType=t.pointerType,this.clientX=t.clientX,this.clientY=t.clientY}return r}(),yr={mousedown:function(r){r=Je(this.dom,r),this.__mayPointerCapture=[r.zrX,r.zrY],this.trigger("mousedown",r)},mousemove:function(r){r=Je(this.dom,r);var e=this.__mayPointerCapture;e&&(r.zrX!==e[0]||r.zrY!==e[1])&&this.__togglePointerCapture(!0),this.trigger("mousemove",r)},mouseup:function(r){r=Je(this.dom,r),this.__togglePointerCapture(!1),this.trigger("mouseup",r)},mouseout:function(r){r=Je(this.dom,r);var e=r.toElement||r.relatedTarget;Qw(this,e)||(this.__pointerCapturing&&(r.zrEventControl="no_globalout"),this.trigger("mouseout",r))},wheel:function(r){Wm=!0,r=Je(this.dom,r),this.trigger("mousewheel",r)},mousewheel:function(r){Wm||(r=Je(this.dom,r),this.trigger("mousewheel",r))},touchstart:function(r){r=Je(this.dom,r),fv(r),this.__lastTouchMoment=new Date,this.handler.processGesture(r,"start"),yr.mousemove.call(this,r),yr.mousedown.call(this,r)},touchmove:function(r){r=Je(this.dom,r),fv(r),this.handler.processGesture(r,"change"),yr.mousemove.call(this,r)},touchend:function(r){r=Je(this.dom,r),fv(r),this.handler.processGesture(r,"end"),yr.mouseup.call(this,r),+new Date-+this.__lastTouchMomentXm||r<-Xm}var vn=[],bi=[],vv=Ge(),cv=Math.abs,fa=function(){function r(){}return r.prototype.getLocalTransform=function(e){return r.getLocalTransform(this,e)},r.prototype.setPosition=function(e){this.x=e[0],this.y=e[1]},r.prototype.setScale=function(e){this.scaleX=e[0],this.scaleY=e[1]},r.prototype.setSkew=function(e){this.skewX=e[0],this.skewY=e[1]},r.prototype.setOrigin=function(e){this.originX=e[0],this.originY=e[1]},r.prototype.needLocalTransform=function(){return hn(this.rotation)||hn(this.x)||hn(this.y)||hn(this.scaleX-1)||hn(this.scaleY-1)||hn(this.skewX)||hn(this.skewY)},r.prototype.updateTransform=function(){var e=this.parent&&this.parent.transform,t=this.needLocalTransform(),a=this.transform;if(!(t||e)){a&&(Ym(a),this.invTransform=null);return}a=a||Ge(),t?this.getLocalTransform(a):Ym(a),e&&(t?Yr(a,e,a):ph(a,e)),this.transform=a,this._resolveGlobalScaleRatio(a)},r.prototype._resolveGlobalScaleRatio=function(e){var t=this.globalScaleRatio;if(t!=null&&t!==1){this.getGlobalScale(vn);var a=vn[0]<0?-1:1,n=vn[1]<0?-1:1,i=((vn[0]-a)*t+a)/vn[0]||0,o=((vn[1]-n)*t+n)/vn[1]||0;e[0]*=i,e[1]*=i,e[2]*=o,e[3]*=o}this.invTransform=this.invTransform||Ge(),vi(this.invTransform,e)},r.prototype.getComputedTransform=function(){for(var e=this,t=[];e;)t.push(e),e=e.parent;for(;e=t.pop();)e.updateTransform();return this.transform},r.prototype.setLocalTransform=function(e){if(e){var t=e[0]*e[0]+e[1]*e[1],a=e[2]*e[2]+e[3]*e[3],n=Math.atan2(e[1],e[0]),i=Math.PI/2+n-Math.atan2(e[3],e[2]);a=Math.sqrt(a)*Math.cos(i),t=Math.sqrt(t),this.skewX=i,this.skewY=0,this.rotation=-n,this.x=+e[4],this.y=+e[5],this.scaleX=t,this.scaleY=a,this.originX=0,this.originY=0}},r.prototype.decomposeTransform=function(){if(this.transform){var e=this.parent,t=this.transform;e&&e.transform&&(e.invTransform=e.invTransform||Ge(),Yr(bi,e.invTransform,t),t=bi);var a=this.originX,n=this.originY;(a||n)&&(vv[4]=a,vv[5]=n,Yr(bi,t,vv),bi[4]-=a,bi[5]-=n,t=bi),this.setLocalTransform(t)}},r.prototype.getGlobalScale=function(e){var t=this.transform;return e=e||[],t?(e[0]=Math.sqrt(t[0]*t[0]+t[1]*t[1]),e[1]=Math.sqrt(t[2]*t[2]+t[3]*t[3]),t[0]<0&&(e[0]=-e[0]),t[3]<0&&(e[1]=-e[1]),e):(e[0]=1,e[1]=1,e)},r.prototype.transformCoordToLocal=function(e,t){var a=[e,t],n=this.invTransform;return n&&le(a,a,n),a},r.prototype.transformCoordToGlobal=function(e,t){var a=[e,t],n=this.transform;return n&&le(a,a,n),a},r.prototype.getLineScale=function(){var e=this.transform;return e&&cv(e[0]-1)>1e-10&&cv(e[3]-1)>1e-10?Math.sqrt(cv(e[0]*e[3]-e[2]*e[1])):1},r.prototype.copyTransform=function(e){tT(this,e)},r.getLocalTransform=function(e,t){t=t||[];var a=e.originX||0,n=e.originY||0,i=e.scaleX,o=e.scaleY,s=e.anchorX,l=e.anchorY,u=e.rotation||0,f=e.x,h=e.y,v=e.skewX?Math.tan(e.skewX):0,c=e.skewY?Math.tan(-e.skewY):0;if(a||n||s||l){var p=a+s,d=n+l;t[4]=-p*i-v*d*o,t[5]=-d*o-c*p*i}else t[4]=t[5]=0;return t[0]=i,t[3]=o,t[1]=c*i,t[2]=v*o,u&&an(t,t,u),t[4]+=a+f,t[5]+=n+h,t},r.initDefaultProps=function(){var e=r.prototype;e.scaleX=e.scaleY=e.globalScaleRatio=1,e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0}(),r}(),$r=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function tT(r,e){for(var t=0;t<$r.length;t++){var a=$r[t];r[a]=e[a]}}var $m={};function Xe(r,e){e=e||Ka;var t=$m[e];t||(t=$m[e]=new xl(500));var a=t.get(r);return a==null&&(a=Cr.measureText(r,e).width,t.put(r,a)),a}function Zm(r,e,t,a){var n=Xe(r,e),i=yh(e),o=ps(0,n,t),s=Hi(0,i,a),l=new ht(o,s,n,i);return l}function bl(r,e,t,a){var n=((r||"")+"").split(` +`),i=n.length;if(i===1)return Zm(n[0],e,t,a);for(var o=new ht(0,0,0,0),s=0;s=0?parseFloat(r)/100*e:parseFloat(r):r}function xf(r,e,t){var a=e.position||"inside",n=e.distance!=null?e.distance:5,i=t.height,o=t.width,s=i/2,l=t.x,u=t.y,f="left",h="top";if(a instanceof Array)l+=Mr(a[0],t.width),u+=Mr(a[1],t.height),f=null,h=null;else switch(a){case"left":l-=n,u+=s,f="right",h="middle";break;case"right":l+=n+o,u+=s,h="middle";break;case"top":l+=o/2,u-=n,f="center",h="bottom";break;case"bottom":l+=o/2,u+=i+n,f="center";break;case"inside":l+=o/2,u+=s,f="center",h="middle";break;case"insideLeft":l+=n,u+=s,h="middle";break;case"insideRight":l+=o-n,u+=s,f="right",h="middle";break;case"insideTop":l+=o/2,u+=n,f="center";break;case"insideBottom":l+=o/2,u+=i-n,f="center",h="bottom";break;case"insideTopLeft":l+=n,u+=n;break;case"insideTopRight":l+=o-n,u+=n,f="right";break;case"insideBottomLeft":l+=n,u+=i-n,h="bottom";break;case"insideBottomRight":l+=o-n,u+=i-n,f="right",h="bottom";break}return r=r||{},r.x=l,r.y=u,r.align=f,r.verticalAlign=h,r}var pv="__zr_normal__",dv=$r.concat(["ignore"]),L2=lr($r,function(r,e){return r[e]=!0,r},{ignore:!1}),wi={},P2=new ht(0,0,0,0),mh=function(){function r(e){this.id=fg(),this.animators=[],this.currentStates=[],this.states={},this._init(e)}return r.prototype._init=function(e){this.attr(e)},r.prototype.drift=function(e,t,a){switch(this.draggable){case"horizontal":t=0;break;case"vertical":e=0;break}var n=this.transform;n||(n=this.transform=[1,0,0,1,0,0]),n[4]+=e,n[5]+=t,this.decomposeTransform(),this.markRedraw()},r.prototype.beforeUpdate=function(){},r.prototype.afterUpdate=function(){},r.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},r.prototype.updateInnerText=function(e){var t=this._textContent;if(t&&(!t.ignore||e)){this.textConfig||(this.textConfig={});var a=this.textConfig,n=a.local,i=t.innerTransformable,o=void 0,s=void 0,l=!1;i.parent=n?this:null;var u=!1;if(i.copyTransform(t),a.position!=null){var f=P2;a.layoutRect?f.copy(a.layoutRect):f.copy(this.getBoundingRect()),n||f.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(wi,a,f):xf(wi,a,f),i.x=wi.x,i.y=wi.y,o=wi.align,s=wi.verticalAlign;var h=a.origin;if(h&&a.rotation!=null){var v=void 0,c=void 0;h==="center"?(v=f.width*.5,c=f.height*.5):(v=Mr(h[0],f.width),c=Mr(h[1],f.height)),u=!0,i.originX=-i.x+v+(n?0:f.x),i.originY=-i.y+c+(n?0:f.y)}}a.rotation!=null&&(i.rotation=a.rotation);var p=a.offset;p&&(i.x+=p[0],i.y+=p[1],u||(i.originX=-p[0],i.originY=-p[1]));var d=a.inside==null?typeof a.position=="string"&&a.position.indexOf("inside")>=0:a.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),y=void 0,m=void 0,_=void 0;d&&this.canBeInsideText()?(y=a.insideFill,m=a.insideStroke,(y==null||y==="auto")&&(y=this.getInsideTextFill()),(m==null||m==="auto")&&(m=this.getInsideTextStroke(y),_=!0)):(y=a.outsideFill,m=a.outsideStroke,(y==null||y==="auto")&&(y=this.getOutsideFill()),(m==null||m==="auto")&&(m=this.getOutsideStroke(y),_=!0)),y=y||"#000",(y!==g.fill||m!==g.stroke||_!==g.autoStroke||o!==g.align||s!==g.verticalAlign)&&(l=!0,g.fill=y,g.stroke=m,g.autoStroke=_,g.align=o,g.verticalAlign=s,t.setDefaultTextStyle(g)),t.__dirty|=He,l&&t.dirtyStyle(!0)}},r.prototype.canBeInsideText=function(){return!0},r.prototype.getInsideTextFill=function(){return"#fff"},r.prototype.getInsideTextStroke=function(e){return"#000"},r.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?Bp:Np},r.prototype.getOutsideStroke=function(e){var t=this.__zr&&this.__zr.getBackgroundColor(),a=typeof t=="string"&&Ie(t);a||(a=[255,255,255,1]);for(var n=a[3],i=this.__zr.isDarkMode(),o=0;o<3;o++)a[o]=a[o]*n+(i?0:255)*(1-n);return a[3]=1,Tr(a,"rgba")},r.prototype.traverse=function(e,t){},r.prototype.attrKV=function(e,t){e==="textConfig"?this.setTextConfig(t):e==="textContent"?this.setTextContent(t):e==="clipPath"?this.setClipPath(t):e==="extra"?(this.extra=this.extra||{},V(this.extra,t)):this[e]=t},r.prototype.hide=function(){this.ignore=!0,this.markRedraw()},r.prototype.show=function(){this.ignore=!1,this.markRedraw()},r.prototype.attr=function(e,t){if(typeof e=="string")this.attrKV(e,t);else if(tt(e))for(var a=e,n=St(a),i=0;i0},r.prototype.getState=function(e){return this.states[e]},r.prototype.ensureState=function(e){var t=this.states;return t[e]||(t[e]={}),t[e]},r.prototype.clearStates=function(e){this.useState(pv,!1,e)},r.prototype.useState=function(e,t,a,n){var i=e===pv,o=this.hasState();if(!(!o&&i)){var s=this.currentStates,l=this.stateTransition;if(!(vt(s,e)>=0&&(t||s.length===1))){var u;if(this.stateProxy&&!i&&(u=this.stateProxy(e)),u||(u=this.states&&this.states[e]),!u&&!i){fh("State "+e+" not exists.");return}i||this.saveCurrentToNormalState(u);var f=!!(u&&u.hoverLayer||n);f&&this._toggleHoverLayerFlag(!0),this._applyStateObj(e,u,this._normalState,t,!a&&!this.__inHover&&l&&l.duration>0,l);var h=this._textContent,v=this._textGuide;return h&&h.useState(e,t,a,f),v&&v.useState(e,t,a,f),i?(this.currentStates=[],this._normalState={}):t?this.currentStates.push(e):this.currentStates=[e],this._updateAnimationTargets(),this.markRedraw(),!f&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=~He),u}}},r.prototype.useStates=function(e,t,a){if(!e.length)this.clearStates();else{var n=[],i=this.currentStates,o=e.length,s=o===i.length;if(s){for(var l=0;l0,p);var d=this._textContent,g=this._textGuide;d&&d.useStates(e,t,v),g&&g.useStates(e,t,v),this._updateAnimationTargets(),this.currentStates=e.slice(),this.markRedraw(),!v&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=~He)}},r.prototype.isSilent=function(){for(var e=this.silent,t=this.parent;!e&&t;){if(t.silent){e=!0;break}t=t.parent}return e},r.prototype._updateAnimationTargets=function(){for(var e=0;e=0){var a=this.currentStates.slice();a.splice(t,1),this.useStates(a)}},r.prototype.replaceState=function(e,t,a){var n=this.currentStates.slice(),i=vt(n,e),o=vt(n,t)>=0;i>=0?o?n.splice(i,1):n[i]=t:a&&!o&&n.push(t),this.useStates(n)},r.prototype.toggleState=function(e,t){t?this.useState(e,!0):this.removeState(e)},r.prototype._mergeStates=function(e){for(var t={},a,n=0;n=0&&i.splice(o,1)}),this.animators.push(e),a&&a.animation.addAnimator(e),a&&a.wakeUp()},r.prototype.updateDuringAnimation=function(e){this.markRedraw()},r.prototype.stopAnimation=function(e,t){for(var a=this.animators,n=a.length,i=[],o=0;o0&&t.during&&i[0].during(function(p,d){t.during(d)});for(var v=0;v0||n.force&&!o.length){var T=void 0,A=void 0,D=void 0;if(s){A={},v&&(T={});for(var S=0;S<_;S++){var y=d[S];A[y]=t[y],v?T[y]=a[y]:t[y]=a[y]}}else if(v){D={};for(var S=0;S<_;S++){var y=d[S];D[y]=Is(t[y]),E2(t,a,y)}}var b=new mg(t,!1,!1,h?Pt(p,function(I){return I.targetName===e}):null);b.targetName=e,n.scope&&(b.scope=n.scope),v&&T&&b.whenWithKeys(0,T,d),D&&b.whenWithKeys(0,D,d),b.whenWithKeys(u??500,s?A:a,d).delay(f||0),r.addAnimator(b,e),o.push(b)}}var at=function(r){k(e,r);function e(t){var a=r.call(this)||this;return a.isGroup=!0,a._children=[],a.attr(t),a}return e.prototype.childrenRef=function(){return this._children},e.prototype.children=function(){return this._children.slice()},e.prototype.childAt=function(t){return this._children[t]},e.prototype.childOfName=function(t){for(var a=this._children,n=0;n=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,a){var n=vt(this._children,t);return n>=0&&this.replaceAt(a,n),this},e.prototype.replaceAt=function(t,a){var n=this._children,i=n[a];if(t&&t!==this&&t.parent!==this&&t!==i){n[a]=t,i.parent=null;var o=this.__zr;o&&i.removeSelfFromZr(o),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var a=this.__zr;a&&a!==t.__zr&&t.addSelfToZr(a),a&&a.refresh()},e.prototype.remove=function(t){var a=this.__zr,n=this._children,i=vt(n,t);return i<0?this:(n.splice(i,1),t.parent=null,a&&t.removeSelfFromZr(a),a&&a.refresh(),this)},e.prototype.removeAll=function(){for(var t=this._children,a=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},r.prototype.setSleepAfterStill=function(e){this._sleepAfterStill=e},r.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},r.prototype.refreshHover=function(){this._needsRefreshHover=!0},r.prototype.refreshHoverImmediately=function(){this._disposed||(this._needsRefreshHover=!1,this.painter.refreshHover&&this.painter.getType()==="canvas"&&this.painter.refreshHover())},r.prototype.resize=function(e){this._disposed||(e=e||{},this.painter.resize(e.width,e.height),this.handler.resize())},r.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},r.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},r.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},r.prototype.setCursorStyle=function(e){this._disposed||this.handler.setCursorStyle(e)},r.prototype.findHover=function(e,t){if(!this._disposed)return this.handler.findHover(e,t)},r.prototype.on=function(e,t,a){return this._disposed||this.handler.on(e,t,a),this},r.prototype.off=function(e,t){this._disposed||this.handler.off(e,t)},r.prototype.trigger=function(e,t){this._disposed||this.handler.trigger(e,t)},r.prototype.clear=function(){if(!this._disposed){for(var e=this.storage.getRoots(),t=0;t0){if(r<=n)return o;if(r>=i)return s}else{if(r>=n)return o;if(r<=i)return s}else{if(r===n)return o;if(r===i)return s}return(r-n)/l*u+o}function W(r,e){switch(r){case"center":case"middle":r="50%";break;case"left":case"top":r="0%";break;case"right":case"bottom":r="100%";break}return U(r)?W2(r).match(/%$/)?parseFloat(r)/100*e:parseFloat(r):r==null?NaN:+r}function Ht(r,e,t){return e==null&&(e=10),e=Math.min(Math.max(0,e),iT),r=(+r).toFixed(e),t?r:+r}function Ue(r){return r.sort(function(e,t){return e-t}),r}function Sr(r){if(r=+r,isNaN(r))return 0;if(r>1e-14){for(var e=1,t=0;t<15;t++,e*=10)if(Math.round(r*e)/e===r)return t}return oT(r)}function oT(r){var e=r.toString().toLowerCase(),t=e.indexOf("e"),a=t>0?+e.slice(t+1):0,n=t>0?t:e.length,i=e.indexOf("."),o=i<0?0:n-1-i;return Math.max(0,o-a)}function _g(r,e){var t=Math.log,a=Math.LN10,n=Math.floor(t(r[1]-r[0])/a),i=Math.round(t(Math.abs(e[1]-e[0]))/a),o=Math.min(Math.max(-n+i,0),20);return isFinite(o)?o:20}function U2(r,e,t){if(!r[e])return 0;var a=sT(r,t);return a[e]||0}function sT(r,e){var t=lr(r,function(c,p){return c+(isNaN(p)?0:p)},0);if(t===0)return[];for(var a=Math.pow(10,e),n=G(r,function(c){return(isNaN(c)?0:c)/t*a*100}),i=a*100,o=G(n,function(c){return Math.floor(c)}),s=lr(o,function(c,p){return c+p},0),l=G(n,function(c,p){return c-o[p]});su&&(u=l[h],f=h);++o[f],l[f]=0,++s}return G(o,function(c){return c/a})}function Y2(r,e){var t=Math.max(Sr(r),Sr(e)),a=r+e;return t>iT?a:Ht(a,t)}var Gp=9007199254740991;function Sg(r){var e=Math.PI*2;return(r%e+e)%e}function io(r){return r>-qm&&r=10&&e++,e}function xg(r,e){var t=_h(r),a=Math.pow(10,t),n=r/a,i;return e?n<1.5?i=1:n<2.5?i=2:n<4?i=3:n<7?i=5:i=10:n<1?i=1:n<2?i=2:n<3?i=3:n<5?i=5:i=10,r=i*a,t>=-20?+r.toFixed(t<0?-t:0):r}function Ku(r,e){var t=(r.length-1)*e+1,a=Math.floor(t),n=+r[a-1],i=t-a;return i?n+i*(r[a]-n):n}function Fp(r){r.sort(function(l,u){return s(l,u,0)?-1:1});for(var e=-1/0,t=1,a=0;a=0||i&&vt(i,l)<0)){var u=a.getShallow(l,e);u!=null&&(o[r[s][0]]=u)}}return o}}var gP=[["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]],yP=ii(gP),mP=function(){function r(){}return r.prototype.getAreaStyle=function(e,t){return yP(this,e,t)},r}(),Wp=new xl(50);function _P(r){if(typeof r=="string"){var e=Wp.get(r);return e&&e.image}else return r}function Cg(r,e,t,a,n){if(r)if(typeof r=="string"){if(e&&e.__zrImageSrc===r||!t)return e;var i=Wp.get(r),o={hostEl:t,cb:a,cbPayload:n};return i?(e=i.image,!xh(e)&&i.pending.push(o)):(e=Cr.loadImage(r,Jm,Jm),e.__zrImageSrc=r,Wp.put(r,e.__cachedImgObj={image:e,pending:[o]})),e}else return r;else return e}function Jm(){var r=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=o;l++)s-=o;var u=Xe(t,e);return u>s&&(t="",u=0),s=r-u,n.ellipsis=t,n.ellipsisWidth=u,n.contentWidth=s,n.containerWidth=r,n}function xT(r,e){var t=e.containerWidth,a=e.font,n=e.contentWidth;if(!t)return"";var i=Xe(r,a);if(i<=t)return r;for(var o=0;;o++){if(i<=n||o>=e.maxIterations){r+=e.ellipsis;break}var s=o===0?SP(r,n,e.ascCharWidth,e.cnCharWidth):i>0?Math.floor(r.length*n/i):0;r=r.substr(0,s),i=Xe(r,a)}return r===""&&(r=e.placeholder),r}function SP(r,e,t,a){for(var n=0,i=0,o=r.length;ic&&u){var p=Math.floor(c/s);h=h.slice(0,p)}if(r&&i&&f!=null)for(var d=ST(f,n,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),g=0;gs&&_v(t,r.substring(s,u),e,o),_v(t,l[2],e,o,l[1]),s=mv.lastIndex}sn){b>0?(m.tokens=m.tokens.slice(0,b),g(m,S,_),t.lines=t.lines.slice(0,y+1)):t.lines=t.lines.slice(0,y);break t}var I=w.width,L=I==null||I==="auto";if(typeof I=="string"&&I.charAt(I.length-1)==="%")x.percentWidth=I,f.push(x),x.contentWidth=Xe(x.text,D);else{if(L){var P=w.backgroundColor,R=P&&P.image;R&&(R=_P(R),xh(R)&&(x.width=Math.max(x.width,R.width*M/R.height)))}var E=p&&a!=null?a-S:null;E!=null&&E0&&p+a.accumWidth>a.width&&(f=e.split(` +`),u=!0),a.accumWidth=p}else{var d=bT(e,l,a.width,a.breakAll,a.accumWidth);a.accumWidth=d.accumWidth+c,h=d.linesWidths,f=d.lines}}else f=e.split(` +`);for(var g=0;g=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}var CP=lr(",&?/;] ".split(""),function(r,e){return r[e]=!0,r},{});function DP(r){return AP(r)?!!CP[r]:!0}function bT(r,e,t,a,n){for(var i=[],o=[],s="",l="",u=0,f=0,h=0;ht:n+f+c>t){f?(s||l)&&(p?(s||(s=l,l="",u=0,f=u),i.push(s),o.push(f-u),l+=v,u+=c,s="",f=u):(l&&(s+=l,l="",u=0),i.push(s),o.push(f),s=v,f=c)):p?(i.push(l),o.push(u),l=v,u=c):(i.push(v),o.push(c));continue}f+=c,p?(l+=v,u+=c):(l&&(s+=l,l="",u=0),s+=v)}return!i.length&&!s&&(s=r,l="",u=0),l&&(s+=l),s&&(i.push(s),o.push(f)),i.length===1&&(f+=n),{accumWidth:f,lines:i,linesWidths:o}}var Up="__zr_style_"+Math.round(Math.random()*10),Kn={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},bh={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};Kn[Up]=!0;var e0=["z","z2","invisible"],MP=["invisible"],ur=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype._init=function(t){for(var a=St(t),n=0;n1e-4){s[0]=r-t,s[1]=e-a,l[0]=r+t,l[1]=e+a;return}if(Ql[0]=wv(n)*t+r,Ql[1]=bv(n)*a+e,Jl[0]=wv(i)*t+r,Jl[1]=bv(i)*a+e,u(s,Ql,Jl),f(l,Ql,Jl),n=n%pn,n<0&&(n=n+pn),i=i%pn,i<0&&(i=i+pn),n>i&&!o?i+=pn:nn&&(tu[0]=wv(c)*t+r,tu[1]=bv(c)*a+e,u(s,tu,s),f(l,tu,l))}var Ot={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},dn=[],gn=[],Pr=[],Ta=[],Rr=[],Er=[],Tv=Math.min,Av=Math.max,yn=Math.cos,mn=Math.sin,ra=Math.abs,Yp=Math.PI,Ra=Yp*2,Cv=typeof Float32Array<"u",Ho=[];function Dv(r){var e=Math.round(r/Yp*1e8)/1e8;return e%2*Yp}function Dg(r,e){var t=Dv(r[0]);t<0&&(t+=Ra);var a=t-r[0],n=r[1];n+=a,!e&&n-t>=Ra?n=t+Ra:e&&t-n>=Ra?n=t-Ra:!e&&t>n?n=t+(Ra-Dv(t-n)):e&&t0&&(this._ux=ra(a/Sf/e)||0,this._uy=ra(a/Sf/t)||0)},r.prototype.setDPR=function(e){this.dpr=e},r.prototype.setContext=function(e){this._ctx=e},r.prototype.getContext=function(){return this._ctx},r.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},r.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},r.prototype.moveTo=function(e,t){return this._drawPendingPt(),this.addData(Ot.M,e,t),this._ctx&&this._ctx.moveTo(e,t),this._x0=e,this._y0=t,this._xi=e,this._yi=t,this},r.prototype.lineTo=function(e,t){var a=ra(e-this._xi),n=ra(t-this._yi),i=a>this._ux||n>this._uy;if(this.addData(Ot.L,e,t),this._ctx&&i&&this._ctx.lineTo(e,t),i)this._xi=e,this._yi=t,this._pendingPtDist=0;else{var o=a*a+n*n;o>this._pendingPtDist&&(this._pendingPtX=e,this._pendingPtY=t,this._pendingPtDist=o)}return this},r.prototype.bezierCurveTo=function(e,t,a,n,i,o){return this._drawPendingPt(),this.addData(Ot.C,e,t,a,n,i,o),this._ctx&&this._ctx.bezierCurveTo(e,t,a,n,i,o),this._xi=i,this._yi=o,this},r.prototype.quadraticCurveTo=function(e,t,a,n){return this._drawPendingPt(),this.addData(Ot.Q,e,t,a,n),this._ctx&&this._ctx.quadraticCurveTo(e,t,a,n),this._xi=a,this._yi=n,this},r.prototype.arc=function(e,t,a,n,i,o){this._drawPendingPt(),Ho[0]=n,Ho[1]=i,Dg(Ho,o),n=Ho[0],i=Ho[1];var s=i-n;return this.addData(Ot.A,e,t,a,a,n,s,0,o?0:1),this._ctx&&this._ctx.arc(e,t,a,n,i,o),this._xi=yn(i)*a+e,this._yi=mn(i)*a+t,this},r.prototype.arcTo=function(e,t,a,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(e,t,a,n,i),this},r.prototype.rect=function(e,t,a,n){return this._drawPendingPt(),this._ctx&&this._ctx.rect(e,t,a,n),this.addData(Ot.R,e,t,a,n),this},r.prototype.closePath=function(){this._drawPendingPt(),this.addData(Ot.Z);var e=this._ctx,t=this._x0,a=this._y0;return e&&e.closePath(),this._xi=t,this._yi=a,this},r.prototype.fill=function(e){e&&e.fill(),this.toStatic()},r.prototype.stroke=function(e){e&&e.stroke(),this.toStatic()},r.prototype.len=function(){return this._len},r.prototype.setData=function(e){var t=e.length;!(this.data&&this.data.length===t)&&Cv&&(this.data=new Float32Array(t));for(var a=0;af.length&&(this._expandData(),f=this.data);for(var h=0;h0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},r.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var e=[],t=0;t11&&(this.data=new Float32Array(e)))}},r.prototype.getBoundingRect=function(){Pr[0]=Pr[1]=Rr[0]=Rr[1]=Number.MAX_VALUE,Ta[0]=Ta[1]=Er[0]=Er[1]=-Number.MAX_VALUE;var e=this.data,t=0,a=0,n=0,i=0,o;for(o=0;oa||ra(_)>n||v===t-1)&&(d=Math.sqrt(m*m+_*_),i=g,o=y);break}case Ot.C:{var S=e[v++],b=e[v++],g=e[v++],y=e[v++],x=e[v++],w=e[v++];d=jL(i,o,S,b,g,y,x,w,10),i=x,o=w;break}case Ot.Q:{var S=e[v++],b=e[v++],g=e[v++],y=e[v++];d=JL(i,o,S,b,g,y,10),i=g,o=y;break}case Ot.A:var T=e[v++],A=e[v++],D=e[v++],M=e[v++],I=e[v++],L=e[v++],P=L+I;v+=1,p&&(s=yn(I)*D+T,l=mn(I)*M+A),d=Av(D,M)*Tv(Ra,Math.abs(L)),i=yn(P)*D+T,o=mn(P)*M+A;break;case Ot.R:{s=i=e[v++],l=o=e[v++];var R=e[v++],E=e[v++];d=R*2+E*2;break}case Ot.Z:{var m=s-i,_=l-o;d=Math.sqrt(m*m+_*_),i=s,o=l;break}}d>=0&&(u[h++]=d,f+=d)}return this._pathLen=f,f},r.prototype.rebuildPath=function(e,t){var a=this.data,n=this._ux,i=this._uy,o=this._len,s,l,u,f,h,v,c=t<1,p,d,g=0,y=0,m,_=0,S,b;if(!(c&&(this._pathSegLen||this._calculateLength(),p=this._pathSegLen,d=this._pathLen,m=t*d,!m)))t:for(var x=0;x0&&(e.lineTo(S,b),_=0),w){case Ot.M:s=u=a[x++],l=f=a[x++],e.moveTo(u,f);break;case Ot.L:{h=a[x++],v=a[x++];var A=ra(h-u),D=ra(v-f);if(A>n||D>i){if(c){var M=p[y++];if(g+M>m){var I=(m-g)/M;e.lineTo(u*(1-I)+h*I,f*(1-I)+v*I);break t}g+=M}e.lineTo(h,v),u=h,f=v,_=0}else{var L=A*A+D*D;L>_&&(S=h,b=v,_=L)}break}case Ot.C:{var P=a[x++],R=a[x++],E=a[x++],N=a[x++],O=a[x++],B=a[x++];if(c){var M=p[y++];if(g+M>m){var I=(m-g)/M;ja(u,P,E,O,I,dn),ja(f,R,N,B,I,gn),e.bezierCurveTo(dn[1],gn[1],dn[2],gn[2],dn[3],gn[3]);break t}g+=M}e.bezierCurveTo(P,R,E,N,O,B),u=O,f=B;break}case Ot.Q:{var P=a[x++],R=a[x++],E=a[x++],N=a[x++];if(c){var M=p[y++];if(g+M>m){var I=(m-g)/M;Us(u,P,E,I,dn),Us(f,R,N,I,gn),e.quadraticCurveTo(dn[1],gn[1],dn[2],gn[2]);break t}g+=M}e.quadraticCurveTo(P,R,E,N),u=E,f=N;break}case Ot.A:var F=a[x++],H=a[x++],Y=a[x++],j=a[x++],J=a[x++],ct=a[x++],xt=a[x++],pt=!a[x++],rt=Y>j?Y:j,dt=ra(Y-j)>.001,lt=J+ct,q=!1;if(c){var M=p[y++];g+M>m&&(lt=J+ct*(m-g)/M,q=!0),g+=M}if(dt&&e.ellipse?e.ellipse(F,H,Y,j,xt,J,lt,pt):e.arc(F,H,rt,J,lt,pt),q)break t;T&&(s=yn(J)*Y+F,l=mn(J)*j+H),u=yn(lt)*Y+F,f=mn(lt)*j+H;break;case Ot.R:s=u=a[x],l=f=a[x+1],h=a[x++],v=a[x++];var ut=a[x++],Gt=a[x++];if(c){var M=p[y++];if(g+M>m){var At=m-g;e.moveTo(h,v),e.lineTo(h+Tv(At,ut),v),At-=ut,At>0&&e.lineTo(h+ut,v+Tv(At,Gt)),At-=Gt,At>0&&e.lineTo(h+Av(ut-At,0),v+Gt),At-=ut,At>0&&e.lineTo(h,v+Av(Gt-At,0));break t}g+=M}e.rect(h,v,ut,Gt);break;case Ot.Z:if(c){var M=p[y++];if(g+M>m){var I=(m-g)/M;e.lineTo(u*(1-I)+s*I,f*(1-I)+l*I);break t}g+=M}e.closePath(),u=s,f=l}}},r.prototype.clone=function(){var e=new r,t=this.data;return e.data=t.slice?t.slice():Array.prototype.slice.call(t),e._len=this._len,e},r.CMD=Ot,r.initDefaultProps=function(){var e=r.prototype;e._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,e._version=0}(),r}();function ka(r,e,t,a,n,i,o){if(n===0)return!1;var s=n,l=0,u=r;if(o>e+s&&o>a+s||or+s&&i>t+s||ie+h&&f>a+h&&f>i+h&&f>s+h||fr+h&&u>t+h&&u>n+h&&u>o+h||ue+u&&l>a+u&&l>i+u||lr+u&&s>t+u&&s>n+u||st||f+un&&(n+=Wo);var v=Math.atan2(l,s);return v<0&&(v+=Wo),v>=a&&v<=n||v+Wo>=a&&v+Wo<=n}function oa(r,e,t,a,n,i){if(i>e&&i>a||in?s:0}var Aa=qr.CMD,_n=Math.PI*2,OP=1e-4;function NP(r,e){return Math.abs(r-e)e&&u>a&&u>i&&u>s||u1&&BP(),c=te(e,a,i,s,er[0]),v>1&&(p=te(e,a,i,s,er[1]))),v===2?ge&&s>a&&s>i||s=0&&u<=1){for(var f=0,h=se(e,a,i,u),v=0;vt||s<-t)return 0;var l=Math.sqrt(t*t-s*s);Ae[0]=-l,Ae[1]=l;var u=Math.abs(a-n);if(u<1e-4)return 0;if(u>=_n-1e-4){a=0,n=_n;var f=i?1:-1;return o>=Ae[0]+r&&o<=Ae[1]+r?f:0}if(a>n){var h=a;a=n,n=h}a<0&&(a+=_n,n+=_n);for(var v=0,c=0;c<2;c++){var p=Ae[c];if(p+r>o){var d=Math.atan2(s,p),f=i?1:-1;d<0&&(d=_n+d),(d>=a&&d<=n||d+_n>=a&&d+_n<=n)&&(d>Math.PI/2&&d1&&(t||(s+=oa(l,u,f,h,a,n))),g&&(l=i[p],u=i[p+1],f=l,h=u),d){case Aa.M:f=i[p++],h=i[p++],l=f,u=h;break;case Aa.L:if(t){if(ka(l,u,i[p],i[p+1],e,a,n))return!0}else s+=oa(l,u,i[p],i[p+1],a,n)||0;l=i[p++],u=i[p++];break;case Aa.C:if(t){if(EP(l,u,i[p++],i[p++],i[p++],i[p++],i[p],i[p+1],e,a,n))return!0}else s+=VP(l,u,i[p++],i[p++],i[p++],i[p++],i[p],i[p+1],a,n)||0;l=i[p++],u=i[p++];break;case Aa.Q:if(t){if(wT(l,u,i[p++],i[p++],i[p],i[p+1],e,a,n))return!0}else s+=zP(l,u,i[p++],i[p++],i[p],i[p+1],a,n)||0;l=i[p++],u=i[p++];break;case Aa.A:var y=i[p++],m=i[p++],_=i[p++],S=i[p++],b=i[p++],x=i[p++];p+=1;var w=!!(1-i[p++]);v=Math.cos(b)*_+y,c=Math.sin(b)*S+m,g?(f=v,h=c):s+=oa(l,u,v,c,a,n);var T=(a-y)*S/_+y;if(t){if(kP(y,m,S,b,b+x,w,e,T,n))return!0}else s+=GP(y,m,S,b,b+x,w,T,n);l=Math.cos(b+x)*_+y,u=Math.sin(b+x)*S+m;break;case Aa.R:f=l=i[p++],h=u=i[p++];var A=i[p++],D=i[p++];if(v=f+A,c=h+D,t){if(ka(f,h,v,h,e,a,n)||ka(v,h,v,c,e,a,n)||ka(v,c,f,c,e,a,n)||ka(f,c,f,h,e,a,n))return!0}else s+=oa(v,h,v,c,a,n),s+=oa(f,c,f,h,a,n);break;case Aa.Z:if(t){if(ka(l,u,f,h,e,a,n))return!0}else s+=oa(l,u,f,h,a,n);l=f,u=h;break}}return!t&&!NP(u,h)&&(s+=oa(l,u,f,h,a,n)||0),s!==0}function FP(r,e,t){return TT(r,0,!1,e,t)}function HP(r,e,t,a){return TT(r,e,!0,t,a)}var bf=Q({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},Kn),WP={style:Q({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},bh.style)},Mv=$r.concat(["invisible","culling","z","z2","zlevel","parent"]),yt=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.update=function(){var t=this;r.prototype.update.call(this);var a=this.style;if(a.decal){var n=this._decalEl=this._decalEl||new e;n.buildPath===e.prototype.buildPath&&(n.buildPath=function(l){t.buildPath(l,t.shape)}),n.silent=!0;var i=n.style;for(var o in a)i[o]!==a[o]&&(i[o]=a[o]);i.fill=a.fill?a.decal:null,i.decal=null,i.shadowColor=null,a.strokeFirst&&(i.stroke=null);for(var s=0;s.5?Np:a>.2?I2:Bp}else if(t)return Bp}return Np},e.prototype.getInsideTextStroke=function(t){var a=this.style.fill;if(U(a)){var n=this.__zr,i=!!(n&&n.isDarkMode()),o=$s(t,0)0))},e.prototype.hasFill=function(){var t=this.style,a=t.fill;return a!=null&&a!=="none"},e.prototype.getBoundingRect=function(){var t=this._rect,a=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var o=this.path;(i||this.__dirty&Fi)&&(o.beginPath(),this.buildPath(o,this.shape,!1),this.pathUpdated()),t=o.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var s=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){s.copy(t);var l=a.strokeNoScale?this.getLineScale():1,u=a.lineWidth;if(!this.hasFill()){var f=this.strokeContainThreshold;u=Math.max(u,f??4)}l>1e-10&&(s.width+=u/l,s.height+=u/l,s.x-=u/l/2,s.y-=u/l/2)}return s}return t},e.prototype.contain=function(t,a){var n=this.transformCoordToLocal(t,a),i=this.getBoundingRect(),o=this.style;if(t=n[0],a=n[1],i.contain(t,a)){var s=this.path;if(this.hasStroke()){var l=o.lineWidth,u=o.strokeNoScale?this.getLineScale():1;if(u>1e-10&&(this.hasFill()||(l=Math.max(l,this.strokeContainThreshold)),HP(s,l/u,t,a)))return!0}if(this.hasFill())return FP(s,t,a)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=Fi,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){t==="style"?this.dirtyStyle():t==="shape"?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(t,a){t==="shape"?this.setShape(a):r.prototype.attrKV.call(this,t,a)},e.prototype.setShape=function(t,a){var n=this.shape;return n||(n=this.shape={}),typeof t=="string"?n[t]=a:V(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(this.__dirty&Fi)},e.prototype.createStyle=function(t){return _l(bf,t)},e.prototype._innerSaveToNormal=function(t){r.prototype._innerSaveToNormal.call(this,t);var a=this._normalState;t.shape&&!a.shape&&(a.shape=V({},this.shape))},e.prototype._applyStateObj=function(t,a,n,i,o,s){r.prototype._applyStateObj.call(this,t,a,n,i,o,s);var l=!(a&&i),u;if(a&&a.shape?o?i?u=a.shape:(u=V({},n.shape),V(u,a.shape)):(u=V({},i?this.shape:n.shape),V(u,a.shape)):l&&(u=n.shape),u)if(o){this.shape=V({},this.shape);for(var f={},h=St(u),v=0;v0},e.prototype.hasFill=function(){var t=this.style,a=t.fill;return a!=null&&a!=="none"},e.prototype.createStyle=function(t){return _l(UP,t)},e.prototype.setBoundingRect=function(t){this._rect=t},e.prototype.getBoundingRect=function(){var t=this.style;if(!this._rect){var a=t.text;a!=null?a+="":a="";var n=bl(a,t.font,t.textAlign,t.textBaseline);if(n.x+=t.x||0,n.y+=t.y||0,this.hasStroke()){var i=t.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},e.initDefaultProps=function(){var t=e.prototype;t.dirtyRectTolerance=10}(),e}(ur);oo.prototype.type="tspan";var YP=Q({x:0,y:0},Kn),XP={style:Q({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},bh.style)};function $P(r){return!!(r&&typeof r!="string"&&r.width&&r.height)}var oe=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.createStyle=function(t){return _l(YP,t)},e.prototype._getSize=function(t){var a=this.style,n=a[t];if(n!=null)return n;var i=$P(a.image)?a.image:this.__image;if(!i)return 0;var o=t==="width"?"height":"width",s=a[o];return s==null?i[t]:i[t]/i[o]*s},e.prototype.getWidth=function(){return this._getSize("width")},e.prototype.getHeight=function(){return this._getSize("height")},e.prototype.getAnimationStyleProps=function(){return XP},e.prototype.getBoundingRect=function(){var t=this.style;return this._rect||(this._rect=new ht(t.x||0,t.y||0,this.getWidth(),this.getHeight())),this._rect},e}(ur);oe.prototype.type="image";function ZP(r,e){var t=e.x,a=e.y,n=e.width,i=e.height,o=e.r,s,l,u,f;n<0&&(t=t+n,n=-n),i<0&&(a=a+i,i=-i),typeof o=="number"?s=l=u=f=o:o instanceof Array?o.length===1?s=l=u=f=o[0]:o.length===2?(s=u=o[0],l=f=o[1]):o.length===3?(s=o[0],l=f=o[1],u=o[2]):(s=o[0],l=o[1],u=o[2],f=o[3]):s=l=u=f=0;var h;s+l>n&&(h=s+l,s*=n/h,l*=n/h),u+f>n&&(h=u+f,u*=n/h,f*=n/h),l+u>i&&(h=l+u,l*=i/h,u*=i/h),s+f>i&&(h=s+f,s*=i/h,f*=i/h),r.moveTo(t+s,a),r.lineTo(t+n-l,a),l!==0&&r.arc(t+n-l,a+l,l,-Math.PI/2,0),r.lineTo(t+n,a+i-u),u!==0&&r.arc(t+n-u,a+i-u,u,0,Math.PI/2),r.lineTo(t+f,a+i),f!==0&&r.arc(t+f,a+i-f,f,Math.PI/2,Math.PI),r.lineTo(t,a+s),s!==0&&r.arc(t+s,a+s,s,Math.PI,Math.PI*1.5)}var $i=Math.round;function AT(r,e,t){if(e){var a=e.x1,n=e.x2,i=e.y1,o=e.y2;r.x1=a,r.x2=n,r.y1=i,r.y2=o;var s=t&&t.lineWidth;return s&&($i(a*2)===$i(n*2)&&(r.x1=r.x2=Hn(a,s,!0)),$i(i*2)===$i(o*2)&&(r.y1=r.y2=Hn(i,s,!0))),r}}function CT(r,e,t){if(e){var a=e.x,n=e.y,i=e.width,o=e.height;r.x=a,r.y=n,r.width=i,r.height=o;var s=t&&t.lineWidth;return s&&(r.x=Hn(a,s,!0),r.y=Hn(n,s,!0),r.width=Math.max(Hn(a+i,s,!1)-r.x,i===0?0:1),r.height=Math.max(Hn(n+o,s,!1)-r.y,o===0?0:1)),r}}function Hn(r,e,t){if(!e)return r;var a=$i(r*2);return(a+$i(e))%2===0?a/2:(a+(t?1:-1))/2}var qP=function(){function r(){this.x=0,this.y=0,this.width=0,this.height=0}return r}(),KP={},wt=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.getDefaultShape=function(){return new qP},e.prototype.buildPath=function(t,a){var n,i,o,s;if(this.subPixelOptimize){var l=CT(KP,a,this.style);n=l.x,i=l.y,o=l.width,s=l.height,l.r=a.r,a=l}else n=a.x,i=a.y,o=a.width,s=a.height;a.r?ZP(t,a):t.rect(n,i,o,s)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(yt);wt.prototype.type="rect";var o0={fill:"#000"},s0=2,jP={style:Q({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},bh.style)},bt=function(r){k(e,r);function e(t){var a=r.call(this)||this;return a.type="text",a._children=[],a._defaultStyle=o0,a.attr(t),a}return e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){r.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var t=0;t0,I=t.width!=null&&(t.overflow==="truncate"||t.overflow==="break"||t.overflow==="breakAll"),L=o.calculatedLineHeight,P=0;P=0&&(P=x[L],P.align==="right");)this._placeToken(P,t,T,y,I,"right",_),A-=P.width,I-=P.width,L--;for(M+=(i-(M-g)-(m-I)-A)/2;D<=L;)P=x[D],this._placeToken(P,t,T,y,M+P.width/2,"center",_),M+=P.width,D++;y+=T}},e.prototype._placeToken=function(t,a,n,i,o,s,l){var u=a.rich[t.styleName]||{};u.text=t.text;var f=t.verticalAlign,h=i+n/2;f==="top"?h=i+t.height/2:f==="bottom"&&(h=i+n-t.height/2);var v=!t.isLineHolder&&Iv(u);v&&this._renderBackground(u,a,s==="right"?o-t.width:s==="center"?o-t.width/2:o,h-t.height/2,t.width,t.height);var c=!!u.backgroundColor,p=t.textPadding;p&&(o=c0(o,s,p),h-=t.height/2-p[0]-t.innerHeight/2);var d=this._getOrCreateChild(oo),g=d.createStyle();d.useStyle(g);var y=this._defaultStyle,m=!1,_=0,S=v0("fill"in u?u.fill:"fill"in a?a.fill:(m=!0,y.fill)),b=h0("stroke"in u?u.stroke:"stroke"in a?a.stroke:!c&&!l&&(!y.autoStroke||m)?(_=s0,y.stroke):null),x=u.textShadowBlur>0||a.textShadowBlur>0;g.text=t.text,g.x=o,g.y=h,x&&(g.shadowBlur=u.textShadowBlur||a.textShadowBlur||0,g.shadowColor=u.textShadowColor||a.textShadowColor||"transparent",g.shadowOffsetX=u.textShadowOffsetX||a.textShadowOffsetX||0,g.shadowOffsetY=u.textShadowOffsetY||a.textShadowOffsetY||0),g.textAlign=s,g.textBaseline="middle",g.font=t.font||Ka,g.opacity=br(u.opacity,a.opacity,1),u0(g,u),b&&(g.lineWidth=br(u.lineWidth,a.lineWidth,_),g.lineDash=ot(u.lineDash,a.lineDash),g.lineDashOffset=a.lineDashOffset||0,g.stroke=b),S&&(g.fill=S);var w=t.contentWidth,T=t.contentHeight;d.setBoundingRect(new ht(ps(g.x,w,g.textAlign),Hi(g.y,T,g.textBaseline),w,T))},e.prototype._renderBackground=function(t,a,n,i,o,s){var l=t.backgroundColor,u=t.borderWidth,f=t.borderColor,h=l&&l.image,v=l&&!h,c=t.borderRadius,p=this,d,g;if(v||t.lineHeight||u&&f){d=this._getOrCreateChild(wt),d.useStyle(d.createStyle()),d.style.fill=null;var y=d.shape;y.x=n,y.y=i,y.width=o,y.height=s,y.r=c,d.dirtyShape()}if(v){var m=d.style;m.fill=l||null,m.fillOpacity=ot(t.fillOpacity,1)}else if(h){g=this._getOrCreateChild(oe),g.onload=function(){p.dirtyStyle()};var _=g.style;_.image=l.image,_.x=n,_.y=i,_.width=o,_.height=s}if(u&&f){var m=d.style;m.lineWidth=u,m.stroke=f,m.strokeOpacity=ot(t.strokeOpacity,1),m.lineDash=t.borderDash,m.lineDashOffset=t.borderDashOffset||0,d.strokeContainThreshold=0,d.hasFill()&&d.hasStroke()&&(m.strokeFirst=!0,m.lineWidth*=2)}var S=(d||g).style;S.shadowBlur=t.shadowBlur||0,S.shadowColor=t.shadowColor||"transparent",S.shadowOffsetX=t.shadowOffsetX||0,S.shadowOffsetY=t.shadowOffsetY||0,S.opacity=br(t.opacity,a.opacity,1)},e.makeFont=function(t){var a="";return MT(t)&&(a=[t.fontStyle,t.fontWeight,DT(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),a&&or(a)||t.textFont||t.font},e}(ur),QP={left:!0,right:1,center:1},JP={top:1,bottom:1,middle:1},l0=["fontStyle","fontWeight","fontSize","fontFamily"];function DT(r){return typeof r=="string"&&(r.indexOf("px")!==-1||r.indexOf("rem")!==-1||r.indexOf("em")!==-1)?r:isNaN(+r)?sg+"px":r+"px"}function u0(r,e){for(var t=0;t=0,i=!1;if(r instanceof yt){var o=IT(r),s=n&&o.selectFill||o.normalFill,l=n&&o.selectStroke||o.normalStroke;if(Ti(s)||Ti(l)){a=a||{};var u=a.style||{};u.fill==="inherit"?(i=!0,a=V({},a),u=V({},u),u.fill=s):!Ti(u.fill)&&Ti(s)?(i=!0,a=V({},a),u=V({},u),u.fill=mf(s)):!Ti(u.stroke)&&Ti(l)&&(i||(a=V({},a),u=V({},u)),u.stroke=mf(l)),a.style=u}}if(a&&a.z2==null){i||(a=V({},a));var f=r.z2EmphasisLift;a.z2=r.z2+(f??mo)}return a}function oR(r,e,t){if(t&&t.z2==null){t=V({},t);var a=r.z2SelectLift;t.z2=r.z2+(a??eR)}return t}function sR(r,e,t){var a=vt(r.currentStates,e)>=0,n=r.style.opacity,i=a?null:nR(r,["opacity"],e,{opacity:1});t=t||{};var o=t.style||{};return o.opacity==null&&(t=V({},t),o=V({opacity:a?n:i.opacity*.1},o),t.style=o),t}function Lv(r,e){var t=this.states[r];if(this.style){if(r==="emphasis")return iR(this,r,e,t);if(r==="blur")return sR(this,r,t);if(r==="select")return oR(this,r,t)}return t}function oi(r){r.stateProxy=Lv;var e=r.getTextContent(),t=r.getTextGuideLine();e&&(e.stateProxy=Lv),t&&(t.stateProxy=Lv)}function m0(r,e){!NT(r,e)&&!r.__highByOuter&&ba(r,LT)}function _0(r,e){!NT(r,e)&&!r.__highByOuter&&ba(r,PT)}function da(r,e){r.__highByOuter|=1<<(e||0),ba(r,LT)}function ga(r,e){!(r.__highByOuter&=~(1<<(e||0)))&&ba(r,PT)}function ET(r){ba(r,Lg)}function Pg(r){ba(r,RT)}function kT(r){ba(r,rR)}function OT(r){ba(r,aR)}function NT(r,e){return r.__highDownSilentOnTouch&&e.zrByTouch}function BT(r){var e=r.getModel(),t=[],a=[];e.eachComponent(function(n,i){var o=Mg(i),s=n==="series",l=s?r.getViewOfSeriesModel(i):r.getViewOfComponentModel(i);!s&&a.push(l),o.isBlured&&(l.group.traverse(function(u){RT(u)}),s&&t.push(i)),o.isBlured=!1}),C(a,function(n){n&&n.toggleBlurSeries&&n.toggleBlurSeries(t,!1,e)})}function $p(r,e,t,a){var n=a.getModel();t=t||"coordinateSystem";function i(u,f){for(var h=0;h0){var s={dataIndex:o,seriesIndex:t.seriesIndex};i!=null&&(s.dataType=i),e.push(s)}})}),e}function Ya(r,e,t){Wn(r,!0),ba(r,oi),qp(r,e,t)}function cR(r){Wn(r,!1)}function Wt(r,e,t,a){a?cR(r):Ya(r,e,t)}function qp(r,e,t){var a=nt(r);e!=null?(a.focus=e,a.blurScope=t):a.focus&&(a.focus=null)}var x0=["emphasis","blur","select"],pR={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function he(r,e,t,a){t=t||"itemStyle";for(var n=0;n1&&(o*=Pv(p),s*=Pv(p));var d=(n===i?-1:1)*Pv((o*o*(s*s)-o*o*(c*c)-s*s*(v*v))/(o*o*(c*c)+s*s*(v*v)))||0,g=d*o*c/s,y=d*-s*v/o,m=(r+t)/2+ru(h)*g-eu(h)*y,_=(e+a)/2+eu(h)*g+ru(h)*y,S=A0([1,0],[(v-g)/o,(c-y)/s]),b=[(v-g)/o,(c-y)/s],x=[(-1*v-g)/o,(-1*c-y)/s],w=A0(b,x);if(jp(b,x)<=-1&&(w=Uo),jp(b,x)>=1&&(w=0),w<0){var T=Math.round(w/Uo*1e6)/1e6;w=Uo*2+T%2*Uo}f.addData(u,m,_,o,s,S,w,h,i)}var SR=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig,xR=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;function bR(r){var e=new qr;if(!r)return e;var t=0,a=0,n=t,i=a,o,s=qr.CMD,l=r.match(SR);if(!l)return e;for(var u=0;uP*P+R*R&&(T=D,A=M),{cx:T,cy:A,x0:-f,y0:-h,x1:T*(n/b-1),y1:A*(n/b-1)}}function IR(r){var e;if(z(r)){var t=r.length;if(!t)return r;t===1?e=[r[0],r[0],0,0]:t===2?e=[r[0],r[0],r[1],r[1]]:t===3?e=r.concat(r[2]):e=r}else e=[r,r,r,r];return e}function LR(r,e){var t,a=ds(e.r,0),n=ds(e.r0||0,0),i=a>0,o=n>0;if(!(!i&&!o)){if(i||(a=n,n=0),n>a){var s=a;a=n,n=s}var l=e.startAngle,u=e.endAngle;if(!(isNaN(l)||isNaN(u))){var f=e.cx,h=e.cy,v=!!e.clockwise,c=D0(u-l),p=c>Rv&&c%Rv;if(p>gr&&(c=p),!(a>gr))r.moveTo(f,h);else if(c>Rv-gr)r.moveTo(f+a*Ci(l),h+a*Sn(l)),r.arc(f,h,a,l,u,!v),n>gr&&(r.moveTo(f+n*Ci(u),h+n*Sn(u)),r.arc(f,h,n,u,l,v));else{var d=void 0,g=void 0,y=void 0,m=void 0,_=void 0,S=void 0,b=void 0,x=void 0,w=void 0,T=void 0,A=void 0,D=void 0,M=void 0,I=void 0,L=void 0,P=void 0,R=a*Ci(l),E=a*Sn(l),N=n*Ci(u),O=n*Sn(u),B=c>gr;if(B){var F=e.cornerRadius;F&&(t=IR(F),d=t[0],g=t[1],y=t[2],m=t[3]);var H=D0(a-n)/2;if(_=kr(H,y),S=kr(H,m),b=kr(H,d),x=kr(H,g),A=w=ds(_,S),D=T=ds(b,x),(w>gr||T>gr)&&(M=a*Ci(u),I=a*Sn(u),L=n*Ci(l),P=n*Sn(l),cgr){var dt=kr(y,A),lt=kr(m,A),q=au(L,P,R,E,a,dt,v),ut=au(M,I,N,O,a,lt,v);r.moveTo(f+q.cx+q.x0,h+q.cy+q.y0),A0&&r.arc(f+q.cx,h+q.cy,dt,pe(q.y0,q.x0),pe(q.y1,q.x1),!v),r.arc(f,h,a,pe(q.cy+q.y1,q.cx+q.x1),pe(ut.cy+ut.y1,ut.cx+ut.x1),!v),lt>0&&r.arc(f+ut.cx,h+ut.cy,lt,pe(ut.y1,ut.x1),pe(ut.y0,ut.x0),!v))}else r.moveTo(f+R,h+E),r.arc(f,h,a,l,u,!v);if(!(n>gr)||!B)r.lineTo(f+N,h+O);else if(D>gr){var dt=kr(d,D),lt=kr(g,D),q=au(N,O,M,I,n,-lt,v),ut=au(R,E,L,P,n,-dt,v);r.lineTo(f+q.cx+q.x0,h+q.cy+q.y0),D0&&r.arc(f+q.cx,h+q.cy,lt,pe(q.y0,q.x0),pe(q.y1,q.x1),!v),r.arc(f,h,n,pe(q.cy+q.y1,q.cx+q.x1),pe(ut.cy+ut.y1,ut.cx+ut.x1),v),dt>0&&r.arc(f+ut.cx,h+ut.cy,dt,pe(ut.y1,ut.x1),pe(ut.y0,ut.x0),!v))}else r.lineTo(f+N,h+O),r.arc(f,h,n,u,l,v)}r.closePath()}}}var PR=function(){function r(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=Math.PI*2,this.clockwise=!0,this.cornerRadius=0}return r}(),_e=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.getDefaultShape=function(){return new PR},e.prototype.buildPath=function(t,a){LR(t,a)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(yt);_e.prototype.type="sector";var RR=function(){function r(){this.cx=0,this.cy=0,this.r=0,this.r0=0}return r}(),_o=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.getDefaultShape=function(){return new RR},e.prototype.buildPath=function(t,a){var n=a.cx,i=a.cy,o=Math.PI*2;t.moveTo(n+a.r,i),t.arc(n,i,a.r,0,o,!1),t.moveTo(n+a.r0,i),t.arc(n,i,a.r0,0,o,!0)},e}(yt);_o.prototype.type="ring";function ER(r,e,t,a){var n=[],i=[],o=[],s=[],l,u,f,h;if(a){f=[1/0,1/0],h=[-1/0,-1/0];for(var v=0,c=r.length;v=2){if(a){var i=ER(n,a,t,e.smoothConstraint);r.moveTo(n[0][0],n[0][1]);for(var o=n.length,s=0;s<(t?o:o-1);s++){var l=i[s*2],u=i[s*2+1],f=n[(s+1)%o];r.bezierCurveTo(l[0],l[1],u[0],u[1],f[0],f[1])}}else{r.moveTo(n[0][0],n[0][1]);for(var s=1,h=n.length;sbn[1]){if(s=!1,i)return s;var f=Math.abs(bn[0]-xn[1]),h=Math.abs(xn[0]-bn[1]);Math.min(f,h)>n.len()&&(f0){var h=f.duration,v=f.delay,c=f.easing,p={duration:h,delay:v||0,easing:c,done:i,force:!!i||!!o,setToFinal:!u,scope:r,during:o};s?e.animateFrom(t,p):e.animateTo(t,p)}else e.stopAnimation(),!s&&e.attr(t),o&&o(1),i&&i()}function Dt(r,e,t,a,n,i){Og("update",r,e,t,a,n,i)}function Vt(r,e,t,a,n,i){Og("enter",r,e,t,a,n,i)}function Ji(r){if(!r.__zr)return!0;for(var e=0;eMath.abs(i[1])?i[0]>0?"right":"left":i[1]>0?"bottom":"top"}function L0(r){return!r.isGroup}function UR(r){return r.shape!=null}function Ml(r,e,t){if(!r||!e)return;function a(o){var s={};return o.traverse(function(l){L0(l)&&l.anid&&(s[l.anid]=l)}),s}function n(o){var s={x:o.x,y:o.y,rotation:o.rotation};return UR(o)&&(s.shape=V({},o.shape)),s}var i=a(r);e.traverse(function(o){if(L0(o)&&o.anid){var s=i[o.anid];if(s){var l=n(o);o.attr(n(s)),Dt(o,l,t,nt(o).dataIndex)}}})}function Vg(r,e){return G(r,function(t){var a=t[0];a=Af(a,e.x),a=Cf(a,e.x+e.width);var n=t[1];return n=Af(n,e.y),n=Cf(n,e.y+e.height),[a,n]})}function jT(r,e){var t=Af(r.x,e.x),a=Cf(r.x+r.width,e.x+e.width),n=Af(r.y,e.y),i=Cf(r.y+r.height,e.y+e.height);if(a>=t&&i>=n)return{x:t,y:n,width:a-t,height:i-n}}function wo(r,e,t){var a=V({rectHover:!0},e),n=a.style={strokeNoScale:!0};if(t=t||{x:-1,y:-1,width:2,height:2},r)return r.indexOf("image://")===0?(n.image=r.slice(8),Q(n,t),new oe(a)):Dl(r.replace("path://",""),a,t,"center")}function gs(r,e,t,a,n){for(var i=0,o=n[n.length-1];i1)return!1;var g=Ev(c,p,f,h)/v;return!(g<0||g>1)}function Ev(r,e,t,a){return r*a-t*e}function YR(r){return r<=1e-6&&r>=-1e-6}function To(r){var e=r.itemTooltipOption,t=r.componentModel,a=r.itemName,n=U(e)?{formatter:e}:e,i=t.mainType,o=t.componentIndex,s={componentType:i,name:a,$vars:["name"]};s[i+"Index"]=o;var l=r.formatterParamsExtra;l&&C(St(l),function(f){$(s,f)||(s[f]=l[f],s.$vars.push(f))});var u=nt(r.el);u.componentMainType=i,u.componentIndex=o,u.tooltipConfig={name:a,option:Q({content:a,encodeHTMLContent:!0,formatterParams:s},n)}}function P0(r,e){var t;r.isGroup&&(t=e(r)),t||r.traverse(e)}function nn(r,e){if(r)if(z(r))for(var t=0;t=0&&s.push(l)}),s}}function on(r,e){return st(st({},r,!0),e,!0)}const aE={time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}},nE={time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}};var Mf="ZH",Gg="EN",to=Gg,tf={},Fg={},nA=_t.domSupported?function(){var r=(document.documentElement.lang||navigator.language||navigator.browserLanguage||to).toUpperCase();return r.indexOf(Mf)>-1?Mf:to}():to;function iA(r,e){r=r.toUpperCase(),Fg[r]=new Mt(e),tf[r]=e}function iE(r){if(U(r)){var e=tf[r.toUpperCase()]||{};return r===Mf||r===Gg?et(e):st(et(e),et(tf[to]),!1)}else return st(et(r),et(tf[to]),!1)}function td(r){return Fg[r]}function oE(){return Fg[to]}iA(Gg,aE);iA(Mf,nE);var Hg=1e3,Wg=Hg*60,Os=Wg*60,ir=Os*24,N0=ir*365,ys={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},ou="{yyyy}-{MM}-{dd}",B0={year:"{yyyy}",month:"{yyyy}-{MM}",day:ou,hour:ou+" "+ys.hour,minute:ou+" "+ys.minute,second:ou+" "+ys.second,millisecond:ys.none},Nv=["year","month","day","hour","minute","second","millisecond"],oA=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Ce(r,e){return r+="","0000".substr(0,e-r.length)+r}function eo(r){switch(r){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return r}}function sE(r){return r===eo(r)}function lE(r){switch(r){case"year":case"month":return"day";case"millisecond":return"millisecond";default:return"second"}}function Il(r,e,t,a){var n=$e(r),i=n[Ug(t)](),o=n[ro(t)]()+1,s=Math.floor((o-1)/3)+1,l=n[Lh(t)](),u=n["get"+(t?"UTC":"")+"Day"](),f=n[Js(t)](),h=(f-1)%12+1,v=n[Ph(t)](),c=n[Rh(t)](),p=n[Eh(t)](),d=f>=12?"pm":"am",g=d.toUpperCase(),y=a instanceof Mt?a:td(a||nA)||oE(),m=y.getModel("time"),_=m.get("month"),S=m.get("monthAbbr"),b=m.get("dayOfWeek"),x=m.get("dayOfWeekAbbr");return(e||"").replace(/{a}/g,d+"").replace(/{A}/g,g+"").replace(/{yyyy}/g,i+"").replace(/{yy}/g,Ce(i%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,_[o-1]).replace(/{MMM}/g,S[o-1]).replace(/{MM}/g,Ce(o,2)).replace(/{M}/g,o+"").replace(/{dd}/g,Ce(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,b[u]).replace(/{ee}/g,x[u]).replace(/{e}/g,u+"").replace(/{HH}/g,Ce(f,2)).replace(/{H}/g,f+"").replace(/{hh}/g,Ce(h+"",2)).replace(/{h}/g,h+"").replace(/{mm}/g,Ce(v,2)).replace(/{m}/g,v+"").replace(/{ss}/g,Ce(c,2)).replace(/{s}/g,c+"").replace(/{SSS}/g,Ce(p,3)).replace(/{S}/g,p+"")}function uE(r,e,t,a,n){var i=null;if(U(t))i=t;else if(K(t))i=t(r.value,e,{level:r.level});else{var o=V({},ys);if(r.level>0)for(var s=0;s=0;--s)if(l[u]){i=l[u];break}i=i||o.none}if(z(i)){var h=r.level==null?0:r.level>=0?r.level:i.length+r.level;h=Math.min(h,i.length-1),i=i[h]}}return Il(new Date(r.value),i,n,a)}function sA(r,e){var t=$e(r),a=t[ro(e)]()+1,n=t[Lh(e)](),i=t[Js(e)](),o=t[Ph(e)](),s=t[Rh(e)](),l=t[Eh(e)](),u=l===0,f=u&&s===0,h=f&&o===0,v=h&&i===0,c=v&&n===1,p=c&&a===1;return p?"year":c?"month":v?"day":h?"hour":f?"minute":u?"second":"millisecond"}function V0(r,e,t){var a=Ct(r)?$e(r):r;switch(e=e||sA(r,t),e){case"year":return a[Ug(t)]();case"half-year":return a[ro(t)]()>=6?1:0;case"quarter":return Math.floor((a[ro(t)]()+1)/4);case"month":return a[ro(t)]();case"day":return a[Lh(t)]();case"half-day":return a[Js(t)]()/24;case"hour":return a[Js(t)]();case"minute":return a[Ph(t)]();case"second":return a[Rh(t)]();case"millisecond":return a[Eh(t)]()}}function Ug(r){return r?"getUTCFullYear":"getFullYear"}function ro(r){return r?"getUTCMonth":"getMonth"}function Lh(r){return r?"getUTCDate":"getDate"}function Js(r){return r?"getUTCHours":"getHours"}function Ph(r){return r?"getUTCMinutes":"getMinutes"}function Rh(r){return r?"getUTCSeconds":"getSeconds"}function Eh(r){return r?"getUTCMilliseconds":"getMilliseconds"}function fE(r){return r?"setUTCFullYear":"setFullYear"}function lA(r){return r?"setUTCMonth":"setMonth"}function uA(r){return r?"setUTCDate":"setDate"}function fA(r){return r?"setUTCHours":"setHours"}function hA(r){return r?"setUTCMinutes":"setMinutes"}function vA(r){return r?"setUTCSeconds":"setSeconds"}function cA(r){return r?"setUTCMilliseconds":"setMilliseconds"}function hE(r,e,t,a,n,i,o,s){var l=new bt({style:{text:r,font:e,align:t,verticalAlign:a,padding:n,rich:i,overflow:o?"truncate":null,lineHeight:s}});return l.getBoundingRect()}function Yg(r){if(!bg(r))return U(r)?r:"-";var e=(r+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function Xg(r,e){return r=(r||"").toLowerCase().replace(/-(.)/g,function(t,a){return a.toUpperCase()}),e&&r&&(r=r.charAt(0).toUpperCase()+r.slice(1)),r}var pi=ch;function ed(r,e,t){var a="{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}";function n(f){return f&&or(f)?f:"-"}function i(f){return!!(f!=null&&!isNaN(f)&&isFinite(f))}var o=e==="time",s=r instanceof Date;if(o||s){var l=o?$e(r):r;if(isNaN(+l)){if(s)return"-"}else return Il(l,a,t)}if(e==="ordinal")return hf(r)?n(r):Ct(r)&&i(r)?r+"":"-";var u=Zr(r);return i(u)?Yg(u):hf(r)?n(r):typeof r=="boolean"?r+"":"-"}var z0=["a","b","c","d","e","f","g"],Bv=function(r,e){return"{"+r+(e??"")+"}"};function $g(r,e,t){z(e)||(e=[e]);var a=e.length;if(!a)return"";for(var n=e[0].$vars||[],i=0;i':'';var o=t.markerId||"markerX";return{renderMode:i,content:"{"+o+"|} ",style:n==="subItem"?{width:4,height:4,borderRadius:2,backgroundColor:a}:{width:10,height:10,borderRadius:5,backgroundColor:a}}}function cE(r,e,t){(r==="week"||r==="month"||r==="quarter"||r==="half-year"||r==="year")&&(r=`MM-dd +yyyy`);var a=$e(e),n=t?"getUTC":"get",i=a[n+"FullYear"](),o=a[n+"Month"]()+1,s=a[n+"Date"](),l=a[n+"Hours"](),u=a[n+"Minutes"](),f=a[n+"Seconds"](),h=a[n+"Milliseconds"]();return r=r.replace("MM",Ce(o,2)).replace("M",o).replace("yyyy",i).replace("yy",Ce(i%100+"",2)).replace("dd",Ce(s,2)).replace("d",s).replace("hh",Ce(l,2)).replace("h",l).replace("mm",Ce(u,2)).replace("m",u).replace("ss",Ce(f,2)).replace("s",f).replace("SSS",Ce(h,3)),r}function pE(r){return r&&r.charAt(0).toUpperCase()+r.substr(1)}function si(r,e){return e=e||"transparent",U(r)?r:tt(r)&&r.colorStops&&(r.colorStops[0]||{}).color||e}function If(r,e){if(e==="_blank"||e==="blank"){var t=window.open();t.opener=null,t.location.href=r}else window.open(r,e)}var ef=C,dA=["left","right","top","bottom","width","height"],Un=[["width","left","right"],["height","top","bottom"]];function Zg(r,e,t,a,n){var i=0,o=0;a==null&&(a=1/0),n==null&&(n=1/0);var s=0;e.eachChild(function(l,u){var f=l.getBoundingRect(),h=e.childAt(u+1),v=h&&h.getBoundingRect(),c,p;if(r==="horizontal"){var d=f.width+(v?-v.x+f.x:0);c=i+d,c>a||l.newline?(i=0,c=d,o+=s+t,s=f.height):s=Math.max(s,f.height)}else{var g=f.height+(v?-v.y+f.y:0);p=o+g,p>n||l.newline?(i+=s+t,o=0,p=g,s=f.width):s=Math.max(s,f.width)}l.newline||(l.x=i,l.y=o,l.markRedraw(),r==="horizontal"?i=c+t:o=p+t)})}var Qn=Zg;it(Zg,"vertical");it(Zg,"horizontal");function dE(r,e,t){var a=e.width,n=e.height,i=W(r.left,a),o=W(r.top,n),s=W(r.right,a),l=W(r.bottom,n);return(isNaN(i)||isNaN(parseFloat(r.left)))&&(i=0),(isNaN(s)||isNaN(parseFloat(r.right)))&&(s=a),(isNaN(o)||isNaN(parseFloat(r.top)))&&(o=0),(isNaN(l)||isNaN(parseFloat(r.bottom)))&&(l=n),t=pi(t||0),{width:Math.max(s-i-t[1]-t[3],0),height:Math.max(l-o-t[0]-t[2],0)}}function jt(r,e,t){t=pi(t||0);var a=e.width,n=e.height,i=W(r.left,a),o=W(r.top,n),s=W(r.right,a),l=W(r.bottom,n),u=W(r.width,a),f=W(r.height,n),h=t[2]+t[0],v=t[1]+t[3],c=r.aspect;switch(isNaN(u)&&(u=a-s-v-i),isNaN(f)&&(f=n-l-h-o),c!=null&&(isNaN(u)&&isNaN(f)&&(c>a/n?u=a*.8:f=n*.8),isNaN(u)&&(u=c*f),isNaN(f)&&(f=u/c)),isNaN(i)&&(i=a-s-u-v),isNaN(o)&&(o=n-l-f-h),r.left||r.right){case"center":i=a/2-u/2-t[3];break;case"right":i=a-u-v;break}switch(r.top||r.bottom){case"middle":case"center":o=n/2-f/2-t[0];break;case"bottom":o=n-f-h;break}i=i||0,o=o||0,isNaN(u)&&(u=a-v-i-(s||0)),isNaN(f)&&(f=n-h-o-(l||0));var p=new ht(i+t[3],o+t[0],u,f);return p.margin=t,p}function kh(r,e,t,a,n,i){var o=!n||!n.hv||n.hv[0],s=!n||!n.hv||n.hv[1],l=n&&n.boundingMode||"all";if(i=i||r,i.x=r.x,i.y=r.y,!o&&!s)return!1;var u;if(l==="raw")u=r.type==="group"?new ht(0,0,+e.width||0,+e.height||0):r.getBoundingRect();else if(u=r.getBoundingRect(),r.needLocalTransform()){var f=r.getLocalTransform();u=u.clone(),u.applyTransform(f)}var h=jt(Q({width:u.width,height:u.height},e),t,a),v=o?h.x-u.x:0,c=s?h.y-u.y:0;return l==="raw"?(i.x=v,i.y=c):(i.x+=v,i.y+=c),i===r&&r.markRedraw(),!0}function gE(r,e){return r[Un[e][0]]!=null||r[Un[e][1]]!=null&&r[Un[e][2]]!=null}function tl(r){var e=r.layoutMode||r.constructor.layoutMode;return tt(e)?e:e?{type:e}:null}function Ja(r,e,t){var a=t&&t.ignoreSize;!z(a)&&(a=[a,a]);var n=o(Un[0],0),i=o(Un[1],1);u(Un[0],r,n),u(Un[1],r,i);function o(f,h){var v={},c=0,p={},d=0,g=2;if(ef(f,function(_){p[_]=r[_]}),ef(f,function(_){s(e,_)&&(v[_]=p[_]=e[_]),l(v,_)&&c++,l(p,_)&&d++}),a[h])return l(e,f[1])?p[f[2]]=null:l(e,f[2])&&(p[f[1]]=null),p;if(d===g||!c)return p;if(c>=g)return v;for(var y=0;y=0;l--)s=st(s,n[l],!0);a.defaultOption=s}return a.defaultOption},e.prototype.getReferringComponents=function(t,a){var n=t+"Index",i=t+"Id";return wl(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},a)},e.prototype.getBoxLayoutParams=function(){var t=this;return{left:t.get("left"),top:t.get("top"),right:t.get("right"),bottom:t.get("bottom"),width:t.get("width"),height:t.get("height")}},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=function(){var t=e.prototype;t.type="component",t.id="",t.name="",t.mainType="",t.subType="",t.componentIndex=0}(),e}(Mt);mT(mt,Mt);Sh(mt);eE(mt);rE(mt,mE);function mE(r){var e=[];return C(mt.getClassesByMainType(r),function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])}),e=G(e,function(t){return Fr(t).main}),r!=="dataset"&&vt(e,"dataset")<=0&&e.unshift("dataset"),e}var yA="";typeof navigator<"u"&&(yA=navigator.platform||"");var Di="rgba(0, 0, 0, 0.2)";const _E={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Di,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Di,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Di,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Di,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Di,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Di,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:yA.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1};var mA=Z(["tooltip","label","itemName","itemId","itemGroupId","itemChildGroupId","seriesName"]),vr="original",xe="arrayRows",cr="objectRows",jr="keyedColumns",$a="typedArray",_A="unknown",Xr="column",Mo="row",ue={Must:1,Might:2,Not:3},SA=Tt();function SE(r){SA(r).datasetMap=Z()}function xA(r,e,t){var a={},n=Kg(e);if(!n||!r)return a;var i=[],o=[],s=e.ecModel,l=SA(s).datasetMap,u=n.uid+"_"+t.seriesLayoutBy,f,h;r=r.slice(),C(r,function(d,g){var y=tt(d)?d:r[g]={name:d};y.type==="ordinal"&&f==null&&(f=g,h=p(y)),a[y.name]=[]});var v=l.get(u)||l.set(u,{categoryWayDim:h,valueWayDim:0});C(r,function(d,g){var y=d.name,m=p(d);if(f==null){var _=v.valueWayDim;c(a[y],_,m),c(o,_,m),v.valueWayDim+=m}else if(f===g)c(a[y],0,m),c(i,0,m);else{var _=v.categoryWayDim;c(a[y],_,m),c(o,_,m),v.categoryWayDim+=m}});function c(d,g,y){for(var m=0;me)return r[a];return r[t-1]}function TA(r,e,t,a,n,i,o){i=i||r;var s=e(i),l=s.paletteIdx||0,u=s.paletteNameMap=s.paletteNameMap||{};if(u.hasOwnProperty(n))return u[n];var f=o==null||!a?t:AE(a,o);if(f=f||t,!(!f||!f.length)){var h=f[l];return n&&(u[n]=h),s.paletteIdx=(l+1)%f.length,h}}function CE(r,e){e(r).paletteIdx=0,e(r).paletteNameMap={}}var su,Yo,F0,H0="\0_ec_inner",DE=1,Qg=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.init=function(t,a,n,i,o,s){i=i||{},this.option=null,this._theme=new Mt(i),this._locale=new Mt(o),this._optionManager=s},e.prototype.setOption=function(t,a,n){var i=Y0(a);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,a){return this._resetOption(t,Y0(a))},e.prototype._resetOption=function(t,a){var n=!1,i=this._optionManager;if(!t||t==="recreate"){var o=i.mountOption(t==="recreate");!this.option||t==="recreate"?F0(this,o):(this.restoreData(),this._mergeOption(o,a)),n=!0}if((t==="timeline"||t==="media")&&this.restoreData(),!t||t==="recreate"||t==="timeline"){var s=i.getTimelineOption(this);s&&(n=!0,this._mergeOption(s,a))}if(!t||t==="recreate"||t==="media"){var l=i.getMediaOption(this);l.length&&C(l,function(u){n=!0,this._mergeOption(u,a)},this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,a){var n=this.option,i=this._componentsMap,o=this._componentsCount,s=[],l=Z(),u=a&&a.replaceMergeMainTypeMap;SE(this),C(t,function(h,v){h!=null&&(mt.hasClass(v)?v&&(s.push(v),l.set(v,!0)):n[v]=n[v]==null?et(h):st(n[v],h,!0))}),u&&u.each(function(h,v){mt.hasClass(v)&&!l.get(v)&&(s.push(v),l.set(v,!0))}),mt.topologicalTravel(s,mt.getAllClassMainTypes(),f,this);function f(h){var v=wE(this,h,Et(t[h])),c=i.get(h),p=c?u&&u.get(h)?"replaceMerge":"normalMerge":"replaceAll",d=cT(c,v,p);eP(d,h,mt),n[h]=null,i.set(h,null),o.set(h,0);var g=[],y=[],m=0,_;C(d,function(S,b){var x=S.existing,w=S.newOption;if(!w)x&&(x.mergeOption({},this),x.optionUpdated({},!1));else{var T=h==="series",A=mt.getClass(h,S.keyInfo.subType,!T);if(!A)return;if(h==="tooltip"){if(_)return;_=!0}if(x&&x.constructor===A)x.name=S.keyInfo.name,x.mergeOption(w,this),x.optionUpdated(w,!1);else{var D=V({componentIndex:b},S.keyInfo);x=new A(w,this,this,D),V(x,D),S.brandNew&&(x.__requireNewView=!0),x.init(w,this,this),x.optionUpdated(null,!0)}}x?(g.push(x.option),y.push(x),m++):(g.push(void 0),y.push(void 0))},this),n[h]=g,i.set(h,y),o.set(h,m),h==="series"&&su(this)}this._seriesIndices||su(this)},e.prototype.getOption=function(){var t=et(this.option);return C(t,function(a,n){if(mt.hasClass(n)){for(var i=Et(a),o=i.length,s=!1,l=o-1;l>=0;l--)i[l]&&!qs(i[l])?s=!0:(i[l]=null,!s&&o--);i.length=o,t[n]=i}}),delete t[H0],t},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,a){var n=this._componentsMap.get(t);if(n){var i=n[a||0];if(i)return i;if(a==null){for(var o=0;o=e:t==="max"?r<=e:r===e}function NE(r,e){return r.join(",")===e.join(",")}var pr=C,el=tt,X0=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function zv(r){var e=r&&r.itemStyle;if(e)for(var t=0,a=X0.length;t=0;g--){var y=r[g];if(s||(p=y.data.rawIndexOf(y.stackedByDimension,c)),p>=0){var m=y.data.getByRawIndex(y.stackResultDimension,p);if(l==="all"||l==="positive"&&m>0||l==="negative"&&m<0||l==="samesign"&&v>=0&&m>0||l==="samesign"&&v<=0&&m<0){v=Y2(v,m),d=m;break}}}return a[0]=v,a[1]=d,a})})}var Oh=function(){function r(e){this.data=e.data||(e.sourceFormat===jr?{}:[]),this.sourceFormat=e.sourceFormat||_A,this.seriesLayoutBy=e.seriesLayoutBy||Xr,this.startIndex=e.startIndex||0,this.dimensionsDetectedCount=e.dimensionsDetectedCount,this.metaRawOption=e.metaRawOption;var t=this.dimensionsDefine=e.dimensionsDefine;if(t)for(var a=0;ad&&(d=_)}c[0]=p,c[1]=d}},n=function(){return this._data?this._data.length/this._dimSize:0};J0=(e={},e[xe+"_"+Xr]={pure:!0,appendData:i},e[xe+"_"+Mo]={pure:!0,appendData:function(){throw new Error('Do not support appendData when set seriesLayoutBy: "row".')}},e[cr]={pure:!0,appendData:i},e[jr]={pure:!0,appendData:function(o){var s=this._data;C(o,function(l,u){for(var f=s[u]||(s[u]=[]),h=0;h<(l||[]).length;h++)f.push(l[h])})}},e[vr]={appendData:i},e[$a]={persistent:!1,pure:!0,appendData:function(o){this._data=o},clean:function(){this._offset+=this.count(),this._data=null}},e);function i(o){for(var s=0;s=0&&(d=o.interpolatedValue[g])}return d!=null?d+"":""})}},r.prototype.getRawValue=function(e,t){return lo(this.getData(t),e)},r.prototype.formatTooltip=function(e,t,a){},r}();function a_(r){var e,t;return tt(r)?r.type&&(t=r):e=r,{text:e,frag:t}}function Ns(r){return new JE(r)}var JE=function(){function r(e){e=e||{},this._reset=e.reset,this._plan=e.plan,this._count=e.count,this._onDirty=e.onDirty,this._dirty=!0}return r.prototype.perform=function(e){var t=this._upstream,a=e&&e.skip;if(this._dirty&&t){var n=this.context;n.data=n.outputData=t.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this);var i;this._plan&&!a&&(i=this._plan(this.context));var o=f(this._modBy),s=this._modDataCount||0,l=f(e&&e.modBy),u=e&&e.modDataCount||0;(o!==l||s!==u)&&(i="reset");function f(m){return!(m>=1)&&(m=1),m}var h;(this._dirty||i==="reset")&&(this._dirty=!1,h=this._doReset(a)),this._modBy=l,this._modDataCount=u;var v=e&&e.step;if(t?this._dueEnd=t._outputDueEnd:this._dueEnd=this._count?this._count(this.context):1/0,this._progress){var c=this._dueIndex,p=Math.min(v!=null?this._dueIndex+v:1/0,this._dueEnd);if(!a&&(h||c1&&a>0?s:o}};return i;function o(){return e=r?null:le},gte:function(r,e){return r>=e}},ek=function(){function r(e,t){if(!Ct(t)){var a="";It(a)}this._opFn=OA[e],this._rvalFloat=Zr(t)}return r.prototype.evaluate=function(e){return Ct(e)?this._opFn(e,this._rvalFloat):this._opFn(Zr(e),this._rvalFloat)},r}(),NA=function(){function r(e,t){var a=e==="desc";this._resultLT=a?1:-1,t==null&&(t=a?"min":"max"),this._incomparable=t==="min"?-1/0:1/0}return r.prototype.evaluate=function(e,t){var a=Ct(e)?e:Zr(e),n=Ct(t)?t:Zr(t),i=isNaN(a),o=isNaN(n);if(i&&(a=this._incomparable),o&&(n=this._incomparable),i&&o){var s=U(e),l=U(t);s&&(a=l?e:0),l&&(n=s?t:0)}return an?-this._resultLT:0},r}(),rk=function(){function r(e,t){this._rval=t,this._isEQ=e,this._rvalTypeof=typeof t,this._rvalFloat=Zr(t)}return r.prototype.evaluate=function(e){var t=e===this._rval;if(!t){var a=typeof e;a!==this._rvalTypeof&&(a==="number"||this._rvalTypeof==="number")&&(t=Zr(e)===this._rvalFloat)}return this._isEQ?t:!t},r}();function ak(r,e){return r==="eq"||r==="ne"?new rk(r==="eq",e):$(OA,r)?new ek(r,e):null}var nk=function(){function r(){}return r.prototype.getRawData=function(){throw new Error("not supported")},r.prototype.getRawDataItem=function(e){throw new Error("not supported")},r.prototype.cloneRawData=function(){},r.prototype.getDimensionInfo=function(e){},r.prototype.cloneAllDimensionInfo=function(){},r.prototype.count=function(){},r.prototype.retrieveValue=function(e,t){},r.prototype.retrieveValueFromItem=function(e,t){},r.prototype.convertValue=function(e,t){return Za(e,t)},r}();function ik(r,e){var t=new nk,a=r.data,n=t.sourceFormat=r.sourceFormat,i=r.startIndex,o="";r.seriesLayoutBy!==Xr&&It(o);var s=[],l={},u=r.dimensionsDefine;if(u)C(u,function(d,g){var y=d.name,m={index:g,name:y,displayName:d.displayName};if(s.push(m),y!=null){var _="";$(l,y)&&It(_),l[y]=m}});else for(var f=0;f65535?ck:pk}function Mi(){return[1/0,-1/0]}function dk(r){var e=r.constructor;return e===Array?r.slice():new e(r)}function o_(r,e,t,a,n){var i=zA[t||"float"];if(n){var o=r[e],s=o&&o.length;if(s!==a){for(var l=new i(a),u=0;ug[1]&&(g[1]=d)}return this._rawCount=this._count=l,{start:s,end:l}},r.prototype._initDataFromProvider=function(e,t,a){for(var n=this._provider,i=this._chunks,o=this._dimensions,s=o.length,l=this._rawExtent,u=G(o,function(m){return m.property}),f=0;fy[1]&&(y[1]=g)}}!n.persistent&&n.clean&&n.clean(),this._rawCount=this._count=t,this._extent=[]},r.prototype.count=function(){return this._count},r.prototype.get=function(e,t){if(!(t>=0&&t=0&&t=this._rawCount||e<0)return-1;if(!this._indices)return e;var t=this._indices,a=t[e];if(a!=null&&ae)i=o-1;else return o}return-1},r.prototype.indicesOfNearest=function(e,t,a){var n=this._chunks,i=n[e],o=[];if(!i)return o;a==null&&(a=1/0);for(var s=1/0,l=-1,u=0,f=0,h=this.count();f=0&&l<0)&&(s=p,l=c,u=0),c===l&&(o[u++]=f))}return o.length=u,o},r.prototype.getIndices=function(){var e,t=this._indices;if(t){var a=t.constructor,n=this._count;if(a===Array){e=new a(n);for(var i=0;i=h&&m<=v||isNaN(m))&&(l[u++]=d),d++}p=!0}else if(i===2){for(var g=c[n[0]],_=c[n[1]],S=e[n[1]][0],b=e[n[1]][1],y=0;y=h&&m<=v||isNaN(m))&&(x>=S&&x<=b||isNaN(x))&&(l[u++]=d),d++}p=!0}}if(!p)if(i===1)for(var y=0;y=h&&m<=v||isNaN(m))&&(l[u++]=w)}else for(var y=0;ye[D][1])&&(T=!1)}T&&(l[u++]=t.getRawIndex(y))}return uy[1]&&(y[1]=g)}}}},r.prototype.lttbDownSample=function(e,t){var a=this.clone([e],!0),n=a._chunks,i=n[e],o=this.count(),s=0,l=Math.floor(1/t),u=this.getRawIndex(0),f,h,v,c=new($o(this._rawCount))(Math.min((Math.ceil(o/l)+2)*2,o));c[s++]=u;for(var p=1;pf&&(f=h,v=S)}M>0&&Mf-p&&(l=f-p,s.length=l);for(var d=0;dh[1]&&(h[1]=y),v[c++]=m}return i._count=c,i._indices=v,i._updateGetRawIdx(),i},r.prototype.each=function(e,t){if(this._count)for(var a=e.length,n=this._chunks,i=0,o=this.count();il&&(l=h)}return o=[s,l],this._extent[e]=o,o},r.prototype.getRawDataItem=function(e){var t=this.getRawIndex(e);if(this._provider.persistent)return this._provider.getItem(t);for(var a=[],n=this._chunks,i=0;i=0?this._indices[e]:-1},r.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},r.internalField=function(){function e(t,a,n,i){return Za(t[i],this._dimensions[i])}Hv={arrayRows:e,objectRows:function(t,a,n,i){return Za(t[a],this._dimensions[i])},keyedColumns:e,original:function(t,a,n,i){var o=t&&(t.value==null?t:t.value);return Za(o instanceof Array?o[i]:o,this._dimensions[i])},typedArray:function(t,a,n,i){return t[i]}}}(),r}(),GA=function(){function r(e){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=e}return r.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},r.prototype._setLocalSource=function(e,t){this._sourceList=e,this._upstreamSignList=t,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},r.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},r.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},r.prototype._createSource=function(){this._setLocalSource([],[]);var e=this._sourceHost,t=this._getUpstreamSourceManagers(),a=!!t.length,n,i;if(lu(e)){var o=e,s=void 0,l=void 0,u=void 0;if(a){var f=t[0];f.prepareSource(),u=f.getSource(),s=u.data,l=u.sourceFormat,i=[f._getVersionSign()]}else s=o.get("data",!0),l=Re(s)?$a:vr,i=[];var h=this._getSourceMetaRawOption()||{},v=u&&u.metaRawOption||{},c=ot(h.seriesLayoutBy,v.seriesLayoutBy)||null,p=ot(h.sourceHeader,v.sourceHeader),d=ot(h.dimensions,v.dimensions),g=c!==v.seriesLayoutBy||!!p!=!!v.sourceHeader||d;n=g?[nd(s,{seriesLayoutBy:c,sourceHeader:p,dimensions:d},l)]:[]}else{var y=e;if(a){var m=this._applyTransform(t);n=m.sourceList,i=m.upstreamSignList}else{var _=y.get("source",!0);n=[nd(_,this._getSourceMetaRawOption(),null)],i=[]}}this._setLocalSource(n,i)},r.prototype._applyTransform=function(e){var t=this._sourceHost,a=t.get("transform",!0),n=t.get("fromTransformResult",!0);if(n!=null){var i="";e.length!==1&&l_(i)}var o,s=[],l=[];return C(e,function(u){u.prepareSource();var f=u.getSource(n||0),h="";n!=null&&!f&&l_(h),s.push(f),l.push(u._getVersionSign())}),a?o=hk(a,s,{datasetIndex:t.componentIndex}):n!=null&&(o=[XE(s[0])]),{sourceList:o,upstreamSignList:l}},r.prototype._isDirty=function(){if(this._dirty)return!0;for(var e=this._getUpstreamSourceManagers(),t=0;t1||t>0&&!r.noHeader;return C(r.blocks,function(n){var i=UA(n);i>=e&&(e=i+ +(a&&(!i||od(n)&&!n.noHeader)))}),e}return 0}function mk(r,e,t,a){var n=e.noHeader,i=Sk(UA(e)),o=[],s=e.blocks||[];me(!s||z(s)),s=s||[];var l=r.orderMode;if(e.sortBlocks&&l){s=s.slice();var u={valueAsc:"asc",valueDesc:"desc"};if($(u,l)){var f=new NA(u[l],null);s.sort(function(p,d){return f.evaluate(p.sortParam,d.sortParam)})}else l==="seriesDesc"&&s.reverse()}C(s,function(p,d){var g=e.valueFormatter,y=WA(p)(g?V(V({},r),{valueFormatter:g}):r,p,d>0?i.html:0,a);y!=null&&o.push(y)});var h=r.renderMode==="richText"?o.join(i.richText):sd(o.join(""),n?t:i.html);if(n)return h;var v=ed(e.header,"ordinal",r.useUTC),c=HA(a,r.renderMode).nameStyle;return r.renderMode==="richText"?YA(r,v,c)+i.richText+h:sd('
'+Me(v)+"
"+h,t)}function _k(r,e,t,a){var n=r.renderMode,i=e.noName,o=e.noValue,s=!e.markerType,l=e.name,u=r.useUTC,f=e.valueFormatter||r.valueFormatter||function(S){return S=z(S)?S:[S],G(S,function(b,x){return ed(b,z(c)?c[x]:c,u)})};if(!(i&&o)){var h=s?"":r.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||"#333",n),v=i?"":ed(l,"ordinal",u),c=e.valueType,p=o?[]:f(e.value,e.dataIndex),d=!s||!i,g=!s&&i,y=HA(a,n),m=y.nameStyle,_=y.valueStyle;return n==="richText"?(s?"":h)+(i?"":YA(r,v,m))+(o?"":wk(r,p,d,g,_)):sd((s?"":h)+(i?"":xk(v,!s,m))+(o?"":bk(p,d,g,_)),t)}}function u_(r,e,t,a,n,i){if(r){var o=WA(r),s={useUTC:n,renderMode:t,orderMode:a,markupStyleCreator:e,valueFormatter:r.valueFormatter};return o(s,r,0,i)}}function Sk(r){return{html:gk[r],richText:yk[r]}}function sd(r,e){var t='
',a="margin: "+e+"px 0 0";return'
'+r+t+"
"}function xk(r,e,t){var a=e?"margin-left:2px":"";return''+Me(r)+""}function bk(r,e,t,a){var n=t?"10px":"20px",i=e?"float:right;margin-left:"+n:"";return r=z(r)?r:[r],''+G(r,function(o){return Me(o)}).join("  ")+""}function YA(r,e,t){return r.markupStyleCreator.wrapRichTextStyle(e,t)}function wk(r,e,t,a,n){var i=[n],o=a?10:20;return t&&i.push({padding:[0,0,0,o],align:"right"}),r.markupStyleCreator.wrapRichTextStyle(z(e)?e.join(" "):e,i)}function XA(r,e){var t=r.getData().getItemVisual(e,"style"),a=t[r.visualDrawType];return si(a)}function $A(r,e){var t=r.get("padding");return t??(e==="richText"?[8,10]:10)}var Wv=function(){function r(){this.richTextStyles={},this._nextStyleNameId=uT()}return r.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},r.prototype.makeTooltipMarker=function(e,t,a){var n=a==="richText"?this._generateStyleName():null,i=pA({color:t,type:e,renderMode:a,markerId:n});return U(i)?i:(this.richTextStyles[n]=i.style,i.content)},r.prototype.wrapRichTextStyle=function(e,t){var a={};z(t)?C(t,function(i){return V(a,i)}):V(a,t);var n=this._generateStyleName();return this.richTextStyles[n]=a,"{"+n+"|"+e+"}"},r}();function ZA(r){var e=r.series,t=r.dataIndex,a=r.multipleSeries,n=e.getData(),i=n.mapDimensionsAll("defaultedTooltip"),o=i.length,s=e.getRawValue(t),l=z(s),u=XA(e,t),f,h,v,c;if(o>1||l&&!o){var p=Tk(s,e,t,i,u);f=p.inlineValues,h=p.inlineValueTypes,v=p.blocks,c=p.inlineValues[0]}else if(o){var d=n.getDimensionInfo(i[0]);c=f=lo(n,t,i[0]),h=d.type}else c=f=l?s[0]:s;var g=wg(e),y=g&&e.name||"",m=n.getName(t),_=a?y:m;return ie("section",{header:y,noHeader:a||!g,sortParam:c,blocks:[ie("nameValue",{markerType:"item",markerColor:u,name:_,noName:!or(_),value:f,valueType:h,dataIndex:t})].concat(v||[])})}function Tk(r,e,t,a,n){var i=e.getData(),o=lr(r,function(h,v,c){var p=i.getDimensionInfo(c);return h=h||p&&p.tooltip!==!1&&p.displayName!=null},!1),s=[],l=[],u=[];a.length?C(a,function(h){f(lo(i,t,h),h)}):C(r,f);function f(h,v){var c=i.getDimensionInfo(v);!c||c.otherDims.tooltip===!1||(o?u.push(ie("nameValue",{markerType:"subItem",markerColor:n,name:c.displayName,value:h,valueType:c.type})):(s.push(h),l.push(c.type)))}return{inlineValues:s,inlineValueTypes:l,blocks:u}}var Ca=Tt();function uu(r,e){return r.getName(e)||r.getId(e)}var rf="__universalTransitionEnabled",kt=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t._selectedDataIndicesMap={},t}return e.prototype.init=function(t,a,n){this.seriesIndex=this.componentIndex,this.dataTask=Ns({count:Ck,reset:Dk}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n);var i=Ca(this).sourceManager=new GA(this);i.prepareSource();var o=this.getInitialData(t,n);h_(o,this),this.dataTask.context.data=o,Ca(this).dataBeforeProcessed=o,f_(this),this._initSelectedMapFromData(o)},e.prototype.mergeDefaultAndTheme=function(t,a){var n=tl(this),i=n?Do(t):{},o=this.subType;mt.hasClass(o)&&(o+="Series"),st(t,a.getTheme().get(this.subType)),st(t,this.getDefaultOption()),ai(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&Ja(t,i,n)},e.prototype.mergeOption=function(t,a){t=st(this.option,t,!0),this.fillDataTextStyle(t.data);var n=tl(this);n&&Ja(this.option,t,n);var i=Ca(this).sourceManager;i.dirty(),i.prepareSource();var o=this.getInitialData(t,a);h_(o,this),this.dataTask.dirty(),this.dataTask.context.data=o,Ca(this).dataBeforeProcessed=o,f_(this),this._initSelectedMapFromData(o)},e.prototype.fillDataTextStyle=function(t){if(t&&!Re(t))for(var a=["show"],n=0;nthis.getShallow("animationThreshold")&&(a=!1),!!a},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,a,n){var i=this.ecModel,o=jg.prototype.getColorFromPalette.call(this,t,a,n);return o||(o=i.getColorFromPalette(t,a,n)),o},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,a){this._innerSelect(this.getData(a),t)},e.prototype.unselect=function(t,a){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,o=this.getData(a);if(i==="series"||n==="all"){this.option.selectedMap={},this._selectedDataIndicesMap={};return}for(var s=0;s=0&&n.push(o)}return n},e.prototype.isSelected=function(t,a){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(a);return(n==="all"||n[uu(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this[rf])return!0;var t=this.option.universalTransition;return t?t===!0?!0:t&&t.enabled:!1},e.prototype._innerSelect=function(t,a){var n,i,o=this.option,s=o.selectedMode,l=a.length;if(!(!s||!l)){if(s==="series")o.selectedMap="all";else if(s==="multiple"){tt(o.selectedMap)||(o.selectedMap={});for(var u=o.selectedMap,f=0;f0&&this._innerSelect(t,a)}},e.registerClass=function(t){return mt.registerClass(t)},e.protoInitialize=function(){var t=e.prototype;t.type="series.__base__",t.seriesIndex=0,t.ignoreStyleOnData=!1,t.hasSymbolVisual=!1,t.defaultSymbol="circle",t.visualStyleAccessPath="itemStyle",t.visualDrawType="fill"}(),e}(mt);Xt(kt,Nh);Xt(kt,jg);mT(kt,mt);function f_(r){var e=r.name;wg(r)||(r.name=Ak(r)||e)}function Ak(r){var e=r.getRawData(),t=e.mapDimensionsAll("seriesName"),a=[];return C(t,function(n){var i=e.getDimensionInfo(n);i.displayName&&a.push(i.displayName)}),a.join(" ")}function Ck(r){return r.model.getRawData().count()}function Dk(r){var e=r.model;return e.setData(e.getRawData().cloneShallow()),Mk}function Mk(r,e){e.outputData&&r.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function h_(r,e){C(Hs(r.CHANGABLE_METHODS,r.DOWNSAMPLE_METHODS),function(t){r.wrapMethod(t,it(Ik,e))})}function Ik(r,e){var t=ld(r);return t&&t.setOutputEnd((e||this).count()),e}function ld(r){var e=(r.ecModel||{}).scheduler,t=e&&e.getPipeline(r.uid);if(t){var a=t.currentTask;if(a){var n=a.agentStubMap;n&&(a=n.get(r.uid))}return a}}var zt=function(){function r(){this.group=new at,this.uid=Co("viewComponent")}return r.prototype.init=function(e,t){},r.prototype.render=function(e,t,a,n){},r.prototype.dispose=function(e,t){},r.prototype.updateView=function(e,t,a,n){},r.prototype.updateLayout=function(e,t,a,n){},r.prototype.updateVisual=function(e,t,a,n){},r.prototype.toggleBlurSeries=function(e,t,a){},r.prototype.eachRendered=function(e){var t=this.group;t&&t.traverse(e)},r}();Ag(zt);Sh(zt);function Lo(){var r=Tt();return function(e){var t=r(e),a=e.pipelineContext,n=!!t.large,i=!!t.progressiveRender,o=t.large=!!(a&&a.large),s=t.progressiveRender=!!(a&&a.progressiveRender);return(n!==o||i!==s)&&"reset"}}var qA=Tt(),Lk=Lo(),Rt=function(){function r(){this.group=new at,this.uid=Co("viewChart"),this.renderTask=Ns({plan:Pk,reset:Rk}),this.renderTask.context={view:this}}return r.prototype.init=function(e,t){},r.prototype.render=function(e,t,a,n){},r.prototype.highlight=function(e,t,a,n){var i=e.getData(n&&n.dataType);i&&c_(i,n,"emphasis")},r.prototype.downplay=function(e,t,a,n){var i=e.getData(n&&n.dataType);i&&c_(i,n,"normal")},r.prototype.remove=function(e,t){this.group.removeAll()},r.prototype.dispose=function(e,t){},r.prototype.updateView=function(e,t,a,n){this.render(e,t,a,n)},r.prototype.updateLayout=function(e,t,a,n){this.render(e,t,a,n)},r.prototype.updateVisual=function(e,t,a,n){this.render(e,t,a,n)},r.prototype.eachRendered=function(e){nn(this.group,e)},r.markUpdateMethod=function(e,t){qA(e).updateMethod=t},r.protoInitialize=function(){var e=r.prototype;e.type="chart"}(),r}();function v_(r,e,t){r&&js(r)&&(e==="emphasis"?da:ga)(r,t)}function c_(r,e,t){var a=ni(r,e),n=e&&e.highlightKey!=null?gR(e.highlightKey):null;a!=null?C(Et(a),function(i){v_(r.getItemGraphicEl(i),t,n)}):r.eachItemGraphicEl(function(i){v_(i,t,n)})}Ag(Rt);Sh(Rt);function Pk(r){return Lk(r.model)}function Rk(r){var e=r.model,t=r.ecModel,a=r.api,n=r.payload,i=e.pipelineContext.progressiveRender,o=r.view,s=n&&qA(n).updateMethod,l=i?"incrementalPrepareRender":s&&o[s]?s:"render";return l!=="render"&&o[l](e,t,a,n),Ek[l]}var Ek={incrementalPrepareRender:{progress:function(r,e){e.view.incrementalRender(r,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(r,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},Lf="\0__throttleOriginMethod",p_="\0__throttleRate",d_="\0__throttleType";function ay(r,e,t){var a,n=0,i=0,o=null,s,l,u,f;e=e||0;function h(){i=new Date().getTime(),o=null,r.apply(l,u||[])}var v=function(){for(var c=[],p=0;p=0?h():o=setTimeout(h,-s),n=a};return v.clear=function(){o&&(clearTimeout(o),o=null)},v.debounceNextCall=function(c){f=c},v}function Po(r,e,t,a){var n=r[e];if(n){var i=n[Lf]||n,o=n[d_],s=n[p_];if(s!==t||o!==a){if(t==null||!a)return r[e]=i;n=r[e]=ay(i,t,a==="debounce"),n[Lf]=i,n[d_]=a,n[p_]=t}return n}}function rl(r,e){var t=r[e];t&&t[Lf]&&(t.clear&&t.clear(),r[e]=t[Lf])}var g_=Tt(),y_={itemStyle:ii(aA,!0),lineStyle:ii(rA,!0)},kk={lineStyle:"stroke",itemStyle:"fill"};function KA(r,e){var t=r.visualStyleMapper||y_[e];return t||(console.warn("Unknown style type '"+e+"'."),y_.itemStyle)}function jA(r,e){var t=r.visualDrawType||kk[e];return t||(console.warn("Unknown style type '"+e+"'."),"fill")}var Ok={createOnAllSeries:!0,performRawSeries:!0,reset:function(r,e){var t=r.getData(),a=r.visualStyleAccessPath||"itemStyle",n=r.getModel(a),i=KA(r,a),o=i(n),s=n.getShallow("decal");s&&(t.setVisual("decal",s),s.dirty=!0);var l=jA(r,a),u=o[l],f=K(u)?u:null,h=o.fill==="auto"||o.stroke==="auto";if(!o[l]||f||h){var v=r.getColorFromPalette(r.name,null,e.getSeriesCount());o[l]||(o[l]=v,t.setVisual("colorFromPalette",!0)),o.fill=o.fill==="auto"||K(o.fill)?v:o.fill,o.stroke=o.stroke==="auto"||K(o.stroke)?v:o.stroke}if(t.setVisual("style",o),t.setVisual("drawType",l),!e.isSeriesFiltered(r)&&f)return t.setVisual("colorFromPalette",!1),{dataEach:function(c,p){var d=r.getDataParams(p),g=V({},o);g[l]=f(d),c.setItemVisual(p,"style",g)}}}},Zo=new Mt,Nk={createOnAllSeries:!0,performRawSeries:!0,reset:function(r,e){if(!(r.ignoreStyleOnData||e.isSeriesFiltered(r))){var t=r.getData(),a=r.visualStyleAccessPath||"itemStyle",n=KA(r,a),i=t.getVisual("drawType");return{dataEach:t.hasItemOption?function(o,s){var l=o.getRawDataItem(s);if(l&&l[a]){Zo.option=l[a];var u=n(Zo),f=o.ensureUniqueItemVisual(s,"style");V(f,u),Zo.option.decal&&(o.setItemVisual(s,"decal",Zo.option.decal),Zo.option.decal.dirty=!0),i in u&&o.setItemVisual(s,"colorFromPalette",!1)}}:null}}}},Bk={performRawSeries:!0,overallReset:function(r){var e=Z();r.eachSeries(function(t){var a=t.getColorBy();if(!t.isColorBySeries()){var n=t.type+"-"+a,i=e.get(n);i||(i={},e.set(n,i)),g_(t).scope=i}}),r.eachSeries(function(t){if(!(t.isColorBySeries()||r.isSeriesFiltered(t))){var a=t.getRawData(),n={},i=t.getData(),o=g_(t).scope,s=t.visualStyleAccessPath||"itemStyle",l=jA(t,s);i.each(function(u){var f=i.getRawIndex(u);n[f]=u}),a.each(function(u){var f=n[u],h=i.getItemVisual(f,"colorFromPalette");if(h){var v=i.ensureUniqueItemVisual(f,"style"),c=a.getName(u)||u+"",p=a.count();v[l]=t.getColorFromPalette(c,o,p)}})}})}},fu=Math.PI;function Vk(r,e){e=e||{},Q(e,{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var t=new at,a=new wt({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});t.add(a);var n=new bt({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),i=new wt({style:{fill:"none"},textContent:n,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});t.add(i);var o;return e.showSpinner&&(o=new Cl({shape:{startAngle:-fu/2,endAngle:-fu/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001}),o.animateShape(!0).when(1e3,{endAngle:fu*3/2}).start("circularInOut"),o.animateShape(!0).when(1e3,{startAngle:fu*3/2}).delay(300).start("circularInOut"),t.add(o)),t.resize=function(){var s=n.getBoundingRect().width,l=e.showSpinner?e.spinnerRadius:0,u=(r.getWidth()-l*2-(e.showSpinner&&s?10:0)-s)/2-(e.showSpinner&&s?0:5+s/2)+(e.showSpinner?0:s/2)+(s?0:l),f=r.getHeight()/2;e.showSpinner&&o.setShape({cx:u,cy:f}),i.setShape({x:u-l,y:f-l,width:l*2,height:l*2}),a.setShape({x:0,y:0,width:r.getWidth(),height:r.getHeight()})},t.resize(),t}var QA=function(){function r(e,t,a,n){this._stageTaskMap=Z(),this.ecInstance=e,this.api=t,a=this._dataProcessorHandlers=a.slice(),n=this._visualHandlers=n.slice(),this._allHandlers=a.concat(n)}return r.prototype.restoreData=function(e,t){e.restoreData(t),this._stageTaskMap.each(function(a){var n=a.overallTask;n&&n.dirty()})},r.prototype.getPerformArgs=function(e,t){if(e.__pipeline){var a=this._pipelineMap.get(e.__pipeline.id),n=a.context,i=!t&&a.progressiveEnabled&&(!n||n.progressiveRender)&&e.__idxInPipeline>a.blockIndex,o=i?a.step:null,s=n&&n.modDataCount,l=s!=null?Math.ceil(s/o):null;return{step:o,modBy:l,modDataCount:s}}},r.prototype.getPipeline=function(e){return this._pipelineMap.get(e)},r.prototype.updateStreamModes=function(e,t){var a=this._pipelineMap.get(e.uid),n=e.getData(),i=n.count(),o=a.progressiveEnabled&&t.incrementalPrepareRender&&i>=a.threshold,s=e.get("large")&&i>=e.get("largeThreshold"),l=e.get("progressiveChunkMode")==="mod"?i:null;e.pipelineContext=a.context={progressiveRender:o,modDataCount:l,large:s}},r.prototype.restorePipelines=function(e){var t=this,a=t._pipelineMap=Z();e.eachSeries(function(n){var i=n.getProgressive(),o=n.uid;a.set(o,{id:o,head:null,tail:null,threshold:n.getProgressiveThreshold(),progressiveEnabled:i&&!(n.preventIncremental&&n.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),t._pipe(n,n.dataTask)})},r.prototype.prepareStageTasks=function(){var e=this._stageTaskMap,t=this.api.getModel(),a=this.api;C(this._allHandlers,function(n){var i=e.get(n.uid)||e.set(n.uid,{}),o="";me(!(n.reset&&n.overallReset),o),n.reset&&this._createSeriesStageTask(n,i,t,a),n.overallReset&&this._createOverallStageTask(n,i,t,a)},this)},r.prototype.prepareView=function(e,t,a,n){var i=e.renderTask,o=i.context;o.model=t,o.ecModel=a,o.api=n,i.__block=!e.incrementalPrepareRender,this._pipe(t,i)},r.prototype.performDataProcessorTasks=function(e,t){this._performStageTasks(this._dataProcessorHandlers,e,t,{block:!0})},r.prototype.performVisualTasks=function(e,t,a){this._performStageTasks(this._visualHandlers,e,t,a)},r.prototype._performStageTasks=function(e,t,a,n){n=n||{};var i=!1,o=this;C(e,function(l,u){if(!(n.visualType&&n.visualType!==l.visualType)){var f=o._stageTaskMap.get(l.uid),h=f.seriesTaskMap,v=f.overallTask;if(v){var c,p=v.agentStubMap;p.each(function(g){s(n,g)&&(g.dirty(),c=!0)}),c&&v.dirty(),o.updatePayload(v,a);var d=o.getPerformArgs(v,n.block);p.each(function(g){g.perform(d)}),v.perform(d)&&(i=!0)}else h&&h.each(function(g,y){s(n,g)&&g.dirty();var m=o.getPerformArgs(g,n.block);m.skip=!l.performRawSeries&&t.isSeriesFiltered(g.context.model),o.updatePayload(g,a),g.perform(m)&&(i=!0)})}});function s(l,u){return l.setDirty&&(!l.dirtyMap||l.dirtyMap.get(u.__pipeline.id))}this.unfinished=i||this.unfinished},r.prototype.performSeriesTasks=function(e){var t;e.eachSeries(function(a){t=a.dataTask.perform()||t}),this.unfinished=t||this.unfinished},r.prototype.plan=function(){this._pipelineMap.each(function(e){var t=e.tail;do{if(t.__block){e.blockIndex=t.__idxInPipeline;break}t=t.getUpstream()}while(t)})},r.prototype.updatePayload=function(e,t){t!=="remain"&&(e.context.payload=t)},r.prototype._createSeriesStageTask=function(e,t,a,n){var i=this,o=t.seriesTaskMap,s=t.seriesTaskMap=Z(),l=e.seriesType,u=e.getTargetSeries;e.createOnAllSeries?a.eachRawSeries(f):l?a.eachRawSeriesByType(l,f):u&&u(a,n).each(f);function f(h){var v=h.uid,c=s.set(v,o&&o.get(v)||Ns({plan:Wk,reset:Uk,count:Xk}));c.context={model:h,ecModel:a,api:n,useClearVisual:e.isVisual&&!e.isLayout,plan:e.plan,reset:e.reset,scheduler:i},i._pipe(h,c)}},r.prototype._createOverallStageTask=function(e,t,a,n){var i=this,o=t.overallTask=t.overallTask||Ns({reset:zk});o.context={ecModel:a,api:n,overallReset:e.overallReset,scheduler:i};var s=o.agentStubMap,l=o.agentStubMap=Z(),u=e.seriesType,f=e.getTargetSeries,h=!0,v=!1,c="";me(!e.createOnAllSeries,c),u?a.eachRawSeriesByType(u,p):f?f(a,n).each(p):(h=!1,C(a.getSeries(),p));function p(d){var g=d.uid,y=l.set(g,s&&s.get(g)||(v=!0,Ns({reset:Gk,onDirty:Hk})));y.context={model:d,overallProgress:h},y.agent=o,y.__block=h,i._pipe(d,y)}v&&o.dirty()},r.prototype._pipe=function(e,t){var a=e.uid,n=this._pipelineMap.get(a);!n.head&&(n.head=t),n.tail&&n.tail.pipe(t),n.tail=t,t.__idxInPipeline=n.count++,t.__pipeline=n},r.wrapStageHandler=function(e,t){return K(e)&&(e={overallReset:e,seriesType:$k(e)}),e.uid=Co("stageHandler"),t&&(e.visualType=t),e},r}();function zk(r){r.overallReset(r.ecModel,r.api,r.payload)}function Gk(r){return r.overallProgress&&Fk}function Fk(){this.agent.dirty(),this.getDownstream().dirty()}function Hk(){this.agent&&this.agent.dirty()}function Wk(r){return r.plan?r.plan(r.model,r.ecModel,r.api,r.payload):null}function Uk(r){r.useClearVisual&&r.data.clearAllVisual();var e=r.resetDefines=Et(r.reset(r.model,r.ecModel,r.api,r.payload));return e.length>1?G(e,function(t,a){return JA(a)}):Yk}var Yk=JA(0);function JA(r){return function(e,t){var a=t.data,n=t.resetDefines[r];if(n&&n.dataEach)for(var i=e.start;i0&&c===u.length-v.length){var p=u.slice(0,c);p!=="data"&&(t.mainType=p,t[v.toLowerCase()]=l,f=!0)}}s.hasOwnProperty(u)&&(a[u]=l,f=!0),f||(n[u]=l)})}return{cptQuery:t,dataQuery:a,otherQuery:n}},r.prototype.filter=function(e,t){var a=this.eventInfo;if(!a)return!0;var n=a.targetEl,i=a.packedEvent,o=a.model,s=a.view;if(!o||!s)return!0;var l=t.cptQuery,u=t.dataQuery;return f(l,o,"mainType")&&f(l,o,"subType")&&f(l,o,"index","componentIndex")&&f(l,o,"name")&&f(l,o,"id")&&f(u,i,"name")&&f(u,i,"dataIndex")&&f(u,i,"dataType")&&(!s.filterForExposedEvent||s.filterForExposedEvent(e,t.otherQuery,n,i));function f(h,v,c,p){return h[c]==null||v[p||c]===h[c]}},r.prototype.afterTrigger=function(){this.eventInfo=null},r}(),ud=["symbol","symbolSize","symbolRotate","symbolOffset"],x_=ud.concat(["symbolKeepAspect"]),Kk={createOnAllSeries:!0,performRawSeries:!0,reset:function(r,e){var t=r.getData();if(r.legendIcon&&t.setVisual("legendIcon",r.legendIcon),!r.hasSymbolVisual)return;for(var a={},n={},i=!1,o=0;o=0&&Xn(l)?l:.5;var u=r.createRadialGradient(o,s,0,o,s,l);return u}function fd(r,e,t){for(var a=e.type==="radial"?hO(r,e,t):fO(r,e,t),n=e.colorStops,i=0;i0)?null:r==="dashed"?[4*e,2*e]:r==="dotted"?[e]:Ct(r)?[r]:z(r)?r:null}function iy(r){var e=r.style,t=e.lineDash&&e.lineWidth>0&&cO(e.lineDash,e.lineWidth),a=e.lineDashOffset;if(t){var n=e.strokeNoScale&&r.getLineScale?r.getLineScale():1;n&&n!==1&&(t=G(t,function(i){return i/n}),a/=n)}return[t,a]}var pO=new qr(!0);function Ef(r){var e=r.stroke;return!(e==null||e==="none"||!(r.lineWidth>0))}function b_(r){return typeof r=="string"&&r!=="none"}function kf(r){var e=r.fill;return e!=null&&e!=="none"}function w_(r,e){if(e.fillOpacity!=null&&e.fillOpacity!==1){var t=r.globalAlpha;r.globalAlpha=e.fillOpacity*e.opacity,r.fill(),r.globalAlpha=t}else r.fill()}function T_(r,e){if(e.strokeOpacity!=null&&e.strokeOpacity!==1){var t=r.globalAlpha;r.globalAlpha=e.strokeOpacity*e.opacity,r.stroke(),r.globalAlpha=t}else r.stroke()}function hd(r,e,t){var a=Cg(e.image,e.__image,t);if(xh(a)){var n=r.createPattern(a,e.repeat||"repeat");if(typeof DOMMatrix=="function"&&n&&n.setTransform){var i=new DOMMatrix;i.translateSelf(e.x||0,e.y||0),i.rotateSelf(0,0,(e.rotation||0)*Ts),i.scaleSelf(e.scaleX||1,e.scaleY||1),n.setTransform(i)}return n}}function dO(r,e,t,a){var n,i=Ef(t),o=kf(t),s=t.strokePercent,l=s<1,u=!e.path;(!e.silent||l)&&u&&e.createPathProxy();var f=e.path||pO,h=e.__dirty;if(!a){var v=t.fill,c=t.stroke,p=o&&!!v.colorStops,d=i&&!!c.colorStops,g=o&&!!v.image,y=i&&!!c.image,m=void 0,_=void 0,S=void 0,b=void 0,x=void 0;(p||d)&&(x=e.getBoundingRect()),p&&(m=h?fd(r,v,x):e.__canvasFillGradient,e.__canvasFillGradient=m),d&&(_=h?fd(r,c,x):e.__canvasStrokeGradient,e.__canvasStrokeGradient=_),g&&(S=h||!e.__canvasFillPattern?hd(r,v,e):e.__canvasFillPattern,e.__canvasFillPattern=S),y&&(b=h||!e.__canvasStrokePattern?hd(r,c,e):e.__canvasStrokePattern,e.__canvasStrokePattern=S),p?r.fillStyle=m:g&&(S?r.fillStyle=S:o=!1),d?r.strokeStyle=_:y&&(b?r.strokeStyle=b:i=!1)}var w=e.getGlobalScale();f.setScale(w[0],w[1],e.segmentIgnoreThreshold);var T,A;r.setLineDash&&t.lineDash&&(n=iy(e),T=n[0],A=n[1]);var D=!0;(u||h&Fi)&&(f.setDPR(r.dpr),l?f.setContext(null):(f.setContext(r),D=!1),f.reset(),e.buildPath(f,e.shape,a),f.toStatic(),e.pathUpdated()),D&&f.rebuildPath(r,l?s:1),T&&(r.setLineDash(T),r.lineDashOffset=A),a||(t.strokeFirst?(i&&T_(r,t),o&&w_(r,t)):(o&&w_(r,t),i&&T_(r,t))),T&&r.setLineDash([])}function gO(r,e,t){var a=e.__image=Cg(t.image,e.__image,e,e.onload);if(!(!a||!xh(a))){var n=t.x||0,i=t.y||0,o=e.getWidth(),s=e.getHeight(),l=a.width/a.height;if(o==null&&s!=null?o=s*l:s==null&&o!=null?s=o/l:o==null&&s==null&&(o=a.width,s=a.height),t.sWidth&&t.sHeight){var u=t.sx||0,f=t.sy||0;r.drawImage(a,u,f,t.sWidth,t.sHeight,n,i,o,s)}else if(t.sx&&t.sy){var u=t.sx,f=t.sy,h=o-u,v=s-f;r.drawImage(a,u,f,h,v,n,i,o,s)}else r.drawImage(a,n,i,o,s)}}function yO(r,e,t){var a,n=t.text;if(n!=null&&(n+=""),n){r.font=t.font||Ka,r.textAlign=t.textAlign,r.textBaseline=t.textBaseline;var i=void 0,o=void 0;r.setLineDash&&t.lineDash&&(a=iy(e),i=a[0],o=a[1]),i&&(r.setLineDash(i),r.lineDashOffset=o),t.strokeFirst?(Ef(t)&&r.strokeText(n,t.x,t.y),kf(t)&&r.fillText(n,t.x,t.y)):(kf(t)&&r.fillText(n,t.x,t.y),Ef(t)&&r.strokeText(n,t.x,t.y)),i&&r.setLineDash([])}}var A_=["shadowBlur","shadowOffsetX","shadowOffsetY"],C_=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function iC(r,e,t,a,n){var i=!1;if(!a&&(t=t||{},e===t))return!1;if(a||e.opacity!==t.opacity){ze(r,n),i=!0;var o=Math.max(Math.min(e.opacity,1),0);r.globalAlpha=isNaN(o)?Kn.opacity:o}(a||e.blend!==t.blend)&&(i||(ze(r,n),i=!0),r.globalCompositeOperation=e.blend||Kn.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,a,n){if(!this[de]){if(this._disposed){this.id;return}var i,o,s;if(tt(a)&&(n=a.lazyUpdate,i=a.silent,o=a.replaceMerge,s=a.transition,a=a.notMerge),this[de]=!0,!this._model||a){var l=new RE(this._api),u=this._theme,f=this._model=new Qg;f.scheduler=this._scheduler,f.ssr=this._ssr,f.init(null,null,null,u,this._locale,l)}this._model.setOption(t,{replaceMerge:o},cd);var h={seriesTransition:s,optionChanged:!0};if(n)this[Ne]={silent:i,updateParams:h},this[de]=!1,this.getZr().wakeUp();else{try{Li(this),Da.update.call(this,null,h)}catch(v){throw this[Ne]=null,this[de]=!1,v}this._ssr||this._zr.flush(),this[Ne]=null,this[de]=!1,qo.call(this,i),Ko.call(this,i)}}},e.prototype.setTheme=function(){},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||_t.hasGlobalWindow&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var a=this._zr.painter;return a.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var a=this._zr.painter;return a.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){if(_t.svgSupported){var t=this._zr,a=t.storage.getDisplayList();return C(a,function(n){n.stopAnimation(null,!0)}),t.painter.toDataURL()}},e.prototype.getDataURL=function(t){if(this._disposed){this.id;return}t=t||{};var a=t.excludeComponents,n=this._model,i=[],o=this;C(a,function(l){n.eachComponent({mainType:l},function(u){var f=o._componentsMap[u.__viewId];f.group.ignore||(i.push(f),f.group.ignore=!0)})});var s=this._zr.painter.getType()==="svg"?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return C(i,function(l){l.group.ignore=!1}),s},e.prototype.getConnectedDataURL=function(t){if(this._disposed){this.id;return}var a=t.type==="svg",n=this.group,i=Math.min,o=Math.max,s=1/0;if(Vf[n]){var l=s,u=s,f=-s,h=-s,v=[],c=t&&t.pixelRatio||this.getDevicePixelRatio();C(Jn,function(_,S){if(_.group===n){var b=a?_.getZr().painter.getSvgDom().innerHTML:_.renderToCanvas(et(t)),x=_.getDom().getBoundingClientRect();l=i(x.left,l),u=i(x.top,u),f=o(x.right,f),h=o(x.bottom,h),v.push({dom:b,left:x.left,top:x.top})}}),l*=c,u*=c,f*=c,h*=c;var p=f-l,d=h-u,g=Cr.createCanvas(),y=Vp(g,{renderer:a?"svg":"canvas"});if(y.resize({width:p,height:d}),a){var m="";return C(v,function(_){var S=_.left-l,b=_.top-u;m+=''+_.dom+""}),y.painter.getSvgRoot().innerHTML=m,t.connectedBackgroundColor&&y.painter.setBackgroundColor(t.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}else return t.connectedBackgroundColor&&y.add(new wt({shape:{x:0,y:0,width:p,height:d},style:{fill:t.connectedBackgroundColor}})),C(v,function(_){var S=new oe({style:{x:_.left*c-l,y:_.top*c-u,image:_.dom}});y.add(S)}),y.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}else return this.getDataURL(t)},e.prototype.convertToPixel=function(t,a){return Zv(this,"convertToPixel",t,a)},e.prototype.convertFromPixel=function(t,a){return Zv(this,"convertFromPixel",t,a)},e.prototype.containPixel=function(t,a){if(this._disposed){this.id;return}var n=this._model,i,o=Ps(n,t);return C(o,function(s,l){l.indexOf("Models")>=0&&C(s,function(u){var f=u.coordinateSystem;if(f&&f.containPoint)i=i||!!f.containPoint(a);else if(l==="seriesModels"){var h=this._chartsMap[u.__viewId];h&&h.containPoint&&(i=i||h.containPoint(a,u))}},this)},this),!!i},e.prototype.getVisual=function(t,a){var n=this._model,i=Ps(n,t,{defaultMainType:"series"}),o=i.seriesModel,s=o.getData(),l=i.hasOwnProperty("dataIndexInside")?i.dataIndexInside:i.hasOwnProperty("dataIndex")?s.indexOfRawIndex(i.dataIndex):null;return l!=null?ny(s,l,a):Ll(s,a)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t=this;C(FO,function(a){var n=function(i){var o=t.getModel(),s=i.target,l,u=a==="globalout";if(u?l={}:s&&Yn(s,function(p){var d=nt(p);if(d&&d.dataIndex!=null){var g=d.dataModel||o.getSeriesByIndex(d.seriesIndex);return l=g&&g.getDataParams(d.dataIndex,d.dataType,s)||{},!0}else if(d.eventData)return l=V({},d.eventData),!0},!0),l){var f=l.componentType,h=l.componentIndex;(f==="markLine"||f==="markPoint"||f==="markArea")&&(f="series",h=l.seriesIndex);var v=f&&h!=null&&o.getComponent(f,h),c=v&&t[v.mainType==="series"?"_chartsMap":"_componentsMap"][v.__viewId];l.event=i,l.type=a,t._$eventProcessor.eventInfo={targetEl:s,packedEvent:l,model:v,view:c},t.trigger(a,l)}};n.zrEventfulCallAtLast=!0,t._zr.on(a,n,t)}),C(Bs,function(a,n){t._messageCenter.on(n,function(i){this.trigger(n,i)},t)}),C(["selectchanged"],function(a){t._messageCenter.on(a,function(n){this.trigger(a,n)},t)}),Qk(this._messageCenter,this,this._api)},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){if(this._disposed){this.id;return}this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed){this.id;return}this._disposed=!0;var t=this.getDom();t&&dT(this.getDom(),ly,"");var a=this,n=a._api,i=a._model;C(a._componentsViews,function(o){o.dispose(i,n)}),C(a._chartsViews,function(o){o.dispose(i,n)}),a._zr.dispose(),a._dom=a._model=a._chartsMap=a._componentsMap=a._chartsViews=a._componentsViews=a._scheduler=a._api=a._zr=a._throttledZrFlush=a._theme=a._coordSysMgr=a._messageCenter=null,delete Jn[a.id]},e.prototype.resize=function(t){if(!this[de]){if(this._disposed){this.id;return}this._zr.resize(t);var a=this._model;if(this._loadingFX&&this._loadingFX.resize(),!!a){var n=a.resetOption("media"),i=t&&t.silent;this[Ne]&&(i==null&&(i=this[Ne].silent),n=!0,this[Ne]=null),this[de]=!0;try{n&&Li(this),Da.update.call(this,{type:"resize",animation:V({duration:0},t&&t.animation)})}catch(o){throw this[de]=!1,o}this[de]=!1,qo.call(this,i),Ko.call(this,i)}}},e.prototype.showLoading=function(t,a){if(this._disposed){this.id;return}if(tt(t)&&(a=t,t=""),t=t||"default",this.hideLoading(),!!pd[t]){var n=pd[t](this._api,a),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){if(this._disposed){this.id;return}this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null},e.prototype.makeActionFromEvent=function(t){var a=V({},t);return a.type=Bs[t.type],a},e.prototype.dispatchAction=function(t,a){if(this._disposed){this.id;return}if(tt(a)||(a={silent:!!a}),!!Nf[t.type]&&this._model){if(this[de]){this._pendingActions.push(t);return}var n=a.silent;Kv.call(this,t,n);var i=a.flush;i?this._zr.flush():i!==!1&&_t.browser.weChat&&this._throttledZrFlush(),qo.call(this,n),Ko.call(this,n)}},e.prototype.updateLabelLayout=function(){mr.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed){this.id;return}var a=t.seriesIndex,n=this.getModel(),i=n.getSeriesByIndex(a);i.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()},e.internalField=function(){Li=function(h){var v=h._scheduler;v.restorePipelines(h._model),v.prepareStageTasks(),$v(h,!0),$v(h,!1),v.plan()},$v=function(h,v){for(var c=h._model,p=h._scheduler,d=v?h._componentsViews:h._chartsViews,g=v?h._componentsMap:h._chartsMap,y=h._zr,m=h._api,_=0;_v.get("hoverLayerThreshold")&&!_t.node&&!_t.worker&&v.eachSeries(function(g){if(!g.preventUsingHoverLayer){var y=h._chartsMap[g.__viewId];y.__alive&&y.eachRendered(function(m){m.states.emphasis&&(m.states.emphasis.hoverLayer=!0)})}})}function o(h,v){var c=h.get("blendMode")||null;v.eachRendered(function(p){p.isGroup||(p.style.blend=c)})}function s(h,v){if(!h.preventAutoZ){var c=h.get("z")||0,p=h.get("zlevel")||0;v.eachRendered(function(d){return l(d,c,p,-1/0),!0})}}function l(h,v,c,p){var d=h.getTextContent(),g=h.getTextGuideLine(),y=h.isGroup;if(y)for(var m=h.childrenRef(),_=0;_0?{duration:d,delay:c.get("delay"),easing:c.get("easing")}:null;v.eachRendered(function(y){if(y.states&&y.states.emphasis){if(Ji(y))return;if(y instanceof yt&&yR(y),y.__dirty){var m=y.prevStates;m&&y.useStates(m)}if(p){y.stateTransition=g;var _=y.getTextContent(),S=y.getTextGuideLine();_&&(_.stateTransition=g),S&&(S.stateTransition=g)}y.__dirty&&n(y)}})}z_=function(h){return new(function(v){k(c,v);function c(){return v!==null&&v.apply(this,arguments)||this}return c.prototype.getCoordinateSystems=function(){return h._coordSysMgr.getCoordinateSystems()},c.prototype.getComponentByElement=function(p){for(;p;){var d=p.__ecComponentInfo;if(d!=null)return h._model.getComponent(d.mainType,d.index);p=p.parent}},c.prototype.enterEmphasis=function(p,d){da(p,d),Ze(h)},c.prototype.leaveEmphasis=function(p,d){ga(p,d),Ze(h)},c.prototype.enterBlur=function(p){ET(p),Ze(h)},c.prototype.leaveBlur=function(p){Pg(p),Ze(h)},c.prototype.enterSelect=function(p){kT(p),Ze(h)},c.prototype.leaveSelect=function(p){OT(p),Ze(h)},c.prototype.getModel=function(){return h.getModel()},c.prototype.getViewOfComponentModel=function(p){return h.getViewOfComponentModel(p)},c.prototype.getViewOfSeriesModel=function(p){return h.getViewOfSeriesModel(p)},c}(AA))(h)},xC=function(h){function v(c,p){for(var d=0;d=0)){F_.push(t);var i=QA.wrapStageHandler(t,n);i.__prio=e,i.__raw=t,r.push(i)}}function DC(r,e){pd[r]=e}function p7(r){pL({createCanvas:r})}function qO(r,e,t){var a=vC("registerMap");a&&a(r,e,t)}function d7(r){var e=vC("getMap");return e&&e(r)}var KO=fk;gi(oy,Ok);gi(Vh,Nk);gi(Vh,Bk);gi(oy,Kk);gi(Vh,jk);gi(dC,AO);AC(DA);CC(IO,UE);DC("default",Vk);Qr({type:jn,event:jn,update:jn},Yt);Qr({type:ju,event:ju,update:ju},Yt);Qr({type:Rs,event:Rs,update:Rs},Yt);Qr({type:Qu,event:Qu,update:Qu},Yt);Qr({type:Es,event:Es,update:Es},Yt);TC("light",Zk);TC("dark",rC);var g7={},H_=[],jO={registerPreprocessor:AC,registerProcessor:CC,registerPostInit:YO,registerPostUpdate:XO,registerUpdateLifecycle:uy,registerAction:Qr,registerCoordinateSystem:$O,registerLayout:ZO,registerVisual:gi,registerTransform:KO,registerLoading:DC,registerMap:qO,registerImpl:CO,PRIORITY:VO,ComponentModel:mt,ComponentView:zt,SeriesModel:kt,ChartView:Rt,registerComponentModel:function(r){mt.registerClass(r)},registerComponentView:function(r){zt.registerClass(r)},registerSeriesModel:function(r){kt.registerClass(r)},registerChartView:function(r){Rt.registerClass(r)},registerSubTypeDefaulter:function(r,e){mt.registerSubTypeDefaulter(r,e)},registerPainter:function(r,e){rT(r,e)}};function gt(r){if(z(r)){C(r,function(e){gt(e)});return}vt(H_,r)>=0||(H_.push(r),K(r)&&(r={install:r}),r.install(jO))}function jo(r){return r==null?0:r.length||1}function W_(r){return r}var ya=function(){function r(e,t,a,n,i,o){this._old=e,this._new=t,this._oldKeyGetter=a||W_,this._newKeyGetter=n||W_,this.context=i,this._diffModeMultiple=o==="multiple"}return r.prototype.add=function(e){return this._add=e,this},r.prototype.update=function(e){return this._update=e,this},r.prototype.updateManyToOne=function(e){return this._updateManyToOne=e,this},r.prototype.updateOneToMany=function(e){return this._updateOneToMany=e,this},r.prototype.updateManyToMany=function(e){return this._updateManyToMany=e,this},r.prototype.remove=function(e){return this._remove=e,this},r.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},r.prototype._executeOneToOne=function(){var e=this._old,t=this._new,a={},n=new Array(e.length),i=new Array(t.length);this._initIndexMap(e,null,n,"_oldKeyGetter"),this._initIndexMap(t,a,i,"_newKeyGetter");for(var o=0;o1){var f=l.shift();l.length===1&&(a[s]=l[0]),this._update&&this._update(f,o)}else u===1?(a[s]=null,this._update&&this._update(l,o)):this._remove&&this._remove(o)}this._performRestAdd(i,a)},r.prototype._executeMultiple=function(){var e=this._old,t=this._new,a={},n={},i=[],o=[];this._initIndexMap(e,a,i,"_oldKeyGetter"),this._initIndexMap(t,n,o,"_newKeyGetter");for(var s=0;s1&&v===1)this._updateManyToOne&&this._updateManyToOne(f,u),n[l]=null;else if(h===1&&v>1)this._updateOneToMany&&this._updateOneToMany(f,u),n[l]=null;else if(h===1&&v===1)this._update&&this._update(f,u),n[l]=null;else if(h>1&&v>1)this._updateManyToMany&&this._updateManyToMany(f,u),n[l]=null;else if(h>1)for(var c=0;c1)for(var s=0;s30}var Qo=tt,Ma=G,aN=typeof Int32Array>"u"?Array:Int32Array,nN="e\0\0",U_=-1,iN=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],oN=["_approximateExtent"],Y_,du,Jo,ts,Jv,gu,tc,Le=function(){function r(e,t){this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var a,n=!1;IC(e)?(a=e.dimensions,this._dimOmitted=e.isDimensionOmitted(),this._schema=e):(n=!0,a=e),a=a||["x","y"];for(var i={},o=[],s={},l=!1,u={},f=0;f=t)){var a=this._store,n=a.getProvider();this._updateOrdinalMeta();var i=this._nameList,o=this._idList,s=n.getSource().sourceFormat,l=s===vr;if(l&&!n.pure)for(var u=[],f=e;f0},r.prototype.ensureUniqueItemVisual=function(e,t){var a=this._itemVisuals,n=a[e];n||(n=a[e]={});var i=n[t];return i==null&&(i=this.getVisual(t),z(i)?i=i.slice():Qo(i)&&(i=V({},i)),n[t]=i),i},r.prototype.setItemVisual=function(e,t,a){var n=this._itemVisuals[e]||{};this._itemVisuals[e]=n,Qo(t)?V(n,t):n[t]=a},r.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},r.prototype.setLayout=function(e,t){Qo(e)?V(this._layout,e):this._layout[e]=t},r.prototype.getLayout=function(e){return this._layout[e]},r.prototype.getItemLayout=function(e){return this._itemLayouts[e]},r.prototype.setItemLayout=function(e,t,a){this._itemLayouts[e]=a?V(this._itemLayouts[e]||{},t):t},r.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},r.prototype.setItemGraphicEl=function(e,t){var a=this.hostModel&&this.hostModel.seriesIndex;Xp(a,this.dataType,e,t),this._graphicEls[e]=t},r.prototype.getItemGraphicEl=function(e){return this._graphicEls[e]},r.prototype.eachItemGraphicEl=function(e,t){C(this._graphicEls,function(a,n){a&&e&&e.call(t,a,n)})},r.prototype.cloneShallow=function(e){return e||(e=new r(this._schema?this._schema:Ma(this.dimensions,this._getDimInfo,this),this.hostModel)),Jv(e,this),e._store=this._store,e},r.prototype.wrapMethod=function(e,t){var a=this[e];K(a)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(e),this[e]=function(){var n=a.apply(this,arguments);return t.apply(this,[n].concat(vh(arguments)))})},r.internalField=function(){Y_=function(e){var t=e._invertedIndicesMap;C(t,function(a,n){var i=e._dimInfos[n],o=i.ordinalMeta,s=e._store;if(o){a=t[n]=new aN(o.categories.length);for(var l=0;l1&&(l+="__ec__"+f),n[t]=l}}}(),r}();function sN(r,e){return Eo(r,e).dimensions}function Eo(r,e){Jg(r)||(r=ty(r)),e=e||{};var t=e.coordDimensions||[],a=e.dimensionsDefine||r.dimensionsDefine||[],n=Z(),i=[],o=uN(r,t,a,e.dimensionsCount),s=e.canOmitUnusedDimensions&&RC(o),l=a===r.dimensionsDefine,u=l?PC(r):LC(a),f=e.encodeDefine;!f&&e.encodeDefaulter&&(f=e.encodeDefaulter(r,o));for(var h=Z(f),v=new VA(o),c=0;c0&&(a.name=n+(i-1)),i++,e.set(n,i)}}function uN(r,e,t,a){var n=Math.max(r.dimensionsDetectedCount||1,e.length,t.length,a||0);return C(e,function(i){var o;tt(i)&&(o=i.dimsDef)&&(n=Math.max(n,o.length))}),n}function fN(r,e,t){if(t||e.hasKey(r)){for(var a=0;e.hasKey(r+a);)a++;r+=a}return e.set(r,!0),r}var hN=function(){function r(e){this.coordSysDims=[],this.axisMap=Z(),this.categoryAxisMap=Z(),this.coordSysName=e}return r}();function vN(r){var e=r.get("coordinateSystem"),t=new hN(e),a=cN[e];if(a)return a(r,t,t.axisMap,t.categoryAxisMap),t}var cN={cartesian2d:function(r,e,t,a){var n=r.getReferringComponents("xAxis",Kt).models[0],i=r.getReferringComponents("yAxis",Kt).models[0];e.coordSysDims=["x","y"],t.set("x",n),t.set("y",i),Pi(n)&&(a.set("x",n),e.firstCategoryDimIndex=0),Pi(i)&&(a.set("y",i),e.firstCategoryDimIndex==null&&(e.firstCategoryDimIndex=1))},singleAxis:function(r,e,t,a){var n=r.getReferringComponents("singleAxis",Kt).models[0];e.coordSysDims=["single"],t.set("single",n),Pi(n)&&(a.set("single",n),e.firstCategoryDimIndex=0)},polar:function(r,e,t,a){var n=r.getReferringComponents("polar",Kt).models[0],i=n.findAxisModel("radiusAxis"),o=n.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],t.set("radius",i),t.set("angle",o),Pi(i)&&(a.set("radius",i),e.firstCategoryDimIndex=0),Pi(o)&&(a.set("angle",o),e.firstCategoryDimIndex==null&&(e.firstCategoryDimIndex=1))},geo:function(r,e,t,a){e.coordSysDims=["lng","lat"]},parallel:function(r,e,t,a){var n=r.ecModel,i=n.getComponent("parallel",r.get("parallelIndex")),o=e.coordSysDims=i.dimensions.slice();C(i.parallelAxisIndex,function(s,l){var u=n.getComponent("parallelAxis",s),f=o[l];t.set(f,u),Pi(u)&&(a.set(f,u),e.firstCategoryDimIndex==null&&(e.firstCategoryDimIndex=l))})}};function Pi(r){return r.get("type")==="category"}function EC(r,e,t){t=t||{};var a=t.byIndex,n=t.stackedCoordDimension,i,o,s;pN(e)?i=e:(o=e.schema,i=o.dimensions,s=e.store);var l=!!(r&&r.get("stack")),u,f,h,v;if(C(i,function(m,_){U(m)&&(i[_]=m={name:m}),l&&!m.isExtraCoord&&(!a&&!u&&m.ordinalMeta&&(u=m),!f&&m.type!=="ordinal"&&m.type!=="time"&&(!n||n===m.coordDim)&&(f=m))}),f&&!a&&!u&&(a=!0),f){h="__\0ecstackresult_"+r.id,v="__\0ecstackedover_"+r.id,u&&(u.createInvertedIndices=!0);var c=f.coordDim,p=f.type,d=0;C(i,function(m){m.coordDim===c&&d++});var g={name:h,coordDim:c,coordDimIndex:d,type:p,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},y={name:v,coordDim:v,coordDimIndex:d+1,type:p,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};o?(s&&(g.storeDimIndex=s.ensureCalculationDimension(v,p),y.storeDimIndex=s.ensureCalculationDimension(h,p)),o.appendCalculationDimension(g),o.appendCalculationDimension(y)):(i.push(g),i.push(y))}return{stackedDimension:f&&f.name,stackedByDimension:u&&u.name,isStackedByIndex:a,stackedOverDimension:v,stackResultDimension:h}}function pN(r){return!IC(r.schema)}function ma(r,e){return!!e&&e===r.getCalculationInfo("stackedDimension")}function hy(r,e){return ma(r,e)?r.getCalculationInfo("stackResultDimension"):e}function dN(r,e){var t=r.get("coordinateSystem"),a=Io.get(t),n;return e&&e.coordSysDims&&(n=G(e.coordSysDims,function(i){var o={name:i},s=e.axisMap.get(i);if(s){var l=s.get("type");o.type=zf(l)}return o})),n||(n=a&&(a.getDimensionsInfo?a.getDimensionsInfo():a.dimensions.slice())||["x","y"]),n}function gN(r,e,t){var a,n;return t&&C(r,function(i,o){var s=i.coordDim,l=t.categoryAxisMap.get(s);l&&(a==null&&(a=o),i.ordinalMeta=l.getOrdinalMeta(),e&&(i.createInvertedIndices=!0)),i.otherDims.itemName!=null&&(n=!0)}),!n&&a!=null&&(r[a].otherDims.itemName=0),a}function Jr(r,e,t){t=t||{};var a=e.getSourceManager(),n,i=!1;r?(i=!0,n=ty(r)):(n=a.getSource(),i=n.sourceFormat===vr);var o=vN(e),s=dN(e,o),l=t.useEncodeDefaulter,u=K(l)?l:l?it(xA,s,e):null,f={coordDimensions:s,generateCoord:t.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!i},h=Eo(n,f),v=gN(h.dimensions,t.createInvertedIndices,o),c=i?null:a.getSharedDataStore(h),p=EC(e,{schema:h,store:c}),d=new Le(h,e);d.setCalculationInfo(p);var g=v!=null&&yN(n)?function(y,m,_,S){return S===v?_:this.defaultDimValueGetter(y,m,_,S)}:null;return d.hasItemOption=!1,d.initData(i?n:c,null,g),d}function yN(r){if(r.sourceFormat===vr){var e=mN(r.data||[]);return!z(yo(e))}}function mN(r){for(var e=0;et[1]&&(t[1]=e[1])},r.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getApproximateExtent(t))},r.prototype.getExtent=function(){return this._extent.slice()},r.prototype.setExtent=function(e,t){var a=this._extent;isNaN(e)||(a[0]=e),isNaN(t)||(a[1]=t)},r.prototype.isInExtentRange=function(e){return this._extent[0]<=e&&this._extent[1]>=e},r.prototype.isBlank=function(){return this._isBlank},r.prototype.setBlank=function(e){this._isBlank=e},r}();Sh(ta);var _N=0,dd=function(){function r(e){this.categories=e.categories||[],this._needCollect=e.needCollect,this._deduplication=e.deduplication,this.uid=++_N}return r.createByAxisModel=function(e){var t=e.option,a=t.data,n=a&&G(a,SN);return new r({categories:n,needCollect:!n,deduplication:t.dedplication!==!1})},r.prototype.getOrdinal=function(e){return this._getOrCreateMap().get(e)},r.prototype.parseAndCollect=function(e){var t,a=this._needCollect;if(!U(e)&&!a)return e;if(a&&!this._deduplication)return t=this.categories.length,this.categories[t]=e,t;var n=this._getOrCreateMap();return t=n.get(e),t==null&&(a?(t=this.categories.length,this.categories[t]=e,n.set(e,t)):t=NaN),t},r.prototype._getOrCreateMap=function(){return this._map||(this._map=Z(this.categories))},r}();function SN(r){return tt(r)&&r.value!=null?r.value:r+""}function gd(r){return r.type==="interval"||r.type==="log"}function xN(r,e,t,a){var n={},i=r[1]-r[0],o=n.interval=xg(i/e,!0);t!=null&&oa&&(o=n.interval=a);var s=n.intervalPrecision=kC(o),l=n.niceTickExtent=[Ht(Math.ceil(r[0]/o)*o,s),Ht(Math.floor(r[1]/o)*o,s)];return bN(l,r),n}function ec(r){var e=Math.pow(10,_h(r)),t=r/e;return t?t===2?t=3:t===3?t=5:t*=2:t=1,Ht(t*e)}function kC(r){return Sr(r)+2}function X_(r,e,t){r[e]=Math.max(Math.min(r[e],t[1]),t[0])}function bN(r,e){!isFinite(r[0])&&(r[0]=e[0]),!isFinite(r[1])&&(r[1]=e[1]),X_(r,0,e),X_(r,1,e),r[0]>r[1]&&(r[0]=r[1])}function zh(r,e){return r>=e[0]&&r<=e[1]}function Gh(r,e){return e[1]===e[0]?.5:(r-e[0])/(e[1]-e[0])}function Fh(r,e){return r*(e[1]-e[0])+e[0]}var Hh=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;a.type="ordinal";var n=a.getSetting("ordinalMeta");return n||(n=new dd({})),z(n)&&(n=new dd({categories:G(n,function(i){return tt(i)?i.value:i})})),a._ordinalMeta=n,a._extent=a.getSetting("extent")||[0,n.categories.length-1],a}return e.prototype.parse=function(t){return t==null?NaN:U(t)?this._ordinalMeta.getOrdinal(t):Math.round(t)},e.prototype.contain=function(t){return t=this.parse(t),zh(t,this._extent)&&this._ordinalMeta.categories[t]!=null},e.prototype.normalize=function(t){return t=this._getTickNumber(this.parse(t)),Gh(t,this._extent)},e.prototype.scale=function(t){return t=Math.round(Fh(t,this._extent)),this.getRawOrdinalNumber(t)},e.prototype.getTicks=function(){for(var t=[],a=this._extent,n=a[0];n<=a[1];)t.push({value:n}),n++;return t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(t==null){this._ordinalNumbersByTick=this._ticksByOrdinalNumber=null;return}for(var a=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],o=0,s=this._ordinalMeta.categories.length,l=Math.min(s,a.length);o=0&&t=0&&t=t},e.prototype.getOrdinalMeta=function(){return this._ordinalMeta},e.prototype.calcNiceTicks=function(){},e.prototype.calcNiceExtent=function(){},e.type="ordinal",e}(ta);ta.registerClass(Hh);var Cn=Ht,_a=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type="interval",t._interval=0,t._intervalPrecision=2,t}return e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return zh(t,this._extent)},e.prototype.normalize=function(t){return Gh(t,this._extent)},e.prototype.scale=function(t){return Fh(t,this._extent)},e.prototype.setExtent=function(t,a){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(a)||(n[1]=parseFloat(a))},e.prototype.unionExtent=function(t){var a=this._extent;t[0]a[1]&&(a[1]=t[1]),this.setExtent(a[0],a[1])},e.prototype.getInterval=function(){return this._interval},e.prototype.setInterval=function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=kC(t)},e.prototype.getTicks=function(t){var a=this._interval,n=this._extent,i=this._niceExtent,o=this._intervalPrecision,s=[];if(!a)return s;var l=1e4;n[0]l)return[];var f=s.length?s[s.length-1].value:i[1];return n[1]>f&&(t?s.push({value:Cn(f+a,o)}):s.push({value:n[1]})),s},e.prototype.getMinorTicks=function(t){for(var a=this.getTicks(!0),n=[],i=this.getExtent(),o=1;oi[0]&&c0&&(i=i===null?s:Math.min(i,s))}t[a]=i}}return t}function BC(r){var e=AN(r),t=[];return C(r,function(a){var n=a.coordinateSystem,i=n.getBaseAxis(),o=i.getExtent(),s;if(i.type==="category")s=i.getBandWidth();else if(i.type==="value"||i.type==="time"){var l=i.dim+"_"+i.index,u=e[l],f=Math.abs(o[1]-o[0]),h=i.scale.getExtent(),v=Math.abs(h[1]-h[0]);s=u?f/v*u:f}else{var c=a.getData();s=Math.abs(o[1]-o[0])/c.count()}var p=W(a.get("barWidth"),s),d=W(a.get("barMaxWidth"),s),g=W(a.get("barMinWidth")||(HC(a)?.5:1),s),y=a.get("barGap"),m=a.get("barCategoryGap");t.push({bandWidth:s,barWidth:p,barMaxWidth:d,barMinWidth:g,barGap:y,barCategoryGap:m,axisKey:cy(i),stackId:vy(a)})}),VC(t)}function VC(r){var e={};C(r,function(a,n){var i=a.axisKey,o=a.bandWidth,s=e[i]||{bandWidth:o,remainedWidth:o,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},l=s.stacks;e[i]=s;var u=a.stackId;l[u]||s.autoWidthCount++,l[u]=l[u]||{width:0,maxWidth:0};var f=a.barWidth;f&&!l[u].width&&(l[u].width=f,f=Math.min(s.remainedWidth,f),s.remainedWidth-=f);var h=a.barMaxWidth;h&&(l[u].maxWidth=h);var v=a.barMinWidth;v&&(l[u].minWidth=v);var c=a.barGap;c!=null&&(s.gap=c);var p=a.barCategoryGap;p!=null&&(s.categoryGap=p)});var t={};return C(e,function(a,n){t[n]={};var i=a.stacks,o=a.bandWidth,s=a.categoryGap;if(s==null){var l=St(i).length;s=Math.max(35-l*4,15)+"%"}var u=W(s,o),f=W(a.gap,1),h=a.remainedWidth,v=a.autoWidthCount,c=(h-u)/(v+(v-1)*f);c=Math.max(c,0),C(i,function(y){var m=y.maxWidth,_=y.minWidth;if(y.width){var S=y.width;m&&(S=Math.min(S,m)),_&&(S=Math.max(S,_)),y.width=S,h-=S+f*S,v--}else{var S=c;m&&mS&&(S=_),S!==c&&(y.width=S,h-=S+f*S,v--)}}),c=(h-u)/(v+(v-1)*f),c=Math.max(c,0);var p=0,d;C(i,function(y,m){y.width||(y.width=c),d=y,p+=y.width*(1+f)}),d&&(p-=d.width*f);var g=-p/2;C(i,function(y,m){t[n][m]=t[n][m]||{bandWidth:o,offset:g,width:y.width},g+=y.width*(1+f)})}),t}function CN(r,e,t){if(r&&e){var a=r[cy(e)];return a!=null&&t!=null?a[vy(t)]:a}}function zC(r,e){var t=NC(r,e),a=BC(t);C(t,function(n){var i=n.getData(),o=n.coordinateSystem,s=o.getBaseAxis(),l=vy(n),u=a[cy(s)][l],f=u.offset,h=u.width;i.setLayout({bandWidth:u.bandWidth,offset:f,size:h})})}function GC(r){return{seriesType:r,plan:Lo(),reset:function(e){if(FC(e)){var t=e.getData(),a=e.coordinateSystem,n=a.getBaseAxis(),i=a.getOtherAxis(n),o=t.getDimensionIndex(t.mapDimension(i.dim)),s=t.getDimensionIndex(t.mapDimension(n.dim)),l=e.get("showBackground",!0),u=t.mapDimension(i.dim),f=t.getCalculationInfo("stackResultDimension"),h=ma(t,u)&&!!t.getCalculationInfo("stackedOnSeries"),v=i.isHorizontal(),c=DN(n,i),p=HC(e),d=e.get("barMinHeight")||0,g=f&&t.getDimensionIndex(f),y=t.getLayout("size"),m=t.getLayout("offset");return{progress:function(_,S){for(var b=_.count,x=p&&Hr(b*3),w=p&&l&&Hr(b*3),T=p&&Hr(b),A=a.master.getRect(),D=v?A.width:A.height,M,I=S.getStore(),L=0;(M=_.next())!=null;){var P=I.get(h?g:o,M),R=I.get(s,M),E=c,N=void 0;h&&(N=+P-I.get(o,M));var O=void 0,B=void 0,F=void 0,H=void 0;if(v){var Y=a.dataToPoint([P,R]);if(h){var j=a.dataToPoint([N,R]);E=j[0]}O=E,B=Y[1]+m,F=Y[0]-E,H=y,Math.abs(F)0?t:1:t))}var MN=function(r,e,t,a){for(;t>>1;r[n][1]n&&(this._approxInterval=n);var s=yu.length,l=Math.min(MN(yu,this._approxInterval,0,s),s-1);this._interval=yu[l][1],this._minLevelUnit=yu[Math.max(l-1,0)][0]},e.prototype.parse=function(t){return Ct(t)?t:+$e(t)},e.prototype.contain=function(t){return zh(this.parse(t),this._extent)},e.prototype.normalize=function(t){return Gh(this.parse(t),this._extent)},e.prototype.scale=function(t){return Fh(t,this._extent)},e.type="time",e}(_a),yu=[["second",Hg],["minute",Wg],["hour",Os],["quarter-day",Os*6],["half-day",Os*12],["day",ir*1.2],["half-week",ir*3.5],["week",ir*7],["month",ir*31],["quarter",ir*95],["half-year",N0/2],["year",N0]];function IN(r,e,t,a){var n=$e(e),i=$e(t),o=function(p){return V0(n,p,a)===V0(i,p,a)},s=function(){return o("year")},l=function(){return s()&&o("month")},u=function(){return l()&&o("day")},f=function(){return u()&&o("hour")},h=function(){return f()&&o("minute")},v=function(){return h()&&o("second")},c=function(){return v()&&o("millisecond")};switch(r){case"year":return s();case"month":return l();case"day":return u();case"hour":return f();case"minute":return h();case"second":return v();case"millisecond":return c()}}function LN(r,e){return r/=ir,r>16?16:r>7.5?7:r>3.5?4:r>1.5?2:1}function PN(r){var e=30*ir;return r/=e,r>6?6:r>3?3:r>2?2:1}function RN(r){return r/=Os,r>12?12:r>6?6:r>3.5?4:r>2?2:1}function $_(r,e){return r/=e?Wg:Hg,r>30?30:r>20?20:r>15?15:r>10?10:r>5?5:r>2?2:1}function EN(r){return xg(r,!0)}function kN(r,e,t){var a=new Date(r);switch(eo(e)){case"year":case"month":a[lA(t)](0);case"day":a[uA(t)](1);case"hour":a[fA(t)](0);case"minute":a[hA(t)](0);case"second":a[vA(t)](0),a[cA(t)](0)}return a.getTime()}function ON(r,e,t,a){var n=1e4,i=oA,o=0;function s(D,M,I,L,P,R,E){for(var N=new Date(M),O=M,B=N[L]();O1&&R===0&&I.unshift({value:I[0].value-O})}}for(var R=0;R=a[0]&&m<=a[1]&&h++)}var _=(a[1]-a[0])/e;if(h>_*1.5&&v>_/1.5||(u.push(g),h>_||r===i[c]))break}f=[]}}}for(var S=Pt(G(u,function(D){return Pt(D,function(M){return M.value>=a[0]&&M.value<=a[1]&&!M.notAdd})}),function(D){return D.length>0}),b=[],x=S.length-1,c=0;c0;)i*=10;var s=[Ht(VN(a[0]/i)*i),Ht(BN(a[1]/i)*i)];this._interval=i,this._niceExtent=s}},e.prototype.calcNiceExtent=function(t){Vs.calcNiceExtent.call(this,t),this._fixMin=t.fixMin,this._fixMax=t.fixMax},e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return t=dr(t)/dr(this.base),zh(t,this._extent)},e.prototype.normalize=function(t){return t=dr(t)/dr(this.base),Gh(t,this._extent)},e.prototype.scale=function(t){return t=Fh(t,this._extent),mu(this.base,t)},e.type="log",e}(ta),WC=dy.prototype;WC.getMinorTicks=Vs.getMinorTicks;WC.getLabel=Vs.getLabel;function _u(r,e){return NN(r,Sr(e))}ta.registerClass(dy);var zN=function(){function r(e,t,a){this._prepareParams(e,t,a)}return r.prototype._prepareParams=function(e,t,a){a[1]0&&l>0&&!u&&(s=0),s<0&&l<0&&!f&&(l=0));var v=this._determinedMin,c=this._determinedMax;return v!=null&&(s=v,u=!0),c!=null&&(l=c,f=!0),{min:s,max:l,minFixed:u,maxFixed:f,isBlank:h}},r.prototype.modifyDataMinMax=function(e,t){this[FN[e]]=t},r.prototype.setDeterminedMinMax=function(e,t){var a=GN[e];this[a]=t},r.prototype.freeze=function(){this.frozen=!0},r}(),GN={min:"_determinedMin",max:"_determinedMax"},FN={min:"_dataMin",max:"_dataMax"};function UC(r,e,t){var a=r.rawExtentInfo;return a||(a=new zN(r,e,t),r.rawExtentInfo=a,a)}function Su(r,e){return e==null?null:no(e)?NaN:r.parse(e)}function YC(r,e){var t=r.type,a=UC(r,e,r.getExtent()).calculate();r.setBlank(a.isBlank);var n=a.min,i=a.max,o=e.ecModel;if(o&&t==="time"){var s=NC("bar",o),l=!1;if(C(s,function(h){l=l||h.getBaseAxis()===e.axis}),l){var u=BC(s),f=HN(n,i,e,u);n=f.min,i=f.max}}return{extent:[n,i],fixMin:a.minFixed,fixMax:a.maxFixed}}function HN(r,e,t,a){var n=t.axis.getExtent(),i=n[1]-n[0],o=CN(a,t.axis);if(o===void 0)return{min:r,max:e};var s=1/0;C(o,function(c){s=Math.min(c.offset,s)});var l=-1/0;C(o,function(c){l=Math.max(c.offset+c.width,l)}),s=Math.abs(s),l=Math.abs(l);var u=s+l,f=e-r,h=1-(s+l)/i,v=f/h-f;return e+=v*(l/u),r-=v*(s/u),{min:r,max:e}}function li(r,e){var t=e,a=YC(r,t),n=a.extent,i=t.get("splitNumber");r instanceof dy&&(r.base=t.get("logBase"));var o=r.type,s=t.get("interval"),l=o==="interval"||o==="time";r.setExtent(n[0],n[1]),r.calcNiceExtent({splitNumber:i,fixMin:a.fixMin,fixMax:a.fixMax,minInterval:l?t.get("minInterval"):null,maxInterval:l?t.get("maxInterval"):null}),s!=null&&r.setInterval&&r.setInterval(s)}function Pl(r,e){if(e=e||r.get("type"),e)switch(e){case"category":return new Hh({ordinalMeta:r.getOrdinalMeta?r.getOrdinalMeta():r.getCategories(),extent:[1/0,-1/0]});case"time":return new py({locale:r.ecModel.getLocaleModel(),useUTC:r.ecModel.get("useUTC")});default:return new(ta.getClass(e)||_a)}}function WN(r){var e=r.scale.getExtent(),t=e[0],a=e[1];return!(t>0&&a>0||t<0&&a<0)}function ko(r){var e=r.getLabelModel().get("formatter"),t=r.type==="category"?r.scale.getExtent()[0]:null;return r.scale.type==="time"?function(a){return function(n,i){return r.scale.getFormattedLabel(n,i,a)}}(e):U(e)?function(a){return function(n){var i=r.scale.getLabel(n),o=a.replace("{value}",i??"");return o}}(e):K(e)?function(a){return function(n,i){return t!=null&&(i=n.value-t),a(gy(r,n),i,n.level!=null?{level:n.level}:null)}}(e):function(a){return r.scale.getLabel(a)}}function gy(r,e){return r.type==="category"?r.scale.getLabel(e):e.value}function UN(r){var e=r.model,t=r.scale;if(!(!e.get(["axisLabel","show"])||t.isBlank())){var a,n,i=t.getExtent();t instanceof Hh?n=t.count():(a=t.getTicks(),n=a.length);var o=r.getLabelModel(),s=ko(r),l,u=1;n>40&&(u=Math.ceil(n/40));for(var f=0;fr[1]&&(r[1]=n[1])})}var Oo=function(){function r(){}return r.prototype.getNeedCrossZero=function(){var e=this.option;return!e.scale},r.prototype.getCoordSysModel=function(){},r}();function $N(r){return Jr(null,r)}var ZN={isDimensionStacked:ma,enableDataStack:EC,getStackedDimension:hy};function qN(r,e){var t=e;e instanceof Mt||(t=new Mt(e));var a=Pl(t);return a.setExtent(r[0],r[1]),li(a,t),a}function KN(r){Xt(r,Oo)}function jN(r,e){return e=e||{},Nt(r,null,null,e.state!=="normal")}const y7=Object.freeze(Object.defineProperty({__proto__:null,createDimensions:sN,createList:$N,createScale:qN,createSymbol:Zt,createTextStyle:jN,dataStack:ZN,enableHoverEmphasis:Ya,getECData:nt,getLayoutRect:jt,mixinAxisModelCommonMethods:KN},Symbol.toStringTag,{value:"Module"}));var QN=1e-8;function q_(r,e){return Math.abs(r-e)n&&(a=o,n=l)}if(a)return tB(a.exterior);var u=this.getBoundingRect();return[u.x+u.width/2,u.y+u.height/2]},e.prototype.getBoundingRect=function(t){var a=this._rect;if(a&&!t)return a;var n=[1/0,1/0],i=[-1/0,-1/0],o=this.geometries;return C(o,function(s){s.type==="polygon"?K_(s.exterior,n,i,t):C(s.points,function(l){K_(l,n,i,t)})}),isFinite(n[0])&&isFinite(n[1])&&isFinite(i[0])&&isFinite(i[1])||(n[0]=n[1]=i[0]=i[1]=0),a=new ht(n[0],n[1],i[0]-n[0],i[1]-n[1]),t||(this._rect=a),a},e.prototype.contain=function(t){var a=this.getBoundingRect(),n=this.geometries;if(!a.contain(t[0],t[1]))return!1;t:for(var i=0,o=n.length;i>1^-(s&1),l=l>>1^-(l&1),s+=n,l+=i,n=s,i=l,a.push([s/t,l/t])}return a}function aB(r,e){return r=rB(r),G(Pt(r.features,function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0}),function(t){var a=t.properties,n=t.geometry,i=[];switch(n.type){case"Polygon":var o=n.coordinates;i.push(new j_(o[0],o.slice(1)));break;case"MultiPolygon":C(n.coordinates,function(l){l[0]&&i.push(new j_(l[0],l.slice(1)))});break;case"LineString":i.push(new Q_([n.coordinates]));break;case"MultiLineString":i.push(new Q_(n.coordinates))}var s=new ZC(a[e||"name"],i,a.cp);return s.properties=a,s})}const m7=Object.freeze(Object.defineProperty({__proto__:null,MAX_SAFE_INTEGER:Gp,asc:Ue,getPercentWithPrecision:U2,getPixelPrecision:_g,getPrecision:Sr,getPrecisionSafe:oT,isNumeric:bg,isRadianAroundZero:io,linearMap:Lt,nice:xg,numericToNumber:Zr,parseDate:$e,quantile:Ku,quantity:lT,quantityExponent:_h,reformIntervals:Fp,remRadian:Sg,round:Ht},Symbol.toStringTag,{value:"Module"})),_7=Object.freeze(Object.defineProperty({__proto__:null,format:Il,parse:$e},Symbol.toStringTag,{value:"Module"})),S7=Object.freeze(Object.defineProperty({__proto__:null,Arc:Cl,BezierCurve:So,BoundingRect:ht,Circle:Kr,CompoundPath:Ch,Ellipse:Al,Group:at,Image:oe,IncrementalDisplayable:XT,Line:Jt,LinearGradient:xo,Polygon:Se,Polyline:be,RadialGradient:kg,Rect:wt,Ring:_o,Sector:_e,Text:bt,clipPointsByRect:Vg,clipRectByRect:jT,createIcon:wo,extendPath:qT,extendShape:ZT,getShapeClass:Dh,getTransform:Xa,initProps:Vt,makeImage:Ng,makePath:Dl,mergePath:We,registerShape:hr,resizePath:Bg,updateProps:Dt},Symbol.toStringTag,{value:"Module"})),x7=Object.freeze(Object.defineProperty({__proto__:null,addCommas:Yg,capitalFirst:pE,encodeHTML:Me,formatTime:cE,formatTpl:$g,getTextRect:hE,getTooltipMarker:pA,normalizeCssArray:pi,toCamelCase:Xg,truncateText:_T},Symbol.toStringTag,{value:"Module"})),b7=Object.freeze(Object.defineProperty({__proto__:null,bind:X,clone:et,curry:it,defaults:Q,each:C,extend:V,filter:Pt,indexOf:vt,inherits:hg,isArray:z,isFunction:K,isObject:tt,isString:U,map:G,merge:st,reduce:lr},Symbol.toStringTag,{value:"Module"}));var il=Tt();function KC(r,e){var t=G(e,function(a){return r.scale.parse(a)});return r.type==="time"&&t.length>0&&(t.sort(),t.unshift(t[0]),t.push(t[t.length-1])),t}function nB(r){var e=r.getLabelModel().get("customValues");if(e){var t=ko(r);return{labels:KC(r,e).map(function(a){var n={value:a};return{formattedLabel:t(n),rawLabel:r.scale.getLabel(n),tickValue:a}})}}return r.type==="category"?oB(r):lB(r)}function iB(r,e){var t=r.getTickModel().get("customValues");return t?{ticks:KC(r,t)}:r.type==="category"?sB(r,e):{ticks:G(r.scale.getTicks(),function(a){return a.value})}}function oB(r){var e=r.getLabelModel(),t=jC(r,e);return!e.get("show")||r.scale.isBlank()?{labels:[],labelCategoryInterval:t.labelCategoryInterval}:t}function jC(r,e){var t=QC(r,"labels"),a=yy(e),n=JC(t,a);if(n)return n;var i,o;return K(a)?i=rD(r,a):(o=a==="auto"?uB(r):a,i=eD(r,o)),tD(t,a,{labels:i,labelCategoryInterval:o})}function sB(r,e){var t=QC(r,"ticks"),a=yy(e),n=JC(t,a);if(n)return n;var i,o;if((!e.get("show")||r.scale.isBlank())&&(i=[]),K(a))i=rD(r,a,!0);else if(a==="auto"){var s=jC(r,r.getLabelModel());o=s.labelCategoryInterval,i=G(s.labels,function(l){return l.tickValue})}else o=a,i=eD(r,o,!0);return tD(t,a,{ticks:i,tickCategoryInterval:o})}function lB(r){var e=r.scale.getTicks(),t=ko(r);return{labels:G(e,function(a,n){return{level:a.level,formattedLabel:t(a,n),rawLabel:r.scale.getLabel(a),tickValue:a.value}})}}function QC(r,e){return il(r)[e]||(il(r)[e]=[])}function JC(r,e){for(var t=0;t40&&(s=Math.max(1,Math.floor(o/40)));for(var l=i[0],u=r.dataToCoord(l+1)-r.dataToCoord(l),f=Math.abs(u*Math.cos(a)),h=Math.abs(u*Math.sin(a)),v=0,c=0;l<=i[1];l+=s){var p=0,d=0,g=bl(t({value:l}),e.font,"center","top");p=g.width*1.3,d=g.height*1.3,v=Math.max(v,p,7),c=Math.max(c,d,7)}var y=v/f,m=c/h;isNaN(y)&&(y=1/0),isNaN(m)&&(m=1/0);var _=Math.max(0,Math.floor(Math.min(y,m))),S=il(r.model),b=r.getExtent(),x=S.lastAutoInterval,w=S.lastTickCount;return x!=null&&w!=null&&Math.abs(x-_)<=1&&Math.abs(w-o)<=1&&x>_&&S.axisExtent0===b[0]&&S.axisExtent1===b[1]?_=x:(S.lastTickCount=o,S.lastAutoInterval=_,S.axisExtent0=b[0],S.axisExtent1=b[1]),_}function hB(r){var e=r.getLabelModel();return{axisRotate:r.getRotate?r.getRotate():r.isHorizontal&&!r.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}function eD(r,e,t){var a=ko(r),n=r.scale,i=n.getExtent(),o=r.getLabelModel(),s=[],l=Math.max((e||0)+1,1),u=i[0],f=n.count();u!==0&&l>1&&f/l>2&&(u=Math.round(Math.ceil(u/l)*l));var h=XC(r),v=o.get("showMinLabel")||h,c=o.get("showMaxLabel")||h;v&&u!==i[0]&&d(i[0]);for(var p=u;p<=i[1];p+=l)d(p);c&&p-l!==i[1]&&d(i[1]);function d(g){var y={value:g};s.push(t?g:{formattedLabel:a(y),rawLabel:n.getLabel(y),tickValue:g})}return s}function rD(r,e,t){var a=r.scale,n=ko(r),i=[];return C(a.getTicks(),function(o){var s=a.getLabel(o),l=o.value;e(o.value,s)&&i.push(t?l:{formattedLabel:n(o),rawLabel:s,tickValue:l})}),i}var J_=[0,1],Lr=function(){function r(e,t,a){this.onBand=!1,this.inverse=!1,this.dim=e,this.scale=t,this._extent=a||[0,0]}return r.prototype.contain=function(e){var t=this._extent,a=Math.min(t[0],t[1]),n=Math.max(t[0],t[1]);return e>=a&&e<=n},r.prototype.containData=function(e){return this.scale.contain(e)},r.prototype.getExtent=function(){return this._extent.slice()},r.prototype.getPixelPrecision=function(e){return _g(e||this.scale.getExtent(),this._extent)},r.prototype.setExtent=function(e,t){var a=this._extent;a[0]=e,a[1]=t},r.prototype.dataToCoord=function(e,t){var a=this._extent,n=this.scale;return e=n.normalize(e),this.onBand&&n.type==="ordinal"&&(a=a.slice(),t1(a,n.count())),Lt(e,J_,a,t)},r.prototype.coordToData=function(e,t){var a=this._extent,n=this.scale;this.onBand&&n.type==="ordinal"&&(a=a.slice(),t1(a,n.count()));var i=Lt(e,a,J_,t);return this.scale.scale(i)},r.prototype.pointToData=function(e,t){},r.prototype.getTicksCoords=function(e){e=e||{};var t=e.tickModel||this.getTickModel(),a=iB(this,t),n=a.ticks,i=G(n,function(s){return{coord:this.dataToCoord(this.scale.type==="ordinal"?this.scale.getRawOrdinalNumber(s):s),tickValue:s}},this),o=t.get("alignWithLabel");return vB(this,i,o,e.clamp),i},r.prototype.getMinorTicksCoords=function(){if(this.scale.type==="ordinal")return[];var e=this.model.getModel("minorTick"),t=e.get("splitNumber");t>0&&t<100||(t=5);var a=this.scale.getMinorTicks(t),n=G(a,function(i){return G(i,function(o){return{coord:this.dataToCoord(o),tickValue:o}},this)},this);return n},r.prototype.getViewLabels=function(){return nB(this).labels},r.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},r.prototype.getTickModel=function(){return this.model.getModel("axisTick")},r.prototype.getBandWidth=function(){var e=this._extent,t=this.scale.getExtent(),a=t[1]-t[0]+(this.onBand?1:0);a===0&&(a=1);var n=Math.abs(e[1]-e[0]);return Math.abs(n)/a},r.prototype.calculateCategoryInterval=function(){return fB(this)},r}();function t1(r,e){var t=r[1]-r[0],a=e,n=t/a/2;r[0]+=n,r[1]-=n}function vB(r,e,t,a){var n=e.length;if(!r.onBand||t||!n)return;var i=r.getExtent(),o,s;if(n===1)e[0].coord=i[0],o=e[1]={coord:i[1]};else{var l=e[n-1].tickValue-e[0].tickValue,u=(e[n-1].coord-e[0].coord)/l;C(e,function(c){c.coord-=u/2});var f=r.scale.getExtent();s=1+f[1]-e[n-1].tickValue,o={coord:e[n-1].coord+u*s},e.push(o)}var h=i[0]>i[1];v(e[0].coord,i[0])&&(a?e[0].coord=i[0]:e.shift()),a&&v(i[0],e[0].coord)&&e.unshift({coord:i[0]}),v(i[1],o.coord)&&(a?o.coord=i[1]:e.pop()),a&&v(o.coord,i[1])&&e.push({coord:i[1]});function v(c,p){return c=Ht(c),p=Ht(p),h?c>p:cn&&(n+=es);var c=Math.atan2(s,o);if(c<0&&(c+=es),c>=a&&c<=n||c+es>=a&&c+es<=n)return l[0]=f,l[1]=h,u-t;var p=t*Math.cos(a)+r,d=t*Math.sin(a)+e,g=t*Math.cos(n)+r,y=t*Math.sin(n)+e,m=(p-o)*(p-o)+(d-s)*(d-s),_=(g-o)*(g-o)+(y-s)*(y-s);return m<_?(l[0]=p,l[1]=d,Math.sqrt(m)):(l[0]=g,l[1]=y,Math.sqrt(_))}function Ff(r,e,t,a,n,i,o,s){var l=n-r,u=i-e,f=t-r,h=a-e,v=Math.sqrt(f*f+h*h);f/=v,h/=v;var c=l*f+u*h,p=c/v;s&&(p=Math.min(Math.max(p,0),1)),p*=v;var d=o[0]=r+p*f,g=o[1]=e+p*h;return Math.sqrt((d-n)*(d-n)+(g-i)*(g-i))}function aD(r,e,t,a,n,i,o){t<0&&(r=r+t,t=-t),a<0&&(e=e+a,a=-a);var s=r+t,l=e+a,u=o[0]=Math.min(Math.max(n,r),s),f=o[1]=Math.min(Math.max(i,e),l);return Math.sqrt((u-n)*(u-n)+(f-i)*(f-i))}var _r=[];function gB(r,e,t){var a=aD(e.x,e.y,e.width,e.height,r.x,r.y,_r);return t.set(_r[0],_r[1]),a}function yB(r,e,t){for(var a=0,n=0,i=0,o=0,s,l,u=1/0,f=e.data,h=r.x,v=r.y,c=0;c0){e=e/180*Math.PI,xr.fromArray(r[0]),Bt.fromArray(r[1]),$t.fromArray(r[2]),ft.sub(Wr,xr,Bt),ft.sub(Gr,$t,Bt);var t=Wr.len(),a=Gr.len();if(!(t<.001||a<.001)){Wr.scale(1/t),Gr.scale(1/a);var n=Wr.dot(Gr),i=Math.cos(e);if(i1&&ft.copy(De,$t),De.toArray(r[1])}}}}function mB(r,e,t){if(t<=180&&t>0){t=t/180*Math.PI,xr.fromArray(r[0]),Bt.fromArray(r[1]),$t.fromArray(r[2]),ft.sub(Wr,Bt,xr),ft.sub(Gr,$t,Bt);var a=Wr.len(),n=Gr.len();if(!(a<.001||n<.001)){Wr.scale(1/a),Gr.scale(1/n);var i=Wr.dot(e),o=Math.cos(t);if(i=l)ft.copy(De,$t);else{De.scaleAndAdd(Gr,s/Math.tan(Math.PI/2-f));var h=$t.x!==Bt.x?(De.x-Bt.x)/($t.x-Bt.x):(De.y-Bt.y)/($t.y-Bt.y);if(isNaN(h))return;h<0?ft.copy(De,Bt):h>1&&ft.copy(De,$t)}De.toArray(r[1])}}}}function nc(r,e,t,a){var n=t==="normal",i=n?r:r.ensureState(t);i.ignore=e;var o=a.get("smooth");o&&o===!0&&(o=.3),i.shape=i.shape||{},o>0&&(i.shape.smooth=o);var s=a.getModel("lineStyle").getLineStyle();n?r.useStyle(s):i.style=s}function _B(r,e){var t=e.smooth,a=e.points;if(a)if(r.moveTo(a[0][0],a[0][1]),t>0&&a.length>=3){var n=sa(a[0],a[1]),i=sa(a[1],a[2]);if(!n||!i){r.lineTo(a[1][0],a[1][1]),r.lineTo(a[2][0],a[2][1]);return}var o=Math.min(n,i)*t,s=Cs([],a[1],a[0],o/n),l=Cs([],a[1],a[2],o/i),u=Cs([],s,l,.5);r.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),r.bezierCurveTo(l[0],l[1],l[0],l[1],a[2][0],a[2][1])}else for(var f=1;f0&&i&&b(-f/o,0,o);var d=r[0],g=r[o-1],y,m;_(),y<0&&x(-y,.8),m<0&&x(m,.8),_(),S(y,m,1),S(m,y,-1),_(),y<0&&w(-y),m<0&&w(m);function _(){y=d.rect[e]-a,m=n-g.rect[e]-g.rect[t]}function S(T,A,D){if(T<0){var M=Math.min(A,-T);if(M>0){b(M*D,0,o);var I=M+T;I<0&&x(-I*D,1)}else x(-T*D,1)}}function b(T,A,D){T!==0&&(u=!0);for(var M=A;M0)for(var I=0;I0;I--){var E=D[I-1]*R;b(-E,I,o)}}}function w(T){var A=T<0?-1:1;T=Math.abs(T);for(var D=Math.ceil(T/(o-1)),M=0;M0?b(D,0,M+1):b(-D,o-M-1,o),T-=D,T<=0)return}return u}function SB(r,e,t,a){return oD(r,"x","width",e,t,a)}function sD(r,e,t,a){return oD(r,"y","height",e,t,a)}function lD(r){var e=[];r.sort(function(d,g){return g.priority-d.priority});var t=new ht(0,0,0,0);function a(d){if(!d.ignore){var g=d.ensureState("emphasis");g.ignore==null&&(g.ignore=!1)}d.ignore=!0}for(var n=0;n=0&&a.attr(i.oldLayoutSelect),vt(v,"emphasis")>=0&&a.attr(i.oldLayoutEmphasis)),Dt(a,u,t,l)}else if(a.attr(u),!Ao(a).valueAnimation){var h=ot(a.style.opacity,1);a.style.opacity=0,Vt(a,{style:{opacity:h}},t,l)}if(i.oldLayout=u,a.states.select){var c=i.oldLayoutSelect={};xu(c,u,bu),xu(c,a.states.select,bu)}if(a.states.emphasis){var p=i.oldLayoutEmphasis={};xu(p,u,bu),xu(p,a.states.emphasis,bu)}eA(a,l,f,t,t)}if(n&&!n.ignore&&!n.invisible){var i=wB(n),o=i.oldLayout,d={points:n.shape.points};o?(n.attr({shape:o}),Dt(n,{shape:d},t)):(n.setShape(d),n.style.strokePercent=0,Vt(n,{style:{strokePercent:1}},t)),i.oldLayout=d}},r}(),oc=Tt();function AB(r){r.registerUpdateLifecycle("series:beforeupdate",function(e,t,a){var n=oc(t).labelManager;n||(n=oc(t).labelManager=new TB),n.clearLabels()}),r.registerUpdateLifecycle("series:layoutlabels",function(e,t,a){var n=oc(t).labelManager;a.updatedSeries.forEach(function(i){n.addLabelsOfSeries(t.getViewOfSeriesModel(i))}),n.updateLayoutConfig(t),n.layout(t),n.processLabelsOverall()})}var sc=Math.sin,lc=Math.cos,uD=Math.PI,Mn=Math.PI*2,CB=180/uD,fD=function(){function r(){}return r.prototype.reset=function(e){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,e||4)},r.prototype.moveTo=function(e,t){this._add("M",e,t)},r.prototype.lineTo=function(e,t){this._add("L",e,t)},r.prototype.bezierCurveTo=function(e,t,a,n,i,o){this._add("C",e,t,a,n,i,o)},r.prototype.quadraticCurveTo=function(e,t,a,n){this._add("Q",e,t,a,n)},r.prototype.arc=function(e,t,a,n,i,o){this.ellipse(e,t,a,a,0,n,i,o)},r.prototype.ellipse=function(e,t,a,n,i,o,s,l){var u=s-o,f=!l,h=Math.abs(u),v=za(h-Mn)||(f?u>=Mn:-u>=Mn),c=u>0?u%Mn:u%Mn+Mn,p=!1;v?p=!0:za(h)?p=!1:p=c>=uD==!!f;var d=e+a*lc(o),g=t+n*sc(o);this._start&&this._add("M",d,g);var y=Math.round(i*CB);if(v){var m=1/this._p,_=(f?1:-1)*(Mn-m);this._add("A",a,n,y,1,+f,e+a*lc(o+_),t+n*sc(o+_)),m>.01&&this._add("A",a,n,y,0,+f,d,g)}else{var S=e+a*lc(s),b=t+n*sc(s);this._add("A",a,n,y,+p,+f,S,b)}},r.prototype.rect=function(e,t,a,n){this._add("M",e,t),this._add("l",a,0),this._add("l",0,n),this._add("l",-a,0),this._add("Z")},r.prototype.closePath=function(){this._d.length>0&&this._add("Z")},r.prototype._add=function(e,t,a,n,i,o,s,l,u){for(var f=[],h=this._p,v=1;v"}function OB(r){return""}function xy(r,e){e=e||{};var t=e.newline?` +`:"";function a(n){var i=n.children,o=n.tag,s=n.attrs,l=n.text;return kB(o,s)+(o!=="style"?Me(l):l||"")+(i?""+t+G(i,function(u){return a(u)}).join(t)+t:"")+OB(o)}return a(r)}function NB(r,e,t){t=t||{};var a=t.newline?` +`:"",n=" {"+a,i=a+"}",o=G(St(r),function(l){return l+n+G(St(r[l]),function(u){return u+":"+r[l][u]+";"}).join(a)+i}).join(a),s=G(St(e),function(l){return"@keyframes "+l+n+G(St(e[l]),function(u){return u+n+G(St(e[l][u]),function(f){var h=e[l][u][f];return f==="d"&&(h='path("'+h+'")'),f+":"+h+";"}).join(a)+i}).join(a)+i}).join(a);return!o&&!s?"":[""].join(a)}function _d(r){return{zrId:r,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function n1(r,e,t,a){return re("svg","root",{width:r,height:e,xmlns:hD,"xmlns:xlink":vD,version:"1.1",baseProfile:"full",viewBox:a?"0 0 "+r+" "+e:!1},t)}var BB=0;function pD(){return BB++}var i1={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},Pn="transform-origin";function VB(r,e,t){var a=V({},r.shape);V(a,e),r.buildPath(t,a);var n=new fD;return n.reset(qw(r)),t.rebuildPath(n,1),n.generateStr(),n.getStr()}function zB(r,e){var t=e.originX,a=e.originY;(t||a)&&(r[Pn]=t+"px "+a+"px")}var GB={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function dD(r,e){var t=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[t]=r,t}function FB(r,e,t){var a=r.shape.paths,n={},i,o;if(C(a,function(l){var u=_d(t.zrId);u.animation=!0,Wh(l,{},u,!0);var f=u.cssAnims,h=u.cssNodes,v=St(f),c=v.length;if(c){o=v[c-1];var p=f[o];for(var d in p){var g=p[d];n[d]=n[d]||{d:""},n[d].d+=g.d||""}for(var y in h){var m=h[y].animation;m.indexOf(o)>=0&&(i=m)}}}),!!i){e.d=!1;var s=dD(n,t);return i.replace(o,s)}}function o1(r){return U(r)?i1[r]?"cubic-bezier("+i1[r]+")":dg(r)?r:"":""}function Wh(r,e,t,a){var n=r.animators,i=n.length,o=[];if(r instanceof Ch){var s=FB(r,e,t);if(s)o.push(s);else if(!i)return}else if(!i)return;for(var l={},u=0;u0}).length){var pt=dD(w,t);return pt+" "+m[0]+" both"}}for(var g in l){var s=d(l[g]);s&&o.push(s)}if(o.length){var y=t.zrId+"-cls-"+pD();t.cssNodes["."+y]={animation:o.join(",")},e.class=y}}function HB(r,e,t){if(!r.ignore)if(r.isSilent()){var a={"pointer-events":"none"};s1(a,e,t)}else{var n=r.states.emphasis&&r.states.emphasis.style?r.states.emphasis.style:{},i=n.fill;if(!i){var o=r.style&&r.style.fill,s=r.states.select&&r.states.select.style&&r.states.select.style.fill,l=r.currentStates.indexOf("select")>=0&&s||o;l&&(i=mf(l))}var u=n.lineWidth;if(u){var f=!n.strokeNoScale&&r.transform?r.transform[0]:1;u=u/f}var a={cursor:"pointer"};i&&(a.fill=i),n.stroke&&(a.stroke=n.stroke),u&&(a["stroke-width"]=u),s1(a,e,t)}}function s1(r,e,t,a){var n=JSON.stringify(r),i=t.cssStyleCache[n];i||(i=t.zrId+"-cls-"+pD(),t.cssStyleCache[n]=i,t.cssNodes["."+i+":hover"]=r),e.class=e.class?e.class+" "+i:i}var ol=Math.round;function gD(r){return r&&U(r.src)}function yD(r){return r&&K(r.toDataURL)}function by(r,e,t,a){PB(function(n,i){var o=n==="fill"||n==="stroke";o&&Zw(i)?_D(e,r,n,a):o&&yg(i)?SD(t,r,n,a):o&&i==="none"?r[n]="transparent":r[n]=i},e,t,!1),qB(t,r,a)}function wy(r,e){var t=aT(e);t&&(t.each(function(a,n){a!=null&&(r[(a1+n).toLowerCase()]=a+"")}),e.isSilent()&&(r[a1+"silent"]="true"))}function l1(r){return za(r[0]-1)&&za(r[1])&&za(r[2])&&za(r[3]-1)}function WB(r){return za(r[4])&&za(r[5])}function Ty(r,e,t){if(e&&!(WB(e)&&l1(e))){var a=1e4;r.transform=l1(e)?"translate("+ol(e[4]*a)/a+" "+ol(e[5]*a)/a+")":u2(e)}}function u1(r,e,t){for(var a=r.points,n=[],i=0;i"u"){var g="Image width/height must been given explictly in svg-ssr renderer.";me(v,g),me(c,g)}else if(v==null||c==null){var y=function(D,M){if(D){var I=D.elm,L=v||M.width,P=c||M.height;D.tag==="pattern"&&(u?(P=1,L/=i.width):f&&(L=1,P/=i.height)),D.attrs.width=L,D.attrs.height=P,I&&(I.setAttribute("width",L),I.setAttribute("height",P))}},m=Cg(p,null,r,function(D){l||y(x,D),y(h,D)});m&&m.width&&m.height&&(v=v||m.width,c=c||m.height)}h=re("image","img",{href:p,width:v,height:c}),o.width=v,o.height=c}else n.svgElement&&(h=et(n.svgElement),o.width=n.svgWidth,o.height=n.svgHeight);if(h){var _,S;l?_=S=1:u?(S=1,_=o.width/i.width):f?(_=1,S=o.height/i.height):o.patternUnits="userSpaceOnUse",_!=null&&!isNaN(_)&&(o.width=_),S!=null&&!isNaN(S)&&(o.height=S);var b=Kw(n);b&&(o.patternTransform=b);var x=re("pattern","",o,[h]),w=xy(x),T=a.patternCache,A=T[w];A||(A=a.zrId+"-p"+a.patternIdx++,T[w]=A,o.id=A,x=a.defs[A]=re("pattern",A,o,[h])),e[t]=gh(A)}}function KB(r,e,t){var a=t.clipPathCache,n=t.defs,i=a[r.id];if(!i){i=t.zrId+"-c"+t.clipPathIdx++;var o={id:i};a[r.id]=i,n[i]=re("clipPath",i,o,[mD(r,t)])}e["clip-path"]=gh(i)}function v1(r){return document.createTextNode(r)}function Vn(r,e,t){r.insertBefore(e,t)}function c1(r,e){r.removeChild(e)}function p1(r,e){r.appendChild(e)}function xD(r){return r.parentNode}function bD(r){return r.nextSibling}function uc(r,e){r.textContent=e}var d1=58,jB=120,QB=re("","");function Sd(r){return r===void 0}function Vr(r){return r!==void 0}function JB(r,e,t){for(var a={},n=e;n<=t;++n){var i=r[n].key;i!==void 0&&(a[i]=n)}return a}function _s(r,e){var t=r.key===e.key,a=r.tag===e.tag;return a&&t}function sl(r){var e,t=r.children,a=r.tag;if(Vr(a)){var n=r.elm=cD(a);if(Ay(QB,r),z(t))for(e=0;ei?(p=t[l+1]==null?null:t[l+1].elm,wD(r,p,t,n,l)):Wf(r,e,a,i))}function Wi(r,e){var t=e.elm=r.elm,a=r.children,n=e.children;r!==e&&(Ay(r,e),Sd(e.text)?Vr(a)&&Vr(n)?a!==n&&tV(t,a,n):Vr(n)?(Vr(r.text)&&uc(t,""),wD(t,null,n,0,n.length-1)):Vr(a)?Wf(t,a,0,a.length-1):Vr(r.text)&&uc(t,""):r.text!==e.text&&(Vr(a)&&Wf(t,a,0,a.length-1),uc(t,e.text)))}function eV(r,e){if(_s(r,e))Wi(r,e);else{var t=r.elm,a=xD(t);sl(e),a!==null&&(Vn(a,e.elm,bD(t)),Wf(a,[r],0,0))}return e}var rV=0,aV=function(){function r(e,t,a){if(this.type="svg",this.refreshHover=g1(),this.configLayer=g1(),this.storage=t,this._opts=a=V({},a),this.root=e,this._id="zr"+rV++,this._oldVNode=n1(a.width,a.height),e&&!a.ssr){var n=this._viewport=document.createElement("div");n.style.cssText="position:relative;overflow:hidden";var i=this._svgDom=this._oldVNode.elm=cD("svg");Ay(null,this._oldVNode),n.appendChild(i),e.appendChild(n)}this.resize(a.width,a.height)}return r.prototype.getType=function(){return this.type},r.prototype.getViewportRoot=function(){return this._viewport},r.prototype.getViewportRootOffset=function(){var e=this.getViewportRoot();if(e)return{offsetLeft:e.offsetLeft||0,offsetTop:e.offsetTop||0}},r.prototype.getSvgDom=function(){return this._svgDom},r.prototype.refresh=function(){if(this.root){var e=this.renderToVNode({willUpdate:!0});e.attrs.style="position:absolute;left:0;top:0;user-select:none",eV(this._oldVNode,e),this._oldVNode=e}},r.prototype.renderOneToVNode=function(e){return h1(e,_d(this._id))},r.prototype.renderToVNode=function(e){e=e||{};var t=this.storage.getDisplayList(!0),a=this._width,n=this._height,i=_d(this._id);i.animation=e.animation,i.willUpdate=e.willUpdate,i.compress=e.compress,i.emphasis=e.emphasis;var o=[],s=this._bgVNode=nV(a,n,this._backgroundColor,i);s&&o.push(s);var l=e.compress?null:this._mainVNode=re("g","main",{},[]);this._paintList(t,i,l?l.children:o),l&&o.push(l);var u=G(St(i.defs),function(v){return i.defs[v]});if(u.length&&o.push(re("defs","defs",{},u)),e.animation){var f=NB(i.cssNodes,i.cssAnims,{newline:!0});if(f){var h=re("style","stl",{},[],f);o.push(h)}}return n1(a,n,o,e.useViewBox)},r.prototype.renderToString=function(e){return e=e||{},xy(this.renderToVNode({animation:ot(e.cssAnimation,!0),emphasis:ot(e.cssEmphasis,!0),willUpdate:!1,compress:!0,useViewBox:ot(e.useViewBox,!0)}),{newline:!0})},r.prototype.setBackgroundColor=function(e){this._backgroundColor=e},r.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},r.prototype._paintList=function(e,t,a){for(var n=e.length,i=[],o=0,s,l,u=0,f=0;f=0&&!(v&&l&&v[d]===l[d]);d--);for(var g=p-1;g>d;g--)o--,s=i[o-1];for(var y=d+1;y=s)}}for(var h=this.__startIndex;h15)break}}P.prevElClipPaths&&y.restore()};if(m)if(m.length===0)T=g.__endIndex;else for(var D=c.dpr,M=0;M0&&e>n[0]){for(l=0;le);l++);s=a[n[l]]}if(n.splice(l+1,0,e),a[e]=t,!t.virtual)if(s){var u=s.dom;u.nextSibling?o.insertBefore(t.dom,u.nextSibling):o.appendChild(t.dom)}else o.firstChild?o.insertBefore(t.dom,o.firstChild):o.appendChild(t.dom);t.painter||(t.painter=this)}},r.prototype.eachLayer=function(e,t){for(var a=this._zlevelList,n=0;n0?wu:0),this._needsManuallyCompositing),f.__builtin__||fh("ZLevel "+u+" has been used by unkown layer "+f.id),f!==i&&(f.__used=!0,f.__startIndex!==l&&(f.__dirty=!0),f.__startIndex=l,f.incremental?f.__drawIndex=-1:f.__drawIndex=l,t(l),i=f),n.__dirty&He&&!n.__inHover&&(f.__dirty=!0,f.incremental&&f.__drawIndex<0&&(f.__drawIndex=l))}t(l),this.eachBuiltinLayer(function(h,v){!h.__used&&h.getElementCount()>0&&(h.__dirty=!0,h.__startIndex=h.__endIndex=h.__drawIndex=0),h.__dirty&&h.__drawIndex<0&&(h.__drawIndex=h.__startIndex)})},r.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},r.prototype._clearLayer=function(e){e.clear()},r.prototype.setBackgroundColor=function(e){this._backgroundColor=e,C(this._layers,function(t){t.setUnpainted()})},r.prototype.configLayer=function(e,t){if(t){var a=this._layerConfig;a[e]?st(a[e],t,!0):a[e]=t;for(var n=0;n-1&&(u.style.stroke=u.style.fill,u.style.fill="#fff",u.style.lineWidth=2),a},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},e}(kt);function fo(r,e){var t=r.mapDimensionsAll("defaultedLabel"),a=t.length;if(a===1){var n=lo(r,e,t[0]);return n!=null?n+"":null}else if(a){for(var i=[],o=0;o=0&&a.push(e[i])}return a.join(" ")}var Rl=function(r){k(e,r);function e(t,a,n,i){var o=r.call(this)||this;return o.updateData(t,a,n,i),o}return e.prototype._createSymbol=function(t,a,n,i,o){this.removeAll();var s=Zt(t,-1,-1,2,2,null,o);s.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),s.drift=vV,this._symbolType=t,this.add(s)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){da(this.childAt(0))},e.prototype.downplay=function(){ga(this.childAt(0))},e.prototype.setZ=function(t,a){var n=this.childAt(0);n.zlevel=t,n.z=a},e.prototype.setDraggable=function(t,a){var n=this.childAt(0);n.draggable=t,n.cursor=!a&&t?"move":n.cursor},e.prototype.updateData=function(t,a,n,i){this.silent=!1;var o=t.getItemVisual(a,"symbol")||"circle",s=t.hostModel,l=e.getSymbolSize(t,a),u=o!==this._symbolType,f=i&&i.disableAnimation;if(u){var h=t.getItemVisual(a,"symbolKeepAspect");this._createSymbol(o,t,a,l,h)}else{var v=this.childAt(0);v.silent=!1;var c={scaleX:l[0]/2,scaleY:l[1]/2};f?v.attr(c):Dt(v,c,s,a),Ir(v)}if(this._updateCommon(t,a,l,n,i),u){var v=this.childAt(0);if(!f){var c={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:v.style.opacity}};v.scaleX=v.scaleY=0,v.style.opacity=0,Vt(v,c,s,a)}}f&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,a,n,i,o){var s=this.childAt(0),l=t.hostModel,u,f,h,v,c,p,d,g,y;if(i&&(u=i.emphasisItemStyle,f=i.blurItemStyle,h=i.selectItemStyle,v=i.focus,c=i.blurScope,d=i.labelStatesModels,g=i.hoverScale,y=i.cursorStyle,p=i.emphasisDisabled),!i||t.hasItemOption){var m=i&&i.itemModel?i.itemModel:t.getItemModel(a),_=m.getModel("emphasis");u=_.getModel("itemStyle").getItemStyle(),h=m.getModel(["select","itemStyle"]).getItemStyle(),f=m.getModel(["blur","itemStyle"]).getItemStyle(),v=_.get("focus"),c=_.get("blurScope"),p=_.get("disabled"),d=ne(m),g=_.getShallow("scale"),y=m.getShallow("cursor")}var S=t.getItemVisual(a,"symbolRotate");s.attr("rotation",(S||0)*Math.PI/180||0);var b=di(t.getItemVisual(a,"symbolOffset"),n);b&&(s.x=b[0],s.y=b[1]),y&&s.attr("cursor",y);var x=t.getItemVisual(a,"style"),w=x.fill;if(s instanceof oe){var T=s.style;s.useStyle(V({image:T.image,x:T.x,y:T.y,width:T.width,height:T.height},x))}else s.__isEmptyBrush?s.useStyle(V({},x)):s.useStyle(x),s.style.decal=null,s.setColor(w,o&&o.symbolInnerColor),s.style.strokeNoScale=!0;var A=t.getItemVisual(a,"liftZ"),D=this._z2;A!=null?D==null&&(this._z2=s.z2,s.z2+=A):D!=null&&(s.z2=D,this._z2=null);var M=o&&o.useNameLabel;ve(s,d,{labelFetcher:l,labelDataIndex:a,defaultText:I,inheritColor:w,defaultOpacity:x.opacity});function I(R){return M?t.getName(R):fo(t,R)}this._sizeX=n[0]/2,this._sizeY=n[1]/2;var L=s.ensureState("emphasis");L.style=u,s.ensureState("select").style=h,s.ensureState("blur").style=f;var P=g==null||g===!0?Math.max(1.1,3/this._sizeY):isFinite(g)&&g>0?+g:1;L.scaleX=this._sizeX*P,L.scaleY=this._sizeY*P,this.setSymbolScale(1),Wt(this,v,c,p)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,a,n){var i=this.childAt(0),o=nt(this).dataIndex,s=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var l=i.getTextContent();l&&Qa(l,{style:{opacity:0}},a,{dataIndex:o,removeOpt:s,cb:function(){i.removeTextContent()}})}else i.removeTextContent();Qa(i,{style:{opacity:0},scaleX:0,scaleY:0},a,{dataIndex:o,cb:t,removeOpt:s})},e.getSymbolSize=function(t,a){return Ro(t.getItemVisual(a,"symbolSize"))},e}(at);function vV(r,e){this.parent.drift(r,e)}function hc(r,e,t,a){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(a.isIgnore&&a.isIgnore(t))&&!(a.clipShape&&!a.clipShape.contain(e[0],e[1]))&&r.getItemVisual(t,"symbol")!=="none"}function _1(r){return r!=null&&!tt(r)&&(r={isIgnore:r}),r||{}}function S1(r){var e=r.hostModel,t=e.getModel("emphasis");return{emphasisItemStyle:t.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:t.get("focus"),blurScope:t.get("blurScope"),emphasisDisabled:t.get("disabled"),hoverScale:t.get("scale"),labelStatesModels:ne(e),cursorStyle:e.get("cursor")}}var El=function(){function r(e){this.group=new at,this._SymbolCtor=e||Rl}return r.prototype.updateData=function(e,t){this._progressiveEls=null,t=_1(t);var a=this.group,n=e.hostModel,i=this._data,o=this._SymbolCtor,s=t.disableAnimation,l=S1(e),u={disableAnimation:s},f=t.getSymbolPoint||function(h){return e.getItemLayout(h)};i||a.removeAll(),e.diff(i).add(function(h){var v=f(h);if(hc(e,v,h,t)){var c=new o(e,h,l,u);c.setPosition(v),e.setItemGraphicEl(h,c),a.add(c)}}).update(function(h,v){var c=i.getItemGraphicEl(v),p=f(h);if(!hc(e,p,h,t)){a.remove(c);return}var d=e.getItemVisual(h,"symbol")||"circle",g=c&&c.getSymbolType&&c.getSymbolType();if(!c||g&&g!==d)a.remove(c),c=new o(e,h,l,u),c.setPosition(p);else{c.updateData(e,h,l,u);var y={x:p[0],y:p[1]};s?c.attr(y):Dt(c,y,n)}a.add(c),e.setItemGraphicEl(h,c)}).remove(function(h){var v=i.getItemGraphicEl(h);v&&v.fadeOut(function(){a.remove(v)},n)}).execute(),this._getSymbolPoint=f,this._data=e},r.prototype.updateLayout=function(){var e=this,t=this._data;t&&t.eachItemGraphicEl(function(a,n){var i=e._getSymbolPoint(n);a.setPosition(i),a.markRedraw()})},r.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=S1(e),this._data=null,this.group.removeAll()},r.prototype.incrementalUpdate=function(e,t,a){this._progressiveEls=[],a=_1(a);function n(l){l.isGroup||(l.incremental=!0,l.ensureState("emphasis").hoverLayer=!0)}for(var i=e.start;i0?t=a[0]:a[1]<0&&(t=a[1]),t}function CD(r,e,t,a){var n=NaN;r.stacked&&(n=t.get(t.getCalculationInfo("stackedOverDimension"),a)),isNaN(n)&&(n=r.valueStart);var i=r.baseDataOffset,o=[];return o[i]=t.get(r.baseDim,a),o[1-i]=n,e.dataToPoint(o)}function pV(r,e){var t=[];return e.diff(r).add(function(a){t.push({cmd:"+",idx:a})}).update(function(a,n){t.push({cmd:"=",idx:n,idx1:a})}).remove(function(a){t.push({cmd:"-",idx:a})}).execute(),t}function dV(r,e,t,a,n,i,o,s){for(var l=pV(r,e),u=[],f=[],h=[],v=[],c=[],p=[],d=[],g=AD(n,e,o),y=r.getLayout("points")||[],m=e.getLayout("points")||[],_=0;_=n||d<0)break;if(ti(y,m)){if(l){d+=i;continue}break}if(d===t)r[i>0?"moveTo":"lineTo"](y,m),h=y,v=m;else{var _=y-u,S=m-f;if(_*_+S*S<.5){d+=i;continue}if(o>0){for(var b=d+i,x=e[b*2],w=e[b*2+1];x===y&&w===m&&g=a||ti(x,w))c=y,p=m;else{D=x-u,M=w-f;var P=y-u,R=x-y,E=m-f,N=w-m,O=void 0,B=void 0;if(s==="x"){O=Math.abs(P),B=Math.abs(R);var F=D>0?1:-1;c=y-F*O*o,p=m,I=y+F*B*o,L=m}else if(s==="y"){O=Math.abs(E),B=Math.abs(N);var H=M>0?1:-1;c=y,p=m-H*O*o,I=y,L=m+H*B*o}else O=Math.sqrt(P*P+E*E),B=Math.sqrt(R*R+N*N),A=B/(B+O),c=y-D*o*(1-A),p=m-M*o*(1-A),I=y+D*o*A,L=m+M*o*A,I=Ia(I,La(x,y)),L=Ia(L,La(w,m)),I=La(I,Ia(x,y)),L=La(L,Ia(w,m)),D=I-y,M=L-m,c=y-D*O/B,p=m-M*O/B,c=Ia(c,La(u,y)),p=Ia(p,La(f,m)),c=La(c,Ia(u,y)),p=La(p,Ia(f,m)),D=y-c,M=m-p,I=y+D*B/O,L=m+M*B/O}r.bezierCurveTo(h,v,c,p,y,m),h=I,v=L}else r.lineTo(y,m)}u=y,f=m,d+=i}return g}var DD=function(){function r(){this.smooth=0,this.smoothConstraint=!0}return r}(),gV=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="ec-polyline",a}return e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new DD},e.prototype.buildPath=function(t,a){var n=a.points,i=0,o=n.length/2;if(a.connectNulls){for(;o>0&&ti(n[o*2-2],n[o*2-1]);o--);for(;i=0){var S=u?(p-l)*_+l:(c-s)*_+s;return u?[t,S]:[S,t]}s=c,l=p;break;case o.C:c=i[h++],p=i[h++],d=i[h++],g=i[h++],y=i[h++],m=i[h++];var b=u?gf(s,c,d,y,t,f):gf(l,p,g,m,t,f);if(b>0)for(var x=0;x=0){var S=u?te(l,p,g,m,w):te(s,c,d,y,w);return u?[t,S]:[S,t]}}s=y,l=m;break}}},e}(yt),yV=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e}(DD),MD=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="ec-polygon",a}return e.prototype.getDefaultShape=function(){return new yV},e.prototype.buildPath=function(t,a){var n=a.points,i=a.stackedOnPoints,o=0,s=n.length/2,l=a.smoothMonotone;if(a.connectNulls){for(;s>0&&ti(n[s*2-2],n[s*2-1]);s--);for(;oe){i?t.push(o(i,l,e)):n&&t.push(o(n,l,0),o(n,l,e));break}else n&&(t.push(o(n,l,0)),n=null),t.push(l),i=l}return t}function SV(r,e,t){var a=r.getVisual("visualMeta");if(!(!a||!a.length||!r.count())&&e.type==="cartesian2d"){for(var n,i,o=a.length-1;o>=0;o--){var s=r.getDimensionInfo(a[o].dimension);if(n=s&&s.coordDim,n==="x"||n==="y"){i=a[o];break}}if(i){var l=e.getAxis(n),u=G(i.stops,function(_){return{coord:l.toGlobalCoord(l.dataToCoord(_.value)),color:_.color}}),f=u.length,h=i.outerColors.slice();f&&u[0].coord>u[f-1].coord&&(u.reverse(),h.reverse());var v=_V(u,n==="x"?t.getWidth():t.getHeight()),c=v.length;if(!c&&f)return u[0].coord<0?h[1]?h[1]:u[f-1].color:h[0]?h[0]:u[0].color;var p=10,d=v[0].coord-p,g=v[c-1].coord+p,y=g-d;if(y<.001)return"transparent";C(v,function(_){_.offset=(_.coord-d)/y}),v.push({offset:c?v[c-1].offset:.5,color:h[1]||"transparent"}),v.unshift({offset:c?v[0].offset:.5,color:h[0]||"transparent"});var m=new xo(0,0,0,0,v,!0);return m[n]=d,m[n+"2"]=g,m}}}function xV(r,e,t){var a=r.get("showAllSymbol"),n=a==="auto";if(!(a&&!n)){var i=t.getAxesByScale("ordinal")[0];if(i&&!(n&&bV(i,e))){var o=e.mapDimension(i.dim),s={};return C(i.getViewLabels(),function(l){var u=i.scale.getRawOrdinalNumber(l.tickValue);s[u]=1}),function(l){return!s.hasOwnProperty(e.get(o,l))}}}}function bV(r,e){var t=r.getExtent(),a=Math.abs(t[1]-t[0])/r.scale.count();isNaN(a)&&(a=0);for(var n=e.count(),i=Math.max(1,Math.round(n/5)),o=0;oa)return!1;return!0}function wV(r,e){return isNaN(r)||isNaN(e)}function TV(r){for(var e=r.length/2;e>0&&wV(r[e*2-2],r[e*2-1]);e--);return e-1}function A1(r,e){return[r[e*2],r[e*2+1]]}function AV(r,e,t){for(var a=r.length/2,n=t==="x"?0:1,i,o,s=0,l=-1,u=0;u=e||i>=e&&o<=e){l=u;break}s=u,i=o}return{range:[s,l],t:(e-i)/(o-i)}}function PD(r){if(r.get(["endLabel","show"]))return!0;for(var e=0;e0&&t.get(["emphasis","lineStyle","width"])==="bolder"){var F=d.getState("emphasis").style;F.lineWidth=+d.style.lineWidth+1}nt(d).seriesIndex=t.seriesIndex,Wt(d,N,O,B);var H=T1(t.get("smooth")),Y=t.get("smoothMonotone");if(d.setShape({smooth:H,smoothMonotone:Y,connectNulls:T}),g){var j=l.getCalculationInfo("stackedOnSeries"),J=0;g.useStyle(Q(f.getAreaStyle(),{fill:L,opacity:.7,lineJoin:"bevel",decal:l.getVisual("style").decal})),j&&(J=T1(j.get("smooth"))),g.setShape({smooth:H,stackedOnSmooth:J,smoothMonotone:Y,connectNulls:T}),he(g,t,"areaStyle"),nt(g).seriesIndex=t.seriesIndex,Wt(g,N,O,B)}var ct=function(xt){i._changePolyState(xt)};l.eachItemGraphicEl(function(xt){xt&&(xt.onHoverStateChange=ct)}),this._polyline.onHoverStateChange=ct,this._data=l,this._coordSys=o,this._stackedOnPoints=x,this._points=h,this._step=M,this._valueOrigin=S,t.get("triggerLineEvent")&&(this.packEventData(t,d),g&&this.packEventData(t,g))},e.prototype.packEventData=function(t,a){nt(a).eventData={componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line"}},e.prototype.highlight=function(t,a,n,i){var o=t.getData(),s=ni(o,i);if(this._changePolyState("emphasis"),!(s instanceof Array)&&s!=null&&s>=0){var l=o.getLayout("points"),u=o.getItemGraphicEl(s);if(!u){var f=l[s*2],h=l[s*2+1];if(isNaN(f)||isNaN(h)||this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(f,h))return;var v=t.get("zlevel")||0,c=t.get("z")||0;u=new Rl(o,s),u.x=f,u.y=h,u.setZ(v,c);var p=u.getSymbolPath().getTextContent();p&&(p.zlevel=v,p.z=c,p.z2=this._polyline.z2+1),u.__temp=!0,o.setItemGraphicEl(s,u),u.stopSymbolAnimation(!0),this.group.add(u)}u.highlight()}else Rt.prototype.highlight.call(this,t,a,n,i)},e.prototype.downplay=function(t,a,n,i){var o=t.getData(),s=ni(o,i);if(this._changePolyState("normal"),s!=null&&s>=0){var l=o.getItemGraphicEl(s);l&&(l.__temp?(o.setItemGraphicEl(s,null),this.group.remove(l)):l.downplay())}else Rt.prototype.downplay.call(this,t,a,n,i)},e.prototype._changePolyState=function(t){var a=this._polygon;wf(this._polyline,t),a&&wf(a,t)},e.prototype._newPolyline=function(t){var a=this._polyline;return a&&this._lineGroup.remove(a),a=new gV({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(a),this._polyline=a,a},e.prototype._newPolygon=function(t,a){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new MD({shape:{points:t,stackedOnPoints:a},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,a,n){var i,o,s=a.getBaseAxis(),l=s.inverse;a.type==="cartesian2d"?(i=s.isHorizontal(),o=!1):a.type==="polar"&&(i=s.dim==="angle",o=!0);var u=t.hostModel,f=u.get("animationDuration");K(f)&&(f=f(null));var h=u.get("animationDelay")||0,v=K(h)?h(null):h;t.eachItemGraphicEl(function(c,p){var d=c;if(d){var g=[c.x,c.y],y=void 0,m=void 0,_=void 0;if(n)if(o){var S=n,b=a.pointToCoord(g);i?(y=S.startAngle,m=S.endAngle,_=-b[1]/180*Math.PI):(y=S.r0,m=S.r,_=b[0])}else{var x=n;i?(y=x.x,m=x.x+x.width,_=c.x):(y=x.y+x.height,m=x.y,_=c.y)}var w=m===y?0:(_-y)/(m-y);l&&(w=1-w);var T=K(h)?h(p):f*w+v,A=d.getSymbolPath(),D=A.getTextContent();d.attr({scaleX:0,scaleY:0}),d.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:T}),D&&D.animateFrom({style:{opacity:0}},{duration:300,delay:T}),A.disableLabelAnimation=!0}})},e.prototype._initOrUpdateEndLabel=function(t,a,n){var i=t.getModel("endLabel");if(PD(t)){var o=t.getData(),s=this._polyline,l=o.getLayout("points");if(!l){s.removeTextContent(),this._endLabel=null;return}var u=this._endLabel;u||(u=this._endLabel=new bt({z2:200}),u.ignoreClip=!0,s.setTextContent(this._endLabel),s.disableLabelAnimation=!0);var f=TV(l);f>=0&&(ve(s,ne(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:f,defaultText:function(h,v,c){return c!=null?TD(o,c):fo(o,h)},enableTextSetter:!0},CV(i,a)),s.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,a,n,i,o,s,l){var u=this._endLabel,f=this._polyline;if(u){t<1&&i.originalX==null&&(i.originalX=u.x,i.originalY=u.y);var h=n.getLayout("points"),v=n.hostModel,c=v.get("connectNulls"),p=s.get("precision"),d=s.get("distance")||0,g=l.getBaseAxis(),y=g.isHorizontal(),m=g.inverse,_=a.shape,S=m?y?_.x:_.y+_.height:y?_.x+_.width:_.y,b=(y?d:0)*(m?-1:1),x=(y?0:-d)*(m?-1:1),w=y?"x":"y",T=AV(h,S,w),A=T.range,D=A[1]-A[0],M=void 0;if(D>=1){if(D>1&&!c){var I=A1(h,A[0]);u.attr({x:I[0]+b,y:I[1]+x}),o&&(M=v.getRawValue(A[0]))}else{var I=f.getPointOn(S,w);I&&u.attr({x:I[0]+b,y:I[1]+x});var L=v.getRawValue(A[0]),P=v.getRawValue(A[1]);o&&(M=gT(n,p,L,P,T.t))}i.lastFrameIndex=A[0]}else{var R=t===1||i.lastFrameIndex>0?A[0]:0,I=A1(h,R);o&&(M=v.getRawValue(R)),u.attr({x:I[0]+b,y:I[1]+x})}if(o){var E=Ao(u);typeof E.setLabelText=="function"&&E.setLabelText(M)}}},e.prototype._doUpdateAnimation=function(t,a,n,i,o,s,l){var u=this._polyline,f=this._polygon,h=t.hostModel,v=dV(this._data,t,this._stackedOnPoints,a,this._coordSys,n,this._valueOrigin),c=v.current,p=v.stackedOnCurrent,d=v.next,g=v.stackedOnNext;if(o&&(c=Pa(v.current,n,o,l),p=Pa(v.stackedOnCurrent,n,o,l),d=Pa(v.next,n,o,l),g=Pa(v.stackedOnNext,n,o,l)),w1(c,d)>3e3||f&&w1(p,g)>3e3){u.stopAnimation(),u.setShape({points:d}),f&&(f.stopAnimation(),f.setShape({points:d,stackedOnPoints:g}));return}u.shape.__points=v.current,u.shape.points=c;var y={shape:{points:d}};v.current!==c&&(y.shape.__points=v.next),u.stopAnimation(),Dt(u,y,h),f&&(f.setShape({points:c,stackedOnPoints:p}),f.stopAnimation(),Dt(f,{shape:{stackedOnPoints:g}},h),u.shape.points!==f.shape.points&&(f.shape.points=u.shape.points));for(var m=[],_=v.status,S=0;S<_.length;S++){var b=_[S].cmd;if(b==="="){var x=t.getItemGraphicEl(_[S].idx1);x&&m.push({el:x,ptIdx:S})}}u.animators&&u.animators.length&&u.animators[0].during(function(){f&&f.dirtyShape();for(var w=u.shape.__points,T=0;Te&&(e=r[t]);return isFinite(e)?e:NaN},min:function(r){for(var e=1/0,t=0;te&&(e=i,t=n)}return isFinite(t)?t:NaN},nearest:function(r){return r[0]}},IV=function(r){return Math.round(r.length/2)};function RD(r){return{seriesType:r,reset:function(e,t,a){var n=e.getData(),i=e.get("sampling"),o=e.coordinateSystem,s=n.count();if(s>10&&o.type==="cartesian2d"&&i){var l=o.getBaseAxis(),u=o.getOtherAxis(l),f=l.getExtent(),h=a.getDevicePixelRatio(),v=Math.abs(f[1]-f[0])*(h||1),c=Math.round(s/v);if(isFinite(c)&&c>1){i==="lttb"&&e.setData(n.lttbDownSample(n.mapDimension(u.dim),1/c));var p=void 0;U(i)?p=MV[i]:K(i)&&(p=i),p&&e.setData(n.downSample(n.mapDimension(u.dim),1/c,p,IV))}}}}}function LV(r){r.registerChartView(DV),r.registerSeriesModel(hV),r.registerLayout(Ol("line",!0)),r.registerVisual({seriesType:"line",reset:function(e){var t=e.getData(),a=e.getModel("lineStyle").getLineStyle();a&&!a.stroke&&(a.stroke=t.getVisual("style").fill),t.setVisual("legendLineStyle",a)}}),r.registerProcessor(r.PRIORITY.PROCESSOR.STATISTIC,RD("line"))}var ll=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.getInitialData=function(t,a){return Jr(null,this,{useEncodeDefaulter:!0})},e.prototype.getMarkerPosition=function(t,a,n){var i=this.coordinateSystem;if(i&&i.clampData){var o=i.clampData(t),s=i.dataToPoint(o);if(n)C(i.getAxes(),function(v,c){if(v.type==="category"&&a!=null){var p=v.getTicksCoords(),d=v.getTickModel().get("alignWithLabel"),g=o[c],y=a[c]==="x1"||a[c]==="y1";if(y&&!d&&(g+=1),p.length<2)return;if(p.length===2){s[c]=v.toGlobalCoord(v.getExtent()[y?1:0]);return}for(var m=void 0,_=void 0,S=1,b=0;bg){_=(x+m)/2;break}b===1&&(S=w-p[0].tickValue)}_==null&&(m?m&&(_=p[p.length-1].coord):_=p[0].coord),s[c]=v.toGlobalCoord(_)}});else{var l=this.getData(),u=l.getLayout("offset"),f=l.getLayout("size"),h=i.getBaseAxis().isHorizontal()?0:1;s[h]+=u+f/2}return s}return[NaN,NaN]},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},e}(kt);kt.registerClass(ll);var PV=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.getInitialData=function(){return Jr(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return this.get("large")?this.get("progressive"):!1},e.prototype.getProgressiveThreshold=function(){var t=this.get("progressiveThreshold"),a=this.get("largeThreshold");return a>t&&(t=a),t},e.prototype.brushSelector=function(t,a,n){return n.rect(a.getItemLayout(t))},e.type="series.bar",e.dependencies=["grid","polar"],e.defaultOption=on(ll.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),e}(ll),RV=function(){function r(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=Math.PI*2,this.clockwise=!0}return r}(),Uf=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="sausage",a}return e.prototype.getDefaultShape=function(){return new RV},e.prototype.buildPath=function(t,a){var n=a.cx,i=a.cy,o=Math.max(a.r0||0,0),s=Math.max(a.r,0),l=(s-o)*.5,u=o+l,f=a.startAngle,h=a.endAngle,v=a.clockwise,c=Math.PI*2,p=v?h-fMath.PI/2&&fs)return!0;s=h}return!1},e.prototype._isOrderDifferentInView=function(t,a){for(var n=a.scale,i=n.getExtent(),o=Math.max(0,i[0]),s=Math.min(i[1],n.getOrdinalMeta().categories.length-1);o<=s;++o)if(t.ordinalNumbers[o]!==n.getRawOrdinalNumber(o))return!0},e.prototype._updateSortWithinSameData=function(t,a,n,i){if(this._isOrderChangedWithinSameData(t,a,n)){var o=this._dataSort(t,n,a);this._isOrderDifferentInView(o,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:o}))}},e.prototype._dispatchInitSort=function(t,a,n){var i=a.baseAxis,o=this._dataSort(t,i,function(s){return t.get(t.mapDimension(a.otherAxis.dim),s)});n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:o})},e.prototype.remove=function(t,a){this._clear(this._model),this._removeOnRenderedListener(a)},e.prototype.dispose=function(t,a){this._removeOnRenderedListener(a)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var a=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl(function(i){Qs(i,t,nt(i).dataIndex)})):a.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type="bar",e}(Rt),C1={cartesian2d:function(r,e){var t=e.width<0?-1:1,a=e.height<0?-1:1;t<0&&(e.x+=e.width,e.width=-e.width),a<0&&(e.y+=e.height,e.height=-e.height);var n=r.x+r.width,i=r.y+r.height,o=cc(e.x,r.x),s=pc(e.x+e.width,n),l=cc(e.y,r.y),u=pc(e.y+e.height,i),f=sn?s:o,e.y=h&&l>i?u:l,e.width=f?0:s-o,e.height=h?0:u-l,t<0&&(e.x+=e.width,e.width=-e.width),a<0&&(e.y+=e.height,e.height=-e.height),f||h},polar:function(r,e){var t=e.r0<=e.r?1:-1;if(t<0){var a=e.r;e.r=e.r0,e.r0=a}var n=pc(e.r,r.r),i=cc(e.r0,r.r0);e.r=n,e.r0=i;var o=n-i<0;if(t<0){var a=e.r;e.r=e.r0,e.r0=a}return o}},D1={cartesian2d:function(r,e,t,a,n,i,o,s,l){var u=new wt({shape:V({},a),z2:1});if(u.__dataIndex=t,u.name="item",i){var f=u.shape,h=n?"height":"width";f[h]=0}return u},polar:function(r,e,t,a,n,i,o,s,l){var u=!n&&l?Uf:_e,f=new u({shape:a,z2:1});f.name="item";var h=ED(n);if(f.calculateTextPosition=EV(h,{isRoundCap:u===Uf}),i){var v=f.shape,c=n?"r":"endAngle",p={};v[c]=n?a.r0:a.startAngle,p[c]=a[c],(s?Dt:Vt)(f,{shape:p},i)}return f}};function BV(r,e){var t=r.get("realtimeSort",!0),a=e.getBaseAxis();if(t&&a.type==="category"&&e.type==="cartesian2d")return{baseAxis:a,otherAxis:e.getOtherAxis(a)}}function M1(r,e,t,a,n,i,o,s){var l,u;i?(u={x:a.x,width:a.width},l={y:a.y,height:a.height}):(u={y:a.y,height:a.height},l={x:a.x,width:a.width}),s||(o?Dt:Vt)(t,{shape:l},e,n,null);var f=e?r.baseAxis.model:null;(o?Dt:Vt)(t,{shape:u},f,n)}function I1(r,e){for(var t=0;t0?1:-1,o=a.height>0?1:-1;return{x:a.x+i*n/2,y:a.y+o*n/2,width:a.width-i*n,height:a.height-o*n}},polar:function(r,e,t){var a=r.getItemLayout(e);return{cx:a.cx,cy:a.cy,r0:a.r0,r:a.r,startAngle:a.startAngle,endAngle:a.endAngle,clockwise:a.clockwise}}};function GV(r){return r.startAngle!=null&&r.endAngle!=null&&r.startAngle===r.endAngle}function ED(r){return function(e){var t=e?"Arc":"Angle";return function(a){switch(a){case"start":case"insideStart":case"end":case"insideEnd":return a+t;default:return a}}}(r)}function P1(r,e,t,a,n,i,o,s){var l=e.getItemVisual(t,"style");if(s){if(!i.get("roundCap")){var f=r.shape,h=Zn(a.getModel("itemStyle"),f,!0);V(f,h),r.setShape(f)}}else{var u=a.get(["itemStyle","borderRadius"])||0;r.setShape("r",u)}r.useStyle(l);var v=a.getShallow("cursor");v&&r.attr("cursor",v);var c=s?o?n.r>=n.r0?"endArc":"startArc":n.endAngle>=n.startAngle?"endAngle":"startAngle":o?n.height>=0?"bottom":"top":n.width>=0?"right":"left",p=ne(a);ve(r,p,{labelFetcher:i,labelDataIndex:t,defaultText:fo(i.getData(),t),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:c});var d=r.getTextContent();if(s&&d){var g=a.get(["label","position"]);r.textConfig.inside=g==="middle"?!0:null,kV(r,g==="outside"?c:g,ED(o),a.get(["label","rotate"]))}tA(d,p,i.getRawValue(t),function(m){return TD(e,m)});var y=a.getModel(["emphasis"]);Wt(r,y.get("focus"),y.get("blurScope"),y.get("disabled")),he(r,a),GV(n)&&(r.style.fill="none",r.style.stroke="none",C(r.states,function(m){m.style&&(m.style.fill=m.style.stroke="none")}))}function FV(r,e){var t=r.get(["itemStyle","borderColor"]);if(!t||t==="none")return 0;var a=r.get(["itemStyle","borderWidth"])||0,n=isNaN(e.width)?Number.MAX_VALUE:Math.abs(e.width),i=isNaN(e.height)?Number.MAX_VALUE:Math.abs(e.height);return Math.min(a,n,i)}var HV=function(){function r(){}return r}(),R1=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="largeBar",a}return e.prototype.getDefaultShape=function(){return new HV},e.prototype.buildPath=function(t,a){for(var n=a.points,i=this.baseDimIdx,o=1-this.baseDimIdx,s=[],l=[],u=this.barWidth,f=0;f=0?t:null},30,!1);function WV(r,e,t){for(var a=r.baseDimIdx,n=1-a,i=r.shape.points,o=r.largeDataIndices,s=[],l=[],u=r.barWidth,f=0,h=i.length/3;f=s[0]&&e<=s[0]+l[0]&&t>=s[1]&&t<=s[1]+l[1])return o[f]}return-1}function kD(r,e,t){if(yi(t,"cartesian2d")){var a=e,n=t.getArea();return{x:r?a.x:n.x,y:r?n.y:a.y,width:r?a.width:n.width,height:r?n.height:a.height}}else{var n=t.getArea(),i=e;return{cx:n.cx,cy:n.cy,r0:r?n.r0:i.r0,r:r?n.r:i.r,startAngle:r?i.startAngle:0,endAngle:r?i.endAngle:Math.PI*2}}}function UV(r,e,t){var a=r.type==="polar"?_e:wt;return new a({shape:kD(e,t,r),silent:!0,z2:0})}function YV(r){r.registerChartView(NV),r.registerSeriesModel(PV),r.registerLayout(r.PRIORITY.VISUAL.LAYOUT,it(zC,"bar")),r.registerLayout(r.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,GC("bar")),r.registerProcessor(r.PRIORITY.PROCESSOR.STATISTIC,RD("bar")),r.registerAction({type:"changeAxisOrder",event:"changeAxisOrder",update:"update"},function(e,t){var a=e.componentType||"series";t.eachComponent({mainType:a,query:e},function(n){e.sortInfo&&n.axis.setCategorySortInfo(e.sortInfo)})})}var O1=Math.PI*2,Du=Math.PI/180;function OD(r,e){return jt(r.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function ND(r,e){var t=OD(r,e),a=r.get("center"),n=r.get("radius");z(n)||(n=[0,n]);var i=W(t.width,e.getWidth()),o=W(t.height,e.getHeight()),s=Math.min(i,o),l=W(n[0],s/2),u=W(n[1],s/2),f,h,v=r.coordinateSystem;if(v){var c=v.dataToPoint(a);f=c[0]||0,h=c[1]||0}else z(a)||(a=[a,a]),f=W(a[0],i)+t.x,h=W(a[1],o)+t.y;return{cx:f,cy:h,r0:l,r:u}}function XV(r,e,t){e.eachSeriesByType(r,function(a){var n=a.getData(),i=n.mapDimension("value"),o=OD(a,t),s=ND(a,t),l=s.cx,u=s.cy,f=s.r,h=s.r0,v=-a.get("startAngle")*Du,c=a.get("endAngle"),p=a.get("padAngle")*Du;c=c==="auto"?v-O1:-c*Du;var d=a.get("minAngle")*Du,g=d+p,y=0;n.each(i,function(N){!isNaN(N)&&y++});var m=n.getSum(i),_=Math.PI/(m||y)*2,S=a.get("clockwise"),b=a.get("roseType"),x=a.get("stillShowZeroSum"),w=n.getDataExtent(i);w[0]=0;var T=S?1:-1,A=[v,c],D=T*p/2;Dg(A,!S),v=A[0],c=A[1];var M=BD(a);M.startAngle=v,M.endAngle=c,M.clockwise=S;var I=Math.abs(c-v),L=I,P=0,R=v;if(n.setLayout({viewRect:o,r:f}),n.each(i,function(N,O){var B;if(isNaN(N)){n.setItemLayout(O,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:S,cx:l,cy:u,r0:h,r:b?NaN:f});return}b!=="area"?B=m===0&&x?_:N*_:B=I/y,BB?(H=R+T*B/2,Y=H):(H=R+D,Y=F-D),n.setItemLayout(O,{angle:B,startAngle:H,endAngle:Y,clockwise:S,cx:l,cy:u,r0:h,r:b?Lt(N,w,[h,f]):f}),R=F}),Lt?y:g,b=Math.abs(_.label.y-t);if(b>=S.maxY){var x=_.label.x-e-_.len2*n,w=a+_.len,T=Math.abs(x)r.unconstrainedWidth?null:c:null;a.setStyle("width",p)}var d=a.getBoundingRect();i.width=d.width;var g=(a.style.margin||0)+2.1;i.height=d.height+g,i.y-=(i.height-h)/2}}}function dc(r){return r.position==="center"}function qV(r){var e=r.getData(),t=[],a,n,i=!1,o=(r.get("minShowLabelAngle")||0)*$V,s=e.getLayout("viewRect"),l=e.getLayout("r"),u=s.width,f=s.x,h=s.y,v=s.height;function c(x){x.ignore=!0}function p(x){if(!x.ignore)return!0;for(var w in x.states)if(x.states[w].ignore===!1)return!0;return!1}e.each(function(x){var w=e.getItemGraphicEl(x),T=w.shape,A=w.getTextContent(),D=w.getTextGuideLine(),M=e.getItemModel(x),I=M.getModel("label"),L=I.get("position")||M.get(["emphasis","label","position"]),P=I.get("distanceToLabelLine"),R=I.get("alignTo"),E=W(I.get("edgeDistance"),u),N=I.get("bleedMargin"),O=M.getModel("labelLine"),B=O.get("length");B=W(B,u);var F=O.get("length2");if(F=W(F,u),Math.abs(T.endAngle-T.startAngle)0?"right":"left":Y>0?"left":"right"}var Ut=Math.PI,Ft=0,ce=I.get("rotate");if(Ct(ce))Ft=ce*(Ut/180);else if(L==="center")Ft=0;else if(ce==="radial"||ce===!0){var ea=Y<0?-H+Ut:-H;Ft=ea}else if(ce==="tangential"&&L!=="outside"&&L!=="outer"){var ke=Math.atan2(Y,j);ke<0&&(ke=Ut*2+ke);var Fl=j>0;Fl&&(ke=Ut+ke),Ft=ke-Ut}if(i=!!Ft,A.x=J,A.y=ct,A.rotation=Ft,A.setStyle({verticalAlign:"middle"}),rt){A.setStyle({align:pt});var jh=A.states.select;jh&&(jh.x+=A.x,jh.y+=A.y)}else{var wa=A.getBoundingRect().clone();wa.applyTransform(A.getComputedTransform());var bm=(A.style.margin||0)+2.1;wa.y-=bm/2,wa.height+=bm,t.push({label:A,labelLine:D,position:L,len:B,len2:F,minTurnAngle:O.get("minTurnAngle"),maxSurfaceAngle:O.get("maxSurfaceAngle"),surfaceNormal:new ft(Y,j),linePoints:xt,textAlign:pt,labelDistance:P,labelAlignTo:R,edgeDistance:E,bleedMargin:N,rect:wa,unconstrainedWidth:wa.width,labelStyleWidth:A.style.width})}w.setTextConfig({inside:rt})}}),!i&&r.get("avoidLabelOverlap")&&ZV(t,a,n,l,u,v,f,h);for(var d=0;d0){for(var f=o.getItemLayout(0),h=1;isNaN(f&&f.startAngle)&&h=i.r0}},e.type="pie",e}(Rt);function No(r,e,t){e=z(e)&&{coordDimensions:e}||V({encodeDefine:r.getEncode()},e);var a=r.getSource(),n=Eo(a,e).dimensions,i=new Le(n,r);return i.initData(a,t),i}var Bl=function(){function r(e,t){this._getDataWithEncodedVisual=e,this._getRawData=t}return r.prototype.getAllNames=function(){var e=this._getRawData();return e.mapArray(e.getName)},r.prototype.containName=function(e){var t=this._getRawData();return t.indexOfName(e)>=0},r.prototype.indexOfName=function(e){var t=this._getDataWithEncodedVisual();return t.indexOfName(e)},r.prototype.getItemVisual=function(e,t){var a=this._getDataWithEncodedVisual();return a.getItemVisual(e,t)},r}(),QV=Tt(),JV=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.init=function(t){r.prototype.init.apply(this,arguments),this.legendVisualProvider=new Bl(X(this.getData,this),X(this.getRawData,this)),this._defaultLabelLine(t)},e.prototype.mergeOption=function(){r.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return No(this,{coordDimensions:["value"],encodeDefaulter:it(qg,this)})},e.prototype.getDataParams=function(t){var a=this.getData(),n=QV(a),i=n.seats;if(!i){var o=[];a.each(a.mapDimension("value"),function(l){o.push(l)}),i=n.seats=sT(o,a.hostModel.get("percentPrecision"))}var s=r.prototype.getDataParams.call(this,t);return s.percent=i[t]||0,s.$vars.push("percent"),s},e.prototype._defaultLabelLine=function(t){ai(t,"labelLine",["show"]);var a=t.labelLine,n=t.emphasis.labelLine;a.show=a.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series.pie",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(kt);function tz(r){return{seriesType:r,reset:function(e,t){var a=e.getData();a.filterSelf(function(n){var i=a.mapDimension("value"),o=a.get(i,n);return!(Ct(o)&&!isNaN(o)&&o<0)})}}}function ez(r){r.registerChartView(jV),r.registerSeriesModel(JV),nC("pie",r.registerAction),r.registerLayout(it(XV,"pie")),r.registerProcessor(Nl("pie")),r.registerProcessor(tz("pie"))}var rz=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.hasSymbolVisual=!0,t}return e.prototype.getInitialData=function(t,a){return Jr(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return t??(this.option.large?5e3:this.get("progressive"))},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return t??(this.option.large?1e4:this.get("progressiveThreshold"))},e.prototype.brushSelector=function(t,a,n){return n.point(a.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}},e}(kt),zD=4,az=function(){function r(){}return r}(),nz=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a._off=0,a.hoverDataIdx=-1,a}return e.prototype.getDefaultShape=function(){return new az},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.buildPath=function(t,a){var n=a.points,i=a.size,o=this.symbolProxy,s=o.shape,l=t.getContext?t.getContext():t,u=l&&i[0]=0;u--){var f=u*2,h=i[f]-s/2,v=i[f+1]-l/2;if(t>=h&&a>=v&&t<=h+s&&a<=v+l)return u}return-1},e.prototype.contain=function(t,a){var n=this.transformCoordToLocal(t,a),i=this.getBoundingRect();if(t=n[0],a=n[1],i.contain(t,a)){var o=this.hoverDataIdx=this.findDataIndex(t,a);return o>=0}return this.hoverDataIdx=-1,!1},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var a=this.shape,n=a.points,i=a.size,o=i[0],s=i[1],l=1/0,u=1/0,f=-1/0,h=-1/0,v=0;v=0&&(u.dataIndex=h+(e.startIndex||0))})},r.prototype.remove=function(){this._clear()},r.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},r}(),oz=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=t.getData(),o=this._updateSymbolDraw(i,t);o.updateData(i,{clipShape:this._getClipShape(t)}),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,a,n){var i=t.getData(),o=this._updateSymbolDraw(i,t);o.incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,a,n){this._symbolDraw.incrementalUpdate(t,a.getData(),{clipShape:this._getClipShape(a)}),this._finished=t.end===a.getData().count()},e.prototype.updateTransform=function(t,a,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var o=Ol("").reset(t,a,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._getClipShape=function(t){if(t.get("clip",!0)){var a=t.coordinateSystem;return a&&a.getArea&&a.getArea(.1)}},e.prototype._updateSymbolDraw=function(t,a){var n=this._symbolDraw,i=a.pipelineContext,o=i.large;return(!n||o!==this._isLargeDraw)&&(n&&n.remove(),n=this._symbolDraw=o?new iz:new El,this._isLargeDraw=o,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,a){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(Rt),sz=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},e}(mt),bd=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Kt).models[0]},e.type="cartesian2dAxis",e}(mt);Xt(bd,Oo);var GD={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},lz=st({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},GD),Cy=st({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},GD),uz=st({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},Cy),fz=Q({logBase:10},Cy);const FD={category:lz,value:Cy,time:uz,log:fz};var hz={value:1,category:1,time:1,log:1};function ho(r,e,t,a){C(hz,function(n,i){var o=st(st({},FD[i],!0),a,!0),s=function(l){k(u,l);function u(){var f=l!==null&&l.apply(this,arguments)||this;return f.type=e+"Axis."+i,f}return u.prototype.mergeDefaultAndTheme=function(f,h){var v=tl(this),c=v?Do(f):{},p=h.getTheme();st(f,p.get(i+"Axis")),st(f,this.getDefaultOption()),f.type=B1(f),v&&Ja(f,c,v)},u.prototype.optionUpdated=function(){var f=this.option;f.type==="category"&&(this.__ordinalMeta=dd.createByAxisModel(this))},u.prototype.getCategories=function(f){var h=this.option;if(h.type==="category")return f?h.data:this.__ordinalMeta.categories},u.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},u.type=e+"Axis."+i,u.defaultOption=o,u}(t);r.registerComponentModel(s)}),r.registerSubTypeDefaulter(e+"Axis",B1)}function B1(r){return r.type||(r.data?"category":"value")}var vz=function(){function r(e){this.type="cartesian",this._dimList=[],this._axes={},this.name=e||""}return r.prototype.getAxis=function(e){return this._axes[e]},r.prototype.getAxes=function(){return G(this._dimList,function(e){return this._axes[e]},this)},r.prototype.getAxesByScale=function(e){return e=e.toLowerCase(),Pt(this.getAxes(),function(t){return t.scale.type===e})},r.prototype.addAxis=function(e){var t=e.dim;this._axes[t]=e,this._dimList.push(t)},r}(),wd=["x","y"];function V1(r){return r.type==="interval"||r.type==="time"}var cz=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type="cartesian2d",t.dimensions=wd,t}return e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,a=this.getAxis("y").scale;if(!(!V1(t)||!V1(a))){var n=t.getExtent(),i=a.getExtent(),o=this.dataToPoint([n[0],i[0]]),s=this.dataToPoint([n[1],i[1]]),l=n[1]-n[0],u=i[1]-i[0];if(!(!l||!u)){var f=(s[0]-o[0])/l,h=(s[1]-o[1])/u,v=o[0]-n[0]*f,c=o[1]-i[0]*h,p=this._transform=[f,0,0,h,v,c];this._invTransform=vi([],p)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var a=this.getAxis("x"),n=this.getAxis("y");return a.contain(a.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,a){var n=this.dataToPoint(t),i=this.dataToPoint(a),o=this.getArea(),s=new ht(n[0],n[1],i[0]-n[0],i[1]-n[1]);return o.intersect(s)},e.prototype.dataToPoint=function(t,a,n){n=n||[];var i=t[0],o=t[1];if(this._transform&&i!=null&&isFinite(i)&&o!=null&&isFinite(o))return le(n,t,this._transform);var s=this.getAxis("x"),l=this.getAxis("y");return n[0]=s.toGlobalCoord(s.dataToCoord(i,a)),n[1]=l.toGlobalCoord(l.dataToCoord(o,a)),n},e.prototype.clampData=function(t,a){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,o=n.getExtent(),s=i.getExtent(),l=n.parse(t[0]),u=i.parse(t[1]);return a=a||[],a[0]=Math.min(Math.max(Math.min(o[0],o[1]),l),Math.max(o[0],o[1])),a[1]=Math.min(Math.max(Math.min(s[0],s[1]),u),Math.max(s[0],s[1])),a},e.prototype.pointToData=function(t,a){var n=[];if(this._invTransform)return le(n,t,this._invTransform);var i=this.getAxis("x"),o=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),a),n[1]=o.coordToData(o.toLocalCoord(t[1]),a),n},e.prototype.getOtherAxis=function(t){return this.getAxis(t.dim==="x"?"y":"x")},e.prototype.getArea=function(t){t=t||0;var a=this.getAxis("x").getGlobalExtent(),n=this.getAxis("y").getGlobalExtent(),i=Math.min(a[0],a[1])-t,o=Math.min(n[0],n[1])-t,s=Math.max(a[0],a[1])-i+t,l=Math.max(n[0],n[1])-o+t;return new ht(i,o,s,l)},e}(vz),pz=function(r){k(e,r);function e(t,a,n,i,o){var s=r.call(this,t,a,n)||this;return s.index=0,s.type=i||"value",s.position=o||"bottom",s}return e.prototype.isHorizontal=function(){var t=this.position;return t==="top"||t==="bottom"},e.prototype.getGlobalExtent=function(t){var a=this.getExtent();return a[0]=this.toGlobalCoord(a[0]),a[1]=this.toGlobalCoord(a[1]),t&&a[0]>a[1]&&a.reverse(),a},e.prototype.pointToData=function(t,a){return this.coordToData(this.toLocalCoord(t[this.dim==="x"?0:1]),a)},e.prototype.setCategorySortInfo=function(t){if(this.type!=="category")return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(Lr);function Td(r,e,t){t=t||{};var a=r.coordinateSystem,n=e.axis,i={},o=n.getAxesOnZeroOf()[0],s=n.position,l=o?"onZero":s,u=n.dim,f=a.getRect(),h=[f.x,f.x+f.width,f.y,f.y+f.height],v={left:0,right:1,top:0,bottom:1,onZero:2},c=e.get("offset")||0,p=u==="x"?[h[2]-c,h[3]+c]:[h[0]-c,h[1]+c];if(o){var d=o.toGlobalCoord(o.dataToCoord(0));p[v.onZero]=Math.max(Math.min(d,p[1]),p[0])}i.position=[u==="y"?p[v[l]]:h[0],u==="x"?p[v[l]]:h[3]],i.rotation=Math.PI/2*(u==="x"?0:1);var g={top:-1,bottom:1,left:-1,right:1};i.labelDirection=i.tickDirection=i.nameDirection=g[s],i.labelOffset=o?p[v[s]]-p[v.onZero]:0,e.get(["axisTick","inside"])&&(i.tickDirection=-i.tickDirection),ee(t.labelInside,e.get(["axisLabel","inside"]))&&(i.labelDirection=-i.labelDirection);var y=e.get(["axisLabel","rotate"]);return i.labelRotate=l==="top"?-y:y,i.z2=1,i}function z1(r){return r.get("coordinateSystem")==="cartesian2d"}function G1(r){var e={xAxisModel:null,yAxisModel:null};return C(e,function(t,a){var n=a.replace(/Model$/,""),i=r.getReferringComponents(n,Kt).models[0];e[a]=i}),e}var gc=Math.log;function HD(r,e,t){var a=_a.prototype,n=a.getTicks.call(t),i=a.getTicks.call(t,!0),o=n.length-1,s=a.getInterval.call(t),l=YC(r,e),u=l.extent,f=l.fixMin,h=l.fixMax;if(r.type==="log"){var v=gc(r.base);u=[gc(u[0])/v,gc(u[1])/v]}r.setExtent(u[0],u[1]),r.calcNiceExtent({splitNumber:o,fixMin:f,fixMax:h});var c=a.getExtent.call(r);f&&(u[0]=c[0]),h&&(u[1]=c[1]);var p=a.getInterval.call(r),d=u[0],g=u[1];if(f&&h)p=(g-d)/o;else if(f)for(g=u[0]+p*o;gu[0]&&isFinite(d)&&isFinite(u[0]);)p=ec(p),d=u[1]-p*o;else{var y=r.getTicks().length-1;y>o&&(p=ec(p));var m=p*o;g=Math.ceil(u[1]/p)*p,d=Ht(g-m),d<0&&u[0]>=0?(d=0,g=Ht(m)):g>0&&u[1]<=0&&(g=0,d=-Ht(m))}var _=(n[0].value-i[0].value)/s,S=(n[o].value-i[o].value)/s;a.setExtent.call(r,d+p*_,g+p*S),a.setInterval.call(r,p),(_||S)&&a.setNiceExtent.call(r,d+p,g-p)}var dz=function(){function r(e,t,a){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=wd,this._initCartesian(e,t,a),this.model=e}return r.prototype.getRect=function(){return this._rect},r.prototype.update=function(e,t){var a=this._axesMap;this._updateScale(e,this.model);function n(o){var s,l=St(o),u=l.length;if(u){for(var f=[],h=u-1;h>=0;h--){var v=+l[h],c=o[v],p=c.model,d=c.scale;gd(d)&&p.get("alignTicks")&&p.get("interval")==null?f.push(c):(li(d,p),gd(d)&&(s=c))}f.length&&(s||(s=f.pop(),li(s.scale,s.model)),C(f,function(g){HD(g.scale,g.model,s.scale)}))}}n(a.x),n(a.y);var i={};C(a.x,function(o){F1(a,"y",o,i)}),C(a.y,function(o){F1(a,"x",o,i)}),this.resize(this.model,t)},r.prototype.resize=function(e,t,a){var n=e.getBoxLayoutParams(),i=!a&&e.get("containLabel"),o=jt(n,{width:t.getWidth(),height:t.getHeight()});this._rect=o;var s=this._axesList;l(),i&&(C(s,function(u){if(!u.model.get(["axisLabel","inside"])){var f=UN(u);if(f){var h=u.isHorizontal()?"height":"width",v=u.model.get(["axisLabel","margin"]);o[h]-=f[h]+v,u.position==="top"?o.y+=f.height+v:u.position==="left"&&(o.x+=f.width+v)}}}),l()),C(this._coordsList,function(u){u.calcAffineTransform()});function l(){C(s,function(u){var f=u.isHorizontal(),h=f?[0,o.width]:[0,o.height],v=u.inverse?1:0;u.setExtent(h[v],h[1-v]),gz(u,f?o.x:o.y)})}},r.prototype.getAxis=function(e,t){var a=this._axesMap[e];if(a!=null)return a[t||0]},r.prototype.getAxes=function(){return this._axesList.slice()},r.prototype.getCartesian=function(e,t){if(e!=null&&t!=null){var a="x"+e+"y"+t;return this._coordsMap[a]}tt(e)&&(t=e.yAxisIndex,e=e.xAxisIndex);for(var n=0,i=this._coordsList;n0?"top":"bottom",i="center"):io(n-Ga)?(o=a>0?"bottom":"top",i="center"):(o="middle",n>0&&n0?"right":"left":i=a>0?"left":"right"),{rotation:n,textAlign:i,textVerticalAlign:o}},r.makeAxisEventDataBase=function(e){var t={componentType:e.mainType,componentIndex:e.componentIndex};return t[e.mainType+"Index"]=e.componentIndex,t},r.isLabelSilent=function(e){var t=e.get("tooltip");return e.get("silent")||!(e.get("triggerEvent")||t&&t.show)},r}(),W1={axisLine:function(r,e,t,a){var n=e.get(["axisLine","show"]);if(n==="auto"&&r.handleAutoShown&&(n=r.handleAutoShown("axisLine")),!!n){var i=e.axis.getExtent(),o=a.transform,s=[i[0],0],l=[i[1],0],u=s[0]>l[0];o&&(le(s,s,o),le(l,l,o));var f=V({lineCap:"round"},e.getModel(["axisLine","lineStyle"]).getLineStyle()),h=new Jt({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:f,strokeContainThreshold:r.strokeContainThreshold||5,silent:!0,z2:1});so(h.shape,h.style.lineWidth),h.anid="line",t.add(h);var v=e.get(["axisLine","symbol"]);if(v!=null){var c=e.get(["axisLine","symbolSize"]);U(v)&&(v=[v,v]),(U(c)||Ct(c))&&(c=[c,c]);var p=di(e.get(["axisLine","symbolOffset"])||0,c),d=c[0],g=c[1];C([{rotate:r.rotation+Math.PI/2,offset:p[0],r:0},{rotate:r.rotation-Math.PI/2,offset:p[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],function(y,m){if(v[m]!=="none"&&v[m]!=null){var _=Zt(v[m],-d/2,-g/2,d,g,f.stroke,!0),S=y.r+y.offset,b=u?l:s;_.attr({rotation:y.rotate,x:b[0]+S*Math.cos(r.rotation),y:b[1]-S*Math.sin(r.rotation),silent:!0,z2:11}),t.add(_)}})}}},axisTickLabel:function(r,e,t,a){var n=_z(t,a,e,r),i=xz(t,a,e,r);if(mz(e,i,n),Sz(t,a,e,r.tickDirection),e.get(["axisLabel","hideOverlap"])){var o=iD(G(i,function(s){return{label:s,priority:s.z2,defaultAttr:{ignore:s.ignore}}}));lD(o)}},axisName:function(r,e,t,a){var n=ee(r.axisName,e.get("name"));if(n){var i=e.get("nameLocation"),o=r.nameDirection,s=e.getModel("nameTextStyle"),l=e.get("nameGap")||0,u=e.axis.getExtent(),f=u[0]>u[1]?-1:1,h=[i==="start"?u[0]-f*l:i==="end"?u[1]+f*l:(u[0]+u[1])/2,Y1(i)?r.labelOffset+o*l:0],v,c=e.get("nameRotate");c!=null&&(c=c*Ga/180);var p;Y1(i)?v=Pe.innerTextLayout(r.rotation,c??r.rotation,o):(v=yz(r.rotation,i,c||0,u),p=r.axisNameAvailableWidth,p!=null&&(p=Math.abs(p/Math.sin(v.rotation)),!isFinite(p)&&(p=null)));var d=s.getFont(),g=e.get("nameTruncate",!0)||{},y=g.ellipsis,m=ee(r.nameTruncateMaxWidth,g.maxWidth,p),_=new bt({x:h[0],y:h[1],rotation:v.rotation,silent:Pe.isLabelSilent(e),style:Nt(s,{text:n,font:d,overflow:"truncate",width:m,ellipsis:y,fill:s.getTextColor()||e.get(["axisLine","lineStyle","color"]),align:s.get("align")||v.textAlign,verticalAlign:s.get("verticalAlign")||v.textVerticalAlign}),z2:1});if(To({el:_,componentModel:e,itemName:n}),_.__fullText=n,_.anid="name",e.get("triggerEvent")){var S=Pe.makeAxisEventDataBase(e);S.targetType="axisName",S.name=n,nt(_).eventData=S}a.add(_),_.updateTransform(),t.add(_),_.decomposeTransform()}}};function yz(r,e,t,a){var n=Sg(t-r),i,o,s=a[0]>a[1],l=e==="start"&&!s||e!=="start"&&s;return io(n-Ga/2)?(o=l?"bottom":"top",i="center"):io(n-Ga*1.5)?(o=l?"top":"bottom",i="center"):(o="middle",nGa/2?i=l?"left":"right":i=l?"right":"left"),{rotation:n,textAlign:i,textVerticalAlign:o}}function mz(r,e,t){if(!XC(r.axis)){var a=r.get(["axisLabel","showMinLabel"]),n=r.get(["axisLabel","showMaxLabel"]);e=e||[],t=t||[];var i=e[0],o=e[1],s=e[e.length-1],l=e[e.length-2],u=t[0],f=t[1],h=t[t.length-1],v=t[t.length-2];a===!1?(qe(i),qe(u)):U1(i,o)&&(a?(qe(o),qe(f)):(qe(i),qe(u))),n===!1?(qe(s),qe(h)):U1(l,s)&&(n?(qe(l),qe(v)):(qe(s),qe(h)))}}function qe(r){r&&(r.ignore=!0)}function U1(r,e){var t=r&&r.getBoundingRect().clone(),a=e&&e.getBoundingRect().clone();if(!(!t||!a)){var n=Sl([]);return an(n,n,-r.rotation),t.applyTransform(Yr([],n,r.getLocalTransform())),a.applyTransform(Yr([],n,e.getLocalTransform())),t.intersect(a)}}function Y1(r){return r==="middle"||r==="center"}function WD(r,e,t,a,n){for(var i=[],o=[],s=[],l=0;l=0||r===e}function Dz(r){var e=Dy(r);if(e){var t=e.axisPointerModel,a=e.axis.scale,n=t.option,i=t.get("status"),o=t.get("value");o!=null&&(o=a.parse(o));var s=Ad(t);i==null&&(n.status=s?"show":"hide");var l=a.getExtent().slice();l[0]>l[1]&&l.reverse(),(o==null||o>l[1])&&(o=l[1]),o0&&!p.min?p.min=0:p.min!=null&&p.min<0&&!p.max&&(p.max=0);var d=l;p.color!=null&&(d=Q({color:p.color},l));var g=st(et(p),{boundaryGap:t,splitNumber:a,scale:n,axisLine:i,axisTick:o,axisLabel:s,name:p.text,showName:u,nameLocation:"end",nameGap:h,nameTextStyle:d,triggerEvent:v},!1);if(U(f)){var y=g.name;g.name=f.replace("{value}",y??"")}else K(f)&&(g.name=f(g.name,g));var m=new Mt(g,null,this.ecModel);return Xt(m,Oo.prototype),m.mainType="radar",m.componentIndex=this.componentIndex,m},this);this._indicatorModels=c},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type="radar",e.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:st({lineStyle:{color:"#bbb"}},rs.axisLine),axisLabel:Mu(rs.axisLabel,!1),axisTick:Mu(rs.axisTick,!1),splitLine:Mu(rs.splitLine,!0),splitArea:Mu(rs.splitArea,!0),indicator:[]},e}(mt),Gz=["axisLine","axisTickLabel","axisName"],Fz=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=this.group;i.removeAll(),this._buildAxes(t),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t){var a=t.coordinateSystem,n=a.getIndicatorAxes(),i=G(n,function(o){var s=o.model.get("showName")?o.name:"",l=new Pe(o.model,{axisName:s,position:[a.cx,a.cy],rotation:o.angle,labelDirection:-1,tickDirection:-1,nameDirection:1});return l});C(i,function(o){C(Gz,o.add,o),this.group.add(o.getGroup())},this)},e.prototype._buildSplitLineAndArea=function(t){var a=t.coordinateSystem,n=a.getIndicatorAxes();if(!n.length)return;var i=t.get("shape"),o=t.getModel("splitLine"),s=t.getModel("splitArea"),l=o.getModel("lineStyle"),u=s.getModel("areaStyle"),f=o.get("show"),h=s.get("show"),v=l.get("color"),c=u.get("color"),p=z(v)?v:[v],d=z(c)?c:[c],g=[],y=[];function m(R,E,N){var O=N%E.length;return R[O]=R[O]||[],O}if(i==="circle")for(var _=n[0].getTicksCoords(),S=a.cx,b=a.cy,x=0;x<_.length;x++){if(f){var w=m(g,p,x);g[w].push(new Kr({shape:{cx:S,cy:b,r:_[x].coord}}))}if(h&&x<_.length-1){var w=m(y,d,x);y[w].push(new _o({shape:{cx:S,cy:b,r0:_[x].coord,r:_[x+1].coord}}))}}else for(var T,A=G(n,function(R,E){var N=R.getTicksCoords();return T=T==null?N.length-1:Math.min(N.length-1,T),G(N,function(O){return a.coordToPoint(O.coord,E)})}),D=[],x=0;x<=T;x++){for(var M=[],I=0;I3?1.4:o>1?1.2:1.1,f=i>0?u:1/u;_c(this,"zoom","zoomOnMouseWheel",t,{scale:f,originX:s,originY:l,isAvailableBehavior:null})}if(n){var h=Math.abs(i),v=(i>0?1:-1)*(h>3?.4:h>1?.15:.05);_c(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:v,originX:s,originY:l,isAvailableBehavior:null})}}},e.prototype._pinchHandler=function(t){if(!j1(this._zr,"globalPan")){var a=t.pinchScale>1?1.1:1/1.1;_c(this,"zoom",null,t,{scale:a,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})}},e}(fr);function _c(r,e,t,a,n){r.pointerChecker&&r.pointerChecker(a,n.originX,n.originY)&&(pa(a.event),qD(r,e,t,a,n))}function qD(r,e,t,a,n){n.isAvailableBehavior=X(nf,null,t,a),r.trigger(e,n)}function nf(r,e,t){var a=t[r];return!r||a&&(!U(a)||e.event[a+"Key"])}function Iy(r,e,t){var a=r.target;a.x+=e,a.y+=t,a.dirty()}function Ly(r,e,t,a){var n=r.target,i=r.zoomLimit,o=r.zoom=r.zoom||1;if(o*=e,i){var s=i.min||0,l=i.max||1/0;o=Math.max(Math.min(l,o),s)}var u=o/r.zoom;r.zoom=o,n.x-=(t-n.x)*(u-1),n.y-=(a-n.y)*(u-1),n.scaleX*=u,n.scaleY*=u,n.dirty()}var Zz={axisPointer:1,tooltip:1,brush:1};function Uh(r,e,t){var a=e.getComponentByElement(r.topTarget),n=a&&a.coordinateSystem;return a&&a!==t&&!Zz.hasOwnProperty(a.mainType)&&n&&n.model!==t}function KD(r){if(U(r)){var e=new DOMParser;r=e.parseFromString(r,"text/xml")}var t=r;for(t.nodeType===9&&(t=t.firstChild);t.nodeName.toLowerCase()!=="svg"||t.nodeType!==1;)t=t.nextSibling;return t}var Sc,Yf={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},Q1=St(Yf),Xf={"alignment-baseline":"textBaseline","stop-color":"stopColor"},J1=St(Xf),qz=function(){function r(){this._defs={},this._root=null}return r.prototype.parse=function(e,t){t=t||{};var a=KD(e);this._defsUsePending=[];var n=new at;this._root=n;var i=[],o=a.getAttribute("viewBox")||"",s=parseFloat(a.getAttribute("width")||t.width),l=parseFloat(a.getAttribute("height")||t.height);isNaN(s)&&(s=null),isNaN(l)&&(l=null),Fe(a,n,null,!0,!1);for(var u=a.firstChild;u;)this._parseNode(u,n,i,null,!1,!1),u=u.nextSibling;Qz(this._defs,this._defsUsePending),this._defsUsePending=[];var f,h;if(o){var v=Yh(o);v.length>=4&&(f={x:parseFloat(v[0]||0),y:parseFloat(v[1]||0),width:parseFloat(v[2]),height:parseFloat(v[3])})}if(f&&s!=null&&l!=null&&(h=QD(f,{x:0,y:0,width:s,height:l}),!t.ignoreViewBox)){var c=n;n=new at,n.add(c),c.scaleX=c.scaleY=h.scale,c.x=h.x,c.y=h.y}return!t.ignoreRootClip&&s!=null&&l!=null&&n.setClipPath(new wt({shape:{x:0,y:0,width:s,height:l}})),{root:n,width:s,height:l,viewBoxRect:f,viewBoxTransform:h,named:i}},r.prototype._parseNode=function(e,t,a,n,i,o){var s=e.nodeName.toLowerCase(),l,u=n;if(s==="defs"&&(i=!0),s==="text"&&(o=!0),s==="defs"||s==="switch")l=t;else{if(!i){var f=Sc[s];if(f&&$(Sc,s)){l=f.call(this,e,t);var h=e.getAttribute("name");if(h){var v={name:h,namedFrom:null,svgNodeTagLower:s,el:l};a.push(v),s==="g"&&(u=v)}else n&&a.push({name:n.name,namedFrom:n,svgNodeTagLower:s,el:l});t.add(l)}}var c=tS[s];if(c&&$(tS,s)){var p=c.call(this,e),d=e.getAttribute("id");d&&(this._defs[d]=p)}}if(l&&l.isGroup)for(var g=e.firstChild;g;)g.nodeType===1?this._parseNode(g,l,a,u,i,o):g.nodeType===3&&o&&this._parseText(g,l),g=g.nextSibling},r.prototype._parseText=function(e,t){var a=new oo({style:{text:e.textContent},silent:!0,x:this._textX||0,y:this._textY||0});Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),Kz(a,t);var n=a.style,i=n.fontSize;i&&i<9&&(n.fontSize=9,a.scaleX*=i/9,a.scaleY*=i/9);var o=(n.fontSize||n.fontFamily)&&[n.fontStyle,n.fontWeight,(n.fontSize||12)+"px",n.fontFamily||"sans-serif"].join(" ");n.font=o;var s=a.getBoundingRect();return this._textX+=s.width,t.add(a),a},r.internalField=function(){Sc={g:function(e,t){var a=new at;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a},rect:function(e,t){var a=new wt;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a.setShape({x:parseFloat(e.getAttribute("x")||"0"),y:parseFloat(e.getAttribute("y")||"0"),width:parseFloat(e.getAttribute("width")||"0"),height:parseFloat(e.getAttribute("height")||"0")}),a.silent=!0,a},circle:function(e,t){var a=new Kr;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),r:parseFloat(e.getAttribute("r")||"0")}),a.silent=!0,a},line:function(e,t){var a=new Jt;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a.setShape({x1:parseFloat(e.getAttribute("x1")||"0"),y1:parseFloat(e.getAttribute("y1")||"0"),x2:parseFloat(e.getAttribute("x2")||"0"),y2:parseFloat(e.getAttribute("y2")||"0")}),a.silent=!0,a},ellipse:function(e,t){var a=new Al;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a.setShape({cx:parseFloat(e.getAttribute("cx")||"0"),cy:parseFloat(e.getAttribute("cy")||"0"),rx:parseFloat(e.getAttribute("rx")||"0"),ry:parseFloat(e.getAttribute("ry")||"0")}),a.silent=!0,a},polygon:function(e,t){var a=e.getAttribute("points"),n;a&&(n=aS(a));var i=new Se({shape:{points:n||[]},silent:!0});return Ke(t,i),Fe(e,i,this._defsUsePending,!1,!1),i},polyline:function(e,t){var a=e.getAttribute("points"),n;a&&(n=aS(a));var i=new be({shape:{points:n||[]},silent:!0});return Ke(t,i),Fe(e,i,this._defsUsePending,!1,!1),i},image:function(e,t){var a=new oe;return Ke(t,a),Fe(e,a,this._defsUsePending,!1,!1),a.setStyle({image:e.getAttribute("xlink:href")||e.getAttribute("href"),x:+e.getAttribute("x"),y:+e.getAttribute("y"),width:+e.getAttribute("width"),height:+e.getAttribute("height")}),a.silent=!0,a},text:function(e,t){var a=e.getAttribute("x")||"0",n=e.getAttribute("y")||"0",i=e.getAttribute("dx")||"0",o=e.getAttribute("dy")||"0";this._textX=parseFloat(a)+parseFloat(i),this._textY=parseFloat(n)+parseFloat(o);var s=new at;return Ke(t,s),Fe(e,s,this._defsUsePending,!1,!0),s},tspan:function(e,t){var a=e.getAttribute("x"),n=e.getAttribute("y");a!=null&&(this._textX=parseFloat(a)),n!=null&&(this._textY=parseFloat(n));var i=e.getAttribute("dx")||"0",o=e.getAttribute("dy")||"0",s=new at;return Ke(t,s),Fe(e,s,this._defsUsePending,!1,!0),this._textX+=parseFloat(i),this._textY+=parseFloat(o),s},path:function(e,t){var a=e.getAttribute("d")||"",n=HT(a);return Ke(t,n),Fe(e,n,this._defsUsePending,!1,!1),n.silent=!0,n}}}(),r}(),tS={lineargradient:function(r){var e=parseInt(r.getAttribute("x1")||"0",10),t=parseInt(r.getAttribute("y1")||"0",10),a=parseInt(r.getAttribute("x2")||"10",10),n=parseInt(r.getAttribute("y2")||"0",10),i=new xo(e,t,a,n);return eS(r,i),rS(r,i),i},radialgradient:function(r){var e=parseInt(r.getAttribute("cx")||"0",10),t=parseInt(r.getAttribute("cy")||"0",10),a=parseInt(r.getAttribute("r")||"0",10),n=new kg(e,t,a);return eS(r,n),rS(r,n),n}};function eS(r,e){var t=r.getAttribute("gradientUnits");t==="userSpaceOnUse"&&(e.global=!0)}function rS(r,e){for(var t=r.firstChild;t;){if(t.nodeType===1&&t.nodeName.toLocaleLowerCase()==="stop"){var a=t.getAttribute("offset"),n=void 0;a&&a.indexOf("%")>0?n=parseInt(a,10)/100:a?n=parseFloat(a):n=0;var i={};jD(t,i,i);var o=i.stopColor||t.getAttribute("stop-color")||"#000000";e.colorStops.push({offset:n,color:o})}t=t.nextSibling}}function Ke(r,e){r&&r.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),Q(e.__inheritedStyle,r.__inheritedStyle))}function aS(r){for(var e=Yh(r),t=[],a=0;a0;i-=2){var o=a[i],s=a[i-1],l=Yh(o);switch(n=n||Ge(),s){case"translate":Dr(n,n,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":dh(n,n,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":an(n,n,-parseFloat(l[0])*xc,[parseFloat(l[1]||"0"),parseFloat(l[2]||"0")]);break;case"skewX":var u=Math.tan(parseFloat(l[0])*xc);Yr(n,[1,0,u,1,0,0],n);break;case"skewY":var f=Math.tan(parseFloat(l[0])*xc);Yr(n,[1,f,0,1,0,0],n);break;case"matrix":n[0]=parseFloat(l[0]),n[1]=parseFloat(l[1]),n[2]=parseFloat(l[2]),n[3]=parseFloat(l[3]),n[4]=parseFloat(l[4]),n[5]=parseFloat(l[5]);break}}e.setLocalTransform(n)}}var iS=/([^\s:;]+)\s*:\s*([^:;]+)/g;function jD(r,e,t){var a=r.getAttribute("style");if(a){iS.lastIndex=0;for(var n;(n=iS.exec(a))!=null;){var i=n[1],o=$(Yf,i)?Yf[i]:null;o&&(e[o]=n[2]);var s=$(Xf,i)?Xf[i]:null;s&&(t[s]=n[2])}}}function r5(r,e,t){for(var a=0;a0,g={api:a,geo:l,mapOrGeoModel:e,data:s,isVisualEncodedByVisualMap:d,isGeo:o,transformInfoRaw:v};l.resourceType==="geoJSON"?this._buildGeoJSON(g):l.resourceType==="geoSVG"&&this._buildSVG(g),this._updateController(e,t,a),this._updateMapSelectHandler(e,u,a,n)},r.prototype._buildGeoJSON=function(e){var t=this._regionsGroupByName=Z(),a=Z(),n=this._regionsGroup,i=e.transformInfoRaw,o=e.mapOrGeoModel,s=e.data,l=e.geo.projection,u=l&&l.stream;function f(c,p){return p&&(c=p(c)),c&&[c[0]*i.scaleX+i.x,c[1]*i.scaleY+i.y]}function h(c){for(var p=[],d=!u&&l&&l.project,g=0;g=0)&&(v=n);var c=o?{normal:{align:"center",verticalAlign:"middle"}}:null;ve(e,ne(a),{labelFetcher:v,labelDataIndex:h,defaultText:t},c);var p=e.getTextContent();if(p&&(JD(p).ignore=p.ignore,e.textConfig&&o)){var d=e.getBoundingRect().clone();e.textConfig.layoutRect=d,e.textConfig.position=[(o[0]-d.x)/d.width*100+"%",(o[1]-d.y)/d.height*100+"%"]}e.disableLabelAnimation=!0}else e.removeTextContent(),e.removeTextConfig(),e.disableLabelAnimation=null}function fS(r,e,t,a,n,i){r.data?r.data.setItemGraphicEl(i,e):nt(e).eventData={componentType:"geo",componentIndex:n.componentIndex,geoIndex:n.componentIndex,name:t,region:a&&a.option||{}}}function hS(r,e,t,a,n){r.data||To({el:e,componentModel:n,itemName:t,itemTooltipOption:a.get("tooltip")})}function vS(r,e,t,a,n){e.highDownSilentOnTouch=!!n.get("selectedMode");var i=a.getModel("emphasis"),o=i.get("focus");return Wt(e,o,i.get("blurScope"),i.get("disabled")),r.isGeo&&dR(e,n,t),o}function cS(r,e,t){var a=[],n;function i(){n=[]}function o(){n.length&&(a.push(n),n=[])}var s=e({polygonStart:i,polygonEnd:o,lineStart:i,lineEnd:o,point:function(l,u){isFinite(l)&&isFinite(u)&&n.push([l,u])},sphere:function(){}});return!t&&s.polygonStart(),C(r,function(l){s.lineStart();for(var u=0;u-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},e.type="series.map",e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"},e}(kt);function b5(r,e){var t={};return C(r,function(a){a.each(a.mapDimension("value"),function(n,i){var o="ec-"+a.getName(i);t[o]=t[o]||[],isNaN(n)||t[o].push(n)})}),r[0].map(r[0].mapDimension("value"),function(a,n){for(var i="ec-"+r[0].getName(n),o=0,s=1/0,l=-1/0,u=t[i].length,f=0;f1?(S.width=_,S.height=_/g):(S.height=_,S.width=_*g),S.y=m[1]-S.height/2,S.x=m[0]-S.width/2;else{var b=r.getBoxLayoutParams();b.aspect=g,S=jt(b,{width:p,height:d})}this.setViewRect(S.x,S.y,S.width,S.height),this.setCenter(r.get("center"),e),this.setZoom(r.get("zoom"))}function C5(r,e){C(e.get("geoCoord"),function(t,a){r.addGeoCoord(a,t)})}var D5=function(){function r(){this.dimensions=eM}return r.prototype.create=function(e,t){var a=[];function n(o){return{nameProperty:o.get("nameProperty"),aspectScale:o.get("aspectScale"),projection:o.get("projection")}}e.eachComponent("geo",function(o,s){var l=o.get("map"),u=new Md(l+s,l,V({nameMap:o.get("nameMap")},n(o)));u.zoomLimit=o.get("scaleLimit"),a.push(u),o.coordinateSystem=u,u.model=o,u.resize=yS,u.resize(o,t)}),e.eachSeries(function(o){var s=o.get("coordinateSystem");if(s==="geo"){var l=o.get("geoIndex")||0;o.coordinateSystem=a[l]}});var i={};return e.eachSeriesByType("map",function(o){if(!o.getHostGeoModel()){var s=o.getMapType();i[s]=i[s]||[],i[s].push(o)}}),C(i,function(o,s){var l=G(o,function(f){return f.get("nameMap")}),u=new Md(s,s,V({nameMap:hh(l)},n(o[0])));u.zoomLimit=ee.apply(null,G(o,function(f){return f.get("scaleLimit")})),a.push(u),u.resize=yS,u.resize(o[0],t),C(o,function(f){f.coordinateSystem=u,C5(u,f)})}),a},r.prototype.getFilledRegions=function(e,t,a,n){for(var i=(e||[]).slice(),o=Z(),s=0;s=0;o--){var s=n[o];s.hierNode={defaultAncestor:null,ancestor:s,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},t.push(s)}}function E5(r,e){var t=r.isExpand?r.children:[],a=r.parentNode.children,n=r.hierNode.i?a[r.hierNode.i-1]:null;if(t.length){N5(r);var i=(t[0].hierNode.prelim+t[t.length-1].hierNode.prelim)/2;n?(r.hierNode.prelim=n.hierNode.prelim+e(r,n),r.hierNode.modifier=r.hierNode.prelim-i):r.hierNode.prelim=i}else n&&(r.hierNode.prelim=n.hierNode.prelim+e(r,n));r.parentNode.hierNode.defaultAncestor=B5(r,n,r.parentNode.hierNode.defaultAncestor||a[0],e)}function k5(r){var e=r.hierNode.prelim+r.parentNode.hierNode.modifier;r.setLayout({x:e},!0),r.hierNode.modifier+=r.parentNode.hierNode.modifier}function _S(r){return arguments.length?r:G5}function Ss(r,e){return r-=Math.PI/2,{x:e*Math.cos(r),y:e*Math.sin(r)}}function O5(r,e){return jt(r.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function N5(r){for(var e=r.children,t=e.length,a=0,n=0;--t>=0;){var i=e[t];i.hierNode.prelim+=a,i.hierNode.modifier+=a,n+=i.hierNode.change,a+=i.hierNode.shift+n}}function B5(r,e,t,a){if(e){for(var n=r,i=r,o=i.parentNode.children[0],s=e,l=n.hierNode.modifier,u=i.hierNode.modifier,f=o.hierNode.modifier,h=s.hierNode.modifier;s=bc(s),i=wc(i),s&&i;){n=bc(n),o=wc(o),n.hierNode.ancestor=r;var v=s.hierNode.prelim+h-i.hierNode.prelim-u+a(s,i);v>0&&(z5(V5(s,r,t),r,v),u+=v,l+=v),h+=s.hierNode.modifier,u+=i.hierNode.modifier,l+=n.hierNode.modifier,f+=o.hierNode.modifier}s&&!bc(n)&&(n.hierNode.thread=s,n.hierNode.modifier+=h-l),i&&!wc(o)&&(o.hierNode.thread=i,o.hierNode.modifier+=u-f,t=r)}return t}function bc(r){var e=r.children;return e.length&&r.isExpand?e[e.length-1]:r.hierNode.thread}function wc(r){var e=r.children;return e.length&&r.isExpand?e[0]:r.hierNode.thread}function V5(r,e,t){return r.hierNode.ancestor.parentNode===e.parentNode?r.hierNode.ancestor:t}function z5(r,e,t){var a=t/(e.hierNode.i-r.hierNode.i);e.hierNode.change-=a,e.hierNode.shift+=t,e.hierNode.modifier+=t,e.hierNode.prelim+=t,r.hierNode.change+=a}function G5(r,e){return r.parentNode===e.parentNode?1:2}var F5=function(){function r(){this.parentPoint=[],this.childPoints=[]}return r}(),H5=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new F5},e.prototype.buildPath=function(t,a){var n=a.childPoints,i=n.length,o=a.parentPoint,s=n[0],l=n[i-1];if(i===1){t.moveTo(o[0],o[1]),t.lineTo(s[0],s[1]);return}var u=a.orient,f=u==="TB"||u==="BT"?0:1,h=1-f,v=W(a.forkPosition,1),c=[];c[f]=o[f],c[h]=o[h]+(l[h]-o[h])*v,t.moveTo(o[0],o[1]),t.lineTo(c[0],c[1]),t.moveTo(s[0],s[1]),c[f]=s[f],t.lineTo(c[0],c[1]),c[f]=l[f],t.lineTo(c[0],c[1]),t.lineTo(l[0],l[1]);for(var p=1;pm.x,b||(S=S-Math.PI));var w=b?"left":"right",T=s.getModel("label"),A=T.get("rotate"),D=A*(Math.PI/180),M=g.getTextContent();M&&(g.setTextConfig({position:T.get("position")||w,rotation:A==null?-S:D,origin:"center"}),M.setStyle("verticalAlign","middle"))}var I=s.get(["emphasis","focus"]),L=I==="relative"?Hs(o.getAncestorsIndices(),o.getDescendantIndices()):I==="ancestor"?o.getAncestorsIndices():I==="descendant"?o.getDescendantIndices():null;L&&(nt(t).focus=L),U5(n,o,f,t,p,c,d,a),t.__edge&&(t.onHoverStateChange=function(P){if(P!=="blur"){var R=o.parentNode&&r.getItemGraphicEl(o.parentNode.dataIndex);R&&R.hoverState===Tl||wf(t.__edge,P)}})}function U5(r,e,t,a,n,i,o,s){var l=e.getModel(),u=r.get("edgeShape"),f=r.get("layout"),h=r.getOrient(),v=r.get(["lineStyle","curveness"]),c=r.get("edgeForkPosition"),p=l.getModel("lineStyle").getLineStyle(),d=a.__edge;if(u==="curve")e.parentNode&&e.parentNode!==t&&(d||(d=a.__edge=new So({shape:Id(f,h,v,n,n)})),Dt(d,{shape:Id(f,h,v,i,o)},r));else if(u==="polyline"&&f==="orthogonal"&&e!==t&&e.children&&e.children.length!==0&&e.isExpand===!0){for(var g=e.children,y=[],m=0;mt&&(t=n.height)}this.height=t+1},r.prototype.getNodeById=function(e){if(this.getId()===e)return this;for(var t=0,a=this.children,n=a.length;t=0&&this.hostTree.data.setItemLayout(this.dataIndex,e,t)},r.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},r.prototype.getModel=function(e){if(!(this.dataIndex<0)){var t=this.hostTree,a=t.data.getItemModel(this.dataIndex);return a.getModel(e)}},r.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},r.prototype.setVisual=function(e,t){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,e,t)},r.prototype.getVisual=function(e){return this.hostTree.data.getItemVisual(this.dataIndex,e)},r.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},r.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},r.prototype.getChildIndex=function(){if(this.parentNode){for(var e=this.parentNode.children,t=0;t=0){var a=t.getData().tree.root,n=r.targetNode;if(U(n)&&(n=a.getNodeById(n)),n&&a.contains(n))return{node:n};var i=r.targetNodeId;if(i!=null&&(n=a.getNodeById(i)))return{node:n}}}function sM(r){for(var e=[];r;)r=r.parentNode,r&&e.push(r);return e.reverse()}function Oy(r,e){var t=sM(r);return vt(t,e)>=0}function Xh(r,e){for(var t=[];r;){var a=r.dataIndex;t.push({name:r.name,dataIndex:a,value:e.getRawValue(a)}),r=r.parentNode}return t.reverse(),t}var J5=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.hasSymbolVisual=!0,t.ignoreStyleOnData=!0,t}return e.prototype.getInitialData=function(t){var a={name:t.name,children:t.data},n=t.leaves||{},i=new Mt(n,this,this.ecModel),o=ky.createTree(a,this,s);function s(h){h.wrapMethod("getItemModel",function(v,c){var p=o.getNodeByDataIndex(c);return p&&p.children.length&&p.isExpand||(v.parentModel=i),v})}var l=0;o.eachNode("preorder",function(h){h.depth>l&&(l=h.depth)});var u=t.expandAndCollapse,f=u&&t.initialTreeDepth>=0?t.initialTreeDepth:l;return o.root.eachNode("preorder",function(h){var v=h.hostTree.data.getRawDataItem(h.dataIndex);h.isExpand=v&&v.collapsed!=null?!v.collapsed:h.depth<=f}),o.data},e.prototype.getOrient=function(){var t=this.get("orient");return t==="horizontal"?t="LR":t==="vertical"&&(t="TB"),t},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.formatTooltip=function(t,a,n){for(var i=this.getData().tree,o=i.root.children[0],s=i.getNodeByDataIndex(t),l=s.getValue(),u=s.name;s&&s!==o;)u=s.parentNode.name+"."+u,s=s.parentNode;return ie("nameValue",{name:u,value:l,noValue:isNaN(l)||l==null})},e.prototype.getDataParams=function(t){var a=r.prototype.getDataParams.apply(this,arguments),n=this.getData().tree.getNodeByDataIndex(t);return a.treeAncestors=Xh(n,this),a.collapsed=!n.isExpand,a},e.type="series.tree",e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(kt);function tG(r,e,t){for(var a=[r],n=[],i;i=a.pop();)if(n.push(i),i.isExpand){var o=i.children;if(o.length)for(var s=0;s=0;i--)t.push(n[i])}}function eG(r,e){r.eachSeriesByType("tree",function(t){rG(t,e)})}function rG(r,e){var t=O5(r,e);r.layoutInfo=t;var a=r.get("layout"),n=0,i=0,o=null;a==="radial"?(n=2*Math.PI,i=Math.min(t.height,t.width)/2,o=_S(function(_,S){return(_.parentNode===S.parentNode?1:2)/_.depth})):(n=t.width,i=t.height,o=_S());var s=r.getData().tree.root,l=s.children[0];if(l){R5(s),tG(l,E5,o),s.hierNode.modifier=-l.hierNode.prelim,ns(l,k5);var u=l,f=l,h=l;ns(l,function(_){var S=_.getLayout().x;Sf.getLayout().x&&(f=_),_.depth>h.depth&&(h=_)});var v=u===f?1:o(u,f)/2,c=v-u.getLayout().x,p=0,d=0,g=0,y=0;if(a==="radial")p=n/(f.getLayout().x+v+c),d=i/(h.depth-1||1),ns(l,function(_){g=(_.getLayout().x+c)*p,y=(_.depth-1)*d;var S=Ss(g,y);_.setLayout({x:S.x,y:S.y,rawX:g,rawY:y},!0)});else{var m=r.getOrient();m==="RL"||m==="LR"?(d=i/(f.getLayout().x+v+c),p=n/(h.depth-1||1),ns(l,function(_){y=(_.getLayout().x+c)*d,g=m==="LR"?(_.depth-1)*p:n-(_.depth-1)*p,_.setLayout({x:g,y},!0)})):(m==="TB"||m==="BT")&&(p=n/(f.getLayout().x+v+c),d=i/(h.depth-1||1),ns(l,function(_){g=(_.getLayout().x+c)*p,y=m==="TB"?(_.depth-1)*d:i-(_.depth-1)*d,_.setLayout({x:g,y},!0)}))}}}function aG(r){r.eachSeriesByType("tree",function(e){var t=e.getData(),a=t.tree;a.eachNode(function(n){var i=n.getModel(),o=i.getModel("itemStyle").getItemStyle(),s=t.ensureUniqueItemVisual(n.dataIndex,"style");V(s,o)})})}function nG(r){r.registerAction({type:"treeExpandAndCollapse",event:"treeExpandAndCollapse",update:"update"},function(e,t){t.eachComponent({mainType:"series",subType:"tree",query:e},function(a){var n=e.dataIndex,i=a.getData().tree,o=i.getNodeByDataIndex(n);o.isExpand=!o.isExpand})}),r.registerAction({type:"treeRoam",event:"treeRoam",update:"none"},function(e,t,a){t.eachComponent({mainType:"series",subType:"tree",query:e},function(n){var i=n.coordinateSystem,o=Ry(i,e,void 0,a);n.setCenter&&n.setCenter(o.center),n.setZoom&&n.setZoom(o.zoom)})})}function iG(r){r.registerChartView(W5),r.registerSeriesModel(J5),r.registerLayout(eG),r.registerVisual(aG),nG(r)}var TS=["treemapZoomToNode","treemapRender","treemapMove"];function oG(r){for(var e=0;e1;)i=i.parentNode;var o=ad(r.ecModel,i.name||i.dataIndex+"",a);n.setVisual("decal",o)})}var sG=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.preventUsingHoverLayer=!0,t}return e.prototype.getInitialData=function(t,a){var n={name:t.name,children:t.data};uM(n);var i=t.levels||[],o=this.designatedVisualItemStyle={},s=new Mt({itemStyle:o},this,a);i=t.levels=lG(i,a);var l=G(i||[],function(h){return new Mt(h,s,a)},this),u=ky.createTree(n,this,f);function f(h){h.wrapMethod("getItemModel",function(v,c){var p=u.getNodeByDataIndex(c),d=p?l[p.depth]:null;return v.parentModel=d||s,v})}return u.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,a,n){var i=this.getData(),o=this.getRawValue(t),s=i.getName(t);return ie("nameValue",{name:s,value:o})},e.prototype.getDataParams=function(t){var a=r.prototype.getDataParams.apply(this,arguments),n=this.getData().tree.getNodeByDataIndex(t);return a.treeAncestors=Xh(n,this),a.treePathInfo=a.treeAncestors,a},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},V(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var a=this._idIndexMap;a||(a=this._idIndexMap=Z(),this._idIndexMapCount=0);var n=a.get(t);return n==null&&a.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var a=this.getRawData().tree.root;(!t||t!==a&&!a.contains(t))&&(this._viewRoot=a)},e.prototype.enableAriaDecal=function(){lM(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.32*.32,scaleLimit:null,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(kt);function uM(r){var e=0;C(r.children,function(a){uM(a);var n=a.value;z(n)&&(n=n[0]),e+=n});var t=r.value;z(t)&&(t=t[0]),(t==null||isNaN(t))&&(t=e),t<0&&(t=0),z(r.value)?r.value[0]=t:r.value=t}function lG(r,e){var t=Et(e.get("color")),a=Et(e.get(["aria","decal","decals"]));if(t){r=r||[];var n,i;C(r,function(s){var l=new Mt(s),u=l.get("color"),f=l.get("decal");(l.get(["itemStyle","color"])||u&&u!=="none")&&(n=!0),(l.get(["itemStyle","decal"])||f&&f!=="none")&&(i=!0)});var o=r[0]||(r[0]={});return n||(o.color=t.slice()),!i&&a&&(o.decal=a.slice()),r}}var uG=8,AS=8,Tc=5,fG=function(){function r(e){this.group=new at,e.add(this.group)}return r.prototype.render=function(e,t,a,n){var i=e.getModel("breadcrumb"),o=this.group;if(o.removeAll(),!(!i.get("show")||!a)){var s=i.getModel("itemStyle"),l=i.getModel("emphasis"),u=s.getModel("textStyle"),f=l.getModel(["itemStyle","textStyle"]),h={pos:{left:i.get("left"),right:i.get("right"),top:i.get("top"),bottom:i.get("bottom")},box:{width:t.getWidth(),height:t.getHeight()},emptyItemWidth:i.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(a,h,u),this._renderContent(e,h,s,l,u,f,n),kh(o,h.pos,h.box)}},r.prototype._prepare=function(e,t,a){for(var n=e;n;n=n.parentNode){var i=Qt(n.getModel().get("name"),""),o=a.getTextRect(i),s=Math.max(o.width+uG*2,t.emptyItemWidth);t.totalWidth+=s+AS,t.renderList.push({node:n,text:i,width:s})}},r.prototype._renderContent=function(e,t,a,n,i,o,s){for(var l=0,u=t.emptyItemWidth,f=e.get(["breadcrumb","height"]),h=dE(t.pos,t.box),v=t.totalWidth,c=t.renderList,p=n.getModel("itemStyle").getItemStyle(),d=c.length-1;d>=0;d--){var g=c[d],y=g.node,m=g.width,_=g.text;v>h.width&&(v-=m-u,m=u,_=null);var S=new Se({shape:{points:hG(l,0,m,f,d===c.length-1,d===0)},style:Q(a.getItemStyle(),{lineJoin:"bevel"}),textContent:new bt({style:Nt(i,{text:_})}),textConfig:{position:"inside"},z2:mo*1e4,onclick:it(s,y)});S.disableLabelAnimation=!0,S.getTextContent().ensureState("emphasis").style=Nt(o,{text:_}),S.ensureState("emphasis").style=p,Wt(S,n.get("focus"),n.get("blurScope"),n.get("disabled")),this.group.add(S),vG(S,e,y),l+=m+AS}},r.prototype.remove=function(){this.group.removeAll()},r}();function hG(r,e,t,a,n,i){var o=[[n?r:r-Tc,e],[r+t,e],[r+t,e+a],[n?r:r-Tc,e+a]];return!i&&o.splice(2,0,[r+t+Tc,e+a/2]),!n&&o.push([r,e+a/2]),o}function vG(r,e,t){nt(r).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:t&&t.dataIndex,name:t&&t.name},treePathInfo:t&&Xh(t,e)}}var cG=function(){function r(){this._storage=[],this._elExistsMap={}}return r.prototype.add=function(e,t,a,n,i){return this._elExistsMap[e.id]?!1:(this._elExistsMap[e.id]=!0,this._storage.push({el:e,target:t,duration:a,delay:n,easing:i}),!0)},r.prototype.finished=function(e){return this._finishedCallback=e,this},r.prototype.start=function(){for(var e=this,t=this._storage.length,a=function(){t--,t<=0&&(e._storage.length=0,e._elExistsMap={},e._finishedCallback&&e._finishedCallback())},n=0,i=this._storage.length;nDS||Math.abs(t.dy)>DS)){var a=this.seriesModel.getData().tree.root;if(!a)return;var n=a.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var a=t.originX,n=t.originY,i=t.scale;if(this._state!=="animating"){var o=this.seriesModel.getData().tree.root;if(!o)return;var s=o.getLayout();if(!s)return;var l=new ht(s.x,s.y,s.width,s.height),u=null,f=this._controllerHost;u=f.zoomLimit;var h=f.zoom=f.zoom||1;if(h*=i,u){var v=u.min||0,c=u.max||1/0;h=Math.max(Math.min(c,h),v)}var p=h/f.zoom;f.zoom=h;var d=this.seriesModel.layoutInfo;a-=d.x,n-=d.y;var g=Ge();Dr(g,g,[-a,-n]),dh(g,g,[p,p]),Dr(g,g,[a,n]),l.applyTransform(g),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:l.x,y:l.y,width:l.width,height:l.height}})}},e.prototype._initEvents=function(t){var a=this;t.on("click",function(n){if(a._state==="ready"){var i=a.seriesModel.get("nodeClick",!0);if(i){var o=a.findTarget(n.offsetX,n.offsetY);if(o){var s=o.node;if(s.getLayout().isLeafRoot)a._rootToNode(o);else if(i==="zoomToNode")a._zoomToNode(o);else if(i==="link"){var l=s.hostTree.data.getItemModel(s.dataIndex),u=l.get("link",!0),f=l.get("target",!0)||"blank";u&&If(u,f)}}}}},this)},e.prototype._renderBreadcrumb=function(t,a,n){var i=this;n||(n=t.get("leafDepth",!0)!=null?{node:t.getViewRoot()}:this.findTarget(a.getWidth()/2,a.getHeight()/2),n||(n={node:t.getData().tree.root})),(this._breadcrumb||(this._breadcrumb=new fG(this.group))).render(t,a,n.node,function(o){i._state!=="animating"&&(Oy(t.getViewRoot(),o)?i._rootToNode({node:o}):i._zoomToNode({node:o}))})},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage=is(),this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,a){var n,i=this.seriesModel.getViewRoot();return i.eachNode({attr:"viewChildren",order:"preorder"},function(o){var s=this._storage.background[o.getRawIndex()];if(s){var l=s.transformCoordToLocal(t,a),u=s.shape;if(u.x<=l[0]&&l[0]<=u.x+u.width&&u.y<=l[1]&&l[1]<=u.y+u.height)n={node:o,offsetX:l[0],offsetY:l[1]};else return!1}},this),n},e.type="treemap",e}(Rt);function is(){return{nodeGroup:[],background:[],content:[]}}function _G(r,e,t,a,n,i,o,s,l,u){if(!o)return;var f=o.getLayout(),h=r.getData(),v=o.getModel();if(h.setItemGraphicEl(o.dataIndex,null),!f||!f.isInView)return;var c=f.width,p=f.height,d=f.borderWidth,g=f.invisible,y=o.getRawIndex(),m=s&&s.getRawIndex(),_=o.viewChildren,S=f.upperHeight,b=_&&_.length,x=v.getModel("itemStyle"),w=v.getModel(["emphasis","itemStyle"]),T=v.getModel(["blur","itemStyle"]),A=v.getModel(["select","itemStyle"]),D=x.get("borderRadius")||0,M=J("nodeGroup",Ld);if(!M)return;if(l.add(M),M.x=f.x||0,M.y=f.y||0,M.markRedraw(),$f(M).nodeWidth=c,$f(M).nodeHeight=p,f.isAboveViewRoot)return M;var I=J("background",CS,u,gG);I&&B(M,I,b&&f.upperLabelHeight);var L=v.getModel("emphasis"),P=L.get("focus"),R=L.get("blurScope"),E=L.get("disabled"),N=P==="ancestor"?o.getAncestorsIndices():P==="descendant"?o.getDescendantIndices():P;if(b)js(M)&&Wn(M,!1),I&&(Wn(I,!E),h.setItemGraphicEl(o.dataIndex,I),qp(I,N,R));else{var O=J("content",CS,u,yG);O&&F(M,O),I.disableMorphing=!0,I&&js(I)&&Wn(I,!1),Wn(M,!E),h.setItemGraphicEl(o.dataIndex,M),qp(M,N,R)}return M;function B(pt,rt,dt){var lt=nt(rt);if(lt.dataIndex=o.dataIndex,lt.seriesIndex=r.seriesIndex,rt.setShape({x:0,y:0,width:c,height:p,r:D}),g)H(rt);else{rt.invisible=!1;var q=o.getVisual("style"),ut=q.stroke,Gt=LS(x);Gt.fill=ut;var At=En(w);At.fill=w.get("borderColor");var Ut=En(T);Ut.fill=T.get("borderColor");var Ft=En(A);if(Ft.fill=A.get("borderColor"),dt){var ce=c-2*d;Y(rt,ut,q.opacity,{x:d,y:0,width:ce,height:S})}else rt.removeTextContent();rt.setStyle(Gt),rt.ensureState("emphasis").style=At,rt.ensureState("blur").style=Ut,rt.ensureState("select").style=Ft,oi(rt)}pt.add(rt)}function F(pt,rt){var dt=nt(rt);dt.dataIndex=o.dataIndex,dt.seriesIndex=r.seriesIndex;var lt=Math.max(c-2*d,0),q=Math.max(p-2*d,0);if(rt.culling=!0,rt.setShape({x:d,y:d,width:lt,height:q,r:D}),g)H(rt);else{rt.invisible=!1;var ut=o.getVisual("style"),Gt=ut.fill,At=LS(x);At.fill=Gt,At.decal=ut.decal;var Ut=En(w),Ft=En(T),ce=En(A);Y(rt,Gt,ut.opacity,null),rt.setStyle(At),rt.ensureState("emphasis").style=Ut,rt.ensureState("blur").style=Ft,rt.ensureState("select").style=ce,oi(rt)}pt.add(rt)}function H(pt){!pt.invisible&&i.push(pt)}function Y(pt,rt,dt,lt){var q=v.getModel(lt?IS:MS),ut=Qt(v.get("name"),null),Gt=q.getShallow("show");ve(pt,ne(v,lt?IS:MS),{defaultText:Gt?ut:null,inheritColor:rt,defaultOpacity:dt,labelFetcher:r,labelDataIndex:o.dataIndex});var At=pt.getTextContent();if(At){var Ut=At.style,Ft=ch(Ut.padding||0);lt&&(pt.setTextConfig({layoutRect:lt}),At.disableLabelLayout=!0),At.beforeUpdate=function(){var ea=Math.max((lt?lt.width:pt.shape.width)-Ft[1]-Ft[3],0),ke=Math.max((lt?lt.height:pt.shape.height)-Ft[0]-Ft[2],0);(Ut.width!==ea||Ut.height!==ke)&&At.setStyle({width:ea,height:ke})},Ut.truncateMinChar=2,Ut.lineOverflow="truncate",j(Ut,lt,f);var ce=At.getState("emphasis");j(ce?ce.style:null,lt,f)}}function j(pt,rt,dt){var lt=pt?pt.text:null;if(!rt&&dt.isLeafRoot&<!=null){var q=r.get("drillDownIcon",!0);pt.text=q?q+" "+lt:lt}}function J(pt,rt,dt,lt){var q=m!=null&&t[pt][m],ut=n[pt];return q?(t[pt][m]=null,ct(ut,q)):g||(q=new rt,q instanceof ur&&(q.z2=SG(dt,lt)),xt(ut,q)),e[pt][y]=q}function ct(pt,rt){var dt=pt[y]={};rt instanceof Ld?(dt.oldX=rt.x,dt.oldY=rt.y):dt.oldShape=V({},rt.shape)}function xt(pt,rt){var dt=pt[y]={},lt=o.parentNode,q=rt instanceof at;if(lt&&(!a||a.direction==="drillDown")){var ut=0,Gt=0,At=n.background[lt.getRawIndex()];!a&&At&&At.oldShape&&(ut=At.oldShape.width,Gt=At.oldShape.height),q?(dt.oldX=0,dt.oldY=Gt):dt.oldShape={x:ut,y:Gt,width:0,height:0}}dt.fadein=!q}}function SG(r,e){return r*dG+e}var hl=C,xG=tt,Zf=-1,ae=function(){function r(e){var t=e.mappingMethod,a=e.type,n=this.option=et(e);this.type=a,this.mappingMethod=t,this._normalizeData=TG[t];var i=r.visualHandlers[a];this.applyVisual=i.applyVisual,this.getColorMapper=i.getColorMapper,this._normalizedToVisual=i._normalizedToVisual[t],t==="piecewise"?(Ac(n),bG(n)):t==="category"?n.categories?wG(n):Ac(n,!0):(me(t!=="linear"||n.dataExtent),Ac(n))}return r.prototype.mapValueToVisual=function(e){var t=this._normalizeData(e);return this._normalizedToVisual(t,e)},r.prototype.getNormalizer=function(){return X(this._normalizeData,this)},r.listVisualTypes=function(){return St(r.visualHandlers)},r.isValidType=function(e){return r.visualHandlers.hasOwnProperty(e)},r.eachVisual=function(e,t,a){tt(e)?C(e,t,a):t.call(a,e)},r.mapVisual=function(e,t,a){var n,i=z(e)?[]:tt(e)?{}:(n=!0,null);return r.eachVisual(e,function(o,s){var l=t.call(a,o,s);n?i=l:i[s]=l}),i},r.retrieveVisuals=function(e){var t={},a;return e&&hl(r.visualHandlers,function(n,i){e.hasOwnProperty(i)&&(t[i]=e[i],a=!0)}),a?t:null},r.prepareVisualTypes=function(e){if(z(e))e=e.slice();else if(xG(e)){var t=[];hl(e,function(a,n){t.push(n)}),e=t}else return[];return e.sort(function(a,n){return n==="color"&&a!=="color"&&a.indexOf("color")===0?1:-1}),e},r.dependsOn=function(e,t){return t==="color"?!!(e&&e.indexOf(t)===0):e===t},r.findPieceIndex=function(e,t,a){for(var n,i=1/0,o=0,s=t.length;o=0;i--)a[i]==null&&(delete t[e[i]],e.pop())}function Ac(r,e){var t=r.visual,a=[];tt(t)?hl(t,function(i){a.push(i)}):t!=null&&a.push(t);var n={color:1,symbol:1};!e&&a.length===1&&!n.hasOwnProperty(r.type)&&(a[1]=a[0]),fM(r,a)}function Lu(r){return{applyVisual:function(e,t,a){var n=this.mapValueToVisual(e);a("color",r(t("color"),n))},_normalizedToVisual:Pd([0,1])}}function PS(r){var e=this.option.visual;return e[Math.round(Lt(r,[0,1],[0,e.length-1],!0))]||{}}function os(r){return function(e,t,a){a(r,this.mapValueToVisual(e))}}function xs(r){var e=this.option.visual;return e[this.option.loop&&r!==Zf?r%e.length:r]}function kn(){return this.option.visual[0]}function Pd(r){return{linear:function(e){return Lt(e,r,this.option.visual,!0)},category:xs,piecewise:function(e,t){var a=Rd.call(this,t);return a==null&&(a=Lt(e,r,this.option.visual,!0)),a},fixed:kn}}function Rd(r){var e=this.option,t=e.pieceList;if(e.hasSpecialVisual){var a=ae.findPieceIndex(r,t),n=t[a];if(n&&n.visual)return n.visual[this.type]}}function fM(r,e){return r.visual=e,r.type==="color"&&(r.parsedVisual=G(e,function(t){var a=Ie(t);return a||[0,0,0,1]})),e}var TG={linear:function(r){return Lt(r,this.option.dataExtent,[0,1],!0)},piecewise:function(r){var e=this.option.pieceList,t=ae.findPieceIndex(r,e,!0);if(t!=null)return Lt(t,[0,e.length-1],[0,1],!0)},category:function(r){var e=this.option.categories?this.option.categoryMap[r]:r;return e??Zf},fixed:Yt};function Pu(r,e,t){return r?e<=t:e=t.length||d===t[d.depth]){var y=LG(n,l,d,g,p,a);vM(d,y,t,a)}})}}}function DG(r,e,t){var a=V({},e),n=t.designatedVisualItemStyle;return C(["color","colorAlpha","colorSaturation"],function(i){n[i]=e[i];var o=r.get(i);n[i]=null,o!=null&&(a[i]=o)}),a}function RS(r){var e=Cc(r,"color");if(e){var t=Cc(r,"colorAlpha"),a=Cc(r,"colorSaturation");return a&&(e=Qi(e,null,null,a)),t&&(e=Xs(e,t)),e}}function MG(r,e){return e!=null?Qi(e,null,null,r):null}function Cc(r,e){var t=r[e];if(t!=null&&t!=="none")return t}function IG(r,e,t,a,n,i){if(!(!i||!i.length)){var o=Dc(e,"color")||n.color!=null&&n.color!=="none"&&(Dc(e,"colorAlpha")||Dc(e,"colorSaturation"));if(o){var s=e.get("visualMin"),l=e.get("visualMax"),u=t.dataExtent.slice();s!=null&&su[1]&&(u[1]=l);var f=e.get("colorMappingBy"),h={type:o.name,dataExtent:u,visual:o.range};h.type==="color"&&(f==="index"||f==="id")?(h.mappingMethod="category",h.loop=!0):h.mappingMethod="linear";var v=new ae(h);return hM(v).drColorMappingBy=f,v}}}function Dc(r,e){var t=r.get(e);return z(t)&&t.length?{name:e,range:t}:null}function LG(r,e,t,a,n,i){var o=V({},e);if(n){var s=n.type,l=s==="color"&&hM(n).drColorMappingBy,u=l==="index"?a:l==="id"?i.mapIdToIndex(t.getId()):t.getValue(r.get("visualDimension"));o[s]=n.mapValueToVisual(u)}return o}var vl=Math.max,qf=Math.min,ES=ee,Ny=C,cM=["itemStyle","borderWidth"],PG=["itemStyle","gapWidth"],RG=["upperLabel","show"],EG=["upperLabel","height"];const kG={seriesType:"treemap",reset:function(r,e,t,a){var n=t.getWidth(),i=t.getHeight(),o=r.option,s=jt(r.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()}),l=o.size||[],u=W(ES(s.width,l[0]),n),f=W(ES(s.height,l[1]),i),h=a&&a.type,v=["treemapZoomToNode","treemapRootToNode"],c=fl(a,v,r),p=h==="treemapRender"||h==="treemapMove"?a.rootRect:null,d=r.getViewRoot(),g=sM(d);if(h!=="treemapMove"){var y=h==="treemapZoomToNode"?GG(r,c,d,u,f):p?[p.width,p.height]:[u,f],m=o.sort;m&&m!=="asc"&&m!=="desc"&&(m="desc");var _={squareRatio:o.squareRatio,sort:m,leafDepth:o.leafDepth};d.hostTree.clearLayouts();var S={x:0,y:0,width:y[0],height:y[1],area:y[0]*y[1]};d.setLayout(S),pM(d,_,!1,0),S=d.getLayout(),Ny(g,function(x,w){var T=(g[w+1]||d).getValue();x.setLayout(V({dataExtent:[T,T],borderWidth:0,upperHeight:0},S))})}var b=r.getData().tree.root;b.setLayout(FG(s,p,c),!0),r.setLayoutInfo(s),dM(b,new ht(-s.x,-s.y,n,i),g,d,0)}};function pM(r,e,t,a){var n,i;if(!r.isRemoved()){var o=r.getLayout();n=o.width,i=o.height;var s=r.getModel(),l=s.get(cM),u=s.get(PG)/2,f=gM(s),h=Math.max(l,f),v=l-u,c=h-u;r.setLayout({borderWidth:l,upperHeight:h,upperLabelHeight:f},!0),n=vl(n-2*v,0),i=vl(i-v-c,0);var p=n*i,d=OG(r,s,p,e,t,a);if(d.length){var g={x:v,y:c,width:n,height:i},y=qf(n,i),m=1/0,_=[];_.area=0;for(var S=0,b=d.length;S=0;l--){var u=n[a==="asc"?o-l-1:l].getValue();u/t*es[1]&&(s[1]=u)})),{sum:a,dataExtent:s}}function zG(r,e,t){for(var a=0,n=1/0,i=0,o=void 0,s=r.length;ia&&(a=o));var l=r.area*r.area,u=e*e*t;return l?vl(u*a/l,l/(u*n)):1/0}function kS(r,e,t,a,n){var i=e===t.width?0:1,o=1-i,s=["x","y"],l=["width","height"],u=t[s[i]],f=e?r.area/e:0;(n||f>t[l[o]])&&(f=t[l[o]]);for(var h=0,v=r.length;hGp&&(u=Gp),i=s}ua&&(a=e);var i=a%2?a+2:a+3;n=[];for(var o=0;o0&&(b[0]=-b[0],b[1]=-b[1]);var w=S[0]<0?-1:1;if(i.__position!=="start"&&i.__position!=="end"){var T=-Math.atan2(S[1],S[0]);h[0].8?"left":v[0]<-.8?"right":"center",d=v[1]>.8?"top":v[1]<-.8?"bottom":"middle";break;case"start":i.x=-v[0]*y+f[0],i.y=-v[1]*m+f[1],p=v[0]>.8?"right":v[0]<-.8?"left":"center",d=v[1]>.8?"bottom":v[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=y*w+f[0],i.y=f[1]+A,p=S[0]<0?"right":"left",i.originX=-y*w,i.originY=-A;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+A,p="center",i.originY=-A;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-y*w+h[0],i.y=h[1]+A,p=S[0]>=0?"right":"left",i.originX=y*w,i.originY=-A;break}i.scaleX=i.scaleY=o,i.setStyle({verticalAlign:i.__verticalAlign||d,align:i.__align||p})}},e}(at),Fy=function(){function r(e){this.group=new at,this._LineCtor=e||Gy}return r.prototype.updateData=function(e){var t=this;this._progressiveEls=null;var a=this,n=a.group,i=a._lineData;a._lineData=e,i||n.removeAll();var o=GS(e);e.diff(i).add(function(s){t._doAdd(e,s,o)}).update(function(s,l){t._doUpdate(i,e,l,s,o)}).remove(function(s){n.remove(i.getItemGraphicEl(s))}).execute()},r.prototype.updateLayout=function(){var e=this._lineData;e&&e.eachItemGraphicEl(function(t,a){t.updateLayout(e,a)},this)},r.prototype.incrementalPrepareUpdate=function(e){this._seriesScope=GS(e),this._lineData=null,this.group.removeAll()},r.prototype.incrementalUpdate=function(e,t){this._progressiveEls=[];function a(s){!s.isGroup&&!iF(s)&&(s.incremental=!0,s.ensureState("emphasis").hoverLayer=!0)}for(var n=e.start;n0}function GS(r){var e=r.hostModel,t=e.getModel("emphasis");return{lineStyle:e.getModel("lineStyle").getLineStyle(),emphasisLineStyle:t.getModel(["lineStyle"]).getLineStyle(),blurLineStyle:e.getModel(["blur","lineStyle"]).getLineStyle(),selectLineStyle:e.getModel(["select","lineStyle"]).getLineStyle(),emphasisDisabled:t.get("disabled"),blurScope:t.get("blurScope"),focus:t.get("focus"),labelStatesModels:ne(e)}}function FS(r){return isNaN(r[0])||isNaN(r[1])}function Rc(r){return r&&!FS(r[0])&&!FS(r[1])}var Ec=[],kc=[],Oc=[],ki=se,Nc=Wa,HS=Math.abs;function WS(r,e,t){for(var a=r[0],n=r[1],i=r[2],o=1/0,s,l=t*t,u=.1,f=.1;f<=.9;f+=.1){Ec[0]=ki(a[0],n[0],i[0],f),Ec[1]=ki(a[1],n[1],i[1],f);var h=HS(Nc(Ec,e)-l);h=0?s=s+u:s=s-u:p>=0?s=s-u:s=s+u}return s}function Bc(r,e){var t=[],a=Us,n=[[],[],[]],i=[[],[]],o=[];e/=2,r.eachEdge(function(s,l){var u=s.getLayout(),f=s.getVisual("fromSymbol"),h=s.getVisual("toSymbol");u.__original||(u.__original=[Ur(u[0]),Ur(u[1])],u[2]&&u.__original.push(Ur(u[2])));var v=u.__original;if(u[2]!=null){if(ge(n[0],v[0]),ge(n[1],v[2]),ge(n[2],v[1]),f&&f!=="none"){var c=ws(s.node1),p=WS(n,v[0],c*e);a(n[0][0],n[1][0],n[2][0],p,t),n[0][0]=t[3],n[1][0]=t[4],a(n[0][1],n[1][1],n[2][1],p,t),n[0][1]=t[3],n[1][1]=t[4]}if(h&&h!=="none"){var c=ws(s.node2),p=WS(n,v[1],c*e);a(n[0][0],n[1][0],n[2][0],p,t),n[1][0]=t[1],n[2][0]=t[2],a(n[0][1],n[1][1],n[2][1],p,t),n[1][1]=t[1],n[2][1]=t[2]}ge(u[0],n[0]),ge(u[1],n[2]),ge(u[2],n[1])}else{if(ge(i[0],v[0]),ge(i[1],v[1]),Na(o,i[1],i[0]),hi(o,o),f&&f!=="none"){var c=ws(s.node1);vf(i[0],i[0],o,c*e)}if(h&&h!=="none"){var c=ws(s.node2);vf(i[1],i[1],o,-c*e)}ge(u[0],i[0]),ge(u[1],i[1])}})}function US(r){return r.type==="view"}var oF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t,a){var n=new El,i=new Fy,o=this.group;this._controller=new Vl(a.getZr()),this._controllerHost={target:o},o.add(n.group),o.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},e.prototype.render=function(t,a,n){var i=this,o=t.coordinateSystem;this._model=t;var s=this._symbolDraw,l=this._lineDraw,u=this.group;if(US(o)){var f={x:o.x,y:o.y,scaleX:o.scaleX,scaleY:o.scaleY};this._firstRender?u.attr(f):Dt(u,f,t)}Bc(t.getGraph(),bs(t));var h=t.getData();s.updateData(h);var v=t.getEdgeData();l.updateData(v),this._updateNodeAndLinkScale(),this._updateController(t,a,n),clearTimeout(this._layoutTimeout);var c=t.forceLayout,p=t.get(["force","layoutAnimation"]);c&&this._startForceLayoutIteration(c,p);var d=t.get("layout");h.graph.eachNode(function(_){var S=_.dataIndex,b=_.getGraphicEl(),x=_.getModel();if(b){b.off("drag").off("dragend");var w=x.get("draggable");w&&b.on("drag",function(A){switch(d){case"force":c.warmUp(),!i._layouting&&i._startForceLayoutIteration(c,p),c.setFixed(S),h.setItemLayout(S,[b.x,b.y]);break;case"circular":h.setItemLayout(S,[b.x,b.y]),_.setLayout({fixed:!0},!0),zy(t,"symbolSize",_,[A.offsetX,A.offsetY]),i.updateLayout(t);break;case"none":default:h.setItemLayout(S,[b.x,b.y]),Vy(t.getGraph(),t),i.updateLayout(t);break}}).on("dragend",function(){c&&c.setUnfixed(S)}),b.setDraggable(w,!!x.get("cursor"));var T=x.get(["emphasis","focus"]);T==="adjacency"&&(nt(b).focus=_.getAdjacentDataIndices())}}),h.graph.eachEdge(function(_){var S=_.getGraphicEl(),b=_.getModel().get(["emphasis","focus"]);S&&b==="adjacency"&&(nt(S).focus={edge:[_.dataIndex],node:[_.node1.dataIndex,_.node2.dataIndex]})});var g=t.get("layout")==="circular"&&t.get(["circular","rotateLabel"]),y=h.getLayout("cx"),m=h.getLayout("cy");h.graph.eachNode(function(_){SM(_,g,y,m)}),this._firstRender=!1},e.prototype.dispose=function(){this.remove(),this._controller&&this._controller.dispose(),this._controllerHost=null},e.prototype._startForceLayoutIteration=function(t,a){var n=this;(function i(){t.step(function(o){n.updateLayout(n._model),(n._layouting=!o)&&(a?n._layoutTimeout=setTimeout(i,16):i())})})()},e.prototype._updateController=function(t,a,n){var i=this,o=this._controller,s=this._controllerHost,l=this.group;if(o.setPointerChecker(function(u,f,h){var v=l.getBoundingRect();return v.applyTransform(l.transform),v.contain(f,h)&&!Uh(u,n,t)}),!US(t.coordinateSystem)){o.disable();return}o.enable(t.get("roam")),s.zoomLimit=t.get("scaleLimit"),s.zoom=t.coordinateSystem.getZoom(),o.off("pan").off("zoom").on("pan",function(u){Iy(s,u.dx,u.dy),n.dispatchAction({seriesId:t.id,type:"graphRoam",dx:u.dx,dy:u.dy})}).on("zoom",function(u){Ly(s,u.scale,u.originX,u.originY),n.dispatchAction({seriesId:t.id,type:"graphRoam",zoom:u.scale,originX:u.originX,originY:u.originY}),i._updateNodeAndLinkScale(),Bc(t.getGraph(),bs(t)),i._lineDraw.updateLayout(),n.updateLabelLayout()})},e.prototype._updateNodeAndLinkScale=function(){var t=this._model,a=t.getData(),n=bs(t);a.eachItemGraphicEl(function(i,o){i&&i.setSymbolScale(n)})},e.prototype.updateLayout=function(t){Bc(t.getGraph(),bs(t)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},e.prototype.remove=function(){clearTimeout(this._layoutTimeout),this._layouting=!1,this._layoutTimeout=null,this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},e.type="graph",e}(Rt);function Oi(r){return"_EC_"+r}var sF=function(){function r(e){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=e||!1}return r.prototype.isDirected=function(){return this._directed},r.prototype.addNode=function(e,t){e=e==null?""+t:""+e;var a=this._nodesMap;if(!a[Oi(e)]){var n=new On(e,t);return n.hostGraph=this,this.nodes.push(n),a[Oi(e)]=n,n}},r.prototype.getNodeByIndex=function(e){var t=this.data.getRawIndex(e);return this.nodes[t]},r.prototype.getNodeById=function(e){return this._nodesMap[Oi(e)]},r.prototype.addEdge=function(e,t,a){var n=this._nodesMap,i=this._edgesMap;if(Ct(e)&&(e=this.nodes[e]),Ct(t)&&(t=this.nodes[t]),e instanceof On||(e=n[Oi(e)]),t instanceof On||(t=n[Oi(t)]),!(!e||!t)){var o=e.id+"-"+t.id,s=new bM(e,t,a);return s.hostGraph=this,this._directed&&(e.outEdges.push(s),t.inEdges.push(s)),e.edges.push(s),e!==t&&t.edges.push(s),this.edges.push(s),i[o]=s,s}},r.prototype.getEdgeByIndex=function(e){var t=this.edgeData.getRawIndex(e);return this.edges[t]},r.prototype.getEdge=function(e,t){e instanceof On&&(e=e.id),t instanceof On&&(t=t.id);var a=this._edgesMap;return this._directed?a[e+"-"+t]:a[e+"-"+t]||a[t+"-"+e]},r.prototype.eachNode=function(e,t){for(var a=this.nodes,n=a.length,i=0;i=0&&e.call(t,a[i],i)},r.prototype.eachEdge=function(e,t){for(var a=this.edges,n=a.length,i=0;i=0&&a[i].node1.dataIndex>=0&&a[i].node2.dataIndex>=0&&e.call(t,a[i],i)},r.prototype.breadthFirstTraverse=function(e,t,a,n){if(t instanceof On||(t=this._nodesMap[Oi(t)]),!!t){for(var i=a==="out"?"outEdges":a==="in"?"inEdges":"edges",o=0;o=0&&l.node2.dataIndex>=0});for(var i=0,o=n.length;i=0&&this[r][e].setItemVisual(this.dataIndex,t,a)},getVisual:function(t){return this[r][e].getItemVisual(this.dataIndex,t)},setLayout:function(t,a){this.dataIndex>=0&&this[r][e].setItemLayout(this.dataIndex,t,a)},getLayout:function(){return this[r][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[r][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[r][e].getRawIndex(this.dataIndex)}}}Xt(On,wM("hostGraph","data"));Xt(bM,wM("hostGraph","edgeData"));function TM(r,e,t,a,n){for(var i=new sF(a),o=0;o "+v)),u++)}var c=t.get("coordinateSystem"),p;if(c==="cartesian2d"||c==="polar")p=Jr(r,t);else{var d=Io.get(c),g=d?d.dimensions||[]:[];vt(g,"value")<0&&g.concat(["value"]);var y=Eo(r,{coordDimensions:g,encodeDefine:t.getEncode()}).dimensions;p=new Le(y,t),p.initData(r)}var m=new Le(["value"],t);return m.initData(l,s),n&&n(p,m),iM({mainData:p,struct:i,structAttr:"graph",datas:{node:p,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),i.update(),i}var lF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.hasSymbolVisual=!0,t}return e.prototype.init=function(t){r.prototype.init.apply(this,arguments);var a=this;function n(){return a._categoriesData}this.legendVisualProvider=new Bl(n,n),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},e.prototype.mergeOption=function(t){r.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(t.edges||t.links),this._updateCategoriesData()},e.prototype.mergeDefaultAndTheme=function(t){r.prototype.mergeDefaultAndTheme.apply(this,arguments),ai(t,"edgeLabel",["show"])},e.prototype.getInitialData=function(t,a){var n=t.edges||t.links||[],i=t.data||t.nodes||[],o=this;if(i&&n){ZG(this);var s=TM(i,n,this,!0,l);return C(s.edges,function(u){qG(u.node1,u.node2,this,u.dataIndex)},this),s.data}function l(u,f){u.wrapMethod("getItemModel",function(p){var d=o._categoriesModels,g=p.getShallow("category"),y=d[g];return y&&(y.parentModel=p.parentModel,p.parentModel=y),p});var h=Mt.prototype.getModel;function v(p,d){var g=h.call(this,p,d);return g.resolveParentPath=c,g}f.wrapMethod("getItemModel",function(p){return p.resolveParentPath=c,p.getModel=v,p});function c(p){if(p&&(p[0]==="label"||p[1]==="label")){var d=p.slice();return p[0]==="label"?d[0]="edgeLabel":p[1]==="label"&&(d[1]="edgeLabel"),d}return p}}},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.getCategoriesData=function(){return this._categoriesData},e.prototype.formatTooltip=function(t,a,n){if(n==="edge"){var i=this.getData(),o=this.getDataParams(t,n),s=i.graph.getEdgeByIndex(t),l=i.getName(s.node1.dataIndex),u=i.getName(s.node2.dataIndex),f=[];return l!=null&&f.push(l),u!=null&&f.push(u),ie("nameValue",{name:f.join(" > "),value:o.value,noValue:o.value==null})}var h=ZA({series:this,dataIndex:t,multipleSeries:a});return h},e.prototype._updateCategoriesData=function(){var t=G(this.option.categories||[],function(n){return n.value!=null?n:V({value:0},n)}),a=new Le(["value"],this);a.initData(t),this._categoriesData=a,this._categoriesModels=a.mapArray(function(n){return a.getItemModel(n)})},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.isAnimationEnabled=function(){return r.prototype.isAnimationEnabled.call(this)&&!(this.get("layout")==="force"&&this.get(["force","layoutAnimation"]))},e.type="series.graph",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(kt),uF={type:"graphRoam",event:"graphRoam",update:"none"};function fF(r){r.registerChartView(oF),r.registerSeriesModel(lF),r.registerProcessor(WG),r.registerVisual(UG),r.registerVisual(YG),r.registerLayout(KG),r.registerLayout(r.PRIORITY.VISUAL.POST_CHART_LAYOUT,QG),r.registerLayout(tF),r.registerCoordinateSystem("graphView",{dimensions:zl.dimensions,create:rF}),r.registerAction({type:"focusNodeAdjacency",event:"focusNodeAdjacency",update:"series:focusNodeAdjacency"},Yt),r.registerAction({type:"unfocusNodeAdjacency",event:"unfocusNodeAdjacency",update:"series:unfocusNodeAdjacency"},Yt),r.registerAction(uF,function(e,t,a){t.eachComponent({mainType:"series",query:e},function(n){var i=n.coordinateSystem,o=Ry(i,e,void 0,a);n.setCenter&&n.setCenter(o.center),n.setZoom&&n.setZoom(o.zoom)})})}var hF=function(){function r(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0}return r}(),vF=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="pointer",a}return e.prototype.getDefaultShape=function(){return new hF},e.prototype.buildPath=function(t,a){var n=Math.cos,i=Math.sin,o=a.r,s=a.width,l=a.angle,u=a.x-n(l)*s*(s>=o/3?1:2),f=a.y-i(l)*s*(s>=o/3?1:2);l=a.angle-Math.PI/2,t.moveTo(u,f),t.lineTo(a.x+n(l)*s,a.y+i(l)*s),t.lineTo(a.x+n(a.angle)*o,a.y+i(a.angle)*o),t.lineTo(a.x-n(l)*s,a.y-i(l)*s),t.lineTo(u,f)},e}(yt);function cF(r,e){var t=r.get("center"),a=e.getWidth(),n=e.getHeight(),i=Math.min(a,n),o=W(t[0],e.getWidth()),s=W(t[1],e.getHeight()),l=W(r.get("radius"),i/2);return{cx:o,cy:s,r:l}}function Eu(r,e){var t=r==null?"":r+"";return e&&(U(e)?t=e.replace("{value}",t):K(e)&&(t=e(r))),t}var pF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),o=cF(t,n);this._renderMain(t,a,n,i,o),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,a,n,i,o){var s=this.group,l=t.get("clockwise"),u=-t.get("startAngle")/180*Math.PI,f=-t.get("endAngle")/180*Math.PI,h=t.getModel("axisLine"),v=h.get("roundCap"),c=v?Uf:_e,p=h.get("show"),d=h.getModel("lineStyle"),g=d.get("width"),y=[u,f];Dg(y,!l),u=y[0],f=y[1];for(var m=f-u,_=u,S=[],b=0;p&&b=A&&(D===0?0:i[D-1][0])Math.PI/2&&(J+=Math.PI)):j==="tangential"?J=-T-Math.PI/2:Ct(j)&&(J=j*Math.PI/180),J===0?h.add(new bt({style:Nt(_,{text:B,x:H,y:Y,verticalAlign:R<-.8?"top":R>.8?"bottom":"middle",align:P<-.4?"left":P>.4?"right":"center"},{inheritColor:F}),silent:!0})):h.add(new bt({style:Nt(_,{text:B,x:H,y:Y,verticalAlign:"middle",align:"center"},{inheritColor:F}),silent:!0,originX:H,originY:Y,rotation:J}))}if(m.get("show")&&E!==S){var N=m.get("distance");N=N?N+f:f;for(var ct=0;ct<=b;ct++){P=Math.cos(T),R=Math.sin(T);var xt=new Jt({shape:{x1:P*(p-N)+v,y1:R*(p-N)+c,x2:P*(p-w-N)+v,y2:R*(p-w-N)+c},silent:!0,style:I});I.stroke==="auto"&&xt.setStyle({stroke:i((E+ct/b)/S)}),h.add(xt),T+=D}T-=D}else T+=A}},e.prototype._renderPointer=function(t,a,n,i,o,s,l,u,f){var h=this.group,v=this._data,c=this._progressEls,p=[],d=t.get(["pointer","show"]),g=t.getModel("progress"),y=g.get("show"),m=t.getData(),_=m.mapDimension("value"),S=+t.get("min"),b=+t.get("max"),x=[S,b],w=[s,l];function T(D,M){var I=m.getItemModel(D),L=I.getModel("pointer"),P=W(L.get("width"),o.r),R=W(L.get("length"),o.r),E=t.get(["pointer","icon"]),N=L.get("offsetCenter"),O=W(N[0],o.r),B=W(N[1],o.r),F=L.get("keepAspect"),H;return E?H=Zt(E,O-P/2,B-R,P,R,null,F):H=new vF({shape:{angle:-Math.PI/2,width:P,r:R,x:O,y:B}}),H.rotation=-(M+Math.PI/2),H.x=o.cx,H.y=o.cy,H}function A(D,M){var I=g.get("roundCap"),L=I?Uf:_e,P=g.get("overlap"),R=P?g.get("width"):f/m.count(),E=P?o.r-R:o.r-(D+1)*R,N=P?o.r:o.r-D*R,O=new L({shape:{startAngle:s,endAngle:M,cx:o.cx,cy:o.cy,clockwise:u,r0:E,r:N}});return P&&(O.z2=b-m.get(_,D)%b),O}(y||d)&&(m.diff(v).add(function(D){var M=m.get(_,D);if(d){var I=T(D,s);Vt(I,{rotation:-((isNaN(+M)?w[0]:Lt(M,x,w,!0))+Math.PI/2)},t),h.add(I),m.setItemGraphicEl(D,I)}if(y){var L=A(D,s),P=g.get("clip");Vt(L,{shape:{endAngle:Lt(M,x,w,P)}},t),h.add(L),Xp(t.seriesIndex,m.dataType,D,L),p[D]=L}}).update(function(D,M){var I=m.get(_,D);if(d){var L=v.getItemGraphicEl(M),P=L?L.rotation:s,R=T(D,P);R.rotation=P,Dt(R,{rotation:-((isNaN(+I)?w[0]:Lt(I,x,w,!0))+Math.PI/2)},t),h.add(R),m.setItemGraphicEl(D,R)}if(y){var E=c[M],N=E?E.shape.endAngle:s,O=A(D,N),B=g.get("clip");Dt(O,{shape:{endAngle:Lt(I,x,w,B)}},t),h.add(O),Xp(t.seriesIndex,m.dataType,D,O),p[D]=O}}).execute(),m.each(function(D){var M=m.getItemModel(D),I=M.getModel("emphasis"),L=I.get("focus"),P=I.get("blurScope"),R=I.get("disabled");if(d){var E=m.getItemGraphicEl(D),N=m.getItemVisual(D,"style"),O=N.fill;if(E instanceof oe){var B=E.style;E.useStyle(V({image:B.image,x:B.x,y:B.y,width:B.width,height:B.height},N))}else E.useStyle(N),E.type!=="pointer"&&E.setColor(O);E.setStyle(M.getModel(["pointer","itemStyle"]).getItemStyle()),E.style.fill==="auto"&&E.setStyle("fill",i(Lt(m.get(_,D),x,[0,1],!0))),E.z2EmphasisLift=0,he(E,M),Wt(E,L,P,R)}if(y){var F=p[D];F.useStyle(m.getItemVisual(D,"style")),F.setStyle(M.getModel(["progress","itemStyle"]).getItemStyle()),F.z2EmphasisLift=0,he(F,M),Wt(F,L,P,R)}}),this._progressEls=p)},e.prototype._renderAnchor=function(t,a){var n=t.getModel("anchor"),i=n.get("show");if(i){var o=n.get("size"),s=n.get("icon"),l=n.get("offsetCenter"),u=n.get("keepAspect"),f=Zt(s,a.cx-o/2+W(l[0],a.r),a.cy-o/2+W(l[1],a.r),o,o,null,u);f.z2=n.get("showAbove")?1:0,f.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(f)}},e.prototype._renderTitleAndDetail=function(t,a,n,i,o){var s=this,l=t.getData(),u=l.mapDimension("value"),f=+t.get("min"),h=+t.get("max"),v=new at,c=[],p=[],d=t.isAnimationEnabled(),g=t.get(["pointer","showAbove"]);l.diff(this._data).add(function(y){c[y]=new bt({silent:!0}),p[y]=new bt({silent:!0})}).update(function(y,m){c[y]=s._titleEls[m],p[y]=s._detailEls[m]}).execute(),l.each(function(y){var m=l.getItemModel(y),_=l.get(u,y),S=new at,b=i(Lt(_,[f,h],[0,1],!0)),x=m.getModel("title");if(x.get("show")){var w=x.get("offsetCenter"),T=o.cx+W(w[0],o.r),A=o.cy+W(w[1],o.r),D=c[y];D.attr({z2:g?0:2,style:Nt(x,{x:T,y:A,text:l.getName(y),align:"center",verticalAlign:"middle"},{inheritColor:b})}),S.add(D)}var M=m.getModel("detail");if(M.get("show")){var I=M.get("offsetCenter"),L=o.cx+W(I[0],o.r),P=o.cy+W(I[1],o.r),R=W(M.get("width"),o.r),E=W(M.get("height"),o.r),N=t.get(["progress","show"])?l.getItemVisual(y,"style").fill:b,D=p[y],O=M.get("formatter");D.attr({z2:g?0:2,style:Nt(M,{x:L,y:P,text:Eu(_,O),width:isNaN(R)?null:R,height:isNaN(E)?null:E,align:"center",verticalAlign:"middle"},{inheritColor:N})}),tA(D,{normal:M},_,function(F){return Eu(F,O)}),d&&eA(D,y,l,t,{getFormattedLabel:function(F,H,Y,j,J,ct){return Eu(ct?ct.interpolatedValue:_,O)}}),S.add(D)}v.add(S)}),this.group.add(v),this._titleEls=c,this._detailEls=p},e.type="gauge",e}(Rt),dF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.visualStyleAccessPath="itemStyle",t}return e.prototype.getInitialData=function(t,a){return No(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(kt);function gF(r){r.registerChartView(pF),r.registerSeriesModel(dF)}var yF=["itemStyle","opacity"],mF=function(r){k(e,r);function e(t,a){var n=r.call(this)||this,i=n,o=new be,s=new bt;return i.setTextContent(s),n.setTextGuideLine(o),n.updateData(t,a,!0),n}return e.prototype.updateData=function(t,a,n){var i=this,o=t.hostModel,s=t.getItemModel(a),l=t.getItemLayout(a),u=s.getModel("emphasis"),f=s.get(yF);f=f??1,n||Ir(i),i.useStyle(t.getItemVisual(a,"style")),i.style.lineJoin="round",n?(i.setShape({points:l.points}),i.style.opacity=0,Vt(i,{style:{opacity:f}},o,a)):Dt(i,{style:{opacity:f},shape:{points:l.points}},o,a),he(i,s),this._updateLabel(t,a),Wt(this,u.get("focus"),u.get("blurScope"),u.get("disabled"))},e.prototype._updateLabel=function(t,a){var n=this,i=this.getTextGuideLine(),o=n.getTextContent(),s=t.hostModel,l=t.getItemModel(a),u=t.getItemLayout(a),f=u.label,h=t.getItemVisual(a,"style"),v=h.fill;ve(o,ne(l),{labelFetcher:t.hostModel,labelDataIndex:a,defaultOpacity:h.opacity,defaultText:t.getName(a)},{normal:{align:f.textAlign,verticalAlign:f.verticalAlign}}),n.setTextConfig({local:!0,inside:!!f.inside,insideStroke:v,outsideFill:v});var c=f.linePoints;i.setShape({points:c}),n.textGuideLineConfig={anchor:c?new ft(c[0][0],c[0][1]):null},Dt(o,{style:{x:f.x,y:f.y}},s,a),o.attr({rotation:f.rotation,originX:f.x,originY:f.y,z2:10}),my(n,_y(l),{stroke:v})},e}(Se),_F=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.ignoreLabelLineUpdate=!0,t}return e.prototype.render=function(t,a,n){var i=t.getData(),o=this._data,s=this.group;i.diff(o).add(function(l){var u=new mF(i,l);i.setItemGraphicEl(l,u),s.add(u)}).update(function(l,u){var f=o.getItemGraphicEl(u);f.updateData(i,l),s.add(f),i.setItemGraphicEl(l,f)}).remove(function(l){var u=o.getItemGraphicEl(l);Qs(u,t,l)}).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type="funnel",e}(Rt),SF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t){r.prototype.init.apply(this,arguments),this.legendVisualProvider=new Bl(X(this.getData,this),X(this.getRawData,this)),this._defaultLabelLine(t)},e.prototype.getInitialData=function(t,a){return No(this,{coordDimensions:["value"],encodeDefaulter:it(qg,this)})},e.prototype._defaultLabelLine=function(t){ai(t,"labelLine",["show"]);var a=t.labelLine,n=t.emphasis.labelLine;a.show=a.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(t){var a=this.getData(),n=r.prototype.getDataParams.call(this,t),i=a.mapDimension("value"),o=a.getSum(i);return n.percent=o?+(a.get(i,t)/o*100).toFixed(2):0,n.$vars.push("percent"),n},e.type="series.funnel",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(kt);function xF(r,e){return jt(r.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function bF(r,e){for(var t=r.mapDimension("value"),a=r.mapArray(t,function(l){return l}),n=[],i=e==="ascending",o=0,s=r.count();oVF)return;var n=this._model.coordinateSystem.getSlidedAxisExpandWindow([r.offsetX,r.offsetY]);n.behavior!=="none"&&this._dispatchExpand({axisExpandWindow:n.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(r){if(!(this._mouseDownPoint||!zc(this,"mousemove"))){var e=this._model,t=e.coordinateSystem.getSlidedAxisExpandWindow([r.offsetX,r.offsetY]),a=t.behavior;a==="jump"&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand(a==="none"?null:{axisExpandWindow:t.axisExpandWindow,animation:a==="jump"?null:{duration:0}})}}};function zc(r,e){var t=r._model;return t.get("axisExpandable")&&t.get("axisExpandTriggerOn")===e}var FF=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(){r.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var a=this.option;t&&st(a,t,!0),this._initDimensions()},e.prototype.contains=function(t,a){var n=t.get("parallelIndex");return n!=null&&a.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){C(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],function(a){t.hasOwnProperty(a)&&(this.option[a]=t[a])},this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],a=this.parallelAxisIndex=[],n=Pt(this.ecModel.queryComponents({mainType:"parallelAxis"}),function(i){return(i.get("parallelIndex")||0)===this.componentIndex},this);C(n,function(i){t.push("dim"+i.get("dim")),a.push(i.componentIndex)})},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(mt),HF=function(r){k(e,r);function e(t,a,n,i,o){var s=r.call(this,t,a,n)||this;return s.type=i||"value",s.axisIndex=o,s}return e.prototype.isHorizontal=function(){return this.coordinateSystem.getModel().get("layout")!=="horizontal"},e}(Lr);function _i(r,e,t,a,n,i){r=r||0;var o=t[1]-t[0];if(n!=null&&(n=Ni(n,[0,o])),i!=null&&(i=Math.max(i,n??0)),a==="all"){var s=Math.abs(e[1]-e[0]);s=Ni(s,[0,o]),n=i=Ni(s,[n,i]),a=0}e[0]=Ni(e[0],t),e[1]=Ni(e[1],t);var l=Gc(e,a);e[a]+=r;var u=n||0,f=t.slice();l.sign<0?f[0]+=u:f[1]-=u,e[a]=Ni(e[a],f);var h;return h=Gc(e,a),n!=null&&(h.sign!==l.sign||h.spani&&(e[1-a]=e[a]+h.sign*i),e}function Gc(r,e){var t=r[e]-r[1-e];return{span:Math.abs(t),sign:t>0?-1:t<0?1:e?-1:1}}function Ni(r,e){return Math.min(e[1]!=null?e[1]:1/0,Math.max(e[0]!=null?e[0]:-1/0,r))}var Fc=C,CM=Math.min,DM=Math.max,$S=Math.floor,WF=Math.ceil,ZS=Ht,UF=Math.PI,YF=function(){function r(e,t,a){this.type="parallel",this._axesMap=Z(),this._axesLayout={},this.dimensions=e.dimensions,this._model=e,this._init(e,t,a)}return r.prototype._init=function(e,t,a){var n=e.dimensions,i=e.parallelAxisIndex;Fc(n,function(o,s){var l=i[s],u=t.getComponent("parallelAxis",l),f=this._axesMap.set(o,new HF(o,Pl(u),[0,0],u.get("type"),l)),h=f.type==="category";f.onBand=h&&u.get("boundaryGap"),f.inverse=u.get("inverse"),u.axis=f,f.model=u,f.coordinateSystem=u.coordinateSystem=this},this)},r.prototype.update=function(e,t){this._updateAxesFromSeries(this._model,e)},r.prototype.containPoint=function(e){var t=this._makeLayoutInfo(),a=t.axisBase,n=t.layoutBase,i=t.pixelDimIndex,o=e[1-i],s=e[i];return o>=a&&o<=a+t.axisLength&&s>=n&&s<=n+t.layoutLength},r.prototype.getModel=function(){return this._model},r.prototype._updateAxesFromSeries=function(e,t){t.eachSeries(function(a){if(e.contains(a,t)){var n=a.getData();Fc(this.dimensions,function(i){var o=this._axesMap.get(i);o.scale.unionExtentFromData(n,n.mapDimension(i)),li(o.scale,o.model)},this)}},this)},r.prototype.resize=function(e,t){this._rect=jt(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()}),this._layoutAxes()},r.prototype.getRect=function(){return this._rect},r.prototype._makeLayoutInfo=function(){var e=this._model,t=this._rect,a=["x","y"],n=["width","height"],i=e.get("layout"),o=i==="horizontal"?0:1,s=t[n[o]],l=[0,s],u=this.dimensions.length,f=ku(e.get("axisExpandWidth"),l),h=ku(e.get("axisExpandCount")||0,[0,u]),v=e.get("axisExpandable")&&u>3&&u>h&&h>1&&f>0&&s>0,c=e.get("axisExpandWindow"),p;if(c)p=ku(c[1]-c[0],l),c[1]=c[0]+p;else{p=ku(f*(h-1),l);var d=e.get("axisExpandCenter")||$S(u/2);c=[f*d-p/2],c[1]=c[0]+p}var g=(s-p)/(u-h);g<3&&(g=0);var y=[$S(ZS(c[0]/f,1))+1,WF(ZS(c[1]/f,1))-1],m=g/f*c[0];return{layout:i,pixelDimIndex:o,layoutBase:t[a[o]],layoutLength:s,axisBase:t[a[1-o]],axisLength:t[n[1-o]],axisExpandable:v,axisExpandWidth:f,axisCollapseWidth:g,axisExpandWindow:c,axisCount:u,winInnerIndices:y,axisExpandWindow0Pos:m}},r.prototype._layoutAxes=function(){var e=this._rect,t=this._axesMap,a=this.dimensions,n=this._makeLayoutInfo(),i=n.layout;t.each(function(o){var s=[0,n.axisLength],l=o.inverse?1:0;o.setExtent(s[l],s[1-l])}),Fc(a,function(o,s){var l=(n.axisExpandable?$F:XF)(s,n),u={horizontal:{x:l.position,y:n.axisLength},vertical:{x:0,y:l.position}},f={horizontal:UF/2,vertical:0},h=[u[i].x+e.x,u[i].y+e.y],v=f[i],c=Ge();an(c,c,v),Dr(c,c,h),this._axesLayout[o]={position:h,rotation:v,transform:c,axisNameAvailableWidth:l.axisNameAvailableWidth,axisLabelShow:l.axisLabelShow,nameTruncateMaxWidth:l.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}},this)},r.prototype.getAxis=function(e){return this._axesMap.get(e)},r.prototype.dataToPoint=function(e,t){return this.axisCoordToPoint(this._axesMap.get(t).dataToCoord(e),t)},r.prototype.eachActiveState=function(e,t,a,n){a==null&&(a=0),n==null&&(n=e.count());var i=this._axesMap,o=this.dimensions,s=[],l=[];C(o,function(g){s.push(e.mapDimension(g)),l.push(i.get(g).model)});for(var u=this.hasAxisBrushed(),f=a;fi*(1-h[0])?(u="jump",l=s-i*(1-h[2])):(l=s-i*h[1])>=0&&(l=s-i*(1-h[1]))<=0&&(l=0),l*=t.axisExpandWidth/f,l?_i(l,n,o,"all"):u="none";else{var c=n[1]-n[0],p=o[1]*s/c;n=[DM(0,p-c/2)],n[1]=CM(o[1],n[0]+c),n[0]=n[1]-c}return{axisExpandWindow:n,behavior:u}},r}();function ku(r,e){return CM(DM(r,e[0]),e[1])}function XF(r,e){var t=e.layoutLength/(e.axisCount-1);return{position:t*r,axisNameAvailableWidth:t,axisLabelShow:!0}}function $F(r,e){var t=e.layoutLength,a=e.axisExpandWidth,n=e.axisCount,i=e.axisCollapseWidth,o=e.winInnerIndices,s,l=i,u=!1,f;return r=0;n--)Ue(a[n])},e.prototype.getActiveState=function(t){var a=this.activeIntervals;if(!a.length)return"normal";if(t==null||isNaN(+t))return"inactive";if(a.length===1){var n=a[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,o=a.length;iQF}function EM(r){var e=r.length-1;return e<0&&(e=0),[r[0],r[e]]}function kM(r,e,t,a){var n=new at;return n.add(new wt({name:"main",style:Xy(t),silent:!0,draggable:!0,cursor:"move",drift:it(jS,r,e,n,["n","s","w","e"]),ondragend:it(fi,e,{isEnd:!0})})),C(a,function(i){n.add(new wt({name:i.join(""),style:{opacity:0},draggable:!0,silent:!0,invisible:!0,drift:it(jS,r,e,n,i),ondragend:it(fi,e,{isEnd:!0})}))}),n}function OM(r,e,t,a){var n=a.brushStyle.lineWidth||0,i=vo(n,JF),o=t[0][0],s=t[1][0],l=o-n/2,u=s-n/2,f=t[0][1],h=t[1][1],v=f-i+n/2,c=h-i+n/2,p=f-o,d=h-s,g=p+n,y=d+n;na(r,e,"main",o,s,p,d),a.transformable&&(na(r,e,"w",l,u,i,y),na(r,e,"e",v,u,i,y),na(r,e,"n",l,u,g,i),na(r,e,"s",l,c,g,i),na(r,e,"nw",l,u,i,i),na(r,e,"ne",v,u,i,i),na(r,e,"sw",l,c,i,i),na(r,e,"se",v,c,i,i))}function Vd(r,e){var t=e.__brushOption,a=t.transformable,n=e.childAt(0);n.useStyle(Xy(t)),n.attr({silent:!a,cursor:a?"move":"default"}),C([["w"],["e"],["n"],["s"],["s","e"],["s","w"],["n","e"],["n","w"]],function(i){var o=e.childOfName(i.join("")),s=i.length===1?zd(r,i[0]):i3(r,i);o&&o.attr({silent:!a,invisible:!a,cursor:a?e3[s]+"-resize":null})})}function na(r,e,t,a,n,i,o){var s=e.childOfName(t);s&&s.setShape(s3($y(r,e,[[a,n],[a+i,n+o]])))}function Xy(r){return Q({strokeNoScale:!0},r.brushStyle)}function NM(r,e,t,a){var n=[pl(r,t),pl(e,a)],i=[vo(r,t),vo(e,a)];return[[n[0],i[0]],[n[1],i[1]]]}function n3(r){return Xa(r.group)}function zd(r,e){var t={w:"left",e:"right",n:"top",s:"bottom"},a={left:"w",right:"e",top:"n",bottom:"s"},n=Mh(t[e],n3(r));return a[n]}function i3(r,e){var t=[zd(r,e[0]),zd(r,e[1])];return(t[0]==="e"||t[0]==="w")&&t.reverse(),t.join("")}function jS(r,e,t,a,n,i){var o=t.__brushOption,s=r.toRectRange(o.range),l=BM(e,n,i);C(a,function(u){var f=t3[u];s[f[0]][f[1]]+=l[f[0]]}),o.range=r.fromRectRange(NM(s[0][0],s[1][0],s[0][1],s[1][1])),Wy(e,t),fi(e,{isEnd:!1})}function o3(r,e,t,a){var n=e.__brushOption.range,i=BM(r,t,a);C(n,function(o){o[0]+=i[0],o[1]+=i[1]}),Wy(r,e),fi(r,{isEnd:!1})}function BM(r,e,t){var a=r.group,n=a.transformCoordToLocal(e,t),i=a.transformCoordToLocal(0,0);return[n[0]-i[0],n[1]-i[1]]}function $y(r,e,t){var a=RM(r,e);return a&&a!==ui?a.clipPath(t,r._transform):et(t)}function s3(r){var e=pl(r[0][0],r[1][0]),t=pl(r[0][1],r[1][1]),a=vo(r[0][0],r[1][0]),n=vo(r[0][1],r[1][1]);return{x:e,y:t,width:a-e,height:n-t}}function l3(r,e,t){if(!(!r._brushType||f3(r,e.offsetX,e.offsetY))){var a=r._zr,n=r._covers,i=Yy(r,e,t);if(!r._dragging)for(var o=0;oa.getWidth()||t<0||t>a.getHeight()}var Zh={lineX:tx(0),lineY:tx(1),rect:{createCover:function(r,e){function t(a){return a}return kM({toRectRange:t,fromRectRange:t},r,e,[["w"],["e"],["n"],["s"],["s","e"],["s","w"],["n","e"],["n","w"]])},getCreatingRange:function(r){var e=EM(r);return NM(e[1][0],e[1][1],e[0][0],e[0][1])},updateCoverShape:function(r,e,t,a){OM(r,e,t,a)},updateCommon:Vd,contain:Fd},polygon:{createCover:function(r,e){var t=new at;return t.add(new be({name:"main",style:Xy(e),silent:!0})),t},getCreatingRange:function(r){return r},endCreating:function(r,e){e.remove(e.childAt(0)),e.add(new Se({name:"main",draggable:!0,drift:it(o3,r,e),ondragend:it(fi,r,{isEnd:!0})}))},updateCoverShape:function(r,e,t,a){e.childAt(0).setShape({points:$y(r,e,t)})},updateCommon:Vd,contain:Fd}};function tx(r){return{createCover:function(e,t){return kM({toRectRange:function(a){var n=[a,[0,100]];return r&&n.reverse(),n},fromRectRange:function(a){return a[r]}},e,t,[[["w"],["e"]],[["n"],["s"]]][r])},getCreatingRange:function(e){var t=EM(e),a=pl(t[0][r],t[1][r]),n=vo(t[0][r],t[1][r]);return[a,n]},updateCoverShape:function(e,t,a,n){var i,o=RM(e,t);if(o!==ui&&o.getLinearBrushOtherExtent)i=o.getLinearBrushOtherExtent(r);else{var s=e._zr;i=[0,[s.getWidth(),s.getHeight()][1-r]]}var l=[a,i];r&&l.reverse(),OM(e,t,l,n)},updateCommon:Vd,contain:Fd}}function zM(r){return r=Zy(r),function(e){return Vg(e,r)}}function GM(r,e){return r=Zy(r),function(t){var a=e??t,n=a?r.width:r.height,i=a?r.x:r.y;return[i,i+(n||0)]}}function FM(r,e,t){var a=Zy(r);return function(n,i){return a.contain(i[0],i[1])&&!Uh(n,e,t)}}function Zy(r){return ht.create(r)}var h3=["axisLine","axisTickLabel","axisName"],v3=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t,a){r.prototype.init.apply(this,arguments),(this._brushController=new Hy(a.getZr())).on("brush",X(this._onBrush,this))},e.prototype.render=function(t,a,n,i){if(!c3(t,a,i)){this.axisModel=t,this.api=n,this.group.removeAll();var o=this._axisGroup;if(this._axisGroup=new at,this.group.add(this._axisGroup),!!t.get("show")){var s=d3(t,a),l=s.coordinateSystem,u=t.getAreaSelectStyle(),f=u.width,h=t.axis.dim,v=l.getAxisLayout(h),c=V({strokeContainThreshold:f},v),p=new Pe(t,c);C(h3,p.add,p),this._axisGroup.add(p.getGroup()),this._refreshBrushController(c,u,t,s,f,n),Ml(o,this._axisGroup,t)}}},e.prototype._refreshBrushController=function(t,a,n,i,o,s){var l=n.axis.getExtent(),u=l[1]-l[0],f=Math.min(30,Math.abs(u)*.1),h=ht.create({x:l[0],y:-o/2,width:u,height:o});h.x-=f,h.width+=2*f,this._brushController.mount({enableGlobalPan:!0,rotation:t.rotation,x:t.position[0],y:t.position[1]}).setPanels([{panelId:"pl",clipPath:zM(h),isTargetByCursor:FM(h,s,i),getLinearBrushOtherExtent:GM(h,0)}]).enableBrush({brushType:"lineX",brushStyle:a,removeOnClick:!0}).updateCovers(p3(n))},e.prototype._onBrush=function(t){var a=t.areas,n=this.axisModel,i=n.axis,o=G(a,function(s){return[i.coordToData(s.range[0],!0),i.coordToData(s.range[1],!0)]});(!n.option.realtime===t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"axisAreaSelect",parallelAxisId:n.id,intervals:o})},e.prototype.dispose=function(){this._brushController.dispose()},e.type="parallelAxis",e}(zt);function c3(r,e,t){return t&&t.type==="axisAreaSelect"&&e.findComponents({mainType:"parallelAxis",query:t})[0]===r}function p3(r){var e=r.axis;return G(r.activeIntervals,function(t){return{brushType:"lineX",panelId:"pl",range:[e.dataToCoord(t[0],!0),e.dataToCoord(t[1],!0)]}})}function d3(r,e){return e.getComponent("parallel",r.get("parallelIndex"))}var g3={type:"axisAreaSelect",event:"axisAreaSelected"};function y3(r){r.registerAction(g3,function(e,t){t.eachComponent({mainType:"parallelAxis",query:e},function(a){a.axis.model.setActiveIntervals(e.intervals)})}),r.registerAction("parallelAxisExpand",function(e,t){t.eachComponent({mainType:"parallel",query:e},function(a){a.setAxisExpand(e)})})}var m3={type:"value",areaSelectStyle:{width:20,borderWidth:1,borderColor:"rgba(160,197,232)",color:"rgba(160,197,232)",opacity:.3},realtime:!0,z:10};function HM(r){r.registerComponentView(zF),r.registerComponentModel(FF),r.registerCoordinateSystem("parallel",qF),r.registerPreprocessor(OF),r.registerComponentModel(Nd),r.registerComponentView(v3),ho(r,"parallel",Nd,m3),y3(r)}function _3(r){gt(HM),r.registerChartView(DF),r.registerSeriesModel(LF),r.registerVisual(r.PRIORITY.VISUAL.BRUSH,kF)}var S3=function(){function r(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.cpx1=0,this.cpy1=0,this.cpx2=0,this.cpy2=0,this.extent=0}return r}(),x3=function(r){k(e,r);function e(t){return r.call(this,t)||this}return e.prototype.getDefaultShape=function(){return new S3},e.prototype.buildPath=function(t,a){var n=a.extent;t.moveTo(a.x1,a.y1),t.bezierCurveTo(a.cpx1,a.cpy1,a.cpx2,a.cpy2,a.x2,a.y2),a.orient==="vertical"?(t.lineTo(a.x2+n,a.y2),t.bezierCurveTo(a.cpx2+n,a.cpy2,a.cpx1+n,a.cpy1,a.x1+n,a.y1)):(t.lineTo(a.x2,a.y2+n),t.bezierCurveTo(a.cpx2,a.cpy2+n,a.cpx1,a.cpy1+n,a.x1,a.y1+n)),t.closePath()},e.prototype.highlight=function(){da(this)},e.prototype.downplay=function(){ga(this)},e}(yt),b3=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t._focusAdjacencyDisabled=!1,t}return e.prototype.render=function(t,a,n){var i=this,o=t.getGraph(),s=this.group,l=t.layoutInfo,u=l.width,f=l.height,h=t.getData(),v=t.getData("edge"),c=t.get("orient");this._model=t,s.removeAll(),s.x=l.x,s.y=l.y,o.eachEdge(function(p){var d=new x3,g=nt(d);g.dataIndex=p.dataIndex,g.seriesIndex=t.seriesIndex,g.dataType="edge";var y=p.getModel(),m=y.getModel("lineStyle"),_=m.get("curveness"),S=p.node1.getLayout(),b=p.node1.getModel(),x=b.get("localX"),w=b.get("localY"),T=p.node2.getLayout(),A=p.node2.getModel(),D=A.get("localX"),M=A.get("localY"),I=p.getLayout(),L,P,R,E,N,O,B,F;d.shape.extent=Math.max(1,I.dy),d.shape.orient=c,c==="vertical"?(L=(x!=null?x*u:S.x)+I.sy,P=(w!=null?w*f:S.y)+S.dy,R=(D!=null?D*u:T.x)+I.ty,E=M!=null?M*f:T.y,N=L,O=P*(1-_)+E*_,B=R,F=P*_+E*(1-_)):(L=(x!=null?x*u:S.x)+S.dx,P=(w!=null?w*f:S.y)+I.sy,R=D!=null?D*u:T.x,E=(M!=null?M*f:T.y)+I.ty,N=L*(1-_)+R*_,O=P,B=L*_+R*(1-_),F=E),d.setShape({x1:L,y1:P,x2:R,y2:E,cpx1:N,cpy1:O,cpx2:B,cpy2:F}),d.useStyle(m.getItemStyle()),ex(d.style,c,p);var H=""+y.get("value"),Y=ne(y,"edgeLabel");ve(d,Y,{labelFetcher:{getFormattedLabel:function(ct,xt,pt,rt,dt,lt){return t.getFormattedLabel(ct,xt,"edge",rt,br(dt,Y.normal&&Y.normal.get("formatter"),H),lt)}},labelDataIndex:p.dataIndex,defaultText:H}),d.setTextConfig({position:"inside"});var j=y.getModel("emphasis");he(d,y,"lineStyle",function(ct){var xt=ct.getItemStyle();return ex(xt,c,p),xt}),s.add(d),v.setItemGraphicEl(p.dataIndex,d);var J=j.get("focus");Wt(d,J==="adjacency"?p.getAdjacentDataIndices():J==="trajectory"?p.getTrajectoryDataIndices():J,j.get("blurScope"),j.get("disabled"))}),o.eachNode(function(p){var d=p.getLayout(),g=p.getModel(),y=g.get("localX"),m=g.get("localY"),_=g.getModel("emphasis"),S=g.get(["itemStyle","borderRadius"])||0,b=new wt({shape:{x:y!=null?y*u:d.x,y:m!=null?m*f:d.y,width:d.dx,height:d.dy,r:S},style:g.getModel("itemStyle").getItemStyle(),z2:10});ve(b,ne(g),{labelFetcher:{getFormattedLabel:function(w,T){return t.getFormattedLabel(w,T,"node")}},labelDataIndex:p.dataIndex,defaultText:p.id}),b.disableLabelAnimation=!0,b.setStyle("fill",p.getVisual("color")),b.setStyle("decal",p.getVisual("style").decal),he(b,g),s.add(b),h.setItemGraphicEl(p.dataIndex,b),nt(b).dataType="node";var x=_.get("focus");Wt(b,x==="adjacency"?p.getAdjacentDataIndices():x==="trajectory"?p.getTrajectoryDataIndices():x,_.get("blurScope"),_.get("disabled"))}),h.eachItemGraphicEl(function(p,d){var g=h.getItemModel(d);g.get("draggable")&&(p.drift=function(y,m){i._focusAdjacencyDisabled=!0,this.shape.x+=y,this.shape.y+=m,this.dirty(),n.dispatchAction({type:"dragNode",seriesId:t.id,dataIndex:h.getRawIndex(d),localX:this.shape.x/u,localY:this.shape.y/f})},p.ondragend=function(){i._focusAdjacencyDisabled=!1},p.draggable=!0,p.cursor="move")}),!this._data&&t.isAnimationEnabled()&&s.setClipPath(w3(s.getBoundingRect(),t,function(){s.removeClipPath()})),this._data=t.getData()},e.prototype.dispose=function(){},e.type="sankey",e}(Rt);function ex(r,e,t){switch(r.fill){case"source":r.fill=t.node1.getVisual("color"),r.decal=t.node1.getVisual("style").decal;break;case"target":r.fill=t.node2.getVisual("color"),r.decal=t.node2.getVisual("style").decal;break;case"gradient":var a=t.node1.getVisual("color"),n=t.node2.getVisual("color");U(a)&&U(n)&&(r.fill=new xo(0,0,+(e==="horizontal"),+(e==="vertical"),[{color:a,offset:0},{color:n,offset:1}]))}}function w3(r,e,t){var a=new wt({shape:{x:r.x-10,y:r.y-10,width:0,height:r.height+20}});return Vt(a,{shape:{width:r.width+20}},e,t),a}var T3=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.getInitialData=function(t,a){var n=t.edges||t.links,i=t.data||t.nodes,o=t.levels;this.levelModels=[];for(var s=this.levelModels,l=0;l=0&&(s[o[l].depth]=new Mt(o[l],this,a));if(i&&n){var u=TM(i,n,this,!0,f);return u.data}function f(h,v){h.wrapMethod("getItemModel",function(c,p){var d=c.parentModel,g=d.getData().getItemLayout(p);if(g){var y=g.depth,m=d.levelModels[y];m&&(c.parentModel=m)}return c}),v.wrapMethod("getItemModel",function(c,p){var d=c.parentModel,g=d.getGraph().getEdgeByIndex(p),y=g.node1.getLayout();if(y){var m=y.depth,_=d.levelModels[m];_&&(c.parentModel=_)}return c})}},e.prototype.setNodePosition=function(t,a){var n=this.option.data||this.option.nodes,i=n[t];i.localX=a[0],i.localY=a[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,a,n){function i(c){return isNaN(c)||c==null}if(n==="edge"){var o=this.getDataParams(t,n),s=o.data,l=o.value,u=s.source+" -- "+s.target;return ie("nameValue",{name:u,value:l,noValue:i(l)})}else{var f=this.getGraph().getNodeByIndex(t),h=f.getLayout().value,v=this.getDataParams(t,n).data.name;return ie("nameValue",{name:v!=null?v+"":null,value:h,noValue:i(h)})}},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(t,a){var n=r.prototype.getDataParams.call(this,t,a);if(n.value==null&&a==="node"){var i=this.getGraph().getNodeByIndex(t),o=i.getLayout().value;n.value=o}return n},e.type="series.sankey",e.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3},e}(kt);function A3(r,e){r.eachSeriesByType("sankey",function(t){var a=t.get("nodeWidth"),n=t.get("nodeGap"),i=C3(t,e);t.layoutInfo=i;var o=i.width,s=i.height,l=t.getGraph(),u=l.nodes,f=l.edges;M3(u);var h=Pt(u,function(d){return d.getLayout().value===0}),v=h.length!==0?0:t.get("layoutIterations"),c=t.get("orient"),p=t.get("nodeAlign");D3(u,f,a,n,o,s,v,c,p)})}function C3(r,e){return jt(r.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function D3(r,e,t,a,n,i,o,s,l){I3(r,e,t,n,i,s,l),E3(r,e,i,n,a,o,s),H3(r,s)}function M3(r){C(r,function(e){var t=qa(e.outEdges,Kf),a=qa(e.inEdges,Kf),n=e.getValue()||0,i=Math.max(t,a,n);e.setLayout({value:i},!0)})}function I3(r,e,t,a,n,i,o){for(var s=[],l=[],u=[],f=[],h=0,v=0;v=0;y&&g.depth>c&&(c=g.depth),d.setLayout({depth:y?g.depth:h},!0),i==="vertical"?d.setLayout({dy:t},!0):d.setLayout({dx:t},!0);for(var m=0;mh-1?c:h-1;o&&o!=="left"&&L3(r,o,i,w);var T=i==="vertical"?(n-t)/w:(a-t)/w;R3(r,T,i)}function WM(r){var e=r.hostGraph.data.getRawDataItem(r.dataIndex);return e.depth!=null&&e.depth>=0}function L3(r,e,t,a){if(e==="right"){for(var n=[],i=r,o=0;i.length;){for(var s=0;s0;i--)l*=.99,N3(s,l,o),Hc(s,n,t,a,o),F3(s,l,o),Hc(s,n,t,a,o)}function k3(r,e){var t=[],a=e==="vertical"?"y":"x",n=Hp(r,function(i){return i.getLayout()[a]});return n.keys.sort(function(i,o){return i-o}),C(n.keys,function(i){t.push(n.buckets.get(i))}),t}function O3(r,e,t,a,n,i){var o=1/0;C(r,function(s){var l=s.length,u=0;C(s,function(h){u+=h.getLayout().value});var f=i==="vertical"?(a-(l-1)*n)/u:(t-(l-1)*n)/u;f0&&(s=l.getLayout()[i]+u,n==="vertical"?l.setLayout({x:s},!0):l.setLayout({y:s},!0)),f=l.getLayout()[i]+l.getLayout()[v]+e;var p=n==="vertical"?a:t;if(u=f-e-p,u>0){s=l.getLayout()[i]-u,n==="vertical"?l.setLayout({x:s},!0):l.setLayout({y:s},!0),f=s;for(var c=h-2;c>=0;--c)l=o[c],u=l.getLayout()[i]+l.getLayout()[v]+e-f,u>0&&(s=l.getLayout()[i]-u,n==="vertical"?l.setLayout({x:s},!0):l.setLayout({y:s},!0)),f=l.getLayout()[i]}})}function N3(r,e,t){C(r.slice().reverse(),function(a){C(a,function(n){if(n.outEdges.length){var i=qa(n.outEdges,B3,t)/qa(n.outEdges,Kf);if(isNaN(i)){var o=n.outEdges.length;i=o?qa(n.outEdges,V3,t)/o:0}if(t==="vertical"){var s=n.getLayout().x+(i-tn(n,t))*e;n.setLayout({x:s},!0)}else{var l=n.getLayout().y+(i-tn(n,t))*e;n.setLayout({y:l},!0)}}})})}function B3(r,e){return tn(r.node2,e)*r.getValue()}function V3(r,e){return tn(r.node2,e)}function z3(r,e){return tn(r.node1,e)*r.getValue()}function G3(r,e){return tn(r.node1,e)}function tn(r,e){return e==="vertical"?r.getLayout().x+r.getLayout().dx/2:r.getLayout().y+r.getLayout().dy/2}function Kf(r){return r.getValue()}function qa(r,e,t){for(var a=0,n=r.length,i=-1;++io&&(o=l)}),C(a,function(s){var l=new ae({type:"color",mappingMethod:"linear",dataExtent:[i,o],visual:e.get("color")}),u=l.mapValueToVisual(s.getLayout().value),f=s.getModel().get(["itemStyle","color"]);f!=null?(s.setVisual("color",f),s.setVisual("style",{fill:f})):(s.setVisual("color",u),s.setVisual("style",{fill:u}))})}n.length&&C(n,function(s){var l=s.getModel().get("lineStyle");s.setVisual("style",l)})})}function U3(r){r.registerChartView(b3),r.registerSeriesModel(T3),r.registerLayout(A3),r.registerVisual(W3),r.registerAction({type:"dragNode",event:"dragnode",update:"update"},function(e,t){t.eachComponent({mainType:"series",subType:"sankey",query:e},function(a){a.setNodePosition(e.dataIndex,[e.localX,e.localY])})})}var UM=function(){function r(){}return r.prototype.getInitialData=function(e,t){var a,n=t.getComponent("xAxis",this.get("xAxisIndex")),i=t.getComponent("yAxis",this.get("yAxisIndex")),o=n.get("type"),s=i.get("type"),l;o==="category"?(e.layout="horizontal",a=n.getOrdinalMeta(),l=!0):s==="category"?(e.layout="vertical",a=i.getOrdinalMeta(),l=!0):e.layout=e.layout||"horizontal";var u=["x","y"],f=e.layout==="horizontal"?0:1,h=this._baseAxisDim=u[f],v=u[1-f],c=[n,i],p=c[f].get("type"),d=c[1-f].get("type"),g=e.data;if(g&&l){var y=[];C(g,function(S,b){var x;z(S)?(x=S.slice(),S.unshift(b)):z(S.value)?(x=V({},S),x.value=x.value.slice(),S.value.unshift(b)):x=S,y.push(x)}),e.data=y}var m=this.defaultValueDimensions,_=[{name:h,type:zf(p),ordinalMeta:a,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:v,type:zf(d),dimsDef:m.slice()}];return No(this,{coordDimensions:_,dimensionsCount:m.length+1,encodeDefaulter:it(xA,_,this)})},r.prototype.getBaseAxis=function(){var e=this._baseAxisDim;return this.ecModel.getComponent(e+"Axis",this.get(e+"AxisIndex")).axis},r}(),YM=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],t.visualDrawType="stroke",t}return e.type="series.boxplot",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},e}(kt);Xt(YM,UM,!0);var Y3=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=t.getData(),o=this.group,s=this._data;this._data||o.removeAll();var l=t.get("layout")==="horizontal"?1:0;i.diff(s).add(function(u){if(i.hasValue(u)){var f=i.getItemLayout(u),h=rx(f,i,u,l,!0);i.setItemGraphicEl(u,h),o.add(h)}}).update(function(u,f){var h=s.getItemGraphicEl(f);if(!i.hasValue(u)){o.remove(h);return}var v=i.getItemLayout(u);h?(Ir(h),XM(v,h,i,u)):h=rx(v,i,u,l),o.add(h),i.setItemGraphicEl(u,h)}).remove(function(u){var f=s.getItemGraphicEl(u);f&&o.remove(f)}).execute(),this._data=i},e.prototype.remove=function(t){var a=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl(function(i){i&&a.remove(i)})},e.type="boxplot",e}(Rt),X3=function(){function r(){}return r}(),$3=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="boxplotBoxPath",a}return e.prototype.getDefaultShape=function(){return new X3},e.prototype.buildPath=function(t,a){var n=a.points,i=0;for(t.moveTo(n[i][0],n[i][1]),i++;i<4;i++)t.lineTo(n[i][0],n[i][1]);for(t.closePath();id){var S=[y,_];a.push(S)}}}return{boxData:t,outliers:a}}var tH={type:"echarts:boxplot",transform:function(e){var t=e.upstream;if(t.sourceFormat!==xe){var a="";It(a)}var n=J3(t.getRawData(),e.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:n.boxData},{data:n.outliers}]}};function eH(r){r.registerSeriesModel(YM),r.registerChartView(Y3),r.registerLayout(q3),r.registerTransform(tH)}var rH=["color","borderColor"],aH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,a,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,a,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,a):this._incrementalRenderNormal(t,a)},e.prototype.eachRendered=function(t){nn(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var a=t.pipelineContext.large;(this._isLargeDraw==null||a!==this._isLargeDraw)&&(this._isLargeDraw=a,this._clear())},e.prototype._renderNormal=function(t){var a=t.getData(),n=this._data,i=this.group,o=a.getLayout("isSimpleBox"),s=t.get("clip",!0),l=t.coordinateSystem,u=l.getArea&&l.getArea();this._data||i.removeAll(),a.diff(n).add(function(f){if(a.hasValue(f)){var h=a.getItemLayout(f);if(s&&ax(u,h))return;var v=Wc(h,f,!0);Vt(v,{shape:{points:h.ends}},t,f),Uc(v,a,f,o),i.add(v),a.setItemGraphicEl(f,v)}}).update(function(f,h){var v=n.getItemGraphicEl(h);if(!a.hasValue(f)){i.remove(v);return}var c=a.getItemLayout(f);if(s&&ax(u,c)){i.remove(v);return}v?(Dt(v,{shape:{points:c.ends}},t,f),Ir(v)):v=Wc(c),Uc(v,a,f,o),i.add(v),a.setItemGraphicEl(f,v)}).remove(function(f){var h=n.getItemGraphicEl(f);h&&i.remove(h)}).execute(),this._data=a},e.prototype._renderLarge=function(t){this._clear(),nx(t,this.group);var a=t.get("clip",!0)?kl(t.coordinateSystem,!1,t):null;a?this.group.setClipPath(a):this.group.removeClipPath()},e.prototype._incrementalRenderNormal=function(t,a){for(var n=a.getData(),i=n.getLayout("isSimpleBox"),o;(o=t.next())!=null;){var s=n.getItemLayout(o),l=Wc(s);Uc(l,n,o,i),l.incremental=!0,this.group.add(l),this._progressiveEls.push(l)}},e.prototype._incrementalRenderLarge=function(t,a){nx(a,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),this._data=null},e.type="candlestick",e}(Rt),nH=function(){function r(){}return r}(),iH=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a.type="normalCandlestickBox",a}return e.prototype.getDefaultShape=function(){return new nH},e.prototype.buildPath=function(t,a){var n=a.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(yt);function Wc(r,e,t){var a=r.ends;return new iH({shape:{points:t?oH(a,r):a},z2:100})}function ax(r,e){for(var t=!0,a=0;a0?"borderColor":"borderColor0"])||t.get(["itemStyle",r>0?"color":"color0"]);r===0&&(n=t.get(["itemStyle","borderColorDoji"]));var i=t.getModel("itemStyle").getItemStyle(rH);e.useStyle(i),e.style.fill=null,e.style.stroke=n}var $M=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],t}return e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,a,n){var i=a.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series.candlestick",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(kt);Xt($M,UM,!0);function lH(r){!r||!z(r.series)||C(r.series,function(e){tt(e)&&e.type==="k"&&(e.type="candlestick")})}var uH=["itemStyle","borderColor"],fH=["itemStyle","borderColor0"],hH=["itemStyle","borderColorDoji"],vH=["itemStyle","color"],cH=["itemStyle","color0"],pH={seriesType:"candlestick",plan:Lo(),performRawSeries:!0,reset:function(r,e){function t(i,o){return o.get(i>0?vH:cH)}function a(i,o){return o.get(i===0?hH:i>0?uH:fH)}if(!e.isSeriesFiltered(r)){var n=r.pipelineContext.large;return!n&&{progress:function(i,o){for(var s;(s=i.next())!=null;){var l=o.getItemModel(s),u=o.getItemLayout(s).sign,f=l.getItemStyle();f.fill=t(u,l),f.stroke=a(u,l)||f.fill;var h=o.ensureUniqueItemVisual(s,"style");V(h,f)}}}}}},dH={seriesType:"candlestick",plan:Lo(),reset:function(r){var e=r.coordinateSystem,t=r.getData(),a=gH(r,t),n=0,i=1,o=["x","y"],s=t.getDimensionIndex(t.mapDimension(o[n])),l=G(t.mapDimensionsAll(o[i]),t.getDimensionIndex,t),u=l[0],f=l[1],h=l[2],v=l[3];if(t.setLayout({candleWidth:a,isSimpleBox:a<=1.3}),s<0||l.length<4)return;return{progress:r.pipelineContext.large?p:c};function c(d,g){for(var y,m=g.getStore();(y=d.next())!=null;){var _=m.get(s,y),S=m.get(u,y),b=m.get(f,y),x=m.get(h,y),w=m.get(v,y),T=Math.min(S,b),A=Math.max(S,b),D=N(T,_),M=N(A,_),I=N(x,_),L=N(w,_),P=[];O(P,M,0),O(P,D,1),P.push(F(L),F(M),F(I),F(D));var R=g.getItemModel(y),E=!!R.get(["itemStyle","borderColorDoji"]);g.setItemLayout(y,{sign:ix(m,y,S,b,f,E),initBaseline:S>b?M[i]:D[i],ends:P,brushRect:B(x,w,_)})}function N(H,Y){var j=[];return j[n]=Y,j[i]=H,isNaN(Y)||isNaN(H)?[NaN,NaN]:e.dataToPoint(j)}function O(H,Y,j){var J=Y.slice(),ct=Y.slice();J[n]=Ju(J[n]+a/2,1,!1),ct[n]=Ju(ct[n]-a/2,1,!0),j?H.push(J,ct):H.push(ct,J)}function B(H,Y,j){var J=N(H,j),ct=N(Y,j);return J[n]-=a/2,ct[n]-=a/2,{x:J[0],y:J[1],width:a,height:ct[1]-J[1]}}function F(H){return H[n]=Ju(H[n],1),H}}function p(d,g){for(var y=Hr(d.count*4),m=0,_,S=[],b=[],x,w=g.getStore(),T=!!r.get(["itemStyle","borderColorDoji"]);(x=d.next())!=null;){var A=w.get(s,x),D=w.get(u,x),M=w.get(f,x),I=w.get(h,x),L=w.get(v,x);if(isNaN(A)||isNaN(I)||isNaN(L)){y[m++]=NaN,m+=3;continue}y[m++]=ix(w,x,D,M,f,T),S[n]=A,S[i]=I,_=e.dataToPoint(S,null,b),y[m++]=_?_[0]:NaN,y[m++]=_?_[1]:NaN,S[i]=L,_=e.dataToPoint(S,null,b),y[m++]=_?_[1]:NaN}g.setLayout("largePoints",y)}}};function ix(r,e,t,a,n,i){var o;return t>a?o=-1:t0?r.get(n,e-1)<=a?1:-1:1,o}function gH(r,e){var t=r.getBaseAxis(),a,n=t.type==="category"?t.getBandWidth():(a=t.getExtent(),Math.abs(a[1]-a[0])/e.count()),i=W(ot(r.get("barMaxWidth"),n),n),o=W(ot(r.get("barMinWidth"),1),n),s=r.get("barWidth");return s!=null?W(s,n):Math.max(Math.min(n/2,i),o)}function yH(r){r.registerChartView(aH),r.registerSeriesModel($M),r.registerPreprocessor(lH),r.registerVisual(pH),r.registerLayout(dH)}function ox(r,e){var t=e.rippleEffectColor||e.color;r.eachChild(function(a){a.attr({z:e.z,zlevel:e.zlevel,style:{stroke:e.brushType==="stroke"?t:null,fill:e.brushType==="fill"?t:null}})})}var mH=function(r){k(e,r);function e(t,a){var n=r.call(this)||this,i=new Rl(t,a),o=new at;return n.add(i),n.add(o),n.updateData(t,a),n}return e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var a=t.symbolType,n=t.color,i=t.rippleNumber,o=this.childAt(1),s=0;s0&&(s=this._getLineLength(i)/f*1e3),s!==this._period||l!==this._loop||u!==this._roundTrip){i.stopAnimation();var v=void 0;K(h)?v=h(n):v=h,i.__t>0&&(v=-s*i.__t),this._animateSymbol(i,s,v,l,u)}this._period=s,this._loop=l,this._roundTrip=u}},e.prototype._animateSymbol=function(t,a,n,i,o){if(a>0){t.__t=0;var s=this,l=t.animate("",i).when(o?a*2:a,{__t:o?2:1}).delay(n).during(function(){s._updateSymbolPosition(t)});i||l.done(function(){s.remove(t)}),l.start()}},e.prototype._getLineLength=function(t){return sa(t.__p1,t.__cp1)+sa(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,a){t.__p1=a[0],t.__p2=a[1],t.__cp1=a[2]||[(a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2]},e.prototype.updateData=function(t,a,n){this.childAt(0).updateData(t,a,n),this._updateEffectSymbol(t,a)},e.prototype._updateSymbolPosition=function(t){var a=t.__p1,n=t.__p2,i=t.__cp1,o=t.__t<1?t.__t:2-t.__t,s=[t.x,t.y],l=s.slice(),u=se,f=Cp;s[0]=u(a[0],i[0],n[0],o),s[1]=u(a[1],i[1],n[1],o);var h=t.__t<1?f(a[0],i[0],n[0],o):f(n[0],i[0],a[0],1-o),v=t.__t<1?f(a[1],i[1],n[1],o):f(n[1],i[1],a[1],1-o);t.rotation=-Math.atan2(v,h)-Math.PI/2,(this._symbolType==="line"||this._symbolType==="rect"||this._symbolType==="roundRect")&&(t.__lastT!==void 0&&t.__lastT=0&&!(i[l]<=a);l--);l=Math.min(l,o-2)}else{for(l=s;la);l++);l=Math.min(l-1,o-2)}var f=(a-i[l])/(i[l+1]-i[l]),h=n[l],v=n[l+1];t.x=h[0]*(1-f)+f*v[0],t.y=h[1]*(1-f)+f*v[1];var c=t.__t<1?v[0]-h[0]:h[0]-v[0],p=t.__t<1?v[1]-h[1]:h[1]-v[1];t.rotation=-Math.atan2(p,c)-Math.PI/2,this._lastFrame=l,this._lastFramePercent=a,t.ignore=!1}},e}(ZM),wH=function(){function r(){this.polyline=!1,this.curveness=0,this.segs=[]}return r}(),TH=function(r){k(e,r);function e(t){var a=r.call(this,t)||this;return a._off=0,a.hoverDataIdx=-1,a}return e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new wH},e.prototype.buildPath=function(t,a){var n=a.segs,i=a.curveness,o;if(a.polyline)for(o=this._off;o0){t.moveTo(n[o++],n[o++]);for(var l=1;l0){var c=(u+h)/2-(f-v)*i,p=(f+v)/2-(h-u)*i;t.quadraticCurveTo(c,p,h,v)}else t.lineTo(h,v)}this.incremental&&(this._off=o,this.notClear=!0)},e.prototype.findDataIndex=function(t,a){var n=this.shape,i=n.segs,o=n.curveness,s=this.style.lineWidth;if(n.polyline)for(var l=0,u=0;u0)for(var h=i[u++],v=i[u++],c=1;c0){var g=(h+p)/2-(v-d)*o,y=(v+d)/2-(p-h)*o;if(wT(h,v,g,y,p,d,s,t,a))return l}else if(ka(h,v,p,d,s,t,a))return l;l++}return-1},e.prototype.contain=function(t,a){var n=this.transformCoordToLocal(t,a),i=this.getBoundingRect();if(t=n[0],a=n[1],i.contain(t,a)){var o=this.hoverDataIdx=this.findDataIndex(t,a);return o>=0}return this.hoverDataIdx=-1,!1},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var a=this.shape,n=a.segs,i=1/0,o=1/0,s=-1/0,l=-1/0,u=0;u0&&(o.dataIndex=l+e.__startIndex)})},r.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},r}(),KM={seriesType:"lines",plan:Lo(),reset:function(r){var e=r.coordinateSystem;if(e){var t=r.get("polyline"),a=r.pipelineContext.large;return{progress:function(n,i){var o=[];if(a){var s=void 0,l=n.end-n.start;if(t){for(var u=0,f=n.start;f0&&(f||u.configLayer(s,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(l/10+.9,1),0)})),o.updateData(i);var h=t.get("clip",!0)&&kl(t.coordinateSystem,!1,t);h?this.group.setClipPath(h):this.group.removeClipPath(),this._lastZlevel=s,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,a,n){var i=t.getData(),o=this._updateLineDraw(i,t);o.incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,a,n){this._lineDraw.incrementalUpdate(t,a.getData()),this._finished=t.end===a.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,a,n){var i=t.getData(),o=t.pipelineContext;if(!this._finished||o.large||o.progressiveRender)return{update:!0};var s=KM.reset(t,a,n);s.progress&&s.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,a){var n=this._lineDraw,i=this._showEffect(a),o=!!a.get("polyline"),s=a.pipelineContext,l=s.large;return(!n||i!==this._hasEffet||o!==this._isPolyline||l!==this._isLargeDraw)&&(n&&n.remove(),n=this._lineDraw=l?new AH:new Fy(o?i?bH:qM:i?ZM:Gy),this._hasEffet=i,this._isPolyline=o,this._isLargeDraw=l),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var a=t.getZr(),n=a.painter.getType()==="svg";!n&&this._lastZlevel!=null&&a.painter.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,a){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(a)},e.prototype.dispose=function(t,a){this.remove(t,a)},e.type="lines",e}(Rt),DH=typeof Uint32Array>"u"?Array:Uint32Array,MH=typeof Float64Array>"u"?Array:Float64Array;function sx(r){var e=r.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(r.data=G(e,function(t){var a=[t[0].coord,t[1].coord],n={coords:a};return t[0].name&&(n.fromName=t[0].name),t[1].name&&(n.toName=t[1].name),hh([n,t[0],t[1]])}))}var IH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.visualStyleAccessPath="lineStyle",t.visualDrawType="stroke",t}return e.prototype.init=function(t){t.data=t.data||[],sx(t);var a=this._processFlatCoordsArray(t.data);this._flatCoords=a.flatCoords,this._flatCoordsOffset=a.flatCoordsOffset,a.flatCoords&&(t.data=new Float32Array(a.count)),r.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(t){if(sx(t),t.data){var a=this._processFlatCoordsArray(t.data);this._flatCoords=a.flatCoords,this._flatCoordsOffset=a.flatCoordsOffset,a.flatCoords&&(t.data=new Float32Array(a.count))}r.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var a=this._processFlatCoordsArray(t.data);a.flatCoords&&(this._flatCoords?(this._flatCoords=Hs(this._flatCoords,a.flatCoords),this._flatCoordsOffset=Hs(this._flatCoordsOffset,a.flatCoordsOffset)):(this._flatCoords=a.flatCoords,this._flatCoordsOffset=a.flatCoordsOffset),t.data=new Float32Array(a.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var a=this.getData().getItemModel(t),n=a.option instanceof Array?a.option:a.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[t*2+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,a){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[t*2],i=this._flatCoordsOffset[t*2+1],o=0;o ")})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return t??(this.option.large?1e4:this.get("progressive"))},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return t??(this.option.large?2e4:this.get("progressiveThreshold"))},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),a=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&a>0?a+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(kt);function Ou(r){return r instanceof Array||(r=[r,r]),r}var LH={seriesType:"lines",reset:function(r){var e=Ou(r.get("symbol")),t=Ou(r.get("symbolSize")),a=r.getData();a.setVisual("fromSymbol",e&&e[0]),a.setVisual("toSymbol",e&&e[1]),a.setVisual("fromSymbolSize",t&&t[0]),a.setVisual("toSymbolSize",t&&t[1]);function n(i,o){var s=i.getItemModel(o),l=Ou(s.getShallow("symbol",!0)),u=Ou(s.getShallow("symbolSize",!0));l[0]&&i.setItemVisual(o,"fromSymbol",l[0]),l[1]&&i.setItemVisual(o,"toSymbol",l[1]),u[0]&&i.setItemVisual(o,"fromSymbolSize",u[0]),u[1]&&i.setItemVisual(o,"toSymbolSize",u[1])}return{dataEach:a.hasItemOption?n:null}}};function PH(r){r.registerChartView(CH),r.registerSeriesModel(IH),r.registerLayout(KM),r.registerVisual(LH)}var RH=256,EH=function(){function r(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var e=Cr.createCanvas();this.canvas=e}return r.prototype.update=function(e,t,a,n,i,o){var s=this._getBrush(),l=this._getGradient(i,"inRange"),u=this._getGradient(i,"outOfRange"),f=this.pointSize+this.blurSize,h=this.canvas,v=h.getContext("2d"),c=e.length;h.width=t,h.height=a;for(var p=0;p0){var I=o(_)?l:u;_>0&&(_=_*D+T),b[x++]=I[M],b[x++]=I[M+1],b[x++]=I[M+2],b[x++]=I[M+3]*_*256}else x+=4}return v.putImageData(S,0,0),h},r.prototype._getBrush=function(){var e=this._brushCanvas||(this._brushCanvas=Cr.createCanvas()),t=this.pointSize+this.blurSize,a=t*2;e.width=a,e.height=a;var n=e.getContext("2d");return n.clearRect(0,0,a,a),n.shadowOffsetX=a,n.shadowBlur=this.blurSize,n.shadowColor="#000",n.beginPath(),n.arc(-t,t,this.pointSize,0,Math.PI*2,!0),n.closePath(),n.fill(),e},r.prototype._getGradient=function(e,t){for(var a=this._gradientPixels,n=a[t]||(a[t]=new Uint8ClampedArray(256*4)),i=[0,0,0,0],o=0,s=0;s<256;s++)e[t](s/255,!0,i),n[o++]=i[0],n[o++]=i[1],n[o++]=i[2],n[o++]=i[3];return n},r}();function kH(r,e,t){var a=r[1]-r[0];e=G(e,function(o){return{interval:[(o.interval[0]-r[0])/a,(o.interval[1]-r[0])/a]}});var n=e.length,i=0;return function(o){var s;for(s=i;s=0;s--){var l=e[s].interval;if(l[0]<=o&&o<=l[1]){i=s;break}}return s>=0&&s=e[0]&&a<=e[1]}}function lx(r){var e=r.dimensions;return e[0]==="lng"&&e[1]==="lat"}var NH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i;a.eachComponent("visualMap",function(s){s.eachTargetSeries(function(l){l===t&&(i=s)})}),this._progressiveEls=null,this.group.removeAll();var o=t.coordinateSystem;o.type==="cartesian2d"||o.type==="calendar"?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):lx(o)&&this._renderOnGeo(o,t,i,n)},e.prototype.incrementalPrepareRender=function(t,a,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,a,n,i){var o=a.coordinateSystem;o&&(lx(o)?this.render(a,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(a,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){nn(this._progressiveEls||this.group,t)},e.prototype._renderOnCartesianAndCalendar=function(t,a,n,i,o){var s=t.coordinateSystem,l=yi(s,"cartesian2d"),u,f,h,v;if(l){var c=s.getAxis("x"),p=s.getAxis("y");u=c.getBandWidth()+.5,f=p.getBandWidth()+.5,h=c.scale.getExtent(),v=p.scale.getExtent()}for(var d=this.group,g=t.getData(),y=t.getModel(["emphasis","itemStyle"]).getItemStyle(),m=t.getModel(["blur","itemStyle"]).getItemStyle(),_=t.getModel(["select","itemStyle"]).getItemStyle(),S=t.get(["itemStyle","borderRadius"]),b=ne(t),x=t.getModel("emphasis"),w=x.get("focus"),T=x.get("blurScope"),A=x.get("disabled"),D=l?[g.mapDimension("x"),g.mapDimension("y"),g.mapDimension("value")]:[g.mapDimension("time"),g.mapDimension("value")],M=n;Mh[1]||Rv[1])continue;var E=s.dataToPoint([P,R]);I=new wt({shape:{x:E[0]-u/2,y:E[1]-f/2,width:u,height:f},style:L})}else{if(isNaN(g.get(D[1],M)))continue;I=new wt({z2:1,shape:s.dataToRect([g.get(D[0],M)]).contentShape,style:L})}if(g.hasItemOption){var N=g.getItemModel(M),O=N.getModel("emphasis");y=O.getModel("itemStyle").getItemStyle(),m=N.getModel(["blur","itemStyle"]).getItemStyle(),_=N.getModel(["select","itemStyle"]).getItemStyle(),S=N.get(["itemStyle","borderRadius"]),w=O.get("focus"),T=O.get("blurScope"),A=O.get("disabled"),b=ne(N)}I.shape.r=S;var B=t.getRawValue(M),F="-";B&&B[2]!=null&&(F=B[2]+""),ve(I,b,{labelFetcher:t,labelDataIndex:M,defaultOpacity:L.opacity,defaultText:F}),I.ensureState("emphasis").style=y,I.ensureState("blur").style=m,I.ensureState("select").style=_,Wt(I,w,T,A),I.incremental=o,o&&(I.states.emphasis.hoverLayer=!0),d.add(I),g.setItemGraphicEl(M,I),this._progressiveEls&&this._progressiveEls.push(I)}},e.prototype._renderOnGeo=function(t,a,n,i){var o=n.targetVisuals.inRange,s=n.targetVisuals.outOfRange,l=a.getData(),u=this._hmLayer||this._hmLayer||new EH;u.blurSize=a.get("blurSize"),u.pointSize=a.get("pointSize"),u.minOpacity=a.get("minOpacity"),u.maxOpacity=a.get("maxOpacity");var f=t.getViewRect().clone(),h=t.getRoamTransform();f.applyTransform(h);var v=Math.max(f.x,0),c=Math.max(f.y,0),p=Math.min(f.width+f.x,i.getWidth()),d=Math.min(f.height+f.y,i.getHeight()),g=p-v,y=d-c,m=[l.mapDimension("lng"),l.mapDimension("lat"),l.mapDimension("value")],_=l.mapArray(m,function(w,T,A){var D=t.dataToPoint([w,T]);return D[0]-=v,D[1]-=c,D.push(A),D}),S=n.getExtent(),b=n.type==="visualMap.continuous"?OH(S,n.option.range):kH(S,n.getPieceList(),n.option.selected);u.update(_,g,y,o.color.getNormalizer(),{inRange:o.color.getColorMapper(),outOfRange:s.color.getColorMapper()},b);var x=new oe({style:{width:g,height:y,x:v,y:c,image:u.canvas},silent:!0});this.group.add(x)},e.type="heatmap",e}(Rt),BH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.getInitialData=function(t,a){return Jr(null,this,{generateCoord:"value"})},e.prototype.preventIncremental=function(){var t=Io.get(this.get("coordinateSystem"));if(t&&t.dimensions)return t.dimensions[0]==="lng"&&t.dimensions[1]==="lat"},e.type="series.heatmap",e.dependencies=["grid","geo","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,geoIndex:0,blurSize:30,pointSize:20,maxOpacity:1,minOpacity:0,select:{itemStyle:{borderColor:"#212121"}}},e}(kt);function VH(r){r.registerChartView(NH),r.registerSeriesModel(BH)}var zH=["itemStyle","borderWidth"],ux=[{xy:"x",wh:"width",index:0,posDesc:["left","right"]},{xy:"y",wh:"height",index:1,posDesc:["top","bottom"]}],$c=new Kr,GH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=this.group,o=t.getData(),s=this._data,l=t.coordinateSystem,u=l.getBaseAxis(),f=u.isHorizontal(),h=l.master.getRect(),v={ecSize:{width:n.getWidth(),height:n.getHeight()},seriesModel:t,coordSys:l,coordSysExtent:[[h.x,h.x+h.width],[h.y,h.y+h.height]],isHorizontal:f,valueDim:ux[+f],categoryDim:ux[1-+f]};o.diff(s).add(function(p){if(o.hasValue(p)){var d=hx(o,p),g=fx(o,p,d,v),y=vx(o,v,g);o.setItemGraphicEl(p,y),i.add(y),px(y,v,g)}}).update(function(p,d){var g=s.getItemGraphicEl(d);if(!o.hasValue(p)){i.remove(g);return}var y=hx(o,p),m=fx(o,p,y,v),_=rI(o,m);g&&_!==g.__pictorialShapeStr&&(i.remove(g),o.setItemGraphicEl(p,null),g=null),g?$H(g,v,m):g=vx(o,v,m,!0),o.setItemGraphicEl(p,g),g.__pictorialSymbolMeta=m,i.add(g),px(g,v,m)}).remove(function(p){var d=s.getItemGraphicEl(p);d&&cx(s,p,d.__pictorialSymbolMeta.animationModel,d)}).execute();var c=t.get("clip",!0)?kl(t.coordinateSystem,!1,t):null;return c?i.setClipPath(c):i.removeClipPath(),this._data=o,this.group},e.prototype.remove=function(t,a){var n=this.group,i=this._data;t.get("animation")?i&&i.eachItemGraphicEl(function(o){cx(i,nt(o).dataIndex,t,o)}):n.removeAll()},e.type="pictorialBar",e}(Rt);function fx(r,e,t,a){var n=r.getItemLayout(e),i=t.get("symbolRepeat"),o=t.get("symbolClip"),s=t.get("symbolPosition")||"start",l=t.get("symbolRotate"),u=(l||0)*Math.PI/180||0,f=t.get("symbolPatternSize")||2,h=t.isAnimationEnabled(),v={dataIndex:e,layout:n,itemModel:t,symbolType:r.getItemVisual(e,"symbol")||"circle",style:r.getItemVisual(e,"style"),symbolClip:o,symbolRepeat:i,symbolRepeatDirection:t.get("symbolRepeatDirection"),symbolPatternSize:f,rotation:u,animationModel:h?t:null,hoverScale:h&&t.get(["emphasis","scale"]),z2:t.getShallow("z",!0)||0};FH(t,i,n,a,v),HH(r,e,n,i,o,v.boundingLength,v.pxSign,f,a,v),WH(t,v.symbolScale,u,a,v);var c=v.symbolSize,p=di(t.get("symbolOffset"),c);return UH(t,c,n,i,o,p,s,v.valueLineWidth,v.boundingLength,v.repeatCutLength,a,v),v}function FH(r,e,t,a,n){var i=a.valueDim,o=r.get("symbolBoundingData"),s=a.coordSys.getOtherAxis(a.coordSys.getBaseAxis()),l=s.toGlobalCoord(s.dataToCoord(0)),u=1-+(t[i.wh]<=0),f;if(z(o)){var h=[Zc(s,o[0])-l,Zc(s,o[1])-l];h[1]0?1:-1}function Zc(r,e){return r.toGlobalCoord(r.dataToCoord(r.scale.parse(e)))}function HH(r,e,t,a,n,i,o,s,l,u){var f=l.valueDim,h=l.categoryDim,v=Math.abs(t[h.wh]),c=r.getItemVisual(e,"symbolSize"),p;z(c)?p=c.slice():c==null?p=["100%","100%"]:p=[c,c],p[h.index]=W(p[h.index],v),p[f.index]=W(p[f.index],a?v:Math.abs(i)),u.symbolSize=p;var d=u.symbolScale=[p[0]/s,p[1]/s];d[f.index]*=(l.isHorizontal?-1:1)*o}function WH(r,e,t,a,n){var i=r.get(zH)||0;i&&($c.attr({scaleX:e[0],scaleY:e[1],rotation:t}),$c.updateTransform(),i/=$c.getLineScale(),i*=e[a.valueDim.index]),n.valueLineWidth=i||0}function UH(r,e,t,a,n,i,o,s,l,u,f,h){var v=f.categoryDim,c=f.valueDim,p=h.pxSign,d=Math.max(e[c.index]+s,0),g=d;if(a){var y=Math.abs(l),m=ee(r.get("symbolMargin"),"15%")+"",_=!1;m.lastIndexOf("!")===m.length-1&&(_=!0,m=m.slice(0,m.length-1));var S=W(m,e[c.index]),b=Math.max(d+S*2,0),x=_?0:S*2,w=bg(a),T=w?a:dx((y+x)/b),A=y-T*d;S=A/2/(_?T:Math.max(T-1,1)),b=d+S*2,x=_?0:S*2,!w&&a!=="fixed"&&(T=u?dx((Math.abs(u)+x)/b):0),g=T*b-x,h.repeatTimes=T,h.symbolMargin=S}var D=p*(g/2),M=h.pathPosition=[];M[v.index]=t[v.wh]/2,M[c.index]=o==="start"?D:o==="end"?l-D:l/2,i&&(M[0]+=i[0],M[1]+=i[1]);var I=h.bundlePosition=[];I[v.index]=t[v.xy],I[c.index]=t[c.xy];var L=h.barRectShape=V({},t);L[c.wh]=p*Math.max(Math.abs(t[c.wh]),Math.abs(M[c.index]+D)),L[v.wh]=t[v.wh];var P=h.clipShape={};P[v.xy]=-t[v.xy],P[v.wh]=f.ecSize[v.wh],P[c.xy]=0,P[c.wh]=t[c.wh]}function jM(r){var e=r.symbolPatternSize,t=Zt(r.symbolType,-e/2,-e/2,e,e);return t.attr({culling:!0}),t.type!=="image"&&t.setStyle({strokeNoScale:!0}),t}function QM(r,e,t,a){var n=r.__pictorialBundle,i=t.symbolSize,o=t.valueLineWidth,s=t.pathPosition,l=e.valueDim,u=t.repeatTimes||0,f=0,h=i[e.valueDim.index]+o+t.symbolMargin*2;for(qy(r,function(d){d.__pictorialAnimationIndex=f,d.__pictorialRepeatTimes=u,f0:y<0)&&(m=u-1-d),g[l.index]=h*(m-u/2+.5)+s[l.index],{x:g[0],y:g[1],scaleX:t.symbolScale[0],scaleY:t.symbolScale[1],rotation:t.rotation}}}function JM(r,e,t,a){var n=r.__pictorialBundle,i=r.__pictorialMainPath;i?ao(i,null,{x:t.pathPosition[0],y:t.pathPosition[1],scaleX:t.symbolScale[0],scaleY:t.symbolScale[1],rotation:t.rotation},t,a):(i=r.__pictorialMainPath=jM(t),n.add(i),ao(i,{x:t.pathPosition[0],y:t.pathPosition[1],scaleX:0,scaleY:0,rotation:t.rotation},{scaleX:t.symbolScale[0],scaleY:t.symbolScale[1]},t,a))}function tI(r,e,t){var a=V({},e.barRectShape),n=r.__pictorialBarRect;n?ao(n,null,{shape:a},e,t):(n=r.__pictorialBarRect=new wt({z2:2,shape:a,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}}),n.disableMorphing=!0,r.add(n))}function eI(r,e,t,a){if(t.symbolClip){var n=r.__pictorialClipPath,i=V({},t.clipShape),o=e.valueDim,s=t.animationModel,l=t.dataIndex;if(n)Dt(n,{shape:i},s,l);else{i[o.wh]=0,n=new wt({shape:i}),r.__pictorialBundle.setClipPath(n),r.__pictorialClipPath=n;var u={};u[o.wh]=t.clipShape[o.wh],ci[a?"updateProps":"initProps"](n,{shape:u},s,l)}}}function hx(r,e){var t=r.getItemModel(e);return t.getAnimationDelayParams=YH,t.isAnimationEnabled=XH,t}function YH(r){return{index:r.__pictorialAnimationIndex,count:r.__pictorialRepeatTimes}}function XH(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function vx(r,e,t,a){var n=new at,i=new at;return n.add(i),n.__pictorialBundle=i,i.x=t.bundlePosition[0],i.y=t.bundlePosition[1],t.symbolRepeat?QM(n,e,t):JM(n,e,t),tI(n,t,a),eI(n,e,t,a),n.__pictorialShapeStr=rI(r,t),n.__pictorialSymbolMeta=t,n}function $H(r,e,t){var a=t.animationModel,n=t.dataIndex,i=r.__pictorialBundle;Dt(i,{x:t.bundlePosition[0],y:t.bundlePosition[1]},a,n),t.symbolRepeat?QM(r,e,t,!0):JM(r,e,t,!0),tI(r,t,!0),eI(r,e,t,!0)}function cx(r,e,t,a){var n=a.__pictorialBarRect;n&&n.removeTextContent();var i=[];qy(a,function(o){i.push(o)}),a.__pictorialMainPath&&i.push(a.__pictorialMainPath),a.__pictorialClipPath&&(t=null),C(i,function(o){Qa(o,{scaleX:0,scaleY:0},t,e,function(){a.parent&&a.parent.remove(a)})}),r.setItemGraphicEl(e,null)}function rI(r,e){return[r.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function qy(r,e,t){C(r.__pictorialBundle.children(),function(a){a!==r.__pictorialBarRect&&e.call(t,a)})}function ao(r,e,t,a,n,i){e&&r.attr(e),a.symbolClip&&!n?t&&r.attr(t):t&&ci[n?"updateProps":"initProps"](r,t,a.animationModel,a.dataIndex,i)}function px(r,e,t){var a=t.dataIndex,n=t.itemModel,i=n.getModel("emphasis"),o=i.getModel("itemStyle").getItemStyle(),s=n.getModel(["blur","itemStyle"]).getItemStyle(),l=n.getModel(["select","itemStyle"]).getItemStyle(),u=n.getShallow("cursor"),f=i.get("focus"),h=i.get("blurScope"),v=i.get("scale");qy(r,function(d){if(d instanceof oe){var g=d.style;d.useStyle(V({image:g.image,x:g.x,y:g.y,width:g.width,height:g.height},t.style))}else d.useStyle(t.style);var y=d.ensureState("emphasis");y.style=o,v&&(y.scaleX=d.scaleX*1.1,y.scaleY=d.scaleY*1.1),d.ensureState("blur").style=s,d.ensureState("select").style=l,u&&(d.cursor=u),d.z2=t.z2});var c=e.valueDim.posDesc[+(t.boundingLength>0)],p=r.__pictorialBarRect;p.ignoreClip=!0,ve(p,ne(n),{labelFetcher:e.seriesModel,labelDataIndex:a,defaultText:fo(e.seriesModel.getData(),a),inheritColor:t.style.fill,defaultOpacity:t.style.opacity,defaultOutsidePosition:c}),Wt(r,f,h,i.get("disabled"))}function dx(r){var e=Math.round(r);return Math.abs(r-e)<1e-4?e:Math.ceil(r)}var ZH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.hasSymbolVisual=!0,t.defaultSymbol="roundRect",t}return e.prototype.getInitialData=function(t){return t.stack=null,r.prototype.getInitialData.apply(this,arguments)},e.type="series.pictorialBar",e.dependencies=["grid"],e.defaultOption=on(ll.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",clip:!1,progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}}),e}(ll);function qH(r){r.registerChartView(GH),r.registerSeriesModel(ZH),r.registerLayout(r.PRIORITY.VISUAL.LAYOUT,it(zC,"pictorialBar")),r.registerLayout(r.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,GC("pictorialBar"))}var KH=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t._layers=[],t}return e.prototype.render=function(t,a,n){var i=t.getData(),o=this,s=this.group,l=t.getLayerSeries(),u=i.getLayout("layoutInfo"),f=u.rect,h=u.boundaryGap;s.x=0,s.y=f.y+h[0];function v(g){return g.name}var c=new ya(this._layersSeries||[],l,v,v),p=[];c.add(X(d,this,"add")).update(X(d,this,"update")).remove(X(d,this,"remove")).execute();function d(g,y,m){var _=o._layers;if(g==="remove"){s.remove(_[y]);return}for(var S=[],b=[],x,w=l[y].indices,T=0;Ti&&(i=s),a.push(s)}for(var u=0;ui&&(i=h)}return{y0:n,max:i}}function e4(r){r.registerChartView(KH),r.registerSeriesModel(QH),r.registerLayout(JH),r.registerProcessor(Nl("themeRiver"))}var r4=2,a4=4,yx=function(r){k(e,r);function e(t,a,n,i){var o=r.call(this)||this;o.z2=r4,o.textConfig={inside:!0},nt(o).seriesIndex=a.seriesIndex;var s=new bt({z2:a4,silent:t.getModel().get(["label","silent"])});return o.setTextContent(s),o.updateData(!0,t,a,n,i),o}return e.prototype.updateData=function(t,a,n,i,o){this.node=a,a.piece=this,n=n||this._seriesModel,i=i||this._ecModel;var s=this;nt(s).dataIndex=a.dataIndex;var l=a.getModel(),u=l.getModel("emphasis"),f=a.getLayout(),h=V({},f);h.label=null;var v=a.getVisual("style");v.lineJoin="bevel";var c=a.getVisual("decal");c&&(v.decal=uo(c,o));var p=Zn(l.getModel("itemStyle"),h,!0);V(h,p),C(Ee,function(m){var _=s.ensureState(m),S=l.getModel([m,"itemStyle"]);_.style=S.getItemStyle();var b=Zn(S,h);b&&(_.shape=b)}),t?(s.setShape(h),s.shape.r=f.r0,Vt(s,{shape:{r:f.r}},n,a.dataIndex)):(Dt(s,{shape:h},n),Ir(s)),s.useStyle(v),this._updateLabel(n);var d=l.getShallow("cursor");d&&s.attr("cursor",d),this._seriesModel=n||this._seriesModel,this._ecModel=i||this._ecModel;var g=u.get("focus"),y=g==="ancestor"?a.getAncestorsIndices():g==="descendant"?a.getDescendantIndices():g;Wt(this,y,u.get("blurScope"),u.get("disabled"))},e.prototype._updateLabel=function(t){var a=this,n=this.node.getModel(),i=n.getModel("label"),o=this.node.getLayout(),s=o.endAngle-o.startAngle,l=(o.startAngle+o.endAngle)/2,u=Math.cos(l),f=Math.sin(l),h=this,v=h.getTextContent(),c=this.node.dataIndex,p=i.get("minAngle")/180*Math.PI,d=i.get("show")&&!(p!=null&&Math.abs(s)P&&!io(E-P)&&E0?(o.virtualPiece?o.virtualPiece.updateData(!1,m,t,a,n):(o.virtualPiece=new yx(m,t,a,n),f.add(o.virtualPiece)),_.piece.off("click"),o.virtualPiece.on("click",function(S){o._rootToNode(_.parentNode)})):o.virtualPiece&&(f.remove(o.virtualPiece),o.virtualPiece=null)}},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",function(a){var n=!1,i=t.seriesModel.getViewRoot();i.eachNode(function(o){if(!n&&o.piece&&o.piece===a.target){var s=o.getModel().get("nodeClick");if(s==="rootToNode")t._rootToNode(o);else if(s==="link"){var l=o.getModel(),u=l.get("link");if(u){var f=l.get("target",!0)||"_blank";If(u,f)}}n=!0}})})},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:Hd,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,a){var n=a.getData(),i=n.getItemLayout(0);if(i){var o=t[0]-i.cx,s=t[1]-i.cy,l=Math.sqrt(o*o+s*s);return l<=i.r&&l>=i.r0}},e.type="sunburst",e}(Rt),s4=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.ignoreStyleOnData=!0,t}return e.prototype.getInitialData=function(t,a){var n={name:t.name,children:t.data};aI(n);var i=this._levelModels=G(t.levels||[],function(l){return new Mt(l,this,a)},this),o=ky.createTree(n,this,s);function s(l){l.wrapMethod("getItemModel",function(u,f){var h=o.getNodeByDataIndex(f),v=i[h.depth];return v&&(u.parentModel=v),u})}return o.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.getDataParams=function(t){var a=r.prototype.getDataParams.apply(this,arguments),n=this.getData().tree.getNodeByDataIndex(t);return a.treePathInfo=Xh(n,this),a},e.prototype.getLevelModel=function(t){return this._levelModels&&this._levelModels[t.depth]},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var a=this.getRawData().tree.root;(!t||t!==a&&!a.contains(t))&&(this._viewRoot=a)},e.prototype.enableAriaDecal=function(){lM(this)},e.type="series.sunburst",e.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"},e}(kt);function aI(r){var e=0;C(r.children,function(a){aI(a);var n=a.value;z(n)&&(n=n[0]),e+=n});var t=r.value;z(t)&&(t=t[0]),(t==null||isNaN(t))&&(t=e),t<0&&(t=0),z(r.value)?r.value[0]=t:r.value=t}var _x=Math.PI/180;function l4(r,e,t){e.eachSeriesByType(r,function(a){var n=a.get("center"),i=a.get("radius");z(i)||(i=[0,i]),z(n)||(n=[n,n]);var o=t.getWidth(),s=t.getHeight(),l=Math.min(o,s),u=W(n[0],o),f=W(n[1],s),h=W(i[0],l/2),v=W(i[1],l/2),c=-a.get("startAngle")*_x,p=a.get("minAngle")*_x,d=a.getData().tree.root,g=a.getViewRoot(),y=g.depth,m=a.get("sort");m!=null&&nI(g,m);var _=0;C(g.children,function(E){!isNaN(E.getValue())&&_++});var S=g.getValue(),b=Math.PI/(S||_)*2,x=g.depth>0,w=g.height-(x?-1:1),T=(v-h)/(w||1),A=a.get("clockwise"),D=a.get("stillShowZeroSum"),M=A?1:-1,I=function(E,N){if(E){var O=N;if(E!==d){var B=E.getValue(),F=S===0&&D?b:B*b;F1;)o=o.parentNode;var s=n.getColorFromPalette(o.name||o.dataIndex+"",e);return a.depth>1&&U(s)&&(s=yf(s,(a.depth-1)/(i-1)*.5)),s}r.eachSeriesByType("sunburst",function(a){var n=a.getData(),i=n.tree;i.eachNode(function(o){var s=o.getModel(),l=s.getModel("itemStyle").getItemStyle();l.fill||(l.fill=t(o,a,i.root.height));var u=n.ensureUniqueItemVisual(o.dataIndex,"style");V(u,l)})})}function h4(r){r.registerChartView(o4),r.registerSeriesModel(s4),r.registerLayout(it(l4,"sunburst")),r.registerProcessor(it(Nl,"sunburst")),r.registerVisual(f4),i4(r)}var Sx={color:"fill",borderColor:"stroke"},v4={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},va=Tt(),c4=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},e.prototype.getInitialData=function(t,a){return Jr(null,this)},e.prototype.getDataParams=function(t,a,n){var i=r.prototype.getDataParams.call(this,t,a);return n&&(i.info=va(n).info),i},e.type="series.custom",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},e}(kt);function p4(r,e){return e=e||[0,0],G(["x","y"],function(t,a){var n=this.getAxis(t),i=e[a],o=r[a]/2;return n.type==="category"?n.getBandWidth():Math.abs(n.dataToCoord(i-o)-n.dataToCoord(i+o))},this)}function d4(r){var e=r.master.getRect();return{coordSys:{type:"cartesian2d",x:e.x,y:e.y,width:e.width,height:e.height},api:{coord:function(t){return r.dataToPoint(t)},size:X(p4,r)}}}function g4(r,e){return e=e||[0,0],G([0,1],function(t){var a=e[t],n=r[t]/2,i=[],o=[];return i[t]=a-n,o[t]=a+n,i[1-t]=o[1-t]=e[1-t],Math.abs(this.dataToPoint(i)[t]-this.dataToPoint(o)[t])},this)}function y4(r){var e=r.getBoundingRect();return{coordSys:{type:"geo",x:e.x,y:e.y,width:e.width,height:e.height,zoom:r.getZoom()},api:{coord:function(t){return r.dataToPoint(t)},size:X(g4,r)}}}function m4(r,e){var t=this.getAxis(),a=e instanceof Array?e[0]:e,n=(r instanceof Array?r[0]:r)/2;return t.type==="category"?t.getBandWidth():Math.abs(t.dataToCoord(a-n)-t.dataToCoord(a+n))}function _4(r){var e=r.getRect();return{coordSys:{type:"singleAxis",x:e.x,y:e.y,width:e.width,height:e.height},api:{coord:function(t){return r.dataToPoint(t)},size:X(m4,r)}}}function S4(r,e){return e=e||[0,0],G(["Radius","Angle"],function(t,a){var n="get"+t+"Axis",i=this[n](),o=e[a],s=r[a]/2,l=i.type==="category"?i.getBandWidth():Math.abs(i.dataToCoord(o-s)-i.dataToCoord(o+s));return t==="Angle"&&(l=l*Math.PI/180),l},this)}function x4(r){var e=r.getRadiusAxis(),t=r.getAngleAxis(),a=e.getExtent();return a[0]>a[1]&&a.reverse(),{coordSys:{type:"polar",cx:r.cx,cy:r.cy,r:a[1],r0:a[0]},api:{coord:function(n){var i=e.dataToRadius(n[0]),o=t.dataToAngle(n[1]),s=r.coordToPoint([i,o]);return s.push(i,o*Math.PI/180),s},size:X(S4,r)}}}function b4(r){var e=r.getRect(),t=r.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:r.getCellWidth(),cellHeight:r.getCellHeight(),rangeInfo:{start:t.start,end:t.end,weeks:t.weeks,dayCount:t.allDay}},api:{coord:function(a,n){return r.dataToPoint(a,n)}}}}function iI(r,e,t,a){return r&&(r.legacy||r.legacy!==!1&&!t&&!a&&e!=="tspan"&&(e==="text"||$(r,"text")))}function oI(r,e,t){var a=r,n,i,o;if(e==="text")o=a;else{o={},$(a,"text")&&(o.text=a.text),$(a,"rich")&&(o.rich=a.rich),$(a,"textFill")&&(o.fill=a.textFill),$(a,"textStroke")&&(o.stroke=a.textStroke),$(a,"fontFamily")&&(o.fontFamily=a.fontFamily),$(a,"fontSize")&&(o.fontSize=a.fontSize),$(a,"fontStyle")&&(o.fontStyle=a.fontStyle),$(a,"fontWeight")&&(o.fontWeight=a.fontWeight),i={type:"text",style:o,silent:!0},n={};var s=$(a,"textPosition");t?n.position=s?a.textPosition:"inside":s&&(n.position=a.textPosition),$(a,"textPosition")&&(n.position=a.textPosition),$(a,"textOffset")&&(n.offset=a.textOffset),$(a,"textRotation")&&(n.rotation=a.textRotation),$(a,"textDistance")&&(n.distance=a.textDistance)}return xx(o,r),C(o.rich,function(l){xx(l,l)}),{textConfig:n,textContent:i}}function xx(r,e){e&&(e.font=e.textFont||e.font,$(e,"textStrokeWidth")&&(r.lineWidth=e.textStrokeWidth),$(e,"textAlign")&&(r.align=e.textAlign),$(e,"textVerticalAlign")&&(r.verticalAlign=e.textVerticalAlign),$(e,"textLineHeight")&&(r.lineHeight=e.textLineHeight),$(e,"textWidth")&&(r.width=e.textWidth),$(e,"textHeight")&&(r.height=e.textHeight),$(e,"textBackgroundColor")&&(r.backgroundColor=e.textBackgroundColor),$(e,"textPadding")&&(r.padding=e.textPadding),$(e,"textBorderColor")&&(r.borderColor=e.textBorderColor),$(e,"textBorderWidth")&&(r.borderWidth=e.textBorderWidth),$(e,"textBorderRadius")&&(r.borderRadius=e.textBorderRadius),$(e,"textBoxShadowColor")&&(r.shadowColor=e.textBoxShadowColor),$(e,"textBoxShadowBlur")&&(r.shadowBlur=e.textBoxShadowBlur),$(e,"textBoxShadowOffsetX")&&(r.shadowOffsetX=e.textBoxShadowOffsetX),$(e,"textBoxShadowOffsetY")&&(r.shadowOffsetY=e.textBoxShadowOffsetY))}function bx(r,e,t){var a=r;a.textPosition=a.textPosition||t.position||"inside",t.offset!=null&&(a.textOffset=t.offset),t.rotation!=null&&(a.textRotation=t.rotation),t.distance!=null&&(a.textDistance=t.distance);var n=a.textPosition.indexOf("inside")>=0,i=r.fill||"#000";Tx(a,e);var o=a.textFill==null;return n?o&&(a.textFill=t.insideFill||"#fff",!a.textStroke&&t.insideStroke&&(a.textStroke=t.insideStroke),!a.textStroke&&(a.textStroke=i),a.textStrokeWidth==null&&(a.textStrokeWidth=2)):(o&&(a.textFill=r.fill||t.outsideFill||"#000"),!a.textStroke&&t.outsideStroke&&(a.textStroke=t.outsideStroke)),a.text=e.text,a.rich=e.rich,C(e.rich,function(s){Tx(s,s)}),a}function Tx(r,e){e&&($(e,"fill")&&(r.textFill=e.fill),$(e,"stroke")&&(r.textStroke=e.fill),$(e,"lineWidth")&&(r.textStrokeWidth=e.lineWidth),$(e,"font")&&(r.font=e.font),$(e,"fontStyle")&&(r.fontStyle=e.fontStyle),$(e,"fontWeight")&&(r.fontWeight=e.fontWeight),$(e,"fontSize")&&(r.fontSize=e.fontSize),$(e,"fontFamily")&&(r.fontFamily=e.fontFamily),$(e,"align")&&(r.textAlign=e.align),$(e,"verticalAlign")&&(r.textVerticalAlign=e.verticalAlign),$(e,"lineHeight")&&(r.textLineHeight=e.lineHeight),$(e,"width")&&(r.textWidth=e.width),$(e,"height")&&(r.textHeight=e.height),$(e,"backgroundColor")&&(r.textBackgroundColor=e.backgroundColor),$(e,"padding")&&(r.textPadding=e.padding),$(e,"borderColor")&&(r.textBorderColor=e.borderColor),$(e,"borderWidth")&&(r.textBorderWidth=e.borderWidth),$(e,"borderRadius")&&(r.textBorderRadius=e.borderRadius),$(e,"shadowColor")&&(r.textBoxShadowColor=e.shadowColor),$(e,"shadowBlur")&&(r.textBoxShadowBlur=e.shadowBlur),$(e,"shadowOffsetX")&&(r.textBoxShadowOffsetX=e.shadowOffsetX),$(e,"shadowOffsetY")&&(r.textBoxShadowOffsetY=e.shadowOffsetY),$(e,"textShadowColor")&&(r.textShadowColor=e.textShadowColor),$(e,"textShadowBlur")&&(r.textShadowBlur=e.textShadowBlur),$(e,"textShadowOffsetX")&&(r.textShadowOffsetX=e.textShadowOffsetX),$(e,"textShadowOffsetY")&&(r.textShadowOffsetY=e.textShadowOffsetY))}var sI={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},Ax=St(sI);lr($r,function(r,e){return r[e]=1,r},{});$r.join(", ");var jf=["","style","shape","extra"],co=Tt();function Ky(r,e,t,a,n){var i=r+"Animation",o=bo(r,a,n)||{},s=co(e).userDuring;return o.duration>0&&(o.during=s?X(D4,{el:e,userDuring:s}):null,o.setToFinal=!0,o.scope=r),V(o,t[i]),o}function of(r,e,t,a){a=a||{};var n=a.dataIndex,i=a.isInit,o=a.clearStyle,s=t.isAnimationEnabled(),l=co(r),u=e.style;l.userDuring=e.during;var f={},h={};if(I4(r,e,h),Dx("shape",e,h),Dx("extra",e,h),!i&&s&&(M4(r,e,f),Cx("shape",r,e,f),Cx("extra",r,e,f),L4(r,e,u,f)),h.style=u,w4(r,h,o),A4(r,e),s)if(i){var v={};C(jf,function(p){var d=p?e[p]:e;d&&d.enterFrom&&(p&&(v[p]=v[p]||{}),V(p?v[p]:v,d.enterFrom))});var c=Ky("enter",r,e,t,n);c.duration>0&&r.animateFrom(v,c)}else T4(r,e,n||0,t,f);lI(r,e),u?r.dirty():r.markRedraw()}function lI(r,e){for(var t=co(r).leaveToProps,a=0;a0&&r.animateFrom(n,i)}}function A4(r,e){$(e,"silent")&&(r.silent=e.silent),$(e,"ignore")&&(r.ignore=e.ignore),r instanceof ur&&$(e,"invisible")&&(r.invisible=e.invisible),r instanceof yt&&$(e,"autoBatch")&&(r.autoBatch=e.autoBatch)}var Nr={},C4={setTransform:function(r,e){return Nr.el[r]=e,this},getTransform:function(r){return Nr.el[r]},setShape:function(r,e){var t=Nr.el,a=t.shape||(t.shape={});return a[r]=e,t.dirtyShape&&t.dirtyShape(),this},getShape:function(r){var e=Nr.el.shape;if(e)return e[r]},setStyle:function(r,e){var t=Nr.el,a=t.style;return a&&(a[r]=e,t.dirtyStyle&&t.dirtyStyle()),this},getStyle:function(r){var e=Nr.el.style;if(e)return e[r]},setExtra:function(r,e){var t=Nr.el.extra||(Nr.el.extra={});return t[r]=e,this},getExtra:function(r){var e=Nr.el.extra;if(e)return e[r]}};function D4(){var r=this,e=r.el;if(e){var t=co(e).userDuring,a=r.userDuring;if(t!==a){r.el=r.userDuring=null;return}Nr.el=e,a(C4)}}function Cx(r,e,t,a){var n=t[r];if(n){var i=e[r],o;if(i){var s=t.transition,l=n.transition;if(l)if(!o&&(o=a[r]={}),ei(l))V(o,i);else for(var u=Et(l),f=0;f=0){!o&&(o=a[r]={});for(var c=St(i),f=0;f=0)){var v=r.getAnimationStyleProps(),c=v?v.style:null;if(c){!i&&(i=a.style={});for(var p=St(t),u=0;u=0?e.getStore().get(N,R):void 0}var O=e.get(E.name,R),B=E&&E.ordinalMeta;return B?B.categories[O]:O}function x(P,R){R==null&&(R=u);var E=e.getItemVisual(R,"style"),N=E&&E.fill,O=E&&E.opacity,B=m(R,Fa).getItemStyle();N!=null&&(B.fill=N),O!=null&&(B.opacity=O);var F={inheritColor:U(N)?N:"#000"},H=_(R,Fa),Y=Nt(H,null,F,!1,!0);Y.text=H.getShallow("show")?ot(r.getFormattedLabel(R,Fa),fo(e,R)):null;var j=Df(H,F,!1);return A(P,B),B=bx(B,Y,j),P&&T(B,P),B.legacy=!0,B}function w(P,R){R==null&&(R=u);var E=m(R,ca).getItemStyle(),N=_(R,ca),O=Nt(N,null,null,!0,!0);O.text=N.getShallow("show")?br(r.getFormattedLabel(R,ca),r.getFormattedLabel(R,Fa),fo(e,R)):null;var B=Df(N,null,!0);return A(P,E),E=bx(E,O,B),P&&T(E,P),E.legacy=!0,E}function T(P,R){for(var E in R)$(R,E)&&(P[E]=R[E])}function A(P,R){P&&(P.textFill&&(R.textFill=P.textFill),P.textPosition&&(R.textPosition=P.textPosition))}function D(P,R){if(R==null&&(R=u),$(Sx,P)){var E=e.getItemVisual(R,"style");return E?E[Sx[P]]:null}if($(v4,P))return e.getItemVisual(R,P)}function M(P){if(i.type==="cartesian2d"){var R=i.getBaseAxis();return TN(Q({axis:R},P))}}function I(){return t.getCurrentSeriesIndices()}function L(P){return zg(P,t)}}function G4(r){var e={};return C(r.dimensions,function(t){var a=r.getDimensionInfo(t);if(!a.isExtraCoord){var n=a.coordDim,i=e[n]=e[n]||[];i[a.coordDimIndex]=r.getDimensionIndex(t)}}),e}function Qc(r,e,t,a,n,i,o){if(!a){i.remove(e);return}var s=em(r,e,t,a,n,i);return s&&o.setItemGraphicEl(t,s),s&&Wt(s,a.focus,a.blurScope,a.emphasisDisabled),s}function em(r,e,t,a,n,i){var o=-1,s=e;e&&vI(e,a,n)&&(o=vt(i.childrenRef(),e),e=null);var l=!e,u=e;u?u.clearStates():(u=Jy(a),s&&N4(s,u)),a.morph===!1?u.disableMorphing=!0:u.disableMorphing&&(u.disableMorphing=!1),je.normal.cfg=je.normal.conOpt=je.emphasis.cfg=je.emphasis.conOpt=je.blur.cfg=je.blur.conOpt=je.select.cfg=je.select.conOpt=null,je.isLegacy=!1,H4(u,t,a,n,l,je),F4(u,t,a,n,l),tm(r,u,t,a,je,n,l),$(a,"info")&&(va(u).info=a.info);for(var f=0;f=0?i.replaceAt(u,o):i.add(u),u}function vI(r,e,t){var a=va(r),n=e.type,i=e.shape,o=e.style;return t.isUniversalTransitionEnabled()||n!=null&&n!==a.customGraphicType||n==="path"&&$4(i)&&cI(i)!==a.customPathData||n==="image"&&$(o,"image")&&o.image!==a.customImagePath}function F4(r,e,t,a,n){var i=t.clipPath;if(i===!1)r&&r.getClipPath()&&r.removeClipPath();else if(i){var o=r.getClipPath();o&&vI(o,i,a)&&(o=null),o||(o=Jy(i),r.setClipPath(o)),tm(null,o,e,i,null,a,n)}}function H4(r,e,t,a,n,i){if(!r.isGroup){Ix(t,null,i),Ix(t,ca,i);var o=i.normal.conOpt,s=i.emphasis.conOpt,l=i.blur.conOpt,u=i.select.conOpt;if(o!=null||s!=null||u!=null||l!=null){var f=r.getTextContent();if(o===!1)f&&r.removeTextContent();else{o=i.normal.conOpt=o||{type:"text"},f?f.clearStates():(f=Jy(o),r.setTextContent(f)),tm(null,f,e,o,null,a,n);for(var h=o&&o.style,v=0;v=f;c--){var p=e.childAt(c);U4(e,p,n)}}}function U4(r,e,t){e&&qh(e,va(r).option,t)}function Y4(r){new ya(r.oldChildren,r.newChildren,Lx,Lx,r).add(Px).update(Px).remove(X4).execute()}function Lx(r,e){var t=r&&r.name;return t??k4+e}function Px(r,e){var t=this.context,a=r!=null?t.newChildren[r]:null,n=e!=null?t.oldChildren[e]:null;em(t.api,n,t.dataIndex,a,t.seriesModel,t.group)}function X4(r){var e=this.context,t=e.oldChildren[r];t&&qh(t,va(t).option,e.seriesModel)}function cI(r){return r&&(r.pathData||r.d)}function $4(r){return r&&($(r,"pathData")||$(r,"d"))}function Z4(r){r.registerChartView(B4),r.registerSeriesModel(c4)}var zn=Tt(),Rx=et,Jc=X,am=function(){function r(){this._dragging=!1,this.animationThreshold=15}return r.prototype.render=function(e,t,a,n){var i=t.get("value"),o=t.get("status");if(this._axisModel=e,this._axisPointerModel=t,this._api=a,!(!n&&this._lastValue===i&&this._lastStatus===o)){this._lastValue=i,this._lastStatus=o;var s=this._group,l=this._handle;if(!o||o==="hide"){s&&s.hide(),l&&l.hide();return}s&&s.show(),l&&l.show();var u={};this.makeElOption(u,i,e,t,a);var f=u.graphicKey;f!==this._lastGraphicKey&&this.clear(a),this._lastGraphicKey=f;var h=this._moveAnimation=this.determineAnimation(e,t);if(!s)s=this._group=new at,this.createPointerEl(s,u,e,t),this.createLabelEl(s,u,e,t),a.getZr().add(s);else{var v=it(Ex,t,h);this.updatePointerEl(s,u,v),this.updateLabelEl(s,u,v,t)}Ox(s,t,!0),this._renderHandle(i)}},r.prototype.remove=function(e){this.clear(e)},r.prototype.dispose=function(e){this.clear(e)},r.prototype.determineAnimation=function(e,t){var a=t.get("animation"),n=e.axis,i=n.type==="category",o=t.get("snap");if(!o&&!i)return!1;if(a==="auto"||a==null){var s=this.animationThreshold;if(i&&n.getBandWidth()>s)return!0;if(o){var l=Dy(e).seriesDataCount,u=n.getExtent();return Math.abs(u[0]-u[1])/l>s}return!1}return a===!0},r.prototype.makeElOption=function(e,t,a,n,i){},r.prototype.createPointerEl=function(e,t,a,n){var i=t.pointer;if(i){var o=zn(e).pointerEl=new ci[i.type](Rx(t.pointer));e.add(o)}},r.prototype.createLabelEl=function(e,t,a,n){if(t.label){var i=zn(e).labelEl=new bt(Rx(t.label));e.add(i),kx(i,n)}},r.prototype.updatePointerEl=function(e,t,a){var n=zn(e).pointerEl;n&&t.pointer&&(n.setStyle(t.pointer.style),a(n,{shape:t.pointer.shape}))},r.prototype.updateLabelEl=function(e,t,a,n){var i=zn(e).labelEl;i&&(i.setStyle(t.label.style),a(i,{x:t.label.x,y:t.label.y}),kx(i,n))},r.prototype._renderHandle=function(e){if(!(this._dragging||!this.updateHandleTransform)){var t=this._axisPointerModel,a=this._api.getZr(),n=this._handle,i=t.getModel("handle"),o=t.get("status");if(!i.get("show")||!o||o==="hide"){n&&a.remove(n),this._handle=null;return}var s;this._handle||(s=!0,n=this._handle=wo(i.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(u){pa(u.event)},onmousedown:Jc(this._onHandleDragMove,this,0,0),drift:Jc(this._onHandleDragMove,this),ondragend:Jc(this._onHandleDragEnd,this)}),a.add(n)),Ox(n,t,!1),n.setStyle(i.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var l=i.get("size");z(l)||(l=[l,l]),n.scaleX=l[0]/2,n.scaleY=l[1]/2,Po(this,"_doDispatchAxisPointer",i.get("throttle")||0,"fixRate"),this._moveHandleToValue(e,s)}},r.prototype._moveHandleToValue=function(e,t){Ex(this._axisPointerModel,!t&&this._moveAnimation,this._handle,tp(this.getHandleTransform(e,this._axisModel,this._axisPointerModel)))},r.prototype._onHandleDragMove=function(e,t){var a=this._handle;if(a){this._dragging=!0;var n=this.updateHandleTransform(tp(a),[e,t],this._axisModel,this._axisPointerModel);this._payloadInfo=n,a.stopAnimation(),a.attr(tp(n)),zn(a).lastProp=null,this._doDispatchAxisPointer()}},r.prototype._doDispatchAxisPointer=function(){var e=this._handle;if(e){var t=this._payloadInfo,a=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:a.axis.dim,axisIndex:a.componentIndex}]})}},r.prototype._onHandleDragEnd=function(){this._dragging=!1;var e=this._handle;if(e){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},r.prototype.clear=function(e){this._lastValue=null,this._lastStatus=null;var t=e.getZr(),a=this._group,n=this._handle;t&&a&&(this._lastGraphicKey=null,a&&t.remove(a),n&&t.remove(n),this._group=null,this._handle=null,this._payloadInfo=null),rl(this,"_doDispatchAxisPointer")},r.prototype.doClear=function(){},r.prototype.buildLabel=function(e,t,a){return a=a||0,{x:e[a],y:e[1-a],width:t[a],height:t[1-a]}},r}();function Ex(r,e,t,a){pI(zn(t).lastProp,a)||(zn(t).lastProp=a,e?Dt(t,a,r):(t.stopAnimation(),t.attr(a)))}function pI(r,e){if(tt(r)&&tt(e)){var t=!0;return C(e,function(a,n){t=t&&pI(r[n],a)}),!!t}else return r===e}function kx(r,e){r[e.get(["label","show"])?"show":"hide"]()}function tp(r){return{x:r.x||0,y:r.y||0,rotation:r.rotation||0}}function Ox(r,e,t){var a=e.get("z"),n=e.get("zlevel");r&&r.traverse(function(i){i.type!=="group"&&(a!=null&&(i.z=a),n!=null&&(i.zlevel=n),i.silent=t)})}function nm(r){var e=r.get("type"),t=r.getModel(e+"Style"),a;return e==="line"?(a=t.getLineStyle(),a.fill=null):e==="shadow"&&(a=t.getAreaStyle(),a.stroke=null),a}function dI(r,e,t,a,n){var i=t.get("value"),o=gI(i,e.axis,e.ecModel,t.get("seriesDataIndices"),{precision:t.get(["label","precision"]),formatter:t.get(["label","formatter"])}),s=t.getModel("label"),l=pi(s.get("padding")||0),u=s.getFont(),f=bl(o,u),h=n.position,v=f.width+l[1]+l[3],c=f.height+l[0]+l[2],p=n.align;p==="right"&&(h[0]-=v),p==="center"&&(h[0]-=v/2);var d=n.verticalAlign;d==="bottom"&&(h[1]-=c),d==="middle"&&(h[1]-=c/2),q4(h,v,c,a);var g=s.get("backgroundColor");(!g||g==="auto")&&(g=e.get(["axisLine","lineStyle","color"])),r.label={x:h[0],y:h[1],style:Nt(s,{text:o,font:u,fill:s.getTextColor(),padding:l,backgroundColor:g}),z2:10}}function q4(r,e,t,a){var n=a.getWidth(),i=a.getHeight();r[0]=Math.min(r[0]+e,n)-e,r[1]=Math.min(r[1]+t,i)-t,r[0]=Math.max(r[0],0),r[1]=Math.max(r[1],0)}function gI(r,e,t,a,n){r=e.scale.parse(r);var i=e.scale.getLabel({value:r},{precision:n.precision}),o=n.formatter;if(o){var s={value:gy(e,{value:r}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};C(a,function(l){var u=t.getSeriesByIndex(l.seriesIndex),f=l.dataIndexInside,h=u&&u.getDataParams(f);h&&s.seriesData.push(h)}),U(o)?i=o.replace("{value}",i):K(o)&&(i=o(s))}return i}function im(r,e,t){var a=Ge();return an(a,a,t.rotation),Dr(a,a,t.position),Ar([r.dataToCoord(e),(t.labelOffset||0)+(t.labelDirection||1)*(t.labelMargin||0)],a)}function yI(r,e,t,a,n,i){var o=Pe.innerTextLayout(t.rotation,0,t.labelDirection);t.labelMargin=n.get(["label","margin"]),dI(e,a,n,i,{position:im(a.axis,r,t),align:o.textAlign,verticalAlign:o.textVerticalAlign})}function om(r,e,t){return t=t||0,{x1:r[t],y1:r[1-t],x2:e[t],y2:e[1-t]}}function mI(r,e,t){return t=t||0,{x:r[t],y:r[1-t],width:e[t],height:e[1-t]}}function Nx(r,e,t,a,n,i){return{cx:r,cy:e,r0:t,r:a,startAngle:n,endAngle:i,clockwise:!0}}var K4=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.makeElOption=function(t,a,n,i,o){var s=n.axis,l=s.grid,u=i.get("type"),f=Bx(l,s).getOtherAxis(s).getGlobalExtent(),h=s.toGlobalCoord(s.dataToCoord(a,!0));if(u&&u!=="none"){var v=nm(i),c=j4[u](s,h,f);c.style=v,t.graphicKey=c.type,t.pointer=c}var p=Td(l.model,n);yI(a,t,p,n,i,o)},e.prototype.getHandleTransform=function(t,a,n){var i=Td(a.axis.grid.model,a,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var o=im(a.axis,t,i);return{x:o[0],y:o[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,a,n,i){var o=n.axis,s=o.grid,l=o.getGlobalExtent(!0),u=Bx(s,o).getOtherAxis(o).getGlobalExtent(),f=o.dim==="x"?0:1,h=[t.x,t.y];h[f]+=a[f],h[f]=Math.min(l[1],h[f]),h[f]=Math.max(l[0],h[f]);var v=(u[1]+u[0])/2,c=[v,v];c[f]=h[f];var p=[{verticalAlign:"middle"},{align:"center"}];return{x:h[0],y:h[1],rotation:t.rotation,cursorPoint:c,tooltipOption:p[f]}},e}(am);function Bx(r,e){var t={};return t[e.dim+"AxisIndex"]=e.index,r.getCartesian(t)}var j4={line:function(r,e,t){var a=om([e,t[0]],[e,t[1]],Vx(r));return{type:"Line",subPixelOptimize:!0,shape:a}},shadow:function(r,e,t){var a=Math.max(1,r.getBandWidth()),n=t[1]-t[0];return{type:"Rect",shape:mI([e-a/2,t[0]],[a,n],Vx(r))}}};function Vx(r){return r.dim==="x"?0:1}var Q4=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},e}(mt),ha=Tt(),J4=C;function _I(r,e,t){if(!_t.node){var a=e.getZr();ha(a).records||(ha(a).records={}),tW(a,e);var n=ha(a).records[r]||(ha(a).records[r]={});n.handler=t}}function tW(r,e){if(ha(r).initialized)return;ha(r).initialized=!0,t("click",it(zx,"click")),t("mousemove",it(zx,"mousemove")),t("globalout",rW);function t(a,n){r.on(a,function(i){var o=aW(e);J4(ha(r).records,function(s){s&&n(s,i,o.dispatchAction)}),eW(o.pendings,e)})}}function eW(r,e){var t=r.showTip.length,a=r.hideTip.length,n;t?n=r.showTip[t-1]:a&&(n=r.hideTip[a-1]),n&&(n.dispatchAction=null,e.dispatchAction(n))}function rW(r,e,t){r.handler("leave",null,t)}function zx(r,e,t,a){e.handler(r,t,a)}function aW(r){var e={showTip:[],hideTip:[]},t=function(a){var n=e[a.type];n?n.push(a):(a.dispatchAction=t,r.dispatchAction(a))};return{dispatchAction:t,pendings:e}}function Yd(r,e){if(!_t.node){var t=e.getZr(),a=(ha(t).records||{})[r];a&&(ha(t).records[r]=null)}}var nW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=a.getComponent("tooltip"),o=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";_I("axisPointer",n,function(s,l,u){o!=="none"&&(s==="leave"||o.indexOf(s)>=0)&&u({type:"updateAxisPointer",currTrigger:s,x:l&&l.offsetX,y:l&&l.offsetY})})},e.prototype.remove=function(t,a){Yd("axisPointer",a)},e.prototype.dispose=function(t,a){Yd("axisPointer",a)},e.type="axisPointer",e}(zt);function SI(r,e){var t=[],a=r.seriesIndex,n;if(a==null||!(n=e.getSeriesByIndex(a)))return{point:[]};var i=n.getData(),o=ni(i,r);if(o==null||o<0||z(o))return{point:[]};var s=i.getItemGraphicEl(o),l=n.coordinateSystem;if(n.getTooltipPosition)t=n.getTooltipPosition(o)||[];else if(l&&l.dataToPoint)if(r.isStacked){var u=l.getBaseAxis(),f=l.getOtherAxis(u),h=f.dim,v=u.dim,c=h==="x"||h==="radius"?1:0,p=i.mapDimension(v),d=[];d[c]=i.get(p,o),d[1-c]=i.get(i.getCalculationInfo("stackResultDimension"),o),t=l.dataToPoint(d)||[]}else t=l.dataToPoint(i.getValues(G(l.dimensions,function(y){return i.mapDimension(y)}),o))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),t=[g.x+g.width/2,g.y+g.height/2]}return{point:t,el:s}}var Gx=Tt();function iW(r,e,t){var a=r.currTrigger,n=[r.x,r.y],i=r,o=r.dispatchAction||X(t.dispatchAction,t),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){sf(n)&&(n=SI({seriesIndex:i.seriesIndex,dataIndex:i.dataIndex},e).point);var l=sf(n),u=i.axesInfo,f=s.axesInfo,h=a==="leave"||sf(n),v={},c={},p={list:[],map:{}},d={showPointer:it(sW,c),showTooltip:it(lW,p)};C(s.coordSysMap,function(y,m){var _=l||y.containPoint(n);C(s.coordSysAxesInfo[m],function(S,b){var x=S.axis,w=vW(u,S);if(!h&&_&&(!u||w)){var T=w&&w.value;T==null&&!l&&(T=x.pointToData(n)),T!=null&&Fx(S,T,d,!1,v)}})});var g={};return C(f,function(y,m){var _=y.linkGroup;_&&!c[m]&&C(_.axesInfo,function(S,b){var x=c[b];if(S!==y&&x){var w=x.value;_.mapper&&(w=y.axis.scale.parse(_.mapper(w,Hx(S),Hx(y)))),g[y.key]=w}})}),C(g,function(y,m){Fx(f[m],y,d,!0,v)}),uW(c,f,v),fW(p,n,r,o),hW(f,o,t),v}}function Fx(r,e,t,a,n){var i=r.axis;if(!(i.scale.isBlank()||!i.containData(e))){if(!r.involveSeries){t.showPointer(r,e);return}var o=oW(e,r),s=o.payloadBatch,l=o.snapToValue;s[0]&&n.seriesIndex==null&&V(n,s[0]),!a&&r.snap&&i.containData(l)&&l!=null&&(e=l),t.showPointer(r,e,s),t.showTooltip(r,o,l)}}function oW(r,e){var t=e.axis,a=t.dim,n=r,i=[],o=Number.MAX_VALUE,s=-1;return C(e.seriesModels,function(l,u){var f=l.getData().mapDimensionsAll(a),h,v;if(l.getAxisTooltipData){var c=l.getAxisTooltipData(f,r,t);v=c.dataIndices,h=c.nestestValue}else{if(v=l.getData().indicesOfNearest(f[0],r,t.type==="category"?.5:null),!v.length)return;h=l.getData().get(f[0],v[0])}if(!(h==null||!isFinite(h))){var p=r-h,d=Math.abs(p);d<=o&&((d=0&&s<0)&&(o=d,s=p,n=h,i.length=0),C(v,function(g){i.push({seriesIndex:l.seriesIndex,dataIndexInside:g,dataIndex:l.getData().getRawIndex(g)})}))}}),{payloadBatch:i,snapToValue:n}}function sW(r,e,t,a){r[e.key]={value:t,payloadBatch:a}}function lW(r,e,t,a){var n=t.payloadBatch,i=e.axis,o=i.model,s=e.axisPointerModel;if(!(!e.triggerTooltip||!n.length)){var l=e.coordSys.model,u=ul(l),f=r.map[u];f||(f=r.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},r.list.push(f)),f.dataByAxis.push({axisDim:i.dim,axisIndex:o.componentIndex,axisType:o.type,axisId:o.id,value:a,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:n.slice()})}}function uW(r,e,t){var a=t.axesInfo=[];C(e,function(n,i){var o=n.axisPointerModel.option,s=r[i];s?(!n.useHandle&&(o.status="show"),o.value=s.value,o.seriesDataIndices=(s.payloadBatch||[]).slice()):!n.useHandle&&(o.status="hide"),o.status==="show"&&a.push({axisDim:n.axis.dim,axisIndex:n.axis.model.componentIndex,value:o.value})})}function fW(r,e,t,a){if(sf(e)||!r.list.length){a({type:"hideTip"});return}var n=((r.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};a({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:t.tooltipOption,position:t.position,dataIndexInside:n.dataIndexInside,dataIndex:n.dataIndex,seriesIndex:n.seriesIndex,dataByCoordSys:r.list})}function hW(r,e,t){var a=t.getZr(),n="axisPointerLastHighlights",i=Gx(a)[n]||{},o=Gx(a)[n]={};C(r,function(u,f){var h=u.axisPointerModel.option;h.status==="show"&&u.triggerEmphasis&&C(h.seriesDataIndices,function(v){var c=v.seriesIndex+" | "+v.dataIndex;o[c]=v})});var s=[],l=[];C(i,function(u,f){!o[f]&&l.push(u)}),C(o,function(u,f){!i[f]&&s.push(u)}),l.length&&t.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&t.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}function vW(r,e){for(var t=0;t<(r||[]).length;t++){var a=r[t];if(e.axis.dim===a.axisDim&&e.axis.model.componentIndex===a.axisIndex)return a}}function Hx(r){var e=r.axis.model,t={},a=t.axisDim=r.axis.dim;return t.axisIndex=t[a+"AxisIndex"]=e.componentIndex,t.axisName=t[a+"AxisName"]=e.name,t.axisId=t[a+"AxisId"]=e.id,t}function sf(r){return!r||r[0]==null||isNaN(r[0])||r[1]==null||isNaN(r[1])}function Gl(r){mi.registerAxisPointerClass("CartesianAxisPointer",K4),r.registerComponentModel(Q4),r.registerComponentView(nW),r.registerPreprocessor(function(e){if(e){(!e.axisPointer||e.axisPointer.length===0)&&(e.axisPointer={});var t=e.axisPointer.link;t&&!z(t)&&(e.axisPointer.link=[t])}}),r.registerProcessor(r.PRIORITY.PROCESSOR.STATISTIC,function(e,t){e.getComponent("axisPointer").coordSysAxesInfo=bz(e,t)}),r.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},iW)}function cW(r){gt(ZD),gt(Gl)}var pW=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.makeElOption=function(t,a,n,i,o){var s=n.axis;s.dim==="angle"&&(this.animationThreshold=Math.PI/18);var l=s.polar,u=l.getOtherAxis(s),f=u.getExtent(),h=s.dataToCoord(a),v=i.get("type");if(v&&v!=="none"){var c=nm(i),p=gW[v](s,l,h,f);p.style=c,t.graphicKey=p.type,t.pointer=p}var d=i.get(["label","margin"]),g=dW(a,n,i,l,d);dI(t,n,i,o,g)},e}(am);function dW(r,e,t,a,n){var i=e.axis,o=i.dataToCoord(r),s=a.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l=a.getRadiusAxis().getExtent(),u,f,h;if(i.dim==="radius"){var v=Ge();an(v,v,s),Dr(v,v,[a.cx,a.cy]),u=Ar([o,-n],v);var c=e.getModel("axisLabel").get("rotate")||0,p=Pe.innerTextLayout(s,c*Math.PI/180,-1);f=p.textAlign,h=p.textVerticalAlign}else{var d=l[1];u=a.coordToPoint([d+n,o]);var g=a.cx,y=a.cy;f=Math.abs(u[0]-g)/d<.3?"center":u[0]>g?"left":"right",h=Math.abs(u[1]-y)/d<.3?"middle":u[1]>y?"top":"bottom"}return{position:u,align:f,verticalAlign:h}}var gW={line:function(r,e,t,a){return r.dim==="angle"?{type:"Line",shape:om(e.coordToPoint([a[0],t]),e.coordToPoint([a[1],t]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:t}}},shadow:function(r,e,t,a){var n=Math.max(1,r.getBandWidth()),i=Math.PI/180;return r.dim==="angle"?{type:"Sector",shape:Nx(e.cx,e.cy,a[0],a[1],(-t-n/2)*i,(-t+n/2)*i)}:{type:"Sector",shape:Nx(e.cx,e.cy,t-n/2,t+n/2,0,Math.PI*2)}}},yW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.findAxisModel=function(t){var a,n=this.ecModel;return n.eachComponent(t,function(i){i.getCoordSysModel()===this&&(a=i)},this),a},e.type="polar",e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(mt),sm=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Kt).models[0]},e.type="polarAxis",e}(mt);Xt(sm,Oo);var mW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="angleAxis",e}(sm),_W=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="radiusAxis",e}(sm),lm=function(r){k(e,r);function e(t,a){return r.call(this,"radius",t,a)||this}return e.prototype.pointToData=function(t,a){return this.polar.pointToData(t,a)[this.dim==="radius"?0:1]},e}(Lr);lm.prototype.dataToRadius=Lr.prototype.dataToCoord;lm.prototype.radiusToData=Lr.prototype.coordToData;var SW=Tt(),um=function(r){k(e,r);function e(t,a){return r.call(this,"angle",t,a||[0,360])||this}return e.prototype.pointToData=function(t,a){return this.polar.pointToData(t,a)[this.dim==="radius"?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,a=t.getLabelModel(),n=t.scale,i=n.getExtent(),o=n.count();if(i[1]-i[0]<1)return 0;var s=i[0],l=t.dataToCoord(s+1)-t.dataToCoord(s),u=Math.abs(l),f=bl(s==null?"":s+"",a.getFont(),"center","top"),h=Math.max(f.height,7),v=h/u;isNaN(v)&&(v=1/0);var c=Math.max(0,Math.floor(v)),p=SW(t.model),d=p.lastAutoInterval,g=p.lastTickCount;return d!=null&&g!=null&&Math.abs(d-c)<=1&&Math.abs(g-o)<=1&&d>c?c=d:(p.lastTickCount=o,p.lastAutoInterval=c),c},e}(Lr);um.prototype.dataToAngle=Lr.prototype.dataToCoord;um.prototype.angleToData=Lr.prototype.coordToData;var xI=["radius","angle"],xW=function(){function r(e){this.dimensions=xI,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new lm,this._angleAxis=new um,this.axisPointerEnabled=!0,this.name=e||"",this._radiusAxis.polar=this._angleAxis.polar=this}return r.prototype.containPoint=function(e){var t=this.pointToCoord(e);return this._radiusAxis.contain(t[0])&&this._angleAxis.contain(t[1])},r.prototype.containData=function(e){return this._radiusAxis.containData(e[0])&&this._angleAxis.containData(e[1])},r.prototype.getAxis=function(e){var t="_"+e+"Axis";return this[t]},r.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},r.prototype.getAxesByScale=function(e){var t=[],a=this._angleAxis,n=this._radiusAxis;return a.scale.type===e&&t.push(a),n.scale.type===e&&t.push(n),t},r.prototype.getAngleAxis=function(){return this._angleAxis},r.prototype.getRadiusAxis=function(){return this._radiusAxis},r.prototype.getOtherAxis=function(e){var t=this._angleAxis;return e===t?this._radiusAxis:t},r.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},r.prototype.getTooltipAxes=function(e){var t=e!=null&&e!=="auto"?this.getAxis(e):this.getBaseAxis();return{baseAxes:[t],otherAxes:[this.getOtherAxis(t)]}},r.prototype.dataToPoint=function(e,t){return this.coordToPoint([this._radiusAxis.dataToRadius(e[0],t),this._angleAxis.dataToAngle(e[1],t)])},r.prototype.pointToData=function(e,t){var a=this.pointToCoord(e);return[this._radiusAxis.radiusToData(a[0],t),this._angleAxis.angleToData(a[1],t)]},r.prototype.pointToCoord=function(e){var t=e[0]-this.cx,a=e[1]-this.cy,n=this.getAngleAxis(),i=n.getExtent(),o=Math.min(i[0],i[1]),s=Math.max(i[0],i[1]);n.inverse?o=s-360:s=o+360;var l=Math.sqrt(t*t+a*a);t/=l,a/=l;for(var u=Math.atan2(-a,t)/Math.PI*180,f=us;)u+=f*360;return[l,u]},r.prototype.coordToPoint=function(e){var t=e[0],a=e[1]/180*Math.PI,n=Math.cos(a)*t+this.cx,i=-Math.sin(a)*t+this.cy;return[n,i]},r.prototype.getArea=function(){var e=this.getAngleAxis(),t=this.getRadiusAxis(),a=t.getExtent().slice();a[0]>a[1]&&a.reverse();var n=e.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:a[0],r:a[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:e.inverse,contain:function(o,s){var l=o-this.cx,u=s-this.cy,f=l*l+u*u-1e-4,h=this.r,v=this.r0;return f<=h*h&&f>=v*v}}},r.prototype.convertToPixel=function(e,t,a){var n=Wx(t);return n===this?this.dataToPoint(a):null},r.prototype.convertFromPixel=function(e,t,a){var n=Wx(t);return n===this?this.pointToData(a):null},r}();function Wx(r){var e=r.seriesModel,t=r.polarModel;return t&&t.coordinateSystem||e&&e.coordinateSystem}function bW(r,e,t){var a=e.get("center"),n=t.getWidth(),i=t.getHeight();r.cx=W(a[0],n),r.cy=W(a[1],i);var o=r.getRadiusAxis(),s=Math.min(n,i)/2,l=e.get("radius");l==null?l=[0,"100%"]:z(l)||(l=[0,l]);var u=[W(l[0],s),W(l[1],s)];o.inverse?o.setExtent(u[1],u[0]):o.setExtent(u[0],u[1])}function wW(r,e){var t=this,a=t.getAngleAxis(),n=t.getRadiusAxis();if(a.scale.setExtent(1/0,-1/0),n.scale.setExtent(1/0,-1/0),r.eachSeries(function(s){if(s.coordinateSystem===t){var l=s.getData();C(Gf(l,"radius"),function(u){n.scale.unionExtentFromData(l,u)}),C(Gf(l,"angle"),function(u){a.scale.unionExtentFromData(l,u)})}}),li(a.scale,a.model),li(n.scale,n.model),a.type==="category"&&!a.onBand){var i=a.getExtent(),o=360/a.scale.count();a.inverse?i[1]+=o:i[1]-=o,a.setExtent(i[0],i[1])}}function TW(r){return r.mainType==="angleAxis"}function Ux(r,e){var t;if(r.type=e.get("type"),r.scale=Pl(e),r.onBand=e.get("boundaryGap")&&r.type==="category",r.inverse=e.get("inverse"),TW(e)){r.inverse=r.inverse!==e.get("clockwise");var a=e.get("startAngle"),n=(t=e.get("endAngle"))!==null&&t!==void 0?t:a+(r.inverse?-360:360);r.setExtent(a,n)}e.axis=r,r.model=e}var AW={dimensions:xI,create:function(r,e){var t=[];return r.eachComponent("polar",function(a,n){var i=new xW(n+"");i.update=wW;var o=i.getRadiusAxis(),s=i.getAngleAxis(),l=a.findAxisModel("radiusAxis"),u=a.findAxisModel("angleAxis");Ux(o,l),Ux(s,u),bW(i,a,e),t.push(i),a.coordinateSystem=i,i.model=a}),r.eachSeries(function(a){if(a.get("coordinateSystem")==="polar"){var n=a.getReferringComponents("polar",Kt).models[0];a.coordinateSystem=n.coordinateSystem}}),t}},CW=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function Nu(r,e,t){e[1]>e[0]&&(e=e.slice().reverse());var a=r.coordToPoint([e[0],t]),n=r.coordToPoint([e[1],t]);return{x1:a[0],y1:a[1],x2:n[0],y2:n[1]}}function Bu(r){var e=r.getRadiusAxis();return e.inverse?0:1}function Yx(r){var e=r[0],t=r[r.length-1];e&&t&&Math.abs(Math.abs(e.coord-t.coord)-360)<1e-4&&r.pop()}var DW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.axisPointerClass="PolarAxisPointer",t}return e.prototype.render=function(t,a){if(this.group.removeAll(),!!t.get("show")){var n=t.axis,i=n.polar,o=i.getRadiusAxis().getExtent(),s=n.getTicksCoords(),l=n.getMinorTicksCoords(),u=G(n.getViewLabels(),function(f){f=et(f);var h=n.scale,v=h.type==="ordinal"?h.getRawOrdinalNumber(f.tickValue):f.tickValue;return f.coord=n.dataToCoord(v),f});Yx(u),Yx(s),C(CW,function(f){t.get([f,"show"])&&(!n.scale.isBlank()||f==="axisLine")&&MW[f](this.group,t,i,s,l,o,u)},this)}},e.type="angleAxis",e}(mi),MW={axisLine:function(r,e,t,a,n,i){var o=e.getModel(["axisLine","lineStyle"]),s=t.getAngleAxis(),l=Math.PI/180,u=s.getExtent(),f=Bu(t),h=f?0:1,v,c=Math.abs(u[1]-u[0])===360?"Circle":"Arc";i[h]===0?v=new ci[c]({shape:{cx:t.cx,cy:t.cy,r:i[f],startAngle:-u[0]*l,endAngle:-u[1]*l,clockwise:s.inverse},style:o.getLineStyle(),z2:1,silent:!0}):v=new _o({shape:{cx:t.cx,cy:t.cy,r:i[f],r0:i[h]},style:o.getLineStyle(),z2:1,silent:!0}),v.style.fill=null,r.add(v)},axisTick:function(r,e,t,a,n,i){var o=e.getModel("axisTick"),s=(o.get("inside")?-1:1)*o.get("length"),l=i[Bu(t)],u=G(a,function(f){return new Jt({shape:Nu(t,[l,l+s],f.coord)})});r.add(We(u,{style:Q(o.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(r,e,t,a,n,i){if(n.length){for(var o=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(o.get("inside")?-1:1)*s.get("length"),u=i[Bu(t)],f=[],h=0;hy?"left":"right",S=Math.abs(g[1]-m)/d<.3?"middle":g[1]>m?"top":"bottom";if(s&&s[p]){var b=s[p];tt(b)&&b.textStyle&&(c=new Mt(b.textStyle,l,l.ecModel))}var x=new bt({silent:Pe.isLabelSilent(e),style:Nt(c,{x:g[0],y:g[1],fill:c.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:h.formattedLabel,align:_,verticalAlign:S})});if(r.add(x),f){var w=Pe.makeAxisEventDataBase(e);w.targetType="axisLabel",w.value=h.rawLabel,nt(x).eventData=w}},this)},splitLine:function(r,e,t,a,n,i){var o=e.getModel("splitLine"),s=o.getModel("lineStyle"),l=s.get("color"),u=0;l=l instanceof Array?l:[l];for(var f=[],h=0;h=0?"p":"n",R=A;b&&(a[f][L]||(a[f][L]={p:A,n:A}),R=a[f][L][P]);var E=void 0,N=void 0,O=void 0,B=void 0;if(p.dim==="radius"){var F=p.dataToCoord(I)-A,H=l.dataToCoord(L);Math.abs(F)=B})}}})}function OW(r){var e={};C(r,function(a,n){var i=a.getData(),o=a.coordinateSystem,s=o.getBaseAxis(),l=wI(o,s),u=s.getExtent(),f=s.type==="category"?s.getBandWidth():Math.abs(u[1]-u[0])/i.count(),h=e[l]||{bandWidth:f,remainedWidth:f,autoWidthCount:0,categoryGap:"20%",gap:"30%",stacks:{}},v=h.stacks;e[l]=h;var c=bI(a);v[c]||h.autoWidthCount++,v[c]=v[c]||{width:0,maxWidth:0};var p=W(a.get("barWidth"),f),d=W(a.get("barMaxWidth"),f),g=a.get("barGap"),y=a.get("barCategoryGap");p&&!v[c].width&&(p=Math.min(h.remainedWidth,p),v[c].width=p,h.remainedWidth-=p),d&&(v[c].maxWidth=d),g!=null&&(h.gap=g),y!=null&&(h.categoryGap=y)});var t={};return C(e,function(a,n){t[n]={};var i=a.stacks,o=a.bandWidth,s=W(a.categoryGap,o),l=W(a.gap,1),u=a.remainedWidth,f=a.autoWidthCount,h=(u-s)/(f+(f-1)*l);h=Math.max(h,0),C(i,function(d,g){var y=d.maxWidth;y&&y=t.y&&e[1]<=t.y+t.height:a.contain(a.toLocalCoord(e[1]))&&e[0]>=t.y&&e[0]<=t.y+t.height},r.prototype.pointToData=function(e){var t=this.getAxis();return[t.coordToData(t.toLocalCoord(e[t.orient==="horizontal"?0:1]))]},r.prototype.dataToPoint=function(e){var t=this.getAxis(),a=this.getRect(),n=[],i=t.orient==="horizontal"?0:1;return e instanceof Array&&(e=e[0]),n[i]=t.toGlobalCoord(t.dataToCoord(+e)),n[1-i]=i===0?a.y+a.height/2:a.x+a.width/2,n},r.prototype.convertToPixel=function(e,t,a){var n=Xx(t);return n===this?this.dataToPoint(a):null},r.prototype.convertFromPixel=function(e,t,a){var n=Xx(t);return n===this?this.pointToData(a):null},r}();function Xx(r){var e=r.seriesModel,t=r.singleAxisModel;return t&&t.coordinateSystem||e&&e.coordinateSystem}function XW(r,e){var t=[];return r.eachComponent("singleAxis",function(a,n){var i=new YW(a,r,e);i.name="single_"+n,i.resize(a,e),a.coordinateSystem=i,t.push(i)}),r.eachSeries(function(a){if(a.get("coordinateSystem")==="singleAxis"){var n=a.getReferringComponents("singleAxis",Kt).models[0];a.coordinateSystem=n&&n.coordinateSystem}}),t}var $W={create:XW,dimensions:TI},$x=["x","y"],ZW=["width","height"],qW=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.makeElOption=function(t,a,n,i,o){var s=n.axis,l=s.coordinateSystem,u=ep(l,1-th(s)),f=l.dataToPoint(a)[0],h=i.get("type");if(h&&h!=="none"){var v=nm(i),c=KW[h](s,f,u);c.style=v,t.graphicKey=c.type,t.pointer=c}var p=Xd(n);yI(a,t,p,n,i,o)},e.prototype.getHandleTransform=function(t,a,n){var i=Xd(a,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var o=im(a.axis,t,i);return{x:o[0],y:o[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,a,n,i){var o=n.axis,s=o.coordinateSystem,l=th(o),u=ep(s,l),f=[t.x,t.y];f[l]+=a[l],f[l]=Math.min(u[1],f[l]),f[l]=Math.max(u[0],f[l]);var h=ep(s,1-l),v=(h[1]+h[0])/2,c=[v,v];return c[l]=f[l],{x:f[0],y:f[1],rotation:t.rotation,cursorPoint:c,tooltipOption:{verticalAlign:"middle"}}},e}(am),KW={line:function(r,e,t){var a=om([e,t[0]],[e,t[1]],th(r));return{type:"Line",subPixelOptimize:!0,shape:a}},shadow:function(r,e,t){var a=r.getBandWidth(),n=t[1]-t[0];return{type:"Rect",shape:mI([e-a/2,t[0]],[a,n],th(r))}}};function th(r){return r.isHorizontal()?0:1}function ep(r,e){var t=r.getRect();return[t[$x[e]],t[$x[e]]+t[ZW[e]]]}var jW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="single",e}(zt);function QW(r){gt(Gl),mi.registerAxisPointerClass("SingleAxisPointer",qW),r.registerComponentView(jW),r.registerComponentView(HW),r.registerComponentModel(lf),ho(r,"single",lf,lf.defaultOption),r.registerCoordinateSystem("single",$W)}var JW=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t,a,n){var i=Do(t);r.prototype.init.apply(this,arguments),Zx(t,i)},e.prototype.mergeOption=function(t){r.prototype.mergeOption.apply(this,arguments),Zx(this.option,t)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(mt);function Zx(r,e){var t=r.cellSize,a;z(t)?a=t:a=r.cellSize=[t,t],a.length===1&&(a[1]=a[0]);var n=G([0,1],function(i){return gE(e,i)&&(a[i]="auto"),a[i]!=null&&a[i]!=="auto"});Ja(r,e,{type:"box",ignoreSize:n})}var t6=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){var i=this.group;i.removeAll();var o=t.coordinateSystem,s=o.getRangeInfo(),l=o.getOrient(),u=a.getLocaleModel();this._renderDayRect(t,s,i),this._renderLines(t,s,l,i),this._renderYearText(t,s,l,i),this._renderMonthText(t,u,l,i),this._renderWeekText(t,u,s,l,i)},e.prototype._renderDayRect=function(t,a,n){for(var i=t.coordinateSystem,o=t.getModel("itemStyle").getItemStyle(),s=i.getCellWidth(),l=i.getCellHeight(),u=a.start.time;u<=a.end.time;u=i.getNextNDay(u,1).time){var f=i.dataToRect([u],!1).tl,h=new wt({shape:{x:f[0],y:f[1],width:s,height:l},cursor:"default",style:o});n.add(h)}},e.prototype._renderLines=function(t,a,n,i){var o=this,s=t.coordinateSystem,l=t.getModel(["splitLine","lineStyle"]).getLineStyle(),u=t.get(["splitLine","show"]),f=l.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var h=a.start,v=0;h.time<=a.end.time;v++){p(h.formatedDate),v===0&&(h=s.getDateInfo(a.start.y+"-"+a.start.m));var c=h.date;c.setMonth(c.getMonth()+1),h=s.getDateInfo(c)}p(s.getNextNDay(a.end.time,1).formatedDate);function p(d){o._firstDayOfMonth.push(s.getDateInfo(d)),o._firstDayPoints.push(s.dataToRect([d],!1).tl);var g=o._getLinePointsOfOneWeek(t,d,n);o._tlpoints.push(g[0]),o._blpoints.push(g[g.length-1]),u&&o._drawSplitline(g,l,i)}u&&this._drawSplitline(o._getEdgesPoints(o._tlpoints,f,n),l,i),u&&this._drawSplitline(o._getEdgesPoints(o._blpoints,f,n),l,i)},e.prototype._getEdgesPoints=function(t,a,n){var i=[t[0].slice(),t[t.length-1].slice()],o=n==="horizontal"?0:1;return i[0][o]=i[0][o]-a/2,i[1][o]=i[1][o]+a/2,i},e.prototype._drawSplitline=function(t,a,n){var i=new be({z2:20,shape:{points:t},style:a});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,a,n){for(var i=t.coordinateSystem,o=i.getDateInfo(a),s=[],l=0;l<7;l++){var u=i.getNextNDay(o.time,l),f=i.dataToRect([u.time],!1);s[2*u.day]=f.tl,s[2*u.day+1]=f[n==="horizontal"?"bl":"tr"]}return s},e.prototype._formatterLabel=function(t,a){return U(t)&&t?vE(t,a):K(t)?t(a):a.nameMap},e.prototype._yearTextPositionControl=function(t,a,n,i,o){var s=a[0],l=a[1],u=["center","bottom"];i==="bottom"?(l+=o,u=["center","top"]):i==="left"?s-=o:i==="right"?(s+=o,u=["center","top"]):l-=o;var f=0;return(i==="left"||i==="right")&&(f=Math.PI/2),{rotation:f,x:s,y:l,style:{align:u[0],verticalAlign:u[1]}}},e.prototype._renderYearText=function(t,a,n,i){var o=t.getModel("yearLabel");if(o.get("show")){var s=o.get("margin"),l=o.get("position");l||(l=n!=="horizontal"?"top":"left");var u=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],f=(u[0][0]+u[1][0])/2,h=(u[0][1]+u[1][1])/2,v=n==="horizontal"?0:1,c={top:[f,u[v][1]],bottom:[f,u[1-v][1]],left:[u[1-v][0],h],right:[u[v][0],h]},p=a.start.y;+a.end.y>+a.start.y&&(p=p+"-"+a.end.y);var d=o.get("formatter"),g={start:a.start.y,end:a.end.y,nameMap:p},y=this._formatterLabel(d,g),m=new bt({z2:30,style:Nt(o,{text:y})});m.attr(this._yearTextPositionControl(m,c[l],n,l,s)),i.add(m)}},e.prototype._monthTextPositionControl=function(t,a,n,i,o){var s="left",l="top",u=t[0],f=t[1];return n==="horizontal"?(f=f+o,a&&(s="center"),i==="start"&&(l="bottom")):(u=u+o,a&&(l="middle"),i==="start"&&(s="right")),{x:u,y:f,align:s,verticalAlign:l}},e.prototype._renderMonthText=function(t,a,n,i){var o=t.getModel("monthLabel");if(o.get("show")){var s=o.get("nameMap"),l=o.get("margin"),u=o.get("position"),f=o.get("align"),h=[this._tlpoints,this._blpoints];(!s||U(s))&&(s&&(a=td(s)||a),s=a.get(["time","monthAbbr"])||[]);var v=u==="start"?0:1,c=n==="horizontal"?0:1;l=u==="start"?-l:l;for(var p=f==="center",d=0;d=n.start.time&&a.times.end.time&&t.reverse(),t},r.prototype._getRangeInfo=function(e){var t=[this.getDateInfo(e[0]),this.getDateInfo(e[1])],a;t[0].time>t[1].time&&(a=!0,t.reverse());var n=Math.floor(t[1].time/rp)-Math.floor(t[0].time/rp)+1,i=new Date(t[0].time),o=i.getDate(),s=t[1].date.getDate();i.setDate(o+n-1);var l=i.getDate();if(l!==s)for(var u=i.getTime()-t[1].time>0?1:-1;(l=i.getDate())!==s&&(i.getTime()-t[1].time)*u>0;)n-=u,i.setDate(l-u);var f=Math.floor((n+t[0].day+6)/7),h=a?-f+1:f-1;return a&&t.reverse(),{range:[t[0].formatedDate,t[1].formatedDate],start:t[0],end:t[1],allDay:n,weeks:f,nthWeek:h,fweek:t[0].day,lweek:t[1].day}},r.prototype._getDateByWeeksAndDay=function(e,t,a){var n=this._getRangeInfo(a);if(e>n.weeks||e===0&&tn.lweek)return null;var i=(e-1)*7-n.fweek+t,o=new Date(n.start.time);return o.setDate(+n.start.d+i),this.getDateInfo(o)},r.create=function(e,t){var a=[];return e.eachComponent("calendar",function(n){var i=new r(n);a.push(i),n.coordinateSystem=i}),e.eachSeries(function(n){n.get("coordinateSystem")==="calendar"&&(n.coordinateSystem=a[n.get("calendarIndex")||0])}),a},r.dimensions=["time","value"],r}();function qx(r){var e=r.calendarModel,t=r.seriesModel,a=e?e.coordinateSystem:t?t.coordinateSystem:null;return a}function r6(r){r.registerComponentModel(JW),r.registerComponentView(t6),r.registerCoordinateSystem("calendar",e6)}function a6(r,e){var t=r.existing;if(e.id=r.keyInfo.id,!e.type&&t&&(e.type=t.type),e.parentId==null){var a=e.parentOption;a?e.parentId=a.id:t&&(e.parentId=t.parentId)}e.parentOption=null}function Kx(r,e){var t;return C(e,function(a){r[a]!=null&&r[a]!=="auto"&&(t=!0)}),t}function n6(r,e,t){var a=V({},t),n=r[e],i=t.$action||"merge";i==="merge"?n?(st(n,a,!0),Ja(n,a,{ignoreSize:!0}),gA(t,n),Vu(t,n),Vu(t,n,"shape"),Vu(t,n,"style"),Vu(t,n,"extra"),t.clipPath=n.clipPath):r[e]=a:i==="replace"?r[e]=a:i==="remove"&&n&&(r[e]=null)}var AI=["transition","enterFrom","leaveTo"],i6=AI.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function Vu(r,e,t){if(t&&(!r[t]&&e[t]&&(r[t]={}),r=r[t],e=e[t]),!(!r||!e))for(var a=t?AI:i6,n=0;n=0;f--){var h=n[f],v=Qt(h.id,null),c=v!=null?o.get(v):null;if(c){var p=c.parent,y=rr(p),m=p===i?{width:s,height:l}:{width:y.width,height:y.height},_={},S=kh(c,h,m,null,{hv:h.hv,boundingMode:h.bounding},_);if(!rr(c).isNew&&S){for(var b=h.transition,x={},w=0;w=0)?x[T]=A:c[T]=A}Dt(c,x,t,0)}else c.attr(_)}}},e.prototype._clear=function(){var t=this,a=this._elMap;a.each(function(n){uf(n,rr(n).option,a,t._lastGraphicModel)}),this._elMap=Z()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(zt);function $d(r){var e=$(jx,r)?jx[r]:Dh(r),t=new e({});return rr(t).type=r,t}function Qx(r,e,t,a){var n=$d(t);return e.add(n),a.set(r,n),rr(n).id=r,rr(n).isNew=!0,n}function uf(r,e,t,a){var n=r&&r.parent;n&&(r.type==="group"&&r.traverse(function(i){uf(i,e,t,a)}),qh(r,e,a),t.removeKey(rr(r).id))}function Jx(r,e,t,a){r.isGroup||C([["cursor",ur.prototype.cursor],["zlevel",a||0],["z",t||0],["z2",0]],function(n){var i=n[0];$(e,i)?r[i]=ot(e[i],n[1]):r[i]==null&&(r[i]=n[1])}),C(St(e),function(n){if(n.indexOf("on")===0){var i=e[n];r[n]=K(i)?i:null}}),$(e,"draggable")&&(r.draggable=e.draggable),e.name!=null&&(r.name=e.name),e.id!=null&&(r.id=e.id)}function u6(r){return r=V({},r),C(["id","parentId","$action","hv","bounding","textContent","clipPath"].concat(dA),function(e){delete r[e]}),r}function f6(r,e,t){var a=nt(r).eventData;!r.silent&&!r.ignore&&!a&&(a=nt(r).eventData={componentType:"graphic",componentIndex:e.componentIndex,name:r.name}),a&&(a.info=t.info)}function h6(r){r.registerComponentModel(s6),r.registerComponentView(l6),r.registerPreprocessor(function(e){var t=e.graphic;z(t)?!t[0]||!t[0].elements?e.graphic=[{elements:t}]:e.graphic=[e.graphic[0]]:t&&!t.elements&&(e.graphic=[{elements:[t]}])})}var tb=["x","y","radius","angle","single"],v6=["cartesian2d","polar","singleAxis"];function c6(r){var e=r.get("coordinateSystem");return vt(v6,e)>=0}function Ha(r){return r+"Axis"}function p6(r,e){var t=Z(),a=[],n=Z();r.eachComponent({mainType:"dataZoom",query:e},function(f){n.get(f.uid)||s(f)});var i;do i=!1,r.eachComponent("dataZoom",o);while(i);function o(f){!n.get(f.uid)&&l(f)&&(s(f),i=!0)}function s(f){n.set(f.uid,!0),a.push(f),u(f)}function l(f){var h=!1;return f.eachTargetAxis(function(v,c){var p=t.get(v);p&&p[c]&&(h=!0)}),h}function u(f){f.eachTargetAxis(function(h,v){(t.get(h)||t.set(h,[]))[v]=!0})}return a}function CI(r){var e=r.ecModel,t={infoList:[],infoMap:Z()};return r.eachTargetAxis(function(a,n){var i=e.getComponent(Ha(a),n);if(i){var o=i.getCoordSysModel();if(o){var s=o.uid,l=t.infoMap.get(s);l||(l={model:o,axisModels:[]},t.infoList.push(l),t.infoMap.set(s,l)),l.axisModels.push(i)}}}),t}var ap=function(){function r(){this.indexList=[],this.indexMap=[]}return r.prototype.add=function(e){this.indexMap[e]||(this.indexList.push(e),this.indexMap[e]=!0)},r}(),dl=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t._autoThrottle=!0,t._noTarget=!0,t._rangePropMode=["percent","percent"],t}return e.prototype.init=function(t,a,n){var i=eb(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var a=eb(t);st(this.option,t,!0),st(this.settledOption,a,!0),this._doInit(a)},e.prototype._doInit=function(t){var a=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;C([["start","startValue"],["end","endValue"]],function(i,o){this._rangePropMode[o]==="value"&&(a[i[0]]=n[i[0]]=null)},this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),a=this._targetAxisInfoMap=Z(),n=this._fillSpecifiedTargetAxis(a);n?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(a,this._orient)),this._noTarget=!0,a.each(function(i){i.indexList.length&&(this._noTarget=!1)},this)},e.prototype._fillSpecifiedTargetAxis=function(t){var a=!1;return C(tb,function(n){var i=this.getReferringComponents(Ha(n),iP);if(i.specified){a=!0;var o=new ap;C(i.models,function(s){o.add(s.componentIndex)}),t.set(n,o)}},this),a},e.prototype._fillAutoTargetAxisByOrient=function(t,a){var n=this.ecModel,i=!0;if(i){var o=a==="vertical"?"y":"x",s=n.findComponents({mainType:o+"Axis"});l(s,o)}if(i){var s=n.findComponents({mainType:"singleAxis",filter:function(f){return f.get("orient",!0)===a}});l(s,"single")}function l(u,f){var h=u[0];if(h){var v=new ap;if(v.add(h.componentIndex),t.set(f,v),i=!1,f==="x"||f==="y"){var c=h.getReferringComponents("grid",Kt).models[0];c&&C(u,function(p){h.componentIndex!==p.componentIndex&&c===p.getReferringComponents("grid",Kt).models[0]&&v.add(p.componentIndex)})}}}i&&C(tb,function(u){if(i){var f=n.findComponents({mainType:Ha(u),filter:function(v){return v.get("type",!0)==="category"}});if(f[0]){var h=new ap;h.add(f[0].componentIndex),t.set(u,h),i=!1}}},this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis(function(a){!t&&(t=a)},this),t==="y"?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var a=this.ecModel.option;this.option.throttle=a.animation&&a.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var a=this._rangePropMode,n=this.get("rangeMode");C([["start","startValue"],["end","endValue"]],function(i,o){var s=t[i[0]]!=null,l=t[i[1]]!=null;s&&!l?a[o]="percent":!s&&l?a[o]="value":n?a[o]=n[o]:s&&(a[o]="percent")})},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis(function(a,n){t==null&&(t=this.ecModel.getComponent(Ha(a),n))},this),t},e.prototype.eachTargetAxis=function(t,a){this._targetAxisInfoMap.each(function(n,i){C(n.indexList,function(o){t.call(a,i,o)})})},e.prototype.getAxisProxy=function(t,a){var n=this.getAxisModel(t,a);if(n)return n.__dzAxisProxy},e.prototype.getAxisModel=function(t,a){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[a])return this.ecModel.getComponent(Ha(t),a)},e.prototype.setRawRange=function(t){var a=this.option,n=this.settledOption;C([["start","startValue"],["end","endValue"]],function(i){(t[i[0]]!=null||t[i[1]]!=null)&&(a[i[0]]=n[i[0]]=t[i[0]],a[i[1]]=n[i[1]]=t[i[1]])},this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var a=this.option;C(["start","startValue","end","endValue"],function(n){a[n]=t[n]})},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},e.prototype.getValueRange=function(t,a){if(t==null&&a==null){var n=this.findRepresentativeAxisProxy();if(n)return n.getDataValueWindow()}else return this.getAxisProxy(t,a).getDataValueWindow()},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return t.__dzAxisProxy;for(var a,n=this._targetAxisInfoMap.keys(),i=0;io[1];if(_&&!S&&!b)return!0;_&&(g=!0),S&&(p=!0),b&&(d=!0)}return g&&p&&d})}else Ui(f,function(c){if(i==="empty")l.setData(u=u.map(c,function(d){return s(d)?d:NaN}));else{var p={};p[c]=o,u.selectRange(p)}});Ui(f,function(c){u.setApproximateExtent(o,c)})}});function s(l){return l>=o[0]&&l<=o[1]}},r.prototype._updateMinMaxSpan=function(){var e=this._minMaxSpan={},t=this._dataZoomModel,a=this._dataExtent;Ui(["min","max"],function(n){var i=t.get(n+"Span"),o=t.get(n+"ValueSpan");o!=null&&(o=this.getAxisModel().axis.scale.parse(o)),o!=null?i=Lt(a[0]+o,a,[0,100],!0):i!=null&&(o=Lt(i,[0,100],a,!0)-a[0]),e[n+"Span"]=i,e[n+"ValueSpan"]=o},this)},r.prototype._setAxisModel=function(){var e=this.getAxisModel(),t=this._percentWindow,a=this._valueWindow;if(t){var n=_g(a,[0,500]);n=Math.min(n,20);var i=e.axis.scale.rawExtentInfo;t[0]!==0&&i.setDeterminedMinMax("min",+a[0].toFixed(n)),t[1]!==100&&i.setDeterminedMinMax("max",+a[1].toFixed(n)),i.freeze()}},r}();function m6(r,e,t){var a=[1/0,-1/0];Ui(t,function(o){XN(a,o.getData(),e)});var n=r.getAxisModel(),i=UC(n.axis.scale,n,a).calculate();return[i.min,i.max]}var _6={getTargetSeries:function(r){function e(n){r.eachComponent("dataZoom",function(i){i.eachTargetAxis(function(o,s){var l=r.getComponent(Ha(o),s);n(o,s,l,i)})})}e(function(n,i,o,s){o.__dzAxisProxy=null});var t=[];e(function(n,i,o,s){o.__dzAxisProxy||(o.__dzAxisProxy=new y6(n,i,s,r),t.push(o.__dzAxisProxy))});var a=Z();return C(t,function(n){C(n.getTargetSeriesModels(),function(i){a.set(i.uid,i)})}),a},overallReset:function(r,e){r.eachComponent("dataZoom",function(t){t.eachTargetAxis(function(a,n){t.getAxisProxy(a,n).reset(t)}),t.eachTargetAxis(function(a,n){t.getAxisProxy(a,n).filterData(t,e)})}),r.eachComponent("dataZoom",function(t){var a=t.findRepresentativeAxisProxy();if(a){var n=a.getDataPercentWindow(),i=a.getDataValueWindow();t.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}})}};function S6(r){r.registerAction("dataZoom",function(e,t){var a=p6(t,e);C(a,function(n){n.setRawRange({start:e.start,end:e.end,startValue:e.startValue,endValue:e.endValue})})})}var ab=!1;function hm(r){ab||(ab=!0,r.registerProcessor(r.PRIORITY.PROCESSOR.FILTER,_6),S6(r),r.registerSubTypeDefaulter("dataZoom",function(){return"slider"}))}function x6(r){r.registerComponentModel(d6),r.registerComponentView(g6),hm(r)}var nr=function(){function r(){}return r}(),DI={};function Yi(r,e){DI[r]=e}function MI(r){return DI[r]}var b6=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.optionUpdated=function(){r.prototype.optionUpdated.apply(this,arguments);var t=this.ecModel;C(this.option.feature,function(a,n){var i=MI(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(t)),st(a,i.defaultOption))})},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}},e}(mt);function w6(r,e,t){var a=e.getBoxLayoutParams(),n=e.get("padding"),i={width:t.getWidth(),height:t.getHeight()},o=jt(a,i,n);Qn(e.get("orient"),r,e.get("itemGap"),o.width,o.height),kh(r,a,i,n)}function II(r,e){var t=pi(e.get("padding")),a=e.getItemStyle(["color","opacity"]);return a.fill=e.get("backgroundColor"),r=new wt({shape:{x:r.x-t[3],y:r.y-t[0],width:r.width+t[1]+t[3],height:r.height+t[0]+t[2],r:e.get("borderRadius")},style:a,silent:!0,z2:-1}),r}var T6=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.render=function(t,a,n,i){var o=this.group;if(o.removeAll(),!t.get("show"))return;var s=+t.get("itemSize"),l=t.get("orient")==="vertical",u=t.get("feature")||{},f=this._features||(this._features={}),h=[];C(u,function(p,d){h.push(d)}),new ya(this._featureNames||[],h).add(v).update(v).remove(it(v,null)).execute(),this._featureNames=h;function v(p,d){var g=h[p],y=h[d],m=u[g],_=new Mt(m,t,t.ecModel),S;if(i&&i.newTitle!=null&&i.featureName===g&&(m.title=i.newTitle),g&&!y){if(A6(g))S={onclick:_.option.onclick,featureName:g};else{var b=MI(g);if(!b)return;S=new b}f[g]=S}else if(S=f[y],!S)return;S.uid=Co("toolbox-feature"),S.model=_,S.ecModel=a,S.api=n;var x=S instanceof nr;if(!g&&y){x&&S.dispose&&S.dispose(a,n);return}if(!_.get("show")||x&&S.unusable){x&&S.remove&&S.remove(a,n);return}c(_,S,g),_.setIconStatus=function(w,T){var A=this.option,D=this.iconPaths;A.iconStatus=A.iconStatus||{},A.iconStatus[w]=T,D[w]&&(T==="emphasis"?da:ga)(D[w])},S instanceof nr&&S.render&&S.render(_,a,n,i)}function c(p,d,g){var y=p.getModel("iconStyle"),m=p.getModel(["emphasis","iconStyle"]),_=d instanceof nr&&d.getIcons?d.getIcons():p.get("icon"),S=p.get("title")||{},b,x;U(_)?(b={},b[g]=_):b=_,U(S)?(x={},x[g]=S):x=S;var w=p.iconPaths={};C(b,function(T,A){var D=wo(T,{},{x:-s/2,y:-s/2,width:s,height:s});D.setStyle(y.getItemStyle());var M=D.ensureState("emphasis");M.style=m.getItemStyle();var I=new bt({style:{text:x[A],align:m.get("textAlign"),borderRadius:m.get("textBorderRadius"),padding:m.get("textPadding"),fill:null,font:zg({fontStyle:m.get("textFontStyle"),fontFamily:m.get("textFontFamily"),fontSize:m.get("textFontSize"),fontWeight:m.get("textFontWeight")},a)},ignore:!0});D.setTextContent(I),To({el:D,componentModel:t,itemName:A,formatterParamsExtra:{title:x[A]}}),D.__title=x[A],D.on("mouseover",function(){var L=m.getItemStyle(),P=l?t.get("right")==null&&t.get("left")!=="right"?"right":"left":t.get("bottom")==null&&t.get("top")!=="bottom"?"bottom":"top";I.setStyle({fill:m.get("textFill")||L.fill||L.stroke||"#000",backgroundColor:m.get("textBackgroundColor")}),D.setTextConfig({position:m.get("textPosition")||P}),I.ignore=!t.get("showTitle"),n.enterEmphasis(this)}).on("mouseout",function(){p.get(["iconStatus",A])!=="emphasis"&&n.leaveEmphasis(this),I.hide()}),(p.get(["iconStatus",A])==="emphasis"?da:ga)(D),o.add(D),D.on("click",X(d.onclick,d,a,n,A)),w[A]=D})}w6(o,t,n),o.add(II(o.getBoundingRect(),t)),l||o.eachChild(function(p){var d=p.__title,g=p.ensureState("emphasis"),y=g.textConfig||(g.textConfig={}),m=p.getTextContent(),_=m&&m.ensureState("emphasis");if(_&&!K(_)&&d){var S=_.style||(_.style={}),b=bl(d,bt.makeFont(S)),x=p.x+o.x,w=p.y+o.y+s,T=!1;w+b.height>n.getHeight()&&(y.position="top",T=!0);var A=T?-5-b.height:s+10;x+b.width/2>n.getWidth()?(y.position=["100%",A],S.align="right"):x-b.width/2<0&&(y.position=[0,A],S.align="left")}})},e.prototype.updateView=function(t,a,n,i){C(this._features,function(o){o instanceof nr&&o.updateView&&o.updateView(o.model,a,n,i)})},e.prototype.remove=function(t,a){C(this._features,function(n){n instanceof nr&&n.remove&&n.remove(t,a)}),this.group.removeAll()},e.prototype.dispose=function(t,a){C(this._features,function(n){n instanceof nr&&n.dispose&&n.dispose(t,a)})},e.type="toolbox",e}(zt);function A6(r){return r.indexOf("my")===0}var C6=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.onclick=function(t,a){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o=a.getZr().painter.getType()==="svg",s=o?"svg":n.get("type",!0)||"png",l=a.getConnectedDataURL({type:s,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),u=_t.browser;if(typeof MouseEvent=="function"&&(u.newEdge||!u.ie&&!u.edge)){var f=document.createElement("a");f.download=i+"."+s,f.target="_blank",f.href=l;var h=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});f.dispatchEvent(h)}else if(window.navigator.msSaveOrOpenBlob||o){var v=l.split(","),c=v[0].indexOf("base64")>-1,p=o?decodeURIComponent(v[1]):v[1];c&&(p=window.atob(p));var d=i+"."+s;if(window.navigator.msSaveOrOpenBlob){for(var g=p.length,y=new Uint8Array(g);g--;)y[g]=p.charCodeAt(g);var m=new Blob([y]);window.navigator.msSaveOrOpenBlob(m,d)}else{var _=document.createElement("iframe");document.body.appendChild(_);var S=_.contentWindow,b=S.document;b.open("image/svg+xml","replace"),b.write(p),b.close(),S.focus(),b.execCommand("SaveAs",!0,d),document.body.removeChild(_)}}else{var x=n.get("lang"),w='',T=window.open();T.document.write(w),T.document.title=i}},e.getDefaultOption=function(t){var a={show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])};return a},e}(nr),nb="__ec_magicType_stack__",D6=[["line","bar"],["stack"]],M6=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.getIcons=function(){var t=this.model,a=t.get("icon"),n={};return C(t.get("type"),function(i){a[i]&&(n[i]=a[i])}),n},e.getDefaultOption=function(t){var a={show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}};return a},e.prototype.onclick=function(t,a,n){var i=this.model,o=i.get(["seriesIndex",n]);if(ib[n]){var s={series:[]},l=function(h){var v=h.subType,c=h.id,p=ib[n](v,c,h,i);p&&(Q(p,h.option),s.series.push(p));var d=h.coordinateSystem;if(d&&d.type==="cartesian2d"&&(n==="line"||n==="bar")){var g=d.getAxesByScale("ordinal")[0];if(g){var y=g.dim,m=y+"Axis",_=h.getReferringComponents(m,Kt).models[0],S=_.componentIndex;s[m]=s[m]||[];for(var b=0;b<=S;b++)s[m][S]=s[m][S]||{};s[m][S].boundaryGap=n==="bar"}}};C(D6,function(h){vt(h,n)>=0&&C(h,function(v){i.setIconStatus(v,"normal")})}),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:o==null?null:{seriesIndex:o}},l);var u,f=n;n==="stack"&&(u=st({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),i.get(["iconStatus",n])!=="emphasis"&&(f="tiled")),a.dispatchAction({type:"changeMagicType",currentType:f,newOption:s,newTitle:u,featureName:"magicType"})}},e}(nr),ib={line:function(r,e,t,a){if(r==="bar")return st({id:e,type:"line",data:t.get("data"),stack:t.get("stack"),markPoint:t.get("markPoint"),markLine:t.get("markLine")},a.get(["option","line"])||{},!0)},bar:function(r,e,t,a){if(r==="line")return st({id:e,type:"bar",data:t.get("data"),stack:t.get("stack"),markPoint:t.get("markPoint"),markLine:t.get("markLine")},a.get(["option","bar"])||{},!0)},stack:function(r,e,t,a){var n=t.get("stack")===nb;if(r==="line"||r==="bar")return a.setIconStatus("stack",n?"normal":"emphasis"),st({id:e,stack:n?"":nb},a.get(["option","stack"])||{},!0)}};Qr({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},function(r,e){e.mergeOption(r.newOption)});var Kh=new Array(60).join("-"),po=" ";function I6(r){var e={},t=[],a=[];return r.eachRawSeries(function(n){var i=n.coordinateSystem;if(i&&(i.type==="cartesian2d"||i.type==="polar")){var o=i.getBaseAxis();if(o.type==="category"){var s=o.dim+"_"+o.index;e[s]||(e[s]={categoryAxis:o,valueAxis:i.getOtherAxis(o),series:[]},a.push({axisDim:o.dim,axisIndex:o.index})),e[s].series.push(n)}else t.push(n)}else t.push(n)}),{seriesGroupByCategoryAxis:e,other:t,meta:a}}function L6(r){var e=[];return C(r,function(t,a){var n=t.categoryAxis,i=t.valueAxis,o=i.dim,s=[" "].concat(G(t.series,function(c){return c.name})),l=[n.model.getCategories()];C(t.series,function(c){var p=c.getRawData();l.push(c.getRawData().mapArray(p.mapDimension(o),function(d){return d}))});for(var u=[s.join(po)],f=0;f=0)return!0}var Zd=new RegExp("["+po+"]+","g");function k6(r){for(var e=r.split(/\n+/g),t=eh(e.shift()).split(Zd),a=[],n=G(t,function(l){return{name:l,data:[]}}),i=0;i=0;i--){var o=t[i];if(o[n])break}if(i<0){var s=r.queryComponents({mainType:"dataZoom",subType:"select",id:n})[0];if(s){var l=s.getPercentRange();t[0][n]={dataZoomId:n,start:l[0],end:l[1]}}}}),t.push(e)}function G6(r){var e=vm(r),t=e[e.length-1];e.length>1&&e.pop();var a={};return LI(t,function(n,i){for(var o=e.length-1;o>=0;o--)if(n=e[o][i],n){a[i]=n;break}}),a}function F6(r){PI(r).snapshots=null}function H6(r){return vm(r).length}function vm(r){var e=PI(r);return e.snapshots||(e.snapshots=[{}]),e.snapshots}var W6=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.onclick=function(t,a){F6(t),a.dispatchAction({type:"restore",from:this.uid})},e.getDefaultOption=function(t){var a={show:!0,icon:"M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5",title:t.getLocaleModel().get(["toolbox","restore","title"])};return a},e}(nr);Qr({type:"restore",event:"restore",update:"prepareAndUpdate"},function(r,e){e.resetOption("recreate")});var U6=["grid","xAxis","yAxis","geo","graph","polar","radiusAxis","angleAxis","bmap"],cm=function(){function r(e,t,a){var n=this;this._targetInfoList=[];var i=ob(t,e);C(Y6,function(o,s){(!a||!a.include||vt(a.include,s)>=0)&&o(i,n._targetInfoList)})}return r.prototype.setOutputRanges=function(e,t){return this.matchOutputRanges(e,t,function(a,n,i){if((a.coordRanges||(a.coordRanges=[])).push(n),!a.coordRange){a.coordRange=n;var o=np[a.brushType](0,i,n);a.__rangeOffset={offset:fb[a.brushType](o.values,a.range,[1,1]),xyMinMax:o.xyMinMax}}}),e},r.prototype.matchOutputRanges=function(e,t,a){C(e,function(n){var i=this.findTargetInfo(n,t);i&&i!==!0&&C(i.coordSyses,function(o){var s=np[n.brushType](1,o,n.range,!0);a(n,s.values,o,t)})},this)},r.prototype.setInputRanges=function(e,t){C(e,function(a){var n=this.findTargetInfo(a,t);if(a.range=a.range||[],n&&n!==!0){a.panelId=n.panelId;var i=np[a.brushType](0,n.coordSys,a.coordRange),o=a.__rangeOffset;a.range=o?fb[a.brushType](i.values,o.offset,X6(i.xyMinMax,o.xyMinMax)):i.values}},this)},r.prototype.makePanelOpts=function(e,t){return G(this._targetInfoList,function(a){var n=a.getPanelRect();return{panelId:a.panelId,defaultBrushType:t?t(a):null,clipPath:zM(n),isTargetByCursor:FM(n,e,a.coordSysModel),getLinearBrushOtherExtent:GM(n)}})},r.prototype.controlSeries=function(e,t,a){var n=this.findTargetInfo(e,a);return n===!0||n&&vt(n.coordSyses,t.coordinateSystem)>=0},r.prototype.findTargetInfo=function(e,t){for(var a=this._targetInfoList,n=ob(t,e),i=0;ir[1]&&r.reverse(),r}function ob(r,e){return Ps(r,e,{includeMainTypes:U6})}var Y6={grid:function(r,e){var t=r.xAxisModels,a=r.yAxisModels,n=r.gridModels,i=Z(),o={},s={};!t&&!a&&!n||(C(t,function(l){var u=l.axis.grid.model;i.set(u.id,u),o[u.id]=!0}),C(a,function(l){var u=l.axis.grid.model;i.set(u.id,u),s[u.id]=!0}),C(n,function(l){i.set(l.id,l),o[l.id]=!0,s[l.id]=!0}),i.each(function(l){var u=l.coordinateSystem,f=[];C(u.getCartesians(),function(h,v){(vt(t,h.getAxis("x").model)>=0||vt(a,h.getAxis("y").model)>=0)&&f.push(h)}),e.push({panelId:"grid--"+l.id,gridModel:l,coordSysModel:l,coordSys:f[0],coordSyses:f,getPanelRect:lb.grid,xAxisDeclared:o[l.id],yAxisDeclared:s[l.id]})}))},geo:function(r,e){C(r.geoModels,function(t){var a=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:a,coordSyses:[a],getPanelRect:lb.geo})})}},sb=[function(r,e){var t=r.xAxisModel,a=r.yAxisModel,n=r.gridModel;return!n&&t&&(n=t.axis.grid.model),!n&&a&&(n=a.axis.grid.model),n&&n===e.gridModel},function(r,e){var t=r.geoModel;return t&&t===e.geoModel}],lb={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var r=this.coordSys,e=r.getBoundingRect().clone();return e.applyTransform(Xa(r)),e}},np={lineX:it(ub,0),lineY:it(ub,1),rect:function(r,e,t,a){var n=r?e.pointToData([t[0][0],t[1][0]],a):e.dataToPoint([t[0][0],t[1][0]],a),i=r?e.pointToData([t[0][1],t[1][1]],a):e.dataToPoint([t[0][1],t[1][1]],a),o=[qd([n[0],i[0]]),qd([n[1],i[1]])];return{values:o,xyMinMax:o}},polygon:function(r,e,t,a){var n=[[1/0,-1/0],[1/0,-1/0]],i=G(t,function(o){var s=r?e.pointToData(o,a):e.dataToPoint(o,a);return n[0][0]=Math.min(n[0][0],s[0]),n[1][0]=Math.min(n[1][0],s[1]),n[0][1]=Math.max(n[0][1],s[0]),n[1][1]=Math.max(n[1][1],s[1]),s});return{values:i,xyMinMax:n}}};function ub(r,e,t,a){var n=t.getAxis(["x","y"][r]),i=qd(G([0,1],function(s){return e?n.coordToData(n.toLocalCoord(a[s]),!0):n.toGlobalCoord(n.dataToCoord(a[s]))})),o=[];return o[r]=i,o[1-r]=[NaN,NaN],{values:i,xyMinMax:o}}var fb={lineX:it(hb,0),lineY:it(hb,1),rect:function(r,e,t){return[[r[0][0]-t[0]*e[0][0],r[0][1]-t[0]*e[0][1]],[r[1][0]-t[1]*e[1][0],r[1][1]-t[1]*e[1][1]]]},polygon:function(r,e,t){return G(r,function(a,n){return[a[0]-t[0]*e[n][0],a[1]-t[1]*e[n][1]]})}};function hb(r,e,t,a){return[e[0]-a[r]*t[0],e[1]-a[r]*t[1]]}function X6(r,e){var t=vb(r),a=vb(e),n=[t[0]/a[0],t[1]/a[1]];return isNaN(n[0])&&(n[0]=1),isNaN(n[1])&&(n[1]=1),n}function vb(r){return r?[r[0][1]-r[0][0],r[1][1]-r[1][0]]:[NaN,NaN]}var Kd=C,$6=tP("toolbox-dataZoom_"),Z6=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.render=function(t,a,n,i){this._brushController||(this._brushController=new Hy(n.getZr()),this._brushController.on("brush",X(this._onBrush,this)).mount()),j6(t,a,this,i,n),K6(t,a)},e.prototype.onclick=function(t,a,n){q6[n].call(this)},e.prototype.remove=function(t,a){this._brushController&&this._brushController.unmount()},e.prototype.dispose=function(t,a){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var a=t.areas;if(!t.isEnd||!a.length)return;var n={},i=this.ecModel;this._brushController.updateCovers([]);var o=new cm(pm(this.model),i,{include:["grid"]});o.matchOutputRanges(a,i,function(u,f,h){if(h.type==="cartesian2d"){var v=u.brushType;v==="rect"?(s("x",h,f[0]),s("y",h,f[1])):s({lineX:"x",lineY:"y"}[v],h,f)}}),z6(i,n),this._dispatchZoomAction(n);function s(u,f,h){var v=f.getAxis(u),c=v.model,p=l(u,c,i),d=p.findRepresentativeAxisProxy(c).getMinMaxSpan();(d.minValueSpan!=null||d.maxValueSpan!=null)&&(h=_i(0,h.slice(),v.scale.getExtent(),0,d.minValueSpan,d.maxValueSpan)),p&&(n[p.id]={dataZoomId:p.id,startValue:h[0],endValue:h[1]})}function l(u,f,h){var v;return h.eachComponent({mainType:"dataZoom",subType:"select"},function(c){var p=c.getAxisModel(u,f.componentIndex);p&&(v=c)}),v}},e.prototype._dispatchZoomAction=function(t){var a=[];Kd(t,function(n,i){a.push(et(n))}),a.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:a})},e.getDefaultOption=function(t){var a={show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}};return a},e}(nr),q6={zoom:function(){var r=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:r})},back:function(){this._dispatchZoomAction(G6(this.ecModel))}};function pm(r){var e={xAxisIndex:r.get("xAxisIndex",!0),yAxisIndex:r.get("yAxisIndex",!0),xAxisId:r.get("xAxisId",!0),yAxisId:r.get("yAxisId",!0)};return e.xAxisIndex==null&&e.xAxisId==null&&(e.xAxisIndex="all"),e.yAxisIndex==null&&e.yAxisId==null&&(e.yAxisIndex="all"),e}function K6(r,e){r.setIconStatus("back",H6(e)>1?"emphasis":"normal")}function j6(r,e,t,a,n){var i=t._isZoomActive;a&&a.type==="takeGlobalCursor"&&(i=a.key==="dataZoomSelect"?a.dataZoomSelectActive:!1),t._isZoomActive=i,r.setIconStatus("zoom",i?"emphasis":"normal");var o=new cm(pm(r),e,{include:["grid"]}),s=o.makePanelOpts(n,function(l){return l.xAxisDeclared&&!l.yAxisDeclared?"lineX":!l.xAxisDeclared&&l.yAxisDeclared?"lineY":"rect"});t._brushController.setPanels(s).enableBrush(i&&s.length?{brushType:"auto",brushStyle:r.getModel("brushStyle").getItemStyle()}:!1)}bE("dataZoom",function(r){var e=r.getComponent("toolbox",0),t=["feature","dataZoom"];if(!e||e.get(t)==null)return;var a=e.getModel(t),n=[],i=pm(a),o=Ps(r,i);Kd(o.xAxisModels,function(l){return s(l,"xAxis","xAxisIndex")}),Kd(o.yAxisModels,function(l){return s(l,"yAxis","yAxisIndex")});function s(l,u,f){var h=l.componentIndex,v={type:"select",$fromToolbox:!0,filterMode:a.get("filterMode",!0)||"filter",id:$6+u+h};v[f]=h,n.push(v)}return n});function Q6(r){r.registerComponentModel(b6),r.registerComponentView(T6),Yi("saveAsImage",C6),Yi("magicType",M6),Yi("dataView",B6),Yi("dataZoom",Z6),Yi("restore",W6),gt(x6)}var J6=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},e}(mt);function RI(r){var e=r.get("confine");return e!=null?!!e:r.get("renderMode")==="richText"}function EI(r){if(_t.domSupported){for(var e=document.documentElement.style,t=0,a=r.length;t-1?(s+="top:50%",l+="translateY(-50%) rotate("+(u=i==="left"?-225:-45)+"deg)"):(s+="left:50%",l+="translateX(-50%) rotate("+(u=i==="top"?225:45)+"deg)");var f=u*Math.PI/180,h=o+n,v=h*Math.abs(Math.cos(f))+h*Math.abs(Math.sin(f)),c=Math.round(((v-Math.SQRT2*n)/2+Math.SQRT2*n-(v-h)/2)*100)/100;s+=";"+i+":-"+c+"px";var p=e+" solid "+n+"px;",d=["position:absolute;width:"+o+"px;height:"+o+"px;z-index:-1;",s+";"+l+";","border-bottom:"+p,"border-right:"+p,"background-color:"+a+";"];return'
'}function oU(r,e){var t="cubic-bezier(0.23,1,0.32,1)",a=" "+r/2+"s "+t,n="opacity"+a+",visibility"+a;return e||(a=" "+r+"s "+t,n+=_t.transformSupported?","+dm+a:",left"+a+",top"+a),rU+":"+n}function cb(r,e,t){var a=r.toFixed(0)+"px",n=e.toFixed(0)+"px";if(!_t.transformSupported)return t?"top:"+n+";left:"+a+";":[["top",n],["left",a]];var i=_t.transform3dSupported,o="translate"+(i?"3d":"")+"("+a+","+n+(i?",0":"")+")";return t?"top:0;left:0;"+dm+":"+o+";":[["top",0],["left",0],[kI,o]]}function sU(r){var e=[],t=r.get("fontSize"),a=r.getTextColor();a&&e.push("color:"+a),e.push("font:"+r.getFont()),t&&e.push("line-height:"+Math.round(t*3/2)+"px");var n=r.get("textShadowColor"),i=r.get("textShadowBlur")||0,o=r.get("textShadowOffsetX")||0,s=r.get("textShadowOffsetY")||0;return n&&i&&e.push("text-shadow:"+o+"px "+s+"px "+i+"px "+n),C(["decoration","align"],function(l){var u=r.get(l);u&&e.push("text-"+l+":"+u)}),e.join(";")}function lU(r,e,t){var a=[],n=r.get("transitionDuration"),i=r.get("backgroundColor"),o=r.get("shadowBlur"),s=r.get("shadowColor"),l=r.get("shadowOffsetX"),u=r.get("shadowOffsetY"),f=r.getModel("textStyle"),h=$A(r,"html"),v=l+"px "+u+"px "+o+"px "+s;return a.push("box-shadow:"+v),e&&n&&a.push(oU(n,t)),i&&a.push("background-color:"+i),C(["width","color","radius"],function(c){var p="border-"+c,d=Xg(p),g=r.get(d);g!=null&&a.push(p+":"+g+(c==="color"?"":"px"))}),a.push(sU(f)),h!=null&&a.push("padding:"+pi(h).join("px ")+"px"),a.join(";")+";"}function pb(r,e,t,a,n){var i=e&&e.painter;if(t){var o=i&&i.getViewportRoot();o&&PL(r,o,t,a,n)}else{r[0]=a,r[1]=n;var s=i&&i.getViewportRootOffset();s&&(r[0]+=s.offsetLeft,r[1]+=s.offsetTop)}r[2]=r[0]/e.getWidth(),r[3]=r[1]/e.getHeight()}var uU=function(){function r(e,t){if(this._show=!1,this._styleCoord=[0,0,0,0],this._enterable=!0,this._alwaysShowContent=!1,this._firstShow=!0,this._longHide=!0,_t.wxa)return null;var a=document.createElement("div");a.domBelongToZr=!0,this.el=a;var n=this._zr=e.getZr(),i=t.appendTo,o=i&&(U(i)?document.querySelector(i):ri(i)?i:K(i)&&i(e.getDom()));pb(this._styleCoord,n,o,e.getWidth()/2,e.getHeight()/2),(o||e.getDom()).appendChild(a),this._api=e,this._container=o;var s=this;a.onmouseenter=function(){s._enterable&&(clearTimeout(s._hideTimeout),s._show=!0),s._inContent=!0},a.onmousemove=function(l){if(l=l||window.event,!s._enterable){var u=n.handler,f=n.painter.getViewportRoot();Je(f,l,!0),u.dispatch("mousemove",l)}},a.onmouseleave=function(){s._inContent=!1,s._enterable&&s._show&&s.hideLater(s._hideDelay)}}return r.prototype.update=function(e){if(!this._container){var t=this._api.getDom(),a=eU(t,"position"),n=t.style;n.position!=="absolute"&&a!=="absolute"&&(n.position="relative")}var i=e.get("alwaysShowContent");i&&this._moveIfResized(),this._alwaysShowContent=i,this.el.className=e.get("className")||""},r.prototype.show=function(e,t){clearTimeout(this._hideTimeout),clearTimeout(this._longHideTimeout);var a=this.el,n=a.style,i=this._styleCoord;a.innerHTML?n.cssText=aU+lU(e,!this._firstShow,this._longHide)+cb(i[0],i[1],!0)+("border-color:"+si(t)+";")+(e.get("extraCssText")||"")+(";pointer-events:"+(this._enterable?"auto":"none")):n.display="none",this._show=!0,this._firstShow=!1,this._longHide=!1},r.prototype.setContent=function(e,t,a,n,i){var o=this.el;if(e==null){o.innerHTML="";return}var s="";if(U(i)&&a.get("trigger")==="item"&&!RI(a)&&(s=iU(a,n,i)),U(e))o.innerHTML=e+s;else if(e){o.innerHTML="",z(e)||(e=[e]);for(var l=0;l=0?this._tryShow(i,o):n==="leave"&&this._hide(o))},this))},e.prototype._keepShow=function(){var t=this._tooltipModel,a=this._ecModel,n=this._api,i=t.get("triggerOn");if(this._lastX!=null&&this._lastY!=null&&i!=="none"&&i!=="click"){var o=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout(function(){!n.isDisposed()&&o.manuallyShowTip(t,a,n,{x:o._lastX,y:o._lastY,dataByCoordSys:o._lastDataByCoordSys})})}},e.prototype.manuallyShowTip=function(t,a,n,i){if(!(i.from===this.uid||_t.node||!n.getDom())){var o=yb(i,n);this._ticket="";var s=i.dataByCoordSys,l=gU(i,a,n);if(l){var u=l.el.getBoundingRect().clone();u.applyTransform(l.el.transform),this._tryShow({offsetX:u.x+u.width/2,offsetY:u.y+u.height/2,target:l.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&i.x!=null&&i.y!=null){var f=hU;f.x=i.x,f.y=i.y,f.update(),nt(f).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:f},o)}else if(s)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:s,tooltipOption:i.tooltipOption},o);else if(i.seriesIndex!=null){if(this._manuallyAxisShowTip(t,a,n,i))return;var h=SI(i,a),v=h.point[0],c=h.point[1];v!=null&&c!=null&&this._tryShow({offsetX:v,offsetY:c,target:h.el,position:i.position,positionDefault:"bottom"},o)}else i.x!=null&&i.y!=null&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,a,n,i){var o=this._tooltipContent;this._tooltipModel&&o.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(yb(i,n))},e.prototype._manuallyAxisShowTip=function(t,a,n,i){var o=i.seriesIndex,s=i.dataIndex,l=a.getComponent("axisPointer").coordSysAxesInfo;if(!(o==null||s==null||l==null)){var u=a.getSeriesByIndex(o);if(u){var f=u.getData(),h=ss([f.getItemModel(s),u,(u.coordinateSystem||{}).model],this._tooltipModel);if(h.get("trigger")==="axis")return n.dispatchAction({type:"updateAxisPointer",seriesIndex:o,dataIndex:s,position:i.position}),!0}}},e.prototype._tryShow=function(t,a){var n=t.target,i=this._tooltipModel;if(i){this._lastX=t.offsetX,this._lastY=t.offsetY;var o=t.dataByCoordSys;if(o&&o.length)this._showAxisTooltip(o,t);else if(n){var s=nt(n);if(s.ssrType==="legend")return;this._lastDataByCoordSys=null;var l,u;Yn(n,function(f){if(nt(f).dataIndex!=null)return l=f,!0;if(nt(f).tooltipConfig!=null)return u=f,!0},!0),l?this._showSeriesItemTooltip(t,l,a):u?this._showComponentItemTooltip(t,u,a):this._hide(a)}else this._lastDataByCoordSys=null,this._hide(a)}},e.prototype._showOrMove=function(t,a){var n=t.get("showDelay");a=X(a,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(a,n):a()},e.prototype._showAxisTooltip=function(t,a){var n=this._ecModel,i=this._tooltipModel,o=[a.offsetX,a.offsetY],s=ss([a.tooltipOption],i),l=this._renderMode,u=[],f=ie("section",{blocks:[],noHeader:!0}),h=[],v=new Wv;C(t,function(m){C(m.dataByAxis,function(_){var S=n.getComponent(_.axisDim+"Axis",_.axisIndex),b=_.value;if(!(!S||b==null)){var x=gI(b,S.axis,n,_.seriesDataIndices,_.valueLabelOpt),w=ie("section",{header:x,noHeader:!or(x),sortBlocks:!0,blocks:[]});f.blocks.push(w),C(_.seriesDataIndices,function(T){var A=n.getSeriesByIndex(T.seriesIndex),D=T.dataIndexInside,M=A.getDataParams(D);if(!(M.dataIndex<0)){M.axisDim=_.axisDim,M.axisIndex=_.axisIndex,M.axisType=_.axisType,M.axisId=_.axisId,M.axisValue=gy(S.axis,{value:b}),M.axisValueLabel=x,M.marker=v.makeTooltipMarker("item",si(M.color),l);var I=a_(A.formatTooltip(D,!0,null)),L=I.frag;if(L){var P=ss([A],i).get("valueFormatter");w.blocks.push(P?V({valueFormatter:P},L):L)}I.text&&h.push(I.text),u.push(M)}})}})}),f.blocks.reverse(),h.reverse();var c=a.position,p=s.get("order"),d=u_(f,v,l,p,n.get("useUTC"),s.get("textStyle"));d&&h.unshift(d);var g=l==="richText"?` + +`:"
",y=h.join(g);this._showOrMove(s,function(){this._updateContentNotChangedOnAxis(t,u)?this._updatePosition(s,c,o[0],o[1],this._tooltipContent,u):this._showTooltipContent(s,y,u,Math.random()+"",o[0],o[1],c,null,v)})},e.prototype._showSeriesItemTooltip=function(t,a,n){var i=this._ecModel,o=nt(a),s=o.seriesIndex,l=i.getSeriesByIndex(s),u=o.dataModel||l,f=o.dataIndex,h=o.dataType,v=u.getData(h),c=this._renderMode,p=t.positionDefault,d=ss([v.getItemModel(f),u,l&&(l.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),g=d.get("trigger");if(!(g!=null&&g!=="item")){var y=u.getDataParams(f,h),m=new Wv;y.marker=m.makeTooltipMarker("item",si(y.color),c);var _=a_(u.formatTooltip(f,!1,h)),S=d.get("order"),b=d.get("valueFormatter"),x=_.frag,w=x?u_(b?V({valueFormatter:b},x):x,m,c,S,i.get("useUTC"),d.get("textStyle")):_.text,T="item_"+u.name+"_"+f;this._showOrMove(d,function(){this._showTooltipContent(d,w,y,T,t.offsetX,t.offsetY,t.position,t.target,m)}),n({type:"showTip",dataIndexInside:f,dataIndex:v.getRawIndex(f),seriesIndex:s,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,a,n){var i=this._renderMode==="html",o=nt(a),s=o.tooltipConfig,l=s.option||{},u=l.encodeHTMLContent;if(U(l)){var f=l;l={content:f,formatter:f},u=!0}u&&i&&l.content&&(l=et(l),l.content=Me(l.content));var h=[l],v=this._ecModel.getComponent(o.componentMainType,o.componentIndex);v&&h.push(v),h.push({formatter:l.content});var c=t.positionDefault,p=ss(h,this._tooltipModel,c?{position:c}:null),d=p.get("content"),g=Math.random()+"",y=new Wv;this._showOrMove(p,function(){var m=et(p.get("formatterParams")||{});this._showTooltipContent(p,d,m,g,t.offsetX,t.offsetY,t.position,a,y)}),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,a,n,i,o,s,l,u,f){if(this._ticket="",!(!t.get("showContent")||!t.get("show"))){var h=this._tooltipContent;h.setEnterable(t.get("enterable"));var v=t.get("formatter");l=l||t.get("position");var c=a,p=this._getNearestPoint([o,s],n,t.get("trigger"),t.get("borderColor")),d=p.color;if(v)if(U(v)){var g=t.ecModel.get("useUTC"),y=z(n)?n[0]:n,m=y&&y.axisType&&y.axisType.indexOf("time")>=0;c=v,m&&(c=Il(y.axisValue,c,g)),c=$g(c,n,!0)}else if(K(v)){var _=X(function(S,b){S===this._ticket&&(h.setContent(b,f,t,d,l),this._updatePosition(t,l,o,s,h,n,u))},this);this._ticket=i,c=v(n,i,_)}else c=v;h.setContent(c,f,t,d,l),h.show(t,d),this._updatePosition(t,l,o,s,h,n,u)}},e.prototype._getNearestPoint=function(t,a,n,i){if(n==="axis"||z(a))return{color:i||(this._renderMode==="html"?"#fff":"none")};if(!z(a))return{color:i||a.color||a.borderColor}},e.prototype._updatePosition=function(t,a,n,i,o,s,l){var u=this._api.getWidth(),f=this._api.getHeight();a=a||t.get("position");var h=o.getSize(),v=t.get("align"),c=t.get("verticalAlign"),p=l&&l.getBoundingRect().clone();if(l&&p.applyTransform(l.transform),K(a)&&(a=a([n,i],s,o.el,p,{viewSize:[u,f],contentSize:h.slice()})),z(a))n=W(a[0],u),i=W(a[1],f);else if(tt(a)){var d=a;d.width=h[0],d.height=h[1];var g=jt(d,{width:u,height:f});n=g.x,i=g.y,v=null,c=null}else if(U(a)&&l){var y=dU(a,p,h,t.get("borderWidth"));n=y[0],i=y[1]}else{var y=cU(n,i,o,u,f,v?null:20,c?null:20);n=y[0],i=y[1]}if(v&&(n-=mb(v)?h[0]/2:v==="right"?h[0]:0),c&&(i-=mb(c)?h[1]/2:c==="bottom"?h[1]:0),RI(t)){var y=pU(n,i,o,u,f);n=y[0],i=y[1]}o.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,a){var n=this._lastDataByCoordSys,i=this._cbParamsList,o=!!n&&n.length===t.length;return o&&C(n,function(s,l){var u=s.dataByAxis||[],f=t[l]||{},h=f.dataByAxis||[];o=o&&u.length===h.length,o&&C(u,function(v,c){var p=h[c]||{},d=v.seriesDataIndices||[],g=p.seriesDataIndices||[];o=o&&v.value===p.value&&v.axisType===p.axisType&&v.axisId===p.axisId&&d.length===g.length,o&&C(d,function(y,m){var _=g[m];o=o&&y.seriesIndex===_.seriesIndex&&y.dataIndex===_.dataIndex}),i&&C(v.seriesDataIndices,function(y){var m=y.seriesIndex,_=a[m],S=i[m];_&&S&&S.data!==_.data&&(o=!1)})})}),this._lastDataByCoordSys=t,this._cbParamsList=a,!!o},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,a){_t.node||!a.getDom()||(rl(this,"_updatePosition"),this._tooltipContent.dispose(),Yd("itemTooltip",a))},e.type="tooltip",e}(zt);function ss(r,e,t){var a=e.ecModel,n;t?(n=new Mt(t,a,a),n=new Mt(e.option,n,a)):n=e;for(var i=r.length-1;i>=0;i--){var o=r[i];o&&(o instanceof Mt&&(o=o.get("tooltip",!0)),U(o)&&(o={formatter:o}),o&&(n=new Mt(o,n,a)))}return n}function yb(r,e){return r.dispatchAction||X(e.dispatchAction,e)}function cU(r,e,t,a,n,i,o){var s=t.getSize(),l=s[0],u=s[1];return i!=null&&(r+l+i+2>a?r-=l+i:r+=i),o!=null&&(e+u+o>n?e-=u+o:e+=o),[r,e]}function pU(r,e,t,a,n){var i=t.getSize(),o=i[0],s=i[1];return r=Math.min(r+o,a)-o,e=Math.min(e+s,n)-s,r=Math.max(r,0),e=Math.max(e,0),[r,e]}function dU(r,e,t,a){var n=t[0],i=t[1],o=Math.ceil(Math.SQRT2*a)+8,s=0,l=0,u=e.width,f=e.height;switch(r){case"inside":s=e.x+u/2-n/2,l=e.y+f/2-i/2;break;case"top":s=e.x+u/2-n/2,l=e.y-i-o;break;case"bottom":s=e.x+u/2-n/2,l=e.y+f+o;break;case"left":s=e.x-n-o,l=e.y+f/2-i/2;break;case"right":s=e.x+u+o,l=e.y+f/2-i/2}return[s,l]}function mb(r){return r==="center"||r==="middle"}function gU(r,e,t){var a=Tg(r).queryOptionMap,n=a.keys()[0];if(!(!n||n==="series")){var i=wl(e,n,a.get(n),{useDefault:!1,enableAll:!1,enableNone:!1}),o=i.models[0];if(o){var s=t.getViewOfComponentModel(o),l;if(s.group.traverse(function(u){var f=nt(u).tooltipConfig;if(f&&f.name===r.name)return l=u,!0}),l)return{componentMainType:n,componentIndex:o.componentIndex,el:l}}}}function yU(r){gt(Gl),r.registerComponentModel(J6),r.registerComponentView(vU),r.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},Yt),r.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},Yt)}var mU=["rect","polygon","keep","clear"];function _U(r,e){var t=Et(r?r.brush:[]);if(t.length){var a=[];C(t,function(l){var u=l.hasOwnProperty("toolbox")?l.toolbox:[];u instanceof Array&&(a=a.concat(u))});var n=r&&r.toolbox;z(n)&&(n=n[0]),n||(n={feature:{}},r.toolbox=[n]);var i=n.feature||(n.feature={}),o=i.brush||(i.brush={}),s=o.type||(o.type=[]);s.push.apply(s,a),SU(s),e&&!s.length&&s.push.apply(s,mU)}}function SU(r){var e={};C(r,function(t){e[t]=1}),r.length=0,C(e,function(t,a){r.push(a)})}var _b=C;function Sb(r){if(r){for(var e in r)if(r.hasOwnProperty(e))return!0}}function jd(r,e,t){var a={};return _b(e,function(i){var o=a[i]=n();_b(r[i],function(s,l){if(ae.isValidType(l)){var u={type:l,visual:s};t&&t(u,i),o[l]=new ae(u),l==="opacity"&&(u=et(u),u.type="colorAlpha",o.__hidden.__alphaForOpacity=new ae(u))}})}),a;function n(){var i=function(){};i.prototype.__hidden=i.prototype;var o=new i;return o}}function NI(r,e,t){var a;C(t,function(n){e.hasOwnProperty(n)&&Sb(e[n])&&(a=!0)}),a&&C(t,function(n){e.hasOwnProperty(n)&&Sb(e[n])?r[n]=et(e[n]):delete r[n]})}function xU(r,e,t,a,n,i){var o={};C(r,function(h){var v=ae.prepareVisualTypes(e[h]);o[h]=v});var s;function l(h){return ny(t,s,h)}function u(h,v){aC(t,s,h,v)}t.each(f);function f(h,v){s=h;var c=t.getRawDataItem(s);if(!(c&&c.visualMap===!1))for(var p=a.call(n,h),d=e[p],g=o[p],y=0,m=g.length;ye[0][1]&&(e[0][1]=i[0]),i[1]e[1][1]&&(e[1][1]=i[1])}return e&&Ab(e)}};function Ab(r){return new ht(r[0][0],r[1][0],r[0][1]-r[0][0],r[1][1]-r[1][0])}var IU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t,a){this.ecModel=t,this.api=a,this.model,(this._brushController=new Hy(a.getZr())).on("brush",X(this._onBrush,this)).mount()},e.prototype.render=function(t,a,n,i){this.model=t,this._updateController(t,a,n,i)},e.prototype.updateTransform=function(t,a,n,i){BI(a),this._updateController(t,a,n,i)},e.prototype.updateVisual=function(t,a,n,i){this.updateTransform(t,a,n,i)},e.prototype.updateView=function(t,a,n,i){this._updateController(t,a,n,i)},e.prototype._updateController=function(t,a,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var a=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:a,areas:et(n),$from:a}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:a,areas:et(n),$from:a})},e.type="brush",e}(zt),LU="#ddd",PU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.areas=[],t.brushOption={},t}return e.prototype.optionUpdated=function(t,a){var n=this.option;!a&&NI(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:LU},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=G(t,function(a){return Cb(this.option,a)},this))},e.prototype.setBrushOption=function(t){this.brushOption=Cb(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(210,219,238,0.3)",borderColor:"#D2DBEE"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},e}(mt);function Cb(r,e){return st({brushType:r.brushType,brushMode:r.brushMode,transformable:r.transformable,brushStyle:new Mt(r.brushStyle).getItemStyle(),removeOnClick:r.removeOnClick,z:r.z},e,!0)}var RU=["rect","polygon","lineX","lineY","keep","clear"],EU=function(r){k(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.render=function(t,a,n){var i,o,s;a.eachComponent({mainType:"brush"},function(l){i=l.brushType,o=l.brushOption.brushMode||"single",s=s||!!l.areas.length}),this._brushType=i,this._brushMode=o,C(t.get("type",!0),function(l){t.setIconStatus(l,(l==="keep"?o==="multiple":l==="clear"?s:l===i)?"emphasis":"normal")})},e.prototype.updateView=function(t,a,n){this.render(t,a,n)},e.prototype.getIcons=function(){var t=this.model,a=t.get("icon",!0),n={};return C(t.get("type",!0),function(i){a[i]&&(n[i]=a[i])}),n},e.prototype.onclick=function(t,a,n){var i=this._brushType,o=this._brushMode;n==="clear"?(a.dispatchAction({type:"axisAreaSelect",intervals:[]}),a.dispatchAction({type:"brush",command:"clear",areas:[]})):a.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:n==="keep"?i:i===n?!1:n,brushMode:n==="keep"?o==="multiple"?"single":"multiple":o}})},e.getDefaultOption=function(t){var a={show:!0,type:RU.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])};return a},e}(nr);function kU(r){r.registerComponentView(IU),r.registerComponentModel(PU),r.registerPreprocessor(_U),r.registerVisual(r.PRIORITY.VISUAL.BRUSH,TU),r.registerAction({type:"brush",event:"brush",update:"updateVisual"},function(e,t){t.eachComponent({mainType:"brush",query:e},function(a){a.setAreas(e.areas)})}),r.registerAction({type:"brushSelect",event:"brushSelected",update:"none"},Yt),r.registerAction({type:"brushEnd",event:"brushEnd",update:"none"},Yt),Yi("brush",EU)}var OU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.layoutMode={type:"box",ignoreSize:!0},t}return e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:"#464646"},subtextStyle:{fontSize:12,color:"#6E7079"}},e}(mt),NU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.render=function(t,a,n){if(this.group.removeAll(),!!t.get("show")){var i=this.group,o=t.getModel("textStyle"),s=t.getModel("subtextStyle"),l=t.get("textAlign"),u=ot(t.get("textBaseline"),t.get("textVerticalAlign")),f=new bt({style:Nt(o,{text:t.get("text"),fill:o.getTextColor()},{disableBox:!0}),z2:10}),h=f.getBoundingRect(),v=t.get("subtext"),c=new bt({style:Nt(s,{text:v,fill:s.getTextColor(),y:h.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),p=t.get("link"),d=t.get("sublink"),g=t.get("triggerEvent",!0);f.silent=!p&&!g,c.silent=!d&&!g,p&&f.on("click",function(){If(p,"_"+t.get("target"))}),d&&c.on("click",function(){If(d,"_"+t.get("subtarget"))}),nt(f).eventData=nt(c).eventData=g?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(f),v&&i.add(c);var y=i.getBoundingRect(),m=t.getBoxLayoutParams();m.width=y.width,m.height=y.height;var _=jt(m,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));l||(l=t.get("left")||t.get("right"),l==="middle"&&(l="center"),l==="right"?_.x+=_.width:l==="center"&&(_.x+=_.width/2)),u||(u=t.get("top")||t.get("bottom"),u==="center"&&(u="middle"),u==="bottom"?_.y+=_.height:u==="middle"&&(_.y+=_.height/2),u=u||"top"),i.x=_.x,i.y=_.y,i.markRedraw();var S={align:l,verticalAlign:u};f.setStyle(S),c.setStyle(S),y=i.getBoundingRect();var b=_.margin,x=t.getItemStyle(["color","opacity"]);x.fill=t.get("backgroundColor");var w=new wt({shape:{x:y.x-b[3],y:y.y-b[0],width:y.width+b[1]+b[3],height:y.height+b[0]+b[2],r:t.get("borderRadius")},style:x,subPixelOptimize:!0,silent:!0});i.add(w)}},e.type="title",e}(zt);function BU(r){r.registerComponentModel(OU),r.registerComponentView(NU)}var Db=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.layoutMode="box",t}return e.prototype.init=function(t,a,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(t){r.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){t==null&&(t=this.option.currentIndex);var a=this._data.count();this.option.loop?t=(t%a+a)%a:(t>=a&&(t=a-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t=this.option,a=t.data||[],n=t.axisType,i=this._names=[],o;n==="category"?(o=[],C(a,function(u,f){var h=Qt(yo(u),""),v;tt(u)?(v=et(u),v.value=f):v=f,o.push(v),i.push(h)})):o=a;var s={category:"ordinal",time:"time",value:"number"}[n]||"number",l=this._data=new Le([{name:"value",type:s}],this);l.initData(o,i)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if(this.get("axisType")==="category")return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},e}(mt),VI=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="timeline.slider",e.defaultOption=on(Db.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),e}(Db);Xt(VI,Nh.prototype);var VU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="timeline",e}(zt),zU=function(r){k(e,r);function e(t,a,n,i){var o=r.call(this,t,a,n)||this;return o.type=i||"value",o}return e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return this.model.get("orient")==="horizontal"},e}(Lr),op=Math.PI,Mb=Tt(),GU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(t,a){this.api=a},e.prototype.render=function(t,a,n){if(this.model=t,this.api=n,this.ecModel=a,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),o=this._createGroup("_mainGroup"),s=this._createGroup("_labelGroup"),l=this._axis=this._createAxis(i,t);t.formatTooltip=function(u){var f=l.scale.getLabel({value:u});return ie("nameValue",{noName:!0,value:f})},C(["AxisLine","AxisTick","Control","CurrentPointer"],function(u){this["_render"+u](i,o,l,t)},this),this._renderAxisLabel(i,s,l,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,a){var n=t.get(["label","position"]),i=t.get("orient"),o=HU(t,a),s;n==null||n==="auto"?s=i==="horizontal"?o.y+o.height/2=0||s==="+"?"left":"right"},u={horizontal:s>=0||s==="+"?"top":"bottom",vertical:"middle"},f={horizontal:0,vertical:op/2},h=i==="vertical"?o.height:o.width,v=t.getModel("controlStyle"),c=v.get("show",!0),p=c?v.get("itemSize"):0,d=c?v.get("itemGap"):0,g=p+d,y=t.get(["label","rotate"])||0;y=y*op/180;var m,_,S,b=v.get("position",!0),x=c&&v.get("showPlayBtn",!0),w=c&&v.get("showPrevBtn",!0),T=c&&v.get("showNextBtn",!0),A=0,D=h;b==="left"||b==="bottom"?(x&&(m=[0,0],A+=g),w&&(_=[A,0],A+=g),T&&(S=[D-p,0],D-=g)):(x&&(m=[D-p,0],D-=g),w&&(_=[0,0],A+=g),T&&(S=[D-p,0],D-=g));var M=[A,D];return t.get("inverse")&&M.reverse(),{viewRect:o,mainLength:h,orient:i,rotation:f[i],labelRotation:y,labelPosOpt:s,labelAlign:t.get(["label","align"])||l[i],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||u[i],playPosition:m,prevBtnPosition:_,nextBtnPosition:S,axisExtent:M,controlSize:p,controlGap:d}},e.prototype._position=function(t,a){var n=this._mainGroup,i=this._labelGroup,o=t.viewRect;if(t.orient==="vertical"){var s=Ge(),l=o.x,u=o.y+o.height;Dr(s,s,[-l,-u]),an(s,s,-op/2),Dr(s,s,[l,u]),o=o.clone(),o.applyTransform(s)}var f=m(o),h=m(n.getBoundingRect()),v=m(i.getBoundingRect()),c=[n.x,n.y],p=[i.x,i.y];p[0]=c[0]=f[0][0];var d=t.labelPosOpt;if(d==null||U(d)){var g=d==="+"?0:1;_(c,h,f,1,g),_(p,v,f,1,1-g)}else{var g=d>=0?0:1;_(c,h,f,1,g),p[1]=c[1]+d}n.setPosition(c),i.setPosition(p),n.rotation=i.rotation=t.rotation,y(n),y(i);function y(S){S.originX=f[0][0]-S.x,S.originY=f[1][0]-S.y}function m(S){return[[S.x,S.x+S.width],[S.y,S.y+S.height]]}function _(S,b,x,w,T){S[w]+=x[w][T]-b[w][T]}},e.prototype._createAxis=function(t,a){var n=a.getData(),i=a.get("axisType"),o=FU(a,i);o.getTicks=function(){return n.mapArray(["value"],function(u){return{value:u}})};var s=n.getDataExtent("value");o.setExtent(s[0],s[1]),o.calcNiceTicks();var l=new zU("value",o,t.axisExtent,i);return l.model=a,l},e.prototype._createGroup=function(t){var a=this[t]=new at;return this.group.add(a),a},e.prototype._renderAxisLine=function(t,a,n,i){var o=n.getExtent();if(i.get(["lineStyle","show"])){var s=new Jt({shape:{x1:o[0],y1:0,x2:o[1],y2:0},style:V({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});a.add(s);var l=this._progressLine=new Jt({shape:{x1:o[0],x2:this._currentPointer?this._currentPointer.x:o[0],y1:0,y2:0},style:Q({lineCap:"round",lineWidth:s.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});a.add(l)}},e.prototype._renderAxisTick=function(t,a,n,i){var o=this,s=i.getData(),l=n.scale.getTicks();this._tickSymbols=[],C(l,function(u){var f=n.dataToCoord(u.value),h=s.getItemModel(u.value),v=h.getModel("itemStyle"),c=h.getModel(["emphasis","itemStyle"]),p=h.getModel(["progress","itemStyle"]),d={x:f,y:0,onclick:X(o._changeTimeline,o,u.value)},g=Ib(h,v,a,d);g.ensureState("emphasis").style=c.getItemStyle(),g.ensureState("progress").style=p.getItemStyle(),Ya(g);var y=nt(g);h.get("tooltip")?(y.dataIndex=u.value,y.dataModel=i):y.dataIndex=y.dataModel=null,o._tickSymbols.push(g)})},e.prototype._renderAxisLabel=function(t,a,n,i){var o=this,s=n.getLabelModel();if(s.get("show")){var l=i.getData(),u=n.getViewLabels();this._tickLabels=[],C(u,function(f){var h=f.tickValue,v=l.getItemModel(h),c=v.getModel("label"),p=v.getModel(["emphasis","label"]),d=v.getModel(["progress","label"]),g=n.dataToCoord(f.tickValue),y=new bt({x:g,y:0,rotation:t.labelRotation-t.rotation,onclick:X(o._changeTimeline,o,h),silent:!1,style:Nt(c,{text:f.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});y.ensureState("emphasis").style=Nt(p),y.ensureState("progress").style=Nt(d),a.add(y),Ya(y),Mb(y).dataIndex=h,o._tickLabels.push(y)})}},e.prototype._renderControl=function(t,a,n,i){var o=t.controlSize,s=t.rotation,l=i.getModel("controlStyle").getItemStyle(),u=i.getModel(["emphasis","controlStyle"]).getItemStyle(),f=i.getPlayState(),h=i.get("inverse",!0);v(t.nextBtnPosition,"next",X(this._changeTimeline,this,h?"-":"+")),v(t.prevBtnPosition,"prev",X(this._changeTimeline,this,h?"+":"-")),v(t.playPosition,f?"stop":"play",X(this._handlePlayClick,this,!f),!0);function v(c,p,d,g){if(c){var y=Mr(ot(i.get(["controlStyle",p+"BtnSize"]),o),o),m=[0,-y/2,y,y],_=WU(i,p+"Icon",m,{x:c[0],y:c[1],originX:o/2,originY:0,rotation:g?-s:0,rectHover:!0,style:l,onclick:d});_.ensureState("emphasis").style=u,a.add(_),Ya(_)}}},e.prototype._renderCurrentPointer=function(t,a,n,i){var o=i.getData(),s=i.getCurrentIndex(),l=o.getItemModel(s).getModel("checkpointStyle"),u=this,f={onCreate:function(h){h.draggable=!0,h.drift=X(u._handlePointerDrag,u),h.ondragend=X(u._handlePointerDragend,u),Lb(h,u._progressLine,s,n,i,!0)},onUpdate:function(h){Lb(h,u._progressLine,s,n,i)}};this._currentPointer=Ib(l,l,this._mainGroup,{},this._currentPointer,f)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,a,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,a){var n=this._toAxisCoord(t)[0],i=this._axis,o=Ue(i.getExtent().slice());n>o[1]&&(n=o[1]),n=0&&(o[i]=+o[i].toFixed(v)),[o,h]}var sp={min:it(Fu,"min"),max:it(Fu,"max"),average:it(Fu,"average"),median:it(Fu,"median")};function gl(r,e){if(e){var t=r.getData(),a=r.coordinateSystem,n=a&&a.dimensions;if(!qU(e)&&!z(e.coord)&&z(n)){var i=zI(e,t,a,r);if(e=et(e),e.type&&sp[e.type]&&i.baseAxis&&i.valueAxis){var o=vt(n,i.baseAxis.dim),s=vt(n,i.valueAxis.dim),l=sp[e.type](t,i.baseDataDim,i.valueDataDim,o,s);e.coord=l[0],e.value=l[1]}else e.coord=[e.xAxis!=null?e.xAxis:e.radiusAxis,e.yAxis!=null?e.yAxis:e.angleAxis]}if(e.coord==null||!z(n))e.coord=[];else for(var u=e.coord,f=0;f<2;f++)sp[u[f]]&&(u[f]=ym(t,t.mapDimension(n[f]),u[f]));return e}}function zI(r,e,t,a){var n={};return r.valueIndex!=null||r.valueDim!=null?(n.valueDataDim=r.valueIndex!=null?e.getDimension(r.valueIndex):r.valueDim,n.valueAxis=t.getAxis(KU(a,n.valueDataDim)),n.baseAxis=t.getOtherAxis(n.valueAxis),n.baseDataDim=e.mapDimension(n.baseAxis.dim)):(n.baseAxis=a.getBaseAxis(),n.valueAxis=t.getOtherAxis(n.baseAxis),n.baseDataDim=e.mapDimension(n.baseAxis.dim),n.valueDataDim=e.mapDimension(n.valueAxis.dim)),n}function KU(r,e){var t=r.getData().getDimensionInfo(e);return t&&t.coordDim}function yl(r,e){return r&&r.containData&&e.coord&&!Jd(e)?r.containData(e.coord):!0}function jU(r,e,t){return r&&r.containZone&&e.coord&&t.coord&&!Jd(e)&&!Jd(t)?r.containZone(e.coord,t.coord):!0}function GI(r,e){return r?function(t,a,n,i){var o=i<2?t.coord&&t.coord[i]:t.value;return Za(o,e[i])}:function(t,a,n,i){return Za(t.value,e[i])}}function ym(r,e,t){if(t==="average"){var a=0,n=0;return r.each(e,function(i,o){isNaN(i)||(a+=i,n++)}),a/n}else return t==="median"?r.getMedian(e):r.getDataExtent(e)[t==="max"?1:0]}var lp=Tt(),mm=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.init=function(){this.markerGroupMap=Z()},e.prototype.render=function(t,a,n){var i=this,o=this.markerGroupMap;o.each(function(s){lp(s).keep=!1}),a.eachSeries(function(s){var l=xa.getMarkerModelFromSeries(s,i.type);l&&i.renderSeries(s,l,a,n)}),o.each(function(s){!lp(s).keep&&i.group.remove(s.group)})},e.prototype.markKeep=function(t){lp(t).keep=!0},e.prototype.toggleBlurSeries=function(t,a){var n=this;C(t,function(i){var o=xa.getMarkerModelFromSeries(i,n.type);if(o){var s=o.getData();s.eachItemGraphicEl(function(l){l&&(a?ET(l):Pg(l))})}})},e.type="marker",e}(zt);function Rb(r,e,t){var a=e.coordinateSystem;r.each(function(n){var i=r.getItemModel(n),o,s=W(i.get("x"),t.getWidth()),l=W(i.get("y"),t.getHeight());if(!isNaN(s)&&!isNaN(l))o=[s,l];else if(e.getMarkerPosition)o=e.getMarkerPosition(r.getValues(r.dimensions,n));else if(a){var u=r.get(a.dimensions[0],n),f=r.get(a.dimensions[1],n);o=a.dataToPoint([u,f])}isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),r.setItemLayout(n,o)})}var QU=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.updateTransform=function(t,a,n){a.eachSeries(function(i){var o=xa.getMarkerModelFromSeries(i,"markPoint");o&&(Rb(o.getData(),i,n),this.markerGroupMap.get(i.id).updateLayout())},this)},e.prototype.renderSeries=function(t,a,n,i){var o=t.coordinateSystem,s=t.id,l=t.getData(),u=this.markerGroupMap,f=u.get(s)||u.set(s,new El),h=JU(o,t,a);a.setData(h),Rb(a.getData(),t,i),h.each(function(v){var c=h.getItemModel(v),p=c.getShallow("symbol"),d=c.getShallow("symbolSize"),g=c.getShallow("symbolRotate"),y=c.getShallow("symbolOffset"),m=c.getShallow("symbolKeepAspect");if(K(p)||K(d)||K(g)||K(y)){var _=a.getRawValue(v),S=a.getDataParams(v);K(p)&&(p=p(_,S)),K(d)&&(d=d(_,S)),K(g)&&(g=g(_,S)),K(y)&&(y=y(_,S))}var b=c.getModel("itemStyle").getItemStyle(),x=Ll(l,"color");b.fill||(b.fill=x),h.setItemVisual(v,{symbol:p,symbolSize:d,symbolRotate:g,symbolOffset:y,symbolKeepAspect:m,style:b})}),f.updateData(h),this.group.add(f.group),h.eachItemGraphicEl(function(v){v.traverse(function(c){nt(c).dataModel=a})}),this.markKeep(f),f.group.silent=a.get("silent")||t.get("silent")},e.type="markPoint",e}(mm);function JU(r,e,t){var a;r?a=G(r&&r.dimensions,function(s){var l=e.getData().getDimensionInfo(e.getData().mapDimension(s))||{};return V(V({},l),{name:s,ordinalMeta:null})}):a=[{name:"value",type:"float"}];var n=new Le(a,t),i=G(t.get("data"),it(gl,e));r&&(i=Pt(i,it(yl,r)));var o=GI(!!r,a);return n.initData(i,null,o),n}function t8(r){r.registerComponentModel(ZU),r.registerComponentView(QU),r.registerPreprocessor(function(e){gm(e.series,"markPoint")&&(e.markPoint=e.markPoint||{})})}var e8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.createMarkerModelFromSeries=function(t,a,n){return new e(t,a,n)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(xa),Hu=Tt(),r8=function(r,e,t,a){var n=r.getData(),i;if(z(a))i=a;else{var o=a.type;if(o==="min"||o==="max"||o==="average"||o==="median"||a.xAxis!=null||a.yAxis!=null){var s=void 0,l=void 0;if(a.yAxis!=null||a.xAxis!=null)s=e.getAxis(a.yAxis!=null?"y":"x"),l=ee(a.yAxis,a.xAxis);else{var u=zI(a,n,e,r);s=u.valueAxis;var f=hy(n,u.valueDataDim);l=ym(n,f,o)}var h=s.dim==="x"?0:1,v=1-h,c=et(a),p={coord:[]};c.type=null,c.coord=[],c.coord[v]=-1/0,p.coord[v]=1/0;var d=t.get("precision");d>=0&&Ct(l)&&(l=+l.toFixed(Math.min(d,20))),c.coord[h]=p.coord[h]=l,i=[c,p,{type:o,valueIndex:a.valueIndex,value:l}]}else i=[]}var g=[gl(r,i[0]),gl(r,i[1]),V({},i[2])];return g[2].type=g[2].type||null,st(g[2],g[0]),st(g[2],g[1]),g};function rh(r){return!isNaN(r)&&!isFinite(r)}function Eb(r,e,t,a){var n=1-r,i=a.dimensions[r];return rh(e[n])&&rh(t[n])&&e[r]===t[r]&&a.getAxis(i).containData(e[r])}function a8(r,e){if(r.type==="cartesian2d"){var t=e[0].coord,a=e[1].coord;if(t&&a&&(Eb(1,t,a,r)||Eb(0,t,a,r)))return!0}return yl(r,e[0])&&yl(r,e[1])}function up(r,e,t,a,n){var i=a.coordinateSystem,o=r.getItemModel(e),s,l=W(o.get("x"),n.getWidth()),u=W(o.get("y"),n.getHeight());if(!isNaN(l)&&!isNaN(u))s=[l,u];else{if(a.getMarkerPosition)s=a.getMarkerPosition(r.getValues(r.dimensions,e));else{var f=i.dimensions,h=r.get(f[0],e),v=r.get(f[1],e);s=i.dataToPoint([h,v])}if(yi(i,"cartesian2d")){var c=i.getAxis("x"),p=i.getAxis("y"),f=i.dimensions;rh(r.get(f[0],e))?s[0]=c.toGlobalCoord(c.getExtent()[t?0:1]):rh(r.get(f[1],e))&&(s[1]=p.toGlobalCoord(p.getExtent()[t?0:1]))}isNaN(l)||(s[0]=l),isNaN(u)||(s[1]=u)}r.setItemLayout(e,s)}var n8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.updateTransform=function(t,a,n){a.eachSeries(function(i){var o=xa.getMarkerModelFromSeries(i,"markLine");if(o){var s=o.getData(),l=Hu(o).from,u=Hu(o).to;l.each(function(f){up(l,f,!0,i,n),up(u,f,!1,i,n)}),s.each(function(f){s.setItemLayout(f,[l.getItemLayout(f),u.getItemLayout(f)])}),this.markerGroupMap.get(i.id).updateLayout()}},this)},e.prototype.renderSeries=function(t,a,n,i){var o=t.coordinateSystem,s=t.id,l=t.getData(),u=this.markerGroupMap,f=u.get(s)||u.set(s,new Fy);this.group.add(f.group);var h=i8(o,t,a),v=h.from,c=h.to,p=h.line;Hu(a).from=v,Hu(a).to=c,a.setData(p);var d=a.get("symbol"),g=a.get("symbolSize"),y=a.get("symbolRotate"),m=a.get("symbolOffset");z(d)||(d=[d,d]),z(g)||(g=[g,g]),z(y)||(y=[y,y]),z(m)||(m=[m,m]),h.from.each(function(S){_(v,S,!0),_(c,S,!1)}),p.each(function(S){var b=p.getItemModel(S).getModel("lineStyle").getLineStyle();p.setItemLayout(S,[v.getItemLayout(S),c.getItemLayout(S)]),b.stroke==null&&(b.stroke=v.getItemVisual(S,"style").fill),p.setItemVisual(S,{fromSymbolKeepAspect:v.getItemVisual(S,"symbolKeepAspect"),fromSymbolOffset:v.getItemVisual(S,"symbolOffset"),fromSymbolRotate:v.getItemVisual(S,"symbolRotate"),fromSymbolSize:v.getItemVisual(S,"symbolSize"),fromSymbol:v.getItemVisual(S,"symbol"),toSymbolKeepAspect:c.getItemVisual(S,"symbolKeepAspect"),toSymbolOffset:c.getItemVisual(S,"symbolOffset"),toSymbolRotate:c.getItemVisual(S,"symbolRotate"),toSymbolSize:c.getItemVisual(S,"symbolSize"),toSymbol:c.getItemVisual(S,"symbol"),style:b})}),f.updateData(p),h.line.eachItemGraphicEl(function(S){nt(S).dataModel=a,S.traverse(function(b){nt(b).dataModel=a})});function _(S,b,x){var w=S.getItemModel(b);up(S,b,x,t,i);var T=w.getModel("itemStyle").getItemStyle();T.fill==null&&(T.fill=Ll(l,"color")),S.setItemVisual(b,{symbolKeepAspect:w.get("symbolKeepAspect"),symbolOffset:ot(w.get("symbolOffset",!0),m[x?0:1]),symbolRotate:ot(w.get("symbolRotate",!0),y[x?0:1]),symbolSize:ot(w.get("symbolSize"),g[x?0:1]),symbol:ot(w.get("symbol",!0),d[x?0:1]),style:T})}this.markKeep(f),f.group.silent=a.get("silent")||t.get("silent")},e.type="markLine",e}(mm);function i8(r,e,t){var a;r?a=G(r&&r.dimensions,function(u){var f=e.getData().getDimensionInfo(e.getData().mapDimension(u))||{};return V(V({},f),{name:u,ordinalMeta:null})}):a=[{name:"value",type:"float"}];var n=new Le(a,t),i=new Le(a,t),o=new Le([],t),s=G(t.get("data"),it(r8,e,r,t));r&&(s=Pt(s,it(a8,r)));var l=GI(!!r,a);return n.initData(G(s,function(u){return u[0]}),null,l),i.initData(G(s,function(u){return u[1]}),null,l),o.initData(G(s,function(u){return u[2]})),o.hasItemOption=!0,{from:n,to:i,line:o}}function o8(r){r.registerComponentModel(e8),r.registerComponentView(n8),r.registerPreprocessor(function(e){gm(e.series,"markLine")&&(e.markLine=e.markLine||{})})}var s8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.createMarkerModelFromSeries=function(t,a,n){return new e(t,a,n)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(xa),Wu=Tt(),l8=function(r,e,t,a){var n=a[0],i=a[1];if(!(!n||!i)){var o=gl(r,n),s=gl(r,i),l=o.coord,u=s.coord;l[0]=ee(l[0],-1/0),l[1]=ee(l[1],-1/0),u[0]=ee(u[0],1/0),u[1]=ee(u[1],1/0);var f=hh([{},o,s]);return f.coord=[o.coord,s.coord],f.x0=o.x,f.y0=o.y,f.x1=s.x,f.y1=s.y,f}};function ah(r){return!isNaN(r)&&!isFinite(r)}function kb(r,e,t,a){var n=1-r;return ah(e[n])&&ah(t[n])}function u8(r,e){var t=e.coord[0],a=e.coord[1],n={coord:t,x:e.x0,y:e.y0},i={coord:a,x:e.x1,y:e.y1};return yi(r,"cartesian2d")?t&&a&&(kb(1,t,a)||kb(0,t,a))?!0:jU(r,n,i):yl(r,n)||yl(r,i)}function Ob(r,e,t,a,n){var i=a.coordinateSystem,o=r.getItemModel(e),s,l=W(o.get(t[0]),n.getWidth()),u=W(o.get(t[1]),n.getHeight());if(!isNaN(l)&&!isNaN(u))s=[l,u];else{if(a.getMarkerPosition){var f=r.getValues(["x0","y0"],e),h=r.getValues(["x1","y1"],e),v=i.clampData(f),c=i.clampData(h),p=[];t[0]==="x0"?p[0]=v[0]>c[0]?h[0]:f[0]:p[0]=v[0]>c[0]?f[0]:h[0],t[1]==="y0"?p[1]=v[1]>c[1]?h[1]:f[1]:p[1]=v[1]>c[1]?f[1]:h[1],s=a.getMarkerPosition(p,t,!0)}else{var d=r.get(t[0],e),g=r.get(t[1],e),y=[d,g];i.clampData&&i.clampData(y,y),s=i.dataToPoint(y,!0)}if(yi(i,"cartesian2d")){var m=i.getAxis("x"),_=i.getAxis("y"),d=r.get(t[0],e),g=r.get(t[1],e);ah(d)?s[0]=m.toGlobalCoord(m.getExtent()[t[0]==="x0"?0:1]):ah(g)&&(s[1]=_.toGlobalCoord(_.getExtent()[t[1]==="y0"?0:1]))}isNaN(l)||(s[0]=l),isNaN(u)||(s[1]=u)}return s}var Nb=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],f8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.updateTransform=function(t,a,n){a.eachSeries(function(i){var o=xa.getMarkerModelFromSeries(i,"markArea");if(o){var s=o.getData();s.each(function(l){var u=G(Nb,function(h){return Ob(s,l,h,i,n)});s.setItemLayout(l,u);var f=s.getItemGraphicEl(l);f.setShape("points",u)})}},this)},e.prototype.renderSeries=function(t,a,n,i){var o=t.coordinateSystem,s=t.id,l=t.getData(),u=this.markerGroupMap,f=u.get(s)||u.set(s,{group:new at});this.group.add(f.group),this.markKeep(f);var h=h8(o,t,a);a.setData(h),h.each(function(v){var c=G(Nb,function(T){return Ob(h,v,T,t,i)}),p=o.getAxis("x").scale,d=o.getAxis("y").scale,g=p.getExtent(),y=d.getExtent(),m=[p.parse(h.get("x0",v)),p.parse(h.get("x1",v))],_=[d.parse(h.get("y0",v)),d.parse(h.get("y1",v))];Ue(m),Ue(_);var S=!(g[0]>m[1]||g[1]_[1]||y[1]<_[0]),b=!S;h.setItemLayout(v,{points:c,allClipped:b});var x=h.getItemModel(v).getModel("itemStyle").getItemStyle(),w=Ll(l,"color");x.fill||(x.fill=w,U(x.fill)&&(x.fill=Xs(x.fill,.4))),x.stroke||(x.stroke=w),h.setItemVisual(v,"style",x)}),h.diff(Wu(f).data).add(function(v){var c=h.getItemLayout(v);if(!c.allClipped){var p=new Se({shape:{points:c.points}});h.setItemGraphicEl(v,p),f.group.add(p)}}).update(function(v,c){var p=Wu(f).data.getItemGraphicEl(c),d=h.getItemLayout(v);d.allClipped?p&&f.group.remove(p):(p?Dt(p,{shape:{points:d.points}},a,v):p=new Se({shape:{points:d.points}}),h.setItemGraphicEl(v,p),f.group.add(p))}).remove(function(v){var c=Wu(f).data.getItemGraphicEl(v);f.group.remove(c)}).execute(),h.eachItemGraphicEl(function(v,c){var p=h.getItemModel(c),d=h.getItemVisual(c,"style");v.useStyle(h.getItemVisual(c,"style")),ve(v,ne(p),{labelFetcher:a,labelDataIndex:c,defaultText:h.getName(c)||"",inheritColor:U(d.fill)?Xs(d.fill,1):"#000"}),he(v,p),Wt(v,null,null,p.get(["emphasis","disabled"])),nt(v).dataModel=a}),Wu(f).data=h,f.group.silent=a.get("silent")||t.get("silent")},e.type="markArea",e}(mm);function h8(r,e,t){var a,n,i=["x0","y0","x1","y1"];if(r){var o=G(r&&r.dimensions,function(u){var f=e.getData(),h=f.getDimensionInfo(f.mapDimension(u))||{};return V(V({},h),{name:u,ordinalMeta:null})});n=G(i,function(u,f){return{name:u,type:o[f%2].type}}),a=new Le(n,t)}else n=[{name:"value",type:"float"}],a=new Le(n,t);var s=G(t.get("data"),it(l8,e,r,t));r&&(s=Pt(s,it(u8,r)));var l=r?function(u,f,h,v){var c=u.coord[Math.floor(v/2)][v%2];return Za(c,n[v])}:function(u,f,h,v){return Za(u.value,n[v])};return a.initData(s,null,l),a.hasItemOption=!0,a}function v8(r){r.registerComponentModel(s8),r.registerComponentView(f8),r.registerPreprocessor(function(e){gm(e.series,"markArea")&&(e.markArea=e.markArea||{})})}var c8=function(r,e){if(e==="all")return{type:"all",title:r.getLocaleModel().get(["legend","selector","all"])};if(e==="inverse")return{type:"inverse",title:r.getLocaleModel().get(["legend","selector","inverse"])}},tg=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.layoutMode={type:"box",ignoreSize:!0},t}return e.prototype.init=function(t,a,n){this.mergeDefaultAndTheme(t,n),t.selected=t.selected||{},this._updateSelector(t)},e.prototype.mergeOption=function(t,a){r.prototype.mergeOption.call(this,t,a),this._updateSelector(t)},e.prototype._updateSelector=function(t){var a=t.selector,n=this.ecModel;a===!0&&(a=t.selector=["all","inverse"]),z(a)&&C(a,function(i,o){U(i)&&(i={type:i}),a[o]=st(i,c8(n,i.type))})},e.prototype.optionUpdated=function(){this._updateData(this.ecModel);var t=this._data;if(t[0]&&this.get("selectedMode")==="single"){for(var a=!1,n=0;n=0},e.prototype.getOrient=function(){return this.get("orient")==="vertical"?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(mt),Bi=it,eg=C,Uu=at,FI=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.newlineDisabled=!1,t}return e.prototype.init=function(){this.group.add(this._contentGroup=new Uu),this.group.add(this._selectorGroup=new Uu),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,a,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),!!t.get("show",!0)){var o=t.get("align"),s=t.get("orient");(!o||o==="auto")&&(o=t.get("left")==="right"&&s==="vertical"?"right":"left");var l=t.get("selector",!0),u=t.get("selectorPosition",!0);l&&(!u||u==="auto")&&(u=s==="horizontal"?"end":"start"),this.renderInner(o,t,a,n,l,s,u);var f=t.getBoxLayoutParams(),h={width:n.getWidth(),height:n.getHeight()},v=t.get("padding"),c=jt(f,h,v),p=this.layoutInner(t,o,c,i,l,u),d=jt(Q({width:p.width,height:p.height},f),h,v);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=II(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,a,n,i,o,s,l){var u=this.getContentGroup(),f=Z(),h=a.get("selectedMode"),v=[];n.eachRawSeries(function(c){!c.get("legendHoverLink")&&v.push(c.id)}),eg(a.getData(),function(c,p){var d=c.get("name");if(!this.newlineDisabled&&(d===""||d===` +`)){var g=new Uu;g.newline=!0,u.add(g);return}var y=n.getSeriesByName(d)[0];if(!f.get(d))if(y){var m=y.getData(),_=m.getVisual("legendLineStyle")||{},S=m.getVisual("legendIcon"),b=m.getVisual("style"),x=this._createItem(y,d,p,c,a,t,_,b,S,h,i);x.on("click",Bi(Bb,d,null,i,v)).on("mouseover",Bi(rg,y.name,null,i,v)).on("mouseout",Bi(ag,y.name,null,i,v)),n.ssr&&x.eachChild(function(w){var T=nt(w);T.seriesIndex=y.seriesIndex,T.dataIndex=p,T.ssrType="legend"}),f.set(d,!0)}else n.eachRawSeries(function(w){if(!f.get(d)&&w.legendVisualProvider){var T=w.legendVisualProvider;if(!T.containName(d))return;var A=T.indexOfName(d),D=T.getItemVisual(A,"style"),M=T.getItemVisual(A,"legendIcon"),I=Ie(D.fill);I&&I[3]===0&&(I[3]=.2,D=V(V({},D),{fill:Tr(I,"rgba")}));var L=this._createItem(w,d,p,c,a,t,{},D,M,h,i);L.on("click",Bi(Bb,null,d,i,v)).on("mouseover",Bi(rg,null,d,i,v)).on("mouseout",Bi(ag,null,d,i,v)),n.ssr&&L.eachChild(function(P){var R=nt(P);R.seriesIndex=w.seriesIndex,R.dataIndex=p,R.ssrType="legend"}),f.set(d,!0)}},this)},this),o&&this._createSelector(o,a,i,s,l)},e.prototype._createSelector=function(t,a,n,i,o){var s=this.getSelectorGroup();eg(t,function(u){var f=u.type,h=new bt({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:f==="all"?"legendAllSelect":"legendInverseSelect"})}});s.add(h);var v=a.getModel("selectorLabel"),c=a.getModel(["emphasis","selectorLabel"]);ve(h,{normal:v,emphasis:c},{defaultText:u.title}),Ya(h)})},e.prototype._createItem=function(t,a,n,i,o,s,l,u,f,h,v){var c=t.visualDrawType,p=o.get("itemWidth"),d=o.get("itemHeight"),g=o.isSelected(a),y=i.get("symbolRotate"),m=i.get("symbolKeepAspect"),_=i.get("icon");f=_||f||"roundRect";var S=p8(f,i,l,u,c,g,v),b=new Uu,x=i.getModel("textStyle");if(K(t.getLegendIcon)&&(!_||_==="inherit"))b.add(t.getLegendIcon({itemWidth:p,itemHeight:d,icon:f,iconRotate:y,itemStyle:S.itemStyle,lineStyle:S.lineStyle,symbolKeepAspect:m}));else{var w=_==="inherit"&&t.getData().getVisual("symbol")?y==="inherit"?t.getData().getVisual("symbolRotate"):y:0;b.add(d8({itemWidth:p,itemHeight:d,icon:f,iconRotate:w,itemStyle:S.itemStyle,lineStyle:S.lineStyle,symbolKeepAspect:m}))}var T=s==="left"?p+5:-5,A=s,D=o.get("formatter"),M=a;U(D)&&D?M=D.replace("{name}",a??""):K(D)&&(M=D(a));var I=g?x.getTextColor():i.get("inactiveColor");b.add(new bt({style:Nt(x,{text:M,x:T,y:d/2,fill:I,align:A,verticalAlign:"middle"},{inheritColor:I})}));var L=new wt({shape:b.getBoundingRect(),style:{fill:"transparent"}}),P=i.getModel("tooltip");return P.get("show")&&To({el:L,componentModel:o,itemName:a,itemTooltipOption:P.option}),b.add(L),b.eachChild(function(R){R.silent=!0}),L.silent=!h,this.getContentGroup().add(b),Ya(b),b.__legendDataIndex=n,b},e.prototype.layoutInner=function(t,a,n,i,o,s){var l=this.getContentGroup(),u=this.getSelectorGroup();Qn(t.get("orient"),l,t.get("itemGap"),n.width,n.height);var f=l.getBoundingRect(),h=[-f.x,-f.y];if(u.markRedraw(),l.markRedraw(),o){Qn("horizontal",u,t.get("selectorItemGap",!0));var v=u.getBoundingRect(),c=[-v.x,-v.y],p=t.get("selectorButtonGap",!0),d=t.getOrient().index,g=d===0?"width":"height",y=d===0?"height":"width",m=d===0?"y":"x";s==="end"?c[d]+=f[g]+p:h[d]+=v[g]+p,c[1-d]+=f[y]/2-v[y]/2,u.x=c[0],u.y=c[1],l.x=h[0],l.y=h[1];var _={x:0,y:0};return _[g]=f[g]+p+v[g],_[y]=Math.max(f[y],v[y]),_[m]=Math.min(0,v[m]+c[1-d]),_}else return l.x=h[0],l.y=h[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(zt);function p8(r,e,t,a,n,i,o){function s(g,y){g.lineWidth==="auto"&&(g.lineWidth=y.lineWidth>0?2:0),eg(g,function(m,_){g[_]==="inherit"&&(g[_]=y[_])})}var l=e.getModel("itemStyle"),u=l.getItemStyle(),f=r.lastIndexOf("empty",0)===0?"fill":"stroke",h=l.getShallow("decal");u.decal=!h||h==="inherit"?a.decal:uo(h,o),u.fill==="inherit"&&(u.fill=a[n]),u.stroke==="inherit"&&(u.stroke=a[f]),u.opacity==="inherit"&&(u.opacity=(n==="fill"?a:t).opacity),s(u,a);var v=e.getModel("lineStyle"),c=v.getLineStyle();if(s(c,t),u.fill==="auto"&&(u.fill=a.fill),u.stroke==="auto"&&(u.stroke=a.fill),c.stroke==="auto"&&(c.stroke=a.fill),!i){var p=e.get("inactiveBorderWidth"),d=u[f];u.lineWidth=p==="auto"?a.lineWidth>0&&d?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),c.stroke=v.get("inactiveColor"),c.lineWidth=v.get("inactiveWidth")}return{itemStyle:u,lineStyle:c}}function d8(r){var e=r.icon||"roundRect",t=Zt(e,0,0,r.itemWidth,r.itemHeight,r.itemStyle.fill,r.symbolKeepAspect);return t.setStyle(r.itemStyle),t.rotation=(r.iconRotate||0)*Math.PI/180,t.setOrigin([r.itemWidth/2,r.itemHeight/2]),e.indexOf("empty")>-1&&(t.style.stroke=t.style.fill,t.style.fill="#fff",t.style.lineWidth=2),t}function Bb(r,e,t,a){ag(r,e,t,a),t.dispatchAction({type:"legendToggleSelect",name:r??e}),rg(r,e,t,a)}function HI(r){for(var e=r.getZr().storage.getDisplayList(),t,a=0,n=e.length;an[o],g=[-c.x,-c.y];a||(g[i]=f[u]);var y=[0,0],m=[-p.x,-p.y],_=ot(t.get("pageButtonGap",!0),t.get("itemGap",!0));if(d){var S=t.get("pageButtonPosition",!0);S==="end"?m[i]+=n[o]-p[o]:y[i]+=p[o]+_}m[1-i]+=c[s]/2-p[s]/2,f.setPosition(g),h.setPosition(y),v.setPosition(m);var b={x:0,y:0};if(b[o]=d?n[o]:c[o],b[s]=Math.max(c[s],p[s]),b[l]=Math.min(0,p[l]+m[1-i]),h.__rectSize=n[o],d){var x={x:0,y:0};x[o]=Math.max(n[o]-p[o]-_,0),x[s]=b[s],h.setClipPath(new wt({shape:x})),h.__rectSize=x[o]}else v.eachChild(function(T){T.attr({invisible:!0,silent:!0})});var w=this._getPageInfo(t);return w.pageIndex!=null&&Dt(f,{x:w.contentPosition[0],y:w.contentPosition[1]},d?t:null),this._updatePageInfoView(t,w),b},e.prototype._pageGo=function(t,a,n){var i=this._getPageInfo(a)[t];i!=null&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:a.id})},e.prototype._updatePageInfoView=function(t,a){var n=this._controllerGroup;C(["pagePrev","pageNext"],function(f){var h=f+"DataIndex",v=a[h]!=null,c=n.childOfName(f);c&&(c.setStyle("fill",v?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),c.cursor=v?"pointer":"default")});var i=n.childOfName("pageText"),o=t.get("pageFormatter"),s=a.pageIndex,l=s!=null?s+1:0,u=a.pageCount;i&&o&&i.setStyle("text",U(o)?o.replace("{current}",l==null?"":l+"").replace("{total}",u==null?"":u+""):o({current:l,total:u}))},e.prototype._getPageInfo=function(t){var a=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,o=t.getOrient().index,s=fp[o],l=hp[o],u=this._findTargetItemIndex(a),f=n.children(),h=f[u],v=f.length,c=v?1:0,p={contentPosition:[n.x,n.y],pageCount:c,pageIndex:c-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!h)return p;var d=S(h);p.contentPosition[o]=-d.s;for(var g=u+1,y=d,m=d,_=null;g<=v;++g)_=S(f[g]),(!_&&m.e>y.s+i||_&&!b(_,y.s))&&(m.i>y.i?y=m:y=_,y&&(p.pageNextDataIndex==null&&(p.pageNextDataIndex=y.i),++p.pageCount)),m=_;for(var g=u-1,y=d,m=d,_=null;g>=-1;--g)_=S(f[g]),(!_||!b(m,_.s))&&y.i=w&&x.s<=w+i}},e.prototype._findTargetItemIndex=function(t){if(!this._showController)return 0;var a,n=this.getContentGroup(),i;return n.eachChild(function(o,s){var l=o.__legendDataIndex;i==null&&l!=null&&(i=s),l===t&&(a=s)}),a??i},e.type="legend.scroll",e}(FI);function S8(r){r.registerAction("legendScroll","legendscroll",function(e,t){var a=e.scrollDataIndex;a!=null&&t.eachComponent({mainType:"legend",subType:"scroll",query:e},function(n){n.setScrollDataIndex(a)})})}function x8(r){gt(WI),r.registerComponentModel(m8),r.registerComponentView(_8),S8(r)}function b8(r){gt(WI),gt(x8)}var w8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="dataZoom.inside",e.defaultOption=on(dl.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(dl),_m=Tt();function T8(r,e,t){_m(r).coordSysRecordMap.each(function(a){var n=a.dataZoomInfoMap.get(e.uid);n&&(n.getRange=t)})}function A8(r,e){for(var t=_m(r).coordSysRecordMap,a=t.keys(),n=0;na[t+e]&&(e=s),n=n&&o.get("preventDefaultMouseMove",!0)}),{controlType:e,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!n}}}function L8(r){r.registerProcessor(r.PRIORITY.PROCESSOR.FILTER,function(e,t){var a=_m(t),n=a.coordSysRecordMap||(a.coordSysRecordMap=Z());n.each(function(i){i.dataZoomInfoMap=null}),e.eachComponent({mainType:"dataZoom",subType:"inside"},function(i){var o=CI(i);C(o.infoList,function(s){var l=s.model.uid,u=n.get(l)||n.set(l,C8(t,s.model)),f=u.dataZoomInfoMap||(u.dataZoomInfoMap=Z());f.set(i.uid,{dzReferCoordSysInfo:s,model:i,getRange:null})})}),n.each(function(i){var o=i.controller,s,l=i.dataZoomInfoMap;if(l){var u=l.keys()[0];u!=null&&(s=l.get(u))}if(!s){UI(n,i);return}var f=I8(l);o.enable(f.controlType,f.opt),o.setPointerChecker(i.containsPoint),Po(i,"dispatchAction",s.model.get("throttle",!0),"fixRate")})})}var P8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type="dataZoom.inside",t}return e.prototype.render=function(t,a,n){if(r.prototype.render.apply(this,arguments),t.noTarget()){this._clear();return}this.range=t.getPercentRange(),T8(n,t,{pan:X(vp.pan,this),zoom:X(vp.zoom,this),scrollMove:X(vp.scrollMove,this)})},e.prototype.dispose=function(){this._clear(),r.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){A8(this.api,this.dataZoomModel),this.range=null},e.type="dataZoom.inside",e}(fm),vp={zoom:function(r,e,t,a){var n=this.range,i=n.slice(),o=r.axisModels[0];if(o){var s=cp[e](null,[a.originX,a.originY],o,t,r),l=(s.signal>0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(i[1]-i[0])+i[0],u=Math.max(1/a.scale,0);i[0]=(i[0]-l)*u+l,i[1]=(i[1]-l)*u+l;var f=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();if(_i(0,i,[0,100],0,f.minSpan,f.maxSpan),this.range=i,n[0]!==i[0]||n[1]!==i[1])return i}},pan:Gb(function(r,e,t,a,n,i){var o=cp[a]([i.oldX,i.oldY],[i.newX,i.newY],e,n,t);return o.signal*(r[1]-r[0])*o.pixel/o.pixelLength}),scrollMove:Gb(function(r,e,t,a,n,i){var o=cp[a]([0,0],[i.scrollDelta,i.scrollDelta],e,n,t);return o.signal*(r[1]-r[0])*i.scrollDelta})};function Gb(r){return function(e,t,a,n){var i=this.range,o=i.slice(),s=e.axisModels[0];if(s){var l=r(o,s,e,t,a,n);if(_i(l,o,[0,100],"all"),this.range=o,i[0]!==o[0]||i[1]!==o[1])return o}}}var cp={grid:function(r,e,t,a,n){var i=t.axis,o={},s=n.model.coordinateSystem.getRect();return r=r||[0,0],i.dim==="x"?(o.pixel=e[0]-r[0],o.pixelLength=s.width,o.pixelStart=s.x,o.signal=i.inverse?1:-1):(o.pixel=e[1]-r[1],o.pixelLength=s.height,o.pixelStart=s.y,o.signal=i.inverse?-1:1),o},polar:function(r,e,t,a,n){var i=t.axis,o={},s=n.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return r=r?s.pointToCoord(r):[0,0],e=s.pointToCoord(e),t.mainType==="radiusAxis"?(o.pixel=e[0]-r[0],o.pixelLength=l[1]-l[0],o.pixelStart=l[0],o.signal=i.inverse?1:-1):(o.pixel=e[1]-r[1],o.pixelLength=u[1]-u[0],o.pixelStart=u[0],o.signal=i.inverse?-1:1),o},singleAxis:function(r,e,t,a,n){var i=t.axis,o=n.model.coordinateSystem.getRect(),s={};return r=r||[0,0],i.orient==="horizontal"?(s.pixel=e[0]-r[0],s.pixelLength=o.width,s.pixelStart=o.x,s.signal=i.inverse?1:-1):(s.pixel=e[1]-r[1],s.pixelLength=o.height,s.pixelStart=o.y,s.signal=i.inverse?-1:1),s}};function YI(r){hm(r),r.registerComponentModel(w8),r.registerComponentView(P8),L8(r)}var R8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=on(dl.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),e}(dl),fs=wt,Fb=7,E8=1,pp=30,k8=7,hs="horizontal",Hb="vertical",O8=5,N8=["line","bar","candlestick","scatter"],B8={easing:"cubicOut",duration:100,delay:0},V8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t._displayables={},t}return e.prototype.init=function(t,a){this.api=a,this._onBrush=X(this._onBrush,this),this._onBrushEnd=X(this._onBrushEnd,this)},e.prototype.render=function(t,a,n,i){if(r.prototype.render.apply(this,arguments),Po(this,"_dispatchZoomAction",t.get("throttle"),"fixRate"),this._orient=t.getOrient(),t.get("show")===!1){this.group.removeAll();return}if(t.noTarget()){this._clear(),this.group.removeAll();return}(!i||i.type!=="dataZoom"||i.from!==this.uid)&&this._buildView(),this._updateView()},e.prototype.dispose=function(){this._clear(),r.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){rl(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var a=this._displayables.sliderGroup=new at;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(a),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,a=this.api,n=t.get("brushSelect"),i=n?k8:0,o=this._findCoordRect(),s={width:a.getWidth(),height:a.getHeight()},l=this._orient===hs?{right:s.width-o.x-o.width,top:s.height-pp-Fb-i,width:o.width,height:pp}:{right:Fb,top:o.y,width:pp,height:o.height},u=Do(t.option);C(["right","top","width","height"],function(h){u[h]==="ph"&&(u[h]=l[h])});var f=jt(u,s);this._location={x:f.x,y:f.y},this._size=[f.width,f.height],this._orient===Hb&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,a=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),o=i&&i.get("inverse"),s=this._displayables.sliderGroup,l=(this._dataShadowInfo||{}).otherAxisInverse;s.attr(n===hs&&!o?{scaleY:l?1:-1,scaleX:1}:n===hs&&o?{scaleY:l?1:-1,scaleX:-1}:n===Hb&&!o?{scaleY:l?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:l?-1:1,scaleX:-1,rotation:Math.PI/2});var u=t.getBoundingRect([s]);t.x=a.x-u.x,t.y=a.y-u.y,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,a=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new fs({silent:!0,shape:{x:0,y:0,width:a[0],height:a[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var o=new fs({shape:{x:0,y:0,width:a[0],height:a[1]},style:{fill:"transparent"},z2:0,onclick:X(this._onClickPanel,this)}),s=this.api.getZr();i?(o.on("mousedown",this._onBrushStart,this),o.cursor="crosshair",s.on("mousemove",this._onBrush),s.on("mouseup",this._onBrushEnd)):(s.off("mousemove",this._onBrush),s.off("mouseup",this._onBrushEnd)),n.add(o)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],!t)return;var a=this._size,n=this._shadowSize||[],i=t.series,o=i.getRawData(),s=i.getShadowDim&&i.getShadowDim(),l=s&&o.getDimensionInfo(s)?i.getShadowDim():t.otherDim;if(l==null)return;var u=this._shadowPolygonPts,f=this._shadowPolylinePts;if(o!==this._shadowData||l!==this._shadowDim||a[0]!==n[0]||a[1]!==n[1]){var h=o.getDataExtent(l),v=(h[1]-h[0])*.3;h=[h[0]-v,h[1]+v];var c=[0,a[1]],p=[0,a[0]],d=[[a[0],0],[0,0]],g=[],y=p[1]/(o.count()-1),m=0,_=Math.round(o.count()/a[0]),S;o.each([l],function(A,D){if(_>0&&D%_){m+=y;return}var M=A==null||isNaN(A)||A==="",I=M?0:Lt(A,h,c,!0);M&&!S&&D?(d.push([d[d.length-1][0],0]),g.push([g[g.length-1][0],0])):!M&&S&&(d.push([m,0]),g.push([m,0])),d.push([m,I]),g.push([m,I]),m+=y,S=M}),u=this._shadowPolygonPts=d,f=this._shadowPolylinePts=g}this._shadowData=o,this._shadowDim=l,this._shadowSize=[a[0],a[1]];var b=this.dataZoomModel;function x(A){var D=b.getModel(A?"selectedDataBackground":"dataBackground"),M=new at,I=new Se({shape:{points:u},segmentIgnoreThreshold:1,style:D.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),L=new be({shape:{points:f},segmentIgnoreThreshold:1,style:D.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return M.add(I),M.add(L),M}for(var w=0;w<3;w++){var T=x(w===1);this._displayables.sliderGroup.add(T),this._displayables.dataShadowSegs.push(T)}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,a=t.get("showDataShadow");if(a!==!1){var n,i=this.ecModel;return t.eachTargetAxis(function(o,s){var l=t.getAxisProxy(o,s).getTargetSeriesModels();C(l,function(u){if(!n&&!(a!==!0&&vt(N8,u.get("type"))<0)){var f=i.getComponent(Ha(o),s).axis,h=z8(o),v,c=u.coordinateSystem;h!=null&&c.getOtherAxis&&(v=c.getOtherAxis(f).inverse),h=u.getData().mapDimension(h),n={thisAxis:f,series:u,thisDim:o,otherDim:h,otherAxisInverse:v}}},this)},this),n}},e.prototype._renderHandle=function(){var t=this.group,a=this._displayables,n=a.handles=[null,null],i=a.handleLabels=[null,null],o=this._displayables.sliderGroup,s=this._size,l=this.dataZoomModel,u=this.api,f=l.get("borderRadius")||0,h=l.get("brushSelect"),v=a.filler=new fs({silent:h,style:{fill:l.get("fillerColor")},textConfig:{position:"inside"}});o.add(v),o.add(new fs({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:s[0],height:s[1],r:f},style:{stroke:l.get("dataBackgroundColor")||l.get("borderColor"),lineWidth:E8,fill:"rgba(0,0,0,0)"}})),C([0,1],function(_){var S=l.get("handleIcon");!Rf[S]&&S.indexOf("path://")<0&&S.indexOf("image://")<0&&(S="path://"+S);var b=Zt(S,-1,0,2,2,null,!0);b.attr({cursor:Wb(this._orient),draggable:!0,drift:X(this._onDragMove,this,_),ondragend:X(this._onDragEnd,this),onmouseover:X(this._showDataInfo,this,!0),onmouseout:X(this._showDataInfo,this,!1),z2:5});var x=b.getBoundingRect(),w=l.get("handleSize");this._handleHeight=W(w,this._size[1]),this._handleWidth=x.width/x.height*this._handleHeight,b.setStyle(l.getModel("handleStyle").getItemStyle()),b.style.strokeNoScale=!0,b.rectHover=!0,b.ensureState("emphasis").style=l.getModel(["emphasis","handleStyle"]).getItemStyle(),Ya(b);var T=l.get("handleColor");T!=null&&(b.style.fill=T),o.add(n[_]=b);var A=l.getModel("textStyle");t.add(i[_]=new bt({silent:!0,invisible:!0,style:Nt(A,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:A.getTextColor(),font:A.getFont()}),z2:10}))},this);var c=v;if(h){var p=W(l.get("moveHandleSize"),s[1]),d=a.moveHandle=new wt({style:l.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:s[1]-.5,height:p}}),g=p*.8,y=a.moveHandleIcon=Zt(l.get("moveHandleIcon"),-g/2,-g/2,g,g,"#fff",!0);y.silent=!0,y.y=s[1]+p/2-.5,d.ensureState("emphasis").style=l.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var m=Math.min(s[1]/2,Math.max(p,10));c=a.moveZone=new wt({invisible:!0,shape:{y:s[1]-m,height:p+m}}),c.on("mouseover",function(){u.enterEmphasis(d)}).on("mouseout",function(){u.leaveEmphasis(d)}),o.add(d),o.add(y),o.add(c)}c.attr({draggable:!0,cursor:Wb(this._orient),drift:X(this._onDragMove,this,"all"),ondragstart:X(this._showDataInfo,this,!0),ondragend:X(this._onDragEnd,this),onmouseover:X(this._showDataInfo,this,!0),onmouseout:X(this._showDataInfo,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),a=this._getViewExtent();this._handleEnds=[Lt(t[0],[0,100],a,!0),Lt(t[1],[0,100],a,!0)]},e.prototype._updateInterval=function(t,a){var n=this.dataZoomModel,i=this._handleEnds,o=this._getViewExtent(),s=n.findRepresentativeAxisProxy().getMinMaxSpan(),l=[0,100];_i(a,i,o,n.get("zoomLock")?"all":t,s.minSpan!=null?Lt(s.minSpan,l,o,!0):null,s.maxSpan!=null?Lt(s.maxSpan,l,o,!0):null);var u=this._range,f=this._range=Ue([Lt(i[0],o,l,!0),Lt(i[1],o,l,!0)]);return!u||u[0]!==f[0]||u[1]!==f[1]},e.prototype._updateView=function(t){var a=this._displayables,n=this._handleEnds,i=Ue(n.slice()),o=this._size;C([0,1],function(c){var p=a.handles[c],d=this._handleHeight;p.attr({scaleX:d/2,scaleY:d/2,x:n[c]+(c?-1:1),y:o[1]/2-d/2})},this),a.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:o[1]});var s={x:i[0],width:i[1]-i[0]};a.moveHandle&&(a.moveHandle.setShape(s),a.moveZone.setShape(s),a.moveZone.getBoundingRect(),a.moveHandleIcon&&a.moveHandleIcon.attr("x",s.x+s.width/2));for(var l=a.dataShadowSegs,u=[0,i[0],i[1],o[0]],f=0;fa[0]||n[1]<0||n[1]>a[1])){var i=this._handleEnds,o=(i[0]+i[1])/2,s=this._updateInterval("all",n[0]-o);this._updateView(),s&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var a=t.offsetX,n=t.offsetY;this._brushStart=new ft(a,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var a=this._displayables.brushRect;if(this._brushing=!1,!!a){a.attr("ignore",!0);var n=a.shape,i=+new Date;if(!(i-this._brushStartTime<200&&Math.abs(n.width)<5)){var o=this._getViewExtent(),s=[0,100];this._range=Ue([Lt(n.x,o,s,!0),Lt(n.x+n.width,o,s,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(pa(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,a){var n=this._displayables,i=this.dataZoomModel,o=n.brushRect;o||(o=n.brushRect=new fs({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(o)),o.attr("ignore",!1);var s=this._brushStart,l=this._displayables.sliderGroup,u=l.transformCoordToLocal(t,a),f=l.transformCoordToLocal(s.x,s.y),h=this._size;u[0]=Math.max(Math.min(h[0],u[0]),0),o.setShape({x:f[0],y:0,width:u[0]-f[0],height:h[1]})},e.prototype._dispatchZoomAction=function(t){var a=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?B8:null,start:a[0],end:a[1]})},e.prototype._findCoordRect=function(){var t,a=CI(this.dataZoomModel).infoList;if(!t&&a.length){var n=a[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),o=this.api.getHeight();t={x:i*.2,y:o*.2,width:i*.6,height:o*.6}}return t},e.type="dataZoom.slider",e}(fm);function z8(r){var e={x:"y",y:"x",radius:"angle",angle:"radius"};return e[r]}function Wb(r){return r==="vertical"?"ns-resize":"ew-resize"}function XI(r){r.registerComponentModel(R8),r.registerComponentView(V8),hm(r)}function G8(r){gt(YI),gt(XI)}var $I={get:function(r,e,t){var a=et((F8[r]||{})[e]);return t&&z(a)?a[a.length-1]:a}},F8={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},Ub=ae.mapVisual,H8=ae.eachVisual,W8=z,Yb=C,U8=Ue,Y8=Lt,nh=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t.stateList=["inRange","outOfRange"],t.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],t.layoutMode={type:"box",ignoreSize:!0},t.dataBound=[-1/0,1/0],t.targetVisuals={},t.controllerVisuals={},t}return e.prototype.init=function(t,a,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,a){var n=this.option;!a&&NI(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var a=this.stateList;t=X(t,this),this.controllerVisuals=jd(this.option.controller,a,t),this.targetVisuals=jd(this.option.target,a,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this.option.seriesIndex,a=[];return t==null||t==="all"?this.ecModel.eachSeries(function(n,i){a.push(i)}):a=Et(t),a},e.prototype.eachTargetSeries=function(t,a){C(this.getTargetSeriesIndices(),function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(a,i)},this)},e.prototype.isTargetSeries=function(t){var a=!1;return this.eachTargetSeries(function(n){n===t&&(a=!0)}),a},e.prototype.formatValueText=function(t,a,n){var i=this.option,o=i.precision,s=this.dataBound,l=i.formatter,u;n=n||["<",">"],z(t)&&(t=t.slice(),u=!0);var f=a?t:u?[h(t[0]),h(t[1])]:h(t);if(U(l))return l.replace("{value}",u?f[0]:f).replace("{value2}",u?f[1]:f);if(K(l))return u?l(t[0],t[1]):l(t);if(u)return t[0]===s[0]?n[0]+" "+f[1]:t[1]===s[1]?n[1]+" "+f[0]:f[0]+" - "+f[1];return f;function h(v){return v===s[0]?"min":v===s[1]?"max":(+v).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,a=U8([t.min,t.max]);this._dataExtent=a},e.prototype.getDataDimensionIndex=function(t){var a=this.option.dimension;if(a!=null)return t.getDimensionIndex(a);for(var n=t.dimensions,i=n.length-1;i>=0;i--){var o=n[i],s=t.getDimensionInfo(o);if(!s.isCalculationCoord)return s.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,a=this.option,n={inRange:a.inRange,outOfRange:a.outOfRange},i=a.target||(a.target={}),o=a.controller||(a.controller={});st(i,n),st(o,n);var s=this.isCategory();l.call(this,i),l.call(this,o),u.call(this,i,"inRange","outOfRange"),f.call(this,o);function l(h){W8(a.color)&&!h.inRange&&(h.inRange={color:a.color.slice().reverse()}),h.inRange=h.inRange||{color:t.get("gradientColor")}}function u(h,v,c){var p=h[v],d=h[c];p&&!d&&(d=h[c]={},Yb(p,function(g,y){if(ae.isValidType(y)){var m=$I.get(y,"inactive",s);m!=null&&(d[y]=m,y==="color"&&!d.hasOwnProperty("opacity")&&!d.hasOwnProperty("colorAlpha")&&(d.opacity=[0,0]))}}))}function f(h){var v=(h.inRange||{}).symbol||(h.outOfRange||{}).symbol,c=(h.inRange||{}).symbolSize||(h.outOfRange||{}).symbolSize,p=this.get("inactiveColor"),d=this.getItemSymbol(),g=d||"roundRect";Yb(this.stateList,function(y){var m=this.itemSize,_=h[y];_||(_=h[y]={color:s?p:[p]}),_.symbol==null&&(_.symbol=v&&et(v)||(s?g:[g])),_.symbolSize==null&&(_.symbolSize=c&&et(c)||(s?m[0]:[m[0],m[0]])),_.symbol=Ub(_.symbol,function(x){return x==="none"?g:x});var S=_.symbolSize;if(S!=null){var b=-1/0;H8(S,function(x){x>b&&(b=x)}),_.symbolSize=Ub(S,function(x){return Y8(x,[0,b],[0,m[0]],!0)})}},this)}},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},e}(mt),Xb=[20,140],X8=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.optionUpdated=function(t,a){r.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual(function(n){n.mappingMethod="linear",n.dataExtent=this.getExtent()}),this._resetRange()},e.prototype.resetItemSize=function(){r.prototype.resetItemSize.apply(this,arguments);var t=this.itemSize;(t[0]==null||isNaN(t[0]))&&(t[0]=Xb[0]),(t[1]==null||isNaN(t[1]))&&(t[1]=Xb[1])},e.prototype._resetRange=function(){var t=this.getExtent(),a=this.option.range;!a||a.auto?(t.auto=1,this.option.range=t):z(a)&&(a[0]>a[1]&&a.reverse(),a[0]=Math.max(a[0],t[0]),a[1]=Math.min(a[1],t[1]))},e.prototype.completeVisualOption=function(){r.prototype.completeVisualOption.apply(this,arguments),C(this.stateList,function(t){var a=this.option.controller[t].symbolSize;a&&a[0]!==a[1]&&(a[0]=a[1]/3)},this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),a=Ue((this.get("range")||[]).slice());return a[0]>t[1]&&(a[0]=t[1]),a[1]>t[1]&&(a[1]=t[1]),a[0]=n[1]||t<=a[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var a=[];return this.eachTargetSeries(function(n){var i=[],o=n.getData();o.each(this.getDataDimensionIndex(o),function(s,l){t[0]<=s&&s<=t[1]&&i.push(l)},this),a.push({seriesId:n.id,dataIndex:i})},this),a},e.prototype.getVisualMeta=function(t){var a=$b(this,"outOfRange",this.getExtent()),n=$b(this,"inRange",this.option.range.slice()),i=[];function o(c,p){i.push({value:c,color:t(c,p)})}for(var s=0,l=0,u=n.length,f=a.length;lt[1])break;i.push({color:this.getControllerVisual(l,"color",a),offset:s/n})}return i.push({color:this.getControllerVisual(t[1],"color",a),offset:1}),i},e.prototype._createBarPoints=function(t,a){var n=this.visualMapModel.itemSize;return[[n[0]-a[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-a[1],t[1]]]},e.prototype._createBarGroup=function(t){var a=this._orient,n=this.visualMapModel.get("inverse");return new at(a==="horizontal"&&!n?{scaleX:t==="bottom"?1:-1,rotation:Math.PI/2}:a==="horizontal"&&n?{scaleX:t==="bottom"?-1:1,rotation:-Math.PI/2}:a==="vertical"&&!n?{scaleX:t==="left"?1:-1,scaleY:-1}:{scaleX:t==="left"?1:-1})},e.prototype._updateHandle=function(t,a){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,o=n.handleThumbs,s=n.handleLabels,l=i.itemSize,u=i.getExtent();$8([0,1],function(f){var h=o[f];h.setStyle("fill",a.handlesColor[f]),h.y=t[f];var v=Br(t[f],[0,l[1]],u,!0),c=this.getControllerVisual(v,"symbolSize");h.scaleX=h.scaleY=c/l[0],h.x=l[0]-c/2;var p=Ar(n.handleLabelPoints[f],Xa(h,this.group));s[f].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[f]),verticalAlign:"middle",align:this._orient==="vertical"?this._applyTransform("left",n.mainGroup):"center"})},this)}},e.prototype._showIndicator=function(t,a,n,i){var o=this.visualMapModel,s=o.getExtent(),l=o.itemSize,u=[0,l[1]],f=this._shapes,h=f.indicator;if(h){h.attr("invisible",!1);var v={convertOpacityToAlpha:!0},c=this.getControllerVisual(t,"color",v),p=this.getControllerVisual(t,"symbolSize"),d=Br(t,s,u,!0),g=l[0]-p/2,y={x:h.x,y:h.y};h.y=d,h.x=g;var m=Ar(f.indicatorLabelPoint,Xa(h,this.group)),_=f.indicatorLabel;_.attr("invisible",!1);var S=this._applyTransform("left",f.mainGroup),b=this._orient,x=b==="horizontal";_.setStyle({text:(n||"")+o.formatValueText(a),verticalAlign:x?S:"middle",align:x?"center":S});var w={x:g,y:d,style:{fill:c}},T={style:{x:m[0],y:m[1]}};if(o.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var A={duration:100,easing:"cubicInOut",additive:!0};h.x=y.x,h.y=y.y,h.animateTo(w,A),_.animateTo(T,A)}else h.attr(w),_.attr(T);this._firstShowIndicator=!1;var D=this._shapes.handleLabels;if(D)for(var M=0;Mo[1]&&(h[1]=1/0),a&&(h[0]===-1/0?this._showIndicator(f,h[1],"< ",l):h[1]===1/0?this._showIndicator(f,h[0],"> ",l):this._showIndicator(f,f,"≈ ",l));var v=this._hoverLinkDataIndices,c=[];(a||jb(n))&&(c=this._hoverLinkDataIndices=n.findTargetDataIndices(h));var p=aP(v,c);this._dispatchHighDown("downplay",ff(p[0],n)),this._dispatchHighDown("highlight",ff(p[1],n))}},e.prototype._hoverLinkFromSeriesMouseOver=function(t){var a;if(Yn(t.target,function(l){var u=nt(l);if(u.dataIndex!=null)return a=u,!0},!0),!!a){var n=this.ecModel.getSeriesByIndex(a.seriesIndex),i=this.visualMapModel;if(i.isTargetSeries(n)){var o=n.getData(a.dataType),s=o.getStore().get(i.getDataDimensionIndex(o),a.dataIndex);isNaN(s)||this._showIndicator(s,s)}}},e.prototype._hideIndicator=function(){var t=this._shapes;t.indicator&&t.indicator.attr("invisible",!0),t.indicatorLabel&&t.indicatorLabel.attr("invisible",!0);var a=this._shapes.handleLabels;if(a)for(var n=0;n=0&&(i.dimension=o,a.push(i))}}),r.getData().setVisual("visualMeta",a)}}];function eY(r,e,t,a){for(var n=e.targetVisuals[a],i=ae.prepareVisualTypes(n),o={color:Ll(r.getData(),"color")},s=0,l=i.length;s0:e.splitNumber>0)||e.calculable)?"continuous":"piecewise"}),r.registerAction(Q8,J8),C(tY,function(e){r.registerVisual(r.PRIORITY.VISUAL.COMPONENT,e)}),r.registerPreprocessor(rY))}function jI(r){r.registerComponentModel(X8),r.registerComponentView(K8),KI(r)}var aY=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t._pieceList=[],t}return e.prototype.optionUpdated=function(t,a){r.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var n=this._mode=this._determineMode();this._pieceList=[],nY[this._mode].call(this,this._pieceList),this._resetSelected(t,a);var i=this.option.categories;this.resetVisual(function(o,s){n==="categories"?(o.mappingMethod="category",o.categories=et(i)):(o.dataExtent=this.getExtent(),o.mappingMethod="piecewise",o.pieceList=G(this._pieceList,function(l){return l=et(l),s!=="inRange"&&(l.visual=null),l}))})},e.prototype.completeVisualOption=function(){var t=this.option,a={},n=ae.listVisualTypes(),i=this.isCategory();C(t.pieces,function(s){C(n,function(l){s.hasOwnProperty(l)&&(a[l]=1)})}),C(a,function(s,l){var u=!1;C(this.stateList,function(f){u=u||o(t,f,l)||o(t.target,f,l)},this),!u&&C(this.stateList,function(f){(t[f]||(t[f]={}))[l]=$I.get(l,f==="inRange"?"active":"inactive",i)})},this);function o(s,l,u){return s&&s[l]&&s[l].hasOwnProperty(u)}r.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,a){var n=this.option,i=this._pieceList,o=(a?n:t).selected||{};if(n.selected=o,C(i,function(l,u){var f=this.getSelectedMapKey(l);o.hasOwnProperty(f)||(o[f]=!0)},this),n.selectedMode==="single"){var s=!1;C(i,function(l,u){var f=this.getSelectedMapKey(l);o[f]&&(s?o[f]=!1:s=!0)},this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return this._mode==="categories"?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=et(t)},e.prototype.getValueState=function(t){var a=ae.findPieceIndex(t,this._pieceList);return a!=null&&this.option.selected[this.getSelectedMapKey(this._pieceList[a])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var a=[],n=this._pieceList;return this.eachTargetSeries(function(i){var o=[],s=i.getData();s.each(this.getDataDimensionIndex(s),function(l,u){var f=ae.findPieceIndex(l,n);f===t&&o.push(u)},this),a.push({seriesId:i.id,dataIndex:o})},this),a},e.prototype.getRepresentValue=function(t){var a;if(this.isCategory())a=t.value;else if(t.value!=null)a=t.value;else{var n=t.interval||[];a=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return a},e.prototype.getVisualMeta=function(t){if(this.isCategory())return;var a=[],n=["",""],i=this;function o(f,h){var v=i.getRepresentValue({interval:f});h||(h=i.getValueState(v));var c=t(v,h);f[0]===-1/0?n[0]=c:f[1]===1/0?n[1]=c:a.push({value:f[0],color:c},{value:f[1],color:c})}var s=this._pieceList.slice();if(!s.length)s.push({interval:[-1/0,1/0]});else{var l=s[0].interval[0];l!==-1/0&&s.unshift({interval:[-1/0,l]}),l=s[s.length-1].interval[1],l!==1/0&&s.push({interval:[l,1/0]})}var u=-1/0;return C(s,function(f){var h=f.interval;h&&(h[0]>u&&o([u,h[0]],"outOfRange"),o(h.slice()),u=h[1])},this),{stops:a,outerColors:n}},e.type="visualMap.piecewise",e.defaultOption=on(nh.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(nh),nY={splitNumber:function(r){var e=this.option,t=Math.min(e.precision,20),a=this.getExtent(),n=e.splitNumber;n=Math.max(parseInt(n,10),1),e.splitNumber=n;for(var i=(a[1]-a[0])/n;+i.toFixed(t)!==i&&t<5;)t++;e.precision=t,i=+i.toFixed(t),e.minOpen&&r.push({interval:[-1/0,a[0]],close:[0,0]});for(var o=0,s=a[0];o","≥"][a[0]]];t.text=t.text||this.formatValueText(t.value!=null?t.value:t.interval,!1,n)},this)}};function ew(r,e){var t=r.inverse;(r.orient==="vertical"?!t:t)&&e.reverse()}var iY=function(r){k(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.type=e.type,t}return e.prototype.doRender=function(){var t=this.group;t.removeAll();var a=this.visualMapModel,n=a.get("textGap"),i=a.textStyleModel,o=i.getFont(),s=i.getTextColor(),l=this._getItemAlign(),u=a.itemSize,f=this._getViewData(),h=f.endsText,v=ee(a.get("showLabel",!0),!h);h&&this._renderEndsText(t,h[0],u,v,l),C(f.viewPieceList,function(c){var p=c.piece,d=new at;d.onclick=X(this._onItemClick,this,p),this._enableHoverLink(d,c.indexInModelPieceList);var g=a.getRepresentValue(p);if(this._createItemSymbol(d,g,[0,0,u[0],u[1]]),v){var y=this.visualMapModel.getValueState(g);d.add(new bt({style:{x:l==="right"?-n:u[0]+n,y:u[1]/2,text:p.text,verticalAlign:"middle",align:l,font:o,fill:s,opacity:y==="outOfRange"?.5:1}}))}t.add(d)},this),h&&this._renderEndsText(t,h[1],u,v,l),Qn(a.get("orient"),t,a.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,a){var n=this;t.on("mouseover",function(){return i("highlight")}).on("mouseout",function(){return i("downplay")});var i=function(o){var s=n.visualMapModel;s.option.hoverLink&&n.api.dispatchAction({type:o,batch:ff(s.findTargetDataIndices(a),s)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,a=t.option;if(a.orient==="vertical")return qI(t,this.api,t.itemSize);var n=a.align;return(!n||n==="auto")&&(n="left"),n},e.prototype._renderEndsText=function(t,a,n,i,o){if(a){var s=new at,l=this.visualMapModel.textStyleModel;s.add(new bt({style:Nt(l,{x:i?o==="right"?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?o:"center",text:a})})),t.add(s)}},e.prototype._getViewData=function(){var t=this.visualMapModel,a=G(t.getPieceList(),function(s,l){return{piece:s,indexInModelPieceList:l}}),n=t.get("text"),i=t.get("orient"),o=t.get("inverse");return(i==="horizontal"?o:!o)?a.reverse():n&&(n=n.slice().reverse()),{viewPieceList:a,endsText:n}},e.prototype._createItemSymbol=function(t,a,n){t.add(Zt(this.getControllerVisual(a,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(a,"color")))},e.prototype._onItemClick=function(t){var a=this.visualMapModel,n=a.option,i=n.selectedMode;if(i){var o=et(n.selected),s=a.getSelectedMapKey(t);i==="single"||i===!0?(o[s]=!0,C(o,function(l,u){o[u]=u===s})):o[s]=!o[s],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:o})}},e.type="visualMap.piecewise",e}(ZI);function QI(r){r.registerComponentModel(aY),r.registerComponentView(iY),KI(r)}function oY(r){gt(jI),gt(QI)}var sY={label:{enabled:!0},decal:{show:!1}},rw=Tt(),lY={};function uY(r,e){var t=r.getModel("aria");if(!t.get("enabled"))return;var a=et(sY);st(a.label,r.getLocaleModel().get("aria"),!1),st(t.option,a,!1),n(),i();function n(){var u=t.getModel("decal"),f=u.get("show");if(f){var h=Z();r.eachSeries(function(v){if(!v.isColorBySeries()){var c=h.get(v.type);c||(c={},h.set(v.type,c)),rw(v).scope=c}}),r.eachRawSeries(function(v){if(r.isSeriesFiltered(v))return;if(K(v.enableAriaDecal)){v.enableAriaDecal();return}var c=v.getData();if(v.isColorBySeries()){var m=ad(v.ecModel,v.name,lY,r.getSeriesCount()),_=c.getVisual("decal");c.setVisual("decal",S(_,m))}else{var p=v.getRawData(),d={},g=rw(v).scope;c.each(function(b){var x=c.getRawIndex(b);d[x]=b});var y=p.count();p.each(function(b){var x=d[b],w=p.getName(b)||b+"",T=ad(v.ecModel,w,g,y),A=c.getItemVisual(x,"decal");c.setItemVisual(x,"decal",S(A,T))})}function S(b,x){var w=b?V(V({},x),b):x;return w.dirty=!0,w}})}}function i(){var u=e.getZr().dom;if(u){var f=r.getLocaleModel().get("aria"),h=t.getModel("label");if(h.option=Q(h.option,f),!!h.get("enabled")){if(h.get("description")){u.setAttribute("aria-label",h.get("description"));return}var v=r.getSeriesCount(),c=h.get(["data","maxCount"])||10,p=h.get(["series","maxCount"])||10,d=Math.min(v,p),g;if(!(v<1)){var y=s();if(y){var m=h.get(["general","withTitle"]);g=o(m,{title:y})}else g=h.get(["general","withoutTitle"]);var _=[],S=v>1?h.get(["series","multiple","prefix"]):h.get(["series","single","prefix"]);g+=o(S,{seriesCount:v}),r.eachSeries(function(T,A){if(A1?h.get(["series","multiple",I]):h.get(["series","single",I]),D=o(D,{seriesId:T.seriesIndex,seriesName:T.get("name"),seriesType:l(T.subType)});var L=T.getData();if(L.count()>c){var P=h.get(["data","partialData"]);D+=o(P,{displayCnt:c})}else D+=h.get(["data","allData"]);for(var R=h.get(["data","separator","middle"]),E=h.get(["data","separator","end"]),N=[],O=0;O":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},vY=function(){function r(e){var t=this._condVal=U(e)?new RegExp(e):Aw(e)?e:null;if(t==null){var a="";It(a)}}return r.prototype.evaluate=function(e){var t=typeof e;return U(t)?this._condVal.test(e):Ct(t)?this._condVal.test(e+""):!1},r}(),cY=function(){function r(){}return r.prototype.evaluate=function(){return this.value},r}(),pY=function(){function r(){}return r.prototype.evaluate=function(){for(var e=this.children,t=0;t2&&a.push(n),n=[L,P]}function f(L,P,R,E){qi(L,R)&&qi(P,E)||n.push(L,P,R,E,R,E)}function h(L,P,R,E,N,O){var B=Math.abs(P-L),F=Math.tan(B/4)*4/3,H=PT:M2&&a.push(n),a}function ig(r,e,t,a,n,i,o,s,l,u){if(qi(r,t)&&qi(e,a)&&qi(n,o)&&qi(i,s)){l.push(o,s);return}var f=2/u,h=f*f,v=o-r,c=s-e,p=Math.sqrt(v*v+c*c);v/=p,c/=p;var d=t-r,g=a-e,y=n-o,m=i-s,_=d*d+g*g,S=y*y+m*m;if(_=0&&T=0){l.push(o,s);return}var A=[],D=[];ja(r,t,n,o,.5,A),ja(e,a,i,s,.5,D),ig(A[0],D[0],A[1],D[1],A[2],D[2],A[3],D[3],l,u),ig(A[4],D[4],A[5],D[5],A[6],D[6],A[7],D[7],l,u)}function MY(r,e){var t=ng(r),a=[];e=e||1;for(var n=0;n0)for(var u=0;uMath.abs(u),h=tL([l,u],f?0:1,e),v=(f?s:u)/h.length,c=0;cn,o=tL([a,n],i?0:1,e),s=i?"width":"height",l=i?"height":"width",u=i?"x":"y",f=i?"y":"x",h=r[s]/o.length,v=0;v1?null:new ft(d*l+r,d*u+e)}function PY(r,e,t){var a=new ft;ft.sub(a,t,e),a.normalize();var n=new ft;ft.sub(n,r,e);var i=n.dot(a);return i}function zi(r,e){var t=r[r.length-1];t&&t[0]===e[0]&&t[1]===e[1]||r.push(e)}function RY(r,e,t){for(var a=r.length,n=[],i=0;io?(u.x=f.x=s+i/2,u.y=l,f.y=l+o):(u.y=f.y=l+o/2,u.x=s,f.x=s+i),RY(e,u,f)}function ih(r,e,t,a){if(t===1)a.push(e);else{var n=Math.floor(t/2),i=r(e);ih(r,i[0],n,a),ih(r,i[1],t-n,a)}return a}function EY(r,e){for(var t=[],a=0;a0;u/=2){var f=0,h=0;(r&u)>0&&(f=1),(e&u)>0&&(h=1),s+=u*u*(3*f^h),h===0&&(f===1&&(r=u-1-r,e=u-1-e),l=r,r=e,e=l)}return s}function lh(r){var e=1/0,t=1/0,a=-1/0,n=-1/0,i=G(r,function(s){var l=s.getBoundingRect(),u=s.getComputedTransform(),f=l.x+l.width/2+(u?u[4]:0),h=l.y+l.height/2+(u?u[5]:0);return e=Math.min(f,e),t=Math.min(h,t),a=Math.max(f,a),n=Math.max(h,n),[f,h]}),o=G(i,function(s,l){return{cp:s,z:HY(s[0],s[1],e,t,a,n),path:r[l]}});return o.sort(function(s,l){return s.z-l.z}).map(function(s){return s.path})}function aL(r){return NY(r.path,r.count)}function og(){return{fromIndividuals:[],toIndividuals:[],count:0}}function WY(r,e,t){var a=[];function n(b){for(var x=0;x=0;n--)if(!t[n].many.length){var l=t[s].many;if(l.length<=1)if(s)s=0;else return t;var i=l.length,u=Math.ceil(i/2);t[n].many=l.slice(u,i),t[s].many=l.slice(0,u),s++}return t}var YY={clone:function(r){for(var e=[],t=1-Math.pow(1-r.path.style.opacity,1/r.count),a=0;a0))return;var s=a.getModel("universalTransition").get("delay"),l=Object.assign({setToFinal:!0},o),u,f;hw(r)&&(u=r,f=e),hw(e)&&(u=e,f=r);function h(y,m,_,S,b){var x=y.many,w=y.one;if(x.length===1&&!b){var T=m?x[0]:w,A=m?w:x[0];if(oh(T))h({many:[T],one:A},!0,_,S,!0);else{var D=s?Q({delay:s(_,S)},l):l;xm(T,A,D),i(T,A,T,A,D)}}else for(var M=Q({dividePath:YY[t],individualDelay:s&&function(N,O,B,F){return s(N+_,S)}},l),I=m?WY(x,w,M):UY(w,x,M),L=I.fromIndividuals,P=I.toIndividuals,R=L.length,E=0;Ee.length,c=u?vw(f,u):vw(v?e:r,[v?r:e]),p=0,d=0;dnL))for(var i=a.getIndices(),o=0;o0&&x.group.traverse(function(T){T instanceof yt&&!T.animators.length&&T.animateFrom({style:{opacity:0}},w)})})}function yw(r){var e=r.getModel("universalTransition").get("seriesKey");return e||r.id}function mw(r){return z(r)?r.sort().join(","):r}function Oa(r){if(r.hostModel)return r.hostModel.getModel("universalTransition").get("divideShape")}function QY(r,e){var t=Z(),a=Z(),n=Z();return C(r.oldSeries,function(i,o){var s=r.oldDataGroupIds[o],l=r.oldData[o],u=yw(i),f=mw(u);a.set(f,{dataGroupId:s,data:l}),z(u)&&C(u,function(h){n.set(h,{key:f,dataGroupId:s,data:l})})}),C(e.updatedSeries,function(i){if(i.isUniversalTransitionEnabled()&&i.isAnimationEnabled()){var o=i.get("dataGroupId"),s=i.getData(),l=yw(i),u=mw(l),f=a.get(u);if(f)t.set(u,{oldSeries:[{dataGroupId:f.dataGroupId,divide:Oa(f.data),data:f.data}],newSeries:[{dataGroupId:o,divide:Oa(s),data:s}]});else if(z(l)){var h=[];C(l,function(p){var d=a.get(p);d.data&&h.push({dataGroupId:d.dataGroupId,divide:Oa(d.data),data:d.data})}),h.length&&t.set(u,{oldSeries:h,newSeries:[{dataGroupId:o,data:s,divide:Oa(s)}]})}else{var v=n.get(l);if(v){var c=t.get(v.key);c||(c={oldSeries:[{dataGroupId:v.dataGroupId,data:v.data,divide:Oa(v.data)}],newSeries:[]},t.set(v.key,c)),c.newSeries.push({dataGroupId:o,data:s,divide:Oa(s)})}}}}),t}function _w(r,e){for(var t=0;t=0&&n.push({dataGroupId:e.oldDataGroupIds[s],data:e.oldData[s],divide:Oa(e.oldData[s]),groupIdDim:o.dimension})}),C(Et(r.to),function(o){var s=_w(t.updatedSeries,o);if(s>=0){var l=t.updatedSeries[s].getData();i.push({dataGroupId:e.oldDataGroupIds[s],data:l,divide:Oa(l),groupIdDim:o.dimension})}}),n.length>0&&i.length>0&&iL(n,i,a)}function t7(r){r.registerUpdateLifecycle("series:beforeupdate",function(e,t,a){C(Et(a.seriesTransition),function(n){C(Et(n.to),function(i){for(var o=a.updatedSeries,s=0;s包含CICD、Kubernetes

\\n","autoDesc":true}');export{s as comp,m as data}; diff --git a/assets/index.html-BKAyyte1.js b/assets/index.html-BKAyyte1.js new file mode 100644 index 00000000..155bc9a8 --- /dev/null +++ b/assets/index.html-BKAyyte1.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function n(r,c){return a(),t("div")}const i=e(o,[["render",n],["__file","index.html.vue"]]),l=JSON.parse('{"path":"/tag/","title":"标签","lang":"zh-CN","frontmatter":{"title":"标签","dir":{"index":false},"index":false,"feed":false,"sitemap":false,"blog":{"type":"category","key":"tag"},"layout":"BlogCategory","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/tag/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"标签"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"标签\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":""}');export{i as comp,l as data}; diff --git a/assets/index.html-BTDM2Wpl.js b/assets/index.html-BTDM2Wpl.js new file mode 100644 index 00000000..db699cd8 --- /dev/null +++ b/assets/index.html-BTDM2Wpl.js @@ -0,0 +1 @@ +import{_ as t,c as a,b as o,o as c,r as s}from"./app-ftEjETWs.js";const r={};function n(l,i){const e=s("Catalog");return c(),a("div",null,[o(e)])}const m=t(r,[["render",n],["__file","index.html.vue"]]),h=JSON.parse('{"path":"/database/elasticsearch/","title":"Elasticsearch","lang":"zh-CN","frontmatter":{"title":"Elasticsearch","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/elasticsearch/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Elasticsearch"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Elasticsearch\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,h as data}; diff --git a/assets/index.html-BXiz75su.js b/assets/index.html-BXiz75su.js new file mode 100644 index 00000000..4ae6e5ab --- /dev/null +++ b/assets/index.html-BXiz75su.js @@ -0,0 +1 @@ +import{_ as t,c as o,a as i,o as n}from"./app-ftEjETWs.js";const a={};function r(p,e){return n(),o("div",null,e[0]||(e[0]=[i("p",null,"jofjweoiaejof",-1)]))}const m=t(a,[["render",r],["__file","index.html.vue"]]),l=JSON.parse('{"path":"/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/","title":"个人网站","lang":"zh-CN","frontmatter":{"title":"个人网站","icon":"pen","dir":{"collapsible":true,"order":1},"index":false,"description":"jofjweoiaejof","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"个人网站"}],["meta",{"property":"og:description","content":"jofjweoiaejof"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"个人网站\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.05,"words":15},"filePathRelative":"interesting/个人网站/README.md","localizedDate":"2024年2月16日","excerpt":"

jofjweoiaejof

\\n","autoDesc":true}');export{m as comp,l as data}; diff --git a/assets/index.html-BcZZ4qCt.js b/assets/index.html-BcZZ4qCt.js new file mode 100644 index 00000000..31bf30d1 --- /dev/null +++ b/assets/index.html-BcZZ4qCt.js @@ -0,0 +1 @@ +import{_ as t,c as o,a,o as r}from"./app-ftEjETWs.js";const n={};function i(c,e){return r(),o("div",null,e[0]||(e[0]=[a("p",null,"jfaowejfoewj",-1)]))}const m=t(n,[["render",i],["__file","index.html.vue"]]),s=JSON.parse('{"path":"/open-source-project/","title":"","lang":"zh-CN","frontmatter":{"description":"jfaowejfoewj","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/open-source-project/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:description","content":"jfaowejfoewj"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T10:30:22.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-24T10:30:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T10:30:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706092222000,"updatedTime":1706092222000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0,"words":1},"filePathRelative":"open-source-project/README.md","localizedDate":"2024年1月24日","excerpt":"

jfaowejfoewj

\\n","autoDesc":true}');export{m as comp,s as data}; diff --git a/assets/index.html-BwVnwmVL.js b/assets/index.html-BwVnwmVL.js new file mode 100644 index 00000000..d5294dad --- /dev/null +++ b/assets/index.html-BwVnwmVL.js @@ -0,0 +1 @@ +import{_ as t,c as n,b as a,o,r}from"./app-ftEjETWs.js";const l={};function i(c,s){const e=r("Catalog");return o(),n("div",null,[a(e)])}const m=t(l,[["render",i],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/middleware/sentinel/","title":"Sentinel","lang":"zh-CN","frontmatter":{"title":"Sentinel","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/sentinel/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Sentinel"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Sentinel\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,d as data}; diff --git a/assets/index.html-By-pQ7Wl.js b/assets/index.html-By-pQ7Wl.js new file mode 100644 index 00000000..08d26477 --- /dev/null +++ b/assets/index.html-By-pQ7Wl.js @@ -0,0 +1 @@ +import{_ as e,c as t,o}from"./app-ftEjETWs.js";const n={};function r(a,i){return o(),t("div")}const p=e(n,[["render",r],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/","title":"首页","lang":"zh-CN","frontmatter":{"home":true,"layout":"BlogHome","icon":"home","title":"首页","description":"一个基于VuePress的个人博客。","hero":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"首页"}],["meta",{"property":"og:description","content":"一个基于VuePress的个人博客。"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-20T16:57:46.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-20T16:57:46.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"首页\\",\\"description\\":\\"一个基于VuePress的个人博客。\\"}"]]},"headers":[],"git":{"createdTime":1566398987000,"updatedTime":1716224266000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":13}]},"readingTime":{"minutes":0.07,"words":22},"filePathRelative":"README.md","localizedDate":"2019年8月21日","excerpt":""}');export{p as comp,m as data}; diff --git a/assets/index.html-C9P5ouBn.js b/assets/index.html-C9P5ouBn.js new file mode 100644 index 00000000..3e1ef671 --- /dev/null +++ b/assets/index.html-C9P5ouBn.js @@ -0,0 +1 @@ +import{_ as t,c as o,b as a,o as n,r}from"./app-ftEjETWs.js";const s={};function c(p,l){const e=r("Catalog");return n(),o("div",null,[a(e)])}const m=t(s,[["render",c],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/","title":"推荐系统","lang":"zh-CN","frontmatter":{"title":"推荐系统","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"推荐系统"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"推荐系统\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,d as data}; diff --git a/assets/index.html-CD6aFyf5.js b/assets/index.html-CD6aFyf5.js new file mode 100644 index 00000000..1436b270 --- /dev/null +++ b/assets/index.html-CD6aFyf5.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function n(r,c){return a(),t("div")}const p=e(o,[["render",n],["__file","index.html.vue"]]),l=JSON.parse('{"path":"/article/","title":"文章","lang":"zh-CN","frontmatter":{"title":"文章","dir":{"index":false},"index":false,"feed":false,"sitemap":false,"blog":{"type":"type","key":"article"},"layout":"BlogType","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/article/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"文章"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"文章\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":""}');export{p as comp,l as data}; diff --git a/assets/index.html-CMe--mJk.js b/assets/index.html-CMe--mJk.js new file mode 100644 index 00000000..60f4bdaa --- /dev/null +++ b/assets/index.html-CMe--mJk.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function i(r,n){return a(),t("div")}const p=e(o,[["render",i],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/java/%E7%BA%BF%E7%A8%8B/","title":"线程","lang":"zh-CN","frontmatter":{"title":"线程","icon":"pen","dir":{"collapsible":true,"order":100},"index":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"线程"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T04:00:26.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T04:00:26.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"线程\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T04:00:26.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1708056026000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"java/线程/README.md","localizedDate":"2024年2月16日","excerpt":""}');export{p as comp,m as data}; diff --git a/assets/index.html-CY6pLx2d.js b/assets/index.html-CY6pLx2d.js new file mode 100644 index 00000000..48e0dc8e --- /dev/null +++ b/assets/index.html-CY6pLx2d.js @@ -0,0 +1 @@ +import{_ as t,c as a,b as o,o as n,r}from"./app-ftEjETWs.js";const c={};function l(p,s){const e=r("Catalog");return n(),a("div",null,[o(e)])}const m=t(c,[["render",l],["__file","index.html.vue"]]),f=JSON.parse('{"path":"/middleware/kafka/","title":"Kafka","lang":"zh-CN","frontmatter":{"title":"Kafka","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/kafka/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Kafka"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Kafka\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,f as data}; diff --git a/assets/index.html-CdxL9VtY.js b/assets/index.html-CdxL9VtY.js new file mode 100644 index 00000000..c716cef5 --- /dev/null +++ b/assets/index.html-CdxL9VtY.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as n}from"./app-ftEjETWs.js";const o={};function a(i,r){return n(),t("div")}const p=e(o,[["render",a],["__file","index.html.vue"]]),c=JSON.parse('{"path":"/timeline/","title":"时间轴","lang":"zh-CN","frontmatter":{"title":"时间轴","dir":{"index":false},"index":false,"feed":false,"sitemap":false,"blog":{"type":"type","key":"timeline"},"layout":"Timeline","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/timeline/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"时间轴"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"时间轴\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":""}');export{p as comp,c as data}; diff --git a/assets/index.html-CpCz4LWW.js b/assets/index.html-CpCz4LWW.js new file mode 100644 index 00000000..664a1519 --- /dev/null +++ b/assets/index.html-CpCz4LWW.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function i(r,n){return a(),t("div")}const p=e(o,[["render",i],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/java/io/","title":"I/O","lang":"zh-CN","frontmatter":{"title":"I/O","icon":"pen","dir":{"collapsible":true,"order":100},"index":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/io/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"I/O"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"I/O\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":11},"filePathRelative":"java/io/README.md","localizedDate":"2024年2月16日","excerpt":""}');export{p as comp,m as data}; diff --git a/assets/index.html-D1R9zQcp.js b/assets/index.html-D1R9zQcp.js new file mode 100644 index 00000000..2e77b925 --- /dev/null +++ b/assets/index.html-D1R9zQcp.js @@ -0,0 +1 @@ +import{_ as a,c as r,a as e,o}from"./app-ftEjETWs.js";const n={};function i(p,t){return o(),r("div",null,t[0]||(t[0]=[e("h1",{id:"spark",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#spark"},[e("span",null,"Spark")])],-1)]))}const m=a(n,[["render",i],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/bigdata/","title":"Spark","lang":"zh-CN","frontmatter":{"description":"Spark","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/bigdata/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Spark"}],["meta",{"property":"og:description","content":"Spark"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:53:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:53:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Spark\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-06T07:53:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1566721640000,"updatedTime":1707206019000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":0,"words":1},"filePathRelative":"bigdata/README.md","localizedDate":"2019年8月25日","excerpt":"\\n","autoDesc":true}');export{m as comp,d as data}; diff --git a/assets/index.html-D4WG7yPk.js b/assets/index.html-D4WG7yPk.js new file mode 100644 index 00000000..0de93901 --- /dev/null +++ b/assets/index.html-D4WG7yPk.js @@ -0,0 +1 @@ +import{_ as a,c as i,a as e,o as n}from"./app-ftEjETWs.js";const o={};function r(c,t){return n(),i("div",null,t[0]||(t[0]=[e("h1",{id:"微信支付",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#微信支付"},[e("span",null,"微信支付")])],-1),e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/wechat-pay.png",alt:"微信支付",tabindex:"0",loading:"lazy"}),e("figcaption",null,"微信支付")],-1)]))}const m=a(o,[["render",r],["__file","index.html.vue"]]),h=JSON.parse('{"path":"/donate/","title":"微信支付","lang":"zh-CN","frontmatter":{"description":"微信支付 微信支付微信支付","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/donate/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"微信支付"}],["meta",{"property":"og:description","content":"微信支付 微信支付微信支付"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/wechat-pay.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T17:18:59.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T17:18:59.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"微信支付\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/wechat-pay.png\\"],\\"dateModified\\":\\"2024-01-26T17:18:59.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706169851000,"updatedTime":1706289539000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"donate/README.md","localizedDate":"2024年1月25日","excerpt":"\\n
\\"微信支付\\"
微信支付
","autoDesc":true}');export{m as comp,h as data}; diff --git a/assets/index.html-DGy09Ap4.js b/assets/index.html-DGy09Ap4.js new file mode 100644 index 00000000..efbc5f4d --- /dev/null +++ b/assets/index.html-DGy09Ap4.js @@ -0,0 +1 @@ +import{_ as e,c as t,o}from"./app-ftEjETWs.js";const a={};function i(r,n){return o(),t("div")}const c=e(a,[["render",i],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/java/SpringBoot/","title":"SpringBoot","lang":"zh-CN","frontmatter":{"title":"SpringBoot","icon":"pen","dir":{"collapsible":true,"order":100},"index":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/SpringBoot/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"SpringBoot"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T04:00:26.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T04:00:26.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"SpringBoot\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T04:00:26.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1708056026000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":11},"filePathRelative":"java/SpringBoot/README.md","localizedDate":"2024年2月16日","excerpt":""}');export{c as comp,m as data}; diff --git a/assets/index.html-DJHdkCFZ.js b/assets/index.html-DJHdkCFZ.js new file mode 100644 index 00000000..f4de4d9a --- /dev/null +++ b/assets/index.html-DJHdkCFZ.js @@ -0,0 +1 @@ +import{_ as t,c as o,b as a,o as n,r}from"./app-ftEjETWs.js";const c={};function i(l,p){const e=r("Catalog");return n(),o("div",null,[a(e)])}const m=t(c,[["render",i],["__file","index.html.vue"]]),f=JSON.parse('{"path":"/life/","title":"Life","lang":"zh-CN","frontmatter":{"title":"Life","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/life/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Life"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Life\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,f as data}; diff --git a/assets/index.html-DJZmVM8a.js b/assets/index.html-DJZmVM8a.js new file mode 100644 index 00000000..dc76e5e7 --- /dev/null +++ b/assets/index.html-DJZmVM8a.js @@ -0,0 +1 @@ +import{_ as e,c as t,o}from"./app-ftEjETWs.js";const n={};function a(r,p){return o(),t("div")}const i=e(n,[["render",a],["__file","index.html.vue"]]),l=JSON.parse('{"path":"/slide/","title":"","lang":"zh-CN","frontmatter":{"blog":{"type":"type","key":"slide"},"layout":"BlogType","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/slide/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":""}');export{i as comp,l as data}; diff --git a/assets/index.html-DQFfT5fQ.js b/assets/index.html-DQFfT5fQ.js new file mode 100644 index 00000000..4cf9a9ad --- /dev/null +++ b/assets/index.html-DQFfT5fQ.js @@ -0,0 +1 @@ +import{_ as t,c as n,b as o,o as a,r}from"./app-ftEjETWs.js";const i={};function c(p,l){const e=r("Catalog");return a(),n("div",null,[o(e)])}const m=t(i,[["render",c],["__file","index.html.vue"]]),_=JSON.parse('{"path":"/interview/","title":"Interview","lang":"zh-CN","frontmatter":{"title":"Interview","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interview/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Interview"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Interview\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,_ as data}; diff --git a/assets/index.html-DRyhNgG6.js b/assets/index.html-DRyhNgG6.js new file mode 100644 index 00000000..93ff899f --- /dev/null +++ b/assets/index.html-DRyhNgG6.js @@ -0,0 +1 @@ +import{_ as t,c as a,b as n,o,r}from"./app-ftEjETWs.js";const l={};function c(p,s){const e=r("Catalog");return o(),a("div",null,[n(e)])}const m=t(l,[["render",c],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/middleware/canal/","title":"Canal","lang":"zh-CN","frontmatter":{"title":"Canal","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/canal/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Canal"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Canal\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,d as data}; diff --git a/assets/index.html-DWXiwN5o.js b/assets/index.html-DWXiwN5o.js new file mode 100644 index 00000000..d3196884 --- /dev/null +++ b/assets/index.html-DWXiwN5o.js @@ -0,0 +1 @@ +import{_ as a,c as o,a as e,o as n}from"./app-ftEjETWs.js";const r={};function i(c,t){return n(),o("div",null,t[0]||(t[0]=[e("h1",{id:"database",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#database"},[e("span",null,"Database")])],-1),e("p",null,"目录留空",-1)]))}const s=a(r,[["render",i],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/database/","title":"Database","lang":"zh-CN","frontmatter":{"description":"Database 目录留空","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Database"}],["meta",{"property":"og:description","content":"Database 目录留空"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-03T17:57:14.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-03T17:57:14.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Database\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-03T17:57:14.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706182936000,"updatedTime":1706983034000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.02,"words":5},"filePathRelative":"database/README.md","localizedDate":"2024年1月25日","excerpt":"\\n

目录留空

\\n","autoDesc":true}');export{s as comp,d as data}; diff --git a/assets/index.html-DYmvtxA9.js b/assets/index.html-DYmvtxA9.js new file mode 100644 index 00000000..1b4c2f1c --- /dev/null +++ b/assets/index.html-DYmvtxA9.js @@ -0,0 +1 @@ +import{_ as e,c as t,o}from"./app-ftEjETWs.js";const a={};function r(n,c){return o(),t("div")}const i=e(a,[["render",r],["__file","index.html.vue"]]),l=JSON.parse('{"path":"/category/","title":"分类","lang":"zh-CN","frontmatter":{"title":"分类","dir":{"index":false},"index":false,"feed":false,"sitemap":false,"blog":{"type":"category","key":"category"},"layout":"BlogCategory","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/category/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"分类"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"分类\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null,"excerpt":""}');export{i as comp,l as data}; diff --git a/assets/index.html-DaAoZU-K.js b/assets/index.html-DaAoZU-K.js new file mode 100644 index 00000000..ffb542c3 --- /dev/null +++ b/assets/index.html-DaAoZU-K.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function i(r,n){return a(),t("div")}const p=e(o,[["render",i],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/java/%E7%BD%91%E7%BB%9C/","title":"网络","lang":"zh-CN","frontmatter":{"title":"网络","icon":"pen","dir":{"collapsible":true,"order":100},"index":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"网络"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T04:00:26.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T04:00:26.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"网络\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T04:00:26.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1708056026000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"java/网络/README.md","localizedDate":"2024年2月16日","excerpt":""}');export{p as comp,m as data}; diff --git a/assets/index.html-DlwW5Vje.js b/assets/index.html-DlwW5Vje.js new file mode 100644 index 00000000..ecc35eeb --- /dev/null +++ b/assets/index.html-DlwW5Vje.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./app-ftEjETWs.js";const o={};function i(n,r){return a(),t("div")}const m=e(o,[["render",i],["__file","index.html.vue"]]),p=JSON.parse('{"path":"/cloudnative/","title":"","lang":"zh-CN","frontmatter":{"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/cloudnative/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1566721640000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0,"words":0},"filePathRelative":"cloudnative/README.md","localizedDate":"2019年8月25日","excerpt":""}');export{m as comp,p as data}; diff --git a/assets/index.html-DqVhfNm4.js b/assets/index.html-DqVhfNm4.js new file mode 100644 index 00000000..66eedfa5 --- /dev/null +++ b/assets/index.html-DqVhfNm4.js @@ -0,0 +1 @@ +import{_ as a,c as o,a as e,o as n}from"./app-ftEjETWs.js";const r={};function i(c,t){return n(),o("div",null,t[0]||(t[0]=[e("h1",{id:"关于我",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#关于我"},[e("span",null,"关于我")])],-1),e("p",null,"留空",-1)]))}const m=a(r,[["render",i],["__file","index.html.vue"]]),h=JSON.parse('{"path":"/about-the-author/","title":"关于我","lang":"zh-CN","frontmatter":{"description":"关于我 留空","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"关于我"}],["meta",{"property":"og:description","content":"关于我 留空"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T17:34:45.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-25T17:34:45.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"关于我\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T17:34:45.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706204085000,"updatedTime":1706204085000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.02,"words":5},"filePathRelative":"about-the-author/README.md","localizedDate":"2024年1月25日","excerpt":"\\n

留空

\\n","autoDesc":true}');export{m as comp,h as data}; diff --git a/assets/index.html-DtDo2qGn.js b/assets/index.html-DtDo2qGn.js new file mode 100644 index 00000000..60410b51 --- /dev/null +++ b/assets/index.html-DtDo2qGn.js @@ -0,0 +1 @@ +import{_ as t,c as o,b as a,o as n,r}from"./app-ftEjETWs.js";const l={};function s(c,i){const e=r("Catalog");return n(),o("div",null,[a(e)])}const m=t(l,[["render",s],["__file","index.html.vue"]]),f=JSON.parse('{"path":"/about-the-author/personal-life/","title":"Personal Life","lang":"zh-CN","frontmatter":{"title":"Personal Life","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/personal-life/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Personal Life"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Personal Life\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,f as data}; diff --git a/assets/index.html-KwQlr7R4.js b/assets/index.html-KwQlr7R4.js new file mode 100644 index 00000000..85dd42d7 --- /dev/null +++ b/assets/index.html-KwQlr7R4.js @@ -0,0 +1 @@ +import{_ as t,c as n,b as o,o as a,r as s}from"./app-ftEjETWs.js";const r={};function c(i,p){const e=s("Catalog");return a(),n("div",null,[o(e)])}const m=t(r,[["render",c],["__file","index.html.vue"]]),g=JSON.parse('{"path":"/system-design/","title":"System Design","lang":"zh-CN","frontmatter":{"title":"System Design","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"System Design"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"System Design\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,g as data}; diff --git a/assets/index.html-SdCcjUz-.js b/assets/index.html-SdCcjUz-.js new file mode 100644 index 00000000..7234fd5b --- /dev/null +++ b/assets/index.html-SdCcjUz-.js @@ -0,0 +1 @@ +import{_ as t,c as o,b as a,o as n,r}from"./app-ftEjETWs.js";const s={};function c(p,l){const e=r("Catalog");return n(),o("div",null,[a(e)])}const m=t(s,[["render",c],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/","title":"计算广告","lang":"zh-CN","frontmatter":{"title":"计算广告","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"计算广告"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"计算广告\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,d as data}; diff --git a/assets/index.html-TDa4J3OS.js b/assets/index.html-TDa4J3OS.js new file mode 100644 index 00000000..561fd743 --- /dev/null +++ b/assets/index.html-TDa4J3OS.js @@ -0,0 +1 @@ +import{_ as t,c as o,b as a,o as r,r as n}from"./app-ftEjETWs.js";const s={};function c(p,l){const e=n("Catalog");return r(),o("div",null,[a(e)])}const m=t(s,[["render",c],["__file","index.html.vue"]]),h=JSON.parse('{"path":"/about-the-author/works/","title":"Works","lang":"zh-CN","frontmatter":{"title":"Works","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/works/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Works"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Works\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,h as data}; diff --git a/assets/index.html-Uqis1PLJ.js b/assets/index.html-Uqis1PLJ.js new file mode 100644 index 00000000..7c9906c1 --- /dev/null +++ b/assets/index.html-Uqis1PLJ.js @@ -0,0 +1 @@ +import{_ as t,c as a,b as o,o as n,r as s}from"./app-ftEjETWs.js";const r={};function l(c,p){const e=s("Catalog");return n(),a("div",null,[o(e)])}const m=t(r,[["render",l],["__file","index.html.vue"]]),d=JSON.parse('{"path":"/database/mysql/","title":"Mysql","lang":"zh-CN","frontmatter":{"title":"Mysql","article":false,"feed":false,"sitemap":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Mysql"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"Mysql\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0,"words":1},"filePathRelative":null,"excerpt":""}');export{m as comp,d as data}; diff --git a/assets/index.html-jGjDZXTI.js b/assets/index.html-jGjDZXTI.js new file mode 100644 index 00000000..55a9f80a --- /dev/null +++ b/assets/index.html-jGjDZXTI.js @@ -0,0 +1 @@ +import{_ as o,c as n,a as e,b as r,o as i,r as c}from"./app-ftEjETWs.js";const p={};function m(s,t){const a=c("Catalog");return i(),n("div",null,[t[0]||(t[0]=e("h1",{id:"java",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#java"},[e("span",null,"Java")])],-1)),r(a)])}const d=o(p,[["render",m],["__file","index.html.vue"]]),h=JSON.parse('{"path":"/java/","title":"Java","lang":"zh-CN","frontmatter":{"index":false,"description":"Java","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Java"}],["meta",{"property":"og:description","content":"Java"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-18T19:54:10.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-18T19:54:10.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Java\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-18T19:54:10.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1566721640000,"updatedTime":1716062050000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":8}]},"readingTime":{"minutes":0.01,"words":4},"filePathRelative":"java/README.md","localizedDate":"2019年8月25日","excerpt":"\\n","autoDesc":true}');export{d as comp,h as data}; diff --git a/assets/index.html-muK9iEFv.js b/assets/index.html-muK9iEFv.js new file mode 100644 index 00000000..5c4f0d59 --- /dev/null +++ b/assets/index.html-muK9iEFv.js @@ -0,0 +1 @@ +import{_ as t,c as o,a,o as n}from"./app-ftEjETWs.js";const r={};function i(p,e){return n(),o("div",null,e[0]||(e[0]=[a("p",null,"jofjweoiaejof",-1)]))}const s=t(r,[["render",i],["__file","index.html.vue"]]),m=JSON.parse('{"path":"/kubernetes/devops/","title":"DevOps","lang":"zh-CN","frontmatter":{"title":"DevOps","icon":"pen","dir":{"collapsible":true,"order":1},"index":false,"description":"jofjweoiaejof","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/devops/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"DevOps"}],["meta",{"property":"og:description","content":"jofjweoiaejof"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:22:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:22:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"DevOps\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T09:22:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1708075355000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"kubernetes/devops/README.md","localizedDate":"2024年2月16日","excerpt":"

jofjweoiaejof

\\n","autoDesc":true}');export{s as comp,m as data}; diff --git a/assets/index.html-nZcQpO1z.js b/assets/index.html-nZcQpO1z.js new file mode 100644 index 00000000..6e14909f --- /dev/null +++ b/assets/index.html-nZcQpO1z.js @@ -0,0 +1 @@ +import{_ as o,c as p,a as e,b as a,o as r,r as i}from"./app-ftEjETWs.js";const c={};function d(h,t){const n=i("ECharts");return r(),p("div",null,[t[0]||(t[0]=e("h1",{id:"股票预测",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#股票预测"},[e("span",null,"股票预测")])],-1)),a(n,{id:"echarts-4",config:"eJyVVHtr2zAQ/z+f4jAM5DnLkr2brIOy0lFY6aAwGCGMa60kWm3JSHJbM/rdd3r40WYpW0hi6x6/u/vd6a6UNBaU5MfYwCF8ePdm6j6LUVDkaJHEy9ViVHALUt3SSfJbOEbL2ezg4P0YDsbwOg36GyxqThZnaLcTjTJXJUvhOcw8ZMQMcgJwyKQ+/AS/R/AYO3PnLGZG8NCBh2e2E+TVDF5QJGequa219LAEjCWfO/iJVRdWC7lh6dhrPNIclv4AsHQ2G25P6qL4wVGTmXcj0ZmSdktRMph1Mp9mupr8UkKy5GUSQSEmpmqZMx8hKlbucb8Y0Xe0VhqYY0xQQdMFPT4GkkBkWeppn1S12bKeLJYSC5FCVVmhJLm6Cq1ShRXVPJZLFW42XM8hwTthkhCb4pVorROva3nlvVmFGkuTRj+AcCbU8LKcUteD5ga1y8mx3zUoGE0cu7497hN5Z/EI3qnnCrJOQXwNTqy1G/C837o17vs0NAWqfHCOafpGLGerKI8Z3wd2HFHfqIuen5YNlIIoI6KIMiwMD6bew//dHZFXR3pT0SAlVpQ8Mm6qQtivQpK4RTRbdbsHrPkLmE85ol26aULdfEHq83I6hoSm5VniR+p/Y7l5uVR3rW2wtLqOhmuO1MQBVIn6uj/tOnQ0+s7gd8FJ/ZS5mxPMz2XRPMhwgKO5sWqYw5NRDd7wI3Na4ubfPHoqDNeCE+9hA7SuYWEkJ3jN3ahj7EHXmIKY7mQuykVTXqriUTGOi7n/H0SlhtH1D5fYDctpTlfKcHvqZo8azvp9KNbAyubzFrWd/MyFqZThedpesauCBr/zClBuP/irvrNa3sa9Eir028VsxdqyeA/27htKm35tGpToud88LI7OA/p6AneL77gPS5CQ78d+4dHbH0CcwCk=",title:"%E8%B5%9B%E5%8A%9B%E6%96%AF",type:"js"}),a(n,{id:"echarts-9",config:"eJztWc1u5MYRvu9TEBMEkLDrFrvZ/NNaDhQ7ySEREGBvFnSgRpTENYcccDheKfa+Qe55grxFnifXvEK+qmrOsFuzWu3BPnmw2KGaXV3VVV99Vd2z7LvNGG3X3/ZtP0Rn0eJ39TLGZ/H21fTqj/1wUw/7CcW5N+Gm/9DtX8bxTZUsvZehfBwXfzYFprg5m3XbjN9VY4W3R0P1gR6Po7Nvop9eRZFMWVZjfdcPj27W5dXb3asfq3Zbb9wgRm+h5aitx6jBWPwWX19HblHV1t3deI+x16+PeXUsMltZrbeb+8mCy+ZKkWXL+ih+E+njy/jqmLRGTmM4mV9+JAuGetwO3YH1T72/3swWO3XfNPbx7Sv8e3VyEv2pWt5HzVivTqN+XXf/+88/l22/qfHd9h/qzYiH++buHk+Tt7FsjG3vPHp0iQUvFybWyYk+MXbxJjKJiZXJ5g+myFVCA5lRpb0iI/YyKcvE8IExpeZ5pijcCnGhkiIQKEjAlKlKUppiM0Xf3kBpApmSldhcGUMPaaHKgh4SGMZCWaICPUnMMlmscp5RGGVZBqsUJY8kKs8CIc1CeGN5w0WqLGvMtTI8gi0WnnXmRGTyXFmNB6tLFbNMVqo0pxGjlU59Gfa0NSm2yg8FZrBwrlKy0to48Jw5YU9bzUqSRGmyx8YJu9zCEzawK+P5iVEZr5hYcYA1uWKzrFZ54ovkIhKrjDxkdaFS2bUVz5PeovRlOJ5WZyqTGfAzD0Ada0lUHGjRIgL9JW8G/kkJLlbD3bwba/1wQoYhYHfYRDhZOMkTsZG2Fee+kBEMUDg54mWuNGMgxw5ZGiM+oiHk4lkg0JwFCBIjKC6VJsfJq0DISOpQgtADdq+nDMj5AU4tgj253MGe2HWkSVTCDzGbl0A6FOKwJhrgJU2mBAD4oSgdrgGdXAdCHFiD3ea8A0hzPE1pEB3WbVUWhEkylfaUCJ5TlbIC7Ilhm2QZdjkXQg5J1lmVsh9S5IDsBNtnPyArMi8ZEkmGBMCJKTgGEeUNQdaw4wG61AtSIrlgcsCEVzeZSujBILAygIQKRMRvWEsiDxoQLAB9bFiqlQ1E2GvkT8ubgY+MOAAP7McUNntISCQdyCAhDqSQ6AMixHtkgYdTOE28himcqomOZa4pQY5iZKzK0NMCOUKlkDQykL7h+4JDCkXGy1XIJOw3JA8j2YA2S35IMZdpGKskHg4gxPExaSrgIR8bDhSEnNczwCkQkggh2m5K4YJKhSLRFOtAQOpCBo7nrcPrTKPIUWUmLXGAHGEF8IUrVClYkCsQgYmlM5QkLxmAbGYFmssZlIBzE3ECcagbKQJvO1ZAONkYxoJIO/gQwENFjhQALIdJ9hvFmb0OyAcCQghgarEMKMrFoNKxHRFjENOJEMDVHBUkecbbKVB2dtKhkCMEhIdNilHfBEWMUYMUDOLj6jbRLQMlAVCcp9k0eRPISHjAYsy/JMNMBzWSuAbFvvCSwQqDwDRl2SAqWzyAKIkIMOvpsSfsZ5rJWqi+iYhLXAKqXxztiSQCFUeRSR3sEHpJBFSnxKMDK6mtkZMceYPIc5bqAgTOHgesU8/R9kQ8QIWNZ0CPJNFuhILtJRw8IAAFxEp2MWDAxZ7XF6egqAbGCYeQi7k1gp0uULQjFrLUAQVC4jlgJ+bdxwAcS4PhxHXQbbxUgBCjVAM7HHgNnHHR02iBElkGlgd+0IxSTexPW9HoKAoyT6OqsEryqw0dwSjVaA25SGnUOp5KZELO08B66iUdZCRKMIprKU2h3escDucdYY9+QwIZiRKSgBOG+CSTuSBioTrUlUCRZDc5nKfCu8KpwJTzd4rlAhmBHeZyHujC1UkNRnYJQa8CIeZfTU24BBRN9STN3SADJBQS/kWh14xRynSOFjWwvMyTig8hDpJBT8k0QAGVECO3uXobqkwehlJJPo2iIGFETWYnajCvFliEFT+V7KMXMZsSo7PwBtBz+GSaSvnmDGAnY/Fdyrkw+N1fKtWb2EBMT6BEREAtEjJqJn0ZITkEQYAMehKSc+cAKoImMMyVIOSdqAEfSqqDQBg7dOAI1EwZ7npMpkFJdbSAggNi8UDIgQdvZF3knVRIaJLyhxoUeNoVb1DwRG/cWZLLC4Er0tiDAWQcdjBXyBG56sAk2QArtZffkJH40KaFcgEzVgTECOOhV/KpHkISIovOledSFvBciwSXEWKmAG/iO25U6IF4iM0kHsI3dURJKCPkSP0/l1HsXjKnRF5zjcRI4AVXvZEMcoCM5WBDLbMcDqnHD3NBQkQ8JTUxdZWBsoIXATMGEZL0JuutdGDu0MV9lhyNw7ICIfEc9XwceWiU5hlnHTmBgMwCb08nb2mvqS1hGmHN3I4g6wJsu4M32gFp54EEOdvSkVZaGBv0i0huOXXhRCjtO3AkHgQU5BxGVdDjKwhJf4WSLa0PMa+0wIimWIf89TRlQiM0VRpETHUOc8RI5xy/pmRy2GAksI8RTOkTUYbFk3jjS7izBp2q3IOUZVwtCFrRmPoJnjm2wqLSjlJxkLRGVyudNyLnxTRzfIWpUj7gLSmmqH6SQGgzgs0IJWhCMX0jbbhEaKraUv3wBiJ0++TugJZVu9y2uGe6OKf7tJvq8dt+243BhdpQb7bt+PytGd8mKXfbdeDqrLmNjmTepELG6QaMVpcbssVXC3dvxrrHptvW8vdH/p+UbrYrVkuWzGx5L7a8n+nAX2QACbw+i17PTbxsoq+i91eXWnbkW0ECJ3tLw7s6mcoXb+Kffj02fQf9tKWx79uxWZ+6/Y1Dc3dXD6fRonpoNgu5x6PHv/dNN9KLyQ/j47rGtOXQb6Z5H/mKj/5r67u6u5km005OEfj//uvff0XMFxfnBEt8cSnBN9MivpF9jBFe425odiu09e0IZTr+vVMFM+/9ket+HHvcJi50KkO8yMM5bN9tzpnsLiqdoFgn3n56h3kNn95Uw+NfKvjotmo39d4nf2s6rPdT1Hff10PvXoveSO4ppxmb+/5D8H7VdLCF1F40nTNlVT1MY9XDfhOP801skARYcxy2zhJWdD7U2MQUG1G3n7IPDK39fU9+otvTKIxm022am9pZg3XGaoCXce51AxxUjQvT/bL7NUKtu1U3LRYddquOPVy5KHeRe5keBsamHhq6UfaN76oVqRF4BfCsupsWl8jN8ofdq3nA91fTnPW4j343PpJ/p7UpsXG5fzr9jDBNdeMxFpp+I9i/ut7/KkCCsx8JDs5xixyY5jxMyBh+4BScW9ZW13U7H2B+WVUjJ+rRuhqqFXHj7H0U8Wh0dhZ127aN/hAtFtFpdFGN92ogqIuU+OVYjf07MEJ3d3S8N3xn0y6xZ+vPbdkF5gLG77w/OQ+bJU7wS2gcc5j3H7bjVC7pvRcHQ+UFbDHcXR+hE8viN0V6HOifbSL44+AO3E8RYk6wlEMacjd4wZO/a4iTnLw34fNa5ZeQZ5XuqOOAUhH/Qp3Vj/VQ3dWyDpgt4p9lDmt3cz9twVPZmQGzSAdFSD4HoSzwVWRs9DpafH09fLPAg0Mt4VExtKKffwayD6HWSynHz7tZm8fVdY+Uulx0fQfLI/memfoU8vPn0Kk7t94O/Spy8Rz76BAeng3r5wI7N36xbIZlG4Zlev+u+QdU6CCdDpLJnNZnpW//8eBEn3q1vq82+2L1IgXPqjigJBgI/gzX/2R6fjZBf/OpfDxKPkgawGvUIpU+wxfPstWzXPEpvdXDi/Q+R80v4yg3FnQ8U32jftbvO8iqoOGYHV6O0h0zbVZ9P977bRMJP6lt/bpaNuPjaYTD5Qutovb6i8zS8a9iF7X7X2SX+XXsosP/F9mV/AJ2AXI4qf0fzCuQFw==",title:"Stocks",type:"js"})])}const s=o(c,[["render",d],["__file","index.html.vue"]]),M=JSON.parse('{"path":"/stock/","title":"股票预测","lang":"zh-CN","frontmatter":{"description":"股票预测","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/stock/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"股票预测"}],["meta",{"property":"og:description","content":"股票预测"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-07-02T14:33:09.000Z"}],["meta",{"property":"article:modified_time","content":"2024-07-02T14:33:09.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"股票预测\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-07-02T14:33:09.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1719930789000,"updatedTime":1719930789000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.15,"words":946},"filePathRelative":"stock/README.md","localizedDate":"2024年7月2日","excerpt":"\\n","autoDesc":true}');export{s as comp,M as data}; diff --git a/assets/index.html-wU0YCmBl.js b/assets/index.html-wU0YCmBl.js new file mode 100644 index 00000000..5ac21d39 --- /dev/null +++ b/assets/index.html-wU0YCmBl.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as i}from"./app-ftEjETWs.js";const n={};function o(r,a){return i(),t("div")}const m=e(n,[["render",o],["__file","index.html.vue"]]),p=JSON.parse('{"path":"/interesting/mini%E4%B8%BB%E6%9C%BA/","title":"mini主机","lang":"zh-CN","frontmatter":{"title":"mini主机","icon":"pen","dir":{"collapsible":true,"order":2},"index":false,"head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"mini主机"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T18:45:10.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T18:45:10.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"mini主机\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-11-29T18:45:10.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708056026000,"updatedTime":1732905910000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":0.04,"words":13},"filePathRelative":"interesting/mini主机/README.md","localizedDate":"2024年2月16日","excerpt":""}');export{m as comp,p as data}; diff --git "a/assets/jvm(java\350\231\232\346\213\237\346\234\272).html-CpFuSPpy.js" "b/assets/jvm(java\350\231\232\346\213\237\346\234\272).html-CpFuSPpy.js" new file mode 100644 index 00000000..5592037b --- /dev/null +++ "b/assets/jvm(java\350\231\232\346\213\237\346\234\272).html-CpFuSPpy.js" @@ -0,0 +1,7 @@ +import{_ as e,c as r,d as n,o as i}from"./app-ftEjETWs.js";const t={};function o(s,a){return i(),r("div",null,a[0]||(a[0]=[n(`

一、了解JVM

1、什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

微信图片_20220522232432.png
微信图片_20220522232432.png

2、JRE/JDK/JVM是什么关系

JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
JDK(Java Development Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
JVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。

3、JVM体系结构:

类加载器:加载class文件;
运行时数据区:包括方法区、堆、Java栈、程序计数器、本地方法栈
执行引擎:执行字节码或者执行本地方法

img
img

二、运行时数据区

方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。从 JDK 8 开始,HotSpot 虚拟机移除了方法区,取而代之的是元空间(Metaspace)。元空间并不在 Java 虚拟机内存中,而是使用了本地(即操作系统)的内存。这个改变主要是为了解决方法区可能出现的内存溢出问题。
:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。
:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。

元空间

三、类加载机制

类加载器通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。

1、类的生命周期(7个)

加载、验证、准备、解析、初始化、使用、卸载

2、类加载的五个过程

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

加载:类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
初始化:前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

3、类加载器

启动类加载器(BootStrap ClassLoader):主要负责加载jre/lib/rt.jar相关的字节码文件的。
扩展类加载器(Extension Class Loader):主要负载加载 jre/lib/ext/*.jar 这些jar包的。
应用程序类加载器(Application Class Loader):主要负责加载用户自定义的类以及classpath环境变量所配置的jar包的。
自定义类加载器(User Class Loader):负责加载程序员指定的特殊目录下的字节码文件的。大多数情况下,自定义类加载器只需要继承ClassLoader这个抽象类,重写findClass()和loadClass()两个方法即可。

4、类加载机制(双亲委派)

类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。

img
img

工作过程:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。

四、垃圾回收

程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。 Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

1、判断对象已死

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

引用计数法:给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;

img
img
    注意:如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的            两块内存依然保持着互相引用无法回收。引用计数法很难解决循环引用问题; 
+

可达性分析:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

img
img
    可作为 GC Roots 的对象:
+    1)虚拟机栈中引用的对象
+    2)方法区中类静态属性引用的对象
+    3)方法区中常量引用的对象
+    4)本地方法栈中native方法引用的对象 
+

引用:下面四种引用强度依次减弱
强引用:默认情况下,对象采用的均为强引用;
软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

2、垃圾收集算法

标记清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
标记整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记清除算法。

3、垃圾收集器

Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
Parnew收集器:Serial收集器多线程版本。
Parallel Scavenge收集器:使用复制算法的新生代收集器。
Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。
G1收集器有以下特点
1)并行与并发:无需停顿Java线程来执行GC动作。
2)分代收集:可独立管理整个GC堆。
3)空间整合:运行期间不会产生内存空间碎片。
4)可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

4、JVM内存分代机制

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。

新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。
GC 过程:新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区 没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于 Eden区和Survivor From区,Survivor To区是空的(作为保留区域)。GC进行时,Eden区中 所有存活的对象都会被复制到Survivor To区,而在Survivor From区中,仍存活的对象会根据 它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾 回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有 达到阀值的对象会被复制到Survivor To区。接着清空Eden区和Survivor From区,新生代中存 活的对象都在Survivor To区。接着,Survivor From区和Survivor To区会交换它们的角色,也 就是新的Survivor To区就是上次GC清空的Survivor From区,新的Survivor From区就是上次 GC的Survivor To区,总之,不管怎样都会保证Survivor To区在一轮GC后是空的。GC时当 Survivor To区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进 行分配担保,将这些对象存放在老年代中。

5、Minor GC、Major GC、Full GC之间的区别

Minor GC:Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
Major GC:指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
Full GC:Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

6、Minor GC、Major GC、Full GC触发条件

Minor GC触发条件
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总 空间
1)如果大于的话,直接执行minorGC
2)如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3)如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历 次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC
4)如果大于的话,执行minorGC

Full GC触发条件
1)老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存 在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是 不要创建太大的对象。
2)方法区空间不足:系统当中需要加载的类,调用的方法很多,同时方法区当中没有足够的 空间,就出触发一次Full GC
3)老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
4)调用System.gc()时(系统建议执行Full GC,但是不必然执行)

`,48)]))}const g=e(t,[["render",o],["__file","jvm(java虚拟机).html.vue"]]),c=JSON.parse('{"path":"/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html","title":"JVM(java虚拟机)","lang":"zh-CN","frontmatter":{"title":"JVM(java虚拟机)","icon":"pen","dir":{"collapsible":true,"order":100},"index":false,"description":"一、了解JVM 1、什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"JVM(java虚拟机)"}],["meta",{"property":"og:description","content":"一、了解JVM 1、什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/31584f04c69f4fdaa922c5bd1517cc97.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"JVM(java虚拟机)\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/31584f04c69f4fdaa922c5bd1517cc97.png\\",\\"https://github-images.wenzhihuai.com/images/1a14ee042cfa45999fa30c5f14f930e0.png\\",\\"https://github-images.wenzhihuai.com/images/3fa489e7f78042da8d9aa8a4bd1fd2a4.png\\",\\"https://github-images.wenzhihuai.com/images/c30fea6ea6e048289ea3e754919cb2a8.png\\",\\"https://github-images.wenzhihuai.com/images/cb3d4fadc552626ae0aaabc46d59b357.png\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、了解JVM","slug":"一、了解jvm","link":"#一、了解jvm","children":[]},{"level":2,"title":"二、运行时数据区","slug":"二、运行时数据区","link":"#二、运行时数据区","children":[]},{"level":2,"title":"三、类加载机制","slug":"三、类加载机制","link":"#三、类加载机制","children":[{"level":3,"title":"1、类的生命周期(7个)","slug":"_1、类的生命周期-7个","link":"#_1、类的生命周期-7个","children":[]},{"level":3,"title":"2、类加载的五个过程","slug":"_2、类加载的五个过程","link":"#_2、类加载的五个过程","children":[]},{"level":3,"title":"3、类加载器","slug":"_3、类加载器","link":"#_3、类加载器","children":[]},{"level":3,"title":"4、类加载机制(双亲委派)","slug":"_4、类加载机制-双亲委派","link":"#_4、类加载机制-双亲委派","children":[]}]},{"level":2,"title":"四、垃圾回收","slug":"四、垃圾回收","link":"#四、垃圾回收","children":[{"level":3,"title":"1、判断对象已死","slug":"_1、判断对象已死","link":"#_1、判断对象已死","children":[]},{"level":3,"title":"2、垃圾收集算法","slug":"_2、垃圾收集算法","link":"#_2、垃圾收集算法","children":[]},{"level":3,"title":"3、垃圾收集器","slug":"_3、垃圾收集器","link":"#_3、垃圾收集器","children":[]},{"level":3,"title":"4、JVM内存分代机制","slug":"_4、jvm内存分代机制","link":"#_4、jvm内存分代机制","children":[]},{"level":3,"title":"5、Minor GC、Major GC、Full GC之间的区别","slug":"_5、minor-gc、major-gc、full-gc之间的区别","link":"#_5、minor-gc、major-gc、full-gc之间的区别","children":[]},{"level":3,"title":"6、Minor GC、Major GC、Full GC触发条件","slug":"_6、minor-gc、major-gc、full-gc触发条件","link":"#_6、minor-gc、major-gc、full-gc触发条件","children":[]}]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":12.39,"words":3717},"filePathRelative":"java/JVM/jvm(java虚拟机).md","localizedDate":"2024年3月10日","excerpt":"

一、了解JVM

\\n

1、什么是JVM

\\n

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

","autoDesc":true}');export{g as comp,c as data}; diff --git a/assets/kafka.html-tMeeu2BM.js b/assets/kafka.html-tMeeu2BM.js new file mode 100644 index 00000000..25822ad4 --- /dev/null +++ b/assets/kafka.html-tMeeu2BM.js @@ -0,0 +1 @@ +import{_ as r,c as e,d as k,o}from"./app-ftEjETWs.js";const p={};function b(t,a){return o(),e("div",null,a[0]||(a[0]=[k('

kafka面试题

1、请说明什么是Apache Kafka?
Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

2、说说Kafka的使用场景?
①异步处理
②应用解耦
③流量削峰
④日志处理
⑤消息通讯等。

3、使用Kafka有什么优点和缺点?
优点:
①支持跨数据中心的消息复制;
②单机吞吐量:十万级,最大的优点,就是吞吐量高;
③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;
④时效性:ms级;
⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;
⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。

缺点:
①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序;
②有可能消息重复消费;
③依赖zookeeper进行元数据管理,等等。

4、为什么说Kafka性能很好,体现在哪里?
①顺序读写
②零拷贝
③分区
④批量发送
⑤数据压缩

5、请说明什么是传统的消息传递方法?
传统的消息传递方法包括两种:
排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。
发布-订阅:在这个模型中,消息被广播给所有的用户。

6、请说明Kafka相对传统技术有什么优势?
①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。
②可伸缩:在一组机器上对数据进行分区
③和简化,以支持更大的数据
④持久:消息是持久性的,并在集群中进
⑤行复制,以防止数据丢失。
⑥设计:它提供了容错保证和持久性

7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗?
Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。
不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。
Zookeeper主要用于在集群中不同节点之间进行通信
在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

8、解释Kafka的用户如何消费信息?
在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。

9、解释如何提高远程用户的吞吐量?
如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。

10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息?
在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。
这里有两种方法,可以在数据生成时准确地获得一个语义:
每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功
在消息中包含一个主键(UUID或其他),并在用户中进行反复制

11、解释如何减少ISR中的扰动?broker什么时候离开ISR?
ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。

12、Kafka为什么需要复制?
Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。

13、如果副本在ISR中停留了很长时间表明什么?
如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。

14、请说明如果首选的副本不在ISR中会发生什么?
如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。

15、有可能在生产后发生消息偏移吗?
在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。

作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。

16、Kafka的设计时什么样的呢?
Kafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息

17、数据传输的事物定义有哪三种?
(1)最多一次:
消息不会被重复发送,最多被传输一次,但也有可能一次不传输
(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

18、Kafka判断一个节点是否还活着有那两个条件?
(1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接
(2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久

19、producer是否直接将数据发送到broker的leader(主节点)?
producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。

20、Kafa consumer是否可以消费指定分区消息?
Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的

21、Kafka消息是采用Pull模式,还是Push模式?
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式

Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略

Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

22、Kafka存储在硬盘上的消息格式是什么?
消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。
消息长度: 4 bytes (value: 1+4+n)
版本号: 1 byte
CRC校验码: 4 bytes
具体的消息: n bytes

23、Kafka高效文件存储设计特点:
(1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
(2).通过索引信息可以快速定位message和确定response的最大大小。
(3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
(4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

24、Kafka 与传统消息系统之间有三个关键区别
(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留
(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
(3).Kafka 支持实时的流式处理

25、Kafka创建Topic时如何将分区放置到不同的Broker中
副本因子不能大于 Broker 的个数;
第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的

26、Kafka新建的分区会在哪个目录下创建
在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。

27、partition的数据如何保存到硬盘
topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名

28、kafka的ack机制
request.required.acks有三个值 0 1 -1
0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据
1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失
-1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失

29、Kafka的消费者如何消费数据
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。

30、消费者负载均衡策略
结合consumer的加入和退出进行再平衡策略。

31、kafka消息数据是否有序?
消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。

32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中
每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition

33、kafka consumer 什么情况会触发再平衡reblance?
①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。
②订阅主题分区发生变化,所有消费者也都要再平衡。

34、描述下kafka consumer 再平衡步骤?
①关闭数据拉取线程,情空队列和消息流,提交偏移量;
②释放分区所有权,删除zk中分区和消费者的所有者关系;
③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区;
④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息;
⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。

35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么
36、Kafka中的HW、LEO、LSO、LW等分别代表什么?
37、Kafka中是怎么体现消息顺序性的?
38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
39、Kafka生产者客户端的整体结构是什么样子的?
40、Kafka生产者客户端中使用了几个线程来处理?分别是什么?
41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷?
42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段?
43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
44、有哪些情形会造成重复消费?
45、那些情景下会造成消息漏消费?
46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?
47、简述消费者与消费组之间的关系
48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
51、创建topic时如何选择合适的分区数?
52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么?
53、优先副本是什么?它有什么特殊的作用?
54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理
55、简述Kafka的日志目录结构
56、Kafka中有那些索引文件?
57、如果我指定了一个offset,Kafka怎么查找到对应的消息?
58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息?
59、聊一聊你对Kafka的Log Retention的理解
60、聊一聊你对Kafka的Log Compaction的理解
61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层)
62、聊一聊Kafka的延时操作的原理
63、聊一聊Kafka控制器的作用
64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器)
65、Kafka中的幂等是怎么实现的
66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”)
67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
68、失效副本是指什么?有那些应对措施?
69、多副本下,各个副本中的HW和LEO的演变过程
70、为什么Kafka不支持读写分离?
71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch)
72、Kafka中怎么实现死信队列和重试队列?
73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?)
74、Kafka中怎么做消息审计?
75、Kafka中怎么做消息轨迹?
76、Kafka中有那些配置参数比较有意思?聊一聊你的看法
77、Kafka中有那些命名比较有意思?聊一聊你的看法
78、Kafka有哪些指标需要着重关注?
79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同)
80、Kafka的那些设计让它有如此高的性能?
81、Kafka有什么优缺点?
82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点?
83、为什么选择Kafka?
84、在使用Kafka的过程中遇到过什么困难?怎么解决的?
85、怎么样才能确保Kafka极大程度上的可靠性?
86、聊一聊你对Kafka生态的理解

',40)]))}const c=r(p,[["render",b],["__file","kafka.html.vue"]]),i=JSON.parse('{"path":"/middleware/kafka/kafka.html","title":"kafka面试题","lang":"zh-CN","frontmatter":{"description":"kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/kafka/kafka.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"kafka面试题"}],["meta",{"property":"og:description","content":"kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T17:34:45.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-25T17:34:45.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"kafka面试题\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T17:34:45.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1647789280000,"updatedTime":1706204085000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":17.01,"words":5103},"filePathRelative":"middleware/kafka/kafka.md","localizedDate":"2022年3月20日","excerpt":"\\n

1、请说明什么是Apache Kafka?
\\nApache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

","autoDesc":true}');export{c as comp,i as data}; diff --git "a/assets/mac\350\277\236\346\216\245\344\270\273\346\234\272.html-BbWWw_2k.js" "b/assets/mac\350\277\236\346\216\245\344\270\273\346\234\272.html-BbWWw_2k.js" new file mode 100644 index 00000000..9bb670e0 --- /dev/null +++ "b/assets/mac\350\277\236\346\216\245\344\270\273\346\234\272.html-BbWWw_2k.js" @@ -0,0 +1,41 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const l={};function t(p,s){return e(),a("div",null,s[0]||(s[0]=[n(`

mac通过网线连接主机(fnOS)

一、mac端

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

点击详细信息,配置IPv4选择使用DHCP

二、主机端

主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。

root@server:~# sudo lshw -C network
+  *-network
+       description: Wireless interface
+       product: Wi-Fi 6 AX210/AX211/AX411 160MHz
+       vendor: Intel Corporation
+       physical id: 0
+       bus info: pci@0000:01:00.0
+       logical name: wlp1s0
+       version: 1a
+       serial: 10:5f:ad:d6:2b:ee
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix bus_master cap_list ethernet physical wireless
+       configuration: broadcast=yes driver=iwlwifi driverversion=6.6.38-trim firmware=72.daa05125.0 ty-a0-gf-a0-72.uc ip=192.168.0.113 latency=0 link=yes multicast=yes wireless=IEEE 802.11
+       resources: irq:18 memory:80900000-80903fff
+  *-network UNCLAIMED
+       description: Ethernet controller
+       product: RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
+       vendor: Realtek Semiconductor Co., Ltd.
+       physical id: 0
+       bus info: pci@0000:02:00.0
+       version: 2b
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix cap_list
+       configuration: latency=0
+       resources: ioport:3000(size=256) memory:80804000-80804fff memory:80800000-80803fff

*-network UNCLAIMED 表明你的网络接口未被驱动程序识别和管理。为了解决这个问题,你需要安装或重新加载正确的网络驱动程序。

下载并安装 Realtek 网络驱动程序

访问 Realtek 官方网站下载适用于你的 RTL8111/8168/8411 网络控制器的驱动程序,通常是 r8168 驱动程序。

root@server:/vol1# cd /vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00/
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# ls
+autorun.sh  Makefile  README  src
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# sh autorun.sh
+
+Check old driver and unload it.
+Build the module and install
+Warning: modules_install: missing 'System.map' file. Skipping depmod.
+Backup r8169.ko
+rename r8169.ko to r8169.bak
+DEPMOD 6.6.38-trim
+load module r8168
+Updating initramfs. Please wait.
+update-initramfs: Generating /boot/initrd.img-6.6.38-trim
+Completed.

安装好了之后,理论上自动显示网口2,但IP什么的都是空的,需要点击编辑,然后按照下面的填一下。

三、最后

最后就可以互相ping通了,很稳定,传输速度很快很快,基本都能1ms以内

`,19)]))}const h=i(l,[["render",t],["__file","mac连接主机.html.vue"]]),d=JSON.parse('{"path":"/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html","title":"mac通过网线连接主机(fnOS)","lang":"zh-CN","frontmatter":{"description":"mac通过网线连接主机(fnOS) 一、mac端 mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的 点击详细信息,配置IPv4选择使用DHCP 二、主机端 主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。 *-network UNCLAIMED 表明你的...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"mac通过网线连接主机(fnOS)"}],["meta",{"property":"og:description","content":"mac通过网线连接主机(fnOS) 一、mac端 mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的 点击详细信息,配置IPv4选择使用DHCP 二、主机端 主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。 *-network UNCLAIMED 表明你的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/QQ_1732811324411.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T18:45:10.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T18:45:10.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"mac通过网线连接主机(fnOS)\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/QQ_1732811324411.png\\",\\"https://github-images.wenzhihuai.com/images/b1efd8cf4b16b9b7ffe0e11500258a91.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1732806426577.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1732806404944.png\\"],\\"dateModified\\":\\"2024-11-29T18:45:10.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、mac端","slug":"一、mac端","link":"#一、mac端","children":[]},{"level":2,"title":"二、主机端","slug":"二、主机端","link":"#二、主机端","children":[]},{"level":2,"title":"三、最后","slug":"三、最后","link":"#三、最后","children":[]}],"git":{"createdTime":1732905910000,"updatedTime":1732905910000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":1.72,"words":515},"filePathRelative":"interesting/mini主机/mac连接主机.md","localizedDate":"2024年11月29日","excerpt":"\\n

一、mac端

\\n

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

","autoDesc":true}');export{h as comp,d as data}; diff --git a/assets/main.html-B2OQipck.js b/assets/main.html-B2OQipck.js new file mode 100644 index 00000000..da3d8414 --- /dev/null +++ b/assets/main.html-B2OQipck.js @@ -0,0 +1 @@ +import{_ as o,c as n,a as i,b as t,o as r,r as p}from"./app-ftEjETWs.js";const m={};function s(h,a){const e=p("SiteInfo");return r(),n("div",null,[a[0]||(a[0]=i("h1",{id:"友链地址",tabindex:"-1"},[i("a",{class:"header-anchor",href:"#友链地址"},[i("span",null,"友链地址")])],-1)),t(e,{name:"张笨笨的博客",url:"https://www.256kb.cn/public/index/",preview:"https://github-images.wenzhihuai.com/images/image-20240202154324914.png"}),t(e,{name:"javaguide",url:"https://javaguide.cn/",preview:"https://github-images.wenzhihuai.com/images/image-20240223152512731.png"}),t(e,{name:"二哥的Java进阶之路",url:"https://javabetter.cn/",preview:"https://github-images.wenzhihuai.com/images/image-20240223152910564.png"}),t(e,{name:"Mr.Hope's Blog",desc:"Where there is light, there is hope",url:"https://mister-hope.com",logo:"https://mister-hope.com/logo.svg",repo:"https://github.com/Mister-Hope/Mister-Hope.github.io",preview:"https://theme-hope.vuejs.press/assets/image/mrhope.jpg"})])}const l=o(m,[["render",s],["__file","main.html.vue"]]),g=JSON.parse('{"path":"/link/main.html","title":"友链地址","lang":"zh-CN","frontmatter":{"description":"友链地址","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/link/main.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"友链地址"}],["meta",{"property":"og:description","content":"友链地址"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-23T07:30:13.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-23T07:30:13.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"友链地址\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-23T07:30:13.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706846339000,"updatedTime":1708673413000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":0.28,"words":83},"filePathRelative":"link/main.md","localizedDate":"2024年2月2日","excerpt":"\\n","autoDesc":true}');export{l as comp,g as data}; diff --git a/assets/nio.html-DVcQhX_V.js b/assets/nio.html-DVcQhX_V.js new file mode 100644 index 00000000..86943ad5 --- /dev/null +++ b/assets/nio.html-DVcQhX_V.js @@ -0,0 +1 @@ +import{_ as e,c as a,d as o,o as d}from"./app-ftEjETWs.js";const n={};function r(i,t){return d(),a("div",null,t[0]||(t[0]=[o('

JAVA NIO

一、简介

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

3)Java AIO:异步非阻塞,在AIO模型中,当我们发起一个I/O操作(如读或写)时,我们不需要等待它完成,我们的代码会立即返回,可以继续执行其他任务。当I/O操作完成时,我们之前注册的回调函数会被自动调用,我们可以在这个回调函数中处理I/O操作的结果。

1.1典型的多路复用IO实现

目前流程的多路复用IO实现主要包括四种: selectpollepollkqueue。下表是他们的一些重要特性的比较:

IO模型相对性能数据结构关键思路操作系统JAVA支持情况
select较高位图Reactorwindows/Linux支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型
poll较高数组或者链表ReactorLinuxLinux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式
epoll红黑树Reactor/ProactorLinuxLinux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO
kqueueProactorLinux目前JAVA的版本不支持

多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用IO技术发挥不出

参考

1.元动力

',11)]))}const c=e(n,[["render",r],["__file","nio.html.vue"]]),p=JSON.parse('{"path":"/java/io/nio.html","title":"JAVA NIO","lang":"zh-CN","frontmatter":{"description":"JAVA NIO 一、简介 1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/io/nio.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"JAVA NIO"}],["meta",{"property":"og:description","content":"JAVA NIO 一、简介 1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"JAVA NIO\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、简介","slug":"一、简介","link":"#一、简介","children":[{"level":3,"title":"1.1典型的多路复用IO实现","slug":"_1-1典型的多路复用io实现","link":"#_1-1典型的多路复用io实现","children":[]}]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":1.96,"words":587},"filePathRelative":"java/io/nio.md","localizedDate":"2024年3月10日","excerpt":"\\n

一、简介

\\n

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

","autoDesc":true}');export{c as comp,p as data}; diff --git a/assets/photoswipe.esm-GXRgw7eJ.js b/assets/photoswipe.esm-GXRgw7eJ.js new file mode 100644 index 00000000..2fd7cfd3 --- /dev/null +++ b/assets/photoswipe.esm-GXRgw7eJ.js @@ -0,0 +1,4 @@ +/*! + * PhotoSwipe 5.4.4 - https://photoswipe.com + * (c) 2024 Dmytro Semenov + */function f(r,t,i){const e=document.createElement(t);return r&&(e.className=r),i&&i.appendChild(e),e}function p(r,t){return r.x=t.x,r.y=t.y,t.id!==void 0&&(r.id=t.id),r}function M(r){r.x=Math.round(r.x),r.y=Math.round(r.y)}function A(r,t){const i=Math.abs(r.x-t.x),e=Math.abs(r.y-t.y);return Math.sqrt(i*i+e*e)}function x(r,t){return r.x===t.x&&r.y===t.y}function I(r,t,i){return Math.min(Math.max(r,t),i)}function b(r,t,i){let e=`translate3d(${r}px,${t||0}px,0)`;return i!==void 0&&(e+=` scale3d(${i},${i},1)`),e}function y(r,t,i,e){r.style.transform=b(t,i,e)}const $="cubic-bezier(.4,0,.22,1)";function R(r,t,i,e){r.style.transition=t?`${t} ${i}ms ${e||$}`:"none"}function L(r,t,i){r.style.width=typeof t=="number"?`${t}px`:t,r.style.height=typeof i=="number"?`${i}px`:i}function U(r){R(r)}function q(r){return"decode"in r?r.decode().catch(()=>{}):r.complete?Promise.resolve(r):new Promise((t,i)=>{r.onload=()=>t(r),r.onerror=i})}const _={IDLE:"idle",LOADING:"loading",LOADED:"loaded",ERROR:"error"};function G(r){return"button"in r&&r.button===1||r.ctrlKey||r.metaKey||r.altKey||r.shiftKey}function K(r,t,i=document){let e=[];if(r instanceof Element)e=[r];else if(r instanceof NodeList||Array.isArray(r))e=Array.from(r);else{const s=typeof r=="string"?r:t;s&&(e=Array.from(i.querySelectorAll(s)))}return e}function C(){return!!(navigator.vendor&&navigator.vendor.match(/apple/i))}let F=!1;try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>{F=!0}}))}catch{}class X{constructor(){this._pool=[]}add(t,i,e,s){this._toggleListener(t,i,e,s)}remove(t,i,e,s){this._toggleListener(t,i,e,s,!0)}removeAll(){this._pool.forEach(t=>{this._toggleListener(t.target,t.type,t.listener,t.passive,!0,!0)}),this._pool=[]}_toggleListener(t,i,e,s,n,o){if(!t)return;const a=n?"removeEventListener":"addEventListener";i.split(" ").forEach(l=>{if(l){o||(n?this._pool=this._pool.filter(d=>d.type!==l||d.listener!==e||d.target!==t):this._pool.push({target:t,type:l,listener:e,passive:s}));const c=F?{passive:s||!1}:!1;t[a](l,e,c)}})}}function B(r,t){if(r.getViewportSizeFn){const i=r.getViewportSizeFn(r,t);if(i)return i}return{x:document.documentElement.clientWidth,y:window.innerHeight}}function S(r,t,i,e,s){let n=0;if(t.paddingFn)n=t.paddingFn(i,e,s)[r];else if(t.padding)n=t.padding[r];else{const o="padding"+r[0].toUpperCase()+r.slice(1);t[o]&&(n=t[o])}return Number(n)||0}function N(r,t,i,e){return{x:t.x-S("left",r,t,i,e)-S("right",r,t,i,e),y:t.y-S("top",r,t,i,e)-S("bottom",r,t,i,e)}}class Y{constructor(t){this.slide=t,this.currZoomLevel=1,this.center={x:0,y:0},this.max={x:0,y:0},this.min={x:0,y:0}}update(t){this.currZoomLevel=t,this.slide.width?(this._updateAxis("x"),this._updateAxis("y"),this.slide.pswp.dispatch("calcBounds",{slide:this.slide})):this.reset()}_updateAxis(t){const{pswp:i}=this.slide,e=this.slide[t==="x"?"width":"height"]*this.currZoomLevel,n=S(t==="x"?"left":"top",i.options,i.viewportSize,this.slide.data,this.slide.index),o=this.slide.panAreaSize[t];this.center[t]=Math.round((o-e)/2)+n,this.max[t]=e>o?Math.round(o-e)+n:this.center[t],this.min[t]=e>o?n:this.center[t]}reset(){this.center.x=0,this.center.y=0,this.max.x=0,this.max.y=0,this.min.x=0,this.min.y=0}correctPan(t,i){return I(i,this.max[t],this.min[t])}}const T=4e3;class H{constructor(t,i,e,s){this.pswp=s,this.options=t,this.itemData=i,this.index=e,this.panAreaSize=null,this.elementSize=null,this.fit=1,this.fill=1,this.vFill=1,this.initial=1,this.secondary=1,this.max=1,this.min=1}update(t,i,e){const s={x:t,y:i};this.elementSize=s,this.panAreaSize=e;const n=e.x/s.x,o=e.y/s.y;this.fit=Math.min(1,no?n:o),this.vFill=Math.min(1,o),this.initial=this._getInitial(),this.secondary=this._getSecondary(),this.max=Math.max(this.initial,this.secondary,this._getMax()),this.min=Math.min(this.fit,this.initial,this.secondary),this.pswp&&this.pswp.dispatch("zoomLevelsUpdate",{zoomLevels:this,slideData:this.itemData})}_parseZoomLevelOption(t){const i=t+"ZoomLevel",e=this.options[i];if(e)return typeof e=="function"?e(this):e==="fill"?this.fill:e==="fit"?this.fit:Number(e)}_getSecondary(){let t=this._parseZoomLevelOption("secondary");return t||(t=Math.min(1,this.fit*3),this.elementSize&&t*this.elementSize.x>T&&(t=T/this.elementSize.x),t)}_getInitial(){return this._parseZoomLevelOption("initial")||this.fit}_getMax(){return this._parseZoomLevelOption("max")||Math.max(1,this.fit*4)}}class j{constructor(t,i,e){this.data=t,this.index=i,this.pswp=e,this.isActive=i===e.currIndex,this.currentResolution=0,this.panAreaSize={x:0,y:0},this.pan={x:0,y:0},this.isFirstSlide=this.isActive&&!e.opener.isOpen,this.zoomLevels=new H(e.options,t,i,e),this.pswp.dispatch("gettingData",{slide:this,data:this.data,index:i}),this.content=this.pswp.contentLoader.getContentBySlide(this),this.container=f("pswp__zoom-wrap","div"),this.holderElement=null,this.currZoomLevel=1,this.width=this.content.width,this.height=this.content.height,this.heavyAppended=!1,this.bounds=new Y(this),this.prevDisplayedWidth=-1,this.prevDisplayedHeight=-1,this.pswp.dispatch("slideInit",{slide:this})}setIsActive(t){t&&!this.isActive?this.activate():!t&&this.isActive&&this.deactivate()}append(t){this.holderElement=t,this.container.style.transformOrigin="0 0",this.data&&(this.calculateSize(),this.load(),this.updateContentSize(),this.appendHeavy(),this.holderElement.appendChild(this.container),this.zoomAndPanToInitial(),this.pswp.dispatch("firstZoomPan",{slide:this}),this.applyCurrentZoomPan(),this.pswp.dispatch("afterSetContent",{slide:this}),this.isActive&&this.activate())}load(){this.content.load(!1),this.pswp.dispatch("slideLoad",{slide:this})}appendHeavy(){const{pswp:t}=this;this.heavyAppended||!t.opener.isOpen||t.mainScroll.isShifted()||!this.isActive&&!!0||this.pswp.dispatch("appendHeavy",{slide:this}).defaultPrevented||(this.heavyAppended=!0,this.content.append(),this.pswp.dispatch("appendHeavyContent",{slide:this}))}activate(){this.isActive=!0,this.appendHeavy(),this.content.activate(),this.pswp.dispatch("slideActivate",{slide:this})}deactivate(){this.isActive=!1,this.content.deactivate(),this.currZoomLevel!==this.zoomLevels.initial&&this.calculateSize(),this.currentResolution=0,this.zoomAndPanToInitial(),this.applyCurrentZoomPan(),this.updateContentSize(),this.pswp.dispatch("slideDeactivate",{slide:this})}destroy(){this.content.hasSlide=!1,this.content.remove(),this.container.remove(),this.pswp.dispatch("slideDestroy",{slide:this})}resize(){this.currZoomLevel===this.zoomLevels.initial||!this.isActive?(this.calculateSize(),this.currentResolution=0,this.zoomAndPanToInitial(),this.applyCurrentZoomPan(),this.updateContentSize()):(this.calculateSize(),this.bounds.update(this.currZoomLevel),this.panTo(this.pan.x,this.pan.y))}updateContentSize(t){const i=this.currentResolution||this.zoomLevels.initial;if(!i)return;const e=Math.round(this.width*i)||this.pswp.viewportSize.x,s=Math.round(this.height*i)||this.pswp.viewportSize.y;!this.sizeChanged(e,s)&&!t||this.content.setDisplayedSize(e,s)}sizeChanged(t,i){return t!==this.prevDisplayedWidth||i!==this.prevDisplayedHeight?(this.prevDisplayedWidth=t,this.prevDisplayedHeight=i,!0):!1}getPlaceholderElement(){var t;return(t=this.content.placeholder)===null||t===void 0?void 0:t.element}zoomTo(t,i,e,s){const{pswp:n}=this;if(!this.isZoomable()||n.mainScroll.isShifted())return;n.dispatch("beforeZoomTo",{destZoomLevel:t,centerPoint:i,transitionDuration:e}),n.animations.stopAllPan();const o=this.currZoomLevel;s||(t=I(t,this.zoomLevels.min,this.zoomLevels.max)),this.setZoomLevel(t),this.pan.x=this.calculateZoomToPanOffset("x",i,o),this.pan.y=this.calculateZoomToPanOffset("y",i,o),M(this.pan);const a=()=>{this._setResolution(t),this.applyCurrentZoomPan()};e?n.animations.startTransition({isPan:!0,name:"zoomTo",target:this.container,transform:this.getCurrentTransform(),onComplete:a,duration:e,easing:n.options.easing}):a()}toggleZoom(t){this.zoomTo(this.currZoomLevel===this.zoomLevels.initial?this.zoomLevels.secondary:this.zoomLevels.initial,t,this.pswp.options.zoomAnimationDuration)}setZoomLevel(t){this.currZoomLevel=t,this.bounds.update(this.currZoomLevel)}calculateZoomToPanOffset(t,i,e){if(this.bounds.max[t]-this.bounds.min[t]===0)return this.bounds.center[t];i||(i=this.pswp.getViewportCenterPoint()),e||(e=this.zoomLevels.initial);const n=this.currZoomLevel/e;return this.bounds.correctPan(t,(this.pan[t]-i[t])*n+i[t])}panTo(t,i){this.pan.x=this.bounds.correctPan("x",t),this.pan.y=this.bounds.correctPan("y",i),this.applyCurrentZoomPan()}isPannable(){return!!this.width&&this.currZoomLevel>this.zoomLevels.fit}isZoomable(){return!!this.width&&this.content.isZoomable()}applyCurrentZoomPan(){this._applyZoomTransform(this.pan.x,this.pan.y,this.currZoomLevel),this===this.pswp.currSlide&&this.pswp.dispatch("zoomPanUpdate",{slide:this})}zoomAndPanToInitial(){this.currZoomLevel=this.zoomLevels.initial,this.bounds.update(this.currZoomLevel),p(this.pan,this.bounds.center),this.pswp.dispatch("initialZoomPan",{slide:this})}_applyZoomTransform(t,i,e){e/=this.currentResolution||this.zoomLevels.initial,y(this.container,t,i,e)}calculateSize(){const{pswp:t}=this;p(this.panAreaSize,N(t.options,t.viewportSize,this.data,this.index)),this.zoomLevels.update(this.width,this.height,this.panAreaSize),t.dispatch("calcSlideSize",{slide:this})}getCurrentTransform(){const t=this.currZoomLevel/(this.currentResolution||this.zoomLevels.initial);return b(this.pan.x,this.pan.y,t)}_setResolution(t){t!==this.currentResolution&&(this.currentResolution=t,this.updateContentSize(),this.pswp.dispatch("resolutionChanged"))}}const Q=.35,J=.6,z=.4,E=.5;function tt(r,t){return r*t/(1-t)}class it{constructor(t){this.gestures=t,this.pswp=t.pswp,this.startPan={x:0,y:0}}start(){this.pswp.currSlide&&p(this.startPan,this.pswp.currSlide.pan),this.pswp.animations.stopAll()}change(){const{p1:t,prevP1:i,dragAxis:e}=this.gestures,{currSlide:s}=this.pswp;if(e==="y"&&this.pswp.options.closeOnVerticalDrag&&s&&s.currZoomLevel<=s.zoomLevels.fit&&!this.gestures.isMultitouch){const n=s.pan.y+(t.y-i.y);if(!this.pswp.dispatch("verticalDrag",{panY:n}).defaultPrevented){this._setPanWithFriction("y",n,J);const o=1-Math.abs(this._getVerticalDragRatio(s.pan.y));this.pswp.applyBgOpacity(o),s.applyCurrentZoomPan()}}else this._panOrMoveMainScroll("x")||(this._panOrMoveMainScroll("y"),s&&(M(s.pan),s.applyCurrentZoomPan()))}end(){const{velocity:t}=this.gestures,{mainScroll:i,currSlide:e}=this.pswp;let s=0;if(this.pswp.animations.stopAll(),i.isShifted()){const o=(i.x-i.getCurrSlideX())/this.pswp.viewportSize.x;t.x<-E&&o<0||t.x<.1&&o<-.5?(s=1,t.x=Math.min(t.x,0)):(t.x>E&&o>0||t.x>-.1&&o>.5)&&(s=-1,t.x=Math.max(t.x,0)),i.moveIndexBy(s,!0,t.x)}e&&e.currZoomLevel>e.zoomLevels.max||this.gestures.isMultitouch?this.gestures.zoomLevels.correctZoomPan(!0):(this._finishPanGestureForAxis("x"),this._finishPanGestureForAxis("y"))}_finishPanGestureForAxis(t){const{velocity:i}=this.gestures,{currSlide:e}=this.pswp;if(!e)return;const{pan:s,bounds:n}=e,o=s[t],a=this.pswp.bgOpacity<1&&t==="y",l=o+tt(i[t],.995);if(a){const v=this._getVerticalDragRatio(o),w=this._getVerticalDragRatio(l);if(v<0&&w<-z||v>0&&w>z){this.pswp.close();return}}const c=n.correctPan(t,l);if(o===c)return;const d=c===l?1:.82,u=this.pswp.bgOpacity,m=c-o;this.pswp.animations.startSpring({name:"panGesture"+t,isPan:!0,start:o,end:c,velocity:i[t],dampingRatio:d,onUpdate:v=>{if(a&&this.pswp.bgOpacity<1){const w=1-(c-v)/m;this.pswp.applyBgOpacity(I(u+(1-u)*w,0,1))}s[t]=Math.floor(v),e.applyCurrentZoomPan()}})}_panOrMoveMainScroll(t){const{p1:i,dragAxis:e,prevP1:s,isMultitouch:n}=this.gestures,{currSlide:o,mainScroll:a}=this.pswp,h=i[t]-s[t],l=a.x+h;if(!h||!o)return!1;if(t==="x"&&!o.isPannable()&&!n)return a.moveTo(l,!0),!0;const{bounds:c}=o,d=o.pan[t]+h;if(this.pswp.options.allowPanToNext&&e==="x"&&t==="x"&&!n){const u=a.getCurrSlideX(),m=a.x-u,v=h>0,w=!v;if(d>c.min[t]&&v){if(c.min[t]<=this.startPan[t])return a.moveTo(l,!0),!0;this._setPanWithFriction(t,d)}else if(d0)return a.moveTo(Math.max(l,u),!0),!0;if(m<0)return a.moveTo(Math.min(l,u),!0),!0}else this._setPanWithFriction(t,d)}else t==="y"?!a.isShifted()&&c.min.y!==c.max.y&&this._setPanWithFriction(t,d):this._setPanWithFriction(t,d);return!1}_getVerticalDragRatio(t){var i,e;return(t-((i=(e=this.pswp.currSlide)===null||e===void 0?void 0:e.bounds.center.y)!==null&&i!==void 0?i:0))/(this.pswp.viewportSize.y/3)}_setPanWithFriction(t,i,e){const{currSlide:s}=this.pswp;if(!s)return;const{pan:n,bounds:o}=s;if(o.correctPan(t,i)!==i||e){const h=Math.round(i-n[t]);n[t]+=h*(e||Q)}else n[t]=i}}const et=.05,st=.15;function O(r,t,i){return r.x=(t.x+i.x)/2,r.y=(t.y+i.y)/2,r}class nt{constructor(t){this.gestures=t,this._startPan={x:0,y:0},this._startZoomPoint={x:0,y:0},this._zoomPoint={x:0,y:0},this._wasOverFitZoomLevel=!1,this._startZoomLevel=1}start(){const{currSlide:t}=this.gestures.pswp;t&&(this._startZoomLevel=t.currZoomLevel,p(this._startPan,t.pan)),this.gestures.pswp.animations.stopAllPan(),this._wasOverFitZoomLevel=!1}change(){const{p1:t,startP1:i,p2:e,startP2:s,pswp:n}=this.gestures,{currSlide:o}=n;if(!o)return;const a=o.zoomLevels.min,h=o.zoomLevels.max;if(!o.isZoomable()||n.mainScroll.isShifted())return;O(this._startZoomPoint,i,s),O(this._zoomPoint,t,e);let l=1/A(i,s)*A(t,e)*this._startZoomLevel;if(l>o.zoomLevels.initial+o.zoomLevels.initial/15&&(this._wasOverFitZoomLevel=!0),lh&&(l=h+(l-h)*et);o.pan.x=this._calculatePanForZoomLevel("x",l),o.pan.y=this._calculatePanForZoomLevel("y",l),o.setZoomLevel(l),o.applyCurrentZoomPan()}end(){const{pswp:t}=this.gestures,{currSlide:i}=t;(!i||i.currZoomLevele.zoomLevels.max?n=e.zoomLevels.max:(o=!1,n=s);const a=i.bgOpacity,h=i.bgOpacity<1,l=p({x:0,y:0},e.pan);let c=p({x:0,y:0},l);t&&(this._zoomPoint.x=0,this._zoomPoint.y=0,this._startZoomPoint.x=0,this._startZoomPoint.y=0,this._startZoomLevel=s,p(this._startPan,l)),o&&(c={x:this._calculatePanForZoomLevel("x",n),y:this._calculatePanForZoomLevel("y",n)}),e.setZoomLevel(n),c={x:e.bounds.correctPan("x",c.x),y:e.bounds.correctPan("y",c.y)},e.setZoomLevel(s);const d=!x(c,l);if(!d&&!o&&!h){e._setResolution(n),e.applyCurrentZoomPan();return}i.animations.stopAllPan(),i.animations.startSpring({isPan:!0,start:0,end:1e3,velocity:0,dampingRatio:1,naturalFrequency:40,onUpdate:u=>{if(u/=1e3,d||o){if(d&&(e.pan.x=l.x+(c.x-l.x)*u,e.pan.y=l.y+(c.y-l.y)*u),o){const m=s+(n-s)*u;e.setZoomLevel(m)}e.applyCurrentZoomPan()}h&&i.bgOpacity<1&&i.applyBgOpacity(I(a+(1-a)*u,0,1))},onComplete:()=>{e._setResolution(n),e.applyCurrentZoomPan()}})}}function Z(r){return!!r.target.closest(".pswp__container")}class ot{constructor(t){this.gestures=t}click(t,i){const e=i.target.classList,s=e.contains("pswp__img"),n=e.contains("pswp__item")||e.contains("pswp__zoom-wrap");s?this._doClickOrTapAction("imageClick",t,i):n&&this._doClickOrTapAction("bgClick",t,i)}tap(t,i){Z(i)&&this._doClickOrTapAction("tap",t,i)}doubleTap(t,i){Z(i)&&this._doClickOrTapAction("doubleTap",t,i)}_doClickOrTapAction(t,i,e){var s;const{pswp:n}=this.gestures,{currSlide:o}=n,a=t+"Action",h=n.options[a];if(!n.dispatch(a,{point:i,originalEvent:e}).defaultPrevented){if(typeof h=="function"){h.call(n,i,e);return}switch(h){case"close":case"next":n[h]();break;case"zoom":o==null||o.toggleZoom(i);break;case"zoom-or-close":o!=null&&o.isZoomable()&&o.zoomLevels.secondary!==o.zoomLevels.initial?o.toggleZoom(i):n.options.clickToCloseNonZoomable&&n.close();break;case"toggle-controls":(s=this.gestures.pswp.element)===null||s===void 0||s.classList.toggle("pswp--ui-visible");break}}}}const rt=10,at=300,ht=25;class lt{constructor(t){this.pswp=t,this.dragAxis=null,this.p1={x:0,y:0},this.p2={x:0,y:0},this.prevP1={x:0,y:0},this.prevP2={x:0,y:0},this.startP1={x:0,y:0},this.startP2={x:0,y:0},this.velocity={x:0,y:0},this._lastStartP1={x:0,y:0},this._intervalP1={x:0,y:0},this._numActivePoints=0,this._ongoingPointers=[],this._touchEventEnabled="ontouchstart"in window,this._pointerEventEnabled=!!window.PointerEvent,this.supportsTouch=this._touchEventEnabled||this._pointerEventEnabled&&navigator.maxTouchPoints>1,this._numActivePoints=0,this._intervalTime=0,this._velocityCalculated=!1,this.isMultitouch=!1,this.isDragging=!1,this.isZooming=!1,this.raf=null,this._tapTimer=null,this.supportsTouch||(t.options.allowPanToNext=!1),this.drag=new it(this),this.zoomLevels=new nt(this),this.tapHandler=new ot(this),t.on("bindEvents",()=>{t.events.add(t.scrollWrap,"click",this._onClick.bind(this)),this._pointerEventEnabled?this._bindEvents("pointer","down","up","cancel"):this._touchEventEnabled?(this._bindEvents("touch","start","end","cancel"),t.scrollWrap&&(t.scrollWrap.ontouchmove=()=>{},t.scrollWrap.ontouchend=()=>{})):this._bindEvents("mouse","down","up")})}_bindEvents(t,i,e,s){const{pswp:n}=this,{events:o}=n,a=s?t+s:"";o.add(n.scrollWrap,t+i,this.onPointerDown.bind(this)),o.add(window,t+"move",this.onPointerMove.bind(this)),o.add(window,t+e,this.onPointerUp.bind(this)),a&&o.add(n.scrollWrap,a,this.onPointerUp.bind(this))}onPointerDown(t){const i=t.type==="mousedown"||t.pointerType==="mouse";if(i&&t.button>0)return;const{pswp:e}=this;if(!e.opener.isOpen){t.preventDefault();return}e.dispatch("pointerDown",{originalEvent:t}).defaultPrevented||(i&&(e.mouseDetected(),this._preventPointerEventBehaviour(t,"down")),e.animations.stopAll(),this._updatePoints(t,"down"),this._numActivePoints===1&&(this.dragAxis=null,p(this.startP1,this.p1)),this._numActivePoints>1?(this._clearTapTimer(),this.isMultitouch=!0):this.isMultitouch=!1)}onPointerMove(t){this._preventPointerEventBehaviour(t,"move"),this._numActivePoints&&(this._updatePoints(t,"move"),!this.pswp.dispatch("pointerMove",{originalEvent:t}).defaultPrevented&&(this._numActivePoints===1&&!this.isDragging?(this.dragAxis||this._calculateDragDirection(),this.dragAxis&&!this.isDragging&&(this.isZooming&&(this.isZooming=!1,this.zoomLevels.end()),this.isDragging=!0,this._clearTapTimer(),this._updateStartPoints(),this._intervalTime=Date.now(),this._velocityCalculated=!1,p(this._intervalP1,this.p1),this.velocity.x=0,this.velocity.y=0,this.drag.start(),this._rafStopLoop(),this._rafRenderLoop())):this._numActivePoints>1&&!this.isZooming&&(this._finishDrag(),this.isZooming=!0,this._updateStartPoints(),this.zoomLevels.start(),this._rafStopLoop(),this._rafRenderLoop())))}_finishDrag(){this.isDragging&&(this.isDragging=!1,this._velocityCalculated||this._updateVelocity(!0),this.drag.end(),this.dragAxis=null)}onPointerUp(t){this._numActivePoints&&(this._updatePoints(t,"up"),!this.pswp.dispatch("pointerUp",{originalEvent:t}).defaultPrevented&&(this._numActivePoints===0&&(this._rafStopLoop(),this.isDragging?this._finishDrag():!this.isZooming&&!this.isMultitouch&&this._finishTap(t)),this._numActivePoints<2&&this.isZooming&&(this.isZooming=!1,this.zoomLevels.end(),this._numActivePoints===1&&(this.dragAxis=null,this._updateStartPoints()))))}_rafRenderLoop(){(this.isDragging||this.isZooming)&&(this._updateVelocity(),this.isDragging?x(this.p1,this.prevP1)||this.drag.change():(!x(this.p1,this.prevP1)||!x(this.p2,this.prevP2))&&this.zoomLevels.change(),this._updatePrevPoints(),this.raf=requestAnimationFrame(this._rafRenderLoop.bind(this)))}_updateVelocity(t){const i=Date.now(),e=i-this._intervalTime;e<50&&!t||(this.velocity.x=this._getVelocity("x",e),this.velocity.y=this._getVelocity("y",e),this._intervalTime=i,p(this._intervalP1,this.p1),this._velocityCalculated=!0)}_finishTap(t){const{mainScroll:i}=this.pswp;if(i.isShifted()){i.moveIndexBy(0,!0);return}if(t.type.indexOf("cancel")>0)return;if(t.type==="mouseup"||t.pointerType==="mouse"){this.tapHandler.click(this.startP1,t);return}const e=this.pswp.options.doubleTapAction?at:0;this._tapTimer?(this._clearTapTimer(),A(this._lastStartP1,this.startP1){this.tapHandler.tap(this.startP1,t),this._clearTapTimer()},e))}_clearTapTimer(){this._tapTimer&&(clearTimeout(this._tapTimer),this._tapTimer=null)}_getVelocity(t,i){const e=this.p1[t]-this._intervalP1[t];return Math.abs(e)>1&&i>5?e/i:0}_rafStopLoop(){this.raf&&(cancelAnimationFrame(this.raf),this.raf=null)}_preventPointerEventBehaviour(t,i){this.pswp.applyFilters("preventPointerEvent",!0,t,i)&&t.preventDefault()}_updatePoints(t,i){if(this._pointerEventEnabled){const e=t,s=this._ongoingPointers.findIndex(n=>n.id===e.pointerId);i==="up"&&s>-1?this._ongoingPointers.splice(s,1):i==="down"&&s===-1?this._ongoingPointers.push(this._convertEventPosToPoint(e,{x:0,y:0})):s>-1&&this._convertEventPosToPoint(e,this._ongoingPointers[s]),this._numActivePoints=this._ongoingPointers.length,this._numActivePoints>0&&p(this.p1,this._ongoingPointers[0]),this._numActivePoints>1&&p(this.p2,this._ongoingPointers[1])}else{const e=t;this._numActivePoints=0,e.type.indexOf("touch")>-1?e.touches&&e.touches.length>0&&(this._convertEventPosToPoint(e.touches[0],this.p1),this._numActivePoints++,e.touches.length>1&&(this._convertEventPosToPoint(e.touches[1],this.p2),this._numActivePoints++)):(this._convertEventPosToPoint(t,this.p1),i==="up"?this._numActivePoints=0:this._numActivePoints++)}}_updatePrevPoints(){p(this.prevP1,this.p1),p(this.prevP2,this.p2)}_updateStartPoints(){p(this.startP1,this.p1),p(this.startP2,this.p2),this._updatePrevPoints()}_calculateDragDirection(){if(this.pswp.mainScroll.isShifted())this.dragAxis="x";else{const t=Math.abs(this.p1.x-this.startP1.x)-Math.abs(this.p1.y-this.startP1.y);if(t!==0){const i=t>0?"x":"y";Math.abs(this.p1[i]-this.startP1[i])>=rt&&(this.dragAxis=i)}}}_convertEventPosToPoint(t,i){return i.x=t.pageX-this.pswp.offset.x,i.y=t.pageY-this.pswp.offset.y,"pointerId"in t?i.id=t.pointerId:t.identifier!==void 0&&(i.id=t.identifier),i}_onClick(t){this.pswp.mainScroll.isShifted()&&(t.preventDefault(),t.stopPropagation())}}const ct=.35;class dt{constructor(t){this.pswp=t,this.x=0,this.slideWidth=0,this._currPositionIndex=0,this._prevPositionIndex=0,this._containerShiftIndex=-1,this.itemHolders=[]}resize(t){const{pswp:i}=this,e=Math.round(i.viewportSize.x+i.viewportSize.x*i.options.spacing),s=e!==this.slideWidth;s&&(this.slideWidth=e,this.moveTo(this.getCurrSlideX())),this.itemHolders.forEach((n,o)=>{s&&y(n.el,(o+this._containerShiftIndex)*this.slideWidth),t&&n.slide&&n.slide.resize()})}resetPosition(){this._currPositionIndex=0,this._prevPositionIndex=0,this.slideWidth=0,this._containerShiftIndex=-1}appendHolders(){this.itemHolders=[];for(let t=0;t<3;t++){const i=f("pswp__item","div",this.pswp.container);i.setAttribute("role","group"),i.setAttribute("aria-roledescription","slide"),i.setAttribute("aria-hidden","true"),i.style.display=t===1?"block":"none",this.itemHolders.push({el:i})}}canBeSwiped(){return this.pswp.getNumItems()>1}moveIndexBy(t,i,e){const{pswp:s}=this;let n=s.potentialIndex+t;const o=s.getNumItems();if(s.canLoop()){n=s.getLoopedIndex(n);const h=(t+o)%o;h<=o/2?t=h:t=h-o}else n<0?n=0:n>=o&&(n=o-1),t=n-s.potentialIndex;s.potentialIndex=n,this._currPositionIndex-=t,s.animations.stopMainScroll();const a=this.getCurrSlideX();if(!i)this.moveTo(a),this.updateCurrItem();else{s.animations.startSpring({isMainScroll:!0,start:this.x,end:a,velocity:e||0,naturalFrequency:30,dampingRatio:1,onUpdate:l=>{this.moveTo(l)},onComplete:()=>{this.updateCurrItem(),s.appendHeavy()}});let h=s.potentialIndex-s.currIndex;if(s.canLoop()){const l=(h+o)%o;l<=o/2?h=l:h=l-o}Math.abs(h)>1&&this.updateCurrItem()}return!!t}getCurrSlideX(){return this.slideWidth*this._currPositionIndex}isShifted(){return this.x!==this.getCurrSlideX()}updateCurrItem(){var t;const{pswp:i}=this,e=this._prevPositionIndex-this._currPositionIndex;if(!e)return;this._prevPositionIndex=this._currPositionIndex,i.currIndex=i.potentialIndex;let s=Math.abs(e),n;s>=3&&(this._containerShiftIndex+=e+(e>0?-3:3),s=3,this.itemHolders.forEach(o=>{var a;(a=o.slide)===null||a===void 0||a.destroy(),o.slide=void 0}));for(let o=0;o0?(n=this.itemHolders.shift(),n&&(this.itemHolders[2]=n,this._containerShiftIndex++,y(n.el,(this._containerShiftIndex+2)*this.slideWidth),i.setContent(n,i.currIndex-s+o+2))):(n=this.itemHolders.pop(),n&&(this.itemHolders.unshift(n),this._containerShiftIndex--,y(n.el,this._containerShiftIndex*this.slideWidth),i.setContent(n,i.currIndex+s-o-2)));Math.abs(this._containerShiftIndex)>50&&!this.isShifted()&&(this.resetPosition(),this.resize()),i.animations.stopAllPan(),this.itemHolders.forEach((o,a)=>{o.slide&&o.slide.setIsActive(a===1)}),i.currSlide=(t=this.itemHolders[1])===null||t===void 0?void 0:t.slide,i.contentLoader.updateLazy(e),i.currSlide&&i.currSlide.applyCurrentZoomPan(),i.dispatch("change")}moveTo(t,i){if(!this.pswp.canLoop()&&i){let e=(this.slideWidth*this._currPositionIndex-t)/this.slideWidth;e+=this.pswp.currIndex;const s=Math.round(t-this.x);(e<0&&s>0||e>=this.pswp.getNumItems()-1&&s<0)&&(t=this.x+s*ct)}this.x=t,this.pswp.container&&y(this.pswp.container,t),this.pswp.dispatch("moveMainScroll",{x:t,dragging:i??!1})}}const pt={Escape:27,z:90,ArrowLeft:37,ArrowUp:38,ArrowRight:39,ArrowDown:40,Tab:9},g=(r,t)=>t?r:pt[r];class ut{constructor(t){this.pswp=t,this._wasFocused=!1,t.on("bindEvents",()=>{t.options.trapFocus&&(t.options.initialPointerPos||this._focusRoot(),t.events.add(document,"focusin",this._onFocusIn.bind(this))),t.events.add(document,"keydown",this._onKeyDown.bind(this))});const i=document.activeElement;t.on("destroy",()=>{t.options.returnFocus&&i&&this._wasFocused&&i.focus()})}_focusRoot(){!this._wasFocused&&this.pswp.element&&(this.pswp.element.focus(),this._wasFocused=!0)}_onKeyDown(t){const{pswp:i}=this;if(i.dispatch("keydown",{originalEvent:t}).defaultPrevented||G(t))return;let e,s,n=!1;const o="key"in t;switch(o?t.key:t.keyCode){case g("Escape",o):i.options.escKey&&(e="close");break;case g("z",o):e="toggleZoom";break;case g("ArrowLeft",o):s="x";break;case g("ArrowUp",o):s="y";break;case g("ArrowRight",o):s="x",n=!0;break;case g("ArrowDown",o):n=!0,s="y";break;case g("Tab",o):this._focusRoot();break}if(s){t.preventDefault();const{currSlide:a}=i;i.options.arrowKeys&&s==="x"&&i.getNumItems()>1?e=n?"next":"prev":a&&a.currZoomLevel>a.zoomLevels.fit&&(a.pan[s]+=n?-80:80,a.panTo(a.pan.x,a.pan.y))}e&&(t.preventDefault(),i[e]())}_onFocusIn(t){const{template:i}=this.pswp;i&&document!==t.target&&i!==t.target&&!i.contains(t.target)&&i.focus()}}const mt="cubic-bezier(.4,0,.22,1)";class ft{constructor(t){var i;this.props=t;const{target:e,onComplete:s,transform:n,onFinish:o=()=>{},duration:a=333,easing:h=mt}=t;this.onFinish=o;const l=n?"transform":"opacity",c=(i=t[l])!==null&&i!==void 0?i:"";this._target=e,this._onComplete=s,this._finished=!1,this._onTransitionEnd=this._onTransitionEnd.bind(this),this._helperTimeout=setTimeout(()=>{R(e,l,a,h),this._helperTimeout=setTimeout(()=>{e.addEventListener("transitionend",this._onTransitionEnd,!1),e.addEventListener("transitioncancel",this._onTransitionEnd,!1),this._helperTimeout=setTimeout(()=>{this._finalizeAnimation()},a+500),e.style[l]=c},30)},0)}_onTransitionEnd(t){t.target===this._target&&this._finalizeAnimation()}_finalizeAnimation(){this._finished||(this._finished=!0,this.onFinish(),this._onComplete&&this._onComplete())}destroy(){this._helperTimeout&&clearTimeout(this._helperTimeout),U(this._target),this._target.removeEventListener("transitionend",this._onTransitionEnd,!1),this._target.removeEventListener("transitioncancel",this._onTransitionEnd,!1),this._finished||this._finalizeAnimation()}}const _t=12,vt=.75;class gt{constructor(t,i,e){this.velocity=t*1e3,this._dampingRatio=i||vt,this._naturalFrequency=e||_t,this._dampedFrequency=this._naturalFrequency,this._dampingRatio<1&&(this._dampedFrequency*=Math.sqrt(1-this._dampingRatio*this._dampingRatio))}easeFrame(t,i){let e=0,s;i/=1e3;const n=Math.E**(-this._dampingRatio*this._naturalFrequency*i);if(this._dampingRatio===1)s=this.velocity+this._naturalFrequency*t,e=(t+s*i)*n,this.velocity=e*-this._naturalFrequency+s*n;else if(this._dampingRatio<1){s=1/this._dampedFrequency*(this._dampingRatio*this._naturalFrequency*t+this.velocity);const o=Math.cos(this._dampedFrequency*i),a=Math.sin(this._dampedFrequency*i);e=n*(t*o+s*a),this.velocity=e*-this._naturalFrequency*this._dampingRatio+n*(-this._dampedFrequency*t*a+this._dampedFrequency*s*o)}return e}}class yt{constructor(t){this.props=t,this._raf=0;const{start:i,end:e,velocity:s,onUpdate:n,onComplete:o,onFinish:a=()=>{},dampingRatio:h,naturalFrequency:l}=t;this.onFinish=a;const c=new gt(s,h,l);let d=Date.now(),u=i-e;const m=()=>{this._raf&&(u=c.easeFrame(u,Date.now()-d),Math.abs(u)<1&&Math.abs(c.velocity)<50?(n(e),o&&o(),this.onFinish()):(d=Date.now(),n(u+e),this._raf=requestAnimationFrame(m)))};this._raf=requestAnimationFrame(m)}destroy(){this._raf>=0&&cancelAnimationFrame(this._raf),this._raf=0}}class wt{constructor(){this.activeAnimations=[]}startSpring(t){this._start(t,!0)}startTransition(t){this._start(t)}_start(t,i){const e=i?new yt(t):new ft(t);return this.activeAnimations.push(e),e.onFinish=()=>this.stop(e),e}stop(t){t.destroy();const i=this.activeAnimations.indexOf(t);i>-1&&this.activeAnimations.splice(i,1)}stopAll(){this.activeAnimations.forEach(t=>{t.destroy()}),this.activeAnimations=[]}stopAllPan(){this.activeAnimations=this.activeAnimations.filter(t=>t.props.isPan?(t.destroy(),!1):!0)}stopMainScroll(){this.activeAnimations=this.activeAnimations.filter(t=>t.props.isMainScroll?(t.destroy(),!1):!0)}isPanRunning(){return this.activeAnimations.some(t=>t.props.isPan)}}class Pt{constructor(t){this.pswp=t,t.events.add(t.element,"wheel",this._onWheel.bind(this))}_onWheel(t){t.preventDefault();const{currSlide:i}=this.pswp;let{deltaX:e,deltaY:s}=t;if(i&&!this.pswp.dispatch("wheel",{originalEvent:t}).defaultPrevented)if(t.ctrlKey||this.pswp.options.wheelToZoom){if(i.isZoomable()){let n=-s;t.deltaMode===1?n*=.05:n*=t.deltaMode?1:.002,n=2**n;const o=i.currZoomLevel*n;i.zoomTo(o,{x:t.clientX,y:t.clientY})}}else i.isPannable()&&(t.deltaMode===1&&(e*=18,s*=18),i.panTo(i.pan.x-e,i.pan.y-s))}}function St(r){if(typeof r=="string")return r;if(!r||!r.isCustomSVG)return"";const t=r;let i='",i}class xt{constructor(t,i){var e;const s=i.name||i.className;let n=i.html;if(t.options[s]===!1)return;typeof t.options[s+"SVG"]=="string"&&(n=t.options[s+"SVG"]),t.dispatch("uiElementCreate",{data:i});let o="";i.isButton?(o+="pswp__button ",o+=i.className||`pswp__button--${i.name}`):o+=i.className||`pswp__${i.name}`;let a=i.isButton?i.tagName||"button":i.tagName||"div";a=a.toLowerCase();const h=f(o,a);if(i.isButton){a==="button"&&(h.type="button");let{title:d}=i;const{ariaLabel:u}=i;typeof t.options[s+"Title"]=="string"&&(d=t.options[s+"Title"]),d&&(h.title=d);const m=u||d;m&&h.setAttribute("aria-label",m)}h.innerHTML=St(n),i.onInit&&i.onInit(h,t),i.onClick&&(h.onclick=d=>{typeof i.onClick=="string"?t[i.onClick]():typeof i.onClick=="function"&&i.onClick(d,h,t)});const l=i.appendTo||"bar";let c=t.element;l==="bar"?(t.topBar||(t.topBar=f("pswp__top-bar pswp__hide-on-close","div",t.scrollWrap)),c=t.topBar):(h.classList.add("pswp__hide-on-close"),l==="wrapper"&&(c=t.scrollWrap)),(e=c)===null||e===void 0||e.appendChild(t.applyFilters("uiElement",h,i))}}function k(r,t,i){r.classList.add("pswp__button--arrow"),r.setAttribute("aria-controls","pswp__items"),t.on("change",()=>{t.options.loop||(i?r.disabled=!(t.currIndex0))})}const bt={name:"arrowPrev",className:"pswp__button--arrow--prev",title:"Previous",order:10,isButton:!0,appendTo:"wrapper",html:{isCustomSVG:!0,size:60,inner:'',outlineID:"pswp__icn-arrow"},onClick:"prev",onInit:k},It={name:"arrowNext",className:"pswp__button--arrow--next",title:"Next",order:11,isButton:!0,appendTo:"wrapper",html:{isCustomSVG:!0,size:60,inner:'',outlineID:"pswp__icn-arrow"},onClick:"next",onInit:(r,t)=>{k(r,t,!0)}},At={name:"close",title:"Close",order:20,isButton:!0,html:{isCustomSVG:!0,inner:'',outlineID:"pswp__icn-close"},onClick:"close"},Lt={name:"zoom",title:"Zoom",order:10,isButton:!0,html:{isCustomSVG:!0,inner:'',outlineID:"pswp__icn-zoom"},onClick:"toggleZoom"},Ct={name:"preloader",appendTo:"bar",order:7,html:{isCustomSVG:!0,inner:'',outlineID:"pswp__icn-loading"},onInit:(r,t)=>{let i,e=null;const s=(a,h)=>{r.classList.toggle("pswp__preloader--"+a,h)},n=a=>{i!==a&&(i=a,s("active",a))},o=()=>{var a;if(!((a=t.currSlide)!==null&&a!==void 0&&a.content.isLoading())){n(!1),e&&(clearTimeout(e),e=null);return}e||(e=setTimeout(()=>{var h;n(!!(!((h=t.currSlide)===null||h===void 0)&&h.content.isLoading())),e=null},t.options.preloaderDelay))};t.on("change",o),t.on("loadComplete",a=>{t.currSlide===a.slide&&o()}),t.ui&&(t.ui.updatePreloaderVisibility=o)}},Tt={name:"counter",order:5,onInit:(r,t)=>{t.on("change",()=>{r.innerText=t.currIndex+1+t.options.indexIndicatorSep+t.getNumItems()})}};function D(r,t){r.classList.toggle("pswp--zoomed-in",t)}class zt{constructor(t){this.pswp=t,this.isRegistered=!1,this.uiElementsData=[],this.items=[],this.updatePreloaderVisibility=()=>{},this._lastUpdatedZoomLevel=void 0}init(){const{pswp:t}=this;this.isRegistered=!1,this.uiElementsData=[At,bt,It,Lt,Ct,Tt],t.dispatch("uiRegister"),this.uiElementsData.sort((i,e)=>(i.order||0)-(e.order||0)),this.items=[],this.isRegistered=!0,this.uiElementsData.forEach(i=>{this.registerElement(i)}),t.on("change",()=>{var i;(i=t.element)===null||i===void 0||i.classList.toggle("pswp--one-slide",t.getNumItems()===1)}),t.on("zoomPanUpdate",()=>this._onZoomPanUpdate())}registerElement(t){this.isRegistered?this.items.push(new xt(this.pswp,t)):this.uiElementsData.push(t)}_onZoomPanUpdate(){const{template:t,currSlide:i,options:e}=this.pswp;if(this.pswp.opener.isClosing||!t||!i)return;let{currZoomLevel:s}=i;if(this.pswp.opener.isOpen||(s=i.zoomLevels.initial),s===this._lastUpdatedZoomLevel)return;this._lastUpdatedZoomLevel=s;const n=i.zoomLevels.initial-i.zoomLevels.secondary;if(Math.abs(n)<.01||!i.isZoomable()){D(t,!1),t.classList.remove("pswp--zoom-allowed");return}t.classList.add("pswp--zoom-allowed");const o=s===i.zoomLevels.initial?i.zoomLevels.secondary:i.zoomLevels.initial;D(t,o<=s),(e.imageClickAction==="zoom"||e.imageClickAction==="zoom-or-close")&&t.classList.add("pswp--click-to-zoom")}}function Et(r){const t=r.getBoundingClientRect();return{x:t.left,y:t.top,w:t.width}}function Ot(r,t,i){const e=r.getBoundingClientRect(),s=e.width/t,n=e.height/i,o=s>n?s:n,a=(e.width-t*o)/2,h=(e.height-i*o)/2,l={x:e.left+a,y:e.top+h,w:t*o};return l.innerRect={w:e.width,h:e.height,x:a,y:h},l}function Zt(r,t,i){const e=i.dispatch("thumbBounds",{index:r,itemData:t,instance:i});if(e.thumbBounds)return e.thumbBounds;const{element:s}=t;let n,o;if(s&&i.options.thumbSelector!==!1){const a=i.options.thumbSelector||"img";o=s.matches(a)?s:s.querySelector(a)}return o=i.applyFilters("thumbEl",o,t,r),o&&(t.thumbCropped?n=Ot(o,t.width||t.w||0,t.height||t.h||0):n=Et(o)),i.applyFilters("thumbBounds",n,t,r)}class Dt{constructor(t,i){this.type=t,this.defaultPrevented=!1,i&&Object.assign(this,i)}preventDefault(){this.defaultPrevented=!0}}class Mt{constructor(){this._listeners={},this._filters={},this.pswp=void 0,this.options=void 0}addFilter(t,i,e=100){var s,n,o;this._filters[t]||(this._filters[t]=[]),(s=this._filters[t])===null||s===void 0||s.push({fn:i,priority:e}),(n=this._filters[t])===null||n===void 0||n.sort((a,h)=>a.priority-h.priority),(o=this.pswp)===null||o===void 0||o.addFilter(t,i,e)}removeFilter(t,i){this._filters[t]&&(this._filters[t]=this._filters[t].filter(e=>e.fn!==i)),this.pswp&&this.pswp.removeFilter(t,i)}applyFilters(t,...i){var e;return(e=this._filters[t])===null||e===void 0||e.forEach(s=>{i[0]=s.fn.apply(this,i)}),i[0]}on(t,i){var e,s;this._listeners[t]||(this._listeners[t]=[]),(e=this._listeners[t])===null||e===void 0||e.push(i),(s=this.pswp)===null||s===void 0||s.on(t,i)}off(t,i){var e;this._listeners[t]&&(this._listeners[t]=this._listeners[t].filter(s=>i!==s)),(e=this.pswp)===null||e===void 0||e.off(t,i)}dispatch(t,i){var e;if(this.pswp)return this.pswp.dispatch(t,i);const s=new Dt(t,i);return(e=this._listeners[t])===null||e===void 0||e.forEach(n=>{n.call(this,s)}),s}}class Rt{constructor(t,i){if(this.element=f("pswp__img pswp__img--placeholder",t?"img":"div",i),t){const e=this.element;e.decoding="async",e.alt="",e.src=t,e.setAttribute("role","presentation")}this.element.setAttribute("aria-hidden","true")}setDisplayedSize(t,i){this.element&&(this.element.tagName==="IMG"?(L(this.element,250,"auto"),this.element.style.transformOrigin="0 0",this.element.style.transform=b(0,0,t/250)):L(this.element,t,i))}destroy(){var t;(t=this.element)!==null&&t!==void 0&&t.parentNode&&this.element.remove(),this.element=null}}class Ft{constructor(t,i,e){this.instance=i,this.data=t,this.index=e,this.element=void 0,this.placeholder=void 0,this.slide=void 0,this.displayedImageWidth=0,this.displayedImageHeight=0,this.width=Number(this.data.w)||Number(this.data.width)||0,this.height=Number(this.data.h)||Number(this.data.height)||0,this.isAttached=!1,this.hasSlide=!1,this.isDecoding=!1,this.state=_.IDLE,this.data.type?this.type=this.data.type:this.data.src?this.type="image":this.type="html",this.instance.dispatch("contentInit",{content:this})}removePlaceholder(){this.placeholder&&!this.keepPlaceholder()&&setTimeout(()=>{this.placeholder&&(this.placeholder.destroy(),this.placeholder=void 0)},1e3)}load(t,i){if(this.slide&&this.usePlaceholder())if(this.placeholder){const e=this.placeholder.element;e&&!e.parentElement&&this.slide.container.prepend(e)}else{const e=this.instance.applyFilters("placeholderSrc",this.data.msrc&&this.slide.isFirstSlide?this.data.msrc:!1,this);this.placeholder=new Rt(e,this.slide.container)}this.element&&!i||this.instance.dispatch("contentLoad",{content:this,isLazy:t}).defaultPrevented||(this.isImageContent()?(this.element=f("pswp__img","img"),this.displayedImageWidth&&this.loadImage(t)):(this.element=f("pswp__content","div"),this.element.innerHTML=this.data.html||""),i&&this.slide&&this.slide.updateContentSize(!0))}loadImage(t){var i,e;if(!this.isImageContent()||!this.element||this.instance.dispatch("contentLoadImage",{content:this,isLazy:t}).defaultPrevented)return;const s=this.element;this.updateSrcsetSizes(),this.data.srcset&&(s.srcset=this.data.srcset),s.src=(i=this.data.src)!==null&&i!==void 0?i:"",s.alt=(e=this.data.alt)!==null&&e!==void 0?e:"",this.state=_.LOADING,s.complete?this.onLoaded():(s.onload=()=>{this.onLoaded()},s.onerror=()=>{this.onError()})}setSlide(t){this.slide=t,this.hasSlide=!0,this.instance=t.pswp}onLoaded(){this.state=_.LOADED,this.slide&&this.element&&(this.instance.dispatch("loadComplete",{slide:this.slide,content:this}),this.slide.isActive&&this.slide.heavyAppended&&!this.element.parentNode&&(this.append(),this.slide.updateContentSize(!0)),(this.state===_.LOADED||this.state===_.ERROR)&&this.removePlaceholder())}onError(){this.state=_.ERROR,this.slide&&(this.displayError(),this.instance.dispatch("loadComplete",{slide:this.slide,isError:!0,content:this}),this.instance.dispatch("loadError",{slide:this.slide,content:this}))}isLoading(){return this.instance.applyFilters("isContentLoading",this.state===_.LOADING,this)}isError(){return this.state===_.ERROR}isImageContent(){return this.type==="image"}setDisplayedSize(t,i){if(this.element&&(this.placeholder&&this.placeholder.setDisplayedSize(t,i),!this.instance.dispatch("contentResize",{content:this,width:t,height:i}).defaultPrevented&&(L(this.element,t,i),this.isImageContent()&&!this.isError()))){const e=!this.displayedImageWidth&&t;this.displayedImageWidth=t,this.displayedImageHeight=i,e?this.loadImage(!1):this.updateSrcsetSizes(),this.slide&&this.instance.dispatch("imageSizeChange",{slide:this.slide,width:t,height:i,content:this})}}isZoomable(){return this.instance.applyFilters("isContentZoomable",this.isImageContent()&&this.state!==_.ERROR,this)}updateSrcsetSizes(){if(!this.isImageContent()||!this.element||!this.data.srcset)return;const t=this.element,i=this.instance.applyFilters("srcsetSizesWidth",this.displayedImageWidth,this);(!t.dataset.largestUsedSize||i>parseInt(t.dataset.largestUsedSize,10))&&(t.sizes=i+"px",t.dataset.largestUsedSize=String(i))}usePlaceholder(){return this.instance.applyFilters("useContentPlaceholder",this.isImageContent(),this)}lazyLoad(){this.instance.dispatch("contentLazyLoad",{content:this}).defaultPrevented||this.load(!0)}keepPlaceholder(){return this.instance.applyFilters("isKeepingPlaceholder",this.isLoading(),this)}destroy(){this.hasSlide=!1,this.slide=void 0,!this.instance.dispatch("contentDestroy",{content:this}).defaultPrevented&&(this.remove(),this.placeholder&&(this.placeholder.destroy(),this.placeholder=void 0),this.isImageContent()&&this.element&&(this.element.onload=null,this.element.onerror=null,this.element=void 0))}displayError(){if(this.slide){var t,i;let e=f("pswp__error-msg","div");e.innerText=(t=(i=this.instance.options)===null||i===void 0?void 0:i.errorMsg)!==null&&t!==void 0?t:"",e=this.instance.applyFilters("contentErrorElement",e,this),this.element=f("pswp__content pswp__error-msg-container","div"),this.element.appendChild(e),this.slide.container.innerText="",this.slide.container.appendChild(this.element),this.slide.updateContentSize(!0),this.removePlaceholder()}}append(){if(this.isAttached||!this.element)return;if(this.isAttached=!0,this.state===_.ERROR){this.displayError();return}if(this.instance.dispatch("contentAppend",{content:this}).defaultPrevented)return;const t="decode"in this.element;this.isImageContent()?t&&this.slide&&(!this.slide.isActive||C())?(this.isDecoding=!0,this.element.decode().catch(()=>{}).finally(()=>{this.isDecoding=!1,this.appendImage()})):this.appendImage():this.slide&&!this.element.parentNode&&this.slide.container.appendChild(this.element)}activate(){this.instance.dispatch("contentActivate",{content:this}).defaultPrevented||!this.slide||(this.isImageContent()&&this.isDecoding&&!C()?this.appendImage():this.isError()&&this.load(!1,!0),this.slide.holderElement&&this.slide.holderElement.setAttribute("aria-hidden","false"))}deactivate(){this.instance.dispatch("contentDeactivate",{content:this}),this.slide&&this.slide.holderElement&&this.slide.holderElement.setAttribute("aria-hidden","true")}remove(){this.isAttached=!1,!this.instance.dispatch("contentRemove",{content:this}).defaultPrevented&&(this.element&&this.element.parentNode&&this.element.remove(),this.placeholder&&this.placeholder.element&&this.placeholder.element.remove())}appendImage(){this.isAttached&&(this.instance.dispatch("contentAppendImage",{content:this}).defaultPrevented||(this.slide&&this.element&&!this.element.parentNode&&this.slide.container.appendChild(this.element),(this.state===_.LOADED||this.state===_.ERROR)&&this.removePlaceholder()))}}const Bt=5;function W(r,t,i){const e=t.createContentFromData(r,i);let s;const{options:n}=t;if(n){s=new H(n,r,-1);let o;t.pswp?o=t.pswp.viewportSize:o=B(n,t);const a=N(n,o,r,i);s.update(e.width,e.height,a)}return e.lazyLoad(),s&&e.setDisplayedSize(Math.ceil(e.width*s.initial),Math.ceil(e.height*s.initial)),e}function Nt(r,t){const i=t.getItemData(r);if(!t.dispatch("lazyLoadSlide",{index:r,itemData:i}).defaultPrevented)return W(i,t,r)}class Ht{constructor(t){this.pswp=t,this.limit=Math.max(t.options.preload[0]+t.options.preload[1]+1,Bt),this._cachedItems=[]}updateLazy(t){const{pswp:i}=this;if(i.dispatch("lazyLoad").defaultPrevented)return;const{preload:e}=i.options,s=t===void 0?!0:t>=0;let n;for(n=0;n<=e[1];n++)this.loadSlideByIndex(i.currIndex+(s?n:-n));for(n=1;n<=e[0];n++)this.loadSlideByIndex(i.currIndex+(s?-n:n))}loadSlideByIndex(t){const i=this.pswp.getLoopedIndex(t);let e=this.getContentByIndex(i);e||(e=Nt(i,this.pswp),e&&this.addToCache(e))}getContentBySlide(t){let i=this.getContentByIndex(t.index);return i||(i=this.pswp.createContentFromData(t.data,t.index),this.addToCache(i)),i.setSlide(t),i}addToCache(t){if(this.removeByIndex(t.index),this._cachedItems.push(t),this._cachedItems.length>this.limit){const i=this._cachedItems.findIndex(e=>!e.isAttached&&!e.hasSlide);i!==-1&&this._cachedItems.splice(i,1)[0].destroy()}}removeByIndex(t){const i=this._cachedItems.findIndex(e=>e.index===t);i!==-1&&this._cachedItems.splice(i,1)}getContentByIndex(t){return this._cachedItems.find(i=>i.index===t)}destroy(){this._cachedItems.forEach(t=>t.destroy()),this._cachedItems=[]}}class kt extends Mt{getNumItems(){var t;let i=0;const e=(t=this.options)===null||t===void 0?void 0:t.dataSource;e&&"length"in e?i=e.length:e&&"gallery"in e&&(e.items||(e.items=this._getGalleryDOMElements(e.gallery)),e.items&&(i=e.items.length));const s=this.dispatch("numItems",{dataSource:e,numItems:i});return this.applyFilters("numItems",s.numItems,e)}createContentFromData(t,i){return new Ft(t,this,i)}getItemData(t){var i;const e=(i=this.options)===null||i===void 0?void 0:i.dataSource;let s={};Array.isArray(e)?s=e[t]:e&&"gallery"in e&&(e.items||(e.items=this._getGalleryDOMElements(e.gallery)),s=e.items[t]);let n=s;n instanceof Element&&(n=this._domElementToItemData(n));const o=this.dispatch("itemData",{itemData:n||{},index:t});return this.applyFilters("itemData",o.itemData,t)}_getGalleryDOMElements(t){var i,e;return(i=this.options)!==null&&i!==void 0&&i.children||(e=this.options)!==null&&e!==void 0&&e.childSelector?K(this.options.children,this.options.childSelector,t)||[]:[t]}_domElementToItemData(t){const i={element:t},e=t.tagName==="A"?t:t.querySelector("a");if(e){i.src=e.dataset.pswpSrc||e.href,e.dataset.pswpSrcset&&(i.srcset=e.dataset.pswpSrcset),i.width=e.dataset.pswpWidth?parseInt(e.dataset.pswpWidth,10):0,i.height=e.dataset.pswpHeight?parseInt(e.dataset.pswpHeight,10):0,i.w=i.width,i.h=i.height,e.dataset.pswpType&&(i.type=e.dataset.pswpType);const n=t.querySelector("img");if(n){var s;i.msrc=n.currentSrc||n.src,i.alt=(s=n.getAttribute("alt"))!==null&&s!==void 0?s:""}(e.dataset.pswpCropped||e.dataset.cropped)&&(i.thumbCropped=!0)}return this.applyFilters("domItemData",i,t,e)}lazyLoadData(t,i){return W(t,this,i)}}const P=.003;class Wt{constructor(t){this.pswp=t,this.isClosed=!0,this.isOpen=!1,this.isClosing=!1,this.isOpening=!1,this._duration=void 0,this._useAnimation=!1,this._croppedZoom=!1,this._animateRootOpacity=!1,this._animateBgOpacity=!1,this._placeholder=void 0,this._opacityElement=void 0,this._cropContainer1=void 0,this._cropContainer2=void 0,this._thumbBounds=void 0,this._prepareOpen=this._prepareOpen.bind(this),t.on("firstZoomPan",this._prepareOpen)}open(){this._prepareOpen(),this._start()}close(){if(this.isClosed||this.isClosing||this.isOpening)return;const t=this.pswp.currSlide;this.isOpen=!1,this.isOpening=!1,this.isClosing=!0,this._duration=this.pswp.options.hideAnimationDuration,t&&t.currZoomLevel*t.width>=this.pswp.options.maxWidthToAnimate&&(this._duration=0),this._applyStartProps(),setTimeout(()=>{this._start()},this._croppedZoom?30:0)}_prepareOpen(){if(this.pswp.off("firstZoomPan",this._prepareOpen),!this.isOpening){const t=this.pswp.currSlide;this.isOpening=!0,this.isClosing=!1,this._duration=this.pswp.options.showAnimationDuration,t&&t.zoomLevels.initial*t.width>=this.pswp.options.maxWidthToAnimate&&(this._duration=0),this._applyStartProps()}}_applyStartProps(){const{pswp:t}=this,i=this.pswp.currSlide,{options:e}=t;if(e.showHideAnimationType==="fade"?(e.showHideOpacity=!0,this._thumbBounds=void 0):e.showHideAnimationType==="none"?(e.showHideOpacity=!1,this._duration=0,this._thumbBounds=void 0):this.isOpening&&t._initialThumbBounds?this._thumbBounds=t._initialThumbBounds:this._thumbBounds=this.pswp.getThumbBounds(),this._placeholder=i==null?void 0:i.getPlaceholderElement(),t.animations.stopAll(),this._useAnimation=!!(this._duration&&this._duration>50),this._animateZoom=!!this._thumbBounds&&(i==null?void 0:i.content.usePlaceholder())&&(!this.isClosing||!t.mainScroll.isShifted()),!this._animateZoom)this._animateRootOpacity=!0,this.isOpening&&i&&(i.zoomAndPanToInitial(),i.applyCurrentZoomPan());else{var s;this._animateRootOpacity=(s=e.showHideOpacity)!==null&&s!==void 0?s:!1}if(this._animateBgOpacity=!this._animateRootOpacity&&this.pswp.options.bgOpacity>P,this._opacityElement=this._animateRootOpacity?t.element:t.bg,!this._useAnimation){this._duration=0,this._animateZoom=!1,this._animateBgOpacity=!1,this._animateRootOpacity=!0,this.isOpening&&(t.element&&(t.element.style.opacity=String(P)),t.applyBgOpacity(1));return}if(this._animateZoom&&this._thumbBounds&&this._thumbBounds.innerRect){var n;this._croppedZoom=!0,this._cropContainer1=this.pswp.container,this._cropContainer2=(n=this.pswp.currSlide)===null||n===void 0?void 0:n.holderElement,t.container&&(t.container.style.overflow="hidden",t.container.style.width=t.viewportSize.x+"px")}else this._croppedZoom=!1;this.isOpening?(this._animateRootOpacity?(t.element&&(t.element.style.opacity=String(P)),t.applyBgOpacity(1)):(this._animateBgOpacity&&t.bg&&(t.bg.style.opacity=String(P)),t.element&&(t.element.style.opacity="1")),this._animateZoom&&(this._setClosedStateZoomPan(),this._placeholder&&(this._placeholder.style.willChange="transform",this._placeholder.style.opacity=String(P)))):this.isClosing&&(t.mainScroll.itemHolders[0]&&(t.mainScroll.itemHolders[0].el.style.display="none"),t.mainScroll.itemHolders[2]&&(t.mainScroll.itemHolders[2].el.style.display="none"),this._croppedZoom&&t.mainScroll.x!==0&&(t.mainScroll.resetPosition(),t.mainScroll.resize()))}_start(){this.isOpening&&this._useAnimation&&this._placeholder&&this._placeholder.tagName==="IMG"?new Promise(t=>{let i=!1,e=!0;q(this._placeholder).finally(()=>{i=!0,e||t(!0)}),setTimeout(()=>{e=!1,i&&t(!0)},50),setTimeout(t,250)}).finally(()=>this._initiate()):this._initiate()}_initiate(){var t,i;(t=this.pswp.element)===null||t===void 0||t.style.setProperty("--pswp-transition-duration",this._duration+"ms"),this.pswp.dispatch(this.isOpening?"openingAnimationStart":"closingAnimationStart"),this.pswp.dispatch("initialZoom"+(this.isOpening?"In":"Out")),(i=this.pswp.element)===null||i===void 0||i.classList.toggle("pswp--ui-visible",this.isOpening),this.isOpening?(this._placeholder&&(this._placeholder.style.opacity="1"),this._animateToOpenState()):this.isClosing&&this._animateToClosedState(),this._useAnimation||this._onAnimationComplete()}_onAnimationComplete(){const{pswp:t}=this;if(this.isOpen=this.isOpening,this.isClosed=this.isClosing,this.isOpening=!1,this.isClosing=!1,t.dispatch(this.isOpen?"openingAnimationEnd":"closingAnimationEnd"),t.dispatch("initialZoom"+(this.isOpen?"InEnd":"OutEnd")),this.isClosed)t.destroy();else if(this.isOpen){var i;this._animateZoom&&t.container&&(t.container.style.overflow="visible",t.container.style.width="100%"),(i=t.currSlide)===null||i===void 0||i.applyCurrentZoomPan()}}_animateToOpenState(){const{pswp:t}=this;this._animateZoom&&(this._croppedZoom&&this._cropContainer1&&this._cropContainer2&&(this._animateTo(this._cropContainer1,"transform","translate3d(0,0,0)"),this._animateTo(this._cropContainer2,"transform","none")),t.currSlide&&(t.currSlide.zoomAndPanToInitial(),this._animateTo(t.currSlide.container,"transform",t.currSlide.getCurrentTransform()))),this._animateBgOpacity&&t.bg&&this._animateTo(t.bg,"opacity",String(t.options.bgOpacity)),this._animateRootOpacity&&t.element&&this._animateTo(t.element,"opacity","1")}_animateToClosedState(){const{pswp:t}=this;this._animateZoom&&this._setClosedStateZoomPan(!0),this._animateBgOpacity&&t.bgOpacity>.01&&t.bg&&this._animateTo(t.bg,"opacity","0"),this._animateRootOpacity&&t.element&&this._animateTo(t.element,"opacity","0")}_setClosedStateZoomPan(t){if(!this._thumbBounds)return;const{pswp:i}=this,{innerRect:e}=this._thumbBounds,{currSlide:s,viewportSize:n}=i;if(this._croppedZoom&&e&&this._cropContainer1&&this._cropContainer2){const o=-n.x+(this._thumbBounds.x-e.x)+e.w,a=-n.y+(this._thumbBounds.y-e.y)+e.h,h=n.x-e.w,l=n.y-e.h;t?(this._animateTo(this._cropContainer1,"transform",b(o,a)),this._animateTo(this._cropContainer2,"transform",b(h,l))):(y(this._cropContainer1,o,a),y(this._cropContainer2,h,l))}s&&(p(s.pan,e||this._thumbBounds),s.currZoomLevel=this._thumbBounds.w/s.width,t?this._animateTo(s.container,"transform",s.getCurrentTransform()):s.applyCurrentZoomPan())}_animateTo(t,i,e){if(!this._duration){t.style[i]=e;return}const{animations:s}=this.pswp,n={duration:this._duration,easing:this.pswp.options.easing,onComplete:()=>{s.activeAnimations.length||this._onAnimationComplete()},target:t};n[i]=e,s.startTransition(n)}}const Vt={allowPanToNext:!0,spacing:.1,loop:!0,pinchToClose:!0,closeOnVerticalDrag:!0,hideAnimationDuration:333,showAnimationDuration:333,zoomAnimationDuration:333,escKey:!0,arrowKeys:!0,trapFocus:!0,returnFocus:!0,maxWidthToAnimate:4e3,clickToCloseNonZoomable:!0,imageClickAction:"zoom-or-close",bgClickAction:"close",tapAction:"toggle-controls",doubleTapAction:"zoom",indexIndicatorSep:" / ",preloaderDelay:2e3,bgOpacity:.8,index:0,errorMsg:"The image cannot be loaded",preload:[1,2],easing:"cubic-bezier(.4,0,.22,1)"};class $t extends kt{constructor(t){super(),this.options=this._prepareOptions(t||{}),this.offset={x:0,y:0},this._prevViewportSize={x:0,y:0},this.viewportSize={x:0,y:0},this.bgOpacity=1,this.currIndex=0,this.potentialIndex=0,this.isOpen=!1,this.isDestroying=!1,this.hasMouse=!1,this._initialItemData={},this._initialThumbBounds=void 0,this.topBar=void 0,this.element=void 0,this.template=void 0,this.container=void 0,this.scrollWrap=void 0,this.currSlide=void 0,this.events=new X,this.animations=new wt,this.mainScroll=new dt(this),this.gestures=new lt(this),this.opener=new Wt(this),this.keyboard=new ut(this),this.contentLoader=new Ht(this)}init(){if(this.isOpen||this.isDestroying)return!1;this.isOpen=!0,this.dispatch("init"),this.dispatch("beforeOpen"),this._createMainStructure();let t="pswp--open";return this.gestures.supportsTouch&&(t+=" pswp--touch"),this.options.mainClass&&(t+=" "+this.options.mainClass),this.element&&(this.element.className+=" "+t),this.currIndex=this.options.index||0,this.potentialIndex=this.currIndex,this.dispatch("firstUpdate"),this.scrollWheel=new Pt(this),(Number.isNaN(this.currIndex)||this.currIndex<0||this.currIndex>=this.getNumItems())&&(this.currIndex=0),this.gestures.supportsTouch||this.mouseDetected(),this.updateSize(),this.offset.y=window.pageYOffset,this._initialItemData=this.getItemData(this.currIndex),this.dispatch("gettingData",{index:this.currIndex,data:this._initialItemData,slide:void 0}),this._initialThumbBounds=this.getThumbBounds(),this.dispatch("initialLayout"),this.on("openingAnimationEnd",()=>{const{itemHolders:i}=this.mainScroll;i[0]&&(i[0].el.style.display="block",this.setContent(i[0],this.currIndex-1)),i[2]&&(i[2].el.style.display="block",this.setContent(i[2],this.currIndex+1)),this.appendHeavy(),this.contentLoader.updateLazy(),this.events.add(window,"resize",this._handlePageResize.bind(this)),this.events.add(window,"scroll",this._updatePageScrollOffset.bind(this)),this.dispatch("bindEvents")}),this.mainScroll.itemHolders[1]&&this.setContent(this.mainScroll.itemHolders[1],this.currIndex),this.dispatch("change"),this.opener.open(),this.dispatch("afterInit"),!0}getLoopedIndex(t){const i=this.getNumItems();return this.options.loop&&(t>i-1&&(t-=i),t<0&&(t+=i)),I(t,0,i-1)}appendHeavy(){this.mainScroll.itemHolders.forEach(t=>{var i;(i=t.slide)===null||i===void 0||i.appendHeavy()})}goTo(t){this.mainScroll.moveIndexBy(this.getLoopedIndex(t)-this.potentialIndex)}next(){this.goTo(this.potentialIndex+1)}prev(){this.goTo(this.potentialIndex-1)}zoomTo(...t){var i;(i=this.currSlide)===null||i===void 0||i.zoomTo(...t)}toggleZoom(){var t;(t=this.currSlide)===null||t===void 0||t.toggleZoom()}close(){!this.opener.isOpen||this.isDestroying||(this.isDestroying=!0,this.dispatch("close"),this.events.removeAll(),this.opener.close())}destroy(){var t;if(!this.isDestroying){this.options.showHideAnimationType="none",this.close();return}this.dispatch("destroy"),this._listeners={},this.scrollWrap&&(this.scrollWrap.ontouchmove=null,this.scrollWrap.ontouchend=null),(t=this.element)===null||t===void 0||t.remove(),this.mainScroll.itemHolders.forEach(i=>{var e;(e=i.slide)===null||e===void 0||e.destroy()}),this.contentLoader.destroy(),this.events.removeAll()}refreshSlideContent(t){this.contentLoader.removeByIndex(t),this.mainScroll.itemHolders.forEach((i,e)=>{var s,n;let o=((s=(n=this.currSlide)===null||n===void 0?void 0:n.index)!==null&&s!==void 0?s:0)-1+e;if(this.canLoop()&&(o=this.getLoopedIndex(o)),o===t&&(this.setContent(i,t,!0),e===1)){var a;this.currSlide=i.slide,(a=i.slide)===null||a===void 0||a.setIsActive(!0)}}),this.dispatch("change")}setContent(t,i,e){if(this.canLoop()&&(i=this.getLoopedIndex(i)),t.slide){if(t.slide.index===i&&!e)return;t.slide.destroy(),t.slide=void 0}if(!this.canLoop()&&(i<0||i>=this.getNumItems()))return;const s=this.getItemData(i);t.slide=new j(s,i,this),i===this.currIndex&&(this.currSlide=t.slide),t.slide.append(t.el)}getViewportCenterPoint(){return{x:this.viewportSize.x/2,y:this.viewportSize.y/2}}updateSize(t){if(this.isDestroying)return;const i=B(this.options,this);!t&&x(i,this._prevViewportSize)||(p(this._prevViewportSize,i),this.dispatch("beforeResize"),p(this.viewportSize,this._prevViewportSize),this._updatePageScrollOffset(),this.dispatch("viewportSize"),this.mainScroll.resize(this.opener.isOpen),!this.hasMouse&&window.matchMedia("(any-hover: hover)").matches&&this.mouseDetected(),this.dispatch("resize"))}applyBgOpacity(t){this.bgOpacity=Math.max(t,0),this.bg&&(this.bg.style.opacity=String(this.bgOpacity*this.options.bgOpacity))}mouseDetected(){if(!this.hasMouse){var t;this.hasMouse=!0,(t=this.element)===null||t===void 0||t.classList.add("pswp--has_mouse")}}_handlePageResize(){this.updateSize(),/iPhone|iPad|iPod/i.test(window.navigator.userAgent)&&setTimeout(()=>{this.updateSize()},500)}_updatePageScrollOffset(){this.setScrollOffset(0,window.pageYOffset)}setScrollOffset(t,i){this.offset.x=t,this.offset.y=i,this.dispatch("updateScrollOffset")}_createMainStructure(){this.element=f("pswp","div"),this.element.setAttribute("tabindex","-1"),this.element.setAttribute("role","dialog"),this.template=this.element,this.bg=f("pswp__bg","div",this.element),this.scrollWrap=f("pswp__scroll-wrap","section",this.element),this.container=f("pswp__container","div",this.scrollWrap),this.scrollWrap.setAttribute("aria-roledescription","carousel"),this.container.setAttribute("aria-live","off"),this.container.setAttribute("id","pswp__items"),this.mainScroll.appendHolders(),this.ui=new zt(this),this.ui.init(),(this.options.appendToEl||document.body).appendChild(this.element)}getThumbBounds(){return Zt(this.currIndex,this.currSlide?this.currSlide.data:this._initialItemData,this)}canLoop(){return this.options.loop&&this.getNumItems()>2}_prepareOptions(t){return window.matchMedia("(prefers-reduced-motion), (update: slow)").matches&&(t.showHideAnimationType="none",t.zoomAnimationDuration=0),{...Vt,...t}}}export{$t as default}; diff --git "a/assets/redis\347\274\223\345\255\230.html-H1xCPktw.js" "b/assets/redis\347\274\223\345\255\230.html-H1xCPktw.js" new file mode 100644 index 00000000..e0987269 --- /dev/null +++ "b/assets/redis\347\274\223\345\255\230.html-H1xCPktw.js" @@ -0,0 +1,87 @@ +import{_ as s,c as a,d as n,o as t}from"./app-ftEjETWs.js";const e={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[n(`

Redis缓存

一、概述

1.1 缓存介绍

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
缓存常用语:
数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
可查看Redis实战(一) 使用缓存合理性

1.2 本站缓存架构

从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。

步骤:
(1)用户发送一个请求到nginx,nginx对请求进行分发。
(2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。
(3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。
(4)查询数据库的时候,mysql作了主主备份。

二、Mybatis缓存

2.1 mybatis一级缓存

Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。

我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap<k,v>来进行缓存。
(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。
(3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

![](http://image.wenzhihuai.com/images/20180120022427.png)

2.2 mybatis二级缓存

Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,

2.2.1 MyBatis二级缓存的划分

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:

a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);
+b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);

2.2.2 二级缓存的开启

在mybatis的配置文件中添加:

<settings>
+   <!--开启二级缓存-->
+    <setting name="cacheEnabled" value="true"/>
+</settings>

然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):

    <cache eviction="LRU"
+           type="org.apache.ibatis.cache.impl.PerpetualCache"
+           flushInterval="120000"
+           size="1024"
+           readOnly="true"/>

2.2.3 使用第三方支持的二级缓存的实现

MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache  type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
  1. MyBatis自身提供的缓存实现;
  2. 用户自定义的Cache接口实现;
  3. 跟第三方内存缓存库的集成;
    具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置

MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):

2.3 Mybatis在分布式环境下脏读问题

(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。

下面将介绍使用Redis集中式缓存在个人网站的应用。

三、Redis缓存

Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。

3.1 Spring Cache

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
下面是Spring Cache常用的注解:

(1)@Cacheable
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

属性介绍例子
value缓存的名称,必选@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}
key缓存的key,可选,需要按照SpEL表达式填写@Cacheable(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存@Cacheable(value=”testcache”,key=”#userName”)

(2)@CachePut
@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用

属性介绍例子
value缓存的名称,必选@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}
key缓存的key,可选,需要按照SpEL表达式填写@Cacheable(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存@Cacheable(value=”testcache”,key=”#userName”)

(3)@CacheEvict
@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空

属性介绍例子
value缓存的名称,必选@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”}
key缓存的key,可选,需要按照SpEL表达式填写@Cacheable(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,只有为 true 才进行缓存@Cacheable(value=”testcache”,key=”#userName”)
allEntries是否清空所有缓存内容,默认为false@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation是否在方法执行前就清空,缺省为 false@CachEvict(value=”testcache”,beforeInvocation=true)

但是有个问题:
Spring官方认为:缓存过期时间由各个产商决定,所以并不提供缓存过期时间的注解。所以,如果想实现各个元素过期时间不同,就需要自己重写一下Spring cache。

3.2 引入包

一般是Spring常用的包+Spring data redis的包,记得注意去掉所有冲突的包,之前才过坑,Spring-data-MongoDB已经有SpEL的库了,和自己新引进去的冲突,搞得我以为自己是配置配错了,真是个坑,注意,开发过程中一定要去除掉所有冲突的包!!!

3.3 ApplicationContext.xml

需要启用缓存的注解开关,并配置好Redis。序列化方式也要带上,否则会碰到幽灵bug。

    <!-- 启用缓存注解开关,此处可自定义keyGenerator -->
+    <cache:annotation-driven/>
+    <bean id="jedisConnectionFactory"
+          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+        <property name="hostName" value="\${host}"/>
+        <property name="port" value="\${port}"/>
+        <property name="password" value="\${password}"/>
+        <property name="database" value="\${redis.default.db}"/>
+        <property name="timeout" value="\${timeout}"/>
+        <property name="poolConfig" ref="jedisPoolConfig"/>
+        <property name="usePool" value="true"/>
+    </bean>
+    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+        <property name="connectionFactory" ref="jedisConnectionFactory"/>
+        <!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
+        <property name="keySerializer">
+            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+        </property>
+        <property name="hashKeySerializer">
+            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
+        </property>
+        <property name="valueSerializer">
+            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+        </property>
+        <property name="hashValueSerializer">
+            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
+        </property>
+    </bean>
+    <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
+        <constructor-arg name="redisOperations" ref="redisTemplate" />
+        <!--统一过期时间-->
+        <property name="defaultExpiration" value="\${redis.defaultExpiration}"/>
+    </bean>

3.5 自定义KeyGenerator

在分布式系统中,很容易存在不同类相同名字的方法,如A.getAll(),B.getAll(),默认的key(getAll)都是一样的,会很容易产生问题,所以,需要自定义key来实现分布式环境下的不同。

@Component("customKeyGenerator")
+public class CustomKeyGenerator implements KeyGenerator {
+    @Override
+    public Object generate(Object o, Method method, Object... objects) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(o.getClass().getName());
+        sb.append(".");
+        sb.append(method.getName());
+        for (Object obj : objects) {
+            sb.append(obj.toString());
+        }
+        return sb.toString();
+    }
+}

之后,存储的key就变为:com.myblog.service.impl.BlogServiceImpl.getBanner。

3.4 添加注解

在所需要的方法上添加注解,比如,首页中的那几张幻灯片,每次进入首页都需要查询数据库,这里,我们直接放入缓存里,减少数据库的压力,还有就是那些热门文章,访问量比较大的,也放进数据库里。

    @Override
+    @Cacheable(value = "getBanner", keyGenerator = "customKeyGenerator")
+    public List<Blog> getBanner() {
+        return blogMapper.getBanner();
+    }
+    @Override
+    @Cacheable(value = "getBlogDetail", key = "'blogid'.concat(#blogid)")
+    public Blog getBlogDetail(Integer blogid) {
+        Blog blog = blogMapper.selectByPrimaryKey(blogid);
+        if (blog == null) {
+            return null;
+        }
+        Category category = categoryMapper.selectByPrimaryKey(blog.getCategoryid());
+        blog.setCategory(category);
+        List<Tag> tags = tagMapper.getTagByBlogId(blog.getBlogid());
+        blog.setTags(tags.size() > 0 ? tags : null);
+        asyncService.updatebloghits(blogid);//异步更新阅读次数
+        logger.info("没有走缓存");
+        return blog;
+    }

3.5 测试

我们调用一个getBlogDetail(获取博客详情)100次来对比一下时间。连接的数据库在深圳,本人在广州,还是有那么一丢丢的网路延时的。

public class SpringTest {
+    @Test
+    public void init() {
+        ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring-test.xml");
+        IBlogService blogService = (IBlogService) ctx.getBean("blogService");
+        long startTime = System.currentTimeMillis();
+        for (int i = 0; i < 100; i++) {
+            blogService.getBlogDetail(615);
+        }
+        System.out.println(System.currentTimeMillis() - startTime);
+    }
+}

为了做一下对比,我们同时使用mybatis自身缓存来进行测试。

3.6 实验结果

统计出结果如下:

没有使用任何缓存(mybatis一级缓存没有关闭):18305
+使用远程Redis缓存:12727
+使用Mybatis缓存:6649
+使用本地Redis缓存:5818

由结果看出,缓存的使用大大较少了获取数据的时间。

部署进个人博客之后,redis已经缓存的数据:

3.7 分页的数据怎么办

个人网站中共有两个栏目,一个是技术杂谈,另一个是生活笔记,每点击一次栏目的时候,会根据页数从数据库中查询数据,百度了下,大概有三种方法:
(1)以页码作为Key,然后缓存整个页面。
(2)分条存取,只从数据库中获取分页的文章ID序列,然后从service(缓存策略在service中实现)中获取。
第一种,由于使用了第三方的插件PageHelper,分页获取的话会比较麻烦,同时整页缓存对内存压力也蛮大的,毕竟服务器只有2g。第二条实现方式简单,缺陷是依旧需要查询数据库,想了想还是放弃了。缓存的初衷是对请求频繁又不易变的数据,实际使用中很少会反复的请求同一页的数据(查询条件也相同),当然对数据中某些字段做缓存还是有必要的。

四、如何解决脏读?

对于文章来说,内容是不经常更新的,没有涉及到缓存一致性,但是对于文章的阅读量,用户每点击一次,就应该更新浏览量的。对于文章的缓存,常规的设计是将文章存储进数据库中,然后读取的时候放入缓存中,然后将浏览量以文章ID+浏览量的结构实时的存入redis服务器中。本站当初设计不合理,直接将浏览量作为一个字段,用户每点击一次的时候就异步更新浏览量,但是此处没有更新缓存,如果手动更新缓存的话,基本上每点击一次都得执行更新操作,同样也不合理。所以,目前本站,你们在页面上看到的浏览量和数据库中的浏览量并不是一致的。有兴趣的可以点击我的网站玩玩~~

五、题外话

兄弟姐妹们啊,个人网站只是个小项目,纯属为了学习而用的,文章可以看看,但是,就不要抓取了吧。。。。一个小时抓取6万次宝宝心脏真的受不了,虽然服务器一切都还稳定==

个人网站http://www.wenzhihuai.com
个人网站源码,希望能给个starhttps://github.com/Zephery/newblog

参考:
1.《深入理解mybatis原理》 MyBatis的一级缓存实现详解
2.《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
3.聊聊Mybatis缓存机制
4.Spring思维导图
5.SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
6.《深入分布式缓存:从原理到实践》

`,75)]))}const p=s(e,[["render",h],["__file","redis缓存.html.vue"]]),r=JSON.parse('{"path":"/database/redis/redis%E7%BC%93%E5%AD%98.html","title":"Redis缓存","lang":"zh-CN","frontmatter":{"description":"Redis缓存 一、概述 1.1 缓存介绍 系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 缓存常用语: 数据不...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Redis缓存"}],["meta",{"property":"og:description","content":"Redis缓存 一、概述 1.1 缓存介绍 系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 缓存常用语: 数据不..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20180121034503.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:53:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:53:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Redis缓存\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20180121034503.png\\",\\"https://github-images.wenzhihuai.com/images/20180120015614.png\\",\\"http://image.wenzhihuai.com/images/20180120022427.png\\",\\"https://github-images.wenzhihuai.com/images/20180120030017.png\\",\\"https://github-images.wenzhihuai.com/images/20180120120015.png\\",\\"https://github-images.wenzhihuai.com/images/20180119110640.png\\",\\"https://github-images.wenzhihuai.com/images/20180120052757.png\\",\\"https://github-images.wenzhihuai.com/images/20180119044345.png\\"],\\"dateModified\\":\\"2024-02-06T07:53:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、概述","slug":"一、概述","link":"#一、概述","children":[{"level":3,"title":"1.1 缓存介绍","slug":"_1-1-缓存介绍","link":"#_1-1-缓存介绍","children":[]},{"level":3,"title":"1.2 本站缓存架构","slug":"_1-2-本站缓存架构","link":"#_1-2-本站缓存架构","children":[]}]},{"level":2,"title":"二、Mybatis缓存","slug":"二、mybatis缓存","link":"#二、mybatis缓存","children":[{"level":3,"title":"2.1 mybatis一级缓存","slug":"_2-1-mybatis一级缓存","link":"#_2-1-mybatis一级缓存","children":[]},{"level":3,"title":"2.2 mybatis二级缓存","slug":"_2-2-mybatis二级缓存","link":"#_2-2-mybatis二级缓存","children":[]},{"level":3,"title":"2.3 Mybatis在分布式环境下脏读问题","slug":"_2-3-mybatis在分布式环境下脏读问题","link":"#_2-3-mybatis在分布式环境下脏读问题","children":[]}]},{"level":2,"title":"三、Redis缓存","slug":"三、redis缓存","link":"#三、redis缓存","children":[{"level":3,"title":"3.1 Spring Cache","slug":"_3-1-spring-cache","link":"#_3-1-spring-cache","children":[]},{"level":3,"title":"3.2 引入包","slug":"_3-2-引入包","link":"#_3-2-引入包","children":[]},{"level":3,"title":"3.3 ApplicationContext.xml","slug":"_3-3-applicationcontext-xml","link":"#_3-3-applicationcontext-xml","children":[]},{"level":3,"title":"3.5 自定义KeyGenerator","slug":"_3-5-自定义keygenerator","link":"#_3-5-自定义keygenerator","children":[]},{"level":3,"title":"3.4 添加注解","slug":"_3-4-添加注解","link":"#_3-4-添加注解","children":[]},{"level":3,"title":"3.5 测试","slug":"_3-5-测试","link":"#_3-5-测试","children":[]},{"level":3,"title":"3.6 实验结果","slug":"_3-6-实验结果","link":"#_3-6-实验结果","children":[]},{"level":3,"title":"3.7 分页的数据怎么办","slug":"_3-7-分页的数据怎么办","link":"#_3-7-分页的数据怎么办","children":[]}]},{"level":2,"title":"四、如何解决脏读?","slug":"四、如何解决脏读","link":"#四、如何解决脏读","children":[]},{"level":2,"title":"五、题外话","slug":"五、题外话","link":"#五、题外话","children":[]}],"git":{"createdTime":1579957849000,"updatedTime":1707206019000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":12.08,"words":3623},"filePathRelative":"database/redis/redis缓存.md","localizedDate":"2020年1月25日","excerpt":"\\n

一、概述

\\n

1.1 缓存介绍

\\n

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
\\n缓存常用语:
\\n数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
\\n可查看Redis实战(一) 使用缓存合理性

","autoDesc":true}');export{p as comp,r as data}; diff --git a/assets/request_limit.html-CBNYeI2L.js b/assets/request_limit.html-CBNYeI2L.js new file mode 100644 index 00000000..c2d4c26c --- /dev/null +++ b/assets/request_limit.html-CBNYeI2L.js @@ -0,0 +1,7 @@ +import{_ as s,c as i,d as t,o as n}from"./app-ftEjETWs.js";const r={};function a(l,e){return n(),i("div",null,e[0]||(e[0]=[t(`

Kubernetes之request 和 limit详解

我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

resources:  
+    limits:    
+        cpu: "1"
+        memory: "500Mi"
+    requests:    
+        cpu: "100m"
+        memory: "1000Mi"

下面我们首先来了解一下上面这段 yaml 文件中字段的含义:requests 和 limits:

requests 定义了对应的容器所需要的最小资源量。
limits 定义了对应容器最大可以消耗的资源上限。
cpu 等于1一般等同于1CPU 核心,1个VCPU或者一个超线程,具体要看服务器的CPU。而 limits 这里设置的 100m 则叫做100毫核,100m就表示0.1个核,所以这里也可以用0.1代替。
memory 等于500Mi,(备注:1Mi=10241024;1M=10001000)
接下来我们来初步理解 requests 和 limits 这两个资源限制类型,在 Kubernetes 对 CPU 和内存资源限额的设计,通常是指用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。所以一般来说,在调度的时候 requests 比较重要,在运行时 limits 比较重要。

而对应实际的业务场景来说,以 java 应用为例,requests 对应的就是JVM虚拟机所需资源的最小值,而 limits 对应的就是 JVM 虚拟机所能够使用的资源最大值。以内存资源为例一般就是指:Xms 和 Xmx,如果 requests 值设置的小于JVM虚拟机 Xms 的值,那么就会导致 Pod 内存溢出,从而导致 Pod 被杀掉,而后重新创建一个Pod。

那么如果 CPU 资源使用超过了 limits,Pod会不会被杀掉呢?答案是不会,但是被限制。如果没有设置 limits ,Pod 可以使用全部空闲的资源。另外如果设置了 limits而没有设置 requests 时,Kubernetes 默认会将 requests 等于 limits。

这里通常还会将 requests 和 limits 描述的资源分为两类:可压缩资源(compressible resources) 和不可压缩资源(incompressible resources)。这里不难看出CPU这类型资源为可压缩资源,而内存这类型资源为不可压缩资源。所以合理设置不可压缩资源的limits值就相当重要了。

`,8)]))}const o=s(r,[["render",a],["__file","request_limit.html.vue"]]),u=JSON.parse('{"path":"/kubernetes/request_limit.html","title":"Kubernetes之request 和 limit详解","lang":"zh-CN","frontmatter":{"description":"Kubernetes之request 和 limit详解 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 ...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/request_limit.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Kubernetes之request 和 limit详解"}],["meta",{"property":"og:description","content":"Kubernetes之request 和 limit详解 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T11:46:28.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T11:46:28.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Kubernetes之request 和 limit详解\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T11:46:28.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708083988000,"updatedTime":1708083988000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":2.21,"words":663},"filePathRelative":"kubernetes/request_limit.md","localizedDate":"2024年2月16日","excerpt":"\\n

我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

","autoDesc":true}');export{o as comp,u as data}; diff --git a/assets/serverlog.html-bICZU0Nm.js b/assets/serverlog.html-bICZU0Nm.js new file mode 100644 index 00000000..bfc37fdc --- /dev/null +++ b/assets/serverlog.html-bICZU0Nm.js @@ -0,0 +1,2 @@ +import{_ as i,c as e,d as t,o as s}from"./app-ftEjETWs.js";const n={};function h(p,a){return s(),e("div",null,a[0]||(a[0]=[t(`

被挖矿攻击

服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:

stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

使用top查看:

启动iptables,参考http://www.setphp.com/981.html
http://www.setphp.com/981.html

iptables -A INPUT -s xmr.crypto-pool.fr -j DROP
+iptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP

2017-10-02 再次遭到挖矿攻击

2017-11-20

19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。

2017-12-03

2017-12-05 wipefs

`,14)]))}const o=i(n,[["render",h],["__file","serverlog.html.vue"]]),g=JSON.parse('{"path":"/java/serverlog.html","title":"被挖矿攻击","lang":"zh-CN","frontmatter":{"description":"被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJY...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/serverlog.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"被挖矿攻击"}],["meta",{"property":"og:description","content":"被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJY..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"http://image.wenzhihuai.com/images/553ac1ca20170923033013.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T10:30:22.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-24T10:30:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"被挖矿攻击\\",\\"image\\":[\\"http://image.wenzhihuai.com/images/553ac1ca20170923033013.png\\",\\"http://image.wenzhihuai.com/images/20170923033252.png\\",\\"http://image.wenzhihuai.com/images/20170923034427.png\\",\\"http://image.wenzhihuai.com/images/20170923040503.png\\",\\"http://image.wenzhihuai.com/images/20171203010136.png\\",\\"http://image.wenzhihuai.com/images/20171205091617.png\\"],\\"dateModified\\":\\"2024-01-24T10:30:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1579957849000,"updatedTime":1706092222000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.57,"words":172},"filePathRelative":"java/serverlog.md","localizedDate":"2020年1月25日","excerpt":"\\n

服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
\\n\\"\\"
\\nstratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

","autoDesc":true}');export{o as comp,g as data}; diff --git a/assets/spark on k8s operator.html-D9Tpznzk.js b/assets/spark on k8s operator.html-D9Tpznzk.js new file mode 100644 index 00000000..9407cb8b --- /dev/null +++ b/assets/spark on k8s operator.html-D9Tpznzk.js @@ -0,0 +1,67 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

spark on k8s operator

Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。

Spark Operator包括如下几个组件:

  1. SparkApplication控制器:该控制器用于创建、更新、删除SparkApplication对象,同时控制器还会监控相应的事件,执行相应的动作。
  2. Submission Runner:负责调用spark-submit提交Spark作业,作业提交的流程完全复用Spark on K8s的模式。
  3. Spark Pod Monitor:监控Spark作业相关Pod的状态,并同步到控制器中。
  4. Mutating Admission Webhook:可选模块,基于注解来实现Driver/Executor Pod的一些定制化需求。
  5. SparkCtl:用于和Spark Operator交互的命令行工具。

安装

# 添加源
+helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator
+# 安装
+helm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace

这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。

forbidden: error looking up service account default/spark: serviceaccount \\"spark\\" not found.\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport

同时也要安装rbac

rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见rbac.yaml

执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。

代码如下:

 SparkConf sparkConf = new SparkConf().setAppName("Spark WordCount Application (java)");
+        sparkConf.set("es.nodes", "xxx")
+                .set("es.port", "8xx0")
+                .set("es.nodes.wan.only", "true")
+                .set("es.net.http.auth.user", "xxx")
+                .set("es.net.http.auth.pass", "xxx-xxx");
+        try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {
+            JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce").values();
+
+            System.out.println(esRDD.partitions().size());
+
+            esRDD.map(x -> x.get("customer_full_name"))
+                    .countByValue()
+                    .forEach((x, y) -> System.out.println(x + ":" + y));
+            TimeUnit.HOURS.sleep(1);
+
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }

打包之后kubectl create -f k8s.yaml即可

apiVersion: "sparkoperator.k8s.io/v1beta2"
+kind: SparkApplication
+metadata:
+  name: spark-demo
+  namespace: default
+spec:
+  type: Scala
+  mode: cluster
+  image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3
+  imagePullPolicy: Always
+  mainClass: com.spark.es.demo.EsReadGroupByName
+  mainApplicationFile: "local:///opt/spark/examples/jars/app.jar"
+  sparkVersion: "3.1.1"
+  restartPolicy:
+    type: Never
+  driver:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    coreLimit: "1200m"
+    memory: "512m"
+    labels:
+      version: 3.1.1
+    serviceAccount: sparkoperator
+  executor:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    instances: 1
+    memory: "512m"
+    labels:
+      version: 3.1.1
spark-demo-driver                                          1/1     Running   0          2m30s
+spark-wordcount-application-java-0ac352810a9728e1-exec-1   1/1     Running   0          2m1s

同时会自动生成相应的service。

image-20220528203941897
image-20220528203941897
image-20220528203816649

Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,

源码解析

architecture-diagram

Spark Operator的主要组件如下:

1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;

2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;

3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;

4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。

5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。

image-20220612194403742

apis:用户编写yaml时的解析

Batchscheduler:批处理的调度,提供了支持volcano的接口

Client:

leader的选举

electionCfg := leaderelection.LeaderElectionConfig{
+   Lock:          resourceLock,
+   LeaseDuration: *leaderElectionLeaseDuration,
+   RenewDeadline: *leaderElectionRenewDeadline,
+   RetryPeriod:   *leaderElectionRetryPeriod,
+   Callbacks: leaderelection.LeaderCallbacks{
+      OnStartedLeading: func(c context.Context) {
+         close(startCh)
+      },
+      OnStoppedLeading: func() {
+         close(stopCh)
+      },
+   },
+}
+
+elector, err := leaderelection.NewLeaderElector(electionCfg)

参考:

1.k8s client-go中Leader选举实现

2.[Kubernetes基于leaderelection选举策略实现组件高可用

`,38)]))}const p=i(l,[["render",t],["__file","spark on k8s operator.html.vue"]]),r=JSON.parse('{"path":"/kubernetes/spark%20on%20k8s%20operator.html","title":"spark on k8s operator","lang":"zh-CN","frontmatter":{"description":"spark on k8s operator Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。 使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"spark on k8s operator"}],["meta",{"property":"og:description","content":"spark on k8s operator Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。 使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/e6c9d24egy1h2oeh4gm96j21qg02wweq.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-27T18:29:27.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-27T18:29:27.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"spark on k8s operator\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/e6c9d24egy1h2oeh4gm96j21qg02wweq.jpg\\"],\\"dateModified\\":\\"2024-01-27T18:29:27.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1655036363000,"updatedTime":1706380167000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":3.51,"words":1054},"filePathRelative":"kubernetes/spark on k8s operator.md","localizedDate":"2022年6月12日","excerpt":"\\n

Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

","autoDesc":true}');export{p as comp,r as data}; diff --git a/assets/springcloud_sentinel.html-CFCKUB71.js b/assets/springcloud_sentinel.html-CFCKUB71.js new file mode 100644 index 00000000..7f8d65a6 --- /dev/null +++ b/assets/springcloud_sentinel.html-CFCKUB71.js @@ -0,0 +1 @@ +import{_ as t,c as n,d as i,o as r}from"./app-ftEjETWs.js";const o={};function a(l,e){return r(),n("div",null,e[0]||(e[0]=[i('

Spring Cloud Sentinel

参考

1.超详细springcloud sentinel教程~

',3)]))}const s=t(o,[["render",a],["__file","springcloud_sentinel.html.vue"]]),c=JSON.parse('{"path":"/middleware/sentinel/springcloud_sentinel.html","title":"Spring Cloud Sentinel","lang":"zh-CN","frontmatter":{"index":false,"description":"Spring Cloud Sentinel 参考 1.超详细springcloud sentinel教程~","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Spring Cloud Sentinel"}],["meta",{"property":"og:description","content":"Spring Cloud Sentinel 参考 1.超详细springcloud sentinel教程~"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Spring Cloud Sentinel\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.06,"words":17},"filePathRelative":"middleware/sentinel/springcloud_sentinel.md","localizedDate":"2024年3月10日","excerpt":"\\n

参考

\\n

1.超详细springcloud sentinel教程~

","autoDesc":true}');export{s as comp,c as data}; diff --git a/assets/starcraft-ai.html-C018rwGF.js b/assets/starcraft-ai.html-C018rwGF.js new file mode 100644 index 00000000..a7415207 --- /dev/null +++ b/assets/starcraft-ai.html-C018rwGF.js @@ -0,0 +1 @@ +import{_ as e,c as a,d as i,o as r}from"./app-ftEjETWs.js";const n={};function p(o,t){return r(),a("div",null,t[0]||(t[0]=[i('

StarCraft Ⅱ 人工智能教程

非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

一、其他的太抽象了,先讲人机对战吧

sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的就是,需要以管理员身份运行,下面详细过程,翻译自 ProBots 2021 Season 1 - Human vs Bot

1.安装星际争霸2,地址,至于要不要下载国际服,似乎没有必要
2.下载ProBots vs Humans.Zip
3.解压,附带了地图,主要是sc2aiapp

4.可选,下载相关地图,可以从竞技场里下,需要放到星际争霸2的目录下,mac的是/Applications/StarCraft II/Maps
5.打开步骤2的目录
6.打开sc2aiapp,打开的时候有可能报错:

右键sc2aiapp,以管理员身份运行即可,现在不让注册了,直接continue without login

7.全屏快捷键,Alt + Enter,进行对战

我这录制了个我对战的视频,bilibili,感觉AI在对战里很容易只有一样打法,据说是强化训练后的最优选导致的,这个也不知怎么整,个人感觉MicroMachine这个AI打法稍微多样,可以多和它对战下。

二、AI天梯

目前没有看到什么办法让暴雪允许AI在实际的天梯上进行运行,但社区搞了个专门的AI天梯,sc2ai,可以将代码上传到里面进行对战,实时流我没看到,对战完后可以下载replay复盘。下面讲下如何上传代码进行对战。
1.第一步肯定是先要注册登录
2.u

3.主要是这个Bot zip,基本的代码架构还是要固定的

具体可以看下sc2-api-simple-bot这里,记得把它打包即可
4.成功之后,即可从profile里看到自己的机器人

5.此时,bot是不会进行比赛,需要参赛,点击Competitions,然后选择赛季

6.比赛是随机的放到队列里的,可能需要排队进行比赛,也可能主动申请和具体的机器人进行比赛,点击Request Match,进行申请比赛。

7.慢慢等待,比赛结束之前都看不到结果的,也没有实时流进行查看的,结束之后就可以看到结果以及下载replay。其中arena会随机的进行一些比赛,也有可能是别人随机选的,一个bot一天大概能有50场比赛,arena也会提供统计,胜率、ELO(分数)等

Bot开发样例

https://github.com/Zephery/sc2-api-simple-bot.git
https://community.eschamp.com/t/simple-starcraft-2-bot-template-to-get-started/155

',24)]))}const g=e(n,[["render",p],["__file","starcraft-ai.html.vue"]]),h=JSON.parse('{"path":"/interesting/starcraft-ai.html","title":"StarCraft Ⅱ 人工智能教程","lang":"zh-CN","frontmatter":{"description":"StarCraft Ⅱ 人工智能教程 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。 一、其他的太抽象了,先讲人机对战吧 sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/starcraft-ai.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"StarCraft Ⅱ 人工智能教程"}],["meta",{"property":"og:description","content":"StarCraft Ⅱ 人工智能教程 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。 一、其他的太抽象了,先讲人机对战吧 sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/755525-20211220223200207-5235039.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"StarCraft Ⅱ 人工智能教程\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/755525-20211220223200207-5235039.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223205053-593748265.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223209872-1962720900.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223216057-996863489.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223227654-1821240851.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223232318-1161227676.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223243665-397946151.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223244870-738059998.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223250129-209699308.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20211220223256462-1602936807.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706288023000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":2.72,"words":817},"filePathRelative":"interesting/starcraft-ai.md","localizedDate":"2024年1月26日","excerpt":"\\n

非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

","autoDesc":true}');export{g as comp,h as data}; diff --git a/assets/style-BjEFg8CQ.css b/assets/style-BjEFg8CQ.css new file mode 100644 index 00000000..33be04b6 --- /dev/null +++ b/assets/style-BjEFg8CQ.css @@ -0,0 +1 @@ +:root{--code-padding-x: 1.25rem;--code-padding-y: 1rem;--code-border-radius: 6px;--code-line-height: 1.6;--code-font-size: 14px;--code-font-family: consolas, monaco, "Andale Mono", "Ubuntu Mono", monospace}div[class*=language-]{position:relative;border-radius:var(--code-border-radius);background-color:var(--code-c-bg)}div[class*=language-]:before{content:attr(data-title);position:absolute;top:.8em;right:1em;z-index:3;color:var(--code-c-text);font-size:.75rem}div[class*=language-] pre{position:relative;z-index:1;overflow-x:auto;margin:0;border-radius:var(--code-border-radius);font-size:var(--code-font-size);font-family:var(--code-font-family);line-height:var(--code-line-height)}div[class*=language-] pre code{display:block;box-sizing:border-box;width:-moz-fit-content;width:fit-content;min-width:100%;padding:var(--code-padding-y) var(--code-padding-x);background-color:#0000!important;color:var(--code-c-text);overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}:root{--code-c-text: #9e9e9e;--code-c-highlight-bg: rgba(142 150 170 / 14%)}.shiki span{color:var(--shiki-light, inherit)}[data-theme=dark] .shiki span{color:var(--shiki-dark, inherit)}div[data-highlighter=shiki]{background-color:var(--code-c-bg, var(--shiki-light-bg))}[data-theme=dark] div[data-highlighter=shiki]{background-color:var(--code-c-bg, var(--shiki-dark-bg))}:root{--code-line-number-width: 3rem}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-line-number-width);height:100%;border-right:1px solid var(--code-c-highlight-bg, var(--code-c-text));border-radius:var(--code-border-radius) 0 0 var(--code-border-radius);transition:border var(--vp-t-color)}div[class*=language-].line-numbers-mode pre{vertical-align:middle;margin-left:var(--code-line-number-width)}div[class*=language-].line-numbers-mode code{padding-left:1rem}div[class*=language-].line-numbers-mode .line-numbers{counter-reset:line-number;position:absolute;top:0;width:var(--code-line-number-width);padding-top:var(--code-padding-y);color:var(--code-c-line-number, var(--code-c-text));font-size:var(--code-font-size);line-height:var(--code-line-height);text-align:center}div[class*=language-].line-numbers-mode .line-number{position:relative;z-index:3;font-family:var(--code-font-family);-webkit-user-select:none;-moz-user-select:none;user-select:none}div[class*=language-].line-numbers-mode .line-number:before{content:counter(line-number);counter-increment:line-number}div[class*=language-] .line.highlighted{display:inline-block;width:100%;margin:0 calc(-1*var(--code-padding-x));padding:0 var(--code-padding-x);background-color:var(--code-c-highlight-bg)}:root{--vp-c-bg: #fff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #fff;--vp-c-text: rgb(60, 60, 67);--vp-c-border: #c2c2c4;--vp-c-gutter: #e2e2e3;--vp-c-shadow: rgba(0, 0, 0, .15);--vp-c-control: rgba(142, 150, 170, .1);--vp-c-control-hover: rgba(142, 150, 170, .16);--vp-c-control-disabled: #eaeaea;--navbar-height: 3.75rem;--navbar-padding-x: 1.5rem;--navbar-padding-y: .7rem;--navbar-mobile-height: 3.25rem;--navbar-mobile-padding-x: 1rem;--navbar-mobile-padding-y: .5rem;--sidebar-width: 18rem;--sidebar-mobile-width: 16rem;--content-width: 780px;--home-page-width: 1160px;--vp-t-color: .3s ease;--vp-t-transform: .3s ease;--vp-c-accent: rgb(46.6962025316, 131.8037974684, 93.3924050633);--vp-c-accent-bg: rgb(60.0379746835, 169.4620253165, 120.0759493671);--vp-c-accent-hover: rgb(66.7088607595, 188.2911392405, 133.417721519);--vp-c-accent-soft: rgba(20.4, 183.6, 109.9433628319, .14);--vp-c-accent-text: var(--vp-c-white);--vp-c-bg-soft: rgba(255, 255, 255, .9);--vp-c-bg-elv-soft: rgba(255, 255, 255, .9);--vp-c-text-mute: rgba(60, 60, 67, .78);--vp-c-text-subtle: rgba(60, 60, 67, .56);--vp-c-border-hard: rgb(184.0875, 184.0875, 186.4125)}[data-theme=dark]{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-text: rgba(235, 235, 245, .86);--vp-c-border: #3c3f44;--vp-c-gutter: #000;--vp-c-shadow: rgba(0, 0, 0, .3);--vp-c-control: rgba(101, 117, 133, .12);--vp-c-control-hover: rgba(101, 117, 133, .18);--vp-c-control-disabled: #363636;--vp-c-accent: rgb(85.5379746835, 194.9620253165, 145.5759493671);--vp-c-accent-bg: rgb(63.3734177215, 178.8765822785, 126.746835443);--vp-c-accent-hover: rgb(60.0379746835, 169.4620253165, 120.0759493671);--vp-c-accent-soft: rgba(20.4, 183.6, 109.9433628319, .16);--vp-c-bg-soft: rgba(27, 27, 31, .9);--vp-c-bg-elv-soft: rgba(32, 33, 39, .9);--vp-c-text-mute: rgba(235, 235, 245, .602);--vp-c-text-subtle: rgba(235, 235, 245, .3784);--vp-c-border-hard: rgb(68.953125, 72.40078125, 78.146875)}.vp-catalog{margin-top:8px;margin-bottom:8px}.vp-catalog-item{counter-increment:catalog-item}.vp-catalog-main-title{margin-top:calc(1rem - var(--catalog-header-offset));margin-bottom:.5rem;padding-top:var(--catalog-header-offset);font-weight:500;font-size:1.75rem}.vp-catalog-main-title:first-child{margin-bottom:.5rem!important}.vp-catalog-main-title:only-child{margin-bottom:0!important}.vp-catalog-main-title .vp-link{text-decoration:none!important}.vp-catalog-child-title{margin-bottom:.5rem!important}.vp-catalog-child-title.has-children{margin-top:calc(1rem - var(--catalog-header-offset));padding-top:var(--catalog-header-offset);border-bottom:1px solid var(--catalog-c-gutter);font-weight:500;font-size:1.3rem;transition:border-color .3s}.vp-catalog-child-title.has-children:only-child{margin-bottom:0!important}.vp-catalog-child-title .vp-link{text-decoration:none!important}.vp-catalog-sub-title{font-weight:500;font-size:1.1rem}.vp-catalog-sub-title:only-child{margin-bottom:0!important}.vp-catalog-title{color:inherit;text-decoration:none}.vp-catalog-title:hover{color:var(--catalog-c-accent)}.vp-catalog.index ol{padding-inline-start:0}.vp-catalog.index li{list-style-type:none}.vp-catalog.index .vp-catalog-list{padding-inline-start:0}.vp-catalog.index .vp-catalog-item{list-style-type:none}.vp-catalog.index .vp-catalog-title:before{content:"§" counter(catalog-item,upper-roman) " "}.vp-catalog.index .vp-child-catalogs{counter-reset:child-catalog}.vp-catalog.index .vp-child-catalog{counter-increment:child-catalog}.vp-catalog.index .vp-child-catalog .vp-catalog-title:before{content:counter(catalog-item) "." counter(child-catalog) " "}.vp-catalog.index .vp-sub-catalogs{padding-inline-start:.5rem}.vp-catalog-list{counter-reset:catalog-item;margin:0}.vp-catalog-list.deep{padding-inline-start:0}.vp-catalog-list.deep .vp-catalog-item{list-style-type:none}.vp-catalog-list .font-icon{vertical-align:baseline;margin-inline-end:.25rem}.vp-child-catalogs{margin:0}.vp-child-catalog{list-style-type:disc}.vp-sub-catalogs{counter-reset:sub-catalog}.vp-sub-catalog{counter-increment:sub-catalog}.vp-sub-catalog .vp-link:before{content:counter(catalog-item) "." counter(child-catalog) "." counter(sub-catalog) " "}.vp-sub-catalogs-wrapper{display:flex;flex-wrap:wrap}.vp-sub-catalog-link{display:inline-block;margin:4px 8px;padding:4px 8px;border-radius:6px;background-color:var(--catalog-c-control);line-height:1.5;overflow-wrap:break-word;transition:background-color .3s,color .3s}.vp-sub-catalog-link:hover{background-color:var(--catalog-c-control-hover);color:var(--catalog-c-accent);text-decoration:none!important}.vp-catalog-header-anchor{float:left;margin-top:.125em;margin-left:-1em;padding-right:0;font-size:.85em;text-decoration:none;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{.vp-catalog-header-anchor{display:none}}h2:hover .vp-catalog-header-anchor,h3:hover .vp-catalog-header-anchor{text-decoration:none;opacity:1}.vp-catalog-header-anchor:focus-visible{opacity:1}.vp-empty-catalog{font-size:1.25rem;text-align:center}:root{--catalog-c-accent: var(--vp-c-accent);--catalog-c-accent-text: var(--vp-c-accent-text);--catalog-c-control: var(--vp-c-bg-alt);--catalog-c-control-hover: var(--vp-c-bg-alt);--catalog-c-gutter: var(--vp-c-gutter);--catalog-header-offset: var(--header-offset, 3.6rem)}.vp-back-to-top-button{position:fixed!important;inset-inline-end:1rem;bottom:4rem;z-index:100;width:48px;height:48px;padding:12px;border-width:0;border-radius:50%;background:var(--back-to-top-c-bg);color:var(--back-to-top-c-accent-bg);box-shadow:2px 2px 10px 4px var(--back-to-top-c-shadow);cursor:pointer}@media (max-width: 959px){.vp-back-to-top-button{transform:scale(.8);transform-origin:100% 100%}}@media print{.vp-back-to-top-button{display:none}}.vp-back-to-top-button:hover{color:var(--back-to-top-c-accent-hover)}.vp-back-to-top-button .back-to-top-icon{overflow:hidden;width:24px;height:24px;margin:0 auto;background:var(--back-to-top-c-icon);-webkit-mask-image:var(--back-to-top-icon);mask-image:var(--back-to-top-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover}.vp-scroll-progress{position:absolute;right:-2px;bottom:-2px;width:52px;height:52px}.vp-scroll-progress svg{width:100%;height:100%}.vp-scroll-progress circle{opacity:.9;transform:rotate(-90deg);transform-origin:50% 50%}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--back-to-top-z-index: 5;--back-to-top-icon: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2048%2048'%3e%3cpath%20fill='none'%20stroke='currentColor'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='4'%20d='M24.008%2014.1V42M12%2026l12-12l12%2012M12%206h24'%20/%3e%3c/svg%3e");--back-to-top-c-bg: var(--vp-c-bg);--back-to-top-c-accent-bg: var(--vp-c-accent-bg);--back-to-top-c-accent-hover: var(--vp-c-accent-hover);--back-to-top-c-shadow: var(--vp-c-shadow);--back-to-top-c-icon: currentcolor}:root{--nprogress-c: var(--vp-c-accent);--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{position:fixed;top:0;left:0;z-index:var(--nprogress-z-index);width:100%;height:2px;background:var(--nprogress-c)}.vp-copy-code-button{position:absolute;top:.5em;right:.5em;z-index:5;width:2.5rem;height:2.5rem;padding:0;border-width:0;border-radius:.5rem;background:#0000;outline:none;opacity:0;cursor:pointer;transition:opacity .4s}@media print{.vp-copy-code-button{display:none}}.vp-copy-code-button:before{content:"";display:inline-block;width:1.25rem;height:1.25rem;padding:.625rem;background:currentcolor;color:var(--copy-code-c-text);font-size:1.25rem;-webkit-mask-image:var(--code-copy-icon);mask-image:var(--code-copy-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.vp-copy-code-button:focus,.vp-copy-code-button.copied{opacity:1}.vp-copy-code-button:hover,.vp-copy-code-button.copied{background:var(--copy-code-c-hover)}.vp-copy-code-button.copied:before{-webkit-mask-image:var(--code-copied-icon);mask-image:var(--code-copied-icon)}.vp-copy-code-button.copied:after{content:attr(data-copied);position:absolute;top:0;right:calc(100% + .25rem);display:block;height:1.25rem;padding:.625rem;border-radius:.5rem;background:var(--copy-code-c-hover);color:var(--copy-code-c-text);font-weight:500;line-height:1.25rem;white-space:nowrap}.no-copy-code .vp-copy-code-button{display:none}body:not(.no-copy-code) div[class*=language-]:hover:before{display:none}body:not(.no-copy-code) div[class*=language-]:hover .vp-copy-code-button{opacity:1}:root{--code-copy-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23808080' stroke-width='2'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2' /%3e%3c/svg%3e");--code-copied-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23808080' stroke-width='2'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4' /%3e%3c/svg%3e");--copy-code-c-text: var(--code-c-line-number);--copy-code-c-hover: var(--code-c-highlight-bg)}.hint-container{position:relative;background:var(--hint-c-soft);transition:background var(--vp-t-color),color var(--vp-t-color)}@media print{.hint-container{page-break-inside:avoid}}.hint-container>.hint-container-title{color:var(--hint-c-title)}.hint-container :not(pre)>code{background:var(--hint-c-soft)}.hint-container .hint-container-title{position:relative;margin-block:.75em;font-weight:600;line-height:1.25}.hint-container.important,.hint-container.info,.hint-container.note,.hint-container.tip,.hint-container.warning,.hint-container.caution{margin-block:.75rem;padding:.25em 1em;border-radius:.5em;color:inherit;font-size:var(--hint-font-size)}@media print{.hint-container.important,.hint-container.info,.hint-container.note,.hint-container.tip,.hint-container.warning,.hint-container.caution{border-inline-start-width:.25em;border-inline-start-style:solid}}.hint-container.important .hint-container-title,.hint-container.info .hint-container-title,.hint-container.note .hint-container-title,.hint-container.tip .hint-container-title,.hint-container.warning .hint-container-title,.hint-container.caution .hint-container-title{padding-inline-start:1.75em}@media print{.hint-container.important .hint-container-title,.hint-container.info .hint-container-title,.hint-container.note .hint-container-title,.hint-container.tip .hint-container-title,.hint-container.warning .hint-container-title,.hint-container.caution .hint-container-title{padding-inline-start:0}}.hint-container.important .hint-container-title:before,.hint-container.info .hint-container-title:before,.hint-container.note .hint-container-title:before,.hint-container.tip .hint-container-title:before,.hint-container.warning .hint-container-title:before,.hint-container.caution .hint-container-title:before{content:" ";position:absolute;inset-inline-start:0;top:calc(50% - .6125em);width:1.25em;height:1.25em;font-size:1.25em}@media print{.hint-container.important .hint-container-title:before,.hint-container.info .hint-container-title:before,.hint-container.note .hint-container-title:before,.hint-container.tip .hint-container-title:before,.hint-container.warning .hint-container-title:before,.hint-container.caution .hint-container-title:before{display:none}}.hint-container.important p,.hint-container.info p,.hint-container.note p,.hint-container.tip p,.hint-container.warning p,.hint-container.caution p{line-height:1.5}.hint-container.important a,.hint-container.info a,.hint-container.note a,.hint-container.tip a,.hint-container.warning a,.hint-container.caution a{color:var(--vp-c-accent)}.hint-container.important{--hint-c-accent: var(--important-c-accent);--hint-c-title: var(--important-c-text);--hint-c-soft: var(--important-c-soft)}.hint-container.important>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.info{--hint-c-accent: var(--info-c-accent);--hint-c-title: var(--info-c-text);--hint-c-soft: var(--info-c-soft)}.hint-container.info>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.note{--hint-c-accent: var(--note-c-accent);--hint-c-title: var(--note-c-text);--hint-c-soft: var(--note-c-soft)}.hint-container.note>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.tip{--hint-c-accent: var(--tip-c-accent);--hint-c-title: var(--tip-c-text);--hint-c-soft: var(--tip-c-soft)}.hint-container.tip>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.warning{--hint-c-accent: var(--warning-c-accent);--hint-c-title: var(--warning-c-text);--hint-c-soft: var(--warning-c-soft)}.hint-container.warning>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.caution{--hint-c-accent: var(--caution-c-accent);--hint-c-title: var(--caution-c-text);--hint-c-soft: var(--caution-c-soft)}.hint-container.caution>.hint-container-title:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.hint-container.details{position:relative;display:block;margin-block:.75rem;padding:1.25rem 1rem;border-radius:.5rem;background:var(--detail-c-bg);transition:background var(--vp-t-transform),color var(--vp-t-transform)}.hint-container.details h4{margin-top:0}.hint-container.details figure:last-child,.hint-container.details p:last-child{margin-bottom:0;padding-bottom:0}.hint-container.details a{color:var(--vp-c-accent)}.hint-container.details :not(pre)>code{background:var(--detail-c-soft)}.hint-container.details summary{position:relative;margin:-1rem;padding-block:1em;padding-inline:3em 1.5em;list-style:none;font-size:var(--hint-font-size);cursor:pointer}.hint-container.details summary::-webkit-details-marker{display:none}.hint-container.details summary::marker{color:#0000;font-size:0}.hint-container.details summary:before{background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:1em;mask-size:1em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:" ";position:absolute;inset-inline-start:.8em;top:calc(50% - .5em);width:1em;height:1em;font-size:1.25rem;line-height:normal;transition:color,var(--vp-t-color),transform var(--vp-t-transform);transform:rotate(90deg)}.hint-container.details[open]>summary{margin-bottom:.5em}.hint-container.details[open]>summary:before{transform:rotate(180deg)}:root{--hint-font-size: .92rem;--important-c-accent: var(--vp-c-purple-bg);--important-c-text: var(--vp-c-purple-text);--important-c-soft: var(--vp-c-purple-soft);--info-c-accent: var(--vp-c-blue-bg);--info-c-text: var(--vp-c-blue-text);--info-c-soft: var(--vp-c-blue-soft);--note-c-accent: var(--vp-c-grey-bg);--note-c-text: var(--vp-c-grey-text);--note-c-soft: var(--vp-c-grey-soft);--tip-c-accent: var(--vp-c-green-bg);--tip-c-text: var(--vp-c-green-text);--tip-c-soft: var(--vp-c-green-soft);--warning-c-accent: var(--vp-c-yellow-bg);--warning-c-text: var(--vp-c-yellow-text);--warning-c-soft: var(--vp-c-yellow-soft);--caution-c-accent: var(--vp-c-red-bg);--caution-c-text: var(--vp-c-red-text);--caution-c-soft: var(--vp-c-red-soft);--detail-c-bg: var(--vp-c-control);--detail-c-icon: var(--vp-c-border);--detail-c-soft: var(--vp-c-grey-soft)}figure{text-align:center}figure>a[href*="://"]:after,figure>a[target=_blank]:after{display:none!important}figcaption{font-size:.8rem}.echarts-title{margin:1rem auto .5rem;font-weight:600;font-size:18px;text-align:center}.echarts-wrapper{position:relative;text-align:center;transition:all 1s}@media (max-width: 719px){.echarts-wrapper{margin:0 -1rem}}.echarts-wrapper .echarts-container{min-height:360px}@media print{.echarts-wrapper .echarts-container{page-break-inside:avoid}}.echarts-wrapper .echarts-loading{position:absolute;inset:0 0 auto;color:var(--vp-c-accent-bg)}.footnote-item{margin-top:calc(0rem - var(--header-offset, 3.6rem));padding-top:calc(var(--header-offset, 3.6rem) + .5rem)}.footnote-item>p{margin-bottom:0}.footnote-ref{position:relative}.footnote-anchor{position:absolute;top:calc(-.5rem - var(--header-offset, 3.6rem))}.task-list-container{padding-inline-start:0;list-style-position:inside}.task-list-container .task-list-container{padding-inline-start:1.5em}.task-list-item{list-style:none}.task-list-item-checkbox{position:relative;vertical-align:text-bottom;height:1em;margin-inline-end:1.5em;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none}.task-list-item-checkbox:after{content:" ";position:absolute;top:0;display:inline-block;box-sizing:border-box;width:1em;height:1em;padding-inline-start:0;border:1px solid var(--vp-c-border);border-radius:2px;background:var(--vp-c-control);text-align:center;visibility:visible;transition:border-color var(--vp-t-color),background var(--vp-t-color)}@media print{.task-list-item-checkbox:after{border-color:var(--vp-c-text)}}.task-list-item-checkbox:checked:after{content:"";border-color:var(--vp-c-accent-bg);background:var(--vp-c-accent-bg)}@media print{.task-list-item-checkbox:checked:after{border-color:var(--vp-c-text);background:transparent}}.task-list-item-checkbox:checked:before{content:"";position:absolute;inset-inline-start:.35em;top:.1em;z-index:1;width:.2em;height:.5em;border:solid var(--vp-c-white);border-width:0 .15em .15em 0;transition:border-color var(--vp-t-color);transform:rotate(45deg)}@media print{.task-list-item-checkbox:checked:before{border-color:var(--vp-c-text)}}:root{--photo-swipe-c-bullet: var(--vp-c-bg-elv);--photo-swipe-c-bullet-active: var(--vp-c-accent)}/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */.pswp{--pswp-bg: #000;--pswp-placeholder-bg: #222;--pswp-root-z-index: 100000;--pswp-preloader-color: rgba(79, 79, 79, .4);--pswp-preloader-color-secondary: rgba(255, 255, 255, .9);--pswp-icon-color: #fff;--pswp-icon-color-secondary: #4f4f4f;--pswp-icon-stroke-color: #4f4f4f;--pswp-icon-stroke-width: 2px;--pswp-error-text-color: var(--pswp-icon-color)}.pswp{position:fixed;top:0;left:0;width:100%;height:100%;z-index:var(--pswp-root-z-index);display:none;touch-action:none;outline:0;opacity:.003;contain:layout style size;-webkit-tap-highlight-color:rgba(0,0,0,0)}.pswp:focus{outline:0}.pswp *{box-sizing:border-box}.pswp img{max-width:none}.pswp--open{display:block}.pswp,.pswp__bg{transform:translateZ(0);will-change:opacity}.pswp__bg{opacity:.005;background:var(--pswp-bg)}.pswp,.pswp__scroll-wrap{overflow:hidden}.pswp__scroll-wrap,.pswp__bg,.pswp__container,.pswp__item,.pswp__content,.pswp__img,.pswp__zoom-wrap{position:absolute;top:0;left:0;width:100%;height:100%}.pswp__img,.pswp__zoom-wrap{width:auto;height:auto}.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img{cursor:zoom-in}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img{cursor:move;cursor:grab}.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active{cursor:grabbing}.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,.pswp__img{cursor:zoom-out}.pswp__container,.pswp__img,.pswp__button,.pswp__counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.pswp__item{z-index:1;overflow:hidden}.pswp__hidden{display:none!important}.pswp__content{pointer-events:none}.pswp__content>*{pointer-events:auto}.pswp__error-msg-container{display:grid}.pswp__error-msg{margin:auto;font-size:1em;line-height:1;color:var(--pswp-error-text-color)}.pswp .pswp__hide-on-close{opacity:.005;will-change:opacity;transition:opacity var(--pswp-transition-duration) cubic-bezier(.4,0,.22,1);z-index:10;pointer-events:none}.pswp--ui-visible .pswp__hide-on-close{opacity:1;pointer-events:auto}.pswp__button{position:relative;display:block;width:50px;height:60px;padding:0;margin:0;overflow:hidden;cursor:pointer;background:none;border:0;box-shadow:none;opacity:.85;-webkit-appearance:none;-webkit-touch-callout:none}.pswp__button:hover,.pswp__button:active,.pswp__button:focus{transition:none;padding:0;background:none;border:0;box-shadow:none;opacity:1}.pswp__button:disabled{opacity:.3;cursor:auto}.pswp__icn{fill:var(--pswp-icon-color);color:var(--pswp-icon-color-secondary)}.pswp__icn{position:absolute;top:14px;left:9px;width:32px;height:32px;overflow:hidden;pointer-events:none}.pswp__icn-shadow{stroke:var(--pswp-icon-stroke-color);stroke-width:var(--pswp-icon-stroke-width);fill:none}.pswp__icn:focus{outline:0}div.pswp__img--placeholder,.pswp__img--with-bg{background:var(--pswp-placeholder-bg)}.pswp__top-bar{position:absolute;left:0;top:0;width:100%;height:60px;display:flex;flex-direction:row;justify-content:flex-end;z-index:10;pointer-events:none!important}.pswp__top-bar>*{pointer-events:auto;will-change:opacity}.pswp__button--close{margin-right:6px}.pswp__button--arrow{position:absolute;width:75px;height:100px;top:50%;margin-top:-50px}.pswp__button--arrow:disabled{display:none;cursor:default}.pswp__button--arrow .pswp__icn{top:50%;margin-top:-30px;width:60px;height:60px;background:none;border-radius:0}.pswp--one-slide .pswp__button--arrow{display:none}.pswp--touch .pswp__button--arrow{visibility:hidden}.pswp--has_mouse .pswp__button--arrow{visibility:visible}.pswp__button--arrow--prev{right:auto;left:0}.pswp__button--arrow--next{right:0}.pswp__button--arrow--next .pswp__icn{left:auto;right:14px;transform:scaleX(-1)}.pswp__button--zoom{display:none}.pswp--zoom-allowed .pswp__button--zoom{display:block}.pswp--zoomed-in .pswp__zoom-icn-bar-v{display:none}.pswp__preloader{position:relative;overflow:hidden;width:50px;height:60px;margin-right:auto}.pswp__preloader .pswp__icn{opacity:0;transition:opacity .2s linear;animation:pswp-clockwise .6s linear infinite}.pswp__preloader--active .pswp__icn{opacity:.85}@keyframes pswp-clockwise{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pswp__counter{height:30px;margin-top:15px;margin-inline-start:20px;font-size:14px;line-height:30px;color:var(--pswp-icon-color);text-shadow:1px 1px 3px var(--pswp-icon-color-secondary);opacity:.85}.pswp--one-slide .pswp__counter{display:none}.photo-swipe-loading{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center}.photo-swipe-bullets-indicator{position:absolute;bottom:30px;left:50%;display:flex;flex-direction:row;align-items:center;transform:translate(-50%)}.photo-swipe-bullet{width:12px;height:6px;margin:0 5px;border-radius:3px;background:var(--photo-swipe-c-bullet);transition:width .3s,color .3s}.photo-swipe-bullet.active{width:30px;background:var(--photo-swipe-c-bullet-active)}:root{--search-bg-color: #ffffff;--search-accent-color: #3eaf7c;--search-text-color: #2c3e50;--search-border-color: #eaecef;--search-item-text-color: #5d81a5;--search-item-focus-bg-color: #f3f4f5;--search-input-width: 8rem;--search-result-width: 20rem}.search-box{display:inline-block;position:relative;margin-left:1rem}@media print{.search-box{display:none}}.search-box input{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:text;width:var(--search-input-width);height:2rem;color:var(--search-text-color);display:inline-block;border:1px solid var(--search-border-color);border-radius:2rem;font-size:.9rem;line-height:2rem;padding:0 .5rem 0 2rem;outline:none;transition:all ease .3s;background:var(--search-bg-color) url("data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'?%3e%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='12'%20height='13'%3e%3cg%20stroke-width='2'%20stroke='%23aaa'%20fill='none'%3e%3cpath%20d='M11.29%2011.71l-4-4'/%3e%3ccircle%20cx='5'%20cy='5'%20r='4'/%3e%3c/g%3e%3c/svg%3e") .6rem .5rem no-repeat;background-size:1rem}@media (max-width: 719px){.search-box input{cursor:pointer;width:0;border-color:transparent;position:relative}}.search-box input:focus{cursor:auto;border-color:var(--search-accent-color)}@media (max-width: 719px){.search-box input:focus{cursor:text;left:0;width:10rem}}@media (max-width: 419px){.search-box input:focus{width:8rem}}.search-box .suggestions{background:var(--search-bg-color);width:var(--search-result-width);position:absolute;top:2rem;right:0;border:1px solid var(--search-border-color);border-radius:6px;padding:.4rem;list-style-type:none}@media (max-width: 419px){.search-box .suggestions{width:calc(100vw - 4rem);right:-.5rem}}.search-box .suggestion{line-height:1.4;padding:.4rem .6rem;border-radius:4px;cursor:pointer}.search-box .suggestion a{white-space:normal;color:var(--search-item-text-color)}.search-box .suggestion.focus{background-color:var(--search-item-focus-bg-color)}.search-box .suggestion.focus a{color:var(--search-accent-color)}.search-box .suggestion .page-title{font-weight:600}.search-box .suggestion .page-header{font-size:.9em;margin-left:.25em}.redirect-modal-fade-enter-active,.redirect-modal-fade-leave-active{transition:opacity .5s}.redirect-modal-fade-enter,.redirect-modal-fade-leave-to{opacity:0}.redirect-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--redirect-z-index);display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media print{.redirect-modal-mask{display:none}}.redirect-modal-wrapper{position:relative;z-index:1500;overflow:hidden;max-width:80vw;padding:1rem 2rem;border-radius:8px;background:var(--redirect-c-bg);box-shadow:0 2px 6px 0 var(--redirect-c-shadow)}.redirect-modal-hint{margin-top:.5rem;color:var(--vp-c-text-mute);font-size:14px;text-align:start}.redirect-modal-hint input[type=checkbox]{position:relative;vertical-align:text-bottom;height:1em;margin-inline-end:18px;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none}.redirect-modal-hint input[type=checkbox]:after{content:" ";position:absolute;top:0;display:inline-block;box-sizing:border-box;width:14px;height:14px;padding-inline-start:0;border:1px solid var(--vp-c-border);border-radius:50%;background:var(--vp-c-control);text-align:center;visibility:visible}.redirect-modal-hint input[type=checkbox]:checked:after{content:"";border-color:var(--vp-c-accent-bg);background:var(--vp-c-accent-bg)}.redirect-modal-hint input[type=checkbox]:checked:before{content:"";position:absolute;inset-inline-start:5px;top:2px;z-index:1;width:2px;height:6px;border:solid var(--vp-c-white);border-width:0 2px 2px 0;transform:rotate(45deg)}.redirect-modal-hint label{display:inline-block}.redirect-modal-action{display:block;width:100%;margin:1rem 0;padding:.5rem .75rem;border:none;border-radius:8px;background-color:var(--redirect-c-control);color:inherit;cursor:pointer}.redirect-modal-action:hover{background-color:var(--redirect-c-control-hover)}.redirect-modal-action.primary{background-color:var(--redirect-c-accent-bg);color:var(--redirect-c-accent-text)}.redirect-modal-action.primary:hover{background-color:var(--redirect-c-accent-hover)}:root{--redirect-z-index: 1499;--redirect-c-bg: var(--vp-c-bg-elv);--redirect-c-text: var(--vp-c-text);--redirect-c-accent-bg: var(--vp-c-accent-bg);--redirect-c-accent-hover: var(--vp-c-accent-hover);--redirect-c-accent-text: var(--vp-c-accent-text);--redirect-c-control: var(--vp-c-control);--redirect-c-control-hover: var(--vp-c-control-hover);--redirect-c-shadow: var(--vp-c-shadow)}.vp-footer-wrapper{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-evenly;padding-block:.75rem;padding-inline:calc(var(--sidebar-space) + 2rem) 2rem;border-top:1px solid var(--vp-c-border);background:var(--vp-c-bg);color:var(--vp-c-text-mute);text-align:center;transition:border-top-color var(--vp-t-color),background var(--vp-t-color),padding var(--vp-t-transform)}@media (max-width: 719px){.vp-footer-wrapper{padding-inline-start:2rem}}@media (min-width: 1440px){.vp-footer-wrapper{z-index:50;padding-inline-start:2rem}}@media print{.vp-footer-wrapper{margin:0!important;padding:0!important}}@media (max-width: 419px){.vp-footer-wrapper{display:block}}.no-sidebar .vp-footer-wrapper,.sidebar-collapsed .vp-footer-wrapper{padding-inline-start:2rem}.vp-footer{margin:.5rem 1rem;font-size:14px}@media print{.vp-footer{display:none}}.vp-copyright{margin:6px 0;font-size:13px}.vp-page:not(.not-found)+.vp-footer-wrapper{margin-top:-2rem}.vp-dropdown-wrapper{cursor:pointer}.vp-dropdown-wrapper:not(:hover) .arrow{transform:rotate(-180deg)}.vp-dropdown-wrapper .auto-link{position:relative;display:block;margin-bottom:0;border-bottom:none;color:var(--vp-c-text);font-weight:400;font-size:.875rem;line-height:1.7rem;transition:color var(--vp-t-color)}.vp-dropdown-wrapper .auto-link:hover,.vp-dropdown-wrapper .auto-link.route-link-active{color:var(--vp-c-accent)}.vp-dropdown-title{border-width:0;background:transparent;cursor:pointer;padding:0 .25rem;color:var(--vp-c-text);font-weight:500;font-size:inherit;font-family:inherit;line-height:inherit;cursor:inherit}.vp-dropdown-title:hover{border-color:transparent}.vp-dropdown-title .icon{margin-inline-end:.25em;font-size:1em}.vp-dropdown-title .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s;font-size:1.2em}[data-theme=dark] .vp-dropdown-title .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-dropdown-title .arrow.down{transform:rotate(180deg)}[dir=rtl] .vp-dropdown-title .arrow.down{transform:rotate(-180deg)}.vp-dropdown-title .arrow.end{transform:rotate(90deg)}[dir=rtl] .vp-dropdown-title .arrow.end,.vp-dropdown-title .arrow.start{transform:rotate(-90deg)}[dir=rtl] .vp-dropdown-title .arrow.start{transform:rotate(90deg)}.vp-dropdown{position:absolute;inset-inline-end:0;top:100%;overflow-y:auto;box-sizing:border-box;min-width:6rem;max-height:calc(100vh - var(--navbar-height));margin:0;padding:.5rem .75rem;border:1px solid var(--vp-c-gutter);border-radius:.5rem;background:var(--vp-c-bg-elv);box-shadow:2px 2px 10px var(--vp-c-shadow);list-style-type:none;text-align:start;white-space:nowrap;opacity:0;visibility:hidden;transition:all .18s ease-out;transform:scale(.9)}.vp-dropdown-wrapper:hover .vp-dropdown,.vp-dropdown-wrapper.open .vp-dropdown{z-index:2;opacity:1;visibility:visible;transform:none}.vp-dropdown-item{color:inherit;line-height:1.7rem}.vp-dropdown-subtitle{margin:0;padding:.5rem .25rem 0;color:var(--vp-c-text-mute);font-weight:600;font-size:.75rem;line-height:2;text-transform:uppercase}.vp-dropdown-item:first-child .vp-dropdown-subtitle{padding-top:0}.vp-dropdown-subitems{margin:0;padding:0 0 .25rem;list-style-type:none}.vp-dropdown-item:last-child .vp-dropdown-subitems{padding-bottom:0}.vp-nav-screen-menu-title{border-width:0;background:transparent;position:relative;display:flex;align-items:center;width:100%;padding:0;color:var(--vp-c-text);font-size:inherit;font-family:inherit;text-align:start;cursor:pointer}.vp-nav-screen-menu-title:hover,.vp-nav-screen-menu-title.active{color:var(--vp-c-text)}.vp-nav-screen-menu-title .text{flex:1}.vp-nav-screen-menu-title .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}[data-theme=dark] .vp-nav-screen-menu-title .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-nav-screen-menu-title .arrow.down{transform:rotate(180deg)}[dir=rtl] .vp-nav-screen-menu-title .arrow.down{transform:rotate(-180deg)}.vp-nav-screen-menu-title .arrow.end{transform:rotate(90deg)}[dir=rtl] .vp-nav-screen-menu-title .arrow.end,.vp-nav-screen-menu-title .arrow.start{transform:rotate(-90deg)}[dir=rtl] .vp-nav-screen-menu-title .arrow.start{transform:rotate(90deg)}.vp-nav-screen-menu{overflow:hidden;margin:.5rem 0 0;padding:0;list-style:none;transition:transform .1s ease-out;transform:scaleY(1);transform-origin:top}.vp-nav-screen-menu.hide{height:0;margin:0;transform:scaleY(0)}.vp-nav-screen-menu .auto-link{position:relative;display:block;padding-inline-start:.5rem;font-weight:400;line-height:2}.vp-nav-screen-menu .auto-link:hover,.vp-nav-screen-menu .auto-link.route-link-active{color:var(--vp-c-accent)}.vp-nav-screen-menu .auto-link .icon{font-size:1em}.vp-nav-screen-menu-item{color:inherit;line-height:1.7rem}.vp-nav-screen-menu-subtitle{margin:0;padding-inline-start:.25rem;color:var(--vp-c-text-mute);font-weight:600;font-size:.75rem;line-height:2;text-transform:uppercase}.vp-nav-screen-menu-subtitle .auto-link{padding:0}.vp-nav-screen-menu-subitems{margin:0;padding:0;list-style:none}.vp-nav-screen-menu-subitem{padding-inline-start:.5rem;font-size:.9em}.nav-screen-links{display:none;padding-bottom:.75rem}@media (max-width: 719px){.nav-screen-links{display:block}}.nav-screen-links .navbar-links-item{position:relative;display:block;padding:12px 4px 11px 0;border-bottom:1px solid var(--vp-c-border);font-size:16px;line-height:1.5rem}.nav-screen-links .auto-link{display:inline-block;width:100%;color:var(--vp-c-text);font-weight:400}.nav-screen-links .auto-link:hover{color:var(--vp-c-text)}.nav-screen-links .auto-link.route-link-active{color:var(--vp-c-accent)}.vp-color-mode-switch{border-width:0;background:transparent;vertical-align:middle;padding:6px;color:var(--vp-c-text-mute);cursor:pointer;transition:color var(--vp-t-color)}.vp-color-mode-switch:hover{color:var(--vp-c-accent-bg)}.vp-color-mode-switch .icon{width:1.25rem;height:1.25rem}.vp-color-mode-title{display:block;margin:0;padding:0 .25rem;color:var(--vp-c-text-subtle);font-weight:600;font-size:.75rem;line-height:2}.vp-theme-color-picker{display:flex;margin:0;padding:0;list-style-type:none;font-size:14px}.vp-theme-color-picker li span{display:inline-block;vertical-align:middle;width:15px;height:15px;margin:0 2px;border-radius:2px}.vp-theme-color-picker li span.theme-color{background:#3ca978}.vp-theme-color-picker li span.theme-color [data-theme=dark]{background:#3fb37f}:root.theme-1{--vp-c-accent: rgb(9.1538461538, 98.4038461538, 169.3461538462);--vp-c-accent-bg: rgb(11.7692307692, 126.5192307692, 217.7307692308);--vp-c-accent-hover: rgb(13.0769230769, 140.5769230769, 241.9230769231);--vp-c-accent-soft: rgba(20.4, 111.3257142857, 183.6, .15)}[data-theme=dark].theme-1{--vp-c-accent: rgb(37.2692307692, 152.0192307692, 243.2307692308);--vp-c-accent-bg: rgb(12.4230769231, 133.5480769231, 229.8269230769);--vp-c-accent-hover: rgb(11.7692307692, 126.5192307692, 217.7307692308)}:root.theme-2{--vp-c-accent: rgb(163.9056603774, 14.5943396226, 14.5943396226);--vp-c-accent-bg: rgb(210.7358490566, 18.7641509434, 18.7641509434);--vp-c-accent-hover: rgb(234.1509433962, 20.8490566038, 20.8490566038);--vp-c-accent-soft: rgba(183.6, 20.4, 20.4, .15)}[data-theme=dark].theme-2{--vp-c-accent: rgb(236.2358490566, 44.2641509434, 44.2641509434);--vp-c-accent-bg: rgb(222.4433962264, 19.8066037736, 19.8066037736);--vp-c-accent-hover: rgb(210.7358490566, 18.7641509434, 18.7641509434)}:root.theme-3{--vp-c-accent: rgb(46.6962025316, 131.8037974684, 93.3924050633);--vp-c-accent-bg: rgb(60.0379746835, 169.4620253165, 120.0759493671);--vp-c-accent-hover: rgb(66.7088607595, 188.2911392405, 133.417721519);--vp-c-accent-soft: rgba(20.4, 183.6, 109.9433628319, .15)}[data-theme=dark].theme-3{--vp-c-accent: rgb(85.5379746835, 194.9620253165, 145.5759493671);--vp-c-accent-bg: rgb(63.3734177215, 178.8765822785, 126.746835443);--vp-c-accent-hover: rgb(60.0379746835, 169.4620253165, 120.0759493671)}:root.theme-4{--vp-c-accent: rgb(174.1463414634, 69.6585365854, 4.3536585366);--vp-c-accent-bg: rgb(223.9024390244, 89.5609756098, 5.5975609756);--vp-c-accent-hover: rgb(248.7804878049, 99.512195122, 6.2195121951);--vp-c-accent-soft: rgba(183.6, 83.1692307692, 20.4, .15)}[data-theme=dark].theme-4{--vp-c-accent: rgb(249.4024390244, 115.0609756098, 31.0975609756);--vp-c-accent-bg: rgb(236.3414634146, 94.5365853659, 5.9085365854);--vp-c-accent-hover: rgb(223.9024390244, 89.5609756098, 5.5975609756)}.vp-theme-color-title{display:block;margin:0;padding:0 .25rem;color:var(--vp-c-text-subtle);font-weight:600;font-size:.75rem;line-height:2}.full-screen,.cancel-full-screen{border-width:0;background:transparent;vertical-align:middle;padding:.25rem;color:var(--vp-c-text-mute);cursor:pointer}@media print{.full-screen,.cancel-full-screen{display:none}}.full-screen:hover,.cancel-full-screen:hover{color:var(--vp-c-accent-hover)}.full-screen .icon,.cancel-full-screen .icon{width:1.25rem;height:1.25rem}@media print{.full-screen-wrapper{display:none}}.full-screen-title{display:block;margin:0;padding:0 .25rem;color:var(--vp-c-text-subtle);font-weight:600;font-size:.75rem;line-height:2}.vp-nav-screen{position:fixed;inset:var(--navbar-height) 0 0 0;z-index:150;display:none;overflow-y:auto;padding:0 2rem;background:var(--vp-c-bg)}@media (max-width: 719px){.vp-nav-screen{display:block}}.vp-nav-screen.fade-enter-active,.vp-nav-screen.fade-leave-active{transition:opacity .25s}.vp-nav-screen.fade-enter-active .vp-nav-screen-container,.vp-nav-screen.fade-leave-active .vp-nav-screen-container{transition:transform .25s ease}.vp-nav-screen.fade-enter-from,.vp-nav-screen.fade-leave-to{opacity:0}.vp-nav-screen.fade-enter-from .vp-nav-screen-container,.vp-nav-screen.fade-leave-to .vp-nav-screen-container{transform:translateY(-8px)}.vp-nav-screen .icon{margin-inline-end:.25em;font-size:1em}.vp-nav-screen img.icon{vertical-align:-.125em;height:1em}.vp-nav-screen-container{max-width:320px;margin:0 auto;padding:2rem 0 4rem}.vp-outlook-wrapper{display:flex;justify-content:space-around}.vp-nav-logo{vertical-align:top;height:var(--navbar-line-height);margin-inline-end:.8rem}.vp-nav-logo.light{display:inline-block}.vp-nav-logo.dark,[data-theme=dark] .vp-nav-logo.light{display:none}[data-theme=dark] .vp-nav-logo.dark{display:inline-block}.vp-site-name{position:relative;color:var(--vp-c-text);font-size:1.25rem}@media (max-width: 719px){.vp-site-name{overflow:hidden;width:calc(100vw - 9.4rem);text-overflow:ellipsis;white-space:nowrap}}.vp-brand:hover .vp-site-name{color:var(--vp-c-accent)}.vp-nav-links{display:flex;align-items:center;font-size:.875rem}.vp-nav-item{position:relative;margin:0 .25rem;line-height:2rem}.vp-nav-item:first-child{margin-inline-start:0}.vp-nav-item:last-child{margin-inline-end:0}.vp-nav-item>.auto-link{color:var(--vp-c-text)}.vp-nav-item>.auto-link:before{content:" ";position:absolute;inset:auto 50% 0;height:2px;border-radius:1px;background:var(--vp-c-accent-hover);visibility:hidden;transition:inset .2s ease-in-out}.vp-nav-item>.auto-link.route-link-active{color:var(--vp-c-accent)}.vp-nav-item>.auto-link:hover:before,.vp-nav-item>.auto-link.route-link-active:before{inset:auto 0 0;visibility:visible}.vp-nav-item .i18n-icon{color:var(--vp-c-text-mute)}.vp-navbar .vp-action{margin:0!important}.vp-navbar .vp-action-link{display:inline-block;margin:auto;padding:6px;color:var(--vp-c-text-mute);line-height:1}.vp-navbar .vp-action-link:hover,.vp-navbar .vp-action-link:active{color:var(--vp-c-accent-bg)}.vp-toggle-navbar-button{border-width:0;background:transparent;cursor:pointer;position:relative;display:none;align-items:center;justify-content:center;padding:6px}@media screen and (max-width: 719px){.vp-toggle-navbar-button{display:flex}}.vp-toggle-navbar-button>span{position:relative;overflow:hidden;width:16px;height:14px}.vp-toggle-navbar-button .vp-top,.vp-toggle-navbar-button .vp-middle,.vp-toggle-navbar-button .vp-bottom{position:absolute;width:16px;height:2px;background:var(--vp-c-text);transition:top .25s,background .5s,transform .25s}.vp-toggle-navbar-button .vp-top{top:0;left:0;transform:translate(0)}.vp-toggle-navbar-button .vp-middle{top:6px;left:0;transform:translate(8px)}.vp-toggle-navbar-button .vp-bottom{top:12px;left:0;transform:translate(4px)}.vp-toggle-navbar-button:hover .vp-top{top:0;left:0;transform:translate(4px)}.vp-toggle-navbar-button:hover .vp-middle{top:6;left:0;transform:translate(0)}.vp-toggle-navbar-button:hover .vp-bottom{top:12px;left:0;transform:translate(8px)}.vp-toggle-navbar-button.is-active .vp-top{top:6px;transform:translate(0) rotate(225deg)}.vp-toggle-navbar-button.is-active .vp-middle{top:6px;transform:translate(16px)}.vp-toggle-navbar-button.is-active .vp-bottom{top:6px;transform:translate(0) rotate(135deg)}.vp-toggle-navbar-button.is-active:hover .vp-top,.vp-toggle-navbar-button.is-active:hover .vp-middle,.vp-toggle-navbar-button.is-active:hover .vp-bottom{background:var(--vp-c-accent-bg);transition:top .25s,background .25s,transform .25s}.vp-toggle-sidebar-button{border-width:0;background:transparent;cursor:pointer;display:none;vertical-align:middle;box-sizing:content-box;width:1rem;height:1rem;padding:.5rem;font:unset;transition:transform .2s ease-in-out}@media screen and (max-width: 719px){.vp-toggle-sidebar-button{display:block;padding-inline-end:var(--navbar-mobile-padding-x)}}.vp-toggle-sidebar-button:before,.vp-toggle-sidebar-button:after,.vp-toggle-sidebar-button .icon{display:block;width:100%;height:2px;border-radius:.05em;background:var(--vp-c-text);transition:transform .2s ease-in-out}.vp-toggle-sidebar-button:before{content:" ";margin-top:.125em}.sidebar-open .vp-toggle-sidebar-button:before{transform:translateY(.34rem) rotate(135deg)}.vp-toggle-sidebar-button:after{content:" ";margin-bottom:.125em}.sidebar-open .vp-toggle-sidebar-button:after{transform:translateY(-.34rem) rotate(-135deg)}.vp-toggle-sidebar-button .icon{margin:.2em 0}.sidebar-open .vp-toggle-sidebar-button .icon{transform:scale(0)}.vp-outlook-button{border-width:0;background:transparent;cursor:pointer;position:relative;padding:.375rem;color:var(--vp-c-text-mute)}.vp-outlook-button .icon{vertical-align:middle;width:1.25rem;height:1.25rem}.vp-outlook-dropdown{position:absolute;inset-inline-end:0;top:100%;overflow-y:auto;box-sizing:border-box;min-width:100px;margin:0;padding:.5rem .75rem;border:1px solid var(--vp-c-gutter);border-radius:.25rem;background:var(--vp-c-bg);box-shadow:2px 2px 10px var(--vp-c-shadow);text-align:start;white-space:nowrap;opacity:0;visibility:hidden;transition:all .18s ease-out;transform:scale(.8)}.vp-outlook-dropdown>*:not(:last-child){padding-bottom:.5rem;border-bottom:1px solid var(--vp-c-border)}.vp-outlook-button:hover .vp-outlook-dropdown,.vp-outlook-button.open .vp-outlook-dropdown{z-index:2;opacity:1;visibility:visible;transform:scale(1)}.vp-navbar{--navbar-line-height: calc( var(--navbar-height) - var(--navbar-padding-y) * 2 );position:fixed;inset:0 0 auto;z-index:175;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;height:var(--navbar-height);padding:var(--navbar-padding-y) var(--navbar-padding-x);background:var(--navbar-c-bg);box-shadow:0 2px 8px var(--vp-c-shadow);line-height:var(--navbar-line-height);white-space:nowrap;-webkit-backdrop-filter:saturate(150%) blur(12px);backdrop-filter:saturate(150%) blur(12px);transition:transform var(--vp-t-transform)}@media print{.vp-navbar{display:none}}.hide-navbar .vp-navbar.auto-hide{transform:translateY(-100%)}.vp-navbar .auto-link{padding:0 .25rem;color:var(--vp-c-text)}.vp-navbar .auto-link.route-link-active{color:var(--vp-c-accent)}.vp-navbar .auto-link .icon{margin-inline-end:.25em;font-size:1em}.vp-navbar .auto-link img.icon{vertical-align:-.125em;height:1em}.vp-navbar-start,.vp-navbar-end,.vp-navbar-center{display:flex;flex:1;align-items:center}.vp-navbar-start>*,.vp-navbar-end>*,.vp-navbar-center>*{position:relative;margin:0 .25rem!important}.vp-navbar-start>*:first-child,.vp-navbar-end>*:first-child,.vp-navbar-center>*:first-child{margin-inline-start:0!important}.vp-navbar-start>*:last-child,.vp-navbar-end>*:last-child,.vp-navbar-center>*:last-child{margin-inline-end:0!important}.vp-navbar-start{justify-content:start}.vp-navbar-center{justify-content:center}.vp-navbar-end{justify-content:end}.vp-sidebar-link{display:inline-block;box-sizing:border-box;width:calc(100% - 1rem);margin-inline:.5rem;padding:.25rem .5rem;border-radius:.375rem;color:var(--vp-c-text);font-weight:400;font-size:1em;line-height:1.5}.vp-sidebar-link:hover{background:var(--vp-c-control)}.vp-sidebar-link.active{background:var(--vp-c-accent-soft);color:var(--vp-c-accent);font-weight:500}.vp-sidebar-link.active .icon{color:var(--vp-c-accent)}.vp-sidebar-group:not(.collapsible) .vp-sidebar-header:not(.clickable){color:inherit;cursor:auto}.vp-sidebar-group .vp-sidebar-group .vp-sidebar-header{font-size:1em}.vp-sidebar-header{display:flex;align-items:center;overflow:hidden;box-sizing:border-box;width:calc(100% - 1rem);margin:0;margin-inline:.5rem;padding:.25rem .5rem;border-width:0;border-radius:.375rem;background:transparent;color:var(--vp-c-text);font-size:1.1em;line-height:1.5;text-align:start;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:color .15s ease;transform:rotate(0)}.vp-sidebar-header.open{color:inherit}.vp-sidebar-header.clickable{border-width:0;background:transparent;cursor:pointer;font-family:inherit}.vp-sidebar-header.clickable:hover{background:var(--vp-c-control)}.vp-sidebar-header.clickable.exact{border-inline-start-color:var(--vp-c-accent-bg);color:var(--vp-c-accent)}.vp-sidebar-header.clickable.exact a{color:inherit}.vp-sidebar-header .vp-sidebar-title{flex:1}.vp-sidebar-header .vp-arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}[data-theme=dark] .vp-sidebar-header .vp-arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-sidebar-header .vp-arrow.down{transform:rotate(180deg)}[dir=rtl] .vp-sidebar-header .vp-arrow.down{transform:rotate(-180deg)}.vp-sidebar-header .vp-arrow.end{transform:rotate(90deg)}[dir=rtl] .vp-sidebar-header .vp-arrow.end,.vp-sidebar-header .vp-arrow.start{transform:rotate(-90deg)}[dir=rtl] .vp-sidebar-header .vp-arrow.start{transform:rotate(90deg)}.vp-sidebar-header .vp-arrow{font-size:1.5em}.vp-sidebar-links{margin:0 0 0 .75em;padding:0}.vp-sidebar-links li{list-style-type:none}.vp-sidebar>.vp-sidebar-links{margin:0;padding:1.5rem 0}@media (max-width: 719px){.vp-sidebar>.vp-sidebar-links{padding:1rem 0}}.vp-sidebar>.vp-sidebar-links>li>.vp-sidebar-link{font-size:1.1em}.vp-sidebar>.vp-sidebar-links>li:not(:first-child){margin-top:.5rem}.vp-sidebar{position:fixed;inset-inline-start:0;top:0;bottom:0;z-index:1;overflow-y:auto;width:var(--sidebar-width);margin:0;padding-inline-start:calc(var(--sidebar-space) - var(--sidebar-width));background:var(--sidebar-c-bg);box-shadow:2px 0 8px var(--vp-c-shadow);font-size:.94rem;-webkit-backdrop-filter:saturate(150%) blur(12px);backdrop-filter:saturate(150%) blur(12px);transition:padding var(--vp-t-transform),transform var(--vp-t-transform);scrollbar-color:var(--vp-c-accent-bg) var(--vp-c-border);scrollbar-width:thin}@media (max-width: 959px){.vp-sidebar{font-size:.86em}}@media (max-width: 719px){.vp-sidebar{z-index:125;box-shadow:none;transform:translate(-100%)}[dir=rtl] .vp-sidebar{transform:translate(100%)}}@media (min-width: 1440px){.vp-sidebar{padding-bottom:3rem;box-shadow:none;font-size:1rem}}@media print{.vp-sidebar{display:none}}.vp-sidebar a{display:inline-block;color:var(--vp-c-text);font-weight:400}.vp-sidebar .icon{margin-inline-end:.25em;font-size:1em}.vp-sidebar img.icon{vertical-align:-.125em;height:1em}.vp-sidebar-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:100;background:#00000026}.vp-sidebar-mask.fade-enter-active,.vp-sidebar-mask.fade-leave-active{transition:opacity .25s}.vp-sidebar-mask.fade-enter-from,.vp-sidebar-mask.fade-leave-to{opacity:0}.toggle-sidebar-wrapper{position:fixed;inset-inline-start:var(--sidebar-space);top:var(--navbar-height);bottom:0;z-index:100;display:flex;align-items:center;justify-content:center;font-size:2rem;transition:inset-inline-start var(--vp-t-transform)}@media (max-width: 719px){.toggle-sidebar-wrapper{display:none}}@media (min-width: 1440px){.toggle-sidebar-wrapper{display:none}}@media print{.toggle-sidebar-wrapper{display:none}}.toggle-sidebar-wrapper:hover{background:#7f7f7f0d;cursor:pointer}.toggle-sidebar-wrapper .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}[data-theme=dark] .toggle-sidebar-wrapper .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.toggle-sidebar-wrapper .arrow.down{transform:rotate(180deg)}[dir=rtl] .toggle-sidebar-wrapper .arrow.down{transform:rotate(-180deg)}.toggle-sidebar-wrapper .arrow.end{transform:rotate(90deg)}[dir=rtl] .toggle-sidebar-wrapper .arrow.end,.toggle-sidebar-wrapper .arrow.start{transform:rotate(-90deg)}[dir=rtl] .toggle-sidebar-wrapper .arrow.start{transform:rotate(90deg)}.theme-container{display:flex;flex-flow:column;justify-content:space-between;min-height:100vh}.theme-container .vp-page{padding-top:var(--navbar-height);padding-inline-start:calc(var(--sidebar-space) + 2rem)}@media (max-width: 719px){.theme-container .vp-page{padding-inline:0}}@media (min-width: 1440px){.theme-container .vp-page{padding-inline-end:calc(100vw - var(--content-width) - var(--sidebar-space) - 6rem)}}.theme-container .vp-sidebar{top:var(--navbar-height)}.theme-container.no-navbar .vp-page{padding-top:0}.theme-container.no-navbar .vp-sidebar{top:0}@media (max-width: 719px){.theme-container.no-navbar .vp-sidebar{top:0}}@media (max-width: 719px){.theme-container.hide-navbar .vp-sidebar{top:0}}.theme-container.sidebar-collapsed .vp-page{padding-inline-start:0}.theme-container.sidebar-collapsed .vp-sidebar{box-shadow:none;transform:translate(-100%)}[dir=rtl] .theme-container.sidebar-collapsed .vp-sidebar{transform:translate(100%)}.theme-container.sidebar-collapsed .toggle-sidebar-wrapper{inset-inline-start:0}.theme-container.no-sidebar .vp-page{padding-inline:0}@media (min-width: 1440px){.theme-container.no-sidebar.has-toc .vp-page{padding-inline-end:16rem}}.theme-container.no-sidebar .vp-toggle-sidebar-button,.theme-container.no-sidebar .toggle-sidebar-wrapper,.theme-container.no-sidebar .vp-sidebar{display:none}.theme-container.sidebar-open .vp-sidebar{box-shadow:2px 0 8px var(--vp-c-shadow);transform:translate(0)}.vp-feature-wrapper{position:relative}.vp-feature-bg{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-attachment:fixed;background-position:50%;background-size:cover}.vp-feature-bg.light{display:inline-block}.vp-feature-bg.dark,[data-theme=dark] .vp-feature-bg.light{display:none}[data-theme=dark] .vp-feature-bg.dark{display:inline-block}.vp-feature{position:relative;z-index:1;margin:0 auto;padding:1.5rem 1rem;color:var(--vp-c-text-mute);text-align:center}.vp-feature-bg+.vp-feature{color:#222}[data-theme=dark] .vp-feature-bg+.vp-feature{color:#eee}.vp-feature-bg+.vp-feature .icon{color:inherit}.vp-feature-image{height:10rem;margin:0 auto}@media (max-width: 959px){.vp-feature-image{height:8rem}}.vp-feature-image.light{display:inline-block}.vp-feature-image.dark,[data-theme=dark] .vp-feature-image.light{display:none}[data-theme=dark] .vp-feature-image.dark{display:inline-block}.vp-feature-header{margin-bottom:1.5rem;border-bottom:none;font-size:3rem;font-family:var(--vp-font);text-align:center}@media (max-width: 959px){.vp-feature-header{font-size:2.5rem}}@media (max-width: 719px){.vp-feature-header{font-size:2.25rem}}@media (max-width: 419px){.vp-feature-header{font-size:2rem}}.vp-feature-description{font-size:1.125rem}.vp-features{z-index:1;display:flex;flex-wrap:wrap;place-content:stretch center;align-items:stretch;margin:1rem 0;text-align:start}@media print{.vp-features{display:block}}.vp-features:first-child{border-top:1px solid var(--vp-c-border)}.vp-feature-item{position:relative;display:block;flex-basis:calc(33% - 3rem);margin:.5rem;padding:1rem;border-radius:.5rem;color:inherit;transition:background var(--vp-t-color),box-shadow var(--vp-t-transform),transform var(--vp-t-transform)}@media (min-width: 1440px){.vp-feature-item{flex-basis:calc(25% - 3rem)}}@media (max-width: 959px){.vp-feature-item{flex-basis:calc(50% - 3rem)}}@media (max-width: 719px){.vp-feature-item{flex-basis:100%;font-size:.95rem}}@media (max-width: 419px){.vp-feature-item{margin:.5rem 0;font-size:.9rem}}.vp-feature-item.link{cursor:pointer;transition:transform var(--vp-t-transform)}@media print{.vp-feature-item.link{text-decoration:none}}.vp-feature-item.link:hover{background-color:var(--vp-c-grey-soft)}.vp-feature-bg+.vp-feature .vp-feature-item.link:hover{background-color:transparent;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.vp-feature-item.link:hover:before{content:"➜";display:block;float:right}.vp-feature-item.link:active{transform:scale(.96)}.vp-feature-item .icon{display:inline-block;height:1.1em;margin-inline-end:.5rem;color:var(--vp-c-accent);font-weight:400;font-size:1.1em}.vp-feature-item:only-child{flex-basis:100%}.vp-feature-item:first-child:nth-last-child(2),.vp-feature-item:nth-child(2):last-child{flex-basis:calc(50% - 3rem)}@media (max-width: 719px){.vp-feature-item:first-child:nth-last-child(2),.vp-feature-item:nth-child(2):last-child{flex-basis:100%}}.vp-feature-title{margin:.25rem 0 .5rem;font-weight:700;font-size:1.3rem;font-family:var(--vp-font)}@media (max-width: 419px){.vp-feature-title{font-size:1.2rem}}.vp-feature-details{margin:0;line-height:1.4}.fade-slide-y-enter-active{transition:all .3s ease!important}.fade-slide-y-leave-active{transition:all .3s cubic-bezier(1,.5,.8,1)!important}.fade-slide-y-enter-from,.fade-slide-y-leave-to{opacity:0;transform:translateY(10px)}.vp-hero-info-wrapper{position:relative;display:flex;align-items:center;justify-content:center;margin-inline:auto}.vp-hero-info-wrapper.fullscreen{height:calc(100vh - var(--navbar-height))!important}.vp-hero-info{z-index:1;width:100%;padding-inline:2.5rem}@media (max-width: 959px){.vp-hero-info{padding-inline:1.5rem}}@media (min-width: 959px){.vp-hero-info{display:flex;align-items:center;justify-content:space-evenly}}.vp-hero-mask{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-position:50%;background-size:cover}.vp-hero-mask:after{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:block}.vp-hero-mask.light{display:block}[data-theme=dark] .vp-hero-mask.light,.vp-hero-mask.dark{display:none}[data-theme=dark] .vp-hero-mask.dark{display:block}.vp-hero-infos{z-index:1;margin:0 .5rem}.vp-hero-image{display:block;max-width:100%;max-height:18rem;margin:1rem}@media (max-width: 959px){.vp-hero-image{margin:2rem auto}}@media (max-width: 719px){.vp-hero-image{max-height:16rem;margin:1.5rem auto}}@media (max-width: 419px){.vp-hero-image{max-height:14rem}}.vp-hero-image.light{display:block}[data-theme=dark] .vp-hero-image.light,.vp-hero-image.dark{display:none}[data-theme=dark] .vp-hero-image.dark{display:block}.vp-hero-title{margin:.5rem 0;background:linear-gradient(120deg,var(--vp-c-accent-hover),var(--vp-c-accent) 30%,rgb(46.6962025316,85.1075949367,131.8037974684) 100%);-webkit-background-clip:text;background-clip:text;font-weight:700;font-size:3.6rem;font-family:var(--vp-font);line-height:1.5;-webkit-text-fill-color:transparent}@media (max-width: 719px){.vp-hero-title{margin:0}}@media (max-width: 959px){.vp-hero-title{font-size:2.5rem;text-align:center}}@media (max-width: 719px){.vp-hero-title{font-size:2.25rem;text-align:center}}@media (max-width: 419px){.vp-hero-title{margin:0 auto;font-size:2rem}}.vp-hero-title [data-theme=dark]{background:linear-gradient(120deg,var(--vp-c-accent-hover),var(--vp-c-accent) 30%,rgb(85.5379746835,134.9240506329,194.9620253165) 100%)}#main-description,.vp-hero-actions{margin:1.8rem 0}@media (max-width: 719px){#main-description,.vp-hero-actions{margin:1.5rem 0}}@media (max-width: 959px){#main-description,.vp-hero-actions{margin:1.5rem auto;text-align:center}}@media (max-width: 419px){#main-description,.vp-hero-actions{margin:1.2rem 0}}#main-description{max-width:35rem;color:var(--vp-c-text-mute);font-weight:500;font-size:1.6rem;line-height:1.3}@media (max-width: 719px){#main-description{font-size:1.4rem}}@media (max-width: 419px){#main-description{font-size:1.2rem}}.vp-hero-action{display:inline-block;overflow:hidden;min-width:4rem;margin:.5rem;padding:.5em 1.5rem;border-radius:2rem;background:var(--vp-c-control);color:var(--vp-c-text);font-size:1.2rem;text-align:center;transition:color var(--vp-t-color),color var(--vp-t-color),transform var(--vp-t-transform)}@media (max-width: 719px){.vp-hero-action{padding:.5rem 1rem;font-size:1.1rem}}@media (max-width: 419px){.vp-hero-action{font-size:1rem}}@media print{.vp-hero-action{text-decoration:none}}.vp-hero-action:hover{background:var(--vp-c-control-hover)}.vp-hero-action.primary{border-color:var(--vp-c-accent-bg);background:var(--vp-c-accent-bg);color:var(--vp-c-white)}.vp-hero-action.primary:hover{border-color:var(--vp-c-accent-hover);background:var(--vp-c-accent-hover)}.theme-container:not(.pure) .vp-hero-action:active{transform:scale(.96)}.vp-hero-action .icon{margin-inline-end:.25em}.vp-highlight-wrapper{position:relative;display:flex;align-items:center;justify-content:center}.vp-highlight-wrapper:nth-child(odd) .vp-highlight{flex-flow:row-reverse}.vp-highlight{z-index:1;display:flex;flex:1;align-items:center;justify-content:flex-end;max-width:var(--home-page-width);margin:0 auto;padding:1.5rem 2.5rem}@media (max-width: 719px){.vp-highlight{display:block;padding-inline:1.5rem;text-align:center}}.vp-highlight-bg{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;background-attachment:fixed;background-position:50%;background-size:cover}.vp-highlight-bg.light{display:inline-block}.vp-highlight-bg.dark,[data-theme=dark] .vp-highlight-bg.light{display:none}[data-theme=dark] .vp-highlight-bg.dark{display:inline-block}.vp-highlight-image{width:12rem;margin:2rem 4rem}@media (max-width: 959px){.vp-highlight-image{width:10rem}}@media (max-width: 719px){.vp-highlight-image{width:8rem;margin:0 auto}}.vp-highlight-image.light{display:inline-block}.vp-highlight-image.dark,[data-theme=dark] .vp-highlight-image.light{display:none}[data-theme=dark] .vp-highlight-image.dark{display:inline-block}.vp-highlight-info-wrapper{display:flex;flex:1;justify-content:center;padding:2rem}@media (max-width: 719px){.vp-highlight-info-wrapper{padding:1rem 0}}.vp-highlight-info-wrapper:only-child{flex:1 0 100%}.vp-highlight-info{text-align:start}.vp-highlight-header{margin-bottom:1.5rem;border-bottom:none;font-size:3rem;font-family:var(--vp-font)}@media (max-width: 959px){.vp-highlight-header{font-size:2.5rem}}@media (max-width: 719px){.vp-highlight-header{font-size:2.25rem;text-align:center}}@media (max-width: 419px){.vp-highlight-header{font-size:2rem}}.vp-highlight-description{font-size:1.125rem}.vp-highlights{margin-inline-start:-1.25em;padding-inline-start:0}.vp-highlights :not(pre)>code{color:var(--vp-c-text)}.vp-highlight-item-wrapper{position:relative;padding:.5em .5em .5em 1.75em;border-radius:.5rem;list-style:none}.vp-highlight-item-wrapper.link{cursor:pointer;transition:transform var(--vp-t-transform)}.vp-highlight-item-wrapper.link:hover{background-color:var(--vp-c-bg-alt)}.vp-highlight-bg+.vp-highlight .vp-highlight-item-wrapper.link:hover{background-color:transparent;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.vp-highlight-item-wrapper.link:hover:before{content:"➜";display:block;float:right}.vp-highlight-item-wrapper.link:active{transform:scale(.96)}.vp-highlight-item-wrapper::marker{font-weight:700}.vp-highlight-item{display:list-item;color:inherit;list-style:initial}@media print{.vp-highlight-item{text-decoration:none}}.vp-highlight-title{margin:0;font-weight:600;font-size:1.125rem;font-family:var(--vp-font)}.vp-highlight-title .icon{margin-inline-end:.25em;font-size:1em}.vp-highlight-title img.icon{vertical-align:-.125em;height:1em}.vp-highlight-details{margin:.5rem 0 0}.vp-project-home{--content-width: var(--home-page-width);display:block;flex:1;padding-top:var(--navbar-height)}@media screen{.vp-project-home .vp-hero-info-wrapper:not(.fullscreen) .vp-hero-info{max-width:var(--home-page-width)}}@media screen{.vp-project-home .vp-feature{max-width:var(--home-page-width)}}.vp-project-home .theme-hope-content{padding-bottom:1.5rem!important}.vp-project-home .theme-hope-content:empty{padding:0!important}.theme-hope-content:not(.custom)>*:first-child{margin-top:0}.vp-breadcrumb{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem}@media (max-width: 959px){.vp-breadcrumb{padding-inline:1.5rem}}@media print{.vp-breadcrumb{max-width:unset}}.vp-breadcrumb{position:relative;z-index:2;padding-top:1rem;font-size:15px}@media (max-width: 959px){.vp-breadcrumb{font-size:14px}}@media (max-width: 419px){.vp-breadcrumb{padding-top:.5rem;font-size:12.8px}}@media print{.vp-breadcrumb{display:none}}.vp-breadcrumb .icon{margin-inline-end:.25em;font-size:1em}.vp-breadcrumb img.icon{vertical-align:-.125em;height:1em}.vp-breadcrumb a{display:inline-block;padding:0 .5em}.vp-breadcrumb a:before{position:relative;bottom:.125rem;margin-inline-end:.25em}.vp-breadcrumb a:hover{color:var(--vp-c-accent)}.vp-breadcrumb ol{margin:0;padding-inline-start:0;list-style:none}.vp-breadcrumb li{display:inline-block;line-height:1.5}.vp-breadcrumb li:first-child a{padding-inline-start:0}.vp-breadcrumb li:last-child a{padding-inline-end:0}.vp-breadcrumb li.is-active a{color:var(--vp-c-text-mute);cursor:default;pointer-events:none}.vp-breadcrumb li+li:before{content:"/";color:var(--vp-c-text-mute)}.vp-page-nav{display:flex;flex-wrap:wrap;max-width:var(--content-width, 740px);min-height:2rem;margin-inline:auto;margin-top:0;padding-block:.5rem;padding-inline:2rem;border-top:1px solid var(--vp-c-border)}@media (max-width: 959px){.vp-page-nav{padding-inline:1rem}}@media print{.vp-page-nav{display:none}}.vp-page-nav .auto-link{display:inline-block;flex-grow:1;margin:.25rem;padding:.25rem .5rem;border:1px solid var(--vp-c-border);border-radius:.25rem}.vp-page-nav .auto-link:hover{background:var(--vp-c-control)}.vp-page-nav .auto-link .hint{color:var(--vp-c-text-mute);font-size:.875rem;line-height:2}.vp-page-nav .auto-link .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s;font-size:.75rem}[data-theme=dark] .vp-page-nav .auto-link .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-page-nav .auto-link .arrow.down{transform:rotate(180deg)}[dir=rtl] .vp-page-nav .auto-link .arrow.down{transform:rotate(-180deg)}.vp-page-nav .auto-link .arrow.end{transform:rotate(90deg)}[dir=rtl] .vp-page-nav .auto-link .arrow.end,.vp-page-nav .auto-link .arrow.start{transform:rotate(-90deg)}[dir=rtl] .vp-page-nav .auto-link .arrow.start{transform:rotate(90deg)}.vp-page-nav .prev{text-align:start}.vp-page-nav .prev .icon{margin-inline-end:.25em;font-size:1em}.vp-page-nav .prev img.icon{vertical-align:-.125em;height:1em}.vp-page-nav .next{text-align:end}.vp-page-nav .next .icon{margin-inline-start:.25em;font-size:1em}.vp-page-nav .next img.icon{vertical-align:-.125em;height:1em}.page-author-item{display:inline-block;margin:0 4px;font-weight:400;overflow-wrap:break-word}.page-category-info{flex-wrap:wrap}.page-category-item{display:inline-block;margin:.125em .25em;padding:0 .25em;border-radius:.25em;background:var(--vp-c-grey-soft);color:var(--vp-c-text-mute);font-weight:700;font-size:.75rem;line-height:2;transition:background var(--vp-t-color),color var(--vp-t-color)}@media print{.page-category-item{padding:0;font-weight:400}.page-category-item:after{content:", "}.page-category-item:last-of-type:after{content:""}}.page-category-item.clickable{cursor:pointer}.page-category-item.clickable:not([class*=color]):hover{color:var(--vp-c-accent-hover)}.page-category-item.color0{background:#fde5e7;color:#ec2f3e}[data-theme=dark] .page-category-item.color0{background:#340509;color:#ba111f}.page-category-item.color0.clickable:hover{background:#f9bec3}[data-theme=dark] .page-category-item.color0.clickable:hover{background:#53080e}.page-category-item.color1{background:#ffeee8;color:#fb7649}[data-theme=dark] .page-category-item.color1{background:#441201;color:#f54205}.page-category-item.color1.clickable:hover{background:#fed4c6}[data-theme=dark] .page-category-item.color1.clickable:hover{background:#6d1d02}.page-category-item.color2{background:#fef5e7;color:#f5b041}[data-theme=dark] .page-category-item.color2{background:#3e2703;color:#e08e0b}.page-category-item.color2.clickable:hover{background:#fce6c4}[data-theme=dark] .page-category-item.color2.clickable:hover{background:#633f05}.page-category-item.color3{background:#eafaf1;color:#55d98d}[data-theme=dark] .page-category-item.color3{background:#0c331c;color:#29b866}.page-category-item.color3.clickable:hover{background:#caf3db}[data-theme=dark] .page-category-item.color3.clickable:hover{background:#12522d}.page-category-item.color4{background:#e6f9ee;color:#36d278}[data-theme=dark] .page-category-item.color4{background:#092917;color:#219552}.page-category-item.color4.clickable:hover{background:#c0f1d5}[data-theme=dark] .page-category-item.color4.clickable:hover{background:#0f4224}.page-category-item.color5{background:#e1fcfc;color:#16e1e1}[data-theme=dark] .page-category-item.color5{background:#042929;color:#0e9595}.page-category-item.color5.clickable:hover{background:#b4f8f8}[data-theme=dark] .page-category-item.color5.clickable:hover{background:#064242}.page-category-item.color6{background:#e4f0fe;color:#2589f6}[data-theme=dark] .page-category-item.color6{background:#021b36;color:#0862c3}.page-category-item.color6.clickable:hover{background:#bbdafc}[data-theme=dark] .page-category-item.color6.clickable:hover{background:#042c57}.page-category-item.color7{background:#f7f1fd;color:#bb8ced}[data-theme=dark] .page-category-item.color7{background:#2a0b4b;color:#9851e4}.page-category-item.color7.clickable:hover{background:#eadbfa}[data-theme=dark] .page-category-item.color7.clickable:hover{background:#431277}.page-category-item.color8{background:#fdeaf5;color:#ef59ab}[data-theme=dark] .page-category-item.color8{background:#400626;color:#e81689}.page-category-item.color8.clickable:hover{background:#facbe5}[data-theme=dark] .page-category-item.color8.clickable:hover{background:#670a3d}.page-original-info{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;padding:0 .5em;border:.5px solid var(--vp-c-border-hard);border-radius:.75em;background:var(--vp-c-bg);font-size:.75em;line-height:1.5!important}.page-tag-info{flex-wrap:wrap}.page-tag-item{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;min-width:1.5rem;margin:.125rem;padding:.125rem .25rem .125rem .625rem;background:var(--vp-c-grey-soft);background:linear-gradient(135deg,transparent .75em,var(--vp-c-grey-soft) 0) top,linear-gradient(45deg,transparent .75em,var(--vp-c-grey-soft) 0) bottom;background-size:100% 52%!important;background-repeat:no-repeat!important;color:var(--vp-c-text-mute);font-weight:700;font-size:.625rem;line-height:1.5;text-align:center;transition:background var(--vp-t-color),color var(--vp-t-color)}@media print{.page-tag-item{padding:0;font-weight:400}.page-tag-item:after{content:", "}.page-tag-item:last-of-type:after{content:""}}.page-tag-item.clickable{cursor:pointer}.page-tag-item.clickable:not([class*=color]):hover{color:var(--vp-c-accent)}.page-tag-item.color0{background:#fde5e7;background:linear-gradient(135deg,transparent .75em,rgb(252.6123893805,228.9876106195,230.8725663717) 0) top,linear-gradient(45deg,transparent .75em,rgb(252.6123893805,228.9876106195,230.8725663717) 0) bottom;color:#ec2f3e}[data-theme=dark] .page-tag-item.color0{background:#340509;background:linear-gradient(135deg,transparent .75em,rgb(51.75,4.75,8.5) 0) top,linear-gradient(45deg,transparent .75em,rgb(51.75,4.75,8.5) 0) bottom;color:#ba111f}.page-tag-item.color0.clickable:hover{background:#f9bec3;background:linear-gradient(135deg,transparent .75em,rgb(249.0309734513,189.9690265487,194.6814159292) 0) top,linear-gradient(45deg,transparent .75em,rgb(249.0309734513,189.9690265487,194.6814159292) 0) bottom}[data-theme=dark] .page-tag-item.color0.clickable:hover{background:#53080e;background:linear-gradient(135deg,transparent .75em,rgb(82.8,7.6,13.6) 0) top,linear-gradient(45deg,transparent .75em,rgb(82.8,7.6,13.6) 0) bottom}.page-tag-item.color1{background:#ffeee8;background:linear-gradient(135deg,transparent .75em,rgb(254.5,237.9,232.3) 0) top,linear-gradient(45deg,transparent .75em,rgb(254.5,237.9,232.3) 0) bottom;color:#fb7649}[data-theme=dark] .page-tag-item.color1{background:#441201;background:linear-gradient(135deg,transparent .75em,rgb(68.0021551724,18.2737068966,1.4978448276) 0) top,linear-gradient(45deg,transparent .75em,rgb(68.0021551724,18.2737068966,1.4978448276) 0) bottom;color:#f54205}.page-tag-item.color1.clickable:hover{background:#fed4c6;background:linear-gradient(135deg,transparent .75em,rgb(253.75,212.25,198.25) 0) top,linear-gradient(45deg,transparent .75em,rgb(253.75,212.25,198.25) 0) bottom}[data-theme=dark] .page-tag-item.color1.clickable:hover{background:#6d1d02;background:linear-gradient(135deg,transparent .75em,rgb(108.8034482759,29.2379310345,2.3965517241) 0) top,linear-gradient(45deg,transparent .75em,rgb(108.8034482759,29.2379310345,2.3965517241) 0) bottom}.page-tag-item.color2{background:#fef5e7;background:linear-gradient(135deg,transparent .75em,rgb(253.8,245.1,231.3) 0) top,linear-gradient(45deg,transparent .75em,rgb(253.8,245.1,231.3) 0) bottom;color:#f5b041}[data-theme=dark] .page-tag-item.color2{background:#3e2703;background:linear-gradient(135deg,transparent .75em,rgb(62.1054216867,39.3072289157,3.1445783133) 0) top,linear-gradient(45deg,transparent .75em,rgb(62.1054216867,39.3072289157,3.1445783133) 0) bottom;color:#e08e0b}.page-tag-item.color2.clickable:hover{background:#fce6c4;background:linear-gradient(135deg,transparent .75em,rgb(252,230.25,195.75) 0) top,linear-gradient(45deg,transparent .75em,rgb(252,230.25,195.75) 0) bottom}[data-theme=dark] .page-tag-item.color2.clickable:hover{background:#633f05;background:linear-gradient(135deg,transparent .75em,rgb(99.3686746988,62.8915662651,5.0313253012) 0) top,linear-gradient(45deg,transparent .75em,rgb(99.3686746988,62.8915662651,5.0313253012) 0) bottom}.page-tag-item.color3{background:#eafaf1;background:linear-gradient(135deg,transparent .75em,rgb(233.784,250.216,240.752) 0) top,linear-gradient(45deg,transparent .75em,rgb(233.784,250.216,240.752) 0) bottom;color:#55d98d}[data-theme=dark] .page-tag-item.color3{background:#0c331c;background:linear-gradient(135deg,transparent .75em,rgb(11.5,51,28.25) 0) top,linear-gradient(45deg,transparent .75em,rgb(11.5,51,28.25) 0) bottom;color:#29b866}.page-tag-item.color3.clickable:hover{background:#caf3db;background:linear-gradient(135deg,transparent .75em,rgb(201.96,243.04,219.38) 0) top,linear-gradient(45deg,transparent .75em,rgb(201.96,243.04,219.38) 0) bottom}[data-theme=dark] .page-tag-item.color3.clickable:hover{background:#12522d;background:linear-gradient(135deg,transparent .75em,rgb(18.4,81.6,45.2) 0) top,linear-gradient(45deg,transparent .75em,rgb(18.4,81.6,45.2) 0) bottom}.page-tag-item.color4{background:#e6f9ee;background:linear-gradient(135deg,transparent .75em,rgb(229.8415841584,249.3584158416,238.0752475248) 0) top,linear-gradient(45deg,transparent .75em,rgb(229.8415841584,249.3584158416,238.0752475248) 0) bottom;color:#36d278}[data-theme=dark] .page-tag-item.color4{background:#092917;background:linear-gradient(135deg,transparent .75em,rgb(9.25,41.25,22.75) 0) top,linear-gradient(45deg,transparent .75em,rgb(9.25,41.25,22.75) 0) bottom;color:#219552}.page-tag-item.color4.clickable:hover{background:#c0f1d5;background:linear-gradient(135deg,transparent .75em,rgb(192.103960396,240.896039604,212.6881188119) 0) top,linear-gradient(45deg,transparent .75em,rgb(192.103960396,240.896039604,212.6881188119) 0) bottom}[data-theme=dark] .page-tag-item.color4.clickable:hover{background:#0f4224;background:linear-gradient(135deg,transparent .75em,rgb(14.8,66,36.4) 0) top,linear-gradient(45deg,transparent .75em,rgb(14.8,66,36.4) 0) bottom}.page-tag-item.color5{background:#e1fcfc;background:linear-gradient(135deg,transparent .75em,rgb(225.0082872928,252.0917127072,252.0917127072) 0) top,linear-gradient(45deg,transparent .75em,rgb(225.0082872928,252.0917127072,252.0917127072) 0) bottom;color:#16e1e1}[data-theme=dark] .page-tag-item.color5{background:#042929;background:linear-gradient(135deg,transparent .75em,rgb(4,41.25,41.25) 0) top,linear-gradient(45deg,transparent .75em,rgb(4,41.25,41.25) 0) bottom;color:#0e9595}.page-tag-item.color5.clickable:hover{background:#b4f8f8;background:linear-gradient(135deg,transparent .75em,rgb(180.020718232,247.729281768,247.729281768) 0) top,linear-gradient(45deg,transparent .75em,rgb(180.020718232,247.729281768,247.729281768) 0) bottom}[data-theme=dark] .page-tag-item.color5.clickable:hover{background:#064242;background:linear-gradient(135deg,transparent .75em,rgb(6.4,66,66) 0) top,linear-gradient(45deg,transparent .75em,rgb(6.4,66,66) 0) bottom}.page-tag-item.color6{background:#e4f0fe;background:linear-gradient(135deg,transparent .75em,rgb(227.7309734513,240.2973451327,253.8690265487) 0) top,linear-gradient(45deg,transparent .75em,rgb(227.7309734513,240.2973451327,253.8690265487) 0) bottom;color:#2589f6}[data-theme=dark] .page-tag-item.color6{background:#021b36;background:linear-gradient(135deg,transparent .75em,rgb(2.25,27.25,54.25) 0) top,linear-gradient(45deg,transparent .75em,rgb(2.25,27.25,54.25) 0) bottom;color:#0862c3}.page-tag-item.color6.clickable:hover{background:#bbdafc;background:linear-gradient(135deg,transparent .75em,rgb(186.8274336283,218.2433628319,252.1725663717) 0) top,linear-gradient(45deg,transparent .75em,rgb(186.8274336283,218.2433628319,252.1725663717) 0) bottom}[data-theme=dark] .page-tag-item.color6.clickable:hover{background:#042c57;background:linear-gradient(135deg,transparent .75em,rgb(3.6,43.6,86.8) 0) top,linear-gradient(45deg,transparent .75em,rgb(3.6,43.6,86.8) 0) bottom}.page-tag-item.color7{background:#f7f1fd;background:linear-gradient(135deg,transparent .75em,rgb(246.5,240.6,252.8) 0) top,linear-gradient(45deg,transparent .75em,rgb(246.5,240.6,252.8) 0) bottom;color:#bb8ced}[data-theme=dark] .page-tag-item.color7{background:#2a0b4b;background:linear-gradient(135deg,transparent .75em,rgb(41.9638554217,11.3975903614,74.6024096386) 0) top,linear-gradient(45deg,transparent .75em,rgb(41.9638554217,11.3975903614,74.6024096386) 0) bottom;color:#9851e4}.page-tag-item.color7.clickable:hover{background:#eadbfa;background:linear-gradient(135deg,transparent .75em,rgb(233.75,219,249.5) 0) top,linear-gradient(45deg,transparent .75em,rgb(233.75,219,249.5) 0) bottom}[data-theme=dark] .page-tag-item.color7.clickable:hover{background:#431277;background:linear-gradient(135deg,transparent .75em,rgb(67.1421686747,18.2361445783,119.3638554217) 0) top,linear-gradient(45deg,transparent .75em,rgb(67.1421686747,18.2361445783,119.3638554217) 0) bottom}.page-tag-item.color8{background:#fdeaf5;background:linear-gradient(135deg,transparent .75em,rgb(253,234.2,244.5) 0) top,linear-gradient(45deg,transparent .75em,rgb(253,234.2,244.5) 0) bottom;color:#ef59ab}[data-theme=dark] .page-tag-item.color8{background:#400626;background:linear-gradient(135deg,transparent .75em,rgb(64.3157894737,6.1842105263,38.0328947368) 0) top,linear-gradient(45deg,transparent .75em,rgb(64.3157894737,6.1842105263,38.0328947368) 0) bottom;color:#e81689}.page-tag-item.color8.clickable:hover{background:#facbe5;background:linear-gradient(135deg,transparent .75em,rgb(250,203,228.75) 0) top,linear-gradient(45deg,transparent .75em,rgb(250,203,228.75) 0) bottom}[data-theme=dark] .page-tag-item.color8.clickable:hover{background:#670a3d;background:linear-gradient(135deg,transparent .75em,rgb(102.9052631579,9.8947368421,60.8526315789) 0) top,linear-gradient(45deg,transparent .75em,rgb(102.9052631579,9.8947368421,60.8526315789) 0) bottom}:root{--balloon-border-radius: 2px;--balloon-color: rgba(16, 16, 16, .95);--balloon-text-color: #fff;--balloon-font-size: 12px;--balloon-move: 4px}button[aria-label][data-balloon-pos]{overflow:visible}[aria-label][data-balloon-pos]{position:relative;cursor:pointer}[aria-label][data-balloon-pos]:after{opacity:0;pointer-events:none;transition:all .18s ease-out .18s;text-indent:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-weight:400;font-style:normal;text-shadow:none;font-size:var(--balloon-font-size);background:var(--balloon-color);border-radius:2px;color:var(--balloon-text-color);border-radius:var(--balloon-border-radius);content:attr(aria-label);padding:.5em 1em;position:absolute;white-space:nowrap;z-index:10}[aria-label][data-balloon-pos]:before{width:0;height:0;border:5px solid transparent;border-top-color:var(--balloon-color);opacity:0;pointer-events:none;transition:all .18s ease-out .18s;content:"";position:absolute;z-index:10}[aria-label][data-balloon-pos]:hover:before,[aria-label][data-balloon-pos]:hover:after,[aria-label][data-balloon-pos][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-visible]:after,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:before,[aria-label][data-balloon-pos]:not([data-balloon-nofocus]):focus:after{opacity:1;pointer-events:none}[aria-label][data-balloon-pos].font-awesome:after{font-family:FontAwesome,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}[aria-label][data-balloon-pos][data-balloon-break]:after{white-space:pre}[aria-label][data-balloon-pos][data-balloon-break][data-balloon-length]:after{white-space:pre-line;word-break:break-word}[aria-label][data-balloon-pos][data-balloon-blunt]:before,[aria-label][data-balloon-pos][data-balloon-blunt]:after{transition:none}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:after{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=up]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=up][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=down]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=down][data-balloon-visible]:before{transform:translate(-50%)}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:after{left:0}[aria-label][data-balloon-pos][data-balloon-pos*=-left]:before{left:5px}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:after{right:0}[aria-label][data-balloon-pos][data-balloon-pos*=-right]:before{right:5px}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:after,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:after{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-po*=-left]:hover:before,[aria-label][data-balloon-pos][data-balloon-po*=-left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos*=-right][data-balloon-visible]:before{transform:translate(0)}[aria-label][data-balloon-pos][data-balloon-pos^=up]:before,[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{bottom:100%;transform-origin:top;transform:translateY(var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=up]:after{margin-bottom:10px}[aria-label][data-balloon-pos][data-balloon-pos=up]:before,[aria-label][data-balloon-pos][data-balloon-pos=up]:after{left:50%;transform:translate(-50%,var(--balloon-move))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before,[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{top:100%;transform:translateY(calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos^=down]:after{margin-top:10px}[aria-label][data-balloon-pos][data-balloon-pos^=down]:before{width:0;height:0;border:5px solid transparent;border-bottom-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=down]:after,[aria-label][data-balloon-pos][data-balloon-pos=down]:before{left:50%;transform:translate(-50%,calc(var(--balloon-move) * -1))}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:after,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:after{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=left][data-balloon-visible]:before,[aria-label][data-balloon-pos][data-balloon-pos=right]:hover:before,[aria-label][data-balloon-pos][data-balloon-pos=right][data-balloon-visible]:before{transform:translateY(-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after,[aria-label][data-balloon-pos][data-balloon-pos=left]:before{right:100%;top:50%;transform:translate(var(--balloon-move),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=left]:after{margin-right:10px}[aria-label][data-balloon-pos][data-balloon-pos=left]:before{width:0;height:0;border:5px solid transparent;border-left-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after,[aria-label][data-balloon-pos][data-balloon-pos=right]:before{left:100%;top:50%;transform:translate(calc(var(--balloon-move) * -1),-50%)}[aria-label][data-balloon-pos][data-balloon-pos=right]:after{margin-left:10px}[aria-label][data-balloon-pos][data-balloon-pos=right]:before{width:0;height:0;border:5px solid transparent;border-right-color:var(--balloon-color)}[aria-label][data-balloon-pos][data-balloon-length]:after{white-space:normal}[aria-label][data-balloon-pos][data-balloon-length=small]:after{width:80px}[aria-label][data-balloon-pos][data-balloon-length=medium]:after{width:150px}[aria-label][data-balloon-pos][data-balloon-length=large]:after{width:260px}[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:380px}@media screen and (max-width: 768px){[aria-label][data-balloon-pos][data-balloon-length=xlarge]:after{width:90vw}}[aria-label][data-balloon-pos][data-balloon-length=fit]:after{width:100%}.page-info{display:flex;flex-wrap:wrap;place-content:stretch flex-start;align-items:center;color:var(--vp-c-text-mute);font-size:14px}@media print{.page-info{display:flex!important}}.page-info>span{display:flex;align-items:center;max-width:100%;margin-inline-end:.5em;line-height:2}@media (min-width: 1440px){.page-info>span{font-size:1.1em}}@media (max-width: 419px){.page-info>span{margin-inline-end:.3em;font-size:.875em}}@media print{.page-info>span{display:flex!important}}.page-info .icon{position:relative;display:inline-block;vertical-align:middle;width:1em;height:1em;margin-inline-end:.25em}.page-info a{color:inherit}.page-info a:hover,.page-info a:active{color:var(--vp-c-accent)}.vp-page-title{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem}@media (max-width: 959px){.vp-page-title{padding-inline:1.5rem}}@media print{.vp-page-title{max-width:unset}}.vp-page-title{position:relative;z-index:1;padding-top:1rem;padding-bottom:0}@media print{.vp-page-title{padding-inline:0!important}}@media (max-width: 959px){.vp-page-title{padding-top:.5rem}}.vp-page-title h1{margin-top:calc(0px - var(--navbar-height))!important;margin-bottom:1rem;padding-top:var(--navbar-height)!important;font-size:2.2rem}@media (max-width: 959px){.vp-page-title h1{margin-bottom:.5rem}}.vp-page-title h1 .icon{margin-inline-end:.25em;color:var(--vp-c-accent);font-size:.9em}.vp-page-title h1 img.icon{vertical-align:-.125em;height:1em}.theme-hope-content:not(.custom){padding-top:0!important}.theme-hope-content:not(.custom) h1:first-child,.theme-hope-content:not(.custom) h2:first-child,.theme-hope-content:not(.custom) h3:first-child,.theme-hope-content:not(.custom) h4:first-child,.theme-hope-content:not(.custom) h5:first-child,.theme-hope-content:not(.custom) h6:first-child{margin-top:calc(.5rem - var(--navbar-height))!important;padding-top:var(--navbar-height)!important}.theme-hope-content:not(.custom)>h1:first-child{display:none}.vp-page-meta{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem}@media (max-width: 959px){.vp-page-meta{padding-inline:1.5rem}}@media print{.vp-page-meta{max-width:unset}}.vp-page-meta{display:flex;flex-wrap:wrap;justify-content:space-between;overflow:auto;padding-top:.75rem;padding-bottom:.75rem}@media print{.vp-page-meta{margin:0!important;padding-inline:0!important}}@media (max-width: 719px){.vp-page-meta{display:block}}.vp-page-meta .vp-meta-item{flex-grow:1}.vp-page-meta .vp-meta-item .vp-meta-label{font-weight:500}.vp-page-meta .vp-meta-item .vp-meta-label:not(a){color:var(--vp-c-text)}.vp-page-meta .vp-meta-item .vp-meta-info{color:var(--vp-c-text-mute);font-weight:400}.vp-page-meta .git-info{text-align:end}.vp-page-meta .edit-link{margin-top:.25rem;margin-bottom:.25rem;margin-inline-end:.5rem;font-size:14px}@media print{.vp-page-meta .edit-link{display:none}}.vp-page-meta .edit-link .icon{position:relative;bottom:-.125em;width:1em;height:1em;margin-inline-end:.25em}.vp-page-meta .update-time,.vp-page-meta .contributors{margin-top:.25rem;margin-bottom:.25rem;font-size:14px}@media (max-width: 719px){.vp-page-meta .update-time,.vp-page-meta .contributors{font-size:13px;text-align:start}}.print-button{border-width:0;background:transparent;cursor:pointer;box-sizing:content-box;width:1rem;height:1rem;padding:.5rem;border-radius:.25em;color:inherit;font-size:1rem;transform:translateY(.25rem)}@media print{.print-button{display:none}}.vp-toc-placeholder{max-width:var(--content-width, 740px);margin-inline:auto;padding-inline:2.5rem}@media (max-width: 959px){.vp-toc-placeholder{padding-inline:1.5rem}}@media print{.vp-toc-placeholder{max-width:unset}}.vp-toc-placeholder{position:sticky;top:calc(var(--navbar-height) + .5rem);z-index:99;display:none;max-width:var(--content-width, 740px)}@media (max-width: 719px){.hide-navbar .vp-toc-placeholder{top:.5rem}}@media (min-width: 1440px){.vp-toc-placeholder{top:calc(var(--navbar-height) + 2rem)}}@media print{.vp-toc-placeholder{display:none!important}}.vp-toc-placeholder+.theme-hope-content:not(.custom){padding-top:0}.has-toc .vp-toc-placeholder{display:block}#toc{margin-bottom:1rem;border-radius:8px;background:var(--vp-c-bg-alt)}@media (min-width: 1440px){#toc{position:absolute;inset-inline-start:calc(100% + 1rem);min-width:10rem;max-width:15rem;margin-bottom:0;border-radius:0;background:transparent}}.vp-toc-header{padding:.5rem 1rem;font-weight:600}@media (min-width: 1440px){.vp-toc-header{padding-top:0;font-size:.875rem}}.vp-toc-header .arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}[data-theme=dark] .vp-toc-header .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgb(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.vp-toc-header .arrow.down{transform:rotate(180deg)}[dir=rtl] .vp-toc-header .arrow.down{transform:rotate(-180deg)}.vp-toc-header .arrow.end{transform:rotate(90deg)}[dir=rtl] .vp-toc-header .arrow.end,.vp-toc-header .arrow.start{transform:rotate(-90deg)}[dir=rtl] .vp-toc-header .arrow.start{transform:rotate(90deg)}@media (min-width: 1440px){.vp-toc-header .arrow{display:none}}.vp-toc-header .print-button{display:none}@media (min-width: 1440px){.vp-toc-header .print-button{display:inline-block}}.vp-toc-wrapper{position:relative;overflow:hidden auto;height:0;max-height:8rem;margin:0 .5rem;text-overflow:ellipsis;white-space:nowrap;scroll-behavior:smooth;transition:height .5s}@media (min-width: 1440px){.vp-toc-wrapper{height:auto;max-height:75vh}}.vp-toc-wrapper.open{height:auto;margin-top:.5rem;padding-bottom:.5rem}.vp-toc-wrapper::-webkit-scrollbar-track-piece{background:transparent}.vp-toc-wrapper::-webkit-scrollbar{width:3px}.vp-toc-wrapper::-webkit-scrollbar-thumb:vertical{background:var(--vp-c-border)}.vp-toc-wrapper>.vp-toc-list{padding-inline-start:8px}@media (min-width: 1440px){.vp-toc-wrapper>.vp-toc-list:before{content:" ";position:absolute;inset-inline-start:4px;top:0;bottom:0;z-index:-1;width:2px;background:var(--vp-c-border)}}.vp-toc-list{position:relative;margin:0;padding:0}.vp-toc-marker{position:absolute;inset-inline-start:4px;top:0;z-index:2;display:none;width:2px;height:1.7rem;background:var(--vp-c-accent-bg);transition:top var(--vp-t-transform)}@media (min-width: 1440px){.vp-toc-marker{display:block}}.vp-toc-link{position:relative;display:block;overflow:hidden;max-width:100%;color:var(--vp-c-text-mute);line-height:inherit;text-overflow:ellipsis;white-space:nowrap}.vp-toc-link.level2{padding-inline-start:0px;font-size:14px}.vp-toc-link.level3{padding-inline-start:8px;font-size:13px}.vp-toc-link.level4{padding-inline-start:16px;font-size:12px}.vp-toc-link.level5{padding-inline-start:24px;font-size:11px}.vp-toc-link.level6{padding-inline-start:32px;font-size:10px}.vp-toc-item{position:relative;box-sizing:border-box;height:1.7rem;list-style:none;line-height:1.7rem}@media (min-width: 1440px){.vp-toc-item{padding:0 .5rem}}.vp-toc-item:hover>.vp-toc-link{color:var(--vp-c-accent)}.vp-toc-item.active>.vp-toc-link{color:var(--vp-c-accent);font-weight:700}.vp-page{display:block;flex-grow:1;padding-bottom:2rem;transition:padding var(--vp-t-transform)}@media print{.vp-page{min-height:auto!important;margin:0!important;padding:0!important}}.page-cover{width:var(--content-width);margin-inline:auto}@media (max-width: 719px){.page-cover{width:100%}}.page-cover img{-o-object-fit:cover;object-fit:cover;width:100%;max-height:25vh;border-radius:.5rem}@media (max-width: 719px){.page-cover img{border-radius:0}}[vp-comment]{max-width:var(--content-width, 740px);margin:0 auto;padding:2rem 2.5rem}@media (max-width: 959px){[vp-comment]{padding:1.5rem}}@media (max-width: 419px){[vp-comment]{padding:1rem 1.5rem}}@media print{[vp-comment]{max-width:unset}}#iframeu6953633_0[data-v-826d2cba]{width:100%!important}.slot-demo-block[data-v-826d2cba]{margin:1rem .5rem;display:flex;justify-content:center;align-items:center;font-size:13px;color:var(--text-color-light)}.slot-img[data-v-826d2cba]{margin:1rem;width:80%;display:flex;justify-content:center;align-items:center}@keyframes cursor-blink{0%{opacity:1}50%{opacity:0}to{opacity:1}}.vp-portfolio{position:relative;z-index:1;overflow:hidden;box-sizing:border-box}.vp-portfolio:not(.bg){background:var(--vp-c-accent-soft)}.vp-portfolio:not(.bg):after{content:"";position:absolute;top:-200px;left:-200px;z-index:-1;width:1000px;height:1000px;border-radius:50%;background-color:var(--vp-c-white)}@media (max-width: 959px){.vp-portfolio:not(.bg):after{display:none}}.vp-portfolio-mask{position:absolute;top:0;right:0;bottom:0;left:0}.vp-portfolio-mask:after{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:block;background:#888;opacity:.2}.vp-portfolio-mask.light{display:block}[data-theme=dark] .vp-portfolio-mask.light,.vp-portfolio-mask.dark{display:none}[data-theme=dark] .vp-portfolio-mask.dark{display:block}.vp-portfolio-avatar{position:absolute;top:0;bottom:0;left:0;display:flex;align-items:end;justify-content:center;width:50vw;height:100vh;border-radius:10px}@media (min-width: 1440px){.vp-portfolio-avatar{width:60vw}}@media (max-width: 959px){.vp-portfolio-avatar{width:100vw}}.vp-portfolio-avatar img{max-height:93vh}.vp-portfolio-avatar img.light{display:block}[data-theme=dark] .vp-portfolio-avatar img.light,.vp-portfolio-avatar img.dark{display:none}[data-theme=dark] .vp-portfolio-avatar img.dark{display:block}.vp-portfolio-container{position:relative;z-index:1;display:flex;align-items:center;justify-content:flex-end;box-sizing:border-box;min-height:100vh;padding-top:120px;padding-bottom:70px}.vp-portfolio-info{position:relative;flex:0 0 50%;box-sizing:border-box;max-width:50%;padding:0 25px}@media (max-width: 959px){.vp-portfolio-info{position:absolute;right:15px;bottom:15px;left:15px;flex:0 0 100%;max-width:unset;padding:2rem 1.5rem;border-radius:.5rem;background:var(--vp-c-bg-soft);text-align:center}}.vp-portfolio-info .vp-social-medias{justify-content:start}@media (max-width: 959px){.vp-portfolio-info .vp-social-medias{justify-content:center}}.vp-portfolio-welcome{margin:0 0 10px;padding:0;color:var(--vp-c-accent);font-weight:600;font-size:28px}@media (max-width: 959px){.vp-portfolio-welcome{font-size:18px}}.vp-portfolio-name{margin:0 0 10px;padding:0;color:var(--black);font-weight:700;font-size:64px}@media (min-width: 1440px){.vp-portfolio-name{font-size:72px}}@media (max-width: 959px){.vp-portfolio-name{font-size:36px}}.vp-portfolio-title{position:relative;margin:0;padding:0;border-bottom:none;color:var(--vp-c-accent);font-weight:600;font-size:25px}@media (max-width: 959px){.vp-portfolio-title{font-size:18px}}.vp-portfolio-title:after{content:"";display:inline-block;vertical-align:top;width:2px;height:1.3em;background:var(--vp-c-accent-bg);animation-name:cursor-blink;animation-duration:1s;animation-iteration-count:infinite}.vp-portfolio-medias{display:flex;flex-wrap:wrap;justify-content:center;padding-top:20px}@media (min-width: 959px){.vp-portfolio-medias{justify-content:flex-start}}.vp-portfolio-media{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;background:var(--vp-c-accent-bg);color:var(--vp-c-white);transition:ease all .35s}.vp-portfolio-media:hover{background-color:var(--vp-c-accent-hover);cursor:pointer}.vp-portfolio-media:after{--balloon-font-size: 10px;padding:.2em .4em!important}.vp-portfolio-media+a{margin-left:5px}.theme-hope-content.vp-portfolio-content{--content-width: min(84%, 1200px);--header-underline: rgba(46.6962025316, 131.8037974684, 93.3924050633, .4);display:flex;flex-flow:column;align-items:center}@media (min-width: 1280px){.theme-hope-content.vp-portfolio-content{font-size:18px}}@media (max-width: 419px){.theme-hope-content.vp-portfolio-content{font-size:14px}}[data-theme=dark] .theme-hope-content.vp-portfolio-content{--header-underline: rgba(85.5379746835, 194.9620253165, 145.5759493671, .4)}.theme-hope-content.vp-portfolio-content>h2,.theme-hope-content.vp-portfolio-content>h3,.theme-hope-content.vp-portfolio-content>h4,.theme-hope-content.vp-portfolio-content>h5,.theme-hope-content.vp-portfolio-content>h6{margin-bottom:1rem;border-bottom:none;text-align:center}.theme-hope-content.vp-portfolio-content>h2{font-size:2.5rem}@media (max-width: 419px){.theme-hope-content.vp-portfolio-content>h2{font-size:2rem}}.theme-hope-content.vp-portfolio-content>h3{font-size:2rem}@media (max-width: 419px){.theme-hope-content.vp-portfolio-content>h3{font-size:1.75rem}}.theme-hope-content.vp-portfolio-content>p{align-self:stretch}.theme-hope-content.vp-portfolio-content .header-anchor>span{background:linear-gradient(var(--header-underline),var(--header-underline)) no-repeat;background-position:-.1em calc(100% - .05em);background-size:calc(100% + .1em) .2em;text-shadow:.05em 0 var(--vp-c-bg),-.05em 0 var(--vp-c-bg)}.theme-hope-content.vp-portfolio-content .header-anchor>span:before,.theme-hope-content.vp-portfolio-content .header-anchor>span:after{content:"";position:relative;bottom:.05em;display:inline-block;vertical-align:text-bottom;width:.1em;height:.2em;background-color:var(--header-underline)}.theme-hope-content.vp-portfolio-content .header-anchor>span:before{left:-.1em;border-radius:.2em 0 0 .2em}.theme-hope-content.vp-portfolio-content .header-anchor>span:after{right:-.1em;border-radius:0 .2em .2em 0}.vp-skip-link{inset-inline-start:.25rem;top:.25rem;z-index:999;padding:.65rem 1.5rem;border-radius:.5rem;background:var(--vp-c-bg);color:var(--vp-c-accent);box-shadow:var(--vp-c-shadow);font-weight:700;font-size:.9em;text-decoration:none}@media print{.vp-skip-link{display:none}}.vp-skip-link:focus{clip-path:none;clip:auto;width:auto;height:auto}.not-found-hint{padding:2rem}.not-found-hint .error-code{margin:0;font-weight:700;font-size:4rem;line-height:4rem}.not-found-hint .error-title{font-weight:700}.not-found-hint .error-hint{margin:0;padding:12px 0;font-weight:600;font-size:20px;line-height:20px;letter-spacing:2px}.vp-page.not-found{display:flex;flex-flow:column;align-items:center;justify-content:center;box-sizing:border-box;width:100vw;max-width:var(--home-page-width);margin:0 auto;padding:calc(var(--navbar-height) + 1rem) 1rem 1rem!important;text-align:center}.vp-page.not-found .action-button{display:inline-block;box-sizing:border-box;margin:.25rem;padding:.75rem 1rem;border-width:0;border-radius:3rem;background:var(--vp-c-accent-bg);color:var(--vp-c-white);outline:none;font-size:1rem;transition:background var(--vp-t-color)}.vp-page.not-found .action-button:hover{background:var(--vp-c-accent-hover);cursor:pointer}.vp-social-medias{display:flex;flex-wrap:wrap;justify-content:center;margin:8px auto}.vp-social-media{width:26px;height:26px;margin:4px}.vp-social-media svg{transition:transform var(--vp-t-transform)}.vp-social-media:hover{cursor:pointer}.vp-social-media:hover svg{transform:scale(1.1)}.vp-social-media:after{--balloon-font-size: 10px;padding:.2em .4em!important}.vp-social-media .icon{width:100%;height:100%}.vp-blogger-info{padding:.5rem;font-family:var(--vp-font-heading);overflow-wrap:break-word}.vp-page .vp-blogger-info{background:var(--vp-c-bg-elv)}.vp-sidebar .vp-blogger-info.mobile{display:none}@media (max-width: 719px){.vp-sidebar .vp-blogger-info.mobile{display:block}}.vp-sidebar .vp-blogger-info.mobile+hr{display:none}@media (max-width: 719px){.vp-sidebar .vp-blogger-info.mobile+hr{display:block;margin-top:1rem}}.vp-blogger{padding:.5rem;text-align:center}.vp-blogger[role=link]{cursor:pointer}.vp-blogger-avatar{width:8rem;height:8rem;margin:0 auto}.vp-blogger-name{margin:1rem auto;font-size:22px}.vp-blogger-description{margin:1rem auto;font-size:14px}.vp-blog-counts{display:flex;width:80%;margin:0 auto 1rem}.vp-blog-count{display:block;width:25%;color:inherit;font-size:13px;text-align:center;cursor:pointer;transition:color var(--vp-t-color)}.vp-blog-count:hover{color:var(--vp-c-accent)}.vp-blog-count .count{position:relative;margin-bottom:.5rem;font-weight:600;font-size:20px}[data-theme=dark] .empty-icon g.people{opacity:.8}[data-theme=dark] .empty-icon g:not(.people){filter:invert(80%)}.vp-article-wrapper{position:relative;box-sizing:border-box;width:100%;margin:0 auto 1.25rem;text-align:start;overflow-wrap:break-word}@media (max-width: 959px){.vp-article-wrapper{margin:0 auto 1rem}}.vp-article-wrapper:last-child{margin-bottom:0}.vp-article-item{display:block;padding:.75rem 1.25rem;border-radius:.4rem;background:var(--vp-c-bg-elv);color:inherit;box-shadow:0 1px 3px 1px var(--vp-c-shadow);transition:background var(--vp-t-color),box-shadow var(--vp-t-transform)}@media (max-width: 959px){.vp-article-item{padding:.75rem 1rem}}@media (max-width: 419px){.vp-article-item{border-radius:0}}.vp-article-item:hover{box-shadow:0 2px 6px 2px var(--vp-c-shadow)}.vp-article-item .sticky-icon{position:absolute;inset-inline-end:0;top:0;width:1.5rem;height:1.5rem;color:var(--vp-c-accent)}.vp-article-item .page-info>span{display:flex;flex-shrink:0;align-items:center;margin-inline-end:.5em;line-height:1.8}.vp-article-item .page-info>span:after{--balloon-font-size: 12px;padding:.3em .6em!important}.vp-article-hr{margin-block:.375em}.vp-article-title{position:relative;display:inline-block;color:var(--vp-c-text);font-size:1.25rem;font-family:var(--vp-font-heading);line-height:1.6;cursor:pointer}.vp-article-title:after{content:"";position:absolute;inset:auto 0 0;height:2px;background:var(--vp-c-accent);visibility:hidden;transition:transform var(--vp-t-transform);transform:scaleX(0)}.vp-article-title:hover{cursor:pointer}.vp-article-title:hover:after{visibility:visible;transform:scaleX(1)}.vp-article-title a{color:inherit;font-weight:600}.vp-article-title .lock-icon,.vp-article-title .slides-icon{position:relative;bottom:-.125em;display:inline-block;vertical-align:baseline;width:1em;height:1em;margin-inline-end:.25em;color:var(--vp-c-accent)}.vp-article-title>span{word-break:break-word}.vp-article-cover{width:calc(100% + 2.5rem);margin:-.75rem -1.25rem .75rem;border-top-left-radius:.4rem;border-top-right-radius:.4rem}@media (max-width: 959px){.vp-article-cover{width:calc(100% + 2rem);margin:-.75rem -1rem .75rem}}@media (max-width: 419px){.vp-article-cover{border-radius:0}}.vp-article-excerpt{--code-padding-y: .75rem;--code-padding-x: .75rem;--code-line-number-width: 2em;overflow:hidden;line-height:1.6;cursor:default}@media (max-width: 959px){.vp-article-excerpt{font-size:15px}}@media (max-width: 419px){.vp-article-excerpt{font-size:14px}}.vp-article-excerpt h1{display:none}.vp-article-excerpt h2{font-size:1.4em}.vp-article-excerpt h3{font-size:1.2em}.vp-article-excerpt h1,.vp-article-excerpt h2,.vp-article-excerpt h3,.vp-article-excerpt h4,.vp-article-excerpt h5,.vp-article-excerpt h6{margin-top:.5em;margin-bottom:.5em}.vp-article-excerpt h1+p{margin-top:.5em}.vp-article-excerpt p:first-child{margin-top:.5em}.vp-article-excerpt p:last-child{margin-bottom:.5em}.vp-article-excerpt div[class*=language-] pre,.vp-article-excerpt div[class*=language-].line-numbers-mode .line-numbers{line-height:1.5}.vp-article-excerpt .code-demo-wrapper,.vp-article-excerpt .footnote-anchor{display:none}.vp-article-excerpt section.footnotes{display:none}.vp-article-excerpt img{max-width:100%}.vp-article-excerpt figure{display:flex;flex-flow:column;width:auto;margin:1rem auto;text-align:center;transition:transform var(--vp-t-transform)}.vp-article-excerpt figure img{overflow:hidden;margin:0 auto;border-radius:8px}.vp-article-excerpt figure figcaption{display:inline-block;margin:6px auto;font-size:.8rem}.vp-article-excerpt figure figcaption:only-child{display:none}@keyframes message-move-in{0%{opacity:0;transform:translateY(-100%)}to{opacity:1;transform:translateY(0)}}@keyframes message-move-out{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-100%)}}#message-container{position:fixed;inset:calc(var(--message-offset, 3.6rem) + 1rem) 0 auto;z-index:75;text-align:center}#message-container .message{display:inline-block;padding:8px 10px;border-radius:3px;background:var(--vp-c-bg);color:var(--vp-c-text);box-shadow:0 0 10px 0 var(--vp-c-shadow);font-size:14px}#message-container .message.move-in{animation:message-move-in .3s ease-in-out}#message-container .message.move-out{animation:message-move-out .3s ease-in-out;animation-fill-mode:forwards}#message-container .message svg{position:relative;bottom:-.125em;margin-inline-end:5px}.vp-pagination{margin:1.25rem 0 .75rem;font-weight:600;font-size:15px;line-height:2}.vp-pagination-list{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-evenly;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vp-pagination-number{display:flex;align-items:stretch;overflow:hidden;height:30px;margin:0 .5rem;border:1px solid var(--vp-c-border);border-radius:.25rem}.vp-pagination-number div{position:relative;padding:0 .5rem;background:var(--vp-c-bg);color:var(--vp-c-accent);cursor:pointer}.vp-pagination-number div:before{content:" ";position:absolute;inset-inline-start:0;top:0;bottom:0;width:1px;background:var(--vp-c-border)}.vp-pagination-number div:first-child:before{background:transparent}.vp-pagination-number div:hover{color:var(--vp-c-accent-hover)}.vp-pagination-number div.active{background:var(--vp-c-accent-bg);color:var(--vp-c-white)}.vp-pagination-number div.active:before{background:var(--vp-c-accent-bg)}.vp-pagination-number div.active+div:before{background:var(--vp-c-accent-bg)}.vp-pagination-number div.prev,.vp-pagination-number div.next{font-size:13px;line-height:30px}.vp-pagination-number div.active,.vp-pagination-number div.ellipsis{cursor:default}.vp-pagination-nav{display:flex;align-items:center;justify-content:center;margin:.5rem}.vp-pagination-nav input{width:3.5rem;margin:6px 5px;border:1px solid var(--vp-c-border);border-radius:.25em;background:var(--vp-c-bg);color:var(--vp-c-text);outline:none;line-height:2;text-align:center}.vp-pagination-button{overflow:hidden;padding:0 .75em;border:1px solid var(--vp-c-border);border-radius:.25em;background:var(--vp-c-bg);color:var(--vp-c-accent);outline:none;font-weight:600;font-size:15px;line-height:2;cursor:pointer}.vp-pagination-button:hover{color:var(--vp-c-accent-hover)}.vp-article-list{margin-top:calc(-.5rem - var(--navbar-height));padding-top:calc(var(--navbar-height) + .5rem);text-align:center}.vp-article-list:first-child{margin-top:calc(0rem - var(--navbar-height))}.vp-article-list .empty{max-width:560px;margin:0 auto;text-align:center}.vp-category-list{position:relative;z-index:2;padding-inline-start:0;list-style:none;font-size:14px}.vp-category{display:inline-block;vertical-align:middle;overflow:hidden;margin:.3rem .6rem .8rem;padding:.4rem .8rem;border-radius:.25rem;background:var(--vp-c-grey-soft);color:var(--vp-c-text);box-shadow:0 1px 4px 0 var(--vp-c-shadow);word-break:break-word;cursor:pointer;transition:background var(--vp-t-color),color var(--vp-t-color)}@media (max-width: 419px){.vp-category{font-size:.9rem}}.vp-category a{color:inherit}.vp-category .vp-category-count{display:inline-block;min-width:1rem;height:1.2rem;margin-inline-start:.2em;padding:0 .1rem;border-radius:.6rem;background:var(--vp-c-accent-bg);color:var(--vp-c-white);font-size:.7rem;line-height:1.2rem;text-align:center}.vp-category.color0{background:#fde5e7;color:#ba111f}[data-theme=dark] .vp-category.color0{background:#340509;color:#ec2f3e}.vp-category.color0:hover{background:#f9bec3}[data-theme=dark] .vp-category.color0:hover{background:#53080e}.vp-category.color0.active{background:#cf1322;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color0.active{background:#a60f1b}.vp-category.color0.active .count{background:var(--vp-c-white);color:#cf1322}.vp-category.color0 .count{background:#cf1322}.vp-category.color1{background:#ffeee8;color:#f54205}[data-theme=dark] .vp-category.color1{background:#441201;color:#fb7649}.vp-category.color1:hover{background:#fed4c6}[data-theme=dark] .vp-category.color1:hover{background:#6d1d02}.vp-category.color1.active{background:#fa541c;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color1.active{background:#da3a05}.vp-category.color1.active .count{background:var(--vp-c-white);color:#fa541c}.vp-category.color1 .count{background:#fa541c}.vp-category.color2{background:#fef5e7;color:#e08e0b}[data-theme=dark] .vp-category.color2{background:#3e2703;color:#f5b041}.vp-category.color2:hover{background:#fce6c4}[data-theme=dark] .vp-category.color2:hover{background:#633f05}.vp-category.color2.active{background:#f39c12;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color2.active{background:#c77e0a}.vp-category.color2.active .count{background:var(--vp-c-white);color:#f39c12}.vp-category.color2 .count{background:#f39c12}.vp-category.color3{background:#eafaf1;color:#29b866}[data-theme=dark] .vp-category.color3{background:#0c331c;color:#55d98d}.vp-category.color3:hover{background:#caf3db}[data-theme=dark] .vp-category.color3:hover{background:#12522d}.vp-category.color3.active{background:#2ecc71;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color3.active{background:#25a35a}.vp-category.color3.active .count{background:var(--vp-c-white);color:#2ecc71}.vp-category.color3 .count{background:#2ecc71}.vp-category.color4{background:#e6f9ee;color:#219552}[data-theme=dark] .vp-category.color4{background:#092917;color:#36d278}.vp-category.color4:hover{background:#c0f1d5}[data-theme=dark] .vp-category.color4:hover{background:#0f4224}.vp-category.color4.active{background:#25a55b;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color4.active{background:#1e8449}.vp-category.color4.active .count{background:var(--vp-c-white);color:#25a55b}.vp-category.color4 .count{background:#25a55b}.vp-category.color5{background:#e1fcfc;color:#0e9595}[data-theme=dark] .vp-category.color5{background:#042929;color:#16e1e1}.vp-category.color5:hover{background:#b4f8f8}[data-theme=dark] .vp-category.color5:hover{background:#064242}.vp-category.color5.active{background:#10a5a5;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color5.active{background:#0d8484}.vp-category.color5.active .count{background:var(--vp-c-white);color:#10a5a5}.vp-category.color5 .count{background:#10a5a5}.vp-category.color6{background:#e4f0fe;color:#0862c3}[data-theme=dark] .vp-category.color6{background:#021b36;color:#2589f6}.vp-category.color6:hover{background:#bbdafc}[data-theme=dark] .vp-category.color6:hover{background:#042c57}.vp-category.color6.active{background:#096dd9;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color6.active{background:#0757ae}.vp-category.color6.active .count{background:var(--vp-c-white);color:#096dd9}.vp-category.color6 .count{background:#096dd9}.vp-category.color7{background:#f7f1fd;color:#9851e4}[data-theme=dark] .vp-category.color7{background:#2a0b4b;color:#bb8ced}.vp-category.color7:hover{background:#eadbfa}[data-theme=dark] .vp-category.color7:hover{background:#431277}.vp-category.color7.active{background:#aa6fe9;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color7.active{background:#8733e0}.vp-category.color7.active .count{background:var(--vp-c-white);color:#aa6fe9}.vp-category.color7 .count{background:#aa6fe9}.vp-category.color8{background:#fdeaf5;color:#e81689}[data-theme=dark] .vp-category.color8{background:#400626;color:#ef59ab}.vp-category.color8:hover{background:#facbe5}[data-theme=dark] .vp-category.color8:hover{background:#670a3d}.vp-category.color8.active{background:#eb2f96;color:var(--vp-c-white)}[data-theme=dark] .vp-category.color8.active{background:#ce147a}.vp-category.color8.active .count{background:var(--vp-c-white);color:#eb2f96}.vp-category.color8 .count{background:#eb2f96}.vp-tag-list{position:relative;z-index:2;display:flex;flex-wrap:wrap;justify-content:flex-start;padding-inline-start:0;list-style:none}.vp-tag{position:relative;display:inline-block;vertical-align:middle;overflow:hidden;min-width:24px;margin:4px 6px;padding:3px 8px;border-radius:8px;background:var(--vp-c-grey-soft);color:var(--vp-c-text);box-shadow:0 1px 6px 0 var(--vp-c-shadow);font-size:12px;text-align:center;word-break:break-word;cursor:pointer;transition:background var(--vp-t-color),box-shadow var(--vp-t-transform),transform var(--vp-t-transform)}.vp-tag:hover{box-shadow:0 1px 4px 0 var(--vp-c-shadow);cursor:pointer}.vp-tag.active{box-shadow:0 1px 4px 0 var(--vp-c-shadow);transform:scale(1.05)}.vp-tag a{color:inherit}.vp-tag.color0{background:#ef5662}[data-theme=dark] .vp-tag.color0{background:#910d18}.vp-tag.color1{background:#fc906b}[data-theme=dark] .vp-tag.color1{background:#be3304}.vp-tag.color2{background:#f7bf65}[data-theme=dark] .vp-tag.color2{background:#ae6e09}.vp-tag.color3{background:#75e0a2}[data-theme=dark] .vp-tag.color3{background:#208f4f}.vp-tag.color4{background:#5bda91}[data-theme=dark] .vp-tag.color4{background:#1a7440}.vp-tag.color5{background:#3cecec}[data-theme=dark] .vp-tag.color5{background:#0b7474}.vp-tag.color6{background:#4e9ff8}[data-theme=dark] .vp-tag.color6{background:#064c98}.vp-tag.color7{background:#c8a1f1}[data-theme=dark] .vp-tag.color7{background:#7520d1}.vp-tag.color8{background:#f278bb}[data-theme=dark] .vp-tag.color8{background:#b4116a}.vp-tag-count{margin-inline-start:.5em}.timeline-list-wrapper{--dot-color: #fff;--dot-bar-color: #eaecef;--dot-border-color: #ddd;padding:8px 0}[data-theme=dark] .timeline-list-wrapper{--dot-color: #444;--dot-bar-color: #333;--dot-border-color: #555}.timeline-list-wrapper .timeline-list-title{cursor:pointer}.timeline-list-wrapper .timeline-list-title .icon{position:relative;bottom:-.125rem;width:16px;height:16px;margin:0 6px}.timeline-list-wrapper .timeline-list-title .num{position:relative;margin:0 2px;font-size:22px}.timeline-list-wrapper .timeline-content{overflow-y:auto;max-height:80vh}.timeline-list-wrapper .timeline-content::-webkit-scrollbar-track-piece{background:transparent}.timeline-list-wrapper .timeline-list{position:relative;box-sizing:border-box;margin:0 8px;list-style:none}.timeline-list-wrapper .timeline-list:after{content:" ";position:absolute;inset-inline-start:0;top:14px;z-index:-1;width:4px;height:calc(100% - 14px);margin-inline-start:-2px;background:var(--dot-bar-color);transition:background var(--vp-t-color)}.timeline-list-wrapper .timeline-year{position:relative;margin:20px 0 0;color:var(--vp-c-text);font-weight:700;font-size:20px}.timeline-list-wrapper .timeline-year:before{content:" ";position:absolute;z-index:2;border:1px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--vp-t-color),border-color var(--vp-t-color);inset-inline-start:-20px;top:50%;width:8px;height:8px;margin-top:-4px;margin-inline-start:-4px}.timeline-list-wrapper .timeline-year-wrapper{padding-inline-start:0!important}.timeline-list-wrapper .timeline-date{display:inline-block;vertical-align:bottom;width:36px;font-size:12px;line-height:32px;transition:color var(--vp-t-color)}.timeline-list-wrapper .timeline-date:before{content:" ";position:absolute;z-index:2;border:1px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--vp-t-color),border-color var(--vp-t-color);inset-inline-start:-19px;top:24px;width:6px;height:6px;margin-inline-start:-4px}.timeline-list-wrapper .timeline-title{color:inherit;font-size:14px;line-height:32px;cursor:pointer;transition:color var(--vp-t-color)}.timeline-list-wrapper .timeline-item{position:relative;display:flex;padding:12px 0 4px;border-bottom:1px dashed var(--vp-c-border);list-style:none;transition:border-color var(--vp-t-color)}.timeline-list-wrapper .timeline-item:hover .timeline-date{color:var(--vp-c-accent)}.timeline-list-wrapper .timeline-item:hover .timeline-date:before{border-color:var(--dot-color);background:var(--vp-c-accent)}.timeline-list-wrapper .timeline-item:hover .timeline-title{color:var(--vp-c-accent)}.vp-blog-infos{margin:8px auto;padding:8px 16px}.vp-page .vp-blog-infos{border-radius:6px;background:var(--vp-c-bg-elv);box-shadow:0 1px 3px 1px var(--vp-c-shadow);transition:box-shadow var(--vp-t-transform)}.vp-page .vp-blog-infos:hover{box-shadow:0 2px 6px 2px var(--vp-c-shadow)}.vp-blog-infos .timeline-list-wrapper .content{max-height:60vh}.vp-blog-type-switcher{display:flex;justify-content:center;margin-bottom:8px}.vp-blog-type-button{border-width:0;background:transparent;cursor:pointer;width:44px;height:44px;margin:0 8px;padding:4px;color:var(--vp-c-text)}.vp-blog-type-button:focus{outline:none}.vp-blog-type-icon-wrapper{width:20px;height:20px;padding:8px;border-radius:50%;background:#7f7f7f26;transition:background var(--vp-t-color)}[data-theme=dark] .vp-blog-type-icon-wrapper{background:#ffffff26}.vp-blog-type-icon-wrapper:hover{cursor:pointer}.vp-blog-type-icon-wrapper.active{background:var(--vp-c-accent-bg);color:var(--vp-c-accent-text)}.vp-blog-type-icon-wrapper .icon{width:100%;height:100%}.vp-star-article-wrapper,.vp-category-wrapper,.vp-tag-wrapper{padding:8px 0}.vp-star-article-wrapper .title,.vp-category-wrapper .title,.vp-tag-wrapper .title{cursor:pointer}.vp-star-article-wrapper .title .icon,.vp-category-wrapper .title .icon,.vp-tag-wrapper .title .icon{position:relative;bottom:-.125rem;width:16px;height:16px;margin:0 6px}.vp-star-article-wrapper .title .num,.vp-category-wrapper .title .num,.vp-tag-wrapper .title .num{position:relative;margin:0 2px;font-size:22px;font-family:var(--vp-font-heading)}.vp-star-articles{overflow-y:auto;max-height:80vh;margin:8px auto;line-height:1.5}.vp-star-article{padding:12px 8px 4px;border-bottom:1px dashed var(--vp-c-border-hard);transition:border-color var(--vp-t-color),color var(--vp-t-color)}.vp-star-article a{color:inherit}.vp-star-article:hover{cursor:pointer}.vp-star-article:hover a{color:var(--vp-c-accent)}.vp-category-wrapper .vp-category-list,.vp-tag-wrapper .vp-tag-list{overflow-y:auto;max-height:80vh;margin:8px auto}.vp-blog-main{flex:1;width:0;max-width:780px}.vp-sidebar .vp-blog-info-wrapper .vp-blogger-info{display:none}.vp-page .vp-blog-info-wrapper{position:sticky;top:calc(var(--navbar-height) + .75rem);flex:0 0 300px;box-sizing:border-box;width:300px;height:auto;margin-top:.75rem;margin-bottom:.75rem;margin-inline-start:1rem;transition:all .3s}@media (max-width: 719px){.vp-page .vp-blog-info-wrapper{display:none}}.vp-page .vp-blog-info-wrapper .vp-blogger-info{margin-bottom:16px;padding:8px 0;border-radius:8px;box-shadow:0 1px 3px 1px var(--vp-c-shadow)}.vp-page .vp-blog-info-wrapper .vp-blogger-info:hover{box-shadow:0 2px 6px 2px var(--vp-c-shadow)}.vp-blog-hero{position:relative;display:flex;flex-flow:column;justify-content:center;height:450px;margin-bottom:1rem;color:#eee;font-family:var(--vp-font-heading)}@media (max-width: 719px){.vp-blog-hero{height:350px}}@media (max-width: 419px){.vp-blog-hero{margin:0 0 1rem}}.vp-blog-hero.no-bg{color:var(--vp-c-text)}.vp-blog-hero>:not(.vp-blog-mask){position:relative;z-index:2}.vp-blog-hero .slide-down-button{border-width:0;background:transparent;cursor:pointer;position:absolute;bottom:0;left:calc(50vw - 30px);display:none;width:60px;height:60px;padding:10px}.vp-blog-hero .slide-down-button .icon{width:30px;margin:-15px 0;animation-name:bounce-down;animation-duration:1.5s;animation-timing-function:linear;animation-direction:alternate;animation-iteration-count:infinite}.vp-blog-hero .slide-down-button .icon:first-child{color:#ffffff26}.vp-blog-hero .slide-down-button .icon:last-child{color:#ffffff80}.vp-blog-hero.fullscreen{height:calc(100vh - var(--navbar-height))}.vp-blog-hero.fullscreen .vp-blog-mask{background-position-y:top}.vp-blog-hero.fullscreen .slide-down-button{display:block}.vp-blog-mask{position:absolute;top:0;right:0;bottom:0;left:0}.vp-blog-mask:after{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:block;background:var(--vp-c-grey-soft)}.vp-blog-mask.light{display:block}[data-theme=dark] .vp-blog-mask.light,.vp-blog-mask.dark{display:none}[data-theme=dark] .vp-blog-mask.dark{display:block}.vp-blog-hero-title{margin:.5rem auto;font-weight:700;font-size:2rem}@media (min-width: 1440px){.vp-blog-hero-title{font-size:2.25rem}}@media (max-width: 719px){.vp-blog-hero-title{font-size:1.75rem}}.vp-blog-hero-image{display:block;max-width:100%;max-height:15rem;margin:1.5rem auto}@media (max-width: 719px){.vp-blog-hero-image{max-height:12rem}}.vp-blog-hero-image.light{display:block}[data-theme=dark] .vp-blog-hero-image.light,.vp-blog-hero-image.dark{display:none}[data-theme=dark] .vp-blog-hero-image.dark{display:block}.vp-blog-hero-image+.vp-blog-hero-title{margin:0 auto}.vp-blog-hero-description{margin:1.2rem auto 0;font-size:1.5rem}@media (max-width: 719px){.vp-blog-hero-description{font-size:1.25rem}}@keyframes bounce-down{0%{transform:translateY(-5px)}to{transform:translateY(5px)}}.vp-project-panel{position:relative;z-index:2;display:flex;flex-wrap:wrap;place-content:stretch flex-start;align-items:stretch;margin-bottom:12px}.vp-project-panel:empty{margin-bottom:0}.vp-project-card{position:relative;width:calc(33% - 40px);margin:6px 8px;padding:12px;border-radius:8px;background:var(--vp-c-bg-elv);transition:background var(--vp-t-color),transform var(--vp-t-transform)}@media (max-width: 959px){.vp-project-card{width:calc(50% - 40px)}}@media (min-width: 1440px){.vp-project-card{width:calc(25% - 40px)}}.vp-project-card:hover{cursor:pointer;transform:scale(.98)}.vp-project-card .icon{position:relative;z-index:2;float:right;width:20px;height:20px}[dir=rtl] .vp-project-card .icon{float:left}.vp-project-card.color0{background:#fde5e7}.vp-project-card.color0:hover{background:#f9bec3}[data-theme=dark] .vp-project-card.color0{background:#340509}[data-theme=dark] .vp-project-card.color0:hover{background:#53080e}.vp-project-card.color1{background:#ffeee8}.vp-project-card.color1:hover{background:#fed4c6}[data-theme=dark] .vp-project-card.color1{background:#441201}[data-theme=dark] .vp-project-card.color1:hover{background:#6d1d02}.vp-project-card.color2{background:#fef5e7}.vp-project-card.color2:hover{background:#fce6c4}[data-theme=dark] .vp-project-card.color2{background:#3e2703}[data-theme=dark] .vp-project-card.color2:hover{background:#633f05}.vp-project-card.color3{background:#eafaf1}.vp-project-card.color3:hover{background:#caf3db}[data-theme=dark] .vp-project-card.color3{background:#0c331c}[data-theme=dark] .vp-project-card.color3:hover{background:#12522d}.vp-project-card.color4{background:#e6f9ee}.vp-project-card.color4:hover{background:#c0f1d5}[data-theme=dark] .vp-project-card.color4{background:#092917}[data-theme=dark] .vp-project-card.color4:hover{background:#0f4224}.vp-project-card.color5{background:#e1fcfc}.vp-project-card.color5:hover{background:#b4f8f8}[data-theme=dark] .vp-project-card.color5{background:#042929}[data-theme=dark] .vp-project-card.color5:hover{background:#064242}.vp-project-card.color6{background:#e4f0fe}.vp-project-card.color6:hover{background:#bbdafc}[data-theme=dark] .vp-project-card.color6{background:#021b36}[data-theme=dark] .vp-project-card.color6:hover{background:#042c57}.vp-project-card.color7{background:#f7f1fd}.vp-project-card.color7:hover{background:#eadbfa}[data-theme=dark] .vp-project-card.color7{background:#2a0b4b}[data-theme=dark] .vp-project-card.color7:hover{background:#431277}.vp-project-card.color8{background:#fdeaf5}.vp-project-card.color8:hover{background:#facbe5}[data-theme=dark] .vp-project-card.color8{background:#400626}[data-theme=dark] .vp-project-card.color8:hover{background:#670a3d}.vp-project-name{position:relative;z-index:2;color:var(--vp-c-text);font-weight:500;font-size:16px;transition:color var(--vp-t-color)}.vp-project-desc{position:relative;z-index:2;margin:6px 0;color:var(--vp-c-text-mute);font-size:13px}.vp-project-image{position:relative;z-index:2;float:right;width:40px;height:40px}[dir=rtl] .vp-project-image{float:left}.vp-page.vp-blog .vp-blog-home{flex:1;width:0;max-width:780px}.vp-page.vp-blog .theme-hope-content:empty{padding:0}.vp-article-type-wrapper{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;padding-inline-start:0;list-style:none;font-weight:600;font-size:18px}@media (max-width: 419px){.vp-article-type-wrapper{font-size:16px}}.vp-article-type{position:relative;vertical-align:middle;margin:.3em .8em;line-height:1.2;cursor:pointer}.vp-article-type:after{content:" ";position:absolute;inset:auto 50% -6px;height:2px;border-radius:1px;background:var(--vp-c-accent-bg);visibility:hidden;transition:inset .2s ease-in-out}.vp-article-type a{display:inline-block;color:inherit;transition:all .3s ease-in-out}.vp-article-type.active{position:relative}.vp-article-type.active a{color:var(--vp-c-accent);transform:scale(1.1)}.vp-article-type:hover:after,.vp-article-type.active:after{inset:auto calc(50% - 8px) -6px;visibility:visible}.timeline-wrapper{--dot-color: #fff;--dot-bar-color: #eaecef;--dot-border-color: #ddd;max-width:740px;margin:0 auto;padding:40px 0}@media (max-width: 719px){.timeline-wrapper{margin:0 1.2rem}}[data-theme=dark] .timeline-wrapper{--dot-color: #444;--dot-bar-color: #333;--dot-border-color: #555}.timeline-wrapper #toc{inset-inline:unset 0;min-width:0}.timeline-wrapper .toc-wrapper{position:relative;z-index:10}.timeline-wrapper .timeline-content{position:relative;box-sizing:border-box;padding-inline-start:76px;list-style:none}.timeline-wrapper .timeline-content:after{content:" ";position:absolute;inset-inline-start:64px;top:14px;z-index:-1;width:4px;height:calc(100% - 38px);margin-inline-end:-2px;background:var(--dot-bar-color);transition:background var(--vp-t-color)}.timeline-wrapper .motto{position:relative;color:var(--vp-c-text);font-size:18px;transition:color var(--vp-t-color)}@media (min-width: 1280px){.timeline-wrapper .motto{font-size:20px}}.timeline-wrapper .motto:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--vp-t-color),border-color var(--vp-t-color);inset-inline-start:-10px;width:8px;height:8px}.timeline-wrapper .timeline-year-title{margin-top:calc(3rem - var(--navbar-height));margin-bottom:.5rem;padding-top:var(--navbar-height);color:var(--vp-c-text);font-weight:700;font-size:26px;font-family:var(--vp-font-heading);transition:color var(--vp-t-color)}.timeline-wrapper .timeline-year-title span{position:relative}.timeline-wrapper .timeline-year-title span:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--vp-t-color),border-color var(--vp-t-color);inset-inline-start:-10px;width:8px;height:8px}.timeline-wrapper .timeline-year-wrapper{padding-inline-start:0!important}.timeline-wrapper .timeline-date{position:absolute;inset-inline-end:calc(100% + 24px);width:50px;font-size:14px;line-height:30px;text-align:end}.timeline-wrapper .timeline-date:before{content:" ";position:absolute;top:50%;z-index:2;margin-top:-6px;margin-inline-start:-6px;border:2px solid var(--dot-border-color);border-radius:50%;background:var(--dot-color);transition:background var(--vp-t-color),border-color var(--vp-t-color);inset-inline-end:-19px;width:6px;height:6px}.timeline-wrapper .timeline-title{position:relative;display:block;color:inherit;font-size:16px;line-height:30px;transition:color var(--vp-t-color),font-size var(--vp-t-transform)}.timeline-wrapper .timeline-item{position:relative;z-index:3;display:flex;padding:30px 0 10px;border-bottom:1px dashed var(--vp-c-border);list-style:none;transition:border-color var(--vp-t-color)}.timeline-wrapper .timeline-item:hover{cursor:pointer}.timeline-wrapper .timeline-item:hover .timeline-date{font-size:16px;transition:border-color var(--vp-t-color),color var(--vp-t-color),font-size var(--vp-t-transform)}.timeline-wrapper .timeline-item:hover .timeline-date:before{border-color:var(--vp-c-accent-bg);background:var(--vp-c-bg-alt)}.timeline-wrapper .timeline-item:hover .timeline-title{color:var(--vp-c-accent);font-size:18px}.vp-page.vp-blog,.vp-page.vp-blog-home{display:flex;flex-flow:column;justify-content:space-between;box-sizing:border-box;padding-bottom:2rem;background:var(--vp-c-bg)}@media (min-width: 1440px){.theme-container.has-toc .vp-page.vp-blog,.theme-container.has-toc .vp-page.vp-blog-home{padding-inline-end:0}}.blog-page-wrapper{display:flex;align-items:flex-start;justify-content:center;box-sizing:border-box;width:100%;margin:0 auto;padding:0 2rem}@media (max-width: 959px){.blog-page-wrapper{padding:0 1rem}}@media (max-width: 419px){.blog-page-wrapper{padding:0}}:root{--vp-c-white: #fff;--vp-c-black: #000;--vp-c-grey-text: #656869;--vp-c-grey-hover: #e4e4e9;--vp-c-grey-bg: #ebebef;--vp-c-grey-soft: rgb(142 150 170 / 14%);--vp-c-indigo-text: #3451b2;--vp-c-indigo-hover: #3a5ccc;--vp-c-indigo-bg: #5672cd;--vp-c-indigo-soft: rgb(100 108 255 / 14%);--vp-c-purple-text: #6f42c1;--vp-c-purple-hover: #7e4cc9;--vp-c-purple-bg: #8e5cd9;--vp-c-purple-soft: rgb(159 122 234 / 14%);--vp-c-blue-text: #2888a7;--vp-c-blue-hover: #2d98ba;--vp-c-blue-bg: #2fa1c5;--vp-c-blue-soft: rgb(27 178 229 / 14%);--vp-c-green-text: #18794e;--vp-c-green-hover: #299764;--vp-c-green-bg: #30a46c;--vp-c-green-soft: rgb(16 185 129 / 14%);--vp-c-yellow-text: #915930;--vp-c-yellow-hover: #946300;--vp-c-yellow-bg: #c28100;--vp-c-yellow-soft: rgb(234 179 8 / 14%);--vp-c-red-text: #b8272c;--vp-c-red-hover: #d5393e;--vp-c-red-bg: #e0575b;--vp-c-red-soft: rgb(244 63 94 / 14%)}[data-theme=dark]{--vp-c-white: #000;--vp-c-black: #fff;--vp-c-grey-text: #939499;--vp-c-grey-hover: #414853;--vp-c-grey-bg: #32363f;--vp-c-grey-soft: rgb(101 117 133 / 16%);--vp-c-indigo-text: #a8b1ff;--vp-c-indigo-hover: #5c73e7;--vp-c-indigo-bg: #3e63dd;--vp-c-indigo-soft: rgb(100 108 255 / 16%);--vp-c-blue-text: #c9e8f2;--vp-c-blue-hover: #a6d9ea;--vp-c-blue-bg: #2785a3;--vp-c-blue-soft: rgb(27 178 229 / 16%);--vp-c-purple-text: #c8abfa;--vp-c-purple-hover: #a879e6;--vp-c-purple-bg: #8e5cd9;--vp-c-purple-soft: rgb(159 122 234 / 16%);--vp-c-green-text: #3dd68c;--vp-c-green-hover: #30a46c;--vp-c-green-bg: #298459;--vp-c-green-soft: rgb(16 185 129 / 16%);--vp-c-yellow-text: #f9b44e;--vp-c-yellow-hover: #da8b17;--vp-c-yellow-bg: #a46a0a;--vp-c-yellow-soft: rgb(234 179 8 / 16%);--vp-c-red-text: #f66f81;--vp-c-red-hover: #f14158;--vp-c-red-bg: #b62a3c;--vp-c-red-soft: rgb(244 63 94 / 16%)}[data-theme=dark]{color-scheme:dark}html,body{background:var(--vp-c-bg, #fff);accent-color:var(--vp-c-accent, #299764);transition:background-color var(--vp-t-color)}html{font-size:16px;font-display:optional;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none}@media print{html{font-size:12pt}}html[data-theme=dark]{color-scheme:dark}body{min-height:100vh;margin:0;padding:0;color:var(--vp-c-text, rgb(60, 60, 67));font-size:1rem;font-synthesis:style}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;overflow-wrap:break-word}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1{font-size:2rem}h2{padding-bottom:.3rem;border-bottom:1px solid var(--vp-c-gutter, #e2e2e3);font-size:1.65rem;transition:border-color var(--vp-t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}p,ul,ol{line-height:1.6;overflow-wrap:break-word}@media print{p,ul,ol{line-height:1.5}}ul,ol{padding-inline-start:1.2em}a{color:var(--vp-c-accent, #299764);font-weight:500;text-decoration:none;overflow-wrap:break-word}a.header-anchor{position:relative;color:inherit;text-decoration:none}a.header-anchor:before{content:"¶";position:absolute;top:.4167em;left:-.75em;display:none;color:var(--vp-c-accent, #299764);font-size:.75em}[dir=rtl] a.header-anchor:before{right:-.75em}a.header-anchor:hover:before{display:block}a.header-anchor:focus-visible{outline:none}a.header-anchor:focus-visible:before{display:block;outline:auto}strong{font-weight:600}blockquote{margin:1rem 0;padding:.25rem 0 .25rem 1rem;border-inline-start:.2rem solid var(--vp-c-border-hard, #b8b8ba);color:var(--vp-c-text-mute, rgba(60, 60, 67, .78));font-size:1rem;overflow-wrap:break-word;transition:border-color var(--vp-t-color),color var(--vp-t-color)}blockquote>p{margin:0}hr{border:0;border-bottom:1px solid var(--vp-c-gutter, #e2e2e3);transition:border-color var(--vp-t-color)}:not(pre)>code{margin:0;padding:3px 6px;border-radius:4px;background:var(--vp-c-grey-soft, rgba(142, 150, 170, .14));font-size:.875em;overflow-wrap:break-word;transition:background-color var(--vp-t-color),color var(--vp-t-color)}p a code{color:var(--vp-c-accent, #299764);font-weight:400}table code{padding:.1rem .4rem}kbd{display:inline-block;min-width:1em;margin-inline:.125rem;padding:.25em;border:1px solid var(--vp-c-border, #c2c2c4);border-radius:.25em;box-shadow:1px 1px 4px 0 var(--vp-c-shadow, rgba(0, 0, 0, .15));line-height:1;letter-spacing:-.1em;text-align:center}table{display:block;overflow-x:auto;margin:1rem 0;border-collapse:collapse}tbody tr:nth-child(odd){background:var(--vp-c-bg-alt, #f6f8fa);transition:background-color var(--vp-t-color)}th,td{padding:.6em 1em;border:1px solid var(--vp-c-border-hard, #d1d4d7);transition:border-color var(--vp-t-color)}pre{text-align:left;direction:ltr;white-space:pre;word-spacing:normal;word-wrap:normal;word-break:normal;overflow-wrap:unset;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}@media print{pre{white-space:pre-wrap}}pre code{padding:0;border-radius:0}@page{margin:2cm;font-size:12pt;size:a4}@media print{*,:after,:before{box-shadow:none!important;text-shadow:none!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}a{color:inherit;font-weight:inherit!important;font-size:inherit!important;text-decoration:underline}a.header-anchor{text-decoration:none}abbr[title]:after{content:" (" attr(title) ")"}pre{border:1px solid #eee;white-space:pre-wrap!important}pre>code{white-space:pre-wrap!important}blockquote{border-inline-start:.2rem solid #ddd;color:inherit}blockquote,pre{orphans:5;widows:5}img,tr,canvas{page-break-inside:avoid}}@media (prefers-reduced-motion: reduce){*,:before,:after{background-attachment:initial!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important}}:root{--external-link-icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");--external-link-c-icon: var(--vp-c-text-mute)}#app{--navbar-c-bg: var(--vp-c-bg-elv-soft);--sidebar-c-bg: var(--vp-c-bg-soft);--sidebar-space: var(--sidebar-width);--catalog-header-offset: var(--navbar-height);--message-offset: var(--navbar-height)}@media (max-width: 959px){#app{--navbar-height: var(--navbar-mobile-height);--navbar-padding-y: var(--navbar-mobile-padding-y);--navbar-padding-x: var(--navbar-mobile-padding-x);--sidebar-width: var(--sidebar-mobile-width)}}@media (min-width: 1440px){#app{--sidebar-space: clamp( var(--sidebar-width), max(0px, calc((100vw - var(--content-width)) / 2 - 2rem)) , 100vw )}}@font-face{font-weight:400;font-style:normal;font-family:Crimson;src:url(data:font/truetype;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTYr5mwEAAAyMAAAAHEdERUYAKQATAAAMbAAAAB5PUy8yVsJ0MgAAAVgAAABgY21hcBiKDzgAAAHcAAABWGdhc3D//wADAAAMZAAAAAhnbHlmr+DBdQAAA1AAAAdsaGVhZBZwt+8AAADcAAAANmhoZWEFawEuAAABFAAAACRobXR4BksA9gAAAbgAAAAibG9jYQlsC24AAAM0AAAAHG1heHAAEQBZAAABOAAAACBuYW1lLaFDVAAACrwAAAFrcG9zdAC1AHoAAAwoAAAAPAABAAAAAQAAqBd2H18PPPUACwQAAAAAANqqufwAAAAA2qq5/AAb/9wB4QMeAAAACAACAAAAAAAAAAEAAAMs/ywAXAH9AAAAAAHhAAEAAAAAAAAAAAAAAAAAAAAEAAEAAAANAFkAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAH1AZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAIABgMAAAAAAAAAAAABEAAAAAAAAAAAAAAAUGZFZADAADAAOQMs/ywAXAMsANQAAAABAAAAAAMYAAAAAAAgAAEBpwAfAAAAAAFVAAAB/QAfAH0ALQA+ABsAPgAyACgAPgAxAAAAAAADAAAAAwAAABwAAQAAAAAAUgADAAEAAAAcAAQANgAAAAQABAABAAAAOf//AAAAL///AAAAAQAEAAAAAAADAAQABQAGAAcACAAJAAoACwAMAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAwQFBgcICQoLDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACYAJgAmAGIAwAEeAZIBzgJAApYC2gNiA7YAAQAf/9wBhwMeABIAAAEGBwYHATAXFjM2NzY3ASYnJjcBgxwLCgH+zgMECxIKCgIBLgEDAwMDHhQFBgP85wMEAQgJBgMOAwMDEwAAAAIAH//9Ad0CkAAQACEAABMWFxYXNjc2NzQnJicGBwYHNyY3NjcWFxYXFAcGByYnJjcfATo6amo7OQE5OmxrOjkBXQIlJEE5IyIBIyJEOSQjAgFOkV5eBAReXoqJXl4EBF5eggJ0UlEDA09Qe3xVVgMDU1OEAAAAAAEAff/9AYACkQA+AAA3FAcGBwYHBiMGFQYXNjc2MzIXFhc2JzQnIicmJyY1JjURNjc2MSYnJicjBgcGBwYVFBUUFxYXNjc2NzIXFhXkAQEEBRgYDAMBBB4ZGhweGxofBAEDDBgZBQQBAQMEAQIDBAIFNTZCAgMDBA0XFw0LBQV3GBMVDAgEBAUKCgUCAQICAQIFCgoFBAQIDBUTGAGnLxkbBAYFAQIZGh4BAgECBQUEAwUHBwEICRYAAAAAAQAtAAAB0QKRADoAADcGFxYXITY3NjcmJyYjIgcGBwYHBisBNjc2NzY3NjUmJyYnBgcGBxQXFhc2NzY3FhcWFxYHBgcGBwYHLgEEAwMBYwURERADBwYFBAMDAg8VEx/LJkBAOhsQDwIxMkxSMjIHCAYGCSYmPTIfHwEBCgoeLkJBQg8EBQQCETAwKQICAgEBBCgUEylJSUYhJicsRDIzAgY1NRoEBQYBEyEhAwEjIjYlJCQtQlBQSAAAAAABAD7/+wG+ApEASgAANwYXFhcWFxYzNjc2NyYnJic2NzY3JicmIwYHBgcUFxYXNjc2NxYXFhcGBwYHBgcUFRQXNjc2NxYXFhcGBwYnIicmJyYnJiciBwYXPwEIBwUaHB0VZU5NBAMvLi8eIB4DAywsKzwrKxgEAwUIHR4wLRscAQMvLz8BAQYKEhEQNSYmAgImJSsWExQPCw0NFREMDQE7DgsLBQwFBgE8PWpMKSoGECQkMkAiIQIdHyUHBwcBCRscAwEbGSpCIyUOAgMCAwwIAwUEAQEoKD9XJSQBBQYODg8PAQ0NFQAAAgAb//oB4QKTACIAJQAANxQXFhchFRQXFjMyNzYjNTM2NzY1NCcmJyMRNCcmIwYHBgcBExEbAgMFASEJCRIdCAkBRgIBAQUEBTwFAwgHCQkG/vjmxgUGBgOwBQIBAwKzAgQDCBAMDQEBlAYGBgEICQf+cwEs/tQAAQA+//sBvgKTAEoAADcGFxYXFhcWMzY3NjcmJyYnIgcGBzY3NjczMjc2NzY3NjU0JyYnBgcGByMGBwYHFBcWMzY3NjMWFxYHBgcGJyInJicmJyYnIgcGFz8BCAcFGhwdFWVOTQQBMjJbFx8gFwoJCQlWKB0dFQ4JCAQDBQMdHSKXCREQEgMCBA4bGhNYJyUBAiYlKxYTFA8LDQ0VEQwNATsOCwsFDAUGATw9akU2NwMFBggrMC8uAgICExcZBgQCAgMBAwQBMVNUWAUFBAYFBAMxMTNZIyQBBQYODg8PAQ0NFQAAAgAy//oBzQKXACAAMwAANxQXFhc2NzY3NicmJyIHBgc2NzY3NCcmJwYHBgcGBwYXNyY3Njc2FxYXFgcGBwYHJicmNzM1NV5aOTsCAioqahoiIRsnWFhFAwIHQ0tMOTAZGQFbBAQaGxkXRB8fAQEfIDE9Hh4E511FRwQDPT1ZPEJBBQwLF4Y9PRMGCwwBEiwsPDZFRkkTHyAbCAcBAjAwREYsLQEFREVQAAAAAAEAKP/7AdUCiwApAAATFhcWMzI3Njc2NzYzIQYHBgcWFxYzMjcBNjc2NzQnJiMiBwYjIQYHBgcoAwYHAwYDAwELEBEdAQUJYWJXAQ8PDgcDAQ4LCQgBAQEEBhUVFv7JBgsNDAH6DQMCAQEFKRITFMjHjQcFBgMCPxYSEwoEAgMBAhkrKiAAAAADAD7/9wG/ApIAKABBAFgAADcGFxYXNjc2NyYnJicmJzQ3Njc2NyYnJiMGBwYHFhcWFxYVFAcGBwYHNyY3Njc2MzIzMhcyFxYXFhcGBwYHIicmNxMmNzY3FhcWFRQHBgcGByIjIicmJyY3PwE1M1ZQODgDAykpMQIBAyYlJQMCMC9HRjExAgIiIiMCAiMvLwNTBBQTKgEBAQECAQIBEjU1CAEdHjMrISICGAMYGSYvGxoTEx8CAQIBBAMfJCQBoU8tLQECMjFPOC4uGwIBAgEWJiU7SCYoAjEwQzopKhMBAgECEykpQAQsIiEbAQEBBywsQjUeHQEiI0QBZSMhIAECJiYvKh8gFAEBAhAfIEYAAAIAMf/6AcsClwAgADMAABMGFxYXMjc2NwYHBgcUFxYXNjc2NzY3NjUmJyYnBgcGBzcmNzY3FhcWFRQHBgcGJyYnJjc0AyopahoiIRsoV1hFAwIHQ0tMODEZGQE2NF5ZOjoBWgMfHzE9Hh4EGhoaF0QeHwUBy0dBQgUMCxeFPj0SBwsLAREsLD01RkVPV0dFBQQ8PU8UPCwtAQVFRUklIRsHCAECMDBPAAAADACWAAEAAAAAAAEABwAQAAEAAAAAAAIABwAoAAEAAAAAAAMABwBAAAEAAAAAAAQABwBYAAEAAAAAAAUAHgCeAAEAAAAAAAYABwDNAAMAAQQJAAEADgAAAAMAAQQJAAIADgAYAAMAAQQJAAMADgAwAAMAAQQJAAQADgBIAAMAAQQJAAUAPABgAAMAAQQJAAYADgC9AEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAFYAZQByAHMAaQBvAG4AIAAxAC4AMAA7ACAARgBvAG4AdABFAGQAaQB0AG8AcgAgACgAdgAxAC4AMAApAABWZXJzaW9uIDEuMDsgRm9udEVkaXRvciAodjEuMCkAAEMAcgBpAG0AcwBvAG4AAENyaW1zb24AAAACAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAABAAIAEwAUABUAFgAXABgAGQAaABsAHAAAAAH//wACAAEAAAAMAAAAFgAAAAIAAQADAAwAAQAEAAAAAgAAAAAAAAABAAAAANWkJwgAAAAA2qq5/AAAAADaqrn8) format("truetype")}:root{color-scheme:light}body{font-family:var(--vp-font)}@media (min-width: 1440px){body{font-size:17px}}h1,h2,h3,h4,h5,h6{font-family:var(--vp-font-heading)}@media (max-width: 419px){h1{font-size:1.9rem}}code{font-family:var(--vp-font-mono)}@media print{@page{--vp-c-bg: #fff !important;--vp-c-text: #000 !important}div[class*=language-]{position:relative!important}}div[class*=language-]{margin:.75rem 0;transition:background-color var(--vp-t-color),color var(--vp-t-color)}@media (max-width: 419px){.theme-hope-content>div[class*=language-]{--code-border-radius: 0;margin:.75rem -1.5rem}}@media print{div[class*=language-] pre code{padding:.5rem}}div[class*=language-] .line.diff,div[class*=language-] .line.highlighted{transition:background-color var(--vp-t-color)}:root{--code-c-text: #383a42;--code-c-bg: #ecf4fa;--code-c-highlight-bg: rgb(215.75625, 233.00625, 245.94375);--code-c-line-number: rgba(56, 58, 66, .67)}[data-theme=dark]{--code-c-text: #abb2bf;--code-c-bg: #282c34;--code-c-highlight-bg: rgb(46.6326086957, 53.1775362319, 66.2673913043);--code-c-line-number: rgba(171, 178, 191, .67)}.vp-external-link-icon:after{content:"";display:inline-block;flex-shrink:0;width:11px;height:11px;margin-top:-1px;margin-left:4px;background:var(--external-link-c-icon);-webkit-mask-image:var(--external-link-icon);mask-image:var(--external-link-icon)}.external-link-icon .external-link:not(.no-external-link-icon):after{content:"";display:inline-block;flex-shrink:0;width:11px;height:11px;margin-top:-1px;margin-left:4px;background:var(--external-link-c-icon);-webkit-mask-image:var(--external-link-icon);mask-image:var(--external-link-icon)}.external-link-icon .theme-hope-content a[href*="://"]:not(.no-external-link-icon):after,.external-link-icon .theme-hope-content a[target=_blank]:not(.no-external-link-icon):after{content:"";display:inline-block;flex-shrink:0;width:11px;height:11px;margin-top:-1px;margin-left:4px;background:var(--external-link-c-icon);-webkit-mask-image:var(--external-link-icon);mask-image:var(--external-link-icon)}.sr-only{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border-width:0;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{.theme-hope-content{margin:0!important;padding-inline:0!important}}.theme-hope-content.custom{margin:0;padding:0}.theme-hope-content:not(.custom){max-width:var(--content-width, 740px);margin:0 auto;padding:2rem 2.5rem}@media (max-width: 959px){.theme-hope-content:not(.custom){padding:1.5rem}}@media (max-width: 419px){.theme-hope-content:not(.custom){padding:1rem 1.5rem}}@media print{.theme-hope-content:not(.custom){max-width:unset}}.theme-hope-content:not(.custom){padding-top:0}.theme-hope-content:not(.custom)>h1,.theme-hope-content:not(.custom)>h2,.theme-hope-content:not(.custom)>h3,.theme-hope-content:not(.custom)>h4,.theme-hope-content:not(.custom)>h5,.theme-hope-content:not(.custom)>h6{margin-top:calc(.5rem - var(--navbar-height));margin-bottom:.5rem;padding-top:calc(1rem + var(--navbar-height));outline:none}.theme-container.no-navbar .theme-hope-content:not(.custom)>h1,.theme-container.no-navbar .theme-hope-content:not(.custom)>h2,.theme-container.no-navbar .theme-hope-content:not(.custom)>h3,.theme-container.no-navbar .theme-hope-content:not(.custom)>h4,.theme-container.no-navbar .theme-hope-content:not(.custom)>h5,.theme-container.no-navbar .theme-hope-content:not(.custom)>h6{margin-top:1.5rem;padding-top:0}.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:justify;overflow-wrap:break-word;-webkit-hyphens:auto;hyphens:auto}@media (max-width: 419px){.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:start}}@media print{.theme-hope-content:not(.custom)>p,.theme-hope-content:not(.custom)>ul p,.theme-hope-content:not(.custom)>ol p{text-align:start}}.theme-hope-content a:not(.header-anchor):hover{text-decoration:underline}.theme-hope-content img{max-width:100%}.theme-hope-content table img{max-width:unset}::view-transition-old(root),::view-transition-new(root){mix-blend-mode:normal;animation:none}[data-theme=light]::view-transition-old(root),[data-theme=dark]::view-transition-new(root){z-index:1}[data-theme=light]::view-transition-new(root),[data-theme=dark]::view-transition-old(root){z-index:99999}#docsearch-container{min-width:145.7px!important}@media (max-width: 959px){#docsearch-container{min-width:36px!important}}.DocSearch.DocSearch-Button{margin-left:0}@media (max-width: 959px){.DocSearch.DocSearch-Button{min-width:36px!important}}.DocSearch .DocSearch-Button-Placeholder{display:inline-block;padding:4px 12px 4px 6px;font-size:14px}@media (max-width: 719px){.DocSearch .DocSearch-Button-Placeholder{display:none}}.DocSearch .DocSearch-Search-Icon{width:1.25em;height:1.25em}@media (max-width: 959px){.DocSearch .DocSearch-Button-Keys{display:none}}.DocSearch .DocSearch-Button-Key{background:var(--vp-c-bg);box-shadow:none}@media (max-width: 419px){.hint-container{margin-inline:-.75rem}}:root{scrollbar-width:thin}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track-piece{border-radius:6px;background:#0000001a}::-webkit-scrollbar-thumb{border-radius:6px;background:var(--vp-c-accent-bg)}::-webkit-scrollbar-thumb:active{background:var(--vp-c-accent-hover)}@media (max-width: 719px){.hide-in-mobile{display:none!important}}@media (max-width: 959px){.hide-in-pad{display:none!important}}@media (min-width: 1440px){body{font-size:16px}}html[data-theme=dark]{--text-color: #d1d1d1}.giscus-wrapper.input-top .giscus{margin-bottom:-3rem}@media print{.vp-comment{display:none!important}}.vp-site-info{position:relative;display:inline-block;overflow:hidden;width:calc(50% - 16px);margin:4px 8px;border-radius:8px;color:inherit;box-shadow:1px 1px 8px var(--vp-c-shadow);cursor:pointer;transition:box-shadow var(--vp-t-transform)}@media (max-width: 959px){.vp-site-info{width:calc(100% - 24px)}}.vp-site-info:hover{box-shadow:1px 4px 16px var(--vp-c-shadow)}.vp-site-info:hover:after{content:attr(data-name);position:absolute;top:.5rem;left:.5rem;display:block;padding:.5rem;border-radius:.25em;background:#37373780;color:#fff}.vp-site-info:before{content:"";display:inline-block;width:0;padding-top:55%}.vp-site-info-navigator{position:absolute;top:0;right:0;bottom:0;left:0;z-index:2;display:block}.vp-site-info-preview{position:absolute;top:0;right:0;bottom:0;left:0}.vp-site-info-detail{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-flow:column;align-items:center;justify-content:center;padding:.5rem 1rem;text-align:center;opacity:1;transition:opacity ease .6s}.vp-site-info-detail:before{content:" ";position:absolute;top:0;right:0;bottom:0;left:0;background-color:var(--vp-c-bg);opacity:.75}.vp-site-info:hover .vp-site-info-detail{opacity:0}.vp-site-info-logo{z-index:1;height:3rem;margin:0 auto}.vp-site-info-name{z-index:1;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--vp-c-text);color:var(--black);font-weight:700;font-size:20px}.vp-site-info-desc{z-index:1;flex-shrink:1;overflow:hidden;color:var(--black);font-size:15px;line-height:1.3;text-overflow:ellipsis}.vp-site-info-source-wrapper{position:absolute;inset-inline-end:16px;bottom:16px;z-index:3}.vp-site-info-source{display:inline-block;width:20px;height:20px;padding:4px;border-radius:50%;background:var(--black);transition:transform var(--vp-t-transform)}.vp-site-info-source:before{display:none}.vp-site-info-source:hover{transform:scale(1.05)}.vp-site-info-source .icon{width:100%;height:100%;color:var(--vp-c-white)}@media print{.bilibili-desc a{display:block}}.bilibili-iframe{margin:8px 0;border:none;border-radius:8px}@media print{.bilibili-iframe{display:none}}@media screen{.sr-only{position:absolute;overflow:hidden;clip:rect 0,0,0,0;width:1px;height:1px;margin:-1px;padding:0;border:0}}@media print{.sr-only{display:none}} diff --git a/assets/synchronized.html-B2MTfSju.js b/assets/synchronized.html-B2MTfSju.js new file mode 100644 index 00000000..1755b63f --- /dev/null +++ b/assets/synchronized.html-B2MTfSju.js @@ -0,0 +1,59 @@ +import{_ as s,c as n,d as a,o as e}from"./app-ftEjETWs.js";const t={};function l(h,i){return e(),n("div",null,i[0]||(i[0]=[a(`

synchronized

偏向锁在JDK 15后已经废弃

一、什么是synchronized

关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。

img
img

学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:

  • 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。

关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。

具体使用规则如下:
在这里插入图片描述

二、Synchronized 原理

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

流程图如下:
在这里插入图片描述

通过一段代码来演示:

public static void main(String[] args) {
+    synchronized (Synchronize.class){
+        System.out.println("Synchronize");
+    }
+}
+12345

使用javap -c Synchronize可以查看编译之后的具体信息。
在这里插入图片描述
可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
在这里插入图片描述
synchronized的特点:
在这里插入图片描述

三、Synchronized 优化

从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

3.1 偏向锁

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

3.1.1 偏向锁的获取过程

(1)访问Mark Word中偏向锁的标识是否设置成“1”,锁标志位是否为“01”——确认为可偏向状态。
(2)如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。

3.1.2 偏向锁的释放

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3.2 轻量锁

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

3.2.1 轻量级锁的加锁过程

(1)在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
(4)如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
(5)如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

3.2.2 轻量级锁的解锁过程

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程完成。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

3.3 其他优化

适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6 加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

通过--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。
通过--XX:PreBlockSpin修改自旋次数,默认值是10次。

锁消除:锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

锁粗化:我们在写代码时推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

注意:在大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

四、扩展

其他控制并发/线程同步方式还有 Lock/ReentrantLock。

4.1 Synchronized 和 ReenTrantLock 的对比

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized依赖于JVM而ReenTrantLock依赖于API

synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReenTrantLock比synchronized增加了一些高级功能

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的,可以通过ReenTrantLoc类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

④ 性能已不是选择标准

在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平。所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销。

// CAS的操作参数
+// 内存位置(A)
+// 预期原值(B)
+// 预期新值(C)
+
+// 使用CAS解决并发的原理:
+// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
+// 2. 通过死循环,以不断尝试尝试更新的方式实现并发
+
+// 伪代码如下
+public boolean compareAndSwap(long memoryA, int oldB, int newC){
+    if(memoryA.get() == oldB){
+        memoryA.set(newC);
+        return true;
+    }
+    return false;
+}
+1234567891011121314151617

具体使用当中CAS有个先检查后执行的操作,而这种操作在 Java 中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
具体过程:

  1. CAS在Java中的体现为Unsafe类。
  2. Unsafe类会通过C++直接获取到属性的内存地址。
  3. 接下来CAS由C++的Atomic::cmpxchg系列方法实现。

AtomicInteger的 i++ 与 i-- 是典型的CAS应用,通过compareAndSet & 一个死循环实现。

private volatile int value; 
+/** 
+* Gets the current value. 
+* 
+* @return the current value 
+*/ 
+public final int get() { 
+   return value; 
+} 
+/** 
+* Atomically increments by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndIncrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current + 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+} 
+
+/** 
+* Atomically decrements by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndDecrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current - 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+}
+123456789101112131415161718192021222324252627282930313233343536

以上内容引用自学习指南
总的来说:
1、synchronized是java关键字,而Lock是java中的一个接口
2、synchronized会自动释放锁,而Lock必须手动释放锁
3、synchronized是不可中断的,Lock可以中断也可以不中断
4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
6、Lock可以使用读锁提高多线程读效率
7、synchronized是非公平锁,ReentranLock可以控制是否公平锁

4.2 Synchronized 与 ThreadLocal 的对比

Synchronized 与 ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:

  1. Synchronized关键字主要解决多线程共享数据同步问题;ThreadLocal主要解决多线程中数据因并发产生不一致问题。
  2. Synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

4.3 synchronized与volatile区别

volatilesynchronized
通过禁止 CPU 缓存优化来保证变量的可见性通过加锁和解锁来保证同步性和原子性
只保证变量的可见性,还可以禁止指令重排保证变量的可见性与原子性
volatile修饰变量,仅用于变量级synchronized锁变量或代码段,锁级
不会造成线程阻塞会造成线程阻塞
只是禁止了缓存优化,因此其开销相对较小需要加锁、解锁等额外的操作,因此其开销相对较大

五、各种锁

公平锁:是指多个线程按照申请锁的顺序来获取锁。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
可重入锁:是指可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提是同一个对象或者class),这样的锁就叫做可重入锁。Lock和synchronized都是可重入锁
独享锁 :该锁每一次只能被一个线程所持有。
共享锁 :该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
互斥锁 :在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
读写锁 :读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的
乐观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
悲观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环 。

参考

1.MarkWord和Synchronized的锁升级机制详解(JDK8)
2.synchronized 关键字原理
3.Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
4.Java:这是一份全面 & 详细的 Sychronized关键字 学习指南

`,68)]))}const p=s(t,[["render",l],["__file","synchronized.html.vue"]]),k=JSON.parse('{"path":"/java/%E7%BA%BF%E7%A8%8B/synchronized.html","title":"synchronized","lang":"zh-CN","frontmatter":{"description":"synchronized 偏向锁在JDK 15后已经废弃 一、什么是synchronized 关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。 imgimg 学习Java的小伙伴都知道sync...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"synchronized"}],["meta",{"property":"og:description","content":"synchronized 偏向锁在JDK 15后已经废弃 一、什么是synchronized 关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。 imgimg 学习Java的小伙伴都知道sync..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/v2-47781295251ded0e8ff32cf6a73fbfd0_1440w.webp"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-17T16:08:07.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-17T16:08:07.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"synchronized\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/v2-47781295251ded0e8ff32cf6a73fbfd0_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNjc1OTQwNQ==,size_16,color_FFFFFF,t_70.png\\",\\"https://github-images.wenzhihuai.com/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNjc1OTQwNQ==,size_16,color_FFFFFF,t_70.jpeg\\",\\"https://github-images.wenzhihuai.com/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNjc1OTQwNQ==,size_16,color_FFFFFF,t_70-20240217012902416.png\\",\\"https://github-images.wenzhihuai.com/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNjc1OTQwNQ==,size_16,color_FFFFFF,t_70-20240217012902463.png\\",\\"https://github-images.wenzhihuai.com/images/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zNjc1OTQwNQ==,size_16,color_FFFFFF,t_70-20240217012902480.png\\"],\\"dateModified\\":\\"2024-02-17T16:08:07.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、什么是synchronized","slug":"一、什么是synchronized","link":"#一、什么是synchronized","children":[]},{"level":2,"title":"二、Synchronized 原理","slug":"二、synchronized-原理","link":"#二、synchronized-原理","children":[]},{"level":2,"title":"三、Synchronized 优化","slug":"三、synchronized-优化","link":"#三、synchronized-优化","children":[{"level":3,"title":"3.1 偏向锁","slug":"_3-1-偏向锁","link":"#_3-1-偏向锁","children":[]},{"level":3,"title":"3.2 轻量锁","slug":"_3-2-轻量锁","link":"#_3-2-轻量锁","children":[]},{"level":3,"title":"3.3 其他优化","slug":"_3-3-其他优化","link":"#_3-3-其他优化","children":[]}]},{"level":2,"title":"四、扩展","slug":"四、扩展","link":"#四、扩展","children":[{"level":3,"title":"4.1 Synchronized 和 ReenTrantLock 的对比","slug":"_4-1-synchronized-和-reentrantlock-的对比","link":"#_4-1-synchronized-和-reentrantlock-的对比","children":[]},{"level":3,"title":"4.2 Synchronized 与 ThreadLocal 的对比","slug":"_4-2-synchronized-与-threadlocal-的对比","link":"#_4-2-synchronized-与-threadlocal-的对比","children":[]},{"level":3,"title":"4.3 synchronized与volatile区别","slug":"_4-3-synchronized与volatile区别","link":"#_4-3-synchronized与volatile区别","children":[]}]},{"level":2,"title":"五、各种锁","slug":"五、各种锁","link":"#五、各种锁","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1708075355000,"updatedTime":1708186087000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":16.38,"words":4915},"filePathRelative":"java/线程/synchronized.md","localizedDate":"2024年2月16日","excerpt":"\\n

偏向锁在JDK 15后已经废弃

\\n

一、什么是synchronized

","autoDesc":true}');export{p as comp,k as data}; diff --git a/assets/tesla.html-K0riacrL.js b/assets/tesla.html-K0riacrL.js new file mode 100644 index 00000000..42edc2ed --- /dev/null +++ b/assets/tesla.html-K0riacrL.js @@ -0,0 +1,23 @@ +import{_ as s,c as a,d as e,o as n}from"./app-ftEjETWs.js";const t={};function l(h,i){return n(),a("div",null,i[0]||(i[0]=[e(`

Tesla api

一、特斯拉应用申请

1.1 创建 Tesla 账户

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.

image-20231210142130054image-20231210142415598

注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。

1.2 提交访问请求

点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。

这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑

(1)Invalid domain

image-20231210144504486

无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。

(2)Unable to Onboard

image-20231210142930976

应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。

(3) Rejected

image-20231210143156308

这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。

1.3 访问应用程序凭据

一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。

申请好就可以在自己的账号下看到自己的应用了,

image-20231210144244888

1.4 开始 API 集成

按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。

二、开发之前的准备

由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。

2.1 认识token

app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:

(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌

(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。

此外,还需要注意下,中国大陆地区对应的api地址是 https://fleet-api.prd.cn.vn.cloud.tesla.cn,不要调到别的地址去了。

2.2 获取第三方应用token

这一步官网列的很详细,就不在详述了。

CLIENT_ID=<command to obtain a client_id>
+CLIENT_SECRET=<secure command to obtain a client_secret>
+AUDIENCE="https://fleet-api.prd.na.vn.cloud.tesla.com"
+# Partner authentication token request
+curl --request POST \\
+  --header 'Content-Type: application/x-www-form-urlencoded' \\
+  --data-urlencode 'grant_type=client_credentials' \\
+  --data-urlencode "client_id=$CLIENT_ID" \\
+  --data-urlencode "client_secret=$CLIENT_SECRET" \\
+  --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \\
+  --data-urlencode "audience=$AUDIENCE" \\
+  'https://auth.tesla.com/oauth2/v3/token'

2.3 验证域名归属

2.1.1 Register

完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。

cd vehicle-command/cmd/tesla-keygen
+go build .
+./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem

这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。

    location ~ ^/.well-known {
+           default_type text/html;
+           alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;
+    }
+

随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。

curl --header 'Content-Type: application/json' \\
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \\
+  --data '{"domain":"string"}' \\
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'

2.1.2 public_key

最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)

curl --header 'Content-Type: application/json' \\
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \\
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'

得到下面内容就代表成功了

{"response":{"public_key":"xxxx"}}

至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。

`,50)]))}const r=s(t,[["render",l],["__file","tesla.html.vue"]]),k=JSON.parse('{"path":"/interesting/tesla.html","title":"Tesla api","lang":"zh-CN","frontmatter":{"description":"Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator. image-20231210142130054 image-2023121014241559...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/tesla.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Tesla api"}],["meta",{"property":"og:description","content":"Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator. image-20231210142130054 image-2023121014241559..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Tesla api\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、特斯拉应用申请","slug":"一、特斯拉应用申请","link":"#一、特斯拉应用申请","children":[{"level":3,"title":"1.1 创建 Tesla 账户","slug":"_1-1-创建-tesla-账户","link":"#_1-1-创建-tesla-账户","children":[]},{"level":3,"title":"1.2 提交访问请求","slug":"_1-2-提交访问请求","link":"#_1-2-提交访问请求","children":[]},{"level":3,"title":"1.3 访问应用程序凭据","slug":"_1-3-访问应用程序凭据","link":"#_1-3-访问应用程序凭据","children":[]},{"level":3,"title":"1.4 开始 API 集成","slug":"_1-4-开始-api-集成","link":"#_1-4-开始-api-集成","children":[]},{"level":3,"title":"2.1 认识token","slug":"_2-1-认识token","link":"#_2-1-认识token","children":[]},{"level":3,"title":"2.2 获取第三方应用token","slug":"_2-2-获取第三方应用token","link":"#_2-2-获取第三方应用token","children":[]},{"level":3,"title":"2.3 验证域名归属","slug":"_2-3-验证域名归属","link":"#_2-3-验证域名归属","children":[]}]}],"git":{"createdTime":1706186538000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":4.5,"words":1351},"filePathRelative":"interesting/tesla.md","localizedDate":"2024年1月25日","excerpt":"\\n

一、特斯拉应用申请

\\n

1.1 创建 Tesla 账户

\\n

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

","autoDesc":true}');export{r as comp,k as data}; diff --git a/assets/tiktok2023.html-BxK5gJWF.js b/assets/tiktok2023.html-BxK5gJWF.js new file mode 100644 index 00000000..ec623f6f --- /dev/null +++ b/assets/tiktok2023.html-BxK5gJWF.js @@ -0,0 +1 @@ +import{_ as e,c as r,d as a,o as n}from"./app-ftEjETWs.js";const c={};function i(o,t){return n(),r("div",null,t[0]||(t[0]=[a("

tt一面:
1.全程项目
2.lc3,最长无重复子串,滑动窗口解决

tt二面:
全程基础,一直追问
1.java内存模型介绍一下
2.volatile原理
3.内存屏障,使用场景?(我提了在gc中有使用)
4.gc中具体是怎么使用内存屏障的,详细介绍
5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
6.线程内存分配方式,tlab相关
7.happens-before介绍一下
8.总线风暴是什么,怎么解决
9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

tt三面:
大部分聊项目,基础随便问了问
1.分布式事务,两阶段三阶段流程,区别
2.mysql主从,同步复制,半同步复制,异步复制
3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转
4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万

",3)]))}const p=e(c,[["render",i],["__file","tiktok2023.html.vue"]]),s=JSON.parse('{"path":"/interview/tiktok2023.html","title":"","lang":"zh-CN","frontmatter":{"description":"tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interview/tiktok2023.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:description","content":"tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-24T03:25:58.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-24T03:25:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-24T03:25:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1680437706000,"updatedTime":1706066758000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":1.15,"words":344},"filePathRelative":"interview/tiktok2023.md","localizedDate":"2023年4月2日","excerpt":"

tt一面:
\\n1.全程项目
\\n2.lc3,最长无重复子串,滑动窗口解决

\\n

tt二面:
\\n全程基础,一直追问
\\n1.java内存模型介绍一下
\\n2.volatile原理
\\n3.内存屏障,使用场景?(我提了在gc中有使用)
\\n4.gc中具体是怎么使用内存屏障的,详细介绍
\\n5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
\\n6.线程内存分配方式,tlab相关
\\n7.happens-before介绍一下
\\n8.总线风暴是什么,怎么解决
\\n9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
\\n10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
\\n11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

","autoDesc":true}');export{p as comp,s as data}; diff --git a/assets/volatile.html-C-UWQAjc.js b/assets/volatile.html-C-UWQAjc.js new file mode 100644 index 00000000..500490a8 --- /dev/null +++ b/assets/volatile.html-C-UWQAjc.js @@ -0,0 +1 @@ +import{_ as a,c as t,d as r,o as i}from"./app-ftEjETWs.js";const o={};function n(l,e){return i(),t("div",null,e[0]||(e[0]=[r('

volatile

Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

image-20240217004404932
image-20240217004404932

可见性

有序性

',5)]))}const h=a(o,[["render",n],["__file","volatile.html.vue"]]),s=JSON.parse('{"path":"/java/%E7%BA%BF%E7%A8%8B/volatile.html","title":"volatile","lang":"zh-CN","frontmatter":{"description":"volatile Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。 image-20240217004404932imag...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"volatile"}],["meta",{"property":"og:description","content":"volatile Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。 image-20240217004404932imag..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20240217004404932.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T17:21:55.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T17:21:55.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"volatile\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20240217004404932.png\\"],\\"dateModified\\":\\"2024-02-16T17:21:55.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"可见性","slug":"可见性","link":"#可见性","children":[]},{"level":2,"title":"有序性","slug":"有序性","link":"#有序性","children":[]}],"git":{"createdTime":1708056026000,"updatedTime":1708104115000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.54,"words":161},"filePathRelative":"java/线程/volatile.md","localizedDate":"2024年2月16日","excerpt":"\\n

Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

","autoDesc":true}');export{h as comp,s as data}; diff --git a/assets/webflux.html-KjGFiVkN.js b/assets/webflux.html-KjGFiVkN.js new file mode 100644 index 00000000..dbe0e5cb --- /dev/null +++ b/assets/webflux.html-KjGFiVkN.js @@ -0,0 +1 @@ +import{_ as t,c as l,d as a,o as r}from"./app-ftEjETWs.js";const o={};function p(n,e){return r(),l("div",null,e[0]||(e[0]=[a('

Webflux

1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?

ChatGpt:
Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

Servlet 3.1:虽然 Servlet 3.1 支持非阻塞 IO,但是它的 API 设计仍然是基于传统的阻塞模型,这意味着每个请求通常会在一个单独的线程中处理,直到请求处理完成并返回响应。这种模型在并发请求数量较少的情况下工作得很好,但是在高并发的情况下,可能会因为线程数量过多而导致资源消耗过大,性能下降。

WebFlux:WebFlux 是基于反应式编程模型的,它使用少量的线程(通常与 CPU 核心数相同)和事件驱动的方式来处理大量的并发请求。这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

所以,虽然 Servlet 3.1 和 WebFlux 底层都使用了 NIO,但是由于他们的编程模型和如何使用 NIO 的方式不同,WebFlux 在处理高并发、流式数据等场景时,可能会有更好的性能。但是,这并不意味着 WebFlux 在所有情况下都比 Servlet 3.1 有更好的性能,具体还需要根据应用的实际需求和场景来选择合适的技术。

webflux和springboot虚拟线程对比

ChatGpt:
WebFlux 和 Spring Boot 使用虚拟线程(也称为纤程或轻量级线程)是两种不同的并发模型,它们各自有各自的优点和适用场景。

WebFlux:WebFlux 是 Spring 5 的一部分,它提供了一个基于反应式编程模型的非阻塞 Web 框架。WebFlux 使用少量的线程和事件驱动的方式来处理大量的并发请求,这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

Spring Boot 使用虚拟线程:Java 在其后续版本(如 Java 16)中引入了虚拟线程(Project Loom)的概念,这是一种新的并发模型,它可以创建大量的虚拟线程,每个虚拟线程都有自己的调用栈,但是它们在操作系统级别并不是真正的线程,因此创建和切换的成本更低。虚拟线程使得开发者可以使用传统的阻塞式编程模型,但是在运行时,这些阻塞操作并不会阻塞真正的物理线程,从而可以处理大量的并发请求。

WebFlux 和 Spring Boot 使用虚拟线程的主要区别在于它们的编程模型:WebFlux 使用反应式编程模型,而虚拟线程则使用更传统的阻塞式编程模型。虚拟线程的优点是编程模型更简单,更接近传统的 Java 开发方式,但是它需要更新的 Java 版本,并且目前还在实验阶段。WebFlux 的优点是它可以在任何支持 Java 8 或更高版本的环境中运行,但是它需要开发者熟悉反应式编程模型。

至于哪种模型的性能更好,这取决于具体的应用场景和负载。在某些情况下,WebFlux 可能会有更好的性能,而在其他情况下,使用虚拟线程可能会更有效。

',12)]))}const u=t(o,[["render",p],["__file","webflux.html.vue"]]),b=JSON.parse('{"path":"/java/SpringBoot/webflux.html","title":"Webflux","lang":"zh-CN","frontmatter":{"description":"Webflux 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? ChatGpt: Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。 Servlet 3.1:虽然 Servlet 3.1 支持非阻塞...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/SpringBoot/webflux.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Webflux"}],["meta",{"property":"og:description","content":"Webflux 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? ChatGpt: Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。 Servlet 3.1:虽然 Servlet 3.1 支持非阻塞..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-27T09:51:43.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-27T09:51:43.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Webflux\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-27T09:51:43.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?","slug":"_1-servlet-3-1就已经支持异步非阻塞-为什么要用webflux","link":"#_1-servlet-3-1就已经支持异步非阻塞-为什么要用webflux","children":[]},{"level":3,"title":"webflux和springboot虚拟线程对比","slug":"webflux和springboot虚拟线程对比","link":"#webflux和springboot虚拟线程对比","children":[]}],"git":{"createdTime":1706349103000,"updatedTime":1706349103000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.07,"words":921},"filePathRelative":"java/SpringBoot/webflux.md","localizedDate":"2024年1月27日","excerpt":"\\n

1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?

\\n

ChatGpt:
\\nServlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

","autoDesc":true}');export{u as comp,b as data}; diff --git a/assets/wewe.html-BLY5_jfL.js b/assets/wewe.html-BLY5_jfL.js new file mode 100644 index 00000000..39fd7b55 --- /dev/null +++ b/assets/wewe.html-BLY5_jfL.js @@ -0,0 +1 @@ +import{_ as a,c as i,a as e,o}from"./app-ftEjETWs.js";const n={};function r(p,t){return o(),i("div",null,t[0]||(t[0]=[e("h1",{id:"自我介绍",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#自我介绍"},[e("span",null,"自我介绍")])],-1),e("p",null,"安安静静的开发,搞点好玩的。",-1),e("figure",null,[e("img",{src:"https://github-images.wenzhihuai.com/images/webp.jpg",alt:"硅谷",tabindex:"0",loading:"lazy"}),e("figcaption",null,"硅谷")],-1)]))}const m=a(n,[["render",r],["__file","wewe.html.vue"]]),h=JSON.parse('{"path":"/about-the-author/personal-life/wewe.html","title":"自我介绍","lang":"zh-CN","frontmatter":{"description":"自我介绍 安安静静的开发,搞点好玩的。 硅谷硅谷","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"自我介绍"}],["meta",{"property":"og:description","content":"自我介绍 安安静静的开发,搞点好玩的。 硅谷硅谷"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/webp.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-03T17:57:14.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-03T17:57:14.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"自我介绍\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/webp.jpg\\"],\\"dateModified\\":\\"2024-02-03T17:57:14.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706244827000,"updatedTime":1706983034000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":0.07,"words":21},"filePathRelative":"about-the-author/personal-life/wewe.md","localizedDate":"2024年1月26日","excerpt":"\\n

安安静静的开发,搞点好玩的。

\\n
\\"硅谷\\"
硅谷
","autoDesc":true}');export{m as comp,h as data}; diff --git a/assets/zgc.html-Cj33F57G.js b/assets/zgc.html-Cj33F57G.js new file mode 100644 index 00000000..b188b846 --- /dev/null +++ b/assets/zgc.html-Cj33F57G.js @@ -0,0 +1,4 @@ +import{_ as a,c as i,d as t,o as n}from"./app-ftEjETWs.js";const r={};function p(s,e){return n(),i("div",null,e[0]||(e[0]=[t(`

ZGC

本文转载自12 张图带你彻底理解 ZGC

ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。

ZGC 有 3 个重要特性:

  • 暂停时间不会超过 10 ms。

JDK 16 发布后,GC 暂停时间已经缩小到 1 ms 以内,并且时间复杂度是 o(1),这也就是说 GC 停顿时间是一个固定值了,并不会受堆内存大小影响。
下面图片来自:https://malloc.se/blog/zgc-jdk16

img
img
  • 最大支持 16TB 的大堆,最小支持 8MB 的小堆。
  • 跟 G1 相比,对应用程序吞吐量的影响小于 15 %。

1 内存多重映射

内存多重映射,就是使用 mmap 把不同的虚拟内存地址映射到同一个物理内存地址上。如下图:

img
img

ZGC 为了更灵活高效地管理内存,使用了内存多重映射,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。

Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

2 染色指针

2.1 三色标记回顾

我们知道 G1 垃圾收集器使用了三色标记,这里先做一个回顾。下面是一个三色标记过程中的对象引用示例图:

img
img

总共有三种颜色,说明如下:

  • 白色:本对象还没有被标记线程访问过。
  • 灰色:本对象已经被访问过,但是本对象引用的其他对象还没有被全部访问。
  • 黑色:本对象已经被访问过,并且本对象引用的其他对象也都被访问过了。

三色标记的过程如下:

  1. 初始阶段,所有对象都是白色。
  2. 将 GC Roots 直接引用的对象标记为灰色。
  3. 处理灰色对象,把当前灰色对象引用的所有对象都变成灰色,之后将当前灰色对象变成黑色。
  4. 重复步骤 3,直到不存在灰色对象为止。

三色标记结束后,白色对象就是没有被引用的对象(比如上图中的 H 和 G),可以被回收了。

2.2 染色指针

ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如下图:

img
img

前 62位保存了 GC 信息,最后两位保存了锁标志。

ZGC 的一大创举是将 GC 信息保存在了染色指针上。染色指针是一种将少量信息直接存储在指针上的技术。在 64 位 JVM 中,对象指针是 64 位,如下图:

img
img

在这个 64 位的指针上,高 16 位都是 0,暂时不用来寻址。剩下的 48 位支持的内存可以达到 256 TB(2 ^48),这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 48 位都用来保存对象信息,而是用高 4 位保存了四个标志位,这样 ZGC 可以管理的最大内存可以达到 16 TB(2 ^ 44)。

通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。

无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。

3 内存布局

首先我们回顾一下 G1 垃圾收集器的内存布局。G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:

img
img

跟 G1 类似,ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的。不同的是,ZGC 的 Region 支持动态地创建和销毁,并且 Region 的大小不是固定的,包括三种类型的 Region :

  • Small Region:2MB,主要用于放置小于 256 KB 的小对象。
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
  • Large Region:N * 2MB。这个类型的 Region 是可以动态变化的,不过必须是 2MB 的整数倍,最小支持 4 MB。每个 Large Region 只放置一个大对象,并且是不会被重分配的。

4 读屏障

读屏障类似于 Spring AOP 的前置增强,是 JVM 向应用代码中插入一小段代码,当应用线程从堆中读取对象的引用时,会先执行这段代码。注意:只有从堆内存中读取对象的引用时,才会执行这个代码。下面代码只有第一行需要加入读屏障。

Object o = obj.FieldA
+Object p = o //不是从堆中读取引用
+o.dosomething() //不是从堆中读取引用
+int i =  obj.FieldB //不是引用类型

读屏障在解释执行时通过 load 相关的字节码指令加载数据。作用是在对象标记和转移过程中,判断对象的引用地址是否满足条件,并作出相应动作。如下图:

img
img

标记、转移和重定位这些过程请看下一节。

读屏障会对应用程序的性能有一定影响,据测试,对性能的最高影响达到 4%,但提高了 GC 并发能力,降低了 STW。

5 GC 过程

前面已经讲过,ZGC 使用内存多重映射技术,把物理内存映射为 Marked0、Marked1 和 Remapped 三个地址视图,利用地址视图的切换,ZGC 实现了高效的并发收集。

ZGC 的垃圾收集过程包括标记、转移和重定位三个阶段。如下图:

img
img

ZGC 初始化后,整个内存空间的地址视图被设置为 Remapped。

5.1 初始标记

从 GC Roots 出发,找出 GC Roots 直接引用的对象,放入活跃对象集合,这个过程需要 STW,不过 STW 的时间跟 GC Roots 数量成正比,耗时比较短。

5.2 并发标记

并发标记过程中,GC 线程和 Java 应用线程会并行运行。这个过程需要注意下面几点:

  • GC 标记线程访问对象时,如果对象地址视图是 Remapped,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理。
  • 标记过程中Java 应用线程新创建的对象会直接进入 Marked0 视图。
  • 标记过程中Java 应用线程访问对象时,如果对象的地址视图是 Remapped,就把对象地址视图切换到 Marked0,可以参考前面讲的读屏障。
  • 标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。

标记阶段的活跃视图也可能是 Marked1,为什么会采用两个视图呢?

这里采用两个视图是为了区分前一次标记和这一次标记。如果这次标记的视图是 Marked0,那下一次并发标记就会把视图切换到 Marked1。这样做可以配合 ZGC 按照页回收垃圾的做法。如下图:

img
img

第二次标记的时候,如果还是切换到 Marked0,那么 2 这个对象区分不出是活跃的还是上次标记过的。如果第二次标记切换到 Marked1,就可以区分出了。

这时 Marked0 这个视图的对象就是上次标记过程被标记过活跃,转移的时候没有被转移,但这次标记没有被标记为活跃的对象。Marked1 视图的对象是这次标记被标记为活跃的对象。Remapped 视图的对象是上次垃圾回收发生转移或者是被 Java 应用线程访问过,本次垃圾回收中被标记为不活跃的对象。

5.3 再标记

并发标记阶段 GC 线程和 Java 应用线程并发执行,标记过程中可能会有引用关系发生变化而导致的漏标记问题。再标记阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软应用,虚引用等)进行并行标记。

这个阶段需要 STW,但是需要标记的对象少,耗时很短。

5.4 初始转移

转移就是把活跃对象复制到新的内存,之前的内存空间可以被回收。

初始转移需要扫描 GC Roots 直接引用的对象并进行转移,这个过程需要 STW,STW 时间跟 GC Roots 成正比。

5.5 并发转移

并发转移过程 GC 线程和 Java 线程是并发进行的。上面已经讲过,转移过程中对象视图会被切回 Remapped 。转移过程需要注意以下几点:

  • 如果 GC 线程访问对象的视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • 如果 GC 线程访问对象的视图是 Remapped,说明被其他 GC 线程处理过,跳过不再处理。
  • 并发转移过程中 Java 应用线程创建的新对象地址视图是 Remapped。
  • 如果 Java 应用线程访问的对象被标记为活跃并且对象视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。

5.6 重定位

转移过程对象的地址发生了变化,在这个阶段,把所有指向对象旧地址的指针调整到对象的新地址上。

6 垃圾收集算法

ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象。如下图:

img
img

6.1 JDK 16 之前

在 JDK 16 之前,ZGC 会预留(Reserve)一块儿堆内存,这个预留内存不能用于 Java 线程的内存分配。即使从 Java 线程的角度看堆内存已经满了也不能使用 Reserve,只有 GC 过程中搬移存活对象的时候才可以使用。如下图:

img
img

这样做的好处是算法简单,非常适合并行收集。但这样做有几个问题:

  • 因为有预留内存,能给 Java 线程分配的堆内存小于 JVM 声明的堆内存。
  • Reserve 仅仅用于存放 GC 过程中搬移的对象,有点内存浪费。
  • 因为 Reserve 不能给 GC 过程中搬移对象的 Java 线程使用,搬移线程可能会因为申请不到足够内存而不能完成对象搬移,这返回过来又会导致应用程序的 OOM。

6.2 JDK 16 改进

JDK 16 发布后,ZGC 支持就地搬移对象(G1 在 Full GC 的时候也是就地搬移)。这样做的好处是不用预留空闲内存了。如下图:

img
img

不过就地搬移也有一定的挑战。比如:必须考虑搬移对象的顺序,否则可能会覆盖尚未移动的对象。这就需要 GC 线程之间更好的进行协作,不利于并发收集,同时也会导致搬移对象的 Java 线程需要考虑什么可以做什么不可以做。

为了获得更好的 GC 表现,JDK 16 在支持就地搬移的同时,也支持预留(Reserve)堆内存的方式,并且 ZGC 不需要真的预留空闲的堆内存。默认情况下,只要有空闲的 region,ZGC 就会使用预留堆内存的方式,如果没有空闲的 region,否则 ZGC 就会启用就地搬移。如果有了空闲的 region, ZGC 又会切换到预留堆内存的搬移方式。

7 总结

内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。

ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。

ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。

参考:

1.https://wiki.openjdk.java.net/display/zgc

2.https://openjdk.java.net/jeps/304

3.https://openjdk.java.net/jeps/376

4.https://malloc.se/blog/zgc-jdk16

5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg

6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw

7.https://www.jianshu.com/p/664e4da05b2c

8.https://www.cnblogs.com/jimoer/p/13170249.html

9.https://www.jianshu.com/p/12544c0ad

`,97)]))}const o=a(r,[["render",p],["__file","zgc.html.vue"]]),g=JSON.parse('{"path":"/java/JVM/zgc.html","title":"ZGC","lang":"zh-CN","frontmatter":{"description":"ZGC 本文转载自12 张图带你彻底理解 ZGC ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/zgc.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"ZGC"}],["meta",{"property":"og:description","content":"ZGC 本文转载自12 张图带你彻底理解 ZGC ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/v2-62257a68cd5fa063d16cb4c30bd75ac0_1440w.webp"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T11:46:28.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T11:46:28.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"ZGC\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/v2-62257a68cd5fa063d16cb4c30bd75ac0_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-febbe42423b1c45c891919800e7a9dce_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-fe3f4a72fa6d62f53ed30a54ce6f318e_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-5b89e4cbd40143e1b66c4928ab737692_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-358e6823e714f2c0661414d429cf6bed_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-9bd5069ce01db2607ea2db6751f1cb37_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-1ca4d50695135c37a833b0d25d295a08_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-a0c8cf1fffc8dd487139841e371ce327_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-064d101e87e45dde0f6d25668c2820e4_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-59c87f0385d1c53919152250f73770d8_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-ed0c5178c1a4429f0794f024211e687c_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-af685613a34e774a88b112fe43b2b2a0_1440w.webp\\"],\\"dateModified\\":\\"2024-02-16T11:46:28.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1 内存多重映射","slug":"_1-内存多重映射","link":"#_1-内存多重映射","children":[]},{"level":2,"title":"2 染色指针","slug":"_2-染色指针","link":"#_2-染色指针","children":[{"level":3,"title":"2.1 三色标记回顾","slug":"_2-1-三色标记回顾","link":"#_2-1-三色标记回顾","children":[]},{"level":3,"title":"2.2 染色指针","slug":"_2-2-染色指针","link":"#_2-2-染色指针","children":[]}]},{"level":2,"title":"3 内存布局","slug":"_3-内存布局","link":"#_3-内存布局","children":[]},{"level":2,"title":"4 读屏障","slug":"_4-读屏障","link":"#_4-读屏障","children":[]},{"level":2,"title":"5 GC 过程","slug":"_5-gc-过程","link":"#_5-gc-过程","children":[{"level":3,"title":"5.1 初始标记","slug":"_5-1-初始标记","link":"#_5-1-初始标记","children":[]},{"level":3,"title":"5.2 并发标记","slug":"_5-2-并发标记","link":"#_5-2-并发标记","children":[]},{"level":3,"title":"5.3 再标记","slug":"_5-3-再标记","link":"#_5-3-再标记","children":[]},{"level":3,"title":"5.4 初始转移","slug":"_5-4-初始转移","link":"#_5-4-初始转移","children":[]},{"level":3,"title":"5.5 并发转移","slug":"_5-5-并发转移","link":"#_5-5-并发转移","children":[]},{"level":3,"title":"5.6 重定位","slug":"_5-6-重定位","link":"#_5-6-重定位","children":[]}]},{"level":2,"title":"6 垃圾收集算法","slug":"_6-垃圾收集算法","link":"#_6-垃圾收集算法","children":[{"level":3,"title":"6.1 JDK 16 之前","slug":"_6-1-jdk-16-之前","link":"#_6-1-jdk-16-之前","children":[]},{"level":3,"title":"6.2 JDK 16 改进","slug":"_6-2-jdk-16-改进","link":"#_6-2-jdk-16-改进","children":[]}]},{"level":2,"title":"7 总结","slug":"_7-总结","link":"#_7-总结","children":[]}],"git":{"createdTime":1708056026000,"updatedTime":1708083988000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":11.65,"words":3494},"filePathRelative":"java/JVM/zgc.md","localizedDate":"2024年2月16日","excerpt":"\\n

本文转载自12 张图带你彻底理解 ZGC

","autoDesc":true}');export{o as comp,g as data}; diff --git a/assets/zookeeper.html-Dd8HToeq.js b/assets/zookeeper.html-Dd8HToeq.js new file mode 100644 index 00000000..54825be2 --- /dev/null +++ b/assets/zookeeper.html-Dd8HToeq.js @@ -0,0 +1 @@ +import{_ as o,c as r,a as e,o as a}from"./app-ftEjETWs.js";const p={};function n(i,t){return a(),r("div",null,t[0]||(t[0]=[e("h1",{id:"zookeeper",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#zookeeper"},[e("span",null,"Zookeeper")])],-1),e("p",null,"留空",-1)]))}const m=o(p,[["render",n],["__file","zookeeper.html.vue"]]),l=JSON.parse('{"path":"/middleware/zookeeper/zookeeper.html","title":"Zookeeper","lang":"zh-CN","frontmatter":{"description":"Zookeeper 留空","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"Zookeeper"}],["meta",{"property":"og:description","content":"Zookeeper 留空"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T17:34:45.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-25T17:34:45.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Zookeeper\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T17:34:45.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706204085000,"updatedTime":1706204085000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":"middleware/zookeeper/zookeeper.md","localizedDate":"2024年1月25日","excerpt":"\\n

留空

\\n","autoDesc":true}');export{m as comp,l as data}; diff --git "a/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-Dg8HV8OO.js" "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-Dg8HV8OO.js" new file mode 100644 index 00000000..b672baf0 --- /dev/null +++ "b/assets/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html-Dg8HV8OO.js" @@ -0,0 +1,275 @@ +import{_ as i,c as a,d as n,o as e}from"./app-ftEjETWs.js";const h={};function l(t,s){return e(),a("div",null,s[0]||(s[0]=[n(`

【elasticsearch】搜索过程详解

本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

SearchType

QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。

为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。

POST aaaa-16/_doc
+{
+  "@timestamp": "2022-02-16T16:21:15.000Z",
+  "word":"16"
+}
+
+
+POST aaaa-17/_doc
+{
+  "@timestamp": "2022-02-17T16:21:15.000Z",
+  "word":"17"
+}
+
+POST aaaa-18/_doc
+{
+  "@timestamp": "2022-02-18T16:21:15.000Z",
+  "word":"18"
+}

即可在kibana上看到3条数据

image-20220219195141327
image-20220219195141327

此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?

GET aaaa*/_search
+{
+  "query": {
+    "range": {
+      "@timestamp": {
+        "gte": "2022-02-18T10:21:15.000Z",
+        "lte": "2022-02-18T17:21:15.000Z"
+      }
+    }
+  }
+}

正好命中一条记录返回。

{
+  "took" : 2,
+  "timed_out" : false,
+  "_shards" : {
+    "total" : 3,
+    "successful" : 3,
+    "skipped" : 0,
+    "failed" : 0
+  },
+  "hits" : {
+    "total" : {
+      "value" : 1,
+      "relation" : "eq"
+    },
+    "max_score" : 1.0,
+    "hits" : [
+      {
+        "_index" : "aaaa-18",
+        "_id" : "0zB2O38BoMIMP8QzHgdq",
+        "_score" : 1.0,
+        "_source" : {
+          "@timestamp" : "2022-02-18T16:21:15.000Z",
+          "word" : "18"
+        }
+      }
+    ]
+  }
+}

一、es的分布式搜索过程

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。当搜索任务执行在分布式系统上时,整体流程如下图所示。图片来源Elasitcsearch源码解析与优化实战

2
2

1.1 搜索入口

整个http请求的入口,主要使用的是Netty4HttpRequestHandler。

@ChannelHandler.Sharable
+class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {
+        final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();
+        boolean success = false;
+        try {
+            serverTransport.incomingRequest(httpRequest, channel);
+            success = true;
+        } finally {
+            if (success == false) {
+                httpRequest.release();
+            }
+        }
+    }
+}

二、初步调用流程

调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction

协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。

特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。

public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {
+  ...
+    TaskHolder taskHolder = new TaskHolder();
+    Task task = client.executeLocally(action, request, new ActionListener<>() {
+        @Override
+        public void onResponse(Response response) {
+            try {
+                closeListener.unregisterTask(taskHolder);
+            } finally {
+                listener.onResponse(response);
+            }
+        }
+    });
+  ...
+}

其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程

private SearchPhase searchAsyncAction(...) {
+  ...
+    final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();
+  AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {
+    case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);
+    case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);
+  };
+  return searchAsyncAction;
+  ...
+}

之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。

此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction
+private void executePhase(SearchPhase phase) {
+    try {
+        phase.run();
+    } catch (Exception e) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(new ParameterizedMessage("Failed to execute [{}] while moving to [{}] phase", request, phase.getName()), e);
+        }
+        onPhaseFailure(phase, "", e);
+    }
+}

三、协调节点

两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。

23
23

3.1 query阶段

图片来源官网,比较旧,但任然可用

12
12

(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。
(2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。
(3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。

在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构: 
+public RestChannelConsumer prepareRequest(.. .) {
+    SearchRequest searchRequest = new SearchRequest();
+    request.withContentOrSourceParamParserOrNull (parser ->
+        parseSearchRequest (searchRequest, request, parser, setSize));
+    ...
+}

3.1.1 构造目的shard列表

将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:

private void executeSearch(.. .) {
+  ...
+    final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
+    localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
+    OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
+    assert finalIndices != null;
+    return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
+    }).collect(Collectors.toList());
+	...
+}

查看结果

241
241

3.1.2 对所有分片进行搜索

AbstractSearchAsyncAction.run
+对每个分片进行搜索查询
+for (int i = 0; i < shardsIts.size(); i++) {
+    final SearchShardIterator shardRoutings = shardsIts.get(i);
+    assert shardRoutings.skip() == false;
+    assert shardIndexMap.containsKey(shardRoutings);
+    int shardIndex = shardIndexMap.get(shardRoutings);
+    performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
+}

其中shardsIts是所有aaaa*的所有索引+其中一个副本

2141
2141

3.1.3 分片具体的搜索过程

AbstractSearchAsyncAction.performPhaseOnShard
+private void performPhaseOnShard(. ..) {
+    executePhaseOnShard(.. .) {
+        //收到执行成功的回复
+        public void inne rOnResponse (FirstResult result) {
+            maybeFork (thread, () -> onShardResult (result,shardIt) );
+        }
+        //收到执行失败的回复
+        public void onFailure (Exception t) {
+            maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));
+        }
+    });
+}

分片结果,当前线程

//AbstractSearchAsyncAction.onShardResult
+...
+private void onShardResult (FirstResult result, SearchShardIterator shardIt) {
+    onShardSuccess(result);
+    success fulShardExecution(shardIt);
+}
+...
+//AbstractSearchAsyncAction.onShardResultConsumed
+private void successfulShardExecution (SearchShardIterator shardsIt) {
+    //计数器累加.
+    final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);
+    //检查是否收到全部回复
+    if (xTotalOps == expectedTotalOps) {
+        onPhaseDone ();
+    } else if (xTota1Ops > expectedTotal0ps) {
+        throw new AssertionError(. ..);
+    }
+}
412
412

此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段

3.2 Fetch阶段

取回阶段,图片来自官网

412412
412412

(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document

(2)各个shard将document返回给coordinate node

(3)coordinate node将合并后的document结果返回给client客户端

3.2.1 FetchSearchPhase(对应上面的1)

Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。

private void innerRun() throws Exception {
+    final int numShards = context.getNumShards();
+    final boolean isScrollSearch = context.getRequest().scroll() != null;
+    final List<SearchPhaseResult> phaseResults = queryResults.asList();
+    final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
+    final boolean queryAndFetchOptimization = queryResults.length() == 1;
+    final Runnable finishPhase = () -> moveToNextPhase(
+        searchPhaseController,
+        queryResults,
+        reducedQueryPhase,
+        queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()
+    );
+            for (int i = 0; i < docIdsToLoad.length; i++) {
+                IntArrayList entry = docIdsToLoad[i];
+                SearchPhaseResult queryResult = queryResults.get(i);
+                if (entry == null) { 
+                    if (queryResult != null) {
+                        releaseIrrelevantSearchContext(queryResult.queryResult());
+                        progressListener.notifyFetchResult(i);
+                    }
+                    counter.countDown();
+                }else{
+                    executeFetch(
+                            queryResult.getShardIndex(),
+                            shardTarget,
+                            counter,
+                            fetchSearchRequest,
+                            queryResult.queryResult(),
+                            connection
+                        );
+                }
+        }
+    }
+}

再看源码:

启动一个线程来fetch
+AbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch
+
+private void executeFetch(...) {
+    context.getSearchTransport()
+        .sendExecuteFetch(
+            connection,
+            fetchSearchRequest,
+            context.getTask(),
+            new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
+                @Override
+                public void innerOnResponse(FetchSearchResult result) {
+                  progressListener.notifyFetchResult(shardIndex);
+                  counter.onResult(result);
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                  progressListener.notifyFetchFailure(shardIndex, shardTarget, e);
+                  counter.onFailure(shardIndex, shardTarget, e);
+                }
+            }
+        );
+}
13413
13413

counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。

# CountedCollector.class
+void countDown() {
+    assert counter.isCountedDown() == false : "more operations executed than specified";
+    if (counter.countDown()) {
+        onFinish.run();
+    }
+}

moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。

3.2.2 ExpandSearchPhase(对应上图的2)

AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。

image-20220317000858513
image-20220317000858513

ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:

412412
412412

四、数据节点

4.1 执行query、fetch流程

执行本流程的线程池: search。

对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。

public static void registerRequestHandler(TransportService transportService, SearchService searchService) {
+  ...
+  transportService.registerRequestHandler(
+    QUERY_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    ShardSearchRequest::new,
+    (request, channel, task) -> searchService.executeQueryPhase(
+      request,
+      (SearchShardTask) task,
+      new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)
+    )
+  );
+  ...
+}

4.1.1 执行query请求

# SearchService
+public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {
+    assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1
+        : "empty responses require more than one shard";
+    final IndexShard shard = getShard(request);
+    rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {
+        ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);
+    }));
+}

其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。

1
1

主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:

public void execute (SearchContext searchContext) {
+    aggregationPhase.preProcess (searchContext);
+    boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);
+    if (rescore) {
+        rescorePhase.execute (searchContext);
+        suggestPhase.execute (searchContext);
+        aggregationPhase.execute (searchContext);
+    }
+}

其中包含几个核心功能:

  • execute(),调用Lucene、searcher.search()实现搜索
  • rescorePhase,全文检索且需要打分
  • suggestPhase,自动补全及纠错
  • aggregationPhase,实现聚合

4.1.2 fetch流程

对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。

transportService.registerRequestHandler(
+    QUERY_FETCH_SCROLL_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    InternalScrollSearchRequest::new,
+    (request, channel, task) -> {
+        searchService.executeFetchPhase(
+            request,
+            (SearchShardTask) task,
+            new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)
+        );
+    }
+);

对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。

# FetchPhase
+public void execute(SearchContext context) {
+    Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();
+    SearchHits hits = null;
+    try {
+      //lucene构建搜索的结果
+        hits = buildSearchHits(context, profiler);
+    } finally {
+        ProfileResult profileResult = profiler.finish();
+        // Only set the shardResults if building search hits was successful
+        if (hits != null) {
+            context.fetchResult().shardResult(hits, profileResult);
+        }
+    }
+}

五、数据返回

入口:RestCancellableNodeClient.doExecute
Task task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
image-20220319003638991

其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse

public void sendResponse(RestResponse restResponse) {
+  ...
+   httpChannel.sendResponse(httpResponse, listener);
+  ...
+}

最后由Netty4HttpChannel.sendResponse来响应请求。

六、总结

当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。

本文参考

  1. Elasitcsearch源码解析与优化实战
  2. Elasticsearch源码分析-搜索分析(一)
  3. Elasticsearch源码分析-搜索分析(二)
  4. Elasticsearch源码分析-搜索分析(三)
  5. Elasticsearch 通信模块的分析
  6. Elasticsearch 网络通信线程分析
`,97)]))}const p=i(h,[["render",l],["__file","【elasticsearch】搜索过程详解.html.vue"]]),r=JSON.parse('{"path":"/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html","title":"【elasticsearch】搜索过程详解","lang":"zh-CN","frontmatter":{"description":"【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"【elasticsearch】搜索过程详解"}],["meta",{"property":"og:description","content":"【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/1240.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T17:21:55.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T17:21:55.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"【elasticsearch】搜索过程详解\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/1240.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205906732.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125210335027.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125210338196.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205916232.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205923525.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205928956.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205939879.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205944929.png\\",\\"https://github-images.wenzhihuai.com/images/1339227-c5c82e202430fc45.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205952628.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125205957133.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125210045403.png\\"],\\"dateModified\\":\\"2024-02-16T17:21:55.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"SearchType","slug":"searchtype","link":"#searchtype","children":[]},{"level":2,"title":"一、es的分布式搜索过程","slug":"一、es的分布式搜索过程","link":"#一、es的分布式搜索过程","children":[{"level":3,"title":"1.1 搜索入口","slug":"_1-1-搜索入口","link":"#_1-1-搜索入口","children":[]}]},{"level":2,"title":"二、初步调用流程","slug":"二、初步调用流程","link":"#二、初步调用流程","children":[]},{"level":2,"title":"三、协调节点","slug":"三、协调节点","link":"#三、协调节点","children":[{"level":3,"title":"3.1 query阶段","slug":"_3-1-query阶段","link":"#_3-1-query阶段","children":[]},{"level":3,"title":"3.2 Fetch阶段","slug":"_3-2-fetch阶段","link":"#_3-2-fetch阶段","children":[]}]},{"level":2,"title":"四、数据节点","slug":"四、数据节点","link":"#四、数据节点","children":[{"level":3,"title":"4.1 执行query、fetch流程","slug":"_4-1-执行query、fetch流程","link":"#_4-1-执行query、fetch流程","children":[]}]},{"level":2,"title":"五、数据返回","slug":"五、数据返回","link":"#五、数据返回","children":[]},{"level":2,"title":"六、总结","slug":"六、总结","link":"#六、总结","children":[]},{"level":2,"title":"本文参考","slug":"本文参考","link":"#本文参考","children":[]}],"git":{"createdTime":1647789280000,"updatedTime":1708104115000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":6}]},"readingTime":{"minutes":8.92,"words":2677},"filePathRelative":"database/elasticsearch/【elasticsearch】搜索过程详解.md","localizedDate":"2022年3月20日","excerpt":"\\n

本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html-BtEABYaE.js" "b/assets/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html-BtEABYaE.js" new file mode 100644 index 00000000..157f2e1a --- /dev/null +++ "b/assets/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html-BtEABYaE.js" @@ -0,0 +1 @@ +import{_ as e,c as s,d as t,o as a}from"./app-ftEjETWs.js";const n={};function h(l,i){return a(),s("div",null,i[0]||(i[0]=[t('

一些不常用但很实用的命令

https连接耗时检测

curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\\n" -so /dev/null https://zhls.qq.com/test-nginx

iptables拒绝请求

iptables -A  OUTPUT -d 11.145.18.159 -j REJECT
',5)]))}const p=e(n,[["render",h],["__file","一些不常用但很实用的命令.html.vue"]]),d=JSON.parse('{"path":"/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html","title":"一些不常用但很实用的命令","lang":"zh-CN","frontmatter":{"description":"一些不常用但很实用的命令 https连接耗时检测 iptables拒绝请求","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"一些不常用但很实用的命令"}],["meta",{"property":"og:description","content":"一些不常用但很实用的命令 https连接耗时检测 iptables拒绝请求"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"一些不常用但很实用的命令\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1708752707000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.16,"words":47},"filePathRelative":"interesting/一些不常用但很实用的命令.md","localizedDate":"2024年2月24日","excerpt":"\\n

https连接耗时检测

\\n
curl -w \\"TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\\\\n\\" -so /dev/null https://zhls.qq.com/test-nginx
\\n
","autoDesc":true}');export{p as comp,d as data}; diff --git "a/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-zaM0dK5_.js" "b/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-zaM0dK5_.js" new file mode 100644 index 00000000..3b38854a --- /dev/null +++ "b/assets/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html-zaM0dK5_.js" @@ -0,0 +1,4 @@ +import{_ as s,c as a,d as e,o as t}from"./app-ftEjETWs.js";const n={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[e(`

一次jvm调优过程

前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
该程序task主要分为三个模块:
console进行一些cron的配置(表达式、任务名称、任务组等);
schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
整体架构跟github上开源的xxl-job类似,也可以参考一下。

1. 启用jmx和远程debug模式

容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:

JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n "
+JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "

其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。

堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。

2. memory analyzer、jprofiler进行堆内存分析

先从容器中dump出堆内存

jmap -dump:live,format=b,file=heap.hprof 58

由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。

3. netty的方面的考虑

另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。
首先是查看堆内存的中占比:

可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:

达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。

声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。

最终解决办法:
在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully

提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了

还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。

4. 直接内存排查

第一个想到的就是netty的直接内存,关掉,命令如下:

-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced

查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题

5. 推荐的直接内存排查方法

5.1 pmap

一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。

pmap -d 58  | sort -n -k2
+pmap -x 58  | sort -n -k3
+grep rw-p /proc/$1/maps | sed -n 's/^\\([0-9a-f]*\\)-\\([0-9a-f]*\\) .*$/\\1 \\2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done

这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?

5.2 [gperftools](https://github.com/gperftools/gperftools)

起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。
根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,

pprof --text /usr/bin/java java_58.0001.heap

看着工具高大上的,似乎能找出linux的调用栈,

6. 意外的结果

毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。
修复后的结果如下图所示:

总结

定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。

参考:
1.记一次线上内存泄漏问题的排查过程
2.Java堆外内存增长问题排查Case
3.Troubleshooting Native Memory Leaks in Java Applications

`,51)]))}const r=s(n,[["render",h],["__file","一次jvm调优过程.html.vue"]]),g=JSON.parse('{"path":"/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html","title":"一次jvm调优过程","lang":"zh-CN","frontmatter":{"description":"一次jvm调优过程 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"一次jvm调优过程"}],["meta",{"property":"og:description","content":"一次jvm调优过程 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20200121102100646998927.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:22:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:22:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"一次jvm调优过程\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20200121102100646998927.png\\",\\"https://github-images.wenzhihuai.com/images/20200121102112646209333.png\\",\\"https://github-images.wenzhihuai.com/images/202001211021411245129253.png\\",\\"https://github-images.wenzhihuai.com/images/20200121102201592637921.png\\",\\"https://github-images.wenzhihuai.com/images/202001211022191867685117.png\\",\\"https://github-images.wenzhihuai.com/images/20200121102239634803742.png\\",\\"https://github-images.wenzhihuai.com/images/20200121102251548240533.png\\",\\"https://github-images.wenzhihuai.com/images/202001211023001796761285.png\\",\\"https://github-images.wenzhihuai.com/images/202001211023141978405713.png\\",\\"https://github-images.wenzhihuai.com/images/202001211023231033694109.png\\",\\"https://github-images.wenzhihuai.com/images/202001211023381459531529.png\\",\\"https://github-images.wenzhihuai.com/images/202001211024021477267177.png\\",\\"https://github-images.wenzhihuai.com/images/202001211024142482541.png\\",\\"https://github-images.wenzhihuai.com/images/202001211024261078778632.png\\"],\\"dateModified\\":\\"2024-02-16T09:22:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"1. 启用jmx和远程debug模式","slug":"_1-启用jmx和远程debug模式","link":"#_1-启用jmx和远程debug模式","children":[]},{"level":2,"title":"2. memory analyzer、jprofiler进行堆内存分析","slug":"_2-memory-analyzer、jprofiler进行堆内存分析","link":"#_2-memory-analyzer、jprofiler进行堆内存分析","children":[]},{"level":2,"title":"3. netty的方面的考虑","slug":"_3-netty的方面的考虑","link":"#_3-netty的方面的考虑","children":[]},{"level":2,"title":"4. 直接内存排查","slug":"_4-直接内存排查","link":"#_4-直接内存排查","children":[]},{"level":2,"title":"5. 推荐的直接内存排查方法","slug":"_5-推荐的直接内存排查方法","link":"#_5-推荐的直接内存排查方法","children":[]},{"level":2,"title":"6. 意外的结果","slug":"_6-意外的结果","link":"#_6-意外的结果","children":[]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1579573736000,"updatedTime":1708075355000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":6.28,"words":1884},"filePathRelative":"java/JVM/一次jvm调优过程.md","localizedDate":"2020年1月21日","excerpt":"\\n

前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
\\n该程序task主要分为三个模块:
\\nconsole进行一些cron的配置(表达式、任务名称、任务组等);
\\nschedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
\\nclient接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
\\n整体架构跟github上开源的xxl-job类似,也可以参考一下。

","autoDesc":true}');export{r as comp,g as data}; diff --git "a/assets/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html-a1P2YWrj.js" "b/assets/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html-a1P2YWrj.js" new file mode 100644 index 00000000..3952a177 --- /dev/null +++ "b/assets/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html-a1P2YWrj.js" @@ -0,0 +1 @@ +import{_ as a,c as o,a as e,o as r}from"./app-ftEjETWs.js";const n={};function h(i,t){return r(),o("div",null,t[0]||(t[0]=[e("h1",{id:"一致性hash算法和哈希槽",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#一致性hash算法和哈希槽"},[e("span",null,"一致性hash算法和哈希槽")])],-1)]))}const c=a(n,[["render",h],["__file","一致性hash算法和哈希槽.html.vue"]]),p=JSON.parse('{"path":"/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html","title":"一致性hash算法和哈希槽","lang":"zh-CN","frontmatter":{"index":false,"description":"一致性hash算法和哈希槽","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"一致性hash算法和哈希槽"}],["meta",{"property":"og:description","content":"一致性hash算法和哈希槽"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"一致性hash算法和哈希槽\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.04,"words":12},"filePathRelative":"database/redis/一致性hash算法和哈希槽.md","localizedDate":"2024年3月10日","excerpt":"\\n","autoDesc":true}');export{c as comp,p as data}; diff --git "a/assets/\344\270\252\344\272\272\344\275\234\345\223\201.html-DDpX9I3v.js" "b/assets/\344\270\252\344\272\272\344\275\234\345\223\201.html-DDpX9I3v.js" new file mode 100644 index 00000000..2adc20e7 --- /dev/null +++ "b/assets/\344\270\252\344\272\272\344\275\234\345\223\201.html-DDpX9I3v.js" @@ -0,0 +1 @@ +import{_ as a,c as i,a as t,o}from"./app-ftEjETWs.js";const n={};function h(r,e){return o(),i("div",null,e[0]||(e[0]=[t("h1",{id:"小程序",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#小程序"},[t("span",null,"小程序")])],-1),t("figure",null,[t("img",{src:"https://github-images.wenzhihuai.com/github/gh_89030789780b_344.jpg",alt:"助眠风扇",tabindex:"0",loading:"lazy"}),t("figcaption",null,"助眠风扇")],-1),t("figure",null,[t("img",{src:"https://github-images.wenzhihuai.com/github/gh_068db871b3fd_344.jpg",alt:"MyTesMate",tabindex:"0",loading:"lazy"}),t("figcaption",null,"MyTesMate")],-1)]))}const p=a(n,[["render",h],["__file","个人作品.html.vue"]]),c=JSON.parse('{"path":"/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html","title":"小程序","lang":"zh-CN","frontmatter":{"description":"小程序 助眠风扇助眠风扇 MyTesMateMyTesMate","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"小程序"}],["meta",{"property":"og:description","content":"小程序 助眠风扇助眠风扇 MyTesMateMyTesMate"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/github/gh_89030789780b_344.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-27T09:51:43.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-27T09:51:43.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"小程序\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/github/gh_89030789780b_344.jpg\\",\\"https://github-images.wenzhihuai.com/github/gh_068db871b3fd_344.jpg\\"],\\"dateModified\\":\\"2024-01-27T09:51:43.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1706349103000,"updatedTime":1706349103000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.05,"words":14},"filePathRelative":"about-the-author/works/个人作品.md","localizedDate":"2024年1月27日","excerpt":"\\n
\\"助眠风扇\\"
助眠风扇
","autoDesc":true}');export{p as comp,c as data}; diff --git "a/assets/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html-Bb5S9Lg5.js" "b/assets/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html-Bb5S9Lg5.js" new file mode 100644 index 00000000..e5dd7db5 --- /dev/null +++ "b/assets/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html-Bb5S9Lg5.js" @@ -0,0 +1,4 @@ +import{_ as a,c as t,d as i,o as n}from"./app-ftEjETWs.js";const s={};function l(r,e){return n(),t("div",null,e[0]||(e[0]=[i(`

canal小记

接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

先来了解一下MySQL的主从备份:

从上层来看,复制分成三步:
master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
slave将master的binary log events拷贝到它的中继日志(relay log);
slave重做中继日志中的事件,将改变反映它自己的数据。

问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么

最后发现是沟通问题。。。

排查过程:

  1. 起初,怀疑是es的问题,会不会是string转为long中出现了问题,PUT了个,无异常,这种情况排除。
  2. 再然后以为是代码有问题,可是想了下,rowData.getAfterColumnsList().forEach(column -> data.put(column.getName(), column.getValue()))这句不可能有什么其他的问题啊,而且测试环境中一切都是好好的。
  3. canal安装出错,重新查看了一次canal.properties和instance.properties,并没有发现配置错了啥,如果错了,那为什么只有那几个字段出现异常,其他的都是好好的,郁闷。而且,用测试环境的canal配置生产中的数据库,然后本地调试,结果依旧一样。可能问题出在mysql。

最后发现,居然是沟通问题。。。。测试环境中是从正式环境导入的,用的insert,可是在正式环境里,用的确实insert后update字段,之后发现居然还用delete,,,,晕。。。。之前明确问过了只更新insert的,人与人之间的信任在哪里。。。。

问题二:canal.properties中四种模式的差别

简单的说,canal维护一份增量订阅和消费关系是依靠解析位点和消费位点的,目前提供了一下四种配置,一开始我也是懵的。

#canal.instance.global.spring.xml = classpath:spring/local-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml

local-instance
我也不知道啥。。

memory-instance
所有的组件(parser , sink , store)都选择了内存版模式,记录位点的都选择了memory模式,重启后又会回到初始位点进行解析
特点:速度最快,依赖最少(不需要zookeeper)
场景:一般应用在quickstart,或者是出现问题后,进行数据分析的场景,不应该将其应用于生产环境。
个人建议是调试的时候使用该模式,即新增数据的时候,客户端能马上捕获到改日志,但是由于位点一直都是canal启动的时候最新的,不适用与生产环境。

file-instance
所有的组件(parser , sink , store)都选择了基于file持久化模式,注意,不支持HA机制.
特点:支持单机持久化
场景:生产环境,无HA需求,简单可用.
采用该模式的时候,如果关闭了canal,会在destination中生成一个meta.dat,用来记录关键信息。如果想要启动canal之后马上订阅最新的位点,需要把该文件删掉。
{"clientDatas":[{"clientIdentity":{"clientId":1001,"destination":"example","filter":".\\.."},"cursor":{"identity":{"slaveId":-1,"sourceAddress":{"address":"192.168.6.71","port":3306}},"postion":{"included":false,"journalName":"binlog.008335","position":221691106,"serverId":88888,"timestamp":1524294834000}}}],"destination":"example"}

default-instance
所有的组件(parser , sink , store)都选择了持久化模式,目前持久化的方式主要是写入zookeeper,保证数据集群共享。
特点:支持HA
场景:生产环境,集群化部署.
该模式会记录集群中所有运行的节点,主要用与HA主备模式,节点中的数据如下,可以关闭某一个canal服务来查看running的变化信息。

问题三:如果要订阅的是mysql的从库改怎么做?

生产环境中的主库是不能随便重启的,所以订阅的话必须订阅mysql主从的从库,而从库中是默认下只将主库的操作写进中继日志,并写到自己的二进制日志的,所以需要让其成为canal的主库,必须让其将日志也写到自己的二进制日志里面。处理方法:修改/etc/my.cnf,增加一行log_slave_updates=1,重启数据库后就可以了。

问题四:部分字段没有更新

最终版本是以mysql的id为es的主键,用canal同步到flume,再由flume到kafka,然后再由一个中间件写到es里面去,结果发现,一天之中,会有那么一段时间得出的结果少一丢丢,甚至是骤降,如图。不得不从头开始排查情况,canal到flume,加了canal的重试,以及发送到flume的重试机制,没有报错,所有数据正常发送。flume到kafka不敢怀疑,毕竟公司一直在用,怎么可能有问题。kafka到es的中间件?组长写的,而且一直在用,不可能==最后确认的是flume到kafka,kafka的parition处理速度不同,

check一下flume的文档,可以知道

Property NameDescription
defaultPartitionIdSpecifies a Kafka partition ID (integer) for all events in this channel to be sent to, unless overriden by partitionIdHeader. By default, if this property is not set, events will be distributed by the Kafka Producer’s partitioner - including by key if specified (or by a partitioner specified by kafka.partitioner.class).
partitionIdHeaderWhen set, the producer will take the value of the field named using the value of this property from the event header and send the message to the specified partition of the topic. If the value represents an invalid partition the event will not be accepted into the channel. If the header value is present then this setting overrides defaultPartitionId.

大概意思是flume如果不自定义partitionIdHeader,那么消息将会被分布式kafka的partion处理,kafka本身的设置就是高吞吐量的消息系统,同一partion的消息是可以按照顺序发送的,但是多个partion就不确定了,如果需要将消息按照顺序发送,那么就必须要指定一个parition,即在flume的配置文件中添加:a1.channels.channel1.partitionIdHeader=1,指定parition即可。全部修改完之后,在kibana查看一下曲线:

用sql在数据库确认了下,终于一致了,不容易。。。

`,30)]))}const p=a(s,[["render",l],["__file","中间件——canal小记.html.vue"]]),c=JSON.parse('{"path":"/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html","title":"canal小记","lang":"zh-CN","frontmatter":{"description":"canal小记 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。 先来了解一下MySQL的主从备份: 从上层来看,复制分成三步: master将改...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"canal小记"}],["meta",{"property":"og:description","content":"canal小记 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。 先来了解一下MySQL的主从备份: 从上层来看,复制分成三步: master将改..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20180421025107389573244.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-06T07:53:39.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-06T07:53:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"canal小记\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20180421025107389573244.png\\",\\"https://github-images.wenzhihuai.com/images/2018042101385616022393.png\\",\\"https://github-images.wenzhihuai.com/images/201804210425561692361189.png\\",\\"https://github-images.wenzhihuai.com/images/201804210451321357023546.png\\",\\"https://github-images.wenzhihuai.com/images/20180428015132288764661.png\\",\\"https://github-images.wenzhihuai.com/images/201804290227121343830102.png\\"],\\"dateModified\\":\\"2024-02-06T07:53:39.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么","slug":"问题一-测试环境一切正常-但是正式环境中-这几个字段全为0-不知道为什么","link":"#问题一-测试环境一切正常-但是正式环境中-这几个字段全为0-不知道为什么","children":[]},{"level":2,"title":"问题二:canal.properties中四种模式的差别","slug":"问题二-canal-properties中四种模式的差别","link":"#问题二-canal-properties中四种模式的差别","children":[]},{"level":2,"title":"问题三:如果要订阅的是mysql的从库改怎么做?","slug":"问题三-如果要订阅的是mysql的从库改怎么做","link":"#问题三-如果要订阅的是mysql的从库改怎么做","children":[]},{"level":2,"title":"问题四:部分字段没有更新","slug":"问题四-部分字段没有更新","link":"#问题四-部分字段没有更新","children":[]}],"git":{"createdTime":1707204155000,"updatedTime":1707206019000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":5.37,"words":1610},"filePathRelative":"middleware/canal/中间件——canal小记.md","localizedDate":"2024年2月6日","excerpt":"\\n

接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

","autoDesc":true}');export{p as comp,c as data}; diff --git "a/assets/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html-DMCiuYMs.js" "b/assets/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html-DMCiuYMs.js" new file mode 100644 index 00000000..b7ea0fee --- /dev/null +++ "b/assets/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html-DMCiuYMs.js" @@ -0,0 +1,56 @@ +import{_ as s,c as e,d as a,o as n}from"./app-ftEjETWs.js";const l={};function t(h,i){return n(),e("div",null,i[0]||(i[0]=[a(`

买了个mini主机

虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。

iowejofwjeofjwoeifjwoe
iowejofwjeofjwoeifjwoe

一、系统的安装

系统我看是支持windows,还有现在Ubuntu,但是我这种选择的是centos stream 9, 10的话我也找过,但是发现很多软件还有不兼容。所以最终还是centos stream 9。

1、下载Ventoy软件

去Ventoy官网下载Ventoy软件(Download . Ventoy)如下图界面

QQ_1727625608185
QQ_1727625608185

2、制作启动盘

选择合适的版本以及平台下载好之后,进行解压,解压出来之后进入文件夹,如下图左边所示,双击打开Ventoy2Disk.exe,会出现下图右边的界面,选择好自己需要制作启动盘的U盘,然后点击安装等待安装成功即可顺利制作成功启动U盘。

3、centos安装

直接取官网,下载完放到u盘即可。

QQ_1727625711792
QQ_1727625711792

它的BIOS是按F7启动,直接加载即可。

image-20241007222938414
image-20241007222938414

之后就是正常的centos安装流程了。

二、连接wifi

因为是用作服务器的,所以并没有给它配置个专门的显示器,只要换个网络,就连不上新的wifi了,这里可以用网线连接路由器进行下面的操作即可。

在 CentOS 系统中,通过命令行连接 Wi-Fi 通常需要使用 nmcli(NetworkManager 命令行工具)来管理网络连接。nmcli 是 NetworkManager 的一个命令行接口,可以用于创建、修改、激活和停用网络连接。以下是如何使用 nmcli 命令行工具连接 Wi-Fi 的详细步骤。

步骤 1: 检查网络接口

首先,确认你的 Wi-Fi 网络接口是否被检测到,并且 NetworkManager 是否正在运行。

nmcli device status

输出示例:

DEVICE         TYPE      STATE         CONNECTION
+wlp3s0         wifi      disconnected  --
+enp0s25        ethernet  connected     Wired connection 1
+lo             loopback  unmanaged     --

在这个示例中,wlp3s0 是 Wi-Fi 接口,它当前处于未连接状态。

步骤 2: 启用 Wi-Fi 网卡

如果你的 Wi-Fi 网卡是禁用状态,可以通过以下命令启用:

nmcli radio wifi on

验证 Wi-Fi 是否已启用:

nmcli radio

步骤 3: 扫描可用的 Wi-Fi 网络

使用 nmcli 扫描附近的 Wi-Fi 网络:

nmcli device wifi list

你将看到可用的 Wi-Fi 网络列表,每个网络都会显示 SSID(网络名称)、安全类型等信息。

步骤 4: 连接到 Wi-Fi 网络

使用 nmcli 命令连接到指定的 Wi-Fi 网络。例如,如果你的 Wi-Fi 网络名称(SSID)是 MyWiFiNetwork,并且密码是 password123,你可以使用以下命令连接:

nmcli device wifi connect 'xxxxxx' password 'xxxxx'

你应该会看到类似于以下输出,表明连接成功:

Device 'wlp3s0' successfully activated with 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.

步骤 5: 验证连接状态

验证网络连接状态:

nmcli connection show

查看当前连接的详细信息:

nmcli device show wlp3s0

上面的其实在2024年其实有点问题,因为会默认连接2.4GHz的wifi,使用的时候很明显没有那么快,特别是在用命令行的时候会觉得明显卡顿,现在需要切换到5GHz的wifi。
首先,使用 nmcli 获取可用 WiFi 网络及其 BSSID:

nmcli -f SSID,BSSID,CHAN dev wifi list

示例输出:

SSID       BSSID              CHAN
+MyNetwork  XX:XX:XX:XX:XX:01  36
+MyNetwork  XX:XX:XX:XX:XX:02  1

在这里, XX:XX:XX:XX:XX:01 是 5GHz 网络的 BSSID。

使用 nmcli 连接到特定的 BSSID

nmcli dev wifi connect 'XX:XX:XX:XX:XX:01' password 'your_password'

三、VNC远程连接

桌面还是偶尔需要用一下的,虽然用的不多。

root@master:~# dnf install  -y  tigervnc-server
+root@master:~# vncserver
+bash: vncserver: command not found...
+Install package 'tigervnc-server' to provide command 'vncserver'? [N/y] y
+
+
+ * Waiting in queue... 
+ * Loading list of packages.... 
+The following packages have to be installed:
+ dbus-x11-1:1.12.20-8.el9.x86_64        X11-requiring add-ons for D-BUS
+ tigervnc-license-1.14.0-3.el9.noarch   License of TigerVNC suite
+ tigervnc-selinux-1.14.0-3.el9.noarch   SELinux module for TigerVNC
+ tigervnc-server-1.14.0-3.el9.x86_64    A TigerVNC server
+ tigervnc-server-minimal-1.14.0-3.el9.x86_64    A minimal installation of TigerVNC server
+Proceed with changes? [N/y] y
+
+
+ * Waiting in queue... 
+ * Waiting for authentication... 
+ * Waiting in queue... 
+ * Downloading packages... 
+ * Requesting data... 
+ * Testing changes... 
+ * Installing packages... 
+
+WARNING: vncserver has been replaced by a systemd unit and is now considered deprecated and removed in upstream.
+Please read /usr/share/doc/tigervnc/HOWTO.md for more information.
+
+You will require a password to access your desktops.
+
+getpassword error: Inappropriate ioctl for device
+Password:

之后在mac开启屏幕共享就可以了

image-20241007225855305
image-20241007225855305
QQ_1728313164289
QQ_1728313164289

四、docker 配置

docker安装我以为很简单,没想到这里是最难的一步了。安装完docker之后,总是报错:

Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded

即使改了mirrors也毫无作用

{
+  "registry-mirrors": [
+    "https://ylce84v9.mirror.aliyuncs.com"
+        ]
+}

看起来好像是docker每次pull镜像都要访问一次registry-1.docker.io,但是这个网址国内已经无法连接了,各种折腾,这里只贴一下代码吧,原理就就不讲了(懂得都懂)。

img
img
sslocal -c /etc/shadowsocks.json -d start
+curl --socks5 127.0.0.1:1080 http://httpbin.org/ip
+
+sudo yum -y install privoxy

vim /etc/systemd/system/docker.service.d/http-proxy.conf

[Service]
+Environment="HTTP_PROXY=http://127.0.0.1:8118"

/etc/systemd/system/docker.service.d/https-proxy.conf

[Service]
+Environment="HTTPS_PROXY=http://127.0.0.1:8118"

最后重启docker

systemctl start privoxy
+systemctl enable privoxy
+sudo systemctl daemon-reload
+sudo systemctl restart docker
QQ_1729956484197
QQ_1729956484197

五、文件共享

sd卡好像读取不了,只能换个usb转换器

fdisk -l
+mount /dev/sdb1 /mnt/usb/sd

在CentOS中设置文件共享,可以使用Samba服务。以下是配置Samba以共享文件的基本步骤:

  1. 安装Samba
sudo yum install samba samba-client samba-common
  1. 设置共享目录

    编辑Samba配置文件/etc/samba/smb.conf,在文件末尾添加以下内容:

[shared]
+   path = /path/to/shared/directory
+   writable = yes
+   browseable = yes
+   guest ok = yes
  1. 设置Samba密码

    为了允许访问,需要为用户设置一个Samba密码:

sudo smbpasswd -a your_username
  1. 重启Samba服务
sudo systemctl restart smb.service
+sudo systemctl restart nmb.service
  1. 配置防火墙(如果已启用)

    允许Samba通过防火墙:

sudo firewall-cmd --permanent --zone=public --add-service=samba
+sudo firewall-cmd --reload

现在,您应该能够从网络上的其他计算机通过SMB/CIFS访问共享。在Windows中,你可以使用\\\\centos-ip\\shared,在Linux中,你可以使用smbclient //centos-ip/shared -U your_username

QQ_1730035390803
QQ_1730035390803

参考:

https://shadowsockshelp.github.io/Shadowsocks/linux.html

https://stackoverflow.com/questions/48056365/error-get-https-registry-1-docker-io-v2-net-http-request-canceled-while-b

`,92)]))}const r=s(l,[["render",t],["__file","买了个mini主机.html.vue"]]),p=JSON.parse('{"path":"/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html","title":"买了个mini主机","lang":"zh-CN","frontmatter":{"description":"买了个mini主机 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"买了个mini主机"}],["meta",{"property":"og:description","content":"买了个mini主机 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/iowejofwjeofjwoeifjwoe.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T18:45:10.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T18:45:10.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"买了个mini主机\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/iowejofwjeofjwoeifjwoe.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1727625608185.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1727625711792.png\\",\\"https://github-images.wenzhihuai.com/images/image-20241007222938414.png\\",\\"https://github-images.wenzhihuai.com/images/image-20241007225855305.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1728313164289.png\\",\\"https://github-images.wenzhihuai.com/images/5493f11fb4fe4de1bba971a744bf4f4a_1074242703.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1729956484197.png\\",\\"https://github-images.wenzhihuai.com/images/QQ_1730035390803.png\\"],\\"dateModified\\":\\"2024-11-29T18:45:10.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、系统的安装","slug":"一、系统的安装","link":"#一、系统的安装","children":[]},{"level":2,"title":"二、连接wifi","slug":"二、连接wifi","link":"#二、连接wifi","children":[{"level":3,"title":"步骤 1: 检查网络接口","slug":"步骤-1-检查网络接口","link":"#步骤-1-检查网络接口","children":[]},{"level":3,"title":"步骤 2: 启用 Wi-Fi 网卡","slug":"步骤-2-启用-wi-fi-网卡","link":"#步骤-2-启用-wi-fi-网卡","children":[]},{"level":3,"title":"步骤 3: 扫描可用的 Wi-Fi 网络","slug":"步骤-3-扫描可用的-wi-fi-网络","link":"#步骤-3-扫描可用的-wi-fi-网络","children":[]},{"level":3,"title":"步骤 4: 连接到 Wi-Fi 网络","slug":"步骤-4-连接到-wi-fi-网络","link":"#步骤-4-连接到-wi-fi-网络","children":[]},{"level":3,"title":"步骤 5: 验证连接状态","slug":"步骤-5-验证连接状态","link":"#步骤-5-验证连接状态","children":[]}]},{"level":2,"title":"三、VNC远程连接","slug":"三、vnc远程连接","link":"#三、vnc远程连接","children":[]},{"level":2,"title":"四、docker 配置","slug":"四、docker-配置","link":"#四、docker-配置","children":[]},{"level":2,"title":"五、文件共享","slug":"五、文件共享","link":"#五、文件共享","children":[]}],"git":{"createdTime":1730035985000,"updatedTime":1732905910000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":5.88,"words":1763},"filePathRelative":"interesting/mini主机/买了个mini主机.md","localizedDate":"2024年10月27日","excerpt":"\\n

虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

","autoDesc":true}');export{r as comp,p as data}; diff --git "a/assets/\345\205\245\351\227\250\344\275\277\347\224\250.html-DfOcEEjE.js" "b/assets/\345\205\245\351\227\250\344\275\277\347\224\250.html-DfOcEEjE.js" new file mode 100644 index 00000000..d9e00f59 --- /dev/null +++ "b/assets/\345\205\245\351\227\250\344\275\277\347\224\250.html-DfOcEEjE.js" @@ -0,0 +1 @@ +import{_ as t,c as n,d as a,o as r}from"./app-ftEjETWs.js";const i={};function o(l,e){return r(),n("div",null,e[0]||(e[0]=[a('

入门使用

参考

1.Sentinel入门(干货版)

',3)]))}const p=t(i,[["render",o],["__file","入门使用.html.vue"]]),s=JSON.parse('{"path":"/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html","title":"入门使用","lang":"zh-CN","frontmatter":{"index":false,"description":"入门使用 参考 1.Sentinel入门(干货版)","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"入门使用"}],["meta",{"property":"og:description","content":"入门使用 参考 1.Sentinel入门(干货版)"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"入门使用\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.06,"words":17},"filePathRelative":"middleware/sentinel/入门使用.md","localizedDate":"2024年3月10日","excerpt":"\\n

参考

\\n

1.Sentinel入门(干货版)

","autoDesc":true}');export{p as comp,s as data}; diff --git "a/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-DsVzFk6c.js" "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-DsVzFk6c.js" new file mode 100644 index 00000000..ea8c8ab4 --- /dev/null +++ "b/assets/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html-DsVzFk6c.js" @@ -0,0 +1,54 @@ +import{_ as s,c as a,d as n,o as e}from"./app-ftEjETWs.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

在 Spring 6 中使用虚拟线程

一、简介

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。

首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。

二、 虚拟线程与平台线程

主要区别在于虚拟线程在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。

对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。

从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。

三、在Spring 6中使用虚拟线程

从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的预览功能。这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下代码

<build>
+    <plugins>
+        <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+                <source>19</source>
+                <target>19</target>
+                <compilerArgs>
+                    --enable-preview
+                </compilerArgs>
+            </configuration>
+        </plugin>
+    </plugins>
+</build>

从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:

@EnableAsync
+@Configuration
+@ConditionalOnProperty(
+  value = "spring.thread-executor",
+  havingValue = "virtual"
+)
+public class ThreadConfig {
+    @Bean
+    public AsyncTaskExecutor applicationTaskExecutor() {
+        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
+    }
+
+    @Bean
+    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
+        return protocolHandler -> {
+            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+        };
+    }
+}

第一个 Spring Bean ApplicationTaskExecutor将取代标准的*ApplicationTaskExecutor* ,提供为每个任务启动新虚拟线程的Executor。第二个 bean,名为ProtocolHandlerVirtualThreadExecutorCustomizer,将以相同的方式 自定义标准TomcatProtocolHandler 。我们还添加了注释@ConditionalOnProperty,**以通过切换application.yaml文件中配置属性的值来按需启用虚拟线程:

spring:
+    thread-executor: virtual
+    //...

我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:

@RestController
+@RequestMapping("/thread")
+public class ThreadController {
+    @GetMapping("/name")
+    public String getThreadName() {
+        return Thread.currentThread().toString();
+    }
+}

*Thread*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个curl请求来访问这个端点:

$ curl -s http://localhost:8080/thread/name
+$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4

正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。

四、性能比较

对于此负载测试,我们将使用JMeter。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。

在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:

@RestController
+@RequestMapping("/load")
+public class LoadTestController {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
+
+    @GetMapping
+    public void doSomething() throws InterruptedException {
+        LOG.info("hey, I'm doing something");
+        Thread.sleep(1000);
+    }
+}

请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换

JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:

image-20230827193431807
image-20230827193431807

在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:

image-20230827193511176
image-20230827193511176

发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。

让我们看看虚拟线程会发生什么:

image-20230827193533565
image-20230827193533565

正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。

**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。

`,35)]))}const k=s(l,[["render",t],["__file","在 Spring 6 中使用虚拟线程.html.vue"]]),r=JSON.parse('{"path":"/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html","title":"在 Spring 6 中使用虚拟线程","lang":"zh-CN","frontmatter":{"description":"在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。 ...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"在 Spring 6 中使用虚拟线程"}],["meta",{"property":"og:description","content":"在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。 ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/755525-20230827193604936-1596575555.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"在 Spring 6 中使用虚拟线程\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/755525-20230827193604936-1596575555.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20230827193604663-146471729.png\\",\\"https://github-images.wenzhihuai.com/images/755525-20230827193609186-1264647264.png\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、简介","slug":"一、简介","link":"#一、简介","children":[]},{"level":2,"title":"二、 虚拟线程与平台线程","slug":"二、-虚拟线程与平台线程","link":"#二、-虚拟线程与平台线程","children":[]},{"level":2,"title":"三、在Spring 6中使用虚拟线程","slug":"三、在spring-6中使用虚拟线程","link":"#三、在spring-6中使用虚拟线程","children":[]},{"level":2,"title":"四、性能比较","slug":"四、性能比较","link":"#四、性能比较","children":[]}],"git":{"createdTime":1706186538000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":4.83,"words":1448},"filePathRelative":"java/在 Spring 6 中使用虚拟线程.md","localizedDate":"2024年1月25日","excerpt":"\\n

一、简介

\\n

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

","autoDesc":true}');export{k as comp,r as data}; diff --git "a/assets/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html-BNdB--VB.js" "b/assets/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html-BNdB--VB.js" new file mode 100644 index 00000000..191a07ce --- /dev/null +++ "b/assets/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html-BNdB--VB.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as i,o as r}from"./app-ftEjETWs.js";const n={};function o(s,e){return r(),a("div",null,e[0]||(e[0]=[i('

基于OLAP做业务监控

在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

一、两者的比较

Prometheus 的特点
Prometheus 是一种开源的监控和警报工具包,最初由 SoundCloud 开发并开源。它具有以下特点:
(1)多维度数据模型
Prometheus 采用标签(label)系统,允许用户通过灵活的标签组合来区分和查询监控数据。
(2)Pull 模式采集
Prometheus 使用 Pull 模式从各种目标(如应用程序、数据库、操作系统等)拉取数据,这与其他使用 Push 模式的监控系统有所不同。
(3)警报和告警管理
Prometheus 内置了一个强大的警报管理系统,可以根据预定条件触发警报,并集成到多种通知平台(如邮件、Slack 等)。
(4)生态系统和可扩展性
Prometheus 拥有活跃的社区和广泛的插件支持,可以轻松与其他系统集成,并根据需要进行扩展。

OLAP 数据库

OLAP(Online Analytical Processing)数据库在监控数据存储和分析中具有独特的优势,尤其在数据持久性和实时查询方面。

(1)数据持久性和长期存储
OLAP 数据库通常具有可靠的数据持久性和长期存储能力,能存储大量历史数据,并支持快速的时间范围查询和数据回溯分析。
(2)实时数据加载和查询
现代 OLAP 数据库尽管通常不支持实时数据加载,但一些具有较低的延迟和高吞吐量,可以支持实时查询和分析需求。

二、为什么要用OLAP数据库来做监控

1
1
2
2
3
3
',11)]))}const h=t(n,[["render",o],["__file","基于OLAP做业务监控.html.vue"]]),p=JSON.parse('{"path":"/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html","title":"基于OLAP做业务监控","lang":"zh-CN","frontmatter":{"description":"基于OLAP做业务监控 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 interval 和 for 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。 一、两者的...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"基于OLAP做业务监控"}],["meta",{"property":"og:description","content":"基于OLAP做业务监控 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 interval 和 for 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。 一、两者的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_486b6ebb-0d04-45d2-84b5-e7f157e50ae1.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-10T15:55:36.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-10T15:55:36.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"基于OLAP做业务监控\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_486b6ebb-0d04-45d2-84b5-e7f157e50ae1.png\\",\\"https://github-images.wenzhihuai.com/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_5a47dce2-ec41-4c07-8a5b-141487b8d9ff.png\\",\\"https://github-images.wenzhihuai.com/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_bd50d61d-bf61-45f0-93f7-68f6816db01b.png\\"],\\"dateModified\\":\\"2024-11-10T15:55:36.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、两者的比较","slug":"一、两者的比较","link":"#一、两者的比较","children":[]},{"level":2,"title":"二、为什么要用OLAP数据库来做监控","slug":"二、为什么要用olap数据库来做监控","link":"#二、为什么要用olap数据库来做监控","children":[]}],"git":{"createdTime":1715617566000,"updatedTime":1731254136000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2},{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":1.95,"words":586},"filePathRelative":"interesting/基于OLAP做业务监控.md","localizedDate":"2024年5月13日","excerpt":"\\n

在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

","autoDesc":true}');export{h as comp,p as data}; diff --git "a/assets/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html-D6Y4vXXb.js" "b/assets/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html-D6Y4vXXb.js" new file mode 100644 index 00000000..b065f793 --- /dev/null +++ "b/assets/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html-D6Y4vXXb.js" @@ -0,0 +1,179 @@ +import{_ as s,c as a,d as n,o as h}from"./app-ftEjETWs.js";const l={};function k(t,i){return h(),a("div",null,i[0]||(i[0]=[n(`

基于ZooKeeper的队列爬虫

一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
基本的知识就不过多介绍了,可以参考参考下面这些人的:
ZooKeeper官网
http://www.cnblogs.com/wuxl360/p/5817471.html

一、整体架构

这张图来自skyme,我也是看了这张图的启发写了这篇文章的。

最基本的分布式队列即一个生产者不断抓取链接,然后将链接存储进ZooKeeper的队列节点里,每个节点的value都只是链接,然后消费者从中获取一条url进行抓取。本项目生产这主要是用来生产URL即可,这部分就不要要求太多。然后是消费者,消费者需要解决的问题有:
1.队列如何保证自己的分发正确;
2.消费这如何进行高效的抓取。

二、ZooKeeper队列原理

2.1 介绍

分布式队列,目前此类产品大多类似于ActiveMQ、RabbitMQ等,本文主要介绍的是Zookeeper实现的分布式队列,它的实现方式也有两种,一种是FIFO(先进先出)的队列,另一种是等待队列元素聚集之后才统一安排的Barrier模型。同样,本文主要讲的是FIFO的队列模型。其大体设计思路也很简单,主要是在/SinaQueue下创建顺序节点,如/SinaQueue/qn-000000000,创建完节点之后,根据下面的4个步骤来决定执行的顺序。
1.通过调用getChildren()接口来获取某一节点下的所有节点,即获取队列中的所有元素。
2.确定自己的节点序号在所有子节点中的顺序。
3.如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
4.接收到Watcher通知后,重复步骤1。

2.2 Watcher介绍

znode以某种方式发生变化时,“观察”(watch)机制可以让客户端得到通知.可以针对ZooKeeper服务的“操作”来设置观察,该服务的其他 操作可以触发观察。
1.Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
2.Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调
3.Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。

2.3 源码

在csdn上找到了某个人写的这个过程,使用的是ZKClient,有兴趣可以看看杰布斯的博客,但是没有实现上面过程的第三步(Watcher相关的),这里,我们使用的是Zookeeper的另一个客户端工具curator,其中,curator实现了各种Zookeeper的特性,如:Election(选举),Lock(锁),Barrier(关卡),Atonmic(原子量),Cache(缓存),Queue(队列)等。我们来看看Curator实现的简单的分布式队列的源码。

public class SimpleDistributedQueue {
+    ...
+    private final CuratorFramework client;//连接Zookeeper的客户端
+    private final String path;//路径
+    private final EnsureContainers ensureContainers;//确保原子特性
+    private final String PREFIX = "qn-";//顺序节点的同意前缀,使用qn-
+    ...

其中PREFIX是用来生成顺序节点的,默认不可更改,将生成的路径赋予给path,然后向节点赋予数据。下面是赋予数据的代码

    public boolean offer(byte[] data) throws Exception {
+        String thisPath = ZKPaths.makePath(this.path, "qn-");//生成的路径
+        ((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL)).forPath(thisPath, data);//如果没有路径将生成持久化的路径然后存储节点的数据。
+        return true;
+    }

最关键的来了,队列如何保证自己的分发正确?SimpleDistributedQueue使用take()来取得队列的头部,然后将头部删掉,这一过程的一致性是通过CountDownLatch和Watcher来实现的。

    public byte[] take() throws Exception {//直接调用interPoll,并将超时的设置为0;
+        return this.internalPoll(0L, (TimeUnit)null);
+    }
+    private byte[] internalPoll(long timeout, TimeUnit unit) throws Exception {
+        ...//忽略超时的设置代码
+        while(true) {
+            final CountDownLatch latch = new CountDownLatch(1);//定义一个latch,设置为1,先加锁,然后执行完任务后再释放锁
+            Watcher watcher = new Watcher() {
+                public void process(WatchedEvent event) {
+                    latch.countDown();
+                }
+            };
+            byte[] bytes;
+            try {
+                bytes = this.internalElement(true, watcher);//调用internalElement函数来获取字节流
+            } catch (NoSuchElementException var17) {
+            }
+            ...
+            if (hasTimeout) {
+                long elapsedMs = System.currentTimeMillis() - startMs;
+                long thisWaitMs = maxWaitMs - elapsedMs;
+                if (thisWaitMs <= 0L) {    //如果等待超时了则返回为空
+                    return null;
+                }
+                latch.await(thisWaitMs, TimeUnit.MILLISECONDS);
+            } else {
+                latch.await();
+            }
+        }
+    }
+    private byte[] internalElement(boolean removeIt, Watcher watcher) throws Exception {
+            this.ensurePath();
+            List nodes;
+            try {
+                nodes = watcher != null ? (List)((BackgroundPathable)this.client.getChildren().usingWatcher(watcher)).forPath(this.path) : (List)this.client.getChildren().forPath(this.path);//获取节点下的所有子节点注册监听(watcher默认都不是为空的,每一个都注册)
+            } catch (NoNodeException var8) {
+                throw new NoSuchElementException();
+            }
+    
+            Collections.sort(nodes);//对节点进行排序
+            Iterator var4 = nodes.iterator();
+            while(true) {//遍历
+                while(var4.hasNext()) {
+                    String node = (String)var4.next();//取得当前头结点
+                    if (node.startsWith("qn-")) {
+                        String thisPath = ZKPaths.makePath(this.path, node);
+                        try {
+                            byte[] bytes = (byte[])this.client.getData().forPath(thisPath);
+                            if (removeIt) {
+                                this.client.delete().forPath(thisPath);//删除该节点
+                            }
+                            return bytes;//返回节点的字节流
+            ...
+        }

三、多线程并发

对于分布式爬虫来说,让每一个消费者高效的进行抓取是具有重要意义的,为了加快爬虫的速度,采用多线程爬虫的方法。Java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。其中使用Executors提供了四种声明线程池的方法,分别是newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor和newScheduledThreadPool,为了监控实时监控队列的长度,我们使用数组型的阻塞队列ArrayBlockingQueue。声明方式如下:

    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+    ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+            0L, TimeUnit.MILLISECONDS,
+            queuelength);

四、使用

本次实验主要环境如下:

zookeeper.version=3.5
+java.version=1.8.0_65
+os.arch=amd64
+i5 四核心CPU
+网速为中国电信100M

这里主要是对博客园中的前两千条博客进行爬取,本文主要是对分布式队列的理解,就不再进行什么难度的处理(比如元素的选取、数据的存储等),只输出每篇博客的title即可。
生产者代码:

public class Producer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Producer.class);
+    public static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/Queue");
+    private static Integer j = 0;
+
+    public static void begin(String url) {//对博客园的每一页进行爬取
+        try {
+            String content = HttpHelper.getInstance().get(url);
+            resolveweb(content);
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+    public static void resolveweb(String content) throws Exception {
+        Elements elements = Jsoup.parse(content).select("a.titlelink");//对每篇博客的标题进行获取
+        for (Element element : elements) {
+            String url = element.attr("href");//
+            if (StringUtils.isNotEmpty(url) && !url.contains("javascript") && !url.contains("jump")) {//去除a中调用href过程
+                logger.info(url + " " + String.valueOf(j++));
+                queue.offer(url.getBytes());
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        client.start();
+        for (int i = 0; i < 100; i++) {
+            begin("https://www.cnblogs.com/#p" + String.valueOf(i));
+        }
+    }
+}

消费者

public class Consumer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
+    private static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/SinaQueue");
+    private static Integer i = 0;
+    private static final Integer CORE = Runtime.getRuntime().availableProcessors();
+    //声明为一个数组型的阻塞队列,这里限制大小为
+    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+
+    static class CBCrawler implements Runnable {
+        private String url;
+
+        public CBCrawler(String url) {
+            this.url = url;
+        }
+
+        @Override
+        public void run() {
+            String content = HttpHelper.getInstance().get(url);
+            logger.info(url + " " + Jsoup.parse(content).title());//打印网页的标题
+        }
+    }
+
+    public static void begin() {
+        try {
+            ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+                    0L, TimeUnit.MILLISECONDS,
+                    queuelength);
+            while (client.getChildren().forPath("/SinaQueue").size() > 0) {
+                CBCrawler crawler = new CBCrawler(new String(queue.take()));
+                es.submit(crawler);//执行爬虫
+                i = i + 1;
+                logger.info(String.valueOf(i) + " is finished\\n" + " queue size is" + queuelength.size());//监控当前队列的长度
+            }
+            if (!es.isShutdown()) {//如果线程池没有关闭则关闭
+                es.shutdown();
+            }
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        client.start();
+        begin();
+        client.close();
+        logger.info("start time: " + start);
+        long end = System.currentTimeMillis();
+        logger.info("end time: " + end);
+        logger.info("take time: " + String.valueOf(end - start));//记录开始时间和结束时间
+    }
+}

由于在队列的take中使用了CountDownLatch和Collections.sort(nodes)进行排序,耗时过程变长了不少,2000个节点,单台服务器和多台服务器的耗时是一样的,都是9分钟,具体实验见下面。

实验结果

生产者生产URL:

单机模式下的消费者,耗时:560825/(1000*60)=9分钟

分布式模式下的抓取:

耗时:564374/(1000*60)=9分钟:

由图可见,当每个消费者处理能力大于队列分配的能力时,耗时的过程反而是在队列,毕竟分布式队列在进行take动作的时候对节点进行了加锁,还要对队列进行排序,特别是在节点多达2000+的情况下,耗时是十分严重的。

实验二

实验二的主要解决的问题是将消费者处理的耗时延长,我们使用Thread.sleep(n)来模拟时长。由于博客园突然连不上,为了减少这种不可控的故障,抓取的网页改为新浪,并将抓取后的URL以文本形式保存下来。

public static void sleepUtil(Integer time) {
+    try {
+        Thread.sleep(time * 1000);
+    } catch (Exception e) {
+        logger.error("线程sleep异常", e);
+    }
+}

此时再看程序的输出,可以看出,队列的分发能力已经大于消费者的处理能力,总算是正常了。

分布式队列分发的时间是:341998/(1000*60)=5.6分钟

2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - start time: 1509324606460
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - end time: 1509324948458
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - take time: 341998

两台机子抓取完毕的耗时分别是:

A服务器:08:49:54.509——09:02:07  
+B服务器:08:49:54.509——09:05:05

单机的时候分发时间是:353198/(1000*60)=5.8分钟

2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - start time: 1509326672614
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - end time: 1509327025812
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - take time: 353198

耗时

09:24:33.391——09:51:44.733

分布式下平均耗时约为13分钟,单机模式下耗时约为27分钟,还是蛮符合估算的。

总结

源代码都放在这里了,有兴趣的可以star一下或者下载看一下,也欢迎大家提提意见,没企业级的实战环境,见笑了O(∩_∩)O~

欢迎访问我的个人网站
个人网站网址:http://www.wenzhihuai.com
个人网站代码地址:https://github.com/Zephery/newblog

`,57)]))}const p=s(l,[["render",k],["__file","基于ZooKeeper的队列爬虫.html.vue"]]),r=JSON.parse('{"path":"/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html","title":"基于ZooKeeper的队列爬虫","lang":"zh-CN","frontmatter":{"description":"基于ZooKeeper的队列爬虫 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。 简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的C...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"基于ZooKeeper的队列爬虫"}],["meta",{"property":"og:description","content":"基于ZooKeeper的队列爬虫 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。 简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的C..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171030102024.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-14T17:59:53.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-14T17:59:53.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"基于ZooKeeper的队列爬虫\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171030102024.png\\",\\"https://github-images.wenzhihuai.com/images/20171030102114.png\\",\\"https://github-images.wenzhihuai.com/images/20171026083433.png\\",\\"https://github-images.wenzhihuai.com/images/20171026084324.png\\",\\"https://github-images.wenzhihuai.com/images/20171026075704.png\\",\\"https://github-images.wenzhihuai.com/images/20171030103523.png\\",\\"https://github-images.wenzhihuai.com/images/20171030085127.png\\"],\\"dateModified\\":\\"2024-02-14T17:59:53.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、整体架构","slug":"一、整体架构","link":"#一、整体架构","children":[]},{"level":2,"title":"二、ZooKeeper队列原理","slug":"二、zookeeper队列原理","link":"#二、zookeeper队列原理","children":[{"level":3,"title":"2.1 介绍","slug":"_2-1-介绍","link":"#_2-1-介绍","children":[]},{"level":3,"title":"2.2 Watcher介绍","slug":"_2-2-watcher介绍","link":"#_2-2-watcher介绍","children":[]},{"level":3,"title":"2.3 源码","slug":"_2-3-源码","link":"#_2-3-源码","children":[]}]},{"level":2,"title":"三、多线程并发","slug":"三、多线程并发","link":"#三、多线程并发","children":[]},{"level":2,"title":"四、使用","slug":"四、使用","link":"#四、使用","children":[{"level":3,"title":"实验结果","slug":"实验结果","link":"#实验结果","children":[]},{"level":3,"title":"实验二","slug":"实验二","link":"#实验二","children":[]}]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1707204155000,"updatedTime":1707933593000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":3}]},"readingTime":{"minutes":9.55,"words":2864},"filePathRelative":"middleware/zookeeper/基于ZooKeeper的队列爬虫.md","localizedDate":"2024年2月6日","excerpt":"\\n

一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
\\n简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
\\n基本的知识就不过多介绍了,可以参考参考下面这些人的:
\\nZooKeeper官网
\\nhttp://www.cnblogs.com/wuxl360/p/5817471.html

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-B1ZIsZdu.js" "b/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-B1ZIsZdu.js" new file mode 100644 index 00000000..f80b7013 --- /dev/null +++ "b/assets/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html-B1ZIsZdu.js" @@ -0,0 +1,49 @@ +import{_ as s,c as a,d as e,o as n}from"./app-ftEjETWs.js";const t={};function h(l,i){return n(),a("div",null,i[0]||(i[0]=[e(`

基于kubernetes的分布式限流

做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

一、概念

限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。

1.1 使用场景

限流可以应对:

  • 热点业务带来的突发请求;
  • 调用方 bug 导致的突发请求;
  • 恶意攻击请求。

1.2 维度

对于限流场景,一般需要考虑两个维度的信息:
时间
限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定
资源
基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。
  限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。

image.png
image.png

1.3 分布式限流

分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。

二、分布式限流常用方案

基于Guava的客户端限流
Guava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。

网关层限流
服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。

中间件限流
将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。

限流组件
目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。

Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?

对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。

三、基于kubernetes的分布式限流

在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。

企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png

3.1 kubernetes中的副本数

在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。

func CountDeploymentSize(namespace string, deploymentName string) *int32 {
+	deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
+	if err != nil {
+		return nil
+	}
+	return deployment.Spec.Replicas
+}

用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。

3.2 rateLimiter的创建

在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。

private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
+        .maximumSize(10_000)
+        .refreshAfterWrite(20, TimeUnit.MINUTES)
+        .build(this::createRateLimit);
+//定义一个默认最小的QPS
+private static final Integer minQpsLimit = 3000;

之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。

public RateLimiter createRateLimit(String key) {
+    log.info("createRateLimit,key:{}", key);
+    int totalLimit = 获取总限频数,可以在数据库中定义
+    Integer replicas = kubernetesService.getDeploymentReplicas();
+    RateLimiter rateLimiter;
+    if (totalLimit > 0 && replicas == null) {
+        rateLimiter = RateLimiter.create(totalLimit);
+    } else if (totalLimit > 0) {
+        int nodeQpsLimit = totalLimit / replicas;
+        rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
+    } else {
+        rateLimiter = RateLimiter.create(minQpsLimit);
+    }
+    log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
+    return rateLimiter;
+}

3.3 rateLimiter的获取

根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽

public RateLimiter getRateLimiter(String key) {
+  return loadingCache.get(key);
+}

3.4 filter里的判断

最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS

public class RateLimiterFilter implements Filter {
+    @Resource
+    private RateLimiterService rateLimiterService;
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+        String key = httpServletRequest.getHeader("key");
+        RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
+        if (rateLimiter != null) {
+            if (rateLimiter.tryAcquire()) {
+                filterChain.doFilter(request, response);
+            } else {
+                httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+            }
+        } else {
+            filterChain.doFilter(request, response);
+        }
+    }
+}

四、性能压测

为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。

无限流

企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png

使用redis限流

其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显

企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png

自研限流

性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越

企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png

五、其他问题

5.1 对于保证qps限频准确的时候,应该怎么解决呢?

在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的

5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力

理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的

5.3 如果有多个副本,怎么保证请求是均匀的

这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。

参考

1.常见的分布式限流解决方案
2.分布式服务限流实战
3.高性能

`,56)]))}const p=s(t,[["render",h],["__file","基于kubernetes的分布式限流.html.vue"]]),r=JSON.parse('{"path":"/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html","title":"基于kubernetes的分布式限流","lang":"zh-CN","frontmatter":{"description":"基于kubernetes的分布式限流 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 一、概念 限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"基于kubernetes的分布式限流"}],["meta",{"property":"og:description","content":"基于kubernetes的分布式限流 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 一、概念 限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/1240-20240125210940819-6188218.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-26T03:58:56.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-26T03:58:56.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"基于kubernetes的分布式限流\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/1240-20240125210940819-6188218.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125211018162.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125211018144.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125211018162-6188218.png\\",\\"https://github-images.wenzhihuai.com/images/1240-20240125211018163.png\\"],\\"dateModified\\":\\"2024-01-26T03:58:56.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、概念","slug":"一、概念","link":"#一、概念","children":[{"level":3,"title":"1.1 使用场景","slug":"_1-1-使用场景","link":"#_1-1-使用场景","children":[]},{"level":3,"title":"1.2 维度","slug":"_1-2-维度","link":"#_1-2-维度","children":[]},{"level":3,"title":"1.3 分布式限流","slug":"_1-3-分布式限流","link":"#_1-3-分布式限流","children":[]}]},{"level":2,"title":"二、分布式限流常用方案","slug":"二、分布式限流常用方案","link":"#二、分布式限流常用方案","children":[]},{"level":2,"title":"三、基于kubernetes的分布式限流","slug":"三、基于kubernetes的分布式限流","link":"#三、基于kubernetes的分布式限流","children":[{"level":3,"title":"3.1 kubernetes中的副本数","slug":"_3-1-kubernetes中的副本数","link":"#_3-1-kubernetes中的副本数","children":[]},{"level":3,"title":"3.2 rateLimiter的创建","slug":"_3-2-ratelimiter的创建","link":"#_3-2-ratelimiter的创建","children":[]},{"level":3,"title":"3.3 rateLimiter的获取","slug":"_3-3-ratelimiter的获取","link":"#_3-3-ratelimiter的获取","children":[]},{"level":3,"title":"3.4 filter里的判断","slug":"_3-4-filter里的判断","link":"#_3-4-filter里的判断","children":[]}]},{"level":2,"title":"四、性能压测","slug":"四、性能压测","link":"#四、性能压测","children":[]},{"level":2,"title":"五、其他问题","slug":"五、其他问题","link":"#五、其他问题","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1649482551000,"updatedTime":1706241536000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":4}]},"readingTime":{"minutes":7.18,"words":2155},"filePathRelative":"java/基于kubernetes的分布式限流.md","localizedDate":"2022年4月9日","excerpt":"\\n

做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/\345\237\272\347\241\200.html-Bsofr9G8.js" "b/assets/\345\237\272\347\241\200.html-Bsofr9G8.js" new file mode 100644 index 00000000..32bdbb55 --- /dev/null +++ "b/assets/\345\237\272\347\241\200.html-Bsofr9G8.js" @@ -0,0 +1 @@ +import{_ as a,c as r,d as s,o as t}from"./app-ftEjETWs.js";const i={};function n(c,e){return t(),r("div",null,e[0]||(e[0]=[s('

基础

一、ElasticSearch基础

1、什么是Elasticsearch:
Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

2、Elasticsearch 的基本概念:

(1)index 索引:索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。

(2)type 类型:类型是用来定义数据结构,可以认为是 mysql 中的一张表,type 是 index 中的一个逻辑数据分类

(3)document 文档:类似于 MySQL 中的一行,不同之处在于 ES 中的每个文档可以有不同的字段,但是对于通用字段应该具有相同的数据类型,文档是es中的最小数据单元,可以认为一个文档就是一条记录。

(4)Field 字段:Field是Elasticsearch的最小单位,一个document里面有多个field

(5)shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

(6)replica 副本:任何服务器随时可能故障或宕机,此时 shard 可能会丢失,通过创建 replica 副本,可以在 shard 故障时提供备用服务,保证数据不丢失,另外 replica 还可以提升搜索操作的吞吐量。

shard 分片数量在建立索引时设置,设置后不能修改,默认5个;replica 副本数量默认1个,可随时修改数量;

(7)segment:每个shard分片是一个lucene实例,每个分片由多个segment组成!!每个segment占用内存,文件句柄等。segment 合并的过程,需要先读取小的 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,合并大的 segment 需要消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。所以 Elasticsearch 在默认情况下会对合并线程进行资源限制,确保它不会对搜索性能造成太大影响。

image-20240224213112456
image-20240224213112456

3、什么是倒排索引:
在搜索引擎中,每个文档都有对应的文档 ID,文档内容可以表示为一系列关键词的集合,例如,某个文档经过分词,提取了 20 个关键词,而通过倒排索引,可以记录每个关键词在文档中出现的次数和出现位置。也就是说,倒排索引是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。

要注意倒排索引的两个细节:

倒排索引中的所有词项对应一个或多个文档
倒排索引中的词项 根据字典顺序升序排列

img
img

4、doc_values 的作用:
倒排索引虽然可以提高搜索性能,但也存在缺陷,比如我们需要对数据做排序或聚合等操作时,lucene 会提取所有出现在文档集合的排序字段,然后构建一个排好序的文档集合,而这个步骤是基于内存的,如果排序数据量巨大的话,容易造成内存溢出和性能缓慢。
doc_values 就是 es 在构建倒排索引的同时,会对开启 doc_values 的字段构建一个有序的 “document文档 ==> field value” 的列式存储映射,可以看作是以文档维度,实现了根据指定字段进行排序和聚合的功能,降低对内存的依赖。另外 doc_values 保存在操作系统的磁盘中,当 doc_values 大于节点的可用内存,ES可以从操作系统页缓存中加载或弹出,从而避免发生内存溢出的异常,但如果 docValues 远小于节点的可用内存,操作系统就自然将所有 doc_values 存于内存中(堆外内存),有助于快速访问。

5、text 和 keyword类型的区别:
两个类型的区别主要是分词:keyword 类型是不会分词的,直接根据字符串内容建立倒排索引,所以keyword类型的字段只能通过精确值搜索到;Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引

6、query 和 filter 的区别?

(1)query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;

(2)filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。

二、ES的写入流程

1、ES写数据的整体流程:

image-20240223165512655
image-20240223165512655

(1)客户端选择 ES 的某个 node 发送请求过去,这个 node 就是协调节点 coordinating node
(2)coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)
(3)实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node
(4)coordinating node 等到 primary node 和所有 replica node 都执行成功之后,最后返回响应结果给客户端。
2、ES主分片写数据的详细流程:

Elasticsearch索引文档的过程
Elasticsearch索引文档的过程

(1)主分片先将数据写入ES的 memory buffer,然后定时(默认1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入操作系统缓存 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 segment 文件实际上是一些倒排索引的集合, 只有经历了 refresh 操作之后,这些数据才能变成可检索的。

ES 的近实时性:数据存在 memory buffer 时是搜索不到的,只有数据被 refresh 到 Filesystem cache 之后才能被搜索到,而 refresh 是每秒一次, 所以称 es 是近实时的;可以手动调用 es 的 api 触发一次 refresh 操作,让数据马上可以被搜索到;

(2)由于 memory Buffer 和 Filesystem Cache 都是基于内存,假设服务器宕机,那么数据就会丢失,所以 ES 通过 translog 日志文件来保证数据的可靠性,在数据写入 memory buffer 的同时,将数据也写入 translog 日志文件中,当机器宕机重启时,es 会自动读取 translog 日志文件中的数据,恢复到 memory buffer 和 Filesystem cache 中去。

ES 数据丢失的问题:translog 也是先写入 Filesystem cache,然后默认每隔 5 秒刷一次到磁盘中,所以默认情况下,可能有 5 秒的数据会仅仅停留在 memory buffer 或者 translog 文件的 Filesystem cache中,而不在磁盘上,如果此时机器宕机,会丢失 5 秒钟的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

(3)flush 操作:不断重复上面的步骤,translog 会变得越来越大,不过 translog 文件默认每30分钟或者 阈值超过 512M 时,就会触发 commit 操作,即 flush操作,将 memory buffer 中所有的数据写入新的 segment 文件中, 并将内存中所有的 segment 文件全部落盘,最后清空 translog 事务日志。

① 将 memory buffer 中的数据 refresh 到 Filesystem Cache 中去,清空 buffer;
② 创建一个新的 commit point(提交点),同时强行将 Filesystem Cache 中目前所有的数据都 fsync 到磁盘文件中;
③ 删除旧的 translog 日志文件并创建一个新的 translog 日志文件,此时 commit 操作完成
更多 ES 的数据写入流程的说明欢迎阅读这篇文章:ElasticSearch搜索引擎:数据的写入流程

三、ES的更新和删除流程

​ 删除和更新都是写操作,但是由于 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;所以 ES 利用 .del 文件 标记文档是否被删除,磁盘上的每个段都有一个相应的.del 文件

(1)如果是删除操作,文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉。

(2)如果是更新操作,就是将旧的 doc 标识为 deleted 状态,然后创建一个新的 doc。

memory buffer 每 refresh 一次,就会产生一个 segment 文件 ,所以默认情况下是 1s 生成一个 segment 文件,这样下来 segment 文件会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment 文件合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,不写入到新的 segment 中,然后将新的 segment 文件写入磁盘,这里会写一个 commit point ,标识所有新的 segment 文件,然后打开 segment 文件供搜索使用,同时删除旧的 segment 文件

有关segment段合并过程,欢迎阅读这篇文章:Elasticsearch搜索引擎:ES的segment段合并原理

四、ES的搜索流程

搜索被执行成一个两阶段过程,即 Query Then Fetch:

1、Query阶段:

客户端发送请求到 coordinate node,协调节点将搜索请求广播到所有的 primary shard 或 replica,每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。接着每个分片返回各自优先队列中 所有 docId 和 打分值 给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

2、Fetch阶段:
协调节点根据 Query阶段产生的结果,去各个节点上查询 docId 实际的 document 内容,最后由协调节点返回结果给客户端。

coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
接收请求的 node 返回 document 给 coordinate node 。
coordinate node 返回 document 给客户端。
Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。

五、ES在高并发下如何保证读写一致性?

(1)对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖

每个文档都有一个_version 版本号,这个版本号在文档被改变时加一。Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,它会被简单的忽略。

利用_version的这一优点确保数据不会因为修改冲突而丢失,比如指定文档的version来做更改,如果那个版本号不是现在的,我们的请求就失败了。

(2)对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。

one:写操作只要有一个primary shard是active活跃可用的,就可以执行
all:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
quorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。

六、ES集群如何选举Master节点

1、Elasticsearch 的分布式原理:

Elasticsearch 会对存储的数据进行切分,划分到不同的分片上,同时每一个分片会生成多个副本,从而保证分布式环境的高可用。ES集群中的节点是对等的,节点间会选出集群的 Master,由 Master 会负责维护集群状态信息,并同步给其他节点。

Elasticsearch 的性能会不会很低:不会,ES只有建立 index 和 type 时需要经过 Master,而数据的写入有一个简单的 Routing 规则,可以路由到集群中的任意节点,所以数据写入压力是分散在整个集群的。

2、ES集群 如何 选举 Master:

Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;

(1)确认候选主节点的最少投票通过数量(elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes)
(2)选举时,集群中每个节点对所有 master候选节点(node.master: true)根据 nodeId 进行字典排序,然后选出第一个节点(第0位),暂且认为它是master节点。
(3)如果对某个节点的投票数达到阈值,并且该节点自己也选举自己,那这个节点就是master;否则重新选举一直到满足上述条件。
补充:master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。

3、Elasticsearch是如何避免脑裂现象:

(1)当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;

(2)当集群 master 候选节点 只有两个时,这种情况是不合理的,最好把另外一个node.master改成false。如果我们不改节点设置,还是套上面的(N/2)+1公式,此时discovery.zen.minimum_master_nodes应该设置为2。这就出现一个问题,两个master备选节点,只要有一个挂,就选不出master了

七、建立索引阶段性能提升方法

(1)如果是大批量导入,可以设置 index.number_of_replicas: 0 关闭副本,等数据导入完成之后再开启副本
(2)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
(3)如果搜索结果不需要近实时性,可以把每个索引的 index.refresh_interval 改到30s
(4)增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB
(5)使用 SSD 存储介质
(6)段和合并:Elasticsearch 默认值是 20 MB/s。但如果用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。

八、ES的深度分页与滚动搜索scroll

(1)深度分页:

深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深。

(2)滚动搜索:
一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。

参考

1.ElasticSearch搜索引擎
2.2023最新整理的 Elasticsearch 21道面试题
3.elasticsearch中文官网
4.elasticsearch 倒排索引原理

',69)]))}const p=a(i,[["render",n],["__file","基础.html.vue"]]),o=JSON.parse('{"path":"/database/elasticsearch/%E5%9F%BA%E7%A1%80.html","title":"基础","lang":"zh-CN","frontmatter":{"order":100,"description":"基础 一、ElasticSearch基础 1、什么是Elasticsearch: Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。 全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"基础"}],["meta",{"property":"og:description","content":"基础 一、ElasticSearch基础 1、什么是Elasticsearch: Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。 全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20240224213112456.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-24T15:02:58.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-24T15:02:58.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"基础\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20240224213112456.png\\",\\"https://github-images.wenzhihuai.com/images/v2-e4599b618e270df9b64a75eb77bfb326_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/image-20240223165512655.png\\",\\"https://github-images.wenzhihuai.com/images/format,png.png\\"],\\"dateModified\\":\\"2024-02-24T15:02:58.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、ElasticSearch基础","slug":"一、elasticsearch基础","link":"#一、elasticsearch基础","children":[]},{"level":2,"title":"二、ES的写入流程","slug":"二、es的写入流程","link":"#二、es的写入流程","children":[]},{"level":2,"title":"三、ES的更新和删除流程","slug":"三、es的更新和删除流程","link":"#三、es的更新和删除流程","children":[]},{"level":2,"title":"四、ES的搜索流程","slug":"四、es的搜索流程","link":"#四、es的搜索流程","children":[]},{"level":2,"title":"五、ES在高并发下如何保证读写一致性?","slug":"五、es在高并发下如何保证读写一致性","link":"#五、es在高并发下如何保证读写一致性","children":[]},{"level":2,"title":"六、ES集群如何选举Master节点","slug":"六、es集群如何选举master节点","link":"#六、es集群如何选举master节点","children":[]},{"level":2,"title":"七、建立索引阶段性能提升方法","slug":"七、建立索引阶段性能提升方法","link":"#七、建立索引阶段性能提升方法","children":[]},{"level":2,"title":"八、ES的深度分页与滚动搜索scroll","slug":"八、es的深度分页与滚动搜索scroll","link":"#八、es的深度分页与滚动搜索scroll","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1708786978000,"updatedTime":1708786978000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":14.53,"words":4360},"filePathRelative":"database/elasticsearch/基础.md","localizedDate":"2024年2月24日","excerpt":"\\n

一、ElasticSearch基础

\\n

1、什么是Elasticsearch:
\\nElasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
\\n全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

","autoDesc":true}');export{p as comp,o as data}; diff --git "a/assets/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html-BNnmGgbc.js" "b/assets/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html-BNnmGgbc.js" new file mode 100644 index 00000000..59f2b22d --- /dev/null +++ "b/assets/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html-BNnmGgbc.js" @@ -0,0 +1,8 @@ +import{_ as i,c as a,d as s,o as t}from"./app-ftEjETWs.js";const n={};function p(l,e){return t(),a("div",null,e[0]||(e[0]=[s(`

小程序反编译

第一步:电脑端提取

先找到小程序保存的地址,一般先找到微信的文件管理下

image-20240310171919281

然后到该目录的C:\\Users\\w1570\\Documents\\WeChat Files\\Applet即可看到相关小程序了。

image-20240310173802224
image-20240310173802224

找到需要解压的包__APP__.wxapkg,拷贝到和pc_wxapkg_decrypt.exe的统一路径下。

第二步:解密wxapkg包

pc_wxapkg_decrypt.exe -wxid 微信小程序id -in 要解密的wxapkg路径 -out 解密后的路径
+//示例如下
+pc_wxapkg_decrypt.exe -wxid wx7444167f2a6427b3 -in __APP__.wxapkg

第三步:解包

切换到./nodejs目录下,解压\`node_modules.zip\`后,使用cmd命令打开
+输入下面命令
+
+node wuWxapkg.js ..\\decrypt\\dec.wxapkg
+
+第二个参数为操作的项目,这里操作的是666.wxapkg 记得改为自己的

发现偶尔还是有些html没有正常解析,有点奇怪

`,14)]))}const r=i(n,[["render",p],["__file","小程序反编译.html.vue"]]),d=JSON.parse('{"path":"/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html","title":"小程序反编译","lang":"zh-CN","frontmatter":{"description":"小程序反编译 第一步:电脑端提取 先找到小程序保存的地址,一般先找到微信的文件管理下 image-20240310171919281 然后到该目录的C:\\\\Users\\\\w1570\\\\Documents\\\\WeChat Files\\\\Applet即可看到相关小程序了。 image-20240310173802224image-20240310173802224 ...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"小程序反编译"}],["meta",{"property":"og:description","content":"小程序反编译 第一步:电脑端提取 先找到小程序保存的地址,一般先找到微信的文件管理下 image-20240310171919281 然后到该目录的C:\\\\Users\\\\w1570\\\\Documents\\\\WeChat Files\\\\Applet即可看到相关小程序了。 image-20240310173802224image-20240310173802224 ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20240310173802224.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"小程序反编译\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20240310173802224.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"第一步:电脑端提取","slug":"第一步-电脑端提取","link":"#第一步-电脑端提取","children":[]},{"level":3,"title":"第二步:解密wxapkg包","slug":"第二步-解密wxapkg包","link":"#第二步-解密wxapkg包","children":[]},{"level":3,"title":"第三步:解包","slug":"第三步-解包","link":"#第三步-解包","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.82,"words":246},"filePathRelative":"interesting/小程序反编译.md","localizedDate":"2024年3月10日","excerpt":"\\n

第一步:电脑端提取

\\n

先找到小程序保存的地址,一般先找到微信的文件管理下

","autoDesc":true}');export{r as comp,d as data}; diff --git "a/assets/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html-Br8UVQp8.js" "b/assets/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html-Br8UVQp8.js" new file mode 100644 index 00000000..d274dc88 --- /dev/null +++ "b/assets/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html-Br8UVQp8.js" @@ -0,0 +1,5 @@ +import{_ as s,c as a,d as t,o as e}from"./app-ftEjETWs.js";const n={};function h(l,i){return e(),a("div",null,i[0]||(i[0]=[t(`

主流GC收集器采用的算法

垃圾回收器采用的GC算法代次
Serial复制新生代
Parallel复制新生代
ParNew复制新生代
CMS (Concurrent Mark-Sweep)标记-清除老年代
G1 (Garbage-First)标记-整理老年代
ZGC (Z Garbage Collector)标记-整理老年代
Shenandoah标记-复制(独立的全局复制阶段)老年代

上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。

是否可以使用两种或多种垃圾回收器

在Java中,你不能同时使用两种不同的垃圾回收器。Serial和G1是两种不同的垃圾回收器,它们有不同的设计目标和使用场景。

Serial垃圾回收器是一个单线程的垃圾回收器,它在进行垃圾回收时会暂停所有的应用线程。这种垃圾回收器适合于单核处理器或者内存较小的系统。

G1垃圾回收器是一种并行和并发的垃圾回收器,它可以处理大量的堆内存并且尽量减少垃圾回收引起的暂停时间。G1垃圾回收器适合于多核处理器和内存较大的系统。

如果你想在你的应用中使用某种垃圾回收器,你可以通过JVM的命令行选项来指定,例如"-XX:+UseSerialGC"可以启用Serial垃圾回收器,"-XX:+UseG1GC"可以启用G1垃圾回收器。但是你不能同时启用两种垃圾回收器。

查看java进程用的是哪种垃圾回收器

首先,你需要找到你的Java进程的进程ID。你可以使用jps命令来列出所有的Java进程和它们的进程ID。

然后,你可以使用jinfo -flags 进程ID命令来查看Java进程的JVM标志。在输出的信息中,你可以找到-XX:+UseXXXGC这样的标志,这个标志表示Java进程使用的垃圾回收器。例如,如果你看到-XX:+UseG1GC,那么这个Java进程就是使用G1垃圾回收器。
样例:

[root@xxxx-1-b68d78f6d-b9nnq /data/logs]# jinfo -flags 4868
+VM Flags:
+-XX:CICompilerCount=2 -XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=2 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=2147483648 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=1287651328 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5826188 -XX:NonProfiledCodeHeapSize=122916026 -XX:ProfiledCodeHeapSize=122916026 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

怎么查看java进程eden、survivor等各个区的内存占用

用jstat -gc 进程ID命令来查看Java进程的堆内存使用情况。这个命令会显示一些列的数据,包括:

S0C、S1C、S0U、S1U:Survivor 0和Survivor 1区的当前容量(Capacity)和使用量(Used)。
EC、EU:Eden区的当前容量和使用量。
OC、OU:Old区的当前容量和使用量。
MC、MU:元数据区的当前容量和使用量。
CCSC、CCSU:压缩类空间的当前容量和使用量。
这些数据都是以KB为单位的。你可以通过这些数据来了解Java进程的堆内存使用情况。
样例:

[root@xxxx-1-b68d78f6d-b9nnq /data/logs]# jstat -gc  4868
+ S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT   
+ 0.0   5120.0  0.0   5120.0 1315840.0 871424.0  776192.0   300002.0  123212.0 117919.3 14464.0 12606.6  68825 1274.741   0      0.000  46      0.664 1275.405

参考:

1.儒猿课堂

`,18)]))}const r=s(n,[["render",h],["__file","常用GC回收器.html.vue"]]),p=JSON.parse('{"path":"/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html","title":"主流GC收集器采用的算法","lang":"zh-CN","frontmatter":{"description":"主流GC收集器采用的算法 上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。 是否可以使用两种或多种垃圾回收器 在Java中,你不能同时使用两种不同的垃圾回收器。Serial和G1是两种不同的垃圾回收器,它们...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"主流GC收集器采用的算法"}],["meta",{"property":"og:description","content":"主流GC收集器采用的算法 上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。 是否可以使用两种或多种垃圾回收器 在Java中,你不能同时使用两种不同的垃圾回收器。Serial和G1是两种不同的垃圾回收器,它们..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T11:46:28.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T11:46:28.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"主流GC收集器采用的算法\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T11:46:28.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"是否可以使用两种或多种垃圾回收器","slug":"是否可以使用两种或多种垃圾回收器","link":"#是否可以使用两种或多种垃圾回收器","children":[]},{"level":2,"title":"查看java进程用的是哪种垃圾回收器","slug":"查看java进程用的是哪种垃圾回收器","link":"#查看java进程用的是哪种垃圾回收器","children":[]},{"level":2,"title":"怎么查看java进程eden、survivor等各个区的内存占用","slug":"怎么查看java进程eden、survivor等各个区的内存占用","link":"#怎么查看java进程eden、survivor等各个区的内存占用","children":[]},{"level":2,"title":"参考:","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1708077309000,"updatedTime":1708083988000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":2.95,"words":886},"filePathRelative":"java/JVM/常用GC回收器.md","localizedDate":"2024年2月16日","excerpt":"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
垃圾回收器采用的GC算法代次
Serial复制新生代
Parallel复制新生代
ParNew复制新生代
CMS (Concurrent Mark-Sweep)标记-清除老年代
G1 (Garbage-First)标记-整理老年代
ZGC (Z Garbage Collector)标记-整理老年代
Shenandoah标记-复制(独立的全局复制阶段)老年代
","autoDesc":true}');export{r as comp,p as data}; diff --git "a/assets/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html-CPwBtyqR.js" "b/assets/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html-CPwBtyqR.js" new file mode 100644 index 00000000..776bc041 --- /dev/null +++ "b/assets/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html-CPwBtyqR.js" @@ -0,0 +1,108 @@ +import{_ as s,c as a,d as n,o as h}from"./app-ftEjETWs.js";const t={};function l(e,i){return h(),a("div",null,i[0]||(i[0]=[n(`

广州图书馆借阅抓取

欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于我)页面。整个代码采用HttpClient,存储放在MySql,定时使用Spring自带的Schedule,下面是抓取的过程。

1.页面跳转过程

一般都是进入首页http://www.gzlib.gov.cn/,点击进登陆页面,然后输入账号密码。表面上看起来没什么特别之处,实际上模拟登陆的时候不仅仅是向链接post一个请求那么简单,得到的response要么跳回登陆页面,要么无限制重定向。

事实上,它做了单点登录,如下图,广州图书馆的网址为:www.gzlib.gov.cn,而登陆的网址为:login.gzlib.gov.cn。原理网上很多人都讲的很好了,可以看看这篇文章SSO单点登录

2.处理方法

解决办法不难,只要先模拟访问一下首页即可获取图书馆的session,python的获取代码如:session.get("http://www.gzlib.gov.cn/"),打印cookie之后如下:

[<Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for .gzlib.gov.cn/>, <Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for www.gzlib.gov.cn/>, <Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]

整个登陆抓取的流程如下:

即:
(1)用户先点击广州图书馆的首页,以获取改网址的session,然后点击登录界面,解析html,获取lt(自定义的参数,类似于验证码),以及单点登录服务器的session。
(2)向目标服务器(单点登录服务器)提交post请求,请求参数中包含username(用户名),password(密码),event(时间,默认为submit),lt(自定义请求参数),同时服务端还要验证的参数:refer(来源页面),host(主机信息),Content-Type(类型)。
(3)打印response,搜索你自己的名字,如果有则表示成功了,否则会跳转回登陆页面。
(4)利用cookie去访问其他页面,此处实现的是对借阅历史的抓取,所以访问的页面是:http://www.gzlib.gov.cn/member/historyLoanList.jspx。

基本的模拟登陆和获取就是这些,之后还有对面html的解析,获取书名、书的索引等,然后封装成JavaBean,再之后便是保存入数据库。(去重没有做,不知道用什么方式比较好)

3.代码

3.1 Java中,一般用来提交http请求的大部分用的都是httpclient,首先,需要导入的httpclient相关的包:

<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpclient</artifactId>
+    <version>4.5.3</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpcore</artifactId>
+    <version>4.4.7</version>
+</dependency>

3.2 构建声明全局变量——上下文管理器,其中context为上下文管理器

public class LibraryUtil {
+    private static CloseableHttpClient httpClient = null;
+    private static HttpClientContext context = null;
+    private static CookieStore cookieStore = null;
+    static {
+        init();
+    }
+    private static void init() {
+        context = HttpClientContext.create();
+        cookieStore = new BasicCookieStore();
+        // 配置超时时间
+        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(12000).setSocketTimeout(6000)
+                .setConnectionRequestTimeout(6000).build();
+        // 设置默认跳转以及存储cookie
+        httpClient = HttpClientBuilder.create()
+                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
+                .setRedirectStrategy(new DefaultRedirectStrategy()).setDefaultRequestConfig(requestConfig)
+                .setDefaultCookieStore(cookieStore).build();
+    }
+    ...

3.3 声明一个get函数,其中header可自定义,此处不需要,但是保留着,做成一个通用的吧。

    public static CloseableHttpResponse get(String url, Header[] header) throws IOException {
+        HttpGet httpget = new HttpGet(url);
+        if (header != null && header.length > 0) {
+            httpget.setHeaders(header);
+        }
+        CloseableHttpResponse response = httpClient.execute(httpget, context);//context用于存储上下文
+        return response;
+    }

3.4 访问首页以获得session,服务器上会话是使用session存储的,本地浏览器使用的是cookie,只要本地不退出,那么使用本地的cookie来访问也是可以的,但是为了达到模拟登陆的效果,这里就不再阐述这种方式。

CloseableHttpResponse homeResponse = get("http://www.gzlib.gov.cn/", null);
+homeResponse.close();

此时,如果打印cookie,可以看到目前的cookie如下:

<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]>

3.5 访问登陆页面,获取单点登录服务器之后的cookie,解析网页,获取自定义参数lt。这里的解析网页使用了Jsoup,语法和python中的BeautifulSoup中类似。

String loginURL = "http://login.gzlib.gov.cn/sso-server/login?service=http%3A%2F%2Fwww.gzlib.gov.cn%2Flogin.jspx%3FreturnUrl%3Dhttp%253A%252F%252Fwww.gzlib.gov.cn%252F%26locale%3Dzh_CN&appId=www.gzlib.gov.cn&locale=zh_CN";
+CloseableHttpResponse loginGetResponse = get(loginURL, null);
+String content = toString(loginGetResponse);
+String lt = Jsoup.parse(content).select("form").select("input[name=lt]").attr("value");
+loginGetResponse.close();

此时,再次查看cookie,多了一个(www.gzlib.gov.cn/sso-server):

<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>

3.6 声明一个post函数,用来提交post请求,其中提交的参数默认为

    public static CloseableHttpResponse postParam(String url, String parameters, Header[] headers)
+            throws IOException {
+        System.out.println(parameters);
+        HttpPost httpPost = new HttpPost(url);
+        if (headers != null && headers.length > 0) {
+            for (Header header : headers) {
+                httpPost.addHeader(header);
+            }
+        }
+        List<NameValuePair> nvps = toNameValuePairList(parameters);
+        httpPost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
+        CloseableHttpResponse response = httpClient.execute(httpPost, context);
+        return response;
+    }

3.7 登陆成功后,如果没有声明returnurl,即登录链接为(http://login.gzlib.gov.cn/sso-server/login),那么只是会显示成功登录的页面:

后台应该是定义了一个service用来进行链接跳转的,想要获取登录成功之后的跳转页面可修改service之后的链接,这里将保持原始状态。此时,查看cookie结果如下:

<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie CASTGC=TGT-198235-zkocmYyBP6c9G7EXjKyzgKR7I40QI4JBalTkrnr9U6ZkxuP6Tn for www.gzlib.gov.cn/sso-server>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>

其中,出现CASTGC表明登陆成功了,可以使用该cookie来访问广州图书馆的其他页面,在python中是直接跳转到其他页面,而在java使用httpclient过程中,看到的并不是直接的跳转,而是一个302重定向,打印Header之后结果如下图:

认真研究一下链接,就会发现服务器相当于给了一张通用票ticket,即:可以使用该ticket访问任何页面,而returnUrl则是返回的页面。这里我们直接访问该重定向的url。

Header header = response.getHeaders("Location")[0];
+CloseableHttpResponse home = get(header.getValue(), null);

然后打印页面,即可获取登陆之后跳回的首页。

3.8 解析html
获取session并跳回首页之后,再访问借阅历史页面,然后对结果进行html解析,python中使用了BeautifulSoup,简单而又实用,java中的jsoup也是一个不错的选择。

        String html = getHTML();
+        Element element = Jsoup.parse(html).select("table.jieyue-table").get(0).select("tbody").get(0);
+        Elements trs = element.select("tr");
+        for (int i = 0; i < trs.size(); i++) {
+            Elements tds = trs.get(i).select("td");
+            System.out.println(tds.get(1).text());
+        }

输出结果:

企业IT架构转型之道
+大话Java性能优化
+深入理解Hadoop
+大话Java性能优化
+Java EE开发的颠覆者:Spring Boot实战
+大型网站技术架构:核心原理与案例分析
+Java性能权威指南
+Akka入门与实践
+高性能网站建设进阶指南:Web开发者性能优化最佳实践:Performance best practices for Web developers
+Java EE开发的颠覆者:Spring Boot实战
+深入理解Hadoop
+大话Java性能优化

点击查看源码

总结

目前,改代码已经整合进个人网站之中,每天定时抓取一次,但是仍有很多东西没有做(如分页、去重等),有兴趣的可以研究一下源码,要是能帮忙完善就更好了。感谢Thanks♪(・ω・)ノ。整个代码接近250行,当然...包括了注释,但是使用python之后,也不过25行=w=,这里贴一下python的源码吧。同时,欢迎大家访问我的个人网站,也欢迎大家能给个star

import urllib.parse
+import requests
+from bs4 import BeautifulSoup
+
+session = requests.session()
+session.get("http://www.gzlib.gov.cn/")
+session.headers.update(
+    {"Referer": "http://www.gzlib.gov.cn/member/historyLoanList.jspx",
+     "origin": "http://login.gzlib.gov.cn",
+     'Content-Type': 'application/x-www-form-urlencoded',
+     'host': 'www.gzlib.gov.cn',
+     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'
+     }
+)
+baseURL = "http://login.gzlib.gov.cn/sso-server/login"
+soup = BeautifulSoup(session.get(baseURL).text, "html.parser")
+lt = soup.select("form")[0].find(attrs={'name': 'lt'})['value']
+postdict = {"username": "你的身份证",
+            "password": "密码(默认为身份证后6位)",
+            "_eventId": "submit",
+            "lt": lt
+            }
+postdata = urllib.parse.urlencode(postdict)
+session.post(baseURL, postdata)
+print(session.get("http://www.gzlib.gov.cn/member/historyLoanList.jspx").text)
`,49)]))}const p=s(t,[["render",l],["__file","广州图书馆借阅抓取.html.vue"]]),r=JSON.parse('{"path":"/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html","title":"广州图书馆借阅抓取","lang":"zh-CN","frontmatter":{"description":"广州图书馆借阅抓取 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。 搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"广州图书馆借阅抓取"}],["meta",{"property":"og:description","content":"广州图书馆借阅抓取 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。 搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/20171013042700.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"广州图书馆借阅抓取\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/20171013042700.png\\",\\"https://github-images.wenzhihuai.com/images/20171013043304.png\\",\\"https://github-images.wenzhihuai.com/images/20171014114353.png\\",\\"https://github-images.wenzhihuai.com/images/20171016090010.png\\",\\"https://github-images.wenzhihuai.com/images/20171010062602.png\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"1.页面跳转过程","slug":"_1-页面跳转过程","link":"#_1-页面跳转过程","children":[]},{"level":3,"title":"2.处理方法","slug":"_2-处理方法","link":"#_2-处理方法","children":[]},{"level":3,"title":"3.代码","slug":"_3-代码","link":"#_3-代码","children":[]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1706596625000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":6.83,"words":2049},"filePathRelative":"interesting/广州图书馆借阅抓取.md","localizedDate":"2024年1月30日","excerpt":"\\n

欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html-BKoUISo4.js" "b/assets/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html-BKoUISo4.js" new file mode 100644 index 00000000..8e1e1d62 --- /dev/null +++ "b/assets/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html-BKoUISo4.js" @@ -0,0 +1 @@ +import{_ as o,c as s,d as t,o as a}from"./app-ftEjETWs.js";const h={};function r(n,e){return a(),s("div",null,e[0]||(e[0]=[t('

开发工具整理

ShadowsocksX

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
win端:https://github.com/shadowsocks/shadowsocks-windows
安卓:https://github.com/shadowsocks/shadowsocks-android

IDEA激活

许可证激活(License server)
http://vip.52shizhan.cn
http://fls.itxe.net

',6)]))}const i=o(h,[["render",r],["__file","开发工具整理.html.vue"]]),d=JSON.parse('{"path":"/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html","title":"开发工具整理","lang":"zh-CN","frontmatter":{"description":"开发工具整理 ShadowsocksX mac端:https://github.com/shadowsocks/ShadowsocksX-NG win端:https://github.com/shadowsocks/shadowsocks-windows 安卓:https://github.com/shadowsocks/shadowsocks-and...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"开发工具整理"}],["meta",{"property":"og:description","content":"开发工具整理 ShadowsocksX mac端:https://github.com/shadowsocks/ShadowsocksX-NG win端:https://github.com/shadowsocks/shadowsocks-windows 安卓:https://github.com/shadowsocks/shadowsocks-and..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-05-11T18:24:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-05-11T18:24:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"开发工具整理\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-05-11T18:24:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"ShadowsocksX","slug":"shadowsocksx","link":"#shadowsocksx","children":[]},{"level":2,"title":"IDEA激活","slug":"idea激活","link":"#idea激活","children":[]},{"level":2,"title":"","slug":"","link":"#","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1715451875000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.12,"words":36},"filePathRelative":"interesting/开发工具整理.md","localizedDate":"2024年3月10日","excerpt":"\\n

ShadowsocksX

\\n

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
\\nwin端:https://github.com/shadowsocks/shadowsocks-windows
\\n安卓:https://github.com/shadowsocks/shadowsocks-android

","autoDesc":true}');export{i as comp,d as data}; diff --git "a/assets/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html-BVqGPH4F.js" "b/assets/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html-BVqGPH4F.js" new file mode 100644 index 00000000..a98b9892 --- /dev/null +++ "b/assets/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html-BVqGPH4F.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as n,o as r}from"./app-ftEjETWs.js";const o={};function s(i,e){return r(),a("div",null,e[0]||(e[0]=[n('

引用计数和根可达算法

本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

硬件角度来看什么叫做垃圾?

计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit位构成的。同时,内存需要在通电状态保持按周期频率的刷新,才能够维持数据的状态。

在内存中,某些内存区域(bit位)被使用过了(用来存储对象或者数据)会被打上标记表示已经被使用过了,分配新内存空间的时候就计算机硬件就不会使用这些被标记过的内存区域。通常,这些给内存区域打上标记的活儿一般由操作系统的系统调用来完成,例如C语言中的 allocate()函数调用的系统函数。

在面向对象的语言中,如果程序员新建了一个对象,那么操作系统会为其分配相应的内存区域,且该部分的内存区域会被打上标记说明已经被使用过了,这样操作系统就不会向该区域写入新的数据。

然而,如果这个标记没有被程序员释放(C语言中调用free()来释放内存区域),那么该内存区域会一直被标记成已使用,如果整个内存都被标记成为了已使用,那么当操作系统想要再分配内存的时候就会失败,这个时候只有重启电脑来解决问题了。

所以说,对于内存区域中不再使用的对象,我们需要释放掉其对应的内存区域,方便新的对象创建时有空间为其分配。而这些程序中不再被使用的对象就被称为“垃圾”,“垃圾”往往对应着内存中一块儿需要被释放的区域。

Java中的垃圾

Java中"垃圾"通常指的是不再被程序使用和引用的对象,具体表现在没有被栈、JNI指针和永久代对象所引用的对象。Java作为一种面向对象的编程语言,它使用自动内存管理机制,其中垃圾收集器负责检测和回收不再被程序引用的对象,以释放它们占用的内存空间。以下是一些导致对象成为垃圾的常见情况:

  • 无引用对象: 当一个对象没有任何引用指向它时,它就变得不可达,成为垃圾,Java的垃圾收集器会识别这样的对象,并将它们回收。
  • 引用循环: 如果一组对象彼此引用形成一个循环,而这个循环与程序的其他部分没有引用相连,那么这个循环中的对象就会成为垃圾。Java的垃圾收集器通过识别引用循环并处理它们来防止内存泄漏。
  • 显式置空引用: 如果程序员显式地将一个引用置空(null),而没有其他引用指向相同的对象,那么该对象就变成了垃圾。

垃圾收集器周期性地运行,并识别和回收这些垃圾对象,释放其内存中对应的区域以确保内存能够得到有效利用,这种自动的内存管理机制就叫做垃圾回收。

如何寻找垃圾?

引用计数(Reference Count)

img
img

引用计数算法是一种垃圾标记,其核心思想是通过维护对象的引用计数来判断对象是否可以被回收。每个对象都有一个关联的引用计数,表示当前有多少个指针引用它。当引用计数为零时,意味着没有指针再引用该对象,因此可以安全地回收该对象的内存。

其实引用计数算法的核心思想就是,只要有对象引用我,那么就说明我是有用的,我还不需要被回收,反正,我就是没有用的对象,那么我和我的子对象都应该被回收掉。这里我们说的对象都是堆上的对象,一般是堆上的内存空间需要程序员手动回收,而栈上的内存空间则由操作系统自行回收。由于栈上的对象是操作系统自行管理和回收的,因此栈上的对象以及一些静态对象始终都是出于存活的状态,因此,堆中存活的对象至少会有一个引用(指针)指向它。

但是这样会存在着一个问题,就是对象中的引用关系形成了环状——循环引用,这种情况下环内所有对象的引用都是>1的,这样一来环内的所有都无法被回收从而造成“内存泄漏”。这是引用算法最主要的局限性,也是为什么JVM不采用循环计数的方法来标记垃圾的原因。

由于引用计数算法无法解决“循环引用”的问题,无可避免的会造成内存泄露,因此,Java没有采用引用计数算法来寻找垃圾。而是采用了一种从GC Roots开始搜索存活对象的垃圾标记算法——根可达算法。

img
img

哪些是GC Root?

GC Roots SourceDescription
线程栈 (Thread Stacks)活动线程的栈帧中的本地变量引用的对象。每个线程都有一个栈,栈中的引用对象是潜在的存活对象。
静态变量 (Static Variables)类的静态成员变量引用的对象。静态变量随着类的加载而初始化,它们的引用可能使对象保持存活。
常量池 (Constant Pool)常量池中的引用,包括字符串常量等。这些常量在类加载时被创建,它们的引用也可能使对象保持存活。
JNI 引用 (JNI References)通过 JNI 在本地代码中创建的对象引用。如果 Java 代码通过 JNI 调用了本地方法,并在本地方法中创建了对象,这些对象的引用也是 GC Roots。
监控与管理 MBeans (JMX)活动的监控、管理 MBeans 等通过 JMX 等管理工具注册的对象。这些对象的引用也被视为 GC Roots。

线程栈 (Thread Stacks)

在Java中,当程序运行的时候线程会将一个个方法放到栈上来执行,并且对于方法局部的一些小的对象和变量也会被分配在栈空间上,而栈空间是由操作系统本身来控制什么时候进行释放和分配的。因此,基于这个逻辑我们可以认为对于当前线程来说,存在于栈空间上的变量都是存活的,而且栈空间一般比较小只有几MB的大小,里面存活的变量和对象都是有限的作为GC Roots来说搜索起来也是非常高效的。

静态变量 (Static Variables)

在Java中静态变量一般是随着类加载的时候被创建和初始化的,和Java字节码一样,静态变量也会被加载到元空间(Meta Space,Java 8之前叫做方法区(Method Area)或叫做永久代(Permanent Generation),Java 8之后叫做元空间)

元空间的对象是不会轻易被释放的,而静态变量会随着整个类被释放的时候才会被释放,因此静态变量可以作为GC Root来寻找垃圾。

常量池 (Constant Pool)

常量池(Constant Pool)是Java中一种存放常量的数据结构,用于存储编译期生成的字面量和符号引用。常量池属于元空间(Meta Space,Java 8之前叫做方法区(Method Area)或叫做永久代(Permanent Generation),Java 8之后叫做元空间),具体说是类加载后存放在元空间的一部分内存。

在Java程序的编译阶段,常量池会保存各种字面量和符号引用,包括字符串、类和接口的全限定名、字段和方法的名称和描述符等,这些信息在编译后会被存放在class文件的常量池中,在运行期间这些常量池依旧会存在并且Java根据常量池来映射参数。

所以,处于常量池中的变量也可以作为GC Roots来寻找垃圾

JNI 引用 (JNI References)

JNI(Java Native Interface)引用是指在Java程序中通过JNI创建的与本地代码(C++代码,调用平台相关函数)相互调用的引用。JNI引用包括本地引用(Local Reference)、全局引用(Global Reference)和弱全局引用(Weak Global Reference)。

  • 本地引用(Local Reference): 本地引用是一种短期的引用,用于限定其生命周期。当Java方法调用本地方法时,本地引用会被创建,但在本地方法返回后,这些引用将被自动释放。本地引用不能作为GC Roots。
  • 全局引用(Global Reference): 全局引用是一种长期有效的引用,可以在整个程序的生命周期内使用。全局引用可以防止被引用对象被垃圾回收,因此它可以作为GC Roots。
  • 弱全局引用(Weak Global Reference): 弱全局引用也是一种全局引用,但它对被引用对象的生命周期没有强制影响。如果一个对象只被弱全局引用引用,那么它在垃圾回收时可能被回收。弱全局引用不能作为GC Roots。

JNI引用之所以能作为GC Roots,是因为它们可以在本地方法(C++方法,调用平台相关函数)中持有Java对象的引用,防止这些对象在本地方法执行期间被垃圾回收。全局引用在整个程序的生命周期内有效,因此它们有可能成为根引用,即GC Roots。

根可达算法原理

x知道了什么是GC Roots那么根可达算法理解起来就相对来说会简单一些。GC Roots我们可以简单理解为和Java程序的生命周期强关联、和JVM生命周期强关联或者和当前线程强关联的一些对象。这些对象至少说在发生GC这一时刻是不应该被当成垃圾回收掉的,否则会影响程序的正常使用,因此,我们标记存活对象的时候从GC Roots开始,认为被GC Roots 引用或者间接引用的对象就是存活对象。因此,根可达算法的基本原理和流程如下:

  1. 初始根集合(Initial Roots): 根可达算法从程序的初始根集合开始,这些根是一组特殊的引用,如线程栈中的引用、静态变量、JNI(Java Native Interface)引用等。
  2. 标记阶段(Mark Phase): 算法通过追踪根引用,递归遍历对象图,标记所有可以从根引用访问到的对象。在这个过程中,被标记的对象被认为是可达的,而未被标记的对象被认为是不可达的。
  3. 标记-清除阶段(Mark-Sweep Phase): 在标记完成后,算法执行清除操作,即移除未被标记的对象。这些未被标记的对象被认为是不可达的,可以被垃圾回收器回收。这个阶段的目标是回收不再被程序使用的内存空间。
  4. 压缩(Compaction)或整理(Compaction): 在某些情况下,为了优化内存布局,可能会执行进一步的操作,如将存活对象整理到一起,以减少内存碎片。这个步骤通常与标记-清除阶段结合使用。
  5. 可选的再标记阶段(Optional Re-Mark Phase): 有些算法可能会在标记-清除后执行可选的再标记阶段,以处理在清除阶段可能发生的并发引用更新。这一步确保在垃圾回收过程中引用关系的一致性。
  6. 结束(Finish): 垃圾回收算法完成后,内存中只留下了可达对象,而不可达的对象已被清理。程序可以继续执行。

实际上来说,如CMS和G1之类比较流行的垃圾回收器都是采用的**“三色标记”**算法,而非直接采用的根可达算法来对垃圾进行标记的,这里给出的流程大家参考一下就可以了。

总结

本文先介绍了什么是内存垃圾以及常见的垃圾寻找算法——引用计数和根可达算法,其中,引用计数算法由于无法解决循环引用问题未被Java采用,Java中采用的是类似于“根可达算法”的**三色标记算法**。

根可达算法的核心是从GC Roots开始,标记出所有直接或者间接被GC Roots所引用的对象,这些对象被称为存活对象,剩下的对象就是垃圾。而GC Roots主要由线程、Java进程和JVM当前生命周期中存活的变量组成,主要包括:线程栈变量、静态变量、JNI引用对象和常量池对象。

',43)]))}const l=t(o,[["render",s],["__file","引用计数和根可达算法.html.vue"]]),h=JSON.parse('{"path":"/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html","title":"引用计数和根可达算法","lang":"zh-CN","frontmatter":{"description":"引用计数和根可达算法 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载 硬件角度来看什么叫做垃圾? 计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"引用计数和根可达算法"}],["meta",{"property":"og:description","content":"引用计数和根可达算法 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载 硬件角度来看什么叫做垃圾? 计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/v2-39ba8e013eb4757d88c0582e1a4d8db0_1440w.webp"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T09:22:35.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T09:22:35.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"引用计数和根可达算法\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/v2-39ba8e013eb4757d88c0582e1a4d8db0_1440w.webp\\",\\"https://github-images.wenzhihuai.com/images/v2-47d8f7958e840ee3d16c15d53cd633c8_1440w.webp\\"],\\"dateModified\\":\\"2024-02-16T09:22:35.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":3,"title":"硬件角度来看什么叫做垃圾?","slug":"硬件角度来看什么叫做垃圾","link":"#硬件角度来看什么叫做垃圾","children":[]},{"level":3,"title":"Java中的垃圾","slug":"java中的垃圾","link":"#java中的垃圾","children":[]},{"level":2,"title":"如何寻找垃圾?","slug":"如何寻找垃圾","link":"#如何寻找垃圾","children":[{"level":3,"title":"引用计数(Reference Count)","slug":"引用计数-reference-count","link":"#引用计数-reference-count","children":[]},{"level":3,"title":"根可达算法(Root Search)","slug":"根可达算法-root-search","link":"#根可达算法-root-search","children":[]},{"level":3,"title":"哪些是GC Root?","slug":"哪些是gc-root","link":"#哪些是gc-root","children":[]},{"level":3,"title":"线程栈 (Thread Stacks)","slug":"线程栈-thread-stacks","link":"#线程栈-thread-stacks","children":[]},{"level":3,"title":"静态变量 (Static Variables)","slug":"静态变量-static-variables","link":"#静态变量-static-variables","children":[]},{"level":3,"title":"常量池 (Constant Pool)","slug":"常量池-constant-pool","link":"#常量池-constant-pool","children":[]},{"level":3,"title":"JNI 引用 (JNI References)","slug":"jni-引用-jni-references","link":"#jni-引用-jni-references","children":[]},{"level":3,"title":"根可达算法原理","slug":"根可达算法原理","link":"#根可达算法原理","children":[]}]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1708075355000,"updatedTime":1708075355000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":11.61,"words":3483},"filePathRelative":"java/JVM/引用计数和根可达算法.md","localizedDate":"2024年2月16日","excerpt":"\\n

本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

","autoDesc":true}');export{l as comp,h as data}; diff --git "a/assets/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html-CyDH_f4g.js" "b/assets/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html-CyDH_f4g.js" new file mode 100644 index 00000000..0155f342 --- /dev/null +++ "b/assets/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html-CyDH_f4g.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as i,o as n}from"./app-ftEjETWs.js";const r={};function o(p,e){return n(),a("div",null,e[0]||(e[0]=[i('

推荐工程-概述

随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户来指代。

技术的最终目的都是服务业务的,我们来看下当前电商系统的模式差异。

一、淘宝和拼多多的电商模式差异

淘宝是流量逻辑,主体是搜索,需要海量SKU满足长尾需求;拼多多代表的是匹配逻辑,推荐商品给消费者,SKC有限,但满足结构性丰富。之前的电商模式都是从淘宝挖一块做小淘宝,而拼多多是在另一个维度做电商,满足不同的场景需求。淘宝的千人千面相当于个性化搜索,但搜索本身是长尾的,你就很难做反向定制。而拼多多集中流量到有限商品里,有了规模后再反向定制。

拼多多起来之后,京东、唯品会、蘑菇街都实验过相似模式,对于他们来说,拼团不过是一个创造GMV增量的工具;而拼多多是人的逻辑,我们通过拼团了解人,通过人推荐物,后期会过渡到机器推荐物。拼多多APP里几乎没有搜索,也不设购物车,你可以想像把今日头条下的信息流换成商品流就是拼多多。所以早期看大家都是低价和拼团,但我们的出发点不同、方向不同,长大了也就不一样了。

与其说拼多多是社交电商,不如说它是“人以群分”的电商。以前是人去找东西,现在是相似的人聚集起来,迅速产生需求量,这种模式给供应链优化创造了很大的机会。

二、搜索、推荐、广告三者的异同

搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:

  1. 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
  2. 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
  3. 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
  4. 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。

**广告:**借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

三、推荐整体架构

推荐的框架主要有以下几个模块:

  • 协议调度:请求的发送和结果的回传。在请求中,用户会发送自己的 ID,地理位置等信息。结果回传中会返回推荐系统给用户推荐的结果。
  • 推荐算法:算法按照一定的逻辑为用户产生最终的推荐结果。不同的推荐算法基于不同的逻辑与数据运算过程。
  • 消息队列:数据的上报与处理。根据用户的 ID,拉取例如用户的性别、之前的点击、收藏等用户信息。而用户在 APP 中产生的新行为,例如新的点击会储存在存储单元里面。
  • 存储单元:不同的数据类型和用途会储存在不同的存储单元中,例如内容标签与内容的索引存储在 mysql 里,实时性数据存储在 redis 里,需要进行数据统计的数据存储在 TDW 里。

电商系统演变到现在,各类归纳层出不穷,传统零售是“货-场-人”,而新零售是“人-货-场”,强调以人为核心,根据用户的需求推送产品和打造消费场景。在电商线上平台三者具体表现就是:

**人:**是指店铺访客、流量。通过数据分析、用户画像、顾客细分等手段,可以更加精准地理解顾客,从而提供个性化的产品和服务
**货:**是指商家商品。选择合适的商品,有竞争力的价格,保证商品的质量和供应链的高效运营,都是成功的关键
**场:**由最初的交易平台(如京东、淘宝等)演变到场景(首页、商详推荐页、加购推荐页等),实现千人千面。

回到我们推荐本身,推荐的本质就是排序,把成千上万的商品更精准的展现到用户面前,提升用户的购买量,从而变现。技术层面,最核心的就是推荐算法,目前市面上对推荐系统基本等于推荐算法,而做为一名后台,在推荐系统中的角色介绍可为少之又少,本系列就用后台的角色去看待整个推荐系统

参考

1.FunRec

2.从零开始了解推荐系统全貌

3.从“人、货、场”的不同视角,重新审视电商和新零售

4.“人货场”,在产品业务分析中的具体应用

',26)]))}const l=t(r,[["render",o],["__file","推荐工程-概述.html.vue"]]),g=JSON.parse('{"path":"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html","title":"推荐工程-概述","lang":"zh-CN","frontmatter":{"order":1,"description":"推荐工程-概述 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。 平台方、信息生产者和消费者可以分别用平台方(如:腾...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"推荐工程-概述"}],["meta",{"property":"og:description","content":"推荐工程-概述 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。 平台方、信息生产者和消费者可以分别用平台方(如:腾..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/image-20241130000406534.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T18:45:10.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T18:45:10.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"推荐工程-概述\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/image-20241130000406534.png\\",\\"https://github-images.wenzhihuai.com/images/v2-85666a02ae5e703ffdbf45c6bf9f2523_1440w.jpg\\",\\"https://github-images.wenzhihuai.com/images/image-20241130022550567.png\\"],\\"dateModified\\":\\"2024-11-29T18:45:10.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、淘宝和拼多多的电商模式差异","slug":"一、淘宝和拼多多的电商模式差异","link":"#一、淘宝和拼多多的电商模式差异","children":[]},{"level":2,"title":"二、搜索、推荐、广告三者的异同","slug":"二、搜索、推荐、广告三者的异同","link":"#二、搜索、推荐、广告三者的异同","children":[]},{"level":2,"title":"三、推荐整体架构","slug":"三、推荐整体架构","link":"#三、推荐整体架构","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1732905910000,"updatedTime":1732905910000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":6.19,"words":1856},"filePathRelative":"system-design/推荐系统/推荐工程-概述.md","localizedDate":"2024年11月29日","excerpt":"\\n

随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

","autoDesc":true}');export{l as comp,g as data}; diff --git "a/assets/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html-DF2q_M-N.js" "b/assets/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html-DF2q_M-N.js" new file mode 100644 index 00000000..343751ca --- /dev/null +++ "b/assets/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html-DF2q_M-N.js" @@ -0,0 +1,106 @@ +import{_ as s,c as a,d as n,o as h}from"./app-ftEjETWs.js";const l={};function t(k,i){return h(),a("div",null,i[0]||(i[0]=[n(`

推荐系统入门

转载自:https://www.cnblogs.com/cgli/p/17225189.html

一、背景

近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不数据规模。数据级量小,业务场景也简单,实现起比较的容易,但原理基本上大同小异。

本文尝试从最简单的推荐入手,我们暂不去讨论大规模数据分析和算法。更多从软件工程角度思考问题,那些高大上算法留给读者去思考。开源获取:http://www.jnpfsoft.com/?from=infoq

下面就来谈谈整个推荐的设计实现过程。

二、推荐系统介绍

2.1、为什么需要推荐系统?

首先推荐系统作用是很大的。推荐系统在很多业务场景有广泛使用和发挥的空间,它的应用和影子无处不在,在多媒体内容、广告平台和电商平台尤为常见。大多平台商或企业都是基于大数据算法分析做推荐系统。推荐算法也是层出不穷,比如相似度计算、近邻推荐、概率矩阵分解、概率图等,当然也有综合多种算法融合使用。

互联网时代,数据呈爆炸式增长,前所未有的数据量远远超过受众的接收和处理能力,因此,从海量复杂数据中有效获取关键性有用信息成为必须解决的问题。面对信息过载问题,人们迫切需要一种高效的信息过滤系统,“推荐系统”应运而生。

20 世纪 90 年代以来,尽管推荐系统在理论、方法和应用方面取得了系列重要进展,但数据的稀疏性与长尾性、用户行为模式挖掘、可解释性、社会化推荐等问题仍然是其面临的重要挑战。

进一步地,伴随互联网及信息技术的持续飞速发展,用户规模与项目数量急剧增长,相应地,用户行为数据的稀疏性、长尾性问题更加凸显。也就是说目前各大平台虽然已经推荐系统,但是实际应用当中还是面临很多问题,仍然有很大的提升空间。这是技术挑战也机会,当然这也是我们这些从业者可以发挥的地方。

2.2、推荐系统解决什么问题?

推荐系统从 20 世纪 90 年代就被提出来了,但是真正进入大众视野以及在各大互联网公司中流行起来,还是最近几年的事情。

随着移动互联网的发展,越来越多的信息开始在互联网上传播,产生了严重的信息过载。因此,如何从众多信息中找到用户感兴趣的信息,这个便是推荐系统的价值。精准推荐解决了用户痛点,提升了用户体验,最终便能留住用户。

推荐系统本质上就是一个信息过滤系统,通常分为:召回、排序、重排序这 3 个环节,每个环节逐层过滤,最终从海量的物料库中筛选出几十个用户可能感兴趣的物品推荐给用户。

推荐系统的分阶段过滤流程如下图所示:

img
img

2.3、推荐系统应用场景

哪里有海量信息,哪里就有推荐系统,我们每天最常用的 APP 都涉及到推荐功能:

资讯类:今日头条、腾讯新闻等

电商类:淘宝、京东、拼多多、亚马逊等

娱乐类:抖音、快手、爱奇艺等

生活服务类:美团、大众点评、携程等

社交类:微信、陌陌、脉脉等

img
img

实际例子还有很多,稍微上一点规模的平台或 APP 都有这一个推荐模块。

推荐系统的应用场景通常分为以下两类:

基于用户维度的推荐:根据用户的历史行为和兴趣进行推荐,比如淘宝首页的猜你喜欢、抖音的首页推荐等。

基于物品维度的推荐:根据用户当前浏览的标的物进行推荐,比如打开京东 APP 的商品详情页,会推荐和主商品相关的商品给你。

2.4、搜索、推荐、广告三者的异同

搜索和推荐是 AI 算法最常见的两个应用场景,在技术上有相通的地方。

搜索:有明确的搜索意图,搜索出来的结果和用户的搜索词相关。推荐:不具有目的性,依赖用户的历史行为和画像数据进行个性化推荐。广告:借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

三、推荐系统通用框架

推荐系统涉及周边和自己模块还是比较多,这里主要从最简单推荐系统自身功能去构思设计简单结构。

img
img

上面这个图基本把推荐处理过程画出来,结构比较清晰,看图理想即可。

从分层架构设计视角来说可以分成多层架构形式

分层:排序层、过滤层、召回层、数据存储层、计算平台、数据源。

可以说市面上推荐系统设计都是差不多是这个样子,只是里面使用技术或组件不同而已。

img
img

上面是推荐系统的整体架构图,自下而上分成了多层,各层的主要作用如下:

  • 数据源:推荐算法所依赖的各种数据源,包括物品数据、用户数据、行为日志、其他可利用的业务数据、甚至公司外部的数据。

  • 计算平台:负责对底层的各种异构数据进行清洗、加工,离线计算和实时计算。

  • 数据存储层:存储计算平台处理后的数据,根据需要可落地到不同的存储系统中,比如 Redis 中可以存储用户特征和用户画像数据,ES 中可以用来索引物品数据,Faiss 中可以存储用户或者物品的 embedding 向量等。

  • 召回层:包括各种推荐策略或者算法,比如经典的协同过滤,基于内容的召回,基于向量的召回,用于托底的热门推荐等。为了应对线上高并发的流量,召回结果通常会预计算好,建立好倒排索引后存入缓存中。

  • 融合过滤层:触发多路召回,由于召回层的每个召回源都会返回一个候选集,因此这一层需要进行融合和过滤。

  • 排序层:利用机器学习或者深度学习模型,以及更丰富的特征进行重排序,筛选出更小、更精准的推荐集合返回给上层业务。从数据存储层到召回层、再到融合过滤层和排序层,候选集逐层减少,但是精准性要求越来越高,因此也带来了计算复杂度的逐层增加,这个便是推荐系统的最大挑战。

其实对于推荐引擎来说,最核心的部分主要是两块:特征和算法。

img
img

这些工具和技术框架都是比较成熟稳定的,是众多厂商在实际业务场景中选择应用的。所以也没有太多特殊的地方。

特征计算由于数据量大,通常采用大数据的离线和实时处理技术,像 Spark、Flink 等,然后将计算结果保存在 Redis 或者其他存储系统中(比如 HBase、MongoDB 或者 ES),供召回和排序模块使用。

召回算法的作用是:从海量数据中快速获取一批候选数据,要求是快和尽可能的准。这一层通常有丰富的策略和算法,用来确保多样性,为了更好的推荐效果,某些算法也会做成近实时的。

排序算法的作用是:对多路召回的候选集进行精细化排序。它会利用物品、用户以及它们之间的交叉特征,然后通过复杂的机器学习或者深度学习模型进行打分排序,这一层的特点是计算复杂但是结果更精准。

四、经典算法

了解了推荐系统的整体架构和技术方案后,下面带大家深入一下算法细节。这里选择图解的是推荐系统中的明星算法:协同过滤(Collaborative Filtering,CF)。

对于很多同学来说,可能觉得 AI 算法晦涩难懂,门槛太高,确实很多深度学习算法的确是这样,但是协同过滤却是一个简单同时效果很好的算法,只要你有初中数学的基础就能看懂。

4.1、协同过滤是什么?

协同过滤算法的核心就是「找相似」,它基于用户的历史行为(浏览、收藏、评论等),去发现用户对物品的喜好,并对喜好进行度量和打分,最终筛选出推荐集合。它又包括两个分支:

  • 基于用户的协同过滤:User-CF,核心是找相似的人。比如下图中,用户 A 和用户 C 都购买过物品 a 和物品 b,那么可以认为 A 和 C 是相似的,因为他们共同喜欢的物品多。这样,就可以将用户 A 购买过的物品 d 推荐给用户 C。
img
img

基于用户的协同过滤示例

  • 基于物品的协同过滤:Item-CF,核心是找相似的物品。比如下图中,物品 a 和物品 b 同时被用户 A,B,C 购买了,那么物品 a 和物品 b 被认为是相似的,因为它们的共现次数很高。这样,如果用户 D 购买了物品 a,则可以将和物品 a 最相似的物品 b 推荐给用户 D。
img
img

4.2、如何找相似?

协同过滤的核心就是找相似,User-CF 是找用户之间的相似,Item-CF 是找物品之间的相似,那到底如何衡量两个用户或者物品之间的相似性呢?

我们都知道,对于坐标中的两个点,如果它们之间的夹角越小,这两个点越相似。

这就是初中学过的余弦距离,它的计算公式如下:

img
img

举个例子,A 坐标是(0,3,1),B 坐标是(4,3,0),那么这两个点的余弦距离是 0.569,余弦距离越接近 1,表示它们越相似。

img
img

除了余弦距离,衡量相似性的方法还有很多种,比如:欧式距离、皮尔逊相关系数、Jaccard 相似系数等等,这里不做展开,只是计算公式上的差异而已。

4.3、Item-CF 的算法流程

清楚了相似性的定义后,下面以 Item-CF 为例,详细说下这个算法到底是如何选出推荐物品的?

4.3.1 、整理物品的共现矩阵

假设有 A、B、C、D、E 5 个用户,其中用户 A 喜欢物品 a、b、c,用户 B 喜欢物品 a、b 等等。

img
img

所谓共现,即:两个物品被同一个用户喜欢了。比如物品 a 和 b,由于他们同时被用户 A、B、C 喜欢,所以 a 和 b 的共现次数是 3,采用这种统计方法就可以快速构建出共现矩阵。

4.3.2、 计算物品的相似度矩阵

对于 Item-CF 算法来说,一般不采用前面提到的余弦距离来衡量物品的相似度,而是采用下面的公式:

img
img

其中,N(u) 表示喜欢物品 u 的用户数,N(v) 表示喜欢物品 v 的用户数,两者的交集表示同时喜欢物品 u 和物品 v 的用户数。

很显然,如果两个物品同时被很多人喜欢,那么这两个物品越相似。

基于第 1 步计算出来的共现矩阵以及每个物品的喜欢人数,便可以构造出物品的相似度矩阵:

img
img

4.3.2、 推荐物品

最后一步,便可以基于相似度矩阵推荐物品了,公式如下:

img
img

其中,Puj 表示用户 u 对物品 j 的感兴趣程度,值越大,越值得被推荐。

N(u) 表示用户 u 感兴趣的物品集合,S(j,N) 表示和物品 j 最相似的前 N 个物品,Wij 表示物品 i 和物品 j 的相似度,Rui 表示用户 u 对物品 i 的兴趣度。

上面的公式有点抽象,直接看例子更容易理解,假设我要给用户 E 推荐物品,前面我们已经知道用户 E 喜欢物品 b 和物品 c,喜欢程度假设分别为 0.6 和 0.4。

那么,利用上面的公式计算出来的推荐结果如下:

img
img

因为物品 b 和物品 c 已经被用户 E 喜欢过了,所以不再重复推荐。最终对比用户 E 对物品 a 和物品 d 的感兴趣程度,因为 0.682 > 0.3,因此选择推荐物品 a。

五、如何实现推荐系统

5.1、选择数据集

这里采用的是推荐领域非常经典的 MovieLens 数据集,它是一个关于电影评分的数据集,官网上提供了多个不同大小的版本,下面以 ml-1m 数据集(大约 100 万条用户评分记录)为例。

下载解压后,文件夹中包含:ratings.dat、movies.dat、users.dat 3 个文件,共 6040 个用户,3900 部电影,1000209 条评分记录。各个文件的格式都是一样的,每行表示一条记录,字段之间采用 :: 进行分割。

以 ratings.dat 为例,每一行包括 4 个属性:UserID, MovieID, Rating, Timestamp。

通过脚本可以统计出不同评分的人数分布:

img
img

5.2、读取原始数据

程序主要使用数据集中的 ratings.dat 这个文件,通过解析该文件,抽取出 user_id、movie_id、rating 3 个字段,最终构造出算法依赖的数据,并保存在变量 dataset 中,它的格式为:dict[user_id][movie_id] = rate

5.3、构造物品的相似度矩阵

基于第 2 步的 dataset,可以进一步统计出每部电影的评分次数以及电影的共生矩阵,然后再生成相似度矩阵。

5.4、基于相似度矩阵推荐物品

最后,可以基于相似度矩阵进行推荐了,输入一个用户 id,先针对该用户评分过的电影,依次选出 top 10 最相似的电影,然后加权求和后计算出每个候选电影的最终评分,最后再选择得分前 5 的电影进行推荐。

5.5、调用推荐系统

下面选择 UserId=1 这个用户,看下程序的执行结果。由于推荐程序输出的是 movieId 列表,为了更直观的了解推荐结果,这里转换成电影的标题进行输出。

Java 代码示例

import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+public class CFRecommendation {
+    // 使用MovieLens数据集
+    private static final String RATINGS_FILE = "ratings.csv";
+    // 用户ID-电影ID-打分
+    private static Map<Integer, Map<Integer, Double>> ratings;
+
+    // 加载ratings.csv文件
+    private static void loadRatings() throws IOException {
+        File file = new File(RATINGS_FILE);
+        Scanner scanner = new Scanner(file);
+        ratings = new HashMap<>();
+        while (scanner.hasNextLine()) {
+            String line = scanner.nextLine();
+            String[] data = line.split(",");
+            int userId = Integer.parseInt(data[0]);
+            int movieId = Integer.parseInt(data[1]);
+            double rating = Double.parseDouble(data[2]);
+            // 用户-电影-打分
+            Map<Integer, Double> movieRatings = ratings.get(userId);
+            if (movieRatings == null) {
+                movieRatings = new HashMap<>();
+                ratings.put(userId, movieRatings);
+            }
+            movieRatings.put(movieId, rating);
+        }
+    }
+
+    // 计算两个用户的相似度
+    private static double calculateSimilarity(int user1, int user2) {
+        Map<Integer, Double> rating1 = ratings.get(user1);
+        Map<Integer, Double> rating2 = ratings.get(user2);
+        if (rating1 == null || rating2 == null) {
+            return 0;
+        }
+        double sum1 = 0;
+        double sum2 = 0;
+        double sumProduct = 0;
+        for (int movieId : rating1.keySet()) {
+            if (rating2.containsKey(movieId)) {
+                double rating1Value = rating1.get(movieId);
+                double rating2Value = rating2.get(movieId);
+                sum1 += rating1Value * rating1Value;
+                sum2 += rating2Value * rating2Value;
+                sumProduct += rating1Value * rating2Value;
+            }
+        }
+        return sumProduct / (Math.sqrt(sum1) * Math.sqrt(sum2));
+    }
+    
+    // 计算余弦相似度
+    public static double cosineSim(Map<String, Integer> user1, Map<String, Integer> user2){
+        double result = 0;
+        double denominator1 = 0;
+        double denominator2 = 0;
+        double numerator = 0;
+        for(String key : user1.keySet()){
+            numerator += user1.get(key) * user2.get(key);
+            denominator1 += Math.pow(user1.get(key), 2);
+            denominator2 += Math.pow(user2.get(key), 2);
+        }
+        result = numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2));
+        return result;
+    }
+
+    // 使用协同过滤算法获取用户的推荐列表
+    private static Map<Integer, Double> recommend(int userId) {
+        Map<Integer, Double> recommendList = new HashMap<>();
+        // 遍历所有用户
+        for (int otherUserId : ratings.keySet()) {
+            if (otherUserId != userId) {
+                double similarity = calculateSimilarity(userId, otherUserId);
+                Map<Integer, Double> otherRating = ratings.get(otherUserId);
+                // 遍历其他用户的评分,如果当前用户没有评分,则将其推荐给当前用户
+                for (int movieId : otherRating.keySet()) {
+                    if (!ratings.get(userId).containsKey(movieId)) {
+                        double recommendScore = otherRating.get(movieId) * similarity;
+                        recommendList.put(movieId, recommendScore);
+                    }
+                }
+            }
+        }
+        return recommendList;
+    }
+
+    public static void main(String[] args) throws IOException {
+        loadRatings();
+        // 测试用例:计算用户1与用户2的相似度
+        int user1 = 1;
+        int user2 = 2;
+        double similarity = calculateSimilarity(user1, user2);
+        System.out.println("用户1与用户2的相似度:" + similarity);
+        // 测试用例:为用户1推荐电影
+        int userId = 1;
+        Map<Integer, Double> recommendList = recommend(userId);
+        System.out.println("为用户1推荐的电影:");
+        for (int movieId : recommendList.keySet()) {
+            System.out.println("电影ID:" + movieId + ",推荐分数:" + recommendList.get(movieId));
+        }
+    }
+}

六、问题与展望

通过上面的介绍,大家对推荐系统的基本构成应该有了一个初步认识,但是真正运用到线上真实环境时,还会遇到很多算法和工程上的挑战,绝对不是几十行代码可以搞定的。

问题:

  1. 上面的示例使用了标准化的数据集,而线上环境的数据是非标准化的,因此涉及到海量数据的收集、清洗和加工,最终构造出模型可使用的数据集。

    复杂且繁琐的特征工程,都说算法模型的上限由数据和特征决定。对于线上环境,需要从业务角度选择出可用的特征,然后对数据进行清洗、标准化、归一化、离散化,并通过实验效果进一步验证特征的有效性。

    算法复杂度如何降低?比如上面介绍的 Item-CF 算法,时间和空间复杂度都是 O(N×N),而线上环境的数据都是千万甚至上亿级别的,如果不做算法优化,可能几天都跑不出数据,或者内存中根本放不下如此大的矩阵数据。

    实时性如何满足?因为用户的兴趣随着他们最新的行为在实时变化的,如果模型只是基于历史数据进行推荐,可能结果不够精准。因此,如何满足实时性要求,以及对于新加入的物品或者用户该如何推荐,都是要解决的问题。

    算法效果和性能的权衡。从算法角度追求多样性和准确性,从工程角度追求性能,这两者之间必须找到一个平衡点。

    推荐系统的稳定性和效果追踪。需要有一套完善的数据监控和应用监控体系,同时有 ABTest 平台进行灰度实验,进行效果对比。

展望:

AI 时代,算法会更加复杂和完善,推荐的效果也会越来越好,特别是随着 OpenAI ChatGPT 横空出现,推荐系统最有条件和最适合 GPT 模型去结合使用,当然也会更加高效和智能。期待我们智能版推荐系统早日面世。

`,112)]))}const e=s(l,[["render",t],["__file","推荐系统入门.html.vue"]]),d=JSON.parse('{"path":"/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html","title":"推荐系统入门","lang":"zh-CN","frontmatter":{"description":"推荐系统入门 转载自:https://www.cnblogs.com/cgli/p/17225189.html 一、背景 近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"推荐系统入门"}],["meta",{"property":"og:description","content":"推荐系统入门 转载自:https://www.cnblogs.com/cgli/p/17225189.html 一、背景 近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/test/9cd60b43b0f590e54ff8fd2de18b16a2.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-09-01T13:54:44.000Z"}],["meta",{"property":"article:modified_time","content":"2024-09-01T13:54:44.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"推荐系统入门\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/test/9cd60b43b0f590e54ff8fd2de18b16a2.png\\",\\"https://github-images.wenzhihuai.com/test/38a57bd31efcdd62404364b89d43e74d.png\\",\\"https://github-images.wenzhihuai.com/test/331f2625bd080daf15ab1272ff8b2618.png\\",\\"https://github-images.wenzhihuai.com/test/021e41edfe456b85b854fbfe6324d790.png\\",\\"https://github-images.wenzhihuai.com/test/ee91ac49bd6897ec079c7fb0a4f48339.png\\",\\"https://github-images.wenzhihuai.com/test/81dfa21cedb8b6f682a8e10c445e16a3.png\\",\\"https://github-images.wenzhihuai.com/test/1ae22325d1152c67b6c3d764a5b14da1.png\\",\\"https://github-images.wenzhihuai.com/test/026750069c00863d20f066f05500ce0a.png\\",\\"https://github-images.wenzhihuai.com/test/536eb6ad9babb3c43f7f5a5769e8123e.png\\",\\"https://github-images.wenzhihuai.com/test/b7d64b6e583ea87959599ec8959de2cd.png\\",\\"https://github-images.wenzhihuai.com/test/c29cd8e5ad23469ba51aac2334f4cab5.png\\",\\"https://github-images.wenzhihuai.com/test/d7bcad0dffe0c8e206dee5302f42fbbf.png\\",\\"https://github-images.wenzhihuai.com/test/8ffa869ca03b8b1acddb871541a3659c.png\\",\\"https://github-images.wenzhihuai.com/test/851e6744a622918e04591bf79bfb200b.png\\",\\"https://github-images.wenzhihuai.com/test/28d28f2e8bbf5a4e8ab0dd366b57b67b.png\\"],\\"dateModified\\":\\"2024-09-01T13:54:44.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、背景","slug":"一、背景","link":"#一、背景","children":[]},{"level":2,"title":"二、推荐系统介绍","slug":"二、推荐系统介绍","link":"#二、推荐系统介绍","children":[{"level":3,"title":"2.1、为什么需要推荐系统?","slug":"_2-1、为什么需要推荐系统","link":"#_2-1、为什么需要推荐系统","children":[]},{"level":3,"title":"2.2、推荐系统解决什么问题?","slug":"_2-2、推荐系统解决什么问题","link":"#_2-2、推荐系统解决什么问题","children":[]},{"level":3,"title":"2.3、推荐系统应用场景","slug":"_2-3、推荐系统应用场景","link":"#_2-3、推荐系统应用场景","children":[]},{"level":3,"title":"2.4、搜索、推荐、广告三者的异同","slug":"_2-4、搜索、推荐、广告三者的异同","link":"#_2-4、搜索、推荐、广告三者的异同","children":[]}]},{"level":2,"title":"三、推荐系统通用框架","slug":"三、推荐系统通用框架","link":"#三、推荐系统通用框架","children":[]},{"level":2,"title":"四、经典算法","slug":"四、经典算法","link":"#四、经典算法","children":[{"level":3,"title":"4.1、协同过滤是什么?","slug":"_4-1、协同过滤是什么","link":"#_4-1、协同过滤是什么","children":[]},{"level":3,"title":"4.2、如何找相似?","slug":"_4-2、如何找相似","link":"#_4-2、如何找相似","children":[]},{"level":3,"title":"4.3、Item-CF 的算法流程","slug":"_4-3、item-cf-的算法流程","link":"#_4-3、item-cf-的算法流程","children":[]}]},{"level":2,"title":"五、如何实现推荐系统","slug":"五、如何实现推荐系统","link":"#五、如何实现推荐系统","children":[{"level":3,"title":"5.1、选择数据集","slug":"_5-1、选择数据集","link":"#_5-1、选择数据集","children":[]},{"level":3,"title":"5.2、读取原始数据","slug":"_5-2、读取原始数据","link":"#_5-2、读取原始数据","children":[]},{"level":3,"title":"5.3、构造物品的相似度矩阵","slug":"_5-3、构造物品的相似度矩阵","link":"#_5-3、构造物品的相似度矩阵","children":[]},{"level":3,"title":"5.4、基于相似度矩阵推荐物品","slug":"_5-4、基于相似度矩阵推荐物品","link":"#_5-4、基于相似度矩阵推荐物品","children":[]},{"level":3,"title":"5.5、调用推荐系统","slug":"_5-5、调用推荐系统","link":"#_5-5、调用推荐系统","children":[]}]},{"level":2,"title":"六、问题与展望","slug":"六、问题与展望","link":"#六、问题与展望","children":[]}],"git":{"createdTime":1725198884000,"updatedTime":1725198884000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":16.79,"words":5036},"filePathRelative":"system-design/推荐系统/推荐系统入门.md","localizedDate":"2024年9月1日","excerpt":"\\n

转载自:https://www.cnblogs.com/cgli/p/17225189.html

","autoDesc":true}');export{e as comp,d as data}; diff --git "a/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-DNuD8_hU.js" "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-DNuD8_hU.js" new file mode 100644 index 00000000..92fc0ad0 --- /dev/null +++ "b/assets/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html-DNuD8_hU.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as r,o as n}from"./app-ftEjETWs.js";const o={};function p(s,e){return n(),a("div",null,e[0]||(e[0]=[r('

数据库缓存

在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。

禁用原因:

1.命中率低

2.写时所有都失效

禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。

查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182

',8)]))}const c=t(o,[["render",p],["__file","数据库缓存.html.vue"]]),m=JSON.parse('{"path":"/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html","title":"数据库缓存","lang":"zh-CN","frontmatter":{"description":"数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。 查询缓存...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"数据库缓存"}],["meta",{"property":"og:description","content":"数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。 查询缓存..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-01-25T11:42:16.000Z"}],["meta",{"property":"article:modified_time","content":"2024-01-25T11:42:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"数据库缓存\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-01-25T11:42:16.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1679834663000,"updatedTime":1706182936000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":2}]},"readingTime":{"minutes":0.34,"words":101},"filePathRelative":"database/mysql/数据库缓存.md","localizedDate":"2023年3月26日","excerpt":"\\n

在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

","autoDesc":true}');export{c as comp,m as data}; diff --git "a/assets/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html-CvCVGQll.js" "b/assets/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html-CvCVGQll.js" new file mode 100644 index 00000000..0ad6263a --- /dev/null +++ "b/assets/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html-CvCVGQll.js" @@ -0,0 +1,124 @@ +import{_ as i,c as a,d as n,o as l}from"./app-ftEjETWs.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

流程编排LiteFlow

LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

另外, LiteFlow 和 Activiti 们并不是同一个东西,而是面向不同的使用场景和需求。LiteFlow 更加轻量灵活,适合需要简单流程管理和动态配置的场景;而 Activiti 则是一个全面的 BPM 引擎,适合需要复杂业务流程管理和任务管理的场景。根据具体业务需求,可以选择合适的工具来实现流程编排。

背景

之前做过一个数据分发系统,需要消费kafka的数据,下游有不同的业务,每个业务可能有共同的地方,也有不同的地方,在经过各类的处理之后,最后数据分发到下游里面去。为了简化代码方便理解,我们定义4个Handler(A、B、C、D),然后有3个不同的业务,需要经过不同的Handler,整个流程如下。

image-20241108000137572

如果要在一个代码实现上诉功能,我们第一反应可能是责任链设计模式,每个业务一条链路,在Spring中,类似下面的代码:

public abstract class Handler {
+    abstract void handler(Request request);
+}
+
+@Component
+@Slf4j
+public class HandlerA extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器1");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerB extends Handler {
+    @Override
+    public void handler(Request request) {
+        log.info("处理器2");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerC extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器3");
+    }
+}
+@Component
+@Slf4j
+public class HandlerD extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器4");
+    }
+}
+
+//然后我们定义一个枚举类,用来配置不同业务需要经历过的处理器。
+public enum HandleBuz {
+    Business_1(HandlerA,HandlerB),
+    Business_2(HandlerB,HandlerC),
+    Business_3(HandlerA,HandlerD);
+    public final Class<? extends Handler>[] processors;
+    public HandleBuz(Class<? extends Handler>[] processors){
+        this.processors=processors;
+    }    
+    public void handle(){
+        for (Handler handler : processors) {
+            handler.handler(xxx);
+        }
+    }
+
+}

通过配置责任链,可以灵活地组合处理对象,实现不同的处理流程,并且可以在运行时动态地改变处理的顺序,由于责任链模式遵循开闭原则,新的处理者可以随时被加入到责任链中,不需要修改已有代码,提供了良好的扩展性。但实际上面对各种需求的时候,没法做到完全的解耦,比如对于HandlerA,如果业务1和业务2都有定制化的需求(来自产品提的临时或长期需求),此时是应该再HandlerA中用if else解决,还是再额外开个HandlerA_1和HandlerA_2。这类特性需求会非常多,最终把代码可读性变得越来越低。

一、为什么需要流程编排

LiteFlow由Baidu开源,专注于逻辑驱动流程编排,通过组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑。它以其轻量级、快速、稳定且可编排的特性,在业务流程管理、规则引擎、工作流、订单处理、数据处理、微服务编排以及智能化流程管理等领域都有广泛的应用前景。

img
img

二、它可以解决什么问题

对大部分不断迭代的代码来说,历史遗留的代码加上需要面对各类各样的需求,代码会变得越来越难维护,甚至变成屎山。我们想着不断的去进行解耦,不断的去进行切割拆分,还要兼顾新需求,就怕蝴蝶效应导致大故障,liteflow能帮我们在解耦上更加清晰一点。
(1)复杂业务流程编排和管理
在一些应用场景中,业务逻辑往往非常复杂,涉及多个步骤的执行,并且这些步骤之间具有复杂的依赖关系。LiteFlow 可以帮助开发者通过配置和代码相结合的方式定义和管理这些流程。
(2)流程动态配置
LiteFlow 允许通过配置文件或者数据库动态修改流程,而无需修改代码。这意味着可以根据不同的业务需求快速调整并发布新的流程,而不需要重新部署应用。
(3)流程节点的复用和解耦
在使用 LiteFlow 时,每个业务步骤都可以定义为一个独立的节点(Node),这些节点可以独立开发、测试和维护,并且可以在多个流程中复用。通过这种方式,可以实现业务逻辑的复用和解耦,提高代码的可维护性。
(4)节点状态和错误处理
LiteFlow 提供了丰富的节点状态管理和错误处理机制,允许开发者在流程执行过程中捕获和处理异常,从而确保系统的稳定性和健壮性。
(5) 高扩展性和自定义能力
LiteFlow 具有高度的扩展性,开发者可以根据自身业务的特殊需求定制节点、组件和插件,从而满足复杂场景的要求。

以下是一些实际使用 LiteFlow 的示例场景:
(1)订单处理系统:在电商系统中,订单处理涉及多个步骤,如库存检查、支付处理、订单确认和发货等。LiteFlow 可以帮助将这些步骤分开独立实现,然后通过流程引擎编排执行。
(2)审批流程:在企业中,审批流程通常包括多个节点(如申请、审批、复核、归档等),并且这些节点之间可能有条件和依赖关系。LiteFlow 可以帮助动态配置和管理这些流程,提高审批效率。
(3)营销活动:在一些营销活动中,不同的活动环节和逻辑可能会因用户行为和外部条件而变化。LiteFlow 可以帮助实现灵活的活动规则配置和执行。

三、LiteFlow改造之后

首先定义并实现一些组件,确保SpringBoot会扫描到这些组件并注册进上下文。

@Slf4j
+@LiteflowComponent("a")
+public class HandlerA extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("b")
+public class HandlerB extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("c")
+public class HandlerC extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("d")
+public class HandlerD extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}

同时,你得在resources下的config/flow.el.xml中定义规则:

<?xml version="1.0" encoding="UTF-8"?>
+<flow>
+    <chain name="chain1">
+        THEN(
+        a,b
+        );
+    </chain>
+    <chain name="chain2">
+        THEN(
+        b,c
+        );
+    </chain>
+    <chain name="chain3">
+        THEN(
+        a,d
+        );
+    </chain>
+</flow>

最后,在消费kafka的时候,先定义一个ruleChainMap,用来判断根据唯一的id(业务id或者消息id)来判断走哪条chain、哪个组件等,甚至可以定义方法级别的组件。

    private Map<Integer, String> ruleChainMap = new HashMap<>();
+    @Resource
+    private FlowExecutor flowExecutor;
+
+    @PostConstruct
+    private void init() {
+        ruleChainMap.put(1, "业务1");
+        ruleChainMap.put(2, "业务2");
+        ruleChainMap.put(3, "业务3");
+    }
+
+    @KafkaListener(topics = "xxxx")
+    public void common(List<ConsumerRecord<String, String>> records) {
+        for (ConsumerRecord<String, String> record : records) {
+            ...
+            String chainName = ruleChainMap.get("唯一id(可以是record里的,也可以全局定义的id)");
+            LiteflowResponse response = flowExecutor.execute2Resp(chainName, xxx, xxx, new TempContext());
+        }
+    }

由于篇幅的关系,这里不再讲解怎么传递上下文的关系,可以自己去官网研究一下。另外,上面的例子因为是简化之后的,如果你觉得不够形象,可以看看下面的实际业务。这个如果不使用liteflow,可能就得在主流程代码里增加各种if else,甚至后续改了一小块也不知道对别的地方有没有影响。

image-20241108000231371
image-20241108000231371

总结

后续,如果面对产品经理“来自大领导的一个想法,我不知道后续还会不会一直做下去,反正先做了再说”这类需求,就可以自己定义一个LiteFlow的组件,既不污染主流程的代码,后续下线了删掉即可,赏心悦目。

文档&参考

1.【腾讯文档】业务处理复杂 https://docs.qq.com/flowchart/DZVFURmhCb0JFUHFD
2.【腾讯文档】业务处理复杂2 https://docs.qq.com/flowchart/DZXVOaUV5VGRtc3ZD
3.一文搞懂设计模式—责任链模式
4.LiteFlow官网

`,28)]))}const p=i(h,[["render",k],["__file","流程编排LiteFlow.html.vue"]]),r=JSON.parse('{"path":"/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html","title":"流程编排LiteFlow","lang":"zh-CN","frontmatter":{"description":"流程编排LiteFlow LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"流程编排LiteFlow"}],["meta",{"property":"og:description","content":"流程编排LiteFlow LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/1.svg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-07T16:03:32.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-07T16:03:32.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"流程编排LiteFlow\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/1.svg\\",\\"https://github-images.wenzhihuai.com/images/image-20241108000231371.png\\"],\\"dateModified\\":\\"2024-11-07T16:03:32.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"背景","slug":"背景","link":"#背景","children":[]},{"level":2,"title":"一、为什么需要流程编排","slug":"一、为什么需要流程编排","link":"#一、为什么需要流程编排","children":[]},{"level":2,"title":"二、它可以解决什么问题","slug":"二、它可以解决什么问题","link":"#二、它可以解决什么问题","children":[]},{"level":2,"title":"三、LiteFlow改造之后","slug":"三、liteflow改造之后","link":"#三、liteflow改造之后","children":[]},{"level":2,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":2,"title":"文档&参考","slug":"文档-参考","link":"#文档-参考","children":[]}],"git":{"createdTime":1725814553000,"updatedTime":1730995412000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":2}]},"readingTime":{"minutes":7.11,"words":2134},"filePathRelative":"java/流程编排LiteFlow.md","localizedDate":"2024年9月8日","excerpt":"\\n

LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

","autoDesc":true}');export{p as comp,r as data}; diff --git "a/assets/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html-CwtUep3k.js" "b/assets/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html-CwtUep3k.js" new file mode 100644 index 00000000..18f396b5 --- /dev/null +++ "b/assets/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html-CwtUep3k.js" @@ -0,0 +1 @@ +import{_ as i,c as e,d as p,o as n}from"./app-ftEjETWs.js";const a={};function s(o,t){return n(),e("div",null,t[0]||(t[0]=[p('

程序员副业探索之电商

在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展趋势。结合自身的电商背景与资源,我倾向于专注于小程序电商,但对拼多多店铺和跨境电商也有兴趣。

探索小程序电商的过程中,我结识了许多朋友,也认识了几位从事电商运营的同事。最初,我将重心放在小程序女装项目上,计划从深圳的优质供应商中精选货品,打造一个集商品展示、购买、支付、私域于一体的完整小程序平台。然而,女装行业的高退货率远超预期,几乎每售出十件商品,就有七件被退货,这让项目难以持续。

因此,我开始考虑其他方向,如化妆品。化妆品作为日常消费品需求稳定且退货率较低,因而具有较高的利润。接下来,我将分享个人在化妆品电商领域的探索。

一、小程序化妆品

关于小程序,我们其实考虑过很多种不同的方向。比如,我们曾设想过做一个垂直类的平台,主要销售化妆品或者女装。我们也有其他的想法,比如销售珠宝等产品。但是,经过实际流程的运行后,我们发现小程序在很多方面存在诸多限制。下面,我将简单地介绍一下,如何创建一个小程序。

1.1 小程序准备(营业执照&微信支付&小程序appId)

首先,如果你想开发一个小程序,必须要具备微信支付的功能。而微信支付是个人无法申请的,因此需要一个营业执照。首先,你需要申请一个个人营业执照。

取得营业执照后,接下来就是用这个营业执照来申请开通微信支付。

QQ_1722269773245
QQ_1722269773245

接下来,你需要注册一个小程序。在去年,可以通过一些服务商或代理商注册,每个小程序的注册费用仅需几毛钱。但现在这条途径已被限制,目前注册一个小程序大约需要300元。此外,为了上线小程序,还需要通过审核,审核费用同样大约为300元。

同时,你还需要准备域名和服务器资源。我们选择了一台配置为8G内存的服务器,因为我们的成交量并不大,这样的配置已经足够应付日常需求。

1.2 小程序开发

在小程序开发方面,由于我们是想完全独立开发,因此首先我们从 GitHub 上查找了一个电商小程序的开源项目。尽管我们查看了很多项目,但基本上没有直接能用的,都需要进行小改造。我们对其进行了改进,接入了微信支付和腾讯的 COS 对象存储。

接下来,我们搭建了自己的 MySQL 数据库。考虑到我们的小程序交易量较小,因此没有使用分库分表的复杂结构,单独一个 MySQL 数据库已经足够。如果未来交易量大幅增加,可以考虑使用其他厂商的分库分表方案,例如 TiDB 等。

开发的框架网上的都类似一样,分为下面的几个模块,整体上感觉代码也不复杂(CRUD),就是比较耗时间。

  • 会员系统:用户注册、登录、会员等级、积分管理等。
  • 商品管理:商品的添加、编辑、删除、库存管理、分类管理等。
  • 订单管理:订单生成、支付、取消、退换货、订单状态管理等。
  • 支付系统:支持多种支付方式,如微信支付、支付宝支付、银联支付等。
  • 营销工具:优惠券、拼团、秒杀、砍价、满减活动等。
  • 物流管理:快递公司管理、物流信息追踪等。
  • 数据分析:提供销售数据统计、用户数据分析、商品销售分析等报表功能。
  • 客服系统:在线客服、留言板、消息通知等。
  • 权限管理:用户权限角色管理,不同角色有不同的操作权限。
强化私域流量管理
强化私域流量管理

网上那些代理商有赞、微盟之类的,可以0元搞个小程序,但是如果要用到营销功能(秒杀、优惠券等),都得给几千、几万块。底成本创业,就自己找开元弄一下了,前后端都会点。在成本方面,由于个人对化妆品并不熟悉,也分不清面霜、补水之类的,因此花费了约500元请别人帮忙上传商品信息。效果如下,也可去微信小程序上搜索一下体验下(不要付款)。

QQ_1722270680440
QQ_1722270680440

下面是小程序主要的一些功能:

域名配置:不能通过ip使用,需要申请好域名+https才可。

微信支付:如上一步所说,营业执照后即可把小程序对接微信支付

微信物流(发货信息管理):以前网上可集成快递100的功能,现在需要收费了,好在微信官方提供了自己的微信物流,可以直接对接,免费,填写完发货信息之后微信还可以直接通知用户。

客服:这个很重要,需要直接对接普通用户,回答一些咨询和售后内容。

微信认证:这个微信强求,没办法,300块。确保商品、图片、文字等内容的合法性,避免侵权行为。遵守相关电商法律法规,例如消费者权益保护、退换货政策等。

尽管小程序已经上线,整体功能虽然齐全,但效果不如预期。

我们原本设想,当用户下单后可以从华南、北地区的供货商那里进行发货。然而,整体计算下来,由于缺乏足够的曝光,导致小程序的商品没有得到应有的推广。在微信平台,小程序的商品不会自动得到推广曝光,需要开发者自行进行推广。

我们在开发初期计划通过小程序结合私域流量进行引流,主要策略包括:建立小程序,通过社群引流和推广,吸引粉丝,再通过促销和秒杀等营销活动来提高订单量。然而,第一步的推广就遇到了困难。整个五月份几乎没有用户点击和访问。

最初立下的方向较为宏大,计划通过私域流量提升销售。然而,由于对化妆品行业了解不够深,加之市场上存在假货问题,也是在无法更多的推广。

QQ_1722270494718
QQ_1722270494718

算一下总投入吧:

营业执照(180)+微信支付认证(50)+小程序appId(2)+服务器(100/月)+请人录入化妆品(500),最最最重要的是,营业执照还需要年审,查税什么的,如果不用还得花钱注销。

QQ_1722359862581
QQ_1722359862581

二、拼多多电商

在打造自己的私域生态时,我发现仅依靠小程序并不理想,因此开始考虑其他电商平台,比如小红书和拼多多。

在小红书上,阿里巴巴的 1688 平台不太支持直接批量上传商品,所有商品和图片都需要手动录入和上传,非常不方便。至于选择拼多多还是淘宝,我个人更倾向于拼多多,因为其用户相对较多。

在选择供应商时,发现 1688 上的化妆品要么不存在,要么是一些小众品牌,无法放心销售。因此,我决定选择一个垂直方向的产品,最终专注于女性相关的饰品,比如手链和宠物项链。

我在 1688 上找到了一些手链店铺,并支付了 1000 元的保证金,随后通过 1688 的分销中心将产品直接分销到拼多多。然而,拼多多的定价策略需要自行调整,否则利润非常低,甚至可能亏损。

在开始销售时,由于没注意到新疆和西藏不包邮,导致一起订单亏损了十多元。总体来说,虽然通过广告推广获取了一些流量和曝光,并且实现了约 10 个订单,销售额为 200多 元,但最终有一半订单被退货,只盈利了大约 100 元。

由于退货地址填写的是我自己这边,所有退货商品都被寄回给我。检查后发现,这些产品的质量确实不佳。由于麻烦,我决定不再将退货商品寄回 1688 的商家,干脆自己留着。

clone
clone

成本:成交金额270,退单一半左右,自己承担,没退给1688商家,大概亏损130左右。

三、跨境电商

在腾讯工作期间,我听说有些人通过跨境电商取得了显著的成功,比如将电子烟卖到欧美国家,甚至在一年内赚到了一套深圳的房子。当然,这可能有些夸大。然而,国内电商市场竞争异常激烈,利润微薄,特别是拼多多对工厂的压榨,使得供应商难以生存。广州奥园的事件也是一个例子,显示了供应商在国内激烈竞争中的艰难处境。

我也考虑过跨境电商的可能性,比如建立独立站或在亚马逊上运营。目前,这些想法还在考察阶段,需要进一步了解和评估。

QQ_1722358300059
QQ_1722358300059

偶然听别人说:中产做生意返贫之路,就是奶茶店、跨境电商!

四、总结

小程序+拼多多店,应该亏损1000左右吧,就当做交学费,总的来说,也积累了不少经验。对整体的电商流程,包括开设门店和私域营销,都会有一定了解。但坦率地说,电商市场竞争非常激烈,利润空间被压缩得很厉害。

首先,以拼多多为例,它直接连接了工厂和用户(B2C),消除了很多中间环节。如果你想在这一过程中作为中间商进行营销推广,就必须具备独特的优势。现在一些平台,比如直播带货,采用的就是大批量采购的方式。这种方式确实能降低成本,并且通过批量采购获利。

另一方面,很多用户并不了解像阿里巴巴这样的货源平台,还有些用户甚至还在使用淘宝。利用这种信息差,还是可以赚取一定的利润,但这也让电商行业变得更加竞争激烈。

在电商领域的下一步发展方向上,我个人认为直播带货可能是一个更具潜力的选择。结合种草文化和私域流量管理,这种方式可能相对来说能带来更多的收益。作为中间商,目前似乎也只有这条路可以探索。可惜不是人人都能成为李佳琦或者董宇辉。

',54)]))}const r=i(a,[["render",s],["__file","程序员副业探索之电商.html.vue"]]),h=JSON.parse('{"path":"/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html","title":"程序员副业探索之电商","lang":"zh-CN","frontmatter":{"description":"程序员副业探索之电商 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。 在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"程序员副业探索之电商"}],["meta",{"property":"og:description","content":"程序员副业探索之电商 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。 在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/test/QQ_1722269773245.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-08-04T14:51:49.000Z"}],["meta",{"property":"article:modified_time","content":"2024-08-04T14:51:49.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"程序员副业探索之电商\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/test/QQ_1722269773245.png\\",\\"https://github-images.wenzhihuai.com/test/pro_section3_content.png\\",\\"https://github-images.wenzhihuai.com/test/QQ_1722270680440.png\\",\\"https://github-images.wenzhihuai.com/test/QQ_1722270494718.png\\",\\"https://github-images.wenzhihuai.com/test/QQ_1722359862581.png\\",\\"https://github-images.wenzhihuai.com/test/clone.png\\",\\"https://github-images.wenzhihuai.com/test/QQ_1722358300059.png\\"],\\"dateModified\\":\\"2024-08-04T14:51:49.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、小程序化妆品","slug":"一、小程序化妆品","link":"#一、小程序化妆品","children":[{"level":3,"title":"1.1 小程序准备(营业执照&微信支付&小程序appId)","slug":"_1-1-小程序准备-营业执照-微信支付-小程序appid","link":"#_1-1-小程序准备-营业执照-微信支付-小程序appid","children":[]},{"level":3,"title":"1.2 小程序开发","slug":"_1-2-小程序开发","link":"#_1-2-小程序开发","children":[]}]},{"level":2,"title":"二、拼多多电商","slug":"二、拼多多电商","link":"#二、拼多多电商","children":[]},{"level":2,"title":"三、跨境电商","slug":"三、跨境电商","link":"#三、跨境电商","children":[]},{"level":2,"title":"四、总结","slug":"四、总结","link":"#四、总结","children":[]}],"git":{"createdTime":1722783109000,"updatedTime":1722783109000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":10.01,"words":3002},"filePathRelative":"interesting/程序员副业探索之电商.md","localizedDate":"2024年8月4日","excerpt":"\\n

在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

","autoDesc":true}');export{r as comp,h as data}; diff --git "a/assets/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html-DwU2qcUM.js" "b/assets/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html-DwU2qcUM.js" new file mode 100644 index 00000000..8a0ac69d --- /dev/null +++ "b/assets/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html-DwU2qcUM.js" @@ -0,0 +1 @@ +import{_ as a,c as o,a as e,o as n}from"./app-ftEjETWs.js";const r={};function i(c,t){return n(),o("div",null,t[0]||(t[0]=[e("h1",{id:"行锁-表锁-意向锁",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#行锁-表锁-意向锁"},[e("span",null,"行锁,表锁,意向锁")])],-1)]))}const p=a(r,[["render",i],["__file","行锁,表锁,意向锁.html.vue"]]),s=JSON.parse('{"path":"/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html","title":"行锁,表锁,意向锁","lang":"zh-CN","frontmatter":{"index":false,"description":"行锁,表锁,意向锁","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"行锁,表锁,意向锁"}],["meta",{"property":"og:description","content":"行锁,表锁,意向锁"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"行锁,表锁,意向锁\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.03,"words":9},"filePathRelative":"database/mysql/行锁,表锁,意向锁.md","localizedDate":"2024年3月10日","excerpt":"\\n","autoDesc":true}');export{p as comp,s as data}; diff --git "a/assets/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html-Zcj8usmP.js" "b/assets/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html-Zcj8usmP.js" new file mode 100644 index 00000000..9c6755b7 --- /dev/null +++ "b/assets/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html-Zcj8usmP.js" @@ -0,0 +1 @@ +import{_ as t,c as r,d as p,o as a}from"./app-ftEjETWs.js";const i={};function n(o,e){return a(),r("div",null,e[0]||(e[0]=[p('

计算广告基本概念入门总结

广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

一、计算广告入门
1.1、什么是计算广告
首先要理解广告的概念,广告指的是由已确定的出资人通过各类媒介进行有关产品、观点和服务的、通常是有偿的、有组织的、综合的、劝服性的非人员信息传播活动。

广告的主体包括了三类:

广告主-有发布广告需求的用户
广告媒介-也就是承接广告主需求以及调配广告展示的广告平台
受众-通过媒介看到广告。
广告活动的两个主动参与方—出资人(sponsor) 和媒体(medium) 。 在数字广告这样更加复杂的市场结构中, 我们可以用一般性的术语来描述它们: 需求方(demand) 和供给方(supply) 。 这里的需求方可以是广告主 (advertiser) 、 代表广告主利益的代理商(agency) 或其他技术形态的采买方; 这里的供给方可以是媒体, 也可以是其他技术形态的变现平台。 另外, 要特别注意的是, 广告还有一个被动的参与方, 即受众 (audience) 。

广告的根本目的是,通过广泛的媒体力量,使得广告能够高效、低成本地触达用户,以保持产品或服务的品牌形象或提高短期销量(也就是品牌广告和效果广告)。

计算广告并没有严格的定义, 个人理解计算广告指的是通过大数据、推荐算法等技术对大量数据进行分析,以达到广告的精确受众定向,并且通过计算来调控广告的整个生命周期。计算广告相较于传统广告的特点是以技术和计算为导向,以数据为基准,具备标准化的、可衡量性的广告效果指标。

计算广告的核心问题是如何为一系列用户与环境组合,以找到最合适的广告投放策略以优化整体的投入产出比(ROI)。

1.2、广告与营销的区别
人们通常将广告理解为营销,但是两者是有一定的区别的。

首先广告和营销的共同点都是为了提高产品或服务的销量、营业额及利润,但是区别在于,广告更希望通过低成本触达到用户,这里的用户指的是潜在用户,而营销更希望的是对某类精准用户进行服务,可以理解为广告的目标是增量用户,潜在用户,看着品牌价值和长期利润,而营销更看重目标用户和短期效果。

1.3、在线广告的类型
条幅广告(Banner Ad): 这是显示广告中中最传统,也是最典型的形式。这种广告一般是嵌入在页面中相对固定位置的图片,与内容一样需要占据固定的版面。
文字链广告(Textual Ad): 这种广告的素材形式是一段链接到广告主落地页的文字,在搜索广告中为主流形式,同时在媒体广告中也被广泛采用。
富媒体广告(Rich Media Ad): 这类广告往往是利用视觉冲击力较强的表现形式,在不占用固定版面位置的情况下,向用户侵入式地投送广告素材。
富媒体广告常见的形式有弹窗、对 联、全屏等。它比较适合在高质量的媒体做一些品牌性质比较强的广告投放,但是对用户的使用体验往往影响也较大。
一些门户网站的首页有时会为某个品牌广告主提供专门定制的、交互形式很复杂的富媒体广告, 这样的广告一般不采用受众定向的投送方式,也主要强调创意的冲击力和交互形式的特色。
视频广告(Video Ad): 视频广告有两种最主要的形式:在视频内容播放之前的前插片广告,以及视频播放暂停时的广告。前插片广告一般采用短视频的形式,创意的冲击力和表现力要远远强于普通的显示广告,因此CPM价格往往也比较高;暂停广告则与普通的条幅广告区别不大。
社交广告或信息流广告(Social Ad): 社交广告中最典型的形式,是插入在社交网络信息流中的广告,这种方式最早见于Twitter,产品称为“Promoted Tweets”。社交广告希望达到的效果,是通过用户的扩散式传播获 得更大的影响力,以及更可信的口碑.
移动设备广告(Mobile Ad): 严格来说, 移动互联网上的广告与桌面电脑上的广告没有本质区别.
邮件营销广告(Email Direct Marketing, EDM): EDM是一种主动的广告形式,它不需要等到用户接触的机会产生时才被动地提供广告,而是可以随时向认为合适的用户发送推广信息。不过也正因为如此,EDM非常容易变垃圾邮件的主要来源。
1.4、在线广告发展历程

在这里插入图片描述
在这里插入图片描述

阶段一:传统合约广告模式,花钱买一段时间的广告位或展示次数
阶段二:定向及合约广告阶段,签约形式为合约广告,广告采用了特定用户定向的方式进行展示,提升转化率
阶段三:定向及竞价广告阶段,签约形式为竞价广告,广告采用了特定用户定向的方式进行展示,提升转化率
阶段四:实时竞价模式阶段(重要里程碑),采用广告交易平台(ADX)、需求方平台(DSP)及供给方平台(SSP)完成广告竞价,出现广义第二高价
阶段五:程序化交易,买卖双方无需在线交易,完全由程序进行交易的调控,无需人工参与
1.5、广告有效性模型

在这里插入图片描述
在这里插入图片描述

广告效果生成过程有三个大阶段:选择(Selection)、解释(Interpretation)与态度(Attitude);或者进一步分解为六个小阶段:曝光、关注、理解、接受、保持与决策,其中每两个小阶段对应一个大阶段。

定性地说,越靠前的阶段,其效果的改善对点击率的贡献越大;而越靠后的阶段,其效果的改善对转化率的贡献越大。

曝光(Exposure)阶段: 这一阶段指的是广告物理上展现出来的过程。此阶段的有效性往往与广告位的物理属性有关,并没有太多可以通过技术优化的空间。实际的广告实践中,曝光的有效性对最终结果的影响往往远远高于其他技术性因素。
关注(Attention)阶段:这一阶段指的是受众从物理上接触到广告到意识上注意到它的过程。那么如何使得关注阶段的效率提高呢?我们介绍几个重要的原则:
(1)尽量不要打断用户的任务。
(2)明确传达向用户推送此广告的原因,这一点是受众定向广告创意优化的重要方向。
(3)内容符合用户的兴趣或需求,这是受众定向的原理基础。
理解(Comprehension)阶段:受众意识到了广告的存在,并不意味着他一定能够理解广告传达的信息。
(1)广告内容要在用户能理解的具体兴趣范围内,这就说明了真正精准的受众定向有多么必要。
(2)要注意设定与关注程度相匹配的理解门槛。
接受(Acceptance)阶段:受众理解了广告传达的信息,并不一定表示他认可这些信息。
(1)广告的上下文环境对于广告的接受程度也有着很大的影响, 同一个品牌广告出现在某游戏社区上和门户网站首页上,用户会倾向于认为后者更具说服力,这也就是优质媒体的品牌价值。
(2)在定向广告越来越普遍的今天,如何让合适的广告出现在合适的媒体上,即广告安全(Ad Safety)的问题,正在引起大家越来越多的关注。
保持(Retention)阶段:对于不仅仅追求短期转化的广告商,当然希望广告传达的信息给用户留下长久的记忆,以影响他长时间的选择。
决策(Decision)阶段:成功广告的最终作用是带来用户的转化行为,虽然这一阶段已经离开了广告的业务范围,但好的广告还是能够为转化率的提高做好铺垫。
二、计算广告的核心概念
品牌广告(Brand Awareness)
长期投放,用于建立品牌形象、建立品牌理念,而不诉求直接销售量的广告

直接效果广告/效果广告(Direct Response)
短期投放,为了提升产品销售量或提高知名度而投放的广告,因此通常按效果计费(CPA或CPS)

展示广告(display advertising)
以图形展示为核心的广告形式,如常见的banner、视频广告等,通常以CPM、CPT为计费方式

位置拍卖(position auction)
假设有一组广告位可使用,则将广告位按价值排序,并将广告主出价排序,并依次对应投放,价高者将得到更好的广告位

CPT(Cost Per Time)
按展示时长计费;可以充分发挥橱窗效应,但无法利用受众定向技术,效率较低;使用于高曝光的品牌广告

CPM(Cost Per Mille)
即“千人成本”,按访问量(每千人的访问量)计费,是目前最主流的结算方法,具备受众定向能力,适用于实时竞价广告

eCPM(千人收益)
e指的是expect,也就是预期最优的的千人访问收益。

CPC(Cost Per Click)
按点击次数计费

CPA(Cost Per Action)
按用户行为、实际投放效果计费,如用户的注册、购买、安装行为,供给方运营难度较大,而需求方无任何风险,适合于效果广告

CPS(Cost Per Sell) ROI(Return On Investment)
同CPA,都是按实际效果计费的方式,ROI即常说的“投资回报率”

点击率(Click Through Rate,CTR)
即广告点击与广告展示的比率

到达率 (Reach Rate)
落地页打开次数与点击次数比例,落地页(landing page)即点击后所跳转的页面

转化率CVR (Conversion Rate)
转化次数与到达次数的比例

在线广告交易模式
合约广告、竞价广告、实时竞价、程序化广告

广告交易的相关机构与平台(程序化广告)
广告网络 ADN(ad net-work):连接供给方与需求方,负责批量运营媒体广告位,并对流量进行售卖的平台
需求方平台DSP(demand side platform):为广告主提供跨媒介、跨平台、跨终端的的广告投放平台,通过数据整合、分析实现基于受众的精准投放
供给方平台 SSP(supply side platform):是为媒体提供服务的平台,负责管理媒体广告位库存、优化广告位的售卖,提高其广告资源价值,帮助其提升收益
广告交易平台 ADX(ad Exchange):帮助DSP与SSP通过RTB方式进行广告交易的平台
数据管理平台 DMP(data management platform):负责统合、加工第三方用户数据,并售卖给DSP的数据平台

在这里插入图片描述
在这里插入图片描述

定向广告(targeted advertising)
不再把广告投给所有人,而是面向不同的受众,赋予其不同的用户标签,并投放不同的广告。由此,广告主从广告位的采买,变成了面向受众人群的采买

流量预测
预估该媒体中会产生的流量总数,便于进行售前指导/出价指导,如果低估了平台流量,可能导致售卖不足,利润无法最大化;如果高估平台流量,可能导致超售,无法满足合约

流量塑形(traffic shaping)
通过主动影响流量来实现合约达成,综合性门户网站是典型例子,网站通过优化导流与跳转页布局,为用户点击链接提供便利,实现最大的到达率

在线分配
即通过设计分配策略,实现高效的流量分配。展示合约都面临的问题:在流量/广告位有限,而满足合约的人群大量重叠的情况下,要实现利益最大化,如何让各个合约到最大限度满足

广义第二高价理论:Generalized Second Price, GSP
指在只有一个位置的拍卖中,出价最高的广告主赢得广告位后,只需要支付其下一位广告主的出价(排名第二的广告主),而不用按自己的出价来购买,这是一种最合理的竞价逻辑

搜索广告(search ad)
通过用户的搜索关键字作为特征召回广告内容

上下文广告(contextual advertising)
根据浏览页面的关键词进行投放,而不是用户输入的关键词

原生广告&信息流广告(Native advertising)
广告内容与媒介信息形式高度吻合的广告,都可以称为原生广告,“广告即内容、内容即广告”

',40)]))}const s=t(i,[["render",n],["__file","计算广告基本概念入门总结.html.vue"]]),b=JSON.parse('{"path":"/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html","title":"计算广告基本概念入门总结","lang":"zh-CN","frontmatter":{"description":"计算广告基本概念入门总结 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"计算广告基本概念入门总结"}],["meta",{"property":"og:description","content":"计算广告基本概念入门总结 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/test/70353957ecb1660b1ca67b4c8aea852b.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-09-01T13:54:44.000Z"}],["meta",{"property":"article:modified_time","content":"2024-09-01T13:54:44.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"计算广告基本概念入门总结\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/test/70353957ecb1660b1ca67b4c8aea852b.png\\",\\"https://github-images.wenzhihuai.com/test/926e2083e685daca5ccc3b45e3fe1c27.png\\",\\"https://github-images.wenzhihuai.com/test/142f29cc4578fcdeff0185c2629209b2.png\\"],\\"dateModified\\":\\"2024-09-01T13:54:44.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1725198884000,"updatedTime":1725198884000,"contributors":[{"name":"zhihuaiwen","email":"1570631036@qq.com","commits":1}]},"readingTime":{"minutes":12.31,"words":3692},"filePathRelative":"system-design/计算广告/计算广告基本概念入门总结.md","localizedDate":"2024年9月1日","excerpt":"\\n

广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

","autoDesc":true}');export{s as comp,b as data}; diff --git "a/assets/\350\260\203\344\274\230\346\200\235\350\267\257.html-DzuQiit4.js" "b/assets/\350\260\203\344\274\230\346\200\235\350\267\257.html-DzuQiit4.js" new file mode 100644 index 00000000..972b0faa --- /dev/null +++ "b/assets/\350\260\203\344\274\230\346\200\235\350\267\257.html-DzuQiit4.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as r,o as n}from"./app-ftEjETWs.js";const o={};function i(p,e){return n(),a("div",null,e[0]||(e[0]=[r('

JVM调优思路

在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

1.监控发现问题
2.工具分析问题
3.性能调优
下面开始一步步讲解

一、监控发现问题

通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:

GC频繁
CPU负载过高
OOM
内存泄露
死锁
程序响应时间较长

二、工具分析问题

参考

1.【JVM调优】如何进行JVM调优?一篇文章就够了!

',9)]))}const c=t(o,[["render",i],["__file","调优思路.html.vue"]]),s=JSON.parse('{"path":"/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html","title":"JVM调优思路","lang":"zh-CN","frontmatter":{"index":false,"description":"JVM调优思路 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。 1.监控发现问题 2.工具分析问题 3.性能调优 下面开始一步步讲解 一、监控发现问题 通过监...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"JVM调优思路"}],["meta",{"property":"og:description","content":"JVM调优思路 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。 1.监控发现问题 2.工具分析问题 3.性能调优 下面开始一步步讲解 一、监控发现问题 通过监..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-02-16T11:46:28.000Z"}],["meta",{"property":"article:modified_time","content":"2024-02-16T11:46:28.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"JVM调优思路\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-02-16T11:46:28.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"一、监控发现问题","slug":"一、监控发现问题","link":"#一、监控发现问题","children":[]},{"level":2,"title":"二、工具分析问题","slug":"二、工具分析问题","link":"#二、工具分析问题","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1708083988000,"updatedTime":1708083988000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.7,"words":210},"filePathRelative":"java/JVM/调优思路.md","localizedDate":"2024年2月16日","excerpt":"\\n

在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

","autoDesc":true}');export{c as comp,s as data}; diff --git "a/assets/\350\265\233\345\212\233\346\226\257.html-BrY99iGU.js" "b/assets/\350\265\233\345\212\233\346\226\257.html-BrY99iGU.js" new file mode 100644 index 00000000..fd880d70 --- /dev/null +++ "b/assets/\350\265\233\345\212\233\346\226\257.html-BrY99iGU.js" @@ -0,0 +1 @@ +import{_ as a,c as o,a as e,o as n}from"./app-ftEjETWs.js";const r={};function i(p,t){return n(),o("div",null,t[0]||(t[0]=[e("h1",{id:"赛力斯",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#赛力斯"},[e("span",null,"赛力斯")])],-1),e("p",null,"update",-1)]))}const m=a(r,[["render",i],["__file","赛力斯.html.vue"]]),l=JSON.parse('{"path":"/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html","title":"赛力斯","lang":"zh-CN","frontmatter":{"description":"赛力斯 update","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"赛力斯"}],["meta",{"property":"og:description","content":"赛力斯 update"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-07-02T14:33:09.000Z"}],["meta",{"property":"article:modified_time","content":"2024-07-02T14:33:09.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"赛力斯\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-07-02T14:33:09.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[],"git":{"createdTime":1719930789000,"updatedTime":1719930789000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.01,"words":4},"filePathRelative":"stock/赛力斯.md","localizedDate":"2024年7月2日","excerpt":"\\n

update

\\n","autoDesc":true}');export{m as comp,l as data}; diff --git "a/assets/\351\253\230\345\217\257\347\224\250.html-DjOPo7tP.js" "b/assets/\351\253\230\345\217\257\347\224\250.html-DjOPo7tP.js" new file mode 100644 index 00000000..6b505aea --- /dev/null +++ "b/assets/\351\253\230\345\217\257\347\224\250.html-DjOPo7tP.js" @@ -0,0 +1 @@ +import{_ as e,c as n,d,o as i}from"./app-ftEjETWs.js";const a={};function r(l,t){return i(),n("div",null,t[0]||(t[0]=[d('

高可用

4个9(99.99)

SentinelHystrix(维护状态)Resilience4j(Spring推荐)
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口滑动窗口Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性支持多种数据源支持多种数据源有限支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统的自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统

流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

arch
arch

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

熔断降级

什么是熔断降级

除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。

image
image

Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

熔断降级设计理念

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

Sentinel 对这个问题采取了两种手段:

  • 通过并发线程数进行限制

和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

  • 通过响应时间对资源进行降级

除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

异地多活

参考

1.Sentinel官网
2.Sentinel 集群限流的实现(上)
3.Sentinel 集群限流的实现(下)

',25)]))}const o=e(a,[["render",r],["__file","高可用.html.vue"]]),p=JSON.parse('{"path":"/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html","title":"高可用","lang":"zh-CN","frontmatter":{"description":"高可用 4个9(99.99) 流量控制 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"高可用"}],["meta",{"property":"og:description","content":"高可用 4个9(99.99) 流量控制 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://github-images.wenzhihuai.com/images/sentinel-flow-overview-20240218114454907.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"高可用\\",\\"image\\":[\\"https://github-images.wenzhihuai.com/images/sentinel-flow-overview-20240218114454907.jpg\\",\\"https://github-images.wenzhihuai.com/images/62410811-cd871680-b61d-11e9-9df7-3ee41c618644.png\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"流量控制","slug":"流量控制","link":"#流量控制","children":[]},{"level":2,"title":"熔断降级","slug":"熔断降级","link":"#熔断降级","children":[]},{"level":2,"title":"异地多活","slug":"异地多活","link":"#异地多活","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":4.27,"words":1281},"filePathRelative":"java/高可用.md","localizedDate":"2024年3月10日","excerpt":"\\n

4个9(99.99)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
SentinelHystrix(维护状态)Resilience4j(Spring推荐)
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口滑动窗口Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性支持多种数据源支持多种数据源有限支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统的自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统
","autoDesc":true}');export{o as comp,p as data}; diff --git "a/assets/\351\253\230\345\271\266\345\217\221.html-B4hi1h21.js" "b/assets/\351\253\230\345\271\266\345\217\221.html-B4hi1h21.js" new file mode 100644 index 00000000..b6693258 --- /dev/null +++ "b/assets/\351\253\230\345\271\266\345\217\221.html-B4hi1h21.js" @@ -0,0 +1 @@ +import{_ as t,c as n,d as r,o}from"./app-ftEjETWs.js";const a={};function p(i,e){return o(),n("div",null,e[0]||(e[0]=[r('

高并发

QPS

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

  • 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
  • 吞吐量:单位时间内处理的请求数量。
  • QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。

负载均衡

正所谓双拳难敌四手,高并发撑场面的首选方案就是集群化部署,一台服务器承载的QPS有限,多台服务器叠加效果就不一样了。

如何将流量转发到服务器集群,这里面就要用到负载均衡,比如:LVS 和 Nginx。

常用的负载算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接数法等

业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。

注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx

池化技术

复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,有时间开销浪费。池化技术的核心是资源的“预分配”和“循环使用”,常用的池化技术有线程池、进程池、对象池、内存池、连接池、协程池。

连接池的几个重要参数:最小连接数、空闲连接数、最大连接数

Linux 内核中是以进程为单元来调度资源的,线程也是轻量级进程。所以说,进程、线程都是由内核来创建并调度。协程是由应用程序创建出来的任务执行单元,比如 Go 语言中的协程“goroutine”。协程本身是运行在线程上,由应用程序自己调度,它是比线程更轻量的执行单元。

在 Go 语言中,一个协程初始内存空间是 2KB(Linux 下线程栈大小默认是 8MB),相比线程和进程来说要小很多。协程的创建和销毁完全是在用户态执行的,不涉及用户态和内核态的切换。另外,协程完全由应用程序在用户态下调用,不涉及内核态的上下文切换。协程切换时由于不需要处理线程状态,需要保存的上下文也很少,速度很快。

Go语言中协程池的实现方法有两种:抢占式和调度式。

抢占式协程池,所有任务存放到一个共享的 channel 中,多个协程同时去消费 channel 中的任务,存在锁竞争。
调度式协程池,每个协程都有自己的 channel,每个协程只消费自己的 channel。下发任务的时候,采用负载均衡算法选择合适的协程来执行任务。比如选择排队中任务最少的协程,或者简单轮询。

流量漏斗

上面讲的是正向方式提升系统QPS,我们也可以逆向思维,做减法,拦截非法请求,将核心能力留给正常业务!

互联网高并发流量并不都是纯净的,也有很多恶意流量(比如黑客攻击、恶意爬虫、黄牛、秒杀器等),我们需要设计流量拦截器,将那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。

拦截器分层:

网关和 WAF(Web Application Firewall,Web 应用防火墙)
采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法

风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用。
下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。

',23)]))}const c=t(a,[["render",p],["__file","高并发.html.vue"]]),l=JSON.parse('{"path":"/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html","title":"高并发","lang":"zh-CN","frontmatter":{"description":"高并发 QPS 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。 响应时间:系统对请求做出...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"高并发"}],["meta",{"property":"og:description","content":"高并发 QPS 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。 响应时间:系统对请求做出..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"高并发\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"负载均衡","slug":"负载均衡","link":"#负载均衡","children":[]},{"level":2,"title":"池化技术","slug":"池化技术","link":"#池化技术","children":[]},{"level":2,"title":"流量漏斗","slug":"流量漏斗","link":"#流量漏斗","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":4.28,"words":1285},"filePathRelative":"java/高并发.md","localizedDate":"2024年3月10日","excerpt":"\\n

QPS

\\n

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

","autoDesc":true}');export{c as comp,l as data}; diff --git "a/assets/\351\253\230\346\200\247\350\203\275.html-CN7rmRWu.js" "b/assets/\351\253\230\346\200\247\350\203\275.html-CN7rmRWu.js" new file mode 100644 index 00000000..f95b49bd --- /dev/null +++ "b/assets/\351\253\230\346\200\247\350\203\275.html-CN7rmRWu.js" @@ -0,0 +1 @@ +import{_ as t,c as a,d as p,o as n}from"./app-ftEjETWs.js";const r={};function i(o,e){return n(),a("div",null,e[0]||(e[0]=[p('

高性能

响应时间

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

那么有哪些因素会影响系统的性能呢?

用户网络环境
请求/响应的数据包大小
业务系统 CPU、内存、磁盘等性能
业务链路的长度
下游系统的性能
算法实现是否高效
当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。

1、高性能缓存

对一些热点数据每次都从 DB 中读取,会给 DB 带来较大的压力,导致性能大幅下降。所以,我们需要用缓存来提升热点数据的访问性能,比如将活动信息数据在浏览器的缓存中保存一段时间。

缓存根据性能由高到低分为:寄存器、L1缓存、L2缓存、L3缓存、本地内存、分布式缓存

上层的寄存器、L1 缓存、L2 缓存是位于 CPU 核内的高速缓存,访问延迟通常在 10 纳秒以下。L3 缓存是位于 CPU 核外部但在芯片内部的共享高速缓存,访问延迟通常在十纳秒左右。高速缓存具有成本高、容量小的特点,容量最大的 L3 缓存通常也只有几十MB。

本地内存是计算机内的主存储器,相比 CPU 芯片内部的高速缓存,内存的成本要低很多,容量通常是 GB 级别,访问延迟通常在几十到几百纳秒。

内存和高速缓存都属于掉电易失的存储器,如果机器断电了,这类存储器中的数据就丢失了。

特别说明:在使用缓存时,要注意缓存穿透、缓存雪崩、缓存热点问题、缓存数据一致性问题。当然为了提升整体性能通常会采用多级缓存组合方案(浏览器缓存+服务端本地内存缓存+服务端网络内存缓存)

2、日志优化,避免IO瓶颈

当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”。

在IO中断过程中,如果此时有其他任务线程可调度,系统会直接调度其他线程,这样 CPU 就相应显示为 Usr 或 Sys;但是如果此时系统较空闲,无其他任务可以调度,CPU 就会显示为 iowait(实际上与 idle 无本质区别)。

磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住。

Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除。

这种批量化、顺序写,大大提升了磁盘的吞吐性能!

缓存

异步

I/O(网络、数据、文件)

分库分表

参考

1.怎么优化java项目性能

',24)]))}const h=t(r,[["render",i],["__file","高性能.html.vue"]]),s=JSON.parse('{"path":"/java/%E9%AB%98%E6%80%A7%E8%83%BD.html","title":"高性能","lang":"zh-CN","frontmatter":{"description":"高性能 响应时间 性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。 那么有哪些因素会影响系统的性能呢? 用户网络环境 请求/响应的数据包大小 业务系统 CPU、内存、磁盘等性能 业务链路的长度 下游系统的性能 算法实现是否高效 当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。 1、高性能缓存 对一些热点...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"高性能"}],["meta",{"property":"og:description","content":"高性能 响应时间 性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。 那么有哪些因素会影响系统的性能呢? 用户网络环境 请求/响应的数据包大小 业务系统 CPU、内存、磁盘等性能 业务链路的长度 下游系统的性能 算法实现是否高效 当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。 1、高性能缓存 对一些热点..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"高性能\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"缓存","slug":"缓存","link":"#缓存","children":[]},{"level":2,"title":"异步","slug":"异步","link":"#异步","children":[]},{"level":2,"title":"I/O(网络、数据、文件)","slug":"i-o-网络、数据、文件","link":"#i-o-网络、数据、文件","children":[]},{"level":2,"title":"分库分表","slug":"分库分表","link":"#分库分表","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1710081570000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":3.03,"words":909},"filePathRelative":"java/高性能.md","localizedDate":"2024年3月10日","excerpt":"\\n

响应时间

\\n

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

","autoDesc":true}');export{h as comp,s as data}; diff --git "a/assets/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html-DLPHuyVJ.js" "b/assets/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html-DLPHuyVJ.js" new file mode 100644 index 00000000..47d1b004 --- /dev/null +++ "b/assets/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html-DLPHuyVJ.js" @@ -0,0 +1 @@ +import{_ as a,c as t,d as r,o as n}from"./app-ftEjETWs.js";const l={};function h(o,e){return n(),t("div",null,e[0]||(e[0]=[r('

高性能高并发高可用的一些思考

TODO待补充

异步

Jmeter

JProfiler

火焰图

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

内存火焰图

缓存

限流熔断

Kafka

序列化

参考

1.Java数据库高并发如何解决 java高并发三种解决方法 转载
2.Java多线程梳理之四_其他并发解决方案
3.Java高并发之并发基础

',14)]))}const s=a(l,[["render",h],["__file","高性能高并发高可用的一些思考.html.vue"]]),p=JSON.parse('{"path":"/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html","title":"高性能高并发高可用的一些思考","lang":"zh-CN","frontmatter":{"description":"高性能高并发高可用的一些思考 TODO待补充 异步 Jmeter JProfiler 火焰图 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar 内存火焰图 缓存 限流熔断 Kafka 序列化 参考 1.Java数据库高并发如何解决 java高并发三种解...","head":[["meta",{"property":"og:url","content":"http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html"}],["meta",{"property":"og:site_name","content":"个人博客"}],["meta",{"property":"og:title","content":"高性能高并发高可用的一些思考"}],["meta",{"property":"og:description","content":"高性能高并发高可用的一些思考 TODO待补充 异步 Jmeter JProfiler 火焰图 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar 内存火焰图 缓存 限流熔断 Kafka 序列化 参考 1.Java数据库高并发如何解决 java高并发三种解..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-10T14:39:30.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-10T14:39:30.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"高性能高并发高可用的一些思考\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2024-03-10T14:39:30.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"Zephery\\",\\"url\\":\\"https://wenzhihuai.com/article/\\"}]}"]]},"headers":[{"level":2,"title":"异步","slug":"异步","link":"#异步","children":[]},{"level":2,"title":"Jmeter","slug":"jmeter","link":"#jmeter","children":[]},{"level":2,"title":"JProfiler","slug":"jprofiler","link":"#jprofiler","children":[]},{"level":2,"title":"火焰图","slug":"火焰图","link":"#火焰图","children":[]},{"level":2,"title":"缓存","slug":"缓存","link":"#缓存","children":[]},{"level":2,"title":"限流熔断","slug":"限流熔断","link":"#限流熔断","children":[]},{"level":2,"title":"Kafka","slug":"kafka","link":"#kafka","children":[]},{"level":2,"title":"序列化","slug":"序列化","link":"#序列化","children":[]},{"level":2,"title":"参考","slug":"参考","link":"#参考","children":[]}],"git":{"createdTime":1706983034000,"updatedTime":1710081570000,"contributors":[{"name":"zhihuaiwen","email":"zhihuaiwen@tencent.com","commits":1}]},"readingTime":{"minutes":0.36,"words":109},"filePathRelative":"java/高性能高并发高可用的一些思考.md","localizedDate":"2024年2月3日","excerpt":"\\n

TODO待补充

\\n

异步

\\n

Jmeter

","autoDesc":true}');export{s as comp,p as data}; diff --git a/atom.xml b/atom.xml new file mode 100644 index 00000000..c21ad4c6 --- /dev/null +++ b/atom.xml @@ -0,0 +1,7493 @@ + + + http://www.wenzhihuai.com/ + 个人博客 + 个人博客 + http://www.wenzhihuai.com/favicon.ico + 2024-12-02T15:36:33.160Z + @vuepress/plugin-feed + + + + mac通过网线连接主机(fnOS) + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html + + 2024-11-29T18:45:10.000Z + 一、mac端 +

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

+
+

点击详细信息,配置IPv4选择使用DHCP

+
]]>
+ 一、mac端 +

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

+
+

点击详细信息,配置IPv4选择使用DHCP

+
+

二、主机端

+

主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。

+
root@server:~# sudo lshw -C network
+  *-network
+       description: Wireless interface
+       product: Wi-Fi 6 AX210/AX211/AX411 160MHz
+       vendor: Intel Corporation
+       physical id: 0
+       bus info: pci@0000:01:00.0
+       logical name: wlp1s0
+       version: 1a
+       serial: 10:5f:ad:d6:2b:ee
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix bus_master cap_list ethernet physical wireless
+       configuration: broadcast=yes driver=iwlwifi driverversion=6.6.38-trim firmware=72.daa05125.0 ty-a0-gf-a0-72.uc ip=192.168.0.113 latency=0 link=yes multicast=yes wireless=IEEE 802.11
+       resources: irq:18 memory:80900000-80903fff
+  *-network UNCLAIMED
+       description: Ethernet controller
+       product: RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
+       vendor: Realtek Semiconductor Co., Ltd.
+       physical id: 0
+       bus info: pci@0000:02:00.0
+       version: 2b
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix cap_list
+       configuration: latency=0
+       resources: ioport:3000(size=256) memory:80804000-80804fff memory:80800000-80803fff
+

*-network UNCLAIMED 表明你的网络接口未被驱动程序识别和管理。为了解决这个问题,你需要安装或重新加载正确的网络驱动程序。

+

下载并安装 Realtek 网络驱动程序

+

访问 Realtek 官方网站下载适用于你的 RTL8111/8168/8411 网络控制器的驱动程序,通常是 r8168 驱动程序。

+ +
root@server:/vol1# cd /vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00/
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# ls
+autorun.sh  Makefile  README  src
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# sh autorun.sh
+
+Check old driver and unload it.
+Build the module and install
+Warning: modules_install: missing 'System.map' file. Skipping depmod.
+Backup r8169.ko
+rename r8169.ko to r8169.bak
+DEPMOD 6.6.38-trim
+load module r8168
+Updating initramfs. Please wait.
+update-initramfs: Generating /boot/initrd.img-6.6.38-trim
+Completed.
+

安装好了之后,理论上自动显示网口2,但IP什么的都是空的,需要点击编辑,然后按照下面的填一下。

+
+

三、最后

+

最后就可以互相ping通了,很稳定,传输速度很快很快,基本都能1ms以内

+
+]]>
+ 2024-11-29T18:45:10.000Z +
+ + 推荐工程-概述 + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html + + 2024-11-29T18:45:10.000Z + 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

+

平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户来指代。

+

技术的最终目的都是服务业务的,我们来看下当前电商系统的模式差异。

+
]]>
+ 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

+

平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户来指代。

+

技术的最终目的都是服务业务的,我们来看下当前电商系统的模式差异。

+
+

一、淘宝和拼多多的电商模式差异

+

淘宝是流量逻辑,主体是搜索,需要海量SKU满足长尾需求;拼多多代表的是匹配逻辑,推荐商品给消费者,SKC有限,但满足结构性丰富。之前的电商模式都是从淘宝挖一块做小淘宝,而拼多多是在另一个维度做电商,满足不同的场景需求。淘宝的千人千面相当于个性化搜索,但搜索本身是长尾的,你就很难做反向定制。而拼多多集中流量到有限商品里,有了规模后再反向定制。

+

拼多多起来之后,京东、唯品会、蘑菇街都实验过相似模式,对于他们来说,拼团不过是一个创造GMV增量的工具;而拼多多是人的逻辑,我们通过拼团了解人,通过人推荐物,后期会过渡到机器推荐物。拼多多APP里几乎没有搜索,也不设购物车,你可以想像把今日头条下的信息流换成商品流就是拼多多。所以早期看大家都是低价和拼团,但我们的出发点不同、方向不同,长大了也就不一样了。

+

与其说拼多多是社交电商,不如说它是“人以群分”的电商。以前是人去找东西,现在是相似的人聚集起来,迅速产生需求量,这种模式给供应链优化创造了很大的机会。

+

二、搜索、推荐、广告三者的异同

+

搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:

+
    +
  1. 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
  2. +
  3. 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
  4. +
  5. 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
  6. +
  7. 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。
  8. +
+

**广告:**借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

+

三、推荐整体架构

+
+

推荐的框架主要有以下几个模块:

+
    +
  • 协议调度:请求的发送和结果的回传。在请求中,用户会发送自己的 ID,地理位置等信息。结果回传中会返回推荐系统给用户推荐的结果。
  • +
  • 推荐算法:算法按照一定的逻辑为用户产生最终的推荐结果。不同的推荐算法基于不同的逻辑与数据运算过程。
  • +
  • 消息队列:数据的上报与处理。根据用户的 ID,拉取例如用户的性别、之前的点击、收藏等用户信息。而用户在 APP 中产生的新行为,例如新的点击会储存在存储单元里面。
  • +
  • 存储单元:不同的数据类型和用途会储存在不同的存储单元中,例如内容标签与内容的索引存储在 mysql 里,实时性数据存储在 redis 里,需要进行数据统计的数据存储在 TDW 里。
  • +
+

电商系统演变到现在,各类归纳层出不穷,传统零售是“货-场-人”,而新零售是“人-货-场”,强调以人为核心,根据用户的需求推送产品和打造消费场景。在电商线上平台三者具体表现就是:

+

**人:**是指店铺访客、流量。通过数据分析、用户画像、顾客细分等手段,可以更加精准地理解顾客,从而提供个性化的产品和服务
+**货:**是指商家商品。选择合适的商品,有竞争力的价格,保证商品的质量和供应链的高效运营,都是成功的关键
+**场:**由最初的交易平台(如京东、淘宝等)演变到场景(首页、商详推荐页、加购推荐页等),实现千人千面。

+
+

回到我们推荐本身,推荐的本质就是排序,把成千上万的商品更精准的展现到用户面前,提升用户的购买量,从而变现。技术层面,最核心的就是推荐算法,目前市面上对推荐系统基本等于推荐算法,而做为一名后台,在推荐系统中的角色介绍可为少之又少,本系列就用后台的角色去看待整个推荐系统

+

参考

+

1.FunRec

+

2.从零开始了解推荐系统全貌

+

3.从“人、货、场”的不同视角,重新审视电商和新零售

+

4.“人货场”,在产品业务分析中的具体应用

+]]>
+ 2024-11-29T18:45:10.000Z +
+ + 2024-11-09上海迪斯尼 + http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html + + 2024-11-11T16:10:11.000Z + + + 2024-11-10T15:55:36.000Z + + + 买了个mini主机 + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html + + 2024-11-29T18:45:10.000Z + 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

+

成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。

]]>
+ 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

+

成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。

+
iowejofwjeofjwoeifjwoe
iowejofwjeofjwoeifjwoe
+

一、系统的安装

+

系统我看是支持windows,还有现在Ubuntu,但是我这种选择的是centos stream 9, 10的话我也找过,但是发现很多软件还有不兼容。所以最终还是centos stream 9。

+

1、下载Ventoy软件

+

去Ventoy官网下载Ventoy软件(Download . Ventoy)如下图界面

+
QQ_1727625608185
QQ_1727625608185
+

2、制作启动盘

+

选择合适的版本以及平台下载好之后,进行解压,解压出来之后进入文件夹,如下图左边所示,双击打开Ventoy2Disk.exe,会出现下图右边的界面,选择好自己需要制作启动盘的U盘,然后点击安装等待安装成功即可顺利制作成功启动U盘。

+

3、centos安装

+

直接取官网,下载完放到u盘即可。

+
QQ_1727625711792
QQ_1727625711792
+

它的BIOS是按F7启动,直接加载即可。

+
image-20241007222938414
image-20241007222938414
+

之后就是正常的centos安装流程了。

+

二、连接wifi

+

因为是用作服务器的,所以并没有给它配置个专门的显示器,只要换个网络,就连不上新的wifi了,这里可以用网线连接路由器进行下面的操作即可。

+

在 CentOS 系统中,通过命令行连接 Wi-Fi 通常需要使用 nmcli(NetworkManager 命令行工具)来管理网络连接。nmcli 是 NetworkManager 的一个命令行接口,可以用于创建、修改、激活和停用网络连接。以下是如何使用 nmcli 命令行工具连接 Wi-Fi 的详细步骤。

+

步骤 1: 检查网络接口

+

首先,确认你的 Wi-Fi 网络接口是否被检测到,并且 NetworkManager 是否正在运行。

+
nmcli device status
+

输出示例:

+
DEVICE         TYPE      STATE         CONNECTION
+wlp3s0         wifi      disconnected  --
+enp0s25        ethernet  connected     Wired connection 1
+lo             loopback  unmanaged     --
+

在这个示例中,wlp3s0 是 Wi-Fi 接口,它当前处于未连接状态。

+

步骤 2: 启用 Wi-Fi 网卡

+

如果你的 Wi-Fi 网卡是禁用状态,可以通过以下命令启用:

+
nmcli radio wifi on
+

验证 Wi-Fi 是否已启用:

+
nmcli radio
+

步骤 3: 扫描可用的 Wi-Fi 网络

+

使用 nmcli 扫描附近的 Wi-Fi 网络:

+
nmcli device wifi list
+

你将看到可用的 Wi-Fi 网络列表,每个网络都会显示 SSID(网络名称)、安全类型等信息。

+

步骤 4: 连接到 Wi-Fi 网络

+

使用 nmcli 命令连接到指定的 Wi-Fi 网络。例如,如果你的 Wi-Fi 网络名称(SSID)是 MyWiFiNetwork,并且密码是 password123,你可以使用以下命令连接:

+
nmcli device wifi connect 'xxxxxx' password 'xxxxx'
+

你应该会看到类似于以下输出,表明连接成功:

+
Device 'wlp3s0' successfully activated with 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
+

步骤 5: 验证连接状态

+

验证网络连接状态:

+
nmcli connection show
+

查看当前连接的详细信息:

+
nmcli device show wlp3s0
+

上面的其实在2024年其实有点问题,因为会默认连接2.4GHz的wifi,使用的时候很明显没有那么快,特别是在用命令行的时候会觉得明显卡顿,现在需要切换到5GHz的wifi。
+首先,使用 nmcli 获取可用 WiFi 网络及其 BSSID:

+
nmcli -f SSID,BSSID,CHAN dev wifi list
+

示例输出:

+
SSID       BSSID              CHAN
+MyNetwork  XX:XX:XX:XX:XX:01  36
+MyNetwork  XX:XX:XX:XX:XX:02  1
+

在这里, XX:XX:XX:XX:XX:01 是 5GHz 网络的 BSSID。

+

使用 nmcli 连接到特定的 BSSID

+
nmcli dev wifi connect 'XX:XX:XX:XX:XX:01' password 'your_password'
+

三、VNC远程连接

+

桌面还是偶尔需要用一下的,虽然用的不多。

+
root@master:~# dnf install  -y  tigervnc-server
+root@master:~# vncserver
+bash: vncserver: command not found...
+Install package 'tigervnc-server' to provide command 'vncserver'? [N/y] y
+
+
+ * Waiting in queue... 
+ * Loading list of packages.... 
+The following packages have to be installed:
+ dbus-x11-1:1.12.20-8.el9.x86_64        X11-requiring add-ons for D-BUS
+ tigervnc-license-1.14.0-3.el9.noarch   License of TigerVNC suite
+ tigervnc-selinux-1.14.0-3.el9.noarch   SELinux module for TigerVNC
+ tigervnc-server-1.14.0-3.el9.x86_64    A TigerVNC server
+ tigervnc-server-minimal-1.14.0-3.el9.x86_64    A minimal installation of TigerVNC server
+Proceed with changes? [N/y] y
+
+
+ * Waiting in queue... 
+ * Waiting for authentication... 
+ * Waiting in queue... 
+ * Downloading packages... 
+ * Requesting data... 
+ * Testing changes... 
+ * Installing packages... 
+
+WARNING: vncserver has been replaced by a systemd unit and is now considered deprecated and removed in upstream.
+Please read /usr/share/doc/tigervnc/HOWTO.md for more information.
+
+You will require a password to access your desktops.
+
+getpassword error: Inappropriate ioctl for device
+Password:
+

之后在mac开启屏幕共享就可以了

+
image-20241007225855305
image-20241007225855305
+
QQ_1728313164289
QQ_1728313164289
+

四、docker 配置

+

docker安装我以为很简单,没想到这里是最难的一步了。安装完docker之后,总是报错:

+
Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded
+

即使改了mirrors也毫无作用

+
{
+  "registry-mirrors": [
+    "https://ylce84v9.mirror.aliyuncs.com"
+        ]
+}
+

看起来好像是docker每次pull镜像都要访问一次registry-1.docker.io,但是这个网址国内已经无法连接了,各种折腾,这里只贴一下代码吧,原理就就不讲了(懂得都懂)。

+
img
img
+
sslocal -c /etc/shadowsocks.json -d start
+curl --socks5 127.0.0.1:1080 http://httpbin.org/ip
+
+sudo yum -y install privoxy
+

vim /etc/systemd/system/docker.service.d/http-proxy.conf

+
[Service]
+Environment="HTTP_PROXY=http://127.0.0.1:8118"
+

/etc/systemd/system/docker.service.d/https-proxy.conf

+
[Service]
+Environment="HTTPS_PROXY=http://127.0.0.1:8118"
+

最后重启docker

+
systemctl start privoxy
+systemctl enable privoxy
+sudo systemctl daemon-reload
+sudo systemctl restart docker
+
QQ_1729956484197
QQ_1729956484197
+

五、文件共享

+

sd卡好像读取不了,只能换个usb转换器

+
fdisk -l
+mount /dev/sdb1 /mnt/usb/sd
+

在CentOS中设置文件共享,可以使用Samba服务。以下是配置Samba以共享文件的基本步骤:

+
    +
  1. 安装Samba
  2. +
+
sudo yum install samba samba-client samba-common
+
    +
  1. +

    设置共享目录

    +

    编辑Samba配置文件/etc/samba/smb.conf,在文件末尾添加以下内容:

    +
  2. +
+
[shared]
+   path = /path/to/shared/directory
+   writable = yes
+   browseable = yes
+   guest ok = yes
+
    +
  1. +

    设置Samba密码

    +

    为了允许访问,需要为用户设置一个Samba密码:

    +
  2. +
+
sudo smbpasswd -a your_username
+
    +
  1. 重启Samba服务
  2. +
+
sudo systemctl restart smb.service
+sudo systemctl restart nmb.service
+
    +
  1. +

    配置防火墙(如果已启用)

    +

    允许Samba通过防火墙:

    +
  2. +
+
sudo firewall-cmd --permanent --zone=public --add-service=samba
+sudo firewall-cmd --reload
+

现在,您应该能够从网络上的其他计算机通过SMB/CIFS访问共享。在Windows中,你可以使用\\centos-ip\shared,在Linux中,你可以使用smbclient //centos-ip/shared -U your_username

+
QQ_1730035390803
QQ_1730035390803
+

参考:

+

https://shadowsockshelp.github.io/Shadowsocks/linux.html

+

https://stackoverflow.com/questions/48056365/error-get-https-registry-1-docker-io-v2-net-http-request-canceled-while-b

+]]>
+ 2024-10-27T13:33:05.000Z +
+ + 流程编排LiteFlow + http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html + + 2024-11-07T16:03:32.000Z + LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

+

另外, LiteFlow 和 Activiti 们并不是同一个东西,而是面向不同的使用场景和需求。LiteFlow 更加轻量灵活,适合需要简单流程管理和动态配置的场景;而 Activiti 则是一个全面的 BPM 引擎,适合需要复杂业务流程管理和任务管理的场景。根据具体业务需求,可以选择合适的工具来实现流程编排。

]]>
+ LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

+

另外, LiteFlow 和 Activiti 们并不是同一个东西,而是面向不同的使用场景和需求。LiteFlow 更加轻量灵活,适合需要简单流程管理和动态配置的场景;而 Activiti 则是一个全面的 BPM 引擎,适合需要复杂业务流程管理和任务管理的场景。根据具体业务需求,可以选择合适的工具来实现流程编排。

+

背景

+

之前做过一个数据分发系统,需要消费kafka的数据,下游有不同的业务,每个业务可能有共同的地方,也有不同的地方,在经过各类的处理之后,最后数据分发到下游里面去。为了简化代码方便理解,我们定义4个Handler(A、B、C、D),然后有3个不同的业务,需要经过不同的Handler,整个流程如下。

+image-20241108000137572 +

如果要在一个代码实现上诉功能,我们第一反应可能是责任链设计模式,每个业务一条链路,在Spring中,类似下面的代码:

+
public abstract class Handler {
+    abstract void handler(Request request);
+}
+
+@Component
+@Slf4j
+public class HandlerA extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器1");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerB extends Handler {
+    @Override
+    public void handler(Request request) {
+        log.info("处理器2");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerC extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器3");
+    }
+}
+@Component
+@Slf4j
+public class HandlerD extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器4");
+    }
+}
+
+//然后我们定义一个枚举类,用来配置不同业务需要经历过的处理器。
+public enum HandleBuz {
+    Business_1(HandlerA,HandlerB),
+    Business_2(HandlerB,HandlerC),
+    Business_3(HandlerA,HandlerD);
+    public final Class<? extends Handler>[] processors;
+    public HandleBuz(Class<? extends Handler>[] processors){
+        this.processors=processors;
+    }    
+    public void handle(){
+        for (Handler handler : processors) {
+            handler.handler(xxx);
+        }
+    }
+
+}
+

通过配置责任链,可以灵活地组合处理对象,实现不同的处理流程,并且可以在运行时动态地改变处理的顺序,由于责任链模式遵循开闭原则,新的处理者可以随时被加入到责任链中,不需要修改已有代码,提供了良好的扩展性。但实际上面对各种需求的时候,没法做到完全的解耦,比如对于HandlerA,如果业务1和业务2都有定制化的需求(来自产品提的临时或长期需求),此时是应该再HandlerA中用if else解决,还是再额外开个HandlerA_1和HandlerA_2。这类特性需求会非常多,最终把代码可读性变得越来越低。

+

一、为什么需要流程编排

+

LiteFlow由Baidu开源,专注于逻辑驱动流程编排,通过组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑。它以其轻量级、快速、稳定且可编排的特性,在业务流程管理、规则引擎、工作流、订单处理、数据处理、微服务编排以及智能化流程管理等领域都有广泛的应用前景。

+
img
img
+

二、它可以解决什么问题

+

对大部分不断迭代的代码来说,历史遗留的代码加上需要面对各类各样的需求,代码会变得越来越难维护,甚至变成屎山。我们想着不断的去进行解耦,不断的去进行切割拆分,还要兼顾新需求,就怕蝴蝶效应导致大故障,liteflow能帮我们在解耦上更加清晰一点。
+(1)复杂业务流程编排和管理
+在一些应用场景中,业务逻辑往往非常复杂,涉及多个步骤的执行,并且这些步骤之间具有复杂的依赖关系。LiteFlow 可以帮助开发者通过配置和代码相结合的方式定义和管理这些流程。
+(2)流程动态配置
+LiteFlow 允许通过配置文件或者数据库动态修改流程,而无需修改代码。这意味着可以根据不同的业务需求快速调整并发布新的流程,而不需要重新部署应用。
+(3)流程节点的复用和解耦
+在使用 LiteFlow 时,每个业务步骤都可以定义为一个独立的节点(Node),这些节点可以独立开发、测试和维护,并且可以在多个流程中复用。通过这种方式,可以实现业务逻辑的复用和解耦,提高代码的可维护性。
+(4)节点状态和错误处理
+LiteFlow 提供了丰富的节点状态管理和错误处理机制,允许开发者在流程执行过程中捕获和处理异常,从而确保系统的稳定性和健壮性。
+(5) 高扩展性和自定义能力
+LiteFlow 具有高度的扩展性,开发者可以根据自身业务的特殊需求定制节点、组件和插件,从而满足复杂场景的要求。

+

以下是一些实际使用 LiteFlow 的示例场景:
+(1)订单处理系统:在电商系统中,订单处理涉及多个步骤,如库存检查、支付处理、订单确认和发货等。LiteFlow 可以帮助将这些步骤分开独立实现,然后通过流程引擎编排执行。
+(2)审批流程:在企业中,审批流程通常包括多个节点(如申请、审批、复核、归档等),并且这些节点之间可能有条件和依赖关系。LiteFlow 可以帮助动态配置和管理这些流程,提高审批效率。
+(3)营销活动:在一些营销活动中,不同的活动环节和逻辑可能会因用户行为和外部条件而变化。LiteFlow 可以帮助实现灵活的活动规则配置和执行。

+

三、LiteFlow改造之后

+

首先定义并实现一些组件,确保SpringBoot会扫描到这些组件并注册进上下文。

+
@Slf4j
+@LiteflowComponent("a")
+public class HandlerA extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("b")
+public class HandlerB extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("c")
+public class HandlerC extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("d")
+public class HandlerD extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+

同时,你得在resources下的config/flow.el.xml中定义规则:

+
<?xml version="1.0" encoding="UTF-8"?>
+<flow>
+    <chain name="chain1">
+        THEN(
+        a,b
+        );
+    </chain>
+    <chain name="chain2">
+        THEN(
+        b,c
+        );
+    </chain>
+    <chain name="chain3">
+        THEN(
+        a,d
+        );
+    </chain>
+</flow>
+

最后,在消费kafka的时候,先定义一个ruleChainMap,用来判断根据唯一的id(业务id或者消息id)来判断走哪条chain、哪个组件等,甚至可以定义方法级别的组件。

+
    private Map<Integer, String> ruleChainMap = new HashMap<>();
+    @Resource
+    private FlowExecutor flowExecutor;
+
+    @PostConstruct
+    private void init() {
+        ruleChainMap.put(1, "业务1");
+        ruleChainMap.put(2, "业务2");
+        ruleChainMap.put(3, "业务3");
+    }
+
+    @KafkaListener(topics = "xxxx")
+    public void common(List<ConsumerRecord<String, String>> records) {
+        for (ConsumerRecord<String, String> record : records) {
+            ...
+            String chainName = ruleChainMap.get("唯一id(可以是record里的,也可以全局定义的id)");
+            LiteflowResponse response = flowExecutor.execute2Resp(chainName, xxx, xxx, new TempContext());
+        }
+    }
+

由于篇幅的关系,这里不再讲解怎么传递上下文的关系,可以自己去官网研究一下。另外,上面的例子因为是简化之后的,如果你觉得不够形象,可以看看下面的实际业务。这个如果不使用liteflow,可能就得在主流程代码里增加各种if else,甚至后续改了一小块也不知道对别的地方有没有影响。

+
image-20241108000231371
image-20241108000231371
+

总结

+

后续,如果面对产品经理“来自大领导的一个想法,我不知道后续还会不会一直做下去,反正先做了再说”这类需求,就可以自己定义一个LiteFlow的组件,既不污染主流程的代码,后续下线了删掉即可,赏心悦目。

+

文档&参考

+

1.【腾讯文档】业务处理复杂 https://docs.qq.com/flowchart/DZVFURmhCb0JFUHFD
+2.【腾讯文档】业务处理复杂2 https://docs.qq.com/flowchart/DZXVOaUV5VGRtc3ZD
+3.一文搞懂设计模式—责任链模式
+4.LiteFlow官网

+]]>
+ 2024-09-08T16:55:53.000Z +
+ + 推荐系统入门 + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html + + 2024-09-01T13:54:44.000Z + 转载自:https://www.cnblogs.com/cgli/p/17225189.html

+

一、背景

+

近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不数据规模。数据级量小,业务场景也简单,实现起比较的容易,但原理基本上大同小异。

]]>
+ 转载自:https://www.cnblogs.com/cgli/p/17225189.html

+

一、背景

+

近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不数据规模。数据级量小,业务场景也简单,实现起比较的容易,但原理基本上大同小异。

+

本文尝试从最简单的推荐入手,我们暂不去讨论大规模数据分析和算法。更多从软件工程角度思考问题,那些高大上算法留给读者去思考。开源获取:http://www.jnpfsoft.com/?from=infoq

+

下面就来谈谈整个推荐的设计实现过程。

+

二、推荐系统介绍

+

2.1、为什么需要推荐系统?

+

首先推荐系统作用是很大的。推荐系统在很多业务场景有广泛使用和发挥的空间,它的应用和影子无处不在,在多媒体内容、广告平台和电商平台尤为常见。大多平台商或企业都是基于大数据算法分析做推荐系统。推荐算法也是层出不穷,比如相似度计算、近邻推荐、概率矩阵分解、概率图等,当然也有综合多种算法融合使用。

+

互联网时代,数据呈爆炸式增长,前所未有的数据量远远超过受众的接收和处理能力,因此,从海量复杂数据中有效获取关键性有用信息成为必须解决的问题。面对信息过载问题,人们迫切需要一种高效的信息过滤系统,“推荐系统”应运而生。

+

20 世纪 90 年代以来,尽管推荐系统在理论、方法和应用方面取得了系列重要进展,但数据的稀疏性与长尾性、用户行为模式挖掘、可解释性、社会化推荐等问题仍然是其面临的重要挑战。

+

进一步地,伴随互联网及信息技术的持续飞速发展,用户规模与项目数量急剧增长,相应地,用户行为数据的稀疏性、长尾性问题更加凸显。也就是说目前各大平台虽然已经推荐系统,但是实际应用当中还是面临很多问题,仍然有很大的提升空间。这是技术挑战也机会,当然这也是我们这些从业者可以发挥的地方。

+

2.2、推荐系统解决什么问题?

+

推荐系统从 20 世纪 90 年代就被提出来了,但是真正进入大众视野以及在各大互联网公司中流行起来,还是最近几年的事情。

+

随着移动互联网的发展,越来越多的信息开始在互联网上传播,产生了严重的信息过载。因此,如何从众多信息中找到用户感兴趣的信息,这个便是推荐系统的价值。精准推荐解决了用户痛点,提升了用户体验,最终便能留住用户。

+

推荐系统本质上就是一个信息过滤系统,通常分为:召回、排序、重排序这 3 个环节,每个环节逐层过滤,最终从海量的物料库中筛选出几十个用户可能感兴趣的物品推荐给用户。

+

推荐系统的分阶段过滤流程如下图所示:

+
img
img
+

2.3、推荐系统应用场景

+

哪里有海量信息,哪里就有推荐系统,我们每天最常用的 APP 都涉及到推荐功能:

+

资讯类:今日头条、腾讯新闻等

+

电商类:淘宝、京东、拼多多、亚马逊等

+

娱乐类:抖音、快手、爱奇艺等

+

生活服务类:美团、大众点评、携程等

+

社交类:微信、陌陌、脉脉等

+
img
img
+

实际例子还有很多,稍微上一点规模的平台或 APP 都有这一个推荐模块。

+

推荐系统的应用场景通常分为以下两类:

+

基于用户维度的推荐:根据用户的历史行为和兴趣进行推荐,比如淘宝首页的猜你喜欢、抖音的首页推荐等。

+

基于物品维度的推荐:根据用户当前浏览的标的物进行推荐,比如打开京东 APP 的商品详情页,会推荐和主商品相关的商品给你。

+

2.4、搜索、推荐、广告三者的异同

+

搜索和推荐是 AI 算法最常见的两个应用场景,在技术上有相通的地方。

+

搜索:有明确的搜索意图,搜索出来的结果和用户的搜索词相关。推荐:不具有目的性,依赖用户的历史行为和画像数据进行个性化推荐。广告:借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

+

三、推荐系统通用框架

+

推荐系统涉及周边和自己模块还是比较多,这里主要从最简单推荐系统自身功能去构思设计简单结构。

+
img
img
+

上面这个图基本把推荐处理过程画出来,结构比较清晰,看图理想即可。

+

从分层架构设计视角来说可以分成多层架构形式

+

分层:排序层、过滤层、召回层、数据存储层、计算平台、数据源。

+

可以说市面上推荐系统设计都是差不多是这个样子,只是里面使用技术或组件不同而已。

+
img
img
+

上面是推荐系统的整体架构图,自下而上分成了多层,各层的主要作用如下:

+
    +
  • +

    数据源:推荐算法所依赖的各种数据源,包括物品数据、用户数据、行为日志、其他可利用的业务数据、甚至公司外部的数据。

    +
  • +
  • +

    计算平台:负责对底层的各种异构数据进行清洗、加工,离线计算和实时计算。

    +
  • +
  • +

    数据存储层:存储计算平台处理后的数据,根据需要可落地到不同的存储系统中,比如 Redis 中可以存储用户特征和用户画像数据,ES 中可以用来索引物品数据,Faiss 中可以存储用户或者物品的 embedding 向量等。

    +
  • +
  • +

    召回层:包括各种推荐策略或者算法,比如经典的协同过滤,基于内容的召回,基于向量的召回,用于托底的热门推荐等。为了应对线上高并发的流量,召回结果通常会预计算好,建立好倒排索引后存入缓存中。

    +
  • +
  • +

    融合过滤层:触发多路召回,由于召回层的每个召回源都会返回一个候选集,因此这一层需要进行融合和过滤。

    +
  • +
  • +

    排序层:利用机器学习或者深度学习模型,以及更丰富的特征进行重排序,筛选出更小、更精准的推荐集合返回给上层业务。从数据存储层到召回层、再到融合过滤层和排序层,候选集逐层减少,但是精准性要求越来越高,因此也带来了计算复杂度的逐层增加,这个便是推荐系统的最大挑战。

    +
  • +
+

其实对于推荐引擎来说,最核心的部分主要是两块:特征和算法。

+
img
img
+

这些工具和技术框架都是比较成熟稳定的,是众多厂商在实际业务场景中选择应用的。所以也没有太多特殊的地方。

+

特征计算由于数据量大,通常采用大数据的离线和实时处理技术,像 Spark、Flink 等,然后将计算结果保存在 Redis 或者其他存储系统中(比如 HBase、MongoDB 或者 ES),供召回和排序模块使用。

+

召回算法的作用是:从海量数据中快速获取一批候选数据,要求是快和尽可能的准。这一层通常有丰富的策略和算法,用来确保多样性,为了更好的推荐效果,某些算法也会做成近实时的。

+

排序算法的作用是:对多路召回的候选集进行精细化排序。它会利用物品、用户以及它们之间的交叉特征,然后通过复杂的机器学习或者深度学习模型进行打分排序,这一层的特点是计算复杂但是结果更精准。

+

四、经典算法

+

了解了推荐系统的整体架构和技术方案后,下面带大家深入一下算法细节。这里选择图解的是推荐系统中的明星算法:协同过滤(Collaborative Filtering,CF)。

+

对于很多同学来说,可能觉得 AI 算法晦涩难懂,门槛太高,确实很多深度学习算法的确是这样,但是协同过滤却是一个简单同时效果很好的算法,只要你有初中数学的基础就能看懂。

+

4.1、协同过滤是什么?

+

协同过滤算法的核心就是「找相似」,它基于用户的历史行为(浏览、收藏、评论等),去发现用户对物品的喜好,并对喜好进行度量和打分,最终筛选出推荐集合。它又包括两个分支:

+
    +
  • 基于用户的协同过滤:User-CF,核心是找相似的人。比如下图中,用户 A 和用户 C 都购买过物品 a 和物品 b,那么可以认为 A 和 C 是相似的,因为他们共同喜欢的物品多。这样,就可以将用户 A 购买过的物品 d 推荐给用户 C。
  • +
+
img
img
+

基于用户的协同过滤示例

+
    +
  • 基于物品的协同过滤:Item-CF,核心是找相似的物品。比如下图中,物品 a 和物品 b 同时被用户 A,B,C 购买了,那么物品 a 和物品 b 被认为是相似的,因为它们的共现次数很高。这样,如果用户 D 购买了物品 a,则可以将和物品 a 最相似的物品 b 推荐给用户 D。
  • +
+
img
img
+

4.2、如何找相似?

+

协同过滤的核心就是找相似,User-CF 是找用户之间的相似,Item-CF 是找物品之间的相似,那到底如何衡量两个用户或者物品之间的相似性呢?

+

我们都知道,对于坐标中的两个点,如果它们之间的夹角越小,这两个点越相似。

+

这就是初中学过的余弦距离,它的计算公式如下:

+
img
img
+

举个例子,A 坐标是(0,3,1),B 坐标是(4,3,0),那么这两个点的余弦距离是 0.569,余弦距离越接近 1,表示它们越相似。

+
img
img
+

除了余弦距离,衡量相似性的方法还有很多种,比如:欧式距离、皮尔逊相关系数、Jaccard 相似系数等等,这里不做展开,只是计算公式上的差异而已。

+

4.3、Item-CF 的算法流程

+

清楚了相似性的定义后,下面以 Item-CF 为例,详细说下这个算法到底是如何选出推荐物品的?

+

4.3.1 、整理物品的共现矩阵

+

假设有 A、B、C、D、E 5 个用户,其中用户 A 喜欢物品 a、b、c,用户 B 喜欢物品 a、b 等等。

+
img
img
+

所谓共现,即:两个物品被同一个用户喜欢了。比如物品 a 和 b,由于他们同时被用户 A、B、C 喜欢,所以 a 和 b 的共现次数是 3,采用这种统计方法就可以快速构建出共现矩阵。

+

4.3.2、 计算物品的相似度矩阵

+

对于 Item-CF 算法来说,一般不采用前面提到的余弦距离来衡量物品的相似度,而是采用下面的公式:

+
img
img
+

其中,N(u) 表示喜欢物品 u 的用户数,N(v) 表示喜欢物品 v 的用户数,两者的交集表示同时喜欢物品 u 和物品 v 的用户数。

+

很显然,如果两个物品同时被很多人喜欢,那么这两个物品越相似。

+

基于第 1 步计算出来的共现矩阵以及每个物品的喜欢人数,便可以构造出物品的相似度矩阵:

+
img
img
+

4.3.2、 推荐物品

+

最后一步,便可以基于相似度矩阵推荐物品了,公式如下:

+
img
img
+

其中,Puj 表示用户 u 对物品 j 的感兴趣程度,值越大,越值得被推荐。

+

N(u) 表示用户 u 感兴趣的物品集合,S(j,N) 表示和物品 j 最相似的前 N 个物品,Wij 表示物品 i 和物品 j 的相似度,Rui 表示用户 u 对物品 i 的兴趣度。

+

上面的公式有点抽象,直接看例子更容易理解,假设我要给用户 E 推荐物品,前面我们已经知道用户 E 喜欢物品 b 和物品 c,喜欢程度假设分别为 0.6 和 0.4。

+

那么,利用上面的公式计算出来的推荐结果如下:

+
img
img
+

因为物品 b 和物品 c 已经被用户 E 喜欢过了,所以不再重复推荐。最终对比用户 E 对物品 a 和物品 d 的感兴趣程度,因为 0.682 > 0.3,因此选择推荐物品 a。

+

五、如何实现推荐系统

+

5.1、选择数据集

+

这里采用的是推荐领域非常经典的 MovieLens 数据集,它是一个关于电影评分的数据集,官网上提供了多个不同大小的版本,下面以 ml-1m 数据集(大约 100 万条用户评分记录)为例。

+

下载解压后,文件夹中包含:ratings.dat、movies.dat、users.dat 3 个文件,共 6040 个用户,3900 部电影,1000209 条评分记录。各个文件的格式都是一样的,每行表示一条记录,字段之间采用 :: 进行分割。

+

以 ratings.dat 为例,每一行包括 4 个属性:UserID, MovieID, Rating, Timestamp。

+

通过脚本可以统计出不同评分的人数分布:

+
img
img
+

5.2、读取原始数据

+

程序主要使用数据集中的 ratings.dat 这个文件,通过解析该文件,抽取出 user_id、movie_id、rating 3 个字段,最终构造出算法依赖的数据,并保存在变量 dataset 中,它的格式为:dict[user_id][movie_id] = rate

+

5.3、构造物品的相似度矩阵

+

基于第 2 步的 dataset,可以进一步统计出每部电影的评分次数以及电影的共生矩阵,然后再生成相似度矩阵。

+

5.4、基于相似度矩阵推荐物品

+

最后,可以基于相似度矩阵进行推荐了,输入一个用户 id,先针对该用户评分过的电影,依次选出 top 10 最相似的电影,然后加权求和后计算出每个候选电影的最终评分,最后再选择得分前 5 的电影进行推荐。

+

5.5、调用推荐系统

+

下面选择 UserId=1 这个用户,看下程序的执行结果。由于推荐程序输出的是 movieId 列表,为了更直观的了解推荐结果,这里转换成电影的标题进行输出。

+

Java 代码示例

+
import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+public class CFRecommendation {
+    // 使用MovieLens数据集
+    private static final String RATINGS_FILE = "ratings.csv";
+    // 用户ID-电影ID-打分
+    private static Map<Integer, Map<Integer, Double>> ratings;
+
+    // 加载ratings.csv文件
+    private static void loadRatings() throws IOException {
+        File file = new File(RATINGS_FILE);
+        Scanner scanner = new Scanner(file);
+        ratings = new HashMap<>();
+        while (scanner.hasNextLine()) {
+            String line = scanner.nextLine();
+            String[] data = line.split(",");
+            int userId = Integer.parseInt(data[0]);
+            int movieId = Integer.parseInt(data[1]);
+            double rating = Double.parseDouble(data[2]);
+            // 用户-电影-打分
+            Map<Integer, Double> movieRatings = ratings.get(userId);
+            if (movieRatings == null) {
+                movieRatings = new HashMap<>();
+                ratings.put(userId, movieRatings);
+            }
+            movieRatings.put(movieId, rating);
+        }
+    }
+
+    // 计算两个用户的相似度
+    private static double calculateSimilarity(int user1, int user2) {
+        Map<Integer, Double> rating1 = ratings.get(user1);
+        Map<Integer, Double> rating2 = ratings.get(user2);
+        if (rating1 == null || rating2 == null) {
+            return 0;
+        }
+        double sum1 = 0;
+        double sum2 = 0;
+        double sumProduct = 0;
+        for (int movieId : rating1.keySet()) {
+            if (rating2.containsKey(movieId)) {
+                double rating1Value = rating1.get(movieId);
+                double rating2Value = rating2.get(movieId);
+                sum1 += rating1Value * rating1Value;
+                sum2 += rating2Value * rating2Value;
+                sumProduct += rating1Value * rating2Value;
+            }
+        }
+        return sumProduct / (Math.sqrt(sum1) * Math.sqrt(sum2));
+    }
+    
+    // 计算余弦相似度
+    public static double cosineSim(Map<String, Integer> user1, Map<String, Integer> user2){
+        double result = 0;
+        double denominator1 = 0;
+        double denominator2 = 0;
+        double numerator = 0;
+        for(String key : user1.keySet()){
+            numerator += user1.get(key) * user2.get(key);
+            denominator1 += Math.pow(user1.get(key), 2);
+            denominator2 += Math.pow(user2.get(key), 2);
+        }
+        result = numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2));
+        return result;
+    }
+
+    // 使用协同过滤算法获取用户的推荐列表
+    private static Map<Integer, Double> recommend(int userId) {
+        Map<Integer, Double> recommendList = new HashMap<>();
+        // 遍历所有用户
+        for (int otherUserId : ratings.keySet()) {
+            if (otherUserId != userId) {
+                double similarity = calculateSimilarity(userId, otherUserId);
+                Map<Integer, Double> otherRating = ratings.get(otherUserId);
+                // 遍历其他用户的评分,如果当前用户没有评分,则将其推荐给当前用户
+                for (int movieId : otherRating.keySet()) {
+                    if (!ratings.get(userId).containsKey(movieId)) {
+                        double recommendScore = otherRating.get(movieId) * similarity;
+                        recommendList.put(movieId, recommendScore);
+                    }
+                }
+            }
+        }
+        return recommendList;
+    }
+
+    public static void main(String[] args) throws IOException {
+        loadRatings();
+        // 测试用例:计算用户1与用户2的相似度
+        int user1 = 1;
+        int user2 = 2;
+        double similarity = calculateSimilarity(user1, user2);
+        System.out.println("用户1与用户2的相似度:" + similarity);
+        // 测试用例:为用户1推荐电影
+        int userId = 1;
+        Map<Integer, Double> recommendList = recommend(userId);
+        System.out.println("为用户1推荐的电影:");
+        for (int movieId : recommendList.keySet()) {
+            System.out.println("电影ID:" + movieId + ",推荐分数:" + recommendList.get(movieId));
+        }
+    }
+}
+

六、问题与展望

+

通过上面的介绍,大家对推荐系统的基本构成应该有了一个初步认识,但是真正运用到线上真实环境时,还会遇到很多算法和工程上的挑战,绝对不是几十行代码可以搞定的。

+

问题:

+
    +
  1. +

    上面的示例使用了标准化的数据集,而线上环境的数据是非标准化的,因此涉及到海量数据的收集、清洗和加工,最终构造出模型可使用的数据集。

    +

    复杂且繁琐的特征工程,都说算法模型的上限由数据和特征决定。对于线上环境,需要从业务角度选择出可用的特征,然后对数据进行清洗、标准化、归一化、离散化,并通过实验效果进一步验证特征的有效性。

    +

    算法复杂度如何降低?比如上面介绍的 Item-CF 算法,时间和空间复杂度都是 O(N×N),而线上环境的数据都是千万甚至上亿级别的,如果不做算法优化,可能几天都跑不出数据,或者内存中根本放不下如此大的矩阵数据。

    +

    实时性如何满足?因为用户的兴趣随着他们最新的行为在实时变化的,如果模型只是基于历史数据进行推荐,可能结果不够精准。因此,如何满足实时性要求,以及对于新加入的物品或者用户该如何推荐,都是要解决的问题。

    +

    算法效果和性能的权衡。从算法角度追求多样性和准确性,从工程角度追求性能,这两者之间必须找到一个平衡点。

    +

    推荐系统的稳定性和效果追踪。需要有一套完善的数据监控和应用监控体系,同时有 ABTest 平台进行灰度实验,进行效果对比。

    +
  2. +
+

展望:

+

AI 时代,算法会更加复杂和完善,推荐的效果也会越来越好,特别是随着 OpenAI ChatGPT 横空出现,推荐系统最有条件和最适合 GPT 模型去结合使用,当然也会更加高效和智能。期待我们智能版推荐系统早日面世。

+]]>
+ 2024-09-01T13:54:44.000Z +
+ + 计算广告基本概念入门总结 + http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html + + 2024-09-01T13:54:44.000Z + 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

+

一、计算广告入门
+1.1、什么是计算广告
+首先要理解广告的概念,广告指的是由已确定的出资人通过各类媒介进行有关产品、观点和服务的、通常是有偿的、有组织的、综合的、劝服性的非人员信息传播活动。

]]>
+ 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

+

一、计算广告入门
+1.1、什么是计算广告
+首先要理解广告的概念,广告指的是由已确定的出资人通过各类媒介进行有关产品、观点和服务的、通常是有偿的、有组织的、综合的、劝服性的非人员信息传播活动。

+

广告的主体包括了三类:

+

广告主-有发布广告需求的用户
+广告媒介-也就是承接广告主需求以及调配广告展示的广告平台
+受众-通过媒介看到广告。
+广告活动的两个主动参与方—出资人(sponsor) 和媒体(medium) 。 在数字广告这样更加复杂的市场结构中, 我们可以用一般性的术语来描述它们: 需求方(demand) 和供给方(supply) 。 这里的需求方可以是广告主 (advertiser) 、 代表广告主利益的代理商(agency) 或其他技术形态的采买方; 这里的供给方可以是媒体, 也可以是其他技术形态的变现平台。 另外, 要特别注意的是, 广告还有一个被动的参与方, 即受众 (audience) 。

+

广告的根本目的是,通过广泛的媒体力量,使得广告能够高效、低成本地触达用户,以保持产品或服务的品牌形象或提高短期销量(也就是品牌广告和效果广告)。

+

计算广告并没有严格的定义, 个人理解计算广告指的是通过大数据、推荐算法等技术对大量数据进行分析,以达到广告的精确受众定向,并且通过计算来调控广告的整个生命周期。计算广告相较于传统广告的特点是以技术和计算为导向,以数据为基准,具备标准化的、可衡量性的广告效果指标。

+

计算广告的核心问题是如何为一系列用户与环境组合,以找到最合适的广告投放策略以优化整体的投入产出比(ROI)。

+

1.2、广告与营销的区别
+人们通常将广告理解为营销,但是两者是有一定的区别的。

+

首先广告和营销的共同点都是为了提高产品或服务的销量、营业额及利润,但是区别在于,广告更希望通过低成本触达到用户,这里的用户指的是潜在用户,而营销更希望的是对某类精准用户进行服务,可以理解为广告的目标是增量用户,潜在用户,看着品牌价值和长期利润,而营销更看重目标用户和短期效果。

+

1.3、在线广告的类型
+条幅广告(Banner Ad): 这是显示广告中中最传统,也是最典型的形式。这种广告一般是嵌入在页面中相对固定位置的图片,与内容一样需要占据固定的版面。
+文字链广告(Textual Ad): 这种广告的素材形式是一段链接到广告主落地页的文字,在搜索广告中为主流形式,同时在媒体广告中也被广泛采用。
+富媒体广告(Rich Media Ad): 这类广告往往是利用视觉冲击力较强的表现形式,在不占用固定版面位置的情况下,向用户侵入式地投送广告素材。
+富媒体广告常见的形式有弹窗、对 联、全屏等。它比较适合在高质量的媒体做一些品牌性质比较强的广告投放,但是对用户的使用体验往往影响也较大。
+一些门户网站的首页有时会为某个品牌广告主提供专门定制的、交互形式很复杂的富媒体广告, 这样的广告一般不采用受众定向的投送方式,也主要强调创意的冲击力和交互形式的特色。
+视频广告(Video Ad): 视频广告有两种最主要的形式:在视频内容播放之前的前插片广告,以及视频播放暂停时的广告。前插片广告一般采用短视频的形式,创意的冲击力和表现力要远远强于普通的显示广告,因此CPM价格往往也比较高;暂停广告则与普通的条幅广告区别不大。
+社交广告或信息流广告(Social Ad): 社交广告中最典型的形式,是插入在社交网络信息流中的广告,这种方式最早见于Twitter,产品称为“Promoted Tweets”。社交广告希望达到的效果,是通过用户的扩散式传播获 得更大的影响力,以及更可信的口碑.
+移动设备广告(Mobile Ad): 严格来说, 移动互联网上的广告与桌面电脑上的广告没有本质区别.
+邮件营销广告(Email Direct Marketing, EDM): EDM是一种主动的广告形式,它不需要等到用户接触的机会产生时才被动地提供广告,而是可以随时向认为合适的用户发送推广信息。不过也正因为如此,EDM非常容易变垃圾邮件的主要来源。
+1.4、在线广告发展历程

+
在这里插入图片描述
在这里插入图片描述
+

阶段一:传统合约广告模式,花钱买一段时间的广告位或展示次数
+阶段二:定向及合约广告阶段,签约形式为合约广告,广告采用了特定用户定向的方式进行展示,提升转化率
+阶段三:定向及竞价广告阶段,签约形式为竞价广告,广告采用了特定用户定向的方式进行展示,提升转化率
+阶段四:实时竞价模式阶段(重要里程碑),采用广告交易平台(ADX)、需求方平台(DSP)及供给方平台(SSP)完成广告竞价,出现广义第二高价
+阶段五:程序化交易,买卖双方无需在线交易,完全由程序进行交易的调控,无需人工参与
+1.5、广告有效性模型

+
在这里插入图片描述
在这里插入图片描述
+

广告效果生成过程有三个大阶段:选择(Selection)、解释(Interpretation)与态度(Attitude);或者进一步分解为六个小阶段:曝光、关注、理解、接受、保持与决策,其中每两个小阶段对应一个大阶段。

+

定性地说,越靠前的阶段,其效果的改善对点击率的贡献越大;而越靠后的阶段,其效果的改善对转化率的贡献越大。

+

曝光(Exposure)阶段: 这一阶段指的是广告物理上展现出来的过程。此阶段的有效性往往与广告位的物理属性有关,并没有太多可以通过技术优化的空间。实际的广告实践中,曝光的有效性对最终结果的影响往往远远高于其他技术性因素。
+关注(Attention)阶段:这一阶段指的是受众从物理上接触到广告到意识上注意到它的过程。那么如何使得关注阶段的效率提高呢?我们介绍几个重要的原则:
+(1)尽量不要打断用户的任务。
+(2)明确传达向用户推送此广告的原因,这一点是受众定向广告创意优化的重要方向。
+(3)内容符合用户的兴趣或需求,这是受众定向的原理基础。
+理解(Comprehension)阶段:受众意识到了广告的存在,并不意味着他一定能够理解广告传达的信息。
+(1)广告内容要在用户能理解的具体兴趣范围内,这就说明了真正精准的受众定向有多么必要。
+(2)要注意设定与关注程度相匹配的理解门槛。
+接受(Acceptance)阶段:受众理解了广告传达的信息,并不一定表示他认可这些信息。
+(1)广告的上下文环境对于广告的接受程度也有着很大的影响, 同一个品牌广告出现在某游戏社区上和门户网站首页上,用户会倾向于认为后者更具说服力,这也就是优质媒体的品牌价值。
+(2)在定向广告越来越普遍的今天,如何让合适的广告出现在合适的媒体上,即广告安全(Ad Safety)的问题,正在引起大家越来越多的关注。
+保持(Retention)阶段:对于不仅仅追求短期转化的广告商,当然希望广告传达的信息给用户留下长久的记忆,以影响他长时间的选择。
+决策(Decision)阶段:成功广告的最终作用是带来用户的转化行为,虽然这一阶段已经离开了广告的业务范围,但好的广告还是能够为转化率的提高做好铺垫。
+二、计算广告的核心概念
+品牌广告(Brand Awareness)
+长期投放,用于建立品牌形象、建立品牌理念,而不诉求直接销售量的广告

+

直接效果广告/效果广告(Direct Response)
+短期投放,为了提升产品销售量或提高知名度而投放的广告,因此通常按效果计费(CPA或CPS)

+

展示广告(display advertising)
+以图形展示为核心的广告形式,如常见的banner、视频广告等,通常以CPM、CPT为计费方式

+

位置拍卖(position auction)
+假设有一组广告位可使用,则将广告位按价值排序,并将广告主出价排序,并依次对应投放,价高者将得到更好的广告位

+

CPT(Cost Per Time)
+按展示时长计费;可以充分发挥橱窗效应,但无法利用受众定向技术,效率较低;使用于高曝光的品牌广告

+

CPM(Cost Per Mille)
+即“千人成本”,按访问量(每千人的访问量)计费,是目前最主流的结算方法,具备受众定向能力,适用于实时竞价广告

+

eCPM(千人收益)
+e指的是expect,也就是预期最优的的千人访问收益。

+

CPC(Cost Per Click)
+按点击次数计费

+

CPA(Cost Per Action)
+按用户行为、实际投放效果计费,如用户的注册、购买、安装行为,供给方运营难度较大,而需求方无任何风险,适合于效果广告

+

CPS(Cost Per Sell) ROI(Return On Investment)
+同CPA,都是按实际效果计费的方式,ROI即常说的“投资回报率”

+

点击率(Click Through Rate,CTR)
+即广告点击与广告展示的比率

+

到达率 (Reach Rate)
+落地页打开次数与点击次数比例,落地页(landing page)即点击后所跳转的页面

+

转化率CVR (Conversion Rate)
+转化次数与到达次数的比例

+

在线广告交易模式
+合约广告、竞价广告、实时竞价、程序化广告

+

广告交易的相关机构与平台(程序化广告)
+广告网络 ADN(ad net-work):连接供给方与需求方,负责批量运营媒体广告位,并对流量进行售卖的平台
+需求方平台DSP(demand side platform):为广告主提供跨媒介、跨平台、跨终端的的广告投放平台,通过数据整合、分析实现基于受众的精准投放
+供给方平台 SSP(supply side platform):是为媒体提供服务的平台,负责管理媒体广告位库存、优化广告位的售卖,提高其广告资源价值,帮助其提升收益
+广告交易平台 ADX(ad Exchange):帮助DSP与SSP通过RTB方式进行广告交易的平台
+数据管理平台 DMP(data management platform):负责统合、加工第三方用户数据,并售卖给DSP的数据平台

+
在这里插入图片描述
在这里插入图片描述
+

定向广告(targeted advertising)
+不再把广告投给所有人,而是面向不同的受众,赋予其不同的用户标签,并投放不同的广告。由此,广告主从广告位的采买,变成了面向受众人群的采买

+

流量预测
+预估该媒体中会产生的流量总数,便于进行售前指导/出价指导,如果低估了平台流量,可能导致售卖不足,利润无法最大化;如果高估平台流量,可能导致超售,无法满足合约

+

流量塑形(traffic shaping)
+通过主动影响流量来实现合约达成,综合性门户网站是典型例子,网站通过优化导流与跳转页布局,为用户点击链接提供便利,实现最大的到达率

+

在线分配
+即通过设计分配策略,实现高效的流量分配。展示合约都面临的问题:在流量/广告位有限,而满足合约的人群大量重叠的情况下,要实现利益最大化,如何让各个合约到最大限度满足

+

广义第二高价理论:Generalized Second Price, GSP
+指在只有一个位置的拍卖中,出价最高的广告主赢得广告位后,只需要支付其下一位广告主的出价(排名第二的广告主),而不用按自己的出价来购买,这是一种最合理的竞价逻辑

+

搜索广告(search ad)
+通过用户的搜索关键字作为特征召回广告内容

+

上下文广告(contextual advertising)
+根据浏览页面的关键词进行投放,而不是用户输入的关键词

+

原生广告&信息流广告(Native advertising)
+广告内容与媒介信息形式高度吻合的广告,都可以称为原生广告,“广告即内容、内容即广告”

+]]>
+ 2024-09-01T13:54:44.000Z +
+ + 程序员副业探索之电商 + http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html + + 2024-08-04T14:51:49.000Z + 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

+

在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展趋势。结合自身的电商背景与资源,我倾向于专注于小程序电商,但对拼多多店铺和跨境电商也有兴趣。

+

探索小程序电商的过程中,我结识了许多朋友,也认识了几位从事电商运营的同事。最初,我将重心放在小程序女装项目上,计划从深圳的优质供应商中精选货品,打造一个集商品展示、购买、支付、私域于一体的完整小程序平台。然而,女装行业的高退货率远超预期,几乎每售出十件商品,就有七件被退货,这让项目难以持续。

]]>
+ 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

+

在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展趋势。结合自身的电商背景与资源,我倾向于专注于小程序电商,但对拼多多店铺和跨境电商也有兴趣。

+

探索小程序电商的过程中,我结识了许多朋友,也认识了几位从事电商运营的同事。最初,我将重心放在小程序女装项目上,计划从深圳的优质供应商中精选货品,打造一个集商品展示、购买、支付、私域于一体的完整小程序平台。然而,女装行业的高退货率远超预期,几乎每售出十件商品,就有七件被退货,这让项目难以持续。

+

因此,我开始考虑其他方向,如化妆品。化妆品作为日常消费品需求稳定且退货率较低,因而具有较高的利润。接下来,我将分享个人在化妆品电商领域的探索。

+

一、小程序化妆品

+

关于小程序,我们其实考虑过很多种不同的方向。比如,我们曾设想过做一个垂直类的平台,主要销售化妆品或者女装。我们也有其他的想法,比如销售珠宝等产品。但是,经过实际流程的运行后,我们发现小程序在很多方面存在诸多限制。下面,我将简单地介绍一下,如何创建一个小程序。

+

1.1 小程序准备(营业执照&微信支付&小程序appId)

+

首先,如果你想开发一个小程序,必须要具备微信支付的功能。而微信支付是个人无法申请的,因此需要一个营业执照。首先,你需要申请一个个人营业执照。

+

取得营业执照后,接下来就是用这个营业执照来申请开通微信支付。

+
QQ_1722269773245
QQ_1722269773245
+

接下来,你需要注册一个小程序。在去年,可以通过一些服务商或代理商注册,每个小程序的注册费用仅需几毛钱。但现在这条途径已被限制,目前注册一个小程序大约需要300元。此外,为了上线小程序,还需要通过审核,审核费用同样大约为300元。

+

同时,你还需要准备域名和服务器资源。我们选择了一台配置为8G内存的服务器,因为我们的成交量并不大,这样的配置已经足够应付日常需求。

+

1.2 小程序开发

+

在小程序开发方面,由于我们是想完全独立开发,因此首先我们从 GitHub 上查找了一个电商小程序的开源项目。尽管我们查看了很多项目,但基本上没有直接能用的,都需要进行小改造。我们对其进行了改进,接入了微信支付和腾讯的 COS 对象存储。

+

接下来,我们搭建了自己的 MySQL 数据库。考虑到我们的小程序交易量较小,因此没有使用分库分表的复杂结构,单独一个 MySQL 数据库已经足够。如果未来交易量大幅增加,可以考虑使用其他厂商的分库分表方案,例如 TiDB 等。

+

开发的框架网上的都类似一样,分为下面的几个模块,整体上感觉代码也不复杂(CRUD),就是比较耗时间。

+
    +
  • 会员系统:用户注册、登录、会员等级、积分管理等。
  • +
  • 商品管理:商品的添加、编辑、删除、库存管理、分类管理等。
  • +
  • 订单管理:订单生成、支付、取消、退换货、订单状态管理等。
  • +
  • 支付系统:支持多种支付方式,如微信支付、支付宝支付、银联支付等。
  • +
  • 营销工具:优惠券、拼团、秒杀、砍价、满减活动等。
  • +
  • 物流管理:快递公司管理、物流信息追踪等。
  • +
  • 数据分析:提供销售数据统计、用户数据分析、商品销售分析等报表功能。
  • +
  • 客服系统:在线客服、留言板、消息通知等。
  • +
  • 权限管理:用户权限角色管理,不同角色有不同的操作权限。
  • +
+
强化私域流量管理
强化私域流量管理
+

网上那些代理商有赞、微盟之类的,可以0元搞个小程序,但是如果要用到营销功能(秒杀、优惠券等),都得给几千、几万块。底成本创业,就自己找开元弄一下了,前后端都会点。在成本方面,由于个人对化妆品并不熟悉,也分不清面霜、补水之类的,因此花费了约500元请别人帮忙上传商品信息。效果如下,也可去微信小程序上搜索一下体验下(不要付款)。

+
QQ_1722270680440
QQ_1722270680440
+

下面是小程序主要的一些功能:

+

域名配置:不能通过ip使用,需要申请好域名+https才可。

+

微信支付:如上一步所说,营业执照后即可把小程序对接微信支付

+

微信物流(发货信息管理):以前网上可集成快递100的功能,现在需要收费了,好在微信官方提供了自己的微信物流,可以直接对接,免费,填写完发货信息之后微信还可以直接通知用户。

+

客服:这个很重要,需要直接对接普通用户,回答一些咨询和售后内容。

+

微信认证:这个微信强求,没办法,300块。确保商品、图片、文字等内容的合法性,避免侵权行为。遵守相关电商法律法规,例如消费者权益保护、退换货政策等。

+

尽管小程序已经上线,整体功能虽然齐全,但效果不如预期。

+

我们原本设想,当用户下单后可以从华南、北地区的供货商那里进行发货。然而,整体计算下来,由于缺乏足够的曝光,导致小程序的商品没有得到应有的推广。在微信平台,小程序的商品不会自动得到推广曝光,需要开发者自行进行推广。

+

我们在开发初期计划通过小程序结合私域流量进行引流,主要策略包括:建立小程序,通过社群引流和推广,吸引粉丝,再通过促销和秒杀等营销活动来提高订单量。然而,第一步的推广就遇到了困难。整个五月份几乎没有用户点击和访问。

+

最初立下的方向较为宏大,计划通过私域流量提升销售。然而,由于对化妆品行业了解不够深,加之市场上存在假货问题,也是在无法更多的推广。

+
QQ_1722270494718
QQ_1722270494718
+

算一下总投入吧:

+

营业执照(180)+微信支付认证(50)+小程序appId(2)+服务器(100/月)+请人录入化妆品(500),最最最重要的是,营业执照还需要年审,查税什么的,如果不用还得花钱注销。

+
QQ_1722359862581
QQ_1722359862581
+

二、拼多多电商

+

在打造自己的私域生态时,我发现仅依靠小程序并不理想,因此开始考虑其他电商平台,比如小红书和拼多多。

+

在小红书上,阿里巴巴的 1688 平台不太支持直接批量上传商品,所有商品和图片都需要手动录入和上传,非常不方便。至于选择拼多多还是淘宝,我个人更倾向于拼多多,因为其用户相对较多。

+

在选择供应商时,发现 1688 上的化妆品要么不存在,要么是一些小众品牌,无法放心销售。因此,我决定选择一个垂直方向的产品,最终专注于女性相关的饰品,比如手链和宠物项链。

+

我在 1688 上找到了一些手链店铺,并支付了 1000 元的保证金,随后通过 1688 的分销中心将产品直接分销到拼多多。然而,拼多多的定价策略需要自行调整,否则利润非常低,甚至可能亏损。

+

在开始销售时,由于没注意到新疆和西藏不包邮,导致一起订单亏损了十多元。总体来说,虽然通过广告推广获取了一些流量和曝光,并且实现了约 10 个订单,销售额为 200多 元,但最终有一半订单被退货,只盈利了大约 100 元。

+

由于退货地址填写的是我自己这边,所有退货商品都被寄回给我。检查后发现,这些产品的质量确实不佳。由于麻烦,我决定不再将退货商品寄回 1688 的商家,干脆自己留着。

+
clone
clone
+

成本:成交金额270,退单一半左右,自己承担,没退给1688商家,大概亏损130左右。

+

三、跨境电商

+

在腾讯工作期间,我听说有些人通过跨境电商取得了显著的成功,比如将电子烟卖到欧美国家,甚至在一年内赚到了一套深圳的房子。当然,这可能有些夸大。然而,国内电商市场竞争异常激烈,利润微薄,特别是拼多多对工厂的压榨,使得供应商难以生存。广州奥园的事件也是一个例子,显示了供应商在国内激烈竞争中的艰难处境。

+

我也考虑过跨境电商的可能性,比如建立独立站或在亚马逊上运营。目前,这些想法还在考察阶段,需要进一步了解和评估。

+
QQ_1722358300059
QQ_1722358300059
+

偶然听别人说:中产做生意返贫之路,就是奶茶店、跨境电商!

+

四、总结

+

小程序+拼多多店,应该亏损1000左右吧,就当做交学费,总的来说,也积累了不少经验。对整体的电商流程,包括开设门店和私域营销,都会有一定了解。但坦率地说,电商市场竞争非常激烈,利润空间被压缩得很厉害。

+

首先,以拼多多为例,它直接连接了工厂和用户(B2C),消除了很多中间环节。如果你想在这一过程中作为中间商进行营销推广,就必须具备独特的优势。现在一些平台,比如直播带货,采用的就是大批量采购的方式。这种方式确实能降低成本,并且通过批量采购获利。

+

另一方面,很多用户并不了解像阿里巴巴这样的货源平台,还有些用户甚至还在使用淘宝。利用这种信息差,还是可以赚取一定的利润,但这也让电商行业变得更加竞争激烈。

+

在电商领域的下一步发展方向上,我个人认为直播带货可能是一个更具潜力的选择。结合种草文化和私域流量管理,这种方式可能相对来说能带来更多的收益。作为中间商,目前似乎也只有这条路可以探索。可惜不是人人都能成为李佳琦或者董宇辉。

+]]>
+ 2024-08-04T14:51:49.000Z +
+ + 2024-07-24 + http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html + + 2024-07-24T15:52:12.000Z + 新买了台air m3,午夜色的,心心念念了好久

+IMG_20240723_235443 +

读写速度

+c6c117ff1f5f6371b3ab682621369c1f]]>
+ 新买了台air m3,午夜色的,心心念念了好久

+IMG_20240723_235443 +

读写速度

+c6c117ff1f5f6371b3ab682621369c1f +

和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧

+096e891a0278263d0c790e74f32ef911_720 +

非果粉,但好多苹果设备

+0742e82ab0ba499dea8b267f958cdada +]]>
+ 2024-07-24T15:52:12.000Z +
+ + 股票预测 + http://www.wenzhihuai.com/stock/ + + 2024-07-02T14:33:09.000Z + + + +]]> + + + +]]> + 2024-07-02T14:33:09.000Z + + + 赛力斯 + http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html + + 2024-07-02T14:33:09.000Z + update

+]]>
+ update

+]]>
+ 2024-07-02T14:33:09.000Z +
+ + 好玩的 + http://www.wenzhihuai.com/interesting/ + + 2024-11-29T18:45:10.000Z + + + 2024-05-18T19:54:10.000Z + + + 基于OLAP做业务监控 + http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html + + 2024-11-10T15:55:36.000Z + 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

+

一、两者的比较

+

Prometheus 的特点
+Prometheus 是一种开源的监控和警报工具包,最初由 SoundCloud 开发并开源。它具有以下特点:
+(1)多维度数据模型
+Prometheus 采用标签(label)系统,允许用户通过灵活的标签组合来区分和查询监控数据。
+(2)Pull 模式采集
+Prometheus 使用 Pull 模式从各种目标(如应用程序、数据库、操作系统等)拉取数据,这与其他使用 Push 模式的监控系统有所不同。
+(3)警报和告警管理
+Prometheus 内置了一个强大的警报管理系统,可以根据预定条件触发警报,并集成到多种通知平台(如邮件、Slack 等)。
+(4)生态系统和可扩展性
+Prometheus 拥有活跃的社区和广泛的插件支持,可以轻松与其他系统集成,并根据需要进行扩展。

]]>
+ 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

+

一、两者的比较

+

Prometheus 的特点
+Prometheus 是一种开源的监控和警报工具包,最初由 SoundCloud 开发并开源。它具有以下特点:
+(1)多维度数据模型
+Prometheus 采用标签(label)系统,允许用户通过灵活的标签组合来区分和查询监控数据。
+(2)Pull 模式采集
+Prometheus 使用 Pull 模式从各种目标(如应用程序、数据库、操作系统等)拉取数据,这与其他使用 Push 模式的监控系统有所不同。
+(3)警报和告警管理
+Prometheus 内置了一个强大的警报管理系统,可以根据预定条件触发警报,并集成到多种通知平台(如邮件、Slack 等)。
+(4)生态系统和可扩展性
+Prometheus 拥有活跃的社区和广泛的插件支持,可以轻松与其他系统集成,并根据需要进行扩展。

+

OLAP 数据库

+

OLAP(Online Analytical Processing)数据库在监控数据存储和分析中具有独特的优势,尤其在数据持久性和实时查询方面。

+

(1)数据持久性和长期存储
+OLAP 数据库通常具有可靠的数据持久性和长期存储能力,能存储大量历史数据,并支持快速的时间范围查询和数据回溯分析。
+(2)实时数据加载和查询
+现代 OLAP 数据库尽管通常不支持实时数据加载,但一些具有较低的延迟和高吞吐量,可以支持实时查询和分析需求。

+

二、为什么要用OLAP数据库来做监控

+
1
1
+
2
2
+
3
3
+]]>
+ 2024-05-13T16:26:06.000Z +
+ + Jetbrains Idea设置 + http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html + + 2024-05-12T03:58:39.000Z + 一、类注释 +

想要在生成类的时候,自动带上author和时间,效果如下:

+
/**
+ * @author zhihuaiwen
+ * @since 2024/5/12 00:22
+ */
+public class EFE {
+}
+
]]>
+ 一、类注释 +

想要在生成类的时候,自动带上author和时间,效果如下:

+
/**
+ * @author zhihuaiwen
+ * @since 2024/5/12 00:22
+ */
+public class EFE {
+}
+

放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他:

+
image-20240512002500812
image-20240512002500812
+

二、方法注释

+

想让自动给方法生成注释,以及参数的默认注释:

+
  /**
+   * 获取首页商品列表
+   *
+   * @param type             类型 【1 精品推荐 2 热门榜单 3首发新品 4促销单品】
+   * @param pageParamRequest 分页参数
+   * @return List
+   */
+  @Override
+  public CommonPage<IndexProductResponse> findIndexProductList(Integer type, PageParamRequest pageParamRequest) {
+

去settings里面找到Live Templates,新建一个Template Group叫做user,然后再新建一个Live Template。

+
image-20240512001636936
image-20240512001636936
+

Abbreviation填*,Description随便填,然后再Template text填下:

+
*
+$VAR1$
+ * @return $returns$
+ */
+

编辑变量,Edit Variables:

+
image-20240512001711420.png
image-20240512001711420.png
+

VAR1:

+
groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+=' * @param ' + params[i] + ' ' + params[i] + ((i < params.size() - 1) ? '\\n' : '')}; return result", methodParameters())
+

returns:

+
methodReturnType()
+

三、关闭Annotate With Git Blame

+

Idea这个太烦了,总是不小心误点,想把右侧的两个都给去掉,网上查了有些比较旧,无法解决,下面特别声明下:

+
image-20240512001050312
image-20240512001050312
+

位置:settings直接搜Inlay Hints,把右侧的Usages、Code author给取消勾选即可

+
image-20240512001010797
image-20240512001010797
+

上面的设置放到百度网盘里了,可以下载后导入到Idea

+

链接: https://pan.baidu.com/s/1-3uhY1jssyMWyRbvBLQ2FQ?pwd=epw2 提取码: epw2 复制这段内容后打开百度网盘手机App,操作更方便哦

+image-20240512115716124 +]]>
+ 2024-05-11T16:37:27.000Z +
+ + 小程序反编译 + http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html + + 2024-05-11T18:24:35.000Z + 第一步:电脑端提取 +

先找到小程序保存的地址,一般先找到微信的文件管理下

+image-20240310171919281 +

然后到该目录的C:\Users\w1570\Documents\WeChat Files\Applet即可看到相关小程序了。

+
image-20240310173802224
image-20240310173802224
]]>
+ 第一步:电脑端提取 +

先找到小程序保存的地址,一般先找到微信的文件管理下

+image-20240310171919281 +

然后到该目录的C:\Users\w1570\Documents\WeChat Files\Applet即可看到相关小程序了。

+
image-20240310173802224
image-20240310173802224
+

找到需要解压的包__APP__.wxapkg,拷贝到和pc_wxapkg_decrypt.exe的统一路径下。

+

第二步:解密wxapkg包

+ +
pc_wxapkg_decrypt.exe -wxid 微信小程序id -in 要解密的wxapkg路径 -out 解密后的路径
+//示例如下
+pc_wxapkg_decrypt.exe -wxid wx7444167f2a6427b3 -in __APP__.wxapkg
+

第三步:解包

+ +
切换到./nodejs目录下,解压`node_modules.zip`后,使用cmd命令打开
+输入下面命令
+
+node wuWxapkg.js ..\decrypt\dec.wxapkg
+
+第二个参数为操作的项目,这里操作的是666.wxapkg 记得改为自己的
+

发现偶尔还是有些html没有正常解析,有点奇怪

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 开发工具整理 + http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html + + 2024-05-11T18:24:35.000Z + ShadowsocksX +

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
+win端:https://github.com/shadowsocks/shadowsocks-windows
+安卓:https://github.com/shadowsocks/shadowsocks-android

]]>
+ ShadowsocksX +

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
+win端:https://github.com/shadowsocks/shadowsocks-windows
+安卓:https://github.com/shadowsocks/shadowsocks-android

+

IDEA激活

+

许可证激活(License server)
+http://vip.52shizhan.cn
+http://fls.itxe.net

+

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 高可用 + http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html + + 2024-03-10T14:39:30.000Z + 4个9(99.99)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SentinelHystrix(维护状态)Resilience4j(Spring推荐)
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口滑动窗口Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性支持多种数据源支持多种数据源有限支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统的自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统
]]>
+ 4个9(99.99)

+

| | Sentinel | Hystrix(维护状态) | Resilience4j(Spring推荐) |
+|

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 高并发 + http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html + + 2024-03-10T14:39:30.000Z + QPS

+

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

+
    +
  • 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
  • +
  • 吞吐量:单位时间内处理的请求数量。
  • +
  • QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • +
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
  • +
]]>
+ QPS

+

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

+
    +
  • 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
  • +
  • 吞吐量:单位时间内处理的请求数量。
  • +
  • QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • +
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
  • +
+

负载均衡

+

正所谓双拳难敌四手,高并发撑场面的首选方案就是集群化部署,一台服务器承载的QPS有限,多台服务器叠加效果就不一样了。

+

如何将流量转发到服务器集群,这里面就要用到负载均衡,比如:LVS 和 Nginx。

+

常用的负载算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接数法等

+

业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。

+

注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx

+

池化技术

+

复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,有时间开销浪费。池化技术的核心是资源的“预分配”和“循环使用”,常用的池化技术有线程池、进程池、对象池、内存池、连接池、协程池。

+

连接池的几个重要参数:最小连接数、空闲连接数、最大连接数

+

Linux 内核中是以进程为单元来调度资源的,线程也是轻量级进程。所以说,进程、线程都是由内核来创建并调度。协程是由应用程序创建出来的任务执行单元,比如 Go 语言中的协程“goroutine”。协程本身是运行在线程上,由应用程序自己调度,它是比线程更轻量的执行单元。

+

在 Go 语言中,一个协程初始内存空间是 2KB(Linux 下线程栈大小默认是 8MB),相比线程和进程来说要小很多。协程的创建和销毁完全是在用户态执行的,不涉及用户态和内核态的切换。另外,协程完全由应用程序在用户态下调用,不涉及内核态的上下文切换。协程切换时由于不需要处理线程状态,需要保存的上下文也很少,速度很快。

+

Go语言中协程池的实现方法有两种:抢占式和调度式。

+

抢占式协程池,所有任务存放到一个共享的 channel 中,多个协程同时去消费 channel 中的任务,存在锁竞争。
+调度式协程池,每个协程都有自己的 channel,每个协程只消费自己的 channel。下发任务的时候,采用负载均衡算法选择合适的协程来执行任务。比如选择排队中任务最少的协程,或者简单轮询。

+

流量漏斗

+

上面讲的是正向方式提升系统QPS,我们也可以逆向思维,做减法,拦截非法请求,将核心能力留给正常业务!

+

互联网高并发流量并不都是纯净的,也有很多恶意流量(比如黑客攻击、恶意爬虫、黄牛、秒杀器等),我们需要设计流量拦截器,将那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。

+

拦截器分层:

+

网关和 WAF(Web Application Firewall,Web 应用防火墙)
+采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法

+

风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用。
+下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 高性能 + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html + + 2024-03-10T14:39:30.000Z + 响应时间

+

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

+

那么有哪些因素会影响系统的性能呢?

+

用户网络环境
+请求/响应的数据包大小
+业务系统 CPU、内存、磁盘等性能
+业务链路的长度
+下游系统的性能
+算法实现是否高效
+当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。

+

1、高性能缓存

+

对一些热点数据每次都从 DB 中读取,会给 DB 带来较大的压力,导致性能大幅下降。所以,我们需要用缓存来提升热点数据的访问性能,比如将活动信息数据在浏览器的缓存中保存一段时间。

]]>
+ 响应时间

+

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

+

那么有哪些因素会影响系统的性能呢?

+

用户网络环境
+请求/响应的数据包大小
+业务系统 CPU、内存、磁盘等性能
+业务链路的长度
+下游系统的性能
+算法实现是否高效
+当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。

+

1、高性能缓存

+

对一些热点数据每次都从 DB 中读取,会给 DB 带来较大的压力,导致性能大幅下降。所以,我们需要用缓存来提升热点数据的访问性能,比如将活动信息数据在浏览器的缓存中保存一段时间。

+

缓存根据性能由高到低分为:寄存器、L1缓存、L2缓存、L3缓存、本地内存、分布式缓存

+

上层的寄存器、L1 缓存、L2 缓存是位于 CPU 核内的高速缓存,访问延迟通常在 10 纳秒以下。L3 缓存是位于 CPU 核外部但在芯片内部的共享高速缓存,访问延迟通常在十纳秒左右。高速缓存具有成本高、容量小的特点,容量最大的 L3 缓存通常也只有几十MB。

+

本地内存是计算机内的主存储器,相比 CPU 芯片内部的高速缓存,内存的成本要低很多,容量通常是 GB 级别,访问延迟通常在几十到几百纳秒。

+

内存和高速缓存都属于掉电易失的存储器,如果机器断电了,这类存储器中的数据就丢失了。

+

特别说明:在使用缓存时,要注意缓存穿透、缓存雪崩、缓存热点问题、缓存数据一致性问题。当然为了提升整体性能通常会采用多级缓存组合方案(浏览器缓存+服务端本地内存缓存+服务端网络内存缓存)

+

2、日志优化,避免IO瓶颈

+

当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”。

+

在IO中断过程中,如果此时有其他任务线程可调度,系统会直接调度其他线程,这样 CPU 就相应显示为 Usr 或 Sys;但是如果此时系统较空闲,无其他任务可以调度,CPU 就会显示为 iowait(实际上与 idle 无本质区别)。

+

磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住。

+

Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除。

+

这种批量化、顺序写,大大提升了磁盘的吞吐性能!

+

缓存

+

异步

+

I/O(网络、数据、文件)

+

分库分表

+

参考

+

1.怎么优化java项目性能

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + Feed系统设计 + http://www.wenzhihuai.com/system-design/feed.html + + 2024-03-10T14:39:30.000Z + https://blog.csdn.net/weixin_45583158/article/details/128195940

+]]>
+ https://blog.csdn.net/weixin_45583158/article/details/128195940

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 行锁,表锁,意向锁 + http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html + + 2024-03-10T14:39:30.000Z + + + 2024-03-10T14:39:30.000Z + + + 一致性hash算法和哈希槽 + http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html + + 2024-03-10T14:39:30.000Z + + + 2024-03-10T14:39:30.000Z + + + JVM(java虚拟机) + http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html + + 2024-03-10T14:39:30.000Z + 一、了解JVM +

1、什么是JVM

+

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

+
微信图片_20220522232432.png
微信图片_20220522232432.png
]]>
+ 一、了解JVM +

1、什么是JVM

+

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

+
微信图片_20220522232432.png
微信图片_20220522232432.png
+

2、JRE/JDK/JVM是什么关系

+

JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
+JDK(Java Development Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
+JVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。

+

3、JVM体系结构:

+

类加载器:加载class文件;
+运行时数据区:包括方法区、堆、Java栈、程序计数器、本地方法栈
+执行引擎:执行字节码或者执行本地方法

+
img
img
+

二、运行时数据区

+

方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。从 JDK 8 开始,HotSpot 虚拟机移除了方法区,取而代之的是元空间(Metaspace)。元空间并不在 Java 虚拟机内存中,而是使用了本地(即操作系统)的内存。这个改变主要是为了解决方法区可能出现的内存溢出问题。
+:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。
+:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
+程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
+本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。

+

元空间

+

三、类加载机制

+

类加载器通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。

+

1、类的生命周期(7个)

+

加载、验证、准备、解析、初始化、使用、卸载

+

2、类加载的五个过程

+

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

+

加载:类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
+验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
+准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
+解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
+初始化:前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

+

3、类加载器

+

启动类加载器(BootStrap ClassLoader):主要负责加载jre/lib/rt.jar相关的字节码文件的。
+扩展类加载器(Extension Class Loader):主要负载加载 jre/lib/ext/*.jar 这些jar包的。
+应用程序类加载器(Application Class Loader):主要负责加载用户自定义的类以及classpath环境变量所配置的jar包的。
+自定义类加载器(User Class Loader):负责加载程序员指定的特殊目录下的字节码文件的。大多数情况下,自定义类加载器只需要继承ClassLoader这个抽象类,重写findClass()和loadClass()两个方法即可。

+

4、类加载机制(双亲委派)

+

类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。

+
img
img
+

工作过程:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。

+

四、垃圾回收

+

程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。 Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

+

1、判断对象已死

+

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

+

引用计数法:给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;

+
img
img
+
    注意:如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的            两块内存依然保持着互相引用无法回收。引用计数法很难解决循环引用问题; 
+
+

可达性分析:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

+
img
img
+
    可作为 GC Roots 的对象:
+    1)虚拟机栈中引用的对象
+    2)方法区中类静态属性引用的对象
+    3)方法区中常量引用的对象
+    4)本地方法栈中native方法引用的对象 
+
+

引用:下面四种引用强度依次减弱
+强引用:默认情况下,对象采用的均为强引用;
+软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
+弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
+虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

+

2、垃圾收集算法

+

标记清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
+复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
+标记整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
+分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记清除算法。

+

3、垃圾收集器

+

Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
+Parnew收集器:Serial收集器多线程版本。
+Parallel Scavenge收集器:使用复制算法的新生代收集器。
+Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
+Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
+CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
+G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。
+G1收集器有以下特点
+1)并行与并发:无需停顿Java线程来执行GC动作。
+2)分代收集:可独立管理整个GC堆。
+3)空间整合:运行期间不会产生内存空间碎片。
+4)可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

+

4、JVM内存分代机制

+

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。

+

新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
+老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
+永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
+元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。
+GC 过程:新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区 没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于 Eden区和Survivor From区,Survivor To区是空的(作为保留区域)。GC进行时,Eden区中 所有存活的对象都会被复制到Survivor To区,而在Survivor From区中,仍存活的对象会根据 它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾 回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有 达到阀值的对象会被复制到Survivor To区。接着清空Eden区和Survivor From区,新生代中存 活的对象都在Survivor To区。接着,Survivor From区和Survivor To区会交换它们的角色,也 就是新的Survivor To区就是上次GC清空的Survivor From区,新的Survivor From区就是上次 GC的Survivor To区,总之,不管怎样都会保证Survivor To区在一轮GC后是空的。GC时当 Survivor To区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进 行分配担保,将这些对象存放在老年代中。

+

5、Minor GC、Major GC、Full GC之间的区别

+

Minor GC:Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
+Major GC:指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
+Full GC:Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

+

6、Minor GC、Major GC、Full GC触发条件

+

Minor GC触发条件
+虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总 空间
+1)如果大于的话,直接执行minorGC
+2)如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
+3)如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历 次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC
+4)如果大于的话,执行minorGC

+

Full GC触发条件
+1)老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存 在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是 不要创建太大的对象。
+2)方法区空间不足:系统当中需要加载的类,调用的方法很多,同时方法区当中没有足够的 空间,就出触发一次Full GC
+3)老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
+4)调用System.gc()时(系统建议执行Full GC,但是不必然执行)

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + JAVA NIO + http://www.wenzhihuai.com/java/io/nio.html + + 2024-03-10T14:39:30.000Z + 一、简介 +

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

+

2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

+

3)Java AIO:异步非阻塞,在AIO模型中,当我们发起一个I/O操作(如读或写)时,我们不需要等待它完成,我们的代码会立即返回,可以继续执行其他任务。当I/O操作完成时,我们之前注册的回调函数会被自动调用,我们可以在这个回调函数中处理I/O操作的结果。

]]>
+ 一、简介 +

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

+

2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

+

3)Java AIO:异步非阻塞,在AIO模型中,当我们发起一个I/O操作(如读或写)时,我们不需要等待它完成,我们的代码会立即返回,可以继续执行其他任务。当I/O操作完成时,我们之前注册的回调函数会被自动调用,我们可以在这个回调函数中处理I/O操作的结果。

+

1.1典型的多路复用IO实现

+

目前流程的多路复用IO实现主要包括四种: selectpollepollkqueue。下表是他们的一些重要特性的比较:

+

| IO模型 | 相对性能 | 数据结构 | 关键思路 | 操作系统 | JAVA支持情况 |
+|

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + Spring Cloud Sentinel + http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html + + 2024-03-10T14:39:30.000Z + 参考 +

1.超详细springcloud sentinel教程~

+]]>
+ 参考 +

1.超详细springcloud sentinel教程~

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 入门使用 + http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html + + 2024-03-10T14:39:30.000Z + 参考 +

1.Sentinel入门(干货版)

+]]>
+ 参考 +

1.Sentinel入门(干货版)

+]]>
+ 2024-03-10T14:39:30.000Z +
+ + 基础 + http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html + + 2024-02-24T15:02:58.000Z + 一、ElasticSearch基础 +

1、什么是Elasticsearch:
+Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
+全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

+

2、Elasticsearch 的基本概念:

+

(1)index 索引:索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。

]]>
+ 一、ElasticSearch基础 +

1、什么是Elasticsearch:
+Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
+全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

+

2、Elasticsearch 的基本概念:

+

(1)index 索引:索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。

+

(2)type 类型:类型是用来定义数据结构,可以认为是 mysql 中的一张表,type 是 index 中的一个逻辑数据分类

+

(3)document 文档:类似于 MySQL 中的一行,不同之处在于 ES 中的每个文档可以有不同的字段,但是对于通用字段应该具有相同的数据类型,文档是es中的最小数据单元,可以认为一个文档就是一条记录。

+

(4)Field 字段:Field是Elasticsearch的最小单位,一个document里面有多个field

+

(5)shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

+

(6)replica 副本:任何服务器随时可能故障或宕机,此时 shard 可能会丢失,通过创建 replica 副本,可以在 shard 故障时提供备用服务,保证数据不丢失,另外 replica 还可以提升搜索操作的吞吐量。

+

shard 分片数量在建立索引时设置,设置后不能修改,默认5个;replica 副本数量默认1个,可随时修改数量;

+

(7)segment:每个shard分片是一个lucene实例,每个分片由多个segment组成!!每个segment占用内存,文件句柄等。segment 合并的过程,需要先读取小的 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,合并大的 segment 需要消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。所以 Elasticsearch 在默认情况下会对合并线程进行资源限制,确保它不会对搜索性能造成太大影响。

+
image-20240224213112456
image-20240224213112456
+

3、什么是倒排索引:
+在搜索引擎中,每个文档都有对应的文档 ID,文档内容可以表示为一系列关键词的集合,例如,某个文档经过分词,提取了 20 个关键词,而通过倒排索引,可以记录每个关键词在文档中出现的次数和出现位置。也就是说,倒排索引是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。

+

要注意倒排索引的两个细节:

+

倒排索引中的所有词项对应一个或多个文档
+倒排索引中的词项 根据字典顺序升序排列

+
img
img
+

4、doc_values 的作用:
+倒排索引虽然可以提高搜索性能,但也存在缺陷,比如我们需要对数据做排序或聚合等操作时,lucene 会提取所有出现在文档集合的排序字段,然后构建一个排好序的文档集合,而这个步骤是基于内存的,如果排序数据量巨大的话,容易造成内存溢出和性能缓慢。
+doc_values 就是 es 在构建倒排索引的同时,会对开启 doc_values 的字段构建一个有序的 “document文档 ==> field value” 的列式存储映射,可以看作是以文档维度,实现了根据指定字段进行排序和聚合的功能,降低对内存的依赖。另外 doc_values 保存在操作系统的磁盘中,当 doc_values 大于节点的可用内存,ES可以从操作系统页缓存中加载或弹出,从而避免发生内存溢出的异常,但如果 docValues 远小于节点的可用内存,操作系统就自然将所有 doc_values 存于内存中(堆外内存),有助于快速访问。

+

5、text 和 keyword类型的区别:
+两个类型的区别主要是分词:keyword 类型是不会分词的,直接根据字符串内容建立倒排索引,所以keyword类型的字段只能通过精确值搜索到;Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引

+

6、query 和 filter 的区别?

+

(1)query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;

+

(2)filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。

+

二、ES的写入流程

+

1、ES写数据的整体流程:

+
image-20240223165512655
image-20240223165512655
+

(1)客户端选择 ES 的某个 node 发送请求过去,这个 node 就是协调节点 coordinating node
+(2)coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)
+(3)实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node
+(4)coordinating node 等到 primary node 和所有 replica node 都执行成功之后,最后返回响应结果给客户端。
+2、ES主分片写数据的详细流程:

+
Elasticsearch索引文档的过程
Elasticsearch索引文档的过程
+

(1)主分片先将数据写入ES的 memory buffer,然后定时(默认1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入操作系统缓存 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 segment 文件实际上是一些倒排索引的集合, 只有经历了 refresh 操作之后,这些数据才能变成可检索的。

+

ES 的近实时性:数据存在 memory buffer 时是搜索不到的,只有数据被 refresh 到 Filesystem cache 之后才能被搜索到,而 refresh 是每秒一次, 所以称 es 是近实时的;可以手动调用 es 的 api 触发一次 refresh 操作,让数据马上可以被搜索到;

+

(2)由于 memory Buffer 和 Filesystem Cache 都是基于内存,假设服务器宕机,那么数据就会丢失,所以 ES 通过 translog 日志文件来保证数据的可靠性,在数据写入 memory buffer 的同时,将数据也写入 translog 日志文件中,当机器宕机重启时,es 会自动读取 translog 日志文件中的数据,恢复到 memory buffer 和 Filesystem cache 中去。

+

ES 数据丢失的问题:translog 也是先写入 Filesystem cache,然后默认每隔 5 秒刷一次到磁盘中,所以默认情况下,可能有 5 秒的数据会仅仅停留在 memory buffer 或者 translog 文件的 Filesystem cache中,而不在磁盘上,如果此时机器宕机,会丢失 5 秒钟的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

+

(3)flush 操作:不断重复上面的步骤,translog 会变得越来越大,不过 translog 文件默认每30分钟或者 阈值超过 512M 时,就会触发 commit 操作,即 flush操作,将 memory buffer 中所有的数据写入新的 segment 文件中, 并将内存中所有的 segment 文件全部落盘,最后清空 translog 事务日志。

+

① 将 memory buffer 中的数据 refresh 到 Filesystem Cache 中去,清空 buffer;
+② 创建一个新的 commit point(提交点),同时强行将 Filesystem Cache 中目前所有的数据都 fsync 到磁盘文件中;
+③ 删除旧的 translog 日志文件并创建一个新的 translog 日志文件,此时 commit 操作完成
+更多 ES 的数据写入流程的说明欢迎阅读这篇文章:ElasticSearch搜索引擎:数据的写入流程

+

三、ES的更新和删除流程

+

​ 删除和更新都是写操作,但是由于 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;所以 ES 利用 .del 文件 标记文档是否被删除,磁盘上的每个段都有一个相应的.del 文件

+

(1)如果是删除操作,文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉。

+

(2)如果是更新操作,就是将旧的 doc 标识为 deleted 状态,然后创建一个新的 doc。

+

memory buffer 每 refresh 一次,就会产生一个 segment 文件 ,所以默认情况下是 1s 生成一个 segment 文件,这样下来 segment 文件会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment 文件合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,不写入到新的 segment 中,然后将新的 segment 文件写入磁盘,这里会写一个 commit point ,标识所有新的 segment 文件,然后打开 segment 文件供搜索使用,同时删除旧的 segment 文件

+

有关segment段合并过程,欢迎阅读这篇文章:Elasticsearch搜索引擎:ES的segment段合并原理

+

四、ES的搜索流程

+

搜索被执行成一个两阶段过程,即 Query Then Fetch:

+

1、Query阶段:

+

客户端发送请求到 coordinate node,协调节点将搜索请求广播到所有的 primary shard 或 replica,每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。接着每个分片返回各自优先队列中 所有 docId 和 打分值 给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

+

2、Fetch阶段:
+协调节点根据 Query阶段产生的结果,去各个节点上查询 docId 实际的 document 内容,最后由协调节点返回结果给客户端。

+

coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
+接收请求的 node 返回 document 给 coordinate node 。
+coordinate node 返回 document 给客户端。
+Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。

+

五、ES在高并发下如何保证读写一致性?

+

(1)对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖

+

每个文档都有一个_version 版本号,这个版本号在文档被改变时加一。Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,它会被简单的忽略。

+

利用_version的这一优点确保数据不会因为修改冲突而丢失,比如指定文档的version来做更改,如果那个版本号不是现在的,我们的请求就失败了。

+

(2)对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。

+

one:写操作只要有一个primary shard是active活跃可用的,就可以执行
+all:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
+quorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
+(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。

+

六、ES集群如何选举Master节点

+

1、Elasticsearch 的分布式原理:

+

Elasticsearch 会对存储的数据进行切分,划分到不同的分片上,同时每一个分片会生成多个副本,从而保证分布式环境的高可用。ES集群中的节点是对等的,节点间会选出集群的 Master,由 Master 会负责维护集群状态信息,并同步给其他节点。

+

Elasticsearch 的性能会不会很低:不会,ES只有建立 index 和 type 时需要经过 Master,而数据的写入有一个简单的 Routing 规则,可以路由到集群中的任意节点,所以数据写入压力是分散在整个集群的。

+

2、ES集群 如何 选举 Master:

+

Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;

+

(1)确认候选主节点的最少投票通过数量(elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes)
+(2)选举时,集群中每个节点对所有 master候选节点(node.master: true)根据 nodeId 进行字典排序,然后选出第一个节点(第0位),暂且认为它是master节点。
+(3)如果对某个节点的投票数达到阈值,并且该节点自己也选举自己,那这个节点就是master;否则重新选举一直到满足上述条件。
+补充:master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。

+

3、Elasticsearch是如何避免脑裂现象:

+

(1)当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;

+

(2)当集群 master 候选节点 只有两个时,这种情况是不合理的,最好把另外一个node.master改成false。如果我们不改节点设置,还是套上面的(N/2)+1公式,此时discovery.zen.minimum_master_nodes应该设置为2。这就出现一个问题,两个master备选节点,只要有一个挂,就选不出master了

+

七、建立索引阶段性能提升方法

+

(1)如果是大批量导入,可以设置 index.number_of_replicas: 0 关闭副本,等数据导入完成之后再开启副本
+(2)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
+(3)如果搜索结果不需要近实时性,可以把每个索引的 index.refresh_interval 改到30s
+(4)增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB
+(5)使用 SSD 存储介质
+(6)段和合并:Elasticsearch 默认值是 20 MB/s。但如果用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。

+

八、ES的深度分页与滚动搜索scroll

+

(1)深度分页:

+

深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深。

+

(2)滚动搜索:
+一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。

+

参考

+

1.ElasticSearch搜索引擎
+2.2023最新整理的 Elasticsearch 21道面试题
+3.elasticsearch中文官网
+4.elasticsearch 倒排索引原理

+]]>
+ 2024-02-24T15:02:58.000Z +
+ + 一些不常用但很实用的命令 + http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html + + 2024-05-11T18:24:35.000Z + https连接耗时检测 +
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\n" -so /dev/null https://zhls.qq.com/test-nginx
+
]]>
+ https连接耗时检测 +
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\n" -so /dev/null https://zhls.qq.com/test-nginx
+

iptables拒绝请求

+
iptables -A  OUTPUT -d 11.145.18.159 -j REJECT
+
]]>
+ 2024-02-24T05:31:47.000Z +
+ + Jenkins的一些笔记 + http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html + + 2024-02-24T05:12:56.000Z + 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

+

一、在全局安全配置中

+

1.1 启用安全

+

如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直没有勾选安全域,然后授权策略为登录用户可以做任何事,之后权限这一块就彻底崩溃了,重装了又重装,才知道,需要勾选安全域。

+
+
+
]]>
+ 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

+

一、在全局安全配置中

+

1.1 启用安全

+

如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直没有勾选安全域,然后授权策略为登录用户可以做任何事,之后权限这一块就彻底崩溃了,重装了又重装,才知道,需要勾选安全域。

+
+
+
+

1.2 跨域

+

同时开启跨站请求伪造保护,Jenkins的一些API需要用到的。

+
+
+
+

二、获取TOKEN

+

2.1 TOKEN

+

Jenkins的用户token可以在用户的设置下面获得,但是这种方式如果需要重装Jenkins的话,就得重新修改一次配置文件

+
+
+
+

经过对Jenkins-client的抓包分析,token可以由username+":"+password,然后进行base64加密组成,之后在token前面加上"Basic "即可,代码如下:

+
+
+
+

三、获取Jenkins-Crumb

+

在远程API调用的时候,Jenkins对于某些接口的要求不仅限于Authorization,还必须要有Jenkins-Crumb,这个东西之前在进行获取的时候,有时候会变来变去,比如用curl命令和f12查看的时候发现不一致,实在受不了,感觉毫无规律可言,之后才发现上面的Authorization来直接调用接口获取的才是正确的,再然后想想,可能是之前调用api的时候,没有开启启用安全,再或者是有没有勾选上使用碎片算法。

+
+
+
+

另,附上curl查询Jenkins-Crumb的命令:

+
curl -s 'http://admin:yourtoken@jenkins-url/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+

替换掉yourtoken和jenkins-url即可。

+

四、值得注意的事

+

4.1 API设计

+

Jenkins的API设计可谓是独领风骚,能把一个提交设计成这样真实佩服测试之后才发现只要提交个表单,key为json,value为值即可,其他的都不需要,这个设计我也不知道怎么来的,感觉超级坑。

+
+
+
+

4.2 生成构建job

+

由于我们是将Jenkins集成在我们自己的平台里面,并不暴露Jenkins给用户,所以,创建一个job的时候,必须由我们平台的参数往Jenkins里面提交,这一提交,发现的问题不少。
+一是Jenkins的整个job的提交是由两步组成的,先是创建job,再提交配置。即:/createItem?name=xxx接口。
+二是提交的配置参数,提交的是整个xml,而不是由一个一个参数组成的。对于java来说,就得使用xstream或者其他来转化,甚是折腾,如图这种转化。

+
+
+
+

4.3 构建的队列

+

在点击立即构建的时候,Jenkins是没有返回任何信息,但是在Jenkins的内部,它是通过放到队列里等待的,如果有空闲,就开始构建,否则等待,这个队列是可以获取得到的,我们从里面可以获取上一次构建的信息,是成功还是失败。这种情况下,假设我们多个人同时点击,这下子就有点慌了,如何获取到具体某个人的构建结果,有点虐心。想了半天,最终得出的事:代码相同,意味着每次构建的结果相同,为什么要允许多个人同时点击?就这么解决了:从一个job的构建队列中获取最后一次构建的信息,如果是正在构建,那么不允许构建了,直到构建结果出来。

+

4.4 构建进度的查看

+

需要将Jenkins中的构建进度移植到我们自有的平台,Jenkins的构建进度时通过ajax轮询实现的,获取文本的规则主要从response header里面的两个字段获取
+(1)X-More-Data:是否有更多的数据
+(2)X-Text-Size:从开始到该次调用的文本大小
+我们是通过websocket来将文本内容推送到前端,使用的stomp协议,部分代码如下:

+
        while (true) {
+            ...
+            String string = response.body().string();
+            String header = response.header("X-More-Data");
+            if (!Strings.isNullOrEmpty(header) || start == 0) {
+                template.convertAndSend("/topic/" + uuid, string);
+                String textSize = response.header("X-Text-Size");
+                if (!Strings.isNullOrEmpty(textSize)) {
+                    start = Integer.parseInt(textSize);
+                }
+                TimeUnit.SECONDS.sleep(5);
+            } else {
+                template.convertAndSend("/topic/" + uuid, string);
+                return;
+            }
+        }
+

参考:
+1.通过jenkins API去build一个job
+2.Jenkins Remote API

+]]>
+ 2024-02-24T05:12:56.000Z +
+ + Kubernetes容器日志收集 + http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html + + 2024-02-24T05:35:53.000Z + 日志采集方式 +

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

+

1.原生方式 使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。
+2.DaemonSet方式 在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。
+3.Sidecar方式 一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。
+三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。

]]>
+ 日志采集方式 +

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

+

1.原生方式 使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。
+2.DaemonSet方式 在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。
+3.Sidecar方式 一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。
+三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。

+

一、原生方式

+


+简单的说,原生方式就是直接使用kubectl logs来查看日志,或者将docker的日志通过日志驱动来打到syslog、journal等去,然后再通过命令来排查,这种方式最好的优势就是简单、资源占用率低等,但是,在多容器、弹性伸缩情况下,日志的排查会十分困难,仅仅适用于刚开始研究Kubernetes的公司吧。不过,原生方式确实其他两种方式的基础,因为它的两种最基础的理念,daemonset和sidecar模式都是基于这两种方式而来的。

+

1.1 控制台stdout方式

+

这种方式是daemonset方式的基础。将日志全部输出到控制台,然后docker开启journal,然后就能在/var/log/journal下面看到二进制的journal日志,如果要查看二进制的日志的话,可以使用journalctl来查看日志:journalctl -u docker.service -n 1 --no-pager -o json -o json-pretty

+
{
+        "__CURSOR" : "s=113d7df2f5ff4d0985b08222b365c27a;i=1a5744e3;b=05e0fdf6d1814557939e52c0ac7ea76c;m=5cffae4cd4;t=58a452ca82da8;x=29bef852bcd70ae2",
+        "__REALTIME_TIMESTAMP" : "1559404590149032",
+        "__MONOTONIC_TIMESTAMP" : "399426604244",
+        "_BOOT_ID" : "05e0fdf6d1814557939e52c0ac7ea76c",
+        "PRIORITY" : "6",
+        "CONTAINER_ID_FULL" : "f2108df841b1f72684713998c976db72665f353a3b4ea17cd06b5fc5f0b8ae27",
+        "CONTAINER_NAME" : "k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1",
+        "CONTAINER_TAG" : "f2108df841b1",
+        "CONTAINER_ID" : "f2108df841b1",
+        "_TRANSPORT" : "journal",
+        "_PID" : "6418",
+        "_UID" : "0",
+        "_GID" : "0",
+        "_COMM" : "dockerd-current",
+        "_EXE" : "/usr/bin/dockerd-current",
+        "_CMDLINE" : "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry hub.gcloud.lab --insecure-registry 172.30.0.0/16 --log-level=warn --signature-verification=false --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "_CAP_EFFECTIVE" : "1fffffffff",
+        "_SYSTEMD_CGROUP" : "/system.slice/docker.service",
+        "_SYSTEMD_UNIT" : "docker.service",
+        "_SYSTEMD_SLICE" : "system.slice",
+        "_MACHINE_ID" : "225adcce13bd233a56ab481df7413e0b",
+        "_HOSTNAME" : "dev4.gcloud.set",
+        "MESSAGE" : "I0601 23:56:30.148153       1 event.go:221] Event(v1.ObjectReference{Kind:\"DaemonSet\", Namespace:\"openshift-monitoring\", Name:\"node-exporter\", UID:\"f6d2bdc1-6658-11e9-aca2-fa163e938959\", APIVersion:\"apps/v1\", ResourceVersion:\"15378688\", FieldPath:\"\"}): type: 'Normal' reason: 'SuccessfulCreate' Created pod: node-exporter-hvrpf",
+        "_SOURCE_REALTIME_TIMESTAMP" : "1559404590148488"
+}
+

在上面的json中,_CMDLINE以及其他字段占用量比较大,而且这些没有什么意义,会导致一条简短的日志却被封装成多了几十倍的量,所以的在日志量特别大的情况下,最好进行一下字段的定制,能够减少就减少。
+我们一般需要的字段是CONTAINER_NAME以及MESSAGE,通过CONTAINER_NAME可以获取到Kubernetes的namespace和podName,比如CONTAINER_NAME为k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1的时候
+container name in pod: controllers
+**pod name: **master-controllers-dev4.gcloud.set
+namespace: kube-system
+**pod uid: **dcab37be702c9ab6c2b17122c867c74a_1

+

1.2 新版本的subPathExpr

+

journal方式算是比较标准的方式,如果采用hostPath方式,能够直接将日志输出这里。这种方式唯一的缺点就是在旧Kubernetes中无法获取到podName,但是最新版的Kubernetes1.14的一些特性subPathExpr,就是可以将目录挂载的时候同时将podName写进目录里,但是这个特性仍旧是alpha版本,谨慎使用。
+简单说下实现原理:容器中填写的日志目录,挂载到宿主机的/data/logs/namespace/service_name/$(PodName)/xxx.log里面,如果是sidecar模式,则将改目录挂载到sidecar的收集目录里面进行推送。如果是宿主机安装fluentd模式,则需要匹配编写代码实现识别namespace、service_name、PodName等,然后发送到日志系统。

+

可参考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md
+日志落盘参考细节:

+
    env:
+    - name: POD_NAME
+      valueFrom:
+        fieldRef:
+          apiVersion: v1
+          fieldPath: metadata.name
+   ...
+    volumeMounts:
+    - name: workdir1
+      mountPath: /logs
+      subPathExpr: $(POD_NAME)
+

我们主要使用了在Pod里的主容器挂载了一个fluent-agent的收集器,来将日志进行收集,其中我们修改了Kubernetes-Client的源码使之支持subPathExpr,然后发送到日志系统的kafka。这种方式能够处理多种日志的收集,比如业务方的日志打到控制台了,但是jvm的日志不能同时打到控制台,否则会发生错乱,所以,如果能够将业务日志挂载到宿主机上,同时将一些其他的日志比如jvm的日志挂载到容器上,就可以使用该种方式。

+
{
+    "_fileName":"/data/work/logs/epaas_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"rongqiyun-dev",
+    "_podName":"aofjweojo-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}
+

二、Daemonset方式

+
+

daemonset方式也是基于journal,日志使用journal的log-driver,变成二进制的日志,然后在每个node节点上部署一个日志收集的agent,挂载/var/log/journal的日志进行解析,然后发送到kafka或者es,如果节点或者日志量比较大的话,对es的压力实在太大,所以,我们选择将日志推送到kafka。容器日志收集普遍使用fluentd,资源要求较少,性能高,是目前最成熟的日志收集方案,可惜是使用了ruby来写的,普通人根本没时间去话时间学习这个然后进行定制,好在openshift中提供了origin-aggregated-logging方案。
+我们可以通过fluent.conf来看origin-aggregated-logging做了哪些工作,把注释,空白的一些东西去掉,然后我稍微根据自己的情况修改了下,结果如下:

+
@include configs.d/openshift/system.conf
+设置fluent的日志级别
+@include configs.d/openshift/input-pre-*.conf
+最主要的地方,读取journal的日志
+@include configs.d/dynamic/input-syslog-*.conf
+读取syslog,即操作日志
+<label @INGRESS>
+  @include configs.d/openshift/filter-retag-journal.conf
+  进行匹配
+  @include configs.d/openshift/filter-k8s-meta.conf
+  获取Kubernetes的相关信息  
+  @include configs.d/openshift/filter-viaq-data-model.conf
+  进行模型的定义
+  @include configs.d/openshift/filter-post-*.conf
+  生成es的索引id
+  @include configs.d/openshift/filter-k8s-record-transform.conf
+  修改日志记录,我们在这里进行了字段的定制,移除了不需要的字段
+  @include configs.d/openshift/output-applications.conf
+  输出,默认是es,如果想使用其他的比如kafka,需要自己定制
+</label>
+

当然,细节上并没有那么好理解,换成一步步理解如下:

+

1. 解析journal日志
+origin-aggregated-logging会将二进制的journal日志中的CONTAINER_NAME进行解析,根据匹配规则将字段进行拆解

+
    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    }
+

2. es封装
+主要用的是elasticsearch_genid_ext插件,写在了filter-post-genid.conf上。

+

3. 日志分类
+通过origin-aggregated-logging来收集journal的日志,然后推送至es,origin-aggregated-logging在推送过程中做了不少优化,即适应高ops的、带有等待队列的、推送重试等,详情可以具体查看一下。

+

还有就是对日志进行了分类,分为三种:
+(1).操作日志(在es中以.operations匹配的),记录了对Kubernetes的操作
+(2).项目日志(在es中以project
匹配的),业务日志,日志收集中最重要的
+(3).孤儿日志(在es中以.orphaned.*匹配的),没有namespace的日志都会打到这里

+

4. 日志字段定制
+经过origin-aggregated-logging推送至后采集的一条日志如下:

+
{
+    "CONTAINER_TAG": "4ad125bb7558",
+    "docker": {
+      "container_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c"
+    },
+    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    },
+    "systemd": {
+      "t": {
+        "BOOT_ID": "6246327d7ea441339d6d14b44498b177",
+        "CAP_EFFECTIVE": "1fffffffff",
+        "CMDLINE": "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry 10.77.0.0/16 --log-level=warn --signature-verification=false --bridge=none --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "COMM": "dockerd-current",
+        "EXE": "/usr/bin/dockerd-current",
+        "GID": "0",
+        "MACHINE_ID": "0096083eb4204215a24efd202176f3ec",
+        "PID": "17181",
+        "SYSTEMD_CGROUP": "/system.slice/docker.service",
+        "SYSTEMD_SLICE": "system.slice",
+        "SYSTEMD_UNIT": "docker.service",
+        "TRANSPORT": "journal",
+        "UID": "0"
+      }
+    },
+    "level": "info",
+    "message": "\tat com.sun.proxy.$Proxy242.execute(Unknown Source)",
+    "hostname": "host11.rqy.kx",
+    "pipeline_metadata": {
+      "collector": {
+        "ipaddr4": "10.76.232.16",
+        "ipaddr6": "fe80::a813:abff:fe66:3b0c",
+        "inputname": "fluent-plugin-systemd",
+        "name": "fluentd",
+        "received_at": "2019-05-15T09:22:39.297151+00:00",
+        "version": "0.12.43 1.6.0"
+      }
+    },
+    "@timestamp": "2019-05-06T01:41:01.960000+00:00",
+    "viaq_msg_id": "NjllNmI1ZWQtZGUyMi00NDdkLWEyNzEtMTY3MDQ0ZjEyZjZh"
+  }
+

可以看出,跟原生的journal日志类似,增加了几个字段为了写进es中而已,总体而言,其他字段并没有那么重要,所以我们对其中的字段进行了定制,以减少日志的大小,定制化字段之后,一段日志的输出变为(不是同一段,只是举个例子):

+
{
+    "hostname":"dev18.gcloud.set",
+    "@timestamp":"2019-05-17T04:22:33.139608+00:00",
+    "pod_name":"istio-pilot-8588fcb99f-rqtkd",
+    "appName":"discovery",
+    "container_name":"epaas-discovery",
+    "domain":"istio-system",
+    "sortedId":"NjA3ODVhODMtZDMyYy00ZWMyLWE4NjktZjcwZDMwMjNkYjQ3",
+    "log":"spiffluster.local/ns/istio-system/sa/istio-galley-service-account"
+}
+

5.部署
+最后,在node节点上添加logging-infra-fluentd: "true"的标签,就可以在namespace为openshift-logging中看到节点的收集器了。

+
logging-fluentd-29p8z              1/1       Running   0          6d
+logging-fluentd-bpkjt              1/1       Running   0          6d
+logging-fluentd-br9z5              1/1       Running   0          6d
+logging-fluentd-dkb24              1/1       Running   1          5d
+logging-fluentd-lbvbw              1/1       Running   0          6d
+logging-fluentd-nxmk9              1/1       Running   1          5d
+

6.关于ip
+业务方不仅仅想要podName,同时还有对ip的需求,控制台方式正常上是没有记录ip的,所以这算是一个难点中的难点,我们在kubernetes_metadata_common.rb的kubernetes_metadata中添加了 'pod_ip' => pod_object['status']['podIP'],最终是有些有ip,有些没有ip,这个问题我们继续排查。

+

三、Sidecar模式

+


+这种方式的好处是能够获取日志的文件名、容器的ip地址等,并且配置性比较高,能够很好的进行一系列定制化的操作,比如使用log-pilot或者filebeat或者其他的收集器,还能定制一些特定的字段,比如文件名、ip地址等。
+sidecar模式用来解决日志收集的问题的话,需要将日志目录挂载到宿主机的目录上,然后再mount到收集agent的目录里面,以达到文件共享的目的,默认情况下,使用emptydir来实现文件共享的目的,这里简单介绍下emptyDir的作用。
+EmptyDir类型的volume创建于pod被调度到某个宿主机上的时候,而同一个pod内的容器都能读写EmptyDir中的同一个文件。一旦这个pod离开了这个宿主机,EmptyDir中的数据就会被永久删除。所以目前EmptyDir类型的volume主要用作临时空间,比如Web服务器写日志或者tmp文件需要的临时目录。
+日志如果丢失的话,会对业务造成的影响不可估量,所以,我们使用了尚未成熟的subPathExpr来实现,即挂载到宿主的固定目录/data/logs下,然后是namespace,deploymentName,podName,再然后是日志文件,合成一块便是/data/logs/${namespace}/${deploymentName}/${podName}/xxx.log。
+具体的做法就不在演示了,这里只贴一下yaml文件。

+
apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: xxxx
+  namespace: element-dev
+spec:
+  template:
+    spec:
+      volumes:
+        - name: host-log-path-0
+          hostPath:
+            path: /data/logs/element-dev/xxxx
+            type: DirectoryOrCreate
+      containers:
+        - name: xxxx
+          image: 'xxxxxxx'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)
+        - name: xxxx-elog-agent
+          image: 'agent'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)
+

fluent.conf的配置文件由于保密关系就不贴了,收集后的一条数据如下:

+
{
+    "_fileName":"/data/work/logs/xxx_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"namespace",
+    "_ip":"10.128.93.31",
+    "_podName":"xxxx-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}
+

四、总结

+

总的来说,daemonset方式比较简单,而且适合更加适合微服务化,当然,不是完美的,比如业务方想把业务日志打到控制台上,但是同时也想知道jvm的日志,这种情况下或许sidecar模式更好。但是sidecar也有不完美的地方,每个pod里都要存在一个日志收集的agent实在是太消耗资源了,而且很多问题也难以解决,比如:主容器挂了,agent还没收集完,就把它给kill掉,这个时候日志怎么处理,业务会不会受到要杀掉才能启动新的这一短暂过程的影响等。所以,我们实际使用中首选daemonset方式,但是提供了sidecar模式让用户选择。

+

参考:
+1.Kubernetes日志官方文档
+2.Kubernetes日志采集Sidecar模式介绍
+3.Docker日志收集最佳实践

+]]>
+ 2024-02-24T05:12:56.000Z +
+ + Kubernetes之request 和 limit详解 + http://www.wenzhihuai.com/kubernetes/request_limit.html + + 2024-02-16T11:46:28.000Z + 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

+
resources:  
+    limits:    
+        cpu: "1"
+        memory: "500Mi"
+    requests:    
+        cpu: "100m"
+        memory: "1000Mi"
+
]]>
+ 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

+
resources:  
+    limits:    
+        cpu: "1"
+        memory: "500Mi"
+    requests:    
+        cpu: "100m"
+        memory: "1000Mi"
+

下面我们首先来了解一下上面这段 yaml 文件中字段的含义:requests 和 limits:

+

requests 定义了对应的容器所需要的最小资源量。
+limits 定义了对应容器最大可以消耗的资源上限。
+cpu 等于1一般等同于1CPU 核心,1个VCPU或者一个超线程,具体要看服务器的CPU。而 limits 这里设置的 100m 则叫做100毫核,100m就表示0.1个核,所以这里也可以用0.1代替。
+memory 等于500Mi,(备注:1Mi=10241024;1M=10001000)
+接下来我们来初步理解 requests 和 limits 这两个资源限制类型,在 Kubernetes 对 CPU 和内存资源限额的设计,通常是指用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。所以一般来说,在调度的时候 requests 比较重要,在运行时 limits 比较重要。

+

而对应实际的业务场景来说,以 java 应用为例,requests 对应的就是JVM虚拟机所需资源的最小值,而 limits 对应的就是 JVM 虚拟机所能够使用的资源最大值。以内存资源为例一般就是指:Xms 和 Xmx,如果 requests 值设置的小于JVM虚拟机 Xms 的值,那么就会导致 Pod 内存溢出,从而导致 Pod 被杀掉,而后重新创建一个Pod。

+

那么如果 CPU 资源使用超过了 limits,Pod会不会被杀掉呢?答案是不会,但是被限制。如果没有设置 limits ,Pod 可以使用全部空闲的资源。另外如果设置了 limits而没有设置 requests 时,Kubernetes 默认会将 requests 等于 limits。

+

这里通常还会将 requests 和 limits 描述的资源分为两类:可压缩资源(compressible resources) 和不可压缩资源(incompressible resources)。这里不难看出CPU这类型资源为可压缩资源,而内存这类型资源为不可压缩资源。所以合理设置不可压缩资源的limits值就相当重要了。

+]]>
+ 2024-02-16T11:46:28.000Z +
+ + JVM调优思路 + http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html + + 2024-02-16T11:46:28.000Z + 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

+

1.监控发现问题
+2.工具分析问题
+3.性能调优
+下面开始一步步讲解

+

一、监控发现问题

+

通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:

+

GC频繁
+CPU负载过高
+OOM
+内存泄露
+死锁
+程序响应时间较长

]]>
+ 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

+

1.监控发现问题
+2.工具分析问题
+3.性能调优
+下面开始一步步讲解

+

一、监控发现问题

+

通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:

+

GC频繁
+CPU负载过高
+OOM
+内存泄露
+死锁
+程序响应时间较长

+

二、工具分析问题

+

参考

+

1.【JVM调优】如何进行JVM调优?一篇文章就够了!

+]]>
+ 2024-02-16T11:46:28.000Z +
+ + 主流GC收集器采用的算法 + http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html + + 2024-02-16T11:46:28.000Z + + + +垃圾回收器 +采用的GC算法 +代次 + + + + +Serial +复制 +新生代 + + +Parallel +复制 +新生代 + + +ParNew +复制 +新生代 + + +CMS (Concurrent Mark-Sweep) +标记-清除 +老年代 + + +G1 (Garbage-First) +标记-整理 +老年代 + + +ZGC (Z Garbage Collector) +标记-整理 +老年代 + + +Shenandoah +标记-复制(独立的全局复制阶段) +老年代 + + +]]> + | 垃圾回收器 | 采用的GC算法 | 代次 |
+|

+]]>
+ 2024-02-16T09:55:09.000Z +
+ + Java内存模型(JMM) + http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html + + 2024-02-16T09:55:09.000Z + 本文转载自深入理解JVM-内存模型(jmm)和GC

+

1 CPU和内存的交互

+

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

+

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型

]]>
+ 本文转载自深入理解JVM-内存模型(jmm)和GC

+

1 CPU和内存的交互

+

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

+

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型

+]]>
+ 2024-02-16T09:22:35.000Z +
+ + 引用计数和根可达算法 + http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html + + 2024-02-16T09:22:35.000Z + 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

+

硬件角度来看什么叫做垃圾?

+

计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit位构成的。同时,内存需要在通电状态保持按周期频率的刷新,才能够维持数据的状态。

]]>
+ 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

+

硬件角度来看什么叫做垃圾?

+

计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit位构成的。同时,内存需要在通电状态保持按周期频率的刷新,才能够维持数据的状态。

+

在内存中,某些内存区域(bit位)被使用过了(用来存储对象或者数据)会被打上标记表示已经被使用过了,分配新内存空间的时候就计算机硬件就不会使用这些被标记过的内存区域。通常,这些给内存区域打上标记的活儿一般由操作系统的系统调用来完成,例如C语言中的 allocate()函数调用的系统函数。

+

在面向对象的语言中,如果程序员新建了一个对象,那么操作系统会为其分配相应的内存区域,且该部分的内存区域会被打上标记说明已经被使用过了,这样操作系统就不会向该区域写入新的数据。

+

然而,如果这个标记没有被程序员释放(C语言中调用free()来释放内存区域),那么该内存区域会一直被标记成已使用,如果整个内存都被标记成为了已使用,那么当操作系统想要再分配内存的时候就会失败,这个时候只有重启电脑来解决问题了。

+

所以说,对于内存区域中不再使用的对象,我们需要释放掉其对应的内存区域,方便新的对象创建时有空间为其分配。而这些程序中不再被使用的对象就被称为“垃圾”,“垃圾”往往对应着内存中一块儿需要被释放的区域。

+

Java中的垃圾

+

Java中"垃圾"通常指的是不再被程序使用和引用的对象,具体表现在没有被栈、JNI指针和永久代对象所引用的对象。Java作为一种面向对象的编程语言,它使用自动内存管理机制,其中垃圾收集器负责检测和回收不再被程序引用的对象,以释放它们占用的内存空间。以下是一些导致对象成为垃圾的常见情况:

+
    +
  • 无引用对象: 当一个对象没有任何引用指向它时,它就变得不可达,成为垃圾,Java的垃圾收集器会识别这样的对象,并将它们回收。
  • +
  • 引用循环: 如果一组对象彼此引用形成一个循环,而这个循环与程序的其他部分没有引用相连,那么这个循环中的对象就会成为垃圾。Java的垃圾收集器通过识别引用循环并处理它们来防止内存泄漏。
  • +
  • 显式置空引用: 如果程序员显式地将一个引用置空(null),而没有其他引用指向相同的对象,那么该对象就变成了垃圾。
  • +
+

垃圾收集器周期性地运行,并识别和回收这些垃圾对象,释放其内存中对应的区域以确保内存能够得到有效利用,这种自动的内存管理机制就叫做垃圾回收。

+

如何寻找垃圾?

+

引用计数(Reference Count)

+
img
img
+

引用计数算法是一种垃圾标记,其核心思想是通过维护对象的引用计数来判断对象是否可以被回收。每个对象都有一个关联的引用计数,表示当前有多少个指针引用它。当引用计数为零时,意味着没有指针再引用该对象,因此可以安全地回收该对象的内存。

+

其实引用计数算法的核心思想就是,只要有对象引用我,那么就说明我是有用的,我还不需要被回收,反正,我就是没有用的对象,那么我和我的子对象都应该被回收掉。这里我们说的对象都是堆上的对象,一般是堆上的内存空间需要程序员手动回收,而栈上的内存空间则由操作系统自行回收。由于栈上的对象是操作系统自行管理和回收的,因此栈上的对象以及一些静态对象始终都是出于存活的状态,因此,堆中存活的对象至少会有一个引用(指针)指向它。

+

但是这样会存在着一个问题,就是对象中的引用关系形成了环状——循环引用,这种情况下环内所有对象的引用都是>1的,这样一来环内的所有都无法被回收从而造成“内存泄漏”。这是引用算法最主要的局限性,也是为什么JVM不采用循环计数的方法来标记垃圾的原因。

+

根可达算法(Root Search)

+

由于引用计数算法无法解决“循环引用”的问题,无可避免的会造成内存泄露,因此,Java没有采用引用计数算法来寻找垃圾。而是采用了一种从GC Roots开始搜索存活对象的垃圾标记算法——根可达算法。

+
img
img
+

哪些是GC Root?

+

| GC Roots Source | Description |
+|

+]]>
+ 2024-02-16T09:22:35.000Z +
+ + synchronized + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html + + 2024-02-17T16:08:07.000Z + 偏向锁在JDK 15后已经废弃

+

一、什么是synchronized

+

关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。

+
img
img
]]>
+ 偏向锁在JDK 15后已经废弃

+

一、什么是synchronized

+

关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。

+
img
img
+

学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:

+
    +
  • 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  • +
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • +
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  • +
+

关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。

+

具体使用规则如下:
+在这里插入图片描述

+

二、Synchronized 原理

+

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

+

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

+

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

+

流程图如下:
+在这里插入图片描述

+

通过一段代码来演示:

+
public static void main(String[] args) {
+    synchronized (Synchronize.class){
+        System.out.println("Synchronize");
+    }
+}
+12345
+

使用javap -c Synchronize可以查看编译之后的具体信息。
+在这里插入图片描述
+可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

+

在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
+在这里插入图片描述
+synchronized的特点:
+在这里插入图片描述

+

三、Synchronized 优化

+

从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

+

3.1 偏向锁

+

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

+

3.1.1 偏向锁的获取过程

+

(1)访问Mark Word中偏向锁的标识是否设置成“1”,锁标志位是否为“01”——确认为可偏向状态。
+(2)如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。
+(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

+

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
+(5)执行同步代码。

+

3.1.2 偏向锁的释放

+

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

+

3.2 轻量锁

+

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

+

3.2.1 轻量级锁的加锁过程

+

(1)在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
+(2)拷贝对象头中的Mark Word复制到锁记录中。
+(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
+(4)如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
+(5)如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

+

3.2.2 轻量级锁的解锁过程

+

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
+(2)如果替换成功,整个同步过程完成。
+(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

+

3.3 其他优化

+

适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6 加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

+

通过--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。
+通过--XX:PreBlockSpin修改自旋次数,默认值是10次。

+

锁消除:锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

+

锁粗化:我们在写代码时推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

+

注意:在大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

+

四、扩展

+

其他控制并发/线程同步方式还有 Lock/ReentrantLock。

+

4.1 Synchronized 和 ReenTrantLock 的对比

+

① 两者都是可重入锁

+

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

+

② synchronized依赖于JVM而ReenTrantLock依赖于API

+

synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

+

③ ReenTrantLock比synchronized增加了一些高级功能

+

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

+
    +
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • +
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的,可以通过ReenTrantLoc类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • +
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。
  • +
+

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

+

④ 性能已不是选择标准

+

在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平。所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

+

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销。

+
// CAS的操作参数
+// 内存位置(A)
+// 预期原值(B)
+// 预期新值(C)
+
+// 使用CAS解决并发的原理:
+// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
+// 2. 通过死循环,以不断尝试尝试更新的方式实现并发
+
+// 伪代码如下
+public boolean compareAndSwap(long memoryA, int oldB, int newC){
+    if(memoryA.get() == oldB){
+        memoryA.set(newC);
+        return true;
+    }
+    return false;
+}
+1234567891011121314151617
+

具体使用当中CAS有个先检查后执行的操作,而这种操作在 Java 中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
+具体过程:

+
    +
  1. CAS在Java中的体现为Unsafe类。
  2. +
  3. Unsafe类会通过C++直接获取到属性的内存地址。
  4. +
  5. 接下来CAS由C++的Atomic::cmpxchg系列方法实现。
  6. +
+

AtomicInteger的 i++ 与 i-- 是典型的CAS应用,通过compareAndSet & 一个死循环实现。

+
private volatile int value; 
+/** 
+* Gets the current value. 
+* 
+* @return the current value 
+*/ 
+public final int get() { 
+   return value; 
+} 
+/** 
+* Atomically increments by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndIncrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current + 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+} 
+
+/** 
+* Atomically decrements by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndDecrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current - 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+}
+123456789101112131415161718192021222324252627282930313233343536
+

以上内容引用自学习指南
+总的来说:
+1、synchronized是java关键字,而Lock是java中的一个接口
+2、synchronized会自动释放锁,而Lock必须手动释放锁
+3、synchronized是不可中断的,Lock可以中断也可以不中断
+4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
+5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
+6、Lock可以使用读锁提高多线程读效率
+7、synchronized是非公平锁,ReentranLock可以控制是否公平锁

+

4.2 Synchronized 与 ThreadLocal 的对比

+

Synchronized 与 ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:

+
    +
  1. Synchronized关键字主要解决多线程共享数据同步问题;ThreadLocal主要解决多线程中数据因并发产生不一致问题。
  2. +
  3. Synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  4. +
+

4.3 synchronized与volatile区别

+

| volatile | synchronized |
+|

+]]>
+ 2024-02-16T09:22:35.000Z +
+ + mini主机 + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/ + + 2024-11-29T18:45:10.000Z + 2024-02-16T04:00:26.000Z + + + 个人网站 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/ + + 2024-05-11T18:24:35.000Z + jofjweoiaejof

+]]>
+ jofjweoiaejof

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + JVM + http://www.wenzhihuai.com/java/JVM/ + + 2024-02-16T04:00:26.000Z + 2024-02-16T04:00:26.000Z + + + CMS + http://www.wenzhihuai.com/java/JVM/cms.html + + 2024-02-16T09:22:35.000Z + JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

+

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

+

内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提前触发Full GC。

+

预测性差:CMS垃圾回收器的暂停时间和CPU资源消耗都很难预测,这可能会对系统的性能造成影响。

]]>
+ JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

+

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

+

内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提前触发Full GC。

+

预测性差:CMS垃圾回收器的暂停时间和CPU资源消耗都很难预测,这可能会对系统的性能造成影响。

+

维护复杂:CMS垃圾回收器的代码相对复杂,需要更多的维护工作。

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + G1 + http://www.wenzhihuai.com/java/JVM/g1.html + + 2024-02-16T09:55:09.000Z + 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

+

G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点:

+
    +
  • 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并发的方式,充分利用多核处理器的优势,提高了垃圾回收的效率。
  • +
  • 分代回收: G1垃圾回收器依旧采用分代回收的思想,但是和CMS等分代回收算法不同,G1不是将整个堆内存划分为年轻代、老年代和元空间。而是将堆内存划分为一个个固定大小的region,每个region可以属于年轻代或老年代。垃圾回收的基本单位是region,而不是整个堆,这使得垃圾回收更加灵活。
  • +
  • 增量回收: G1采用增量回收的方式,将整个垃圾回收过程分解为多个阶段进行,这样更有利于分散垃圾回收的压力,减小每次暂停的时间,提高系统的响应性。
  • +
  • Compacting回收: 与CMS回收器不同,G1是一种compacting回收器,其回收的内存空间是连续的。这样就可以避免CMS收集器由于不连续内存空间造成的所需堆空间更大和浮动垃圾的问题。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • +
  • 软实时: G1回收器具有软实时(soft real-time)的特性,用户可以指定垃圾回收时间的限时。虽然G1会努力在限定时间内完成垃圾回收,但并不保证每次都能在时限内完成。通过设定合理的目标,可以使大部分垃圾回收时间都在规定的时限内完成。
  • +
]]>
+ 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

+

G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点:

+
    +
  • 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并发的方式,充分利用多核处理器的优势,提高了垃圾回收的效率。
  • +
  • 分代回收: G1垃圾回收器依旧采用分代回收的思想,但是和CMS等分代回收算法不同,G1不是将整个堆内存划分为年轻代、老年代和元空间。而是将堆内存划分为一个个固定大小的region,每个region可以属于年轻代或老年代。垃圾回收的基本单位是region,而不是整个堆,这使得垃圾回收更加灵活。
  • +
  • 增量回收: G1采用增量回收的方式,将整个垃圾回收过程分解为多个阶段进行,这样更有利于分散垃圾回收的压力,减小每次暂停的时间,提高系统的响应性。
  • +
  • Compacting回收: 与CMS回收器不同,G1是一种compacting回收器,其回收的内存空间是连续的。这样就可以避免CMS收集器由于不连续内存空间造成的所需堆空间更大和浮动垃圾的问题。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • +
  • 软实时: G1回收器具有软实时(soft real-time)的特性,用户可以指定垃圾回收时间的限时。虽然G1会努力在限定时间内完成垃圾回收,但并不保证每次都能在时限内完成。通过设定合理的目标,可以使大部分垃圾回收时间都在规定的时限内完成。
  • +
+

G1垃圾回收器以其创新性的设计和优越的性能特点,逐渐成为Java应用程序中首选的垃圾回收器之一。通过分代、增量、并行、并发等多种技术手段的结合,G1回收器在处理大内存和多核处理器的环境下表现出色,为Java应用程序提供了更好的性能和响应能力。

+

G1的内存模型和分代策略

+

G1收集器相关参数

+

| 参数 | 默认值 | 描述 |
+|

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + ZGC + http://www.wenzhihuai.com/java/JVM/zgc.html + + 2024-02-16T11:46:28.000Z + 本文转载自12 张图带你彻底理解 ZGC

+

ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。

+

ZGC 有 3 个重要特性:

]]>
+ 本文转载自12 张图带你彻底理解 ZGC

+

ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。

+

ZGC 有 3 个重要特性:

+
    +
  • 暂停时间不会超过 10 ms。
  • +
+
+

JDK 16 发布后,GC 暂停时间已经缩小到 1 ms 以内,并且时间复杂度是 o(1),这也就是说 GC 停顿时间是一个固定值了,并不会受堆内存大小影响。
+下面图片来自:https://malloc.se/blog/zgc-jdk16

+
+
img
img
+
    +
  • 最大支持 16TB 的大堆,最小支持 8MB 的小堆。
  • +
  • 跟 G1 相比,对应用程序吞吐量的影响小于 15 %。
  • +
+

1 内存多重映射

+

内存多重映射,就是使用 mmap 把不同的虚拟内存地址映射到同一个物理内存地址上。如下图:

+
img
img
+

ZGC 为了更灵活高效地管理内存,使用了内存多重映射,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。

+

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。

+

Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

+

2 染色指针

+

2.1 三色标记回顾

+

我们知道 G1 垃圾收集器使用了三色标记,这里先做一个回顾。下面是一个三色标记过程中的对象引用示例图:

+
img
img
+

总共有三种颜色,说明如下:

+
    +
  • 白色:本对象还没有被标记线程访问过。
  • +
  • 灰色:本对象已经被访问过,但是本对象引用的其他对象还没有被全部访问。
  • +
  • 黑色:本对象已经被访问过,并且本对象引用的其他对象也都被访问过了。
  • +
+

三色标记的过程如下:

+
    +
  1. 初始阶段,所有对象都是白色。
  2. +
  3. 将 GC Roots 直接引用的对象标记为灰色。
  4. +
  5. 处理灰色对象,把当前灰色对象引用的所有对象都变成灰色,之后将当前灰色对象变成黑色。
  6. +
  7. 重复步骤 3,直到不存在灰色对象为止。
  8. +
+

三色标记结束后,白色对象就是没有被引用的对象(比如上图中的 H 和 G),可以被回收了。

+

2.2 染色指针

+

ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如下图:

+
img
img
+

前 62位保存了 GC 信息,最后两位保存了锁标志。

+

ZGC 的一大创举是将 GC 信息保存在了染色指针上。染色指针是一种将少量信息直接存储在指针上的技术。在 64 位 JVM 中,对象指针是 64 位,如下图:

+
img
img
+

在这个 64 位的指针上,高 16 位都是 0,暂时不用来寻址。剩下的 48 位支持的内存可以达到 256 TB(2 ^48),这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 48 位都用来保存对象信息,而是用高 4 位保存了四个标志位,这样 ZGC 可以管理的最大内存可以达到 16 TB(2 ^ 44)。

+

通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。

+

无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。

+

3 内存布局

+

首先我们回顾一下 G1 垃圾收集器的内存布局。G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:

+
img
img
+

跟 G1 类似,ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的。不同的是,ZGC 的 Region 支持动态地创建和销毁,并且 Region 的大小不是固定的,包括三种类型的 Region :

+
    +
  • Small Region:2MB,主要用于放置小于 256 KB 的小对象。
  • +
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
  • +
  • Large Region:N * 2MB。这个类型的 Region 是可以动态变化的,不过必须是 2MB 的整数倍,最小支持 4 MB。每个 Large Region 只放置一个大对象,并且是不会被重分配的。
  • +
+

4 读屏障

+

读屏障类似于 Spring AOP 的前置增强,是 JVM 向应用代码中插入一小段代码,当应用线程从堆中读取对象的引用时,会先执行这段代码。注意:只有从堆内存中读取对象的引用时,才会执行这个代码。下面代码只有第一行需要加入读屏障。

+
Object o = obj.FieldA
+Object p = o //不是从堆中读取引用
+o.dosomething() //不是从堆中读取引用
+int i =  obj.FieldB //不是引用类型
+

读屏障在解释执行时通过 load 相关的字节码指令加载数据。作用是在对象标记和转移过程中,判断对象的引用地址是否满足条件,并作出相应动作。如下图:

+
img
img
+

标记、转移和重定位这些过程请看下一节。

+
+

读屏障会对应用程序的性能有一定影响,据测试,对性能的最高影响达到 4%,但提高了 GC 并发能力,降低了 STW。

+
+

5 GC 过程

+

前面已经讲过,ZGC 使用内存多重映射技术,把物理内存映射为 Marked0、Marked1 和 Remapped 三个地址视图,利用地址视图的切换,ZGC 实现了高效的并发收集。

+

ZGC 的垃圾收集过程包括标记、转移和重定位三个阶段。如下图:

+
img
img
+

ZGC 初始化后,整个内存空间的地址视图被设置为 Remapped。

+

5.1 初始标记

+

从 GC Roots 出发,找出 GC Roots 直接引用的对象,放入活跃对象集合,这个过程需要 STW,不过 STW 的时间跟 GC Roots 数量成正比,耗时比较短。

+

5.2 并发标记

+

并发标记过程中,GC 线程和 Java 应用线程会并行运行。这个过程需要注意下面几点:

+
    +
  • GC 标记线程访问对象时,如果对象地址视图是 Remapped,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理。
  • +
  • 标记过程中Java 应用线程新创建的对象会直接进入 Marked0 视图。
  • +
  • 标记过程中Java 应用线程访问对象时,如果对象的地址视图是 Remapped,就把对象地址视图切换到 Marked0,可以参考前面讲的读屏障。
  • +
  • 标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。
  • +
+

标记阶段的活跃视图也可能是 Marked1,为什么会采用两个视图呢?

+

这里采用两个视图是为了区分前一次标记和这一次标记。如果这次标记的视图是 Marked0,那下一次并发标记就会把视图切换到 Marked1。这样做可以配合 ZGC 按照页回收垃圾的做法。如下图:

+
img
img
+

第二次标记的时候,如果还是切换到 Marked0,那么 2 这个对象区分不出是活跃的还是上次标记过的。如果第二次标记切换到 Marked1,就可以区分出了。

+

这时 Marked0 这个视图的对象就是上次标记过程被标记过活跃,转移的时候没有被转移,但这次标记没有被标记为活跃的对象。Marked1 视图的对象是这次标记被标记为活跃的对象。Remapped 视图的对象是上次垃圾回收发生转移或者是被 Java 应用线程访问过,本次垃圾回收中被标记为不活跃的对象。

+

5.3 再标记

+

并发标记阶段 GC 线程和 Java 应用线程并发执行,标记过程中可能会有引用关系发生变化而导致的漏标记问题。再标记阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软应用,虚引用等)进行并行标记。

+

这个阶段需要 STW,但是需要标记的对象少,耗时很短。

+

5.4 初始转移

+

转移就是把活跃对象复制到新的内存,之前的内存空间可以被回收。

+

初始转移需要扫描 GC Roots 直接引用的对象并进行转移,这个过程需要 STW,STW 时间跟 GC Roots 成正比。

+

5.5 并发转移

+

并发转移过程 GC 线程和 Java 线程是并发进行的。上面已经讲过,转移过程中对象视图会被切回 Remapped 。转移过程需要注意以下几点:

+
    +
  • 如果 GC 线程访问对象的视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • +
  • 如果 GC 线程访问对象的视图是 Remapped,说明被其他 GC 线程处理过,跳过不再处理。
  • +
  • 并发转移过程中 Java 应用线程创建的新对象地址视图是 Remapped。
  • +
  • 如果 Java 应用线程访问的对象被标记为活跃并且对象视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • +
+

5.6 重定位

+

转移过程对象的地址发生了变化,在这个阶段,把所有指向对象旧地址的指针调整到对象的新地址上。

+

6 垃圾收集算法

+

ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象。如下图:

+
img
img
+

6.1 JDK 16 之前

+

在 JDK 16 之前,ZGC 会预留(Reserve)一块儿堆内存,这个预留内存不能用于 Java 线程的内存分配。即使从 Java 线程的角度看堆内存已经满了也不能使用 Reserve,只有 GC 过程中搬移存活对象的时候才可以使用。如下图:

+
img
img
+

这样做的好处是算法简单,非常适合并行收集。但这样做有几个问题:

+
    +
  • 因为有预留内存,能给 Java 线程分配的堆内存小于 JVM 声明的堆内存。
  • +
  • Reserve 仅仅用于存放 GC 过程中搬移的对象,有点内存浪费。
  • +
  • 因为 Reserve 不能给 GC 过程中搬移对象的 Java 线程使用,搬移线程可能会因为申请不到足够内存而不能完成对象搬移,这返回过来又会导致应用程序的 OOM。
  • +
+

6.2 JDK 16 改进

+

JDK 16 发布后,ZGC 支持就地搬移对象(G1 在 Full GC 的时候也是就地搬移)。这样做的好处是不用预留空闲内存了。如下图:

+
img
img
+

不过就地搬移也有一定的挑战。比如:必须考虑搬移对象的顺序,否则可能会覆盖尚未移动的对象。这就需要 GC 线程之间更好的进行协作,不利于并发收集,同时也会导致搬移对象的 Java 线程需要考虑什么可以做什么不可以做。

+

为了获得更好的 GC 表现,JDK 16 在支持就地搬移的同时,也支持预留(Reserve)堆内存的方式,并且 ZGC 不需要真的预留空闲的堆内存。默认情况下,只要有空闲的 region,ZGC 就会使用预留堆内存的方式,如果没有空闲的 region,否则 ZGC 就会启用就地搬移。如果有了空闲的 region, ZGC 又会切换到预留堆内存的搬移方式。

+

7 总结

+

内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。

+

ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。

+

ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。

+

参考:

+

1.https://wiki.openjdk.java.net/display/zgc

+

2.https://openjdk.java.net/jeps/304

+

3.https://openjdk.java.net/jeps/376

+

4.https://malloc.se/blog/zgc-jdk16

+

5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg

+

6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw

+

7.https://www.jianshu.com/p/664e4da05b2c

+

8.https://www.cnblogs.com/jimoer/p/13170249.html

+

9.https://www.jianshu.com/p/12544c0ad

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + SpringBoot + http://www.wenzhihuai.com/java/SpringBoot/ + + 2024-02-16T04:00:26.000Z + 2024-02-16T04:00:26.000Z + + + I/O + http://www.wenzhihuai.com/java/io/ + + 2024-03-10T14:39:30.000Z + 2024-02-16T04:00:26.000Z + + + 线程 + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/ + + 2024-02-16T04:00:26.000Z + 2024-02-16T04:00:26.000Z + + + volatile + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html + + 2024-02-16T17:21:55.000Z + Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

]]>
+ Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

+
image-20240217004404932
image-20240217004404932
+

可见性

+

有序性

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + 网络 + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/ + + 2024-02-16T04:00:26.000Z + 2024-02-16T04:00:26.000Z + + + DevOps + http://www.wenzhihuai.com/kubernetes/devops/ + + 2024-02-16T09:22:35.000Z + jofjweoiaejof

+]]>
+ jofjweoiaejof

+]]>
+ 2024-02-16T04:00:26.000Z +
+ + RedissonLock分布式锁源码分析 + http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html + + 2024-02-06T07:22:35.000Z + 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
+目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

]]>
+ 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
+目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

+

一、实现分布式锁的要求

+
    +
  1. 互斥性。在任何时候,当且仅有一个客户端能够持有锁。
  2. +
  3. 不能有死锁。持有锁的客户端崩溃后,后续客户端能够加锁。
  4. +
  5. 容错性。大部分Redis或者ZooKeeper节点能够正常运行。
  6. +
  7. 加锁解锁相同。加锁的客户端和解锁的客户端必须为同一个客户端,不能让其他的解锁了。
  8. +
+

二、Redis实现分布式锁的常用命令

+

1.SETNX key val
+当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
+2.expire key timeout
+为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
+3.delete key
+删除key,此处用来解锁使用。
+4.HEXISTS key field
+当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。
+5.HINCRBY key field increment
+将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。

+

三、常见写法

+

由上面三个命令,我们可以很快的写一个分布式锁出来:

+
if (conn.setnx("lock","1").equals(1L)) { 
+    //do something
+    return true; 
+} 
+return false;
+

但是这样也会存在问题,如果获取该锁的客户端挂掉了怎么办?一般而言,我们可以通过设置expire的过期时间来防止客户端挂掉所带来的影响,可以降低应用挂掉所带来的影响,不过当时间失效的时候,要保证其他客户端只有一台能够获取。

+

四、Redisson

+

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。——摘自百度百科

+

4.1 测试例子

+

先在pom引入Redssion

+
<dependency>
+    <groupId>org.redisson</groupId>
+    <artifactId>redisson</artifactId>
+    <version>3.6.1</version>
+</dependency>
+

起100个线程,同时对count进行操作,每次操作减1,加锁的时候能够保持顺序输出,不加的话为随机。

+
public class RedissonTest implements Runnable {
+    private static RedissonClient redisson;
+    private static int count = 10000;
+
+    private static void init() {
+        Config config = new Config();
+        config.useSingleServer()
+                .setAddress("redis://119.23.46.71:6340")
+                .setPassword("root")
+                .setDatabase(10);
+        redisson = Redisson.create(config);
+    }
+
+    @Override
+    public void run() {
+        RLock lock = redisson.getLock("anyLock");
+        lock.lock();
+        count--;
+        System.out.println(count);
+        lock.unlock();
+    }
+
+    public static void main(String[] args) {
+        init();
+        for (int i = 0; i < 100; i++) {
+            new Thread(new RedissonTest()).start();
+        }
+    }
+}
+

输出结果(部分结果):

+
...
+9930
+9929
+9928
+9927
+9926
+9925
+9924
+9923
+9922
+9921
+
+...
+

去掉lock.lock()和lock.unlock()之后(部分结果):

+
...
+9930
+9931
+9933
+9935
+9938
+9937
+9940
+9941
+9942
+9944
+9947
+9946
+9914
+...
+

五、RedissonLock源码分析

+

最新版的Redisson要求redis能够支持eval的命令,否则无法实现,即Redis要求2.6版本以上。在lua脚本中可以调用大部分的Redis命令,使用脚本的好处如下:
+(1)减少网络开销:在Redis操作需求需要向Redis发送5次请求,而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
+(2)原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
+(3)复用:客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。

+

5.1 使用到的全局变量

+

全局变量:
+expirationRenewalMap:存储entryName和其过期时间,底层用的netty的PlatformDependent.newConcurrentHashMap()
+internalLockLeaseTime:锁默认释放的时间:30 * 1000,即30秒
+id:UUID,用作客户端的唯一标识
+PUBSUB:订阅者模式,当释放锁的时候,其他客户端能够知道锁已经被释放的消息,并让队列中的第一个消费者获取锁。使用PUB/SUB消息机制的优点:减少申请锁时的等待时间、安全、 锁带有超时时间、锁的标识唯一,防止死锁 锁设计为可重入,避免死锁。
+commandExecutor:命令执行器,异步执行器

+

5.2 加锁

+

以lock.lock()为例,调用lock之后,底层使用的是lockInterruptibly,之后调用lockInterruptibly(-1, null);

+
+
+
+

(1)我们来看一下lockInterruptibly的源码,如果别的客户端没有加锁,则当前客户端进行加锁并且订阅,其他客户端尝试加锁,并且获取ttl,然后等待已经加了锁的客户端解锁。

+
//leaseTime默认为-1
+public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
+    long threadId = Thread.currentThread().getId();//获取当前线程ID
+    Long ttl = tryAcquire(leaseTime, unit, threadId);//尝试加锁
+    // 如果为空,当前线程获取锁成功,否则已经被其他客户端加锁
+    if (ttl == null) {
+        return;
+    }
+    //等待释放,并订阅锁
+    RFuture<RedissonLockEntry> future = subscribe(threadId);
+    commandExecutor.syncSubscription(future);
+    try {
+        while (true) {
+            // 重新尝试获取锁
+            ttl = tryAcquire(leaseTime, unit, threadId);
+            // 成功获取锁
+            if (ttl == null) {
+                break;
+            }
+            // 等待锁释放
+            if (ttl >= 0) {
+                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
+            } else {
+                getEntry(threadId).getLatch().acquire();
+            }
+        }
+    } finally {
+        // 取消订阅
+        unsubscribe(future, threadId);
+    }
+}
+

(2)下面是tryAcquire的实现,调用的是tryAcquireAsync

+
    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
+        return get(tryAcquireAsync(leaseTime, unit, threadId));
+    }
+

(3)下面是tryAcquireAsync的实现,异步尝试进行加锁,尝试加锁的时候leaseTime为-1。通常如果客户端没有加锁成功,则会进行阻塞,leaseTime为锁释放的时间。

+
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
+    if (leaseTime != -1) {   //在lock.lock()的时候,已经声明了leaseTime为-1,尝试加锁
+        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
+    }
+    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
+    //监听事件,订阅消息
+    ttlRemainingFuture.addListener(new FutureListener<Long>() {
+        @Override
+        public void operationComplete(Future<Long> future) throws Exception {
+            if (!future.isSuccess()) {
+                return;
+            }
+            Long ttlRemaining = future.getNow();
+            // lock acquired
+            if (ttlRemaining == null) {
+                //获取新的超时时间
+                scheduleExpirationRenewal(threadId);
+            }
+        }
+    });
+    return ttlRemainingFuture;  //返回ttl时间
+}
+

(4)下面是tryLockInnerAsyncy异步加锁,使用lua能够保证操作是原子性的

+
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
+    internalLockLeaseTime = unit.toMillis(leaseTime);
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
+              "if (redis.call('exists', KEYS[1]) == 0) then " +
+                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
+                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "return redis.call('pttl', KEYS[1]);",
+                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
+}
+

参数
+KEYS[1](getName()) :需要加锁的key,这里需要是字符串类型。
+ARGV[1](internalLockLeaseTime) :锁的超时时间,防止死锁
+ARGV[2](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId
+lua脚本解释

+
--检查key是否被占用了,如果没有则设置超时时间和唯一标识,初始化value=1
+if (redis.call('exists', KEYS[1]) == 0) then
+  redis.call('hset', KEYS[1], ARGV[2], 1);
+  redis.call('pexpire', KEYS[1], ARGV[1]);
+  return nil; 
+end; 
+--如果锁重入,需要判断锁的key field 都一致情况下 value 加一 
+if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
+  redis.call('hincrby', KEYS[1], ARGV[2], 1);
+  --锁重入重新设置超时时间  
+  redis.call('pexpire', KEYS[1], ARGV[1]); 
+  return nil; 
+end;
+--返回剩余的过期时间
+return redis.call('pttl', KEYS[1]);
+

(5)流程图

+
+
+
+

5.3 解锁

+

解锁的代码很简单,大意是将该节点删除,并发布消息。
+(1)unlock源码

+
    public void unlock() {
+        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
+        if (opStatus == null) {
+            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+                    + id + " thread-id: " + Thread.currentThread().getId());
+        }
+        if (opStatus) {
+            cancelExpirationRenewal();
+        }
+

(2)异步解锁,并返回是否成功

+
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
+            "if (redis.call('exists', KEYS[1]) == 0) then " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; " +
+            "end;" +
+            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
+                "return nil;" +
+            "end; " +
+            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
+            "if (counter > 0) then " +
+                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
+                "return 0; " +
+            "else " +
+                "redis.call('del', KEYS[1]); " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; "+
+            "end; " +
+            "return nil;",
+            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
+
+    }
+

输入的参数有:
+参数:
+KEYS[1](getName()):需要加锁的key,这里需要是字符串类型。
+KEYS[2](getChannelName()):redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName:“redisson_lock__channel__{” + getName() + “}”
+ARGV[1](LockPubSub.unlockMessage):redis消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。
+ARGV[2](internalLockLeaseTime):锁的超时时间,防止死锁
+ARGV[3](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

+

此处lua脚本的作用:

+
--如果keys[1]不存在,则发布消息,说明已经被解锁了
+if (redis.call('exists', KEYS[1]) == 0) then
+    redis.call('publish', KEYS[2], ARGV[1]);
+    return 1;
+end;
+--key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。
+if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
+    return nil;
+end;
+--将value减1,这里主要用在重入锁
+local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
+if (counter > 0) then 
+    redis.call('pexpire', KEYS[1], ARGV[2]); 
+    return 0; 
+else 
+--删除key并消息
+    redis.call('del', KEYS[1]); 
+    redis.call('publish', KEYS[2], ARGV[1]); 
+    return 1;
+end; 
+return nil;
+

(3)删除过期信息

+
void cancelExpirationRenewal() {
+    Timeout task = expirationRenewalMap.remove(getEntryName());
+    if (task != null) {
+        task.cancel();
+    }
+}
+

总结

+

Redis2.6版本之后引入了eval,能够支持lua脚本,更好的保证了redis的原子性,而且redisson采用了大量异步的写法来避免性能所带来的影响。本文只是讲解了下redisson的重入锁,其还有公平锁、联锁、红锁、读写锁等,有兴趣的可以看下。感觉这篇文章写得也不是很好,毕竟netty还没开始学,有些api也不太清楚,希望各位大佬能够建议建议~~

+

参考:
+1.redisson
+2.Redis分布式锁的正确实现方式
+3.分布式锁的多种实现方式
+4.用Redis构建分布式锁
+5.基于Redis的分布式锁实现
+6.基于Redis实现分布式锁,Redisson使用及源码分析

+]]>
+ 2024-02-06T07:22:35.000Z +
+ + canal小记 + http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html + + 2024-02-06T07:53:39.000Z + 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

+

先来了解一下MySQL的主从备份:

+
]]>
+ 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

+

先来了解一下MySQL的主从备份:

+
+

从上层来看,复制分成三步:
+master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
+slave将master的binary log events拷贝到它的中继日志(relay log);
+slave重做中继日志中的事件,将改变反映它自己的数据。

+

问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么

+

最后发现是沟通问题。。。

+
+

排查过程:

+
    +
  1. 起初,怀疑是es的问题,会不会是string转为long中出现了问题,PUT了个,无异常,这种情况排除。
  2. +
  3. 再然后以为是代码有问题,可是想了下,rowData.getAfterColumnsList().forEach(column -> data.put(column.getName(), column.getValue()))这句不可能有什么其他的问题啊,而且测试环境中一切都是好好的。
  4. +
  5. canal安装出错,重新查看了一次canal.properties和instance.properties,并没有发现配置错了啥,如果错了,那为什么只有那几个字段出现异常,其他的都是好好的,郁闷。而且,用测试环境的canal配置生产中的数据库,然后本地调试,结果依旧一样。可能问题出在mysql。
  6. +
+

最后发现,居然是沟通问题。。。。测试环境中是从正式环境导入的,用的insert,可是在正式环境里,用的确实insert后update字段,之后发现居然还用delete,,,,晕。。。。之前明确问过了只更新insert的,人与人之间的信任在哪里。。。。

+

问题二:canal.properties中四种模式的差别

+

简单的说,canal维护一份增量订阅和消费关系是依靠解析位点和消费位点的,目前提供了一下四种配置,一开始我也是懵的。

+
#canal.instance.global.spring.xml = classpath:spring/local-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+

local-instance
+我也不知道啥。。

+

memory-instance
+所有的组件(parser , sink , store)都选择了内存版模式,记录位点的都选择了memory模式,重启后又会回到初始位点进行解析
+特点:速度最快,依赖最少(不需要zookeeper)
+场景:一般应用在quickstart,或者是出现问题后,进行数据分析的场景,不应该将其应用于生产环境。
+个人建议是调试的时候使用该模式,即新增数据的时候,客户端能马上捕获到改日志,但是由于位点一直都是canal启动的时候最新的,不适用与生产环境。

+

file-instance
+所有的组件(parser , sink , store)都选择了基于file持久化模式,注意,不支持HA机制.
+特点:支持单机持久化
+场景:生产环境,无HA需求,简单可用.
+采用该模式的时候,如果关闭了canal,会在destination中生成一个meta.dat,用来记录关键信息。如果想要启动canal之后马上订阅最新的位点,需要把该文件删掉。
+{"clientDatas":[{"clientIdentity":{"clientId":1001,"destination":"example","filter":".\.."},"cursor":{"identity":{"slaveId":-1,"sourceAddress":{"address":"192.168.6.71","port":3306}},"postion":{"included":false,"journalName":"binlog.008335","position":221691106,"serverId":88888,"timestamp":1524294834000}}}],"destination":"example"}

+

default-instance
+所有的组件(parser , sink , store)都选择了持久化模式,目前持久化的方式主要是写入zookeeper,保证数据集群共享。
+特点:支持HA
+场景:生产环境,集群化部署.
+该模式会记录集群中所有运行的节点,主要用与HA主备模式,节点中的数据如下,可以关闭某一个canal服务来查看running的变化信息。

+
+

问题三:如果要订阅的是mysql的从库改怎么做?

+

生产环境中的主库是不能随便重启的,所以订阅的话必须订阅mysql主从的从库,而从库中是默认下只将主库的操作写进中继日志,并写到自己的二进制日志的,所以需要让其成为canal的主库,必须让其将日志也写到自己的二进制日志里面。处理方法:修改/etc/my.cnf,增加一行log_slave_updates=1,重启数据库后就可以了。

+
+

问题四:部分字段没有更新

+

最终版本是以mysql的id为es的主键,用canal同步到flume,再由flume到kafka,然后再由一个中间件写到es里面去,结果发现,一天之中,会有那么一段时间得出的结果少一丢丢,甚至是骤降,如图。不得不从头开始排查情况,canal到flume,加了canal的重试,以及发送到flume的重试机制,没有报错,所有数据正常发送。flume到kafka不敢怀疑,毕竟公司一直在用,怎么可能有问题。kafka到es的中间件?组长写的,而且一直在用,不可能==最后确认的是flume到kafka,kafka的parition处理速度不同,

+
+

check一下flume的文档,可以知道

+

| Property Name | Description|
+|

+]]>
+ 2024-02-06T07:22:35.000Z +
+ + 基于ZooKeeper的队列爬虫 + http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html + + 2024-02-14T17:59:53.000Z + 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
+简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
+基本的知识就不过多介绍了,可以参考参考下面这些人的:
+ZooKeeper官网
+http://www.cnblogs.com/wuxl360/p/5817471.html

]]>
+ 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
+简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
+基本的知识就不过多介绍了,可以参考参考下面这些人的:
+ZooKeeper官网
+http://www.cnblogs.com/wuxl360/p/5817471.html

+

一、整体架构

+

这张图来自skyme,我也是看了这张图的启发写了这篇文章的。

+
+

最基本的分布式队列即一个生产者不断抓取链接,然后将链接存储进ZooKeeper的队列节点里,每个节点的value都只是链接,然后消费者从中获取一条url进行抓取。本项目生产这主要是用来生产URL即可,这部分就不要要求太多。然后是消费者,消费者需要解决的问题有:
+1.队列如何保证自己的分发正确;
+2.消费这如何进行高效的抓取。

+

二、ZooKeeper队列原理

+

2.1 介绍

+

分布式队列,目前此类产品大多类似于ActiveMQ、RabbitMQ等,本文主要介绍的是Zookeeper实现的分布式队列,它的实现方式也有两种,一种是FIFO(先进先出)的队列,另一种是等待队列元素聚集之后才统一安排的Barrier模型。同样,本文主要讲的是FIFO的队列模型。其大体设计思路也很简单,主要是在/SinaQueue下创建顺序节点,如/SinaQueue/qn-000000000,创建完节点之后,根据下面的4个步骤来决定执行的顺序。
+1.通过调用getChildren()接口来获取某一节点下的所有节点,即获取队列中的所有元素。
+2.确定自己的节点序号在所有子节点中的顺序。
+3.如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
+4.接收到Watcher通知后,重复步骤1。

+
+

2.2 Watcher介绍

+

znode以某种方式发生变化时,“观察”(watch)机制可以让客户端得到通知.可以针对ZooKeeper服务的“操作”来设置观察,该服务的其他 操作可以触发观察。
+1.Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
+2.Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调
+3.Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。

+

2.3 源码

+

在csdn上找到了某个人写的这个过程,使用的是ZKClient,有兴趣可以看看杰布斯的博客,但是没有实现上面过程的第三步(Watcher相关的),这里,我们使用的是Zookeeper的另一个客户端工具curator,其中,curator实现了各种Zookeeper的特性,如:Election(选举),Lock(锁),Barrier(关卡),Atonmic(原子量),Cache(缓存),Queue(队列)等。我们来看看Curator实现的简单的分布式队列的源码。

+
public class SimpleDistributedQueue {
+    ...
+    private final CuratorFramework client;//连接Zookeeper的客户端
+    private final String path;//路径
+    private final EnsureContainers ensureContainers;//确保原子特性
+    private final String PREFIX = "qn-";//顺序节点的同意前缀,使用qn-
+    ...
+

其中PREFIX是用来生成顺序节点的,默认不可更改,将生成的路径赋予给path,然后向节点赋予数据。下面是赋予数据的代码

+
    public boolean offer(byte[] data) throws Exception {
+        String thisPath = ZKPaths.makePath(this.path, "qn-");//生成的路径
+        ((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL)).forPath(thisPath, data);//如果没有路径将生成持久化的路径然后存储节点的数据。
+        return true;
+    }
+

最关键的来了,队列如何保证自己的分发正确?SimpleDistributedQueue使用take()来取得队列的头部,然后将头部删掉,这一过程的一致性是通过CountDownLatch和Watcher来实现的。

+
    public byte[] take() throws Exception {//直接调用interPoll,并将超时的设置为0;
+        return this.internalPoll(0L, (TimeUnit)null);
+    }
+    private byte[] internalPoll(long timeout, TimeUnit unit) throws Exception {
+        ...//忽略超时的设置代码
+        while(true) {
+            final CountDownLatch latch = new CountDownLatch(1);//定义一个latch,设置为1,先加锁,然后执行完任务后再释放锁
+            Watcher watcher = new Watcher() {
+                public void process(WatchedEvent event) {
+                    latch.countDown();
+                }
+            };
+            byte[] bytes;
+            try {
+                bytes = this.internalElement(true, watcher);//调用internalElement函数来获取字节流
+            } catch (NoSuchElementException var17) {
+            }
+            ...
+            if (hasTimeout) {
+                long elapsedMs = System.currentTimeMillis() - startMs;
+                long thisWaitMs = maxWaitMs - elapsedMs;
+                if (thisWaitMs <= 0L) {    //如果等待超时了则返回为空
+                    return null;
+                }
+                latch.await(thisWaitMs, TimeUnit.MILLISECONDS);
+            } else {
+                latch.await();
+            }
+        }
+    }
+    private byte[] internalElement(boolean removeIt, Watcher watcher) throws Exception {
+            this.ensurePath();
+            List nodes;
+            try {
+                nodes = watcher != null ? (List)((BackgroundPathable)this.client.getChildren().usingWatcher(watcher)).forPath(this.path) : (List)this.client.getChildren().forPath(this.path);//获取节点下的所有子节点注册监听(watcher默认都不是为空的,每一个都注册)
+            } catch (NoNodeException var8) {
+                throw new NoSuchElementException();
+            }
+    
+            Collections.sort(nodes);//对节点进行排序
+            Iterator var4 = nodes.iterator();
+            while(true) {//遍历
+                while(var4.hasNext()) {
+                    String node = (String)var4.next();//取得当前头结点
+                    if (node.startsWith("qn-")) {
+                        String thisPath = ZKPaths.makePath(this.path, node);
+                        try {
+                            byte[] bytes = (byte[])this.client.getData().forPath(thisPath);
+                            if (removeIt) {
+                                this.client.delete().forPath(thisPath);//删除该节点
+                            }
+                            return bytes;//返回节点的字节流
+            ...
+        }
+

三、多线程并发

+

对于分布式爬虫来说,让每一个消费者高效的进行抓取是具有重要意义的,为了加快爬虫的速度,采用多线程爬虫的方法。Java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。其中使用Executors提供了四种声明线程池的方法,分别是newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor和newScheduledThreadPool,为了监控实时监控队列的长度,我们使用数组型的阻塞队列ArrayBlockingQueue。声明方式如下:

+
    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+    ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+            0L, TimeUnit.MILLISECONDS,
+            queuelength);
+

四、使用

+

本次实验主要环境如下:

+
zookeeper.version=3.5
+java.version=1.8.0_65
+os.arch=amd64
+i5 四核心CPU
+网速为中国电信100M
+

这里主要是对博客园中的前两千条博客进行爬取,本文主要是对分布式队列的理解,就不再进行什么难度的处理(比如元素的选取、数据的存储等),只输出每篇博客的title即可。
+生产者代码:

+
public class Producer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Producer.class);
+    public static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/Queue");
+    private static Integer j = 0;
+
+    public static void begin(String url) {//对博客园的每一页进行爬取
+        try {
+            String content = HttpHelper.getInstance().get(url);
+            resolveweb(content);
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+    public static void resolveweb(String content) throws Exception {
+        Elements elements = Jsoup.parse(content).select("a.titlelink");//对每篇博客的标题进行获取
+        for (Element element : elements) {
+            String url = element.attr("href");//
+            if (StringUtils.isNotEmpty(url) && !url.contains("javascript") && !url.contains("jump")) {//去除a中调用href过程
+                logger.info(url + " " + String.valueOf(j++));
+                queue.offer(url.getBytes());
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        client.start();
+        for (int i = 0; i < 100; i++) {
+            begin("https://www.cnblogs.com/#p" + String.valueOf(i));
+        }
+    }
+}
+

消费者

+
public class Consumer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
+    private static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/SinaQueue");
+    private static Integer i = 0;
+    private static final Integer CORE = Runtime.getRuntime().availableProcessors();
+    //声明为一个数组型的阻塞队列,这里限制大小为
+    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+
+    static class CBCrawler implements Runnable {
+        private String url;
+
+        public CBCrawler(String url) {
+            this.url = url;
+        }
+
+        @Override
+        public void run() {
+            String content = HttpHelper.getInstance().get(url);
+            logger.info(url + " " + Jsoup.parse(content).title());//打印网页的标题
+        }
+    }
+
+    public static void begin() {
+        try {
+            ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+                    0L, TimeUnit.MILLISECONDS,
+                    queuelength);
+            while (client.getChildren().forPath("/SinaQueue").size() > 0) {
+                CBCrawler crawler = new CBCrawler(new String(queue.take()));
+                es.submit(crawler);//执行爬虫
+                i = i + 1;
+                logger.info(String.valueOf(i) + " is finished\n" + " queue size is" + queuelength.size());//监控当前队列的长度
+            }
+            if (!es.isShutdown()) {//如果线程池没有关闭则关闭
+                es.shutdown();
+            }
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        client.start();
+        begin();
+        client.close();
+        logger.info("start time: " + start);
+        long end = System.currentTimeMillis();
+        logger.info("end time: " + end);
+        logger.info("take time: " + String.valueOf(end - start));//记录开始时间和结束时间
+    }
+}
+

由于在队列的take中使用了CountDownLatch和Collections.sort(nodes)进行排序,耗时过程变长了不少,2000个节点,单台服务器和多台服务器的耗时是一样的,都是9分钟,具体实验见下面。

+

实验结果

+

生产者生产URL:

+
+

单机模式下的消费者,耗时:560825/(1000*60)=9分钟

+
+

分布式模式下的抓取:

+
+

耗时:564374/(1000*60)=9分钟:

+
+

由图可见,当每个消费者处理能力大于队列分配的能力时,耗时的过程反而是在队列,毕竟分布式队列在进行take动作的时候对节点进行了加锁,还要对队列进行排序,特别是在节点多达2000+的情况下,耗时是十分严重的。

+

实验二

+

实验二的主要解决的问题是将消费者处理的耗时延长,我们使用Thread.sleep(n)来模拟时长。由于博客园突然连不上,为了减少这种不可控的故障,抓取的网页改为新浪,并将抓取后的URL以文本形式保存下来。

+
public static void sleepUtil(Integer time) {
+    try {
+        Thread.sleep(time * 1000);
+    } catch (Exception e) {
+        logger.error("线程sleep异常", e);
+    }
+}
+

此时再看程序的输出,可以看出,队列的分发能力已经大于消费者的处理能力,总算是正常了。

+
+

分布式队列分发的时间是:341998/(1000*60)=5.6分钟

+
2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - start time: 1509324606460
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - end time: 1509324948458
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - take time: 341998
+

两台机子抓取完毕的耗时分别是:

+
A服务器:08:49:54.509——09:02:07  
+B服务器:08:49:54.509——09:05:05
+

单机的时候分发时间是:353198/(1000*60)=5.8分钟

+
2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - start time: 1509326672614
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - end time: 1509327025812
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - take time: 353198
+

耗时

+
09:24:33.391——09:51:44.733
+

分布式下平均耗时约为13分钟,单机模式下耗时约为27分钟,还是蛮符合估算的。

+

总结

+

源代码都放在这里了,有兴趣的可以star一下或者下载看一下,也欢迎大家提提意见,没企业级的实战环境,见笑了O(∩_∩)O~

+

欢迎访问我的个人网站
+个人网站网址:http://www.wenzhihuai.com
+个人网站代码地址:https://github.com/Zephery/newblog

+]]>
+ 2024-02-06T07:22:35.000Z +
+ + TCP/IP、HTTP、HTTPS、HTTP2.0 + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html + + 2024-02-14T17:59:53.000Z + HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

+

HTTPS,即加密后的HTTP。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。目前TLS的版本是1.2,定义在RFC 5246中,暂时还没有被广泛的使用。

]]>
+ HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

+

HTTPS,即加密后的HTTP。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。目前TLS的版本是1.2,定义在RFC 5246中,暂时还没有被广泛的使用。

+

HTTP2.0,下一代的HTTP协议。相比于HTTP1.x,大幅度的提升了web性能,进一步减少了网络延时和拥塞。

+
+

各自的RFC相关文档自己去搜吧,https://www.rfc-editor.org/

+

一、TCP/IP

+

为了了解HTTP,有必要先理解一下TCP/IP。目前,存在两种划分模型的方法,OSI七层模型和TCP/IP模型,具体的区别不在阐述。HTTP是建立在TCP协议之上,所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性,例如tcp建立连接的3次握手和断开连接的4次挥手以及每次建立连接带来的RTT延迟时间。

+
+

TCP三次握手四次挥手的原理,由于篇幅关系,具体请看TCP协议的三次握手和四次挥手

+

二、HTTP

+

超文本传输协议(HyperText Transfer Protocol) 是伴随着计算机网络和浏览器而诞生,在浏览器出现之前,人们是怎么使用网络的?,不管怎么说,那个时代对于现在的我们,有点难以想象。。。之后,网景发布了Netscape Navigator浏览器,才慢慢打开了互联网的幕布。如果根据OSI来划分的话,HTML属于表示层,而HTTP属于应用层。HTTP发展至今,经过了HTTP0.9、HTTP1.0、HTTP1.1、HTTP2.0的时代,虽然2.0很久之前就正式提出标准,大多浏览器也支持了,但是网络支持HTTP2.0的却很少。

+

2.1 HTTP报文分析

+

报文,是网络中交换和传输的基本单元,即一次性发送的数据块。HTTP的报文是由一行一行组成的,纯文本,而且是明文,即:如果能监听你的网络,那么你发送的所有账号密码都是可以看见的,为了保障数据隐秘性,HTTPS随之而生。

+

2.1.1 请求报文:

+

为了形象点,我们把报文标准和实际的结合起来看。

+
+

下面是实际报文,以访问自己的网站(http://www.wenzhihuai.com)中的一个链接为例。

+
+
请求行
+

请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT,这里我们使用的是GET方法,访问的是/biaoqianyun.do,协议使用的是HTTP/1.1。
+GET:当客户端要从服务器中读取某个资源时,使用GET 方法。如果需要加传参数的话,需要在URL之后加个"?",然后把参数名字和值用=连接起来,传递参数长度受限制,通常IE8的为4076,Chrome的为7675。例如,/index.jsp?id=100&op=bind。
+POST:当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,以名称/值的形式出现,可以传输大量数据;

+
请求头部
+

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
+User-Agent:产生请求的浏览器类型;
+Accept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ / ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
+Accept-Language:客户端可接受的自然语言;
+Accept-Encoding:客户端可接受的编码压缩格式;
+Accept-Charset:可接受的应答的字符集;
+Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
+connection:连接方式(close 或 keepalive),如果是close的话就需要进行TCP四次挥手关闭连接,如果是keepalive,表明还能继续使用,这是HTTP1.1对1.0的新增,加快了网络传输,默认是keepalive;
+Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;

+
空行
+

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头;

+
请求包体
+

请求包体不在 GET 方法中使用,而是在POST 方法中使用。POST 方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型 Content-Type 和包体长度 Content-Length;

+

2.1.2 响应报文

+

同样,先粘贴报文标准。

+
+

抓包,以访问(http://www.wenzhihuai.com)为例。

+
+
状态行
+

状态行由 HTTP 协议版本字段、状态码和状态码的描述文本 3 个部分组成,他们之间使用空格隔开,描述文本一般不显示;
+状态码:由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:
+1xx:服务器已接收,但客户端可能仍要继续发送;
+2xx:成功;
+3xx:重定向;
+4xx:请求非法,或者请求不可达;
+5xx:服务器内部错误;

+
响应头部:响应头可能包括:
+

Location:Location响应报头域用于重定向接受者到一个新的位置。例如:客户端所请求的页面已不存在原先的位置,为了让客户端重定向到这个页面新的位置,服务器端可以发回Location响应报头后使用重定向语句,让客户端去访问新的域名所对应的服务器上的资源;
+Server:Server 响应报头域包含了服务器用来处理请求的软件信息及其版本。它和 User-Agent 请求报头域是相对应的,前者发送服务器端软件的信息,后者发送客户端软件(浏览器)和操作系统的信息。
+Vary:指示不可缓存的请求头列表;
+Connection:连接方式,这个跟rquest的类似。
+空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
+响应包体:服务器返回给客户端的文本信息;

+

2.2 HTTP特性

+

HTTP的主要特点主要能概括如下:

+

2.2.1 无状态性

+

即,当客户端访问完一次服务器再次访问的时候,服务器是无法知道这个客户端之前是否已经访问过了。优点是不需要先前的信息,能够更快的应答,缺点是每次连接传送的数据量增大。这种做法不利于信息的交互,随后,Cookie和Session就应运而生,至于它俩有什么区别,可以看看COOKIE和SESSION有什么区别?
+

+

2.2.2 持久连接

+

HTTP1.1 使用持久连接keepalive,所谓持久连接,就是服务器在发送响应后仍然在一段时间内保持这条连接,允许在同一个连接中存在多次数据请求和响应,即在持久连接情况下,服务器在发送完响应后并不关闭TCP连接,客户端可以通过这个连接继续请求其他对象。

+

2.2.3 其他

+

支持客户/服务器模式、简单快速(请求方法简单Get和POST)、灵活(数据对象任意)

+

2.3 影响HTTP的因素

+

影响HTTP请求的因素:

+
    +
  1. 带宽
    +好像只要上网这个因素是一直都有的。。。即使再快的网络,也会有偶尔网络慢的时候。。。
  2. +
  3. 延迟
    +(1) 浏览器阻塞
    +一个浏览器对于同一个域名,同时只能有4个链接(根据不同浏览器),如果超了后面的会被阻塞。
    +常用浏览器阻塞数量看下图。
  4. +
+
+

(2) DNS查询
+浏览器建立连接是需要知道服务器的IP的,DNS用来将域名解析为IP地址,这个可以通过刷新DNS缓存来加快速度。
+(3) 建立连接
+由之前第一章的就可以看出,HTTP是基于TCP协议的,即使网络、浏览器再快也要进行TCP的三次握手,在高延迟的场景下影响比较明显,慢启动则对文件请求影响较大。

+

2.4 缺陷

+
    +
  1. 耗时:传输数据每次都要建立连接;
  2. +
  3. 不安全:HTTP是明文传输的,只要在路由器或者交换机上截取,所有东西(账号密码)都是可见的;
  4. +
  5. Header内容过大:通常,客户端的请求header变化较小,但是每次都要携带大量的header信息,导致传输成本增大;
  6. +
  7. keepalive压力过大:持久连接虽然有一点的优点,但同时也会给服务器造成大量的性能压力,特别是传输图片的时候。
  8. +
+

BTW:明文传输有多危险,可以去试试,下面是某个政府网站,采用wireshark抓包,身份证、电话号码、住址什么的全暴露出来,所以,,,只要在路由器做点小动作,你的信息是全部能拿得到的,毕竟政府。

+
+

由于涉及的隐私太多,打了马赛克

+

三、HTTPS

+

由于HTTP报文的不安全性,网景在1994年就创建了HTTPS,并用在浏览器中。最初HTTPS是和SSL一起使用,然后演化为TLS。SSL/TLS在OSI模型中都是表示层的协议。SSL使 用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。

+

3.1 SSL/TLS

+

SSL(Secure Sockets Layer),简称安全套接入层,最初由上世纪90年代由网景公司设计。开启 SSL 会增加内存、CPU、网络带宽的开销,后二者跟你使用的 cipher suite 密切相关,其中参数很多,很难一概而论。开启 SSL 的前提是你的 cert 和 key 必须放在 TCP endpoint,你是否信得过那台设备。
+TLS(Transport Layer Security),简称安全传输层协议,该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面,与具体的应用无关,所以,一般把TLS协议归为传输层安全协议。
+由于本人在加密算法上面知识匮乏,就不误人子弟了,有兴趣可以看看百度百科里的资料,SSL,TLS

+

3.2 SPDY

+

2012年google提出了SPDY的方案,大家才开始从正面看待和解决老版本HTTP协议本身的问题,SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:
+降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
+请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
+header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
+基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
+服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。SPDY构成图。

+
+

3.3 HTTPS报文分析

+

跟之前的报文分析一样,我们使用wireshark来抓包分析,以在百度上搜索点东西为例。

+
+

192.168.1.103为本地电脑的ip地址,14.215.177.39为百度服务器地址。下面是步骤:

+
    +
  1. 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
  2. +
  3. 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。之后服务器发送 Certificate 报文。报文中包含公开密钥证书。最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
  4. +
  5. SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。
  6. +
  7. 服务器同样发送 Change Cipher Spec 报文。 服务器同样发送 Finished 报文。
  8. +
  9. 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。 应用层协议通信,即发送 HTTP 响应。
    +当然,用一张图更容易解释
    +简单地说就是下面。
  10. +
+
+

当我们追踪流的数据的时候,可以看到,基本上都是乱码,经过加密,数据是看不到,如果需要在wireshark上看到,则需要在wireshark中配置ssl。

+
+

3.4 HTTPS全站化

+

现今,感觉只要和商业利益有关的,就不得不涉及到加密这类东西。淘宝、京东、唯品会这些电商可谓是最早推行全站https的,这类电商是离用户金钱最近的企业。截止今年底,基本所有商业网站也基本实现了HTTPS。。。。至于小站点,比如个人网站,玩玩还是可以的。如果一个网站需要由HTTP全部变为HTTPS,那么需要关注下面几点:

+
    +
  1. CA证书,大部分证书都是需要收费的,当然,自己在服务器上用openssl也可以,不过浏览器会提示当前私密连接不安全这个警告,普通人看到这种信息是不会继续浏览的,所以,想使用HTTPS,可以使用Let's Encrypt,由谷歌等公司推行。
  2. +
  3. HTTPS性能优化,SSL握手,HTTPS 对速度会有一定程度的降低,但是只要经过合理优化和部署,HTTPS 对速度的影响完全可以接受。
  4. +
  5. CPU计算压力,HTTPS中大量的秘钥算法计算,对CPU的压力可想而知。
    +至于我自己的个人网站,之前实现了https,用的免费证书,但是由于HTTPS下的网站,所有子链都要使用HTTPS,使用了七牛云的CDN,如果要使用HTTPS加速,是要收费的,所以只能放弃。。。
  6. +
+

四、HTTP2.0

+

HTTP2.0,相较于HTTP1.x,大幅度的提升了web性能。在与HTTP/1.1完全语义兼容的基础上,进一步减少了网络延迟和传输的安全性。HTTP2.0可以说是SPDY的升级版(基于SPDY设计的),但是依然存在一些不同点:HTTP2.0支持明文传输,而SPDY强制使用HTTPS;HTTP2.0消息头的压缩算法采用HPACK,而非SPDY采用的DEFLATE。

+

4.1 历史

+

HTTP 2.0在2013年8月进行首次合作共事性测试。在开放互联网上HTTP 2.0将只用于https://网址,而 http://网址将继续使用HTTP/1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。HTTP 2.0是在SPDY(An experimental protocol for a faster web, The Chromium Projects)基础上形成的下一代互联网通信协议。HTTP/2 的目的是通过支持请求与响应的多路复用来较少延迟,通过压缩HTTPS首部字段将协议开销降低,同时增加请求优先级和服务器端推送的支持。

+

4.2 HTTP2.0新特性

+

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

+

4.2.1 二进制帧

+

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的Request Body则封装到DATA frame里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

+
+

4.2.2 多路复用

+

所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完。

+

4.2.3 流量控制

+

TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

+

4.2.4 服务器端推送

+

服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

+
+

4.2.5 首部压缩

+

HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
+如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。

+

4.3 HTTP1.1与HTTP2.0的对比

+

以访问https://http2.akamai.com/demo为例。

+
+

4.4 报文

+

访问https://http2.akamai.com/demo,谷歌浏览器的报文没有显示出协议,此处使用火狐浏览器。
+响应头部分如下。

+
+

请求头如下。

+
+

采用淘宝网站为例,淘宝目前采用主站使用HTTP1.1,资源使用HTTP2.0,少些使用SPDY协议。目前也是业界比较流行的做法。

+
+

参考

+
    +
  1. HTTPS那些事
  2. +
  3. 如何搭建一个HTTP2.0的网站
  4. +
  5. HTTP/2.0 相比1.0有哪些重大改进?
  6. +
  7. HTTP2.0 demo
  8. +
  9. Http、Https、Http2前身
  10. +
  11. HTTP报文
  12. +
  13. HTTP、HTTP2.0、SPDY、HTTPS 你应该知道的一些事
  14. +
  15. HTTPS权威指南
  16. +
  17. HTTP2.0的奇妙日常
  18. +
  19. curl 支持 HTTP2
  20. +
  21. 淘宝HTTPS探索
  22. +
  23. HTTPS完全协议详解
  24. +
+

欢迎访问我的个人网站。https://wenzhihuai.com

+]]>
+ 2024-02-06T07:05:59.000Z +
+ + 高性能高并发高可用的一些思考 + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html + + 2024-03-10T14:39:30.000Z + TODO待补充

+

异步

+

Jmeter

+

JProfiler

+

火焰图

+

curl -O https://arthas.aliyun.com/arthas-boot.jar
+java -jar arthas-boot.jar

+

内存火焰图

+

缓存

+

限流熔断

]]>
+ TODO待补充

+

异步

+

Jmeter

+

JProfiler

+

火焰图

+

curl -O https://arthas.aliyun.com/arthas-boot.jar
+java -jar arthas-boot.jar

+

内存火焰图

+

缓存

+

限流熔断

+

Kafka

+

序列化

+

参考

+

1.Java数据库高并发如何解决 java高并发三种解决方法 转载
+2.Java多线程梳理之四_其他并发解决方案
+3.Java高并发之并发基础

+]]>
+ 2024-02-03T17:57:14.000Z +
+ + 友链地址 + http://www.wenzhihuai.com/link/main.html + + 2024-02-23T07:30:13.000Z + + + 2024-02-02T03:58:59.000Z + + + 广州图书馆借阅抓取 + http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html + + 2024-05-11T18:24:35.000Z + 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

+

搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于我)页面。整个代码采用HttpClient,存储放在MySql,定时使用Spring自带的Schedule,下面是抓取的过程。

]]>
+ 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

+

搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于我)页面。整个代码采用HttpClient,存储放在MySql,定时使用Spring自带的Schedule,下面是抓取的过程。

+

1.页面跳转过程

+

一般都是进入首页http://www.gzlib.gov.cn/,点击进登陆页面,然后输入账号密码。表面上看起来没什么特别之处,实际上模拟登陆的时候不仅仅是向链接post一个请求那么简单,得到的response要么跳回登陆页面,要么无限制重定向。

+
+
+
+

事实上,它做了单点登录,如下图,广州图书馆的网址为:www.gzlib.gov.cn,而登陆的网址为:login.gzlib.gov.cn。原理网上很多人都讲的很好了,可以看看这篇文章SSO单点登录

+
+
+
+

2.处理方法

+

解决办法不难,只要先模拟访问一下首页即可获取图书馆的session,python的获取代码如:session.get("http://www.gzlib.gov.cn/"),打印cookie之后如下:

+
[<Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for .gzlib.gov.cn/>, <Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for www.gzlib.gov.cn/>, <Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]
+

整个登陆抓取的流程如下:

+
+
+
+

即:
+(1)用户先点击广州图书馆的首页,以获取改网址的session,然后点击登录界面,解析html,获取lt(自定义的参数,类似于验证码),以及单点登录服务器的session。
+(2)向目标服务器(单点登录服务器)提交post请求,请求参数中包含username(用户名),password(密码),event(时间,默认为submit),lt(自定义请求参数),同时服务端还要验证的参数:refer(来源页面),host(主机信息),Content-Type(类型)。
+(3)打印response,搜索你自己的名字,如果有则表示成功了,否则会跳转回登陆页面。
+(4)利用cookie去访问其他页面,此处实现的是对借阅历史的抓取,所以访问的页面是:http://www.gzlib.gov.cn/member/historyLoanList.jspx。

+

基本的模拟登陆和获取就是这些,之后还有对面html的解析,获取书名、书的索引等,然后封装成JavaBean,再之后便是保存入数据库。(去重没有做,不知道用什么方式比较好)

+

3.代码

+

3.1 Java中,一般用来提交http请求的大部分用的都是httpclient,首先,需要导入的httpclient相关的包:

+
<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpclient</artifactId>
+    <version>4.5.3</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpcore</artifactId>
+    <version>4.4.7</version>
+</dependency>
+

3.2 构建声明全局变量——上下文管理器,其中context为上下文管理器

+
public class LibraryUtil {
+    private static CloseableHttpClient httpClient = null;
+    private static HttpClientContext context = null;
+    private static CookieStore cookieStore = null;
+    static {
+        init();
+    }
+    private static void init() {
+        context = HttpClientContext.create();
+        cookieStore = new BasicCookieStore();
+        // 配置超时时间
+        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(12000).setSocketTimeout(6000)
+                .setConnectionRequestTimeout(6000).build();
+        // 设置默认跳转以及存储cookie
+        httpClient = HttpClientBuilder.create()
+                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
+                .setRedirectStrategy(new DefaultRedirectStrategy()).setDefaultRequestConfig(requestConfig)
+                .setDefaultCookieStore(cookieStore).build();
+    }
+    ...
+

3.3 声明一个get函数,其中header可自定义,此处不需要,但是保留着,做成一个通用的吧。

+
    public static CloseableHttpResponse get(String url, Header[] header) throws IOException {
+        HttpGet httpget = new HttpGet(url);
+        if (header != null && header.length > 0) {
+            httpget.setHeaders(header);
+        }
+        CloseableHttpResponse response = httpClient.execute(httpget, context);//context用于存储上下文
+        return response;
+    }
+

3.4 访问首页以获得session,服务器上会话是使用session存储的,本地浏览器使用的是cookie,只要本地不退出,那么使用本地的cookie来访问也是可以的,但是为了达到模拟登陆的效果,这里就不再阐述这种方式。

+
CloseableHttpResponse homeResponse = get("http://www.gzlib.gov.cn/", null);
+homeResponse.close();
+

此时,如果打印cookie,可以看到目前的cookie如下:

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]>
+

3.5 访问登陆页面,获取单点登录服务器之后的cookie,解析网页,获取自定义参数lt。这里的解析网页使用了Jsoup,语法和python中的BeautifulSoup中类似。

+
String loginURL = "http://login.gzlib.gov.cn/sso-server/login?service=http%3A%2F%2Fwww.gzlib.gov.cn%2Flogin.jspx%3FreturnUrl%3Dhttp%253A%252F%252Fwww.gzlib.gov.cn%252F%26locale%3Dzh_CN&appId=www.gzlib.gov.cn&locale=zh_CN";
+CloseableHttpResponse loginGetResponse = get(loginURL, null);
+String content = toString(loginGetResponse);
+String lt = Jsoup.parse(content).select("form").select("input[name=lt]").attr("value");
+loginGetResponse.close();
+

此时,再次查看cookie,多了一个(www.gzlib.gov.cn/sso-server):

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
+

3.6 声明一个post函数,用来提交post请求,其中提交的参数默认为

+
    public static CloseableHttpResponse postParam(String url, String parameters, Header[] headers)
+            throws IOException {
+        System.out.println(parameters);
+        HttpPost httpPost = new HttpPost(url);
+        if (headers != null && headers.length > 0) {
+            for (Header header : headers) {
+                httpPost.addHeader(header);
+            }
+        }
+        List<NameValuePair> nvps = toNameValuePairList(parameters);
+        httpPost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
+        CloseableHttpResponse response = httpClient.execute(httpPost, context);
+        return response;
+    }
+

3.7 登陆成功后,如果没有声明returnurl,即登录链接为(http://login.gzlib.gov.cn/sso-server/login),那么只是会显示成功登录的页面:

+
+
+
+

后台应该是定义了一个service用来进行链接跳转的,想要获取登录成功之后的跳转页面可修改service之后的链接,这里将保持原始状态。此时,查看cookie结果如下:

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie CASTGC=TGT-198235-zkocmYyBP6c9G7EXjKyzgKR7I40QI4JBalTkrnr9U6ZkxuP6Tn for www.gzlib.gov.cn/sso-server>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
+

其中,出现CASTGC表明登陆成功了,可以使用该cookie来访问广州图书馆的其他页面,在python中是直接跳转到其他页面,而在java使用httpclient过程中,看到的并不是直接的跳转,而是一个302重定向,打印Header之后结果如下图:

+
+
+
+

认真研究一下链接,就会发现服务器相当于给了一张通用票ticket,即:可以使用该ticket访问任何页面,而returnUrl则是返回的页面。这里我们直接访问该重定向的url。

+
Header header = response.getHeaders("Location")[0];
+CloseableHttpResponse home = get(header.getValue(), null);
+

然后打印页面,即可获取登陆之后跳回的首页。

+

3.8 解析html
+获取session并跳回首页之后,再访问借阅历史页面,然后对结果进行html解析,python中使用了BeautifulSoup,简单而又实用,java中的jsoup也是一个不错的选择。

+
        String html = getHTML();
+        Element element = Jsoup.parse(html).select("table.jieyue-table").get(0).select("tbody").get(0);
+        Elements trs = element.select("tr");
+        for (int i = 0; i < trs.size(); i++) {
+            Elements tds = trs.get(i).select("td");
+            System.out.println(tds.get(1).text());
+        }
+

输出结果:

+
企业IT架构转型之道
+大话Java性能优化
+深入理解Hadoop
+大话Java性能优化
+Java EE开发的颠覆者:Spring Boot实战
+大型网站技术架构:核心原理与案例分析
+Java性能权威指南
+Akka入门与实践
+高性能网站建设进阶指南:Web开发者性能优化最佳实践:Performance best practices for Web developers
+Java EE开发的颠覆者:Spring Boot实战
+深入理解Hadoop
+大话Java性能优化
+

点击查看源码

+

总结

+

目前,改代码已经整合进个人网站之中,每天定时抓取一次,但是仍有很多东西没有做(如分页、去重等),有兴趣的可以研究一下源码,要是能帮忙完善就更好了。感谢Thanks♪(・ω・)ノ。整个代码接近250行,当然...包括了注释,但是使用python之后,也不过25行=w=,这里贴一下python的源码吧。同时,欢迎大家访问我的个人网站,也欢迎大家能给个star

+
import urllib.parse
+import requests
+from bs4 import BeautifulSoup
+
+session = requests.session()
+session.get("http://www.gzlib.gov.cn/")
+session.headers.update(
+    {"Referer": "http://www.gzlib.gov.cn/member/historyLoanList.jspx",
+     "origin": "http://login.gzlib.gov.cn",
+     'Content-Type': 'application/x-www-form-urlencoded',
+     'host': 'www.gzlib.gov.cn',
+     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'
+     }
+)
+baseURL = "http://login.gzlib.gov.cn/sso-server/login"
+soup = BeautifulSoup(session.get(baseURL).text, "html.parser")
+lt = soup.select("form")[0].find(attrs={'name': 'lt'})['value']
+postdict = {"username": "你的身份证",
+            "password": "密码(默认为身份证后6位)",
+            "_eventId": "submit",
+            "lt": lt
+            }
+postdata = urllib.parse.urlencode(postdict)
+session.post(baseURL, postdata)
+print(session.get("http://www.gzlib.gov.cn/member/historyLoanList.jspx").text)
+
]]>
+ 2024-01-30T06:37:05.000Z +
+ + AOP + http://www.wenzhihuai.com/java/SpringBoot/aop.html + + 2024-01-30T06:53:42.000Z + 一、概述 +

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

+
]]>
+ 一、概述 +

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

+
+

Spring自2.0版本开始采用@AspectJ注解非常容易的定义一个切面。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。

+

1.1 特点

+

AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。AOP是OOP的补充,是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP可以分为静态织入与动态织入,静态织入即在编译前将需织入内容写入目标模块中,这样成本非常高。动态织入则不需要改变目标模块。Spring框架实现了AOP,使用注解配置完成AOP比使用XML配置要更加方便与直观。

+

1.2 AOP概述

+

Aspect:一个模块用来关注多个类的切面。在JAVA EE的应用中,事务是AOP的典型例子。
+Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
+Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
+Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
+Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.
+Target(目标对象):代理的目标对象
+Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.spring采用动态代理织入,而AspectJ采用编译期织入和类装在期织入.
+Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类Aspect(切面): 是切入点和通知(引介)的结合

+

二、Spring中的AOP

+

Spring实现AOP主要是由IOC容器来负责生成、管理的。其创建的方式有两种:

+
    +
  1. 默认使用Java动态代理来创建AOP代理;
  2. +
  3. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。高版本的Spring会自动选择是使用动态代理还是CGLIB生成代理内容,当然我们也可以强制使用CGLIB生成代理,那就是aop:config里面有一个"proxy-target-class"属性,这个属性值如果被设置为true,那么基于类的代理将起作用。
  4. +
+

2.1 AspectJ支持5种类型的通知注解:

+

[1] Before:前置通知,在方法执行之前执行
+[2] After:后置通知,在方法执行之后执行
+[3] AfterRunning:返回通知,在方法返回结果之后执行
+[4] AfterThrowing:异常通知,在方法抛出异常之后执行
+[5] Around:环绕通知,围绕着方法执行
+其中,环绕通知是最常见的一种通知注解,特别是在缓存的使用中,例如:Spring-Cache中的使用,在service的方法中添加一个cache的注解,通过AOP来拦截,如果缓存中已经存在,则直接返回结果,如果没有,再进行service的访问。

+

2.2 Spring提供了4种实现AOP的方式:

+
    +
  1. 经典的基于代理的AOP
  2. +
  3. @AspectJ注解驱动的切面
  4. +
  5. 纯POJO切面
  6. +
  7. 注入式AspectJ切面
  8. +
+

三、原理概述

+

Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现。Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入,这点了解一下即可。

+
+

具体的原理请看Spring AOP

+

四、使用

+

网上看别人写了很多入门的例子,自己就不再阐述了,毕竟自己还是菜,下面是关于AOP入门的资料:
+我们为什么要使用AOP?
+Spring中AOP的实现
+关于AOP

+

下面是自己在个人网站中的使用,主要是用来统计一个方法的执行消耗了多少时间,需要引入aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar的包。

+

4.1 在Spring MVC中开启AOP

+
    <!--自动扫描自定义切面-->
+    <aop:aspectj-autoproxy/>
+

4.2 定义一个切面

+
/**
+ * 可以使用 @Order 注解指定切面的优先级, 值越小优先级越高
+ */
+@Order(2)
+@Aspect
+@Component
+public class TimeInterceptor {
+}
+

4.3 声明一个切入点

+
    @Pointcut("execution(* com.myblog.service.impl.BlogServiceImpl.*(..))")
+    public void pointcut() {
+    }
+

4.4 声明一个前置切点

+
    @Before("pointcut()")
+    public void before(JoinPoint jp) {
+        logger.info(jp.getSignature().getName());
+        logger.info("
+
]]>
+ 2024-01-30T06:37:05.000Z +
+ + Spring Boot Prometheus使用 + http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html + + 2024-01-27T13:52:01.000Z + 一、基本原理 +

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

+
image-20240127202704612
image-20240127202704612
]]>
+ 一、基本原理 +

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

+
image-20240127202704612
image-20240127202704612
+

二、具体过程

+
    +
  • Prometheus Daemon负责定时去目标上抓取metrics(指标)数据,每个抓取目标需要暴露一个http服务的接口给它定时抓取。Prometheus支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup等方式指定抓取目标。Prometheus采用PULL的方式进行监控,即服务器可以直接通过目标PULL数据或者间接地通过中间网关来Push数据。
  • +
  • Prometheus在本地存储抓取的所有数据,并通过一定规则进行清理和整理数据,并把得到的结果存储到新的时间序列中。
  • +
  • Prometheus通过PromQL和其他API可视化地展示收集的数据。Prometheus支持很多方式的图表可视化,例如Grafana、自带的Promdash以及自身提供的模版引擎等等。Prometheus还提供HTTP API的查询方式,自定义所需要的输出。
  • +
  • PushGateway支持Client主动推送metrics到PushGateway,而Prometheus只是定时去Gateway上抓取数据。
  • +
  • Alertmanager是独立于Prometheus的一个组件,可以支持Prometheus的查询语句,提供十分灵活的报警方式。
  • +
+

三、pull模式(prometheus主动拉取)

+
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-actuator</artifactId>
+</dependency>
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-registry-prometheus</artifactId>
+</dependency>
+
management:
+  endpoints:
+    web:
+      exposure:
+        include: prometheus
+  metrics:
+    tags:
+      application: ${spring.application.name}
+

之后查看/actuator/prometheus就可以看到

+
image-20240127202722776
image-20240127202722776
+

腾讯云上面有个prometheus的服务,接入云原生监控还要配置一个Servicemonitor、PodMonitor等,详细的可以访问腾讯云的官方文档(https://cloud.tencent.com/document/product/1416/56031)

+

四、主动上报(pushgateway)

+

大部分现实场景中,如果每增加一个服务,都需要开发去配置的话,不仅沟通成本高,也导致因配错而起的运维成本很高,采用主动上报方式比较简单,方便规避一些问题。

+

(1)引入库

+

主动上报除了需要引入spring-boot-starter-actuator、micrometer-registry-prometheus,还要引入simpleclient_pushgateway,三个都是必须的,少一个都不行。

+
        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.prometheus</groupId>
+            <artifactId>simpleclient_pushgateway</artifactId>
+        </dependency>
+

(2)修改配置文件

+

之后,为了安全考虑,需要把endpoints的所有信息都关闭掉,免得被泄露出去。

+
management:
+  endpoints:
+    #一定要设为false,防止不小心暴露/actuator相关接口触发安全工单
+    enabled-by-default: false
+  metrics:
+    tags:
+      #要设置应用的名称,否则会聚合到别的项目里面去
+      application: ${spring.application.name}
+    export:
+      prometheus:
+        pushgateway:
+          base-url: http://xxxxx
+          push-rate: 20s
+          job: ${spring.application.name}
+          enabled: true
+          username: xxxx
+          password: xxx
+          grouping-key:
+            instance: ${HOSTNAME}
+

最好多看一眼/actuator,确认一下是否已经关闭endpoints

+
image-20240127202830908
image-20240127202830908
+

(3)抓包看一下metrics(可跳过)

+

大致看了下 simpleclient_pushgateway和spring-boot-starter-actuator的源码,并没有对http的请求发起日志,调试的时候都不知道是不是正常上报过去了,只能采取抓包来研究下。

+
image-20240127202853218
image-20240127202853218
+
image-20240127202944264
image-20240127202944264
+

平均间隔5s左右,符合配置文件里的设置。

+
image-20240127203001776
image-20240127203001776
+

五、Grafana

+

对于java来说,常用的dashboard是https://grafana.com/grafana/dashboards/4701,也可以用spring boot的https://grafana.com/grafana/dashboards/6756

+
image-20240127203024477
image-20240127203024477
+

六、自定义监控上报

+

除了通用的封装好的指标之外,也可以自定义prometheus的监控。对于Spring Boot来说,只要如下代码即可实现:

+
    @Resource
+    private MeterRegistry meterRegistry;
+
+    public void report() {
+        meterRegistry.counter("指标", "tag的名称", "tag的值").increment();
+    }
+

也可以通过抓包来查看,以及在push-gateway上看到。

+]]>
+ 2024-01-27T13:52:01.000Z +
+ + 小程序 + http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html + + 2024-01-27T09:51:43.000Z + 助眠风扇
助眠风扇
+
MyTesMate
MyTesMate
]]>
+ 助眠风扇
助眠风扇
+
MyTesMate
MyTesMate
+]]>
+ 2024-01-27T09:51:43.000Z +
+ + Webflux + http://www.wenzhihuai.com/java/SpringBoot/webflux.html + + 2024-01-27T09:51:43.000Z + 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? +

ChatGpt:
+Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

+

Servlet 3.1:虽然 Servlet 3.1 支持非阻塞 IO,但是它的 API 设计仍然是基于传统的阻塞模型,这意味着每个请求通常会在一个单独的线程中处理,直到请求处理完成并返回响应。这种模型在并发请求数量较少的情况下工作得很好,但是在高并发的情况下,可能会因为线程数量过多而导致资源消耗过大,性能下降。

]]>
+ 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? +

ChatGpt:
+Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

+

Servlet 3.1:虽然 Servlet 3.1 支持非阻塞 IO,但是它的 API 设计仍然是基于传统的阻塞模型,这意味着每个请求通常会在一个单独的线程中处理,直到请求处理完成并返回响应。这种模型在并发请求数量较少的情况下工作得很好,但是在高并发的情况下,可能会因为线程数量过多而导致资源消耗过大,性能下降。

+

WebFlux:WebFlux 是基于反应式编程模型的,它使用少量的线程(通常与 CPU 核心数相同)和事件驱动的方式来处理大量的并发请求。这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

+

所以,虽然 Servlet 3.1 和 WebFlux 底层都使用了 NIO,但是由于他们的编程模型和如何使用 NIO 的方式不同,WebFlux 在处理高并发、流式数据等场景时,可能会有更好的性能。但是,这并不意味着 WebFlux 在所有情况下都比 Servlet 3.1 有更好的性能,具体还需要根据应用的实际需求和场景来选择合适的技术。

+

webflux和springboot虚拟线程对比

+

ChatGpt:
+WebFlux 和 Spring Boot 使用虚拟线程(也称为纤程或轻量级线程)是两种不同的并发模型,它们各自有各自的优点和适用场景。

+

WebFlux:WebFlux 是 Spring 5 的一部分,它提供了一个基于反应式编程模型的非阻塞 Web 框架。WebFlux 使用少量的线程和事件驱动的方式来处理大量的并发请求,这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

+

Spring Boot 使用虚拟线程:Java 在其后续版本(如 Java 16)中引入了虚拟线程(Project Loom)的概念,这是一种新的并发模型,它可以创建大量的虚拟线程,每个虚拟线程都有自己的调用栈,但是它们在操作系统级别并不是真正的线程,因此创建和切换的成本更低。虚拟线程使得开发者可以使用传统的阻塞式编程模型,但是在运行时,这些阻塞操作并不会阻塞真正的物理线程,从而可以处理大量的并发请求。

+

WebFlux 和 Spring Boot 使用虚拟线程的主要区别在于它们的编程模型:WebFlux 使用反应式编程模型,而虚拟线程则使用更传统的阻塞式编程模型。虚拟线程的优点是编程模型更简单,更接近传统的 Java 开发方式,但是它需要更新的 Java 版本,并且目前还在实验阶段。WebFlux 的优点是它可以在任何支持 Java 8 或更高版本的环境中运行,但是它需要开发者熟悉反应式编程模型。

+

至于哪种模型的性能更好,这取决于具体的应用场景和负载。在某些情况下,WebFlux 可能会有更好的性能,而在其他情况下,使用虚拟线程可能会更有效。

+]]>
+ 2024-01-27T09:51:43.000Z +
+ + StarCraft Ⅱ 人工智能教程 + http://www.wenzhihuai.com/interesting/starcraft-ai.html + + 2024-05-11T18:24:35.000Z + 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

+

一、其他的太抽象了,先讲人机对战吧

+

sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的就是,需要以管理员身份运行,下面详细过程,翻译自 ProBots 2021 Season 1 - Human vs Bot

]]>
+ 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

+

一、其他的太抽象了,先讲人机对战吧

+

sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的就是,需要以管理员身份运行,下面详细过程,翻译自 ProBots 2021 Season 1 - Human vs Bot

+

1.安装星际争霸2,地址,至于要不要下载国际服,似乎没有必要
+2.下载ProBots vs Humans.Zip
+3.解压,附带了地图,主要是sc2aiapp
+

+

4.可选,下载相关地图,可以从竞技场里下,需要放到星际争霸2的目录下,mac的是/Applications/StarCraft II/Maps
+5.打开步骤2的目录
+6.打开sc2aiapp,打开的时候有可能报错:

+
+

右键sc2aiapp,以管理员身份运行即可,现在不让注册了,直接continue without login

+
+
+

7.全屏快捷键,Alt + Enter,进行对战

+

我这录制了个我对战的视频,bilibili,感觉AI在对战里很容易只有一样打法,据说是强化训练后的最优选导致的,这个也不知怎么整,个人感觉MicroMachine这个AI打法稍微多样,可以多和它对战下。

+

二、AI天梯

+

目前没有看到什么办法让暴雪允许AI在实际的天梯上进行运行,但社区搞了个专门的AI天梯,sc2ai,可以将代码上传到里面进行对战,实时流我没看到,对战完后可以下载replay复盘。下面讲下如何上传代码进行对战。
+1.第一步肯定是先要注册登录
+2.u

+
+

3.主要是这个Bot zip,基本的代码架构还是要固定的

+
+

具体可以看下sc2-api-simple-bot这里,记得把它打包即可
+4.成功之后,即可从profile里看到自己的机器人
+

+

5.此时,bot是不会进行比赛,需要参赛,点击Competitions,然后选择赛季
+

+
+

6.比赛是随机的放到队列里的,可能需要排队进行比赛,也可能主动申请和具体的机器人进行比赛,点击Request Match,进行申请比赛。
+

+

7.慢慢等待,比赛结束之前都看不到结果的,也没有实时流进行查看的,结束之后就可以看到结果以及下载replay。其中arena会随机的进行一些比赛,也有可能是别人随机选的,一个bot一天大概能有50场比赛,arena也会提供统计,胜率、ELO(分数)等

+

Bot开发样例

+

https://github.com/Zephery/sc2-api-simple-bot.git
+https://community.eschamp.com/t/simple-starcraft-2-bot-template-to-get-started/155

+]]>
+ 2024-01-26T16:53:43.000Z +
+ + 自我介绍 + http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html + + 2024-02-03T17:57:14.000Z + 安安静静的开发,搞点好玩的。

+
硅谷
硅谷
+]]>
+ 安安静静的开发,搞点好玩的。

+
硅谷
硅谷
+]]>
+ 2024-01-26T04:53:47.000Z +
+ + 关于我 + http://www.wenzhihuai.com/about-the-author/ + + 2024-01-25T17:34:45.000Z + 留空

+]]>
+ 留空

+]]>
+ 2024-01-25T17:34:45.000Z +
+ + Zookeeper + http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html + + 2024-01-25T17:34:45.000Z + 留空

+]]>
+ 留空

+]]>
+ 2024-01-25T17:34:45.000Z +
+ + 微信公众号-chatgpt智能客服搭建 + http://www.wenzhihuai.com/interesting/chatgpt.html + + 2024-05-11T18:24:35.000Z + 想体验的可以去微信上搜索【旅行的树】公众号。

+

一、ChatGPT注册

+

1.1 短信手机号申请

+

openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。

+

国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。

+

国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。

]]>
+ 想体验的可以去微信上搜索【旅行的树】公众号。

+

一、ChatGPT注册

+

1.1 短信手机号申请

+

openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。

+

国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。

+

国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。

+
image-20230220172107296
+

之后再搜索框填openai进行下单购买即可。

+
img
+

1.2 云服务器申请

+

openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。

+image-20230221193455384 +

1.3 ChatGPT注册

+

购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码

+
image-20230222104357230
+

然后回sms查看验证码。

+
image-20230222104225722
+

注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。

+image-20230220173335691 +

通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。

+
image-20230222104316514
+

二、搭建nginx服务器

+

跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。

+
Do not share your API key with interesting, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.
+

windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。

+
    location /v1/completions {
+      proxy_pass https://api.openai.com/v1/completions;
+    }
+

然后使用下面的方法进行调试即可:

+
POST http://YOUR IP/v1/completions
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+
+{
+  "model": "text-davinci-003",
+  "prompt": "Say this is a test",
+  "max_tokens": 7,
+  "temperature": 0
+}
+

三、公众号开发

+

网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。

+

3.1 微信云托管

+

微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。

+
image-20230220192603751
+

提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。

+image-20230220201004069 +

3.2 一个简单ChatGPT简单回复

+

微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。

+

一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。

+
const configuration = new Configuration({
+    apiKey: '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',
+    basePath: 'http://43.153.15.174/v1'
+});
+const openai = new OpenAIApi(configuration);
+
+async function simpleResponse(prompt) {
+    const completion = await openai.createCompletion({
+        model: 'text-davinci-003',
+        prompt,
+        max_tokens: 1024,
+        temperature: 0.1,
+    });
+    const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();
+    return strip(response, ['\n', 'A: ']);
+}
+
+app.post("/message/simple", async (req, res) => {
+    console.log('消息推送', req.body)
+    // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用
+    const appid = req.headers['x-wx-from-appid'] || ''
+    const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body
+    console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
+    if (MsgType === 'text') {
+        message = await simpleResponse(Content)
+        res.send({
+            ToUserName: FromUserName,
+            FromUserName: ToUserName,
+            CreateTime: CreateTime,
+            MsgType: 'text',
+            Content: message,
+        })
+    } else {
+        res.send('success')
+    }
+})
+

本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。

+

3.3 服务部署

+

提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。

+image-20230220203313790 +

部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。

+
image-20230220203711436
+

这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:

+

(1)服务有没有正常启动,看日志

+

(2)端口有没有设置错误,这个很多次没有注意到

+image-20230220203445297 +

保存成功之后,就可以在微信公众号里测试了。

+
image-20230221134250689
+

体验还可以

+

四、没有回复(超时回复问题)

+

很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。

+

订阅号的消息推送分几种:

+
    +
  1. 被动消息回复:指用户给公众号发一条消息,系统接收到后,可以回复一条消息。
  2. +
  3. 主动回复/客服消息:可以脱离被动消息的5秒超时权限,在48小时内可以主动回复。但需要公众号完成微信认证。
  4. +
+

根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html

+image-20230221100251825 +

对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。

+

1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。

+
  // 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。
+  await Message.create({
+    fromUser: FromUserName,
+    response: '',
+    request: Content,
+    aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画
+  });
+

2.抽象一个 chatGPT 请求方法 getAIMessage,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。

+
 // 成功后,更新记录
+  await Message.update(
+    {
+      response: response,
+      status: MESSAGE_STATUS_ANSWERED,
+    },
+    {
+      where: {
+        fromUser: FromUserName,
+        request: Content,
+      },
+    },
+  );
+

3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。

+
  // 找一下,是否已有记录
+  const message = await Message.findOne({
+    where: {
+      fromUser: FromUserName,
+      request: Content,
+    },
+  });
+
+  // 已回答,直接返回消息
+  if (message?.status === MESSAGE_STATUS_ANSWERED) {
+    return `[GPT]: ${message?.response}`;
+  }
+
+  // 在回答中
+  if (message?.status === MESSAGE_STATUS_THINKING) {
+    return AI_THINKING_MESSAGE;
+  }
+

4.最后就是一个 Promise.race

+
  const message = await Promise.race([
+    // 3秒微信服务器就会超时,超过2.9秒要提示用户重试
+    sleep(2900).then(() => AI_THINKING_MESSAGE),
+    getAIMessage({ Content, FromUserName }),
+  ]);
+

这样子大概就能实现超时之前返回了。

+

五、会话保存

+

掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。

+
async function buildCtxPrompt({ FromUserName }) {
+  // 获取最近10条对话
+  const messages = await Message.findAll({
+    where: {
+      fromUser: FromUserName,
+      aiType: AI_TYPE_TEXT,
+    },
+    limit: 10,
+    order: [['updatedAt', 'ASC']],
+  });
+  // 只有一条的时候,就不用封装上下文了
+  return messages.length === 1
+    ? messages[0].request
+    : messages
+        .map(({ response, request }) => `Q: ${request}\n A: ${response}`)
+        .join('\n');
+}
+

之后就可以实现会话之间的保存通信了。

+
image-20230218203309437
+

六、其他问题

+

6.1 限频

+

chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。

+

对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。

+

6.2 秘钥key被更新

+

没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。

+

6.3 为啥和官网的回复不一样

+

我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧

+image-20230221192417900 +

从官方文档来看,官方服务版的 ChatGPT 的模型并非基础版的text-davinci-003,而是经过了「微调:fine-tunes」。文档地址在这:platform.openai.com/docs/guides…

+

6.4 玄学挂掉

+

有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。

+image-20230221175150025 +

参考:https://juejin.cn/post/7200769439335546935

+

最后

+

记得去微信关注【旅行的树】公众号体验
+代码地址:https://github.com/Zephery/wechat-gpt

+]]>
+ 2024-01-25T12:42:18.000Z +
+ + Tesla api + http://www.wenzhihuai.com/interesting/tesla.html + + 2024-05-11T18:24:35.000Z + 一、特斯拉应用申请 +

1.1 创建 Tesla 账户

+

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

+

正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.

+image-20231210142130054 +image-20231210142415598]]>
+ 一、特斯拉应用申请 +

1.1 创建 Tesla 账户

+

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

+

正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.

+image-20231210142130054 +image-20231210142415598 +

注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。

+

1.2 提交访问请求

+

点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。

+

这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑

+

(1)Invalid domain

+image-20231210144504486 +

无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。

+

(2)Unable to Onboard

+image-20231210142930976 +

应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。

+

(3) Rejected

+image-20231210143156308 +

这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。

+

1.3 访问应用程序凭据

+

一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。

+

申请好就可以在自己的账号下看到自己的应用了,

+image-20231210144244888 +

1.4 开始 API 集成

+

按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。

+

二、开发之前的准备

+

由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。

+

2.1 认识token

+

app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:

+

(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌

+

(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。

+

此外,还需要注意下,中国大陆地区对应的api地址是 https://fleet-api.prd.cn.vn.cloud.tesla.cn,不要调到别的地址去了。

+

2.2 获取第三方应用token

+

这一步官网列的很详细,就不在详述了。

+
CLIENT_ID=<command to obtain a client_id>
+CLIENT_SECRET=<secure command to obtain a client_secret>
+AUDIENCE="https://fleet-api.prd.na.vn.cloud.tesla.com"
+# Partner authentication token request
+curl --request POST \
+  --header 'Content-Type: application/x-www-form-urlencoded' \
+  --data-urlencode 'grant_type=client_credentials' \
+  --data-urlencode "client_id=$CLIENT_ID" \
+  --data-urlencode "client_secret=$CLIENT_SECRET" \
+  --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \
+  --data-urlencode "audience=$AUDIENCE" \
+  'https://auth.tesla.com/oauth2/v3/token'
+

2.3 验证域名归属

+

2.1.1 Register

+

完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。

+
cd vehicle-command/cmd/tesla-keygen
+go build .
+./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem
+

这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。

+
    location ~ ^/.well-known {
+           default_type text/html;
+           alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;
+    }
+
+

随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。

+
curl --header 'Content-Type: application/json' \
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \
+  --data '{"domain":"string"}' \
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'
+

2.1.2 public_key

+

最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)

+
curl --header 'Content-Type: application/json' \
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'
+

得到下面内容就代表成功了

+
{"response":{"public_key":"xxxx"}}
+

至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。

+]]>
+ 2024-01-25T12:42:18.000Z +
+ + 在 Spring 6 中使用虚拟线程 + http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html + + 2024-01-26T03:58:56.000Z + 一、简介 +

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

+

虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。

]]>
+ 一、简介 +

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

+

虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。

+

首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。

+

二、 虚拟线程与平台线程

+

主要区别在于虚拟线程在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。

+

对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。

+

从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。

+

三、在Spring 6中使用虚拟线程

+

从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的预览功能。这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下代码

+
<build>
+    <plugins>
+        <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+                <source>19</source>
+                <target>19</target>
+                <compilerArgs>
+                    --enable-preview
+                </compilerArgs>
+            </configuration>
+        </plugin>
+    </plugins>
+</build>
+

从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:

+
@EnableAsync
+@Configuration
+@ConditionalOnProperty(
+  value = "spring.thread-executor",
+  havingValue = "virtual"
+)
+public class ThreadConfig {
+    @Bean
+    public AsyncTaskExecutor applicationTaskExecutor() {
+        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
+    }
+
+    @Bean
+    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
+        return protocolHandler -> {
+            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+        };
+    }
+}
+

第一个 Spring Bean ApplicationTaskExecutor将取代标准的*ApplicationTaskExecutor* ,提供为每个任务启动新虚拟线程的Executor。第二个 bean,名为ProtocolHandlerVirtualThreadExecutorCustomizer,将以相同的方式 自定义标准TomcatProtocolHandler 。我们还添加了注释@ConditionalOnProperty,**以通过切换application.yaml文件中配置属性的值来按需启用虚拟线程:

+
spring:
+    thread-executor: virtual
+    //...
+

我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:

+
@RestController
+@RequestMapping("/thread")
+public class ThreadController {
+    @GetMapping("/name")
+    public String getThreadName() {
+        return Thread.currentThread().toString();
+    }
+}
+

*Thread*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个curl请求来访问这个端点:

+
$ curl -s http://localhost:8080/thread/name
+$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
+

正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。

+

四、性能比较

+

对于此负载测试,我们将使用JMeter。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。

+

在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:

+
@RestController
+@RequestMapping("/load")
+public class LoadTestController {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
+
+    @GetMapping
+    public void doSomething() throws InterruptedException {
+        LOG.info("hey, I'm doing something");
+        Thread.sleep(1000);
+    }
+}
+

请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换

+

JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:

+
image-20230827193431807
image-20230827193431807
+

在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:

+
image-20230827193511176
image-20230827193511176
+

发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。

+

让我们看看虚拟线程会发生什么:

+
image-20230827193533565
image-20230827193533565
+

正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。

+

**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。

+]]>
+ 2024-01-25T12:42:18.000Z +
+ + 【elasticsearch】源码debug + http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html + + 2024-01-26T03:58:56.000Z + 一、下载源代码 +

直接用idea下载代码https://github.com/elastic/elasticsearch.git
+image

]]>
+ 一、下载源代码 +

直接用idea下载代码https://github.com/elastic/elasticsearch.git
+image

+

切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
+image

+

二、修改设置(可选)

+

为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:

+

setting 'xpack.security.enabled', 'false'

+

或者使用其中的用户名密码:

+

user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'

+

三、启动

+

先启动上面的 remote debug, 然后用 gradlew 启动项目:

+

./gradlew :run --debug-jvm
+打开浏览器http://localhost:9200即可看到es相关信息了
+image

+]]>
+ 2024-01-25T12:42:18.000Z +
+ + Database + http://www.wenzhihuai.com/database/ + + 2024-02-03T17:57:14.000Z + 目录留空

+]]>
+ 目录留空

+]]>
+ 2024-01-25T11:42:16.000Z +
+ + 微信支付 + http://www.wenzhihuai.com/donate/ + + 2024-01-26T17:18:59.000Z + 微信支付
微信支付
+]]>
+ 微信支付
微信支付
+]]>
+ 2024-01-25T08:04:11.000Z +
+ + 10.历时8年最终改版 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html + + 2024-05-11T18:24:35.000Z + 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

]]>
+ 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

+
早年的网站截图
早年的网站截图
+

总体而言,自建站对学习知识,了解整个建站的原理能够起到非常重要的作用,但是维护成本实在是太高了,每个月要支付服务器的费用,而且一旦想拿服务器来做点什么,都得提防一下会不会造成破坏。最终还是选择采用vuepress2来重构一下自建站,毕竟把markdown放到github,把图片放到cos里减少了不少的维护量。下面是使用vuepress2建站的代码地址

+

一、博客的安装

+

具体vuepress2官网讲解的很详细了,不用再处理什么,按照步骤创建一个项目即可,为了网站的美观,个人使用了theme hope这款主题。

+

二、配置

+

导航栏、侧边栏官网也有详细的讲解,也不再阐述,需要注意的是自动目录,之前看JavaGuide的样式,他那边的每篇文章都需要写一次ts文件(children),后来发现官网可以把children设置为structure,即可实现根据md文件生成侧边栏目录。注意的是,这里不是根据markdown的文件名来目录名,而是取markdown文件的标题。

+
    {
+        text: "Redis",
+        prefix: "redis/",
+        icon: "redis",
+        collapsible: false,
+        children: "structure"
+    },
+

三、为文章增加评论

+

vuepress-plugin-comment2,使用了Giscus,Giscus绑定了github账号,所以可以从一定程度上防止被别人刷广告,需要再个人的项目Settings->General把Discussions这个选项给勾选上。

+
一定要开启discussions
一定要开启discussions
+

然后去config.ts配置插件。

+
        commentPlugin({
+            provider: "Giscus",
+            comment: true, //启用评论功能
+            repo: "Zephery/MyWebsite", //远程仓库
+            repoId: "MDEwOlJlcG9zaXRvcnkyMDM2MDIyMDQ=", //对应自己的仓库Id
+            category: "General",
+            categoryId: "DIC_kwDODCK5HM4Ccp32" //对应自己的分类Id
+        }),
+

即可在页面上看到效果

+
颜色可自定义
颜色可自定义
+

四、博客的部署

+

官网也有讲解部署的情况,具体可以看官网Github Pages,整体上看速度还是挺慢的,可以尝试去gitee上部署看一下,之后就可以在pages通过域名访问了。需要在项目下创建.github/workflows/docs.yml文件,具体配置参考官网,不需做任何改动。

+

五、Github pages自定义域名

+

github自带的io域名zephery.github.io,做为一名开发,肯定是用自己的域名是比较好的。需要注意下中间的红色框,前面的是分支,后面的是你项目的路径。一般默认即可,不用修改。

+
pages的配置
pages的配置
+

购买域名->域名解析,即把我的个人域名wenzhihuai.com指向zephery.github.io(通过cname)即可,然后开启强制https。如果DNS一直没有校验通过,那么可能是CAA的原因。通过DNS诊断工具来判断。

+
DNS诊断工具
DNS诊断工具
+

上面的custom domain配置好了之后,但DNS一直没有校验正确,原因是CAA没有正确解析,需要加上即可。

+
0 issue "trust-provider.com"
+0 issuewild "trust-provider.com"
+
CAA记录解析
CAA记录解析
+

之后就可以看到Github Pages的DNS校验成功,并且可以强制开启https了。

+

六、Typora图床

+

之前的图床使用的是七牛云和又拍云,都有免费的额度吧,不过看情况未来前景似乎经营不太好,目前改用了腾讯云。存储容量50GB,每个月外网访问流量10GB,满足个人网站使用。具体的配置过程比较简单,就不再阐述了,可以直接看uPic的官方介绍。

+

七、为自己的内容增加收入

+

有钱才有写作的动力,之前的网站开启了几年的捐赠,总共都没有收到过50块钱,只能从广告这一处想想办法,百度、腾讯广告似乎都不支持个人网站,谷歌可以。配置谷歌广告,网上的教程不少,例如: vuepress配置谷歌广告-通过vue-google-adsense库,缺点是,大部分的文章都是需要在自己的markdown文件中新增特定的标识符。比如:

+
# js 模板引擎 mustache 用法
+
+<ArticleTopAd></ArticleTopAd>
+
+## 一. 使用步骤
+

每一篇文章都要新增显然不符合懒惰的人,下面是个人尝试的解决办法,用的是vuepress2提供的slots插槽。

+
谷歌广告adsense
谷歌广告adsense
+

上面的目的是为了获取data-ad-client和data-ad-slot,其中,data-ad-slot为广告单元,不一样。并且,配置完之后可能需要等一个小时才会生效,不要着急。

+

docs/.vuepress/config.ts

+
head: [
+    [
+        "script",
+        {
+            "data-ad-client": "ca-pub-9037099208128116",
+            async: true,
+            src: "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
+        }
+    ],
+]
+
+...
+alias: {
+    "@theme-hope/components/NormalPage": path.resolve(
+        __dirname,
+        "./components/NormalPage.vue",
+    ),
+},
+

docs/.vuepress/components/NormalPage.vue

+
<template>
+  <normal-page>
+    <template #contentBefore>
+      <ins class="adsbygoogle"
+           style="display:block; text-align:center;width: 90%;margin: 0 auto;"
+           data-ad-layout="in-article"
+           data-ad-format="fluid"
+           data-ad-client="ca-pub-9037099208128116"
+           data-ad-slot="8206550629"></ins>
+    </template>
+  </normal-page>
+</template>
+<script>
+import NormalPage from "vuepress-theme-hope/components/NormalPage.js";
+
+export default {
+  name: "adsense-inline",
+  components: {
+    'normal-page': NormalPage,
+  },
+  mounted() {
+    this.adsenseAddLoad();
+  },
+  methods: {
+    adsenseAddLoad() {
+      let inlineScript = document.createElement("script");
+      inlineScript.type = "text/javascript";
+      inlineScript.text = '(adsbygoogle = window.adsbygoogle || []).push({});'
+      document.getElementsByTagName('body')[0].appendChild(inlineScript);
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+</style>
+

本地是没办法进行调试的,可以从官网插槽演示的文章中用div进行调试,等修改完毕发布之后,即可在自己的网站上看到相关的广告和收入(浏览器要把封禁广告的插件关闭)。

+
出现的广告
出现的广告
+
谷歌广告收入
谷歌广告收入
+

收入虽然低,但是基本上个人没有成本,只需要域名的85块钱。

+

常见问题

+

为什么广告不能正常显示?

+

"建议多写原创高质量的文章出来,AdSense才会匹配出合适的广告,用户感兴趣了才会浏览量增加,你才会有更多的广告收入。"

+

还是得多写一写优质的文章。

+

最后,多帮忙点一下个人网站的广告吧,感恩

+

网站地址:https://www.wenzhihuai.com

+

源码地址:https://github.com/Zephery/MyWebsite

+]]>
+ 2024-01-25T06:44:04.000Z +
+ + + http://www.wenzhihuai.com/open-source-project/ + + 2024-01-24T10:30:22.000Z + jfaowejfoewj

+]]>
+ jfaowejfoewj

+]]>
+ 2024-01-24T10:30:22.000Z +
+ + 中间件 + http://www.wenzhihuai.com/middleware/ + + 2024-02-16T04:00:26.000Z + + + 2024-01-24T03:35:40.000Z + + + + http://www.wenzhihuai.com/interview/tiktok2023.html + + 2024-01-24T03:25:58.000Z + tt一面:
+1.全程项目
+2.lc3,最长无重复子串,滑动窗口解决

+

tt二面:
+全程基础,一直追问
+1.java内存模型介绍一下
+2.volatile原理
+3.内存屏障,使用场景?(我提了在gc中有使用)
+4.gc中具体是怎么使用内存屏障的,详细介绍
+5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
+6.线程内存分配方式,tlab相关
+7.happens-before介绍一下
+8.总线风暴是什么,怎么解决
+9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
+10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
+11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

]]>
+ tt一面:
+1.全程项目
+2.lc3,最长无重复子串,滑动窗口解决

+

tt二面:
+全程基础,一直追问
+1.java内存模型介绍一下
+2.volatile原理
+3.内存屏障,使用场景?(我提了在gc中有使用)
+4.gc中具体是怎么使用内存屏障的,详细介绍
+5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
+6.线程内存分配方式,tlab相关
+7.happens-before介绍一下
+8.总线风暴是什么,怎么解决
+9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
+10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
+11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

+

tt三面:
+大部分聊项目,基础随便问了问
+1.分布式事务,两阶段三阶段流程,区别
+2.mysql主从,同步复制,半同步复制,异步复制
+3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转
+4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万

+]]>
+ 2023-04-02T12:15:06.000Z +
+ + Mysql + http://www.wenzhihuai.com/database/mysql/1mysql.html + + 2024-02-06T07:53:39.000Z + img
img
+]]>
+ img
img
+]]>
+ 2023-03-26T12:44:23.000Z +
+ + 数据库缓存 + http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html + + 2024-01-25T11:42:16.000Z + 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

+

将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。

+

禁用原因:

+

1.命中率低

+

2.写时所有都失效

+

禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。

]]>
+ 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

+

将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。

+

禁用原因:

+

1.命中率低

+

2.写时所有都失效

+

禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。

+

查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182

+]]>
+ 2023-03-26T12:44:23.000Z +
+ + spark on k8s operator + http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html + + 2024-01-27T18:29:27.000Z + Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

+

使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。

+

Spark Operator包括如下几个组件:

+
    +
  1. SparkApplication控制器:该控制器用于创建、更新、删除SparkApplication对象,同时控制器还会监控相应的事件,执行相应的动作。
  2. +
  3. Submission Runner:负责调用spark-submit提交Spark作业,作业提交的流程完全复用Spark on K8s的模式。
  4. +
  5. Spark Pod Monitor:监控Spark作业相关Pod的状态,并同步到控制器中。
  6. +
  7. Mutating Admission Webhook:可选模块,基于注解来实现Driver/Executor Pod的一些定制化需求。
  8. +
  9. SparkCtl:用于和Spark Operator交互的命令行工具。
  10. +
]]>
+ Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

+

使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。

+

Spark Operator包括如下几个组件:

+
    +
  1. SparkApplication控制器:该控制器用于创建、更新、删除SparkApplication对象,同时控制器还会监控相应的事件,执行相应的动作。
  2. +
  3. Submission Runner:负责调用spark-submit提交Spark作业,作业提交的流程完全复用Spark on K8s的模式。
  4. +
  5. Spark Pod Monitor:监控Spark作业相关Pod的状态,并同步到控制器中。
  6. +
  7. Mutating Admission Webhook:可选模块,基于注解来实现Driver/Executor Pod的一些定制化需求。
  8. +
  9. SparkCtl:用于和Spark Operator交互的命令行工具。
  10. +
+

安装

+
# 添加源
+helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator
+# 安装
+helm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace
+

这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。

+
forbidden: error looking up service account default/spark: serviceaccount \"spark\" not found.\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport
+

同时也要安装rbac

+

rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见rbac.yaml

+

执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。

+

代码如下:

+
 SparkConf sparkConf = new SparkConf().setAppName("Spark WordCount Application (java)");
+        sparkConf.set("es.nodes", "xxx")
+                .set("es.port", "8xx0")
+                .set("es.nodes.wan.only", "true")
+                .set("es.net.http.auth.user", "xxx")
+                .set("es.net.http.auth.pass", "xxx-xxx");
+        try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {
+            JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce").values();
+
+            System.out.println(esRDD.partitions().size());
+
+            esRDD.map(x -> x.get("customer_full_name"))
+                    .countByValue()
+                    .forEach((x, y) -> System.out.println(x + ":" + y));
+            TimeUnit.HOURS.sleep(1);
+
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+

打包之后kubectl create -f k8s.yaml即可

+
apiVersion: "sparkoperator.k8s.io/v1beta2"
+kind: SparkApplication
+metadata:
+  name: spark-demo
+  namespace: default
+spec:
+  type: Scala
+  mode: cluster
+  image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3
+  imagePullPolicy: Always
+  mainClass: com.spark.es.demo.EsReadGroupByName
+  mainApplicationFile: "local:///opt/spark/examples/jars/app.jar"
+  sparkVersion: "3.1.1"
+  restartPolicy:
+    type: Never
+  driver:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    coreLimit: "1200m"
+    memory: "512m"
+    labels:
+      version: 3.1.1
+    serviceAccount: sparkoperator
+  executor:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    instances: 1
+    memory: "512m"
+    labels:
+      version: 3.1.1
+
spark-demo-driver                                          1/1     Running   0          2m30s
+spark-wordcount-application-java-0ac352810a9728e1-exec-1   1/1     Running   0          2m1s
+

同时会自动生成相应的service。

+
image-20220528203941897
image-20220528203941897
+image-20220528203816649 +

Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,

+

源码解析

+architecture-diagram +

Spark Operator的主要组件如下:

+

1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;

+

2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;

+

3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;

+

4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。

+

5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。

+image-20220612194403742 +

apis:用户编写yaml时的解析

+

Batchscheduler:批处理的调度,提供了支持volcano的接口

+

Client:

+

leader的选举

+
electionCfg := leaderelection.LeaderElectionConfig{
+   Lock:          resourceLock,
+   LeaseDuration: *leaderElectionLeaseDuration,
+   RenewDeadline: *leaderElectionRenewDeadline,
+   RetryPeriod:   *leaderElectionRetryPeriod,
+   Callbacks: leaderelection.LeaderCallbacks{
+      OnStartedLeading: func(c context.Context) {
+         close(startCh)
+      },
+      OnStoppedLeading: func() {
+         close(stopCh)
+      },
+   },
+}
+
+elector, err := leaderelection.NewLeaderElector(electionCfg)
+

参考:

+

1.k8s client-go中Leader选举实现

+

2.[Kubernetes基于leaderelection选举策略实现组件高可用

+]]>
+ 2022-06-12T12:19:23.000Z +
+ + elastic spark + http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html + + 2024-01-25T11:42:16.000Z + Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
+(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
+(2)数据RDBMS:mysql、oracle、mssql
+(3)NOSQL数据库:HBase、ES、Redis
+(4)消息对象:Redis

+

elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。

]]>
+ Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
+(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
+(2)数据RDBMS:mysql、oracle、mssql
+(3)NOSQL数据库:HBase、ES、Redis
+(4)消息对象:Redis

+

elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。

+
Spark - Apache Spark
Spark - Apache Spark
+

一、原生RDD支持

+

1.1 基础配置

+

相关库引入:

+
        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>elasticsearch-spark-30_2.13</artifactId>
+            <version>8.1.3</version>
+        </dependency>
+

SparkConf配置,更多详细的请点击这里或者源码ConfigurationOptions

+
public static SparkConf getSparkConf() {
+    SparkConf sparkConf = new SparkConf().setAppName("elasticsearch-spark-demo");
+    sparkConf.set("es.nodes", "host")
+            .set("es.port", "xxxxxx")
+            .set("es.nodes.wan.only", "true")
+            .set("es.net.http.auth.user", "elxxxxastic")
+            .set("es.net.http.auth.pass", "xxxx")
+            .setMaster("local[*]");
+    return sparkConf;
+}
+

1.2 读取es数据

+

这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。

+
public static void main(String[] args) {
+    SparkConf conf = getSparkConf();
+    try (JavaSparkContext jsc = new JavaSparkContext(conf)) {
+
+        JavaPairRDD<String, Map<String, Object>> esRDD =
+                JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce");
+        esRDD.collect().forEach(System.out::println);
+    }
+}
+

esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。

+

1.3 写数据

+

支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。

+
public static void jsonWrite(){
+    String json1 = "{\"reason\" : \"business\",\"airport\" : \"SFO\"}";
+    String json2 = "{\"participants\" : 5,\"airport\" : \"OTP\"}";
+    JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));
+    JavaEsSpark.saveJsonToEs(stringRDD, "spark-json");
+}
+

比较常用的读写也就这些,更多可以看下官网相关介绍。

+

二、Spark Streaming

+

spark的实时处理,es5.0的时候开始支持,目前

+

三、Spark SQL

+

四、Spark Structure Streaming

+

五、Spark on kubernetes Operator

+

参考:

+

1.Apache Spark support

+

2.elasticsearch-hadoop

+

3.使用SparkSQL操作Elasticsearch - Spark入门教程

+]]>
+ 2022-05-03T11:42:30.000Z +
+ + 基于kubernetes的分布式限流 + http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html + + 2024-01-26T03:58:56.000Z + 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

+

一、概念

+

限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。

+

1.1 使用场景

+

限流可以应对:

+
    +
  • 热点业务带来的突发请求;
  • +
  • 调用方 bug 导致的突发请求;
  • +
  • 恶意攻击请求。
  • +
]]>
+ 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

+

一、概念

+

限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。

+

1.1 使用场景

+

限流可以应对:

+
    +
  • 热点业务带来的突发请求;
  • +
  • 调用方 bug 导致的突发请求;
  • +
  • 恶意攻击请求。
  • +
+

1.2 维度

+

对于限流场景,一般需要考虑两个维度的信息:
+时间
+限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定
+资源
+基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。
+  限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。

+
image.png
image.png
+

1.3 分布式限流

+

分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。

+

二、分布式限流常用方案

+

基于Guava的客户端限流
+Guava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。

+

网关层限流
+服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。

+

中间件限流
+将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。

+

限流组件
+目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。

+

Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?

+

对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。

+

三、基于kubernetes的分布式限流

+

在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。

+
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
+

3.1 kubernetes中的副本数

+

在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。

+
func CountDeploymentSize(namespace string, deploymentName string) *int32 {
+	deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
+	if err != nil {
+		return nil
+	}
+	return deployment.Spec.Replicas
+}
+

用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。

+

3.2 rateLimiter的创建

+

在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。

+
private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
+        .maximumSize(10_000)
+        .refreshAfterWrite(20, TimeUnit.MINUTES)
+        .build(this::createRateLimit);
+//定义一个默认最小的QPS
+private static final Integer minQpsLimit = 3000;
+

之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。

+
public RateLimiter createRateLimit(String key) {
+    log.info("createRateLimit,key:{}", key);
+    int totalLimit = 获取总限频数,可以在数据库中定义
+    Integer replicas = kubernetesService.getDeploymentReplicas();
+    RateLimiter rateLimiter;
+    if (totalLimit > 0 && replicas == null) {
+        rateLimiter = RateLimiter.create(totalLimit);
+    } else if (totalLimit > 0) {
+        int nodeQpsLimit = totalLimit / replicas;
+        rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
+    } else {
+        rateLimiter = RateLimiter.create(minQpsLimit);
+    }
+    log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
+    return rateLimiter;
+}
+

3.3 rateLimiter的获取

+

根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽

+
public RateLimiter getRateLimiter(String key) {
+  return loadingCache.get(key);
+}
+

3.4 filter里的判断

+

最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS

+
public class RateLimiterFilter implements Filter {
+    @Resource
+    private RateLimiterService rateLimiterService;
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+        String key = httpServletRequest.getHeader("key");
+        RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
+        if (rateLimiter != null) {
+            if (rateLimiter.tryAcquire()) {
+                filterChain.doFilter(request, response);
+            } else {
+                httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+            }
+        } else {
+            filterChain.doFilter(request, response);
+        }
+    }
+}
+

四、性能压测

+

为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。

+

无限流

+
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
+

使用redis限流

+

其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显

+
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
+

自研限流

+

性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越

+
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
+

五、其他问题

+

5.1 对于保证qps限频准确的时候,应该怎么解决呢?

+

在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的

+

5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力

+

理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的

+

5.3 如果有多个副本,怎么保证请求是均匀的

+

这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。

+

参考

+

1.常见的分布式限流解决方案
+2.分布式服务限流实战
+3.高性能

+]]>
+ 2022-04-09T05:35:51.000Z +
+ + 【elasticsearch】搜索过程详解 + http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html + + 2024-02-16T17:21:55.000Z + 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

+

SearchType

+

QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
+DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。

]]>
+ 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

+

SearchType

+

QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
+DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。

+

为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。

+
POST aaaa-16/_doc
+{
+  "@timestamp": "2022-02-16T16:21:15.000Z",
+  "word":"16"
+}
+
+
+POST aaaa-17/_doc
+{
+  "@timestamp": "2022-02-17T16:21:15.000Z",
+  "word":"17"
+}
+
+POST aaaa-18/_doc
+{
+  "@timestamp": "2022-02-18T16:21:15.000Z",
+  "word":"18"
+}
+

即可在kibana上看到3条数据

+
image-20220219195141327
image-20220219195141327
+

此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?

+
GET aaaa*/_search
+{
+  "query": {
+    "range": {
+      "@timestamp": {
+        "gte": "2022-02-18T10:21:15.000Z",
+        "lte": "2022-02-18T17:21:15.000Z"
+      }
+    }
+  }
+}
+

正好命中一条记录返回。

+
{
+  "took" : 2,
+  "timed_out" : false,
+  "_shards" : {
+    "total" : 3,
+    "successful" : 3,
+    "skipped" : 0,
+    "failed" : 0
+  },
+  "hits" : {
+    "total" : {
+      "value" : 1,
+      "relation" : "eq"
+    },
+    "max_score" : 1.0,
+    "hits" : [
+      {
+        "_index" : "aaaa-18",
+        "_id" : "0zB2O38BoMIMP8QzHgdq",
+        "_score" : 1.0,
+        "_source" : {
+          "@timestamp" : "2022-02-18T16:21:15.000Z",
+          "word" : "18"
+        }
+      }
+    ]
+  }
+}
+

一、es的分布式搜索过程

+

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。当搜索任务执行在分布式系统上时,整体流程如下图所示。图片来源Elasitcsearch源码解析与优化实战

+
2
2
+

1.1 搜索入口

+

整个http请求的入口,主要使用的是Netty4HttpRequestHandler。

+
@ChannelHandler.Sharable
+class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {
+        final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();
+        boolean success = false;
+        try {
+            serverTransport.incomingRequest(httpRequest, channel);
+            success = true;
+        } finally {
+            if (success == false) {
+                httpRequest.release();
+            }
+        }
+    }
+}
+

二、初步调用流程

+

调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction

+

协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。

+

特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。

+
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {
+  ...
+    TaskHolder taskHolder = new TaskHolder();
+    Task task = client.executeLocally(action, request, new ActionListener<>() {
+        @Override
+        public void onResponse(Response response) {
+            try {
+                closeListener.unregisterTask(taskHolder);
+            } finally {
+                listener.onResponse(response);
+            }
+        }
+    });
+  ...
+}
+

其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程

+
private SearchPhase searchAsyncAction(...) {
+  ...
+    final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();
+  AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {
+    case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);
+    case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);
+  };
+  return searchAsyncAction;
+  ...
+}
+

之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。

+
此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction
+private void executePhase(SearchPhase phase) {
+    try {
+        phase.run();
+    } catch (Exception e) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(new ParameterizedMessage("Failed to execute [{}] while moving to [{}] phase", request, phase.getName()), e);
+        }
+        onPhaseFailure(phase, "", e);
+    }
+}
+

三、协调节点

+

两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。

+
23
23
+

3.1 query阶段

+

图片来源官网,比较旧,但任然可用

+
12
12
+

(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。
+(2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。
+(3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。

+
在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构: 
+public RestChannelConsumer prepareRequest(.. .) {
+    SearchRequest searchRequest = new SearchRequest();
+    request.withContentOrSourceParamParserOrNull (parser ->
+        parseSearchRequest (searchRequest, request, parser, setSize));
+    ...
+}
+

3.1.1 构造目的shard列表

+

将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:

+
private void executeSearch(.. .) {
+  ...
+    final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
+    localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
+    OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
+    assert finalIndices != null;
+    return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
+    }).collect(Collectors.toList());
+	...
+}
+

查看结果

+
241
241
+

3.1.2 对所有分片进行搜索

+
AbstractSearchAsyncAction.run
+对每个分片进行搜索查询
+for (int i = 0; i < shardsIts.size(); i++) {
+    final SearchShardIterator shardRoutings = shardsIts.get(i);
+    assert shardRoutings.skip() == false;
+    assert shardIndexMap.containsKey(shardRoutings);
+    int shardIndex = shardIndexMap.get(shardRoutings);
+    performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
+}
+

其中shardsIts是所有aaaa*的所有索引+其中一个副本

+
2141
2141
+

3.1.3 分片具体的搜索过程

+
AbstractSearchAsyncAction.performPhaseOnShard
+private void performPhaseOnShard(. ..) {
+    executePhaseOnShard(.. .) {
+        //收到执行成功的回复
+        public void inne rOnResponse (FirstResult result) {
+            maybeFork (thread, () -> onShardResult (result,shardIt) );
+        }
+        //收到执行失败的回复
+        public void onFailure (Exception t) {
+            maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));
+        }
+    });
+}
+

分片结果,当前线程

+
//AbstractSearchAsyncAction.onShardResult
+...
+private void onShardResult (FirstResult result, SearchShardIterator shardIt) {
+    onShardSuccess(result);
+    success fulShardExecution(shardIt);
+}
+...
+//AbstractSearchAsyncAction.onShardResultConsumed
+private void successfulShardExecution (SearchShardIterator shardsIt) {
+    //计数器累加.
+    final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);
+    //检查是否收到全部回复
+    if (xTotalOps == expectedTotalOps) {
+        onPhaseDone ();
+    } else if (xTota1Ops > expectedTotal0ps) {
+        throw new AssertionError(. ..);
+    }
+}
+
412
412
+

此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段

+

3.2 Fetch阶段

+

取回阶段,图片来自官网

+
412412
412412
+

(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document

+

(2)各个shard将document返回给coordinate node

+

(3)coordinate node将合并后的document结果返回给client客户端

+

3.2.1 FetchSearchPhase(对应上面的1)

+

Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。

+
private void innerRun() throws Exception {
+    final int numShards = context.getNumShards();
+    final boolean isScrollSearch = context.getRequest().scroll() != null;
+    final List<SearchPhaseResult> phaseResults = queryResults.asList();
+    final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
+    final boolean queryAndFetchOptimization = queryResults.length() == 1;
+    final Runnable finishPhase = () -> moveToNextPhase(
+        searchPhaseController,
+        queryResults,
+        reducedQueryPhase,
+        queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()
+    );
+            for (int i = 0; i < docIdsToLoad.length; i++) {
+                IntArrayList entry = docIdsToLoad[i];
+                SearchPhaseResult queryResult = queryResults.get(i);
+                if (entry == null) { 
+                    if (queryResult != null) {
+                        releaseIrrelevantSearchContext(queryResult.queryResult());
+                        progressListener.notifyFetchResult(i);
+                    }
+                    counter.countDown();
+                }else{
+                    executeFetch(
+                            queryResult.getShardIndex(),
+                            shardTarget,
+                            counter,
+                            fetchSearchRequest,
+                            queryResult.queryResult(),
+                            connection
+                        );
+                }
+        }
+    }
+}
+

再看源码:

+
启动一个线程来fetch
+AbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch
+
+private void executeFetch(...) {
+    context.getSearchTransport()
+        .sendExecuteFetch(
+            connection,
+            fetchSearchRequest,
+            context.getTask(),
+            new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
+                @Override
+                public void innerOnResponse(FetchSearchResult result) {
+                  progressListener.notifyFetchResult(shardIndex);
+                  counter.onResult(result);
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                  progressListener.notifyFetchFailure(shardIndex, shardTarget, e);
+                  counter.onFailure(shardIndex, shardTarget, e);
+                }
+            }
+        );
+}
+
13413
13413
+

counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。

+
# CountedCollector.class
+void countDown() {
+    assert counter.isCountedDown() == false : "more operations executed than specified";
+    if (counter.countDown()) {
+        onFinish.run();
+    }
+}
+

moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。

+

3.2.2 ExpandSearchPhase(对应上图的2)

+

AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。

+
image-20220317000858513
image-20220317000858513
+

ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:

+
412412
412412
+

四、数据节点

+

4.1 执行query、fetch流程

+

执行本流程的线程池: search。

+

对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。

+
public static void registerRequestHandler(TransportService transportService, SearchService searchService) {
+  ...
+  transportService.registerRequestHandler(
+    QUERY_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    ShardSearchRequest::new,
+    (request, channel, task) -> searchService.executeQueryPhase(
+      request,
+      (SearchShardTask) task,
+      new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)
+    )
+  );
+  ...
+}
+

4.1.1 执行query请求

+
# SearchService
+public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {
+    assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1
+        : "empty responses require more than one shard";
+    final IndexShard shard = getShard(request);
+    rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {
+        ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);
+    }));
+}
+

其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。

+
1
1
+

主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:

+
public void execute (SearchContext searchContext) {
+    aggregationPhase.preProcess (searchContext);
+    boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);
+    if (rescore) {
+        rescorePhase.execute (searchContext);
+        suggestPhase.execute (searchContext);
+        aggregationPhase.execute (searchContext);
+    }
+}
+

其中包含几个核心功能:

+
    +
  • execute(),调用Lucene、searcher.search()实现搜索
  • +
  • rescorePhase,全文检索且需要打分
  • +
  • suggestPhase,自动补全及纠错
  • +
  • aggregationPhase,实现聚合
  • +
+

4.1.2 fetch流程

+

对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。

+
transportService.registerRequestHandler(
+    QUERY_FETCH_SCROLL_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    InternalScrollSearchRequest::new,
+    (request, channel, task) -> {
+        searchService.executeFetchPhase(
+            request,
+            (SearchShardTask) task,
+            new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)
+        );
+    }
+);
+

对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。

+
# FetchPhase
+public void execute(SearchContext context) {
+    Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();
+    SearchHits hits = null;
+    try {
+      //lucene构建搜索的结果
+        hits = buildSearchHits(context, profiler);
+    } finally {
+        ProfileResult profileResult = profiler.finish();
+        // Only set the shardResults if building search hits was successful
+        if (hits != null) {
+            context.fetchResult().shardResult(hits, profileResult);
+        }
+    }
+}
+

五、数据返回

+

入口:RestCancellableNodeClient.doExecute
+Task task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
+image-20220319003638991

+

其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse

+
public void sendResponse(RestResponse restResponse) {
+  ...
+   httpChannel.sendResponse(httpResponse, listener);
+  ...
+}
+

最后由Netty4HttpChannel.sendResponse来响应请求。

+

六、总结

+

当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。

+

本文参考

+
    +
  1. Elasitcsearch源码解析与优化实战
  2. +
  3. Elasticsearch源码分析-搜索分析(一)
  4. +
  5. Elasticsearch源码分析-搜索分析(二)
  6. +
  7. Elasticsearch源码分析-搜索分析(三)
  8. +
  9. Elasticsearch 通信模块的分析
  10. +
  11. Elasticsearch 网络通信线程分析
  12. +
+]]>
+ 2022-03-20T15:14:40.000Z +
+ + kafka面试题 + http://www.wenzhihuai.com/middleware/kafka/kafka.html + + 2024-01-25T17:34:45.000Z + 1、请说明什么是Apache Kafka?
+Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

+

2、说说Kafka的使用场景?
+①异步处理
+②应用解耦
+③流量削峰
+④日志处理
+⑤消息通讯等。

+

3、使用Kafka有什么优点和缺点?
+优点:
+①支持跨数据中心的消息复制;
+②单机吞吐量:十万级,最大的优点,就是吞吐量高;
+③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;
+④时效性:ms级;
+⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
+⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;
+⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。

]]>
+ 1、请说明什么是Apache Kafka?
+Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

+

2、说说Kafka的使用场景?
+①异步处理
+②应用解耦
+③流量削峰
+④日志处理
+⑤消息通讯等。

+

3、使用Kafka有什么优点和缺点?
+优点:
+①支持跨数据中心的消息复制;
+②单机吞吐量:十万级,最大的优点,就是吞吐量高;
+③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;
+④时效性:ms级;
+⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
+⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;
+⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。

+

缺点:
+①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序;
+②有可能消息重复消费;
+③依赖zookeeper进行元数据管理,等等。

+

4、为什么说Kafka性能很好,体现在哪里?
+①顺序读写
+②零拷贝
+③分区
+④批量发送
+⑤数据压缩

+

5、请说明什么是传统的消息传递方法?
+传统的消息传递方法包括两种:
+排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。
+发布-订阅:在这个模型中,消息被广播给所有的用户。

+

6、请说明Kafka相对传统技术有什么优势?
+①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。
+②可伸缩:在一组机器上对数据进行分区
+③和简化,以支持更大的数据
+④持久:消息是持久性的,并在集群中进
+⑤行复制,以防止数据丢失。
+⑥设计:它提供了容错保证和持久性

+

7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗?
+Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。
+不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。
+Zookeeper主要用于在集群中不同节点之间进行通信
+在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
+除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

+

8、解释Kafka的用户如何消费信息?
+在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。

+

9、解释如何提高远程用户的吞吐量?
+如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。

+

10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息?
+在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。
+这里有两种方法,可以在数据生成时准确地获得一个语义:
+每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功
+在消息中包含一个主键(UUID或其他),并在用户中进行反复制

+

11、解释如何减少ISR中的扰动?broker什么时候离开ISR?
+ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。

+

12、Kafka为什么需要复制?
+Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。

+

13、如果副本在ISR中停留了很长时间表明什么?
+如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。

+

14、请说明如果首选的副本不在ISR中会发生什么?
+如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。

+

15、有可能在生产后发生消息偏移吗?
+在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。

+

作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。

+

16、Kafka的设计时什么样的呢?
+Kafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息

+

17、数据传输的事物定义有哪三种?
+(1)最多一次:
+消息不会被重复发送,最多被传输一次,但也有可能一次不传输
+(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
+(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

+

18、Kafka判断一个节点是否还活着有那两个条件?
+(1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接
+(2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久

+

19、producer是否直接将数据发送到broker的leader(主节点)?
+producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。

+

20、Kafa consumer是否可以消费指定分区消息?
+Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的

+

21、Kafka消息是采用Pull模式,还是Push模式?
+Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式

+

Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略

+

Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

+

22、Kafka存储在硬盘上的消息格式是什么?
+消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。
+消息长度: 4 bytes (value: 1+4+n)
+版本号: 1 byte
+CRC校验码: 4 bytes
+具体的消息: n bytes

+

23、Kafka高效文件存储设计特点:
+(1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
+(2).通过索引信息可以快速定位message和确定response的最大大小。
+(3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
+(4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

+

24、Kafka 与传统消息系统之间有三个关键区别
+(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留
+(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
+(3).Kafka 支持实时的流式处理

+

25、Kafka创建Topic时如何将分区放置到不同的Broker中
+副本因子不能大于 Broker 的个数;
+第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
+其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
+剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的

+

26、Kafka新建的分区会在哪个目录下创建
+在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。

+

27、partition的数据如何保存到硬盘
+topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名

+

28、kafka的ack机制
+request.required.acks有三个值 0 1 -1
+0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据
+1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失
+-1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失

+

29、Kafka的消费者如何消费数据
+消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。

+

30、消费者负载均衡策略
+结合consumer的加入和退出进行再平衡策略。

+

31、kafka消息数据是否有序?
+消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。

+

32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中
+每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition

+

33、kafka consumer 什么情况会触发再平衡reblance?
+①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。
+②订阅主题分区发生变化,所有消费者也都要再平衡。

+

34、描述下kafka consumer 再平衡步骤?
+①关闭数据拉取线程,情空队列和消息流,提交偏移量;
+②释放分区所有权,删除zk中分区和消费者的所有者关系;
+③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区;
+④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息;
+⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。

+

35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么
+36、Kafka中的HW、LEO、LSO、LW等分别代表什么?
+37、Kafka中是怎么体现消息顺序性的?
+38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
+39、Kafka生产者客户端的整体结构是什么样子的?
+40、Kafka生产者客户端中使用了几个线程来处理?分别是什么?
+41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷?
+42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段?
+43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
+44、有哪些情形会造成重复消费?
+45、那些情景下会造成消息漏消费?
+46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?
+47、简述消费者与消费组之间的关系
+48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
+49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
+50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
+51、创建topic时如何选择合适的分区数?
+52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么?
+53、优先副本是什么?它有什么特殊的作用?
+54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理
+55、简述Kafka的日志目录结构
+56、Kafka中有那些索引文件?
+57、如果我指定了一个offset,Kafka怎么查找到对应的消息?
+58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息?
+59、聊一聊你对Kafka的Log Retention的理解
+60、聊一聊你对Kafka的Log Compaction的理解
+61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层)
+62、聊一聊Kafka的延时操作的原理
+63、聊一聊Kafka控制器的作用
+64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器)
+65、Kafka中的幂等是怎么实现的
+66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”)
+67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
+68、失效副本是指什么?有那些应对措施?
+69、多副本下,各个副本中的HW和LEO的演变过程
+70、为什么Kafka不支持读写分离?
+71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch)
+72、Kafka中怎么实现死信队列和重试队列?
+73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?)
+74、Kafka中怎么做消息审计?
+75、Kafka中怎么做消息轨迹?
+76、Kafka中有那些配置参数比较有意思?聊一聊你的看法
+77、Kafka中有那些命名比较有意思?聊一聊你的看法
+78、Kafka有哪些指标需要着重关注?
+79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同)
+80、Kafka的那些设计让它有如此高的性能?
+81、Kafka有什么优缺点?
+82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点?
+83、为什么选择Kafka?
+84、在使用Kafka的过程中遇到过什么困难?怎么解决的?
+85、怎么样才能确保Kafka极大程度上的可靠性?
+86、聊一聊你对Kafka生态的理解

+]]>
+ 2022-03-20T15:14:40.000Z +
+ + 2017 + http://www.wenzhihuai.com/life/2017.html + + 2024-01-26T04:53:47.000Z + 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
+17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

+
]]>
+ 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
+17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

+
+

回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...

+

春节期间,把自己的网站整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~

+
+

疲惫的面试历程

+

2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。

+

**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?

+

然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。

+

有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。

+

没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?

+

这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。

+

突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。

+

还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。
+  4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。

+

感情、我们毕业了

+

5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。
+  5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。
+  6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。

+
+

准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。

+

996的工作

+

七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。

+

没有环境,就自己创造,下班有时间就改改自己的网站http://www.wenzhihuai.com,GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。

+
+

决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?

+

双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。

+
+

孤独

+

珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!!
+远看:

+
+

近看:

+
+

题外话:
+有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
+今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
+《企业IT架构转型之道》是今年最好的架构图书,没有之一。

+

期望

+

想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗

+
+]]>
+ 2020-01-25T13:18:10.000Z +
+ + 2019 + http://www.wenzhihuai.com/life/2019.html + + 2024-01-26T04:53:47.000Z + 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
+19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

+

工作

+

从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。

]]>
+ 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
+19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

+

工作

+

从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。

+

之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。

+

工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈

+
+

学习

+

项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。
+过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。
+操作系统感觉还是要好好学学了,加油

+
+

Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。

+

感情

+

吵架了,没心思写

+

运动

+

过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。

+
+

还爬了两次南山

+
+

其他

+

看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。
+过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。

+

2020展望

+

每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!

+

当然,如果有可能的话,我想去CSIG

+]]>
+ 2020-01-25T13:18:10.000Z +
+ + 被挖矿攻击 + http://www.wenzhihuai.com/java/serverlog.html + + 2024-01-24T10:30:22.000Z + 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
+
+stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

]]>
+ 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
+
+stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

+

使用top查看:
+

+
+
+

启动iptables,参考http://www.setphp.com/981.html
+http://www.setphp.com/981.html

+
iptables -A INPUT -s xmr.crypto-pool.fr -j DROP
+iptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP
+

2017-10-02 再次遭到挖矿攻击

+

2017-11-20

+

19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。

+

2017-12-03

+
+

2017-12-05 wipefs

+
+]]>
+ 2020-01-25T13:10:49.000Z +
+ + 2018 + http://www.wenzhihuai.com/life/2018.html + + 2024-01-26T04:53:47.000Z + 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

+

想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。

+

心惊胆战的裸辞经历

+

其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。
+去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。

]]>
+ 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

+

想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。

+

心惊胆战的裸辞经历

+

其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。
+去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。

+

and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好

+
+

工作工作

+

入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。
+公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。
+去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。
+这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。

+
+

跑步跑步

+

平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。

+
+

Others

+
    +
  1. 拿到了深圳户口,全程只花了15天,感谢学妹and家人
  2. +
  3. 买了第一台MacBook Pro,对于Java的我真心不好用
  4. +
  5. 盼了好几年的老姐结婚,18年终于了解了这心事
  6. +
  7. 养了只猫,在经历了好几个月的早起之后,晚上睡觉锁厨房,终于安分了,对不起了哈哈哈
  8. +
+
+

2019

+

想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱

+]]>
+ 2020-01-25T13:10:49.000Z +
+ + Redis缓存 + http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html + + 2024-02-06T07:53:39.000Z + 一、概述 +

1.1 缓存介绍

+

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
+缓存常用语:
+数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
+可查看Redis实战(一) 使用缓存合理性

]]>
+ 一、概述 +

1.1 缓存介绍

+

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
+缓存常用语:
+数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
+可查看Redis实战(一) 使用缓存合理性

+

1.2 本站缓存架构

+

从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。

+
+

步骤:
+(1)用户发送一个请求到nginx,nginx对请求进行分发。
+(2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。
+(3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。
+(4)查询数据库的时候,mysql作了主主备份。

+

二、Mybatis缓存

+

2.1 mybatis一级缓存

+

Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。

+
+

我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap<k,v>来进行缓存。
+(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
+(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。
+(3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

+
![](http://image.wenzhihuai.com/images/20180120022427.png)
+

2.2 mybatis二级缓存

+

Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
+

+

2.2.1 MyBatis二级缓存的划分

+

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:

+
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);
+b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);
+

2.2.2 二级缓存的开启

+

在mybatis的配置文件中添加:

+
<settings>
+   <!--开启二级缓存-->
+    <setting name="cacheEnabled" value="true"/>
+</settings>
+

然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):

+
    <cache eviction="LRU"
+           type="org.apache.ibatis.cache.impl.PerpetualCache"
+           flushInterval="120000"
+           size="1024"
+           readOnly="true"/>
+

2.2.3 使用第三方支持的二级缓存的实现

+
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache  type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
+
    +
  1. MyBatis自身提供的缓存实现;
  2. +
  3. 用户自定义的Cache接口实现;
  4. +
  5. 跟第三方内存缓存库的集成;
    +具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
  6. +
+

MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):

+
+

2.3 Mybatis在分布式环境下脏读问题

+

(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
+(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
+(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。

+

下面将介绍使用Redis集中式缓存在个人网站的应用。

+

三、Redis缓存

+

Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。

+
+

3.1 Spring Cache

+

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
+下面是Spring Cache常用的注解:

+

(1)@Cacheable
+@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

+

| 属性 | 介绍 |例子|
+|

+]]>
+ 2020-01-25T13:10:49.000Z +
+ + JVM调优参数 + http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html + + 2024-02-16T04:00:26.000Z + 一、堆大小设置 +

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

+

典型设置:

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+
]]>
+ 一、堆大小设置 +

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

+

典型设置:

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+

-Xmx3550m:设置JVM最大可用内存为3550M。
+-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
+-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
+-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

+
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
+

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
+-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
+-XX:MaxPermSize=16m:设置持久代大小为16m。
+-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

+

二、回收器选择

+

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

+

2.1 吞吐量优先的并行收集器

+

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
+典型配置:

+
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
+

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
+-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
+

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
+

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
+

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

+

2.2 响应时间优先的并发收集器

+

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
+典型配置:

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
+

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
+-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
+

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
+-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

+

三、辅助信息

+

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
+-XX:+PrintGC
+输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
+                [Full GC 121376K->10414K(130112K), 0.0650971 secs]

+

-XX:+PrintGCDetails
+输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
+                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

+

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
+输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
+-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
+输出形式:Application time: 0.5291524 seconds
+-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
+输出形式:Total time for which application threads were stopped: 0.0468229 seconds
+-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
+输出形式:

+
34.702: [GC {Heap before gc invocations=7:
+ def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
+from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
+  to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
+ tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
+ def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
+  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
+  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
+ tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+}
+, 0.0757599 secs]
+

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
+常见配置汇总

+

3.1 堆设置

+

-Xms:初始堆大小
+-Xmx:最大堆大小
+-XX:NewSize=n:设置年轻代大小
+-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
+-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
+-XX:MaxPermSize=n:设置持久代大小

+

3.2 收集器设置

+

-XX:+UseSerialGC:设置串行收集器
+-XX:+UseParallelGC:设置并行收集器
+-XX:+UseParalledlOldGC:设置并行年老代收集器
+-XX:+UseConcMarkSweepGC:设置并发收集器

+

3.3 垃圾回收统计信息

+

-XX:+PrintGC
+-XX:+PrintGCDetails
+-XX:+PrintGCTimeStamps
+-Xloggc:filename

+

3.4 并行收集器设置

+

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
+-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
+-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

+

3.5 并发收集器设置

+

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
+-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

+

四、调优总结

+

4.1 年轻代大小选择

+

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
+吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

+

4.2 年老代大小选择

+

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
+并发垃圾收集信息
+持久代并发收集次数
+传统GC信息
+花在年轻代和年老代回收上的时间比例
+减少年轻代和年老代花费的时间,一般会提高应用的效率
+吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

+

4.3 较小堆引起的碎片问题

+

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
+-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
+-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

+]]>
+ 2020-01-25T13:10:49.000Z +
+ + 1.历史与架构 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html + + 2024-05-11T18:24:35.000Z + 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
+大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

]]>
+ 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
+大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

+
+

由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点有点意思这个页面。
+本文从下面这几个方面来讲讲网站的建立:

+
    +
  1. 建站故事与网站架构
  2. +
  3. lucene搜索的使用
  4. +
  5. 使用quartz来定时备份数据库
  6. +
  7. 使用百度统计api做日志系统
  8. +
  9. Nginx小集群的搭建
  10. +
  11. [数据库]
  12. +
  13. 使用机器学习对微博进行分析
  14. +
  15. 网站性能优化
  16. +
  17. SEO优化
  18. +
+

1.建站故事与网站架构

+

1.1建站过程

+

起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是杨青的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。

+
+

第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。

+

最终版本在考虑时,也找了很多模板,影响深刻的是tale欲思这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。
+页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。
+已经部署在腾讯云服务器上,存储使用了七牛云的cdn。

+

1.2 网站整体技术架构

+

最终版的技术架构图如下:

+
+

网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了张开涛的博客。

+
+

运行流程分析

+
    +
  1. 浏览器发送http请求。/blogdetail.html?blogid=1。
  2. +
  3. tomcat容器初始化,顺序为context-param>listener>filter>servlet,此时,spring中的bean还没有被注入的,不建议在此处加载bean,网站声明了两个类(IPFilter和CacheControlFilter),IPFilter用来拦截IP,CacheControlFilter用来缓存。
  4. +
  5. 初始化Spring。
  6. +
  7. DispatcherServlet——>HandlerMapping进行请求到处理的映射,HandlerMapping将“/blogdetail”路径直接映射到名字为“/blogdetail”的Bean进行处理,即BlogController。
  8. +
  9. 自定义拦截器,其中BaseIntercepter实现了HandleInterceptor的接口,用来记录每次访问的链接以及后台响应的时间。
  10. +
  11. DispatcherServlet——> SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter将HandlerExecutionChain中的处理器适配为BlogController。
  12. +
  13. BlogController执行查询,取得结果集返回数据。
  14. +
  15. blogdetail(ModelAndView的逻辑视图名)——>InternalResourceViewResolver, InternalResourceViewResolver使用JstlView,具体视图页面在/blogdetail.jsp。
  16. +
  17. JstlView(/blogdetail.jsp)——>渲染,将在处理器传入的模型数据(blog=Blog!)在视图中展示出来。
  18. +
  19. 返回响应。
  20. +
+

1.3 日志系统

+

日志系统架构如下:

+
+

日志系统曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。
+出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。

+

1.4 【有点意思】自然语言处理

+

本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击这里尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。

+
+

总结

+

断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:GitHub上求给个Star,以后面试能讲讲这个网站。

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 2.Lucene的使用 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html + + 2024-05-11T18:24:35.000Z + 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

]]>
+ 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

+

Lucene的整体架构

+
image
image
+

搜索引擎的几个重要概念:

+
    +
  1. +

    倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。
    +

    +
  2. +
  3. +

    停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。

    +
  4. +
  5. +

    排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。

    +
  6. +
+

Lucene中的几个概念

+
    +
  1. 文档(Document):文档是一系列域的组合,文档的域则代表一系列域文档相关的内容。
  2. +
  3. 域(Field):每个文档可以包含一个或者多个不同名称的域。
  4. +
  5. 词(Term):Term是搜索的基本单元,与Field相对应,包含了搜索的域的名称和关键词。
  6. +
  7. 查询(Query):一系列Term的条件组合,成为TermQuery,但也有可能是短语查询等。
  8. +
  9. 分词器(Analyzer):主要是用来做分词以及去除停用词的处理。
  10. +
+

索引的建立

+
+

索引的搜索

+
+

lucene在本网站的使用:

+
    +
  1. 搜索 2. 自动分词
  2. +
+

一、搜索

+

注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:

+
<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-core</artifactId><!--lucene核心-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-queryparser</artifactId><!--格式化-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
+    <version>${lucene.version}</version>
+</dependency>
+
    +
  1. 构建索引
  2. +
+
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
+SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
+IndexWriterConfig config = new IndexWriterConfig(analyzer);
+IndexWriter writer = new IndexWriter(dir, config);
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
+writer.addDocument(doc);
+writer.close();
+
    +
  1. 更新与删除
  2. +
+
IndexWriter writer = getWriter();
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
+writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc);   //更新索引
+writer.close();
+
    +
  1. 查询
  2. +
+
private static void search_index(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径,也就是索引所在的位置
+        IndexReader reader = DirectoryReader.open(dir);
+        IndexSearcher searcher = new IndexSearcher(reader);
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        QueryParser parser = new QueryParser("content", analyzer); //查询解析器
+        Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象
+        TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,
+        for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+            System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
+        }
+        reader.close();
+    } catch (IOException | ParseException e) {
+        logger.error(e.toString());
+    }
+}
+
    +
  1. 高亮
  2. +
+
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
+SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
+Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
+highlighter.setTextFragmenter(fragmenter);
+for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+    Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+    String title = doc.get("title");
+    TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
+    String hTitle = highlighter.getBestFragment(tokenStream, title);
+    System.out.println(hTitle);
+}
+

结果

+
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
+
    +
  1. 分页
    +目前lucene分页的方式主要有两种:
    +(1). 每次都全部查询,然后通过截取获得所需要的记录。由于采用了分词与倒排索引,所有速度是足够快的,但是在数据量过大的时候,占用内存过大,容易造成内存溢出
    +(2). 使用searchAfter把数据保存在缓存里面,然后再去取。这种方式对大量的数据友好,但是当数据量比较小的时候,速度会相对慢。
    +lucene中使用searchafter来筛选顺序
  2. +
+
ScoreDoc lastBottom = null;//相当于pageSize
+BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
+QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
+Query query1 = parser1.parse(q);
+booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
+TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits);  //lastBottom(pageSize),pagehits(pagenum)
+
    +
  1. 使用效果
    +全部代码放在这里,代码写的不太好,光从代码规范上就不咋地。在网页上的使用效果如下:
  2. +
+
+

二、lucene自动补全

+

百度、谷歌等在输入文字的时候会弹出补全框,如下图:

+
+

在搭建lucene自动补全的时候,也有考虑过使用SQL语句中使用like来进行,主要还是like对数据库压力会大,而且相关度没有lucene的高。主要使用了官方suggest库以及autocompelte.js这个插件。
+suggest的原理看这,以及索引结构看这

+

使用:

+
    +
  1. 导入maven包
  2. +
+
<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-suggest</artifactId>
+    <version>6.6.0</version>
+</dependency>
+
    +
  1. 如果想将结果反序列化,声明实体类的时候要加上:
  2. +
+
public class Blog implements Serializable {
+
    +
  1. 实现InputIterator接口
  2. +
+
InputIterator的几个方法:
+long weight():返回的权重值,大小会影响排序,默认是1L
+BytesRef payload():对某个对象进行序列化
+boolean hasPayloads():是否有设置payload信息
+Set<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤
+boolean hasContexts():判断是否有下一个,默认为false
+
public class BlogIterator implements InputIterator {
+    /**
+     * logger
+     */
+    private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
+    private Iterator<Blog> blogIterator;
+    private Blog currentBlog;
+
+    public BlogIterator(Iterator<Blog> blogIterator) {
+        this.blogIterator = blogIterator;
+    }
+
+    @Override
+    public boolean hasContexts() {
+        return true;
+    }
+
+    @Override
+    public boolean hasPayloads() {
+        return true;
+    }
+
+    public Comparator<BytesRef> getComparator() {
+        return null;
+    }
+
+    @Override
+    public BytesRef next() {
+        if (blogIterator.hasNext()) {
+            currentBlog = blogIterator.next();
+            try {
+                //返回当前Project的name值,把blog类的name属性值作为key
+                return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 将Blog对象序列化存入payload
+     * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法
+     */
+    @Override
+    public BytesRef payload() {
+        try {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream out = new ObjectOutputStream(bos);
+            out.writeObject(currentBlog);
+            out.close();
+            BytesRef bytesRef = new BytesRef(bos.toByteArray());
+            return bytesRef;
+        } catch (IOException e) {
+            logger.error("", e);
+            return null;
+        }
+    }
+
+    /**
+     * 文章标题
+     */
+    @Override
+    public Set<BytesRef> contexts() {
+        try {
+            Set<BytesRef> regions = new HashSet<BytesRef>();
+            regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
+            return regions;
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Couldn't convert to UTF-8");
+        }
+    }
+
+    /**
+     * 返回权重值,这个值会影响排序
+     * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值
+     */
+    @Override
+    public long weight() {
+        return currentBlog.getHits();   //change to hits
+    }
+}
+
    +
  1. ajax 建立索引
  2. +
+
/**
+ * ajax建立索引
+ */
+@Override
+public void ajaxbuild() {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        //创建Blog测试数据
+        List<Blog> blogs = blogMapper.getAllBlog();
+        suggester.build(new BlogIterator(blogs.iterator()));
+    } catch (IOException e) {
+        System.err.println("Error!");
+    }
+}
+
    +
  1. 查找
    +因为有些文章的标题是一样的,先对list排序,将标题短的放前面,长的放后面,然后使用LinkHashSet来存储。
  2. +
+
@Override
+public Set<String> ajaxsearch(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        List<String> list = lookup(suggester, keyword);
+        list.sort(new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                if (o1.length() > o2.length()) {
+                    return 1;
+                } else {
+                    return -1;
+                }
+            }
+        });
+        Set<String> set = new LinkedHashSet<>();
+        for (String string : list) {
+            set.add(string);
+        }
+        ssubSet(set, 7);
+        return set;
+    } catch (IOException e) {
+        System.err.println("Error!");
+        return null;
+    }
+}
+
    +
  1. controller层
  2. +
+
@RequestMapping("ajaxsearch")
+public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    String keyword = request.getParameter("keyword");
+    if (StringUtils.isEmpty(keyword)) {
+        return;
+    }
+    Set<String> set = blogService.ajaxsearch(keyword);
+    Gson gson = new Gson();
+    response.getWriter().write(gson.toJson(set));//返回的数据使用json
+}
+
    +
  1. ajax来提交请求
    +autocomplete.js源代码与介绍:https://github.com/xdan/autocomplete
  2. +
+
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
+<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
+<script type="text/javascript">
+    /******************** remote start **********************/
+    $('#remote_input').autocomplete({
+        source: [
+            {
+                url: "ajaxsearch.html?keyword=%QUERY%",
+                type: 'remote'
+            }
+        ]
+    });
+    /********************* remote end ********************/
+</script>
+
    +
  1. 效果:
    +
  2. +
+

欢迎访问我的个人网站

+

参考:
+https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/

+

http://iamyida.iteye.com/blog/2205114

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 3.定时任务 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html + + 2024-05-11T18:24:35.000Z + 先看一下Quartz的架构图:

+
+

一.特点:

+
    +
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. +
  3. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  4. +
  5. 分布式和集群能力。
  6. +
]]>
+ 先看一下Quartz的架构图:

+
+

一.特点:

+
    +
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. +
  3. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  4. +
  5. 分布式和集群能力。
  6. +
+

二.主要组成部分

+
    +
  1. JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种信息。
  2. +
  3. JobDetail:QUartz的执行任务的类,通过newInstance的反射机制实例化Job。
  4. +
  5. Trigger: Job的时间触发规则。主要有SimpleTriggerImpl和CronTriggerImpl两个实现类。
  6. +
  7. Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。
  8. +
  9. Scheduler:由上图可以看出,Scheduler是Quartz独立运行的容器。其中,Trigger和JobDetail可以注册到Scheduler中。
  10. +
  11. ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
  12. +
+

三、Quartz设计

+
    +
  1. properties file
    +官网中表明:quartz中使用了quartz.properties来对quartz进行配置,并保留在其jar包中,如果没有定义则默认使用改文件。
  2. +
  3. +
+

四、使用

+
    +
  1. hello world!代码在这
  2. +
  3. 本网站中使用quartz来对数据库进行备份,与Spring结合
    +(1)导入spring的拓展包,其协助spring集成第三方库:邮件服务、定时任务、缓存等。。。
  4. +
+
<dependency>
+    <groupId>org.springframework</groupId>
+    <artifactId>spring-context-support</artifactId>
+    <version>4.2.6.RELEASE</version>
+</dependency>
+

(2)导入quartz包

+
<dependency>
+    <groupId>org.quartz-scheduler</groupId>
+    <artifactId>quartz</artifactId>
+    <version>2.3.0</version>
+</dependency>
+

(3)mysql远程备份
+使用命令行工具仅仅需要一行:

+
mysqldump -u [username] -p[password] -h [hostip] database > file
+

但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed

+
<dependency>
+    <groupId>ch.ethz.ganymed</groupId>
+    <artifactId>ganymed-ssh2</artifactId>
+    <version>262</version>
+</dependency>
+

完整代码如下:

+
@Component("mysqlService")//在spring中注册一个mysqlService的Bean
+public class MysqlUtil {
+    ...
+    StringBuffer sb = new StringBuffer();
+    sb.append("mysqldump -u " + username + " -p" + password + " -h " + host + " " +
+            database + " >" + file);
+    String sql = sb.toString();
+    Connection connection = new Connection(s_host);
+    connection.connect();
+    boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证
+    if (!isAuth) {
+        logger.error("server login error");
+    }
+    Session session = connection.openSession();
+    session.execCommand(sql);//执行linux语句
+    InputStream stdout = new StreamGobbler(session.getStdout());
+    BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
+    ...
+}
+

(4)spring中配置quartz

+
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
+    <property name="targetObject" ref="mysqlService"/>
+    <property name="targetMethod" value="exportDataBase"/>
+</bean>
+<!--定义触发时间  -->
+<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
+    <property name="jobDetail" ref="jobDetail"/>
+    <!-- cron表达式,每周五2点59分运行-->
+    <property name="cronExpression" value="0 59 2 ? * FRI"/>
+</bean>
+<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
+<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
+    <property name="triggers">
+        <list>
+            <ref bean="myTrigger"/>
+        </list>
+    </property>
+</bean>
+

(5)java完整文件在这

+

Spring的高级特性之定时任务

+

java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。

+

(1)spring配置文件中配置:

+
<task:annotation-driven/>
+

(2)最简单的例子,在所需要的函数上添加定时任务即可运行

+
    @Scheduled(fixedRate = 5000)
+    public void reportCurrentTime() {
+        System.out.println("每隔5秒运行一次" + sdf.format(new Date()));
+    }
+

(3)运行的时候会报错:

+
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)
+	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)
+

参考:
+1.http://blog.csdn.net/oarsman/article/details/52801877
+2.http://stackoverflow.com/questions/31199888/spring-task-scheduler-no-qualifying-bean-of-type-org-springframework-scheduli
+Spring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:

+

1.尝试从配置中找到一个TaskScheduler Bean

+

2.寻找ScheduledExecutorService Bean

+

3.使用默认的scheduler
+修改log4j.properties即可:
+log4j.logger.org.springframework.scheduling=INFO
+其实这个功能不影响定时器的功能。
+(4)结果:

+
每隔5秒运行一次14:44:34
+每隔5秒运行一次14:44:39
+每隔5秒运行一次14:44:44
+
]]>
+ 2020-01-21T02:38:05.000Z +
+ + 4.日志系统.md + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html + + 2024-05-11T18:24:35.000Z + 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

+

建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:

]]>
+ 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

+

建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:

+
+

企业级的网站日志不能公开,但是我的是个人网站,用来跟大家一起学习的,所以,需要将百度的统计页面展示出来,但是,百度并不提供日志的图像,只提供API给开发者调用,而且还限制访问次数,一天不能超过2000次,这个对于实时统计来说,确实不够,所以只能展示前几天的访问统计。这里的日志系统分为三个步骤:1.API获取数据;2.存储数据;3.展示数据。页面效果如下,也可以点开我的网站的日志系统:

+
+
+
+

百度统计提供了Tongji API的Java和Python版本,这两个版本及其复杂,可用性极低,所以,本人用Python写了个及其简单的通用版本,整体只有28行,代码在这,https://github.com/Zephery/baidutongji。下面是具体过程

+

1.网站代码安装

+

先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。

+
<script>
+var _hmt = _hmt || [];
+(function() {
+  var hm = document.createElement("script");
+  hm.src = "https://hm.baidu.com/hm.js?code";
+  var s = document.getElementsByTagName("script")[0];
+  s.parentNode.insertBefore(hm, s);
+})();
+</script>
+

同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。

+
+

至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。

+

2.根据API获取数据

+

官网的API详细的记录了接口的参数以及解释,
+链接:https://api.baidu.com/json/tongji/v1/ReportService/getData,详细的官方报告请访问官网TongjiApi
+所需参数(必须):

+

| 参数名称 | 参数类型 |描述 |
+|

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 5.小集群部署.md + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html + + 2024-05-11T18:24:35.000Z + 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
+洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

]]>
+ 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
+洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

+

nginx负载均衡

+

一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。
+通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自张开涛的《亿级流量网站架构核心技术》

+
+

本站并没有那么多的服务器,目前只有两台,搭建不了那么大型的架构,就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构:

+
+

其中服务器A(119.23.46.71)为深圳节点,服务器B(47.95.10.139)为北京节点,搭建Nginx之后流量是这么走的:user->A->B-A->user或者user->A->user,第一条中A将请求转发给B,然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习,所以请不要考虑因为地域导致延时的问题。。。。下面是过程。

+

1.1 Nginx的安装

+

可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从https://pkgs.org/download/nginx可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。

+
+

启动后页面如下:

+
+

记一下常用命令

+
启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。
+[root@beijingali ~]# nginx          #启动nginx
+[root@beijingali ~]# nginx -s reload         #重启nginx
+[root@beijingali ~]# nginx -t           #校验nginx配置文件
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful
+

1.2 Nginx的配置

+

1.2.1 负载均衡算法

+

Nginx常用的算法有:
+(1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。
+(2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。
+(3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。
+使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是tomcat session的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。

+
http{
+    ...
+    upstream backend {
+        hash $uri;
+        # 北京节点
+        server 47.95.10.139:8080;
+        # 深圳节点
+        server 119.23.46.71:8080;
+    }
+
+    server {
+        ...
+        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            ...
+        }
+    ...
+

1.2.2 日志格式

+

之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧

+
    ...
+    log_format main '{"@timestamp":"$time_iso8601",'
+                    '"host":"$server_addr",'
+                    '"clientip":"$remote_addr",'
+                    '"size":$body_bytes_sent,'
+                    '"responsetime":$request_time,'
+                    '"upstreamtime":"$upstream_response_time",'
+                    '"upstreamhost":"$upstream_addr",'
+                    '"http_host":"$host",'
+                    '"url":"$uri",'
+                    '"xff":"$http_x_forwarded_for",'
+                    '"referer":"$http_referer",'
+                    '"agent":"$http_user_agent",'
+                    '"status":"$status"}';
+    access_log  logs/access.log  main;
+    ...
+

1.2.3 HTTP反向代理

+

配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考nginx 配置。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:

+
        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Host $host;
+            proxy_set_header REMOTE-HOST $remote_addr;
+        }
+

(1)proxy_set_header X-real-ip $remote_addr;
+其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取:
+request.getAttribute("X-real-ip")

+

(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

+

X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
+$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。

+

1.2.4 HTTPS

+

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:

+
+

首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:

+
        ssl_certificate 1_www.wenzhihuai.com_bundle.crt;    #主要文件路径
+        ssl_certificate_key 2_www.wenzhihuai.com.key;
+        ssl_session_timeout 5m;         # 超时时间
+        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
+        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
+        ssl_prefer_server_ciphers on;
+

至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可

+

1.2.5 失败重试

+

配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试

+
upstream backend_server {
+    server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
+    server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
+}
+

session共享

+

分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自tomcat 集群中 session 共
+1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。)
+2.使用 tomcat sessionmanager 方法存储。(直接配置即可)
+3.使用 terracotta 服务器共享。(不知道,不了解)
+4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)

+

本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考Spring-Session官网。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。

+

首先,添加相关依赖

+
        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-data-redis</artifactId>
+            <version>1.3.1.RELEASE</version>
+            <type>pom</type>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>${jedis.version}</version>
+        </dependency>
+

新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:

+
    <!-- redis -->
+    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
+    </bean>
+
+    <bean id="jedisConnectionFactory"
+          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+        <property name="hostName" value="${host}" />
+        <property name="port" value="${port}" />
+        <property name="password" value="${password}" />
+        <property name="timeout" value="${timeout}" />
+        <property name="poolConfig" ref="jedisPoolConfig" />
+        <property name="usePool" value="true" />
+    </bean>
+
+    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+        <property name="connectionFactory" ref="jedisConnectionFactory" />
+    </bean>
+
+    <!-- 将session放入redis -->
+    <bean id="redisHttpSessionConfiguration"
+          class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
+        <property name="maxInactiveIntervalInSeconds" value="1800" />
+    </bean>
+

然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截

+
<filter>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+</filter>
+<filter-mapping>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+    <dispatcher>REQUEST</dispatcher>
+    <dispatcher>ERROR</dispatcher>
+</filter-mapping>
+

配置完成,使用RedisDesktopManager查看结果:

+
+

测试:

+

访问http://www.wenzhihuai.com
+tail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录

+
+

访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。

+
+

点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。

+
+

值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。
+欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star
+个人网站网址:http://www.wenzhihuai.com
+个人网站代码地址:https://github.com/Zephery/newblog

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 6.数据库备份 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html + + 2024-05-11T18:24:35.000Z + 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

+
+

由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:

]]>
+ 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

+
+

由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:

+
+

果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:

+
+

大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:

+
+

一、MySql的复制

+

数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
+(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
+(2)slave将master的binary log events拷贝到它的中继日志(relay log);
+(3)slave重做中继日志中的事件,将改变反映它自己的数据。

+
+

该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。
+下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。
+SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。
+此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。
+MySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。

+

二、配置过程

+

2.1 创建所用的复制账号

+

由于是个自己的小网站,就不做过多的操作了,直接使用root账号

+

2.2 配置master

+

接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。

+
[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# master
+log-bin=mysql-bin
+# 设为基于行的复制
+binlog-format=ROW
+# 设置server的唯一id
+server-id=2
+# 忽略的数据库,不使用备份
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 要进行备份的数据库
+binlog-do-db=myblog
+

重启Mysql之后,查看主库状态,show master status。

+
+

其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。

+

2.3 配置slave

+

从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。

+

+[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# slave
+log-bin=mysql-bin
+# 服务器唯一id
+server-id=3
+# 不备份的数据库
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 需要备份的数据库
+replicate-do-db=myblog
+# 其他相关信息
+slave-skip-errors=all
+slave-net-timeout=60
+# 开启中继日志
+relay_log         = mysql-relay-bin
+# 
+log_slave_updates = 1
+# 防止改变数据
+read_only         = 1
+

重启slave,同时启动复制,还需要调整一下命令。

+
mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;
+

可以看见slave已经开始进行同步了。我们使用show slave status\G来查看slave的状态。

+
+

其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。

+

某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:

+
+

原理相对比较简单:

+

1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
+2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
+3.canal解析binary log对象(原始为byte流)

+

其中,配置过程如下:https://github.com/alibaba/canal,可以搭配Zookeeper使用。在ZKUI中能够查看到节点:

+
+

一般情况下,还要配合阿里的另一个开源产品使用otter,相关文档还是找找GitHub吧,个人搭建完了之后,用起来还是不如直接使用mysql的主主复制,而且异地机房同步这种大企业才有的业务。

+

公司又要996了,实在是忙不过来,感觉自己写的还是急躁了点,困==

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 7.那些牛逼的插件 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html + + 2024-05-11T18:24:35.000Z + 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
+建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
+本站主要用到的插件有:
+1.wowslider
+2.畅言
+3.Editor.md
+4.highchart、echart
+5.百度分享
+6.waterfall.js
+7.心知天气
+8.标签云

]]>
+ 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
+建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
+本站主要用到的插件有:
+1.wowslider
+2.畅言
+3.Editor.md
+4.highchart、echart
+5.百度分享
+6.waterfall.js
+7.心知天气
+8.标签云

+

wowslider

+

可能是我这网站中最炫的东西了,图片能够自动像幻灯片一样自动滚动,让网站的首页一看起来就高大上,简直就是建站必备的东西,而且安装也及其简单,有兴趣可以点击官网看看。GitHub里也开放了源代码。安装过程:自己选择“幻灯片”切换效果,保存为html就行了,WORDPREESS中好像有集成这个插件的,做的还更好。感兴趣可以点击我的博客首页看一看。

+
+

不过还有个值得注意的问题,就是wowslider里面带有一个googleapis的服务,即https://fonts.googleapis.com/css?family=Arimo&subset=latin,cyrillic,latin-ext,由于一般用户不能访问谷歌,会导致网页加载速度及其缓慢,所以,去掉为妙

+

畅言

+

作为社交评论的工具,虽然说表示还是想念以前的多说,但是畅言现在做得还是好了,有评论审核,评论导出导入等功能,如果浏览量大的话,还能提供广告服务,让站长也能拿到一丢丢的广告费。本博客中使用了畅言的基本评论、获取某篇文章评论数的功能。可以到我这里留言

+
+

Editor.md

+

一款能将markdown解析为html的插件,国人的作品,博客的文章编辑器一开始想使用的是markdown,想法是:写文章、保存数据库都是markdown格式的,保存在数据库中,读取时有需要解析markdown,这个过程是有点耗时的,但是相比把html式的网页保存在数据库中友好点吧,因为如果一篇文章比较长的话,转成html的格式,光是大小估计也得超过几十kb?所以,还是本人选择的是一切都用源markdown。
+editor.md,是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。页面看起来还是美观的,相比WORDPRESS的那些牛逼插件还差那么一点点,不过从普通人的眼光来看,应该是可以的了。此处,我用它作为解析网页的利器,还有就是后台编辑也是用的这个。

+
+

代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。

+
+

图表

+

目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)

+
+

不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。

+
+

百度分享

+

作为一个以博客为主的网站,免不了使用一些社会化分享的工具,目前主要是jiathis和百度分享,这两者的ui都是相似的(丑爆了)。凭我个人的感觉,jiathis加载实在是太过于缓慢,这点是无法让人忍受的,只好投靠百度。百度分享类似于jiathis,安装也很简单,具体见官网http://share.baidu.com/code/advance#tools。一直点点点,配置完之后,就是下图这种,丑爆了是不是?

+
+

好在对它的美观改变不是很难,此处参考了别人的UI设计,原作者我忘记怎么联系他了。其原理主要是使用图片来替换掉原本的东西。完整的源码可以点击此处

+
#share a {
+    width: 34px;
+    height: 34px;
+    padding: 0;
+    margin: 6px;
+    border-radius: 25px;
+    transition: all .4s;
+}
+/*主要的是替换掉backgound的背景图片*/
+#share a.bds_qzone {
+    background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;
+    background-size: 34px 34px;
+}
+

改完之后的效果。

+
+

瀑布流

+

有段时间,瀑布流特别流行?还有段时间,瀑布流开始遭到各种抵制。。。看看知乎的人怎么说,大部分人不喜欢的原因是让人觉得视觉疲劳,不过瀑布流最大的好处还是有的:提高发现好图的效率以及图片列表页极强的视觉感染力。没错,我还是想把自己的网站弄得铉一些==,所以采用了瀑布流(不过效果不是很好,某些浏览器甚至加载出错),这个大bug有时间再改,毕竟花了很多时间做的这个,效果确实不咋地。目前主要的瀑布流有waterfall.jsmasory.js。这一块目前还不是很完善,希望能得到各位大佬的指点。

+
+

天气插件

+

此类咨询服务还是网上还是挺多的,这一块不知道是不是所谓的“画蛇添足”这部分,主要是我觉得网站右边这部分老是少了点什么,所以加上了天气插件。目前常用的天气插件有中国天气网心知天气等。安装方式见各自的官网,这里不再阐述,我使用的是心知天气。注意:心知天气限制流量的,一个小时内只能使用400次,如果超出了会失效,当然也可以付费使用。

+
+

标签云

+

标签云,弄得好的话应该说是一个网站的点缀。现在好像比较流行3D的标签云?像下面这种。

+
+

从个人的网站风格来看,比较适应PHP形式的,有点颜色而又不绚丽的即可,之前用的跟分类的一样的样式,即双纵列的样式,美观度还行,虽然老是感觉有点怪怪的,如果某个标签的字数过长怎么办,岂不是要顶出div了。所以还是选择换另一种风格,最终偶然一次找到了下面这种,能够自适应宽度,颜色虽然鲜艳了点(以后有空再调一下吧),源码见style.css。 下图为目前的标签页。

+
+

总结

+

作为一个后端人员,调css和js真是痛苦==,好在坚持下来了,虽然还是很多不足,以后有时间慢慢改。说了那么多,感觉自己还是菜的抠脚。
+题外话,搭建一个博客,对于一个新近程序员来说真的是锻炼自己的一个好机会,能够认识到从前端、java后台、linux、jvm等等知识,只是真的有点耗时间(还不如把时间耗在Spring源码),如果不采用别人的框架的话,付出的代价还是蛮大的(所以不要鄙视我啦)。没有什么能够一举两得,看自己的取舍吧。加油💪(ง •_•)ง

+

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

+]]>
+ 2020-01-21T02:38:05.000Z +
+ + 一次jvm调优过程 + http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html + + 2024-02-16T09:22:35.000Z + 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
+该程序task主要分为三个模块:
+console进行一些cron的配置(表达式、任务名称、任务组等);
+schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
+client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
+整体架构跟github上开源的xxl-job类似,也可以参考一下。

]]>
+ 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
+该程序task主要分为三个模块:
+console进行一些cron的配置(表达式、任务名称、任务组等);
+schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
+client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
+整体架构跟github上开源的xxl-job类似,也可以参考一下。

+

1. 启用jmx和远程debug模式

+

容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:

+
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n "
+JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
+

其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。

+
+
+

堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。

+

2. memory analyzer、jprofiler进行堆内存分析

+

先从容器中dump出堆内存

+
jmap -dump:live,format=b,file=heap.hprof 58
+
+

由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。

+

3. netty的方面的考虑

+

另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。
+首先是查看堆内存的中占比:

+
+

可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:

+
+

达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。

+
+

声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。

+
+

最终解决办法:
+在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully

+
+

提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了

+
+

还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。

+
+

4. 直接内存排查

+

第一个想到的就是netty的直接内存,关掉,命令如下:

+
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced
+
+

查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题

+

5. 推荐的直接内存排查方法

+

5.1 pmap

+

一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。

+
pmap -d 58  | sort -n -k2
+pmap -x 58  | sort -n -k3
+grep rw-p /proc/$1/maps | sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done
+

这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?

+

5.2 [gperftools](https://github.com/gperftools/gperftools)

+

起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。
+根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,

+
pprof --text /usr/bin/java java_58.0001.heap
+
+

看着工具高大上的,似乎能找出linux的调用栈,

+

6. 意外的结果

+

毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。
+修复后的结果如下图所示:

+
+
+

总结

+

定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。

+

参考:
+1.记一次线上内存泄漏问题的排查过程
+2.Java堆外内存增长问题排查Case
+3.Troubleshooting Native Memory Leaks in Java Applications

+]]>
+ 2020-01-21T02:28:56.000Z +
+ + Spark + http://www.wenzhihuai.com/bigdata/ + + 2024-02-06T07:53:39.000Z + + + 2019-08-25T08:27:20.000Z + + + Java + http://www.wenzhihuai.com/java/ + + 2024-05-18T19:54:10.000Z + + + 2019-08-25T08:27:20.000Z + + + 目录 + http://www.wenzhihuai.com/kubernetes/ + + 2024-02-16T09:22:35.000Z + 包含CICD、Kubernetes

+]]>
+ 包含CICD、Kubernetes

+]]>
+ 2019-08-25T08:27:20.000Z +
+ + 8.基于贝叶斯的情感分析 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html + + 2024-05-11T18:24:35.000Z + + + 2019-08-25T08:27:20.000Z + + + 9.网站性能优化 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html + + 2024-05-11T18:24:35.000Z + + + 2019-08-25T08:27:20.000Z + + + DevOps平台.md + http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html + + 2024-02-06T07:53:39.000Z + DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

+

公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。

+

一、自由风格的软件项目

+

主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。

]]>
+ DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

+

公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。

+

一、自由风格的软件项目

+

主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。

+

完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。

+
<project>
+    <actions/>
+    <description>xxx</description>
+    <properties>
+        <hudson.model.ParametersDefinitionProperty>
+            <parameterDefinitions>
+                <hudson.model.TextParameterDefinition>
+                    <name>buildParam</name>
+                    <defaultValue>v1</defaultValue>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>codeBranch</name>
+                    <defaultValue>master</defaultValue>
+                </hudson.model.TextParameterDefinition>
+            </parameterDefinitions>
+        </hudson.model.ParametersDefinitionProperty>
+    </properties>
+    <scm class="hudson.plugins.git.GitSCM">
+        <configVersion>2</configVersion>
+        <userRemoteConfigs>
+            <hudson.plugins.git.UserRemoteConfig>
+                <url>http://xxxxx.git</url>
+                <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>
+            </hudson.plugins.git.UserRemoteConfig>
+        </userRemoteConfigs>
+        <branches>
+            <hudson.plugins.git.BranchSpec>
+                <name>${codeBranch}</name>
+            </hudson.plugins.git.BranchSpec>
+        </branches>
+        <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+        <extensions/>
+    </scm>
+    <builders>
+        <hudson.tasks.Shell>
+            <command>ls</command>
+        </hudson.tasks.Shell>
+        <hudson.tasks.Maven>
+            <targets>clean package install -Dmaven.test.skip=true</targets>
+            <mavenName>mvn3.5.4</mavenName>
+        </hudson.tasks.Maven>
+        <com.cloudbees.dockerpublish.DockerBuilder>
+            <server>
+                <uri>unix:///var/run/docker.sock</uri>
+            </server>
+            <registry>
+                <url>http://xxxx</url>
+            </registry>
+            <repoName>xxx/xx</repoName>
+            <forcePull>true</forcePull>
+            <dockerfilePath>Dockerfile</dockerfilePath>
+            <repoTag>${buildParam}</repoTag>
+            <skipTagLatest>true</skipTagLatest>
+        </com.cloudbees.dockerpublish.DockerBuilder>
+    </builders>
+    <publishers>
+        <com.xxxx.notifications.Notifier/>
+    </publishers>
+</project>
+

二、优化之后的CICD

+

上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。

+
+

目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。

+

没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。

+

三、调研期

+

由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。

+

分布式构建jenkins x 首先要解决的是多个构建同时运行的问题,很久之前就调研过jenkins x,它必须要使用在kubernetes上,由于当时官方文档不全,而且我们的DevOps项目处于初始期,所有没有使用。jenkins的master slave结构就不多说了。jenkins x应该说是个全家桶,包含了helm仓库、nexus仓库、docker registry等,代码是jenkins-x-image

+
+

serverless jenkins 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。

+

阿里云云效 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。

+

微软Pipeline 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。

+
+

谷歌tekton kubernetes的官方cicd,目前已用于kubernetes的release发版过程,目前也仅仅是与GitHub相结合,gitlab无法使用,全过程可使用yaml文件来创建,跑起来就是类似kubernetes的job一样,用完即销毁,可惜目前比较新,依旧处于alpha版本,无法用于生产。有兴趣可以参考下:Knative 初体验:CICD 极速入门

+

四、产品化后的DevOps平台

+

在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:

+

4.1 Java代码扫描

+

PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。

+
+
stage('Clone') {
+    steps{
+        git branch: 'master', credentialsId: 'xxxx', url: "xxx"
+    }
+}
+stage('check') {
+    steps{
+        container('maven') {
+            echo "mvn pmd:pmd"
+        }
+    }
+}
+

4.2 Java单元测试

+

Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用"mvn test"命令来代替。

+
+

+stage('Clone') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+    }
+}
+stage('test') {
+    steps{
+        container('maven') {
+            sh "mvn test"
+        }
+    }
+}
+

4.3 Java构建并上传镜像

+

镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。

+
+
+

为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。

+
+
+

+stage('下载代码') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+        script {
+            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+        }
+    }
+}
+stage('打包并构建镜像') {
+    steps{
+        container('maven') {
+            echo "3.Build Docker Image Stage"
+            sh "mvn clean install -Dmaven.test.skip=true"
+            sh "docker build -f xxx/Dockerfile -t xxxxxx:${build_tag} ."
+            sh "docker push xxxxxx:${build_tag}"
+        }
+    }
+}
+

4.4 部署到阿里云k8s

+

CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。

+
def tmp = sh (
+    returnStdout: true,
+    script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+)
+//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+if(tmp.equals('')){
+    sh "helm init --client-only"
+    sh """helm repo add mychartmuseum http://xxxxxx \
+                       --username myuser \
+                       --password=mypass"""
+    sh """helm install --set name=${JOB_NAME} \
+                       --set namespace=${namespace} \
+                       --set deployment.image=${image} \
+                       --set deployment.imagePullSecrets=${harborProject} \
+                       --name ${namespace}-${JOB_NAME} \
+                       mychartmuseum/soa-template"""
+}else{
+    println "已经存在,替换镜像"
+    //epaas中一个pod的容器名称需要带上"-0"来区分
+    sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+}
+

4.5 整体流程

+

代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署

+
+

pipeline:

+
pipeline {
+    agent {
+        label "jenkins-maven"
+    }
+    stages{
+        stage('代码扫描,单元测试,镜像构建'){
+            parallel {
+                stage('并行任务一') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java代码扫描') {
+                        stage('Clone') {
+                            steps{
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('check') {
+                            steps{
+                                container('maven') {
+                                    echo "$BUILD_NUMBER"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务二') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java单元测试') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('test') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn -v"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务三') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('java构建镜像') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                                script {
+                                    build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+                                }
+                            }
+                        }
+                        stage('Build') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn clean install -Dmaven.test.skip=true"
+                                    sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:${build_tag} ."
+                                    sh "docker push hub.gcloud.lab/rongqiyun/epaas:${build_tag}"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        stage('部署'){
+            stages('部署到容器云') {
+                stage('check') {
+                    steps{
+                        container('maven') {
+                            script{
+                                if (deploy_app == "true"){
+                                    def tmp = sh (
+                                        returnStdout: true,
+                                        script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+                                    )
+                                    //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+                                    if(tmp.equals('')){
+                                        sh "helm init --client-only"
+                                        sh """helm repo add mychartmuseum http://xxxxxx \
+                                                           --username myuser \
+                                                           --password=mypass"""
+                                        sh """helm install --set name=${JOB_NAME} \
+                                                           --set namespace=${namespace} \
+                                                           --set deployment.image=${image} \
+                                                           --set deployment.imagePullSecrets=${harborProject} \
+                                                           --name ${namespace}-${JOB_NAME} \
+                                                           mychartmuseum/soa-template"""
+                                    }else{
+                                        println "已经存在,替换镜像"
+                                        //epaas中一个pod的容器名称需要带上"-0"来区分
+                                        sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+                                    }
+                                }else{
+                                    println "用户选择不部署代码"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+

在jenkins x中查看:

+
+

4.4 日志

+

jenkins blue ocean步骤日志:

+
+

云效中的日志:

+
+

4.5 定时触发

+
+
    triggers {
+        cron('H H * * *') //每天
+    }
+

五、其他

+

5.1 Gitlab触发

+

pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。

+

六、总结

+

功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。

+

DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。

+

最后,DevOps是云原生的必经之路!!!

+

文章同步:
+博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
+个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
+gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai

+]]>
+ 2019-08-22T01:35:46.000Z +
+
\ No newline at end of file diff --git a/atom.xsl b/atom.xsl new file mode 100644 index 00000000..26404c71 --- /dev/null +++ b/atom.xsl @@ -0,0 +1,534 @@ + + + + + + + Atom Feed + + + + + + +
+ + + +

+ + atom logo + + +

+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Last update time: + +
Author: + + , + + +
Contributor: + + , + + + +
Categories: + + , + + +
Copyright: + +
+
+ +
+ + +
+
+
+ +
+
+ + + + + + , + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+
+ + + +
+
diff --git a/bdunion.txt b/bdunion.txt new file mode 100644 index 00000000..0b3f2f5a --- /dev/null +++ b/bdunion.txt @@ -0,0 +1 @@ +9ec5b4d13db5711621c27c491c52a3a8 \ No newline at end of file diff --git a/bigdata/index.html b/bigdata/index.html new file mode 100644 index 00000000..7fbc1916 --- /dev/null +++ b/bigdata/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Spark | 个人博客 + + + + + + + + + diff --git a/bigdata/spark/elastic-spark.html b/bigdata/spark/elastic-spark.html new file mode 100644 index 00000000..b1c0bbc7 --- /dev/null +++ b/bigdata/spark/elastic-spark.html @@ -0,0 +1,73 @@ + + + + + + + + + + elastic spark | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/bigdata/spark/index.html b/bigdata/spark/index.html new file mode 100644 index 00000000..d3faf401 --- /dev/null +++ b/bigdata/spark/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Spark | 个人博客 + + + + + + + + + diff --git a/category/index.html b/category/index.html new file mode 100644 index 00000000..a083e26b --- /dev/null +++ b/category/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 分类 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/cloudnative/index.html b/cloudnative/index.html new file mode 100644 index 00000000..3cfa1729 --- /dev/null +++ b/cloudnative/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/database/elasticsearch/elasticsearch\346\272\220\347\240\201debug.html" "b/database/elasticsearch/elasticsearch\346\272\220\347\240\201debug.html" new file mode 100644 index 00000000..06e04d23 --- /dev/null +++ "b/database/elasticsearch/elasticsearch\346\272\220\347\240\201debug.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 【elasticsearch】源码debug | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/database/elasticsearch/index.html b/database/elasticsearch/index.html new file mode 100644 index 00000000..90ed7d7f --- /dev/null +++ b/database/elasticsearch/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Elasticsearch | 个人博客 + + + + + + + + + diff --git "a/database/elasticsearch/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html" "b/database/elasticsearch/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html" new file mode 100644 index 00000000..c7be3f33 --- /dev/null +++ "b/database/elasticsearch/\343\200\220elasticsearch\343\200\221\346\220\234\347\264\242\350\277\207\347\250\213\350\257\246\350\247\243.html" @@ -0,0 +1,321 @@ + + + + + + + + + + 【elasticsearch】搜索过程详解 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/database/elasticsearch/\345\237\272\347\241\200.html" "b/database/elasticsearch/\345\237\272\347\241\200.html" new file mode 100644 index 00000000..08362086 --- /dev/null +++ "b/database/elasticsearch/\345\237\272\347\241\200.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 基础 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/database/index.html b/database/index.html new file mode 100644 index 00000000..5c627f7b --- /dev/null +++ b/database/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Database | 个人博客 + + + + + + + + + diff --git a/database/mysql/1mysql.html b/database/mysql/1mysql.html new file mode 100644 index 00000000..c43d158c --- /dev/null +++ b/database/mysql/1mysql.html @@ -0,0 +1,47 @@ + + + + + + + + + + Mysql | 个人博客 + + + + + + + + + diff --git a/database/mysql/index.html b/database/mysql/index.html new file mode 100644 index 00000000..97e0a8a3 --- /dev/null +++ b/database/mysql/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Mysql | 个人博客 + + + + + + + + + diff --git "a/database/mysql/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html" "b/database/mysql/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html" new file mode 100644 index 00000000..fcf1b869 --- /dev/null +++ "b/database/mysql/\346\225\260\346\215\256\345\272\223\347\274\223\345\255\230.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 数据库缓存 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/database/mysql/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html" "b/database/mysql/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html" new file mode 100644 index 00000000..29ff68b4 --- /dev/null +++ "b/database/mysql/\350\241\214\351\224\201\357\274\214\350\241\250\351\224\201\357\274\214\346\204\217\345\220\221\351\224\201.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 行锁,表锁,意向锁 | 个人博客 + + + + + + + + + diff --git "a/database/redis/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html" "b/database/redis/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html" new file mode 100644 index 00000000..b392cae2 --- /dev/null +++ "b/database/redis/RedissonLock\345\210\206\345\270\203\345\274\217\351\224\201\346\272\220\347\240\201\345\210\206\346\236\220.html" @@ -0,0 +1,245 @@ + + + + + + + + + + RedissonLock分布式锁源码分析 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/database/redis/index.html b/database/redis/index.html new file mode 100644 index 00000000..a31bb38b --- /dev/null +++ b/database/redis/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Redis | 个人博客 + + + + + + + + + diff --git "a/database/redis/redis\347\274\223\345\255\230.html" "b/database/redis/redis\347\274\223\345\255\230.html" new file mode 100644 index 00000000..724cdcd5 --- /dev/null +++ "b/database/redis/redis\347\274\223\345\255\230.html" @@ -0,0 +1,133 @@ + + + + + + + + + + Redis缓存 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/database/redis/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html" "b/database/redis/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html" new file mode 100644 index 00000000..2da42859 --- /dev/null +++ "b/database/redis/\344\270\200\350\207\264\346\200\247hash\347\256\227\346\263\225\345\222\214\345\223\210\345\270\214\346\247\275.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 一致性hash算法和哈希槽 | 个人博客 + + + + + + + + + diff --git a/donate/index.html b/donate/index.html new file mode 100644 index 00000000..6f276670 --- /dev/null +++ b/donate/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 微信支付 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..193188751c798ad909f0b24a34a30c80f0eeaac6 GIT binary patch literal 21722 zcmV)5K*_&}P)Wo%EIF~%0yAizX} zZGvSm$)Jp$^rRQ=dpDi9^USxxKKsd@Kl)Oat9S1?`|SN&zm>lAt@R6TzVqMIsA|+z z1=K(o1PH?rRaK#^N+1dW{yPj%@df-UN(41f*EJ9X2<%(`2n5i-@%?qB@9A`UsOtcA zUD~VodtR|Z7zU^VzP>+%@GqJH&lVjs6w>YnlLH+s1CvZ=>z*>>W-xL zsK!DTP;Y^@G*(OCYsrmKR#P2g%0f-1;6=l3y6wHS2!$6j zf%yl!Ah`g(2)rS&(anSLe$)=TAk^oWK+Trtc_lYRfyw8I-B)uYk4Aqd-bdfZ`$?h% zaTxn^G&@oR7RAjyaB=wQ;(NnDOrltC^KD{|%2Ga`+NJ{6jVW3sJ6(k?g7^2Dn~+Um z0;gscd2YVb{ZS?Bqb4{MUnK(LKIG@{zIcyCg-`_c&%b2NTARidhgzJOn-C3qisJ#xWmLi9G(G|M83z#5Mteu-~ZU#c5YO{spS1X&hzET6sgq@sbP z1P=TuX3K;Z^I2Lil}$~JP-*L?6zQv3i?%jqFcb~TK}%#N z@#Z`Jjfuy0wrhr|xkh2;)8uz{1AcMajk06au!#ARU0u^Zgu1gud-bL~dZZvIFtO|I zhkR>@uw#u7{hBS)$HiMlqvn^mP_*k}g_e;e-yhhD$}V+Z-Iy`mB;r#%%C*hFjYomW z=A+q?`>znws_>ai7-~T{i$1MnZB8A@nC7 zh(e^F5n^5#seU%8#zNlq+ zA@?Kc*DG#$kL*u(n(o{@dg5hdkIIWI8ID7ZfXpQLwRIQj2Yck%>;wwk7C|#|jn2f( z@#k}0`Bh-fqC++d#QQm zemp4~n{6N_!7_HqKs7h=sE?XxI_0vJ#9O}WS_BmZnir|! z==E~?LfcuI@2D-%aGfwwp#p4#N}oXiw2&InV#`Y;HwpE#~FHNFElI}b$ zES0E_Z=`dqYQ%zsXvCH>EN`;!CL=S|cKpu6dgfk{E{v-+ca6x(HH=Guu6vj34&k^z6__p`d z@*)!yFUp6cQIzpgm02qA66}aML3!`^13yz3m}Oj z@s-pW?!2<%?L_DV9Yl46unLi50z{0U?6m^Eswou#5lIl=z_8s>6+TPLq)5u3psko9 z@x#kxRw7veV2NNbx%M=qmgkEI!u8C%n?;Gn69z|`EP2qw>lRgstS0E7f(~AJ%U!ji50xxl$X#gAiX!;7 zC>HiKJBL2TLer$vkq6}t6%@nqFbi=-U^I8c%xGTg3W`%yn&`$1AT;u!*^P-IRzMao zd=QNL0qAwSI_cXE?^T41GtiY6is zYI760BX6Tai~_k1yhpAD?@v%3UrVkBHO$N&Kc800b79|S62Q=Mu>@rsKlqW`?vc-0 z3aoXryg>O#WWacMmf})r;E<@xk^3+&;X^vee{(d#^%a}>`gH;|pEV3k+^)179 znjlyFlH>==9IPcf)xu{W|FZR4^~Prkj2gB?dn^@_N(tdE)nExiRKqOfq=wPSGF-L? zmQnadFn1Z)jax7{ilCdp=`rcLg_HV`2n$LU0iRI}a1HN`gBYb3c0Zkn9Wk$*G2P3rW@x7{T^OkhNbBnpgJ93#J6 zl63l~B1%O&EDWqx`EeNAa z3RJ4p%V0%nVknVS`4VHa#TCm#G{TV6CAe+jyi~gtcvf?(K?&DWC;^6e$Y+|b6-#9W zQO7Eq1iA&hYZ9&g*{zp+#w*_XYc(sW;%B(S1+es@As0){p=L>@Vaz8S^V67Xsr{{r zs+U6a0)$~0Y{ZD_7*)Y=o^XAPg3p;mvT$XVA!IEq09NWbI^`T;RUoPgB#K82w2dJkmd!Fq|@x7c67GVw`m1r%$ zQANcPm(Y|%QV9bS)fmqm{6E}Gc(~@M1Oo10mL(FVt|&`2VRaNq6ePD$7YMQxovOkh z3@|8mu^Q$W4Z0Y06Aa=Iy(B^sg~-bajjt!&?LJ`2YrU5|U~;W0t2HPD^f~Fq8aEqBkk2>UQ;nQm#op69txtxe|1ryO^3) zWf)Wo4Dtn5vKbC0F)rV?fr~Z<*yu$Vb|Q4Cefb<=k;&&oNrFWfqwMrC54+gOD?I$< zQ~1_Sfv1WXRS+TS4lpl(MFn&QLri81B;5{TrXe)1b*bee`oL0=4dIeVp^|xKjzowz zx)I&<%LX@MkNcrEn&J3kQweXCoZNgi0J1}0PQ9~WPl~06XDYs{K2l7g3rq`>?qW@7 z7OuAiHq92Y!!_D60N09?L`a?~FfMRWvrfe*Ew+eA04emM3Slurf4+-jodC}o_HgO2 zkBf!_bWqB7KLUDnfuzbLha)P_aA%GPdD2JGUqRU$;^fvYKKJw<9+=k12oZJ%$b%S* zia}3|f)OVzg9L*b*zj6WKse3eB5JS(VOf^xfBTmca|~y7#G?&Ec{JCU##Yqm(3AvW z63iG6MG|NYSe5~dT0JqPW=2JDG>MV`3z<_vU@vAW9OF2mLQ@o|WZuqikc^%7K9UGb zZseb77*T31=qbR-$S3D=qaS2QvRxdHD?InW2+v;W<3L?vgXWLslbnh&M;C>B#}as` zIb8%X%I*-8FhLr3F{?xT-5B_XofK!bc9Bz?`y-T5f;s&M0~0msN~5#vIj92ht-P?{ zfJ{#WlwI-^o5a<~I|7V*F6RYIGlvpT*fp9mj#rP(rwQ}QB1?e*Y4#ep%z<+Q9jF9e z4Y}!-yCkd^pwaB*vTUVhK-sbC0Qs6M)JRf|pzaE<;7SC?2+1_lYga*7W1i9^4V4iU z0Ns3oQBGl{xc8jD4Wq_-j^n{H5JCELYCNsbpE z8RGImfYoe*^_cLL*08$y97$LS%a(x-wN#6q)QDIXfe~KN>mWmoX_jFx7~+Y&0^i>p zu-B+O$}=E#x+@6Vry-OD&!Jw zQ(^ZS+sS$20h87UwUj{AG)l?@pdSS&(j0NpLr|6yM2ym1T(_@-mmD48Ksmv1v4{0e z2R$k4bC-<~B^7Fh@l_>EBt8Ja;e^l#nZ6bMRfR=aAS){@5MmzouvK?)Di3kGh_RFB znC7YEg7Y**TGm*^BP^m3=D|=<7$)x?R5)*vKbeF(FJ*~z7`M<9Y|BL*ESE^pI58gb zix7niV{9&N^fqlTNhGBbGi-vTn-Ys<0EKATP1LZa`N0@E7A1gwJLh#0dXBshjzv-x=oSm?%lGh-7@-^YBu&raJrv;_ z1=E4JgHEp}2uU$xt`>tCU7qHcOlKm{q}M~I7ZU))B`3W4N>Qb@EQMEeAUX25%q0;c5x;w()NZU+Au^@{oA_x&9G6*%kn zA)Cyxb9xJL5ulpqC{q>-L&Ql({6i9@2*L`JD#2b6q3o<6==G5@NQfeAXC-#37*A&% zJhHXGiMi`4#S?9iS2fD%U%Klg>S{`*O;p-& z%pE*UYZf=P>OVq(VK%|xau+{(-6c4box^%Q!+|6~kh3;4L>+8RorJl>0Bi zlXKuZC(q%jw8X44SORQLFfvQ?%+43rLJiBgcFKU_?`c9kF!3l`;A^h0YY!7{gZ3y4 zaU&LCihs0@A*F+`$3SVHfAb6+y!hogZn+t>(W32 z>P{G_z|uV&!vwE;{^dBFp2NywjCGpYk``@^;e{kSV-d=^vHloTjyezT-bLmXi;onyL4Q3Mg@ojzv0Rh$opc;M_9CwAx9jry3x z>;ZFntXGh=@aP>nN=MU;s?=`dAbP+i)e2*28Diq;fF|F3JO$GwP%|>Db35oWg+{^G zdip>?2_|Y}pBrzzyVkr&%ihoIqF5$}O4U-9kJ6VV{B(Ad*<5enlOkRV6Ru(=qA3QQ z4u-`7gLI6`yE*>V)yHsfaTcR|hBa27OD=jBac_jWo+0T)7_6@&PZFG;&2fHfjM*#| zU)G5_VlJdBMf^jEj0ta5Vm@0S4kGNFKaYnVdI;m`9M65>OAxK^$1Y;*#zQ=KW(TLH z8M46!CP8OeK(q!9EqNI1)_N9)m$&8e!LW zebb9)8dftJwl8hyGpvih6WFHRb7gOG`kN$#X1kpSA^Dq;M}6lhWaIe@Yg>#GQ(?Y# z^96?a43~Ct+0*YBqZi?UM;^na7hi(y^ILfM!H00}{1!??_~Dygg=lRbw(=T# zK?j?2vPL3Q{WVF#-Q4?b=1-wiTj0@H4U38;IB7Ahob%zt9r?R~4I*U2E!BV7MlgRS z@B%z_@+mz2_~ZD{pM4Z3PHp1i ztFA@3vVln*qVBICk2;u_wICuzpb2dQf0(cKK&$}|9N%mB{#FaK?h?l$&F!d0nK=}5 z8YvIa7)92Wv&{D-kTXpb?WO7Ti}ChuyyLw>5fJK@(VKneNa83(G-K@=L)D!spHO>2 z6ys_B%aZ9PDP-5Cm^+%%1W{g~lV_5LU4LYRYu7>%*q~fs&|z&ZMp;KlI}y%I$N0yG z9>!O`^KCqSevC5KI@;lbNAR;h{d&Cd=ih+obd3E6_DKSqr3ES`)`ZT8g5(wc?i=61 zzx^NIkE2&U2g8HMkVQRA3-%jFh&t?5hU9SnlEB<=DQLUof@q+<BH!o45`*4n&xH8@Lg}w$(7n1r}yFL zv%n~ZWPASW( zZzt2zqLo$&F)UJn2yFzE&$GmtT`$e0UUA#qQo!`GKBF9JijYLD>CQ_6%Dq#yZg;Cl zfz7Db%RXsilUnkeb_?Xfo#;p;*bgFfrrS8yDbXLF!l5`rH{C;>&++Cry%|?tdmYdp z;-T+9fPeq~|A>d5K7$!QzT3qecm4|A_=aD=AZC;pq0HtgFjlO$Ht~h~K90TF99KT? z23+yH7oc7{iZ6WQ0sPmGe;!+l097*5ssUGlrEe{%|BFF~yrdO|H>ii1Oj%g-amt%y znLw)^f{e5SFAKzcy%X+BuTBCk-KpMk+n%Ti%z zWFshcD*|*Dk_R0Q-!yjGZa5<$G+{}mBhXZs=NRNO42vQT*or z{C?~^Z~*yKOZ@kJ{xkT45B)JV$J@B!#V^3G-*XR+9X=xAE;$?=0U-_J^u>!5)9r1X zIr9|0`Q7i}c`teqE`R<@@i*Uk6uv{_H!pa7&7*3dV%h-VI;=`z6!pv3Dh(P?# zX>gX0L^9kBI*uWi1VkmFKWuD))zTDA_iRshU?~Vc2{5q=tjN$S=NJ?doPOwA*na9! zbdlncY_1i$D2fpbk;x1f&T@ z$0(`*p99GK^0s`XLIC6 zXlRb)x5!0&Z0YK;q*nqouWz;b+}yV{$d7R-8;xnD0-lij`ssVEDaAERn$kUlG+v_4 ztFYRYgO9Ygc0*d1{r~@x_cxUSYr12tQ_PE&NTRAlw_2cEOtAI%_c1+p5^*ub;f*!? z)^ENaS6p#9s!53_9zB7>#}DIM-~SelTznW;Tz&}x5>X}bWRglwVFE-QQjG#>tFDETz2K<*v>on!aqESKe_KKqG5A%QPqUBGDKLYu%sTg+;|>Nw424-8&TFm z?O_A&cxm4|cU*`P^%?l$X2)VzbDv=W7?pbMk882i~=J{ECGf$jkjSIPYj!8 zJmn>dZEi_zi2@iQ(-CaTJ@{NC&aj1u0%e&asTSzu6YM?xD9)dF81-xy2UZ8T{gzws z3vc*243S{_>?RHzK7d6&L$8-0Tg*_V3)ESTkiuf(+#4bp4wdp&R=EEk?#BoJpFhM; z{`61br8nJ#c{s$Me&TQOnXf&7Suhl$4ta=SSRxKGBUdWPaFEWPfaj5&zmEZXroG?r7lO0bmBpyk(?qR+&j+^tR^ElLqn6c zJj+Nn&i1`PVje9y%g8m%z+~EcVtpx_B82~4iT~)=3ye=cj?)kS6QacgYrO=|z2@2Y z_y6|a;fkY2P|m05_PeO-9O--}sQ>v>r!gdDO#+$#o#6_)tE)iLk&X22?|vJPJoXS? z^75Bq|IxEWV2l6&AOJ~3K~!TnGpX>qe{?S%czTLOFc5)}Z5#w-ALqzPlcTx!_FSi0 zvyz@LII(iQgz8oewhipyc*A}7T-5Sf_e)IztqNviCR7+AwB|FG6NDsHGFn0-Fz<2p zgsmZ>5bn3S4qq&&v%x4%gKO|D^J9&ze@5k2f)&0wjF1yL68|y8Z8LZQPrI~hBQp1KrB&(}ZrxSuq z6kt5v69G<&8asJ{|MhqO2%EDAsR*nqo2DW$sLmvBW0>geLQWlU;*C$UY1hM?jH;TT z3*Q!mf(~tdzoyWdf-M2_h7slcWfs6VpzbBCMqoGH{$9CVr&wosre=opGwTeLwRWze z(V*MHYUpnAVP3-5+w&d5K!Y`(B8sFsoYw_9VS%`qVS4rn>^$)pqS*v1L4Y5<`BixD zyWfMA0gz`?tgVeuWDD_KduKNhmz8kz1!0LIbVnnEqZO%2vvA5}`26;HtQ|gzvy%l* zY)S88=#`x(UAUk&&y*$Nb2M^=!cl~#G(etkd!a3p&=Uz|{ z1?oIQIh!M&%n))=OAw&b>7u*7CIU!N30BG7&iWS_Rl7xiFMRFW_~<9UD3pK<2?`{r zWL6Z(cUl?uUbtnIK}oVp|6%p$mbugdq&AexNG9jKu+L0LH8u~+XOlENWlPg4!G;q~ zZ5drtJva77b=epU>wQ-!%CRglc)yfq=n`TQnSMrys2}>w^xjfOoWI`;R`2tm+7;EMdH~uWBQP4u-G+Uf$Yk zRc57_EmIfuNg8S|SyNiPRa2*Ekt}*M?Z)SKSvktUU$;_T3)6l#?c$XF25Y6gV8XyF zZn;YYCS!!Gyzd<@Vn=BTjRNO8$b%8BA6bpar>iw%40K0&)D=|PNlDyJIP75nV)C7I zf+4HTVTG4ma}{3noa?aK?c<`O$I)43xnEmGGrm6#kuZ`a=E(&U%7OArp58oz@qC5@ z8~d@cx`8P|jO!TRe(WiH=%b&+x!n|Hod{|V5=U7BB+rEX7&M9O&yX34I;lz;1!d$C z!+(w7nxCz1OfT?C4rOyhq&1gqNix^zh?h%FG*z)BIA!7T##(tj8tufkQhiZ{{jjdEwL_Ffl*&%ioRKaSDK=ZL2} z=%r)4{5e*MHIgvG#-R;hkf4re;>gyr&*1~GRhN_7Xnx`dS&<8kE{eOT zqXdh%kDV&Qhwl3vzWnv?V48C0lX^hY6-k>egOq~LqthZTY?Cr`FTUGC@HR=*pG^n7 zEG>)!23{?0f(6g0-heLL0-rYKd4cf@ zWDcP@F@m=wm1+*+sHomrLl>~$y9puvl$`iD>PTt7m`*XO3momoc-}>a@RDn;z`;R+ z0R>O)M-XAKGQePM1gx!agba#E38~zX-6TOLPEgDj>Sscoj+2oTI$XnU8RDVSTlg=3 z`WJZO^m!oaAt3DGu(?p@*y<#4xCl%V=LW6OphLO!TB`TxuBmMq2<#iSi6SkJRX3rD zbvquAt`w|e@u+COcLn&x(IV-h~URSrzgE_jnd@=w)z89f zU-?QL>UWWEoyDL?5lr^5zndVcas&mFVdh2T>TBP%3>E1l!qtdZ2B?yb6l%C!cEm z)Eetxv!R;ZZYI+Qp*Zohtqro4R&Mg3%L51sjQ-YJn7q5QF@G&eg`-Fwc{bSQOK;P$ z4{B|Fe1bJja0j)T(6ss-U^bhf)9pyF755E8bO9J>?+Z3L)ab>r5N-zDE{-2Lf)~E{ zML2TkAg(xm6kO&wn0PF4*93QI?`Ru)u9 zslx@<8tAO_iPlsC8{F0I_}8s(Q*fUlFvSYB4Ov``%qLk+3-Taf>FXOswY*l)_vM~> zxoDUHk4EfwJ8y3_OwxoZqjYo%c99das4(btg>M*?1%|yYHbyJB=p2}&;NV1UO?pTft# z@CBTmOi}fF=&i0J9*mF{6}ETxusfbe<3zVV6f|R+k=H#y6m^kj+(_&I<$CF~P#HWS zj9X2B9ieawqca{Y*pRSW1A!*E@_e+#V{Bkruf{t=+6ShQ?&nV5B&{aYXJ|Rrs5R=w z+L_3Lo9^TYS@SctQ#8-gA(cK*MA~aCG0fR26bCwfsL18};QA^qyXZJ>c+RzW_7zv+ z;MyuydOc-H$tVaNgv=eDXA9(u8F^8{Q>^k-2s(`Tss&3PY@Ub^)d413TNtdbVK>k5 zxvzc|pZL<>AlcZ5-Lk}9ULmESPS{1J!?#HCMp2=PxX(2`y zg?sy#{FOn6?3mU}Kyju%)XE6|9=!0z3uNeZ6O?%>nsM3jqxgx}yc$1r{qv>0bx19t z@8XRxDYRL>6xxEApfm*pm`wJN=NUHER?$zoBDC4gSp43w+h->D@H7TD2u zuxU_f@ftZSh1Xtx6PeZgJQfXJ_4Z${HC$g#4$&uw#jK@<<`L}{SV!W{q4jw*=**p^{YB*fQ-gt^{fBQT5 z)EEC2k3M|{L2rn84NN%Ilu>K1BNRHBB*$G{5@3}9A*e!*yHJ5i+pqcRrW#B?O9VnX zRMo#zP%_iQ(;*KEtQ|?Lk_k$4R%>Ijpu$lx{!MI-=g2r-BI%O;C%ZUem0R72_=TDbU?w38F-KF7_W3rq*PT-Z6J(V%jxd?au1 z%3JOhU6a7JnL%rLg=O+OKhaL`VQ+Gs!@fZWVLC^Ur`X*(hcl;6 zfrZd(uX_&S!9au%5PFdj3OcZv>|VlN-Z7IGnFKmXhx@;hT(2w_mV+>AjL}KDN@uHp z>12w2zb_n)$A54FlgSiY+uL~J>^Xe*kssiZlV?#T1B}ZMLEMo^I3al)Qch9yn@Eke zxZCLvo+w*7DyLW&w%Syb98@q$%aU!e#2aO;Ot|XblrKVa$fiQw_&)5?BFJ`R(aOZ7 zoYl;4;cq$>D1yBD2Y&jIb)2 zDpchmqrelP4fd?P>p1l9ctFJ=R zXE|OugXFR6BM>X(xmdPEg*QTBQm;5rLTC2!EU_453G1TCWu!*s=(nA&V9D%!J`==V z^76dKC;s{?_{-1!9kv!B=48IciH`T9&;pL7Rh}B3&CzWyd3dtyB_l#n?fJX{-r5L2 z2|9eHysa^lbZUiQsn%V&q`9?@>GEQzw0U_WpeNQYIy-E@x?5ykq9any+*zK+U@b#- zAe-!py24r#;5~QVju$`oI&_K@{Uih?yGZwT@aQ8CV|8N#7hQD~YM}#kF&C5A(Lu>l zcqB!Q{arRmo3K>Pc^_O%=ed)~m7q>@t#Xr+&FGSJ1{t4I>vRF>f7YR#Qev4^=ypeV z`1Cpa;k_TnSN`cCq*a8Fd0tLaMM7*Tk)!yokONj?gv`qf8&!cBxNGRUp8?};L%?Pm zKdU?tGmbW2SQ;RmM{j=<;F$Pj3+{6;`GWT2roF_|Z3i$|54-IyLqC{97Lxr7}eg-5wTF(%_6K z9d#sq3q=3(4EbU%;IJgvvfC?E*#e9C40DF)lPTtl85UWJ1(Ex17wh{su>a5j^$nd^ z#u3RJk8TIZ=qGy^hXFqI)%)>BAG;4b(+p)5$t+==lCJX+YtFdmj0x7sWGaC{tWJ|~ z?n-kKlKFWBl8kL-1za0n?$c1su4>)Oo7zRA^`Iaz^P=WUQjSKG%TkpTLun2L|I`f; zyy}kk8q#4lP{K%?hbx)14lX8CDwC;$3eUUjDDHXZoj9_#if}f@bn`S8+vjoa)YF(w z_Hg*3tt_!v%rO~Hgc;T4 zaa66ef~f)*;M}=$*x0{;qeqY8(BVT8fY7f6{S|4AjrQ-y6T1`q&IkV#U;F0wP}B*M zxUVPv*;rPVSU7r52Kn3RZO^O}v%AyzDxQl8G9;mqE^Z?bDY@HGj#l;XphRhe+9}gU zmGlx8fV2)9Qlv=^1QBcC_Yl0Ad6-X|_G+wTfle>hGb$^xmV`@KkJGO@C@f#a87iHk1191&qACd6zi&lcD|e;#`~JIJ$C3XhA~ zRK7_RpG-M5OJEX9$l zuR;IlMfl9$e*+)>_@}WmWtR#koYH*iu~n>xmE4ddkv456u4d{(gh*?5Zj{v%FW?uNuk-Xtb1eNFIYr8X9hHX;tdUvBM#z$g^GAN=eJU`q2UgOS zIk|oa8l_K#pfBMtuK;iQS2yCt*FFoUPCg;Sy;cVu9NE7vzJWJ<=FC|NZHRsALm5uV(mqWV z+gTrfS(ako#g`x)ZJ_Lr@TVX9E8P2u&j@-iP*kX+ zkhH{7$^=xI%deQ6x%_1**#ih&6Ek_-`GQd{-y@q`W*IYev9m#a5}TP(nW=B^lms7b zUSmt+oN}R$>lJ>EF1nNFhgs4mqaD0?O#7SkKu?nMN%yL;(ur};o8E|*UjH0M>oRaF zB%L~)VYo678UW9(B)qVFZX2hbIwMB{ZyY_0?r?}+uZu+5V)bakEaS1eU3qXtzxDJy z=>SPM?_Q~5()1<{S#uH!nN$TGoCWz@S-h-}T(5`{Y|S&&y%qfZx4(}+_>(`w=I&S+ zRRoH)=}vQF4NX_rfcidAeLB!6+b&ygH`3z8I3#<%!$mTPt)*d~$AOGRq@4+R94@%<6 z=rPL|!oi@u64y!YA!|J{!;(8J#`*D{pz~Y+Pk?>rNzIXkP#HU+Ly}3TVfamFEK^S9 z%mX|5!Rl#ES0ta%3`3n0jp+-T!WtQy+ZQ$PAKw4J;L)c}qT~q{GK^098fCxB(Hc4l zNcj~WDXTQRe6JMh7R=CU1CrOdSrtFEJa5fqN)Wd_Pr0dP8ztORfHqfz5v=%E^RdCr zx4l=4jC_03odg_&a3~Wk+p{N&&EMBtaXEhD9q+`!^)-QdSb0{B(LhD6Bm*PuVuJky zaw5_qQ?gFClZXcS`I-gC!b-ugbuv;_q0jtC>RU2!UV;urp`3`u=t-*B)RtJmQIbik zu!lJ5Bbzg-4UwiL!hV9^{+<5|U;FNNP$kMmTd_v1qe-ElSO=4a1!E02JHi4OHYSvn9IhiX zJNkXmGWsDo%3h+-LcSOCx#VHOM9wNXGm<`zU^2}mk0jo=Zv&IrM0{Si*A-LCaq7~G zO*731Fe{OCN0{tPkwyvr{F8r$KmY7!Wk5^S?O>jghQ~x&H=ItxHL{Hz44^e#TX zzLjo_BAug4)-Gzi?lrH*n_mADSm`9lvrNX4i@;>TZ0LSf>Fg&41xZKRjN~jYP9`YG z0FEU3BSR`(%#mdaY@R)X*>oa8BreDtZP*(~^;t|8J5}ODa=5`@fH?(*0K@(O2M_MY zk;8}4UnjMjm=kFUG|pX|Ie8W*&hOw`kDb8BzWf#JQVPQnW{X@3gWb4`svt8&%PVUB zv=pLI@&$N&qfcu575{5Yo@to-pLR33{7xQZ$?wz;Tz;mPZbYDd&^XIkv0{=^d%O89 zHVMaO`Ra%`Jx{A-S-;=KV!SH@PdN7V7k=ia@H0Ppv&@iZWX6(v!P=E`KMd@Zvze<> zG%HOr}d6mITEjFE|ozM4Cml0M_hx4-3=aMKH(i!h&K z>&zLXlPR*vL;?h!>Oj^nQF@L_o#h!8ssNWOmCuphmC zPdFcOho}6g^JmmbGb$r)rL7m3PbOF$4l&=`LpGUW_v|@rojQ%Z^INF097Si2DlRY@ zuA{7C>_2!B_FsM#R*qka-~Pb=jW2%dyGWBB7I}aq;mAE6E|&pi)-=*J!BFElmS_h3 zlbCV7kENnkST7IO%DbBU&6@O;Cu0Kh*;9V(u?yLH%3QMr!}XH@xsETt<9#)K6DlI; zc2YT=JIw+eAkXQl3iQGXhu24V`!BxT5NsFMhT zwKW_*c1$L(9XfbW@~fTQUF`1d2`%mD@#7ebRzx@~ax&5pgohC#8Mz$oAlbg+ep2{j z-&CHX!+*{eSe)C!-kD8Qixi7wiaDm}^;VEqoLxV{v8%5`F<8Y1?)^)A?DJnj9`_{V z?+sRwEpnrWYhiKOKBSr@8e(Ia{O}r?h;1}A_ebrT7EQjM{AY)iwg!lKJxno3HJUz* zS%MwQE1QIz*k`L~8od1WUzh2+MByng*#$xtB^51^18S;tj@3?tV+YpomN)z~4h}jv zOyUgv8r7S=ERMxQrdf(vnhK4ZO>>=oAEWhk^ob=#3HEk(aqj#%LER4=K8)UQC|aZf z<3@4BoL!r|S)0~rXN)PNPNPr>p+-)`NmJCbxo{s47bpV;K^-|RWS&*1`a{%%5&rNa zAH~N%_tz+*4rYr2z5Wmjj-wU4&;z>`(X-*t`|z&i0`tFw|Is?(U7PUK@A0QkC2(|} zmO@UFveTnfi_Bly?8K(Mw#5f=CYHFoSKRTwn$e&OOxRPTNpsM$9N*3mFU2qhj_qH^ zZEyYsTy^{iHbz4W`zxhq6RnU&HJVi}oJMVOfM*;V=R#XaC1K?-=D|=XV}e~s&CRN> zW?EvNg~^JuX_z40<}>uDEFDYB#tZW1$+{v7E0)d|J!@sY$g#6X(cRdGKlt#6@$t`o z4izZ_S&5b5NXGcF)@3s!EkV_02QSZiU?8EG^&eAMObeQb&_=!rF55ccyvZZIobt@- zvXxVoIjMJoIz3#^)@%YB3XCR^rE_YS2u#WkdXz5(#^L@+nc~R4HQe>iJ8|8!E|qZK zPkCk1+wI1JocjQMDl0>9ng*gCG<%DMnoL1c>{MWBvakc&G&Ws+F=2FyfF6QiD1rDl z9y}+^MiKgKz7WW(69J?YTk8+eO?oIeUY18xzbdgi8B0Pv=&qyJ>B~qX7G#N6ju#6gD?@zbQ=i1W_k98>LE^M9 zfu%wp@R^?;r8(Wq$2!PHwll%BlI#a9Xg2M$Hk^jtOv01VhTisS0`tP9PvUO%miy;; zpy5^PM#FA?$G@wkL%+<^|c#z0zX zsf5V4U~B|HIa{8&N?yt?X3Oi^zI6e6C8tU{a_|6~%C-60j+u9mT(C{!Uzn!p0or!! zLJq2A!~>1$_TH{&Teq`6EQ3x&9u!lg zGcSBs#AMyFC|CxlbM}hmSX3E?t0OUy9rk;rnGka5-}Laa0DHT8SY2JkVz!WaSkx;p zPxnMqWt;afy<`et_eX15+bg(PQq-L;wiYQq`Nc2cFFyNu%@+%Mp zJk>5oN>iGIh!U1L402&AKmeG$aOp#+usa=Nof;-+yao?F`WjZ87YhsqLv1h*L+ott z$UtWvyq%Xb%%)THVxs>g^65hUTN(xtV#H*5(3e5YG_jqPE_#Hj7)3Dn;L%rgfXC0C z$NdjHh>w2e3phJnAgkG1wkijHOZnE^fm&blCf09Y>)-5huLjF`niQCjh?MK-fr|4p z9C?=xl*V5>)y-OeTSrf|^E!_rrSHYytl-8wf1{?K0H>STEDZNw%!}lBLwdA)j?0f7 z!aHtxE3P|!F_=(G9jeZy%+92ntR@ParjR8TE?Skbu$kff-Zu6hJRkxQA0`}!P@+2p zOCgcubUYCfsW7y9C4#ubbTScigBr6KtMHhx=krvW{)l!6wL4DG+tv~E}}S0lI_F?F?B5zo@P-G z8q)mGXD4fsb9uFfg+*TzEIQ8U7WYd|L z&Xlb~VU1p=i!x`fHNeV&L+I^4fM9JOKKZ3D<5Pe8RgC8xpQP;>|##wbx-=~SNLs!dO*L23!^cnLP^>Zw&^7$VI97u|5;Of+hHXID((aAgJkUcVX6B`62;DS`3XMJa4}@l*7bo;Wvur|{Ao zONM^e!qknX1A`1DYL zhL&!J*eT&gAt^7edM~)l7r>MKXYuC;+lvjMvtJ6Mlz#~j0rD}lT2lfa+VE$DE z_R?GLk;FNcSsc#Xf7b2*_X%fK| zGU<#ahf@VuwLnnQ9^Q<5r8~gTY{?uW)CdWXKk4&P-kb^noEay@nt0Js~o6a0e z*xkezS+XGwoRhfO<&qaz6a@}#>_e4j67g|jcG&OZ{9=LcK6V0s^yeSN6HlE()ES_x zh|^bc5UX~su&SUf!Fpl_IT~8omPt7R1ZYK>56TVcKA8)xH0(CoZk1yv+E}-XB-ln$ zm04BRTI)HQw>*i!OuIy2H{O1a99Jdzj$A$<&O(=dme1lm%t^8gYrP(BdE+nO=9fPoU5l)x z%G{jJqZMawtgvBodhIH}d`5;-iq*AMan{^bTCik3))nrdpEAMs@H}Q#jXhs!x;Xr- zWWnnq%op<}_nBpxRJQxw0Y<}B!6MoCKzwm)vV*5K&*B3g{1ZI#)LG;*hOZ-Z0`VdY z7AOb{f}|2`V5~M;k+fq^Wr&5EGc_xYFFK6F>u5fu=`FEUj+yWSxuvXPP?qF=!nU$% zfSX4dMBS=5-*nr1YN2RL8Cub4A;C(8Ur9AAUWRFg^?na;{l%Zh%`dwi-HsvI2Elmg zpaC?WQK{v_Wb`wWF?!vej*Z{|%E?4DfI-D@IBd!jLc?a5Y0GEvjI>(SiA)m#> zpX1pS@Po#uYB{Lu(#mhbpu^rBgb%&l?8ue%~D9!O^;HSEoA_&L1#rWc!r>A^WZ#@o$-!VybUQ0=wyD7JTY zWK#?W12KO)J3HdcSw&tQt%}nplqC*W&KnbFVmDTUv)ah%ElS$@uRK9%%f*jvHc z>bi_k=WZ;j0$D&7RfR8p?eFp7k9``aw#Gt#*_#E8rbo3Dkrsfl*d&J=%ex@AzR10*ZQ_&Pu&9b>Li$nH7bLa}Y zW|-86z^qKc7QyfT;BkEL-jCzq6Q?lE0>s_En7U;aW8%}n z?(go$eV_TF2#gbT$~Gcyvh(7iq89dTx}`d{)Jl_~1z|Q#zy#jR>9G0zVsUJ08}C^P zo*X*E?oz3xX<#G%&3X-*&Ghm+?h=7%_lA`(g2Z1#5z2#?qOnRIt zC?>OlY;)I8i+r@4AqFlp+JAy#EO623`>ck!G5 z+kci3M|+%d>cNA9&xSfUAnf!u_g)UpdLSex^!h;qvJQT_c?-!WDc0aS?QPciz&;Su z*}uy+-C9oMz2LB%SKN8GI2a1dO&G&xUYMi88Fa`95}Ka8z^h;O!?@>;UqW2!qETpM zPf@Vron6eQQ|W1D&c(#FlO*DwSmGERJShAS;ov1PPCxaOL`+Pa8P+pW8jr`~*Y?I^ z>|fi!N?+N+O~`ChJNxPE z5`zF|PM^k*)3EDW`jZ(k@?lm+Bhk2S(ibx!To1iqL*T5qNvvtyYfs|)0ybQXv3YtE zdpmn5vr2Nec{<0uSRm;4am97d!TO<#@#(+*GXDH8J|$CiI3nVLlhK=@US_In0**$~ z%c#<4c}TUz<^VU{l$IuW9na6U^oq2g_mbXd<=RcJ*TiV=Sj6qh8*NDDjbajo2d$y3 zj%Aa;gQXMtB$;hNttkuq&~vZDuiySgsXz-zE9W)qEGOyXB_Y)6Kcrie)yv(nw>uHX zPHMpMiU|orpRU2VZ~A0)8+^_ua>hoUodo(ftVZfCEk0HBA?GO z9nZ*$#b`J}6o(Q$)=@3R%l^iO2Ef!yqZ0= zL~at5n&v^Qroqn?n5&t>5}qpCd22><^WiG8FPl-k4EO@{s^xsE;Ko)SCKvYkzA;ZG zSAj{S#(|Y(iR-So6u18LYjJGfnwYBLK#w(KKBP9h1yeT$fkYnd)|j#(0ksHwdv^zG z>+4wG*pRkYX`Yku;aWUoXs2WDmJ3KfC8K|McsHkXv-l{qGj;nsmP)|p-HFuV2E9H8 z{hl5-6%;b-mq%KiAE#Ip5&n<=@<;gk_a8wj2b^jdjVO=vIQUuOr&zzDm0`5%!&96D zsuEtBKmTuUy>)@g0xb_DXh)0U;_YBq4>))aQd?xz!%98DD{s9^;!Lq{y5bUk$|0ld z07U3T~oe&bhemFhE12#b-vI|OsH(7TOJNT@m|q|}k73JPkG^1S5`qt#WJ ziRi-OqRCk>EbCYzDG{NWC|;Lv78jBfZa)XX4hpH zVQaDw3fw0?^#%O#NA8ocvl4O%WxX}_!schlY=2>bC(C)q_N)#gY1F6(;;9L@)ApJW zPX)W|$6MxDGUB3j2BvGdpd4206s`Ud4hNI$-gXEt$T{&5u;7edmZ3YD)HMb~#)}d&4hLH-l#|D6bJ%jXBYsXE)Tejz zc}ecHGP?8rmb5YjXC>eB!eGtP|3WGS`8>z==6R{3ZLF@KtTIFiJ1%RmHIZ|G!_E+2 zz5iSI!w>%@w&tV)5HQfD5M5X`Z`3T7YUt9XfnK_Obb+6l*!n_?yacpu6wjzD7gn~t z(CHDQ5|Wl}yNVpOD$Y0G`fiy{r_F@c+swrzxrSgaU7GPam_V=fdwAckz7yA9dyUYm zV@lJg1rl~ky-4vtJqAUoVSyZkr%WfpQ$+2Dq*t?*RuV^@yP*)2vzHXEH37i!z-d=)^KRWOI9rypHhTBR{~s_k9)*J#iXg zhxr&02~A|hG!q`<4!2qJ%9OGw&aGg(rPV-UO`{B{cbN%d;>S_h++g#U&1+3yI@e3T zJav%G@T%K>&2eCv!X((cC5cjua|E7yicJr7g`fSg*WwL7`8uo)5+I++R5vZ;kx9jX zMRyU)HEdVnPnIzV6EUh}DWBTNvJ7)D4U0K|O9y#zk)(Zy>|O~wdA>r24E++X~FOxr7Dnj9+%t1BzotidVT43eT4=l1r5w?Zn!y&wCuB+~3wNbx@r` zlMWdcKFR(SsIA1_?vC_bGqE0yMoLMbCL@qz?pZhM_6NBCo8QNO{m@5nelixUv1CHd z+@}iA;aQm?FooizCdS4^@5#16LeLZ|Ji2xWRlrGnGP+0!v;R#3;~8c)-^4WRUlv$% zmWXSZ%zjd`X`C+bzPo-EFMQs$Jh%ZNg(?jcG>QL`hAjmqQ6A^-Mj?9iUDExi=2Ixm zHVlitil&puh~ST$Cd!i>vP{e=zZkHvzE7VuT3u6mUS47$9W6{HcI2CyeE9HTtZ@ur z!fB;Ee91{wn7g-iNk zG3%{^wtYuQpkRJXj;m+oPZ7|1T3p=Re;*F{S_j002ovPDHLkV1igk B$FBeY literal 0 HcmV?d00001 diff --git a/feed.json b/feed.json new file mode 100644 index 00000000..892848b3 --- /dev/null +++ b/feed.json @@ -0,0 +1,1158 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "个人博客", + "home_page_url": "http://www.wenzhihuai.com/", + "feed_url": "http://www.wenzhihuai.com/feed.json", + "description": "个人博客", + "favicon": "http://www.wenzhihuai.com/favicon.ico", + "items": [ + { + "title": "mac通过网线连接主机(fnOS)", + "url": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html", + "id": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html", + "summary": "mac通过网线连接主机(fnOS) 一、mac端 mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的 点击详细信息,配置IPv4选择使用DHCP 二、主机端 主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。 *-network UNCLAIMED 表明你的...", + "content_html": "\n

一、mac端

\n

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

\n
\"\"
\n

点击详细信息,配置IPv4选择使用DHCP

\n
\"\"
\n

二、主机端

\n

主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。

\n
root@server:~# sudo lshw -C network\n  *-network\n       description: Wireless interface\n       product: Wi-Fi 6 AX210/AX211/AX411 160MHz\n       vendor: Intel Corporation\n       physical id: 0\n       bus info: pci@0000:01:00.0\n       logical name: wlp1s0\n       version: 1a\n       serial: 10:5f:ad:d6:2b:ee\n       width: 64 bits\n       clock: 33MHz\n       capabilities: pm msi pciexpress msix bus_master cap_list ethernet physical wireless\n       configuration: broadcast=yes driver=iwlwifi driverversion=6.6.38-trim firmware=72.daa05125.0 ty-a0-gf-a0-72.uc ip=192.168.0.113 latency=0 link=yes multicast=yes wireless=IEEE 802.11\n       resources: irq:18 memory:80900000-80903fff\n  *-network UNCLAIMED\n       description: Ethernet controller\n       product: RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller\n       vendor: Realtek Semiconductor Co., Ltd.\n       physical id: 0\n       bus info: pci@0000:02:00.0\n       version: 2b\n       width: 64 bits\n       clock: 33MHz\n       capabilities: pm msi pciexpress msix cap_list\n       configuration: latency=0\n       resources: ioport:3000(size=256) memory:80804000-80804fff memory:80800000-80803fff
\n

*-network UNCLAIMED 表明你的网络接口未被驱动程序识别和管理。为了解决这个问题,你需要安装或重新加载正确的网络驱动程序。

\n

下载并安装 Realtek 网络驱动程序

\n

访问 Realtek 官方网站下载适用于你的 RTL8111/8168/8411 网络控制器的驱动程序,通常是 r8168 驱动程序。

\n\n
root@server:/vol1# cd /vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00/\nroot@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# ls\nautorun.sh  Makefile  README  src\nroot@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# sh autorun.sh\n\nCheck old driver and unload it.\nBuild the module and install\nWarning: modules_install: missing 'System.map' file. Skipping depmod.\nBackup r8169.ko\nrename r8169.ko to r8169.bak\nDEPMOD 6.6.38-trim\nload module r8168\nUpdating initramfs. Please wait.\nupdate-initramfs: Generating /boot/initrd.img-6.6.38-trim\nCompleted.
\n

安装好了之后,理论上自动显示网口2,但IP什么的都是空的,需要点击编辑,然后按照下面的填一下。

\n
\"\"
\n

三、最后

\n

最后就可以互相ping通了,很稳定,传输速度很快很快,基本都能1ms以内

\n
\"\"
\n", + "image": "https://github-images.wenzhihuai.com/images/QQ_1732811324411.png", + "date_published": "2024-11-29T18:45:10.000Z", + "date_modified": "2024-11-29T18:45:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "推荐工程-概述", + "url": "http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html", + "id": "http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html", + "summary": "推荐工程-概述 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。 平台方、信息生产者和消费者可以分别用平台方(如:腾...", + "content_html": "\n

随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

\n

平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户来指代。

\n

技术的最终目的都是服务业务的,我们来看下当前电商系统的模式差异。

\n
\"\"
\n

一、淘宝和拼多多的电商模式差异

\n

淘宝是流量逻辑,主体是搜索,需要海量SKU满足长尾需求;拼多多代表的是匹配逻辑,推荐商品给消费者,SKC有限,但满足结构性丰富。之前的电商模式都是从淘宝挖一块做小淘宝,而拼多多是在另一个维度做电商,满足不同的场景需求。淘宝的千人千面相当于个性化搜索,但搜索本身是长尾的,你就很难做反向定制。而拼多多集中流量到有限商品里,有了规模后再反向定制。

\n

拼多多起来之后,京东、唯品会、蘑菇街都实验过相似模式,对于他们来说,拼团不过是一个创造GMV增量的工具;而拼多多是人的逻辑,我们通过拼团了解人,通过人推荐物,后期会过渡到机器推荐物。拼多多APP里几乎没有搜索,也不设购物车,你可以想像把今日头条下的信息流换成商品流就是拼多多。所以早期看大家都是低价和拼团,但我们的出发点不同、方向不同,长大了也就不一样了。

\n

与其说拼多多是社交电商,不如说它是“人以群分”的电商。以前是人去找东西,现在是相似的人聚集起来,迅速产生需求量,这种模式给供应链优化创造了很大的机会。

\n

二、搜索、推荐、广告三者的异同

\n

搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:

\n
    \n
  1. 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
  2. \n
  3. 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
  4. \n
  5. 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
  6. \n
  7. 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。
  8. \n
\n

**广告:**借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

\n

三、推荐整体架构

\n
\"\"
\n

推荐的框架主要有以下几个模块:

\n
    \n
  • 协议调度:请求的发送和结果的回传。在请求中,用户会发送自己的 ID,地理位置等信息。结果回传中会返回推荐系统给用户推荐的结果。
  • \n
  • 推荐算法:算法按照一定的逻辑为用户产生最终的推荐结果。不同的推荐算法基于不同的逻辑与数据运算过程。
  • \n
  • 消息队列:数据的上报与处理。根据用户的 ID,拉取例如用户的性别、之前的点击、收藏等用户信息。而用户在 APP 中产生的新行为,例如新的点击会储存在存储单元里面。
  • \n
  • 存储单元:不同的数据类型和用途会储存在不同的存储单元中,例如内容标签与内容的索引存储在 mysql 里,实时性数据存储在 redis 里,需要进行数据统计的数据存储在 TDW 里。
  • \n
\n

电商系统演变到现在,各类归纳层出不穷,传统零售是“货-场-人”,而新零售是“人-货-场”,强调以人为核心,根据用户的需求推送产品和打造消费场景。在电商线上平台三者具体表现就是:

\n

**人:**是指店铺访客、流量。通过数据分析、用户画像、顾客细分等手段,可以更加精准地理解顾客,从而提供个性化的产品和服务
\n**货:**是指商家商品。选择合适的商品,有竞争力的价格,保证商品的质量和供应链的高效运营,都是成功的关键
\n**场:**由最初的交易平台(如京东、淘宝等)演变到场景(首页、商详推荐页、加购推荐页等),实现千人千面。

\n
\"\"
\n

回到我们推荐本身,推荐的本质就是排序,把成千上万的商品更精准的展现到用户面前,提升用户的购买量,从而变现。技术层面,最核心的就是推荐算法,目前市面上对推荐系统基本等于推荐算法,而做为一名后台,在推荐系统中的角色介绍可为少之又少,本系列就用后台的角色去看待整个推荐系统

\n

参考

\n

1.FunRec

\n

2.从零开始了解推荐系统全貌

\n

3.从“人、货、场”的不同视角,重新审视电商和新零售

\n

4.“人货场”,在产品业务分析中的具体应用

\n", + "image": "https://github-images.wenzhihuai.com/images/image-20241130000406534.png", + "date_published": "2024-11-29T18:45:10.000Z", + "date_modified": "2024-11-29T18:45:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2024-11-09上海迪斯尼", + "url": "http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html", + "id": "http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html", + "summary": "2024-11-09上海迪斯尼", + "content_html": "\n", + "date_published": "2024-11-10T15:55:36.000Z", + "date_modified": "2024-11-11T16:10:11.000Z", + "authors": [], + "tags": [] + }, + { + "title": "买了个mini主机", + "url": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html", + "id": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html", + "summary": "买了个mini主机 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是...", + "content_html": "\n

虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

\n

成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。

\n
\"iowejofwjeofjwoeifjwoe\"
iowejofwjeofjwoeifjwoe
\n

一、系统的安装

\n

系统我看是支持windows,还有现在Ubuntu,但是我这种选择的是centos stream 9, 10的话我也找过,但是发现很多软件还有不兼容。所以最终还是centos stream 9。

\n

1、下载Ventoy软件

\n

去Ventoy官网下载Ventoy软件(Download . Ventoy)如下图界面

\n
\"QQ_1727625608185\"
QQ_1727625608185
\n

2、制作启动盘

\n

选择合适的版本以及平台下载好之后,进行解压,解压出来之后进入文件夹,如下图左边所示,双击打开Ventoy2Disk.exe,会出现下图右边的界面,选择好自己需要制作启动盘的U盘,然后点击安装等待安装成功即可顺利制作成功启动U盘。

\n

3、centos安装

\n

直接取官网,下载完放到u盘即可。

\n
\"QQ_1727625711792\"
QQ_1727625711792
\n

它的BIOS是按F7启动,直接加载即可。

\n
\"image-20241007222938414\"
image-20241007222938414
\n

之后就是正常的centos安装流程了。

\n

二、连接wifi

\n

因为是用作服务器的,所以并没有给它配置个专门的显示器,只要换个网络,就连不上新的wifi了,这里可以用网线连接路由器进行下面的操作即可。

\n

在 CentOS 系统中,通过命令行连接 Wi-Fi 通常需要使用 nmcli(NetworkManager 命令行工具)来管理网络连接。nmcli 是 NetworkManager 的一个命令行接口,可以用于创建、修改、激活和停用网络连接。以下是如何使用 nmcli 命令行工具连接 Wi-Fi 的详细步骤。

\n

步骤 1: 检查网络接口

\n

首先,确认你的 Wi-Fi 网络接口是否被检测到,并且 NetworkManager 是否正在运行。

\n
nmcli device status
\n

输出示例:

\n
DEVICE         TYPE      STATE         CONNECTION\nwlp3s0         wifi      disconnected  --\nenp0s25        ethernet  connected     Wired connection 1\nlo             loopback  unmanaged     --
\n

在这个示例中,wlp3s0 是 Wi-Fi 接口,它当前处于未连接状态。

\n

步骤 2: 启用 Wi-Fi 网卡

\n

如果你的 Wi-Fi 网卡是禁用状态,可以通过以下命令启用:

\n
nmcli radio wifi on
\n

验证 Wi-Fi 是否已启用:

\n
nmcli radio
\n

步骤 3: 扫描可用的 Wi-Fi 网络

\n

使用 nmcli 扫描附近的 Wi-Fi 网络:

\n
nmcli device wifi list
\n

你将看到可用的 Wi-Fi 网络列表,每个网络都会显示 SSID(网络名称)、安全类型等信息。

\n

步骤 4: 连接到 Wi-Fi 网络

\n

使用 nmcli 命令连接到指定的 Wi-Fi 网络。例如,如果你的 Wi-Fi 网络名称(SSID)是 MyWiFiNetwork,并且密码是 password123,你可以使用以下命令连接:

\n
nmcli device wifi connect 'xxxxxx' password 'xxxxx'
\n

你应该会看到类似于以下输出,表明连接成功:

\n
Device 'wlp3s0' successfully activated with 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
\n

步骤 5: 验证连接状态

\n

验证网络连接状态:

\n
nmcli connection show
\n

查看当前连接的详细信息:

\n
nmcli device show wlp3s0
\n

上面的其实在2024年其实有点问题,因为会默认连接2.4GHz的wifi,使用的时候很明显没有那么快,特别是在用命令行的时候会觉得明显卡顿,现在需要切换到5GHz的wifi。
\n首先,使用 nmcli 获取可用 WiFi 网络及其 BSSID:

\n
nmcli -f SSID,BSSID,CHAN dev wifi list
\n

示例输出:

\n
SSID       BSSID              CHAN\nMyNetwork  XX:XX:XX:XX:XX:01  36\nMyNetwork  XX:XX:XX:XX:XX:02  1
\n

在这里, XX:XX:XX:XX:XX:01 是 5GHz 网络的 BSSID。

\n

使用 nmcli 连接到特定的 BSSID

\n
nmcli dev wifi connect 'XX:XX:XX:XX:XX:01' password 'your_password'
\n

三、VNC远程连接

\n

桌面还是偶尔需要用一下的,虽然用的不多。

\n
root@master:~# dnf install  -y  tigervnc-server\nroot@master:~# vncserver\nbash: vncserver: command not found...\nInstall package 'tigervnc-server' to provide command 'vncserver'? [N/y] y\n\n\n * Waiting in queue... \n * Loading list of packages.... \nThe following packages have to be installed:\n dbus-x11-1:1.12.20-8.el9.x86_64        X11-requiring add-ons for D-BUS\n tigervnc-license-1.14.0-3.el9.noarch   License of TigerVNC suite\n tigervnc-selinux-1.14.0-3.el9.noarch   SELinux module for TigerVNC\n tigervnc-server-1.14.0-3.el9.x86_64    A TigerVNC server\n tigervnc-server-minimal-1.14.0-3.el9.x86_64    A minimal installation of TigerVNC server\nProceed with changes? [N/y] y\n\n\n * Waiting in queue... \n * Waiting for authentication... \n * Waiting in queue... \n * Downloading packages... \n * Requesting data... \n * Testing changes... \n * Installing packages... \n\nWARNING: vncserver has been replaced by a systemd unit and is now considered deprecated and removed in upstream.\nPlease read /usr/share/doc/tigervnc/HOWTO.md for more information.\n\nYou will require a password to access your desktops.\n\ngetpassword error: Inappropriate ioctl for device\nPassword:
\n

之后在mac开启屏幕共享就可以了

\n
\"image-20241007225855305\"
image-20241007225855305
\n
\"QQ_1728313164289\"
QQ_1728313164289
\n

四、docker 配置

\n

docker安装我以为很简单,没想到这里是最难的一步了。安装完docker之后,总是报错:

\n
Error response from daemon: Get \"https://registry-1.docker.io/v2/\": context deadline exceeded
\n

即使改了mirrors也毫无作用

\n
{\n  \"registry-mirrors\": [\n    \"https://ylce84v9.mirror.aliyuncs.com\"\n        ]\n}
\n

看起来好像是docker每次pull镜像都要访问一次registry-1.docker.io,但是这个网址国内已经无法连接了,各种折腾,这里只贴一下代码吧,原理就就不讲了(懂得都懂)。

\n
\"img\"
img
\n
sslocal -c /etc/shadowsocks.json -d start\ncurl --socks5 127.0.0.1:1080 http://httpbin.org/ip\n\nsudo yum -y install privoxy
\n

vim /etc/systemd/system/docker.service.d/http-proxy.conf

\n
[Service]\nEnvironment=\"HTTP_PROXY=http://127.0.0.1:8118\"
\n

/etc/systemd/system/docker.service.d/https-proxy.conf

\n
[Service]\nEnvironment=\"HTTPS_PROXY=http://127.0.0.1:8118\"
\n

最后重启docker

\n
systemctl start privoxy\nsystemctl enable privoxy\nsudo systemctl daemon-reload\nsudo systemctl restart docker
\n
\"QQ_1729956484197\"
QQ_1729956484197
\n

五、文件共享

\n

sd卡好像读取不了,只能换个usb转换器

\n
fdisk -l\nmount /dev/sdb1 /mnt/usb/sd
\n

在CentOS中设置文件共享,可以使用Samba服务。以下是配置Samba以共享文件的基本步骤:

\n
    \n
  1. 安装Samba
  2. \n
\n
sudo yum install samba samba-client samba-common
\n
    \n
  1. \n

    设置共享目录

    \n

    编辑Samba配置文件/etc/samba/smb.conf,在文件末尾添加以下内容:

    \n
  2. \n
\n
[shared]\n   path = /path/to/shared/directory\n   writable = yes\n   browseable = yes\n   guest ok = yes
\n
    \n
  1. \n

    设置Samba密码

    \n

    为了允许访问,需要为用户设置一个Samba密码:

    \n
  2. \n
\n
sudo smbpasswd -a your_username
\n
    \n
  1. 重启Samba服务
  2. \n
\n
sudo systemctl restart smb.service\nsudo systemctl restart nmb.service
\n
    \n
  1. \n

    配置防火墙(如果已启用)

    \n

    允许Samba通过防火墙:

    \n
  2. \n
\n
sudo firewall-cmd --permanent --zone=public --add-service=samba\nsudo firewall-cmd --reload
\n

现在,您应该能够从网络上的其他计算机通过SMB/CIFS访问共享。在Windows中,你可以使用\\\\centos-ip\\shared,在Linux中,你可以使用smbclient //centos-ip/shared -U your_username

\n
\"QQ_1730035390803\"
QQ_1730035390803
\n

参考:

\n

https://shadowsockshelp.github.io/Shadowsocks/linux.html

\n

https://stackoverflow.com/questions/48056365/error-get-https-registry-1-docker-io-v2-net-http-request-canceled-while-b

\n", + "image": "https://github-images.wenzhihuai.com/images/iowejofwjeofjwoeifjwoe.png", + "date_published": "2024-10-27T13:33:05.000Z", + "date_modified": "2024-11-29T18:45:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "流程编排LiteFlow", + "url": "http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html", + "id": "http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html", + "summary": "流程编排LiteFlow LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得...", + "content_html": "\n

LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

\n

另外, LiteFlow 和 Activiti 们并不是同一个东西,而是面向不同的使用场景和需求。LiteFlow 更加轻量灵活,适合需要简单流程管理和动态配置的场景;而 Activiti 则是一个全面的 BPM 引擎,适合需要复杂业务流程管理和任务管理的场景。根据具体业务需求,可以选择合适的工具来实现流程编排。

\n

背景

\n

之前做过一个数据分发系统,需要消费kafka的数据,下游有不同的业务,每个业务可能有共同的地方,也有不同的地方,在经过各类的处理之后,最后数据分发到下游里面去。为了简化代码方便理解,我们定义4个Handler(A、B、C、D),然后有3个不同的业务,需要经过不同的Handler,整个流程如下。

\n\"image-20241108000137572\"\n

如果要在一个代码实现上诉功能,我们第一反应可能是责任链设计模式,每个业务一条链路,在Spring中,类似下面的代码:

\n
public abstract class Handler {\n    abstract void handler(Request request);\n}\n\n@Component\n@Slf4j\npublic class HandlerA extends Handler{\n    @Override\n    public void handler(Request request) {\n        log.info(\"处理器1\");\n    }\n}\n\n@Component\n@Slf4j\npublic class HandlerB extends Handler {\n    @Override\n    public void handler(Request request) {\n        log.info(\"处理器2\");\n    }\n}\n\n@Component\n@Slf4j\npublic class HandlerC extends Handler{\n    @Override\n    public void handler(Request request) {\n        log.info(\"处理器3\");\n    }\n}\n@Component\n@Slf4j\npublic class HandlerD extends Handler{\n    @Override\n    public void handler(Request request) {\n        log.info(\"处理器4\");\n    }\n}\n\n//然后我们定义一个枚举类,用来配置不同业务需要经历过的处理器。\npublic enum HandleBuz {\n    Business_1(HandlerA,HandlerB),\n    Business_2(HandlerB,HandlerC),\n    Business_3(HandlerA,HandlerD);\n    public final Class<? extends Handler>[] processors;\n    public HandleBuz(Class<? extends Handler>[] processors){\n        this.processors=processors;\n    }    \n    public void handle(){\n        for (Handler handler : processors) {\n            handler.handler(xxx);\n        }\n    }\n\n}
\n

通过配置责任链,可以灵活地组合处理对象,实现不同的处理流程,并且可以在运行时动态地改变处理的顺序,由于责任链模式遵循开闭原则,新的处理者可以随时被加入到责任链中,不需要修改已有代码,提供了良好的扩展性。但实际上面对各种需求的时候,没法做到完全的解耦,比如对于HandlerA,如果业务1和业务2都有定制化的需求(来自产品提的临时或长期需求),此时是应该再HandlerA中用if else解决,还是再额外开个HandlerA_1和HandlerA_2。这类特性需求会非常多,最终把代码可读性变得越来越低。

\n

一、为什么需要流程编排

\n

LiteFlow由Baidu开源,专注于逻辑驱动流程编排,通过组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑。它以其轻量级、快速、稳定且可编排的特性,在业务流程管理、规则引擎、工作流、订单处理、数据处理、微服务编排以及智能化流程管理等领域都有广泛的应用前景。

\n
\"img\"
img
\n

二、它可以解决什么问题

\n

对大部分不断迭代的代码来说,历史遗留的代码加上需要面对各类各样的需求,代码会变得越来越难维护,甚至变成屎山。我们想着不断的去进行解耦,不断的去进行切割拆分,还要兼顾新需求,就怕蝴蝶效应导致大故障,liteflow能帮我们在解耦上更加清晰一点。
\n(1)复杂业务流程编排和管理
\n在一些应用场景中,业务逻辑往往非常复杂,涉及多个步骤的执行,并且这些步骤之间具有复杂的依赖关系。LiteFlow 可以帮助开发者通过配置和代码相结合的方式定义和管理这些流程。
\n(2)流程动态配置
\nLiteFlow 允许通过配置文件或者数据库动态修改流程,而无需修改代码。这意味着可以根据不同的业务需求快速调整并发布新的流程,而不需要重新部署应用。
\n(3)流程节点的复用和解耦
\n在使用 LiteFlow 时,每个业务步骤都可以定义为一个独立的节点(Node),这些节点可以独立开发、测试和维护,并且可以在多个流程中复用。通过这种方式,可以实现业务逻辑的复用和解耦,提高代码的可维护性。
\n(4)节点状态和错误处理
\nLiteFlow 提供了丰富的节点状态管理和错误处理机制,允许开发者在流程执行过程中捕获和处理异常,从而确保系统的稳定性和健壮性。
\n(5) 高扩展性和自定义能力
\nLiteFlow 具有高度的扩展性,开发者可以根据自身业务的特殊需求定制节点、组件和插件,从而满足复杂场景的要求。

\n

以下是一些实际使用 LiteFlow 的示例场景:
\n(1)订单处理系统:在电商系统中,订单处理涉及多个步骤,如库存检查、支付处理、订单确认和发货等。LiteFlow 可以帮助将这些步骤分开独立实现,然后通过流程引擎编排执行。
\n(2)审批流程:在企业中,审批流程通常包括多个节点(如申请、审批、复核、归档等),并且这些节点之间可能有条件和依赖关系。LiteFlow 可以帮助动态配置和管理这些流程,提高审批效率。
\n(3)营销活动:在一些营销活动中,不同的活动环节和逻辑可能会因用户行为和外部条件而变化。LiteFlow 可以帮助实现灵活的活动规则配置和执行。

\n

三、LiteFlow改造之后

\n

首先定义并实现一些组件,确保SpringBoot会扫描到这些组件并注册进上下文。

\n
@Slf4j\n@LiteflowComponent(\"a\")\npublic class HandlerA extends NodeComponent {\n    @Override\n    public void process() throws Exception {\n        Customizer contextBean = this.getContextBean(Customizer.class);\n    }\n}\n\n@Slf4j\n@LiteflowComponent(\"b\")\npublic class HandlerB extends NodeComponent {\n    @Override\n    public void process() throws Exception {\n        Customizer contextBean = this.getContextBean(Customizer.class);\n    }\n}\n\n@Slf4j\n@LiteflowComponent(\"c\")\npublic class HandlerC extends NodeComponent {\n    @Override\n    public void process() throws Exception {\n        Customizer contextBean = this.getContextBean(Customizer.class);\n    }\n}\n\n@Slf4j\n@LiteflowComponent(\"d\")\npublic class HandlerD extends NodeComponent {\n    @Override\n    public void process() throws Exception {\n        Customizer contextBean = this.getContextBean(Customizer.class);\n    }\n}
\n

同时,你得在resources下的config/flow.el.xml中定义规则:

\n
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<flow>\n    <chain name=\"chain1\">\n        THEN(\n        a,b\n        );\n    </chain>\n    <chain name=\"chain2\">\n        THEN(\n        b,c\n        );\n    </chain>\n    <chain name=\"chain3\">\n        THEN(\n        a,d\n        );\n    </chain>\n</flow>
\n

最后,在消费kafka的时候,先定义一个ruleChainMap,用来判断根据唯一的id(业务id或者消息id)来判断走哪条chain、哪个组件等,甚至可以定义方法级别的组件。

\n
    private Map<Integer, String> ruleChainMap = new HashMap<>();\n    @Resource\n    private FlowExecutor flowExecutor;\n\n    @PostConstruct\n    private void init() {\n        ruleChainMap.put(1, \"业务1\");\n        ruleChainMap.put(2, \"业务2\");\n        ruleChainMap.put(3, \"业务3\");\n    }\n\n    @KafkaListener(topics = \"xxxx\")\n    public void common(List<ConsumerRecord<String, String>> records) {\n        for (ConsumerRecord<String, String> record : records) {\n            ...\n            String chainName = ruleChainMap.get(\"唯一id(可以是record里的,也可以全局定义的id)\");\n            LiteflowResponse response = flowExecutor.execute2Resp(chainName, xxx, xxx, new TempContext());\n        }\n    }
\n

由于篇幅的关系,这里不再讲解怎么传递上下文的关系,可以自己去官网研究一下。另外,上面的例子因为是简化之后的,如果你觉得不够形象,可以看看下面的实际业务。这个如果不使用liteflow,可能就得在主流程代码里增加各种if else,甚至后续改了一小块也不知道对别的地方有没有影响。

\n
\"image-20241108000231371\"
image-20241108000231371
\n

总结

\n

后续,如果面对产品经理“来自大领导的一个想法,我不知道后续还会不会一直做下去,反正先做了再说”这类需求,就可以自己定义一个LiteFlow的组件,既不污染主流程的代码,后续下线了删掉即可,赏心悦目。

\n

文档&参考

\n

1.【腾讯文档】业务处理复杂 https://docs.qq.com/flowchart/DZVFURmhCb0JFUHFD
\n2.【腾讯文档】业务处理复杂2 https://docs.qq.com/flowchart/DZXVOaUV5VGRtc3ZD
\n3.一文搞懂设计模式—责任链模式
\n4.LiteFlow官网

\n", + "image": "https://github-images.wenzhihuai.com/images/1.svg", + "date_published": "2024-09-08T16:55:53.000Z", + "date_modified": "2024-11-07T16:03:32.000Z", + "authors": [], + "tags": [] + }, + { + "title": "推荐系统入门", + "url": "http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html", + "id": "http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html", + "summary": "推荐系统入门 转载自:https://www.cnblogs.com/cgli/p/17225189.html 一、背景 近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不...", + "content_html": "\n

转载自:https://www.cnblogs.com/cgli/p/17225189.html

\n

一、背景

\n

近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不数据规模。数据级量小,业务场景也简单,实现起比较的容易,但原理基本上大同小异。

\n

本文尝试从最简单的推荐入手,我们暂不去讨论大规模数据分析和算法。更多从软件工程角度思考问题,那些高大上算法留给读者去思考。开源获取:http://www.jnpfsoft.com/?from=infoq

\n

下面就来谈谈整个推荐的设计实现过程。

\n

二、推荐系统介绍

\n

2.1、为什么需要推荐系统?

\n

首先推荐系统作用是很大的。推荐系统在很多业务场景有广泛使用和发挥的空间,它的应用和影子无处不在,在多媒体内容、广告平台和电商平台尤为常见。大多平台商或企业都是基于大数据算法分析做推荐系统。推荐算法也是层出不穷,比如相似度计算、近邻推荐、概率矩阵分解、概率图等,当然也有综合多种算法融合使用。

\n

互联网时代,数据呈爆炸式增长,前所未有的数据量远远超过受众的接收和处理能力,因此,从海量复杂数据中有效获取关键性有用信息成为必须解决的问题。面对信息过载问题,人们迫切需要一种高效的信息过滤系统,“推荐系统”应运而生。

\n

20 世纪 90 年代以来,尽管推荐系统在理论、方法和应用方面取得了系列重要进展,但数据的稀疏性与长尾性、用户行为模式挖掘、可解释性、社会化推荐等问题仍然是其面临的重要挑战。

\n

进一步地,伴随互联网及信息技术的持续飞速发展,用户规模与项目数量急剧增长,相应地,用户行为数据的稀疏性、长尾性问题更加凸显。也就是说目前各大平台虽然已经推荐系统,但是实际应用当中还是面临很多问题,仍然有很大的提升空间。这是技术挑战也机会,当然这也是我们这些从业者可以发挥的地方。

\n

2.2、推荐系统解决什么问题?

\n

推荐系统从 20 世纪 90 年代就被提出来了,但是真正进入大众视野以及在各大互联网公司中流行起来,还是最近几年的事情。

\n

随着移动互联网的发展,越来越多的信息开始在互联网上传播,产生了严重的信息过载。因此,如何从众多信息中找到用户感兴趣的信息,这个便是推荐系统的价值。精准推荐解决了用户痛点,提升了用户体验,最终便能留住用户。

\n

推荐系统本质上就是一个信息过滤系统,通常分为:召回、排序、重排序这 3 个环节,每个环节逐层过滤,最终从海量的物料库中筛选出几十个用户可能感兴趣的物品推荐给用户。

\n

推荐系统的分阶段过滤流程如下图所示:

\n
\"img\"
img
\n

2.3、推荐系统应用场景

\n

哪里有海量信息,哪里就有推荐系统,我们每天最常用的 APP 都涉及到推荐功能:

\n

资讯类:今日头条、腾讯新闻等

\n

电商类:淘宝、京东、拼多多、亚马逊等

\n

娱乐类:抖音、快手、爱奇艺等

\n

生活服务类:美团、大众点评、携程等

\n

社交类:微信、陌陌、脉脉等

\n
\"img\"
img
\n

实际例子还有很多,稍微上一点规模的平台或 APP 都有这一个推荐模块。

\n

推荐系统的应用场景通常分为以下两类:

\n

基于用户维度的推荐:根据用户的历史行为和兴趣进行推荐,比如淘宝首页的猜你喜欢、抖音的首页推荐等。

\n

基于物品维度的推荐:根据用户当前浏览的标的物进行推荐,比如打开京东 APP 的商品详情页,会推荐和主商品相关的商品给你。

\n

2.4、搜索、推荐、广告三者的异同

\n

搜索和推荐是 AI 算法最常见的两个应用场景,在技术上有相通的地方。

\n

搜索:有明确的搜索意图,搜索出来的结果和用户的搜索词相关。推荐:不具有目的性,依赖用户的历史行为和画像数据进行个性化推荐。广告:借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

\n

三、推荐系统通用框架

\n

推荐系统涉及周边和自己模块还是比较多,这里主要从最简单推荐系统自身功能去构思设计简单结构。

\n
\"img\"
img
\n

上面这个图基本把推荐处理过程画出来,结构比较清晰,看图理想即可。

\n

从分层架构设计视角来说可以分成多层架构形式

\n

分层:排序层、过滤层、召回层、数据存储层、计算平台、数据源。

\n

可以说市面上推荐系统设计都是差不多是这个样子,只是里面使用技术或组件不同而已。

\n
\"img\"
img
\n

上面是推荐系统的整体架构图,自下而上分成了多层,各层的主要作用如下:

\n
    \n
  • \n

    数据源:推荐算法所依赖的各种数据源,包括物品数据、用户数据、行为日志、其他可利用的业务数据、甚至公司外部的数据。

    \n
  • \n
  • \n

    计算平台:负责对底层的各种异构数据进行清洗、加工,离线计算和实时计算。

    \n
  • \n
  • \n

    数据存储层:存储计算平台处理后的数据,根据需要可落地到不同的存储系统中,比如 Redis 中可以存储用户特征和用户画像数据,ES 中可以用来索引物品数据,Faiss 中可以存储用户或者物品的 embedding 向量等。

    \n
  • \n
  • \n

    召回层:包括各种推荐策略或者算法,比如经典的协同过滤,基于内容的召回,基于向量的召回,用于托底的热门推荐等。为了应对线上高并发的流量,召回结果通常会预计算好,建立好倒排索引后存入缓存中。

    \n
  • \n
  • \n

    融合过滤层:触发多路召回,由于召回层的每个召回源都会返回一个候选集,因此这一层需要进行融合和过滤。

    \n
  • \n
  • \n

    排序层:利用机器学习或者深度学习模型,以及更丰富的特征进行重排序,筛选出更小、更精准的推荐集合返回给上层业务。从数据存储层到召回层、再到融合过滤层和排序层,候选集逐层减少,但是精准性要求越来越高,因此也带来了计算复杂度的逐层增加,这个便是推荐系统的最大挑战。

    \n
  • \n
\n

其实对于推荐引擎来说,最核心的部分主要是两块:特征和算法。

\n
\"img\"
img
\n

这些工具和技术框架都是比较成熟稳定的,是众多厂商在实际业务场景中选择应用的。所以也没有太多特殊的地方。

\n

特征计算由于数据量大,通常采用大数据的离线和实时处理技术,像 Spark、Flink 等,然后将计算结果保存在 Redis 或者其他存储系统中(比如 HBase、MongoDB 或者 ES),供召回和排序模块使用。

\n

召回算法的作用是:从海量数据中快速获取一批候选数据,要求是快和尽可能的准。这一层通常有丰富的策略和算法,用来确保多样性,为了更好的推荐效果,某些算法也会做成近实时的。

\n

排序算法的作用是:对多路召回的候选集进行精细化排序。它会利用物品、用户以及它们之间的交叉特征,然后通过复杂的机器学习或者深度学习模型进行打分排序,这一层的特点是计算复杂但是结果更精准。

\n

四、经典算法

\n

了解了推荐系统的整体架构和技术方案后,下面带大家深入一下算法细节。这里选择图解的是推荐系统中的明星算法:协同过滤(Collaborative Filtering,CF)。

\n

对于很多同学来说,可能觉得 AI 算法晦涩难懂,门槛太高,确实很多深度学习算法的确是这样,但是协同过滤却是一个简单同时效果很好的算法,只要你有初中数学的基础就能看懂。

\n

4.1、协同过滤是什么?

\n

协同过滤算法的核心就是「找相似」,它基于用户的历史行为(浏览、收藏、评论等),去发现用户对物品的喜好,并对喜好进行度量和打分,最终筛选出推荐集合。它又包括两个分支:

\n
    \n
  • 基于用户的协同过滤:User-CF,核心是找相似的人。比如下图中,用户 A 和用户 C 都购买过物品 a 和物品 b,那么可以认为 A 和 C 是相似的,因为他们共同喜欢的物品多。这样,就可以将用户 A 购买过的物品 d 推荐给用户 C。
  • \n
\n
\"img\"
img
\n

基于用户的协同过滤示例

\n
    \n
  • 基于物品的协同过滤:Item-CF,核心是找相似的物品。比如下图中,物品 a 和物品 b 同时被用户 A,B,C 购买了,那么物品 a 和物品 b 被认为是相似的,因为它们的共现次数很高。这样,如果用户 D 购买了物品 a,则可以将和物品 a 最相似的物品 b 推荐给用户 D。
  • \n
\n
\"img\"
img
\n

4.2、如何找相似?

\n

协同过滤的核心就是找相似,User-CF 是找用户之间的相似,Item-CF 是找物品之间的相似,那到底如何衡量两个用户或者物品之间的相似性呢?

\n

我们都知道,对于坐标中的两个点,如果它们之间的夹角越小,这两个点越相似。

\n

这就是初中学过的余弦距离,它的计算公式如下:

\n
\"img\"
img
\n

举个例子,A 坐标是(0,3,1),B 坐标是(4,3,0),那么这两个点的余弦距离是 0.569,余弦距离越接近 1,表示它们越相似。

\n
\"img\"
img
\n

除了余弦距离,衡量相似性的方法还有很多种,比如:欧式距离、皮尔逊相关系数、Jaccard 相似系数等等,这里不做展开,只是计算公式上的差异而已。

\n

4.3、Item-CF 的算法流程

\n

清楚了相似性的定义后,下面以 Item-CF 为例,详细说下这个算法到底是如何选出推荐物品的?

\n

4.3.1 、整理物品的共现矩阵

\n

假设有 A、B、C、D、E 5 个用户,其中用户 A 喜欢物品 a、b、c,用户 B 喜欢物品 a、b 等等。

\n
\"img\"
img
\n

所谓共现,即:两个物品被同一个用户喜欢了。比如物品 a 和 b,由于他们同时被用户 A、B、C 喜欢,所以 a 和 b 的共现次数是 3,采用这种统计方法就可以快速构建出共现矩阵。

\n

4.3.2、 计算物品的相似度矩阵

\n

对于 Item-CF 算法来说,一般不采用前面提到的余弦距离来衡量物品的相似度,而是采用下面的公式:

\n
\"img\"
img
\n

其中,N(u) 表示喜欢物品 u 的用户数,N(v) 表示喜欢物品 v 的用户数,两者的交集表示同时喜欢物品 u 和物品 v 的用户数。

\n

很显然,如果两个物品同时被很多人喜欢,那么这两个物品越相似。

\n

基于第 1 步计算出来的共现矩阵以及每个物品的喜欢人数,便可以构造出物品的相似度矩阵:

\n
\"img\"
img
\n

4.3.2、 推荐物品

\n

最后一步,便可以基于相似度矩阵推荐物品了,公式如下:

\n
\"img\"
img
\n

其中,Puj 表示用户 u 对物品 j 的感兴趣程度,值越大,越值得被推荐。

\n

N(u) 表示用户 u 感兴趣的物品集合,S(j,N) 表示和物品 j 最相似的前 N 个物品,Wij 表示物品 i 和物品 j 的相似度,Rui 表示用户 u 对物品 i 的兴趣度。

\n

上面的公式有点抽象,直接看例子更容易理解,假设我要给用户 E 推荐物品,前面我们已经知道用户 E 喜欢物品 b 和物品 c,喜欢程度假设分别为 0.6 和 0.4。

\n

那么,利用上面的公式计算出来的推荐结果如下:

\n
\"img\"
img
\n

因为物品 b 和物品 c 已经被用户 E 喜欢过了,所以不再重复推荐。最终对比用户 E 对物品 a 和物品 d 的感兴趣程度,因为 0.682 > 0.3,因此选择推荐物品 a。

\n

五、如何实现推荐系统

\n

5.1、选择数据集

\n

这里采用的是推荐领域非常经典的 MovieLens 数据集,它是一个关于电影评分的数据集,官网上提供了多个不同大小的版本,下面以 ml-1m 数据集(大约 100 万条用户评分记录)为例。

\n

下载解压后,文件夹中包含:ratings.dat、movies.dat、users.dat 3 个文件,共 6040 个用户,3900 部电影,1000209 条评分记录。各个文件的格式都是一样的,每行表示一条记录,字段之间采用 :: 进行分割。

\n

以 ratings.dat 为例,每一行包括 4 个属性:UserID, MovieID, Rating, Timestamp。

\n

通过脚本可以统计出不同评分的人数分布:

\n
\"img\"
img
\n

5.2、读取原始数据

\n

程序主要使用数据集中的 ratings.dat 这个文件,通过解析该文件,抽取出 user_id、movie_id、rating 3 个字段,最终构造出算法依赖的数据,并保存在变量 dataset 中,它的格式为:dict[user_id][movie_id] = rate

\n

5.3、构造物品的相似度矩阵

\n

基于第 2 步的 dataset,可以进一步统计出每部电影的评分次数以及电影的共生矩阵,然后再生成相似度矩阵。

\n

5.4、基于相似度矩阵推荐物品

\n

最后,可以基于相似度矩阵进行推荐了,输入一个用户 id,先针对该用户评分过的电影,依次选出 top 10 最相似的电影,然后加权求和后计算出每个候选电影的最终评分,最后再选择得分前 5 的电影进行推荐。

\n

5.5、调用推荐系统

\n

下面选择 UserId=1 这个用户,看下程序的执行结果。由于推荐程序输出的是 movieId 列表,为了更直观的了解推荐结果,这里转换成电影的标题进行输出。

\n

Java 代码示例

\n
import java.io.File;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Scanner;\n\npublic class CFRecommendation {\n    // 使用MovieLens数据集\n    private static final String RATINGS_FILE = \"ratings.csv\";\n    // 用户ID-电影ID-打分\n    private static Map<Integer, Map<Integer, Double>> ratings;\n\n    // 加载ratings.csv文件\n    private static void loadRatings() throws IOException {\n        File file = new File(RATINGS_FILE);\n        Scanner scanner = new Scanner(file);\n        ratings = new HashMap<>();\n        while (scanner.hasNextLine()) {\n            String line = scanner.nextLine();\n            String[] data = line.split(\",\");\n            int userId = Integer.parseInt(data[0]);\n            int movieId = Integer.parseInt(data[1]);\n            double rating = Double.parseDouble(data[2]);\n            // 用户-电影-打分\n            Map<Integer, Double> movieRatings = ratings.get(userId);\n            if (movieRatings == null) {\n                movieRatings = new HashMap<>();\n                ratings.put(userId, movieRatings);\n            }\n            movieRatings.put(movieId, rating);\n        }\n    }\n\n    // 计算两个用户的相似度\n    private static double calculateSimilarity(int user1, int user2) {\n        Map<Integer, Double> rating1 = ratings.get(user1);\n        Map<Integer, Double> rating2 = ratings.get(user2);\n        if (rating1 == null || rating2 == null) {\n            return 0;\n        }\n        double sum1 = 0;\n        double sum2 = 0;\n        double sumProduct = 0;\n        for (int movieId : rating1.keySet()) {\n            if (rating2.containsKey(movieId)) {\n                double rating1Value = rating1.get(movieId);\n                double rating2Value = rating2.get(movieId);\n                sum1 += rating1Value * rating1Value;\n                sum2 += rating2Value * rating2Value;\n                sumProduct += rating1Value * rating2Value;\n            }\n        }\n        return sumProduct / (Math.sqrt(sum1) * Math.sqrt(sum2));\n    }\n    \n    // 计算余弦相似度\n    public static double cosineSim(Map<String, Integer> user1, Map<String, Integer> user2){\n        double result = 0;\n        double denominator1 = 0;\n        double denominator2 = 0;\n        double numerator = 0;\n        for(String key : user1.keySet()){\n            numerator += user1.get(key) * user2.get(key);\n            denominator1 += Math.pow(user1.get(key), 2);\n            denominator2 += Math.pow(user2.get(key), 2);\n        }\n        result = numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2));\n        return result;\n    }\n\n    // 使用协同过滤算法获取用户的推荐列表\n    private static Map<Integer, Double> recommend(int userId) {\n        Map<Integer, Double> recommendList = new HashMap<>();\n        // 遍历所有用户\n        for (int otherUserId : ratings.keySet()) {\n            if (otherUserId != userId) {\n                double similarity = calculateSimilarity(userId, otherUserId);\n                Map<Integer, Double> otherRating = ratings.get(otherUserId);\n                // 遍历其他用户的评分,如果当前用户没有评分,则将其推荐给当前用户\n                for (int movieId : otherRating.keySet()) {\n                    if (!ratings.get(userId).containsKey(movieId)) {\n                        double recommendScore = otherRating.get(movieId) * similarity;\n                        recommendList.put(movieId, recommendScore);\n                    }\n                }\n            }\n        }\n        return recommendList;\n    }\n\n    public static void main(String[] args) throws IOException {\n        loadRatings();\n        // 测试用例:计算用户1与用户2的相似度\n        int user1 = 1;\n        int user2 = 2;\n        double similarity = calculateSimilarity(user1, user2);\n        System.out.println(\"用户1与用户2的相似度:\" + similarity);\n        // 测试用例:为用户1推荐电影\n        int userId = 1;\n        Map<Integer, Double> recommendList = recommend(userId);\n        System.out.println(\"为用户1推荐的电影:\");\n        for (int movieId : recommendList.keySet()) {\n            System.out.println(\"电影ID:\" + movieId + \",推荐分数:\" + recommendList.get(movieId));\n        }\n    }\n}
\n

六、问题与展望

\n

通过上面的介绍,大家对推荐系统的基本构成应该有了一个初步认识,但是真正运用到线上真实环境时,还会遇到很多算法和工程上的挑战,绝对不是几十行代码可以搞定的。

\n

问题:

\n
    \n
  1. \n

    上面的示例使用了标准化的数据集,而线上环境的数据是非标准化的,因此涉及到海量数据的收集、清洗和加工,最终构造出模型可使用的数据集。

    \n

    复杂且繁琐的特征工程,都说算法模型的上限由数据和特征决定。对于线上环境,需要从业务角度选择出可用的特征,然后对数据进行清洗、标准化、归一化、离散化,并通过实验效果进一步验证特征的有效性。

    \n

    算法复杂度如何降低?比如上面介绍的 Item-CF 算法,时间和空间复杂度都是 O(N×N),而线上环境的数据都是千万甚至上亿级别的,如果不做算法优化,可能几天都跑不出数据,或者内存中根本放不下如此大的矩阵数据。

    \n

    实时性如何满足?因为用户的兴趣随着他们最新的行为在实时变化的,如果模型只是基于历史数据进行推荐,可能结果不够精准。因此,如何满足实时性要求,以及对于新加入的物品或者用户该如何推荐,都是要解决的问题。

    \n

    算法效果和性能的权衡。从算法角度追求多样性和准确性,从工程角度追求性能,这两者之间必须找到一个平衡点。

    \n

    推荐系统的稳定性和效果追踪。需要有一套完善的数据监控和应用监控体系,同时有 ABTest 平台进行灰度实验,进行效果对比。

    \n
  2. \n
\n

展望:

\n

AI 时代,算法会更加复杂和完善,推荐的效果也会越来越好,特别是随着 OpenAI ChatGPT 横空出现,推荐系统最有条件和最适合 GPT 模型去结合使用,当然也会更加高效和智能。期待我们智能版推荐系统早日面世。

\n", + "image": "https://github-images.wenzhihuai.com/test/9cd60b43b0f590e54ff8fd2de18b16a2.png", + "date_published": "2024-09-01T13:54:44.000Z", + "date_modified": "2024-09-01T13:54:44.000Z", + "authors": [], + "tags": [] + }, + { + "title": "计算广告基本概念入门总结", + "url": "http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html", + "id": "http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html", + "summary": "计算广告基本概念入门总结 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门...", + "content_html": "\n

广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

\n

一、计算广告入门
\n1.1、什么是计算广告
\n首先要理解广告的概念,广告指的是由已确定的出资人通过各类媒介进行有关产品、观点和服务的、通常是有偿的、有组织的、综合的、劝服性的非人员信息传播活动。

\n

广告的主体包括了三类:

\n

广告主-有发布广告需求的用户
\n广告媒介-也就是承接广告主需求以及调配广告展示的广告平台
\n受众-通过媒介看到广告。
\n广告活动的两个主动参与方—出资人(sponsor) 和媒体(medium) 。 在数字广告这样更加复杂的市场结构中, 我们可以用一般性的术语来描述它们: 需求方(demand) 和供给方(supply) 。 这里的需求方可以是广告主 (advertiser) 、 代表广告主利益的代理商(agency) 或其他技术形态的采买方; 这里的供给方可以是媒体, 也可以是其他技术形态的变现平台。 另外, 要特别注意的是, 广告还有一个被动的参与方, 即受众 (audience) 。

\n

广告的根本目的是,通过广泛的媒体力量,使得广告能够高效、低成本地触达用户,以保持产品或服务的品牌形象或提高短期销量(也就是品牌广告和效果广告)。

\n

计算广告并没有严格的定义, 个人理解计算广告指的是通过大数据、推荐算法等技术对大量数据进行分析,以达到广告的精确受众定向,并且通过计算来调控广告的整个生命周期。计算广告相较于传统广告的特点是以技术和计算为导向,以数据为基准,具备标准化的、可衡量性的广告效果指标。

\n

计算广告的核心问题是如何为一系列用户与环境组合,以找到最合适的广告投放策略以优化整体的投入产出比(ROI)。

\n

1.2、广告与营销的区别
\n人们通常将广告理解为营销,但是两者是有一定的区别的。

\n

首先广告和营销的共同点都是为了提高产品或服务的销量、营业额及利润,但是区别在于,广告更希望通过低成本触达到用户,这里的用户指的是潜在用户,而营销更希望的是对某类精准用户进行服务,可以理解为广告的目标是增量用户,潜在用户,看着品牌价值和长期利润,而营销更看重目标用户和短期效果。

\n

1.3、在线广告的类型
\n条幅广告(Banner Ad): 这是显示广告中中最传统,也是最典型的形式。这种广告一般是嵌入在页面中相对固定位置的图片,与内容一样需要占据固定的版面。
\n文字链广告(Textual Ad): 这种广告的素材形式是一段链接到广告主落地页的文字,在搜索广告中为主流形式,同时在媒体广告中也被广泛采用。
\n富媒体广告(Rich Media Ad): 这类广告往往是利用视觉冲击力较强的表现形式,在不占用固定版面位置的情况下,向用户侵入式地投送广告素材。
\n富媒体广告常见的形式有弹窗、对 联、全屏等。它比较适合在高质量的媒体做一些品牌性质比较强的广告投放,但是对用户的使用体验往往影响也较大。
\n一些门户网站的首页有时会为某个品牌广告主提供专门定制的、交互形式很复杂的富媒体广告, 这样的广告一般不采用受众定向的投送方式,也主要强调创意的冲击力和交互形式的特色。
\n视频广告(Video Ad): 视频广告有两种最主要的形式:在视频内容播放之前的前插片广告,以及视频播放暂停时的广告。前插片广告一般采用短视频的形式,创意的冲击力和表现力要远远强于普通的显示广告,因此CPM价格往往也比较高;暂停广告则与普通的条幅广告区别不大。
\n社交广告或信息流广告(Social Ad): 社交广告中最典型的形式,是插入在社交网络信息流中的广告,这种方式最早见于Twitter,产品称为“Promoted Tweets”。社交广告希望达到的效果,是通过用户的扩散式传播获 得更大的影响力,以及更可信的口碑.
\n移动设备广告(Mobile Ad): 严格来说, 移动互联网上的广告与桌面电脑上的广告没有本质区别.
\n邮件营销广告(Email Direct Marketing, EDM): EDM是一种主动的广告形式,它不需要等到用户接触的机会产生时才被动地提供广告,而是可以随时向认为合适的用户发送推广信息。不过也正因为如此,EDM非常容易变垃圾邮件的主要来源。
\n1.4、在线广告发展历程

\n
\"在这里插入图片描述\"
在这里插入图片描述
\n

阶段一:传统合约广告模式,花钱买一段时间的广告位或展示次数
\n阶段二:定向及合约广告阶段,签约形式为合约广告,广告采用了特定用户定向的方式进行展示,提升转化率
\n阶段三:定向及竞价广告阶段,签约形式为竞价广告,广告采用了特定用户定向的方式进行展示,提升转化率
\n阶段四:实时竞价模式阶段(重要里程碑),采用广告交易平台(ADX)、需求方平台(DSP)及供给方平台(SSP)完成广告竞价,出现广义第二高价
\n阶段五:程序化交易,买卖双方无需在线交易,完全由程序进行交易的调控,无需人工参与
\n1.5、广告有效性模型

\n
\"在这里插入图片描述\"
在这里插入图片描述
\n

广告效果生成过程有三个大阶段:选择(Selection)、解释(Interpretation)与态度(Attitude);或者进一步分解为六个小阶段:曝光、关注、理解、接受、保持与决策,其中每两个小阶段对应一个大阶段。

\n

定性地说,越靠前的阶段,其效果的改善对点击率的贡献越大;而越靠后的阶段,其效果的改善对转化率的贡献越大。

\n

曝光(Exposure)阶段: 这一阶段指的是广告物理上展现出来的过程。此阶段的有效性往往与广告位的物理属性有关,并没有太多可以通过技术优化的空间。实际的广告实践中,曝光的有效性对最终结果的影响往往远远高于其他技术性因素。
\n关注(Attention)阶段:这一阶段指的是受众从物理上接触到广告到意识上注意到它的过程。那么如何使得关注阶段的效率提高呢?我们介绍几个重要的原则:
\n(1)尽量不要打断用户的任务。
\n(2)明确传达向用户推送此广告的原因,这一点是受众定向广告创意优化的重要方向。
\n(3)内容符合用户的兴趣或需求,这是受众定向的原理基础。
\n理解(Comprehension)阶段:受众意识到了广告的存在,并不意味着他一定能够理解广告传达的信息。
\n(1)广告内容要在用户能理解的具体兴趣范围内,这就说明了真正精准的受众定向有多么必要。
\n(2)要注意设定与关注程度相匹配的理解门槛。
\n接受(Acceptance)阶段:受众理解了广告传达的信息,并不一定表示他认可这些信息。
\n(1)广告的上下文环境对于广告的接受程度也有着很大的影响, 同一个品牌广告出现在某游戏社区上和门户网站首页上,用户会倾向于认为后者更具说服力,这也就是优质媒体的品牌价值。
\n(2)在定向广告越来越普遍的今天,如何让合适的广告出现在合适的媒体上,即广告安全(Ad Safety)的问题,正在引起大家越来越多的关注。
\n保持(Retention)阶段:对于不仅仅追求短期转化的广告商,当然希望广告传达的信息给用户留下长久的记忆,以影响他长时间的选择。
\n决策(Decision)阶段:成功广告的最终作用是带来用户的转化行为,虽然这一阶段已经离开了广告的业务范围,但好的广告还是能够为转化率的提高做好铺垫。
\n二、计算广告的核心概念
\n品牌广告(Brand Awareness)
\n长期投放,用于建立品牌形象、建立品牌理念,而不诉求直接销售量的广告

\n

直接效果广告/效果广告(Direct Response)
\n短期投放,为了提升产品销售量或提高知名度而投放的广告,因此通常按效果计费(CPA或CPS)

\n

展示广告(display advertising)
\n以图形展示为核心的广告形式,如常见的banner、视频广告等,通常以CPM、CPT为计费方式

\n

位置拍卖(position auction)
\n假设有一组广告位可使用,则将广告位按价值排序,并将广告主出价排序,并依次对应投放,价高者将得到更好的广告位

\n

CPT(Cost Per Time)
\n按展示时长计费;可以充分发挥橱窗效应,但无法利用受众定向技术,效率较低;使用于高曝光的品牌广告

\n

CPM(Cost Per Mille)
\n即“千人成本”,按访问量(每千人的访问量)计费,是目前最主流的结算方法,具备受众定向能力,适用于实时竞价广告

\n

eCPM(千人收益)
\ne指的是expect,也就是预期最优的的千人访问收益。

\n

CPC(Cost Per Click)
\n按点击次数计费

\n

CPA(Cost Per Action)
\n按用户行为、实际投放效果计费,如用户的注册、购买、安装行为,供给方运营难度较大,而需求方无任何风险,适合于效果广告

\n

CPS(Cost Per Sell) ROI(Return On Investment)
\n同CPA,都是按实际效果计费的方式,ROI即常说的“投资回报率”

\n

点击率(Click Through Rate,CTR)
\n即广告点击与广告展示的比率

\n

到达率 (Reach Rate)
\n落地页打开次数与点击次数比例,落地页(landing page)即点击后所跳转的页面

\n

转化率CVR (Conversion Rate)
\n转化次数与到达次数的比例

\n

在线广告交易模式
\n合约广告、竞价广告、实时竞价、程序化广告

\n

广告交易的相关机构与平台(程序化广告)
\n广告网络 ADN(ad net-work):连接供给方与需求方,负责批量运营媒体广告位,并对流量进行售卖的平台
\n需求方平台DSP(demand side platform):为广告主提供跨媒介、跨平台、跨终端的的广告投放平台,通过数据整合、分析实现基于受众的精准投放
\n供给方平台 SSP(supply side platform):是为媒体提供服务的平台,负责管理媒体广告位库存、优化广告位的售卖,提高其广告资源价值,帮助其提升收益
\n广告交易平台 ADX(ad Exchange):帮助DSP与SSP通过RTB方式进行广告交易的平台
\n数据管理平台 DMP(data management platform):负责统合、加工第三方用户数据,并售卖给DSP的数据平台

\n
\"在这里插入图片描述\"
在这里插入图片描述
\n

定向广告(targeted advertising)
\n不再把广告投给所有人,而是面向不同的受众,赋予其不同的用户标签,并投放不同的广告。由此,广告主从广告位的采买,变成了面向受众人群的采买

\n

流量预测
\n预估该媒体中会产生的流量总数,便于进行售前指导/出价指导,如果低估了平台流量,可能导致售卖不足,利润无法最大化;如果高估平台流量,可能导致超售,无法满足合约

\n

流量塑形(traffic shaping)
\n通过主动影响流量来实现合约达成,综合性门户网站是典型例子,网站通过优化导流与跳转页布局,为用户点击链接提供便利,实现最大的到达率

\n

在线分配
\n即通过设计分配策略,实现高效的流量分配。展示合约都面临的问题:在流量/广告位有限,而满足合约的人群大量重叠的情况下,要实现利益最大化,如何让各个合约到最大限度满足

\n

广义第二高价理论:Generalized Second Price, GSP
\n指在只有一个位置的拍卖中,出价最高的广告主赢得广告位后,只需要支付其下一位广告主的出价(排名第二的广告主),而不用按自己的出价来购买,这是一种最合理的竞价逻辑

\n

搜索广告(search ad)
\n通过用户的搜索关键字作为特征召回广告内容

\n

上下文广告(contextual advertising)
\n根据浏览页面的关键词进行投放,而不是用户输入的关键词

\n

原生广告&信息流广告(Native advertising)
\n广告内容与媒介信息形式高度吻合的广告,都可以称为原生广告,“广告即内容、内容即广告”

\n", + "image": "https://github-images.wenzhihuai.com/test/70353957ecb1660b1ca67b4c8aea852b.png", + "date_published": "2024-09-01T13:54:44.000Z", + "date_modified": "2024-09-01T13:54:44.000Z", + "authors": [], + "tags": [] + }, + { + "title": "程序员副业探索之电商", + "url": "http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html", + "id": "http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html", + "summary": "程序员副业探索之电商 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。 在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展...", + "content_html": "\n

在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

\n

在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展趋势。结合自身的电商背景与资源,我倾向于专注于小程序电商,但对拼多多店铺和跨境电商也有兴趣。

\n

探索小程序电商的过程中,我结识了许多朋友,也认识了几位从事电商运营的同事。最初,我将重心放在小程序女装项目上,计划从深圳的优质供应商中精选货品,打造一个集商品展示、购买、支付、私域于一体的完整小程序平台。然而,女装行业的高退货率远超预期,几乎每售出十件商品,就有七件被退货,这让项目难以持续。

\n

因此,我开始考虑其他方向,如化妆品。化妆品作为日常消费品需求稳定且退货率较低,因而具有较高的利润。接下来,我将分享个人在化妆品电商领域的探索。

\n

一、小程序化妆品

\n

关于小程序,我们其实考虑过很多种不同的方向。比如,我们曾设想过做一个垂直类的平台,主要销售化妆品或者女装。我们也有其他的想法,比如销售珠宝等产品。但是,经过实际流程的运行后,我们发现小程序在很多方面存在诸多限制。下面,我将简单地介绍一下,如何创建一个小程序。

\n

1.1 小程序准备(营业执照&微信支付&小程序appId)

\n

首先,如果你想开发一个小程序,必须要具备微信支付的功能。而微信支付是个人无法申请的,因此需要一个营业执照。首先,你需要申请一个个人营业执照。

\n

取得营业执照后,接下来就是用这个营业执照来申请开通微信支付。

\n
\"QQ_1722269773245\"
QQ_1722269773245
\n

接下来,你需要注册一个小程序。在去年,可以通过一些服务商或代理商注册,每个小程序的注册费用仅需几毛钱。但现在这条途径已被限制,目前注册一个小程序大约需要300元。此外,为了上线小程序,还需要通过审核,审核费用同样大约为300元。

\n

同时,你还需要准备域名和服务器资源。我们选择了一台配置为8G内存的服务器,因为我们的成交量并不大,这样的配置已经足够应付日常需求。

\n

1.2 小程序开发

\n

在小程序开发方面,由于我们是想完全独立开发,因此首先我们从 GitHub 上查找了一个电商小程序的开源项目。尽管我们查看了很多项目,但基本上没有直接能用的,都需要进行小改造。我们对其进行了改进,接入了微信支付和腾讯的 COS 对象存储。

\n

接下来,我们搭建了自己的 MySQL 数据库。考虑到我们的小程序交易量较小,因此没有使用分库分表的复杂结构,单独一个 MySQL 数据库已经足够。如果未来交易量大幅增加,可以考虑使用其他厂商的分库分表方案,例如 TiDB 等。

\n

开发的框架网上的都类似一样,分为下面的几个模块,整体上感觉代码也不复杂(CRUD),就是比较耗时间。

\n
    \n
  • 会员系统:用户注册、登录、会员等级、积分管理等。
  • \n
  • 商品管理:商品的添加、编辑、删除、库存管理、分类管理等。
  • \n
  • 订单管理:订单生成、支付、取消、退换货、订单状态管理等。
  • \n
  • 支付系统:支持多种支付方式,如微信支付、支付宝支付、银联支付等。
  • \n
  • 营销工具:优惠券、拼团、秒杀、砍价、满减活动等。
  • \n
  • 物流管理:快递公司管理、物流信息追踪等。
  • \n
  • 数据分析:提供销售数据统计、用户数据分析、商品销售分析等报表功能。
  • \n
  • 客服系统:在线客服、留言板、消息通知等。
  • \n
  • 权限管理:用户权限角色管理,不同角色有不同的操作权限。
  • \n
\n
\"强化私域流量管理\"
强化私域流量管理
\n

网上那些代理商有赞、微盟之类的,可以0元搞个小程序,但是如果要用到营销功能(秒杀、优惠券等),都得给几千、几万块。底成本创业,就自己找开元弄一下了,前后端都会点。在成本方面,由于个人对化妆品并不熟悉,也分不清面霜、补水之类的,因此花费了约500元请别人帮忙上传商品信息。效果如下,也可去微信小程序上搜索一下体验下(不要付款)。

\n
\"QQ_1722270680440\"
QQ_1722270680440
\n

下面是小程序主要的一些功能:

\n

域名配置:不能通过ip使用,需要申请好域名+https才可。

\n

微信支付:如上一步所说,营业执照后即可把小程序对接微信支付

\n

微信物流(发货信息管理):以前网上可集成快递100的功能,现在需要收费了,好在微信官方提供了自己的微信物流,可以直接对接,免费,填写完发货信息之后微信还可以直接通知用户。

\n

客服:这个很重要,需要直接对接普通用户,回答一些咨询和售后内容。

\n

微信认证:这个微信强求,没办法,300块。确保商品、图片、文字等内容的合法性,避免侵权行为。遵守相关电商法律法规,例如消费者权益保护、退换货政策等。

\n

尽管小程序已经上线,整体功能虽然齐全,但效果不如预期。

\n

我们原本设想,当用户下单后可以从华南、北地区的供货商那里进行发货。然而,整体计算下来,由于缺乏足够的曝光,导致小程序的商品没有得到应有的推广。在微信平台,小程序的商品不会自动得到推广曝光,需要开发者自行进行推广。

\n

我们在开发初期计划通过小程序结合私域流量进行引流,主要策略包括:建立小程序,通过社群引流和推广,吸引粉丝,再通过促销和秒杀等营销活动来提高订单量。然而,第一步的推广就遇到了困难。整个五月份几乎没有用户点击和访问。

\n

最初立下的方向较为宏大,计划通过私域流量提升销售。然而,由于对化妆品行业了解不够深,加之市场上存在假货问题,也是在无法更多的推广。

\n
\"QQ_1722270494718\"
QQ_1722270494718
\n

算一下总投入吧:

\n

营业执照(180)+微信支付认证(50)+小程序appId(2)+服务器(100/月)+请人录入化妆品(500),最最最重要的是,营业执照还需要年审,查税什么的,如果不用还得花钱注销。

\n
\"QQ_1722359862581\"
QQ_1722359862581
\n

二、拼多多电商

\n

在打造自己的私域生态时,我发现仅依靠小程序并不理想,因此开始考虑其他电商平台,比如小红书和拼多多。

\n

在小红书上,阿里巴巴的 1688 平台不太支持直接批量上传商品,所有商品和图片都需要手动录入和上传,非常不方便。至于选择拼多多还是淘宝,我个人更倾向于拼多多,因为其用户相对较多。

\n

在选择供应商时,发现 1688 上的化妆品要么不存在,要么是一些小众品牌,无法放心销售。因此,我决定选择一个垂直方向的产品,最终专注于女性相关的饰品,比如手链和宠物项链。

\n

我在 1688 上找到了一些手链店铺,并支付了 1000 元的保证金,随后通过 1688 的分销中心将产品直接分销到拼多多。然而,拼多多的定价策略需要自行调整,否则利润非常低,甚至可能亏损。

\n

在开始销售时,由于没注意到新疆和西藏不包邮,导致一起订单亏损了十多元。总体来说,虽然通过广告推广获取了一些流量和曝光,并且实现了约 10 个订单,销售额为 200多 元,但最终有一半订单被退货,只盈利了大约 100 元。

\n

由于退货地址填写的是我自己这边,所有退货商品都被寄回给我。检查后发现,这些产品的质量确实不佳。由于麻烦,我决定不再将退货商品寄回 1688 的商家,干脆自己留着。

\n
\"clone\"
clone
\n

成本:成交金额270,退单一半左右,自己承担,没退给1688商家,大概亏损130左右。

\n

三、跨境电商

\n

在腾讯工作期间,我听说有些人通过跨境电商取得了显著的成功,比如将电子烟卖到欧美国家,甚至在一年内赚到了一套深圳的房子。当然,这可能有些夸大。然而,国内电商市场竞争异常激烈,利润微薄,特别是拼多多对工厂的压榨,使得供应商难以生存。广州奥园的事件也是一个例子,显示了供应商在国内激烈竞争中的艰难处境。

\n

我也考虑过跨境电商的可能性,比如建立独立站或在亚马逊上运营。目前,这些想法还在考察阶段,需要进一步了解和评估。

\n
\"QQ_1722358300059\"
QQ_1722358300059
\n

偶然听别人说:中产做生意返贫之路,就是奶茶店、跨境电商!

\n

四、总结

\n

小程序+拼多多店,应该亏损1000左右吧,就当做交学费,总的来说,也积累了不少经验。对整体的电商流程,包括开设门店和私域营销,都会有一定了解。但坦率地说,电商市场竞争非常激烈,利润空间被压缩得很厉害。

\n

首先,以拼多多为例,它直接连接了工厂和用户(B2C),消除了很多中间环节。如果你想在这一过程中作为中间商进行营销推广,就必须具备独特的优势。现在一些平台,比如直播带货,采用的就是大批量采购的方式。这种方式确实能降低成本,并且通过批量采购获利。

\n

另一方面,很多用户并不了解像阿里巴巴这样的货源平台,还有些用户甚至还在使用淘宝。利用这种信息差,还是可以赚取一定的利润,但这也让电商行业变得更加竞争激烈。

\n

在电商领域的下一步发展方向上,我个人认为直播带货可能是一个更具潜力的选择。结合种草文化和私域流量管理,这种方式可能相对来说能带来更多的收益。作为中间商,目前似乎也只有这条路可以探索。可惜不是人人都能成为李佳琦或者董宇辉。

\n", + "image": "https://github-images.wenzhihuai.com/test/QQ_1722269773245.png", + "date_published": "2024-08-04T14:51:49.000Z", + "date_modified": "2024-08-04T14:51:49.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2024-07-24", + "url": "http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html", + "id": "http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html", + "summary": "2024-07-24 新买了台air m3,午夜色的,心心念念了好久 IMG_20240723_235443 读写速度 c6c117ff1f5f6371b3ab682621369c1f 和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧 096e891a0278263d0c790e74f32ef911_720 非果粉,但好多苹果设备 0742...", + "content_html": "\n

新买了台air m3,午夜色的,心心念念了好久

\n\"IMG_20240723_235443\"\n

读写速度

\n\"c6c117ff1f5f6371b3ab682621369c1f\"\n

和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧

\n\"096e891a0278263d0c790e74f32ef911_720\"\n

非果粉,但好多苹果设备

\n\"0742e82ab0ba499dea8b267f958cdada\"\n", + "date_published": "2024-07-24T15:52:12.000Z", + "date_modified": "2024-07-24T15:52:12.000Z", + "authors": [], + "tags": [] + }, + { + "title": "股票预测", + "url": "http://www.wenzhihuai.com/stock/", + "id": "http://www.wenzhihuai.com/stock/", + "summary": "股票预测", + "content_html": "\n\n\n\n", + "date_published": "2024-07-02T14:33:09.000Z", + "date_modified": "2024-07-02T14:33:09.000Z", + "authors": [], + "tags": [] + }, + { + "title": "赛力斯", + "url": "http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html", + "id": "http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html", + "summary": "赛力斯 update", + "content_html": "\n

update

\n", + "date_published": "2024-07-02T14:33:09.000Z", + "date_modified": "2024-07-02T14:33:09.000Z", + "authors": [], + "tags": [] + }, + { + "title": "好玩的", + "url": "http://www.wenzhihuai.com/interesting/", + "id": "http://www.wenzhihuai.com/interesting/", + "summary": "好玩的", + "content_html": "\n", + "date_published": "2024-05-18T19:54:10.000Z", + "date_modified": "2024-11-29T18:45:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "基于OLAP做业务监控", + "url": "http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html", + "id": "http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html", + "summary": "基于OLAP做业务监控 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 interval 和 for 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。 一、两者的...", + "content_html": "\n

在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

\n

一、两者的比较

\n

Prometheus 的特点
\nPrometheus 是一种开源的监控和警报工具包,最初由 SoundCloud 开发并开源。它具有以下特点:
\n(1)多维度数据模型
\nPrometheus 采用标签(label)系统,允许用户通过灵活的标签组合来区分和查询监控数据。
\n(2)Pull 模式采集
\nPrometheus 使用 Pull 模式从各种目标(如应用程序、数据库、操作系统等)拉取数据,这与其他使用 Push 模式的监控系统有所不同。
\n(3)警报和告警管理
\nPrometheus 内置了一个强大的警报管理系统,可以根据预定条件触发警报,并集成到多种通知平台(如邮件、Slack 等)。
\n(4)生态系统和可扩展性
\nPrometheus 拥有活跃的社区和广泛的插件支持,可以轻松与其他系统集成,并根据需要进行扩展。

\n

OLAP 数据库

\n

OLAP(Online Analytical Processing)数据库在监控数据存储和分析中具有独特的优势,尤其在数据持久性和实时查询方面。

\n

(1)数据持久性和长期存储
\nOLAP 数据库通常具有可靠的数据持久性和长期存储能力,能存储大量历史数据,并支持快速的时间范围查询和数据回溯分析。
\n(2)实时数据加载和查询
\n现代 OLAP 数据库尽管通常不支持实时数据加载,但一些具有较低的延迟和高吞吐量,可以支持实时查询和分析需求。

\n

二、为什么要用OLAP数据库来做监控

\n
\"1\"
1
\n
\"2\"
2
\n
\"3\"
3
\n", + "image": "https://github-images.wenzhihuai.com/images/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_486b6ebb-0d04-45d2-84b5-e7f157e50ae1.png", + "date_published": "2024-05-13T16:26:06.000Z", + "date_modified": "2024-11-10T15:55:36.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Jetbrains Idea设置", + "url": "http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html", + "id": "http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html", + "summary": "Jetbrains Idea设置 一、类注释 想要在生成类的时候,自动带上author和时间,效果如下: 放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他: image-20240512002500812image-20240512002500812 二、方法注释 想让自动给方法生成注释,以及参数的默认...", + "content_html": "\n

一、类注释

\n

想要在生成类的时候,自动带上author和时间,效果如下:

\n
/**\n * @author zhihuaiwen\n * @since 2024/5/12 00:22\n */\npublic class EFE {\n}
\n

放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他:

\n
\"image-20240512002500812\"
image-20240512002500812
\n

二、方法注释

\n

想让自动给方法生成注释,以及参数的默认注释:

\n
  /**\n   * 获取首页商品列表\n   *\n   * @param type             类型 【1 精品推荐 2 热门榜单 3首发新品 4促销单品】\n   * @param pageParamRequest 分页参数\n   * @return List\n   */\n  @Override\n  public CommonPage<IndexProductResponse> findIndexProductList(Integer type, PageParamRequest pageParamRequest) {
\n

去settings里面找到Live Templates,新建一个Template Group叫做user,然后再新建一个Live Template。

\n
\"image-20240512001636936\"
image-20240512001636936
\n

Abbreviation填*,Description随便填,然后再Template text填下:

\n
*\n$VAR1$\n * @return $returns$\n */
\n

编辑变量,Edit Variables:

\n
\"image-20240512001711420.png\"
image-20240512001711420.png
\n

VAR1:

\n
groovyScript(\"def result=''; def params=\\\"${_1}\\\".replaceAll('[\\\\\\\\[|\\\\\\\\]|\\\\\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+=' * @param ' + params[i] + ' ' + params[i] + ((i < params.size() - 1) ? '\\\\n' : '')}; return result\", methodParameters())
\n

returns:

\n
methodReturnType()
\n

三、关闭Annotate With Git Blame

\n

Idea这个太烦了,总是不小心误点,想把右侧的两个都给去掉,网上查了有些比较旧,无法解决,下面特别声明下:

\n
\"image-20240512001050312\"
image-20240512001050312
\n

位置:settings直接搜Inlay Hints,把右侧的Usages、Code author给取消勾选即可

\n
\"image-20240512001010797\"
image-20240512001010797
\n

上面的设置放到百度网盘里了,可以下载后导入到Idea

\n

链接: https://pan.baidu.com/s/1-3uhY1jssyMWyRbvBLQ2FQ?pwd=epw2 提取码: epw2 复制这段内容后打开百度网盘手机App,操作更方便哦

\n\"image-20240512115716124\"\n", + "image": "https://github-images.wenzhihuai.com/images/image-20240512002500812.png", + "date_published": "2024-05-11T16:37:27.000Z", + "date_modified": "2024-05-12T03:58:39.000Z", + "authors": [], + "tags": [] + }, + { + "title": "小程序反编译", + "url": "http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html", + "id": "http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html", + "summary": "小程序反编译 第一步:电脑端提取 先找到小程序保存的地址,一般先找到微信的文件管理下 image-20240310171919281 然后到该目录的C:\\Users\\w1570\\Documents\\WeChat Files\\Applet即可看到相关小程序了。 image-20240310173802224image-20240310173802224 ...", + "content_html": "\n

第一步:电脑端提取

\n

先找到小程序保存的地址,一般先找到微信的文件管理下

\n\"image-20240310171919281\"\n

然后到该目录的C:\\Users\\w1570\\Documents\\WeChat Files\\Applet即可看到相关小程序了。

\n
\"image-20240310173802224\"
image-20240310173802224
\n

找到需要解压的包__APP__.wxapkg,拷贝到和pc_wxapkg_decrypt.exe的统一路径下。

\n

第二步:解密wxapkg包

\n\n
pc_wxapkg_decrypt.exe -wxid 微信小程序id -in 要解密的wxapkg路径 -out 解密后的路径\n//示例如下\npc_wxapkg_decrypt.exe -wxid wx7444167f2a6427b3 -in __APP__.wxapkg
\n

第三步:解包

\n\n
切换到./nodejs目录下,解压`node_modules.zip`后,使用cmd命令打开\n输入下面命令\n\nnode wuWxapkg.js ..\\decrypt\\dec.wxapkg\n\n第二个参数为操作的项目,这里操作的是666.wxapkg 记得改为自己的
\n

发现偶尔还是有些html没有正常解析,有点奇怪

\n", + "image": "https://github-images.wenzhihuai.com/images/image-20240310173802224.png", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "开发工具整理", + "url": "http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html", + "id": "http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html", + "summary": "开发工具整理 ShadowsocksX mac端:https://github.com/shadowsocks/ShadowsocksX-NG win端:https://github.com/shadowsocks/shadowsocks-windows 安卓:https://github.com/shadowsocks/shadowsocks-and...", + "content_html": "\n

ShadowsocksX

\n

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
\nwin端:https://github.com/shadowsocks/shadowsocks-windows
\n安卓:https://github.com/shadowsocks/shadowsocks-android

\n

IDEA激活

\n

许可证激活(License server)
\nhttp://vip.52shizhan.cn
\nhttp://fls.itxe.net

\n

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "高可用", + "url": "http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html", + "id": "http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html", + "summary": "高可用 4个9(99.99) 流量控制 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,...", + "content_html": "\n

4个9(99.99)

\n

| | Sentinel | Hystrix(维护状态) | Resilience4j(Spring推荐) |
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/sentinel-flow-overview-20240218114454907.jpg", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "高并发", + "url": "http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html", + "id": "http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html", + "summary": "高并发 QPS 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。 响应时间:系统对请求做出...", + "content_html": "\n

QPS

\n

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

\n
    \n
  • 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
  • \n
  • 吞吐量:单位时间内处理的请求数量。
  • \n
  • QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • \n
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
  • \n
\n

负载均衡

\n

正所谓双拳难敌四手,高并发撑场面的首选方案就是集群化部署,一台服务器承载的QPS有限,多台服务器叠加效果就不一样了。

\n

如何将流量转发到服务器集群,这里面就要用到负载均衡,比如:LVS 和 Nginx。

\n

常用的负载算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接数法等

\n

业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。

\n

注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx

\n

池化技术

\n

复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,有时间开销浪费。池化技术的核心是资源的“预分配”和“循环使用”,常用的池化技术有线程池、进程池、对象池、内存池、连接池、协程池。

\n

连接池的几个重要参数:最小连接数、空闲连接数、最大连接数

\n

Linux 内核中是以进程为单元来调度资源的,线程也是轻量级进程。所以说,进程、线程都是由内核来创建并调度。协程是由应用程序创建出来的任务执行单元,比如 Go 语言中的协程“goroutine”。协程本身是运行在线程上,由应用程序自己调度,它是比线程更轻量的执行单元。

\n

在 Go 语言中,一个协程初始内存空间是 2KB(Linux 下线程栈大小默认是 8MB),相比线程和进程来说要小很多。协程的创建和销毁完全是在用户态执行的,不涉及用户态和内核态的切换。另外,协程完全由应用程序在用户态下调用,不涉及内核态的上下文切换。协程切换时由于不需要处理线程状态,需要保存的上下文也很少,速度很快。

\n

Go语言中协程池的实现方法有两种:抢占式和调度式。

\n

抢占式协程池,所有任务存放到一个共享的 channel 中,多个协程同时去消费 channel 中的任务,存在锁竞争。
\n调度式协程池,每个协程都有自己的 channel,每个协程只消费自己的 channel。下发任务的时候,采用负载均衡算法选择合适的协程来执行任务。比如选择排队中任务最少的协程,或者简单轮询。

\n

流量漏斗

\n

上面讲的是正向方式提升系统QPS,我们也可以逆向思维,做减法,拦截非法请求,将核心能力留给正常业务!

\n

互联网高并发流量并不都是纯净的,也有很多恶意流量(比如黑客攻击、恶意爬虫、黄牛、秒杀器等),我们需要设计流量拦截器,将那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。

\n

拦截器分层:

\n

网关和 WAF(Web Application Firewall,Web 应用防火墙)
\n采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法

\n

风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用。
\n下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "高性能", + "url": "http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html", + "id": "http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html", + "summary": "高性能 响应时间 性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。 那么有哪些因素会影响系统的性能呢? 用户网络环境 请求/响应的数据包大小 业务系统 CPU、内存、磁盘等性能 业务链路的长度 下游系统的性能 算法实现是否高效 当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。 1、高性能缓存 对一些热点...", + "content_html": "\n

响应时间

\n

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

\n

那么有哪些因素会影响系统的性能呢?

\n

用户网络环境
\n请求/响应的数据包大小
\n业务系统 CPU、内存、磁盘等性能
\n业务链路的长度
\n下游系统的性能
\n算法实现是否高效
\n当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。

\n

1、高性能缓存

\n

对一些热点数据每次都从 DB 中读取,会给 DB 带来较大的压力,导致性能大幅下降。所以,我们需要用缓存来提升热点数据的访问性能,比如将活动信息数据在浏览器的缓存中保存一段时间。

\n

缓存根据性能由高到低分为:寄存器、L1缓存、L2缓存、L3缓存、本地内存、分布式缓存

\n

上层的寄存器、L1 缓存、L2 缓存是位于 CPU 核内的高速缓存,访问延迟通常在 10 纳秒以下。L3 缓存是位于 CPU 核外部但在芯片内部的共享高速缓存,访问延迟通常在十纳秒左右。高速缓存具有成本高、容量小的特点,容量最大的 L3 缓存通常也只有几十MB。

\n

本地内存是计算机内的主存储器,相比 CPU 芯片内部的高速缓存,内存的成本要低很多,容量通常是 GB 级别,访问延迟通常在几十到几百纳秒。

\n

内存和高速缓存都属于掉电易失的存储器,如果机器断电了,这类存储器中的数据就丢失了。

\n

特别说明:在使用缓存时,要注意缓存穿透、缓存雪崩、缓存热点问题、缓存数据一致性问题。当然为了提升整体性能通常会采用多级缓存组合方案(浏览器缓存+服务端本地内存缓存+服务端网络内存缓存)

\n

2、日志优化,避免IO瓶颈

\n

当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”。

\n

在IO中断过程中,如果此时有其他任务线程可调度,系统会直接调度其他线程,这样 CPU 就相应显示为 Usr 或 Sys;但是如果此时系统较空闲,无其他任务可以调度,CPU 就会显示为 iowait(实际上与 idle 无本质区别)。

\n

磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住。

\n

Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除。

\n

这种批量化、顺序写,大大提升了磁盘的吞吐性能!

\n

缓存

\n

异步

\n

I/O(网络、数据、文件)

\n

分库分表

\n

参考

\n

1.怎么优化java项目性能

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Feed系统设计", + "url": "http://www.wenzhihuai.com/system-design/feed.html", + "id": "http://www.wenzhihuai.com/system-design/feed.html", + "summary": "Feed系统设计 https://blog.csdn.net/weixin_45583158/article/details/128195940", + "content_html": "\n

https://blog.csdn.net/weixin_45583158/article/details/128195940

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "行锁,表锁,意向锁", + "url": "http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html", + "id": "http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html", + "summary": "行锁,表锁,意向锁", + "content_html": "\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "一致性hash算法和哈希槽", + "url": "http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html", + "id": "http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html", + "summary": "一致性hash算法和哈希槽", + "content_html": "\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "JVM(java虚拟机)", + "url": "http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html", + "id": "http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html", + "summary": "一、了解JVM 1、什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码...", + "content_html": "

一、了解JVM

\n

1、什么是JVM

\n

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

\n
\"微信图片_20220522232432.png\"
微信图片_20220522232432.png
\n

2、JRE/JDK/JVM是什么关系

\n

JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
\nJDK(Java Development Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
\nJVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。

\n

3、JVM体系结构:

\n

类加载器:加载class文件;
\n运行时数据区:包括方法区、堆、Java栈、程序计数器、本地方法栈
\n执行引擎:执行字节码或者执行本地方法

\n
\"img\"
img
\n

二、运行时数据区

\n

方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。从 JDK 8 开始,HotSpot 虚拟机移除了方法区,取而代之的是元空间(Metaspace)。元空间并不在 Java 虚拟机内存中,而是使用了本地(即操作系统)的内存。这个改变主要是为了解决方法区可能出现的内存溢出问题。
\n:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。
\n:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
\n程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
\n本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。

\n

元空间

\n

三、类加载机制

\n

类加载器通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。

\n

1、类的生命周期(7个)

\n

加载、验证、准备、解析、初始化、使用、卸载

\n

2、类加载的五个过程

\n

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

\n

加载:类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
\n验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
\n准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
\n解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
\n初始化:前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

\n

3、类加载器

\n

启动类加载器(BootStrap ClassLoader):主要负责加载jre/lib/rt.jar相关的字节码文件的。
\n扩展类加载器(Extension Class Loader):主要负载加载 jre/lib/ext/*.jar 这些jar包的。
\n应用程序类加载器(Application Class Loader):主要负责加载用户自定义的类以及classpath环境变量所配置的jar包的。
\n自定义类加载器(User Class Loader):负责加载程序员指定的特殊目录下的字节码文件的。大多数情况下,自定义类加载器只需要继承ClassLoader这个抽象类,重写findClass()和loadClass()两个方法即可。

\n

4、类加载机制(双亲委派)

\n

类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。

\n
\"img\"
img
\n

工作过程:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。

\n

四、垃圾回收

\n

程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。 Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

\n

1、判断对象已死

\n

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

\n

引用计数法:给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;

\n
\"img\"
img
\n
    注意:如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的            两块内存依然保持着互相引用无法回收。引用计数法很难解决循环引用问题; \n
\n

可达性分析:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

\n
\"img\"
img
\n
    可作为 GC Roots 的对象:\n    1)虚拟机栈中引用的对象\n    2)方法区中类静态属性引用的对象\n    3)方法区中常量引用的对象\n    4)本地方法栈中native方法引用的对象 \n
\n

引用:下面四种引用强度依次减弱
\n强引用:默认情况下,对象采用的均为强引用;
\n软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
\n弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
\n虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

\n

2、垃圾收集算法

\n

标记清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
\n复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
\n标记整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
\n分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记清除算法。

\n

3、垃圾收集器

\n

Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
\nParnew收集器:Serial收集器多线程版本。
\nParallel Scavenge收集器:使用复制算法的新生代收集器。
\nSerial Old收集器:使用标记-整理算法的老年代单线程收集器。
\nParallel Old收集器:使用标记-整理算法的老年代多线程收集器。
\nCMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
\nG1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。
\nG1收集器有以下特点
\n1)并行与并发:无需停顿Java线程来执行GC动作。
\n2)分代收集:可独立管理整个GC堆。
\n3)空间整合:运行期间不会产生内存空间碎片。
\n4)可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

\n

4、JVM内存分代机制

\n

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。

\n

新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
\n老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
\n永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
\n元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。
\nGC 过程:新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区 没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于 Eden区和Survivor From区,Survivor To区是空的(作为保留区域)。GC进行时,Eden区中 所有存活的对象都会被复制到Survivor To区,而在Survivor From区中,仍存活的对象会根据 它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾 回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有 达到阀值的对象会被复制到Survivor To区。接着清空Eden区和Survivor From区,新生代中存 活的对象都在Survivor To区。接着,Survivor From区和Survivor To区会交换它们的角色,也 就是新的Survivor To区就是上次GC清空的Survivor From区,新的Survivor From区就是上次 GC的Survivor To区,总之,不管怎样都会保证Survivor To区在一轮GC后是空的。GC时当 Survivor To区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进 行分配担保,将这些对象存放在老年代中。

\n

5、Minor GC、Major GC、Full GC之间的区别

\n

Minor GC:Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
\nMajor GC:指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
\nFull GC:Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

\n

6、Minor GC、Major GC、Full GC触发条件

\n

Minor GC触发条件
\n虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总 空间
\n1)如果大于的话,直接执行minorGC
\n2)如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
\n3)如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历 次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC
\n4)如果大于的话,执行minorGC

\n

Full GC触发条件
\n1)老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存 在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是 不要创建太大的对象。
\n2)方法区空间不足:系统当中需要加载的类,调用的方法很多,同时方法区当中没有足够的 空间,就出触发一次Full GC
\n3)老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
\n4)调用System.gc()时(系统建议执行Full GC,但是不必然执行)

\n", + "image": "https://github-images.wenzhihuai.com/images/31584f04c69f4fdaa922c5bd1517cc97.png", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "JAVA NIO", + "url": "http://www.wenzhihuai.com/java/io/nio.html", + "id": "http://www.wenzhihuai.com/java/io/nio.html", + "summary": "JAVA NIO 一、简介 1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路...", + "content_html": "\n

一、简介

\n

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

\n

2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

\n

3)Java AIO:异步非阻塞,在AIO模型中,当我们发起一个I/O操作(如读或写)时,我们不需要等待它完成,我们的代码会立即返回,可以继续执行其他任务。当I/O操作完成时,我们之前注册的回调函数会被自动调用,我们可以在这个回调函数中处理I/O操作的结果。

\n

1.1典型的多路复用IO实现

\n

目前流程的多路复用IO实现主要包括四种: selectpollepollkqueue。下表是他们的一些重要特性的比较:

\n

| IO模型 | 相对性能 | 数据结构 | 关键思路 | 操作系统 | JAVA支持情况 |
\n|

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Spring Cloud Sentinel", + "url": "http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html", + "id": "http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html", + "summary": "Spring Cloud Sentinel 参考 1.超详细springcloud sentinel教程~", + "content_html": "\n

参考

\n

1.超详细springcloud sentinel教程~

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "入门使用", + "url": "http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html", + "id": "http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html", + "summary": "入门使用 参考 1.Sentinel入门(干货版)", + "content_html": "\n

参考

\n

1.Sentinel入门(干货版)

\n", + "date_published": "2024-03-10T14:39:30.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "基础", + "url": "http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html", + "id": "http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html", + "summary": "基础 一、ElasticSearch基础 1、什么是Elasticsearch: Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。 全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并...", + "content_html": "\n

一、ElasticSearch基础

\n

1、什么是Elasticsearch:
\nElasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
\n全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

\n

2、Elasticsearch 的基本概念:

\n

(1)index 索引:索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。

\n

(2)type 类型:类型是用来定义数据结构,可以认为是 mysql 中的一张表,type 是 index 中的一个逻辑数据分类

\n

(3)document 文档:类似于 MySQL 中的一行,不同之处在于 ES 中的每个文档可以有不同的字段,但是对于通用字段应该具有相同的数据类型,文档是es中的最小数据单元,可以认为一个文档就是一条记录。

\n

(4)Field 字段:Field是Elasticsearch的最小单位,一个document里面有多个field

\n

(5)shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

\n

(6)replica 副本:任何服务器随时可能故障或宕机,此时 shard 可能会丢失,通过创建 replica 副本,可以在 shard 故障时提供备用服务,保证数据不丢失,另外 replica 还可以提升搜索操作的吞吐量。

\n

shard 分片数量在建立索引时设置,设置后不能修改,默认5个;replica 副本数量默认1个,可随时修改数量;

\n

(7)segment:每个shard分片是一个lucene实例,每个分片由多个segment组成!!每个segment占用内存,文件句柄等。segment 合并的过程,需要先读取小的 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,合并大的 segment 需要消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。所以 Elasticsearch 在默认情况下会对合并线程进行资源限制,确保它不会对搜索性能造成太大影响。

\n
\"image-20240224213112456\"
image-20240224213112456
\n

3、什么是倒排索引:
\n在搜索引擎中,每个文档都有对应的文档 ID,文档内容可以表示为一系列关键词的集合,例如,某个文档经过分词,提取了 20 个关键词,而通过倒排索引,可以记录每个关键词在文档中出现的次数和出现位置。也就是说,倒排索引是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。

\n

要注意倒排索引的两个细节:

\n

倒排索引中的所有词项对应一个或多个文档
\n倒排索引中的词项 根据字典顺序升序排列

\n
\"img\"
img
\n

4、doc_values 的作用:
\n倒排索引虽然可以提高搜索性能,但也存在缺陷,比如我们需要对数据做排序或聚合等操作时,lucene 会提取所有出现在文档集合的排序字段,然后构建一个排好序的文档集合,而这个步骤是基于内存的,如果排序数据量巨大的话,容易造成内存溢出和性能缓慢。
\ndoc_values 就是 es 在构建倒排索引的同时,会对开启 doc_values 的字段构建一个有序的 “document文档 ==> field value” 的列式存储映射,可以看作是以文档维度,实现了根据指定字段进行排序和聚合的功能,降低对内存的依赖。另外 doc_values 保存在操作系统的磁盘中,当 doc_values 大于节点的可用内存,ES可以从操作系统页缓存中加载或弹出,从而避免发生内存溢出的异常,但如果 docValues 远小于节点的可用内存,操作系统就自然将所有 doc_values 存于内存中(堆外内存),有助于快速访问。

\n

5、text 和 keyword类型的区别:
\n两个类型的区别主要是分词:keyword 类型是不会分词的,直接根据字符串内容建立倒排索引,所以keyword类型的字段只能通过精确值搜索到;Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引

\n

6、query 和 filter 的区别?

\n

(1)query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;

\n

(2)filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。

\n

二、ES的写入流程

\n

1、ES写数据的整体流程:

\n
\"image-20240223165512655\"
image-20240223165512655
\n

(1)客户端选择 ES 的某个 node 发送请求过去,这个 node 就是协调节点 coordinating node
\n(2)coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)
\n(3)实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node
\n(4)coordinating node 等到 primary node 和所有 replica node 都执行成功之后,最后返回响应结果给客户端。
\n2、ES主分片写数据的详细流程:

\n
\"Elasticsearch索引文档的过程\"
Elasticsearch索引文档的过程
\n

(1)主分片先将数据写入ES的 memory buffer,然后定时(默认1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入操作系统缓存 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 segment 文件实际上是一些倒排索引的集合, 只有经历了 refresh 操作之后,这些数据才能变成可检索的。

\n

ES 的近实时性:数据存在 memory buffer 时是搜索不到的,只有数据被 refresh 到 Filesystem cache 之后才能被搜索到,而 refresh 是每秒一次, 所以称 es 是近实时的;可以手动调用 es 的 api 触发一次 refresh 操作,让数据马上可以被搜索到;

\n

(2)由于 memory Buffer 和 Filesystem Cache 都是基于内存,假设服务器宕机,那么数据就会丢失,所以 ES 通过 translog 日志文件来保证数据的可靠性,在数据写入 memory buffer 的同时,将数据也写入 translog 日志文件中,当机器宕机重启时,es 会自动读取 translog 日志文件中的数据,恢复到 memory buffer 和 Filesystem cache 中去。

\n

ES 数据丢失的问题:translog 也是先写入 Filesystem cache,然后默认每隔 5 秒刷一次到磁盘中,所以默认情况下,可能有 5 秒的数据会仅仅停留在 memory buffer 或者 translog 文件的 Filesystem cache中,而不在磁盘上,如果此时机器宕机,会丢失 5 秒钟的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

\n

(3)flush 操作:不断重复上面的步骤,translog 会变得越来越大,不过 translog 文件默认每30分钟或者 阈值超过 512M 时,就会触发 commit 操作,即 flush操作,将 memory buffer 中所有的数据写入新的 segment 文件中, 并将内存中所有的 segment 文件全部落盘,最后清空 translog 事务日志。

\n

① 将 memory buffer 中的数据 refresh 到 Filesystem Cache 中去,清空 buffer;
\n② 创建一个新的 commit point(提交点),同时强行将 Filesystem Cache 中目前所有的数据都 fsync 到磁盘文件中;
\n③ 删除旧的 translog 日志文件并创建一个新的 translog 日志文件,此时 commit 操作完成
\n更多 ES 的数据写入流程的说明欢迎阅读这篇文章:ElasticSearch搜索引擎:数据的写入流程

\n

三、ES的更新和删除流程

\n

​ 删除和更新都是写操作,但是由于 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;所以 ES 利用 .del 文件 标记文档是否被删除,磁盘上的每个段都有一个相应的.del 文件

\n

(1)如果是删除操作,文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉。

\n

(2)如果是更新操作,就是将旧的 doc 标识为 deleted 状态,然后创建一个新的 doc。

\n

memory buffer 每 refresh 一次,就会产生一个 segment 文件 ,所以默认情况下是 1s 生成一个 segment 文件,这样下来 segment 文件会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment 文件合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,不写入到新的 segment 中,然后将新的 segment 文件写入磁盘,这里会写一个 commit point ,标识所有新的 segment 文件,然后打开 segment 文件供搜索使用,同时删除旧的 segment 文件

\n

有关segment段合并过程,欢迎阅读这篇文章:Elasticsearch搜索引擎:ES的segment段合并原理

\n

四、ES的搜索流程

\n

搜索被执行成一个两阶段过程,即 Query Then Fetch:

\n

1、Query阶段:

\n

客户端发送请求到 coordinate node,协调节点将搜索请求广播到所有的 primary shard 或 replica,每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。接着每个分片返回各自优先队列中 所有 docId 和 打分值 给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

\n

2、Fetch阶段:
\n协调节点根据 Query阶段产生的结果,去各个节点上查询 docId 实际的 document 内容,最后由协调节点返回结果给客户端。

\n

coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
\n接收请求的 node 返回 document 给 coordinate node 。
\ncoordinate node 返回 document 给客户端。
\nQuery Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。

\n

五、ES在高并发下如何保证读写一致性?

\n

(1)对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖

\n

每个文档都有一个_version 版本号,这个版本号在文档被改变时加一。Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,它会被简单的忽略。

\n

利用_version的这一优点确保数据不会因为修改冲突而丢失,比如指定文档的version来做更改,如果那个版本号不是现在的,我们的请求就失败了。

\n

(2)对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。

\n

one:写操作只要有一个primary shard是active活跃可用的,就可以执行
\nall:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
\nquorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
\n(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。

\n

六、ES集群如何选举Master节点

\n

1、Elasticsearch 的分布式原理:

\n

Elasticsearch 会对存储的数据进行切分,划分到不同的分片上,同时每一个分片会生成多个副本,从而保证分布式环境的高可用。ES集群中的节点是对等的,节点间会选出集群的 Master,由 Master 会负责维护集群状态信息,并同步给其他节点。

\n

Elasticsearch 的性能会不会很低:不会,ES只有建立 index 和 type 时需要经过 Master,而数据的写入有一个简单的 Routing 规则,可以路由到集群中的任意节点,所以数据写入压力是分散在整个集群的。

\n

2、ES集群 如何 选举 Master:

\n

Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;

\n

(1)确认候选主节点的最少投票通过数量(elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes)
\n(2)选举时,集群中每个节点对所有 master候选节点(node.master: true)根据 nodeId 进行字典排序,然后选出第一个节点(第0位),暂且认为它是master节点。
\n(3)如果对某个节点的投票数达到阈值,并且该节点自己也选举自己,那这个节点就是master;否则重新选举一直到满足上述条件。
\n补充:master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。

\n

3、Elasticsearch是如何避免脑裂现象:

\n

(1)当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;

\n

(2)当集群 master 候选节点 只有两个时,这种情况是不合理的,最好把另外一个node.master改成false。如果我们不改节点设置,还是套上面的(N/2)+1公式,此时discovery.zen.minimum_master_nodes应该设置为2。这就出现一个问题,两个master备选节点,只要有一个挂,就选不出master了

\n

七、建立索引阶段性能提升方法

\n

(1)如果是大批量导入,可以设置 index.number_of_replicas: 0 关闭副本,等数据导入完成之后再开启副本
\n(2)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
\n(3)如果搜索结果不需要近实时性,可以把每个索引的 index.refresh_interval 改到30s
\n(4)增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB
\n(5)使用 SSD 存储介质
\n(6)段和合并:Elasticsearch 默认值是 20 MB/s。但如果用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。

\n

八、ES的深度分页与滚动搜索scroll

\n

(1)深度分页:

\n

深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深。

\n

(2)滚动搜索:
\n一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。

\n

参考

\n

1.ElasticSearch搜索引擎
\n2.2023最新整理的 Elasticsearch 21道面试题
\n3.elasticsearch中文官网
\n4.elasticsearch 倒排索引原理

\n", + "image": "https://github-images.wenzhihuai.com/images/image-20240224213112456.png", + "date_published": "2024-02-24T15:02:58.000Z", + "date_modified": "2024-02-24T15:02:58.000Z", + "authors": [], + "tags": [] + }, + { + "title": "一些不常用但很实用的命令", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html", + "summary": "一些不常用但很实用的命令 https连接耗时检测 iptables拒绝请求", + "content_html": "\n

https连接耗时检测

\n
curl -w \"TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\\n\" -so /dev/null https://zhls.qq.com/test-nginx
\n

iptables拒绝请求

\n
iptables -A  OUTPUT -d 11.145.18.159 -j REJECT
\n
", + "date_published": "2024-02-24T05:31:47.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Jenkins的一些笔记", + "url": "http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html", + "id": "http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html", + "summary": "Jenkins的一些笔记 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。 一、在全局安全配置中 1.1 启用安全 如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直...", + "content_html": "\n

公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

\n

一、在全局安全配置中

\n

1.1 启用安全

\n

如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直没有勾选安全域,然后授权策略为登录用户可以做任何事,之后权限这一块就彻底崩溃了,重装了又重装,才知道,需要勾选安全域。

\n
\n
\"\"
\n
\n

1.2 跨域

\n

同时开启跨站请求伪造保护,Jenkins的一些API需要用到的。

\n
\n
\"\"
\n
\n

二、获取TOKEN

\n

2.1 TOKEN

\n

Jenkins的用户token可以在用户的设置下面获得,但是这种方式如果需要重装Jenkins的话,就得重新修改一次配置文件

\n
\n
\"\"
\n
\n

经过对Jenkins-client的抓包分析,token可以由username+\":\"+password,然后进行base64加密组成,之后在token前面加上\"Basic \"即可,代码如下:

\n
\n
\"\"
\n
\n

三、获取Jenkins-Crumb

\n

在远程API调用的时候,Jenkins对于某些接口的要求不仅限于Authorization,还必须要有Jenkins-Crumb,这个东西之前在进行获取的时候,有时候会变来变去,比如用curl命令和f12查看的时候发现不一致,实在受不了,感觉毫无规律可言,之后才发现上面的Authorization来直接调用接口获取的才是正确的,再然后想想,可能是之前调用api的时候,没有开启启用安全,再或者是有没有勾选上使用碎片算法。

\n
\n
\"\"
\n
\n

另,附上curl查询Jenkins-Crumb的命令:

\n
curl -s 'http://admin:yourtoken@jenkins-url/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)'
\n

替换掉yourtoken和jenkins-url即可。

\n

四、值得注意的事

\n

4.1 API设计

\n

Jenkins的API设计可谓是独领风骚,能把一个提交设计成这样真实佩服测试之后才发现只要提交个表单,key为json,value为值即可,其他的都不需要,这个设计我也不知道怎么来的,感觉超级坑。

\n
\n
\"\"
\n
\n

4.2 生成构建job

\n

由于我们是将Jenkins集成在我们自己的平台里面,并不暴露Jenkins给用户,所以,创建一个job的时候,必须由我们平台的参数往Jenkins里面提交,这一提交,发现的问题不少。
\n一是Jenkins的整个job的提交是由两步组成的,先是创建job,再提交配置。即:/createItem?name=xxx接口。
\n二是提交的配置参数,提交的是整个xml,而不是由一个一个参数组成的。对于java来说,就得使用xstream或者其他来转化,甚是折腾,如图这种转化。

\n
\n
\"\"
\n
\n

4.3 构建的队列

\n

在点击立即构建的时候,Jenkins是没有返回任何信息,但是在Jenkins的内部,它是通过放到队列里等待的,如果有空闲,就开始构建,否则等待,这个队列是可以获取得到的,我们从里面可以获取上一次构建的信息,是成功还是失败。这种情况下,假设我们多个人同时点击,这下子就有点慌了,如何获取到具体某个人的构建结果,有点虐心。想了半天,最终得出的事:代码相同,意味着每次构建的结果相同,为什么要允许多个人同时点击?就这么解决了:从一个job的构建队列中获取最后一次构建的信息,如果是正在构建,那么不允许构建了,直到构建结果出来。

\n

4.4 构建进度的查看

\n

需要将Jenkins中的构建进度移植到我们自有的平台,Jenkins的构建进度时通过ajax轮询实现的,获取文本的规则主要从response header里面的两个字段获取
\n(1)X-More-Data:是否有更多的数据
\n(2)X-Text-Size:从开始到该次调用的文本大小
\n我们是通过websocket来将文本内容推送到前端,使用的stomp协议,部分代码如下:

\n
        while (true) {\n            ...\n            String string = response.body().string();\n            String header = response.header(\"X-More-Data\");\n            if (!Strings.isNullOrEmpty(header) || start == 0) {\n                template.convertAndSend(\"/topic/\" + uuid, string);\n                String textSize = response.header(\"X-Text-Size\");\n                if (!Strings.isNullOrEmpty(textSize)) {\n                    start = Integer.parseInt(textSize);\n                }\n                TimeUnit.SECONDS.sleep(5);\n            } else {\n                template.convertAndSend(\"/topic/\" + uuid, string);\n                return;\n            }\n        }
\n

参考:
\n1.通过jenkins API去build一个job
\n2.Jenkins Remote API

\n", + "image": "https://github-images.wenzhihuai.com/images/20181027023132911846860.png", + "date_published": "2024-02-24T05:12:56.000Z", + "date_modified": "2024-02-24T05:12:56.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Kubernetes容器日志收集", + "url": "http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html", + "id": "http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html", + "summary": "Kubernetes容器日志收集 日志采集方式 日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。 1.原生方式 使用 kubectl ...", + "content_html": "\n

日志采集方式

\n

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

\n

1.原生方式 使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。
\n2.DaemonSet方式 在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。
\n3.Sidecar方式 一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。
\n三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。

\n

一、原生方式

\n

\"\"
\n简单的说,原生方式就是直接使用kubectl logs来查看日志,或者将docker的日志通过日志驱动来打到syslog、journal等去,然后再通过命令来排查,这种方式最好的优势就是简单、资源占用率低等,但是,在多容器、弹性伸缩情况下,日志的排查会十分困难,仅仅适用于刚开始研究Kubernetes的公司吧。不过,原生方式确实其他两种方式的基础,因为它的两种最基础的理念,daemonset和sidecar模式都是基于这两种方式而来的。

\n

1.1 控制台stdout方式

\n

这种方式是daemonset方式的基础。将日志全部输出到控制台,然后docker开启journal,然后就能在/var/log/journal下面看到二进制的journal日志,如果要查看二进制的日志的话,可以使用journalctl来查看日志:journalctl -u docker.service -n 1 --no-pager -o json -o json-pretty

\n
{\n        \"__CURSOR\" : \"s=113d7df2f5ff4d0985b08222b365c27a;i=1a5744e3;b=05e0fdf6d1814557939e52c0ac7ea76c;m=5cffae4cd4;t=58a452ca82da8;x=29bef852bcd70ae2\",\n        \"__REALTIME_TIMESTAMP\" : \"1559404590149032\",\n        \"__MONOTONIC_TIMESTAMP\" : \"399426604244\",\n        \"_BOOT_ID\" : \"05e0fdf6d1814557939e52c0ac7ea76c\",\n        \"PRIORITY\" : \"6\",\n        \"CONTAINER_ID_FULL\" : \"f2108df841b1f72684713998c976db72665f353a3b4ea17cd06b5fc5f0b8ae27\",\n        \"CONTAINER_NAME\" : \"k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1\",\n        \"CONTAINER_TAG\" : \"f2108df841b1\",\n        \"CONTAINER_ID\" : \"f2108df841b1\",\n        \"_TRANSPORT\" : \"journal\",\n        \"_PID\" : \"6418\",\n        \"_UID\" : \"0\",\n        \"_GID\" : \"0\",\n        \"_COMM\" : \"dockerd-current\",\n        \"_EXE\" : \"/usr/bin/dockerd-current\",\n        \"_CMDLINE\" : \"/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry hub.gcloud.lab --insecure-registry 172.30.0.0/16 --log-level=warn --signature-verification=false --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450\",\n        \"_CAP_EFFECTIVE\" : \"1fffffffff\",\n        \"_SYSTEMD_CGROUP\" : \"/system.slice/docker.service\",\n        \"_SYSTEMD_UNIT\" : \"docker.service\",\n        \"_SYSTEMD_SLICE\" : \"system.slice\",\n        \"_MACHINE_ID\" : \"225adcce13bd233a56ab481df7413e0b\",\n        \"_HOSTNAME\" : \"dev4.gcloud.set\",\n        \"MESSAGE\" : \"I0601 23:56:30.148153       1 event.go:221] Event(v1.ObjectReference{Kind:\\\"DaemonSet\\\", Namespace:\\\"openshift-monitoring\\\", Name:\\\"node-exporter\\\", UID:\\\"f6d2bdc1-6658-11e9-aca2-fa163e938959\\\", APIVersion:\\\"apps/v1\\\", ResourceVersion:\\\"15378688\\\", FieldPath:\\\"\\\"}): type: 'Normal' reason: 'SuccessfulCreate' Created pod: node-exporter-hvrpf\",\n        \"_SOURCE_REALTIME_TIMESTAMP\" : \"1559404590148488\"\n}
\n

在上面的json中,_CMDLINE以及其他字段占用量比较大,而且这些没有什么意义,会导致一条简短的日志却被封装成多了几十倍的量,所以的在日志量特别大的情况下,最好进行一下字段的定制,能够减少就减少。
\n我们一般需要的字段是CONTAINER_NAME以及MESSAGE,通过CONTAINER_NAME可以获取到Kubernetes的namespace和podName,比如CONTAINER_NAME为k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1的时候
\ncontainer name in pod: controllers
\n**pod name: **master-controllers-dev4.gcloud.set
\nnamespace: kube-system
\n**pod uid: **dcab37be702c9ab6c2b17122c867c74a_1

\n

1.2 新版本的subPathExpr

\n

journal方式算是比较标准的方式,如果采用hostPath方式,能够直接将日志输出这里。这种方式唯一的缺点就是在旧Kubernetes中无法获取到podName,但是最新版的Kubernetes1.14的一些特性subPathExpr,就是可以将目录挂载的时候同时将podName写进目录里,但是这个特性仍旧是alpha版本,谨慎使用。
\n简单说下实现原理:容器中填写的日志目录,挂载到宿主机的/data/logs/namespace/service_name/$(PodName)/xxx.log里面,如果是sidecar模式,则将改目录挂载到sidecar的收集目录里面进行推送。如果是宿主机安装fluentd模式,则需要匹配编写代码实现识别namespace、service_name、PodName等,然后发送到日志系统。

\n

可参考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md
\n日志落盘参考细节:

\n
    env:\n    - name: POD_NAME\n      valueFrom:\n        fieldRef:\n          apiVersion: v1\n          fieldPath: metadata.name\n   ...\n    volumeMounts:\n    - name: workdir1\n      mountPath: /logs\n      subPathExpr: $(POD_NAME)
\n

我们主要使用了在Pod里的主容器挂载了一个fluent-agent的收集器,来将日志进行收集,其中我们修改了Kubernetes-Client的源码使之支持subPathExpr,然后发送到日志系统的kafka。这种方式能够处理多种日志的收集,比如业务方的日志打到控制台了,但是jvm的日志不能同时打到控制台,否则会发生错乱,所以,如果能够将业务日志挂载到宿主机上,同时将一些其他的日志比如jvm的日志挂载到容器上,就可以使用该种方式。

\n
{\n    \"_fileName\":\"/data/work/logs/epaas_2019-05-22-0.log\",\n    \"_sortedId\":\"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea\",\n    \"_collectTime\":\"2019-05-22 17:23:58\",\n    \"_log\":\"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev\",\n    \"_domain\":\"rongqiyun-dev\",\n    \"_podName\":\"aofjweojo-5679849765-gncbf\",\n    \"_hostName\":\"dev4.gcloud.set\"\n}
\n

二、Daemonset方式

\n
\"\"
\n

daemonset方式也是基于journal,日志使用journal的log-driver,变成二进制的日志,然后在每个node节点上部署一个日志收集的agent,挂载/var/log/journal的日志进行解析,然后发送到kafka或者es,如果节点或者日志量比较大的话,对es的压力实在太大,所以,我们选择将日志推送到kafka。容器日志收集普遍使用fluentd,资源要求较少,性能高,是目前最成熟的日志收集方案,可惜是使用了ruby来写的,普通人根本没时间去话时间学习这个然后进行定制,好在openshift中提供了origin-aggregated-logging方案。
\n我们可以通过fluent.conf来看origin-aggregated-logging做了哪些工作,把注释,空白的一些东西去掉,然后我稍微根据自己的情况修改了下,结果如下:

\n
@include configs.d/openshift/system.conf\n设置fluent的日志级别\n@include configs.d/openshift/input-pre-*.conf\n最主要的地方,读取journal的日志\n@include configs.d/dynamic/input-syslog-*.conf\n读取syslog,即操作日志\n<label @INGRESS>\n  @include configs.d/openshift/filter-retag-journal.conf\n  进行匹配\n  @include configs.d/openshift/filter-k8s-meta.conf\n  获取Kubernetes的相关信息  \n  @include configs.d/openshift/filter-viaq-data-model.conf\n  进行模型的定义\n  @include configs.d/openshift/filter-post-*.conf\n  生成es的索引id\n  @include configs.d/openshift/filter-k8s-record-transform.conf\n  修改日志记录,我们在这里进行了字段的定制,移除了不需要的字段\n  @include configs.d/openshift/output-applications.conf\n  输出,默认是es,如果想使用其他的比如kafka,需要自己定制\n</label>
\n

当然,细节上并没有那么好理解,换成一步步理解如下:

\n

1. 解析journal日志
\norigin-aggregated-logging会将二进制的journal日志中的CONTAINER_NAME进行解析,根据匹配规则将字段进行拆解

\n
    \"kubernetes\": {\n      \"container_name\": \"fas-dataservice-dev-new\",\n      \"namespace_name\": \"fas-cost-dev\",\n      \"pod_name\": \"fas-dataservice-dev-new-5c48d7c967-kb79l\",\n      \"pod_id\": \"4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c\",\n      \"namespace_id\": \"f95238a6-3a67-11e9-a211-20040fe7b690\"\n    }
\n

2. es封装
\n主要用的是elasticsearch_genid_ext插件,写在了filter-post-genid.conf上。

\n

3. 日志分类
\n通过origin-aggregated-logging来收集journal的日志,然后推送至es,origin-aggregated-logging在推送过程中做了不少优化,即适应高ops的、带有等待队列的、推送重试等,详情可以具体查看一下。

\n

还有就是对日志进行了分类,分为三种:
\n(1).操作日志(在es中以.operations匹配的),记录了对Kubernetes的操作
\n(2).项目日志(在es中以project
匹配的),业务日志,日志收集中最重要的
\n(3).孤儿日志(在es中以.orphaned.*匹配的),没有namespace的日志都会打到这里

\n

4. 日志字段定制
\n经过origin-aggregated-logging推送至后采集的一条日志如下:

\n
{\n    \"CONTAINER_TAG\": \"4ad125bb7558\",\n    \"docker\": {\n      \"container_id\": \"4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c\"\n    },\n    \"kubernetes\": {\n      \"container_name\": \"fas-dataservice-dev-new\",\n      \"namespace_name\": \"fas-cost-dev\",\n      \"pod_name\": \"fas-dataservice-dev-new-5c48d7c967-kb79l\",\n      \"pod_id\": \"4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c\",\n      \"namespace_id\": \"f95238a6-3a67-11e9-a211-20040fe7b690\"\n    },\n    \"systemd\": {\n      \"t\": {\n        \"BOOT_ID\": \"6246327d7ea441339d6d14b44498b177\",\n        \"CAP_EFFECTIVE\": \"1fffffffff\",\n        \"CMDLINE\": \"/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry 10.77.0.0/16 --log-level=warn --signature-verification=false --bridge=none --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450\",\n        \"COMM\": \"dockerd-current\",\n        \"EXE\": \"/usr/bin/dockerd-current\",\n        \"GID\": \"0\",\n        \"MACHINE_ID\": \"0096083eb4204215a24efd202176f3ec\",\n        \"PID\": \"17181\",\n        \"SYSTEMD_CGROUP\": \"/system.slice/docker.service\",\n        \"SYSTEMD_SLICE\": \"system.slice\",\n        \"SYSTEMD_UNIT\": \"docker.service\",\n        \"TRANSPORT\": \"journal\",\n        \"UID\": \"0\"\n      }\n    },\n    \"level\": \"info\",\n    \"message\": \"\\tat com.sun.proxy.$Proxy242.execute(Unknown Source)\",\n    \"hostname\": \"host11.rqy.kx\",\n    \"pipeline_metadata\": {\n      \"collector\": {\n        \"ipaddr4\": \"10.76.232.16\",\n        \"ipaddr6\": \"fe80::a813:abff:fe66:3b0c\",\n        \"inputname\": \"fluent-plugin-systemd\",\n        \"name\": \"fluentd\",\n        \"received_at\": \"2019-05-15T09:22:39.297151+00:00\",\n        \"version\": \"0.12.43 1.6.0\"\n      }\n    },\n    \"@timestamp\": \"2019-05-06T01:41:01.960000+00:00\",\n    \"viaq_msg_id\": \"NjllNmI1ZWQtZGUyMi00NDdkLWEyNzEtMTY3MDQ0ZjEyZjZh\"\n  }
\n

可以看出,跟原生的journal日志类似,增加了几个字段为了写进es中而已,总体而言,其他字段并没有那么重要,所以我们对其中的字段进行了定制,以减少日志的大小,定制化字段之后,一段日志的输出变为(不是同一段,只是举个例子):

\n
{\n    \"hostname\":\"dev18.gcloud.set\",\n    \"@timestamp\":\"2019-05-17T04:22:33.139608+00:00\",\n    \"pod_name\":\"istio-pilot-8588fcb99f-rqtkd\",\n    \"appName\":\"discovery\",\n    \"container_name\":\"epaas-discovery\",\n    \"domain\":\"istio-system\",\n    \"sortedId\":\"NjA3ODVhODMtZDMyYy00ZWMyLWE4NjktZjcwZDMwMjNkYjQ3\",\n    \"log\":\"spiffluster.local/ns/istio-system/sa/istio-galley-service-account\"\n}
\n

5.部署
\n最后,在node节点上添加logging-infra-fluentd: \"true\"的标签,就可以在namespace为openshift-logging中看到节点的收集器了。

\n
logging-fluentd-29p8z              1/1       Running   0          6d\nlogging-fluentd-bpkjt              1/1       Running   0          6d\nlogging-fluentd-br9z5              1/1       Running   0          6d\nlogging-fluentd-dkb24              1/1       Running   1          5d\nlogging-fluentd-lbvbw              1/1       Running   0          6d\nlogging-fluentd-nxmk9              1/1       Running   1          5d
\n

6.关于ip
\n业务方不仅仅想要podName,同时还有对ip的需求,控制台方式正常上是没有记录ip的,所以这算是一个难点中的难点,我们在kubernetes_metadata_common.rb的kubernetes_metadata中添加了 'pod_ip' => pod_object['status']['podIP'],最终是有些有ip,有些没有ip,这个问题我们继续排查。

\n

三、Sidecar模式

\n

\"\"
\n这种方式的好处是能够获取日志的文件名、容器的ip地址等,并且配置性比较高,能够很好的进行一系列定制化的操作,比如使用log-pilot或者filebeat或者其他的收集器,还能定制一些特定的字段,比如文件名、ip地址等。
\nsidecar模式用来解决日志收集的问题的话,需要将日志目录挂载到宿主机的目录上,然后再mount到收集agent的目录里面,以达到文件共享的目的,默认情况下,使用emptydir来实现文件共享的目的,这里简单介绍下emptyDir的作用。
\nEmptyDir类型的volume创建于pod被调度到某个宿主机上的时候,而同一个pod内的容器都能读写EmptyDir中的同一个文件。一旦这个pod离开了这个宿主机,EmptyDir中的数据就会被永久删除。所以目前EmptyDir类型的volume主要用作临时空间,比如Web服务器写日志或者tmp文件需要的临时目录。
\n日志如果丢失的话,会对业务造成的影响不可估量,所以,我们使用了尚未成熟的subPathExpr来实现,即挂载到宿主的固定目录/data/logs下,然后是namespace,deploymentName,podName,再然后是日志文件,合成一块便是/data/logs/${namespace}/${deploymentName}/${podName}/xxx.log。
\n具体的做法就不在演示了,这里只贴一下yaml文件。

\n
apiVersion: extensions/v1beta1\nkind: Deployment\nmetadata:\n  name: xxxx\n  namespace: element-dev\nspec:\n  template:\n    spec:\n      volumes:\n        - name: host-log-path-0\n          hostPath:\n            path: /data/logs/element-dev/xxxx\n            type: DirectoryOrCreate\n      containers:\n        - name: xxxx\n          image: 'xxxxxxx'\n          volumeMounts:\n            - name: host-log-path-0\n              mountPath: /data/work/logs/\n              subPathExpr: $(POD_NAME)\n        - name: xxxx-elog-agent\n          image: 'agent'\n          volumeMounts:\n            - name: host-log-path-0\n              mountPath: /data/work/logs/\n              subPathExpr: $(POD_NAME)
\n

fluent.conf的配置文件由于保密关系就不贴了,收集后的一条数据如下:

\n
{\n    \"_fileName\":\"/data/work/logs/xxx_2019-05-22-0.log\",\n    \"_sortedId\":\"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea\",\n    \"_collectTime\":\"2019-05-22 17:23:58\",\n    \"_log\":\"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev\",\n    \"_domain\":\"namespace\",\n    \"_ip\":\"10.128.93.31\",\n    \"_podName\":\"xxxx-5679849765-gncbf\",\n    \"_hostName\":\"dev4.gcloud.set\"\n}
\n

四、总结

\n

总的来说,daemonset方式比较简单,而且适合更加适合微服务化,当然,不是完美的,比如业务方想把业务日志打到控制台上,但是同时也想知道jvm的日志,这种情况下或许sidecar模式更好。但是sidecar也有不完美的地方,每个pod里都要存在一个日志收集的agent实在是太消耗资源了,而且很多问题也难以解决,比如:主容器挂了,agent还没收集完,就把它给kill掉,这个时候日志怎么处理,业务会不会受到要杀掉才能启动新的这一短暂过程的影响等。所以,我们实际使用中首选daemonset方式,但是提供了sidecar模式让用户选择。

\n

参考:
\n1.Kubernetes日志官方文档
\n2.Kubernetes日志采集Sidecar模式介绍
\n3.Docker日志收集最佳实践

\n", + "image": "https://upyuncdn.wenzhihuai.com/201906020557591917511053.png", + "date_published": "2024-02-24T05:12:56.000Z", + "date_modified": "2024-02-24T05:35:53.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Kubernetes之request 和 limit详解", + "url": "http://www.wenzhihuai.com/kubernetes/request_limit.html", + "id": "http://www.wenzhihuai.com/kubernetes/request_limit.html", + "summary": "Kubernetes之request 和 limit详解 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 ...", + "content_html": "\n

我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

\n
resources:  \n    limits:    \n        cpu: \"1\"\n        memory: \"500Mi\"\n    requests:    \n        cpu: \"100m\"\n        memory: \"1000Mi\"
\n

下面我们首先来了解一下上面这段 yaml 文件中字段的含义:requests 和 limits:

\n

requests 定义了对应的容器所需要的最小资源量。
\nlimits 定义了对应容器最大可以消耗的资源上限。
\ncpu 等于1一般等同于1CPU 核心,1个VCPU或者一个超线程,具体要看服务器的CPU。而 limits 这里设置的 100m 则叫做100毫核,100m就表示0.1个核,所以这里也可以用0.1代替。
\nmemory 等于500Mi,(备注:1Mi=10241024;1M=10001000)
\n接下来我们来初步理解 requests 和 limits 这两个资源限制类型,在 Kubernetes 对 CPU 和内存资源限额的设计,通常是指用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。所以一般来说,在调度的时候 requests 比较重要,在运行时 limits 比较重要。

\n

而对应实际的业务场景来说,以 java 应用为例,requests 对应的就是JVM虚拟机所需资源的最小值,而 limits 对应的就是 JVM 虚拟机所能够使用的资源最大值。以内存资源为例一般就是指:Xms 和 Xmx,如果 requests 值设置的小于JVM虚拟机 Xms 的值,那么就会导致 Pod 内存溢出,从而导致 Pod 被杀掉,而后重新创建一个Pod。

\n

那么如果 CPU 资源使用超过了 limits,Pod会不会被杀掉呢?答案是不会,但是被限制。如果没有设置 limits ,Pod 可以使用全部空闲的资源。另外如果设置了 limits而没有设置 requests 时,Kubernetes 默认会将 requests 等于 limits。

\n

这里通常还会将 requests 和 limits 描述的资源分为两类:可压缩资源(compressible resources) 和不可压缩资源(incompressible resources)。这里不难看出CPU这类型资源为可压缩资源,而内存这类型资源为不可压缩资源。所以合理设置不可压缩资源的limits值就相当重要了。

\n", + "date_published": "2024-02-16T11:46:28.000Z", + "date_modified": "2024-02-16T11:46:28.000Z", + "authors": [], + "tags": [] + }, + { + "title": "JVM调优思路", + "url": "http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html", + "id": "http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html", + "summary": "JVM调优思路 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。 1.监控发现问题 2.工具分析问题 3.性能调优 下面开始一步步讲解 一、监控发现问题 通过监...", + "content_html": "\n

在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

\n

1.监控发现问题
\n2.工具分析问题
\n3.性能调优
\n下面开始一步步讲解

\n

一、监控发现问题

\n

通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:

\n

GC频繁
\nCPU负载过高
\nOOM
\n内存泄露
\n死锁
\n程序响应时间较长

\n

二、工具分析问题

\n

参考

\n

1.【JVM调优】如何进行JVM调优?一篇文章就够了!

\n", + "date_published": "2024-02-16T11:46:28.000Z", + "date_modified": "2024-02-16T11:46:28.000Z", + "authors": [], + "tags": [] + }, + { + "title": "主流GC收集器采用的算法", + "url": "http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html", + "id": "http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html", + "summary": "主流GC收集器采用的算法 上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。 是否可以使用两种或多种垃圾回收器 在Java中,你不能同时使用两种不同的垃圾回收器。Serial和G1是两种不同的垃圾回收器,它们...", + "content_html": "\n

| 垃圾回收器 | 采用的GC算法 | 代次 |
\n|

\n", + "date_published": "2024-02-16T09:55:09.000Z", + "date_modified": "2024-02-16T11:46:28.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Java内存模型(JMM)", + "url": "http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html", + "id": "http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html", + "summary": "Java内存模型(JMM) 本文转载自深入理解JVM-内存模型(jmm)和GC 1 CPU和内存的交互 了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】 有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型 在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存...", + "content_html": "\n

本文转载自深入理解JVM-内存模型(jmm)和GC

\n

1 CPU和内存的交互

\n

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

\n

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型

\n", + "image": "https://github-images.wenzhihuai.com/images/10006199-d3fc8462f127a2c7-20240216171550523.jpg", + "date_published": "2024-02-16T09:22:35.000Z", + "date_modified": "2024-02-16T09:55:09.000Z", + "authors": [], + "tags": [] + }, + { + "title": "引用计数和根可达算法", + "url": "http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html", + "id": "http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html", + "summary": "引用计数和根可达算法 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载 硬件角度来看什么叫做垃圾? 计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit...", + "content_html": "\n

本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

\n

硬件角度来看什么叫做垃圾?

\n

计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit位构成的。同时,内存需要在通电状态保持按周期频率的刷新,才能够维持数据的状态。

\n

在内存中,某些内存区域(bit位)被使用过了(用来存储对象或者数据)会被打上标记表示已经被使用过了,分配新内存空间的时候就计算机硬件就不会使用这些被标记过的内存区域。通常,这些给内存区域打上标记的活儿一般由操作系统的系统调用来完成,例如C语言中的 allocate()函数调用的系统函数。

\n

在面向对象的语言中,如果程序员新建了一个对象,那么操作系统会为其分配相应的内存区域,且该部分的内存区域会被打上标记说明已经被使用过了,这样操作系统就不会向该区域写入新的数据。

\n

然而,如果这个标记没有被程序员释放(C语言中调用free()来释放内存区域),那么该内存区域会一直被标记成已使用,如果整个内存都被标记成为了已使用,那么当操作系统想要再分配内存的时候就会失败,这个时候只有重启电脑来解决问题了。

\n

所以说,对于内存区域中不再使用的对象,我们需要释放掉其对应的内存区域,方便新的对象创建时有空间为其分配。而这些程序中不再被使用的对象就被称为“垃圾”,“垃圾”往往对应着内存中一块儿需要被释放的区域。

\n

Java中的垃圾

\n

Java中\"垃圾\"通常指的是不再被程序使用和引用的对象,具体表现在没有被栈、JNI指针和永久代对象所引用的对象。Java作为一种面向对象的编程语言,它使用自动内存管理机制,其中垃圾收集器负责检测和回收不再被程序引用的对象,以释放它们占用的内存空间。以下是一些导致对象成为垃圾的常见情况:

\n
    \n
  • 无引用对象: 当一个对象没有任何引用指向它时,它就变得不可达,成为垃圾,Java的垃圾收集器会识别这样的对象,并将它们回收。
  • \n
  • 引用循环: 如果一组对象彼此引用形成一个循环,而这个循环与程序的其他部分没有引用相连,那么这个循环中的对象就会成为垃圾。Java的垃圾收集器通过识别引用循环并处理它们来防止内存泄漏。
  • \n
  • 显式置空引用: 如果程序员显式地将一个引用置空(null),而没有其他引用指向相同的对象,那么该对象就变成了垃圾。
  • \n
\n

垃圾收集器周期性地运行,并识别和回收这些垃圾对象,释放其内存中对应的区域以确保内存能够得到有效利用,这种自动的内存管理机制就叫做垃圾回收。

\n

如何寻找垃圾?

\n

引用计数(Reference Count)

\n
\"img\"
img
\n

引用计数算法是一种垃圾标记,其核心思想是通过维护对象的引用计数来判断对象是否可以被回收。每个对象都有一个关联的引用计数,表示当前有多少个指针引用它。当引用计数为零时,意味着没有指针再引用该对象,因此可以安全地回收该对象的内存。

\n

其实引用计数算法的核心思想就是,只要有对象引用我,那么就说明我是有用的,我还不需要被回收,反正,我就是没有用的对象,那么我和我的子对象都应该被回收掉。这里我们说的对象都是堆上的对象,一般是堆上的内存空间需要程序员手动回收,而栈上的内存空间则由操作系统自行回收。由于栈上的对象是操作系统自行管理和回收的,因此栈上的对象以及一些静态对象始终都是出于存活的状态,因此,堆中存活的对象至少会有一个引用(指针)指向它。

\n

但是这样会存在着一个问题,就是对象中的引用关系形成了环状——循环引用,这种情况下环内所有对象的引用都是>1的,这样一来环内的所有都无法被回收从而造成“内存泄漏”。这是引用算法最主要的局限性,也是为什么JVM不采用循环计数的方法来标记垃圾的原因。

\n

根可达算法(Root Search)

\n

由于引用计数算法无法解决“循环引用”的问题,无可避免的会造成内存泄露,因此,Java没有采用引用计数算法来寻找垃圾。而是采用了一种从GC Roots开始搜索存活对象的垃圾标记算法——根可达算法。

\n
\"img\"
img
\n

哪些是GC Root?

\n

| GC Roots Source | Description |
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/v2-39ba8e013eb4757d88c0582e1a4d8db0_1440w.webp", + "date_published": "2024-02-16T09:22:35.000Z", + "date_modified": "2024-02-16T09:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "synchronized", + "url": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html", + "id": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html", + "summary": "synchronized 偏向锁在JDK 15后已经废弃 一、什么是synchronized 关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。 imgimg 学习Java的小伙伴都知道sync...", + "content_html": "\n

偏向锁在JDK 15后已经废弃

\n

一、什么是synchronized

\n

关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。

\n
\"img\"
img
\n

学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:

\n
    \n
  • 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  • \n
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • \n
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  • \n
\n

关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。

\n

具体使用规则如下:
\n\"在这里插入图片描述\"

\n

二、Synchronized 原理

\n

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

\n

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

\n

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

\n

流程图如下:
\n\"在这里插入图片描述\"

\n

通过一段代码来演示:

\n
public static void main(String[] args) {\n    synchronized (Synchronize.class){\n        System.out.println(\"Synchronize\");\n    }\n}\n12345
\n

使用javap -c Synchronize可以查看编译之后的具体信息。
\n\"在这里插入图片描述\"
\n可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

\n

在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
\n\"在这里插入图片描述\"
\nsynchronized的特点:
\n\"在这里插入图片描述\"

\n

三、Synchronized 优化

\n

从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

\n

3.1 偏向锁

\n

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

\n

3.1.1 偏向锁的获取过程

\n

(1)访问Mark Word中偏向锁的标识是否设置成“1”,锁标志位是否为“01”——确认为可偏向状态。
\n(2)如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。
\n(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

\n

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
\n(5)执行同步代码。

\n

3.1.2 偏向锁的释放

\n

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

\n

3.2 轻量锁

\n

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

\n

3.2.1 轻量级锁的加锁过程

\n

(1)在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
\n(2)拷贝对象头中的Mark Word复制到锁记录中。
\n(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
\n(4)如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
\n(5)如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

\n

3.2.2 轻量级锁的解锁过程

\n

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
\n(2)如果替换成功,整个同步过程完成。
\n(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

\n

3.3 其他优化

\n

适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6 加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

\n

通过--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。
\n通过--XX:PreBlockSpin修改自旋次数,默认值是10次。

\n

锁消除:锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

\n

锁粗化:我们在写代码时推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

\n

注意:在大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

\n

四、扩展

\n

其他控制并发/线程同步方式还有 Lock/ReentrantLock。

\n

4.1 Synchronized 和 ReenTrantLock 的对比

\n

① 两者都是可重入锁

\n

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

\n

② synchronized依赖于JVM而ReenTrantLock依赖于API

\n

synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

\n

③ ReenTrantLock比synchronized增加了一些高级功能

\n

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

\n
    \n
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • \n
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的,可以通过ReenTrantLoc类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • \n
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。
  • \n
\n

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

\n

④ 性能已不是选择标准

\n

在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平。所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

\n

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销。

\n
// CAS的操作参数\n// 内存位置(A)\n// 预期原值(B)\n// 预期新值(C)\n\n// 使用CAS解决并发的原理:\n// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;\n// 2. 通过死循环,以不断尝试尝试更新的方式实现并发\n\n// 伪代码如下\npublic boolean compareAndSwap(long memoryA, int oldB, int newC){\n    if(memoryA.get() == oldB){\n        memoryA.set(newC);\n        return true;\n    }\n    return false;\n}\n1234567891011121314151617
\n

具体使用当中CAS有个先检查后执行的操作,而这种操作在 Java 中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
\n具体过程:

\n
    \n
  1. CAS在Java中的体现为Unsafe类。
  2. \n
  3. Unsafe类会通过C++直接获取到属性的内存地址。
  4. \n
  5. 接下来CAS由C++的Atomic::cmpxchg系列方法实现。
  6. \n
\n

AtomicInteger的 i++ 与 i-- 是典型的CAS应用,通过compareAndSet & 一个死循环实现。

\n
private volatile int value; \n/** \n* Gets the current value. \n* \n* @return the current value \n*/ \npublic final int get() { \n   return value; \n} \n/** \n* Atomically increments by one the current value. \n* \n* @return the previous value \n*/ \npublic final int getAndIncrement() { \n   for (;;) { \n       int current = get(); \n       int next = current + 1; \n       if (compareAndSet(current, next)) \n           return current; \n   } \n} \n\n/** \n* Atomically decrements by one the current value. \n* \n* @return the previous value \n*/ \npublic final int getAndDecrement() { \n   for (;;) { \n       int current = get(); \n       int next = current - 1; \n       if (compareAndSet(current, next)) \n           return current; \n   } \n}\n123456789101112131415161718192021222324252627282930313233343536
\n

以上内容引用自学习指南
\n总的来说:
\n1、synchronized是java关键字,而Lock是java中的一个接口
\n2、synchronized会自动释放锁,而Lock必须手动释放锁
\n3、synchronized是不可中断的,Lock可以中断也可以不中断
\n4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
\n5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
\n6、Lock可以使用读锁提高多线程读效率
\n7、synchronized是非公平锁,ReentranLock可以控制是否公平锁

\n

4.2 Synchronized 与 ThreadLocal 的对比

\n

Synchronized 与 ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:

\n
    \n
  1. Synchronized关键字主要解决多线程共享数据同步问题;ThreadLocal主要解决多线程中数据因并发产生不一致问题。
  2. \n
  3. Synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  4. \n
\n

4.3 synchronized与volatile区别

\n

| volatile | synchronized |
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/v2-47781295251ded0e8ff32cf6a73fbfd0_1440w.webp", + "date_published": "2024-02-16T09:22:35.000Z", + "date_modified": "2024-02-17T16:08:07.000Z", + "authors": [], + "tags": [] + }, + { + "title": "mini主机", + "url": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/", + "id": "http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-11-29T18:45:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "个人网站", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/", + "summary": "jofjweoiaejof", + "content_html": "

jofjweoiaejof

\n", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "JVM", + "url": "http://www.wenzhihuai.com/java/JVM/", + "id": "http://www.wenzhihuai.com/java/JVM/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "CMS", + "url": "http://www.wenzhihuai.com/java/JVM/cms.html", + "id": "http://www.wenzhihuai.com/java/JVM/cms.html", + "summary": "CMS JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。 CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。 内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提...", + "content_html": "\n

JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

\n

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

\n

内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提前触发Full GC。

\n

预测性差:CMS垃圾回收器的暂停时间和CPU资源消耗都很难预测,这可能会对系统的性能造成影响。

\n

维护复杂:CMS垃圾回收器的代码相对复杂,需要更多的维护工作。

\n", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T09:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "G1", + "url": "http://www.wenzhihuai.com/java/JVM/g1.html", + "id": "http://www.wenzhihuai.com/java/JVM/g1.html", + "summary": "G1 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器 G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点: 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并...", + "content_html": "\n

本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

\n

G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点:

\n
    \n
  • 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并发的方式,充分利用多核处理器的优势,提高了垃圾回收的效率。
  • \n
  • 分代回收: G1垃圾回收器依旧采用分代回收的思想,但是和CMS等分代回收算法不同,G1不是将整个堆内存划分为年轻代、老年代和元空间。而是将堆内存划分为一个个固定大小的region,每个region可以属于年轻代或老年代。垃圾回收的基本单位是region,而不是整个堆,这使得垃圾回收更加灵活。
  • \n
  • 增量回收: G1采用增量回收的方式,将整个垃圾回收过程分解为多个阶段进行,这样更有利于分散垃圾回收的压力,减小每次暂停的时间,提高系统的响应性。
  • \n
  • Compacting回收: 与CMS回收器不同,G1是一种compacting回收器,其回收的内存空间是连续的。这样就可以避免CMS收集器由于不连续内存空间造成的所需堆空间更大和浮动垃圾的问题。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • \n
  • 软实时: G1回收器具有软实时(soft real-time)的特性,用户可以指定垃圾回收时间的限时。虽然G1会努力在限定时间内完成垃圾回收,但并不保证每次都能在时限内完成。通过设定合理的目标,可以使大部分垃圾回收时间都在规定的时限内完成。
  • \n
\n

G1垃圾回收器以其创新性的设计和优越的性能特点,逐渐成为Java应用程序中首选的垃圾回收器之一。通过分代、增量、并行、并发等多种技术手段的结合,G1回收器在处理大内存和多核处理器的环境下表现出色,为Java应用程序提供了更好的性能和响应能力。

\n

G1的内存模型和分代策略

\n

G1收集器相关参数

\n

| 参数 | 默认值 | 描述 |
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/v2-ddd707766e13fb354940a1a2e8ab5055_1440w.webp", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T09:55:09.000Z", + "authors": [], + "tags": [] + }, + { + "title": "ZGC", + "url": "http://www.wenzhihuai.com/java/JVM/zgc.html", + "id": "http://www.wenzhihuai.com/java/JVM/zgc.html", + "summary": "ZGC 本文转载自12 张图带你彻底理解 ZGC ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC...", + "content_html": "\n

本文转载自12 张图带你彻底理解 ZGC

\n

ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。

\n

ZGC 有 3 个重要特性:

\n
    \n
  • 暂停时间不会超过 10 ms。
  • \n
\n
\n

JDK 16 发布后,GC 暂停时间已经缩小到 1 ms 以内,并且时间复杂度是 o(1),这也就是说 GC 停顿时间是一个固定值了,并不会受堆内存大小影响。
\n下面图片来自:https://malloc.se/blog/zgc-jdk16

\n
\n
\"img\"
img
\n
    \n
  • 最大支持 16TB 的大堆,最小支持 8MB 的小堆。
  • \n
  • 跟 G1 相比,对应用程序吞吐量的影响小于 15 %。
  • \n
\n

1 内存多重映射

\n

内存多重映射,就是使用 mmap 把不同的虚拟内存地址映射到同一个物理内存地址上。如下图:

\n
\"img\"
img
\n

ZGC 为了更灵活高效地管理内存,使用了内存多重映射,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。

\n

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。

\n

Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

\n

2 染色指针

\n

2.1 三色标记回顾

\n

我们知道 G1 垃圾收集器使用了三色标记,这里先做一个回顾。下面是一个三色标记过程中的对象引用示例图:

\n
\"img\"
img
\n

总共有三种颜色,说明如下:

\n
    \n
  • 白色:本对象还没有被标记线程访问过。
  • \n
  • 灰色:本对象已经被访问过,但是本对象引用的其他对象还没有被全部访问。
  • \n
  • 黑色:本对象已经被访问过,并且本对象引用的其他对象也都被访问过了。
  • \n
\n

三色标记的过程如下:

\n
    \n
  1. 初始阶段,所有对象都是白色。
  2. \n
  3. 将 GC Roots 直接引用的对象标记为灰色。
  4. \n
  5. 处理灰色对象,把当前灰色对象引用的所有对象都变成灰色,之后将当前灰色对象变成黑色。
  6. \n
  7. 重复步骤 3,直到不存在灰色对象为止。
  8. \n
\n

三色标记结束后,白色对象就是没有被引用的对象(比如上图中的 H 和 G),可以被回收了。

\n

2.2 染色指针

\n

ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如下图:

\n
\"img\"
img
\n

前 62位保存了 GC 信息,最后两位保存了锁标志。

\n

ZGC 的一大创举是将 GC 信息保存在了染色指针上。染色指针是一种将少量信息直接存储在指针上的技术。在 64 位 JVM 中,对象指针是 64 位,如下图:

\n
\"img\"
img
\n

在这个 64 位的指针上,高 16 位都是 0,暂时不用来寻址。剩下的 48 位支持的内存可以达到 256 TB(2 ^48),这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 48 位都用来保存对象信息,而是用高 4 位保存了四个标志位,这样 ZGC 可以管理的最大内存可以达到 16 TB(2 ^ 44)。

\n

通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。

\n

无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。

\n

3 内存布局

\n

首先我们回顾一下 G1 垃圾收集器的内存布局。G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:

\n
\"img\"
img
\n

跟 G1 类似,ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的。不同的是,ZGC 的 Region 支持动态地创建和销毁,并且 Region 的大小不是固定的,包括三种类型的 Region :

\n
    \n
  • Small Region:2MB,主要用于放置小于 256 KB 的小对象。
  • \n
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
  • \n
  • Large Region:N * 2MB。这个类型的 Region 是可以动态变化的,不过必须是 2MB 的整数倍,最小支持 4 MB。每个 Large Region 只放置一个大对象,并且是不会被重分配的。
  • \n
\n

4 读屏障

\n

读屏障类似于 Spring AOP 的前置增强,是 JVM 向应用代码中插入一小段代码,当应用线程从堆中读取对象的引用时,会先执行这段代码。注意:只有从堆内存中读取对象的引用时,才会执行这个代码。下面代码只有第一行需要加入读屏障。

\n
Object o = obj.FieldA\nObject p = o //不是从堆中读取引用\no.dosomething() //不是从堆中读取引用\nint i =  obj.FieldB //不是引用类型
\n

读屏障在解释执行时通过 load 相关的字节码指令加载数据。作用是在对象标记和转移过程中,判断对象的引用地址是否满足条件,并作出相应动作。如下图:

\n
\"img\"
img
\n

标记、转移和重定位这些过程请看下一节。

\n
\n

读屏障会对应用程序的性能有一定影响,据测试,对性能的最高影响达到 4%,但提高了 GC 并发能力,降低了 STW。

\n
\n

5 GC 过程

\n

前面已经讲过,ZGC 使用内存多重映射技术,把物理内存映射为 Marked0、Marked1 和 Remapped 三个地址视图,利用地址视图的切换,ZGC 实现了高效的并发收集。

\n

ZGC 的垃圾收集过程包括标记、转移和重定位三个阶段。如下图:

\n
\"img\"
img
\n

ZGC 初始化后,整个内存空间的地址视图被设置为 Remapped。

\n

5.1 初始标记

\n

从 GC Roots 出发,找出 GC Roots 直接引用的对象,放入活跃对象集合,这个过程需要 STW,不过 STW 的时间跟 GC Roots 数量成正比,耗时比较短。

\n

5.2 并发标记

\n

并发标记过程中,GC 线程和 Java 应用线程会并行运行。这个过程需要注意下面几点:

\n
    \n
  • GC 标记线程访问对象时,如果对象地址视图是 Remapped,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理。
  • \n
  • 标记过程中Java 应用线程新创建的对象会直接进入 Marked0 视图。
  • \n
  • 标记过程中Java 应用线程访问对象时,如果对象的地址视图是 Remapped,就把对象地址视图切换到 Marked0,可以参考前面讲的读屏障。
  • \n
  • 标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。
  • \n
\n

标记阶段的活跃视图也可能是 Marked1,为什么会采用两个视图呢?

\n

这里采用两个视图是为了区分前一次标记和这一次标记。如果这次标记的视图是 Marked0,那下一次并发标记就会把视图切换到 Marked1。这样做可以配合 ZGC 按照页回收垃圾的做法。如下图:

\n
\"img\"
img
\n

第二次标记的时候,如果还是切换到 Marked0,那么 2 这个对象区分不出是活跃的还是上次标记过的。如果第二次标记切换到 Marked1,就可以区分出了。

\n

这时 Marked0 这个视图的对象就是上次标记过程被标记过活跃,转移的时候没有被转移,但这次标记没有被标记为活跃的对象。Marked1 视图的对象是这次标记被标记为活跃的对象。Remapped 视图的对象是上次垃圾回收发生转移或者是被 Java 应用线程访问过,本次垃圾回收中被标记为不活跃的对象。

\n

5.3 再标记

\n

并发标记阶段 GC 线程和 Java 应用线程并发执行,标记过程中可能会有引用关系发生变化而导致的漏标记问题。再标记阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软应用,虚引用等)进行并行标记。

\n

这个阶段需要 STW,但是需要标记的对象少,耗时很短。

\n

5.4 初始转移

\n

转移就是把活跃对象复制到新的内存,之前的内存空间可以被回收。

\n

初始转移需要扫描 GC Roots 直接引用的对象并进行转移,这个过程需要 STW,STW 时间跟 GC Roots 成正比。

\n

5.5 并发转移

\n

并发转移过程 GC 线程和 Java 线程是并发进行的。上面已经讲过,转移过程中对象视图会被切回 Remapped 。转移过程需要注意以下几点:

\n
    \n
  • 如果 GC 线程访问对象的视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • \n
  • 如果 GC 线程访问对象的视图是 Remapped,说明被其他 GC 线程处理过,跳过不再处理。
  • \n
  • 并发转移过程中 Java 应用线程创建的新对象地址视图是 Remapped。
  • \n
  • 如果 Java 应用线程访问的对象被标记为活跃并且对象视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • \n
\n

5.6 重定位

\n

转移过程对象的地址发生了变化,在这个阶段,把所有指向对象旧地址的指针调整到对象的新地址上。

\n

6 垃圾收集算法

\n

ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象。如下图:

\n
\"img\"
img
\n

6.1 JDK 16 之前

\n

在 JDK 16 之前,ZGC 会预留(Reserve)一块儿堆内存,这个预留内存不能用于 Java 线程的内存分配。即使从 Java 线程的角度看堆内存已经满了也不能使用 Reserve,只有 GC 过程中搬移存活对象的时候才可以使用。如下图:

\n
\"img\"
img
\n

这样做的好处是算法简单,非常适合并行收集。但这样做有几个问题:

\n
    \n
  • 因为有预留内存,能给 Java 线程分配的堆内存小于 JVM 声明的堆内存。
  • \n
  • Reserve 仅仅用于存放 GC 过程中搬移的对象,有点内存浪费。
  • \n
  • 因为 Reserve 不能给 GC 过程中搬移对象的 Java 线程使用,搬移线程可能会因为申请不到足够内存而不能完成对象搬移,这返回过来又会导致应用程序的 OOM。
  • \n
\n

6.2 JDK 16 改进

\n

JDK 16 发布后,ZGC 支持就地搬移对象(G1 在 Full GC 的时候也是就地搬移)。这样做的好处是不用预留空闲内存了。如下图:

\n
\"img\"
img
\n

不过就地搬移也有一定的挑战。比如:必须考虑搬移对象的顺序,否则可能会覆盖尚未移动的对象。这就需要 GC 线程之间更好的进行协作,不利于并发收集,同时也会导致搬移对象的 Java 线程需要考虑什么可以做什么不可以做。

\n

为了获得更好的 GC 表现,JDK 16 在支持就地搬移的同时,也支持预留(Reserve)堆内存的方式,并且 ZGC 不需要真的预留空闲的堆内存。默认情况下,只要有空闲的 region,ZGC 就会使用预留堆内存的方式,如果没有空闲的 region,否则 ZGC 就会启用就地搬移。如果有了空闲的 region, ZGC 又会切换到预留堆内存的搬移方式。

\n

7 总结

\n

内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。

\n

ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。

\n

ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。

\n

参考:

\n

1.https://wiki.openjdk.java.net/display/zgc

\n

2.https://openjdk.java.net/jeps/304

\n

3.https://openjdk.java.net/jeps/376

\n

4.https://malloc.se/blog/zgc-jdk16

\n

5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg

\n

6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw

\n

7.https://www.jianshu.com/p/664e4da05b2c

\n

8.https://www.cnblogs.com/jimoer/p/13170249.html

\n

9.https://www.jianshu.com/p/12544c0ad

\n", + "image": "https://github-images.wenzhihuai.com/images/v2-62257a68cd5fa063d16cb4c30bd75ac0_1440w.webp", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T11:46:28.000Z", + "authors": [], + "tags": [] + }, + { + "title": "SpringBoot", + "url": "http://www.wenzhihuai.com/java/SpringBoot/", + "id": "http://www.wenzhihuai.com/java/SpringBoot/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "I/O", + "url": "http://www.wenzhihuai.com/java/io/", + "id": "http://www.wenzhihuai.com/java/io/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "线程", + "url": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/", + "id": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "volatile", + "url": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html", + "id": "http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html", + "summary": "volatile Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。 image-20240217004404932imag...", + "content_html": "\n

Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

\n
\"image-20240217004404932\"
image-20240217004404932
\n

可见性

\n

有序性

\n", + "image": "https://github-images.wenzhihuai.com/images/image-20240217004404932.png", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T17:21:55.000Z", + "authors": [], + "tags": [] + }, + { + "title": "网络", + "url": "http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/", + "id": "http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/", + "content_html": "", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "DevOps", + "url": "http://www.wenzhihuai.com/kubernetes/devops/", + "id": "http://www.wenzhihuai.com/kubernetes/devops/", + "summary": "jofjweoiaejof", + "content_html": "

jofjweoiaejof

\n", + "date_published": "2024-02-16T04:00:26.000Z", + "date_modified": "2024-02-16T09:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "RedissonLock分布式锁源码分析", + "url": "http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html", + "id": "http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html", + "summary": "RedissonLock分布式锁源码分析 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。 目前分布式锁常...", + "content_html": "\n

最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
\n目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

\n

一、实现分布式锁的要求

\n
    \n
  1. 互斥性。在任何时候,当且仅有一个客户端能够持有锁。
  2. \n
  3. 不能有死锁。持有锁的客户端崩溃后,后续客户端能够加锁。
  4. \n
  5. 容错性。大部分Redis或者ZooKeeper节点能够正常运行。
  6. \n
  7. 加锁解锁相同。加锁的客户端和解锁的客户端必须为同一个客户端,不能让其他的解锁了。
  8. \n
\n

二、Redis实现分布式锁的常用命令

\n

1.SETNX key val
\n当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
\n2.expire key timeout
\n为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
\n3.delete key
\n删除key,此处用来解锁使用。
\n4.HEXISTS key field
\n当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。
\n5.HINCRBY key field increment
\n将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。

\n

三、常见写法

\n

由上面三个命令,我们可以很快的写一个分布式锁出来:

\n
if (conn.setnx(\"lock\",\"1\").equals(1L)) { \n    //do something\n    return true; \n} \nreturn false;
\n

但是这样也会存在问题,如果获取该锁的客户端挂掉了怎么办?一般而言,我们可以通过设置expire的过期时间来防止客户端挂掉所带来的影响,可以降低应用挂掉所带来的影响,不过当时间失效的时候,要保证其他客户端只有一台能够获取。

\n

四、Redisson

\n

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。——摘自百度百科

\n

4.1 测试例子

\n

先在pom引入Redssion

\n
<dependency>\n    <groupId>org.redisson</groupId>\n    <artifactId>redisson</artifactId>\n    <version>3.6.1</version>\n</dependency>
\n

起100个线程,同时对count进行操作,每次操作减1,加锁的时候能够保持顺序输出,不加的话为随机。

\n
public class RedissonTest implements Runnable {\n    private static RedissonClient redisson;\n    private static int count = 10000;\n\n    private static void init() {\n        Config config = new Config();\n        config.useSingleServer()\n                .setAddress(\"redis://119.23.46.71:6340\")\n                .setPassword(\"root\")\n                .setDatabase(10);\n        redisson = Redisson.create(config);\n    }\n\n    @Override\n    public void run() {\n        RLock lock = redisson.getLock(\"anyLock\");\n        lock.lock();\n        count--;\n        System.out.println(count);\n        lock.unlock();\n    }\n\n    public static void main(String[] args) {\n        init();\n        for (int i = 0; i < 100; i++) {\n            new Thread(new RedissonTest()).start();\n        }\n    }\n}
\n

输出结果(部分结果):

\n
...\n9930\n9929\n9928\n9927\n9926\n9925\n9924\n9923\n9922\n9921\n\n...
\n

去掉lock.lock()和lock.unlock()之后(部分结果):

\n
...\n9930\n9931\n9933\n9935\n9938\n9937\n9940\n9941\n9942\n9944\n9947\n9946\n9914\n...
\n

五、RedissonLock源码分析

\n

最新版的Redisson要求redis能够支持eval的命令,否则无法实现,即Redis要求2.6版本以上。在lua脚本中可以调用大部分的Redis命令,使用脚本的好处如下:
\n(1)减少网络开销:在Redis操作需求需要向Redis发送5次请求,而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
\n(2)原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
\n(3)复用:客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。

\n

5.1 使用到的全局变量

\n

全局变量:
\nexpirationRenewalMap:存储entryName和其过期时间,底层用的netty的PlatformDependent.newConcurrentHashMap()
\ninternalLockLeaseTime:锁默认释放的时间:30 * 1000,即30秒
\nid:UUID,用作客户端的唯一标识
\nPUBSUB:订阅者模式,当释放锁的时候,其他客户端能够知道锁已经被释放的消息,并让队列中的第一个消费者获取锁。使用PUB/SUB消息机制的优点:减少申请锁时的等待时间、安全、 锁带有超时时间、锁的标识唯一,防止死锁 锁设计为可重入,避免死锁。
\ncommandExecutor:命令执行器,异步执行器

\n

5.2 加锁

\n

以lock.lock()为例,调用lock之后,底层使用的是lockInterruptibly,之后调用lockInterruptibly(-1, null);

\n
\n
\"\"
\n
\n

(1)我们来看一下lockInterruptibly的源码,如果别的客户端没有加锁,则当前客户端进行加锁并且订阅,其他客户端尝试加锁,并且获取ttl,然后等待已经加了锁的客户端解锁。

\n
//leaseTime默认为-1\npublic void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {\n    long threadId = Thread.currentThread().getId();//获取当前线程ID\n    Long ttl = tryAcquire(leaseTime, unit, threadId);//尝试加锁\n    // 如果为空,当前线程获取锁成功,否则已经被其他客户端加锁\n    if (ttl == null) {\n        return;\n    }\n    //等待释放,并订阅锁\n    RFuture<RedissonLockEntry> future = subscribe(threadId);\n    commandExecutor.syncSubscription(future);\n    try {\n        while (true) {\n            // 重新尝试获取锁\n            ttl = tryAcquire(leaseTime, unit, threadId);\n            // 成功获取锁\n            if (ttl == null) {\n                break;\n            }\n            // 等待锁释放\n            if (ttl >= 0) {\n                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);\n            } else {\n                getEntry(threadId).getLatch().acquire();\n            }\n        }\n    } finally {\n        // 取消订阅\n        unsubscribe(future, threadId);\n    }\n}
\n

(2)下面是tryAcquire的实现,调用的是tryAcquireAsync

\n
    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {\n        return get(tryAcquireAsync(leaseTime, unit, threadId));\n    }
\n

(3)下面是tryAcquireAsync的实现,异步尝试进行加锁,尝试加锁的时候leaseTime为-1。通常如果客户端没有加锁成功,则会进行阻塞,leaseTime为锁释放的时间。

\n
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {\n    if (leaseTime != -1) {   //在lock.lock()的时候,已经声明了leaseTime为-1,尝试加锁\n        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);\n    }\n    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);\n    //监听事件,订阅消息\n    ttlRemainingFuture.addListener(new FutureListener<Long>() {\n        @Override\n        public void operationComplete(Future<Long> future) throws Exception {\n            if (!future.isSuccess()) {\n                return;\n            }\n            Long ttlRemaining = future.getNow();\n            // lock acquired\n            if (ttlRemaining == null) {\n                //获取新的超时时间\n                scheduleExpirationRenewal(threadId);\n            }\n        }\n    });\n    return ttlRemainingFuture;  //返回ttl时间\n}
\n

(4)下面是tryLockInnerAsyncy异步加锁,使用lua能够保证操作是原子性的

\n
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {\n    internalLockLeaseTime = unit.toMillis(leaseTime);\n    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,\n              \"if (redis.call('exists', KEYS[1]) == 0) then \" +\n                  \"redis.call('hset', KEYS[1], ARGV[2], 1); \" +\n                  \"redis.call('pexpire', KEYS[1], ARGV[1]); \" +\n                  \"return nil; \" +\n              \"end; \" +\n              \"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then \" +\n                  \"redis.call('hincrby', KEYS[1], ARGV[2], 1); \" +\n                  \"redis.call('pexpire', KEYS[1], ARGV[1]); \" +\n                  \"return nil; \" +\n              \"end; \" +\n              \"return redis.call('pttl', KEYS[1]);\",\n                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));\n}
\n

参数
\nKEYS[1](getName()) :需要加锁的key,这里需要是字符串类型。
\nARGV[1](internalLockLeaseTime) :锁的超时时间,防止死锁
\nARGV[2](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId
\nlua脚本解释

\n
--检查key是否被占用了,如果没有则设置超时时间和唯一标识,初始化value=1\nif (redis.call('exists', KEYS[1]) == 0) then\n  redis.call('hset', KEYS[1], ARGV[2], 1);\n  redis.call('pexpire', KEYS[1], ARGV[1]);\n  return nil; \nend; \n--如果锁重入,需要判断锁的key field 都一致情况下 value 加一 \nif (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then \n  redis.call('hincrby', KEYS[1], ARGV[2], 1);\n  --锁重入重新设置超时时间  \n  redis.call('pexpire', KEYS[1], ARGV[1]); \n  return nil; \nend;\n--返回剩余的过期时间\nreturn redis.call('pttl', KEYS[1]);
\n

(5)流程图

\n
\n
\"\"
\n
\n

5.3 解锁

\n

解锁的代码很简单,大意是将该节点删除,并发布消息。
\n(1)unlock源码

\n
    public void unlock() {\n        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));\n        if (opStatus == null) {\n            throw new IllegalMonitorStateException(\"attempt to unlock lock, not locked by current thread by node id: \"\n                    + id + \" thread-id: \" + Thread.currentThread().getId());\n        }\n        if (opStatus) {\n            cancelExpirationRenewal();\n        }
\n

(2)异步解锁,并返回是否成功

\n
protected RFuture<Boolean> unlockInnerAsync(long threadId) {\n    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,\n            \"if (redis.call('exists', KEYS[1]) == 0) then \" +\n                \"redis.call('publish', KEYS[2], ARGV[1]); \" +\n                \"return 1; \" +\n            \"end;\" +\n            \"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then \" +\n                \"return nil;\" +\n            \"end; \" +\n            \"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); \" +\n            \"if (counter > 0) then \" +\n                \"redis.call('pexpire', KEYS[1], ARGV[2]); \" +\n                \"return 0; \" +\n            \"else \" +\n                \"redis.call('del', KEYS[1]); \" +\n                \"redis.call('publish', KEYS[2], ARGV[1]); \" +\n                \"return 1; \"+\n            \"end; \" +\n            \"return nil;\",\n            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));\n\n    }
\n

输入的参数有:
\n参数:
\nKEYS[1](getName()):需要加锁的key,这里需要是字符串类型。
\nKEYS[2](getChannelName()):redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName:“redisson_lock__channel__{” + getName() + “}”
\nARGV[1](LockPubSub.unlockMessage):redis消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。
\nARGV[2](internalLockLeaseTime):锁的超时时间,防止死锁
\nARGV[3](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

\n

此处lua脚本的作用:

\n
--如果keys[1]不存在,则发布消息,说明已经被解锁了\nif (redis.call('exists', KEYS[1]) == 0) then\n    redis.call('publish', KEYS[2], ARGV[1]);\n    return 1;\nend;\n--key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。\nif (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then\n    return nil;\nend;\n--将value减1,这里主要用在重入锁\nlocal counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); \nif (counter > 0) then \n    redis.call('pexpire', KEYS[1], ARGV[2]); \n    return 0; \nelse \n--删除key并消息\n    redis.call('del', KEYS[1]); \n    redis.call('publish', KEYS[2], ARGV[1]); \n    return 1;\nend; \nreturn nil;
\n

(3)删除过期信息

\n
void cancelExpirationRenewal() {\n    Timeout task = expirationRenewalMap.remove(getEntryName());\n    if (task != null) {\n        task.cancel();\n    }\n}
\n

总结

\n

Redis2.6版本之后引入了eval,能够支持lua脚本,更好的保证了redis的原子性,而且redisson采用了大量异步的写法来避免性能所带来的影响。本文只是讲解了下redisson的重入锁,其还有公平锁、联锁、红锁、读写锁等,有兴趣的可以看下。感觉这篇文章写得也不是很好,毕竟netty还没开始学,有些api也不太清楚,希望各位大佬能够建议建议~~

\n

参考:
\n1.redisson
\n2.Redis分布式锁的正确实现方式
\n3.分布式锁的多种实现方式
\n4.用Redis构建分布式锁
\n5.基于Redis的分布式锁实现
\n6.基于Redis实现分布式锁,Redisson使用及源码分析

\n", + "image": "https://upyuncdn.wenzhihuai.com/20180316081203383746214.png", + "date_published": "2024-02-06T07:22:35.000Z", + "date_modified": "2024-02-06T07:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "canal小记", + "url": "http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html", + "id": "http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html", + "summary": "canal小记 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。 先来了解一下MySQL的主从备份: 从上层来看,复制分成三步: master将改...", + "content_html": "\n

接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

\n

先来了解一下MySQL的主从备份:

\n
\"\"
\n

从上层来看,复制分成三步:
\nmaster将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
\nslave将master的binary log events拷贝到它的中继日志(relay log);
\nslave重做中继日志中的事件,将改变反映它自己的数据。

\n

问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么

\n

最后发现是沟通问题。。。

\n
\"\"
\n

排查过程:

\n
    \n
  1. 起初,怀疑是es的问题,会不会是string转为long中出现了问题,PUT了个,无异常,这种情况排除。
  2. \n
  3. 再然后以为是代码有问题,可是想了下,rowData.getAfterColumnsList().forEach(column -> data.put(column.getName(), column.getValue()))这句不可能有什么其他的问题啊,而且测试环境中一切都是好好的。
  4. \n
  5. canal安装出错,重新查看了一次canal.properties和instance.properties,并没有发现配置错了啥,如果错了,那为什么只有那几个字段出现异常,其他的都是好好的,郁闷。而且,用测试环境的canal配置生产中的数据库,然后本地调试,结果依旧一样。可能问题出在mysql。
  6. \n
\n

最后发现,居然是沟通问题。。。。测试环境中是从正式环境导入的,用的insert,可是在正式环境里,用的确实insert后update字段,之后发现居然还用delete,,,,晕。。。。之前明确问过了只更新insert的,人与人之间的信任在哪里。。。。

\n

问题二:canal.properties中四种模式的差别

\n

简单的说,canal维护一份增量订阅和消费关系是依靠解析位点和消费位点的,目前提供了一下四种配置,一开始我也是懵的。

\n
#canal.instance.global.spring.xml = classpath:spring/local-instance.xml\n#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml\ncanal.instance.global.spring.xml = classpath:spring/file-instance.xml\n#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
\n

local-instance
\n我也不知道啥。。

\n

memory-instance
\n所有的组件(parser , sink , store)都选择了内存版模式,记录位点的都选择了memory模式,重启后又会回到初始位点进行解析
\n特点:速度最快,依赖最少(不需要zookeeper)
\n场景:一般应用在quickstart,或者是出现问题后,进行数据分析的场景,不应该将其应用于生产环境。
\n个人建议是调试的时候使用该模式,即新增数据的时候,客户端能马上捕获到改日志,但是由于位点一直都是canal启动的时候最新的,不适用与生产环境。

\n

file-instance
\n所有的组件(parser , sink , store)都选择了基于file持久化模式,注意,不支持HA机制.
\n特点:支持单机持久化
\n场景:生产环境,无HA需求,简单可用.
\n采用该模式的时候,如果关闭了canal,会在destination中生成一个meta.dat,用来记录关键信息。如果想要启动canal之后马上订阅最新的位点,需要把该文件删掉。
\n{\"clientDatas\":[{\"clientIdentity\":{\"clientId\":1001,\"destination\":\"example\",\"filter\":\".\\..\"},\"cursor\":{\"identity\":{\"slaveId\":-1,\"sourceAddress\":{\"address\":\"192.168.6.71\",\"port\":3306}},\"postion\":{\"included\":false,\"journalName\":\"binlog.008335\",\"position\":221691106,\"serverId\":88888,\"timestamp\":1524294834000}}}],\"destination\":\"example\"}

\n

default-instance
\n所有的组件(parser , sink , store)都选择了持久化模式,目前持久化的方式主要是写入zookeeper,保证数据集群共享。
\n特点:支持HA
\n场景:生产环境,集群化部署.
\n该模式会记录集群中所有运行的节点,主要用与HA主备模式,节点中的数据如下,可以关闭某一个canal服务来查看running的变化信息。

\n
\"\"
\n

问题三:如果要订阅的是mysql的从库改怎么做?

\n

生产环境中的主库是不能随便重启的,所以订阅的话必须订阅mysql主从的从库,而从库中是默认下只将主库的操作写进中继日志,并写到自己的二进制日志的,所以需要让其成为canal的主库,必须让其将日志也写到自己的二进制日志里面。处理方法:修改/etc/my.cnf,增加一行log_slave_updates=1,重启数据库后就可以了。

\n
\"\"
\n

问题四:部分字段没有更新

\n

最终版本是以mysql的id为es的主键,用canal同步到flume,再由flume到kafka,然后再由一个中间件写到es里面去,结果发现,一天之中,会有那么一段时间得出的结果少一丢丢,甚至是骤降,如图。不得不从头开始排查情况,canal到flume,加了canal的重试,以及发送到flume的重试机制,没有报错,所有数据正常发送。flume到kafka不敢怀疑,毕竟公司一直在用,怎么可能有问题。kafka到es的中间件?组长写的,而且一直在用,不可能==最后确认的是flume到kafka,kafka的parition处理速度不同,

\n
\"\"
\n

check一下flume的文档,可以知道

\n

| Property Name | Description|
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/20180421025107389573244.png", + "date_published": "2024-02-06T07:22:35.000Z", + "date_modified": "2024-02-06T07:53:39.000Z", + "authors": [], + "tags": [] + }, + { + "title": "基于ZooKeeper的队列爬虫", + "url": "http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html", + "id": "http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html", + "summary": "基于ZooKeeper的队列爬虫 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。 简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的C...", + "content_html": "\n

一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
\n简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
\n基本的知识就不过多介绍了,可以参考参考下面这些人的:
\nZooKeeper官网
\nhttp://www.cnblogs.com/wuxl360/p/5817471.html

\n

一、整体架构

\n

这张图来自skyme,我也是看了这张图的启发写了这篇文章的。

\n
\"\"
\n

最基本的分布式队列即一个生产者不断抓取链接,然后将链接存储进ZooKeeper的队列节点里,每个节点的value都只是链接,然后消费者从中获取一条url进行抓取。本项目生产这主要是用来生产URL即可,这部分就不要要求太多。然后是消费者,消费者需要解决的问题有:
\n1.队列如何保证自己的分发正确;
\n2.消费这如何进行高效的抓取。

\n

二、ZooKeeper队列原理

\n

2.1 介绍

\n

分布式队列,目前此类产品大多类似于ActiveMQ、RabbitMQ等,本文主要介绍的是Zookeeper实现的分布式队列,它的实现方式也有两种,一种是FIFO(先进先出)的队列,另一种是等待队列元素聚集之后才统一安排的Barrier模型。同样,本文主要讲的是FIFO的队列模型。其大体设计思路也很简单,主要是在/SinaQueue下创建顺序节点,如/SinaQueue/qn-000000000,创建完节点之后,根据下面的4个步骤来决定执行的顺序。
\n1.通过调用getChildren()接口来获取某一节点下的所有节点,即获取队列中的所有元素。
\n2.确定自己的节点序号在所有子节点中的顺序。
\n3.如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
\n4.接收到Watcher通知后,重复步骤1。

\n
\"\"
\n

2.2 Watcher介绍

\n

znode以某种方式发生变化时,“观察”(watch)机制可以让客户端得到通知.可以针对ZooKeeper服务的“操作”来设置观察,该服务的其他 操作可以触发观察。
\n1.Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
\n2.Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调
\n3.Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。

\n

2.3 源码

\n

在csdn上找到了某个人写的这个过程,使用的是ZKClient,有兴趣可以看看杰布斯的博客,但是没有实现上面过程的第三步(Watcher相关的),这里,我们使用的是Zookeeper的另一个客户端工具curator,其中,curator实现了各种Zookeeper的特性,如:Election(选举),Lock(锁),Barrier(关卡),Atonmic(原子量),Cache(缓存),Queue(队列)等。我们来看看Curator实现的简单的分布式队列的源码。

\n
public class SimpleDistributedQueue {\n    ...\n    private final CuratorFramework client;//连接Zookeeper的客户端\n    private final String path;//路径\n    private final EnsureContainers ensureContainers;//确保原子特性\n    private final String PREFIX = \"qn-\";//顺序节点的同意前缀,使用qn-\n    ...
\n

其中PREFIX是用来生成顺序节点的,默认不可更改,将生成的路径赋予给path,然后向节点赋予数据。下面是赋予数据的代码

\n
    public boolean offer(byte[] data) throws Exception {\n        String thisPath = ZKPaths.makePath(this.path, \"qn-\");//生成的路径\n        ((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL)).forPath(thisPath, data);//如果没有路径将生成持久化的路径然后存储节点的数据。\n        return true;\n    }
\n

最关键的来了,队列如何保证自己的分发正确?SimpleDistributedQueue使用take()来取得队列的头部,然后将头部删掉,这一过程的一致性是通过CountDownLatch和Watcher来实现的。

\n
    public byte[] take() throws Exception {//直接调用interPoll,并将超时的设置为0;\n        return this.internalPoll(0L, (TimeUnit)null);\n    }\n    private byte[] internalPoll(long timeout, TimeUnit unit) throws Exception {\n        ...//忽略超时的设置代码\n        while(true) {\n            final CountDownLatch latch = new CountDownLatch(1);//定义一个latch,设置为1,先加锁,然后执行完任务后再释放锁\n            Watcher watcher = new Watcher() {\n                public void process(WatchedEvent event) {\n                    latch.countDown();\n                }\n            };\n            byte[] bytes;\n            try {\n                bytes = this.internalElement(true, watcher);//调用internalElement函数来获取字节流\n            } catch (NoSuchElementException var17) {\n            }\n            ...\n            if (hasTimeout) {\n                long elapsedMs = System.currentTimeMillis() - startMs;\n                long thisWaitMs = maxWaitMs - elapsedMs;\n                if (thisWaitMs <= 0L) {    //如果等待超时了则返回为空\n                    return null;\n                }\n                latch.await(thisWaitMs, TimeUnit.MILLISECONDS);\n            } else {\n                latch.await();\n            }\n        }\n    }\n    private byte[] internalElement(boolean removeIt, Watcher watcher) throws Exception {\n            this.ensurePath();\n            List nodes;\n            try {\n                nodes = watcher != null ? (List)((BackgroundPathable)this.client.getChildren().usingWatcher(watcher)).forPath(this.path) : (List)this.client.getChildren().forPath(this.path);//获取节点下的所有子节点注册监听(watcher默认都不是为空的,每一个都注册)\n            } catch (NoNodeException var8) {\n                throw new NoSuchElementException();\n            }\n    \n            Collections.sort(nodes);//对节点进行排序\n            Iterator var4 = nodes.iterator();\n            while(true) {//遍历\n                while(var4.hasNext()) {\n                    String node = (String)var4.next();//取得当前头结点\n                    if (node.startsWith(\"qn-\")) {\n                        String thisPath = ZKPaths.makePath(this.path, node);\n                        try {\n                            byte[] bytes = (byte[])this.client.getData().forPath(thisPath);\n                            if (removeIt) {\n                                this.client.delete().forPath(thisPath);//删除该节点\n                            }\n                            return bytes;//返回节点的字节流\n            ...\n        }
\n

三、多线程并发

\n

对于分布式爬虫来说,让每一个消费者高效的进行抓取是具有重要意义的,为了加快爬虫的速度,采用多线程爬虫的方法。Java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。其中使用Executors提供了四种声明线程池的方法,分别是newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor和newScheduledThreadPool,为了监控实时监控队列的长度,我们使用数组型的阻塞队列ArrayBlockingQueue。声明方式如下:

\n
    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);\n    ExecutorService es = new ThreadPoolExecutor(CORE, CORE,\n            0L, TimeUnit.MILLISECONDS,\n            queuelength);
\n

四、使用

\n

本次实验主要环境如下:

\n
zookeeper.version=3.5\njava.version=1.8.0_65\nos.arch=amd64\ni5 四核心CPU\n网速为中国电信100M
\n

这里主要是对博客园中的前两千条博客进行爬取,本文主要是对分布式队列的理解,就不再进行什么难度的处理(比如元素的选取、数据的存储等),只输出每篇博客的title即可。
\n生产者代码:

\n
public class Producer {\n    //logger\n    private static final Logger logger = LoggerFactory.getLogger(Producer.class);\n    public static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString(\"119.23.46.71:2181\")\n            .sessionTimeoutMs(1000)\n            .connectionTimeoutMs(1000)\n            .canBeReadOnly(false)\n            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))\n            .defaultData(null)\n            .build();\n    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, \"/Queue\");\n    private static Integer j = 0;\n\n    public static void begin(String url) {//对博客园的每一页进行爬取\n        try {\n            String content = HttpHelper.getInstance().get(url);\n            resolveweb(content);\n        } catch (Exception e) {\n            logger.error(\"\", e);\n        }\n    }\n    public static void resolveweb(String content) throws Exception {\n        Elements elements = Jsoup.parse(content).select(\"a.titlelink\");//对每篇博客的标题进行获取\n        for (Element element : elements) {\n            String url = element.attr(\"href\");//\n            if (StringUtils.isNotEmpty(url) && !url.contains(\"javascript\") && !url.contains(\"jump\")) {//去除a中调用href过程\n                logger.info(url + \" \" + String.valueOf(j++));\n                queue.offer(url.getBytes());\n            }\n        }\n    }\n\n    public static void main(String[] args) {\n        client.start();\n        for (int i = 0; i < 100; i++) {\n            begin(\"https://www.cnblogs.com/#p\" + String.valueOf(i));\n        }\n    }\n}
\n

消费者

\n
public class Consumer {\n    //logger\n    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);\n    private static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString(\"119.23.46.71:2181\")\n            .sessionTimeoutMs(1000)\n            .connectionTimeoutMs(1000)\n            .canBeReadOnly(false)\n            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))\n            .defaultData(null)\n            .build();\n    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, \"/SinaQueue\");\n    private static Integer i = 0;\n    private static final Integer CORE = Runtime.getRuntime().availableProcessors();\n    //声明为一个数组型的阻塞队列,这里限制大小为\n    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);\n\n    static class CBCrawler implements Runnable {\n        private String url;\n\n        public CBCrawler(String url) {\n            this.url = url;\n        }\n\n        @Override\n        public void run() {\n            String content = HttpHelper.getInstance().get(url);\n            logger.info(url + \" \" + Jsoup.parse(content).title());//打印网页的标题\n        }\n    }\n\n    public static void begin() {\n        try {\n            ExecutorService es = new ThreadPoolExecutor(CORE, CORE,\n                    0L, TimeUnit.MILLISECONDS,\n                    queuelength);\n            while (client.getChildren().forPath(\"/SinaQueue\").size() > 0) {\n                CBCrawler crawler = new CBCrawler(new String(queue.take()));\n                es.submit(crawler);//执行爬虫\n                i = i + 1;\n                logger.info(String.valueOf(i) + \" is finished\\n\" + \" queue size is\" + queuelength.size());//监控当前队列的长度\n            }\n            if (!es.isShutdown()) {//如果线程池没有关闭则关闭\n                es.shutdown();\n            }\n        } catch (Exception e) {\n            logger.error(\"\", e);\n        }\n    }\n\n    public static void main(String[] args) {\n        long start = System.currentTimeMillis();\n        client.start();\n        begin();\n        client.close();\n        logger.info(\"start time: \" + start);\n        long end = System.currentTimeMillis();\n        logger.info(\"end time: \" + end);\n        logger.info(\"take time: \" + String.valueOf(end - start));//记录开始时间和结束时间\n    }\n}
\n

由于在队列的take中使用了CountDownLatch和Collections.sort(nodes)进行排序,耗时过程变长了不少,2000个节点,单台服务器和多台服务器的耗时是一样的,都是9分钟,具体实验见下面。

\n

实验结果

\n

生产者生产URL:

\n
\"\"
\n

单机模式下的消费者,耗时:560825/(1000*60)=9分钟

\n
\"\"
\n

分布式模式下的抓取:

\n
\"\"
\n

耗时:564374/(1000*60)=9分钟:

\n
\"\"
\n

由图可见,当每个消费者处理能力大于队列分配的能力时,耗时的过程反而是在队列,毕竟分布式队列在进行take动作的时候对节点进行了加锁,还要对队列进行排序,特别是在节点多达2000+的情况下,耗时是十分严重的。

\n

实验二

\n

实验二的主要解决的问题是将消费者处理的耗时延长,我们使用Thread.sleep(n)来模拟时长。由于博客园突然连不上,为了减少这种不可控的故障,抓取的网页改为新浪,并将抓取后的URL以文本形式保存下来。

\n
public static void sleepUtil(Integer time) {\n    try {\n        Thread.sleep(time * 1000);\n    } catch (Exception e) {\n        logger.error(\"线程sleep异常\", e);\n    }\n}
\n

此时再看程序的输出,可以看出,队列的分发能力已经大于消费者的处理能力,总算是正常了。

\n
\"\"
\n

分布式队列分发的时间是:341998/(1000*60)=5.6分钟

\n
2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - start time: 1509324606460\n2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - end time: 1509324948458\n2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - take time: 341998
\n

两台机子抓取完毕的耗时分别是:

\n
A服务器:08:49:54.509——09:02:07  \nB服务器:08:49:54.509——09:05:05
\n

单机的时候分发时间是:353198/(1000*60)=5.8分钟

\n
2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - start time: 1509326672614\n2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - end time: 1509327025812\n2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - take time: 353198
\n

耗时

\n
09:24:33.391——09:51:44.733
\n

分布式下平均耗时约为13分钟,单机模式下耗时约为27分钟,还是蛮符合估算的。

\n

总结

\n

源代码都放在这里了,有兴趣的可以star一下或者下载看一下,也欢迎大家提提意见,没企业级的实战环境,见笑了O(∩_∩)O~

\n

欢迎访问我的个人网站
\n个人网站网址:http://www.wenzhihuai.com
\n个人网站代码地址:https://github.com/Zephery/newblog

\n", + "image": "https://github-images.wenzhihuai.com/images/20171030102024.png", + "date_published": "2024-02-06T07:22:35.000Z", + "date_modified": "2024-02-14T17:59:53.000Z", + "authors": [], + "tags": [] + }, + { + "title": "TCP/IP、HTTP、HTTPS、HTTP2.0", + "url": "http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html", + "id": "http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html", + "summary": "TCP/IP、HTTP、HTTPS、HTTP2.0 HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认...", + "content_html": "\n

HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

\n

HTTPS,即加密后的HTTP。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。目前TLS的版本是1.2,定义在RFC 5246中,暂时还没有被广泛的使用。

\n

HTTP2.0,下一代的HTTP协议。相比于HTTP1.x,大幅度的提升了web性能,进一步减少了网络延时和拥塞。

\n
\"\"
\n

各自的RFC相关文档自己去搜吧,https://www.rfc-editor.org/

\n

一、TCP/IP

\n

为了了解HTTP,有必要先理解一下TCP/IP。目前,存在两种划分模型的方法,OSI七层模型和TCP/IP模型,具体的区别不在阐述。HTTP是建立在TCP协议之上,所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性,例如tcp建立连接的3次握手和断开连接的4次挥手以及每次建立连接带来的RTT延迟时间。

\n
\"\"
\n

TCP三次握手四次挥手的原理,由于篇幅关系,具体请看TCP协议的三次握手和四次挥手

\n

二、HTTP

\n

超文本传输协议(HyperText Transfer Protocol) 是伴随着计算机网络和浏览器而诞生,在浏览器出现之前,人们是怎么使用网络的?,不管怎么说,那个时代对于现在的我们,有点难以想象。。。之后,网景发布了Netscape Navigator浏览器,才慢慢打开了互联网的幕布。如果根据OSI来划分的话,HTML属于表示层,而HTTP属于应用层。HTTP发展至今,经过了HTTP0.9、HTTP1.0、HTTP1.1、HTTP2.0的时代,虽然2.0很久之前就正式提出标准,大多浏览器也支持了,但是网络支持HTTP2.0的却很少。

\n

2.1 HTTP报文分析

\n

报文,是网络中交换和传输的基本单元,即一次性发送的数据块。HTTP的报文是由一行一行组成的,纯文本,而且是明文,即:如果能监听你的网络,那么你发送的所有账号密码都是可以看见的,为了保障数据隐秘性,HTTPS随之而生。

\n

2.1.1 请求报文:

\n

为了形象点,我们把报文标准和实际的结合起来看。

\n
\"\"
\n

下面是实际报文,以访问自己的网站(http://www.wenzhihuai.com)中的一个链接为例。

\n
\"\"
\n
请求行
\n

请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT,这里我们使用的是GET方法,访问的是/biaoqianyun.do,协议使用的是HTTP/1.1。
\nGET:当客户端要从服务器中读取某个资源时,使用GET 方法。如果需要加传参数的话,需要在URL之后加个\"?\",然后把参数名字和值用=连接起来,传递参数长度受限制,通常IE8的为4076,Chrome的为7675。例如,/index.jsp?id=100&op=bind。
\nPOST:当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,以名称/值的形式出现,可以传输大量数据;

\n
请求头部
\n

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
\nUser-Agent:产生请求的浏览器类型;
\nAccept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ / ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
\nAccept-Language:客户端可接受的自然语言;
\nAccept-Encoding:客户端可接受的编码压缩格式;
\nAccept-Charset:可接受的应答的字符集;
\nHost:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
\nconnection:连接方式(close 或 keepalive),如果是close的话就需要进行TCP四次挥手关闭连接,如果是keepalive,表明还能继续使用,这是HTTP1.1对1.0的新增,加快了网络传输,默认是keepalive;
\nCookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;

\n
空行
\n

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头;

\n
请求包体
\n

请求包体不在 GET 方法中使用,而是在POST 方法中使用。POST 方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型 Content-Type 和包体长度 Content-Length;

\n

2.1.2 响应报文

\n

同样,先粘贴报文标准。

\n
\"\"
\n

抓包,以访问(http://www.wenzhihuai.com)为例。

\n
\"\"
\n
状态行
\n

状态行由 HTTP 协议版本字段、状态码和状态码的描述文本 3 个部分组成,他们之间使用空格隔开,描述文本一般不显示;
\n状态码:由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:
\n1xx:服务器已接收,但客户端可能仍要继续发送;
\n2xx:成功;
\n3xx:重定向;
\n4xx:请求非法,或者请求不可达;
\n5xx:服务器内部错误;

\n
响应头部:响应头可能包括:
\n

Location:Location响应报头域用于重定向接受者到一个新的位置。例如:客户端所请求的页面已不存在原先的位置,为了让客户端重定向到这个页面新的位置,服务器端可以发回Location响应报头后使用重定向语句,让客户端去访问新的域名所对应的服务器上的资源;
\nServer:Server 响应报头域包含了服务器用来处理请求的软件信息及其版本。它和 User-Agent 请求报头域是相对应的,前者发送服务器端软件的信息,后者发送客户端软件(浏览器)和操作系统的信息。
\nVary:指示不可缓存的请求头列表;
\nConnection:连接方式,这个跟rquest的类似。
\n空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
\n响应包体:服务器返回给客户端的文本信息;

\n

2.2 HTTP特性

\n

HTTP的主要特点主要能概括如下:

\n

2.2.1 无状态性

\n

即,当客户端访问完一次服务器再次访问的时候,服务器是无法知道这个客户端之前是否已经访问过了。优点是不需要先前的信息,能够更快的应答,缺点是每次连接传送的数据量增大。这种做法不利于信息的交互,随后,Cookie和Session就应运而生,至于它俩有什么区别,可以看看COOKIE和SESSION有什么区别?
\n

\n

2.2.2 持久连接

\n

HTTP1.1 使用持久连接keepalive,所谓持久连接,就是服务器在发送响应后仍然在一段时间内保持这条连接,允许在同一个连接中存在多次数据请求和响应,即在持久连接情况下,服务器在发送完响应后并不关闭TCP连接,客户端可以通过这个连接继续请求其他对象。

\n

2.2.3 其他

\n

支持客户/服务器模式、简单快速(请求方法简单Get和POST)、灵活(数据对象任意)

\n

2.3 影响HTTP的因素

\n

影响HTTP请求的因素:

\n
    \n
  1. 带宽
    \n好像只要上网这个因素是一直都有的。。。即使再快的网络,也会有偶尔网络慢的时候。。。
  2. \n
  3. 延迟
    \n(1) 浏览器阻塞
    \n一个浏览器对于同一个域名,同时只能有4个链接(根据不同浏览器),如果超了后面的会被阻塞。
    \n常用浏览器阻塞数量看下图。
  4. \n
\n
\"\"
\n

(2) DNS查询
\n浏览器建立连接是需要知道服务器的IP的,DNS用来将域名解析为IP地址,这个可以通过刷新DNS缓存来加快速度。
\n(3) 建立连接
\n由之前第一章的就可以看出,HTTP是基于TCP协议的,即使网络、浏览器再快也要进行TCP的三次握手,在高延迟的场景下影响比较明显,慢启动则对文件请求影响较大。

\n

2.4 缺陷

\n
    \n
  1. 耗时:传输数据每次都要建立连接;
  2. \n
  3. 不安全:HTTP是明文传输的,只要在路由器或者交换机上截取,所有东西(账号密码)都是可见的;
  4. \n
  5. Header内容过大:通常,客户端的请求header变化较小,但是每次都要携带大量的header信息,导致传输成本增大;
  6. \n
  7. keepalive压力过大:持久连接虽然有一点的优点,但同时也会给服务器造成大量的性能压力,特别是传输图片的时候。
  8. \n
\n

BTW:明文传输有多危险,可以去试试,下面是某个政府网站,采用wireshark抓包,身份证、电话号码、住址什么的全暴露出来,所以,,,只要在路由器做点小动作,你的信息是全部能拿得到的,毕竟政府。

\n
\"\"
\n

由于涉及的隐私太多,打了马赛克

\n

三、HTTPS

\n

由于HTTP报文的不安全性,网景在1994年就创建了HTTPS,并用在浏览器中。最初HTTPS是和SSL一起使用,然后演化为TLS。SSL/TLS在OSI模型中都是表示层的协议。SSL使 用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。

\n

3.1 SSL/TLS

\n

SSL(Secure Sockets Layer),简称安全套接入层,最初由上世纪90年代由网景公司设计。开启 SSL 会增加内存、CPU、网络带宽的开销,后二者跟你使用的 cipher suite 密切相关,其中参数很多,很难一概而论。开启 SSL 的前提是你的 cert 和 key 必须放在 TCP endpoint,你是否信得过那台设备。
\nTLS(Transport Layer Security),简称安全传输层协议,该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面,与具体的应用无关,所以,一般把TLS协议归为传输层安全协议。
\n由于本人在加密算法上面知识匮乏,就不误人子弟了,有兴趣可以看看百度百科里的资料,SSL,TLS

\n

3.2 SPDY

\n

2012年google提出了SPDY的方案,大家才开始从正面看待和解决老版本HTTP协议本身的问题,SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:
\n降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
\n请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
\nheader压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
\n基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
\n服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。SPDY构成图。

\n
\"\"
\n

3.3 HTTPS报文分析

\n

跟之前的报文分析一样,我们使用wireshark来抓包分析,以在百度上搜索点东西为例。

\n
\"\"
\n

192.168.1.103为本地电脑的ip地址,14.215.177.39为百度服务器地址。下面是步骤:

\n
    \n
  1. 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
  2. \n
  3. 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。之后服务器发送 Certificate 报文。报文中包含公开密钥证书。最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
  4. \n
  5. SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。
  6. \n
  7. 服务器同样发送 Change Cipher Spec 报文。 服务器同样发送 Finished 报文。
  8. \n
  9. 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。 应用层协议通信,即发送 HTTP 响应。
    \n当然,用一张图更容易解释
    \n简单地说就是下面。
  10. \n
\n
\"\"
\n

当我们追踪流的数据的时候,可以看到,基本上都是乱码,经过加密,数据是看不到,如果需要在wireshark上看到,则需要在wireshark中配置ssl。

\n
\"\"
\n

3.4 HTTPS全站化

\n

现今,感觉只要和商业利益有关的,就不得不涉及到加密这类东西。淘宝、京东、唯品会这些电商可谓是最早推行全站https的,这类电商是离用户金钱最近的企业。截止今年底,基本所有商业网站也基本实现了HTTPS。。。。至于小站点,比如个人网站,玩玩还是可以的。如果一个网站需要由HTTP全部变为HTTPS,那么需要关注下面几点:

\n
    \n
  1. CA证书,大部分证书都是需要收费的,当然,自己在服务器上用openssl也可以,不过浏览器会提示当前私密连接不安全这个警告,普通人看到这种信息是不会继续浏览的,所以,想使用HTTPS,可以使用Let's Encrypt,由谷歌等公司推行。
  2. \n
  3. HTTPS性能优化,SSL握手,HTTPS 对速度会有一定程度的降低,但是只要经过合理优化和部署,HTTPS 对速度的影响完全可以接受。
  4. \n
  5. CPU计算压力,HTTPS中大量的秘钥算法计算,对CPU的压力可想而知。
    \n至于我自己的个人网站,之前实现了https,用的免费证书,但是由于HTTPS下的网站,所有子链都要使用HTTPS,使用了七牛云的CDN,如果要使用HTTPS加速,是要收费的,所以只能放弃。。。
  6. \n
\n

四、HTTP2.0

\n

HTTP2.0,相较于HTTP1.x,大幅度的提升了web性能。在与HTTP/1.1完全语义兼容的基础上,进一步减少了网络延迟和传输的安全性。HTTP2.0可以说是SPDY的升级版(基于SPDY设计的),但是依然存在一些不同点:HTTP2.0支持明文传输,而SPDY强制使用HTTPS;HTTP2.0消息头的压缩算法采用HPACK,而非SPDY采用的DEFLATE。

\n

4.1 历史

\n

HTTP 2.0在2013年8月进行首次合作共事性测试。在开放互联网上HTTP 2.0将只用于https://网址,而 http://网址将继续使用HTTP/1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。HTTP 2.0是在SPDY(An experimental protocol for a faster web, The Chromium Projects)基础上形成的下一代互联网通信协议。HTTP/2 的目的是通过支持请求与响应的多路复用来较少延迟,通过压缩HTTPS首部字段将协议开销降低,同时增加请求优先级和服务器端推送的支持。

\n

4.2 HTTP2.0新特性

\n

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

\n

4.2.1 二进制帧

\n

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的Request Body则封装到DATA frame里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

\n
\"\"
\n

4.2.2 多路复用

\n

所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完。

\n

4.2.3 流量控制

\n

TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

\n

4.2.4 服务器端推送

\n

服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

\n
\"\"
\n

4.2.5 首部压缩

\n

HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
\n如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。

\n

4.3 HTTP1.1与HTTP2.0的对比

\n

以访问https://http2.akamai.com/demo为例。

\n
\"\"
\n

4.4 报文

\n

访问https://http2.akamai.com/demo,谷歌浏览器的报文没有显示出协议,此处使用火狐浏览器。
\n响应头部分如下。

\n
\"\"
\n

请求头如下。

\n
\"\"
\n

采用淘宝网站为例,淘宝目前采用主站使用HTTP1.1,资源使用HTTP2.0,少些使用SPDY协议。目前也是业界比较流行的做法。

\n
\"\"
\n

参考

\n
    \n
  1. HTTPS那些事
  2. \n
  3. 如何搭建一个HTTP2.0的网站
  4. \n
  5. HTTP/2.0 相比1.0有哪些重大改进?
  6. \n
  7. HTTP2.0 demo
  8. \n
  9. Http、Https、Http2前身
  10. \n
  11. HTTP报文
  12. \n
  13. HTTP、HTTP2.0、SPDY、HTTPS 你应该知道的一些事
  14. \n
  15. HTTPS权威指南
  16. \n
  17. HTTP2.0的奇妙日常
  18. \n
  19. curl 支持 HTTP2
  20. \n
  21. 淘宝HTTPS探索
  22. \n
  23. HTTPS完全协议详解
  24. \n
\n

欢迎访问我的个人网站。https://wenzhihuai.com

\n", + "image": "https://github-images.wenzhihuai.com/images/20171224034237.png", + "date_published": "2024-02-06T07:05:59.000Z", + "date_modified": "2024-02-14T17:59:53.000Z", + "authors": [], + "tags": [] + }, + { + "title": "高性能高并发高可用的一些思考", + "url": "http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html", + "id": "http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html", + "summary": "高性能高并发高可用的一些思考 TODO待补充 异步 Jmeter JProfiler 火焰图 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar 内存火焰图 缓存 限流熔断 Kafka 序列化 参考 1.Java数据库高并发如何解决 java高并发三种解...", + "content_html": "\n

TODO待补充

\n

异步

\n

Jmeter

\n

JProfiler

\n

火焰图

\n

curl -O https://arthas.aliyun.com/arthas-boot.jar
\njava -jar arthas-boot.jar

\n

内存火焰图

\n

缓存

\n

限流熔断

\n

Kafka

\n

序列化

\n

参考

\n

1.Java数据库高并发如何解决 java高并发三种解决方法 转载
\n2.Java多线程梳理之四_其他并发解决方案
\n3.Java高并发之并发基础

\n", + "date_published": "2024-02-03T17:57:14.000Z", + "date_modified": "2024-03-10T14:39:30.000Z", + "authors": [], + "tags": [] + }, + { + "title": "友链地址", + "url": "http://www.wenzhihuai.com/link/main.html", + "id": "http://www.wenzhihuai.com/link/main.html", + "summary": "友链地址", + "content_html": "\n", + "date_published": "2024-02-02T03:58:59.000Z", + "date_modified": "2024-02-23T07:30:13.000Z", + "authors": [], + "tags": [] + }, + { + "title": "广州图书馆借阅抓取", + "url": "http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html", + "id": "http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html", + "summary": "广州图书馆借阅抓取 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。 搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于...", + "content_html": "\n

欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

\n

搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于我)页面。整个代码采用HttpClient,存储放在MySql,定时使用Spring自带的Schedule,下面是抓取的过程。

\n

1.页面跳转过程

\n

一般都是进入首页http://www.gzlib.gov.cn/,点击进登陆页面,然后输入账号密码。表面上看起来没什么特别之处,实际上模拟登陆的时候不仅仅是向链接post一个请求那么简单,得到的response要么跳回登陆页面,要么无限制重定向。

\n
\n
\"\"
\n
\n

事实上,它做了单点登录,如下图,广州图书馆的网址为:www.gzlib.gov.cn,而登陆的网址为:login.gzlib.gov.cn。原理网上很多人都讲的很好了,可以看看这篇文章SSO单点登录

\n
\n
\"\"
\n
\n

2.处理方法

\n

解决办法不难,只要先模拟访问一下首页即可获取图书馆的session,python的获取代码如:session.get(\"http://www.gzlib.gov.cn/\"),打印cookie之后如下:

\n
[<Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for .gzlib.gov.cn/>, <Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for www.gzlib.gov.cn/>, <Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]
\n

整个登陆抓取的流程如下:

\n
\n
\"\"
\n
\n

即:
\n(1)用户先点击广州图书馆的首页,以获取改网址的session,然后点击登录界面,解析html,获取lt(自定义的参数,类似于验证码),以及单点登录服务器的session。
\n(2)向目标服务器(单点登录服务器)提交post请求,请求参数中包含username(用户名),password(密码),event(时间,默认为submit),lt(自定义请求参数),同时服务端还要验证的参数:refer(来源页面),host(主机信息),Content-Type(类型)。
\n(3)打印response,搜索你自己的名字,如果有则表示成功了,否则会跳转回登陆页面。
\n(4)利用cookie去访问其他页面,此处实现的是对借阅历史的抓取,所以访问的页面是:http://www.gzlib.gov.cn/member/historyLoanList.jspx。

\n

基本的模拟登陆和获取就是这些,之后还有对面html的解析,获取书名、书的索引等,然后封装成JavaBean,再之后便是保存入数据库。(去重没有做,不知道用什么方式比较好)

\n

3.代码

\n

3.1 Java中,一般用来提交http请求的大部分用的都是httpclient,首先,需要导入的httpclient相关的包:

\n
<dependency>\n    <groupId>org.apache.httpcomponents</groupId>\n    <artifactId>httpclient</artifactId>\n    <version>4.5.3</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.httpcomponents</groupId>\n    <artifactId>httpcore</artifactId>\n    <version>4.4.7</version>\n</dependency>
\n

3.2 构建声明全局变量——上下文管理器,其中context为上下文管理器

\n
public class LibraryUtil {\n    private static CloseableHttpClient httpClient = null;\n    private static HttpClientContext context = null;\n    private static CookieStore cookieStore = null;\n    static {\n        init();\n    }\n    private static void init() {\n        context = HttpClientContext.create();\n        cookieStore = new BasicCookieStore();\n        // 配置超时时间\n        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(12000).setSocketTimeout(6000)\n                .setConnectionRequestTimeout(6000).build();\n        // 设置默认跳转以及存储cookie\n        httpClient = HttpClientBuilder.create()\n                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())\n                .setRedirectStrategy(new DefaultRedirectStrategy()).setDefaultRequestConfig(requestConfig)\n                .setDefaultCookieStore(cookieStore).build();\n    }\n    ...
\n

3.3 声明一个get函数,其中header可自定义,此处不需要,但是保留着,做成一个通用的吧。

\n
    public static CloseableHttpResponse get(String url, Header[] header) throws IOException {\n        HttpGet httpget = new HttpGet(url);\n        if (header != null && header.length > 0) {\n            httpget.setHeaders(header);\n        }\n        CloseableHttpResponse response = httpClient.execute(httpget, context);//context用于存储上下文\n        return response;\n    }
\n

3.4 访问首页以获得session,服务器上会话是使用session存储的,本地浏览器使用的是cookie,只要本地不退出,那么使用本地的cookie来访问也是可以的,但是为了达到模拟登陆的效果,这里就不再阐述这种方式。

\n
CloseableHttpResponse homeResponse = get(\"http://www.gzlib.gov.cn/\", null);\nhomeResponse.close();
\n

此时,如果打印cookie,可以看到目前的cookie如下:

\n
<RequestsCookieJar[\n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, \n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, \n<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]>
\n

3.5 访问登陆页面,获取单点登录服务器之后的cookie,解析网页,获取自定义参数lt。这里的解析网页使用了Jsoup,语法和python中的BeautifulSoup中类似。

\n
String loginURL = \"http://login.gzlib.gov.cn/sso-server/login?service=http%3A%2F%2Fwww.gzlib.gov.cn%2Flogin.jspx%3FreturnUrl%3Dhttp%253A%252F%252Fwww.gzlib.gov.cn%252F%26locale%3Dzh_CN&appId=www.gzlib.gov.cn&locale=zh_CN\";\nCloseableHttpResponse loginGetResponse = get(loginURL, null);\nString content = toString(loginGetResponse);\nString lt = Jsoup.parse(content).select(\"form\").select(\"input[name=lt]\").attr(\"value\");\nloginGetResponse.close();
\n

此时,再次查看cookie,多了一个(www.gzlib.gov.cn/sso-server):

\n
<RequestsCookieJar[\n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, \n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, \n<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, \n<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
\n

3.6 声明一个post函数,用来提交post请求,其中提交的参数默认为

\n
    public static CloseableHttpResponse postParam(String url, String parameters, Header[] headers)\n            throws IOException {\n        System.out.println(parameters);\n        HttpPost httpPost = new HttpPost(url);\n        if (headers != null && headers.length > 0) {\n            for (Header header : headers) {\n                httpPost.addHeader(header);\n            }\n        }\n        List<NameValuePair> nvps = toNameValuePairList(parameters);\n        httpPost.setEntity(new UrlEncodedFormEntity(nvps, \"UTF-8\"));\n        CloseableHttpResponse response = httpClient.execute(httpPost, context);\n        return response;\n    }
\n

3.7 登陆成功后,如果没有声明returnurl,即登录链接为(http://login.gzlib.gov.cn/sso-server/login),那么只是会显示成功登录的页面:

\n
\n
\"\"
\n
\n

后台应该是定义了一个service用来进行链接跳转的,想要获取登录成功之后的跳转页面可修改service之后的链接,这里将保持原始状态。此时,查看cookie结果如下:

\n
<RequestsCookieJar[\n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, \n<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, \n<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, \n<Cookie CASTGC=TGT-198235-zkocmYyBP6c9G7EXjKyzgKR7I40QI4JBalTkrnr9U6ZkxuP6Tn for www.gzlib.gov.cn/sso-server>, \n<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
\n

其中,出现CASTGC表明登陆成功了,可以使用该cookie来访问广州图书馆的其他页面,在python中是直接跳转到其他页面,而在java使用httpclient过程中,看到的并不是直接的跳转,而是一个302重定向,打印Header之后结果如下图:

\n
\n
\"\"
\n
\n

认真研究一下链接,就会发现服务器相当于给了一张通用票ticket,即:可以使用该ticket访问任何页面,而returnUrl则是返回的页面。这里我们直接访问该重定向的url。

\n
Header header = response.getHeaders(\"Location\")[0];\nCloseableHttpResponse home = get(header.getValue(), null);
\n

然后打印页面,即可获取登陆之后跳回的首页。

\n

3.8 解析html
\n获取session并跳回首页之后,再访问借阅历史页面,然后对结果进行html解析,python中使用了BeautifulSoup,简单而又实用,java中的jsoup也是一个不错的选择。

\n
        String html = getHTML();\n        Element element = Jsoup.parse(html).select(\"table.jieyue-table\").get(0).select(\"tbody\").get(0);\n        Elements trs = element.select(\"tr\");\n        for (int i = 0; i < trs.size(); i++) {\n            Elements tds = trs.get(i).select(\"td\");\n            System.out.println(tds.get(1).text());\n        }
\n

输出结果:

\n
企业IT架构转型之道\n大话Java性能优化\n深入理解Hadoop\n大话Java性能优化\nJava EE开发的颠覆者:Spring Boot实战\n大型网站技术架构:核心原理与案例分析\nJava性能权威指南\nAkka入门与实践\n高性能网站建设进阶指南:Web开发者性能优化最佳实践:Performance best practices for Web developers\nJava EE开发的颠覆者:Spring Boot实战\n深入理解Hadoop\n大话Java性能优化
\n

点击查看源码

\n

总结

\n

目前,改代码已经整合进个人网站之中,每天定时抓取一次,但是仍有很多东西没有做(如分页、去重等),有兴趣的可以研究一下源码,要是能帮忙完善就更好了。感谢Thanks♪(・ω・)ノ。整个代码接近250行,当然...包括了注释,但是使用python之后,也不过25行=w=,这里贴一下python的源码吧。同时,欢迎大家访问我的个人网站,也欢迎大家能给个star

\n
import urllib.parse\nimport requests\nfrom bs4 import BeautifulSoup\n\nsession = requests.session()\nsession.get(\"http://www.gzlib.gov.cn/\")\nsession.headers.update(\n    {\"Referer\": \"http://www.gzlib.gov.cn/member/historyLoanList.jspx\",\n     \"origin\": \"http://login.gzlib.gov.cn\",\n     'Content-Type': 'application/x-www-form-urlencoded',\n     'host': 'www.gzlib.gov.cn',\n     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'\n     }\n)\nbaseURL = \"http://login.gzlib.gov.cn/sso-server/login\"\nsoup = BeautifulSoup(session.get(baseURL).text, \"html.parser\")\nlt = soup.select(\"form\")[0].find(attrs={'name': 'lt'})['value']\npostdict = {\"username\": \"你的身份证\",\n            \"password\": \"密码(默认为身份证后6位)\",\n            \"_eventId\": \"submit\",\n            \"lt\": lt\n            }\npostdata = urllib.parse.urlencode(postdict)\nsession.post(baseURL, postdata)\nprint(session.get(\"http://www.gzlib.gov.cn/member/historyLoanList.jspx\").text)
\n
", + "image": "https://github-images.wenzhihuai.com/images/20171013042700.png", + "date_published": "2024-01-30T06:37:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "AOP", + "url": "http://www.wenzhihuai.com/java/SpringBoot/aop.html", + "id": "http://www.wenzhihuai.com/java/SpringBoot/aop.html", + "summary": "AOP 一、概述 在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会...", + "content_html": "\n

一、概述

\n

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

\n
\"\"
\n

Spring自2.0版本开始采用@AspectJ注解非常容易的定义一个切面。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。

\n

1.1 特点

\n

AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。AOP是OOP的补充,是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP可以分为静态织入与动态织入,静态织入即在编译前将需织入内容写入目标模块中,这样成本非常高。动态织入则不需要改变目标模块。Spring框架实现了AOP,使用注解配置完成AOP比使用XML配置要更加方便与直观。

\n

1.2 AOP概述

\n

Aspect:一个模块用来关注多个类的切面。在JAVA EE的应用中,事务是AOP的典型例子。
\nJoinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
\nPointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
\nAdvice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
\nIntroduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.
\nTarget(目标对象):代理的目标对象
\nWeaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.spring采用动态代理织入,而AspectJ采用编译期织入和类装在期织入.
\nProxy(代理):一个类被AOP织入增强后,就产生一个结果代理类Aspect(切面): 是切入点和通知(引介)的结合

\n

二、Spring中的AOP

\n

Spring实现AOP主要是由IOC容器来负责生成、管理的。其创建的方式有两种:

\n
    \n
  1. 默认使用Java动态代理来创建AOP代理;
  2. \n
  3. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。高版本的Spring会自动选择是使用动态代理还是CGLIB生成代理内容,当然我们也可以强制使用CGLIB生成代理,那就是aop:config里面有一个\"proxy-target-class\"属性,这个属性值如果被设置为true,那么基于类的代理将起作用。
  4. \n
\n

2.1 AspectJ支持5种类型的通知注解:

\n

[1] Before:前置通知,在方法执行之前执行
\n[2] After:后置通知,在方法执行之后执行
\n[3] AfterRunning:返回通知,在方法返回结果之后执行
\n[4] AfterThrowing:异常通知,在方法抛出异常之后执行
\n[5] Around:环绕通知,围绕着方法执行
\n其中,环绕通知是最常见的一种通知注解,特别是在缓存的使用中,例如:Spring-Cache中的使用,在service的方法中添加一个cache的注解,通过AOP来拦截,如果缓存中已经存在,则直接返回结果,如果没有,再进行service的访问。

\n

2.2 Spring提供了4种实现AOP的方式:

\n
    \n
  1. 经典的基于代理的AOP
  2. \n
  3. @AspectJ注解驱动的切面
  4. \n
  5. 纯POJO切面
  6. \n
  7. 注入式AspectJ切面
  8. \n
\n

三、原理概述

\n

Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现。Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入,这点了解一下即可。

\n
\"\"
\n

具体的原理请看Spring AOP

\n

四、使用

\n

网上看别人写了很多入门的例子,自己就不再阐述了,毕竟自己还是菜,下面是关于AOP入门的资料:
\n我们为什么要使用AOP?
\nSpring中AOP的实现
\n关于AOP

\n

下面是自己在个人网站中的使用,主要是用来统计一个方法的执行消耗了多少时间,需要引入aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar的包。

\n

4.1 在Spring MVC中开启AOP

\n
    <!--自动扫描自定义切面-->\n    <aop:aspectj-autoproxy/>
\n

4.2 定义一个切面

\n
/**\n * 可以使用 @Order 注解指定切面的优先级, 值越小优先级越高\n */\n@Order(2)\n@Aspect\n@Component\npublic class TimeInterceptor {\n}
\n

4.3 声明一个切入点

\n
    @Pointcut(\"execution(* com.myblog.service.impl.BlogServiceImpl.*(..))\")\n    public void pointcut() {\n    }
\n

4.4 声明一个前置切点

\n
    @Before(\"pointcut()\")\n    public void before(JoinPoint jp) {\n        logger.info(jp.getSignature().getName());\n        logger.info(\"
\n
", + "image": "https://github-images.wenzhihuai.com/images/20180118085015.png", + "date_published": "2024-01-30T06:37:05.000Z", + "date_modified": "2024-01-30T06:53:42.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Spring Boot Prometheus使用", + "url": "http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html", + "id": "http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html", + "summary": "Spring Boot Prometheus使用 一、基本原理 Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exp...", + "content_html": "\n

一、基本原理

\n

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

\n
\"image-20240127202704612\"
image-20240127202704612
\n

二、具体过程

\n
    \n
  • Prometheus Daemon负责定时去目标上抓取metrics(指标)数据,每个抓取目标需要暴露一个http服务的接口给它定时抓取。Prometheus支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup等方式指定抓取目标。Prometheus采用PULL的方式进行监控,即服务器可以直接通过目标PULL数据或者间接地通过中间网关来Push数据。
  • \n
  • Prometheus在本地存储抓取的所有数据,并通过一定规则进行清理和整理数据,并把得到的结果存储到新的时间序列中。
  • \n
  • Prometheus通过PromQL和其他API可视化地展示收集的数据。Prometheus支持很多方式的图表可视化,例如Grafana、自带的Promdash以及自身提供的模版引擎等等。Prometheus还提供HTTP API的查询方式,自定义所需要的输出。
  • \n
  • PushGateway支持Client主动推送metrics到PushGateway,而Prometheus只是定时去Gateway上抓取数据。
  • \n
  • Alertmanager是独立于Prometheus的一个组件,可以支持Prometheus的查询语句,提供十分灵活的报警方式。
  • \n
\n

三、pull模式(prometheus主动拉取)

\n
<dependency>\n    <groupId>org.springframework.boot</groupId>\n    <artifactId>spring-boot-starter-actuator</artifactId>\n</dependency>\n<dependency>\n    <groupId>io.micrometer</groupId>\n    <artifactId>micrometer-registry-prometheus</artifactId>\n</dependency>
\n
management:\n  endpoints:\n    web:\n      exposure:\n        include: prometheus\n  metrics:\n    tags:\n      application: ${spring.application.name}
\n

之后查看/actuator/prometheus就可以看到

\n
\"image-20240127202722776\"
image-20240127202722776
\n

腾讯云上面有个prometheus的服务,接入云原生监控还要配置一个Servicemonitor、PodMonitor等,详细的可以访问腾讯云的官方文档(https://cloud.tencent.com/document/product/1416/56031)

\n

四、主动上报(pushgateway)

\n

大部分现实场景中,如果每增加一个服务,都需要开发去配置的话,不仅沟通成本高,也导致因配错而起的运维成本很高,采用主动上报方式比较简单,方便规避一些问题。

\n

(1)引入库

\n

主动上报除了需要引入spring-boot-starter-actuator、micrometer-registry-prometheus,还要引入simpleclient_pushgateway,三个都是必须的,少一个都不行。

\n
        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-registry-prometheus</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.prometheus</groupId>\n            <artifactId>simpleclient_pushgateway</artifactId>\n        </dependency>
\n

(2)修改配置文件

\n

之后,为了安全考虑,需要把endpoints的所有信息都关闭掉,免得被泄露出去。

\n
management:\n  endpoints:\n    #一定要设为false,防止不小心暴露/actuator相关接口触发安全工单\n    enabled-by-default: false\n  metrics:\n    tags:\n      #要设置应用的名称,否则会聚合到别的项目里面去\n      application: ${spring.application.name}\n    export:\n      prometheus:\n        pushgateway:\n          base-url: http://xxxxx\n          push-rate: 20s\n          job: ${spring.application.name}\n          enabled: true\n          username: xxxx\n          password: xxx\n          grouping-key:\n            instance: ${HOSTNAME}
\n

最好多看一眼/actuator,确认一下是否已经关闭endpoints

\n
\"image-20240127202830908\"
image-20240127202830908
\n

(3)抓包看一下metrics(可跳过)

\n

大致看了下 simpleclient_pushgateway和spring-boot-starter-actuator的源码,并没有对http的请求发起日志,调试的时候都不知道是不是正常上报过去了,只能采取抓包来研究下。

\n
\"image-20240127202853218\"
image-20240127202853218
\n
\"image-20240127202944264\"
image-20240127202944264
\n

平均间隔5s左右,符合配置文件里的设置。

\n
\"image-20240127203001776\"
image-20240127203001776
\n

五、Grafana

\n

对于java来说,常用的dashboard是https://grafana.com/grafana/dashboards/4701,也可以用spring boot的https://grafana.com/grafana/dashboards/6756

\n
\"image-20240127203024477\"
image-20240127203024477
\n

六、自定义监控上报

\n

除了通用的封装好的指标之外,也可以自定义prometheus的监控。对于Spring Boot来说,只要如下代码即可实现:

\n
    @Resource\n    private MeterRegistry meterRegistry;\n\n    public void report() {\n        meterRegistry.counter(\"指标\", \"tag的名称\", \"tag的值\").increment();\n    }
\n

也可以通过抓包来查看,以及在push-gateway上看到。

\n", + "image": "https://github-images.wenzhihuai.com/images/image-20240127202704612.png", + "date_published": "2024-01-27T13:52:01.000Z", + "date_modified": "2024-01-27T13:52:01.000Z", + "authors": [], + "tags": [] + }, + { + "title": "小程序", + "url": "http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html", + "id": "http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html", + "summary": "小程序 助眠风扇助眠风扇 MyTesMateMyTesMate", + "content_html": "\n
\"助眠风扇\"
助眠风扇
\n
\"MyTesMate\"
MyTesMate
\n", + "image": "https://github-images.wenzhihuai.com/github/gh_89030789780b_344.jpg", + "date_published": "2024-01-27T09:51:43.000Z", + "date_modified": "2024-01-27T09:51:43.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Webflux", + "url": "http://www.wenzhihuai.com/java/SpringBoot/webflux.html", + "id": "http://www.wenzhihuai.com/java/SpringBoot/webflux.html", + "summary": "Webflux 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? ChatGpt: Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。 Servlet 3.1:虽然 Servlet 3.1 支持非阻塞...", + "content_html": "\n

1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux?

\n

ChatGpt:
\nServlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

\n

Servlet 3.1:虽然 Servlet 3.1 支持非阻塞 IO,但是它的 API 设计仍然是基于传统的阻塞模型,这意味着每个请求通常会在一个单独的线程中处理,直到请求处理完成并返回响应。这种模型在并发请求数量较少的情况下工作得很好,但是在高并发的情况下,可能会因为线程数量过多而导致资源消耗过大,性能下降。

\n

WebFlux:WebFlux 是基于反应式编程模型的,它使用少量的线程(通常与 CPU 核心数相同)和事件驱动的方式来处理大量的并发请求。这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

\n

所以,虽然 Servlet 3.1 和 WebFlux 底层都使用了 NIO,但是由于他们的编程模型和如何使用 NIO 的方式不同,WebFlux 在处理高并发、流式数据等场景时,可能会有更好的性能。但是,这并不意味着 WebFlux 在所有情况下都比 Servlet 3.1 有更好的性能,具体还需要根据应用的实际需求和场景来选择合适的技术。

\n

webflux和springboot虚拟线程对比

\n

ChatGpt:
\nWebFlux 和 Spring Boot 使用虚拟线程(也称为纤程或轻量级线程)是两种不同的并发模型,它们各自有各自的优点和适用场景。

\n

WebFlux:WebFlux 是 Spring 5 的一部分,它提供了一个基于反应式编程模型的非阻塞 Web 框架。WebFlux 使用少量的线程和事件驱动的方式来处理大量的并发请求,这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

\n

Spring Boot 使用虚拟线程:Java 在其后续版本(如 Java 16)中引入了虚拟线程(Project Loom)的概念,这是一种新的并发模型,它可以创建大量的虚拟线程,每个虚拟线程都有自己的调用栈,但是它们在操作系统级别并不是真正的线程,因此创建和切换的成本更低。虚拟线程使得开发者可以使用传统的阻塞式编程模型,但是在运行时,这些阻塞操作并不会阻塞真正的物理线程,从而可以处理大量的并发请求。

\n

WebFlux 和 Spring Boot 使用虚拟线程的主要区别在于它们的编程模型:WebFlux 使用反应式编程模型,而虚拟线程则使用更传统的阻塞式编程模型。虚拟线程的优点是编程模型更简单,更接近传统的 Java 开发方式,但是它需要更新的 Java 版本,并且目前还在实验阶段。WebFlux 的优点是它可以在任何支持 Java 8 或更高版本的环境中运行,但是它需要开发者熟悉反应式编程模型。

\n

至于哪种模型的性能更好,这取决于具体的应用场景和负载。在某些情况下,WebFlux 可能会有更好的性能,而在其他情况下,使用虚拟线程可能会更有效。

\n", + "date_published": "2024-01-27T09:51:43.000Z", + "date_modified": "2024-01-27T09:51:43.000Z", + "authors": [], + "tags": [] + }, + { + "title": "StarCraft Ⅱ 人工智能教程", + "url": "http://www.wenzhihuai.com/interesting/starcraft-ai.html", + "id": "http://www.wenzhihuai.com/interesting/starcraft-ai.html", + "summary": "StarCraft Ⅱ 人工智能教程 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。 一、其他的太抽象了,先讲人机对战吧 sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的...", + "content_html": "\n

非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

\n

一、其他的太抽象了,先讲人机对战吧

\n

sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的就是,需要以管理员身份运行,下面详细过程,翻译自 ProBots 2021 Season 1 - Human vs Bot

\n

1.安装星际争霸2,地址,至于要不要下载国际服,似乎没有必要
\n2.下载ProBots vs Humans.Zip
\n3.解压,附带了地图,主要是sc2aiapp
\n\"\"

\n

4.可选,下载相关地图,可以从竞技场里下,需要放到星际争霸2的目录下,mac的是/Applications/StarCraft II/Maps
\n5.打开步骤2的目录
\n6.打开sc2aiapp,打开的时候有可能报错:

\n
\"\"
\n

右键sc2aiapp,以管理员身份运行即可,现在不让注册了,直接continue without login

\n
\"\"
\n
\"\"
\n

7.全屏快捷键,Alt + Enter,进行对战

\n

我这录制了个我对战的视频,bilibili,感觉AI在对战里很容易只有一样打法,据说是强化训练后的最优选导致的,这个也不知怎么整,个人感觉MicroMachine这个AI打法稍微多样,可以多和它对战下。

\n

二、AI天梯

\n

目前没有看到什么办法让暴雪允许AI在实际的天梯上进行运行,但社区搞了个专门的AI天梯,sc2ai,可以将代码上传到里面进行对战,实时流我没看到,对战完后可以下载replay复盘。下面讲下如何上传代码进行对战。
\n1.第一步肯定是先要注册登录
\n2.u

\n
\"\"
\n

3.主要是这个Bot zip,基本的代码架构还是要固定的

\n
\"\"
\n

具体可以看下sc2-api-simple-bot这里,记得把它打包即可
\n4.成功之后,即可从profile里看到自己的机器人
\n\"\"

\n

5.此时,bot是不会进行比赛,需要参赛,点击Competitions,然后选择赛季
\n\"\"

\n
\"\"
\n

6.比赛是随机的放到队列里的,可能需要排队进行比赛,也可能主动申请和具体的机器人进行比赛,点击Request Match,进行申请比赛。
\n\"\"

\n

7.慢慢等待,比赛结束之前都看不到结果的,也没有实时流进行查看的,结束之后就可以看到结果以及下载replay。其中arena会随机的进行一些比赛,也有可能是别人随机选的,一个bot一天大概能有50场比赛,arena也会提供统计,胜率、ELO(分数)等

\n

Bot开发样例

\n

https://github.com/Zephery/sc2-api-simple-bot.git
\nhttps://community.eschamp.com/t/simple-starcraft-2-bot-template-to-get-started/155

\n", + "image": "https://github-images.wenzhihuai.com/images/755525-20211220223200207-5235039.png", + "date_published": "2024-01-26T16:53:43.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "自我介绍", + "url": "http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html", + "id": "http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html", + "summary": "自我介绍 安安静静的开发,搞点好玩的。 硅谷硅谷", + "content_html": "\n

安安静静的开发,搞点好玩的。

\n
\"硅谷\"
硅谷
\n", + "image": "https://github-images.wenzhihuai.com/images/webp.jpg", + "date_published": "2024-01-26T04:53:47.000Z", + "date_modified": "2024-02-03T17:57:14.000Z", + "authors": [], + "tags": [] + }, + { + "title": "关于我", + "url": "http://www.wenzhihuai.com/about-the-author/", + "id": "http://www.wenzhihuai.com/about-the-author/", + "summary": "关于我 留空", + "content_html": "\n

留空

\n", + "date_published": "2024-01-25T17:34:45.000Z", + "date_modified": "2024-01-25T17:34:45.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Zookeeper", + "url": "http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html", + "id": "http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html", + "summary": "Zookeeper 留空", + "content_html": "\n

留空

\n", + "date_published": "2024-01-25T17:34:45.000Z", + "date_modified": "2024-01-25T17:34:45.000Z", + "authors": [], + "tags": [] + }, + { + "title": "微信公众号-chatgpt智能客服搭建", + "url": "http://www.wenzhihuai.com/interesting/chatgpt.html", + "id": "http://www.wenzhihuai.com/interesting/chatgpt.html", + "summary": "微信公众号-chatgpt智能客服搭建 想体验的可以去微信上搜索【旅行的树】公众号。 一、ChatGPT注册 1.1 短信手机号申请 openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。 国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。 国外手机号,没有的话也可以去https:/...", + "content_html": "\n

想体验的可以去微信上搜索【旅行的树】公众号。

\n

一、ChatGPT注册

\n

1.1 短信手机号申请

\n

openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。

\n

国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。

\n

国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。

\n
\"image-20230220172107296\"
\n

之后再搜索框填openai进行下单购买即可。

\n
\"img\"
\n

1.2 云服务器申请

\n

openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。

\n\"image-20230221193455384\"\n

1.3 ChatGPT注册

\n

购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码

\n
\"image-20230222104357230\"
\n

然后回sms查看验证码。

\n
\"image-20230222104225722\"
\n

注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。

\n\"image-20230220173335691\"\n

通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。

\n
\"image-20230222104316514\"
\n

二、搭建nginx服务器

\n

跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。

\n
Do not share your API key with interesting, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.
\n

windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。

\n
    location /v1/completions {\n      proxy_pass https://api.openai.com/v1/completions;\n    }
\n

然后使用下面的方法进行调试即可:

\n
POST http://YOUR IP/v1/completions\nAuthorization: Bearer YOUR_API_KEY\nContent-Type: application/json\n\n{\n  \"model\": \"text-davinci-003\",\n  \"prompt\": \"Say this is a test\",\n  \"max_tokens\": 7,\n  \"temperature\": 0\n}
\n

三、公众号开发

\n

网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。

\n

3.1 微信云托管

\n

微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。

\n
\"image-20230220192603751\"
\n

提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。

\n\"image-20230220201004069\"\n

3.2 一个简单ChatGPT简单回复

\n

微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。

\n

一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。

\n
const configuration = new Configuration({\n    apiKey: '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',\n    basePath: 'http://43.153.15.174/v1'\n});\nconst openai = new OpenAIApi(configuration);\n\nasync function simpleResponse(prompt) {\n    const completion = await openai.createCompletion({\n        model: 'text-davinci-003',\n        prompt,\n        max_tokens: 1024,\n        temperature: 0.1,\n    });\n    const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();\n    return strip(response, ['\\n', 'A: ']);\n}\n\napp.post(\"/message/simple\", async (req, res) => {\n    console.log('消息推送', req.body)\n    // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用\n    const appid = req.headers['x-wx-from-appid'] || ''\n    const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body\n    console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)\n    if (MsgType === 'text') {\n        message = await simpleResponse(Content)\n        res.send({\n            ToUserName: FromUserName,\n            FromUserName: ToUserName,\n            CreateTime: CreateTime,\n            MsgType: 'text',\n            Content: message,\n        })\n    } else {\n        res.send('success')\n    }\n})
\n

本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。

\n

3.3 服务部署

\n

提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。

\n\"image-20230220203313790\"\n

部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。

\n
\"image-20230220203711436\"
\n

这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:

\n

(1)服务有没有正常启动,看日志

\n

(2)端口有没有设置错误,这个很多次没有注意到

\n\"image-20230220203445297\"\n

保存成功之后,就可以在微信公众号里测试了。

\n
\"image-20230221134250689\"
\n

体验还可以

\n

四、没有回复(超时回复问题)

\n

很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。

\n

订阅号的消息推送分几种:

\n
    \n
  1. 被动消息回复:指用户给公众号发一条消息,系统接收到后,可以回复一条消息。
  2. \n
  3. 主动回复/客服消息:可以脱离被动消息的5秒超时权限,在48小时内可以主动回复。但需要公众号完成微信认证。
  4. \n
\n

根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html

\n\"image-20230221100251825\"\n

对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。

\n

1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。

\n
  // 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。\n  await Message.create({\n    fromUser: FromUserName,\n    response: '',\n    request: Content,\n    aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画\n  });
\n

2.抽象一个 chatGPT 请求方法 getAIMessage,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。

\n
 // 成功后,更新记录\n  await Message.update(\n    {\n      response: response,\n      status: MESSAGE_STATUS_ANSWERED,\n    },\n    {\n      where: {\n        fromUser: FromUserName,\n        request: Content,\n      },\n    },\n  );
\n

3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。

\n
  // 找一下,是否已有记录\n  const message = await Message.findOne({\n    where: {\n      fromUser: FromUserName,\n      request: Content,\n    },\n  });\n\n  // 已回答,直接返回消息\n  if (message?.status === MESSAGE_STATUS_ANSWERED) {\n    return `[GPT]: ${message?.response}`;\n  }\n\n  // 在回答中\n  if (message?.status === MESSAGE_STATUS_THINKING) {\n    return AI_THINKING_MESSAGE;\n  }
\n

4.最后就是一个 Promise.race

\n
  const message = await Promise.race([\n    // 3秒微信服务器就会超时,超过2.9秒要提示用户重试\n    sleep(2900).then(() => AI_THINKING_MESSAGE),\n    getAIMessage({ Content, FromUserName }),\n  ]);
\n

这样子大概就能实现超时之前返回了。

\n

五、会话保存

\n

掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。

\n
async function buildCtxPrompt({ FromUserName }) {\n  // 获取最近10条对话\n  const messages = await Message.findAll({\n    where: {\n      fromUser: FromUserName,\n      aiType: AI_TYPE_TEXT,\n    },\n    limit: 10,\n    order: [['updatedAt', 'ASC']],\n  });\n  // 只有一条的时候,就不用封装上下文了\n  return messages.length === 1\n    ? messages[0].request\n    : messages\n        .map(({ response, request }) => `Q: ${request}\\n A: ${response}`)\n        .join('\\n');\n}
\n

之后就可以实现会话之间的保存通信了。

\n
\"image-20230218203309437\"
\n

六、其他问题

\n

6.1 限频

\n

chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。

\n

对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。

\n

6.2 秘钥key被更新

\n

没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。

\n

6.3 为啥和官网的回复不一样

\n

我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧

\n\"image-20230221192417900\"\n

从官方文档来看,官方服务版的 ChatGPT 的模型并非基础版的text-davinci-003,而是经过了「微调:fine-tunes」。文档地址在这:platform.openai.com/docs/guides…

\n

6.4 玄学挂掉

\n

有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。

\n\"image-20230221175150025\"\n

参考:https://juejin.cn/post/7200769439335546935

\n

最后

\n

记得去微信关注【旅行的树】公众号体验
\n代码地址:https://github.com/Zephery/wechat-gpt

\n", + "date_published": "2024-01-25T12:42:18.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Tesla api", + "url": "http://www.wenzhihuai.com/interesting/tesla.html", + "id": "http://www.wenzhihuai.com/interesting/tesla.html", + "summary": "Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator. image-20231210142130054 image-2023121014241559...", + "content_html": "\n

一、特斯拉应用申请

\n

1.1 创建 Tesla 账户

\n

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

\n

正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.

\n\"image-20231210142130054\"\n\"image-20231210142415598\"\n

注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。

\n

1.2 提交访问请求

\n

点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。

\n

这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑

\n

(1)Invalid domain

\n\"image-20231210144504486\"\n

无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。

\n

(2)Unable to Onboard

\n\"image-20231210142930976\"\n

应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。

\n

(3) Rejected

\n\"image-20231210143156308\"\n

这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。

\n

1.3 访问应用程序凭据

\n

一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。

\n

申请好就可以在自己的账号下看到自己的应用了,

\n\"image-20231210144244888\"\n

1.4 开始 API 集成

\n

按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。

\n

二、开发之前的准备

\n

由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。

\n

2.1 认识token

\n

app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:

\n

(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌

\n

(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。

\n

此外,还需要注意下,中国大陆地区对应的api地址是 https://fleet-api.prd.cn.vn.cloud.tesla.cn,不要调到别的地址去了。

\n

2.2 获取第三方应用token

\n

这一步官网列的很详细,就不在详述了。

\n
CLIENT_ID=<command to obtain a client_id>\nCLIENT_SECRET=<secure command to obtain a client_secret>\nAUDIENCE=\"https://fleet-api.prd.na.vn.cloud.tesla.com\"\n# Partner authentication token request\ncurl --request POST \\\n  --header 'Content-Type: application/x-www-form-urlencoded' \\\n  --data-urlencode 'grant_type=client_credentials' \\\n  --data-urlencode \"client_id=$CLIENT_ID\" \\\n  --data-urlencode \"client_secret=$CLIENT_SECRET\" \\\n  --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \\\n  --data-urlencode \"audience=$AUDIENCE\" \\\n  'https://auth.tesla.com/oauth2/v3/token'
\n

2.3 验证域名归属

\n

2.1.1 Register

\n

完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。

\n
cd vehicle-command/cmd/tesla-keygen\ngo build .\n./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem
\n

这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。

\n
    location ~ ^/.well-known {\n           default_type text/html;\n           alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;\n    }\n
\n

随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。

\n
curl --header 'Content-Type: application/json' \\\n  --header \"Authorization: Bearer $TESLA_API_TOKEN\" \\\n  --data '{\"domain\":\"string\"}' \\\n  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'
\n

2.1.2 public_key

\n

最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)

\n
curl --header 'Content-Type: application/json' \\\n  --header \"Authorization: Bearer $TESLA_API_TOKEN\" \\\n  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'
\n

得到下面内容就代表成功了

\n
{\"response\":{\"public_key\":\"xxxx\"}}
\n

至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。

\n", + "date_published": "2024-01-25T12:42:18.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "在 Spring 6 中使用虚拟线程", + "url": "http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html", + "id": "http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html", + "summary": "在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。 ...", + "content_html": "\n

一、简介

\n

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

\n

虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。

\n

首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。

\n

二、 虚拟线程与平台线程

\n

主要区别在于虚拟线程在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。

\n

对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。

\n

从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。

\n

三、在Spring 6中使用虚拟线程

\n

从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的预览功能。这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下代码

\n
<build>\n    <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <configuration>\n                <source>19</source>\n                <target>19</target>\n                <compilerArgs>\n                    --enable-preview\n                </compilerArgs>\n            </configuration>\n        </plugin>\n    </plugins>\n</build>
\n

从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:

\n
@EnableAsync\n@Configuration\n@ConditionalOnProperty(\n  value = \"spring.thread-executor\",\n  havingValue = \"virtual\"\n)\npublic class ThreadConfig {\n    @Bean\n    public AsyncTaskExecutor applicationTaskExecutor() {\n        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());\n    }\n\n    @Bean\n    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {\n        return protocolHandler -> {\n            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());\n        };\n    }\n}
\n

第一个 Spring Bean ApplicationTaskExecutor将取代标准的*ApplicationTaskExecutor* ,提供为每个任务启动新虚拟线程的Executor。第二个 bean,名为ProtocolHandlerVirtualThreadExecutorCustomizer,将以相同的方式 自定义标准TomcatProtocolHandler 。我们还添加了注释@ConditionalOnProperty,**以通过切换application.yaml文件中配置属性的值来按需启用虚拟线程:

\n
spring:\n    thread-executor: virtual\n    //...
\n

我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:

\n
@RestController\n@RequestMapping(\"/thread\")\npublic class ThreadController {\n    @GetMapping(\"/name\")\n    public String getThreadName() {\n        return Thread.currentThread().toString();\n    }\n}
\n

*Thread*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个curl请求来访问这个端点:

\n
$ curl -s http://localhost:8080/thread/name\n$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
\n

正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。

\n

四、性能比较

\n

对于此负载测试,我们将使用JMeter。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。

\n

在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:

\n
@RestController\n@RequestMapping(\"/load\")\npublic class LoadTestController {\n\n    private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);\n\n    @GetMapping\n    public void doSomething() throws InterruptedException {\n        LOG.info(\"hey, I'm doing something\");\n        Thread.sleep(1000);\n    }\n}
\n

请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换

\n

JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:

\n
\"image-20230827193431807\"
image-20230827193431807
\n

在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:

\n
\"image-20230827193511176\"
image-20230827193511176
\n

发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。

\n

让我们看看虚拟线程会发生什么:

\n
\"image-20230827193533565\"
image-20230827193533565
\n

正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。

\n

**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。

\n", + "image": "https://github-images.wenzhihuai.com/images/755525-20230827193604936-1596575555.png", + "date_published": "2024-01-25T12:42:18.000Z", + "date_modified": "2024-01-26T03:58:56.000Z", + "authors": [], + "tags": [] + }, + { + "title": "【elasticsearch】源码debug", + "url": "http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html", + "id": "http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html", + "summary": "【elasticsearch】源码debug 一、下载源代码 直接用idea下载代码https://github.com/elastic/elasticsearch.git image 切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好 image 二、修改设置(可选) ...", + "content_html": "\n

一、下载源代码

\n

直接用idea下载代码https://github.com/elastic/elasticsearch.git
\n\"image\"

\n

切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
\n\"image\"

\n

二、修改设置(可选)

\n

为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:

\n

setting 'xpack.security.enabled', 'false'

\n

或者使用其中的用户名密码:

\n

user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'

\n

三、启动

\n

先启动上面的 remote debug, 然后用 gradlew 启动项目:

\n

./gradlew :run --debug-jvm
\n打开浏览器http://localhost:9200即可看到es相关信息了
\n\"image\"

\n", + "image": "https://github-images.wenzhihuai.com/images/755525-20220124160719006-851383635.png", + "date_published": "2024-01-25T12:42:18.000Z", + "date_modified": "2024-01-26T03:58:56.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Database", + "url": "http://www.wenzhihuai.com/database/", + "id": "http://www.wenzhihuai.com/database/", + "summary": "Database 目录留空", + "content_html": "\n

目录留空

\n", + "date_published": "2024-01-25T11:42:16.000Z", + "date_modified": "2024-02-03T17:57:14.000Z", + "authors": [], + "tags": [] + }, + { + "title": "微信支付", + "url": "http://www.wenzhihuai.com/donate/", + "id": "http://www.wenzhihuai.com/donate/", + "summary": "微信支付 微信支付微信支付", + "content_html": "\n
\"微信支付\"
微信支付
\n", + "image": "https://github-images.wenzhihuai.com/images/wechat-pay.png", + "date_published": "2024-01-25T08:04:11.000Z", + "date_modified": "2024-01-26T17:18:59.000Z", + "authors": [], + "tags": [] + }, + { + "title": "10.历时8年最终改版", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html", + "summary": "10.历时8年最终改版 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。...", + "content_html": "\n

不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

\n
\"早年的网站截图\"
早年的网站截图
\n

总体而言,自建站对学习知识,了解整个建站的原理能够起到非常重要的作用,但是维护成本实在是太高了,每个月要支付服务器的费用,而且一旦想拿服务器来做点什么,都得提防一下会不会造成破坏。最终还是选择采用vuepress2来重构一下自建站,毕竟把markdown放到github,把图片放到cos里减少了不少的维护量。下面是使用vuepress2建站的代码地址

\n

一、博客的安装

\n

具体vuepress2官网讲解的很详细了,不用再处理什么,按照步骤创建一个项目即可,为了网站的美观,个人使用了theme hope这款主题。

\n

二、配置

\n

导航栏、侧边栏官网也有详细的讲解,也不再阐述,需要注意的是自动目录,之前看JavaGuide的样式,他那边的每篇文章都需要写一次ts文件(children),后来发现官网可以把children设置为structure,即可实现根据md文件生成侧边栏目录。注意的是,这里不是根据markdown的文件名来目录名,而是取markdown文件的标题。

\n
    {\n        text: \"Redis\",\n        prefix: \"redis/\",\n        icon: \"redis\",\n        collapsible: false,\n        children: \"structure\"\n    },
\n

三、为文章增加评论

\n

vuepress-plugin-comment2,使用了Giscus,Giscus绑定了github账号,所以可以从一定程度上防止被别人刷广告,需要再个人的项目Settings->General把Discussions这个选项给勾选上。

\n
\"一定要开启discussions\"
一定要开启discussions
\n

然后去config.ts配置插件。

\n
        commentPlugin({\n            provider: \"Giscus\",\n            comment: true, //启用评论功能\n            repo: \"Zephery/MyWebsite\", //远程仓库\n            repoId: \"MDEwOlJlcG9zaXRvcnkyMDM2MDIyMDQ=\", //对应自己的仓库Id\n            category: \"General\",\n            categoryId: \"DIC_kwDODCK5HM4Ccp32\" //对应自己的分类Id\n        }),
\n

即可在页面上看到效果

\n
\"颜色可自定义\"
颜色可自定义
\n

四、博客的部署

\n

官网也有讲解部署的情况,具体可以看官网Github Pages,整体上看速度还是挺慢的,可以尝试去gitee上部署看一下,之后就可以在pages通过域名访问了。需要在项目下创建.github/workflows/docs.yml文件,具体配置参考官网,不需做任何改动。

\n

五、Github pages自定义域名

\n

github自带的io域名zephery.github.io,做为一名开发,肯定是用自己的域名是比较好的。需要注意下中间的红色框,前面的是分支,后面的是你项目的路径。一般默认即可,不用修改。

\n
\"pages的配置\"
pages的配置
\n

购买域名->域名解析,即把我的个人域名wenzhihuai.com指向zephery.github.io(通过cname)即可,然后开启强制https。如果DNS一直没有校验通过,那么可能是CAA的原因。通过DNS诊断工具来判断。

\n
\"DNS诊断工具\"
DNS诊断工具
\n

上面的custom domain配置好了之后,但DNS一直没有校验正确,原因是CAA没有正确解析,需要加上即可。

\n
0 issue \"trust-provider.com\"\n0 issuewild \"trust-provider.com\"
\n
\"CAA记录解析\"
CAA记录解析
\n

之后就可以看到Github Pages的DNS校验成功,并且可以强制开启https了。

\n

六、Typora图床

\n

之前的图床使用的是七牛云和又拍云,都有免费的额度吧,不过看情况未来前景似乎经营不太好,目前改用了腾讯云。存储容量50GB,每个月外网访问流量10GB,满足个人网站使用。具体的配置过程比较简单,就不再阐述了,可以直接看uPic的官方介绍。

\n

七、为自己的内容增加收入

\n

有钱才有写作的动力,之前的网站开启了几年的捐赠,总共都没有收到过50块钱,只能从广告这一处想想办法,百度、腾讯广告似乎都不支持个人网站,谷歌可以。配置谷歌广告,网上的教程不少,例如: vuepress配置谷歌广告-通过vue-google-adsense库,缺点是,大部分的文章都是需要在自己的markdown文件中新增特定的标识符。比如:

\n
# js 模板引擎 mustache 用法\n\n<ArticleTopAd></ArticleTopAd>\n\n## 一. 使用步骤
\n

每一篇文章都要新增显然不符合懒惰的人,下面是个人尝试的解决办法,用的是vuepress2提供的slots插槽。

\n
\"谷歌广告adsense\"
谷歌广告adsense
\n

上面的目的是为了获取data-ad-client和data-ad-slot,其中,data-ad-slot为广告单元,不一样。并且,配置完之后可能需要等一个小时才会生效,不要着急。

\n

docs/.vuepress/config.ts

\n
head: [\n    [\n        \"script\",\n        {\n            \"data-ad-client\": \"ca-pub-9037099208128116\",\n            async: true,\n            src: \"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"\n        }\n    ],\n]\n\n...\nalias: {\n    \"@theme-hope/components/NormalPage\": path.resolve(\n        __dirname,\n        \"./components/NormalPage.vue\",\n    ),\n},
\n

docs/.vuepress/components/NormalPage.vue

\n
<template>\n  <normal-page>\n    <template #contentBefore>\n      <ins class=\"adsbygoogle\"\n           style=\"display:block; text-align:center;width: 90%;margin: 0 auto;\"\n           data-ad-layout=\"in-article\"\n           data-ad-format=\"fluid\"\n           data-ad-client=\"ca-pub-9037099208128116\"\n           data-ad-slot=\"8206550629\"></ins>\n    </template>\n  </normal-page>\n</template>\n<script>\nimport NormalPage from \"vuepress-theme-hope/components/NormalPage.js\";\n\nexport default {\n  name: \"adsense-inline\",\n  components: {\n    'normal-page': NormalPage,\n  },\n  mounted() {\n    this.adsenseAddLoad();\n  },\n  methods: {\n    adsenseAddLoad() {\n      let inlineScript = document.createElement(\"script\");\n      inlineScript.type = \"text/javascript\";\n      inlineScript.text = '(adsbygoogle = window.adsbygoogle || []).push({});'\n      document.getElementsByTagName('body')[0].appendChild(inlineScript);\n    }\n  }\n}\n</script>\n\n\n<style lang=\"scss\" scoped>\n</style>
\n

本地是没办法进行调试的,可以从官网插槽演示的文章中用div进行调试,等修改完毕发布之后,即可在自己的网站上看到相关的广告和收入(浏览器要把封禁广告的插件关闭)。

\n
\"出现的广告\"
出现的广告
\n
\"谷歌广告收入\"
谷歌广告收入
\n

收入虽然低,但是基本上个人没有成本,只需要域名的85块钱。

\n

常见问题

\n

为什么广告不能正常显示?

\n

\"建议多写原创高质量的文章出来,AdSense才会匹配出合适的广告,用户感兴趣了才会浏览量增加,你才会有更多的广告收入。\"

\n

还是得多写一写优质的文章。

\n

最后,多帮忙点一下个人网站的广告吧,感恩

\n

网站地址:https://www.wenzhihuai.com

\n

源码地址:https://github.com/Zephery/MyWebsite

\n", + "image": "https://github-images.wenzhihuai.com/images/600-20240126113210252-20240131213935629.png", + "date_published": "2024-01-25T06:44:04.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "", + "url": "http://www.wenzhihuai.com/open-source-project/", + "id": "http://www.wenzhihuai.com/open-source-project/", + "summary": "jfaowejfoewj", + "content_html": "

jfaowejfoewj

\n", + "date_published": "2024-01-24T10:30:22.000Z", + "date_modified": "2024-01-24T10:30:22.000Z", + "authors": [], + "tags": [] + }, + { + "title": "中间件", + "url": "http://www.wenzhihuai.com/middleware/", + "id": "http://www.wenzhihuai.com/middleware/", + "summary": "中间件", + "content_html": "\n", + "date_published": "2024-01-24T03:35:40.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "", + "url": "http://www.wenzhihuai.com/interview/tiktok2023.html", + "id": "http://www.wenzhihuai.com/interview/tiktok2023.html", + "summary": "tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,...", + "content_html": "

tt一面:
\n1.全程项目
\n2.lc3,最长无重复子串,滑动窗口解决

\n

tt二面:
\n全程基础,一直追问
\n1.java内存模型介绍一下
\n2.volatile原理
\n3.内存屏障,使用场景?(我提了在gc中有使用)
\n4.gc中具体是怎么使用内存屏障的,详细介绍
\n5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
\n6.线程内存分配方式,tlab相关
\n7.happens-before介绍一下
\n8.总线风暴是什么,怎么解决
\n9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
\n10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
\n11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

\n

tt三面:
\n大部分聊项目,基础随便问了问
\n1.分布式事务,两阶段三阶段流程,区别
\n2.mysql主从,同步复制,半同步复制,异步复制
\n3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转
\n4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万

\n", + "date_published": "2023-04-02T12:15:06.000Z", + "date_modified": "2024-01-24T03:25:58.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Mysql", + "url": "http://www.wenzhihuai.com/database/mysql/1mysql.html", + "id": "http://www.wenzhihuai.com/database/mysql/1mysql.html", + "summary": "Mysql imgimg", + "content_html": "\n
\"img\"
img
\n", + "image": "https://github-images.wenzhihuai.com/images/img.png", + "date_published": "2023-03-26T12:44:23.000Z", + "date_modified": "2024-02-06T07:53:39.000Z", + "authors": [], + "tags": [] + }, + { + "title": "数据库缓存", + "url": "http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html", + "id": "http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html", + "summary": "数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。 查询缓存...", + "content_html": "\n

在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

\n

将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。

\n

禁用原因:

\n

1.命中率低

\n

2.写时所有都失效

\n

禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。

\n

查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182

\n", + "date_published": "2023-03-26T12:44:23.000Z", + "date_modified": "2024-01-25T11:42:16.000Z", + "authors": [], + "tags": [] + }, + { + "title": "spark on k8s operator", + "url": "http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html", + "id": "http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html", + "summary": "spark on k8s operator Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。 使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管...", + "content_html": "\n

Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

\n

使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。

\n

Spark Operator包括如下几个组件:

\n
    \n
  1. SparkApplication控制器:该控制器用于创建、更新、删除SparkApplication对象,同时控制器还会监控相应的事件,执行相应的动作。
  2. \n
  3. Submission Runner:负责调用spark-submit提交Spark作业,作业提交的流程完全复用Spark on K8s的模式。
  4. \n
  5. Spark Pod Monitor:监控Spark作业相关Pod的状态,并同步到控制器中。
  6. \n
  7. Mutating Admission Webhook:可选模块,基于注解来实现Driver/Executor Pod的一些定制化需求。
  8. \n
  9. SparkCtl:用于和Spark Operator交互的命令行工具。
  10. \n
\n

安装

\n
# 添加源\nhelm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator\n# 安装\nhelm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace
\n

这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。

\n
forbidden: error looking up service account default/spark: serviceaccount \\\"spark\\\" not found.\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\\n\\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport
\n

同时也要安装rbac

\n

rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见rbac.yaml

\n

执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。

\n

代码如下:

\n
 SparkConf sparkConf = new SparkConf().setAppName(\"Spark WordCount Application (java)\");\n        sparkConf.set(\"es.nodes\", \"xxx\")\n                .set(\"es.port\", \"8xx0\")\n                .set(\"es.nodes.wan.only\", \"true\")\n                .set(\"es.net.http.auth.user\", \"xxx\")\n                .set(\"es.net.http.auth.pass\", \"xxx-xxx\");\n        try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {\n            JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, \"kibana_sample_data_ecommerce\").values();\n\n            System.out.println(esRDD.partitions().size());\n\n            esRDD.map(x -> x.get(\"customer_full_name\"))\n                    .countByValue()\n                    .forEach((x, y) -> System.out.println(x + \":\" + y));\n            TimeUnit.HOURS.sleep(1);\n\n        } catch (InterruptedException e) {\n            throw new RuntimeException(e);\n        }
\n

打包之后kubectl create -f k8s.yaml即可

\n
apiVersion: \"sparkoperator.k8s.io/v1beta2\"\nkind: SparkApplication\nmetadata:\n  name: spark-demo\n  namespace: default\nspec:\n  type: Scala\n  mode: cluster\n  image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3\n  imagePullPolicy: Always\n  mainClass: com.spark.es.demo.EsReadGroupByName\n  mainApplicationFile: \"local:///opt/spark/examples/jars/app.jar\"\n  sparkVersion: \"3.1.1\"\n  restartPolicy:\n    type: Never\n  driver:\n    # 用cores必须要大于等于1,这里用coreRequest\n    coreRequest: \"100m\"\n    coreLimit: \"1200m\"\n    memory: \"512m\"\n    labels:\n      version: 3.1.1\n    serviceAccount: sparkoperator\n  executor:\n    # 用cores必须要大于等于1,这里用coreRequest\n    coreRequest: \"100m\"\n    instances: 1\n    memory: \"512m\"\n    labels:\n      version: 3.1.1
\n
spark-demo-driver                                          1/1     Running   0          2m30s\nspark-wordcount-application-java-0ac352810a9728e1-exec-1   1/1     Running   0          2m1s
\n

同时会自动生成相应的service。

\n
\"image-20220528203941897\"
image-20220528203941897
\n\"image-20220528203816649\"\n

Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,

\n

源码解析

\n\"architecture-diagram\"\n

Spark Operator的主要组件如下:

\n

1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;

\n

2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;

\n

3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;

\n

4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。

\n

5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。

\n\"image-20220612194403742\"\n

apis:用户编写yaml时的解析

\n

Batchscheduler:批处理的调度,提供了支持volcano的接口

\n

Client:

\n

leader的选举

\n
electionCfg := leaderelection.LeaderElectionConfig{\n   Lock:          resourceLock,\n   LeaseDuration: *leaderElectionLeaseDuration,\n   RenewDeadline: *leaderElectionRenewDeadline,\n   RetryPeriod:   *leaderElectionRetryPeriod,\n   Callbacks: leaderelection.LeaderCallbacks{\n      OnStartedLeading: func(c context.Context) {\n         close(startCh)\n      },\n      OnStoppedLeading: func() {\n         close(stopCh)\n      },\n   },\n}\n\nelector, err := leaderelection.NewLeaderElector(electionCfg)
\n

参考:

\n

1.k8s client-go中Leader选举实现

\n

2.[Kubernetes基于leaderelection选举策略实现组件高可用

\n", + "image": "https://github-images.wenzhihuai.com/images/e6c9d24egy1h2oeh4gm96j21qg02wweq.jpg", + "date_published": "2022-06-12T12:19:23.000Z", + "date_modified": "2024-01-27T18:29:27.000Z", + "authors": [], + "tags": [] + }, + { + "title": "elastic spark", + "url": "http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html", + "id": "http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html", + "summary": "elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、t...", + "content_html": "\n

Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
\n(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
\n(2)数据RDBMS:mysql、oracle、mssql
\n(3)NOSQL数据库:HBase、ES、Redis
\n(4)消息对象:Redis

\n

elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。

\n
\"Spark
Spark - Apache Spark
\n

一、原生RDD支持

\n

1.1 基础配置

\n

相关库引入:

\n
        <dependency>\n            <groupId>org.elasticsearch</groupId>\n            <artifactId>elasticsearch-spark-30_2.13</artifactId>\n            <version>8.1.3</version>\n        </dependency>
\n

SparkConf配置,更多详细的请点击这里或者源码ConfigurationOptions

\n
public static SparkConf getSparkConf() {\n    SparkConf sparkConf = new SparkConf().setAppName(\"elasticsearch-spark-demo\");\n    sparkConf.set(\"es.nodes\", \"host\")\n            .set(\"es.port\", \"xxxxxx\")\n            .set(\"es.nodes.wan.only\", \"true\")\n            .set(\"es.net.http.auth.user\", \"elxxxxastic\")\n            .set(\"es.net.http.auth.pass\", \"xxxx\")\n            .setMaster(\"local[*]\");\n    return sparkConf;\n}
\n

1.2 读取es数据

\n

这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。

\n
public static void main(String[] args) {\n    SparkConf conf = getSparkConf();\n    try (JavaSparkContext jsc = new JavaSparkContext(conf)) {\n\n        JavaPairRDD<String, Map<String, Object>> esRDD =\n                JavaEsSpark.esRDD(jsc, \"kibana_sample_data_ecommerce\");\n        esRDD.collect().forEach(System.out::println);\n    }\n}
\n

esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。

\n

1.3 写数据

\n

支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。

\n
public static void jsonWrite(){\n    String json1 = \"{\\\"reason\\\" : \\\"business\\\",\\\"airport\\\" : \\\"SFO\\\"}\";\n    String json2 = \"{\\\"participants\\\" : 5,\\\"airport\\\" : \\\"OTP\\\"}\";\n    JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));\n    JavaEsSpark.saveJsonToEs(stringRDD, \"spark-json\");\n}
\n

比较常用的读写也就这些,更多可以看下官网相关介绍。

\n

二、Spark Streaming

\n

spark的实时处理,es5.0的时候开始支持,目前

\n

三、Spark SQL

\n

四、Spark Structure Streaming

\n

五、Spark on kubernetes Operator

\n

参考:

\n

1.Apache Spark support

\n

2.elasticsearch-hadoop

\n

3.使用SparkSQL操作Elasticsearch - Spark入门教程

\n", + "image": "https://databricks.com/wp-content/uploads/2019/02/largest-open-source-apache-spark.png", + "date_published": "2022-05-03T11:42:30.000Z", + "date_modified": "2024-01-25T11:42:16.000Z", + "authors": [], + "tags": [] + }, + { + "title": "基于kubernetes的分布式限流", + "url": "http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html", + "id": "http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html", + "summary": "基于kubernetes的分布式限流 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 一、概念 限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,...", + "content_html": "\n

做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

\n

一、概念

\n

限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。

\n

1.1 使用场景

\n

限流可以应对:

\n
    \n
  • 热点业务带来的突发请求;
  • \n
  • 调用方 bug 导致的突发请求;
  • \n
  • 恶意攻击请求。
  • \n
\n

1.2 维度

\n

对于限流场景,一般需要考虑两个维度的信息:
\n时间
\n限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定
\n资源
\n基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。
\n  限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。

\n
\"image.png\"
image.png
\n

1.3 分布式限流

\n

分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。

\n

二、分布式限流常用方案

\n

基于Guava的客户端限流
\nGuava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。

\n

网关层限流
\n服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。

\n

中间件限流
\n将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。

\n

限流组件
\n目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。

\n

Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?

\n

对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。

\n

三、基于kubernetes的分布式限流

\n

在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。

\n
\"企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png\"
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
\n

3.1 kubernetes中的副本数

\n

在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。

\n
func CountDeploymentSize(namespace string, deploymentName string) *int32 {\n\tdeployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn deployment.Spec.Replicas\n}
\n

用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。

\n

3.2 rateLimiter的创建

\n

在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。

\n
private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()\n        .maximumSize(10_000)\n        .refreshAfterWrite(20, TimeUnit.MINUTES)\n        .build(this::createRateLimit);\n//定义一个默认最小的QPS\nprivate static final Integer minQpsLimit = 3000;
\n

之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。

\n
public RateLimiter createRateLimit(String key) {\n    log.info(\"createRateLimit,key:{}\", key);\n    int totalLimit = 获取总限频数,可以在数据库中定义\n    Integer replicas = kubernetesService.getDeploymentReplicas();\n    RateLimiter rateLimiter;\n    if (totalLimit > 0 && replicas == null) {\n        rateLimiter = RateLimiter.create(totalLimit);\n    } else if (totalLimit > 0) {\n        int nodeQpsLimit = totalLimit / replicas;\n        rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);\n    } else {\n        rateLimiter = RateLimiter.create(minQpsLimit);\n    }\n    log.info(\"create rateLimiter success,key:{},rateLimiter:{}\", key, rateLimiter);\n    return rateLimiter;\n}
\n

3.3 rateLimiter的获取

\n

根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽

\n
public RateLimiter getRateLimiter(String key) {\n  return loadingCache.get(key);\n}
\n

3.4 filter里的判断

\n

最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS

\n
public class RateLimiterFilter implements Filter {\n    @Resource\n    private RateLimiterService rateLimiterService;\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {\n        HttpServletRequest httpServletRequest = (HttpServletRequest) request;\n        HttpServletResponse httpServletResponse = (HttpServletResponse) response;\n        String key = httpServletRequest.getHeader(\"key\");\n        RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);\n        if (rateLimiter != null) {\n            if (rateLimiter.tryAcquire()) {\n                filterChain.doFilter(request, response);\n            } else {\n                httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());\n            }\n        } else {\n            filterChain.doFilter(request, response);\n        }\n    }\n}
\n

四、性能压测

\n

为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。

\n

无限流

\n
\"企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png\"
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
\n

使用redis限流

\n

其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显

\n
\"企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png\"
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
\n

自研限流

\n

性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越

\n
\"企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png\"
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
\n

五、其他问题

\n

5.1 对于保证qps限频准确的时候,应该怎么解决呢?

\n

在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的

\n

5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力

\n

理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的

\n

5.3 如果有多个副本,怎么保证请求是均匀的

\n

这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。

\n

参考

\n

1.常见的分布式限流解决方案
\n2.分布式服务限流实战
\n3.高性能

\n", + "image": "https://github-images.wenzhihuai.com/images/1240-20240125210940819-6188218.png", + "date_published": "2022-04-09T05:35:51.000Z", + "date_modified": "2024-01-26T03:58:56.000Z", + "authors": [], + "tags": [] + }, + { + "title": "【elasticsearch】搜索过程详解", + "url": "http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html", + "id": "http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html", + "summary": "【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(...", + "content_html": "\n

本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

\n

SearchType

\n

QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
\nDFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。

\n

为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。

\n
POST aaaa-16/_doc\n{\n  \"@timestamp\": \"2022-02-16T16:21:15.000Z\",\n  \"word\":\"16\"\n}\n\n\nPOST aaaa-17/_doc\n{\n  \"@timestamp\": \"2022-02-17T16:21:15.000Z\",\n  \"word\":\"17\"\n}\n\nPOST aaaa-18/_doc\n{\n  \"@timestamp\": \"2022-02-18T16:21:15.000Z\",\n  \"word\":\"18\"\n}
\n

即可在kibana上看到3条数据

\n
\"image-20220219195141327\"
image-20220219195141327
\n

此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?

\n
GET aaaa*/_search\n{\n  \"query\": {\n    \"range\": {\n      \"@timestamp\": {\n        \"gte\": \"2022-02-18T10:21:15.000Z\",\n        \"lte\": \"2022-02-18T17:21:15.000Z\"\n      }\n    }\n  }\n}
\n

正好命中一条记录返回。

\n
{\n  \"took\" : 2,\n  \"timed_out\" : false,\n  \"_shards\" : {\n    \"total\" : 3,\n    \"successful\" : 3,\n    \"skipped\" : 0,\n    \"failed\" : 0\n  },\n  \"hits\" : {\n    \"total\" : {\n      \"value\" : 1,\n      \"relation\" : \"eq\"\n    },\n    \"max_score\" : 1.0,\n    \"hits\" : [\n      {\n        \"_index\" : \"aaaa-18\",\n        \"_id\" : \"0zB2O38BoMIMP8QzHgdq\",\n        \"_score\" : 1.0,\n        \"_source\" : {\n          \"@timestamp\" : \"2022-02-18T16:21:15.000Z\",\n          \"word\" : \"18\"\n        }\n      }\n    ]\n  }\n}
\n

一、es的分布式搜索过程

\n

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。当搜索任务执行在分布式系统上时,整体流程如下图所示。图片来源Elasitcsearch源码解析与优化实战

\n
\"2\"
2
\n

1.1 搜索入口

\n

整个http请求的入口,主要使用的是Netty4HttpRequestHandler。

\n
@ChannelHandler.Sharable\nclass Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {\n        final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();\n        boolean success = false;\n        try {\n            serverTransport.incomingRequest(httpRequest, channel);\n            success = true;\n        } finally {\n            if (success == false) {\n                httpRequest.release();\n            }\n        }\n    }\n}
\n

二、初步调用流程

\n

调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction

\n

协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。

\n

特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。

\n
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {\n  ...\n    TaskHolder taskHolder = new TaskHolder();\n    Task task = client.executeLocally(action, request, new ActionListener<>() {\n        @Override\n        public void onResponse(Response response) {\n            try {\n                closeListener.unregisterTask(taskHolder);\n            } finally {\n                listener.onResponse(response);\n            }\n        }\n    });\n  ...\n}
\n

其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程

\n
private SearchPhase searchAsyncAction(...) {\n  ...\n    final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();\n  AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {\n    case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);\n    case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);\n  };\n  return searchAsyncAction;\n  ...\n}
\n

之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。

\n
此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction\nprivate void executePhase(SearchPhase phase) {\n    try {\n        phase.run();\n    } catch (Exception e) {\n        if (logger.isDebugEnabled()) {\n            logger.debug(new ParameterizedMessage(\"Failed to execute [{}] while moving to [{}] phase\", request, phase.getName()), e);\n        }\n        onPhaseFailure(phase, \"\", e);\n    }\n}
\n

三、协调节点

\n

两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。

\n
\"23\"
23
\n

3.1 query阶段

\n

图片来源官网,比较旧,但任然可用

\n
\"12\"
12
\n

(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。
\n(2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。
\n(3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。

\n
在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构: \npublic RestChannelConsumer prepareRequest(.. .) {\n    SearchRequest searchRequest = new SearchRequest();\n    request.withContentOrSourceParamParserOrNull (parser ->\n        parseSearchRequest (searchRequest, request, parser, setSize));\n    ...\n}
\n

3.1.1 构造目的shard列表

\n

将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:

\n
private void executeSearch(.. .) {\n  ...\n    final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);\n    localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {\n    OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());\n    assert finalIndices != null;\n    return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);\n    }).collect(Collectors.toList());\n\t...\n}
\n

查看结果

\n
\"241\"
241
\n

3.1.2 对所有分片进行搜索

\n
AbstractSearchAsyncAction.run\n对每个分片进行搜索查询\nfor (int i = 0; i < shardsIts.size(); i++) {\n    final SearchShardIterator shardRoutings = shardsIts.get(i);\n    assert shardRoutings.skip() == false;\n    assert shardIndexMap.containsKey(shardRoutings);\n    int shardIndex = shardIndexMap.get(shardRoutings);\n    performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());\n}
\n

其中shardsIts是所有aaaa*的所有索引+其中一个副本

\n
\"2141\"
2141
\n

3.1.3 分片具体的搜索过程

\n
AbstractSearchAsyncAction.performPhaseOnShard\nprivate void performPhaseOnShard(. ..) {\n    executePhaseOnShard(.. .) {\n        //收到执行成功的回复\n        public void inne rOnResponse (FirstResult result) {\n            maybeFork (thread, () -> onShardResult (result,shardIt) );\n        }\n        //收到执行失败的回复\n        public void onFailure (Exception t) {\n            maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));\n        }\n    });\n}
\n

分片结果,当前线程

\n
//AbstractSearchAsyncAction.onShardResult\n...\nprivate void onShardResult (FirstResult result, SearchShardIterator shardIt) {\n    onShardSuccess(result);\n    success fulShardExecution(shardIt);\n}\n...\n//AbstractSearchAsyncAction.onShardResultConsumed\nprivate void successfulShardExecution (SearchShardIterator shardsIt) {\n    //计数器累加.\n    final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);\n    //检查是否收到全部回复\n    if (xTotalOps == expectedTotalOps) {\n        onPhaseDone ();\n    } else if (xTota1Ops > expectedTotal0ps) {\n        throw new AssertionError(. ..);\n    }\n}
\n
\"412\"
412
\n

此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段

\n

3.2 Fetch阶段

\n

取回阶段,图片来自官网

\n
\"412412\"
412412
\n

(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document

\n

(2)各个shard将document返回给coordinate node

\n

(3)coordinate node将合并后的document结果返回给client客户端

\n

3.2.1 FetchSearchPhase(对应上面的1)

\n

Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。

\n
private void innerRun() throws Exception {\n    final int numShards = context.getNumShards();\n    final boolean isScrollSearch = context.getRequest().scroll() != null;\n    final List<SearchPhaseResult> phaseResults = queryResults.asList();\n    final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();\n    final boolean queryAndFetchOptimization = queryResults.length() == 1;\n    final Runnable finishPhase = () -> moveToNextPhase(\n        searchPhaseController,\n        queryResults,\n        reducedQueryPhase,\n        queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()\n    );\n            for (int i = 0; i < docIdsToLoad.length; i++) {\n                IntArrayList entry = docIdsToLoad[i];\n                SearchPhaseResult queryResult = queryResults.get(i);\n                if (entry == null) { \n                    if (queryResult != null) {\n                        releaseIrrelevantSearchContext(queryResult.queryResult());\n                        progressListener.notifyFetchResult(i);\n                    }\n                    counter.countDown();\n                }else{\n                    executeFetch(\n                            queryResult.getShardIndex(),\n                            shardTarget,\n                            counter,\n                            fetchSearchRequest,\n                            queryResult.queryResult(),\n                            connection\n                        );\n                }\n        }\n    }\n}
\n

再看源码:

\n
启动一个线程来fetch\nAbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch\n\nprivate void executeFetch(...) {\n    context.getSearchTransport()\n        .sendExecuteFetch(\n            connection,\n            fetchSearchRequest,\n            context.getTask(),\n            new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {\n                @Override\n                public void innerOnResponse(FetchSearchResult result) {\n                  progressListener.notifyFetchResult(shardIndex);\n                  counter.onResult(result);\n                }\n\n                @Override\n                public void onFailure(Exception e) {\n                  progressListener.notifyFetchFailure(shardIndex, shardTarget, e);\n                  counter.onFailure(shardIndex, shardTarget, e);\n                }\n            }\n        );\n}
\n
\"13413\"
13413
\n

counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。

\n
# CountedCollector.class\nvoid countDown() {\n    assert counter.isCountedDown() == false : \"more operations executed than specified\";\n    if (counter.countDown()) {\n        onFinish.run();\n    }\n}
\n

moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。

\n

3.2.2 ExpandSearchPhase(对应上图的2)

\n

AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。

\n
\"image-20220317000858513\"
image-20220317000858513
\n

ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:

\n
\"412412\"
412412
\n

四、数据节点

\n

4.1 执行query、fetch流程

\n

执行本流程的线程池: search。

\n

对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。

\n
public static void registerRequestHandler(TransportService transportService, SearchService searchService) {\n  ...\n  transportService.registerRequestHandler(\n    QUERY_ACTION_NAME,\n    ThreadPool.Names.SAME,\n    ShardSearchRequest::new,\n    (request, channel, task) -> searchService.executeQueryPhase(\n      request,\n      (SearchShardTask) task,\n      new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)\n    )\n  );\n  ...\n}
\n

4.1.1 执行query请求

\n
# SearchService\npublic void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {\n    assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1\n        : \"empty responses require more than one shard\";\n    final IndexShard shard = getShard(request);\n    rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {\n        ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);\n    }));\n}
\n

其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。

\n
\"1\"
1
\n

主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:

\n
public void execute (SearchContext searchContext) {\n    aggregationPhase.preProcess (searchContext);\n    boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);\n    if (rescore) {\n        rescorePhase.execute (searchContext);\n        suggestPhase.execute (searchContext);\n        aggregationPhase.execute (searchContext);\n    }\n}
\n

其中包含几个核心功能:

\n
    \n
  • execute(),调用Lucene、searcher.search()实现搜索
  • \n
  • rescorePhase,全文检索且需要打分
  • \n
  • suggestPhase,自动补全及纠错
  • \n
  • aggregationPhase,实现聚合
  • \n
\n

4.1.2 fetch流程

\n

对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。

\n
transportService.registerRequestHandler(\n    QUERY_FETCH_SCROLL_ACTION_NAME,\n    ThreadPool.Names.SAME,\n    InternalScrollSearchRequest::new,\n    (request, channel, task) -> {\n        searchService.executeFetchPhase(\n            request,\n            (SearchShardTask) task,\n            new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)\n        );\n    }\n);
\n

对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。

\n
# FetchPhase\npublic void execute(SearchContext context) {\n    Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();\n    SearchHits hits = null;\n    try {\n      //lucene构建搜索的结果\n        hits = buildSearchHits(context, profiler);\n    } finally {\n        ProfileResult profileResult = profiler.finish();\n        // Only set the shardResults if building search hits was successful\n        if (hits != null) {\n            context.fetchResult().shardResult(hits, profileResult);\n        }\n    }\n}
\n

五、数据返回

\n

入口:RestCancellableNodeClient.doExecute
\nTask task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
\n\"image-20220319003638991\"

\n

其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse

\n
public void sendResponse(RestResponse restResponse) {\n  ...\n   httpChannel.sendResponse(httpResponse, listener);\n  ...\n}
\n

最后由Netty4HttpChannel.sendResponse来响应请求。

\n

六、总结

\n

当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。

\n

本文参考

\n
    \n
  1. Elasitcsearch源码解析与优化实战
  2. \n
  3. Elasticsearch源码分析-搜索分析(一)
  4. \n
  5. Elasticsearch源码分析-搜索分析(二)
  6. \n
  7. Elasticsearch源码分析-搜索分析(三)
  8. \n
  9. Elasticsearch 通信模块的分析
  10. \n
  11. Elasticsearch 网络通信线程分析
  12. \n
\n", + "image": "https://github-images.wenzhihuai.com/images/1240.png", + "date_published": "2022-03-20T15:14:40.000Z", + "date_modified": "2024-02-16T17:21:55.000Z", + "authors": [], + "tags": [] + }, + { + "title": "kafka面试题", + "url": "http://www.wenzhihuai.com/middleware/kafka/kafka.html", + "id": "http://www.wenzhihuai.com/middleware/kafka/kafka.html", + "summary": "kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②...", + "content_html": "\n

1、请说明什么是Apache Kafka?
\nApache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

\n

2、说说Kafka的使用场景?
\n①异步处理
\n②应用解耦
\n③流量削峰
\n④日志处理
\n⑤消息通讯等。

\n

3、使用Kafka有什么优点和缺点?
\n优点:
\n①支持跨数据中心的消息复制;
\n②单机吞吐量:十万级,最大的优点,就是吞吐量高;
\n③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;
\n④时效性:ms级;
\n⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
\n⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;
\n⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。

\n

缺点:
\n①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序;
\n②有可能消息重复消费;
\n③依赖zookeeper进行元数据管理,等等。

\n

4、为什么说Kafka性能很好,体现在哪里?
\n①顺序读写
\n②零拷贝
\n③分区
\n④批量发送
\n⑤数据压缩

\n

5、请说明什么是传统的消息传递方法?
\n传统的消息传递方法包括两种:
\n排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。
\n发布-订阅:在这个模型中,消息被广播给所有的用户。

\n

6、请说明Kafka相对传统技术有什么优势?
\n①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。
\n②可伸缩:在一组机器上对数据进行分区
\n③和简化,以支持更大的数据
\n④持久:消息是持久性的,并在集群中进
\n⑤行复制,以防止数据丢失。
\n⑥设计:它提供了容错保证和持久性

\n

7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗?
\nZookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。
\n不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。
\nZookeeper主要用于在集群中不同节点之间进行通信
\n在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
\n除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

\n

8、解释Kafka的用户如何消费信息?
\n在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。

\n

9、解释如何提高远程用户的吞吐量?
\n如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。

\n

10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息?
\n在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。
\n这里有两种方法,可以在数据生成时准确地获得一个语义:
\n每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功
\n在消息中包含一个主键(UUID或其他),并在用户中进行反复制

\n

11、解释如何减少ISR中的扰动?broker什么时候离开ISR?
\nISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。

\n

12、Kafka为什么需要复制?
\nKafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。

\n

13、如果副本在ISR中停留了很长时间表明什么?
\n如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。

\n

14、请说明如果首选的副本不在ISR中会发生什么?
\n如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。

\n

15、有可能在生产后发生消息偏移吗?
\n在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。

\n

作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。

\n

16、Kafka的设计时什么样的呢?
\nKafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息

\n

17、数据传输的事物定义有哪三种?
\n(1)最多一次:
\n消息不会被重复发送,最多被传输一次,但也有可能一次不传输
\n(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
\n(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

\n

18、Kafka判断一个节点是否还活着有那两个条件?
\n(1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接
\n(2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久

\n

19、producer是否直接将数据发送到broker的leader(主节点)?
\nproducer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。

\n

20、Kafa consumer是否可以消费指定分区消息?
\nKafa consumer消费消息时,向broker发出\"fetch\"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的

\n

21、Kafka消息是采用Pull模式,还是Push模式?
\nKafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式

\n

Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略

\n

Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

\n

22、Kafka存储在硬盘上的消息格式是什么?
\n消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。
\n消息长度: 4 bytes (value: 1+4+n)
\n版本号: 1 byte
\nCRC校验码: 4 bytes
\n具体的消息: n bytes

\n

23、Kafka高效文件存储设计特点:
\n(1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
\n(2).通过索引信息可以快速定位message和确定response的最大大小。
\n(3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
\n(4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

\n

24、Kafka 与传统消息系统之间有三个关键区别
\n(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留
\n(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
\n(3).Kafka 支持实时的流式处理

\n

25、Kafka创建Topic时如何将分区放置到不同的Broker中
\n副本因子不能大于 Broker 的个数;
\n第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
\n其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
\n剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的

\n

26、Kafka新建的分区会在哪个目录下创建
\n在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。

\n

27、partition的数据如何保存到硬盘
\ntopic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名

\n

28、kafka的ack机制
\nrequest.required.acks有三个值 0 1 -1
\n0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据
\n1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失
\n-1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失

\n

29、Kafka的消费者如何消费数据
\n消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。

\n

30、消费者负载均衡策略
\n结合consumer的加入和退出进行再平衡策略。

\n

31、kafka消息数据是否有序?
\n消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。

\n

32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中
\n每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition

\n

33、kafka consumer 什么情况会触发再平衡reblance?
\n①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。
\n②订阅主题分区发生变化,所有消费者也都要再平衡。

\n

34、描述下kafka consumer 再平衡步骤?
\n①关闭数据拉取线程,情空队列和消息流,提交偏移量;
\n②释放分区所有权,删除zk中分区和消费者的所有者关系;
\n③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区;
\n④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息;
\n⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。

\n

35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么
\n36、Kafka中的HW、LEO、LSO、LW等分别代表什么?
\n37、Kafka中是怎么体现消息顺序性的?
\n38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
\n39、Kafka生产者客户端的整体结构是什么样子的?
\n40、Kafka生产者客户端中使用了几个线程来处理?分别是什么?
\n41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷?
\n42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段?
\n43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
\n44、有哪些情形会造成重复消费?
\n45、那些情景下会造成消息漏消费?
\n46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?
\n47、简述消费者与消费组之间的关系
\n48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
\n49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
\n50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
\n51、创建topic时如何选择合适的分区数?
\n52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么?
\n53、优先副本是什么?它有什么特殊的作用?
\n54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理
\n55、简述Kafka的日志目录结构
\n56、Kafka中有那些索引文件?
\n57、如果我指定了一个offset,Kafka怎么查找到对应的消息?
\n58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息?
\n59、聊一聊你对Kafka的Log Retention的理解
\n60、聊一聊你对Kafka的Log Compaction的理解
\n61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层)
\n62、聊一聊Kafka的延时操作的原理
\n63、聊一聊Kafka控制器的作用
\n64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器)
\n65、Kafka中的幂等是怎么实现的
\n66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”)
\n67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
\n68、失效副本是指什么?有那些应对措施?
\n69、多副本下,各个副本中的HW和LEO的演变过程
\n70、为什么Kafka不支持读写分离?
\n71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch)
\n72、Kafka中怎么实现死信队列和重试队列?
\n73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?)
\n74、Kafka中怎么做消息审计?
\n75、Kafka中怎么做消息轨迹?
\n76、Kafka中有那些配置参数比较有意思?聊一聊你的看法
\n77、Kafka中有那些命名比较有意思?聊一聊你的看法
\n78、Kafka有哪些指标需要着重关注?
\n79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同)
\n80、Kafka的那些设计让它有如此高的性能?
\n81、Kafka有什么优缺点?
\n82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点?
\n83、为什么选择Kafka?
\n84、在使用Kafka的过程中遇到过什么困难?怎么解决的?
\n85、怎么样才能确保Kafka极大程度上的可靠性?
\n86、聊一聊你对Kafka生态的理解

\n", + "date_published": "2022-03-20T15:14:40.000Z", + "date_modified": "2024-01-25T17:34:45.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2017", + "url": "http://www.wenzhihuai.com/life/2017.html", + "id": "http://www.wenzhihuai.com/life/2017.html", + "summary": "2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感...", + "content_html": "\n

2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
\n17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

\n
\"\"
\n

回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...

\n

春节期间,把自己的网站整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~

\n
\"\"
\n

疲惫的面试历程

\n

2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。

\n

**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?

\n

然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。

\n

有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。

\n

没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?

\n

这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。

\n

突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。

\n

还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。
\n  4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。

\n

感情、我们毕业了

\n

5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。
\n  5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。
\n  6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。

\n
\"\"
\n

准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。

\n

996的工作

\n

七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。

\n

没有环境,就自己创造,下班有时间就改改自己的网站http://www.wenzhihuai.com,GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。

\n
\"\"
\n

决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?

\n

双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。

\n
\"\"
\n

孤独

\n

珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!!
\n远看:

\n
\"\"
\n

近看:

\n
\"\"
\n

题外话:
\n有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
\n今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
\n《企业IT架构转型之道》是今年最好的架构图书,没有之一。

\n

期望

\n

想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗

\n
\"\"
\n", + "image": "http://image.wenzhihuai.com/images/20171231044153.png", + "date_published": "2020-01-25T13:18:10.000Z", + "date_modified": "2024-01-26T04:53:47.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2019", + "url": "http://www.wenzhihuai.com/life/2019.html", + "id": "http://www.wenzhihuai.com/life/2019.html", + "summary": "2019 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了...", + "content_html": "\n

2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
\n19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

\n

工作

\n

从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。

\n

之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。

\n

工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈

\n
\"\"
\n

学习

\n

项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。
\n过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。
\n操作系统感觉还是要好好学学了,加油

\n
\"\"
\n

Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。

\n

感情

\n

吵架了,没心思写

\n

运动

\n

过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。

\n
\"\"
\n

还爬了两次南山

\n
\"\"
\n

其他

\n

看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。
\n过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。

\n

2020展望

\n

每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!

\n

当然,如果有可能的话,我想去CSIG

\n", + "image": "https://upyuncdn.wenzhihuai.com/202001010544131765044792.png", + "date_published": "2020-01-25T13:18:10.000Z", + "date_modified": "2024-01-26T04:53:47.000Z", + "authors": [], + "tags": [] + }, + { + "title": "被挖矿攻击", + "url": "http://www.wenzhihuai.com/java/serverlog.html", + "id": "http://www.wenzhihuai.com/java/serverlog.html", + "summary": "被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJY...", + "content_html": "\n

服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
\n\"\"
\nstratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

\n

使用top查看:
\n\"\"

\n
\"\"
\n
\"\"
\n

启动iptables,参考http://www.setphp.com/981.html
\nhttp://www.setphp.com/981.html

\n
iptables -A INPUT -s xmr.crypto-pool.fr -j DROP\niptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP
\n

2017-10-02 再次遭到挖矿攻击

\n

2017-11-20

\n

19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。

\n

2017-12-03

\n
\"\"
\n

2017-12-05 wipefs

\n
\"\"
\n", + "image": "http://image.wenzhihuai.com/images/553ac1ca20170923033013.png", + "date_published": "2020-01-25T13:10:49.000Z", + "date_modified": "2024-01-24T10:30:22.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2018", + "url": "http://www.wenzhihuai.com/life/2018.html", + "id": "http://www.wenzhihuai.com/life/2018.html", + "summary": "2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历 其实校招过...", + "content_html": "\n

年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

\n

想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。

\n

心惊胆战的裸辞经历

\n

其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。
\n去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。

\n

and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好

\n
\"\"
\n

工作工作

\n

入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。
\n公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。
\n去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。
\n这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。

\n
\"\"
\n

跑步跑步

\n

平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。

\n
\"\"
\n

Others

\n
    \n
  1. 拿到了深圳户口,全程只花了15天,感谢学妹and家人
  2. \n
  3. 买了第一台MacBook Pro,对于Java的我真心不好用
  4. \n
  5. 盼了好几年的老姐结婚,18年终于了解了这心事
  6. \n
  7. 养了只猫,在经历了好几个月的早起之后,晚上睡觉锁厨房,终于安分了,对不起了哈哈哈
  8. \n
\n
\"\"
\n

2019

\n

想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱

\n", + "image": "http://image.wenzhihuai.com/images/2019011011294921712062.png", + "date_published": "2020-01-25T13:10:49.000Z", + "date_modified": "2024-01-26T04:53:47.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Redis缓存", + "url": "http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html", + "id": "http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html", + "summary": "Redis缓存 一、概述 1.1 缓存介绍 系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 缓存常用语: 数据不...", + "content_html": "\n

一、概述

\n

1.1 缓存介绍

\n

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
\n缓存常用语:
\n数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
\n可查看Redis实战(一) 使用缓存合理性

\n

1.2 本站缓存架构

\n

从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。

\n
\"\"
\n

步骤:
\n(1)用户发送一个请求到nginx,nginx对请求进行分发。
\n(2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。
\n(3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。
\n(4)查询数据库的时候,mysql作了主主备份。

\n

二、Mybatis缓存

\n

2.1 mybatis一级缓存

\n

Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。

\n
\"\"
\n

我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap<k,v>来进行缓存。
\n(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
\n(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。
\n(3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

\n
![](http://image.wenzhihuai.com/images/20180120022427.png)
\n

2.2 mybatis二级缓存

\n

Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
\n\"\"

\n

2.2.1 MyBatis二级缓存的划分

\n

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:

\n
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);\nb.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);
\n

2.2.2 二级缓存的开启

\n

在mybatis的配置文件中添加:

\n
<settings>\n   <!--开启二级缓存-->\n    <setting name=\"cacheEnabled\" value=\"true\"/>\n</settings>
\n

然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):

\n
    <cache eviction=\"LRU\"\n           type=\"org.apache.ibatis.cache.impl.PerpetualCache\"\n           flushInterval=\"120000\"\n           size=\"1024\"\n           readOnly=\"true\"/>
\n

2.2.3 使用第三方支持的二级缓存的实现

\n
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache  type=\"\">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
\n
    \n
  1. MyBatis自身提供的缓存实现;
  2. \n
  3. 用户自定义的Cache接口实现;
  4. \n
  5. 跟第三方内存缓存库的集成;
    \n具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
  6. \n
\n

MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):

\n
\"\"
\n

2.3 Mybatis在分布式环境下脏读问题

\n

(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
\n(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
\n(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。

\n

下面将介绍使用Redis集中式缓存在个人网站的应用。

\n

三、Redis缓存

\n

Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。

\n
\"\"
\n

3.1 Spring Cache

\n

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
\n下面是Spring Cache常用的注解:

\n

(1)@Cacheable
\n@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

\n

| 属性 | 介绍 |例子|
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/20180121034503.png", + "date_published": "2020-01-25T13:10:49.000Z", + "date_modified": "2024-02-06T07:53:39.000Z", + "authors": [], + "tags": [] + }, + { + "title": "JVM调优参数", + "url": "http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html", + "id": "http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html", + "summary": "JVM调优参数 一、堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。...", + "content_html": "\n

一、堆大小设置

\n

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

\n

典型设置:

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
\n

-Xmx3550m:设置JVM最大可用内存为3550M。
\n-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
\n-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
\n-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

\n
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
\n

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
\n-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
\n-XX:MaxPermSize=16m:设置持久代大小为16m。
\n-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

\n

二、回收器选择

\n

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

\n

2.1 吞吐量优先的并行收集器

\n

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
\n典型配置:

\n
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
\n

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
\n-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
\n

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
\n

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
\n

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

\n

2.2 响应时间优先的并发收集器

\n

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
\n典型配置:

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
\n

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
\n-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

\n
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
\n

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
\n-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

\n

三、辅助信息

\n

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
\n-XX:+PrintGC
\n输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
\n                [Full GC 121376K->10414K(130112K), 0.0650971 secs]

\n

-XX:+PrintGCDetails
\n输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
\n                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

\n

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
\n输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
\n-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
\n输出形式:Application time: 0.5291524 seconds
\n-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
\n输出形式:Total time for which application threads were stopped: 0.0468229 seconds
\n-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
\n输出形式:

\n
34.702: [GC {Heap before gc invocations=7:\n def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)\nfrom space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)\n  to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)\n tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)\n compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:\n def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)\n  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)\n  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)\n tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)\n compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n}\n, 0.0757599 secs]
\n

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
\n常见配置汇总

\n

3.1 堆设置

\n

-Xms:初始堆大小
\n-Xmx:最大堆大小
\n-XX:NewSize=n:设置年轻代大小
\n-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
\n-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
\n-XX:MaxPermSize=n:设置持久代大小

\n

3.2 收集器设置

\n

-XX:+UseSerialGC:设置串行收集器
\n-XX:+UseParallelGC:设置并行收集器
\n-XX:+UseParalledlOldGC:设置并行年老代收集器
\n-XX:+UseConcMarkSweepGC:设置并发收集器

\n

3.3 垃圾回收统计信息

\n

-XX:+PrintGC
\n-XX:+PrintGCDetails
\n-XX:+PrintGCTimeStamps
\n-Xloggc:filename

\n

3.4 并行收集器设置

\n

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
\n-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
\n-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

\n

3.5 并发收集器设置

\n

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
\n-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

\n

四、调优总结

\n

4.1 年轻代大小选择

\n

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
\n吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

\n

4.2 年老代大小选择

\n

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
\n并发垃圾收集信息
\n持久代并发收集次数
\n传统GC信息
\n花在年轻代和年老代回收上的时间比例
\n减少年轻代和年老代花费的时间,一般会提高应用的效率
\n吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

\n

4.3 较小堆引起的碎片问题

\n

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
\n-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
\n-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

\n", + "date_published": "2020-01-25T13:10:49.000Z", + "date_modified": "2024-02-16T04:00:26.000Z", + "authors": [], + "tags": [] + }, + { + "title": "1.历史与架构", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html", + "summary": "1.历史与架构 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。 由原本的ssh变成ssm,再变成ssm+shi...", + "content_html": "\n

各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
\n大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

\n
\"\"
\n

由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点有点意思这个页面。
\n本文从下面这几个方面来讲讲网站的建立:

\n
    \n
  1. 建站故事与网站架构
  2. \n
  3. lucene搜索的使用
  4. \n
  5. 使用quartz来定时备份数据库
  6. \n
  7. 使用百度统计api做日志系统
  8. \n
  9. Nginx小集群的搭建
  10. \n
  11. [数据库]
  12. \n
  13. 使用机器学习对微博进行分析
  14. \n
  15. 网站性能优化
  16. \n
  17. SEO优化
  18. \n
\n

1.建站故事与网站架构

\n

1.1建站过程

\n

起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是杨青的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。

\n
\"\"
\n

第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。

\n

最终版本在考虑时,也找了很多模板,影响深刻的是tale欲思这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。
\n页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。
\n已经部署在腾讯云服务器上,存储使用了七牛云的cdn。

\n

1.2 网站整体技术架构

\n

最终版的技术架构图如下:

\n
\"\"
\n

网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了张开涛的博客。

\n
\"\"
\n

运行流程分析

\n
    \n
  1. 浏览器发送http请求。/blogdetail.html?blogid=1。
  2. \n
  3. tomcat容器初始化,顺序为context-param>listener>filter>servlet,此时,spring中的bean还没有被注入的,不建议在此处加载bean,网站声明了两个类(IPFilter和CacheControlFilter),IPFilter用来拦截IP,CacheControlFilter用来缓存。
  4. \n
  5. 初始化Spring。
  6. \n
  7. DispatcherServlet——>HandlerMapping进行请求到处理的映射,HandlerMapping将“/blogdetail”路径直接映射到名字为“/blogdetail”的Bean进行处理,即BlogController。
  8. \n
  9. 自定义拦截器,其中BaseIntercepter实现了HandleInterceptor的接口,用来记录每次访问的链接以及后台响应的时间。
  10. \n
  11. DispatcherServlet——> SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter将HandlerExecutionChain中的处理器适配为BlogController。
  12. \n
  13. BlogController执行查询,取得结果集返回数据。
  14. \n
  15. blogdetail(ModelAndView的逻辑视图名)——>InternalResourceViewResolver, InternalResourceViewResolver使用JstlView,具体视图页面在/blogdetail.jsp。
  16. \n
  17. JstlView(/blogdetail.jsp)——>渲染,将在处理器传入的模型数据(blog=Blog!)在视图中展示出来。
  18. \n
  19. 返回响应。
  20. \n
\n

1.3 日志系统

\n

日志系统架构如下:

\n
\"\"
\n

日志系统曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。
\n出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。

\n

1.4 【有点意思】自然语言处理

\n

本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击这里尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。

\n
\"\"
\n

总结

\n

断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:GitHub上求给个Star,以后面试能讲讲这个网站。

\n", + "image": "https://github-images.wenzhihuai.com/images/600-20240126113210252.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "2.Lucene的使用", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html", + "summary": "2.Lucene的使用 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。 Lucene的整体架构 imageimage 搜索引擎的几个重要概念: 倒排索引:将...", + "content_html": "\n

首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

\n

Lucene的整体架构

\n
\"image\"
image
\n

搜索引擎的几个重要概念:

\n
    \n
  1. \n

    倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。
    \n\"\"

    \n
  2. \n
  3. \n

    停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。

    \n
  4. \n
  5. \n

    排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。

    \n
  6. \n
\n

Lucene中的几个概念

\n
    \n
  1. 文档(Document):文档是一系列域的组合,文档的域则代表一系列域文档相关的内容。
  2. \n
  3. 域(Field):每个文档可以包含一个或者多个不同名称的域。
  4. \n
  5. 词(Term):Term是搜索的基本单元,与Field相对应,包含了搜索的域的名称和关键词。
  6. \n
  7. 查询(Query):一系列Term的条件组合,成为TermQuery,但也有可能是短语查询等。
  8. \n
  9. 分词器(Analyzer):主要是用来做分词以及去除停用词的处理。
  10. \n
\n

索引的建立

\n
\"\"
\n

索引的搜索

\n
\"\"
\n

lucene在本网站的使用:

\n
    \n
  1. 搜索 2. 自动分词
  2. \n
\n

一、搜索

\n

注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:

\n
<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-core</artifactId><!--lucene核心-->\n    <version>${lucene.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-analyzers-common</artifactId><!--分词器-->\n    <version>${lucene.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->\n    <version>${lucene.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-queryparser</artifactId><!--格式化-->\n    <version>${lucene.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->\n    <version>${lucene.version}</version>\n</dependency>
\n
    \n
  1. 构建索引
  2. \n
\n
Directory dir = FSDirectory.open(Paths.get(\"blog_index\"));//索引存储的位置\nSmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器\nIndexWriterConfig config = new IndexWriterConfig(analyzer);\nIndexWriter writer = new IndexWriter(dir, config);\nDocument doc = new Document();\ndoc.add(new TextField(\"title\", blog.getTitle(), Field.Store.YES)); //对标题做索引\ndoc.add(new TextField(\"content\", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引\nwriter.addDocument(doc);\nwriter.close();
\n
    \n
  1. 更新与删除
  2. \n
\n
IndexWriter writer = getWriter();\nDocument doc = new Document();\ndoc.add(new TextField(\"title\", blog.getTitle(), Field.Store.YES));\ndoc.add(new TextField(\"content\", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));\nwriter.updateDocument(new Term(\"blogid\", String.valueOf(blog.getBlogid())), doc);   //更新索引\nwriter.close();
\n
    \n
  1. 查询
  2. \n
\n
private static void search_index(String keyword) {\n    try {\n        Directory dir = FSDirectory.open(Paths.get(\"blog_index\")); //获取要查询的路径,也就是索引所在的位置\n        IndexReader reader = DirectoryReader.open(dir);\n        IndexSearcher searcher = new IndexSearcher(reader);\n        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();\n        QueryParser parser = new QueryParser(\"content\", analyzer); //查询解析器\n        Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象\n        TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,\n        for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果\n            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档\n            System.out.println(doc.get(\"title\")); //fullPath是刚刚建立索引的时候我们定义的一个字段\n        }\n        reader.close();\n    } catch (IOException | ParseException e) {\n        logger.error(e.toString());\n    }\n}
\n
    \n
  1. 高亮
  2. \n
\n
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);\nSimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter(\"<b><font color='red'>\", \"</font></b>\");\nHighlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);\nhighlighter.setTextFragmenter(fragmenter);\nfor (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果\n    Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档\n    String title = doc.get(\"title\");\n    TokenStream tokenStream = analyzer.tokenStream(\"title\", new StringReader(title));\n    String hTitle = highlighter.getBestFragment(tokenStream, title);\n    System.out.println(hTitle);\n}
\n

结果

\n
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
\n
    \n
  1. 分页
    \n目前lucene分页的方式主要有两种:
    \n(1). 每次都全部查询,然后通过截取获得所需要的记录。由于采用了分词与倒排索引,所有速度是足够快的,但是在数据量过大的时候,占用内存过大,容易造成内存溢出
    \n(2). 使用searchAfter把数据保存在缓存里面,然后再去取。这种方式对大量的数据友好,但是当数据量比较小的时候,速度会相对慢。
    \nlucene中使用searchafter来筛选顺序
  2. \n
\n
ScoreDoc lastBottom = null;//相当于pageSize\nBooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();\nQueryParser parser1 = new QueryParser(\"title\", analyzer);//对文章标题进行搜索\nQuery query1 = parser1.parse(q);\nbooleanQuery.add(query1, BooleanClause.Occur.SHOULD);\nTopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits);  //lastBottom(pageSize),pagehits(pagenum)
\n
    \n
  1. 使用效果
    \n全部代码放在这里,代码写的不太好,光从代码规范上就不咋地。在网页上的使用效果如下:
  2. \n
\n
\"\"
\n

二、lucene自动补全

\n

百度、谷歌等在输入文字的时候会弹出补全框,如下图:

\n
\"\"
\n

在搭建lucene自动补全的时候,也有考虑过使用SQL语句中使用like来进行,主要还是like对数据库压力会大,而且相关度没有lucene的高。主要使用了官方suggest库以及autocompelte.js这个插件。
\nsuggest的原理看这,以及索引结构看这

\n

使用:

\n
    \n
  1. 导入maven包
  2. \n
\n
<dependency>\n    <groupId>org.apache.lucene</groupId>\n    <artifactId>lucene-suggest</artifactId>\n    <version>6.6.0</version>\n</dependency>
\n
    \n
  1. 如果想将结果反序列化,声明实体类的时候要加上:
  2. \n
\n
public class Blog implements Serializable {
\n
    \n
  1. 实现InputIterator接口
  2. \n
\n
InputIterator的几个方法:\nlong weight():返回的权重值,大小会影响排序,默认是1L\nBytesRef payload():对某个对象进行序列化\nboolean hasPayloads():是否有设置payload信息\nSet<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤\nboolean hasContexts():判断是否有下一个,默认为false
\n
public class BlogIterator implements InputIterator {\n    /**\n     * logger\n     */\n    private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);\n    private Iterator<Blog> blogIterator;\n    private Blog currentBlog;\n\n    public BlogIterator(Iterator<Blog> blogIterator) {\n        this.blogIterator = blogIterator;\n    }\n\n    @Override\n    public boolean hasContexts() {\n        return true;\n    }\n\n    @Override\n    public boolean hasPayloads() {\n        return true;\n    }\n\n    public Comparator<BytesRef> getComparator() {\n        return null;\n    }\n\n    @Override\n    public BytesRef next() {\n        if (blogIterator.hasNext()) {\n            currentBlog = blogIterator.next();\n            try {\n                //返回当前Project的name值,把blog类的name属性值作为key\n                return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes(\"utf8\"));\n            } catch (Exception e) {\n                e.printStackTrace();\n                return null;\n            }\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * 将Blog对象序列化存入payload\n     * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法\n     */\n    @Override\n    public BytesRef payload() {\n        try {\n            ByteArrayOutputStream bos = new ByteArrayOutputStream();\n            ObjectOutputStream out = new ObjectOutputStream(bos);\n            out.writeObject(currentBlog);\n            out.close();\n            BytesRef bytesRef = new BytesRef(bos.toByteArray());\n            return bytesRef;\n        } catch (IOException e) {\n            logger.error(\"\", e);\n            return null;\n        }\n    }\n\n    /**\n     * 文章标题\n     */\n    @Override\n    public Set<BytesRef> contexts() {\n        try {\n            Set<BytesRef> regions = new HashSet<BytesRef>();\n            regions.add(new BytesRef(currentBlog.getTitle().getBytes(\"UTF8\")));\n            return regions;\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(\"Couldn't convert to UTF-8\");\n        }\n    }\n\n    /**\n     * 返回权重值,这个值会影响排序\n     * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值\n     */\n    @Override\n    public long weight() {\n        return currentBlog.getHits();   //change to hits\n    }\n}
\n
    \n
  1. ajax 建立索引
  2. \n
\n
/**\n * ajax建立索引\n */\n@Override\npublic void ajaxbuild() {\n    try {\n        Directory dir = FSDirectory.open(Paths.get(\"autocomplete\"));\n        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();\n        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);\n        //创建Blog测试数据\n        List<Blog> blogs = blogMapper.getAllBlog();\n        suggester.build(new BlogIterator(blogs.iterator()));\n    } catch (IOException e) {\n        System.err.println(\"Error!\");\n    }\n}
\n
    \n
  1. 查找
    \n因为有些文章的标题是一样的,先对list排序,将标题短的放前面,长的放后面,然后使用LinkHashSet来存储。
  2. \n
\n
@Override\npublic Set<String> ajaxsearch(String keyword) {\n    try {\n        Directory dir = FSDirectory.open(Paths.get(\"autocomplete\"));\n        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();\n        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);\n        List<String> list = lookup(suggester, keyword);\n        list.sort(new Comparator<String>() {\n            @Override\n            public int compare(String o1, String o2) {\n                if (o1.length() > o2.length()) {\n                    return 1;\n                } else {\n                    return -1;\n                }\n            }\n        });\n        Set<String> set = new LinkedHashSet<>();\n        for (String string : list) {\n            set.add(string);\n        }\n        ssubSet(set, 7);\n        return set;\n    } catch (IOException e) {\n        System.err.println(\"Error!\");\n        return null;\n    }\n}
\n
    \n
  1. controller层
  2. \n
\n
@RequestMapping(\"ajaxsearch\")\npublic void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {\n    String keyword = request.getParameter(\"keyword\");\n    if (StringUtils.isEmpty(keyword)) {\n        return;\n    }\n    Set<String> set = blogService.ajaxsearch(keyword);\n    Gson gson = new Gson();\n    response.getWriter().write(gson.toJson(set));//返回的数据使用json\n}
\n
    \n
  1. ajax来提交请求
    \nautocomplete.js源代码与介绍:https://github.com/xdan/autocomplete
  2. \n
\n
<link rel=\"stylesheet\" href=\"js/autocomplete/jquery.autocomplete.css\">\n<script src=\"js/autocomplete/jquery.autocomplete.js\" type=\"text/javascript\"></script>\n<script type=\"text/javascript\">\n    /******************** remote start **********************/\n    $('#remote_input').autocomplete({\n        source: [\n            {\n                url: \"ajaxsearch.html?keyword=%QUERY%\",\n                type: 'remote'\n            }\n        ]\n    });\n    /********************* remote end ********************/\n</script>
\n
    \n
  1. 效果:
    \n\"\"
  2. \n
\n

欢迎访问我的个人网站

\n

参考:
\nhttps://www.ibm.com/developerworks/cn/java/j-lo-lucene1/

\n

http://iamyida.iteye.com/blog/2205114

\n", + "image": "https://github-images.wenzhihuai.com/images/lucenejoijfoaiwheifuwhifu.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "3.定时任务", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html", + "summary": "3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。 二.主要组成部分 JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种...", + "content_html": "\n

先看一下Quartz的架构图:

\n
\"\"
\n

一.特点:

\n
    \n
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. \n
  3. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  4. \n
  5. 分布式和集群能力。
  6. \n
\n

二.主要组成部分

\n
    \n
  1. JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种信息。
  2. \n
  3. JobDetail:QUartz的执行任务的类,通过newInstance的反射机制实例化Job。
  4. \n
  5. Trigger: Job的时间触发规则。主要有SimpleTriggerImpl和CronTriggerImpl两个实现类。
  6. \n
  7. Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。
  8. \n
  9. Scheduler:由上图可以看出,Scheduler是Quartz独立运行的容器。其中,Trigger和JobDetail可以注册到Scheduler中。
  10. \n
  11. ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
  12. \n
\n

三、Quartz设计

\n
    \n
  1. properties file
    \n官网中表明:quartz中使用了quartz.properties来对quartz进行配置,并保留在其jar包中,如果没有定义则默认使用改文件。
  2. \n
  3. \n
\n

四、使用

\n
    \n
  1. hello world!代码在这
  2. \n
  3. 本网站中使用quartz来对数据库进行备份,与Spring结合
    \n(1)导入spring的拓展包,其协助spring集成第三方库:邮件服务、定时任务、缓存等。。。
  4. \n
\n
<dependency>\n    <groupId>org.springframework</groupId>\n    <artifactId>spring-context-support</artifactId>\n    <version>4.2.6.RELEASE</version>\n</dependency>
\n

(2)导入quartz包

\n
<dependency>\n    <groupId>org.quartz-scheduler</groupId>\n    <artifactId>quartz</artifactId>\n    <version>2.3.0</version>\n</dependency>
\n

(3)mysql远程备份
\n使用命令行工具仅仅需要一行:

\n
mysqldump -u [username] -p[password] -h [hostip] database > file
\n

但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed

\n
<dependency>\n    <groupId>ch.ethz.ganymed</groupId>\n    <artifactId>ganymed-ssh2</artifactId>\n    <version>262</version>\n</dependency>
\n

完整代码如下:

\n
@Component(\"mysqlService\")//在spring中注册一个mysqlService的Bean\npublic class MysqlUtil {\n    ...\n    StringBuffer sb = new StringBuffer();\n    sb.append(\"mysqldump -u \" + username + \" -p\" + password + \" -h \" + host + \" \" +\n            database + \" >\" + file);\n    String sql = sb.toString();\n    Connection connection = new Connection(s_host);\n    connection.connect();\n    boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证\n    if (!isAuth) {\n        logger.error(\"server login error\");\n    }\n    Session session = connection.openSession();\n    session.execCommand(sql);//执行linux语句\n    InputStream stdout = new StreamGobbler(session.getStdout());\n    BufferedReader br = new BufferedReader(new InputStreamReader(stdout));\n    ...\n}
\n

(4)spring中配置quartz

\n
<bean id=\"jobDetail\" class=\"org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean\">\n    <property name=\"targetObject\" ref=\"mysqlService\"/>\n    <property name=\"targetMethod\" value=\"exportDataBase\"/>\n</bean>\n<!--定义触发时间  -->\n<bean id=\"myTrigger\" class=\"org.springframework.scheduling.quartz.CronTriggerFactoryBean\">\n    <property name=\"jobDetail\" ref=\"jobDetail\"/>\n    <!-- cron表达式,每周五2点59分运行-->\n    <property name=\"cronExpression\" value=\"0 59 2 ? * FRI\"/>\n</bean>\n<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->\n<bean id=\"scheduler\" class=\"org.springframework.scheduling.quartz.SchedulerFactoryBean\">\n    <property name=\"triggers\">\n        <list>\n            <ref bean=\"myTrigger\"/>\n        </list>\n    </property>\n</bean>
\n

(5)java完整文件在这

\n

Spring的高级特性之定时任务

\n

java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。

\n

(1)spring配置文件中配置:

\n
<task:annotation-driven/>
\n

(2)最简单的例子,在所需要的函数上添加定时任务即可运行

\n
    @Scheduled(fixedRate = 5000)\n    public void reportCurrentTime() {\n        System.out.println(\"每隔5秒运行一次\" + sdf.format(new Date()));\n    }
\n

(3)运行的时候会报错:

\n
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)\n\tat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)\n\tat org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)\n\tat org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)\n\tat org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)\n\tat org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)\n\tat org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)\n\tat org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)
\n

参考:
\n1.http://blog.csdn.net/oarsman/article/details/52801877
\n2.http://stackoverflow.com/questions/31199888/spring-task-scheduler-no-qualifying-bean-of-type-org-springframework-scheduli
\nSpring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:

\n

1.尝试从配置中找到一个TaskScheduler Bean

\n

2.寻找ScheduledExecutorService Bean

\n

3.使用默认的scheduler
\n修改log4j.properties即可:
\nlog4j.logger.org.springframework.scheduling=INFO
\n其实这个功能不影响定时器的功能。
\n(4)结果:

\n
每隔5秒运行一次14:44:34\n每隔5秒运行一次14:44:39\n每隔5秒运行一次14:44:44
\n
", + "image": "https://github-images.wenzhihuai.com/images/11627468_1438829844Zbz8.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "4.日志系统.md", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html", + "summary": "4.日志系统.md 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使...", + "content_html": "\n

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

\n

建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:

\n
\"\"
\n

企业级的网站日志不能公开,但是我的是个人网站,用来跟大家一起学习的,所以,需要将百度的统计页面展示出来,但是,百度并不提供日志的图像,只提供API给开发者调用,而且还限制访问次数,一天不能超过2000次,这个对于实时统计来说,确实不够,所以只能展示前几天的访问统计。这里的日志系统分为三个步骤:1.API获取数据;2.存储数据;3.展示数据。页面效果如下,也可以点开我的网站的日志系统:

\n
\"\"
\n
\"\"
\n
\"\"
\n

百度统计提供了Tongji API的Java和Python版本,这两个版本及其复杂,可用性极低,所以,本人用Python写了个及其简单的通用版本,整体只有28行,代码在这,https://github.com/Zephery/baidutongji。下面是具体过程

\n

1.网站代码安装

\n

先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。

\n
<script>\nvar _hmt = _hmt || [];\n(function() {\n  var hm = document.createElement(\"script\");\n  hm.src = \"https://hm.baidu.com/hm.js?code\";\n  var s = document.getElementsByTagName(\"script\")[0];\n  s.parentNode.insertBefore(hm, s);\n})();\n</script>
\n

同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。

\n
\"\"
\n

至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。

\n

2.根据API获取数据

\n

官网的API详细的记录了接口的参数以及解释,
\n链接:https://api.baidu.com/json/tongji/v1/ReportService/getData,详细的官方报告请访问官网TongjiApi
\n所需参数(必须):

\n

| 参数名称 | 参数类型 |描述 |
\n|

\n", + "image": "https://github-images.wenzhihuai.com/images/300.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "5.小集群部署.md", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html", + "summary": "5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象...", + "content_html": "\n

欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
\n洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

\n

nginx负载均衡

\n

一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。
\n通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自张开涛的《亿级流量网站架构核心技术》

\n
\"\"
\n

本站并没有那么多的服务器,目前只有两台,搭建不了那么大型的架构,就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构:

\n
\"\"
\n

其中服务器A(119.23.46.71)为深圳节点,服务器B(47.95.10.139)为北京节点,搭建Nginx之后流量是这么走的:user->A->B-A->user或者user->A->user,第一条中A将请求转发给B,然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习,所以请不要考虑因为地域导致延时的问题。。。。下面是过程。

\n

1.1 Nginx的安装

\n

可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从https://pkgs.org/download/nginx可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。

\n
\"\"
\n

启动后页面如下:

\n
\"\"
\n

记一下常用命令

\n
启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。\n[root@beijingali ~]# nginx          #启动nginx\n[root@beijingali ~]# nginx -s reload         #重启nginx\n[root@beijingali ~]# nginx -t           #校验nginx配置文件\nnginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful
\n

1.2 Nginx的配置

\n

1.2.1 负载均衡算法

\n

Nginx常用的算法有:
\n(1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。
\n(2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。
\n(3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。
\n使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是tomcat session的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。

\n
http{\n    ...\n    upstream backend {\n        hash $uri;\n        # 北京节点\n        server 47.95.10.139:8080;\n        # 深圳节点\n        server 119.23.46.71:8080;\n    }\n\n    server {\n        ...\n        location / {\n            root   html;\n            index  index.html index.htm;\n            proxy_pass http://backend;\n            ...\n        }\n    ...
\n

1.2.2 日志格式

\n

之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧

\n
    ...\n    log_format main '{\"@timestamp\":\"$time_iso8601\",'\n                    '\"host\":\"$server_addr\",'\n                    '\"clientip\":\"$remote_addr\",'\n                    '\"size\":$body_bytes_sent,'\n                    '\"responsetime\":$request_time,'\n                    '\"upstreamtime\":\"$upstream_response_time\",'\n                    '\"upstreamhost\":\"$upstream_addr\",'\n                    '\"http_host\":\"$host\",'\n                    '\"url\":\"$uri\",'\n                    '\"xff\":\"$http_x_forwarded_for\",'\n                    '\"referer\":\"$http_referer\",'\n                    '\"agent\":\"$http_user_agent\",'\n                    '\"status\":\"$status\"}';\n    access_log  logs/access.log  main;\n    ...
\n

1.2.3 HTTP反向代理

\n

配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考nginx 配置。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:

\n
        location / {\n            root   html;\n            index  index.html index.htm;\n            proxy_pass http://backend;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header Host $host;\n            proxy_set_header REMOTE-HOST $remote_addr;\n        }
\n

(1)proxy_set_header X-real-ip $remote_addr;
\n其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取:
\nrequest.getAttribute(\"X-real-ip\")

\n

(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

\n

X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
\n$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。

\n

1.2.4 HTTPS

\n

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:

\n
\"\"
\n

首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:

\n
        ssl_certificate 1_www.wenzhihuai.com_bundle.crt;    #主要文件路径\n        ssl_certificate_key 2_www.wenzhihuai.com.key;\n        ssl_session_timeout 5m;         # 超时时间\n        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置\n        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置\n        ssl_prefer_server_ciphers on;
\n

至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可

\n

1.2.5 失败重试

\n

配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试

\n
upstream backend_server {\n    server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;\n    server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;\n}
\n

session共享

\n

分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自tomcat 集群中 session 共
\n1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。)
\n2.使用 tomcat sessionmanager 方法存储。(直接配置即可)
\n3.使用 terracotta 服务器共享。(不知道,不了解)
\n4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)

\n

本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考Spring-Session官网。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。

\n

首先,添加相关依赖

\n
        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-data-redis</artifactId>\n            <version>1.3.1.RELEASE</version>\n            <type>pom</type>\n        </dependency>\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n            <version>${jedis.version}</version>\n        </dependency>
\n

新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:

\n
    <!-- redis -->\n    <bean id=\"jedisPoolConfig\" class=\"redis.clients.jedis.JedisPoolConfig\">\n    </bean>\n\n    <bean id=\"jedisConnectionFactory\"\n          class=\"org.springframework.data.redis.connection.jedis.JedisConnectionFactory\">\n        <property name=\"hostName\" value=\"${host}\" />\n        <property name=\"port\" value=\"${port}\" />\n        <property name=\"password\" value=\"${password}\" />\n        <property name=\"timeout\" value=\"${timeout}\" />\n        <property name=\"poolConfig\" ref=\"jedisPoolConfig\" />\n        <property name=\"usePool\" value=\"true\" />\n    </bean>\n\n    <bean id=\"redisTemplate\" class=\"org.springframework.data.redis.core.StringRedisTemplate\">\n        <property name=\"connectionFactory\" ref=\"jedisConnectionFactory\" />\n    </bean>\n\n    <!-- 将session放入redis -->\n    <bean id=\"redisHttpSessionConfiguration\"\n          class=\"org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration\">\n        <property name=\"maxInactiveIntervalInSeconds\" value=\"1800\" />\n    </bean>
\n

然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截

\n
<filter>\n    <filter-name>springSessionRepositoryFilter</filter-name>\n    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>\n</filter>\n<filter-mapping>\n    <filter-name>springSessionRepositoryFilter</filter-name>\n    <url-pattern>/*</url-pattern>\n    <dispatcher>REQUEST</dispatcher>\n    <dispatcher>ERROR</dispatcher>\n</filter-mapping>
\n

配置完成,使用RedisDesktopManager查看结果:

\n
\"\"
\n

测试:

\n

访问http://www.wenzhihuai.com
\ntail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录

\n
\"\"
\n

访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。

\n
\"\"
\n

点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。

\n
\"\"
\n

值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。
\n欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star
\n个人网站网址:http://www.wenzhihuai.com
\n个人网站代码地址:https://github.com/Zephery/newblog

\n", + "image": "https://github-images.wenzhihuai.com/images/20171018044732.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "6.数据库备份", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html", + "summary": "6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下: 果然,...", + "content_html": "\n

先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

\n
\"\"
\n

由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:

\n
\"\"
\n

果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:

\n
\"\"
\n

大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:

\n
\"\"
\n

一、MySql的复制

\n

数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
\n(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
\n(2)slave将master的binary log events拷贝到它的中继日志(relay log);
\n(3)slave重做中继日志中的事件,将改变反映它自己的数据。

\n
\"\"
\n

该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。
\n下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。
\nSQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。
\n此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。
\nMySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。

\n

二、配置过程

\n

2.1 创建所用的复制账号

\n

由于是个自己的小网站,就不做过多的操作了,直接使用root账号

\n

2.2 配置master

\n

接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。

\n
[client]\ndefault-character-set=utf8\n\n[mysqld]\ncharacter_set_server=utf8\ninit_connect= SET NAMES utf8\n\ndatadir=/var/lib/mysql\nsocket=/var/lib/mysql/mysql.sock\n\nsymbolic-links=0\n\nlog-error=/var/log/mysqld.log\npid-file=/var/run/mysqld/mysqld.pid\n\n# master\nlog-bin=mysql-bin\n# 设为基于行的复制\nbinlog-format=ROW\n# 设置server的唯一id\nserver-id=2\n# 忽略的数据库,不使用备份\nbinlog-ignore-db=information_schema\nbinlog-ignore-db=cluster\nbinlog-ignore-db=mysql\n# 要进行备份的数据库\nbinlog-do-db=myblog
\n

重启Mysql之后,查看主库状态,show master status。

\n
\"\"
\n

其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。

\n

2.3 配置slave

\n

从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。

\n
\n[client]\ndefault-character-set=utf8\n\n[mysqld]\ncharacter_set_server=utf8\ninit_connect= SET NAMES utf8\n\ndatadir=/var/lib/mysql\nsocket=/var/lib/mysql/mysql.sock\n\nsymbolic-links=0\n\nlog-error=/var/log/mysqld.log\npid-file=/var/run/mysqld/mysqld.pid\n\n# slave\nlog-bin=mysql-bin\n# 服务器唯一id\nserver-id=3\n# 不备份的数据库\nbinlog-ignore-db=information_schema\nbinlog-ignore-db=cluster\nbinlog-ignore-db=mysql\n# 需要备份的数据库\nreplicate-do-db=myblog\n# 其他相关信息\nslave-skip-errors=all\nslave-net-timeout=60\n# 开启中继日志\nrelay_log         = mysql-relay-bin\n# \nlog_slave_updates = 1\n# 防止改变数据\nread_only         = 1
\n

重启slave,同时启动复制,还需要调整一下命令。

\n
mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;
\n

可以看见slave已经开始进行同步了。我们使用show slave status\\G来查看slave的状态。

\n
\"\"
\n

其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。

\n

某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:

\n
\"\"
\n

原理相对比较简单:

\n

1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
\n2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
\n3.canal解析binary log对象(原始为byte流)

\n

其中,配置过程如下:https://github.com/alibaba/canal,可以搭配Zookeeper使用。在ZKUI中能够查看到节点:

\n
\"\"
\n

一般情况下,还要配合阿里的另一个开源产品使用otter,相关文档还是找找GitHub吧,个人搭建完了之后,用起来还是不如直接使用mysql的主主复制,而且异地机房同步这种大企业才有的业务。

\n

公司又要996了,实在是忙不过来,感觉自己写的还是急躁了点,困==

\n", + "image": "https://github-images.wenzhihuai.com/images/20171018051437-20240126113709561.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "7.那些牛逼的插件", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html", + "summary": "7.那些牛逼的插件 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Ja...", + "content_html": "\n

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
\n建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
\n本站主要用到的插件有:
\n1.wowslider
\n2.畅言
\n3.Editor.md
\n4.highchart、echart
\n5.百度分享
\n6.waterfall.js
\n7.心知天气
\n8.标签云

\n

wowslider

\n

可能是我这网站中最炫的东西了,图片能够自动像幻灯片一样自动滚动,让网站的首页一看起来就高大上,简直就是建站必备的东西,而且安装也及其简单,有兴趣可以点击官网看看。GitHub里也开放了源代码。安装过程:自己选择“幻灯片”切换效果,保存为html就行了,WORDPREESS中好像有集成这个插件的,做的还更好。感兴趣可以点击我的博客首页看一看。

\n
\"\"
\n

不过还有个值得注意的问题,就是wowslider里面带有一个googleapis的服务,即https://fonts.googleapis.com/css?family=Arimo&subset=latin,cyrillic,latin-ext,由于一般用户不能访问谷歌,会导致网页加载速度及其缓慢,所以,去掉为妙

\n

畅言

\n

作为社交评论的工具,虽然说表示还是想念以前的多说,但是畅言现在做得还是好了,有评论审核,评论导出导入等功能,如果浏览量大的话,还能提供广告服务,让站长也能拿到一丢丢的广告费。本博客中使用了畅言的基本评论、获取某篇文章评论数的功能。可以到我这里留言

\n
\"\"
\n

Editor.md

\n

一款能将markdown解析为html的插件,国人的作品,博客的文章编辑器一开始想使用的是markdown,想法是:写文章、保存数据库都是markdown格式的,保存在数据库中,读取时有需要解析markdown,这个过程是有点耗时的,但是相比把html式的网页保存在数据库中友好点吧,因为如果一篇文章比较长的话,转成html的格式,光是大小估计也得超过几十kb?所以,还是本人选择的是一切都用源markdown。
\neditor.md,是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。页面看起来还是美观的,相比WORDPRESS的那些牛逼插件还差那么一点点,不过从普通人的眼光来看,应该是可以的了。此处,我用它作为解析网页的利器,还有就是后台编辑也是用的这个。

\n
\"\"
\n

代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。

\n
\"\"
\n

图表

\n

目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)

\n
\"\"
\n

不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。

\n
\"\"
\n

百度分享

\n

作为一个以博客为主的网站,免不了使用一些社会化分享的工具,目前主要是jiathis和百度分享,这两者的ui都是相似的(丑爆了)。凭我个人的感觉,jiathis加载实在是太过于缓慢,这点是无法让人忍受的,只好投靠百度。百度分享类似于jiathis,安装也很简单,具体见官网http://share.baidu.com/code/advance#tools。一直点点点,配置完之后,就是下图这种,丑爆了是不是?

\n
\"\"
\n

好在对它的美观改变不是很难,此处参考了别人的UI设计,原作者我忘记怎么联系他了。其原理主要是使用图片来替换掉原本的东西。完整的源码可以点击此处

\n
#share a {\n    width: 34px;\n    height: 34px;\n    padding: 0;\n    margin: 6px;\n    border-radius: 25px;\n    transition: all .4s;\n}\n/*主要的是替换掉backgound的背景图片*/\n#share a.bds_qzone {\n    background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;\n    background-size: 34px 34px;\n}
\n

改完之后的效果。

\n
\"\"
\n

瀑布流

\n

有段时间,瀑布流特别流行?还有段时间,瀑布流开始遭到各种抵制。。。看看知乎的人怎么说,大部分人不喜欢的原因是让人觉得视觉疲劳,不过瀑布流最大的好处还是有的:提高发现好图的效率以及图片列表页极强的视觉感染力。没错,我还是想把自己的网站弄得铉一些==,所以采用了瀑布流(不过效果不是很好,某些浏览器甚至加载出错),这个大bug有时间再改,毕竟花了很多时间做的这个,效果确实不咋地。目前主要的瀑布流有waterfall.jsmasory.js。这一块目前还不是很完善,希望能得到各位大佬的指点。

\n
\"\"
\n

天气插件

\n

此类咨询服务还是网上还是挺多的,这一块不知道是不是所谓的“画蛇添足”这部分,主要是我觉得网站右边这部分老是少了点什么,所以加上了天气插件。目前常用的天气插件有中国天气网心知天气等。安装方式见各自的官网,这里不再阐述,我使用的是心知天气。注意:心知天气限制流量的,一个小时内只能使用400次,如果超出了会失效,当然也可以付费使用。

\n
\"\"
\n

标签云

\n

标签云,弄得好的话应该说是一个网站的点缀。现在好像比较流行3D的标签云?像下面这种。

\n
\"\"
\n

从个人的网站风格来看,比较适应PHP形式的,有点颜色而又不绚丽的即可,之前用的跟分类的一样的样式,即双纵列的样式,美观度还行,虽然老是感觉有点怪怪的,如果某个标签的字数过长怎么办,岂不是要顶出div了。所以还是选择换另一种风格,最终偶然一次找到了下面这种,能够自适应宽度,颜色虽然鲜艳了点(以后有空再调一下吧),源码见style.css。 下图为目前的标签页。

\n
\"\"
\n

总结

\n

作为一个后端人员,调css和js真是痛苦==,好在坚持下来了,虽然还是很多不足,以后有时间慢慢改。说了那么多,感觉自己还是菜的抠脚。
\n题外话,搭建一个博客,对于一个新近程序员来说真的是锻炼自己的一个好机会,能够认识到从前端、java后台、linux、jvm等等知识,只是真的有点耗时间(还不如把时间耗在Spring源码),如果不采用别人的框架的话,付出的代价还是蛮大的(所以不要鄙视我啦)。没有什么能够一举两得,看自己的取舍吧。加油💪(ง •_•)ง

\n

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

\n", + "image": "https://github-images.wenzhihuai.com/images/20171121023427.png", + "date_published": "2020-01-21T02:38:05.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "一次jvm调优过程", + "url": "http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html", + "id": "http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html", + "summary": "一次jvm调优过程 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为...", + "content_html": "\n

前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
\n该程序task主要分为三个模块:
\nconsole进行一些cron的配置(表达式、任务名称、任务组等);
\nschedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
\nclient接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
\n整体架构跟github上开源的xxl-job类似,也可以参考一下。

\n

1. 启用jmx和远程debug模式

\n

容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:

\n
JAVA_DEBUG_OPTS=\" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n \"\nJAVA_JMX_OPTS=\" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false \"
\n

其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。

\n
\"\"
\n
\"\"
\n

堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。

\n

2. memory analyzer、jprofiler进行堆内存分析

\n

先从容器中dump出堆内存

\n
jmap -dump:live,format=b,file=heap.hprof 58
\n
\"\"
\n

由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。

\n

3. netty的方面的考虑

\n

另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。
\n首先是查看堆内存的中占比:

\n
\"\"
\n

可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:

\n
\"\"
\n

达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。

\n
\"\"
\n

声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。

\n
\"\"
\n

最终解决办法:
\n在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully

\n
\"\"
\n

提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了

\n
\"\"
\n

还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。

\n
\"\"
\n

4. 直接内存排查

\n

第一个想到的就是netty的直接内存,关掉,命令如下:

\n
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced
\n
\"\"
\n

查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题

\n

5. 推荐的直接内存排查方法

\n

5.1 pmap

\n

一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。

\n
pmap -d 58  | sort -n -k2\npmap -x 58  | sort -n -k3\ngrep rw-p /proc/$1/maps | sed -n 's/^\\([0-9a-f]*\\)-\\([0-9a-f]*\\) .*$/\\1 \\2/p' | while read start stop; do gdb --batch --pid $1 -ex \"dump memory $1-$start-$stop.dump 0x$start 0x$stop\"; done
\n

这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?

\n

5.2 [gperftools](https://github.com/gperftools/gperftools)

\n

起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。
\n根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,

\n
pprof --text /usr/bin/java java_58.0001.heap
\n
\"\"
\n

看着工具高大上的,似乎能找出linux的调用栈,

\n

6. 意外的结果

\n

毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。
\n修复后的结果如下图所示:

\n
\"\"
\n
\"\"
\n

总结

\n

定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。

\n

参考:
\n1.记一次线上内存泄漏问题的排查过程
\n2.Java堆外内存增长问题排查Case
\n3.Troubleshooting Native Memory Leaks in Java Applications

\n", + "image": "https://github-images.wenzhihuai.com/images/20200121102100646998927.png", + "date_published": "2020-01-21T02:28:56.000Z", + "date_modified": "2024-02-16T09:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Spark", + "url": "http://www.wenzhihuai.com/bigdata/", + "id": "http://www.wenzhihuai.com/bigdata/", + "summary": "Spark", + "content_html": "\n", + "date_published": "2019-08-25T08:27:20.000Z", + "date_modified": "2024-02-06T07:53:39.000Z", + "authors": [], + "tags": [] + }, + { + "title": "Java", + "url": "http://www.wenzhihuai.com/java/", + "id": "http://www.wenzhihuai.com/java/", + "summary": "Java", + "content_html": "\n", + "date_published": "2019-08-25T08:27:20.000Z", + "date_modified": "2024-05-18T19:54:10.000Z", + "authors": [], + "tags": [] + }, + { + "title": "目录", + "url": "http://www.wenzhihuai.com/kubernetes/", + "id": "http://www.wenzhihuai.com/kubernetes/", + "summary": "目录 包含CICD、Kubernetes", + "content_html": "\n

包含CICD、Kubernetes

\n", + "date_published": "2019-08-25T08:27:20.000Z", + "date_modified": "2024-02-16T09:22:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "8.基于贝叶斯的情感分析", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html", + "summary": "8.基于贝叶斯的情感分析", + "content_html": "\n", + "date_published": "2019-08-25T08:27:20.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "9.网站性能优化", + "url": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html", + "id": "http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html", + "summary": "9.网站性能优化", + "content_html": "\n", + "date_published": "2019-08-25T08:27:20.000Z", + "date_modified": "2024-05-11T18:24:35.000Z", + "authors": [], + "tags": [] + }, + { + "title": "DevOps平台.md", + "url": "http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html", + "id": "http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html", + "summary": "DevOps平台.md DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。 公司技术部目前几百人左右吧...", + "content_html": "\n

DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

\n

公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。

\n

一、自由风格的软件项目

\n

主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。

\n

完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。

\n
<project>\n    <actions/>\n    <description>xxx</description>\n    <properties>\n        <hudson.model.ParametersDefinitionProperty>\n            <parameterDefinitions>\n                <hudson.model.TextParameterDefinition>\n                    <name>buildParam</name>\n                    <defaultValue>v1</defaultValue>\n                </hudson.model.TextParameterDefinition>\n                <hudson.model.TextParameterDefinition>\n                    <name>codeBranch</name>\n                    <defaultValue>master</defaultValue>\n                </hudson.model.TextParameterDefinition>\n            </parameterDefinitions>\n        </hudson.model.ParametersDefinitionProperty>\n    </properties>\n    <scm class=\"hudson.plugins.git.GitSCM\">\n        <configVersion>2</configVersion>\n        <userRemoteConfigs>\n            <hudson.plugins.git.UserRemoteConfig>\n                <url>http://xxxxx.git</url>\n                <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>\n            </hudson.plugins.git.UserRemoteConfig>\n        </userRemoteConfigs>\n        <branches>\n            <hudson.plugins.git.BranchSpec>\n                <name>${codeBranch}</name>\n            </hudson.plugins.git.BranchSpec>\n        </branches>\n        <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>\n        <extensions/>\n    </scm>\n    <builders>\n        <hudson.tasks.Shell>\n            <command>ls</command>\n        </hudson.tasks.Shell>\n        <hudson.tasks.Maven>\n            <targets>clean package install -Dmaven.test.skip=true</targets>\n            <mavenName>mvn3.5.4</mavenName>\n        </hudson.tasks.Maven>\n        <com.cloudbees.dockerpublish.DockerBuilder>\n            <server>\n                <uri>unix:///var/run/docker.sock</uri>\n            </server>\n            <registry>\n                <url>http://xxxx</url>\n            </registry>\n            <repoName>xxx/xx</repoName>\n            <forcePull>true</forcePull>\n            <dockerfilePath>Dockerfile</dockerfilePath>\n            <repoTag>${buildParam}</repoTag>\n            <skipTagLatest>true</skipTagLatest>\n        </com.cloudbees.dockerpublish.DockerBuilder>\n    </builders>\n    <publishers>\n        <com.xxxx.notifications.Notifier/>\n    </publishers>\n</project>
\n

二、优化之后的CICD

\n

上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。

\n
\"\"
\n

目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。

\n

没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。

\n

三、调研期

\n

由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。

\n

分布式构建jenkins x 首先要解决的是多个构建同时运行的问题,很久之前就调研过jenkins x,它必须要使用在kubernetes上,由于当时官方文档不全,而且我们的DevOps项目处于初始期,所有没有使用。jenkins的master slave结构就不多说了。jenkins x应该说是个全家桶,包含了helm仓库、nexus仓库、docker registry等,代码是jenkins-x-image

\n
\"\"
\n

serverless jenkins 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。

\n

阿里云云效 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。

\n

微软Pipeline 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。

\n
\"\"
\n

谷歌tekton kubernetes的官方cicd,目前已用于kubernetes的release发版过程,目前也仅仅是与GitHub相结合,gitlab无法使用,全过程可使用yaml文件来创建,跑起来就是类似kubernetes的job一样,用完即销毁,可惜目前比较新,依旧处于alpha版本,无法用于生产。有兴趣可以参考下:Knative 初体验:CICD 极速入门

\n

四、产品化后的DevOps平台

\n

在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:

\n

4.1 Java代码扫描

\n

PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。

\n
\"\"
\n
stage('Clone') {\n    steps{\n        git branch: 'master', credentialsId: 'xxxx', url: \"xxx\"\n    }\n}\nstage('check') {\n    steps{\n        container('maven') {\n            echo \"mvn pmd:pmd\"\n        }\n    }\n}
\n

4.2 Java单元测试

\n

Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用\"mvn test\"命令来代替。

\n
\"\"
\n
\nstage('Clone') {\n    steps{\n        echo \"1.Clone Stage\"\n        git branch: 'master', credentialsId: 'xxxxx', url: \"xxxxxx\"\n    }\n}\nstage('test') {\n    steps{\n        container('maven') {\n            sh \"mvn test\"\n        }\n    }\n}
\n

4.3 Java构建并上传镜像

\n

镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。

\n
\n
\n

为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。

\n
\n
\n
\nstage('下载代码') {\n    steps{\n        echo \"1.Clone Stage\"\n        git branch: 'master', credentialsId: 'xxxxx', url: \"xxxxxx\"\n        script {\n            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()\n        }\n    }\n}\nstage('打包并构建镜像') {\n    steps{\n        container('maven') {\n            echo \"3.Build Docker Image Stage\"\n            sh \"mvn clean install -Dmaven.test.skip=true\"\n            sh \"docker build -f xxx/Dockerfile -t xxxxxx:${build_tag} .\"\n            sh \"docker push xxxxxx:${build_tag}\"\n        }\n    }\n}
\n

4.4 部署到阿里云k8s

\n

CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。

\n
def tmp = sh (\n    returnStdout: true,\n    script: \"kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \\$1}'\"\n)\n//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置\nif(tmp.equals('')){\n    sh \"helm init --client-only\"\n    sh \"\"\"helm repo add mychartmuseum http://xxxxxx \\\n                       --username myuser \\\n                       --password=mypass\"\"\"\n    sh \"\"\"helm install --set name=${JOB_NAME} \\\n                       --set namespace=${namespace} \\\n                       --set deployment.image=${image} \\\n                       --set deployment.imagePullSecrets=${harborProject} \\\n                       --name ${namespace}-${JOB_NAME} \\\n                       mychartmuseum/soa-template\"\"\"\n}else{\n    println \"已经存在,替换镜像\"\n    //epaas中一个pod的容器名称需要带上\"-0\"来区分\n    sh \"kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}\"\n}
\n

4.5 整体流程

\n

代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署

\n
\"\"
\n

pipeline:

\n
pipeline {\n    agent {\n        label \"jenkins-maven\"\n    }\n    stages{\n        stage('代码扫描,单元测试,镜像构建'){\n            parallel {\n                stage('并行任务一') {\n                    agent {\n                        label \"jenkins-maven\"\n                    }\n                    stages('Java代码扫描') {\n                        stage('Clone') {\n                            steps{\n                                git branch: 'master', credentialsId: 'xxxxxxx', url: \"xxxxxxx\"\n                            }\n                        }\n                        stage('check') {\n                            steps{\n                                container('maven') {\n                                    echo \"$BUILD_NUMBER\"\n                                }\n                            }\n                        }\n                    }\n                }\n                stage('并行任务二') {\n                    agent {\n                        label \"jenkins-maven\"\n                    }\n                    stages('Java单元测试') {\n                        stage('Clone') {\n                            steps{\n                                echo \"1.Clone Stage\"\n                                git branch: 'master', credentialsId: 'xxxxxxx', url: \"xxxxxxx\"\n                            }\n                        }\n                        stage('test') {\n                            steps{\n                                container('maven') {\n                                    echo \"3.Build Docker Image Stage\"\n                                    sh \"mvn -v\"\n                                }\n                            }\n                        }\n                    }\n                }\n                stage('并行任务三') {\n                    agent {\n                        label \"jenkins-maven\"\n                    }\n                    stages('java构建镜像') {\n                        stage('Clone') {\n                            steps{\n                                echo \"1.Clone Stage\"\n                                git branch: 'master', credentialsId: 'xxxxxxx', url: \"xxxxxxx\"\n                                script {\n                                    build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()\n                                }\n                            }\n                        }\n                        stage('Build') {\n                            steps{\n                                container('maven') {\n                                    echo \"3.Build Docker Image Stage\"\n                                    sh \"mvn clean install -Dmaven.test.skip=true\"\n                                    sh \"docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:${build_tag} .\"\n                                    sh \"docker push hub.gcloud.lab/rongqiyun/epaas:${build_tag}\"\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        stage('部署'){\n            stages('部署到容器云') {\n                stage('check') {\n                    steps{\n                        container('maven') {\n                            script{\n                                if (deploy_app == \"true\"){\n                                    def tmp = sh (\n                                        returnStdout: true,\n                                        script: \"kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \\$1}'\"\n                                    )\n                                    //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置\n                                    if(tmp.equals('')){\n                                        sh \"helm init --client-only\"\n                                        sh \"\"\"helm repo add mychartmuseum http://xxxxxx \\\n                                                           --username myuser \\\n                                                           --password=mypass\"\"\"\n                                        sh \"\"\"helm install --set name=${JOB_NAME} \\\n                                                           --set namespace=${namespace} \\\n                                                           --set deployment.image=${image} \\\n                                                           --set deployment.imagePullSecrets=${harborProject} \\\n                                                           --name ${namespace}-${JOB_NAME} \\\n                                                           mychartmuseum/soa-template\"\"\"\n                                    }else{\n                                        println \"已经存在,替换镜像\"\n                                        //epaas中一个pod的容器名称需要带上\"-0\"来区分\n                                        sh \"kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}\"\n                                    }\n                                }else{\n                                    println \"用户选择不部署代码\"\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}
\n

在jenkins x中查看:

\n
\"\"
\n

4.4 日志

\n

jenkins blue ocean步骤日志:

\n
\"\"
\n

云效中的日志:

\n
\"\"
\n

4.5 定时触发

\n
\"\"
\n
    triggers {\n        cron('H H * * *') //每天\n    }
\n

五、其他

\n

5.1 Gitlab触发

\n

pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。

\n

六、总结

\n

功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。

\n

DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。

\n

最后,DevOps是云原生的必经之路!!!

\n

文章同步:
\n博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
\n个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
\ngitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai

\n", + "image": "https://github-images.wenzhihuai.com/images/201908111044201770944400.png", + "date_published": "2019-08-22T01:35:46.000Z", + "date_modified": "2024-02-06T07:53:39.000Z", + "authors": [], + "tags": [] + } + ] +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..25076e6e --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + 首页 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interesting/chatgpt.html b/interesting/chatgpt.html new file mode 100644 index 00000000..489bce19 --- /dev/null +++ b/interesting/chatgpt.html @@ -0,0 +1,147 @@ + + + + + + + + + + 微信公众号-chatgpt智能客服搭建 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/idea\350\256\276\347\275\256.html" "b/interesting/idea\350\256\276\347\275\256.html" new file mode 100644 index 00000000..31aea32e --- /dev/null +++ "b/interesting/idea\350\256\276\347\275\256.html" @@ -0,0 +1,63 @@ + + + + + + + + + + Jetbrains Idea设置 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interesting/index.html b/interesting/index.html new file mode 100644 index 00000000..54e08536 --- /dev/null +++ b/interesting/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 好玩的 | 个人博客 + + + + + + + + + diff --git "a/interesting/mini\344\270\273\346\234\272/index.html" "b/interesting/mini\344\270\273\346\234\272/index.html" new file mode 100644 index 00000000..8bcf571d --- /dev/null +++ "b/interesting/mini\344\270\273\346\234\272/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + mini主机 | 个人博客 + + + + + + + + + diff --git "a/interesting/mini\344\270\273\346\234\272/mac\350\277\236\346\216\245\344\270\273\346\234\272.html" "b/interesting/mini\344\270\273\346\234\272/mac\350\277\236\346\216\245\344\270\273\346\234\272.html" new file mode 100644 index 00000000..d5f61fec --- /dev/null +++ "b/interesting/mini\344\270\273\346\234\272/mac\350\277\236\346\216\245\344\270\273\346\234\272.html" @@ -0,0 +1,87 @@ + + + + + + + + + + mac通过网线连接主机(fnOS) | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/mini\344\270\273\346\234\272/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html" "b/interesting/mini\344\270\273\346\234\272/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html" new file mode 100644 index 00000000..babacfc6 --- /dev/null +++ "b/interesting/mini\344\270\273\346\234\272/\344\271\260\344\272\206\344\270\252mini\344\270\273\346\234\272.html" @@ -0,0 +1,102 @@ + + + + + + + + + + 买了个mini主机 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interesting/starcraft-ai.html b/interesting/starcraft-ai.html new file mode 100644 index 00000000..5f345591 --- /dev/null +++ b/interesting/starcraft-ai.html @@ -0,0 +1,47 @@ + + + + + + + + + + StarCraft Ⅱ 人工智能教程 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interesting/tesla.html b/interesting/tesla.html new file mode 100644 index 00000000..a6b023e9 --- /dev/null +++ b/interesting/tesla.html @@ -0,0 +1,69 @@ + + + + + + + + + + Tesla api | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html" "b/interesting/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html" new file mode 100644 index 00000000..8b172c15 --- /dev/null +++ "b/interesting/\344\270\200\344\272\233\344\270\215\345\270\270\347\224\250\344\275\206\345\276\210\345\256\236\347\224\250\347\232\204\345\221\275\344\273\244.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 一些不常用但很实用的命令 | 个人博客 + + + + + + + + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html" new file mode 100644 index 00000000..36f54642 --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/1.\345\216\206\345\217\262\344\270\216\346\236\266\346\236\204.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 1.历史与架构 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html" new file mode 100644 index 00000000..ed6e2fae --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/10.\345\216\206\346\227\2668\345\271\264\346\234\200\347\273\210\346\224\271\347\211\210.html" @@ -0,0 +1,118 @@ + + + + + + + + + + 10.历时8年最终改版 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/2.Lucene\347\232\204\344\275\277\347\224\250.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/2.Lucene\347\232\204\344\275\277\347\224\250.html" new file mode 100644 index 00000000..a6735760 --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/2.Lucene\347\232\204\344\275\277\347\224\250.html" @@ -0,0 +1,272 @@ + + + + + + + + + + 2.Lucene的使用 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/3.\345\256\232\346\227\266\344\273\273\345\212\241.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/3.\345\256\232\346\227\266\344\273\273\345\212\241.html" new file mode 100644 index 00000000..f180467c --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/3.\345\256\232\346\227\266\344\273\273\345\212\241.html" @@ -0,0 +1,107 @@ + + + + + + + + + + 3.定时任务 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/4.\346\227\245\345\277\227\347\263\273\347\273\237.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/4.\346\227\245\345\277\227\347\263\273\347\273\237.html" new file mode 100644 index 00000000..be8901f7 --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/4.\346\227\245\345\277\227\347\263\273\347\273\237.html" @@ -0,0 +1,252 @@ + + + + + + + + + + 4.日志系统.md | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html" new file mode 100644 index 00000000..38163bbb --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/5.\345\260\217\351\233\206\347\276\244\351\203\250\347\275\262.html" @@ -0,0 +1,142 @@ + + + + + + + + + + 5.小集群部署.md | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html" new file mode 100644 index 00000000..77f68bf4 --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/6.\346\225\260\346\215\256\345\272\223\345\244\207\344\273\275.html" @@ -0,0 +1,107 @@ + + + + + + + + + + 6.数据库备份 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html" new file mode 100644 index 00000000..6e5c241c --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/7.\351\202\243\344\272\233\347\211\233\351\200\274\347\232\204\346\217\222\344\273\266.html" @@ -0,0 +1,59 @@ + + + + + + + + + + 7.那些牛逼的插件 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html" new file mode 100644 index 00000000..89f01ff7 --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/8.\345\237\272\344\272\216\350\264\235\345\217\266\346\226\257\347\232\204\346\203\205\346\204\237\345\210\206\346\236\220.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 8.基于贝叶斯的情感分析 | 个人博客 + + + + + + + + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html" new file mode 100644 index 00000000..24b71a7b --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/9.\347\275\221\347\253\231\346\200\247\350\203\275\344\274\230\345\214\226.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 9.网站性能优化 | 个人博客 + + + + + + + + + diff --git "a/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/index.html" "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/index.html" new file mode 100644 index 00000000..bea7a65d --- /dev/null +++ "b/interesting/\344\270\252\344\272\272\347\275\221\347\253\231/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 个人网站 | 个人博客 + + + + + + + + + diff --git "a/interesting/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html" "b/interesting/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html" new file mode 100644 index 00000000..c579772c --- /dev/null +++ "b/interesting/\345\237\272\344\272\216OLAP\345\201\232\344\270\232\345\212\241\347\233\221\346\216\247.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 基于OLAP做业务监控 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html" "b/interesting/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html" new file mode 100644 index 00000000..80190e0d --- /dev/null +++ "b/interesting/\345\260\217\347\250\213\345\272\217\345\217\215\347\274\226\350\257\221.html" @@ -0,0 +1,54 @@ + + + + + + + + + + 小程序反编译 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html" "b/interesting/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html" new file mode 100644 index 00000000..b58f5ce3 --- /dev/null +++ "b/interesting/\345\271\277\345\267\236\345\233\276\344\271\246\351\246\206\345\200\237\351\230\205\346\212\223\345\217\226.html" @@ -0,0 +1,154 @@ + + + + + + + + + + 广州图书馆借阅抓取 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/interesting/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html" "b/interesting/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html" new file mode 100644 index 00000000..b99fe4d9 --- /dev/null +++ "b/interesting/\345\274\200\345\217\221\345\267\245\345\205\267\346\225\264\347\220\206.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 开发工具整理 | 个人博客 + + + + + + + + + diff --git "a/interesting/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html" "b/interesting/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html" new file mode 100644 index 00000000..1668d480 --- /dev/null +++ "b/interesting/\347\250\213\345\272\217\345\221\230\345\211\257\344\270\232\346\216\242\347\264\242\344\271\213\347\224\265\345\225\206.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 程序员副业探索之电商 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interview/index.html b/interview/index.html new file mode 100644 index 00000000..0b175d29 --- /dev/null +++ b/interview/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Interview | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/interview/tiktok2023.html b/interview/tiktok2023.html new file mode 100644 index 00000000..56c49d04 --- /dev/null +++ b/interview/tiktok2023.html @@ -0,0 +1,47 @@ + + + + + + + + + + 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html" "b/java/JVM/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html" new file mode 100644 index 00000000..47df1a68 --- /dev/null +++ "b/java/JVM/JVM\350\260\203\344\274\230\345\217\202\346\225\260.html" @@ -0,0 +1,70 @@ + + + + + + + + + + JVM调优参数 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213.html" "b/java/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213.html" new file mode 100644 index 00000000..eb96665a --- /dev/null +++ "b/java/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213.html" @@ -0,0 +1,196 @@ + + + + + + + + + + Java内存模型(JMM) | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/JVM/cms.html b/java/JVM/cms.html new file mode 100644 index 00000000..d6aa18f8 --- /dev/null +++ b/java/JVM/cms.html @@ -0,0 +1,47 @@ + + + + + + + + + + CMS | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/JVM/g1.html b/java/JVM/g1.html new file mode 100644 index 00000000..f1bd080b --- /dev/null +++ b/java/JVM/g1.html @@ -0,0 +1,47 @@ + + + + + + + + + + G1 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/JVM/index.html b/java/JVM/index.html new file mode 100644 index 00000000..9d3366bf --- /dev/null +++ b/java/JVM/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + JVM | 个人博客 + + + + + + + + + diff --git "a/java/JVM/jvm(java\350\231\232\346\213\237\346\234\272).html" "b/java/JVM/jvm(java\350\231\232\346\213\237\346\234\272).html" new file mode 100644 index 00000000..b86c0cf7 --- /dev/null +++ "b/java/JVM/jvm(java\350\231\232\346\213\237\346\234\272).html" @@ -0,0 +1,53 @@ + + + + + + + + + + JVM(java虚拟机) | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/JVM/zgc.html b/java/JVM/zgc.html new file mode 100644 index 00000000..e26f0ac4 --- /dev/null +++ b/java/JVM/zgc.html @@ -0,0 +1,50 @@ + + + + + + + + + + ZGC | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html" "b/java/JVM/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html" new file mode 100644 index 00000000..dfdf5985 --- /dev/null +++ "b/java/JVM/\344\270\200\346\254\241jvm\350\260\203\344\274\230\350\277\207\347\250\213.html" @@ -0,0 +1,50 @@ + + + + + + + + + + 一次jvm调优过程 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html" "b/java/JVM/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html" new file mode 100644 index 00000000..637299a0 --- /dev/null +++ "b/java/JVM/\345\270\270\347\224\250GC\345\233\236\346\224\266\345\231\250.html" @@ -0,0 +1,51 @@ + + + + + + + + + + 主流GC收集器采用的算法 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html" "b/java/JVM/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html" new file mode 100644 index 00000000..863bd907 --- /dev/null +++ "b/java/JVM/\345\274\225\347\224\250\350\256\241\346\225\260\345\222\214\346\240\271\345\217\257\350\276\276\347\256\227\346\263\225.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 引用计数和根可达算法 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/JVM/\350\260\203\344\274\230\346\200\235\350\267\257.html" "b/java/JVM/\350\260\203\344\274\230\346\200\235\350\267\257.html" new file mode 100644 index 00000000..78cf858c --- /dev/null +++ "b/java/JVM/\350\260\203\344\274\230\346\200\235\350\267\257.html" @@ -0,0 +1,47 @@ + + + + + + + + + + JVM调优思路 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/SpringBoot/Spring Boot Prometheus\344\275\277\347\224\250.html" "b/java/SpringBoot/Spring Boot Prometheus\344\275\277\347\224\250.html" new file mode 100644 index 00000000..2c9146ef --- /dev/null +++ "b/java/SpringBoot/Spring Boot Prometheus\344\275\277\347\224\250.html" @@ -0,0 +1,95 @@ + + + + + + + + + + Spring Boot Prometheus使用 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/SpringBoot/aop.html b/java/SpringBoot/aop.html new file mode 100644 index 00000000..6d81057f --- /dev/null +++ b/java/SpringBoot/aop.html @@ -0,0 +1,97 @@ + + + + + + + + + + AOP | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/SpringBoot/index.html b/java/SpringBoot/index.html new file mode 100644 index 00000000..a68d12d0 --- /dev/null +++ b/java/SpringBoot/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + SpringBoot | 个人博客 + + + + + + + + + diff --git a/java/SpringBoot/webflux.html b/java/SpringBoot/webflux.html new file mode 100644 index 00000000..e53d41e4 --- /dev/null +++ b/java/SpringBoot/webflux.html @@ -0,0 +1,47 @@ + + + + + + + + + + Webflux | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/index.html b/java/index.html new file mode 100644 index 00000000..dff9ef0c --- /dev/null +++ b/java/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Java | 个人博客 + + + + + + + + + diff --git a/java/io/index.html b/java/io/index.html new file mode 100644 index 00000000..8cdf75bc --- /dev/null +++ b/java/io/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + I/O | 个人博客 + + + + + + + + + diff --git a/java/io/nio.html b/java/io/nio.html new file mode 100644 index 00000000..f12d3491 --- /dev/null +++ b/java/io/nio.html @@ -0,0 +1,47 @@ + + + + + + + + + + JAVA NIO | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/java/serverlog.html b/java/serverlog.html new file mode 100644 index 00000000..fc72205f --- /dev/null +++ b/java/serverlog.html @@ -0,0 +1,48 @@ + + + + + + + + + + 被挖矿攻击 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html" "b/java/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html" new file mode 100644 index 00000000..85526ee4 --- /dev/null +++ "b/java/\345\234\250 Spring 6 \344\270\255\344\275\277\347\224\250\350\231\232\346\213\237\347\272\277\347\250\213.html" @@ -0,0 +1,100 @@ + + + + + + + + + + 在 Spring 6 中使用虚拟线程 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html" "b/java/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html" new file mode 100644 index 00000000..11123b62 --- /dev/null +++ "b/java/\345\237\272\344\272\216kubernetes\347\232\204\345\210\206\345\270\203\345\274\217\351\231\220\346\265\201.html" @@ -0,0 +1,95 @@ + + + + + + + + + + 基于kubernetes的分布式限流 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html" "b/java/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html" new file mode 100644 index 00000000..37aa92f7 --- /dev/null +++ "b/java/\346\265\201\347\250\213\347\274\226\346\216\222LiteFlow.html" @@ -0,0 +1,170 @@ + + + + + + + + + + 流程编排LiteFlow | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\347\272\277\347\250\213/index.html" "b/java/\347\272\277\347\250\213/index.html" new file mode 100644 index 00000000..4f6b3741 --- /dev/null +++ "b/java/\347\272\277\347\250\213/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 线程 | 个人博客 + + + + + + + + + diff --git "a/java/\347\272\277\347\250\213/synchronized.html" "b/java/\347\272\277\347\250\213/synchronized.html" new file mode 100644 index 00000000..11295168 --- /dev/null +++ "b/java/\347\272\277\347\250\213/synchronized.html" @@ -0,0 +1,105 @@ + + + + + + + + + + synchronized | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\347\272\277\347\250\213/volatile.html" "b/java/\347\272\277\347\250\213/volatile.html" new file mode 100644 index 00000000..b8008c11 --- /dev/null +++ "b/java/\347\272\277\347\250\213/volatile.html" @@ -0,0 +1,47 @@ + + + + + + + + + + volatile | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\347\275\221\347\273\234/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html" "b/java/\347\275\221\347\273\234/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html" new file mode 100644 index 00000000..f9e5bc9d --- /dev/null +++ "b/java/\347\275\221\347\273\234/IP\343\200\201HTTP\343\200\201HTTPS\343\200\201HTTP2.0.html" @@ -0,0 +1,47 @@ + + + + + + + + + + TCP/IP、HTTP、HTTPS、HTTP2.0 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\347\275\221\347\273\234/index.html" "b/java/\347\275\221\347\273\234/index.html" new file mode 100644 index 00000000..9cf56380 --- /dev/null +++ "b/java/\347\275\221\347\273\234/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 网络 | 个人博客 + + + + + + + + + diff --git "a/java/\351\253\230\345\217\257\347\224\250.html" "b/java/\351\253\230\345\217\257\347\224\250.html" new file mode 100644 index 00000000..7e87a88e --- /dev/null +++ "b/java/\351\253\230\345\217\257\347\224\250.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 高可用 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\351\253\230\345\271\266\345\217\221.html" "b/java/\351\253\230\345\271\266\345\217\221.html" new file mode 100644 index 00000000..c5871d7f --- /dev/null +++ "b/java/\351\253\230\345\271\266\345\217\221.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 高并发 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\351\253\230\346\200\247\350\203\275.html" "b/java/\351\253\230\346\200\247\350\203\275.html" new file mode 100644 index 00000000..2568e22c --- /dev/null +++ "b/java/\351\253\230\346\200\247\350\203\275.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 高性能 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/java/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html" "b/java/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html" new file mode 100644 index 00000000..957aff46 --- /dev/null +++ "b/java/\351\253\230\346\200\247\350\203\275\351\253\230\345\271\266\345\217\221\351\253\230\345\217\257\347\224\250\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 高性能高并发高可用的一些思考 | 个人博客 + + + + + + + + + diff --git "a/kubernetes/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html" "b/kubernetes/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html" new file mode 100644 index 00000000..f4cb86e7 --- /dev/null +++ "b/kubernetes/Jenkins\347\232\204\344\270\200\344\272\233\347\254\224\350\256\260.html" @@ -0,0 +1,62 @@ + + + + + + + + + + Jenkins的一些笔记 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/kubernetes/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html" "b/kubernetes/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html" new file mode 100644 index 00000000..5cc5f975 --- /dev/null +++ "b/kubernetes/Kubernetes\345\256\271\345\231\250\346\227\245\345\277\227\346\224\266\351\233\206.html" @@ -0,0 +1,207 @@ + + + + + + + + + + Kubernetes容器日志收集 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/kubernetes/devops/devops-ping-tai.html b/kubernetes/devops/devops-ping-tai.html new file mode 100644 index 00000000..4782fa11 --- /dev/null +++ b/kubernetes/devops/devops-ping-tai.html @@ -0,0 +1,283 @@ + + + + + + + + + + DevOps平台.md | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/kubernetes/devops/index.html b/kubernetes/devops/index.html new file mode 100644 index 00000000..e2b600ac --- /dev/null +++ b/kubernetes/devops/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + DevOps | 个人博客 + + + + + + + + + diff --git a/kubernetes/index.html b/kubernetes/index.html new file mode 100644 index 00000000..6867d0d1 --- /dev/null +++ b/kubernetes/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 目录 | 个人博客 + + + + + + + + + diff --git a/kubernetes/request_limit.html b/kubernetes/request_limit.html new file mode 100644 index 00000000..05e0ef84 --- /dev/null +++ b/kubernetes/request_limit.html @@ -0,0 +1,53 @@ + + + + + + + + + + Kubernetes之request 和 limit详解 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/kubernetes/spark on k8s operator.html b/kubernetes/spark on k8s operator.html new file mode 100644 index 00000000..ac99d960 --- /dev/null +++ b/kubernetes/spark on k8s operator.html @@ -0,0 +1,113 @@ + + + + + + + + + + spark on k8s operator | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/life/2017.html b/life/2017.html new file mode 100644 index 00000000..42a437a4 --- /dev/null +++ b/life/2017.html @@ -0,0 +1,47 @@ + + + + + + + + + + 2017 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/life/2018.html b/life/2018.html new file mode 100644 index 00000000..782f6c67 --- /dev/null +++ b/life/2018.html @@ -0,0 +1,47 @@ + + + + + + + + + + 2018 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/life/2019.html b/life/2019.html new file mode 100644 index 00000000..ec08139f --- /dev/null +++ b/life/2019.html @@ -0,0 +1,47 @@ + + + + + + + + + + 2019 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/life/index.html b/life/index.html new file mode 100644 index 00000000..f7cac053 --- /dev/null +++ b/life/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Life | 个人博客 + + + + + + + + + diff --git a/link/index.html b/link/index.html new file mode 100644 index 00000000..fd3797a7 --- /dev/null +++ b/link/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Link | 个人博客 + + + + + + + + + diff --git a/link/main.html b/link/main.html new file mode 100644 index 00000000..2835af14 --- /dev/null +++ b/link/main.html @@ -0,0 +1,47 @@ + + + + + + + + + + 友链地址 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..193188751c798ad909f0b24a34a30c80f0eeaac6 GIT binary patch literal 21722 zcmV)5K*_&}P)Wo%EIF~%0yAizX} zZGvSm$)Jp$^rRQ=dpDi9^USxxKKsd@Kl)Oat9S1?`|SN&zm>lAt@R6TzVqMIsA|+z z1=K(o1PH?rRaK#^N+1dW{yPj%@df-UN(41f*EJ9X2<%(`2n5i-@%?qB@9A`UsOtcA zUD~VodtR|Z7zU^VzP>+%@GqJH&lVjs6w>YnlLH+s1CvZ=>z*>>W-xL zsK!DTP;Y^@G*(OCYsrmKR#P2g%0f-1;6=l3y6wHS2!$6j zf%yl!Ah`g(2)rS&(anSLe$)=TAk^oWK+Trtc_lYRfyw8I-B)uYk4Aqd-bdfZ`$?h% zaTxn^G&@oR7RAjyaB=wQ;(NnDOrltC^KD{|%2Ga`+NJ{6jVW3sJ6(k?g7^2Dn~+Um z0;gscd2YVb{ZS?Bqb4{MUnK(LKIG@{zIcyCg-`_c&%b2NTARidhgzJOn-C3qisJ#xWmLi9G(G|M83z#5Mteu-~ZU#c5YO{spS1X&hzET6sgq@sbP z1P=TuX3K;Z^I2Lil}$~JP-*L?6zQv3i?%jqFcb~TK}%#N z@#Z`Jjfuy0wrhr|xkh2;)8uz{1AcMajk06au!#ARU0u^Zgu1gud-bL~dZZvIFtO|I zhkR>@uw#u7{hBS)$HiMlqvn^mP_*k}g_e;e-yhhD$}V+Z-Iy`mB;r#%%C*hFjYomW z=A+q?`>znws_>ai7-~T{i$1MnZB8A@nC7 zh(e^F5n^5#seU%8#zNlq+ zA@?Kc*DG#$kL*u(n(o{@dg5hdkIIWI8ID7ZfXpQLwRIQj2Yck%>;wwk7C|#|jn2f( z@#k}0`Bh-fqC++d#QQm zemp4~n{6N_!7_HqKs7h=sE?XxI_0vJ#9O}WS_BmZnir|! z==E~?LfcuI@2D-%aGfwwp#p4#N}oXiw2&InV#`Y;HwpE#~FHNFElI}b$ zES0E_Z=`dqYQ%zsXvCH>EN`;!CL=S|cKpu6dgfk{E{v-+ca6x(HH=Guu6vj34&k^z6__p`d z@*)!yFUp6cQIzpgm02qA66}aML3!`^13yz3m}Oj z@s-pW?!2<%?L_DV9Yl46unLi50z{0U?6m^Eswou#5lIl=z_8s>6+TPLq)5u3psko9 z@x#kxRw7veV2NNbx%M=qmgkEI!u8C%n?;Gn69z|`EP2qw>lRgstS0E7f(~AJ%U!ji50xxl$X#gAiX!;7 zC>HiKJBL2TLer$vkq6}t6%@nqFbi=-U^I8c%xGTg3W`%yn&`$1AT;u!*^P-IRzMao zd=QNL0qAwSI_cXE?^T41GtiY6is zYI760BX6Tai~_k1yhpAD?@v%3UrVkBHO$N&Kc800b79|S62Q=Mu>@rsKlqW`?vc-0 z3aoXryg>O#WWacMmf})r;E<@xk^3+&;X^vee{(d#^%a}>`gH;|pEV3k+^)179 znjlyFlH>==9IPcf)xu{W|FZR4^~Prkj2gB?dn^@_N(tdE)nExiRKqOfq=wPSGF-L? zmQnadFn1Z)jax7{ilCdp=`rcLg_HV`2n$LU0iRI}a1HN`gBYb3c0Zkn9Wk$*G2P3rW@x7{T^OkhNbBnpgJ93#J6 zl63l~B1%O&EDWqx`EeNAa z3RJ4p%V0%nVknVS`4VHa#TCm#G{TV6CAe+jyi~gtcvf?(K?&DWC;^6e$Y+|b6-#9W zQO7Eq1iA&hYZ9&g*{zp+#w*_XYc(sW;%B(S1+es@As0){p=L>@Vaz8S^V67Xsr{{r zs+U6a0)$~0Y{ZD_7*)Y=o^XAPg3p;mvT$XVA!IEq09NWbI^`T;RUoPgB#K82w2dJkmd!Fq|@x7c67GVw`m1r%$ zQANcPm(Y|%QV9bS)fmqm{6E}Gc(~@M1Oo10mL(FVt|&`2VRaNq6ePD$7YMQxovOkh z3@|8mu^Q$W4Z0Y06Aa=Iy(B^sg~-bajjt!&?LJ`2YrU5|U~;W0t2HPD^f~Fq8aEqBkk2>UQ;nQm#op69txtxe|1ryO^3) zWf)Wo4Dtn5vKbC0F)rV?fr~Z<*yu$Vb|Q4Cefb<=k;&&oNrFWfqwMrC54+gOD?I$< zQ~1_Sfv1WXRS+TS4lpl(MFn&QLri81B;5{TrXe)1b*bee`oL0=4dIeVp^|xKjzowz zx)I&<%LX@MkNcrEn&J3kQweXCoZNgi0J1}0PQ9~WPl~06XDYs{K2l7g3rq`>?qW@7 z7OuAiHq92Y!!_D60N09?L`a?~FfMRWvrfe*Ew+eA04emM3Slurf4+-jodC}o_HgO2 zkBf!_bWqB7KLUDnfuzbLha)P_aA%GPdD2JGUqRU$;^fvYKKJw<9+=k12oZJ%$b%S* zia}3|f)OVzg9L*b*zj6WKse3eB5JS(VOf^xfBTmca|~y7#G?&Ec{JCU##Yqm(3AvW z63iG6MG|NYSe5~dT0JqPW=2JDG>MV`3z<_vU@vAW9OF2mLQ@o|WZuqikc^%7K9UGb zZseb77*T31=qbR-$S3D=qaS2QvRxdHD?InW2+v;W<3L?vgXWLslbnh&M;C>B#}as` zIb8%X%I*-8FhLr3F{?xT-5B_XofK!bc9Bz?`y-T5f;s&M0~0msN~5#vIj92ht-P?{ zfJ{#WlwI-^o5a<~I|7V*F6RYIGlvpT*fp9mj#rP(rwQ}QB1?e*Y4#ep%z<+Q9jF9e z4Y}!-yCkd^pwaB*vTUVhK-sbC0Qs6M)JRf|pzaE<;7SC?2+1_lYga*7W1i9^4V4iU z0Ns3oQBGl{xc8jD4Wq_-j^n{H5JCELYCNsbpE z8RGImfYoe*^_cLL*08$y97$LS%a(x-wN#6q)QDIXfe~KN>mWmoX_jFx7~+Y&0^i>p zu-B+O$}=E#x+@6Vry-OD&!Jw zQ(^ZS+sS$20h87UwUj{AG)l?@pdSS&(j0NpLr|6yM2ym1T(_@-mmD48Ksmv1v4{0e z2R$k4bC-<~B^7Fh@l_>EBt8Ja;e^l#nZ6bMRfR=aAS){@5MmzouvK?)Di3kGh_RFB znC7YEg7Y**TGm*^BP^m3=D|=<7$)x?R5)*vKbeF(FJ*~z7`M<9Y|BL*ESE^pI58gb zix7niV{9&N^fqlTNhGBbGi-vTn-Ys<0EKATP1LZa`N0@E7A1gwJLh#0dXBshjzv-x=oSm?%lGh-7@-^YBu&raJrv;_ z1=E4JgHEp}2uU$xt`>tCU7qHcOlKm{q}M~I7ZU))B`3W4N>Qb@EQMEeAUX25%q0;c5x;w()NZU+Au^@{oA_x&9G6*%kn zA)Cyxb9xJL5ulpqC{q>-L&Ql({6i9@2*L`JD#2b6q3o<6==G5@NQfeAXC-#37*A&% zJhHXGiMi`4#S?9iS2fD%U%Klg>S{`*O;p-& z%pE*UYZf=P>OVq(VK%|xau+{(-6c4box^%Q!+|6~kh3;4L>+8RorJl>0Bi zlXKuZC(q%jw8X44SORQLFfvQ?%+43rLJiBgcFKU_?`c9kF!3l`;A^h0YY!7{gZ3y4 zaU&LCihs0@A*F+`$3SVHfAb6+y!hogZn+t>(W32 z>P{G_z|uV&!vwE;{^dBFp2NywjCGpYk``@^;e{kSV-d=^vHloTjyezT-bLmXi;onyL4Q3Mg@ojzv0Rh$opc;M_9CwAx9jry3x z>;ZFntXGh=@aP>nN=MU;s?=`dAbP+i)e2*28Diq;fF|F3JO$GwP%|>Db35oWg+{^G zdip>?2_|Y}pBrzzyVkr&%ihoIqF5$}O4U-9kJ6VV{B(Ad*<5enlOkRV6Ru(=qA3QQ z4u-`7gLI6`yE*>V)yHsfaTcR|hBa27OD=jBac_jWo+0T)7_6@&PZFG;&2fHfjM*#| zU)G5_VlJdBMf^jEj0ta5Vm@0S4kGNFKaYnVdI;m`9M65>OAxK^$1Y;*#zQ=KW(TLH z8M46!CP8OeK(q!9EqNI1)_N9)m$&8e!LW zebb9)8dftJwl8hyGpvih6WFHRb7gOG`kN$#X1kpSA^Dq;M}6lhWaIe@Yg>#GQ(?Y# z^96?a43~Ct+0*YBqZi?UM;^na7hi(y^ILfM!H00}{1!??_~Dygg=lRbw(=T# zK?j?2vPL3Q{WVF#-Q4?b=1-wiTj0@H4U38;IB7Ahob%zt9r?R~4I*U2E!BV7MlgRS z@B%z_@+mz2_~ZD{pM4Z3PHp1i ztFA@3vVln*qVBICk2;u_wICuzpb2dQf0(cKK&$}|9N%mB{#FaK?h?l$&F!d0nK=}5 z8YvIa7)92Wv&{D-kTXpb?WO7Ti}ChuyyLw>5fJK@(VKneNa83(G-K@=L)D!spHO>2 z6ys_B%aZ9PDP-5Cm^+%%1W{g~lV_5LU4LYRYu7>%*q~fs&|z&ZMp;KlI}y%I$N0yG z9>!O`^KCqSevC5KI@;lbNAR;h{d&Cd=ih+obd3E6_DKSqr3ES`)`ZT8g5(wc?i=61 zzx^NIkE2&U2g8HMkVQRA3-%jFh&t?5hU9SnlEB<=DQLUof@q+<BH!o45`*4n&xH8@Lg}w$(7n1r}yFL zv%n~ZWPASW( zZzt2zqLo$&F)UJn2yFzE&$GmtT`$e0UUA#qQo!`GKBF9JijYLD>CQ_6%Dq#yZg;Cl zfz7Db%RXsilUnkeb_?Xfo#;p;*bgFfrrS8yDbXLF!l5`rH{C;>&++Cry%|?tdmYdp z;-T+9fPeq~|A>d5K7$!QzT3qecm4|A_=aD=AZC;pq0HtgFjlO$Ht~h~K90TF99KT? z23+yH7oc7{iZ6WQ0sPmGe;!+l097*5ssUGlrEe{%|BFF~yrdO|H>ii1Oj%g-amt%y znLw)^f{e5SFAKzcy%X+BuTBCk-KpMk+n%Ti%z zWFshcD*|*Dk_R0Q-!yjGZa5<$G+{}mBhXZs=NRNO42vQT*or z{C?~^Z~*yKOZ@kJ{xkT45B)JV$J@B!#V^3G-*XR+9X=xAE;$?=0U-_J^u>!5)9r1X zIr9|0`Q7i}c`teqE`R<@@i*Uk6uv{_H!pa7&7*3dV%h-VI;=`z6!pv3Dh(P?# zX>gX0L^9kBI*uWi1VkmFKWuD))zTDA_iRshU?~Vc2{5q=tjN$S=NJ?doPOwA*na9! zbdlncY_1i$D2fpbk;x1f&T@ z$0(`*p99GK^0s`XLIC6 zXlRb)x5!0&Z0YK;q*nqouWz;b+}yV{$d7R-8;xnD0-lij`ssVEDaAERn$kUlG+v_4 ztFYRYgO9Ygc0*d1{r~@x_cxUSYr12tQ_PE&NTRAlw_2cEOtAI%_c1+p5^*ub;f*!? z)^ENaS6p#9s!53_9zB7>#}DIM-~SelTznW;Tz&}x5>X}bWRglwVFE-QQjG#>tFDETz2K<*v>on!aqESKe_KKqG5A%QPqUBGDKLYu%sTg+;|>Nw424-8&TFm z?O_A&cxm4|cU*`P^%?l$X2)VzbDv=W7?pbMk882i~=J{ECGf$jkjSIPYj!8 zJmn>dZEi_zi2@iQ(-CaTJ@{NC&aj1u0%e&asTSzu6YM?xD9)dF81-xy2UZ8T{gzws z3vc*243S{_>?RHzK7d6&L$8-0Tg*_V3)ESTkiuf(+#4bp4wdp&R=EEk?#BoJpFhM; z{`61br8nJ#c{s$Me&TQOnXf&7Suhl$4ta=SSRxKGBUdWPaFEWPfaj5&zmEZXroG?r7lO0bmBpyk(?qR+&j+^tR^ElLqn6c zJj+Nn&i1`PVje9y%g8m%z+~EcVtpx_B82~4iT~)=3ye=cj?)kS6QacgYrO=|z2@2Y z_y6|a;fkY2P|m05_PeO-9O--}sQ>v>r!gdDO#+$#o#6_)tE)iLk&X22?|vJPJoXS? z^75Bq|IxEWV2l6&AOJ~3K~!TnGpX>qe{?S%czTLOFc5)}Z5#w-ALqzPlcTx!_FSi0 zvyz@LII(iQgz8oewhipyc*A}7T-5Sf_e)IztqNviCR7+AwB|FG6NDsHGFn0-Fz<2p zgsmZ>5bn3S4qq&&v%x4%gKO|D^J9&ze@5k2f)&0wjF1yL68|y8Z8LZQPrI~hBQp1KrB&(}ZrxSuq z6kt5v69G<&8asJ{|MhqO2%EDAsR*nqo2DW$sLmvBW0>geLQWlU;*C$UY1hM?jH;TT z3*Q!mf(~tdzoyWdf-M2_h7slcWfs6VpzbBCMqoGH{$9CVr&wosre=opGwTeLwRWze z(V*MHYUpnAVP3-5+w&d5K!Y`(B8sFsoYw_9VS%`qVS4rn>^$)pqS*v1L4Y5<`BixD zyWfMA0gz`?tgVeuWDD_KduKNhmz8kz1!0LIbVnnEqZO%2vvA5}`26;HtQ|gzvy%l* zY)S88=#`x(UAUk&&y*$Nb2M^=!cl~#G(etkd!a3p&=Uz|{ z1?oIQIh!M&%n))=OAw&b>7u*7CIU!N30BG7&iWS_Rl7xiFMRFW_~<9UD3pK<2?`{r zWL6Z(cUl?uUbtnIK}oVp|6%p$mbugdq&AexNG9jKu+L0LH8u~+XOlENWlPg4!G;q~ zZ5drtJva77b=epU>wQ-!%CRglc)yfq=n`TQnSMrys2}>w^xjfOoWI`;R`2tm+7;EMdH~uWBQP4u-G+Uf$Yk zRc57_EmIfuNg8S|SyNiPRa2*Ekt}*M?Z)SKSvktUU$;_T3)6l#?c$XF25Y6gV8XyF zZn;YYCS!!Gyzd<@Vn=BTjRNO8$b%8BA6bpar>iw%40K0&)D=|PNlDyJIP75nV)C7I zf+4HTVTG4ma}{3noa?aK?c<`O$I)43xnEmGGrm6#kuZ`a=E(&U%7OArp58oz@qC5@ z8~d@cx`8P|jO!TRe(WiH=%b&+x!n|Hod{|V5=U7BB+rEX7&M9O&yX34I;lz;1!d$C z!+(w7nxCz1OfT?C4rOyhq&1gqNix^zh?h%FG*z)BIA!7T##(tj8tufkQhiZ{{jjdEwL_Ffl*&%ioRKaSDK=ZL2} z=%r)4{5e*MHIgvG#-R;hkf4re;>gyr&*1~GRhN_7Xnx`dS&<8kE{eOT zqXdh%kDV&Qhwl3vzWnv?V48C0lX^hY6-k>egOq~LqthZTY?Cr`FTUGC@HR=*pG^n7 zEG>)!23{?0f(6g0-heLL0-rYKd4cf@ zWDcP@F@m=wm1+*+sHomrLl>~$y9puvl$`iD>PTt7m`*XO3momoc-}>a@RDn;z`;R+ z0R>O)M-XAKGQePM1gx!agba#E38~zX-6TOLPEgDj>Sscoj+2oTI$XnU8RDVSTlg=3 z`WJZO^m!oaAt3DGu(?p@*y<#4xCl%V=LW6OphLO!TB`TxuBmMq2<#iSi6SkJRX3rD zbvquAt`w|e@u+COcLn&x(IV-h~URSrzgE_jnd@=w)z89f zU-?QL>UWWEoyDL?5lr^5zndVcas&mFVdh2T>TBP%3>E1l!qtdZ2B?yb6l%C!cEm z)Eetxv!R;ZZYI+Qp*Zohtqro4R&Mg3%L51sjQ-YJn7q5QF@G&eg`-Fwc{bSQOK;P$ z4{B|Fe1bJja0j)T(6ss-U^bhf)9pyF755E8bO9J>?+Z3L)ab>r5N-zDE{-2Lf)~E{ zML2TkAg(xm6kO&wn0PF4*93QI?`Ru)u9 zslx@<8tAO_iPlsC8{F0I_}8s(Q*fUlFvSYB4Ov``%qLk+3-Taf>FXOswY*l)_vM~> zxoDUHk4EfwJ8y3_OwxoZqjYo%c99das4(btg>M*?1%|yYHbyJB=p2}&;NV1UO?pTft# z@CBTmOi}fF=&i0J9*mF{6}ETxusfbe<3zVV6f|R+k=H#y6m^kj+(_&I<$CF~P#HWS zj9X2B9ieawqca{Y*pRSW1A!*E@_e+#V{Bkruf{t=+6ShQ?&nV5B&{aYXJ|Rrs5R=w z+L_3Lo9^TYS@SctQ#8-gA(cK*MA~aCG0fR26bCwfsL18};QA^qyXZJ>c+RzW_7zv+ z;MyuydOc-H$tVaNgv=eDXA9(u8F^8{Q>^k-2s(`Tss&3PY@Ub^)d413TNtdbVK>k5 zxvzc|pZL<>AlcZ5-Lk}9ULmESPS{1J!?#HCMp2=PxX(2`y zg?sy#{FOn6?3mU}Kyju%)XE6|9=!0z3uNeZ6O?%>nsM3jqxgx}yc$1r{qv>0bx19t z@8XRxDYRL>6xxEApfm*pm`wJN=NUHER?$zoBDC4gSp43w+h->D@H7TD2u zuxU_f@ftZSh1Xtx6PeZgJQfXJ_4Z${HC$g#4$&uw#jK@<<`L}{SV!W{q4jw*=**p^{YB*fQ-gt^{fBQT5 z)EEC2k3M|{L2rn84NN%Ilu>K1BNRHBB*$G{5@3}9A*e!*yHJ5i+pqcRrW#B?O9VnX zRMo#zP%_iQ(;*KEtQ|?Lk_k$4R%>Ijpu$lx{!MI-=g2r-BI%O;C%ZUem0R72_=TDbU?w38F-KF7_W3rq*PT-Z6J(V%jxd?au1 z%3JOhU6a7JnL%rLg=O+OKhaL`VQ+Gs!@fZWVLC^Ur`X*(hcl;6 zfrZd(uX_&S!9au%5PFdj3OcZv>|VlN-Z7IGnFKmXhx@;hT(2w_mV+>AjL}KDN@uHp z>12w2zb_n)$A54FlgSiY+uL~J>^Xe*kssiZlV?#T1B}ZMLEMo^I3al)Qch9yn@Eke zxZCLvo+w*7DyLW&w%Syb98@q$%aU!e#2aO;Ot|XblrKVa$fiQw_&)5?BFJ`R(aOZ7 zoYl;4;cq$>D1yBD2Y&jIb)2 zDpchmqrelP4fd?P>p1l9ctFJ=R zXE|OugXFR6BM>X(xmdPEg*QTBQm;5rLTC2!EU_453G1TCWu!*s=(nA&V9D%!J`==V z^76dKC;s{?_{-1!9kv!B=48IciH`T9&;pL7Rh}B3&CzWyd3dtyB_l#n?fJX{-r5L2 z2|9eHysa^lbZUiQsn%V&q`9?@>GEQzw0U_WpeNQYIy-E@x?5ykq9any+*zK+U@b#- zAe-!py24r#;5~QVju$`oI&_K@{Uih?yGZwT@aQ8CV|8N#7hQD~YM}#kF&C5A(Lu>l zcqB!Q{arRmo3K>Pc^_O%=ed)~m7q>@t#Xr+&FGSJ1{t4I>vRF>f7YR#Qev4^=ypeV z`1Cpa;k_TnSN`cCq*a8Fd0tLaMM7*Tk)!yokONj?gv`qf8&!cBxNGRUp8?};L%?Pm zKdU?tGmbW2SQ;RmM{j=<;F$Pj3+{6;`GWT2roF_|Z3i$|54-IyLqC{97Lxr7}eg-5wTF(%_6K z9d#sq3q=3(4EbU%;IJgvvfC?E*#e9C40DF)lPTtl85UWJ1(Ex17wh{su>a5j^$nd^ z#u3RJk8TIZ=qGy^hXFqI)%)>BAG;4b(+p)5$t+==lCJX+YtFdmj0x7sWGaC{tWJ|~ z?n-kKlKFWBl8kL-1za0n?$c1su4>)Oo7zRA^`Iaz^P=WUQjSKG%TkpTLun2L|I`f; zyy}kk8q#4lP{K%?hbx)14lX8CDwC;$3eUUjDDHXZoj9_#if}f@bn`S8+vjoa)YF(w z_Hg*3tt_!v%rO~Hgc;T4 zaa66ef~f)*;M}=$*x0{;qeqY8(BVT8fY7f6{S|4AjrQ-y6T1`q&IkV#U;F0wP}B*M zxUVPv*;rPVSU7r52Kn3RZO^O}v%AyzDxQl8G9;mqE^Z?bDY@HGj#l;XphRhe+9}gU zmGlx8fV2)9Qlv=^1QBcC_Yl0Ad6-X|_G+wTfle>hGb$^xmV`@KkJGO@C@f#a87iHk1191&qACd6zi&lcD|e;#`~JIJ$C3XhA~ zRK7_RpG-M5OJEX9$l zuR;IlMfl9$e*+)>_@}WmWtR#koYH*iu~n>xmE4ddkv456u4d{(gh*?5Zj{v%FW?uNuk-Xtb1eNFIYr8X9hHX;tdUvBM#z$g^GAN=eJU`q2UgOS zIk|oa8l_K#pfBMtuK;iQS2yCt*FFoUPCg;Sy;cVu9NE7vzJWJ<=FC|NZHRsALm5uV(mqWV z+gTrfS(ako#g`x)ZJ_Lr@TVX9E8P2u&j@-iP*kX+ zkhH{7$^=xI%deQ6x%_1**#ih&6Ek_-`GQd{-y@q`W*IYev9m#a5}TP(nW=B^lms7b zUSmt+oN}R$>lJ>EF1nNFhgs4mqaD0?O#7SkKu?nMN%yL;(ur};o8E|*UjH0M>oRaF zB%L~)VYo678UW9(B)qVFZX2hbIwMB{ZyY_0?r?}+uZu+5V)bakEaS1eU3qXtzxDJy z=>SPM?_Q~5()1<{S#uH!nN$TGoCWz@S-h-}T(5`{Y|S&&y%qfZx4(}+_>(`w=I&S+ zRRoH)=}vQF4NX_rfcidAeLB!6+b&ygH`3z8I3#<%!$mTPt)*d~$AOGRq@4+R94@%<6 z=rPL|!oi@u64y!YA!|J{!;(8J#`*D{pz~Y+Pk?>rNzIXkP#HU+Ly}3TVfamFEK^S9 z%mX|5!Rl#ES0ta%3`3n0jp+-T!WtQy+ZQ$PAKw4J;L)c}qT~q{GK^098fCxB(Hc4l zNcj~WDXTQRe6JMh7R=CU1CrOdSrtFEJa5fqN)Wd_Pr0dP8ztORfHqfz5v=%E^RdCr zx4l=4jC_03odg_&a3~Wk+p{N&&EMBtaXEhD9q+`!^)-QdSb0{B(LhD6Bm*PuVuJky zaw5_qQ?gFClZXcS`I-gC!b-ugbuv;_q0jtC>RU2!UV;urp`3`u=t-*B)RtJmQIbik zu!lJ5Bbzg-4UwiL!hV9^{+<5|U;FNNP$kMmTd_v1qe-ElSO=4a1!E02JHi4OHYSvn9IhiX zJNkXmGWsDo%3h+-LcSOCx#VHOM9wNXGm<`zU^2}mk0jo=Zv&IrM0{Si*A-LCaq7~G zO*731Fe{OCN0{tPkwyvr{F8r$KmY7!Wk5^S?O>jghQ~x&H=ItxHL{Hz44^e#TX zzLjo_BAug4)-Gzi?lrH*n_mADSm`9lvrNX4i@;>TZ0LSf>Fg&41xZKRjN~jYP9`YG z0FEU3BSR`(%#mdaY@R)X*>oa8BreDtZP*(~^;t|8J5}ODa=5`@fH?(*0K@(O2M_MY zk;8}4UnjMjm=kFUG|pX|Ie8W*&hOw`kDb8BzWf#JQVPQnW{X@3gWb4`svt8&%PVUB zv=pLI@&$N&qfcu575{5Yo@to-pLR33{7xQZ$?wz;Tz;mPZbYDd&^XIkv0{=^d%O89 zHVMaO`Ra%`Jx{A-S-;=KV!SH@PdN7V7k=ia@H0Ppv&@iZWX6(v!P=E`KMd@Zvze<> zG%HOr}d6mITEjFE|ozM4Cml0M_hx4-3=aMKH(i!h&K z>&zLXlPR*vL;?h!>Oj^nQF@L_o#h!8ssNWOmCuphmC zPdFcOho}6g^JmmbGb$r)rL7m3PbOF$4l&=`LpGUW_v|@rojQ%Z^INF097Si2DlRY@ zuA{7C>_2!B_FsM#R*qka-~Pb=jW2%dyGWBB7I}aq;mAE6E|&pi)-=*J!BFElmS_h3 zlbCV7kENnkST7IO%DbBU&6@O;Cu0Kh*;9V(u?yLH%3QMr!}XH@xsETt<9#)K6DlI; zc2YT=JIw+eAkXQl3iQGXhu24V`!BxT5NsFMhT zwKW_*c1$L(9XfbW@~fTQUF`1d2`%mD@#7ebRzx@~ax&5pgohC#8Mz$oAlbg+ep2{j z-&CHX!+*{eSe)C!-kD8Qixi7wiaDm}^;VEqoLxV{v8%5`F<8Y1?)^)A?DJnj9`_{V z?+sRwEpnrWYhiKOKBSr@8e(Ia{O}r?h;1}A_ebrT7EQjM{AY)iwg!lKJxno3HJUz* zS%MwQE1QIz*k`L~8od1WUzh2+MByng*#$xtB^51^18S;tj@3?tV+YpomN)z~4h}jv zOyUgv8r7S=ERMxQrdf(vnhK4ZO>>=oAEWhk^ob=#3HEk(aqj#%LER4=K8)UQC|aZf z<3@4BoL!r|S)0~rXN)PNPNPr>p+-)`NmJCbxo{s47bpV;K^-|RWS&*1`a{%%5&rNa zAH~N%_tz+*4rYr2z5Wmjj-wU4&;z>`(X-*t`|z&i0`tFw|Is?(U7PUK@A0QkC2(|} zmO@UFveTnfi_Bly?8K(Mw#5f=CYHFoSKRTwn$e&OOxRPTNpsM$9N*3mFU2qhj_qH^ zZEyYsTy^{iHbz4W`zxhq6RnU&HJVi}oJMVOfM*;V=R#XaC1K?-=D|=XV}e~s&CRN> zW?EvNg~^JuX_z40<}>uDEFDYB#tZW1$+{v7E0)d|J!@sY$g#6X(cRdGKlt#6@$t`o z4izZ_S&5b5NXGcF)@3s!EkV_02QSZiU?8EG^&eAMObeQb&_=!rF55ccyvZZIobt@- zvXxVoIjMJoIz3#^)@%YB3XCR^rE_YS2u#WkdXz5(#^L@+nc~R4HQe>iJ8|8!E|qZK zPkCk1+wI1JocjQMDl0>9ng*gCG<%DMnoL1c>{MWBvakc&G&Ws+F=2FyfF6QiD1rDl z9y}+^MiKgKz7WW(69J?YTk8+eO?oIeUY18xzbdgi8B0Pv=&qyJ>B~qX7G#N6ju#6gD?@zbQ=i1W_k98>LE^M9 zfu%wp@R^?;r8(Wq$2!PHwll%BlI#a9Xg2M$Hk^jtOv01VhTisS0`tP9PvUO%miy;; zpy5^PM#FA?$G@wkL%+<^|c#z0zX zsf5V4U~B|HIa{8&N?yt?X3Oi^zI6e6C8tU{a_|6~%C-60j+u9mT(C{!Uzn!p0or!! zLJq2A!~>1$_TH{&Teq`6EQ3x&9u!lg zGcSBs#AMyFC|CxlbM}hmSX3E?t0OUy9rk;rnGka5-}Laa0DHT8SY2JkVz!WaSkx;p zPxnMqWt;afy<`et_eX15+bg(PQq-L;wiYQq`Nc2cFFyNu%@+%Mp zJk>5oN>iGIh!U1L402&AKmeG$aOp#+usa=Nof;-+yao?F`WjZ87YhsqLv1h*L+ott z$UtWvyq%Xb%%)THVxs>g^65hUTN(xtV#H*5(3e5YG_jqPE_#Hj7)3Dn;L%rgfXC0C z$NdjHh>w2e3phJnAgkG1wkijHOZnE^fm&blCf09Y>)-5huLjF`niQCjh?MK-fr|4p z9C?=xl*V5>)y-OeTSrf|^E!_rrSHYytl-8wf1{?K0H>STEDZNw%!}lBLwdA)j?0f7 z!aHtxE3P|!F_=(G9jeZy%+92ntR@ParjR8TE?Skbu$kff-Zu6hJRkxQA0`}!P@+2p zOCgcubUYCfsW7y9C4#ubbTScigBr6KtMHhx=krvW{)l!6wL4DG+tv~E}}S0lI_F?F?B5zo@P-G z8q)mGXD4fsb9uFfg+*TzEIQ8U7WYd|L z&Xlb~VU1p=i!x`fHNeV&L+I^4fM9JOKKZ3D<5Pe8RgC8xpQP;>|##wbx-=~SNLs!dO*L23!^cnLP^>Zw&^7$VI97u|5;Of+hHXID((aAgJkUcVX6B`62;DS`3XMJa4}@l*7bo;Wvur|{Ao zONM^e!qknX1A`1DYL zhL&!J*eT&gAt^7edM~)l7r>MKXYuC;+lvjMvtJ6Mlz#~j0rD}lT2lfa+VE$DE z_R?GLk;FNcSsc#Xf7b2*_X%fK| zGU<#ahf@VuwLnnQ9^Q<5r8~gTY{?uW)CdWXKk4&P-kb^noEay@nt0Js~o6a0e z*xkezS+XGwoRhfO<&qaz6a@}#>_e4j67g|jcG&OZ{9=LcK6V0s^yeSN6HlE()ES_x zh|^bc5UX~su&SUf!Fpl_IT~8omPt7R1ZYK>56TVcKA8)xH0(CoZk1yv+E}-XB-ln$ zm04BRTI)HQw>*i!OuIy2H{O1a99Jdzj$A$<&O(=dme1lm%t^8gYrP(BdE+nO=9fPoU5l)x z%G{jJqZMawtgvBodhIH}d`5;-iq*AMan{^bTCik3))nrdpEAMs@H}Q#jXhs!x;Xr- zWWnnq%op<}_nBpxRJQxw0Y<}B!6MoCKzwm)vV*5K&*B3g{1ZI#)LG;*hOZ-Z0`VdY z7AOb{f}|2`V5~M;k+fq^Wr&5EGc_xYFFK6F>u5fu=`FEUj+yWSxuvXPP?qF=!nU$% zfSX4dMBS=5-*nr1YN2RL8Cub4A;C(8Ur9AAUWRFg^?na;{l%Zh%`dwi-HsvI2Elmg zpaC?WQK{v_Wb`wWF?!vej*Z{|%E?4DfI-D@IBd!jLc?a5Y0GEvjI>(SiA)m#> zpX1pS@Po#uYB{Lu(#mhbpu^rBgb%&l?8ue%~D9!O^;HSEoA_&L1#rWc!r>A^WZ#@o$-!VybUQ0=wyD7JTY zWK#?W12KO)J3HdcSw&tQt%}nplqC*W&KnbFVmDTUv)ah%ElS$@uRK9%%f*jvHc z>bi_k=WZ;j0$D&7RfR8p?eFp7k9``aw#Gt#*_#E8rbo3Dkrsfl*d&J=%ex@AzR10*ZQ_&Pu&9b>Li$nH7bLa}Y zW|-86z^qKc7QyfT;BkEL-jCzq6Q?lE0>s_En7U;aW8%}n z?(go$eV_TF2#gbT$~Gcyvh(7iq89dTx}`d{)Jl_~1z|Q#zy#jR>9G0zVsUJ08}C^P zo*X*E?oz3xX<#G%&3X-*&Ghm+?h=7%_lA`(g2Z1#5z2#?qOnRIt zC?>OlY;)I8i+r@4AqFlp+JAy#EO623`>ck!G5 z+kci3M|+%d>cNA9&xSfUAnf!u_g)UpdLSex^!h;qvJQT_c?-!WDc0aS?QPciz&;Su z*}uy+-C9oMz2LB%SKN8GI2a1dO&G&xUYMi88Fa`95}Ka8z^h;O!?@>;UqW2!qETpM zPf@Vron6eQQ|W1D&c(#FlO*DwSmGERJShAS;ov1PPCxaOL`+Pa8P+pW8jr`~*Y?I^ z>|fi!N?+N+O~`ChJNxPE z5`zF|PM^k*)3EDW`jZ(k@?lm+Bhk2S(ibx!To1iqL*T5qNvvtyYfs|)0ybQXv3YtE zdpmn5vr2Nec{<0uSRm;4am97d!TO<#@#(+*GXDH8J|$CiI3nVLlhK=@US_In0**$~ z%c#<4c}TUz<^VU{l$IuW9na6U^oq2g_mbXd<=RcJ*TiV=Sj6qh8*NDDjbajo2d$y3 zj%Aa;gQXMtB$;hNttkuq&~vZDuiySgsXz-zE9W)qEGOyXB_Y)6Kcrie)yv(nw>uHX zPHMpMiU|orpRU2VZ~A0)8+^_ua>hoUodo(ftVZfCEk0HBA?GO z9nZ*$#b`J}6o(Q$)=@3R%l^iO2Ef!yqZ0= zL~at5n&v^Qroqn?n5&t>5}qpCd22><^WiG8FPl-k4EO@{s^xsE;Ko)SCKvYkzA;ZG zSAj{S#(|Y(iR-So6u18LYjJGfnwYBLK#w(KKBP9h1yeT$fkYnd)|j#(0ksHwdv^zG z>+4wG*pRkYX`Yku;aWUoXs2WDmJ3KfC8K|McsHkXv-l{qGj;nsmP)|p-HFuV2E9H8 z{hl5-6%;b-mq%KiAE#Ip5&n<=@<;gk_a8wj2b^jdjVO=vIQUuOr&zzDm0`5%!&96D zsuEtBKmTuUy>)@g0xb_DXh)0U;_YBq4>))aQd?xz!%98DD{s9^;!Lq{y5bUk$|0ld z07U3T~oe&bhemFhE12#b-vI|OsH(7TOJNT@m|q|}k73JPkG^1S5`qt#WJ ziRi-OqRCk>EbCYzDG{NWC|;Lv78jBfZa)XX4hpH zVQaDw3fw0?^#%O#NA8ocvl4O%WxX}_!schlY=2>bC(C)q_N)#gY1F6(;;9L@)ApJW zPX)W|$6MxDGUB3j2BvGdpd4206s`Ud4hNI$-gXEt$T{&5u;7edmZ3YD)HMb~#)}d&4hLH-l#|D6bJ%jXBYsXE)Tejz zc}ecHGP?8rmb5YjXC>eB!eGtP|3WGS`8>z==6R{3ZLF@KtTIFiJ1%RmHIZ|G!_E+2 zz5iSI!w>%@w&tV)5HQfD5M5X`Z`3T7YUt9XfnK_Obb+6l*!n_?yacpu6wjzD7gn~t z(CHDQ5|Wl}yNVpOD$Y0G`fiy{r_F@c+swrzxrSgaU7GPam_V=fdwAckz7yA9dyUYm zV@lJg1rl~ky-4vtJqAUoVSyZkr%WfpQ$+2Dq*t?*RuV^@yP*)2vzHXEH37i!z-d=)^KRWOI9rypHhTBR{~s_k9)*J#iXg zhxr&02~A|hG!q`<4!2qJ%9OGw&aGg(rPV-UO`{B{cbN%d;>S_h++g#U&1+3yI@e3T zJav%G@T%K>&2eCv!X((cC5cjua|E7yicJr7g`fSg*WwL7`8uo)5+I++R5vZ;kx9jX zMRyU)HEdVnPnIzV6EUh}DWBTNvJ7)D4U0K|O9y#zk)(Zy>|O~wdA>r24E++X~FOxr7Dnj9+%t1BzotidVT43eT4=l1r5w?Zn!y&wCuB+~3wNbx@r` zlMWdcKFR(SsIA1_?vC_bGqE0yMoLMbCL@qz?pZhM_6NBCo8QNO{m@5nelixUv1CHd z+@}iA;aQm?FooizCdS4^@5#16LeLZ|Ji2xWRlrGnGP+0!v;R#3;~8c)-^4WRUlv$% zmWXSZ%zjd`X`C+bzPo-EFMQs$Jh%ZNg(?jcG>QL`hAjmqQ6A^-Mj?9iUDExi=2Ixm zHVlitil&puh~ST$Cd!i>vP{e=zZkHvzE7VuT3u6mUS47$9W6{HcI2CyeE9HTtZ@ur z!fB;Ee91{wn7g-iNk zG3%{^wtYuQpkRJXj;m+oPZ7|1T3p=Re;*F{S_j002ovPDHLkV1igk B$FBeY literal 0 HcmV?d00001 diff --git a/logo.svg b/logo.svg new file mode 100644 index 00000000..d0df14c8 --- /dev/null +++ b/logo.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/middleware/canal/index.html b/middleware/canal/index.html new file mode 100644 index 00000000..7838845d --- /dev/null +++ b/middleware/canal/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Canal | 个人博客 + + + + + + + + + diff --git "a/middleware/canal/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html" "b/middleware/canal/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html" new file mode 100644 index 00000000..5a6759e0 --- /dev/null +++ "b/middleware/canal/\344\270\255\351\227\264\344\273\266\342\200\224\342\200\224canal\345\260\217\350\256\260.html" @@ -0,0 +1,50 @@ + + + + + + + + + + canal小记 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/middleware/index.html b/middleware/index.html new file mode 100644 index 00000000..32036205 --- /dev/null +++ b/middleware/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 中间件 | 个人博客 + + + + + + + + + diff --git a/middleware/kafka/index.html b/middleware/kafka/index.html new file mode 100644 index 00000000..e6813c0d --- /dev/null +++ b/middleware/kafka/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Kafka | 个人博客 + + + + + + + + + diff --git a/middleware/kafka/kafka.html b/middleware/kafka/kafka.html new file mode 100644 index 00000000..56994867 --- /dev/null +++ b/middleware/kafka/kafka.html @@ -0,0 +1,47 @@ + + + + + + + + + + kafka面试题 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/middleware/sentinel/index.html b/middleware/sentinel/index.html new file mode 100644 index 00000000..c7231c37 --- /dev/null +++ b/middleware/sentinel/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Sentinel | 个人博客 + + + + + + + + + diff --git a/middleware/sentinel/springcloud_sentinel.html b/middleware/sentinel/springcloud_sentinel.html new file mode 100644 index 00000000..15bd89ff --- /dev/null +++ b/middleware/sentinel/springcloud_sentinel.html @@ -0,0 +1,47 @@ + + + + + + + + + + Spring Cloud Sentinel | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/middleware/sentinel/\345\205\245\351\227\250\344\275\277\347\224\250.html" "b/middleware/sentinel/\345\205\245\351\227\250\344\275\277\347\224\250.html" new file mode 100644 index 00000000..070d5d1f --- /dev/null +++ "b/middleware/sentinel/\345\205\245\351\227\250\344\275\277\347\224\250.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 入门使用 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/middleware/zookeeper/index.html b/middleware/zookeeper/index.html new file mode 100644 index 00000000..9f1befbe --- /dev/null +++ b/middleware/zookeeper/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Zookeeper | 个人博客 + + + + + + + + + diff --git a/middleware/zookeeper/zookeeper.html b/middleware/zookeeper/zookeeper.html new file mode 100644 index 00000000..ae25620a --- /dev/null +++ b/middleware/zookeeper/zookeeper.html @@ -0,0 +1,47 @@ + + + + + + + + + + Zookeeper | 个人博客 + + + + + + + + + diff --git "a/middleware/zookeeper/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html" "b/middleware/zookeeper/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html" new file mode 100644 index 00000000..3cfc10c7 --- /dev/null +++ "b/middleware/zookeeper/\345\237\272\344\272\216ZooKeeper\347\232\204\351\230\237\345\210\227\347\210\254\350\231\253.html" @@ -0,0 +1,225 @@ + + + + + + + + + + 基于ZooKeeper的队列爬虫 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/open-source-project/index.html b/open-source-project/index.html new file mode 100644 index 00000000..ee8c3824 --- /dev/null +++ b/open-source-project/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..e509a58e --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ + +User-agent:* +Disallow: + +Sitemap: http://www.wenzhihuai.com/sitemap.xml diff --git a/rss.xml b/rss.xml new file mode 100644 index 00000000..7a87f36f --- /dev/null +++ b/rss.xml @@ -0,0 +1,7048 @@ + + + + + 个人博客 + http://www.wenzhihuai.com/ + 个人博客 + zh-CN + Mon, 02 Dec 2024 15:36:33 GMT + Mon, 02 Dec 2024 15:36:33 GMT + @vuepress/plugin-feed + https://validator.w3.org/feed/docs/rss2.html + + mac通过网线连接主机(fnOS) + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html + mac通过网线连接主机(fnOS) + mac通过网线连接主机(fnOS) 一、mac端 mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的 点击详细信息,配置IPv4选择使用DHCP 二、主机端 主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。 *-network UNCLAIMED 表明你的... + Fri, 29 Nov 2024 18:45:10 GMT + 一、mac端 +

mac是typec的,用了个转接头+网线直连主机,初始化的时候跟下面一致,默认都是自动的

+
+

点击详细信息,配置IPv4选择使用DHCP

+
+

二、主机端

+

主机端是最麻烦的,刚开始的时候怎么也找不到网卡,ifconfg敲了很多遍,最后发现是fnOS没有装驱动。。。

+
root@server:~# sudo lshw -C network
+  *-network
+       description: Wireless interface
+       product: Wi-Fi 6 AX210/AX211/AX411 160MHz
+       vendor: Intel Corporation
+       physical id: 0
+       bus info: pci@0000:01:00.0
+       logical name: wlp1s0
+       version: 1a
+       serial: 10:5f:ad:d6:2b:ee
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix bus_master cap_list ethernet physical wireless
+       configuration: broadcast=yes driver=iwlwifi driverversion=6.6.38-trim firmware=72.daa05125.0 ty-a0-gf-a0-72.uc ip=192.168.0.113 latency=0 link=yes multicast=yes wireless=IEEE 802.11
+       resources: irq:18 memory:80900000-80903fff
+  *-network UNCLAIMED
+       description: Ethernet controller
+       product: RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
+       vendor: Realtek Semiconductor Co., Ltd.
+       physical id: 0
+       bus info: pci@0000:02:00.0
+       version: 2b
+       width: 64 bits
+       clock: 33MHz
+       capabilities: pm msi pciexpress msix cap_list
+       configuration: latency=0
+       resources: ioport:3000(size=256) memory:80804000-80804fff memory:80800000-80803fff
+

*-network UNCLAIMED 表明你的网络接口未被驱动程序识别和管理。为了解决这个问题,你需要安装或重新加载正确的网络驱动程序。

+

下载并安装 Realtek 网络驱动程序

+

访问 Realtek 官方网站下载适用于你的 RTL8111/8168/8411 网络控制器的驱动程序,通常是 r8168 驱动程序。

+ +
root@server:/vol1# cd /vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00/
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# ls
+autorun.sh  Makefile  README  src
+root@server:/vol1/1000/aaa/r8168-8.054.00/r8168-8.054.00.tar/r8168-8.054.00# sh autorun.sh
+
+Check old driver and unload it.
+Build the module and install
+Warning: modules_install: missing 'System.map' file. Skipping depmod.
+Backup r8169.ko
+rename r8169.ko to r8169.bak
+DEPMOD 6.6.38-trim
+load module r8168
+Updating initramfs. Please wait.
+update-initramfs: Generating /boot/initrd.img-6.6.38-trim
+Completed.
+

安装好了之后,理论上自动显示网口2,但IP什么的都是空的,需要点击编辑,然后按照下面的填一下。

+
+

三、最后

+

最后就可以互相ping通了,很稳定,传输速度很快很快,基本都能1ms以内

+
+]]>
+ +
+ + 推荐工程-概述 + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html + 推荐工程-概述 + 推荐工程-概述 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。 平台方、信息生产者和消费者可以分别用平台方(如:腾... + Fri, 29 Nov 2024 18:45:10 GMT + 随着移动互联网的飞速发展,人们已经处于信息过载的时代。在这个时代,信息的生产者很难将信息呈现在对其感兴趣的消费者面前,而信息消费者也难以从海量信息中找到自己感兴趣的内容。推荐系统充当了将信息生产者和信息消费者连接起来的桥梁,平台通常作为推荐系统的载体,实现信息生产者和消费者之间的匹配。

+

平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户来指代。

+

技术的最终目的都是服务业务的,我们来看下当前电商系统的模式差异。

+
+

一、淘宝和拼多多的电商模式差异

+

淘宝是流量逻辑,主体是搜索,需要海量SKU满足长尾需求;拼多多代表的是匹配逻辑,推荐商品给消费者,SKC有限,但满足结构性丰富。之前的电商模式都是从淘宝挖一块做小淘宝,而拼多多是在另一个维度做电商,满足不同的场景需求。淘宝的千人千面相当于个性化搜索,但搜索本身是长尾的,你就很难做反向定制。而拼多多集中流量到有限商品里,有了规模后再反向定制。

+

拼多多起来之后,京东、唯品会、蘑菇街都实验过相似模式,对于他们来说,拼团不过是一个创造GMV增量的工具;而拼多多是人的逻辑,我们通过拼团了解人,通过人推荐物,后期会过渡到机器推荐物。拼多多APP里几乎没有搜索,也不设购物车,你可以想像把今日头条下的信息流换成商品流就是拼多多。所以早期看大家都是低价和拼团,但我们的出发点不同、方向不同,长大了也就不一样了。

+

与其说拼多多是社交电商,不如说它是“人以群分”的电商。以前是人去找东西,现在是相似的人聚集起来,迅速产生需求量,这种模式给供应链优化创造了很大的机会。

+

二、搜索、推荐、广告三者的异同

+

搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:

+
    +
  1. 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
  2. +
  3. 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
  4. +
  5. 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
  6. +
  7. 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。
  8. +
+

**广告:**借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

+

三、推荐整体架构

+
+

推荐的框架主要有以下几个模块:

+
    +
  • 协议调度:请求的发送和结果的回传。在请求中,用户会发送自己的 ID,地理位置等信息。结果回传中会返回推荐系统给用户推荐的结果。
  • +
  • 推荐算法:算法按照一定的逻辑为用户产生最终的推荐结果。不同的推荐算法基于不同的逻辑与数据运算过程。
  • +
  • 消息队列:数据的上报与处理。根据用户的 ID,拉取例如用户的性别、之前的点击、收藏等用户信息。而用户在 APP 中产生的新行为,例如新的点击会储存在存储单元里面。
  • +
  • 存储单元:不同的数据类型和用途会储存在不同的存储单元中,例如内容标签与内容的索引存储在 mysql 里,实时性数据存储在 redis 里,需要进行数据统计的数据存储在 TDW 里。
  • +
+

电商系统演变到现在,各类归纳层出不穷,传统零售是“货-场-人”,而新零售是“人-货-场”,强调以人为核心,根据用户的需求推送产品和打造消费场景。在电商线上平台三者具体表现就是:

+

**人:**是指店铺访客、流量。通过数据分析、用户画像、顾客细分等手段,可以更加精准地理解顾客,从而提供个性化的产品和服务
+**货:**是指商家商品。选择合适的商品,有竞争力的价格,保证商品的质量和供应链的高效运营,都是成功的关键
+**场:**由最初的交易平台(如京东、淘宝等)演变到场景(首页、商详推荐页、加购推荐页等),实现千人千面。

+
+

回到我们推荐本身,推荐的本质就是排序,把成千上万的商品更精准的展现到用户面前,提升用户的购买量,从而变现。技术层面,最核心的就是推荐算法,目前市面上对推荐系统基本等于推荐算法,而做为一名后台,在推荐系统中的角色介绍可为少之又少,本系列就用后台的角色去看待整个推荐系统

+

参考

+

1.FunRec

+

2.从零开始了解推荐系统全貌

+

3.从“人、货、场”的不同视角,重新审视电商和新零售

+

4.“人货场”,在产品业务分析中的具体应用

+]]>
+ +
+ + 2024-11-09上海迪斯尼 + http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html + http://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html + 2024-11-09上海迪斯尼 + 2024-11-09上海迪斯尼 + Sun, 10 Nov 2024 15:55:36 GMT + + + + 买了个mini主机 + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html + 买了个mini主机 + 买了个mini主机 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是... + Sun, 27 Oct 2024 13:33:05 GMT + 虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。

+

成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。

+
iowejofwjeofjwoeifjwoe
iowejofwjeofjwoeifjwoe
+

一、系统的安装

+

系统我看是支持windows,还有现在Ubuntu,但是我这种选择的是centos stream 9, 10的话我也找过,但是发现很多软件还有不兼容。所以最终还是centos stream 9。

+

1、下载Ventoy软件

+

去Ventoy官网下载Ventoy软件(Download . Ventoy)如下图界面

+
QQ_1727625608185
QQ_1727625608185
+

2、制作启动盘

+

选择合适的版本以及平台下载好之后,进行解压,解压出来之后进入文件夹,如下图左边所示,双击打开Ventoy2Disk.exe,会出现下图右边的界面,选择好自己需要制作启动盘的U盘,然后点击安装等待安装成功即可顺利制作成功启动U盘。

+

3、centos安装

+

直接取官网,下载完放到u盘即可。

+
QQ_1727625711792
QQ_1727625711792
+

它的BIOS是按F7启动,直接加载即可。

+
image-20241007222938414
image-20241007222938414
+

之后就是正常的centos安装流程了。

+

二、连接wifi

+

因为是用作服务器的,所以并没有给它配置个专门的显示器,只要换个网络,就连不上新的wifi了,这里可以用网线连接路由器进行下面的操作即可。

+

在 CentOS 系统中,通过命令行连接 Wi-Fi 通常需要使用 nmcli(NetworkManager 命令行工具)来管理网络连接。nmcli 是 NetworkManager 的一个命令行接口,可以用于创建、修改、激活和停用网络连接。以下是如何使用 nmcli 命令行工具连接 Wi-Fi 的详细步骤。

+

步骤 1: 检查网络接口

+

首先,确认你的 Wi-Fi 网络接口是否被检测到,并且 NetworkManager 是否正在运行。

+
nmcli device status
+

输出示例:

+
DEVICE         TYPE      STATE         CONNECTION
+wlp3s0         wifi      disconnected  --
+enp0s25        ethernet  connected     Wired connection 1
+lo             loopback  unmanaged     --
+

在这个示例中,wlp3s0 是 Wi-Fi 接口,它当前处于未连接状态。

+

步骤 2: 启用 Wi-Fi 网卡

+

如果你的 Wi-Fi 网卡是禁用状态,可以通过以下命令启用:

+
nmcli radio wifi on
+

验证 Wi-Fi 是否已启用:

+
nmcli radio
+

步骤 3: 扫描可用的 Wi-Fi 网络

+

使用 nmcli 扫描附近的 Wi-Fi 网络:

+
nmcli device wifi list
+

你将看到可用的 Wi-Fi 网络列表,每个网络都会显示 SSID(网络名称)、安全类型等信息。

+

步骤 4: 连接到 Wi-Fi 网络

+

使用 nmcli 命令连接到指定的 Wi-Fi 网络。例如,如果你的 Wi-Fi 网络名称(SSID)是 MyWiFiNetwork,并且密码是 password123,你可以使用以下命令连接:

+
nmcli device wifi connect 'xxxxxx' password 'xxxxx'
+

你应该会看到类似于以下输出,表明连接成功:

+
Device 'wlp3s0' successfully activated with 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
+

步骤 5: 验证连接状态

+

验证网络连接状态:

+
nmcli connection show
+

查看当前连接的详细信息:

+
nmcli device show wlp3s0
+

上面的其实在2024年其实有点问题,因为会默认连接2.4GHz的wifi,使用的时候很明显没有那么快,特别是在用命令行的时候会觉得明显卡顿,现在需要切换到5GHz的wifi。
+首先,使用 nmcli 获取可用 WiFi 网络及其 BSSID:

+
nmcli -f SSID,BSSID,CHAN dev wifi list
+

示例输出:

+
SSID       BSSID              CHAN
+MyNetwork  XX:XX:XX:XX:XX:01  36
+MyNetwork  XX:XX:XX:XX:XX:02  1
+

在这里, XX:XX:XX:XX:XX:01 是 5GHz 网络的 BSSID。

+

使用 nmcli 连接到特定的 BSSID

+
nmcli dev wifi connect 'XX:XX:XX:XX:XX:01' password 'your_password'
+

三、VNC远程连接

+

桌面还是偶尔需要用一下的,虽然用的不多。

+
root@master:~# dnf install  -y  tigervnc-server
+root@master:~# vncserver
+bash: vncserver: command not found...
+Install package 'tigervnc-server' to provide command 'vncserver'? [N/y] y
+
+
+ * Waiting in queue... 
+ * Loading list of packages.... 
+The following packages have to be installed:
+ dbus-x11-1:1.12.20-8.el9.x86_64        X11-requiring add-ons for D-BUS
+ tigervnc-license-1.14.0-3.el9.noarch   License of TigerVNC suite
+ tigervnc-selinux-1.14.0-3.el9.noarch   SELinux module for TigerVNC
+ tigervnc-server-1.14.0-3.el9.x86_64    A TigerVNC server
+ tigervnc-server-minimal-1.14.0-3.el9.x86_64    A minimal installation of TigerVNC server
+Proceed with changes? [N/y] y
+
+
+ * Waiting in queue... 
+ * Waiting for authentication... 
+ * Waiting in queue... 
+ * Downloading packages... 
+ * Requesting data... 
+ * Testing changes... 
+ * Installing packages... 
+
+WARNING: vncserver has been replaced by a systemd unit and is now considered deprecated and removed in upstream.
+Please read /usr/share/doc/tigervnc/HOWTO.md for more information.
+
+You will require a password to access your desktops.
+
+getpassword error: Inappropriate ioctl for device
+Password:
+

之后在mac开启屏幕共享就可以了

+
image-20241007225855305
image-20241007225855305
+
QQ_1728313164289
QQ_1728313164289
+

四、docker 配置

+

docker安装我以为很简单,没想到这里是最难的一步了。安装完docker之后,总是报错:

+
Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded
+

即使改了mirrors也毫无作用

+
{
+  "registry-mirrors": [
+    "https://ylce84v9.mirror.aliyuncs.com"
+        ]
+}
+

看起来好像是docker每次pull镜像都要访问一次registry-1.docker.io,但是这个网址国内已经无法连接了,各种折腾,这里只贴一下代码吧,原理就就不讲了(懂得都懂)。

+
img
img
+
sslocal -c /etc/shadowsocks.json -d start
+curl --socks5 127.0.0.1:1080 http://httpbin.org/ip
+
+sudo yum -y install privoxy
+

vim /etc/systemd/system/docker.service.d/http-proxy.conf

+
[Service]
+Environment="HTTP_PROXY=http://127.0.0.1:8118"
+

/etc/systemd/system/docker.service.d/https-proxy.conf

+
[Service]
+Environment="HTTPS_PROXY=http://127.0.0.1:8118"
+

最后重启docker

+
systemctl start privoxy
+systemctl enable privoxy
+sudo systemctl daemon-reload
+sudo systemctl restart docker
+
QQ_1729956484197
QQ_1729956484197
+

五、文件共享

+

sd卡好像读取不了,只能换个usb转换器

+
fdisk -l
+mount /dev/sdb1 /mnt/usb/sd
+

在CentOS中设置文件共享,可以使用Samba服务。以下是配置Samba以共享文件的基本步骤:

+
    +
  1. 安装Samba
  2. +
+
sudo yum install samba samba-client samba-common
+
    +
  1. +

    设置共享目录

    +

    编辑Samba配置文件/etc/samba/smb.conf,在文件末尾添加以下内容:

    +
  2. +
+
[shared]
+   path = /path/to/shared/directory
+   writable = yes
+   browseable = yes
+   guest ok = yes
+
    +
  1. +

    设置Samba密码

    +

    为了允许访问,需要为用户设置一个Samba密码:

    +
  2. +
+
sudo smbpasswd -a your_username
+
    +
  1. 重启Samba服务
  2. +
+
sudo systemctl restart smb.service
+sudo systemctl restart nmb.service
+
    +
  1. +

    配置防火墙(如果已启用)

    +

    允许Samba通过防火墙:

    +
  2. +
+
sudo firewall-cmd --permanent --zone=public --add-service=samba
+sudo firewall-cmd --reload
+

现在,您应该能够从网络上的其他计算机通过SMB/CIFS访问共享。在Windows中,你可以使用\\centos-ip\shared,在Linux中,你可以使用smbclient //centos-ip/shared -U your_username

+
QQ_1730035390803
QQ_1730035390803
+

参考:

+

https://shadowsockshelp.github.io/Shadowsocks/linux.html

+

https://stackoverflow.com/questions/48056365/error-get-https-registry-1-docker-io-v2-net-http-request-canceled-while-b

+]]>
+ +
+ + 流程编排LiteFlow + http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html + http://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html + 流程编排LiteFlow + 流程编排LiteFlow LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得... + Sun, 08 Sep 2024 16:55:53 GMT + LiteFlow真的是相见恨晚啊,之前做过的很多系统,都会用各种if else,switch这些来解决不同业务方提出的问题,有时候还要“切一个分支”来搞这些额外的事情,把代码搞得一团糟,毫无可读性而言。如何打破僵局?LiteFlow为解耦逻辑而生,为编排而生,在使用LiteFlow之后,你会发现打造一个低耦合,灵活的系统会变得易如反掌!

+

另外, LiteFlow 和 Activiti 们并不是同一个东西,而是面向不同的使用场景和需求。LiteFlow 更加轻量灵活,适合需要简单流程管理和动态配置的场景;而 Activiti 则是一个全面的 BPM 引擎,适合需要复杂业务流程管理和任务管理的场景。根据具体业务需求,可以选择合适的工具来实现流程编排。

+

背景

+

之前做过一个数据分发系统,需要消费kafka的数据,下游有不同的业务,每个业务可能有共同的地方,也有不同的地方,在经过各类的处理之后,最后数据分发到下游里面去。为了简化代码方便理解,我们定义4个Handler(A、B、C、D),然后有3个不同的业务,需要经过不同的Handler,整个流程如下。

+image-20241108000137572 +

如果要在一个代码实现上诉功能,我们第一反应可能是责任链设计模式,每个业务一条链路,在Spring中,类似下面的代码:

+
public abstract class Handler {
+    abstract void handler(Request request);
+}
+
+@Component
+@Slf4j
+public class HandlerA extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器1");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerB extends Handler {
+    @Override
+    public void handler(Request request) {
+        log.info("处理器2");
+    }
+}
+
+@Component
+@Slf4j
+public class HandlerC extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器3");
+    }
+}
+@Component
+@Slf4j
+public class HandlerD extends Handler{
+    @Override
+    public void handler(Request request) {
+        log.info("处理器4");
+    }
+}
+
+//然后我们定义一个枚举类,用来配置不同业务需要经历过的处理器。
+public enum HandleBuz {
+    Business_1(HandlerA,HandlerB),
+    Business_2(HandlerB,HandlerC),
+    Business_3(HandlerA,HandlerD);
+    public final Class<? extends Handler>[] processors;
+    public HandleBuz(Class<? extends Handler>[] processors){
+        this.processors=processors;
+    }    
+    public void handle(){
+        for (Handler handler : processors) {
+            handler.handler(xxx);
+        }
+    }
+
+}
+

通过配置责任链,可以灵活地组合处理对象,实现不同的处理流程,并且可以在运行时动态地改变处理的顺序,由于责任链模式遵循开闭原则,新的处理者可以随时被加入到责任链中,不需要修改已有代码,提供了良好的扩展性。但实际上面对各种需求的时候,没法做到完全的解耦,比如对于HandlerA,如果业务1和业务2都有定制化的需求(来自产品提的临时或长期需求),此时是应该再HandlerA中用if else解决,还是再额外开个HandlerA_1和HandlerA_2。这类特性需求会非常多,最终把代码可读性变得越来越低。

+

一、为什么需要流程编排

+

LiteFlow由Baidu开源,专注于逻辑驱动流程编排,通过组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑。它以其轻量级、快速、稳定且可编排的特性,在业务流程管理、规则引擎、工作流、订单处理、数据处理、微服务编排以及智能化流程管理等领域都有广泛的应用前景。

+
img
img
+

二、它可以解决什么问题

+

对大部分不断迭代的代码来说,历史遗留的代码加上需要面对各类各样的需求,代码会变得越来越难维护,甚至变成屎山。我们想着不断的去进行解耦,不断的去进行切割拆分,还要兼顾新需求,就怕蝴蝶效应导致大故障,liteflow能帮我们在解耦上更加清晰一点。
+(1)复杂业务流程编排和管理
+在一些应用场景中,业务逻辑往往非常复杂,涉及多个步骤的执行,并且这些步骤之间具有复杂的依赖关系。LiteFlow 可以帮助开发者通过配置和代码相结合的方式定义和管理这些流程。
+(2)流程动态配置
+LiteFlow 允许通过配置文件或者数据库动态修改流程,而无需修改代码。这意味着可以根据不同的业务需求快速调整并发布新的流程,而不需要重新部署应用。
+(3)流程节点的复用和解耦
+在使用 LiteFlow 时,每个业务步骤都可以定义为一个独立的节点(Node),这些节点可以独立开发、测试和维护,并且可以在多个流程中复用。通过这种方式,可以实现业务逻辑的复用和解耦,提高代码的可维护性。
+(4)节点状态和错误处理
+LiteFlow 提供了丰富的节点状态管理和错误处理机制,允许开发者在流程执行过程中捕获和处理异常,从而确保系统的稳定性和健壮性。
+(5) 高扩展性和自定义能力
+LiteFlow 具有高度的扩展性,开发者可以根据自身业务的特殊需求定制节点、组件和插件,从而满足复杂场景的要求。

+

以下是一些实际使用 LiteFlow 的示例场景:
+(1)订单处理系统:在电商系统中,订单处理涉及多个步骤,如库存检查、支付处理、订单确认和发货等。LiteFlow 可以帮助将这些步骤分开独立实现,然后通过流程引擎编排执行。
+(2)审批流程:在企业中,审批流程通常包括多个节点(如申请、审批、复核、归档等),并且这些节点之间可能有条件和依赖关系。LiteFlow 可以帮助动态配置和管理这些流程,提高审批效率。
+(3)营销活动:在一些营销活动中,不同的活动环节和逻辑可能会因用户行为和外部条件而变化。LiteFlow 可以帮助实现灵活的活动规则配置和执行。

+

三、LiteFlow改造之后

+

首先定义并实现一些组件,确保SpringBoot会扫描到这些组件并注册进上下文。

+
@Slf4j
+@LiteflowComponent("a")
+public class HandlerA extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("b")
+public class HandlerB extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("c")
+public class HandlerC extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+
+@Slf4j
+@LiteflowComponent("d")
+public class HandlerD extends NodeComponent {
+    @Override
+    public void process() throws Exception {
+        Customizer contextBean = this.getContextBean(Customizer.class);
+    }
+}
+

同时,你得在resources下的config/flow.el.xml中定义规则:

+
<?xml version="1.0" encoding="UTF-8"?>
+<flow>
+    <chain name="chain1">
+        THEN(
+        a,b
+        );
+    </chain>
+    <chain name="chain2">
+        THEN(
+        b,c
+        );
+    </chain>
+    <chain name="chain3">
+        THEN(
+        a,d
+        );
+    </chain>
+</flow>
+

最后,在消费kafka的时候,先定义一个ruleChainMap,用来判断根据唯一的id(业务id或者消息id)来判断走哪条chain、哪个组件等,甚至可以定义方法级别的组件。

+
    private Map<Integer, String> ruleChainMap = new HashMap<>();
+    @Resource
+    private FlowExecutor flowExecutor;
+
+    @PostConstruct
+    private void init() {
+        ruleChainMap.put(1, "业务1");
+        ruleChainMap.put(2, "业务2");
+        ruleChainMap.put(3, "业务3");
+    }
+
+    @KafkaListener(topics = "xxxx")
+    public void common(List<ConsumerRecord<String, String>> records) {
+        for (ConsumerRecord<String, String> record : records) {
+            ...
+            String chainName = ruleChainMap.get("唯一id(可以是record里的,也可以全局定义的id)");
+            LiteflowResponse response = flowExecutor.execute2Resp(chainName, xxx, xxx, new TempContext());
+        }
+    }
+

由于篇幅的关系,这里不再讲解怎么传递上下文的关系,可以自己去官网研究一下。另外,上面的例子因为是简化之后的,如果你觉得不够形象,可以看看下面的实际业务。这个如果不使用liteflow,可能就得在主流程代码里增加各种if else,甚至后续改了一小块也不知道对别的地方有没有影响。

+
image-20241108000231371
image-20241108000231371
+

总结

+

后续,如果面对产品经理“来自大领导的一个想法,我不知道后续还会不会一直做下去,反正先做了再说”这类需求,就可以自己定义一个LiteFlow的组件,既不污染主流程的代码,后续下线了删掉即可,赏心悦目。

+

文档&参考

+

1.【腾讯文档】业务处理复杂 https://docs.qq.com/flowchart/DZVFURmhCb0JFUHFD
+2.【腾讯文档】业务处理复杂2 https://docs.qq.com/flowchart/DZXVOaUV5VGRtc3ZD
+3.一文搞懂设计模式—责任链模式
+4.LiteFlow官网

+]]>
+ +
+ + 推荐系统入门 + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html + http://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html + 推荐系统入门 + 推荐系统入门 转载自:https://www.cnblogs.com/cgli/p/17225189.html 一、背景 近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不... + Sun, 01 Sep 2024 13:54:44 GMT + 转载自:https://www.cnblogs.com/cgli/p/17225189.html

+

一、背景

+

近期由于公司业务系统需要做一个推荐系统,应该说是实现一个相当简单推荐逻辑。毕竟业务场景相当简单,企业的数据规模也比较小,各种用户数据、交易数据、订单数据、行为数据等加在一起 100 多 TB 量级,这点数据量在巨大的平台商面前都谈不数据规模。数据级量小,业务场景也简单,实现起比较的容易,但原理基本上大同小异。

+

本文尝试从最简单的推荐入手,我们暂不去讨论大规模数据分析和算法。更多从软件工程角度思考问题,那些高大上算法留给读者去思考。开源获取:http://www.jnpfsoft.com/?from=infoq

+

下面就来谈谈整个推荐的设计实现过程。

+

二、推荐系统介绍

+

2.1、为什么需要推荐系统?

+

首先推荐系统作用是很大的。推荐系统在很多业务场景有广泛使用和发挥的空间,它的应用和影子无处不在,在多媒体内容、广告平台和电商平台尤为常见。大多平台商或企业都是基于大数据算法分析做推荐系统。推荐算法也是层出不穷,比如相似度计算、近邻推荐、概率矩阵分解、概率图等,当然也有综合多种算法融合使用。

+

互联网时代,数据呈爆炸式增长,前所未有的数据量远远超过受众的接收和处理能力,因此,从海量复杂数据中有效获取关键性有用信息成为必须解决的问题。面对信息过载问题,人们迫切需要一种高效的信息过滤系统,“推荐系统”应运而生。

+

20 世纪 90 年代以来,尽管推荐系统在理论、方法和应用方面取得了系列重要进展,但数据的稀疏性与长尾性、用户行为模式挖掘、可解释性、社会化推荐等问题仍然是其面临的重要挑战。

+

进一步地,伴随互联网及信息技术的持续飞速发展,用户规模与项目数量急剧增长,相应地,用户行为数据的稀疏性、长尾性问题更加凸显。也就是说目前各大平台虽然已经推荐系统,但是实际应用当中还是面临很多问题,仍然有很大的提升空间。这是技术挑战也机会,当然这也是我们这些从业者可以发挥的地方。

+

2.2、推荐系统解决什么问题?

+

推荐系统从 20 世纪 90 年代就被提出来了,但是真正进入大众视野以及在各大互联网公司中流行起来,还是最近几年的事情。

+

随着移动互联网的发展,越来越多的信息开始在互联网上传播,产生了严重的信息过载。因此,如何从众多信息中找到用户感兴趣的信息,这个便是推荐系统的价值。精准推荐解决了用户痛点,提升了用户体验,最终便能留住用户。

+

推荐系统本质上就是一个信息过滤系统,通常分为:召回、排序、重排序这 3 个环节,每个环节逐层过滤,最终从海量的物料库中筛选出几十个用户可能感兴趣的物品推荐给用户。

+

推荐系统的分阶段过滤流程如下图所示:

+
img
img
+

2.3、推荐系统应用场景

+

哪里有海量信息,哪里就有推荐系统,我们每天最常用的 APP 都涉及到推荐功能:

+

资讯类:今日头条、腾讯新闻等

+

电商类:淘宝、京东、拼多多、亚马逊等

+

娱乐类:抖音、快手、爱奇艺等

+

生活服务类:美团、大众点评、携程等

+

社交类:微信、陌陌、脉脉等

+
img
img
+

实际例子还有很多,稍微上一点规模的平台或 APP 都有这一个推荐模块。

+

推荐系统的应用场景通常分为以下两类:

+

基于用户维度的推荐:根据用户的历史行为和兴趣进行推荐,比如淘宝首页的猜你喜欢、抖音的首页推荐等。

+

基于物品维度的推荐:根据用户当前浏览的标的物进行推荐,比如打开京东 APP 的商品详情页,会推荐和主商品相关的商品给你。

+

2.4、搜索、推荐、广告三者的异同

+

搜索和推荐是 AI 算法最常见的两个应用场景,在技术上有相通的地方。

+

搜索:有明确的搜索意图,搜索出来的结果和用户的搜索词相关。推荐:不具有目的性,依赖用户的历史行为和画像数据进行个性化推荐。广告:借助搜索和推荐技术实现广告的精准投放,可以将广告理解成搜索推荐的一种应用场景,技术方案更复杂,涉及到智能预算控制、广告竞价等。

+

三、推荐系统通用框架

+

推荐系统涉及周边和自己模块还是比较多,这里主要从最简单推荐系统自身功能去构思设计简单结构。

+
img
img
+

上面这个图基本把推荐处理过程画出来,结构比较清晰,看图理想即可。

+

从分层架构设计视角来说可以分成多层架构形式

+

分层:排序层、过滤层、召回层、数据存储层、计算平台、数据源。

+

可以说市面上推荐系统设计都是差不多是这个样子,只是里面使用技术或组件不同而已。

+
img
img
+

上面是推荐系统的整体架构图,自下而上分成了多层,各层的主要作用如下:

+
    +
  • +

    数据源:推荐算法所依赖的各种数据源,包括物品数据、用户数据、行为日志、其他可利用的业务数据、甚至公司外部的数据。

    +
  • +
  • +

    计算平台:负责对底层的各种异构数据进行清洗、加工,离线计算和实时计算。

    +
  • +
  • +

    数据存储层:存储计算平台处理后的数据,根据需要可落地到不同的存储系统中,比如 Redis 中可以存储用户特征和用户画像数据,ES 中可以用来索引物品数据,Faiss 中可以存储用户或者物品的 embedding 向量等。

    +
  • +
  • +

    召回层:包括各种推荐策略或者算法,比如经典的协同过滤,基于内容的召回,基于向量的召回,用于托底的热门推荐等。为了应对线上高并发的流量,召回结果通常会预计算好,建立好倒排索引后存入缓存中。

    +
  • +
  • +

    融合过滤层:触发多路召回,由于召回层的每个召回源都会返回一个候选集,因此这一层需要进行融合和过滤。

    +
  • +
  • +

    排序层:利用机器学习或者深度学习模型,以及更丰富的特征进行重排序,筛选出更小、更精准的推荐集合返回给上层业务。从数据存储层到召回层、再到融合过滤层和排序层,候选集逐层减少,但是精准性要求越来越高,因此也带来了计算复杂度的逐层增加,这个便是推荐系统的最大挑战。

    +
  • +
+

其实对于推荐引擎来说,最核心的部分主要是两块:特征和算法。

+
img
img
+

这些工具和技术框架都是比较成熟稳定的,是众多厂商在实际业务场景中选择应用的。所以也没有太多特殊的地方。

+

特征计算由于数据量大,通常采用大数据的离线和实时处理技术,像 Spark、Flink 等,然后将计算结果保存在 Redis 或者其他存储系统中(比如 HBase、MongoDB 或者 ES),供召回和排序模块使用。

+

召回算法的作用是:从海量数据中快速获取一批候选数据,要求是快和尽可能的准。这一层通常有丰富的策略和算法,用来确保多样性,为了更好的推荐效果,某些算法也会做成近实时的。

+

排序算法的作用是:对多路召回的候选集进行精细化排序。它会利用物品、用户以及它们之间的交叉特征,然后通过复杂的机器学习或者深度学习模型进行打分排序,这一层的特点是计算复杂但是结果更精准。

+

四、经典算法

+

了解了推荐系统的整体架构和技术方案后,下面带大家深入一下算法细节。这里选择图解的是推荐系统中的明星算法:协同过滤(Collaborative Filtering,CF)。

+

对于很多同学来说,可能觉得 AI 算法晦涩难懂,门槛太高,确实很多深度学习算法的确是这样,但是协同过滤却是一个简单同时效果很好的算法,只要你有初中数学的基础就能看懂。

+

4.1、协同过滤是什么?

+

协同过滤算法的核心就是「找相似」,它基于用户的历史行为(浏览、收藏、评论等),去发现用户对物品的喜好,并对喜好进行度量和打分,最终筛选出推荐集合。它又包括两个分支:

+
    +
  • 基于用户的协同过滤:User-CF,核心是找相似的人。比如下图中,用户 A 和用户 C 都购买过物品 a 和物品 b,那么可以认为 A 和 C 是相似的,因为他们共同喜欢的物品多。这样,就可以将用户 A 购买过的物品 d 推荐给用户 C。
  • +
+
img
img
+

基于用户的协同过滤示例

+
    +
  • 基于物品的协同过滤:Item-CF,核心是找相似的物品。比如下图中,物品 a 和物品 b 同时被用户 A,B,C 购买了,那么物品 a 和物品 b 被认为是相似的,因为它们的共现次数很高。这样,如果用户 D 购买了物品 a,则可以将和物品 a 最相似的物品 b 推荐给用户 D。
  • +
+
img
img
+

4.2、如何找相似?

+

协同过滤的核心就是找相似,User-CF 是找用户之间的相似,Item-CF 是找物品之间的相似,那到底如何衡量两个用户或者物品之间的相似性呢?

+

我们都知道,对于坐标中的两个点,如果它们之间的夹角越小,这两个点越相似。

+

这就是初中学过的余弦距离,它的计算公式如下:

+
img
img
+

举个例子,A 坐标是(0,3,1),B 坐标是(4,3,0),那么这两个点的余弦距离是 0.569,余弦距离越接近 1,表示它们越相似。

+
img
img
+

除了余弦距离,衡量相似性的方法还有很多种,比如:欧式距离、皮尔逊相关系数、Jaccard 相似系数等等,这里不做展开,只是计算公式上的差异而已。

+

4.3、Item-CF 的算法流程

+

清楚了相似性的定义后,下面以 Item-CF 为例,详细说下这个算法到底是如何选出推荐物品的?

+

4.3.1 、整理物品的共现矩阵

+

假设有 A、B、C、D、E 5 个用户,其中用户 A 喜欢物品 a、b、c,用户 B 喜欢物品 a、b 等等。

+
img
img
+

所谓共现,即:两个物品被同一个用户喜欢了。比如物品 a 和 b,由于他们同时被用户 A、B、C 喜欢,所以 a 和 b 的共现次数是 3,采用这种统计方法就可以快速构建出共现矩阵。

+

4.3.2、 计算物品的相似度矩阵

+

对于 Item-CF 算法来说,一般不采用前面提到的余弦距离来衡量物品的相似度,而是采用下面的公式:

+
img
img
+

其中,N(u) 表示喜欢物品 u 的用户数,N(v) 表示喜欢物品 v 的用户数,两者的交集表示同时喜欢物品 u 和物品 v 的用户数。

+

很显然,如果两个物品同时被很多人喜欢,那么这两个物品越相似。

+

基于第 1 步计算出来的共现矩阵以及每个物品的喜欢人数,便可以构造出物品的相似度矩阵:

+
img
img
+

4.3.2、 推荐物品

+

最后一步,便可以基于相似度矩阵推荐物品了,公式如下:

+
img
img
+

其中,Puj 表示用户 u 对物品 j 的感兴趣程度,值越大,越值得被推荐。

+

N(u) 表示用户 u 感兴趣的物品集合,S(j,N) 表示和物品 j 最相似的前 N 个物品,Wij 表示物品 i 和物品 j 的相似度,Rui 表示用户 u 对物品 i 的兴趣度。

+

上面的公式有点抽象,直接看例子更容易理解,假设我要给用户 E 推荐物品,前面我们已经知道用户 E 喜欢物品 b 和物品 c,喜欢程度假设分别为 0.6 和 0.4。

+

那么,利用上面的公式计算出来的推荐结果如下:

+
img
img
+

因为物品 b 和物品 c 已经被用户 E 喜欢过了,所以不再重复推荐。最终对比用户 E 对物品 a 和物品 d 的感兴趣程度,因为 0.682 > 0.3,因此选择推荐物品 a。

+

五、如何实现推荐系统

+

5.1、选择数据集

+

这里采用的是推荐领域非常经典的 MovieLens 数据集,它是一个关于电影评分的数据集,官网上提供了多个不同大小的版本,下面以 ml-1m 数据集(大约 100 万条用户评分记录)为例。

+

下载解压后,文件夹中包含:ratings.dat、movies.dat、users.dat 3 个文件,共 6040 个用户,3900 部电影,1000209 条评分记录。各个文件的格式都是一样的,每行表示一条记录,字段之间采用 :: 进行分割。

+

以 ratings.dat 为例,每一行包括 4 个属性:UserID, MovieID, Rating, Timestamp。

+

通过脚本可以统计出不同评分的人数分布:

+
img
img
+

5.2、读取原始数据

+

程序主要使用数据集中的 ratings.dat 这个文件,通过解析该文件,抽取出 user_id、movie_id、rating 3 个字段,最终构造出算法依赖的数据,并保存在变量 dataset 中,它的格式为:dict[user_id][movie_id] = rate

+

5.3、构造物品的相似度矩阵

+

基于第 2 步的 dataset,可以进一步统计出每部电影的评分次数以及电影的共生矩阵,然后再生成相似度矩阵。

+

5.4、基于相似度矩阵推荐物品

+

最后,可以基于相似度矩阵进行推荐了,输入一个用户 id,先针对该用户评分过的电影,依次选出 top 10 最相似的电影,然后加权求和后计算出每个候选电影的最终评分,最后再选择得分前 5 的电影进行推荐。

+

5.5、调用推荐系统

+

下面选择 UserId=1 这个用户,看下程序的执行结果。由于推荐程序输出的是 movieId 列表,为了更直观的了解推荐结果,这里转换成电影的标题进行输出。

+

Java 代码示例

+
import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+public class CFRecommendation {
+    // 使用MovieLens数据集
+    private static final String RATINGS_FILE = "ratings.csv";
+    // 用户ID-电影ID-打分
+    private static Map<Integer, Map<Integer, Double>> ratings;
+
+    // 加载ratings.csv文件
+    private static void loadRatings() throws IOException {
+        File file = new File(RATINGS_FILE);
+        Scanner scanner = new Scanner(file);
+        ratings = new HashMap<>();
+        while (scanner.hasNextLine()) {
+            String line = scanner.nextLine();
+            String[] data = line.split(",");
+            int userId = Integer.parseInt(data[0]);
+            int movieId = Integer.parseInt(data[1]);
+            double rating = Double.parseDouble(data[2]);
+            // 用户-电影-打分
+            Map<Integer, Double> movieRatings = ratings.get(userId);
+            if (movieRatings == null) {
+                movieRatings = new HashMap<>();
+                ratings.put(userId, movieRatings);
+            }
+            movieRatings.put(movieId, rating);
+        }
+    }
+
+    // 计算两个用户的相似度
+    private static double calculateSimilarity(int user1, int user2) {
+        Map<Integer, Double> rating1 = ratings.get(user1);
+        Map<Integer, Double> rating2 = ratings.get(user2);
+        if (rating1 == null || rating2 == null) {
+            return 0;
+        }
+        double sum1 = 0;
+        double sum2 = 0;
+        double sumProduct = 0;
+        for (int movieId : rating1.keySet()) {
+            if (rating2.containsKey(movieId)) {
+                double rating1Value = rating1.get(movieId);
+                double rating2Value = rating2.get(movieId);
+                sum1 += rating1Value * rating1Value;
+                sum2 += rating2Value * rating2Value;
+                sumProduct += rating1Value * rating2Value;
+            }
+        }
+        return sumProduct / (Math.sqrt(sum1) * Math.sqrt(sum2));
+    }
+    
+    // 计算余弦相似度
+    public static double cosineSim(Map<String, Integer> user1, Map<String, Integer> user2){
+        double result = 0;
+        double denominator1 = 0;
+        double denominator2 = 0;
+        double numerator = 0;
+        for(String key : user1.keySet()){
+            numerator += user1.get(key) * user2.get(key);
+            denominator1 += Math.pow(user1.get(key), 2);
+            denominator2 += Math.pow(user2.get(key), 2);
+        }
+        result = numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2));
+        return result;
+    }
+
+    // 使用协同过滤算法获取用户的推荐列表
+    private static Map<Integer, Double> recommend(int userId) {
+        Map<Integer, Double> recommendList = new HashMap<>();
+        // 遍历所有用户
+        for (int otherUserId : ratings.keySet()) {
+            if (otherUserId != userId) {
+                double similarity = calculateSimilarity(userId, otherUserId);
+                Map<Integer, Double> otherRating = ratings.get(otherUserId);
+                // 遍历其他用户的评分,如果当前用户没有评分,则将其推荐给当前用户
+                for (int movieId : otherRating.keySet()) {
+                    if (!ratings.get(userId).containsKey(movieId)) {
+                        double recommendScore = otherRating.get(movieId) * similarity;
+                        recommendList.put(movieId, recommendScore);
+                    }
+                }
+            }
+        }
+        return recommendList;
+    }
+
+    public static void main(String[] args) throws IOException {
+        loadRatings();
+        // 测试用例:计算用户1与用户2的相似度
+        int user1 = 1;
+        int user2 = 2;
+        double similarity = calculateSimilarity(user1, user2);
+        System.out.println("用户1与用户2的相似度:" + similarity);
+        // 测试用例:为用户1推荐电影
+        int userId = 1;
+        Map<Integer, Double> recommendList = recommend(userId);
+        System.out.println("为用户1推荐的电影:");
+        for (int movieId : recommendList.keySet()) {
+            System.out.println("电影ID:" + movieId + ",推荐分数:" + recommendList.get(movieId));
+        }
+    }
+}
+

六、问题与展望

+

通过上面的介绍,大家对推荐系统的基本构成应该有了一个初步认识,但是真正运用到线上真实环境时,还会遇到很多算法和工程上的挑战,绝对不是几十行代码可以搞定的。

+

问题:

+
    +
  1. +

    上面的示例使用了标准化的数据集,而线上环境的数据是非标准化的,因此涉及到海量数据的收集、清洗和加工,最终构造出模型可使用的数据集。

    +

    复杂且繁琐的特征工程,都说算法模型的上限由数据和特征决定。对于线上环境,需要从业务角度选择出可用的特征,然后对数据进行清洗、标准化、归一化、离散化,并通过实验效果进一步验证特征的有效性。

    +

    算法复杂度如何降低?比如上面介绍的 Item-CF 算法,时间和空间复杂度都是 O(N×N),而线上环境的数据都是千万甚至上亿级别的,如果不做算法优化,可能几天都跑不出数据,或者内存中根本放不下如此大的矩阵数据。

    +

    实时性如何满足?因为用户的兴趣随着他们最新的行为在实时变化的,如果模型只是基于历史数据进行推荐,可能结果不够精准。因此,如何满足实时性要求,以及对于新加入的物品或者用户该如何推荐,都是要解决的问题。

    +

    算法效果和性能的权衡。从算法角度追求多样性和准确性,从工程角度追求性能,这两者之间必须找到一个平衡点。

    +

    推荐系统的稳定性和效果追踪。需要有一套完善的数据监控和应用监控体系,同时有 ABTest 平台进行灰度实验,进行效果对比。

    +
  2. +
+

展望:

+

AI 时代,算法会更加复杂和完善,推荐的效果也会越来越好,特别是随着 OpenAI ChatGPT 横空出现,推荐系统最有条件和最适合 GPT 模型去结合使用,当然也会更加高效和智能。期待我们智能版推荐系统早日面世。

+]]>
+ +
+ + 计算广告基本概念入门总结 + http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html + http://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html + 计算广告基本概念入门总结 + 计算广告基本概念入门总结 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门... + Sun, 01 Sep 2024 13:54:44 GMT + 广告业务是各大互联网公司主要的商业化变现营收来源,近年来随着互联网和移动端技术的普及,使得广告的用户触达成本大大降低,而广告的形态和内涵也逐渐变得复杂,广告不再是单纯的展示和计费,而是结合了推荐、大数据等各类计算技术,以达到广告的精准受众定向,计算广告概念由此诞生。本文基于《计算广告》及互联网内容,整理了计算广告核心概念及入门知识,希望对入行广告行业的RD和非技术同学有所帮助。

+

一、计算广告入门
+1.1、什么是计算广告
+首先要理解广告的概念,广告指的是由已确定的出资人通过各类媒介进行有关产品、观点和服务的、通常是有偿的、有组织的、综合的、劝服性的非人员信息传播活动。

+

广告的主体包括了三类:

+

广告主-有发布广告需求的用户
+广告媒介-也就是承接广告主需求以及调配广告展示的广告平台
+受众-通过媒介看到广告。
+广告活动的两个主动参与方—出资人(sponsor) 和媒体(medium) 。 在数字广告这样更加复杂的市场结构中, 我们可以用一般性的术语来描述它们: 需求方(demand) 和供给方(supply) 。 这里的需求方可以是广告主 (advertiser) 、 代表广告主利益的代理商(agency) 或其他技术形态的采买方; 这里的供给方可以是媒体, 也可以是其他技术形态的变现平台。 另外, 要特别注意的是, 广告还有一个被动的参与方, 即受众 (audience) 。

+

广告的根本目的是,通过广泛的媒体力量,使得广告能够高效、低成本地触达用户,以保持产品或服务的品牌形象或提高短期销量(也就是品牌广告和效果广告)。

+

计算广告并没有严格的定义, 个人理解计算广告指的是通过大数据、推荐算法等技术对大量数据进行分析,以达到广告的精确受众定向,并且通过计算来调控广告的整个生命周期。计算广告相较于传统广告的特点是以技术和计算为导向,以数据为基准,具备标准化的、可衡量性的广告效果指标。

+

计算广告的核心问题是如何为一系列用户与环境组合,以找到最合适的广告投放策略以优化整体的投入产出比(ROI)。

+

1.2、广告与营销的区别
+人们通常将广告理解为营销,但是两者是有一定的区别的。

+

首先广告和营销的共同点都是为了提高产品或服务的销量、营业额及利润,但是区别在于,广告更希望通过低成本触达到用户,这里的用户指的是潜在用户,而营销更希望的是对某类精准用户进行服务,可以理解为广告的目标是增量用户,潜在用户,看着品牌价值和长期利润,而营销更看重目标用户和短期效果。

+

1.3、在线广告的类型
+条幅广告(Banner Ad): 这是显示广告中中最传统,也是最典型的形式。这种广告一般是嵌入在页面中相对固定位置的图片,与内容一样需要占据固定的版面。
+文字链广告(Textual Ad): 这种广告的素材形式是一段链接到广告主落地页的文字,在搜索广告中为主流形式,同时在媒体广告中也被广泛采用。
+富媒体广告(Rich Media Ad): 这类广告往往是利用视觉冲击力较强的表现形式,在不占用固定版面位置的情况下,向用户侵入式地投送广告素材。
+富媒体广告常见的形式有弹窗、对 联、全屏等。它比较适合在高质量的媒体做一些品牌性质比较强的广告投放,但是对用户的使用体验往往影响也较大。
+一些门户网站的首页有时会为某个品牌广告主提供专门定制的、交互形式很复杂的富媒体广告, 这样的广告一般不采用受众定向的投送方式,也主要强调创意的冲击力和交互形式的特色。
+视频广告(Video Ad): 视频广告有两种最主要的形式:在视频内容播放之前的前插片广告,以及视频播放暂停时的广告。前插片广告一般采用短视频的形式,创意的冲击力和表现力要远远强于普通的显示广告,因此CPM价格往往也比较高;暂停广告则与普通的条幅广告区别不大。
+社交广告或信息流广告(Social Ad): 社交广告中最典型的形式,是插入在社交网络信息流中的广告,这种方式最早见于Twitter,产品称为“Promoted Tweets”。社交广告希望达到的效果,是通过用户的扩散式传播获 得更大的影响力,以及更可信的口碑.
+移动设备广告(Mobile Ad): 严格来说, 移动互联网上的广告与桌面电脑上的广告没有本质区别.
+邮件营销广告(Email Direct Marketing, EDM): EDM是一种主动的广告形式,它不需要等到用户接触的机会产生时才被动地提供广告,而是可以随时向认为合适的用户发送推广信息。不过也正因为如此,EDM非常容易变垃圾邮件的主要来源。
+1.4、在线广告发展历程

+
在这里插入图片描述
在这里插入图片描述
+

阶段一:传统合约广告模式,花钱买一段时间的广告位或展示次数
+阶段二:定向及合约广告阶段,签约形式为合约广告,广告采用了特定用户定向的方式进行展示,提升转化率
+阶段三:定向及竞价广告阶段,签约形式为竞价广告,广告采用了特定用户定向的方式进行展示,提升转化率
+阶段四:实时竞价模式阶段(重要里程碑),采用广告交易平台(ADX)、需求方平台(DSP)及供给方平台(SSP)完成广告竞价,出现广义第二高价
+阶段五:程序化交易,买卖双方无需在线交易,完全由程序进行交易的调控,无需人工参与
+1.5、广告有效性模型

+
在这里插入图片描述
在这里插入图片描述
+

广告效果生成过程有三个大阶段:选择(Selection)、解释(Interpretation)与态度(Attitude);或者进一步分解为六个小阶段:曝光、关注、理解、接受、保持与决策,其中每两个小阶段对应一个大阶段。

+

定性地说,越靠前的阶段,其效果的改善对点击率的贡献越大;而越靠后的阶段,其效果的改善对转化率的贡献越大。

+

曝光(Exposure)阶段: 这一阶段指的是广告物理上展现出来的过程。此阶段的有效性往往与广告位的物理属性有关,并没有太多可以通过技术优化的空间。实际的广告实践中,曝光的有效性对最终结果的影响往往远远高于其他技术性因素。
+关注(Attention)阶段:这一阶段指的是受众从物理上接触到广告到意识上注意到它的过程。那么如何使得关注阶段的效率提高呢?我们介绍几个重要的原则:
+(1)尽量不要打断用户的任务。
+(2)明确传达向用户推送此广告的原因,这一点是受众定向广告创意优化的重要方向。
+(3)内容符合用户的兴趣或需求,这是受众定向的原理基础。
+理解(Comprehension)阶段:受众意识到了广告的存在,并不意味着他一定能够理解广告传达的信息。
+(1)广告内容要在用户能理解的具体兴趣范围内,这就说明了真正精准的受众定向有多么必要。
+(2)要注意设定与关注程度相匹配的理解门槛。
+接受(Acceptance)阶段:受众理解了广告传达的信息,并不一定表示他认可这些信息。
+(1)广告的上下文环境对于广告的接受程度也有着很大的影响, 同一个品牌广告出现在某游戏社区上和门户网站首页上,用户会倾向于认为后者更具说服力,这也就是优质媒体的品牌价值。
+(2)在定向广告越来越普遍的今天,如何让合适的广告出现在合适的媒体上,即广告安全(Ad Safety)的问题,正在引起大家越来越多的关注。
+保持(Retention)阶段:对于不仅仅追求短期转化的广告商,当然希望广告传达的信息给用户留下长久的记忆,以影响他长时间的选择。
+决策(Decision)阶段:成功广告的最终作用是带来用户的转化行为,虽然这一阶段已经离开了广告的业务范围,但好的广告还是能够为转化率的提高做好铺垫。
+二、计算广告的核心概念
+品牌广告(Brand Awareness)
+长期投放,用于建立品牌形象、建立品牌理念,而不诉求直接销售量的广告

+

直接效果广告/效果广告(Direct Response)
+短期投放,为了提升产品销售量或提高知名度而投放的广告,因此通常按效果计费(CPA或CPS)

+

展示广告(display advertising)
+以图形展示为核心的广告形式,如常见的banner、视频广告等,通常以CPM、CPT为计费方式

+

位置拍卖(position auction)
+假设有一组广告位可使用,则将广告位按价值排序,并将广告主出价排序,并依次对应投放,价高者将得到更好的广告位

+

CPT(Cost Per Time)
+按展示时长计费;可以充分发挥橱窗效应,但无法利用受众定向技术,效率较低;使用于高曝光的品牌广告

+

CPM(Cost Per Mille)
+即“千人成本”,按访问量(每千人的访问量)计费,是目前最主流的结算方法,具备受众定向能力,适用于实时竞价广告

+

eCPM(千人收益)
+e指的是expect,也就是预期最优的的千人访问收益。

+

CPC(Cost Per Click)
+按点击次数计费

+

CPA(Cost Per Action)
+按用户行为、实际投放效果计费,如用户的注册、购买、安装行为,供给方运营难度较大,而需求方无任何风险,适合于效果广告

+

CPS(Cost Per Sell) ROI(Return On Investment)
+同CPA,都是按实际效果计费的方式,ROI即常说的“投资回报率”

+

点击率(Click Through Rate,CTR)
+即广告点击与广告展示的比率

+

到达率 (Reach Rate)
+落地页打开次数与点击次数比例,落地页(landing page)即点击后所跳转的页面

+

转化率CVR (Conversion Rate)
+转化次数与到达次数的比例

+

在线广告交易模式
+合约广告、竞价广告、实时竞价、程序化广告

+

广告交易的相关机构与平台(程序化广告)
+广告网络 ADN(ad net-work):连接供给方与需求方,负责批量运营媒体广告位,并对流量进行售卖的平台
+需求方平台DSP(demand side platform):为广告主提供跨媒介、跨平台、跨终端的的广告投放平台,通过数据整合、分析实现基于受众的精准投放
+供给方平台 SSP(supply side platform):是为媒体提供服务的平台,负责管理媒体广告位库存、优化广告位的售卖,提高其广告资源价值,帮助其提升收益
+广告交易平台 ADX(ad Exchange):帮助DSP与SSP通过RTB方式进行广告交易的平台
+数据管理平台 DMP(data management platform):负责统合、加工第三方用户数据,并售卖给DSP的数据平台

+
在这里插入图片描述
在这里插入图片描述
+

定向广告(targeted advertising)
+不再把广告投给所有人,而是面向不同的受众,赋予其不同的用户标签,并投放不同的广告。由此,广告主从广告位的采买,变成了面向受众人群的采买

+

流量预测
+预估该媒体中会产生的流量总数,便于进行售前指导/出价指导,如果低估了平台流量,可能导致售卖不足,利润无法最大化;如果高估平台流量,可能导致超售,无法满足合约

+

流量塑形(traffic shaping)
+通过主动影响流量来实现合约达成,综合性门户网站是典型例子,网站通过优化导流与跳转页布局,为用户点击链接提供便利,实现最大的到达率

+

在线分配
+即通过设计分配策略,实现高效的流量分配。展示合约都面临的问题:在流量/广告位有限,而满足合约的人群大量重叠的情况下,要实现利益最大化,如何让各个合约到最大限度满足

+

广义第二高价理论:Generalized Second Price, GSP
+指在只有一个位置的拍卖中,出价最高的广告主赢得广告位后,只需要支付其下一位广告主的出价(排名第二的广告主),而不用按自己的出价来购买,这是一种最合理的竞价逻辑

+

搜索广告(search ad)
+通过用户的搜索关键字作为特征召回广告内容

+

上下文广告(contextual advertising)
+根据浏览页面的关键词进行投放,而不是用户输入的关键词

+

原生广告&信息流广告(Native advertising)
+广告内容与媒介信息形式高度吻合的广告,都可以称为原生广告,“广告即内容、内容即广告”

+]]>
+ +
+ + 程序员副业探索之电商 + http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html + http://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html + 程序员副业探索之电商 + 程序员副业探索之电商 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。 在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展... + Sun, 04 Aug 2024 14:51:49 GMT + 在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。

+

在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”文化+电商+私域正成为未来的发展趋势。结合自身的电商背景与资源,我倾向于专注于小程序电商,但对拼多多店铺和跨境电商也有兴趣。

+

探索小程序电商的过程中,我结识了许多朋友,也认识了几位从事电商运营的同事。最初,我将重心放在小程序女装项目上,计划从深圳的优质供应商中精选货品,打造一个集商品展示、购买、支付、私域于一体的完整小程序平台。然而,女装行业的高退货率远超预期,几乎每售出十件商品,就有七件被退货,这让项目难以持续。

+

因此,我开始考虑其他方向,如化妆品。化妆品作为日常消费品需求稳定且退货率较低,因而具有较高的利润。接下来,我将分享个人在化妆品电商领域的探索。

+

一、小程序化妆品

+

关于小程序,我们其实考虑过很多种不同的方向。比如,我们曾设想过做一个垂直类的平台,主要销售化妆品或者女装。我们也有其他的想法,比如销售珠宝等产品。但是,经过实际流程的运行后,我们发现小程序在很多方面存在诸多限制。下面,我将简单地介绍一下,如何创建一个小程序。

+

1.1 小程序准备(营业执照&微信支付&小程序appId)

+

首先,如果你想开发一个小程序,必须要具备微信支付的功能。而微信支付是个人无法申请的,因此需要一个营业执照。首先,你需要申请一个个人营业执照。

+

取得营业执照后,接下来就是用这个营业执照来申请开通微信支付。

+
QQ_1722269773245
QQ_1722269773245
+

接下来,你需要注册一个小程序。在去年,可以通过一些服务商或代理商注册,每个小程序的注册费用仅需几毛钱。但现在这条途径已被限制,目前注册一个小程序大约需要300元。此外,为了上线小程序,还需要通过审核,审核费用同样大约为300元。

+

同时,你还需要准备域名和服务器资源。我们选择了一台配置为8G内存的服务器,因为我们的成交量并不大,这样的配置已经足够应付日常需求。

+

1.2 小程序开发

+

在小程序开发方面,由于我们是想完全独立开发,因此首先我们从 GitHub 上查找了一个电商小程序的开源项目。尽管我们查看了很多项目,但基本上没有直接能用的,都需要进行小改造。我们对其进行了改进,接入了微信支付和腾讯的 COS 对象存储。

+

接下来,我们搭建了自己的 MySQL 数据库。考虑到我们的小程序交易量较小,因此没有使用分库分表的复杂结构,单独一个 MySQL 数据库已经足够。如果未来交易量大幅增加,可以考虑使用其他厂商的分库分表方案,例如 TiDB 等。

+

开发的框架网上的都类似一样,分为下面的几个模块,整体上感觉代码也不复杂(CRUD),就是比较耗时间。

+
    +
  • 会员系统:用户注册、登录、会员等级、积分管理等。
  • +
  • 商品管理:商品的添加、编辑、删除、库存管理、分类管理等。
  • +
  • 订单管理:订单生成、支付、取消、退换货、订单状态管理等。
  • +
  • 支付系统:支持多种支付方式,如微信支付、支付宝支付、银联支付等。
  • +
  • 营销工具:优惠券、拼团、秒杀、砍价、满减活动等。
  • +
  • 物流管理:快递公司管理、物流信息追踪等。
  • +
  • 数据分析:提供销售数据统计、用户数据分析、商品销售分析等报表功能。
  • +
  • 客服系统:在线客服、留言板、消息通知等。
  • +
  • 权限管理:用户权限角色管理,不同角色有不同的操作权限。
  • +
+
强化私域流量管理
强化私域流量管理
+

网上那些代理商有赞、微盟之类的,可以0元搞个小程序,但是如果要用到营销功能(秒杀、优惠券等),都得给几千、几万块。底成本创业,就自己找开元弄一下了,前后端都会点。在成本方面,由于个人对化妆品并不熟悉,也分不清面霜、补水之类的,因此花费了约500元请别人帮忙上传商品信息。效果如下,也可去微信小程序上搜索一下体验下(不要付款)。

+
QQ_1722270680440
QQ_1722270680440
+

下面是小程序主要的一些功能:

+

域名配置:不能通过ip使用,需要申请好域名+https才可。

+

微信支付:如上一步所说,营业执照后即可把小程序对接微信支付

+

微信物流(发货信息管理):以前网上可集成快递100的功能,现在需要收费了,好在微信官方提供了自己的微信物流,可以直接对接,免费,填写完发货信息之后微信还可以直接通知用户。

+

客服:这个很重要,需要直接对接普通用户,回答一些咨询和售后内容。

+

微信认证:这个微信强求,没办法,300块。确保商品、图片、文字等内容的合法性,避免侵权行为。遵守相关电商法律法规,例如消费者权益保护、退换货政策等。

+

尽管小程序已经上线,整体功能虽然齐全,但效果不如预期。

+

我们原本设想,当用户下单后可以从华南、北地区的供货商那里进行发货。然而,整体计算下来,由于缺乏足够的曝光,导致小程序的商品没有得到应有的推广。在微信平台,小程序的商品不会自动得到推广曝光,需要开发者自行进行推广。

+

我们在开发初期计划通过小程序结合私域流量进行引流,主要策略包括:建立小程序,通过社群引流和推广,吸引粉丝,再通过促销和秒杀等营销活动来提高订单量。然而,第一步的推广就遇到了困难。整个五月份几乎没有用户点击和访问。

+

最初立下的方向较为宏大,计划通过私域流量提升销售。然而,由于对化妆品行业了解不够深,加之市场上存在假货问题,也是在无法更多的推广。

+
QQ_1722270494718
QQ_1722270494718
+

算一下总投入吧:

+

营业执照(180)+微信支付认证(50)+小程序appId(2)+服务器(100/月)+请人录入化妆品(500),最最最重要的是,营业执照还需要年审,查税什么的,如果不用还得花钱注销。

+
QQ_1722359862581
QQ_1722359862581
+

二、拼多多电商

+

在打造自己的私域生态时,我发现仅依靠小程序并不理想,因此开始考虑其他电商平台,比如小红书和拼多多。

+

在小红书上,阿里巴巴的 1688 平台不太支持直接批量上传商品,所有商品和图片都需要手动录入和上传,非常不方便。至于选择拼多多还是淘宝,我个人更倾向于拼多多,因为其用户相对较多。

+

在选择供应商时,发现 1688 上的化妆品要么不存在,要么是一些小众品牌,无法放心销售。因此,我决定选择一个垂直方向的产品,最终专注于女性相关的饰品,比如手链和宠物项链。

+

我在 1688 上找到了一些手链店铺,并支付了 1000 元的保证金,随后通过 1688 的分销中心将产品直接分销到拼多多。然而,拼多多的定价策略需要自行调整,否则利润非常低,甚至可能亏损。

+

在开始销售时,由于没注意到新疆和西藏不包邮,导致一起订单亏损了十多元。总体来说,虽然通过广告推广获取了一些流量和曝光,并且实现了约 10 个订单,销售额为 200多 元,但最终有一半订单被退货,只盈利了大约 100 元。

+

由于退货地址填写的是我自己这边,所有退货商品都被寄回给我。检查后发现,这些产品的质量确实不佳。由于麻烦,我决定不再将退货商品寄回 1688 的商家,干脆自己留着。

+
clone
clone
+

成本:成交金额270,退单一半左右,自己承担,没退给1688商家,大概亏损130左右。

+

三、跨境电商

+

在腾讯工作期间,我听说有些人通过跨境电商取得了显著的成功,比如将电子烟卖到欧美国家,甚至在一年内赚到了一套深圳的房子。当然,这可能有些夸大。然而,国内电商市场竞争异常激烈,利润微薄,特别是拼多多对工厂的压榨,使得供应商难以生存。广州奥园的事件也是一个例子,显示了供应商在国内激烈竞争中的艰难处境。

+

我也考虑过跨境电商的可能性,比如建立独立站或在亚马逊上运营。目前,这些想法还在考察阶段,需要进一步了解和评估。

+
QQ_1722358300059
QQ_1722358300059
+

偶然听别人说:中产做生意返贫之路,就是奶茶店、跨境电商!

+

四、总结

+

小程序+拼多多店,应该亏损1000左右吧,就当做交学费,总的来说,也积累了不少经验。对整体的电商流程,包括开设门店和私域营销,都会有一定了解。但坦率地说,电商市场竞争非常激烈,利润空间被压缩得很厉害。

+

首先,以拼多多为例,它直接连接了工厂和用户(B2C),消除了很多中间环节。如果你想在这一过程中作为中间商进行营销推广,就必须具备独特的优势。现在一些平台,比如直播带货,采用的就是大批量采购的方式。这种方式确实能降低成本,并且通过批量采购获利。

+

另一方面,很多用户并不了解像阿里巴巴这样的货源平台,还有些用户甚至还在使用淘宝。利用这种信息差,还是可以赚取一定的利润,但这也让电商行业变得更加竞争激烈。

+

在电商领域的下一步发展方向上,我个人认为直播带货可能是一个更具潜力的选择。结合种草文化和私域流量管理,这种方式可能相对来说能带来更多的收益。作为中间商,目前似乎也只有这条路可以探索。可惜不是人人都能成为李佳琦或者董宇辉。

+]]>
+ +
+ + 2024-07-24 + http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html + http://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html + 2024-07-24 + 2024-07-24 新买了台air m3,午夜色的,心心念念了好久 IMG_20240723_235443 读写速度 c6c117ff1f5f6371b3ab682621369c1f 和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧 096e891a0278263d0c790e74f32ef911_720 非果粉,但好多苹果设备 0742... + Wed, 24 Jul 2024 15:52:12 GMT + 新买了台air m3,午夜色的,心心念念了好久

+IMG_20240723_235443 +

读写速度

+c6c117ff1f5f6371b3ab682621369c1f +

和以前的m1 pro对比了下,感觉差了半截,使用上没有区别吧

+096e891a0278263d0c790e74f32ef911_720 +

非果粉,但好多苹果设备

+0742e82ab0ba499dea8b267f958cdada +]]>
+
+ + 股票预测 + http://www.wenzhihuai.com/stock/ + http://www.wenzhihuai.com/stock/ + 股票预测 + 股票预测 + Tue, 02 Jul 2024 14:33:09 GMT + + + +]]> + + + 赛力斯 + http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html + http://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html + 赛力斯 + 赛力斯 update + Tue, 02 Jul 2024 14:33:09 GMT + update

+]]>
+
+ + 好玩的 + http://www.wenzhihuai.com/interesting/ + http://www.wenzhihuai.com/interesting/ + 好玩的 + 好玩的 + Sat, 18 May 2024 19:54:10 GMT + + + + 基于OLAP做业务监控 + http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html + http://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html + 基于OLAP做业务监控 + 基于OLAP做业务监控 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 interval 和 for 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。 一、两者的... + Mon, 13 May 2024 16:26:06 GMT + 在监控领域,Prometheus 与 Grafana 常被认为是无敌组合。然而,随着成本的增加,不少人开始审慎考虑其他替代方案。本文将深入探讨 Prometheus 的特点,并讨论 Grafana 中关键参数 intervalfor 的意义及其影响。此外,还将分析 OLAP 数据库作为监控数据持久性存储的优势。

+

一、两者的比较

+

Prometheus 的特点
+Prometheus 是一种开源的监控和警报工具包,最初由 SoundCloud 开发并开源。它具有以下特点:
+(1)多维度数据模型
+Prometheus 采用标签(label)系统,允许用户通过灵活的标签组合来区分和查询监控数据。
+(2)Pull 模式采集
+Prometheus 使用 Pull 模式从各种目标(如应用程序、数据库、操作系统等)拉取数据,这与其他使用 Push 模式的监控系统有所不同。
+(3)警报和告警管理
+Prometheus 内置了一个强大的警报管理系统,可以根据预定条件触发警报,并集成到多种通知平台(如邮件、Slack 等)。
+(4)生态系统和可扩展性
+Prometheus 拥有活跃的社区和广泛的插件支持,可以轻松与其他系统集成,并根据需要进行扩展。

+

OLAP 数据库

+

OLAP(Online Analytical Processing)数据库在监控数据存储和分析中具有独特的优势,尤其在数据持久性和实时查询方面。

+

(1)数据持久性和长期存储
+OLAP 数据库通常具有可靠的数据持久性和长期存储能力,能存储大量历史数据,并支持快速的时间范围查询和数据回溯分析。
+(2)实时数据加载和查询
+现代 OLAP 数据库尽管通常不支持实时数据加载,但一些具有较低的延迟和高吞吐量,可以支持实时查询和分析需求。

+

二、为什么要用OLAP数据库来做监控

+
1
1
+
2
2
+
3
3
+]]>
+ +
+ + Jetbrains Idea设置 + http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html + http://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html + Jetbrains Idea设置 + Jetbrains Idea设置 一、类注释 想要在生成类的时候,自动带上author和时间,效果如下: 放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他: image-20240512002500812image-20240512002500812 二、方法注释 想让自动给方法生成注释,以及参数的默认... + Sat, 11 May 2024 16:37:27 GMT + 一、类注释 +

想要在生成类的时候,自动带上author和时间,效果如下:

+
/**
+ * @author zhihuaiwen
+ * @since 2024/5/12 00:22
+ */
+public class EFE {
+}
+

放到File Header里统一下即可,当然,也可以去Files里定义Class File或者其他:

+
image-20240512002500812
image-20240512002500812
+

二、方法注释

+

想让自动给方法生成注释,以及参数的默认注释:

+
  /**
+   * 获取首页商品列表
+   *
+   * @param type             类型 【1 精品推荐 2 热门榜单 3首发新品 4促销单品】
+   * @param pageParamRequest 分页参数
+   * @return List
+   */
+  @Override
+  public CommonPage<IndexProductResponse> findIndexProductList(Integer type, PageParamRequest pageParamRequest) {
+

去settings里面找到Live Templates,新建一个Template Group叫做user,然后再新建一个Live Template。

+
image-20240512001636936
image-20240512001636936
+

Abbreviation填*,Description随便填,然后再Template text填下:

+
*
+$VAR1$
+ * @return $returns$
+ */
+

编辑变量,Edit Variables:

+
image-20240512001711420.png
image-20240512001711420.png
+

VAR1:

+
groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+=' * @param ' + params[i] + ' ' + params[i] + ((i < params.size() - 1) ? '\\n' : '')}; return result", methodParameters())
+

returns:

+
methodReturnType()
+

三、关闭Annotate With Git Blame

+

Idea这个太烦了,总是不小心误点,想把右侧的两个都给去掉,网上查了有些比较旧,无法解决,下面特别声明下:

+
image-20240512001050312
image-20240512001050312
+

位置:settings直接搜Inlay Hints,把右侧的Usages、Code author给取消勾选即可

+
image-20240512001010797
image-20240512001010797
+

上面的设置放到百度网盘里了,可以下载后导入到Idea

+

链接: https://pan.baidu.com/s/1-3uhY1jssyMWyRbvBLQ2FQ?pwd=epw2 提取码: epw2 复制这段内容后打开百度网盘手机App,操作更方便哦

+image-20240512115716124 +]]>
+ +
+ + 小程序反编译 + http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html + http://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html + 小程序反编译 + 小程序反编译 第一步:电脑端提取 先找到小程序保存的地址,一般先找到微信的文件管理下 image-20240310171919281 然后到该目录的C:\Users\w1570\Documents\WeChat Files\Applet即可看到相关小程序了。 image-20240310173802224image-20240310173802224 ... + Sun, 10 Mar 2024 14:39:30 GMT + 第一步:电脑端提取 +

先找到小程序保存的地址,一般先找到微信的文件管理下

+image-20240310171919281 +

然后到该目录的C:\Users\w1570\Documents\WeChat Files\Applet即可看到相关小程序了。

+
image-20240310173802224
image-20240310173802224
+

找到需要解压的包__APP__.wxapkg,拷贝到和pc_wxapkg_decrypt.exe的统一路径下。

+

第二步:解密wxapkg包

+ +
pc_wxapkg_decrypt.exe -wxid 微信小程序id -in 要解密的wxapkg路径 -out 解密后的路径
+//示例如下
+pc_wxapkg_decrypt.exe -wxid wx7444167f2a6427b3 -in __APP__.wxapkg
+

第三步:解包

+ +
切换到./nodejs目录下,解压`node_modules.zip`后,使用cmd命令打开
+输入下面命令
+
+node wuWxapkg.js ..\decrypt\dec.wxapkg
+
+第二个参数为操作的项目,这里操作的是666.wxapkg 记得改为自己的
+

发现偶尔还是有些html没有正常解析,有点奇怪

+]]>
+ +
+ + 开发工具整理 + http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html + http://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html + 开发工具整理 + 开发工具整理 ShadowsocksX mac端:https://github.com/shadowsocks/ShadowsocksX-NG win端:https://github.com/shadowsocks/shadowsocks-windows 安卓:https://github.com/shadowsocks/shadowsocks-and... + Sun, 10 Mar 2024 14:39:30 GMT + ShadowsocksX +

mac端:https://github.com/shadowsocks/ShadowsocksX-NG
+win端:https://github.com/shadowsocks/shadowsocks-windows
+安卓:https://github.com/shadowsocks/shadowsocks-android

+

IDEA激活

+

许可证激活(License server)
+http://vip.52shizhan.cn
+http://fls.itxe.net

+

+]]>
+
+ + 高可用 + http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html + http://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html + 高可用 + 高可用 4个9(99.99) 流量控制 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,... + Sun, 10 Mar 2024 14:39:30 GMT + 4个9(99.99)

+

| | Sentinel | Hystrix(维护状态) | Resilience4j(Spring推荐) |
+|

+]]>
+ +
+ + 高并发 + http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html + http://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html + 高并发 + 高并发 QPS 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。 响应时间:系统对请求做出... + Sun, 10 Mar 2024 14:39:30 GMT + QPS

+

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

+
    +
  • 响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
  • +
  • 吞吐量:单位时间内处理的请求数量。
  • +
  • QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • +
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
  • +
+

负载均衡

+

正所谓双拳难敌四手,高并发撑场面的首选方案就是集群化部署,一台服务器承载的QPS有限,多台服务器叠加效果就不一样了。

+

如何将流量转发到服务器集群,这里面就要用到负载均衡,比如:LVS 和 Nginx。

+

常用的负载算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接数法等

+

业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。

+

注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx

+

池化技术

+

复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,有时间开销浪费。池化技术的核心是资源的“预分配”和“循环使用”,常用的池化技术有线程池、进程池、对象池、内存池、连接池、协程池。

+

连接池的几个重要参数:最小连接数、空闲连接数、最大连接数

+

Linux 内核中是以进程为单元来调度资源的,线程也是轻量级进程。所以说,进程、线程都是由内核来创建并调度。协程是由应用程序创建出来的任务执行单元,比如 Go 语言中的协程“goroutine”。协程本身是运行在线程上,由应用程序自己调度,它是比线程更轻量的执行单元。

+

在 Go 语言中,一个协程初始内存空间是 2KB(Linux 下线程栈大小默认是 8MB),相比线程和进程来说要小很多。协程的创建和销毁完全是在用户态执行的,不涉及用户态和内核态的切换。另外,协程完全由应用程序在用户态下调用,不涉及内核态的上下文切换。协程切换时由于不需要处理线程状态,需要保存的上下文也很少,速度很快。

+

Go语言中协程池的实现方法有两种:抢占式和调度式。

+

抢占式协程池,所有任务存放到一个共享的 channel 中,多个协程同时去消费 channel 中的任务,存在锁竞争。
+调度式协程池,每个协程都有自己的 channel,每个协程只消费自己的 channel。下发任务的时候,采用负载均衡算法选择合适的协程来执行任务。比如选择排队中任务最少的协程,或者简单轮询。

+

流量漏斗

+

上面讲的是正向方式提升系统QPS,我们也可以逆向思维,做减法,拦截非法请求,将核心能力留给正常业务!

+

互联网高并发流量并不都是纯净的,也有很多恶意流量(比如黑客攻击、恶意爬虫、黄牛、秒杀器等),我们需要设计流量拦截器,将那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。

+

拦截器分层:

+

网关和 WAF(Web Application Firewall,Web 应用防火墙)
+采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法

+

风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用。
+下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。

+]]>
+
+ + 高性能 + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html + 高性能 + 高性能 响应时间 性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。 那么有哪些因素会影响系统的性能呢? 用户网络环境 请求/响应的数据包大小 业务系统 CPU、内存、磁盘等性能 业务链路的长度 下游系统的性能 算法实现是否高效 当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。 1、高性能缓存 对一些热点... + Sun, 10 Mar 2024 14:39:30 GMT + 响应时间

+

性能直接影响用户的感官体验,访问一个系统,如果超过5秒没有响应,绝大数用户会选择离开。

+

那么有哪些因素会影响系统的性能呢?

+

用户网络环境
+请求/响应的数据包大小
+业务系统 CPU、内存、磁盘等性能
+业务链路的长度
+下游系统的性能
+算法实现是否高效
+当然,随着并发数的提升,系统压力增大,平均请求延迟也会增大。

+

1、高性能缓存

+

对一些热点数据每次都从 DB 中读取,会给 DB 带来较大的压力,导致性能大幅下降。所以,我们需要用缓存来提升热点数据的访问性能,比如将活动信息数据在浏览器的缓存中保存一段时间。

+

缓存根据性能由高到低分为:寄存器、L1缓存、L2缓存、L3缓存、本地内存、分布式缓存

+

上层的寄存器、L1 缓存、L2 缓存是位于 CPU 核内的高速缓存,访问延迟通常在 10 纳秒以下。L3 缓存是位于 CPU 核外部但在芯片内部的共享高速缓存,访问延迟通常在十纳秒左右。高速缓存具有成本高、容量小的特点,容量最大的 L3 缓存通常也只有几十MB。

+

本地内存是计算机内的主存储器,相比 CPU 芯片内部的高速缓存,内存的成本要低很多,容量通常是 GB 级别,访问延迟通常在几十到几百纳秒。

+

内存和高速缓存都属于掉电易失的存储器,如果机器断电了,这类存储器中的数据就丢失了。

+

特别说明:在使用缓存时,要注意缓存穿透、缓存雪崩、缓存热点问题、缓存数据一致性问题。当然为了提升整体性能通常会采用多级缓存组合方案(浏览器缓存+服务端本地内存缓存+服务端网络内存缓存)

+

2、日志优化,避免IO瓶颈

+

当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 “iowait”。

+

在IO中断过程中,如果此时有其他任务线程可调度,系统会直接调度其他线程,这样 CPU 就相应显示为 Usr 或 Sys;但是如果此时系统较空闲,无其他任务可以调度,CPU 就会显示为 iowait(实际上与 idle 无本质区别)。

+

磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住。

+

Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除。

+

这种批量化、顺序写,大大提升了磁盘的吞吐性能!

+

缓存

+

异步

+

I/O(网络、数据、文件)

+

分库分表

+

参考

+

1.怎么优化java项目性能

+]]>
+
+ + Feed系统设计 + http://www.wenzhihuai.com/system-design/feed.html + http://www.wenzhihuai.com/system-design/feed.html + Feed系统设计 + Feed系统设计 https://blog.csdn.net/weixin_45583158/article/details/128195940 + Sun, 10 Mar 2024 14:39:30 GMT + https://blog.csdn.net/weixin_45583158/article/details/128195940

+]]>
+
+ + 行锁,表锁,意向锁 + http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html + http://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html + 行锁,表锁,意向锁 + 行锁,表锁,意向锁 + Sun, 10 Mar 2024 14:39:30 GMT + + + + 一致性hash算法和哈希槽 + http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html + http://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html + 一致性hash算法和哈希槽 + 一致性hash算法和哈希槽 + Sun, 10 Mar 2024 14:39:30 GMT + + + + JVM(java虚拟机) + http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html + http://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html + JVM(java虚拟机) + 一、了解JVM 1、什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码... + Sun, 10 Mar 2024 14:39:30 GMT + 一、了解JVM +

1、什么是JVM

+

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

+
微信图片_20220522232432.png
微信图片_20220522232432.png
+

2、JRE/JDK/JVM是什么关系

+

JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
+JDK(Java Development Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
+JVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。

+

3、JVM体系结构:

+

类加载器:加载class文件;
+运行时数据区:包括方法区、堆、Java栈、程序计数器、本地方法栈
+执行引擎:执行字节码或者执行本地方法

+
img
img
+

二、运行时数据区

+

方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。从 JDK 8 开始,HotSpot 虚拟机移除了方法区,取而代之的是元空间(Metaspace)。元空间并不在 Java 虚拟机内存中,而是使用了本地(即操作系统)的内存。这个改变主要是为了解决方法区可能出现的内存溢出问题。
+:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。
+:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
+程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
+本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。

+

元空间

+

三、类加载机制

+

类加载器通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。

+

1、类的生命周期(7个)

+

加载、验证、准备、解析、初始化、使用、卸载

+

2、类加载的五个过程

+

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

+

加载:类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
+验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
+准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
+解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
+初始化:前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

+

3、类加载器

+

启动类加载器(BootStrap ClassLoader):主要负责加载jre/lib/rt.jar相关的字节码文件的。
+扩展类加载器(Extension Class Loader):主要负载加载 jre/lib/ext/*.jar 这些jar包的。
+应用程序类加载器(Application Class Loader):主要负责加载用户自定义的类以及classpath环境变量所配置的jar包的。
+自定义类加载器(User Class Loader):负责加载程序员指定的特殊目录下的字节码文件的。大多数情况下,自定义类加载器只需要继承ClassLoader这个抽象类,重写findClass()和loadClass()两个方法即可。

+

4、类加载机制(双亲委派)

+

类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。

+
img
img
+

工作过程:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。

+

四、垃圾回收

+

程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。 Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

+

1、判断对象已死

+

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

+

引用计数法:给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;

+
img
img
+
    注意:如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的            两块内存依然保持着互相引用无法回收。引用计数法很难解决循环引用问题; 
+
+

可达性分析:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

+
img
img
+
    可作为 GC Roots 的对象:
+    1)虚拟机栈中引用的对象
+    2)方法区中类静态属性引用的对象
+    3)方法区中常量引用的对象
+    4)本地方法栈中native方法引用的对象 
+
+

引用:下面四种引用强度依次减弱
+强引用:默认情况下,对象采用的均为强引用;
+软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
+弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
+虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

+

2、垃圾收集算法

+

标记清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
+复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
+标记整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
+分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记清除算法。

+

3、垃圾收集器

+

Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
+Parnew收集器:Serial收集器多线程版本。
+Parallel Scavenge收集器:使用复制算法的新生代收集器。
+Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
+Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
+CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
+G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。
+G1收集器有以下特点
+1)并行与并发:无需停顿Java线程来执行GC动作。
+2)分代收集:可独立管理整个GC堆。
+3)空间整合:运行期间不会产生内存空间碎片。
+4)可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

+

4、JVM内存分代机制

+

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。

+

新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。
+老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
+永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
+元空间(metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。
+GC 过程:新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区 没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于 Eden区和Survivor From区,Survivor To区是空的(作为保留区域)。GC进行时,Eden区中 所有存活的对象都会被复制到Survivor To区,而在Survivor From区中,仍存活的对象会根据 它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾 回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有 达到阀值的对象会被复制到Survivor To区。接着清空Eden区和Survivor From区,新生代中存 活的对象都在Survivor To区。接着,Survivor From区和Survivor To区会交换它们的角色,也 就是新的Survivor To区就是上次GC清空的Survivor From区,新的Survivor From区就是上次 GC的Survivor To区,总之,不管怎样都会保证Survivor To区在一轮GC后是空的。GC时当 Survivor To区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进 行分配担保,将这些对象存放在老年代中。

+

5、Minor GC、Major GC、Full GC之间的区别

+

Minor GC:Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
+Major GC:指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
+Full GC:Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

+

6、Minor GC、Major GC、Full GC触发条件

+

Minor GC触发条件
+虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总 空间
+1)如果大于的话,直接执行minorGC
+2)如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
+3)如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历 次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC
+4)如果大于的话,执行minorGC

+

Full GC触发条件
+1)老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存 在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是 不要创建太大的对象。
+2)方法区空间不足:系统当中需要加载的类,调用的方法很多,同时方法区当中没有足够的 空间,就出触发一次Full GC
+3)老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
+4)调用System.gc()时(系统建议执行Full GC,但是不必然执行)

+]]>
+ +
+ + JAVA NIO + http://www.wenzhihuai.com/java/io/nio.html + http://www.wenzhihuai.com/java/io/nio.html + JAVA NIO + JAVA NIO 一、简介 1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路... + Sun, 10 Mar 2024 14:39:30 GMT + 一、简介 +

1)Java BIO : 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

+

2)Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

+

3)Java AIO:异步非阻塞,在AIO模型中,当我们发起一个I/O操作(如读或写)时,我们不需要等待它完成,我们的代码会立即返回,可以继续执行其他任务。当I/O操作完成时,我们之前注册的回调函数会被自动调用,我们可以在这个回调函数中处理I/O操作的结果。

+

1.1典型的多路复用IO实现

+

目前流程的多路复用IO实现主要包括四种: selectpollepollkqueue。下表是他们的一些重要特性的比较:

+

| IO模型 | 相对性能 | 数据结构 | 关键思路 | 操作系统 | JAVA支持情况 |
+|

+]]>
+
+ + Spring Cloud Sentinel + http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html + http://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html + Spring Cloud Sentinel + Spring Cloud Sentinel 参考 1.超详细springcloud sentinel教程~ + Sun, 10 Mar 2024 14:39:30 GMT + 参考 +

1.超详细springcloud sentinel教程~

+]]>
+
+ + 入门使用 + http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html + http://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html + 入门使用 + 入门使用 参考 1.Sentinel入门(干货版) + Sun, 10 Mar 2024 14:39:30 GMT + 参考 +

1.Sentinel入门(干货版)

+]]>
+
+ + 基础 + http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html + http://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html + 基础 + 基础 一、ElasticSearch基础 1、什么是Elasticsearch: Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。 全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并... + Sat, 24 Feb 2024 15:02:58 GMT + 一、ElasticSearch基础 +

1、什么是Elasticsearch:
+Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
+全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

+

2、Elasticsearch 的基本概念:

+

(1)index 索引:索引类似于mysql 中的数据库,Elasticesearch 中的索引是存在数据的地方,包含了一堆有相似结构的文档数据。

+

(2)type 类型:类型是用来定义数据结构,可以认为是 mysql 中的一张表,type 是 index 中的一个逻辑数据分类

+

(3)document 文档:类似于 MySQL 中的一行,不同之处在于 ES 中的每个文档可以有不同的字段,但是对于通用字段应该具有相同的数据类型,文档是es中的最小数据单元,可以认为一个文档就是一条记录。

+

(4)Field 字段:Field是Elasticsearch的最小单位,一个document里面有多个field

+

(5)shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

+

(6)replica 副本:任何服务器随时可能故障或宕机,此时 shard 可能会丢失,通过创建 replica 副本,可以在 shard 故障时提供备用服务,保证数据不丢失,另外 replica 还可以提升搜索操作的吞吐量。

+

shard 分片数量在建立索引时设置,设置后不能修改,默认5个;replica 副本数量默认1个,可随时修改数量;

+

(7)segment:每个shard分片是一个lucene实例,每个分片由多个segment组成!!每个segment占用内存,文件句柄等。segment 合并的过程,需要先读取小的 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,合并大的 segment 需要消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响。所以 Elasticsearch 在默认情况下会对合并线程进行资源限制,确保它不会对搜索性能造成太大影响。

+
image-20240224213112456
image-20240224213112456
+

3、什么是倒排索引:
+在搜索引擎中,每个文档都有对应的文档 ID,文档内容可以表示为一系列关键词的集合,例如,某个文档经过分词,提取了 20 个关键词,而通过倒排索引,可以记录每个关键词在文档中出现的次数和出现位置。也就是说,倒排索引是 关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了该关键词。

+

要注意倒排索引的两个细节:

+

倒排索引中的所有词项对应一个或多个文档
+倒排索引中的词项 根据字典顺序升序排列

+
img
img
+

4、doc_values 的作用:
+倒排索引虽然可以提高搜索性能,但也存在缺陷,比如我们需要对数据做排序或聚合等操作时,lucene 会提取所有出现在文档集合的排序字段,然后构建一个排好序的文档集合,而这个步骤是基于内存的,如果排序数据量巨大的话,容易造成内存溢出和性能缓慢。
+doc_values 就是 es 在构建倒排索引的同时,会对开启 doc_values 的字段构建一个有序的 “document文档 ==> field value” 的列式存储映射,可以看作是以文档维度,实现了根据指定字段进行排序和聚合的功能,降低对内存的依赖。另外 doc_values 保存在操作系统的磁盘中,当 doc_values 大于节点的可用内存,ES可以从操作系统页缓存中加载或弹出,从而避免发生内存溢出的异常,但如果 docValues 远小于节点的可用内存,操作系统就自然将所有 doc_values 存于内存中(堆外内存),有助于快速访问。

+

5、text 和 keyword类型的区别:
+两个类型的区别主要是分词:keyword 类型是不会分词的,直接根据字符串内容建立倒排索引,所以keyword类型的字段只能通过精确值搜索到;Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立倒排索引

+

6、query 和 filter 的区别?

+

(1)query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;

+

(2)filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。

+

二、ES的写入流程

+

1、ES写数据的整体流程:

+
image-20240223165512655
image-20240223165512655
+

(1)客户端选择 ES 的某个 node 发送请求过去,这个 node 就是协调节点 coordinating node
+(2)coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)
+(3)实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node
+(4)coordinating node 等到 primary node 和所有 replica node 都执行成功之后,最后返回响应结果给客户端。
+2、ES主分片写数据的详细流程:

+
Elasticsearch索引文档的过程
Elasticsearch索引文档的过程
+

(1)主分片先将数据写入ES的 memory buffer,然后定时(默认1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入操作系统缓存 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 segment 文件实际上是一些倒排索引的集合, 只有经历了 refresh 操作之后,这些数据才能变成可检索的。

+

ES 的近实时性:数据存在 memory buffer 时是搜索不到的,只有数据被 refresh 到 Filesystem cache 之后才能被搜索到,而 refresh 是每秒一次, 所以称 es 是近实时的;可以手动调用 es 的 api 触发一次 refresh 操作,让数据马上可以被搜索到;

+

(2)由于 memory Buffer 和 Filesystem Cache 都是基于内存,假设服务器宕机,那么数据就会丢失,所以 ES 通过 translog 日志文件来保证数据的可靠性,在数据写入 memory buffer 的同时,将数据也写入 translog 日志文件中,当机器宕机重启时,es 会自动读取 translog 日志文件中的数据,恢复到 memory buffer 和 Filesystem cache 中去。

+

ES 数据丢失的问题:translog 也是先写入 Filesystem cache,然后默认每隔 5 秒刷一次到磁盘中,所以默认情况下,可能有 5 秒的数据会仅仅停留在 memory buffer 或者 translog 文件的 Filesystem cache中,而不在磁盘上,如果此时机器宕机,会丢失 5 秒钟的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

+

(3)flush 操作:不断重复上面的步骤,translog 会变得越来越大,不过 translog 文件默认每30分钟或者 阈值超过 512M 时,就会触发 commit 操作,即 flush操作,将 memory buffer 中所有的数据写入新的 segment 文件中, 并将内存中所有的 segment 文件全部落盘,最后清空 translog 事务日志。

+

① 将 memory buffer 中的数据 refresh 到 Filesystem Cache 中去,清空 buffer;
+② 创建一个新的 commit point(提交点),同时强行将 Filesystem Cache 中目前所有的数据都 fsync 到磁盘文件中;
+③ 删除旧的 translog 日志文件并创建一个新的 translog 日志文件,此时 commit 操作完成
+更多 ES 的数据写入流程的说明欢迎阅读这篇文章:ElasticSearch搜索引擎:数据的写入流程

+

三、ES的更新和删除流程

+

​ 删除和更新都是写操作,但是由于 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;所以 ES 利用 .del 文件 标记文档是否被删除,磁盘上的每个段都有一个相应的.del 文件

+

(1)如果是删除操作,文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉。

+

(2)如果是更新操作,就是将旧的 doc 标识为 deleted 状态,然后创建一个新的 doc。

+

memory buffer 每 refresh 一次,就会产生一个 segment 文件 ,所以默认情况下是 1s 生成一个 segment 文件,这样下来 segment 文件会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment 文件合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,不写入到新的 segment 中,然后将新的 segment 文件写入磁盘,这里会写一个 commit point ,标识所有新的 segment 文件,然后打开 segment 文件供搜索使用,同时删除旧的 segment 文件

+

有关segment段合并过程,欢迎阅读这篇文章:Elasticsearch搜索引擎:ES的segment段合并原理

+

四、ES的搜索流程

+

搜索被执行成一个两阶段过程,即 Query Then Fetch:

+

1、Query阶段:

+

客户端发送请求到 coordinate node,协调节点将搜索请求广播到所有的 primary shard 或 replica,每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。接着每个分片返回各自优先队列中 所有 docId 和 打分值 给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

+

2、Fetch阶段:
+协调节点根据 Query阶段产生的结果,去各个节点上查询 docId 实际的 document 内容,最后由协调节点返回结果给客户端。

+

coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
+接收请求的 node 返回 document 给 coordinate node 。
+coordinate node 返回 document 给客户端。
+Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。

+

五、ES在高并发下如何保证读写一致性?

+

(1)对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖

+

每个文档都有一个_version 版本号,这个版本号在文档被改变时加一。Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,它会被简单的忽略。

+

利用_version的这一优点确保数据不会因为修改冲突而丢失,比如指定文档的version来做更改,如果那个版本号不是现在的,我们的请求就失败了。

+

(2)对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。

+

one:写操作只要有一个primary shard是active活跃可用的,就可以执行
+all:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
+quorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
+(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。

+

六、ES集群如何选举Master节点

+

1、Elasticsearch 的分布式原理:

+

Elasticsearch 会对存储的数据进行切分,划分到不同的分片上,同时每一个分片会生成多个副本,从而保证分布式环境的高可用。ES集群中的节点是对等的,节点间会选出集群的 Master,由 Master 会负责维护集群状态信息,并同步给其他节点。

+

Elasticsearch 的性能会不会很低:不会,ES只有建立 index 和 type 时需要经过 Master,而数据的写入有一个简单的 Routing 规则,可以路由到集群中的任意节点,所以数据写入压力是分散在整个集群的。

+

2、ES集群 如何 选举 Master:

+

Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;

+

(1)确认候选主节点的最少投票通过数量(elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes)
+(2)选举时,集群中每个节点对所有 master候选节点(node.master: true)根据 nodeId 进行字典排序,然后选出第一个节点(第0位),暂且认为它是master节点。
+(3)如果对某个节点的投票数达到阈值,并且该节点自己也选举自己,那这个节点就是master;否则重新选举一直到满足上述条件。
+补充:master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。

+

3、Elasticsearch是如何避免脑裂现象:

+

(1)当集群中 master 候选节点数量不小于3个时(node.master: true),可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes),设置超过所有候选节点一半以上来解决脑裂问题,即设置为 (N/2)+1;

+

(2)当集群 master 候选节点 只有两个时,这种情况是不合理的,最好把另外一个node.master改成false。如果我们不改节点设置,还是套上面的(N/2)+1公式,此时discovery.zen.minimum_master_nodes应该设置为2。这就出现一个问题,两个master备选节点,只要有一个挂,就选不出master了

+

七、建立索引阶段性能提升方法

+

(1)如果是大批量导入,可以设置 index.number_of_replicas: 0 关闭副本,等数据导入完成之后再开启副本
+(2)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
+(3)如果搜索结果不需要近实时性,可以把每个索引的 index.refresh_interval 改到30s
+(4)增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB
+(5)使用 SSD 存储介质
+(6)段和合并:Elasticsearch 默认值是 20 MB/s。但如果用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。

+

八、ES的深度分页与滚动搜索scroll

+

(1)深度分页:

+

深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深。

+

(2)滚动搜索:
+一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。

+

参考

+

1.ElasticSearch搜索引擎
+2.2023最新整理的 Elasticsearch 21道面试题
+3.elasticsearch中文官网
+4.elasticsearch 倒排索引原理

+]]>
+ +
+ + 一些不常用但很实用的命令 + http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html + http://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html + 一些不常用但很实用的命令 + 一些不常用但很实用的命令 https连接耗时检测 iptables拒绝请求 + Sat, 24 Feb 2024 05:31:47 GMT + https连接耗时检测 +
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\n" -so /dev/null https://zhls.qq.com/test-nginx
+

iptables拒绝请求

+
iptables -A  OUTPUT -d 11.145.18.159 -j REJECT
+
]]>
+
+ + Jenkins的一些笔记 + http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html + http://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html + Jenkins的一些笔记 + Jenkins的一些笔记 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。 一、在全局安全配置中 1.1 启用安全 如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直... + Sat, 24 Feb 2024 05:12:56 GMT + 公司主要要开发自己的paas平台,集成了Jenkins,真的是遇到了很多很多困难,特别是在api调用的权限这一块,这里,把自己遇到的一些坑的解决方法做一下笔记吧。当然,首先要讲的,就是如何在开启安全的情况下进行API调用。

+

一、在全局安全配置中

+

1.1 启用安全

+

如果勾选不对,那么Jenkins有可能崩溃掉,亲身经历,之前一直没有勾选安全域,然后授权策略为登录用户可以做任何事,之后权限这一块就彻底崩溃了,重装了又重装,才知道,需要勾选安全域。

+
+
+
+

1.2 跨域

+

同时开启跨站请求伪造保护,Jenkins的一些API需要用到的。

+
+
+
+

二、获取TOKEN

+

2.1 TOKEN

+

Jenkins的用户token可以在用户的设置下面获得,但是这种方式如果需要重装Jenkins的话,就得重新修改一次配置文件

+
+
+
+

经过对Jenkins-client的抓包分析,token可以由username+":"+password,然后进行base64加密组成,之后在token前面加上"Basic "即可,代码如下:

+
+
+
+

三、获取Jenkins-Crumb

+

在远程API调用的时候,Jenkins对于某些接口的要求不仅限于Authorization,还必须要有Jenkins-Crumb,这个东西之前在进行获取的时候,有时候会变来变去,比如用curl命令和f12查看的时候发现不一致,实在受不了,感觉毫无规律可言,之后才发现上面的Authorization来直接调用接口获取的才是正确的,再然后想想,可能是之前调用api的时候,没有开启启用安全,再或者是有没有勾选上使用碎片算法。

+
+
+
+

另,附上curl查询Jenkins-Crumb的命令:

+
curl -s 'http://admin:yourtoken@jenkins-url/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+

替换掉yourtoken和jenkins-url即可。

+

四、值得注意的事

+

4.1 API设计

+

Jenkins的API设计可谓是独领风骚,能把一个提交设计成这样真实佩服测试之后才发现只要提交个表单,key为json,value为值即可,其他的都不需要,这个设计我也不知道怎么来的,感觉超级坑。

+
+
+
+

4.2 生成构建job

+

由于我们是将Jenkins集成在我们自己的平台里面,并不暴露Jenkins给用户,所以,创建一个job的时候,必须由我们平台的参数往Jenkins里面提交,这一提交,发现的问题不少。
+一是Jenkins的整个job的提交是由两步组成的,先是创建job,再提交配置。即:/createItem?name=xxx接口。
+二是提交的配置参数,提交的是整个xml,而不是由一个一个参数组成的。对于java来说,就得使用xstream或者其他来转化,甚是折腾,如图这种转化。

+
+
+
+

4.3 构建的队列

+

在点击立即构建的时候,Jenkins是没有返回任何信息,但是在Jenkins的内部,它是通过放到队列里等待的,如果有空闲,就开始构建,否则等待,这个队列是可以获取得到的,我们从里面可以获取上一次构建的信息,是成功还是失败。这种情况下,假设我们多个人同时点击,这下子就有点慌了,如何获取到具体某个人的构建结果,有点虐心。想了半天,最终得出的事:代码相同,意味着每次构建的结果相同,为什么要允许多个人同时点击?就这么解决了:从一个job的构建队列中获取最后一次构建的信息,如果是正在构建,那么不允许构建了,直到构建结果出来。

+

4.4 构建进度的查看

+

需要将Jenkins中的构建进度移植到我们自有的平台,Jenkins的构建进度时通过ajax轮询实现的,获取文本的规则主要从response header里面的两个字段获取
+(1)X-More-Data:是否有更多的数据
+(2)X-Text-Size:从开始到该次调用的文本大小
+我们是通过websocket来将文本内容推送到前端,使用的stomp协议,部分代码如下:

+
        while (true) {
+            ...
+            String string = response.body().string();
+            String header = response.header("X-More-Data");
+            if (!Strings.isNullOrEmpty(header) || start == 0) {
+                template.convertAndSend("/topic/" + uuid, string);
+                String textSize = response.header("X-Text-Size");
+                if (!Strings.isNullOrEmpty(textSize)) {
+                    start = Integer.parseInt(textSize);
+                }
+                TimeUnit.SECONDS.sleep(5);
+            } else {
+                template.convertAndSend("/topic/" + uuid, string);
+                return;
+            }
+        }
+

参考:
+1.通过jenkins API去build一个job
+2.Jenkins Remote API

+]]>
+ +
+ + Kubernetes容器日志收集 + http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html + http://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html + Kubernetes容器日志收集 + Kubernetes容器日志收集 日志采集方式 日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。 1.原生方式 使用 kubectl ... + Sat, 24 Feb 2024 05:12:56 GMT + 日志采集方式 +

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

+

1.原生方式 使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。
+2.DaemonSet方式 在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。
+3.Sidecar方式 一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。
+三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。

+

一、原生方式

+


+简单的说,原生方式就是直接使用kubectl logs来查看日志,或者将docker的日志通过日志驱动来打到syslog、journal等去,然后再通过命令来排查,这种方式最好的优势就是简单、资源占用率低等,但是,在多容器、弹性伸缩情况下,日志的排查会十分困难,仅仅适用于刚开始研究Kubernetes的公司吧。不过,原生方式确实其他两种方式的基础,因为它的两种最基础的理念,daemonset和sidecar模式都是基于这两种方式而来的。

+

1.1 控制台stdout方式

+

这种方式是daemonset方式的基础。将日志全部输出到控制台,然后docker开启journal,然后就能在/var/log/journal下面看到二进制的journal日志,如果要查看二进制的日志的话,可以使用journalctl来查看日志:journalctl -u docker.service -n 1 --no-pager -o json -o json-pretty

+
{
+        "__CURSOR" : "s=113d7df2f5ff4d0985b08222b365c27a;i=1a5744e3;b=05e0fdf6d1814557939e52c0ac7ea76c;m=5cffae4cd4;t=58a452ca82da8;x=29bef852bcd70ae2",
+        "__REALTIME_TIMESTAMP" : "1559404590149032",
+        "__MONOTONIC_TIMESTAMP" : "399426604244",
+        "_BOOT_ID" : "05e0fdf6d1814557939e52c0ac7ea76c",
+        "PRIORITY" : "6",
+        "CONTAINER_ID_FULL" : "f2108df841b1f72684713998c976db72665f353a3b4ea17cd06b5fc5f0b8ae27",
+        "CONTAINER_NAME" : "k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1",
+        "CONTAINER_TAG" : "f2108df841b1",
+        "CONTAINER_ID" : "f2108df841b1",
+        "_TRANSPORT" : "journal",
+        "_PID" : "6418",
+        "_UID" : "0",
+        "_GID" : "0",
+        "_COMM" : "dockerd-current",
+        "_EXE" : "/usr/bin/dockerd-current",
+        "_CMDLINE" : "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry hub.gcloud.lab --insecure-registry 172.30.0.0/16 --log-level=warn --signature-verification=false --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "_CAP_EFFECTIVE" : "1fffffffff",
+        "_SYSTEMD_CGROUP" : "/system.slice/docker.service",
+        "_SYSTEMD_UNIT" : "docker.service",
+        "_SYSTEMD_SLICE" : "system.slice",
+        "_MACHINE_ID" : "225adcce13bd233a56ab481df7413e0b",
+        "_HOSTNAME" : "dev4.gcloud.set",
+        "MESSAGE" : "I0601 23:56:30.148153       1 event.go:221] Event(v1.ObjectReference{Kind:\"DaemonSet\", Namespace:\"openshift-monitoring\", Name:\"node-exporter\", UID:\"f6d2bdc1-6658-11e9-aca2-fa163e938959\", APIVersion:\"apps/v1\", ResourceVersion:\"15378688\", FieldPath:\"\"}): type: 'Normal' reason: 'SuccessfulCreate' Created pod: node-exporter-hvrpf",
+        "_SOURCE_REALTIME_TIMESTAMP" : "1559404590148488"
+}
+

在上面的json中,_CMDLINE以及其他字段占用量比较大,而且这些没有什么意义,会导致一条简短的日志却被封装成多了几十倍的量,所以的在日志量特别大的情况下,最好进行一下字段的定制,能够减少就减少。
+我们一般需要的字段是CONTAINER_NAME以及MESSAGE,通过CONTAINER_NAME可以获取到Kubernetes的namespace和podName,比如CONTAINER_NAME为k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1的时候
+container name in pod: controllers
+**pod name: **master-controllers-dev4.gcloud.set
+namespace: kube-system
+**pod uid: **dcab37be702c9ab6c2b17122c867c74a_1

+

1.2 新版本的subPathExpr

+

journal方式算是比较标准的方式,如果采用hostPath方式,能够直接将日志输出这里。这种方式唯一的缺点就是在旧Kubernetes中无法获取到podName,但是最新版的Kubernetes1.14的一些特性subPathExpr,就是可以将目录挂载的时候同时将podName写进目录里,但是这个特性仍旧是alpha版本,谨慎使用。
+简单说下实现原理:容器中填写的日志目录,挂载到宿主机的/data/logs/namespace/service_name/$(PodName)/xxx.log里面,如果是sidecar模式,则将改目录挂载到sidecar的收集目录里面进行推送。如果是宿主机安装fluentd模式,则需要匹配编写代码实现识别namespace、service_name、PodName等,然后发送到日志系统。

+

可参考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md
+日志落盘参考细节:

+
    env:
+    - name: POD_NAME
+      valueFrom:
+        fieldRef:
+          apiVersion: v1
+          fieldPath: metadata.name
+   ...
+    volumeMounts:
+    - name: workdir1
+      mountPath: /logs
+      subPathExpr: $(POD_NAME)
+

我们主要使用了在Pod里的主容器挂载了一个fluent-agent的收集器,来将日志进行收集,其中我们修改了Kubernetes-Client的源码使之支持subPathExpr,然后发送到日志系统的kafka。这种方式能够处理多种日志的收集,比如业务方的日志打到控制台了,但是jvm的日志不能同时打到控制台,否则会发生错乱,所以,如果能够将业务日志挂载到宿主机上,同时将一些其他的日志比如jvm的日志挂载到容器上,就可以使用该种方式。

+
{
+    "_fileName":"/data/work/logs/epaas_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"rongqiyun-dev",
+    "_podName":"aofjweojo-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}
+

二、Daemonset方式

+
+

daemonset方式也是基于journal,日志使用journal的log-driver,变成二进制的日志,然后在每个node节点上部署一个日志收集的agent,挂载/var/log/journal的日志进行解析,然后发送到kafka或者es,如果节点或者日志量比较大的话,对es的压力实在太大,所以,我们选择将日志推送到kafka。容器日志收集普遍使用fluentd,资源要求较少,性能高,是目前最成熟的日志收集方案,可惜是使用了ruby来写的,普通人根本没时间去话时间学习这个然后进行定制,好在openshift中提供了origin-aggregated-logging方案。
+我们可以通过fluent.conf来看origin-aggregated-logging做了哪些工作,把注释,空白的一些东西去掉,然后我稍微根据自己的情况修改了下,结果如下:

+
@include configs.d/openshift/system.conf
+设置fluent的日志级别
+@include configs.d/openshift/input-pre-*.conf
+最主要的地方,读取journal的日志
+@include configs.d/dynamic/input-syslog-*.conf
+读取syslog,即操作日志
+<label @INGRESS>
+  @include configs.d/openshift/filter-retag-journal.conf
+  进行匹配
+  @include configs.d/openshift/filter-k8s-meta.conf
+  获取Kubernetes的相关信息  
+  @include configs.d/openshift/filter-viaq-data-model.conf
+  进行模型的定义
+  @include configs.d/openshift/filter-post-*.conf
+  生成es的索引id
+  @include configs.d/openshift/filter-k8s-record-transform.conf
+  修改日志记录,我们在这里进行了字段的定制,移除了不需要的字段
+  @include configs.d/openshift/output-applications.conf
+  输出,默认是es,如果想使用其他的比如kafka,需要自己定制
+</label>
+

当然,细节上并没有那么好理解,换成一步步理解如下:

+

1. 解析journal日志
+origin-aggregated-logging会将二进制的journal日志中的CONTAINER_NAME进行解析,根据匹配规则将字段进行拆解

+
    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    }
+

2. es封装
+主要用的是elasticsearch_genid_ext插件,写在了filter-post-genid.conf上。

+

3. 日志分类
+通过origin-aggregated-logging来收集journal的日志,然后推送至es,origin-aggregated-logging在推送过程中做了不少优化,即适应高ops的、带有等待队列的、推送重试等,详情可以具体查看一下。

+

还有就是对日志进行了分类,分为三种:
+(1).操作日志(在es中以.operations匹配的),记录了对Kubernetes的操作
+(2).项目日志(在es中以project
匹配的),业务日志,日志收集中最重要的
+(3).孤儿日志(在es中以.orphaned.*匹配的),没有namespace的日志都会打到这里

+

4. 日志字段定制
+经过origin-aggregated-logging推送至后采集的一条日志如下:

+
{
+    "CONTAINER_TAG": "4ad125bb7558",
+    "docker": {
+      "container_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c"
+    },
+    "kubernetes": {
+      "container_name": "fas-dataservice-dev-new",
+      "namespace_name": "fas-cost-dev",
+      "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",
+      "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",
+      "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"
+    },
+    "systemd": {
+      "t": {
+        "BOOT_ID": "6246327d7ea441339d6d14b44498b177",
+        "CAP_EFFECTIVE": "1fffffffff",
+        "CMDLINE": "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry 10.77.0.0/16 --log-level=warn --signature-verification=false --bridge=none --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",
+        "COMM": "dockerd-current",
+        "EXE": "/usr/bin/dockerd-current",
+        "GID": "0",
+        "MACHINE_ID": "0096083eb4204215a24efd202176f3ec",
+        "PID": "17181",
+        "SYSTEMD_CGROUP": "/system.slice/docker.service",
+        "SYSTEMD_SLICE": "system.slice",
+        "SYSTEMD_UNIT": "docker.service",
+        "TRANSPORT": "journal",
+        "UID": "0"
+      }
+    },
+    "level": "info",
+    "message": "\tat com.sun.proxy.$Proxy242.execute(Unknown Source)",
+    "hostname": "host11.rqy.kx",
+    "pipeline_metadata": {
+      "collector": {
+        "ipaddr4": "10.76.232.16",
+        "ipaddr6": "fe80::a813:abff:fe66:3b0c",
+        "inputname": "fluent-plugin-systemd",
+        "name": "fluentd",
+        "received_at": "2019-05-15T09:22:39.297151+00:00",
+        "version": "0.12.43 1.6.0"
+      }
+    },
+    "@timestamp": "2019-05-06T01:41:01.960000+00:00",
+    "viaq_msg_id": "NjllNmI1ZWQtZGUyMi00NDdkLWEyNzEtMTY3MDQ0ZjEyZjZh"
+  }
+

可以看出,跟原生的journal日志类似,增加了几个字段为了写进es中而已,总体而言,其他字段并没有那么重要,所以我们对其中的字段进行了定制,以减少日志的大小,定制化字段之后,一段日志的输出变为(不是同一段,只是举个例子):

+
{
+    "hostname":"dev18.gcloud.set",
+    "@timestamp":"2019-05-17T04:22:33.139608+00:00",
+    "pod_name":"istio-pilot-8588fcb99f-rqtkd",
+    "appName":"discovery",
+    "container_name":"epaas-discovery",
+    "domain":"istio-system",
+    "sortedId":"NjA3ODVhODMtZDMyYy00ZWMyLWE4NjktZjcwZDMwMjNkYjQ3",
+    "log":"spiffluster.local/ns/istio-system/sa/istio-galley-service-account"
+}
+

5.部署
+最后,在node节点上添加logging-infra-fluentd: "true"的标签,就可以在namespace为openshift-logging中看到节点的收集器了。

+
logging-fluentd-29p8z              1/1       Running   0          6d
+logging-fluentd-bpkjt              1/1       Running   0          6d
+logging-fluentd-br9z5              1/1       Running   0          6d
+logging-fluentd-dkb24              1/1       Running   1          5d
+logging-fluentd-lbvbw              1/1       Running   0          6d
+logging-fluentd-nxmk9              1/1       Running   1          5d
+

6.关于ip
+业务方不仅仅想要podName,同时还有对ip的需求,控制台方式正常上是没有记录ip的,所以这算是一个难点中的难点,我们在kubernetes_metadata_common.rb的kubernetes_metadata中添加了 'pod_ip' => pod_object['status']['podIP'],最终是有些有ip,有些没有ip,这个问题我们继续排查。

+

三、Sidecar模式

+


+这种方式的好处是能够获取日志的文件名、容器的ip地址等,并且配置性比较高,能够很好的进行一系列定制化的操作,比如使用log-pilot或者filebeat或者其他的收集器,还能定制一些特定的字段,比如文件名、ip地址等。
+sidecar模式用来解决日志收集的问题的话,需要将日志目录挂载到宿主机的目录上,然后再mount到收集agent的目录里面,以达到文件共享的目的,默认情况下,使用emptydir来实现文件共享的目的,这里简单介绍下emptyDir的作用。
+EmptyDir类型的volume创建于pod被调度到某个宿主机上的时候,而同一个pod内的容器都能读写EmptyDir中的同一个文件。一旦这个pod离开了这个宿主机,EmptyDir中的数据就会被永久删除。所以目前EmptyDir类型的volume主要用作临时空间,比如Web服务器写日志或者tmp文件需要的临时目录。
+日志如果丢失的话,会对业务造成的影响不可估量,所以,我们使用了尚未成熟的subPathExpr来实现,即挂载到宿主的固定目录/data/logs下,然后是namespace,deploymentName,podName,再然后是日志文件,合成一块便是/data/logs/${namespace}/${deploymentName}/${podName}/xxx.log。
+具体的做法就不在演示了,这里只贴一下yaml文件。

+
apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: xxxx
+  namespace: element-dev
+spec:
+  template:
+    spec:
+      volumes:
+        - name: host-log-path-0
+          hostPath:
+            path: /data/logs/element-dev/xxxx
+            type: DirectoryOrCreate
+      containers:
+        - name: xxxx
+          image: 'xxxxxxx'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)
+        - name: xxxx-elog-agent
+          image: 'agent'
+          volumeMounts:
+            - name: host-log-path-0
+              mountPath: /data/work/logs/
+              subPathExpr: $(POD_NAME)
+

fluent.conf的配置文件由于保密关系就不贴了,收集后的一条数据如下:

+
{
+    "_fileName":"/data/work/logs/xxx_2019-05-22-0.log",
+    "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",
+    "_collectTime":"2019-05-22 17:23:58",
+    "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",
+    "_domain":"namespace",
+    "_ip":"10.128.93.31",
+    "_podName":"xxxx-5679849765-gncbf",
+    "_hostName":"dev4.gcloud.set"
+}
+

四、总结

+

总的来说,daemonset方式比较简单,而且适合更加适合微服务化,当然,不是完美的,比如业务方想把业务日志打到控制台上,但是同时也想知道jvm的日志,这种情况下或许sidecar模式更好。但是sidecar也有不完美的地方,每个pod里都要存在一个日志收集的agent实在是太消耗资源了,而且很多问题也难以解决,比如:主容器挂了,agent还没收集完,就把它给kill掉,这个时候日志怎么处理,业务会不会受到要杀掉才能启动新的这一短暂过程的影响等。所以,我们实际使用中首选daemonset方式,但是提供了sidecar模式让用户选择。

+

参考:
+1.Kubernetes日志官方文档
+2.Kubernetes日志采集Sidecar模式介绍
+3.Docker日志收集最佳实践

+]]>
+ +
+ + Kubernetes之request 和 limit详解 + http://www.wenzhihuai.com/kubernetes/request_limit.html + http://www.wenzhihuai.com/kubernetes/request_limit.html + Kubernetes之request 和 limit详解 + Kubernetes之request 和 limit详解 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 ... + Fri, 16 Feb 2024 11:46:28 GMT + 我们都知道 Kubernetes 中最小的原子调度单位是Pod,那么就意味着资源管理和资源调度相关的属性都应该Pod对象的字段,其中我们最常见的就是 Pod 的 CPU 和内存配置,而为了实现 Kubernetes 集群中资源的有效调度和充分利用,Kubernetes采用 requests 和 limits 两种限制类型来对CPU和内存资源进行容器粒度的分配。

+
resources:  
+    limits:    
+        cpu: "1"
+        memory: "500Mi"
+    requests:    
+        cpu: "100m"
+        memory: "1000Mi"
+

下面我们首先来了解一下上面这段 yaml 文件中字段的含义:requests 和 limits:

+

requests 定义了对应的容器所需要的最小资源量。
+limits 定义了对应容器最大可以消耗的资源上限。
+cpu 等于1一般等同于1CPU 核心,1个VCPU或者一个超线程,具体要看服务器的CPU。而 limits 这里设置的 100m 则叫做100毫核,100m就表示0.1个核,所以这里也可以用0.1代替。
+memory 等于500Mi,(备注:1Mi=10241024;1M=10001000)
+接下来我们来初步理解 requests 和 limits 这两个资源限制类型,在 Kubernetes 对 CPU 和内存资源限额的设计,通常是指用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。所以一般来说,在调度的时候 requests 比较重要,在运行时 limits 比较重要。

+

而对应实际的业务场景来说,以 java 应用为例,requests 对应的就是JVM虚拟机所需资源的最小值,而 limits 对应的就是 JVM 虚拟机所能够使用的资源最大值。以内存资源为例一般就是指:Xms 和 Xmx,如果 requests 值设置的小于JVM虚拟机 Xms 的值,那么就会导致 Pod 内存溢出,从而导致 Pod 被杀掉,而后重新创建一个Pod。

+

那么如果 CPU 资源使用超过了 limits,Pod会不会被杀掉呢?答案是不会,但是被限制。如果没有设置 limits ,Pod 可以使用全部空闲的资源。另外如果设置了 limits而没有设置 requests 时,Kubernetes 默认会将 requests 等于 limits。

+

这里通常还会将 requests 和 limits 描述的资源分为两类:可压缩资源(compressible resources) 和不可压缩资源(incompressible resources)。这里不难看出CPU这类型资源为可压缩资源,而内存这类型资源为不可压缩资源。所以合理设置不可压缩资源的limits值就相当重要了。

+]]>
+
+ + JVM调优思路 + http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html + http://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html + JVM调优思路 + JVM调优思路 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。 1.监控发现问题 2.工具分析问题 3.性能调优 下面开始一步步讲解 一、监控发现问题 通过监... + Fri, 16 Feb 2024 11:46:28 GMT + 在项目开发过程中、生产环境中,任何问题的解决、性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题,本文将从这个步骤入手,详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优。

+

1.监控发现问题
+2.工具分析问题
+3.性能调优
+下面开始一步步讲解

+

一、监控发现问题

+

通过监控工具例如Prometheus+Grafana,监控服务器有没有以下情况,有的话需要调优:

+

GC频繁
+CPU负载过高
+OOM
+内存泄露
+死锁
+程序响应时间较长

+

二、工具分析问题

+

参考

+

1.【JVM调优】如何进行JVM调优?一篇文章就够了!

+]]>
+
+ + 主流GC收集器采用的算法 + http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html + http://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html + 主流GC收集器采用的算法 + 主流GC收集器采用的算法 上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。 是否可以使用两种或多种垃圾回收器 在Java中,你不能同时使用两种不同的垃圾回收器。Serial和G1是两种不同的垃圾回收器,它们... + Fri, 16 Feb 2024 09:55:09 GMT + | 垃圾回收器 | 采用的GC算法 | 代次 |
+|

+]]>
+
+ + Java内存模型(JMM) + http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html + http://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html + Java内存模型(JMM) + Java内存模型(JMM) 本文转载自深入理解JVM-内存模型(jmm)和GC 1 CPU和内存的交互 了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】 有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型 在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存... + Fri, 16 Feb 2024 09:22:35 GMT + 本文转载自深入理解JVM-内存模型(jmm)和GC

+

1 CPU和内存的交互

+

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

+

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型

+]]>
+ +
+ + 引用计数和根可达算法 + http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html + http://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html + 引用计数和根可达算法 + 引用计数和根可达算法 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载 硬件角度来看什么叫做垃圾? 计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit... + Fri, 16 Feb 2024 09:22:35 GMT + 本文从JVM是如何寻找垃圾的?——引用计数和根可达算法转载

+

硬件角度来看什么叫做垃圾?

+

计算机的内存也叫做**DRAM(Dynamic Random Access Memory, 动态随机访问内存)**,底层是由一个电容和一个晶体管组成,电容充电表示该bit位数据为1,电容放电表示该bit位数据为0,整个内存就是无数个这样的bit位构成的。同时,内存需要在通电状态保持按周期频率的刷新,才能够维持数据的状态。

+

在内存中,某些内存区域(bit位)被使用过了(用来存储对象或者数据)会被打上标记表示已经被使用过了,分配新内存空间的时候就计算机硬件就不会使用这些被标记过的内存区域。通常,这些给内存区域打上标记的活儿一般由操作系统的系统调用来完成,例如C语言中的 allocate()函数调用的系统函数。

+

在面向对象的语言中,如果程序员新建了一个对象,那么操作系统会为其分配相应的内存区域,且该部分的内存区域会被打上标记说明已经被使用过了,这样操作系统就不会向该区域写入新的数据。

+

然而,如果这个标记没有被程序员释放(C语言中调用free()来释放内存区域),那么该内存区域会一直被标记成已使用,如果整个内存都被标记成为了已使用,那么当操作系统想要再分配内存的时候就会失败,这个时候只有重启电脑来解决问题了。

+

所以说,对于内存区域中不再使用的对象,我们需要释放掉其对应的内存区域,方便新的对象创建时有空间为其分配。而这些程序中不再被使用的对象就被称为“垃圾”,“垃圾”往往对应着内存中一块儿需要被释放的区域。

+

Java中的垃圾

+

Java中"垃圾"通常指的是不再被程序使用和引用的对象,具体表现在没有被栈、JNI指针和永久代对象所引用的对象。Java作为一种面向对象的编程语言,它使用自动内存管理机制,其中垃圾收集器负责检测和回收不再被程序引用的对象,以释放它们占用的内存空间。以下是一些导致对象成为垃圾的常见情况:

+
    +
  • 无引用对象: 当一个对象没有任何引用指向它时,它就变得不可达,成为垃圾,Java的垃圾收集器会识别这样的对象,并将它们回收。
  • +
  • 引用循环: 如果一组对象彼此引用形成一个循环,而这个循环与程序的其他部分没有引用相连,那么这个循环中的对象就会成为垃圾。Java的垃圾收集器通过识别引用循环并处理它们来防止内存泄漏。
  • +
  • 显式置空引用: 如果程序员显式地将一个引用置空(null),而没有其他引用指向相同的对象,那么该对象就变成了垃圾。
  • +
+

垃圾收集器周期性地运行,并识别和回收这些垃圾对象,释放其内存中对应的区域以确保内存能够得到有效利用,这种自动的内存管理机制就叫做垃圾回收。

+

如何寻找垃圾?

+

引用计数(Reference Count)

+
img
img
+

引用计数算法是一种垃圾标记,其核心思想是通过维护对象的引用计数来判断对象是否可以被回收。每个对象都有一个关联的引用计数,表示当前有多少个指针引用它。当引用计数为零时,意味着没有指针再引用该对象,因此可以安全地回收该对象的内存。

+

其实引用计数算法的核心思想就是,只要有对象引用我,那么就说明我是有用的,我还不需要被回收,反正,我就是没有用的对象,那么我和我的子对象都应该被回收掉。这里我们说的对象都是堆上的对象,一般是堆上的内存空间需要程序员手动回收,而栈上的内存空间则由操作系统自行回收。由于栈上的对象是操作系统自行管理和回收的,因此栈上的对象以及一些静态对象始终都是出于存活的状态,因此,堆中存活的对象至少会有一个引用(指针)指向它。

+

但是这样会存在着一个问题,就是对象中的引用关系形成了环状——循环引用,这种情况下环内所有对象的引用都是>1的,这样一来环内的所有都无法被回收从而造成“内存泄漏”。这是引用算法最主要的局限性,也是为什么JVM不采用循环计数的方法来标记垃圾的原因。

+

根可达算法(Root Search)

+

由于引用计数算法无法解决“循环引用”的问题,无可避免的会造成内存泄露,因此,Java没有采用引用计数算法来寻找垃圾。而是采用了一种从GC Roots开始搜索存活对象的垃圾标记算法——根可达算法。

+
img
img
+

哪些是GC Root?

+

| GC Roots Source | Description |
+|

+]]>
+ +
+ + synchronized + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html + synchronized + synchronized 偏向锁在JDK 15后已经废弃 一、什么是synchronized 关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。 imgimg 学习Java的小伙伴都知道sync... + Fri, 16 Feb 2024 09:22:35 GMT + 偏向锁在JDK 15后已经废弃

+

一、什么是synchronized

+

关键字提供了一种简单而有效的方式来控制并发访问共享资源。但是,它也有一些限制,例如性能问题和潜在的死锁风险,在更复杂的并发场景中,可以考虑使用java.util.concurrent包中提供的更灵活的同步机制。

+
img
img
+

学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:

+
    +
  • 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  • +
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  • +
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  • +
+

关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。

+

具体使用规则如下:
+在这里插入图片描述

+

二、Synchronized 原理

+

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

+

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

+

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

+

流程图如下:
+在这里插入图片描述

+

通过一段代码来演示:

+
public static void main(String[] args) {
+    synchronized (Synchronize.class){
+        System.out.println("Synchronize");
+    }
+}
+12345
+

使用javap -c Synchronize可以查看编译之后的具体信息。
+在这里插入图片描述
+可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

+

在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
+在这里插入图片描述
+synchronized的特点:
+在这里插入图片描述

+

三、Synchronized 优化

+

从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

+

3.1 偏向锁

+

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

+

3.1.1 偏向锁的获取过程

+

(1)访问Mark Word中偏向锁的标识是否设置成“1”,锁标志位是否为“01”——确认为可偏向状态。
+(2)如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。
+(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

+

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
+(5)执行同步代码。

+

3.1.2 偏向锁的释放

+

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

+

3.2 轻量锁

+

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

+

3.2.1 轻量级锁的加锁过程

+

(1)在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
+(2)拷贝对象头中的Mark Word复制到锁记录中。
+(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
+(4)如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
+(5)如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

+

3.2.2 轻量级锁的解锁过程

+

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
+(2)如果替换成功,整个同步过程完成。
+(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

+

3.3 其他优化

+

适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6 加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

+

通过--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。
+通过--XX:PreBlockSpin修改自旋次数,默认值是10次。

+

锁消除:锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

+

锁粗化:我们在写代码时推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

+

注意:在大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

+

四、扩展

+

其他控制并发/线程同步方式还有 Lock/ReentrantLock。

+

4.1 Synchronized 和 ReenTrantLock 的对比

+

① 两者都是可重入锁

+

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

+

② synchronized依赖于JVM而ReenTrantLock依赖于API

+

synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

+

③ ReenTrantLock比synchronized增加了一些高级功能

+

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

+
    +
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • +
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的,可以通过ReenTrantLoc类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • +
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。
  • +
+

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

+

④ 性能已不是选择标准

+

在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平。所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

+

CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销。

+
// CAS的操作参数
+// 内存位置(A)
+// 预期原值(B)
+// 预期新值(C)
+
+// 使用CAS解决并发的原理:
+// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
+// 2. 通过死循环,以不断尝试尝试更新的方式实现并发
+
+// 伪代码如下
+public boolean compareAndSwap(long memoryA, int oldB, int newC){
+    if(memoryA.get() == oldB){
+        memoryA.set(newC);
+        return true;
+    }
+    return false;
+}
+1234567891011121314151617
+

具体使用当中CAS有个先检查后执行的操作,而这种操作在 Java 中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
+具体过程:

+
    +
  1. CAS在Java中的体现为Unsafe类。
  2. +
  3. Unsafe类会通过C++直接获取到属性的内存地址。
  4. +
  5. 接下来CAS由C++的Atomic::cmpxchg系列方法实现。
  6. +
+

AtomicInteger的 i++ 与 i-- 是典型的CAS应用,通过compareAndSet & 一个死循环实现。

+
private volatile int value; 
+/** 
+* Gets the current value. 
+* 
+* @return the current value 
+*/ 
+public final int get() { 
+   return value; 
+} 
+/** 
+* Atomically increments by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndIncrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current + 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+} 
+
+/** 
+* Atomically decrements by one the current value. 
+* 
+* @return the previous value 
+*/ 
+public final int getAndDecrement() { 
+   for (;;) { 
+       int current = get(); 
+       int next = current - 1; 
+       if (compareAndSet(current, next)) 
+           return current; 
+   } 
+}
+123456789101112131415161718192021222324252627282930313233343536
+

以上内容引用自学习指南
+总的来说:
+1、synchronized是java关键字,而Lock是java中的一个接口
+2、synchronized会自动释放锁,而Lock必须手动释放锁
+3、synchronized是不可中断的,Lock可以中断也可以不中断
+4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
+5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
+6、Lock可以使用读锁提高多线程读效率
+7、synchronized是非公平锁,ReentranLock可以控制是否公平锁

+

4.2 Synchronized 与 ThreadLocal 的对比

+

Synchronized 与 ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:

+
    +
  1. Synchronized关键字主要解决多线程共享数据同步问题;ThreadLocal主要解决多线程中数据因并发产生不一致问题。
  2. +
  3. Synchronized是利用锁的机制,使变量或代码块只能被一个线程访问。而ThreadLocal为每一个线程都提供变量的副本,使得每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  4. +
+

4.3 synchronized与volatile区别

+

| volatile | synchronized |
+|

+]]>
+ +
+ + mini主机 + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/ + http://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/ + mini主机 + Fri, 16 Feb 2024 04:00:26 GMT + + + 个人网站 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/ + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/ + 个人网站 + jofjweoiaejof + Fri, 16 Feb 2024 04:00:26 GMT + jofjweoiaejof

+]]>
+
+ + JVM + http://www.wenzhihuai.com/java/JVM/ + http://www.wenzhihuai.com/java/JVM/ + JVM + Fri, 16 Feb 2024 04:00:26 GMT + + + CMS + http://www.wenzhihuai.com/java/JVM/cms.html + http://www.wenzhihuai.com/java/JVM/cms.html + CMS + CMS JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。 CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。 内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提... + Fri, 16 Feb 2024 04:00:26 GMT + JDK14已经将CMS回收器完全移除,这里只需要记住它的缺点即可。

+

CPU资源消耗:CMS垃圾回收器在运行过程中会与应用线程并发执行,这可能会导致较高的CPU资源消耗。

+

内存碎片问题:CMS垃圾回收器在进行垃圾回收时,不会对对象进行压缩和整理,这可能会导致内存碎片问题。当内存碎片过多时,可能会导致无法找到足够大的连续内存空间来分配大对象,从而提前触发Full GC。

+

预测性差:CMS垃圾回收器的暂停时间和CPU资源消耗都很难预测,这可能会对系统的性能造成影响。

+

维护复杂:CMS垃圾回收器的代码相对复杂,需要更多的维护工作。

+]]>
+
+ + G1 + http://www.wenzhihuai.com/java/JVM/g1.html + http://www.wenzhihuai.com/java/JVM/g1.html + G1 + G1 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器 G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点: 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并... + Fri, 16 Feb 2024 04:00:26 GMT + 本文转载自增量式垃圾回收器——带你深入理解G1垃圾回收器

+

G1垃圾回收器是在Java 7之后引入的一种垃圾回收器。它被设计为一种分代、增量、并行和并发的标记-复制垃圾回收器,旨在适应不断扩大的内存和增加的处理器数量,以显著降低垃圾回收造成的暂停时间,同时保持良好的吞吐量,主要有以下特点:

+
    +
  • 并行与并发: G1回收器在垃圾回收的不同阶段使用了并行和并发的方式,充分利用多核处理器的优势,提高了垃圾回收的效率。
  • +
  • 分代回收: G1垃圾回收器依旧采用分代回收的思想,但是和CMS等分代回收算法不同,G1不是将整个堆内存划分为年轻代、老年代和元空间。而是将堆内存划分为一个个固定大小的region,每个region可以属于年轻代或老年代。垃圾回收的基本单位是region,而不是整个堆,这使得垃圾回收更加灵活。
  • +
  • 增量回收: G1采用增量回收的方式,将整个垃圾回收过程分解为多个阶段进行,这样更有利于分散垃圾回收的压力,减小每次暂停的时间,提高系统的响应性。
  • +
  • Compacting回收: 与CMS回收器不同,G1是一种compacting回收器,其回收的内存空间是连续的。这样就可以避免CMS收集器由于不连续内存空间造成的所需堆空间更大和浮动垃圾的问题。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  • +
  • 软实时: G1回收器具有软实时(soft real-time)的特性,用户可以指定垃圾回收时间的限时。虽然G1会努力在限定时间内完成垃圾回收,但并不保证每次都能在时限内完成。通过设定合理的目标,可以使大部分垃圾回收时间都在规定的时限内完成。
  • +
+

G1垃圾回收器以其创新性的设计和优越的性能特点,逐渐成为Java应用程序中首选的垃圾回收器之一。通过分代、增量、并行、并发等多种技术手段的结合,G1回收器在处理大内存和多核处理器的环境下表现出色,为Java应用程序提供了更好的性能和响应能力。

+

G1的内存模型和分代策略

+

G1收集器相关参数

+

| 参数 | 默认值 | 描述 |
+|

+]]>
+ +
+ + ZGC + http://www.wenzhihuai.com/java/JVM/zgc.html + http://www.wenzhihuai.com/java/JVM/zgc.html + ZGC + ZGC 本文转载自12 张图带你彻底理解 ZGC ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC... + Fri, 16 Feb 2024 04:00:26 GMT + 本文转载自12 张图带你彻底理解 ZGC

+

ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点。在 JDK 15 中 ZGC 不再是实验功能,可以正式投入生产使用了,使用 –XX:+UseZGC 可以启用 ZGC。

+

ZGC 有 3 个重要特性:

+
    +
  • 暂停时间不会超过 10 ms。
  • +
+
+

JDK 16 发布后,GC 暂停时间已经缩小到 1 ms 以内,并且时间复杂度是 o(1),这也就是说 GC 停顿时间是一个固定值了,并不会受堆内存大小影响。
+下面图片来自:https://malloc.se/blog/zgc-jdk16

+
+
img
img
+
    +
  • 最大支持 16TB 的大堆,最小支持 8MB 的小堆。
  • +
  • 跟 G1 相比,对应用程序吞吐量的影响小于 15 %。
  • +
+

1 内存多重映射

+

内存多重映射,就是使用 mmap 把不同的虚拟内存地址映射到同一个物理内存地址上。如下图:

+
img
img
+

ZGC 为了更灵活高效地管理内存,使用了内存多重映射,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。

+

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。

+

Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

+

2 染色指针

+

2.1 三色标记回顾

+

我们知道 G1 垃圾收集器使用了三色标记,这里先做一个回顾。下面是一个三色标记过程中的对象引用示例图:

+
img
img
+

总共有三种颜色,说明如下:

+
    +
  • 白色:本对象还没有被标记线程访问过。
  • +
  • 灰色:本对象已经被访问过,但是本对象引用的其他对象还没有被全部访问。
  • +
  • 黑色:本对象已经被访问过,并且本对象引用的其他对象也都被访问过了。
  • +
+

三色标记的过程如下:

+
    +
  1. 初始阶段,所有对象都是白色。
  2. +
  3. 将 GC Roots 直接引用的对象标记为灰色。
  4. +
  5. 处理灰色对象,把当前灰色对象引用的所有对象都变成灰色,之后将当前灰色对象变成黑色。
  6. +
  7. 重复步骤 3,直到不存在灰色对象为止。
  8. +
+

三色标记结束后,白色对象就是没有被引用的对象(比如上图中的 H 和 G),可以被回收了。

+

2.2 染色指针

+

ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如下图:

+
img
img
+

前 62位保存了 GC 信息,最后两位保存了锁标志。

+

ZGC 的一大创举是将 GC 信息保存在了染色指针上。染色指针是一种将少量信息直接存储在指针上的技术。在 64 位 JVM 中,对象指针是 64 位,如下图:

+
img
img
+

在这个 64 位的指针上,高 16 位都是 0,暂时不用来寻址。剩下的 48 位支持的内存可以达到 256 TB(2 ^48),这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 48 位都用来保存对象信息,而是用高 4 位保存了四个标志位,这样 ZGC 可以管理的最大内存可以达到 16 TB(2 ^ 44)。

+

通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。

+

无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。

+

3 内存布局

+

首先我们回顾一下 G1 垃圾收集器的内存布局。G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:

+
img
img
+

跟 G1 类似,ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的。不同的是,ZGC 的 Region 支持动态地创建和销毁,并且 Region 的大小不是固定的,包括三种类型的 Region :

+
    +
  • Small Region:2MB,主要用于放置小于 256 KB 的小对象。
  • +
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
  • +
  • Large Region:N * 2MB。这个类型的 Region 是可以动态变化的,不过必须是 2MB 的整数倍,最小支持 4 MB。每个 Large Region 只放置一个大对象,并且是不会被重分配的。
  • +
+

4 读屏障

+

读屏障类似于 Spring AOP 的前置增强,是 JVM 向应用代码中插入一小段代码,当应用线程从堆中读取对象的引用时,会先执行这段代码。注意:只有从堆内存中读取对象的引用时,才会执行这个代码。下面代码只有第一行需要加入读屏障。

+
Object o = obj.FieldA
+Object p = o //不是从堆中读取引用
+o.dosomething() //不是从堆中读取引用
+int i =  obj.FieldB //不是引用类型
+

读屏障在解释执行时通过 load 相关的字节码指令加载数据。作用是在对象标记和转移过程中,判断对象的引用地址是否满足条件,并作出相应动作。如下图:

+
img
img
+

标记、转移和重定位这些过程请看下一节。

+
+

读屏障会对应用程序的性能有一定影响,据测试,对性能的最高影响达到 4%,但提高了 GC 并发能力,降低了 STW。

+
+

5 GC 过程

+

前面已经讲过,ZGC 使用内存多重映射技术,把物理内存映射为 Marked0、Marked1 和 Remapped 三个地址视图,利用地址视图的切换,ZGC 实现了高效的并发收集。

+

ZGC 的垃圾收集过程包括标记、转移和重定位三个阶段。如下图:

+
img
img
+

ZGC 初始化后,整个内存空间的地址视图被设置为 Remapped。

+

5.1 初始标记

+

从 GC Roots 出发,找出 GC Roots 直接引用的对象,放入活跃对象集合,这个过程需要 STW,不过 STW 的时间跟 GC Roots 数量成正比,耗时比较短。

+

5.2 并发标记

+

并发标记过程中,GC 线程和 Java 应用线程会并行运行。这个过程需要注意下面几点:

+
    +
  • GC 标记线程访问对象时,如果对象地址视图是 Remapped,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理。
  • +
  • 标记过程中Java 应用线程新创建的对象会直接进入 Marked0 视图。
  • +
  • 标记过程中Java 应用线程访问对象时,如果对象的地址视图是 Remapped,就把对象地址视图切换到 Marked0,可以参考前面讲的读屏障。
  • +
  • 标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。
  • +
+

标记阶段的活跃视图也可能是 Marked1,为什么会采用两个视图呢?

+

这里采用两个视图是为了区分前一次标记和这一次标记。如果这次标记的视图是 Marked0,那下一次并发标记就会把视图切换到 Marked1。这样做可以配合 ZGC 按照页回收垃圾的做法。如下图:

+
img
img
+

第二次标记的时候,如果还是切换到 Marked0,那么 2 这个对象区分不出是活跃的还是上次标记过的。如果第二次标记切换到 Marked1,就可以区分出了。

+

这时 Marked0 这个视图的对象就是上次标记过程被标记过活跃,转移的时候没有被转移,但这次标记没有被标记为活跃的对象。Marked1 视图的对象是这次标记被标记为活跃的对象。Remapped 视图的对象是上次垃圾回收发生转移或者是被 Java 应用线程访问过,本次垃圾回收中被标记为不活跃的对象。

+

5.3 再标记

+

并发标记阶段 GC 线程和 Java 应用线程并发执行,标记过程中可能会有引用关系发生变化而导致的漏标记问题。再标记阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软应用,虚引用等)进行并行标记。

+

这个阶段需要 STW,但是需要标记的对象少,耗时很短。

+

5.4 初始转移

+

转移就是把活跃对象复制到新的内存,之前的内存空间可以被回收。

+

初始转移需要扫描 GC Roots 直接引用的对象并进行转移,这个过程需要 STW,STW 时间跟 GC Roots 成正比。

+

5.5 并发转移

+

并发转移过程 GC 线程和 Java 线程是并发进行的。上面已经讲过,转移过程中对象视图会被切回 Remapped 。转移过程需要注意以下几点:

+
    +
  • 如果 GC 线程访问对象的视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • +
  • 如果 GC 线程访问对象的视图是 Remapped,说明被其他 GC 线程处理过,跳过不再处理。
  • +
  • 并发转移过程中 Java 应用线程创建的新对象地址视图是 Remapped。
  • +
  • 如果 Java 应用线程访问的对象被标记为活跃并且对象视图是 Marked0,则转移对象,并把对象视图设置成 Remapped。
  • +
+

5.6 重定位

+

转移过程对象的地址发生了变化,在这个阶段,把所有指向对象旧地址的指针调整到对象的新地址上。

+

6 垃圾收集算法

+

ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象。如下图:

+
img
img
+

6.1 JDK 16 之前

+

在 JDK 16 之前,ZGC 会预留(Reserve)一块儿堆内存,这个预留内存不能用于 Java 线程的内存分配。即使从 Java 线程的角度看堆内存已经满了也不能使用 Reserve,只有 GC 过程中搬移存活对象的时候才可以使用。如下图:

+
img
img
+

这样做的好处是算法简单,非常适合并行收集。但这样做有几个问题:

+
    +
  • 因为有预留内存,能给 Java 线程分配的堆内存小于 JVM 声明的堆内存。
  • +
  • Reserve 仅仅用于存放 GC 过程中搬移的对象,有点内存浪费。
  • +
  • 因为 Reserve 不能给 GC 过程中搬移对象的 Java 线程使用,搬移线程可能会因为申请不到足够内存而不能完成对象搬移,这返回过来又会导致应用程序的 OOM。
  • +
+

6.2 JDK 16 改进

+

JDK 16 发布后,ZGC 支持就地搬移对象(G1 在 Full GC 的时候也是就地搬移)。这样做的好处是不用预留空闲内存了。如下图:

+
img
img
+

不过就地搬移也有一定的挑战。比如:必须考虑搬移对象的顺序,否则可能会覆盖尚未移动的对象。这就需要 GC 线程之间更好的进行协作,不利于并发收集,同时也会导致搬移对象的 Java 线程需要考虑什么可以做什么不可以做。

+

为了获得更好的 GC 表现,JDK 16 在支持就地搬移的同时,也支持预留(Reserve)堆内存的方式,并且 ZGC 不需要真的预留空闲的堆内存。默认情况下,只要有空闲的 region,ZGC 就会使用预留堆内存的方式,如果没有空闲的 region,否则 ZGC 就会启用就地搬移。如果有了空闲的 region, ZGC 又会切换到预留堆内存的搬移方式。

+

7 总结

+

内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。

+

ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。

+

ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。

+

参考:

+

1.https://wiki.openjdk.java.net/display/zgc

+

2.https://openjdk.java.net/jeps/304

+

3.https://openjdk.java.net/jeps/376

+

4.https://malloc.se/blog/zgc-jdk16

+

5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg

+

6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw

+

7.https://www.jianshu.com/p/664e4da05b2c

+

8.https://www.cnblogs.com/jimoer/p/13170249.html

+

9.https://www.jianshu.com/p/12544c0ad

+]]>
+ +
+ + SpringBoot + http://www.wenzhihuai.com/java/SpringBoot/ + http://www.wenzhihuai.com/java/SpringBoot/ + SpringBoot + Fri, 16 Feb 2024 04:00:26 GMT + + + I/O + http://www.wenzhihuai.com/java/io/ + http://www.wenzhihuai.com/java/io/ + I/O + Fri, 16 Feb 2024 04:00:26 GMT + + + 线程 + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/ + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/ + 线程 + Fri, 16 Feb 2024 04:00:26 GMT + + + volatile + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html + http://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html + volatile + volatile Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。 image-20240217004404932imag... + Fri, 16 Feb 2024 04:00:26 GMT + Volatile是Java中的一种轻量级同步机制,用于保证变量的可见性和禁止指令重排。当一个变量被声明为Volatile类型时,任何修改该变量的操作都会立即被所有线程看到。也就是说,Volatile修饰的变量在每次修改时都会强制将修改刷新到主内存中,具有很好的可见性和线程安全性。

+
image-20240217004404932
image-20240217004404932
+

可见性

+

有序性

+]]>
+ +
+ + 网络 + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/ + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/ + 网络 + Fri, 16 Feb 2024 04:00:26 GMT + + + DevOps + http://www.wenzhihuai.com/kubernetes/devops/ + http://www.wenzhihuai.com/kubernetes/devops/ + DevOps + jofjweoiaejof + Fri, 16 Feb 2024 04:00:26 GMT + jofjweoiaejof

+]]>
+
+ + RedissonLock分布式锁源码分析 + http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html + http://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html + RedissonLock分布式锁源码分析 + RedissonLock分布式锁源码分析 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。 目前分布式锁常... + Tue, 06 Feb 2024 07:22:35 GMT + 最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等,则执行,否则不执行,想想也是一种比较简单的方式吧,但是感觉很low很low,所以改用分布式锁。
+目前分布式锁常用的三种方式:1.数据库的锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。其中数据库中的锁有共享锁和排他锁,这两种都无法直接解决数据库的单点和可重入的问题,所以,本章还是来讲讲基于Redis的分布式锁,也可以用其他缓存(Memcache、Tair等)来实现。

+

一、实现分布式锁的要求

+
    +
  1. 互斥性。在任何时候,当且仅有一个客户端能够持有锁。
  2. +
  3. 不能有死锁。持有锁的客户端崩溃后,后续客户端能够加锁。
  4. +
  5. 容错性。大部分Redis或者ZooKeeper节点能够正常运行。
  6. +
  7. 加锁解锁相同。加锁的客户端和解锁的客户端必须为同一个客户端,不能让其他的解锁了。
  8. +
+

二、Redis实现分布式锁的常用命令

+

1.SETNX key val
+当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
+2.expire key timeout
+为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
+3.delete key
+删除key,此处用来解锁使用。
+4.HEXISTS key field
+当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。
+5.HINCRBY key field increment
+将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。

+

三、常见写法

+

由上面三个命令,我们可以很快的写一个分布式锁出来:

+
if (conn.setnx("lock","1").equals(1L)) { 
+    //do something
+    return true; 
+} 
+return false;
+

但是这样也会存在问题,如果获取该锁的客户端挂掉了怎么办?一般而言,我们可以通过设置expire的过期时间来防止客户端挂掉所带来的影响,可以降低应用挂掉所带来的影响,不过当时间失效的时候,要保证其他客户端只有一台能够获取。

+

四、Redisson

+

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。——摘自百度百科

+

4.1 测试例子

+

先在pom引入Redssion

+
<dependency>
+    <groupId>org.redisson</groupId>
+    <artifactId>redisson</artifactId>
+    <version>3.6.1</version>
+</dependency>
+

起100个线程,同时对count进行操作,每次操作减1,加锁的时候能够保持顺序输出,不加的话为随机。

+
public class RedissonTest implements Runnable {
+    private static RedissonClient redisson;
+    private static int count = 10000;
+
+    private static void init() {
+        Config config = new Config();
+        config.useSingleServer()
+                .setAddress("redis://119.23.46.71:6340")
+                .setPassword("root")
+                .setDatabase(10);
+        redisson = Redisson.create(config);
+    }
+
+    @Override
+    public void run() {
+        RLock lock = redisson.getLock("anyLock");
+        lock.lock();
+        count--;
+        System.out.println(count);
+        lock.unlock();
+    }
+
+    public static void main(String[] args) {
+        init();
+        for (int i = 0; i < 100; i++) {
+            new Thread(new RedissonTest()).start();
+        }
+    }
+}
+

输出结果(部分结果):

+
...
+9930
+9929
+9928
+9927
+9926
+9925
+9924
+9923
+9922
+9921
+
+...
+

去掉lock.lock()和lock.unlock()之后(部分结果):

+
...
+9930
+9931
+9933
+9935
+9938
+9937
+9940
+9941
+9942
+9944
+9947
+9946
+9914
+...
+

五、RedissonLock源码分析

+

最新版的Redisson要求redis能够支持eval的命令,否则无法实现,即Redis要求2.6版本以上。在lua脚本中可以调用大部分的Redis命令,使用脚本的好处如下:
+(1)减少网络开销:在Redis操作需求需要向Redis发送5次请求,而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
+(2)原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
+(3)复用:客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。

+

5.1 使用到的全局变量

+

全局变量:
+expirationRenewalMap:存储entryName和其过期时间,底层用的netty的PlatformDependent.newConcurrentHashMap()
+internalLockLeaseTime:锁默认释放的时间:30 * 1000,即30秒
+id:UUID,用作客户端的唯一标识
+PUBSUB:订阅者模式,当释放锁的时候,其他客户端能够知道锁已经被释放的消息,并让队列中的第一个消费者获取锁。使用PUB/SUB消息机制的优点:减少申请锁时的等待时间、安全、 锁带有超时时间、锁的标识唯一,防止死锁 锁设计为可重入,避免死锁。
+commandExecutor:命令执行器,异步执行器

+

5.2 加锁

+

以lock.lock()为例,调用lock之后,底层使用的是lockInterruptibly,之后调用lockInterruptibly(-1, null);

+
+
+
+

(1)我们来看一下lockInterruptibly的源码,如果别的客户端没有加锁,则当前客户端进行加锁并且订阅,其他客户端尝试加锁,并且获取ttl,然后等待已经加了锁的客户端解锁。

+
//leaseTime默认为-1
+public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
+    long threadId = Thread.currentThread().getId();//获取当前线程ID
+    Long ttl = tryAcquire(leaseTime, unit, threadId);//尝试加锁
+    // 如果为空,当前线程获取锁成功,否则已经被其他客户端加锁
+    if (ttl == null) {
+        return;
+    }
+    //等待释放,并订阅锁
+    RFuture<RedissonLockEntry> future = subscribe(threadId);
+    commandExecutor.syncSubscription(future);
+    try {
+        while (true) {
+            // 重新尝试获取锁
+            ttl = tryAcquire(leaseTime, unit, threadId);
+            // 成功获取锁
+            if (ttl == null) {
+                break;
+            }
+            // 等待锁释放
+            if (ttl >= 0) {
+                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
+            } else {
+                getEntry(threadId).getLatch().acquire();
+            }
+        }
+    } finally {
+        // 取消订阅
+        unsubscribe(future, threadId);
+    }
+}
+

(2)下面是tryAcquire的实现,调用的是tryAcquireAsync

+
    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
+        return get(tryAcquireAsync(leaseTime, unit, threadId));
+    }
+

(3)下面是tryAcquireAsync的实现,异步尝试进行加锁,尝试加锁的时候leaseTime为-1。通常如果客户端没有加锁成功,则会进行阻塞,leaseTime为锁释放的时间。

+
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
+    if (leaseTime != -1) {   //在lock.lock()的时候,已经声明了leaseTime为-1,尝试加锁
+        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
+    }
+    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
+    //监听事件,订阅消息
+    ttlRemainingFuture.addListener(new FutureListener<Long>() {
+        @Override
+        public void operationComplete(Future<Long> future) throws Exception {
+            if (!future.isSuccess()) {
+                return;
+            }
+            Long ttlRemaining = future.getNow();
+            // lock acquired
+            if (ttlRemaining == null) {
+                //获取新的超时时间
+                scheduleExpirationRenewal(threadId);
+            }
+        }
+    });
+    return ttlRemainingFuture;  //返回ttl时间
+}
+

(4)下面是tryLockInnerAsyncy异步加锁,使用lua能够保证操作是原子性的

+
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
+    internalLockLeaseTime = unit.toMillis(leaseTime);
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
+              "if (redis.call('exists', KEYS[1]) == 0) then " +
+                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
+                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
+                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
+                  "return nil; " +
+              "end; " +
+              "return redis.call('pttl', KEYS[1]);",
+                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
+}
+

参数
+KEYS[1](getName()) :需要加锁的key,这里需要是字符串类型。
+ARGV[1](internalLockLeaseTime) :锁的超时时间,防止死锁
+ARGV[2](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId
+lua脚本解释

+
--检查key是否被占用了,如果没有则设置超时时间和唯一标识,初始化value=1
+if (redis.call('exists', KEYS[1]) == 0) then
+  redis.call('hset', KEYS[1], ARGV[2], 1);
+  redis.call('pexpire', KEYS[1], ARGV[1]);
+  return nil; 
+end; 
+--如果锁重入,需要判断锁的key field 都一致情况下 value 加一 
+if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
+  redis.call('hincrby', KEYS[1], ARGV[2], 1);
+  --锁重入重新设置超时时间  
+  redis.call('pexpire', KEYS[1], ARGV[1]); 
+  return nil; 
+end;
+--返回剩余的过期时间
+return redis.call('pttl', KEYS[1]);
+

(5)流程图

+
+
+
+

5.3 解锁

+

解锁的代码很简单,大意是将该节点删除,并发布消息。
+(1)unlock源码

+
    public void unlock() {
+        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
+        if (opStatus == null) {
+            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+                    + id + " thread-id: " + Thread.currentThread().getId());
+        }
+        if (opStatus) {
+            cancelExpirationRenewal();
+        }
+

(2)异步解锁,并返回是否成功

+
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
+    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
+            "if (redis.call('exists', KEYS[1]) == 0) then " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; " +
+            "end;" +
+            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
+                "return nil;" +
+            "end; " +
+            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
+            "if (counter > 0) then " +
+                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
+                "return 0; " +
+            "else " +
+                "redis.call('del', KEYS[1]); " +
+                "redis.call('publish', KEYS[2], ARGV[1]); " +
+                "return 1; "+
+            "end; " +
+            "return nil;",
+            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
+
+    }
+

输入的参数有:
+参数:
+KEYS[1](getName()):需要加锁的key,这里需要是字符串类型。
+KEYS[2](getChannelName()):redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName:“redisson_lock__channel__{” + getName() + “}”
+ARGV[1](LockPubSub.unlockMessage):redis消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。
+ARGV[2](internalLockLeaseTime):锁的超时时间,防止死锁
+ARGV[3](getLockName(threadId)) :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

+

此处lua脚本的作用:

+
--如果keys[1]不存在,则发布消息,说明已经被解锁了
+if (redis.call('exists', KEYS[1]) == 0) then
+    redis.call('publish', KEYS[2], ARGV[1]);
+    return 1;
+end;
+--key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。
+if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
+    return nil;
+end;
+--将value减1,这里主要用在重入锁
+local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
+if (counter > 0) then 
+    redis.call('pexpire', KEYS[1], ARGV[2]); 
+    return 0; 
+else 
+--删除key并消息
+    redis.call('del', KEYS[1]); 
+    redis.call('publish', KEYS[2], ARGV[1]); 
+    return 1;
+end; 
+return nil;
+

(3)删除过期信息

+
void cancelExpirationRenewal() {
+    Timeout task = expirationRenewalMap.remove(getEntryName());
+    if (task != null) {
+        task.cancel();
+    }
+}
+

总结

+

Redis2.6版本之后引入了eval,能够支持lua脚本,更好的保证了redis的原子性,而且redisson采用了大量异步的写法来避免性能所带来的影响。本文只是讲解了下redisson的重入锁,其还有公平锁、联锁、红锁、读写锁等,有兴趣的可以看下。感觉这篇文章写得也不是很好,毕竟netty还没开始学,有些api也不太清楚,希望各位大佬能够建议建议~~

+

参考:
+1.redisson
+2.Redis分布式锁的正确实现方式
+3.分布式锁的多种实现方式
+4.用Redis构建分布式锁
+5.基于Redis的分布式锁实现
+6.基于Redis实现分布式锁,Redisson使用及源码分析

+]]>
+ +
+ + canal小记 + http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html + http://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html + canal小记 + canal小记 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql&gt;&gt;canal&gt;&gt;flume&gt;&gt;kafka&gt;&gt;es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。 先来了解一下MySQL的主从备份: 从上层来看,复制分成三步: master将改... + Tue, 06 Feb 2024 07:22:35 GMT + 接到个小需求,将mysql的部分数据增量同步到es,但是不仅仅是使用canal而已,整体的流程是mysql>>canal>>flume>>kafka>>es,说难倒也不难,只是做起来碰到的坑实在太多,特别是中间套了那么多中间件,出了故障找起来真的特别麻烦。

+

先来了解一下MySQL的主从备份:

+
+

从上层来看,复制分成三步:
+master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
+slave将master的binary log events拷贝到它的中继日志(relay log);
+slave重做中继日志中的事件,将改变反映它自己的数据。

+

问题一:测试环境一切正常,但是正式环境中,这几个字段全为0,不知道为什么

+

最后发现是沟通问题。。。

+
+

排查过程:

+
    +
  1. 起初,怀疑是es的问题,会不会是string转为long中出现了问题,PUT了个,无异常,这种情况排除。
  2. +
  3. 再然后以为是代码有问题,可是想了下,rowData.getAfterColumnsList().forEach(column -> data.put(column.getName(), column.getValue()))这句不可能有什么其他的问题啊,而且测试环境中一切都是好好的。
  4. +
  5. canal安装出错,重新查看了一次canal.properties和instance.properties,并没有发现配置错了啥,如果错了,那为什么只有那几个字段出现异常,其他的都是好好的,郁闷。而且,用测试环境的canal配置生产中的数据库,然后本地调试,结果依旧一样。可能问题出在mysql。
  6. +
+

最后发现,居然是沟通问题。。。。测试环境中是从正式环境导入的,用的insert,可是在正式环境里,用的确实insert后update字段,之后发现居然还用delete,,,,晕。。。。之前明确问过了只更新insert的,人与人之间的信任在哪里。。。。

+

问题二:canal.properties中四种模式的差别

+

简单的说,canal维护一份增量订阅和消费关系是依靠解析位点和消费位点的,目前提供了一下四种配置,一开始我也是懵的。

+
#canal.instance.global.spring.xml = classpath:spring/local-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
+canal.instance.global.spring.xml = classpath:spring/file-instance.xml
+#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
+

local-instance
+我也不知道啥。。

+

memory-instance
+所有的组件(parser , sink , store)都选择了内存版模式,记录位点的都选择了memory模式,重启后又会回到初始位点进行解析
+特点:速度最快,依赖最少(不需要zookeeper)
+场景:一般应用在quickstart,或者是出现问题后,进行数据分析的场景,不应该将其应用于生产环境。
+个人建议是调试的时候使用该模式,即新增数据的时候,客户端能马上捕获到改日志,但是由于位点一直都是canal启动的时候最新的,不适用与生产环境。

+

file-instance
+所有的组件(parser , sink , store)都选择了基于file持久化模式,注意,不支持HA机制.
+特点:支持单机持久化
+场景:生产环境,无HA需求,简单可用.
+采用该模式的时候,如果关闭了canal,会在destination中生成一个meta.dat,用来记录关键信息。如果想要启动canal之后马上订阅最新的位点,需要把该文件删掉。
+{"clientDatas":[{"clientIdentity":{"clientId":1001,"destination":"example","filter":".\.."},"cursor":{"identity":{"slaveId":-1,"sourceAddress":{"address":"192.168.6.71","port":3306}},"postion":{"included":false,"journalName":"binlog.008335","position":221691106,"serverId":88888,"timestamp":1524294834000}}}],"destination":"example"}

+

default-instance
+所有的组件(parser , sink , store)都选择了持久化模式,目前持久化的方式主要是写入zookeeper,保证数据集群共享。
+特点:支持HA
+场景:生产环境,集群化部署.
+该模式会记录集群中所有运行的节点,主要用与HA主备模式,节点中的数据如下,可以关闭某一个canal服务来查看running的变化信息。

+
+

问题三:如果要订阅的是mysql的从库改怎么做?

+

生产环境中的主库是不能随便重启的,所以订阅的话必须订阅mysql主从的从库,而从库中是默认下只将主库的操作写进中继日志,并写到自己的二进制日志的,所以需要让其成为canal的主库,必须让其将日志也写到自己的二进制日志里面。处理方法:修改/etc/my.cnf,增加一行log_slave_updates=1,重启数据库后就可以了。

+
+

问题四:部分字段没有更新

+

最终版本是以mysql的id为es的主键,用canal同步到flume,再由flume到kafka,然后再由一个中间件写到es里面去,结果发现,一天之中,会有那么一段时间得出的结果少一丢丢,甚至是骤降,如图。不得不从头开始排查情况,canal到flume,加了canal的重试,以及发送到flume的重试机制,没有报错,所有数据正常发送。flume到kafka不敢怀疑,毕竟公司一直在用,怎么可能有问题。kafka到es的中间件?组长写的,而且一直在用,不可能==最后确认的是flume到kafka,kafka的parition处理速度不同,

+
+

check一下flume的文档,可以知道

+

| Property Name | Description|
+|

+]]>
+ +
+ + 基于ZooKeeper的队列爬虫 + http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html + http://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html + 基于ZooKeeper的队列爬虫 + 基于ZooKeeper的队列爬虫 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。 简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的C... + Tue, 06 Feb 2024 07:22:35 GMT + 一直琢磨着分布式的东西怎么搞,公司也没有相关的项目能够参与,所以还是回归自己的专长来吧——基于ZooKeeper的分布式队列爬虫,由于没什么人能够一起沟通分布式的相关知识,下面的小项目纯属“胡编乱造”。
+简单介绍下ZooKeeper:ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
+基本的知识就不过多介绍了,可以参考参考下面这些人的:
+ZooKeeper官网
+http://www.cnblogs.com/wuxl360/p/5817471.html

+

一、整体架构

+

这张图来自skyme,我也是看了这张图的启发写了这篇文章的。

+
+

最基本的分布式队列即一个生产者不断抓取链接,然后将链接存储进ZooKeeper的队列节点里,每个节点的value都只是链接,然后消费者从中获取一条url进行抓取。本项目生产这主要是用来生产URL即可,这部分就不要要求太多。然后是消费者,消费者需要解决的问题有:
+1.队列如何保证自己的分发正确;
+2.消费这如何进行高效的抓取。

+

二、ZooKeeper队列原理

+

2.1 介绍

+

分布式队列,目前此类产品大多类似于ActiveMQ、RabbitMQ等,本文主要介绍的是Zookeeper实现的分布式队列,它的实现方式也有两种,一种是FIFO(先进先出)的队列,另一种是等待队列元素聚集之后才统一安排的Barrier模型。同样,本文主要讲的是FIFO的队列模型。其大体设计思路也很简单,主要是在/SinaQueue下创建顺序节点,如/SinaQueue/qn-000000000,创建完节点之后,根据下面的4个步骤来决定执行的顺序。
+1.通过调用getChildren()接口来获取某一节点下的所有节点,即获取队列中的所有元素。
+2.确定自己的节点序号在所有子节点中的顺序。
+3.如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
+4.接收到Watcher通知后,重复步骤1。

+
+

2.2 Watcher介绍

+

znode以某种方式发生变化时,“观察”(watch)机制可以让客户端得到通知.可以针对ZooKeeper服务的“操作”来设置观察,该服务的其他 操作可以触发观察。
+1.Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
+2.Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调
+3.Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。

+

2.3 源码

+

在csdn上找到了某个人写的这个过程,使用的是ZKClient,有兴趣可以看看杰布斯的博客,但是没有实现上面过程的第三步(Watcher相关的),这里,我们使用的是Zookeeper的另一个客户端工具curator,其中,curator实现了各种Zookeeper的特性,如:Election(选举),Lock(锁),Barrier(关卡),Atonmic(原子量),Cache(缓存),Queue(队列)等。我们来看看Curator实现的简单的分布式队列的源码。

+
public class SimpleDistributedQueue {
+    ...
+    private final CuratorFramework client;//连接Zookeeper的客户端
+    private final String path;//路径
+    private final EnsureContainers ensureContainers;//确保原子特性
+    private final String PREFIX = "qn-";//顺序节点的同意前缀,使用qn-
+    ...
+

其中PREFIX是用来生成顺序节点的,默认不可更改,将生成的路径赋予给path,然后向节点赋予数据。下面是赋予数据的代码

+
    public boolean offer(byte[] data) throws Exception {
+        String thisPath = ZKPaths.makePath(this.path, "qn-");//生成的路径
+        ((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL)).forPath(thisPath, data);//如果没有路径将生成持久化的路径然后存储节点的数据。
+        return true;
+    }
+

最关键的来了,队列如何保证自己的分发正确?SimpleDistributedQueue使用take()来取得队列的头部,然后将头部删掉,这一过程的一致性是通过CountDownLatch和Watcher来实现的。

+
    public byte[] take() throws Exception {//直接调用interPoll,并将超时的设置为0;
+        return this.internalPoll(0L, (TimeUnit)null);
+    }
+    private byte[] internalPoll(long timeout, TimeUnit unit) throws Exception {
+        ...//忽略超时的设置代码
+        while(true) {
+            final CountDownLatch latch = new CountDownLatch(1);//定义一个latch,设置为1,先加锁,然后执行完任务后再释放锁
+            Watcher watcher = new Watcher() {
+                public void process(WatchedEvent event) {
+                    latch.countDown();
+                }
+            };
+            byte[] bytes;
+            try {
+                bytes = this.internalElement(true, watcher);//调用internalElement函数来获取字节流
+            } catch (NoSuchElementException var17) {
+            }
+            ...
+            if (hasTimeout) {
+                long elapsedMs = System.currentTimeMillis() - startMs;
+                long thisWaitMs = maxWaitMs - elapsedMs;
+                if (thisWaitMs <= 0L) {    //如果等待超时了则返回为空
+                    return null;
+                }
+                latch.await(thisWaitMs, TimeUnit.MILLISECONDS);
+            } else {
+                latch.await();
+            }
+        }
+    }
+    private byte[] internalElement(boolean removeIt, Watcher watcher) throws Exception {
+            this.ensurePath();
+            List nodes;
+            try {
+                nodes = watcher != null ? (List)((BackgroundPathable)this.client.getChildren().usingWatcher(watcher)).forPath(this.path) : (List)this.client.getChildren().forPath(this.path);//获取节点下的所有子节点注册监听(watcher默认都不是为空的,每一个都注册)
+            } catch (NoNodeException var8) {
+                throw new NoSuchElementException();
+            }
+    
+            Collections.sort(nodes);//对节点进行排序
+            Iterator var4 = nodes.iterator();
+            while(true) {//遍历
+                while(var4.hasNext()) {
+                    String node = (String)var4.next();//取得当前头结点
+                    if (node.startsWith("qn-")) {
+                        String thisPath = ZKPaths.makePath(this.path, node);
+                        try {
+                            byte[] bytes = (byte[])this.client.getData().forPath(thisPath);
+                            if (removeIt) {
+                                this.client.delete().forPath(thisPath);//删除该节点
+                            }
+                            return bytes;//返回节点的字节流
+            ...
+        }
+

三、多线程并发

+

对于分布式爬虫来说,让每一个消费者高效的进行抓取是具有重要意义的,为了加快爬虫的速度,采用多线程爬虫的方法。Java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。其中使用Executors提供了四种声明线程池的方法,分别是newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor和newScheduledThreadPool,为了监控实时监控队列的长度,我们使用数组型的阻塞队列ArrayBlockingQueue。声明方式如下:

+
    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+    ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+            0L, TimeUnit.MILLISECONDS,
+            queuelength);
+

四、使用

+

本次实验主要环境如下:

+
zookeeper.version=3.5
+java.version=1.8.0_65
+os.arch=amd64
+i5 四核心CPU
+网速为中国电信100M
+

这里主要是对博客园中的前两千条博客进行爬取,本文主要是对分布式队列的理解,就不再进行什么难度的处理(比如元素的选取、数据的存储等),只输出每篇博客的title即可。
+生产者代码:

+
public class Producer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Producer.class);
+    public static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/Queue");
+    private static Integer j = 0;
+
+    public static void begin(String url) {//对博客园的每一页进行爬取
+        try {
+            String content = HttpHelper.getInstance().get(url);
+            resolveweb(content);
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+    public static void resolveweb(String content) throws Exception {
+        Elements elements = Jsoup.parse(content).select("a.titlelink");//对每篇博客的标题进行获取
+        for (Element element : elements) {
+            String url = element.attr("href");//
+            if (StringUtils.isNotEmpty(url) && !url.contains("javascript") && !url.contains("jump")) {//去除a中调用href过程
+                logger.info(url + " " + String.valueOf(j++));
+                queue.offer(url.getBytes());
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        client.start();
+        for (int i = 0; i < 100; i++) {
+            begin("https://www.cnblogs.com/#p" + String.valueOf(i));
+        }
+    }
+}
+

消费者

+
public class Consumer {
+    //logger
+    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
+    private static final CuratorFramework client = CuratorFrameworkFactory.builder().connectString("119.23.46.71:2181")
+            .sessionTimeoutMs(1000)
+            .connectionTimeoutMs(1000)
+            .canBeReadOnly(false)
+            .retryPolicy(new ExponentialBackoffRetry(1000, Integer.MAX_VALUE))
+            .defaultData(null)
+            .build();
+    private static SimpleDistributedQueue queue = new SimpleDistributedQueue(client, "/SinaQueue");
+    private static Integer i = 0;
+    private static final Integer CORE = Runtime.getRuntime().availableProcessors();
+    //声明为一个数组型的阻塞队列,这里限制大小为
+    private static final BlockingQueue<Runnable> queuelength = new ArrayBlockingQueue<>(1000);
+
+    static class CBCrawler implements Runnable {
+        private String url;
+
+        public CBCrawler(String url) {
+            this.url = url;
+        }
+
+        @Override
+        public void run() {
+            String content = HttpHelper.getInstance().get(url);
+            logger.info(url + " " + Jsoup.parse(content).title());//打印网页的标题
+        }
+    }
+
+    public static void begin() {
+        try {
+            ExecutorService es = new ThreadPoolExecutor(CORE, CORE,
+                    0L, TimeUnit.MILLISECONDS,
+                    queuelength);
+            while (client.getChildren().forPath("/SinaQueue").size() > 0) {
+                CBCrawler crawler = new CBCrawler(new String(queue.take()));
+                es.submit(crawler);//执行爬虫
+                i = i + 1;
+                logger.info(String.valueOf(i) + " is finished\n" + " queue size is" + queuelength.size());//监控当前队列的长度
+            }
+            if (!es.isShutdown()) {//如果线程池没有关闭则关闭
+                es.shutdown();
+            }
+        } catch (Exception e) {
+            logger.error("", e);
+        }
+    }
+
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        client.start();
+        begin();
+        client.close();
+        logger.info("start time: " + start);
+        long end = System.currentTimeMillis();
+        logger.info("end time: " + end);
+        logger.info("take time: " + String.valueOf(end - start));//记录开始时间和结束时间
+    }
+}
+

由于在队列的take中使用了CountDownLatch和Collections.sort(nodes)进行排序,耗时过程变长了不少,2000个节点,单台服务器和多台服务器的耗时是一样的,都是9分钟,具体实验见下面。

+

实验结果

+

生产者生产URL:

+
+

单机模式下的消费者,耗时:560825/(1000*60)=9分钟

+
+

分布式模式下的抓取:

+
+

耗时:564374/(1000*60)=9分钟:

+
+

由图可见,当每个消费者处理能力大于队列分配的能力时,耗时的过程反而是在队列,毕竟分布式队列在进行take动作的时候对节点进行了加锁,还要对队列进行排序,特别是在节点多达2000+的情况下,耗时是十分严重的。

+

实验二

+

实验二的主要解决的问题是将消费者处理的耗时延长,我们使用Thread.sleep(n)来模拟时长。由于博客园突然连不上,为了减少这种不可控的故障,抓取的网页改为新浪,并将抓取后的URL以文本形式保存下来。

+
public static void sleepUtil(Integer time) {
+    try {
+        Thread.sleep(time * 1000);
+    } catch (Exception e) {
+        logger.error("线程sleep异常", e);
+    }
+}
+

此时再看程序的输出,可以看出,队列的分发能力已经大于消费者的处理能力,总算是正常了。

+
+

分布式队列分发的时间是:341998/(1000*60)=5.6分钟

+
2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - start time: 1509324606460
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - end time: 1509324948458
+2017-10-30  08:55:48.458 [main] INFO  com.crawler.Consumer - take time: 341998
+

两台机子抓取完毕的耗时分别是:

+
A服务器:08:49:54.509——09:02:07  
+B服务器:08:49:54.509——09:05:05
+

单机的时候分发时间是:353198/(1000*60)=5.8分钟

+
2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - start time: 1509326672614
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - end time: 1509327025812
+2017-10-30  09:30:25.812 [main] INFO  com.crawler.Consumer - take time: 353198
+

耗时

+
09:24:33.391——09:51:44.733
+

分布式下平均耗时约为13分钟,单机模式下耗时约为27分钟,还是蛮符合估算的。

+

总结

+

源代码都放在这里了,有兴趣的可以star一下或者下载看一下,也欢迎大家提提意见,没企业级的实战环境,见笑了O(∩_∩)O~

+

欢迎访问我的个人网站
+个人网站网址:http://www.wenzhihuai.com
+个人网站代码地址:https://github.com/Zephery/newblog

+]]>
+ +
+ + TCP/IP、HTTP、HTTPS、HTTP2.0 + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html + http://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html + TCP/IP、HTTP、HTTPS、HTTP2.0 + TCP/IP、HTTP、HTTPS、HTTP2.0 HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认... + Tue, 06 Feb 2024 07:05:59 GMT + HTTP,全称超文本传输协议(HTTP,HyperText Transfer Protocol),是一个客户端和服务器端请求和应答的标准(TCP),互联网上应用最为广泛的一种网络协议。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。

+

HTTPS,即加密后的HTTP。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。目前TLS的版本是1.2,定义在RFC 5246中,暂时还没有被广泛的使用。

+

HTTP2.0,下一代的HTTP协议。相比于HTTP1.x,大幅度的提升了web性能,进一步减少了网络延时和拥塞。

+
+

各自的RFC相关文档自己去搜吧,https://www.rfc-editor.org/

+

一、TCP/IP

+

为了了解HTTP,有必要先理解一下TCP/IP。目前,存在两种划分模型的方法,OSI七层模型和TCP/IP模型,具体的区别不在阐述。HTTP是建立在TCP协议之上,所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性,例如tcp建立连接的3次握手和断开连接的4次挥手以及每次建立连接带来的RTT延迟时间。

+
+

TCP三次握手四次挥手的原理,由于篇幅关系,具体请看TCP协议的三次握手和四次挥手

+

二、HTTP

+

超文本传输协议(HyperText Transfer Protocol) 是伴随着计算机网络和浏览器而诞生,在浏览器出现之前,人们是怎么使用网络的?,不管怎么说,那个时代对于现在的我们,有点难以想象。。。之后,网景发布了Netscape Navigator浏览器,才慢慢打开了互联网的幕布。如果根据OSI来划分的话,HTML属于表示层,而HTTP属于应用层。HTTP发展至今,经过了HTTP0.9、HTTP1.0、HTTP1.1、HTTP2.0的时代,虽然2.0很久之前就正式提出标准,大多浏览器也支持了,但是网络支持HTTP2.0的却很少。

+

2.1 HTTP报文分析

+

报文,是网络中交换和传输的基本单元,即一次性发送的数据块。HTTP的报文是由一行一行组成的,纯文本,而且是明文,即:如果能监听你的网络,那么你发送的所有账号密码都是可以看见的,为了保障数据隐秘性,HTTPS随之而生。

+

2.1.1 请求报文:

+

为了形象点,我们把报文标准和实际的结合起来看。

+
+

下面是实际报文,以访问自己的网站(http://www.wenzhihuai.com)中的一个链接为例。

+
+
请求行
+

请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT,这里我们使用的是GET方法,访问的是/biaoqianyun.do,协议使用的是HTTP/1.1。
+GET:当客户端要从服务器中读取某个资源时,使用GET 方法。如果需要加传参数的话,需要在URL之后加个"?",然后把参数名字和值用=连接起来,传递参数长度受限制,通常IE8的为4076,Chrome的为7675。例如,/index.jsp?id=100&op=bind。
+POST:当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,以名称/值的形式出现,可以传输大量数据;

+
请求头部
+

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
+User-Agent:产生请求的浏览器类型;
+Accept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ / ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
+Accept-Language:客户端可接受的自然语言;
+Accept-Encoding:客户端可接受的编码压缩格式;
+Accept-Charset:可接受的应答的字符集;
+Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
+connection:连接方式(close 或 keepalive),如果是close的话就需要进行TCP四次挥手关闭连接,如果是keepalive,表明还能继续使用,这是HTTP1.1对1.0的新增,加快了网络传输,默认是keepalive;
+Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;

+
空行
+

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头;

+
请求包体
+

请求包体不在 GET 方法中使用,而是在POST 方法中使用。POST 方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型 Content-Type 和包体长度 Content-Length;

+

2.1.2 响应报文

+

同样,先粘贴报文标准。

+
+

抓包,以访问(http://www.wenzhihuai.com)为例。

+
+
状态行
+

状态行由 HTTP 协议版本字段、状态码和状态码的描述文本 3 个部分组成,他们之间使用空格隔开,描述文本一般不显示;
+状态码:由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:
+1xx:服务器已接收,但客户端可能仍要继续发送;
+2xx:成功;
+3xx:重定向;
+4xx:请求非法,或者请求不可达;
+5xx:服务器内部错误;

+
响应头部:响应头可能包括:
+

Location:Location响应报头域用于重定向接受者到一个新的位置。例如:客户端所请求的页面已不存在原先的位置,为了让客户端重定向到这个页面新的位置,服务器端可以发回Location响应报头后使用重定向语句,让客户端去访问新的域名所对应的服务器上的资源;
+Server:Server 响应报头域包含了服务器用来处理请求的软件信息及其版本。它和 User-Agent 请求报头域是相对应的,前者发送服务器端软件的信息,后者发送客户端软件(浏览器)和操作系统的信息。
+Vary:指示不可缓存的请求头列表;
+Connection:连接方式,这个跟rquest的类似。
+空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
+响应包体:服务器返回给客户端的文本信息;

+

2.2 HTTP特性

+

HTTP的主要特点主要能概括如下:

+

2.2.1 无状态性

+

即,当客户端访问完一次服务器再次访问的时候,服务器是无法知道这个客户端之前是否已经访问过了。优点是不需要先前的信息,能够更快的应答,缺点是每次连接传送的数据量增大。这种做法不利于信息的交互,随后,Cookie和Session就应运而生,至于它俩有什么区别,可以看看COOKIE和SESSION有什么区别?
+

+

2.2.2 持久连接

+

HTTP1.1 使用持久连接keepalive,所谓持久连接,就是服务器在发送响应后仍然在一段时间内保持这条连接,允许在同一个连接中存在多次数据请求和响应,即在持久连接情况下,服务器在发送完响应后并不关闭TCP连接,客户端可以通过这个连接继续请求其他对象。

+

2.2.3 其他

+

支持客户/服务器模式、简单快速(请求方法简单Get和POST)、灵活(数据对象任意)

+

2.3 影响HTTP的因素

+

影响HTTP请求的因素:

+
    +
  1. 带宽
    +好像只要上网这个因素是一直都有的。。。即使再快的网络,也会有偶尔网络慢的时候。。。
  2. +
  3. 延迟
    +(1) 浏览器阻塞
    +一个浏览器对于同一个域名,同时只能有4个链接(根据不同浏览器),如果超了后面的会被阻塞。
    +常用浏览器阻塞数量看下图。
  4. +
+
+

(2) DNS查询
+浏览器建立连接是需要知道服务器的IP的,DNS用来将域名解析为IP地址,这个可以通过刷新DNS缓存来加快速度。
+(3) 建立连接
+由之前第一章的就可以看出,HTTP是基于TCP协议的,即使网络、浏览器再快也要进行TCP的三次握手,在高延迟的场景下影响比较明显,慢启动则对文件请求影响较大。

+

2.4 缺陷

+
    +
  1. 耗时:传输数据每次都要建立连接;
  2. +
  3. 不安全:HTTP是明文传输的,只要在路由器或者交换机上截取,所有东西(账号密码)都是可见的;
  4. +
  5. Header内容过大:通常,客户端的请求header变化较小,但是每次都要携带大量的header信息,导致传输成本增大;
  6. +
  7. keepalive压力过大:持久连接虽然有一点的优点,但同时也会给服务器造成大量的性能压力,特别是传输图片的时候。
  8. +
+

BTW:明文传输有多危险,可以去试试,下面是某个政府网站,采用wireshark抓包,身份证、电话号码、住址什么的全暴露出来,所以,,,只要在路由器做点小动作,你的信息是全部能拿得到的,毕竟政府。

+
+

由于涉及的隐私太多,打了马赛克

+

三、HTTPS

+

由于HTTP报文的不安全性,网景在1994年就创建了HTTPS,并用在浏览器中。最初HTTPS是和SSL一起使用,然后演化为TLS。SSL/TLS在OSI模型中都是表示层的协议。SSL使 用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。

+

3.1 SSL/TLS

+

SSL(Secure Sockets Layer),简称安全套接入层,最初由上世纪90年代由网景公司设计。开启 SSL 会增加内存、CPU、网络带宽的开销,后二者跟你使用的 cipher suite 密切相关,其中参数很多,很难一概而论。开启 SSL 的前提是你的 cert 和 key 必须放在 TCP endpoint,你是否信得过那台设备。
+TLS(Transport Layer Security),简称安全传输层协议,该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面,与具体的应用无关,所以,一般把TLS协议归为传输层安全协议。
+由于本人在加密算法上面知识匮乏,就不误人子弟了,有兴趣可以看看百度百科里的资料,SSL,TLS

+

3.2 SPDY

+

2012年google提出了SPDY的方案,大家才开始从正面看待和解决老版本HTTP协议本身的问题,SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:
+降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
+请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
+header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
+基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
+服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。SPDY构成图。

+
+

3.3 HTTPS报文分析

+

跟之前的报文分析一样,我们使用wireshark来抓包分析,以在百度上搜索点东西为例。

+
+

192.168.1.103为本地电脑的ip地址,14.215.177.39为百度服务器地址。下面是步骤:

+
    +
  1. 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
  2. +
  3. 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。之后服务器发送 Certificate 报文。报文中包含公开密钥证书。最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
  4. +
  5. SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。
  6. +
  7. 服务器同样发送 Change Cipher Spec 报文。 服务器同样发送 Finished 报文。
  8. +
  9. 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。 应用层协议通信,即发送 HTTP 响应。
    +当然,用一张图更容易解释
    +简单地说就是下面。
  10. +
+
+

当我们追踪流的数据的时候,可以看到,基本上都是乱码,经过加密,数据是看不到,如果需要在wireshark上看到,则需要在wireshark中配置ssl。

+
+

3.4 HTTPS全站化

+

现今,感觉只要和商业利益有关的,就不得不涉及到加密这类东西。淘宝、京东、唯品会这些电商可谓是最早推行全站https的,这类电商是离用户金钱最近的企业。截止今年底,基本所有商业网站也基本实现了HTTPS。。。。至于小站点,比如个人网站,玩玩还是可以的。如果一个网站需要由HTTP全部变为HTTPS,那么需要关注下面几点:

+
    +
  1. CA证书,大部分证书都是需要收费的,当然,自己在服务器上用openssl也可以,不过浏览器会提示当前私密连接不安全这个警告,普通人看到这种信息是不会继续浏览的,所以,想使用HTTPS,可以使用Let's Encrypt,由谷歌等公司推行。
  2. +
  3. HTTPS性能优化,SSL握手,HTTPS 对速度会有一定程度的降低,但是只要经过合理优化和部署,HTTPS 对速度的影响完全可以接受。
  4. +
  5. CPU计算压力,HTTPS中大量的秘钥算法计算,对CPU的压力可想而知。
    +至于我自己的个人网站,之前实现了https,用的免费证书,但是由于HTTPS下的网站,所有子链都要使用HTTPS,使用了七牛云的CDN,如果要使用HTTPS加速,是要收费的,所以只能放弃。。。
  6. +
+

四、HTTP2.0

+

HTTP2.0,相较于HTTP1.x,大幅度的提升了web性能。在与HTTP/1.1完全语义兼容的基础上,进一步减少了网络延迟和传输的安全性。HTTP2.0可以说是SPDY的升级版(基于SPDY设计的),但是依然存在一些不同点:HTTP2.0支持明文传输,而SPDY强制使用HTTPS;HTTP2.0消息头的压缩算法采用HPACK,而非SPDY采用的DEFLATE。

+

4.1 历史

+

HTTP 2.0在2013年8月进行首次合作共事性测试。在开放互联网上HTTP 2.0将只用于https://网址,而 http://网址将继续使用HTTP/1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。HTTP 2.0是在SPDY(An experimental protocol for a faster web, The Chromium Projects)基础上形成的下一代互联网通信协议。HTTP/2 的目的是通过支持请求与响应的多路复用来较少延迟,通过压缩HTTPS首部字段将协议开销降低,同时增加请求优先级和服务器端推送的支持。

+

4.2 HTTP2.0新特性

+

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

+

4.2.1 二进制帧

+

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的Request Body则封装到DATA frame里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

+
+

4.2.2 多路复用

+

所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完。

+

4.2.3 流量控制

+

TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

+

4.2.4 服务器端推送

+

服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

+
+

4.2.5 首部压缩

+

HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
+如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。

+

4.3 HTTP1.1与HTTP2.0的对比

+

以访问https://http2.akamai.com/demo为例。

+
+

4.4 报文

+

访问https://http2.akamai.com/demo,谷歌浏览器的报文没有显示出协议,此处使用火狐浏览器。
+响应头部分如下。

+
+

请求头如下。

+
+

采用淘宝网站为例,淘宝目前采用主站使用HTTP1.1,资源使用HTTP2.0,少些使用SPDY协议。目前也是业界比较流行的做法。

+
+

参考

+
    +
  1. HTTPS那些事
  2. +
  3. 如何搭建一个HTTP2.0的网站
  4. +
  5. HTTP/2.0 相比1.0有哪些重大改进?
  6. +
  7. HTTP2.0 demo
  8. +
  9. Http、Https、Http2前身
  10. +
  11. HTTP报文
  12. +
  13. HTTP、HTTP2.0、SPDY、HTTPS 你应该知道的一些事
  14. +
  15. HTTPS权威指南
  16. +
  17. HTTP2.0的奇妙日常
  18. +
  19. curl 支持 HTTP2
  20. +
  21. 淘宝HTTPS探索
  22. +
  23. HTTPS完全协议详解
  24. +
+

欢迎访问我的个人网站。https://wenzhihuai.com

+]]>
+ +
+ + 高性能高并发高可用的一些思考 + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html + http://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html + 高性能高并发高可用的一些思考 + 高性能高并发高可用的一些思考 TODO待补充 异步 Jmeter JProfiler 火焰图 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar 内存火焰图 缓存 限流熔断 Kafka 序列化 参考 1.Java数据库高并发如何解决 java高并发三种解... + Sat, 03 Feb 2024 17:57:14 GMT + TODO待补充

+

异步

+

Jmeter

+

JProfiler

+

火焰图

+

curl -O https://arthas.aliyun.com/arthas-boot.jar
+java -jar arthas-boot.jar

+

内存火焰图

+

缓存

+

限流熔断

+

Kafka

+

序列化

+

参考

+

1.Java数据库高并发如何解决 java高并发三种解决方法 转载
+2.Java多线程梳理之四_其他并发解决方案
+3.Java高并发之并发基础

+]]>
+
+ + 友链地址 + http://www.wenzhihuai.com/link/main.html + http://www.wenzhihuai.com/link/main.html + 友链地址 + 友链地址 + Fri, 02 Feb 2024 03:58:59 GMT + + + + 广州图书馆借阅抓取 + http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html + http://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html + 广州图书馆借阅抓取 + 广州图书馆借阅抓取 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。 搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于... + Tue, 30 Jan 2024 06:37:05 GMT + 欢迎访问我的个人网站,要是能在GitHub上对网站源码给个star就更好了。

+

搭建自己的网站的时候,想把自己读过借过的书都想记录一下,大学也做过自己学校的借书记录的爬取,但是数据库删掉了==,只保留一张截图。所以还是要好好珍惜自己阅读的日子吧,记录自己的借书记录——广州图书馆,现在代码已经放在服务器上定时运行,结果查看我的网站(关于我)页面。整个代码采用HttpClient,存储放在MySql,定时使用Spring自带的Schedule,下面是抓取的过程。

+

1.页面跳转过程

+

一般都是进入首页http://www.gzlib.gov.cn/,点击进登陆页面,然后输入账号密码。表面上看起来没什么特别之处,实际上模拟登陆的时候不仅仅是向链接post一个请求那么简单,得到的response要么跳回登陆页面,要么无限制重定向。

+
+
+
+

事实上,它做了单点登录,如下图,广州图书馆的网址为:www.gzlib.gov.cn,而登陆的网址为:login.gzlib.gov.cn。原理网上很多人都讲的很好了,可以看看这篇文章SSO单点登录

+
+
+
+

2.处理方法

+

解决办法不难,只要先模拟访问一下首页即可获取图书馆的session,python的获取代码如:session.get("http://www.gzlib.gov.cn/"),打印cookie之后如下:

+
[<Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for .gzlib.gov.cn/>, <Cookie JSESSIONID=19E2DDED4FE7756AA9161A52737D6B8E for www.gzlib.gov.cn/>, <Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]
+

整个登陆抓取的流程如下:

+
+
+
+

即:
+(1)用户先点击广州图书馆的首页,以获取改网址的session,然后点击登录界面,解析html,获取lt(自定义的参数,类似于验证码),以及单点登录服务器的session。
+(2)向目标服务器(单点登录服务器)提交post请求,请求参数中包含username(用户名),password(密码),event(时间,默认为submit),lt(自定义请求参数),同时服务端还要验证的参数:refer(来源页面),host(主机信息),Content-Type(类型)。
+(3)打印response,搜索你自己的名字,如果有则表示成功了,否则会跳转回登陆页面。
+(4)利用cookie去访问其他页面,此处实现的是对借阅历史的抓取,所以访问的页面是:http://www.gzlib.gov.cn/member/historyLoanList.jspx。

+

基本的模拟登陆和获取就是这些,之后还有对面html的解析,获取书名、书的索引等,然后封装成JavaBean,再之后便是保存入数据库。(去重没有做,不知道用什么方式比较好)

+

3.代码

+

3.1 Java中,一般用来提交http请求的大部分用的都是httpclient,首先,需要导入的httpclient相关的包:

+
<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpclient</artifactId>
+    <version>4.5.3</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.httpcomponents</groupId>
+    <artifactId>httpcore</artifactId>
+    <version>4.4.7</version>
+</dependency>
+

3.2 构建声明全局变量——上下文管理器,其中context为上下文管理器

+
public class LibraryUtil {
+    private static CloseableHttpClient httpClient = null;
+    private static HttpClientContext context = null;
+    private static CookieStore cookieStore = null;
+    static {
+        init();
+    }
+    private static void init() {
+        context = HttpClientContext.create();
+        cookieStore = new BasicCookieStore();
+        // 配置超时时间
+        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(12000).setSocketTimeout(6000)
+                .setConnectionRequestTimeout(6000).build();
+        // 设置默认跳转以及存储cookie
+        httpClient = HttpClientBuilder.create()
+                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
+                .setRedirectStrategy(new DefaultRedirectStrategy()).setDefaultRequestConfig(requestConfig)
+                .setDefaultCookieStore(cookieStore).build();
+    }
+    ...
+

3.3 声明一个get函数,其中header可自定义,此处不需要,但是保留着,做成一个通用的吧。

+
    public static CloseableHttpResponse get(String url, Header[] header) throws IOException {
+        HttpGet httpget = new HttpGet(url);
+        if (header != null && header.length > 0) {
+            httpget.setHeaders(header);
+        }
+        CloseableHttpResponse response = httpClient.execute(httpget, context);//context用于存储上下文
+        return response;
+    }
+

3.4 访问首页以获得session,服务器上会话是使用session存储的,本地浏览器使用的是cookie,只要本地不退出,那么使用本地的cookie来访问也是可以的,但是为了达到模拟登陆的效果,这里就不再阐述这种方式。

+
CloseableHttpResponse homeResponse = get("http://www.gzlib.gov.cn/", null);
+homeResponse.close();
+

此时,如果打印cookie,可以看到目前的cookie如下:

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>]>
+

3.5 访问登陆页面,获取单点登录服务器之后的cookie,解析网页,获取自定义参数lt。这里的解析网页使用了Jsoup,语法和python中的BeautifulSoup中类似。

+
String loginURL = "http://login.gzlib.gov.cn/sso-server/login?service=http%3A%2F%2Fwww.gzlib.gov.cn%2Flogin.jspx%3FreturnUrl%3Dhttp%253A%252F%252Fwww.gzlib.gov.cn%252F%26locale%3Dzh_CN&appId=www.gzlib.gov.cn&locale=zh_CN";
+CloseableHttpResponse loginGetResponse = get(loginURL, null);
+String content = toString(loginGetResponse);
+String lt = Jsoup.parse(content).select("form").select("input[name=lt]").attr("value");
+loginGetResponse.close();
+

此时,再次查看cookie,多了一个(www.gzlib.gov.cn/sso-server):

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
+

3.6 声明一个post函数,用来提交post请求,其中提交的参数默认为

+
    public static CloseableHttpResponse postParam(String url, String parameters, Header[] headers)
+            throws IOException {
+        System.out.println(parameters);
+        HttpPost httpPost = new HttpPost(url);
+        if (headers != null && headers.length > 0) {
+            for (Header header : headers) {
+                httpPost.addHeader(header);
+            }
+        }
+        List<NameValuePair> nvps = toNameValuePairList(parameters);
+        httpPost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
+        CloseableHttpResponse response = httpClient.execute(httpPost, context);
+        return response;
+    }
+

3.7 登陆成功后,如果没有声明returnurl,即登录链接为(http://login.gzlib.gov.cn/sso-server/login),那么只是会显示成功登录的页面:

+
+
+
+

后台应该是定义了一个service用来进行链接跳转的,想要获取登录成功之后的跳转页面可修改service之后的链接,这里将保持原始状态。此时,查看cookie结果如下:

+
<RequestsCookieJar[
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for .gzlib.gov.cn/>, 
+<Cookie JSESSIONID=54702A995ECFC684B192A86467066F20 for www.gzlib.gov.cn/>, 
+<Cookie clientlanguage=zh_CN for www.gzlib.gov.cn/>, 
+<Cookie CASTGC=TGT-198235-zkocmYyBP6c9G7EXjKyzgKR7I40QI4JBalTkrnr9U6ZkxuP6Tn for www.gzlib.gov.cn/sso-server>, 
+<Cookie JSESSIONID=9918DDF929757B244456D4ECD2DAB2CB for www.gzlib.gov.cn/sso-server/>]>
+

其中,出现CASTGC表明登陆成功了,可以使用该cookie来访问广州图书馆的其他页面,在python中是直接跳转到其他页面,而在java使用httpclient过程中,看到的并不是直接的跳转,而是一个302重定向,打印Header之后结果如下图:

+
+
+
+

认真研究一下链接,就会发现服务器相当于给了一张通用票ticket,即:可以使用该ticket访问任何页面,而returnUrl则是返回的页面。这里我们直接访问该重定向的url。

+
Header header = response.getHeaders("Location")[0];
+CloseableHttpResponse home = get(header.getValue(), null);
+

然后打印页面,即可获取登陆之后跳回的首页。

+

3.8 解析html
+获取session并跳回首页之后,再访问借阅历史页面,然后对结果进行html解析,python中使用了BeautifulSoup,简单而又实用,java中的jsoup也是一个不错的选择。

+
        String html = getHTML();
+        Element element = Jsoup.parse(html).select("table.jieyue-table").get(0).select("tbody").get(0);
+        Elements trs = element.select("tr");
+        for (int i = 0; i < trs.size(); i++) {
+            Elements tds = trs.get(i).select("td");
+            System.out.println(tds.get(1).text());
+        }
+

输出结果:

+
企业IT架构转型之道
+大话Java性能优化
+深入理解Hadoop
+大话Java性能优化
+Java EE开发的颠覆者:Spring Boot实战
+大型网站技术架构:核心原理与案例分析
+Java性能权威指南
+Akka入门与实践
+高性能网站建设进阶指南:Web开发者性能优化最佳实践:Performance best practices for Web developers
+Java EE开发的颠覆者:Spring Boot实战
+深入理解Hadoop
+大话Java性能优化
+

点击查看源码

+

总结

+

目前,改代码已经整合进个人网站之中,每天定时抓取一次,但是仍有很多东西没有做(如分页、去重等),有兴趣的可以研究一下源码,要是能帮忙完善就更好了。感谢Thanks♪(・ω・)ノ。整个代码接近250行,当然...包括了注释,但是使用python之后,也不过25行=w=,这里贴一下python的源码吧。同时,欢迎大家访问我的个人网站,也欢迎大家能给个star

+
import urllib.parse
+import requests
+from bs4 import BeautifulSoup
+
+session = requests.session()
+session.get("http://www.gzlib.gov.cn/")
+session.headers.update(
+    {"Referer": "http://www.gzlib.gov.cn/member/historyLoanList.jspx",
+     "origin": "http://login.gzlib.gov.cn",
+     'Content-Type': 'application/x-www-form-urlencoded',
+     'host': 'www.gzlib.gov.cn',
+     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'
+     }
+)
+baseURL = "http://login.gzlib.gov.cn/sso-server/login"
+soup = BeautifulSoup(session.get(baseURL).text, "html.parser")
+lt = soup.select("form")[0].find(attrs={'name': 'lt'})['value']
+postdict = {"username": "你的身份证",
+            "password": "密码(默认为身份证后6位)",
+            "_eventId": "submit",
+            "lt": lt
+            }
+postdata = urllib.parse.urlencode(postdict)
+session.post(baseURL, postdata)
+print(session.get("http://www.gzlib.gov.cn/member/historyLoanList.jspx").text)
+
]]>
+ +
+ + AOP + http://www.wenzhihuai.com/java/SpringBoot/aop.html + http://www.wenzhihuai.com/java/SpringBoot/aop.html + AOP + AOP 一、概述 在通常的开发过程中,我们调用的顺序通常是controller-&gt;service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会... + Tue, 30 Jan 2024 06:37:05 GMT + 一、概述 +

在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务耗时过久,在aop出现之前,方式一般是在函数中开始写一个startTime,结尾再写一个endTime来查看执行该函数的耗时,过多的使用此类方式会导致代码的耦合性太高,不利于管理,于是,AOP(面向切面)出现了。AOP关注的是横向的,而OOP的是纵向。

+
+

Spring自2.0版本开始采用@AspectJ注解非常容易的定义一个切面。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。

+

1.1 特点

+

AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。AOP是OOP的补充,是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP可以分为静态织入与动态织入,静态织入即在编译前将需织入内容写入目标模块中,这样成本非常高。动态织入则不需要改变目标模块。Spring框架实现了AOP,使用注解配置完成AOP比使用XML配置要更加方便与直观。

+

1.2 AOP概述

+

Aspect:一个模块用来关注多个类的切面。在JAVA EE的应用中,事务是AOP的典型例子。
+Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
+Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义.
+Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
+Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.
+Target(目标对象):代理的目标对象
+Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.spring采用动态代理织入,而AspectJ采用编译期织入和类装在期织入.
+Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类Aspect(切面): 是切入点和通知(引介)的结合

+

二、Spring中的AOP

+

Spring实现AOP主要是由IOC容器来负责生成、管理的。其创建的方式有两种:

+
    +
  1. 默认使用Java动态代理来创建AOP代理;
  2. +
  3. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。高版本的Spring会自动选择是使用动态代理还是CGLIB生成代理内容,当然我们也可以强制使用CGLIB生成代理,那就是aop:config里面有一个"proxy-target-class"属性,这个属性值如果被设置为true,那么基于类的代理将起作用。
  4. +
+

2.1 AspectJ支持5种类型的通知注解:

+

[1] Before:前置通知,在方法执行之前执行
+[2] After:后置通知,在方法执行之后执行
+[3] AfterRunning:返回通知,在方法返回结果之后执行
+[4] AfterThrowing:异常通知,在方法抛出异常之后执行
+[5] Around:环绕通知,围绕着方法执行
+其中,环绕通知是最常见的一种通知注解,特别是在缓存的使用中,例如:Spring-Cache中的使用,在service的方法中添加一个cache的注解,通过AOP来拦截,如果缓存中已经存在,则直接返回结果,如果没有,再进行service的访问。

+

2.2 Spring提供了4种实现AOP的方式:

+
    +
  1. 经典的基于代理的AOP
  2. +
  3. @AspectJ注解驱动的切面
  4. +
  5. 纯POJO切面
  6. +
  7. 注入式AspectJ切面
  8. +
+

三、原理概述

+

Spring AOP的实现原理是基于动态织入的动态代理技术,而AspectJ则是静态织入,而动态代理技术又分为Java JDK动态代理和CGLIB动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现。Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入,这点了解一下即可。

+
+

具体的原理请看Spring AOP

+

四、使用

+

网上看别人写了很多入门的例子,自己就不再阐述了,毕竟自己还是菜,下面是关于AOP入门的资料:
+我们为什么要使用AOP?
+Spring中AOP的实现
+关于AOP

+

下面是自己在个人网站中的使用,主要是用来统计一个方法的执行消耗了多少时间,需要引入aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar的包。

+

4.1 在Spring MVC中开启AOP

+
    <!--自动扫描自定义切面-->
+    <aop:aspectj-autoproxy/>
+

4.2 定义一个切面

+
/**
+ * 可以使用 @Order 注解指定切面的优先级, 值越小优先级越高
+ */
+@Order(2)
+@Aspect
+@Component
+public class TimeInterceptor {
+}
+

4.3 声明一个切入点

+
    @Pointcut("execution(* com.myblog.service.impl.BlogServiceImpl.*(..))")
+    public void pointcut() {
+    }
+

4.4 声明一个前置切点

+
    @Before("pointcut()")
+    public void before(JoinPoint jp) {
+        logger.info(jp.getSignature().getName());
+        logger.info("
+
]]>
+ +
+ + Spring Boot Prometheus使用 + http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html + http://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html + Spring Boot Prometheus使用 + Spring Boot Prometheus使用 一、基本原理 Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exp... + Sat, 27 Jan 2024 13:52:01 GMT + 一、基本原理 +

Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用,比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。

+
image-20240127202704612
image-20240127202704612
+

二、具体过程

+
    +
  • Prometheus Daemon负责定时去目标上抓取metrics(指标)数据,每个抓取目标需要暴露一个http服务的接口给它定时抓取。Prometheus支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup等方式指定抓取目标。Prometheus采用PULL的方式进行监控,即服务器可以直接通过目标PULL数据或者间接地通过中间网关来Push数据。
  • +
  • Prometheus在本地存储抓取的所有数据,并通过一定规则进行清理和整理数据,并把得到的结果存储到新的时间序列中。
  • +
  • Prometheus通过PromQL和其他API可视化地展示收集的数据。Prometheus支持很多方式的图表可视化,例如Grafana、自带的Promdash以及自身提供的模版引擎等等。Prometheus还提供HTTP API的查询方式,自定义所需要的输出。
  • +
  • PushGateway支持Client主动推送metrics到PushGateway,而Prometheus只是定时去Gateway上抓取数据。
  • +
  • Alertmanager是独立于Prometheus的一个组件,可以支持Prometheus的查询语句,提供十分灵活的报警方式。
  • +
+

三、pull模式(prometheus主动拉取)

+
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-actuator</artifactId>
+</dependency>
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-registry-prometheus</artifactId>
+</dependency>
+
management:
+  endpoints:
+    web:
+      exposure:
+        include: prometheus
+  metrics:
+    tags:
+      application: ${spring.application.name}
+

之后查看/actuator/prometheus就可以看到

+
image-20240127202722776
image-20240127202722776
+

腾讯云上面有个prometheus的服务,接入云原生监控还要配置一个Servicemonitor、PodMonitor等,详细的可以访问腾讯云的官方文档(https://cloud.tencent.com/document/product/1416/56031)

+

四、主动上报(pushgateway)

+

大部分现实场景中,如果每增加一个服务,都需要开发去配置的话,不仅沟通成本高,也导致因配错而起的运维成本很高,采用主动上报方式比较简单,方便规避一些问题。

+

(1)引入库

+

主动上报除了需要引入spring-boot-starter-actuator、micrometer-registry-prometheus,还要引入simpleclient_pushgateway,三个都是必须的,少一个都不行。

+
        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.prometheus</groupId>
+            <artifactId>simpleclient_pushgateway</artifactId>
+        </dependency>
+

(2)修改配置文件

+

之后,为了安全考虑,需要把endpoints的所有信息都关闭掉,免得被泄露出去。

+
management:
+  endpoints:
+    #一定要设为false,防止不小心暴露/actuator相关接口触发安全工单
+    enabled-by-default: false
+  metrics:
+    tags:
+      #要设置应用的名称,否则会聚合到别的项目里面去
+      application: ${spring.application.name}
+    export:
+      prometheus:
+        pushgateway:
+          base-url: http://xxxxx
+          push-rate: 20s
+          job: ${spring.application.name}
+          enabled: true
+          username: xxxx
+          password: xxx
+          grouping-key:
+            instance: ${HOSTNAME}
+

最好多看一眼/actuator,确认一下是否已经关闭endpoints

+
image-20240127202830908
image-20240127202830908
+

(3)抓包看一下metrics(可跳过)

+

大致看了下 simpleclient_pushgateway和spring-boot-starter-actuator的源码,并没有对http的请求发起日志,调试的时候都不知道是不是正常上报过去了,只能采取抓包来研究下。

+
image-20240127202853218
image-20240127202853218
+
image-20240127202944264
image-20240127202944264
+

平均间隔5s左右,符合配置文件里的设置。

+
image-20240127203001776
image-20240127203001776
+

五、Grafana

+

对于java来说,常用的dashboard是https://grafana.com/grafana/dashboards/4701,也可以用spring boot的https://grafana.com/grafana/dashboards/6756

+
image-20240127203024477
image-20240127203024477
+

六、自定义监控上报

+

除了通用的封装好的指标之外,也可以自定义prometheus的监控。对于Spring Boot来说,只要如下代码即可实现:

+
    @Resource
+    private MeterRegistry meterRegistry;
+
+    public void report() {
+        meterRegistry.counter("指标", "tag的名称", "tag的值").increment();
+    }
+

也可以通过抓包来查看,以及在push-gateway上看到。

+]]>
+ +
+ + 小程序 + http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html + http://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html + 小程序 + 小程序 助眠风扇助眠风扇 MyTesMateMyTesMate + Sat, 27 Jan 2024 09:51:43 GMT + 助眠风扇
助眠风扇
+
MyTesMate
MyTesMate
+]]>
+ +
+ + Webflux + http://www.wenzhihuai.com/java/SpringBoot/webflux.html + http://www.wenzhihuai.com/java/SpringBoot/webflux.html + Webflux + Webflux 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? ChatGpt: Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。 Servlet 3.1:虽然 Servlet 3.1 支持非阻塞... + Sat, 27 Jan 2024 09:51:43 GMT + 1.Servlet 3.1就已经支持异步非阻塞,为什么要用webflux? +

ChatGpt:
+Servlet 3.1 和 WebFlux 虽然底层都使用了 Java 的 NIO(非阻塞 IO),但是他们的编程模型和如何使用 NIO 是不同的,这也是导致他们性能差异的原因。

+

Servlet 3.1:虽然 Servlet 3.1 支持非阻塞 IO,但是它的 API 设计仍然是基于传统的阻塞模型,这意味着每个请求通常会在一个单独的线程中处理,直到请求处理完成并返回响应。这种模型在并发请求数量较少的情况下工作得很好,但是在高并发的情况下,可能会因为线程数量过多而导致资源消耗过大,性能下降。

+

WebFlux:WebFlux 是基于反应式编程模型的,它使用少量的线程(通常与 CPU 核心数相同)和事件驱动的方式来处理大量的并发请求。这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

+

所以,虽然 Servlet 3.1 和 WebFlux 底层都使用了 NIO,但是由于他们的编程模型和如何使用 NIO 的方式不同,WebFlux 在处理高并发、流式数据等场景时,可能会有更好的性能。但是,这并不意味着 WebFlux 在所有情况下都比 Servlet 3.1 有更好的性能,具体还需要根据应用的实际需求和场景来选择合适的技术。

+

webflux和springboot虚拟线程对比

+

ChatGpt:
+WebFlux 和 Spring Boot 使用虚拟线程(也称为纤程或轻量级线程)是两种不同的并发模型,它们各自有各自的优点和适用场景。

+

WebFlux:WebFlux 是 Spring 5 的一部分,它提供了一个基于反应式编程模型的非阻塞 Web 框架。WebFlux 使用少量的线程和事件驱动的方式来处理大量的并发请求,这种模型可以更有效地利用系统资源,特别是在高并发的情况下。此外,WebFlux 还支持 backpressure(背压),可以更好地处理流式数据。

+

Spring Boot 使用虚拟线程:Java 在其后续版本(如 Java 16)中引入了虚拟线程(Project Loom)的概念,这是一种新的并发模型,它可以创建大量的虚拟线程,每个虚拟线程都有自己的调用栈,但是它们在操作系统级别并不是真正的线程,因此创建和切换的成本更低。虚拟线程使得开发者可以使用传统的阻塞式编程模型,但是在运行时,这些阻塞操作并不会阻塞真正的物理线程,从而可以处理大量的并发请求。

+

WebFlux 和 Spring Boot 使用虚拟线程的主要区别在于它们的编程模型:WebFlux 使用反应式编程模型,而虚拟线程则使用更传统的阻塞式编程模型。虚拟线程的优点是编程模型更简单,更接近传统的 Java 开发方式,但是它需要更新的 Java 版本,并且目前还在实验阶段。WebFlux 的优点是它可以在任何支持 Java 8 或更高版本的环境中运行,但是它需要开发者熟悉反应式编程模型。

+

至于哪种模型的性能更好,这取决于具体的应用场景和负载。在某些情况下,WebFlux 可能会有更好的性能,而在其他情况下,使用虚拟线程可能会更有效。

+]]>
+
+ + StarCraft Ⅱ 人工智能教程 + http://www.wenzhihuai.com/interesting/starcraft-ai.html + http://www.wenzhihuai.com/interesting/starcraft-ai.html + StarCraft Ⅱ 人工智能教程 + StarCraft Ⅱ 人工智能教程 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。 一、其他的太抽象了,先讲人机对战吧 sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的... + Fri, 26 Jan 2024 16:53:43 GMT + 非深度学习人士,仅仅是兴趣驱动,可能有很多不对的地方,也欢迎大家指正。这里主要讲解如何将AI运行起来、机器人对战、人机对战、天梯上分等等,希望能对大家的人工智能工程道路上有所帮助。

+

一、其他的太抽象了,先讲人机对战吧

+

sc2的wiki资料很全,可以从这里下载并运行,目前人机对战只能在win下运行,这边特别强调一下的就是,需要以管理员身份运行,下面详细过程,翻译自 ProBots 2021 Season 1 - Human vs Bot

+

1.安装星际争霸2,地址,至于要不要下载国际服,似乎没有必要
+2.下载ProBots vs Humans.Zip
+3.解压,附带了地图,主要是sc2aiapp
+

+

4.可选,下载相关地图,可以从竞技场里下,需要放到星际争霸2的目录下,mac的是/Applications/StarCraft II/Maps
+5.打开步骤2的目录
+6.打开sc2aiapp,打开的时候有可能报错:

+
+

右键sc2aiapp,以管理员身份运行即可,现在不让注册了,直接continue without login

+
+
+

7.全屏快捷键,Alt + Enter,进行对战

+

我这录制了个我对战的视频,bilibili,感觉AI在对战里很容易只有一样打法,据说是强化训练后的最优选导致的,这个也不知怎么整,个人感觉MicroMachine这个AI打法稍微多样,可以多和它对战下。

+

二、AI天梯

+

目前没有看到什么办法让暴雪允许AI在实际的天梯上进行运行,但社区搞了个专门的AI天梯,sc2ai,可以将代码上传到里面进行对战,实时流我没看到,对战完后可以下载replay复盘。下面讲下如何上传代码进行对战。
+1.第一步肯定是先要注册登录
+2.u

+
+

3.主要是这个Bot zip,基本的代码架构还是要固定的

+
+

具体可以看下sc2-api-simple-bot这里,记得把它打包即可
+4.成功之后,即可从profile里看到自己的机器人
+

+

5.此时,bot是不会进行比赛,需要参赛,点击Competitions,然后选择赛季
+

+
+

6.比赛是随机的放到队列里的,可能需要排队进行比赛,也可能主动申请和具体的机器人进行比赛,点击Request Match,进行申请比赛。
+

+

7.慢慢等待,比赛结束之前都看不到结果的,也没有实时流进行查看的,结束之后就可以看到结果以及下载replay。其中arena会随机的进行一些比赛,也有可能是别人随机选的,一个bot一天大概能有50场比赛,arena也会提供统计,胜率、ELO(分数)等

+

Bot开发样例

+

https://github.com/Zephery/sc2-api-simple-bot.git
+https://community.eschamp.com/t/simple-starcraft-2-bot-template-to-get-started/155

+]]>
+ +
+ + 自我介绍 + http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html + http://www.wenzhihuai.com/about-the-author/personal-life/wewe.html + 自我介绍 + 自我介绍 安安静静的开发,搞点好玩的。 硅谷硅谷 + Fri, 26 Jan 2024 04:53:47 GMT + 安安静静的开发,搞点好玩的。

+
硅谷
硅谷
+]]>
+ +
+ + 关于我 + http://www.wenzhihuai.com/about-the-author/ + http://www.wenzhihuai.com/about-the-author/ + 关于我 + 关于我 留空 + Thu, 25 Jan 2024 17:34:45 GMT + 留空

+]]>
+
+ + Zookeeper + http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html + http://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html + Zookeeper + Zookeeper 留空 + Thu, 25 Jan 2024 17:34:45 GMT + 留空

+]]>
+
+ + 微信公众号-chatgpt智能客服搭建 + http://www.wenzhihuai.com/interesting/chatgpt.html + http://www.wenzhihuai.com/interesting/chatgpt.html + 微信公众号-chatgpt智能客服搭建 + 微信公众号-chatgpt智能客服搭建 想体验的可以去微信上搜索【旅行的树】公众号。 一、ChatGPT注册 1.1 短信手机号申请 openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。 国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。 国外手机号,没有的话也可以去https:/... + Thu, 25 Jan 2024 12:42:18 GMT + 想体验的可以去微信上搜索【旅行的树】公众号。

+

一、ChatGPT注册

+

1.1 短信手机号申请

+

openai提供服务的区域,美国最好,这个解决办法是搞个翻墙,或者买一台美国的服务器更好。

+

国外邮箱,hotmail或者google最好,qq邮箱可能会对这些平台进行邮件过滤。

+

国外手机号,没有的话也可以去https://sms-activate.org,费用大概需要1美元,这个网站记得也用国外邮箱注册,需要先充值,使用支付宝支付。

+
image-20230220172107296
+

之后再搜索框填openai进行下单购买即可。

+
img
+

1.2 云服务器申请

+

openai在国内不提供服务的,而且也通过ip识别是不是在国内,解决办法用vpn也行,或者,自己去买一台国外的服务器也行。我这里使用的是腾讯云轻量服务器,最低配置54元/月,选择windows的主要原因毕竟需要注册openai,需要看页面,同时也可以搭建nginx,当然,用ubuntu如果能自己搞界面也行。

+image-20230221193455384 +

1.3 ChatGPT注册

+

购买完之后,就可以直接打开openai的官网了,然后去https://platform.openai.com/signup官网里注册,注册过程具体就不讲了,讲下核心问题——短信验证码

+
image-20230222104357230
+

然后回sms查看验证码。

+
image-20230222104225722
+

注册成功之后就可以在chatgpt里聊天啦,能够识别各种语言,发起多轮会话的时候,可能回出现访问超过限制什么的。

+image-20230220173335691 +

通过chatgpt聊天不是我们最终想要的,我们需要的是在微信公众号也提供智能客服的聊天回复,所以我们需要在通过openai的api来进行调用。

+
image-20230222104316514
+

二、搭建nginx服务器

+

跟页面一样,OpenAI的调用也是不能再国内访问的,这里,我们使用同一台服务器来搭建nginx,还是保留使用windows吧,主要还是得注意下面这段话,如果API key被泄露了,OpenAI可能会自动重新更新你的API key,这个规则似乎是API key如果被多个ip使用,就会触发这个规则,调试阶段还是尽量使用windows的服务器吧,万一被更新了,还能去页面上重新找到。

+
Do not share your API key with interesting, or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may also automatically rotate any API key that we've found has leaked publicly.
+

windows的安装过程参考网上的来,我们只需要添加下面这个配置即可,原理主要是将调用OpenAI的接口全部往官网转发。

+
    location /v1/completions {
+      proxy_pass https://api.openai.com/v1/completions;
+    }
+

然后使用下面的方法进行调试即可:

+
POST http://YOUR IP/v1/completions
+Authorization: Bearer YOUR_API_KEY
+Content-Type: application/json
+
+{
+  "model": "text-davinci-003",
+  "prompt": "Say this is a test",
+  "max_tokens": 7,
+  "temperature": 0
+}
+

三、公众号开发

+

网上有很多关于微信通过chatgpt回复的文章,有些使用自己微信号直接做为载体,因为要扫码网页登陆,而且是网页端在国外,很容易被封;有些是使用公众号,相对来说,公众号被封也不至于导致个人微信号出问题。

+

3.1 微信云托管

+

微信公众平台提供了微信云托管,无需鉴权,比其他方式都方便不少,可以免费试用3个月,继续薅羊毛,当然,如果自己开发能力足够,也可以自己从0开始开发。

+
image-20230220192603751
+

提供了各种语言的模版,方便快速开发,OpenAI官方提供的sdk是node和python,这里我们选择express(node)。

+image-20230220201004069 +

3.2 一个简单ChatGPT简单回复

+

微信官方的源码在这,https://github.com/WeixinCloud/wxcloudrun-express,我们直接fork一份自己来开发。

+

一个简单的消息回复功能(无db),直接在我们的index.js里添加如下代码。

+
const configuration = new Configuration({
+    apiKey: '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',
+    basePath: 'http://43.153.15.174/v1'
+});
+const openai = new OpenAIApi(configuration);
+
+async function simpleResponse(prompt) {
+    const completion = await openai.createCompletion({
+        model: 'text-davinci-003',
+        prompt,
+        max_tokens: 1024,
+        temperature: 0.1,
+    });
+    const response = (completion?.data?.choices?.[0].text || 'AI 挂了').trim();
+    return strip(response, ['\n', 'A: ']);
+}
+
+app.post("/message/simple", async (req, res) => {
+    console.log('消息推送', req.body)
+    // 从 header 中取appid,如果 from-appid 不存在,则不是资源复用场景,可以直接传空字符串,使用环境所属账号发起云调用
+    const appid = req.headers['x-wx-from-appid'] || ''
+    const {ToUserName, FromUserName, MsgType, Content, CreateTime} = req.body
+    console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
+    if (MsgType === 'text') {
+        message = await simpleResponse(Content)
+        res.send({
+            ToUserName: FromUserName,
+            FromUserName: ToUserName,
+            CreateTime: CreateTime,
+            MsgType: 'text',
+            Content: message,
+        })
+    } else {
+        res.send('success')
+    }
+})
+

本地可以直接使用http请求测试,成功调用之后直接提交发布即可,在微信云托管上需要先授权代码库,即可使用云托管的流水线,一键发布。注意,api_base和api_key可以填写在服务设置-基础信息那里,一个是OPENAI_API_BASE,一个是OPENAI_API_KEY,服务会自动从环境变量里取。

+

3.3 服务部署

+

提交代码只github或者gitee都可以,值得注意的是,OpenAI判断key泄露的规则,不知道是不是判断调用的ip地址不一样,还是github的提交记录里含有这块,有点玄学,同样的key本地调用一次,然后在云托管也调用的话,OpenAI就很容易把key给重新更新。

+image-20230220203313790 +

部署完之后,云托管也提供了云端调试功能,相当于在服务里发送了http请求。这一步很重要,如果没有调用成功,则无法进行云托管消息推送。

+
image-20230220203711436
+

这里填上你自己的url,我们这里配置的是/meesage/simple,如果没有成功,需要进行下面步骤进行排查:

+

(1)服务有没有正常启动,看日志

+

(2)端口有没有设置错误,这个很多次没有注意到

+image-20230220203445297 +

保存成功之后,就可以在微信公众号里测试了。

+
image-20230221134250689
+

体验还可以

+

四、没有回复(超时回复问题)

+

很多OpenAI的回答都要几十秒,有的甚至更久,比如对chatgpt问“写一篇1000字关于深圳的文章”,就需要几十秒,而微信的主动回复接口,是需要我们3s内返回给用户。

+

订阅号的消息推送分几种:

+
    +
  1. 被动消息回复:指用户给公众号发一条消息,系统接收到后,可以回复一条消息。
  2. +
  3. 主动回复/客服消息:可以脱离被动消息的5秒超时权限,在48小时内可以主动回复。但需要公众号完成微信认证。
  4. +
+

根据微信官方文档,没有认证的公众号是没有调用主动回复接口权限的,https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Explanation_of_interface_privileges.html

+image-20230221100251825 +

对于有微信认证的订阅号或者服务号,可以调用微信官方的/cgi-bin/message/custom/send接口来实现主动回复,但是对于个人的公众号,没有权限调用,只能尝试别的办法。想来想去,只能在3s内返回让用户重新复制发送的信息,同时后台里保存记录异步调用,用户重新发送的时候再从数据库里提取回复。

+

1.先往数据库存一条 回复记录,把用户的提问存下来,以便后续查询。设置回复的内容为空,设置状态为 回复中(thinking)。

+
  // 因为AI响应比较慢,容易超时,先插入一条记录,维持状态,待后续更新记录。
+  await Message.create({
+    fromUser: FromUserName,
+    response: '',
+    request: Content,
+    aiType: AI_TYPE_TEXT, // 为其他AI回复拓展,比如AI作画
+  });
+

2.抽象一个 chatGPT 请求方法 getAIMessage,函数内部得到 GPT 响应后,会更新之前那条记录(通过用户id & 用户提问 查询),把状态更新为 已回答(answered),并把回复内容更新上。

+
 // 成功后,更新记录
+  await Message.update(
+    {
+      response: response,
+      status: MESSAGE_STATUS_ANSWERED,
+    },
+    {
+      where: {
+        fromUser: FromUserName,
+        request: Content,
+      },
+    },
+  );
+

3.前置增加一些判断,当用户在请求时,如果 AI 还没完成响应,直接回复用户 AI 还在响应,让用户过一会儿再重试。如果 AI 此时已响应完成,则直接把 内容返回给用户。

+
  // 找一下,是否已有记录
+  const message = await Message.findOne({
+    where: {
+      fromUser: FromUserName,
+      request: Content,
+    },
+  });
+
+  // 已回答,直接返回消息
+  if (message?.status === MESSAGE_STATUS_ANSWERED) {
+    return `[GPT]: ${message?.response}`;
+  }
+
+  // 在回答中
+  if (message?.status === MESSAGE_STATUS_THINKING) {
+    return AI_THINKING_MESSAGE;
+  }
+

4.最后就是一个 Promise.race

+
  const message = await Promise.race([
+    // 3秒微信服务器就会超时,超过2.9秒要提示用户重试
+    sleep(2900).then(() => AI_THINKING_MESSAGE),
+    getAIMessage({ Content, FromUserName }),
+  ]);
+

这样子大概就能实现超时之前返回了。

+

五、会话保存

+

掉接口是一次性的,一次接口调用完之后怎么做到下一次通话的时候,还能继续保持会话,是不是应该类似客户端与服务端那种有个session这种,但是实际上在openai里是没有session这种东西的,令人震惊的是,chatgpt里是通过前几次会话拼接起来一起发送给chatgpt里的,需要通过回车符来拼接。

+
async function buildCtxPrompt({ FromUserName }) {
+  // 获取最近10条对话
+  const messages = await Message.findAll({
+    where: {
+      fromUser: FromUserName,
+      aiType: AI_TYPE_TEXT,
+    },
+    limit: 10,
+    order: [['updatedAt', 'ASC']],
+  });
+  // 只有一条的时候,就不用封装上下文了
+  return messages.length === 1
+    ? messages[0].request
+    : messages
+        .map(({ response, request }) => `Q: ${request}\n A: ${response}`)
+        .join('\n');
+}
+

之后就可以实现会话之间的保存通信了。

+
image-20230218203309437
+

六、其他问题

+

6.1 限频

+

chatgpt毕竟也是新上线的,火热是肯定的,聊天窗口只能开几个,api调用的话,也是有限频的,但是规则具体没有找到,只是在调用次数过多的时候会报429的错误,出现之后就需要等待一个小时左右。

+

对于这个的解决办法只能是多开几个账号,一旦429就只能换个账号重试了。

+

6.2 秘钥key被更新

+

没有找到详细的规则,凭个人经验的话,可能github提交的代码会被扫描,可能ip调用的来源不一样,最好还是开发一个秘钥,生产一个秘钥吧。

+

6.3 为啥和官网的回复不一样

+

我们这里用的模型算法是text-davinci-003,具体可以参考:https://platform.openai.com/docs/models/overview,也算是一个比较老的样本了吧

+image-20230221192417900 +

从官方文档来看,官方服务版的 ChatGPT 的模型并非基础版的text-davinci-003,而是经过了「微调:fine-tunes」。文档地址在这:platform.openai.com/docs/guides…

+

6.4 玄学挂掉

+

有时候消息没有回复,真的不是我们的问题,chatgpt毕竟太火了,官网的这个能力都经常挂掉,也可以订阅官网修复的通知,一旦修复则会发邮件告知你。

+image-20230221175150025 +

参考:https://juejin.cn/post/7200769439335546935

+

最后

+

记得去微信关注【旅行的树】公众号体验
+代码地址:https://github.com/Zephery/wechat-gpt

+]]>
+
+ + Tesla api + http://www.wenzhihuai.com/interesting/tesla.html + http://www.wenzhihuai.com/interesting/tesla.html + Tesla api + Tesla api 一、特斯拉应用申请 1.1 创建 Tesla 账户 如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。 正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator. image-20231210142130054 image-2023121014241559... + Thu, 25 Jan 2024 12:42:18 GMT + 一、特斯拉应用申请 +

1.1 创建 Tesla 账户

+

如果您还没有 Tesla 账户,请创建账户。验证您的电子邮件并设置多重身份验证。

+

正常创建用户就好,然后需要开启多重身份认证,这边常用的是mircrosoft的Authenticator.

+image-20231210142130054 +image-20231210142415598 +

注意点:(1)不要用自己车辆的邮箱来注册(2)有些邮箱不是特斯拉开发者的邮箱,可能用这些有些无法正常提交访问请求。

+

1.2 提交访问请求

+

点击下方的“提交请求”按钮,请求应用程序访问权限。登录后,请提供您的合法企业详细信息、应用程序名称及描述和使用目的。在您提交了详细信息之后,我们会审核您的请求并通过电子邮件向您发送状态更新。

+

这一步很坑,多次尝试之后都无果,原因也不知道是为啥,只能自己去看返回报文琢磨,太难受了,下面是自己踩的坑

+

(1)Invalid domain

+image-20231210144504486 +

无效的域名,这里我用的域名是腾讯云个人服务器的域名,证书是腾讯云免费一年的证书,印象中第一申请的时候还是能过的,第二次的时候就不行了,可能被识别到免费的ssl证书不符合规范,还是需要由合法机构的颁发证书才行。所以,为了金快速申请通过,先填个https://baidu.com吧。当然,后续需要彻底解决自己域名证书的问题,我改为使用阿里云的ssl证书,3个月到期的那种。

+

(2)Unable to Onboard

+image-20231210142930976 +

应用无法上架,可能原因为邮箱不对,用了之前消费者账号(即自己的车辆账号),建议换别的邮箱试试。

+

(3) Rejected

+image-20231210143156308 +

这一步尝试了很多次,具体原因为国内还无法正常使用tesla api,只能切换至美国服务器申请下(截止2023-11-15),后续留意官网通知。

+

1.3 访问应用程序凭据

+

一旦获得批准,将为您的应用程序生成可在登录后访问的客户端 ID 和客户端密钥。使用这些凭据,通过 OAuth 2.0 身份验证获取用户访问令牌。访问令牌可用于对提供私人用户账户信息或代表其他账户执行操作的请求进行身份验证。

+

申请好就可以在自己的账号下看到自己的应用了,

+image-20231210144244888 +

1.4 开始 API 集成

+

按照 API 文档和设置说明将您的应用程序与 Tesla 车队 API 集成。您需要生成并注册公钥,请求用户授权并按照规格要求拨打电话。完成后您将能够与 API 进行交互,并开始围绕 Tesla 设备构建集成。

+

二、开发之前的准备

+

由于特斯拉刚推出,并且国内进展缓慢,很多都申请不下来,下面内容均以北美区域进行调用。

+

2.1 认识token

+

app与特斯拉交互的共有两个令牌(token)方式,在调用api的时候,特别需要注意使用的是哪种token,下面是两种token的说明:

+

(1)合作伙伴身份验证令牌:这个就是你申请的app的令牌

+

(2)客户生成第三方令牌:这个是消费者在你这个app下授权之后的令牌。

+

此外,还需要注意下,中国大陆地区对应的api地址是 https://fleet-api.prd.cn.vn.cloud.tesla.cn,不要调到别的地址去了。

+

2.2 获取第三方应用token

+

这一步官网列的很详细,就不在详述了。

+
CLIENT_ID=<command to obtain a client_id>
+CLIENT_SECRET=<secure command to obtain a client_secret>
+AUDIENCE="https://fleet-api.prd.na.vn.cloud.tesla.com"
+# Partner authentication token request
+curl --request POST \
+  --header 'Content-Type: application/x-www-form-urlencoded' \
+  --data-urlencode 'grant_type=client_credentials' \
+  --data-urlencode "client_id=$CLIENT_ID" \
+  --data-urlencode "client_secret=$CLIENT_SECRET" \
+  --data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \
+  --data-urlencode "audience=$AUDIENCE" \
+  'https://auth.tesla.com/oauth2/v3/token'
+

2.3 验证域名归属

+

2.1.1 Register

+

完成注册合作方账号之后才可以访问API. 每个developer.tesla.cn上的应用程序都必须完成此步骤。官网挂了一个github的代码,看了半天,以为要搞很多东西,实际上只需要几步就可以了。

+
cd vehicle-command/cmd/tesla-keygen
+go build .
+./tesla-keygen -f -keyring-debug -key-file=private create > public_key.pem
+

这里只是生成了公钥,需要把公钥挂载到域名之下,我们用的是nginx,所以只要指向pem文件就可以了,注意下nginx是否可以访问到改文件,如果不行,把nginx的user改为root。

+
    location ~ ^/.well-known {
+           default_type text/html;
+           alias /root/vehicle-command/cmd/tesla-keygen/public_key.pem;
+    }
+
+

随后,便是向tesla注册你的域名,域名必须和你申请的时候填的一样。

+
curl --header 'Content-Type: application/json' \
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \
+  --data '{"domain":"string"}' \
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts'
+

2.1.2 public_key

+

最后,验证一下是否真的注册成功(GET /api/1/partner_accounts/public_key)

+
curl --header 'Content-Type: application/json' \
+  --header "Authorization: Bearer $TESLA_API_TOKEN" \
+  'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/partner_accounts/public_key'
+

得到下面内容就代表成功了

+
{"response":{"public_key":"xxxx"}}
+

至此,我们的开发准备就完成了,接下来就是正常的开发与用户交互的api。

+]]>
+
+ + 在 Spring 6 中使用虚拟线程 + http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html + http://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html + 在 Spring 6 中使用虚拟线程 + 在 Spring 6 中使用虚拟线程 一、简介 在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。 虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。 ... + Thu, 25 Jan 2024 12:42:18 GMT + 一、简介 +

在这个简短的教程中,我们将了解如何在 Spring Boot 应用程序中利用虚拟线程的强大功能。

+

虚拟线程是Java 19 的预览功能,这意味着它们将在未来 12 个月内包含在官方 JDK 版本中。Spring 6 版本最初由 Project Loom 引入,为开发人员提供了开始尝试这一出色功能的选项。

+

首先,我们将看到“平台线程”和“虚拟线程”之间的主要区别。接下来,我们将使用虚拟线程从头开始构建一个 Spring-Boot 应用程序。最后,我们将创建一个小型测试套件,以查看简单 Web 应用程序吞吐量的最终改进。

+

二、 虚拟线程与平台线程

+

主要区别在于虚拟线程在其操作周期中不依赖于操作系统线程:它们与硬件解耦,因此有了“虚拟”这个词。这种解耦是由 JVM 提供的抽象层实现的。

+

对于本教程来说,必须了解虚拟线程的运行成本远低于平台线程。它们消耗的分配内存量要少得多。这就是为什么可以创建数百万个虚拟线程而不会出现内存不足问题,而不是使用标准平台(或内核)线程创建几百个虚拟线程。

+

从理论上讲,这赋予了开发人员一种超能力:无需依赖异步代码即可管理高度可扩展的应用程序。

+

三、在Spring 6中使用虚拟线程

+

从 Spring Framework 6(和 Spring Boot 3)开始,虚拟线程功能正式公开,但虚拟线程是Java 19 的预览功能。这意味着我们需要告诉 JVM 我们要在应用程序中启用它们。由于我们使用 Maven 来构建应用程序,因此我们希望确保在 pom.xml 中包含以下代码

+
<build>
+    <plugins>
+        <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+                <source>19</source>
+                <target>19</target>
+                <compilerArgs>
+                    --enable-preview
+                </compilerArgs>
+            </configuration>
+        </plugin>
+    </plugins>
+</build>
+

从 Java 的角度来看,要使用 Apache Tomcat 和虚拟线程,我们需要一个带有几个 bean 的简单配置类:

+
@EnableAsync
+@Configuration
+@ConditionalOnProperty(
+  value = "spring.thread-executor",
+  havingValue = "virtual"
+)
+public class ThreadConfig {
+    @Bean
+    public AsyncTaskExecutor applicationTaskExecutor() {
+        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
+    }
+
+    @Bean
+    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
+        return protocolHandler -> {
+            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+        };
+    }
+}
+

第一个 Spring Bean ApplicationTaskExecutor将取代标准的*ApplicationTaskExecutor* ,提供为每个任务启动新虚拟线程的Executor。第二个 bean,名为ProtocolHandlerVirtualThreadExecutorCustomizer,将以相同的方式 自定义标准TomcatProtocolHandler 。我们还添加了注释@ConditionalOnProperty,**以通过切换application.yaml文件中配置属性的值来按需启用虚拟线程:

+
spring:
+    thread-executor: virtual
+    //...
+

我们来测试一下Spring Boot应用程序是否使用虚拟线程来处理Web请求调用。为此,我们需要构建一个简单的控制器来返回所需的信息:

+
@RestController
+@RequestMapping("/thread")
+public class ThreadController {
+    @GetMapping("/name")
+    public String getThreadName() {
+        return Thread.currentThread().toString();
+    }
+}
+

*Thread*对象的toString *()*方法将返回我们需要的所有信息:线程 ID、线程名称、线程组和优先级。让我们通过一个curl请求来访问这个端点:

+
$ curl -s http://localhost:8080/thread/name
+$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4
+

正如我们所看到的,响应明确表示我们正在使用虚拟线程来处理此 Web 请求。换句话说,*Thread.currentThread()*调用返回虚拟线程类的实例。现在让我们通过简单但有效的负载测试来看看虚拟线程的有效性。

+

四、性能比较

+

对于此负载测试,我们将使用JMeter。这不是虚拟线程和标准线程之间的完整性能比较,而是我们可以使用不同参数构建其他测试的起点。

+

在这种特殊的场景中,我们将调用Rest Controller中的一个端点,该端点将简单地让执行休眠一秒钟,模拟复杂的异步任务:

+
@RestController
+@RequestMapping("/load")
+public class LoadTestController {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);
+
+    @GetMapping
+    public void doSomething() throws InterruptedException {
+        LOG.info("hey, I'm doing something");
+        Thread.sleep(1000);
+    }
+}
+

请记住,由于*@ConditionalOnProperty* 注释,我们只需更改 application.yaml 中变量的值即可在虚拟线程和标准线程之间切换

+

JMeter 测试将仅包含一个线程组,模拟 1000 个并发用户访问*/load* 端点 100 秒:

+
image-20230827193431807
image-20230827193431807
+

在本例中,采用这一新功能所带来的性能提升是显而易见的。让我们比较不同实现的“响应时间图”。这是标准线程的响应图。我们可以看到,立即完成一次调用所需的时间达到 5000 毫秒:

+
image-20230827193511176
image-20230827193511176
+

发生这种情况是因为平台线程是一种有限的资源,当所有计划的和池化的线程都忙时,Spring 应用程序除了将请求搁置直到一个线程空闲之外别无选择。

+

让我们看看虚拟线程会发生什么:

+
image-20230827193533565
image-20230827193533565
+

正如我们所看到的,响应稳定在 1000 毫秒。虚拟线程在请求后立即创建和使用,因为从资源的角度来看它们非常便宜。在本例中,我们正在比较 spring 默认固定标准线程池(默认为 200)和 spring 默认无界虚拟线程池的使用情况。

+

**这种性能提升之所以可能,是因为场景过于简单,并且没有考虑 Spring Boot 应用程序可以执行的全部操作。**从底层操作系统基础设施中采用这种抽象可能是有好处的,但并非在所有情况下都是如此。

+]]>
+ +
+ + 【elasticsearch】源码debug + http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html + http://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html + 【elasticsearch】源码debug + 【elasticsearch】源码debug 一、下载源代码 直接用idea下载代码https://github.com/elastic/elasticsearch.git image 切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好 image 二、修改设置(可选) ... + Thu, 25 Jan 2024 12:42:18 GMT + 一、下载源代码 +

直接用idea下载代码https://github.com/elastic/elasticsearch.git
+image

+

切换到特定版本的分支:比如7.17,之后idea会自己加上Run/Debug Elasitcsearch的,配置可以不用改,默认就好
+image

+

二、修改设置(可选)

+

为了方便, 在 gradle/run.gradle 中关闭 Auth 认证:

+

setting 'xpack.security.enabled', 'false'

+

或者使用其中的用户名密码:

+

user username: 'elastic-admin', password: 'elastic-password', role: 'superuser'

+

三、启动

+

先启动上面的 remote debug, 然后用 gradlew 启动项目:

+

./gradlew :run --debug-jvm
+打开浏览器http://localhost:9200即可看到es相关信息了
+image

+]]>
+ +
+ + Database + http://www.wenzhihuai.com/database/ + http://www.wenzhihuai.com/database/ + Database + Database 目录留空 + Thu, 25 Jan 2024 11:42:16 GMT + 目录留空

+]]>
+
+ + 微信支付 + http://www.wenzhihuai.com/donate/ + http://www.wenzhihuai.com/donate/ + 微信支付 + 微信支付 微信支付微信支付 + Thu, 25 Jan 2024 08:04:11 GMT + 微信支付
微信支付
+]]>
+ +
+ + 10.历时8年最终改版 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html + 10.历时8年最终改版 + 10.历时8年最终改版 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。... + Thu, 25 Jan 2024 06:44:04 GMT + 不知不觉,自建站https://www.wenzhihuai.com已经接近8年了,大二的时候开启使用ssh+jsp框架来做了一个自己的网站,完全自写前后端,过程中不断进化,改用ssm,整合es做文章搜索,加kafka,加redis缓存,整体上对个人来说还是学习到了不少东西。但是随之而来的问题也不少,被挖矿攻击、服务器被黑等等。有时候因为要用服务器搞一些别的东西,直接没有备份数据库就重装,导致不少文章丢失,虽然别的平台可能零散分布。

+
早年的网站截图
早年的网站截图
+

总体而言,自建站对学习知识,了解整个建站的原理能够起到非常重要的作用,但是维护成本实在是太高了,每个月要支付服务器的费用,而且一旦想拿服务器来做点什么,都得提防一下会不会造成破坏。最终还是选择采用vuepress2来重构一下自建站,毕竟把markdown放到github,把图片放到cos里减少了不少的维护量。下面是使用vuepress2建站的代码地址

+

一、博客的安装

+

具体vuepress2官网讲解的很详细了,不用再处理什么,按照步骤创建一个项目即可,为了网站的美观,个人使用了theme hope这款主题。

+

二、配置

+

导航栏、侧边栏官网也有详细的讲解,也不再阐述,需要注意的是自动目录,之前看JavaGuide的样式,他那边的每篇文章都需要写一次ts文件(children),后来发现官网可以把children设置为structure,即可实现根据md文件生成侧边栏目录。注意的是,这里不是根据markdown的文件名来目录名,而是取markdown文件的标题。

+
    {
+        text: "Redis",
+        prefix: "redis/",
+        icon: "redis",
+        collapsible: false,
+        children: "structure"
+    },
+

三、为文章增加评论

+

vuepress-plugin-comment2,使用了Giscus,Giscus绑定了github账号,所以可以从一定程度上防止被别人刷广告,需要再个人的项目Settings->General把Discussions这个选项给勾选上。

+
一定要开启discussions
一定要开启discussions
+

然后去config.ts配置插件。

+
        commentPlugin({
+            provider: "Giscus",
+            comment: true, //启用评论功能
+            repo: "Zephery/MyWebsite", //远程仓库
+            repoId: "MDEwOlJlcG9zaXRvcnkyMDM2MDIyMDQ=", //对应自己的仓库Id
+            category: "General",
+            categoryId: "DIC_kwDODCK5HM4Ccp32" //对应自己的分类Id
+        }),
+

即可在页面上看到效果

+
颜色可自定义
颜色可自定义
+

四、博客的部署

+

官网也有讲解部署的情况,具体可以看官网Github Pages,整体上看速度还是挺慢的,可以尝试去gitee上部署看一下,之后就可以在pages通过域名访问了。需要在项目下创建.github/workflows/docs.yml文件,具体配置参考官网,不需做任何改动。

+

五、Github pages自定义域名

+

github自带的io域名zephery.github.io,做为一名开发,肯定是用自己的域名是比较好的。需要注意下中间的红色框,前面的是分支,后面的是你项目的路径。一般默认即可,不用修改。

+
pages的配置
pages的配置
+

购买域名->域名解析,即把我的个人域名wenzhihuai.com指向zephery.github.io(通过cname)即可,然后开启强制https。如果DNS一直没有校验通过,那么可能是CAA的原因。通过DNS诊断工具来判断。

+
DNS诊断工具
DNS诊断工具
+

上面的custom domain配置好了之后,但DNS一直没有校验正确,原因是CAA没有正确解析,需要加上即可。

+
0 issue "trust-provider.com"
+0 issuewild "trust-provider.com"
+
CAA记录解析
CAA记录解析
+

之后就可以看到Github Pages的DNS校验成功,并且可以强制开启https了。

+

六、Typora图床

+

之前的图床使用的是七牛云和又拍云,都有免费的额度吧,不过看情况未来前景似乎经营不太好,目前改用了腾讯云。存储容量50GB,每个月外网访问流量10GB,满足个人网站使用。具体的配置过程比较简单,就不再阐述了,可以直接看uPic的官方介绍。

+

七、为自己的内容增加收入

+

有钱才有写作的动力,之前的网站开启了几年的捐赠,总共都没有收到过50块钱,只能从广告这一处想想办法,百度、腾讯广告似乎都不支持个人网站,谷歌可以。配置谷歌广告,网上的教程不少,例如: vuepress配置谷歌广告-通过vue-google-adsense库,缺点是,大部分的文章都是需要在自己的markdown文件中新增特定的标识符。比如:

+
# js 模板引擎 mustache 用法
+
+<ArticleTopAd></ArticleTopAd>
+
+## 一. 使用步骤
+

每一篇文章都要新增显然不符合懒惰的人,下面是个人尝试的解决办法,用的是vuepress2提供的slots插槽。

+
谷歌广告adsense
谷歌广告adsense
+

上面的目的是为了获取data-ad-client和data-ad-slot,其中,data-ad-slot为广告单元,不一样。并且,配置完之后可能需要等一个小时才会生效,不要着急。

+

docs/.vuepress/config.ts

+
head: [
+    [
+        "script",
+        {
+            "data-ad-client": "ca-pub-9037099208128116",
+            async: true,
+            src: "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
+        }
+    ],
+]
+
+...
+alias: {
+    "@theme-hope/components/NormalPage": path.resolve(
+        __dirname,
+        "./components/NormalPage.vue",
+    ),
+},
+

docs/.vuepress/components/NormalPage.vue

+
<template>
+  <normal-page>
+    <template #contentBefore>
+      <ins class="adsbygoogle"
+           style="display:block; text-align:center;width: 90%;margin: 0 auto;"
+           data-ad-layout="in-article"
+           data-ad-format="fluid"
+           data-ad-client="ca-pub-9037099208128116"
+           data-ad-slot="8206550629"></ins>
+    </template>
+  </normal-page>
+</template>
+<script>
+import NormalPage from "vuepress-theme-hope/components/NormalPage.js";
+
+export default {
+  name: "adsense-inline",
+  components: {
+    'normal-page': NormalPage,
+  },
+  mounted() {
+    this.adsenseAddLoad();
+  },
+  methods: {
+    adsenseAddLoad() {
+      let inlineScript = document.createElement("script");
+      inlineScript.type = "text/javascript";
+      inlineScript.text = '(adsbygoogle = window.adsbygoogle || []).push({});'
+      document.getElementsByTagName('body')[0].appendChild(inlineScript);
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+</style>
+

本地是没办法进行调试的,可以从官网插槽演示的文章中用div进行调试,等修改完毕发布之后,即可在自己的网站上看到相关的广告和收入(浏览器要把封禁广告的插件关闭)。

+
出现的广告
出现的广告
+
谷歌广告收入
谷歌广告收入
+

收入虽然低,但是基本上个人没有成本,只需要域名的85块钱。

+

常见问题

+

为什么广告不能正常显示?

+

"建议多写原创高质量的文章出来,AdSense才会匹配出合适的广告,用户感兴趣了才会浏览量增加,你才会有更多的广告收入。"

+

还是得多写一写优质的文章。

+

最后,多帮忙点一下个人网站的广告吧,感恩

+

网站地址:https://www.wenzhihuai.com

+

源码地址:https://github.com/Zephery/MyWebsite

+]]>
+ +
+ + + http://www.wenzhihuai.com/open-source-project/ + http://www.wenzhihuai.com/open-source-project/ + + jfaowejfoewj + Wed, 24 Jan 2024 10:30:22 GMT + jfaowejfoewj

+]]>
+
+ + 中间件 + http://www.wenzhihuai.com/middleware/ + http://www.wenzhihuai.com/middleware/ + 中间件 + 中间件 + Wed, 24 Jan 2024 03:35:40 GMT + + + + + http://www.wenzhihuai.com/interview/tiktok2023.html + http://www.wenzhihuai.com/interview/tiktok2023.html + + tt一面: 1.全程项目 2.lc3,最长无重复子串,滑动窗口解决 tt二面: 全程基础,一直追问 1.java内存模型介绍一下 2.volatile原理 3.内存屏障,使用场景?(我提了在gc中有使用) 4.gc中具体是怎么使用内存屏障的,详细介绍 5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么 6.线程内存分配方式,... + Sun, 02 Apr 2023 12:15:06 GMT + tt一面:
+1.全程项目
+2.lc3,最长无重复子串,滑动窗口解决

+

tt二面:
+全程基础,一直追问
+1.java内存模型介绍一下
+2.volatile原理
+3.内存屏障,使用场景?(我提了在gc中有使用)
+4.gc中具体是怎么使用内存屏障的,详细介绍
+5.cms和g1介绍一下,g1能取代cms吗?g1和cms各自的使用场景是什么
+6.线程内存分配方式,tlab相关
+7.happens-before介绍一下
+8.总线风暴是什么,怎么解决
+9.网络相关,time_wait,close_wait产生原因,带来的影响,怎么解决
+10.算法题,给你一个数字n,求用这个n的各位上的数字组合出最大的小于n的数字m,注意边界case,如2222
+11.场景题,设计一个微信朋友圈,功能包含feed流拉取,评论,转发,点赞等操作

+

tt三面:
+大部分聊项目,基础随便问了问
+1.分布式事务,两阶段三阶段流程,区别
+2.mysql主从,同步复制,半同步复制,异步复制
+3.算法题,k个一组翻转链表,类似lc25,区别是最后一组不足k个也要翻转
+4.场景题,设计一个评论系统,包含发布评论,回复评论,评论点赞等功能,用户过亿,qps百万

+]]>
+
+ + Mysql + http://www.wenzhihuai.com/database/mysql/1mysql.html + http://www.wenzhihuai.com/database/mysql/1mysql.html + Mysql + Mysql imgimg + Sun, 26 Mar 2023 12:44:23 GMT + img
img
+]]>
+ +
+ + 数据库缓存 + http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html + http://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html + 数据库缓存 + 数据库缓存 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。 将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。 禁用原因: 1.命中率低 2.写时所有都失效 禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。 查询缓存... + Sun, 26 Mar 2023 12:44:23 GMT + 在MySQL 5.6开始,就已经默认禁用查询缓存了。在MySQL 8.0,就已经删除查询缓存功能了。

+

将select语句和语句的结果做hash映射关系后保存在一定的内存区域内。

+

禁用原因:

+

1.命中率低

+

2.写时所有都失效

+

禁用了:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg。

+

查询缓存讲解:https://blog.csdn.net/zzddada/article/details/124116182

+]]>
+
+ + spark on k8s operator + http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html + http://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html + spark on k8s operator + spark on k8s operator Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。 使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管... + Sun, 12 Jun 2022 12:19:23 GMT + Spark operator是一个管理Kubernetes上的Apache Spark应用程序生命周期的operator,旨在像在Kubernetes上运行其他工作负载一样简单的指定和运行spark应用程序。

+

使用Spark Operator管理Spark应用,能更好的利用Kubernetes原生能力控制和管理Spark应用的生命周期,包括应用状态监控、日志获取、应用运行控制等,弥补Spark on Kubernetes方案在集成Kubernetes上与其他类型的负载之间存在的差距。

+

Spark Operator包括如下几个组件:

+
    +
  1. SparkApplication控制器:该控制器用于创建、更新、删除SparkApplication对象,同时控制器还会监控相应的事件,执行相应的动作。
  2. +
  3. Submission Runner:负责调用spark-submit提交Spark作业,作业提交的流程完全复用Spark on K8s的模式。
  4. +
  5. Spark Pod Monitor:监控Spark作业相关Pod的状态,并同步到控制器中。
  6. +
  7. Mutating Admission Webhook:可选模块,基于注解来实现Driver/Executor Pod的一些定制化需求。
  8. +
  9. SparkCtl:用于和Spark Operator交互的命令行工具。
  10. +
+

安装

+
# 添加源
+helm repo add spark-operator https://googlecloudplatform.github.io/spark-on-k8s-operator
+# 安装
+helm install my-release spark-operator/spark-operator --namespace spark-operator --create-namespace
+

这个时候如果直接执行kubectl apply -f examples/spark-pi.yaml调用样例,会出现以下报错,需要创建对象的serviceaccount。

+
forbidden: error looking up service account default/spark: serviceaccount \"spark\" not found.\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:568)\n\tat io.fabric8.kubernetes.client.dsl.base.OperationSupport
+

同时也要安装rbac

+

rbac的文件在manifest/spark-operator-install/spark-operator-rbac.yaml,需要把namespace全部改为需要运行的namespace,这里我们任务放在default命名空间下,所以全部改为default,源文件见rbac.yaml

+

执行脚本,这里用的app.jar是一个读取es数据源,然后计算,为了展示效果,在程序的后面用了TimeUnit.HOURS.sleep(1)方便观察。

+

代码如下:

+
 SparkConf sparkConf = new SparkConf().setAppName("Spark WordCount Application (java)");
+        sparkConf.set("es.nodes", "xxx")
+                .set("es.port", "8xx0")
+                .set("es.nodes.wan.only", "true")
+                .set("es.net.http.auth.user", "xxx")
+                .set("es.net.http.auth.pass", "xxx-xxx");
+        try (JavaSparkContext jsc = new JavaSparkContext(sparkConf)) {
+            JavaRDD<Map<String, Object>> esRDD = JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce").values();
+
+            System.out.println(esRDD.partitions().size());
+
+            esRDD.map(x -> x.get("customer_full_name"))
+                    .countByValue()
+                    .forEach((x, y) -> System.out.println(x + ":" + y));
+            TimeUnit.HOURS.sleep(1);
+
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+

打包之后kubectl create -f k8s.yaml即可

+
apiVersion: "sparkoperator.k8s.io/v1beta2"
+kind: SparkApplication
+metadata:
+  name: spark-demo
+  namespace: default
+spec:
+  type: Scala
+  mode: cluster
+  image: fewfew-docker.pkg.coding.net/spark-java-demo/spark-java-demo-new/spark-java-demo:master-1d8c164bced70a1c66837ea5c0180c61dfb48ac3
+  imagePullPolicy: Always
+  mainClass: com.spark.es.demo.EsReadGroupByName
+  mainApplicationFile: "local:///opt/spark/examples/jars/app.jar"
+  sparkVersion: "3.1.1"
+  restartPolicy:
+    type: Never
+  driver:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    coreLimit: "1200m"
+    memory: "512m"
+    labels:
+      version: 3.1.1
+    serviceAccount: sparkoperator
+  executor:
+    # 用cores必须要大于等于1,这里用coreRequest
+    coreRequest: "100m"
+    instances: 1
+    memory: "512m"
+    labels:
+      version: 3.1.1
+
spark-demo-driver                                          1/1     Running   0          2m30s
+spark-wordcount-application-java-0ac352810a9728e1-exec-1   1/1     Running   0          2m1s
+

同时会自动生成相应的service。

+
image-20220528203941897
image-20220528203941897
+image-20220528203816649 +

Spark on k8s operator大大减少了spark的部署与运维成本,用容器的调度来替换掉yarn,

+

源码解析

+architecture-diagram +

Spark Operator的主要组件如下:

+

1、SparkApplication Controller : 用于监控并相应SparkApplication的相关对象的创建、更新和删除事件;

+

2、Submission Runner:用于接收Controller的提交指令,并通过spark-submit 来提交Spark作业到K8S集群并创建Driver Pod,driver正常运行之后将启动Executor Pod;

+

3、Spark Pod Monitor:实时监控Spark作业相关Pod(Driver、Executor)的运行状态,并将这些状态信息同步到Controller ;

+

4、Mutating Admission Webhook:可选模块,但是在Spark Operator中基本上所有的有关Spark pod在Kubernetes上的定制化功能都需要使用到该模块,因此建议将enableWebhook这个选项设置为true。

+

5、sparkctl: 基于Spark Operator的情况下可以不再使用kubectl来管理Spark作业了,而是采用Spark Operator专用的作业管理工具sparkctl,该工具功能较kubectl功能更为强大、方便易用。

+image-20220612194403742 +

apis:用户编写yaml时的解析

+

Batchscheduler:批处理的调度,提供了支持volcano的接口

+

Client:

+

leader的选举

+
electionCfg := leaderelection.LeaderElectionConfig{
+   Lock:          resourceLock,
+   LeaseDuration: *leaderElectionLeaseDuration,
+   RenewDeadline: *leaderElectionRenewDeadline,
+   RetryPeriod:   *leaderElectionRetryPeriod,
+   Callbacks: leaderelection.LeaderCallbacks{
+      OnStartedLeading: func(c context.Context) {
+         close(startCh)
+      },
+      OnStoppedLeading: func() {
+         close(stopCh)
+      },
+   },
+}
+
+elector, err := leaderelection.NewLeaderElector(electionCfg)
+

参考:

+

1.k8s client-go中Leader选举实现

+

2.[Kubernetes基于leaderelection选举策略实现组件高可用

+]]>
+ +
+ + elastic spark + http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html + http://www.wenzhihuai.com/bigdata/spark/elastic-spark.html + elastic spark + elastic spark Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有: (1)文件系统:LocalFS、HDFS、Hive、t... + Tue, 03 May 2022 11:42:30 GMT + Hadoop允许Elasticsearch在Spark中以两种方式使用:通过自2.1以来的原生RDD支持,或者通过自2.0以来的Map/Reduce桥接器。从5.0版本开始,elasticsearch-hadoop就支持Spark 2.0。目前spark支持的数据源有:
+(1)文件系统:LocalFS、HDFS、Hive、text、parquet、orc、json、csv
+(2)数据RDBMS:mysql、oracle、mssql
+(3)NOSQL数据库:HBase、ES、Redis
+(4)消息对象:Redis

+

elasticsearch相对hdfs来说,容易搭建、并且有可视化kibana支持,非常方便spark的初学入门,本文主要讲解用elasticsearch-spark的入门。

+
Spark - Apache Spark
Spark - Apache Spark
+

一、原生RDD支持

+

1.1 基础配置

+

相关库引入:

+
        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>elasticsearch-spark-30_2.13</artifactId>
+            <version>8.1.3</version>
+        </dependency>
+

SparkConf配置,更多详细的请点击这里或者源码ConfigurationOptions

+
public static SparkConf getSparkConf() {
+    SparkConf sparkConf = new SparkConf().setAppName("elasticsearch-spark-demo");
+    sparkConf.set("es.nodes", "host")
+            .set("es.port", "xxxxxx")
+            .set("es.nodes.wan.only", "true")
+            .set("es.net.http.auth.user", "elxxxxastic")
+            .set("es.net.http.auth.pass", "xxxx")
+            .setMaster("local[*]");
+    return sparkConf;
+}
+

1.2 读取es数据

+

这里用的是kibana提供的sample data里面的索引kibana_sample_data_ecommerce,也可以替换成自己的索引。

+
public static void main(String[] args) {
+    SparkConf conf = getSparkConf();
+    try (JavaSparkContext jsc = new JavaSparkContext(conf)) {
+
+        JavaPairRDD<String, Map<String, Object>> esRDD =
+                JavaEsSpark.esRDD(jsc, "kibana_sample_data_ecommerce");
+        esRDD.collect().forEach(System.out::println);
+    }
+}
+

esRDD同时也支持query语句esRDD(final JavaSparkContext jsc, final String resource, final String query),一般对es的查询都需要根据时间筛选一下,不过相对于es的官方sdk,并没有那么友好的api,只能直接使用原生的dsl语句。

+

1.3 写数据

+

支持序列化对象、json,并且能够使用占位符动态索引写入数据(使用较少),不过多介绍了。

+
public static void jsonWrite(){
+    String json1 = "{\"reason\" : \"business\",\"airport\" : \"SFO\"}";
+    String json2 = "{\"participants\" : 5,\"airport\" : \"OTP\"}";
+    JavaRDD<String> stringRDD = jsc.parallelize(ImmutableList.of(json1, json2));
+    JavaEsSpark.saveJsonToEs(stringRDD, "spark-json");
+}
+

比较常用的读写也就这些,更多可以看下官网相关介绍。

+

二、Spark Streaming

+

spark的实时处理,es5.0的时候开始支持,目前

+

三、Spark SQL

+

四、Spark Structure Streaming

+

五、Spark on kubernetes Operator

+

参考:

+

1.Apache Spark support

+

2.elasticsearch-hadoop

+

3.使用SparkSQL操作Elasticsearch - Spark入门教程

+]]>
+ +
+ + 基于kubernetes的分布式限流 + http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html + http://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html + 基于kubernetes的分布式限流 + 基于kubernetes的分布式限流 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 一、概念 限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,... + Sat, 09 Apr 2022 05:35:51 GMT + 做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

+

一、概念

+

限流(Ratelimiting)指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒,对超过限制的请求则进行快速失败或丢弃。

+

1.1 使用场景

+

限流可以应对:

+
    +
  • 热点业务带来的突发请求;
  • +
  • 调用方 bug 导致的突发请求;
  • +
  • 恶意攻击请求。
  • +
+

1.2 维度

+

对于限流场景,一般需要考虑两个维度的信息:
+时间
+限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定
+资源
+基于可用资源的限制,比如设定最大访问次数,或最高可用连接数。
+  限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。

+
image.png
image.png
+

1.3 分布式限流

+

分布式限流相比于单机限流,只是把限流频次分配到各个节点中,比如限制某个服务访问100qps,如果有10个节点,那么每个节点理论上能够平均被访问10次,如果超过了则进行频率限制。

+

二、分布式限流常用方案

+

基于Guava的客户端限流
+Guava是一个客户端组件,在其多线程模块下提供了以RateLimiter为首的几个限流支持类。它只能对“当前”服务进行限流,即它不属于分布式限流的解决方案。

+

网关层限流
+服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求。我们在网关层进行限流,就可以达到了整体限流的目的了。目前,主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件为代表的F5。

+

中间件限流
+将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。

+

限流组件
+目前也有一些开源组件提供了限流的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中。Hystrix也具有限流的功能。

+

Guava的Ratelimiter设计实现相当不错,可惜只能支持单机,网关层限流如果是单机则不太满足高可用,并且分布式网关的话还是需要依赖中间件限流,而redis之类的网络通信需要占用一小部分的网络消耗。阿里的Sentinel也是同理,底层使用的是redis或者zookeeper,每次访问都需要调用一次redis或者zk的接口。那么在云原生场景下,我们有没有什么更好的办法呢?

+

对于极致追求高性能的服务不需要考虑熔断、降级来说,是需要尽量减少网络之间的IO,那么是否可以通过一个总限频然后分配到具体的单机里面去,在单机中实现平均的限流,比如限制某个ip的qps为100,服务总共有10个节点,那么平均到每个服务里就是10qps,此时就可以通过guava的ratelimiter来实现了,甚至说如果服务的节点动态调整,单个服务的qps也能动态调整。

+

三、基于kubernetes的分布式限流

+

在Spring Boot应用中,定义一个filter,获取请求参数里的key(ip、userId等),然后根据key来获取rateLimiter,其中,rateLimiter的创建由数据库定义的限频数和副本数来判断,最后,再通过rateLimiter.tryAcquire来判断是否可以通过。

+
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
企业微信截图_868136b4-f9e2-4813-bc02-281a66756ecd.png
+

3.1 kubernetes中的副本数

+

在实际的服务中,数据上报服务一般无法确定客户端的上报时间、上报量,特别是对于这种要求高性能,服务一般都会用到HPA来实现动态扩缩容,所以,需要去间隔一段时间去获取服务的副本数。

+
func CountDeploymentSize(namespace string, deploymentName string) *int32 {
+	deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
+	if err != nil {
+		return nil
+	}
+	return deployment.Spec.Replicas
+}
+

用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。

+

3.2 rateLimiter的创建

+

在RateLimiterService中定义一个LoadingCache<String, RateLimiter>,其中,key可以为ip、userId等,并且,在多线程的情况下,使用refreshAfterWrite只阻塞加载数据的线程,其他线程则返回旧数据,极致发挥缓存的作用。

+
private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
+        .maximumSize(10_000)
+        .refreshAfterWrite(20, TimeUnit.MINUTES)
+        .build(this::createRateLimit);
+//定义一个默认最小的QPS
+private static final Integer minQpsLimit = 3000;
+

之后是创建rateLimiter,获取总限频数totalLimit和副本数replicas,之后是自己所需的逻辑判断,可以根据totalLimit和replicas的情况来进行qps的限定。

+
public RateLimiter createRateLimit(String key) {
+    log.info("createRateLimit,key:{}", key);
+    int totalLimit = 获取总限频数,可以在数据库中定义
+    Integer replicas = kubernetesService.getDeploymentReplicas();
+    RateLimiter rateLimiter;
+    if (totalLimit > 0 && replicas == null) {
+        rateLimiter = RateLimiter.create(totalLimit);
+    } else if (totalLimit > 0) {
+        int nodeQpsLimit = totalLimit / replicas;
+        rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
+    } else {
+        rateLimiter = RateLimiter.create(minQpsLimit);
+    }
+    log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
+    return rateLimiter;
+}
+

3.3 rateLimiter的获取

+

根据key获取RateLimiter,如果有特殊需求的话,需要判断key不存在的尝尽

+
public RateLimiter getRateLimiter(String key) {
+  return loadingCache.get(key);
+}
+

3.4 filter里的判断

+

最后一步,就是使用rateLimiter来进行限流,如果rateLimiter.tryAcquire()为true,则进行filterChain.doFilter(request, response),如果为false,则返回HttpStatus.TOO_MANY_REQUESTS

+
public class RateLimiterFilter implements Filter {
+    @Resource
+    private RateLimiterService rateLimiterService;
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+        String key = httpServletRequest.getHeader("key");
+        RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
+        if (rateLimiter != null) {
+            if (rateLimiter.tryAcquire()) {
+                filterChain.doFilter(request, response);
+            } else {
+                httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+            }
+        } else {
+            filterChain.doFilter(request, response);
+        }
+    }
+}
+

四、性能压测

+

为了方便对比性能之间的差距,我们在本地单机做了下列测试,其中,总限频都设置为3万。

+

无限流

+
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
企业微信截图_ea1b7815-3b89-43bf-aed7-240e135bdad1.png
+

使用redis限流

+

其中,ping redis大概6-7ms左右,对应的,每次请求需要访问redis,时延都有大概6-7ms,性能下降明显

+
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
企业微信截图_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png
+

自研限流

+

性能几乎追平无限流的场景,guava的rateLimiter确实表现卓越

+
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
企业微信截图_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png
+

五、其他问题

+

5.1 对于保证qps限频准确的时候,应该怎么解决呢?

+

在k8s中,服务是动态扩缩容的,相应的,每个节点应该都要有所变化,如果对外宣称限频100qps,而且后续业务方真的要求百分百准确,只能把LoadingCache<String, RateLimiter>的过期时间调小一点,让它能够近实时的更新单节点的qps。这里还需要考虑一下k8s的压力,因为每次都要获取副本数,这里也是需要做缓存的

+

5.2 服务从1个节点动态扩为4个节点,这个时候新节点识别为4,但其实有些并没有启动完,会不会造成某个节点承受了太大的压力

+

理论上是存在这个可能的,这个时候需要考虑一下初始的副本数的,扩缩容不能一蹴而就,一下子从1变为4变为几十个这种。一般的话,生产环境肯定是不能只有一个节点,并且要考虑扩缩容的话,至于要有多个副本预备的

+

5.3 如果有多个副本,怎么保证请求是均匀的

+

这个是依赖于k8s的service负载均衡策略的,这个我们之前做过实验,流量确实是能够均匀的落到节点上的。还有就是,我们整个限流都是基于k8s的,如果k8s出现问题,那就是整个集群所有服务都有可能出现问题了。

+

参考

+

1.常见的分布式限流解决方案
+2.分布式服务限流实战
+3.高性能

+]]>
+ +
+ + 【elasticsearch】搜索过程详解 + http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html + http://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html + 【elasticsearch】搜索过程详解 + 【elasticsearch】搜索过程详解 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。 SearchType QUERY_THEN_FETCH(... + Sun, 20 Mar 2022 15:14:40 GMT + 本文基于elasticsearch8.1。在es搜索中,经常会使用索引+星号,采用时间戳来进行搜索,比如aaaa-*在es中是怎么处理这类请求的呢?是对匹配的进行搜索呢还是仅仅根据时间找出索引,然后才遍历索引进行搜索。在了解其原理前先了解一些基本知识。

+

SearchType

+

QUERY_THEN_FETCH(默认):第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
+DFS_QUERY_THEN_FETCH:比第1种方式多了一个初始化散发(initial scatter)步骤。

+

为了能够深刻了解es的搜索过程,首先创建3个索引,每个索引指定一天的一条记录。

+
POST aaaa-16/_doc
+{
+  "@timestamp": "2022-02-16T16:21:15.000Z",
+  "word":"16"
+}
+
+
+POST aaaa-17/_doc
+{
+  "@timestamp": "2022-02-17T16:21:15.000Z",
+  "word":"17"
+}
+
+POST aaaa-18/_doc
+{
+  "@timestamp": "2022-02-18T16:21:15.000Z",
+  "word":"18"
+}
+

即可在kibana上看到3条数据

+
image-20220219195141327
image-20220219195141327
+

此时,假设我们用一个索引+星号来搜索,es内部的搜索是怎么样的呢?

+
GET aaaa*/_search
+{
+  "query": {
+    "range": {
+      "@timestamp": {
+        "gte": "2022-02-18T10:21:15.000Z",
+        "lte": "2022-02-18T17:21:15.000Z"
+      }
+    }
+  }
+}
+

正好命中一条记录返回。

+
{
+  "took" : 2,
+  "timed_out" : false,
+  "_shards" : {
+    "total" : 3,
+    "successful" : 3,
+    "skipped" : 0,
+    "failed" : 0
+  },
+  "hits" : {
+    "total" : {
+      "value" : 1,
+      "relation" : "eq"
+    },
+    "max_score" : 1.0,
+    "hits" : [
+      {
+        "_index" : "aaaa-18",
+        "_id" : "0zB2O38BoMIMP8QzHgdq",
+        "_score" : 1.0,
+        "_source" : {
+          "@timestamp" : "2022-02-18T16:21:15.000Z",
+          "word" : "18"
+        }
+      }
+    ]
+  }
+}
+

一、es的分布式搜索过程

+

一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。当搜索任务执行在分布式系统上时,整体流程如下图所示。图片来源Elasitcsearch源码解析与优化实战

+
2
2
+

1.1 搜索入口

+

整个http请求的入口,主要使用的是Netty4HttpRequestHandler。

+
@ChannelHandler.Sharable
+class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<HttpPipelinedRequest> {
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest httpRequest) {
+        final Netty4HttpChannel channel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get();
+        boolean success = false;
+        try {
+            serverTransport.incomingRequest(httpRequest, channel);
+            success = true;
+        } finally {
+            if (success == false) {
+                httpRequest.release();
+            }
+        }
+    }
+}
+

二、初步调用流程

+

调用链路过程:Netty4HttpRequestHandler.channelRead0->AbstractHttpServerTransport.incomingRequest->AbstractHttpServerTransport.handleIncomingRequest->AbstractHttpServerTransport.dispatchRequest->RestController.dispatchRequest(实现了HttpServerTransport.Dispatcher)->SecurityRestFilter.handleRequest->BaseRestHandler.handleRequest->action.accept(channel)->RestCancellableNodeClient.doExecute->NodeClient.executeLocally->RequestFilterChain.proceed->TransportAction.proceed->TransportSearchAction.doExecute->TransportSearchAction.executeRequest(判断是本地执行还是远程执行)->TransportSearchAction.searchAsyncAction

+

协调节点的主要功能是接收请求,解析并构建目的的shard列表,然后异步发送到数据节点进行请求查询。具体就不细讲了,可按着debug的来慢慢调试。

+

特别注意下RestCancellableNodeClient.doExecute,从executeLocally执行所有的查询过程,并注册监听listener.onResponse(response),然后响应。

+
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(...) {
+  ...
+    TaskHolder taskHolder = new TaskHolder();
+    Task task = client.executeLocally(action, request, new ActionListener<>() {
+        @Override
+        public void onResponse(Response response) {
+            try {
+                closeListener.unregisterTask(taskHolder);
+            } finally {
+                listener.onResponse(response);
+            }
+        }
+    });
+  ...
+}
+

其次要注意的是:TransportSearchAction.searchAsyncAction才开始真正的搜索过程

+
private SearchPhase searchAsyncAction(...) {
+  ...
+    final QueryPhaseResultConsumer queryResultConsumer = searchPhaseController.newSearchPhaseResults();
+  AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction = switch (searchRequest.searchType()) {
+    case DFS_QUERY_THEN_FETCH -> new SearchDfsQueryThenFetchAsyncAction(...);
+    case QUERY_THEN_FETCH -> new SearchQueryThenFetchAsyncAction(...);
+  };
+  return searchAsyncAction;
+  ...
+}
+

之后就是执行AbstractSearchAsyncAction.start,启动AbstractSearchAsyncAction.executePhase的查询动作。

+
此处的SearchPhase实现类为SearchQueryThenFetchAsyncAction
+private void executePhase(SearchPhase phase) {
+    try {
+        phase.run();
+    } catch (Exception e) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(new ParameterizedMessage("Failed to execute [{}] while moving to [{}] phase", request, phase.getName()), e);
+        }
+        onPhaseFailure(phase, "", e);
+    }
+}
+

三、协调节点

+

两阶段相应的实现位置:查询(Query)阶段—search.SearchQueryThenFetchAsyncAction;取回(Fetch)阶段—search.FetchSearchPhase。它们都继承自SearchPhase,如下图所示。

+
23
23
+

3.1 query阶段

+

图片来源官网,比较旧,但任然可用

+
12
12
+

(1)客户端发送一个search请求到node3,node3创建一个大小为from,to的优先级队列。
+(2)node3转发转发search请求至索引的主分片或者副本,每个分片执行查询请求,并且将结果放到一个排序之后的from、to大小的优先级队列。
+(3)每个分片把文档id和排序之后的值返回给协调节点node3,node3把结果合并然后创建一个全局排序之后的结果。

+
在RestSearchAction#prepareRequest方法中将请求体解析为SearchRequest 数据结构: 
+public RestChannelConsumer prepareRequest(.. .) {
+    SearchRequest searchRequest = new SearchRequest();
+    request.withContentOrSourceParamParserOrNull (parser ->
+        parseSearchRequest (searchRequest, request, parser, setSize));
+    ...
+}
+

3.1.1 构造目的shard列表

+

将请求涉及的本集群shard列表和远程集群的shard列表(远程集群用于跨集群访问)合并:

+
private void executeSearch(.. .) {
+  ...
+    final GroupShardsIterator<SearchShardIterator> shardIterators = mergeShardsIterators(localShardIterators, remoteShardIterators);
+    localShardIterators = StreamSupport.stream(localShardRoutings.spliterator(), false).map(it -> {
+    OriginalIndices finalIndices = finalIndicesMap.get(it.shardId().getIndex().getUUID());
+    assert finalIndices != null;
+    return new SearchShardIterator(searchRequest.getLocalClusterAlias(), it.shardId(), it.getShardRoutings(), finalIndices);
+    }).collect(Collectors.toList());
+	...
+}
+

查看结果

+
241
241
+

3.1.2 对所有分片进行搜索

+
AbstractSearchAsyncAction.run
+对每个分片进行搜索查询
+for (int i = 0; i < shardsIts.size(); i++) {
+    final SearchShardIterator shardRoutings = shardsIts.get(i);
+    assert shardRoutings.skip() == false;
+    assert shardIndexMap.containsKey(shardRoutings);
+    int shardIndex = shardIndexMap.get(shardRoutings);
+    performPhaseOnShard(shardIndex, shardRoutings, shardRoutings.nextOrNull());
+}
+

其中shardsIts是所有aaaa*的所有索引+其中一个副本

+
2141
2141
+

3.1.3 分片具体的搜索过程

+
AbstractSearchAsyncAction.performPhaseOnShard
+private void performPhaseOnShard(. ..) {
+    executePhaseOnShard(.. .) {
+        //收到执行成功的回复
+        public void inne rOnResponse (FirstResult result) {
+            maybeFork (thread, () -> onShardResult (result,shardIt) );
+        }
+        //收到执行失败的回复
+        public void onFailure (Exception t) {
+            maybeFork(thread, () -> onShardFailure (shardIndex, shard, shard. currentNodeId(),shardIt, t));
+        }
+    });
+}
+

分片结果,当前线程

+
//AbstractSearchAsyncAction.onShardResult
+...
+private void onShardResult (FirstResult result, SearchShardIterator shardIt) {
+    onShardSuccess(result);
+    success fulShardExecution(shardIt);
+}
+...
+//AbstractSearchAsyncAction.onShardResultConsumed
+private void successfulShardExecution (SearchShardIterator shardsIt) {
+    //计数器累加.
+    final int xTotalOps = totalOps.addAndGet (remainingOpsOnIterator);
+    //检查是否收到全部回复
+    if (xTotalOps == expectedTotalOps) {
+        onPhaseDone ();
+    } else if (xTota1Ops > expectedTotal0ps) {
+        throw new AssertionError(. ..);
+    }
+}
+
412
412
+

此处忽略了搜索结果totalHits为0的结果,并将结果进行累加,当xTotalOps等于expectedTotalOps时开始AbstractSearchAsyncAction.onPhaseDone再进行AbstractSearchAsyncAction.executeNextPhase取回阶段

+

3.2 Fetch阶段

+

取回阶段,图片来自官网

+
412412
412412
+

(1)各个shard 返回的只是各文档的id和排序值 IDs and sort values ,coordinate node根据这些id&sort values 构建完priority queue之后,然后把程序需要的document 的id发送mget请求去所有shard上获取对应的document

+

(2)各个shard将document返回给coordinate node

+

(3)coordinate node将合并后的document结果返回给client客户端

+

3.2.1 FetchSearchPhase(对应上面的1)

+

Query阶段的executeNextPhase方法触发Fetch阶段,Fetch阶段的起点为FetchSearchPhase#innerRun函数,从查询阶段的shard列表中遍历,跳过查询结果为空的shard,对特定目标shard执行executeFetch来获取数据,其中包括分页信息。对scroll请求的处理也在FetchSearchPhase#innerRun函数中。

+
private void innerRun() throws Exception {
+    final int numShards = context.getNumShards();
+    final boolean isScrollSearch = context.getRequest().scroll() != null;
+    final List<SearchPhaseResult> phaseResults = queryResults.asList();
+    final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
+    final boolean queryAndFetchOptimization = queryResults.length() == 1;
+    final Runnable finishPhase = () -> moveToNextPhase(
+        searchPhaseController,
+        queryResults,
+        reducedQueryPhase,
+        queryAndFetchOptimization ? queryResults : fetchResults.getAtomicArray()
+    );
+            for (int i = 0; i < docIdsToLoad.length; i++) {
+                IntArrayList entry = docIdsToLoad[i];
+                SearchPhaseResult queryResult = queryResults.get(i);
+                if (entry == null) { 
+                    if (queryResult != null) {
+                        releaseIrrelevantSearchContext(queryResult.queryResult());
+                        progressListener.notifyFetchResult(i);
+                    }
+                    counter.countDown();
+                }else{
+                    executeFetch(
+                            queryResult.getShardIndex(),
+                            shardTarget,
+                            counter,
+                            fetchSearchRequest,
+                            queryResult.queryResult(),
+                            connection
+                        );
+                }
+        }
+    }
+}
+

再看源码:

+
启动一个线程来fetch
+AbstractSearchAsyncAction.executePhase->FetchSearchPhase.run->FetchSearchPhase.innerRun->FetchSearchPhase.executeFetch
+
+private void executeFetch(...) {
+    context.getSearchTransport()
+        .sendExecuteFetch(
+            connection,
+            fetchSearchRequest,
+            context.getTask(),
+            new SearchActionListener<FetchSearchResult>(shardTarget, shardIndex) {
+                @Override
+                public void innerOnResponse(FetchSearchResult result) {
+                  progressListener.notifyFetchResult(shardIndex);
+                  counter.onResult(result);
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                  progressListener.notifyFetchFailure(shardIndex, shardTarget, e);
+                  counter.onFailure(shardIndex, shardTarget, e);
+                }
+            }
+        );
+}
+
13413
13413
+

counter是一个收集器CountedCollector,onResult(result)主要是每次收到的shard数据存放,并且执行一次countDown,当所有shard数据收集完之后,然后触发一次finishPhase。

+
# CountedCollector.class
+void countDown() {
+    assert counter.isCountedDown() == false : "more operations executed than specified";
+    if (counter.countDown()) {
+        onFinish.run();
+    }
+}
+

moveToNextPhase方法执行下一阶段,下-阶段要执行的任务定义在FetchSearchPhase构造 函数中,主要是触发ExpandSearchPhase。

+

3.2.2 ExpandSearchPhase(对应上图的2)

+

AbstractSearchAsyncAction.executePhase->ExpandSearchPhase.run。取回阶段完成之后执行ExpandSearchPhase#run,主要判断是否启用字段折叠,根据需要实现字段折叠功能,如果没有实现字段折叠,则直接返回给客户端。

+
image-20220317000858513
image-20220317000858513
+

ExpandSearchPhase执行完之后回复客户端,在AbstractSearchAsyncAction.sendSearchResponse方法中实现:

+
412412
412412
+

四、数据节点

+

4.1 执行query、fetch流程

+

执行本流程的线程池: search。

+

对各种Query请求的处理入口注册于SearchTransportService.registerRequestHandler。

+
public static void registerRequestHandler(TransportService transportService, SearchService searchService) {
+  ...
+  transportService.registerRequestHandler(
+    QUERY_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    ShardSearchRequest::new,
+    (request, channel, task) -> searchService.executeQueryPhase(
+      request,
+      (SearchShardTask) task,
+      new ChannelActionListener<>(channel, QUERY_ACTION_NAME, request)
+    )
+  );
+  ...
+}
+

4.1.1 执行query请求

+
# SearchService
+public void executeQueryPhase(ShardSearchRequest request, SearchShardTask task, ActionListener<SearchPhaseResult> listener) {
+    assert request.canReturnNullResponseIfMatchNoDocs() == false || request.numberOfShards() > 1
+        : "empty responses require more than one shard";
+    final IndexShard shard = getShard(request);
+    rewriteAndFetchShardRequest(shard, request, listener.delegateFailure((l, orig) -> {
+        ensureAfterSeqNoRefreshed(shard, orig, () -> executeQueryPhase(orig, task), l);
+    }));
+}
+

其中ensureAfterSeqNoRefreshed是把request任务放到一个名为search的线程池里面执行,容量大小为1000。

+
1
1
+

主要是用来执行SearchService.executeQueryPhase->SearchService.loadOrExecuteQueryPhase->QueryPhase.execute。核心的查询封装在queryPhase.execute(context)中,其中调用Lucene实现检索,同时实现聚合:

+
public void execute (SearchContext searchContext) {
+    aggregationPhase.preProcess (searchContext);
+    boolean rescore = execute ( searchContext, searchContext.searcher(), searcher::setCheckCancelled, indexSort);
+    if (rescore) {
+        rescorePhase.execute (searchContext);
+        suggestPhase.execute (searchContext);
+        aggregationPhase.execute (searchContext);
+    }
+}
+

其中包含几个核心功能:

+
    +
  • execute(),调用Lucene、searcher.search()实现搜索
  • +
  • rescorePhase,全文检索且需要打分
  • +
  • suggestPhase,自动补全及纠错
  • +
  • aggregationPhase,实现聚合
  • +
+

4.1.2 fetch流程

+

对各种Fetch请求的处理入口注册于SearchTransportService.registerRequestHandler。

+
transportService.registerRequestHandler(
+    QUERY_FETCH_SCROLL_ACTION_NAME,
+    ThreadPool.Names.SAME,
+    InternalScrollSearchRequest::new,
+    (request, channel, task) -> {
+        searchService.executeFetchPhase(
+            request,
+            (SearchShardTask) task,
+            new ChannelActionListener<>(channel, QUERY_FETCH_SCROLL_ACTION_NAME, request)
+        );
+    }
+);
+

对Fetch响应的实现封装在SearchService.executeFetchPhase中,核心是调用fetchPhase.execute(context)。按照命中的doc取得相关数据,填充到SearchHits中,最终封装到FetchSearchResult中。

+
# FetchPhase
+public void execute(SearchContext context) {
+    Profiler profiler = context.getProfilers() == null ? Profiler.NOOP : context.getProfilers().startProfilingFetchPhase();
+    SearchHits hits = null;
+    try {
+      //lucene构建搜索的结果
+        hits = buildSearchHits(context, profiler);
+    } finally {
+        ProfileResult profileResult = profiler.finish();
+        // Only set the shardResults if building search hits was successful
+        if (hits != null) {
+            context.fetchResult().shardResult(hits, profileResult);
+        }
+    }
+}
+

五、数据返回

+

入口:RestCancellableNodeClient.doExecute
+Task task = client.executeLocally主要执行查询,并使用了ActionListener来进行监听
+image-20220319003638991

+

其中onResponse的调用链路如下:RestActionListener.onResponse->RestResponseListener.processResponse->RestController.sendResponse->DefaultRestChannel.sendResponse->Netty4HttpChannel.sendResponse

+
public void sendResponse(RestResponse restResponse) {
+  ...
+   httpChannel.sendResponse(httpResponse, listener);
+  ...
+}
+

最后由Netty4HttpChannel.sendResponse来响应请求。

+

六、总结

+

当我们以aaaa*这样来搜索的时候,实际上查询了所有匹配以aaaa开头的索引,并且对所有的索引的分片都进行了一次Query,再然后对有结果的分片进行一次fetch,最终才能展示结果。可能觉得好奇,对所有分片都进行一次搜索,遍历分片所有的Lucene分段,会不会太过于消耗资源,因此合并Lucene分段对搜索性能有好处,这里下篇文章在讲吧。同时,聚合是发生在fetch过程中的,并不是lucene。

+

本文参考

+
    +
  1. Elasitcsearch源码解析与优化实战
  2. +
  3. Elasticsearch源码分析-搜索分析(一)
  4. +
  5. Elasticsearch源码分析-搜索分析(二)
  6. +
  7. Elasticsearch源码分析-搜索分析(三)
  8. +
  9. Elasticsearch 通信模块的分析
  10. +
  11. Elasticsearch 网络通信线程分析
  12. +
+]]>
+ +
+ + kafka面试题 + http://www.wenzhihuai.com/middleware/kafka/kafka.html + http://www.wenzhihuai.com/middleware/kafka/kafka.html + kafka面试题 + kafka面试题 1、请说明什么是Apache Kafka? Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。 2、说说Kafka的使用场景? ①异步处理 ②应用解耦 ③流量削峰 ④日志处理 ⑤消息通讯等。 3、使用Kafka有什么优点和缺点? 优点: ①支持跨数据中心的消息复制; ②... + Sun, 20 Mar 2022 15:14:40 GMT + 1、请说明什么是Apache Kafka?
+Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和可复制的提交日志服务。

+

2、说说Kafka的使用场景?
+①异步处理
+②应用解耦
+③流量削峰
+④日志处理
+⑤消息通讯等。

+

3、使用Kafka有什么优点和缺点?
+优点:
+①支持跨数据中心的消息复制;
+②单机吞吐量:十万级,最大的优点,就是吞吐量高;
+③topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源;
+④时效性:ms级;
+⑤可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
+⑥消息可靠性:经过参数优化配置,消息可以做到0丢失;
+⑦功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用。

+

缺点:
+①由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序;
+②有可能消息重复消费;
+③依赖zookeeper进行元数据管理,等等。

+

4、为什么说Kafka性能很好,体现在哪里?
+①顺序读写
+②零拷贝
+③分区
+④批量发送
+⑤数据压缩

+

5、请说明什么是传统的消息传递方法?
+传统的消息传递方法包括两种:
+排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。
+发布-订阅:在这个模型中,消息被广播给所有的用户。

+

6、请说明Kafka相对传统技术有什么优势?
+①快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。
+②可伸缩:在一组机器上对数据进行分区
+③和简化,以支持更大的数据
+④持久:消息是持久性的,并在集群中进
+⑤行复制,以防止数据丢失。
+⑥设计:它提供了容错保证和持久性

+

7、解释Kafka的Zookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka吗?
+Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。
+不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。
+Zookeeper主要用于在集群中不同节点之间进行通信
+在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
+除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

+

8、解释Kafka的用户如何消费信息?
+在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。

+

9、解释如何提高远程用户的吞吐量?
+如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。

+

10、解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息?
+在数据中,为了精确地获得Kafka的消息,你必须遵循两件事: 在数据消耗期间避免重复,在数据生产过程中避免重复。
+这里有两种方法,可以在数据生成时准确地获得一个语义:
+每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功
+在消息中包含一个主键(UUID或其他),并在用户中进行反复制

+

11、解释如何减少ISR中的扰动?broker什么时候离开ISR?
+ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会从ISR中删除。

+

12、Kafka为什么需要复制?
+Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。

+

13、如果副本在ISR中停留了很长时间表明什么?
+如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。

+

14、请说明如果首选的副本不在ISR中会发生什么?
+如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。

+

15、有可能在生产后发生消息偏移吗?
+在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。

+

作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。

+

16、Kafka的设计时什么样的呢?
+Kafka将消息以topic为单位进行归纳 将向Kafka topic发布消息的程序成为producers. 将预订topics并消费消息的程序成为consumer. Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker. producers通过网络将消息发送到Kafka集群,集群向消费者提供消息

+

17、数据传输的事物定义有哪三种?
+(1)最多一次:
+消息不会被重复发送,最多被传输一次,但也有可能一次不传输
+(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
+(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

+

18、Kafka判断一个节点是否还活着有那两个条件?
+(1)节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接
+(2)如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久

+

19、producer是否直接将数据发送到broker的leader(主节点)?
+producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发,为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活动的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。

+

20、Kafa consumer是否可以消费指定分区消息?
+Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的

+

21、Kafka消息是采用Pull模式,还是Push模式?
+Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式

+

Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略

+

Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

+

22、Kafka存储在硬盘上的消息格式是什么?
+消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。
+消息长度: 4 bytes (value: 1+4+n)
+版本号: 1 byte
+CRC校验码: 4 bytes
+具体的消息: n bytes

+

23、Kafka高效文件存储设计特点:
+(1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
+(2).通过索引信息可以快速定位message和确定response的最大大小。
+(3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
+(4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

+

24、Kafka 与传统消息系统之间有三个关键区别
+(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留
+(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
+(3).Kafka 支持实时的流式处理

+

25、Kafka创建Topic时如何将分区放置到不同的Broker中
+副本因子不能大于 Broker 的个数;
+第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
+其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
+剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的

+

26、Kafka新建的分区会在哪个目录下创建
+在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。 但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。

+

27、partition的数据如何保存到硬盘
+topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增, 且消息有序 Partition文件下有多个segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名

+

28、kafka的ack机制
+request.required.acks有三个值 0 1 -1
+0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据
+1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失
+-1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失

+

29、Kafka的消费者如何消费数据
+消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费。同时也可以按照指定的offset进行重新消费。

+

30、消费者负载均衡策略
+结合consumer的加入和退出进行再平衡策略。

+

31、kafka消息数据是否有序?
+消费者组里某具体分区是有序的,所以要保证有序只能建一个分区,但是实际这样会存在性能问题,具体业务具体分析后确认。

+

32、kafaka生产数据时数据的分组策略,生产者决定数据产生到集群的哪个partition中
+每一条消息都是以(key,value)格式 Key是由生产者发送数据传入 所以生产者(key)决定了数据产生到集群的哪个partition

+

33、kafka consumer 什么情况会触发再平衡reblance?
+①一旦消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中的所有消费者都要执行再平衡。
+②订阅主题分区发生变化,所有消费者也都要再平衡。

+

34、描述下kafka consumer 再平衡步骤?
+①关闭数据拉取线程,情空队列和消息流,提交偏移量;
+②释放分区所有权,删除zk中分区和消费者的所有者关系;
+③将所有分区重新分配给每个消费者,每个消费者都会分到不同分区;
+④将分区对应的消费者所有关系写入ZK,记录分区的所有权信息;
+⑤重启消费者拉取线程管理器,管理每个分区的拉取线程。

+

35、Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么
+36、Kafka中的HW、LEO、LSO、LW等分别代表什么?
+37、Kafka中是怎么体现消息顺序性的?
+38、Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
+39、Kafka生产者客户端的整体结构是什么样子的?
+40、Kafka生产者客户端中使用了几个线程来处理?分别是什么?
+41、Kafka的旧版Scala的消费者客户端的设计有什么缺陷?
+42、“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段?
+43、消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
+44、有哪些情形会造成重复消费?
+45、那些情景下会造成消息漏消费?
+46、KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?
+47、简述消费者与消费组之间的关系
+48、当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
+49、topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
+50、topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
+51、创建topic时如何选择合适的分区数?
+52、Kafka目前有那些内部topic,它们都有什么特征?各自的作用又是什么?
+53、优先副本是什么?它有什么特殊的作用?
+54、Kafka有哪几处地方有分区分配的概念?简述大致的过程及原理
+55、简述Kafka的日志目录结构
+56、Kafka中有那些索引文件?
+57、如果我指定了一个offset,Kafka怎么查找到对应的消息?
+58、如果我指定了一个timestamp,Kafka怎么查找到对应的消息?
+59、聊一聊你对Kafka的Log Retention的理解
+60、聊一聊你对Kafka的Log Compaction的理解
+61、聊一聊你对Kafka底层存储的理解(页缓存、内核层、块层、设备层)
+62、聊一聊Kafka的延时操作的原理
+63、聊一聊Kafka控制器的作用
+64、消费再均衡的原理是什么?(提示:消费者协调器和消费组协调器)
+65、Kafka中的幂等是怎么实现的
+66、Kafka中的事务是怎么实现的(这题我去面试6家被问4次,照着答案念也要念十几分钟,面试官简直凑不要脸。实在记不住的话...只要简历上不写精通Kafka一般不会问到,我简历上写的是“熟悉Kafka,了解RabbitMQ....”)
+67、Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
+68、失效副本是指什么?有那些应对措施?
+69、多副本下,各个副本中的HW和LEO的演变过程
+70、为什么Kafka不支持读写分离?
+71、Kafka在可靠性方面做了哪些改进?(HW, LeaderEpoch)
+72、Kafka中怎么实现死信队列和重试队列?
+73、Kafka中的延迟队列怎么实现(这题被问的比事务那题还要多!!!听说你会Kafka,那你说说延迟队列怎么实现?)
+74、Kafka中怎么做消息审计?
+75、Kafka中怎么做消息轨迹?
+76、Kafka中有那些配置参数比较有意思?聊一聊你的看法
+77、Kafka中有那些命名比较有意思?聊一聊你的看法
+78、Kafka有哪些指标需要着重关注?
+79、怎么计算Lag?(注意read_uncommitted和read_committed状态下的不同)
+80、Kafka的那些设计让它有如此高的性能?
+81、Kafka有什么优缺点?
+82、还用过什么同质类的其它产品,与Kafka相比有什么优缺点?
+83、为什么选择Kafka?
+84、在使用Kafka的过程中遇到过什么困难?怎么解决的?
+85、怎么样才能确保Kafka极大程度上的可靠性?
+86、聊一聊你对Kafka生态的理解

+]]>
+
+ + 2017 + http://www.wenzhihuai.com/life/2017.html + http://www.wenzhihuai.com/life/2017.html + 2017 + 2017 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。 17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感... + Sat, 25 Jan 2020 13:18:10 GMT + 2016年6月,大三,考完最后一科,默默的看着大四那群人一个一个的走了,想着想着,明年毕业的时候,自己将会失去你所拥有的一切,大学那群认识的人,可能这辈子都不会见到了——致远离家乡出省读书的娃。然而,我并没有想到,自己失去的,不仅仅是那群人,几乎是一切。。。。
+17年1月,因为受不了北京雾霾(其实是次因),从实习的公司跑路了,很不舍,只是,感觉自己还有很多事要做,很多梦想要去实现,毕竟,看着身边的人,去了美团,拿着15k的月薪,有点不平衡,感觉他也不是很厉害吧,我努力努力10k应该没问题吧?

+
+

回到学校,待了几天,那个整天给别人灌毒鸡汤的人还是不着急找工作:该有的时候总会有的啦,然后继续打游戏、那个天天喊着吃麻辣香锅的人,记忆中好像今年都没有吃上,哈哈、厕所还是那样偶尔炸一下,嗯,感觉一切都很熟悉,只是,我有了点伤感,毕竟身边又要换一群不认识的人了。考完试,终于能回广东了,一年没回了,开心不得了,更多的是,为了那个喜欢了好几年没有表白的妹纸,之前从没想过真的能每天跟一个妹纸道晚安,秒回,但是,对她,我还真做到了。1月份的广州,不热不冷,空气很清新,比北京、天津那股泥土味好多了,只是,我被拒了,男闺蜜还是男闺蜜,备胎还是备胎,那会还没想过放弃,直到...

+

春节期间,把自己的网站整理了一下,看了看JVM和并发的知识,偶尔刷刷牛客网的题,觉得自己没多大问题了,跟同学出发,去深圳,我并没有一个做马化腾、马云的梦,但是我想,要做出点东西,至少,不是个平庸的人。投了几个,第一个面试,知道是培训兼外包后果断弃了,春招还没开始,再多准备准备,有时间就往深圳图书馆里跑,找个位置,静静的复习,那会,感觉自己一定能去个大公司吧,搞搞分布式、大数据之类的~

+
+

疲惫的面试历程

+

2月份底开始慢慢投简历,跑招聘会,深圳招聘的太少了,那就去广州,大学城,于是,开始了自己一周两天都在广深往返。有的公司笔试要跑一次广州,一面要跑一次,二面还要跑一次,每次看着路费,真的承受不起,但是有希望就行。乐观之下,到来的是一重又一重的打击。

+

**数码,很看好的公司,在广州也算是老品牌了,笔试的时候面试官没有带够卷子,就拿了大数据的做,还过了,面试的时候被面到union和union all的区别,答反了,还有left join和right join的原理,一脸懵逼,过后去了百度搜了搜,这东西哪来的原理?

+

然后是个企业建站的,笔试完了,面试官照着题问然后没说啥就让我走了,额,我好像还没自我介绍呢。

+

有点想回北京了,往北京投了一份,A轮公司,月薪10k,过了,觉得自己技术应该不差吧,不过还是拒了,因为一想起那里满脑子都是雾霾。

+

没事没事,下一家,某游戏公司,笔试过了,一面也过了,然后被问flume收集的时候断网处理机制、MD5和RSA原理、Angulajs和Vuejs用没用过、千万级的数据是如何存储的?额,跟你们想的一样,我一道也答出来,然后就挂了,面试官跟我说,你要是来早三个月这些都会啦,说的我很紧张,广东的同学都那么强么?

+

这个时候还没质疑自己的技术,去了趟深圳某家小公司,金融的,HR问我是不是培训过来的,心里开始突然间好苦涩,大学花那么多时间学习居然被别人用来和培训机构的人相提并论,笔试不知从哪整来的英文逻辑题,做完了然后上机笔试,做不出来不能见面试官,开始感觉这公司有点。。。。还是撤了撤了。

+

突然感觉面试面的绝望,是自己的问题么?是自己技术太菜了么?开始怀疑自己,感觉也是,已经3月份底了,毕竟别人都在实习,而我已经离职3个月没工作了,但是那些问题不是有点离谱了,一开始是开始心态爆炸觉得怎么可以对应届生问这些问题,不应该是基础么?算法什么的,后来是无奈了,开始上网搜各种不沾边的技术。

+

还有其他的面试。。。有的工资不高(6K不到),有的面试问题全答对就因为说了有点想做大数据就不要你,有的聊得很欢快却没消息,有的说招java但是笔试一道java题都没有全是前端的,有的打着互联网实际却是外包的公司。。。。
+  4月初真的很绝望很绝望,不想跟别人说话,是不是很可笑,一个从北京B+轮的公司回广东,面试近15个,居然只能拿到2个offer,其中之一国企外包的,都不知说什么好。13号要拍毕业照了,没有回去,大学四年,毕竟大家个个圈子之间没什么交流,没什么友谊,果然,整个专业的人只有2/3的人去拍了毕业照。4月23号,拉勾网深圳站,梦想着市集,抱着一点点的小希望,去投了5个,结果一个面试邀请都没有,换之而来的是一片绝望、黑暗。。。。

+

感情、我们毕业了

+

5月初回学校了赶毕业设计,回学校也是,天天往图书馆跑,为了面试,毕业设计拉下太多了。悠闲的时候,总会想起我喜欢的那个人,过的怎么样了,微信一开,好像自己被屏蔽了还是被删了?这三个月来的面试打击,真的不知道怎么应对,我更不知道怎么跟她聊天,以至于慢慢隔离,这段感情,或许只有我知道自己有多心疼,只有我自己sb得一点点的呵护着。想起上次跟她聊天是4月中,还是她提了个问题其他什么都没问,彼此之前已经没有关心了。删了吧删了吧,就这样一句再见都没有说,一句“祝福”都没有说。
+  5月底遇到两次还算开心的事,导师问我要不要评优秀论文,不是很想折腾,拒了,导师仍旧劝我,只是写个3页的小总结而已,评上的话能留图书馆收藏。另一个件是珍爱网打电话过来说我一面过了,什么时候二面,天啊,3月份一面,5月底才通知,心里想骂,但是真的好开心好开心,黑暗中的光芒,泪奔。。。。约到了6月15号面试。
+  6月初开始自己的回家倒计时,请那唯一一个跟我好的妹纸吃饭,忙完答辩,然后是优秀论文的小报告,13号下午交了上去,再回到宿舍,空无一人,一遍忍着不要悲伤,一遍收拾东西,然后向每个宿舍敲门,告别,一个人拖着行李箱,嗯,我要淡定。

+
+

准备了有点久,我以为自己准备好了,15号,深圳,珍爱网,被问到:有没有JVM调优经验?ZK是如何保持一致性的?有没有写过ELK索引经验?谈谈你对dubbo的理解。。。事后,得知自己没有过。心态真的炸了,通宵打了两天游戏。月底得知,优秀论文也没有被评上,已经没有骂人吐槽的想法,只是哦的一声,6月,宣告了自己春招结束,宣告自己要去外包,同时也宣告自己大学这些努力全部白费,跟那个灌毒鸡汤天天打游戏的人同一起跑线。

+

996的工作

+

七月初入职的时候,问了问那些一起应届入职的同学,跟别人交流一下面试被问的问题,只有线程池,JVM回收算法,他们也没用过,我无奈,为什么我被问到的,全是Angularjs、MD5加密算法、ZK一致性原理这种奇葩的问题。。。。。

+

没有环境,就自己创造,下班有时间就改改自己的网站http://www.wenzhihuai.com,GitHub目前有154个star了,感觉也是个小作品吧,把常用的Java技术栈(RabbitMQ、MongoDB等)加了上去,虽然没啥使用场景,但是自己好好努力吧,毕竟高并发、高可用是自己的梦想。今年GitHub自己的提交次数。

+
+

决心先好好呆着,学习学习,把公司的产品业务技术专研专研,7、8月份是挺开心的,没怎么加班,虽然下班我都留在公司学习,虽然公司的产品不咋地,但是还是有可学之处的,随后,我没想到的是,公司要实行996,,,9月份至12月,每天都是改bug,开发模块,写业务,全都是增删改查,没有涉及redis、rabbitmq,就真的只有使用Hibernate、spring,而做出的东西,要使用JDK6来运行,至今都能看到sun的类。每天的状态就是累累累,我们组还不是最惨的,隔壁项目组,一周通宵两次,9月到10都是如此,国庆加班,每次经过他们组,我都觉得自己还算是幸福。很厌恶这种不把员工当人看的行为,有同事调侃,明年估计我们这些应届过来的,是不是要走掉2/3的人?疯狂加班的代价是牺牲积极性,我问过那些一周通宵两次的人,敲代码,程序员你喜欢这份工作么?不喜欢!!!有那么一两次,实在是太累了,我都感觉自己有点不行了,想离职,可是,会有公司要我么?会有公司不问那些奇葩的问题么?

+

双十一,花了500多买了十本书,神书啊,计算机届的圣经啊,然而至今一本都没看完,甚至是都没怎么翻,每天下班回去之后就是9点半了,烧水,洗澡,洗衣服,近11点了,躺床上,一遍看书一遍看鹌鹑直播,一小时才翻几页,996真的太累了,实在看不下书。经过这几个月的加班,感觉自己技术增长为负数,也病了几次,同学见我,都感觉我老了不少,心里不平,可是又没有什么办法。

+
+

孤独

+

珠江新城,一年四季都那么美,今晚应该又是万人一起倒计时了吧?已经好久没跟别人一起出去玩过了。今年貌似得了自闭症?社交全靠黄段子。想找人出门走走,然而手机已经没有那种几年的朋友,大学同学一个都不在,哈哈哈,别人说社交软件让人离得更近,我怎么发现我自己一个好朋友都没有了呢,哭笑ing。或许是自己的确不善于跟别人保持联系吧,这个要改,一定要改!!!
+远看:

+
+

近看:

+
+

题外话:
+有些时候,遇到不顺,感觉自己不是神,没必要忍住,就开始宣泄自己的情绪,比如疯狂打游戏,只能这样度过了吧。
+今年听过最多的一句话,你好厉害,你好牛逼==我感觉也是,可惜,要被业务耽误了不少时间吧。。
+《企业IT架构转型之道》是今年最好的架构图书,没有之一。

+

期望

+

想起初中,老师让我在黑板写自己的英文作文,好像是信件,描述什么状况来这?最后夸了夸我在文章后面加的一段大概是劝别人不要悲伤要灿烂。也想起一个故事,二战以后,德国满目疮痍,他们处在这样一个凄惨的境地,还能想到在桌上摆设一些花,就一定能在废墟上重建家园。我所看到的是即使在再黑暗的状态下,要永远抱着希望。明年要更加努力的学习,而不是工作,是该挽留一些人了,多运动。说了那么多,其实,我只希望自己的运气不要再差了,再差就没法活了。人在遇到不顺的时候,往往最难过的一关便是自己。哈哈哈,只能学我那个同学给自己灌毒鸡汤了。加油加油↖(^ω^)↗

+
+]]>
+ +
+ + 2019 + http://www.wenzhihuai.com/life/2019.html + http://www.wenzhihuai.com/life/2019.html + 2019 + 2019 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。 19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了... + Sat, 25 Jan 2020 13:18:10 GMT + 2019,本命年,1字头开头的年份,就这么过去了,迎来了2开头的十年,12月过的不是很好,每隔几天就吵架,都没怎么想起写自己的年终总结了,对这个跨年也不是很重视,貌似有点浑浑噩噩的样子。今天1号,就继续来公司学习学习,就写写文章吧。
+19年一月,没发生什么事,对老板也算是失望,打算过完春节就出去外面试试,完全的架构错误啊,怎么守得了,然后开了个年会,没什么好看的,唯一的例外就是看见漂漂亮亮的她,可惜不敢搭话,就这么的呗。2月以奇奇怪怪的方式走到了一起,开心,又有点伤心,然后就开始了这个欢喜而又悲剧的本命年。

+

工作

+

从18年底开始吧,大概就知道领导对容器云这么快的完全架构设计错误,就是一个完完全全错误的产品,其他人也觉得有点很有问题,但老板就是固执不听,结果一个跑路,一个被压得懒得反抗,然后我一个人跟老板和项目经理怼了前前后后半年,真是惨啊,还要被骂,各种拉到小黑屋里批斗,一直在对他们强调:没有这么做的!!!然后被以年轻人,不懂事,不要抱怨,长大了以后就领会了怼回来,当时气得真是无语。。。。。自己调研的阿里云、腾讯云、青云,还有其他小公司的容器,发现真的只有我们这么设计,完全错误的方向,,,,直到老板走了之后,才开始决定重构,走上正轨。

+

之后有了点起色,然后开始着重强调了DevOps的重要性,期初也没人理解,还被骂天天研究技术不干活。。。这东西没技术,但是真的是公司十分需要的公司好么!!可惜公司对这个产品很失望,也不给人,期间还说要暂停项目,唉,这么大的项目就给几个人怎么做,谁都不乐意,就这么过的很憋屈吧,开始了维护的日子。。。

+

工作上,蛮多人对我挺好的,领导,同事,不过有时候憋屈的时候还是有了点情绪化,有时候有点悲伤的想法,浮躁、暴躁,出去面试了几次,感觉不是自己能力不好,而是市场上的真的需要3+年以上的。。。。不过感觉自己能在这里憋屈的过了这么久,也是很佩服自己的,哈哈,用同事的话来说就是:当做游戏里的练级,锻炼自己哈哈哈

+
+

学习

+

项目的方向错误,导致自己写的代码都是很简单的增删改查,没有技术含量的那种,也最终导致自己浪费了不少时间,上半年算是很气愤吧,不想就这么浪费掉这些时间,虽然期间看了各种各样的东西,比如Netty、Go、操作系统什么的,最终发现,如果不在项目中使用的话,真的会忘,而且很快就忘掉,最后的还是决定学习项目相关度比较大的东西了,比如Go,如果项目不是很大,小项目的话,就用Go来写,这么做感觉提升还是挺大的。
+过去一年往南山图书馆借了不少书,省了不少钱,虽然没怎么看,但我至少有个奋斗的心啊,哈哈哈哈哈哈哈哈,文章写得少了,因为感觉写不出好东西,总是入门的那种,不深,不值得学习,想想到时候把别人给坑了,也不好意思。
+操作系统感觉还是要好好学学了,加油

+
+

Kubernetes就是未来的方向,有时候翻开源码看看,又不敢往下面看了,对底层不熟吧,今年要多多研究下Kubernetes的源码了,至于Spring那一套,也不知道该不该放弃,或者都学习一下?云原生就是趋势。

+

感情

+

吵架了,没心思写

+

运动

+

过去一年还是把运动算是坚持了下来,每个月必去深圳湾跑一次。还是没怎么睡好,工作感情上都有点不顺,加上自己本身就难以入睡,有时候躺床上就是怎么也睡不着,还长了痘痘,要跑去医院那种,可怕,老了,还是要早点睡觉,多走走多运动,好久没打羽毛球了,自己也不想有空的时候只会打游戏,今年继续加油,多运动吧。

+
+

还爬了两次南山

+
+

其他

+

看了周围蛮多同事去了腾讯阿里,有点心动,好想去csig,没到3年真的让人很抓狂。
+过去一年过的蛮憋屈的,特别是工作不顺,加上跟女朋友吵架,心态爆炸。。。

+

2020展望

+

每年这个时候,都不敢有太大的期望了,祝大家都健健康康的,工作顺利!!

+

当然,如果有可能的话,我想去CSIG

+]]>
+ +
+ + 被挖矿攻击 + http://www.wenzhihuai.com/java/serverlog.html + http://www.wenzhihuai.com/java/serverlog.html + 被挖矿攻击 + 被挖矿攻击 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看: stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJY... + Sat, 25 Jan 2020 13:10:49 GMT + 服务器被黑,进程占用率高达99%,查看了一下,是/usr/sbin/bashd的原因,怎么删也删不掉,使用ps查看:
+
+stratum+tcp://get.bi-chi.com:3333 -u 47EAoaBc5TWDZKVaAYvQ7Y4ZfoJMFathAR882gabJ43wHEfxEp81vfJ3J3j6FQGJxJNQTAwvmJYS2Ei8dbkKcwfPFst8FhG

+

使用top查看:
+

+
+
+

启动iptables,参考http://www.setphp.com/981.html
+http://www.setphp.com/981.html

+
iptables -A INPUT -s xmr.crypto-pool.fr -j DROP
+iptables -A OUTPUT -d xmr.crypto-pool.fr -j DROP
+

2017-10-02 再次遭到挖矿攻击

+

2017-11-20

+

19号添加mongodb之后,20号重启了服务器,但是忘记启动mongodb,导致后台一直在重连mongodb,也就导致了服务访问超级超级慢,记住要启动所需要的组件。

+

2017-12-03

+
+

2017-12-05 wipefs

+
+]]>
+ +
+ + 2018 + http://www.wenzhihuai.com/life/2018.html + http://www.wenzhihuai.com/life/2018.html + 2018 + 2018 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。 想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。 心惊胆战的裸辞经历 其实校招过... + Sat, 25 Jan 2020 13:10:49 GMT + 年底老姐结婚,跑回家去了,忙着一直没写,本来想回深圳的时候空余的时候写写,没想到一直加班,嗯,9点半下班那种,偶尔也趁着间隙的时间,一段一段的凑着吧。

+

想着17年12月31号写的那篇文章https://www.cnblogs.com/w1570631036/p/8158284.html,感叹18年还算恢复了点。

+

心惊胆战的裸辞经历

+

其实校招过去的那家公司,真的不是很喜欢,996、技术差、产品差,实在受不了,春节前提出了离职,老大也挽留了下,以“来了个阿里的带我们重构,能学不了东西”来挽留,虽然我对技术比较痴迷,但离职去深圳的决心还是没有动摇,嗯,就这么开始了自己的裸辞过程。3月8号拿到离职,回公司的时候跟跟同事吹吹水,吃个饭,某同事还喊:“周末来公司玩玩么,我给你开门”,哈哈哈,泼出去的水,回不去了,偶尔有点伤感。
+去深圳面试,第一家随手记,之前超级想去这家公司的,金融这一块,有钱,只可惜,没过,一面面试官一直夸我,我觉得稳了,二面没转过来,就这么挂了,有点不甘心吧,在这,感谢那个内推我的人吧,面经在这,之后就是大大小小的面试,联想、恒大、期待金融什么的,都没拿到offer,裸辞真的慌得一比,一天没工作,感觉一年没有工作没人要的样子。。。。没面试就跑去广州图书馆,复习,反思,一天一天的过着,回家就去楼下吃烧烤,打游戏,2点3点才睡觉,boss直聘、拉勾网见一个问一个,迷迷糊糊慌慌张张,那段期间真的想有点把所有书卖掉回家啃老的想法,好在最后联系了一家公司,电商、大小周,知乎上全是差评,不过评价的都不是技术的,更重要的是,岗位是中间件的,嗯,去吧。

+

and ... 广州拜拜,晚上人少的时候,跟妹纸去珠江新城真的很美好

+
+

工作工作

+

入职的时候是做日志收集的,就是flume+kafka+es这一套,遇到了不少大佬,嗯,感觉挺好的,打算好好干,一个半月后不知怎么滴,被拉去做容器云了,然后就开始了天天调jenkins、gitlab这些没什么用的api,开始了增删改查的历程,很烦,研究jenkins、gitlab对于自己的技术没什么提升,又不得不做,而且大小周+加班这么做,回到家自己看java、netty、golang的书籍,可是没项目做没实践,过几个月就忘光了,有段时间真的很烦很烦,根本不想工作,天天调api,而且是jenkins、gitlab这些没什么卵用的api,至今也没找到什么办法。。。。
+公司发展太快,也实在让人不知所措,一不小心部门被架空了,什么预兆都没有,被分到其他部门,然后发现这个部门领导内斗真的是,“三国争霸”吧,无奈成为炮灰,不同的领导不同的要求,安安静静敲个代码怎么这么难。。。。
+去年的学习拉下了不少,文章也写的少了,总写那些入门的文章确实没有什么意思,买的书虽然没有17年的多吧,不过也不少了,也看了挺多本书的,虽然我还是不满意自己的阅读量。
+这几年要开始抓准一个框架了,好好专研一下,也要开始学习一下go了,希望能参与一些github的项目,好好努力吧。

+
+

跑步跑步

+

平时没事总是宅在家里打游戏,差不多持续到下半年吧,那会打游戏去楼下吃东西的时候老是觉得眼神恍惚,盯着电子产品太多了,然后就跟别人跑步去了,每周去一次人才公园,跑步健身,人才公园真的是深圳跑步最好的一个地方了,桥上总有一群摄影师,好想在深圳湾买一套房子,看了看房价,算了算了,深圳的房,真的贵的离谱。19年也要多多运动,健康第一健康第一。

+
+

Others

+
    +
  1. 拿到了深圳户口,全程只花了15天,感谢学妹and家人
  2. +
  3. 买了第一台MacBook Pro,对于Java的我真心不好用
  4. +
  5. 盼了好几年的老姐结婚,18年终于了解了这心事
  6. +
  7. 养了只猫,在经历了好几个月的早起之后,晚上睡觉锁厨房,终于安分了,对不起了哈哈哈
  8. +
+
+

2019

+

想不起来还有什么要说的了,毕竟程序员,好像每天的生活都一样,就展望一下19年吧。今年,最最重要的是家里人以及身边还有所有人都健健康康的,哈哈哈。然后是安安静静的敲代码~~就酱

+]]>
+ +
+ + Redis缓存 + http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html + http://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html + Redis缓存 + Redis缓存 一、概述 1.1 缓存介绍 系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 缓存常用语: 数据不... + Sat, 25 Jan 2020 13:10:49 GMT + 一、概述 +

1.1 缓存介绍

+

系统的性能指标一般包括响应时间、延迟时间、吞吐量,并发用户数和资源利用率等。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
+缓存常用语:
+数据不一致性、缓存更新机制、缓存可用性、缓存服务降级、缓存预热、缓存穿透
+可查看Redis实战(一) 使用缓存合理性

+

1.2 本站缓存架构

+

从没有使用缓存,到使用mybatis缓存,然后使用了ehcache,再然后是mybatis+redis缓存。

+
+

步骤:
+(1)用户发送一个请求到nginx,nginx对请求进行分发。
+(2)请求进入controller,service,service中查询缓存,如果命中,则直接返回结果,否则去调用mybatis。
+(3)mybatis的缓存调用步骤:二级缓存->一级缓存->直接查询数据库。
+(4)查询数据库的时候,mysql作了主主备份。

+

二、Mybatis缓存

+

2.1 mybatis一级缓存

+

Mybatis的一级缓存是指Session回话级别的缓存,也称作本地缓存。一级缓存的作用域是一个SqlSession。Mybatis默认开启一级缓存。在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。Mybatis 默认支持一级缓存,不需要在配置文件中配置。

+
+

我们来查看一下源码的类图,具体的源码分析简单概括一下:SqlSession实际上是使用PerpetualCache来维护的,PerpetualCache中定义了一个HashMap<k,v>来进行缓存。
+(1)当会话开始时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
+(2)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。如果命中,则返回结果,如果没有命中,则去数据库中查询,再将结果存储到cache中,最后返回结果。如果执行增删改,则执行flushCacheIfRequired方法刷新缓存。
+(3)当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

+
![](http://image.wenzhihuai.com/images/20180120022427.png)
+

2.2 mybatis二级缓存

+

Mybatis的二级缓存是指mapper映射文件,为Application应用级别的缓存,生命周期长。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。在同一个namespace下的mapper文件中,执行相同的查询SQL。实现二级缓存,关键是要对Executor对象做文章,Mybatis给Executor对象加上了一个CachingExecutor,使用了设计模式中的装饰者模式,
+

+

2.2.1 MyBatis二级缓存的划分

+

MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:

+
a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置);
+b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置);
+

2.2.2 二级缓存的开启

+

在mybatis的配置文件中添加:

+
<settings>
+   <!--开启二级缓存-->
+    <setting name="cacheEnabled" value="true"/>
+</settings>
+

然后再需要开启二级缓存的mapper.xml中添加(本站使用了LRU算法,时间为120000毫秒):

+
    <cache eviction="LRU"
+           type="org.apache.ibatis.cache.impl.PerpetualCache"
+           flushInterval="120000"
+           size="1024"
+           readOnly="true"/>
+

2.2.3 使用第三方支持的二级缓存的实现

+
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache  type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached、Redis的集成,总之,使用MyBatis的二级缓存有三个选择:
+
    +
  1. MyBatis自身提供的缓存实现;
  2. +
  3. 用户自定义的Cache接口实现;
  4. +
  5. 跟第三方内存缓存库的集成;
    +具体的实现,可参照:SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置
  6. +
+

MyBatis中一级缓存和二级缓存的组织如下图所示(图片来自深入理解mybatis原理):

+
+

2.3 Mybatis在分布式环境下脏读问题

+

(1)如果是一级缓存,在多个SqlSession或者分布式的环境下,数据库的写操作会引起脏数据,多数情况可以通过设置缓存级别为Statement来解决。
+(2)如果是二级缓存,虽然粒度比一级缓存更细,但是在进行多表查询时,依旧可能会出现脏数据。
+(3)Mybatis的缓存默认是本地的,分布式环境下出现脏读问题是不可避免的,虽然可以通过实现Mybatis的Cache接口,但还不如直接使用集中式缓存如Redis、Memcached好。

+

下面将介绍使用Redis集中式缓存在个人网站的应用。

+

三、Redis缓存

+

Redis运行于独立的进程,通过网络协议和应用交互,将数据保存在内存中,并提供多种手段持久化内存的数据。同时具备服务器的水平拆分、复制等分布式特性,使得其成为缓存服务器的主流。为了与Spring更好的结合使用,我们使用的是Spring-Data-Redis。此处省略安装过程和Redis的命令讲解。

+
+

3.1 Spring Cache

+

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。Spring 的缓存技术还具备相当的灵活性,不仅能够使用 **SpEL(Spring Expression Language)**来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
+下面是Spring Cache常用的注解:

+

(1)@Cacheable
+@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

+

| 属性 | 介绍 |例子|
+|

+]]>
+ +
+ + JVM调优参数 + http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html + http://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html + JVM调优参数 + JVM调优参数 一、堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。... + Sat, 25 Jan 2020 13:10:49 GMT + 一、堆大小设置 +

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

+

典型设置:

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
+

-Xmx3550m:设置JVM最大可用内存为3550M。
+-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
+-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
+-Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内 存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

+
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
+

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
+-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
+-XX:MaxPermSize=16m:设置持久代大小为16m。
+-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

+

二、回收器选择

+

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

+

2.1 吞吐量优先的并行收集器

+

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
+典型配置:

+
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
+

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
+-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
+

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
+

-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
+

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

+

2.2 响应时间优先的并发收集器

+

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
+典型配置:

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
+

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
+-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

+
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
+

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
+-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

+

三、辅助信息

+

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
+-XX:+PrintGC
+输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
+                [Full GC 121376K->10414K(130112K), 0.0650971 secs]

+

-XX:+PrintGCDetails
+输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
+                [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

+

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
+输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
+-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
+输出形式:Application time: 0.5291524 seconds
+-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
+输出形式:Total time for which application threads were stopped: 0.0468229 seconds
+-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
+输出形式:

+
34.702: [GC {Heap before gc invocations=7:
+ def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
+from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
+  to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
+ tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
+ def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
+eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
+  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
+  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
+ tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
+the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
+ compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
+   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
+    ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
+    rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
+}
+, 0.0757599 secs]
+

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
+常见配置汇总

+

3.1 堆设置

+

-Xms:初始堆大小
+-Xmx:最大堆大小
+-XX:NewSize=n:设置年轻代大小
+-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
+-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
+-XX:MaxPermSize=n:设置持久代大小

+

3.2 收集器设置

+

-XX:+UseSerialGC:设置串行收集器
+-XX:+UseParallelGC:设置并行收集器
+-XX:+UseParalledlOldGC:设置并行年老代收集器
+-XX:+UseConcMarkSweepGC:设置并发收集器

+

3.3 垃圾回收统计信息

+

-XX:+PrintGC
+-XX:+PrintGCDetails
+-XX:+PrintGCTimeStamps
+-Xloggc:filename

+

3.4 并行收集器设置

+

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
+-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
+-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

+

3.5 并发收集器设置

+

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
+-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

+

四、调优总结

+

4.1 年轻代大小选择

+

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
+吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

+

4.2 年老代大小选择

+

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
+并发垃圾收集信息
+持久代并发收集次数
+传统GC信息
+花在年轻代和年老代回收上的时间比例
+减少年轻代和年老代花费的时间,一般会提高应用的效率
+吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

+

4.3 较小堆引起的碎片问题

+

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他 会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么 并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
+-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
+-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

+]]>
+
+ + 1.历史与架构 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html + 1.历史与架构 + 1.历史与架构 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog 。 大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。 由原本的ssh变成ssm,再变成ssm+shi... + Tue, 21 Jan 2020 02:38:05 GMT + 各位大佬瞄一眼我的个人网站呗 。如果觉得不错,希望能在GitHub上麻烦给个star,GitHub地址https://github.com/Zephery/newblog
+大学的时候萌生的一个想法,就是建立一个个人网站,前前后后全部推翻重构了4、5遍,现在终于能看了,下面是目前的首页。

+
+

由原本的ssh变成ssm,再变成ssm+shiro+lucene,到现在的前后台分离。前台使用ssm+lucene,后台使用spring boot+shiro。其中,使用pagehelper作为分页,lucene用来搜索和自动补全,使用百度统计的API做了个日志系统,统计pv和uv什么的,同时,还有使用了JMX来观察JVM的使用和cpu的使用率,机器学习方面,使用了adaboost和朴素贝叶斯对微博进行分类,有兴趣的可以点点有点意思这个页面。
+本文从下面这几个方面来讲讲网站的建立:

+
    +
  1. 建站故事与网站架构
  2. +
  3. lucene搜索的使用
  4. +
  5. 使用quartz来定时备份数据库
  6. +
  7. 使用百度统计api做日志系统
  8. +
  9. Nginx小集群的搭建
  10. +
  11. [数据库]
  12. +
  13. 使用机器学习对微博进行分析
  14. +
  15. 网站性能优化
  16. +
  17. SEO优化
  18. +
+

1.建站故事与网站架构

+

1.1建站过程

+

起初,是因为学习的时候老是找不到什么好玩而又有挑战性的项目,看着struts、spring、hibernate的书,全都是一些小项目,做了感觉也没啥意义,有时候在博客园看到别人还有自己的网站,特羡慕,最终就选择了自己做一个个人网站。期初刚学的ssh,于是开始了自己的ssh搭建个人网站的行程,但是对于一个后端的人来说,前端是个大问题啊。。。。所以初版的时候差不多全是纯生的html、css、js,至少发博客这个功能实现了,但是确实没法看。前前后后折腾了一个多月,决定推翻重做,到百度看看别人怎么做的。首先看到的是杨青的网站,已经好几年没更新了,前端的代码看起来比较简单,也是自己能够掌握的,但是不够美观,继续找,在模板之家发现了一个高大上的模板。

+
+

第二版的界面确实是这样的,只是把图片的切换变成了wowslider,也是简单的用bootstrap和pagehelper做了下分页,现在的最终版保留了它的header,然后评论框使用了多说(超级怀念多说)。后端也由原来的ssh变成了ssm,之后加上了lucene来对文章进行索引。之后,随着多说要关闭了,突然之间有很多div都不适应了(我写死了的。。。),再一次,没法看,不想看,一怒之下再次推翻重做,变成了现在这个版本。

+

最终版本在考虑时,也找了很多模板,影响深刻的是tale欲思这两个主题,期中,tale使用的是java语言写的,刚知道的那一刻我就没好感了,java后端我是要自己全部写的,tale这个页面简洁但是不够炫,而且内容量太低,可能就只是个纯博客,之后发现了欲思,拓展性强,只可惜没有静态的版本,后台是纯生的PHP(嗯,PHP是世界上最好的语言),看了看,没事,保存网页,前端自己改,后端自己全部重写,最终变成了现在这个版本,虽然拼接的时候各种css、js混入。。。。还好在做网站性能优化的时候差不多全部去掉了。最终版加入redis、quartz、shiro等,还有python机器学习、flask的restful api,可谓是大杂烩了。
+页面看着还算凑合,至少不是那种看都看不过去的那种了,但是仔细看看,还是有不少问题的,比如瀑布流,还有排版什么的。只能等自己什么时候想认真学学前端的东西了。
+已经部署在腾讯云服务器上,存储使用了七牛云的cdn。

+

1.2 网站整体技术架构

+

最终版的技术架构图如下:

+
+

网站核心主要采用Spring SpringMVC和Mybatis,下图是当访问一篇博客的时候的运行流程,参考了张开涛的博客。

+
+

运行流程分析

+
    +
  1. 浏览器发送http请求。/blogdetail.html?blogid=1。
  2. +
  3. tomcat容器初始化,顺序为context-param>listener>filter>servlet,此时,spring中的bean还没有被注入的,不建议在此处加载bean,网站声明了两个类(IPFilter和CacheControlFilter),IPFilter用来拦截IP,CacheControlFilter用来缓存。
  4. +
  5. 初始化Spring。
  6. +
  7. DispatcherServlet——>HandlerMapping进行请求到处理的映射,HandlerMapping将“/blogdetail”路径直接映射到名字为“/blogdetail”的Bean进行处理,即BlogController。
  8. +
  9. 自定义拦截器,其中BaseIntercepter实现了HandleInterceptor的接口,用来记录每次访问的链接以及后台响应的时间。
  10. +
  11. DispatcherServlet——> SimpleControllerHandlerAdapter,SimpleControllerHandlerAdapter将HandlerExecutionChain中的处理器适配为BlogController。
  12. +
  13. BlogController执行查询,取得结果集返回数据。
  14. +
  15. blogdetail(ModelAndView的逻辑视图名)——>InternalResourceViewResolver, InternalResourceViewResolver使用JstlView,具体视图页面在/blogdetail.jsp。
  16. +
  17. JstlView(/blogdetail.jsp)——>渲染,将在处理器传入的模型数据(blog=Blog!)在视图中展示出来。
  18. +
  19. 返回响应。
  20. +
+

1.3 日志系统

+

日志系统架构如下:

+
+

日志系统曾经尝试采用过ELK,实时监控实在是让人不能不称赞,本地也跑起来了,但是一到服务器,卡卡卡,毕竟(1Ghz CPU、1G内存),只能放弃ELK,采用百度统计。百度统计提供了Tongji API供开发者使用,只是有次数限制,2000/日,实时的话实在有点低,只能统计前几天的PV、UV等开放出来。其实这个存放在mysql也行,不过这些零碎的数据还是放在redis中,方便管理。
+出了日志系统,自己对服务器的一些使用率也是挺关心的,毕竟服务器配置太低,于是利用了使用了tomcat的JMX来对CPU和jvm使用情况进行监控,这两个都是实时的。出了这两个,对内存的分配做了监控,Eden、Survivor、Tenured的使用情况。

+

1.4 【有点意思】自然语言处理

+

本人大学里的毕业设计就是基于AdaBoost算法的情感分类,学到的东西还是要经常拿出来看看,要不然真的浪费了我这么久努力做的毕业设计啊。构建了一个基本的情感分类小系统,每天抓取微博进行分类存储在MySql上,并使用flask提供Restful API给java调用,可以点击这里尝试(请忽略Google的图片)。目前分类效果不是很明显,准确率大概只有百分之70%,因为训练样本只有500条(找不到训练样本),机器学习真的太依赖样本的标注。这个,只能请教各位路人大神指导指导了。

+
+

总结

+

断断续续,也总算做了一个能拿得出手仍要遮遮掩掩才能给别人看的网站,哈哈哈哈哈,也劳烦各位帮忙找找bug。前前后后从初学Java EE就一直设想的事也还算有了个了结,以后要多多看书,写点精品文章。PS:GitHub上求给个Star,以后面试能讲讲这个网站。

+]]>
+ +
+ + 2.Lucene的使用 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html + 2.Lucene的使用 + 2.Lucene的使用 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog 。 Lucene的整体架构 imageimage 搜索引擎的几个重要概念: 倒排索引:将... + Tue, 21 Jan 2020 02:38:05 GMT + 首先,帮忙点击一下我的网站http://www.wenzhihuai.com/ 。谢谢啊,如果可以,GitHub上麻烦给个star,以后面试能讲讲这个项目,GitHub地址https://github.com/Zephery/newblog

+

Lucene的整体架构

+
image
image
+

搜索引擎的几个重要概念:

+
    +
  1. +

    倒排索引:将文档中的词作为关键字,建立词与文档的映射关系,通过对倒排索引的检索,可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。
    +

    +
  2. +
  3. +

    停用词:在一段句子中,去掉之后对句子的表达意向没有印象的词语,如“非常”、“如果”,中文中主要包括冠词,副词等。

    +
  4. +
  5. +

    排序:搜索引擎在对一个关键词进行搜索时,可能会命中许多文档,这个时候,搜索引擎就需要快速的查找的用户所需要的文档,因此,相关度大的结果需要进行排序,这个设计到搜索引擎的相关度算法。

    +
  6. +
+

Lucene中的几个概念

+
    +
  1. 文档(Document):文档是一系列域的组合,文档的域则代表一系列域文档相关的内容。
  2. +
  3. 域(Field):每个文档可以包含一个或者多个不同名称的域。
  4. +
  5. 词(Term):Term是搜索的基本单元,与Field相对应,包含了搜索的域的名称和关键词。
  6. +
  7. 查询(Query):一系列Term的条件组合,成为TermQuery,但也有可能是短语查询等。
  8. +
  9. 分词器(Analyzer):主要是用来做分词以及去除停用词的处理。
  10. +
+

索引的建立

+
+

索引的搜索

+
+

lucene在本网站的使用:

+
    +
  1. 搜索 2. 自动分词
  2. +
+

一、搜索

+

注意:本文使用最新的lucene,版本6.6.0。lucene的版本更新很快,每跨越一次大版本,使用方式就不一样。首先需要导入lucene所使用的包。使用maven:

+
<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-core</artifactId><!--lucene核心-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-queryparser</artifactId><!--格式化-->
+    <version>${lucene.version}</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
+    <version>${lucene.version}</version>
+</dependency>
+
    +
  1. 构建索引
  2. +
+
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
+SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
+IndexWriterConfig config = new IndexWriterConfig(analyzer);
+IndexWriter writer = new IndexWriter(dir, config);
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
+writer.addDocument(doc);
+writer.close();
+
    +
  1. 更新与删除
  2. +
+
IndexWriter writer = getWriter();
+Document doc = new Document();
+doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
+doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
+writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc);   //更新索引
+writer.close();
+
    +
  1. 查询
  2. +
+
private static void search_index(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径,也就是索引所在的位置
+        IndexReader reader = DirectoryReader.open(dir);
+        IndexSearcher searcher = new IndexSearcher(reader);
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        QueryParser parser = new QueryParser("content", analyzer); //查询解析器
+        Query query = parser.parse(keyword); //通过解析要查询的String,获取查询对象
+        TopDocs docs = searcher.search(query, 10);//开始查询,查询前10条数据,将记录保存在docs中,
+        for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+            System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
+        }
+        reader.close();
+    } catch (IOException | ParseException e) {
+        logger.error(e.toString());
+    }
+}
+
    +
  1. 高亮
  2. +
+
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
+SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
+Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
+highlighter.setTextFragmenter(fragmenter);
+for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
+    Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
+    String title = doc.get("title");
+    TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
+    String hTitle = highlighter.getBestFragment(tokenStream, title);
+    System.out.println(hTitle);
+}
+

结果

+
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
+
    +
  1. 分页
    +目前lucene分页的方式主要有两种:
    +(1). 每次都全部查询,然后通过截取获得所需要的记录。由于采用了分词与倒排索引,所有速度是足够快的,但是在数据量过大的时候,占用内存过大,容易造成内存溢出
    +(2). 使用searchAfter把数据保存在缓存里面,然后再去取。这种方式对大量的数据友好,但是当数据量比较小的时候,速度会相对慢。
    +lucene中使用searchafter来筛选顺序
  2. +
+
ScoreDoc lastBottom = null;//相当于pageSize
+BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
+QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
+Query query1 = parser1.parse(q);
+booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
+TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits);  //lastBottom(pageSize),pagehits(pagenum)
+
    +
  1. 使用效果
    +全部代码放在这里,代码写的不太好,光从代码规范上就不咋地。在网页上的使用效果如下:
  2. +
+
+

二、lucene自动补全

+

百度、谷歌等在输入文字的时候会弹出补全框,如下图:

+
+

在搭建lucene自动补全的时候,也有考虑过使用SQL语句中使用like来进行,主要还是like对数据库压力会大,而且相关度没有lucene的高。主要使用了官方suggest库以及autocompelte.js这个插件。
+suggest的原理看这,以及索引结构看这

+

使用:

+
    +
  1. 导入maven包
  2. +
+
<dependency>
+    <groupId>org.apache.lucene</groupId>
+    <artifactId>lucene-suggest</artifactId>
+    <version>6.6.0</version>
+</dependency>
+
    +
  1. 如果想将结果反序列化,声明实体类的时候要加上:
  2. +
+
public class Blog implements Serializable {
+
    +
  1. 实现InputIterator接口
  2. +
+
InputIterator的几个方法:
+long weight():返回的权重值,大小会影响排序,默认是1L
+BytesRef payload():对某个对象进行序列化
+boolean hasPayloads():是否有设置payload信息
+Set<BytesRef> contexts():存入context,context里可以是任意的自定义数据,一般用于数据过滤
+boolean hasContexts():判断是否有下一个,默认为false
+
public class BlogIterator implements InputIterator {
+    /**
+     * logger
+     */
+    private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
+    private Iterator<Blog> blogIterator;
+    private Blog currentBlog;
+
+    public BlogIterator(Iterator<Blog> blogIterator) {
+        this.blogIterator = blogIterator;
+    }
+
+    @Override
+    public boolean hasContexts() {
+        return true;
+    }
+
+    @Override
+    public boolean hasPayloads() {
+        return true;
+    }
+
+    public Comparator<BytesRef> getComparator() {
+        return null;
+    }
+
+    @Override
+    public BytesRef next() {
+        if (blogIterator.hasNext()) {
+            currentBlog = blogIterator.next();
+            try {
+                //返回当前Project的name值,把blog类的name属性值作为key
+                return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 将Blog对象序列化存入payload
+     * 可以只将所需要的字段存入payload,这里对整个实体类进行序列化,方便以后需求,不建议采用这种方法
+     */
+    @Override
+    public BytesRef payload() {
+        try {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream out = new ObjectOutputStream(bos);
+            out.writeObject(currentBlog);
+            out.close();
+            BytesRef bytesRef = new BytesRef(bos.toByteArray());
+            return bytesRef;
+        } catch (IOException e) {
+            logger.error("", e);
+            return null;
+        }
+    }
+
+    /**
+     * 文章标题
+     */
+    @Override
+    public Set<BytesRef> contexts() {
+        try {
+            Set<BytesRef> regions = new HashSet<BytesRef>();
+            regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
+            return regions;
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Couldn't convert to UTF-8");
+        }
+    }
+
+    /**
+     * 返回权重值,这个值会影响排序
+     * 这里以产品的销售量作为权重值,weight值即最终返回的热词列表里每个热词的权重值
+     */
+    @Override
+    public long weight() {
+        return currentBlog.getHits();   //change to hits
+    }
+}
+
    +
  1. ajax 建立索引
  2. +
+
/**
+ * ajax建立索引
+ */
+@Override
+public void ajaxbuild() {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        //创建Blog测试数据
+        List<Blog> blogs = blogMapper.getAllBlog();
+        suggester.build(new BlogIterator(blogs.iterator()));
+    } catch (IOException e) {
+        System.err.println("Error!");
+    }
+}
+
    +
  1. 查找
    +因为有些文章的标题是一样的,先对list排序,将标题短的放前面,长的放后面,然后使用LinkHashSet来存储。
  2. +
+
@Override
+public Set<String> ajaxsearch(String keyword) {
+    try {
+        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
+        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
+        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
+        List<String> list = lookup(suggester, keyword);
+        list.sort(new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                if (o1.length() > o2.length()) {
+                    return 1;
+                } else {
+                    return -1;
+                }
+            }
+        });
+        Set<String> set = new LinkedHashSet<>();
+        for (String string : list) {
+            set.add(string);
+        }
+        ssubSet(set, 7);
+        return set;
+    } catch (IOException e) {
+        System.err.println("Error!");
+        return null;
+    }
+}
+
    +
  1. controller层
  2. +
+
@RequestMapping("ajaxsearch")
+public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    String keyword = request.getParameter("keyword");
+    if (StringUtils.isEmpty(keyword)) {
+        return;
+    }
+    Set<String> set = blogService.ajaxsearch(keyword);
+    Gson gson = new Gson();
+    response.getWriter().write(gson.toJson(set));//返回的数据使用json
+}
+
    +
  1. ajax来提交请求
    +autocomplete.js源代码与介绍:https://github.com/xdan/autocomplete
  2. +
+
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
+<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
+<script type="text/javascript">
+    /******************** remote start **********************/
+    $('#remote_input').autocomplete({
+        source: [
+            {
+                url: "ajaxsearch.html?keyword=%QUERY%",
+                type: 'remote'
+            }
+        ]
+    });
+    /********************* remote end ********************/
+</script>
+
    +
  1. 效果:
    +
  2. +
+

欢迎访问我的个人网站

+

参考:
+https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/

+

http://iamyida.iteye.com/blog/2205114

+]]>
+ +
+ + 3.定时任务 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html + 3.定时任务 + 3.定时任务 先看一下Quartz的架构图: 一.特点: 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求; 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式; 分布式和集群能力。 二.主要组成部分 JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种... + Tue, 21 Jan 2020 02:38:05 GMT + 先看一下Quartz的架构图:

+
+

一.特点:

+
    +
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. +
  3. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  4. +
  5. 分布式和集群能力。
  6. +
+

二.主要组成部分

+
    +
  1. JobDetail:需实现该接口定义的人物,其中JobExecutionContext提供了上下文的各种信息。
  2. +
  3. JobDetail:QUartz的执行任务的类,通过newInstance的反射机制实例化Job。
  4. +
  5. Trigger: Job的时间触发规则。主要有SimpleTriggerImpl和CronTriggerImpl两个实现类。
  6. +
  7. Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。
  8. +
  9. Scheduler:由上图可以看出,Scheduler是Quartz独立运行的容器。其中,Trigger和JobDetail可以注册到Scheduler中。
  10. +
  11. ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
  12. +
+

三、Quartz设计

+
    +
  1. properties file
    +官网中表明:quartz中使用了quartz.properties来对quartz进行配置,并保留在其jar包中,如果没有定义则默认使用改文件。
  2. +
  3. +
+

四、使用

+
    +
  1. hello world!代码在这
  2. +
  3. 本网站中使用quartz来对数据库进行备份,与Spring结合
    +(1)导入spring的拓展包,其协助spring集成第三方库:邮件服务、定时任务、缓存等。。。
  4. +
+
<dependency>
+    <groupId>org.springframework</groupId>
+    <artifactId>spring-context-support</artifactId>
+    <version>4.2.6.RELEASE</version>
+</dependency>
+

(2)导入quartz包

+
<dependency>
+    <groupId>org.quartz-scheduler</groupId>
+    <artifactId>quartz</artifactId>
+    <version>2.3.0</version>
+</dependency>
+

(3)mysql远程备份
+使用命令行工具仅仅需要一行:

+
mysqldump -u [username] -p[password] -h [hostip] database > file
+

但是java不能直接执行linux的命令,仍旧需要依赖第三方库ganymed

+
<dependency>
+    <groupId>ch.ethz.ganymed</groupId>
+    <artifactId>ganymed-ssh2</artifactId>
+    <version>262</version>
+</dependency>
+

完整代码如下:

+
@Component("mysqlService")//在spring中注册一个mysqlService的Bean
+public class MysqlUtil {
+    ...
+    StringBuffer sb = new StringBuffer();
+    sb.append("mysqldump -u " + username + " -p" + password + " -h " + host + " " +
+            database + " >" + file);
+    String sql = sb.toString();
+    Connection connection = new Connection(s_host);
+    connection.connect();
+    boolean isAuth = connection.authenticateWithPassword(s_username, s_password);//进行远程服务器登陆认证
+    if (!isAuth) {
+        logger.error("server login error");
+    }
+    Session session = connection.openSession();
+    session.execCommand(sql);//执行linux语句
+    InputStream stdout = new StreamGobbler(session.getStdout());
+    BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
+    ...
+}
+

(4)spring中配置quartz

+
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
+    <property name="targetObject" ref="mysqlService"/>
+    <property name="targetMethod" value="exportDataBase"/>
+</bean>
+<!--定义触发时间  -->
+<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
+    <property name="jobDetail" ref="jobDetail"/>
+    <!-- cron表达式,每周五2点59分运行-->
+    <property name="cronExpression" value="0 59 2 ? * FRI"/>
+</bean>
+<!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 -->
+<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
+    <property name="triggers">
+        <list>
+            <ref bean="myTrigger"/>
+        </list>
+    </property>
+</bean>
+

(5)java完整文件在这

+

Spring的高级特性之定时任务

+

java ee项目的定时任务中除了运行quartz之外,spring3+还提供了task,可以看做是一个轻量级的Quartz,而且使用起来比Quartz简单的多。

+

(1)spring配置文件中配置:

+
<task:annotation-driven/>
+

(2)最简单的例子,在所需要的函数上添加定时任务即可运行

+
    @Scheduled(fixedRate = 5000)
+    public void reportCurrentTime() {
+        System.out.println("每隔5秒运行一次" + sdf.format(new Date()));
+    }
+

(3)运行的时候会报错:

+
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
+	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
+	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:163)
+	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:136)
+	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:380)
+

参考:
+1.http://blog.csdn.net/oarsman/article/details/52801877
+2.http://stackoverflow.com/questions/31199888/spring-task-scheduler-no-qualifying-bean-of-type-org-springframework-scheduli
+Spring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:

+

1.尝试从配置中找到一个TaskScheduler Bean

+

2.寻找ScheduledExecutorService Bean

+

3.使用默认的scheduler
+修改log4j.properties即可:
+log4j.logger.org.springframework.scheduling=INFO
+其实这个功能不影响定时器的功能。
+(4)结果:

+
每隔5秒运行一次14:44:34
+每隔5秒运行一次14:44:39
+每隔5秒运行一次14:44:44
+
]]>
+ +
+ + 4.日志系统.md + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html + 4.日志系统.md + 4.日志系统.md 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使... + Tue, 21 Jan 2020 02:38:05 GMT + 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

+

建立网站少不了日志系统,用来查看网站的访问次数、停留时间、抓取量、目录抓取统计、页面抓取统计等,其中,最常用的方法还是使用ELK,但是,本网站的服务器配置实在太低了(1GHZ、2G内存),压根就跑不起ELK,所以只能寻求其他方式,目前最常用的有百度统计友盟,这里,本人使用的是百度统计,提供了API给开发者使用,能够将自己所需要的图表移植到自己的网站上。日志是网站及其重要的文件,通过对日志进行统计、分析、综合,就能有效地掌握网站运行状况,发现和排除错误原因,了解客户访问分布等,更好的加强系统的维护和管理。下面是我的百度统计的概览页面:

+
+

企业级的网站日志不能公开,但是我的是个人网站,用来跟大家一起学习的,所以,需要将百度的统计页面展示出来,但是,百度并不提供日志的图像,只提供API给开发者调用,而且还限制访问次数,一天不能超过2000次,这个对于实时统计来说,确实不够,所以只能展示前几天的访问统计。这里的日志系统分为三个步骤:1.API获取数据;2.存储数据;3.展示数据。页面效果如下,也可以点开我的网站的日志系统:

+
+
+
+

百度统计提供了Tongji API的Java和Python版本,这两个版本及其复杂,可用性极低,所以,本人用Python写了个及其简单的通用版本,整体只有28行,代码在这,https://github.com/Zephery/baidutongji。下面是具体过程

+

1.网站代码安装

+

先在百度统计中注册登录之后,进入管理页面,新增网站,然后在代码管理中获取安装代码,大部分人的代码都是类似的,除了hm.js?后面的参数,是记录该网站的唯一标识。

+
<script>
+var _hmt = _hmt || [];
+(function() {
+  var hm = document.createElement("script");
+  hm.src = "https://hm.baidu.com/hm.js?code";
+  var s = document.getElementsByTagName("script")[0];
+  s.parentNode.insertBefore(hm, s);
+})();
+</script>
+

同时,需要在申请其他设置->数据导出服务中开通数据导出服务,百度统计Tongji API可以为网站接入者提供便捷的获取网站流量数据的通道。

+
+

至此,我们获得了username、password、token,然后开始使用三个参数来获取数据。

+

2.根据API获取数据

+

官网的API详细的记录了接口的参数以及解释,
+链接:https://api.baidu.com/json/tongji/v1/ReportService/getData,详细的官方报告请访问官网TongjiApi
+所需参数(必须):

+

| 参数名称 | 参数类型 |描述 |
+|

+]]>
+ +
+ + 5.小集群部署.md + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html + 5.小集群部署.md + 5.小集群部署.md 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog。 洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象... + Tue, 21 Jan 2020 02:38:05 GMT + 欢迎访问我的个人网站O(∩_∩)O哈哈~希望大佬们能给个star,个人网站网址:http://www.wenzhihuai.com,个人网站代码地址:https://github.com/Zephery/newblog
+洋洋洒洒的买了两个服务器,用来学习分布式、集群之类的东西,整来整去,感觉分布式这种东西没人指导一下真的是太抽象了,先从网站的分布式部署一步一步学起来吧,虽然网站本身的访问量不大==。

+

nginx负载均衡

+

一般情况下,当单实例无法支撑起用户的请求时,就需要就行扩容,部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区,比如:深圳的用户却访问到了北京的节点,然后还得从北京返回处理之后的数据,光是来回就至少得30ms。这部分可以通过智能DNS(就近访问)解决。而分机房,需要将请求合理的分配到不同的服务器,这部分就是我们所需要处理的。
+通常,负载均衡分为硬件和软件两种,硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层,LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡,但是暂不讨论(我木有硬件条件)。下图来自张开涛的《亿级流量网站架构核心技术》

+
+

本站并没有那么多的服务器,目前只有两台,搭建不了那么大型的架构,就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构:

+
+

其中服务器A(119.23.46.71)为深圳节点,服务器B(47.95.10.139)为北京节点,搭建Nginx之后流量是这么走的:user->A->B-A->user或者user->A->user,第一条中A将请求转发给B,然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习,所以请不要考虑因为地域导致延时的问题。。。。下面是过程。

+

1.1 Nginx的安装

+

可以选择tar.gz、yum、rpm安装等,这里,由于编译、nginx配置比较复杂,要是没有把握还是使用rpm来安装吧,比较简单。从https://pkgs.org/download/nginx可以找到最新的rpm包,然后rpm -ivh 文件,然后在命令行中输入nginx即可启动,可以使用netstat检查一下端口。

+
+

启动后页面如下:

+
+

记一下常用命令

+
启动nginx,由于是采用rpm方式,所以环境变量什么的都配置好了。
+[root@beijingali ~]# nginx          #启动nginx
+[root@beijingali ~]# nginx -s reload         #重启nginx
+[root@beijingali ~]# nginx -t           #校验nginx配置文件
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful
+

1.2 Nginx的配置

+

1.2.1 负载均衡算法

+

Nginx常用的算法有:
+(1)round-robin:轮询,nginx默认的算法,从词语上可以看出,轮流访问服务器,也可以通过weight来控制访问次数。
+(2)ip_hash:根据访客的ip,一个ip地址对应一个服务器。
+(3)hash算法:hash算法常用的方式有根据uri、动态指定的consistent_key两种。
+使用hash算法的缺点是当添加服务器的时候,只有少部分的uri能够被重新分配到新的服务器。这里,本站使用的是hash uri的算法,将不同的uri分配到不同的服务器,但是由于是不同的服务器,tomcat中的session是不一致,解决办法是tomcat session的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。

+
http{
+    ...
+    upstream backend {
+        hash $uri;
+        # 北京节点
+        server 47.95.10.139:8080;
+        # 深圳节点
+        server 119.23.46.71:8080;
+    }
+
+    server {
+        ...
+        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            ...
+        }
+    ...
+

1.2.2 日志格式

+

之前有使用过ELK来跟踪日志,所以将日志格式化成了json的格式,这里贴一下吧

+
    ...
+    log_format main '{"@timestamp":"$time_iso8601",'
+                    '"host":"$server_addr",'
+                    '"clientip":"$remote_addr",'
+                    '"size":$body_bytes_sent,'
+                    '"responsetime":$request_time,'
+                    '"upstreamtime":"$upstream_response_time",'
+                    '"upstreamhost":"$upstream_addr",'
+                    '"http_host":"$host",'
+                    '"url":"$uri",'
+                    '"xff":"$http_x_forwarded_for",'
+                    '"referer":"$http_referer",'
+                    '"agent":"$http_user_agent",'
+                    '"status":"$status"}';
+    access_log  logs/access.log  main;
+    ...
+

1.2.3 HTTP反向代理

+

配置完上流服务器之后,需要配置Http的代理,将请求的端口转发到proxy_pass设定的上流服务器,即当我们访问http://wwww.wenzhihuai.com的时候,请求会被转发到backend中配置的服务器,此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是,仔细注意之后,我们会发现,tomcat中的访问日志ip来源都是127.0.0.1,相当于本地访问自己的资源。由于后台中有处理ip的代码,对客户端的ip、访问uri等记录下来,所以需要设置nginx来获取用户的实际ip,参考nginx 配置。参考文中的一句话:经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,通过$remote_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的,也就是说nginx使用$remote_addr变量时获得的是用户的真实ip,如果我们想要在web端获得用户的真实ip,就必须在nginx这里作一个赋值操作,如下:

+
        location / {
+            root   html;
+            index  index.html index.htm;
+            proxy_pass http://backend;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Host $host;
+            proxy_set_header REMOTE-HOST $remote_addr;
+        }
+

(1)proxy_set_header X-real-ip $remote_addr;
+其中这个X-real-ip是一个自定义的变量名,名字可以随意取,这样做完之后,用户的真实ip就被放在X-real-ip这个变量里了,然后,在web端可以这样获取:
+request.getAttribute("X-real-ip")

+

(2)proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

+

X-Forwarded-For:squid开发的,用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准,这个不是默认有的,其经过代理转发之后,格式为client1, proxy1, proxy2,如果想通过这个变量来获取用户的ip,那么需要和$proxy_add_x_forwarded_for一起使用。
+$proxy_add_x_forwarded_for:现在的$proxy_add_x_forwarded_for变量,X-Forwarded-For部分包含的是用户的真实ip,$remote_addr部分的值是上一台nginx的ip地址,于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip,第一台nginx的ip”。

+

1.2.4 HTTPS

+

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。一般情况下,能通过服务器的ssh来生成ssl证书,但是如果使用是自己的,一般浏览器(谷歌、360等)都会报证书不安全的错误,正常用户都不敢访问吧==,所以现在使用的是腾讯跟别的机构颁发的:

+
+

首先需要下载证书,放在nginx.conf相同目录下,nginx上的配置也需要有所改变,在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥:

+
        ssl_certificate 1_www.wenzhihuai.com_bundle.crt;    #主要文件路径
+        ssl_certificate_key 2_www.wenzhihuai.com.key;
+        ssl_session_timeout 5m;         # 超时时间
+        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
+        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
+        ssl_prefer_server_ciphers on;
+

至此,可以使用https来访问了。https带来的安全性(保证信息安全、识别钓鱼网站等)是http远远不能比拟的,目前大部分网站都是实现全站https,还能将http自动重定向为https,此处,需要在server中添加rewrite ^(.*) https://$server_name$1 permanent;即可

+

1.2.5 失败重试

+

配置好了负载均衡之后,如果有一台服务器挂了怎么办?nginx中提供了可配置的服务器存活的识别,主要是通过max_fails失败请求次数,fail_timeout超时时间,weight为权重,下面的配置的意思是当服务器超时10秒,并失败了两次的时候,nginx将认为上游服务器不可用,将会摘掉上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试

+
upstream backend_server {
+    server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
+    server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
+}
+

session共享

+

分布式情况下难免会要解决session共享的问题,目前推荐的方法基本上都是使用redis,网上查找的方法目前流行的有下面四种,参考自tomcat 集群中 session 共
+1.使用 filter 方法存储。(推荐,因为它的服务器使用范围比较多,不仅限于tomcat ,而且实现的原理比较简单容易控制。)
+2.使用 tomcat sessionmanager 方法存储。(直接配置即可)
+3.使用 terracotta 服务器共享。(不知道,不了解)
+4.使用spring-session。(spring的一个小项目,其原理也和第一种基本一致)

+

本站使用spring-session,毕竟是spring下的子项目,学习下还是挺好的。参考Spring-Session官网。官方文档提供了spring-boot、spring等例子,可以参考参考。目前最新版本是2.0.0,不同版本使用方式不同,建议看官网的文档吧。

+

首先,添加相关依赖

+
        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-data-redis</artifactId>
+            <version>1.3.1.RELEASE</version>
+            <type>pom</type>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>${jedis.version}</version>
+        </dependency>
+

新建一个session.xml,然后在spring的配置文件中添加该文件,然后在session.xml中添加:

+
    <!-- redis -->
+    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
+    </bean>
+
+    <bean id="jedisConnectionFactory"
+          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
+        <property name="hostName" value="${host}" />
+        <property name="port" value="${port}" />
+        <property name="password" value="${password}" />
+        <property name="timeout" value="${timeout}" />
+        <property name="poolConfig" ref="jedisPoolConfig" />
+        <property name="usePool" value="true" />
+    </bean>
+
+    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
+        <property name="connectionFactory" ref="jedisConnectionFactory" />
+    </bean>
+
+    <!-- 将session放入redis -->
+    <bean id="redisHttpSessionConfiguration"
+          class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
+        <property name="maxInactiveIntervalInSeconds" value="1800" />
+    </bean>
+

然后我们需要保证servlet容器(tomcat)针对每一个请求都使用springSessionRepositoryFilter来拦截

+
<filter>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+</filter>
+<filter-mapping>
+    <filter-name>springSessionRepositoryFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+    <dispatcher>REQUEST</dispatcher>
+    <dispatcher>ERROR</dispatcher>
+</filter-mapping>
+

配置完成,使用RedisDesktopManager查看结果:

+
+

测试:

+

访问http://www.wenzhihuai.com
+tail -f localhost_access_log.2017-11-05.txt查看日志,然后清空一下当前记录

+
+

访问技术杂谈页面,此时nginx将请求转发到119.23.46.71服务器,session为28424f91-5bc5-4bba-99ec-f725401d7318。

+
+

点击生活笔记页面,转发到的服务器为47.95.10.139,session为28424f91-5bc5-4bba-99ec-f725401d7318,与上面相同。session已保持一致。

+
+

值得注意的是:同一个浏览器,在没有关闭的情况下,即使通过域名访问和ip访问得到的session是不同的。
+欢迎访问我的个人网站O(∩_∩)O哈哈~希望能给个star
+个人网站网址:http://www.wenzhihuai.com
+个人网站代码地址:https://github.com/Zephery/newblog

+]]>
+ +
+ + 6.数据库备份 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html + 6.数据库备份 + 6.数据库备份 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地: 由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下: 果然,... + Tue, 21 Jan 2020 02:38:05 GMT + 先来回顾一下上一篇的小集群架构,tomcat集群,nginx进行反向代理,服务器异地:

+
+

由上一篇讲到,部署的时候,将war部署在不同的服务器里,通过spring-session实现了session共享,基本的分布式部署还算是完善了点,但是想了想数据库的访问会不会延迟太大,毕竟一个服务器在北京,一个在深圳,然后试着ping了一下:

+
+

果然,36ms。。。看起来挺小的,但是对比一下sql执行语句的时间:

+
+

大部分都能在10ms内完成,而最长的语句是insert语句,可见,由于异地导致的36ms延时还是比较大的,捣鼓了一下,最后还是选择换个架构,每个服务器读取自己的数据库,然后数据库底层做一下主主复制,让数据同步。最终架构如下:

+
+

一、MySql的复制

+

数据库复制的基本问题就是让一台服务器的数据与其他服务器保持同步。MySql目前支持两种复制方式:基于行的复制和基于语句的复制,这两者的基本过程都是在主库上记录二进制的日志、在备库上重放日志的方式来实现异步的数据复制。其过程分为三步:
+(1)master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
+(2)slave将master的binary log events拷贝到它的中继日志(relay log);
+(3)slave重做中继日志中的事件,将改变反映它自己的数据。

+
+

该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。
+下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。
+SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。
+此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。
+MySql的基本复制方式有主从复制、主主复制,主主复制即把主从复制的配置倒过来再配置一遍即可,下面的配置则是主从复制的过程,到时候可自行改为主主复制。其他的架构如:一主库多备库、环形复制、树或者金字塔型都是基于这两种方式,可参考《高性能MySql》。

+

二、配置过程

+

2.1 创建所用的复制账号

+

由于是个自己的小网站,就不做过多的操作了,直接使用root账号

+

2.2 配置master

+

接下来要对mysql的serverID,日志位置,复制方式等进行操作,使用vim打开my.cnf。

+
[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# master
+log-bin=mysql-bin
+# 设为基于行的复制
+binlog-format=ROW
+# 设置server的唯一id
+server-id=2
+# 忽略的数据库,不使用备份
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 要进行备份的数据库
+binlog-do-db=myblog
+

重启Mysql之后,查看主库状态,show master status。

+
+

其中,File为日志文件,指定Slave从哪个日志文件开始读复制数据,Position为偏移,从哪个POSITION号开始读,Binlog_Do_DB为要备份的数据库。

+

2.3 配置slave

+

从库的配置跟主库类似,vim /etc/my.cnf配置从库信息。

+

+[client]
+default-character-set=utf8
+
+[mysqld]
+character_set_server=utf8
+init_connect= SET NAMES utf8
+
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+
+# slave
+log-bin=mysql-bin
+# 服务器唯一id
+server-id=3
+# 不备份的数据库
+binlog-ignore-db=information_schema
+binlog-ignore-db=cluster
+binlog-ignore-db=mysql
+# 需要备份的数据库
+replicate-do-db=myblog
+# 其他相关信息
+slave-skip-errors=all
+slave-net-timeout=60
+# 开启中继日志
+relay_log         = mysql-relay-bin
+# 
+log_slave_updates = 1
+# 防止改变数据
+read_only         = 1
+

重启slave,同时启动复制,还需要调整一下命令。

+
mysql> CHANGE MASTER TO MASTER_HOST = '119.23.46.71', MASTER_USER = 'root', MASTER_PASSWORD = 'helloroot', MASTER_PORT = 3306, MASTER_LOG_FILE = 'mysql-bin.000009', MASTER_LOG_POS = 346180;
+

可以看见slave已经开始进行同步了。我们使用show slave status\G来查看slave的状态。

+
+

其中日志文件和POSITION不一致是合理的,配置好了的话,即使重启,也不会影响到主从复制的配置。

+

某天在Github上漂游,发现了阿里的canal,同时才知道上面这个业务是叫异地跨机房同步,早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。下面是基本的原理:

+
+

原理相对比较简单:

+

1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
+2.mysql master收到dump请求,开始推送binary log给slave(也就是canal)
+3.canal解析binary log对象(原始为byte流)

+

其中,配置过程如下:https://github.com/alibaba/canal,可以搭配Zookeeper使用。在ZKUI中能够查看到节点:

+
+

一般情况下,还要配合阿里的另一个开源产品使用otter,相关文档还是找找GitHub吧,个人搭建完了之后,用起来还是不如直接使用mysql的主主复制,而且异地机房同步这种大企业才有的业务。

+

公司又要996了,实在是忙不过来,感觉自己写的还是急躁了点,困==

+]]>
+ +
+ + 7.那些牛逼的插件 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html + 7.那些牛逼的插件 + 7.那些牛逼的插件 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog 。 建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Ja... + Tue, 21 Jan 2020 02:38:05 GMT + 欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog
+建站的一开始,我也想自己全部实现,各种布局,各种炫丽的效果,想做点能让大家佩服的UI出来,但是,事实上,自己作为专注Java的程序员,前端的东西一碰脑子就有“我又不是前端,浪费时间在这合适么?”这种想法,捣鼓来捣鼓去,做出的东西实在是没法看,我就觉得,如果自己的“产品”连自己都看不下去了,那还好意思给别人看?特别是留言板那块,初版的页面简直low的要死。所以,还是踏踏实实的“站在巨人的肩膀上”吧,改用别人的插件。但不要纯粹的使用别人的博客模板了,如hexo,wordpress这些,就算是自己拼凑过来的也比这些强。下面是本博客中所用到的插件,给大家介绍介绍,共同学习学习。
+本站主要用到的插件有:
+1.wowslider
+2.畅言
+3.Editor.md
+4.highchart、echart
+5.百度分享
+6.waterfall.js
+7.心知天气
+8.标签云

+

wowslider

+

可能是我这网站中最炫的东西了,图片能够自动像幻灯片一样自动滚动,让网站的首页一看起来就高大上,简直就是建站必备的东西,而且安装也及其简单,有兴趣可以点击官网看看。GitHub里也开放了源代码。安装过程:自己选择“幻灯片”切换效果,保存为html就行了,WORDPREESS中好像有集成这个插件的,做的还更好。感兴趣可以点击我的博客首页看一看。

+
+

不过还有个值得注意的问题,就是wowslider里面带有一个googleapis的服务,即https://fonts.googleapis.com/css?family=Arimo&subset=latin,cyrillic,latin-ext,由于一般用户不能访问谷歌,会导致网页加载速度及其缓慢,所以,去掉为妙

+

畅言

+

作为社交评论的工具,虽然说表示还是想念以前的多说,但是畅言现在做得还是好了,有评论审核,评论导出导入等功能,如果浏览量大的话,还能提供广告服务,让站长也能拿到一丢丢的广告费。本博客中使用了畅言的基本评论、获取某篇文章评论数的功能。可以到我这里留言

+
+

Editor.md

+

一款能将markdown解析为html的插件,国人的作品,博客的文章编辑器一开始想使用的是markdown,想法是:写文章、保存数据库都是markdown格式的,保存在数据库中,读取时有需要解析markdown,这个过程是有点耗时的,但是相比把html式的网页保存在数据库中友好点吧,因为如果一篇文章比较长的话,转成html的格式,光是大小估计也得超过几十kb?所以,还是本人选择的是一切都用源markdown。
+editor.md,是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。页面看起来还是美观的,相比WORDPRESS的那些牛逼插件还差那么一点点,不过从普通人的眼光来看,应该是可以的了。此处,我用它作为解析网页的利器,还有就是后台编辑也是用的这个。

+
+

代码样式,这一点是不如WORDPRESS的插件了,不过已经可以了。

+
+

图表

+

目前最常用的是highcharts跟echart,目前个人博客中的日志系统主要还是采用了highcharts,主要还是颜色什么的格调比较相符吧,其次是因为对echarts印象不太友好,比如下面做这张,打开网页后,缩小浏览器,百度的地域图却不能自适应,出现了越界,而highcharts的全部都能自适应调整。想起有次面试,我说我用的highcharts,面试官一脸嫌弃。。。(网上这么多人鄙视百度是假的?)

+
+

不过地图确确实实是echarts的优势,毕竟还是自家的东西了解自家,不过前段时间去看了看echarts的官网,已经不提供下载了。如果有需要,还是去csdn上搜索吧,或者替换为highmap。

+
+

百度分享

+

作为一个以博客为主的网站,免不了使用一些社会化分享的工具,目前主要是jiathis和百度分享,这两者的ui都是相似的(丑爆了)。凭我个人的感觉,jiathis加载实在是太过于缓慢,这点是无法让人忍受的,只好投靠百度。百度分享类似于jiathis,安装也很简单,具体见官网http://share.baidu.com/code/advance#tools。一直点点点,配置完之后,就是下图这种,丑爆了是不是?

+
+

好在对它的美观改变不是很难,此处参考了别人的UI设计,原作者我忘记怎么联系他了。其原理主要是使用图片来替换掉原本的东西。完整的源码可以点击此处

+
#share a {
+    width: 34px;
+    height: 34px;
+    padding: 0;
+    margin: 6px;
+    border-radius: 25px;
+    transition: all .4s;
+}
+/*主要的是替换掉backgound的背景图片*/
+#share a.bds_qzone {
+    background: url(http://image.wenzhihuai.com/t_QQZone.png) no-repeat;
+    background-size: 34px 34px;
+}
+

改完之后的效果。

+
+

瀑布流

+

有段时间,瀑布流特别流行?还有段时间,瀑布流开始遭到各种抵制。。。看看知乎的人怎么说,大部分人不喜欢的原因是让人觉得视觉疲劳,不过瀑布流最大的好处还是有的:提高发现好图的效率以及图片列表页极强的视觉感染力。没错,我还是想把自己的网站弄得铉一些==,所以采用了瀑布流(不过效果不是很好,某些浏览器甚至加载出错),这个大bug有时间再改,毕竟花了很多时间做的这个,效果确实不咋地。目前主要的瀑布流有waterfall.jsmasory.js。这一块目前还不是很完善,希望能得到各位大佬的指点。

+
+

天气插件

+

此类咨询服务还是网上还是挺多的,这一块不知道是不是所谓的“画蛇添足”这部分,主要是我觉得网站右边这部分老是少了点什么,所以加上了天气插件。目前常用的天气插件有中国天气网心知天气等。安装方式见各自的官网,这里不再阐述,我使用的是心知天气。注意:心知天气限制流量的,一个小时内只能使用400次,如果超出了会失效,当然也可以付费使用。

+
+

标签云

+

标签云,弄得好的话应该说是一个网站的点缀。现在好像比较流行3D的标签云?像下面这种。

+
+

从个人的网站风格来看,比较适应PHP形式的,有点颜色而又不绚丽的即可,之前用的跟分类的一样的样式,即双纵列的样式,美观度还行,虽然老是感觉有点怪怪的,如果某个标签的字数过长怎么办,岂不是要顶出div了。所以还是选择换另一种风格,最终偶然一次找到了下面这种,能够自适应宽度,颜色虽然鲜艳了点(以后有空再调一下吧),源码见style.css。 下图为目前的标签页。

+
+

总结

+

作为一个后端人员,调css和js真是痛苦==,好在坚持下来了,虽然还是很多不足,以后有时间慢慢改。说了那么多,感觉自己还是菜的抠脚。
+题外话,搭建一个博客,对于一个新近程序员来说真的是锻炼自己的一个好机会,能够认识到从前端、java后台、linux、jvm等等知识,只是真的有点耗时间(还不如把时间耗在Spring源码),如果不采用别人的框架的话,付出的代价还是蛮大的(所以不要鄙视我啦)。没有什么能够一举两得,看自己的取舍吧。加油💪(ง •_•)ง

+

欢迎访问我的网站http://www.wenzhihuai.com/ 。感谢,如果可以,希望能在GitHub上给个star,GitHub地址https://github.com/Zephery/newblog

+]]>
+ +
+ + 一次jvm调优过程 + http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html + http://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html + 一次jvm调优过程 + 一次jvm调优过程 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。 该程序task主要分为... + Tue, 21 Jan 2020 02:28:56 GMT + 前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
+该程序task主要分为三个模块:
+console进行一些cron的配置(表达式、任务名称、任务组等);
+schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
+client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
+整体架构跟github上开源的xxl-job类似,也可以参考一下。

+

1. 启用jmx和远程debug模式

+

容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:

+
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n "
+JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
+

其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。

+
+
+

堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。

+

2. memory analyzer、jprofiler进行堆内存分析

+

先从容器中dump出堆内存

+
jmap -dump:live,format=b,file=heap.hprof 58
+
+

由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。

+

3. netty的方面的考虑

+

另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。
+首先是查看堆内存的中占比:

+
+

可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:

+
+

达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。

+
+

声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。

+
+

最终解决办法:
+在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully

+
+

提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了

+
+

还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。

+
+

4. 直接内存排查

+

第一个想到的就是netty的直接内存,关掉,命令如下:

+
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced
+
+

查看了一下java的nio直接内存,发现也就几十kb,然而直接内存还是慢慢往上涨。毫无头绪,然后开始了自己的从linux层面开始排查问题

+

5. 推荐的直接内存排查方法

+

5.1 pmap

+

一般配合pmap使用,从内核中读取内存块,然后使用views 内存块来判断错误,我简单试了下,乱码,都是二进制的东西,看不出所以然来。

+
pmap -d 58  | sort -n -k2
+pmap -x 58  | sort -n -k3
+grep rw-p /proc/$1/maps | sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done
+

这个时候真的懵了,不知道从何入手了,难道是linux操作系统方面的问题?

+

5.2 [gperftools](https://github.com/gperftools/gperftools)

+

起初,在网上看到有人说是因为linux自带的glibc版本太低了,导致的内存溢出,考虑一下。初步觉得也可能是因为这个问题,所以开始慢慢排查。oracle官方有一个jemalloc用来替换linux自带的,谷歌那边也有一个tcmalloc,据说性能比glibc、jemalloc都强,开始换一下。
+根据网上说的,在容器里装libunwind,然后再装perf-tools,然后各种捣鼓,到最后发现,执行不了,

+
pprof --text /usr/bin/java java_58.0001.heap
+
+

看着工具高大上的,似乎能找出linux的调用栈,

+

6. 意外的结果

+

毫无头绪的时候,回想到了linux的top命令以及日志情况,测试环境是由于太多执行端业务方都没有维护,导致调度系统一直会出错,一出错就会导致大量刷错误日志,平均一天一个容器大概就有3G的日志,cron一旦到准点,就会有大量的任务要同时执行,而且容器中是做了对io的限制,磁盘也限制为10G,导致大量的日志都堆积在buff/cache里面,最终直接内存一直在涨,这个时候,系统不会挂,但是先会一直显示内存使用率达到100%。
+修复后的结果如下图所示:

+
+
+

总结

+

定时调度这个系统当时并没有考虑到公司的系统会用的这么多,设计的时候也仅仅是为了实现上千的量,没想到到最后变成了一天的调度都有几百万次。最初那批开发也就使用了大量的本地缓存map来临时存储数据,然后面向简历编程各种用netty自己实现了通信的方式,一堆坑都留给了后人。目前也算是解决掉了一个由于线程过多导致系统不可用的情况而已,但是由于存在大量的map,系统还是得偶尔重启一下比较好。

+

参考:
+1.记一次线上内存泄漏问题的排查过程
+2.Java堆外内存增长问题排查Case
+3.Troubleshooting Native Memory Leaks in Java Applications

+]]>
+ +
+ + Spark + http://www.wenzhihuai.com/bigdata/ + http://www.wenzhihuai.com/bigdata/ + Spark + Spark + Sun, 25 Aug 2019 08:27:20 GMT + + + + Java + http://www.wenzhihuai.com/java/ + http://www.wenzhihuai.com/java/ + Java + Java + Sun, 25 Aug 2019 08:27:20 GMT + + + + 目录 + http://www.wenzhihuai.com/kubernetes/ + http://www.wenzhihuai.com/kubernetes/ + 目录 + 目录 包含CICD、Kubernetes + Sun, 25 Aug 2019 08:27:20 GMT + 包含CICD、Kubernetes

+]]>
+
+ + 8.基于贝叶斯的情感分析 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html + 8.基于贝叶斯的情感分析 + 8.基于贝叶斯的情感分析 + Sun, 25 Aug 2019 08:27:20 GMT + + + + 9.网站性能优化 + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html + http://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html + 9.网站性能优化 + 9.网站性能优化 + Sun, 25 Aug 2019 08:27:20 GMT + + + + DevOps平台.md + http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html + http://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html + DevOps平台.md + DevOps平台.md DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。 公司技术部目前几百人左右吧... + Thu, 22 Aug 2019 01:35:46 GMT + DevOps定义(来自维基百科): DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

+

公司技术部目前几百人左右吧,但是整个技术栈还是比较落后的,尤其是DevOps、容器这一块,需要将全线打通,当时进来也主要是负责DevOps这一块的工作,应该说也是没怎么做好,其中也走了不少弯路,下面主要是自己踩过的坑吧。

+

一、自由风格的软件项目

+

主要还是基于jenkins里面构建一个自由风格的软件项目,当时参考的是阿里的codepipeline,就是对jenkins封装一层,包括创建job、立即构建、获取构建进度等都进行封装,并将需要的东西进行存库,没有想到码代码的时候,一堆的坑,比如: 1.连续点击立即构建,jenkins是不按顺序返回的,(分布式锁解决) 2.跨域调用,csrf,这个还好,不过容易把jenkins搞的无法登录(注意配置,具体可以点击这里) 3.创建job的时候只支持xml格式,还要转换一下,超级坑(xstream强行转换) 4.docker构建的时候,需要挂载宿主机的docker(想过用远程的,但效率不高) 5.数据库与jenkins的job一致性问题,任务创建失败,批量删除太慢(目前没想好怎么解决) 6.由于使用了数据库,需要检测job是否构建完成,为了自定义参数,我们自写了个通知插件,将构建状态返回到kafka,然后管理平台在进行消息处理。

+

完成了以上的东西,不过由于太过于简单,导致只能进行单条线的CICD,而且CI仅仅实现了打包,没有将CD的过程一同串行起来。简单来说就是,用户点击了构建只是能够打出一个镜像,但是如果要部署到kubernetes,还是需要在应用里手动更换一下镜像版本。总体而言,这个版本的jenkins我们使用的还是单点的,不足以支撑构建量比较大的情况,甚至如果当前服务挂了,断网了,整一块的构建功能都不能用。

+
<project>
+    <actions/>
+    <description>xxx</description>
+    <properties>
+        <hudson.model.ParametersDefinitionProperty>
+            <parameterDefinitions>
+                <hudson.model.TextParameterDefinition>
+                    <name>buildParam</name>
+                    <defaultValue>v1</defaultValue>
+                </hudson.model.TextParameterDefinition>
+                <hudson.model.TextParameterDefinition>
+                    <name>codeBranch</name>
+                    <defaultValue>master</defaultValue>
+                </hudson.model.TextParameterDefinition>
+            </parameterDefinitions>
+        </hudson.model.ParametersDefinitionProperty>
+    </properties>
+    <scm class="hudson.plugins.git.GitSCM">
+        <configVersion>2</configVersion>
+        <userRemoteConfigs>
+            <hudson.plugins.git.UserRemoteConfig>
+                <url>http://xxxxx.git</url>
+                <credentialsId>002367566a4eb4bb016a4eb723550054</credentialsId>
+            </hudson.plugins.git.UserRemoteConfig>
+        </userRemoteConfigs>
+        <branches>
+            <hudson.plugins.git.BranchSpec>
+                <name>${codeBranch}</name>
+            </hudson.plugins.git.BranchSpec>
+        </branches>
+        <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+        <extensions/>
+    </scm>
+    <builders>
+        <hudson.tasks.Shell>
+            <command>ls</command>
+        </hudson.tasks.Shell>
+        <hudson.tasks.Maven>
+            <targets>clean package install -Dmaven.test.skip=true</targets>
+            <mavenName>mvn3.5.4</mavenName>
+        </hudson.tasks.Maven>
+        <com.cloudbees.dockerpublish.DockerBuilder>
+            <server>
+                <uri>unix:///var/run/docker.sock</uri>
+            </server>
+            <registry>
+                <url>http://xxxx</url>
+            </registry>
+            <repoName>xxx/xx</repoName>
+            <forcePull>true</forcePull>
+            <dockerfilePath>Dockerfile</dockerfilePath>
+            <repoTag>${buildParam}</repoTag>
+            <skipTagLatest>true</skipTagLatest>
+        </com.cloudbees.dockerpublish.DockerBuilder>
+    </builders>
+    <publishers>
+        <com.xxxx.notifications.Notifier/>
+    </publishers>
+</project>
+

二、优化之后的CICD

+

上面的过程也仍然没有没住DevOps的流程,人工干预的东西依旧很多,由于上级急需产出,所以只能将就着继续下去。我们将构建、部署每个当做一个小块,一个CICD的过程可以选择构建、部署,花了很大的精力,完成了串行化的别样的CICD。 以下图为例,整个流程的底层为:paas平台-jenkins-kakfa-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发构建-jenkins-kafka-管理平台(选择cicd的下一步)-kafka-cicd组件调用管理平台触发部署。

+
+

目前实现了串行化的CICD构建部署,之后考虑实现多个CICD并行,并且一个CICD能够调用另一个CICD,实际运行中,出现了一大堆问题。由于经过的组件太多,一次cicd的运行报错,却很难排查到问题出现的原因,业务方的投诉也开始慢慢多了起来,只能说劝导他们不要用这个功能。

+

没有CICD,就无法帮助公司上容器云,无法合理的利用容器云的特性,更无法走上云原生的道路。于是,我们决定另谋出路。

+

三、调研期

+

由于之前的CICD问题太多,特别是经过的组件太多了,导致出现问题的时候无法正常排查,为了能够更加稳定可靠,还是决定了要更换一下底层。 我们重新审视了下pipeline,觉得这才是正确的做法,可惜不知道如果做成一个产品样子的东西,用户方Dockerfile都不怎么会写,你让他写一个Jenkinsfile?不合理!在此之外,我们看到了serverless jenkins、谷歌的tekton。 GitLab-CICD Gitlab中自带了cicd的工具,需要配置一下runner,然后配置一下.gitlab-ci.yml写一下程序的cicd过程即可,构建镜像的时候我们使用的是kaniko,整个gitlab的cicd在我们公司小项目中大范围使用,但是学习成本过高,尤其是引入了kaniko之后,还是寻找一个产品化的CICD方案。

+

分布式构建jenkins x 首先要解决的是多个构建同时运行的问题,很久之前就调研过jenkins x,它必须要使用在kubernetes上,由于当时官方文档不全,而且我们的DevOps项目处于初始期,所有没有使用。jenkins的master slave结构就不多说了。jenkins x应该说是个全家桶,包含了helm仓库、nexus仓库、docker registry等,代码是jenkins-x-image

+
+

serverless jenkins 好像跟谷歌的tekton相关,用了下,没调通,只能用于GitHub。感觉还不如直接使用tekton。

+

阿里云云效 提供了图形化配置DevOps流程,支持定时触发,可惜没有跟gitlab触发结合,如果需要个公司级的DevOps,需要将公司的jira、gitlab、jenkins等集合起来,但是图形化jenkins pipeline是个特别好的参考方向,可以结合阿里云云效来做一个自己的DevOps产品。

+

微软Pipeline 微软也是提供了DevOps解决方案的,也是提供了yaml格式的写法,即:在右边填写完之后会转化成yaml。如果想把DevOps打造成一款产品,这样的设计显然不是最好的。

+
+

谷歌tekton kubernetes的官方cicd,目前已用于kubernetes的release发版过程,目前也仅仅是与GitHub相结合,gitlab无法使用,全过程可使用yaml文件来创建,跑起来就是类似kubernetes的job一样,用完即销毁,可惜目前比较新,依旧处于alpha版本,无法用于生产。有兴趣可以参考下:Knative 初体验:CICD 极速入门

+

四、产品化后的DevOps平台

+

在调研DockOne以及各个产商的DevOps产品时,发现,真的只有阿里云的云效才是真正比较完美的DevOps产品,用户不需要知道pipeline的语法,也不需要掌握kubernetes的相关知识,甚至不用写yaml文件,对于开发、测试来说简直就是神一样的存在了。云效对小公司(创业公司)免费,但是有一定的量之后,就要开始收费了。在调研了一番云效的东西之后,发现云效也是基于jenkins x改造的,不过阿里毕竟人多,虽然能约莫看出是pipeline的语法,但是阿里彻底改造成了能够使用yaml来与后台交互。 下面是以阿里云的云效界面以及配合jenkins的pipeline语法来讲解:

+

4.1 Java代码扫描

+

PMD是一款可拓展的静态代码分析器它不仅可以对代码分析器,它不仅可以对代码风格进行检查,还可以检查设计、多线程、性能等方面的问题。阿里云的是简单的集成了一下而已,对于我们来说,底层使用了sonar来接入,所有的代码扫描结果都接入了sonar。

+
+
stage('Clone') {
+    steps{
+        git branch: 'master', credentialsId: 'xxxx', url: "xxx"
+    }
+}
+stage('check') {
+    steps{
+        container('maven') {
+            echo "mvn pmd:pmd"
+        }
+    }
+}
+

4.2 Java单元测试

+

Java的单元测试一般用的是Junit,在阿里云中,使用了surefire插件,用来在maven构建生命周期的test phase执行一个应用的单元测试。它会产生两种不同形式的测试结果报告。我这里就简单的过一下,使用"mvn test"命令来代替。

+
+

+stage('Clone') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+    }
+}
+stage('test') {
+    steps{
+        container('maven') {
+            sh "mvn test"
+        }
+    }
+}
+

4.3 Java构建并上传镜像

+

镜像的构建比较想使用kaniko,尝试找了不少方法,到最后还是只能使用dind(docker in docker),挂载宿主机的docker来进行构建,如果能有其他方案,希望能提醒下。目前jenkins x使用的是dind,挂载的时候需要配置一下config.json,然后挂载到容器的/root/.docker目录,才能在容器中使用docker。

+
+
+

为什么不推荐dind:挂载了宿主机的docker,就可以使用docker ps查看正在运行的容器,也就意味着可以使用docker stop、docker rm来控制宿主机的容器,虽然kubernetes会重新调度起来,但是这一段的重启时间极大的影响业务。

+
+
+

+stage('下载代码') {
+    steps{
+        echo "1.Clone Stage"
+        git branch: 'master', credentialsId: 'xxxxx', url: "xxxxxx"
+        script {
+            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+        }
+    }
+}
+stage('打包并构建镜像') {
+    steps{
+        container('maven') {
+            echo "3.Build Docker Image Stage"
+            sh "mvn clean install -Dmaven.test.skip=true"
+            sh "docker build -f xxx/Dockerfile -t xxxxxx:${build_tag} ."
+            sh "docker push xxxxxx:${build_tag}"
+        }
+    }
+}
+

4.4 部署到阿里云k8s

+

CD过程有点困难,由于我们的kubernetes平台是图形化的,类似于阿里云,用户根本不需要自己写deployment,只需要在图形化界面做一下操作即可部署。对于CD过程来说,如果应用存在的话,就可以直接替换掉镜像版本即可,如果没有应用,就提供个简单的界面让用户新建应用。当然,在容器最初推行的时候,对于用户来说,一下子需要接受docker、kubernetes、helm等概念是十分困难的,不能一个一个帮他们写deployment这些yaml文件,只能用helm创建一个通用的spring boot或者其他的模板,然后让业务方修改自己的配置,每次构建的时候只需要替换镜像即可。

+
def tmp = sh (
+    returnStdout: true,
+    script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+)
+//如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+if(tmp.equals('')){
+    sh "helm init --client-only"
+    sh """helm repo add mychartmuseum http://xxxxxx \
+                       --username myuser \
+                       --password=mypass"""
+    sh """helm install --set name=${JOB_NAME} \
+                       --set namespace=${namespace} \
+                       --set deployment.image=${image} \
+                       --set deployment.imagePullSecrets=${harborProject} \
+                       --name ${namespace}-${JOB_NAME} \
+                       mychartmuseum/soa-template"""
+}else{
+    println "已经存在,替换镜像"
+    //epaas中一个pod的容器名称需要带上"-0"来区分
+    sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+}
+

4.5 整体流程

+

代码扫描,单元测试,构建镜像三个并行运行,等三个完成之后,在进行部署

+
+

pipeline:

+
pipeline {
+    agent {
+        label "jenkins-maven"
+    }
+    stages{
+        stage('代码扫描,单元测试,镜像构建'){
+            parallel {
+                stage('并行任务一') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java代码扫描') {
+                        stage('Clone') {
+                            steps{
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('check') {
+                            steps{
+                                container('maven') {
+                                    echo "$BUILD_NUMBER"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务二') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('Java单元测试') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                            }
+                        }
+                        stage('test') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn -v"
+                                }
+                            }
+                        }
+                    }
+                }
+                stage('并行任务三') {
+                    agent {
+                        label "jenkins-maven"
+                    }
+                    stages('java构建镜像') {
+                        stage('Clone') {
+                            steps{
+                                echo "1.Clone Stage"
+                                git branch: 'master', credentialsId: 'xxxxxxx', url: "xxxxxxx"
+                                script {
+                                    build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
+                                }
+                            }
+                        }
+                        stage('Build') {
+                            steps{
+                                container('maven') {
+                                    echo "3.Build Docker Image Stage"
+                                    sh "mvn clean install -Dmaven.test.skip=true"
+                                    sh "docker build -f epaas-portal/Dockerfile -t hub.gcloud.lab/rongqiyun/epaas:${build_tag} ."
+                                    sh "docker push hub.gcloud.lab/rongqiyun/epaas:${build_tag}"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        stage('部署'){
+            stages('部署到容器云') {
+                stage('check') {
+                    steps{
+                        container('maven') {
+                            script{
+                                if (deploy_app == "true"){
+                                    def tmp = sh (
+                                        returnStdout: true,
+                                        script: "kubectl get deployment -n ${namespace} | grep ${JOB_NAME} | awk '{print \$1}'"
+                                    )
+                                    //如果是第一次,则使用helm模板创建,创建完后需要去epaas修改pod的配置
+                                    if(tmp.equals('')){
+                                        sh "helm init --client-only"
+                                        sh """helm repo add mychartmuseum http://xxxxxx \
+                                                           --username myuser \
+                                                           --password=mypass"""
+                                        sh """helm install --set name=${JOB_NAME} \
+                                                           --set namespace=${namespace} \
+                                                           --set deployment.image=${image} \
+                                                           --set deployment.imagePullSecrets=${harborProject} \
+                                                           --name ${namespace}-${JOB_NAME} \
+                                                           mychartmuseum/soa-template"""
+                                    }else{
+                                        println "已经存在,替换镜像"
+                                        //epaas中一个pod的容器名称需要带上"-0"来区分
+                                        sh "kubectl set image deployment/${JOB_NAME} ${JOB_NAME}-0=${image} -n ${namespace}"
+                                    }
+                                }else{
+                                    println "用户选择不部署代码"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+

在jenkins x中查看:

+
+

4.4 日志

+

jenkins blue ocean步骤日志:

+
+

云效中的日志:

+
+

4.5 定时触发

+
+
    triggers {
+        cron('H H * * *') //每天
+    }
+

五、其他

+

5.1 Gitlab触发

+

pipeline中除了有对于时间的trigger,还支持了gitlab的触发,需要各种配置,不过如果真的对于gitlab的cicd有要求,直接使用gitlab-ci会更好,我们同时也对gitlab进行了runner的配置来支持gitlab的cicd。gitlab的cicd也提供了构建完后即销毁的过程。

+

六、总结

+

功能最强大的过程莫过于自己使用pipeline脚本实现,选取最适合自己的,但是对于一个公司来说,如果要求业务方来掌握这些,特别是IT流动性大的时候,既需要重新培训,同个问题又会被问多遍,所以,只能将DevOps实现成一个图形化的东西,方便,简单,相对来说功能还算强大。

+

DevOps最难的可能都不是以上这些,关键是让用户接受,容器云最初推行时,公司原本传统的很多发版方式都需要进行改变,有些业务方不愿意改,或者有些代码把持久化的东西存到了代码中而不是分布式存储里,甚至有些用户方都不愿意维护老代码,看都不想看然后想上容器,一个公司在做技术架构的时候,过于混乱到最后填坑要么需要耗费太多精力甚至大换血。

+

最后,DevOps是云原生的必经之路!!!

+

文章同步:
+博客园:https://www.cnblogs.com/w1570631036/p/11524673.html
+个人网站:http://www.wenzhihuai.com/getblogdetail.html?blogid=663
+gitbook:https://gitbook.wenzhihuai.com/devops/devops-ping-tai

+]]>
+ +
+
+
\ No newline at end of file diff --git a/rss.xsl b/rss.xsl new file mode 100644 index 00000000..97256599 --- /dev/null +++ b/rss.xsl @@ -0,0 +1,506 @@ + + + + + + + RSS Feed + + + + + + +
+ + + +

+ +

+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Language: + +
Published Date: + +
Last Build Date: + +
Copyright: + +
+ Catetory: + + + , + + +
+
+ +
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + , + + + + + + + + + + + +
+
+
+ +
+ +
+
+
+ + + +
+
diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..b347dea9 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + +http://www.wenzhihuai.com/2024-05-20T16:57:46.000Zdailyhttp://www.wenzhihuai.com/about-the-author/2024-01-25T17:34:45.000Zdailyhttp://www.wenzhihuai.com/bigdata/2024-02-06T07:53:39.000Zdailyhttp://www.wenzhihuai.com/cloudnative/2024-01-24T03:25:58.000Zdailyhttp://www.wenzhihuai.com/database/2024-02-03T17:57:14.000Zdailyhttp://www.wenzhihuai.com/donate/2024-01-26T17:18:59.000Zdailyhttp://www.wenzhihuai.com/interesting/2024-11-29T18:45:10.000Zdailyhttp://www.wenzhihuai.com/interesting/chatgpt.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/idea%E8%AE%BE%E7%BD%AE.html2024-05-12T03:58:39.000Zdailyhttp://www.wenzhihuai.com/interesting/starcraft-ai.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/tesla.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%80%E4%BA%9B%E4%B8%8D%E5%B8%B8%E7%94%A8%E4%BD%86%E5%BE%88%E5%AE%9E%E7%94%A8%E7%9A%84%E5%91%BD%E4%BB%A4.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E5%9F%BA%E4%BA%8EOLAP%E5%81%9A%E4%B8%9A%E5%8A%A1%E7%9B%91%E6%8E%A7.html2024-11-10T15:55:36.000Zdailyhttp://www.wenzhihuai.com/interesting/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%8F%8D%E7%BC%96%E8%AF%91.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E5%B9%BF%E5%B7%9E%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%80%9F%E9%98%85%E6%8A%93%E5%8F%96.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E6%95%B4%E7%90%86.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%89%AF%E4%B8%9A%E6%8E%A2%E7%B4%A2%E4%B9%8B%E7%94%B5%E5%95%86.html2024-08-04T14:51:49.000Zdailyhttp://www.wenzhihuai.com/interview/tiktok2023.html2024-01-24T03:25:58.000Zdailyhttp://www.wenzhihuai.com/java/2024-05-18T19:54:10.000Zdailyhttp://www.wenzhihuai.com/java/serverlog.html2024-01-24T10:30:22.000Zdailyhttp://www.wenzhihuai.com/java/%E5%9C%A8%20Spring%206%20%E4%B8%AD%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B.html2024-01-26T03:58:56.000Zdailyhttp://www.wenzhihuai.com/java/%E5%9F%BA%E4%BA%8Ekubernetes%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81.html2024-01-26T03:58:56.000Zdailyhttp://www.wenzhihuai.com/java/%E6%B5%81%E7%A8%8B%E7%BC%96%E6%8E%92LiteFlow.html2024-11-07T16:03:32.000Zdailyhttp://www.wenzhihuai.com/java/%E9%AB%98%E5%8F%AF%E7%94%A8.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/%E9%AB%98%E5%B9%B6%E5%8F%91.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/%E9%AB%98%E6%80%A7%E8%83%BD%E9%AB%98%E5%B9%B6%E5%8F%91%E9%AB%98%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/kubernetes/Jenkins%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0.html2024-02-24T05:12:56.000Zdailyhttp://www.wenzhihuai.com/kubernetes/Kubernetes%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86.html2024-02-24T05:35:53.000Zdailyhttp://www.wenzhihuai.com/kubernetes/2024-02-16T09:22:35.000Zdailyhttp://www.wenzhihuai.com/kubernetes/request_limit.html2024-02-16T11:46:28.000Zdailyhttp://www.wenzhihuai.com/kubernetes/spark%20on%20k8s%20operator.html2024-01-27T18:29:27.000Zdailyhttp://www.wenzhihuai.com/life/2017.html2024-01-26T04:53:47.000Zdailyhttp://www.wenzhihuai.com/life/2018.html2024-01-26T04:53:47.000Zdailyhttp://www.wenzhihuai.com/life/2019.html2024-01-26T04:53:47.000Zdailyhttp://www.wenzhihuai.com/link/main.html2024-02-23T07:30:13.000Zdailyhttp://www.wenzhihuai.com/middleware/2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/open-source-project/2024-01-24T10:30:22.000Zdailyhttp://www.wenzhihuai.com/stock/2024-07-02T14:33:09.000Zdailyhttp://www.wenzhihuai.com/stock/%E8%B5%9B%E5%8A%9B%E6%96%AF.html2024-07-02T14:33:09.000Zdailyhttp://www.wenzhihuai.com/system-design/feed.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/about-the-author/personal-life/2024-07-24.html2024-07-24T15:52:12.000Zdailyhttp://www.wenzhihuai.com/about-the-author/personal-life/2024-11-09%E4%B8%8A%E6%B5%B7%E8%BF%AA%E6%96%AF%E5%B0%BC.html2024-11-11T16:10:11.000Zdailyhttp://www.wenzhihuai.com/about-the-author/personal-life/wewe.html2024-02-03T17:57:14.000Zdailyhttp://www.wenzhihuai.com/about-the-author/works/%E4%B8%AA%E4%BA%BA%E4%BD%9C%E5%93%81.html2024-01-27T09:51:43.000Zdailyhttp://www.wenzhihuai.com/bigdata/spark/elastic-spark.html2024-01-25T11:42:16.000Zdailyhttp://www.wenzhihuai.com/database/elasticsearch/elasticsearch%E6%BA%90%E7%A0%81debug.html2024-01-26T03:58:56.000Zdailyhttp://www.wenzhihuai.com/database/elasticsearch/%E3%80%90elasticsearch%E3%80%91%E6%90%9C%E7%B4%A2%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.html2024-02-16T17:21:55.000Zdailyhttp://www.wenzhihuai.com/database/elasticsearch/%E5%9F%BA%E7%A1%80.html2024-02-24T15:02:58.000Zdailyhttp://www.wenzhihuai.com/database/mysql/1mysql.html2024-02-06T07:53:39.000Zdailyhttp://www.wenzhihuai.com/database/mysql/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BC%93%E5%AD%98.html2024-01-25T11:42:16.000Zdailyhttp://www.wenzhihuai.com/database/mysql/%E8%A1%8C%E9%94%81%EF%BC%8C%E8%A1%A8%E9%94%81%EF%BC%8C%E6%84%8F%E5%90%91%E9%94%81.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/database/redis/RedissonLock%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html2024-02-06T07:22:35.000Zdailyhttp://www.wenzhihuai.com/database/redis/redis%E7%BC%93%E5%AD%98.html2024-02-06T07:53:39.000Zdailyhttp://www.wenzhihuai.com/database/redis/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95%E5%92%8C%E5%93%88%E5%B8%8C%E6%A7%BD.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/2024-11-29T18:45:10.000Zdailyhttp://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/mac%E8%BF%9E%E6%8E%A5%E4%B8%BB%E6%9C%BA.html2024-11-29T18:45:10.000Zdailyhttp://www.wenzhihuai.com/interesting/mini%E4%B8%BB%E6%9C%BA/%E4%B9%B0%E4%BA%86%E4%B8%AAmini%E4%B8%BB%E6%9C%BA.html2024-11-29T18:45:10.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/1.%E5%8E%86%E5%8F%B2%E4%B8%8E%E6%9E%B6%E6%9E%84.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/10.%E5%8E%86%E6%97%B68%E5%B9%B4%E6%9C%80%E7%BB%88%E6%94%B9%E7%89%88.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2.Lucene%E7%9A%84%E4%BD%BF%E7%94%A8.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/3.%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/4.%E6%97%A5%E5%BF%97%E7%B3%BB%E7%BB%9F.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/5.%E5%B0%8F%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/6.%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/7.%E9%82%A3%E4%BA%9B%E7%89%9B%E9%80%BC%E7%9A%84%E6%8F%92%E4%BB%B6.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/8.%E5%9F%BA%E4%BA%8E%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%9A%84%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/9.%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/interesting/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/2024-05-11T18:24:35.000Zdailyhttp://www.wenzhihuai.com/java/JVM/JVM%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0.html2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/java/JVM/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.html2024-02-16T09:55:09.000Zdailyhttp://www.wenzhihuai.com/java/JVM/2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/java/JVM/cms.html2024-02-16T09:22:35.000Zdailyhttp://www.wenzhihuai.com/java/JVM/g1.html2024-02-16T09:55:09.000Zdailyhttp://www.wenzhihuai.com/java/JVM/jvm(java%E8%99%9A%E6%8B%9F%E6%9C%BA).html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/JVM/zgc.html2024-02-16T11:46:28.000Zdailyhttp://www.wenzhihuai.com/java/JVM/%E4%B8%80%E6%AC%A1jvm%E8%B0%83%E4%BC%98%E8%BF%87%E7%A8%8B.html2024-02-16T09:22:35.000Zdailyhttp://www.wenzhihuai.com/java/JVM/%E5%B8%B8%E7%94%A8GC%E5%9B%9E%E6%94%B6%E5%99%A8.html2024-02-16T11:46:28.000Zdailyhttp://www.wenzhihuai.com/java/JVM/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E5%92%8C%E6%A0%B9%E5%8F%AF%E8%BE%BE%E7%AE%97%E6%B3%95.html2024-02-16T09:22:35.000Zdailyhttp://www.wenzhihuai.com/java/JVM/%E8%B0%83%E4%BC%98%E6%80%9D%E8%B7%AF.html2024-02-16T11:46:28.000Zdailyhttp://www.wenzhihuai.com/java/SpringBoot/2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/java/SpringBoot/Spring%20Boot%20Prometheus%E4%BD%BF%E7%94%A8.html2024-01-27T13:52:01.000Zdailyhttp://www.wenzhihuai.com/java/SpringBoot/aop.html2024-01-30T06:53:42.000Zdailyhttp://www.wenzhihuai.com/java/SpringBoot/webflux.html2024-01-27T09:51:43.000Zdailyhttp://www.wenzhihuai.com/java/io/2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/io/nio.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/synchronized.html2024-02-17T16:08:07.000Zdailyhttp://www.wenzhihuai.com/java/%E7%BA%BF%E7%A8%8B/volatile.html2024-02-16T17:21:55.000Zdailyhttp://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/IP%E3%80%81HTTP%E3%80%81HTTPS%E3%80%81HTTP2.0.html2024-02-14T17:59:53.000Zdailyhttp://www.wenzhihuai.com/java/%E7%BD%91%E7%BB%9C/2024-02-16T04:00:26.000Zdailyhttp://www.wenzhihuai.com/kubernetes/devops/2024-02-16T09:22:35.000Zdailyhttp://www.wenzhihuai.com/kubernetes/devops/devops-ping-tai.html2024-02-06T07:53:39.000Zdailyhttp://www.wenzhihuai.com/middleware/canal/%E4%B8%AD%E9%97%B4%E4%BB%B6%E2%80%94%E2%80%94canal%E5%B0%8F%E8%AE%B0.html2024-02-06T07:53:39.000Zdailyhttp://www.wenzhihuai.com/middleware/kafka/kafka.html2024-01-25T17:34:45.000Zdailyhttp://www.wenzhihuai.com/middleware/sentinel/springcloud_sentinel.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/middleware/sentinel/%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8.html2024-03-10T14:39:30.000Zdailyhttp://www.wenzhihuai.com/middleware/zookeeper/zookeeper.html2024-01-25T17:34:45.000Zdailyhttp://www.wenzhihuai.com/middleware/zookeeper/%E5%9F%BA%E4%BA%8EZooKeeper%E7%9A%84%E9%98%9F%E5%88%97%E7%88%AC%E8%99%AB.html2024-02-14T17:59:53.000Zdailyhttp://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E5%B7%A5%E7%A8%8B-%E6%A6%82%E8%BF%B0.html2024-11-29T18:45:10.000Zdailyhttp://www.wenzhihuai.com/system-design/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8.html2024-09-01T13:54:44.000Zdailyhttp://www.wenzhihuai.com/system-design/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%85%A5%E9%97%A8%E6%80%BB%E7%BB%93.html2024-09-01T13:54:44.000Zdailyhttp://www.wenzhihuai.com/slide/daily \ No newline at end of file diff --git a/sitemap.xsl b/sitemap.xsl new file mode 100644 index 00000000..a76881a4 --- /dev/null +++ b/sitemap.xsl @@ -0,0 +1,207 @@ + + + + + + + XML Sitemap + + + + + +
+

Sitemap

+ + + + + + + + + + + + + + + + + + + + + +
+ + PriorityChange FrequencyLast Updated Time
+ + + + + + + + + + + + + 0.5 + + + + + + + + + - + + + + +
+
+ + + +
+
diff --git a/slide/index.html b/slide/index.html new file mode 100644 index 00000000..8c4c2c0d --- /dev/null +++ b/slide/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/star/index.html b/star/index.html new file mode 100644 index 00000000..396c57a8 --- /dev/null +++ b/star/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 星标 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/stock/index.html b/stock/index.html new file mode 100644 index 00000000..0a24f4e1 --- /dev/null +++ b/stock/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 股票预测 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/stock/\350\265\233\345\212\233\346\226\257.html" "b/stock/\350\265\233\345\212\233\346\226\257.html" new file mode 100644 index 00000000..95a90e36 --- /dev/null +++ "b/stock/\350\265\233\345\212\233\346\226\257.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 赛力斯 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/system-design/feed.html b/system-design/feed.html new file mode 100644 index 00000000..fbdd58d0 --- /dev/null +++ b/system-design/feed.html @@ -0,0 +1,47 @@ + + + + + + + + + + Feed系统设计 | 个人博客 + + + + + + + + + diff --git a/system-design/index.html b/system-design/index.html new file mode 100644 index 00000000..f6532750 --- /dev/null +++ b/system-design/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + System Design | 个人博客 + + + + + + + + + diff --git "a/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/index.html" "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/index.html" new file mode 100644 index 00000000..b22599f4 --- /dev/null +++ "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 推荐系统 | 个人博客 + + + + + + + + + diff --git "a/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html" "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html" new file mode 100644 index 00000000..f25ee898 --- /dev/null +++ "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\345\267\245\347\250\213-\346\246\202\350\277\260.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 推荐工程-概述 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html" "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html" new file mode 100644 index 00000000..eaa180d6 --- /dev/null +++ "b/system-design/\346\216\250\350\215\220\347\263\273\347\273\237/\346\216\250\350\215\220\347\263\273\347\273\237\345\205\245\351\227\250.html" @@ -0,0 +1,152 @@ + + + + + + + + + + 推荐系统入门 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git "a/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/index.html" "b/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/index.html" new file mode 100644 index 00000000..8c56edfa --- /dev/null +++ "b/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/index.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 计算广告 | 个人博客 + + + + + + + + + diff --git "a/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html" "b/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html" new file mode 100644 index 00000000..1045b00c --- /dev/null +++ "b/system-design/\350\256\241\347\256\227\345\271\277\345\221\212/\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265\345\205\245\351\227\250\346\200\273\347\273\223.html" @@ -0,0 +1,47 @@ + + + + + + + + + + 计算广告基本概念入门总结 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/tag/index.html b/tag/index.html new file mode 100644 index 00000000..52857026 --- /dev/null +++ b/tag/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 标签 | 个人博客 + + + + + +
跳至主要內容
+ + + diff --git a/timeline/index.html b/timeline/index.html new file mode 100644 index 00000000..da8090f1 --- /dev/null +++ b/timeline/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + 时间轴 | 个人博客 + + + + + +
跳至主要內容
+ + +