From 8a08fdf1a8f6123734008d815ae91d1d2a529cd5 Mon Sep 17 00:00:00 2001 From: WindyDante <125888129+WindyDante@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:57:53 +0800 Subject: [PATCH] add md! --- package-lock.json | 351 + package.json | 6 +- public/markdowns/Axure9.md | 186 + public/markdowns/Docker.md | 1000 ++ public/markdowns/Git.md | 420 + ...01\347\224\237\346\210\220\345\231\250.md" | 139 + .../MySql\345\237\272\347\241\200.md" | 607 + .../MySql\351\253\230\347\272\247.md" | 102 + .../Postman\345\267\245\345\205\267.md" | 83 + .../Redis\345\205\245\351\227\250.md" | 746 + ...is\345\237\272\347\241\200\347\257\207.md" | 1121 ++ ...is\345\256\236\346\210\230\347\257\207.md" | 2320 +++ ...57\345\212\250\345\221\275\344\273\244.md" | 11 + ...t2\345\237\272\347\241\200\347\257\207.md" | 1775 +++ ...t2\351\253\230\347\272\247\347\257\207.md" | 5020 +++++++ public/markdowns/SpringBoot3.md | 9 + public/markdowns/SpringCloud.md | 5349 +++++++ public/markdowns/SpringSecurity.md | 228 + public/markdowns/Vue.md | 11821 ++++++++++++++++ public/markdowns/Vue3.md | 1244 ++ .../markdowns/android-Java\347\211\210.md" | 6005 ++++++++ .../markdowns/c\350\257\255\350\250\200.md" | 7123 ++++++++++ .../freemarker\345\237\272\347\241\200.md" | 734 + public/markdowns/mongoDB.md | 555 + ...06\347\276\244\346\220\255\345\273\272.md" | 370 + ...01\347\232\204\345\255\227\344\275\223.md" | 46 + ...et\347\232\204\344\275\277\347\224\250.md" | 206 + .../markdowns/\345\206\205\345\212\237.md" | 1908 +++ ...10\345\271\266\345\217\213\351\223\276.md" | 127 + ...41\345\260\217\347\250\213\345\272\217.md" | 1339 ++ ...04\344\270\216\347\256\227\346\263\225.md" | 2109 +++ ...36\345\220\211\345\244\226\345\215\226.md" | 6080 ++++++++ ...26\345\215\226\344\274\230\345\214\226.md" | 1789 +++ ...16\347\253\257\345\210\206\347\246\273.md" | 67 + ...61\350\257\255\350\257\255\346\263\225.md" | 713 + ...\210\231\345\274\225\346\223\216Drools.md" | 775 + ...57\344\273\266\346\265\213\350\257\225.md" | 349 + ...30\347\255\211\346\225\260\345\255\246.md" | 550 + src/components/Header.vue | 2 +- src/components/Main.vue | 232 +- src/views/Life.vue | 38 +- 41 files changed, 63629 insertions(+), 26 deletions(-) create mode 100644 public/markdowns/Axure9.md create mode 100644 public/markdowns/Docker.md create mode 100644 public/markdowns/Git.md create mode 100644 "public/markdowns/MyBatis-Plus\344\273\243\347\240\201\347\224\237\346\210\220\345\231\250.md" create mode 100644 "public/markdowns/MySql\345\237\272\347\241\200.md" create mode 100644 "public/markdowns/MySql\351\253\230\347\272\247.md" create mode 100644 "public/markdowns/Postman\345\267\245\345\205\267.md" create mode 100644 "public/markdowns/Redis\345\205\245\351\227\250.md" create mode 100644 "public/markdowns/Redis\345\237\272\347\241\200\347\257\207.md" create mode 100644 "public/markdowns/Redis\345\256\236\346\210\230\347\257\207.md" create mode 100644 "public/markdowns/RocketMq\345\237\272\346\234\254\345\220\257\345\212\250\345\221\275\344\273\244.md" create mode 100644 "public/markdowns/SpringBoot2\345\237\272\347\241\200\347\257\207.md" create mode 100644 "public/markdowns/SpringBoot2\351\253\230\347\272\247\347\257\207.md" create mode 100644 public/markdowns/SpringBoot3.md create mode 100644 public/markdowns/SpringCloud.md create mode 100644 public/markdowns/SpringSecurity.md create mode 100644 public/markdowns/Vue.md create mode 100644 public/markdowns/Vue3.md create mode 100644 "public/markdowns/android-Java\347\211\210.md" create mode 100644 "public/markdowns/c\350\257\255\350\250\200.md" create mode 100644 "public/markdowns/freemarker\345\237\272\347\241\200.md" create mode 100644 public/markdowns/mongoDB.md create mode 100644 "public/markdowns/nacos\351\233\206\347\276\244\346\220\255\345\273\272.md" create mode 100644 "public/markdowns/vscode\345\256\211\350\243\205\346\203\263\350\246\201\347\232\204\345\255\227\344\275\223.md" create mode 100644 "public/markdowns/webSocket\347\232\204\344\275\277\347\224\250.md" create mode 100644 "public/markdowns/\345\206\205\345\212\237.md" create mode 100644 "public/markdowns/\345\246\202\344\275\225\345\260\206\350\207\252\345\267\261\346\211\213\345\206\231\347\232\204vue3\351\241\271\347\233\256\350\207\252\345\212\250\345\214\226\351\203\250\347\275\262\345\217\212\350\207\252\345\212\250\345\214\226\345\220\210\345\271\266\345\217\213\351\223\276.md" create mode 100644 "public/markdowns/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217.md" create mode 100644 "public/markdowns/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225.md" create mode 100644 "public/markdowns/\347\221\236\345\220\211\345\244\226\345\215\226.md" create mode 100644 "public/markdowns/\347\221\236\345\220\211\345\244\226\345\215\226\344\274\230\345\214\226.md" create mode 100644 "public/markdowns/\350\213\245\344\276\235\346\241\206\346\236\266\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273.md" create mode 100644 "public/markdowns/\350\213\261\350\257\255\350\257\255\346\263\225.md" create mode 100644 "public/markdowns/\350\247\204\345\210\231\345\274\225\346\223\216Drools.md" create mode 100644 "public/markdowns/\350\275\257\344\273\266\346\265\213\350\257\225.md" create mode 100644 "public/markdowns/\351\253\230\347\255\211\346\225\260\345\255\246.md" diff --git a/package-lock.json b/package-lock.json index 2ad37ee..d24ad4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "headwriteblog", "version": "0.0.0", "dependencies": { + "axios": "^1.7.7", + "dayjs": "^1.11.13", + "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", + "marked": "^14.1.3", "vue": "^3.4.29", "vue-router": "^4.3.3" }, @@ -214,11 +218,50 @@ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", @@ -268,11 +311,108 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", @@ -284,6 +424,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz", @@ -292,6 +440,36 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/marked": { + "version": "14.1.3", + "resolved": "https://registry.npmmirror.com/marked/-/marked-14.1.3.tgz", + "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz", @@ -341,6 +519,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/rollup": { "version": "4.22.4", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.22.4.tgz", @@ -376,6 +559,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -384,6 +579,19 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -645,11 +853,44 @@ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "entities": { "version": "4.5.0", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", @@ -686,11 +927,74 @@ "@esbuild/win32-x64": "0.21.5" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "requires": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", @@ -699,6 +1003,11 @@ "argparse": "^2.0.1" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, "magic-string": { "version": "0.30.11", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz", @@ -707,6 +1016,24 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "marked": { + "version": "14.1.3", + "resolved": "https://registry.npmmirror.com/marked/-/marked-14.1.3.tgz", + "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz", @@ -727,6 +1054,11 @@ "source-map-js": "^1.2.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "rollup": { "version": "4.22.4", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.22.4.tgz", @@ -753,11 +1085,30 @@ "fsevents": "~2.3.2" } }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + } + }, "source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 851ab9d..796715c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.7", + "dayjs": "^1.11.13", + "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", + "marked": "^14.1.3", "vue": "^3.4.29", "vue-router": "^4.3.3" }, @@ -18,4 +22,4 @@ "@vitejs/plugin-vue": "^5.0.5", "vite": "^5.3.1" } -} \ No newline at end of file +} diff --git a/public/markdowns/Axure9.md b/public/markdowns/Axure9.md new file mode 100644 index 0000000..4fafd42 --- /dev/null +++ b/public/markdowns/Axure9.md @@ -0,0 +1,186 @@ +--- +title: Axure9 +abbrlink: f07f281e +date: 2024-05-22 14:59:57 +tags: Axure +categories: + - 产品设计 +description: Axure原型设计 +--- + +# Axure9的页面介绍 + +image-20240522150116683 + +# Axure9的基础功能 + +## 菜单栏 + +![image-20240522221315713](https://s2.loli.net/2024/05/22/8yipHZgRPQ1Eqnj.png) + +建议将备份设置为5分钟 + +点击左上角文件菜单栏->自动备份设置,来进行设置 + +偏好设置:可以对Axure的默认显示页面及辅助线、显示样式等进行设置; + + + +Axure中,总共有4种类型的文件; + +- .rp:原型文件 +- .rplib:元件库文件 +- .rpteam:团队项目文件 + - 我们可以在左上角菜单栏中找到团队,进行团队项目的发布和分享 +- .html:网页文件 + + + +备份设置:建议自动备份间隔5分钟; + +可以在视图中,对快速功能区进行自定义; + +发布模块,可以对预览选项进行编辑; + + + +如果想修改画布的尺寸可以在右侧样式->页面尺寸中进行调整 + +如果不小心对页面中的一些窗口拖动,拖没了或拖错了,可以通过左侧菜单栏中的视图->重置视图来进行视图的重置 + + + +如果想让多个形状或图片水平居中对齐,先选中对应的形状,再依次点击上方的中部和水平即可 + +![image-20240523133711984](https://s2.loli.net/2024/05/23/AlYGu69ZxNPekqt.png) + + + +如果我们想看之前已经做好的内容,可以在菜单栏中找到发布->预览,进行查看 + +## 工具栏 + +在插入中可以进行形状的插入,如果我们想画一个正圆,可以通过按住shift再拖动即可 + +![image-20240523135218121](https://s2.loli.net/2024/05/23/ydDxGNL5hT26C7E.png) + +在预览中,可以体验自己编写好的交互 + +## 母版 + +image-20240523144717637 + +## 样式面板 + +![image-20240523152615606](https://s2.loli.net/2024/05/23/2GBvyxXj5InMHVE.png) + +## 快捷键 + +![image-20240523165409324](https://s2.loli.net/2024/05/23/QXdbLBtaqs1p7xS.png) + +# 元件 + +![image-20240523165748199](https://s2.loli.net/2024/05/23/OkRUw4xr2B7Nv3d.png) + +如果想让放入到图片组件中的图片与图片组件一样大,令图片组件旁边的小按钮变为白色即可 + +![image-20240523170746247](https://s2.loli.net/2024/05/23/v2TY8ItC39PDduF.png) + +如果小按钮是黄色,此时图片则为原来的大小 + + + +在使用文本标签时,如果小按钮为黄色,输入字体后不会自动换行,当小按钮为白色,则会自动换行 + +![image-20240523205352222](https://s2.loli.net/2024/05/23/Za5OnGheYCgqj9s.png) + + + +# 热区、动态面板 + +Axure中的热区可以让某个元件的触发范围变大,在热区中对触发条件进行添加即可 + + + +动态面板: + +直接拖出动态面板后,双击动态面板,就能进入到对应的效果页面 + +如图所示: + +image-20240523222403064 + +上方有一个状态栏,每个状态都代表了不同的动态面板,我们对当前state1的面板画一个圆,然后修改状态 + +image-20240523222538595 + +切换状态后,圆不见了 + +# 内联框架、中继器 + +拖出内联框架,双击后,可以链接到网页或当前原型中的页面 + +如果链接到网页中,点击预览后,对应的页面会缩小到内联框架对应的大小内 + + + +中继器的使用方式,双击后会有一个单独像单个值一样的边框,修改该内容,在关闭中继器后外部的多个中继器都会被修改 + +# 表单元件 + +image-20240524081648908 + +在文本框中,可以编写一些对应的操作内容 + +image-20240524081936393 + +在多选框中,如果我们想默认勾选上第一项的内容,可以这样操作 + +勾选上你想默认的那一项即可 + +image-20240524083034737 + +单选按钮需要注意的是,需要提前为它设定组,才能起到单选按钮的效果 + +选中对应的按钮 + +image-20240524090355021 + +# 菜单表格 + +没啥难度,正常使用即可 + +image-20240524091146222 + +# 标记元件 + +快照使用较少,其他正常使用即可 + +# 事件 + +## 常用的交互设计 + +image-20240524145204739 + +对元件选中后添加交互样式 + +image-20240524145705763 + +# 中继器 + +- 载入时 +- 每项加载时 +- 列表项尺寸改变 + +当我们想写一个商品列表时,就可以用到中继器 + +先编写好中继器的模板,接着编辑对应的数据 + +image-20240526152220907 + +需要通过交互事件来获取中继器中写好的数据 + +image-20240526152639383 + +此时如果我们写了四行,那么第四个就会超出这个掉在边框外面,如果你想有规律的放置每一行的项数,在样式->布局->网格分布, 然后填写每行项数量即可 + diff --git a/public/markdowns/Docker.md b/public/markdowns/Docker.md new file mode 100644 index 0000000..ef1e44a --- /dev/null +++ b/public/markdowns/Docker.md @@ -0,0 +1,1000 @@ +--- +title: Docker +abbrlink: f5f9fa9b +date: 2023-11-07 13:38:38 +tags: + - Docker + - MQ +categories: + - 微服务 +description: Docker快速入门教程 +--- + +# Docker的安装 + +## 删除Docker + +在安装Docker之前需要先进行Docker的删除,防止本地存在Docker导致冲突 + +```bash +yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-engine +``` + +## 配置Docker的yum库 + +首先要安装一个yum工具 + +```bash +yum install -y yum-utils +``` + +安装成功后,执行命令,配置Docker的yum源: + +```Bash +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +``` + +## 安装Docker + +执行命令,安装Docker + +```Bash +yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +``` + +安装完成后,输入命令`docker -v`可以查看docker的版本 + +## 启动和校验 + +这里可以设置docker为开机自启 + +然后启动docker,并输入`docker images`或者`docker ps`查看是否有效,如果有效就说明启动成功了 + +```Bash +# 启动Docker +systemctl start docker + +# 停止Docker +systemctl stop docker + +# 重启 +systemctl restart docker + +# 设置开机自启 +systemctl enable docker + +# 执行docker ps命令,如果不报错,说明安装启动成功 +docker ps +``` + +## 配置镜像加速 + +这里配置阿里的镜像加速 + +注册阿里云账号https://www.aliyun.com/ + +image-20231107144316412 + +![image-20231107144348307](https://s2.loli.net/2023/11/07/2RufPhqJEAtQoI9.png) + +具体命令如下: + +```Bash +# 创建目录 +mkdir -p /etc/docker + +# 复制内容,注意把其中的镜像加速地址改成你自己的 +tee /etc/docker/daemon.json <<-'EOF' +{ + "registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"] +} +EOF + +# 重新加载配置 +systemctl daemon-reload + +# 重启Docker +systemctl restart docker +``` + +# 部署MySQL + +部署MySQL只需要一条指令 + +```bash +docker run -d \ + --name mysql \ + -p 3306:3306 \ + -e TZ=Asia/Shanghai \ + -e MYSQL_ROOT_PASSWORD=123 \ + mysql +``` + +接着你可以打开你使用的sql工具,来连接docker刚刚部署的mysql,连接成功说明,部署成功了 + +## 镜像和容器 + +当我们利用Docker安装应用时,Docker会自动搜索并下载应用**镜像(image)**。镜像不仅包含应用本身,还包含应用运行所需要的环境、配置、系统函数库。Docker会在运行镜像时创建一个隔离环境。称为**容器(container)**。 + +**镜像仓库**:存储和管理镜像的平台,Docker官方维护了一个公共仓库:Docker Hub。 + +在我们启动Docker的服务器后,docker daemon守护进程会对docker命令进行监听,当我们运行docker xxx的命令后,守护进程就会去查看本地是否存在镜像,存在就直接使用,否则就回去镜像仓库进行下载,下载完成后作为容器来使用 + +## 命令解读 + +```bash +docker run -d \ + --name mysql \ + -p 3306:3306 \ + -e TZ=Asia/Shanghai \ + -e MYSQL_ROOT_PASSWORD=123 \ + mysql +``` + +- `docker run -d` :创建并运行一个容器,`-d`则是让容器以后台进程运行 +- `--name mysql ` : 给容器起个名字叫`mysql`,你可以叫别的 +- `-p 3306:3306` : 设置端口映射。 + - **容器是隔离环境**,外界不可访问。但是可以将宿主机端口映射容器内到端口,当访问宿主机指定端口时,就是在访问容器内的端口了。 + - 容器内端口往往是由容器内的进程决定,例如MySQL进程默认端口是3306,因此容器内端口一定是3306;而宿主机端口则可以任意指定,一般与容器内保持一致。 + - 格式: `-p 宿主机端口:容器内端口`,示例中就是将宿主机的3306映射到容器内的3306端口 +- `-e TZ=Asia/Shanghai` : 配置容器内进程运行时的一些参数 + - 格式:`-e KEY=VALUE`,KEY和VALUE都由容器内进程决定 + - 案例中,`TZ=Asia/Shanghai`是设置时区;`MYSQL_ROOT_PASSWORD=123`是设置MySQL默认密码 +- `mysql`:设置**镜像**名称,Docker会根据这个名字搜索并下载镜像 + - 格式:`REPOSITORY:TAG`,例如`mysql:8.0`,其中`REPOSITORY`可以理解为镜像名,`TAG`是版本号 + - 在未指定`TAG`的情况下,默认是最新版本,也就是`mysql:latest` + +### 镜像命名规范 + +- 镜像名称一般分两部分组成:[repository]:[tag] + - 其中repository就是镜像名 + - tag是镜像的版本 +- 在没有指定tag时,默认是latest(最新),代表最新版本的镜像 + +# Docker基础 + +Docker最常见的命令就是操作镜像、容器的命令,详见官方文档:https://docs.docker.com/ + +Docker常见命令,可以参考官方文档:https://docs.docker.com/engine/reference/commandline/cli/ + +比较常见的命令有: + +| **命令** | **说明** | **文档地址** | +| :------------- | :----------------------------- | :----------------------------------------------------------- | +| docker pull | 拉取镜像 | [docker pull](https://docs.docker.com/engine/reference/commandline/pull/) | +| docker push | 推送镜像到DockerRegistry | [docker push](https://docs.docker.com/engine/reference/commandline/push/) | +| docker images | 查看本地镜像 | [docker images](https://docs.docker.com/engine/reference/commandline/images/) | +| docker rmi | 删除本地镜像 | [docker rmi](https://docs.docker.com/engine/reference/commandline/rmi/) | +| docker run | 创建并运行容器(不能重复创建) | [docker run](https://docs.docker.com/engine/reference/commandline/run/) | +| docker stop | 停止指定容器 | [docker stop](https://docs.docker.com/engine/reference/commandline/stop/) | +| docker start | 启动指定容器 | [docker start](https://docs.docker.com/engine/reference/commandline/start/) | +| docker restart | 重新启动容器 | [docker restart](https://docs.docker.com/engine/reference/commandline/restart/) | +| docker rm | 删除指定容器 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/rm/) | +| docker ps | 查看容器 | [docker ps](https://docs.docker.com/engine/reference/commandline/ps/) | +| docker logs | 查看容器运行日志 | [docker logs](https://docs.docker.com/engine/reference/commandline/logs/) | +| docker exec | 进入容器 | [docker exec](https://docs.docker.com/engine/reference/commandline/exec/) | +| docker save | 保存镜像到本地压缩文件 | [docker save](https://docs.docker.com/engine/reference/commandline/save/) | +| docker load | 加载本地压缩文件到镜像 | [docker load](https://docs.docker.com/engine/reference/commandline/load/) | +| docker inspect | 查看容器详细信息 | [docker inspect](https://docs.docker.com/engine/reference/commandline/inspect/) | + +## 保存镜像到本地压缩文件 + +```bash +docker save -o 镜像名称.tar 镜像名称:版本 +``` + +## 删除镜像 + +删除镜像时如果遇到以下报错:Error response from daemon: conflict: unable to remove repository reference "mysql:latest" (must force) - container 0fa37bf7c610 is using its referenced image 3218b38490ce + +报错内容是因为镜像被容器引用,那么删除容器再删除镜像。 + +此时使用`docker rm 0fa37bf7c610 ` + +再次执行即可 + +```bash +docker rmi 镜像名:版本 +``` + +## 加载已有镜像 + +-p:不输出任何内容 + +``` +docker load -i 读取的镜像名称 -p +``` + +## 创建并运行容器 + +docker run + +-d:后台运行 + +--name:容器名称 + +-p:端口号,分为宿主机端口:容器内端口 + +-e:相关配置 + +最后是设置镜像名称 + +```bash +docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123 mysql +``` + +## 停止容器运行 + +```bash +docker stop 容器名 +``` + +## 查看容器是否运行 + +```bash +docker ps +``` + +查看所有容器,包括没有运行的容器 + +```bash +docker ps -a +``` + +## 启动容器 + +```bash +docker start 容器名 +``` + +## 查看容器日志 + +```bash +docker logs 容器名 +``` + +### 持续查看容器日志 + +```bash +docker logs -f 容器名 +``` + +## 进入容器内部 + +-it:添加一个可输入的终端 + +bash:命令行交互 + +```bash +docker exec -it 容器名 bash +# 退出 +exit +``` + +## 删除容器 + +删除容器需要先停止容器再删除 + +```bash +docker stop 被删除的容器名 +``` + +```bash +docker rm 容器名 +``` + +也可以强制删除docker + +```bash +docker rm 容器名 -f +``` + +## 命令别名 + +```bash +# 修改/root/.bashrc文件 +vi /root/.bashrc +内容如下: +# .bashrc + +# User specific aliases and functions + +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' +alias dps='docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}"' +alias dis='docker images' + +# Source global definitions +if [ -f /etc/bashrc ]; then + . /etc/bashrc +fi +``` + + + +# 数据卷 + +数据卷(volume)是一个虚拟目录,是**容器内目录**与**宿主机目录**之间映射的桥梁 + +数据卷可以映射容器内目录到宿主机上,因为容器内目录是无法直接修改里面的内容的,原因是容器内目录是最小化的服务器,只有服务器的功能,其他的功能都没有,所以无法通过命令修改,通过数据卷可以将容器内的目录映射到一个固定的目录下进行修改 + +![image-20231107193919346](https://s2.loli.net/2023/11/07/6SnXYUs3gqoNI8t.png) + +## 数据卷命令 + +| **命令** | **说明** | **文档地址** | +| :-------------------- | :------------------- | :----------------------------------------------------------- | +| docker volume create | 创建数据卷 | [docker volume create](https://docs.docker.com/engine/reference/commandline/volume_create/) | +| docker volume ls | 查看所有数据卷 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/volume_ls/) | +| docker volume rm | 删除指定数据卷 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/volume_prune/) | +| docker volume inspect | 查看某个数据卷的详情 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/volume_inspect/) | +| docker volume prune | 清除数据卷 | [docker volume prune](https://docs.docker.com/engine/reference/commandline/volume_prune/) | + +## 创建相关数据卷 + +注意:容器与数据卷的挂载要在创建容器时配置,对于创建好的容器,是不能设置数据卷的。而且**创建容器的过程中,数据卷会自动创建**。 + +也就是说,数据卷要在容器创建前进行创建,所以之前的容器需要删除,我们可以将之前的nginx容器删除 + +```bash +docker rm -f nginx +``` + +- 在执行docker run命令时,使用-v 数据卷名称:容器内目录 可以完成数据卷挂载 +- 当创建容器时,如果挂载了数据卷且数据卷不存在,会自动创建数据卷 + +创建容器并创建对应数据卷,-d以后台形式运行,名称为nginx,开放宿主机端口为80且容器内端口为80,使用-v 数据卷名称:容器内目录来映射对应的数据卷完成数据卷挂载,最后的nginx设置的是数据卷的镜像名称,镜像名称需要自己特意指定,会在本地和docker仓库进行搜索 + +```bash +docker run -d --name nginx -p 80:80 -v html:/usr/share/nginx/html nginx +``` + +查看nginx容器是否创建成功 + +```bash +docker ps +``` + +查看数据卷是否创建成功 + +```bash +docker volume ls +``` + +展示数据卷的详细信息 + +```bash +docker volume inspect 数据卷的名称 +``` + +这里的数据卷是html,所以通过html来查询,`docker volume inspect html` + +```bash +[ + { + "CreatedAt": "2023-11-07T05:27:47-08:00", + "Driver": "local", + "Labels": null, + "Mountpoint": "/var/lib/docker/volumes/html/_data", + "Name": "html", + "Options": null, + "Scope": "local" + } +] +``` + +**Mountpoint**是所挂载的宿主机的位置 + +此时,如果你进入挂载到的位置进行查看,就会看到你所访问的位置与之前nginx位置的内容是一致的 + +## 数据卷的常用命令 + +- docker volume ls:查看数据卷 +- docker volume rm:删除数据卷 +- docker volume inspect:查看数据卷详情 +- docker volume prune:删除未使用的数据卷 + +## 本地目录挂载 + +### 查看容器详情 + +```bash +docker inspect 容器名 +``` + +- 在执行docker run命令时,使用-v 本地目录:容器内目录可以完成本地目录挂载 +- 本地目录必须以`/`或`./`开头,如果直接以名称开头,会被识别为数据卷而非本地目录 + - -v mysql:/var/lib/mysql 会被识别为一个数据卷叫mysql + - -v ./mysql:/var/lib/mysql 会被识别当前目录下的mysql目录挂载 + +挂载MySQL的目录到本地上 + +```bash +docker run -d \ +--name mysql \ +-p 3306:3306 \ +-e MYSQL_ROOT_PASSWORD=123 \ +-v /root/mysql/data:/var/lib/mysql \ +-v /root/mysql/init/:/docker-entrypoint-initdb.d \ +-v /root/mysql/conf/:/etc/mysql/conf.d \ +mysql +``` + +创建完成后 + +查看目前启动的容器 + +```bash +docker ps +``` + +此时如果数据库中存在内容,说明挂载完成了,即使mysql镜像被删除了,只要你不删除挂载目录,数据就不会丢失,你只需要重新挂载到这些目录下,数据就能回来 + +# 自定义镜像 + +前面我们一直在使用别人准备好的镜像,那如果我要部署一个Java项目,把它打包为一个镜像该怎么做呢? + +## 镜像结构 + +要想自己构建镜像,必须先了解镜像的结构。 + +之前我们说过,镜像之所以能让我们快速跨操作系统部署应用而忽略其运行环境、配置,就是因为镜像中包含了程序运行需要的系统函数库、环境、配置、依赖。 + +因此,自定义镜像本质就是依次准备好程序运行的基础环境、依赖、应用本身、运行配置等文件,并且打包而成。 + +举个例子,我们要从0部署一个Java应用,大概流程是这样: + +- 准备一个linux服务(CentOS或者Ubuntu均可) + +- 安装并配置JDK +- 上传Jar包 +- 运行jar包 + +那因此,我们打包镜像也是分成这么几步: + +- 准备Linux运行环境(java项目并不需要完整的操作系统,仅仅是基础运行环境即可) +- 安装并配置JDK +- 拷贝jar包 +- 配置启动脚本 + +上述步骤中的每一次操作其实都是在生产一些文件(系统运行环境、函数库、配置最终都是磁盘文件),所以**镜像就是一堆文件的集合**。 + +但需要注意的是,镜像文件不是随意堆放的,而是按照操作的步骤分层叠加而成,每一层形成的文件都会单独打包并标记一个唯一id,称为**Layer**(**层**)。这样,如果我们构建时用到的某些层其他人已经制作过,就可以直接拷贝使用这些层,而不用重复制作。 + +例如,第一步中需要的Linux运行环境,通用性就很强,所以Docker官方就制作了这样的只包含Linux运行环境的镜像。我们在制作java镜像时,就无需重复制作,直接使用Docker官方提供的CentOS或Ubuntu镜像作为基础镜像。然后再搭建其它层即可,这样逐层搭建,最终整个Java项目的镜像结构如图所示: + +![image-20231109134001580](https://s2.loli.net/2023/11/09/bmZzw9PnUyKThCB.png) + +此时我们可以拉取一个镜像来测试一下 +我们可以拉取redis的镜像来测试 + +```bash +docker pull redis +``` + +![image-20231109134258944](https://s2.loli.net/2023/11/09/7yneNKGZ6iajrTq.png) + +此时我们可以看到Already exists,重复的存在 + +这是因为镜像文件中存在着重复的层,所以可以直接拷贝来加快镜像的拉取速度 + +## Dockerfile + +由于制作镜像的过程中,需要逐层处理和打包,比较复杂,所以Docker就提供了自动打包镜像的功能。我们只需要将打包的过程,每一层要做的事情用固定的语法写下来,交给Docker去执行即可。 + +而这种记录镜像结构的文件就称为**Dockerfile**,其对应的语法可以参考官方文档: + +https://docs.docker.com/engine/reference/builder/ + +其中的语法比较多,比较常用的有: + +| **指令** | **说明** | **示例** | +| :------------- | :------------------------------------------- | :--------------------------- | +| **FROM** | 指定基础镜像 | `FROM centos:6` | +| **ENV** | 设置环境变量,可在后面指令使用 | `ENV key value` | +| **COPY** | 拷贝本地文件到镜像的指定目录 | `COPY ./xx.jar /tmp/app.jar` | +| **RUN** | 执行Linux的shell命令,一般是安装过程的命令 | `RUN yum install gcc` | +| **EXPOSE** | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 | +| **ENTRYPOINT** | 镜像中应用的启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar | + +例如,要基于Ubuntu镜像来构建一个Java应用,其Dockerfile内容如下: + +```bash +# 指定基础镜像 +FROM ubuntu:16.04 +# 配置环境变量,JDK的安装目录、容器内时区 +ENV JAVA_DIR=/usr/local +ENV TZ=Asia/Shanghai +# 拷贝jdk和java项目的包 +COPY ./jdk8.tar.gz $JAVA_DIR/ +COPY ./docker-demo.jar /tmp/app.jar +# 设定时区 +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# 安装JDK +RUN cd $JAVA_DIR \ + && tar -xf ./jdk8.tar.gz \ + && mv ./jdk1.8.0_144 ./java8 +# 配置环境变量 +ENV JAVA_HOME=$JAVA_DIR/java8 +ENV PATH=$PATH:$JAVA_HOME/bin +# 指定项目监听的端口 +EXPOSE 8080 +# 入口,java项目的启动命令 +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +以后我们会有很多很多java项目需要打包为镜像,他们都需要Linux系统环境、JDK环境这两层,只有上面的3层不同(因为jar包不同)。如果每次制作java镜像都重复制作前两层镜像,是不是很麻烦。 + +所以,就有人提供了基础的系统加JDK环境,我们在此基础上制作java镜像,就可以省去JDK的配置了: + +```Dockerfile +# 基础镜像 +FROM openjdk:11.0-jre-buster +# 设定时区 +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# 拷贝jar包 +COPY docker-demo.jar /app.jar +# 入口 +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +## 自定义镜像 + +当编写好了Dockerfile,可以利用下面的命令来构建镜像: + +```bash +docker build -t myTest:1.0 . +``` + +- `-t`:是给镜像起名,格式依然是repository:tag的格式,不指定tag时,默认为latest(最新) +- `.`:是指定Dockerfile所在目录,如果就在当前目录,则指定为`.`,如果是指定了当前目录的话,就需要对Dockerfile的目录做一些调整 + +将资料中的demo目录拷贝到根目录下,进入demo目录 + +```bash +cd /root/demo +``` + +如果觉得下载镜像不方便可以拷贝资料中的images目录到根目录下 + +并通过 + +```bash +# 加载为docker的镜像 +docker load -i tar包的名称 +``` + +构建镜像 + +```bash +docker build -t docker-demo:1.0 . +``` + +命令说明: + +- `docker build `: 就是构建一个docker镜像 +- `-t docker-demo:1.0` :`-t`参数是指定镜像的名称(`repository`和`tag`) +- `.` : 最后的点是指构建时Dockerfile所在路径,由于我们进入了demo目录,所以指定的是`.`代表当前目录,也可以直接指定Dockerfile目录: + +```bash +# 直接指定Dockerfile目录 +docker build -t docker-demo:1.0 /root/demo +``` + +运行`docker images` + +```bash +REPOSITORY TAG IMAGE ID CREATED SIZE +docker-demo latest cee3813af51e 16 seconds ago 319MB +nginx latest 605c77e624dd 22 months ago 141MB +redis latest 7614ae9453d1 22 months ago 113MB +mysql latest 3218b38490ce 22 months ago 516MB +openjdk 11.0-jre-buster 57925f2e4cff 23 months ago 301MB +``` + +此时发现镜像被引入进来了 + +接着运行该镜像 + +```bash +docker run -d --name demo -p 8080:8080 docker-demo +``` + +查看该镜像是否正在运行 + +```bash +docker ps +``` + +接着可以看看它的运行日志 + +```bash +docker logs -f demo +``` + +来到浏览器中 + +``` +http://自己的ip地址:8080/hello/count +``` + +访问一下是否有效 + +总结: + +镜像的结构是怎样的? + +- 镜像中包含了应用程序所需要的运行环境、函数库、配置、以及应用本身等各种文件,这些文件分层打包而成 + +Dockerfile是做什么的? + +- Dockerfile就是利用固定的指令来描述镜像的结构和构建过程,这样Docker才可以依次来构建镜像 + +构建镜像的命令是什么? + +- docker build -t 镜像名 Dockerfile目录 + +# 容器网络互连 + +java项目有时候需要访问其它各种中间件,例如MySQL、Redis等。现在,我们的容器之间能否互相访问呢?我们来测试一下 + +首先,我们查看下MySQL容器的详细信息,重点关注其中的网络IP地址: + +记得启动mysql容器 + +```bash +docker inspect mysql +``` + +得到的IP地址如下:`"IPAddress": "172.17.0.3"` + +然后通过命令进入demo容器 `docker exec -it demo bash` + +在demo容器中ping mysql容器,查看是否能成功 + +```bash +64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.168 ms +64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.087 ms +64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.077 ms +64 bytes from 172.17.0.3: icmp_seq=4 ttl=64 time=0.077 ms +``` + +测试表明,可以互联 + +默认情况下,所有容器是以bridge(网桥)方式连接到Docker的一个虚拟网桥上: + +![image-20231109145543378](https://s2.loli.net/2023/11/10/MIFjfg6iJeA8kW9.png) + +但是,容器的网络IP其实是一个虚拟的IP,其值并不固定与某一个容器绑定,如果我们在开发时写死某个IP,而在部署时很可能MySQL容器的IP会发生变化,连接会失败。 + +所以,我们必须借助于docker的网络功能来解决这个问题,官方文档: + +https://docs.docker.com/engine/reference/commandline/network/ + +常见命令有: + +| **命令** | **说明** | **文档地址** | +| :------------------------ | :----------------------- | :----------------------------------------------------------- | +| docker network create | 创建一个网络 | [docker network create](https://docs.docker.com/engine/reference/commandline/network_create/) | +| docker network ls | 查看所有网络 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/network_ls/) | +| docker network rm | 删除指定网络 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/network_rm/) | +| docker network prune | 清除未使用的网络 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/network_prune/) | +| docker network connect | 使指定容器连接加入某网络 | [docs.docker.com](https://docs.docker.com/engine/reference/commandline/network_connect/) | +| docker network disconnect | 使指定容器连接离开某网络 | [docker network disconnect](https://docs.docker.com/engine/reference/commandline/network_disconnect/) | +| docker network inspect | 查看网络详细信息 | [docker network inspect](https://docs.docker.com/engine/reference/commandline/network_inspect/) | + +## 自定义网络 + +```bash +docker network ls +``` + +先查看存在的网络有哪些 + +接着我们可以创建一个网络 + +创建一个eastwind的网络,并查看所有网络 + +```bash +docker network create eastwind +docker network ls +``` + +```bash +NETWORK ID NAME DRIVER SCOPE +3de0c3b573b5 bridge bridge local +76140a3e4693 eastwind bridge local +8c7edfa12652 host host local +8ef97f11560b none null local +``` + +此时发现刚刚创建的网络也在其中 +如果想加入一个容器到自定义网络中,可以使用`connect` + +```bash +docker network connect eastwind mysql +``` + +这样就可以将mysql加入到eastwind网络中了 + +此时可以通过 + +```bash +# 查看网络 +docker inspect mysql +``` + +image-20231109154755754 + +上面这种是可以在容器存在时进行网络连接,下面这种方法可以让容器在创建时就进行连接 +我们先将之前的demo容器删除 + +接着在创建容器时,可以添加一个参数`--network`,后面跟自定义网络的名称 + +```bash +docker run -d --name demo -p 8080:8080 --network eastwind docker-demo +``` + +```bash +docker inspect +``` + +此时如果我们进入demo容器中ping mysql和nginx + +```bash +docker exec -it demo bash +ping mysql +ping nginx +``` + +因为nginx与demo不在一个网段中,所以无法ping通,但mysql可以 + +**加入自定义网络的容器才可以通过容器名互相访问** + +# 部署Java项目 + +将资料中的项目打包为jar包 + +将jar包和Dockerfile文件传入linux下 + +```bash +docker build -t emall . +``` + +查看docker 镜像是否部署成功 + +```bash +docker images +``` + +接着运行该镜像 + +代码释义: + +后台运行该镜像,名称为emall,指定开放端口为8080,设置网络为eastwind,指定运行的镜像为emall + +```bash +docker run -d --name emall -p 8080:8080 --network eastwind emall +``` + +接着可以通过查看日志 + +```bash +docker logs -f emall +``` + +访问该路径 + +``` +http://192.168.10.142:8080/hi +``` + +# 部署前端 + +修改nginx目录下的nginx.conf为 + +这里的proxy_pass需要修改http://自己的后端镜像:8080 + +```conf +server { + listen 18080; + # 指定前端项目所在的位置 + location / { + root /usr/share/nginx/html/hmall-portal; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + location /api { + rewrite /api/(.*) /$1 break; + proxy_pass http://eastwind:8080; + } + } + server { + listen 18081; + # 指定前端项目所在的位置 + location / { + root /usr/share/nginx/html/hmall-admin; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + location /api { + rewrite /api/(.*) /$1 break; + proxy_pass http://eastwind:8080; + } + } +``` + +将前台的打包好的内容上传 + +运行前台的项目 + +命令解读: + +以后台模式运行一个叫nginx的容器 + +开放端口18080和18081,并且与容器内的队友路径进行关联 + +设置对应的网络为自定义网络eastwind + +设置使用的镜像为nginx + +```bash +docker run -d \ +--name nginx \ +-p 18080:18080 \ +-p 18081:18081 \ +-v /root/nginx/html:/usr/share/nginx/html \ +-v /root/nginx/nginx.conf:/etc/nginx/nginx.conf \ +--network eastwind \ +nginx +``` + +访问http://你的虚拟机ip:18080 + +# DockerCompose + +我们部署一个简单的java项目,其中包含3个容器: + +- MySQL +- Nginx +- Java项目 + +而稍微复杂的项目,其中还会有各种各样的其它中间件,需要部署的东西远不止3个。如果还像之前那样手动的逐一部署,就太麻烦了。 + +而Docker Compose就可以帮助我们实现**多个相互关联的Docker容器的快速部署**。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器。 + +## 基本语法 + +docker-compose.yml文件的基本语法可以参考官方文档:https://docs.docker.com/compose/compose-file/compose-file-v3/ + +docker-compose文件中可以定义多个相互关联的应用容器,每一个应用容器被称为一个服务(service)。由于service就是在定义某个应用的运行时参数,因此与`docker run`参数非常相似。 + +举例来说,用docker run部署MySQL的命令如下: + +```bash +docker run -d \ + --name mysql \ + -p 3306:3306 \ + -e TZ=Asia/Shanghai \ + -e MYSQL_ROOT_PASSWORD=123 \ + -v ./mysql/data:/var/lib/mysql \ + -v ./mysql/conf:/etc/mysql/conf.d \ + -v ./mysql/init:/docker-entrypoint-initdb.d \ + --network eastwind + mysql +``` + +如果用`docker-compose.yml`文件来定义,就是这样: + +```yaml +version: "3.8" + +services: + mysql: + image: mysql + container_name: mysql + ports: + - "3306:3306" + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: 123 + volumes: + - "./mysql/conf:/etc/mysql/conf.d" + - "./mysql/data:/var/lib/mysql" + networks: + - new +networks: + new: + name: eastwind +``` + +对比如下: + +| **docker run 参数** | **docker compose 指令** | **说明** | +| :------------------ | :---------------------- | :--------- | +| --name | container_name | 容器名称 | +| -p | ports | 端口映射 | +| -e | environment | 环境变量 | +| -v | volumes | 数据卷配置 | +| --network | networks | 网络 | + +商城部署文件: + +```YAML +version: "3.8" + +services: + mysql: + image: mysql + container_name: mysql + ports: + - "3306:3306" + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: 123 + volumes: + - "./mysql/conf:/etc/mysql/conf.d" + - "./mysql/data:/var/lib/mysql" + - "./mysql/init:/docker-entrypoint-initdb.d" + networks: + - hm-net + hmall: + build: + context: . + dockerfile: Dockerfile + container_name: hmall + ports: + - "8080:8080" + networks: + - hm-net + depends_on: + - mysql + nginx: + image: nginx + container_name: nginx + ports: + - "18080:18080" + - "18081:18081" + volumes: + - "./nginx/nginx.conf:/etc/nginx/nginx.conf" + - "./nginx/html:/usr/share/nginx/html" + depends_on: + - hmall + networks: + - hm-net +networks: + hm-net: + name: hmall +``` + +编写好docker-compose.yml文件,就可以部署项目了。常见的命令:https://docs.docker.com/compose/reference/ + +基本语法如下: + +```bash +docker compose [OPTIONS] [COMMAND] +``` + +其中,OPTIONS和COMMAND都是可选参数,比较常见的有: + +![image-20231110152104483](https://s2.loli.net/2023/11/10/kpv3fbNEjPGnVqh.png) + +需要删除旧容器及旧镜像 + +在root目录运行即可docker compose up -d + diff --git a/public/markdowns/Git.md b/public/markdowns/Git.md new file mode 100644 index 0000000..0d2c5c7 --- /dev/null +++ b/public/markdowns/Git.md @@ -0,0 +1,420 @@ +--- +title: Git工具 +tags: + - Git + - 瑞吉外卖 +categories: + - Git +description: Git快速入门教程 +abbrlink: a97adaa0 +--- + +# 1、Git概述 + +- Git是一个分布式版本控制工具,主要用于管理开发过程中的源代码文件(Java类、xml文件、html页面等),在软件开发过程中被广泛使用。 +- 作用 + 1. 代码回溯 + 2. 版本切换 + 3. 多人协作 + 4. 远程备份 + +## 简介 + +- Git 是一个分布式版本控制工具,通常用来对软件开发过程中的源代码文件进行管理。通过Git仓库来存储和管理这些文件,Git仓库分为两种: + 1. 本地仓库:开发人员自己电脑上的Git仓库 + 2. 远程仓库:远程服务器上的Git仓库 +- `commit`:提交。将本地文件和版本信息保存到本地仓库 +- `push`:推送,将本地仓库文件和版本信息上传到远程仓库 +- `pull`:拉取,将远程仓库文件和版本信息下载到本地仓库 + +Git一般的操作就是将本地仓库中的数据提交后推送到远程仓库上,而另一个本地仓库需要时只需要拉取一下在远程仓库上的数据即可,反之同理 + +## 下载和安装 + +- 这个应该没啥好说的,安装完毕之后再任意目录点击右键出现如下菜单,则说明安装成功 + +image-20230801081300669 + +一般经常使用的都是`Git Bash Here` + + + +# Git代码托管服务 + +## 常用的Git代码托管服务 + +- Git中存在两种类型的仓库,即`本地仓库`和`远程仓库`,那么我们如何搭建`- Git远程仓库`呢? +- 我们可以借助互联网上提供的一些代码托管服务俩实现,比较常用的有GitHub、Gitee(狗都不用)、GitLab等 + - `GitHub`( 地址:`https://github.com/` ),是一个面向开源私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub + - `Gitee`(地址: `https://gitee.com/` ),又名码云,是国内的一个代码托管平台,由于服务器在国内,所以相比较于GitHub,Gitee速度更快 + - `GitLab`(地址: `https://about.gitlab.com/` ),是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的Web服务 + - `BitBucket`(地址:`https://bitbucket.org/`) ,是一家代码托管网站,采用Mercurial和Git作为分布式版本控制系统,同时提供商业计划和免费账户 + +## 使用GitHub代码托管服务 + +具体步骤如下 + +### 注册登录GitHub账号 + +注册Github账号`https://github.com/` + +通过这个网址来注册github账号,注册和登录就没什么好说的了 + + + +### 创建一个远程仓库 + +![image-20230801084001670](https://s2.loli.net/2023/08/14/btKhIm6Lv5NEVjC.png) + +右上角有一个+号,点开后New repository可以来创建远程仓库 + +image-20230801085420384 + +就正常的创建就行了,下面都有这些英文解释,基本上不需要太大的变动 + +创建完成后是这个样子 + +image-20230801085648789 + + + +### 邀请其他用户成为仓库成员 + +![image-20230801085909782](https://s2.loli.net/2023/08/14/E6tsWbdlpzfhIym.png) + + + +# Git常用命令 + +## Git全局设置 + +安装Git后首先要做的是设置用户名称和email地址。这是非常重要的,因为每次Git提交都会使用该用户的信息 + + + +在Git命令行中执行下面的命令: + +- 设置用户信息 + +```bash +git config --global user.name "你的用户名" +``` + +```bash +git config --global user.email "你的邮箱" +``` + +- 查看配置信息 + +```bash +git config --list +``` + +- 签名的作用是为了区分不同操作者的身份 +- 用户的签名信息在每一个版本的提交信息中能够看到,以确认本次提交是谁做的 +- Git首次安装必须设置一下用户签名,否则无法提交代码 + +注意:上面设置的user.name和user.email并不是在Github上注册的用户名和邮箱,此处可以任意设置\ + +通过上面的命令设置的信息都保存在`~/.gitconfig`文件中 + +## 获取Git仓库 + +要使用Git对我们的代码进行版本控制,首先需要获得Git仓库 + +`获取Git仓库的命令` + +本地初始化(不常用) + +```bash +git init +``` + +从远程仓库克隆(常用) + +```bash +git clone +``` + +这里给一个简单示例将远程仓库克隆过来 + +我们创建一个文件夹暂存一下我们的Git仓库 + +![image-20230801095043522](https://s2.loli.net/2023/08/14/lHNgJVjkiuGXZoe.png) + +这里是我们的一个文件夹,我们右击鼠标Git Bash Here + +image-20230801095132780 + +这样就来到了我们的一个文件夹内 + +我们去GitHub上面复制一下HTTPS到这来 + +![image-20230801095830555](https://s2.loli.net/2023/08/14/KIPNd3jXfHQzFZw.png) + +我们执行git clone "你的远程仓库地址" + +![image-20230801095904294](https://s2.loli.net/2023/08/14/WE39t6KP7GkCvSQ.png) + +因为我的远程仓库是空的,所以报了一个警告,我们再到文件夹中查看 + +image-20230801095935106 + +此时,远程仓库就被克隆过来了 + +注意:仓库是不能嵌套的,不能在一个仓库目录内,克隆/创建另一个仓库。 + +## 工作区、暂存区、版本库概念 + +为了更好的学习Git,我们需要了解一下Git相关的一些概念,这些概念在后面的学习中会经常提到。 + +- 版本库:其实你在`git init`之后,会在当前文件夹创建一个隐藏文件`.git`,这个文件就是版本库,版本库中存储了很多的配置信息、日志信息和文件版本信息等 +- 工作目录(工作区):包含`.git`文件夹的目录就是工作目录,主要用于存放开发的代码 +- 暂存区:一个临时保存修改文件的地方 + - `.git`文件夹内有很多文件,其中有一个名为`index`的文件就是暂存区,也可以叫做`stage` + +## Git工作区中文件的状态 + +- Git工作目录下的文件存在两种状态 + - `untracked`未跟踪(未被纳入版本控制) + - `tracked`已跟踪(被纳入版本控制) + - `Unmodified`未修改状态 + - `Modified`已修改状态 + - `Staged`已暂存状态 + +这些文件的状态会随着我们执行Git的命令发生变化 + +## 本地仓库操作 + +## 本地仓库相关命令 + +| 命令 | 功能 | +| :---------------------------------: | :--------------------------------------: | +| git config —global user.name 用户名 | 设置用户签名 | +| git config —global user.email 邮箱 | 设置用户签名 | +| git init | 初始化本地库 | +| git status | 查看文件状态 | +| git add [文件名称] | 将文件的修改加入暂存区 | +| git reset [文件名称] | 将暂存区的文件取消暂存 | +| git reset —hard [版本号] | 切换到指定版本 | +| git commit [文件名] | 将暂存区文件提交到版本库中 | +| git commit -m “日志信息” | 将暂存区文件提交到版本库中并增加日志信息 | +| git log | 查看日志 | +| git reflog | 查看历史记录 | + +### git status + +image-20230801100210629 + +### git add [文件名称] + +将未跟踪的文件加入到暂存区,并查看文件状态 + +image-20230801100430539 + +此时发现暂存区就有文件了 + + + +### gie reset + +将暂存区的文件取消暂存,并查看文件状态 + +image-20230801100523282 + +### git commit -m "日志信息" [文件名] + +将暂存区的文件提交到版本库中 + +- 若提交多次,会产生多个版本号 +- 使用`git log`来查看版本号 + +image-20230801100838568 + +提交之前需要先添加到暂存区 + + + +### git reset --hard [版本号]` + +切换到指定版本 + +这个版本号是`git commit`提交后在`git log`上查看的,这里就不做演示了 + + + +### git rm [文件名] + +删除工作区文件,并查看状态,提交之后再次查看状态 + +![image-20230801101357472](https://s2.loli.net/2023/08/14/EfvZsrLyqwptnAO.png) + +删除的只是工作区的,仓库里的并没有被删除 + +## 远程仓库操作 + +| 命令 | 功能 | +| :---------------------------------: | :----------------------: | +| git remote | 查看远程仓库 | +| git remote -v | 查看当前所有远程地址别名 | +| git remote add [short-name] [url] | 添加远程仓库 | +| git remote rm [short-name] | 移除远程仓库 | +| git clone [url] | 从远程仓库克隆 | +| git pull [short-name] [branch-name] | 从远程仓库拉取 | +| git push [short-name] [branch-name] | 推送到远程仓库 | + +### git remote查看远程仓库 + +- 如果想查看已经配置的远程仓库服务器,可以运行`git remote`命令。它会列出指定的每一个远程服务器的简写 +- 如果已经克隆了远程仓库,那么至少应该能看到`origin`,这是`Git克隆`的仓库服务器的`默认`名字 + +### git remote -v则可以查看更为详细的信息 + +![image-20230802064828067](https://s2.loli.net/2023/08/14/SX37Reap2HVlbvc.png) + +### git remote add [short-name] [url] + +添加远程仓库,添加一个新的远程Git仓库,同时也指定搞一个可以引用的缩写 + +- 使用`git remote -v`查看是否连接成功,成功连接之后,我们就可以向仓库推送/拉取数据了 +- 移除远程仓库,运行`git remote rm [short-name]` +- 命令`git remote add `:将远程仓库唯一的URL`` 映射成为 在本地仓库中对远程仓库起的别名``。 +- 参数``:`在本地仓库中对远程仓库起的别名`。而我们按照Git官方教程,一般会把参数``设置为`origin`。 + 为什么要强调`在本地仓库中`?因为我们要知道`git remote add `是在我们自己的本地仓库对远程仓库起的别名,这个别名只能在我们自己的本地仓库使用,在真正的远程仓库那边,远程仓库的名字是一个绝对唯一的URL(比如:`git@github.com:michaelliao/learngit.git`),而不是`origin`。甚至我们的开发团队成员也可以自定义这个开发团队成员他个人的本地仓库中对远程仓库起的别名 + +![image-20230802070539065](https://s2.loli.net/2023/08/14/lKOcaJjy467M8wG.png) + +克隆仓库的命令格式是`git clone [url]` + +- 如果你想获得一份已经存在了的Git仓库的拷贝,那么就需要`git clone`命令 +- Git克隆的是该Git仓库服务器上的几乎所有数据(包括日志信息,历史记录等),而不仅仅是复制工作所需要的的文件 +- 当你执行`git clone`命令时,默认配置下远程Git仓库的每一个文件的每一个版本都会被拉取下来 + +使用`git push [remote-name] [branch-name]`推送文件到远程仓库 + +branch-name是分支的名字,也就是仓库名旁边那个蓝色的东西,我这里设置成了master,可以自己调整 + +![image-20230802070836415](https://s2.loli.net/2023/08/14/OqyWIBUi5X7gYCz.png) + +如果使用Gitee,可能需要登录验证 + + + +### git pull [short-name] [branch-name] + +从远程仓库获取最新版本并合并到本地仓库 + +- 一般是多人协作的时候用,我和小明分工合作,一人写一部分,他写完上传,然后我拉取下来合并到本地仓库,会方便很多 +- 注意:如果当前本地仓库不是从远程仓库克隆,而是本地创建的仓库,并且仓库中存在文件,此时再从远程仓库拉取文件的时候会报错`fatal: refusing to merge unrelated histories` +- 解决此问题可以在`git pull`命令后加入参数`--allow-unrelated-histories` + +![image-20230802071235216](https://s2.loli.net/2023/08/14/CsNBILUPOXjAzac.png) + +## 分支操作 + +### 简述 + +- 分支是Git使用过程中非常重要的概念 +- 几乎所有的版本控制系统个都以某种形式支持分支 +- 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线 +- Git的`master`分支并不是一个特殊的分支,它与其他分支没有区别 +- 之所以几乎每一个仓库都有`master`分支,是因为git init命令默认创建它,而大多数人都懒得去改它 +- 同一个仓库可以有多个分支,各个分支相互独立,互不干扰 + +### 分支操作常用命令 + +| 命令 | 功能 | +| :------------------------------------: | :----------------------------------------: | +| git branch | 查看分支:列出本地的所有分支 | +| git branch -r | 查看分支:列出所有的远程分支 | +| git branch -a | 查看分支:列出所有的本地分支和远程分支 | +| git branch [branch-name] | 创建分支 | +| git checkout [branch-name] | 切换分支 | +| git push [short-Name] [branch-name] | 推送至远程仓库分支 | +| git merge [branch-name] | 合并分支 | +| git branch -d [branch-name] | 删除分支 | +| git branch -D [branch-name] | 删除分支(即使该分支中进行了一些开发动作) | +| git push [short-Name] –d [branch-name] | 删除远程仓库中的分支 | + +### 分支操作展示 + +- 列出本地的所有分支 + - `git branch` +- 列出所有的远程分支 + - `git branch -r` +- 列出所有的本地和远程分支 + - `git branch -a` + +![image-20230802211110085](https://s2.loli.net/2023/08/14/j5uiOqsIEy7UJo3.png) + +- 创建分支 + - `git branch [branch-name]` +- 切换分支 + - `git checkout [branch-name]` +- 推送至远程仓库分支 + - `git push [short-Name] [branch-name]` + +![image-20230802212112830](https://s2.loli.net/2023/08/14/mhTvjgnL2Z65yIA.png) + +创建完成后,可以在Github对应的仓库中查看分支 + +image-20230802212100028 + +合并分支 + +- `git merge [branch-name]` +- 我们先在`test01`分支中添加点东西,然后再切换到`master`分支,合并分支,在`master`分支中可以看到`test01`分支中新添加的文件 + +![image-20230802212537427](https://s2.loli.net/2023/08/14/xmqpPZfnLz9JSUi.png) + +合并分支的时候可能会出现冲突 + +- 冲突产生的原因 + - 合并分支时,两个分支在同一个文件的同一个位置有两套完全不同的修改。 + - Git 无法替我们决定使用哪一个。必须通过手动操作来决定新代码内容。 +- 解决方案 + - 编辑有冲突的文件,决定要使用的内容 + - 将编辑好的文件添加到暂存区,(使用`git add`命令) + - 最后执行提交(注意:此时的提交只输入`git commit`,不要加文件名,或者执行`git commit [文件名] -i`) + +删除分支 + +- `git branch -d [branch-name]` + +![image-20230802212658822](https://s2.loli.net/2023/08/14/yUbsQZLRD7OrCSI.png) + +## 标签操作 + +### 简述 + +- Git中的标签,指的是某个分支特定时间点的状态。 +- 通过标签,我们可以很方便地切换到标记时的状态(给我的感觉像是Linux的快照) + +### 标签操作的常用命令 + +| 命令 | 功能 | +| :-----------------------------: | :------------------: | +| git tag | 创建标签 | +| git tag [name] | 创建标签 | +| git push [shortName] [name] | 将标签推送到远程仓库 | +| git checkout -b [branch] [name] | 检出标签 | + +image-20230802214005607 + +分支与标签的区别 + +- `标签` 是一个`静态`的概念,一旦标签确定后,其中的文件的状态就确定了。 +- `分支` 是一个`动态`的概念,其中的文件的状态可以发生变化。 + +# 在IDEA中使用Git + +- 在IDEA中使用Git,其实也是用的我们自己安装的Git +- 在 IDEA 中使用 Git 获取仓库有两种方式: + 1. 本地初始仓库 + - 选择VCS选项卡 —> 创建Git仓库 —> 选择需要被Git管理的目录 —> 确定 + 2. 从远程仓库克隆(常用) + - 这个可以自己选择克隆的本地位置 + - 可以直接把远程仓库的代码都克隆到本地 + - 远程克隆下来的项目会自带一个文件:`.gitignore`文件,在里面的信息是代表哪些文件不需要交给git管理 + +关于本地仓库操作、远程仓库操作、分支操作,IDEA都给我们提供了图形化界面,方便我们使用,这里就不再展开说明了,不太明白的地方可以上百度搜搜 diff --git "a/public/markdowns/MyBatis-Plus\344\273\243\347\240\201\347\224\237\346\210\220\345\231\250.md" "b/public/markdowns/MyBatis-Plus\344\273\243\347\240\201\347\224\237\346\210\220\345\231\250.md" new file mode 100644 index 0000000..fd9bdeb --- /dev/null +++ "b/public/markdowns/MyBatis-Plus\344\273\243\347\240\201\347\224\237\346\210\220\345\231\250.md" @@ -0,0 +1,139 @@ +--- +title: MyBatis-Plus代码生成器 +tags: + - MyBatis-Plus + - 实用工具 +categories: + - 实用工具 +description: Git快速入门教程 +abbrlink: ced26210 +date: 2023-08-19 13:18:36 +--- + +`MyBatis-Plus-Generator`:自动生成 Controller Service Mapper/DAO层等的基本代码,免去自己去写实体类映射数据库的繁琐操作 + + + +# 添加依赖 + +```xml + + + + com.baomidou + mybatis-plus-generator + 3.4.1 + + + + org.apache.velocity + velocity-engine-core + 2.0 + + +``` + + + +# 运行代码 + +在测试类中运行代码即可 + +```java +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.PackageConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; + + + +public class MyBatisPlusGenerator { + + public static void main(String[] args) { + //1. 全局配置 + GlobalConfig config = new GlobalConfig(); + // 是否支持AR模式 + config.setActiveRecord(true) + // 作者 + .setAuthor("EastWind") + // 生成路径,最好使用绝对路径 + .setOutputDir("D:\\test") + // 文件覆盖 + .setFileOverride(true) + // 主键策略 + .setIdType(IdType.AUTO) + + .setDateType(DateType.ONLY_DATE) + // 设置生成的service接口的名字 + // xxxService + .setServiceName("%sService") + + //实体类结尾名称 + .setEntityName("%s") + + //生成基本的resultMap + .setBaseResultMap(true) + + //不使用AR模式 + .setActiveRecord(false) + + //生成基本的SQL片段 + .setBaseColumnList(true); + + //2. 数据源配置 + DataSourceConfig dsConfig = new DataSourceConfig(); + // 设置数据库类型 + dsConfig.setDbType(DbType.MYSQL) + .setDriverName("com.mysql.cj.jdbc.Driver") + //TODO TODO TODO TODO + .setUrl("jdbc:mysql://localhost:13306/mall?serverTimezone=UTC&useSSL=false") + .setUsername("root") + .setPassword("密码"); + + //3. 策略配置globalConfiguration中 + StrategyConfig stConfig = new StrategyConfig(); + + //全局大写命名 + stConfig.setCapitalMode(true) + // 数据库表映射到实体的命名策略 + .setNaming(NamingStrategy.underline_to_camel) + + //使用restcontroller注解 + .setRestControllerStyle(true) + + // 生成的表, 支持多表一起生成,以数组形式填写 + // 设置需要生成的表 + .setInclude("自己在数据库中的表名"); + + //4. 包名策略配置 + PackageConfig pkConfig = new PackageConfig(); + pkConfig.setParent("fun.eastwind.mall") + .setMapper("mapper") + .setService("service") + .setController("controller") + .setEntity("pojo"); + + + //5. 整合配置 + AutoGenerator ag = new AutoGenerator(); + ag.setGlobalConfig(config) + .setDataSource(dsConfig) + .setStrategy(stConfig) + .setPackageInfo(pkConfig); + + //6. 执行操作 + ag.execute(); + System.out.println("======= Done 相关代码生成完毕 ========"); + } + + +} +``` + +运行后就会在D:/test目录下发现自己生成的代码了! + diff --git "a/public/markdowns/MySql\345\237\272\347\241\200.md" "b/public/markdowns/MySql\345\237\272\347\241\200.md" new file mode 100644 index 0000000..f4bbcea --- /dev/null +++ "b/public/markdowns/MySql\345\237\272\347\241\200.md" @@ -0,0 +1,607 @@ +--- +title: MySql基础 +tags: MySql +abbrlink: 8a2dddd9 +date: 2023-12-19 14:52:48 +--- + +# 什么是数据库? + +数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。 + +每个数据库都有一个或多个不同的API用于创建,访问,管理,搜索和复制所保存的数据 + +我们也可以将数据存储在文件中,但是在文件中读写数据速度相对较慢 + +所以,我们使用关系型数据库管理系统(RDBMS)来存储和管理大数据量。所谓的关系型数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据 + +RDBMS即关系数据库管理系统(Relational Database Management System)的特点: + +1. 数据以表格的形式出现 +2. 每行为各种记录名称 +3. 每列为记录名称所对应的数据域 +4. 许多的行和列组成一张表单 +5. 若干的表单组成database + +# RDBMS术语 + +在学习前,先了解RDBMS(关系型数据库管理系统)的一些术语 + +- 数据库:数据库是一些关联表的集合 +- 数据表:表是数据的矩阵。在一个数据库中的表看起来像一个简单的电子表格 +- 列:一列(数据元素)包含了相同类型的数据,例如姓名、年龄的数据 +- 一行(元组,或记录)是一组相关的数据,比如一个用户所属的相关信息 +- 冗余:存储两倍数据,冗余降低了性能,但提高了数据的安全性 +- 主键:主键是唯一的。一个数据表中只能包含一个主键。你可以使用主键来查询数据 +- 外键:外键用于关联两个表 +- 复合键:复合键(组合键)将多个列作为一个索引键,一般用于复合索引 +- 索引:使用索引可快速访问数据库表中的特定信息。索引是对数据库表中一列或多列的值进行排序的一种结构。例如书籍的目录 +- 参照完整性:参照的完整性要求关系中不允许引用不存在的实体。与实体完整性是关系模型必须满足的完整性约束条件,目的是保证数据的一致性 + +MySql为关系型数据库(Relational Database Management System),这种所谓的“关系型”可以理解为表格的概念,一个关系型数据库由一个或数个表格组成,表格如图所示 + +| id | name | age | +| :--: | :-------: | :--: | +| 1 | zhangsan1 | 10 | +| 2 | zhangsan2 | 20 | +| 3 | zhangsan3 | 30 | + +![image-20231219150732146](https://s2.loli.net/2023/12/19/HRmKjXhGQBFVxL1.png) + +- 表头(header):每一列的名称 +- 列(col):具有相同数据类型的数据的集合 +- 行(row):每一行用来描述某条记录的具体信息 +- 值(value):行的具体信息,每个值需要与该列的数据类型相同 +- 主键(key):键的值在当前列,具有**唯一性** + +# MySql安装 + +这里以Windows安装MySql为例,这里附上MySQL的Windows版下载地址:https://dev.mysql.com/downloads/mysql/ + +选择合适自己的版本,直接下载即可 + +下载完成后,将压缩包解压到相应的目录下 + +具体的安装流程可以参考这篇博客:https://blog.csdn.net/qq_53381910/article/details/131277067 + +# MySql连接 + +win+r,打开运行窗口,输入cmd,接着就会打开命令行窗口,在命令行窗口中输入对应的命令就可以进入mysql + +```cmd +mysql -u 你的用户名 -p +``` + +参数说明: + +- -u 参数用于指定用户名 +- -p 参数表示需要输入密码 + +敲完按回车,接着输入你的密码,密码是不可见的,正常输入完成后,就会弹出MySQL的欢迎提示了 + +成功连接到MySql后,你可以在命令行中执行执行SQL查询。 + +列出所有可用的数据库: + +```SQL +SHOW DATABASES +``` + +选择要使用的数据库 + +```sql +USE 你的数据库名 +``` + +列出所选数据库中的所有表 + +```SQL +SHOW TABLES +``` + +退出`mysql>`命令提示窗口可以使用`exit`命令 + +```cmd +EXIT; +``` + +# MySql创建数据库 + +我们可以在登录MySql服务后,使用`create`命令创建数据库,语法如下: + +```sql +CREATE DATABASE 数据库名 +``` + +假设我们想创建一个eastwind的数据库 + +```sql +CREATE DATABASE eastwind +``` + +建数据库的基本语法如下: + +```sql +CREATE DATABASE [IF NOT EXISTS] database_name + [CHARACTER SET charset_name] + [COLLATE collation_name] +``` + +这里的中括号是可选项,可填可不填,`IF NOT EXISTS`是说,如果不存在,才对该数据库进行创建 + +- `CHARACTER SET charset_name`:指定字符集 +- `COLLATE collation_name`:指定排序规则 + +# MySql删除数据库 + +使用普通用户登录MySql服务器,需要特定的权限才能创建或者删除数据库,所以通过root用户登录,root用户拥有最高权限 + +在删除数据库过程中,务必要十分谨慎,因为在执行删除命令后,所有的数据将会消失。 + +## drop命令删除数据库 + +drop命令格式: + +```sql +DROP DATABASE ; -- 直接删除数据库,不检查是否存在 +或 +DROP DATABASE [IF EXISTS] ; +``` + +参数说明: + +- IF EXISTS是一个可选的字句,表示如果数据库存在才执行删除操作,避免因为数据库不存在而引发错误 +- database_name是你要删除的数据库的名称 + +假设要删除的数据库是eastwind + +```sql +DROP DATABASE IF EXISTS eastwind +``` + +# MySql选择数据库 + +在你连接到MySql数据库后,可能有多个可以操作的数据库,所以你需要选择你要操作的数据库 + +使用`USE`命令选择使用的数据库 + +```sql +USE database_name +``` + +参数说明: + +- database_name 是你要选择的数据库的名称 + +选择数据库后,后续的操作在你选择的数据库上执行。 + +# 数据类型 + +MySql中定义数据字段的类型对你数据库的优化是非常重要的。 + +MySql支持多种类型,大致分为三类:数据、日期/时间和字符串(字符)类型 + +MySql支持所有标准SQL数值数据类型 + +这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL和NUMERIC),以及近似数值数据类型(FLOAT、REAL和DOUBLE PRECISION) + +## 数值类型 + +| 类型 | 大小 | 范围(有符号) | 范围(无符号) | 用途 | +| :----------: | :--------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :-------------: | +| TINYINT | 1 Bytes | (-128,127) | (0,255) | 小整数值 | +| SMALLINT | 2 Bytes | (-32 768,32 767) | (0,65 535) | 大整数值 | +| MEDIUMINT | 3 Bytes | (-8 388 608,8 388 607) | (0,16 777 215) | 大整数值 | +| INT或INTEGER | 4 Bytes | (-2 147 483 648,2 147 483 647) | (0,4 294 967 295) | 大整数值 | +| BIGINT | 8 Bytes | (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 极大整数值 | +| FLOAT | 4 Bytes | (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0,(1.175 494 351 E-38,3.402 823 466 E+38) | 单精度 浮点数值 | +| DOUBLE | 8 Bytes | (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 双精度 浮点数值 | +| DECIMAL | 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 | 依赖于M和D的值 | 依赖于M和D的值 | 小数值 | + +## 日期和时间类型 + +表示时间值的日期和时间类型为DATETIME、DATE、TIMESTAMP、TIME和YEAR + +每个时间类型有一个有效值范围和一个零值,当指定不合法的MySql不能表示的值时使用零值 + +TIMESTAMP类型有专有的自动更新特性 + +| 类型 | 大小 ( bytes) | 范围 | 格式 | 用途 | +| :-------: | :-----------: | :----------------------------------------------------------: | :-----------------: | :----------------------: | +| DATE | 3 | 1000-01-01/9999-12-31 | YYYY-MM-DD | 日期值 | +| TIME | 3 | '-838:59:59'/'838:59:59' | HH:MM:SS | 时间值或持续时间 | +| YEAR | 1 | 1901/2155 | YYYY | 年份值 | +| DATETIME | 8 | '1000-01-01 00:00:00' 到 '9999-12-31 23:59:59' | YYYY-MM-DD hh:mm:ss | 混合日期和时间值 | +| TIMESTAMP | 4 | '1970-01-01 00:00:01' UTC 到 '2038-01-19 03:14:07' UTC 结束时间是第 **2147483647** 秒,北京时间 **2038-1-19 11:14:07**,格林尼治时间 2038年1月19日 凌晨 03:14:07 | YYYY-MM-DD hh:mm:ss | 混合日期和时间值,时间戳 | + +## 字符串类型 + +字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。 + +| 类型 | 大小 | 用途 | +| :--------: | :-------------------: | :-----------------------------: | +| CHAR | 0-255 bytes | 定长字符串 | +| VARCHAR | 0-65535 bytes | 变长字符串 | +| TINYBLOB | 0-255 bytes | 不超过 255 个字符的二进制字符串 | +| TINYTEXT | 0-255 bytes | 短文本字符串 | +| BLOB | 0-65 535 bytes | 二进制形式的长文本数据 | +| TEXT | 0-65 535 bytes | 长文本数据 | +| MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 | +| MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 | +| LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 | +| LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 | + +注意:char(n)和varchar(n)中的n代表字符的个数,并不代表字节个数,比如CHAR(30)就可以存储30个字符 + +CHAR和VARCHAR类型类似,但它们保存和检索的方式不同。它们的最大长度和是否尾部空格被保留等方面也不同。在存储和检索过程中不进行大小写转换 + +BINARY和VARBINARY类似于CHAR和VARCHAR,不同的是它们包含二进制字符串而不要非二进制字符串。也就是说,它们包含字节字符串而不是字符字符串。也说明它们没有字符集,并且排序和比较基于列值字节的数值值 + +BLOB是一个二进制大对象,可以容纳可变数量的数据。有4种BLOB类型:TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB。它们在区别在于可容纳存储范围不同 + +有4种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT,对应着存储不同最大长度 + +## 枚举和集合类型(Enumeration and Set Typres) + +- ENUM:枚举类型,用于存储单一值,可以选择一个预定义的集合 +- SET:集合类型,用于存储多个值,可以选择多个预定义的集合 + +# MySql创建数据表 + +创建数据表需要以下信息: + +- 表名 +- 表字段名 +- 定义每个表字段的数据类型 + +语法 + +```sql +CREATE TABLE table_name( + column1 datatype, + column2 datatype +) +``` + +参数说明: + +- table_name 是你要创建的表的名称 +- column1,column2是表中的列名,或者说是字段名 +- datatype是每个列的数据类型 + +这里看一个全面的实例 + +```SQL +CREATE TABLE IF NOT EXISTS `EASTWIND`( + `ID` INT PRIMARY KEY AUTO_INCREMENT, + `TITLE` VARCHAR(100) NOT NULL, + `AUTHOR` VARCHAR(40) NOT NULL, + `DATE` DATE +)ENGINE=INNODB DEFAULT CHARSET = UTF8; +``` + +CREATE TABLE IF EXISTS `EASTWIND`:如果EASTWIND这个表不存在,就进行创建 + +```sql +`ID` INT PRIMARY KEY AUTO_INCREMENT, + `TITLE` VARCHAR(100) NOT NULL, + `AUTHOR` VARCHAR(40) NOT NULL, + `DATE` DATE +ID、TITLE、AUTHOR、DATE这些是column +VARCHAR、DATE这些是datatype +而datatype后面跟的PRIMARY KEY、NOT NULL这些,就是给出一些条件 +``` + +**AUTO_INCREMENT**:定义列为自增的属性,一般用于主键,数值会自动加1 + +**PRIMARY KEY**:用于定义列为主键 + +ENGINE=INNODB DEFAULT CHARSET = UTF8:指定存储引擎为INNODB,CHARSE设置编码 + +# MySql删除数据表 + +和删除数据库基本一致 + +语法 + +```sql +DROP TABLE 表名 -- 直接删除表,不检查是否存在 +或 +DROP TABLE [IF EXISTS] table_name; +``` + +- table_name是要删除的表的名称 +- IF EXISTS是一个可选的子句,表示如果表存在才执行删除操作,避免因为表不存在而引发错误 + +# MySql插入数据 + +MySql表中使用`insert into`语句来插入数据 + +语法 + +```SQL +INSERT INTO TABLE_NAME (COLUMN1,COLUMN2,COLUMN3,...) +VALUES(VALUE1,VALUE2,VALUE3,...) +``` + +参数说明: + +- table_name是你要插入数据的表的名称 +- column1,column2,column3是表中的列名 +- value1,value2,value3是要插入的具体数值 + +如果数据是字符型,必须使用单引号`'`或者双引号`"`,如:'value1'、'value2' + +假如我们想插入一行数据到eastwind表中 + +```sql +INSERT INTO eastwind(id,name,age) +values(1,"zhangsan",18) +``` + +- id:主键唯一标识,整数类型 +- name:姓名,字符串类型 +- age:年龄,整数类型 + +如果是插入所有列的数据,可以省略列名 + +```SQL +INSERT INTO eastwind +values(NULL,"zhangsan",18) +``` + +这里,`NULL`是用于自增长列的占位符,表示系统将为id列生成一个唯一的值 + +如果要插入多行数据,可以在VALUES子句中指定多组数值 + +```sql +INSERT INTO eastwind +values +(NULL,"zhangsan",18), +(NULL,"zhangsan",19), +(NULL,"zhangsan",17) +``` + +# MySql查询数据 + +MySql数据库使用`SELECT`语句来查询数据 + +语法 + +```SQL +SELECT COLUMN1,COLUMN2,... +FROM TABLE_NAME +[WHERE CONDITION] +[ORDER BY COLUMN_NAME [ASC | DESC]] +[LIMIT NUMBER] +``` + +参数说明: + +- column1,column2是你想要选择的列的名称,如果使用*表示选择所有列 +- table_name是你要查询数据的数据表的名称 +- WHERE CONDITION是一个可选字句,用于过滤条件,返回符合条件的行 +- ORDER BY column_name [ASC | DESC]是一个可选的子句,用于指定结果集的排序顺序,默认是升序(ASC) +- LIMIT NUMBER是一个可选的子句,用于限制返回的行数 + +```SQL +-- 选择所有列的行 +SELECT * FROM EASTWIND; + +-- 选择特定列的所有行 +SELECT ID,NAME FROM EASTWIND; + +-- 添加Where子句,选择满足条件的行 +SELECT * FROM EASTWIND WHERE NAME = 'zhangsan'; + +-- 添加ORDER BY子句,按照某列的升序进行排序 +SELECT * FROM EASTWIND ORDER BY NAME; + +-- 添加ORDER BY子句,按照某列的降序进行排序 +SELECT * FROM EASTWIND ORDER BY NAME DESC; + +-- 添加LIMIT 子句,限制返回的行数 +SELECT * FROM EASTWIND LIMIT 5 +``` + +## Where子句 + +我们知道从MySql表中使用`SELECT`语句来读取数据 + +如果想要带条件从表中筛选数据,可以将WHERE子句添加到SELECT语句中 + +WHERE子句用于在MySql中过滤查询结果,只返回满足特定条件的行 + +语法 + +```SQL +SELECT COLUMN1,COLUMN2,... +FROM TABLE_NAME +WHERE CONDITION; +``` + +参数说明: + +- column1,column2,...是你要选择的列的名称,如果使用*表示选择所有列 +- table_name是你要从中查询数据的表的名称 +- WHERE condition是用于指定过滤条件的子句 + +更多说明: + +- 查询语句中你可以使用一个或者多个表,表之间使用逗号`,`分割,并使用WHERE语句来设定查询条件 +- 可以在WHERE子句中指定任何条件 +- 可以使用AND或者OR指定一个或多个条件 +- WHERE子句也可以运用SQL的DELETE或UPDATE命令 +- WHERE子句类似于程序语言中的if条件,根据MySQL表中的字段值来读取指定的数据 + +| 操作符 | 描述 | 实例 | +| :----: | :----------------------------------------------------------: | :------------------: | +| = | 等号,检测两个值是否相等,如果相等返回true | (A = B) 返回false。 | +| <>, != | 不等于,检测两个值是否相等,如果不相等返回true | (A != B) 返回 true。 | +| > | 大于号,检测左边的值是否大于右边的值, 如果左边的值大于右边的值返回true | (A > B) 返回false。 | +| < | 小于号,检测左边的值是否小于右边的值, 如果左边的值小于右边的值返回true | (A < B) 返回 true。 | +| >= | 大于等于号,检测左边的值是否大于或等于右边的值, 如果左边的值大于或等于右边的值返回true | (A >= B) 返回false。 | +| <= | 小于等于号,检测左边的值是否小于或等于右边的值, 如果左边的值小于或等于右边的值返回true | (A <= B) 返回 true。 | + +等于条件 + +```SQL +SELECT * FROM EASTWIND WHERE XXX = XXX +``` + +不等于条件 + +```sql +SELECT * FROM EASTWIND WHERE XXX != XXX +SELECT * FROM EASTWIND WHERE XXX <> XXX +``` + +大于、大于等于条件 + +```sql +SELECT * FROM EASTWIND WHERE XXX > XXX +SELECT * FROM EASTWIND WHERE XXX >= XXX +``` + +小于、小于等于条件 + +```sql +SELECT * FROM EASTWIND WHERE XXX < XXX +SELECT * FROM EASTWIND WHERE XXX <= XXX +``` + +组合条件(AND、OR) + +```SQL +SELECT * FROM EASTWIND WHERE XXX = XXX AND XXX = XXX +SELECT * FROM EASTWIND WHERE XXX = XXX OR XXX = XXX +``` + +模糊匹配条件(LIKE) + +LIKE是正则匹配内容 + +`%`表示匹配任意,`_`表示匹配单个 + +```SQL +SELECT * FROM EASTWIND WHERE XXX LIKE 'X%' +``` + +IN条件 + +比如说1 in (1,2,3),这会返回满足条件的行数据 + +```SQL +SELECT * FROM EASTWIND WHERE XXX IN (XXX,XXX) +``` + +NOT条件 + +跟取反是一个意义的,比如1=1是true,NOT 1=1就是false + +```SQL +SELECT * FROM EASTWIND WHERE NOT 1 = 1 +``` + +BETWEEN条件 + +BETWEEN...AND 从字面上翻译过来是从...到...,所以意为XXX由1到3 + +```SQL +SELECT * FROM EASTWIND WHERE XXX BETWEEN 1 AND 3 +``` + +ISNULL条件 + +判断xxx是否为空 + +```SQL +SELECT * FROM EASTWIND WHERE XXX IS NULL +``` + +IS NOT NULL条件 + +判断xxx是否不为空 + +```SQL +SELECT * FROM EASTWIND WHERE XXX IS NOT NULL +``` + +# MySQL Update更新数据 + +如果需要更新或修改MySql中的数据,我们可以使用`UPDATE`命令来操作 + +语法 + +```SQL +UPDATE TABLE_NAME +SET COLUMN1 = XXX,COLUMN2 = XXX,COLUMN3 = ... +[WHERE CONDITION] +``` + +- TABLE_NAME是需要更新的表的名称 +- column1,column2是你要更新的列的名称 +- value1,value2是新的值 +- WHERE CONDITION是一个可选的子句,用于指定更新的行,如果省略WHERE,将更新表中所有行 + +更新某一列的值 + +```SQL +UPDATE EASTWIND +SET XXX = XXX +WHERE CONDITION = XXX +``` + +更新多个列的值 + +```SQL +UPDATE EASTWIND +SET XXX = XXX,XXX = XXX +WHERE CONDITION = XXX +``` + +使用表达式更新值 + +```SQL +UPDATE EASTWIND +SET XXX = XXX * 2 +WHERE XXX = CONDITION +``` + +# MySQL DELETE语句 + +如果想删除数据表中的记录,可以使用`DELETE FROM`来删除MySQL数据表中的记录 + +语法 + +```SQL +DELETE FROM TABLE_NAME +WHERE CONDITION +``` + +参数说明: + +- table_name是你要删除数据的表的名称 +- WHERE CONDITION是一个可选的子句,用于指定删除的行,如果省略WHERE子句,将删除表中所有行 + +更多说明: + +- 如果没有指定WHERE子句,MySQL表中的所有记录将被删除 +- 你可以在WHERE子句中指定任何条件 +- 你可以在单个表中一次性删除记录 + +当你想删除指定记录时,请使用WHERE子句 + +删除符合条件的行 + +```SQL +DELETE FROM EASTWIND +WHERE XXX = XXX; +``` + +删除所有行 + +```SQL +DELETE FROM TABLE_NAME; +``` + diff --git "a/public/markdowns/MySql\351\253\230\347\272\247.md" "b/public/markdowns/MySql\351\253\230\347\272\247.md" new file mode 100644 index 0000000..ea7554e --- /dev/null +++ "b/public/markdowns/MySql\351\253\230\347\272\247.md" @@ -0,0 +1,102 @@ +--- +title: MySql高级 +abbrlink: af491aa0 +date: 2023-12-18 19:54:02 +tags: +categories: + - 微服务 +description: MySql高级 +--- + +# 存储引擎 + +## MySql体系结构 + +![image-20231215165433759](C:/Users/Administrator/AppData/Roaming/Typora/typora-user-images/image-20231215165433759.png) + +- 连接层 + +最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限 + +- 服务层 + +第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等 + +- 引擎层 + +存储引擎真正的负责了MySql中的数据存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样可以通过自己的需要,选取合适的存储引擎 + +- 存储层 + +主要是将数据存储在文件系统之上,并完成与存储引擎的交互 + +## 存储引擎简介 + +存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型 + +查询建表语句 + +```sql +show create table --tableName-- +``` + +```sql +CREATE TABLE 表名( +字段1 字段1类型 [ COMMENT 字段1注释 ] , +...... +字段n 字段n类型 [COMMENT 字段n注释 ] +) ENGINE = INNODB [ COMMENT 表注释 ] ; +``` + +默认的存储引擎是`INNODB` + +创建表时指定存储引擎,ENGINE = xxx存储引擎 + +如果不知道使用什么存储引擎,可以通过`show engines` + +![image-20231215171626159](C:/Users/Administrator/AppData/Roaming/Typora/typora-user-images/image-20231215171626159.png) + +InnoDB是MySql默认的存储引擎,而MyISAM是MySql早期的存储引擎 + +## 存储引擎特点 + +### InnoDB + +- 介绍 + - InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在MySql5.5之后,InnoDB是默认的MySql存储引擎 + +- 特点 + - DML操作遵循ACID模型,支持事务; + - 行级锁,提高并发访问性能; + - 支持外键FOREIGN KEY约束,保证数据的完整性和正确性; + +- 文件 + + - xxx.ibd:xxx代表的是表名,innoDB引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm、sdi)、数据和索引 + + - 参数:innodb_file_per_table + +- 逻辑存储结构 + + 每一个表都是一个表空间,在表空间中存在着段,在段中存在着区,在区中存在着页,在页中存在着行,每一行就是一个数据 + + ![image-20231215182801802](C:/Users/Administrator/AppData/Roaming/Typora/typora-user-images/image-20231215182801802.png) + +### MyISAM + +- 介绍 + +MyISAM是MySql早期的默认存储引擎 + +- 特点 + - 不支持事务,不支持外键 + - 支持表锁,不支持行锁 + - 访问速度快 +- 文件 + - xxx.sdi:存储表结构信息 + - xxx.MYD:存储数据 + - xxx.MYI:存储索引 + + + +## 存储引擎选择 diff --git "a/public/markdowns/Postman\345\267\245\345\205\267.md" "b/public/markdowns/Postman\345\267\245\345\205\267.md" new file mode 100644 index 0000000..fa2cbd8 --- /dev/null +++ "b/public/markdowns/Postman\345\267\245\345\205\267.md" @@ -0,0 +1,83 @@ +--- +title: Postman工具 +abbrlink: 8cbd8d9c +date: 2023-08-17 08:32:01 +tags: + - Postman + - 实用工具 +categories: + - 实用工具 +description: Postman的常见使用方法 +--- + +由于Postman在2018年后,就不支持浏览器版本,所以需要下载客户端再安装使用 + +下面以Windows为例进行安装 + +# 下载与安装 + +访问postman官方网站,下载最新版本:https://www.postman.com/ + +![image-20230817085128946](https://s2.loli.net/2023/08/17/bxGYAruhDpitvmg.png) + +根据自己电脑的系统来下载 + +![image-20230817085254054](https://s2.loli.net/2023/08/17/vc7gTsW2HaXYRKw.png) + +下载好之后,进行安装,安装就不讲了,跟普通软件的安装方式是一样的,安装完成后,进行一个注册和登录,就可以进到页面里了 + +# 使用方法 + +image-20230817085610314 + +点击创建一个工作空间 + +image-20230817085643819 + +![image-20230817085753069](https://s2.loli.net/2023/08/17/DLQHYWeMcE3Utq2.png) + +创建完成后会来到如下页面 + +点击这个按钮会新建一个连接 + +![image-20230817085905695](https://s2.loli.net/2023/08/17/Pj4eq6tByIkKxFO.png) + +右击这个按钮,添加一个请求 + +image-20230817090053973 + +默认是get请求,你可以根据自己的需求来确定 + +![image-20230817090116652](https://s2.loli.net/2023/08/17/a897CmKxLyqpkJX.png) + + + +# 常见的连接方式 + +讲一下常见的几种连接参数 + +## Get请求 + +一般把地址拷贝到地址栏里就可以了,下面的Response是响应的内容 + +image-20230817090340322 + +## Post请求 + +### 表单类型的接口请求 + +表单类型数据其实就是在请求头中查看Content-Type,它的值如果是:application/x-www-form-urlencoded ,那么就说明客户端提交的数据是以表单形式提交的,这里一般发送的都是表单类的请求测试 + +![image-20230817090617628](https://s2.loli.net/2023/08/17/OXoKswUz6jq3YmI.png) + +### 上传文件的表单请求 + +跟表单数据差不多,其实就是将x-www-form-urlencoded改为了form-data,可以携带的参数由Text变为File,File的Value就可以选择文件了 + +![image-20230817091134556](https://s2.loli.net/2023/08/17/RW1nczqJyA35sk7.png) + +### json类型的接口请求 + +都没啥区别,其实还是跟上面的类似,只需要简单的变动一下参数 + +image-20230817091440542 diff --git "a/public/markdowns/Redis\345\205\245\351\227\250.md" "b/public/markdowns/Redis\345\205\245\351\227\250.md" new file mode 100644 index 0000000..8e3f2a4 --- /dev/null +++ "b/public/markdowns/Redis\345\205\245\351\227\250.md" @@ -0,0 +1,746 @@ +--- +title: Redis入门 +tags: + - Redis + - 瑞吉外卖 +categories: + - Redis +description: Redis的快速入门教程 +abbrlink: 6a343b7f +--- + +# Redis入门 + +- Redis是一个基于内存的`key-value`结构数据库 + - 基于内存存储,读写性能高 + - 适合存储热点数据(热点商品、咨询、新闻) +- 官网:`https://redis.io/` + + + +# Redis的简介 + +- Redis是用C语言开发的一个开源的、高性能的键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。它存储的value类型比较丰富,也被称为结构化NoSql数据库 + +- NoSql(Not Only Sql),不仅仅是SQL,泛指非关系型数据库,NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充 + + 关系型数据库(RDBMS):MySQL、Oracl、DB2、SQLServer + 非关系型数据库(NoSql):Redis、Mongo DB、MemCached + +- Redis应用场景:缓存、消息队列、任务队列、分布式锁 + + + +## 下载与安装 + +- 这里我们在Linux和Windows上都装一下 + - Windows 版:`https://github.com/microsoftarchive/redis/releases` + - Linux 版:`https://download.redis.io/releases/` + +### Windows安装Redis + +- 直接下载对应版本的`.zip`压缩包,直接解压 + + + +### Linux安装Redis + +- Linux系统安装Redis步骤: + + 1. 将Redis安装包上传到Linux + + 2. 解压安装包,改成自己的redis版本 + + ```bash + tar -zxvf redis-4.0.0.tar.gz -C /usr/local/ + ``` + + 3. 安装Redis的依赖环境gcc + + ```bash + # 安装依赖环境 + yum install gcc-c++ + ``` + + 4. 进入`/usr/local/redis根目录`,进行编译 + + ```bash + # 进入到根目录 + cd /usr/local/redis根目录 + + # 编译 + make + ``` + + 5. 进入redis的src目录,进行安装 + + ```bash + # 进入到src目录 + cd /usr/local/redis根目录/src + # 进行安装 + make install + ``` + + + +## 服务启动与停止 + +### Linux启动与停止 + +- 进入到`/src`目录下,执行`redis-server`即可启动服务,默认端口号为`6379` + +```bash +# 进入到根目录 +cd /usr/local/redis/src + +# 执行redis-server +./redis-server +``` + +​ + +### Linux设置后台运行 + +进入到redis根目录下,修改配置redis.conf文件 + +```bash +# 进入到redis根目录下 +cd /usr/local/redis + +# 修改配置文件 +vim redis.conf +``` + +找到`daemonize no`字段,将其修改为`daemonize yes` + +在redis根目录以redis.conf作为配置文件在后台运行 + +```bash +src/redis-server ./redis.conf +``` + + + +### Linux开启密码校验 + +- 还是修改redis.conf配置文件,找到`requirepass`这行,将其注释去掉,并在后面写上自己的密码 +- 然后杀掉原进程再重新启动 + +![image-20230804214017944](https://s2.loli.net/2023/08/14/lgT1NfVWrIxQCjY.png) + +```bash +# 重新启动 +src/redis-server ./redis.conf + +# 登录时同时进行认证(连接的是cli客户端服务,-h是本地服务,-p设置的是端口号,-a是auth也相当于密码) +src/redis-cli -h localhost -p 6379 -a 密码 +``` + +修改完毕之后还是杀进程,然后重启服务 + +![image-20230804214221395](https://s2.loli.net/2023/08/14/wDLpANBq8C2hcJd.png) + + + +### Linux开启远程连接 + +- 还是修改redis.conf配置文件,找到`bind 127.0.0.1`这行,把这行注释掉,这一行是让我们本地进行连接的,注释之后就是开启了远程连接 +- 之后设置防火墙,开启6379端口 +- 杀死进程,并重新启动服务 + +```bash +# 开启6379端口 +firewall-cmd --zone=public --add-port=6379/tcp --permanent + +# 设置立即生效 +firewall-cmd --reload + +# 查看开放的端口 +firewall-cmd --zone=public --list-ports +``` + +最后在Windows的redis根目录下,按住Shift+右键打开PowerShell窗口,连接Linux的Redis + +```bash +.\redis-cli.exe -h 虚拟机的ip地址 -p 6379 -a 密码 +``` + + + +# Redis数据类型 + +## 介绍 + +Redis存储的是key-value结构的数据,其中key是字符串类型,value有五种常用的数据类型 + +- 字符串 string(普通字符串,常用) +- 哈希 hash(hash适合存储对象) +- 列表 list(list按照插入顺序排序,可以有重复元素) +- 集合 set(无序集合,没有重复元素) +- 有序集合 sorted set(有序集合,没有重复元素) + + + +## Redis常用命令 + +### 字符串string操作命令 + +| 命令 | 描述 | +| :---------------------: | :---------------------------------------------: | +| SET key value | 设置指定key的值 | +| GET key | 获取指定key的值 | +| SETEX key seconds value | 设置指定key的值,并将key的过期时间设为seconds秒 | +| SETNX key value | 只有在key不存在时设置key的值 | + +#### 操作命令示例: + +```bash +127.0.0.1:6379> set name 666 +OK +127.0.0.1:6379> get name +"666" +127.0.0.1:6379> setex name2 3 888 +OK +127.0.0.1:6379> get name2 +(nil) +127.0.0.1:6379> setnx name2 888 +(integer) 1 +127.0.0.1:6379> setnx name2 555 +(integer) 0 +127.0.0.1:6379> keys * +1) "name2" +2) "name" +127.0.0.1:6379> get name2 +"888" +``` + +### 哈希hash操作命令 + +`Redis Hash`是一个`String`类型的`Field`和`Value`的映射表,`Hash`特别适合用于存储对象 + +| 命令 | 描述 | +| :------------------: | :------------------------------------: | +| HSET key field value | 将哈希表key 中的字段field的值设为value | +| HGET key field | 获取存储在哈希表中指定字段的值 | +| HDEL key field | 删除存储在哈希表中的指定字段 | +| HKEYS key | 获取哈希表中所有字段 | +| HVALS key | 获取哈希表中所有值 | +| HGETALL key | 获取在哈希表中指定key的所有字段和值 | + +#### 操作命令示例 + +```bash +127.0.0.1:6379> hset table1 name wangwu // 设置table1表中的name字段值为wangwu +(integer) 1 +127.0.0.1:6379> hget table1 name // 获取存储在table1表中的name字段 +"wangwu" +127.0.0.1:6379> hdel table1 name // 删除存储在table1表中的name字段 +(integer) 1 +127.0.0.1:6379> hget table1 name // 此时已经被删除了,得到的是nil +(nil) +127.0.0.1:6379> hset table1 name lisi // 设置table1表中的name字段值为lisi +(integer) 1 +127.0.0.1:6379> hset table1 age 10 // 设置table1表中的age字段值为10 +(integer) 1 +127.0.0.1:6379> hkeys table1 // 获取哈希表中所有字段 +1) "name" +2) "age" +127.0.0.1:6379> hvals table1 // 获取哈希表中所有值 +1) "wangwu" +2) "10" +127.0.0.1:6379> hgetall table1 // 获取在哈希表中指定key的所有字段和值 +1) "name" +2) "wangwu" +3) "age" +4) "10" +``` + + + +### 列表list操作命令 + +`Redis List`是简单的字符串列表,按照插入顺序排序 + +| 命令 | 描述 | +| :-------------------------: | :----------------------------------------------------------: | +| `LPUSH` key value1 [value2] | 将一个或多个值插入到列表头部 | +| `LRANGE` key start stop | 获取列表指定范围内的元素 | +| `RPOP` key | 移除并获取列表最后一个元素 | +| `LLEN` key | 获取列表长度 | +| `BRPOP` key1 [key2] timeout | 移出并获取列表的最后一个元素 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | + +#### 操作命令示例 + +```bash +127.0.0.1:6379> lpush list a b c // 将a、b、c插入到列表头部 +(integer) 3 +127.0.0.1:6379> lrange list 0 -1 // 查询从0到-1位置的元素,-1表示最后一个值 +1) "c" // c在最前面是因为列表是按照插入顺序排序的,最后插入的在最前面 +2) "b" +3) "a" +127.0.0.1:6379> rpop list // 删除最后一个元素,并得到值,a是最先插入的,所以是最后一个 +"a" +127.0.0.1:6379> llen list // 获取长度 +(integer) 2 +127.0.0.1:6379> brpop list 3 // 移除列表的最后一个元素,如果列表没有值,会阻塞timeout秒,否则删除 +1) "list" +2) "b" +127.0.0.1:6379> brpop list 3 +1) "list" +2) "c" +127.0.0.1:6379> brpop list 3 // 阻塞3秒 +(nil) +(3.09s) +``` + +### 集合set操作命令 + +`Redis set`是`String`类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据 +概念和数学中的集合概念基本一致 + +| 命令 | 描述 | +| :------------------------: | :----------------------: | +| SADD key member1 [member2] | 向集合添加一个或多个成员 | +| SMEMBERS key | 返回集合中的所有成员 | +| SCARD key | 获取集合的成员数 | +| SINTER key1 [key2] | 返回给定所有集合的交集 | +| SUNION key1 [key2] | 返回所有给定集合的并集 | +| SDIFF key1 [key2] | 返回给定所有集合的差集 | +| SREM key member1 [member2] | 移除集合中一个或多个成员 | + +#### 操作命令示例 + +```bash +127.0.0.1:6379> sadd set a a b b c d // 向set集合添加a、a、b、b、c、d +(integer) 4 +127.0.0.1:6379> smembers set // 由于集合的唯一性,所以,只有a、b、c、d +1) "d" +2) "c" +3) "b" +4) "a" +127.0.0.1:6379> scard set // 获取长度 +(integer) 4 +127.0.0.1:6379> srem set a b c // 删除集合中指定的对象 +(integer) 3 +127.0.0.1:6379> smembers set +1) "d" +``` + +### 有序集合sorted set常用命令 + +`Redis Sorted Set`有序集合是`String`类型元素的集合,且不允许重复的成员。每个元素都会关联一个`double`类型的分数(`score`) 。`Redis`正是通过分数来为集合中的成员进行从小到大排序。有序集合的成员是唯一的,但分数却可以重复。 + +| 命令 | 描述 | +| :--------------------------------------: | :----------------------------------------------------: | +| ZADD key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | +| ZRANGE key start stop [WITHSCORES] | 通过索引区间返回有序集合中指定区间内的成员 | +| ZINCRBY key increment member | 有序集合中对指定成员的分数加上增量increment | +| ZREM key member [member …] | 移除有序集合中的一个或多个成员 | + +#### 操作命令示例 + +```bash +127.0.0.1:6379> zadd sortset 3 a 1 c 2 b // 向集合sortset添加值与分数 +(integer) 3 +127.0.0.1:6379> zrange sortset 0 -1 // 查询,按照分数从小到大排序,最小的在最前面 +1) "c" +2) "b" +3) "a" +127.0.0.1:6379> zincrby sortset 3 c // 为字段c加3分 +"4" +127.0.0.1:6379> zrange sortset 0 -1 // 查询,此时c>a>b在最下面 +1) "b" +2) "a" +3) "c" +127.0.0.1:6379> zrem sortset a b c // 删除有序集合元素 +(integer) 3 +127.0.0.1:6379> zrange sortset 0 -1 +(empty list or set) +127.0.0.1:6379> +``` + +### 通用命令 + +针对key来操作 + +| 命令 | 描述 | +| :----------: | :------------------------------------------------------: | +| KEYs pattern | 查找所有符合给定模式(pattern)的key | +| EXISTs key | 检查给定key是否存在 | +| TYPE key | 返回key所储存的值的类型 | +| TTL key | 返回给定key的剩余生存时间(TTL, time to live),以秒为单位 | +| DEL key | 该命令用于在key存在是删除key | + +```bash +127.0.0.1:6379> keys * // 查看所有的key +1) "name" +2) "table2" +3) "table1" +4) "NewName" +5) "set" +127.0.0.1:6379> exists name // 查看某个key是否存在,存在返回1,不存在返回0 +(integer) 1 +127.0.0.1:6379> exists abc +(integer) 0 +127.0.0.1:6379> type name // 查看key的类型 +string +127.0.0.1:6379> type set +set +127.0.0.1:6379> type table1 +hash +127.0.0.1:6379> ttl name // 查看key的剩余存活时间,-1表示永久存活 +(integer) -1 +127.0.0.1:6379> setex test 10 test // 设置test字段存活时间10s,值为test +OK +127.0.0.1:6379> ttl test // 查看test +(integer) 8 +127.0.0.1:6379> ttl test +(integer) 2 +127.0.0.1:6379> keys * // 查看所有key +1) "name" +2) "table2" +3) "table1" +4) "NewName" +5) "set" +127.0.0.1:6379> del name // 删除name字段 +(integer) 1 +127.0.0.1:6379> keys * // 查看所有key,此时name字段就不存在了 +1) "table2" +2) "table1" +3) "NewName" +4) "set" +``` + + + +# 在Java中使用Redis + +## 简介 + +- Redis的Java客户端有很多,官方推荐的有三种 + - `Jedis` + - `Lettuce` + - `Redisson` +- Spring对Redis客户端进行了整合,提供了SpringDataRedis,在Spring Boot项目中还提供了对应的Starter,即`spring-boot-starter-data-redis` + + + +## Jedis + +- 使用Jedis的步骤 + 1. 获取连接 + 2. 执行操作 + 3. 关闭连接 +- 在此之前我们需要导入一下Jedis的maven坐标 + +```xml + + redis.clients + jedis + 2.8.0 + +``` + +编写测试类(测试之前记得打开Redis的服务) + +```java +@SpringBootTest +class RedisTestApplicationTests { + + @Test + void contextLoads() { + //1. 获取连接 + Jedis jedis = new Jedis("localhost", 6379); + //2. 执行具体操作 + jedis.set("name", "zhangsan"); + + jedis.hset("stu", "name", "Jerry"); + jedis.hset("stu", "age", "18"); + jedis.hset("stu", "num", "4204000400"); + + Map map = jedis.hgetAll("stu"); + Set keySet = map.keySet(); + for (String key : keySet) { + String value = map.get(key); + System.out.println(key + ":" + value); + } + String name = jedis.get("name"); + System.out.println(name); + //3. 关闭连接 + jedis.close(); + } + +} +``` + +Jedis我们了解一下即可,大多数情况下我们还是用SpringDataRedis的 + + + +## Spring Data Redis + +SpringBoot项目中,可以使用SpringDataRedis来简化Redis(常用) + +Spring Data Redis中提供了一个高度封装的类:RedisTemplate,针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下: + +- ValueOperations:简单K-V操作 +- SetOperations:set类型数据操作 +- ZSetOperations:zset类型数据操作 +- HashOperations:针对map类型的数据操作 +- ListOperations:针对list类型的数据操作 + +使用SpringDataRedis,我们首先需要导入它的maven坐标 + +```xml + + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +之后重新设置一下序列化器,防止出现乱码,在config包下创建`RedisConfig`配置类 + +键和值的序列化器都需要统一,不能单一的只统一一个,否则会乱码 + +```java +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig extends CachingConfigurerSupport { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + + RedisTemplate redisTemplate = new RedisTemplate<>(); + + //默认的Key序列化器为:JdkSerializationRedisSerializer + // 设置键的序列化器统一 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + // 设置值的序列化器统一 + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(connectionFactory); + + return redisTemplate; + } +} +``` + +随后配置一下连接redis的相关配置 + +```yaml +spring: + redis: + host: localhost + port: 6379 + #password: 123456 + database: 0 #操作的是0号数据库 + jedis: + #Redis连接池配置 + pool: + max-active: 8 #最大连接数 + max-wait: 1ms #连接池最大阻塞等待时间 + max-idle: 4 #连接池中的最大空闲连接 + min-idle: 0 #连接池中的最小空闲连接 +``` + +**可以通过命令来改变自己操作的数据库,默认是0号** + +在安装Redis的目录下有一个文件:`redis.windows.conf` + +打开它,可以修改数据库的数量 + +![image-20230805141353134](https://s2.loli.net/2023/08/14/KSWzXx7TV3jcBPn.png) + +### string操作 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + +@SpringBootTest +class Demo1ApplicationTests { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + void test1() { + // 设置string + redisTemplate.opsForValue().set("name","zhangsan"); + + // 获得string + String name = (String) redisTemplate.opsForValue().get("name"); + System.out.println(name); + + // 设置string的超时时间(timeout是一个long型变量,TimeUnit是一个工具类,可以设置秒分时这种) + redisTemplate.opsForValue().set("key1","value1",10L, TimeUnit.SECONDS); + + // 当某个key不存在,才执行设置操作,否则不执行 + // 返回boolean类型的值,如果是true,说明执行成功,否则失败 + Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("key1", "value2"); + System.out.println(aBoolean); + + } +} +``` + +### hash操作 + +```java +@Test +void hashTest() { + HashOperations hashOperations = redisTemplate.opsForHash(); + // put(哪个hash表,什么字段,什么值) + hashOperations.put("4204000400", "name", "Hades"); + hashOperations.put("4204000400", "age", "18"); + hashOperations.put("4204000400", "hobby", "Apex"); + //获取map集合 + Map map = hashOperations.entries("4204000400"); + Set keySet = map.keySet(); + // 获取方式和Java的hash操作一致 + for (String hashKey : keySet) { + System.out.println(hashKey + ":" + map.get(hashKey)); + } + System.out.println("----------------"); + //只获取keys + Set keys = hashOperations.keys("4204000400"); + for (String key : keys) { + System.out.println(key); + } + System.out.println("----------------"); + //只获取values + List values = hashOperations.values("4204000400"); + for (String value : values) { + System.out.println(value); + } +} +``` + +### list操作 + +```java +@Test +void listTest() { + ListOperations listOperations = redisTemplate.opsForList(); + //存数据 + // 为testData列表添加A + listOperations.leftPush("testData", "A"); + // 多值添加 + listOperations.leftPushAll("testData", "B", "C", "D"); + // 遵循先进后出原则,所以A是在最下面依次往上 + List testDatas = listOperations.range("testData", 0, -1); + //遍历 + for (String tableData : testDatas) { + System.out.print(tableData + " "); + } + System.out.println(); + //获取当前list长度,用于遍历 + Long size = listOperations.size("testData"); + int value = size.intValue(); + //遍历输出并删除 + for (int i = 0; i < value; i++) { + System.out.print(listOperations.leftPop("testData") + " "); + } + //最后输出一下当前list长度 + System.out.println(); + System.out.println(listOperations.size("testData")); +} +``` + +### set操作 + +```java +@Test +void setTest() { + SetOperations setOperations = redisTemplate.opsForSet(); + //存数据,这里存了两个a + setOperations.add("tmp", "a", "b", "c", "d", "a"); + //遍历输出(跟Java差不多) + Set tmpData = setOperations.members("tmp"); + for (String value : tmpData) { + System.out.print(value + " "); + } + System.out.println(); + System.out.println("---------------"); + //删除多值 + setOperations.remove("tmp", "b", "c"); + //再次遍历输出 + tmpData = setOperations.members("tmp"); + for (String value : tmpData) { + System.out.print(value + " "); + } +} +``` + +### ZSet数据操作 + +```java +@Test +void zsetTest() { + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + //存scope值 + zSetOperations.add("myZset", "a", 0.0); + zSetOperations.add("myZset", "b", 1.0); + zSetOperations.add("myZset", "c", 2.0); + zSetOperations.add("myZset", "a", 3.0); + //遍历所有 + Set myZset = zSetOperations.range("myZset", 0, -1); + for (String s : myZset) { + System.out.println(s); + } + //修改scope + zSetOperations.incrementScore("myZset", "b", 4.0); + //取值 + System.out.println("--------------------"); + myZset = zSetOperations.range("myZset", 0, -1); + for (String s : myZset) { + System.out.println(s); + } + //删除成员 + zSetOperations.remove("myZset", "a", "b"); + //取值 + System.out.println("-------------------"); + myZset = zSetOperations.range("myZset", 0, -1); + for (String s : myZset) { + System.out.println(s); + } +} +``` + +### 通用操作 + +```java +@Test +void commonTest() { + //查看所有key(keys *) + Set keys = redisTemplate.keys("*"); + for (String key : keys) { + System.out.println(key); + } + //查看是否存在指定key + System.out.println("----------------------------"); + System.out.println(redisTemplate.hasKey("Random")); + System.out.println("----------------------------"); + //删除指定key,并再次查看 + redisTemplate.delete("myZset"); + keys = redisTemplate.keys("*"); + for (String key : keys) { + System.out.println(key); + } + System.out.println("----------------------------"); + //输出指定key的类型 + System.out.println(redisTemplate.type("tmp")); +} +``` diff --git "a/public/markdowns/Redis\345\237\272\347\241\200\347\257\207.md" "b/public/markdowns/Redis\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..22913b0 --- /dev/null +++ "b/public/markdowns/Redis\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,1121 @@ +--- +title: Redis基础篇 +tags: + - Redis +categories: + - Redis +description: Redis基础篇 +abbrlink: 4604f75b +--- +# 初识Redis + +## 认识NoSQL + +![image-20230826191849828](https://s2.loli.net/2023/08/26/z1VS4BrAotuqgpQ.png) + +SQL的全称是结构化查询语言(Structured Query Language),是一种用于关系型数据库的查询语言 + +NOSQL的全称是Not Only SQL,不限于SQL,也可以说是非关系型数据库,说明它于传统的SQL有所不同,而Redis正是一种NOSQL,接下来看一下两者的不同 + +| | SQL | NOSQL | +| :------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| 数据结构 | 结构化(Structured) | 非结构化 | +| 数据关联 | 关联的(Relational) | 无关联的 | +| 查询方式 | SQL查询 | 非SQL | +| 事务特性 | ACID | BASE | +| 存储方式 | 磁盘 | 内存 | +| 拓展性 | 垂直 | 水平 | +| 使用场景 | 1、数据结构固定
2、相关业务对数据安全性、一致性要求较高 | 1、数据结构不固定
2、对一致性、安全性要求不高
3、对性能要求较高 | + +NOSQL的非结构化类型主要有: + +- 键值类型(Redis) +- 文档类型(MongoDB) +- 列类型(Hbase) +- Graph类型(Neo4j) + +NOSQL没有统一的SQL语句 + +## 认识Redis + +Redis诞生于2009年,全称是Remote Dictionary Server,远程词典服务器,是一个基于内存的键值型NoSQL数据库。 + +特征: + +- 键值(key-value)型,value支持多种不同数据结构,功能丰富 +- 单线程,每个命令具备原子性 +- 低延迟,速度快(基于内存、IO多路复用、良好的编码) +- 支持数据持久化 +- 支持主从集群、分片集群 +- 支持多语言客户端 + +## 安装Redis + +这里简单讲述一下Linux中Redis的安装过程 + +Redis的官方网站:https://redis.io/ + +因为Redis是基于C语言编写的,因此需要安装Redis所需要的gcc依赖 + +```bash +yum install -y gcc tcl +``` + +在Redis官方网站下载好安装包,并上传到Linux上,目录看自己喜好,上传完成后进行解压 + +```bash +tar -zxvf redis的文件名 +``` + +进入redis的安装目录 + +```bash +cd 解压完成后的redis目录 +``` + +运行编译命令 + +make是编译,make install是安装 + +`make & make install`是编译并安装 + +```bash +make & make install +``` + +默认的安装路径就是在`/usr/local/bin`目录下 + +该目录以及默认配置到环境变量,因此可以在任意目录下运行这些命令 + +- redis-cli:redis提供的命令行客户端 +- redis-server:redis提供的服务端启动脚本 +- redis-sentinel:是redis的哨兵启动脚本 + +### 默认启动 + +```bash +redis-server +``` + +启动后,你会发现你无法连接客户端,因为服务端已经建立连接了,如果你想连接客户端,你得重新打开一个页面建立客户端的连接,我们发现这样很麻烦,所以我们需要让它后台启动 + + + +### 指定配置启动 + +在我们之前解压的redis安装包下,有一个redis.conf的配置文件,我们先将这个文件备份一份 + +```bash +cp redis.conf redis.conf.bck +``` + +然后对redis.conf进行修改 + +```bash +vi redis.conf +``` + +#### Redis的常用配置 + +```properties +# 监听的地址,默认是127.0.0.1,会导致只能在本地访问,修改为0.0.0.0可以在任意IP进行访问 +bind 0.0.0.0 +# 守护进程,修改为yes后即可在后台运行 +daemonize yes +# 密码,设置后访问Redis必须输入密码 +requirepass 123456 +``` + +#### Redis的其他配置 + +```properties +# 监听的端口 +port 6379 +# 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志、持久化等文件会保存在这个目录 +dir . +# 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号为0~15 +databases 1 +# 设置redis能够使用的最大内存 +maxmemory 512mb +# 日志文件,默认为空,不记录日志,可以指定日志文件名 +logfile "redis.log" +``` + +指定配置文件启动,如果在redis-server命令所在的文件夹下启动的,则不需要指定,默认为当前目录下的redis.conf配置文件,如需指定,可使用下面的代码 + +```bash +redis-server redis.conf +``` + +查看redis进程 + +```bash +ps -ef | grep redis +``` + +停止redis进程 + +```bash +kill -9 redis对应的进程号 +``` + + + +#### 默认自启动 + + + +## Redis客户端 + +### Redis命令行客户端 + +Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下 + +```bash +redis-cli [options] [commonds] +``` + +options是可选项,常见options有: + +- `-h 127.0.0.1`:指定要连接的redis节点的IP地址,默认是127.0.0.1 +- `-p 6379`:指定要连接的redis节点的端口,默认是6379 +- -a 123456:指定redis的访问密码 + +commonds是Redis的操作命令,例如: + +`ping`:与redis服务做连通测试,服务端正常会返回`pong` + +不指定commonds时,会进入`redis-cli`的交互控制台 + + + +### Redis图形化客户端 + +Redis图形化客户端是一个GitHub上的大神出的,所以需要在GitHub上下载 + +地址如下:https://github.com/MicrosoftArchive/redis/releases + +这里就不讲关于Redis图形化客户端的安装了,都很简单,我们讲讲如何使用 + +image-20230827093318995 + +打开后的页面如上图所示,我们单击`连接到Redis服务器` + +image-20230827093621171 + +image-20230827093720046 + +连接成功后一路确定即可 + +image-20230827093906710 + +添加键后的效果如下 + +image-20230827093944301 + + + +# Redis常见命令 + +## Redis数据结构介绍 + +Redis是一个key-value的数据库,key一般是String类型,value的类型多样 + +image-20230827094756629 + +学习Redis可以多看看Redis的官方文档:https://redis.io/commands/ + +甚至你可以在Redis的命令行中使用help来查看帮助 + +```bash +help +``` + + + +## Redis的通用命令 + +### KEYS + +一般是用来查找满足条件的key + +```bash +KEYS pattern +``` + +示例: + +```bash +KEYS * +``` + +**查询所有的key** + +![image-20230827095911637](https://s2.loli.net/2023/08/27/tQ25iOsqBRvH8rn.png) + +一般都是根据pattern通配符来查找专门的key + +**不建议在生产环境使用,因为Redis是单线程的,模糊查询很慢,如果数据量较大,一查可能会出现问题** + + + +### DEL + +删除一个指定的key + +废话不多说,直接上示例 + +**删除单个key** + +![image-20230827100250004](https://s2.loli.net/2023/08/27/VbUXdKpfEB2uSj8.png) + +**删除多个key,中间用空格隔开** + +![image-20230827100415966](https://s2.loli.net/2023/08/27/cu4tLTFKMIsVJlU.png) + + + +### EXISTS + +判断key是否存在 + +EXISTS后面可以传递多个key,返回的是key存在的数量 + +![image-20230827100944335](https://s2.loli.net/2023/08/27/Z9ErATS2UOwvhqn.png) + + + +### EXPIRE + +为key设置有效期,到期后key会被自动删除,设置的有效期一般为秒 + +image-20230827101505382 + +### TTL + +查看key的有效时间,一般和EXPIRE联用 + +```bash +TTL key +``` + +使用TTL查看一个key的默认有效时间 + +![image-20230827101635387](https://s2.loli.net/2023/08/27/BQrjkHKyEOpD3Cx.png) + +-1代表永久有效 + + + +## String类型 + +String类型,也就是字符串类型,是Redis中最简单的存储类型 + +其value是字符串,不过根据字符串的格式不同,又可以分为3类: + +- string:普通字符串 +- int:整数类型 +- float:浮点类型,可以做自增、自减操作 + +不管哪种格式,底层都是字节数组形式存储,只不过编码方式不同。字符串类型的最大空间不能超过512m + +| Key | Value | +| :---: | :---------: | +| msg | hello world | +| num | 10 | +| score | 66.6 | + +### String类型的常见命令 + +#### SET、GET + +set:添加或者修改已经存在的一个String类型的键值对 + +get:根据key获取String类型的键值对 + +![image-20230827131504197](https://s2.loli.net/2023/08/27/EKuqxZXl5cBWbmR.png) + + + +#### MSET、MGET + +mset:批量添加多个String类型的键值对 + +mget:根据多个key获取多个String类型的value + +![image-20230827131727462](https://s2.loli.net/2023/08/27/rEqiWePjmzKaG85.png) + + + +#### INCR、INCRBY、INCRBYFLOAT + +incr:让一个整型的key自增1 + +incrby:让一个整型的key自增,并指定步长,例如:incrby num 2,让num值自增2 + +incrbyfloat:让一个浮点类型的数字自增并指定步长 + +image-20230827132414507 + +#### SETNX、SETEX + +setnx:添加一个String类型的键值对,前提是这个key不存在,否则不执行 + +setex:添加一个String类型的键值对,并且指定有效期 + +image-20230827132614979 + + + +## Key的层级格式 + +Redis没有类似MySQL中的Table的概念,我们该如何区分不同类型的key呢? + +Redis的key允许有多个单词形成层级结构,多个单词之间用`:`隔开,格式如下: + +```txt +项目名:业务名:类型:id +``` + +这个格式并非固定,也可以根据自己的需求来删除和添加词条 + +例如我们的项目叫eastwind,有user和product两种不同类型的数据,我们可以这样定义key + +- user相关的key:eastwind:user:1 +- product相关的key:eastwind:product:1 + + + +如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储: + +| KEY | VALUE | +| -------------------- | ------------------------------------------- | +| eastwind:user:1 | {"id":1,"name":"Jack","age":21} | +| eastwind:product:1 | {"id":1,"name":"小米x66","price":4999} | + +测试一下层级 + +![image-20230827135124187](https://s2.loli.net/2023/08/27/fmQskoAgWD9KHrN.png) + +添加以下代码后,来到redis的桌面端查看 + +![image-20230827135158442](https://s2.loli.net/2023/08/27/mFqXskBpoAZlb8i.png) + +此时这里就有了层级关系 + + + +## Hash类型 + +Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构 + +String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便 + +image-20230827142321008 + +Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD: + +image-20230827142353944 + +### Hash类型的常见命令 + +#### HSET、HGET + +hset:添加或修改hash类型key的field的值 + +``` +hset hash名 字段名 值 +``` + +hget:获取一个hash类型key的field的值 + +``` +hget hash名 字段名 +``` + +![image-20230827142618982](https://s2.loli.net/2023/08/27/1BlDsJ4h5q7AYIa.png) + + + +#### HMSET、HMGET + +hmset:添加多个键值对 + +hmget:获取多个键值对 + +![image-20230827143330348](https://s2.loli.net/2023/08/27/orPqGvnkEThxfbm.png) + + + +#### HGETALL、HKEYS、HVALS + +hgetall:获取一个hash类型中的所有字段和值 + +hkeys:获取一个hash类型中所有的字段 + +hvals:获取一个hash类型中所有的值 + +image-20230827143953427 + +#### HINCRBY + +hincrby:让一个hash类型的字段值自增并指定步长 + +![image-20230827144034484](https://s2.loli.net/2023/08/27/YRDoqEfh3pH72LB.png) + +#### HSETNX + +hsetnx:添加一个hash类型的字段值,前提是该字段不存在,否则不执行 + +![image-20230827144125389](https://s2.loli.net/2023/08/27/NE78mlIGWQUxPza.png) + + + +## List类型 + +Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。 + +特征也与LinkedList类似: + +- 有序 +- 元素可以重复 +- 插入和删除快 +- 查询速度一般 + +常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。 + + + +关于左侧和右侧插入的情况,举出一个例子 + +``` +我们想插入值C,如果在左侧插入,那么就在A的前面,如果在右侧插入,就在B的后面 +A-B +C-A-B +A-B-C +``` + + + +### List类型的常见命令 + +#### LPUSH + +向列表左侧插入一个或多个元素 + +![image-20230827144933737](https://s2.loli.net/2023/08/27/yhTBz5AoNZSgJ8L.png) + +根据上面的例子,我们会依次插入name、123、jsda,都是左侧插入,所以插入顺序如下 + +``` +jsda-123-name +``` + +![image-20230827144944032](https://s2.loli.net/2023/08/27/v9uDgO6rTsVhCcP.png) + +#### LPOP + +lpop:移除并返回列表左侧的第一个元素,没有则返回nil + +![image-20230827145219579](https://s2.loli.net/2023/08/27/yoNhUH8c27gISum.png) + +#### RPUSH + +向列表右侧插入一个或多个元素 + +![image-20230827145457900](https://s2.loli.net/2023/08/27/3caqeusUOSob2h9.png) + +右侧插入也是同理,我们这次只插入了一个age + +``` +123-name-age +``` + +![image-20230827145528649](https://s2.loli.net/2023/08/27/vlhzHmoicn7pLBA.png) + +#### LRANGE + +xxxxxxxxxx @Testvoid commonTest() {    //查看所有key(keys *)    Set keys = redisTemplate.keys("*");    for (String key : keys) {        System.out.println(key);   }    //查看是否存在指定key    System.out.println("----------------------------");    System.out.println(redisTemplate.hasKey("Random"));    System.out.println("----------------------------");    //删除指定key,并再次查看    redisTemplate.delete("myZset");    keys = redisTemplate.keys("*");    for (String key : keys) {        System.out.println(key);   }    System.out.println("----------------------------");    //输出指定key的类型    System.out.println(redisTemplate.type("tmp"));}java + +#### BLPOP、BRPOP + +与LPOP和RPOP类似,唯一不同是会在没有元素时等待指定时间,而不是直接返回nil + +## Set + +Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征: + +- 无序 +- 元素不可重复 +- 查找快 +- 支持交集、并集、差集等功能 + +### Set类型常见命令 + +#### SADD、SREM、SCARD + +sadd:向set中添加一个或多个元素 + +**不会重复** + +image-20230827151033266 + +srem:移除set中的指定元素 + +![image-20230827151130254](https://s2.loli.net/2023/08/27/YcHpdsnK5ECuMx2.png) + +![image-20230827151139268](https://s2.loli.net/2023/08/27/v1j23dDscpzJFQr.png) + +scard:返回set中元素的个数 + +![image-20230827151155115](https://s2.loli.net/2023/08/27/PORxqeutNZbL5VQ.png) + +#### SISMEMBER、SMEMBERS + +sismember:判断一个元素是否存在于set中 + +smembers:获取set中的所有元素 + +![image-20230827151430246](https://s2.loli.net/2023/08/27/HDUvFaWLM6rm4Ez.png) + +#### SINTER、SUNION、SDIFF + +sinter:求两个集合的交集 + +sunion:求两个集合的并集 + +sdiff:求两个集合的差集(补集) + +## SortedSet + +Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。 + +SortedSet具备下列特性: + +- 可排序 +- 元素不重复 +- 查询速度快 + +因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。 + +### SortedSet的常见命令 + +#### ZADD、ZREM + +zadd:添加一个或多个元素到sorted set,如果已经存在则更新其score值 + +zrem:删除sorted set中的一个指定元素 + +![image-20230827152759717](https://s2.loli.net/2023/08/27/cWQL2aUnB4wYJXt.png) + +image-20230827152812548 + +![image-20230827152844811](https://s2.loli.net/2023/08/27/fH2Y4uxJzeCNy1M.png) + +image-20230827152854442 + +#### ZSCORE、ZRANK + +zscore:获取sorted set中指定元素的score值 + +zrank:获取sorted set中的指定元素的排名,一般分数越高,排名越靠后 + +![image-20230827153031622](https://s2.loli.net/2023/08/27/AIfCSXVT3OUrDje.png) + +#### ZCARD、ZCOUNT + +zcard:统计元素个数 + +zcount:统计score值在给定范围内的所有元素的个数 + +![image-20230827153415587](https://s2.loli.net/2023/08/27/mR71UZgJlNoQTIb.png) + +#### ZINCRBY + +让sorted set中的指定元素自增,步长为指定的值,添加的是score + +![image-20230827153606740](https://s2.loli.net/2023/08/27/4uMvHGZSz9fAm53.png) + +#### ZRANGE + +按照score排序后,获取指定范围内的元素,这里的范围代指索引,也可以称为排名 + +![image-20230827153718727](https://s2.loli.net/2023/08/27/xn3Ee6ATyWlq5tV.png) + +#### ZRANGEBYSCORE + +按照score排序后,获取score范围内的元素,根据score来获取值 + +![image-20230827153927534](https://s2.loli.net/2023/08/27/ObIiZut8kcgUxRq.png) + +#### ZDIFF、ZINTER、ZUNION + +求差集、交集、并集,跟set一致 + + + +注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如: + +- `升序`获取sorted set 中的指定元素的排名:ZRANK key member +- `降序`获取sorted set 中的指定元素的排名:ZREVRANK key memeber + + + +# Redis的Java客户端 + +## Jedis + +### 快速入门 + +新建一个maven工程 + +引入依赖 + +```xml + + + redis.clients + jedis + 3.7.0 + + + + org.junit.jupiter + junit-jupiter + 5.7.0 + test + +``` + +在test下新建Java类 + +建立连接 + +```java +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import redis.clients.jedis.Jedis; + +public class JedisTest { + private Jedis jedis; + + // @BeforeEach在测试类方法执行前所执行的方法 + @BeforeEach + void setup(){ + // 1、建立连接 + jedis = new Jedis("127.0.0.1",6379); + // 2、设置密码 + jedis.auth("123456"); + // 3、选择库 + jedis.select(0); + } + + @AfterEach + void tearDown() { + // 关闭连接 + if (jedis != null) + jedis.close(); + } + + @Test + void test1(){ + // 存储数据 + String result = jedis.set("name", "zhangsan"); + System.out.println("result = " + result); + // 获取数据 + String s = jedis.get("name"); + System.out.println("newName = " + s); + } +} +``` + +结果如下 + +报错是正常的,这个不用管 + +![image-20230827175632015](https://s2.loli.net/2023/08/27/QfJaHh19KVqgoWD.png) + +接着测试一下其他的写法 + +Hash + +```java +@Test +void test2(){ + jedis.hset("user:1","name","zhangsan"); + jedis.hset("user:1","age","10"); + jedis.hset("user:1","score","99.9"); + Map hgetAll = jedis.hgetAll("user:1"); + System.out.println(hgetAll); +} +``` + +![image-20230827191620229](https://s2.loli.net/2023/08/27/BsVNfn7dovMywLC.png) + +其他的也是类似的,只要知道命令就会写了 + + + +### Jedis连接池 + +Jedis本身是线程不安全的,并且频繁的创建和销毁连接会造成有性能损耗,因此我们推荐Jedis连接池代替Jedis的直连方式 + +```java +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +public class JedisConnectionFactory { + private static final JedisPool jedisPool; + + static { + // 配置连接池 + JedisPoolConfig poolConfig = new JedisPoolConfig(); + // 最大连接数 + poolConfig.setMaxTotal(8); + // 设置最大空闲时间,预备8个池子,即使没人访问,依然有8个池子 + poolConfig.setMaxIdle(8); + // 最少可以为0,即使没人用,也可以释放掉 + poolConfig.setMinIdle(0); + // 设置等待时间为1秒,池子满了无法连接一秒后就报错 + poolConfig.setMaxWaitMillis(1000); + // 创建连接池对象 + jedisPool = new JedisPool(poolConfig,"127.0.0.1",6379,1000,"123456"); + } + + public static Jedis getJedis(){ + return jedisPool.getResource(); + } +} +``` + +并修改JedisTest中的连接方法 + +```java +// @BeforeEach在测试类方法执行前所执行的方法 +@BeforeEach +void setup(){ + // 1、建立连接 + jedis = JedisConnectionFactory.getJedis(); + // 2、设置密码 + jedis.auth("123456"); + // 3、选择库 + jedis.select(0); +} +``` + +再次测试 + +![image-20230827193556729](https://s2.loli.net/2023/08/27/VCX4Sn1axfOiLwh.png) + + + +## SpringDataRedis + +SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis + +官网地址:https://spring.io/projects/spring-data-redis + +- 提供了对不同Redis客户端的整合(Lettuce和Jedis) +- 提供了RedisTemplate统一API来操作Redis +- 支持Redis的发布订阅模型 +- 支持Redis哨兵和Redis集群 +- 支持基于Lettuce的响应式编程 +- 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化 +- 支持基于Redis的JDKCollection实现 + + + +SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中: + +![image-20230827194234532](https://s2.loli.net/2023/08/27/AC9oBh7J41cR5OX.png) + + + +### 快速入门 + +创建一个Spring Boot项目 + +引入依赖 + +```xml + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.15 + + + fun.eastwind + RedisDemo2 + 0.0.1-SNAPSHOT + RedisDemo2 + RedisDemo2 + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + +``` + +配置application.yaml文件 + +```yaml +spring: + redis: + host: 127.0.0.1 + port: 6379 + password: 123456 + lettuce: + pool: + # 最大连接数 + max-active: 8 + # 空闲时最大可连接数 + max-idle: 8 + # 空闲时最少可连接数 + min-idle: 0 + # 连接的最大等待时间 + max-wait: 100ms +``` + +自动注入RedisTemplate并测试 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +@SpringBootTest +class RedisDemo2ApplicationTests { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + void contextLoads() { + redisTemplate.opsForValue().set("name","zhangsan"); + String name = (String) redisTemplate.opsForValue().get("name"); + System.out.println("name = " + name); + } + +} +``` + + + +### RedisTemplate的RedisSerializer(序列化器) + +为什么说序列化器呢,当你执行完刚刚的测试代码后,去Redis客户端中get这个name字段 + +![image-20230827200137337](https://s2.loli.net/2023/08/27/Wh6RFD9ef8uG4wY.png) + +此时会发现是zhangsan,当你在Redis客户端后重新设置name,name会改变,而你在自己写的测试上运行后,再次get,依然是客户端上的值,这是怎么回事呢,我们keys *查看一下所有的key![image-20230827200254513](https://s2.loli.net/2023/08/27/tR4qKX3raJ6gdMP.png) + +此时发现了一个很奇怪的东西,但不可否认的,这个key就是你刚刚set上去的name + +RedisTemplate可以接收任意Object作为值写入Redis + +只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的 + +`"\xAC\xED\x00\x05t\x00\x04name"` + +缺点: + +- 可读性差 +- 内存占用较大 + +我们可以自定义RedisTemplate的序列化方式 + +引入JackSon依赖 + +```xml + + + com.fasterxml.jackson.core + jackson-databind + +``` + +编写配置类 + +```java +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; + +@Configuration +public class RedisTemplateSerializer { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + // 创建RedisTemplate对象 + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 设置连接工厂 + redisTemplate.setConnectionFactory(connectionFactory); + // 设置JSON序列化工具 + GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); + // 设置key的序列化 + redisTemplate.setKeySerializer(RedisSerializer.string()); + redisTemplate.setHashKeySerializer(RedisSerializer.string()); + // 设置value的序列化 + redisTemplate.setValueSerializer(jsonRedisSerializer); + redisTemplate.setHashValueSerializer(jsonRedisSerializer); + // 返回 + return redisTemplate; + + } +} +``` + +测试类 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +@SpringBootTest +class RedisDemo2ApplicationTests { + + @Autowired + private RedisTemplate redisTemplate; + + @Test + void contextLoads() { + redisTemplate.opsForValue().set("name","lisi"); + String name = (String) redisTemplate.opsForValue().get("name"); + System.out.println("name = " + name); + } + +} +``` + +![image-20230827201822126](https://s2.loli.net/2023/08/27/WfTNQ1qrOpLvd5P.png) + +此时就成功了,如果你存入对象的话,会自动转为json对象并存入 + + + +### StringRedisTemplate + +为了节省内存空间,我们可以不使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。 + +因为存入和读取时的序列化及反序列化都是我们自己实现的,SpringDataRedis就不会将class信息写入Redis了 + +这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。 + +普通属性(非对象) + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; + +@SpringBootTest +class RedisDemo2ApplicationTests { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Test + void contextLoads() { + stringRedisTemplate.opsForValue().set("name","lisi"); + String name = (String) stringRedisTemplate.opsForValue().get("name"); + System.out.println("name = " + name); + } + +} +``` + +对象(手动序列化) + +```java +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; + +@SpringBootTest +class RedisDemo2ApplicationTests { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + private static final ObjectMapper mapper = new ObjectMapper(); + @Test + void contextLoads() throws JsonProcessingException { + User zhangsan = new User("zhangsan", 66); + // 手动序列化对象(将对象写成json) + String value = mapper.writeValueAsString(zhangsan); + // 写入数据 + stringRedisTemplate.opsForValue().set("user:1",value); + // 获取数据 + String jsonUser = stringRedisTemplate.opsForValue().get("user:1"); + // 手动反序列化对象 + User user = mapper.readValue(jsonUser, User.class); + System.out.println("user = " + user); + } + +} +``` + +优点:大大减少了字节码文件的空间 + +![image-20230827203329137](https://s2.loli.net/2023/08/27/URK3DC5YnwWTBNA.png) + +关于其他的类型,其实都是类似的,这里也不再赘述了 diff --git "a/public/markdowns/Redis\345\256\236\346\210\230\347\257\207.md" "b/public/markdowns/Redis\345\256\236\346\210\230\347\257\207.md" new file mode 100644 index 0000000..3e76a39 --- /dev/null +++ "b/public/markdowns/Redis\345\256\236\346\210\230\347\257\207.md" @@ -0,0 +1,2320 @@ +--- +title: Redis实战篇 +tags: + - Redis +categories: + - Redis +description: Redis实战篇 +abbrlink: 4604f75b +--- + +# 短信登录 + +## 导入项目 + +### 导入SQL + +在资料中运行对应的SQL文件即可,其中的表有 + +| 表 | 说明 | +| :--------------: | :-----------------------: | +| tb_user | 用户表 | +| tb_user_info | 用户详情表 | +| tb_shop | 商户信息表 | +| tb_shop_type | 商户类型表 | +| tb_blog | 用户日记表(达人探店日记) | +| tb_follow | 用户关注表 | +| tb_voucher | 优惠券表 | +| tb_voucher_order | 优惠券的订单表 | + +### 导入后端项目 + +在idea中打开资料中的项目,并修改一下里面的application.yaml文件里的配置 + +添加以下SpringBoot的启动模块 + +![image-20230828091255195](https://s2.loli.net/2023/08/28/OskUp3KSiCFNPEQ.png) + +启动项目,访问http://localhost:8081/shop-type/list测试一下效果 + +效果如下,说明成功了 + +![image-20230828091640486](https://s2.loli.net/2023/08/28/AacYuoVZ1yKdGB7.png) + + + +### 导入前端项目 + +在`nginx.exe`所在的目录打开cmd + +输入`start nginx.exe` + +**nginx.exe所在的目录不允许存在中文!!!** + +访问`localhost:8080` + +image-20230828093008480 + +## 基于Session实现登录 + +### 工作流程 + +![image-20230828102525375](https://s2.loli.net/2023/08/28/GeIup5vWVZr3HP8.png) + +### 发送短信验证码 + +进入短信验证码的页面,点击按钮查看对应的请求地址 + +请求方式为`POST`,请求路径为`/user/code`,请求参数为phone(电话号码) + +![image-20230828145307766](https://s2.loli.net/2023/08/28/ZMFg6tCIsLW19ve.png) + +来到UserController下进行代码的编写 + +```java +/** + * 发送手机验证码 + */ +@PostMapping("code") +public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { + // 发送短信验证码并保存验证码 + userService.sendCode(phone,session); + return Result.fail("功能未完成"); +} +``` + +IUserService + +```java +Result sendCode(String phone, HttpSession session); +``` + +UserServiceImpl + +```java +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.hmdp.dto.Result; +import com.hmdp.entity.User; +import com.hmdp.mapper.UserMapper; +import com.hmdp.service.IUserService; +import com.hmdp.utils.RegexUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; + +/** + *

+ * 服务实现类 + *

+ * + * @author 虎哥 + * @since 2021-12-22 + */ +@Slf4j +@Service +public class UserServiceImpl extends ServiceImpl implements IUserService { + + + @Override + public Result sendCode(String phone, HttpSession session) { + // 1、校验手机号(RegexUtils里面封装了判断是否是手机号的方法) + boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone); + + // 2、如果不符合,返回错误信息 + if (!phoneInvalid) { + return Result.fail("请输入正确的手机号"); + } + + // 3、符合,生成验证码(使用随机生成器生成验证码) + String code = RandomUtil.randomNumbers(6); + + // 4、保存验证码到session + session.setAttribute("code", code); + + // 5、发送验证码(假设验证码发送成功-->由于手机验证码办理太过复杂,所以就不发送了,直接在底层查看) + log.info("验证码为{}", code); + + return Result.ok(); + } +} +``` + +重启服务器,并测试 + +![image-20230828172917490](https://s2.loli.net/2023/08/28/EMvdnqopFkJixZy.png) + +这里得到了验证码,说明没毛病了 + + + +### 短信验证码登录 + +点击登录,查看一下请求地址`http://localhost:8080/api/user/login` + +请求方式为`POST`,请求地址为`/user/login`,请求参数是以json形式传递的一个phone(电话号码),和code(验证码),无返回值 + +在`Payload`这里可以看到传递来的参数 + +![image-20230828173753182](https://s2.loli.net/2023/08/28/v2HXBjLe1IcuDkm.png) + +这里我们采用邮箱验证,先在数据库中更改phone字段类型,将varchar的长度改为100 + +导入邮箱验证需要的maven坐标 + +```xml + + + javax.activation + activation + 1.1.1 + + + + javax.mail + mail + 1.4.7 + + + + org.apache.commons + commons-email + 1.4 + +``` + +编写邮箱验证需要的工具类 + +```java +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import javax.mail.Authenticator; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMessage.RecipientType; + +public class MailUtils { + public static void main(String[] args) throws MessagingException { + //可以在这里直接测试方法,填自己的邮箱即可 + sendTestMail("3162106996@qq.com", new MailUtils().achieveCode()); + } + + public static void sendTestMail(String email, String code) throws MessagingException { + // 创建Properties 类用于记录邮箱的一些属性 + Properties props = new Properties(); + // 表示SMTP发送邮件,必须进行身份验证 + props.put("mail.smtp.auth", "true"); + //此处填写SMTP服务器 + props.put("mail.smtp.host", "smtp.qq.com"); + //端口号,QQ邮箱端口587 + props.put("mail.smtp.port", "587"); + // 此处填写,写信人的账号 + props.put("mail.user", "3162106996@qq.com"); + // 此处填写16位STMP口令 + props.put("mail.password", "你的口令"); + // 构建授权信息,用于进行SMTP进行身份验证 + Authenticator authenticator = new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + // 用户名、密码 + String userName = props.getProperty("mail.user"); + String password = props.getProperty("mail.password"); + return new PasswordAuthentication(userName, password); + } + }; + // 使用环境属性和授权信息,创建邮件会话 + Session mailSession = Session.getInstance(props, authenticator); + // 创建邮件消息 + MimeMessage message = new MimeMessage(mailSession); + // 设置发件人 + InternetAddress form = new InternetAddress(props.getProperty("mail.user")); + message.setFrom(form); + // 设置收件人的邮箱 + InternetAddress to = new InternetAddress(email); + message.setRecipient(RecipientType.TO, to); + // 设置邮件标题 + message.setSubject("Eastwind 邮件测试"); + // 设置邮件的内容体 + message.setContent("尊敬的用户:你好!\n注册验证码为:" + code + "(有效期为一分钟,请勿告知他人)", "text/html;charset=UTF-8"); + // 最后当然就是发送邮件啦 + Transport.send(message); + } + + public static String achieveCode() { //由于数字 1 、 0 和字母 O 、l 有时分不清楚,所以,没有数字 1 、 0 + String[] beforeShuffle = new String[]{"2", "3", "4", "5", "6", "7", "8", "9", "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"}; + List list = Arrays.asList(beforeShuffle);//将数组转换为集合 + Collections.shuffle(list); //打乱集合顺序 + StringBuilder sb = new StringBuilder(); + for (String s : list) { + sb.append(s); //将集合转化为字符串 + } + return sb.substring(3, 8); + } +} +``` + +修改sendCode方法,逻辑如下 + +- 验证手机号/邮箱格式 +- 正确则发送验证码 + +```java +@Override + public Result sendCode(String phone, HttpSession session) throws MessagingException { + // 1、校验邮箱(RegexUtils里面封装了判断是否是邮箱的方法) + boolean phoneInvalid = RegexUtils.isEmailInvalid(phone); + + // 2、如果不符合,返回错误信息 + if (!phoneInvalid) { + return Result.fail("请输入正确的邮箱号码"); + } + + // 3、符合,生成验证码(这里使用刚刚的邮箱工具类,来生成随机邮箱验证码) + String code = MailUtils.achieveCode(); + + // 4、保存验证码到session + session.setAttribute("code", code); + + // 5、发送验证码(假设验证码发送成功-->由于手机验证码办理太过复杂,所以就不发送了,直接在底层查看) + log.info("验证码为{}", code); + MailUtils.sendTestMail(phone,code); + + return Result.ok(); + } +``` + +然后输入邮箱,发送验证码,看看能否接收到验证码 + +测试没有问题之后,我们继续来编写登录功能 + +```java +/** + * 登录功能 + * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 + */ + @PostMapping("/login") + public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ + // 实现登录功能 + return userService.login(loginForm,session); + } +``` + +IUserService + +```java +Result login(LoginFormDTO loginForm, HttpSession session); +``` + +UserServiceImpl + +```java +@Autowired +private IUserService userService; + +@Override +public Result login(LoginFormDTO loginForm, HttpSession session) { + // 1、校验手机号 + // 获取登录账号 + String loginFormPhone = loginForm.getPhone(); + + // 获取登录验证码 + String loginFormCode = loginForm.getCode(); + + // 获取session中的验证码 + String code = (String) session.getAttribute("code"); + + + // 2、校验验证码 + if (!RegexUtils.isEmailInvalid(loginFormCode)){ + // 3、不一致,报错 + return Result.fail("请输入正确的邮箱"); + } + + + // 4、一致,根据手机号查询用户 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getPhone,loginFormPhone); + User user = userService.getOne(queryWrapper); + + // 5、判断用户是否存在 + if (user == null){ + // 6、不存在,创建新用户并保存 + user = createUserWithPhone(loginFormPhone); + } + + // 7、保存用户信息到session中 + session.setAttribute("user",user); + + return Result.ok(); +} + +/** + * 用户登录 + * */ +private User createUserWithPhone(String loginFormPhone) { + // 创建用户 + User user = new User(); + // 设置手机号 + user.setPhone(loginFormPhone); + // 设置昵称(默认名),一个固定前缀+随机字符串 + user.setNickName("user_"+RandomUtil.randomString(8)); + // 保存到数据库 + userService.save(user); + return user; +} +``` + +### 实现登录拦截 + +多个业务逻辑需要校验的话,就要用到拦截器了 + +创建一个`LoginInterceptor`类,实现`HandlerInterceptor`接口,重写其中的两个方法,前置拦截器和完成处理方法,前置拦截器主要用于我们登陆之前的权限校验,完成处理方法是用于处理登录后的信息,避免内存泄露 + +在此之前,需要修改`UserHolder`中所写的ThreadLocal方法,因为它采用的是UserDto,需要改为User + +UserHolder + +```java +import com.hmdp.entity.User; + +public class UserHolder { + private static final ThreadLocal tl = new ThreadLocal<>(); + + public static void saveUser(User user){ + tl.set(user); + } + + public static User getUser(){ + return tl.get(); + } + + public static void removeUser(){ + tl.remove(); + } +} +``` + +LoginInterceptor + +```java +import com.hmdp.entity.User; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** + * 拦截器 + * */ +public class LoginInterceptor implements HandlerInterceptor { + + /** + * 前置拦截 + * */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1、获取session + HttpSession session = request.getSession(); + + // 2、获取session中的用户 + User user = (User) session.getAttribute("user"); + + // 3、判断用户是否存在 + if (user == null) { + // 4、不存在,拦截 + response.setStatus(401); + return false; + } + + // 5、存在,保存用户信息到ThreadLocal(在utils中的UserHolder中已经封装好了这些方法) + UserHolder.saveUser(user); + + // 6、放行 + return true; + + } + + /** + * 视图渲染后,返回给用户前 + * */ + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 移除用户 + UserHolder.removeUser(); + } +} +``` + +配置拦截器 + +MvcConfig + +```java +import com.hmdp.utils.LoginInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MvcConfig implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 排除哪些路径 + registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( + "/user/code", + "/user/login", + "/blog/hot", + "/shop/**", + "/shop-type/**", + "/voucher/**" + ); + } +} +``` + +编写获取当前用户的代码 + +```java +@GetMapping("/me") +public Result me(){ + // 获取当前登录的用户并返回 + User user = UserHolder.getUser(); + return Result.ok(user); +} +``` + +测试 + +先运行一下http://localhost:8080/api/user/me + +因为有拦截器的存在,而且我们设置了状态码为401 + +image-20230828222540216 + +没啥问题,登录后再查看一下,成功 + +image-20230828223436217 + +### 隐藏用户敏感信息 + +如果直接存储一个user对象可能会泄露很多的信息在上面,所以我们需要修改存储session属性的方法 + +```diff + @Override + public Result login(LoginFormDTO loginForm, HttpSession session) { + // 1、校验手机号 + // 获取登录账号 + String loginFormPhone = loginForm.getPhone(); + + // 获取登录验证码 + String loginFormCode = loginForm.getCode(); + + // 获取session中的验证码 + String code = (String) session.getAttribute("code"); + + + // 2、校验验证码 + if (!RegexUtils.isEmailInvalid(loginFormCode)){ + // 3、不一致,报错 + return Result.fail("请输入正确的邮箱"); + } + + + // 4、一致,根据手机号查询用户 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getPhone,loginFormPhone); + User user = userService.getOne(queryWrapper); + + // 5、判断用户是否存在 + if (user == null){ + // 6、不存在,创建新用户并保存 + user = createUserWithPhone(loginFormPhone); + } + + // 7、保存用户信息到session中 +-- session.setAttribute("user",user); +++ session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); + + return Result.ok(); + } +``` + +再去修改获取session属性的方法 + +LoginInterceptor + +```java +import com.hmdp.dto.UserDTO; +import com.hmdp.entity.User; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +/** + * 拦截器 + * */ +public class LoginInterceptor implements HandlerInterceptor { + + /** + * 前置拦截 + * */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1、获取session + HttpSession session = request.getSession(); + + // 2、获取session中的用户 + UserDTO user = (UserDTO) session.getAttribute("user"); + + // 3、判断用户是否存在 + if (user == null) { + // 4、不存在,拦截 + response.setStatus(401); + return false; + } + + // 5、存在,保存用户信息到ThreadLocal(在utils中的UserHolder中已经封装好了这些方法) + UserHolder.saveUser(user); + + // 6、放行 + return true; + + } + + /** + * 视图渲染后,返回给用户前 + * */ + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 移除用户 + UserHolder.removeUser(); + } +} +``` + +UserHolder + +```java +import com.hmdp.dto.UserDTO; +import com.hmdp.entity.User; + +public class UserHolder { + private static final ThreadLocal tl = new ThreadLocal<>(); + + public static void saveUser(UserDTO user){ + tl.set(user); + } + + public static UserDTO getUser(){ + return tl.get(); + } + + public static void removeUser(){ + tl.remove(); + } +} +``` + +这里就得到了数据,而且与想要限定的数值是一样的,没有过多的暴露数据 + +![image-20230829085059993](https://s2.loli.net/2023/08/29/LZojzRI3gEGu8KQ.png) + +## 集群的session共享问题 + +**session共享问题**:多台Tomcat并不共享session存储空间,当请求切换到不同Tomcat服务器时导致数据丢失的问题 + +当有多台Tomcat的服务器时,是不共享session存储空间的,所以Tomcat提出了一个改进,在多台需要共享数据的Tomcat服务器上做一些配置,就可以共享它们的session,也不能完全说共享,准确来说叫拷贝,就是当一台Tomcat接收到数据后,拷贝到另一台Tomcat服务器上,当然,这个拷贝是有时间的,当你正在拷贝时,其他人如果访问了另外的服务器,此时,拷贝还没完成,但是那个人已经访问了,就会导致拿不到对应的数据,也会导致数据丢失 + +解决方案: + +- 使用Redis缓存,因为Redis不属于Tomcat,而是一个个体,当你使用多台Tomcat去访问时,就没啥问题了 + + + +## 基于Redis实现共享session登录 + +思路:校验登录状态时,前端会携带token,token中存储了用户相关的信息,我们通过token去redis中获取之前登录或注册后的保存起来的用户信息,如果用户信息存在,保存用户信息到ThreadLocal,否则就进行拦截 + +而在校验验证码时,我们可以将手机号存到redis中,key为手机号,value为验证码,通过redis中的手机号读取对应的验证码,即使其他人访问,这个数据也会是共享的 + +### 发送短信验证码 + +修改UserServiceImpl中的sendCode方法 + +```java +@Resource + private StringRedisTemplate stringRedisTemplate; + + @Override + public Result sendCode(String phone, HttpSession session) throws MessagingException { + // 1、校验邮箱(RegexUtils里面封装了判断是否是邮箱的方法) + boolean phoneInvalid = RegexUtils.isEmailInvalid(phone); + + // 2、如果不符合(true),返回错误信息 + if (phoneInvalid) { + return Result.fail("请输入正确的邮箱号码"); + } + + // 3、符合,生成验证码(这里使用刚刚的邮箱工具类,来生成随机邮箱验证码) + String code = MailUtils.achieveCode(); + + // 4、保存验证码到Redis(并设置有效期为2分钟) + stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES); + + // 5、发送验证码(假设验证码发送成功-->由于手机验证码办理太过复杂,所以就不发送了,直接在底层查看) + log.info("验证码为{}", code); + MailUtils.sendTestMail(phone,code); + + return Result.ok(); + } +``` + +### 短信验证码登录 + +```java +@Autowired +private IUserService userService; + +@Override +public Result login(LoginFormDTO loginForm, HttpSession session) { + // 1、校验手机号 + // 获取登录账号 + String loginFormPhone = loginForm.getPhone(); + + // 获取登录验证码 + String loginFormCode = loginForm.getCode(); + + // 获取redis中的验证码 + String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginFormPhone); + + // 2、校验验证码 + if (!RegexUtils.isEmailInvalid(loginFormCode)){ + // 3、不一致,报错 + return Result.fail("请输入正确的邮箱"); + } + + + // 4、一致,根据手机号查询用户 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getPhone,loginFormPhone); + User user = userService.getOne(queryWrapper); + + // 5、判断用户是否存在 + if (user == null){ + // 6、不存在,创建新用户并保存 + user = createUserWithPhone(loginFormPhone); + } + + // 7、保存用户信息到Redis中 + // 7.1 随机生成token(UUID),作为登录令牌 + String token = UUID.randomUUID().toString(true); + + // 7.2 将User对象转为Hash存储 + UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); + + // 通过BeanUtil.beanToMap可以将bean转为Hash + Map beanToMap = BeanUtil.beanToMap(userDTO); + + // 7.3 token为key,Hash为值存储到redis中 + // putAll可以一次存多个键值对 + stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token, beanToMap); + + // 7.4设置token有效期为30分钟,这里应该是每次访问地址时都会设置token的有效期 + // 不然只有一次,就没啥效果,在登录拦截器中设置 + stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES); + + // 返回token + return Result.ok(token); +} + +/** + * 用户登录 + * */ +private User createUserWithPhone(String loginFormPhone) { + // 创建用户 + User user = new User(); + // 设置手机号 + user.setPhone(loginFormPhone); + // 设置昵称(默认名),一个固定前缀+随机字符串 + user.setNickName("user_"+RandomUtil.randomString(8)); + // 保存到数据库 + userService.save(user); + return user; +} +``` + +LoginInterceptor + +```java +private StringRedisTemplate stringRedisTemplate; + +public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; +} + +/** + * 前置拦截 + * */ +@Override +public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1、获取请求头中的token + String token = request.getHeader("authorization"); + + if (StrUtil.isBlank(token)) { + // 不存在token,说明未登录,拦截 + response.setStatus(401); + return false; + } + + // 2、基于token获取redis中的用户 + // entries方法可以返回一个Map集合 + Map entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token); + + + + // 3、判断用户是否存在 + if (entries.isEmpty()) { + // 4、不存在,拦截 + response.setStatus(401); + return false; + } + + // 5、将查询到的Hash数据转为Dto对象 + // fillBeanWithMap(传递的map,转换的对象,转换过程中出现错误是否抛出还是忽略) + UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false); + + // 6、存在,保存用户信息到ThreadLocal(在utils中的UserHolder中已经封装好了这些方法) + UserHolder.saveUser(userDTO); + + // 7、刷新token的有效期 + stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL, TimeUnit.MINUTES); + + /// 8、放行 + return true; + +} +``` + +MvcConfig + +因为这里使用的构造方法,所以需要在构造器处添加一个 + + + +```java +@Resource +private StringRedisTemplate stringRedisTemplate; + +@Override +public void addInterceptors(InterceptorRegistry registry) { + // 排除哪些路径 + registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns( + "/user/code", + "/user/login", + "/blog/hot", + "/shop/**", + "/shop-type/**", + "/voucher/**" + ); +} +``` + +UserServiceImpl + +在运行时出现了错误,因为StringRedisTemplate的值只能为String,所以调整了StringRedisTemplate + +```java +@Override +public Result login(LoginFormDTO loginForm, HttpSession session) { + // 1、校验手机号 + // 获取登录账号 + String loginFormPhone = loginForm.getPhone(); + + // 获取登录验证码 + String loginFormCode = loginForm.getCode(); + + // 获取redis中的验证码 + String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginFormPhone); + + // 2、校验验证码 + if (!RegexUtils.isEmailInvalid(loginFormCode)){ + // 3、不一致,报错 + return Result.fail("请输入正确的邮箱"); + } + + + // 4、一致,根据手机号查询用户 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getPhone,loginFormPhone); + User user = userService.getOne(queryWrapper); + + // 5、判断用户是否存在 + if (user == null){ + // 6、不存在,创建新用户并保存 + user = createUserWithPhone(loginFormPhone); + } + + // 7、保存用户信息到Redis中 + // 7.1 随机生成token(UUID),作为登录令牌 + String token = UUID.randomUUID().toString(true); + + // 7.2 将User对象转为Hash存储 + UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); + + // 通过BeanUtil.beanToMap可以将bean转为Hash + // beanToMap可以自定义类型 + // setFieldValueEditor可以改变存储的类型 + // (field,fieldValue) -> fieldValue.toString())可以将返回的值转为String + // 因为StringRedisTemplate里面的值,只能为String,所以需要修改 + Map beanToMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create() + .setIgnoreNullValue(true) + .setFieldValueEditor((field,fieldValue) -> fieldValue.toString())); + + // 7.3 token为key,Hash为值存储到redis中 + // putAll可以一次存多个键值对 + stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token, beanToMap); + + // 7.4设置token有效期为30分钟 + stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES); + + // 返回token + return Result.ok(token); +} +``` + +测试,在Redis中查看验证码是否被缓存以及token的情况 + +![image-20230829121248143](https://s2.loli.net/2023/08/29/d1O5p8bT7oVSaNt.png) + +查看请求头中的token + +![image-20230829121427217](https://s2.loli.net/2023/08/29/Qot6dYW1IxyJnTs.png) + +### 解决登录状态刷新的问题 + +在上面的方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的 + +应该修改为,只要用户登录了,无论访问什么路径都会刷新登录状态的存在时间,这样就解决了问题 + +修改登录拦截器`LoginInterceptor` + +登录拦截器负责拦截无token状态的用户,在token刷新后执行 + +```java +/** + * 拦截器(拦截无token状态的用户) + */ +public class LoginInterceptor implements HandlerInterceptor { + + /** + * 前置拦截 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 判断是否需要拦截(ThreadLocal中是否存在用户) + if (UserHolder.getUser() == null) { + // 如果没有,说明用户未登录,需要拦截 + return false; + } + // 如果有,直接放行 + return true; + + } +} +``` + +`RefreshTokenInterceptor` + +当用户存在并操作时刷新并保存token + +```java +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.hmdp.dto.UserDTO; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; +import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL; + +/** + * 拦截器(登录才进行token刷新) + */ +public class RefreshTokenInterceptor implements HandlerInterceptor { + + private StringRedisTemplate stringRedisTemplate; + + public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 前置拦截 + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1、获取请求头中的token + String token = request.getHeader("authorization"); + if (StrUtil.isBlank(token)) { + // 不管是否存在,都放行 + return true; + } + + // 2、基于token获取redis中的用户 + // entries方法可以返回一个Map集合 + Map entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token); + // 3、判断用户是否存在 + if (entries.isEmpty()) { + return true; + } + + // (token是登录后才有的) + + // 4、将查询到的Hash数据转为Dto对象 + // fillBeanWithMap(传递的map,转换的对象,转换过程中出现错误是否抛出还是忽略) + UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false); + + // 5、存在,保存用户信息到ThreadLocal(在utils中的UserHolder中已经封装好了这些方法) + UserHolder.saveUser(userDTO); + + // 6、刷新token的有效期 + stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES); + + // 7、放行 + return true; + + } + + /** + * 视图渲染后,返回给用户前 + */ + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 移除用户 + UserHolder.removeUser(); + } +} +``` + +配置拦截器`MvcConfig` + +```java +import com.hmdp.utils.LoginInterceptor; +import com.hmdp.utils.RefreshTokenInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +@Configuration +public class MvcConfig implements WebMvcConfigurer { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // excludePathPatterns排除哪些路径 + // 登录拦截器 + registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( + "/user/code", + "/user/login", + "/blog/hot", + "/shop/**", + "/shop-type/**", + "/voucher/**" + ).order(1); + // 拦截所有请求 + // order一般用来决定拦截器的顺序,order越小,越先执行 + // token刷新拦截器 + registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); + } +} +``` + +测试访问不同地址时,登录状态是否刷新token + +# 商户查询缓存 + +## 什么是缓存 + +缓存是数据交换的缓冲期(Cache),是存储数据的临时地方,一般读写性能较高 + +或者另类点的说法,就是当你查看了一件商品,商品数据会留着,当你再次访问时,直接为你返回留下的商品数据,大大的减少了服务器的压力 + +缓存的作用 + +- 降低后端负载 +- 提高读写效率,降低响应时间 + + + +## 添加Redis缓存 + +访问商户,查看一下商户对应的请求 + +![image-20230830085759673](https://s2.loli.net/2023/08/30/eo3PXbG1ChxvLT5.png) + +请求方式`GET`,请求地址`/shop/商户的id` + +这个请求对应着`ShopController`下的`queryShopById`,该方法是直接返回查询到的一个数据库数据 + +### 实现流程 + +缓存的工作流程 + +- 客户端向Redis发起请求 + - 请求命中:直接从Redis返回数据给客户端 + - 请求未命中:从数据库中查询对应数据,并写入到Redis缓存中,再返回数据给客户端 + +根据id查询商铺缓存的流程 + +- 提交商铺id,从Redis中查询商铺缓存 + - 缓存命中:从Redis之间返回商铺缓存 + - 缓存未命中:根据id查询数据库 + - 商铺存在:将商铺数据写入Redis,并返回商铺信息 + - 商铺不存在:返回404 + +![image-20230830090439233](https://s2.loli.net/2023/08/30/FQUntJwTg4uCLNm.png) + +queryShopById + +```java +/** + * 根据id查询商铺信息 + * @param id 商铺id + * @return 商铺详情数据 + */ +@GetMapping("/{id}") +public Result queryShopById(@PathVariable("id") Long id) { + return shopService.queryById(id); +} +``` + +IShopService + +```java +import com.hmdp.dto.Result; +import com.hmdp.entity.Shop; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author 虎哥 + * @since 2021-12-22 + */ +public interface IShopService extends IService { + + Result queryById(Long id); +} +``` + +ShopServiceImpl + +```java +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.hmdp.dto.Result; +import com.hmdp.entity.Shop; +import com.hmdp.mapper.ShopMapper; +import com.hmdp.service.IShopService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY; + +/** + *

+ * 服务实现类 + *

+ * + * @author 虎哥 + * @since 2021-12-22 + */ +@Service +public class ShopServiceImpl extends ServiceImpl implements IShopService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public Result queryById(Long id) { + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)){ + // 存在,直接返回 + // 返回前需要转为对象 + Shop shop = JSONUtil.toBean(cache, Shop.class); + return Result.ok(shop); + } + // 不存在,根据id查询数据库 + Shop shop = getById(id); + if (shop == null){ + // 查询数据库后不存在,返回错误 + return Result.fail("未找到该商铺信息"); + } + // 存在,写入redis + // 转换前需要将对象转为json字符串存入 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop)); + //返回 + return Result.ok(shop); + } +} +``` + +测试,查看redis,此时已经将数据存入了,再次刷新,也没有再去访问数据库了 + +![image-20230830094730782](https://s2.loli.net/2023/08/30/53hGoYtEkFwPHu1.png) + +无缓存时的访问速度 + +![image-20230830095002607](https://s2.loli.net/2023/08/30/ce4dkKR26uBsU9i.png) + +有缓存时的访问速度 + +![image-20230830095016381](https://s2.loli.net/2023/08/30/AjlpfTv7Xe1J6Da.png) + +快了近十倍,缓存真香 + +### 店铺类型查询业务添加缓存 + +#### 使用Redis中的String缓存 + +为ShopTypeController的queryTypeList添加缓存 + +```java +@GetMapping("list") +public Result queryTypeList() { + return typeService.queryList(); +} +``` + +IShopTypeService + +```java +import com.hmdp.dto.Result; +import com.hmdp.entity.ShopType; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author 虎哥 + * @since 2021-12-22 + */ +public interface IShopTypeService extends IService { + + Result queryList(); +} +``` + +ShopTypeServiceImpl + +```java +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.hmdp.dto.Result; +import com.hmdp.entity.ShopType; +import com.hmdp.mapper.ShopTypeMapper; +import com.hmdp.service.IShopTypeService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @author 虎哥 + * @since 2021-12-22 + */ +@Service +public class ShopTypeServiceImpl extends ServiceImpl implements IShopTypeService { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public Result queryList() { + String token = "shop-type:list"; + String shopList = stringRedisTemplate.opsForValue().get(token); + // 查询结果不为空 + if (StrUtil.isNotBlank(shopList)) { + List shopTypes = JSONUtil.toList(shopList, ShopType.class); + return Result.ok(shopTypes); + } + // 查询结果为空 + List sort = query().orderByAsc("sort").list(); + if (sort == null){ + // 如果数据库查到的也为空 + return Result.fail("店铺类型不存在"); + } + // 存入redis + String toJsonStr = JSONUtil.toJsonStr(sort); + stringRedisTemplate.opsForValue().set(token,toJsonStr); + // 返回数据 + return Result.ok(sort); + } +} +``` + +#### 使用Redis中的List缓存 + +```java +@Override +public Result queryList() { + //先从Redis中查,这里的常量值是固定前缀 + 店铺id + List shopTypes = + stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, -1); + //如果不为空(查询到了),则转为ShopType类型直接返回 + if (!shopTypes.isEmpty()) { + List tmp = new ArrayList<>(); + // 遍历查询得到的每一个String类型,通过JSONUtil转为ShopType存到新集合中,并返回 + for (String types : shopTypes) { + ShopType shopType = JSONUtil.toBean(types, ShopType.class); + tmp.add(shopType); + } + return Result.ok(tmp); + } + //否则去数据库中查 + List tmp = query().orderByAsc("sort").list(); + if (tmp == null){ + return Result.fail("店铺类型不存在!!"); + } + //查到了转为json字符串,存入redis + for (ShopType shopType : tmp) { + String jsonStr = JSONUtil.toJsonStr(shopType); + shopTypes.add(jsonStr); + } + stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_TYPE_KEY,shopTypes); + //最终把查询到的商户分类信息返回给前端 + return Result.ok(tmp); +} +``` + +stream流简化代码 + +```java +@Override +public Result queryList() { + // 先从Redis中查,这里的常量值是固定前缀 + 店铺id + List shopTypes = + stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, -1); + // 如果不为空(查询到了),则转为ShopType类型直接返回 + if (!shopTypes.isEmpty()) { + List tmp = shopTypes.stream().map(type -> JSONUtil.toBean(type, ShopType.class)).collect(Collectors.toList()); + return Result.ok(tmp); + } + // 否则去数据库中查 + List tmp = query().orderByAsc("sort").list(); + if (tmp == null){ + return Result.fail("店铺类型不存在!!"); + } + // 查到了转为json字符串,存入redis + shopTypes = tmp.stream().map(type -> JSONUtil.toJsonStr(type)) + .collect(Collectors.toList()); + stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_TYPE_KEY,shopTypes); + // 最终把查询到的商户分类信息返回给前端 + return Result.ok(tmp); +} +``` + +## 缓存更新策略 + +缓存更新是Redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们想Redis插入太多数据,此时就可能会导致缓存中数据过多,所以Redis会对部分数据进行更新,或者把它成为淘汰更合适 + +| 内存淘汰 | 超时剔除 | 主动更新 | | +| :------: | :----------------------------------------------------------: | :----------------------------------------------------------: | ---------------------------------------------- | +| 说明 | 不用自己维护, 利用Redis的内存淘汰机制, 当内存不足时自动淘汰部分数据。 下次查询时更新缓存。 | 给缓存数据添加TTL时间, 到期后自动删除缓存。 下次查询时更新缓存。 | 编写业务逻辑, 在修改数据库的同时, 更新缓存。 | +| 一致性 | 差 | 一般 | 好 | +| 维护成本 | 无 | 低 | 高 | + +- 业务场景 + - 低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存(因为这个很长一段时间都不需要更新) + - 高一致性需求:主动更新,并以超时剔除作为兜底方案,例如店铺详情查询的缓存 + +一般看场景使用 + +### 主动更新策略 + +- Cache Aside Pattern(缓存旁模式):由缓存的调用者,在更新数据库的同时更新缓存 +- Read/Write Through Pattern(读/写 通过模式):缓存与数据库整合为一个服务,由服务来维护一致性。调用者直接调用该服务,无需关心缓存一致性问题 + - 缺点:服务难整合 +- Write Behind Caching Pattern(写在缓存模式后):调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致,或者说缓存被修改后,数据库并不会马上被修改,而是由其他线程异步的将缓存中的数据持久化到数据库中,保证其一致,而且当缓存不断的被操作,异步线程只会取最后的操作效果持久化到数据库 + - 缺点:如果缓存中执行了很多操作后,依然没有触发异步线程,此时,如果突然崩溃了,那缓存中的数据就会丢失了,一致性就不存在了 + +企业中使用较多的是方案一,安全性好,一致性高 + + + +### 操作缓存和数据库的三个问题 + +#### 删除缓存还是更新缓存 + +- 更新缓存:每次更新数据库都更新缓存,无效写操作较多(如果反复更新数据库,而不去读取缓存,那么缓存也被无限制的更新,无效的写操作) +- **删除缓存(使用)**:更新数据库时让缓存失效,查询时再更新缓存(更新数据库时删除缓存,我们只需要删除一次缓存,无论更新多少次数据库都没事,完全不影响,当要查询时再更新缓存,完美!) + +#### 如何保证缓存与数据库的操作同时成功或失败 + +- 单体系统,将缓存和数据库放在一个事务中,数据库失败则缓存失败,数据库成功则缓存成功 +- 分布式系统,利用TCC等分布式事务方案 + +#### 先操作缓存还是先操作数据库 + +- 先删除缓存,再操作数据库 + + - 正常情况 + - 先进行缓存的删除,再更新数据库,此时如果有人查询缓存,必然是没有缓存的,因为缓存被删除了,所以查询数据库,查询得到的是数据库最新的值,写入缓存,查询得到最新的值 + - 异常情况 + - 先进行缓存的删除,缓存删除后并没有更新数据库,就切换了其他线程来查询缓存,缓存被删除了,所以没有缓存,则查询数据库,而数据库还没有更新,所以返回了旧的值,缓存依然是旧的值,此时回归更新数据库线程,更新了数据库,数据库中是新的值,缓存是旧的值,有问题 + + + + 下图为错误图示例 + + image-20230830141231546 + + 读写查很快,而数据库更新很慢,所以极容易出现上述的异常情况 + +- **先操作数据库,再删除缓存(常用)** + + - 正常情况 + - 先更新数据库为新数据,再删除缓存,因为缓存删除了,所以如果有人来查询缓存,此时必然是没有的,会先到数据库查询,再为缓存赋值,是最新数据 + - 异常情况(可能性低) + - 假设一种特殊情况:缓存失效了,此时线程1去查数据库,会得到旧数据,得到之后,被线程2拿到了控制权,线程2要改变数据库,再删缓存,那么流程就是,线程2先更新了数据库,又删除了缓存(此时缓存是空的,删了等于没删),删完之后切回线程1,线程1将刚刚查询得到的旧数据写入缓存,此时的缓存是旧数据,而数据库是新数据,有问题 + + 下图为错误图示例 + + image-20230830141513424 + +缓存更新策略的最佳实践方案: + +1. 低一致性需求:使用Redis自带的内存淘汰机制 +2. 高一致性需求:主动更新,并以超时剔除作为兜底方案 + - 读操作: + - 缓存命中直接返回 + - 缓存未命中则查询数据库,并写入缓存,设定超时时间 + - 写操作: + - 先写数据库,再删除缓存 + - 要确保数据库与缓存操作的原子性 + + + +## 给查询商铺的缓存添加超时剔除和主动更新的策略 + +修改ShopController中的业务逻辑,满足下面的需求: + +- 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间 +- 根据id修改店铺时,先修改数据库,再删除缓存 + +### 超时剔除 + +ShopServiceImpl(添加了一个超时时间) + +```java +@Override + public Result queryById(Long id) { + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)){ + // 存在,直接返回 + // 返回前需要转为对象 + Shop shop = JSONUtil.toBean(cache, Shop.class); + return Result.ok(shop); + } + // 不存在,根据id查询数据库 + Shop shop = getById(id); + if (shop == null){ + // 查询数据库后不存在,返回错误 + return Result.fail("未找到该商铺信息"); + } + // 存在,写入redis + // 转换前需要将对象转为json字符串存入 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); + //返回 + return Result.ok(shop); + } +``` + +### 主动更新 + +ShopController + +```java +@PutMapping +public Result updateShop(@RequestBody Shop shop) { + // 写入数据库 + return shopService.update(shop); +} +``` + +IShopService + +```java +Result update(Shop shop); +``` + +ShopServiceImpl + +```java +@Override +@Transactional +public Result update(Shop shop) { + Long id = shop.getId(); + if (id == null){ + return Result.fail("店铺id不能为空"); + } + // 先更新数据库 + updateById(shop); + // 再删除缓存 + stringRedisTemplate.delete(CACHE_SHOP_KEY+id); + return Result.ok(); +} +``` + +测试Redis中的缓存是否会存储30分钟(成功) + +![image-20230830152921211](https://s2.loli.net/2023/08/30/DiQvteIMja5NmUd.png) + +测试修改后数据库和缓存是否都会被修改(在postman中测试) + +请求方式`PUT`,请求地址http://localhost:8081/shop + +测试数据 + +```json +{ + "area": "大关", + "openHours": "10:00-22:00", + "sold": 4215, + "address": "金华路锦昌文华苑29号", + "comments": 3035, + "avgPrice": 80, + "score": 37, + "name": "102茶餐厅", + "typeId": 1, + "id": 1 +} +``` + +数据库更新成功 + +![image-20230830154434479](https://s2.loli.net/2023/08/30/pWx9OdH5A2fvzPt.png) + +redis中的数据也被删除了,当再次查询时才会从数据库中查询 + +image-20230830154514113 + +## 缓存穿透 + +缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库 + +就是说,如果客户端发起了一个请求,请求一家店铺,但是这家店铺是不存在的,也就是假的请求,然后此时会请求到Redis中,Redis查看后肯定是没有的,毕竟是假的,然后发给数据库,数据库肯定也没有,然后返回 + +此时,如果有一个不坏好意的人,整了多个线程,一直反复的发,反复的请求,可能就要坏事了 + +常见的解决方案有两种 + +- 缓存空对象 + +- 缓存空对象流程图 + + image-20230830180305218 + + - 当你发了一个假的不存在的对象给我,我最后从数据库会得到一个null,此时我将其缓存起来,当你再次请求,我就直接从Redis里面返回给你之前那个null,简单粗暴 + - 优点:实现简单,维护方便 + - 缺点 + - 额外的内存消耗(反复给你发不同的空的对象,会缓存起来一堆垃圾),可以设置一个TTL(缓存有效期),设置为5分钟或者2分钟不等,专门用来缓存垃圾数据 + - 可能造成短期的不一致(如果我们已经在Redis缓存了发来的空对象,此时Redis会缓存起来,如果真的有一个对象就是刚刚被创建出来的,此时,用户再查询,会是null,也就是刚刚缓存的空对象) + +- 布隆过滤 + +- 布隆过滤流程图 + + image-20230830180532253 + + - 客户端请求布隆过滤器,若数据不存在直接拒绝,存在则放行通过去Redis中查询,如果Redis中存在,则从Redis缓存中查询,如果Redis缓存中有,则直接返回,若没有则去数据库中查询,数据库中有返回,否则返回无数据 + - 优点:内存占用较少,没有多余key(拦截后,就会拒绝请求了,不拦截才会去内部查询,有时候过滤器可能会误判,放一些进来也没什么大问题) + - 缺点 + - 实现复杂 + - 存在误判可能 + + + +### 代码实现 + +ShopServiceImpl + +```java +@Override + public Result queryById(Long id) { + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)){ + // 存在,直接返回 + // 返回前需要转为对象 + Shop shop = JSONUtil.toBean(cache, Shop.class); + return Result.ok(shop); + } + + // 如果是空字符串 + if (cache != null){ + // 如果在redis中没有查询到该商铺的信息,说明该缓存为null,去数据库中查询 + // 如果数据库也为空,说明缓存穿透了,然后将空值写入Redis,并返回错误 + // 当再次访问时,查询得到的会是空字符串,而不是空,就会直接拿到redis中的数据,会是空,然后报错 + // StrUtil.isNotBlank会判别空字符串是空的,所以上面的并不会返回数据 + return Result.fail("未找到该商铺信息"); + } + + // 不存在,根据id查询数据库 + Shop shop = getById(id); + if (shop == null){ + // 将空值写入Redis并设置时间为两分钟 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES); + // 查询数据库后不存在,返回错误 + return Result.fail("未找到该商铺信息"); + } + // 存在,写入redis + // 转换前需要将对象转为json字符串存入 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); + //返回 + return Result.ok(shop); + } +``` + +测试 + +访问http://localhost:8080/api/shop/0,这是一个不存在的地址,看控制台返回什么 + +没有找到数据,并且页面也显示为空![image-20230830192507127](https://s2.loli.net/2023/08/30/3Xcrm5IjU4EDqVC.png) + +![image-20230830192444687](https://s2.loli.net/2023/08/30/StMlp2mZ8AB13KN.png) + +再次访问该地址,会发现控制台并没有输出sql语句,而是直接去了redis缓存中查询 + +## 缓存雪崩 + +缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力 + +当Redis服务宕机时,情况就会极为严重,所有的请求都会直接来到数据库中 + +image-20230901215347179 + +解决方案: + +- 给不同的key的TTL(存活时间)添加随机值 +- 利用Redis集群提高服务的高可用性 +- 给缓存业务添加降级限流策略 +- 给业务添加多级缓存 + +## 缓存击穿 + +缓存击穿问题也叫热点Key问题,就是被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问在瞬间给数据库带来巨大的冲击 + +可以理解为,当一个线程去访问一个高并发访问的缓存并且缓存创建业务复杂的key失效了,那么当前线程查询缓存会未命中,而它的流程也比较麻烦,此时如果有其他线程进来查询,也会去数据库查询,因为最开始的线程还没有将数据写入缓存,而它又是一个高并发的线程,所以,可能会导致数据库压力过大,宕机 + +image-20230902095232043 + +常见的解决方案有两种: + +### 互斥锁 + +- 假设若干个线程同时来访问缓存,其中最早的一个获取到了互斥锁,那么这个线程会去查询数据库重建缓存数据,并将查到的内容写入缓存后,释放锁,而其他线程在那个线程获取了互斥锁后,都没办法再获取互斥锁,则会休眠一会再重试,而且会不断重试,直到命中 +- image-20230902095932118 +- 缺点:当数千线程同时进入,只有一个线程可以得到互斥锁并重建缓存数据,其他线程只能无限期的等待,所以性能较差 + +### 逻辑过期 + +- 要设置逻辑过期,需要在缓存的时候添加**逻辑时间**,如果有线程查询缓存,当逻辑时间过期时,就会去获取互斥锁,然后开启一个新线程,由新线程来查询数据库,重建缓存数据并写入缓存,重置逻辑时间,最后释放锁,而之前的现场就会返回一份过期数据,如果在**新线程写入缓存**期间有其他线程来访问的话,查询缓存后,查到的是旧数据,此时会发现逻辑时间已经过期, 就会去获取互斥锁,获取失败后,会直接返回一份过期数据,只有当**新线程写入缓存的事件结束后**,其他线程再来访问,就可以得到新数据 +- image-20230902101201232 + +### 互斥锁与逻辑过期的对比 + +| 解决方案 | 优点 | 缺点 | +| :------: | :----------------------------------------------: | :--------------------------------------------: | +| 互斥锁 | 没有额外的内存消耗
保证一致性
实现简单 | 线程需要等待,性能受影响
可能有死锁的风险 | +| 逻辑过期 | 线程无需等待,性能较好 | 不保证一致性
有额外内存消耗
实现复杂 | + +死锁的概念:***毕业生找工作,公司表示只需要有工作经验才可以来;要想有工作经验,就需要去公司工作......*** + +毕业生找工作,必须要有工作经验才能去公司,而要想有工作经验,就必须要公司工作,而毕业生从来没在公司工作,自然也就无法去公司工作 + +或者说,一个线程获取了锁A,下面将要获取锁B时,结果切换了线程,切换后的线程去获取了锁B,又要去获取锁A,因为此时锁A被另一个线程占用了,所以获取不了,那么只能等待,最后又切换回了锁A的线程,锁A线程重新访问锁B,锁B被另一个线程所占用了,就导致也获取不了,最后无限循环,死锁了 + +```java +public void add(int m) { + synchronized(lockA) { // 获得lockA的锁 + this.value += m; + synchronized(lockB) { // 获得lockB的锁 + this.another += m; + } // 释放lockB的锁 + } // 释放lockA的锁 +} + +public void dec(int m) { + synchronized(lockB) { // 获得lockB的锁 + this.another -= m; + synchronized(lockA) { // 获得lockA的锁 + this.value -= m; + } // 释放lockA的锁 + } // 释放lockB的锁 +} +``` + +### 基于互斥锁方式解决缓存击穿的问题 + +需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题 + +#### 流程分析 + +提交商铺id,从Redis中查询商铺缓存,判断缓存是否命中 + +- 缓存命中:返回数据 +- 缓存未命中:判断是否获取锁 + - 获取锁:根据Id查询数据库,将商铺数据写入Redis,释放锁,并返回数据 + - 未获取锁:休眠一段时间,从Redis查询商铺缓存,再次执行上述的情况,直到锁被释放,从缓存中查询得到最新的数据,并返回 + +image-20230902110255613 + +在ServiceImpl下创建两个方法用于获取锁和释放锁 + +```java +// 获取锁 +private boolean TryLock(String key){ + // 获取锁,并设置保存时间为10秒,因为业务一般1秒内都能结束 + // 如果出现10秒还未结束的业务,说明有问题,将锁删除 + // setIfAbsent,当key存在时,不创建key,否则创建key + Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); + // 返回可能会进行自动拆箱的操作,导致空指针,这里借助具体类修改 + return BooleanUtil.isTrue(aBoolean); +} + +// 释放锁 +private void unlock(String key){ + stringRedisTemplate.delete(key); +} +``` + +将缓存穿透的逻辑封装 + +```java +public Result queryWithPassThrough(Long id){ + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)){ + // 存在,直接返回 + // 返回前需要转为对象 + Shop shop = JSONUtil.toBean(cache, Shop.class); + return Result.ok(shop); + } + + // 如果是空字符串 + if (cache != null){ + // 如果在redis中没有查询到该商铺的信息,说明该缓存为null,去数据库中查询 + // 如果数据库也为空,说明缓存穿透了,然后将空值写入Redis,并返回错误 + // 当再次访问时,查询得到的会是空字符串,而不是空,就会直接拿到redis中的数据,会是空,然后报错 + // StrUtil.isNotBlank会判别空字符串是空的,所以上面的并不会返回数据 + return Result.fail("未找到该商铺信息"); + } + + // 不存在,根据id查询数据库 + Shop shop = getById(id); + if (shop == null){ + // 将空值写入Redis并设置时间为两分钟 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES); + // 查询数据库后不存在,返回错误 + return Result.fail("未找到该商铺信息"); + } + // 存在,写入redis + // 转换前需要将对象转为json字符串存入 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); + //返回 + return Result.ok(shop); +} +``` + +#### 代码实现 + +```java +@Override +public Result queryById(Long id) { + /* 缓存穿透 + Result = queryWithPassThrough(id);*/ + + // 互斥锁解决缓存击穿 + + return queryWithMutex(id); +} + +// 互斥锁 +public Result queryWithMutex(Long id){ + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)){ + // 存在,直接返回 + // 返回前需要转为对象 + Shop shop = JSONUtil.toBean(cache, Shop.class); + return Result.ok(shop); + } + + // 如果是空字符串 + if (cache != null){ + // 如果在redis中没有查询到该商铺的信息,说明该缓存为null,去数据库中查询 + // 如果数据库也为空,说明缓存穿透了,然后将空值写入Redis,并返回错误 + // 当再次访问时,查询得到的会是空字符串,而不是空,就会直接拿到redis中的数据,会是空,然后报错 + // StrUtil.isNotBlank会判别空字符串是空的,所以上面的并不会返回数据 + return Result.fail("未找到该商铺信息"); + } + + // 实现缓存重建 + // 获取互斥锁 + // 为每个商铺都添加一把锁,访问不同的店铺就会互不影响 + String lockKey = "lock:shop" + id; + Shop shop = null; + try { + boolean isLock = TryLock(lockKey); + // 判断是否获取成功 + if (!isLock){ + // 失败,则休眠重试 + Thread.sleep(50); + return queryWithMutex(id); + } + + + // 获取互斥锁成功,根据id查询数据库 + shop = getById(id); + // 数据库不存在 + if (shop == null){ + // 将空值写入Redis并设置时间为两分钟 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES); + // 查询数据库后不存在,返回错误 + return Result.fail("未找到该商铺信息"); + } + // 存在,写入redis + // 转换前需要将对象转为json字符串存入 + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); + } catch (InterruptedException e) { + throw new RuntimeException(e); + }finally { + // 释放互斥锁 + unlock(lockKey); + } + + //返回 + return Result.ok(shop); +} +``` + +### 基于逻辑过期方式解决缓存击穿的问题 + +#### 流程分析 + +提交商铺id,从Redis中查询缓存,并判断缓存是否命中(一般情况下,都是绝对命中的,因为是逻辑过期) + +- 缓存命中:判断缓存是否过期(逻辑过期) + - 缓存未过期:直接返回商铺信息 + - 缓存已过期:尝试获取互斥锁,判断是否能获取锁 + - 获取锁成功:开启独立线程,并根据Id查询数据库,将商铺数据写入Redis,并设置逻辑过期时间,最后释放互斥锁 + - 获取锁失败:说明有其他线程已经获取了锁正在进行操作,可以直接返回旧的商铺信息 +- 缓存未命中:很少出现这种情况,一旦出现了,返回空 + +image-20230902132308504 + +#### 代码实现 + +在原来的对象上添加逻辑过期时间,但是又不能修改原来的对象,修改了可能会影响到整体的代码逻辑,如何来添加新的数据呢 + +**解决方案** + +- 新建一个类,继承对应的对象,并在新的对象上添加自己需要的属性,并创建一个包含原有数据的类data + +```java +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class RedisData { + private LocalDateTime expireTime; + private T data; +} +``` + +在ShopServiceImpl中添加方法,将Shop添加到Redis当中 + +```java +public void savaShop2Redis(Long id,Long expireSecond){ + // 查询店铺数据 + Shop shop = getById(id); + // 封装逻辑过期时间 + RedisData redisData = new RedisData(); + redisData.setData(shop); + // 设置逻辑过期时间为多少秒 + redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond)); + // 写入Redis(转换类型) + stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData)); +} +``` + +在单元测试中测试能不能将数据添加到Redis当中 + +```java +import com.hmdp.service.impl.ShopServiceImpl; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; + +@SpringBootTest +class HmDianPingApplicationTests { + + @Resource + private ShopServiceImpl shopService; + + @Test + public void test1(){ + shopService.savaShop2Redis(1L,10L); + } +} +``` + +image-20230902135306311 + +ShopServiceImpl + +添加一段逻辑过期的代码 + +```java +@Override + public Result queryById(Long id) { + /* 缓存穿透 + Result = queryWithPassThrough(id);*/ + + // 互斥锁解决缓存击穿 + +// return queryWithMutex(id); + + return queryWithLogicalExpire(id); + } + + + // 创建线程池 + private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); + + // 逻辑过期解决缓冲击穿问题 + public Result queryWithLogicalExpire(Long id) { + // 从redis查询商铺缓存 + String cache = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); + // 判断是否存在 + if (StrUtil.isNotBlank(cache)) { + // 不存在,返回空 + // 返回前需要转为对象 + return null; + } + + // 从缓存中命中数据后,反序列化json为对象 + RedisData redisData = JSONUtil.toBean(cache, RedisData.class); + JSONObject data = (JSONObject) redisData.getData(); + Shop jsonShop = JSONUtil.toBean(data, Shop.class); + // 取出过期时间 + LocalDateTime expireTime = redisData.getExpireTime(); + // 判断是否过期 + if (expireTime.isAfter(LocalDateTime.now())) { + // isAfter判断现在的时间是否在过期时间后 + // 如果现在的时间在过期时间之后,说明还没过期 + // 如果在过期时间之前,说明已过期 + //未过期,直接返回商铺信息 + return Result.ok(jsonShop); + } + // 已过期,缓存重建 + // 判断是否获取互斥锁 + if (TryLock(LOCK_SHOP_KEY + id)) { + + // 获取互斥锁成功,开启新线程,实现缓存重建 + // 线程最好通过线程池来获取,节省资源 + CACHE_REBUILD_EXECUTOR.submit(() -> { + try { + // 重建缓存(在savaShop2Redis中有写过这个方法) + savaShop2Redis(id, 30L); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + // 释放锁 + unlock(LOCK_SHOP_KEY + id); + } + }); + + } + // 获取互斥锁失败,直接返回商铺信息 + return Result.ok(jsonShop); + + } +``` + +即使逻辑过期了,也不影响数据,数据并不会消失 + +## 缓存工具封装 + +基于StringRedisTemplate封装一个缓存工具类,满足下列需求: + +1. 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间 +2. 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题 +3. 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透的问题 +4. 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题 + +在utils包下新建CacheClient类,用于缓存工具的封装 + +```java +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + + +@Component +@Slf4j +public class CacheClient { + @Autowired + private final StringRedisTemplate stringRedisTemplate; + + public CacheClient(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间 + * */ + public void set(String key, Object value, Long time, TimeUnit timeUnit){ + stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit); + } + + + /** + * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题 + * */ + public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit timeUnit){ + RedisData redisData = new RedisData<>(); + redisData.setData(value); + // 过期时间是以当前时间为基准后的多少秒为基准 + redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time))); + // 写入Redis + stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData)); + } + + public T queryWithPassThough( + String keyPrefix, ID id, Class type, Function function,Long time,TimeUnit timeUnit){ + // keyPrefix是key的前缀 + // keyPrefix+id = key + + // 根据Key可以从redis中查询 + String key = keyPrefix + id; + + // 从redis中查询缓存 + String cache = stringRedisTemplate.opsForValue().get(key); + + // 判断是否存在,如果存在,直接返回 + if (StrUtil.isNotBlank(cache)){ + // 将cache转为对应的类型 + return JSONUtil.toBean(cache,type); + } + + // 判断是否为null + if (cache != null) { + // 解决缓存穿透 + return null; + } + + // 不存在,根据Id查询数据库 + // Function function + // ID是传递的参数,T是返回的类型 + T t = function.apply(id); + + // 如果得到的结果为Null,说明缓存穿透了,解决这个问题 + // 向Redis中存入一个空值即可 + if (t == null){ + // 将空值写入Redis + stringRedisTemplate.opsForValue().set(key,"",time,timeUnit); + // 返回错误信息 + return null; + } + + // 存在,存入Redis + this.set(key,t,time,timeUnit); + + return t; + } +} +``` + +缓存穿透修改 + +function需要的是传递一个函数 + + + +```java +@Autowired + private CacheClient cacheClient; + + @Override + public Result queryById(Long id) { + /* 缓存穿透 + Result = queryWithPassThrough(id);*/ + + // 互斥锁解决缓存击穿 + +// return queryWithMutex(id); + + Shop shop = cacheClient.queryWithPassThough(CACHE_SHOP_KEY, id, Shop.class, this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES); + + if(shop == null){ + return Result.fail("该店铺信息不存在"); + } + + return Result.ok(shop); + } +``` + +缓存穿透测试,没啥问题 + +缓存击穿`queryWithLogicalExpire` + +```java +// 创建线程池 +private static final ExecutorService CACHE_BUILD_EXECUTOR = Executors.newFixedThreadPool(10); + + +/** + * 缓存击穿(基于逻辑过期实现) + */ +public T queryWithLogicalExpire( + String keyPrefix, ID id, Class type,Function function,Long time,TimeUnit timeUnit) { + String key = keyPrefix + id; + // 从redis中查询缓存 + String cache = stringRedisTemplate.opsForValue().get(key); + // 判断是否存在 + if (StrUtil.isBlank(cache)) { + // 不存在,直接返回 + return null; + } + + // 命中,需要先把json反序列化为对象 + RedisData redisData = JSONUtil.toBean(cache, RedisData.class); + // 获取对应数据 + T t = JSONUtil.toBean((JSONObject) redisData.getData(), type); + LocalDateTime expireTime = redisData.getExpireTime(); + if (expireTime.isAfter(LocalDateTime.now())) { + // 如果超时时间在当前时间之后 + // 说明未过期,直接返回商铺信息 + return t; + } + + // 否则,已过期,需要缓存重建 + // 获取互斥锁 + // 失败 + if (TryLock(key)) { + // 成功 + // 成功后,开启一个新的线程并让新的线程去操作更新的问题 + CACHE_BUILD_EXECUTOR.submit(() -> { + try { + // 查询数据库 + T dbResult = function.apply(id); + // 写入redis + this.setWithLogicalExpire(key,JSONUtil.toJsonStr(dbResult),time,timeUnit); + } + catch (Exception e){ + e.printStackTrace(); + } + finally { + unlock(key); + } + }); + } + + // 获取互斥锁失败,则直接返回旧数据 + return t; +} + +// 获取锁 +private boolean TryLock(String key) { + // 获取锁,并设置保存时间为10秒,因为业务一般1秒内都能结束 + // 如果出现10秒还未结束的业务,说明有问题,将锁删除 + // setIfAbsent,当key存在时,不创建key,否则创建key + Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); + // 返回可能会进行自动拆箱的操作,导致空指针,这里借助具体类修改 + return BooleanUtil.isTrue(aBoolean); +} + +// 释放锁 +private void unlock(String key) { + stringRedisTemplate.delete(key); +} +``` + + + +# 优惠券秒杀 + +## 全局ID生成器 + +每个店铺都可以发布优惠券,当用户抢购时,就会生成订单保存到订单表中,而订单表的id如果使用数据库自增id就存在一些问题: + +- id的规律性太明显 +- 受单表数据量的限制 + +此时就需要用到全局ID生成器 + +全局ID生成器:是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性: + +- 唯一性 +- 高可用 +- 高性能 +- 递增性 +- 安全性 + +为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息: + +![image-20230903123716447.png](https://s2.loli.net/2023/09/03/fL5AIlaoxOQG1Tv.png) + +ID的组成部分: + +- 符号位:1bit +- 时间戳:31bit,以秒为单位,可以使用69年 +- 序列号:32bit,秒内的计数器,支持每秒产生2*32个不同的ID + +在utils包下 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +@Component +public class RedisIdWorker { + + // 开始的时间戳 + private static final long BEGIN_STAMP = 1672531200; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + // 移动的位数 + private int COUNT_BITS = 32; + + public long nextId(String keyPrefix) { + // 生成时间戳 + LocalDateTime now = LocalDateTime.now(); + // 得到时间戳 + long second = now.toEpochSecond(ZoneOffset.UTC); + long timestamp = second - BEGIN_STAMP; + // 生成序列号 + // 获取当前日期,精确到天 + String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); + + + // 采用date的方式,每天可以生成不同的id,不至于让缓存无限堆积,而且也不利于查找 + // 不使用包装类是因为后面还要进行运算 + long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); + + // 拼接并返回(移动指定位数是为了让increment可以填充到后面的位数上返回一个long类型的订单号) + return timestamp< + +- start mqnamesrv.cmd +- start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true diff --git "a/public/markdowns/SpringBoot2\345\237\272\347\241\200\347\257\207.md" "b/public/markdowns/SpringBoot2\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..685357e --- /dev/null +++ "b/public/markdowns/SpringBoot2\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,1775 @@ +--- +title: SpringBoot2基础篇 +tags: + - SpringBoot2 +categories: + - SpringBoot +description: SpringBoot2基础篇 +abbrlink: bd45f405 +--- +# 小技巧(隐藏指定文件/文件夹) + +SpringBoot每次创建时都会携带很多没啥用的文件,可以在设置中进行隐藏,不需要每次都删除 + +image-20230907143641453 + +在设置中,可以进行配置 + +image-20230907144054936 + +# SpringBoot简介 + +- SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程 + - Spring的缺点 + - 依赖设置繁琐 + - 配置繁琐 + - SpringBoot的优点 + - 起步依赖(简化依赖配置) + - 自动配置(简化常用工程相关配置) + - 辅助功能(内置服务器,...) + +## parent + +有时候做项目,可能会导入两个相同的坐标,此时可以将它合并到一个文件中去,直接调用一个文件中的内容即可,但依然不太方便,springboot给出了优化的解决 + +image-20230907151058582 + +它在pom.xml文件中继承了一个父类 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 2.7.15 + + +``` + +而这里面又继承了一个父类 + +```xml + + org.springframework.boot + spring-boot-dependencies + 2.7.15 + +``` + +点进去查看后,发现里面有很多的依赖,这些依赖也有设置了对应的版本,当我们使用某一个依赖时,它就会自动通过场景启动器里的`spring-boot-starter-parent`中的`spring-boot-dependencies`中写入的对应依赖,来自动设置最合适的版本,这样就不会因为版本不同而出现问题 + + + +总结: + +1. 开发Springboot程序要继承spring-boot-starter-parent +2. spring-boot-starter-parent中定义了若干个依赖管理 +3. 继承parent模块可以**避免**多个依赖使用相同技术时出现**依赖**版本**冲突** +4. 继承parent的形式也可以采用引入依赖的形式实现效果 + + + +## starter + +- SpringBoot中常见项目名称,定义了当前项目使用的所有依赖坐标,以达到`减少依赖配置`的目的 + +实际开发 + +- 使用任意坐标时,仅书写GAV中的G和A,V(version)由SpringBoot提供,除非Springboot未提供对应版本的V +- 如发生坐标错误,再指定Version(要小心版本冲突) + +1. 开发SpringBoot程序需要导入坐标时,通常导入对应的starter +2. 每个不同的starter根据功能不同,通常包含多个依赖坐标 +3. 使用starter可以实现快速配置的效果,达到`简化配置`的目的 + + + +## 引导类 + +启动方式 + +```java +@SpringBootApplication +public class BootDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(BootDemoApplication.class, args); + } + +} +``` + +- SpringBoot的引导类是Boot工程的执行入口,运行main方法就可以启动项目 +- SpringBoot工程运行后初始化Spring容器,扫描**引导类所在包**加载bean + + + +## 内嵌Tomcat + +Tomcat其实是依靠一个依赖引入进来的 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +点入该依赖中 + +这里内嵌了一个 + +```xml + + org.springframework.boot + spring-boot-starter-tomcat + 2.7.15 + compile + +``` + +tomcat场景启动器的依赖,里面就有相关的tomcat配置 + +```xml +org.apache.tomcat.embed +tomcat-embed-core +9.0.79 +``` + +`tomcat-embed-core`这个是tomcat的核心嵌入,里面依赖了它,才会有tomcat服务器,而如果,你不想用tomcat,你可以这样做 + +```xml + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + +``` + + + +### 内置服务器 + +- tomcat(默认):apache出品,应用面广,负载了若干较重的组件 +- jetty:更轻量级,负载性能远不及tomcat +- undertow:负载性能**勉强**能跑赢tomcat + + + +总结: + +1. 内嵌Tomcat服务器是SpringBoot**辅助功能**之一 +2. 内嵌Tomcat工作原理是将Tomcat服务器作为对象运行,并将该对象交给**Spring容器管理** +3. 变更内嵌服务器思想是去除现有服务器,添加全新的服务器 + + + +# REST风格开发 + +## REST简介 + +REST(Representtational State Transfer),表现形式状态转换 + +- 传统风格资源描述形式 + - `http://localhost/user/getById?id=1` + - `http://localhost/user/saverUser` +- REST风格描述形式 + - `http://localhost/user/1` + - `http://localhost/user` + - 优点: + - 隐藏资源的访问行为,无法通过地址得知对资源是何种操作 + - 书写简化 + +按照REST风格访问资源时使用**行为动作**区分对资源进行了何种操作 + +- `http://localhost/users` 查询全部用户信息 GET (查询) +- `http://localhost/users/1` 查询指定用户信息 GET (查询) +- `http://localhost/users ` 添加用户信息 POST (新增/保存) +- `http://localhost/users` 修改用户信息 PUT (修改/更新) +- ``http://localhost/users/1` 删除用户信息 DELETE (删除) + +- 根据REST风格对资源进行访问称为RESTful + +注意事项: + +上述行为是约定方式,约定不是规范,可以打破,所以称为REST风格,而不是REST规范 + +描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,比如:users、books、accounts......... + +## 参数接收的注解介绍 + +`@RequestBody`:用于接收json数据 + +`@PathVariable`:用于接收url地址传参或表单传参 + +`@RequestParam`:用于接收路径参数,使用{参数名称}描述路径参数 + +应用: + +- 后期开发中,发送请求参数超过1个时,以json为主,@RequestBody应用较广 +- 如果发送非json格式数据,选用@RequestParam接收请求参数 +- 采用RESTful进行开发,当参数数量较少时,例如1个,可以采用@PathVariable接收请求路径变量,通常用于传递id值 + +## RSET快速开发 + +编写测试代码 + +引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@Slf4j +public class HelloController { + + // 查询全部 + @GetMapping("/hellos") + public void getAll(){ + System.out.println("getAll"); + } + + // 查询单个 + @GetMapping("/hellos/{id}") + public void getById(@PathVariable Integer id){ + System.out.println("getById"+id); + } + + // 更新 + @PutMapping("/hellos") + public void update(){ + System.out.println("更新"); + } + + // 保存 + @PostMapping("/hellos") + public void save(){ + System.out.println("保存"); + } + + // 删除 + // 更新 + @DeleteMapping("/hellos/{id}") + public void delete(@PathVariable Integer id){ + System.out.println("删除"); + } + +} +``` + +优化了之前冗余的代码,并且有了一个相当好的风格 + + + +# 基础配置 + +一般的文件配置可以在application.properties中修改 + +修改服务器端口 + +```properties +# 服务器端口配置 +server.port= 80 +# 修改banner,这里off表示是关闭了 +spring.main.banner-mode=off +# spring.banner.image.location=识别banner的图片位置 + +# 控制日志 + +# 日志级别 +logging.level.root = info +``` + +- SpringBoot中导入对应的starter后,提供对应配置属性 +- 书写SpringBoot配置采用关键字+提示形式书写 + + + +## 三种配置文件类型 + +SpringBoot提供了多种属性配置方式 + +- application.properties + + ```properties + server.port= 80 + ``` + +- application.yml + + ```yml + server: + port:81 + ``` + +- application.yaml + + ```yaml + server: + port:81 + ``` + +配置文件之间的加载优先级 + +- properties(最高) +- yml +- yaml(最低) + +不同配置文件中相同配置按照加载优先级相互覆盖,不同配置文件中的**不同配置**全部保留 + + + +## yml或yaml属性消失提示 + +按照步骤操作即可 + +image-20230908163529742 + +image-20230908163648643 + + + +## yaml数据样式 + +YAML(YAML Ain't Markup Language),一种数据序列化格式 + +优点: + +- 容易阅读 +- 容易与脚本语言交互 +- 以数据为核心,重数据轻格式 + +YAML文件拓展名 + +- .yml +- .yaml + +yaml语法规则 + +- 大小写敏感 +- 属性层级关系使用多行描述,每行结尾使用冒号结束 +- 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键) +- 属性值前添加空格(属性名与属性值之间使用冒号+空格作为分隔) + + + +核心规则:**数据前面要加空格与冒号隔开** + +字面值的表示方式 + +![image-20230908170754388](https://s2.loli.net/2023/09/08/29xoM4ClyJQrnHf.png) + +数组表示方式:在属性名书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据空格分隔 + +![image-20230908170943814](https://s2.loli.net/2023/09/08/9cDzQ8PYZxAlERI.png) + + + +## 读取yaml单一属性数据 + +application.yml + +```yaml +server: + port: 8080 + +count: "100" + +likes: + - game + - games + - games2 +``` + +HelloController + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +@RestController +@Slf4j +public class HelloController { + +// 读取yaml数据中的单一数据 + @Value("${server.port}") + private String port; + +// 读取多级数据 + @Value("${count}") + private String count; + +// 读取数组数据 + @Value("${likes[1]}") + private String game; + +// 读取对象数组(同样的道理):对象名[第几个对象的索引].字段 + @GetMapping("/test") + public String getPort(){ + System.out.println("端口号为+"+port); + System.out.println("统计+"+count); + System.out.println("game"+game); + return "测试完成"; + } + +} +``` + +使用@Value读取单个数据,属性名引用方式:${一级属性名.二级属性名......} + + + +## yaml文件中的变量引用 + +application.yml + +```yaml +baseDir: /var/lib + +# 使用${属性名}引用数据 +#tempDir: /var/lib/1 +tempDir: ${baseDir}/1 + +# 引用的值为数组的情况 +TestDir: + - test1: /var/lib/1 + - test2: /var/lib/2 + - test3: /var/lib/3 + +UseDir: ${TestDir[0].test1}/test +``` + +HelloController + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +@RestController +@Slf4j +public class HelloController { + + @Value("${tempDir}") + private String tempDir; + + @Value("${UseDir}") + private String useDir; + +// 读取对象数组(同样的道理):对象名[第几个对象的索引].字段 + @GetMapping("/test") + public String getPort(){ + System.out.println("temp"+tempDir); + System.out.println("useDir = " + useDir); + return "测试完成"; + } + +} +``` + +运行测试 + +![image-20230909090455825](https://s2.loli.net/2023/09/09/KRgYqyn8DdJ9HES.png) + +此时,这里也获取到了对应的数据 + +如果需要对代码转义,可以在上面加上字符串,下方的\t就会变为对应的转义字符 + +```yaml +UseDir: "${TestDir[0].test1}\test" +``` + +结果如下 + +```java +useDir = /var/lib/1 est +``` + + + +总结: + +- 在配置文件中可以使用属性名引用方式引用属性 +- 属性值如果出现转义字符,需要使用双引号包裹 + + + +## 读取yaml全部属性数据 + +创建一个`Environment`并将全部属性自动装配进去,通过`Environment.getProperty`来得到环境中对应的属性 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.*; + +@RestController +@Slf4j +public class HelloController { + + @Value("${tempDir}") + private String tempDir; + + @Value("${UseDir}") + private String useDir; + + @Autowired + private Environment environment; + + +// 读取对象数组(同样的道理):对象名[第几个对象的索引].字段 + @GetMapping("/test") + public String getPort(){ + System.out.println("temp"+tempDir); + System.out.println("useDir = " + useDir); + System.out.println("------------------"); + System.out.println(environment.getProperty("tempDir")); + System.out.println(environment.getProperty("UseDir")); + return "测试完成"; + } + +} +``` + + + +## 读取yaml引用数据类型数据 + +application.yaml + +```yaml +# 创建类,用于封装下面的数据 +# 由spring帮我们去加载数据到对象中 +# 使用时,直接从spring中获取信息使用 + +datasource: + driver: com.mysql.jdbc.Driver + url: jdbc:mysql//localhost:db + username: root + password: root +``` + +模型类 + +```java +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +// 定义数据模型,封装yaml文件中对应的数据 +// 定义为spring管控的bean +@Data +@Component +// 指定加载的数据 +// @ConfigurationProperties("写入对应封装的对象名") +@ConfigurationProperties(prefix = "datasource") +public class MyDataSource { + private String driver; + private String url; + + private String username; + + private String password; +} +``` + +```java +import fun.eastwind.studyboot.MyDataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@Slf4j +public class HelloController { + + @Autowired + private MyDataSource myDataSource; + + @GetMapping("/test") + public String getPort(){ + System.out.println(myDataSource); + return "测试完成"; + } + +} +``` + +测试得出结果 + +![image-20230909095906918](https://s2.loli.net/2023/09/09/a1JblMtNzgoyxSd.png) + +# 整合第三方技术 + +## 整合Junit + +步骤: + +1、导入测试对应的starter + +```xml + + org.springframework.boot + spring-boot-starter-test + test + +``` + +2、测试类使用@SpringBootTest修饰(一般都是默认自带的) + +```java +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BootDemo2ApplicationTests { + + @Test + void contextLoads() { + } + +} +``` + +3、使用自动装配(@Autowired)的形式添加要测试的对象 + + + +如果将测试类切换到其他的包下,不在初始的包下,会报错,原因是与引导类不在同一个包下 + +此时你需要在@SpringBootTest上添加 + +```java +@SpringBootTest(classes = BootDemo2Application.class) +``` + +指定对应的引导类,这样测试类就可以找到对应的引导类了 + +或者还有一种方法,与上面的方法是一样的效果 + +```java +@ContextConfiguration(classes = BootDemo2Application.class) +``` + + + +### 注解学习 + +名称:@SpringBootTest + +类型:测试类注解 + +位置:测试类定义上方 + +作用:设置Junit加载的SpringBoot启动类 + +相关属性: + +- classes:设置SpringBoot启动类 + +**注意:如果测试类在SpringBoot启动类的包或子包中,可以省略启动类的设置,也就是省略classes的设定** + + + +## 整合MyBatis + +- 核心配置:数据库连接相关信息(连什么?连谁?什么权限) +- 映射配置:SQL映射(XML/注解) + +创建一个Spring Initializr项目 + +并勾选需要的MyBatis相关依赖 + +image-20230909130337593 + +在application.yml中编写对应配置 + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + url: jdbc:mysql://localhost:3306/study +``` + +这里的配置改成自己的 +创建实体类 + +```java +public class User { + private Integer id; + private String name; + private String sex; + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + ", sex='" + sex + '\'' + + '}'; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} +``` + +编写Mapper + +```java +import fun.eastwind.domain.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface UserMapper { + + @Select("select * from studytable where id = #{id}") + public User getUser(Integer id); +} +``` + +在测试类中进行测试 + +```java +import fun.eastwind.domain.User; +import fun.eastwind.mapper.UserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + User user = userMapper.getUser(1); + System.out.println("user = " + user); + } + +} +``` + +测试后,发现没有问题,说明整合完成 + +### 整合MyBatis的小问题 + +1. MySQL8.x驱动强制要求设置时区 + - 修改url,添加serverTimezone设定 + - 修改MYSQL数据库配置(略) +2. 驱动类过时,提醒更换为com.mysql.cj.jdbc.Driver + + + +## 整合MyBatisPlus + +MyBtais-Plus与MyBatis的区别 + +- 导入坐标不同 +- 数据层实现简化 + +由于MyBatisPlus未被Spring收录,这里只引入一个mysql的坐标 + +mybatisplus需要自己引入对应的坐标 + +```xml + + + com.baomidou + mybatis-plus-boot-starter + 3.5.2 + +``` + +导入后,你可以将spring-boot-starter的依赖删除,因为mybatis-plus-boot-starter是包含着它的 + +```xml + + org.springframework.boot + spring-boot-starter + +``` + +application.yml + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + url: jdbc:mysql://localhost:3306/study +``` + +实体类 + +```java +public class User { + private Integer id; + private String name; + private String sex; + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + ", sex='" + sex + '\'' + + '}'; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} +``` + +UserMapper + +```java +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import fun.eastwind.demo1.domain.User; +import org.apache.ibatis.annotations.Mapper; + + +@Mapper +public interface UserMapper extends BaseMapper { + +} +``` + +测试类 + +```java +import fun.eastwind.demo1.domain.User; +import fun.eastwind.demo1.mapper.UserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Demo1ApplicationTests { + + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + User user = userMapper.selectById(1); + System.out.println("user = " + user); + } + +} +``` + +运行后,会出现一个错误`Cause: java.sql.SQLSyntaxErrorException: Table 'study.user' doesn't exist` + +说是表不存在,为什么呢,因为MP(mybatisplus)会直接将实体类的名称小写后,作为表名 + +解决方法:为实体类直接添加表名 + +在实体类上方添加注解`@TableName("studytable")`,这样就可以指定表名了 + +此时,就查询得到结果了 + +`user = User{id=1, name='张三', sex='男'}` + + + +## 整合Druid + +druid的依赖包需要自己导入,在新建项目时勾选mybatis和mysql,因为druid是给数据库使用的 + +```xml + + + com.alibaba + druid-spring-boot-starter + 1.2.8 + +``` + +配置与实体类,同整合mybatis与mybatisplus时相同 + +这里我使用的mybatisplus,所以引入一下依赖 + +```xml + + + com.baomidou + mybatis-plus-boot-starter + 3.5.2 + +``` + +配置方式: + +1、直接在application.yml中编写 + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + url: jdbc:mysql://localhost:3306/study + type: com.alibaba.druid.pool.DruidDataSource +``` + +2、推荐使用第二种,虽然两种都行 + +```yaml +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + url: jdbc:mysql://localhost:3306/study +``` + +测试类 + +```java +import fun.eastwind.demo2.mapper.UserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Demo2ApplicationTests { + + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + System.out.println("userMapper.selectById(1) = " + userMapper.selectById(1)); + } + +} +``` + +查看打印结果 + +这里显示初始化数据源 + +![image-20230909143701462](https://s2.loli.net/2023/09/09/2ZLrncsFiwSQvpt.png) + + + +# SSMP整合 + +SSMP(spring、springmvc、mybatisplus) + + + +## 整合分析 + +- 实体类开发:使用Lombok快速制作实体类 +- Dao开发:整合MyBatisplus,制作数据层测试类 +- Service开发:基于MyBatisplus进行增量开发,制作业务层测试类 +- Controller开发:基于Restful开发,使用PostMan测试接口功能,前后端开发协议制作 +- 页面开发:基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理 + - 列表、新增、修改、删除、分页、查询 +- 项目异常处理 +- 按条件查询:页面功能调整、Controller修正功能、Service修正功能 + + + +## 模块创建 + +导入依赖 + +```xml + + + + com.alibaba + druid-spring-boot-starter + 1.2.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.2 + + +``` + +修改配置 + +application.yml + +```yaml +server: + port: 80 +``` + + + +## 实体类开发 + +```java +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; + +@TableName("studytable") +@Data +@AllArgsConstructor +public class User { + private String name; + private String sex; +} +``` + + + +## 数据层标准开发 + +修改配置 + +```yaml +server: + port: 80 + +spring: + datasource: + druid: + password: 123456 + username: root + url: jdbc:mysql://localhost:3306/study + driver-class-name: com.mysql.cj.jdbc.Driver +``` + +编写Mapper接口 + +```java +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import fun.eastwind.module_practice.domain.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { + +} +``` + +编写测试类 + +```java +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.mapper.UserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ModulePracticeApplicationTests { + + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + userMapper.insert(new User("test","女")); + } + +} +``` + +测试一下,没啥问题 + +为MP添加一下id自增策略,不然后面可能会有问题,因为表中的id采用的是自增策略 + +```yaml +mybatis-plus: + global-config: + db-config: + id-type: auto +``` + + + +## 开启MP的运行日志 + +```yaml +mybatis-plus: + global-config: + db-config: + id-type: auto + configuration: + # 标准输出 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +``` + +再次运行测试类,就可以在控制台看到打印的日志信息了 + + + +## 分页 + +创建拦截器 + +```java +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MPConfig { + + // 创建一个mybatisPlus的拦截器 + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor(){ + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 添加分页的拦截器 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + // 返回mybatisPlus的拦截器 + return interceptor; + } + +} +``` + +编写测试类 + +```java +@Test +void test2() { + // page对象需要当前页和页码 + IPage page = new Page(1,5); + // selectPage里需要一个page对象 + userMapper.selectPage(page, null); +} +``` + +image-20230909162052949 + +分页的方法 + +image-20230909161834629 + +## 条件查询 + +使用QueryWrapper对象封装查询条件,推荐使用LambdaQueryWrapper对象,所有查询操作封装成方法调用 + +编写代码 + +```java +@Test +void test3() { + // QueryWrapper是用来进行查询的 + QueryWrapper queryWrapper = new QueryWrapper<>(); + // like是进行模糊匹配 + queryWrapper.like("name","张"); + List userList = userMapper.selectList(queryWrapper); +} +``` + +这里可以很明显的看到下面的语句是模糊查询 + +image-20230909163349509 + +也可以使用另一种 + +```java +@Test +void test3() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 提升了安全性,这样不会轻易的写错字段名之类的 + queryWrapper.like(User::getName,"张"); + List userList = userMapper.selectList(queryWrapper); +} +``` + +一般情况下,数据是由外界传递的,万一出现状况,数据传递出来一个Null,此时,就会出现问题,像这样的like匹配,就会将null传递过来,并由null进行模糊匹配(%null%) + +queryWrapper这里可以添加一个condition(条件),在这个条件中就可以对null值进行判断了 + +```java +@Test +void test3() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + String name = "张"; + // 提升了安全性,这样不会轻易的写错字段名之类的 + // 当name不为空时,才进行模糊匹配 + queryWrapper.like(name != null,User::getName,name); + List userList = userMapper.selectList(queryWrapper); +} +``` + + + +## 业务层开发 + +- Service层接口定义与数据层接口定义具有较大区别,不要混用 + - selectByUserNameAndPassword(String username,String password) + - login(String username,String password) + +UserService + +```java +import fun.eastwind.module_practice.domain.User; + +import java.util.List; + +public interface UserService { + Boolean save(User user); + Boolean update(User user); + Boolean delete(Integer id); + User selectById(Integer id); + List selectAll(); +} +``` + +进行数据库增删改操作时,会返回一个数值,如果数值为整数,说明操作成功,否则失败 + +```java +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.mapper.UserMapper; +import fun.eastwind.module_practice.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + + @Override + public Boolean save(User user) { + return userMapper.insert(user) > 0; + } + + @Override + public Boolean update(User user) { + return userMapper.updateById(user) > 0; + } + + @Override + public Boolean delete(Integer id) { + return userMapper.deleteById(id) > 0; + } + + @Override + public User selectById(Integer id) { + return userMapper.selectById(id); + } + + @Override + public List selectAll() { + return userMapper.selectList(null); + } +} +``` + +测试代码 + +```java +import fun.eastwind.module_practice.domain.User; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class UserServiceTest { + + @Autowired + private UserService userService; + + @Test + public void test1(){ + userService.save(new User("zhangsan","男")); + } + +} +``` + +没啥问题 + +刚刚少写了一个分页,再到service中进行编写 +UserService + +```java +IPage page(int current, int pageSize); +``` + +UserServiceImpl + +```java +@Override +public IPage page(int current, int pageSize) { + IPage iPage = new Page(current,pageSize); + return userMapper.selectPage(iPage,null); +} +``` + +测试一下 + +```java +@Test +public void test2(){ + userService.page(1,5); +} +``` + + image-20230910130738572 + +这里对UserService的方法说明一下 + +```java +@Override + public Boolean save(User user) { + return userMapper.insert(user) > 0; + } +``` + +为什么是判断是否大于0呢,因为这里操作成功后都是返回正值,而失败都是负值,测试一下,来看看效果 + +![image-20230910131130122](https://s2.loli.net/2023/09/10/IKFOVTXPfUcwGqH.png) + +测试service可能不太明确,直接拿mapper来进行一个测试 + +```java +@Autowired +private UserMapper userMapper; + +@Test +public void test3(){ + System.out.println("--------------------------------"); + System.out.println(userMapper.insert(new User("zhangsan", "男"))); + System.out.println("--------------------------------"); +} +``` + +![image-20230910131317212](https://s2.loli.net/2023/09/10/x6O9pbQJnvqzG4Y.png) + +这里发现成功后,会返回正值1,插入失败,通常会返回-1 + + + +## 业务层快速开发 + +快速开发方案 + +- 使用MyBatisPlus提供有业务层通用接口(IService)与业务层通用实现类(ServiceImpl) +- 使用通用类基础上做功能重载或功能追加 +- 注意重载时不要覆盖原始操作,避免原始提供的功能丢失 + +IUserService + +```java +import com.baomidou.mybatisplus.extension.service.IService; +import fun.eastwind.module_practice.domain.User; + +public interface IUserService extends IService { +} +``` + +IUserServiceImpl + +```java +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.mapper.UserMapper; +import fun.eastwind.module_practice.service.IUserService; +import org.springframework.stereotype.Service; + +@Service +public class IUserServiceImpl extends ServiceImpl implements IUserService { +} +``` + +测试代码 + +```java +import fun.eastwind.module_practice.domain.User; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +@SpringBootTest +public class IUserServiceTest { + + @Autowired + private IUserService userService; + + @Test + public void test1(){ + List list = userService.list(); + for (User user : list) { + System.out.println(user); + } + } + +} +``` + +没啥问题 + +## 表现层标准开发 + +- 基于Restful进行表现层接口开发 +- 使用Postman测试表现层接口功能 + +UserController + +```java +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.service.IUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private IUserService iUserService; + + @GetMapping + public List getUsers() { + return iUserService.list(); + } + +} +``` + +在postman中测试一下,得到了查询的数据 + +image-20230910140359295 + +接着补全一下其他的代码,方法就不测试了,效果都差不多 + +```java +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.service.IUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private IUserService iUserService; + + @GetMapping + public List getUsers() { + return iUserService.list(); + } + + @PostMapping + public Boolean save(@RequestBody User user) { + return iUserService.save(user); + } + + @PutMapping + public Boolean update(@RequestBody User user){ + return iUserService.updateById(user); + } + + @DeleteMapping("{id}") + public Boolean delete(@PathVariable Integer id){ + return iUserService.removeById(id); + } + + @GetMapping("{id}") + public User getUser(@PathVariable Integer id){ + return iUserService.getById(id); + } + +} +``` + + + +总结: + +1、基于Restful制作表现层接口 + +- 新增:POST +- 删除:DELETE +- 修改:PUT +- 查询:GET + +2、接收参数 + +- 实体数据:@RequestBody +- 路径变量:@PathVariable + +## 表现层消息的一致性处理 + +设置表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为**前后端数据协议** + +Result(模型类) + +模型类中的flag表示是否成功,data接收数据 + +```java +import lombok.Data; + +@Data +public class Result { + private Boolean flag; + private Object data; + +} +``` + +但是这样写,每次都需要set里面的flag和data + +所以,进行一下优化 + +```java +import lombok.Data; + +@Data +public class Result { + private Boolean flag; + private Object data; + + public Result() { + } + + public Result(Boolean flag, Object data) { + this.flag = flag; + this.data = data; + } + + public Result(Boolean flag) { + this.flag = flag; + } +} +``` + +编写控制类统一结果返回 + +```java +import fun.eastwind.module_practice.domain.User; +import fun.eastwind.module_practice.service.IUserService; +import fun.eastwind.module_practice.utils.Result; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private IUserService iUserService; + + @GetMapping + public Result getUsers() { + return new Result(true,iUserService.list()); + } + + @PostMapping + public Result save(@RequestBody User user) { + return new Result(iUserService.save(user)); + } + + @PutMapping + public Result update(@RequestBody User user){ + return new Result(iUserService.updateById(user)); + } + + @DeleteMapping("{id}") + public Result delete(@PathVariable Integer id){ + return new Result(iUserService.removeById(id)); + } + + @GetMapping("{id}") + public Result getUser(@PathVariable Integer id){ + return new Result(true,iUserService.getById(id)); + } + +} +``` + +## 统一异常处理 + +创建一个异常处理器 + +```java +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class AllException { + + @ExceptionHandler + public Result doException(Exception ex){ + ex.printStackTrace(); + return new Result("出现异常,请联系程序猿小哥为您服务"); + } + +} +``` + +修改之前的Result对象(一致性处理的对象),出现异常被异常处理器拦截后,肯定会有提示信息,所以需要新增一个msg(消息) + +```java +import lombok.Data; + +@Data +public class Result { + private Boolean flag; + private Object data; + + private String msg; + + public Result(String msg) { + this.flag = false; + this.msg = msg; + } + + public Result() { + } + + public Result(Boolean flag, Object data) { + this.flag = flag; + this.data = data; + } + + public Result(Boolean flag) { + this.flag = flag; + } +} +``` + +报个异常测试一下 + +![image-20230910165417121](https://s2.loli.net/2023/09/10/YT3BSvctsIhGKAD.png) + +如果想要指定某些异常,可以在`@ExceptionHandler`后添加对应的异常类 + +```java +@ExceptionHandler(Exception.class) +public Result doException(Exception ex){ + ex.printStackTrace(); + return new Result("出现异常,请联系程序猿小哥为您服务"); +} +``` + + + +总结: + +1. 使用注解@RestControllerAdvice定义SpringMVC异常处理器来处理异常 +2. 异常处理器必须被扫描加载,否则无法生效 +3. 表现层返回结果的模型类中添加消息属性用来传递消息到页面中 + + + +## 基础篇完结 + +- pom.xml:配置起步依赖 +- application.yml:设置数据源、端口、框架技术相关配置等 +- mapper:继承BaseMapper、设置@Mapper注解 +- mapper测试类 +- service:调用数据层接口或MyBatis-Plus提供的接口快速开发 +- service测试类 +- controller:基于Restful开发,使用Postman测试跑通功能 +- 页面:放置在resources目录下的static目录中 + + + diff --git "a/public/markdowns/SpringBoot2\351\253\230\347\272\247\347\257\207.md" "b/public/markdowns/SpringBoot2\351\253\230\347\272\247\347\257\207.md" new file mode 100644 index 0000000..bc9cdcf --- /dev/null +++ "b/public/markdowns/SpringBoot2\351\253\230\347\272\247\347\257\207.md" @@ -0,0 +1,5020 @@ +--- +title: SpringBoot2高级篇 +tags: + - SpringBoot2 +categories: + - SpringBoot +description: SpringBoot2高级篇 +abbrlink: '28014337' +--- +# 运维篇 + +## 打包与运行 + +### 程序的打包与运行(Windows版) + +如何打包,打开IDEA,在右侧菜单栏,找到Maven,这里以demo2为例 + +image-20230911103125916 + +package是打包的命令,双击执行,一般打包之前,如果有对应的target目录,都会先clean一下。清除之前打包的内容,然后再打包 + +![image-20230911103408433](https://s2.loli.net/2023/09/11/iPHBdADcbZxOXCW.png) + +在demo2下,我们看到一个target目录,我们需要clean先清除它,再package打包 + +完成后,打开target目录,就能看到对应的jar包 + +image-20230911103522880 + +在jar包目录下运行cmd,并输入`java -jar 你的jar包名称`,就可以启动对应的jar包了 + +启动后,访问之前写的Controller类的地址,就能得到对应的数据了 + + + +有时候打包不需要将测试打进去,所以我们可以在IDEA上修改 + +image-20230911104758405 + +此时test就被禁用了 + +总结: + +1、对SpringBoot项目打包(执行Maven构建指令) + +```shell +mvn package +``` + +2、运行项目(执行启动指令) + +```shell +java -jar 对应的jar包名 +``` + +注意事项: + +jar支持命令行启动需要依赖maven插件支持,请确认打包时是否具有SpringBoot对应的maven插件 + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +#### 端口占用的情况 +有时候运行程序,端口可能被占用,所以需要修改或删除端口,下面是一组端口的命令 + +```shell + +# 查询端口 +netstat -ano +# 查询指定端口 +netstat -ano | findstr "端口号" +# 根据进程PID查询进程名称 +tasklist | findstr "进程PID号" +# 根据PID杀死任务 +taskkill /F /PID "进程PID号" +# 根据进程名称杀死任务 +taskkill -f -t -im "进程名称" +``` + +### 程序运行(Linux版) + +创建一个虚拟机,环境任意 + +进入虚拟机并查看其IP地址 + +输入`ifconfig`,可以查看IP,记住这个IP,然后打开虚拟机的连接工具,任意,这里我选择的是Xshell + +进入/usr/local文件夹下,并创建一个文件夹app,进入app + +```shell +cd /usr/local/ +mkdir app +cd app +``` + +将之前打包的文件上传到该目录下,用虚拟机的连接工具进行上传 + +使用java -jar jar包名称运行 + +刚刚的虚拟机ip,复制下来,然后输入`http://你的ip地址/对应的地址`,此时运行后没有数据,而且一直报错 + +原因是:linux系统下,没有对应的mysql库结构,导致数据库查询不到数据 + +需要在linux系统中安装一个mysql,安装就不做演示了 + +使用Navicat来管理linux的数据库比较方便,所以与linux数据库建立一下连接 + +然后将之前学习用的数据,转为sql文件,并在linux数据库上运行 + +运行完成后,在linux下运行对应的jar包,再次访问地址,此时就可以正常的返回数据了 + +一般不这样启动,一般在后台启动 + +```shell +nohup java -jar 对应的jar包名 > server.log 2>&1 & +``` + +这段代码是在Linux或Unix系统中使用的一条命令,用于在后台启动一个Java程序,并将输出重定向到一个名为'server.log'的文件中。具体解释如下: + +- 'nohup':这是一个Unix命令,用于在你退出系统/关闭终端后继续运行相应的进程。 +- 'java -jar 对应的jar包名':这是用Java运行一个jar包的命令。'对应的jar包名'应该被替换为你要运行的jar包的实际文件名。 +- '> server.log':这部分将标准输出重定向到一个名为'server.log'的文件中。如果文件不存在,那么命令会创建它。如果文件已经存在,那么命令会覆盖它。 +- '2>&1':这部分将标准错误(2)重定向到标准输出(1)。这意味着所有的错误消息也会被写入'server.log'文件。 +- '&':这个符号让命令在后台运行。 + +此时,就可以在后台启动,而信息都输出到了server.log中 + +## 配置高级 + +### 临时属性设置 + +在命令行中,这样来进行修改临时属性 + +``` +java -jar 对应的jar包名 --server.port=8080 +``` + +如果属性不止一个 + +``` +java -jar 对应的jar包名 --server.port=8080 --需要修改的属性 +``` + +这里的属性与前面的格式是一致的 + +操作步骤如下: + +1、使用jar命令启动SpringBoot工程时可以使用临时属性替换配置文件中的属性 + +2、临时属性添加方式:java -jar 工程名.jar --属性名=值 + +3、多个临时属性之间使用空格分割 + +4、临时属性必须是当前boot工程支持的属性,否则设置无效 + + + +### 临时属性如何在IDEA中测试 + +点击Edit Configurations + +image-20230911202729488 + +如果没有Program arguments,可以按照以下方法打开并添加 + +image-20230911203942521 + +image-20230911204047210 + +image-20230911203848311 + +这里修改了临时属性的端口号,测试一下 + +![image-20230911204156301](https://s2.loli.net/2023/09/11/GLifk3KxSs86OrY.png) + +控制台可以看到,端口被修改为8081了 + +如果想获取临时属性的参数,可以在启动程序下的args进行打印 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ModulePracticeApplication { + + public static void main(String[] args) { + for (String arg : args) { + System.out.println(arg); + } + SpringApplication.run(ModulePracticeApplication.class, args); + } + +} +``` + +![image-20230911204518626](https://s2.loli.net/2023/09/11/wKYrhDC8Pd64VJI.png) + +此时,args中就获取到对应的参数了 + +如果想在运行时进行端口的修改,可以在运行主程序前,给args赋值,但是需要在主程序上带上args的参数 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ModulePracticeApplication { + + public static void main(String[] args) { + args = new String[1]; + args[0] = "--server.port=8082"; + SpringApplication.run(ModulePracticeApplication.class, args); + } + +} +``` + +可以在启动boot程序时断开读取外部临时配置对应的入口,也就是去掉读取外部参数的形参,可以提高安全性 + +### 配置文件4级分类 + +SpringBoot中4级配置文件 + +1级:file:config/application.yml【最高】 + +2级:file:application.yml + +3级:classpath:config/application.yml + +4级:classpath:application.yml【最低】 + +作用: + +- 1级与2级留作系统打包后设置通用属性,1级常用于运维经理进行线上整体项目部署方案调控 +- 3级与4级用于系统开发阶段设置通用属性,3级常用于项目经理进行整体项目属性调控 + +总结: + +- 配置文件分为4种 + - 项目类路径配置文件:服务于开发人员本机开发与测试 + - 项目类路径config目录中配置文件:服务于项目经理整体调控 + - 工程路径配置文件:服务于运维人员配置涉密线上环境 + - 工程路径config目录中配置文件:服务于运维经理整体调控 +- 多层级配置文件间的属性采用叠加并覆盖的形式作用于程序 + +### 自定义配置属性 + +新建一个模块,添加Spring Web依赖 + +随意写一个控制类 + +```java +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test") + public void test(){ + System.out.println("测试已运行"); + } + + +} +``` + +将配置文件改成任意的名字,并写入`server.port = 80`,看看是否生效 + +![image-20230912081432416](https://s2.loli.net/2023/09/12/m9LyRjg1szIMTo4.png) + +此时发现,并没有生效,如果我们要它生效该怎么办呢? + +在之前配置临时属性的位置,添加一条 + +``` +--spring.config.name=你修改后的文件名(不需要加后缀名) +``` + +重新运行并测试 + +![image-20230912082115979](https://s2.loli.net/2023/09/12/aKdAk9i6OlMLXvh.png) + +此时临时配置就生效了 + +修改刚刚的临时配置文件,properties文件的后缀为yml文件 + +```yaml +server: + port: 83 +``` + +![image-20230912082409369](https://s2.loli.net/2023/09/12/GHXYhp5a7VefBNO.png) + +yml也是有效存在的 + +再次回到之前配置临时属性的位置,修改此处的代码为 + +``` +--spring.config.location=classpath:/你修改后的文件名.yml +``` + +修改yaml文件中的内容为 + +```yaml +server: + port: 88 +``` + +运行并测试 + +![image-20230912082754430](https://s2.loli.net/2023/09/12/2RhHWgoKIAuOB5T.png) + + + +**自定义配置文件-重要说明** + +- 单服务器项目:使用自定义配置文件需求较低 +- 多服务器项目:使用自定义配置文件需求较高,将所有配置放置在一个目录中,统一管理 +- 基于SpringCloud技术,所有的服务器将不再设置配置文件,而是通过配置中心进行设定,动态加载配置信息 + + + +## 多环境开发 + +什么是多环境呢? + +环境分为三个:开发环境、测试环境、生产环境 + +### yaml版 + +在application.yml文件中进行配置,如果有多个不同环境的配置,可以使用三个`-`分割开 + +在yml文件中,配置了三个环境,并且每个环境都配置了不同的端口 + +```yaml +# 应用环境 +# 公共配置 +spring: + profiles: + active: pro + +--- +# 设置环境 +# 生产环境 +# 为三个环境起个名字 +server: + port: 80 +spring: + profiles: pro + +--- +# 开发环境 +spring: + profiles: dev +server: + port: 81 + + +--- +# 测试环境 +spring: + profiles: test +server: + port: 82 +``` + +运行测试前记得把之前在临时属性处的配置删除 + +![image-20230912085143625](https://s2.loli.net/2023/09/12/opgWucQ72VxSXkn.png) + +这里使用的是pro进行的测试,也可以切换不同的生产环境进行测试 + +公共配置里写的是三个环境都需要的配置 + +我们在文件中可能会看到profiles上是有一条删除线的,这说明该方法是过时的 + +```yaml +spring: + profiles: +``` + +怎样让它不过时呢,它有一个标准格式 + +```yaml +spring: + config: + activate: + on-profile: test +``` + +但是这个比较长,但又是标准格式 + +没啥影响,只是换了个名,都能用 + +### yaml安全版 + +将yml文件复制三份并粘贴在与yml文件同目录的位置,分别命名为 + +- application-pro.yml +- application-dev.yml +- application-test.yml + +并对其中对应的生产环境修改为对应的环境内容 + +application.yml + +```yaml +# 应用环境 +# 公共配置 +spring: + profiles: + active: pro +``` + +application-pro.yml + +```yaml +server: + port: 80 +spring: + profiles: pro +``` + +application-dev.yml + +```yaml +spring: + profiles: dev +server: + port: 81 +``` + +application-test.yml + +```yaml +server: + port: 82 +spring: + config: + activate: + on-profile: test +``` + +运行测试,结果与yaml版中的一致 + +1. **使用独立配置文件定义环境属性** +2. **独立配置文件便于线上系统维护更新并保障系统安全性** + +### properties版 + +properties与yml几乎一模一样,就语法不同,然后改了个文件后缀名为properties即可,这里就不进行讲述了 + +### 多环境开发独立配置文件书写技巧 + +- 根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下 + - application.devDB.yml + - application.devRedis.yml + - application.devMVC.yml +- 使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔 + +```yaml +spring: + profiles: + active: dev + include: devDB,devRedis,devMVC +``` + +新建不同的配置文件,并改为对应需求的名字 + +application.yml + +```yaml +spring: + profiles: + active: dev +``` + +application-dev.yml + +```yaml +server: + port: 8088 +``` + +application-devDB.yml + +``` +# 数据库相关配置 +``` + +application-devMVC.yml + +```yaml +server: + servlet: + context-path: /test +``` + +以此类推,这里就不写了 + +测试启动一下 + +![image-20230912101052138](https://s2.loli.net/2023/09/12/nhDwzAZeTac84OC.png) + +这里的8088生效了,但是application-devMVC.yml没有生效 + +原因是:**在加载配置时,并没有包含其他的配置文件** + +application.yml + +```yaml +spring: + profiles: + active: dev + include: devMVC,devDB +``` + +将配置文件包含在内,使用include来进行包含,包含名称为文件--后的名称 + +image-20230912101546943 + +此时,就将这些配置包含进来了,但在这里面,似乎有一个配置的顺序,我们来测试一下看看谁先谁后 +在application-devDB.yml添加 + +```yaml +server: + port: 8089 +``` + +在application-devMVC.yml添加 + +```yaml +server: + port: 8090 +``` + +运行,并进行测试 + +![image-20230912101940193](https://s2.loli.net/2023/09/12/exPrROVC6vNwQzK.png) + +原因是:dev文件是最后一个加载的,所以端口号是8088,注释掉dev文件中的代码,我们再看其他两个文件,查看是谁大谁小 + +![image-20230912102115571](https://s2.loli.net/2023/09/12/QqzsbhZtpod9KjY.png) + +此时是8089,因为devDB是最后一个加载的,覆盖了前面的 + +得到一个结论,后加载的会覆盖先加载的,而include中的顺序会决定谁先加载,而主启动的dev永远是最后一个,无论前面加载了什么,最后的dev都会将其覆盖 + +在`Spring2.4`后,使用group替代了include属性,因为include属性有限制性,当active为dev时,include只能使用dev的内容,而为其他时,include的都要被替换为其他的内容,很麻烦 + +```yaml +spring: + profiles: + active: dev + group: + # 当你启动环境后,启动的就不是dev,而是dev对应的组的环境,其他也是一样,不同的active激活不同的组 + "dev": devMVC,devDB + "pro": proMVC,proDB +``` + +启动查看一下效果 + +![image-20230912103024752](https://s2.loli.net/2023/09/12/NhaQRm4JwSkVZEL.png) + +**启动后发现,顺序变了,原来dev是在最后的,顺序变了,这里要注意,是个坑** + +**当主环境dev与其他环境有相同属性时,主环境属性生效;其他环境中有相同属性时,最后加载的环境属性生效** + + + +### 多环境开发控制 + +当maven和boot冲突时,应该让boot使用maven中的配置环境,因为boot是依赖于maven的 + +在pom.xml文件中设置多环境 + +```xml + + + + eastwind_dev + + dev + + + + true + + + + eastwind_pro + + pro + + + + +``` + +在application.yml中引用并读取对应的属性 + +```yaml +spring: + profiles: + # 读取xml文件中对应的值 + active: @profile.active@ + group: + # 当你启动环境后,启动的就不是dev,而是dev对应的组的环境,其他也是一样,不同的active激活不同的组 + "dev": devMVC,devDB + "pro": proMVC,proDB +``` + +使用maven进行打包,打包完成后,用解压工具查看里面的application.yml文件 + +路径为:/BOOT-INF/classesapplication.yml + +image-20230912110020922 + +查看后,这里的active已经被初始化成功了,此时就相当于用maven控制了boot程序 + +### 多环境开发控制bug + +有时候可能会遇到一个bug,就是当运行时,会一直是一个环境,而且切换环境后或是clean清除后,依然是之前的环境,启动刚刚的环境,运行后,会是dev的环境,此时我们切换为pro的环境 + +此时,还是dev,`The following 3 profiles are active: "dev", "devMVC", "devDB"` + +解决方法:![image-20230912120947849](https://s2.loli.net/2023/09/12/XkWriGCNRVmTMDI.png) + +点击一下compile(编译),再次运行 + +此时的环境,更新为了`pro`,`The following 3 profiles are active: "pro", "proMVC", "proDB"` + + + +总结: + +- 当Maven与SpringBoot同时对多环境进行控制时,以Maven为主,SpringBoot使用@..@占位符读取Maven对应的配置属性值 +- 基于SpringBoot读取Maven配置属性的前提下,如果在IDEA中测试工程时,pom.xml每次更新需要手动compile方可生效 + + + +## 日志 + +### 日志基础 + +日志(log)作用 + +- 编程期调试代码 +- 运营期记录信息 + - 记录日常运营重要信息(峰值流量,平均响应时长......) + - 记录应用报错信息(错误堆栈) + - 记录运维过程数据(扩容、宕机、报警......) + +编写日志 + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + // 创建记录日志的对象 + private static final Logger log = LoggerFactory.getLogger(TestController.class); + + @GetMapping("/test") + public void test(){ + // 级别过低,一般没人使用 + // log.trace("堆栈信息"); + // debug调试信息 + log.debug("debug"); + // info记录运行信息 + log.info("运行信息"); + // warn记录警告信息 + log.warn("警告信息"); + // error记录报错信息 + log.error("报错信息"); + } + + +} +``` + +运行代码 + +![image-20230912131415711](https://s2.loli.net/2023/09/12/7CSgfArbNF6OBkG.png) + +此时发现,这里只有三行,并没有debug的提示信息,是因为系统默认启动后是info信息,所以看到的都是info及以上的提示信息,如果需要查看debug的信息,可以在`application.yml`中进行修改 + +```yaml +debug: true +``` + +不推荐使用上面这种形式 + +一般情况下都是这样使用的 + +```yaml +logging: + level: + root: debug +``` + +第一种呢是开启debug模式,输出调试信息,常用于检查系统运行状况 + +第二种是设置日志级别,root表示根节点,即整体应用日志级别 + +第一种只是输出与springboot有关的信息,而第二种的是所有包的信息 + +#### 设置单一包下的日志 + +```yaml +logging: + group: + eastwind: fun.eastwind.demo3.controller,fun.eastwind.demo3.service,fun.eastwind.demo3.mapper + level: + root: debug + # 设置某个包的日志级别(其他都是debug级别,而声明的包是info级别) + fun.eastwind.demo3.controller: info + # 设置分组,对某个组设置日志级别(这里对eastwind组内的包级别设置为info) + eastwind: info +``` + +#### 日志级别 + +- TRACE:运行堆栈信息,使用率低 +- DEBUG:程序员调试代码使用 +- INFO:记录运维过程数据 +- WARN:记录运维过程报警数据 +- ERROR:记录错误堆栈信息 +- FATAL:灾难信息,合并计入ERROR + + + +### 快速创建日志对象 + +创建一个日志对象BaseClass + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaseClass { + private Class clazz; + public static Logger log; + + public BaseClass(){ + // 从构造器中获取当前类 + clazz = this.getClass(); + // 传递到日志工厂中使用 + log = LoggerFactory.getLogger(clazz); + } + +} +``` + +测试,运行没啥毛病 + +你也可以引入lombok的包,里面有个注解`@Slf4j`,就是用于日志内容的 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +public class TestController{ + + @GetMapping("/test") + public void test(){ + // 级别过低,一般没人使用 + // log.trace("堆栈信息"); + // debug调试信息 + log.debug("debug"); + // info记录运行信息 + log.info("运行信息"); + // warn记录警告信息 + log.warn("警告信息"); + // error记录报错信息 + log.error("报错信息"); + } + + +} +``` + + + +### 日志输出格式控制 + +![image-20230912144108431](https://s2.loli.net/2023/09/12/kZY3ufgrDMLtv2N.png) + +- PID:进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值用于协助程序员调试程序 +- 所属类/接口名:当前显示信息为SpringBoot重写后的信息,名称过长时,简化包名书写为首字母,甚至直接删除 + +#### 设置日志输出格式 + +在application.yml文件中书写 + +```yaml +# 设置日志的模版格式 +logging: + pattern: + console: "%d - %m%n" +``` + +%d:日期 + +%m:消息 + +%n:换行 + +%p:日志级别 + +%clr(里面可以是%p或%d或者其他的任意的**文字**):设置文字的颜色,在%clr(){加入颜色的名称,red 或者其他的,只有系统内有的才可以使用} + +%t:线程名 + +%c:类名 + +如果类名太长,可以通过%-40.40c的方式,将类名压缩到40位,如果超过了,就进行截取,-号是左对齐 + +如果需要设置占位,例如[ main],可以在%号前面加入数字,例如%16t等 + +![image-20230912144657067](https://s2.loli.net/2023/09/12/8TsIB1h7bZmlKSx.png) + +运行后发现,日志已经与模版格式一致了 + +格式不固定,随意的写 + +### 文件记录日志 + +日志信息一直显示在控制台,不太方便查找,需要记录到文件中比较好,在application.yml中进行配置,可以将日志记录到文件中 + +```yaml +logging: + file: + path: server.log +``` + +设置完成后,运行一下,等服务器停止后,再到工程下,就可以看到日志了 + +![image-20230912150402735](https://s2.loli.net/2023/09/12/PAgu1GCikxdTKlt.png) + +如果日志只存放在一个文件中,那么这个日志文件一定非常大,那我们该怎么办呢? + +分文件,每天记一个 + +```yaml +logging: + file: + path: server.log + logback: + rolling policy: + # 日志文件的最大大小为 1KB + # 当文件大小超过1KB时,就创建一个新的文件 + max-file-size: 1KB + # file-name-pattern文件名称: server.年月日.今天第几个.log + file-name-pattern: server.%d{yyyy-MM-dd}.%i.log +``` + +开启服务器后多运行几次,就发现出现了几个不同的文件了 + +![image-20230912151051976](https://s2.loli.net/2023/09/12/uNY7XBkqeZWO5xz.png) + +# 开发篇 + +## 热部署 + +热部署是为了快速的重启项目而产生的一个工具 + +### 启动热部署 + +使用前需要先导入对应的依赖开启开发者工具 + +```xml + + org.springframework.boot + spring-boot-devtools + 2.5.15 + +``` + +写一些测试代码 + +```java +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Test2Controller { + + @GetMapping("/test") + public String getTest(){ + System.out.println("Test2Controller!"); + return "Test2Controller"; + } + +} +``` + +打开postman进行测试,直接运行,控制台会打印一条 + +修改代码 + +```java +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Test2Controller { + + @GetMapping("/test") + public String getTest(){ + System.out.println("Test2Controller!"); + System.out.println("Test2Controller!"); + System.out.println("Test2Controller!"); + return "Test2Controller"; + } + +} +``` + +如果我们选择右上角的按钮重启服务,会很慢,所以我们使用热部署 + +**激活热部署:Ctrl + F9**或 + +image-20230914134456456 + +热部署很快就完成了,并且也成功出现了修改后代码的效果 + + + +关于热部署: + +- 重启(Restart):自定义开发代码,包含类、页面、配置文件等,加载位置restart类加载器(热部署采用该方法) +- 重载(ReLoad):jar包,加载位置base类加载器 + +区别在于重启是重启的代码等内容,而重载会重载jar包 + +打开服务器时,这两个方法都会使用;而热部署时,只会使用重启,更新代码等 + +热部署仅仅加载当前开发者自定义开发的资源,不加载jar资源 + + + +### 自动启动热部署 + +操作步骤如下: + +image-20230914140022827 + +image-20230914140123999 + +image-20230914140757952 + +设置完成后,我们只需要启动工程,修改一下测试代码 + +当IDEA这个工具失去焦点,也就是不动这个工具五秒后,就会自动进行热部署 + + + +### 热部署范围配置 + +默认不触发重启的目录列表 + +- /META-INF/maven +- /META-INF/resources +- /resources +- /static +- /public +- /templates + +在application.yml文件中修改 + +```yaml +spring: + devtools: + restart: + # 设置不参与热部署的文件夹或文件 + exclude: static/**,public/**,config/**,application.yml +``` + + + +### 关闭热部署 + +```yaml +spring: + devtools: + restart: + exclude: static/**,public/**,config/** + # 设置为不启用 + enabled: false +``` + +热部署一般只在开发环境使用,在其他环境都不使用,可能你在这关闭了,但是有比你权限更高的人不小心开启了,那就不太好了,所以我们需要用到系统属性配置,来让热部署的优先级变高 + +系统属性配置的优先级是比application.yml文件的优先级更高的 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Demo3Application { + + public static void main(String[] args) { + System.setProperty("spring.devtools.restart.enabled","false"); + SpringApplication.run(Demo3Application.class, args); + } + +} +``` + +## 配置高级 + +### 第三方bean属性绑定 + +创建一个springboot的项目,不需要勾选任何内容 + +引入依赖 + +```xml + + org.projectlombok + lombok + +``` + +新建一个服务器的配置类 + +```java +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Data +// 配置属性前缀为servers +@ConfigurationProperties(prefix = "servers") +public class ServerConfig { + private String ipaddress; + private int port; + private long timeout; +} +``` + +在application.yml文件中进行配置 + +利用servers的前缀来和配置文件里的内容进行连接 + +```yaml +servers: + ipAddress: 192.168.0.1 + port: 6666 + timeout: -1 +``` + +在主程序中测试一下 + +```java +import fun.eastwind.demo5.config.ServerConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +@SpringBootApplication +public class Demo5Application { + + public static void main(String[] args) { + ConfigurableApplicationContext run = SpringApplication.run(Demo5Application.class, args); + System.out.println(run.getBean(ServerConfig.class)); + } + +} +``` + +得到结果`ServerConfig(ipaddress=192.168.0.1, port=6666, timeout=-1)` + + + +自己定义的bean可以这样绑定属性,第三方bean如何进行属性绑定呢? + +引入第三方依赖 + +```xml + + com.alibaba + druid + 1.1.16. + +``` + +注入第三方的bean + +```java +import com.alibaba.druid.pool.DruidDataSource; +import fun.eastwind.demo5.config.ServerConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class Demo5Application { + + @Bean + public DruidDataSource getDruidDataSource(){ + DruidDataSource druidDataSource = new DruidDataSource(); + return druidDataSource; + } + + public static void main(String[] args) { + ConfigurableApplicationContext run = SpringApplication.run(Demo5Application.class, args); + System.out.println(run.getBean(ServerConfig.class)); + DruidDataSource druidDataSource = run.getBean(DruidDataSource.class); + System.out.println(druidDataSource); + } + +} +``` + +运行后可以输出对应的bean的属性,但是里面都是空的,因为我们没有进行设置 + +设置第三方bean的属性 + +```java +@Bean +public DruidDataSource getDruidDataSource(){ + DruidDataSource druidDataSource = new DruidDataSource(); + druidDataSource.setPassword("123456"); + return druidDataSource; +} + +public static void main(String[] args) { + ConfigurableApplicationContext run = SpringApplication.run(Demo5Application.class, args); + System.out.println(run.getBean(ServerConfig.class)); + DruidDataSource druidDataSource = run.getBean(DruidDataSource.class); + System.out.println(druidDataSource.getPassword()); + } +``` + +测试一下 + + + +如何将配置文件中的属性注入到第三方bean中 + +@ConfigurationProperties:为第三方bean绑定属性 + +```yaml +datasource: + driver-class-name: com.alibaba.druid.proxy.DruidDriver +``` + +```java +@Bean +@ConfigurationProperties(prefix = "datasource") +public DruidDataSource getDruidDataSource(){ + DruidDataSource druidDataSource = new DruidDataSource(); + return druidDataSource; +} + +public static void main(String[] args) { + ConfigurableApplicationContext run = SpringApplication.run(Demo5Application.class, args); + System.out.println(run.getBean(ServerConfig.class)); + DruidDataSource druidDataSource = run.getBean(DruidDataSource.class); + System.out.println(druidDataSource.getDriverClassName()); +} +``` + +还有另一种方式,直接在主程序加入`@EnableConfigurationProperties(ServerConfig.class)` + +`@EnableConfigurationProperties`可以将使用`@ConfigurationProperties`注解对应的类加入到容器中,跟使用了`@Component`是一个道理 + +这个是开启配置属性,在里面写入需要加入的配置属性类,就不需要在ServerConfig这个类中加入`@Component` + +注意: + +`@EnableConfigurationProperties`和`@Component`不能同时使用,如果同时使用会报错,说有两个容器 + + + +- 解除使用@ConfigurationProperties注释警告 + +![image-20230914194317005](https://s2.loli.net/2023/09/14/uz42NonAUmbXGJE.png) + +需要注入依赖 + +```xml + + org.springframework.boot + spring-boot-configuration-processor + +``` + + + +### 松散绑定 + +当我们使用`@ConfigurationProperties`绑定属性支持属性名宽松绑定时,支持各种形式的绑定 + +分别有:驼峰模式、下划线模式、中划线模式、常量模式 + +当前缀(prefix绑定完成后),里面的数据格式要求会简单很多 + +```yaml +servers: +# ipAddress: 192.168.0.1 驼峰 +# IP_ADDRESS: 192.168.0.2 常量 +# ip-A-d-d-res-s: 192.168.0.3 +# i_p_A_d_d_r_e_ss: 192.168.0.4 unline + ip-Address: 192.168.0.1 # 烤肉串模式 xx-xx-xx-xx + port: 6666 + timeout: -1 +datasource: + driver-class-name: com.alibaba.druid.proxy.DruidDriver +``` + +```java +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +//@Component +@Data +// 配置属性前缀为servers +@ConfigurationProperties(prefix = "servers") +public class ServerConfig { + private String ipaddress; + private int port; + private long timeout; +} +``` + +在servers被绑定后的ipAddress,会自动省略大小写以及_和-,即使改动了ServerConfig中的ipaddress也会自动忽略大小写和下划线 + + + +**注意事项:** + +- **宽松绑定不支持注解@Value引用单个属性的方式** +- **绑定前缀名命名规范:仅能使用纯小写字母、数字、下划线作为合法的字符** + + + +总结: + +- 使用`@ConfigurationProperties`时的命名规范,以及在application.yml文件中进行属性配置时,使用的绑定规则 + + + +### 常用计量单位应用 + +有时候需要设置服务器的超时时间,当服务器的超时时间很大的时候,就会出现问题,例如300000000000,这样的话,我们不清楚这个时间的单位是什么,是300000000000秒,还是300000000000分钟,还是其他什么的 + +**使用jdk8提供的一套单位`Duration`** + +```java +// 将属性设置为小时 +@DurationUnit(ChronoUnit.HOURS) +private Duration serverTimeout; +``` + +```yaml +servers: + port: 6666 + timeout: -1 + serverTimeout: 10 +``` + +此时的结果就会以小时的形式显示 + +`ServerConfig(ipaddress=null, port=6666, timeout=-1, serverTimeout=PT10H)` + +**配置存储容量的大小** + +```java +// 将大小设置为MB +@DataSizeUnit(DataUnit.MEGABYTES) +private DataSize dataSize; +``` + +```yaml +servers: + port: 6666 + timeout: -1 + serverTimeout: 10 +datasource: + driver-class-name: com.alibaba.druid.proxy.DruidDriver +``` + +也可以这样`private DataSize dataSize;`,然后在yml文件中修改为`dataSize: 10MB`,但要注意,这样打印出来的会是**Byte**的形式`dataSize=10485760B` + + + +### bean属性校验 + +开启数据校验有助于系统安全性,J2EE规范中JSR303规范定义了一组有关数据校验相关的API + +使用规范需要导入依赖 + +**导入JSR303规范** + +```xml + + + javax.validation + validation-api + +``` + +开启对当前bean的属性注入校验 + +在需要属性注入校验的类上加入该注解 + +```java +@Validated +``` + +`@Max`:value设置的最大值限制,message是超过后的提示信息 + +```java +// @Max是校验最大值限制的,value是值,message是提示消息 +@Max(value = 8888,message = "最大值不能超过8888") +private int port; +``` + +`@Min`:value设置的最小值限制,message是小于时的提示信息 + +```java +@Min(value = 266,message = "最小值不能低于266") +private int port; +``` + +运行后报错`The Bean Validation API is on the classpath but no implementation could be found` + +说这个bean的api的实现类没有找到 + +`Add an implementation, such as Hibernate Validator, to the classpath` + +添加一个`Hibernate` 实现类,把它放到类路径下 + +导入依赖 + +```xml + + + org.hibernate.validator + hibernate-validator + +``` + +修改为可能会出警告的值即可 + +```yaml +servers: + port: 9999 + timeout: -1 + serverTimeout: 10 + dataSize: 10MB +``` + +运行即可 + +此时校验效果就出来了,没啥毛病 + +![image-20230915153055404](https://s2.loli.net/2023/09/15/ngqS9jTYG3UQmFX.png) + + + +## 测试 + +### 加载测试专用属性 + +新建一个spring的模块,什么也不需要勾选 + +先在测试类中测试配置文件中的属性能否正常读取 + +application.yml + +```yaml +test: + value: 66 +``` + +测试类 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Demo6ApplicationTests { + + @Value("${test.value}") + private int value; + + @Test + void contextLoads() { + System.out.println(value); + } + +} +``` + +为当前测试用例添加临时属性 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(properties = {"test.value=888"}) +class Demo6ApplicationTests { + + @Value("${test.value}") + private int value; + + @Test + void contextLoads() { + System.out.println(value); + } + +} +``` + +`@SpringBootTest(properties = {"test.value=888"})`在SpringBootTest上可以添加properties属性,这个属性是一个String类型的数组,它可以添加和修改对应的临时属性 +**临时配置是覆盖配置文件的,可以理解为配置文件加载完成后,再次加载了一组临时配置** + + + +第二种方式,通过args属性进行配置 + +```java +@SpringBootTest(args = {"--test.value=888"}) +``` + +这里的args与之前命令行修改临时属性是类似的 + + + +**args参数的优先级比properties设置的优先级较高** + + + +### 加载测试专用配置 + +有时候需要在测试时导入一些临时的配置类,可以使用`@Import`注解 + +在test目录下添加一个测试配置类 + +```java +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MsgConfig { + + @Bean + public String test(){ + return "test"; + } + +} +``` + +编写测试类 + +```java +import fun.eastwind.demo6.config.MsgConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@SpringBootTest +// 导入相关的配置类 +@Import({MsgConfig.class}) +public class ConfigurationTest { + + @Autowired + private String msg; + + @Test + void test1(){ + System.out.println(msg); + } + +} +``` + +测试得到对应配置类的结果 + + + +### 测试类中启动web环境 + +在pom.xml文件中修改配置 + +```diff + + org.springframework.boot +- spring-boot-starter ++ spring-boot-starter-web + +``` + +运行主程序类查看效果 + +然后创建一个测试类,然后看看该测试类启动后能否启动web环境 +运行后发现,测试类只是一个普通的Java程序 + +我们需要在测试类中进行设置 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +// 启动默认端口 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class ConfigurationTest { + + @Test + void test1(){ + + } + +} +``` + +再次运行测试,往下一直翻,就能看到对应的服务器被启动了 + +image-20230916094354526 + +也可以使用随机端口来启动测试 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ConfigurationTest { + + @Test + void test1(){ + + } + +} +``` + +image-20230916094746668 + + + +### 发送虚拟请求 + +我们一般都会在java这个目录下写代码启动服务器来发送请求,那么,如何在测试下发送请求呢? + +先写一个控制类,用于在测试类中测试 + +```java +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/hellos") +public class HelloController { + + @GetMapping + public String hello(){ + System.out.println("6666666"); + System.out.println("hello"); + return "Hello"; + } + +} +``` + +在测试类下编写如下代码 + +```java +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + + +// 使用随机端口启动服务器 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + +// 开启虚拟MVC调用 +@AutoConfigureMockMvc +public class ConfigurationTest { + + @Test + // 注入虚拟mvc调用对象 + public void testWeb(@Autowired MockMvc mvc) throws Exception { + // 创建虚拟请求,访问指定地址(get发送get请求,也可以指定为其他的) +// MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.delete("/hello"); + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/hellos"); + // 执行请求 + mvc.perform(builder); + } + +} +``` + + + +### 匹配响应执行状态 + +直接上代码了 + +在测试类中添加 + +```java +@Test +void testStatus(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); + ResultActions actions = mvc.perform(builder); + // 匹配执行状态(是否预期值) + // 定义执行状态匹配器 + StatusResultMatchers status = MockMvcResultMatchers.status(); + // 定义预期执行状态 + ResultMatcher ok = status.isOk(); + actions.andExpect(ok); +} +``` + +上面这一段是定义了一个响应执行状态,当代码正常的情况下,只会普通的输出内容,而出现异常时,就会打印出对应的信息 + +假设将测试类中的testStatus方法修改一下 + +```java +@Test + void testStatus(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1"); + ResultActions actions = mvc.perform(builder); + // 匹配执行状态(是否预期值) + // 定义执行状态匹配器 + StatusResultMatchers status = MockMvcResultMatchers.status(); + // 定义预期执行状态 + ResultMatcher ok = status.isOk(); + actions.andExpect(ok); + } +``` + +/books1是一个不存在的地址,访问后会出现404的异常 + +运行一下,查看响应执行状态 + +```shell +MockHttpServletRequest: + HTTP Method = GET + Request URI = /books1 + Parameters = {} + Headers = [] + Body = null + Session Attrs = {} + +Handler: + Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler + +Async: + Async started = false + Async result = null + +Resolved Exception: + Type = null + +ModelAndView: + View name = null + View = null + Model = null + +FlashMap: + Attributes = null + +MockHttpServletResponse: + Status = 404 + Error message = null + Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"] + Content type = null + Body = + Forwarded URL = null + Redirected URL = null + Cookies = [] + +java.lang.AssertionError: Status expected:<200> but was:<404> +Expected :200 +Actual :404 +``` + +### 匹配响应体 + +这里的匹配就是匹配响应的内容了,不一致就报错 + +测试类中编写代码 + +```java +@Test +void testBody(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); + ResultActions actions = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试失败 + // 定义本次调用的预期值 + ContentResultMatchers content = MockMvcResultMatchers.content(); + ResultMatcher springboot2 = content.string("springboot2"); + // 添加预计值到本次调用过程中进行匹配 + actions.andExpect(springboot2); +} +``` + +预期结果为springboot2,实际为SpringBoot,响应断言异常 + +```error +java.lang.AssertionError: Response content expected: but was: +Expected :springboot2 +Actual :SpringBoot + +``` + +### 匹配响应体(Json) + +匹配json的测试基本上没什么变化 + +```java +@Test + void testJson(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); + ResultActions action = mvc.perform(builder); + + //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 + //定义本次调用的预期值 + ContentResultMatchers content = MockMvcResultMatchers.content(); + ResultMatcher result = content.json + ("{\"id\":1,\"name\":\"springboot2\",\"type\":\"springboot2\",\"description\":\"springboot\"}"); + //添加预计值到本次调用过程中进行匹配 + action.andExpect(result); + + } +``` + +看错误后的结果 + +这里的断言很明显,断言是name出错了,说明预期的是springboot2,获取到了springboot所导致的,/books这里我是模拟了一个book的对象,然后给了一些测试数据 + +```java +@GetMapping +public Book getById(){ + System.out.println("getById is running ....."); + + Book book = new Book(); + book.setId(1); + book.setName("springboot"); + book.setType("springboot"); + book.setDescription("springboot"); + + return book; +} +``` + +![image-20231211144956873](https://s2.loli.net/2023/12/11/AckP8v3X1WG9jwJ.png) + +### 匹配响应头 + +```java +@Test +void testContentType(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books"); + ResultActions action = mvc.perform(builder); + + //设定预期值 与真实值进行比较,成功测试通过,失败测试失败 + //定义本次调用的预期值 + HeaderResultMatchers header = MockMvcResultMatchers.header(); + ResultMatcher contentType = header.string("Content-Type", "application/json"); + //添加预计值到本次调用过程中进行匹配 + action.andExpect(contentType); + +} +``` + +看一下报错结果,其实也和前面是类似的 + +报错是说响应头Content-Type预期是json结果是text,没啥好说的,预计不通过所导致的 + +![image-20231211145543485](https://s2.loli.net/2023/12/11/cVTGKIvtgkBaQRE.png) + +### 业务层测试事务回滚 + +一般情况下的测试,也会测试数据库的crud操作,当crud成功后,一般会在数据库中留下数据,但是一般测试是不会留下数据的,所以我们需要让测试进行事务回滚,让crud的数据成功后回滚 + +我们可以通过为测试用例添加一个注解`@Transactional`,在带有`@SpringBootTest`的测试类中加上该注解,表明,该测试类所进行的数据库操作都会被回滚,如果我们想进行数据库操作且不进行回滚,我们可以再加入一个注解`@Rollback(false)`,默认是`@Rollback(true)`回滚,改为false就是不回滚了,下面给出一个案例代码 + +这里设置`@Rollback(true)`说明是回滚的,执行完这段后,数据库的操作是成功了,但是会被回滚 + +```java +@SpringBootTest +@Transactional +@Rollback(true) +public class DaoTest { + + @Autowired + private BookService bookService; + + @Test + void testSave(){ + Book book = new Book(); + book.setName("springboot3"); + book.setType("springboot3"); + book.setDescription("springboot3"); + + bookService.save(book); + } + +} +``` + +### 测试用例设置随机数据 + +想要在测试用例中设置随机数据,可以在application.yml中进行随机测试用例数据的编写 + +${random.xxx},这里的xxx是随机值的格式 + +```yaml +testcase: + book: + id: ${random.int} + id2: ${random.int(10)} + type: ${random.int!5,10!} + name: ${random.value} # 字符串 + uuid: ${random.uuid} # uuid + publishTime: ${random.long} # 长整型 +``` + +想要使用,可以通过`@ConfigurationProperties`来进行属性的配置,这里的prefix就指定了在application.yml文件中的随机属性,当然,名称需要同上 + +```java +@Component +@Data +@ConfigurationProperties(prefix = "testcase.book") +public class BookCase { + private int id; + private int id2; + private int type; + private String name; + private String uuid; + private long publishTime; +} +``` + +对于随机值的产生,还有一些小的限定规则,比如数值可以限定范围等 + +想要为数值限定范围的话,可以通过 + +- ${random.int}表示随机整数 +- ${random.int(10)}表示10以内的随机数 +- ${random.int(10,20)}表示10到20的随机数 +- 其中()可以是任意字符,例如[],!!均可,例如${random.int!10,20!} + +## SpringBoot内置的数据层解决方案 + +### SQL + +#### 内置数据源 + +springboot提供了3款内嵌数据源技术,分别如下: + +- HikariCP +- Tomcat提供DataSource +- Commons DBCP + +目前我们所使用的数据源是Druid,运行时可以在日志中看到数据源的初始化信息 + +``` +INFO 28600 --- [ main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource +INFO 28600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited +``` + +当我们删除Druid数据源,会出现什么呢 + +``` +INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +``` + +此时出现了一个陌生的数据源,也就是说,当我们不使用任何数据源时,springboot会加载一个默认的数据源`HikariDataSource`,而且我们可以对这个默认的数据源在application.yml文件中进行一个配置 + +```yaml +spring: + datasource: + url: 路径 + hikari: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + maximum-pool-size: 50 +``` + +专门的对默认的数据源做一个配置 + +#### JDBCTemplate + +JdbcTemplate是一套现成的数据库技术,由Spring提供,想要使用它,需要下面几个步骤 + +- 导入坐标 + +```xml + + org.springframework.boot + spring-boot-starter-jdbc +> maps = jdbcTemplate.queryForList(sql); + System.out.println(maps); +} +``` + +- 使用JdbcTemplate实现查询操作(实体类封装数据的查询操作) +- 这里的`RowMapper`是获取出来的每一个数据模型,返回的是一个对象的集合,对象集合中是该对象所对应的字段及类型,此时返回的就是每一个Book对象了 + +```java +@Test +void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){ + + String sql = "select * from tbl_book"; + RowMapper rm = new RowMapper() { + @Override + public Book mapRow(ResultSet rs, int rowNum) throws SQLException { + Book temp = new Book(); + temp.setId(rs.getInt("id")); + temp.setName(rs.getString("name")); + temp.setType(rs.getString("type")); + temp.setDescription(rs.getString("description")); + return temp; + } + }; + List list = jdbcTemplate.query(sql, rm); + System.out.println(list); +} +``` + +- 使用JdbcTemplate实现增删改操作 + +```java +@Test +void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate){ + String sql = "insert into tbl_book values(3,'springboot1','springboot2','springboot3')"; + jdbcTemplate.update(sql); +} +``` + +删除修改也是同理的 + +传一个sql语句进去 + +- 如果想对JdbcTemplate对象进行相关配置,可以在yml文件中进行设定,具体如下: + +```yaml +spring: + jdbc: + template: + query-timeout: -1 # 查询超时时间 + max-rows: 500 # 最大行数 + fetch-size: -1 # 缓存行数 +``` + +#### 数据库 + +springboot提供了3款内置的数据库,分别是 + +- H2 +- HSQL +- Derby + +内嵌数据库的优势是在进行测试时,数据无需存储在磁盘上,而是运行在内存中,当服务器关闭后,数据库也跟着消失,方便功能测试。 + +- 导入H2数据库的坐标 + +```xml + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-data-jpa + +``` + +- 将工程设置为web工程,启动工程时启动H2数据库 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +- 通过yml配置开启H2数据库控制台访问程序,也可以使用其他的数据库连接软件操作 + +```yaml +spring: + h2: + console: + enabled: true + path: /h2 +``` + +web端访问路径/h2,访问密码123456,如果访问失败,先配置下列数据源,启动程序运行后再次访问/h2路径就可以正常访问了 + +```yaml +datasource: + url: jdbc:h2:~/test + hikari: + driver-class-name: org.h2.Driver + username: eastwind + password: 123456 +``` + +其他的都没有任何变化,只是更换了一个数据库而已 + +**总结** + +1. H2内嵌式数据库启动方式,添加坐标,添加配置 +2. H2数据库线上运行时请**务必关闭** + +​ 到这里SQL相关的数据层解决方案就讲完了,现在的可选技术就丰富的多了。 + +- 数据源技术:Druid、Hikari、tomcat DataSource、DBCP +- 持久化技术:MyBatisPlus、MyBatis、JdbcTemplate +- 数据库技术:MySQL、H2、HSQL、Derby + +### NOSQL + +nosql,其实指代的就是非关系型数据库,就是说,数据该存存,该取取 + +#### SpringBoot整合Redis(默认的lettucs) + +​ Redis是一款采用key-value数据存储格式的内存级NoSQL数据库,重点关注数据存储格式,是key-value格式,也就是键值对的存储形式。与MySQL数据库不同,MySQL数据库有表、有字段、有记录,Redis没有这些东西,就是一个名称对应一个值,并且数据以存储在内存中使用为主。什么叫以存储在内存中为主?其实Redis有它的数据持久化方案,分别是RDB和AOF,但是Redis自身并不是为了数据持久化而生的,主要是在内存中保存数据,加速数据访问的,所以说是一款内存级数据库。Redis支持多种数据存储格式,比如可以直接存字符串,也可以存一个map集合,list集合。 + +​ Redis默认的端口号是6379,可以下载Windows版的:windows版安装包下载地址:https://github.com/tporadowski/redis/releases,因为Linux不是特别友好,后面专门学习的时候再进行学习 + +安装完毕后,打开刚刚安装的文件夹 + +![image-20231211190742149](https://s2.loli.net/2023/12/11/mWfl36PnkKBMepI.png)需要先启动服务器再启动客户端,可以双击图标直接启动,也可以指定服务器的配置文件,这需要在当前redis-serve.exe目录下的命令行进行操作 + +`redis-server.exe redis.windows.conf`这里的redis.windows.conf是一个配置文件,在当前文件夹下很明显也可以看到 + +启动客户端可以直接双击,也可以通过命令行的形式,`redis-cli.exe` + +放置一个字符串数据到redis中,先为数据定义一个名称,比如name,age等,然后使用命令set设置数据到redis服务器中即可 + +```CMD +set name itheima +set age 12 +``` + +​ 从redis中取出已经放入的数据,根据名称取,就可以得到对应数据。如果没有对应数据就会得到(nil) + +```CMD +get name +get age +``` + +​ 以上使用的数据存储是一个名称对应一个值,如果要维护的数据过多,可以使用别的数据存储结构。例如hash,它是一种一个名称下可以存储多个数据的存储模型,并且每个数据也可以有自己的二级存储名称。向hash结构中存储数据格式如下: + +``` +hset a a1 aa1 #对外key名称是a,在名称为a的存储模型中,a1这个key中保存了数据aa1 +hset a a2 aa2 +``` + +​ 获取hash结构中的数据命令如下 + +```CMD +hget a a1 #得到aa1 +hget a a2 #得到aa2 +``` + +整合的话,一般是三个步骤,导入坐标,配置信息,进行编写 + +- 导入坐标 + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +- 基础配置 + +```yaml +spring: + redis: + host: localhost + port: 6379 +``` + +- 使用springboot整合redis的专用客户端接口操作,此处使用的是RedisTemplate + +```java +@SpringBootTest +class Springboot16RedisApplicationTests { + @Autowired + private RedisTemplate redisTemplate; + @Test + void set() { + ValueOperations ops = redisTemplate.opsForValue(); + ops.set("age",41); + } + @Test + void get() { + ValueOperations ops = redisTemplate.opsForValue(); + Object age = ops.get("name"); + System.out.println(age); + } + @Test + void hset() { + HashOperations ops = redisTemplate.opsForHash(); + ops.put("info","b","bb"); + } + @Test + void hget() { + HashOperations ops = redisTemplate.opsForHash(); + Object val = ops.get("info", "b"); + System.out.println(val); + } +} + +``` + +##### StringRedisTemplate + +由于redis内部不提供java对象的存储格式,因此当操作的数据以对象的形式存在时,会进行转码,转换成字符串格式后进行操作。为了方便开发者使用基于字符串为数据的操作,springboot整合redis时提供了专用的API接口StringRedisTemplate,你可以理解为这是RedisTemplate的一种指定数据泛型的操作API。 + +```java +@SpringBootTest +public class StringRedisTemplateTest { + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Test + void get(){ + ValueOperations ops = stringRedisTemplate.opsForValue(); + String name = ops.get("name"); + System.out.println(name); + } +} +``` + +##### jedis + +​ springboot整合redis技术提供了多种客户端兼容模式,默认提供的是lettucs客户端技术,也可以根据需要切换成指定客户端技术,例如jedis客户端技术,切换成jedis客户端技术操作步骤如下: + +- 导入坐标 + +```xml + + redis.clients + jedis + +``` + +jedis坐标受springboot管理,无需提供版本号 + +- 配置客户端技术类型,设置为jedis + +```yml +spring: + redis: + host: localhost + port: 6379 + client-type: jedis +``` + +- 根据需要设置对应的配置 + +```yaml +spring: + redis: + host: localhost + port: 6379 + client-type: jedis + lettuce: + pool: + max-active: 16 + jedis: + pool: + max-active: 16 +``` + +**lettcus与jedis区别** + +- jedis连接Redis服务器是直连模式,当多线程模式下使用jedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,这样整体性能就大受影响 +- lettcus基于Netty框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnection。 StatefulRedisConnection自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。当然lettcus也支持多连接实例一起工作 + +#### SpringBoot整合MongoDB + +使用Redis技术可以有效的提高数据访问速度,但是由于Redis的数据格式单一性,无法操作结构化数据,当操作对象型的数据时,Redis就显得捉襟见肘。在保障访问速度的情况下,如果想操作结构化数据,看来Redis无法满足要求了,此时需要使用全新的数据存储结束来解决此问题,本节讲解springboot如何整合MongoDB技术。 + +​ MongoDB是一个开源、高性能、无模式的文档型数据库,它是NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库。 + +​ 上述描述中几个词,其中对于我们最陌生的词是无模式的。什么叫无模式呢?简单说就是作为一款数据库,没有固定的数据存储结构,第一条数据可能有A、B、C一共3个字段,第二条数据可能有D、E、F也是3个字段,第三条数据可能是A、C、E3个字段,也就是说数据的结构不固定,这就是无模式。有人会说这有什么用啊?灵活,随时变更,不受约束。基于上述特点,MongoDB的应用面也会产生一些变化。以下列出了一些可以使用MongoDB作为数据存储的场景,但是并不是必须使用MongoDB的场景: + +- 淘宝用户数据 + - 存储位置:数据库 + - 特征:永久性存储,修改频度极低 +- 游戏装备数据、游戏道具数据 + - 存储位置:数据库、Mongodb + - 特征:永久性存储与临时存储相结合、修改频度较高 +- 直播数据、打赏数据、粉丝数据 + - 存储位置:数据库、Mongodb + - 特征:永久性存储与临时存储相结合,修改频度极高 +- 物联网数据 + - 存储位置:Mongodb + - 特征:临时存储,修改频度飞速 + +##### 基础操作及其安装 + +windows版安装包下载地址:https://www.mongodb.com/try/download + +将安装包下载完成后解压到自己的某盘下,接着进到该目录,在与bin目录同级的目录中,新建一个data目录,再到data目录下新建db目录用来存储数据 + +回到mongodb的bin目录下,在该目录下,有一个叫`mongod.exe`的文件,这个文件是运行mongodb的,在这之前,我们还需要指定mongodb的数据存储目录:`mongod --dbpath=..\data\db` + +这个命令指定了数据存储的位置,..\是返回上一级目录,bin目录的上一级目录中存在data,data下存在db目录 + +默认服务端口是27017。 + +**启动客户端** + +服务器启动了,接着启动客户端,依然是在刚刚的bin目录打开cmd,输入如下命令,指定一下服务端口,我这里的服务端口是27017 + +`mongo --host=127.0.0.1 --port=27017` + +看到这个页面,说明你成功的登入了mongodb的客户端 + +image-20231211195647761 + +这里的话,使用mongo命令行写很麻烦,所以这里用mongo的客户端来进行代码编写 + +安装的软件是Robo3t,自行下载即可,下载的软件版本需要和mongo的版本保持一致最好,否则容易有兼容性问题 + +打开后,需要先创建mongo的连接 + +image-20231211200607378 + +对连接的一些设置 + +image-20231211200801375 + +配置好设置后,点击Save,再点击连接,即可 + +创建数据库:在左侧菜单中使用右键创建,输入数据库名称即可 + +![image-20231211201523803](https://s2.loli.net/2023/12/12/EwdtMBRzcFOraQj.png) + +创建集合:在数据库中的Collections上使用右键创建,输入集合名称即可,集合等同于数据库中的表的作用 + +这里的集合可以理解为mysql中的表,双击这个表就能进入客户端了 + +在这里可以调整数据的展示模式 + +![image-20231211203135570](https://s2.loli.net/2023/12/11/Ht7EJZFe9dV4omS.png) + +注释使用// + +新增文档:(文档是一种类似json格式的数据,初学者可以先把数据理解为就是json数据) + +```CMD +db.集合名称.insert/save/insertOne(文档) +``` + +删除文档: + +```CMD +db.集合名称.remove(条件) +``` + +修改文档: + +```cmd +db.集合名称.update(条件,{操作种类:{文档}}) +``` + +查询文档: + +```CMD +基础查询 +查询全部: db.集合.find(); +查第一条: db.集合.findOne() +查询指定数量文档: db.集合.find().limit(10) //查10条文档 +跳过指定数量文档: db.集合.find().skip(20) //跳过20条文档 +统计: db.集合.count() +排序: db.集合.sort({age:1}) //按age升序排序 +投影: db.集合名称.find(条件,{name:1,age:1}) //仅保留name与age域 + +条件查询 +基本格式: db.集合.find({条件}) +模糊查询: db.集合.find({域名:/正则表达式/}) //等同SQL中的like,比like强大,可以执行正则所有规则 +条件比较运算: db.集合.find({域名:{$gt:值}}) //等同SQL中的数值比较操作,例如:name>18 +包含查询: db.集合.find({域名:{$in:[值1,值2]}}) //等同于SQL中的in +条件连接查询: db.集合.find({$and:[{条件1},{条件2}]}) //等同于SQL中的and、or +``` + +案例 + +```mongodb +// 新增文档 +db.book.insert({name:"springboot"}) + +db.book.save({name:'springboot2'}) + +db.book.insertOne({name:'springboot3'}) + +// 删除文档 + +db.book.remove({name:'springboot'}) + +// 修改文档 +// update仅更新找到的第一条,update(条件,{更新内容}) +db.book.update({name:'springboot2'},{name:'springboot1'}) +``` + +##### 通过IDEA整合MongoDB + +导入坐标 + +```xml + + org.springframework.boot + spring-boot-starter-data-mongodb + +``` + +配置文件 + +```yaml +spring: + data: + mongodb: + # 这里的uri代指的mongodb中所创建的数据库 + uri: mongodb://localhost/eastwind +``` + +编写实体类 + +```java +import lombok.Data; + +@Data +public class Book { + private int id; + private String name; + private String type; + private String description; +} +``` + +编写测试类 + +```java +import com.itheima.domain.Book; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.MongoTemplate; + +import java.util.List; + +@SpringBootTest +class Springboot17MongodbApplicationTests { + + @Autowired + private MongoTemplate mongoTemplate; + + @Test + void contextLoads() { + // 创建测试的实体类 + Book book = new Book(); + book.setId(2); + book.setName("springboot2"); + book.setType("springboot2"); + book.setDescription("springboot2"); + // 保存实体类到mongoDB中 + mongoTemplate.save(book); + } + + @Test + void find(){ + // 查找mongoDB中的所有数据 + List all = mongoTemplate.findAll(Book.class); + System.out.println(all); + } +} +``` + +#### ES(Elasticsearch) + +ES(Elasticsearch)是一个分布式全文搜索引擎,重点是全文搜索。 + +​ 可以说ES的思想就是**查询对应的关键字,查到关键字后,获取的是其的id,再根据id来进行查询** + +​ 那什么是全文搜索呢?比如用户要买一本书,以Java为关键字进行搜索,不管是书名中还是书的介绍中,甚至是书的作者名字,只要包含java就作为查询结果返回给用户查看,上述过程就使用了全文搜索技术。搜索的条件不再是仅用于对某一个字段进行比对,而是在一条数据中使用搜索条件去比对更多的字段,只要能匹配上就列入查询结果,这就是全文搜索的目的。而ES技术就是一种可以实现上述效果的技术。 + +​ 要实现全文搜索的效果,不可能使用数据库中like操作去进行比对,这种效率太低了。ES设计了一种全新的思想,来实现全文搜索。具体操作过程如下: + +1. 将被查询的字段的数据全部文本信息进行查分,分成若干个词 + + - 例如“中华人民共和国”就会被拆分成三个词,分别是“中华”、“人民”、“共和国”,此过程有专业术语叫做分词。分词的策略不同,分出的效果不一样,不同的分词策略称为分词器。 + +2. 将分词得到的结果存储起来,对应每条数据的id + + - 例如id为1的数据中名称这一项的值是“中华人民共和国”,那么分词结束后,就会出现“中华”对应id为1,“人民”对应id为1,“共和国”对应id为1 + + - 例如id为2的数据中名称这一项的值是“人民代表大会“,那么分词结束后,就会出现“人民”对应id为2,“代表”对应id为2,“大会”对应id为2 + + - 此时就会出现如下对应结果,按照上述形式可以对所有文档进行分词。需要注意分词的过程不是仅对一个字段进行,而是对每一个参与查询的字段都执行,最终结果汇总到一个表格中 + + | 分词结果关键字 | 对应id | + | -------------- | ------ | + | 中华 | 1 | + | 人民 | 1,2 | + | 共和国 | 1 | + | 代表 | 2 | + | 大会 | 2 | + +3. 当进行查询时,如果输入“人民”作为查询条件,可以通过上述表格数据进行比对,得到id值1,2,然后根据id值就可以得到查询的结果数据了。 + +​ 上述过程中分词结果关键字内容每一个都不相同,作用有点类似于数据库中的索引,是用来加速数据查询的。但是数据库中的索引是对某一个字段进行添加索引,而这里的分词结果关键字不是一个完整的字段值,只是一个字段中的其中的一部分内容。并且索引使用时是根据索引内容查找整条数据,全文搜索中的分词结果关键字查询后得到的并不是整条的数据,而是数据的id,要想获得具体数据还要再次查询,因此这里为这种分词结果关键字起了一个全新的名称,叫做**倒排索引**。 + +##### ES的安装 + +windows版安装包下载地址:[https://](https://www.elastic.co/cn/downloads/elasticsearch)[www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) + +​ 下载的安装包是解压缩就能使用的zip文件,解压缩完毕后会得到如下文件 + +![image-20220225132756400](E:/%25E9%25BB%2591%25E9%25A9%25ACSpringBoot2/%25E5%25BC%2580%25E5%258F%2591%25E5%25AE%259E%25E7%2594%25A8%25E7%25AF%2587%25E2%2580%2594%25E8%25B5%2584%25E6%2596%2599/img/image-20220225132756400.png) + +- bin目录:包含所有的可执行命令 +- config目录:包含ES服务器使用的配置文件 +- jdk目录:此目录中包含了一个完整的jdk工具包,版本17,当ES升级时,使用最新版本的jdk确保不会出现版本支持性不足的问题 +- lib目录:包含ES运行的依赖jar文件 +- logs目录:包含ES运行后产生的所有日志文件 +- modules目录:包含ES软件中所有的功能模块,也是一个一个的jar包。和jar目录不同,jar目录是ES运行期间依赖的jar包,modules是ES软件自己的功能jar包 +- plugins目录:包含ES软件安装的插件,默认为空 + +**启动服务器** + +```CMD +elasticsearch.bat +``` + +​ 双击elasticsearch.bat文件即可启动ES服务器,**默认服务端口**9200。通过浏览器访问http://localhost:9200看到如下信息视为ES服务器正常启动 + +``` +{ + "name" : "CZBK-**********", + "cluster_name" : "elasticsearch", + "cluster_uuid" : "j137DSswTPG8U4Yb-0T1Mg", + "version" : { + "number" : "7.16.2", + "build_flavor" : "default", + "build_type" : "zip", + "build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb", + "build_date" : "2021-12-18T19:42:46.604893745Z", + "build_snapshot" : false, + "lucene_version" : "8.10.1", + "minimum_wire_compatibility_version" : "6.8.0", + "minimum_index_compatibility_version" : "6.0.0-beta1" + }, + "tagline" : "You Know, for Search" +} +``` + +##### ES的基本操作 + +​ ES中保存有我们要查询的数据,只不过格式和数据库存储数据格式不同而已。在ES中我们要先创建倒排索引,这个索引的功能又点类似于数据库的表,然后将数据添加到倒排索引中,添加的数据称为文档。所以要进行ES的操作要先创建索引,再添加文档,这样才能进行后续的查询操作。 + +​ 要操作ES可以通过Rest风格的请求来进行,也就是说发送一个请求就可以执行一个操作。比如新建索引,删除索引这些操作都可以使用发送请求的形式来进行。 + +以下发送请求的操作都可以通过PostMan来进行 + +- 创建索引 + +``` +Put请求 http://localhost:9200/books books是索引名称 +``` + +发送请求后,看到如下信息说明索引创建成功 + +``` +{ + "acknowledged": true, + "shards_acknowledged": true, + "index": "books" +} +``` + +重复创建已经存在的索引会出现错误信息,**reason**属性中描述错误原因 + +``` +{ + "error": { + "root_cause": [ + { + "type": "resource_already_exists_exception", + "reason": "index [books/gOLfXbjFQxSLPk-eq9LV8Q] already exists", + "index_uuid": "gOLfXbjFQxSLPk-eq9LV8Q", + "index": "books" + } + ], + "type": "resource_already_exists_exception", + "reason": "index [books/gOLfXbjFQxSLPk-eq9LV8Q] already exists", + "index_uuid": "gOLfXbjFQxSLPk-eq9LV8Q", + "index": "books" + }, + "status": 400 +} +``` + +- 查询索引 + +``` +GET请求 http://localhost:9200/books +``` + +查询索引得到索引相关信息,如下 + +``` +{ + "books": { + "aliases": {}, + "mappings": {}, + "settings": { + "index": { + "routing": { + "allocation": { + "include": { + "_tier_preference": "data_content" + } + } + }, + "number_of_shards": "1", + "provided_name": "books", + "creation_date": "1702356563110", + "number_of_replicas": "1", + "uuid": "gOLfXbjFQxSLPk-eq9LV8Q", + "version": { + "created": "7160299" + } + } + } + } +} +``` + +查询不存在的索引信息会报错,详细描述在reason中 + +``` +{ + "error": { + "root_cause": [ + { + "type": "index_not_found_exception", + "reason": "no such index [books1]", + "resource.type": "index_or_alias", + "resource.id": "books1", + "index_uuid": "_na_", + "index": "books1" + } + ], + "type": "index_not_found_exception", + "reason": "no such index [books1]", + "resource.type": "index_or_alias", + "resource.id": "books1", + "index_uuid": "_na_", + "index": "books1" + }, + "status": 404 +} +``` + +- 删除索引 + +```CMD +DELETE请求 http://localhost:9200/books +``` + +删除所有后,给出删除结果 + +```json +{ + "acknowledged": true +} +``` + +如果删除不存在的,会给出错误信息,同样在reason属性中描述具体的错误原因 + +```JSON +{ + "error": { + "root_cause": [ + { + "type": "index_not_found_exception", + "reason": "no such index [books1]", + "resource.type": "index_or_alias", + "resource.id": "books1", + "index_uuid": "_na_", + "index": "books1" + } + ], + "type": "index_not_found_exception", + "reason": "no such index [books1]", + "resource.type": "index_or_alias", + "resource.id": "books1", + "index_uuid": "_na_", + "index": "books1" + }, + "status": 404 +} +``` + +到这里ES还并没有分词效果,我们需要为ES添加一个分词器 + +在安装ES的目录中 + +![image-20231212125733849](https://s2.loli.net/2023/12/12/gHdeQf1NuWnDrT7.png) + +可以新建一个IK文件夹,解压刚刚的IK分词器到里面,然后我们需要重启ES,因为新添加了一个插件 + +重启完成后,我们新建索引,此时发送请求的话,就需要携带参数来发送了,这里携带的参数,就是分词器的相关信息 + +```json +{ + "mappings":{ # 定义mappings属性,替换创建索引时对应的mappings + "properties":{ # 定义索引中包含的属性设置 + "id":{ # 设置索引中包含id属性 + "type":"keyword" # 当前属性可以被直接搜索 + }, + "name":{ # 设置索引中包含name属性 + "type":"text", # 当前属性是文本信息,参与分词 + "analyzer":"ik_max_word", # 使用IK分词器进行分词 + "copy_to":"all" # 将数据拷贝到all一份 + }, + "type":{ + "type":"keyword" + }, + "description":{ + "type":"text", + "analyzer":"ik_max_word", + "copy_to":"all" + }, + "all":{ + "type":"text", + "analyzer":"ik_max_word" + } + } + } +} +``` + +``` +Put请求 http://localhost:9200/books 携带上述的json参数 +``` + +接着再次查询,查看刚刚添加的内容 + +``` +Get请求 http://localhost:9200/books +``` + +``` +{ + "books": { + "aliases": {}, + "mappings": { + "properties": { + "all": { + "type": "text", + "analyzer": "ik_max_word" + }, + "description": { + "type": "text", + "copy_to": [ + "all" + ], + "analyzer": "ik_max_word" + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text", + "copy_to": [ + "all" + ], + "analyzer": "ik_max_word" + }, + "type": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "routing": { + "allocation": { + "include": { + "_tier_preference": "data_content" + } + } + }, + "number_of_shards": "1", + "provided_name": "books", + "creation_date": "1702358131045", + "number_of_replicas": "1", + "uuid": "kobkcinJSGC6WlBX5BeIhA", + "version": { + "created": "7160299" + } + } + } + } +} +``` + +###### 新建文档 + +``` +POST请求 http://localhost:9200/books/_doc #使用系统自动生成id +POST请求 http://localhost:9200/books/_create/1 #使用指定id +POST请求 http://localhost:9200/books/_doc/1 #使用指定id,不存在创建,存在更新(版本递增) + +文档通过请求参数传递,数据格式json +{ + "id":1, + "name":"springboot", + "type":"springboot", + "description":"springboot" +} +``` + +创建成功后,结果如下 + +``` +{ + "_index": "books", + "_type": "_doc", + "_id": "-6B7XIwBlbXQYAoKOhZf", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 0, + "_primary_term": 1 +} +``` + +###### 查询文档 + +查询时不携带参数 + +``` +GET请求 http://localhost:9200/books/_doc/1 #查询单个文档 +GET请求 http://localhost:9200/books/_search #查询全部文档 +``` + +查询单个文档时所携带的id,是创建时的id,也就是系统生成或者在路径后跟随的指定的那个id,跟json参数中携带的内容无关 + +``` +{ + "_index": "books", + "_type": "_doc", + "_id": "1", + "_version": 1, + "_seq_no": 1, + "_primary_term": 1, + "found": true, + "_source": { + "id": 1, + "name": "springboot", + "type": "springboot", + "description": "springboot" + } +} +``` + +查询全部文档 + +``` +{ + "took": 883, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1.0, + "hits": [ + { + "_index": "books", + "_type": "_doc", + "_id": "-6B7XIwBlbXQYAoKOhZf", + "_score": 1.0, + "_source": { + "id": 1, + "name": "springboot", + "type": "springboot", + "description": "springboot" + } + } + ] + } +} +``` + +###### 其他操作 + +- 条件查询 + + ```json + GET请求 http://localhost:9200/books/_search?q=name:springboot # q=查询属性名:查询属性值 + ``` + +- 删除文档 + + ```json + DELETE请求 http://localhost:9200/books/_doc/1 + ``` + +- 修改文档(全量更新) + + 全覆盖 + + ```json + PUT请求 http://localhost:9200/books/_doc/1 + + 文档通过请求参数传递,数据格式json + { +     "name":"springboot", +     "type":"springboot", +     "description":"springboot" + } + ``` + +- 修改文档(部分更新) + + ```json + POST请求 http://localhost:9200/books/_update/1 + + 文档通过请求参数传递,数据格式json + { + "doc":{ #部分更新并不是对原始文档进行更新,而是对原始文档对象中的doc属性中的指定属性更新 +      "name":"springboot" #仅更新提供的属性值,未提供的属性值不参与更新操作 + } + } + ``` + +##### ES整合 + +- 导入springboot整合ES的starter坐标 + +```xml + + org.springframework.boot + spring-boot-starter-data-elasticsearch + +``` + +- 进行基础配置 + + +```yaml +spring: + elasticsearch: + rest: + uris: http://localhost:9200 +``` + +​ 配置ES服务器地址,端口9200 + +- 使用springboot整合ES的专用客户端接口ElasticsearchRestTemplate来进行操作 + + +```java +@SpringBootTest +class Springboot18EsApplicationTests { + @Autowired + private ElasticsearchRestTemplate template; +} +``` + +​ 上述操作形式是ES早期的操作方式,使用的客户端被称为Low Level Client,这种客户端操作方式性能方面略显不足,于是ES开发了全新的客户端操作方式,称为High Level Client。高级别客户端与ES版本同步更新,但是springboot最初整合ES的时候使用的是低级别客户端,所以企业开发需要更换成高级别的客户端模式。 + +​ 下面使用高级别客户端方式进行springboot整合ES,操作步骤如下: + +- 导入springboot整合ES高级别客户端的坐标,此种形式目前没有对应的starter + + +```xml + + org.elasticsearch.client + elasticsearch-rest-high-level-client + +``` + +- 使用编程的形式设置连接的ES服务器,并获取客户端对象 + + +```java +@SpringBootTest +class Springboot18EsApplicationTests { + private RestHighLevelClient client; + @Test + void testCreateClient() throws IOException { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + client = new RestHighLevelClient(builder); + + client.close(); + } +} +``` + +​ 配置ES服务器地址与端口9200,记得客户端使用完毕需要手工关闭。由于当前客户端是手工维护的,因此不能通过自动装配的形式加载对象。 + +- 使用客户端对象操作ES,例如创建索引 + + +```java +@SpringBootTest +class Springboot18EsApplicationTests { + private RestHighLevelClient client; + @Test + void testCreateIndex() throws IOException { + // 建立连接 + HttpHost host = HttpHost.create("http://localhost:9200"); + // 构建客户端 + RestClientBuilder builder = RestClient.builder(host); + client = new RestHighLevelClient(builder); + + // 创建请求对象 + CreateIndexRequest request = new CreateIndexRequest("books"); + // 使用客户端发送请求 + client.indices().create(request, RequestOptions.DEFAULT); + // 关闭客户端 + client.close(); + } +} +``` + +​ 高级别客户端操作是通过发送请求的方式完成所有操作的,ES针对各种不同的操作,设定了各式各样的请求对象,上例中创建索引的对象是CreateIndexRequest,其他操作也会有自己专用的Request对象。 + +​ 当前操作我们发现,无论进行ES何种操作,第一步永远是获取RestHighLevelClient对象,最后一步永远是关闭该对象的连接。在测试中可以使用测试类的特性去帮助开发者一次性的完成上述操作,但是在业务书写时,还需要自行管理。将上述代码格式转换成使用测试类的初始化方法和销毁方法进行客户端对象的维护。 + +```JAVA +@SpringBootTest +class Springboot18EsApplicationTests { + @BeforeEach //在测试类中每个操作运行前运行的方法 + void setUp() { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + client = new RestHighLevelClient(builder); + } + + @AfterEach //在测试类中每个操作运行后运行的方法 + void tearDown() throws IOException { + client.close(); + } + + private RestHighLevelClient client; + + @Test + void testCreateIndex() throws IOException { + CreateIndexRequest request = new CreateIndexRequest("books"); + client.indices().create(request, RequestOptions.DEFAULT); + } +} +``` + +​ 现在的书写简化了很多,也更合理。下面使用上述模式将所有的ES操作执行一遍,测试结果 + +**创建索引(IK分词器)**: + +```java +@Test +void testCreateIndexByIK() throws IOException { + CreateIndexRequest request = new CreateIndexRequest("books"); + String json = "{\n" + + " \"mappings\":{\n" + + " \"properties\":{\n" + + " \"id\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"name\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"type\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"description\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"all\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + //设置请求中的参数 + request.source(json, XContentType.JSON); + client.indices().create(request, RequestOptions.DEFAULT); +} +``` + +​ IK分词器是通过请求参数的形式进行设置的,设置请求参数使用request对象中的source方法进行设置,至于参数是什么,取决于你的操作种类。当请求中需要参数时,均可使用当前形式进行参数设置。 + +**添加文档**: + +```java +@Test +//添加文档 +void testCreateDoc() throws IOException { + // 从数据库中根据id查询出数据信息 + Book book = bookDao.selectById(1); + // 创建请求,id是数据库中的id + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + // 将数据转为json + String json = JSON.toJSONString(book); + // 发送请求的数据类型是json + request.source(json,XContentType.JSON); + // 发送请求 + client.index(request,RequestOptions.DEFAULT); +} +``` + +​ 添加文档使用的请求对象是IndexRequest,与创建索引使用的请求对象不同。 + +**批量添加文档**: + +```java +@Test +//批量添加文档 +void testCreateDocAll() throws IOException { + List bookList = bookDao.selectList(null); + // 创建一个类似数组的Request + BulkRequest bulk = new BulkRequest(); + for (Book book : bookList) { + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + String json = JSON.toJSONString(book); + request.source(json,XContentType.JSON); + // 将每一个IndexRequest添加到Bulk请求中 + bulk.add(request); + } + client.bulk(bulk,RequestOptions.DEFAULT); +} +``` + +​ 批量做时,先创建一个BulkRequest的对象,可以将该对象理解为是一个保存request对象的容器,将所有的请求都初始化好后,添加到BulkRequest对象中,再使用BulkRequest对象的bulk方法,一次性执行完毕。 + +**按id查询文档**: + +```java +@Test +//按id查询 +void testGet() throws IOException { + // 传入名称和对应的id + GetRequest request = new GetRequest("books","1"); + // 发送请求后返回响应对象 + GetResponse response = client.get(request, RequestOptions.DEFAULT); + String json = response.getSourceAsString(); + System.out.println(json); +} +``` + +​ 根据id查询文档使用的请求对象是GetRequest。 + +**按条件查询文档**: + +```java +@Test +//按条件查询 +void testSearch() throws IOException { + SearchRequest request = new SearchRequest("books"); + + SearchSourceBuilder builder = new SearchSourceBuilder(); + // 根据条件查询文档,通过all里来查询,因为all里面存在合并字段 + builder.query(QueryBuilders.termQuery("all","spring")); + request.source(builder); + + SearchResponse response = client.search(request, RequestOptions.DEFAULT); + // 得到一个类似数组的玩意,遍历一下,得到一堆JSON,再转为Book对象 + SearchHits hits = response.getHits(); + for (SearchHit hit : hits) { + String source = hit.getSourceAsString(); + //System.out.println(source); + Book book = JSON.parseObject(source, Book.class); + System.out.println(book); + } +} +``` + +​ 按条件查询文档使用的请求对象是SearchRequest,查询时调用SearchRequest对象的termQuery方法,需要给出查询属性名,此处支持使用合并字段,也就是前面定义索引属性时添加的all属性。 + +​ springboot整合ES的操作到这里就说完了,与前期进行springboot整合redis和mongodb的差别还是蛮大的,主要原始就是我们没有使用springboot整合ES的客户端对象。至于操作,由于ES操作种类过多,所以显得操作略微有点复杂。有关springboot整合ES就先学习到这里吧。 + +**总结** + +1. springboot整合ES步骤 + 1. 导入springboot整合ES的High Level Client坐标 + 2. 手工管理客户端对象,包括初始化和关闭操作 + 3. 使用High Level Client根据操作的种类不同,选择不同的Request对象完成对应操作 + +## 整合第三方技术 + +### 缓存 + +缓存分为本地缓存和远程缓存,本地缓存中是在客户端本地或应用程序内部的缓存,而远程缓存则缓存在远程服务器上,例如Redis + +企业级应用主要作用是信息处理,当需要读取数据时,由于受限于数据库的访问效率,导致整体系统性能偏低。 + +![image-20231212184358556](https://s2.loli.net/2023/12/12/ZncIX7Q2su1KCNo.png) + +​ 应用程序直接与数据库打交道,访问效率低 + +​ 为了改善上述现象,开发者通常会在应用程序与数据库之间建立一种临时的数据存储机制,该区域中的数据在内存中保存,读写速度较快,可以有效解决数据库访问效率低下的问题。这一块临时存储数据的区域就是缓存。 + +![image-20231212184414836](https://s2.loli.net/2023/12/12/HMfpWXjmYZAFwI4.png) + +**使用缓存后,应用程序与缓存打交道,缓存与数据库打交道,数据访问效率提高** + +​ 缓存是什么?缓存是一种介于数据永久存储介质与应用程序之间的数据临时存储介质,使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能。此外缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间。而springboot提供了对市面上几乎所有的缓存技术进行整合的方案 + +#### SpringBoot内置缓存解决方案 + +​ springboot技术提供有内置的缓存解决方案,可以帮助开发者快速开启缓存技术,并使用缓存技术进行数据的快速操作,例如读取缓存数据和写入数据到缓存。 + +- 导入springboot提供的缓存技术对应的starter + +```xml + + org.springframework.boot + spring-boot-starter-cache + +``` + +- 启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存 + +```java +@SpringBootApplication +//开启缓存功能 +@EnableCaching +public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } +} +``` + +- 设置操作的数据是否使用缓存 + +```java +@Service +public class BookServiceImpl implements BookService { + @Autowired + private BookDao bookDao; + + @Cacheable(value="cacheSpace",key="#id") + public Book getById(Integer id) { + return bookDao.selectById(id); + } +} +``` + +​ 在业务方法上面使用注解@Cacheable声明当前方法的返回值放入缓存中,其中要指定缓存的存储位置,以及缓存中保存当前方法返回值对应的名称。上例中value属性描述缓存的存储位置,可以理解为是一个存储空间名,key属性描述了缓存中保存数据的名称,使用#id读取形参中的id值作为缓存名称。 + +​ 使用@Cacheable注解后,执行当前操作,如果发现对应名称在缓存中没有数据,就正常读取数据,然后放入缓存;如果对应名称在缓存中有数据,就终止当前业务方法执行,直接返回缓存中的数据。 + +#### SpringBoot整合Ehcache缓存 + +- 导入Ehcache的坐标 + +```xml + + net.sf.ehcache + ehcache + +``` + +- 配置缓存技术实现使用Ehcache + +```yaml +spring: + cache: + type: ehcache + ehcache: + # 配置文件 + config: ehcache.xml +``` + +​ 配置缓存的类型type为ehcache,此处需要说明一下,当前springboot可以整合的缓存技术中包含有ehcach,所以可以这样书写。其实这个type不可以随便写的,不是随便写一个名称就可以整合的。 + +​ 由于ehcache的配置有独立的配置文件格式,因此还需要指定ehcache的配置文件,以便于读取相应配置 + +```xml + + + + + + + + + + + + + + + + + +``` + +用法与前面相同 + +**总结** + +1. springboot使用Ehcache作为缓存实现需要导入Ehcache的坐标 +2. 修改设置,配置缓存供应商为ehcache,并提供对应的缓存配置文件 + +#### SpringBoot整合Memcached缓存 + +windows版安装包下载地址:https://www.runoob.com/memcached/window-install-memcached.html + +解压到一个文件夹下,里面有一个memcached.exe,使用该文件可以启动服务,在这个文件夹中打开命令行,通过`memcached.exe -d install`执行安装服务的命令,服务安装后,可以通过 + +```cmd +memcached.exe -d start # 启动服务 +memcached.exe -d stop # 停止服务 +``` + +详细的操作在下载地址中都有 + +​ memcached目前提供有三种客户端技术,分别是Memcached Client for Java、SpyMemcached和Xmemcached,其中性能指标各方面最好的客户端是Xmemcached,本次整合就使用这个作为客户端实现技术了。下面开始使用Xmemcached + +- 导入xmemcached的坐标 + +```xml + + com.googlecode.xmemcached + xmemcached + 2.4.7 + +``` + +- 配置memcached,制作memcached的配置类 + +```java +@Configuration +public class XMemcachedConfig { + @Bean + public MemcachedClient getMemcachedClient() throws IOException { + // 创建一个客户端 + MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder("localhost:11211"); + MemcachedClient memcachedClient = memcachedClientBuilder.build(); + return memcachedClient; + } +} +``` + +memcached默认对外服务端口11211。 + +- 使用xmemcached客户端操作缓存,注入MemcachedClient对象 + +```java +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private MemcachedClient memcachedClient; + + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + try { + // memcachedClient.set(key,过期时间,value); + memcachedClient.set(tele,10,code); + } catch (Exception e) { + e.printStackTrace(); + } + return code; + } + + public boolean checkCode(SMSCode smsCode) { + String code = null; + try { + code = memcachedClient.get(smsCode.getTele()).toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return smsCode.getCode().equals(code); + } +} +``` + +还是一种Get和Set的方法 + +**定义配置属性** + +- 定义配置类,加载必要的配置属性,读取配置文件中memcached节点信息 + +```java +@Component +@ConfigurationProperties(prefix = "memcached") +@Data +public class XMemcachedProperties { + private String servers; + private int poolSize; + private long opTimeout; +} +``` + +- 定义memcached节点信息 + +```yaml +memcached: + servers: localhost:11211 + poolSize: 10 + opTimeout: 3000 +``` + +- 在memcached配置类中加载信息 + +```java +@Configuration +public class XMemcachedConfig { + @Autowired + private XMemcachedProperties props; + @Bean + public MemcachedClient getMemcachedClient() throws IOException { + MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(props.getServers()); + memcachedClientBuilder.setConnectionPoolSize(props.getPoolSize()); + memcachedClientBuilder.setOpTimeout(props.getOpTimeout()); + MemcachedClient memcachedClient = memcachedClientBuilder.build(); + return memcachedClient; + } +} +``` + +**总结** + +1. memcached安装后需要启动对应服务才可以对外提供缓存功能,安装memcached服务需要基于windows系统管理员权限 +2. 由于springboot没有提供对memcached的缓存整合方案,需要采用手工编码的形式创建xmemcached客户端操作缓存 +3. 导入xmemcached坐标后,创建memcached配置类,注册MemcachedClient对应的bean,用于操作缓存 +4. 初始化MemcachedClient对象所需要使用的属性可以通过自定义配置属性类的形式加载 + +#### SpringBoot整合jetcache缓存 + +​ redis和mongodb需要安装独立的服务器,连接时需要输入对应的服务器地址,这种是远程缓存,Ehcache是一个典型的内存级缓存,因为它什么也不用安装,启动后导入jar包就有缓存功能了。这个时候就要问了,能不能这两种缓存一起用呢? + +​ 阿里的jetcache刚好就满足了这一点 + +​ jetcache严格意义上来说,并不是一个缓存解决方案,只能说他算是一个缓存框架,然后把别的缓存放到jetcache中管理,这样就可以支持AB缓存一起用了。并且jetcache参考了springboot整合缓存的思想,整体技术使用方式和springboot的缓存解决方案思想非常类似。 + +​ 目前jetcache支持的缓存方案本地缓存支持两种,远程缓存支持两种,分别如下: + +- 本地缓存(Local) + - LinkedHashMap + - Caffeine +- 远程缓存(Remote) + - Redis + - Tair + +这里使用的是LinkedHashMap+Redis的方案实现本地与远程缓存方案同时使用。 + +##### 纯远程方案 + +导入springboot整合jetcache对应的坐标starter,当前坐标默认使用的远程方案是redis + +```xml + + com.alicp.jetcache + jetcache-starter-redis + 2.6.2 + +``` + +远程方案基本配置 + +```yml +jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +``` + +poolConfig是必配项,否则会报错 + +启用缓存,在引导类上方标注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式创建缓存 + +```java +@SpringBootApplication +//jetcache启用缓存的主开关 +@EnableCreateCacheAnnotation +public class Springboot20JetCacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot20JetCacheApplication.class, args); + } +} +``` + +创建缓存对象Cache,并使用注解@CreateCache标记当前缓存的信息,然后使用Cache对象的API操作缓存,put写缓存,get读缓存。 + +```java +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + + // 创建缓存,name为jetCache_,expire是10秒(保存时间) + @CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS) + private Cache jetCache; + + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + // 创造出code后保存到jetCache中 + jetCache.put(tele,code); + return code; + } + + public boolean checkCode(SMSCode smsCode) { + // 检查code时get出来 + String code = jetCache.get(smsCode.getTele()); + return smsCode.getCode().equals(code); + } +} +``` + +​ 通过上述jetcache使用远程方案连接redis可以看出,jetcache操作缓存时的接口操作更符合开发者习惯,使用缓存就先获取缓存对象Cache,放数据进去就是put,取数据出来就是get,更加简单易懂。并且jetcache操作缓存时,可以为某个缓存对象设置过期时间,将同类型的数据放入缓存中,方便有效周期的管理。 + +​ 上述方案中使用的是配置中定义的default缓存,其实这个default是个名字,可以随便写,也可以随便加。例如再添加一种缓存解决方案,参照如下配置进行 + +```yaml +jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 + # 这里的sms可以是任意的name + sms: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +``` + +如果想使用名称是sms的缓存,需要再创建缓存时指定参数area,声明使用对应缓存即可 + +```java +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + + // area指定使用名称为sms的缓存 + @CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS) + private Cache jetCache; + + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + jetCache.put(tele,code); + return code; + } + + public boolean checkCode(SMSCode smsCode) { + String code = jetCache.get(smsCode.getTele()); + return smsCode.getCode().equals(code); + } +} +``` + +##### **纯本地方案** + +远程方案中,配置中使用remote表示远程,换成local就是本地,只不过类型不一样而已。 + +导入springboot整合jetcache对应的坐标starter + +```xml + + com.alicp.jetcache + jetcache-starter-redis + 2.6.2 + +``` + +本地缓存基本配置 + +```yaml +jetcache: + local: + default: + type: linkedhashmap + keyConvertor: fastjson +``` + +为了加速数据获取时key的匹配速度,jetcache要求指定key的类型转换器。简单说就是,如果你给了一个Object作为key的话,我先用key的类型转换器给转换成字符串,然后再保存。等到获取数据时,仍然是先使用给定的Object转换成字符串,然后根据字符串匹配。由于jetcache是阿里的技术,这里推荐key的类型转换器使用阿里的fastjson。 + +启用缓存 + +```java +@SpringBootApplication +//jetcache启用缓存的主开关 +@EnableCreateCacheAnnotation +public class Springboot20JetCacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot20JetCacheApplication.class, args); + } +} +``` + +创建缓存对象Cache时,标注当前使用本地缓存 + +cacheType控制当前缓存使用本地缓存还是远程缓存,配置cacheType=CacheType.LOCAL即使用本地缓存。 + +```java +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + // cacheType标注使用本地缓存 + @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL) + private Cache jetCache; + + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + jetCache.put(tele,code); + return code; + } + + public boolean checkCode(SMSCode smsCode) { + String code = jetCache.get(smsCode.getTele()); + return smsCode.getCode().equals(code); + } +} +``` + +##### 本地+远程方案 + +本地和远程方法都有了,两种方案一起使用如何配置呢?其实就是将两种配置合并到一起就可以了。 + +```yaml +jetcache: +# 本地 + local: + default: + type: linkedhashmap + keyConvertor: fastjson +# 远程 + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 + sms: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +``` + +在创建缓存的时候,配置cacheType为BOTH即则本地缓存与远程缓存同时使用。 + +```java +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH) + private Cache jetCache; +} +``` + +cacheType如果不进行配置,默认值是REMOTE,即仅使用远程缓存方案。关于jetcache的配置,参考以下信息 + +| 属性 | 默认值 | 说明 | +| --------------------------------------------------------- | ------ | ------------------------------------------------------------ | +| jetcache.statIntervalMinutes | 0 | 统计间隔,0表示不统计 | +| jetcache.hiddenPackages | 无 | 自动生成name时,隐藏指定的包名前缀 | +| jetcache.[local\|remote].${area}.type | 无 | 缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair | +| jetcache.[local\|remote].${area}.keyConvertor | 无 | key转换器,当前仅支持fastjson | +| jetcache.[local\|remote].${area}.valueEncoder | java | 仅remote类型的缓存需要指定,可选java和kryo | +| jetcache.[local\|remote].${area}.valueDecoder | java | 仅remote类型的缓存需要指定,可选java和kryo | +| jetcache.[local\|remote].${area}.limit | 100 | 仅local类型的缓存需要指定,缓存实例最大元素数 | +| jetcache.[local\|remote].${area}.expireAfterWriteInMillis | 无穷大 | 默认过期时间,毫秒单位 | +| jetcache.local.${area}.expireAfterAccessInMillis | 0 | 仅local类型的缓存有效,毫秒单位,最大不活动间隔 | + +以上方案仅支持手工控制缓存,但是springcache方案中的方法缓存特别好用,给一个方法添加一个注解,方法就会自动使用缓存。jetcache也提供了对应的功能,即方法缓存。 + +##### **方法缓存** + +​ jetcache提供了方法缓存方案,只不过名称变更了而已。在对应的操作接口上方使用注解@Cached即可 + +导入springboot整合jetcache对应的坐标starter + +```xml + + com.alicp.jetcache + jetcache-starter-redis + 2.6.2 + +``` + +配置缓存 + +```yaml +jetcache: + local: + default: + type: linkedhashmap + keyConvertor: fastjson + remote: + default: + type: redis + host: localhost + port: 6379 + keyConvertor: fastjson + valueEncode: java + valueDecode: java + poolConfig: + maxTotal: 50 + sms: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +``` + +由于redis缓存中不支持保存对象,因此需要对redis设置当Object类型数据进入到redis中时如何进行类型转换。需要配置keyConvertor表示key的类型转换方式,同时标注value的转换类型方式,值进入redis时是java类型,标注valueEncode为java,值从redis中读取时转换成java,标注valueDecode为java。 + +**注意,为了实现Object类型的值进出redis,需要保障进出redis的Object类型的数据必须实现序列化接口。** + +```java +@Data +public class Book implements Serializable { + private Integer id; + private String type; + private String name; + private String description; +} +``` + +启用缓存时开启方法缓存功能,并配置basePackages,说明在哪些包中开启方法缓存 + +```java +@SpringBootApplication +//jetcache启用缓存的主开关 +@EnableCreateCacheAnnotation +//开启方法注解缓存 +@EnableMethodCache(basePackages = "需要开启缓存的包位置") +public class Springboot20JetCacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot20JetCacheApplication.class, args); + } +} +``` + +使用注解@Cached标注当前方法使用缓存,查询出来的东西会加入到内存中 + +```java +@Service +public class BookServiceImpl implements BookService { + @Autowired + private BookDao bookDao; + + @Override + @Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE) + public Book getById(Integer id) { + return bookDao.selectById(id); + } +} +``` + +##### 远程方案的数据同步 + +​ 由于远程方案中redis保存的数据可以被多个客户端共享,这就存在了数据同步问题。jetcache提供了3个注解解决此问题,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据 + +**更新缓存** + +```java +// #是将该值获取,value="#xxx"就是获取到xxx的值 +@CacheUpdate(name="book_",key="#book.id",value="#book") +public boolean update(Book book) { + return bookDao.updateById(book) > 0; +} +``` + +**删除缓存** + +```JAVA +@CacheInvalidate(name="book_",key = "#id") +public boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; +} +``` + +**定时刷新缓存** + +```JAVA +@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE) +// 当缓存中的数据存在时,每5秒刷新一次 +@CacheRefresh(refresh = 5) +public Book getById(Integer id) { + return bookDao.selectById(id); +} +``` + +##### 数据报表 + +jetcache还提供有简单的数据报表功能,帮助开发者快速查看缓存命中信息,只需要添加一个配置即可 + +```yaml +jetcache: + statIntervalMinutes: 1 +``` + +设置后,每1分钟在控制台输出缓存数据命中信息 + +``` +[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003 +cache | qps| rate| get| hit| fail| expire| avgLoadTime| maxLoadTime +---------+-------+-------+------+-------+-------+---------+--------------+-------------- +book_ | 0.66| 75.86%| 29| 22| 0| 0| 28.0| 188 +---------+-------+-------+------+-------+-------+---------+--------------+-------------- +``` + +**总结** + +1. jetcache是一个类似于springcache的缓存解决方案,自身不具有缓存功能,它提供有本地缓存与远程缓存多级共同使用的缓存解决方案 +2. jetcache提供的缓存解决方案受限于目前支持的方案,本地缓存支持两种,远程缓存支持两种 +3. 注意数据进入远程缓存时的类型转换问题 +4. jetcache提供方法缓存,并提供了对应的缓存更新与刷新功能 +5. jetcache提供有简单的缓存信息命中报表方便开发者即时监控缓存数据命中情况 + +#### SpringBoot整合j2cache缓存 + +导入j2cache、redis、ehcache坐标 + +```xml + + net.oschina.j2cache + j2cache-core + 2.8.4-release + + + net.oschina.j2cache + j2cache-spring-boot2-starter + 2.8.0-release + + + net.sf.ehcache + ehcache + +``` + +j2cache的starter中默认包含了redis坐标,官方推荐使用redis作为二级缓存,因此此处无需导入redis坐标 + +配置一级与二级缓存,并配置一二级缓存间数据传递方式,配置书写在名称为j2cache.properties的文件中。如果使用ehcache还需要单独添加ehcache的配置文件 + +```yaml +server: + port: 80 + +j2cache: +# j2cache的配置文件名 + config-location: j2cache.properties +``` + +j2cache的配置文件:j2cache.properties + +```properties +# 1级缓存的配置 +j2cache.L1.provider_class = ehcache +# 1级缓存的配置文件名 +ehcache.configXml = ehcache.xml + +# 2级缓存的配置 +j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider +# 支持自定义前缀名(j2cache.L2.config_section)这里的内容填什么,下面的前缀就填什么 +j2cache.L2.config_section = redis +redis.hosts = localhost:6379 + +# 1级缓存如何到达2级缓存 +# 通过消息的订阅与发布 +j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy + + +``` + +```java +import com.itheima.domain.SMSCode; +import com.itheima.service.SMSCodeService; +import com.itheima.utils.CodeUtils; +import net.oschina.j2cache.CacheChannel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SMSCodeServiceImpl implements SMSCodeService { + + @Autowired + private CodeUtils codeUtils; + + @Autowired + // 缓存对象 + private CacheChannel cacheChannel; + + @Override + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + // set(name,key,value) + cacheChannel.set("sms",tele,code); + return code; + } + + @Override + public boolean checkCode(SMSCode smsCode) { + // get(name,key) + String code = cacheChannel.get("sms",smsCode.getTele()).asString(); + return smsCode.getCode().equals(code); + } + +} + +``` + +​ j2cache的使用和jetcache比较类似,但是无需开启使用的开关,直接定义缓存对象即可使用,缓存对象名CacheChannel。 + +​ j2cache的使用不复杂,配置是j2cache的核心,毕竟是一个整合型的缓存框架。缓存相关的配置过多,可以查阅j2cache-core核心包中的j2cache.properties文件中的说明。 + +```properties +#J2Cache configuration +######################################### +# Cache Broadcast Method +# values: +# jgroups -> use jgroups's multicast +# redis -> use redis publish/subscribe mechanism (using jedis) +# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend) +# rabbitmq -> use RabbitMQ publisher/consumer mechanism +# rocketmq -> use RocketMQ publisher/consumer mechanism +# none -> don't notify the other nodes in cluster +# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy +######################################### +j2cache.broadcast = redis + +# jgroups properties +jgroups.channel.name = j2cache +jgroups.configXml = /network.xml + +# RabbitMQ properties +rabbitmq.exchange = j2cache +rabbitmq.host = localhost +rabbitmq.port = 5672 +rabbitmq.username = guest +rabbitmq.password = guest + +# RocketMQ properties +rocketmq.name = j2cache +rocketmq.topic = j2cache +# use ; to split multi hosts +rocketmq.hosts = 127.0.0.1:9876 + +######################################### +# Level 1&2 provider +# values: +# none -> disable this level cache +# ehcache -> use ehcache2 as level 1 cache +# ehcache3 -> use ehcache3 as level 1 cache +# caffeine -> use caffeine as level 1 cache(only in memory) +# redis -> use redis as level 2 cache (using jedis) +# lettuce -> use redis as level 2 cache (using lettuce) +# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available. +# memcached -> use memcached as level 2 cache (xmemcached), +# [classname] -> use custom provider +######################################### + +j2cache.L1.provider_class = caffeine +j2cache.L2.provider_class = redis + +# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations +# j2cache.L2.config_section = redis + +# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true) +# NOTICE: redis hash mode (redis.storage = hash) do not support this feature) +j2cache.sync_ttl_to_redis = true + +# Whether to cache null objects by default (default false) +j2cache.default_cache_null_object = true + +######################################### +# Cache Serialization Provider +# values: +# fst -> using fast-serialization (recommend) +# kryo -> using kryo serialization +# json -> using fst's json serialization (testing) +# fastjson -> using fastjson serialization (embed non-static class not support) +# java -> java standard +# fse -> using fse serialization +# [classname implements Serializer] +######################################### + +j2cache.serialization = json +#json.map.person = net.oschina.j2cache.demo.Person + +######################################### +# Ehcache configuration +######################################### + +# ehcache.configXml = /ehcache.xml + +# ehcache3.configXml = /ehcache3.xml +# ehcache3.defaultHeapSize = 1000 + +######################################### +# Caffeine configuration +# caffeine.region.[name] = size, xxxx[s|m|h|d] +# +######################################### +caffeine.properties = /caffeine.properties + +######################################### +# Redis connection configuration +######################################### + +######################################### +# Redis Cluster Mode +# +# single -> single redis server +# sentinel -> master-slaves servers +# cluster -> cluster servers (数据库配置无效,使用 database = 0) +# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0) +# +######################################### + +redis.mode = single + +#redis storage mode (generic|hash) +redis.storage = generic + +## redis pub/sub channel name +redis.channel = j2cache +## redis pub/sub server (using redis.hosts when empty) +redis.channel.host = + +#cluster name just for sharded +redis.cluster_name = j2cache + +## redis cache namespace optional, default[empty] +redis.namespace = + +## redis command scan parameter count, default[1000] +#redis.scanCount = 1000 + +## connection +# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379 + +redis.hosts = 127.0.0.1:6379 +redis.timeout = 2000 +redis.password = +redis.database = 0 +redis.ssl = false + +## redis pool properties +redis.maxTotal = 100 +redis.maxIdle = 10 +redis.maxWaitMillis = 5000 +redis.minEvictableIdleTimeMillis = 60000 +redis.minIdle = 1 +redis.numTestsPerEvictionRun = 10 +redis.lifo = false +redis.softMinEvictableIdleTimeMillis = 10 +redis.testOnBorrow = true +redis.testOnReturn = false +redis.testWhileIdle = true +redis.timeBetweenEvictionRunsMillis = 300000 +redis.blockWhenExhausted = false +redis.jmxEnabled = false + +######################################### +# Lettuce scheme +# +# redis -> single redis server +# rediss -> single redis server with ssl +# redis-sentinel -> redis sentinel +# redis-cluster -> cluster servers +# +######################################### + +######################################### +# Lettuce Mode +# +# single -> single redis server +# sentinel -> master-slaves servers +# cluster -> cluster servers (数据库配置无效,使用 database = 0) +# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0) +# +######################################### + +## redis command scan parameter count, default[1000] +#lettuce.scanCount = 1000 +lettuce.mode = single +lettuce.namespace = +lettuce.storage = hash +lettuce.channel = j2cache +lettuce.scheme = redis +lettuce.hosts = 127.0.0.1:6379 +lettuce.password = +lettuce.database = 0 +lettuce.sentinelMasterId = +lettuce.maxTotal = 100 +lettuce.maxIdle = 10 +lettuce.minIdle = 10 +# timeout in milliseconds +lettuce.timeout = 10000 +# redis cluster topology refresh interval in milliseconds +lettuce.clusterTopologyRefresh = 3000 + +######################################### +# memcached server configurations +# refer to https://gitee.com/mirrors/XMemcached +######################################### + +memcached.servers = 127.0.0.1:11211 +memcached.username = +memcached.password = +memcached.connectionPoolSize = 10 +memcached.connectTimeout = 1000 +memcached.failureMode = false +memcached.healSessionInterval = 1000 +memcached.maxQueuedNoReplyOperations = 100 +memcached.opTimeout = 100 +memcached.sanitizeKeys = false +``` + +**总结** + +1. j2cache是一个缓存框架,自身不具有缓存功能,它提供多种缓存整合在一起使用的方案 +2. j2cache需要通过复杂的配置设置各级缓存,以及缓存之间数据交换的方式 +3. j2cache操作接口通过CacheChannel实现 + +### 定时任务 + +定时任务非常的常见,比如说定时统计报表、缓存统计报告之类的 + +Java就有提供了默认的Api + +```java +import java.util.Timer; +import java.util.TimerTask; + +public class TimerTaskApp { + public static void main(String[] args) { + // 创建定时器的对象 + Timer timer = new Timer(); + // 定时任务 + TimerTask task = new TimerTask() { + @Override + public void run() { + System.out.println("timer task run..."); + } + }; + // schedule(要执行的任务(task),什么时候执行(0表示立刻执行,可以设置为1000,说明是1秒后执行),间隔几秒执行一次(2000毫秒执行一次)) + timer.schedule(task,0,2000); + } +} +``` + +但这样还是不太行,所以出现了一个定时任务框架,Quartz,然后Spring也造出了一个类似的,而且比Quartz快一丢丢,是一个叫Spring Task的玩意 + +想使用Quartz需要先引入依赖,SpringBoot内置有这个quartz,所以不需要添加版本 + +```xml + + org.springframework.boot + spring-boot-starter-quartz + +``` + +创建一个类继承并实现`QuartzJobBean`里面的方法 + +```java +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.scheduling.quartz.QuartzJobBean; + +// 继承QuartzJobBean +public class MyQuartz extends QuartzJobBean { + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + System.out.println("quartz task run..."); + } +} +``` + +设定一个配置类,在配置类里新建一个工作详情(JobDetail)和一个触发器(Trigger),用于绑定这个JobDetail + +```java +import com.itheima.quartz.MyQuartz; +import org.quartz.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// 配置类 +@Configuration +public class QuartzConfig { + + // 工作明细,具体要做什么 + @Bean + public JobDetail printJobDetail(){ + //绑定具体的工作 + // 需要进行工作的类 + // storeDurably:当工作类不使用时,对工作类做持久化操作 + return JobBuilder.newJob(MyQuartz.class).storeDurably().build(); + } + + @Bean + public Trigger printJobTrigger(){ + // 0/5 * * * * ?:从任意年任意月任意日任意时任意分,每5秒执行一次 + ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?"); + //绑定对应的工作明细 + // forJob(printJobDetail触发器) + return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build(); + } + +} +``` + +#### SpringBoot整合Task + +在启动类上开启定时任务功能 +`@EnableScheduling` + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +//开启定时任务功能 +@EnableScheduling +public class Springboot22TaskApplication { + + public static void main(String[] args) { + SpringApplication.run(Springboot22TaskApplication.class, args); + } + +} +``` + +接着定义一个Bean容器用于编写定时任务 + +```java +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + // 注解式定时任务 + @Scheduled(cron = "0/1 * * * * ?") + public void print(){ + System.out.println(Thread.currentThread().getName()+" :spring task run..."); + } + +} +``` + +可以在application.yml中对定时任务进行配置 + +```yaml +spring: + task: + scheduling: + # 任务调度线程池大小 默认1 + pool: + size: 1 + # 调度线程名称前缀 默认scheduling- + thread-name-prefix: spring_tasks_ + # 线程池关闭时的操作 + shutdown: + # 线程池关闭时是否等待所有任务完成 + await-termination: false + # 调度线程关闭前最大等待时间,确保最后一定关闭 + await-termination-period: 10s +``` + +### SpringBoot整合JavaMail + +- SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议 +- POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议 +- IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议 + +#### 简单邮件发送 + +导入坐标 + +```xml + + org.springframework.boot + spring-boot-starter-mail + +``` + +在yml配置文件中进行配置 + +```yml +spring: + mail: + # 邮件供应商,例如163,126等 + # 使用163: smtp.163.com + # 使用qq: smtp.qq.com + # 使用126: smtp.126.com + host: smtp.163.com + # 邮箱的名称 + username: test@126.com + # 密码需要通过邮箱获取 + password: test +``` + +拿QQ邮箱举例,我们需要先开启对应的服务 + +![image-20231213111003856](https://s2.loli.net/2023/12/13/uh1nExr7XTMRe43.png) + +开启之后,会出现一个授权码,这个授权码就是发送邮箱的密码 + +编写一个接口 + +```java +public interface SendMailService { + void sendMail(); +} +``` + +编写对应的实现类 + +```java +import com.itheima.service.SendMailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class SendMailServiceImpl implements SendMailService { + + @Autowired + private JavaMailSender javaMailSender; + + //发送人 + private String from = "test@qq.com"; + //接收人 + private String to = "test@qq.com"; + //标题 + private String subject = "测试邮件"; + //正文 + private String context = "测试邮件正文内容"; + + @Override + public void sendMail() { + SimpleMailMessage message = new SimpleMailMessage(); + // from+"(O了个Big K)"这个+后面的内容会替代发邮件的发件人 + message.setFrom(from+"(O了个Big K)"); + message.setTo(to); + message.setSubject(subject); + message.setText(context); + // 发送邮件 + javaMailSender.send(message); + } +} +``` + +编写测试类 + +```java +import com.itheima.service.SendMailService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Springboot23MailApplicationTests { + + @Autowired + private SendMailService sendMailService; + + @Test + void contextLoads() { + sendMailService.sendMail(); + } +} +``` + +#### 复杂邮件发送 + +```java +import com.itheima.service.SendMailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.mail.internet.MimeMessage; + +@Service +public class SendMailServiceImpl2 implements SendMailService { + + @Autowired + private JavaMailSender javaMailSender; + + //发送人 + private String from = "test@qq.com"; + //接收人 + private String to = "test@126.com"; + //标题 + private String subject = "测试邮件"; + //正文:可以编写类似于html之类的标签内容 + private String context = "" + + "" + + "点开有惊喜"; + + @Override + public void sendMail(int i) { + + try { + MimeMessage message = javaMailSender.createMimeMessage(); + // 这里的ture表示的是否运行添加多附件:MimeMessageHelper helper = new MimeMessageHelper(message,true); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(to + "(小甜甜)"); + helper.setTo(from); + helper.setSubject(subject); + // setText(context,true);这里的true表示的是,是否解析html + helper.setText(context, true); + + //添加附件 +// File f1 = new File("D:\\workspace\\springboot\\springboot_23_mail\\target\\springboot_23_mail-0.0.1-SNAPSHOT.jar"); +// File f2 = new File("D:\\workspace\\springboot\\springboot_23_mail\\src\\main\\resources\\logo.png"); + + // 添加文件名 +// helper.addAttachment(f1.getName(),f1); +// helper.addAttachment("最靠谱的培训结构.png",f2); + + javaMailSender.send(message); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +### 消息 + +#### 简介 + +消息有两种,一种是同步消息,一种是异步消息,比较常用的异步消息,同步消息比较占用性能 + +- 同步消息:当你发出消息后,**必须**得等待对方**给你回应后**,你才能进行下一步的操作 +- 异步消息:当你发出消息后,无论对方是否给你回复,你都可以继续下一步的操作 + +- 企业级应用中广泛使用的三种异步消息传递技术 + - JMS + - AMQP + - MQTT + +##### JMS + +- JMS(Java Message Service):一个规范,等同于JDBC规范,提供了消息服务相关的API接口 +- JMS消息模型 + - peer-2-peer:点对点模型,消息发送到一个队列中,队列保存消息。队列的消息只能被一个消费者消费,或超时 + - publish-subscribe:发布订阅模型,消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在 +- JMS消息种类 + - TextMessage + - MapMessage + - BytesMessage + - StreamMessage + - ObjectMessage + - Messgae(只有消息头和属性) +- JMS实现:ActiveMQ、Redis、HornetMQ、RabbitMQ、RocketMQ(没有完全遵守JMS规范) + +##### AMQP + +- AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也就是消息代理规范),规范了网络交换的数据格式,兼容JMS +- 优点:具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现 +- AMQP消息模型 + - direct exchange + - fanout exchange + - topic exchange + - headers exchange + - system exchange +- AMQP消息种类:byte[] +- AMQP实现:RabbitMQ、StormMQ、RocketMQ + +##### MQTT + +- MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,为小设备设计,是物联网(IOT)生态系统中主要成分之一 + +##### Kafka + +Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能 + +##### ActiveMq的使用及安装 + +windows版安装包下载地址:[https://activemq.apache.org/components/classic/download](https://activemq.apache.org/components/classic/download/)[/](https://activemq.apache.org/components/classic/download/) + +下载下来后,直接解压到本地即可 + +![image-20231213194157602](https://s2.loli.net/2023/12/13/KUe9MmN26s5lPYv.png) + +启动后 + +访问:http://localhost:8161/index.html + +第一次登录可能要用户名和密码,这俩都是**admin** + +接着就登录到管理后台了 + +- 服务端口是61616,管理后台端口:8161 + +###### SpringBoot整合ActiveMq + +导入ActiveMq的依赖 + +```xml + + org.springframework.boot + spring-boot-starter-activemq + +``` + +添加ActiveMq的配置 + +```yaml +spring: + activemq: + broker-url: tcp:localhost:61616 +``` + +编写ActiveMq的准备工作 + +订单类 + +```java +public interface OrderService { + + void order(String id); + +} +``` + +订单实现类,使用该类对订单进行操作 + +```java +import com.example.springboot_mq.service.MessageService; +import com.example.springboot_mq.service.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class OrderServiceImpl implements OrderService { + + @Autowired + private MessageService messageService; + + @Override + public void order(String id) { + // 一系列的操作,包含各种服务调用, 处理各种业务 + System.out.println("订单处理开始"); + // 短信消息处理 + messageService.sendMessage(id); + System.out.println("订单处理接收"); + System.out.println(); + } +} +``` + +消息实现类,编写获取订单id和传入订单的方法 + +```java +public interface MessageService { + // 传入订单id + void sendMessage(String id); + + // 获取订单id + String doMessage(); +} +``` + +消息队列的模拟实现 + +```java +import com.example.springboot_mq.service.MessageService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +//@Service +public class MessageServiceImpl implements MessageService { + + // 存储id,类似于消息队列 + private ArrayList msgList = new ArrayList<>(); + + @Override + public void sendMessage(String id) { + System.out.println("待发送短信的订单已纳入处理队列,id"+id); + // 订单纳入消息队列 + msgList.add(id); + } + + @Override + public String doMessage() { + // 订单移除消息队列 + String id = msgList.remove(0); + System.out.println("已完成短信发送业务,id"+id); + return id; + } +} +``` + +消息队列的真正实现 + +```java +import com.example.springboot_mq.service.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + + +@Service +public class MessageServiceActivemqImpl implements MessageService { + + @Autowired + private JmsMessagingTemplate messagingTemplate; + + @Override + public void sendMessage(String id) { + System.out.println("待发送短信的订单已纳入处理队列,id"+id); + // 将消息加入消息队列 + messagingTemplate.convertAndSend(id); + } + + @Override + public String doMessage() { + // 如何发送的,就需要如何接收,所以就要将类型转为之前发送的类型 + String id = messagingTemplate.receiveAndConvert(String.class); + System.out.println("已完成短信发送业务,id"+id); + return id; + } +} +``` + +还需要修改一下application.yml文件的配置,需要连接到ActiveMq的服务器上,并且配置消息的保存位置,类似于放在哪个消息队列中 + +```yaml +server: + port: 80 +spring: + activemq: + broker-url: tcp://localhost:61616 + user: admin + password: admin + jms: + template: + default-destination: eastwind +``` + +接着是两个Controller用于调试 + +获取消息队列的id + +```java +import com.example.springboot_mq.service.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/msg") +public class MessageController { + + @Autowired + private MessageService messageService; + + @GetMapping + public String doMessage(){ + String id = messageService.doMessage(); + return id; + } + +} +``` + +添加id到消息队列中 + +```java +import com.example.springboot_mq.service.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/orders") +public class OrderController { + + @Autowired + private OrderService orderService; + + @PostMapping("{id}") + public void order(@PathVariable String id){ + orderService.order(id); + } + +} +``` + +访问该地址:`localhost:80//orders/1`,添加一个订单id到消息队列中 + +访问该地址:`localhost:80//msg`,处理一个订单并移除队列 + +接着如果我们再去访问`localhost:80//msg`后,就会出现线程堵塞的问题,因为此时没有订单,你再去获取它,就会等待消息的发布,此时再添加,添加成功的同时,订单也被队列处理了 + +如果添加了订单到消息队列中,访问后台`http://localhost:8161/index.html`后,查看队列,就会发现,出现了一个eastwind的队列 + +![image-20231213214934115](https://s2.loli.net/2023/12/13/vZd6ahMsKYq3WDH.png) + +在后台中,也能很明显的看到对应的信息 + +有一个消息没有被消费,已经被消费的消息数量是2个 + +虽然消息订阅与发布已经完成了,但是这样做不太合理,因为每个消息都发到了这个默认的消息队列(eastwind)中,所以我们需要做一种比较合理的方式 + +```java +import com.example.springboot_mq.service.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + + +@Service +public class MessageServiceActivemqImpl implements MessageService { + + @Autowired + private JmsMessagingTemplate messagingTemplate; + + @Override + public void sendMessage(String id) { + System.out.println("待发送短信的订单已纳入处理队列,id"+id); + // 将消息加入消息队列,并指定所加入的消息队列 + messagingTemplate.convertAndSend("order.queue.id",id); + } + + @Override + public String doMessage() { + // 如何发送的,就需要如何接收,所以就要将类型转为之前发送的类型 + String id = messagingTemplate.receiveAndConvert(String.class); + System.out.println("已完成短信发送业务,id"+id); + return id; + } +} +``` + +当消息投递到队列中后,我们不应该手动去消费,而是让队列自己,主动去消费 + +添加一个队列的监听器,监听指定的一个队列 + +```java +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class MessageListener { + + // 监听投递到该队列的消息,自动处理进行操作 + @JmsListener(destination = "order.queue.id") + public void receive(String id){ + System.out.println("已完成短信发送业务,id" + id); + } + +} +``` + +如果当前队列处理完成后,想交给下一个队列处理,该怎么做呢 + +```java +import org.springframework.jms.annotation.JmsListener; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Component; + +@Component +public class MessageListener { + + // 监听投递到该队列的消息,自动处理进行操作 + @JmsListener(destination = "order.queue.id") + // 该方法结束后的返回值投递到order.test.id的队列中继续进行操作 + @SendTo("order.test.id") + public String receive(String id){ + System.out.println("已完成短信发送业务,id" + id); + return "new " + id; + } + +} +``` + +切换消息模型由点对点模型到发布订阅模型,修改jms配置即可 + +```yaml +spring: + activemq: + broker-url: tcp://localhost:61616 + jms: + pub-sub-domain: true +``` + +pub-sub-domain默认值为false,即点对点模型,修改为true后就是发布订阅模型。 + +##### RabbitMq的使用及安装 + +RabbitMQ是MQ产品中的目前较为流行的产品之一,它遵从AMQP协议。RabbitMQ的底层实现语言使用的是Erlang,所以安装RabbitMQ需要先安装Erlang。 + +​ windows版安装包下载地址:[https](https://www.erlang.org/downloads)[://www.erlang.org/downloads](https://www.erlang.org/downloads) + +​ 下载完毕后得到exe安装文件,一键傻瓜式安装,安装完毕**需要重启,需要重启,需要重启**。 + +Erlang安装后需要配置环境变量,否则RabbitMQ将无法找到安装的Erlang。需要配置项如下,作用等同JDK配置环境变量的作用。 + +- ERLANG_HOME:在环境变量中进行配置。路径就是安装Erlang的路径 + +![image-20231214133504364](https://s2.loli.net/2023/12/14/wXqar4ynupCRWzx.png) + +- 在Path中添加`%ERLANG_HOME%\bin` + +windows版安装包下载地址:[https://](https://rabbitmq.com/install-windows.html)[rabbitmq.com/install-windows.html](https://rabbitmq.com/install-windows.html) + +下载完毕后得到exe安装文件,一键傻瓜式安装 + +在rabbitMq下的sbin目录,该目录就是可执行文件所在的位置 + +在该目录打开cmd,输入`rabbitmq-service.bat start`,启动rabbitmq + +`rabbitmq-service.bat stop`,停止rabbitmq + +![image-20231214135751022](https://s2.loli.net/2023/12/14/mRekIxdvSsPCHDr.png) + +RabbitMQ也提供有web控制台服务,但是此功能是一个插件,需要先启用才可以使用 + +``` +rabbitmq-plugins.bat list # 查看当前所有插件的运行状态 +rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件 +``` + +​ 启动插件后可以在插件运行状态中查看是否运行,运行后通过浏览器即可打开服务后台管理界面 + +``` +http://localhost:15672 +``` + +web管理服务默认端口15672,访问后可以打开RabbitMQ的管理界面 + +首先输入访问用户名和密码,初始化用户名和密码相同,均为:guest,成功登录后进入管理后台界面 + +##### 整合RabbitMq + +###### direct模型 + +导入rabbitmq的依赖 + +```xml + + org.springframework.boot + spring-boot-starter-amqp + +``` + +配置RabbitMQ的服务器地址 + +```yaml +spring: + rabbitmq: + host: localhost + port: 5672 +``` + +编写配置类,配置交换机、队列、交换机和队列绑定的情况 + +```java +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitConfigDirect { + + @Bean + public Queue directQueue(){ + // 第一个true表示:是否持久化 + // 第二个true表示:是否是当前连接专用,如果是连接专用,当连接关闭后,当前队列也随之关闭了 + // 第三个true表示:是否自动删除 + // return new Queue("direct_queue",true,true,true); + return new Queue("direct_queue"); + } + + @Bean + public Queue directQueue2(){ + // 第一个true表示:是否持久化 + // 第二个true表示:是否是当前连接专用,如果是连接专用,当连接关闭后,当前队列也随之关闭了 + // 第三个true表示:是否自动删除 + // return new Queue("direct_queue",true,true,true); + return new Queue("direct_queue2"); + } + + @Bean + public DirectExchange directExchange(){ + return new DirectExchange("directExchange"); + } + + @Bean + public Binding bindingDirect(){ + // 将队列绑定到交换机上,并且叫做direct + return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct"); + } + + @Bean + public Binding bindingDirect2(){ + // 将队列绑定到交换机上,并且叫做direct + return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct2"); + } + +} +``` + +###### 整合(topic模型) + +**步骤①**:同上 + +**步骤②**:同上 + +**步骤③**:初始化主题模式系统设置 + +```JAVA +@Configuration +public class RabbitConfigTopic { + @Bean + public Queue topicQueue(){ + return new Queue("topic_queue"); + } + @Bean + public Queue topicQueue2(){ + return new Queue("topic_queue2"); + } + @Bean + public TopicExchange topicExchange(){ + return new TopicExchange("topicExchange"); + } + @Bean + public Binding bindingTopic(){ + return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id"); + } + @Bean + public Binding bindingTopic2(){ + return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.orders.*"); + } +} +``` + +​ 主题模式支持routingKey匹配模式,*表示匹配一个单词,#表示匹配任意内容,这样就可以通过主题交换机将消息分发到不同的队列中,详细内容请参看RabbitMQ系列课程。 + +| **匹配键** | **topic.\*.\*** | **topic.#** | +| ----------------- | --------------- | ----------- | +| topic.order.id | true | true | +| order.topic.id | false | false | +| topic.sm.order.id | false | true | +| topic.sm.id | false | true | +| topic.id.order | true | true | +| topic.id | false | true | +| topic.order | false | true | + +**步骤④**:使用AmqpTemplate操作RabbitMQ + +```java +@Service +public class MessageServiceRabbitmqTopicImpl implements MessageService { + @Autowired + private AmqpTemplate amqpTemplate; + + @Override + public void sendMessage(String id) { + System.out.println("待发送短信的订单已纳入处理队列(rabbitmq topic),id:"+id); + amqpTemplate.convertAndSend("topicExchange","topic.orders.id",id); + } +} +``` + +​ 发送消息后,根据当前提供的routingKey与绑定交换机时设定的routingKey进行匹配,规则匹配成功消息才会进入到对应的队列中。 + +**步骤⑤**:使用消息监听器在服务器启动后,监听指定队列 + +```JAVA +@Component +public class MessageListener { + @RabbitListener(queues = "topic_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务(rabbitmq topic 1),id:"+id); + } + @RabbitListener(queues = "topic_queue2") + public void receive2(String id){ + System.out.println("已完成短信发送业务(rabbitmq topic 22222222),id:"+id); + } +} +``` + +​ 使用注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列。 + +**总结** + +1. springboot整合RabbitMQ提供了AmqpTemplate对象作为客户端操作消息队列 +2. 操作ActiveMQ需要配置ActiveMQ服务器地址,默认端口5672 +3. 企业开发时通常使用监听器来处理消息队列中的消息,设置监听器使用注解@RabbitListener +4. RabbitMQ有5种消息模型,使用的队列相同,但是交换机不同。交换机不同,对应的消息进入的策略也不同 + diff --git a/public/markdowns/SpringBoot3.md b/public/markdowns/SpringBoot3.md new file mode 100644 index 0000000..aee3da0 --- /dev/null +++ b/public/markdowns/SpringBoot3.md @@ -0,0 +1,9 @@ +--- +title: SpringBoot3 +tags: + - SpringBoot3 +categories: + - SpringBoot +description: SpringBoot3 +abbrlink: 55656a4d +--- diff --git a/public/markdowns/SpringCloud.md b/public/markdowns/SpringCloud.md new file mode 100644 index 0000000..070321e --- /dev/null +++ b/public/markdowns/SpringCloud.md @@ -0,0 +1,5349 @@ +--- +title: SpringCloud +abbrlink: 96a41905 +date: 2024-01-20 10:10:33 +tags: +categories: + - 微服务 +description: SpringCloud全解 +--- + +# 认识微服务 + +## 服务架构演变 + +### 单体架构 + +将业务的所有功能集中在一个项目中开发,打成一个包部署 + +优点: + +- 架构简单 +- 部署成本低 + +缺点: + +- 耦合度高 + +当业务变得很多,就容易造成修改了一个业务,引起其他业务的崩溃这一情况 + +![image-20240120103219346](https://s2.loli.net/2024/01/20/NSthZvCH9o7aQRW.png) + +### 分布式架构 + +根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务 + +优点: + +- 降低服务耦合 +- 有利于服务升级拓展 + +虽然分布式架构降低了服务耦合性,但也带来了一些问题 + +- 服务拆分粒度如何? +- 服务集群地址如何维护? +- 服务之间如何实现远程调用? +- 服务健康状态如何感知? + +![image-20240120103617722](https://s2.loli.net/2024/01/20/7cibE2MNmRaVPDe.png) + +### 微服务 + +微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征: + +- 单一职责:微服务拆分粒度更小,每一个服务都有对应唯一的业务能力,做到单一职责,避免重复业务开发 +- 面向服务:微服务对外暴露业务接口,暴露的业务接口需要统一 +- 自治:团队独立、技术独立、数据独立、部署独立 +- 隔离性强:服务调用做好隔离、容错、降级、避免出现级联问题(比如说,我去调用你的服务,结果你的服务宕机了,导致我的服务也挂了,需要处理这种特殊情况) + +![image-20240120104745551](https://s2.loli.net/2024/01/20/nhoSW2H1MeFgVBb.png) + +总结: + +- 单体架构特点: + - 简单方便,高度耦合,拓展性差,适合小型项目。例如:学生管理系统 +- 分布式架构特点 + - 松耦合,拓展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝 +- 微服务:一种良好的分布式架构方案 + - 优点:拆分粒度更小、服务更独立、耦合度更低 + - 缺点:架构非常复杂,运维、监控、部署难度提高 + +## 微服务结构 + +微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo + +微服务流程如下: + +image-20240120122621112 + +### 微服务技术对比 + +![image-20240120124115660](https://s2.loli.net/2024/01/20/7TqgLsaxkYJDK21.png) + +### 企业需求 + +![image-20240120124144257](https://s2.loli.net/2024/01/20/A5VQwIZYx7DHzbX.png) + +## 了解SpringCloud + +- SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud +- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验: + +image-20240120124728679 + +# 服务拆分及远程调用 + +## 服务拆分 + +### 服务拆分注意事项 + +1. 不同微服务,不要重复开发相同业务 +2. 微服务数据独立,不要访问其他微服务的数据库 +3. 微服务可以将自己的业务暴露为接口,供其他微服务调用 + +### 服务拆分Demo + +导入资料中提供的工程:cloud-demo + +了解项目结构: + +- cloud-demo + - order-service + - 根据id查询订单 + - user-service + - 根据id查询用户 + +导入资料中对应的sql文件 + +修改不同模块下的mysql密码 + +启动两个模块,并访问网址查看是否访问成功 + +user-service:http://localhost:8081/user/1 + +order-service:http://localhost:8080/order/101 + +## 服务远程调用 + +根据订单id查询订单的同时,把订单所属的用户信息一起返回 + +访问之前的订单地址:http://localhost:8080/order/101 + +```json +{"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,"userId":1,"user":null} +``` + +我们会发现缺失了对应的用户信息,这是因为订单模块中只能查询到订单相关的信息,而用户模块里只能查询用户相关信息 + +所以我们需要修改订单功能,当根据订单id查询订单信息时,还需要根据userId查询用户信息,但是它俩的数据库已经完全分离了,所以在订单模块查询订单相关信息时,无法查询到用户模块的用户信息 + +此时,就需要用到服务远程调用了 + +### 远程调用方式分析 + +远程调用方式流程 + +![image-20240120132517167](https://s2.loli.net/2024/01/20/2BAfO8kL9ayi3Rj.png) + +#### 步骤 + +注册RestTemplate + +在order-service的OrderApplication中注册RestTemplate + +```java +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +@MapperScan("cn.itcast.order.mapper") +@SpringBootApplication +public class OrderApplication { + + public static void main(String[] args) { + SpringApplication.run(OrderApplication.class, args); + } + + /** + * 创建RestTemplate 并注入Spring容器 + * */ + @Bean + public RestTemplate restTemplate(){ + return new RestTemplate(); + } + +} +``` + +修改OrderService + +```java +import cn.itcast.order.mapper.OrderMapper; +import cn.itcast.order.pojo.Order; +import cn.itcast.order.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private RestTemplate restTemplate; + + public Order queryOrderById(Long orderId) { + // 1.查询订单 + Order order = orderMapper.findById(orderId); + // 2.利用RestTemplate发起http请求,查询用户 + String url = "http://localhost:8081/user/"+order.getUserId(); + // restTemplate.getForObject(url, User.class):形参为url和返回后可自动反序列化的值 + User user = restTemplate.getForObject(url, User.class); + // 3.存入order + order.setUser(user); + // 4.返回 + return order; + } +} +``` + +重启Order服务 + +```json +{"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,"userId":1,"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}} +``` + +## 提供者和消费者 + +- 服务提供者:一次业务中,被其他微服务调用的服务。(提供接口给其他微服务) +- 服务消费者:一次业务中,调用其他微服务的服务。(调用其他微服务提供的接口) + +假设:服务A调用服务B,服务B调用服务C,那么服务B是什么角色 + +服务B既是提供者,也是消费者 + +# Eureka注册中心 + +## 服务调用出现的问题 + +在刚才编写获取用户信息时,我们采用的是硬编码的方式来编写用户数据所对应服务的ip地址及端口号,这种硬编码的方式极为不友好,当出现多个用户集群时,该如何获取对应服务的ip地址及端口号呢,由此衍生出多个问题 + +- 服务消费者该如何获取服务提供者的地址信息? +- 如果有多个服务提供者,消费者该如何进行选择 +- 消费者如何得知服务提供者的健康状态 + +## Eureka的作用 + +- eureka-server(注册中心):记录和管理服务 +- eureka-client(客户端):包含服务消费者及服务提供者 + +Eureka的工作原理: + +1. 服务提供者向eureka-server注册服务信息,并且进行心跳续约,默认每30秒一次 +2. 当服务消费者需要时,从euraka-server(注册中心)中拉取对应的服务消费者的信息,并且进行负载均衡后,选中最为合适的服务消费者 +3. 进行远程调用 + +假设某服务提供者宕机,由于心跳续约的情况,所以当服务提供者宕机后,不会出现当服务消费者调用对应服务提供者后意外的情况 + +![image-20240120140614231](https://s2.loli.net/2024/01/20/yEb3LxSKUNH19O4.png) + +- 消费者该如何获取服务提供者具体信息? + - 服务提供者启动时向eureka注册自己的信息 + - eureka保存这些信息 + - 消费者根据服务名称向eureka拉取提供者信息 +- 如果有多个服务提供者,消费者该如何选择? + - 服务消费者利用负载均衡算法,从服务列表中挑选一个 +- 消费者如何感知服务提供者的健康状态? + - 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态 + - eureka会更新记录服务列表信息,心跳不正常会被剔除 + - 消费者就可以拉取到最新的信息 + +总结: + +在Eureka架构中,微服务角色有两类: + +- EurekaServer:服务端,注册中心 + - 记录服务信息 + - 心跳监控 +- EurekaClient:客户端 + - Provider:服务提供者,例如案例中的user-service + - 注册自己的信息到EurekaServer + - 每隔30秒向EurekaServer发送心跳 + - consumer:服务消费者,例如案例中的order-service + - 根据服务名称从EurekaServer拉取服务列表 + - 基于服务列表做负载均衡,选中一个微服务后发起远程调用 + +## 搭建Eureka服务 + +- 搭建EurekaServer +- 将user-service、order-service都注册到eureka +- 在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用 + +新建一个空模块,导入对应依赖 + +```xml + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + +``` + +新建类EurekaApplication,并开启EurekaServer + +```java +package cn.itcast.eureka; + +/* +@author zhangJH +@create 2024-01-20-17:42 +*/ + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +// 开启EurekaServer +@EnableEurekaServer +public class EurekaApplication { + + public static void main(String[] args) { + SpringApplication.run(EurekaApplication.class, args); + } + +} +``` + +编写配置文件 + +```yaml +server: + port: 10086 # 服务端口 +spring: + application: + name: eurekaserver # eureka微服务名称 +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka +``` + +启动后可能会报错:`was unable to refresh its cache! This periodic background refresh will be retried in 30 seconds. status = Cannot execute request on any known server stacktrace = com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server` + +或报错:`Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:10086/eureka/}, exception=java.net.ConnectException: Connection refused: connect stacktrace=com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect` + + 以上两个问题,第一个是因为Eureka在启动的时候会获取其他服务的信息,获取不到会报这个异常。第二个是因为,Eureka会把自己当作一个服务注册在注册中心里面。以上两个异常都不会影响程序的正常运行。如果需要修复,需要在配置文件里面加入: + +```yaml +server: + port: 10086 # 服务端口 +spring: + application: + name: eurekaserver # eureka微服务名称 +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka + register-with-eureka: false #Eureka不把自己注册为服务 + fetch-registry: false #不拉取其他服务的信息 +``` + +访问`http://localhost:10086/`,查看Eureka相关信息 + +![image-20240120181219175](https://s2.loli.net/2024/01/20/1wapnIjNCAxEiPu.png) + +![image-20240120181348194](https://s2.loli.net/2024/01/20/ydYQW5FIXOz2Dos.png) + +Eureka会将自身作为实例上传到注册中心,使用的名称就是之前在配置文件中的微服务名称及端口,微服务名称前面的是当前的计算机名称,也可以理解为localhost + +![image-20240120181720311](https://s2.loli.net/2024/01/20/x53qDONtUMdVK2m.png) + +## 服务注册 + +注册前需要引入Eureka客户端依赖 + +在user-service中引入 + +```xml + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +修改user-service的配置文件,为其添加eureka的客户端配置信息 + +- 需要添加spring.application.name和eureka的服务端信息 + +```yaml +spring: + application: + name: xxx # 服务名称 +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka + register-with-eureka: false #Eureka不把自己注册为服务 + fetch-registry: false #不拉取其他服务的信息 +``` + +user-service完整文件如下 + +```yaml +server: + port: 8081 +spring: + application: + name: userservice # user-service名称 + datasource: + url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver +mybatis: + type-aliases-package: cn.itcast.user.pojo + configuration: + map-underscore-to-camel-case: true +logging: + level: + cn.itcast: debug + pattern: + dateformat: MM-dd HH:mm:ss:SSS + +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka + register-with-eureka: false #Eureka不把自己注册为服务 + fetch-registry: false #不拉取其他服务的信息 +``` + +同理,依照上述流程将order-service也存入eureka注册中心 + +注册成功后,结果如下: + +![image-20240120184111096](https://s2.loli.net/2024/01/20/LIGMFEqkZ9a6Rjv.png) + +这里只是启动了单个实例,如果想启动多个实例,可以将某个服务多次启动 + +为了避免端口冲突,需要修改端口设置 + +IDEA2022版本修改情况如下: + +![image-20240120184534351](https://s2.loli.net/2024/01/20/6kKw9Vrun4X1IRN.png) + +image-20240120184555031 + +在里面添加`-Dserver.port=8082` + +复制之前的配置 + +image-20240120193500352 + +接着将之前第一个UserService配置中的VM options删除 + +重新启动全部配置 + +回到之前Eureka的面板中,发现已经注册了多个服务了 + +![image-20240120193858460](https://s2.loli.net/2024/01/20/yOZqgRz8ikXlThn.png) + +## 服务发现 + +在order-service中完成服务拉取 + +- 修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口: + +```java +import cn.itcast.order.mapper.OrderMapper; +import cn.itcast.order.pojo.Order; +import cn.itcast.order.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private RestTemplate restTemplate; + + public Order queryOrderById(Long orderId) { + // 1.查询订单 + Order order = orderMapper.findById(orderId); + // 2.利用RestTemplate发起http请求,查询用户 + // 将ip和端口修改为服务名称 + String url = "http://userservice/user/"+order.getUserId(); + // restTemplate.getForObject(url, User.class):形参为url和返回后可自动反序列化的值 + User user = restTemplate.getForObject(url, User.class); + // 3.存入order + order.setUser(user); + // 4.返回 + return order; + } +} +``` + +- 在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解 + +```java +/** + * 创建RestTemplate 并注入Spring容器 + * */ +@Bean +@LoadBalanced +public RestTemplate restTemplate(){ + return new RestTemplate(); +} +``` + +重启OrderApplication的代码,并且访问一些地址进行测试 + +http://localhost:8080/order/101 + +http://localhost:8080/order/102 + +接着回到userApplication及另一个的日志中进行查看,看看是否进行了负载均衡的操作 + +总结: + +1. 搭建EurekaServer + - 引入eureka-server依赖 + - 添加@EnableEurekaServer注解 + - 在application.yml中配置eureka地址 +2. 服务注册 + - 引入eureka-client依赖 + - 在application.yml中配置eureka地址 +3. 服务发现 + - 引入eureka-client依赖 + - 在application.yml中配置eureka地址 + - 给RestTemplate添加@LoadBalanced注解 + - 用服务提供者的服务名称远程调用 + +# Ribbon负载均衡 + +## 负载均衡流程 + +Ribbon的大致流程 + +- 当服务消费者发起请求后,Ribbon会拦截下来,并拉取eureka-server所对应的服务,接着会返回服务列表,通过轮询的方式向某个服务提供者获取数据 + +![image-20240120200413257](https://s2.loli.net/2024/01/20/qd3FRioBHapDUQm.png) + +Rabbon工作的详细流程如下: + +1. 当服务消费者发起请求后,会被LoadBalancerInterceptor负载均衡拦截器所拦截 +2. 在拦截器中,有一个RibbonLoadBanlancerClient会获取请求中的服务id,也就是userservice +3. 接着交给DynamicServerLoadBalancer做负载均衡,通过轮询或随机的方式得到服务的ip +4. 最终返回并修改url,并发起对应的请求 + +![image-20240120201615996](https://s2.loli.net/2024/01/20/iB1DgcSeQb9ylwJ.png) + +## 负载均衡策略 + +Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则: + +![image-20240120202517709](https://s2.loli.net/2024/01/20/ZcajA5GCkrJHoXI.png) + + + +![image-20240120202613860](https://s2.loli.net/2024/01/20/G7o1jeKQO4npk62.png) + +通过定义IRule实现可以修改负载均衡规则,有两种方式: + +- 代码方式(全体):在服务消费者中的启动类中,定义一个新的IRule: + +```java +@Bean +public IRule randomRule(){ + return new RandomRule(); +} +``` + +- 配置文件方式(针对某个服务而言):在服务消费者的application.yml文件中,添加新的配置也可以修改规则: + +```yaml +userservice: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 +``` + +访问不同的ip及地址进行测试 + +http://localhost:8080/order/101 + +http://localhost:8080/order/102 + +http://localhost:8080/order/103 + +http://localhost:8080/order/104 + +## Ribbon饥饿加载 + +我们第一次访问http://localhost:8080/order/101这个地址的数据时,查看一下访问时间,发现会比较长,大概在几百毫秒的样子,再次访问后,会发现缩短到了几十毫秒的时间 + +因为Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。 + +而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载: + +```yaml +ribbon: + eager-load: + enabled: true # 开启饥饿加载 + clients: userservice # 指定开启饥饿加载的提供者 +``` + +```yaml +ribbon: + eager-load: + enabled: true # 开启饥饿加载 + clients: # 指定开启饥饿加载的提供者,数组形式添加 + - userservice +``` + +总结: + +1. Ribbon负载均衡规则 + - 规则接口是IRule + - 默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询 +2. 负载均衡自定义方式 + - 代码方式:配置灵活,但修改时需要重新打包发布 + - 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置 +3. 饥饿加载 + - 开启饥饿加载 + - 指定饥饿加载的微服务名称 + +# Nacos注册中心 + +## Nacos的安装 + +### Windows安装 + +开发阶段采用单机安装即可。 + +在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码: + +GitHub主页:https://github.com/alibaba/nacos + +GitHub的Release下载页:https://github.com/alibaba/nacos/releases + +如图: + +![image-20240120220127259](https://s2.loli.net/2024/01/20/8hiln3VWj7HckbR.png) + +本课程采用1.4.1.版本的Nacos,课前资料已经准备了安装包: + +![image-20240120220136498](https://s2.loli.net/2024/01/21/yjl6u7cQOx1fI4G.png) + +windows版本使用`nacos-server-1.4.1.zip`包即可。 + +接着将这个包解压到任意非中文目录下,如图: + +![image-20240120220221987](https://s2.loli.net/2024/01/21/qrTSXG9LVsQu7MU.png) + +目录说明: + +- bin:启动脚本 +- conf:配置文件 + +#### 端口配置 + +Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。 + +**如果无法关闭占用8848端口的进程**,也可以进入nacos的conf目录,修改配置文件中的端口: + +![image-20240120220255643](https://s2.loli.net/2024/01/21/piGwD7gX3vNUAzf.png) + +修改其中的内容: + +![image-20240120220305743](https://s2.loli.net/2024/01/21/WUeKmojinQXE3zl.png) + +#### 启动 + +启动非常简单,进入bin目录,结构如下: + +![image-20240120220322299](https://s2.loli.net/2024/01/21/bVZ2YK8REpI6fQs.png) + +然后执行命令即可: + +- windows命令: + + ``` + startup.cmd -m standalone + ``` + +接着在浏览器输入地址:http://127.0.0.1:8848/nacos即可 + +默认的账号和密码都是nacos + +### Linux安装 + +#### 安装JDK + +![image-20240120220547265](https://s2.loli.net/2024/01/21/KyvAQf3DYR8mj2k.png) + +上传到某个目录,例如:`/usr/local/` + +然后解压缩: + +```sh +tar -xvf jdk-8u144-linux-x64.tar.gz +``` + +然后重命名为java + +配置环境变量: + +```sh +export JAVA_HOME=/usr/local/java +export PATH=$PATH:$JAVA_HOME/bin +``` + +设置环境变量: + +```sh +source /etc/profile +``` + +#### 安装nacos + +将对应的tar.gz上传到Linux服务器的某个目录,例如`/usr/local/src`目录下: + +命令解压缩安装包: + +```sh +tar -xvf nacos-server-1.4.1.tar.gz +``` + +然后删除安装包: + +```sh +rm -rf nacos-server-1.4.1.tar.gz +``` + +端口配置与windows一致 + +在nacos/bin目录中,输入命令启动Nacos: + +```sh +sh startup.sh -m standalone +``` + +## Nacos的依赖 + +父工程: + +```xml + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2.2.5.RELEASE + pom + import + +``` + + + +客户端: + +```xml + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + +``` + +## 快速入门 + +为cloud-demo工程添加 + +```xml + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2.2.5.RELEASE + pom + import + +``` + +在user-service中注释之前的eureka依赖 + +替换为对应的nacos + +```xml + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + +``` + +修改user-service中的application.yml文件 + +添加了nacos的服务地址,端口对应着之前所安装的端口号 + +```yaml +server: + port: 8081 +spring: + application: + name: userservice # user-service名称 + datasource: + url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 # nacos服务地址 +mybatis: + type-aliases-package: cn.itcast.user.pojo + configuration: + map-underscore-to-camel-case: true +logging: + level: + cn.itcast: debug + pattern: + dateformat: MM-dd HH:mm:ss:SSS + +#eureka: +# client: +# service-url: # eureka的地址信息 +# defaultZone: http://localhost:10086/eureka +``` + +同样的方法,对order-service也进行修改 + +回到nacos页面,查看是否已经启动成功 + +![image-20240121092231332](https://s2.loli.net/2024/01/21/fPUF32dnZetR8TV.png) + +这里我启动了一个order以及两个user + +http://localhost:8080/order/101 + +http://localhost:8080/order/102 + +http://localhost:8080/order/103 + +http://localhost:8080/order/104 + +访问以下四个地址,查看负载均衡是否成功 + +总结: + +1. Nacos服务搭建 + 1. 下载安装包 + 2. 解压 + 3. 在bin目录下运行指令:startup.cmd -m standalone +2. Nacos服务注册或发现 + 1. 引入nacos.discovery依赖 + 2. 配置nacos地址spring.cloud.nacos.server-addr + + + +## Nacos服务分级存储模型 + +在一个服务中,可以有多个实例,这多个实例,可能存放在一个机房,也可能存放在不同的机房内 + +以机房划分集群,例如a集群,b集群,每个集群下都有该服务的一些实例 + +这就形成了一个树形结构 + +- 服务 + - 集群 + - 实例 + +这相当于一个分级的存储模型,即使某集群出现问题也不会影响到其他的集群 + +image-20240121093811789 + +那么,如何来配置一个集群呢 + +以user-service为例 + +打开user-service的application.yml文件进行配置 + +```yaml +server: + port: 8081 +spring: + application: + name: userservice # user-service名称 + datasource: + url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 # nacos服务地址 + discovery: + cluster-name: SH # 集群名称,这里的SH代指上海 +mybatis: + type-aliases-package: cn.itcast.user.pojo + configuration: + map-underscore-to-camel-case: true +logging: + level: + cn.itcast: debug + pattern: + dateformat: MM-dd HH:mm:ss:SSS + +#eureka: +# client: +# service-url: # eureka的地址信息 +# defaultZone: http://localhost:10086/eureka +``` + +重新启动一个你想指定为SH集群的user-service的实例 + +接着修改`cluster-name`为HZ + +重启另一个user-service的实例,不要全部重启了,重启你需要指定为HZ的实例即可 + +回到Nacos查看对应实例的`详情` + +![image-20240121094630855](https://s2.loli.net/2024/01/21/SEdBoRehzNqxisC.png) + +如果想要让服务消费者优先去寻找某个集群,也可以在application.yml中进行配置 + +在order-service中进行配置cluster-name + +```yaml +server: + port: 8080 +spring: + application: + name: orderservice # orderservice + datasource: + url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false&allowPublicKeyRetrieval=true + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 + discovery: + cluster-name: HZ # 集群名称 +mybatis: + type-aliases-package: cn.itcast.user.pojo + configuration: + map-underscore-to-camel-case: true +logging: + level: + cn.itcast: debug + pattern: + dateformat: MM-dd HH:mm:ss:SSS + +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka +userservice: + ribbon: + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 + +ribbon: + eager-load: + enabled: true # 开启饥饿加载 + clients: userservice +``` + +重启order-service,再次测试,发现依然是轮询机制,这是因为有负载均衡的存在所导致的,所以需要修改order-service的负载均衡规则 + +```yaml +userservice: + ribbon: + NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 +``` + +再次测试,此时会发现,仅在HZ的集群上操作了,并且是一种随机的形式 + +当我们将HZ的集群关闭,再次测试 + +此时依然有效,但访问的是SH的集群,且服务消费者的控制台会输出跨集群访问的地址信息 + +``` +A cross-cluster call occurs,name = userservice, clusterName = HZ, instance = [Instance{instanceId='192.168.200.1#8082#SH#DEFAULT_GROUP@@userservice', ip='192.168.200.1', port=8082, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='SH', serviceName='DEFAULT_GROUP@@userservice', metadata={preserved.register.source=SPRING_CLOUD}}] +``` + +## 服务实例的权重设置 + +实际部署中会出现这样的场景: + +- 服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求 + +Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高 + +进入某个服务后,找到相关的集群 + +点击编辑 + +![image-20240121103530327](https://s2.loli.net/2024/01/21/U7OthIQPX1SG8rL.png) + +在这里可以修改相应的权重 + +image-20240121103545791 + +将你想修改的某个实例的权重进行修改后,就会影响其被访问的次数 + +当权重为0时,该服务不会被访问 + +总结: + +- Nacos控制台可以设置实例的权重值,0~1之间 +- 同集群内的多个实例,权重越高被访问的频率越高 +- 权重设置为0完全不会被访问 + +## 环境隔离 + +Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离 + +![image-20240121105056772](https://s2.loli.net/2024/01/21/kifLarqZA7oCz1h.png) + +nacos有一个默认的namespace,是作为保留空间使用的,且默认服务都是存在于public上的 + +![image-20240121104733838](https://s2.loli.net/2024/01/21/mSoBqXfF4hxev7n.png) + +![image-20240121104633682](https://s2.loli.net/2024/01/21/gsVTUv92PiA7HCu.png) + +新建一个dev的命名空间 + +![image-20240121104918989](https://s2.loli.net/2024/01/21/PcovgIYT7CxbyEs.png) + +回到服务列表,会发现public的旁边出现了一个dev的命名空间,但里面是空的 + +此时如果想进行环境隔离,将某个服务添加到dev中,需要回到配置文件进行配置 + +在order-service中,修改application.yml文件 + +添加namespace + +```yaml +spring: + application: + name: orderservice # orderservice + datasource: + url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false&allowPublicKeyRetrieval=true + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 + discovery: + cluster-name: HZ # 集群名称 + namespace: de2d245e-5b7a-4cf1-84a2-cf5ada88fb3c # 命名空间的id +``` + +此时回到nacos的页面,会发现之前public中的order-service服务已经没了,在dev中存在着 + +此时,如果再次访问http://localhost:8080/order/101 + +会报错,因为它俩不是一个环境的,导致order-service中对于user-service的代码无法使用 + +总结: + +- namespace用来做环境隔离 +- 每个namespace都有唯一id +- 不同namespace下的服务不可见 + +## Nacos和Eureka的区别 + +### nacos注册中心细节分析 + +服务提供者在服务初始化时会注册服务信息给nacos注册中心,当服务消费者需要时,会定时拉取服务,并缓存到服务列表中,进行远程调用 + +nacos的心跳监测与eureka不同,nacos分为两种心跳监测,一种是临时实例,一种是非临时实例 + +- 临时实例所采用的是心跳监测,它主动向nacos发送心跳,让nacos感知他的存在,如果临时实例突然不发送心跳了,那么nacos会直接将它剔除,并主动推送变更消息给服务消费者,让服务消费者进行更新 +- 非临时实例所采用的是nacos的主动询问,当nacos发现非临时实例宕机后,不会剔除,依然是主动推送变更消息给服务消费者,让服务消费者进行更新,并且nacos不会剔除该非临时实例,会等待它恢复健康 + +![image-20240121110422677](https://s2.loli.net/2024/01/21/wIPzaDcmAlQBo8J.png) + +### 临时和非临时实例 + +服务注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置: + +在order-sevice中进行设置并测试 + +```yaml +spring: + application: + name: orderservice # orderservice + datasource: + url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false&allowPublicKeyRetrieval=true + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 + discovery: + cluster-name: HZ # 集群名称 + namespace: de2d245e-5b7a-4cf1-84a2-cf5ada88fb3c # 命名空间的id + ephemeral: false # true是设置为临时实例,false是设置为非临时实例 +``` + +来到nacos中查看,此时发现该集群就不是临时实例了 + +![image-20240121111354389](https://s2.loli.net/2024/01/21/oQYvc6qxe7uI2Bf.png) + +并且,当我们关闭order-service服务时,nacos中不会删除该服务,而是会等待该服务恢复健康,除非你手动删除该服务 + +总结: + +1. Nacos域eureka的共同点 + 1. 都支持服务注册和服务拉取 + 2. 都支持服务提供者心跳方式做健康检测 +2. Nacos域Eureka的区别 + 1. Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式 + 2. 临时实例心跳不正常时会被剔除,非临时实例不会被剔除 + 3. Nacos支持服务列表变更的主动消息推送模式,服务列表更新更及时 + 4. Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP模式 + +# Nacos配置管理 + +## 统一配置管理 + +在更改配置后,通常是个很麻烦的事情,需要重启更改了配置的服务器让其重新读取配置,这一操作可以通过配置更改热更新来解决 + +image-20240121132043939 + +来到nacos下的配置管理下的配置列表,点击最右侧的+号可以新建配置 + +image-20240121132854851 + +## 配置获取流程 + +![image-20240121133811938](https://s2.loli.net/2024/01/21/yFdbXrO3iVPWxpL.png) + +在user-service中引入nacos的配置管理依赖,并且创建一个bootstrap.yml文件,并在里面写入nacos对应的参数 + +因为boostrap.yml文件的优先级高于application.yml,所以项目启动后,会先读取bootstrap中的内容,再从nacos中读取内容 + +```xml + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + +``` + +创建bootstrap.yml文件 + +```yaml +spring: + application: + name: userservice # 服务名称 + profiles: + active: dev # 环境 + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + config: + file-extension: yaml # 文件后缀名 +``` + +编写UserController,通过@Value注解获取yml文件的值,看看是否能获取到nacos中的参数 + +```java +@Value("${pattern.dateformat}") +private String dateFormat; +``` + +如果dateformat加载成功,说明nacos中的参数成功加载了 + +```java +@GetMapping("/now") +public String now(){ + return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat)); +} +``` + +访问:http://localhost:8081/user/now,http://localhost:8082/user/now + +如果被正确的格式化了,说明nacos中的参数加载成功了 + +总结: + +将配置交给Nacos管理的步骤 + +1. 在Nacos中添加配置文件 +2. 在微服务中引入nacos的config依赖 +3. 在微服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去nacos读取哪个文件 + +## 配置热更新 + +修改之前定义好的默认配置 + +```yaml +pattern: + dateformat: yyyy年MM月dd日 HH:mm:ss +``` + +此时再次访问之前的请求地址,会发现没有发生变化,这是因为我们没有配置热更新 + +Nacos的配置文件变更后,微服务无需重启就可以感知。不过需要通过下面两种配置实现: + +- 方式一:在@Value注入的变量所在类上添加注解@RefreshScope +- 方式二:使用@ConfigurationProperties注解 + +使用方式一 + +在刚才的UserController上添加@RefreshScope,重启服务 + +此时再次访问`http://localhost:8081/user/now`肯定是没问题的 + +接着再修改nacos关于user的配置 + +```yaml +pattern: + dateformat: yyyy/MM/dd HH:mm:ss +``` + +直接访问相关路径:http://localhost:8081/user/now + +此时已经发生了变化 + + + +使用方式二 + +新建一个类,在类中对属性进行配置 + +```java +package cn.itcast.user.config; + +/* +@author zhangJH +@create 2024-01-21-15:22 +*/ + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +// 将prefix与dateformat结合后,可以获取到对应的参数值 +@ConfigurationProperties(prefix = "pattern") +public class PatternProperties { + + private String dateformat; + +} +``` + +修改UserController + +将其修改为通过PatternProperties来获取的dateformat + +```java +package cn.itcast.user.web; + +import cn.itcast.user.config.PatternProperties; +import cn.itcast.user.pojo.User; +import cn.itcast.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Slf4j +@RestController +@RequestMapping("/user") +// @RefreshScope +public class UserController { + + @Autowired + private UserService userService; + +// @Value("${pattern.dateformat}") +// private String dateFormat; + + @Autowired + private PatternProperties patternProperties; + + @GetMapping("/now") + public String now(){ + return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); + } + + /** + * 路径: /user/110 + * + * @param id 用户id + * @return 用户 + */ + @GetMapping("/{id}") + public User queryById(@PathVariable("id") Long id) { + return userService.queryById(id); + } +} +``` + +总结: + +Nacos配置更改后,微服务可以实现热更新,方式: + +1. 通过@Value注解注入,结合@RefreshScope来刷新 +2. 通过@ConfigurationProperties注入,自动刷新 + +注意事项: + +- 不是所有配置都适合放到配置中心,维护起来比较麻烦 +- 建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置 + +**在使用热更新时,推荐使用@ConfigurationProperties,因为其可以自动实现热更新,不需要通过两个注解来完成,更方便** + +## 多环境配置共享 + +微服务启动时会从nacos读取多个配置文件: + +- [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml +- [spring.application.name].yml,例如:userservice.yaml + +无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件 + +在nacos中添加一个配置 + +image-20240121160125354 + +在user-service中的PatternProperties类上添加属性 + +```java +private String envSharedValue; +``` + +为UserController添加一个获取对应参数的方法 + +```java +@GetMapping("props") +public PatternProperties patternProperties(){ + return patternProperties; +} +``` + +接着将其中一个UserApplication的环境切换为test,并重启 + +image-20240121160616837 + +此时,bootstrap.yml文件中存在如下内容 + +```yaml +spring: + application: + name: userservice # 服务名称 + profiles: + active: dev # 环境 + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + config: + file-extension: yaml # 文件后缀名 +``` + +在修改环境后的UserApplication中它无法读取到对应的userservice-dev.yaml,但是可以读取到userservice中的内容,因为这里的内容是共享的,只要是userservice中的服务都是可以被读取到的 + +此时我们访问对应的地址 + +这里我对8083做了修改,所以,8083是无法读取到相应参数的 + +http://localhost:8081/user/props + +`{"dateformat":"yyyy/MM/dd HH:mm:ss","envSharedValue":"环境共享属性值"}` + +http://localhost:8083/user/props + +`{"dateformat":null,"envSharedValue":"环境共享属性值"}` + +假设,这两个文件都存在着相同的属性,会以谁的为准呢?再假设,如果本地的IDEA中也存在着与这两个文件有着相同的属性的情况,会以谁的为准呢? + +这里我先为本地添加一个属性,并在PatternProperties类上也进行同名添加 + +在user-service的application.yml中 + +```yaml +pattern: + name: localEnv +``` + +```java +private String name; +``` + +接着重启user-service + +访问:http://localhost:8081/user/props + +此时展示的肯定是`localEnv` + +接着我们为nacos中的userservice.yaml进行配置 + +```yaml +pattern: + envSharedValue: 环境共享属性值 + name: envSharedValue +``` + +访问:http://localhost:8081/user/props + +此时`{"dateformat":"yyyy/MM/dd HH:mm:ss","envSharedValue":"环境共享属性值","name":"envSharedValue"}` + +值变成了`envSharedValue`,说明nacos中共享的userservice.yaml是大于本地配置的 + +接着我们为nacos中的userservice-dev.yaml进行配置 + +```yaml +pattern: + dateformat: yyyy/MM/dd HH:mm:ss + name: envDev +``` + +访问:http://localhost:8081/user/props + +此时`{"dateformat":"yyyy/MM/dd HH:mm:ss","envSharedValue":"环境共享属性值","name":"envDev"}` + +值变成了`envDev`,说明nacos中的userservice-dev.yaml是大于userservice.yaml的 + + + +多种配置的优先级 + +**服务名-profile(环境).yaml** > **服务名.yaml** > **本地配置** + +image-20240121162459088 + +微服务会从nacos读取的配置文件: + +1. [服务名]-[spring.profile.active].yaml,环境配置 +2. [服务名].yaml,默认配置,多环境共享 + +优先级:**服务名-profile(环境).yaml** > **服务名.yaml** > **本地配置** + +## nacos集群搭建 + +我们之前一直使用的都是standalone(单机版),但是在企业中不能使用这种,需要使用Nacos集群的方式,这里不详细讲述,单独使用一篇文章来说明 + +# http客户端Feign + +## RestTemplate方式调用存在的问题 + +当一个url路径很长,参数很多时,我们想要发起请求就变得十分麻烦 + +image-20240121202938838 + +这时候,就需要用到Feign了 + +## Feign的介绍 + +Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign + +其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题 + +为order-service[服务消费者]添加一个feign的客户端依赖 + +```xml + + + org.springframework.cloud + spring-cloud-starter-openfeign + +``` + +开启Feign客户端模式 + +```java +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +@MapperScan("cn.itcast.order.mapper") +@SpringBootApplication +@EnableFeignClients // 开启Feign客户端模式 +public class OrderApplication { + + public static void main(String[] args) { + SpringApplication.run(OrderApplication.class, args); + } + + /** + * 创建RestTemplate 并注入Spring容器 + * */ + @Bean + @LoadBalanced + public RestTemplate restTemplate(){ + return new RestTemplate(); + } + +// @Bean +// public IRule randomRule(){ +// return new RandomRule(); +// } + +} +``` + +编写对应的服务提供者的接口 + +```java +package cn.itcast.order.clients; + +/* +@author zhangJH +@create 2024-01-21-20:55 +*/ + + +import cn.itcast.order.pojo.User; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient("userservice") // 声明-服务名称 +public interface UserClient { + + @GetMapping("/user/{id}") + User findById(@PathVariable("id") Long id); + +} +``` + +记得将之前order-service中划分环境的application.yaml文件修改一下,将namespace注释掉,防止环境不一致的情况 + +```yaml +server: + port: 8080 +spring: + application: + name: orderservice # orderservice + datasource: + url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false&allowPublicKeyRetrieval=true + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + cloud: + nacos: + server-addr: localhost:8848 + discovery: + cluster-name: HZ # 集群名称 +# namespace: de2d245e-5b7a-4cf1-84a2-cf5ada88fb3c # 命名空间的id + ephemeral: false # true是设置为临时实例,false是设置为非临时实例 +mybatis: + type-aliases-package: cn.itcast.user.pojo + configuration: + map-underscore-to-camel-case: true +logging: + level: + cn.itcast: debug + pattern: + dateformat: MM-dd HH:mm:ss:SSS + +eureka: + client: + service-url: # eureka的地址信息 + defaultZone: http://localhost:10086/eureka +userservice: + ribbon: + NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 + +ribbon: + eager-load: + enabled: true # 开启饥饿加载 + clients: userservice +``` + +重启order-service服务 + +访问:http://localhost:8080/order/102 + +回到控制台会发现,不仅实现了发起请求的功能,还实现了负载均衡的功能 + +总结: + +Feign的使用步骤 + +1. 引入依赖 +2. 添加@EnableFeignClients注解 +3. 编写FeignClient接口 +4. 使用FeignClient中定义的方法代替RestTemplate(创建一个接口,其他几乎和RestFul风格的写法一致) + +## Feign的自定义配置 + +![image-20240121221709570](https://s2.loli.net/2024/01/21/o89r5NbSUVOKxwt.png) + +配置日志级别一般有两种方式: + +方式一:配置文件方式 + +- 全局生效 + +```yaml +feign: + client: + config: + default: + logger-level: FULL #全局配置最完整的日志 +``` + +- 局部生效 + +```yaml +feign: + client: + config: + userservice: # 单独的为某个服务进行配置 + logger-level: FULL #全局配置最完整的日志 +``` + +方式二:Java代码方式,需要先声明一个Bean: + +- 全局生效 + +在order-service中新建一个类`DefaultFeignConfiguration`,来作为自定义日志Bean + +```java +import feign.Logger; +import org.springframework.context.annotation.Bean; + +public class DefaultFeignConfiguration { + + @Bean + public Logger.Level logLevel(){ + // 基本日志信息 + return Logger.Level.BASIC; + } + +} +``` + +接着在启动类上添加:**@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)** + +这是说明针对于全局生效的情况 + +- 局部生效(将配置类放到这个 Feign 客户端接口上的 `@FeignClient` 注解中) + +```java +@FeignClient(value = "userservice",configuration = FeignClientProperties.FeignClientConfiguration.class) // 声明-服务名称 +``` + +总结: + +Feign的日志配置 + +1. 方式一是配置文件,feign.client.config.xxx.loggerLevel + 1. 如果xxx是default则代表全局 + 2. 如果xxx是服务名称,例如userservice则代表某服务 +2. 方式二是java代码配置Logger.Level这个Bean + 1. 如果在@EnableFeignClients注解声明则代表全局 + 2. 如果在@FeignClient注解中声明则代表某服务 + +## Feign的性能优化 + +Feign底层的客户端实现: + +- URLConnection:默认实现,不支持连接池 +- Apache HttpClient:支持连接池 +- OKHttp:支持连接池 + + + +因此优化Feign的性能主要包括: + +1. 使用连接池代替默认的URLConnection +2. 日志级别,最好用basic或none,日志级别过大也会拖慢Feign的性能 + +Feign添加HttpClient的支持: + +引入依赖: + +```xml + + + io.github.openfeign + feign-httpclient + +``` + +配置连接池: + +```yaml +feign: + httpclient: + enabled: true # 支持httpclient的开关 + max-connections: 200 # 最大连接数 + max-connections-per-route: 50 # 单个路径的最大连接数 +``` + +总结: + +Feign的优化: + +1. 日志级别尽量用basic +2. 使用HttpClient或OKHttp代替URLConnection + 1. 引入feign-httpClient依赖 + 2. 配置文件开启httpClient功能,设置连接池参数 + +## Feign的最佳实践 + +方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准 + +请看下图 + +下图是一个编写完成的UserAPI + +客户端(消费者)来继承这个接口,作为客户端所需要发送的请求 + +而服务端(提供者)来实现该接口,在实现后的controller中实现业务 + +但官方也说明了:这个方法是一种紧耦合的情况,当接口被改变时,其他地方也需要有相应的改变,**且方法参数是不会被映射的**,也就是说,像@PathVariable long id这种参数,自己还需要重新再写一遍,无法直接使用形参 + +![image-20240122103204017](https://s2.loli.net/2024/01/22/V24kdXthCr8Jpme.png) + +方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用 + +简单来说呢,就是将服务消费者所需要的FeignClient功能,抽取出来,放到一个单独的模块中,若服务消费者需要,就进行该模块的引用,直接调用该模块已经写好的方法即可 + +![image-20240122104104467](https://s2.loli.net/2024/01/22/Zv8jRpn6d9X3MqV.png) + +## 实现Feign的最佳实践 + +实现Feign的最佳实践中的方法二 + +1. 首先创建一个module,命名为feign-api,然后引入feign的starter依赖 +2. 将order-service(消费者)中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中 +3. 将order-service(消费者)中引入feign-api的依赖 +4. 修改order-service(消费者)中的所有与上述三个组件有关的import部分,改成导入feign-api中的包 +5. 重启测试 + +创建无需多言,我这里使用的是IDEA2022版本 + +image-20240122105045753 + +引入feign的依赖 + +```xml + + org.springframework.cloud + spring-cloud-starter-openfeign + +``` + +将order-service(消费者)中的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中 + +基本结构如下 + +![image-20240122105520368](https://s2.loli.net/2024/01/22/y3si89KHqtbdSjx.png) + +再将order-service中之前复制走的内容删除,统一使用feign-api来操作 + +在order-service中的pom.xml中进行添加 + +```xml + + + cn.itcast.demo + feign-api + 1.0 + +``` + +修改order-service中的局部代码 + +order + +```java +package cn.itcast.order.pojo; + +import cn.itcast.feign.pojo.User; +import lombok.Data; + +@Data +public class Order { + private Long id; + private Long price; + private String name; + private Integer num; + private Long userId; + private User user; +} +``` + +orderService + +```java +package cn.itcast.order.service; + +import cn.itcast.feign.clients.UserClient; +import cn.itcast.feign.pojo.User; +import cn.itcast.order.mapper.OrderMapper; +import cn.itcast.order.pojo.Order; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private UserClient userClient; + + public Order queryOrderById(Long orderId) { + // 1.查询订单 + Order order = orderMapper.findById(orderId); + // 2.使用Feign远程调用 + User user = userClient.findById(order.getUserId()); + // 3.存入order + order.setUser(user); + // 4.返回 + return order; + } + +// @Autowired +// private RestTemplate restTemplate; + +// public Order queryOrderById(Long orderId) { +// // 1.查询订单 +// Order order = orderMapper.findById(orderId); +// // 2.利用RestTemplate发起http请求,查询用户 +// // 将ip和端口修改为服务名称 +// String url = "http://userservice/user/"+order.getUserId(); +// // restTemplate.getForObject(url, User.class):形参为url和返回后可自动反序列化的值 +// User user = restTemplate.getForObject(url, User.class); +// // 3.存入order +// order.setUser(user); +// // 4.返回 +// return order; +// } +} +``` + +启动类 + +```java +import cn.itcast.feign.config.DefaultFeignConfiguration; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +@MapperScan("cn.itcast.order.mapper") +@SpringBootApplication +@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) +public class OrderApplication { + + public static void main(String[] args) { + SpringApplication.run(OrderApplication.class, args); + } + + /** + * 创建RestTemplate 并注入Spring容器 + * */ + @Bean + @LoadBalanced + public RestTemplate restTemplate(){ + return new RestTemplate(); + } + +// @Bean +// public IRule randomRule(){ +// return new RandomRule(); +// } + +} +``` + +此时运行会报错,说是FeignClient自动注入的问题,这是因为order-service的启动类无法扫描到关于自动注入的这个类,因为他们不在同一包下所导致的 + +这里有两种方式解决: + +方式一:指定FeignClient所在包 + +```java +@EnableFeignClients(basePackages = "cn.itcast.feign.clients") +``` + +方式二:指定FeignClient字节码 + +里面是一个数组,需要哪些FeignClient都可以进行指定 + +```java +@EnableFeignClients(clients = {UserClient.class}) +``` + +这里使用指定的方式: + +`@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class,clients = {UserClient.class})` + +总结: + +不同包的FeignClient的导入有两种方式: + +1. 在@EnableFeignClients注解中添加basePackages,指定FeignClient所在的包 +2. 在@EnableFeignClients注解中添加clients,指定具体FeignClient的字节码 + +# 统一网关GateWay + +## 为什么需要网关 + +![image-20240122132853581](https://s2.loli.net/2024/01/22/KIqQnobXVkFPHt3.png) + +在SpringCloud中网关的实现包括两种: + +- gateway +- zuul + +Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能 + +总结: + +网关的作用 + +- 对用户请求做身份认证、权限校验 +- 将用户请求路由到微服务,并实现负载均衡 +- 对用户请求做限流 + +## 快速入门 + +搭建网关服务的步骤 + +创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖 + +在cloud-demo下新建模块gateway + +添加对应依赖 + +```xml + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.cloud + spring-cloud-starter-gateway + +``` + +新建main函数 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class,args); + } + +} +``` + +编写路由配置及nacos地址 + +因为Gateway也会将自己作为服务注册进nacos中,所以需要配置nacos地址 + +```yaml +server: + port: 10010 +spring: + application: + name: gateway + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + gateway: + routes: # 底下是一个数组,目的是配置多个路由 + - id: user-service # 路由标识,必须唯一 + uri: lb://userservice # lb是负载均衡的意思,路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/user/** # 路径断言,判断路径是否以/user,如果是则符合放行,并让其去访问目标地址 +``` + +假设我们还想路由到order-service,可以这么写 + +```yaml +server: + port: 10010 +spring: + application: + name: gateway + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + gateway: + routes: # 底下是一个数组,目的是配置多个路由 + - id: user-service # 路由标识,必须唯一 + uri: lb://userservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/user/** # 路径断言,判断路径是否以/user,如果是则符合放行,并让其去访问目标地址 + - id: order-service # 路由标识,必须唯一 + uri: lb://orderservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/order/** # 路径断言,判断路径是否以/order,如果是则符合放行,并让其去访问目标地址 +``` + +启动服务 + +访问地址测试: + +- http://localhost:10010/order/101 +- http://localhost:10010/user/1 + +这样就是为其做了权限控制 + +总结: + +网关搭建步骤: + +1. 创建项目,引入nacos服务发现和gateway依赖 +2. 配置application.yml,包括服务基本信息、nacos地址、路由 + +路由配置包括: + +1. 路由(id):路由的唯一标识 +2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡 +3. 路由断言(predicates):判断路由的规则 + +## 路由断言工厂 + +路由断言工厂又称Route Predicate Factory + +网关路由可以配置的内容包括: + +- 路由id:路由唯一标识 +- uri:路由目的地,支持lb(负载均衡)和http两种 +- **predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地** +- filters:路由过滤器,处理请求或响应 + +- 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件 +- 例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的 +- 像这样的断言工厂在SpringCloudGateway还有十几个 + +如果不清楚这些断言工厂如何编写,可以在Spring的官网进行查看,上面直接给出了对应的示例 + +![image-20240122182206226](https://s2.loli.net/2024/01/22/MnSAdCPybEKeJQO.png) + +## 路由过滤器 + +路由过滤器又称GatewayFilter + +GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理 + +image-20240122193652390 + +Spring提供了31种不同的路由过滤器工厂 + +![image-20240122194049658](https://s2.loli.net/2024/01/22/NJLS5sV3Briz2Yt.png) + +详细可以在Spring官网查看,有对应示例 + +假如我们想添加一个请求头 + +```yaml +server: + port: 10010 +spring: + application: + name: gateway + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + gateway: + routes: # 底下是一个数组,目的是配置多个路由 + - id: user-service # 路由标识,必须唯一 + uri: lb://userservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/user/** # 路径断言,判断路径是否以/user,如果是则符合放行,并让其去访问目标地址 + filters: + - AddRequestHeader=Test,EastWind is freaking aowsome! # 添加请求头,名称为Test,值为逗号后面的文字 + - id: order-service # 路由标识,必须唯一 + uri: lb://orderservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/order/** # 路径断言,判断路径是否以/order,如果是则符合放行,并让其去访问目标地址 + +``` + +接着为一个请求编写获取请求头的形参 + +这里选择user-service下的userController来编写 + +```java +@GetMapping("/{id}") +public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Test",required = false) String test) { + System.out.println("test = " + test); + return userService.queryById(id); +} +``` + +重启服务 + +访问:http://localhost:10010/user/1 + +因为是网关所管理的请求,所以需要向网关发送请求,如果通过order发送,就不会存在网关的情况 + +这样只会针对于某个服务进行生效,是限定的 + +如果想对所有服务生效,可以将过滤器工厂写到default下 + +```yaml +server: + port: 10010 +spring: + application: + name: gateway + cloud: + nacos: + server-addr: localhost:8848 # nacos地址 + gateway: + routes: # 底下是一个数组,目的是配置多个路由 + - id: user-service # 路由标识,必须唯一 + uri: lb://userservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/user/** # 路径断言,判断路径是否以/user,如果是则符合放行,并让其去访问目标地址 + - id: order-service # 路由标识,必须唯一 + uri: lb://orderservice # 路由的目标地址,通过校验后,会去哪里 + predicates: # 路由断言(一个数组),判断请求是否符合规则 + - Path=/order/** # 路径断言,判断路径是否以/order,如果是则符合放行,并让其去访问目标地址 + default-filters: # 默认过滤器,会对所有的路由都生效 + - AddRequestHeader=Test,EastWind is freaking aowsome! # 添加请求头,名称为Test,值为逗号后面的文字 +``` + +修改order-service中orderController的请求,查看是否携带了请求头 + +```java +@GetMapping("{orderId}") +public Order queryOrderByUserId(@PathVariable("orderId") Long orderId,@RequestHeader(value = "Test",required = false) String test) { + // 根据id查询订单并返回 + System.out.println("test = " + test); + return orderService.queryOrderById(orderId); +} +``` + +重启Gateway服务 + +访问: + +- http://localhost:10010/user/1 +- http://localhost:10010/order/101 + +当我们访问`http://localhost:10010/order/101`时,user会输出test=null,是因为此时在orderService中通过Feign远程调用了userService中的方法,没有涉及到网关,所以没有获取到请求头 + +总结: + +- 过滤器的作用是什么? + 1. 对路由的请求或响应做加工处理,比如添加请求头 + 2. 配置在路由下的过滤器只对当前路由的请求生效 +- defaultFilters的作用是什么? + 1. 对所有路由都生效的过滤器 + +## 全局过滤器 + +全局过滤器:GlobalFilter + +全局过滤器的作用是处理一切进入网关的请求和微服务响应,与GatewayFilter(路由过滤器)的作用一样。 + +区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GloablFilter的逻辑需要自己编写,更加灵活 + +定义方式是实现GloablFilter接口。 + +![image-20240122202705963](https://s2.loli.net/2024/01/22/1sLAmcifXoVz9wP.png) + +接着我们来编写一个全局过滤器用于拦截请求,判断请求的参数是否满足下面条件: + +- 参数中是否有test +- test参数值是否为admin + +如果同时满足则放行,否则拦截 + +在gateway中进行编写 + +```java +package cn.itcast.gateway; + +/* +@author zhangJH +@create 2024-01-22-20:31 +*/ + + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +// @Order,用于排序过滤器,根据Order的值进行排序,值越小,优先级越高 +@Order(0) +// @Component注入到容器中被Spring管理 +@Component +public class AuthorizeFilter implements GlobalFilter { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + // 1.获取请求参数 + ServerHttpRequest request = exchange.getRequest(); + MultiValueMap queryParams = request.getQueryParams(); + // 2.获取参数中的Test参数 + String test = queryParams.getFirst("test"); + // 3.判断参数值是否为admin + if ("admin".equals(test)){ + // 4.如果相等则放行 + return chain.filter(exchange); + } + // 5.否则拦截 + // 设置状态码 + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + // 拦截 + return exchange.getResponse().setComplete(); + } +} +``` + +如果想要对过滤器进行排序,除了可以使用@Order(排序值)的情况,还可以实现Ordered这个接口 + +```java +package cn.itcast.gateway; + +/* +@author zhangJH +@create 2024-01-22-20:31 +*/ + + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +// @Order,用于排序过滤器,根据Order的值进行排序,值越小,优先级越高 +@Order(0) +// @Component注入到容器中被Spring管理 +@Component +public class AuthorizeFilter implements GlobalFilter, Ordered { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + // 1.获取请求参数 + ServerHttpRequest request = exchange.getRequest(); + MultiValueMap queryParams = request.getQueryParams(); + // 2.获取参数中的Test参数 + String test = queryParams.getFirst("test"); + // 3.判断参数值是否为admin + if ("admin".equals(test)) { + // 4.如果相等则放行 + return chain.filter(exchange); + } + // 5.否则拦截 + // 设置状态码 + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + // 拦截 + return exchange.getResponse().setComplete(); + } + + @Override + public int getOrder() { + // 与@Order注解是一样的效果 + return 0; + } +} +``` + +重启Gateway + +访问:http://localhost:10010/user/1 + +此时报错401 + +访问:http://localhost:10010/user/1?test=admin + +总结: + +- 全局过滤器的作用是什么? + +对所有路由都生效的过滤器,并且可以自定义处理逻辑 + +- 实现全局过滤器的步骤? + 1. 实现GlobalFilter接口 + 2. 添加@Order注解或实现Ordered接口 + 3. 编写处理逻辑 + +## 过滤器链的执行顺序 + +请求进入网关会碰到三类过滤器:当前路由过滤器、DefaultFilter、GlobalFilter + +请求路由后,会将当前路由过滤器和DefaultFilter、GloablFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器 + +![image-20240122205513042](https://s2.loli.net/2024/01/22/ScODGzVbuxBYCZf.png) + +- 每个过滤器都必须指定一个int类型的order值,**order值越小,优先级越高,执行顺序越靠前。** +- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定 +- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增 +- 当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。 + +总结: + +- 路由过滤器、defaultFilter、全局过滤器的执行顺序? + 1. order值越小,优先级越高 + 2. 当order值一样时,顺序是defaultFilter最先,然后是局部路由过滤器,最后是全局过滤器 + +## 网关的cors跨域配置 + +首先要知道什么是跨域 + +跨域:域名不一致就是跨域,主要包括: + +- 域名不同:`www.taobao.com`和`www.taobao.org`金额`www.jd.com`金额`miaosha.jd.com` +- 域名相同,端口不同:localhost:80和localhost:81 + +跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题 + +解决方案:CORS + +网关处理跨域采用的方式同样是CORS方案,并且只需要简单的配置即可实现: + +image-20240122211851539 + +# Docker + +## 什么是Docker + +大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题: + +- 依赖关系复杂,容易出现兼容性问题 +- 开发、测试、生产环境有差异 + +这个时候就出现了,Docker + +那么,Docker如何解决依赖的兼容问题的? + +- 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包 +- 将每个应用放到一个隔离**容器**去运行,避免互相干扰 + +不同环境的操作系统不同,Docker该如何解决? + +这里了解一下Linux系统的结构 + +其实就是存在三个关系:计算机硬件、内核、系统应用 + +image-20240122215436213 + +拿Ubuntu和Centos为例,它俩都是基于Linux内核,只是系统应用不同,提供的函数库有差异,假设做了迁移,可能有些函数库有出入,会导致出现问题 + +而Docker解决的办法就是将**用户程序所需要调用的系统函数库一起打包** + +总结: + +Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题? + +- Docker允许开发中将应用、依赖、函数库、配置一起**打包**,形成可移植镜像 +- Docker应用运行在容器中,使用沙箱机制,相互**隔离** + +Docker如何解决开发、测试、生产环境有差异的问题 + +- Docker镜像中包含完整的运行环境,包括系统的函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行 + +Docker是快速交付应用、运行应用的技术: + +1. 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统 +2. 运行时利用沙箱机制形参隔离容器,各个应用互不干扰 +3. 启动、移除都可以通过一行命令完成,方便快捷 + +## Docker与虚拟机的差别 + +- docker是一个系统进程;虚拟机是在操作系统中的操作系统 +- docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般 + +## Docker的架构 + +### 镜像和容器 + +**镜像(Image)**:Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像 + +**容器(Container)**:镜像中的,应用程序,运行后,形成的进程,就是**容器**,只是Docker会给容器做隔离,对外不可见 + +镜像中的文件是不允许修改的(read only),当容器想对镜像中的内容做修改时,就需要将镜像中的内容拷贝一份到容器中,单独的在容器中对拷贝来的镜像进行修改和使用 + +![image-20240122221506183](https://s2.loli.net/2024/01/23/MAJBRnZpSt8hxGv.png) + +如果我们想将Docker镜像分享给别人使用怎么办呢 + +我们可以通过docker build构建一个镜像,然后挂载到DockerHub(一个Docker镜像的托管平台)上,也被称为Docker Registry,如果有一些镜像不想让别人使用,只想内部使用,可以构建到私有云中。 + +Docker是一个CS架构的程序,由两部分组成: + +- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等 +- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令 + +下图所示为: + +docker build通过docker守护进程构建镜像并注册到Registry + +docker pull 可以拉取docker镜像 + +docker run可以运行docker镜像 + +![image-20240122222147018](https://s2.loli.net/2024/01/22/74zxVleR8oGj5FC.png) + +总结: + +镜像: + +- 将应用程序及其依赖、环境、配置打包在一起 + +容器: + +- 把镜像运行起来就是容器,一个镜像可以运行多个容器 + +Docker结构: + +- 服务端:接收命令或远程请求,操作镜像或容器 +- 客户端:发送命令或者请求到Docker服务端 + +DockerHub: + +- 一个镜像托管的服务器,类似的还有阿里云的镜像服务,统称为DockerRegistry + +## Docker的安装 + +Docker分为CE和EE两大版本。CE即为社区版(免费,支持周期7个月),EE即企业版,强调安全,付费使用,支持周期24个月。 + +Docker CE分为`stable test`和`nightly`三个更新频道。 + +Docker CE支持64位版本Centos 7,并且要求内核版本不低于3.10,Centos 7满足最低内核的要求,所以我们在Centos 7安装Docker + +### 卸载Docker + +如果之前安装过旧版本的Docker,可以使用下面命令卸载: + +```sh +yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-selinux \ + docker-engine-selinux \ + docker-engine \ + docker-ce +``` + +### 安装Docker + +首先需要虚拟机联网,安装yum工具 + +```sh +yum install -y yum-utils \ + device-mapper-persistent-data \ + lvm2 --skip-broken +``` + + + +然后更新本地镜像源: + +```shell +# 设置docker镜像源 +yum-config-manager \ + --add-repo \ + https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo + +sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo + +yum makecache fast +``` + + + + + +然后输入命令: + +```shell +yum install -y docker-ce +``` + +docker-ce为社区免费版本。稍等片刻,docker即可安装成功。 + + + +### 启动Docker + +Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙! + +启动docker前,一定要关闭防火墙后!! + +启动docker前,一定要关闭防火墙后!! + +启动docker前,一定要关闭防火墙后!! + + + +```sh +# 关闭 +systemctl stop firewalld +# 禁止开机启动防火墙 +systemctl disable firewalld +``` + + + +通过命令启动docker: + +```sh +systemctl start docker # 启动docker服务 + +systemctl status docker # 查看docker服务 + +systemctl stop docker # 停止docker服务 + +systemctl restart docker # 重启docker服务 +``` + + + +然后输入命令,可以查看docker版本: + +``` +docker -v +``` + +### 配置镜像加速 + +docker官方镜像仓库网速较差,我们需要设置国内镜像服务: + +参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors + +## Docker的基本操作 + +### 镜像相关命令 + +- 镜像名称一般分为两部分组成:[repository]:[tag]。 +- 没有指定tag时,默认是latest,代表最新版本的镜像 + +![image-20240123095546083](https://s2.loli.net/2024/01/23/lj4G7XwDeH5u3hJ.png) + +镜像操作的常用命令 + +- docker build:构建镜像 +- docker images:查看镜像 +- docker rmi:删除镜像 +- docker push:推送镜像 +- docker pull:拉取镜像 +- docker save:保存镜像为压缩包 +- docker load:加载压缩包为镜像 + +image-20240123134708026 + +这里做一个小demo学习docker的常用命令 + +从DockerHub中拉取一个nginx镜像并查看 + +首先我们要访问DockerHub:`hub.docker.com` + +在左上角的搜索框中搜索nginx,点进去,就可以看到对应的拉取命令了 + +`docker pull nginx`,这里没有指定nginx的版本,默认是最新版 + +在终端输入命令进行拉取 + +安装完成后,可以输入`docker images`查看镜像 + +![image-20240123142736647](https://s2.loli.net/2024/01/23/GR4yfJgeCaiAvdt.png) + +接着我们可以利用docker save将nginx导出磁盘,再通过load加载回来 + +分为以下几个步骤: + +步骤一:利用docker xxx --help命令查看docker save和docker load的语法 + +步骤二:使用docker tag 创建新镜像mynginx1.0 + +步骤三:使用docker save导出镜像到磁盘 + +通过`docker save --help`我们可以查看save命令的帮助 + +``` +Usage: docker save [OPTIONS] IMAGE [IMAGE...] + +Save one or more images to a tar archive (streamed to STDOUT by default) + +Aliases: + docker image save, docker save + +Options: + -o, --output string Write to a file, instead of STDOUT +``` + +这里有一个多选项,-o的作用是写入文件,而不是输出 + +格式是这样的:docker save [多选项-o] 输出的文件名[镜像名称:版本] + +```sh +docker save -o nginx.tar nginx:latest +``` + +这时候我们再将之前保存的镜像导入回来,在此之前,我们先删除镜像 + +`docker rmi 镜像名称:版本` + +```sh +docker rmi nginx:latest +docker images # 查看镜像是否删除 +``` + +通过`docker load --help` 查看load的命令帮助 + +``` +Usage: docker load [OPTIONS] + +Load an image from a tar archive or STDIN + +Aliases: + docker image load, docker load + +Options: + -i, --input string Read from tar archive file, instead of STDIN + -q, --quiet Suppress the load output +``` + +-i是读取某个镜像 + +-q是不输出日志,`Suppress`有压制的意思 + +具体如下:`docker load -i 被读取的tar文件` + +总结: + +镜像操作有哪些? + +- docker images +- docker rmi +- docker pull +- docker push +- docker save +- docker load + +### 练习 + +完成一个小练习:去DockerHub搜索并拉取一个Redis镜像 + +流程如下: + +1. 去DockerHub搜索Redis镜像 +2. 查看Redis镜像的名称和版本 +3. 利用docker pull命令拉取镜像 +4. 利用docker save命令将redis:latest打包为一个redis.tar包 +5. 利用docker rmi删除本地的redis:latest +6. 利用docker load重新加载redis.tar + +挺简单的,练习一下即可 + +### 容器相关命令 + +- docker run:运行容器 +- docker pause:暂停容器 +- docker unpause:恢复容器为运行状态 +- docker stop:停止容器,它与暂停的区别是,暂停不会杀死容器进程,停止会直接杀死进程 +- docker start:启动容器 +- docker ps:查看所有**运行的**容器及状态 +- docker logs:查看容器运行日志 +- docker exec:进入容器执行命令 +- docker rm:删除容器,删除运行的进程及回收进程,包括硬盘上的文件 + +image-20240123151701097 + +这里我们创建并运行一个Nginx的容器 + +命令格式如下: + +```sh +docker run --name containerName -p 80:80 -d nginx +``` + +命令解读: + +- docker run:创建并运行一个容器 +- --name:容器名称 +- -p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口 +- -d:后台运行容器 +- nginx:镜像名称,例如nginx + +这里的端口映射可以这么来理解,宿主机其实相当于你的电脑,而容器是存在于虚拟机中的,容器会被单独的隔离起来,我们无法直接访问,要通过端口映射的方式进行访问,举个例子,宿主机为ip:80,假设容器是nginx,那么其的端口也是80,如果想要映射,就需要这样写`-p 80:80`,此时,当我们去访问宿主机的80端口时,就能访问到容器的80端口了,宿主机的端口一般是任意的,而容器端口不能任意,有固定的端口,也就是说,宿主机端口可以是8080、8081....这些,如果想要映射到对应服务,就:xxx端口即可 + +接下来编写nginx容器的运行 + +```sh +docker run --name ng -p 80:80 -d nginx +``` + +在运行完成后,返回了一串内容,这一串内容是该容器的id + +输入`docker ps`查看容器是否运行完毕 + +此时,我们可以访问一下nginx服务是否启动成功 + +我们可以通过宿主机来映射容器 ,以此来达成查看nginx的效果 + +我们需要查看当前虚拟机的ip,访问其的80端口,就可以看到nginx的内容了 + +访问nginx后,对应的容器中肯定会出现一些日志,我们查看一下日志的情况 + +输入`docker logs ng`,这里的ng是之前的容器名 + +如果我们想持续跟踪日志,当我们访问nginx主页后,日志中会同步的出现信息 + +我们可以使用`docker logs -f ng`,跟踪日志输出 + +``` +-f, --follow Follow log output +``` + +总结: + +docker run命令的常见参数有哪些? + +- --name:指定容器名称 +- -p:指定端口映射 +- -d:让容器后台运行 + +查看容器日志命令: + +- docker logs +- 添加-f参数可以持续查看日志 + +查看容器状态 + +- docker ps + +如果我们想对Nginx容器中的HTML文件内容进行修改,该如何操作呢 + +步骤如下: + +步骤一:进入容器 + +```sh +docker exec -it ng bash +``` + +命令解读: + +- docker exec :进入容器内部,执行一个命令 +- -it:给当前进入的容器创建一个标准输入、输出终端,运行我们与容器交互 +- ng:要进入容器的名称 +- bash:进入容器后执行的命令,bash是一个linux终端交互命令 + +具体如何查看该容器相关的信息呢,还是在DockerHub中进行查看,看nginx镜像的作者将数据存放的位置在哪了 + +``` +cd /usr/share/nginx/html/ +``` + +进入这个目录,里面有nginx静态资源的信息,我们可以修改里面的index.html文件即可 + +那么我们该如何修改index.html文件呢 + +首先想到的就是vi命令,在尝试后,发现没有vi命令,原因是这里是容器的bash环境,没有完整的linux系统,所以无法使用vi + +这里就需要用到一个命令了:`sed -i 's#Welcome to nginx#This is WindyDante#g' index.html` + +修改完成后,重新访问页面,此时页面就来到了`This is WindyDante` + +此时,如果我们想退出容器,可以输入exit + +如果想停止容器,输入:`docker stop containerName` + +再输入`docker ps`,查看运行中的容器,此时ng就从环境中剔除了 + +如果我们想查看所有容器,可以输入:`docker ps -a` + +如果想删除该容器,需要在该容器**停止时删除**,输入:`docker rm containerName` + +如果想强制删除(当容器运行时也能进行删除)容器,可以输入:`docker rm -f container` + +总结: + +- docker ps +- 添加-a参数查看所有状态的容器 + +删除容器: + +- docker rm +- 不能删除运行中的容器,除非添加-f参数 + +进入容器: + +- 命令是docker exec -it [容器名] [要执行的命令] +- exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的 + +### 练习 + +创建并运行一个redis容器,并且支持数据持久化 + +步骤如下: + +步骤一:到DockerHub搜索Redis镜像 + +步骤二:查看Redis镜像文档中的帮助信息 + +步骤三:利用docker run 命令运行一个Redis容器 + +在之前的练习中,已经拉取了Redis的镜像在本地,所以我们直接运行即可 + +```sh +docker run --name rd -p 6379:6379 -d redis +``` + +```sh +docker ps # 查看容器运行情况 +``` + +输入`docker exec -it rd bash`进入redis容器,并执行redis-cli客户端命令,存入num=666 + +输入: + +`redis-cli` + +`set num 666` + +`get num` + +此时num的值为666说明成功了 + +当然,我们不一定非要使用bash进入,也可以直接`docker exec -it rd redis-cli` + +直接进入redis客户端也是一样的 + +## 数据卷 + +在上面的学习后我们发现 + +- 当我们要修改Nginx的html内容时,需要进入容器内部修改,很不方便 + +- 而且在容器中的修改,对外是不可见的,所有修改对新创建的容器是不可复用的 +- 数据在容器内,如果要升级容器必然删除旧容器,所有数据都跟着删除了 + +此时,我们继续需要一个新功能的出现-->数据卷 + +**数据卷(volume)**是一个虚拟目录,指向宿主机文件系统中的某个目录 + +拿下图来举例 + +Volumes是数据卷,它所映射的是容器中的两个文件夹,而实际的存放位置是在宿主机的文件系统中,当我们想修改容器中的内容时,可以直接去修改数据卷中的内容,它可以映射到对应的容器中起效果,并且,如果有新创建的容器,我们可以将新创建的容器中的目录,挂载上数据卷的目录,这样就可以复用其他容器已经写好的目录了,如果我们升级了容器,删除了容器的所有数据,也不会对数据卷造成影响,因为数据卷是单独在宿主机文件系统有一个存放位置,只要不删除数据卷,就不会对数据造成影响 + +image-20240123191901336 + +### 数据卷的基本命令 + +数据卷的基本语法如下: + +```sh +docker volume [COMMAND] +``` + +docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步的操作: + +- create:创建一个volume +- inspect:显示一个或多个volume的信息 +- ls:列出所有的volume +- prune:删除未使用的volume +- rm:删除一个或多个指定的volume + +接下来我们创建一个数据卷,并查看数据卷在宿主机的目录位置 + +创建数据卷:`docker volume create html` + +列出数据卷:`docker volume ls` + +查看数据卷的存放位置:`docker volume inspect` + +``` +[ + { + "CreatedAt": "2023-11-07T05:27:47-08:00", + "Driver": "local", + "Labels": null, + "Mountpoint": "/var/lib/docker/volumes/html/_data", + "Name": "html", + "Options": null, + "Scope": "local" + } +] +``` + +`Mountpoint`:存放位置 + +删除所有未使用的数据卷:`docker volume prune` + +删除指定数据卷:`docker volume rm html` + +总结: + +数据卷的作用: + +- 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全 + +数据卷操作: + +- docker volume create(创建数据卷) +- docker volume ls(列出所有数据卷) +- docker volume rm(删除指定数据卷) +- docker volume prune(删除未使用的数据卷) + +### 挂载数据卷 + +我们在创建容器时,可以通过-v参数来挂载一个数据卷到某个容器目录 + +```sh +docker run \ +--name ng \ +-v html:/root/html \ +-p 8080:80 +nginx +``` + +参数解析: + +docker run:创建并运行容器 + +--name ng:为容器起名字 + +-v html:/root/html:把html的数据卷挂载到容器中的/root/html目录中 + +-p 8080:80:将宿主机的8080端口映射到容器的80端口中 + +nginx:镜像名称 + +根据上面的命令,我们创建一个nginx容器,修改容器内的html目录内的index.html内容 + +需求说明:上个案例中,我们进入nginx容器内部,已经知道nginx的html目录所在位置/usr/share/nginx/html,我们需要把这个目录挂载到html这个数据卷上,方便操作其内容 + +提示:运行容器时使用-v参数挂载数据卷 + +步骤: + +1. 创建容器并挂载数据卷到容器内的html目录 +2. 进入html数据卷的所在位置,并修改html内容 + +查看是否有nginx的镜像:`docker images` + +在前面我已经从dockerHub上导入过了nginx的镜像,这里就不需要再导入了 + +有了镜像之后,就需要进行容器的创建及数据卷的挂载 + +查看一下数据卷是否创建:`docker volume ls` + +这里我已经创建过了html的数据卷 + +创建容器及数据卷的挂载 + +```sh +docker run \ +--name ng \ +-v html:/usr/share/nginx/html \ +-p 8080:80 \ +-d \ +nginx +``` + +挂载完成后,通过查看数据卷的挂载位置:`docker volume inspect html`查看对应数据卷的位置,前往该位置 + +输入:`当前id:8080`,查看nginx是否启动成功,当我们修改数据卷中的内容后,查看是否有效,会发现已经生效了,如果没生效,说明浏览器有缓存,换个浏览器试一下 + +假设我们没有html数据卷,它是否会为我们创建一个呢? + +先将之前运行的容器和数据卷删除 + +```sh +docker rm -f ng +docker volume rm html +``` + +此时我们不存在之前的数据卷,再次运行之前的命令 + +```sh +docker run \ +--name ng \ +-v html:/usr/share/nginx/html \ +-p 8080:80 \ +-d \ +nginx +``` + +依然成功了,也就是说,当数据卷不存在时,我们可以直接通过创建容器,Docker会自动帮我们创建数据卷 + +总结: + +数据卷挂载方式: + +- -v volumeName :/targetContainerPath +- 如果容器运行时volume不存在,会自动被创建出来 + +### 将宿主机目录挂载到容器 + +- -v [宿主机目录]:[容器内目录] +- -v [宿主机文件]:[容器内文件] + +实现思路如下: + +1. 将资料中的mysql.tar上传到虚拟机,通过load加载为镜像 +2. 创建目录/tmp/myql/data +3. 创建目录/tmp/myql/conf,将资料中提供的hmy.cnf文件上传到/tmp/myql/conf +4. 在DockerHub查阅资料,创建并运行MySql容器,要求: + 1. 挂载/tmp/myql/data到mysql容器内数据存储目录 + 2. 挂载/tmp/myql/conf/hmy.cnf到mysql容器的配置文件 + 3. 设置MySql密码 + +以下操作在/tmp目录下进行 + +加载mysql镜像 + +```sh +docker load -i mysql.tar +``` + +查看docker 镜像是否包含了mysql的镜像 + +```sh +docker images +``` + +创建对应目录 + +```sh +mkdir -p /tmp/myql/data +mkdir -p /tmp/myql/conf +mv hmy.cnf /tmp/myql/conf +``` + +创建并运行MySql容器,挂载到对应目录 + +```sh +docker run \ +--name mq \ +-e MYSQL_ROOT_PASSWORD=123 \ +-p 3306:3306 \ +-v /tmp/myql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ +-v /tmp/myql/data:/var/lib/mysql \ +-d \ +mysql:5.7.25 +``` + +`-e MYSQL_ROOT_PASSWORD=123` :-e是设置环境变量,这里是设置mysql的密码 + +此时来到/tmp/myql/data目录下,查看hmy.cnf下是否有数据 + +总结: + +docker run的命令中通过-v参数挂载文件或目录到容器中: + +1. -v volume名称:容器内目录 +2. -v 宿主机文件:容器内文件 +3. -v 宿主机目录:容器内目录 + +数据卷挂载与目录直接挂载的区别 + +1. 数据卷挂载的耦合度低,由Docker来管理目录,但是目录较深,不好找 +2. 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看 + +## DockerFile自定义镜像 + +### 镜像结构 + +- **入口(Entrypoint)**:镜像运行入口,一般是程序启动的脚本和参数 +- **层(Layer)**:在BaseImage基础上添加安装包、依赖、配置等,每次操作都形成新的一层 +- **基础镜像(BaseImage)**:应用依赖的系统函数库、环境、配置、文件等 + +以下图为例就可以很好的说明上面的结构,最底层是基础镜像,中间是层,入口程序启动的参数和脚本 + +image-20240124091154837 + +总结: + +镜像是分层结构,每一层称为一个Layer + +- BaseImage层:包含基本的系统函数库、环境变量、文件系统 +- Entrypoint:入口,是镜像中应用启动的命令 +- 其他:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置 + +### 什么是Dockerfile + +Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。 + +| 指令 | 说明 | 示例 | +| :--------: | :------------------------------------------: | :-------------------------: | +| FROM | 指定基础镜像 | FROM centos:7 | +| ENV | 设置环境变量,可在后面指令使用 | ENV key value | +| COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql /tmp | +| RUN | 执行linux的shell命令,一般是安装过程的命令 | RUN yum install gcc | +| EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8800 | +| ENTRYPOINT | 镜像中应用启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar | + +这里我们基于Ubuntu镜像构建一个新镜像,运行一个java项目 + +以下操作在tmp目录执行 + +步骤一:新建一个空文件夹docker-demo + +步骤二:拷贝资料中的docker-demo.jar到docker-demo目录 + +步骤三:拷贝资料中的jdk8.tar.gz到docker-demo目录 + +步骤四:拷贝资料中的Dockerfile到docker-demo目录 + +步骤五:进入docker-demo + +步骤6:运行命令 + +```sh +docker build -t javaweb:1.0 . +``` + +这里详细看一下Dockerfile + +导入ubuntu的16.04的镜像,并配置jdk的安装目录 + +拷贝jdk到jdk的安装目录,且拷贝jar包到指定位置并改名 + +安装jdk,配置jdk的环境变量 + +暴露端口,编写java项目的启动命令 + +```dockerfile +# 指定基础镜像 +FROM ubuntu:16.04 +# 配置环境变量,JDK的安装目录 +ENV JAVA_DIR=/usr/local + +# 拷贝jdk和java项目的包 +COPY ./jdk8.tar.gz $JAVA_DIR/ +COPY ./docker-demo.jar /tmp/app.jar + +# 安装JDK +RUN cd $JAVA_DIR \ + && tar -xf ./jdk8.tar.gz \ + && mv ./jdk1.8.0_144 ./java8 + +# 配置环境变量 +ENV JAVA_HOME=$JAVA_DIR/java8 +ENV PATH=$PATH:$JAVA_HOME/bin + +# 暴露端口 +EXPOSE 8090 +# 入口,java项目的启动命令 +ENTRYPOINT java -jar /tmp/app.jar +``` + +具体操作步骤如下: + +```sh +cd /tmp +mkdir docker-demo +cd docker-demo +# 拷贝对应资料到docker-demo +docker build -t javaweb:1.0 . +``` + +此时自定义镜像构建完毕,输入:`docker images`查看镜像是否存在 + +接着我们将这个镜像运行起来 + +```sh +docker run \ +--name web \ +-p 8090:8090 \ +-d \ +javaweb:1.0 +``` + +访问:`http://宿主机ip:8090/hello/count`即可查看相应内容 + +像上述一些操作是可复用的,我们就可以整理出来单独的作为一个镜像,不过像一些最基本的镜像别人已经帮我们制作过了,比如java8的镜像之类的 + +总结: + +1. Dockerfile的本质是一个文件,通过指令描述镜像的构建过程 +2. Dockerfile的第一行必须是FROM,从一个基础镜像来构建 +3. 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine + + + +## DockerCompose + +当生产环境下,微服务一个个的构建就非常麻烦了,此时就需要用到集群部署的方法了 + +- Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器 +- Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行 + +Linux下需要通过命令下载: + +```sh +# 安装 +curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose +``` + +如果下载速度较慢,或者下载失败,可以使用课前资料提供的docker-compose文件,将其上传到`/usr/local/bin`目录 + +修改文件权限: + +为添加执行权限 + +```sh +# 修改权限 +chmod +x /usr/local/bin/docker-compose +``` + +Base自动补全命令: + +添加自动补全后,使用DockerCompose会有提示 + +```sh +# 补全命令 +curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose +``` + +如果这里出现错误,需要修改自己的hosts文件: + +```sh +echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts +``` + +## Docker镜像仓库 + +镜像仓库(Docker Registry)有公共和私有的两种形式: + +- 公共仓库:例如Docker官方的Docker Hub,阿里云镜像服务等等 +- 除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。 + +### 简化版镜像仓库 + +Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。 + +搭建方式比较简单,命令如下: + +```sh +docker run -d \ + --restart=always \ + --name registry \ + -p 5000:5000 \ + -v registry-data:/var/lib/registry \ + registry +``` + + + +命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。 + +访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像 + +### 图形化页面版本 + +使用DockerCompose部署带有图像界面的DockerRegistry,命令如下: + +```yaml +version: '3.0' +services: + registry: + image: registry + volumes: + - ./registry-data:/var/lib/registry + ui: + image: joxit/docker-registry-ui:static + ports: + - 8080:80 + environment: + - REGISTRY_TITLE=Eastwind私有仓库 + - REGISTRY_URL=http://registry:5000 + depends_on: + - registry +``` + +### 配置Docker信任地址 + +我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置: + +```sh +# 打开要修改的文件 +vi /etc/docker/daemon.json +# 添加内容: +"insecure-registries":["http://YouIp:8080"] +# 重加载 +systemctl daemon-reload +# 重启docker +systemctl restart docker +``` + +### 在私有镜像仓库推送或拉取镜像 + +推送镜像到私有镜像服务必须先tag,步骤如下: + +重新tag本地镜像,名称前缀为私有仓库的地址:YouIP:8080/ + +```sh +docker tag nginx:latest YouIP:8080/nginx:1.0 +``` + +推送镜像 + +```sh +docker push YouIP:8080/nginx:1.0 +``` + +拉取镜像 + +```sh +docker pull YouIP:8080/nginx:1.0 +``` + +总结: + +1. 推送本地镜像到仓库前必须重命名(docker tag)镜像,以镜像仓库地址为前缀 +2. 镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json中,被docker信任 +3. 推送使用docker push命令 +4. 拉取使用docker pull命令 +5. 修改镜像名称使用docker tag命令 + +# RabbitMq + +## 同步通讯 + +以下图为例: + +同步通讯:当你向其他人发起视频通话时,对方说的话你能马上知道,并且你说的对方也能马上知道,这种就是同步通讯,并且在同步通讯时,其他人是无法向你发起会话的 + +异步通讯:当你给别人发微信时,别人不一定能秒回你的消息,可能要等到他看到你的消息时,才能进行回复,并且这种消息呢,你可以给多个人发,多个人也可以给你回复,并不是说你给他单独发了,就不能给其他人单独发了 + +image-20240124123300604 + +同步通讯的缺点: + +1. 耦合度高:每次加入新的需求,都要修改原来的代码 +2. 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和 +3. 资源浪费:调用链的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源 +4. 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务集群故障 + +总结: + +同步调用的优点: + +- 时效性较强,可以立即得到结果 + +同步调用的问题: + +- 耦合度高 +- 性能和吞吐能力下降 +- 有额外的资源消耗 +- 有级联失败问题 + +## 异步调用 + +异步调用常见实现就是事件驱动模式 + +以下图为例,这里多出了一个Broker(代理),当支付服务结束后,会发布一个消息,这个消息会去通知后面的服务继续往后执行,而支付服务直接返回支付成功的消息即可,这样需要的耗时只在支付服务上了 + +image-20240124125326566 + +总结: + +异步通信的优点: + +- 耦合度低 +- 吞吐量提升 +- 故障隔离 +- 流量削峰 + +异步通信的缺点: + +- 依赖于Broker的可靠性、安全性、吞吐能力 +- 架构复杂,业务没有明显的流程线,不好追踪管理 + + + +## MQ常见技术介绍 + +MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker(代理) + +![image-20240124131204186](https://s2.loli.net/2024/01/24/nfUtTe41E8IhbVS.png) + +## RabbitMq快速入门 + +首先要将RabbitMq部署到当前系统上 + +### 单机部署 + +方式一:在线拉取 + +从dockerHub上拉取镜像 + +``` sh +docker pull rabbitmq:3-management +``` + +方式二:从本地加载 + +在课前资料已经提供了镜像包,上传到虚拟机中后,使用命令加载镜像即可 + +```sh +docker load -i mq.tar +``` + +#### 安装MQ + +执行下面的命令来运行MQ容器: + +```sh +docker run \ + -e RABBITMQ_DEFAULT_USER=eastwind \ + -e RABBITMQ_DEFAULT_PASS=123 \ + --name mq \ + --hostname mq1 \ + -p 15672:15672 \ + -p 5672:5672 \ + -d \ + rabbitmq:3-management +``` + +15672是控制台端口,所以在浏览器中访问该端口就可以看到rabbitmq了 + +### RabbitMq的结构 + +Google浏览器访问:`YourIp:15672`,在其他浏览器访问不一定能进入与视频相同的页面 + +输入之前在环境变量里面写好的用户名和密码 + +这是一个虚拟主机,每个虚拟主机之间是分离的,都有着各自的内容,这里我们新建一个虚拟主机 + +image-20240124152936083 + +并为其分配用户,我们可以新建一个用户,并为其分配虚拟主机,这样的话,每个用户就有其独立的虚拟主机环境了 + +image-20240124153252727 + +image-20240124153320134 + +image-20240124153518221 + +此时,这两块区域就存在着不同的虚拟主机了 + +image-20240124153630400 + +RabbitMQ的结构和概念 + +下面这个图就基本讲述RabbitMQ的基本结构了 + +Publisher是消息的发送方,他会将消息发送到exchange(交换机上),而exchange会将消息转发到队列上,最后consumer(消息订阅者)会监听队列获取消息 + +image-20240124153908096 + +总结: + +RabbitMQ中的几个概念: + +- channel:操作MQ的工具 +- exchange:路由消息到队列中 +- queue:缓存消息 +- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组 + +### 消息模型 + +- 基本消息队列(BasicQueue) +- 工作消息队列(WorkQueue) + +发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种: + +- Fanout Exchange:广播 +- Direct Exchange:路由 +- Topic Exchange:主题 + +#### 基本消息队列 + +基本消息队列包含三个角色: + +- publisher:消息发布者,将消息发送到队列queue +- queue:消息队列,负责接受并缓存消息 +- consumer:订阅队列,处理队列中的消息 + +实现基本消息队列的步骤: + +- 导入资料中的mq-demo工程 +- 原型publisher服务中的测试类PublisherTest中的测试方法testSendMessage() +- 查看RabbitMQ控制台的消息 +- 启动consumer服务,查看是否能接收消息 + +在消息发送方(publish)和消息接收方(consumer)的测试方法中,修改自己的ip、用户名、密码信息 + +总结: + +基本消息队列的消息发送流程: + +1. 建立connection +2. 创建channel(通道) +3. 利用channel声明队列 +4. 利用channel向队列发送消息 + +基本消息队列的消息接收流程: + +1. 建立connection +2. 创建channel +3. 利用channel声明队列 +4. 定义consumer的消费行为handleDelivery() +5. 利用channel将消费者与队列绑定 + +这里都声明队列是防止队列不存在的情况导致程序出错 + +## SpringAMQP + +### 什么是SpringAMQP + +想知道什么是SpringAMQP,首先要知道什么是AMQP + +AMQP是:Advanced Message Queuing Protocol(高级消息队列协议),是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求 + +SpringAMQP是基于AMQP协议定义的一套API规范,提供了模版来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现 + +### 利用SpringAMQP实现基础消息队列功能 + +流程如下: + +1. 在父工程中引入spring-amqp的依赖 +2. 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列 +3. 在consumer服务中编写消费逻辑,绑定simple.queue这个队列 + +引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-amqp + +``` + +在publisher服务中编写application.yml,添加mq连接信息: + +```yaml +spring: + rabbitmq: + port: 5672 + username: eastwind + password: 123 + virtual-host: / + host: 192.168.200.129 +``` + +新建测试类,用于在publisher中发送消息 + +```java +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringAMQPTest { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Test + public void testSend2SimpleQueue() { + String queueName = "simple.queue"; + String message = "Hello world"; + rabbitTemplate.convertAndSend(queueName,message); + } +} +``` + +这里记得创建一下simple.queue这个队列,防止它不存在 + +![image-20240124191635932](https://s2.loli.net/2024/01/24/Mg7uVR14ek9tKNm.png) + +运行进行测试 + +运行后无问题的话,回到管理页面,进行查看消息是否被队列存储了 + +![image-20240124191801803](https://s2.loli.net/2024/01/24/otUdBDFIxLevP18.png) + +image-20240124191849088 + +消息无误说明消息的发送成功了 + +总结: + +什么是AMQP? + +- 应用间通信的一种协议,与语言平台无关。 + +SpringAMQP如何发送消息? + +- 引入amqp的starter依赖 +- 配置RabbitMQ地址 +- 利用RabbitTemplate的convertAndSend方法 + +发送方结束了,接下来就是接收方了,接收方肯定也需要先配置地址信息 + +信息是一样的,直接从publisher里复制到consumer的application.yml里即可 + +编写消息接收代码 + +```java +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class SpringRabbitListener { + + // 监听simple.queue的消息 + @RabbitListener(queues = "simple.queue") + public void listenSimpleQueueMessage(String msg){ + System.out.println(msg); + } + +} +``` + +运行`ConsumerApplication`,启动后控制台就会打印消息了 + +并且回到Mq的主页,之前的存储在队列的消息也不见了,获取后就自动删除了 + +SpringAMQP如何接收消息? + +- 引入amqp的starter依赖 +- 配置RabbitMQ地址 +- 定义类,添加@Component注解 +- 类中声明方法,添加@RabbitListener注解,方法参数就是对应的消息 + +主要:消息一旦被消费就会从队列中删除,RabbitMQ没有消息回溯功能 + +### Work Queue工作队列 + +Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积 + +Work queue与基础消息队列不同的是,它有两个消费者,可以均匀的来分别处理消息,减少单个消费者的压力 + +image-20240124193630039 + + 模拟WorkQueue,实现一个队列绑定多个消费者 + +基本思路如下: + +1. 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue +2. 在consumer服务中定义两个消息监听者,都监听simple.queue队列 +3. 消息者1每秒处理50条消息,消费者2每秒处理10条消息 + +变为每秒产生50条消息 + +```java +@Test +public void testSend2WorkQueue() throws InterruptedException { + String queueName = "simple.queue"; + String message = "Hello Work message_"; + for (int i = 0; i < 50; i++) { + rabbitTemplate.convertAndSend(queueName,message + i); + Thread.sleep(20); + } +} +``` + +消息消费者变为两个消费者 + +```java +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class SpringRabbitListener { + +// // 监听simple.queue的消息 +// @RabbitListener(queues = "simple.queue") +// public void listenSimpleQueueMessage(String msg){ +// System.out.println(msg); +// } + + @RabbitListener(queues = "simple.queue") + public void listenWorkQueue1Message(String msg) throws InterruptedException { + System.out.println("消费者1------------:"+msg); + Thread.sleep(20); + } + + @RabbitListener(queues = "simple.queue") + public void listenWorkQueue2Message(String msg) throws InterruptedException { + System.out.println("消费者2:==========="+msg); + Thread.sleep(200); + } + +} +``` + +此时我们想达成的效果是,消费者1多消费一些,消费者2少消费一些 + +此时重启运行,会发现两者消费的内容依然是平均的,只是变成了谁先消费而已 + +出现这种情况是因为,两个消费者都会有消息预取的情况,就是说,提前获取消息到消费者上,一个一个的慢慢处理消息 + +我们可以配置预取消息的上限来解决这个问题 + +```yaml +logging: + pattern: + dateformat: MM-dd HH:mm:ss:SSS +spring: + rabbitmq: + port: 5672 + username: eastwind + password: 123 + virtual-host: / + host: 192.168.200.129 + listener: + simple: + prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一条消息 +``` + +此时消费者1的消费就变得很多了,因为消费者2的堵塞时间很长 + +总结: + +Work模型的使用: + +- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理 +- 通过设置prefetch来控制消费者预取的消息数量 + +### 发布订阅模型介绍 + +发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。 + +常见的exchange类型包括: + +- Fanout:广播 +- Direct:路由 +- Topic:话题 + +image-20240124210710571 + +注意:exchange负责路由消息,而不是存储,若路由失败则消息丢失 + +### FanoutExchange + +Fanout Exchange 会将接收到的消息路由到每一个跟其绑定的queue + +简单来说,就是这个交换机在接收到消息后,会给每个队列都发一份消息 + +image-20240125102224788 + +利用SpringAMQP演示FanoutExchange + +实现思路如下: + +1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定 +2. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2 +3. 在publisher中编写测试方法,向eastwind.fanout发送消息 + +Exchange的体系结构 + +![image-20240125104402433](https://s2.loli.net/2024/01/25/oi2EtagQh6lJdu3.png) + +FanoutExchange是Exchange的实现类,直接new就可以 + +在consumer里新建一个配置类 + +```java +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FanoutConfig { + + // 声明fanout交换机 + @Bean + public FanoutExchange fanoutExchange(){ + return new FanoutExchange("eastwind.fanout"); + } + + // 声明队列1 + @Bean + public Queue fanoutQueue1(){ + return new Queue("fanout.queue1"); + } + + // 声明队列2 + @Bean + public Queue fanoutQueue2(){ + return new Queue("fanout.queue2"); + } + + // 将队列1绑定到fanout上 + @Bean + public Binding fanoutBuilder(Queue fanoutQueue1, FanoutExchange fanoutExchange){ + return BindingBuilder + .bind(fanoutQueue1) + .to(fanoutExchange); + } + + // 将队列2绑定到fanout上 + @Bean + public Binding fanoutBuilder2(Queue fanoutQueue2, FanoutExchange fanoutExchange){ + return BindingBuilder + .bind(fanoutQueue2) + .to(fanoutExchange); + } + + +} +``` + +启动该程序,访问mq的主页,是否存在对应的exchange和queue + +接着我们实现消息的发布与订阅 + +先编写consumer的接收消息 + +```java +@RabbitListener(queues = "fanout.queue1") +public void listenToFanout1(String msg) throws InterruptedException { + System.out.println("消费者1:==========="+msg); +} + +@RabbitListener(queues = "fanout.queue2") +public void listenToFanout2(String msg) throws InterruptedException { + System.out.println("消费者2:==========="+msg); +} +``` + +重启服务,并编写消息的发送 + +```java +@Test +public void testSendFanoutExchange() { + // 交换机名称 + String exchangeName = "eastwind.fanout"; + // 消息 + String message = "hello fanout"; + // 交换机名称,routingKey,消息 + // routingKey暂时不用管,后面会说 + rabbitTemplate.convertAndSend(exchangeName,"",message); +} +``` + +运行程序,会发现两个队列中都收到了消息 +总结: + +交换机的作用是什么? + +- 接收publisher发送的消息 +- 将消息按照规则路由到与之绑定的队列 +- 不能缓存消息,路由失败,消息丢失 +- FanoutExchange的会将消息路由到每个绑定的队列 + +声明队列、交换机、绑定关系的Bean是什么? + +- Queue +- FanoutExchange +- Binding + +### DirectExchange + +DirectExchange会将接受到的消息根据规则路由到指定Queue,因此称为路由模式(routes)。 + +- 每一个Queue都与Exchange设置一个BindingKey +- 发布者发送消息时,指定消息的RoutingKey +- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列 + +以下图为例:假设queue1有已经设置好的bindingKey:red和blue,而queue2有已经设置好的yellow和red,当exchange拿着routingKey为yellow的,就会发送给bindingKey为yellow的,也就是queue2,假设exchange拿着routingKey为red的,此时两边都会接收到消息 + +![image-20240125135550224](https://s2.loli.net/2024/01/25/7GmNxIArqKHTcyj.png) + +实现思路如下: + +1. 利用@RabbitListener声明Exchange、Queue、RoutingKey +2. 在consumer服务中,编写两个消费者方法,分别监听direct.queuq1和direct.queue2 +3. 在publisher中编写测试方法,向itcast.direct发送消息 + +在SpringRabbitListener里进行exchange和queue的声明 + +```java +@RabbitListener( + bindings = @QueueBinding( + value = @Queue(name = "direct.queue1"), + exchange = @Exchange(name = "eastwind.direct",type = ExchangeTypes.DIRECT), + key = {"red","blue"} + ) +) +public void listenDirectQueue1(String msg) throws InterruptedException { + System.out.println("消费者1接收到direct.queue1的消息:==========="+msg); +} + +@RabbitListener( + bindings = @QueueBinding( + value = @Queue(name = "direct.queue2"), + exchange = @Exchange(name = "eastwind.direct",type = ExchangeTypes.DIRECT), + key = {"red","yellow"} + ) +) +public void listenDirectQueue2(String msg) throws InterruptedException { + System.out.println("消费者1接收到direct.queue2的消息:==========="+msg); +} +``` + +这里使用的是注解形式的绑定,更方便快捷 + +重启服务,查看mq主页是否出现了队列和交换机 + +编写测试方法,变化不大 + +```java +@Test +public void testSendDirectExchange() { + // 交换机名称 + String exchangeName = "eastwind.direct"; + // 消息 + String message = "hello direct"; + rabbitTemplate.convertAndSend(exchangeName,"red",message); +} +``` + +运行测试,查看是否两个消费者都收到了消息 + +修改测试,查看direct.queue1是否收到消息 + +```java +@Test +public void testSendDirectExchange() { + // 交换机名称 + String exchangeName = "eastwind.direct"; + // 消息 + String message = "hello direct"; + rabbitTemplate.convertAndSend(exchangeName,"blue",message); +} +``` + +总结: + +描述Direct交换机与Fanout交换机的差异? + +- Fanout交换机将消息路由给每一个与之绑定的队列 +- Direct交换机根据RoutingKey判断路由给哪个队列 +- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似 + +基于@RabbitListener注解声明队列与交换机有哪些常见注解? + +- @Queue +- @Exchange + +### TopicExchange + +TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并以`.`分割 + +Queue与Exchange指定BindingKey时可以使用通配符: + +#:代指0个或多个单词 + +*:代指一个单词 + +类似于模糊查询这种 + +image-20240125143829156 + +和普通交换机没啥区别,只是多了一个模糊匹配的routingKey + +```java +@RabbitListener(bindings = @QueueBinding( + value = @Queue(name = "topic.queue1"), + exchange = @Exchange(name = "exchange.topic",type = ExchangeTypes.TOPIC), + key = "china.#" +)) +public void listenTopicQueue1(String msg){ + System.out.println("topic.queue1的消息:==========="+msg); +} + +@RabbitListener(bindings = @QueueBinding( + value = @Queue(name = "topic.queue2"), + exchange = @Exchange(name = "exchange.topic",type = ExchangeTypes.TOPIC), + key = "#.news" +)) +public void listenTopicQueue2(String msg){ + System.out.println("topic.queue2的消息:==========="+msg); +} +``` + +重启服务,查看mq主页是否出现了队列和交换机 + +编写发送代码 + +china.news的测试情况应该是两个都能收到对应的消息 + +```java +@Test +public void testSendTopicExchange() { + // 交换机名称 + String exchangeName = "exchange.topic"; + // 消息 + String message = "hello topic"; + rabbitTemplate.convertAndSend(exchangeName,"china.news",message); +} +``` + +a.news的测试情况应该是topic2能收到 + +```java +@Test +public void testSendTopicExchange() { + // 交换机名称 + String exchangeName = "exchange.topic"; + // 消息 + String message = "hello topic"; + rabbitTemplate.convertAndSend(exchangeName,"a.news",message); +} +``` + +### 消息转化器 + +我们现在发送的消息一直是String类型,如果消息发的是Object类型会怎么样呢? + +我们先在FanoutConfig编写一个测试queue,等会发object的message都在这个queue里 + +```java +@Bean +public Queue objectQueue(){ + return new Queue("object.queue"); +} +``` + +重启服务,编写消息的发送 + +```java +@Test +public void testSendObj() { + Map msg = new HashMap(); + msg.put("name","hello"); + msg.put("age",21); + rabbitTemplate.convertAndSend("object.queue",msg); +} +``` + +运行测试后,在mq中的object.queue队列查看信息 + +![image-20240125152733086](https://s2.loli.net/2024/01/25/8EH4iN5CPRItbXr.png) + +此时,我们发现,消息乱码了,这是因为消息采用的是Java中的序列化方式,导致传输过来的内容是字节的形式 + +我们应该如何修改呢 + +Spring的对消息处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。 + +如果要修改,只需要定义一个MessageConverter类型的Bean即可。推荐用JSON方式序列化,步骤如下: + +在父工程引入JSON序列化依赖: + +```xml + + com.fasterxml.jackson.core + jackson-databind + +``` + +在publisher服务中声明MessageConverter的Bean + +这里我声明在了启动类上 + +```java +@Bean +public MessageConverter messageConverter(){ + return new Jackson2JsonMessageConverter(); +} +``` + +重新发消息测试,此时发送的消息就是正常的了 + +此时如果要接收消息,也需要定义消息转换器 + +步骤是一致的:引入依赖,添加消息转换器,定义消费者监听消息 + +依赖已经在父工程引入过了,所以不需要二次引入了 + +消息转换器和之前是一样的,还是放在启动类里 + +这里接收消息时,**也需要使用Map**,因为发送时是Map,需要使用一个类型 + +```java +@RabbitListener(queues = "object.queue") + public void listenObj(Map msg) throws InterruptedException { + System.out.println("==========="+msg); + } +``` + +重启服务 + +总结: + +SpringAMQP中消息的序列化和反序列化是怎么实现的? + +- 利用MessageConverter实现的,默认是JDK的序列化 +- 注意发送方与接收方必须使用相同的MessageConverter + +# ES + +## 什么是ES + +ES,又称elasticsearch,是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。 + +elastucsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。 + +elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。 + +image-20240125174209259 + +而elasticsearch的底层是Lucene + +Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。 + +官网地址:https://lucene.apache.org/ + +Luecene的优势: + +- 易拓展 +- 高性能(基于倒排索引) + +Luecene的缺点: + +- 只限于Java语言开发 +- 学习曲线陡峭 +- 不支持水平拓展 + +2004年Shay Banon基于Lucene开发了Compass + +2010年Shay Banon重写了Compass,取名Elasticseach + +官网地址:https://www.elastic.co/cn/ + +目前最新的版本是:7.12.1 + +相比于lucene,elasticsearch具备下列优势: + +- 支持分布式,可水平拓展 +- 提供Restful接口,可被任何语言调用 + +总结: + +什么是elasticsearch? + +- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能 + +什么是elastic stack(ELK)? + +- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch + +什么是Lucene? + +- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API + +## 正向索引和倒排索引 + +传统数据库(如MySql)采用正向索引,例如给下表(tb_goods)中的id创建索引: + +它会逐条搜索,查看是否包含,如果包含,就存入结果集,否则丢弃 + +image-20240125181424096 + +elasticsearch采用倒排索引: + +- 文档(document):每条数据就是一个文档 +- 词条(term):文档按照语义分成的词语 + +查询时,也会对查询的条件进行分词,再到倒排索引的表中根据分词后的结果查询词条,会得到一连串的文档id,存入结果集中,最后可以根据文档id去正向索引表中查询结果集 + +image-20240125182500051 + +总结: + +什么是文档和词条? + +- 每一条数据就是一个文档 +- 对文档中的内容分词,得到的词语就是词条 + +什么是正向索引? + +- 基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条 + +什么是倒排索引? + +- 是对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档 + +## es与mysql的对比 + +elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。 + +文档数据会被序列化为json格式后存储在elasticsearch中 + +image-20240125185712060 + +索引: + +- 索引(index):相同类型的文档的集合 +- 映射(mapping):索引中文档的字段约束信息,类似于表的结构约束 + +image-20240125190238145 + +### 概念对比 + +![image-20240125190724388](https://s2.loli.net/2024/01/25/w4HANjXyWTume7d.png) + +Mysql:擅长事务类型操作,可以确保数据的安全和一致性 + +Elasticsearch:擅长海量数据的搜索、分析、计算 + +一般情况下呢,写操作都会写入到mysql中,而读操作都会通过es来读取,此时,这俩都需要有数据,那么从mysql中写入的数据,如何让es中也有呢,这就涉及到了数据同步了,将mysql中的数据同步到es中即可 + +总结: + +文档:一条数据就是一个文档,es中文档是json格式 + +字段:json文档中的字段 + +索引:同类型文档的集合 + +映射:索引中文档的约束,比如字段名称、类型 + +elasticsearch与数据库的关系: + +- 数据库负责事务类型操作 +- elasticsearch负责海量数据的搜索、分析、计算 + +## 安装es、kibana + +### 单点部署es + +因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络: + +```sh +docker network create es-net +``` + +这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。 + +课前资料提供了镜像的tar包 + +将其上传到虚拟机中,然后运行命令加载即可: + +```sh +# 导入数据 +docker load -i es.tar +``` + +同理还有`kibana`的tar包也需要这样做。 + +运行docker命令,部署单点es: + +```sh +docker run -d \ + --name es \ + -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ + -e "discovery.type=single-node" \ + -v es-data:/usr/share/elasticsearch/data \ + -v es-plugins:/usr/share/elasticsearch/plugins \ + --privileged \ + --network es-net \ + -p 9200:9200 \ + -p 9300:9300 \ +elasticsearch:7.12.1 +``` + +命令解释: + +- `-e "cluster.name=es-docker-cluster"`:设置集群名称 +- `-e "http.host=0.0.0.0"`:监听的地址,可以外网访问 +- `-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"`:内存大小 +- `-e "discovery.type=single-node"`:非集群模式 +- `-v es-data:/usr/share/elasticsearch/data`:挂载逻辑卷,绑定es的数据目录 +- `-v es-logs:/usr/share/elasticsearch/logs`:挂载逻辑卷,绑定es的日志目录 +- `-v es-plugins:/usr/share/elasticsearch/plugins`:挂载逻辑卷,绑定es的插件目录 +- `--privileged`:授予逻辑卷访问权 +- `--network es-net` :加入一个名为es-net的网络中 +- `-p 9200:9200`:端口映射配置 + + + +在浏览器中输入:http://yourIp:9200 即可看到elasticsearch的响应结果 + +### 部署kibana + +kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。 + +运行docker命令,部署kibana + +```sh +docker run -d \ +--name kibana \ +-e ELASTICSEARCH_HOSTS=http://es:9200 \ +--network=es-net \ +-p 5601:5601 \ +kibana:7.12.1 +``` + +- `--network es-net` :加入一个名为es-net的网络中,与elasticsearch在同一个网络中 +- `-e ELASTICSEARCH_HOSTS=http://es:9200"`:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch +- `-p 5601:5601`:端口映射配置 + +kibana启动一般比较慢,需要多等待一会,可以通过命令: + +```sh +docker logs -f kibana +``` + +查看运行日志,当查看到下面的日志,说明成功: + +此时,在浏览器输入地址访问:http://yourIp:5601,即可看到结果 + +点击按钮 + +![image-20240125204607559](https://s2.loli.net/2024/01/25/9snu8IREA4KgPQm.png) + +image-20240125205244339 + +我们可以在Dev tools中模拟一下Get请求,之前在访问ES主页时,其实也是发了一个get请求,我们来模拟一下 + +image-20240125205918964 + +## 安装IK分词器 + +es在创建倒排索引时,需要对文档进行分词;在搜索时,需要对用户输入内容分词。但默认的分词规则则对中文处理并不友好。我们在kibana的DevTools中测试: + +```es +POST /_analyze +{ + "analyzer": "english", + "text": "EastWind 太喜欢学Java啦!" +} +``` + +语法说明: + +- POST:请求方式 +- /_analyze:请求路径,这里省略你的IP和端口,有kibana帮我们补充 +- 请求参数,json风格: + - analyzer:分词器类型,这里是默认的standard分词器 + - text:要分词的内容 + +```json +{ + "tokens" : [ + { + "token" : "eastwind", + "start_offset" : 0, + "end_offset" : 8, + "type" : "", + "position" : 0 + }, + { + "token" : "太", + "start_offset" : 9, + "end_offset" : 10, + "type" : "", + "position" : 1 + }, + { + "token" : "喜", + "start_offset" : 10, + "end_offset" : 11, + "type" : "", + "position" : 2 + }, + { + "token" : "欢", + "start_offset" : 11, + "end_offset" : 12, + "type" : "", + "position" : 3 + }, + { + "token" : "学", + "start_offset" : 12, + "end_offset" : 13, + "type" : "", + "position" : 4 + }, + { + "token" : "java", + "start_offset" : 13, + "end_offset" : 17, + "type" : "", + "position" : 5 + }, + { + "token" : "啦", + "start_offset" : 17, + "end_offset" : 18, + "type" : "", + "position" : 6 + } + ] +} +``` + +英文的分词没有任何问题,但是中文分词后是一个一个的汉字,这显然是有问题的 + +### 在线安装IK(较慢) + +```sh +# 进入容器内部 +docker exec -it elasticsearch /bin/bash + +# 在线下载并安装 +./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip + +#退出 +exit +#重启容器 +docker restart elasticsearch +``` + +### 离线安装IK插件(推荐) + +安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看: + +```sh +docker volume inspect es-plugins +``` + +显示结果: + +```json +[ + { + "CreatedAt": "2022-05-06T10:06:34+08:00", + "Driver": "local", + "Labels": null, + "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data", + "Name": "es-plugins", + "Options": null, + "Scope": "local" + } +] +``` + +说明plugins目录被挂载到了:`/var/lib/docker/volumes/es-plugins/_data `这个目录中。 + +下面我们需要把课前资料中的ik分词器解压缩,重命名为ik + +上传到es容器的插件数据卷中,也就是`/var/lib/docker/volumes/es-plugins/_data ` + +```shell +# 4、重启容器 +docker restart es +``` + +```sh +# 查看es日志 +docker logs -f es +``` + +### 测试ik分词 + +IK分词器包含两种模式: + +* `ik_smart`:最少切分 + +* `ik_max_word`:最细切分 + +```es +POST /_analyze +{ + "analyzer": "ik_max_word", + "text": "EastWind 太喜欢学Java啦!" +} +``` + +此时运行后,中文分词也没问题了 + +```json +{ + "tokens" : [ + { + "token" : "eastwind", + "start_offset" : 0, + "end_offset" : 8, + "type" : "ENGLISH", + "position" : 0 + }, + { + "token" : "太", + "start_offset" : 9, + "end_offset" : 10, + "type" : "CN_CHAR", + "position" : 1 + }, + { + "token" : "喜欢", + "start_offset" : 10, + "end_offset" : 12, + "type" : "CN_WORD", + "position" : 2 + }, + { + "token" : "学", + "start_offset" : 12, + "end_offset" : 13, + "type" : "CN_CHAR", + "position" : 3 + }, + { + "token" : "java", + "start_offset" : 13, + "end_offset" : 17, + "type" : "ENGLISH", + "position" : 4 + }, + { + "token" : "啦", + "start_offset" : 17, + "end_offset" : 18, + "type" : "CN_CHAR", + "position" : 5 + } + ] +} +``` + +## ik-拓展词库 + +有些文字比较新颖,有些文字是禁忌,像一些不允许出现的文字或者一些ik中没有的文字,就需要进行拓展了 + +要拓展ik分词器的词库,只需要修改一个ik分词器目录中的config目录的IkAnalyzer.cfg.xml文件: + +一般挂载后在:/var/lib/docker/volumes/es-plugins/_data/ik/config + +在该文件夹找到IkAnalyzer.cfg.xml + +```sh +vi IkAnalyzer.cfg.xml +``` + +![image-20240126175141318](https://s2.loli.net/2024/01/26/IxRY2sl9kfQ3DCZ.png) + +```xml + + + + + IK Analyzer 扩展配置 + + ext.dic + + stopword.dic + + + + + +``` + +这里编写的ext.dic和stopword.dic是文件名,到时候ik会去读取里面的内容 + +```xml + + ext.dic + + stopword.dic +``` + +新建对应的dic文件 + +```sh +touch ext.dic +touch stopword.dic +``` + +在ext.dic和stopword.dic编写想要存入的词汇 + +在stopword.dic中添加了一些英文单词,我们可以通过`cat stopword.dic`进行查看 + +编写完配置文件后,需要重启一下es + +```sh +docker restart es +``` + +回到es的edvtools中,再次进行测试 + +```es +POST /_analyze +{ + "analyzer": "ik_smart", + "text": "天王盖地虎,宝塔镇河妖" +} +``` + +我是在ext.dic中添加了拓展词汇,所以分词后,会得到以下结果,这里我让`analyzer`变为了`ik_smart`,就不会分很多的词汇了 + +```json +{ + "tokens" : [ + { + "token" : "天王盖地虎", + "start_offset" : 0, + "end_offset" : 5, + "type" : "CN_WORD", + "position" : 0 + }, + { + "token" : "宝塔镇河妖", + "start_offset" : 6, + "end_offset" : 11, + "type" : "CN_WORD", + "position" : 1 + } + ] +} +``` + +总结: + +分词器的作用是什么? + +- 创建倒排索引时对文档分词 +- 用户搜索时,对输入的内容分词 + +IK分词器有几种模式? + +- ik_smart:智能切分,粗粒度 +- ik_max_word:最细切分,细粒度 + +IK分词器如何拓展词条?如何停用词条? + +- 利用config目录的IKAnalyzer.cfg.xml文件添加拓展词典和停用词典 +- 在词典中添加拓展词条或停用词条 + +## 索引库操作 + +### mapping属性 + +mappign是对索引库中文档的约束,常见的mapping属性包括: + +- type:字段数据类型,常见的简单类型有: + - 字符串:text(可分词的文本)、keyword(精确值,例如,品牌、国家、ip地址) + - 数值:long、integer、short、byte、double、float + - 布尔:boolean + - 日期:date + - 对象:object +- index:是否创建索引,默认为true +- analyzer:使用哪种分词器 +- properties:该字段的子字段 + +总结: + +mapping常见属性有哪些? + +- type:数据类型 +- index:是否索引 +- analyzer:分词器 +- properties:子字段 + +type常见的有哪些? + +- 字符串:text、keyword +- 数字:long、integer、short、byte、double、float +- 布尔:boolean +- 日期:date +- 对象:object + +### 创建索引库 + +ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下: + +```es +PUT /索引库名称 +{ + "mappings":{ + "properties":{ + "字段名1":{ + "type":"text", + "analyzer":"ik_smart" + }, + "字段名2":{ + "type":"keyword", + index:false + }, + "字段名3":{ + "properties":{ + "子字段":{ + "type":"keyword" + } + } + }, + } + } +} +``` + +这里尝试编写一个索引库 + +```es +# 创建索引库 +PUT /eastwind +{ + "mappings": { + "properties": { + "info":{ + "type": "text", + "analyzer": "ik_smart" + }, + "email":{ + "type": "keyword", + "index":false + }, + "name":{ + "type": "object", + "properties": { + "firstName":{ + "type":"keyword" + }, + "lastName":{ + "type":"keyword" + } + } + } + } + } +} +``` + +### 查询、删除、修改索引库 + +查看索引库语法: + +```es +GET /索引库名 +``` + +删除索引库语法: + +```es +DELETE /索引库名 +``` + +修改索引库 + +索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下: + +```es +PUT /索引库名/_mapping +{ + "properties":{ + "新字段名":{ + "type":"integer" + } + } +} +``` + +示例: + +```es +# 查询索引库 +GET /eastwind + +# 修改索引库,添加新的字段 +PUT /eastwind/_mapping +{ + "properties":{ + "age":{ + "type":"integer" + } + } +} + +# 查询索引库 +GET /eastwind + +# 删除索引库 +DELETE /eastwind + +# 查询索引库 +GET /eastwind +``` + +总结: + +索引库操作有哪些? + +- 创建索引库:PUT /索引库名 +- 查询索引库:GET /索引库名 +- 删除索引库: DELETE/索引库名 +- 添加字段: PUT/索引库名/_mapping + +## 新增、删除、查询文档 + +新增文档的DSL语法如下: + +不写文档id会随机给一个文档id + +```es +POST /索引库名/_doc/文档id +{ + "字段1":"值1", + "字段2":"值2", + "字段3":{ + "子属性1":"值3", + "子属性2":"值4" + } +} +``` + +查看文档的语法: + +```es +GET /索引库名/_doc/文档id +``` + +删除文档的语法: + +```es +DELETE /索引库名/_doc/文档id +``` + +修改文档的语法: + +方式一:全量修改,会删除旧文档,添加新文档 + +```es +PUT /索引库名/_doc/文档id +{ + "字段1":"值1", + "字段2":"值2" +} +``` + +方式二:增量修改,修改指定字段值 + +```es +POST /索引库名/_update/文档id +{ + "doc":{ + "字段名":"值" + } +} +``` + +示例: + +```es +# 新增文档 +POST /eastwind/_doc/1 +{ + "email":"233@163.com", + "info":"test", + "name":{ + "firstName":"name1", + "lastName":"name2" + } +} + +# 查看文档 +GET /eastwind/_doc/1 + +# 全量修改文档,会删除旧文档, +PUT /eastwind/_doc/1 +{ + "email":"667233@163.com", + "info":"test667233", + "name":{ + "firstName":"667233name1", + "lastName":"667233name2" + } +} + +# 增量修改,修改指定文档值 +POST /eastwind/_update/1 +{ + "doc":{ + "name":{ + "firstName" : "667", + "lastName" : "667" + } + } +} + +# 删除文档 +DELETE /eastwind/_doc/1 +``` + +总结: + +文档操作有哪些? + +- 创建文档:POST /索引库/_doc/文档id {json文档} +- 查询文档:GET /索引库名/_doc/文档id +- 删除文档:DELETE /索引库名/_doc/文档id +- 修改文档: + - 全量修改:PUT /索引库名/_doc/文档id {json文档} + - 增量修改:POST /索引库名/_update/文档id {"doc":{字段}} + +**注意**:插入文档时,es会检查文档中的字段是否有mapping,如果没有则按照默认mapping规则来创建索引。如果默认mapping规则不符合你的需求,一定要自己设置字段mapping + +## RestClient操作索引库 + +ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html + +## 索引库demo + +这里通过一个案例来学习如何操作索引库 + +利用JavaRestClient实现创建、删除索引库,判断索引库是否存在 + +根据资料提供的酒店数据创建索引库,索引库名为hotel,mapping属性根据数据库结构定义 + +基本步骤如下: + +1. 导入资料demo +2. 分析数据结构,定义mapping属性 +3. 初始化JavaRestClient +4. 利用JavaRestClient创建索引库 +5. 利用JavaRestClient删除索引库 +6. 利用JavaRestClient判断索引库是否存在 + +新建数据库 + +```sql +CREATE DATABASE heima +``` + +在数据库中运行对应的sql文件 + +在IDEA中打开资料中的hotel-demo + +### 分析数据结构 + +mapping要考虑的问题: + +字段名、数据类型、是否参与搜索、是否分词,如果分词,分词器是什么? + +image-20240127095356039 + +根据上图的表,我们来编写索引库的信息 + +在写之前需要先说明,在ES中支持两种地理坐标数据类型: + +- geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:上述的latitude和longitude +- geo_shape:有多个geo_point组成的复杂几何图形。例如一条直线 + +字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段,例如: + +```es +"all":{ + "type":"text", + "analyzer":"ik_max_word" +}, +"brand":{ + "type":"keyword", + "copy_to":"all" +} +``` + +编写对应的索引库 + +```es +# 编写对应的索引库信息 +PUT /hotel +{ + "mappings": { + "properties": { + "id":{ + "type": "keyword" + }, + "name":{ + "type": "text", + "analyzer": "ik_max_word", + "copy_to": "all" + }, + "address":{ + "type": "text", + "index": false + }, + "price":{ + "type": "integer" + }, + "score":{ + "type": "integer" + }, + "brand":{ + "type": "keyword" + }, + "city":{ + "type": "keyword" + }, + "star_name":{ + "type": "keyword" + }, + "business":{ + "type": "keyword", + "copy_to": "all" + }, + "location":{ + "type": "geo_point" + }, + "pic":{ + "type": "keyword", + "index": false + }, + "all":{ + "type": "text", + "analyzer": "ik_max_word" + } + } + } +} +``` + +这里的location是地址坐标,geo_point说明是以一个点为位置,all是用于将其他的查询参数作为一个整体,比如说business、name,通过all来查询的话,只需要提供business和name的其中之一都可以查询出对应的内容 + +### 初始化RestClient + +引入es的RestHighLevelClient依赖: + +```xml + + org.elasticsearch.client + elasticsearch-rest-high-level-client + 7.12.1 + +``` + +因为SpringBoot默认的ES版本是7.6.2所以我们需要覆盖默认的ES版本 + +```xml + + 1.8 + 7.12.1 + +``` + +初始化client + +```java +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class HotelIndexTest { + + private RestHighLevelClient client; + + @Test + void init(){ + System.out.println(client); + } + + // 创建client + @BeforeEach + void setUp(){ + this.client = new RestHighLevelClient(RestClient.builder( + // ip写自己的,如果需要集群创建就写多个httpHost.create + HttpHost.create("http://192.168.200.129:9200") + )); + } + + + // 结束时,移除client + @AfterEach + void afterAll() throws IOException { + this.client.close(); + } +} +``` + +### 创建索引库 + +```java + @Test + void createHotelIndex() throws IOException { + // 创建Request对象,里面指定了索引库的名称 + CreateIndexRequest hotel = new CreateIndexRequest("hotel"); + // 准备请求的参数:DSL语句,这里指定创建的内容格式为json,由于代码过于臃肿,这里创建常量类放置对应的值 + hotel.source(HotelIndexConstant.MAPPING_TEMPLATE, XContentType.JSON); + // 发送请求,方式为默认 + client.indices().create(hotel, RequestOptions.DEFAULT); + } +``` + +常量类 + +```java +public class HotelIndexConstant { + + public static String MAPPING_TEMPLATE = "{\n" + + " \"mappings\": {\n" + + " \"properties\": {\n" + + " \"info\":{\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_smart\"\n" + + " },\n" + + " \"email\":{\n" + + " \"type\": \"keyword\",\n" + + " \"index\":false\n" + + " },\n" + + " \"name\":{\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"firstName\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"lastName\":{\n" + + " \"type\":\"keyword\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + +} +``` + +运行测试,成功后回到es主页进行查看 + +```es +GET /hotel +``` + +### 删除、判断索引库是否存在 + +```java +// 删除索引库 +@Test +void deleteHotelIndex() throws IOException { + DeleteIndexRequest hotel = new DeleteIndexRequest("hotel"); + client.indices().delete(hotel,RequestOptions.DEFAULT); +} + +// 判断索引库是否存在 +@Test +void decideHotel() throws IOException { + GetIndexRequest hotel = new GetIndexRequest("hotel"); + boolean exists = client.indices().exists(hotel, RequestOptions.DEFAULT); + System.out.println(exists); +} +``` + +总结: + +- 初始化RestHighLevelClient +- 创建xxxIndexRequest。XXX是CREATE、Get、Delete +- 准备DSL(CREATE时需要) +- 发送请求。调用RestHighLevelClient.indices().xxx()方法,xxx是create、exists、delete + +## 文档demo + +去数据库查询酒店数据,导入到hotel索引库,实现酒店数据的crud + +基本步骤如下: + +1. 初始化JavaRestClient +2. 利用JavaRestClient新增酒店数据 +3. 利用JavaRestClient根据id查询酒店数据 +4. 利用JavaRestClient删除酒店数据 +5. 利用JavaRestClient修改酒店数据 + +### 初始化 + +```java +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class HotelTextTest { + private RestHighLevelClient client; + + @Test + void init(){ + System.out.println(client); + } + + // 创建client + @BeforeEach + void setUp(){ + this.client = new RestHighLevelClient(RestClient.builder( + // ip写自己的,如果需要集群创建就写多个httpHost.create + HttpHost.create("http://192.168.200.129:9200") + )); + } + + + // 结束时,移除client + @AfterEach + void afterAll() throws IOException { + this.client.close(); + } + +} +``` + +### 添加 + +```java +@Test +void add() throws IOException { + // 创建Request对象,里面需要索引库的名称和id + IndexRequest hotel = new IndexRequest("hotel").id("1"); + // 准备json数据 + hotel.source( + "{\n" + + " \"email\":\"233@163.com\",\n" + + " \"info\":\"test\",\n" + + " \"name\":{\n" + + " \"firstName\":\"name1\",\n" + + " \"lastName\":\"name2\"\n" + + " }\n" + + "}", XContentType.JSON + ); + // 发送请求 + client.index(hotel, RequestOptions.DEFAULT); +} +``` + +### 根据id查询 + +```java +@Test +void getById() throws IOException { + // 创建request对象,参数1:索引库名称,参数2:文档id + GetRequest hotel = new GetRequest("hotel", "1"); + // 发送请求 + GetResponse res = client.get(hotel, RequestOptions.DEFAULT); + // 转为string类型 + String sourceAsString = res.getSourceAsString(); + System.out.println(sourceAsString); +} +``` + +### 修改文档数据 + +#### 全量更新 + +再次写入id一样的文档,就会删除旧文档,添加新文档,类似于删除之前的添加一个新的,区别不大,这里演示方式二 + +#### 局部更新 + +只更新部分字段 + +```java +@Test +void update() throws IOException { + // 创建request对象 + UpdateRequest hotel = new UpdateRequest("hotel", "1"); + // 准备参数,每2个参数为一对key,value + hotel.doc( + "age",18, + "email","2333" + ); + client.update(hotel,RequestOptions.DEFAULT); +} +``` + +### 删除 + +```java +@Test +void delete() throws IOException { + DeleteRequest hotel = new DeleteRequest("hotel", "1"); + client.delete(hotel,RequestOptions.DEFAULT); +} +``` + +### 批量导入 + +需求:批量查询酒店数据,然后批量导入酒店数据到ES + +思路: + +1. 利用mybatis-plus查询酒店数据 +2. 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc) +3. 利用JavaRestClient的Bulk批处理,实现批量新增文档 + +```java +@Test +void multipleAdd() throws IOException { + // 从数据库中查询对应数据 + List list = hotelService.list(); + BulkRequest request = new BulkRequest(); + // 添加采用indexRequest + for (Hotel hotel : list) { + // 转为HotelDoc并添加 + HotelDoc hotelDoc = new HotelDoc(hotel); + // 添加到请求 + request.add(new IndexRequest("hotel") + .id(hotelDoc.getId().toString()) + .source(JSON.toJSONString(hotelDoc))); + } + client.bulk(request,RequestOptions.DEFAULT); +} +``` + +### 总结 + +文档操作的基本步骤: + +- 初始化RestHighLevelClient +- 创建xxxRequest。xxx是Index、Get、Update、Delete +- 准备参数(Index和Update时需要) +- 发送请求。调用RestHighLevelClient.xxx(),xxx是index、get、update、delete +- 解析结果(GET需要) + diff --git a/public/markdowns/SpringSecurity.md b/public/markdowns/SpringSecurity.md new file mode 100644 index 0000000..14fad35 --- /dev/null +++ b/public/markdowns/SpringSecurity.md @@ -0,0 +1,228 @@ +--- +title: SpringSecurity +abbrlink: 7d2eec83 +date: 2023-10-26 22:38:08 +tags: +categories: + - 安全 +description: SpringSecurity +--- + +# SpringSecurity简介 + +Spring是一个很棒的Java应用开发框架,Spring Security 基于Spring框架,提供了一套Web应用安全性的完整解决方案,一般来说,web应用的安全性包括**用户认证(Authenticaiton)和用户授权(Authorization)**两个部分 + +- 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说,该用户能否访问该系统,用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程 +- 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限 + +在用户认证方面,Spring Security框架支持主流的认证方式,包括Http基本认证、Http表单验证、Http摘要认证、OpenID和LDAP等。在用户授权方面。Spring Security提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制 + +Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制 + +核心思想是通过一系列的filter chain来进行拦截过滤,对用户的访问权限进行控制 + +spring security的核心功能主要包括: + +- 认证(你是谁) +- 授权(你能干什么) +- 攻击防护(防止伪造身份) + +其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是Basic Authentication Filter用来认证用户的身份,一个在spring security 中一种过滤器处理一种认证方式 + +image-20231219131022716 + +比如,对于username password认证过滤器来说, + +``` +会检查是否是一个登录请求; +是否包含username和password(也就是该过滤器需要的一些认证信息); +如果不满足则放行给下一个; +下一个按照自身的职责判定是否是自身需要的信息,basic的特征就是在请求头中有Authorization:Basic eHh4Onh4的信息。中间可能还有更多的认证过滤器。最后一环是FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因),Exception Translation Filter会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示 +从Exception Translation Filter到最后的过滤器都无法控制,其他的可以进行配置是否生效 +``` + + + +# 认证和授权 + + 一般来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开 + +即: + +- 首先需要确认用户身份 +- 再确认用户是否有访问指定资源的权限 + +认证这块的解决方案有很多,主流的有`CAS`、`SAML2`、`OAUTH2`等,常说的单点登录方案(SSO)就是这块授权的话,主流一般是spring security和shiro + +shiro比较轻量级,相比较而言spring security 架构比较复杂 + +## OAuth2 + +OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段认证用户身份 + +并颁发token(令牌),使得第三方应用可以使用该令牌在**限定时间、限定范围**访问**指定资源** + +主要涉及RFC规范有【`RFC6748`(整体授权框架)】、【`RFC6750`(令牌使用)】、【`RFC6819`(威胁模型)】这几个,一般需要了解的是`RFC6749` + +获取令牌的方式主要有四种,分别是`授权码模式`、`简单模式`、`密码模式`、`客户端模型` + +总之:OAuth2是一个授权(Authorization)协议。 + +认证(Authentication)证明你是不是这个人,而授权(Authoration)则是证明这个人有没有访问这个资源(Resource)的权限。 + +### OAuth2的抽象流程 + + +--------+ +---------------+ + | |--(A)- Authorization Request ->| Resource | + | | | Owner | + | |<-(B)-- Authorization Grant ---| | + | | +---------------+ + | | + | | +---------------+ + | |--(C)-- Authorization Grant -->| Authorization | + | Client | | Server | + | |<-(D)----- Access Token -------| | + | | +---------------+ + | | + | | +---------------+ + | |--(E)----- Access Token ------>| Resource | + | | | Server | + | |<-(F)--- Protected Resource ---| | + +--------+ +---------------+ + +上面的图是一张来自OAuth2的抽象流程图 + +Client:客户端应用程序(Application) + +Authorization Server:授权服务器 + +Resource Server:资源服务器 + +解释上图的大致流程: + +- 用户连接客户端应用程序后,客户端应用程序(Client)要求用户给予授权 +- 用户同意给予客户端应用程序授权 +- 客户端应用程序使用上一步获得的授权(Grant),向授权服务器申请令牌 +- 授权服务器对客户端应用程序的授权(Grant)进行验证后,确认无误,发放令牌 +- 客户端应用程序使用令牌,向资源服务器申请获取资源 +- 资源服务器确认令牌无误,同意向客户端应用程序开放资源 + +其实流程无非如下,用户连接-->客户端-->客户端要求用户给出授权-->客户端拿授权申请令牌-->授权服务器校验授权无误后发放令牌-->客户端拿着令牌,找资源服务器要资源-->资源服务器校验令牌无误后发放资源 + +从上面的流程可以看出,如何获取**授权(Grant)**才是关键。拥有正确的授权(Authorzation)就可以去拿到任意的东西了 + +### OAuth2的4种授权类型 + +#### Authorization Code(授权码模式) + +功能最完整、流程最严密的授权模式。通过第三方应用程序服务器与认证服务器进行互动。广泛用于各种第三方认证。 + + +----------+ + | Resource | + | Owner | + | | + +----------+ + ^ + | + (B) + +----|-----+ Client Identifier +---------------+ + | -+----(A)-- & Redirection URI ---->| | + | User- | | Authorization | + | Agent -+----(B)-- User authenticates --->| Server | + | | | | + | -+----(C)-- Authorization Code ---<| | + +-|----|---+ +---------------+ + | | ^ v + (A) (C) | | + | | | | + ^ v | | + +---------+ | | + | |>---(D)-- Authorization Code ---------' | + | Client | & Redirection URI | + | | | + | |<---(E)----- Access Token -------------------' + +---------+ (w/ Optional Refresh Token) + Note: The lines illustrating steps (A), (B), and (C) are broken into + two parts as they pass through the user-agent. + +- 用户(Resource Owner)通过用户代理(User-Agent)访问客户端(Client),客户端索要授权,并将用户导向认证服务器(Authorization Server) +- 用户选择是否给予客户端授权 +- 假设用户给予授权,认证服务器将用户导向客户端事先指定的**重定向URI**,同时附上一个授权码 +- 客户端收到授权码,附上早先的**重定向URI**,向认证服务器申请令牌。这一步是在客户端的后台服务器上完成的,对用户不可见 +- 认证服务器核对授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。这一步对用户也不可见 + +#### Implicit(简化模式) + +不通过第三方应用程序服务器,直接在浏览器中向认证服务器申请令牌,更适用于移动端的App及没有服务器端的第三方单页面应用。 + +##### Resource Owner Password(密码模式) + +用户向客户端服务器提供自己的用户名和密码,用户对客户端高度信任的情况下使用,比如公司、组织的内部系统,SSO + + +----------+ + | Resource | + | Owner | + | | + +----------+ + v + | Resource Owner + (A) Password Credentials + | + v + +---------+ +---------------+ + | |>--(B)---- Resource Owner ------->| | + | | Password Credentials | Authorization | + | Client | | Server | + | |<--(C)---- Access Token ---------<| | + | | (w/ Optional Refresh Token) | | + +---------+ +---------------+ + + Figure 5: Resource Owner Password Credentials Flow + +- 用户(Resource Owner资源持有者)向客户端(Client)提供用户名和密码 +- 客户端将用户名和密码发给认证服务器(Authorization Server),向后者请求令牌 +- 认证服务器确认无误后,向客户端提供访问令牌 + +#### Client Credentials(客户端模式) + +客户端服务器以自己的名义,而不是以用户的名义,向认证服务器进行认证 + +## 令牌刷新 + +``` + +--------+ +---------------+ + | |--(A)------- Authorization Grant --------->| | + | | | | + | |<-(B)----------- Access Token -------------| | + | | & Refresh Token | | + | | | | + | | +----------+ | | + | |--(C)---- Access Token ---->| | | | + | | | | | | + | |<-(D)- Protected Resource --| Resource | | Authorization | + | Client | | Server | | Server | + | |--(E)---- Access Token ---->| | | | + | | | | | | + | |<-(F)- Invalid Token Error -| | | | + | | +----------+ | | + | | | | + | |--(G)----------- Refresh Token ----------->| | + | | | | + | |<-(H)----------- Access Token -------------| | + +--------+ & Optional Refresh Token +---------------+ +``` + +- 客户端找授权服务器索要授权 + +- 当用户同意给予授权后,授权服务器给予令牌,并给予刷新令牌 + +- 此时通过令牌去资源服务器中获取资源 + +- 得到资源 + + 假设token过期 + +- 再次获取资源,发现Token无效了 +- 通过刷新令牌去授权服务器中获取Token +- 通过Token再次获取令牌和刷新令牌,重复流程 + diff --git a/public/markdowns/Vue.md b/public/markdowns/Vue.md new file mode 100644 index 0000000..74b7656 --- /dev/null +++ b/public/markdowns/Vue.md @@ -0,0 +1,11821 @@ +--- +title: Vue2 +tags: + - Vue2 +categories: + - 前端 +description: Vue2 +abbrlink: 4d45ebbb +--- +# Vue核心 + +## 搭建Vue环境 + +先在此处附上Vue的安装教程地址:https://v2.cn.vuejs.org/v2/guide/installation.html + +这里有两个版本,一个开发版本,一个生产版本 + +开发版本是在开发时使用的,当出现问题的时候会在控制台报警告 + +生产版本是在项目上线时使用的,不会有警告,而且体积更小 + +![image-20230919083405727](https://s2.loli.net/2023/09/19/xHtPB7nUyQM3VSF.png) + +在这里,我使用的是开发版本,学习一般使用开发版 + +将开发版本下载后保存到本地,使用方法与jquery是一致的,使用来script引入 + +### 直接用script引入 + +创建一个文件夹,在里面创建一个html文件 + +然后直接引入vue文件即可 + +```html + + + + + + + + Document + + + + + + + + +``` + +运行文件 + +### 消除开发环境提示 + +在console里查看 + +![image-20230919084602099](https://s2.loli.net/2023/09/19/9Jxbg6RQjyc3O7o.png) + +第一个是说下载vue的开发者工具来达到一个更好的开发者体验 + +第二个是说你正在运行开发环境,请你确信在生产环境不要这样做 + +两个小提示,但是不影响,后面再解决 + +如果成功引入了Vue,在console上写入Vue会输出其对应的函数 + +![image-20230919084937292](https://s2.loli.net/2023/09/19/nKf9Jtj4aDRyeiF.png) + +解决第一个提示的方法是下载一个vue的开发者工具即可 + +vue开发者工具的github链接如下:https://github.com/vuejs/devtools#vue-devtools + +找到下面这个地方,点击进入谷歌商店下载 + +![image-20230919085712128](https://s2.loli.net/2023/09/19/3nEbhJ4U2wc6siS.png) + +![image-20230919085756365](https://s2.loli.net/2023/09/19/7fbN283qnAHY1hv.png) + +第二个提示需要在代码中对其进行配置 + +```vue + +``` + +## 模板语法 + +Vue模板语法有2大类: + +1. 插值语法: + + ​ 功能:用于解析标签体内容 + + ​ 写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性 + +2. 指令语法: + + ​ 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件等) + + ​ 举例:v-bind:hreft="xxx"或简写为:href="xxx",xxx同样要写js表达式。且可以直接读取到data中的所有属性 + + ​ 备注:Vue中有很多的指令,且形式都是:v-????? + +### 插值语法 + +```html + + + + + + + + Document + + + + +
+ {{test}} +
+ + + + + +``` + +### 指令语法 + +#### v-bind + +单向绑定数据 + +```html + + + + + +``` + +可以简写为`跳转到百度` + +```html + + + + + +``` + + + +## 数据绑定 + +v-bind:单向数据绑定 + +v-model:双向数据绑定 + +Vue中有2种数据绑定的方式: + +1. 单向绑定(v-bind):数据只能从data流向页面 + +2. 双向绑定(v-model):数据不仅能从data流向页面,还可以从页面流向data + + 备注: + + 1. 双向绑定一般都应用在表单类元素上(如:input、select等) + 2. v-model:value 可以简写为v-model,因为v-model默认收集的就是value值 + + + +- el有两种写法 + + 1. new Vue时配置el属性 + + ```vue + new Vue({ + // el: '#box', 第一种写法 + }) + ``` + + 2. 先创建Vue实例,随后再通过vm.$mount('#root')指定el的值 + + `vm.$mount('#box')` + +- data有2种写法 + + 1. 对象式 + + ```html + new Vue({ + // el: '#box', 第一种写法 + }) + ``` + + 2. 函数式 + + ```html + // data的第二种写法:函数式 + data() { + return { + name: 'zhangsan' + } + } + ``` + +- 一个重要的原则 + + - 由Vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了 + +v-bind的简写方式::xxx="" + +v-model的简写方式:v-model="" + +```html +单向数据绑定:
+双向数据绑定: +``` + +### 单向数据绑定 + +下面这段代码是对其进行了一个单向数据绑定,可以在页面中进行测试 + +```html + +
+ 单向数据绑定: +
+ + + +``` + +之前在网上下载的Vue开发者工具此时就可以使用了 + +打开页面后单击F12,打开这个页面,在最后一栏找到Vue + +image-20230919161313748 + +在name:'zhangsan'的位置进行修改,单向数据绑定处的数据也会发生改变,而在输入框中修改,并不会改变name:'zhangsan'的内容 + +### 双向数据绑定 + +v-model:双向数据绑定 + +```html + +
+ 单向数据绑定:
+ 双向数据绑定: +
+ + + +``` + +被绑定了双向后,在测试工具中输入任意的数据,输入框中的内容都会发生改变,或者在输入框中输入内容,也会互相的改变 + +当输入框中的内容改变时,会带起一系列的连锁反应 + +image-20230919161959667 + +**注意:v-model元素只能应用在表单类元素上(输入类元素),在其他标签类元素上使用会报错** + +## el与data的两种写法 + +对象式 + +```html + +
+ 单向数据绑定:
+ 双向数据绑定: +
+ + + +``` + +相较之下,第二种更灵活,使用的时候两种都可以 + +```html + +
+ 单向数据绑定:
+ 双向数据绑定: +
+ + + +``` + +data可以简写为下面的方式 + +```vue + // data的第二种写法:函数式 + data() { + return { + name: 'zhangsan' + } + } +``` + +## MVVM + +M:模型(Model),对应data中的数据 + +V:视图(View), 模板 + +VM:视图模型(ViewModel),Vue实例对象 + +image-20230920124821781 + +Data Bindings:数据绑定,将Model中的数据绑定到View中 + +DOM Listeners:页面模型监听器 + + + +总结: + +- data中所有的属性,最后都出现了Vue对象上 +- vm身上所有的属性及Vue原型上所有属性,在Vue模板中都可以直接使用 + + + +## 数据代理 + +### Object.defineProperty方法 + +`Object.defineProperty(添加属性的对象名,添加的属性名,{value:添加的值})` + +```html + + + + +``` + +为person对象添加了一个age的属性,值为18 + +![image-20230920132903453](https://s2.loli.net/2023/09/20/NwdsXQ17HbplYai.png) + +这样添加与普通的添加方式有什么区别呢,这样添加的属性是不参与遍历的 + +正常情况下是参与遍历的 + +```html + + + + +``` + +![image-20230920133350604](https://s2.loli.net/2023/09/20/XYSf5Juzpmgkrln.png) + +但是使用该方法进行遍历就遍历不到了 + +```html + + + + +``` + +![image-20230920133321585](https://s2.loli.net/2023/09/20/bz821RoGefdhsv7.png) + +这种情况也叫不可枚举,如果需要遍历该怎么办呢 + +可以在代码中加入 + +`enumerable: true`:控制属性是否可以被枚举,默认是false + +```html + + + + +``` + +再次运行,就可以遍历到了 + +如果你试图修改`Object.defineProperty`添加的方法,可以修改,但不会修改成功 + +如果需要被修改怎么办呢 + +在代码中加入`writable: true`,控制属性是否可以被修改,默认是false + +```html + + + + +``` + +数据需要被删除,可以加入`configurable: true`,控制属性是否可以被删除,默认是false + +#### getter和setter + +下面的代码提供了`Object.defineProperty`的get和set示例 + +```html + + + + +``` + +image-20230920135946414 + + + +### Vue中的数据代理 + +数据代理就是让_data中的数据在Vue身上也有一份,不管是getter还是setter都可以直接通过属性名来获取使用,而不是通过 _data.属性名来使用,这样会非常的冗余,在getter时,数据代理会getter到 _data的身上,而setter时,会setter到 _data的身上,从而达到一个连锁的效果,简化了开发 + +数据代理图示: + +image-20230921125656317 + +Vue先将data中的数据存放到_data中,然后再通过`Object.defineProperty`方法将数据放到vm身上,这样就起到一个数据代理的效果 + +```html + +
+

{{name}}

+

{{age}}

+
+ + + +``` + +以上面这段示例代码为例,Vue中的数据代理其实也是同理的 + +当有人访问页面中的name或age时,它会利用getter去访问data中的name或age + +当有人修改页面中的name或age时,它会利用setter去修改data中的name或age + +此时就不是直接访问,而是以一种代理的形式,通过访问Vue上的getter或setter方法来达到修改data上的数据 + + + +这里对数据代理的两种形式做一个验证 + +修改data中的属性后,查看其中vue中的data是否发生变化 + +image-20230921091925137 + +发现此时这里的name确实是发生改变了,这说明数据改变后,依然是从data中来获取的 + + + +对setter进行一个验证 + +修改对应的属性,然后获取其data中的属性是否发生了改变 + +![image-20230921093045894](https://s2.loli.net/2023/09/21/k4B1dSHMEINqOw3.png) + +这里我们发现,修改了它的属性后,data中的属性也发生了改变,这说明setter方法是存在数据代理的,设置完的属性会放到data中 + + + +总结: + +1. Vue中的数据代理: + + ​ 通过vm对象来代理data对象中属性的操作(读/写) + +2. Vue中数据代理的好处: + + ​ 更加方便的操作data中的数据 + +3. 基本原理 + + ​ 通过Object.defineProperty()把data对象中所有属性添加到vm上 + + ​ 通过每一个添加到vm上的属性,都指定一个getter/setter + + ​ 在getter/setter内部去操作(读/写)data中对应的属性 + + + +## 事件处理 + +### 事件的基本使用 + +事件的基本使用: + +1. 使用v-on:xxx 或 @xxx绑定事件,其中xxx是事件名 +2. 事件的回调需要配置在methods对象中,最终会在vm上 +3. methods配置的函数,不要用箭头函数,否则this就不是vm了 +4. methods中配置的函数,都是被Vue所管理的函数,this的指向是vm或组件实例对象 +5. @click="demo" 和 @click="demo($event)" 效果一致,但后者可以传参 + +#### v-on:click + +**简写形式为:@click** + +当被点击时 + +```html + +
+ +
+ + + +``` + + + +如果需要接收参数,可以这样写 + +```html + +
+ +
+ + + +``` + +但是这样写event就无法使用了 + +可以通过在传参位置加入占位符`$event`的形式传递event参数 + +```html + +
+ +
+ + + +``` + +### 事件修饰符 + +正常的运行下面这段代码,会在代码运行后依然将网页跳转到了百度 + +```html + + + + + +``` + +跳转是a标签的默认行为,我们有什么好的办法来阻止这个默认行为呢 + +可以通过`e.preventDefault`方法对这个默认行为进行阻止 + +```html +methods: { + + jump(e) { + + e.preventDefault(); + + alert("跳走喽") + + } + + }, +``` + +也可以通过Vue中的事件修饰符 + +1. **prevent:阻止默认事件(常用)** +2. **stop:阻止事件冒泡(常用)** +3. **once:事件只触发一次(常用)** +4. capture:使用事件的捕获模式 +5. self:只有event.target是当前操作的元素时才触发事件 +6. passive:事件的默认行为立即执行,无需等待事件回调执行完毕 + +演示一下常用的三种,其余不做演示 + +#### prevent: + +**效果和`e.preventDefault();`是一样的** + +```html + + + + + +``` + +#### stop: + +冒泡是从当没有返回值true或false之类时,会从里向外(向它的父元素执行其父元素的方法直到结束) + +冒泡情况演示 + +```html + +
+
+ +
+
+ + + +``` + +冒泡解决方案: + +```html + +
+
+ +
+
+ + + +``` + +冒泡解决方案Vue版: + +```html + + +
+
+ +
+
+ + + +``` + +#### once: + +事件只触发一次,字面意思,挺好理解的 + +```html + +
+ +
+ + + +``` + + + +### 键盘事件 + +@keyup:当键盘弹起时 + +@keydown:当键盘按下时 + +下面这段代码是当键盘弹起时,如果按下的是回车就打印内容在控制台上 + +```html + +
+ +
+ + + +``` + +这里的判断keyCode也可以置换为vue中的别名 + +```html + +
+ +
+ + + +``` + +#### vue常见按键别名 + +回车 => enter + +删除 => delete(捕获"删除"和"退格"键) + +退出 => esc + +空格 => space + +换行 => tab + +上 => up + +下 => down + +左 => left + +右 => right + + + +Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名) + +短横线命名是什么意思呢? + +就是说,如果某个按键未提供别名,可以使用原始的key去绑定,比如CapsLock,如果想要使用的话,需要转为caps-lock,记得要在两个单词之间加入短横线- + +示例代码 + +```html + +
+ +
+ + + +``` + + + +特殊按键tab:按了之后会离开焦点,这种不适合在keyup上使用,因为在键盘弹起后触发时,焦点已经离开了,方法可能就无法触发了(必须配合keydown使用) + + + +系统修饰键(用法特殊):ctrl、alt、shift、meta(win) + +1. ​ 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发 +2. 配合keydown使用:正常触发事件 + + + +也可以使用keyCode去指定具体的按键(不推荐) + +`` + + + +Vue.config.keyCodes.自定义键名 = 键码,可以定制按键别名 + +`Vue.config.keyCodes.huiche = 13` + +```html + +
+ +
+ + + +``` + + + +### 事件总结 + +- 事件修饰符可以连着写,例如@click.prevent.stop="xxx",效果是停止默认事件并阻止冒泡,但它是有先后顺序的,也就是说,先停止默认事件,再阻止冒泡,谁写在前面谁先 +- 而键盘事件也可以连着写,例如,`@keyup.ctrl.y`,连着写之后的效果就是按下特定的这两位才会触发效果,其他的没效果 + + + +## 计算属性 + +### 姓名案例 + +插值语法实现 + +```html + +
+ 姓:
+ 名:
+ 姓名: {{firstName.slice(0,3)}}-{{lastName}} +
+ + + +``` + +如果我们想为名称加一些新的效果,但是会有很多,这样就会很麻烦,如果我们把它整理成一个methods,看起来就不会特别的冗余了 + +```html + +
+ 姓:
+ 名:
+ 姓名: {{name()}} +
+ + + +``` + +### 计算属性 + +1. 定义:要用的属性不存在,要通过**已有属性(vue中的才叫属性)**计算得来 +2. 原理:底层借助了Object.defineproperty方法提供的getter和setter +3. get函数什么时候执行? + - 初次读取时会执行一次 + - 当依赖的数据发生改变时会被再次调用 +4. 优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便 +5. 备注: + 1. 计算属性最终会出现在vm上,直接读取使用即可 + 2. 如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时**依赖的数据发生改变**,否则无效 + +使用计算属性编写之前的姓名案例 + + 这里的get和`Object.defineProperty`是一样的 + +当有人读取fullName时,get就会被调用,且返回值就作为fullName的值 + +get什么时候调用 + +- 初次读取fullName时 +- 所依赖的数据发生改变时 + +```html + +
+ 姓:
+ 名:
+ 姓名: {{fullName}} +
+ + + +``` + +且get是有缓存的,而methods是没有缓存的,性能上computed更好一些 + +有get肯定就有set,和get的道理其实也是一样的 + +```html + +
+ 姓:
+ 名:
+ 姓名: {{fullName}} +
+ + + +``` + +### 计算属性简写 + +简写的形式只有在**只读不改**的时候才能使用 + +```html + +
+ 姓:
+ 名:
+ 姓名: {{fullName}} +
+ + + +``` + + + +## 监视属性 + +### 天气案例 + +先做一个天气的小案例,点击按钮后变换天气 + +这里使用了计算属性来进行操作 + +```html + +
+

今天天气很{{info}}

+ +
+ + + +``` + +但是在这里有一个小坑 + +如果我们将代码中的这一行改变了 + +改变为`

今天天气很一般

` + +此时页面上就不会用到isHot和info了,如果我们此时点击一下按钮,页面是肯定不会发生变化的,但是你打开开发者工具一看 + +image-20230922140923321 + +开发者工具中却是true和炎热,我们去控制台上看一下 + +image-20230922140756964 + +控制台上就发生了变化了 + +这是为什么呢,原因其实是vue的开发者工具认为你页面没有使用到该数据,就没有为你更新了,这个是官方的一个bug,不影响 + +这里写的代码其实很多,只是为了实现一个切换功能,那还有什么办法能更简单吗 + +```html + +
+

今天天气很{{info}}

+ +
+ + + +``` + +一些简单的代码其实可以直接使用@click来写 + +如果你不仅想做这个操作,还想做别的操作呢,有两种方式 + +1. 直接在methods中写 +2. `@click="isHot = !isHot";你想做的操作` + +比如: + +```html + +
+

今天天气很{{info}}

+ +
+ + + +``` + + + +### 监视属性watch + +1. 当被监视的属性变化时,回调函数自动调用,进行相关操作 +2. 监视的属性必须存在,才能进行监视 +3. 监视的两种写法: + 1. new Vue时传入watch配置 + 2. 通过vm.$watch监视 + +```html + +
+

今天天气很{{info}}

+ +
+ + + +``` + +监视属性是watch,是一个对象的形式,里面可以放置多个对象,每个对象里面有配置属性和handler等 + +handler中有两个参数可以接收,第一个是newValue,第二个是oldValue + +除了上面这种监视属性的写法,还有另一种监视属性的写法 + +```html + +``` + + + +### 深度监视 + +1. Vue中的watch**默认不监测**对象内部值的改变(一层) +2. 配置`deep:true`可以监测对象内部值的改变(多层) + +备注: + +- **Vue自身可以**监测对象内部值的改变,但Vue提供的watch**默认不可以** +- 使用watch时根据数据的具体结构,决定是否采用深度监视,采用深度监视会有效率的问题 + +#### 监视多级结构中某个属性的变化 + +当多级结构中的a发生变化时,可以获取得到它 + +```html + +
+

a的值是:{{numbers.a}}

+ +
+ + + +``` + +#### 检测整体结构变化 + +```html + +
+

a的值是:{{numbers.a}}

+ + + + +
+ + + +``` + + + +#### 检测整体结构的任意一个值的变化 + +为watch中的属性配置deep即可检测值的变化 + +```html + +
+

a的值是:{{numbers.a}}

+ + + + +
+ + + +``` + + + +### 监视的简写形式 + +监视的简写形式只有在仅handler的情况下才可以使用 + +```html + +
+

numbers的值是:{{numbers}}

+ +
+ + + +``` + +但是监视有两种写法,它还有一种vm.$watch的写法,也可以简写 + +```html + +
+

numbers的值是:{{numbers}}

+ +
+ + + +``` + + + +### watch对比computed + +computed和watch之间的区别: + +1. computed能完成的功能,watch都可以完成 +2. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作(定时器) + +两个重要的小原则: + +1. 被Vue所管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象 +2. 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等),最好写成箭头函数,因为在JavaScript中,箭头函数并不会创建自己的this上下文,而是继承其所在代码块的this。因此,在Vue组件的方法中,如果我们使用了箭头函数,那么this才会指向Vue实例。 + +watch写法的姓名案例 + +```html + +
+ 姓: + 名: +
姓名:{{fullName}} +
+ + + +``` + +computed写法的姓名案例 + +```html + +
+ 姓: + 名: +
姓名:{{fullName}} +
+ + + +``` + + + +## 绑定样式 + +### 绑定class样式 + +字符串写法,适用于:样式的类名不确定,需要动态指定 + +可以将样式修改为指定的内容 + +```html + +
+ +
test
+
+ + + +``` + +也可以通过布尔值进行判断之类的情况 + +当isActive为true时,active才会作为类生效 + +```html + +
+
test
+
+ + + +``` + +绑定计算属性作为class的判断 + +```html + +
+
test
+
+ + + +``` + +这段代码的释义是使用了一个计算属性 + +计算属性的含义是classObject,当满足isActive为true并且error不为空的条件时,返回active;当error存在并且error的类型为fatal时返回text-danger + +### 数组语法 + +我们可以把一个数组传给v-bind:class,来应用一个class列表的情况 + +```html + +
+
test
+
+ + + +``` + +最终它会渲染为`class='activeClass isYes'` + +如果想根据条件来切换数组语法中的内容,也是可以的 + +```html +
test
+``` + +可以通过三元表达式来对其进行修改 + +如果觉得三元表达式不够友好,可以采取对象语法的方式 + +```html +
test
+``` + + + +### 绑定内联样式 + +#### 对象语法 + +```html + +
+ +
hahahha
+
+ + + +``` + +或者将样式绑定到一个对象中,直接使用对象 + +```html +
hahahha
+``` + +```js +data: { + styleData: { + backgroundColor: 'red', + fontSize: '30px' + } + } +``` + +#### 数组语法 + +将多个样式对象绑定到一个数组上 + +```html +
hahahha
+``` + +#### 多重值 + +从vue2.3.0开始可以为style绑定中的property提供一个包含多个值的数组,常用于提供多个带前缀的值,例如: + +```html +
+``` + +`'-webkit-box,'-ms-flexbox','flex'`是不同浏览器支持的类型前缀,这样写只会渲染数组中最后一个被浏览器支持的值,如果浏览器支持不带浏览器前缀的flexbox,那么就只会渲染`display:flex` + + + +## 条件渲染 + +### v-if、v-else + +`v-if` 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染。 + +`v-else`的效果是当if不生效时,else生效 + +`v-else` 元素必须紧跟在带 `v-if` 或者 `v-else-if` 的元素的后面,否则它将不会被识别。 + +```html + +
+
66
+
77
+ +
+ + + +``` + +### 在`