\\n </div>\\n <div class=\\"new-img\\">\\n <img style=\\"max-width: 400px;max-height: 500px;\\" :src=\\"newImg\\" alt=\\"\\"/>\\n </div>\\n </div>\\n </div>\\n</template>\\n\\n<script setup>\\nimport { ref } from \'vue\';\\nconst oldImg = ref(\'\');\\nconst newImg = ref(\'\');\\n\\nconst handleChange = info => {\\n const file = info.file;\\n\\n // 使用 FileReader 进行本地文件预览(无论上传是否成功)\\n const reader = new FileReader();\\n reader.onload = () => {\\n oldImg.value = reader.result; // 将本地文件的 Base64 赋值给 oldImg\\n };\\n reader.readAsDataURL(file.originFileObj); // 读取原始文件对象\\n\\n // 原有上传完成逻辑可保留用于处理服务器返回结果\\n if (file.status === \'done\' && file.response) {\\n console.log(file)\\n newImg.value = file.response.url; // 如果上传成功,使用服务器返回的 URL\\n }\\n};\\n\\nconst fileList = ref([]);\\n</script>\\n\\n<style scoped>\\n.diff-wrap {\\n width: 800px;\\n margin: 20px auto;\\n border: 1px solid #ddd;\\n display: flex;\\n}\\n\\n.old-img {\\n flex: 1;\\n height: 500px;\\n border-right: 1px solid #ddd;\\n}\\n\\n.new-img {\\n flex: 1;\\n height: 500px;\\n}\\n</style>\\n\\n
使用 Node.js 的图像处理库 sharp 进行格式转换,安装 sharp。
\\nnpm install sharp\\n
\\n示例代码
\\nconst { Service } = require(\'egg\');\\nconst fs = require(\'fs\');\\nconst path = require(\'path\');\\nconst sharp = require(\'sharp\');\\n\\nclass HomeService extends Service {\\n async index() {\\n return \'hello world\';\\n }\\n\\n async uploadImg() {\\n const { ctx } = this;\\n\\n try {\\n // 1. 获取上传的文件流\\n const stream = await ctx.getFileStream();\\n\\n // 2. 检查是否为支持的图片格式(可选)\\n const allowedMimes = [ \'image/jpeg\', \'image/png\', \'image/gif\', \'image/webp\' ];\\n if (!allowedMimes.includes(stream.mime)) {\\n throw new Error(\'Unsupported image format\');\\n }\\n\\n // 3. 定义路径\\n const tempInputPath = path.join(this.config.baseDir, \'app/public\', `temp_${Date.now()}.tmp`);\\n const outputFilename = `converted_${Date.now()}.webp`;\\n const outputFilePath = path.join(this.config.baseDir, \'app/public\', outputFilename);\\n\\n // 4. 写入临时原始文件\\n const writeStream = fs.createWriteStream(tempInputPath);\\n await new Promise((resolve, reject) => {\\n stream.pipe(writeStream);\\n stream.on(\'end\', resolve);\\n stream.on(\'error\', reject);\\n });\\n\\n // 5. 使用 sharp 转换为 webp\\n await sharp(tempInputPath)\\n .webp({ quality: 80 }) // 可设置压缩质量\\n .toFile(outputFilePath);\\n\\n // 6. 清理临时文件\\n fs.unlinkSync(tempInputPath);\\n\\n // 7. 返回 WebP 图片地址\\n return {\\n url: `/public/${outputFilename}`,\\n filename: outputFilename,\\n };\\n } catch (err) {\\n ctx.logger.error(\'Image upload or conversion failed:\', err);\\n throw new Error(\'Image processing failed: \' + err.message);\\n }\\n }\\n}\\n\\nmodule.exports = HomeService;\\n
","description":"随着互联网的发展,图片作为最直观的内容展示方式逐渐在系统中占用越来越多的版面,但是随之而来的就是系统性能的大幅度下滑。传统的JPEG、PNG、GIF各有优点,也各有弊端,“大一统”的图片格式被需要,于是WebP诞生了。 需求\\n\\nWebP格式文件产生的原因主要是源于对网络图像传输效率的需求以及现有图像格式在某些方面的局限性。\\n\\n在现代互联网网页中图片和视频占据了很大比例。为了提供更吸引人的用户体验,网站需要加载大量的高质量图像。\\n\\n同时智能手机和平板电脑的普及推动了移动互联网的快速发展。在移动设备上,网络速度通常比桌面端慢,且用户的流量是有限的。\\n\\n而JPEG…","guid":"https://juejin.cn/post/7503017777064362010","author":"李剑一","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-12T04:48:35.080Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80557810e29c4a33af8a5e1b74b7ff77~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5YmR5LiA:q75.awebp?rk3s=f64ab15b&x-expires=1747630114&x-signature=juZcn9h6tepj3%2FwE3%2FCmdg4gu4M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15b28df937b945dba51adbd0434f0d2d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5YmR5LiA:q75.awebp?rk3s=f64ab15b&x-expires=1747630114&x-signature=dDKJjz0oTdzaFuc5ToiBW8K0FOQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9cd2a9294dd431b9628cc9f0edc7e8b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5YmR5LiA:q75.awebp?rk3s=f64ab15b&x-expires=1747630114&x-signature=wR5PsTH59AVMNztdlZRrLvXNfbU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","前端","JavaScript"],"attachments":null,"extra":null,"language":null},{"title":"FastMCP(python)和 SolonMCP(java)的体验比较(不能说一样,但真的很像)","url":"https://juejin.cn/post/7502773930349559849","content":"从 MCP SDK 的发展史上看,FastMCP 是前辈,SolonMCP 则是后辈。mcp-python-sdk 功能完善,已经很成熟了。而 mcp-java-sdk 却还不完善,比如:
\\n两者的体验不能说是一样,但真的很像。
\\n计算器工具
\\n@mcp.tool()\\ndef add(a: int, b: int) -> int:\\n \\"\\"\\"将两个数字相加\\"\\"\\"\\n return a + b\\n \\n@mcp.tool()\\ndef subtract(a: int, b: int) -> int:\\n \\"\\"\\"从第一个数中减去第二个数\\"\\"\\"\\n return a - b\\n \\n@mcp.tool()\\ndef multiply(a: int, b: int) -> int:\\n \\"\\"\\"将两个数相乘\\"\\"\\"\\n return a * b\\n \\n@mcp.tool()\\ndef divide(a: float, b: float) -> float:\\n \\"\\"\\"将第一个数除以第二个数\\"\\"\\"\\n if b == 0:\\n raise ValueError(\\"除数不能为零\\")\\n return a / b\\n \\nif __name__ == \\"__main__\\":\\n # 使用stdio传输方式启动服务器\\n mcp.run(transport=\\"stdio\\")\\n
\\n天气工具(有工具,资源,资源模板示例)
\\n@mcp.tool()\\ndef get_weather(city: str) -> dict:\\n \\"\\"\\"获取指定城市的当前天气\\"\\"\\"\\n return \\"24度,晴\\"\\n \\n@mcp.resource(\\"weather://cities\\")\\ndef get_available_cities() -> list:\\n \\"\\"\\"获取所有可用的城市列表\\"\\"\\"\\n return [\\"Tokyo\\", \\"Sydney\\", \\"Tokyo\\"]\\n \\n@mcp.resource(\\"weather://forecast/{city}\\")\\ndef get_forecast(city: str) -> dict:\\n \\"\\"\\"获取指定城市的天气预报资源\\"\\"\\"\\n return {\\n \\"city\\": city,\\n \\"temperature\\": [10,25],\\n \\"condition\\":[\'sunny\', \'clear\', \'hot\'],\\n \\"unit\\": \\"celsius\\"\\n }\\n \\nif __name__ == \\"__main__\\":\\n # 使用SSE传输方式启动服务器\\n mcp.run(transport=\\"sse\\")\\n
\\nSolonMCP(全称:solon-ai-mcp),支持 java8,可提供完成的 mcp 内容支持(工具,资源,资源模板,提示语)。
\\n计算器工具
\\n@McpServerEndpoint(channel = McpChannel.STDIO)\\npublic class CalculatorTools {\\n @ToolMapping(description = \\"将两个数字相加\\")\\n public int add(@Param int a, @Param int b) {\\n return a + b;\\n }\\n\\n @ToolMapping(description = \\"从第一个数中减去第二个数\\")\\n public int subtract(@Param int a, @Param int b) {\\n return a - b;\\n }\\n\\n @ToolMapping(description = \\"将两个数相乘\\")\\n public int multiply(@Param int a, @Param int b) {\\n return a * b;\\n }\\n\\n @ToolMapping(description = \\"将第一个数除以第二个数\\")\\n public float divide(@Param float a, @Param float b) {\\n return a / b;\\n }\\n}\\n
\\n天气工具(有工具,资源,资源模板示例)
\\n@McpServerEndpoint(sseEndpoint = \\"/mcp/sse\\")\\npublic class WeatherTools {\\n @ToolMapping(description = \\"获取指定城市的当前天气\\")\\n public String get_weather(@Param(description=\\"城市\\") String city) {\\n return \\"{city: \'\\" + city + \\"\', temperature:[10,25], condition:[\'sunny\', \'clear\', \'hot\'], unit:celsius}\\";\\n }\\n\\n //可以给前端用,输出严格的 json 格式\\n @Produces(MimeType.APPLICATION_JSON_VALUE)\\n @ResourceMapping(uri = \\"weather://cities\\", description = \\"获取所有可用的城市列表\\")\\n public List<String> get_available_cities() {\\n return Arrays.asList(\\"Tokyo\\", \\"Sydney\\", \\"Tokyo\\");\\n }\\n\\n @ResourceMapping(uri = \\"weather://forecast/{city}\\", description = \\"获取指定城市的天气预报资源\\")\\n public String get_forecast(@Param(description=\\"城市\\") String city) {\\n return \\"{city: \'\\" + city + \\"\', temperature:[10,25], condition:[\'sunny\', \'clear\', \'hot\'], unit:celsius}\\";\\n }\\n}\\n
","description":"从 MCP SDK 的发展史上看,FastMCP 是前辈,SolonMCP 则是后辈。mcp-python-sdk 功能完善,已经很成熟了。而 mcp-java-sdk 却还不完善,比如: 还不支持 http streaming\\n还不支持 resouce template,不过有 pr 在走流程了(SolonMCP 提前提供了支持)\\n只支持 jdk17+(SolonMCP 提供了 jdk8+ 支持)\\n不支持 客户端断线自动重连(SolonMCP 提供了自动重连支持)\\n\\n两者的体验不能说是一样,但真的很像。\\n\\n1、FastMCP 的开发体验(python)…","guid":"https://juejin.cn/post/7502773930349559849","author":"掉鱼的猫","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-12T02:26:03.077Z","media":null,"categories":["后端","Java","MCP","Python"],"attachments":null,"extra":null,"language":null},{"title":"懂点编程做副业真的很简单,某鱼10倍暴利玩法教程 🎉","url":"https://juejin.cn/post/7502647033402490916","content":"大家好,花姐又来了!今天我们聊得程序员副业的话题,并给大家一个可以实操的小案例。
\\n你有没有注意到,某鱼上有不少提供去水印的服务,收取1块钱每张图。最搞笑的是,很多人都没意识到,去水印的技术其实非常简单,尤其是现在AI盛行的时代,好多平台会提供的去水印接口便宜又好用,一张图片只需要几分钱就能搞定,比某鱼的1块钱便宜不知道倍!
\\n重要声明:这不是广告,是教大家如何利用AI去水印的教程
\\n为什么说是暴利呢?因为花姐发现https://www.textin.com/
上面有个图像水印去除的接口,调用1次只需要0.025!!!妥妥的只赚不亏呀!
我们在看看去水印的效果,还是很厉害的,反正让我PS绝对没人家做的这么好,而且图片也没压缩:
\\n官方还贴心的给出了接口调用示例:
\\n接下来我们只需要套个GUI外壳,方便我们批量处理图片即可。
\\n官网虽然给出了调用示例,但是为了提高操作便捷性,我们可以做一个图形化界面(GUI)。别担心,GUI不难,我这就来带你做一个简单的Windows桌面应用,用Tkinter这个Python内建的库。
\\npip install requests pillow\\n
\\nimport os\\nimport tkinter as tk\\nfrom tkinter import filedialog, messagebox\\nimport json\\nimport requests\\nimport base64\\nfrom PIL import Image\\nfrom io import BytesIO\\n\\ndef get_file_content(filePath):\\n with open(filePath, \'rb\') as fp:\\n return fp.read()\\n \\n# 之前的水印去除API调用部分\\nclass CommonOcr(object):\\n def __init__(self, img_path=None, is_url=False):\\n # 图像水印去除\\n self._url = \'https://api.textin.com/ai/service/v1/image/watermark_remove\'\\n # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-app-id\\n # 示例代码中 x-ti-app-id 非真实数据\\n self._app_id = \'73b************************d269\'\\n # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-secret-code\\n # 示例代码中 x-ti-secret-code 非真实数据\\n self._secret_code = \'de1ac7*******************48993131\'\\n self._img_path = img_path\\n self._is_url = is_url\\n\\n def recognize(self):\\n head = {}\\n try:\\n head[\'x-ti-app-id\'] = self._app_id\\n head[\'x-ti-secret-code\'] = self._secret_code\\n if self._is_url:\\n head[\'Content-Type\'] = \'text/plain\'\\n body = self._img_path\\n else:\\n image = get_file_content(self._img_path)\\n head[\'Content-Type\'] = \'application/octet-stream\'\\n body = image\\n result = requests.post(self._url, data=body, headers=head)\\n return result.text\\n except Exception as e:\\n return e\\n\\ndef down_img(base64str,output_folder,img_name):\\n try:\\n img_data = base64.b64decode(base64str)\\n img = Image.open(BytesIO(img_data))\\n file_name = os.path.join(output_folder,img_name)\\n img.save(file_name)\\n # with open(output_folder, \'wb\') as f:\\n # f.write(image_data)\\n print(f\\"去水印图片已经保存到 {file_name}\\")\\n except Exception as e:\\n print(f\\"图片去水印失败: {e}\\")\\n\\n \\n# GUI界面部分\\nclass WatermarkRemoverApp:\\n def __init__(self, root):\\n self.root = root\\n self.root.title(\\"去水印工具\\")\\n self.root.geometry(\\"400x200\\")\\n\\n self.select_button = tk.Button(root, text=\\"选择文件夹\\", command=self.select_folder)\\n self.select_button.pack(pady=20)\\n\\n self.process_button = tk.Button(root, text=\\"开始去水印\\", command=self.process_images, state=tk.DISABLED)\\n self.process_button.pack(pady=20)\\n\\n def select_folder(self):\\n self.folder_path = filedialog.askdirectory()\\n if self.folder_path:\\n self.process_button.config(state=tk.NORMAL)\\n\\n def process_images(self):\\n output_folder = os.path.join(self.folder_path, \\"output\\")\\n os.makedirs(output_folder, exist_ok=True)\\n \\n for file_name in os.listdir(self.folder_path):\\n if file_name.endswith((\\".jpg\\",\\".jpeg\\", \\".png\\",\\".bmp\\")):\\n file_path = os.path.join(self.folder_path, file_name)\\n response = CommonOcr(img_path=file_path).recognize()\\n # 解析JSON字符串为Python字典\\n data = json.loads(response)\\n \\n if data[\\"code\\"] ==200: # 假设接口返回包含\\"success\\"表示成功\\n down_img(data[\\"result\\"][\\"image\\"],output_folder,file_name)\\n else:\\n messagebox.showerror(\\"错误\\", f\\"图片 {file_name} 去水印失败!{data[\'msg\']}\\")\\n \\n messagebox.showinfo(\\"完成\\", \\"所有图片已处理完毕!\\")\\n\\nif __name__ == \\"__main__\\":\\n root = tk.Tk()\\n app = WatermarkRemoverApp(root)\\n root.mainloop()\\n
\\n运行起来是这个样子的
\\noutput
目录下。现在你可以用这个工具来给图片去水印了,接下来就是去某鱼发布去水印的服务。比如,你可以定个价格:每张图片1元,批量去水印,随便做个广告推送,搞定!很多人会觉得手动去水印很麻烦,尤其是做电商的商家,他们更愿意支付小额费用来节省时间。
\\n这篇教程给大家展示了如何利用Python简单地实现一个去水印工具,并且利用textin的接口去水印服务赚取差价。通过GUI界面,你甚至可以让这个工具变得更方便易用,适合没有编程基础的人群。你可以将这款工具用作副业,快速投入市场,甚至可以做得越来越大,开始接更多客户,走上暴富之路(咳咳,开玩笑的)。
\\n顺手点赞+在看就是对花姐最大的支持! 🎉
","description":"大家好,花姐又来了!今天我们聊得程序员副业的话题,并给大家一个可以实操的小案例。 你有没有注意到,某鱼上有不少提供去水印的服务,收取1块钱每张图。最搞笑的是,很多人都没意识到,去水印的技术其实非常简单,尤其是现在AI盛行的时代,好多平台会提供的去水印接口便宜又好用,一张图片只需要几分钱就能搞定,比某鱼的1块钱便宜不知道倍!\\n\\n重要声明:这不是广告,是教大家如何利用AI去水印的教程\\n\\n为什么说是暴利呢?因为花姐发现https://www.textin.com/上面有个图像水印去除的接口,调用1次只需要0.025!!!妥妥的只赚不亏呀!\\n\\n我们在看看去水印的…","guid":"https://juejin.cn/post/7502647033402490916","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-12T01:27:35.238Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b19524bce384f8ba500721c56c8305c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=5HnHDTu7Ii7rn2jTOtdaqQWjGm0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a11144293561468592d7b9b8077d7653~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=GwhrKchUhvrl4%2Bm%2BvRLFKuX%2F67Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78e582f5ea564d84ad3736352fb0401e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=%2B3Lqvo4MR7nUUdqU5pNsej9rvDY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f4f2292e0dac42fc953ca8b0438e6a97~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=kplFp0DOdVlNdlmiSdJOhpBjOfM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3657146c25e942aa819329e4817a69d9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=xkocwBpnEt8DRFBIHUOjrgBA4FQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/83ddb249d3c840009cd8656c88f6f944~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=8J%2FRf2wK7bm6j9bdbEqdX%2F%2B1vy8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/24b077e976bf4308b42fecbb3b786364~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747618055&x-signature=7HnqeDfffHto%2FpGNFX9lJke8%2BsI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot配置文件的12个实用技巧","url":"https://juejin.cn/post/7502685441847689227","content":"配置文件是SpringBoot应用的核心组成部分,它决定了应用的行为、连接参数以及功能特性。
\\n合理利用SpringBoot的配置机制,不仅可以提高开发效率,还能增强应用的灵活性和可维护性。
\\nSpringBoot支持通过profiles实现多环境配置,便于在开发、测试和生产环境之间无缝切换。
\\n创建特定环境的配置文件:
\\napplication-dev.yml
(开发环境)application-test.yml
(测试环境)application-prod.yml
(生产环境)在主配置文件application.yml
中激活特定环境:
spring:\\n profiles:\\n active: dev\\n
\\n使用分组功能(Spring Boot 2.4+)来简化环境配置:
\\nspring:\\n profiles:\\n group:\\n dev: local-db, local-cache, dev-api\\n prod: cloud-db, redis-cache, prod-api\\n
\\n无需修改配置文件,直接在启动时指定环境:
\\njava -jar app.jar --spring.profiles.active=prod\\n
\\n了解SpringBoot配置的优先级顺序,有助于解决配置冲突。
\\n对于数据库URL配置,可以在不同级别设置:
\\n# application.yml (优先级低)\\nspring:\\n datasource:\\n url: jdbc:mysql://localhost:3306/default_db\\n
\\n# 命令行参数 (优先级高)\\njava -jar app.jar --spring.datasource.url=jdbc:mysql://prod-server:3306/prod_db\\n
\\n最终生效的是命令行参数中的URL。
\\nSpringBoot支持多种属性命名风格,自动进行松散绑定,提高配置的灵活性。
\\n对于Java属性serverPort
:
server-port
(推荐用于.properties和.yml文件)serverPort
server_port
(推荐用于环境变量)SERVER_PORT
(环境变量的标准格式)配置文件:
\\nmy-app:\\n connection-timeout: 5000\\n read-timeout: 10000\\n
\\nJava代码:
\\n@ConfigurationProperties(prefix = \\"my-app\\")\\npublic class AppProperties {\\n private int connectionTimeout;\\n private int readTimeout;\\n \\n // getters and setters\\n}\\n
\\nSpringBoot会自动将connection-timeout
绑定到connectionTimeout
属性。
在开发和测试环境中,经常需要生成随机值,SpringBoot提供了内置支持。
\\napp:\\n # 随机整数\\n random-int: ${random.int}\\n # 范围内的随机整数\\n random-int-range: ${random.int[1000,2000]}\\n # 随机长整数\\n random-long: ${random.long}\\n # 随机字符串\\n random-string: ${random.uuid}\\n # 随机字节\\n secret-key: ${random.bytes[16]}\\n
\\n服务器端口随机分配,避免开发环境端口冲突:
\\nserver:\\n port: ${random.int[8000,9000]}\\n
\\n测试环境使用随机密钥:
\\napp:\\n security:\\n secret-key: ${random.uuid}\\n
\\n使用@ConfigurationProperties
绑定结构化配置,提供类型安全和代码自动完成。
配置类:
\\n@Component\\n@ConfigurationProperties(prefix = \\"mail\\")\\n@Validated\\npublic class MailProperties {\\n \\n @NotEmpty\\n private String host;\\n \\n @Min(1025)\\n @Max(65536)\\n private int port = 25;\\n \\n @Email\\n private String from;\\n \\n private boolean enabled;\\n \\n // getters and setters\\n}\\n
\\n配置文件:
\\nmail:\\n host: smtp.example.com\\n port: 587\\n from: noreply@example.com\\n enabled: true\\n
\\nmail:\\n recipients:\\n - admin@example.com\\n - support@example.com\\n connection:\\n timeout: 5000\\n retry: 3\\n additional-headers:\\n X-Priority: 1\\n X-Mailer: MyApp\\n
\\n@ConfigurationProperties(prefix = \\"mail\\")\\npublic class MailProperties {\\n private List<String> recipients = new ArrayList<>();\\n private Connection connection = new Connection();\\n private Map<String, String> additionalHeaders = new HashMap<>();\\n \\n // getters and setters\\n \\n public static class Connection {\\n private int timeout;\\n private int retry;\\n \\n // getters and setters\\n }\\n}\\n
\\n在大型项目中,将配置拆分为多个文件可以提高可维护性。
\\n@Configuration\\n@PropertySource(\\"classpath:db.properties\\")\\n@PropertySource(\\"classpath:cache.properties\\")\\npublic class AppConfig {\\n // ...\\n}\\n
\\n在Spring Boot 2.4+中,可以在主配置文件中导入其他配置:
\\nspring:\\n config:\\n import:\\n - classpath:db.yml\\n - optional:file:./config/local.yml\\n - configserver:http://config-server:8888/\\n
\\n注意optional:
前缀表示文件不存在也不会报错。
在生产环境中,保护敏感配置如密码和API密钥至关重要。
\\n<dependency>\\n <groupId>com.github.ulisesbocchio</groupId>\\n <artifactId>jasypt-spring-boot-starter</artifactId>\\n <version>3.0.4</version>\\n</dependency>\\n
\\n# 加密后的配置\\nspring.datasource.password=ENC(G8Sn36MAJOWJwEgAMZM3Cw0QC9rEEVyn)\\n
\\njava -jar app.jar --jasypt.encryptor.password=mySecretKey\\n
\\nspring:\\n datasource:\\n username: ${DB_USERNAME}\\n password: ${DB_PASSWORD}\\n
\\n对配置属性进行校验,避免不合法的配置导致运行时错误。
\\n@ConfigurationProperties(prefix = \\"app.connection\\")\\n@Validated\\npublic class ConnectionProperties {\\n \\n @NotNull\\n @Min(1000)\\n @Max(10000)\\n private Integer timeout;\\n \\n @Pattern(regexp = \\"^(http|https)://.*$\\")\\n private String serviceUrl;\\n \\n @Email\\n private String supportEmail;\\n \\n // getters and setters\\n}\\n
\\n@Target({ElementType.FIELD})\\n@Retention(RetentionPolicy.RUNTIME)\\n@Constraint(validatedBy = IpAddressValidator.class)\\npublic @interface IpAddress {\\n String message() default \\"Invalid IP address\\";\\n Class<?>[] groups() default {};\\n Class<? extends Payload>[] payload() default {};\\n}\\n\\npublic class IpAddressValidator implements ConstraintValidator<IpAddress, String> {\\n @Override\\n public boolean isValid(String value, ConstraintValidatorContext context) {\\n if (value == null) {\\n return true;\\n }\\n String regex = \\"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$\\";\\n return value.matches(regex);\\n }\\n}\\n\\n@ConfigurationProperties(prefix = \\"app.server\\")\\n@Validated\\npublic class ServerProperties {\\n @IpAddress\\n private String ipAddress;\\n // ...\\n}\\n
\\n在配置文件中使用占位符引用其他配置项,提高灵活性和减少重复。
\\napp:\\n name: MyApp\\n api:\\n base-url: http://api.example.com\\n version: v1\\n full-url: ${app.api.base-url}/${app.api.version}\\n security:\\n timeout: 3600\\n timeout-millis: ${app.security.timeout}000\\n
\\n提供默认值以防配置缺失:
\\napp:\\n cache-dir: ${CACHE_DIR:/tmp/cache}\\n max-threads: ${MAX_THREADS:10}\\n
\\nserver:\\n port: ${PORT:8080}\\n address: ${SERVER_ADDRESS:0.0.0.0}\\n \\nlogging:\\n path: ${LOG_PATH:${user.home}/logs}\\n
\\n使用Spring的条件注解根据条件加载配置,提高灵活性。
\\n@Configuration\\n@Profile(\\"dev\\")\\npublic class DevDatabaseConfig {\\n @Bean\\n public DataSource dataSource() {\\n return new EmbeddedDatabaseBuilder()\\n .setType(EmbeddedDatabaseType.H2)\\n .build();\\n }\\n}\\n\\n@Configuration\\n@Profile(\\"prod\\")\\npublic class ProdDatabaseConfig {\\n @Bean\\n public DataSource dataSource() {\\n HikariDataSource dataSource = new HikariDataSource();\\n dataSource.setJdbcUrl(\\"jdbc:mysql://prod-db:3306/app\\");\\n // 其他配置...\\n return dataSource;\\n }\\n}\\n
\\n@Configuration\\n@ConditionalOnProperty(name = \\"app.cache.enabled\\", havingValue = \\"true\\")\\npublic class CacheConfig {\\n @Bean\\n public CacheManager cacheManager() {\\n return new ConcurrentMapCacheManager();\\n }\\n}\\n\\n@Configuration\\n@ConditionalOnMissingBean(CacheManager.class)\\npublic class NoCacheConfig {\\n // 备用配置\\n}\\n
\\n@Configuration\\n@ConditionalOnClass(name = \\"org.springframework.data.redis.core.RedisTemplate\\")\\npublic class RedisConfig {\\n // Redis相关配置\\n}\\n
\\n在配置文件中有效地表示复杂数据结构。
\\napp:\\n # 简单列表\\n servers:\\n - server1.example.com\\n - server2.example.com\\n - server3.example.com\\n \\n # 对象列表\\n endpoints:\\n - name: users\\n url: /api/users\\n method: GET\\n - name: orders\\n url: /api/orders\\n method: POST\\n
\\n在Java中绑定:
\\n@ConfigurationProperties(prefix = \\"app\\")\\npublic class AppConfig {\\n private List<String> servers = new ArrayList<>();\\n private List<Endpoint> endpoints = new ArrayList<>();\\n \\n // getters and setters\\n \\n public static class Endpoint {\\n private String name;\\n private String url;\\n private String method;\\n \\n // getters and setters\\n }\\n}\\n
\\napp:\\n # 简单映射\\n feature-flags:\\n enableNewUI: true\\n enableAnalytics: false\\n enableNotifications: true\\n \\n # 复杂映射\\n datasources:\\n main:\\n url: jdbc:mysql://main-db:3306/app\\n username: mainuser\\n maxPoolSize: 20\\n report:\\n url: jdbc:mysql://report-db:3306/reports\\n username: reportuser\\n maxPoolSize: 5\\n
\\n在Java中绑定:
\\n@ConfigurationProperties(prefix = \\"app\\")\\npublic class AppConfig {\\n private Map<String, Boolean> featureFlags = new HashMap<>();\\n private Map<String, DataSourceProperties> datasources = new HashMap<>();\\n \\n // getters and setters\\n \\n public static class DataSourceProperties {\\n private String url;\\n private String username;\\n private int maxPoolSize;\\n \\n // getters and setters\\n }\\n}\\n
\\n创建配置元数据,提供IDE自动完成和文档。
\\n<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-configuration-processor</artifactId>\\n <optional>true</optional>\\n</dependency>\\n
\\n@ConfigurationProperties(prefix = \\"acme\\")\\npublic class AcmeProperties {\\n\\n /**\\n * 是否启用ACME服务。\\n */\\n private boolean enabled = false;\\n\\n /**\\n * 服务的远程地址。\\n */\\n @NotEmpty\\n private String remoteAddress;\\n\\n /**\\n * 会话超时时间,单位为秒。\\n * 最小值为1分钟,最大值为1小时。\\n */\\n @Min(60)\\n @Max(3600)\\n private int sessionTimeout = 600;\\n\\n // getters和setters\\n}\\n
\\n创建META-INF/additional-spring-configuration-metadata.json
文件:
{\\n \\"properties\\": [\\n {\\n \\"name\\": \\"app.security.api-key\\",\\n \\"type\\": \\"java.lang.String\\",\\n \\"description\\": \\"API安全密钥,用于外部服务认证。\\",\\n \\"sourceType\\": \\"com.example.AppSecurityProperties\\"\\n },\\n {\\n \\"name\\": \\"app.rate-limit.enabled\\",\\n \\"type\\": \\"java.lang.Boolean\\",\\n \\"description\\": \\"是否启用API速率限制。\\",\\n \\"defaultValue\\": true,\\n \\"deprecation\\": {\\n \\"level\\": \\"warning\\",\\n \\"replacement\\": \\"app.security.rate-limit.enabled\\",\\n \\"reason\\": \\"API速率限制配置已移动到security命名空间。\\"\\n }\\n }\\n ],\\n \\"hints\\": [\\n {\\n \\"name\\": \\"app.log-level\\",\\n \\"values\\": [\\n {\\n \\"value\\": \\"debug\\",\\n \\"description\\": \\"调试日志级别。\\"\\n },\\n {\\n \\"value\\": \\"info\\",\\n \\"description\\": \\"信息日志级别。\\"\\n },\\n {\\n \\"value\\": \\"warn\\",\\n \\"description\\": \\"警告日志级别。\\"\\n },\\n {\\n \\"value\\": \\"error\\",\\n \\"description\\": \\"错误日志级别。\\"\\n }\\n ]\\n }\\n ]\\n}\\n
\\n在实际开发中,我们应根据项目规模和复杂度选择合适的配置策略。
\\n通过合理应用这些技巧,我们可以构建更加灵活、安全且易于维护的SpringBoot应用,为业务需求的快速变化提供坚实的技术支持。
","description":"配置文件是SpringBoot应用的核心组成部分,它决定了应用的行为、连接参数以及功能特性。 合理利用SpringBoot的配置机制,不仅可以提高开发效率,还能增强应用的灵活性和可维护性。\\n\\n1. 多环境配置(Profiles)\\n\\nSpringBoot支持通过profiles实现多环境配置,便于在开发、测试和生产环境之间无缝切换。\\n\\n基本用法\\n\\n创建特定环境的配置文件:\\n\\napplication-dev.yml(开发环境)\\napplication-test.yml(测试环境)\\napplication-prod.yml(生产环境)\\n\\n在主配置文件applicati…","guid":"https://juejin.cn/post/7502685441847689227","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-11T23:44:37.201Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"吃透这六大设计模式,你也能写出优雅代码!","url":"https://juejin.cn/post/7502608144288890895","content":"在系统中,当某个全局使用的类频繁地进行创建与销毁操作,为节省系统资源并确保实例的唯一性,可使用单例模式。例如,日志记录器在整个系统中通常只需要一个实例来记录日志信息,此时单例模式就非常适用。
\\n饿汉模式是单例模式的一种实现方式,在类加载时就创建单例实例。以下是 Java 代码示例:
\\npublic class SingletonHungry {\\n // 私有静态实例\\n private static final SingletonHungry instance = new SingletonHungry();\\n\\n // 私有构造函数,防止外部实例化\\n private SingletonHungry() {}\\n\\n // 提供获取实例的方法\\n public static SingletonHungry getInstance() {\\n return instance;\\n }\\n}\\n
\\n这种方式的优点是简单直接,在类加载时就完成实例的创建,保证了线程安全;缺点是如果实例创建过程复杂或实例在很长时间内不被使用,会造成资源的浪费。
\\n懒汉模式是在需要使用实例时才进行创建。为保证线程安全,通常采用双重检查锁定机制。以下是 Java 代码示例:
\\npublic class SingletonLazy {\\n // 私有静态实例\\n private static SingletonLazy instance;\\n\\n // 私有构造函数,防止外部实例化\\n private SingletonLazy() {}\\n\\n // 提供获取实例的方法\\n public static SingletonLazy getInstance() {\\n if (instance == null) {\\n synchronized (SingletonLazy.class) {\\n if (instance == null) {\\n instance = new SingletonLazy();\\n }\\n }\\n }\\n return instance;\\n }\\n}\\n
\\n这种方式的优点是实现了延迟加载,避免了资源的浪费;缺点是实现相对复杂,需要正确处理多线程环境下的并发问题。
\\n代理模式属于结构型设计模式,一个类(代理类)代表另一个类(目标类)来提供功能。当需要在访问一个类时添加额外的控制逻辑,如权限验证、日志记录等,可使用代理模式。代理类在客户端和目标类之间起到中介作用,客户端通过代理类来访问目标类的方法。
\\n假设我们有一个Subject
接口和其实现类RealSubject
,现在使用静态代理来添加额外的功能(如日志记录)。
// 定义接口\\ninterface Subject {\\n void request();\\n}\\n\\n// 实现类\\nclass RealSubject implements Subject {\\n @Override\\n public void request() {\\n System.out.println(\\"RealSubject: handling request\\");\\n }\\n}\\n\\n// 代理类\\nclass Proxy implements Subject {\\n private RealSubject realSubject;\\n\\n public Proxy(RealSubject realSubject) {\\n this.realSubject = realSubject;\\n }\\n\\n @Override\\n public void request() {\\n System.out.println(\\"Proxy: Logging before request\\");\\n realSubject.request();\\n System.out.println(\\"Proxy: Logging after request\\");\\n }\\n}\\n
\\n在客户端中使用代理:
\\npublic class ProxyPatternDemo {\\n public static void main(String[] args) {\\n RealSubject realSubject = new RealSubject();\\n Proxy proxy = new Proxy(realSubject);\\n proxy.request();\\n }\\n}\\n
\\n动态代理通过反射机制在运行时动态生成代理类。以下是使用 Java 内置的动态代理机制的示例:
\\nimport java.lang.reflect.InvocationHandler;\\nimport java.lang.reflect.Method;\\nimport java.lang.reflect.Proxy;\\n\\n// 定义接口\\ninterface Subject {\\n void request();\\n}\\n\\n// 实现类\\nclass RealSubject implements Subject {\\n @Override\\n public void request() {\\n System.out.println(\\"RealSubject: handling request\\");\\n }\\n}\\n\\n// 调用处理器\\nclass MyInvocationHandler implements InvocationHandler {\\n private Object target;\\n\\n public MyInvocationHandler(Object target) {\\n this.target = target;\\n }\\n\\n @Override\\n public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\\n System.out.println(\\"Dynamic Proxy: Logging before request\\");\\n Object result = method.invoke(target, args);\\n System.out.println(\\"Dynamic Proxy: Logging after request\\");\\n return result;\\n }\\n}\\n\\n// 客户端\\npublic class DynamicProxyPatternDemo {\\n public static void main(String[] args) {\\n RealSubject realSubject = new RealSubject();\\n Subject proxy = (Subject) Proxy.newProxyInstance(\\n realSubject.getClass().getClassLoader(),\\n realSubject.getClass().getInterfaces(),\\n new MyInvocationHandler(realSubject));\\n proxy.request();\\n }\\n}\\n
\\n工厂模式是 Java 中常用的创建型设计模式,它提供了一种创建对象的方式,将对象的创建逻辑封装起来,客户端通过工厂类来获取对象,而无需关心对象的具体创建过程。
\\n工厂模式适用于创建复杂对象的场景。对于简单对象,特别是可以直接通过new
关键字创建的对象,使用工厂模式可能会增加系统的复杂度,此时不一定需要使用工厂模式。
简单工厂模式是工厂模式的基础形式,它定义了一个工厂类,用于创建产品对象。以下是简单工厂模式的示例:
\\n// 定义产品接口\\ninterface Product {\\n void operation();\\n}\\n\\n// 具体产品类\\nclass ConcreteProductA implements Product {\\n @Override\\n public void operation() {\\n System.out.println(\\"ConcreteProductA: performing operation\\");\\n }\\n}\\n\\n// 具体产品类\\nclass ConcreteProductB implements Product {\\n @Override\\n public void operation() {\\n System.out.println(\\"ConcreteProductB: performing operation\\");\\n }\\n}\\n\\n// 工厂类\\nclass SimpleFactory {\\n public static Product createProduct(String type) {\\n if (\\"A\\".equals(type)) {\\n return new ConcreteProductA();\\n } else if (\\"B\\".equals(type)) {\\n return new ConcreteProductB();\\n }\\n return null;\\n }\\n}\\n
\\n在客户端中使用简单工厂:
\\npublic class SimpleFactoryPatternDemo {\\n public static void main(String[] args) {\\n Product productA = SimpleFactory.createProduct(\\"A\\");\\n productA.operation();\\n\\n Product productB = SimpleFactory.createProduct(\\"B\\");\\n productB.operation();\\n }\\n}\\n
\\n简单工厂模式的缺点是扩展性差,当需要新增产品时,需要修改工厂类的代码。
\\n工厂方法模式将对象的创建逻辑延迟到具体的工厂子类中实现。以下是工厂方法模式的示例:
\\n// 定义产品接口\\ninterface Product {\\n void operation();\\n}\\n\\n// 具体产品类\\nclass ConcreteProductA implements Product {\\n @Override\\n public void operation() {\\n System.out.println(\\"ConcreteProductA: performing operation\\");\\n }\\n}\\n\\n// 具体产品类\\nclass ConcreteProductB implements Product {\\n @Override\\n public void operation() {\\n System.out.println(\\"ConcreteProductB: performing operation\\");\\n }\\n}\\n\\n// 定义工厂接口\\ninterface Factory {\\n Product createProduct();\\n}\\n\\n// 具体工厂类\\nclass ConcreteFactoryA implements Factory {\\n @Override\\n public Product createProduct() {\\n return new ConcreteProductA();\\n }\\n}\\n\\n// 具体工厂类\\nclass ConcreteFactoryB implements Factory {\\n @Override\\n public Product createProduct() {\\n return new ConcreteProductB();\\n }\\n}\\n
\\n在客户端中使用工厂方法模式:
\\npublic class FactoryMethodPatternDemo {\\n public static void main(String[] args) {\\n Factory factoryA = new ConcreteFactoryA();\\n Product productA = factoryA.createProduct();\\n productA.operation();\\n\\n Factory factoryB = new ConcreteFactoryB();\\n Product productB = factoryB.createProduct();\\n productB.operation();\\n }\\n}\\n
\\n工厂方法模式提高了代码的扩展性,新增产品时只需创建新的具体工厂类和产品类,无需修改原有的工厂接口和其他具体工厂类。
\\n抽象工厂模式提供了创建一系列相关或相互依赖对象的接口,由具体的工厂类来实现这些接口。以下是抽象工厂模式的示例:
\\n// 定义产品接口\\ninterface ProductA {\\n void operationA();\\n}\\n\\ninterface ProductB {\\n void operationB();\\n}\\n\\n// 具体产品类\\nclass ConcreteProductA1 implements ProductA {\\n @Override\\n public void operationA() {\\n System.out.println(\\"ConcreteProductA1: performing operationA\\");\\n }\\n}\\n\\nclass ConcreteProductA2 implements ProductA {\\n @Override\\n public void operationA() {\\n System.out.println(\\"ConcreteProductA2: performing operationA\\");\\n }\\n}\\n\\nclass ConcreteProductB1 implements ProductB {\\n @Override\\n public void operationB() {\\n System.out.println(\\"ConcreteProductB1: performing operationB\\");\\n }\\n}\\n\\nclass ConcreteProductB2 implements ProductB {\\n @Override\\n public void operationB() {\\n System.out.println(\\"ConcreteProductB2: performing operationB\\");\\n }\\n}\\n\\n// 定义抽象工厂接口\\ninterface AbstractFactory {\\n ProductA createProductA();\\n ProductB createProductB();\\n}\\n\\n// 具体工厂类\\nclass ConcreteFactory1 implements AbstractFactory {\\n @Override\\n public ProductA createProductA() {\\n return new ConcreteProductA1();\\n }\\n\\n @Override\\n public ProductB createProductB() {\\n return new ConcreteProductB1();\\n }\\n}\\n\\nclass ConcreteFactory2 implements AbstractFactory {\\n @Override\\n public ProductA createProductA() {\\n return new ConcreteProductA2();\\n }\\n\\n @Override\\n public ProductB createProductB() {\\n return new ConcreteProductB2();\\n }\\n}\\n
\\n在客户端中使用抽象工厂模式:
\\npublic class AbstractFactoryPatternDemo {\\n public static void main(String[] args) {\\n AbstractFactory factory1 = new ConcreteFactory1();\\n ProductA productA1 = factory1.createProductA();\\n ProductB productB1 = factory1.createProductB();\\n productA1.operationA();\\n productB1.operationB();\\n\\n AbstractFactory factory2 = new ConcreteFactory2();\\n ProductA productA2 = factory2.createProductA();\\n ProductB productB2 = factory2.createProductB();\\n productA2.operationA();\\n productB2.operationB();\\n }\\n}\\n
\\n抽象工厂模式适用于创建一系列相关对象的场景,通过抽象工厂接口和具体工厂类的实现,提高了系统的可维护性和扩展性。
\\n观察者模式是一种行为型设计模式,当对象间存在一对多关系时使用。一个对象(被观察者)的状态发生改变时,会自动通知所有依赖它的对象(观察者)。
\\n例如,在股票交易系统中,多个投资者(观察者)关注某只股票(被观察者)的价格变化。当股票价格发生变化时,系统会自动通知所有关注该股票的投资者。
\\nimport java.util.ArrayList;\\nimport java.util.List;\\n\\n// 定义被观察者接口\\ninterface Observable {\\n void addObserver(Observer observer);\\n void removeObserver(Observer observer);\\n void notifyObservers();\\n}\\n\\n// 具体被观察者类\\nclass Stock implements Observable {\\n private String symbol;\\n private double price;\\n private List<Observer> observers = new ArrayList<>();\\n\\n public Stock(String symbol) {\\n this.symbol = symbol;\\n }\\n\\n public double getPrice() {\\n return price;\\n }\\n\\n public void setPrice(double price) {\\n this.price = price;\\n notifyObservers();\\n }\\n\\n @Override\\n public void addObserver(Observer observer) {\\n observers.add(observer);\\n }\\n\\n @Override\\n public void removeObserver(Observer observer) {\\n observers.remove(observer);\\n }\\n\\n @Override\\n public void notifyObservers() {\\n for (Observer observer : observers) {\\n observer.update(this);\\n }\\n }\\n}\\n\\n// 定义观察者接口\\ninterface Observer {\\n void update(Observable observable);\\n}\\n\\n// 具体观察者类\\nclass Investor implements Observer {\\n private String name;\\n\\n public Investor(String name) {\\n this.name = name;\\n }\\n\\n @Override\\n public void update(Observable observable) {\\n if (observable instanceof Stock) {\\n Stock stock = (Stock) observable;\\n System.out.println(name + \\" received update: \\" + stock.getPrice());\\n }\\n }\\n}\\n
\\n在客户端中使用观察者模式:
\\npublic class ObserverPatternDemo {\\n public static void main(String[] args) {\\n Stock stock = new Stock(\\"AAPL\\");\\n Investor investor1 = new Investor(\\"Investor 1\\");\\n Investor investor2 = new Investor(\\"Investor 2\\");\\n\\n stock.addObserver(investor1);\\n stock.addObserver(investor2);\\n\\n stock.setPrice(150.0);\\n }\\n}\\n
\\n享元模式是一种结构型设计模式,它通过共享对象来减少对象的创建数量,从而降低内存占用和提高性能。享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
\\nimport java.util.HashMap;\\nimport java.util.Map;\\n\\n// 定义享元接口\\ninterface Shape {\\n void draw();\\n}\\n\\n// 具体享元类\\nclass Circle implements Shape {\\n private String color;\\n private int x;\\n private int y;\\n private int radius;\\n\\n public Circle(String color) {\\n this.color = color;\\n }\\n\\n public void setX(int x) {\\n this.x = x;\\n }\\n\\n public void setY(int y) {\\n this.y = y;\\n }\\n\\n public void setRadius(int radius) {\\n this.radius = radius;\\n }\\n
\\n上述六种设计模式从不同角度为软件开发提供了解决方案。单例模式确保实例唯一性,有效节省资源;代理模式通过中间层实现对目标对象的访问控制与功能增强;工厂模式将对象创建逻辑封装,便于管理与扩展;观察者模式实现对象间的联动响应;享元模式通过对象共享减少内存占用;策略模式支持算法的动态切换,提升系统灵活性。这些设计模式各有优劣和适用场景,开发者在实际项目中,需根据具体需求与业务场景灵活选择、组合运用,从而构建出结构清晰、可维护性高、性能优良的软件系统 。
","description":"一、单例模式 1.1 使用场景\\n\\n在系统中,当某个全局使用的类频繁地进行创建与销毁操作,为节省系统资源并确保实例的唯一性,可使用单例模式。例如,日志记录器在整个系统中通常只需要一个实例来记录日志信息,此时单例模式就非常适用。\\n\\n1.2 注意事项\\n单例类在整个系统中只能有一个实例。\\n单例类需自行创建并管理这个唯一的实例。\\n单例类要为系统中的其他对象提供获取该实例的方法。\\n1.3 饿汉模式\\n\\n饿汉模式是单例模式的一种实现方式,在类加载时就创建单例实例。以下是 Java 代码示例:\\n\\npublic class SingletonHungry {\\n // 私有…","guid":"https://juejin.cn/post/7502608144288890895","author":"天天摸鱼的java工程师","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-11T12:31:49.985Z","media":null,"categories":["后端","Java","面试"],"attachments":null,"extra":null,"language":null},{"title":"从来只嫌弃 SpringBoot2 慢 ,SpringBoot3 这些技术确实快了","url":"https://juejin.cn/post/7502648188379119666","content":"上一篇看了 SpringBoot2 的一些能力 ,这一篇来看看 SpringBoot3 的游戏额优化
\\n这一部分不在本次的主要研究范围 ,但是可以大致提一下 :如果使用 SpringBoot3 , 那么必然不会在沿用 JDK8了 。
\\n所有 SpringBoot3 的性能天然要把 JDK 性能算进去。
\\n\\n\\n\\n更优秀的 垃圾回收器
\\n
👆👆 在 JDK21 的时候 ,ZGC 已经可以稳定使用了 ,这是新一代的垃圾回收器 ,不论是停顿时间还是大堆的处理都非常地优秀 ,这里就不详细说说 ,可以看看上面地文章。
\\n\\n\\n虚拟线程
\\n
虚拟线程是由 Project Loom 引入的一个轻量级线程模型 , 除了极大的简化了高并发编程以外,更提高了线程的性能。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方面 | 传统线程(JDK 8) | 虚拟线程(Project Loom) |
---|---|---|
内存消耗 | 每个线程预分配较大内存栈,约 1MB 起步 | 栈大小按需分配,内存利用率极高 |
线程数量上限 | 几千到几万个线程左右,根据系统资源决定 | 数百万,理论上无限制 |
上下文切换开销 | 线程切换涉及内核态,开销大 | 用户态切换,开销微乎其微 |
阻塞处理 | 阻塞线程会占用内核线程资源 | 阻塞自动挂起,不占内核线程,提升资源利用率 |
开发复杂度 | 高,依赖线程池和异步API,代码复杂 | 低,可使用传统同步阻塞模型,代码简洁 |
CPU 利用率 | 线程过多时上下文切换导致 CPU 利用率下降 | 更少上下文切换,用更少线程实现更多任务 |
与现有代码兼容性 | 完全兼容 | 兼容 Thread API,易于迁移 |
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n即时编译器(JIT)
\\n
方面 | JDK 8 JIT | JDK 24 JIT(含 GraalVM) |
---|---|---|
编译器技术 | HotSpot C2 编译器 | 集成 GraalVM,高级动态优化引擎 |
指令集支持 | 基础 SSE 和 AVX 支持 | 最新指令集(AVX-512、ARM SVE)支持 |
编译速度 | 单线程或低并行度编译 | 多线程并行编译,减少延迟 |
动态内联和逃逸分析 | 基本实现 | 更智能、精准,减少不必要开销 |
虚拟线程支持 | 无 | JIT 优化虚拟线程协作,提升并发性能 |
启动性能与预热 | 传统预热阶段 | 改进预热策略,配合 CDS 快速启动 |
代码缓存管理 | 容量有限,溢出时影响性能 | 增强缓存管理,提升长期运行稳定性 |
\\n\\n阶段总结 :
\\n
虽然 Java 已经不是当年称霸的时代了 ,但是性能方面一直都有做优化。
\\n这些东西不在本次深入的过程中,所以不进行代码层面的展开,下面着重看这些 ,SpringBoot 的新技术。
\\n\\n\\nAOT 如何进行优化
\\n
AOT(Ahead-Of-Time)优化是从Spring Framework 6开始引入的一个新特性,用于提升应用的启动性能和运行时效率。
\\n用一句话去解释它 :
\\n<plugins>\\n <!-- Spring Boot Maven 插件,包含 AOT 编译目标 --\x3e\\n <plugin>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-maven-plugin</artifactId>\\n <executions>\\n <execution>\\n <goals>\\n <goal>process-aot</goal>\\n </goals>\\n </execution>\\n </executions>\\n <configuration>\\n <classifier>exec</classifier>\\n </configuration>\\n </plugin>\\n</plugins>\\n
\\n优势类别 | 具体好处 | 说明 |
---|---|---|
启动时间优化 | 大幅缩短应用启动时间 | 通过提前生成代码,避免运行时反射,启动更快。 |
内存占用减少 | 降低运行时内存消耗 | 减少动态代理和反射,节省堆和元空间内存。 |
云原生支持 | 更适合云环境和无服务器场景 | 快速启动和低内存消耗提升云环境资源利用效率。 |
本地镜像构建 | 可生成高性能的原生本地执行镜像 | 结合 GraalVM,实现完全本地编译,提升性能。 |
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nGraalVM 能做些什么 ?
\\n
核心特性 | 说明 |
---|---|
多语言支持 | 支持多种语言(Java, JS, Ruby, Python 等) |
高性能即时编译器(JIT) | 比传统 HotSpot 编译器更高效,提升运行性能 |
原生镜像构建 | 生成快速启动、低内存占用的原生可执行文件 |
Polyglot 互操作性 | 多语言代码在同一进程内无缝互调 |
运行时动态优化 | 动态内联、逃逸分析和向量化等高级编译优化 |
// S1 : 准备 GraalVM JDK21\\n@ https://www.graalvm.org/latest/getting-started/windows/\\n\\n- PS :网络上绝大多数文档都是 < GraalVM JDK21 , 这就导致按照那一套基本上走不通\\n- 首先安装 graalvm-jdk-21.0.1\\n- 其次安装 Visual Studio Build ToolsVisual Studio Build Tools (这里按照名字取下载地址搜就行)\\n- 也可以看这篇 @ https://zhuanlan.zhihu.com/p/675468375\\n- 注意 : 安装后配置一下 GRAALVM_HOME\\n\\n\\n// S2 : 搭建一个 SpringBoot3 的项目 (其实重点就算这个 builde)\\n<build>\\n <plugins>\\n <plugin>\\n <groupId>org.graalvm.buildtools</groupId>\\n <artifactId>native-maven-plugin</artifactId>\\n </plugin>\\n <plugin>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-maven-plugin</artifactId>\\n </plugin>\\n </plugins>\\n</build>\\n\\n\\n// S3 : 执行 MVN 命令打包原生镜像\\nmvn -Pnative -DskipTests native:compile\\n\\n
\\n// 问题 : GraalVm: Failed to find ‘vcvarsall.bat’ in a Visual Studio installation\\n\\n// 找到这条日志\\n[INFO] Executing: D:\\\\code\\\\java\\\\jdk\\\\graalvm-jdk-21.0.7+8.1\\\\bin\\\\native-image.cmd @target\\\\tmp\\\\native-image-6991173578604340885.args\\n\\n\\n// 编辑其中的 native-image.cmd ,在第二行加入如下 :\\ncall \\"D:\\\\software\\\\vstool\\\\main\\\\VC\\\\Auxiliary\\\\Build\\\\vcvars64.bat\\" > null\\n\\n
\\n❗ ❗ ❗ 说实话 ,快是快了, 但是真不好用。
\\n尤其是第三点,非常麻烦。 可能要深入研究了这一块 ,才知道如何去进行这些配置,上手难度太高了。
\\n上手难度高了 ,企业转化起来就更复杂 ,用的人就更少 ,资料也就更少了。
\\n虽然说 AOT 和 GraalVM 这些不咋好用 ,但是就说快没快吧。
\\nSpringBoot3 本身也在做一些优化 ,确实是在处理这一块,只不过主要还是在借用外部的能力。
\\n🤖【前方高能】当你颤抖的手点开这篇文章时——
\\n恭喜你!即将解锁《人类早期驯服HTML的珍贵录像》📹
✅ 看教程像看天书,每个汉字都认识但连起来就...
\\n✅ 永远分不清 <div>
和<span>
谁是霸道总裁
\\n✅ 写代码5分钟,找消失的</p>
花半小时🔍
\\n✅ 别人的网页像科幻片,你的像石器时代🪓
「一看就废」综合症
\\n「学完就忘」拖延症
\\n「标签太多」密集恐惧症
✨ 把HTML标签变成乐高积木的魔法
\\n✨ VS Code隐藏骚操作(让键盘冒火星!)
\\n✨ 连奶奶都能听懂的表格/表单秘籍
\\n✨ 最最最重要的是—— 从此写代码时露出变态の微笑😏
⚠️警告:接下来的内容可能引起极度舒适,建议准备好奶茶/肥宅快乐水,我们马上发车!( • ̀ω•́ )✧
\\n\\n\\n如果把网页比作人体 👩💻
\\n\\n
\\n- 骨骼 = HTML(结构)
\\n- 肌肉 = CSS(样式)
\\n- 灵魂 = JavaScript(交互)
\\n
接下来让我们看一看HTML的基础骨架吧
\\n< html > < head > < body > < meta > < title >
\\n<!DOCTYPE html> ✨防崩声明:告诉浏览器你不是个野鸡页面\\n<html lang=\\"zh\\"> 🌐汉语结界:让浏览器知道你在说中文\\n<head> \\n <meta charset=\\"UTF-8\\"> 🔠中文密码本(防止乱码)\\n <title>我的第一个网页</title> 📛顶栏小标题(SEO加塞小能手)\\n</head>\\n<body> \\n 👻 这里会显示页面内容(用户看得到的地方)\\n</body>\\n</html>\\n\\n
\\nDOCTYPE是document type的缩写,是H5的声明,位于文档最前面,网页必备的组成部分,避免浏览器怪异模式。
\\nhead用于定义文档头部,包含各种属性和信息,包括文档标题,在Web的位置,以及和其他文档的关系,绝大多数文档头部包含的数据不会作为真正的内容展示给读者。
\\n其元素定义在文档的主体,包含文档的所有内容(文本、超链接、图像、表格和列表....)
\\n它会直接在页面显示出来,是用户可以直观看到的内容。
\\n< title >是< head >中唯一必须要求包含的东西,也就是说写head一定要有title
\\n< title > 的增加有利于SEO的优化。
\\nSEO:Search Engine Optimization(搜索引擎优化的缩写)
\\n听起来很高级对吧,其实简单来讲这个东西就是会影响搜索引擎能不能搜到你的网站,你的SEO做的好一点,被搜到的几率就高一点,当然建议用必应hhhhhh,不像国内某搜索引擎,一搜全是野鸡网站😓😓,你说对吧,某d。
\\n其为单标签,用来描述HTML网页文档的属性,例如 charset=‘utf-8’,就是说这个文档的编码是utf-8.
\\n<h1>我是大哥大</h1> 👑字号最大,SEO最重视\\n<h3>我是中不溜秋</h3> 🤏字号适中(你存在感最弱)\\n<h6>最后的倔强</h6> 😭蚂蚁级别的文字\\n
\\n< h1 > ~ < h6 > 六个标题标签。
\\n一次性打出6个标签的魔术方法(Magic~):h$\\\\6
\\n <h1> </h1>\\n <h2> </h2>\\n <h3> </h3>\\n <h4> </h4>\\n <h5> </h5>\\n <h6> </h6>\\n
\\n🚨作死型用法
\\n<h1>这段文字只是要变特大号!</h1> <!-- 浏览器:你是标题刺客吗? --\x3e\\n<h4>这段只是想要倾斜效果</h4> <!-- 搜索引擎:你礼貌吗? --\x3e\\n
\\n🎯 生存法则
\\n王者法则:标题是金字塔👑,不是平头哥!
\\n<!-- 正解 ↓ --\x3e\\n<h1>人类驯服野生HTML实录</h1> \\n<h2>第一章:被div支配的恐惧</h2>\\n<!-- 像剥洋葱一样层层递进 --\x3e\\n
\\nSEO秘籍:正确使用标题 = 在搜索引擎里\\"混脸熟\\"👥
\\n(SEO机器人:这网站标题结构好清晰,多给点流量!)
⚠️ 血的教训:CSS才是管样式的老大!想变大变粗?请找style娘娘~
\\nalign=‘left|center|right\',分别对应将文本放在左、中、右边。
\\n<h2 align=\\"left\\">👈 社恐模式:贴墙站,生怕被人看到</h2> \\n<h1 align=\\"center\\">🎤 C位出道:头顶聚光灯,全场我最靓</h1> \\n<h3 align=\\"right\\">👉 右翼反骨:歪嘴一笑,就爱搞不对称</h3>\\n
\\n段落是通过 < p >来定义的,直接在body写的文本效果和< p >一样,但是不推荐,因为后续要对于文本进行css样式改变。
\\n< br > < /br > (单标签) 换行
\\n <p>你猜猜<br/>我在干嘛</p>\\n <!--关于单标签,建议斜杠写在后面,HTML5可以不写。写在前面可能会被 错误认为是闭合标签。<br/> √ </br> × --\x3e\\n
\\n< hr/> 无论你多么牛b,HR才是决定你能不能进入公司的关键😋,所以 < hr >为分界线。
\\n它可以设置属性如color、width、size、align(默认居中)。
\\n <hr color=\\"red\\" size=\\"10px\\" width=\\"300px\\" align=\\"left\\">\\n <!-- size为分界线的宽度,width为分界线的长度,即延长多少。--\x3e\\n
\\n <img src=\\"猫主子.jpg\\" alt=\\"加载失败显示文字\\" width=\\"500px\\" height=\\"500px\\" title=\'我的朋友圈\'>\\n <!--像极了发朋友圈:没图时显示\\"[图片加载中]\\"--\x3e\\n
\\nimg是单标签,不需要闭合操作
\\nsrc: 图片的路径
\\nalt:规定图像的替代文本(图片显示不出来时,会显示此文本)
\\nwidth:规定图像宽度
\\nheight:规定图像高度
\\ntitle:鼠标悬停在图片上给予提示
\\n <img src=\\"C:\\\\Users\\\\17718\\\\Desktop\\\\素材\\\\11.jpg\\" width=200 px alt=\\"Meow\\" title=\\"Kitty\\">\\n
\\n正常显示:
\\n\\n不正常显示:
\\n鼠标悬停在图片上会有提示:Kitty
<img src=\\"C:\\\\Users\\\\17718\\\\Desktop\\\\素材\\\\11.jpg\\"> \\n <!-- 由盘符开始访问 --\x3e\\n<!-- 浏览器: \\n 你的电脑:已找到!💡 \\n 别人的电脑:C盘?我选择死亡.jpg 💀 \\n--\x3e \\n
\\n翻译成人类语言:
\\n子级关系:/ 从当前目录往下找
\\n父级关系:../ 返回上一级目录往下找
\\n同级关系:./ 在同级目录开始找
\\n翻译一下:我在地球·亚洲·中国·XX市·XX小区3栋1801,
\\n如果我用 / ,则我从1801里开始寻找我要的东西,
\\n我要用 ./ 我就可以从1802,1803....(和1801同级) 开始找我要的东西,
\\n如果我用 ../ 则我从3栋开始找我要的东西...
\\n<img src=\\"https://别人的服务器/土豪图片库.jpg\\"> \\n<!-- 实际效果: \\n 图片正常:薅到羊毛了!🎉 \\n 图片挂了:链接被大佬删库跑路了…💥 \\n--\x3e \\n
\\n人类使用守则:
\\n暴言总结:
\\n< a >用于文字/图片........
\\n <a href=\'www.xxxx.com\'>跳转</a>\\n
\\n利用 target=\'_blank\'
在新标签页内打开(直接在a中写,此非style样式),
text-decoration
=none,去除下划线,
color
: inherit,继承父文本颜色。(这是作者在写某超级复古JSP技术作业时遇到的一种需求)
作用:帮你显示不一样的文字
\\n< em > 定义着重文字
\\n< b > 定义粗体文本
\\n< i > 定义斜体字
\\n< strong > 定义加重语气
\\n< del > 定义删除字
\\n< span > 元素没有特殊的含义
\\n(PS:这些都是闭合标签哦~)
\\n区别:
\\n注意: 常用文本标签和段落是不同的,段落代表一段文本,而文本标签一般表示文本词汇。
\\n简而言之,就是写出来的列表是有序号的呗~
\\n嘿~ 还以为多高级的东西呢,还不如我每天早晨在东直门喝的豆汁儿呢~
\\n <ol>\\n <li> 苹果 </li>\\n <li> 香蕉 </li>\\n <li> 梨 </li>\\n </ol>\\n\\n <!--\\n <ol>的type属性拥有的选项:\\n 1. 1 表示列表项目用数字标号(1,2,3...)\\n 2. a 表示列表项目用小写字母标号(a,b,c..)\\n 3. A 表示列表项目用大写字母标号(A,B,C...)\\n 4. i 表示列表项目用小写罗马字母标号(i,ii,iii...)\\n 5. I 表示列表项目用大写罗马字母标号(I,II,III...)\\n --\x3e\\n
\\n哦!对了!列表也可以进行嵌套哦~
\\n\\n\\n <ol type=\\"A\\">\\n <li>水果</li>\\n <li>\\n <ol type=\\"a\\">\\n <li> 苹果 </li>\\n <li> 香蕉 </li>\\n <li> 梨 </li>\\n </ol>\\n </li>\\n <li>蔬菜</li>\\n </ol>\\n
\\n <ul>\\n <li>水果</li>\\n <li>蔬菜</li>\\n <li>肉</li>\\n </ul>\\n\\n <!--\\n <ul>的type属性拥有的选项:\\n 1. disc 实心圆\\n 2. circle 空心圆\\n 3. square 实心正方形\\n 4. none 无效果\\n\\n --\x3e\\n
\\n\\n当然老实的有序列表可以进行嵌套,我们的无序列表也可以 0v0!
快捷生成:ul>li*3(数字根据想生成的li的数量来修改)
\\n表格组成和特点:
\\n行、列、单元格
\\n单元格特点:同行等高、同列等宽。
\\n表格标签:
\\n< table > 表格
\\n< tr > 行
\\n< td > 列(单元格)
\\n表格属性:
\\nborder:设置表格的边框
\\nwidth:设置表格的宽度
\\nheight:设置表格的高度
\\n*快捷键:table>tr*3>td*2{damn!}
\\n <table>\\n <tr>\\n <td>damn!</td>\\n <td>damn!</td>\\n </tr>\\n <tr>\\n <td>damn!</td>\\n <td>damn!</td>\\n </tr>\\n <tr>\\n <td>damn!</td>\\n <td>damn!</td>\\n </tr>\\n </table>\\n
\\n <!--\\n\\n colspan 左右合并:保留左边内容 col:列,跨列合并,所以是左右合并\\n rowspan 上下合并:保留上面内容 row:行,跨行合并,所以是上下合并\\n\\n --\x3e\\n <table width=\\"40px\\" height=\\"40px\\" border=1>\\n <tr>\\n <td colspan=\\"2\\">damn!</td>\\n \\n </tr>\\n <tr>\\n <td>damn!</td>\\n <td rowspan=\\"2\\">damn!</td>\\n </tr>\\n <tr>\\n <td>damn!</td>\\n \\n </tr>\\n </table>\\n
\\n作用:使得网页具有交互性,例如输入信息并提交。
\\n <form action=\'url\' method=\'post/get\'name=\'myform\'>\\n \\n </form>\\n
\\n/login
)GET | POST | |
---|---|---|
数据传送 | URL明文传送(像裸奔) | 请求体加密(穿隐身衣) |
安全性 | 永远不要用来传密码! | 稍微安全(但也要HTTPS护体) |
容量限制 | URL最长约3000字符(像小书包) | 无限制(像集装箱) |
典型场景 | 搜索关键词、分页参数 | 登录注册、文件上传 |
血泪案例
\\n<!-- 用GET传密码 ↓ 全网直播 ↓ --\x3e\\n<form method=\\"get\\"> \\n<input type=\\"password\\" name=\\"password\\"> \\n<input type=\\"submit\\"></form>\\n
\\n→ 提交后地址栏变身:?password=iloveyou
⚠️社死现场警报!
表单元素: 一般包含三个组成部分:
\\n表单标签(form标签)、表单域(输入框)、表单按钮(提交按钮)。
\\n🛠️ 表单三兄弟の技能树
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n成员 | 必杀技 | 武器库案例 |
---|---|---|
表单标签 | <form> 结界展开 | 设定攻击路线(method)和传送门(action) |
表单域 | <input> 读心术大师 | 文字、密码、按钮...七十二变 |
表单按钮 | <input type=\\"submit\\"> | \\"提交\\"核弹发射按钮 |
表单域,表单域,表单里面的区域就是表单域!嗯,就是这样,比如下面被form标签包裹的区域就是表单域。
\\n <form> \\n <input type=\'text\' name=\'用户名\'> \\n <input type=\'password\' name=\'密码\'>\\n <input type=\'submit\' value=\'提交\'>\\n <!-- 或者 <button type=\'submit\'>提交</button> --\x3e\\n </form>\\n
\\n块级元素 | 内联元素 |
---|---|
霸道总裁(在页面中独占一行,能包养所有元素) | 小鸟依人(行内元素不会独占页面的一行,只占自身的大小 ) |
可以设置width、height属性 | 行内元素设置width和height无效 |
一般块级元素可以包含行内元素和其他块级元素 | 一般行内元素可以包含行内元素而不能包含块级元素 |
\\n\\n常见的块级元素: div、form、h1~h6、hr、p、table、ul 等
\\n
\\n\\n常见的行内元素: a、b、em、i、span、strong 等
\\n
\\n\\n行内块级元素: button、img、input 等 (特点:不换行、能识别宽高)
\\n
display属性用于控制元素的显示方式。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\ndisplay 值 | 描述 | 特点 |
---|---|---|
block | 块级元素 | 独占一行,可设置宽高、内边距、外边距 |
inline | 内联元素 | 不独占一行,宽高由内容决定,无法设置宽高 |
inline-block | 行内块级元素 | 不独占一行,但可以设置宽高 |
none | 隐藏元素 | 元素不显示,且不占据空间 |
记住了没有? 孩子,记不住就对了哈哈哈哈哈,达康书记来了他也记不住啊,这东西太多了,大家不要想着看完就记住,要多看,多用,到了实际生产中多用一用就记住了不是~~~
\\ndiv是容器元素,用来给页面分区。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n作用 | 说明 |
---|---|
分组 | 组织 HTML 结构 |
布局 | 配合 CSS 实现页面布局 |
样式 | 设置颜色、边框、阴影等 |
交互 | 配合 JavaScript 实现动态效果 |
新增标签:header、nav、article、section、silder、footer
\\n注意: 这些新特性在某些老版本的浏览器不支持,所以某些网站为了兼容性也是利用div + id来设计网页。
\\n <!-- 新增前 --\x3e\\n <div id=\'header\'>\\n \\n </div>\\n <div id=\'nav\'>\\n \\n </div>\\n <div id=\'article\'>\\n <div id=\'section\'>\\n \\n </div>\\n </div>\\n \\n <div id=\'slider\'>\\n \\n </div>\\n <div id=\'footer\'>\\n \\n </div>\\n\\n\\n
\\n <!-- 新增后 --\x3e\\n <header></header> 头部\\n <nav></nav> 导航\\n <article> 代表一个独立的、完整的相关内容块、例如一篇完整的论坛帖子、一篇博客、一个用户评论等。\\n <section></section> 定义文档中的节,比如章节、页眉、页脚\\n </article>\\n <aside></aside> 侧边栏\\n <footer></footer> 脚部\\n
\\n至此,你已解锁HTML核心奥义——
\\n从「肉身骨架」般的表格布局,到「话痨达人」的交互表单,
\\n此时的网页虽能传递信息,却仿佛身穿囚服的哲学家:灵魂强大,外表寒酸。
但!真正的暴击即将到来!
\\n下一场代码革命,是给HTML穿上高定时装的魔法——
\\n你将在《CSS:让网页从裸奔到维密大秀的致命诱惑》中:
🦄 警告:
\\n学习CSS后,你可能再也无法直视黑白命令行——
\\n毕竟,谁能拒绝让表格变水晶宫、让输入框发光的诱惑呢?
(下一页PPT正在加载像素...)
","description":"引入 🤖【前方高能】当你颤抖的手点开这篇文章时——\\n 恭喜你!即将解锁《人类早期驯服HTML的珍贵录像》📹\\n\\n😱 你正在经历这些吗?\\n\\n✅ 看教程像看天书,每个汉字都认识但连起来就...\\n ✅ 永远分不清作为一个想打造旅游网页的 “技术小白”,我曾无数次被复杂的代码折磨到崩溃,在寻找稳定服务器资源时焦头烂额,更因地图数据接入的繁琐流程屡屡碰壁。想要呈现出集导航、景点推荐、行程规划于一体的旅游网页,仅凭一己之力,几乎是不可能完成的任务。直到遇见 cursor、腾讯云 Edge MCP 和高德地图 MCP2.0,我的困境才迎来转机 —— 它们就像为我量身定制的 “救星”,以各自的强大功能,填补了我技术与资源上的所有缺口,让梦想中的旅游网页从空想变为现实。
\\n\\n腾讯云 EdgeOne Pages MCP 是基于边缘计算和大语言模型的新型开发工具\\n用户在代码编辑器中通过自然语言指令,就能让 AI 生成完整的 HTML 网页内容,并自动调用 EdgeOne 边缘计算 API,将网站快速部署到 CDN 节点,生成可公开访问的链接。例如,只需在编辑器里用 “人话” 跟大模型说 “帮我生成一个特定主题的页面并帮我直接部署”,无需手动编写代码或配置服务器,即可快速完成网页的生成与部署,对编程新手和经验丰富的开发者都非常友好。
高德地图 MCP 2.0 是高德地图开放平台于 2025 年 4 月 30 日正式发布的全新版本,它实现了 AI 攻略与地图应用的深度融合,全面升级了用户出行体验
一键生成专属地图:用户可一键将 AI 生成的攻略内容转化为专属地图,攻略中的点位、描述、行程规划等个性化信息将自动整合,形成用户私有地图,实现了从攻略查看到导航、打车、订票的流畅转换。\\n
深度理解用户需求:能够深度理解用户的出行需求,制定出行计划,并自动生成高德地图 APP 内的专属地图,为用户提供更精准、贴心的服务,就像拥有一个 “真正懂你的出行秘书”。
动态地图与联动按钮:允许用户在出行计划中直接嵌入动态地图和与高德地图 APP 联动的功能按钮,用户在出行过程中无需再切换应用,便可随时查看最新的位置信息和导航指引,简化了操作步骤,实现 “行前 - 行中 - 行后” 全流程覆盖。\\n1、专属地图 Tools:充分理解用户出行场景诉求,制定出行计划,并在高德地图APP生成专属地图,满足用户打车、导航、酒店预订、门票预订、餐厅预订、加油充电等一系列出行服务需求。
2、唤端Tools & 动态地图:开发者可以使用高德MCP,直接在出行计划中载入动态地图,并嵌入与高德地图APP联动按钮,实现一键导航、打车等功能。
\\n我们打开cursor进入到CurSor Setting\\n
将下面的代码输入进去
\\n{\\n \\"mcpServers\\": {\\n \\"amap-amap-sse\\": {\\n \\"url\\": \\"https://mcp.amap.com/sse?key=您在高德官网上申请的key\\"\\n }\\n }\\n}\\n
\\n这里的我们需要去申请一个自己的API KEYS了\\n点击进入到高德平台\\n进行注册并且登录操作\\n在高德平台右上角进行应用的创建\\n\\n创建好了系统之后我们进行添加Key的操作\\n
\\n这里的话我们就选择了Web服务了\\n
\\n然后点击提交就能看到我们的API Keys了\\n
\\n为了对 Key 的安全有效管理,请妥善保管你的 Key。
然后获取到了API之后,我们将这个API替换到原代码中的部分
\\n配置好了之后并且链接成功了就是这个样子的\\n
在腾讯云开发者MCP广场找到我们的EdgeOne Pages\\n\\n点击进去就会有详细的解释,关于EdgeOne Mcp的相关信息\\n
\\n我们将配置MCP的json代码复制
{\\n \\"mcpServers\\": {\\n \\"edgeone-pages-mcp-server\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\"edgeone-pages-mcp\\"]\\n }\\n }\\n}\\n
\\n和上面的方式一样,在trae中添加MCP,成功之后就是这个样子的\\n
我们对ai说:
\\n\\n\\n我现在想去北京玩几天,第一题天想去天安门,第二天想去故宫,第三天想去颐和园,第四天想去长城,帮制作旅行攻略,考虑出行时间和路线,以及天气状况路线规划。制作网页地图自定义绘制旅游路线和位置。网页使用简约美观页面风格,景区图片以卡片展示。行程规划结果在高德地图app展示,并集成到h5页面中。同一天行程景区之间我想打车前往。生成的文件名字叫trive.html。我需要的在网页上展示的地图是想通过 自定义添加,景点标记和一些路线图并不是之间嵌入PC地图,并且最后使用edge进行网页的部署操作
\\n
在我们的第一轮对话还是比较顺利的,他利用我们的高德MCP获取了北京的最近的几天天气信息\\n\\n但是我们这里还不是很满意的,我需要将路线更加清晰,并且生成的程序可以在手机高德地图上打开
我们第二轮继续对话
\\n\\n\\n我需要你在地图的左下角进行第几天第几天路程的安排,并且地图中的路线以不同的颜色进行安排。并且这个html我可以在手机的高德地图上进行打开,你帮我把代码重新修改下,并且使用edgeone进行网页的部署操作\\n
\\n
\\n
\\n但是生成出代码还是存在差错,地图无法显示,我还发现这个每个景点的下方介绍太少了,我们在第三轮对话这么说
\\n\\n我觉得这个太单调了,可以对这四天的每个景点做一个规划,更详细点,比如说附近有啥玩的有啥吃的,介绍的更加详细点。并且这个地图似乎没有显示出来了,你再根据要求优化下代码
\\n
\\n这里我们再给AI添点料,我想在手机端访问链接生成我的个人地图,并且可以分享给我的家人
\\n\\n能否根据需求,生成可视化网页的同时,调用高德地图APP生成专属地图,邀请同行好友进入专属地图,共享攻略、位置,同时基于专属地图使用高德各种出行服务\\n
\\n\\n
下面我们看看效果\\n\\n在手机上我们能通过点击相关的信息生成个人高德地图,并且可以进行路线分享、打车服务,根据专门的路线行程特定的方案\\n
\\n
\\n点击相关的链接可以生成个人地图,记录相关的地图信息以及快速进行打车的各种操作,更加方便了,不用像之前一样到处去搜索\\n
链接访问地址:\\nmcp.edgeone.site/share/4WifH…
\\n\\n<!DOCTYPE html>\\n<html lang=\\"zh-CN\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>北京4日游行程规划</title>\\n <link rel=\\"stylesheet\\" href=\\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css\\">\\n <link href=\\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css\\" rel=\\"stylesheet\\">\\n <script type=\\"text/javascript\\" src=\\"https://webapi.amap.com/maps?v=2.0&key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\\"></script>\\n <style>\\n body {\\n font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, \\"Helvetica Neue\\", Arial, sans-serif;\\n line-height: 1.6;\\n color: #333;\\n background-color: #f8f9fa;\\n }\\n .container {\\n max-width: 1200px;\\n margin: 0 auto;\\n padding: 15px;\\n }\\n .header {\\n text-align: center;\\n margin-bottom: 20px;\\n padding: 20px;\\n background: white;\\n border-radius: 15px;\\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\\n }\\n .map-container {\\n position: relative;\\n height: 70vh;\\n margin-bottom: 20px;\\n border-radius: 15px;\\n overflow: hidden;\\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\\n }\\n #container {\\n height: 100%;\\n width: 100%;\\n }\\n .legend {\\n position: absolute;\\n bottom: 20px;\\n left: 20px;\\n background: white;\\n padding: 10px;\\n border-radius: 8px;\\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\\n z-index: 1000;\\n }\\n .legend-item {\\n display: flex;\\n align-items: center;\\n margin: 5px 0;\\n }\\n .legend-color {\\n width: 20px;\\n height: 3px;\\n margin-right: 8px;\\n }\\n .schedule-card {\\n background: white;\\n border-radius: 15px;\\n padding: 20px;\\n margin-bottom: 20px;\\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\\n transition: transform 0.2s;\\n }\\n .schedule-card:hover {\\n transform: translateY(-5px);\\n }\\n .schedule-card img {\\n width: 100%;\\n height: 200px;\\n object-fit: cover;\\n border-radius: 12px;\\n margin-bottom: 15px;\\n }\\n .info-section {\\n margin-top: 15px;\\n padding: 15px;\\n background-color: #fff;\\n border-radius: 8px;\\n border-left: 4px solid;\\n }\\n .info-section.attractions {\\n border-left-color: #1890ff;\\n }\\n .info-section.food {\\n border-left-color: #f5222d;\\n }\\n .info-section.tips {\\n border-left-color: #52c41a;\\n }\\n .weather-info {\\n background-color: #e9ecef;\\n padding: 12px;\\n border-radius: 8px;\\n margin-top: 10px;\\n }\\n .route-info {\\n margin-top: 15px;\\n padding: 12px;\\n background-color: #f8f9fa;\\n border-radius: 8px;\\n }\\n .open-amap-btn {\\n display: block;\\n width: 100%;\\n padding: 12px;\\n background: #1890ff;\\n color: white;\\n text-align: center;\\n border-radius: 8px;\\n text-decoration: none;\\n margin-top: 15px;\\n transition: background 0.3s;\\n }\\n .open-amap-btn:hover {\\n background: #096dd9;\\n color: white;\\n }\\n .section-title {\\n font-size: 1.1rem;\\n font-weight: 600;\\n margin-bottom: 10px;\\n color: #1890ff;\\n }\\n .food-item, .attraction-item {\\n margin-bottom: 10px;\\n padding-left: 20px;\\n position: relative;\\n }\\n .food-item:before, .attraction-item:before {\\n content: \\"•\\";\\n position: absolute;\\n left: 0;\\n color: #1890ff;\\n }\\n @media (max-width: 768px) {\\n .container {\\n padding: 10px;\\n }\\n .map-container {\\n height: 50vh;\\n }\\n .legend {\\n bottom: 10px;\\n left: 10px;\\n font-size: 12px;\\n }\\n }\\n .view-full-trip {\\n margin: 20px 0;\\n }\\n .view-full-trip .btn {\\n padding: 15px;\\n font-size: 1.1rem;\\n font-weight: 500;\\n border-radius: 10px;\\n box-shadow: 0 4px 6px rgba(255, 107, 0, 0.2);\\n transition: all 0.3s ease;\\n }\\n .view-full-trip .btn:hover {\\n transform: translateY(-2px);\\n box-shadow: 0 6px 8px rgba(255, 107, 0, 0.3);\\n }\\n .transport-info {\\n background: white;\\n padding: 20px;\\n border-radius: 15px;\\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\\n }\\n .transport-info .btn {\\n padding: 12px;\\n font-weight: 500;\\n background-color: #FF6B00;\\n border: none;\\n transition: all 0.3s ease;\\n }\\n .transport-info .btn:hover {\\n background-color: #FF8534;\\n transform: translateY(-2px);\\n }\\n @media (max-width: 768px) {\\n .transport-info .row > div {\\n margin-bottom: 10px;\\n }\\n .view-full-trip .btn {\\n font-size: 1rem;\\n padding: 12px;\\n }\\n }\\n .custom-map-section {\\n margin: 20px 0;\\n }\\n .custom-map-card {\\n background: white;\\n padding: 25px;\\n border-radius: 15px;\\n box-shadow: 0 4px 6px rgba(0,0,0,0.1);\\n }\\n .custom-map-features {\\n margin-top: 20px;\\n }\\n .custom-map-btn {\\n width: 100%;\\n padding: 15px;\\n font-size: 1.2rem;\\n background-color: #FF6B00;\\n border: none;\\n margin-bottom: 20px;\\n transition: all 0.3s ease;\\n }\\n .custom-map-btn:hover {\\n background-color: #FF8534;\\n transform: translateY(-2px);\\n box-shadow: 0 6px 8px rgba(255, 107, 0, 0.3);\\n }\\n .feature-grid {\\n display: grid;\\n grid-template-columns: repeat(2, 1fr);\\n gap: 15px;\\n margin-top: 20px;\\n }\\n .feature-item {\\n display: flex;\\n align-items: center;\\n padding: 15px;\\n background: #f8f9fa;\\n border-radius: 10px;\\n transition: all 0.3s ease;\\n }\\n .feature-item:hover {\\n background: #e9ecef;\\n transform: translateY(-2px);\\n }\\n .feature-item i {\\n font-size: 1.5rem;\\n color: #FF6B00;\\n margin-right: 10px;\\n }\\n .feature-item span {\\n font-size: 1rem;\\n color: #333;\\n font-weight: 500;\\n }\\n @media (max-width: 768px) {\\n .feature-grid {\\n grid-template-columns: 1fr;\\n }\\n .custom-map-btn {\\n font-size: 1rem;\\n padding: 12px;\\n }\\n }\\n </style>\\n</head>\\n<body>\\n <div class=\\"container\\">\\n <div class=\\"header\\">\\n <h1>北京4日游行程规划</h1>\\n <p class=\\"text-muted\\">精心规划的北京文化之旅</p>\\n </div>\\n\\n <div class=\\"map-container\\">\\n <div id=\\"container\\"></div>\\n <div class=\\"legend\\">\\n <div class=\\"legend-item\\">\\n <div class=\\"legend-color\\" style=\\"background: #FF4D4F\\"></div>\\n <span>第一天:天安门</span>\\n </div>\\n <div class=\\"legend-item\\">\\n <div class=\\"legend-color\\" style=\\"background: #1890FF\\"></div>\\n <span>第二天:故宫</span>\\n </div>\\n <div class=\\"legend-item\\">\\n <div class=\\"legend-color\\" style=\\"background: #52C41A\\"></div>\\n <span>第三天:颐和园</span>\\n </div>\\n <div class=\\"legend-item\\">\\n <div class=\\"legend-color\\" style=\\"background: #722ED1\\"></div>\\n <span>第四天:八达岭长城</span>\\n </div>\\n </div>\\n </div>\\n\\n <!-- 添加专属地图功能区 --\x3e\\n <div class=\\"custom-map-section mb-4\\">\\n <div class=\\"row\\">\\n <div class=\\"col-12\\">\\n <div class=\\"custom-map-card\\">\\n <h3 class=\\"section-title\\">\\n <i class=\\"fas fa-map-marked-alt\\"></i> 专属地图\\n </h3>\\n <div class=\\"custom-map-features\\">\\n <a href=\\"amapuri://workInAmap/createWithToken?polymericId=mcp_74e1c260269c470493d51c0c71570dce&from=MCP\\" class=\\"btn btn-primary btn-lg btn-block custom-map-btn\\">\\n <i class=\\"fas fa-map\\"></i> 打开专属地图\\n </a>\\n <div class=\\"feature-grid\\">\\n <div class=\\"feature-item\\">\\n <i class=\\"fas fa-users\\"></i>\\n <span>邀请好友同游</span>\\n </div>\\n <div class=\\"feature-item\\">\\n <i class=\\"fas fa-location-arrow\\"></i>\\n <span>实时位置共享</span>\\n </div>\\n <div class=\\"feature-item\\">\\n <i class=\\"fas fa-route\\"></i>\\n <span>路线导航</span>\\n </div>\\n <div class=\\"feature-item\\">\\n <i class=\\"fas fa-car\\"></i>\\n <span>打车服务</span>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- 添加完整行程查看按钮 --\x3e\\n <div class=\\"view-full-trip\\">\\n <a href=\\"androidamap://route/plan/?dlat=40.359876&dlon=116.024067&dname=八达岭长城&slat=39.90923&slon=116.397428&sname=天安门&way=0,1,2,3&dev=0&t=0\\" class=\\"btn btn-primary btn-lg btn-block mb-4\\" style=\\"background-color: #FF6B00; border: none;\\">\\n <i class=\\"fas fa-map-marked-alt\\"></i> 在高德地图App中查看完整行程\\n </a>\\n </div>\\n\\n <!-- 添加景点间交通信息 --\x3e\\n <div class=\\"transport-info mb-4\\">\\n <h3 class=\\"section-title\\" style=\\"color: #333;\\">\\n <i class=\\"fas fa-car\\"></i> 景点间交通\\n </h3>\\n <p class=\\"text-muted mb-3\\">景点之间可打车前往,单程约15-20分钟。</p>\\n <div class=\\"row g-3\\">\\n <div class=\\"col-md-4\\">\\n <a href=\\"androidamap://route/plan/?dlat=39.917544&dlon=116.403414&dname=故宫&slat=39.90923&slon=116.397428&sname=天安门&dev=0&t=0\\" class=\\"btn btn-warning w-100\\">\\n 打车去故宫博物院\\n </a>\\n </div>\\n <div class=\\"col-md-4\\">\\n <a href=\\"androidamap://route/plan/?dlat=39.999493&dlon=116.275177&dname=颐和园&slat=39.917544&slon=116.403414&sname=故宫&dev=0&t=0\\" class=\\"btn btn-warning w-100\\">\\n 打车去颐和园\\n </a>\\n </div>\\n <div class=\\"col-md-4\\">\\n <a href=\\"androidamap://route/plan/?dlat=40.359876&dlon=116.024067&dname=八达岭长城&slat=39.999493&slon=116.275177&sname=颐和园&dev=0&t=0\\" class=\\"btn btn-warning w-100\\">\\n 打车去八达岭长城\\n </a>\\n </div>\\n </div>\\n </div>\\n\\n <div class=\\"row\\">\\n <!-- Day 1 --\x3e\\n <div class=\\"col-md-6 col-lg-3\\">\\n <div class=\\"schedule-card\\">\\n <h3>第一天 - 天安门</h3>\\n <img src=\\"http://store.is.autonavi.com/showpic/17a36a737908810a310387c7d53e878a\\" alt=\\"天安门\\">\\n <div class=\\"weather-info\\">\\n <i class=\\"fas fa-sun\\"></i> 天气:晴\\n <br>\\n 温度:23°C / 12°C\\n </div>\\n <div class=\\"info-section attractions\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-landmark\\"></i> 景点介绍</div>\\n <p>天安门是中华人民共和国的象征,也是世界上最大的城市广场之一。这里不仅有着深厚的历史文化底蕴,更承载着无数重要的历史时刻。</p>\\n <div class=\\"attraction-item\\">国旗护卫队升旗仪式(建议清晨观看)</div>\\n <div class=\\"attraction-item\\">人民英雄纪念碑</div>\\n <div class=\\"attraction-item\\">毛主席纪念堂</div>\\n <div class=\\"attraction-item\\">天安门城楼</div>\\n </div>\\n <div class=\\"info-section food\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-utensils\\"></i> 美食推荐</div>\\n <div class=\\"food-item\\">全聚德烤鸭(前门店)</div>\\n <div class=\\"food-item\\">东来顺涮羊肉</div>\\n <div class=\\"food-item\\">护国寺小吃(特色小吃)</div>\\n <div class=\\"food-item\\">都一处烧麦(老字号)</div>\\n </div>\\n <div class=\\"info-section tips\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-lightbulb\\"></i> 游玩贴士</div>\\n <ul>\\n <li>早晨6点可以观看升旗仪式</li>\\n <li>参观毛主席纪念堂需要携带身份证</li>\\n <li>广场禁止携带大型背包</li>\\n <li>建议参观时间:2-3小时</li>\\n </ul>\\n </div>\\n <div class=\\"route-info\\">\\n <p><strong>建议游览时间:</strong> 2-3小时</p>\\n <p><strong>交通方式:</strong> 地铁1号线天安门东站下</p>\\n </div>\\n <a href=\\"androidamap://route/plan/?dlat=39.90923&dlon=116.397428&dname=天安门&dev=0&t=0\\" class=\\"open-amap-btn\\">\\n <i class=\\"fas fa-map-marker-alt\\"></i> 在高德地图中打开\\n </a>\\n </div>\\n </div>\\n\\n <!-- Day 2 --\x3e\\n <div class=\\"col-md-6 col-lg-3\\">\\n <div class=\\"schedule-card\\">\\n <h3>第二天 - 故宫</h3>\\n <img src=\\"http://store.is.autonavi.com/showpic/ae0a73885ccb64f09b73adef515c8c28\\" alt=\\"故宫\\">\\n <div class=\\"weather-info\\">\\n <i class=\\"fas fa-sun\\"></i> 天气:晴\\n <br>\\n 温度:25°C / 15°C\\n </div>\\n <div class=\\"info-section attractions\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-landmark\\"></i> 景点介绍</div>\\n <p>故宫是中国最大的古代宫殿建筑群,也是世界上保存最完整、规模最大的木质宫殿建筑。</p>\\n <div class=\\"attraction-item\\">太和殿(金銮殿)</div>\\n <div class=\\"attraction-item\\">乾清宫</div>\\n <div class=\\"attraction-item\\">御花园</div>\\n <div class=\\"attraction-item\\">珍宝馆</div>\\n </div>\\n <div class=\\"info-section food\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-utensils\\"></i> 美食推荐</div>\\n <div class=\\"food-item\\">故宫角楼咖啡厅</div>\\n <div class=\\"food-item\\">景山公园小吃街</div>\\n <div class=\\"food-item\\">南池子大街美食</div>\\n <div class=\\"food-item\\">文华酒店中餐厅</div>\\n </div>\\n <div class=\\"info-section tips\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-lightbulb\\"></i> 游玩贴士</div>\\n <ul>\\n <li>提前在网上预约门票</li>\\n <li>建议从南到北参观</li>\\n <li>周一闭馆(除法定节假日)</li>\\n <li>建议参观时间:4-5小时</li>\\n </ul>\\n </div>\\n <div class=\\"route-info\\">\\n <p><strong>建议游览时间:</strong> 4-5小时</p>\\n <p><strong>交通方式:</strong> 天安门广场步行可达</p>\\n </div>\\n <a href=\\"androidamap://route/plan/?dlat=39.917544&dlon=116.403414&dname=故宫&dev=0&t=0\\" class=\\"open-amap-btn\\">\\n <i class=\\"fas fa-map-marker-alt\\"></i> 在高德地图中打开\\n </a>\\n </div>\\n </div>\\n\\n <!-- Day 3 --\x3e\\n <div class=\\"col-md-6 col-lg-3\\">\\n <div class=\\"schedule-card\\">\\n <h3>第三天 - 颐和园</h3>\\n <img src=\\"http://store.is.autonavi.com/showpic/415dba21b4b140c18dca5af366930b3a\\" alt=\\"颐和园\\">\\n <div class=\\"weather-info\\">\\n <i class=\\"fas fa-cloud-sun\\"></i> 天气:多云\\n <br>\\n 温度:27°C / 17°C\\n </div>\\n <div class=\\"info-section attractions\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-landmark\\"></i> 景点介绍</div>\\n <p>颐和园是中国现存最大、保存最完整的皇家园林,被誉为\\"皇家园林博物馆\\"。</p>\\n <div class=\\"attraction-item\\">佛香阁</div>\\n <div class=\\"attraction-item\\">长廊</div>\\n <div class=\\"attraction-item\\">昆明湖</div>\\n <div class=\\"attraction-item\\">玉澜堂</div>\\n </div>\\n <div class=\\"info-section food\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-utensils\\"></i> 美食推荐</div>\\n <div class=\\"food-item\\">颐和园仿膳</div>\\n <div class=\\"food-item\\">江南村(农家菜)</div>\\n <div class=\\"food-item\\">北大西门小吃街</div>\\n <div class=\\"food-item\\">苏州街美食</div>\\n </div>\\n <div class=\\"info-section tips\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-lightbulb\\"></i> 游玩贴士</div>\\n <ul>\\n <li>建议购买联票</li>\\n <li>可以乘船游览昆明湖</li>\\n <li>建议早晨或傍晚游览</li>\\n <li>建议参观时间:4-5小时</li>\\n </ul>\\n </div>\\n <div class=\\"route-info\\">\\n <p><strong>建议游览时间:</strong> 4-5小时</p>\\n <p><strong>交通方式:</strong> 地铁4号线颐和园站下</p>\\n </div>\\n <a href=\\"androidamap://route/plan/?dlat=39.999493&dlon=116.275177&dname=颐和园&dev=0&t=0\\" class=\\"open-amap-btn\\">\\n <i class=\\"fas fa-map-marker-alt\\"></i> 在高德地图中打开\\n </a>\\n </div>\\n </div>\\n\\n <!-- Day 4 --\x3e\\n <div class=\\"col-md-6 col-lg-3\\">\\n <div class=\\"schedule-card\\">\\n <h3>第四天 - 八达岭长城</h3>\\n <img src=\\"http://store.is.autonavi.com/showpic/73e202337e15794120c2004ef9ced36d\\" alt=\\"八达岭长城\\">\\n <div class=\\"weather-info\\">\\n <i class=\\"fas fa-sun\\"></i> 天气:晴\\n <br>\\n 温度:27°C / 17°C\\n </div>\\n <div class=\\"info-section attractions\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-landmark\\"></i> 景点介绍</div>\\n <p>八达岭长城是万里长城中最具代表性的一段,也是最早对外开放的长城景区。</p>\\n <div class=\\"attraction-item\\">北八楼</div>\\n <div class=\\"attraction-item\\">南七楼</div>\\n <div class=\\"attraction-item\\">八达岭长城博物馆</div>\\n <div class=\\"attraction-item\\">长城电影城</div>\\n </div>\\n <div class=\\"info-section food\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-utensils\\"></i> 美食推荐</div>\\n <div class=\\"food-item\\">军营锅盔</div>\\n <div class=\\"food-item\\">农家院(农家菜)</div>\\n <div class=\\"food-item\\">长城饭店</div>\\n <div class=\\"food-item\\">关东煮(冬季)</div>\\n </div>\\n <div class=\\"info-section tips\\">\\n <div class=\\"section-title\\"><i class=\\"fas fa-lightbulb\\"></i> 游玩贴士</div>\\n <ul>\\n <li>建议早晨出发避开人流</li>\\n <li>可以选择缆车上下</li>\\n <li>准备舒适的登山鞋</li>\\n <li>建议参观时间:4-5小时</li>\\n </ul>\\n </div>\\n <div class=\\"route-info\\">\\n <p><strong>建议游览时间:</strong> 4-5小时</p>\\n <p><strong>交通方式:</strong> S2线八达岭长城站下</p>\\n </div>\\n <a href=\\"androidamap://route/plan/?dlat=40.359876&dlon=116.024067&dname=八达岭长城&dev=0&t=0\\" class=\\"open-amap-btn\\">\\n <i class=\\"fas fa-map-marker-alt\\"></i> 在高德地图中打开\\n </a>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <script>\\n // 初始化地图\\n var map = new AMap.Map(\'container\', {\\n zoom: 10,\\n center: [116.397428, 39.90923],\\n mapStyle: \'amap://styles/fresh\'\\n });\\n\\n // 景点坐标和路线颜色\\n const routes = [\\n {\\n name: \'天安门\',\\n position: [116.397428, 39.90923],\\n day: 1,\\n color: \'#FF4D4F\'\\n },\\n {\\n name: \'故宫\',\\n position: [116.403414, 39.917544],\\n day: 2,\\n color: \'#1890FF\'\\n },\\n {\\n name: \'颐和园\',\\n position: [116.275177, 39.999493],\\n day: 3,\\n color: \'#52C41A\'\\n },\\n {\\n name: \'八达岭长城\',\\n position: [116.024067, 40.359876],\\n day: 4,\\n color: \'#722ED1\'\\n }\\n ];\\n\\n // 添加标记和路线\\n for (let i = 0; i < routes.length; i++) {\\n const current = routes[i];\\n const next = routes[i + 1];\\n\\n // 添加标记\\n const marker = new AMap.Marker({\\n position: current.position,\\n title: current.name,\\n label: {\\n content: `第${current.day}天`,\\n direction: \'top\'\\n }\\n });\\n\\n const content = `\\n <div class=\\"info-window\\" style=\\"padding: 15px;\\">\\n <h4 style=\\"margin-bottom: 10px;\\">第${current.day}天 - ${current.name}</h4>\\n <p style=\\"margin-bottom: 5px;\\">点击卡片查看详细行程</p>\\n </div>\\n `;\\n\\n const infoWindow = new AMap.InfoWindow({\\n content: content,\\n offset: new AMap.Pixel(0, -30)\\n });\\n\\n marker.on(\'click\', () => {\\n infoWindow.open(map, current.position);\\n });\\n\\n map.add(marker);\\n\\n // 如果有下一个点,绘制路线\\n if (next) {\\n const polyline = new AMap.Polyline({\\n path: [current.position, next.position],\\n strokeColor: current.color,\\n strokeWeight: 4,\\n strokeStyle: \\"solid\\",\\n lineJoin: \'round\',\\n showDir: true\\n });\\n\\n map.add(polyline);\\n }\\n }\\n\\n // 自适应显示所有点\\n map.setFitView();\\n </script>\\n <script src=\\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js\\"></script>\\n</body>\\n</html> \\n
\\n在旅游网页开发这片充满挑战的 “技术丛林” 里,我们常常因代码的晦涩、资源的匮乏而迷失方向。本文就像一位经验丰富的向导,引领我们走进 cursor、腾讯云 Edge MCP 和高德地图 MCP 2.0 构建的奇妙世界。开篇抛出技术融合的 “魔法石”,从痛点的 “泥沼” 中拉起苦苦挣扎的开发者。
\\n接着,腾讯云 Edge MCP 化身贴心的代码精灵,以自然语言驱动开发,让网页部署如施魔法般快捷;高德地图 MCP 2.0 则是智慧的出行伙伴,一键生成专属地图,精准规划行程,让出行体验全面升级。
\\n最后,通过实际呈现的旅游网页成果,导航的灵动指引、景点购票的便捷操作,就像一场精彩的魔术表演,向我们证明这三大工具携手,能将开发难题化作绚丽的技术烟火,不仅让旅游网页开发变得轻松有趣,更点亮了用户出行体验的璀璨星空,开启了旅游与技术融合的梦幻新旅程!
","description":"作为一个想打造旅游网页的 “技术小白”,我曾无数次被复杂的代码折磨到崩溃,在寻找稳定服务器资源时焦头烂额,更因地图数据接入的繁琐流程屡屡碰壁。想要呈现出集导航、景点推荐、行程规划于一体的旅游网页,仅凭一己之力,几乎是不可能完成的任务。直到遇见 cursor、腾讯云 Edge MCP 和高德地图 MCP2.0,我的困境才迎来转机 —— 它们就像为我量身定制的 “救星”,以各自的强大功能,填补了我技术与资源上的所有缺口,让梦想中的旅游网页从空想变为现实。 关于Edge MCP的介绍\\n\\n腾讯云 EdgeOne Pages MCP…","guid":"https://juejin.cn/post/7502278666211557413","author":"Undoom","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T15:29:29.185Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d758f02c8d254fb7886447a72660889b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=7RJ7zJFlPke84GYk8ozle1AhKJo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6157dcca2ce34c9fae58cc41512745ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=5QGc%2Fk%2BBymhNMRff2HxIffQzA38%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f332b09e30844804a731c1289590a287~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=8VUgpdPeQ7n64%2FNsSrPbO3vVeic%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/657e025a0797435cacf4949042124ea6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=V2HaQzTNb1grSg6VNU9mynynkwQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6fc62ecbb618470c90c04db672ef39f3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=1xD7u%2Bqm%2Bj3VREH3jmYi6DV6p3Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1cd11ad4dd8247b094747e330faa43d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=8H4XpnzyRIQ9FZntefOOYeBKMos%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a869e3ae86248e5a105bb4459a1e596~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=lOVv%2B1Y01vvGd%2F7QQIrkRD8QAAo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3c7042a0cdbf4ca0ba188db02ec1c8d2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=KhRdV8230Fsl1tfDO3dRvxC8vkQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2979263b837a4023b85147cb0293dc49~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=AnarXMNSt2MJV0Z6F%2B3EiIXeCg0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d823c1586974488a1abc2c80df93d3e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=Ni4bRVJ36gbSylqcVB3PgP6PJiQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c55dc58d867f4d03a47296f6a5606138~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=D4IhR6K7F5MIHkHqm0IdWbnGQJg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6102fd089f1b4140a63c3e7753aed68b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=p2te2O4weTNXbLe63XgjSMu%2FcZU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a90de9196c9c427bba91a6c9bbe7c6f6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=B87otH7XlpyM3nD%2F%2B3E0%2F7ScGDY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9d68a2327db4a6c88e25ac25cd9ea2f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=%2F6gDRVjtmRAkhZTIOsZ%2Bt0%2Fz%2BuM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9fed820237b2433296f02ffc38ca760e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=NzzFJnIUglkQ0LNSluYNDhVIGRg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ac2be3d3b37d4872be91d625a88ce3ae~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=5Of1JWR368KC3ST6SaOL8kgEHdk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/471101b7c7284356a3490dd4b2587d35~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=bfAGxqzeU4Yt0yKAtuGWE0IhNAs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c489cf6d51a9494290d51fe979cb75f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=qt0nRHiDbymllMkT5%2Bqv5xjEEE0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2862b6936acb46b1a5b76b86d46bb5cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=8ClThguQActlu1hux2e%2FuPfaOPU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/468a299d89c5432b82893a8d684a496b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=%2BRMqEfggIlxC31kJQIq9v6dCrXA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/790816853d754ad2ac560516e43b2f77~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=E7OXqW8oANVtdnjzCwgTXPPqFFs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a4be9a1d2ae0426b9323439b4dd21d54~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=wAAMno%2FC%2BCJ%2FlPiNBsxcswi4FIw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e4d8dbf59414fa49b59ea811fa6d267~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=8bupqj2bMR2%2BquHI9t3M%2BGG6di8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/62a86e98ba3e43d1aa9418ba7a1c26e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=gqZ9Iy%2F%2BktzMMMHEqObVH%2B8Nh7o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1b8ef0ef1d04018b429d9720a504edd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=0%2Fdx3oz9g%2BJ1pSDsbc71OGQvu2g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/42f41296cc7b480993c4f51c32195a3b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=NCcSpcZGTJEr%2B27%2B6NBIp0hK9Og%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d7de8031cb140f2a36dbe6cb45388f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=2jzW%2F%2BRKMJ%2BNSz2T9KWIvpWQAU4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e3c8952de1114e6081076a56b1f61768~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgVW5kb29t:q75.awebp?rk3s=f64ab15b&x-expires=1747409369&x-signature=BBtts6r6CPZmtWhgGxhSFdj01fg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","MCP"],"attachments":null,"extra":null,"language":null},{"title":"美团面试题:MySQL事务ACID是如何实现的?","url":"https://juejin.cn/post/7502272729144770570","content":"文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\nMySQ技术内幕第5版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n深入浅出MySQL:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n高性能MySQL第三版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
在 MySQL 的 InnoDB 存储引擎中,事务需要满足 ACID(原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability)特性。MySQL 主要通过 Undo Log、Redo Log、Binlog、MVCC、锁机制 等手段来实现 ACID。
\\n原子性指的是事务中的所有操作要么全部成功,要么全部失败回滚。
\\n✅ 通过 Undo Log(撤销日志)
\\nBEGIN;\\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\\nROLLBACK; -- 触发原子性回滚\\n
\\nROLLBACK
时,Undo Log 用于恢复 balance
的原始值。一致性指的是事务执行后,数据库应从一个一致状态转换到另一个一致状态。
\\n✅ 通过数据库约束
\\n✅ 通过 ACID 机制共同保障
\\nBEGIN;\\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\\nCOMMIT;\\n
\\n保证一致性的机制
\\n隔离性指的是多个事务并发执行时,互不干扰,避免脏读、不可重复读、幻读等问题。
\\n✅ 通过事务隔离级别
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n隔离级别 | 解决问题 | 实现方式 |
---|---|---|
读未提交(Read Uncommitted) | 可能发生脏读 | 无隔离 |
读已提交(Read Committed) | 解决脏读 | 每次查询生成新 ReadView |
可重复读(Repeatable Read) | 解决脏读、不可重复读 | 事务开始时生成 ReadView |
串行化(Serializable) | 解决所有问题(但性能低) | 事务间强制加锁,串行执行 |
✅ 通过 MVCC(多版本并发控制)
\\n✅ 通过锁机制
\\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\\nBEGIN;\\nSELECT balance FROM accounts WHERE id = 1; -- 读取时创建 ReadView\\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\\nCOMMIT;\\n
\\n隔离性保证 :
\\nSELECT
之后即使有其他事务更新 balance
,事务内的 balance
值不会变化。持久性指的是事务提交后,数据必须永久保存,即使系统崩溃,数据也不会丢失。
\\n✅ 通过 Redo Log(重做日志)
\\n✅ 通过 Binlog(归档日志)
\\nBinlog
,确保事务提交后数据不会丢失。Binlog
可以用来重放事务。BEGIN;\\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\\nCOMMIT; -- 事务提交,刷新 Redo Log\\n
\\nACID 特性 | 实现机制 |
---|---|
原子性(Atomicity) | Undo Log(回滚日志) |
一致性(Consistency) | 事务 ACID 机制共同保障 + 约束(外键、主键等) |
隔离性(Isolation) | 事务隔离级别 + MVCC + 锁机制 |
持久性(Durability) | Redo Log(崩溃恢复)+ Binlog(数据复制) |
✅ 提高事务持久性
\\nSET GLOBAL innodb_flush_log_at_trx_commit = 1; -- 每次提交时刷新 Redo Log\\nSET GLOBAL sync_binlog = 1; -- 事务提交时同步写入 Binlog\\n
\\n✅ 优化事务隔离性
\\nREAD COMMITTED
,避免间隙锁,提高并发能力。REPEATABLE READ
确保数据一致性。✅ 减少 Undo Log 开销
\\nUndo Log
过大,占用大量存储。✅ 优化数据一致性
\\nALTER TABLE
)会导致事务隐式提交,应注意事务边界。MySQL 的事务 ACID 主要通过 日志(Undo Log、Redo Log、Binlog)、MVCC、事务隔离级别、锁机制 来实现:
\\nUndo Log
,保证事务失败时数据回滚。Redo Log
和 Binlog
,保证崩溃恢复能力。很多小伙伴在工作中遇到拦截需求就无脑写HandlerInterceptor,结果被复杂场景搞得鼻青脸肿。
\\n作为一名有多年开发经验的程序员,今天领大家到SpringBoot的山头认认6把交椅:
\\n这篇文章以梁山为背景的介绍SpringBoot中的拦截器,可能更通俗易懂。
\\n希望对你会有所帮助,记得点赞和收藏。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\nFilter是梁山中的总寨主。
\\n典型战斗场面:全局鉴权/接口耗时统计
\\n@WebFilter(\\"/*\\") \\npublic class CostFilter implements Filter {\\n @Override\\n public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {\\n long start = System.currentTimeMillis();\\n chain.doFilter(req, res); // 放行江湖令箭\\n System.out.println(\\"接口耗时:\\"+(System.currentTimeMillis()-start)+\\"ms\\");\\n }\\n}\\n
\\n起义缘由:必须是最高寨主,因为他在Servlet容器滚刀肉层面出手。想当年有个兄弟在Filter里调用Spring Bean,结果NPE错杀千人(要用WebApplicationContextUtils拿Bean才是正解)
\\nHandlerInterceptor是梁山中的二当家。
\\n必杀场景:接口权限验证/请求参数自动装填
\\npublic class AuthInterceptor implements HandlerInterceptor {\\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\\n String token = request.getHeader(\\"X-Token\\");\\n if(!\\"vip666\\".equals(token)){\\n response.setStatus(403);\\n returnfalse; // 关门放狗\\n }\\n returntrue;\\n }\\n}\\n\\n// 衙门张贴告示\\n@Configuration\\npublicclass WebMvcConfig implements WebMvcConfigurer {\\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n registry.addInterceptor(new AuthInterceptor())\\n .addPathPatterns(\\"/api/**\\")\\n .excludePathPatterns(\\"/api/login\\");\\n }\\n}\\n
\\n二当家的雷区:
\\nAOP是梁山中的军师智多星。
\\n运筹帷幄场景:服务层方法缓存/事务管理
\\n@Aspect\\n@Component\\npublic class CacheAspect {\\n @Around(\\"@annotation(com.example.anno.Cacheable)\\")\\n public Object aroundCache(ProceedingJoinPoint jp) {\\n String cacheKey = buildKey(jp);\\n Object cacheVal = redisTemplate.opsForValue().get(cacheKey);\\n if(cacheVal != null) return cacheVal;\\n \\n Object result = jp.proceed();\\n redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);\\n return result;\\n }\\n}\\n
\\n军师锦囊:
\\nRestTemplate是梁山中的水军头领。
\\n远程战事:统一添加请求头/加密请求参数
\\npublic class TraceInterceptor implements ClientHttpRequestInterceptor {\\n @Override\\n public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) {\\n request.getHeaders().add(\\"X-TraceId\\", UUID.randomUUID().toString());\\n return execution.execute(request, body);\\n }\\n}\\n\\n// 注册水军\\n@Bean\\npublic RestTemplate restTemplate() {\\n RestTemplate rt = new RestTemplate();\\n rt.getInterceptors().add(new TraceInterceptor());\\n return rt;\\n}\\n
\\n总督黑历史:
\\nFeign拦截器是梁山中的外交使节。
\\n出使外国:统一签名计算/Header透传
\\npublic class FeignAuthInterceptor implements RequestInterceptor {\\n @Override\\n public void apply(RequestTemplate template) {\\n template.header(\\"Authorization\\", \\"Bearer \\" + SecurityContext.getToken());\\n }\\n}\\n\\n// 缔结合约\\n@Configuration\\npublicclass FeignConfig {\\n @Bean\\n public FeignAuthInterceptor feignAuthInterceptor() {\\n returnnew FeignAuthInterceptor();\\n }\\n}\\n
\\n使节烫手山芋:
\\nWebFilter是梁山中的特种兵。
\\n闪电战场景:响应式编程统一编码/跨域处理
\\n@Component\\npublic class CorsWebFilter implements WebFilter {\\n @Override\\n public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\\n ServerHttpResponse response = exchange.getResponse();\\n response.getHeaders().add(\\"Access-Control-Allow-Origin\\", \\"*\\");\\n return chain.filter(exchange);\\n }\\n}\\n
\\n作战条件:
\\n最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。
\\n门派 | 攻击范围 | 招式复杂度 | 内力消耗 | 首选战场 |
---|---|---|---|---|
Filter | 全局最外层 | ★★☆☆☆ | 低 | 安全校验/日志记录 |
Handler | MVC控制器层 | ★★★☆☆ | 中 | 权限控制 |
AOP | 业务方法级 | ★★★★☆ | 高 | 缓存/事务 |
RestTemplate | HTTP客户端 | ★★★☆☆ | 中 | 服务间调用 |
Feign | 声明式客户端 | ★★★★☆ | 高 | 微服务通信 |
WebFilter | 响应式全链路 | ★★★★★ | 极高 | WebFlux应用 |
Filter -> Interceptor -> AOP ,越早拦截越省力(但别在Filter里做业务)
\\n用Arthas监控拦截链路耗时,避免拦截器连环夺命call
\\n# 查看HandlerInterceptor耗时\\ntrace *.preHandle \'#cost>10\'\\n \\n# 诊断AOP切面\\nwatch com.example.aop.*Aspect * \'{params,returnObj}\' -x 3\\n
\\n最后送给各位江湖儿女一句话:
\\n拦截是门艺术,别让好刀砍了自己人!
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n很多小伙伴在工作中遇到拦截需求就无脑写HandlerInterceptor,结果被复杂场景搞得鼻青脸肿。\\n\\n作为一名有多年开发经验的程序员,今天领大家到SpringBoot的山头认认6把交椅:\\n\\n这篇文章以梁山为背景的介绍SpringBoot中的拦截器,可能更通俗易懂。\\n\\n希望对你会有所帮助,记得点赞和收藏。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。\\n\\n第一把交椅:Filter\\n\\nFilter是梁山中的总寨主…","guid":"https://juejin.cn/post/7501994018013315113","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T03:16:34.759Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0a4622703a1e45afb292738917d8e7c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747365394&x-signature=tAx%2FFb76SQZVfick84%2Fk2cmyAes%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Spring 6.0+Boot 3.0:秒级启动、万级并发的开发新姿势","url":"https://juejin.cn/post/7501994018012807209","content":"最低JDK 17: 全面拥抱Java模块化特性,优化现代JVM性能
\\n虚拟线程(Loom项目): 轻量级线程支持高并发场景(需JDK 19+)
\\n// 示例:虚拟线程使用\\nThread.ofVirtual().name(\\"my-virtual-thread\\").start(() -> {\\n // 业务逻辑\\n});\\n
\\n虚拟线程(Project Loom)
\\n应用场景: 电商秒杀系统、实时聊天服务等高并发场景
\\n// 传统线程池 vs 虚拟线程\\n// 旧方案(平台线程)\\nExecutorService executor = Executors.newFixedThreadPool(200);\\n// 新方案(虚拟线程)\\nExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();\\n// 处理10000个并发请求\\nIntStream.range(0, 10000).forEach(i -> \\n virtualExecutor.submit(() -> {\\n // 处理订单逻辑\\n processOrder(i);\\n })\\n);\\n
\\n@HttpExchange注解: 类似Feign的声明式REST调用
\\n@HttpExchange(url = \\"/api/users\\")\\npublic interface UserClient {\\n @GetExchange\\n List<User> listUsers();\\n}\\n
\\n应用场景: 微服务间API调用
\\n@HttpExchange(url = \\"/products\\", accept = \\"application/json\\")\\npublicinterface ProductServiceClient {\\n @GetExchange(\\"/{id}\\")\\n Product getProduct(@PathVariable String id);\\n @PostExchange\\n Product createProduct(@RequestBody Product product);\\n}\\n// 自动注入使用\\n@Service\\npublicclass OrderService {\\n @Autowired\\n private ProductServiceClient productClient;\\n \\n public void validateProduct(String productId) {\\n Product product = productClient.getProduct(productId);\\n // 校验逻辑...\\n }\\n}\\n
\\nRFC 7807标准: 标准化错误响应格式
\\n{\\n \\"type\\": \\"https://example.com/errors/insufficient-funds\\",\\n \\"title\\": \\"余额不足\\",\\n \\"status\\": 400,\\n \\"detail\\": \\"当前账户余额为50元,需支付100元\\"\\n}\\n
\\n应用场景: 统一API错误响应格式
\\n@RestControllerAdvice\\npublicclass GlobalExceptionHandler {\\n @ExceptionHandler(ProductNotFoundException.class)\\n public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {\\n ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);\\n problem.setType(URI.create(\\"/errors/product-not-found\\"));\\n problem.setTitle(\\"商品不存在\\");\\n problem.setDetail(\\"商品ID: \\" + ex.getProductId());\\n return problem;\\n }\\n}\\n// 触发异常示例\\n@GetMapping(\\"/products/{id}\\")\\npublic Product getProduct(@PathVariable String id) {\\n return productRepo.findById(id)\\n .orElseThrow(() -> new ProductNotFoundException(id));\\n}\\n
\\nAOT编译优化: 启动时间缩短至毫秒级,内存占用降低50%+
\\n编译命令示例:
\\nnative-image -jar myapp.jar\\n
\\nJakarta EE 9+: 包名javax
→jakarta
全量替换
自动配置优化: 更智能的条件装配策略
\\nOAuth2授权服务器 应用场景: 构建企业级认证中心
\\n# application.yml配置\\nspring:\\n security:\\n oauth2:\\n authorization-server:\\n issuer-url: https://auth.yourcompany.com\\n token:\\n access-token-time-to-live: 1h\\n
\\n定义权限端点
\\n@Configuration\\n@EnableWebSecurity\\npublic class AuthServerConfig {\\n @Bean\\n public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {\\n http\\n .authorizeRequests(authorize -> authorize\\n .anyRequest().authenticated()\\n )\\n .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);\\n return http.build();\\n }\\n}\\n
\\n应用场景: 云原生Serverless函数
\\n# 打包命令(需安装GraalVM)\\nmvn clean package -Pnative\\n
\\n运行效果对比:
\\nMicrometer 1.10+: 支持OpenTelemetry
标准
全新/actuator/prometheus端点: 原生Prometheus
格式指标
应用场景: 微服务健康监测
\\n// 自定义业务指标\\n@RestController\\npublic class OrderController {\\n private final Counter orderCounter = Metrics.counter(\\"orders.total\\");\\n @PostMapping(\\"/orders\\")\\n public Order createOrder() {\\n orderCounter.increment();\\n // 创建订单逻辑...\\n }\\n}\\n# Prometheus监控指标示例\\norders_total{application=\\"order-service\\"} 42\\nhttp_server_requests_seconds_count{uri=\\"/orders\\"} 15\\n
\\n// 商品查询服务(组合使用新特性)\\n@RestController\\npublicclass ProductController {\\n // 声明式调用库存服务\\n @Autowired\\n private StockServiceClient stockClient;\\n // 虚拟线程处理高并发查询\\n @GetMapping(\\"/products/{id}\\")\\n public ProductDetail getProduct(@PathVariable String id) {\\n return CompletableFuture.supplyAsync(() -> {\\n Product product = productRepository.findById(id)\\n .orElseThrow(() -> new ProductNotFoundException(id));\\n \\n // 并行查询库存\\n Integer stock = stockClient.getStock(id);\\n returnnew ProductDetail(product, stock);\\n }, Executors.newVirtualThreadPerTaskExecutor()).join();\\n }\\n}\\n
\\nSpring Boot 3.x
→ 再启用Spring 6特性spring-boot-properties-migrator
检测配置变更通过以上升级方案:
\\nProblemDetail
统一异常格式Prometheus
监控接口性能本次升级标志着Spring生态正式进入云原生时代。
\\n重点关注: 虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向
\\n如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
\\n关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!
","description":"点击上方“程序员蜗牛g”,选择“设为星标”跟蜗牛哥一起,每天进步一点点程序员蜗牛g大厂程序员一枚 跟蜗牛一起 每天进步一点点31篇原创内容**公众号 一、Spring 6.0核心特性详解\\n1. Java版本基线升级\\n\\n最低JDK 17: 全面拥抱Java模块化特性,优化现代JVM性能\\n\\n虚拟线程(Loom项目): 轻量级线程支持高并发场景(需JDK 19+)\\n\\n// 示例:虚拟线程使用\\nThread.ofVirtual().name(\\"my-virtual-thread\\").start(() -> {\\n // 业务逻辑\\n});\\n\\n\\n虚拟线程…","guid":"https://juejin.cn/post/7501994018012807209","author":"程序员蜗牛","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T02:32:18.098Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a898d699b1fb48089edf0843c7acafac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6JyX54mb:q75.awebp?rk3s=f64ab15b&x-expires=1747362738&x-signature=wpUl2Zmwk%2FkejiJ2EkQFBANta%2Bs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f2b9d25005f042f48aea3eb2083621f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6JyX54mb:q75.awebp?rk3s=f64ab15b&x-expires=1747362738&x-signature=ZXyjdOegthpxnqhrTpKtEyEzJd0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/be4c6d005ad6466bb8a90e10a2f3098b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6JyX54mb:q75.awebp?rk3s=f64ab15b&x-expires=1747362738&x-signature=Bm8JxuDgvNDtlPFePq1jVK9WJ9Q%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Hibernate Validator:自定义注解与校验器,为枚举数据校验赋能","url":"https://juejin.cn/post/7501944090591592511","content":"\\n\\n数据校验在数据处理中起到保驾护航的作用,它能有效确保数据的准确性、完整性和一致性,从而为后续分析和决策提供可靠依据,避免因错误数据导致的不良后果。
\\n本文将详细介绍一下
\\nHibernate Validator
,并详细说说如何自定义扩展。
Hibernate Validator 是一个实现 Java Bean Validation (JSR 380) 规范的校验框架。
\\n它提供了丰富的内置校验注解,支持自定义校验逻辑,能够无缝集成到 Spring Boot 和其他 Java 框架中。
\\nJSR 380 是 Java 的标准校验规范,定义了一套统一的校验机制。它是 Java EE 和 Java SE 的一部分,用于通过注解(如 @NotNull、@Min 和 @Max)来验证 Java Bean 的属性是否符合特定条件。JSR 380 是 JSR 303 的升级版本,需要 Java 8 或更高版本,并利用了 Java 8 的特性,例如类型注解和对 Optional 和 LocalDate 等类的支持。
\\nHibernate Validator 是该规范的参考实现,完全兼容规范定义的校验注解。
\\n通过使用 JSR 380,开发者可以更优雅地实现参数校验,避免硬编码校验逻辑,从而提高代码的可维护性和可读性。
\\n丰富的内置注解:提供了如 @NotNull
、@Size
、@Email
等常用校验注解。
灵活的自定义校验:支持自定义注解和校验器,满足复杂业务需求。
\\n集成友好:与 Spring Boot、Spring MVC 等框架无缝集成。
\\n性能高效:校验逻辑在编译时生成,运行时性能优异。
\\n在 Spring Boot 项目中,Hibernate Validator 是默认集成的,无需额外添加依赖。
\\n如果是纯 Java 项目,需要手动添加依赖:
\\n<dependency>\\n <groupId>org.hibernate.validator</groupId>\\n <artifactId>hibernate-validator</artifactId>\\n <version>8.0.1.Final</version>\\n</dependency>\\n
\\nmaven仓库地址:hibernate-validator
\\n示例代码:
\\nimport javax.validation.constraints.Email;\\nimport javax.validation.constraints.NotBlank;\\nimport javax.validation.constraints.Size;\\n\\npublic class User {\\n @NotBlank(message = \\"用户名不能为空\\")\\n @Size(min = 3, max = 50, message = \\"用户名长度必须在3到50之间\\")\\n private String username;\\n\\n @Email(message = \\"邮箱格式不正确\\")\\n private String email;\\n\\n // 省略 getter 和 setter 方法\\n}\\n
\\n@Valid
或 @Validated
注解触发校验。@RestController\\npublic class UserController {\\n @PostMapping(\\"/user\\")\\n public ResponseEntity<?> createUser(@Valid @RequestBody User user) {\\n return ResponseEntity.ok(\\"用户创建成功\\");\\n }\\n}\\n
\\n捕获校验异常:
\\n@ControllerAdvice
捕获校验异常并返回统一的错误响应。@ControllerAdvice\\npublic class GlobalExceptionHandler {\\n @ExceptionHandler(MethodArgumentNotValidException.class)\\n public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex) {\\n Map<String, String> errors = new HashMap<>();\\n ex.getBindingResult().getAllErrors().forEach(error -> {\\n String fieldName = ((FieldError) error).getField();\\n String errorMessage = error.getDefaultMessage();\\n errors.put(fieldName, errorMessage);\\n });\\n return ResponseEntity.badRequest().body(errors);\\n }\\n}\\n
\\n内置注解无法满足所有业务需求,例如枚举值校验、复杂逻辑校验等。
\\n自定义注解可以扩展校验功能,提高代码的复用性和可维护性。
\\nimport javax.validation.Constraint;\\nimport javax.validation.Payload;\\nimport java.lang.annotation.ElementType;\\nimport java.lang.annotation.Retention;\\nimport java.lang.annotation.RetentionPolicy;\\nimport java.lang.annotation.Target;\\n\\n/**\\n * 枚举值校验注解\\n *\\n * @author bamboo panda\\n * @version 1.0\\n * @date 2025/5/7 13:54\\n */\\n@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})\\n@Retention(RetentionPolicy.RUNTIME)\\n@Constraint(validatedBy = EnumCheckValidator.class)\\npublic @interface EnumCheck {\\n\\n String message() default \\"\\";\\n\\n Class<?>[] groups() default {};\\n\\n Class<? extends Payload>[] payload() default {};\\n\\n Class<? extends Enum<?>> enumClass();\\n\\n String enumMethod();\\n}\\n
\\nimport javax.validation.ConstraintValidator;\\nimport javax.validation.ConstraintValidatorContext;\\nimport java.lang.reflect.Method;\\nimport java.lang.reflect.Modifier;\\n\\n/**\\n * 枚举值校验器\\n *\\n * @author bamboo panda\\n * @version 1.0\\n * @date 2025/5/7 13:56\\n */\\npublic class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {\\n\\n private Class<? extends Enum<?>> enumClass;\\n private String enumMethod;\\n\\n\\n @Override\\n public void initialize(EnumCheck enumCheck) {\\n enumMethod = enumCheck.enumMethod();\\n enumClass = enumCheck.enumClass();\\n\\n }\\n\\n @Override\\n public boolean isValid(Object value, ConstraintValidatorContext context) {\\n if (null == value) {\\n return Boolean.FALSE;\\n }\\n if (enumClass == null || enumMethod == null) {\\n return Boolean.FALSE;\\n }\\n\\n Class<?> valueClass = value.getClass();\\n try {\\n Method method = enumClass.getMethod(enumMethod, valueClass);\\n if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {\\n throw new RuntimeException(String.format(\\"%s method return is not boolean type in the %s class\\", enumMethod, enumClass));\\n }\\n\\n if (!Modifier.isStatic(method.getModifiers())) {\\n throw new RuntimeException(String.format(\\"%s method is not static method in the %s class\\", enumMethod, enumClass));\\n }\\n\\n Boolean result = (Boolean) method.invoke(null, value);\\n return result == null ? false : result;\\n } catch (NoSuchMethodException | SecurityException e) {\\n throw new RuntimeException(String.format(\\"This %s(%s) method does not exist in the %s\\", enumMethod, valueClass, enumClass), e);\\n } catch (Exception e) {\\n throw new RuntimeException(e);\\n }\\n }\\n}\\n
\\n注意:这里主要关注isValid方法,是枚举注解里面使用的方法名称
\\n/**\\n * 送达状态枚举\\n *\\n * @author bamboo panda\\n * @version 1.0\\n * @date 2025/5/7 14:00\\n */\\npublic enum ServiceStatusEnum {\\n\\n PEPPERY(0, \\"待送达\\"),\\n NOT_PEPPERY(1, \\"已送达\\"),\\n ALL_IS_OK(2, \\"未送达\\");\\n\\n private Integer value;\\n\\n private String name;\\n\\n ServiceStatusEnum(Integer value, String name) {\\n this.value = value;\\n this.name = name;\\n }\\n\\n public Integer getValue() {\\n return value;\\n }\\n\\n public String getName() {\\n return name;\\n }\\n\\n /**\\n * 判断参数合法性\\n *\\n * @param value 值\\n * @return true 合法 false 不合法\\n */\\n public static boolean isValid(Integer value) {\\n ServiceStatusEnum[] values = ServiceStatusEnum.values();\\n for (ServiceStatusEnum tempEnum : values) {\\n if (tempEnum.getValue().compareTo(value) == 0) {\\n return true;\\n }\\n }\\n return false;\\n }\\n\\n}\\n
\\n/**\\n * 送达状态 0待送达 1已送达 2未送达\\n */\\n@NotNull(message = \\"不能为空\\", groups = {Edit.class})\\n@EnumCheck(message = \\"送达状态不合法\\", enumClass = ServiceStatusEnum.class, enumMethod = \\"isValid\\", groups = {Edit.class})\\nprivate Integer serviceStatus;\\n
\\n注意:我们的注解校验器里面,为空默认是不行的,所以这里也可以不要@NotNull
到这里已经介绍清楚了Hibernate Validator是干嘛的?它给我们提供了丰富的内置注解,方便了我们快速优雅的开发。
\\n虽然内置注解丰富,但无法满足所有业务需求。例如,对于枚举值的校验、复杂逻辑的校验等,内置注解可能无能为力。
\\n这个时候就需要我们自己去扩展,这个扩展也非常简单,俗话说得好,自己动手,丰衣足食。
\\n\\n","description":"引言 数据校验在数据处理中起到保驾护航的作用,它能有效确保数据的准确性、完整性和一致性,从而为后续分析和决策提供可靠依据,避免因错误数据导致的不良后果。\\n\\n本文将详细介绍一下 Hibernate Validator,并详细说说如何自定义扩展。\\n\\n一、认识Hibernate Validator\\n\\n1.1 什么是Hibernate Validator?\\n\\nHibernate Validator 是一个实现 Java Bean Validation (JSR 380) 规范的校验框架。\\n\\n它提供了丰富的内置校验注解,支持自定义校验逻辑,能够无缝集成到…","guid":"https://juejin.cn/post/7501944090591592511","author":"竹子爱揍功夫熊猫","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T02:01:37.082Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/46a30e03917844fabc42d3e9854cc9f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1747360897&x-signature=iM7z7eF7GEj%2B9o13t6sxK3fdi1I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/336d69e66180435490d38002bc2c93bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1747360897&x-signature=4jBIXP60Drp5I%2F1jvcoHcvvl2hY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/652ebf5dce874c72b83b5271f9383e48~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1747360897&x-signature=ZjrUzUvRhGnw8Q8Dm5VK5MtFdAI%3D","type":"photo"},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1133c4e4c763453fb1fb002b5c19a56c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1747360897&x-signature=irKWjt8yQObj%2FqcnYx5jKLaPWIU%3D","type":"photo"}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot中的9个自带实用过滤器","url":"https://juejin.cn/post/7501903720378007615","content":"最后:如果你从本篇文章中学到一点点东西,麻烦发财的小手点一下赞,如有疑问或者错误,欢迎提出和指正。
\\n
在SpringBoot应用中,过滤器(Filter)是处理HTTP请求和响应的重要组件,它们能在请求到达控制器前或响应返回客户端前进行拦截处理。
\\nSpringBoot自带了许多实用的过滤器,如字符编码、跨域请求、缓存控制等。
\\nCharacterEncodingFilter
是SpringBoot中最常用的过滤器之一,它确保HTTP请求和响应使用正确的字符编码,避免出现乱码问题。
在SpringBoot应用中,该过滤器默认已启用,使用UTF-8编码。你可以通过以下属性进行自定义配置:
\\n# application.properties\\nserver.servlet.encoding.charset=UTF-8\\nserver.servlet.encoding.force=true\\nserver.servlet.encoding.enabled=true\\n
\\n@Bean\\npublic FilterRegistrationBean<CharacterEncodingFilter> characterEncodingFilter() {\\n CharacterEncodingFilter filter = new CharacterEncodingFilter();\\n filter.setEncoding(\\"UTF-8\\");\\n filter.setForceEncoding(true);\\n \\n FilterRegistrationBean<CharacterEncodingFilter> registrationBean = new FilterRegistrationBean<>();\\n registrationBean.setFilter(filter);\\n registrationBean.addUrlPatterns(\\"/*\\");\\n registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);\\n return registrationBean;\\n}\\n
\\n实际应用:当你的应用需要处理多语言内容,特别是包含中文、日文、阿拉伯文等非ASCII字符时,此过滤器能确保数据在传输过程中不会出现乱码。
\\nHTML表单只支持GET和POST方法,但RESTful API通常需要PUT、DELETE等HTTP方法。HiddenHttpMethodFilter
通过识别表单中的隐藏字段来模拟这些HTTP方法。
# 默认是开启的,如需禁用可设置为false\\nspring.mvc.hiddenmethod.filter.enabled=true\\n
\\n<form action=\\"/users/123\\" method=\\"post\\">\\n <input type=\\"hidden\\" name=\\"_method\\" value=\\"DELETE\\"/>\\n <button type=\\"submit\\">删除用户</button>\\n</form>\\n
\\n上述表单提交后,过滤器将把POST请求转换为DELETE请求,路由到对应的删除处理方法。
\\n@DeleteMapping(\\"/users/{id}\\")\\npublic String deleteUser(@PathVariable Long id) {\\n userService.deleteUser(id);\\n return \\"redirect:/users\\";\\n}\\n
\\n实际应用:当你构建不使用JavaScript的传统Web应用,而又希望遵循RESTful设计原则时,这个过滤器非常有用。
\\nFormContentFilter
允许处理非POST请求(如PUT、PATCH等)中的表单数据,使这些请求的表单数据能像POST请求一样被解析。
spring.mvc.formcontent.filter.enabled=true\\n
\\n当客户端需要通过PUT请求更新资源,并以表单形式提交数据时,该过滤器能确保SpringMVC正确解析请求数据。
\\n@PutMapping(\\"/users/{id}\\")\\npublic String updateUser(@PathVariable Long id, UserForm form) {\\n // 没有FormContentFilter时,form对象的属性将无法被正确填充\\n userService.updateUser(id, form);\\n return \\"redirect:/users\\";\\n}\\n
\\nRequestContextFilter
创建并维护一个RequestContext对象,使其在整个请求处理过程中可用,便于访问特定于请求的信息。
@Component\\npublic class RequestInfoService {\\n \\n public String getClientInfo() {\\n ServletRequestAttributes attributes = \\n (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\\n HttpServletRequest request = attributes.getRequest();\\n \\n return String.format(\\"Client IP: %s, User-Agent: %s\\", \\n request.getRemoteAddr(), \\n request.getHeader(\\"User-Agent\\"));\\n }\\n}\\n
\\n实际应用:当你需要在非Controller组件(如Service层)中访问当前HTTP请求信息时,此过滤器提供的功能非常有用。
\\nCorsFilter
实现了跨域资源共享(CORS)规范,允许浏览器向不同域的服务器发送请求,解决同源策略的限制。
@Bean\\npublic CorsFilter corsFilter() {\\n CorsConfiguration config = new CorsConfiguration();\\n config.setAllowCredentials(true);\\n config.addAllowedOrigin(\\"https://example.com\\");\\n config.addAllowedHeader(\\"*\\");\\n config.addAllowedMethod(\\"*\\");\\n \\n UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\\n source.registerCorsConfiguration(\\"/api/**\\", config);\\n \\n return new CorsFilter(source);\\n}\\n
\\n也可通过属性配置:
\\nspring.web.cors.allowed-origins=https://example.com\\nspring.web.cors.allowed-methods=GET,POST,PUT,DELETE\\nspring.web.cors.allowed-headers=Authorization,Content-Type\\nspring.web.cors.allow-credentials=true\\n
\\n实际应用:当你的前端应用和API部署在不同域名下时,如前端在example.com,API在api.example.com,CORS过滤器是必不可少的。
\\nShallowEtagHeaderFilter
自动为HTTP响应添加ETag头信息,帮助客户端实现高效的缓存策略,减少不必要的网络传输。
此过滤器通过计算响应内容的哈希值生成ETag,当客户端再次请求相同资源时,可以通过比对ETag决定是否返回304(Not Modified)状态码。
\\n@Bean\\npublic FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {\\n FilterRegistrationBean<ShallowEtagHeaderFilter> registration = new FilterRegistrationBean<>();\\n registration.setFilter(new ShallowEtagHeaderFilter());\\n registration.addUrlPatterns(\\"/api/*\\");\\n registration.setName(\\"shallowEtagHeaderFilter\\");\\n return registration;\\n}\\n
\\n实际应用:当你的应用提供大量静态内容或不频繁变化的API响应时,此过滤器能显著减少带宽使用并提高响应速度。
\\n在使用负载均衡器或反向代理时,ForwardedHeaderFilter
能处理转发的头信息,确保应用能正确识别客户端的原始信息。
@Bean\\npublic FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {\\n FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>();\\n registration.setFilter(new ForwardedHeaderFilter());\\n registration.setOrder(Ordered.HIGHEST_PRECEDENCE);\\n return registration;\\n}\\n
\\n实际应用:当你的SpringBoot应用部署在Nginx或AWS ELB等反向代理后面时,此过滤器能确保生成的URL和重定向使用正确的协议、主机名和端口。
\\nOrderedRequestContextFilter
是RequestContextFilter
的扩展版本,实现了Ordered
接口,使其在过滤器链中的执行顺序可以更精确控制。
FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000
当你有多个过滤器,且它们之间存在依赖关系时,OrderedRequestContextFilter
能确保在正确的时机执行请求上下文初始化。
ResourceUrlEncodingFilter
主要用于处理静态资源的版本化URL,特别是在使用资源指纹或版本策略时。
@Configuration\\npublic class WebConfig implements WebMvcConfigurer {\\n \\n @Bean\\n public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {\\n return new ResourceUrlEncodingFilter();\\n }\\n \\n @Override\\n public void addResourceHandlers(ResourceHandlerRegistry registry) {\\n registry.addResourceHandler(\\"/resources/**\\")\\n .addResourceLocations(\\"classpath:/static/\\")\\n .setCachePeriod(3600)\\n .resourceChain(true)\\n .addResolver(new VersionResourceResolver()\\n .addContentVersionStrategy(\\"/**\\"));\\n }\\n}\\n
\\n在模板中使用:
\\n<!-- Thymeleaf --\x3e\\n<link rel=\\"stylesheet\\" th:href=\\"@{/resources/css/main.css}\\"/>\\n\\n<!-- 渲染后可能变为 --\x3e\\n<link rel=\\"stylesheet\\" href=\\"/resources/css/main-d41d8cd98f00b204e9800998ecf8427e.css\\"/>\\n
\\n实际应用:当你需要实现高效的前端资源缓存策略,特别是在部署新版本时确保用户获取最新资源而不受浏览器缓存影响时,此过滤器非常有用。
\\n在实际项目中,根据应用需求选择合适的过滤器,并正确配置它们的执行顺序,将极大地提升应用的质量和开发效率。
","description":"在SpringBoot应用中,过滤器(Filter)是处理HTTP请求和响应的重要组件,它们能在请求到达控制器前或响应返回客户端前进行拦截处理。 SpringBoot自带了许多实用的过滤器,如字符编码、跨域请求、缓存控制等。\\n\\n1. CharacterEncodingFilter - 字符编码过滤器\\n\\nCharacterEncodingFilter是SpringBoot中最常用的过滤器之一,它确保HTTP请求和响应使用正确的字符编码,避免出现乱码问题。\\n\\n功能和配置\\n\\n在SpringBoot应用中,该过滤器默认已启用,使用UTF-8编码…","guid":"https://juejin.cn/post/7501903720378007615","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T23:55:06.899Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"【Trae编程】AI自动化实践:爬取小红书热门话题","url":"https://juejin.cn/post/7501892144794681354","content":"我正在参加Trae「超级体验官」创意实践征文,本文所使用的 Trae 免费下载链接:www.trae.com.cn/?utm_source…
\\n在人工智能快速发展的时代,AI技术不仅重塑了传统行业,也极大提高了开发者的工作效率。腾讯云推出的 MCP(Model Context Protocol) ,作为一种创新的技术协议,能够帮助开发者将云能力、AI模型与自动化流程高效整合,让开发者可以将更多精力集中在业务逻辑和创新上,而不再是重复性工作。
\\n最近,腾讯云推出了**MCP广场,一个帮助开发者轻松创建与管理自动化应用的平台。在探索过程中,我发现了一个非常有趣的工具——超浏览器AI自动化**,它能让开发者通过浏览器模拟技术实现自动化操作。我曾经看到过一个小红书爬取的代码,但由于种种原因没有成功运行。于是,我决定尝试利用超浏览器AI自动化功能,结合腾讯云MCP的能力,进行小红书热门话题的自动化爬取与分析,最终成功实现了这一目标。
\\n通过本次实践,成功构建了一个自动化流程,它能够:
\\n组件 | 用途 | 工具 |
---|---|---|
用户输入 | 提供创作方向 | 自然语言输入 |
MCP能力平台 | 提供发现、管理和调用各类 MCP 工具的平台。是整个自动化实践的入口。 | 腾讯云MCP |
MCP LLM工具链 | 解析输入并生成话题 | Trae |
超浏览器自动化 | 模拟浏览器行为,获取平台信息 | Playwright/Selenium |
数据处理与输出 | 格式化生成内容,提供创作灵感 | markdown |
如前所述,对于小红书这类大量使用JavaScript动态加载内容的网站,传统的静态HTML解析方法往往无法获取到完整的页面信息。
\\n以下是我发现并运行过过的爬取小红书的代码(未能成功运行):
\\nimport requests\\nfrom bs4 import BeautifulSoup\\n\\nheaders = {\'User-Agent\':\'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36\'}\\nurl = \'https://www.xiaohongshu.com/explore\' \\n\\nres = requests.get(url, headers=headers)\\nsoup = BeautifulSoup(res.text,\'html.parser\')\\n\\nhot_list = []\\nfor item in soup.select(\'.hot-item\'):\\n title = item.select(\'.title\')[0].text.strip()\\n hot_value = item.select(\'.hot-value\')[0].text\\n hot_list.append(f\\"{title}🔥{hot_value}\\")\\n\\nprint(\\"实时热榜TOP10:\\", hot_list[:10])\\n
\\n之前看到的代码如下(来源):
\\n这段代码的逻辑是发送一个HTTP请求,然后使用 BeautifulSoup
解析返回的HTML文本,尝试通过CSS选择器(.hot-item
, .title
, .hot-value
)来定位热门话题元素。
然而,当我尝试在本地运行这段代码时,并没有得到预期的热门话题列表:
\\n运行了个寂寞。哈哈哈哈。
\\n1.打开腾讯云MCP广场(点击前往了解详情),点击浏览器自动化,找到超浏览器AI自动化。
\\n2.在超浏览器AI自动化页面可以看到安装,使用,工具,配置以及许可证。
\\n3.根据说明,进入Hyperbrowser官网,找到API配置页面。按照官方文档的指导,创建并配置 API_KEY
,再到Trae CN中进行配置,就可以开始使用了。
代码如下(修改API_KEY):
\\n{\\n \\"mcpServers\\": {\\n \\"hyperbrowser\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\"-y\\", \\"hyperbrowser-mcp\\"],\\n \\"env\\": {\\n \\"HYPERBROWSER_API_KEY\\": \\"API_KEY\\"\\n }\\n }\\n }\\n}\\n
\\n配置成功的截图如下:
\\nTrae CN 是一个强大的开发工具,它集成了 AI 能力和 MCP 调用能力,允许我们通过自然语言或代码来 orchestrate 自动化流程。
\\n新建一个文件夹,命名为:浏览器自动化,使用Trae CN打开这个文件夹
\\n使用Ctrl+U
唤醒对话,在对话中选择Builder with MCP
,使用下面的提示词。
使用 Claude 计算机使用进行爬取小红书的前10个笔记。 #要求: \\n1.目标网站:https://www.xiaohongshu.com/explore \\n2.前10个笔记的标题、作者和点赞数信息,并保存到一个markdown格式的文件文件中。\\n3.根据这10个笔记,综合数据生成一个热门话题。\\n
\\nAI 代理(Claude)接收到这个指令后,会理解我们的意图,并智能地将任务分解:
\\nxiaohongshu_top10.md
)。在 Trae CN 中,我们可以看到 AI 代理逐步执行这些任务的过程以及它的思考链条。它可能会显示正在调用 MCP 工具、正在处理数据、正在生成内容等提示信息。
\\n成功完成小红书前10个笔记的爬取和保存任务,并分析了这些笔记内容。以下是总结:
\\n在小红书探索页面爬取了前10个笔记的标题、作者和点赞数信息
\\n将这些信息保存到了 xiaohongshu_top10.md
文件中
分析这些笔记内容后,发现当前热门话题集中在:
\\n优化总结输出:
\\n根据这10个笔记,综合数据生成一个热门话题。并且加入到markdown文档中。\\n
\\n结果:
\\n# 热门话题\\n\\n根据前10篇笔记的内容分析,当前最热门的话题是 **\\"生活方式与个人体验\\"**。这些笔记涵盖了从婚姻生活、旅行计划到日常生活趣事等多个方面,反映了小红书用户对真实生活体验的分享和关注。\\n
\\n进一步优化输出:生成具体的热门标题
\\nAI 代理基于数据分析生成了热门话题方向(“生活方式与个人体验”),但如果我们希望得到一个更具体、更具吸引力的笔记标题,可以进一步细化指令。
\\n继续在 Trae CN 的对话窗口中,针对刚刚完成的任务,输入新的提示词:
\\n根据前10篇笔记的内容分析,帮我想一个具体的热门标题,并且加入到markdown文档中。\\n
\\n结果:
\\n## 热门标题推荐\\n\\n**\\"婚后生活大揭秘:从柴米油盐到浪漫旅行,真实夫妻生活全记录\\"**\\n\\n这个标题结合了当前最热门的生活方式和体验主题,涵盖了婚姻生活、旅行计划等元素,能够吸引用户的关注和共鸣。\\n
\\n至此,我们成功地利用腾讯云 MCP 广场、超浏览器AI自动化以及 Trae,实现了小红书热门话题和笔记数据的自动化爬取、分析与内容生成。
\\n腾讯云MCP广场堪称当前AI自动化领域的集大成者,它不仅提供了高度模块化的能力调用平台,还极大地简化了从“想法”到“落地”的整个开发流程。通过统一的接口与多种预置组件,开发者无需掌握复杂的工程细节,也能像搭积木一样构建强大且可扩展的自动化系统。尤其值得一提的是其“超浏览器AI自动化”工具,真正做到了低代码、甚至零代码完成复杂网页的动态数据抓取,这一突破对于以往困于JS渲染与反爬机制的开发者来说,无疑是生产力的质变飞跃。MCP广场不仅是工具集合,更是AI能力与开发者之间的高效桥梁,是每一个希望拥抱AI未来的技术人员不可或缺的平台。
\\nTrae不仅是一个开发工具,它更像是一个真正懂你意图的“AI编程助手”。通过自然语言指令,开发者无需编写冗长代码,就能完成复杂的任务编排与AI能力调用。在本次项目中,Trae展现出卓越的任务理解能力——从输入一句“爬取小红书热门笔记”,到自动识别目标、调用MCP超浏览器工具、抓取数据、格式化输出、分析趋势,甚至推荐热门标题,全流程几乎无需人工干预。Trae就像一位经验丰富的AI工程师,能听懂你的话、明白你的目标,并迅速将其转化为可执行的方案。它大幅度提升了开发体验,也重新定义了“AI助力开发”的边界,是MCP生态中不可多得的“智慧中枢”。
","description":"基于腾讯云MCP广场的AI自动化实践:爬取小红书热门话题 我正在参加Trae「超级体验官」创意实践征文,本文所使用的 Trae 免费下载链接:www.trae.com.cn/?utm_source…\\n\\n🔎 背景\\n\\n在人工智能快速发展的时代,AI技术不仅重塑了传统行业,也极大提高了开发者的工作效率。腾讯云推出的 MCP(Model Context Protocol) ,作为一种创新的技术协议,能够帮助开发者将云能力、AI模型与自动化流程高效整合,让开发者可以将更多精力集中在业务逻辑和创新上,而不再是重复性工作。\\n\\n最近,腾讯云推出了**MCP广场,一个帮助开…","guid":"https://juejin.cn/post/7501892144794681354","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T14:29:00.014Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5ee381e022c54ceebea7b1c97566f24b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=neCVaNpfbvivArZaXlVhO7Hdwv8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e63523a739b042fca42c7ff5ba00fe54~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=YI7fORyn7T7RvYexS5PAUyQ42tM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d06d85e792141868414a696a30cecc0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=SBr69AfyaHU8FeBK4u7KBbhJrGo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5d0f85399e724481be96a461c0400f6a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=mNgSedSM6Ygtjx6uiJEunNvroM0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/478f30b0a636494194bd8dfb0906f631~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=pIMfGJ9L4XaEf68PeYZjBJCwqB4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c038137fe399451cbdc1ae4cc86c74d0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=AQ8xIdYrh4B6gspQIeX6HX1ar2I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a8938983adce4655a8415de4f7672278~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=pUEwiRe8ddkq1dICiwjF%2Fs5hdpg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c49d6464ce7c437bb0515c3297f4fa97~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=GGFPR5aoD0FnQdKrcQgltsyEX9s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/877ec6c8f67843de8ab8aff308ef4bf2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=fXZQelbCNnaGMvg6tw6i3FPzIkg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89e6ba1b47704ff0b7cf9091dc1e1bca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=54VERYQrkdlh523zHoJeZCpPr5U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7ee1f32935e746b19f7e04abd1bfe0d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=o%2BSS%2BMZheKDcBcX0ed%2FaVOHeDdw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a402f34d8f54a839813c0c4ae61c1a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=%2B6oYXrWHuhW5tLXpBj2hQ4FQ3VI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1a64df42fc847a08434b5d04da35bc7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1747319340&x-signature=ND6YkUxbsB8gaaNl6pOIMnQEUXY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Trae"],"attachments":null,"extra":null,"language":null},{"title":"排行榜的5种实现方案!","url":"https://juejin.cn/post/7501582081631043625","content":"在工作的这些年中,我见证过太多团队在实现排行榜功能时踩过的坑。
\\n今天我想和大家分享 6 种不同的排行榜实现方案,从简单到复杂,从单机到分布式,希望能帮助大家在实际工作中做出更合适的选择。
\\n有些小伙伴在工作中可能会觉得:不就是个排行榜吗?搞个数据库排序不就完了?
\\n但实际情况远比这复杂得多。
\\n当数据量达到百万级、千万级时,简单的数据库查询可能就会成为系统的瓶颈。
\\n接下来,我将为大家详细剖析 6 种不同的实现方案,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\n适用场景:数据量小(万级以下),实时性要求不高
\\n这是最简单直接的方案,几乎每个开发者最先想到的方法。
\\n示例代码如下:
\\npublic List<UserScore> getRankingList() {\\n String sql = \\"SELECT user_id, score FROM user_scores ORDER BY score DESC LIMIT 100\\";\\n return jdbcTemplate.query(sql, new UserScoreRowMapper());\\n}\\n
\\n优点:
\\n缺点:
\\n架构图如下:
\\n适用场景:数据量中等(十万级),可以接受分钟级延迟
\\n这个方案在方案一的基础上引入了缓存机制。
\\n示例代码如下:
\\n@Scheduled(fixedRate = 60000) // 每分钟执行一次\\npublic void updateRankingCache() {\\n List<UserScore> rankings = userScoreDao.getTop1000Scores();\\n redisTemplate.opsForValue().set(\\"ranking_list\\", rankings);\\n}\\n\\npublic List<UserScore> getRankingList() {\\n return (List<UserScore>) redisTemplate.opsForValue().get(\\"ranking_list\\");\\n}\\n
\\n优点:
\\n缺点:
\\n架构图如下:
\\n适用场景:数据量大(百万级),需要实时更新
\\nRedis的有序集合(Sorted Set)是实现排行榜的利器。
\\n示例代码如下:
\\npublic void addUserScore(String userId, double score) {\\n redisTemplate.opsForZSet().add(\\"ranking\\", userId, score);\\n}\\n\\npublic List<String> getTopUsers(int topN) {\\n return redisTemplate.opsForZSet().reverseRange(\\"ranking\\", 0, topN - 1);\\n}\\n\\npublic Long getUserRank(String userId) {\\n return redisTemplate.opsForZSet().reverseRank(\\"ranking\\", userId) + 1;\\n}\\n
\\n优点:
\\n缺点:
\\n架构图如下:
最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。
\\n适用场景:超大规模数据(千万级以上),高并发场景
\\n当单机Redis无法满足需求时,可以采用分片方案。
\\n示例代码如下:
\\n// \\npublic void addUserScore(String userId, double score) {\\n RScoredSortedSet<String> set = redisson.getScoredSortedSet(\\"ranking:\\" + getShard(userId));\\n set.add(score, userId);\\n}\\n\\nprivate String getShard(String userId) {\\n // 简单哈希分片\\n int shard = Math.abs(userId.hashCode()) % 16;\\n return \\"shard_\\" + shard;\\n}\\n
\\n在这里我们以Redisson客户端为例。
\\n优点:
\\n缺点:
\\n架构图如下:
\\n适用场景:排行榜更新不频繁,但访问量极大
\\n这种方案结合了预计算和多级缓存。
\\n示例代码如下:
\\n@Scheduled(cron = \\"0 0 * * * ?\\") // 每小时计算一次\\npublic void precomputeRanking() {\\n Map<String, Integer> rankings = calculateRankings();\\n redisTemplate.opsForHash().putAll(\\"ranking:hourly\\", rankings);\\n \\n // 同步到本地缓存\\n localCache.putAll(rankings);\\n}\\n\\npublic Integer getUserRank(String userId) {\\n // 1. 先查本地缓存\\n Integer rank = localCache.get(userId);\\n if (rank != null) return rank;\\n \\n // 2. 再查Redis\\n rank = (Integer) redisTemplate.opsForHash().get(\\"ranking:hourly\\", userId);\\n if (rank != null) {\\n localCache.put(userId, rank); // 回填本地缓存\\n return rank;\\n }\\n \\n // 3. 最后查DB\\n return userScoreDao.getUserRank(userId);\\n}\\n
\\n优点:
\\n缺点:
\\n架构图如下:
\\n适用场景:需要实时更新且数据量极大的社交平台
\\n这种方案采用流处理技术实现实时排行榜。
\\n使用Apache Flink示例如下:
\\nDataStream<UserAction> actions = env.addSource(new UserActionSource());\\n\\nDataStream<Tuple2<String, Double>> scores = actions\\n .keyBy(UserAction::getUserId)\\n .process(new ProcessFunction<UserAction, Tuple2<String, Double>>() {\\n private MapState<String, Double> userScores;\\n \\n public void open(Configuration parameters) {\\n MapStateDescriptor<String, Double> descriptor = \\n new MapStateDescriptor<>(\\"userScores\\", String.class, Double.class);\\n userScores = getRuntimeContext().getMapState(descriptor);\\n }\\n \\n public void processElement(UserAction action, Context ctx, Collector<Tuple2<String, Double>> out) {\\n double newScore = userScores.getOrDefault(action.getUserId(), 0.0) + calculateScore(action);\\n userScores.put(action.getUserId(), newScore);\\n out.collect(new Tuple2<>(action.getUserId(), newScore));\\n }\\n });\\n\\nscores.keyBy(0)\\n .process(new RankProcessFunction())\\n .addSink(new RankingSink());\\n
\\n优点:
\\n缺点:
\\n架构图如下:
\\n方案 | 数据量 | 实时性 | 复杂度 | 适用场景 |
---|---|---|---|---|
数据库排序 | 小 | 低 | 低 | 个人项目、小规模应用 |
缓存+定时任务 | 中 | 中 | 中 | 中小型应用,可接受延迟 |
Redis有序集合 | 大 | 高 | 中 | 大型应用,需要实时更新 |
分片+Redis集群 | 超大 | 高 | 高 | 超大型应用,超高并发 |
预计算+分层缓存 | 大 | 中高 | 高 | 读多写少,访问量极大 |
实时计算+流处理 | 超大 | 实时 | 极高 | 社交平台,需要实时排名 |
在选择排行榜实现方案时,我们需要综合考虑以下几个因素:
\\n对于大多数中小型应用,方案二(缓存+定时任务)或方案三(Redis有序集合)已经足够。如
\\n果业务增长迅速,可以逐步演进到方案四(分片+Redis集群)。
\\n而对于社交平台等需要实时更新的场景,则需要考虑方案五(预计算+分层缓存)或方案六(实时计算+流处理),但要做好技术储备和架构设计。
\\n最后,无论选择哪种方案,都要做好监控和性能测试。排行榜作为高频访问的功能,其性能直接影响用户体验。
\\n建议在实际环境中进行压测,根据测试结果调整方案。
\\n希望这六种方案的详细解析能帮助大家在工作中做出更合适的选择。
\\n\\n\\n记住,没有最好的方案,只有最适合的方案。
\\n
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"引言 在工作的这些年中,我见证过太多团队在实现排行榜功能时踩过的坑。\\n\\n今天我想和大家分享 6 种不同的排行榜实现方案,从简单到复杂,从单机到分布式,希望能帮助大家在实际工作中做出更合适的选择。\\n\\n有些小伙伴在工作中可能会觉得:不就是个排行榜吗?搞个数据库排序不就完了?\\n\\n但实际情况远比这复杂得多。\\n\\n当数据量达到百万级、千万级时,简单的数据库查询可能就会成为系统的瓶颈。\\n\\n接下来,我将为大家详细剖析 6 种不同的实现方案,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文…","guid":"https://juejin.cn/post/7501582081631043625","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T03:06:44.797Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df15374f6d294b739853268f98015cdb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=TotIkF7ukex7UFK3GI0zh67xbMQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c257cb3db992478bb120462207f752c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=IFZUzLSQIWOAEBOvwIvrP8M2Dig%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/51bbadec44454ecca9c52ad8f3608259~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=TYxrRFp3lWJ1BbSc7ylrIKOUb10%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2d9b3d907674548b1fcb00c43dd478a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=WvorEqwi0GgBsv6frAlkicR5Sfo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/117f518454bf4a5fba455e28d60193a7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=eKwL2dNNWkw2lelzVy45BDz%2BQs4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/26246a27b88c444dbb88d64d2469be23~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747278404&x-signature=z8R0Wj7er5C2Ub22r8x8VxBnifw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"后端接口没做参数检验导致服务雪崩,被批评代码健壮性太差......","url":"https://juejin.cn/post/7501589510267715584","content":"首先,我这里想先说下心里活动,后端接口参数检验这个知识点比较基础,是每个Java后端开发必须掌握的“入坑”技能,哦不,是入门技能,所以我之前都有点不太愿意花时间精力去总结。但是无奈前不久发生了一个血淋淋的事故,情况大概是这样,前端在某种情况下会触发某个参数传空的情况,然后后端是用这参数去数据库查询数据的,如果这个参数为空,就会查出全表数据百万级别加载到内存中导致频繁fullgc,最终内存OOM服务不可用。
\\n复盘原因的时候被领导无情地吐槽我们后端团队写的代码健壮性不咋地~~~只能默默抗下了所有。其实刚刚工作的时候,当初的部门leader就给我说过一句话,后端写接口要按照是对外提供的open接口级别进行开发,要做到严谨、安全、可靠,因为外部一切资源都是不值得信任的。但大部分的开发随着时光荏苒,岁月蹉跎,团队换了一茬又一茬,项目做了一个又一个,当年的初心可能早已不再,大家都随波逐流了。所以今天痛定思痛,总结下后端接口参数校验的相关知识点和小伙伴们分享下。因为参数没传后端没检验的报错比比皆是,什么空指针、写入数据库报错等等,参数没检验后端接口报错后端就得背锅,这跑不脱的。
\\n由此看来,在现代Web应用开发中,参数校验是保证系统健壮性和安全性的重要环节。Spring Boot提供了强大的参数校验机制,可以帮助开发者轻松实现各种校验需求。本文将全面介绍Spring Boot中的参数校验,从基础使用到实践掌握。
\\n在使用框架组件进行参数检验之前,先来看看在代码中手动校验数据,直接手写 if-else 来做这些基础校验:
\\n@RestController\\n@RequestMapping(\\"/user\\")\\npublic class UserController {\\n @Resource\\n private UserService userService;\\n\\n @PostMapping\\n public void addUser(@RequestBody UserParam userParam) {\\n if (StringUtils.isBlank(userParam.getUserNo())) {\\n throw new BizException(\\"用户名不能为空\\");\\n }\\n if (StringUtils.isBlank(userParam.getName())) {\\n throw new BizException(\\"姓名不能为空\\");\\n }\\n if (StringUtils.isBlank(userParam.getPhone())) {\\n throw new BizException(\\"手机号不能为空\\");\\n }\\n if (StringUtils.isBlank(userParam.getUserNo())) {\\n throw new BizException(\\"账号不能为空\\");\\n }\\n if (Objects.isNull(userParam.getGender())) {\\n throw new BizException(\\"性别不能为空\\");\\n }\\n }\\n}\\n
\\n可以看出手动检验参数少一点还好,多一点就会带来代码冗余、错误处理的一致性以及业务规则的维护等问题。通过引入 Spring Validator
,我们能够有效解决这些痛点,提高代码的可读性、可维护性,并确保校验逻辑的一致性。
在项目中添加校验相关的依赖:
\\n<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-validation</artifactId>\\n</dependency>\\n
\\n直接来看检验接口参数的相关注解信息如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n分类 | 注解 | 说明 | 适用类型 |
---|---|---|---|
空值检查 | @NotNull | 验证对象是否为null | 任意类型 |
@Null | 验证对象必须为null | 任意类型 | |
@NotBlank | 验证字符串不为空且长度>0(至少一个非空格字符) | String | |
@NotEmpty | 验证对象不为null且不为空(集合/数组长度>0,字符串长度>0) | String, Collection, Map, Array | |
布尔检查 | @AssertTrue | 验证布尔值为true | boolean, Boolean |
@AssertFalse | 验证布尔值为false | boolean, Boolean | |
长度检查 | @Size(min, max) | 验证对象长度在范围内(字符串/集合/数组) | String, Collection, Map, Array |
@Length(min, max) | Hibernate扩展,验证字符串长度 | String | |
数值检查 | @Min(value) | 验证数字最小值(含边界) | 数字类型(int, long等) |
@Max(value) | 验证数字最大值(含边界) | 数字类型 | |
@DecimalMin(value) | 验证小数值最小值(含边界,可配置是否包含) | BigDecimal, BigInteger, String | |
@DecimalMax(value) | 验证小数值最大值(含边界,可配置是否包含) | BigDecimal, BigInteger, String | |
@Digits(integer, fraction) | 验证数字整数位和小数位长度 | 数字类型 | |
@Positive | 验证数字为正数(不包括0) | 数字类型 | |
@PositiveOrZero | 验证数字为正数或0 | 数字类型 | |
@Negative | 验证数字为负数(不包括0) | 数字类型 | |
@NegativeOrZero | 验证数字为负数或0 | 数字类型 | |
日期检查 | @Past | 验证日期是否在当前时间之前 | Date, LocalDate等 |
@PastOrPresent | 验证日期是否在当前时间或之前 | Date, LocalDate等 | |
@Future | 验证日期是否在当前时间之后 | Date, LocalDate等 | |
@FutureOrPresent | 验证日期是否在当前时间或之后 | Date, LocalDate等 | |
格式检查 | @Email | 验证字符串是否符合邮箱格式 | String |
@Pattern(regex) | 验证字符串是否符合正则表达式 | String | |
@URL | 验证字符串是否符合URL格式 | String | |
@CreditCardNumber | 验证字符串是否符合信用卡格式(Luhn算法) | String | |
集合检查 | @Valid | 对集合/数组中的每个元素进行验证 | Collection, Array |
级联检查 | @Valid | 对对象属性进行级联验证 | 自定义对象 |
特殊检查 | @Range(min, max) | Hibernate扩展,验证数值在范围内 | 数字类型 |
@SafeHtml | Hibernate扩展,验证字符串不包含恶意HTML | String | |
@ScriptAssert | 类级别验证,通过脚本表达式验证 |
接下来就来看看使用注解校验参数有多简单,我们可以根据上面列出来的注解做对应的参数校验
\\n@Data\\npublic class UserParam {\\n private Long id;\\n @NotBlank(message = \\"用户名不能为空\\")\\n @Size(min = 8, max = 16, message = \\"长度必须在8~16个字符之间\\")\\n private String userNo;\\n @NotBlank(message = \\"姓名不能为空\\")\\n @Size(max = 32, message = \\"姓名不能超过32个字符\\")\\n private String name;\\n @NotNull(message = \\"性别不能为空\\")\\n private Integer gender;\\n @Past(message = \\"出身日期必须在当前日期之前\\")\\n private Date birthday;\\n @Email(message = \\"邮箱格式不对\\")\\n private String email;\\n @Pattern(regexp = \\"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\\\d{8}$\\", message = \\"手机号格式不对\\")\\n private String phone;\\n}\\n\\n
\\n接口调整为:在参数对象加上@Validated
注解就能实现自动参数校验,当然了使用@Valid
也是可以的
@PostMapping\\npublic void addUser(@RequestBody @Validated UserParam userParam) {\\n System.out.println(userParam);\\n}\\n
\\n在postman调接口http://127.0.0.1:18000/user
, body传参如下:
{\\n \\"name\\":\\"张三\\",\\n \\"gender\\":0\\n}\\n
\\n日志报错如下:
\\n[common-demo] [] [2025-05-07 16:38:47.139] [WARN] [http-nio-18000-exec-2@2165] org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver logException: Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public void com.shepherd.basedemo.controller.UserController.addUser(com.shepherd.basedemo.param.UserParam): [Field error in object \'userParam\' on field \'userNo\': rejected value [null]; codes [NotBlank.userParam.userNo,NotBlank.userNo,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userParam.userNo,userNo]; arguments []; default message [userNo]]; default message [用户名不能为空]] ]\\n\\n
\\n接口返回:
\\n{\\n \\"timestamp\\": \\"2025-05-07 16:38:47\\",\\n \\"status\\": 400,\\n \\"error\\": \\"Bad Request\\",\\n \\"path\\": \\"/user\\"\\n}\\n
\\n如果校验失败,会抛出MethodArgumentNotValidException
或者ConstraintViolationException
异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示,关于统一结果格式封装和全局异常统一处理,请看之前我们总结的:
Spring Boot如何优雅实现结果统一封装和异常统一处理
\\n这里简单看看统一处理参数校验异常:
\\n@ControllerAdvice\\n@Slf4j\\npublic class GlobalExceptionHandler {\\n\\n /**\\n * 全局异常处理\\n * @param e\\n * @return\\n */\\n @ResponseBody\\n @ResponseStatus(HttpStatus.BAD_REQUEST)\\n @ExceptionHandler(Exception.class)\\n public ResponseVO exceptionHandler(Exception e){\\n // 处理业务异常\\n if (e instanceof BizException) {\\n BizException bizException = (BizException) e;\\n if (bizException.getCode() == null) {\\n bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());\\n }\\n return ResponseVO.failure(bizException.getCode(), bizException.getMessage());\\n } else if (e instanceof MethodArgumentNotValidException) {\\n // 参数检验异常\\n MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;\\n Map<String, String> map = new HashMap<>();\\n BindingResult result = methodArgumentNotValidException.getBindingResult();\\n result.getFieldErrors().forEach((item)->{\\n String message = item.getDefaultMessage();\\n String field = item.getField();\\n map.put(field, message);\\n });\\n log.error(\\"数据校验出现错误:\\", e);\\n return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);\\n }else if (e instanceof ConstraintViolationException) {\\n log.error(\\"数据校验出现错误:\\", e);\\n return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, e.getMessage());\\n }else if (e instanceof HttpRequestMethodNotSupportedException) {\\n log.error(\\"请求方法错误:\\", e);\\n return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), \\"请求方法不正确\\");\\n } else if (e instanceof MissingServletRequestParameterException) {\\n log.error(\\"请求参数缺失:\\", e);\\n MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;\\n return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), \\"请求参数缺少: \\" + ex.getParameterName());\\n } else if (e instanceof MethodArgumentTypeMismatchException) {\\n log.error(\\"请求参数类型错误:\\", e);\\n MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;\\n return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), \\"请求参数类型不正确:\\" + ex.getName());\\n } else if (e instanceof NoHandlerFoundException) {\\n NoHandlerFoundException ex = (NoHandlerFoundException) e;\\n log.error(\\"请求地址不存在:\\", e);\\n return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());\\n } else {\\n //如果是系统的异常,比如空指针这些异常\\n log.error(\\"【系统异常】\\", e);\\n return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());\\n }\\n }\\n}\\n\\n
\\n添加全局异常统一处理之后再次调接口,返回结果如下:
\\n{\\n \\"code\\": 400,\\n \\"msg\\": \\"Bad Request\\",\\n \\"data\\": {\\n \\"userNo\\": \\"用户名不能为空\\"\\n }\\n}\\n
\\n是不是直接明了多了,关于参数的其他的验证,你可以一一输入参数慢慢去验证,这里碍于篇幅问题,就不赘述了。
\\nrequestParam/PathVariable
参数校验GET
请求一般会使用requestParam/PathVariable
传参,在这种情况下,必须在Controller
类上标注@Validated
注解,并在入参上声明约束注解(如@Min
等) 。如果校验失败,会抛出ConstraintViolationException
异常。代码示例如下:
@GetMapping(\\"/{userId}\\")\\n public void detail(@PathVariable(\\"userId\\") @Min(value = 1L, message = \\"userId必须大于0\\") Long userId) {\\n System.out.println(userId);\\n }\\n\\n @GetMapping(\\"/info\\")\\n public void getUserInfo(@RequestParam(\\"userId\\") @Max(value = 10L, message = \\"userId必须不超过10\\") Long userId) {\\n System.out.println(userId);\\n }\\n
\\n上面的示例中,入参都比较简单,但是当入参中包括其他对象参数,我们要对此对象进行参数检验,也就是嵌套检验,这时候就只能使用@Valid
进行级联校验了,注意,并不能使用@Validated
,这是这两个注解的一大区别:
**@Valid
和@Validated
**区别
区别 | @Valid | @Validated |
---|---|---|
提供者 | JSR-303规范 | Spring |
是否支持分组 | 不支持 | 支持 |
标注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
嵌套校验 | 支持 | 不支持 |
我们在上面的userParam
里面增加了一个地址集合字段:
@NotEmpty(message = \\"地址不能为空\\")\\n@Valid\\nprivate List<Address> addressList;\\n
\\n地址对象address要求省份和城市不能为空:
\\n@Data\\npublic class Address {\\n @NotBlank(message = \\"省份不能为空\\")\\n private String province;\\n @NotBlank(message = \\"城市不能为空\\")\\n private String city;\\n private String region;\\n private String address;\\n}\\n\\n
\\n再次调用上面新增用户的接口,入参如下:
\\n{\\n \\"userNo\\":\\"zfj-001000\\",\\n \\"name\\":\\"张三\\",\\n \\"gender\\":0,\\n \\"addressList\\":[\\n {\\n \\"province\\":\\"浙江省\\"\\n }\\n\\n ]\\n}\\n
\\n返回结果如下:
\\n{\\n \\"code\\": 400,\\n \\"msg\\": \\"Bad Request\\",\\n \\"data\\": {\\n \\"addressList[0].city\\": \\"城市不能为空\\"\\n }\\n}\\n
\\n在实际项目开发中,可能有多个接口方法使用同一个类来接收参数,比如说新增用户、更新用户、删除用户等操作,每个操作对入参字段的要求都不一样,比如在用户创建时可能强调用户名和密码的合法性,而在用户更新时可能更关心其他信息的完整性。如果我们针对每个操作单独新建一个入参类来接收参数单独检验这是没问题的,但是随着业务场景的多变性,会造成类的膨胀,业务的重复实现,难以维护。这也是很多后端开发不想做接口检验的主要原因,不做检验一个公共入参类随便你拿去接收相关接口的参数。为了解决这个不同操作分组问题,spring-validation
提供的@Validated
支持了分组校验的功能,专门用来解决这类问题,这里就不能用@Valid
,这也是两者的一大区别。
比如上面的userParam
,在新增用户的时候用户名userNo
不能为空,在更新用户时id
不能为空,如下所示:
@Data\\npublic class UserParam {\\n @NotNull(message = \\"id不能为空\\", groups = {Update.class})\\n private Long id;\\n @NotBlank(message = \\"用户名不能为空\\", groups = {Insert.class})\\n @Size(min = 8, max = 16, message = \\"长度必须在8~16个字符之间\\")\\n private String userNo;\\n @NotBlank(message = \\"姓名不能为空\\")\\n @Size(max = 32, message = \\"姓名不能超过32个字符\\")\\n private String name;\\n @NotNull(message = \\"性别不能为空\\")\\n private Integer gender;\\n @Past(message = \\"出身日期必须在当前日期之前\\")\\n private Date birthday;\\n @Email(message = \\"邮箱格式不对\\")\\n private String email;\\n @Pattern(regexp = \\"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\\\d{8}$\\", message = \\"手机号格式不对\\")\\n private String phone;\\n\\n @NotEmpty(message = \\"地址不能为空\\")\\n @Valid\\n private List<Address> addressList;\\n\\n\\n public interface Insert{};\\n\\n public interface Update{};\\n\\n}\\n
\\n接口调整为:
\\n @PostMapping\\n public void addUser(@RequestBody @Validated({UserParam.Insert.class}) UserParam userParam) {\\n System.out.println(userParam);\\n }\\n\\n @PutMapping\\n public void updateUser(@RequestBody @Validated(UserParam.Update.class) UserParam userParam) {\\n System.out.println(userParam);\\n }\\n
\\n调用接口发现userNo
和id
是能分别在新增,更新两个场景下单独检验了,但是姓名、性别字段竟然不校验了,我的本意是这些字段不论是新增还是编辑都要检验,这可咋整?其实问题就出在我们的分组定义上。
public interface Insert extends Default {};\\n\\n public interface Update extends Default {};\\n
\\n我们需要在定义分组的时候继承默认分组,这样就能实现指定了分组的校验规则,分别在对应的分组校验中生效,没有指定分组使用默认分组Default
,即对所有的校验都生效。也就是使用@Validated({UserParam.Insert.class}) UserParam userParam
代表着Insert分组包括了Default分组了
至此,Spring Boot如何优雅实现后端接口参数检验实践教程已经讲述完了,Spring Boot提供了强大而灵活的参数校验机制,从简单的字段校验到复杂的自定义校验都能很好支持。合理使用参数校验可以:
\\n在实际项目中,建议将校验逻辑集中在参数传输DTO层,保持业务逻辑的纯净性,同时结合全局异常处理提供友好的错误信息。
\\n最后的最后,这里放个彩蛋,这里的总结对参数校验完全够用了,但是我们一般都要知其然知其所以然,肯定要知道其实现原理、高级自定义实现,多语言环境整合等等,我会在下一篇文章中详细奉上,敬请期待~~~
","description":"1.背景 首先,我这里想先说下心里活动,后端接口参数检验这个知识点比较基础,是每个Java后端开发必须掌握的“入坑”技能,哦不,是入门技能,所以我之前都有点不太愿意花时间精力去总结。但是无奈前不久发生了一个血淋淋的事故,情况大概是这样,前端在某种情况下会触发某个参数传空的情况,然后后端是用这参数去数据库查询数据的,如果这个参数为空,就会查出全表数据百万级别加载到内存中导致频繁fullgc,最终内存OOM服务不可用。\\n\\n复盘原因的时候被领导无情地吐槽我们后端团队写的代码健壮性不咋地~~~只能默默抗下了所有。其实刚刚工作的时候…","guid":"https://juejin.cn/post/7501589510267715584","author":"Shepherd","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T01:39:45.559Z","media":null,"categories":["后端","Spring Boot","代码规范"],"attachments":null,"extra":null,"language":null},{"title":"为什么要用JdbcTemplate","url":"https://juejin.cn/post/7501528667958313014","content":"工作这么多年了,第一次使用JdbcTemplate
。使用的时候当然要全方位的了解它。了解它的使用方式,使用场景以及注意事项等相关知识,今天就和大家一起学习一下吧😁
JdbcTemplate是Spring框架核心包的一部分,位于spring-jdbc
模块中,因此通常不用我们再额外引入依赖,直接就可以。它封装了JDBC的核心流程,消除了传统的JDBC开发中大量重复的代码。当然最核心的就是直接在代码中执行SQL
:
// 插入数据\\njdbcTemplate.update(\\"INSERT INTO users(name, email) VALUES(?, ?)\\", \\"张三\\", \\"zhangsan@example.com\\");\\n\\n// 查询单条记录\\nUser user = jdbcTemplate.queryForObject(\\n \\"SELECT * FROM users WHERE id = ?\\", \\n new Object[]{1}, \\n new BeanPropertyRowMapper<>(User.class));\\n
\\n因此它和 mybatis
这些 orm
框架相比使用就更加的简单了,效率也更高。但是在开发中我们不会去用它来写业务代码,毕竟规范通常来说更加的重要。
它的使用场景在哪些地方呢,以本次工作场景为例。这次的业务需求 需要实现动态建表的功能,以及在插入数据的时候,表名 和 字段都是动态的。利用orm
框架去处理ddl
语句显示不是很好,以及表名和字段都是需要计算才知道显然在java
代码中拼接好SQL
之后直接执行更好。
所以在执行ddl
语句,存储过程,以及SQL
必须在程序中进行拼接的场景,用JdbcTemplate
更为合适
\\n\\n在实际项目中,JdbcTemplate常与JPA或MyBatis等ORM框架混合使用。ORM框架处理常规业务逻辑,而JdbcTemplate处理特殊场景如ddl、复杂查询、批量操作等。这种混合模式能兼顾开发效率和执行性能。(并且在springboot工程中通常不需要在单独引入依赖,直接使用就可以了)
\\n
在Spring Boot中,只需在application.properties中配置数据源,Spring会自动创建JdbcTemplate bean:
\\nspring.datasource.url=jdbc:mysql://localhost:3306/test\\nspring.datasource.username=username\\nspring.datasource.password=password\\nspring.datasource.driver-class-name=com.mysql.jdbc.Driver\\n
\\n导入bean就可以直接使用了
\\n@Resource\\nprivate JdbcTemplate jdbcTemplate;\\n
\\nJdbcTemplate提供了多种查询方法,以下是一些常用示例:
\\n查询单个值:
\\n// 查询用户总数\\nint count = jdbcTemplate.queryForObject(\\n \\"SELECT COUNT(*) FROM users\\", Integer.class);\\n
\\n查询单条记录:
\\n// 使用RowMapper将结果集映射为对象\\nUser user = jdbcTemplate.queryForObject(\\n \\"SELECT * FROM users WHERE id = ?\\", \\n new Object[]{1}, \\n new BeanPropertyRowMapper<>(User.class));\\n
\\n查询多条记录:
\\n// 查询所有用户\\nList<User> users = jdbcTemplate.query(\\n \\"SELECT * FROM users\\", \\n new BeanPropertyRowMapper<>(User.class));\\n
\\n插入数据:
\\njdbcTemplate.update(\\n \\"INSERT INTO users(name, email) VALUES(?, ?)\\", \\n \\"张三\\", \\"zhangsan@example.com\\");\\n
\\n更新数据:
\\nint rowsAffected = jdbcTemplate.update(\\n \\"UPDATE users SET email = ? WHERE id = ?\\", \\n \\"newemail@example.com\\", 1);\\n
\\n删除数据:
\\nint rowsDeleted = jdbcTemplate.update(\\n \\"DELETE FROM users WHERE id = ?\\", 1);\\n
\\nJdbcTemplate支持高效的批量操作:
\\njdbcTemplate.batchUpdate(\\n \\"INSERT INTO users(name, email) VALUES(?, ?)\\",\\n new BatchPreparedStatementSetter() {\\n @Override\\n public void setValues(PreparedStatement ps, int i) throws SQLException {\\n ps.setString(1, \\"User\\" + i);\\n ps.setString(2, \\"user\\" + i + \\"@example.com\\");\\n }\\n \\n @Override\\n public int getBatchSize() {\\n return 10;\\n }\\n });\\n
\\n使用NamedParameterJdbcTemplate可以更清晰地处理参数:
\\nNamedParameterJdbcTemplate namedTemplate = \\n new NamedParameterJdbcTemplate(dataSource);\\n\\nMap<String, Object> params = new HashMap<>();\\nparams.put(\\"id\\", 1);\\nparams.put(\\"email\\", \\"newemail@example.com\\");\\n\\nnamedTemplate.update(\\n \\"UPDATE users SET email = :email WHERE id = :id\\", \\n params);\\n
\\n直接把原生的SQL语句执行就行了,比如建表操作:
\\npublic void createUserTable() {\\n jdbcTemplate.execute(\\"CREATE TABLE users (\\"\\n + \\"id BIGINT PRIMARY KEY AUTO_INCREMENT,\\"\\n + \\"username VARCHAR(50) NOT NULL UNIQUE,\\"\\n + \\"password VARCHAR(100) NOT NULL,\\"\\n + \\"email VARCHAR(100),\\"\\n + \\"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\\"\\n + \\"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\\"\\n + \\")\\");\\n}\\n
\\n实际使用中仍有一些需要注意的关键点,遵循这些最佳实践可以避免常见问题并提高代码质量和性能。\\n关键注意事项包括:
\\n问题:直接拼接SQL字符串会导致SQL注入风险
\\n正确做法:
\\n// ❌ 错误做法(有SQL注入风险)\\nString sql = \\"SELECT * FROM users WHERE name = \'\\" + name + \\"\'\\";\\njdbcTemplate.query(sql, rowMapper);\\n\\n// ✅ 正确做法(使用参数占位符)\\njdbcTemplate.query(\\"SELECT * FROM users WHERE name = ?\\", new Object[]{name}, rowMapper);\\n
\\n问题:虽然JdbcTemplate会关闭连接,但某些资源仍需注意
\\n注意事项:
\\nRowCallbackHandler
而非RowMapper
进行流式处理JdbcTemplate.query()
方法时,确保结果集不会过大导致内存溢出// 处理大型结果集的推荐方式\\njdbcTemplate.query(\\"SELECT * FROM large_table\\", \\n rs -> {\\n // 逐行处理,不一次性加载所有数据\\n while (rs.next()) {\\n processRow(rs);\\n }\\n });\\n
\\n问题:JdbcTemplate会将SQLException转换为DataAccessException
\\n正确处理:
\\ntry {\\n jdbcTemplate.update(\\"UPDATE accounts SET balance = ? WHERE id = ?\\", amount, accountId);\\n} catch (DuplicateKeyException e) {\\n // 处理唯一键冲突\\n log.error(\\"Duplicate key violation\\", e);\\n throw new BusinessException(\\"Account already exists\\");\\n} catch (DataAccessException e) {\\n // 处理其他数据库异常\\n log.error(\\"Database access error\\", e);\\n throw new ServiceException(\\"Database operation failed\\");\\n}\\n
\\n问题:默认情况下每个操作是独立事务;
\\n\\n\\n注意:DDL语句通常是自动提交的,不受常规事务管理控制
\\n
最佳实践:
\\n@Transactional
注解@Transactional\\npublic void transferMoney(Long fromId, Long toId, BigDecimal amount) {\\n jdbcTemplate.update(\\"UPDATE accounts SET balance = balance - ? WHERE id = ?\\", amount, fromId);\\n jdbcTemplate.update(\\"UPDATE accounts SET balance = balance + ? WHERE id = ?\\", amount, toId);\\n}\\n
\\n注意事项:
\\nbatchUpdate
而非循环执行单条更新NamedParameterJdbcTemplate
提高复杂SQL可读性// 批量插入优化示例\\njdbcTemplate.batchUpdate(\\n \\"INSERT INTO users (name, email) VALUES (?, ?)\\",\\n new BatchPreparedStatementSetter() {\\n public void setValues(PreparedStatement ps, int i) throws SQLException {\\n User user = users.get(i);\\n ps.setString(1, user.getName());\\n ps.setString(2, user.getEmail());\\n }\\n public int getBatchSize() {\\n return users.size();\\n }\\n });\\n
\\n问题:SQL
硬编码在Java类中难以维护,这也是我们平时做业务开发,不用使用jdbcTemplate
重要原因之一
解决方案:
\\n@Sql
注解管理测试数据// 将SQL提取为常量\\nprivate static final String FIND_USER_BY_ID = \\n \\"SELECT id, name, email FROM users WHERE id = ?\\";\\n\\npublic User findById(Long id) {\\n return jdbcTemplate.queryForObject(\\n FIND_USER_BY_ID,\\n new Object[]{id},\\n new BeanPropertyRowMapper<>(User.class));\\n}\\n
\\nJdbcTemplate
在工作中还是很少用到的,业务代码还是用orm
。特殊的场景比如执行ddl
、存储过程,或者orm
不方便操作的可以考虑使用jdbcTemplate
执行这部分语句 。当然在使用过程的注意事项还是要了解的,比如防止SQL
注入等问题。
在 Java 生态中,Apache Kafka 通过 kafka-clients.jar
提供了原生客户端支持。开发者需要手动创建 KafkaConsumer
实例并订阅指定主题(Topic)来实现消息消费。典型实现如下:
public void pollMessages() {\\n // 1. 初始化消费者实例\\n Consumer<String, String> consumer = new KafkaConsumer<>(getConsumerConfig());\\n // 2. 订阅主题并设置重平衡监听器\\n consumer.subscribe(Collections.singleton(topic), new RebalanceListener());\\n // 3. 轮询获取消息(超时时间1秒)\\n ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));\\n // 4. 同步提交偏移量\\n consumer.commitSync();\\n}\\n
\\nSpring Kafka 在原生客户端基础上进行了深度封装,通过声明式注解显著简化了开发流程。例如,只需使用 @KafkaListener
注解即可实现消息监听:
@KafkaListener(id = \\"orderService\\", topics = \\"order.topic\\")\\npublic void handleOrderEvent(ConsumerRecord<String, String> record) {\\n // 业务处理逻辑\\n}\\n
\\n这种简洁的语法背后,Spring Kafka 实际上构建了一套完整的消费者(Consumer)管理机制。那么问题来了:Spring Kafka 是如何创建这些消费者的呢?
\\n\\n\\n\\n本文源码版本:spring-kafka v2.6.6
\\n
首先,我们通过一个完整的项目集成示例,具体说明其实现步骤。项目里要接入 Spring Kafka,通常需要经过以下几个步骤。
\\n需在项目中声明 Spring Kafka Starter 依赖。
\\n<dependency>\\n <groupId>org.springframework.kafka</groupId>\\n <artifactId>spring-kafka</artifactId>\\n <version>2.6.6</version>\\n</dependency>\\n
\\n配置类上添加 @EnableKafka
注解,并初始化 ConcurrentKafkaListenerContainerFactory
Bean,这是最常见的使用方式。
@Configuration\\n@EnableKafka\\npublic class Config {\\n\\n /** 消费者工厂 */\\n @Bean\\n public ConsumerFactory<String, String> consumerFactory() {\\n Map<String, Object> configs = new HashMap<>();\\n configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, \\"localhost:9092\\");\\n configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);\\n configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);\\n return new DefaultKafkaConsumerFactory<>(configs);\\n }\\n \\n /** 监听容器工厂 */\\n @Bean\\n ConcurrentKafkaListenerContainerFactory<String, String>\\n kafkaListenerContainerFactory(ConsumerFactory<String, String> consumerFactory) {\\n ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();\\n factory.setConsumerFactory(consumerFactory);\\n factory.setConcurrency(3); // 设置消费者线程数\\n return factory;\\n }\\n}\\n
\\n在业务层方法上添加 @KafkaListener
注解,实现消息监听。
@Service\\npublic class OrderMessageListener {\\n @KafkaListener(id = \\"orderService\\", topics = \\"order.topic\\")\\n public void handleOrderEvent(ConsumerRecord<String, String> record) {\\n // 业务处理逻辑\\n }\\n}\\n
\\n至此,我们已经完成 Spring Kafka 的基础集成。接下来将深入分析@KafkaListener
注解背后的消费者创建过程,揭示 Spring 是如何构建 KafkaConsumer 实例的。
基于上面示例,我们以 @EnableKafka
注解为切入点,源码如下:
@Import(KafkaListenerConfigurationSelector.class)\\npublic @interface EnableKafka {\\n}\\n\\n@Order\\npublic class KafkaListenerConfigurationSelector implements DeferredImportSelector {\\n @Override\\n public String[] selectImports(AnnotationMetadata importingClassMetadata) {\\n return new String[] { KafkaBootstrapConfiguration.class.getName() };\\n }\\n}\\n
\\n该注解的核心作用是通过KafkaBootstrapConfiguration
向 Spring 容器注册两个关键 Bean。注册的核心 Bean 如下所示:
public class KafkaBootstrapConfiguration implements ImportBeanDefinitionRegistrar {\\n @Override\\n public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {\\n // 省略无关代码\\n \\n // 注册注解处理器\\n // beanName: org.springframework.kafka.config.internalKafkaListenerAnnotationProcessor\\n registry.registerBeanDefinition(\\n KafkaListenerConfigUtils.KAFKA_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,\\n new RootBeanDefinition(KafkaListenerAnnotationBeanPostProcessor.class));\\n \\n // 注册监听器容器注册表\\n // beanName: org.springframework.kafka.config.internalKafkaListenerEndpointRegistry\\n registry.registerBeanDefinition(\\n KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,\\n new RootBeanDefinition(KafkaListenerEndpointRegistry.class));\\n }\\n}\\n
\\n注解处理器(KafkaListenerAnnotationBeanPostProcessor
)负责扫描和解析@KafkaListener
及其派生注解,并将监听方法转换为可执行的端点描述符(KafkaListenerEndpointDescriptor
)。
容器注册表(KafkaListenerEndpointRegistry
)作为所有消息监听容器的中央仓库,实现了生命周期管理(启动/停止容器)。
\\n\\n代码阅读小记:
\\n\\n切入点: @EnableKafka\\n-> KafkaListenerConfigurationSelector\\n-> KafkaBootstrapConfiguration\\n [注册Bean: KafkaListenerAnnotationBeanPostProcessor]\\n [注册Bean: KafkaListenerEndpointRegistry]\\n
接下来,我们就重点剖析一下这两个 Bean。
\\n首先来看第一个 Bean: KafkaListenerAnnotationBeanPostProcessor
,它通过 Spring 后置处理器机制(postProcessAfterInitialization
)实现了注解扫描:
public class KafkaListenerAnnotationBeanPostProcessor<K, V>\\n implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {\\n @Override\\n public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {\\n // 省略无关代码\\n \\n // 使用 MethodIntrospector 进行元数据查找\\n // 查找被 @KafkaListener (及其派生注解) 标记的方法\\n Map<Method, Set<KafkaListener>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,\\n (MethodIntrospector.MetadataLookup<Set<KafkaListener>>) method -> {\\n Set<KafkaListener> listenerMethods = findListenerAnnotations(method);\\n return (!listenerMethods.isEmpty() ? listenerMethods : null);\\n });\\n \\n // 处理每个找到的监听方法\\n for (Map.Entry<Method, Set<KafkaListener>> entry : annotatedMethods.entrySet()) {\\n Method method = entry.getKey();\\n for (KafkaListener listener : entry.getValue()) {\\n processKafkaListener(listener, method, bean, beanName);\\n }\\n }\\n return bean;\\n }\\n}\\n
\\n上述代码有两个关键点,第一是通过MetadataLookup
支持派生注解;第二是处理 @KafkaListener
监听方法。
什么是MetadataLookup
呢?
举个例子,我们定义了一个新的注解 @EventHandler
,并在该注解上标记 @KafkaListener
。
@KafkaListener\\npublic @interface EventHandler {\\n @AliasFor(annotation = KafkaListener.class, attribute = \\"topics\\")\\n String value();\\n // 其他属性映射...\\n}\\n
\\n这种设计使得业务注解(如@EventHandler
)可以透明地继承@KafkaListener
的全部功能。
@Service\\npublic class OrderMessageListener {\\n @EventHandler(\\"order.topic\\")\\n public void handleOrderEvent(ConsumerRecord<String, String> record) {\\n // 业务处理逻辑\\n }\\n}\\n
\\n我们继续处理 KafkaListener 代码跟踪,现在来到了 KafkaListenerAnnotationBeanPostProcessor
的 processListener()
方法。
public class KafkaListenerAnnotationBeanPostProcessor<K, V>\\n implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {\\n // KafkaListener 注册器\\n private final KafkaListenerEndpointRegistrar registrar = new KafkaListenerEndpointRegistrar();\\n \\n protected void processListener(MethodKafkaListenerEndpoint<?, ?> endpoint, KafkaListener kafkaListener,\\n Object bean, Object adminTarget, String beanName) {\\n // 设置端点属性\\n endpoint.setBean(bean);\\n endpoint.setId(getEndpointId(kafkaListener));\\n endpoint.setTopics(resolveTopics(kafkaListener));\\n // 委托注册器进行注册\\n this.registrar.registerEndpoint(endpoint, factory);\\n }\\n}\\n
\\n被 @KafkaListener
标记的方法会被封装为 MethodKafkaListenerEndpoint
,并由注册器 KafkaListenerEndpointRegistrar
进行注册,注册器内部维护了一个端点描述符列表:
public class KafkaListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean {\\n private final List<KafkaListenerEndpointDescriptor> endpointDescriptors = new ArrayList<>();\\n \\n public void registerEndpoint(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory) {\\n // 省略无关代码\\n KafkaListenerEndpointDescriptor descriptor = new KafkaListenerEndpointDescriptor(endpoint, factory);\\n this.endpointDescriptors.add(descriptor);\\n }\\n}\\n
\\n由此可见,KafkaListener
会被注册到 List 集合中。
\\n\\n代码阅读小记:
\\n\\n-> KafkaListenerAnnotationBeanPostProcessor#postProcessAfterInitialization()\\n-> processKafkaListener()\\n-> processListener()\\n-> KafkaListenerEndpointRegistrar#registerEndpoint()\\n-> endpointDescriptors [注册到容器里List]\\n
到这里,BeanPostProcessor
的 postProcessAfterInitialization
方法已经执行完了,程序完成了 KafkaListener
的注册并存储至 endpointDescriptors 中。
当所有 Bean 初始化完成后,接下来会通过afterSingletonsInstantiated
触发最终注册:
public class KafkaListenerAnnotationBeanPostProcessor<K, V>\\n implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {\\n // KafkaListener 注册器\\n private final KafkaListenerEndpointRegistrar registrar = new KafkaListenerEndpointRegistrar();\\n \\n @Override\\n public void afterSingletonsInstantiated() {\\n // 注册所有 KafkaListener\\n this.registrar.afterPropertiesSet();\\n }\\n}\\n
\\n注册器 KafkaListenerEndpointRegistrar
的注册逻辑如下。
public class KafkaListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean {\\n \\n private KafkaListenerEndpointRegistry endpointRegistry;\\n \\n @Override\\n public void afterPropertiesSet() {\\n registerAllEndpoints();\\n }\\n\\n protected void registerAllEndpoints() {\\n // 注册到注册表当中\\n for (KafkaListenerEndpointDescriptor descriptor : this.endpointDescriptors) {\\n this.endpointRegistry.registerListenerContainer(\\n descriptor.endpoint, resolveContainerFactory(descriptor));\\n }\\n }\\n}\\n
\\n可见,注册器最终委托给了注册表处理,注册表中由一个 ConcurrentHashMap
进行保存。
public class KafkaListenerEndpointRegistry implements DisposableBean, \\n SmartLifecycle, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent> {\\n \\n private final Map<String, MessageListenerContainer> listenerContainers = new ConcurrentHashMap<>();\\n \\n // 注册表的注册逻辑\\n public void registerListenerContainer(KafkaListenerEndpoint endpoint, \\n KafkaListenerContainerFactory<?> factory) {\\n registerListenerContainer(endpoint, factory, false);\\n }\\n \\n public void registerListenerContainer(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory,\\n boolean startImmediately) {\\n String id = endpoint.getId();\\n // 通过工厂创建, 最终创建出来的 ConcurrentMessageListenerContainer\\n MessageListenerContainer container = createListenerContainer(endpoint, factory);\\n this.listenerContainers.put(id, container);\\n }\\n}\\n
\\n在示例中,我们配置的是 ConcurrentKafkaListenerContainerFactory
来创建 KafkaListener
容器的,因此这里往注册表(KafkaListenerEndpointRegistry)里添加的是 ConcurrentMessageListenerContainer
对象实例。
\\n\\n代码阅读小记:
\\n\\nKafkaListenerAnnotationBeanPostProcessor#afterSingletonsInstantiated()\\n-> KafkaListenerEndpointRegistrar#afterPropertiesSet()\\n-> registerAllEndpoints()\\n-> KafkaListenerEndpointRegistry#registerListenerContainer()\\n-> [listenerContainers] [注册到容器里Map]\\n
再来看第二个 Bean: KafkaListenerEndpointRegistry
。它实现了 Spring 生命周期 SmartLifecycle 接口,在程序启动时,会调用它的 start
方法。
public class KafkaListenerEndpointRegistry implements DisposableBean, \\n SmartLifecycle, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent> {\\n \\n @Override\\n public void start() {\\n // ConcurrentMessageListenerContainer 实例\\n for (MessageListenerContainer listenerContainer : getListenerContainers()) {\\n startIfNecessary(listenerContainer);\\n }\\n }\\n \\n private void startIfNecessary(MessageListenerContainer listenerContainer) {\\n if (this.contextRefreshed || listenerContainer.isAutoStartup()) {\\n listenerContainer.start();\\n }\\n }\\n}\\n
\\n注册表(KafkaListenerEndpointRegistry
)维护的容器(MessageListenerContainer
)实例分为两类:
ConcurrentMessageListenerContainer
:多线程容器KafkaMessageListenerContainer
:单线程容器ConcurrentMessageListenerContainer
内部通过创建多个单线程容器实现并发:
public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageListenerContainer<K, V> {\\n\\n @Override\\n protected void doStart() {\\n for (int i = 0; i < this.concurrency; i++) {\\n KafkaMessageListenerContainer<K, V> container =\\n constructContainer(containerProperties, topicPartitions, i);\\n // 启动每个子容器\\n container.start();\\n }\\n }\\n}\\n
\\n可见,ConcurrentMessageListenerContainer
通过委托给多个KafkaMessageListenerContainer
实例从而实现多线程消费。
最终我们在 KafkaMessageListenerContainer
的内部类 ListenerConsumer
中发现了 kafka-clients.jar 中的 Consumer 接口类。它的创建过程是由 ConsumerFactory
代为创建,ConsumerFactory
是一个接口类,它只有一个实现:DefaultKafkaConsumerFactory
。
public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListenerContainer<K, V> {\\n \\n private volatile ListenerConsumer listenerConsumer;\\n \\n @Override\\n protected void doStart() {\\n this.listenerConsumer = new ListenerConsumer(listener, listenerType);\\n }\\n \\n private final class ListenerConsumer implements SchedulingAwareRunnable, ConsumerSeekCallback {\\n // Consumer 是 kafka-clients.jar 中的接口类\\n private final Consumer<K, V> consumer;\\n // ConsumerFactory 是一个接口,只有一个实现类 DefaultKafkaConsumerFactory\\n protected final ConsumerFactory<K, V> consumerFactory;\\n \\n ListenerConsumer(GenericMessageListener<?> listener, ListenerType listenerType) {\\n Properties consumerProperties = propertiesFromProperties();\\n this.consumer = this.consumerFactory.createConsumer(\\n this.consumerGroupId,\\n this.containerProperties.getClientId(),\\n KafkaMessageListenerContainer.this.clientIdSuffix,\\n consumerProperties);\\n // 监听 Topic\\n subscribeOrAssignTopics(this.consumer);\\n }\\n }\\n}\\n
\\nConsumer 消费者的创建代码如下。
\\npublic class DefaultKafkaConsumerFactory<K, V> extends KafkaResourceFactory\\n implements ConsumerFactory<K, V>, BeanNameAware {\\n\\n protected Consumer<K, V> createKafkaConsumer(Map<String, Object> configProps) {\\n return createRawConsumer(configProps);\\n }\\n protected Consumer<K, V> createRawConsumer(Map<String, Object> configProps) {\\n // KafkaConsumer 是 kafka-clients.jar 中的类\\n return new KafkaConsumer<>(configProps, this.keyDeserializerSupplier.get(),\\n this.valueDeserializerSupplier.get());\\n }\\n}\\n
\\n\\n\\n代码阅读小记:
\\n\\nKafkaListenerEndpointRegistry#start()\\n-> AbstractMessageListenerContainer#start()\\n-> ConcurrentMessageListenerContainer#doStart() (concurrency不能大于partitions)\\n-> KafkaMessageListenerContainer#start() -> doStart()\\n-> DefaultKafkaConsumerFactory#createRawConsumer()\\n
总结一下上文中各部分的代码阅读小记,得到如下代码链路:
\\n切入点: @EnableKafka\\n-> KafkaListenerConfigurationSelector\\n-> KafkaBootstrapConfiguration\\n [注册Bean:KafkaListenerAnnotationBeanPostProcessor]\\n [注册Bean:KafkaListenerEndpointRegistry]\\n-> KafkaListenerAnnotationBeanPostProcessor#postProcessAfterInitialization()\\n-> processKafkaListener()\\n-> processListener()\\n-> KafkaListenerEndpointRegistrar#registerEndpoint()\\n-> endpointDescriptors [注册到容器里List]\\n(===== 此时,程序里已经有endpointDescriptor了 =====)\\n\\n(===== 开始遍历endpointDescriptors =====)\\nKafkaListenerAnnotationBeanPostProcessor#afterSingletonsInstantiated()\\n-> KafkaListenerEndpointRegistrar#afterPropertiesSet()\\n-> registerAllEndpoints()\\n-> KafkaListenerEndpointRegistry#registerListenerContainer()\\n-> [listenerContainers] [注册到容器里Map]\\n\\n(===== 开始启动监听 =====)\\nKafkaListenerEndpointRegistry#start()\\n-> AbstractMessageListenerContainer#start()\\n-> ConcurrentMessageListenerContainer#doStart() (concurrency不能大于partitions)\\n-> KafkaMessageListenerContainer#start() -> doStart()\\n-> DefaultKafkaConsumerFactory#createRawConsumer()\\n
\\n文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\nMySQ技术内幕第5版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n深入浅出MySQL:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n高性能MySQL第三版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
在 MySQL InnoDB 存储引擎中,一次 INSERT
操作涉及 Undo Log(撤销日志)、Redo Log(重做日志)、Binlog(归档日志) 三种日志,它们的写入顺序如下:
日志类型 | 作用 | 位置 | 适用场景 |
---|---|---|---|
Undo Log | 记录修改前的数据,用于回滚 | InnoDB 内部 | 事务回滚、MVCC |
Redo Log | 记录数据修改,保证事务持久化 | InnoDB 内部 | 崩溃恢复 |
Binlog | 记录逻辑操作(SQL 语句或行变更) | MySQL Server 层 | 主从复制、数据恢复 |
INSERT
操作的日志写入顺序Undo Log → Redo Log (prepare) → Binlog → Redo Log (commit)\\n
\\n写入 Undo Log
\\nINSERT
操作不涉及回滚前的数据,因此 Undo Log
仅用于 MVCC)。Undo Log
主要用于 事务回滚,以及支持 快照读(MVCC) 。写入 Redo Log(Prepare 阶段)
\\nprepare
状态,表示事务已经执行,但尚未提交。写入 Binlog
\\nINSERT
语句或行数据到 Binlog
(归档日志)。Binlog
以追加写(append-only) 方式记录,并最终刷盘。提交 Redo Log(Commit 阶段)
\\nRedo Log
进入 commit
状态,标志事务完成。Redo Log
处于 commit
状态,则事务可恢复,否则回滚。Undo Log
先写入
Undo Log
记录数据的 旧值,在事务回滚或 MVCC 需要时使用。Redo Log
采用两阶段提交(2PC)
先写 Redo Log(Prepare):
\\nBinlog
时崩溃导致数据不一致。再写 Binlog:
\\nBinlog
,确保所有变更都可用于 主从复制 和 灾难恢复。Redo Log
提交后 MySQL 崩溃,导致主库和从库数据不一致。最后提交 Redo Log(Commit):
\\n保证 Binlog
和 Redo Log
的一致性
为了保证 Redo Log 和 Binlog 具有一致性,MySQL 采用两阶段提交(2PC,Two-Phase Commit) 机制:
\\nRedo Log
,标记为 prepare
状态。Redo Log
还未提交)。Binlog
,然后提交 Redo Log
。Binlog
写入失败,Redo Log
也不会提交,避免数据不一致。✅ 两阶段提交示例
\\n1. 写入 Undo Log\\n2. 写入 Redo Log(Prepare 阶段)\\n3. 写入 Binlog\\n4. 提交 Redo Log(Commit 阶段)\\n
\\n这种方式保证:
\\n假设 INSERT
语句:
sql\\nBEGIN;\\nINSERT INTO users (id, name) VALUES (1, \'Alice\');\\nCOMMIT;\\n
\\n步骤 | 操作 | 作用 |
---|---|---|
1 | Undo Log 写入 | 记录事务前状态(用于 MVCC) |
2 | Redo Log (prepare) | 记录事务的物理变更(但未提交) |
3 | Binlog 写入 | 记录逻辑 SQL 语句(用于恢复和复制) |
4 | Redo Log (commit) | 事务正式提交,数据落盘 |
崩溃发生在 prepare
阶段:
崩溃发生在 Binlog
写入后,但 Redo Log
未提交:
崩溃发生在 Redo Log
提交后:
Redo Log
恢复数据,确保持久化。INSERT
事务日志写入顺序Undo Log → Redo Log (prepare) → Binlog → Redo Log (commit)\\n
\\nUndo Log
先写入,保证事务可回滚,支持 MVCC。
Redo Log
采用两阶段提交(Prepare → Commit):
Prepare
阶段记录变更但不提交,避免 MySQL 崩溃导致数据不一致。Commit
阶段确保事务完整提交,保证持久化。Binlog
记录 SQL 逻辑变更,确保主从复制一致性。
✅ 崩溃恢复:Redo Log 记录事务变更,保证宕机后数据一致。 ✅ 数据一致性:两阶段提交(2PC)确保 Binlog 和 Redo Log 同步,避免主从数据不一致。 ✅ 高效查询:Undo Log 支持 MVCC,提高事务并发性能。
\\n🔥 优化建议 1️⃣ 使用 innodb_flush_log_at_trx_commit=1
确保事务提交后 Redo Log 持久化,防止丢失数据。 2️⃣ 使用 sync_binlog=1
让 Binlog
也同步刷盘,避免崩溃导致主从数据不一致。 3️⃣ 合理使用索引,减少 Undo Log
开销,提高查询性能。
就在前两天,Redis 8.0 正式版 (GA) 来了!这并不是一次简单的更新,Redis 8.0 不仅带来了性能上的进一步提升,还带来一些实用的新特性与功能增强。并且,最重要的是拥抱 AGPLv3 重归开源!
\\n下面,简单聊聊 Redis 8.0 到底带来了哪些重磅更新。
\\n2024 年 3 月,Redis 宣布将其许可证从 BSD 切换到 RSALv2/SSPLv1 双许可证。这在社区引起了轩然大波,因为 SSPLv1 并不被开源促进会(OSI)认可为真正的开源许可证,这让很多开发者和云厂商感到不满,甚至催生了像 Valkey 这样的社区分支。大家都在担心 Redis 是不是要离开源越来越远了。
\\n《Redis设计与实现》作者黄健宏老师当时也发表了自己的看法:
\\n在 Redis 8.0 中,他们做出了一个关键决定:在保留原有 RSALv2/SSPLv1 许可证的同时,新增了 OSI 批准的 AGPLv3 (Affero General Public License v3) 作为授权选项!
\\n为了体现决心,Redis 将其免费产品的名称从“Redis Community Edition”更改为“Redis Open Source”,更加强调其开源属性。
\\n这表明 Redis 公司还是比较重视社区的反馈,想要做出一些事情来改变。
\\n新增 Vector Set 数据结构 (Beta 版): 专为 AI 应用设计,用于存储和查询高维向量嵌入 (Vector Embeddings),极大地增强了 Redis 在向量相似性搜索 (VSS)、语义搜索、推荐系统等场景下的能力。它补充了 Redis 查询引擎中已有的向量搜索能力。
\\n原生支持 JSON 数据结构: 这意味着直接在 Redis 中存储和操作 JSON 文档。Redis 提供了基于 JSONPath 语法的命令,可以高效地访问和修改 JSON 文档中的特定元素。
\\n新增 5 种概率数据结构:
\\n不仅仅是简单的 Key-Value 查找!现在支持在 Hash 和 JSON 数据结构上创建二级索引。支持更复杂的查询:精确匹配、范围查询、全文搜索(支持词干提取、同义词扩展、模糊匹配)以及向量相似性搜索。
\\n对比 Redis 7.2.5 版本,Redis 8.0 引入了超过 30 项性能改进,多达 90 个常用命令的延迟降低了 5.4% 至 87.4% !大部分应用升级后都能感受到明显的性能提升。
\\nRedis 8.0 改进了自 Redis 6 以来的 I/O 线程实现。通过配置 io-threads
参数(例如设置为 8),在多核 CPU 上,吞吐量(每秒操作数)最高可提升 112% (约 2 倍)!
Redis 8.0 引入了新的复制机制,主从同步(特别是大数据集的全量同步+增量同步)期间,主节点处理写入操作的平均速率、复制总耗时都有明显提升。
\\nRedis Insight 和 Redis for VS Code 完全兼容 Redis 8.0。并且,Redis Insight 特别集成了 Redis Copilot(自然语言 AI 助手),用来提升数据和命令处理体验。
\\nRedis 8.0 算是一个里程碑的版本,拥抱 AGPLv3 重归开源,还引入了一些实用的新特性与功能增强。
\\n如果想要了解更多 Redis 8.0 的信息,可以查看官方文档:redis.io/blog/redis-… 。
\\nRedis 常见面试题总结,感兴趣的话,可以看看:
\\n从职业生涯伊始,笔者一直是微服务架构的坚定拥趸,这很大程度上源于长期深耕互联网行业,习惯了高并发、分布式架构下的开发模式。
\\n然而,去年在优化某航空公司核心用户中心系统时,笔者的技术认知受到了不小的冲击。
\\n因为航空公司用户中心是一个典型的单体应用——尽管业务量不小,但稳定的业务模型和有限的扩展需求,使得单体架构反而比微服务更高效、更经济。
\\n事实上,单体应用在 IT 领域始终占据着不可替代的生态位。甚至可以说,在大多数业务场景下,单体架构不仅完全够用,甚至可能是更优解。
\\n它的价值主要体现在:开发效率高、运维复杂度低,尤其适合业务边界清晰、迭代节奏可控的中小型系统。
\\n笔者在知乎、Github 上搜索不少快速开发框架 ,很多的话题都绕不开若依 RuoYi 。
\\n开源世界 RuoYi 单体框架有三个不同的项目,分别是:ruoyi-vue 、ruoyi-vue-plus 、ruoyi-vue-pro 。
\\n这三个项目,笔者把它们的源码基本都过了一次,接下来分享下学习心得。
\\nRuoYi 作为国内流行的 Java 快速开发框架,衍生出了多个版本,主要分为 RuoYi(经典单体版)、RuoYi-Vue(前后端分离单体版)、RuoYi-Cloud(微服务版) 。
\\nRuoYi-Vue 基于经典技术组合(Spring Boot、Spring Security、MyBatis、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。
\\n对于后台管理系统来讲,RuoYi-Vue 实现的功能还是很全的,基本做到了开箱即用。
\\n同时,RuoYi-Vue 的文档非常细致 ,且全部公开。
\\n接下来,我们看下源码:
\\n笔者认为 RuoYi-Vue 的技术栈非常朴实,代码实现很简洁,没有用各种奇技淫巧,对于中小公司来讲,非常利于二次开发。
\\n知乎上的反馈也是不错,很多人不乏溢美之词。
\\n网友 1:
\\n\\n\\n先坚决表明态度,ruoyi牛逼不接受反驳。
\\n所有喷ruoyi垃圾的人,我在这里等着和你们正面对线。
\\n若依用的技术都是行业主流技术,而且代码规范的,框架设计很简洁,没有过度封装的东西,简单易上手。java是所有语言里水最深的,java程序员水平良莠不齐,但是若依这个框架真正做到了适合大众,有能力的开发可以自己往里加东西,框架的简洁不过度封装支持你往里各种塞技术,没能力的就凑合着用基础版,基础的东西都有绝对够用了。并且现在的若依越来越为大众所熟知,生态越来越多样,作者一直开源不管是文档还是源码。请问这么一个框架,难道不能称之为程序员的福音么???
\\n
网友 2:
\\n\\n\\n没有若依之前,小型的IT企业、开发团队、个人,想要独立完成一个企业项目,是一个个非常困难的故事。若依的出现,把项目的准入门槛,一下拉低了
\\n很多人力非常有限的小团体,基本上可以站在若依的肩膀上,也能做独立的企业项目。若依帮这些小团队,渡过最艰难的起步期。可以这么说,若依是唯一能帮人搞定事的人。而其他呢,除了能打嘴炮,都是然并卵用的人。
\\n
网友 3:
\\n\\n\\n有他之前,企业想开发个后台项目起步打底得 50 万,之后复制粘贴成本递减。若依把这个起步门槛降低到 5000,所以一票小软件开发公司没生意,死了。
\\n
RuoYi-Vue-Plus 是开源组织 Dromara 旗下一款多租户权限管理系统。
\\n相比 RuoYi-Vue , RuoYi-Vue-Plus 的功能更加完善。
\\nRuoYi-Vue-Plus 增强了多租户、文件存储、短信服务、脱敏、Redis 框架、SSE 等增强功能。
\\n项目的文档也非常全,核心功能基本都有对应的文档。
\\n最后,我们看下项目源码:
\\n相比 ruoyi-vue , RuoYi-Vue-Plus 的模块分布更加清晰了。
\\n笔者觉得 RuoYi-Vue-Plus 项目还是很优秀的,比如前后端加密通讯、文件存储、WebSocket/SSE 推送模块这些都非常有学习价值。
\\nruoyi-vue-pro 也是一款后台快速开发平台,Github 上 star 数非常高。
\\n模块设计角度来看,它还内置了很多的功能,比如商城、ERP 、三方支付、三方登录、AI 大模型等等。
\\n框架本身提供了支持不同的 JDK 和 SpringBoot 的版本。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n版本 | JDK 8 + Spring Boot 2.7 | JDK 17/21 + Spring Boot 3.2 |
---|---|---|
【完整版】ruoyi-vue-pro | master 分支 | master-jdk17 分支 |
【精简版】yudao-boot-mini | master 分支 | master-jdk17 分支 |
前端支持三种技术选型 :
\\n从整体来看,ruoyi-vue-pro 的作者还是花费了很多心血的。
\\n但笔者在整合中还是遇到了不少问题,核心问题是:内置模块太多。
\\nruoyi-vue-pro 设计里有不少亮点,比如分布式锁、Redisson 接入、限流等等。
\\n笔者新增了 Token 模块、Id 生成器两个模块,精简部分模块:
\\n系统界面:
\\n评估维度 | RuoYi-Vue | RuoYi-Vue-Plus | RuoYi-Vue-Pro |
---|---|---|---|
核心优势 | 极简开箱即用 | 多租户/功能增强 | 全生态功能预制 |
二次开发 | ⭐⭐⭐⭐⭐(源码简洁) | ⭐⭐⭐☆(需理解模块化) | ⭐⭐(需深度裁剪) |
成本效益 | 人力/时间成本最低 | 中等投入高回报 | (中等偏上)需评估功能利用率 |
RuoYi-Vue 的架构和分层非常适合新手入门,文档详细,社区活跃,最重要的是作者在持续维护。
\\n因此,它是笔者心中快速开发平台的王者。
","description":"从职业生涯伊始,笔者一直是微服务架构的坚定拥趸,这很大程度上源于长期深耕互联网行业,习惯了高并发、分布式架构下的开发模式。 然而,去年在优化某航空公司核心用户中心系统时,笔者的技术认知受到了不小的冲击。\\n\\n因为航空公司用户中心是一个典型的单体应用——尽管业务量不小,但稳定的业务模型和有限的扩展需求,使得单体架构反而比微服务更高效、更经济。\\n\\n事实上,单体应用在 IT 领域始终占据着不可替代的生态位。甚至可以说,在大多数业务场景下,单体架构不仅完全够用,甚至可能是更优解。\\n\\n它的价值主要体现在:开发效率高、运维复杂度低,尤其适合业务边界清晰…","guid":"https://juejin.cn/post/7501235908777836553","author":"勇哥Java实战","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-07T06:58:43.077Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2670af0ff204903a3dd599c7eec1a55~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=MBMx6CU%2F1s6US5aJ1xWUkvLZ1l4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b379252e4b247568cc148a5778f87fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=MnD2DHrwRDP2sKCl6lNYD9fcFWM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ade0044c47934b19bd53baf6a7cab427~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=2acd72OXNo5But2kCGv1HMTDBAQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d556ba77ba9640c5995fb6d905c6dcab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=oGVZ06z8lhqmn6Cx0gxkeMDa%2B0I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0c917f96a0ef4565abbf99c55bebcc18~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=%2BUEo1rgFms45MAJF3FtErZrezJ8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/599d066a2b0a43cb8bd3ac078f551e27~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=m2g9ZqfD2qXvq3l4KjqWDpBJebM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bad57c3bd7ee4cf58a3260abc2cb8efe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=TdNdSMOZFRn1zeZE%2FlH2AOXfm%2FE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/571459aef6884602a14385f358568d04~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=V3nGmOLEyxnENuVZ15ZJuakQEoI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d5b54c5c4cd4d758da06f16e6da18b5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=4kxKCcpqqTDojBNhrdBEIKcORDc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3748c7b2a89b4395a131b1def4199eed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=EKnA%2Fj6gavSKl%2BNGnLpMHdhXqts%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/227e3813133444a985ebe2a96911d996~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=2e7BGr9GkK73q3fywzHCwqVzxZM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6828f4136b0f43a7be25cab2a2019d12~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=1BAZe6S6HqYIR%2B%2B6JPuLHD67yY4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9befad5163fd4d77a6b4f788ad2f6300~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YuH5ZOlSmF2YeWunuaImA==:q75.awebp?rk3s=f64ab15b&x-expires=1747205923&x-signature=3ci%2F2TdF22MEUMxeNt5s0bu1fo4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"为什么不推荐使用@Transactional声明事务","url":"https://juejin.cn/post/7501244292184948748","content":"在日常 Spring 开发中,我们经常看到如下代码:
\\n@Transactional\\npublic void saveUser(User user) {\\n userRepository.save(user);\\n log.info(\\"User saved\\");\\n}\\n
\\n只需一个注解 @Transactional
,开发者就可以轻松开启事务。它用起来确实简单,但你是否真正了解它的工作原理?在一些复杂或易变的业务场景中,@Transactional
其实并不是最佳选择。本文将介绍 Spring 的两种事务管理方式,并解释为什么你可能不该总是依赖 @Transactional
。
Spring 提供两种主要的事务管理方式:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方式 | 使用形式 | 常见场景 |
---|---|---|
声明式事务管理(@Transactional) | 使用注解在方法或类上标记 | 简单业务逻辑、标准的业务服务层 |
编程式事务管理(TransactionTemplate) | 显式调用模板执行事务 | 复杂逻辑、多事务组合、可控性强的场景 |
@Service\\npublic class UserService {\\n\\n @Autowired\\n private UserRepository userRepository;\\n\\n @Transactional\\n public void createUser(User user) {\\n userRepository.save(user);\\n // 模拟异常\\n if (true) {\\n throw new RuntimeException(\\"模拟异常\\");\\n }\\n }\\n}\\n
\\n@Service\\npublic class OrderService {\\n\\n public void outerMethod() {\\n innerTransactionalMethod(); // 无效!\\n }\\n\\n @Transactional\\n public void innerTransactionalMethod() {\\n // 事务不会生效\\n }\\n}\\n
\\n@Transactional\\npublic void updateUser() throws IOException {\\n userRepository.save(user);\\n throw new IOException(); // 不会回滚!\\n}\\n
\\n事务只对当前线程有效,线程池或异步任务中的事务不会自动传播。
\\n@Transactional\\npublic void updateUser() throws IOException {\\n userRepository.save(user);\\n // 远程调用消息服务\\n messageApi.sendMessage(user); //远程调用不受事务控制,可能导致事务超时或数据不一致\\n}\\n
\\n@Service\\npublic class UserService {\\n\\n @Autowired\\n private TransactionTemplate transactionTemplate;\\n\\n @Autowired\\n private UserRepository userRepository;\\n\\n public void createUser(User user) {\\n transactionTemplate.executeWithoutResult(status -> {\\n try {\\n userRepository.save(user);\\n if (true) throw new RuntimeException(\\"模拟异常\\");\\n } catch (Exception e) {\\n status.setRollbackOnly();\\n throw e;\\n }\\n });\\n }\\n}\\n
\\n特性 | @Transactional | TransactionTemplate |
---|---|---|
使用简便性 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
灵活性 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
异常控制 | ⭐⭐(需配置) | ⭐⭐⭐⭐⭐(手动) |
内部方法事务 | ❌(无效) | ✅ |
代码清晰度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
多线程支持 | ❌ | ✅(手动管理) |
虽然 @Transactional
看起来更优雅,但它隐藏了很多细节和坑,在中大型项目或高复杂度业务系统中,这种“隐藏的魔法”常常导致不可预期的结果。而 TransactionTemplate
虽然代码更多,却明确可控,更适合团队协作、复杂流程、以及代码可读性更重要的场合。
\\n\\n优雅不是省代码,而是写出“让人一眼看懂”的逻辑。
\\n
当然了,如果你的团队中每个人都能避免@Transactional
潜在的问题,那么使用@Transactional
也没有问题,这是比较理想的情况
本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
\\n最近很多同学面试都被问到了这个问题,但他们给出的技术方案并不能让面试官满意,我在本文中给出一个无法辩驳的最优解。
\\n目前主流的技术方案有如下四种:基于RocketMQ的延时队列方案、基于Redis ZSet的延时队列方案、基于Redis的过期监听方案,以及基于XXL-JOB的定时轮询方案。
\\nRocketMQ是直接支持延时队列的,在其4.X版本:支持18个固定延迟级别
\\n(1s/5s/10s/30s/1m/.../30m/1h/2h),需根据业务需求选择合适的级别。
\\n生产者端代码如下:
\\nMessage msg = new Message(\\"ORDER_DELAY_TOPIC\\", \\n \\"订单已创建,等待支付\\".getBytes());\\n// 设置延迟级别(对应30分钟:RocketMQ预设level=16)\\nmsg.setDelayTimeLevel(16); \\nSendResult sendResult = producer.send(msg);\\n
\\n消费者端代码如下:
\\nconsumer.subscribe(\\"ORDER_DELAY_TOPIC\\", \\"*\\", new MessageListener() {\\n public Action consume(Message message, ConsumeContext context) {\\n String orderId = parseOrderId(message);\\n if (checkOrderUnpaid(orderId)) { // 校验订单未支付\\n cancelOrder(orderId); // 执行取消操作\\n }\\n return Action.CommitMessage;\\n }\\n});\\n
\\n而到了RocketMQ 5.X版本,就支持任意时刻的延迟消息了,可通过客户端API直接指定延迟时间,灵活性更高。
\\nRedis Zset(SortedSet),是Set的可排序版,是通过增加一个排序属性score来实现的,适用于排行榜和时间线之类的业务场景。
\\n如下图所示,这里的score属性对应的是销售额:
\\n我们可以通过Redis Zset来实现延时队列的功能,具体思路是:在生产者端向Zset中添加元素,并设置score值为元素过期的时间。
\\n在消费端对Zset进行轮询,将元素的score值与当前时间进行比对,将小于当前时间的过期key进行移除。
\\n生产者端代码如下:
\\n// 用户下单后,设置30分钟超时\\nlong cancelTime = System.currentTimeMillis() + 30 * 60 * 1000;\\nredisTemplate.opsForZSet().add(\\"order:delay:queue\\", orderId, cancelTime);\\n
\\n消费者端代码如下:
\\n // 获取当前时间前到期的订单ID集合(0表示从最小分数开始)\\n Set<String> expiredOrders = redisTemplate.opsForZSet().rangeByScore(\\n \\"order:delay:queue\\", 0, now);\\n\\n if (expiredOrders != null && !expiredOrders.isEmpty()) {\\n // 原子操作:删除已处理订单并返回实际删除数量\\n Long removedCount = redisTemplate.opsForZSet().remove(\\n \\"order:delay:queue\\", expiredOrders.toArray(new String[0]));\\n if (removedCount > 0) {\\n // 批量取消订单(幂等处理)\\n batchCancelOrders(expiredOrders);\\n }\\n }\\n Thread.sleep(1000);\\n}\\n
\\n基于Redis的过期监听实现取消订单的方案,核心是利用Redis的键过期事件,在订单的取消时间到达时自动触发取消逻辑。
\\n修改redis.conf文件:
notify-keyspace-events Ex # 启用键过期事件\\n
\\n订单创建代码:
\\n// Spring Boot 示例(RedisTemplate)\\npublic void createOrder(Order order) {\\n // 写入数据库(状态为未支付)\\n orderRepository.save(order);\\n\\n // 设置 Redis 键,30分钟过期\\n String key = \\"order:unpaid:\\" + order.getId();\\n redisTemplate.opsForValue().set(key, order.getId());\\n redisTemplate.expire(key, 30, TimeUnit.MINUTES);\\n}\\n
\\n订单过期事件监听:
\\n@Component\\npublic class RedisKeyExpirationListener {\\n @Autowired\\n private OrderService orderService;\\n\\n @EventListener\\n public void handleKeyExpiration(RedisKeyExpiredEvent<?> event) {\\n String orderId = new String(event.getId());\\n if (orderId.startsWith(\\"order:unpaid:\\")) {\\n orderService.cancelOrder(orderId.replace(\\"order:unpaid:\\", \\"\\"));\\n }\\n }\\n}\\n
\\n基于XXL-JOB的定时轮询实现取消订单的方案,是一种通过分布式任务调度平台定时扫描数据库,取消超时未支付订单的方式。
\\nXXL-JOB调度中心:作为核心控制平台,负责任务的配置、触发和状态记录,支持集群化部署,保障任务调度的高可用性。
\\n业务系统执行器:集成XXL-JOB客户端,接收调度中心指令,执行订单取消逻辑,包括订单状态更新、库存释放等操作。
\\n执行器代码如下:
\\n@XxlJob(\\"cancelExpiredOrdersJob\\")\\npublic void cancelExpiredOrders() {\\n // 查询超时未支付订单\\n List<Order> expiredOrders = orderService.findExpiredOrders();\\n for (Order order : expiredOrders) {\\n try {\\n // 执行取消订单操作,如更新状态、释放库存等\\n orderService.cancelOrder(order.getId());\\n log.info(\\"成功取消订单: {}\\", order.getId());\\n } catch (Exception e) {\\n log.error(\\"取消订单失败,订单ID: {}\\", order.getId(), e);\\n }\\n }\\n}\\n
\\n我们在本文开头中提到过,最近很多同学面试都被问到了这个问题,但他们给出的技术方案并不能让面试官满意,当然也包括上述四种技术方案。
\\n因为面试官通常会问如下两个问题:
\\n1、你通过RocketMQ/Redis/XXL-JOB来实现的取消超时未支付的订单,那如果RocketMQ/Redis/XXL-JOB挂了怎么办?
\\n2、RocketMQ/Redis/XXL-JOB可能会存在处理延迟的问题,不能在30分钟精准地取消过期未支付订单,那你要如何解决?
\\n而这两个问题通常会把面试经验不足的同学难住。
\\n对于第一个问题,确实在RocketMQ和Redis挂了且没做Plan B的情况下,那只能寄希望于RocketMQ和Redis集群快点儿恢复了。
\\n这还不算完,还需要对故障时间内的过期未关单数据进行手动处理。
\\n但XXL-JOB就不一样了,就算它的调度中心集群宕机了,我们仍然可以直接向执行器发送请求即可触发任务,或者通过一个操作系统任务定时发送请求。
\\n格式如下:
\\nPOST http://执行器IP:端口/run\\nContent-Type: application/json\\n\\n{\\n \\"jobId\\": 任务ID,\\n \\"executorHandler\\": \\"任务处理器名称\\",\\n \\"executorParams\\": \\"任务参数\\",\\n \\"logId\\": 日志ID(可随机生成),\\n \\"broadcastIndex\\": 0,\\n \\"logDateTime\\": 当前时间戳\\n}\\n
\\n对于第二个问题,其实根本就TMD不是问题。
\\n我们试想一下,如果用户在第30分01秒对未支付订单进行支付了,那到底是给公司带来损失了,还是带来收入了?
\\n显然是后者!
\\n所以做技术不能死做技术,应该去结合业务场景去制定技术方案,取消超时未支付的订单本质上是为了让订单进入终态,以解决财务结算的问题而已。
\\n当然,从技术方案上也是有解的,我们想想在Redis底层是如何清理过期Key的?
\\nRedis用的是定期清理和惰性清理相结合的方式。
\\n定期清理
\\nRedis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的key。
\\nRedis默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略。
\\n(1)从过期字典中随机取出20个key。
\\n(2)清理这20个key中已经过期的key。
\\n(3)如果过期的key比率超过25%,那就再重复执行一次步骤(1)(2)。
\\n同时,Redis为保证定期扫描不会出现“贪心”过度,从而导致线程卡死现象,在算法上还增加了扫描时间的上限,默认不会超过25ms。
\\n惰性删除
\\n在客户端访问某个key的时候,Redis对该key的过期时间进行检查,如果过期了就立即删除。
\\n回到原题上,如果真的需要精准地取消超时未支付的订单,那我们参考Redis的惰性删除策略,在订单支付的时候进行二次校验就好了。
\\n所以,解决取消超时未支付的订单的终极方案,就是基于XXL-JOB的定时轮询 + 订单支付时二次校验兜底。
\\n这样可以在功能可用性、开发复杂性和处理时延性上达到最优解。
","description":"本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~ 最近很多同学面试都被问到了这个问题,但他们给出的技术方案并不能让面试官满意,我在本文中给出一个无法辩驳的最优解。\\n\\n目前主流的技术方案有如下四种:基于RocketMQ的延时队列方案、基于Redis ZSet的延时队列方案、基于Redis的过期监听方案,以及基于XXL-JOB的定时轮询方案。\\n\\n1、基于RocketMQ的延时队列方案\\n\\nRocketMQ是直接支持延时队列的,在其4.X版本:支持18个固定延迟级别\\n\\n(1s/5s/10s/30s/1m/.../30m/1h…","guid":"https://juejin.cn/post/7501233551037775924","author":"托尼学长","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-07T02:28:43.555Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/99a6c2b8d3f24d359beb63fe9cac0768~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1747189723&x-signature=w4CWQPQ8Ks4swK9t%2FCzfZJWWZJk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c5c67730409f4b51a903dcc336136ba8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1747189723&x-signature=UWyvBF4amJY7vWoHeJjqoqTwRC4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b8577518423f47799e582c650d36895e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1747189723&x-signature=GrExdH1dsLLRzyycbJE3WDlelMY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构","Java"],"attachments":null,"extra":null,"language":null},{"title":"这种小工具居然也能在某鱼卖钱?我用Python一天能写100个,纯干货!","url":"https://juejin.cn/post/7501221695550914575","content":"前两天在某鱼闲逛,本来想找个二手机械键盘,结果刷着刷着突然看到有人在卖——Word 批量转 PDF 小工具,还挺火,价格也不高,但销量出奇地高,评论里一堆人在夸“好用”、“终于不用一篇篇点了”啥的。
\\n说实话,当时我人都愣住了——
\\n这个功能我用 Python 十分钟能写完啊!
\\n然后我又搜了其它小工具,pdf转Word,Word转图片,Word加水印什么的……好多
\\n好家伙,花姐以前教大家做的办公自动化小工具原来都能卖钱呀!
\\n那咱今天先复刻一个Word 批量转 PDF 小工具,顺便升级点功能,做个更丝滑的版本。
\\n保准你看完就能自己写个卖钱去。
\\n你别看这功能听起来挺“高端”的,其实本质上干的事就是——
\\n\\n\\n把一堆 Word 文档用程序打开,然后保存为 PDF 格式。
\\n
换句话说,这活本质就是个“批处理”。用 Python 来干,简直再合适不过。
\\n我们需要的工具是 python-docx
?NoNoNo——这个库不支持保存为 PDF。真正的主角其实是:
win32com.client
:用来操作 Word 应用(需要 Windows 系统+装了 Office)好,开门见山,先上最基础的版本:
\\nimport os\\nimport win32com.client\\n\\ndef word_to_pdf(input_path, output_path):\\n word = win32com.client.Dispatch(\\"Word.Application\\")\\n word.Visible = False # 不弹窗,后台运行\\n doc = word.Documents.Open(input_path)\\n doc.SaveAs(output_path, FileFormat=17) # 17 是 PDF 格式\\n doc.Close()\\n word.Quit()\\n\\n# 示例用法\\nword_to_pdf(\\"C:/Users/你的用户名/Desktop/测试文档.docx\\", \\n \\"C:/Users/你的用户名/Desktop/测试文档.pdf\\")\\n
\\nDispatch(\\"Word.Application\\")
就是打开 Word 应用;FileFormat=17
是告诉它“嘿,我要存成 PDF”;Quit()
很重要,不然 Word 可能会在后台一直挂着,占资源。Dispatch(\\"Word.Application\\")
这里改成Dispatch(\\"kwps.Application\\")
,不然会报错是不是很简单?连我猫都看懂了。
\\n很多人痛苦的点是“文档太多,一个个转太麻烦”。
\\n那好说,我们搞个批量版本,让它一口气全转了:
\\ndef batch_convert(folder_path):\\n word = win32com.client.Dispatch(\\"Word.Application\\")\\n word.Visible = False\\n\\n for file in os.listdir(folder_path):\\n if file.endswith(\\".doc\\") or file.endswith(\\".docx\\"):\\n doc_path = os.path.join(folder_path, file)\\n pdf_path = os.path.splitext(doc_path)[0] + \\".pdf\\"\\n try:\\n doc = word.Documents.Open(doc_path)\\n doc.SaveAs(pdf_path, FileFormat=17)\\n doc.Close()\\n print(f\\"✅ 转换成功:{file}\\")\\n except Exception as e:\\n print(f\\"❌ 转换失败:{file},原因:{e}\\")\\n\\n word.Quit()\\n
\\nbatch_convert(r\\"C:\\\\Users\\\\你的用户名\\\\Desktop\\\\word文件夹\\")\\n
\\n写得简单不难,难的是兼容和细节。
\\n这玩意底层其实就是用 COM 调用了 Word 的功能,所以没有装 Word 是用不了的。
\\n有些文档打开会弹窗提示宏或者密码,那个得手动改设置,程序跑不过去。
\\n有时候路径太奇怪,Word 会打不开,转不了,建议统一放到纯英文文件夹里。
\\ndef gen_output_folder():\\n folder = os.path.dirname(os.path.abspath(__file__))\\n timestamp = datetime.datetime.now().strftime(\\"%Y%m%d_%H%M%S\\")\\n output_folder = os.path.join(folder, f\\"pdf_{timestamp}\\")\\n os.makedirs(output_folder, exist_ok=True)\\n return output_folder\\n
\\n这太简单了:
\\nimport os\\n\\ndef get_word_files_from_current_folder():\\n folder = os.path.dirname(os.path.abspath(__file__))\\n word_files = []\\n for file in os.listdir(folder):\\n if file.endswith(\\".doc\\") or file.endswith(\\".docx\\"):\\n word_files.append(os.path.join(folder, file))\\n return word_files\\n
\\n我们可以尝试用 win32com.client.gencache.EnsureDispatch()
去判断这两个程序是否存在。
import win32com.client\\n\\ndef detect_office_or_wps():\\n try:\\n word = win32com.client.gencache.EnsureDispatch(\\"Word.Application\\")\\n return \\"office\\"\\n except:\\n try:\\n wps = win32com.client.gencache.EnsureDispatch(\\"Kwps.Application\\")\\n return \\"wps\\"\\n except:\\n return None\\n
\\nimport os\\nimport win32com.client\\n\\ndef convert_word_to_pdf_auto(input_path, output_path, engine):\\n if engine == \\"office\\":\\n app = win32com.client.Dispatch(\\"Word.Application\\")\\n elif engine == \\"wps\\":\\n app = win32com.client.Dispatch(\\"Kwps.Application\\")\\n else:\\n print(\\"❌ 没有检测到可用的 Office 或 WPS\\")\\n return\\n\\n app.Visible = False\\n\\n try:\\n doc = app.Documents.Open(input_path)\\n doc.SaveAs(output_path, FileFormat=17)\\n doc.Close()\\n print(f\\"✅ 转换成功:{input_path}\\")\\n except Exception as e:\\n print(f\\"❌ 转换失败:{input_path},原因:{e}\\")\\n\\n try:\\n app.Quit()\\n except:\\n print(\\"⚠️ 当前环境不支持 Quit,跳过退出。\\")\\n
\\ndef batch_convert_here():\\n engine = detect_office_or_wps()\\n if not engine:\\n print(\\"😭 系统里没有安装 Office 或 WPS,没法转换\\")\\n return\\n\\n folder = os.path.dirname(os.path.abspath(__file__))\\n word_files = get_word_files_from_current_folder()\\n\\n if not word_files:\\n print(\\"🤷♀️ 当前文件夹没有发现 Word 文件\\")\\n return\\n\\n output_folder = os.path.join(folder, \\"pdf输出\\")\\n os.makedirs(output_folder, exist_ok=True)\\n\\n for word_file in word_files:\\n filename = os.path.splitext(os.path.basename(word_file))[0]\\n pdf_path = os.path.join(output_folder, f\\"{filename}.pdf\\")\\n convert_word_to_pdf_auto(word_file, pdf_path, engine)\\n\\n print(\\"🎉 所有文件转换完成啦!PDF 都在 \'pdf输出\' 文件夹里\\")\\n
\\n🧪 运行方式(放在脚本结尾):
\\nif __name__ == \\"__main__\\":\\n batch_convert_here()\\n
\\n最后一步,把咱的脚本打包成 .exe
,丢到某鱼卖钱(手动狗头🐶)
命令就一句话:
\\npyinstaller -F word2pdf.py\\n
\\n生成的 dist/word2pdf.exe
就是可执行文件,随便拿给谁用都行(当然系统要有 Word)。
import os\\nimport win32com.client\\nimport sys\\nimport datetime\\n\\ndef get_real_path():\\n \\"\\"\\"兼容开发与打包环境的路径获取\\"\\"\\"\\n if getattr(sys, \'frozen\', False):\\n base_dir = os.path.dirname(sys.executable) # EXE文件所在目录[1,7](@ref)\\n else:\\n base_dir = os.path.dirname(os.path.abspath(__file__))\\n \\n return base_dir\\n\\n# 生成时间戳文件夹\\ndef gen_output_folder(folder):\\n # folder = os.path.dirname(os.path.abspath(__file__))\\n timestamp = datetime.datetime.now().strftime(\\"%Y%m%d_%H%M%S\\")\\n output_folder = os.path.join(folder, f\\"pdf_{timestamp}\\")\\n os.makedirs(output_folder, exist_ok=True)\\n return output_folder\\n \\n# 自动获取当前脚本目录下的 Word 文件\\ndef get_word_files_from_current_folder(folder):\\n # folder = os.path.dirname(os.path.abspath(__file__))\\n word_files = []\\n for file in os.listdir(folder):\\n if file.endswith(\\".doc\\") or file.endswith(\\".docx\\"):\\n word_files.append(os.path.join(folder, file))\\n return word_files\\n\\n# 检测 Office 和 WPS 的方法\\ndef detect_office_or_wps():\\n try:\\n word = win32com.client.gencache.EnsureDispatch(\\"Word.Application\\")\\n return \\"office\\"\\n except:\\n try:\\n wps = win32com.client.gencache.EnsureDispatch(\\"Kwps.Application\\")\\n return \\"wps\\"\\n except:\\n return None\\n\\n# 自动选择引擎并批量转换\\ndef convert_word_to_pdf_auto(input_path, output_path, engine):\\n if engine == \\"office\\":\\n app = win32com.client.Dispatch(\\"Word.Application\\")\\n elif engine == \\"wps\\":\\n app = win32com.client.Dispatch(\\"Kwps.Application\\")\\n else:\\n print(\\"没有检测到可用的 Office 或 WPS\\")\\n return\\n\\n app.Visible = False\\n\\n try:\\n doc = app.Documents.Open(input_path)\\n doc.SaveAs(output_path, FileFormat=17)\\n doc.Close()\\n print(f\\"转换成功:{input_path}\\")\\n except Exception as e:\\n print(f\\"转换失败:{input_path},原因:{e}\\")\\n\\n try:\\n app.Quit()\\n except:\\n print(\\"当前环境不支持 Quit,跳过退出。\\")\\n\\n# 主函数 \\ndef batch_convert_here():\\n engine = detect_office_or_wps()\\n if not engine:\\n print(\\"系统里没有安装 Office 或 WPS,没法转换\\")\\n return\\n\\n folder = get_real_path()\\n word_files = get_word_files_from_current_folder(folder)\\n \\n\\n if not word_files:\\n print(\\"当前文件夹没有发现 Word 文件\\")\\n return\\n\\n output_folder = gen_output_folder(folder)\\n\\n for word_file in word_files:\\n filename = os.path.splitext(os.path.basename(word_file))[0]\\n pdf_path = os.path.join(output_folder, f\\"{filename}.pdf\\")\\n convert_word_to_pdf_auto(word_file, pdf_path, engine)\\n\\n print(\\"所有文件转换完成啦!PDF 都在 \'output_folder\' 文件夹里\\")\\n \\nif __name__ == \\"__main__\\":\\n try:\\n batch_convert_here()\\n print(\\"按 Enter 键退出...\\")\\n input() # 等待用户按 Enter 键\\n except Exception as e:\\n print(e)\\n print(\\"程序运行错误,按 Enter 键退出...\\")\\n input() # 等待用户按 Enter 键\\n
\\n你可能觉得:“这不就是几十行代码嘛,卖这个会有人买吗?”
\\n我一开始也这么想。后来我想通了,某鱼上很多买家,根本不懂技术,他们在意的是:
\\n✅ 能不能一键搞定?
\\n✅ 会不会太复杂?
\\n✅ 省不省事?
所以啊,写工具 + 提供说明 + 包装打包,这些就构成了“产品”。
\\n我们程序员有时候太低估自己的能力了——其实你随手写的脚本,真的能解决很多人的问题。
","description":"前两天在某鱼闲逛,本来想找个二手机械键盘,结果刷着刷着突然看到有人在卖——Word 批量转 PDF 小工具,还挺火,价格也不高,但销量出奇地高,评论里一堆人在夸“好用”、“终于不用一篇篇点了”啥的。 说实话,当时我人都愣住了——\\n\\n这个功能我用 Python 十分钟能写完啊!\\n\\n然后我又搜了其它小工具,pdf转Word,Word转图片,Word加水印什么的……好多\\n\\n好家伙,花姐以前教大家做的办公自动化小工具原来都能卖钱呀!\\n\\n那咱今天先复刻一个Word 批量转 PDF 小工具,顺便升级点功能,做个更丝滑的版本。\\n\\n保准你看完就能自己写个卖钱去。\\n\\n💡思路先…","guid":"https://juejin.cn/post/7501221695550914575","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-07T01:20:39.582Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c388b5a764e8408bb9dc8e3c40ad72fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747185639&x-signature=XG0tEoV4t99PH6gGvdz0GPveWr0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70c1b7e1bdbe4523b09cb82da4cfa53f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747185639&x-signature=xHPPeyaQF0fe%2BOdsJIu0G3Sa5hc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"字节一面:20亿手机号存储选int还是string?varchar还是char?为什么?","url":"https://juejin.cn/post/7501105891158310966","content":"大家好,我是田螺。
\\n最近一位星球粉丝说,他去面试了字节,问了这么一道题,20亿手机号存储,选int还是string?varchar还是char?为什么?
\\n他支支吾吾回答了几句,好像看起来,面试官面色凝重,对他不是很满意,果然最好还是挂了。。。
\\n本文跟大家聊聊我的思路。
\\n首先,我们都知道手机号,是11位的数字,比如13728199213
.
在Java中,int是 32位,最大值为 2^31 - 1 = 2,147,483,647
。约等于 2×10⁹。显然,如果用int,根本存不下 11位的手机号码。
要想存的下,得用64位的Long类型,也就是对应数据库的bigInt。
\\n例如手机号01324567890,用Long存会变成1324567890,直接破坏数据完整性。
\\nLong phoneNumber =01324567890L; //编译报错,Java不允许前导0的Long整数 \\n
\\n并且,有时候,有些手机号可能包含国家代码如(+86)
,或者有些时候,是有连字符的,比如137-2819-9213
. 这些原因都导致不能用整型类型存储。
比如,你要查找,手机号是137
开头的手机号号码,如果用BigInt(Long类型)需先转字符串再模糊匹配,效率暴跌。
CREATE TABLE user_tab (\\n id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT \'用户ID\',\\n phone_number VARCHAR(20) NOT NULL COMMENT \'手机号\',\\n PRIMARY KEY (id),\\n UNIQUE KEY idx_phone (phone_number)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=\'用户表\';\\n
\\n面试的时候,面试官主要考察候选人的一些业务扩展性、数据容错性、思考问题全面性等能力。我们先通过:为什么用 VARCHAR(20) 而不是 VARCHAR(11),来给面试官秀一波肌肉~~
\\n我们就拿手机号来说,为什么更建议用 VARCHAR(20),而不是VARCHAR(11)呢?
\\n因为我们都知道,手机号是11
位的,为什么不直接用VARCHAR(11)
呢?
如果你日常开发中,就有思考数据容错性习惯的话,就会想到:
\\n这些场景,都会导致VARCHAR(11)
报错崩盘。
其次就是业务扩展性思考:VARCHAR(11)只能存纯11位数字,假设未来业务需要:
\\n因此,字段长度和类型需提前为业务变化留余地,避免频繁改表。这就是日常开发中的,业务扩展性思维思考。
\\n还有数据容错性思考,
\\n还有思考问题全面性,比如存储成本思考。
\\n所以面试官期待的答案公式
\\n合理长度 = 基础需求 + 国际扩展 + 容错缓冲 \\n
\\n当然,这个不是固定答案,主要还是面试的时候,你回答面试官的思路和表达,最好体现你有这几个方面的思考:业务扩展性、数据容错性、思考问题全面性。
\\n如果手机号是纯数字,并且第一位不是0的话,可以用BIGINT的,但是永远不要使用INT。通过这些极端场景的举例,也体现你思考问题全面性的一个能力。
\\n设计手机号存储的时候,有哪些需要避的坑的。
\\n主要有这几个吧:
\\n用 VARCHAR(11) 只存纯数字,遇到 +8613822223333(14位)直接截断。
\\n\\n\\n用 VARCHAR(20) 兼容国际号、分机号(如 13822223333#123)。\'
\\n
使用 utf8 字符集,无法存储 emoji 或特殊符号
\\n\\n\\n用 utf8mb4 + utf8mb4_unicode_ci,兼容所有 Unicode 字符(如 + * #)。
\\n
未对手机号加唯一索引,导致重复数据。
\\n添加 UNIQUE 约束:ALTER TABLE user ADD UNIQUE INDEX idx_phone (phone);\\n
\\n用户输入 138-2222-3333 或 138 222 23333,直接存储导致格式混乱。
\\n\\n\\n\\n
\\n- 入库前统一清洗:移除空格、横杠等符号,只保留 + 和数字。
\\n- 正则校验:例如 ^+?\\\\d{8,20}$(允许带 + 号的 8~20 位数字)。
\\n
明文存储手机号,泄露用户隐私。
\\n\\n\\n\\n
\\n- 加密存储:使用 AES 加密或数据库内置加密函数。
\\n- 脱敏显示:查询结果返回 138****3333。
\\n
// 严格校验(11位纯数字,无国际码)\\nString regex = \\"^1(3[0-9]|4[579]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\\\\\\\\d{8}$\\";\\n\\n// 宽松校验(允许带国际码,如+86 13812345678)\\nString looseRegex = \\"^(\\\\\\\\+\\\\\\\\d{1,3})?1(3\\\\\\\\d|4[579]|5[0-35-9]|6[2567]|7[0-8]|8\\\\\\\\d|9[0-35-9])\\\\\\\\d{8}$\\";\\n\\n
","description":"前言 大家好,我是田螺。\\n\\n最近一位星球粉丝说,他去面试了字节,问了这么一道题,20亿手机号存储,选int还是string?varchar还是char?为什么?\\n\\n他支支吾吾回答了几句,好像看起来,面试官面色凝重,对他不是很满意,果然最好还是挂了。。。\\n\\n本文跟大家聊聊我的思路。\\n\\n20亿数据,用Int存储存在哪些问题?bigInt行不行\\n面试官的隐藏考察点\\n日常开发避坑点\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\n1. 20亿数据,用Int或者Long存储存可能有在哪些问题?\\n1.1…","guid":"https://juejin.cn/post/7501105891158310966","author":"捡田螺的小男孩","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-06T14:12:49.171Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"抖音 renderD128 系统级疑难OOM分析与解决","url":"https://juejin.cn/post/7501119171795976243","content":"抖音长期存在renderD128内存占用过多导致的虚拟内存OOM,且多次出现renderD128内存激增导致OOM指标严重劣化甚至发版熔断。因受限于闭源的GPU驱动、现场有效信息极少、线上线下的Native内存检测工具均未能检测到相关内存分配等原因,多个团队都进行过分析,但一直未能定位到问题根因,问题反馈到厂商也一直没有结论。
\\n以往发生renderD128内存激增时,解决办法往往都是通过二分法去定位导致问题MR进行回滚(MR代码写法并无问题,仅仅是正常调用系统Api),但是回滚业务代码会影响业务正常需求的合入,也无法从根本上解决该问题,而且每次都会消耗我们大量人力去分析排查,因此我们有必要投入更多时间和精力定位根因并彻底解决该问题。在历经数月的深入分析和排查后,我们最终定位了问题根因并彻底解决了该问题,也取得了显著的OOM收益,renderD128导致发版熔断的问题再也没有发生过。
\\n接下来,将详细介绍下我们是如何一步步深入分析定位到问题根因,以及最终如何将这个问题给彻底解决的。
\\n主要集中在华为Android10系统, 表现为renderD128内存占用过多。
\\n机型特征: 联发科芯片、PowerVR GPU
\\n如:华为y6p/华为畅享e
\\nOS version: Android 10(主要),少量Android 8.1.0/9.0/11.0/12.0
\\nabi: armeabi-v7a, armeabi
\\n崩溃原因: 虚拟内存耗尽,主要由于/dev/dri/renderD128类型的内存占用过多(1G左右)
\\n我们根据抖音过往导致renderD128内存激增的MR,找到了一种能稳定复现该问题的办法“新增View,并调用View.setAlpha会引发renderD128内存上涨”。
\\n\\n\\n复现机型:华为畅享10e(Android 10)
\\n
测试方式:
\\n测试结果:
\\n对照组: 新增View,renderD128内存无变化
\\n实验组: 新增View,renderD128内存出现显著上涨,且每增加1个View,renderD128内存增加大概25M
\\n结论: 如果view被设置了透明度,绘制时会申请大量内存,且绘制完成不会释放
\\n我们在线上线下都开启了虚拟内存监控,但是均并未找到renderD128相关的内存监控信息(分配线程、堆栈等)
\\n以下是我们Hook相关接口开启虚拟内存监控的情况
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n接口 | 是否可以监控 | 备注 |
---|---|---|
mmap/mmap64/mremap/__mmap2 | 监控不到 | |
ioctl | 仅监控到一个命令,但该命令并没有映射内存操作 | 1. 命令调用前后renderD128相关内存并无变化 |
根据hook ioctl接口获取到的相关堆栈(虽然ioctl操作并没有影响内存,也可通过堆栈找到关键so库)
\\n由于关键接口代理均无法监控到renderD128相关的内存申请,此时猜想:可能是在内核中分配的内存?
\\n于是找到了华为畅享e的内核源代码,阅读其中DRM驱动的相关代码
\\n找到了唯一一个ioctl调用对应命令(0xc0206440)的定义和参数数据结构。
\\n根据参数的数据结构,很容易理解驱动应该是根据传入的bridge_id和bridge_func_id来决定做何操作的。(根据堆栈其实也能大致推测每个id对应的操作,但此处暂时不对其进行研究)
\\n但除此之外,在内核代码中并没有找到“内存是在内核中分配的”证据,猜测应该还是用户空间申请的,比较有“嫌疑”的库是libdrm.so、libsrv_um.so和gralloc.mt6765.so
\\n\\n\\nDRM
\\nDRM是Linux内核层的显示驱动框架,它把显示功能封装成 open/close/ioctl 等标准接口,用户空间的程序调用这些接口,驱动设备,显示数据。libdrm库封装了DRM driver提供的这些接口。通过libdrm库,程序可以间接调用DRM Driver
\\n
但libdrm库中的drm_mmap
是调用 mmap
或__mmap2
(都是监控中的接口)
#if defined(ANDROID) && !defined(__LP64__)\\nextern void *__mmap2(void *, size_t, int, int, int, size_t);\\n\\nstatic inline void *drm_mmap(void *addr, size_t length, int prot, int flags,\\n int fd, loff_t offset)\\n{\\n /* offset must be aligned to 4096 (not necessarily the page size) */\\n if (offset & 4095) {\\n errno = EINVAL;\\n return MAP_FAILED;\\n }\\n\\n return __mmap2(addr, length, prot, flags, fd, (size_t) (offset >> 12));\\n}\\n#else\\n/* assume large file support exists */\\n# define drm_mmap(addr, length, prot, flags, fd, offset) \\\\\\n mmap(addr, length, prot, flags, fd, offset)\\n
\\n\\n\\nmesa3D
\\nmesa3D中是通过调用libdrm库中的接口,间接调用DRM Driver的
\\n\\n
在mesa的源代码中找到了类似libsrv_um.so中PRVSRVBridgeCall
的函数 pvr_srv_bridge_call
static int pvr_srv_bridge_call(int fd,\\n uint8_t bridge_id,\\n uint32_t function_id,\\n void *input,\\n uint32_t input_buffer_size,\\n void *output,\\n uint32_t output_buffer_size)\\n{\\n struct drm_srvkm_cmd cmd = {\\n .bridge_id = bridge_id,\\n .bridge_func_id = function_id,\\n .in_data_ptr = (uint64_t)(uintptr_t)input,\\n .out_data_ptr = (uint64_t)(uintptr_t)output,\\n .in_data_size = input_buffer_size,\\n .out_data_size = output_buffer_size,\\n };\\n\\n int ret = drmIoctl(fd, DRM_IOCTL_SRVKM_CMD, &cmd);\\n if (unlikely(ret))\\n return ret;\\n\\n VG(VALGRIND_MAKE_MEM_DEFINED(output, output_buffer_size));\\n\\n return 0U;\\n}\\n
\\n同时发现了BridgeCall的相关id定义
\\n通过提交的commit了解到这部分代码是为powerVR rogue GPU增加的驱动
\\ncommit链接:gitlab.freedesktop.org/mesa/mesa/-…
\\n存在renderD128内存问题的机型使用的GPU也是PowerVR GPU,那么内存申请关键逻辑应该确实就在libsrv_um.so和gralloc.mt6765.so中
\\nHuawei Y6p - Full phone specifications
\\n暂时无法在飞书文档外展示此内容
\\n暂时无法在飞书文档外展示此内容
\\n奇怪的是,libsrv_um.so中只有munmap的符号,却没有mmap的符号(gralloc.mt6765.so同样没有)
\\n这比较不符合常理,一般来说,mmap和munmap都是成对出现的,猜测有三种可能性:
\\n在其他库中mmap
\\n用其他方式实现mmap操作
\\n使用dlsym拿到mmap等的符号,再调用 ❌
\\n调用ioctl实现mmap操作 ❌
\\n直接使用系统调用 ✅
\\n在libsrv_um.so中发现调用了syscall,系统调用号是0xC0(192),正是mmap的系统调用号!
\\ngralloc.mt6765.so同libsrv_um.so,也是通过系统调用进行mmap的!
\\n结论:hook syscall 应该可以监控到renderD128相关内存的调用!
\\n监控方式:
\\n测试: 播放视频
\\n测试结果:
\\n堆栈:
\\n内存变化:
\\n结论: 底层驱动可能考虑到架构适配或者效率问题,直接使用系统调用而非通用接口调用。在之前的监控中并未考虑到这种情况,所以会导致监控不全。
\\n内存监控工具完善之后,从线上我们收集到如下的堆栈信息:
\\n从堆栈上可以看到 libIMGegl.so有一个方法KEGLGetPoolBuffers,这个方法中会调用PVRSRVAcquireCPUMapping申请内存;
\\n从“KEGLGetPoolBuffers”这个方法名可以推断:
\\n有一个缓存池
\\n可以调用KEGLGetPoolBuffers从缓存池中获取buffer
\\n如果缓存池中有空闲buffer,会直接分配,无须从系统分配内存
\\n如果缓存池中无空闲buffer,会调用PVRSRVAcquireCPUMapping从系统中申请内存
\\n我们继续通过hook KEGLGetPoolBuffers 打印一些关键日志来确认猜想
\\n日志中前两次调用KEGLGetPoolBuffers没有申请内存,符合“存在空闲buffer直接分配”的猜想。
\\n后面的多次调用,每次都会连续调用5次 PVRSRVAcquireCPUMapping,分配5个大小不一的内存块(猜测应该是5类buffer),一共25M内存,和前面测试的结果刚好一致
\\n既然有内部分配,必然有其对应的内存释放,我们hook 泄漏线程RenderThread线程的munmap调用,抓到下面的堆栈,libsrv_um.so中相对偏移0xf060处(对应下面栈回溯#04栈帧,0xf061最后一位是1代表是thumb指令)的方法是DevmemReleaseCpuVirtAddr,但DevmemReleaseCpuVirtAddr这个方法并没有导出,glUnmapBuffer其实是调用了PVRSRVReleaseCPUMapping方法,在PVRSRVReleaseCPUMapping调用了DevmemReleaseCpuVirtAddr,进而最终调用到munmap方法释放内存的。
\\n之所以在堆栈中没有PVRSRVReleaseCPUMapping这层栈帧,是因为PVRSRVReleaseCPUMapping跳转到DevmemReleaseCpuVirtAddr使用的是指令b(而非bl指令)
\\n(glUnmapBuffer --\x3e PVRSRVReleaseCPUMapping --\x3e DevmemReleaseCpuVirtAddr --\x3e ... --\x3e munmap )
\\n#01 pc 00009f41 /data/app/com.example.crash.test-bqPIslSQVErr7gyFpcHl_w==/lib/arm/libnpth_vm_monitor.so (proxy_munmap)\\n#02 pc 0001474b /vendor/lib/libsrv_um.so\\n#03 pc 000115d9 /vendor/lib/libsrv_um.so\\n#04 pc 0000f061 /vendor/lib/libsrv_um.so(DevmemReleaseCpuVirtAddr+44)\\n#05 pc 00015db1 /vendor/lib/egl/libGLESv2_mtk.so (glUnmapBuffer+536)\\n#06 pc 003b865d /system/lib/libhwui.so!libhwui.so (offset 0x244000) (GrGLBuffer::onUnmap()+54)\\n#07 pc 001a0eb3 /system/lib/libhwui.so (GrResourceProvider::createPatternedIndexBuffer(unsigned short const*, int, int, int, GrUniqueKey const*)+174)\\n#08 pc 001666b9 /system/lib/libhwui.so (GrResourceProvider::createQuadIndexBuffer()+24)\\n#09 pc 00153df1 /system/lib/libhwui.so (GrResourceProvider::refQuadIndexBuffer()+44)\\n#10 pc 001535c9 /system/lib/libhwui.so (GrAtlasTextOp::onPrepareDraws(GrMeshDrawOp::Target*)+328)\\n
\\nPVRSRVAcquireCPUMapping和PVRSRVReleaseCPUMapping是libsrv_um.so中进行内存分配和释放的一对方法
\\n同理,KEGLGetPoolBuffers和KEGLReleasePoolBuffers是libIMGegl.so中分配和释放缓存buffer的一对方法
\\n但在测试过程中,并没有看到在为buffer分配内存之后有调用PVRSRVReleaseCPUMapping释放内存,在绘制结束前,会调用KEGLReleasePoolBuffers释放buffer(但并未释放内存),查看KEGLReleasePoolBuffers的汇编发现方法内部只是对buffer标记可用,并不存在内存释放。
\\n(左图KEGLGetPoolBuffers申请buffer,会申请内存;右图KEGLReleasePoolBuffers释放buffer,但不释放内存)
\\n看来这个缓存池可能是统一释放内存的,由于libIMGegl.so中大部分方法都没有符号,从这层比较难推进,不妨再从上层场景分析一下,跟绘制相关的缓存池会什么时候释放呢?首先想到的可能是Activity销毁的时候,经过测试发现并没有……
\\n但是在一次测试中发现 在Activity销毁之后,过了一段时间(1min左右)再启动一个新的Activity时突然释放了一堆renderD128相关的内存,抓到的是下面的堆栈。RenderThreaad中会执行销毁CanvasContext的任务,每次销毁CanvasContext时都会释放在一定时间范围内(30s)未使用的一些资源。销毁CanvasContext的时机是Activity Destroy时。(这里其实有些疑问,应该还有释放时机没有被发现)
\\n #01 pc 0000edc1 /data/app/com.example.crash.test-o-BAwGot5UWCmlHJALMy2g==/lib/arm/libnpth_vm_monitor.so\\n #02 pc 0001d29b /vendor/lib/libIMGegl.so\\n #03 pc 0001af31 /vendor/lib/libIMGegl.so\\n #04 pc 000187c1 /vendor/lib/libIMGegl.so\\n #05 pc 0001948b /vendor/lib/libIMGegl.so\\n #06 pc 00018753 /vendor/lib/libIMGegl.so\\n #07 pc 0000b179 /vendor/lib/libIMGegl.so\\n #08 pc 0000f473 /vendor/lib/libIMGegl.so (IMGeglDestroySurface+462)\\n #09 pc 000171bd /system/lib/libEGL.so (android::eglDestroySurfaceImpl(void*, void*)+48)\\n #10 pc 0025d40b /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::renderthread::EglManager::destroySurface(void*)+30)\\n #11 pc 0025d2f7 /system/lib/libhwui.so!libhwui.so (offset 0x245000) (android::uirenderer::skiapipeline::SkiaOpenGLPipeline::setSurface(ANativeWindow*, android::uirenderer::renderthread::SwapBehavior, android::uirenderer::renderthrea \\n #12 pc 00244c03 /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::setSurface(android::sp<android::Surface>&&)+110)\\n #13 pc 00244af5 /system/lib/libhwui.so!libhwui.so (offset 0x243000) (android::uirenderer::renderthread::CanvasContext::destroy()+48)\\n #15 pc 0023015f /system/lib/libhwui.so!libhwui.so (offset 0x208000) (std::__1::packaged_task<void ()>::operator()()+50)\\n #16 pc 0020da97 /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::WorkQueue::process()+158)\\n #17 pc 0020d8f5 /system/lib/libhwui.so!libhwui.so (offset 0x208000) (android::uirenderer::renderthread::RenderThread::threadLoop()+72)\\n #18 pc 0000d91b /system/lib/libutils.so (android::Thread::_threadLoop(void*)+182)\\n #19 pc 0009b543 /apex/com.android.runtime/lib/bionic/libc.so!libc.so (offset 0x8d000) (__pthread_start(void*)+20)\\n
\\nrenderD128类内存导致的OOM问题,并非由于内存泄漏,而是大量内存长期不释放导致。在大型APP中,Activity存活的时间可能会很长,如果缓存池只能等到Activity销毁时才能释放,大量内存长期无法释放,就极易发生OOM。
\\n从相关内存的分配和释放章节的分析来看,get & release buffer的操作有点不对称,我们期望:
\\n分配缓存:有可用buffer直接使用;无可用buffer则申请新的;
\\n释放缓存:标记buffer空闲,空闲buffer达到某一阈值后则释放。
\\n而现状是空闲buffer达到某一阈值后并不会释放,是否可以尝试手动释放呢?
\\n首先需要了解缓存池的结构
\\n由于相关so代码闭源,我们通过反汇编推导出缓存池的结构,大致如下图所示,pb_global是缓存池的管理结构体,其中的buffers_list中分别保存了5类buffer的list,内存组织方式如下示意
\\nKEGLReleasePoolBuffers中会标记每一个buffer->flag为0(空闲)
\\n暂时无法在飞书文档外展示此内容
\\n手动释放内存的方式
\\n在KEGLReleasePoolBuffers标记buffer为空闲之后,检查当前空闲buffer个数是否超过阈值(或者检查当前render D128相关内存是否超过阈值),如果超过阈值则释放一批buffer,并将buffer从链表中取下。
\\n(相关代码如下👇)
\\nstatic void release_freed_buffer(pb_ctx_t* ctx) {\\n /** 一些检查和判空操作会省略 **/\\n ...\\n /** 阈值检查 **/\\n if (!limit_check(ctx)) return;\\n\\n // 拿到buffer_list\\n pb_buffer_list_t* buffers_list = ctx->pb_global->buffers_list;\\n\\n pb_buffer_info_t *buffer_info, *prev_info;\\n for (int i = 0; i < 5; i++) {\\n buffer_info = buffer_info->buffers[i];\\n if (buffer_info == NULL) continue;\\n\\n /** 第一个buffer不释放,简化逻辑 **/\\n while(buffer_info) {\\n prev_info = buffer_info;\\n buffer_info = buffer_info->next;\\n\\n if (buffer_info && buffer_info->flag == 0) {\\n int ret = pvrsrvReleaseCPUMapping((void**)buffer_info->sparse_buffer->cpu_mapping_info->info);\\n\\n LOGE(\\"%s, release cpu mapping ret: %d\\", __FUNCTION__, ret);\\n if (ret == 0) {\\n buffer_info->flag = 1;\\n buffer_info->sparse_buffer->mmap_ptr = NULL;\\n prev_info->next = buffer_info->next;\\n buffers_list->buffer_size[i]--;\\n free(buffer_info);\\n buffer_info = prev_info;\\n }\\n }\\n }\\n }\\n}\\n
\\n方案效果
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n测试环境和方式与前面“问题复现”章节一致
\\n
内存释放时机 | 绘制结束后renderD128相关内存大小 | 结果比较 |
---|---|---|
每次释放缓存 | 33M 左右 | 与不设置透明度的对照组结果接近 |
renderD128内存> 100M | 86M 左右 | 100M以下,符合预期 |
renderD128内存> 300M | 295M 左右 | 跟实验组一致,因为并没有超过300M的阈值。符合预期 |
buffer总数 > 5 | 33M 左右 | 与不设置透明度的对照组结果接近,绘制结束时会释放完所有空闲buffer |
buffer总数 > 10 | ||
buffer总数 > 20 | 295M 左右 | 跟实验组一致,因为并没有超过20个buffer的阈值(10个view大概会用到10~15个buffer)。符合预期 |
空闲buffer > 5 | 138M 左右 | 空闲buffer个数不太可控,无法精确控制内存水位 |
空闲buffer > 10 | 33M 左右 |
方案结论:
\\n这个方案虽然也可缓解问题,但是存在以下问题:
\\n性能影响(理论,未测)
\\n稳定性
\\n这个方案应该不是最优解,先做备用方案,再探索一下
\\n从前面“相关内存释放”章节的分析可知,缓存池的内存并不是不会释放,而是释放时机很晚,那么能否早点释放呢?
\\n查看CanvasContext的释放路径,仅发现了一个可操作点(尝试了一些方式都会崩溃,会释放掉正在使用的资源),CacheManager::trimStaleResources方法中可以把释放30s内未使用的资源,改成释放1s(或10s)内未使用的资源
\\n修改指令:MOVW R2, #30000 ==> MOVW R2,#1000
\\n(相关代码如下👇)
\\n#define ORIGIN_TIME_LIMIT_INST 0x5230f247 // 30s\\n#define NEW_TIME_LIMIT_INST 0x32e8f240 // 1s 提前构造好的指令编码\\n#define FUNC_SYM \\"_ZN7android10uirenderer12renderthread12CacheManager18trimStaleResourcesEv\\"\\n\\nstatic void change_destroy_wait_time() {\\n /** 一些检查和判空操作会省略 **/\\n#ifdef __arm__\\n void* handle = dlopen(\\"libhwui.so\\");\\n // 从trimStaleResources方法的起始地址开始搜索内存\\n void* sym_ptr = dlsym(handle, FUNC_SYM);\\n\\n sym_ptr = (void*)((uint32_t)sym_ptr & 0xfffffffc);\\n\\n uint32_t* inst_start = (uint32_t*)sym_ptr;\\n uint32_t* search_limit = inst_start + 12;\\n\\n while(inst_start < search_limit) {\\n /* 找到并修改对应指令 */\\n if (*inst_start == ORIGIN_TIME_LIMIT_INST) {\\n if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {\\n return;\\n }\\n\\n *inst_start = NEW_TIME_LIMIT_INST;\\n flash_page_cache(inst_start);\\n\\n if(mprotect((void*)((uint32_t)inst_start & (unsigned int) PAGE_MASK), PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC)) {\\n return;\\n }\\n break;\\n }\\n\\n inst_start++;\\n }\\n#endif\\n}\\n
\\n方案结论: 该方案还是依赖于Activity销毁,只是销毁后能更快释放资源,所以缓解内存方面起到的作用很有限
\\n在尝试前面两个方案之后,这个问题逐渐让人崩溃,似乎已经没有什么好的解决办法了,已经准备就此放弃。
\\n山重水复疑无路,柳岸花明又一村。在后续的一次压测中,我们发现了一个新的突破点“每次调用一次renderD128 内存会上涨25M,但是并不是无限上涨,上涨到1.3G左右就不再增长了”,且另外翻看线上相关OOM问题,renderD128内存占用最多的也在1.3G上下,由此我们大胆猜测renderD128 内存缓存池大小应该是有上限的,这个上限大概在1.3G上下,那么我们可以尝试从调小缓存池的阈值入手。
\\n再次尝试:
\\n我们再次尝试复现该问题,并hook相关内存分配 ;从日志可以看到,在内存增长到1.3G后
\\n再增加多一点信息,发现当KEGLGetPoolBuffers获取buffer失败后,会有KEGLReleasePoolBuffers调用,释放了大量buffer,之后再重新调用KEGLGetPoolBuffers
\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1265852416, after: 1292066816, alloc: 26214400\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1292066816, after: 1318281216, alloc: 26214400\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x0 ==> before: 1318281216, after: 1318281216, alloc: 0\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\n...\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\nKEGLReleasePoolBuffers end, ret: 1 ==> before: 1318281216, after: 1318281216, release: 0\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0\\nKEGLGetPoolBuffers end, a1: 0xd1b95068, a2: 0x1, ret: 0x1 ==> before: 1318281216, after: 1318281216, alloc: 0\\n
\\n从堆栈看应该是提前flush了,所以就可以释放之前的buffer
\\n#01 pc 0000ebf5 /data/app/com.example.crash.test-1hHKnp6FBSv-HjrVtXQo1Q==/lib/arm/libnpth_vm_monitor.so (proxy_KEGLReleasePoolBuffers)\\n#02 pc 00047c2d /vendor/lib/egl/libGLESv2_mtk.so\\n#03 pc 00046a7b /vendor/lib/egl/libGLESv2_mtk.so (ResetSurface)\\n#04 pc 00028bf7 /vendor/lib/egl/libGLESv2_mtk.so\\n#05 pc 000d2165 /vendor/lib/egl/libGLESv2_mtk.so (RM_FlushHWQueue)\\n#06 pc 00028c73 /vendor/lib/egl/libGLESv2_mtk.so \\n#07 pc 000453fd /vendor/lib/egl/libGLESv2_mtk.so (PrepareToDraw)\\n#08 pc 0001d977 /vendor/lib/egl/libGLESv2_mtk.so (glDrawArrays+738)\\n#09 pc 00009edd /system/lib/libGameGraphicsOpt.so (hw_glDrawArraysHookV2+18)\\n#10 pc 001d1769 /system/lib/libhwui.so (GrGLGpu::sendMeshToGpu(GrPrimitiveType, GrBuffer const*, int, int)+74)\\n#11 pc 001d15f3 /system/lib/libhwui.so (GrMesh::sendToGpu(GrMesh::SendToGpuImpl*) const+38)\\n#12 pc 001d13e5 /system/lib/libhwui.so (GrGLGpu::draw(GrRenderTarget*, GrSurfaceOrigin, GrPrimitiveProcessor const&, GrPipeline const\\n
\\n2. #### 方案三:KEGLGetPoolBuffers中限制buffer分配
\\n根据上面的分析,发现可以尝试:
\\n暂时无法在飞书文档外展示此内容
\\n方案结论: 该方案需要每次分配内存前读取maps获取renderD128占用内存大小,对性能不是很友好
\\n从上面的分析,我们知道KEGLGetPoolBuffers函数返回0时分配失败,会开始释放buffer。我们继续反汇编KEGLGetPoolBuffers函数,根据KEGLGetPoolBuffers的返回值为0 可以回溯到汇编中进行阈值判断的逻辑
\\nv8:buffers_list
\\nv7:buffer类型(0~4)
\\nv8+4*v7+24:v7这个buffer类型 的buffer数量(右图中的buffer_size[i]
\\nv49:buffer_info
\\nv49 + 28: buffer_limit 缓存池中每种类型的buffer 的阈值(右图中的buffer_limits)
\\n简单来说,这里将buffer_limits与buffer_size[i]进行比较,如果buffer_size[i]大于等于阈值,就会返回0,分配失败
\\n接下来的操作就很简单了,只需对buffer_limits进行修改就行,在测试设备上buffer_limits值是50(50*25M 大约是1.25G),我们将buffer_limits改小一点就可以将renderD128内存值控制在一个更小的阈值范围内,以此降低renderD128内存占用。
\\n(相关代码如下👇)
\\nint opt_mtk_buffer(int api_level, int new_buffer_size) {\\n ...(无关代码省略)\\n if (check_buffer_size(new_buffer_size)) {\\n prefered_buffer_size = new_buffer_size;\\n }\\n\\n KEGLGetPoolBuffers_stub = bytehook_hook_single(\\n \\"libGLESv2_mtk.so\\",\\n NULL,\\n \\"KEGLGetPoolBuffers\\",\\n (void*)proxy_KEGLGetPoolBuffers,\\n (bytehook_hooked_t)bytehook_hooked_mtk,\\n NULL);\\n ...(无关代码省略)\\n\\n return 0;\\n}\\n\\nstatic void* proxy_KEGLGetPoolBuffers(void** a1, void* a2, int a3, int a4) {\\n //修改buffer_limits\\n modify_buffer_size((pb_ctx_t*)a1);\\n void* ret = BYTEHOOK_CALL_PREV(proxy_KEGLGetPoolBuffers, KEGLGetPoolBuffers_t, a1, a2, a3, a4);\\n BYTEHOOK_POP_STACK();\\n return ret;\\n}\\n\\nstatic void modify_buffer_size(pb_ctx_t* ctx) {\\n if (__predict_false(ctx == NULL || ctx->node == NULL || ctx->node->buffer_inner == NULL)) {\\n return;\\n }\\n\\n if (ctx->node->buffer_inner->num == ORIGIN_BUFFER_SIZE) {\\n ctx->node->buffer_inner->num = prefered_buffer_size;\\n }\\n}\\n
\\nDemo验证:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n缓存值阈值 | 内存峰值 |
---|---|
50 | 1.3G |
20 | 530M |
10 | 269M |
方案结论: 该方案修改少,性能影响小,且稳定性可控
\\n通过的上面的分析,由于方案四“修改缓存池阈值”修改少,性能影响小,且稳定性可控 , 最终我们决定选用该方案。
\\n开启修复实验后相关机型OOM崩溃率显著下降近50% ,观察数周之后各项业务指标也均为正向,符合预期。全量上线后大盘renderD128相关OOM也大幅下降,另外renderD128导致发版熔断的情况从此再也没有发生过。
\\n在分析内存问题时,不论是系统申请的内存还是业务申请的内存,都需要明确申请逻辑和释放逻辑,才能确定是否发生泄漏还是长期不释放,再从内存申请和释放逻辑中寻找可优化点。
\\n相关资料
\\n在分布式架构中,MySQL与Elasticsearch(ES)的协同已成为解决高并发查询与复杂检索的标配组合。
\\n然而,如何实现两者间的高效数据同步,是架构设计中绕不开的难题。
\\n这篇文章跟大家一起聊聊MySQL同步ES的6种主流方案,结合代码示例与场景案例,帮助开发者避开常见陷阱,做出最优技术选型。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\n场景:适用于对数据实时性要求极高,且业务逻辑简单的场景,如金融交易记录同步。
\\n在业务代码中同时写入MySQL与ES。
\\n代码如下:
\\n@Transactional \\npublic void createOrder(Order order) { \\n // 写入MySQL \\n orderMapper.insert(order); \\n // 同步写入ES \\n IndexRequest request = new IndexRequest(\\"orders\\") \\n .id(order.getId()) \\n .source(JSON.toJSONString(order), XContentType.JSON); \\n client.index(request, RequestOptions.DEFAULT); \\n}\\n
\\n痛点:
\\n场景:电商订单状态更新后需同步至ES供客服系统检索。
\\n我们可以使用MQ进行解耦。
\\n架构图如下:
\\n代码示例如下:
\\n// 生产者端 \\npublic void updateProduct(Product product) { \\n productMapper.update(product); \\n kafkaTemplate.send(\\"product-update\\", product.getId()); \\n} \\n\\n// 消费者端 \\n@KafkaListener(topics = \\"product-update\\") \\npublic void syncToEs(String productId) { \\n Product product = productMapper.selectById(productId); \\n esClient.index(product); \\n}\\n
\\n优势:
\\n缺陷:
\\n场景:用户行为日志的T+1分析场景。
\\n该方案低侵入但高延迟。
\\n配置示例如下:
\\ninput {\\njdbc{\\n jdbc_driver=>\\"com.mysql.jdbc.Driver\\"\\n jdbc_url=>\\"jdbc:mysql://localhost:3306/log_db\\"\\n schedule=>\\"*/5 * * * *\\"# 每5分钟执行 \\n statement=>\\"SELECT * FROM user_log WHERE update_time > :sql_last_value\\"\\n}\\n}\\noutput{\\nelasticsearch{\\n hosts=>[\\"es-host:9200\\"]\\n index=>\\"user_logs\\"\\n}\\n}\\n
\\n适用性分析:
\\n最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。
\\n场景:社交平台动态实时搜索(如微博热搜更新)。
\\n技术栈:Canal + RocketMQ + ES
该方案高实时,并且低侵入。
\\n架构流程如下:
\\n关键配置:
\\n# canal.properties \\ncanal.instance.master.address=127.0.0.1:3306 \\ncanal.mq.topic=canal.es.sync\\n
\\n避坑指南:
\\n_id
唯一键避免重复写入。场景:将历史订单数据从分库分表MySQL迁移至ES。
\\n该方案是大数据迁移的首选。
\\n配置文件如下:
\\n{ \\n\\"job\\":{\\n \\"content\\":[{\\n \\"reader\\":{\\n \\"name\\":\\"mysqlreader\\",\\n \\"parameter\\":{\\"splitPk\\":\\"id\\",\\"querySql\\":\\"SELECT * FROM orders\\"}\\n },\\n \\"writer\\":{\\n \\"name\\":\\"elasticsearchwriter\\",\\n \\"parameter\\":{\\"endpoint\\":\\"http://es-host:9200\\",\\"index\\":\\"orders\\"}\\n }\\n }]\\n}\\n}\\n
\\n性能调优:
\\nchannel
数提升并发(建议与分片数对齐)limit
分批查询避免OOM场景:商品价格变更时,需关联用户画像计算实时推荐评分。
\\n该方案适合于复杂的ETL场景。
\\n代码片段如下:
\\nStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); \\nenv.addSource(new CanalSource()) \\n .map(record -> parseToPriceEvent(record)) \\n .keyBy(event -> event.getProductId()) \\n .connect(userProfileBroadcastStream) \\n .process(new PriceRecommendationProcess()) \\n .addSink(new ElasticsearchSink());\\n
\\n优势:
\\n对于文章上面给出的这6种技术方案,我们在实际工作中,该如何做选型呢?
\\n下面用一张表格做对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 实时性 | 侵入性 | 复杂度 | 适用阶段 |
---|---|---|---|---|
同步双写 | 秒级 | 高 | 低 | 小型单体项目 |
MQ异步 | 秒级 | 中 | 中 | 中型分布式系统 |
Logstash | 分钟级 | 无 | 低 | 离线分析 |
Canal | 毫秒级 | 无 | 高 | 高并发生产环境 |
DataX | 小时级 | 无 | 中 | 历史数据迁移 |
Flink | 毫秒级 | 低 | 极高 | 实时数仓 |
苏三的建议:
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 引言\\n\\n在分布式架构中,MySQL与Elasticsearch(ES)的协同已成为解决高并发查询与复杂检索的标配组合。\\n\\n然而,如何实现两者间的高效数据同步,是架构设计中绕不开的难题。\\n\\n这篇文章跟大家一起聊聊MySQL同步ES的6种主流方案,结合代码示例与场景案例,帮助开发者避开常见陷阱,做出最优技术选型。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。\\n\\n方案一:同步双写\\n\\n场景:适用于对数据实时性要求极高…","guid":"https://juejin.cn/post/7500133896291762195","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-06T01:18:31.403Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ac19d43d558840a091e77d83970d3e22~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747099111&x-signature=g8KAztvcckz3lg6KDKmVbsqp8Es%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91da980205334dcbabfe38de5807a603~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747099111&x-signature=r%2FPZ%2FWAlmbUGbDJbJlSUahgKofw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"基于 AI 的日常新闻 收集/处理/判断 有几种玩法?","url":"https://juejin.cn/post/7500248719310078002","content":"近期一直在尝试实现一个功能 ,做了很多尝试后 ,有一些零零散散的记录 ,这里拿出来给大家分享一下。
\\n在 AI 还没发展出来 ,一般可以通过新闻聚合提供的接口 或者 爬虫 来拉取消息数据。
\\n其中新闻聚合在 AI 出现之后 ,逐渐转变成支持组件 ,为 AI 智能体提供 MCP 服务。 比如博查/聚合数据这类。
\\n而爬虫这东西, 具有一定的风险 ,可能涉及到违法,这里 Demo 就很简单的说说 :
\\nrequests
:发送HTTP请求抓取网页内容beautifulsoup4
:解析网页HTML,提取新闻信息fake_useragent
(可选):动态伪装请求头,避免被服务器拒绝\\nimport requests \\nfrom bs4 import BeautifulSoup \\nfrom fake_useragent import UserAgent \\nimport time \\nfrom urllib.parse import urljoin \\n\\ndef log(msg): \\n print(f\\"[LOG] {msg}\\") \\n\\ndef fetch_sina_finance_news(): \\n # 某财经网站作为案例\\n base_url = \\"https://xxx.xxx.xxx.cn/stock/\\" \\n log(f\\"开始访问 {base_url}\\") \\n\\n headers = { \\n \\"User-Agent\\": UserAgent().random \\n } \\n\\n try: \\n response = requests.get(base_url, headers=headers, timeout=10) \\n response.raise_for_status() \\n log(\\"网页请求成功\\") \\n except Exception as e: \\n log(f\\"请求失败: {e}\\") \\n return \\n\\n soup = BeautifulSoup(response.content, \\"html.parser\\") \\n\\n # 找到第一个<div class=\\"tabs-cont sto_cont0\\">对应要闻板块 \\n news_container = soup.find(\\"div\\", class_=\\"tabs-cont sto_cont0\\") \\n if not news_container: \\n log(\\"未找到要闻内容区,网页结构可能已变更\\") \\n return \\n\\n # 找到里面第一个<ul class=\\"list01\\" data-client=\\"scroll\\">就是要闻新闻列表 \\n news_list = news_container.find(\\"ul\\", class_=\\"list01\\", attrs={\\"data-client\\": \\"scroll\\"}) \\n if not news_list: \\n log(\\"未找到要闻新闻列表\\") \\n return \\n\\n news_items = news_list.find_all(\\"li\\", recursive=False) # 只查一级li,避免抓取嵌套的 \\n\\n if not news_items: \\n log(\\"新闻列表为空\\") \\n return \\n\\n log(f\\"共找到 {len(news_items)} 条新闻,开始提取标题和链接\\") \\n\\n for index, li in enumerate(news_items, 1): \\n a_tag = li.find(\\"a\\") \\n if not a_tag: \\n log(f\\"第{index}条新闻无链接,跳过\\") \\n continue \\n title = a_tag.get_text(strip=True) \\n href = a_tag.get(\\"href\\") \\n full_url = urljoin(base_url, href) # 完整url拼接 \\n print(f\\"{index}. {title}\\\\n 链接: {full_url}\\") \\n\\n log(\\"新闻提取完成\\") \\n\\nif __name__ == \\"__main__\\": \\n fetch_sina_finance_news() \\n\\n\\n
\\n\\n\\n阶段总结 :
\\n
如果规模比较大 ,一般就会考虑通过聚合平台来获取数据了 ,聚合平台会把各平台的数据进行聚合 ,只需要调用接口就能获取对应的新闻信息 : ( 案例平台 : 博查AI开放平台 | Search API, Reranker API)
\\nfrom typing import List, Optional, Dict, Any\\nfrom dataclasses import asdict, dataclass, field\\nimport requests \\n\\n\\n\\n@dataclass\\nclass WebPageItem:\\n id: Optional[str] = None\\n name: Optional[str] = None\\n url: Optional[str] = None\\n displayUrl: Optional[str] = None # 修改为匹配 JSON 中的键名\\n snippet: Optional[str] = None\\n siteName: Optional[str] = None # 修改为匹配 JSON 中的键名\\n siteIcon: Optional[str] = None # 修改为匹配 JSON 中的键名\\n datePublished: Optional[str] = None # 修改为匹配 JSON 中的键名\\n dateLastCrawled: Optional[str] = None # 修改为匹配 JSON 中的键名\\n cachedPageUrl: Optional[str] = None # 修改为匹配 JSON 中的键名\\n language: Optional[str] = None\\n isFamilyFriendly: Optional[bool] = None\\n isNavigational: Optional[bool] = None\\n\\n\\n@dataclass\\nclass ImageItem:\\n webSearchUrl: Optional[str] = None # 修改为匹配 JSON 中的键名\\n name: Optional[str] = None\\n thumbnailUrl: Optional[str] = None # 修改为匹配 JSON 中的键名\\n datePublished: Optional[str] = None\\n contentUrl: Optional[str] = None\\n hostPageUrl: Optional[str] = None\\n contentSize: Optional[str] = None\\n encodingFormat: Optional[str] = None\\n hostPageDisplayUrl: Optional[str] = None\\n width: Optional[int] = None\\n height: Optional[int] = None\\n thumbnail: Optional[str] = None \\n\\n\\n@dataclass\\nclass SearchResponse:\\n code: int\\n log_id: str\\n msg: Optional[str] = None\\n data: Dict[str, Any] = field(default_factory=dict)\\n web_pages: List[WebPageItem] = field(default_factory=list)\\n images: List[ImageItem] = field(default_factory=list)\\n\\n def __post_init__(self):\\n if self.data and \'webPages\' in self.data:\\n self.web_pages = [WebPageItem(\\n **page) for page in self.data[\'webPages\'].get(\'value\', [])]\\n if self.data and \'images\' in self.data:\\n self.images = [ImageItem(**image)\\n for image in self.data[\'images\'].get(\'value\', [])]\\n\\nclass WebSearchClient:\\n def __init__(self, api_key):\\n self.api_key = api_key\\n self.base_url = \\"https://api.bochaai.com/v1/web-search\\"\\n print(f\\"初始化 WebSearchClient,API端点: {self.base_url}\\")\\n\\n def search(self, query):\\n \\"\\"\\"通过 Web Search API 查询网络内容\\"\\"\\"\\n headers = {\\n \\"Authorization\\": f\\"Bearer {self.api_key}\\",\\n \\"Content-Type\\": \\"application/json\\"\\n }\\n payload = {\\n \\"query\\": query,\\n \\"freshness\\": \\"oneDay\\",\\n \\"summary\\": \\"false\\",\\n \\"count\\": 10\\n }\\n\\n print(f\\"发起搜索查询:{query}\\")\\n\\n try:\\n response = requests.post(\\n self.base_url,\\n headers=headers,\\n json=payload\\n )\\n response.raise_for_status()\\n results = response.json()\\n\\n # 使用 DTO 对象处理结果\\n search_response = SearchResponse(**results)\\n print(f\\"搜索成功,返回结果数量:{len(search_response.web_pages)}\\")\\n return search_response\\n\\n except requests.exceptions.RequestException as e:\\n print(f\\"请求失败: {e}\\")\\n return None\\n \\ndef main():\\n # API 密钥\\n api_key = \\"sk-11111\\"\\n\\n # 预定义查询关键词\\n query = \\"当天和经济金融股票相关的新闻\\"\\n\\n web_search = WebSearchClient(api_key)\\n\\n print(f\\"开始查询:{query}\\")\\n results = web_search.search(query)\\n print(f\\"查询结果{results}\\")\\n\\nif __name__ == \\"__main__\\":\\n main()\\n\\n\\n
\\n\\n\\n阶段总结 :
\\n
平台很多,大家可以自行斟酌
)AI Search 及 Agent Search
部分 AI 厂商本身就集成了 WebSearch 的功能 ,只需要直接调用接口 ,就可以快速搜索, 这里就算入门了 ,不再需要额外的分析操作,交给大模型进行处理 :
\\nimport os\\nimport json\\nfrom typing import Dict, Any, List, Optional\\n\\nfrom openai import OpenAI\\nfrom openai.types.chat.chat_completion import Choice\\n\\n\\nclass SinaFinanceNewsSearcher:\\n def __init__(self, api_key: Optional[str] = None):\\n \\"\\"\\" \\n 初始化 SinaFinanceNewsSearcher \\n\\n :param api_key: 可选的 API Key,如果为 None 则尝试从本地或 DataMap 获取 \\n \\"\\"\\"\\n self.api_key = self._get_api_key(api_key)\\n\\n self.client = OpenAI(\\n base_url=\\"https://api.moonshot.cn/v1\\",\\n api_key=self.api_key\\n )\\n\\n self.initial_messages = [\\n {\\n \\"role\\": \\"system\\",\\n \\"content\\": \\"你是一个金融工作者,现在要收集每天相关的金融信息,你从新浪财经(https://finance.sina.com.cn/)获取数据。数据的返回通过 JSON 返回给我\\"\\n }\\n ]\\n\\n def _get_api_key(self, provided_key: Optional[str] = None) -> str:\\n \\"\\"\\" \\n 获取 API Key 的优先级方法 \\n\\n 优先级: \\n 1. 直接提供的 API Key \\n 2. 本地文件 (moonshot_api_key.txt) \\n 3. DataMap 中获取 \\n 4. 抛出异常 \\n\\n :param provided_key: 直接提供的 API Key \\n :return: API Key \\n \\"\\"\\"\\n # 1. 如果直接提供了 API Key,直接返回\\n if provided_key:\\n return provided_key\\n\\n # 2. 尝试从本地文件获取\\n local_file_path = os.path.join(\\n os.path.dirname(__file__), \\"moonshot_api_key.txt\\")\\n if os.path.exists(local_file_path):\\n with open(local_file_path, \\"r\\") as file:\\n local_key = file.read().strip()\\n if local_key:\\n return local_key\\n\\n def _search_impl(self, arguments: Dict[str, Any]) -> Any:\\n \\"\\"\\" \\n 搜索实现,目前直接返回参数 \\n\\n :param arguments: 搜索参数 \\n :return: 搜索结果 \\n \\"\\"\\"\\n return arguments\\n\\n def search_finance_news(self, query: Optional[str] = None) -> str:\\n \\"\\"\\" \\n 搜索金融新闻 \\n\\n :param query: 可选的搜索查询,默认为今天的热点新闻 \\n :return: 搜索结果内容 \\n \\"\\"\\"\\n messages = self.initial_messages.copy()\\n\\n # 如果没有提供查询,使用默认查询\\n if query is None:\\n query = \\"搜索今天有什么热点新闻\\"\\n\\n messages.append({\\n \\"role\\": \\"user\\",\\n \\"content\\": query\\n })\\n\\n finish_reason = None\\n while finish_reason is None or finish_reason == \\"tool_calls\\":\\n choice = self._chat(messages)\\n finish_reason = choice.finish_reason\\n\\n if finish_reason == \\"tool_calls\\":\\n messages.append(choice.message)\\n\\n for tool_call in choice.message.tool_calls:\\n tool_call_name = tool_call.function.name\\n tool_call_arguments = json.loads(\\n tool_call.function.arguments)\\n\\n if tool_call_name == \\"$web_search\\":\\n tool_result = self._search_impl(tool_call_arguments)\\n else:\\n tool_result = f\\"Error: unable to find tool by name \'{tool_call_name}\'\\"\\n\\n messages.append({\\n \\"role\\": \\"tool\\",\\n \\"tool_call_id\\": tool_call.id,\\n \\"name\\": tool_call_name,\\n \\"content\\": json.dumps(tool_result)\\n })\\n\\n return choice.message.content\\n\\n def _chat(self, messages: List[Dict[str, str]]) -> Choice:\\n \\"\\"\\" \\n 调用 Moonshot AI 聊天接口 \\n\\n :param messages: 消息列表 \\n :return: 聊天返回结果 \\n \\"\\"\\"\\n completion = self.client.chat.completions.create(\\n model=\\"moonshot-v1-128k\\",\\n messages=messages,\\n temperature=0.3,\\n tools=[\\n {\\n \\"type\\": \\"builtin_function\\",\\n \\"function\\": {\\n \\"name\\": \\"$web_search\\",\\n }\\n }\\n ]\\n )\\n return completion.choices[0]\\n\\n\\ndef get_sina_finance_news(api_key: Optional[str] = None, query: Optional[str] = None) -> str:\\n \\"\\"\\" \\n 外部调用函数,获取新浪财经新闻 \\n\\n :param api_key: 可选的 Moonshot AI 的 API Key \\n :param query: 可选的搜索查询 \\n :return: 新闻内容 \\n \\"\\"\\"\\n searcher = SinaFinanceNewsSearcher(api_key)\\n return searcher.search_finance_news(query)\\n\\n\\n# 使用示例\\nif __name__ == \\"__main__\\":\\n # 三种使用方式:\\n # 1. 不提供 API Key,自动从本地或 DataMap 获取\\n news1 = get_sina_finance_news(None, \\"搜索最近的科技股新闻\\")\\n\\n # # 2. 直接提供 API Key\\n # news2 = get_sina_finance_news(\\"your-specific-api-key\\")\\n\\n # # 3. 提供 API Key 和具体查询\\n # news3 = get_sina_finance_news(\\"your-specific-api-key\\", \\"搜索最近的科技股新闻\\")\\n\\n print(news1)\\n # print(news2)\\n # print(news3)\\n\\n
\\n\\n\\n阶段总结 :
\\n
虽然上面的方式已经足够我们了解到日常的新闻需求 ,但是其更依赖于单个厂商 ,如果需要更灵活的整合 ,就需要综合性的 AI 平台处理了 , 这里我使用的是阿里云的 :
\\n\\n\\nClient 端进行查询
\\n
import os\\nfrom http import HTTPStatus\\nfrom dashscope import Application\\n\\nresponse = Application.call(\\n # 若没有配置环境变量,可用百炼API Key将下行替换为:api_key=\\"sk-xxx\\"。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。\\n api_key=\\"sk-xxxx\\",\\n app_id=\'xxxx\',# 替换为实际的应用 ID\\n prompt=\'给我当天的热点新闻\')\\n\\nif response.status_code != HTTPStatus.OK:\\n print(f\'request_id={response.request_id}\')\\n print(f\'code={response.status_code}\')\\n print(f\'message={response.message}\')\\n print(f\'请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code\')\\nelse:\\n print(response.output.text)\\n
\\n\\n\\n阶段总结
\\n
文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\nJava虚拟机规范.Java SE 8版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n深入理解Java虚拟机:JVM高级特性与最佳实践(第3版):
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
JVM 采用类加载器(ClassLoader) 来动态加载 .class
文件。从 JVM 角度来看,类加载器分为两类:
java.lang.ClassLoader
,用于加载应用程序类。在 JDK 1.8 及之前,Java 使用 3 种默认类加载器:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类加载器 | 实现类 | 作用 |
---|---|---|
启动类加载器(Bootstrap ClassLoader) | C++ 实现 | 加载 JAVA_HOME/lib 目录下的核心类库(如 rt.jar ) |
扩展类加载器(Extension ClassLoader) | sun.misc.Launcher$ExtClassLoader | 加载 JAVA_HOME/lib/ext 目录下的扩展类 |
应用类加载器(Application ClassLoader) | sun.misc.Launcher$AppClassLoader | 加载 classpath 目录下的应用类 |
JDK 1.9 引入 Jigsaw 模块化系统(Project Jigsaw) ,对类加载器进行了调整:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nJDK 1.8 | JDK 1.9 及之后 | 变化 |
---|---|---|
扩展类加载器(Extension ClassLoader) | 平台类加载器(Platform ClassLoader) | 负责加载 JDK 模块化系统中的非核心模块 |
应用类加载器(Application ClassLoader) | 应用类加载器(Application ClassLoader) | 负责加载应用程序的 classpath |
JDK 1.9 之后,扩展类加载器被移除,改为 Platform ClassLoader。
\\nPlatform ClassLoader 作用
\\n:
\\njava.sql
)。java.base
模块(由 Bootstrap ClassLoader
加载)。classpath
,但可以通过 ModuleLayer
访问。plaintext\\nBootstrap ClassLoader\\n ├── Extension ClassLoader\\n │ ├── 加载 JAVA_HOME/lib/ext 目录\\n ├── Application ClassLoader\\n ├── 加载 classpath 目录\\n ├── 自定义类加载器(User-defined ClassLoader)\\n
\\nplaintext\\nBootstrap ClassLoader\\n ├── Platform ClassLoader (JDK 9+ 新增)\\n │ ├── 加载 JDK 平台模块(如 java.sql)\\n ├── Application ClassLoader\\n ├── 加载 classpath 目录\\n ├── 自定义类加载器(User-defined ClassLoader)\\n
\\n类加载器并不是继承关系,而是 组合关系,即父类加载器是一个 ClassLoader 类型的成员变量:
\\njava\\npublic abstract class ClassLoader {\\n private final ClassLoader parent; // 组合关系\\n}\\n
\\n在 ClassLoader
内部,如果找不到类,会委派给 parent
继续查找:
java\\nprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {\\n Class<?> c = findLoadedClass(name);\\n if (c == null) {\\n if (parent != null) {\\n c = parent.loadClass(name, false);\\n } else {\\n c = findBootstrapClassOrNull(name);\\n }\\n }\\n return c;\\n}\\n
\\nJDK 版本 | 主要类加载器 | 变化 |
---|---|---|
JDK 1.8 及之前 | Bootstrap、Extension、Application | 使用 Extension ClassLoader 加载 lib/ext |
JDK 1.9 及之后 | Bootstrap、Platform、Application | Extension ClassLoader 被 Platform ClassLoader 取代 |
模块化影响类加载
\\nmodule-info.java
中,非公开模块不能被直接访问。Platform ClassLoader 只能加载特定的 JDK 模块
\\nclasspath
中的类。自定义类加载器仍然适用
\\n小伙伴们,你们好呀,我是老寇,跟我一起学习对接MQTT
\\n采用docker-compose一键式,启动!!!
\\n还没有安装docker朋友,参考文章下面两篇文章
\\n\\n\\nservices:\\n emqx:\\n image: emqx/emqx:5.4.1\\n container_name: emqx\\n # 保持容器在没有守护程序的情况下运行\\n tty: true\\n restart: always\\n privileged: true\\n ports:\\n - \\"1883:1883\\"\\n - \\"8083:8083\\"\\n - \\"8883:8883\\"\\n - \\"18083:18083\\"\\n environment:\\n - TZ=Asia/Shanghai\\n volumes:\\n # 挂载数据存储\\n - ./emqx/data:/opt/emqx/data\\n # 挂载日志文件\\n - ./emqx/log:/opt/emqx/log\\n networks:\\n - laokou_network\\nnetworks:\\n laokou_network:\\n driver: bridge\\n
\\n访问 http://127.0.0.1:18083 设置密码
\\nMQTT 是物联网 (IoT) 的 OASIS 标准消息传递协议。它被设计为一种极轻量的发布/订阅消息传输协议,非常适合以较小的代码占用空间和极低的网络带宽连接远程设备。MQTT 目前广泛应用于汽车、制造、电信、石油和天然气等众多行业。
\\nEMQX 完全兼容 MQTT 5.0 和 3.x,本节将介绍 MQTT 相关功能的基本配置项,包括基本 MQTT 设置、订阅设置、会话设置、强制关闭设置和强制垃圾回收设置等
\\n本文章采用三种客户端对接
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n维度 | Paho | Hivemq-MQTT-Client | Vert.x MQTT Client |
---|---|---|---|
协议支持 | MQTT 3.1.1(5.0 实验性) | MQTT 5.0 完整支持 | MQTT 5.0(较新版本) |
性能 | 中(同步模式) | 高(异步非阻塞) | 极高(响应式架构) |
依赖复杂度 | 低 | 中(仅 Netty) | 高(需 Vert.x 生态) |
社区资源 | 丰富 | 较少 | 中等 |
适用场景 | 传统 IoT、跨语言项目 | 企业级 MQTT 5.0、高吞吐 | 响应式系统、高并发微服务 |
<dependencies>\\n <dependency>\\n <groupId>org.eclipse.paho</groupId>\\n <artifactId>org.eclipse.paho.mqttv5.client</artifactId>\\n <version>1.2.5</version>\\n </dependency>\\n <dependency>\\n <groupId>org.eclipse.paho</groupId>\\n <artifactId>org.eclipse.paho.client.mqttv3</artifactId>\\n <version>1.2.5</version>\\n </dependency>\\n</dependencies>\\n
\\nPahoProperties
\\n/**\\n * @author laokou\\n */\\n@Data\\npublic class PahoProperties {\\n\\n private boolean auth = true;\\n\\n private String username = \\"emqx\\";\\n\\n private String password = \\"laokou123\\";\\n\\n private String host = \\"127.0.0.1\\";\\n\\n private int port = 1883;\\n\\n private String clientId;\\n\\n private int subscribeQos = 1;\\n\\n private int publishQos = 0;\\n\\n private int willQos = 1;\\n\\n private int connectionTimeout = 60;\\n\\n private boolean manualAcks = false;\\n\\n // @formatter:off\\n /**\\n * 控制是否创建新会话(true=新建,false=复用历史会话). clearStart=true => Broker 会在连接断开后立即清除所有会话信息.\\n * clearStart=false => Broker 会在连接断开后保存会话信息,并在重新连接后复用会话信息.\\n * <a href=\\"https://github.com/hivemq/hivemq-mqtt-client/issues/627\\">...</a>\\n */\\n // @formatter:on\\n private boolean clearStart = false;\\n\\n private int receiveMaximum = 10000;\\n\\n private int maximumPacketSize = 10000;\\n\\n // @formatter:off\\n /**\\n * 默认会话保留一天.\\n * 最大值,4294967295L,会话过期时间【永不过期,单位秒】.\\n * 定义客户端断开后会话保留的时间(仅在 Clean Session = false 时生效).\\n */\\n private long sessionExpiryInterval = 86400L;\\n // @formatter:on\\n\\n /**\\n * 心跳包每隔60秒发一次.\\n */\\n private int keepAliveInterval = 60;\\n\\n private boolean automaticReconnect = true;\\n\\n private Set<String> topics = new HashSet<>(0);\\n\\n}\\n
\\nPahoMqttClientMessageCallbackV5
\\n/**\\n * @author laokou\\n */\\n@Slf4j\\n@RequiredArgsConstructor\\npublic class PahoMqttClientMessageCallbackV5 implements MqttCallback {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Override\\n public void disconnected(MqttDisconnectResponse disconnectResponse) {\\n log.error(\\"【Paho-V5】 => MQTT关闭连接\\");\\n }\\n\\n @Override\\n public void mqttErrorOccurred(MqttException ex) {\\n log.error(\\"【Paho-V5】 => MQTT报错,错误信息:{}\\", ex.getMessage());\\n }\\n\\n @Override\\n public void messageArrived(String topic, MqttMessage message) {\\n for (MessageHandler messageHandler : messageHandlers) {\\n if (messageHandler.isSubscribe(topic)) {\\n log.info(\\"【Paho-V5】 => MQTT接收到消息,Topic:{}\\", topic);\\n messageHandler.handle(new org.laokou.sample.mqtt.handler.MqttMessage(message.getPayload(), topic));\\n }\\n }\\n }\\n\\n @Override\\n public void deliveryComplete(IMqttToken token) {\\n log.info(\\"【Paho-V5】 => MQTT消息发送成功,消息ID:{}\\", token.getMessageId());\\n }\\n\\n @Override\\n public void connectComplete(boolean reconnect, String uri) {\\n if (reconnect) {\\n log.info(\\"【Paho-V5】 => MQTT重连成功,URI:{}\\", uri);\\n }\\n else {\\n log.info(\\"【Paho-V5】 => MQTT建立连接,URI:{}\\", uri);\\n }\\n }\\n\\n @Override\\n public void authPacketArrived(int reasonCode, MqttProperties properties) {\\n log.info(\\"【Paho-V5】 => 接收到身份验证数据包:{}\\", reasonCode);\\n }\\n\\n}\\n
\\nPahoV5MqttClientTest
\\n/**\\n * @author laokou\\n */\\n@SpringBootTest\\n@RequiredArgsConstructor\\n@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)\\nclass PahoV5MqttClientTest {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Test\\n void testMqttClient() throws InterruptedException {\\n ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(16);\\n\\n PahoProperties pahoProperties = new PahoProperties();\\n pahoProperties.setClientId(\\"test-client-3\\");\\n pahoProperties.setTopics(Set.of(\\"/test-topic-3/#\\"));\\n PahoMqttClientV5 pahoMqttClientV5 = new PahoMqttClientV5(pahoProperties, messageHandlers, scheduledExecutorService);\\n pahoMqttClientV5.open();\\n Thread.sleep(1000);\\n pahoMqttClientV5.publish(\\"/test-topic-3/789\\", \\"Hello World789\\".getBytes());\\n }\\n\\n}\\n
\\nPahoMqttClientMessageCallbackV3
\\n/**\\n * @author laokou\\n */\\n@Slf4j\\n@RequiredArgsConstructor\\npublic class PahoMqttClientMessageCallbackV3 implements MqttCallback {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Override\\n public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {\\n log.info(\\"【Paho-V3】 => MQTT消息发送成功,消息ID:{}\\", iMqttDeliveryToken.getMessageId());\\n }\\n\\n @Override\\n public void connectionLost(Throwable throwable) {\\n log.error(\\"【Paho-V3】 => MQTT关闭连接\\");\\n }\\n\\n @Override\\n public void messageArrived(String topic, MqttMessage message) throws Exception {\\n for (MessageHandler messageHandler : messageHandlers) {\\n if (messageHandler.isSubscribe(topic)) {\\n log.info(\\"【Paho-V3】 => MQTT接收到消息,Topic:{}\\", topic);\\n messageHandler.handle(new org.laokou.sample.mqtt.handler.MqttMessage(message.getPayload(), topic));\\n }\\n }\\n }\\n}\\n
\\nPahoV3MqttClientTest
\\n/**\\n * @author laokou\\n */\\n@SpringBootTest\\n@RequiredArgsConstructor\\n@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)\\nclass PahoV3MqttClientTest {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Test\\n void testMqttClient() throws InterruptedException {\\n ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(16);\\n\\n PahoProperties pahoProperties2 = new PahoProperties();\\n pahoProperties2.setClientId(\\"test-client-4\\");\\n pahoProperties2.setTopics(Set.of(\\"/test-topic-4/#\\"));\\n PahoMqttClientV3 pahoMqttClientV3 = new PahoMqttClientV3(pahoProperties2, messageHandlers, scheduledExecutorService);\\n pahoMqttClientV3.open();\\n Thread.sleep(1000);\\n pahoMqttClientV3.publish(\\"/test-topic-4/000\\", \\"Hello World000\\".getBytes());\\n }\\n\\n}\\n
\\n注意:订阅一段时间收不到数据,标准mqtt5.0协议,不兼容emqx broker mqtt5.0
\\n\\n<dependencies>\\n <dependency>\\n <groupId>com.hivemq</groupId>\\n <artifactId>hivemq-mqtt-client-reactor</artifactId>\\n <version>1.3.5</version>\\n </dependency>\\n <dependency>\\n <groupId>com.hivemq</groupId>\\n <artifactId>hivemq-mqtt-client-epoll</artifactId>\\n <version>1.3.5</version>\\n <type>pom</type>\\n </dependency>\\n<dependencies>\\n
\\nHivemqProperties
\\n/**\\n * @author laokou\\n */\\n@Data\\npublic class HivemqProperties {\\n\\n private boolean auth = true;\\n\\n private String username = \\"emqx\\";\\n\\n private String password = \\"laokou123\\";\\n\\n private String host = \\"127.0.0.1\\";\\n\\n private int port = 1883;\\n\\n private String clientId;\\n\\n private int subscribeQos = 1;\\n\\n private int publishQos = 0;\\n\\n private int willQos = 1;\\n\\n // @formatter:off\\n /**\\n * 控制是否创建新会话(true=新建,false=复用历史会话). clearStart=true => Broker 会在连接断开后立即清除所有会话信息.\\n * clearStart=false => Broker 会在连接断开后保存会话信息,并在重新连接后复用会话信息.\\n * <a href=\\"https://github.com/hivemq/hivemq-mqtt-client/issues/627\\">...</a>\\n */\\n // @formatter:on\\n private boolean clearStart = false;\\n\\n private int receiveMaximum = 10000;\\n\\n private int sendMaximum = 10000;\\n\\n private int maximumPacketSize = 10000;\\n\\n private int sendMaximumPacketSize = 10000;\\n\\n private int topicAliasMaximum = 1024;\\n\\n private int sendTopicAliasMaximum = 2048;\\n\\n private long messageExpiryInterval = 86400L;\\n\\n private boolean requestProblemInformation = true;\\n\\n private boolean requestResponseInformation = true;\\n\\n // @formatter:off\\n /**\\n * 默认会话保留一天.\\n * 最大值,4294967295L,会话过期时间【永不过期,单位秒】.\\n * 定义客户端断开后会话保留的时间(仅在 Clean Session = false 时生效).\\n */\\n private long sessionExpiryInterval = 86400L;\\n // @formatter:on\\n\\n /**\\n * 心跳包每隔60秒发一次.\\n */\\n private int keepAliveInterval = 60;\\n\\n private boolean automaticReconnect = true;\\n\\n private long automaticReconnectMaxDelay = 5;\\n\\n private long automaticReconnectInitialDelay = 1;\\n\\n private Set<String> topics = new HashSet<>(0);\\n\\n private int nettyThreads = 32;\\n\\n private boolean retain = false;\\n\\n private boolean noLocal = false;\\n\\n}\\n
\\nHivemqClientV5
\\n/**\\n * @author laokou\\n */\\n@Slf4j\\npublic class HivemqClientV5 {\\n\\n /**\\n * 响应主题.\\n */\\n private final String RESPONSE_TOPIC = \\"response/topic\\";\\n\\n /**\\n * 服务下线数据.\\n */\\n private final byte[] WILL_PAYLOAD = \\"offline\\".getBytes(UTF_8);\\n\\n /**\\n * 相关数据.\\n */\\n private final byte[] CORRELATION_DATA = \\"correlationData\\".getBytes(UTF_8);\\n\\n private final HivemqProperties hivemqProperties;\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n private volatile Mqtt5RxClient client;\\n\\n private final Object lock = new Object();\\n\\n private volatile Disposable connectDisposable;\\n\\n private volatile Disposable subscribeDisposable;\\n\\n private volatile Disposable unSubscribeDisposable;\\n\\n private volatile Disposable publishDisposable;\\n\\n private volatile Disposable disconnectDisposable;\\n\\n private volatile Disposable consumeDisposable;\\n\\n public HivemqClientV5(HivemqProperties hivemqProperties, List<MessageHandler> messageHandlers) {\\n this.hivemqProperties = hivemqProperties;\\n this.messageHandlers = messageHandlers;\\n }\\n\\n public void open() {\\n if (Objects.isNull(client)) {\\n synchronized (lock) {\\n if (Objects.isNull(client)) {\\n client = getMqtt5ClientBuilder().buildRx();\\n }\\n }\\n }\\n connect();\\n consume();\\n }\\n\\n public void close() {\\n if (!Objects.isNull(client)) {\\n disconnectDisposable = client.disconnectWith()\\n .sessionExpiryInterval(hivemqProperties.getSessionExpiryInterval())\\n .applyDisconnect()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(() -> log.info(\\"【Hivemq-V5】 => MQTT断开连接成功,客户端ID:{}\\", hivemqProperties.getClientId()),\\n e -> log.error(\\"【Hivemq-V5】 => MQTT断开连接失败,错误信息:{}\\", e.getMessage(), e));\\n }\\n }\\n\\n public void subscribe() {\\n String[] topics = getTopics();\\n subscribe(topics, getQosArray(topics));\\n }\\n\\n public String[] getTopics() {\\n return hivemqProperties.getTopics().toArray(String[]::new);\\n }\\n\\n public int[] getQosArray(String[] topics) {\\n return Stream.of(topics).mapToInt(item -> hivemqProperties.getSubscribeQos()).toArray();\\n }\\n\\n public void subscribe(String[] topics, int[] qosArray) {\\n checkTopicAndQos(topics, qosArray);\\n if (!Objects.isNull(client)) {\\n List<Mqtt5Subscription> subscriptions = new ArrayList<>(topics.length);\\n for (int i = 0; i < topics.length; i++) {\\n subscriptions.add(Mqtt5Subscription.builder()\\n .topicFilter(topics[i])\\n .qos(getMqttQos(qosArray[i]))\\n .retainAsPublished(hivemqProperties.isRetain())\\n .noLocal(hivemqProperties.isNoLocal())\\n .build());\\n }\\n subscribeDisposable = client.subscribeWith()\\n .addSubscriptions(subscriptions)\\n .applySubscribe()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(ack -> log.info(\\"【Hivemq-V5】 => MQTT订阅成功,主题: {}\\", String.join(\\"、\\", topics)), e -> log\\n .error(\\"【Hivemq-V5】 => MQTT订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics), e.getMessage(), e));\\n }\\n }\\n\\n public void unSubscribe() {\\n String[] topics = hivemqProperties.getTopics().toArray(String[]::new);\\n unSubscribe(topics);\\n }\\n\\n public void unSubscribe(String[] topics) {\\n checkTopic(topics);\\n if (!Objects.isNull(client)) {\\n List<MqttTopicFilter> matchedTopics = new ArrayList<>(topics.length);\\n for (String topic : topics) {\\n matchedTopics.add(MqttTopicFilter.of(topic));\\n }\\n unSubscribeDisposable = client.unsubscribeWith()\\n .addTopicFilters(matchedTopics)\\n .applyUnsubscribe()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(ack -> log.info(\\"【Hivemq-V5】 => MQTT取消订阅成功,主题:{}\\", String.join(\\"、\\", topics)), e -> log\\n .error(\\"【Hivemq-V5】 => MQTT取消订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics), e.getMessage(), e));\\n }\\n }\\n\\n public void publish(String topic, byte[] payload, int qos) {\\n if (!Objects.isNull(client)) {\\n publishDisposable = client\\n .publish(Flowable.just(Mqtt5Publish.builder()\\n .topic(topic)\\n .qos(getMqttQos(qos))\\n .payload(payload)\\n .noMessageExpiry()\\n .retain(hivemqProperties.isRetain())\\n .messageExpiryInterval(hivemqProperties.getMessageExpiryInterval())\\n .correlationData(CORRELATION_DATA)\\n .payloadFormatIndicator(Mqtt5PayloadFormatIndicator.UTF_8)\\n .contentType(\\"text/plain\\")\\n .responseTopic(RESPONSE_TOPIC)\\n .build()))\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(ack -> log.info(\\"【Hivemq-V5】 => MQTT消息发布成功,topic:{}\\", topic),\\n e -> log.error(\\"【Hivemq-V5】 => MQTT消息发布失败,topic:{},错误信息:{}\\", topic, e.getMessage(), e));\\n }\\n }\\n\\n public void publish(String topic, byte[] payload) {\\n publish(topic, payload, hivemqProperties.getPublishQos());\\n }\\n\\n public void dispose(Disposable disposable) {\\n if (!Objects.isNull(disposable) && !disposable.isDisposed()) {\\n // 显式取消订阅\\n disposable.dispose();\\n }\\n }\\n\\n public void dispose() {\\n dispose(connectDisposable);\\n dispose(subscribeDisposable);\\n dispose(unSubscribeDisposable);\\n dispose(publishDisposable);\\n dispose(consumeDisposable);\\n dispose(disconnectDisposable);\\n }\\n\\n public void reSubscribe() {\\n log.info(\\"【Hivemq-V5】 => MQTT重新订阅开始\\");\\n dispose(subscribeDisposable);\\n subscribe();\\n log.info(\\"【Hivemq-V5】 => MQTT重新订阅结束\\");\\n }\\n\\n private MqttQos getMqttQos(int qos) {\\n return MqttQos.fromCode(qos);\\n }\\n\\n private void connect() {\\n connectDisposable = client.connectWith()\\n .keepAlive(hivemqProperties.getKeepAliveInterval())\\n .cleanStart(hivemqProperties.isClearStart())\\n .sessionExpiryInterval(hivemqProperties.getSessionExpiryInterval())\\n .willPublish()\\n .topic(\\"will/topic\\")\\n .payload(WILL_PAYLOAD)\\n .qos(getMqttQos(hivemqProperties.getWillQos()))\\n .retain(true)\\n .messageExpiryInterval(100)\\n .delayInterval(10)\\n .payloadFormatIndicator(Mqtt5PayloadFormatIndicator.UTF_8)\\n .contentType(\\"text/plain\\")\\n .responseTopic(RESPONSE_TOPIC)\\n .correlationData(CORRELATION_DATA)\\n .applyWillPublish()\\n .restrictions()\\n .receiveMaximum(hivemqProperties.getReceiveMaximum())\\n .sendMaximum(hivemqProperties.getSendMaximum())\\n .maximumPacketSize(hivemqProperties.getMaximumPacketSize())\\n .sendMaximumPacketSize(hivemqProperties.getSendMaximumPacketSize())\\n .topicAliasMaximum(hivemqProperties.getTopicAliasMaximum())\\n .sendTopicAliasMaximum(hivemqProperties.getSendTopicAliasMaximum())\\n .requestProblemInformation(hivemqProperties.isRequestProblemInformation())\\n .requestResponseInformation(hivemqProperties.isRequestResponseInformation())\\n .applyRestrictions()\\n .applyConnect()\\n .toFlowable()\\n .firstElement()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(\\n ack -> log.info(\\"【Hivemq-V5】 => MQTT连接成功,主机:{},端口:{},客户端ID:{}\\", hivemqProperties.getHost(),\\n hivemqProperties.getPort(), hivemqProperties.getClientId()),\\n e -> log.error(\\"【Hivemq-V5】 => MQTT连接失败,错误信息:{}\\", e.getMessage(), e));\\n }\\n\\n private void consume() {\\n if (!Objects.isNull(client)) {\\n consumeDisposable = client.publishes(MqttGlobalPublishFilter.ALL)\\n .onBackpressureBuffer(8192)\\n .observeOn(Schedulers.computation(), false, 8192)\\n .doOnSubscribe(subscribe -> {\\n log.info(\\"【Hivemq-V5】 => MQTT开始订阅消息,请稍候。。。。。。\\");\\n reSubscribe();\\n })\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(publish -> {\\n for (MessageHandler messageHandler : messageHandlers) {\\n if (messageHandler.isSubscribe(publish.getTopic().toString())) {\\n log.info(\\"【Hivemq-V5】 => MQTT接收到消息,Topic:{}\\", publish.getTopic());\\n messageHandler\\n .handle(new MqttMessage(publish.getPayloadAsBytes(), publish.getTopic().toString()));\\n }\\n }\\n }, e -> log.error(\\"【Hivemq-V5】 => MQTT消息处理失败,错误信息:{}\\", e.getMessage(), e),\\n () -> log.info(\\"【Hivemq-V5】 => MQTT订阅消息结束,请稍候。。。。。。\\"));\\n }\\n }\\n\\n private Mqtt5ClientBuilder getMqtt5ClientBuilder() {\\n Mqtt5ClientBuilder builder = Mqtt5Client.builder().addConnectedListener(listener -> {\\n Optional<? extends MqttClientConnectionConfig> config = Optional\\n .of(listener.getClientConfig().getConnectionConfig())\\n .get();\\n config.ifPresent(mqttClientConnectionConfig -> log.info(\\"【Hivemq-V5】 => MQTT连接保持时间:{}ms\\",\\n mqttClientConnectionConfig.getKeepAlive()));\\n log.info(\\"【Hivemq-V5】 => MQTT已连接,客户端ID:{}\\", hivemqProperties.getClientId());\\n })\\n .addDisconnectedListener(\\n listener -> log.error(\\"【Hivemq-V5】 => MQTT已断开连接,客户端ID:{}\\", hivemqProperties.getClientId()))\\n .identifier(hivemqProperties.getClientId())\\n .serverHost(hivemqProperties.getHost())\\n .serverPort(hivemqProperties.getPort())\\n .executorConfig(MqttClientExecutorConfig.builder()\\n .nettyExecutor(ThreadUtils.newVirtualTaskExecutor())\\n .nettyThreads(hivemqProperties.getNettyThreads())\\n .applicationScheduler(Schedulers.from(ThreadUtils.newVirtualTaskExecutor()))\\n .build());\\n // 开启重连\\n if (hivemqProperties.isAutomaticReconnect()) {\\n builder.automaticReconnect()\\n .initialDelay(hivemqProperties.getAutomaticReconnectInitialDelay(), TimeUnit.SECONDS)\\n .maxDelay(hivemqProperties.getAutomaticReconnectMaxDelay(), TimeUnit.SECONDS)\\n .applyAutomaticReconnect();\\n }\\n if (hivemqProperties.isAuth()) {\\n builder.simpleAuth()\\n .username(hivemqProperties.getUsername())\\n .password(hivemqProperties.getPassword().getBytes())\\n .applySimpleAuth();\\n }\\n return builder;\\n }\\n\\n private void checkTopicAndQos(String[] topics, int[] qosArray) {\\n if (topics == null || qosArray == null) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V5\\" + \\"】 => Topics and QoS arrays cannot be null\\");\\n }\\n if (topics.length != qosArray.length) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V5\\" + \\"】 => Topics and QoS arrays must have the same length\\");\\n }\\n if (topics.length == 0) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V5\\" + \\"】 => Topics array cannot be empty\\");\\n }\\n }\\n\\n private void checkTopic(String[] topics) {\\n if (topics.length == 0) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V5\\" + \\"】 => Topics array cannot be empty\\");\\n }\\n }\\n\\n}\\n
\\nHivemqV5MqttClientTest
\\n/**\\n * @author laokou\\n */\\n@SpringBootTest\\n@RequiredArgsConstructor\\n@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)\\nclass HivemqV5MqttClientTest {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Test\\n void testMqttClient() throws InterruptedException {\\n HivemqProperties hivemqProperties = new HivemqProperties();\\n hivemqProperties.setClientId(\\"test-client-1\\");\\n hivemqProperties.setTopics(Set.of(\\"/test-topic-1/#\\"));\\n HivemqClientV5 hivemqClientV5 = new HivemqClientV5(hivemqProperties, messageHandlers);\\n hivemqClientV5.open();\\n hivemqClientV5.publish(\\"/test-topic-1/123\\", \\"Hello World123\\".getBytes());\\n }\\n\\n}\\n
\\nHivemqClientV3
\\n/**\\n * @author laokou\\n */\\n@Slf4j\\npublic class HivemqClientV3 {\\n\\n /**\\n * 服务下线数据.\\n */\\n private final byte[] WILL_PAYLOAD = \\"offline\\".getBytes(UTF_8);\\n\\n private final HivemqProperties hivemqProperties;\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n private volatile Mqtt3RxClient client;\\n\\n private final Object lock = new Object();\\n\\n private volatile Disposable connectDisposable;\\n\\n private volatile Disposable subscribeDisposable;\\n\\n private volatile Disposable unSubscribeDisposable;\\n\\n private volatile Disposable publishDisposable;\\n\\n private volatile Disposable disconnectDisposable;\\n\\n private volatile Disposable consumeDisposable;\\n\\n public HivemqClientV3(HivemqProperties hivemqProperties, List<MessageHandler> messageHandlers) {\\n this.hivemqProperties = hivemqProperties;\\n this.messageHandlers = messageHandlers;\\n }\\n\\n public void open() {\\n if (Objects.isNull(client)) {\\n synchronized (lock) {\\n if (Objects.isNull(client)) {\\n client = getMqtt3ClientBuilder().buildRx();\\n }\\n }\\n }\\n connect();\\n consume();\\n }\\n\\n public void close() {\\n if (!Objects.isNull(client)) {\\n disconnectDisposable = client.disconnect()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(() -> log.info(\\"【Hivemq-V3】 => MQTT断开连接成功,客户端ID:{}\\", hivemqProperties.getClientId()),\\n e -> log.error(\\"【Hivemq-V3】 => MQTT断开连接失败,错误信息:{}\\", e.getMessage(), e));\\n }\\n }\\n\\n public void subscribe() {\\n String[] topics = getTopics();\\n subscribe(topics, getQosArray(topics));\\n }\\n\\n public String[] getTopics() {\\n return hivemqProperties.getTopics().toArray(String[]::new);\\n }\\n\\n public int[] getQosArray(String[] topics) {\\n return Stream.of(topics).mapToInt(item -> hivemqProperties.getSubscribeQos()).toArray();\\n }\\n\\n public void subscribe(String[] topics, int[] qosArray) {\\n checkTopicAndQos(topics, qosArray);\\n if (!Objects.isNull(client)) {\\n List<Mqtt3Subscription> subscriptions = new ArrayList<>(topics.length);\\n for (int i = 0; i < topics.length; i++) {\\n subscriptions.add(Mqtt3Subscription.builder()\\n .topicFilter(topics[i])\\n .qos(getMqttQos(qosArray[i]))\\n .build());\\n }\\n subscribeDisposable = client.subscribeWith()\\n .addSubscriptions(subscriptions)\\n .applySubscribe()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(ack -> log.info(\\"【Hivemq-V3】 => MQTT订阅成功,主题: {}\\", String.join(\\"、\\", topics)), e -> log\\n .error(\\"【Hivemq-V3】 => MQTT订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics), e.getMessage(), e));\\n }\\n }\\n\\n public void unSubscribe() {\\n String[] topics = hivemqProperties.getTopics().toArray(String[]::new);\\n unSubscribe(topics);\\n }\\n\\n public void unSubscribe(String[] topics) {\\n checkTopic(topics);\\n if (!Objects.isNull(client)) {\\n List<MqttTopicFilter> matchedTopics = new ArrayList<>(topics.length);\\n for (String topic : topics) {\\n matchedTopics.add(MqttTopicFilter.of(topic));\\n }\\n unSubscribeDisposable = client.unsubscribeWith()\\n .addTopicFilters(matchedTopics)\\n .applyUnsubscribe()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(() -> log.info(\\"【Hivemq-V3】 => MQTT取消订阅成功,主题:{}\\", String.join(\\"、\\", topics)), e -> log\\n .error(\\"【Hivemq-V3】 => MQTT取消订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics), e.getMessage(), e));\\n }\\n }\\n\\n public void publish(String topic, byte[] payload, int qos) {\\n if (!Objects.isNull(client)) {\\n publishDisposable = client\\n .publish(Flowable.just(Mqtt3Publish.builder()\\n .topic(topic)\\n .qos(getMqttQos(qos))\\n .payload(payload)\\n .retain(hivemqProperties.isRetain())\\n .build()))\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(ack -> log.info(\\"【Hivemq-V3】 => MQTT消息发布成功,topic:{}\\", topic),\\n e -> log.error(\\"【Hivemq-V3】 => MQTT消息发布失败,topic:{},错误信息:{}\\", topic, e.getMessage(), e));\\n }\\n }\\n\\n public void publish(String topic, byte[] payload) {\\n publish(topic, payload, hivemqProperties.getPublishQos());\\n }\\n\\n public void dispose(Disposable disposable) {\\n if (!Objects.isNull(disposable) && !disposable.isDisposed()) {\\n // 显式取消订阅\\n disposable.dispose();\\n }\\n }\\n\\n public void dispose() {\\n dispose(connectDisposable);\\n dispose(subscribeDisposable);\\n dispose(unSubscribeDisposable);\\n dispose(publishDisposable);\\n dispose(consumeDisposable);\\n dispose(disconnectDisposable);\\n }\\n\\n public void reSubscribe() {\\n log.info(\\"【Hivemq-V3】 => MQTT重新订阅开始\\");\\n dispose(subscribeDisposable);\\n subscribe();\\n log.info(\\"【Hivemq-V3】 => MQTT重新订阅结束\\");\\n }\\n\\n private MqttQos getMqttQos(int qos) {\\n return MqttQos.fromCode(qos);\\n }\\n\\n private void connect() {\\n connectDisposable = client.connectWith()\\n .keepAlive(hivemqProperties.getKeepAliveInterval())\\n .willPublish()\\n .topic(\\"will/topic\\")\\n .payload(WILL_PAYLOAD)\\n .qos(getMqttQos(hivemqProperties.getWillQos()))\\n .retain(true)\\n .applyWillPublish()\\n .restrictions()\\n .sendMaximum(hivemqProperties.getSendMaximum())\\n .sendMaximumPacketSize(hivemqProperties.getSendMaximumPacketSize())\\n .applyRestrictions()\\n .applyConnect()\\n .toFlowable()\\n .firstElement()\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(\\n ack -> log.info(\\"【Hivemq-V3】 => MQTT连接成功,主机:{},端口:{},客户端ID:{}\\", hivemqProperties.getHost(),\\n hivemqProperties.getPort(), hivemqProperties.getClientId()),\\n e -> log.error(\\"【Hivemq-V3】 => MQTT连接失败,错误信息:{}\\", e.getMessage(), e));\\n }\\n\\n private void consume() {\\n if (!Objects.isNull(client)) {\\n consumeDisposable = client.publishes(MqttGlobalPublishFilter.ALL)\\n .onBackpressureBuffer(8192)\\n .observeOn(Schedulers.computation(), false, 8192)\\n .doOnSubscribe(subscribe -> {\\n log.info(\\"【Hivemq-V3】 => MQTT开始订阅消息,请稍候。。。。。。\\");\\n reSubscribe();\\n })\\n .subscribeOn(Schedulers.io())\\n .retryWhen(errors -> errors.scan(1, (retryCount, error) -> retryCount > 5 ? -1 : retryCount + 1)\\n .takeWhile(retryCount -> retryCount != -1)\\n .flatMap(retryCount -> Flowable.timer((long) Math.pow(2, retryCount) * 100, TimeUnit.MILLISECONDS)))\\n .subscribe(publish -> {\\n for (MessageHandler messageHandler : messageHandlers) {\\n if (messageHandler.isSubscribe(publish.getTopic().toString())) {\\n log.info(\\"【Hivemq-V3】 => MQTT接收到消息,Topic:{}\\", publish.getTopic());\\n messageHandler\\n .handle(new MqttMessage(publish.getPayloadAsBytes(), publish.getTopic().toString()));\\n }\\n }\\n }, e -> log.error(\\"【Hivemq-V3】 => MQTT消息处理失败,错误信息:{}\\", e.getMessage(), e),\\n () -> log.info(\\"【Hivemq-V3】 => MQTT订阅消息结束,请稍候。。。。。。\\"));\\n }\\n }\\n\\n private Mqtt3ClientBuilder getMqtt3ClientBuilder() {\\n Mqtt3ClientBuilder builder = Mqtt3Client.builder().addConnectedListener(listener -> {\\n Optional<? extends MqttClientConnectionConfig> config = Optional\\n .of(listener.getClientConfig().getConnectionConfig())\\n .get();\\n config.ifPresent(mqttClientConnectionConfig -> log.info(\\"【Hivemq-V5】 => MQTT连接保持时间:{}ms\\",\\n mqttClientConnectionConfig.getKeepAlive()));\\n log.info(\\"【Hivemq-V3】 => MQTT已连接,客户端ID:{}\\", hivemqProperties.getClientId());\\n })\\n .addDisconnectedListener(\\n listener -> log.error(\\"【Hivemq-V3】 => MQTT已断开连接,客户端ID:{}\\", hivemqProperties.getClientId()))\\n .identifier(hivemqProperties.getClientId())\\n .serverHost(hivemqProperties.getHost())\\n .serverPort(hivemqProperties.getPort())\\n .executorConfig(MqttClientExecutorConfig.builder()\\n .nettyExecutor(ThreadUtils.newVirtualTaskExecutor())\\n .nettyThreads(hivemqProperties.getNettyThreads())\\n .applicationScheduler(Schedulers.from(ThreadUtils.newVirtualTaskExecutor()))\\n .build());\\n // 开启重连\\n if (hivemqProperties.isAutomaticReconnect()) {\\n builder.automaticReconnect()\\n .initialDelay(hivemqProperties.getAutomaticReconnectInitialDelay(), TimeUnit.SECONDS)\\n .maxDelay(hivemqProperties.getAutomaticReconnectMaxDelay(), TimeUnit.SECONDS)\\n .applyAutomaticReconnect();\\n }\\n if (hivemqProperties.isAuth()) {\\n builder.simpleAuth()\\n .username(hivemqProperties.getUsername())\\n .password(hivemqProperties.getPassword().getBytes())\\n .applySimpleAuth();\\n }\\n return builder;\\n }\\n\\n private void checkTopicAndQos(String[] topics, int[] qosArray) {\\n if (topics == null || qosArray == null) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V3\\" + \\"】 => Topics and QoS arrays cannot be null\\");\\n }\\n if (topics.length != qosArray.length) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V3\\" + \\"】 => Topics and QoS arrays must have the same length\\");\\n }\\n if (topics.length == 0) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V3\\" + \\"】 => Topics array cannot be empty\\");\\n }\\n }\\n\\n private void checkTopic(String[] topics) {\\n if (topics.length == 0) {\\n throw new IllegalArgumentException(\\"【\\" + \\"Hivemq-V3\\" + \\"】 => Topics array cannot be empty\\");\\n }\\n }\\n\\n}\\n
\\nHivemqV3MqttClientTest
\\n/**\\n * @author laokou\\n */\\n@SpringBootTest\\n@RequiredArgsConstructor\\n@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)\\nclass HivemqV3MqttClientTest {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n @Test\\n void testMqttClient() throws InterruptedException {\\n HivemqProperties hivemqProperties2 = new HivemqProperties();\\n hivemqProperties2.setClientId(\\"test-client-2\\");\\n hivemqProperties2.setTopics(Set.of(\\"/test-topic-2/#\\"));\\n HivemqClientV3 hivemqClientV3 = new HivemqClientV3(hivemqProperties2, messageHandlers);\\n hivemqClientV3.open();\\n hivemqClientV3.publish(\\"/test-topic-2/456\\", \\"Hello World456\\".getBytes());\\n }\\n\\n}\\n
\\n<dependencies>\\n <dependency>\\n <groupId>io.vertx</groupId>\\n <artifactId>vertx-mqtt</artifactId>\\n <version>4.5.14</version>\\n </dependency>\\n <dependency>\\n <groupId>io.projectreactor</groupId>\\n <artifactId>reactor-core</artifactId>\\n <version>3.7.5</version>\\n </dependency>\\n</dependencies>\\n
\\nMqttClientProperties
\\n/**\\n * @author laokou\\n */\\n@Data\\npublic class MqttClientProperties {\\n\\n private boolean auth = true;\\n\\n private String username = \\"emqx\\";\\n\\n private String password = \\"laokou123\\";\\n\\n private String host = \\"127.0.0.1\\";\\n\\n private int port = 1883;\\n\\n private String clientId = UUIDGenerator.generateUUID();\\n\\n // @formatter:off\\n /**\\n * 控制是否创建新会话(true=新建,false=复用历史会话). clearStart=true => Broker 会在连接断开后立即清除所有会话信息.\\n * clearStart=false => Broker 会在连接断开后保存会话信息,并在重新连接后复用会话信息.\\n */\\n // @formatter:on\\n private boolean clearSession = false;\\n\\n private int receiveBufferSize = Integer.MAX_VALUE;\\n\\n private int maxMessageSize = -1;\\n\\n /**\\n * 心跳包每隔60秒发一次.\\n */\\n private int keepAliveInterval = 60;\\n\\n private boolean autoKeepAlive = true;\\n\\n private long reconnectInterval = 1000;\\n\\n private int reconnectAttempts = Integer.MAX_VALUE;\\n\\n private Map<String, Integer> topics = new HashMap<>(0);\\n\\n private int willQos = 1;\\n\\n private boolean willRetain = false;\\n\\n private int ackTimeout = -1;\\n\\n private boolean autoAck = true;\\n\\n /**\\n * 服务下线主题.\\n */\\n private String willTopic = \\"/will\\";\\n\\n /**\\n * 服务下线数据.\\n */\\n private String willPayload = \\"offline\\";\\n\\n}\\n
\\nVertxConfig
\\n/**\\n * @author laokou\\n */\\n@Configuration\\npublic class VertxConfig {\\n\\n @Bean\\n public Vertx vertx() {\\n VertxOptions vertxOptions = new VertxOptions();\\n vertxOptions.setMaxEventLoopExecuteTime(60);\\n vertxOptions.setMaxWorkerExecuteTime(60);\\n vertxOptions.setMaxEventLoopExecuteTimeUnit(TimeUnit.SECONDS);\\n vertxOptions.setMaxWorkerExecuteTimeUnit(TimeUnit.SECONDS);\\n vertxOptions.setPreferNativeTransport(true);\\n return Vertx.vertx(vertxOptions);\\n }\\n\\n}\\n
\\nVertxMqttClient
\\n注意:vertx-mqtt不支持客户端自动断线重连,网络不通畅或连接关闭,需要自己手动调用连接!!!实现这个重连的功能
\\n/**\\n * @author laokou\\n */\\n@Slf4j\\npublic class VertxMqttClient {\\n\\n private final Sinks.Many<MqttPublishMessage> messageSink = Sinks.many()\\n .multicast()\\n .onBackpressureBuffer(Integer.MAX_VALUE, false);\\n\\n private final MqttClient mqttClient;\\n\\n private final Vertx vertx;\\n\\n private final MqttClientProperties mqttClientProperties;\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n private final List<Disposable> disposables;\\n\\n private final AtomicBoolean isConnected = new AtomicBoolean(false);\\n\\n private final AtomicBoolean isLoaded = new AtomicBoolean(false);\\n\\n private final AtomicBoolean isReconnected = new AtomicBoolean(true);\\n\\n public VertxMqttClient(final Vertx vertx, final MqttClientProperties mqttClientProperties,\\n final List<MessageHandler> messageHandlers) {\\n this.vertx = vertx;\\n this.mqttClientProperties = mqttClientProperties;\\n this.mqttClient = MqttClient.create(vertx, getOptions());\\n this.messageHandlers = messageHandlers;\\n this.disposables = Collections.synchronizedList(new ArrayList<>());\\n }\\n\\n public void open() {\\n mqttClient.closeHandler(v -> {\\n isConnected.set(false);\\n log.error(\\"【Vertx-MQTT】 => MQTT连接断开,客户端ID:{}\\", mqttClientProperties.getClientId());\\n reconnect();\\n })\\n .publishHandler(messageSink::tryEmitNext)\\n // 仅接收QoS1和QoS2的消息\\n .publishCompletionHandler(id -> {\\n // log.info(\\"【Vertx-MQTT】 => 接收MQTT的PUBACK或PUBCOMP数据包,数据包ID:{}\\", id);\\n })\\n .subscribeCompletionHandler(ack -> {\\n // log.info(\\"【Vertx-MQTT】 => 接收MQTT的SUBACK数据包,数据包ID:{}\\", ack.messageId());\\n })\\n .unsubscribeCompletionHandler(id -> {\\n // log.info(\\"【Vertx-MQTT】 => 接收MQTT的UNSUBACK数据包,数据包ID:{}\\", id);\\n })\\n .pingResponseHandler(s -> {\\n // log.info(\\"【Vertx-MQTT】 => 接收MQTT的PINGRESP数据包\\");\\n })\\n .connect(mqttClientProperties.getPort(), mqttClientProperties.getHost(), connectResult -> {\\n if (connectResult.succeeded()) {\\n isConnected.set(true);\\n log.info(\\"【Vertx-MQTT】 => MQTT连接成功,主机:{},端口:{},客户端ID:{}\\", mqttClientProperties.getHost(),\\n mqttClientProperties.getPort(), mqttClientProperties.getClientId());\\n resubscribe();\\n }\\n else {\\n isConnected.set(false);\\n Throwable ex = connectResult.cause();\\n log.error(\\"【Vertx-MQTT】 => MQTT连接失败,原因:{},客户端ID:{}\\", ex.getMessage(),\\n mqttClientProperties.getClientId(), ex);\\n reconnect();\\n }\\n });\\n }\\n\\n public void close() {\\n disconnect();\\n }\\n\\n /**\\n * Sends the PUBLISH message to the remote MQTT server.\\n * @param topic topic on which the message is published\\n * @param payload message payload\\n * @param qos QoS level\\n * @param isDup if the message is a duplicate\\n * @param isRetain if the message needs to be retained\\n */\\n public void publish(String topic, int qos, String payload, boolean isDup, boolean isRetain) {\\n mqttClient.publish(topic, Buffer.buffer(payload), convertQos(qos), isDup, isRetain);\\n }\\n\\n private void reconnect() {\\n if (isReconnected.get()) {\\n log.info(\\"【Vertx-MQTT】 => MQTT尝试重连\\");\\n vertx.setTimer(mqttClientProperties.getReconnectInterval(),\\n handler -> ThreadUtils.newVirtualTaskExecutor().execute(this::open));\\n }\\n }\\n\\n private void subscribe() {\\n Map<String, Integer> topics = mqttClientProperties.getTopics();\\n checkTopicAndQos(topics);\\n mqttClient.subscribe(topics, subscribeResult -> {\\n if (subscribeResult.succeeded()) {\\n log.info(\\"【Vertx-MQTT】 => MQTT订阅成功,主题: {}\\", String.join(\\"、\\", topics.keySet()));\\n }\\n else {\\n Throwable ex = subscribeResult.cause();\\n log.error(\\"【Vertx-MQTT】 => MQTT订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics.keySet()), ex.getMessage(),\\n ex);\\n }\\n });\\n }\\n\\n private void resubscribe() {\\n if (isConnected.get() || mqttClient.isConnected()) {\\n ThreadUtils.newVirtualTaskExecutor().execute(this::subscribe);\\n }\\n if (isLoaded.compareAndSet(false, true)) {\\n ThreadUtils.newVirtualTaskExecutor().execute(this::consume);\\n }\\n }\\n\\n private void consume() {\\n Disposable disposable = messageSink.asFlux().doOnNext(mqttPublishMessage -> {\\n String topic = mqttPublishMessage.topicName();\\n log.info(\\"【Vertx-MQTT】 => MQTT接收到消息,Topic:{}\\", topic);\\n for (MessageHandler messageHandler : messageHandlers) {\\n if (messageHandler.isSubscribe(topic)) {\\n messageHandler.handle(new MqttMessage(mqttPublishMessage.payload(), topic));\\n }\\n }\\n }).subscribeOn(Schedulers.boundedElastic()).subscribe();\\n disposables.add(disposable);\\n }\\n\\n private void disposable() {\\n for (Disposable disposable : disposables) {\\n if (ObjectUtils.isNotNull(disposable) && !disposable.isDisposed()) {\\n disposable.dispose();\\n }\\n }\\n }\\n\\n private void disconnect() {\\n isReconnected.set(false);\\n mqttClient.disconnect(disconnectResult -> {\\n if (disconnectResult.succeeded()) {\\n disposable();\\n log.info(\\"【Vertx-MQTT】 => MQTT断开连接成功\\");\\n disposables.clear();\\n }\\n else {\\n Throwable ex = disconnectResult.cause();\\n log.error(\\"【Vertx-MQTT】 => MQTT断开连接失败,错误信息:{}\\", ex.getMessage(), ex);\\n }\\n });\\n }\\n\\n private void unsubscribe(List<String> topics) {\\n checkTopic(topics);\\n mqttClient.unsubscribe(topics, unsubscribeResult -> {\\n if (unsubscribeResult.succeeded()) {\\n log.info(\\"【Vertx-MQTT】 => MQTT取消订阅成功,主题:{}\\", String.join(\\"、\\", topics));\\n }\\n else {\\n Throwable ex = unsubscribeResult.cause();\\n log.error(\\"【Vertx-MQTT】 => MQTT取消订阅失败,主题:{},错误信息:{}\\", String.join(\\"、\\", topics), ex.getMessage(), ex);\\n }\\n });\\n }\\n\\n private MqttClientOptions getOptions() {\\n MqttClientOptions options = new MqttClientOptions();\\n options.setClientId(mqttClientProperties.getClientId());\\n options.setCleanSession(mqttClientProperties.isClearSession());\\n options.setAutoKeepAlive(mqttClientProperties.isAutoKeepAlive());\\n options.setKeepAliveInterval(mqttClientProperties.getKeepAliveInterval());\\n options.setReconnectAttempts(mqttClientProperties.getReconnectAttempts());\\n options.setReconnectInterval(mqttClientProperties.getReconnectInterval());\\n options.setWillQoS(mqttClientProperties.getWillQos());\\n options.setWillTopic(mqttClientProperties.getWillTopic());\\n options.setAutoAck(mqttClientProperties.isAutoAck());\\n options.setAckTimeout(mqttClientProperties.getAckTimeout());\\n options.setWillRetain(mqttClientProperties.isWillRetain());\\n options.setWillMessageBytes(Buffer.buffer(mqttClientProperties.getWillPayload()));\\n options.setReceiveBufferSize(mqttClientProperties.getReceiveBufferSize());\\n options.setMaxMessageSize(mqttClientProperties.getMaxMessageSize());\\n if (mqttClientProperties.isAuth()) {\\n options.setPassword(mqttClientProperties.getPassword());\\n options.setUsername(mqttClientProperties.getUsername());\\n }\\n return options;\\n }\\n\\n private void checkTopicAndQos(Map<String, Integer> topics) {\\n topics.forEach((topic, qos) -> {\\n if (StringUtils.isEmpty(topic) || ObjectUtils.isNull(qos)) {\\n throw new IllegalArgumentException(\\"【Vertx-MQTT】 => Topic and QoS cannot be null\\");\\n }\\n });\\n }\\n\\n private void checkTopic(List<String> topics) {\\n if (CollectionUtils.isEmpty(topics)) {\\n throw new IllegalArgumentException(\\"【Vertx-MQTT】 => Topics list cannot be empty\\");\\n }\\n }\\n\\n private MqttQoS convertQos(int qos) {\\n return MqttQoS.valueOf(qos);\\n }\\n\\n}\\n
\\nVertxMqttClientTest
\\n/**\\n * @author laokou\\n */\\n@SpringBootTest\\n@RequiredArgsConstructor\\n@ContextConfiguration(classes = { DefaultMessageHandler.class, VertxConfig.class })\\n@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)\\nclass VertxMqttClientTest {\\n\\n private final List<MessageHandler> messageHandlers;\\n\\n private final Vertx vertx;\\n\\n @Test\\n void testMqttClient() throws InterruptedException {\\n MqttClientProperties properties = new MqttClientProperties();\\n properties.setHost(\\"127.0.0.1\\");\\n properties.setPort(1883);\\n properties.setUsername(\\"emqx\\");\\n properties.setPassword(\\"laokou123\\");\\n properties.setClientId(\\"test-client-1\\");\\n properties.setTopics(Map.of(\\"/test-topic-1/#\\", 1));\\n VertxMqttClient vertxMqttClient = new VertxMqttClient(vertx, properties, messageHandlers);\\n Assertions.assertDoesNotThrow(vertxMqttClient::open);\\n Thread.sleep(500);\\n Assertions.assertDoesNotThrow(() -> vertxMqttClient.publish(\\"/test-topic-1/test\\", 1, \\"test\\", false, false));\\n Thread.sleep(500);\\n Assertions.assertDoesNotThrow(vertxMqttClient::close);\\n Thread.sleep(500);\\n }\\n\\n}\\n
\\n\\n非常推荐使用vertx-mqtt,项目平稳运行好用!!!
\\n但是,需要时注意的是,项目部署到Linux系统,需要最少分配 -Xmx2100m -Xms2100m 内存,不然连接会关闭!
\\n我是老寇,我们下次再见啦~
","description":"小伙伴们,你们好呀,我是老寇,跟我一起学习对接MQTT 安装EMQX\\n\\n采用docker-compose一键式,启动!!!\\n\\n还没有安装docker朋友,参考文章下面两篇文章\\n\\n# Ubuntu20.04安装Docker\\n\\n# Centos7安装Docker 23.0.6\\n\\n使用 emqx 5.4.1,按照老夫的教程来,请不要改版本号!!!\\n使用 emqx 5.4.1,按照老夫的教程来,请不要改版本号!!!\\n使用 emqx 5.4.1,按照老夫的教程来,请不要改版本号!!!\\nservices:\\n emqx:\\n image: emqx/emqx:5…","guid":"https://juejin.cn/post/7499508126200561676","author":"K神","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-05T06:49:42.026Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d15ae88627c14bac98a46a4607b123d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgS-elng==:q75.awebp?rk3s=f64ab15b&x-expires=1747032842&x-signature=klEAtY9yG8PrDC%2B6GeTiH%2BqvrGA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","物联网"],"attachments":null,"extra":null,"language":null},{"title":"Excel百万数据高性能导出方案!","url":"https://juejin.cn/post/7499640198797115431","content":"在我们的日常工作中,经常会有Excel数据导出的需求。
\\n但可能会遇到性能和内存的问题。
\\n今天这篇文章跟大家一起聊聊Excel高性能导出的方案,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\n很多小伙伴门在开发数据导出功能时,习惯性使用Apache POI的HSSF/XSSF组件。
\\n这类方案在数据量超过5万行时,会出现明显的性能断崖式下跌。
\\n根本原因在于内存对象模型的设计缺陷:每个Cell对象占用约1KB内存,百万级数据直接导致JVM堆内存爆炸。
\\n示例代码(反面教材):
\\n// 典型内存杀手写法\\nWorkbook workbook = new XSSFWorkbook();\\nSheet sheet = workbook.createSheet();\\nfor (int i = 0; i < 1000000; i++) {\\n Row row = sheet.createRow(i); // 每行产生Row对象\\n row.createCell(0).setCellValue(\\"数据\\"+i); // 每个Cell独立存储\\n}\\n
\\n这种写法会产生约100万个Row对象和1000万个Cell对象(假设每行10列),直接导致内存占用突破1GB。
\\n更致命的是频繁Full GC会导致系统卡顿甚至OOM崩溃。
\\n最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:所在城市,即可进群。
\\n高性能导出的核心在于内存与磁盘的平衡。
\\n这里给出两种经过生产验证的方案:
\\n使用SXSSFWorkbook类,它是Apache POI的增强版。
\\n具体示例如下:
\\n// 内存中只保留1000行窗口\\nSXSSFWorkbook workbook = new SXSSFWorkbook(1000); \\nSheet sheet = workbook.createSheet();\\nfor (int i = 0; i < 1000000; i++) {\\n Row row = sheet.createRow(i);\\n // 写入后立即刷新到临时文件\\n if(i % 1000 == 0) {\\n ((SXSSFSheet)sheet).flushRows(1000); \\n }\\n}\\n
\\n通过设置滑动窗口机制,将已处理数据写入磁盘临时文件,内存中仅保留当前处理批次。实测百万数据内存占用稳定在200MB以内。
\\nEasyExcel是阿里巴巴开源的Excel高性能处理框架,目前在业界使用比较多。
\\n最近EasyExcel的作者又推出了FastExcel,它是EasyExcel的升级版。
\\n// 极简流式API示例\\nString fileName = \\"data.xlsx\\";\\nEasyExcel.write(fileName, DataModel.class)\\n .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())\\n .sheet(\\"Sheet1\\")\\n .doWrite(data -> {\\n // 分页查询数据\\n int page = 0;\\n while (true) {\\n List<DataModel> list = queryByPage(page, 5000);\\n if (CollectionUtils.isEmpty(list)) break;\\n data.write(list);\\n page++;\\n }\\n });\\n
\\n该方案通过事件驱动模型和对象复用池技术,百万数据导出内存占用可控制在50MB以下。
\\n其核心优势在于:
\\n即便导出工具优化到位,若数据查询环节存在瓶颈,整体性能仍会大打折扣。这里给出三个关键优化点:
\\n传统分页查询在百万级数据时会出现性能雪崩:
\\nSELECT * FROM table LIMIT 900000, 1000 -- 越往后越慢!\\n
\\n正确姿势应使用游标方式:
\\n// 基于自增ID的递进查询\\nLong lastId = 0L;\\nint pageSize = 5000;\\ndo {\\n List<Data> list = jdbcTemplate.query(\\n \\"SELECT * FROM table WHERE id > ? ORDER BY id LIMIT ?\\",\\n new BeanPropertyRowMapper<>(Data.class),\\n lastId, pageSize);\\n if(list.isEmpty()) break;\\n lastId = list.get(list.size()-1).getId();\\n // 处理数据...\\n} while (true);\\n
\\n该方案利用索引的有序性,将时间复杂度从O(N²)降为O(N)。
\\n-- 错误写法:全字段查询\\nSELECT * FROM big_table \\n\\n-- 正确姿势:仅取必要字段\\nSELECT id,name,create_time FROM big_table\\n
\\n实测显示,当单行数据从20个字段缩减到5个字段时,查询耗时降低40%,网络传输量减少70%。
\\n# SpringBoot配置示例\\nspring:\\n datasource:\\n hikari:\\n maximum-pool-size: 20 # 根据CPU核数调整\\n connection-timeout: 30000\\n idle-timeout: 600000\\n max-lifetime: 1800000\\n
\\n导出场景建议使用独立连接池,避免影响主业务。
\\n连接数计算公式:线程数 = CPU核心数 * 2 + 磁盘数
。
想要提升Excel数据导出的性能,我们必须使用多线程异步导出的方案。
\\n具体示例如下:
\\n@Async(\\"exportExecutor\\")\\npublic CompletableFuture<String> asyncExport(ExportParam param) {\\n // 1. 计算分片数量\\n int total = dataService.count(param);\\n int shardSize = total / 100000; \\n\\n // 2. 并行处理分片\\n List<CompletableFuture<Void>> futures = new ArrayList<>();\\n for (int i = 0; i < shardSize; i++) {\\n int finalI = i;\\n futures.add(CompletableFuture.runAsync(() -> {\\n exportShard(param, finalI * 100000, 100000);\\n }, forkJoinPool.commonPool()));\\n }\\n\\n // 3. 合并文件\\n CompletableFuture.allOf(futures.toArray(new CompletableFuture)\\n .thenApply(v -> mergeFiles(shardSize));\\n return CompletableFuture.completedFuture(taskId);\\n}\\n
\\n通过分治策略将任务拆解为多个子任务并行执行,结合线程池管理实现资源可控。
\\n我们需要配置JVM参数,并且需要对这些参数进行调优:
\\n// JVM启动参数示例\\n-Xmx4g -Xms4g \\n-XX:+UseG1GC \\n-XX:MaxGCPauseMillis=200\\n-XX:ParallelGCThreads=4\\n-XX:ConcGCThreads=2\\n-XX:InitiatingHeapOccupancyPercent=35\\n
\\n这样可以有效的提升性能。
\\n导出场景需特别注意:
\\nExcel高性能导出的方案如下图所示:
\\n用户点击导出按钮,会写入DB,生成一个唯一的任务ID,任务状态为待执行。
\\n然后后台异步处理,可以分页将数据写入到Excel中(这个过程可以使用多线程实现)。
\\n将Excel文件存储到云存储中。
\\n然后更新任务状态为以完成。
\\n最后通过WebSocket通知用户导出结果。
\\n经过多个千万级项目的锤炼,我们总结出Excel高性能导出的黄金公式:
\\n高性能 = 流式处理引擎 + 分页查询优化 + 资源管控
\\n具体实施时可参考以下决策树:
\\n最后给小伙伴们的三个忠告:
\\n希望本文能帮助大家在数据导出的战场上,真正实现\\"百万数据,弹指之间\\"!
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n在我们的日常工作中,经常会有Excel数据导出的需求。\\n\\n但可能会遇到性能和内存的问题。\\n\\n今天这篇文章跟大家一起聊聊Excel高性能导出的方案,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。\\n\\n1 传统方案的问题\\n\\n很多小伙伴门在开发数据导出功能时,习惯性使用Apache POI的HSSF/XSSF组件。\\n\\n这类方案在数据量超过5万行时,会出现明显的性能断崖式下跌。\\n\\n根本原因在于内存对象模型的…","guid":"https://juejin.cn/post/7499640198797115431","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-05T06:26:10.940Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b20e8882d1fd4c21be006f2f03ddf37b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747031169&x-signature=mZGCZBxVJ%2Br9J4Pt3pd%2FB6pPOt8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/07aaa856db01471b9f1cdec2c997f0fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1747031169&x-signature=ozJ1KdyA7oi7cIJteefx4blSBxI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"go-doudou + langchaingo 微内核架构RAG大模型知识库实战(二)","url":"https://juejin.cn/post/7499441782163652646","content":"在上一篇文章中,我们介绍了go-doudou框架中的插件机制与模块化可插拔微内核架构的基本概念和原理。本篇文章将详细讲解如何从零开始搭建一个基于go-doudou的微内核架构应用,帮助新人快速上手开发。
\\n微内核架构(也称为插件架构)将应用分为核心系统和插件模块:
\\n这种架构的优势在于:
\\ngo-doudou框架通过其强大的CLI工具和插件机制,使得构建微内核架构应用变得更加简单高效。
\\n首先,我们需要安装go-doudou命令行工具。对于Go 1.17及以上版本,推荐使用以下命令全局安装:
\\ngo install -v github.com/unionj-cloud/go-doudou/v2@v2.5.9\\n
\\n安装完成后,可以通过以下命令验证安装是否成功:
\\ngo-doudou version\\n
\\ngo-doudou提供了work
命令来创建和管理工作空间,这是构建微内核架构应用的第一步。
# 创建一个名为go-doudou-rag的工作空间\\ngo-doudou work init go-doudou-rag\\n\\n# 进入工作空间目录\\ncd go-doudou-rag\\n
\\n这个命令会创建一个包含以下结构的工作空间:
\\ngo-doudou-rag/\\n ├── go.work # Go工作空间文件\\n ├── main/ # 主应用模块\\n │ ├── cmd/ # 主程序入口\\n │ └── config/ # 主程序配置\\n
\\n工作空间创建后,自动初始化了Git仓库并生成了.gitignore
文件。main
模块是应用的核心,它将负责加载和管理所有插件模块。
主模块是微内核架构的核心,负责加载和管理插件。我们需要理解并修改主模块的核心代码。
\\n主模块的cmd/main.go
文件包含了初始化和启动应用的代码。在go-doudou微内核架构中,这个文件通常包含以下内容:
package main\\n\\nimport (\\n \\"github.com/unionj-cloud/go-doudou/v2/framework/grpcx\\"\\n \\"github.com/unionj-cloud/go-doudou/v2/framework/plugin\\"\\n \\"github.com/unionj-cloud/go-doudou/v2/framework/rest\\"\\n \\"github.com/unionj-cloud/toolkit/pipeconn\\"\\n \\"github.com/unionj-cloud/toolkit/zlogger\\"\\n \\n // 以下是导入的插件模块,初始阶段可能没有\\n)\\n\\nfunc main() {\\n // 创建REST服务器\\n srv := rest.NewRestServer()\\n \\n // 创建gRPC服务器(如果需要)\\n grpcServer := grpcx.NewGrpcServer()\\n lis, dialCtx := pipeconn.NewPipeListener()\\n \\n // 获取所有注册的服务插件\\n plugins := plugin.GetServicePlugins()\\n for _, key := range plugins.Keys() {\\n value, _ := plugins.Get(key)\\n // 初始化每个插件\\n value.Initialize(srv, grpcServer, dialCtx)\\n }\\n \\n // 资源清理\\n defer func() {\\n if r := recover(); r != nil {\\n zlogger.Info().Msgf(\\"Recovered. Error: %v\\\\n\\", r)\\n }\\n // 关闭所有插件\\n for _, key := range plugins.Keys() {\\n value, _ := plugins.Get(key)\\n value.Close()\\n }\\n }()\\n \\n // 启动gRPC服务器\\n go func() {\\n grpcServer.RunWithPipe(lis)\\n }()\\n \\n // 添加API文档路由\\n srv.AddRoutes(rest.DocRoutes(\\"\\"))\\n \\n // 启动REST服务器\\n srv.Run()\\n}\\n
\\n这段代码实现了微内核架构的核心功能:获取注册的插件,初始化它们,并在应用退出时释放资源。
\\n在主模块中,我们通常会添加一些通用的中间件和工具,例如认证、日志、监控等。以JWT认证中间件为例:
\\nmkdir -p toolkit/auth\\n
\\n在toolkit/auth
目录下创建auth.go
文件,实现JWT认证中间件:
package auth\\n\\nimport (\\n\\"context\\"\\n\\"fmt\\"\\n\\"github.com/golang-jwt/jwt/v5\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/rest/httprouter\\"\\n\\"github.com/unionj-cloud/toolkit/copier\\"\\n\\"go-doudou-rag/toolkit/config\\"\\n\\"net/http\\"\\n\\"slices\\"\\n\\"strings\\"\\n\\"time\\"\\n)\\n\\nvar authMiddleware *AuthMiddleware\\n\\nfunc init() {\\nconf := config.LoadFromEnv()\\nauthMiddleware = &AuthMiddleware{\\nJwtSecret: conf.Auth.JwtSecret,\\nJwtExpiresIn: conf.Auth.JwtExpiresIn,\\n}\\n}\\n\\nfunc JwtToken(userInfo UserInfo) string {\\nreturn authMiddleware.JwtToken(userInfo)\\n}\\n\\nfunc Jwt(inner http.Handler) http.Handler {\\nreturn authMiddleware.Jwt(inner)\\n}\\n\\ntype AuthMiddleware struct {\\nJwtSecret string\\nJwtExpiresIn time.Duration\\n}\\n\\ntype UserInfo struct {\\nUsername string `json:\\"username\\"`\\n}\\n\\ntype ctxKey int\\n\\nconst userInfoKey ctxKey = ctxKey(0)\\n\\nfunc NewUserInfoContext(ctx context.Context, userInfo UserInfo) context.Context {\\nreturn context.WithValue(ctx, userInfoKey, userInfo)\\n}\\n\\nfunc UserInfoFromContext(ctx context.Context) (UserInfo, bool) {\\nuserInfo, ok := ctx.Value(userInfoKey).(UserInfo)\\nreturn userInfo, ok\\n}\\n\\nfunc (auth *AuthMiddleware) JwtToken(userInfo UserInfo) string {\\nvar claims jwt.MapClaims\\nerr := copier.DeepCopy(userInfo, &claims)\\nif err != nil {\\npanic(err)\\n}\\n\\nclaims[\\"exp\\"] = time.Now().Add(auth.JwtExpiresIn).Unix()\\n\\ntoken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(auth.JwtSecret))\\nif err != nil {\\npanic(err)\\n}\\nreturn token\\n}\\n\\nfunc (auth *AuthMiddleware) Jwt(inner http.Handler) http.Handler {\\nreturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\\nparamsFromCtx := httprouter.ParamsFromContext(r.Context())\\nrouteName := paramsFromCtx.MatchedRouteName()\\n\\nannotation, ok := framework.GetAnnotation(routeName, \\"@role\\")\\nif ok && slices.Contains(annotation.Params, \\"guest\\") {\\ninner.ServeHTTP(w, r)\\nreturn\\n}\\n\\nauthHeader := r.Header.Get(\\"Authorization\\")\\ntokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, \\"Bearer \\"))\\n\\ntoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\\nif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\\nreturn nil, fmt.Errorf(\\"unexpected signing method: %v\\", token.Header[\\"alg\\"])\\n}\\nreturn []byte(auth.JwtSecret), nil\\n})\\nif err != nil || !token.Valid {\\nw.WriteHeader(401)\\nw.Write([]byte(\\"Unauthorised.\\\\n\\"))\\nreturn\\n}\\n\\nclaims := token.Claims.(jwt.MapClaims)\\n\\nvar userInfo UserInfo\\nerr = copier.DeepCopy(claims, &userInfo)\\nif err != nil {\\npanic(err)\\n}\\n\\nr = r.WithContext(NewUserInfoContext(r.Context(), userInfo))\\ninner.ServeHTTP(w, r)\\n})\\n}\\n
\\n然后在主模块的main.go
中使用这个中间件:
func main() {\\n srv := rest.NewRestServer()\\n // 添加JWT中间件\\n srv.Use(auth.Jwt)\\n \\n // 其他代码...\\n}\\n
\\n接下来,我们创建具体的功能模块。每个模块是一个独立的Go模块,但会被注册为主应用的插件。我们将创建三个示例模块:认证模块、知识库模块和聊天模块。
\\n# 在工作空间根目录执行\\n# 创建认证模块\\ngo-doudou svc init module-auth -m go-doudou-rag/module-auth --module --case snake -t rest\\n
\\n参数说明:
\\nsvc init
: 初始化服务module-auth
: 服务名称-m go-doudou-rag/module-auth
: 模块导入路径--module
: 指定这是工作空间中的一个模块--case snake
:使用蛇形命名风格-t rest
:生成RESTful服务这个命令会创建module-auth
目录,并生成基本的模块结构:
module-auth/\\n ├── cmd/ # 独立运行入口\\n ├── config/ # 模块配置\\n ├── dto/ # 数据传输对象\\n ├── plugin/ # 插件实现\\n ├── transport/ # 传输层\\n │ └── httpsrv/ # HTTP服务\\n ├── go.mod # Go模块文件\\n ├── svc.go # 服务接口定义\\n └── svcimpl.go # 服务实现\\n
\\n同时,go-doudou会自动执行go work use module-auth
将新模块添加到工作空间,并更新主模块的main.go
文件,添加对新模块插件的导入:
import (\\n // 其他导入...\\n _ \\"go-doudou-rag/module-auth/plugin\\"\\n)\\n
\\n编辑module-auth/svc.go
文件,定义认证服务的接口:
package service\\n\\nimport (\\n\\"context\\"\\n\\"go-doudou-rag/module-auth/dto\\"\\n\\"go-doudou-rag/module-auth/internal/model\\"\\n)\\n\\n//go:generate go-doudou svc http --case snake\\n\\ntype ModuleAuth interface {\\n// PostLogin @role(guest)\\nPostLogin(ctx context.Context, req dto.LoginReq) (data string, err error)\\nGetMe(ctx context.Context) (data *model.User, err error)\\n}\\n
\\n注意//go:generate
指令,它告诉go-doudou生成HTTP相关代码。
在dto
目录下创建login.go
文件:
package dto\\n\\ntype LoginReq struct {\\n Username string `json:\\"username\\" validate:\\"required\\"`\\n Password string `json:\\"password\\" validate:\\"required\\"`\\n}\\n
\\n在internal/model
目录下创建user.go
文件:
package model\\n\\nimport \\"time\\"\\n\\ntype User struct {\\n ID uint `gorm:\\"primarykey\\" json:\\"id\\"`\\n Username string `json:\\"username\\"`\\n Password string `json:\\"-\\"`\\n CreatedAt time.Time `json:\\"created_at\\"`\\n UpdatedAt time.Time `json:\\"updated_at\\"`\\n}\\n
\\n编辑module-auth/svcimpl.go
文件,实现认证服务的逻辑:
package service\\n\\nimport (\\n\\"context\\"\\n\\"go-doudou-rag/module-auth/config\\"\\n\\"go-doudou-rag/module-auth/dto\\"\\n\\"go-doudou-rag/module-auth/internal/dao\\"\\n\\"go-doudou-rag/module-auth/internal/model\\"\\n\\"go-doudou-rag/toolkit/auth\\"\\n)\\n\\nvar _ ModuleAuth = (*ModuleAuthImpl)(nil)\\n\\ntype ModuleAuthImpl struct {\\nconf *config.Config\\n}\\n\\nfunc NewModuleAuth(conf *config.Config) *ModuleAuthImpl {\\nreturn &ModuleAuthImpl{\\nconf: conf,\\n}\\n}\\n\\nfunc (receiver *ModuleAuthImpl) PostLogin(ctx context.Context, req dto.LoginReq) (data string, err error) {\\nuserRepo := dao.GetUserRepo()\\nuser := userRepo.FindOneByUsername(ctx, req.Username)\\nif user == nil {\\npanic(\\"user not found\\")\\n}\\n\\nif user.Password != req.Password {\\npanic(\\"wrong password\\")\\n}\\n\\ndata = auth.JwtToken(auth.UserInfo{\\nUsername: user.Username,\\n})\\nreturn data, nil\\n}\\n\\nfunc (receiver *ModuleAuthImpl) GetMe(ctx context.Context) (data *model.User, err error) {\\nuserInfo, _ := auth.UserInfoFromContext(ctx)\\n\\nuserRepo := dao.GetUserRepo()\\nuser := userRepo.FindOneByUsername(ctx, userInfo.Username)\\n\\nreturn user, nil\\n}\\n
\\n在internal/dao
目录下创建user.go
文件,实现数据访问:
package dao\\n\\nimport (\\n\\"context\\"\\n\\"go-doudou-rag/module-auth/internal/model\\"\\n\\"gorm.io/gorm\\"\\n)\\n\\nvar userRepo *UserRepo\\n\\nfunc init() {\\nuserRepo = &UserRepo{}\\n}\\n\\ntype UserRepo struct {\\ndb *gorm.DB\\n}\\n\\nfunc (ur *UserRepo) Use(db *gorm.DB) {\\nur.db = db\\n}\\n\\nfunc (ur *UserRepo) Init() {\\nadmin := model.User{\\nUsername: \\"admin\\",\\nPassword: \\"admin\\",\\n}\\nif err := ur.db.Save(&admin).Error; err != nil {\\npanic(err)\\n}\\n}\\n\\nfunc (ur *UserRepo) FindOneByUsername(ctx context.Context, username string) *model.User {\\nvar users []*model.User\\nif err := ur.db.Where(\\"username = ?\\", username).Find(&users).Error; err != nil {\\npanic(err)\\n}\\n\\nif len(users) == 0 {\\nreturn nil\\n}\\nreturn users[0]\\n}\\n
\\n编辑module-auth/config/config.go
文件,定义模块配置:
package config\\n\\nimport (\\n_ \\"github.com/unionj-cloud/go-doudou/v2/framework/config\\"\\n\\"github.com/unionj-cloud/toolkit/envconfig\\"\\n\\"github.com/unionj-cloud/toolkit/zlogger\\"\\n)\\n\\nvar G_Config *Config\\n\\ntype Config struct {\\nBiz struct {\\n}\\nDb struct {\\nDsn string\\n}\\n}\\n\\nfunc init() {\\nvar conf Config\\nerr := envconfig.Process(\\"moduleauth\\", &conf)\\nif err != nil {\\nzlogger.Panic().Msgf(\\"Error processing environment variables: %v\\", err)\\n}\\nG_Config = &conf\\n}\\n\\nfunc LoadFromEnv() *Config {\\nreturn G_Config\\n}\\n
\\nplugin
目录下已经生成了插件的基本实现。我们需要确保该插件正确初始化数据库和服务。编辑module-auth/plugin/plugin.go
文件:
package plugin\\n\\nimport (\\n\\"github.com/glebarez/sqlite\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/grpcx\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/plugin\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/rest\\"\\n\\"github.com/unionj-cloud/toolkit/pipeconn\\"\\n\\"github.com/unionj-cloud/toolkit/stringutils\\"\\nservice \\"go-doudou-rag/module-auth\\"\\n\\"go-doudou-rag/module-auth/config\\"\\n\\"go-doudou-rag/module-auth/internal/dao\\"\\n\\"go-doudou-rag/module-auth/internal/model\\"\\n\\"go-doudou-rag/module-auth/transport/httpsrv\\"\\n\\"google.golang.org/grpc\\"\\n\\"gorm.io/gorm\\"\\n\\"os\\"\\n)\\n\\nvar _ plugin.ServicePlugin = (*ModuleAuthPlugin)(nil)\\n\\ntype ModuleAuthPlugin struct {\\ngrpcConns []*grpc.ClientConn\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) Close() {\\nfor _, item := range receiver.grpcConns {\\nitem.Close()\\n}\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) GoDoudouServicePlugin() {\\n\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) GetName() string {\\nname := os.Getenv(\\"GDD_SERVICE_NAME\\")\\nif stringutils.IsEmpty(name) {\\nname = \\"cloud.unionj.ModuleAuth\\"\\n}\\nreturn name\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) Initialize(restServer *rest.RestServer, grpcServer *grpcx.GrpcServer, dialCtx pipeconn.DialContextFunc) {\\nconf := config.LoadFromEnv()\\n\\ndb, err := gorm.Open(sqlite.Open(conf.Db.Dsn), &gorm.Config{})\\nif err != nil {\\npanic(\\"failed to connect database\\")\\n}\\n\\nif err = db.AutoMigrate(&model.User{}); err != nil {\\npanic(err)\\n}\\n\\ndao.Use(db)\\ndao.Init()\\n\\nsvc := service.NewModuleAuth(conf)\\nroutes := httpsrv.Routes(httpsrv.NewModuleAuthHandler(svc))\\nrestServer.GroupRoutes(\\"/moduleauth\\", routes)\\nrestServer.GroupRoutes(\\"/moduleauth\\", rest.DocRoutes(service.Oas))\\n}\\n\\nfunc init() {\\nplugin.RegisterServicePlugin(&ModuleAuthPlugin{})\\n}\\n
\\n现在,我们需要生成HTTP相关的代码。在module-auth
目录下执行:
go-doudou svc http --case snake\\n
\\n这个命令会根据svc.go
中定义的接口,生成HTTP路由、处理器和OpenAPI文档。
按照类似的步骤,创建知识库模块和聊天模块:
\\n# 创建知识库模块\\ngo-doudou svc init module-knowledge -m go-doudou-rag/module-knowledge --module --case snake -t rest\\n\\n# 创建聊天模块\\ngo-doudou svc init module-chat -m go-doudou-rag/module-chat --module --case snake -t rest\\n
\\n为每个模块定义服务接口、实现服务逻辑、配置插件等。以下是一个知识库模块的服务接口示例:
\\npackage service\\n\\nimport (\\n\\"context\\"\\nv3 \\"github.com/unionj-cloud/toolkit/openapi/v3\\"\\n\\"go-doudou-rag/module-knowledge/dto\\"\\n)\\n\\n//go:generate go-doudou svc http --case snake\\n\\ntype ModuleKnowledge interface {\\nUpload(ctx context.Context, file v3.FileModel) (data dto.UploadResult, err error)\\nGetList(ctx context.Context) (data []dto.FileDTO, err error)\\nGetQuery(ctx context.Context, req dto.QueryReq) (data []dto.QueryResult, err error)\\n}\\n
\\n微内核架构应用中,模块之间需要进行通信。go-doudou提供了两种主要的通信方式:直接导入和依赖注入。
\\npackage service\\n\\nimport (\\n \\"context\\"\\n knowledge \\"go-doudou-rag/module-knowledge\\"\\n \\"go-doudou-rag/module-chat/dto\\"\\n)\\n\\nfunc (receiver *ModuleChatImpl) Chat(ctx context.Context, req dto.ChatRequest) (err error) {\\n // 直接导入知识库模块的服务接口\\n knowService := knowledge.NewModuleKnowledge(knowConf)\\n queryResults, err := knowService.GetQuery(ctx, knowledge.QueryReq{\\n Text: req.Prompt,\\n Top: 10,\\n })\\n \\n // 处理结果...\\n}\\n
\\n更推荐的方式是使用依赖注入,这可以使模块之间的耦合更加松散:
\\n// 在知识库模块的plugin/plugin.go文件中注册服务\\nfunc init() {\\n plugin.RegisterServicePlugin(&ModuleKnowledgePlugin{})\\n\\n do.Provide[service.ModuleKnowledge](nil, func(injector *do.Injector) (service.ModuleKnowledge, error) {\\n conf := config.LoadFromEnv()\\n \\n // 初始化数据库...\\n \\n svc := service.NewModuleKnowledge(conf)\\n return svc, nil\\n })\\n}\\n\\n// 在聊天模块中使用依赖注入获取服务\\nimport (\\n \\"github.com/samber/do\\"\\n know \\"go-doudou-rag/module-knowledge\\"\\n)\\n\\nfunc (receiver *ModuleChatImpl) Chat(ctx context.Context, req dto.ChatRequest) (err error) {\\n // 使用依赖注入获取知识库服务\\n knowService := do.MustInvoke[know.ModuleKnowledge](nil)\\n queryResults, err := knowService.GetQuery(ctx, know.QueryReq{\\n Text: req.Prompt,\\n Top: 10,\\n })\\n \\n // 处理结果...\\n}\\n
\\ngo-doudou微内核架构应用使用分层的配置管理方式,结合了配置文件和环境变量。
\\n在工作空间根目录创建app.yml
文件:
toolkit:\\n auth:\\n jwt-secret: \\"my-jwt-secret\\"\\n jwt-expires-in: \\"12h\\"\\n\\nmoduleauth:\\n db:\\n# dsn: \\":memory:\\"\\n dsn: \\"/Users/wubin1989/workspace/cloud/unionj-cloud/go-doudou-rag/data/auth.db\\"\\n\\nmoduleknowledge:\\n biz:\\n file-save-path: \\"/Users/wubin1989/workspace/cloud/unionj-cloud/go-doudou-rag/data/files\\"\\n vector-store:\\n export-to-file: \\"/Users/wubin1989/workspace/cloud/unionj-cloud/go-doudou-rag/data/chromem-go.gob\\"\\n db:\\n dsn: \\"/Users/wubin1989/workspace/cloud/unionj-cloud/go-doudou-rag/data/knowledge.db\\"\\n openai:\\n base-url: \\"https://api.siliconflow.cn/v1\\"\\n token:\\n embedding-model: \\"BAAI/bge-large-zh-v1.5\\"\\n\\nmodulechat:\\n openai:\\n base-url: \\"https://api.siliconflow.cn/v1\\"\\n token:\\n embedding-model: \\"BAAI/bge-large-zh-v1.5\\"\\n model: \\"Qwen/Qwen2.5-32B-Instruct\\"\\n
\\ngo-doudou允许通过环境变量覆盖配置文件中的值:
\\n# JWT密钥\\nexport TOOLKIT_AUTH_JWTSECRET=\\"awesome-jwt-secret\\"\\n\\n# JWT密钥过期时间\\nexport TOOLKIT_AUTH_JWTEXPIRESIN=\\"24h\\"\\n\\n# 数据库连接字符串\\nexport MODULEAUTH_DB_DSN=\\"/data/production/auth.db\\"\\n
\\n环境变量名的构成规则是:模块前缀(大写)+ 下划线 + 配置路径(大写,用下划线分隔),ymal格式配置中的中横线在环境变量里需去掉。
\\n使用示例:
\\nTOOLKIT_AUTH_JWTEXPIRESIN=24h TOOLKIT_AUTH_JWTSECRET=awesome-jwt-secret go run cmd/main.go\\n
\\n在工作空间根目录执行:
\\ncd main\\ngo run cmd/main.go\\n
\\n这将启动主程序,加载所有注册的插件模块。
\\n每个模块都可以独立运行,这在开发阶段非常有用:
\\ncd module-auth\\ngo run cmd/main.go\\n
\\n独立运行时,模块会启动自己的HTTP服务器,而不会加载其他模块。未来需要扩展成微服务架构的时候,可以轻松实现架构升级。
\\ngo-doudou自动为每个模块生成OpenAPI 3.0规范文档,可以通过以下URL访问:
\\nhttp://localhost:6060/go-doudou/doc
http://localhost:6060/moduleauth/go-doudou/doc
http://localhost:6060/modulechat/go-doudou/doc
http://localhost:6060/moduleknowledge/go-doudou/doc
具体如何自定义OpenAPI 3.0规范的文档说明,请参考go-doudou官方文档 接口定义 一节。
\\n有时我们需要更精细地控制插件的初始化过程:
\\nfunc (receiver *ModuleChatPlugin) Initialize(restServer *rest.RestServer, grpcServer *grpcx.GrpcServer, dialCtx pipeconn.DialContextFunc) {\\nconf := config.LoadFromEnv()\\nsvc := service.NewModuleChat(conf)\\nroutes := httpsrv.Routes(httpsrv.NewModuleChatHandler(svc))\\n\\n // httpsrv.InjectResponseWriter是一个自定义路由中间件,针对以/modulechat开头的一组路由生效\\nrestServer.GroupRoutes(\\"/modulechat\\", routes, httpsrv.InjectResponseWriter)\\nrestServer.GroupRoutes(\\"/modulechat\\", rest.DocRoutes(service.Oas))\\n}\\n
\\n依赖注入时可以使用不同的作用域:
\\n// 单例模式\\ndo.Provide[service.ModuleKnowledge](nil, func(injector *do.Injector) (service.ModuleKnowledge, error) {\\n // ...\\n})\\n\\n// 请求作用域(每次请求创建新实例)\\ndo.ProvideNamed[service.ModuleKnowledge](\\"request\\", nil, func(injector *do.Injector) (service.ModuleKnowledge, error) {\\n // ...\\n})\\n\\n// 使用命名注入\\nknowService := do.MustInvokeNamed[know.ModuleKnowledge](\\"request\\", nil)\\n
\\n通过本文的详细指南,我们展示了如何使用go-doudou从零构建一个微内核架构应用。这种架构模式具有高度的模块化和可扩展性,非常适合微内核系统和大型应用的开发。
\\ngo-doudou的CLI工具和插件机制大大简化了微内核架构的实现,让开发者可以专注于业务逻辑,而不必过多关注基础设施的搭建。通过遵循本文介绍的开发流程和最佳实践,您可以快速掌握基于go-doudou构建微内核架构应用的方法。
\\n目前的使用方式是基于命令行或者postman的,在《go-doudou + langchaingo 微内核架构RAG大模型知识库实战(三)》中,我们将加上基于vue 3实现的对话界面,且将前端资源打包编译进聊天模块,实现全栈式开发和轻量化部署。
","description":"在上一篇文章中,我们介绍了go-doudou框架中的插件机制与模块化可插拔微内核架构的基本概念和原理。本篇文章将详细讲解如何从零开始搭建一个基于go-doudou的微内核架构应用,帮助新人快速上手开发。 1. 微内核架构应用的概念回顾\\n\\n微内核架构(也称为插件架构)将应用分为核心系统和插件模块:\\n\\n核心系统:提供基础服务和插件管理机制\\n插件模块:独立开发、独立部署的功能单元\\n\\n这种架构的优势在于:\\n\\n高内聚、低耦合:各模块之间通过定义明确的接口通信\\n可扩展性强:无需修改核心系统即可添加新功能\\n灵活部署:可按需加载模块,系统更加轻量\\n独立开发:团队可以并行开…","guid":"https://juejin.cn/post/7499441782163652646","author":"武斌","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-04T11:00:55.339Z","media":null,"categories":["后端","Go","LangChain"],"attachments":null,"extra":null,"language":null},{"title":"XXL-JOB v3.1.0 | 分布式任务调度平台","url":"https://juejin.cn/post/7500070714625769482","content":"此处以 difyWorkflowJobHandler 为例,注意需要前置部署AI执行器(xxl-job-executor-sample-ai),可参考官方文档说明。
\\nXXL-JOB支持多模式任务,下文以简单的“Bean模式任务”为例介绍,三步快速开发接入。
\\n@XxlJob(\\"demoJobHandler\\")\\n public void demoJobHandler() throws Exception {\\n XxlJobHelper.log(\\"XXL-JOB, Hello World.\\");\\n}\\n
\\nXXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
\\n\\n一个轻量级、跨语言远程过程调用实现,基于json、http实现。
\\n第一步:RPC业务服务开发
\\npublic interface UserService {\\n public ResultDTO createUser(UserDTO userDTO);\\n public UserDTO loadUser(String name);\\n ... ...\\n}\\n
\\n第二步:JsonRpc服务端配置
\\n// a、JsonRpcServer 初始化\\nJsonRpcServer jsonRpcServer = new JsonRpcServer();\\n\\n// b、业务服务注册(支持多服务注册)\\njsonRpcServer.register(\\"userService\\", new UserServiceImpl());\\n\\n// c、Web框架集成,该入口为RPC统一流量入口(springmvc 集成;理论上支持任意web框架集成,其他框架参考集成)\\n@RequestMapping(\\"/openapi\\")\\n@ResponseBody\\npublic String api(@RequestBody(required = false) String requestBody){\\n// 核心代码:Http请求的 RequestBody 作为入参;业务响应作为输出;\\n return jsonRpcServer.invoke(requestBody);\\n}\\n
\\n第三步:JsonRpc客户端配置
\\n// 方式1:代理方式使用 (针对接口构建代理,通过代理对象实现远程调用;)\\nUserService userService = new JsonRpcClient(\\"http://localhost:8080/jsonrpc\\", 3000).proxy(\\"userService\\", UserService.class);\\n\\n// 发起RPC请求;\\nUserDTO result = userService.loadUser(\\"zhangsan\\");\\n
\\n高性能内存队列,单机支持 30W+ TPS,具备良好的性能及高并发优势,支持生产消费模型。
\\n// a、定义队列:指定 消费者数量、批量消费数量、消费者逻辑等\\nMessageQueue<String> messageQueue = new MessageQueue<>(\\n\\"demoQueue\\",\\nmessages -> {\\n// 消费逻辑\\nSystem.out.println(\\"Consume: \\" + messages);\\n},\\n10,// 自定义消费者线程\\n20// 自定义批量消费数量\\n);\\n\\n// b、生产消息\\nmessageQueue.produce(\\"test-\\" + i);\\n
\\n时间轮算法实现,具备高精度、多任务、以及线程安全等优势。
\\n// a、时间轮定义,自定义时间轮刻度、间隔等\\nTimeWheel timeWheel = new TimeWheel(60, 1000);\\n\\n// b、提交时间轮任务(定时任务)\\ntimeWheel.submitTask(System.currentTimeMillis() + 3000, () -> {\\n System.out.println(\\"Task delay \\" + waitTime + \\"ms executed at: \\" );\\n});\\n
\\nJWT工具,提供JWT生成及解析能力
\\n// a、JwtTool 初始化,自定义 Signer和 Verifier\\nJwtTool jwtTool = new JwtTool(SECRET); // 默认使用 MACSigner/MACVerifier,支持多构造方法自定义实现;\\n\\n// b、创建token\\nString token = jwtTool.createToken(\\n {用户标识},\\n {自定义声明数据,map形式},\\n {自定义过期时间}\\n );\\n \\n// c、验证token\\nboolean isValid = jwtTool.validateToken(token); \\n// d、获取claim\\nObject userId = jwtTool.getClaim(token, {自定义声明数据key});\\n// e、获取过期时间\\nDate expirationTime = jwtTool.getExpirationTime(token);\\n
\\nXXL-TOOL 是一个Java工具类库,致力于让Java开发更高效。包含 “集合、字符串、缓存、并发、Excel、Emoji、Response、Pipeline……” 等数十个模块。
\\n模块 | 说明 |
---|---|
Core模块 | 包含集合、缓存、日期……等基础组件工具。 |
Gson模块 | json序列化、反序列化工具封装,基于Gson。 |
Json模块 | json序列化、反序列化自研工具 。 |
Response模块 | 统一响应数据结构体,标准化数据结构、状态码等,降低协作成本。 |
Pipeline模块 | 高扩展性流程编排引擎。 |
Excel模块 | 一个灵活的Java对象和Excel文档相互转换的工具。一行代码完成Java对象和Excel之间的转换。 |
Emoji模块 | 一个灵活可扩展的Emoji表情编解码库,可快速实现Emoji表情的编解码。 |
Freemarker模块 | 模板引擎工具,支持根据模板文件实现 动态文本生成、静态文件生成 等,支持邮件发送、网页静态化场景。 |
IO模块 | 一系列处理IO(输入/输出)操作的工具。 |
Encrypt模块 | 一系列处理编解码、加解密的工具,包括 Md5Tool、HexTool、Base64Tool...等。 |
Http模块 | 一系列处理Http通讯、IP、Cookie等相关工具。 |
JsonRpc模块 | 一个轻量级、跨语言远程过程调用实现,基于json、http实现(对比传统RPC框架:XXL-RPC)。 |
Concurrent模块 | 一系列并发编程工具,具备良好的线程安全、高并发及高性能优势,包括CyclicThread(后台循环线程)、MessageQueue(高性能内存队列,30W+ TPS)、TimeWheel(时间轮组件)等。 |
Exception模块 | 异常处理相关工具。 |
Auth模块 | 一系列权限认证相关工具,包括JwtTool...等。 |
... | ... |
在 Spring Boot 开发中,框架内置的诸多实用功能犹如一把把利刃,能让开发者在项目的各个阶段都事半功倍。这些功能无需额外集成,通过简单配置或编码即可快速实现常见需求。下面将为你深入解析一系列极具价值的内置功能,帮助你更高效地构建应用。
\\n在调试和监控阶段,记录请求的完整信息是定位问题的关键。Spring Boot 提供的 CommonsRequestLoggingFilter
可轻松实现请求数据的详细日志记录。
多维度数据采集:支持记录请求参数(includeQueryString
)、请求体(includePayload
)、请求头(includeHeaders
)及客户端 IP 等信息。
灵活日志格式:通过 setAfterMessagePrefix
自定义日志前缀,方便日志分类与检索。
配置过滤器:
\\n@Configuration\\npublic class RequestLoggingConfig {\\n @Bean\\n public CommonsRequestLoggingFilter logFilter() {\\n CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();\\n filter.setIncludeQueryString(true); // 包含查询参数\\n filter.setIncludePayload(true); // 包含请求体\\n filter.setMaxPayloadLength(1024); // 限制请求体日志长度(避免大字段溢出)\\n filter.setAfterMessagePrefix(\\"[REQUEST DATA] \\");\\n return filter;\\n }\\n}\\n
\\n设置日志级别:在 application.properties
中开启 DEBUG 级日志:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG\\n
\\n[REQUEST DATA] POST /api/user, client=192.168.1.1, headers=[Content-Type:application/json], payload={\\"username\\":\\"test\\",\\"email\\":\\"test@example.com\\"}\\n
\\n原生 HttpServletRequest
和 HttpServletResponse
的输入输出流仅支持单次读取,这在需要多次处理数据(如日志记录与业务逻辑分离)时存在局限。Spring 提供的 ContentCachingRequestWrapper
和 ContentCachingResponseWrapper
完美解决了这一问题。
请求包装器(ContentCachingRequestWrapper) :缓存请求体字节数据,允许多次读取。典型场景:记录请求日志后,控制器仍能正常解析请求体。
\\n响应包装器(ContentCachingResponseWrapper) :缓存响应输出流,支持在响应提交前修改内容(如添加签名、动态拼接数据)。
\\n// 请求包装器过滤器\\n@Component\\npublic class RequestLogFilter extends OncePerRequestFilter {\\n @Override\\n protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)\\n throws ServletException, IOException {\\n // 包装请求,缓存输入流\\n ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);\\n byte[] requestBody = wrappedRequest.getContentAsByteArray();\\n \\n // 记录请求日志(可在此处添加自定义逻辑)\\n log.debug(\\"Received request body: {}\\", new String(requestBody));\\n \\n // 传递包装后的请求,确保后续组件能重复读取\\n filterChain.doFilter(wrappedRequest, response);\\n }\\n}\\n\\n// 响应包装器过滤器\\n@Component\\npublic class ResponseSignFilter extends OncePerRequestFilter {\\n @Override\\n protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)\\n throws ServletException, IOException {\\n // 包装响应,缓存输出流\\n ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);\\n \\n // 执行后续处理(控制器逻辑)\\n filterChain.doFilter(request, wrappedResponse);\\n \\n // 响应后处理:添加签名\\n byte[] responseBody = wrappedResponse.getContentAsByteArray();\\n String signature = generateSignature(responseBody);\\n wrappedResponse.setHeader(\\"X-Response-Signature\\", signature);\\n \\n // 必须调用此方法将缓存内容写入原始响应\\n wrappedResponse.copyBodyToResponse();\\n }\\n\\n private String generateSignature(byte[] body) {\\n // 自定义签名逻辑\\n return Base64.getEncoder().encodeToString(body);\\n }\\n}\\n
\\n在请求转发(forward)或包含(include)场景中,普通过滤器可能重复执行,导致逻辑混乱。OncePerRequestFilter
确保过滤器在请求生命周期内仅执行一次,是处理状态性逻辑的理想选择。
避免重复处理:通过 shouldNotFilter
方法内部逻辑,自动识别同一请求的多次调度,确保 doFilterInternal
仅执行一次。
简化开发:只需重写 doFilterInternal
方法,无需手动处理请求标识或缓存执行状态。
日志记录:避免转发时重复打印日志。
\\n安全校验:如 JWT 解析,确保身份验证仅执行一次。
\\n性能监控:精确记录单次请求处理耗时,避免统计误差。
\\nSpring AOP 的强大离不开三个辅助类,它们简化了代理对象操作与反射逻辑,是切面编程的得力助手。
\\n当同一类中方法调用导致注解(如 @Transactional
)失效时,AopContext.currentProxy()
可获取当前代理对象,确保切面逻辑正确触发:
public class ServiceImpl {\\n @Transactional\\n public void innerMethod() {\\n // 正常事务逻辑\\n }\\n\\n public void outerMethod() {\\n // 直接调用 innerMethod 会跳过代理,导致事务失效\\n // 正确方式:通过代理对象调用\\n ((ServiceImpl) AopContext.currentProxy()).innerMethod();\\n }\\n}\\n
\\n提供静态方法快速识别代理类型,便于动态处理不同代理逻辑:
\\nif (AopUtils.isJdkDynamicProxy(proxyObject)) {\\n // 处理 JDK 动态代理\\n} else if (AopUtils.isCglibProxy(proxyObject)) {\\n // 处理 CGLIB 代理\\n}\\n
\\n封装繁琐的反射 API,支持安全访问私有成员:
\\n// 访问私有字段\\nField field = ReflectionUtils.findField(MyClass.class, \\"privateField\\");\\nReflectionUtils.makeAccessible(field);\\nObject value = ReflectionUtils.getField(field, objectInstance);\\n\\n// 调用私有方法\\nMethod method = ReflectionUtils.findMethod(MyClass.class, \\"privateMethod\\", String.class);\\nReflectionUtils.invokeMethod(method, objectInstance, \\"参数\\");\\n
\\nSpring Boot 最显著的生产力提升工具之一是 Starter 依赖体系,通过命名规范清晰的 “一站式” 依赖包,开发者无需手动搜索和匹配兼容版本,框架自动处理传递依赖冲突。
\\n极简依赖声明:例如添加 Web 开发依赖只需引入 spring-boot-starter-web
,框架自动包含 Tomcat、Spring MVC、Jackson 等必需库。
版本统一管理:通过 spring-boot-dependencies
父 POM 锁定所有依赖的兼容版本,避免版本冲突。
功能场景 | Starter 坐标 | 自动包含核心组件 |
---|---|---|
Web 开发 | spring-boot-starter-web | Spring MVC、Tomcat、Jackson |
数据库连接 | spring-boot-starter-jdbc | JDBC 驱动、HikariCP 连接池 |
NoSQL(MongoDB) | spring-boot-starter-data-mongodb | MongoDB 驱动、Reactive 客户端(可选) |
安全认证 | spring-boot-starter-security | Spring Security 核心、OAuth2 支持(可选) |
异步消息 | spring-boot-starter-kafka | Kafka 客户端、消费者 / 生产者自动配置 |
若需封装内部通用功能,可创建自定义 Starter:
\\n在 src/main/resources/META-INF
下添加 spring.factories
:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\\\\ncom.example.custom.MyCustomAutoConfiguration\\n
\\n编写自动配置类,利用 @ConditionalOnClass
、@ConditionalOnMissingBean
等条件注解实现智能装配。
Spring Boot 通过 自动配置(Auto Configuration) 和 配置属性绑定(@ConfigurationProperties) 大幅减少样板代码,支持从 application.properties
/yml
中动态注入配置。
框架通过 @EnableAutoConfiguration
扫描类路径,根据依赖自动配置 Bean。例如:
引入 spring-boot-starter-jdbc
后,自动配置 DataSource
、JdbcTemplate
。
检测到 spring-data-mongodb
,自动配置 MongoDB 相关连接和操作组件。
通过 @ConfigurationProperties
将配置文件映射到 POJO,避免硬编码:
@Configuration\\n@ConfigurationProperties(prefix = \\"app\\")\\npublic class AppConfig {\\n private String env;\\n private DatabaseConfig database;\\n\\n // getter/setter\\n public static class DatabaseConfig {\\n private String url;\\n private String username;\\n // ...\\n }\\n}\\n
\\napplication.yml
配置:
app:\\n env: production\\n database:\\n url: jdbc:mysql://localhost:3306/test\\n username: root\\n
\\n支持在配置中使用 ${}
引用其他配置或系统变量,结合 @Value
注入:
@Value(\\"${app.env:dev}\\") // 默认值处理\\nprivate String environment;\\n
\\nSpring Boot 通过 @Async
和 @Scheduled
注解简化异步任务和定时任务开发,无需手动编写线程池或 Quartz 配置。
标注 @Async
的方法会在独立线程中执行,配合 @EnableAsync
启用:
@Service\\npublic class AsyncService {\\n @Async(\\"customExecutor\\") // 指定线程池\\n public CompletableFuture<Void> processAsyncTask(String taskId) {\\n // 耗时操作\\n return CompletableFuture.completedFuture(null);\\n }\\n}\\n\\n// 配置自定义线程池\\n@Configuration\\n@EnableAsync\\npublic class AsyncConfig {\\n @Bean(\\"customExecutor\\")\\n public Executor asyncExecutor() {\\n ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\\n executor.setCorePoolSize(5);\\n executor.setMaxPoolSize(10);\\n executor.setQueueCapacity(20);\\n executor.initialize();\\n return executor;\\n }\\n}\\n
\\n使用 @Scheduled
定义周期性任务,支持 Cron 表达式或固定间隔:
@Service\\npublic class ScheduledService {\\n // 每天凌晨 1 点执行\\n @Scheduled(cron = \\"0 0 1 * * ?\\") \\n public void dailyCleanup() {\\n // 数据清理逻辑\\n }\\n\\n // 每隔 5 秒执行(上一次完成后等待)\\n @Scheduled(fixedDelay = 5000) \\n public void periodicSync() {\\n // 同步任务\\n }\\n}\\n
\\nSpring Boot Actuator 提供开箱即用的端点(Endpoint),帮助开发者监控应用状态、查看性能指标,甚至动态调整配置。
\\n端点路径 | 功能描述 | 启用方式(application.yml) |
---|---|---|
/health | 健康检查(数据库连接、缓存状态等) | management.endpoints.web.exposure.include=health |
/metrics | 指标统计(内存、CPU、自定义指标) | 需引入 spring-boot-starter-actuator |
/env | 环境变量与配置属性展示 | 同上 |
/threaddump | 线程堆栈信息 | 生产环境需权限控制 |
/logfile | 日志文件内容(需配置 logging.file ) | 同上 |
通过 MeterRegistry
添加业务指标:
@Autowired\\nprivate MeterRegistry meterRegistry;\\n\\npublic void recordOrder(String status) {\\n meterRegistry.counter(\\"order.processed\\", \\"status\\", status).increment();\\n}\\n
\\nSpring 表达式语言(SpEL)允许在配置、注解甚至代码中动态求值,实现灵活的逻辑控制。
\\nBean 定义中的表达式:
\\n<bean id=\\"userService\\" class=\\"com.example.UserService\\">\\n <property name=\\"defaultTimeout\\" value=\\"#{T(java.lang.Integer).parseInt(\'1000\')}\\"/>\\n</bean>\\n
\\n条件注解中的逻辑:
\\n@ConditionalOnExpression(\\"${app.env} == \'prod\' && @environment.getProperty(\'server.port\') == 8080\\")\\npublic class ProdConfig {\\n // 生产环境专属配置\\n}\\n
\\n安全表达式(Spring Security) :
\\n@PreAuthorize(\\"hasRole(\'ADMIN\') or @accessService.hasPermission(#userId)\\")\\npublic void deleteUser(Long userId) {\\n // 权限控制逻辑\\n}\\n
\\nSpring Boot 的这些内置功能覆盖了从开发到运维的全链路流程,合理运用这些工具,既能减少重复代码,又能提升系统的可维护性与健壮性。建议开发者在实际项目中根据需求灵活组合,充分发挥框架的原生优势。
","description":"在 Spring Boot 开发中,框架内置的诸多实用功能犹如一把把利刃,能让开发者在项目的各个阶段都事半功倍。这些功能无需额外集成,通过简单配置或编码即可快速实现常见需求。下面将为你深入解析一系列极具价值的内置功能,帮助你更高效地构建应用。 一、请求数据全链路追踪:CommonsRequestLoggingFilter\\n\\n在调试和监控阶段,记录请求的完整信息是定位问题的关键。Spring Boot 提供的 CommonsRequestLoggingFilter 可轻松实现请求数据的详细日志记录。\\n\\n核心能力\\n\\n多维度数据采集:支持记录请求参数(inclu…","guid":"https://juejin.cn/post/7499508081651695667","author":"全栈智擎","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-03T23:16:06.441Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"招行2面:为什么需要序列化和反序列?为什么不能直接使用对象?","url":"https://juejin.cn/post/7499078331708932134","content":"Hi,你好,我是猿java。
\\n工作中,我们经常听到序列化
和反序列化
,那么,什么是序列化
?什么又是反序列化
?这篇文章,我们来分析一个招商的面试题:为什么需要序列化
和反序列化
?
简单来说,序列化
就是把一个Java对象转换成一系列字节的过程,这些字节可以被存储到文件、数据库,或者通过网络传输。反过来,反序列化
则是把这些字节重新转换成Java对象的过程。
想象一下,你有一个手机应用中的用户对象(比如用户的名字、年龄等信息)。如果你想将这个用户对象存储起来,或者发送给服务器,你就需要先序列化它。等到需要使用的时候,再通过反序列化把它恢复成原来的对象。
\\n“为什么需要序列化?为什么不能直接使用对象呢?”这确实是一个好问题,而且很多工作多年的程序员不一定能回答清楚。综合来看:需要序列化的主要原因有以下三点:
\\n更直白的说,序列化是为了实现持久化和网络传输,对象是应用层的东西,不同的语言(比如:java,go,python)创建的对象还不一样,实现持久化和网络传输的载体不认这些对象。
\\nJava中的序列化是通过实现java.io.Serializable
接口来实现的。这个接口是一个标记接口,意味着它本身没有任何方法,只是用来标记这个类的对象是可序列化的。
当你序列化一个对象时,Java会将对象的所有非瞬态(transient
)和非静态字段的值转换成字节流。这包括对象的基本数据类型、引用类型,甚至是继承自父类的字段。
Serializable
接口:你的类需要实现这个接口。ObjectOutputStream
:用于将对象转换成字节流。writeObject
方法:将对象写入输出流。反序列化的步骤大致相同,只不过是使用ObjectInputStream
和readObject
方法。
让我们通过一个简单的例子来看看实际操作是怎样的。
\\nimport java.io.Serializable;\\n\\npublic class User implements Serializable {\\n private static final long serialVersionUID = 1L; // 推荐定义序列化版本号\\n private String name;\\n private int age;\\n private transient String password; // transient字段不会被序列化\\n\\n public User(String name, int age, String password) {\\n this.name = name;\\n this.age = age;\\n this.password = password;\\n }\\n\\n // 省略getter和setter方法\\n\\n @Override\\n public String toString() {\\n return \\"User{name=\'\\" + name + \\"\', age=\\" + age + \\", password=\'\\" + password + \\"\'}\\";\\n }\\n}\\n
\\nimport java.io.FileOutputStream;\\nimport java.io.IOException;\\nimport java.io.ObjectOutputStream;\\n\\npublic class SerializeDemo {\\n public static void main(String[] args) {\\n User user = new User(\\"Alice\\", 30, \\"secret123\\");\\n\\n try (FileOutputStream fileOut = new FileOutputStream(\\"user.ser\\");\\n ObjectOutputStream out = new ObjectOutputStream(fileOut)) {\\n \\n out.writeObject(user);\\n System.out.println(\\"对象已序列化到 user.ser 文件中.\\");\\n } catch (IOException i) {\\n i.printStackTrace();\\n }\\n }\\n}\\n
\\n运行上述代码后,你会发现当前目录下生成了一个名为user.ser
的文件,这就是序列化后的字节流。
import java.io.FileInputStream;\\nimport java.io.IOException;\\nimport java.io.ObjectInputStream;\\n\\npublic class DeserializeDemo {\\n public static void main(String[] args) {\\n User user = null;\\n\\n try (FileInputStream fileIn = new FileInputStream(\\"user.ser\\");\\n ObjectInputStream in = new ObjectInputStream(fileIn)) {\\n \\n user = (User) in.readObject();\\n System.out.println(\\"反序列化后的对象: \\" + user);\\n } catch (IOException | ClassNotFoundException i) {\\n i.printStackTrace();\\n }\\n }\\n}\\n
\\n运行这段代码,你会看到输出:
\\n反序列化后的对象: User{name=\'Alice\', age=30, password=\'null\'}\\n
\\n注意到password
字段为空,这是因为它被声明为transient
,在序列化过程中被忽略了。
serialVersionUID
是序列化时用来验证版本兼容性的一个标识符。如果你不显式定义它,Java会根据类的结构自动生成。但为了避免类结构变化导致序列化失败,建议手动定义一个固定的值。
如果一个类的父类没有实现Serializable
接口,那么在序列化子类对象时,父类的字段不会被序列化。反序列化时,父类的构造函数会被调用初始化父类部分。
使用transient
关键字可以防止敏感信息被序列化,比如密码字段。此外,你也可以自定义序列化逻辑,通过实现writeObject
和readObject
方法来更精细地控制序列化过程。
本文,我们深入浅出地探讨了Java中的序列化和反序列化,从基本概念到原理分析,再到实际的代码示例,希望你对这两个重要的技术点有了更清晰的理解。
\\n为什么需要序列化和反序列化?
\\n最直白的说,如果不进行持久化和网络传输,根本不需要序列化和反序列化。如果需要实现持久化和网络传输,就必须序列化和反序列化,因为对象是应用层的东西,不同的语言(比如:java,go,python)创建的对象还不一样,实现持久化和网络传输的载体根本不认这些对象。
\\n最后,把猿哥的座右铭送给你:投资自己才是最大的财富。 如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
","description":"Hi,你好,我是猿java。 工作中,我们经常听到序列化和反序列化,那么,什么是序列化?什么又是反序列化?这篇文章,我们来分析一个招商的面试题:为什么需要序列化和反序列化?\\n\\n1. 什么是序列化和反序列化?\\n\\n简单来说,序列化就是把一个Java对象转换成一系列字节的过程,这些字节可以被存储到文件、数据库,或者通过网络传输。反过来,反序列化则是把这些字节重新转换成Java对象的过程。\\n\\n想象一下,你有一个手机应用中的用户对象(比如用户的名字、年龄等信息)。如果你想将这个用户对象存储起来,或者发送给服务器,你就需要先序列化它。等到需要使用的时候…","guid":"https://juejin.cn/post/7499078331708932134","author":"猿java","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-03T11:33:03.505Z","media":null,"categories":["后端","面试","架构"],"attachments":null,"extra":null,"language":null},{"title":"📨 Spring Boot 3 整合 MQ 构建聊天消息存储系统","url":"https://juejin.cn/post/7499332652014485556","content":"在构建实时聊天服务时,我们既要保证消息的即时传递,又需要对消息进行持久化存储以便查询历史记录。然而,直接同步写入数据库在高并发场景下容易成为性能瓶颈,影响消息的实时性。秉承\\"没有什么问题是加一层解决不了的\\"理念,引入消息队列(MQ)进行异步存储是一个优雅的解决方案。消息先快速写入MQ确保即时送达,随后由专门的消费者服务从队列取出,平稳写入数据库。
\\n在本文中,我们将详细探讨如何利用Spring Boot 3 结合消息队列技术,构建一个高效可靠的聊天消息存储系统。
\\nMQ在这里主要的作用是实现解耦,将聊天功能与聊天内容的存储过程分离。这种机制很像工厂与批发商之间的订货关系优化——传统模式下,工厂每次出货都需要逐一通知各个批发商。
\\n而引入MQ后,这一流程变得优雅高效,就像工厂只需在一个微信群里发布消息,所有批发商便能同时获取信息,无需一对一通知。工厂专注生产,批发商按需处理,两端各司其职。
\\n消息队列作为服务间通信的中间媒介,在分布式系统中扮演着至关重要的角色。常见的解决方案有专业的消息队列系统(如RabbitMQ、Kafka、RocketMQ等)、分布式协调服务Zookeeper,以及基于Redis实现的轻量级队列。
\\n在众多消息队列产品中,各有其特点和适用场景:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n消息队列 | 开发语言 | 特点 | 适用场景 |
---|---|---|---|
RabbitMQ | Erlang | 成熟稳定、易于部署、丰富的路由功能、社区活跃 | 复杂路由需求、中小规模消息量、需要可靠性保证 |
ActiveMQ | Java | 老牌MQ、JMS实现、资源消耗较高 | 传统企业应用、与Java生态紧密结合 |
RocketMQ | Java | 高吞吐、低延迟、金融级可靠性、支持大量堆积 | 大规模互联网应用、金融支付场景 |
Kafka | Scala/Java | 超高吞吐量、持久化、分区设计、擅长流处理 | 日志收集、大数据实时处理、流数据分析 |
ZeroMQ | C++ | 轻量级、无中心化、嵌入式库 | 对性能极为敏感的场景、点对点通信 |
Redis队列 | C | 轻量简单、基于内存、低延迟 | 简单场景、临时队列、对持久化要求不高 |
对于我们的聊天消息存储场景,最终选择了 RabbitMQ,主要基于以下考虑:
\\n虽然在极高并发场景下 Kafka 或 RocketMQ 可能有更好的吞吐性能,但考虑到我们这里重点在系统的解耦上,RabbitMQ 已经能够很好地满足需求,同时降低了开发和维护成本。
\\n消息队列在系统架构中有多种经典应用场景:
\\n异步处理:将耗时操作(如邮件发送、日志处理)交由消息队列异步处理,快速响应用户请求,提升体验。
\\n性能提升:通过异步解耦,减少系统响应时间,提高吞吐量,尤其适合I/O密集型操作。
\\n系统解耦:降低服务间直接依赖,提高系统弹性和可维护性,便于独立扩展和升级。
\\n削峰填谷:在流量高峰期,消息队列可缓存请求,按处理能力逐步消费,防止系统过载崩溃。
\\n在聊天消息存储场景中,我们主要利用RabbitMQ实现消息异步存储,既保证了聊天功能的响应速度,又能可靠地将消息持久化到数据库,同时为系统提供了应对消息高峰的能力。
\\n一条消息在RabbitMQ中的完整生命周期如下:
\\nRabbitMQ的安装可以通过多种方式进行,而Docker提供了最便捷的部署方案。以下是使用Docker快速部署RabbitMQ的步骤:
\\n首先从Docker Hub拉取RabbitMQ官方镜像,建议选择带management
标签的版本,它包含了Web管理界面,便于后续的可视化操作和监控:
docker pull rabbitmq:4.1-management\\n
\\n\\n\\n提示:各位读者在实操时可以访问Docker Hub查看并使用最新的版本
\\n
拉取镜像后,通过以下命令启动RabbitMQ容器:
\\ndocker run --name rabbitmq -p 5681:5671 -p 5682:5672 -p 4379:4369 -p 15681:15671 -p 15682:15672 -p 25682:25672 --restart always -d rabbitmq:4.1-management\\n
\\n这里我们做了以下映射和配置:
\\n启动成功后,在浏览器中访问http://127.0.0.1:15682
打开RabbitMQ管理控制台:
使用默认的用户名和密码登录(均为guest
):
登录成功后,您将看到RabbitMQ的管理界面,可以在这里创建交换机、队列、查看连接状态以及监控消息吞吐量等重要指标。
\\n\\n\\n注意:默认的guest用户只能从localhost访问,如需远程访问,建议创建新的管理员用户并设置适当的权限。
\\n
在开始之前,我们先创建消息表。本文的聊天服务基于之前的文章《Java 工程师进阶必备:Spring Boot 3 + Netty 构建高并发即时通讯服务》,感兴趣的读者可以自行查阅。
\\nDROP TABLE IF EXISTS `chat_message`;\\nCREATE TABLE `chat_message` (\\n `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,\\n `sender_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'发送者的用户id\',\\n `receiver_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'接受者的用户id\',\\n `receiver_type` int(11) NULL DEFAULT NULL COMMENT \'消息接受者的类型,可以作为扩展字段\',\\n `msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'聊天内容\',\\n `msg_type` int(11) NOT NULL COMMENT \'消息类型,有文字类、图片类、视频类...等,详见枚举类\',\\n `chat_time` datetime NOT NULL COMMENT \'消息的聊天时间,既是发送者的发送时间、又是接受者的接受时间\',\\n `show_msg_date_time_flag` int(11) NULL DEFAULT NULL COMMENT \'标记存储数据库,用于历史展示。每超过1分钟,则显示聊天时间,前端可以控制时间长短(扩展字段)\',\\n `video_path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT \'视频地址\',\\n `video_width` int(11) NULL DEFAULT NULL COMMENT \'视频宽度\',\\n `video_height` int(11) NULL DEFAULT NULL COMMENT \'视频高度\',\\n `video_times` int(11) NULL DEFAULT NULL COMMENT \'视频时间\',\\n `voice_path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT \'语音地址\',\\n `speak_voice_duration` int(11) NULL DEFAULT NULL COMMENT \'语音时长\',\\n `is_read` tinyint(1) NULL DEFAULT NULL COMMENT \'语音消息标记是否已读未读,true: 已读,false: 未读\',\\n PRIMARY KEY (`id`) USING BTREE\\n) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = \'聊天信息存储表\' ROW_FORMAT = Dynamic;\\n
\\n首先,在项目的 pom.xml
文件中添加 RabbitMQ 依赖:
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-amqp</artifactId>\\n</dependency>\\n
\\n在 application.yml
或 application.properties
文件中添加 RabbitMQ 的配置:
spring: \\n rabbitmq:\\n host: 127.0.0.1\\n port: 5682\\n username: guest\\n password: guest\\n virtual-host: /\\n
\\n创建一个消息发布者类,用于发送消息到 RabbitMQ:
\\nimport com.pitayafruits.pojo.netty.ChatMsg;\\nimport com.pitayafruits.utils.JsonUtils;\\n\\n\\npublic class MessagePublisher {\\n\\n // 定义交换机的名字\\n public static final String EXCHANGE = \\"pitayafruits_exchange\\";\\n\\n // 定义队列的名字\\n public static final String QUEUE = \\"pitayafruits_queue\\";\\n\\n // 发送信息到消息队列接受并且保存到数据库的路由地址\\n public static final String ROUTING_KEY_SEND = \\"pitayafruits.wechat.send\\";\\n\\n\\n public static void sendMsgToSave(ChatMsg msg) throws Exception {\\n RabbitMQConnectUtils connectUtils = new RabbitMQConnectUtils();\\n connectUtils.sendMsg(JsonUtils.objectToJson(msg),\\n EXCHANGE,\\n ROUTING_KEY_SEND);\\n }\\n \\n}\\n
\\nimport com.rabbitmq.client.*;\\n\\nimport java.util.ArrayList;\\nimport java.util.List;\\n\\npublic class RabbitMQConnectUtils {\\n\\n private final List<Connection> connections = new ArrayList<>();\\n private final int maxConnection = 20;\\n\\n // 开发环境 dev\\n private final String host = \\"127.0.0.1\\";\\n private final int port = 5682;\\n private final String username = \\"guest\\";\\n private final String password = \\"guest\\";\\n private final String virtualHost = \\"/\\";\\n\\n public ConnectionFactory factory;\\n\\n public ConnectionFactory getRabbitMqConnection() {\\n return getFactory();\\n }\\n\\n public ConnectionFactory getFactory() {\\n initFactory();\\n return factory;\\n }\\n\\n private void initFactory() {\\n try {\\n if (factory == null) {\\n factory = new ConnectionFactory();\\n factory.setHost(host);\\n factory.setPort(port);\\n factory.setUsername(username);\\n factory.setPassword(password);\\n factory.setVirtualHost(virtualHost);\\n }\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n\\n public void sendMsg(String message, String queue) throws Exception {\\n Connection connection = getConnection();\\n Channel channel = connection.createChannel();\\n channel.basicPublish(\\"\\",\\n queue,\\n MessageProperties.PERSISTENT_TEXT_PLAIN,\\n message.getBytes(\\"utf-8\\"));\\n channel.close();\\n setConnection(connection);\\n }\\n\\n public void sendMsg(String message, String exchange, String routingKey) throws Exception {\\n Connection connection = getConnection();\\n Channel channel = connection.createChannel();\\n channel.basicPublish(exchange,\\n routingKey,\\n MessageProperties.PERSISTENT_TEXT_PLAIN,\\n message.getBytes(\\"utf-8\\"));\\n channel.close();\\n setConnection(connection);\\n }\\n\\n public GetResponse basicGet(String queue, boolean autoAck) throws Exception {\\n GetResponse getResponse = null;\\n Connection connection = getConnection();\\n Channel channel = connection.createChannel();\\n getResponse = channel.basicGet(queue, autoAck);\\n channel.close();\\n setConnection(connection);\\n return getResponse;\\n }\\n\\n public Connection getConnection() throws Exception {\\n return getAndSetConnection(true, null);\\n }\\n\\n public void setConnection(Connection connection) throws Exception {\\n getAndSetConnection(false, connection);\\n }\\n\\n private synchronized Connection getAndSetConnection(boolean isGet, Connection connection) throws Exception {\\n getRabbitMqConnection();\\n\\n if (isGet) {\\n if (connections.isEmpty()) {\\n return factory.newConnection();\\n }\\n Connection newConnection = connections.get(0);\\n connections.remove(0);\\n if (newConnection.isOpen()) {\\n return newConnection;\\n } else {\\n return factory.newConnection();\\n }\\n } else {\\n if (connections.size() < maxConnection) {\\n connections.add(connection);\\n }\\n return null;\\n }\\n }\\n\\n}\\n
\\n创建一个消息消费者类,用于接收并处理消息:
\\nimport com.pitayafruits.pojo.netty.ChatMsg;\\nimport com.pitayafruits.service.ChatMessageService;\\nimport com.pitayafruits.utils.JsonUtils;\\nimport jakarta.annotation.Resource;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springframework.amqp.core.Message;\\nimport org.springframework.amqp.rabbit.annotation.RabbitListener;\\nimport org.springframework.stereotype.Component;\\n\\n/**\\n * @Auther 风间影月\\n */\\n@Component\\n@Slf4j\\npublic class RabbitMQConsumer {\\n\\n @Resource\\n private ChatMessageService chatMessageService;\\n\\n @RabbitListener(queues = {RabbitMQConfig.QUEUE})\\n public void watchQueue(String payload, Message message) {\\n String routingKey = message.getMessageProperties().getReceivedRoutingKey();\\n log.info(\\"routingKey = \\" + routingKey);\\n\\n if (routingKey.equals(RabbitMQConfig.ROUTING_KEY_SEND)) {\\n String msg = payload;\\n ChatMsg chatMsg = JsonUtils.jsonToPojo(msg, ChatMsg.class);\\n\\n chatMessageService.saveMsg(chatMsg);\\n }\\n\\n }\\n
\\n通过 Spring Boot 整合 RabbitMQ,我们实现了消息的异步处理机制,将聊天消息的存储操作解耦,提高了系统的性能和可扩展性。当用户发送消息时,我们将消息发送到 RabbitMQ,然后由消费者异步处理并保存到数据库中,避免了直接操作数据库导致的性能瓶颈。
","description":"引子 在构建实时聊天服务时,我们既要保证消息的即时传递,又需要对消息进行持久化存储以便查询历史记录。然而,直接同步写入数据库在高并发场景下容易成为性能瓶颈,影响消息的实时性。秉承\\"没有什么问题是加一层解决不了的\\"理念,引入消息队列(MQ)进行异步存储是一个优雅的解决方案。消息先快速写入MQ确保即时送达,随后由专门的消费者服务从队列取出,平稳写入数据库。\\n\\n在本文中,我们将详细探讨如何利用Spring Boot 3 结合消息队列技术,构建一个高效可靠的聊天消息存储系统。\\n\\n关于MQ\\n\\nMQ在这里主要的作用是实现解耦,将聊天功能与聊天内容的存储过程分离…","guid":"https://juejin.cn/post/7499332652014485556","author":"别惹CC","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-03T10:23:48.643Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e52ebf4da5964bcb9bd2ae1c5767a3f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=sT%2BRvP70Q0Q50YXO9%2Bv3FMFLeK8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a72088967c104050a95c16f716e08a88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=wOhzgNymtiiwpzkb0Gw05wFRgyM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/44dbc67910d44bc19f48ea11851f4dd2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=bqbpCRIelcfSGtylcTdzQAD5FkM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fce6b3e00708414bbb3a76e90682ef41~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=QmDSkLKp4%2BPXysuLyZTyH%2FNg1p0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/782c8115e9f841e5bf0a10263f25f09a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=UkFtWyCgnuIXQijAujWo5hy5ndg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f97ae1860295403f856a8c3704ecf01b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1746872628&x-signature=LMMkGZHjI2EkZ8BieW%2BdljPh7qc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Spring Boot","RabbitMQ"],"attachments":null,"extra":null,"language":null},{"title":"别再重复造轮子!SpringBoot 内置的 20个高效工具类","url":"https://juejin.cn/post/7499089664567672858","content":"想获取更多高质量的Java技术文章?欢迎访问 Java技术小馆官网,持续更新优质内容,助力技术成长!
\\n还在自己写工具类?你已经被SpringBoot抛弃了!很多人嘴上说“别重复造轮子”,结果还在项目里一遍遍写String工具、Bean拷贝、文件处理、反射操作……
\\n
\\nSpringBoot 内置了 20个宝藏工具类,轻量级、性能强、低耦合,很多大厂架构师都在用,可你可能连名字都没听过!
有人说:“用这些就像开了外挂。”
\\n也有人说:“太黑箱,宁愿自己写。”
\\n你怎么看?
StringUtils
Spring 提供的 StringUtils
相比 Apache Commons Lang 版本更加轻量,且与 Spring 生态完美集成。它提供了字符串判空、截取、拼接等常用操作,比如:
// 判断字符串是否为空\\nStringUtils.isEmpty(str); \\n\\n// 判断字符串是否有内容\\nStringUtils.hasText(str);\\n\\n// 数组合并成字符串\\nStringUtils.arrayToCommaDelimitedString(arr);\\n
\\nObjectUtils
处理对象判空和默认值时,不要再写 if-else 了:
\\n// 安全判空\\nObjectUtils.isEmpty(obj);\\n\\n// 获取第一个非空对象\\nObjectUtils.firstNonNull(obj1, obj2, defaultValue);\\n\\n// 空安全toString\\nObjectUtils.nullSafeToString(obj);\\n
\\nCollectionUtils
Spring 的集合工具类让集合操作更加优雅:
\\n// 集合判空\\nCollectionUtils.isEmpty(collection);\\n\\n// 查找第一个匹配元素\\nCollectionUtils.find(collection, predicate);\\n\\n// 合并数组\\nCollectionUtils.mergeArrayIntoCollection(arr, collection);\\n
\\nRestTemplate
传统同步请求虽然已被标记为过时,但在非响应式项目中依然实用:
\\n// 简单GET请求\\nString result = restTemplate.getForObject(url, String.class);\\n\\n// 带参数的POST请求\\nResponseEntity<String> response = restTemplate.postForEntity(\\n url, request, String.class);\\n
\\nWebClient
Spring 5 引入的响应式 HTTP 客户端:
\\nWebClient.create()\\n .get()\\n .uri(url)\\n .retrieve()\\n .bodyToMono(String.class)\\n .subscribe(result -> System.out.println(result));\\n
\\nTestTemplate
专门为测试设计的增强版 RestTemplate:
\\n@Test\\npublic void testApi() {\\n ResponseEntity<String> response = testRestTemplate\\n .getForEntity(\\"/api/test\\", String.class);\\n assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\\n}\\n
\\nMockRestServiceServer
测试时模拟外部 API 调用:
\\nMockRestServiceServer server = MockRestServiceServer\\n .bindTo(restTemplate).build();\\n\\nserver.expect(requestTo(\\"/external/api\\"))\\n .andRespond(withSuccess(\\"mock response\\", MediaType.APPLICATION_JSON));\\n
\\nCacheManager
Spring 的缓存抽象层支持多种实现:
\\n@Cacheable(value = \\"users\\", key = \\"#id\\")\\npublic User getUser(Long id) {\\n // 数据库查询\\n}\\n\\n@CacheEvict(value = \\"users\\", key = \\"#id\\")\\npublic void updateUser(User user) {\\n // 更新逻辑\\n}\\n
\\n@Async
+ TaskExecutor
轻松实现方法异步执行:
\\n@Async\\npublic CompletableFuture<User> asyncGetUser(Long id) {\\n // 耗时操作\\n return CompletableFuture.completedFuture(user);\\n}\\n\\n// 配置线程池\\n@Bean\\npublic TaskExecutor taskExecutor() {\\n ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\\n executor.setCorePoolSize(5);\\n return executor;\\n}\\n
\\nEventPublishe
实现应用内事件发布订阅:
\\n// 定义事件\\npublic class UserRegisteredEvent extends ApplicationEvent {\\n public UserRegisteredEvent(User user) {\\n super(user);\\n }\\n}\\n\\n// 发布事件\\napplicationEventPublisher.publishEvent(new UserRegisteredEvent(user));\\n\\n// 监听事件\\n@EventListener\\npublic void handleEvent(UserRegisteredEvent event) {\\n // 处理逻辑\\n}\\n
\\nAssert
Spring 的断言工具让参数检查更简洁:
\\n// 参数校验\\nAssert.notNull(param, \\"参数不能为空\\");\\nAssert.isTrue(value > 0, \\"值必须大于0\\");\\n\\n// 状态检查\\nAssert.state(isValid, \\"状态不合法\\");\\n
\\n@Validated
+ BindingResult
结合 JSR-303 实现参数校验:
\\n@PostMapping(\\"/users\\")\\npublic ResponseEntity createUser(\\n @Validated @RequestBody User user,\\n BindingResult result) {\\n \\n if (result.hasErrors()) {\\n // 处理校验错误\\n }\\n // 业务逻辑\\n}\\n
\\nLogback
/Log4j2
SpringBoot 自动配置的日志系统:
\\n# application.properties\\nlogging.level.root=INFO\\nlogging.level.com.example=DEBUG\\nlogging.file.name=app.log\\nlogging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n\\n
\\nMockMvc
测试 Controller 层的利器:
\\n@SpringBootTest\\n@AutoConfigureMockMvc\\nclass UserControllerTest {\\n\\n @Autowired\\n private MockMvc mockMvc;\\n\\n @Test\\n void testGetUser() throws Exception {\\n mockMvc.perform(get(\\"/users/1\\"))\\n .andExpect(status().isOk())\\n .andExpect(jsonPath(\\"$.name\\").value(\\"张三\\"));\\n }\\n}\\n
\\nOutputCapture
捕获并验证日志输出:
\\n@SpringBootTest\\nclass LoggingTest {\\n\\n @Autowired\\n private MyService service;\\n\\n @Rule\\n public OutputCapture outputCapture = new OutputCapture();\\n\\n @Test\\n void testLogging() {\\n service.doSomething();\\n assertThat(outputCapture.toString())\\n .contains(\\"操作已完成\\");\\n }\\n}\\n
\\nTestPropertyValues
灵活修改测试环境配置:
\\n@Test\\nvoid testWithDynamicProperties() {\\n TestPropertyValues.of(\\n \\"app.timeout=5000\\",\\n \\"app.enabled=true\\"\\n ).applyTo(environment);\\n \\n // 执行测试\\n}\\n
\\nSpringBootTest
完整的集成测试支持:
\\n@SpringBootTest(\\n webEnvironment = WebEnvironment.RANDOM_PORT,\\n properties = {\\"app.env=test\\"}\\n)\\nclass FullIntegrationTest {\\n\\n @LocalServerPort\\n private int port;\\n\\n @Test\\n void testFullStack() {\\n // 测试完整应用栈\\n }\\n}\\n
\\nBannerCustomizer
让应用启动更有个性:
\\n@Bean\\npublic BannerCustomizer myBannerCustomizer() {\\n return banner -> {\\n banner.setBanner(new ResourceBanner(\\n new ClassPathResource(\\"banner.txt\\")));\\n banner.setMode(Banner.Mode.LOG);\\n };\\n}\\n
\\nEnvironment
灵活访问环境变量和配置:
\\n@Autowired\\nprivate Environment env;\\n\\npublic void someMethod() {\\n String dbUrl = env.getProperty(\\"spring.datasource.url\\");\\n boolean debug = env.getProperty(\\"app.debug\\", Boolean.class, false);\\n}\\n
\\nSpelExpressionParser
运行时执行 SpEL 表达式:
\\nExpressionParser parser = new SpelExpressionParser();\\nExpression exp = parser.parseExpression(\\"name.toUpperCase()\\");\\n\\nString result = exp.getValue(userContext, String.class);\\n
","description":"想获取更多高质量的Java技术文章?欢迎访问 Java技术小馆官网,持续更新优质内容,助力技术成长! SpringBoot 内置的 20 个高效工具类\\n\\n还在自己写工具类?你已经被SpringBoot抛弃了!很多人嘴上说“别重复造轮子”,结果还在项目里一遍遍写String工具、Bean拷贝、文件处理、反射操作……\\n\\n\\n SpringBoot 内置了 20个宝藏工具类,轻量级、性能强、低耦合,很多大厂架构师都在用,可你可能连名字都没听过!\\n\\n有人说:“用这些就像开了外挂。”\\n 也有人说:“太黑箱,宁愿自己写。”\\n 你怎么看?\\n\\n一、数据处理\\n1. StringUtils…","guid":"https://juejin.cn/post/7499089664567672858","author":"Java技术小馆","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-03T06:23:11.375Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/031543843c0a4d3ebf5d94d519b161ba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeaKgOacr-Wwj-mmhg==:q75.awebp?rk3s=f64ab15b&x-expires=1746858191&x-signature=ltqcg4a3ZnveRe85OQeg1ukVNEE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"阿里面试:千万级大表如何快速删除大量数据","url":"https://juejin.cn/post/7499021253459656742","content":"大家好,我是田螺。
\\n分享一道阿里面试真题:千万级大表如何快速删除大量数据。
\\n我们如何更好回答这个问题呢?如果是我来回答的话,我会按照这些思路来跟面试官阐述:
\\n千万级的大表,如果一次性删除所有数据,可能会导致数据库锁表、日志膨胀爆炸、CPU飙升、主从延迟等问题。
\\n\\n\\n删除1000万条数据耗时2小时 → 期间用户无法下单或查询。
\\n
\\n\\n删除1亿条数据,日志可能增长到500GB → 磁盘直接写满。
\\n
\\n\\n删除期间数据库CPU飙升至100%,正常查询延迟可能从2ms升到10秒。
\\n
\\n\\n比如主库删除耗时2小时 → 从库延迟3小时,期间报表数据错误。
\\n
\\n\\n删除5000万条数据1小时后中断 → 回滚需要2小时,业务停摆更久。
\\n
删除前,我们可能要有一些预演、确认的东西。比如评估要删除的数据量、对应的方案确认、删除条件是否加索引、数据备份等等
\\n在删除前,我们要评估一下,要删除的数据量是多少。因为不同的数据量,要选定的删除方案其实是不一样的。
\\n比如:
\\n要删除前,我们要确认删除条件是否有索引。删除条件的优化非常重要。你可以确保删除操作是基于索引来执行的,从而加速查找和删除过程。
\\n在进行大规模删除操作前,确保你有数据的备份。大表删除不可避免地会有数据丢失的风险,备份可以帮助你恢复数据。
\\n田螺哥给点小建议:
\\n为什么用分批思想呢?
\\n\\n\\n打个比喻:假如你需要搬一万块砖到楼顶,你有一个电梯,电梯一次可以放适量的砖(最多放500),你可以选择一次运送一块砖,也可以一次运送500,你觉得哪个时间消耗大?
\\n
分批删除:避免单次事务过大。
\\ndelete from tianluo_tab where 条件 LIMIT 1000; -- 每次删1000条\\n
\\n循环执行直到删完,每批后加短暂停顿(如0.1秒)。
\\n我们还可以关闭自动提交:减少事务开销。
\\nSET autocommit=0; -- 手动控制事务\\n-- 执行删除...\\nCOMMIT;\\n
\\n如果是经常需要删除大量数据的场景,考虑将表设计为分区表。这样,你可以通过删除分区来大大减少操作时间,而无需逐行删除数据。
\\n\\n\\n比如:数据按时间或范围分区(如日志表)。
\\n
-- 直接删除整个分区(秒级完成)\\nALTER TABLE table DROP PARTITION partition_name;\\n
\\n如果删除超过50%的数据,或需要保留的数据较少。可以使用创建新表并删除旧表的方式。
\\n步骤如下:
\\nCREATE TABLE new_table AS \\nSELECT * FROM old_table WHERE 保留条件;\\n
\\nRENAME TABLE old_table TO old_table_backup, new_table TO old_table;\\n
\\nDROP TABLE old_table_backup;\\n
\\n如果我们要删除整个表数据,TRUNCATE 通常比 DELETE 更高效,因为它不会逐行删除数据,而是直接释放表的空间。
\\n-- 高效清空整个表(保留表结构)\\nTRUNCATE TABLE tianluo_tab;\\n
\\n删除之后,我们还要有一些后置处理。比如数据验证、资源释放(清理物理空间)、监控与日志
\\n-- 确认目标数据已删除(如按时间条件删除)\\nSELECT COUNT(*) FROM tianluo_tab WHERE create_time < \'2025-05-02\';\\n-- 结果应为0,否则说明有残留\\n
\\n\\n\\n随机抽查未删除的数据,确保未误删有效数据(如 WHERE status=\'正常\' 的记录)。
\\n
\\n\\n检查依赖该表的业务功能是否正常(如报表、API接口)。比如:订单查询页是否因数据缺失报错?
\\n
\\n\\n\\n
\\n- 观察删除后的CPU、内存、I/O是否回归正常水平。
\\n- 检查慢查询日志,确认无因索引失效导致的性能问题。
\\n
\\n\\n\\n
\\n- 记录删除的时间、数据量、操作人,便于审计和追溯。
\\n
回收磁盘空间(某些数据库不会自动释放):
\\n-- MySQL(InnoDB)\\nOPTIMIZE TABLE tianluo_tab; -- 重建表并释放空间(谨慎使用,会锁表!)\\n-- PostgreSQL\\nVACUUM FULL tianluo_tab;\\n
\\n如果觉得本文对你有帮助的,麻烦给个三连支持一下哈~
\\n\\n\\n坚持原创不容易,这篇是我今早写的,写了一个多小时吧。大家有兴趣,支持一下我的付费内容哈,支持一下坚持六年的原创博主,很多干货~(面试技巧专栏、踩坑专栏、知识星球辅导面试)
\\n
122
\\n\\n\\n我的八股文面试技巧专栏已经更新38篇啦,篇篇经典,主要是教大家如何回答更全面,面试找工作的小伙伴可以购买,29.9永久买断(扫描下图二维码购买,后面在新语小程序就可以看了哈)。
\\n
123
\\n\\n","description":"前言 大家好,我是田螺。\\n\\n分享一道阿里面试真题:千万级大表如何快速删除大量数据。\\n\\n我们如何更好回答这个问题呢?如果是我来回答的话,我会按照这些思路来跟面试官阐述:\\n\\n直接一次性删除大量数据,可能存在哪些问题?\\n删除前预演:评估数据量、方案确认、删除条件是否加索引\\n删除大批量数据的常见方案\\n删除后,一些后置处理\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\n1. 一次性直接删除大量数据,可能存在哪些问题?\\n\\n千万级的大表,如果一次性删除所有数据,可能会导致数据库锁表、日志膨胀爆炸、CPU飙升…","guid":"https://juejin.cn/post/7499021253459656742","author":"捡田螺的小男孩","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-02T16:08:45.259Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"你真的会用ThreadLocal吗——使用篇","url":"https://juejin.cn/post/7498970712473600041","content":"我的代码踩坑专栏挺不错的。平时我晚上下班和周末都在总结代码踩坑点,花了很多心血,都是我工作遇到或者从一些前辈那里请教来的踩坑点。很实用的。目前已经更新到92篇啦,今天刚更新一篇添加数据库表字段可能踩的坑,很多伙伴可能都踩过的坑~~
\\n
记得那是一个上线前的深夜,大家都在紧张地进行最后的集成测试。突然,测试环境的某个核心服务开始疯狂报警,错误日志刷得飞起 🚀。
\\n错误信息很诡异,大概意思是用户A的操作数据莫名其妙地串到了用户B的请求里。这可是个大事故啊!所有人都被叫了起来,包括正在梦里撸猫的我 😴。
\\n我们几个老鸟围着日志和代码,排查了半天。数据隔离问题?事务问题?缓存问题?各种猜测满天飞。
\\n最后,目光聚焦在了一个不起眼的工具类上,里面用到了 ThreadLocal
来传递用户信息。代码看起来没啥毛病,用户信息 set
进去,后续链路也能 get
到。但问题是,我们的服务是基于Tomcat线程池的,线程是会被复用的啊!
经过一番紧张的Debug和分析,真相大白:开发这个工具类的同学,在请求处理结束时,忘了调用 ThreadLocal
的 remove()
方法!😱 这就导致了线程被回收到池中,下一次请求复用这个线程时,竟然取到了上一个请求残留的用户信息!
所有人恍然大悟,问题解决,虚惊一场。
\\n这次经历让我意识到,ThreadLocal
这个看似简单的工具,很多人可能只是“会用”,但并没有真正“用对”。它的坑,踩下去也是挺疼的。
所以,我决定写两篇文章,跟大家彻底聊聊 ThreadLocal
。
这一篇,我们先聚焦“怎么用”,由浅入深,把它的使用场景和注意事项掰扯清楚。至于它背后的原理,为什么会内存泄漏,ThreadLocalMap
是个啥?咱们留到下一篇《原理篇》再细说,先卖个关子 😉。
耐心看完,你一定有所收获。
\\n简单来说,ThreadLocal
提供了一种线程(Thread)级别的局部(Local)变量。
它最大的特点是:为每个使用该变量的线程都提供一个独立的变量副本。
\\n啥意思呢?
\\n就是说,你创建了一个 ThreadLocal
变量,比如 threadLocalUser
,然后线程A通过 threadLocalUser.set(\\"User A\\")
设置了值,线程B通过 threadLocalUser.set(\\"User B\\")
设置了值。那么,在线程A内部,任何时候通过 threadLocalUser.get()
获取到的都是 “User A”,而在线程B内部,获取到的永远是 “User B”。
它们俩互不干扰,就像每个线程都有自己的“小金库”💰,存取都在自己的空间里。
\\n这玩意儿主要用来解决什么问题呢?
\\n线程安全问题
\\n当多个线程需要共享某个非线程安全的对象时,一种常见的做法是加锁(synchronized
或 Lock
)。
但加锁会带来性能开销和死锁风险。
\\nThreadLocal
提供了一种“空间换时间”的思路,给每个线程一个独立副本,避免了线程间的竞争,自然也就线程安全了,而且通常比锁更快。
线程上下文传递
\\n在一个请求处理链路中(比如Web应用),很多时候我们需要在不同的方法、不同的类之间传递一些公共信息(比如当前登录用户、事务ID、Trace ID等)。
\\n如果一层层通过方法参数传递,代码会变得非常臃肿难看。
\\nThreadLocal
可以把这些信息存起来,链路中的任何地方都能方便地获取,代码更优雅。✨
ThreadLocal
的核心API非常简单,记住这三个就差不多了:
void set(T value)
: 将当前线程的此线程局部变量的副本设置为指定值。T get()
: 返回当前线程的此线程局部变量的副本中的值。如果这是线程第一次调用该方法,则会通过调用 initialValue()
方法来初始化值(除非之前调用过 set
)。void remove()
: (敲黑板,划重点!🚨) 移除此线程局部变量的当前线程值。我们来看个简单的例子:
\\npublic class ThreadLocalDemo {\\n\\n // 1. 创建一个 ThreadLocal 变量\\n private static final ThreadLocal<String> userContext = new ThreadLocal<>();\\n\\n public static void main(String[] args) {\\n // 线程 A\\n new Thread(() -> {\\n String userName = \\"酷炫张三\\";\\n // 2. 设置值\\n userContext.set(userName);\\n System.out.println(\\"Thread A set user: \\" + userName);\\n\\n try {\\n // 模拟业务处理\\n Thread.sleep(100);\\n processUserData(); // 在其他方法中获取\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n } finally {\\n // 4. (关键!)移除值\\n System.out.println(\\"Thread A removing user: \\" + userContext.get());\\n userContext.remove();\\n }\\n }, \\"Thread-A\\").start();\\n\\n // 线程 B\\n new Thread(() -> {\\n String userName = \\"低调李四\\";\\n // 2. 设置值\\n userContext.set(userName);\\n System.out.println(\\"Thread B set user: \\" + userName);\\n\\n try {\\n // 模拟业务处理\\n Thread.sleep(50);\\n processUserData(); // 在其他方法中获取\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n } finally {\\n // 4. (关键!)移除值\\n System.out.println(\\"Thread B removing user: \\" + userContext.get());\\n userContext.remove();\\n }\\n }, \\"Thread-B\\").start();\\n }\\n\\n private static void processUserData() {\\n // 3. 获取值\\n String currentUser = userContext.get();\\n System.out.println(Thread.currentThread().getName() + \\" processing data for user: \\" + currentUser);\\n // ... 其他业务逻辑 ...\\n }\\n}\\n\\n
\\n运行这段代码,你会看到线程A和线程B各自打印自己的用户信息,互不干扰。✅
\\n是不是使用起来很简单?
\\nremove()
remove()
!别忘了 remove()
!别忘了 remove()
!重要的事情说三遍!为啥 remove()
这么重要?
还记得开头那个月黑风高的故事吗?问题就出在忘了 remove()
。
在现代Java应用中,尤其是Web服务器(如Tomcat)和各种框架(如Spring),大量使用线程池来处理请求和任务。
\\n而线程池里的线程是会被复用的。
\\n看一下这个流程:
\\nThreadLocal
里设置了用户A的信息。remove()
。ThreadLocalMap
(这是ThreadLocal
存数据的地方,原理篇细讲)还持有用户A信息的引用。ThreadLocal
获取用户信息 get()
。糟糕!它取到了上次请求A留下的用户A的信息! 🤯 数据就串了!ThreadLocal
存的是比较大的对象,并且不断有新的请求进来,线程不断被复用且不 remove()
,这些“残留”的对象会一直存在于线程的 ThreadLocalMap
中,无法被GC回收,最终可能导致内存泄漏 ,把你的服务器内存撑爆!💥所以,最佳实践是: 在使用 ThreadLocal
的代码块(通常是 try-finally
结构)的 finally
中,一定、必须、务必调用 remove()
方法,确保线程执行完毕后清理掉 ThreadLocal
变量。
比如下面这个示例:
\\n\\nThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();\\n\\npublic void handleRequest(Request req) {\\n UserInfo userInfo = getUserInfoFromRequest(req);\\n userInfoThreadLocal.set(userInfo);\\n try {\\n // ... 执行业务逻辑,中间可能会调用N多方法 ...\\n serviceA();\\n serviceB();\\n // ... 这些方法内部可以通过 userInfoThreadLocal.get() 获取用户信息 ...\\n } finally {\\n // 无论业务逻辑是否异常,都要清理!\\n userInfoThreadLocal.remove(); // <-- 千万别忘了这一步!\\n System.out.println(\\"ThreadLocal for user \\" + userInfo.getId() + \\" removed.\\");\\n }\\n}\\n\\n
\\ninitialValue()
和 withInitial()
有时候,我们希望 ThreadLocal
在第一次 get()
并且没有 set()
过的时候,能返回一个默认值,而不是 null
。可以通过重写 initialValue()
方法或者使用 ThreadLocal.withInitial()
工厂方法来实现。
方式一:重写 initialValue()
private static final ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {\\n @Override\\n protected Integer initialValue() {\\n System.out.println(Thread.currentThread().getName() + \\" initializing counter to 0\\");\\n return 0; // 初始值为 0\\n }\\n};\\n\\npublic static void main(String[] args) {\\n new Thread(() -> {\\n System.out.println(Thread.currentThread().getName() + \\" initial get: \\" + counter.get()); // 首次get,会调用initialValue\\n counter.set(counter.get() + 1);\\n System.out.println(Thread.currentThread().getName() + \\" after increment: \\" + counter.get());\\n counter.remove();\\n }, \\"Thread-C\\").start();\\n\\n new Thread(() -> {\\n System.out.println(Thread.currentThread().getName() + \\" initial get: \\" + counter.get()); // 另一个线程首次get,也会调用initialValue\\n counter.set(counter.get() + 5);\\n System.out.println(Thread.currentThread().getName() + \\" after increment: \\" + counter.get());\\n counter.remove();\\n }, \\"Thread-D\\").start();\\n}\\n\\n
\\n方式二:使用 withInitial()
👍 这种方式更简洁,推荐使用!
\\nprivate static final ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> {\\n System.out.println(Thread.currentThread().getName() + \\" initializing counter to 0 via withInitial\\");\\n return 0; // 使用 Lambda 表达式提供初始值\\n});\\n
\\n注意:
\\ninitialValue()
或 withInitial()
提供的初始值,只会在当前线程第一次调用 get()
且没有调用过 set()
时被设置。set()
,再调用 get()
就会返回 set
的值。remove()
之后再调用 get()
,则会重新触发初始化逻辑。InheritableThreadLocal
父子线程传递还有一个 ThreadLocal
的亲戚叫 InheritableThreadLocal
。
它的特殊之处在于:当父线程创建一个子线程时,子线程会自动继承父线程中 InheritableThreadLocal
变量的值。
直接看代码:
\\n// 使用 InheritableThreadLocal\\nprivate static final ThreadLocal<String> inheritableContext = new InheritableThreadLocal<>();\\n\\npublic static void main(String[] args) {\\n inheritableContext.set(\\"Value from Main Thread\\");\\n System.out.println(\\"Main thread value: \\" + inheritableContext.get());\\n\\n new Thread(() -> {\\n // 子线程可以获取到父线程设置的值\\n System.out.println(\\"Child thread inherited value: \\" + inheritableContext.get());\\n\\n // 子线程修改值,不影响父线程\\n inheritableContext.set(\\"Value modified by Child Thread\\");\\n System.out.println(\\"Child thread modified value: \\" + inheritableContext.get());\\n\\n // 同样需要 remove\\n inheritableContext.remove();\\n }).start();\\n\\n // 等待子线程执行完毕\\n try {\\n Thread.sleep(100);\\n } catch (InterruptedException e) {}\\n\\n // 父线程的值不受子线程修改影响\\n System.out.println(\\"Main thread value after child finished: \\" + inheritableContext.get());\\n inheritableContext.remove();\\n}\\n\\n
\\n注意点:
\\nInheritableThreadLocal
的值互不影响。InheritableThreadLocal
在线程池中使用时要特别小心!因为线程复用,父线程设置的值可能会被“意外”带到后续不相关的任务中。如果父任务创建了子任务并提交到线程池,这种继承关系可能会导致混乱和内存泄漏。阿里巴巴的 transmittable-thread-local
(TTL) 库就是为了解决这个问题而生的,感兴趣可以去了解下。所以除非你非常明确知道需要父子线程传递数据,并且清楚其潜在风险,否则优先使用普通的 ThreadLocal
,并不简易直接使用 InheritableThreadLocal
Web 应用中的用户身份传递
\\n在 Filter 或 Interceptor 中获取用户信息,set
到 ThreadLocal
,后续 Controller、Service 层都可以方便地 get
到。
请求结束时在 Filter 或 Interceptor 的 finally
块中 remove
。
事务管理
\\nSpring 框架广泛使用了 ThreadLocal
来管理事务状态(TransactionSynchronizationManager
)。
每个线程持有自己的事务信息(是否开启事务、隔离级别、是否只读等)。
\\n日志链路追踪 (Trace ID)
\\n在分布式系统中,为了追踪一个请求的完整调用链路,通常会生成一个全局唯一的 Trace ID。
\\n这个 Trace ID 可以放在 ThreadLocal
中,随着请求在服务内部的线程调用栈中传递,打印日志时带上它,方便串联日志。
好了,关于 ThreadLocal
的使用篇就聊到这里。我们从一个真实的“踩坑”故事出发,了解了 ThreadLocal
是什么,为什么用它,怎么用它。
也了解了它的核心API (set
, get
, remove
)和重要事项(必须remove()
)等。
希望通过这篇“使用篇”,你能对 ThreadLocal
的正确用法有一个更清晰、更深入的认识。下次再遇到需要它解决问题的场景时,能胸有成竹,用得明明白白,避免重蹈我们的覆辙。
当然,仅仅知道怎么用还不够“酷” 😎。
\\n想知道 ThreadLocal
底层是怎么为每个线程维护独立副本的吗?ThreadLocalMap
到底长啥样?为什么 remove()
如此关键,不 remove
就一定会内存泄漏吗?弱引用(WeakReference)在其中扮演了什么角色?
别急,这些问题的答案,我们将在下一篇 《你真的会用ThreadLocal吗——原理篇》 中为你揭晓!敬请期待! 😉
\\n本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
\\n我们先来看下微服务网关的定义。
\\n微服务网关是指为前端或客户端提供的统一服务入口,用于处理后端程序的通用逻辑,诸如:认证鉴权、动态路由、限流熔断、API监控等。
\\n网关在微服务架构中所处的生态位,如下图所示:
\\n认证鉴权:对用户请求进行识别判定,拦截非法请求,保障系统安全性。
\\n动态路由:按照配置规则,动态地将用户请求路由到不同的微服务集群上。
\\n限流熔断:旨在提升高并发场景下的系统可用性。
\\n限流,控制单位时间内的请求流量,防止突发流量压垮后端服务,确保系统资源合理分配。
\\n熔断,当下游非核心服务出现故障(如超时、异常)时,快速中断请求链路并返回预设响应,避免故障扩散引发雪崩效应。
\\nAPI监控:通过集成多维度监控工具,实现对 API 请求的全链路追踪、性能指标收集与可视化分析,帮助开发者实时掌握网关流量状态与系统健康度。
\\n接下来我们以Spring Cloud Gateway为例,看看这些功能是如何实现的,再回答“为什么微服务架构一定要有网关”这个问题,这样逻辑会清晰很多。
\\nSpring Cloud Gateway实现认证鉴权的核心是通过过滤器链(Filter Chain)拦截请求,结合安全框架(如 Spring Security)或自定义逻辑对用户身份和权限进行验证。
\\n目前主流的认证鉴权方式是基于JWT来实现的,由
..三部分组成,具备无状态性、跨域支持和灵活扩展等优点。\\n代码实现如下:
\\n@Component\\npublic class JwtAuthFilter implements GlobalFilter, Ordered {\\n @Override\\n public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {\\n String token = exchange.getRequest().getHeaders().getFirst(\\"Authorization\\");\\n if (token == null || !token.startsWith(\\"Bearer \\")) {\\n exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);\\n return exchange.getResponse().setComplete();\\n }\\n try {\\n String jwtToken = token.substring(7); // 去掉 \\"Bearer \\" 前缀\\n List<String> roles = JwtValidator.getRoles(jwtToken);\\n // 检查角色是否匹配(示例:仅允许 ROLE_ADMIN 访问)\\n if (!roles.contains(\\"ROLE_ADMIN\\")) {\\n exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);\\n return exchange.getResponse().setComplete();\\n }\\n } catch (JwtException e) {\\n exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);\\n return exchange.getResponse().setComplete();\\n }\\n return chain.filter(exchange);\\n }\\n @Override\\n public int getOrder() {\\n return -1; // 优先级\\n }\\n}\\n
\\nSpring Cloud Gateway可通过与Nacos、Apollo等配置中心配合的方式实现动态
\\n路由,由配置中心管理路由配置,Gateway监听配置变化并动态更新路由。
\\n配置中心中的路由规则如下:
\\n[ { \\"id\\": \\"user-service\\", \\"predicates\\": [{\\"name\\": \\"Path\\", \\"args\\": {\\"pattern\\": \\"/api/user/**\\"}}],\\n \\"filters\\": [{\\"name\\": \\"StripPrefix\\", \\"args\\": {\\"parts\\": 1}}],\\n \\"uri\\": \\"lb://user-service\\",\\n \\"order\\": 0\\n }\\n]\\n
\\n然后在代码中对路由配置变化进行监听:
\\n@Component \\npublic class NacosRouteDefinitionRepository implements RouteDefinitionRepository { \\n @Autowired \\n private NacosConfigManager nacosConfigManager; \\n\\n @Override \\n public Flux<RouteDefinition> getRouteDefinitions() { \\n // 从 Nacos 加载路由配置:ml-citation{ref=\\"6,8\\" data=\\"citationList\\"} \\n return Flux.fromIterable(loadRoutesFromNacos()); \\n } \\n\\n @EventListener \\n public void onRefreshRoutesEvent(RefreshRoutesEvent event) { \\n // 触发路由更新:ml-citation{ref=\\"6\\" data=\\"citationList\\"} \\n } \\n} \\n
\\n对于限流熔断,目前的主流技术方案,是通过Spring Cloud Alibaba的Sentinel组件进行实现。
\\n我们可以在Sentinel自带的控制台中进行精细化限流熔断参数配置,并可以实时生效。
\\n如下图所示:
\\n限流方面,我们可以简单粗暴地对Spring Cloud Gateway进行整体限流,也可以按需进行更加精细化的多维度限流。
\\n比如:
\\n(1)可以通过精确、前缀和正则表达式的方式进行API分组,对不同分组设置不同的限流策略。
\\n(2)可以根据用户ID或外部系统编码进行限流,相比较于整体限流,这样可以更好地防止资源倾斜。
\\n如上文所述,Sentinel是支持动态配置并实时生效的,但我们实时修改配置一定是需要一句的,这个依据就是对Spring Cloud Gateway中的API进行监控。
\\n目前行业内的主流做法是,集成Prometheus监控平台进行数据收集和存储,并通过Grafana进行API指标(QPS、响应时间、错误率等)的可视化展示。
\\nSpring Cloud Gateway还可以与Spring Cloud Sleuth + Zipkin进行集成,自动追踪请求在多个服务间的流转路径,记录耗时、状态码等关键信息,提升工程师的问题排查效率。
\\n对于C端的高并发系统(QPS 10000+),我们可以调整采用率的方式来减轻存储压力,如:spring.sleuth.sampler.probability = 0.1。
\\n与此同时,我们还可以在代码中对关键业务接口专门配置注解,以使其进行强制采样。@Trace(sampler = Sampler.ALWAYS_SAMPLE)
\\n我们把网关所涉及到的功能梳理一遍后,接下来聊下“为什么微服务架构一定要有网关”。
\\n如果没有网关层的话,那路由调用的工作只能由前端或客户端来完成,一定程度上增加了它们的工作复杂度。
\\n如果没有网关层的话,那认证鉴权、限流熔断、API监控等工作只能由每个微服务都实现一遍,增加了很多重复的代码和工作量。
\\n这两种副作用显然都不是我们想要看到的。
\\n接下来我们再从系统架构的角度聊下微服务网关存在的意义。
\\n系统架构设计通常有两个思路,那就是分治化和中心化。
\\n分治化,其思路是将复杂问题进行拆解简化并逐个击破,而中心化则是将散点问题集中在一起进行解决,以起到提效的目的。
\\n如果说我们将系统从单体架构拆分为微服务架构是分治化的思路,那网关层的中心化引入,则是在一定程度上弥补微服务拆分后所带来的研发效率问题。
","description":"本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~ 我们先来看下微服务网关的定义。\\n\\n微服务网关是指为前端或客户端提供的统一服务入口,用于处理后端程序的通用逻辑,诸如:认证鉴权、动态路由、限流熔断、API监控等。\\n\\n网关在微服务架构中所处的生态位,如下图所示:\\n\\n认证鉴权:对用户请求进行识别判定,拦截非法请求,保障系统安全性。\\n\\n动态路由:按照配置规则,动态地将用户请求路由到不同的微服务集群上。\\n\\n限流熔断:旨在提升高并发场景下的系统可用性。\\n\\n限流,控制单位时间内的请求流量,防止突发流量压垮后端服务…","guid":"https://juejin.cn/post/7499013941127086080","author":"托尼学长","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-02T13:47:31.668Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0322c672cb764fd0b4150e6f03a55f23~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1746798451&x-signature=vv4%2BICgOHxhusILjtcccnZykZmU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/41cb2285525441aba2095d520d82aad4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1746798451&x-signature=6QjUPqkdiOLEUMWYEn28eShg7Qk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e03e819bb1a432b95a4c72de748e039~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1746798451&x-signature=%2FQ7NlzUB8mGO20%2F2eSJsrDSd%2F1s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/127a7fe37cb64b7ab57bd394f8a88eb9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1746798451&x-signature=zPnxRqFv9Taw2AJRKSIYfx4FJd4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab4f4ee73a09438cb73f49e829d1775a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5omY5bC85a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1746798451&x-signature=nplHAC0fKZt15%2BD%2BEvsUZ4%2B4cHU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","微服务"],"attachments":null,"extra":null,"language":null},{"title":"MyBatis 拦截器:引入分页插件导致自定义插件失效“之密”","url":"https://juejin.cn/post/7498988293210079286","content":"\\n\\n\\n
MyBatis
的拦截器采用责任链设计模式,多个拦截器之间的责任链是通过动态代理组织的。我们一般都会在拦截器中的intercept
方法中往往会有invocation.proceed()
语句,其作用是将拦截器责任链向后传递,本质上便是动态代理的invoke
。
在日常开发中,为了能使数据进行分页。通常会向Spring
容器中注入MyBatis-plus
的分页PaginationInnerInterceptor
拦截器。此时码逻辑大致如下:
@Bean\\npublic MybatisPlusInterceptor mybatisPlusInterceptor() {\\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\\n // 添加分页插件\\n interceptor.addInnerInterceptor(new PaginationInnerInterceptor());\\n return interceptor;\\n}\\n
\\n其原理为将PaginationInnerInterceptor
加入MybatisPlusInterceptor
的内部拦截链中,并通过@Bean
将MybatisPlusInterceptor
注入到Mybatis
的拦截器中,从而在Sql
执行时实现对数据的分页操作。
如果对MybatisPlusInterceptor
插件原理和Spring
中Bean
对象的初始逻辑掌握不清晰的的话,很可能导致向Spring
容器中注入的自定义的Mybatis
插件不生效。
日常开发中,对于常用的分页插件外,对于项目我们通常会自定义一些Mybatis
插件,从而实现对Sql
的打印、监控、改写等。
为了复现Mybatis-plus
分页插件所导致的问题,此处我们首先自定义一个简单的Mybatis
的插件,其逻辑也很简单,仅是打印一段enter SqlInterceptor
其主要作用在于验证我们的拦截器是否执行。
@Intercepts({\\n @Signature(type = Executor.class, method = \\"query\\",\\n args = {MappedStatement.class, Object.class,\\n RowBounds.class, ResultHandler.class})\\n})\\n@Slf4j\\npublic class SqlInterceptor implements Interceptor {\\n\\n\\n @Override\\n public Object intercept(Invocation invocation) throws Throwable {\\n log.info(\\"enter SqlInterceptor......\\");\\n return invocation.proceed();\\n }\\n
\\n上述代码中,我们自定义一个SqlInterceptor
的插件,接下来我们将其与Mybatis-plus
的分页插件一同注入到我们的Spring
容器中。
@Configuration\\npublic class MybatisConfig {\\n\\n @Bean\\n public SqlInterceptor sqlInterceptor() {\\n return new SqlInterceptor();\\n }\\n\\n\\n /**\\n * 配置 MyBatis-Plus 分页插件\\n * @return MybatisPlusInterceptor 包含分页插件的拦截器实例\\n */\\n @Bean\\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\\n // 添加分页插件\\n interceptor.addInnerInterceptor(new PaginationInnerInterceptor());\\n return interceptor;\\n }\\n \\n}\\n
\\n通过上述操作,此时在Spring
容器中存在两个Mybaits
的插件,一个是我们自定义的SqlInterceptor
另一个则是Mybatis-plus
的分页插件。按照我们对Mybaits
的认识,在执行sql
时,上述插件应该可以顺利执行,即程序代码中既可以顺利实现数据分页,同时也能在控制台看到\\"enter SqlInterceptor......\\"
的提示信息。
为此,我们构建一个简单的控制器,用以检验Mybaits
插件的生效情况。
@GetMapping(\\"t2\\")\\npublic IPage<User> t2(Integer current ,Integer size){\\n IPage<User> page = userMapper.selectPage(new Page<>(current,size), null);\\n return page;\\n}\\n
\\n代码执行结果如下:
\\n通过上述执行结果,不难发现在程序中数据可顺利实现分页,但却并未看到我们自定义的SqlInterceptor
的执行。换言之,我们自定义的SqlInterceptor
并未执行。
对于这类Bean
不生效的例子,我们首先应该想到的原因应该是Bean
是否注入。换言之,我们所需要的Bean
是否注入到Spring
容器中。
回到我们本文的例子,我们在容器Bean
注入时,我们自定义的SqlInterceptor
和Mybatis-plus
提供的MybatisPlusInterceptor
采用了相同的注入方式,同时我们注入注入的分页
插件已经生效,显然相同代码不会出现一个Bean
成功注入,另一个Bean
注入失败的情况,看到此可能还是会有人持有怀疑精神。针对质疑,事实远比无依据的猜测更具说服力。
那面对Mybatis
茫茫多的代码,又该在何处进行断点
调试进行验证呢?众所众知,当Mybatis
内部在初始化 SqlSessionFactory
时,MyBatis
会读取并解析配置文件。在 XMLConfigBuilder
类的 parse
方法里,会调用 parseConfiguration
方法解析配置文件中的各个部分,其中就包含对插件配置的解析。具体逻辑如下:
public Configuration parse() {\\n if (parsed) {\\n throw new BuilderException(\\"Each XMLConfigBuilder can only be used once.\\");\\n }\\n parsed = true;\\n parseConfiguration(parser.evalNode(\\"/configuration\\"));\\n return configuration;\\n}\\n\\nprivate void parseConfiguration(XNode root) {\\n try {\\n // 其他配置解析...\\n pluginElement(root.evalNode(\\"plugins\\"));\\n // 其他配置解析...\\n } catch (Exception e) {\\n throw new BuilderException(\\"Error parsing SQL Mapper Configuration. Cause: \\" + e, e);\\n }\\n}\\n
\\n进一步来看,pluginElement
方法的作用是解析 <plugins>
标签下的所有 <plugin>
标签,为每个插件创建实例并且添加到 InterceptorChain
中。
private void pluginElement(XNode parent) throws Exception {\\n if (parent != null) {\\n for (XNode child : parent.getChildren()) {\\n String interceptor = child.getStringAttribute(\\"interceptor\\");\\n Properties properties = child.getChildrenAsProperties();\\n Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();\\n interceptorInstance.setProperties(properties);\\n configuration.addInterceptor(interceptorInstance);\\n }\\n }\\n}\\n
\\n而 InterceptorChain
信息则被维护至Configuration
之中,同时Configuration
被SqlSessionFactory
所持有。所以,如果要找寻Mybatis
中插件的初始化逻辑,我们首先应该找到SqlSessionFactory
的初始化位置,方能顺藤摸瓜的找到Interceptor
的注入的问题。
对于SpringBoot
整合Mybatis-plus
的项目而言,其通常会在 MybatisPlusAutoConfiguration
的配置类中完成Mybatis
相关所需Bean
的注入,具体来看,有关SqlSessionFactory
的构建逻辑如下:
@Bean\\n@ConditionalOnMissingBean\\npublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {\\n // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean\\n MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();\\n // .... 省略相关无关代码\\n // 添加拦截器\\n if (!ObjectUtils.isEmpty(this.interceptors)) {\\n factory.setPlugins(this.interceptors);\\n }\\n \\n // .... 省略相关无关代码\\n}\\n
\\n可以看到Mybatis-plus
在向容器中添加SqlSessionFactory
中注入时,会将容器中全部的插件Interceptor
保存到SqlSessionFactory
之中,所以我们完全可以在!ObjectUtils.isEmpty(this.interceptors)
此处添加断点,以确定自定义的SqlInterceptor
是否注入容器。
通过图示,可以看到自定义SqlInterceptor
确实注入到容器中,既然SqlInterceptor
注入到容器中,为什么我们的SqlInterceptor
没能顺利执行呢?
Mybatis
插件执行逻辑事实上,如果要搞清楚SqlInterceptor
为什么没能执行,我们需要对Mybatis
的插件的执行顺序有一定的了解。
在 MyBatis
里,拦截器
的执行顺序和注册顺序
是相关的,其在实际拦截逻辑执行时会形成类似洋葱模型的嵌套结构。例如,
<plugins>\\n <plugin interceptor=\\"com.example.FirstInterceptor\\"/>\\n <plugin interceptor=\\"com.example.SecondInterceptor\\"/>\\n</plugins>\\n
\\n在上述代码中我们先后定义了SecondInterceptor
和FirstInterceptor
两个拦截器,而在Mybaits
内部构建代理时,其逻辑大致如下:
FirstInterceptor
的 plugin
方法,对目标对象进行第一次包装。SecondInterceptor
的 plugin
方法,对已经被 FirstInterceptor
包装过的对象进行第二次包装。内部逻辑具体如下所示:换言之,在执行器执行Sql
时,会按照 SecondInterceptor>FirstInterceptor>Executor>FirstInterceptor>SecondInterceptor
的顺序去执行的。而插件间的执行的传递则主要通过invocation.proceed()
语句来进行向下传递。具体到上述例子,拦截器执行顺序如下:
SecondInterceptor
的 intercept
方法,此时 SecondInterceptor
的 intercept
方法开始执行,但在执行到调用下一个拦截器或者目标方法之前,不会结束。FirstInterceptor
的 intercept
方法,FirstInterceptor
的 intercept
方法开始执行回到我们本文主题,那为什么我们注册的SqlInterceptor
会失效呢?通过前面对Mybatis
插件执行顺序的介绍。我们知道对于Mybatis
中的插件而言,插件间的调用通过invocation.proceed()
来完成调用,且插件执行顺序其实和插件注册顺序相悖。 在结合我们之前看到的插件读取顺序,可以知道在程序内部,我们插件的执行应该为 MybatisPlusInterceptor -> SqlInterceptor
。
至此,我们自定义插件 SqlInterceptor
失效的原因其实已经呼之欲出了,其无非就是MybatisPlusInterceptor
内部未继续执行invocation.proceed()
来调用我们的自定义插件SqlInterceptor
。
为了验证我们的猜想,我们不妨在MybatisPlusInterceptor
进行断点观察:
可以看到,在MybatisPlusInterceptor
内部在执行插件时,其实是不会执行到 invocation.proceed()
的,这也就是引入MybatisPlusInterceptor
导致自定义插件失效的罪魁祸首。
对于引入Mybaits-plus
导致自定义插件失效的解决方法其实有很多种,最简单的就是调换配置类
中插件注入顺序。例如,如下代码
@Configuration\\npublic class MybatisConfig {\\n\\n \\n\\n /**\\n * 配置 MyBatis-Plus 分页插件\\n * @return MybatisPlusInterceptor 包含分页插件的拦截器实例\\n */\\n @Bean\\n public MybatisPlusInterceptor mybatisPlusInterceptor() {\\n MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();\\n // 添加分页插件\\n interceptor.addInnerInterceptor(new PaginationInnerInterceptor());\\n return interceptor;\\n }\\n \\n @Bean\\n public SqlInterceptor sqlInterceptor() {\\n return new SqlInterceptor();\\n }\\n\\n \\n}\\n
\\n或者通过ConfigurationCustomizer
来指定拦截器顺序。
@Bean\\n public ConfigurationCustomizer configurationCustomizer() {\\n return configuration -> {\\n MyInterceptor myInterceptor = new MyInterceptor();\\n myInterceptor.setProperties(myInterceptorProperties.getProperties());\\n configuration.addInterceptor(myInterceptor);\\n };\\n }\\n
\\n在这还可以通过@Order
注解或者BeanDefinitionRegistryPostProcessor
来控制Mybaits
自定义拦截器
在容器中的注入顺序。
总之,解决注入Mybaits-plus
分之导致自定义失效的方式有很多种,但其核心逻辑完全在于控制Mybatis-Plus
插件先与我们注解进行注入。当然,如果自定义拦截器切入的拦截点与Mybaits-plus
插件不一样,则完全不用担心此问题插件不生效的问题~~~
设计模式是软件开发中解决特定问题的经验总结。在SpringBoot框架中,设计模式被巧妙地融入各个环节,不仅提高了框架的灵活性和扩展性,还为开发者提供了优雅的解决方案。
\\n本文将介绍13种设计模式在SpringBoot中的实际应用,每个模式都配有具体的代码实现和应用场景。
\\n单例模式确保一个类只有一个实例,并提供一个全局访问点。
\\nSpringBoot中的Bean默认都是单例的,由Spring容器负责创建和管理,保证全局唯一性。
\\n@Service\\npublic class UserService {\\n \\n // 构造方法私有化防止外部直接创建实例\\n private UserService() {\\n System.out.println(\\"UserService实例被创建\\");\\n }\\n \\n // Bean的作用域默认为singleton\\n @Autowired\\n private UserRepository userRepository;\\n \\n public User findById(Long id) {\\n return userRepository.findById(id).orElse(null);\\n }\\n}\\n\\n@RestController\\npublic class UserController {\\n \\n // 注入的是同一个UserService实例\\n @Autowired\\n private UserService userService;\\n \\n @GetMapping(\\"/users/{id}\\")\\n public User getUser(@PathVariable Long id) {\\n return userService.findById(id);\\n }\\n}\\n
\\n工厂方法模式定义一个用于创建对象的接口,让子类决定实例化哪一个类。
\\nSpringBoot中的BeanFactory就是典型的工厂方法模式应用,它负责创建和管理Bean实例。
\\n// 支付处理器接口\\npublic interface PaymentProcessor {\\n void processPayment(double amount);\\n}\\n\\n// 具体实现 - 信用卡支付\\n@Component(\\"creditCard\\")\\npublic class CreditCardProcessor implements PaymentProcessor {\\n @Override\\n public void processPayment(double amount) {\\n System.out.println(\\"Processing credit card payment of $\\" + amount);\\n }\\n}\\n\\n// 具体实现 - PayPal支付\\n@Component(\\"paypal\\")\\npublic class PayPalProcessor implements PaymentProcessor {\\n @Override\\n public void processPayment(double amount) {\\n System.out.println(\\"Processing PayPal payment of $\\" + amount);\\n }\\n}\\n\\n// 支付处理器工厂\\n@Component\\npublic class PaymentProcessorFactory {\\n \\n @Autowired\\n private Map<String, PaymentProcessor> processors;\\n \\n public PaymentProcessor getProcessor(String type) {\\n PaymentProcessor processor = processors.get(type);\\n if (processor == null) {\\n throw new IllegalArgumentException(\\"No payment processor found for type: \\" + type);\\n }\\n return processor;\\n }\\n}\\n\\n// 使用工厂\\n@Service\\npublic class OrderService {\\n \\n @Autowired\\n private PaymentProcessorFactory processorFactory;\\n \\n public void placeOrder(String paymentType, double amount) {\\n PaymentProcessor processor = processorFactory.getProcessor(paymentType);\\n processor.processPayment(amount);\\n // 处理订单其余部分...\\n }\\n}\\n
\\n抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
\\nSpringBoot中的多环境配置和数据源创建就是抽象工厂模式的应用。
\\n// 抽象产品 - 连接\\npublic interface Connection {\\n void connect();\\n void executeQuery(String query);\\n void close();\\n}\\n\\n// 抽象产品 - 事务\\npublic interface Transaction {\\n void begin();\\n void commit();\\n void rollback();\\n}\\n\\n// 抽象工厂\\npublic interface DatabaseFactory {\\n Connection createConnection();\\n Transaction createTransaction();\\n}\\n\\n// 具体工厂 - MySQL\\n@Component(\\"mysqlFactory\\")\\n@ConditionalOnProperty(name = \\"db.type\\", havingValue = \\"mysql\\")\\npublic class MySQLDatabaseFactory implements DatabaseFactory {\\n @Override\\n public Connection createConnection() {\\n return new MySQLConnection();\\n }\\n \\n @Override\\n public Transaction createTransaction() {\\n return new MySQLTransaction();\\n }\\n}\\n\\n// 具体工厂 - PostgreSQL\\n@Component(\\"postgresFactory\\")\\n@ConditionalOnProperty(name = \\"db.type\\", havingValue = \\"postgres\\")\\npublic class PostgresDatabaseFactory implements DatabaseFactory {\\n @Override\\n public Connection createConnection() {\\n return new PostgresConnection();\\n }\\n \\n @Override\\n public Transaction createTransaction() {\\n return new PostgresTransaction();\\n }\\n}\\n\\n// 具体产品实现 - MySQL连接\\npublic class MySQLConnection implements Connection {\\n @Override\\n public void connect() {\\n System.out.println(\\"Connecting to MySQL database\\");\\n }\\n \\n @Override\\n public void executeQuery(String query) {\\n System.out.println(\\"Executing query on MySQL: \\" + query);\\n }\\n \\n @Override\\n public void close() {\\n System.out.println(\\"Closing MySQL connection\\");\\n }\\n}\\n\\n// 使用抽象工厂\\n@Service\\npublic class QueryService {\\n \\n private final DatabaseFactory databaseFactory;\\n \\n @Autowired\\n public QueryService(@Qualifier(\\"mysqlFactory\\") DatabaseFactory databaseFactory) {\\n this.databaseFactory = databaseFactory;\\n }\\n \\n public void executeQuery(String query) {\\n Connection connection = databaseFactory.createConnection();\\n Transaction transaction = databaseFactory.createTransaction();\\n \\n try {\\n connection.connect();\\n transaction.begin();\\n connection.executeQuery(query);\\n transaction.commit();\\n } catch (Exception e) {\\n transaction.rollback();\\n } finally {\\n connection.close();\\n }\\n }\\n}\\n
\\n建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
\\nSpringBoot中的配置类和链式API设计中大量使用了建造者模式。
\\n// 产品类\\n@Data\\npublic class EmailMessage {\\n private String from;\\n private List<String> to;\\n private List<String> cc;\\n private String subject;\\n private String body;\\n private boolean html;\\n private List<String> attachments;\\n private LocalDateTime scheduledTime;\\n \\n // 私有构造器,只能通过Builder创建\\n private EmailMessage() {}\\n \\n // 建造者类\\n public static class Builder {\\n private EmailMessage message;\\n \\n public Builder() {\\n message = new EmailMessage();\\n message.to = new ArrayList<>();\\n message.cc = new ArrayList<>();\\n message.attachments = new ArrayList<>();\\n }\\n \\n public Builder from(String from) {\\n message.from = from;\\n return this;\\n }\\n \\n public Builder to(String to) {\\n message.to.add(to);\\n return this;\\n }\\n \\n public Builder cc(String cc) {\\n message.cc.add(cc);\\n return this;\\n }\\n \\n public Builder subject(String subject) {\\n message.subject = subject;\\n return this;\\n }\\n \\n public Builder body(String body) {\\n message.body = body;\\n return this;\\n }\\n \\n public Builder html(boolean html) {\\n message.html = html;\\n return this;\\n }\\n \\n public Builder attachment(String attachment) {\\n message.attachments.add(attachment);\\n return this;\\n }\\n \\n public Builder scheduledTime(LocalDateTime scheduledTime) {\\n message.scheduledTime = scheduledTime;\\n return this;\\n }\\n \\n public EmailMessage build() {\\n if (message.from == null || message.to.isEmpty() || message.subject == null) {\\n throw new IllegalStateException(\\"From, To and Subject are required\\");\\n }\\n return message;\\n }\\n }\\n}\\n\\n// 服务使用建造者模式\\n@Service\\npublic class EmailService {\\n \\n // 使用建造者构造复杂对象\\n public void sendWelcomeEmail(User user) {\\n EmailMessage message = new EmailMessage.Builder()\\n .from(\\"noreply@example.com\\")\\n .to(user.getEmail())\\n .subject(\\"Welcome to Our Platform\\")\\n .body(\\"<h1>Welcome, \\" + user.getName() + \\"!</h1><p>Thanks for joining us.</p>\\")\\n .html(true)\\n .build();\\n \\n sendEmail(message);\\n }\\n \\n private void sendEmail(EmailMessage message) {\\n // 发送邮件逻辑\\n System.out.println(\\"Sending email: \\" + message);\\n }\\n}\\n
\\n原型模式通过复制现有对象而非创建新实例来创建对象,用于创建成本高昂的对象。
\\nSpringBoot中的bean作用域prototype就是原型模式的应用,每次获取都会创建新的实例。
\\n// 支持克隆的配置类\\n@Component\\n@Scope(\\"prototype\\")\\npublic class ReportConfiguration implements Cloneable {\\n private String reportType;\\n private List<String> columns;\\n private String sortBy;\\n private boolean ascending;\\n private String dateRange;\\n \\n // 默认构造器设置基础配置\\n public ReportConfiguration() {\\n this.reportType = \\"summary\\";\\n this.columns = new ArrayList<>(Arrays.asList(\\"id\\", \\"name\\", \\"date\\"));\\n this.sortBy = \\"date\\";\\n this.ascending = false;\\n this.dateRange = \\"last7days\\";\\n }\\n \\n @Override\\n public ReportConfiguration clone() {\\n try {\\n ReportConfiguration clone = (ReportConfiguration) super.clone();\\n // 深复制列表\\n clone.columns = new ArrayList<>(this.columns);\\n return clone;\\n } catch (CloneNotSupportedException e) {\\n throw new RuntimeException(e);\\n }\\n }\\n \\n // getter和setter方法\\n // ...\\n}\\n\\n// 报表工厂使用原型模式\\n@Service\\npublic class ReportFactory {\\n \\n // 注入原型bean\\n @Autowired\\n private ReportConfiguration defaultConfig;\\n \\n // 存储预定义的模板\\n private Map<String, ReportConfiguration> templates = new HashMap<>();\\n \\n @PostConstruct\\n public void initTemplates() {\\n // 创建财务报表模板\\n ReportConfiguration financialTemplate = defaultConfig.clone();\\n financialTemplate.setReportType(\\"financial\\");\\n financialTemplate.setColumns(Arrays.asList(\\"id\\", \\"amount\\", \\"transaction_date\\", \\"category\\"));\\n financialTemplate.setSortBy(\\"amount\\");\\n templates.put(\\"financial\\", financialTemplate);\\n \\n // 创建用户活动报表模板\\n ReportConfiguration activityTemplate = defaultConfig.clone();\\n activityTemplate.setReportType(\\"activity\\");\\n activityTemplate.setColumns(Arrays.asList(\\"user_id\\", \\"action\\", \\"timestamp\\", \\"ip_address\\"));\\n activityTemplate.setSortBy(\\"timestamp\\");\\n templates.put(\\"activity\\", activityTemplate);\\n }\\n \\n // 基于已有模板创建新配置\\n public ReportConfiguration createFromTemplate(String templateName) {\\n ReportConfiguration template = templates.get(templateName);\\n if (template == null) {\\n throw new IllegalArgumentException(\\"Template not found: \\" + templateName);\\n }\\n return template.clone();\\n }\\n \\n // 基于默认配置创建新配置\\n public ReportConfiguration createDefault() {\\n return defaultConfig.clone();\\n }\\n}\\n\\n// 使用原型模式\\n@RestController\\n@RequestMapping(\\"/reports\\")\\npublic class ReportController {\\n \\n @Autowired\\n private ReportFactory reportFactory;\\n \\n @GetMapping(\\"/financial\\")\\n public String generateFinancialReport(@RequestParam Map<String, String> params) {\\n // 获取财务报表模板并自定义\\n ReportConfiguration config = reportFactory.createFromTemplate(\\"financial\\");\\n \\n // 根据请求参数自定义报表配置\\n if (params.containsKey(\\"dateRange\\")) {\\n config.setDateRange(params.get(\\"dateRange\\"));\\n }\\n \\n // 使用配置生成报表...\\n return \\"Financial report generated with config: \\" + config;\\n }\\n}\\n
\\n适配器模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
\\nSpringBoot中的各种适配器广泛应用于MVC框架和第三方集成中。
\\n// 旧的支付服务接口\\npublic interface LegacyPaymentService {\\n boolean processPayment(String accountNumber, double amount, String currency);\\n String getTransactionStatus(String transactionId);\\n}\\n\\n// 旧的支付服务实现\\n@Service(\\"legacyPaymentService\\")\\npublic class LegacyPaymentServiceImpl implements LegacyPaymentService {\\n @Override\\n public boolean processPayment(String accountNumber, double amount, String currency) {\\n System.out.println(\\"Processing payment using legacy system\\");\\n // 旧系统的支付处理逻辑\\n return true;\\n }\\n \\n @Override\\n public String getTransactionStatus(String transactionId) {\\n // 旧系统获取交易状态的逻辑\\n return \\"COMPLETED\\";\\n }\\n}\\n\\n// 新的支付接口\\npublic interface ModernPaymentGateway {\\n PaymentResponse pay(PaymentRequest request);\\n TransactionStatus checkStatus(String reference);\\n}\\n\\n// 支付请求和响应模型\\n@Data\\npublic class PaymentRequest {\\n private String customerId;\\n private BigDecimal amount;\\n private String currencyCode;\\n private String paymentMethod;\\n private Map<String, String> metadata;\\n}\\n\\n@Data\\npublic class PaymentResponse {\\n private String referenceId;\\n private boolean successful;\\n private String message;\\n}\\n\\npublic enum TransactionStatus {\\n PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED\\n}\\n\\n// 适配器:将旧接口适配到新接口\\n@Component\\npublic class LegacyPaymentAdapter implements ModernPaymentGateway {\\n \\n private final LegacyPaymentService legacyService;\\n \\n @Autowired\\n public LegacyPaymentAdapter(LegacyPaymentService legacyService) {\\n this.legacyService = legacyService;\\n }\\n \\n @Override\\n public PaymentResponse pay(PaymentRequest request) {\\n // 将新的请求模型转换为旧接口参数\\n boolean result = legacyService.processPayment(\\n request.getCustomerId(),\\n request.getAmount().doubleValue(),\\n request.getCurrencyCode()\\n );\\n \\n // 将旧接口结果转换为新的响应模型\\n PaymentResponse response = new PaymentResponse();\\n response.setSuccessful(result);\\n response.setReferenceId(UUID.randomUUID().toString());\\n response.setMessage(result ? \\"Payment processed successfully\\" : \\"Payment failed\\");\\n \\n return response;\\n }\\n \\n @Override\\n public TransactionStatus checkStatus(String reference) {\\n // 将旧接口的状态映射为新接口的枚举\\n String status = legacyService.getTransactionStatus(reference);\\n \\n switch (status) {\\n case \\"COMPLETED\\":\\n return TransactionStatus.COMPLETED;\\n case \\"FAILED\\":\\n return TransactionStatus.FAILED;\\n case \\"IN_PROGRESS\\":\\n return TransactionStatus.PROCESSING;\\n default:\\n return TransactionStatus.PENDING;\\n }\\n }\\n}\\n\\n// 使用新接口\\n@Service\\npublic class CheckoutService {\\n \\n private final ModernPaymentGateway paymentGateway;\\n \\n @Autowired\\n public CheckoutService(ModernPaymentGateway paymentGateway) {\\n this.paymentGateway = paymentGateway;\\n }\\n \\n public void processCheckout(Cart cart, String customerId) {\\n // 创建支付请求\\n PaymentRequest request = new PaymentRequest();\\n request.setCustomerId(customerId);\\n request.setAmount(cart.getTotal());\\n request.setCurrencyCode(\\"USD\\");\\n \\n // 使用适配后的接口处理支付\\n PaymentResponse response = paymentGateway.pay(request);\\n \\n if (response.isSuccessful()) {\\n // 处理成功逻辑\\n } else {\\n // 处理失败逻辑\\n }\\n }\\n}\\n
\\n装饰器模式动态地给一个对象添加一些额外的职责,相比生成子类更为灵活。
\\nSpringBoot中的@Cacheable等注解就是装饰器模式的应用,对原有方法进行增强。
\\n// 基础接口\\npublic interface NotificationService {\\n void send(String message, String recipient);\\n}\\n\\n// 基础实现\\n@Service\\n@Primary\\npublic class EmailNotificationService implements NotificationService {\\n @Override\\n public void send(String message, String recipient) {\\n System.out.println(\\"Sending email to \\" + recipient + \\": \\" + message);\\n // 实际发送邮件的逻辑\\n }\\n}\\n\\n// 装饰器基类\\npublic abstract class NotificationDecorator implements NotificationService {\\n protected NotificationService wrapped;\\n \\n public NotificationDecorator(NotificationService wrapped) {\\n this.wrapped = wrapped;\\n }\\n}\\n\\n// 日志装饰器\\n@Component\\npublic class LoggingNotificationDecorator extends NotificationDecorator {\\n \\n private final Logger logger = LoggerFactory.getLogger(LoggingNotificationDecorator.class);\\n \\n public LoggingNotificationDecorator(NotificationService wrapped) {\\n super(wrapped);\\n }\\n \\n @Override\\n public void send(String message, String recipient) {\\n logger.info(\\"Sending notification to: {}\\", recipient);\\n long startTime = System.currentTimeMillis();\\n \\n wrapped.send(message, recipient);\\n \\n long endTime = System.currentTimeMillis();\\n logger.info(\\"Notification sent in {}ms\\", (endTime - startTime));\\n }\\n}\\n\\n// 重试装饰器\\n@Component\\npublic class RetryNotificationDecorator extends NotificationDecorator {\\n \\n private final Logger logger = LoggerFactory.getLogger(RetryNotificationDecorator.class);\\n private final int maxRetries;\\n \\n public RetryNotificationDecorator(\\n @Qualifier(\\"loggingNotificationDecorator\\") NotificationService wrapped, \\n @Value(\\"${notification.max-retries:3}\\") int maxRetries) {\\n super(wrapped);\\n this.maxRetries = maxRetries;\\n }\\n \\n @Override\\n public void send(String message, String recipient) {\\n int attempts = 0;\\n boolean sent = false;\\n \\n while (!sent && attempts < maxRetries) {\\n try {\\n attempts++;\\n wrapped.send(message, recipient);\\n sent = true;\\n } catch (Exception e) {\\n logger.warn(\\"Failed to send notification (attempt {}): {}\\", \\n attempts, e.getMessage());\\n \\n if (attempts >= maxRetries) {\\n logger.error(\\"Max retries reached, giving up\\");\\n throw e;\\n }\\n \\n try {\\n // 指数退避\\n Thread.sleep((long) Math.pow(2, attempts) * 100);\\n } catch (InterruptedException ie) {\\n Thread.currentThread().interrupt();\\n }\\n }\\n }\\n }\\n}\\n\\n// 加密装饰器\\n@Component\\npublic class EncryptionNotificationDecorator extends NotificationDecorator {\\n \\n public EncryptionNotificationDecorator(\\n @Qualifier(\\"retryNotificationDecorator\\") NotificationService wrapped) {\\n super(wrapped);\\n }\\n \\n @Override\\n public void send(String message, String recipient) {\\n String encryptedMessage = encrypt(message);\\n wrapped.send(encryptedMessage, recipient);\\n }\\n \\n private String encrypt(String message) {\\n // 加密逻辑\\n return \\"ENCRYPTED[\\" + message + \\"]\\";\\n }\\n}\\n\\n// 装饰器配置\\n@Configuration\\npublic class NotificationConfig {\\n \\n @Bean\\n public NotificationService loggingNotificationDecorator(\\n @Qualifier(\\"emailNotificationService\\") NotificationService emailService) {\\n return new LoggingNotificationDecorator(emailService);\\n }\\n \\n @Bean\\n public NotificationService retryNotificationDecorator(\\n @Qualifier(\\"loggingNotificationDecorator\\") NotificationService loggingDecorator,\\n @Value(\\"${notification.max-retries:3}\\") int maxRetries) {\\n return new RetryNotificationDecorator(loggingDecorator, maxRetries);\\n }\\n \\n @Bean\\n public NotificationService encryptionNotificationDecorator(\\n @Qualifier(\\"retryNotificationDecorator\\") NotificationService retryDecorator) {\\n return new EncryptionNotificationDecorator(retryDecorator);\\n }\\n \\n @Bean\\n @Primary\\n public NotificationService notificationService(\\n @Qualifier(\\"encryptionNotificationDecorator\\") NotificationService encryptionDecorator) {\\n return encryptionDecorator;\\n }\\n}\\n\\n// 使用装饰后的服务\\n@Service\\npublic class UserService {\\n \\n private final NotificationService notificationService;\\n \\n @Autowired\\n public UserService(NotificationService notificationService) {\\n this.notificationService = notificationService;\\n }\\n \\n public void notifyUser(User user, String message) {\\n // 这里使用的是经过多层装饰的服务:加密->重试->日志->邮件\\n notificationService.send(message, user.getEmail());\\n }\\n}\\n
\\n观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
\\nSpringBoot中的事件机制是观察者模式的典型应用,如ApplicationEvent和ApplicationListener。
\\n// 自定义事件\\npublic class UserRegisteredEvent extends ApplicationEvent {\\n \\n private final User user;\\n \\n public UserRegisteredEvent(Object source, User user) {\\n super(source);\\n this.user = user;\\n }\\n \\n public User getUser() {\\n return user;\\n }\\n}\\n\\n// 事件发布者\\n@Service\\npublic class UserRegistrationService {\\n \\n private final ApplicationEventPublisher eventPublisher;\\n private final UserRepository userRepository;\\n \\n @Autowired\\n public UserRegistrationService(\\n ApplicationEventPublisher eventPublisher,\\n UserRepository userRepository) {\\n this.eventPublisher = eventPublisher;\\n this.userRepository = userRepository;\\n }\\n \\n @Transactional\\n public User registerUser(UserRegistrationDto registrationDto) {\\n // 创建并保存用户\\n User user = new User();\\n user.setUsername(registrationDto.getUsername());\\n user.setEmail(registrationDto.getEmail());\\n user.setPassword(encodePassword(registrationDto.getPassword()));\\n user.setRegistrationDate(LocalDateTime.now());\\n \\n User savedUser = userRepository.save(user);\\n \\n // 发布用户注册事件\\n eventPublisher.publishEvent(new UserRegisteredEvent(this, savedUser));\\n \\n return savedUser;\\n }\\n \\n private String encodePassword(String password) {\\n // 密码加密逻辑\\n return \\"{bcrypt}\\" + password;\\n }\\n}\\n\\n// 事件监听器 - 发送欢迎邮件\\n@Component\\npublic class WelcomeEmailListener implements ApplicationListener<UserRegisteredEvent> {\\n \\n private final EmailService emailService;\\n \\n @Autowired\\n public WelcomeEmailListener(EmailService emailService) {\\n this.emailService = emailService;\\n }\\n \\n @Override\\n public void onApplicationEvent(UserRegisteredEvent event) {\\n User user = event.getUser();\\n \\n // 发送欢迎邮件\\n emailService.sendWelcomeEmail(user);\\n }\\n}\\n\\n// 事件监听器 - 创建用户资料\\n@Component\\npublic class UserProfileInitializer implements ApplicationListener<UserRegisteredEvent> {\\n \\n private final ProfileService profileService;\\n \\n @Autowired\\n public UserProfileInitializer(ProfileService profileService) {\\n this.profileService = profileService;\\n }\\n \\n @Override\\n public void onApplicationEvent(UserRegisteredEvent event) {\\n User user = event.getUser();\\n \\n // 创建用户资料\\n profileService.createInitialProfile(user);\\n }\\n}\\n\\n// 使用注解方式的事件监听器\\n@Component\\npublic class MarketingSubscriptionHandler {\\n \\n private final MarketingService marketingService;\\n \\n @Autowired\\n public MarketingSubscriptionHandler(MarketingService marketingService) {\\n this.marketingService = marketingService;\\n }\\n \\n @EventListener\\n @Async\\n public void handleUserRegistered(UserRegisteredEvent event) {\\n User user = event.getUser();\\n \\n // 添加到营销列表\\n marketingService.addUserToDefaultNewsletters(user);\\n }\\n}\\n\\n// 异步事件配置\\n@Configuration\\n@EnableAsync\\npublic class AsyncConfig implements AsyncConfigurer {\\n \\n @Override\\n public Executor getAsyncExecutor() {\\n ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\\n executor.setCorePoolSize(5);\\n executor.setMaxPoolSize(10);\\n executor.setQueueCapacity(25);\\n executor.setThreadNamePrefix(\\"EventHandler-\\");\\n executor.initialize();\\n return executor;\\n }\\n}\\n
\\n策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换,让算法的变化独立于使用它的客户。
\\n策略模式广泛用于SpringBoot中的各种可配置策略,如缓存策略、认证策略等。
\\n// 折扣策略接口\\npublic interface DiscountStrategy {\\n BigDecimal applyDiscount(BigDecimal amount, User user);\\n boolean isApplicable(User user, ShoppingCart cart);\\n}\\n\\n// 新用户折扣策略\\n@Component\\npublic class NewUserDiscountStrategy implements DiscountStrategy {\\n \\n @Value(\\"${discount.new-user.percentage:10}\\")\\n private int discountPercentage;\\n \\n @Override\\n public BigDecimal applyDiscount(BigDecimal amount, User user) {\\n BigDecimal discountFactor = BigDecimal.valueOf(discountPercentage)\\n .divide(BigDecimal.valueOf(100));\\n BigDecimal discount = amount.multiply(discountFactor);\\n return amount.subtract(discount);\\n }\\n \\n @Override\\n public boolean isApplicable(User user, ShoppingCart cart) {\\n LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);\\n return user.getRegistrationDate().isAfter(thirtyDaysAgo);\\n }\\n}\\n\\n// 会员折扣策略\\n@Component\\npublic class PremiumMemberDiscountStrategy implements DiscountStrategy {\\n \\n @Value(\\"${discount.premium-member.percentage:15}\\")\\n private int discountPercentage;\\n \\n @Override\\n public BigDecimal applyDiscount(BigDecimal amount, User user) {\\n BigDecimal discountFactor = BigDecimal.valueOf(discountPercentage)\\n .divide(BigDecimal.valueOf(100));\\n BigDecimal discount = amount.multiply(discountFactor);\\n return amount.subtract(discount);\\n }\\n \\n @Override\\n public boolean isApplicable(User user, ShoppingCart cart) {\\n return \\"PREMIUM\\".equals(user.getMembershipLevel());\\n }\\n}\\n\\n// 大订单折扣策略\\n@Component\\npublic class LargeOrderDiscountStrategy implements DiscountStrategy {\\n \\n @Value(\\"${discount.large-order.threshold:1000}\\")\\n private BigDecimal threshold;\\n \\n @Value(\\"${discount.large-order.percentage:5}\\")\\n private int discountPercentage;\\n \\n @Override\\n public BigDecimal applyDiscount(BigDecimal amount, User user) {\\n BigDecimal discountFactor = BigDecimal.valueOf(discountPercentage)\\n .divide(BigDecimal.valueOf(100));\\n BigDecimal discount = amount.multiply(discountFactor);\\n return amount.subtract(discount);\\n }\\n \\n @Override\\n public boolean isApplicable(User user, ShoppingCart cart) {\\n return cart.getTotalAmount().compareTo(threshold) >= 0;\\n }\\n}\\n\\n// 策略上下文\\n@Service\\npublic class DiscountService {\\n \\n private final List<DiscountStrategy> discountStrategies;\\n \\n @Autowired\\n public DiscountService(List<DiscountStrategy> discountStrategies) {\\n this.discountStrategies = discountStrategies;\\n }\\n \\n public BigDecimal calculateDiscountedAmount(BigDecimal originalAmount, User user, ShoppingCart cart) {\\n // 查找最佳折扣策略\\n DiscountStrategy bestStrategy = findBestDiscountStrategy(user, cart);\\n \\n if (bestStrategy != null) {\\n return bestStrategy.applyDiscount(originalAmount, user);\\n }\\n \\n // 无可用折扣\\n return originalAmount;\\n }\\n \\n private DiscountStrategy findBestDiscountStrategy(User user, ShoppingCart cart) {\\n BigDecimal originalAmount = cart.getTotalAmount();\\n BigDecimal bestDiscount = BigDecimal.ZERO;\\n DiscountStrategy bestStrategy = null;\\n \\n for (DiscountStrategy strategy : discountStrategies) {\\n if (strategy.isApplicable(user, cart)) {\\n BigDecimal discountedAmount = strategy.applyDiscount(originalAmount, user);\\n BigDecimal discount = originalAmount.subtract(discountedAmount);\\n \\n if (discount.compareTo(bestDiscount) > 0) {\\n bestDiscount = discount;\\n bestStrategy = strategy;\\n }\\n }\\n }\\n \\n return bestStrategy;\\n }\\n}\\n
\\n模板方法模式定义了一个算法的骨架,将一些步骤延迟到子类中实现,使子类可以不改变算法结构的情况下重定义算法的某些步骤。
\\nSpringBoot中的JdbcTemplate、RestTemplate等都是模板方法模式的应用。
\\n// 抽象导出处理器\\n@Component\\npublic abstract class AbstractReportExporter {\\n \\n // 模板方法定义了算法的骨架\\n public final void exportReport(ReportRequest request, OutputStream output) {\\n try {\\n // 1. 验证请求\\n validateRequest(request);\\n \\n // 2. 获取数据\\n ReportData data = fetchData(request);\\n \\n // 3. 处理数据\\n ReportData processedData = processData(data);\\n \\n // 4. 格式化数据(由子类实现)\\n byte[] formattedData = formatData(processedData);\\n \\n // 5. 写入输出流\\n output.write(formattedData);\\n output.flush();\\n \\n // 6. 记录导出操作\\n logExport(request, processedData);\\n \\n } catch (Exception e) {\\n handleExportError(e, request);\\n }\\n }\\n \\n // 默认实现的方法\\n protected void validateRequest(ReportRequest request) {\\n if (request == null) {\\n throw new IllegalArgumentException(\\"Report request cannot be null\\");\\n }\\n \\n if (request.getStartDate() == null || request.getEndDate() == null) {\\n throw new IllegalArgumentException(\\"Start date and end date are required\\");\\n }\\n \\n if (request.getStartDate().isAfter(request.getEndDate())) {\\n throw new IllegalArgumentException(\\"Start date cannot be after end date\\");\\n }\\n }\\n \\n // 抽象方法,必须由子类实现\\n protected abstract ReportData fetchData(ReportRequest request);\\n \\n // 钩子方法,子类可以选择性覆盖\\n protected ReportData processData(ReportData data) {\\n // 默认实现:不做任何处理\\n return data;\\n }\\n \\n // 抽象方法,必须由子类实现\\n protected abstract byte[] formatData(ReportData data) throws IOException;\\n \\n // 默认实现的方法\\n protected void logExport(ReportRequest request, ReportData data) {\\n System.out.println(\\"Report exported for period: \\" + \\n request.getStartDate() + \\" to \\" + request.getEndDate() + \\n \\", records: \\" + data.getRecords().size());\\n }\\n \\n // 默认实现的方法\\n protected void handleExportError(Exception e, ReportRequest request) {\\n System.err.println(\\"Error exporting report: \\" + e.getMessage());\\n throw new ReportExportException(\\"Failed to export report\\", e);\\n }\\n}\\n\\n// PDF导出器实现\\n@Component(\\"pdfExporter\\")\\npublic class PdfReportExporter extends AbstractReportExporter {\\n \\n @Autowired\\n private ReportRepository reportRepository;\\n \\n @Override\\n protected ReportData fetchData(ReportRequest request) {\\n // 从数据库获取报表数据\\n List<ReportRecord> records = reportRepository.findByDateRange(\\n request.getStartDate(), request.getEndDate());\\n \\n return new ReportData(records, request.getStartDate(), request.getEndDate());\\n }\\n \\n @Override\\n protected ReportData processData(ReportData data) {\\n // 处理数据,如排序、分组、计算统计值等\\n List<ReportRecord> processedRecords = data.getRecords().stream()\\n .sorted(Comparator.comparing(ReportRecord::getDate))\\n .collect(Collectors.toList());\\n \\n return new ReportData(processedRecords, data.getStartDate(), data.getEndDate());\\n }\\n \\n @Override\\n protected byte[] formatData(ReportData data) throws IOException {\\n Document document = new Document();\\n ByteArrayOutputStream baos = new ByteArrayOutputStream();\\n \\n try {\\n PdfWriter.getInstance(document, baos);\\n document.open();\\n \\n // 添加标题\\n document.add(new Paragraph(\\"Report from \\" + \\n data.getStartDate() + \\" to \\" + data.getEndDate()));\\n \\n // 创建表格\\n PdfPTable table = new PdfPTable(3);\\n table.addCell(\\"Date\\");\\n table.addCell(\\"Description\\");\\n table.addCell(\\"Amount\\");\\n \\n // 添加数据行\\n for (ReportRecord record : data.getRecords()) {\\n table.addCell(record.getDate().toString());\\n table.addCell(record.getDescription());\\n table.addCell(record.getAmount().toString());\\n }\\n \\n document.add(table);\\n \\n } finally {\\n if (document.isOpen()) {\\n document.close();\\n }\\n }\\n \\n return baos.toByteArray();\\n }\\n}\\n\\n// Excel导出器实现\\n@Component(\\"excelExporter\\")\\npublic class ExcelReportExporter extends AbstractReportExporter {\\n \\n @Autowired\\n private ReportRepository reportRepository;\\n \\n @Override\\n protected ReportData fetchData(ReportRequest request) {\\n // 从数据库获取报表数据\\n List<ReportRecord> records = reportRepository.findByDateRange(\\n request.getStartDate(), request.getEndDate());\\n \\n return new ReportData(records, request.getStartDate(), request.getEndDate());\\n }\\n \\n @Override\\n protected byte[] formatData(ReportData data) throws IOException {\\n Workbook workbook = new XSSFWorkbook();\\n ByteArrayOutputStream baos = new ByteArrayOutputStream();\\n \\n try {\\n Sheet sheet = workbook.createSheet(\\"Report\\");\\n \\n // 创建标题行\\n Row headerRow = sheet.createRow(0);\\n headerRow.createCell(0).setCellValue(\\"Date\\");\\n headerRow.createCell(1).setCellValue(\\"Description\\");\\n headerRow.createCell(2).setCellValue(\\"Amount\\");\\n \\n // 添加数据行\\n int rowNum = 1;\\n for (ReportRecord record : data.getRecords()) {\\n Row row = sheet.createRow(rowNum++);\\n row.createCell(0).setCellValue(record.getDate().toString());\\n row.createCell(1).setCellValue(record.getDescription());\\n row.createCell(2).setCellValue(record.getAmount().doubleValue());\\n }\\n \\n // 调整列宽\\n for (int i = 0; i < 3; i++) {\\n sheet.autoSizeColumn(i);\\n }\\n \\n workbook.write(baos);\\n \\n } finally {\\n workbook.close();\\n }\\n \\n return baos.toByteArray();\\n }\\n}\\n\\n// 使用模板方法模式\\n@RestController\\n@RequestMapping(\\"/reports\\")\\npublic class ReportController {\\n \\n @Autowired\\n @Qualifier(\\"pdfExporter\\")\\n private AbstractReportExporter pdfExporter;\\n \\n @Autowired\\n @Qualifier(\\"excelExporter\\")\\n private AbstractReportExporter excelExporter;\\n \\n @GetMapping(value = \\"/export/pdf\\", produces = MediaType.APPLICATION_PDF_VALUE)\\n public void exportPdf(\\n @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,\\n @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,\\n HttpServletResponse response) throws IOException {\\n \\n response.setHeader(\\"Content-Disposition\\", \\"attachment; filename=report.pdf\\");\\n \\n ReportRequest request = new ReportRequest();\\n request.setStartDate(startDate);\\n request.setEndDate(endDate);\\n \\n pdfExporter.exportReport(request, response.getOutputStream());\\n }\\n \\n @GetMapping(value = \\"/export/excel\\", \\n produces = \\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\\")\\n public void exportExcel(\\n @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,\\n @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,\\n HttpServletResponse response) throws IOException {\\n \\n response.setHeader(\\"Content-Disposition\\", \\"attachment; filename=report.xlsx\\");\\n \\n ReportRequest request = new ReportRequest();\\n request.setStartDate(startDate);\\n request.setEndDate(endDate);\\n \\n excelExporter.exportReport(request, response.getOutputStream());\\n }\\n}\\n
\\n责任链模式为请求创建了一个接收者对象的链,请求会沿着这条链传递,直到有一个对象处理它为止。
\\nSpringBoot中的Filter链就是责任链模式的应用,多个Filter依次处理请求。
\\n// 抽象处理器\\npublic abstract class PaymentHandler {\\n \\n protected PaymentHandler nextHandler;\\n \\n public void setNext(PaymentHandler handler) {\\n this.nextHandler = handler;\\n }\\n \\n public abstract PaymentResponse handle(PaymentRequest request);\\n}\\n\\n// 验证处理器\\n@Component\\npublic class ValidationHandler extends PaymentHandler {\\n \\n @Override\\n public PaymentResponse handle(PaymentRequest request) {\\n // 验证支付请求\\n if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {\\n return new PaymentResponse(false, \\"Payment amount must be greater than zero\\");\\n }\\n \\n if (request.getCardNumber() == null || request.getCardNumber().isEmpty()) {\\n return new PaymentResponse(false, \\"Card number is required\\");\\n }\\n \\n if (request.getCardNumber().length() < 13 || request.getCardNumber().length() > 19) {\\n return new PaymentResponse(false, \\"Invalid card number length\\");\\n }\\n \\n if (request.getExpiryDate() == null) {\\n return new PaymentResponse(false, \\"Expiry date is required\\");\\n }\\n \\n if (request.getExpiryDate().isBefore(YearMonth.now())) {\\n return new PaymentResponse(false, \\"Card has expired\\");\\n }\\n \\n // 验证通过,继续下一个处理器\\n if (nextHandler != null) {\\n return nextHandler.handle(request);\\n }\\n \\n return new PaymentResponse(true, \\"Validation successful\\");\\n }\\n}\\n\\n// 欺诈检测处理器\\n@Component\\npublic class FraudDetectionHandler extends PaymentHandler {\\n \\n @Autowired\\n private FraudDetectionService fraudService;\\n \\n @Override\\n public PaymentResponse handle(PaymentRequest request) {\\n // 检查是否存在欺诈风险\\n FraudCheckResult checkResult = fraudService.checkForFraud(\\n request.getCardNumber(), \\n request.getAmount(),\\n request.getIpAddress());\\n \\n if (checkResult.isFraudulent()) {\\n return new PaymentResponse(false, \\"Transaction flagged as potentially fraudulent: \\" + \\n checkResult.getReason());\\n }\\n \\n // 欺诈检查通过,继续下一个处理器\\n if (nextHandler != null) {\\n return nextHandler.handle(request);\\n }\\n \\n return new PaymentResponse(true, \\"Fraud check passed\\");\\n }\\n}\\n\\n// 支付处理器\\n@Component\\npublic class PaymentProcessingHandler extends PaymentHandler {\\n \\n @Autowired\\n private PaymentGateway paymentGateway;\\n \\n @Override\\n public PaymentResponse handle(PaymentRequest request) {\\n // 实际执行支付\\n PaymentGatewayResponse gatewayResponse = paymentGateway.processPayment(\\n request.getCardNumber(),\\n request.getExpiryDate(),\\n request.getCvv(),\\n request.getAmount());\\n \\n if (!gatewayResponse.isSuccessful()) {\\n return new PaymentResponse(false, \\"Payment failed: \\" + gatewayResponse.getMessage());\\n }\\n \\n // 支付成功,继续下一个处理器\\n if (nextHandler != null) {\\n PaymentResponse nextResponse = nextHandler.handle(request);\\n \\n // 如果下一环节失败,需要进行退款\\n if (!nextResponse.isSuccess()) {\\n paymentGateway.refund(gatewayResponse.getTransactionId(), request.getAmount());\\n return nextResponse;\\n }\\n \\n // 添加交易ID到响应\\n nextResponse.setTransactionId(gatewayResponse.getTransactionId());\\n return nextResponse;\\n }\\n \\n return new PaymentResponse(true, \\"Payment processed successfully\\", \\n gatewayResponse.getTransactionId());\\n }\\n}\\n\\n// 通知处理器\\n@Component\\npublic class NotificationHandler extends PaymentHandler {\\n \\n @Autowired\\n private NotificationService notificationService;\\n \\n @Override\\n public PaymentResponse handle(PaymentRequest request) {\\n // 发送支付成功通知\\n notificationService.sendPaymentConfirmation(\\n request.getEmail(),\\n request.getAmount(),\\n LocalDateTime.now());\\n \\n // 继续下一个处理器\\n if (nextHandler != null) {\\n return nextHandler.handle(request);\\n }\\n \\n return new PaymentResponse(true, \\"Payment completed and notification sent\\");\\n }\\n}\\n\\n// 责任链配置\\n@Configuration\\npublic class PaymentHandlerConfig {\\n \\n @Bean\\n public PaymentHandler paymentHandlerChain(\\n ValidationHandler validationHandler,\\n FraudDetectionHandler fraudDetectionHandler,\\n PaymentProcessingHandler paymentProcessingHandler,\\n NotificationHandler notificationHandler) {\\n \\n // 构建处理链\\n validationHandler.setNext(fraudDetectionHandler);\\n fraudDetectionHandler.setNext(paymentProcessingHandler);\\n paymentProcessingHandler.setNext(notificationHandler);\\n \\n // 返回链的第一个处理器\\n return validationHandler;\\n }\\n}\\n\\n// 支付服务\\n@Service\\npublic class PaymentService {\\n \\n private final PaymentHandler paymentHandlerChain;\\n \\n @Autowired\\n public PaymentService(PaymentHandler paymentHandlerChain) {\\n this.paymentHandlerChain = paymentHandlerChain;\\n }\\n \\n public PaymentResponse processPayment(PaymentRequest request) {\\n // 启动责任链处理\\n return paymentHandlerChain.handle(request);\\n }\\n}\\n
\\n命令模式将请求封装成对象,使发出请求的责任和执行请求的责任分割开,支持请求排队、回退等功能。
\\n在SpringBoot应用的事件处理、任务调度中经常使用命令模式。
\\n// 命令接口\\npublic interface Command {\\n void execute();\\n void undo();\\n String getDescription();\\n}\\n\\n// 具体命令 - 创建订单\\n@Component\\npublic class CreateOrderCommand implements Command {\\n \\n private final OrderService orderService;\\n private final OrderRepository orderRepository;\\n \\n private Order createdOrder;\\n private final Order orderToCreate;\\n \\n public CreateOrderCommand(\\n OrderService orderService,\\n OrderRepository orderRepository,\\n Order orderToCreate) {\\n this.orderService = orderService;\\n this.orderRepository = orderRepository;\\n this.orderToCreate = orderToCreate;\\n }\\n \\n @Override\\n public void execute() {\\n createdOrder = orderService.createOrder(orderToCreate);\\n }\\n \\n @Override\\n public void undo() {\\n if (createdOrder != null) {\\n orderRepository.delete(createdOrder);\\n createdOrder = null;\\n }\\n }\\n \\n @Override\\n public String getDescription() {\\n return \\"Create order for customer: \\" + orderToCreate.getCustomerId();\\n }\\n}\\n\\n// 具体命令 - 扣减库存\\n@Component\\npublic class DeductInventoryCommand implements Command {\\n \\n private final InventoryService inventoryService;\\n \\n private final Long productId;\\n private final int quantity;\\n private boolean executed = false;\\n \\n public DeductInventoryCommand(\\n InventoryService inventoryService,\\n Long productId,\\n int quantity) {\\n this.inventoryService = inventoryService;\\n this.productId = productId;\\n this.quantity = quantity;\\n }\\n \\n @Override\\n public void execute() {\\n inventoryService.deductStock(productId, quantity);\\n executed = true;\\n }\\n \\n @Override\\n public void undo() {\\n if (executed) {\\n inventoryService.addStock(productId, quantity);\\n executed = false;\\n }\\n }\\n \\n @Override\\n public String getDescription() {\\n return \\"Deduct \\" + quantity + \\" units from product: \\" + productId;\\n }\\n}\\n\\n// 具体命令 - 处理支付\\n@Component\\npublic class ProcessPaymentCommand implements Command {\\n \\n private final PaymentService paymentService;\\n \\n private final PaymentRequest paymentRequest;\\n private String transactionId;\\n \\n public ProcessPaymentCommand(\\n PaymentService paymentService,\\n PaymentRequest paymentRequest) {\\n this.paymentService = paymentService;\\n this.paymentRequest = paymentRequest;\\n }\\n \\n @Override\\n public void execute() {\\n PaymentResponse response = paymentService.processPayment(paymentRequest);\\n \\n if (!response.isSuccess()) {\\n throw new PaymentFailedException(response.getMessage());\\n }\\n \\n this.transactionId = response.getTransactionId();\\n }\\n \\n @Override\\n public void undo() {\\n if (transactionId != null) {\\n paymentService.refundPayment(transactionId);\\n transactionId = null;\\n }\\n }\\n \\n @Override\\n public String getDescription() {\\n return \\"Process payment of \\" + paymentRequest.getAmount() + \\n \\" for order: \\" + paymentRequest.getOrderId();\\n }\\n}\\n\\n// 命令历史记录\\n@Component\\npublic class CommandHistory {\\n \\n private final Deque<Command> history = new ArrayDeque<>();\\n \\n public void push(Command command) {\\n history.push(command);\\n }\\n \\n public Command pop() {\\n return history.isEmpty() ? null : history.pop();\\n }\\n \\n public boolean isEmpty() {\\n return history.isEmpty();\\n }\\n \\n public List<Command> getExecutedCommands() {\\n return new ArrayList<>(history);\\n }\\n}\\n\\n// 命令执行器\\n@Service\\npublic class CommandInvoker {\\n \\n private final CommandHistory history;\\n private final TransactionTemplate transactionTemplate;\\n \\n @Autowired\\n public CommandInvoker(\\n CommandHistory history,\\n PlatformTransactionManager transactionManager) {\\n this.history = history;\\n this.transactionTemplate = new TransactionTemplate(transactionManager);\\n }\\n \\n public void executeCommand(Command command) {\\n transactionTemplate.execute(status -> {\\n try {\\n command.execute();\\n history.push(command);\\n return null;\\n } catch (Exception e) {\\n status.setRollbackOnly();\\n throw e;\\n }\\n });\\n }\\n \\n public void executeCommands(List<Command> commands) {\\n transactionTemplate.execute(status -> {\\n List<Command> executedCommands = new ArrayList<>();\\n \\n try {\\n for (Command command : commands) {\\n command.execute();\\n executedCommands.add(command);\\n }\\n \\n // 所有命令执行成功后添加到历史记录\\n for (Command command : executedCommands) {\\n history.push(command);\\n }\\n \\n return null;\\n } catch (Exception e) {\\n // 出现异常,回滚已执行的命令\\n for (int i = executedCommands.size() - 1; i >= 0; i--) {\\n executedCommands.get(i).undo();\\n }\\n \\n status.setRollbackOnly();\\n throw e;\\n }\\n });\\n }\\n \\n public void undoLastCommand() {\\n Command command = history.pop();\\n \\n if (command != null) {\\n transactionTemplate.execute(status -> {\\n try {\\n command.undo();\\n return null;\\n } catch (Exception e) {\\n status.setRollbackOnly();\\n // 撤销失败,将命令重新放回历史\\n history.push(command);\\n throw e;\\n }\\n });\\n }\\n }\\n}\\n\\n// 订单处理服务\\n@Service\\npublic class OrderProcessingService {\\n \\n private final CommandInvoker commandInvoker;\\n private final OrderService orderService;\\n private final InventoryService inventoryService;\\n private final PaymentService paymentService;\\n \\n @Autowired\\n public OrderProcessingService(\\n CommandInvoker commandInvoker,\\n OrderService orderService,\\n InventoryService inventoryService,\\n PaymentService paymentService) {\\n this.commandInvoker = commandInvoker;\\n this.orderService = orderService;\\n this.inventoryService = inventoryService;\\n this.paymentService = paymentService;\\n }\\n \\n public Order placeOrder(OrderRequest orderRequest) {\\n // 准备订单数据\\n Order order = new Order();\\n order.setCustomerId(orderRequest.getCustomerId());\\n order.setItems(orderRequest.getItems());\\n order.setTotalAmount(calculateTotal(orderRequest.getItems()));\\n \\n // 创建支付请求\\n PaymentRequest paymentRequest = new PaymentRequest();\\n paymentRequest.setAmount(order.getTotalAmount());\\n paymentRequest.setCardNumber(orderRequest.getPaymentDetails().getCardNumber());\\n paymentRequest.setExpiryDate(orderRequest.getPaymentDetails().getExpiryDate());\\n paymentRequest.setCvv(orderRequest.getPaymentDetails().getCvv());\\n \\n // 创建命令列表\\n List<Command> commands = new ArrayList<>();\\n \\n // 1. 创建订单命令\\n Command createOrderCommand = new CreateOrderCommand(orderService, orderService.getRepository(), order);\\n commands.add(createOrderCommand);\\n \\n // 2. 扣减库存命令\\n for (OrderItem item : order.getItems()) {\\n Command deductInventoryCommand = new DeductInventoryCommand(\\n inventoryService, \\n item.getProductId(), \\n item.getQuantity());\\n commands.add(deductInventoryCommand);\\n }\\n \\n // 3. 处理支付命令\\n Command processPaymentCommand = new ProcessPaymentCommand(paymentService, paymentRequest);\\n commands.add(processPaymentCommand);\\n \\n // 执行命令序列\\n commandInvoker.executeCommands(commands);\\n \\n return order;\\n }\\n \\n private BigDecimal calculateTotal(List<OrderItem> items) {\\n return items.stream()\\n .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))\\n .reduce(BigDecimal.ZERO, BigDecimal::add);\\n }\\n}\\n
\\n状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
\\n在业务流程处理、订单状态管理等场景常用状态模式。
\\n// 订单状态接口\\npublic interface OrderState {\\n OrderState confirm(Order order);\\n OrderState pay(Order order);\\n OrderState ship(Order order);\\n OrderState deliver(Order order);\\n OrderState cancel(Order order);\\n OrderState refund(Order order);\\n String getStatus();\\n}\\n\\n// 具体状态 - 新建\\n@Component\\npublic class NewOrderState implements OrderState {\\n \\n @Autowired\\n private ConfirmedOrderState confirmedOrderState;\\n \\n @Autowired\\n private CancelledOrderState cancelledOrderState;\\n \\n @Override\\n public OrderState confirm(Order order) {\\n // 执行确认逻辑\\n order.setConfirmedAt(LocalDateTime.now());\\n return confirmedOrderState;\\n }\\n \\n @Override\\n public OrderState pay(Order order) {\\n throw new IllegalStateException(\\"Cannot pay for an order that has not been confirmed\\");\\n }\\n \\n @Override\\n public OrderState ship(Order order) {\\n throw new IllegalStateException(\\"Cannot ship an order that has not been confirmed and paid\\");\\n }\\n \\n @Override\\n public OrderState deliver(Order order) {\\n throw new IllegalStateException(\\"Cannot deliver an order that has not been shipped\\");\\n }\\n \\n @Override\\n public OrderState cancel(Order order) {\\n // 执行取消逻辑\\n order.setCancelledAt(LocalDateTime.now());\\n order.setCancellationReason(\\"Cancelled by customer before confirmation\\");\\n return cancelledOrderState;\\n }\\n \\n @Override\\n public OrderState refund(Order order) {\\n throw new IllegalStateException(\\"Cannot refund an order that has not been paid\\");\\n }\\n \\n @Override\\n public String getStatus() {\\n return \\"NEW\\";\\n }\\n}\\n\\n// 具体状态 - 已确认\\n@Component\\npublic class ConfirmedOrderState implements OrderState {\\n \\n @Autowired\\n private PaidOrderState paidOrderState;\\n \\n @Autowired\\n private CancelledOrderState cancelledOrderState;\\n \\n @Override\\n public OrderState confirm(Order order) {\\n throw new IllegalStateException(\\"Order is already confirmed\\");\\n }\\n \\n @Override\\n public OrderState pay(Order order) {\\n // 执行支付逻辑\\n order.setPaidAt(LocalDateTime.now());\\n return paidOrderState;\\n }\\n \\n @Override\\n public OrderState ship(Order order) {\\n throw new IllegalStateException(\\"Cannot ship an order that has not been paid\\");\\n }\\n \\n @Override\\n public OrderState deliver(Order order) {\\n throw new IllegalStateException(\\"Cannot deliver an order that has not been shipped\\");\\n }\\n \\n @Override\\n public OrderState cancel(Order order) {\\n // 执行取消逻辑\\n order.setCancelledAt(LocalDateTime.now());\\n order.setCancellationReason(\\"Cancelled by customer after confirmation\\");\\n return cancelledOrderState;\\n }\\n \\n @Override\\n public OrderState refund(Order order) {\\n throw new IllegalStateException(\\"Cannot refund an order that has not been paid\\");\\n }\\n \\n @Override\\n public String getStatus() {\\n return \\"CONFIRMED\\";\\n }\\n}\\n\\n// 具体状态 - 已支付\\n@Component\\npublic class PaidOrderState implements OrderState {\\n \\n @Autowired\\n private ShippedOrderState shippedOrderState;\\n \\n @Autowired\\n private RefundedOrderState refundedOrderState;\\n \\n @Override\\n public OrderState confirm(Order order) {\\n throw new IllegalStateException(\\"Order is already confirmed\\");\\n }\\n \\n @Override\\n public OrderState pay(Order order) {\\n throw new IllegalStateException(\\"Order is already paid\\");\\n }\\n \\n @Override\\n public OrderState ship(Order order) {\\n // 执行发货逻辑\\n order.setShippedAt(LocalDateTime.now());\\n return shippedOrderState;\\n }\\n \\n @Override\\n public OrderState deliver(Order order) {\\n throw new IllegalStateException(\\"Cannot deliver an order that has not been shipped\\");\\n }\\n \\n @Override\\n public OrderState cancel(Order order) {\\n throw new IllegalStateException(\\"Cannot cancel an order that has been paid, please request a refund\\");\\n }\\n \\n @Override\\n public OrderState refund(Order order) {\\n // 执行退款逻辑\\n order.setRefundedAt(LocalDateTime.now());\\n return refundedOrderState;\\n }\\n \\n @Override\\n public String getStatus() {\\n return \\"PAID\\";\\n }\\n}\\n\\n// 更多状态类实现...\\n\\n// 订单状态上下文类\\n@Entity\\n@Table(name = \\"orders\\")\\n@Data\\npublic class Order {\\n \\n @Id\\n @GeneratedValue(strategy = GenerationType.IDENTITY)\\n private Long id;\\n \\n private Long customerId;\\n \\n @OneToMany(cascade = CascadeType.ALL)\\n private List<OrderItem> items;\\n \\n private BigDecimal totalAmount;\\n \\n private LocalDateTime createdAt = LocalDateTime.now();\\n \\n private LocalDateTime confirmedAt;\\n \\n private LocalDateTime paidAt;\\n \\n private LocalDateTime shippedAt;\\n \\n private LocalDateTime deliveredAt;\\n \\n private LocalDateTime cancelledAt;\\n \\n private String cancellationReason;\\n \\n private LocalDateTime refundedAt;\\n \\n @Transient\\n private OrderState currentState;\\n \\n @Column(name = \\"status\\")\\n private String status = \\"NEW\\";\\n \\n @PostLoad\\n private void onLoad() {\\n initState();\\n }\\n \\n private void initState() {\\n if (status == null) {\\n status = \\"NEW\\";\\n }\\n \\n switch (status) {\\n case \\"NEW\\":\\n currentState = SpringContextHolder.getBean(NewOrderState.class);\\n break;\\n case \\"CONFIRMED\\":\\n currentState = SpringContextHolder.getBean(ConfirmedOrderState.class);\\n break;\\n case \\"PAID\\":\\n currentState = SpringContextHolder.getBean(PaidOrderState.class);\\n break;\\n case \\"SHIPPED\\":\\n currentState = SpringContextHolder.getBean(ShippedOrderState.class);\\n break;\\n case \\"DELIVERED\\":\\n currentState = SpringContextHolder.getBean(DeliveredOrderState.class);\\n break;\\n case \\"CANCELLED\\":\\n currentState = SpringContextHolder.getBean(CancelledOrderState.class);\\n break;\\n case \\"REFUNDED\\":\\n currentState = SpringContextHolder.getBean(RefundedOrderState.class);\\n break;\\n default:\\n throw new IllegalStateException(\\"Unknown order status: \\" + status);\\n }\\n }\\n \\n // 状态转换方法\\n public void confirm() {\\n currentState = currentState.confirm(this);\\n status = currentState.getStatus();\\n }\\n \\n public void pay() {\\n currentState = currentState.pay(this);\\n status = currentState.getStatus();\\n }\\n \\n public void ship() {\\n currentState = currentState.ship(this);\\n status = currentState.getStatus();\\n }\\n \\n public void deliver() {\\n currentState = currentState.deliver(this);\\n status = currentState.getStatus();\\n }\\n \\n public void cancel() {\\n currentState = currentState.cancel(this);\\n status = currentState.getStatus();\\n }\\n \\n public void refund() {\\n currentState = currentState.refund(this);\\n status = currentState.getStatus();\\n }\\n}\\n\\n// 订单服务\\n@Service\\npublic class OrderService {\\n \\n @Autowired\\n private OrderRepository orderRepository;\\n \\n @Autowired\\n private NewOrderState initialState;\\n \\n @Transactional\\n public Order createOrder(Long customerId, List<OrderItem> items, BigDecimal totalAmount) {\\n Order order = new Order();\\n order.setCustomerId(customerId);\\n order.setItems(items);\\n order.setTotalAmount(totalAmount);\\n order.setCurrentState(initialState);\\n \\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order confirmOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.confirm();\\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order payOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.pay();\\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order shipOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.ship();\\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order deliverOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.deliver();\\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order cancelOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.cancel();\\n return orderRepository.save(order);\\n }\\n \\n @Transactional\\n public Order refundOrder(Long orderId) {\\n Order order = findOrderById(orderId);\\n order.refund();\\n return orderRepository.save(order);\\n }\\n \\n private Order findOrderById(Long orderId) {\\n return orderRepository.findById(orderId)\\n .orElseThrow(() -> new OrderNotFoundException(\\"Order not found: \\" + orderId));\\n }\\n}\\n
\\n@Component\\npublic class SpringContextHolder implements ApplicationContextAware {\\n \\n private static ApplicationContext context;\\n \\n @Override\\n public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\\n context = applicationContext;\\n }\\n \\n public static <T> T getBean(Class<T> clazz) {\\n return context.getBean(clazz);\\n }\\n \\n public static Object getBean(String name) {\\n return context.getBean(name);\\n }\\n}\\n
\\n在实际开发中,这些设计模式往往不是孤立使用的,而是相互配合,共同解决复杂的业务问题。掌握这些设计模式及其应用场景,能够帮助开发者设计出更加灵活、可维护、可扩展的应用。
\\n最后,设计模式是工具而非目的,应根据实际问题选择合适的模式,避免过度设计。合理运用设计模式,才能真正提升代码质量和开发效率。
","description":"设计模式是软件开发中解决特定问题的经验总结。在SpringBoot框架中,设计模式被巧妙地融入各个环节,不仅提高了框架的灵活性和扩展性,还为开发者提供了优雅的解决方案。 本文将介绍13种设计模式在SpringBoot中的实际应用,每个模式都配有具体的代码实现和应用场景。\\n\\n1. 单例模式 (Singleton Pattern)\\n模式概述\\n\\n单例模式确保一个类只有一个实例,并提供一个全局访问点。\\n\\nSpringBoot应用\\n\\nSpringBoot中的Bean默认都是单例的,由Spring容器负责创建和管理,保证全局唯一性。\\n\\n实现示例\\n@Service\\npublic…","guid":"https://juejin.cn/post/7498709492122665014","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T21:41:31.104Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"日志框架简介-Slf4j+Logback入门实践","url":"https://juejin.cn/post/7498737799202734106","content":"作者:京东零售 张洪
\\n随着互联网和大数据的迅猛发展,分布式日志系统和日志分析系统已广泛应用,几乎所有应用程序都使用各种日志框架记录程序运行信息。因此,作为工程师,了解主流的日志记录框架非常重要。虽然应用程序的运行结果不受日志的有无影响,但没有日志的应用程序是不完整的,甚至可以说是有缺陷的。优秀的日志系统可以记录操作轨迹、监控系统运行状态和解决系统故障。
\\n\ufeff
\\n早期 Java 日志框架没有制定统一的标准,使得很多应用程序会同时使用多种日志框架。Java 日志框架的发展历程大致可分为以下几个阶段:
\\n\ufeff
\\n\ufeff\ufeff
\\n1.Log4j: Apache Log4j是一种基于Java的日志记录工具。该项目由Ceki Gülcü于1999年创建,并几乎成为了Java日志框架的实际标准。
\\n2.JUL: Apache 希望将 Log4j 引入 jdk,不过被 sun 公司拒绝了。随后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
\\n3.Commons Logging: 为了解耦日志接口与实现,Apache在2002年推出了JCL(Jakarta Commons Logging)。JCL定义了一套日志接口,具体的实现由Log4j或JUL完成。Commons Logging使用动态绑定来实现日志记录,编码时只需要使用它定义的接口即可,程序运行时会使用ClassLoader来查找和加载底层的日志库,因此可以灵活选择Log4j或JUL来实现日志功能。
\\n4.Slf4j&Logback: Ceki Gülcü与Apache基金会在Commons-Logging标准上存在分歧。后来,Ceki Gülcü离开了Apache,并创建了Slf4j和Logback两个项目。Slf4j是一个日志门面,仅提供接口,可以支持Logback、JUL、log4j等日志实现。而Logback则提供了具体的实现。相比于log4j,Logback具有更快的执行速度和更完善的功能。
\\n5.Log4j 2: 为了保持在Java日志领域的地位,防止JCL和Log4j被Slf4j和Logback取代,Apache在2014年推出了Log4j 2。Log4j 2与log4j不兼容,经过大量深度优化,其性能得到显著提升。
\\n\ufeff
\\n在上文中已经提及,目前常用的日志框架有 Log4j,Log4j 2,Commons Logging,Slf4j,Logback,JUL。这些日志框架可以分为两种类型:门面日志和日志系统。
\\n日志门面(Logging Facade) 是一种设计模式,用于在应用程序中实现日志记录的抽象层。它提供了一组统一的接口和方法,即相应的 API,而不提供具体的接口实现。日志门面在使用时,可以动态或者静态地指定具体的日志框架实现,解除了接口和实现的耦合,使用户可以灵活地选择日志的具体实现框架。
\\n日志系统(Logging System) 是指用于记录和管理应用程序运行时产生的日志信息的软件工具或框架。与日志门面相对,它提供了具体的日志接口实现,应用程序通过它执行日志打印的功能,如日志级别管理、日志格式化、日志输出目标设置等。常见的日志系统包括Log4j、Logback、Java Util Logging等。
\\n\ufeff
\\n\ufeff\ufeff
\\n通过使用日志门面,我们可以在应用程序中使用统一的API进行日志记录,而具体的日志实现可以根据需要选择和配置。这样,我们可以根据项目需求和团队喜好来灵活选择、切换和配置日志系统,而不会对应用程序代码造成太大影响。
\\nSlf4j 的作者 Ceki Gülcü 当年因为觉得 Commons-Logging 的 API 设计的不好,性能也不够高,因而设计了 Slf4j。而他为了 Slf4j 能够兼容各种类型的日志系统实现,还设计了相当多的 adapter 和 bridge 来连接,如下图所示:
\\n\ufeff
\\n\ufeff\ufeff
\\n鉴于此,在引入日志框架依赖的时候要尽力避免,比如以下组合就不能同时出现:
\\n•jcl-over-slf4j 和 slf4j-jcl
\\n•log4j-over-slf4j 和 slf4j-log4j12
\\n•jul-to-slf4j 和 slf4j-jdk14
\\n常用的组合使用方式是 Slf4j & Logback 组合使用,Commons Logging & Log4j 组合使用。
\\n推荐:
\\nSlf4j & Logback
\\n原因:
\\nSlf4j 实现机制决定 Slf4j 限制较少,使用范围更广。相较于 Commons-Logging,Slf4j 在编译期间便静态绑定本地的 Log 库,其通用性要好得多;
\\nLogback 拥有更好的性能。Logback 声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高,这个操作在 Logback 中只需 3 ns,而在 Log4j 则需要 30 ns;
\\nSlf4j 支持参数化,使用占位符号,代码更为简洁,如下例子:
\\n// 在使用 Commons-Logging 时,通常的做法是 \\nif(log.isDebugEnabled()){ \\n log.debug(\\"User name: \\" + user.getName() + \\" buy goods id :\\" + good.getId()); \\n} \\n\\n// 在 Slf4j 阵营,你只需这么做: \\nlog.debug(\\"User name:{} ,buy goods id :{}\\", user.getName(),good.getId());\\n
\\n4. Logback 的所有文档是免费提供的,Log4j 只提供部分免费文档而需要用户去购买付费文档;
\\nMDC (Mapped Diagnostic Contexts) 用 Filter,将当前用户名等业务信息放入 MDC 中,在日志 format 定义中即可使用该变量。具体而言,在诊断问题时,通常需要打出日志。如果使用 Log4j,则只能降低日志级别,但是这样会打出大量的日志,影响应用性能;如果使用 Logback,保持原定日志级别而过滤某种特殊情况,如 Alice 这个用户登录,日志将打在 DEBUG 级别而其它用户可以继续打在 WARN 级别。实现这个功能只需加 4 行 XML 配置;
\\n自动压缩日志。RollingFileAppender 在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩过程是异步的,因此在压缩过程中应用几乎不会受影响。
\\n\ufeff
\\npom.xml
\\n<!--日志框架接口--\x3e\\n<dependency>\\n <groupId>org.slf4j</groupId>\\n <artifactId>slf4j-api</artifactId>\\n</dependency>\\n<!--日志框架接口实现--\x3e\\n<dependency>\\n <groupId>ch.qos.logback</groupId>\\n <artifactId>logback-classic</artifactId>\\n</dependency>\\n<!--日志框架核心组件--\x3e\\n<dependency>\\n <groupId>ch.qos.logback</groupId>\\n <artifactId>logback-core</artifactId>\\n</dependency>\\n\\n<!--自动化注解工具--\x3e\\n<dependency>\\n <groupId>org.projectlombok</groupId>\\n <artifactId>lombok</artifactId>\\n <version>1.18.16</version>\\n</dependency>\\n
\\nlogback.xml
\\n<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<configuration>\\n\\n <!--默认日志配置--\x3e\\n <include resource=\\"org/springframework/boot/logging/logback/defaults.xml\\"/>\\n\\n <!-- 控制台日志 --\x3e\\n <appender name=\\"CONSOLE\\" class=\\"ch.qos.logback.core.ConsoleAppender\\">\\n <encoder charset=\\"UTF-8\\">\\n <pattern>${CONSOLE_LOG_PATTERN}</pattern>\\n </encoder>\\n </appender>\\n\\n <!-- Info日志 --\x3e\\n <appender name=\\"FILE-INFO\\" class=\\"ch.qos.logback.core.rolling.RollingFileAppender\\">\\n <file>${LOG_PATH}/${LOG_FILE}-info.log</file>\\n <append>true</append>\\n <filter class=\\"ch.qos.logback.classic.filter.LevelFilter\\">\\n <level>INFO</level>\\n <onMatch>ACCEPT</onMatch>\\n <onMismatch>NEUTRAL</onMismatch>\\n </filter>\\n <rollingPolicy class=\\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\\">\\n <fileNamePattern>${LOG_PATH}/${LOG_FILE}-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 日志文件的路径和名称 --\x3e\\n <timeBasedFileNamingAndTriggeringPolicy class=\\"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP\\">\\n <maxFileSize>200MB</maxFileSize> <!-- 单个日志文件的最大大小 --\x3e\\n </timeBasedFileNamingAndTriggeringPolicy>\\n <maxHistory>15</maxHistory> <!-- 保留的历史日志文件数量 --\x3e\\n <totalSizeCap>2GB</totalSizeCap> <!-- 所有日志文件的总大小上限 --\x3e\\n <cleanHistoryOnStart>true</cleanHistoryOnStart> <!-- 在启动时清除历史日志文件 --\x3e\\n </rollingPolicy>\\n <encoder charset=\\"UTF-8\\">\\n <pattern>${FILE_LOG_PATTERN}</pattern>\\n </encoder>\\n </appender>\\n\\n <!-- Warn日志 --\x3e\\n <appender name=\\"FILE-WARN\\" class=\\"ch.qos.logback.core.rolling.RollingFileAppender\\">\\n <file>${LOG_PATH}/${LOG_FILE}-warn.log</file>\\n <append>true</append>\\n <filter class=\\"ch.qos.logback.classic.filter.LevelFilter\\">\\n <level>WARN</level>\\n <onMatch>ACCEPT</onMatch>\\n <onMismatch>DENY</onMismatch>\\n </filter>\\n <rollingPolicy class=\\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\\">\\n <fileNamePattern>${LOG_PATH}/${LOG_FILE}-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 日志文件的路径和名称 --\x3e\\n <timeBasedFileNamingAndTriggeringPolicy class=\\"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP\\">\\n <maxFileSize>200MB</maxFileSize> <!-- 单个日志文件的最大大小 --\x3e\\n </timeBasedFileNamingAndTriggeringPolicy>\\n <maxHistory>15</maxHistory> <!-- 保留的历史日志文件数量 --\x3e\\n <totalSizeCap>2GB</totalSizeCap> <!-- 所有日志文件的总大小上限 --\x3e\\n <cleanHistoryOnStart>true</cleanHistoryOnStart> <!-- 在启动时清除历史日志文件 --\x3e\\n </rollingPolicy>\\n <encoder charset=\\"UTF-8\\">\\n <pattern>${FILE_LOG_PATTERN}</pattern>\\n </encoder>\\n </appender>\\n\\n <!-- Error日志 --\x3e\\n <appender name=\\"FILE-ERROR\\" class=\\"ch.qos.logback.core.rolling.RollingFileAppender\\">\\n <file>${LOG_PATH}/${LOG_FILE}-error.log</file>\\n <append>true</append>\\n <filter class=\\"ch.qos.logback.classic.filter.LevelFilter\\">\\n <level>ERROR</level>\\n <onMatch>ACCEPT</onMatch>\\n <onMismatch>DENY</onMismatch>\\n </filter>\\n <rollingPolicy class=\\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\\">\\n <fileNamePattern>${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>\\n <timeBasedFileNamingAndTriggeringPolicy class=\\"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP\\">\\n <maxFileSize>200MB</maxFileSize>\\n </timeBasedFileNamingAndTriggeringPolicy>\\n <maxHistory>15</maxHistory>\\n <totalSizeCap>2GB</totalSizeCap>\\n <cleanHistoryOnStart>true</cleanHistoryOnStart>\\n </rollingPolicy>\\n <encoder charset=\\"UTF-8\\">\\n <pattern>${FILE_LOG_PATTERN}</pattern>\\n </encoder>\\n </appender>\\n\\n <!-- 异步输出 --\x3e\\n <appender name=\\"info-asyn\\" class=\\"ch.qos.logback.classic.AsyncAppender\\">\\n <appender-ref ref=\\"FILE-INFO\\"/>\\n <queueSize>512</queueSize> <!-- 异步队列的大小 --\x3e\\n </appender>\\n <appender name=\\"warn-asyn\\" class=\\"ch.qos.logback.classic.AsyncAppender\\">\\n <appender-ref ref=\\"FILE-WARN\\"/>\\n <queueSize>512</queueSize> <!-- 异步队列的大小 --\x3e\\n </appender>\\n <appender name=\\"error-asyn\\" class=\\"ch.qos.logback.classic.AsyncAppender\\">\\n <appender-ref ref=\\"FILE-ERROR\\"/>\\n <queueSize>512</queueSize>\\n </appender>\\n\\n <!-- 应用日志 --\x3e\\n <logger name=\\"com.improve.fuqige.bronze\\" additivity=\\"false\\">\\n <appender-ref ref=\\"CONSOLE\\"/>\\n <appender-ref ref=\\"FILE-INFO\\"/>\\n <appender-ref ref=\\"FILE-WARN\\"/>\\n <appender-ref ref=\\"FILE-ERROR\\"/>\\n </logger>\\n\\n\\n <!-- 总日志出口 --\x3e\\n <root level=\\"${logging.level.root}\\">\\n <appender-ref ref=\\"CONSOLE\\"/>\\n <appender-ref ref=\\"info-asyn\\"/>\\n <appender-ref ref=\\"warn-asyn\\"/>\\n <appender-ref ref=\\"error-asyn\\"/>\\n </root>\\n</configuration>\\n
\\napplicantion.properties
\\nlogging.file=fuqige-bronze\\nlogging.path=XXXXXX/Logs/XXXXXX\\nlogging.level.root=info\\nlogging.level.com.improve.fuqige.bronze=info\\nlogging.pattern.console=%cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) %yellow([%thread]) %highlight(%-5level) %boldGreen(%logger{80}[LineNumber:%L]): %highlight(%msg%n)\\nlogging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] %-5level --- [%thread] %logger{80}[LineNumber:%L]: %msg%n\\n
\\n@Slf4j\\n@RestController\\n@RequestMapping(\\"/test\\")\\npublic class TestController {\\n\\n @GetMapping(\\"/hello\\")\\n public String hello() {\\n log.info(\\"进来了!\\");\\n log.warn(\\"进来了!\\");\\n log.error(\\"进来了!\\");\\n return \\"hello, world! requestId=\\" + MDC.get(\\"requestId\\");\\n }\\n}\\n
","description":"作者:京东零售 张洪 前言\\n\\n随着互联网和大数据的迅猛发展,分布式日志系统和日志分析系统已广泛应用,几乎所有应用程序都使用各种日志框架记录程序运行信息。因此,作为工程师,了解主流的日志记录框架非常重要。虽然应用程序的运行结果不受日志的有无影响,但没有日志的应用程序是不完整的,甚至可以说是有缺陷的。优秀的日志系统可以记录操作轨迹、监控系统运行状态和解决系统故障。\\n\\n\ufeff\\n\\nJava 日志框架进化史\\n\\n早期 Java 日志框架没有制定统一的标准,使得很多应用程序会同时使用多种日志框架。Java 日志框架的发展历程大致可分为以下几个阶段:\\n\\n\ufeff\\n\\n\ufeff\ufeff\\n\\n1.Log4j:…","guid":"https://juejin.cn/post/7498737799202734106","author":"京东云开发者","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T06:19:41.444Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/92687d6d71ea4147b3573e34d77beb46~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1746598780&x-signature=MwuiiD763eQJoid0TxUrRXpOrjg%3D","type":"photo"},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/999b366f2a4444ffbbf3e88440ce9ae3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1746598780&x-signature=BjH8Kl3lpic11bLSQWT0xOkHISA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9c904f275cf4996b25eac78045911e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1746598780&x-signature=3Um6ZNTJ%2BqDntmh8nGss2ZOYz%2BQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"Java/Go双修 - Go并发Goroutine与Java对比","url":"https://juejin.cn/post/7498636615951745024","content":"协程的概念:juejin.cn/post/746181…
\\nGoroutine就是go语言对于协程的支持,go的并发只会用到goroutine,并不需要我们去考虑多进程或者多线程
\\n一般OS线程栈大小为2MB-8MB,且线程在创建和上下文切换的时候是需要消耗资源的,会带来性能损耗
\\n所以我们在使用多线程的时候,我们往往会通过池化技术,即创建线程池来管理一定数量的线程
\\n而在Go语言中,我们用多Goroutine去执行这个函数就可以了,并不需要我们来维护类似线程池的东西
\\n也不需要使用者去关心协程是怎么调度和切换的,因为这些都已经有Go语言内置的调度器帮我们做了
\\nGoroutine使用起来非常方便,通常我们会将需要并发的任务封装成一个函数,然后加上go关键字就等于开启了一个Goroutine
\\nfunc()\\ngo func()\\n
\\n和Java一样,Go程序的入口也是main函数。在程序开始执行的时候Go程序会为main()函数创建一个默认的Goroutine
\\n我们称之为主协程,我们后来认为的创建了一些Goroutine,都是在这个主Goroutine的基础上进行的
\\npackage main\\nimport \\"fmt\\"\\n\\nfunc myGroutine() {\\nfmt.Println(\\"myGroutine\\")\\n}\\nfunc main() {\\ngo myGroutine()\\nfmt.Println(\\"main end\\")\\n}\\n
\\n输出结果:
\\nmain end\\n
\\n为什么只输出main end呢?明明是多协程任务,为什么只打印了主协程里的end,没有打印我们开启的协程输出的myGroutine
\\n这是因为:当main()函数结束返回的时候主Goroutine已经结束了,当主协程退出的时候,其他剩余的协程不管是否运行完也一起退出
\\n当主协程结束时,我们开启的协程还没有执行到fmt.Println(\\"myGroutine\\"),所以我们创建的协程也就跟着退出了
\\n就是Java里面的CountDownLatch,等待所有的协程执行完毕后主协程才能走下去
\\npackage main\\n\\nimport (\\n \\"fmt\\"\\n \\"sync\\"\\n \\"time\\"\\n)\\n\\nfunc myGoroutine(name string, wg *sync.WaitGroup) {\\n defer wg.Done()\\n\\n for i := 0; i < 5; i++ {\\n fmt.Printf(\\"myGroutine %s\\\\n\\", name)\\n time.Sleep(10 * time.Millisecond)\\n }\\n}\\n\\nfunc main() {\\n var wg sync.WaitGroup\\n wg.Add(2)\\n\\n go myGoroutine(\\"goroutine1\\", &wg)\\n go myGoroutine(\\"goroutine2\\", &wg)\\n\\n wg.Wait()\\n fmt.Println(\\"main end\\")\\n}\\n
\\n输出结果:
\\nmyGoroutine goroutine2\\nmyGoroutine goroutine1\\nmyGoroutine goroutine1\\nmyGoroutine goroutine2\\nmyGoroutine goroutine1\\nmyGoroutine goroutine2\\nmyGoroutine goroutine2\\nmyGoroutine goroutine1\\nmyGoroutine goroutine1\\nmyGoroutine goroutine2\\nmain end\\n
\\nimport java.util.concurrent.ExecutorService;\\nimport java.util.concurrent.Executors;\\n\\npublic class ThreadPoolExample {\\n public static void main(String[] args) {\\n // 创建一个固定大小的线程池(5个线程)\\n ExecutorService executor = Executors.newFixedThreadPool(5);\\n \\n // 提交一个Runnable任务\\n executor.execute(() -> {\\n System.out.println(\\"任务正在执行,线程: \\" + Thread.currentThread().getName());\\n });\\n \\n }\\n}\\n
\\nimport java.util.concurrent.*;\\n\\npublic class ThreadPoolWithCountDownLatch {\\n public static void main(String[] args) throws InterruptedException {\\n // 创建线程池\\n ExecutorService executor = Executors.newFixedThreadPool(3);\\n \\n // 创建CountDownLatch,计数为5\\n CountDownLatch latch = new CountDownLatch(5);\\n \\n // 提交5个任务\\n for (int i = 1; i <= 5; i++) {\\n final int taskId = i;\\n executor.execute(() -> {\\n try {\\n System.out.println(\\"任务\\" + taskId + \\"开始执行\\");\\n // 模拟任务执行时间\\n Thread.sleep(1000 + taskId * 200);\\n System.out.println(\\"任务\\" + taskId + \\"执行完成\\");\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n } finally {\\n // 每个任务完成后计数器减1\\n latch.countDown();\\n }\\n });\\n }\\n \\n System.out.println(\\"主线程等待所有任务完成...\\");\\n // 主线程等待,直到计数器减到0\\n latch.await();\\n System.out.println(\\"所有任务已完成,主线程继续执行\\");\\n }\\n}\\n
\\n·
\\nGo语言虽然有着高效的GMP调度模型,理论上支持成千上万的Goroutine,但是不代表我们完完全全不需要管理Goroutine
\\n什么东西都是过多会导致一些问题,Goroutine过多,会对调度、GC以及系统内存都会造成压力,这样会使我们的服务性能不升反降
\\n那Java通过线程池来管理线程,那我们也可以用池化技术,构造一个协程池,把进程中的协程控制在一定数量
\\n我们实现一个协程池不需要像Java一样需要去管理线程的复用。协程用完就丢,不需要复用
\\npackage main\\n\\nimport (\\n \\"fmt\\"\\n \\"sync\\"\\n \\"sync/atomic\\"\\n \\"time\\"\\n)\\n\\ntype Task struct {\\n f func() error // 具体的任务逻辑\\n}\\n\\nfunc NewTask(funcArg func() error) *Task {\\n return &Task{\\n f: funcArg,\\n }\\n}\\n\\ntype Pool struct {\\n RunningWorkers int64 // 运行着的worker数量\\n Capacity int64 // 协程池worker容量\\n JobCh chan *Task // 用于worker取任务\\n sync.Mutex\\n}\\n\\nfunc NewPool(capacity int64, taskNum int) *Pool {\\n return &Pool{\\n Capacity: capacity,\\n JobCh: make(chan *Task, taskNum),\\n }\\n}\\n\\nfunc (p *Pool) GetCap() int64 {\\n return p.Capacity\\n}\\n\\nfunc (p *Pool) incRunning() { // runningWorkers + 1\\n atomic.AddInt64(&p.RunningWorkers, 1)\\n}\\n\\nfunc (p *Pool) decRunning() { // runningWorkers - 1\\n atomic.AddInt64(&p.RunningWorkers, -1)\\n}\\n\\nfunc (p *Pool) GetRunningWorkers() int64 {\\n return atomic.LoadInt64(&p.RunningWorkers)\\n}\\n\\nfunc (p *Pool) run() {\\n p.incRunning()\\n go func() {\\n defer func() {\\n p.decRunning()\\n }()\\n for task := range p.JobCh {\\n task.f()\\n }\\n }()\\n}\\n\\n// AddTask 往协程池添加任务\\nfunc (p *Pool) AddTask(task *Task) {\\n // 加锁防止启动多个 worker\\n p.Lock()\\n defer p.Unlock()\\n\\n if p.GetRunningWorkers() < p.GetCap() { // 如果任务池满,则不再创建 worker\\n // 创建启动一个 worker\\n p.run()\\n }\\n\\n // 将任务推入队列,等待消费\\n p.JobCh <- task\\n}\\n\\nfunc main() {\\n // 创建任务池\\n pool := NewPool(3, 10)\\n\\n for i := 0; i < 20; i++ {\\n // 任务放入池中\\n pool.AddTask(NewTask(func() error {\\n fmt.Printf(\\"I am Task\\\\n\\")\\n return nil\\n }))\\n }\\n\\n time.Sleep(1e9) // 等待执行\\n}\\n
\\n协程的实现方式分为有栈协程和无栈协程两种。有栈协程指每个协程会保存单独的上下文(执行栈、寄存器等)
\\n有栈协程的唤醒和挂起就是拷贝、切换上下文,无栈协程指单个线程内的所有协程都共享一个执行栈,协程的切换就是简单的函数返回
\\n有栈协程
\\n函数运行在调用栈上,把函数作为一个协程,那么协程的上下文就是这个函数及其嵌套函数的(连续的)栈帧和寄存器的值
\\n如果我们进行协程的调度,也就是保存当前正在运行的协程上下文,然后恢复下一个将要运行的协程的上下文
\\n因为保存上下文和普通函数执行的上下文是一样的,所以有栈协程可以在任意嵌套函数中挂起 (无栈协程不行)
\\n有栈协程的优点在易用性上,通常只需要调用对应的方法,就可以切换上下文挂起协程
\\n在有栈协程调度的时候,需要频繁的切换上下文,开销比较大
\\n从实现上看,有栈协程更接近于内核级线程,都需要为每个线程保存单独的上下文
\\n区别在于有栈协程的调度是由应用程序自行实现的,对内核是透明的,而内核级线程的调度由系统内核完成的
\\n无栈协程
\\n相比于有栈协程直接切换栈帧的思路,无栈协程在不改变函数调用栈的情况下,采用类似生成器的思路实现了上下文切换
\\n通过编译器将生成器改写为对应的迭代器类型 (内部是一个状态机)
\\n无栈协程需要在编译器将代码编译为对应的状态机代码,挂起的位置在编译器确定
\\n无栈协程的优点在于不需要保存单独的上下文,内存占用低,切换成本也低,性能高
\\n缺点就是需要编译器提供语义支持,无栈协程的实现是通过编译器对语法糖做了支持
\\n场景1:银行每日利息计算
\\npie\\n title 利息计算数据规模\\n \\"活期账户\\" : 850000\\n \\"定期账户\\" : 150000\\n \\"VIP大额账户\\" : 5000\\n
\\n场景2:电商订单归档
\\n// 传统SQL示例(存在性能问题)\\nDELETE FROM active_orders \\nWHERE create_time < \'2023-01-01\'\\nLIMIT 5000; // 需循环执行直到无数据\\n
\\n场景3:日志分析
\\nflowchart LR\\n A[原始日志文件] --\x3e B{文件大小?}\\n B --\x3e|>1GB| C[自动分割文件]\\n B --\x3e|正常| D[解析关键字段]\\n D --\x3e E[生成API调用统计报表]\\n
\\n场景4:医疗数据迁移
\\ngantt\\n title 医院系统迁移计划\\n dateFormat YYYY-MM-DD\\n section 数据迁移\\n 患者基础信息 :done, des1, 2023-01-01, 7d\\n 电子病历迁移 :active, des2, 2023-01-08, 10d\\n 影像数据迁移 : des3, 2023-01-15, 14d\\n
\\nflowchart TD\\n A[手工实现批处理] --\x3e B[数据读取]\\n A --\x3e C[业务处理]\\n A --\x3e D[结果写入]\\n \\n B --\x3e B1[分页查询实现复杂]\\n B --\x3e B2[大文件读取内存溢出]\\n \\n C --\x3e C1[多线程协调困难]\\n C --\x3e C2[事务边界难以控制]\\n \\n D --\x3e D1[批量写入效率低下]\\n D --\x3e D2[失败回滚策略缺失]\\n \\n E[运维监控] --\x3e E1[无法查看进度]\\n E --\x3e E2[失败原因难以追踪]\\n E --\x3e E3[无法重跑特定区间]\\n
\\n详细解释每个痛点:
\\n// 典型的多线程错误示例\\nExecutorService executor = Executors.newFixedThreadPool(8);\\ntry {\\n while(hasNextPage()) {\\n List<Data> page = fetchNextPage();\\n executor.submit(() -> processPage(page)); // 可能引发内存泄漏\\n }\\n} finally {\\n executor.shutdown(); // 忘记调用会导致线程堆积\\n}\\n
\\n// 伪代码:脆弱的错误处理\\nfor (int i=0; i<3; i++) {\\n try {\\n processBatch();\\n break;\\n } catch (Exception e) {\\n if (i == 2) sendAlert(); // 简单重试无法处理部分成功场景\\n }\\n}\\n
\\n# 典型硬编码配置\\nbatch.size=1000\\ninput.path=/data/in\\noutput.path=/data/out\\n
\\n# 开发人员常用的临时方案\\nnohup java -jar batch.jar > log.txt 2>&1 &\\ntail -f log.txt # 无法获知实时进度\\n
\\nSpring Batch对比优势表
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n功能点 | 传统方式 | Spring Batch方案 |
---|---|---|
任务重启 | 需从零开始 | 支持断点续处理 |
事务管理 | 手动控制commit/rollback | 自动分块事务 |
错误处理 | try-catch嵌套地狱 | Skip/Retry策略声明式配置 |
监控 | 查看日志文件 | 数据库存储执行元数据 |
扩展性 | 修改代码才能增加处理步骤 | 通过Step组合灵活编排 |
组件1:Job(作业工厂)
\\nclassDiagram\\n class Job {\\n +String name\\n +List<Step> steps\\n +JobParametersValidator validator\\n +start(Step)\\n +next(Step)\\n +decision(JobExecutionDecider)\\n }\\n
\\n@Bean\\npublic Job reconciliationJob() {\\n return jobBuilderFactory.get(\\"dailyReconciliation\\")\\n .start(downloadBankFileStep())\\n .next(validateDataStep())\\n .next(generateReportStep())\\n .build();\\n}\\n
\\n组件2:Step(装配流水线)
\\nflowchart LR\\n A[Step开始] --\x3e B[读取100条数据]\\n B --\x3e C{处理完成?}\\n C --\x3e|否| B\\n C --\x3e|是| D[提交事务]\\n D --\x3e E[Step结束]\\n
\\n@Bean\\npublic Step importStep() {\\n return stepBuilderFactory.get(\\"csvImport\\")\\n .<User, User>chunk(500) // 每500条提交一次\\n .reader(csvReader())\\n .processor(validationProcessor())\\n .writer(dbWriter())\\n .faultTolerant()\\n .skipLimit(10)\\n .skip(DataIntegrityViolationException.class)\\n .build();\\n}\\n
\\n组件3:ItemReader(数据搬运工)
\\npie\\n title 常用Reader类型占比\\n \\"文件读取\\" : 45\\n \\"数据库查询\\" : 35\\n \\"消息队列\\" : 15\\n \\"其他\\" : 5\\n
\\n// 读取CSV文件示例\\n@Bean\\npublic FlatFileItemReader<User> csvReader() {\\n return new FlatFileItemReaderBuilder<User>()\\n .name(\\"userReader\\")\\n .resource(new FileSystemResource(\\"data/users.csv\\"))\\n .delimited().delimiter(\\",\\")\\n .names(\\"id\\", \\"name\\", \\"email\\")\\n .fieldSetMapper(new BeanWrapperFieldSetMapper<User>() {{\\n setTargetType(User.class);\\n }})\\n .linesToSkip(1) // 跳过标题行\\n .build();\\n}\\n
\\n组件4:ItemWriter(数据收纳师)
\\nflowchart LR\\n A[接收数据块] --\x3e B{写入目标类型?}\\n B --\x3e|数据库| C[JdbcBatchItemWriter]\\n B --\x3e|文件| D[FlatFileItemWriter]\\n B --\x3e|消息队列| E[JmsItemWriter]\\n B --\x3e|混合输出| F[CompositeItemWriter]\\n
\\n@Bean\\npublic CompositeItemWriter<User> compositeWriter() {\\n return new CompositeItemWriterBuilder<User>()\\n .delegates(dbWriter(), logWriter(), mqWriter())\\n .build();\\n}\\n\\n// 数据库写入组件\\nprivate JdbcBatchItemWriter<User> dbWriter() {\\n return new JdbcBatchItemWriterBuilder<User>()\\n .dataSource(dataSource)\\n .sql(\\"INSERT INTO users (name,email) VALUES (:name,:email)\\")\\n .beanMapped()\\n .build();\\n}\\n
\\ngraph TD\\n JOB[Job] --\x3e STEP1(Step1: 下载文件)\\n JOB --\x3e STEP2(Step2: 数据处理)\\n JOB --\x3e STEP3(Step3: 生成报告)\\n \\n STEP1 --\x3e R1[FTP下载Reader]\\n STEP1 --\x3e W1[本地文件Writer]\\n \\n STEP2 --\x3e R2[文件读取Reader]\\n STEP2 --\x3e P2[数据清洗Processor]\\n STEP2 --\x3e W2[数据库Writer]\\n \\n STEP3 --\x3e R3[SQL查询Reader]\\n STEP3 --\x3e P3[报表生成Processor]\\n STEP3 --\x3e W3[Excel文件Writer]\\n \\n classDef job fill:#f9d5e5,stroke:#c81d6e;\\n classDef step fill:#e3eaa7,stroke:#86af49;\\n classDef component fill:#b2e2f2,stroke:#3a9bd5;\\n class JOB job;\\n class STEP1,STEP2,STEP3 step;\\n class R1,W1,R2,P2,W2,R3,P3,W3 component\\n
\\nflowchart LR\\n A[原始数据] --\x3e B{需要处理?}\\n B --\x3e|是| C[数据清洗]\\n C --\x3e D[格式转换]\\n D --\x3e E[业务计算]\\n E --\x3e F[过滤无效数据]\\n B --\x3e|否| F\\n
\\npublic class DataMaskProcessor implements ItemProcessor<User, User> {\\n @Override\\n public User process(User user) {\\n // 手机号脱敏\\n String phone = user.getPhone();\\n user.setPhone(phone.replaceAll(\\"(\\\\\\\\d{3})\\\\\\\\d{4}(\\\\\\\\d{4})\\", \\"$1****$2\\"));\\n \\n // 邮箱转小写\\n user.setEmail(user.getEmail().toLowerCase());\\n \\n return user;\\n }\\n}\\n
\\nsequenceDiagram\\n participant J as JobLauncher\\n participant Job\\n participant S as Step\\n participant R as Reader\\n participant P as Processor\\n participant W as Writer\\n \\n J->>Job: 启动Job\\n loop 每个Step\\n Job->>S: 执行Step\\n S->>R: open()\\n loop 每个Chunk\\n R->>R: read()\\n R->>P: process()\\n P->>W: write()\\n end\\n S->>R: close()\\n end\\n Job->>J: 返回结果\\n
\\n<!-- 完整POM配置 --\x3e\\n<parent>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-parent</artifactId>\\n <version>3.1.5</version>\\n</parent>\\n\\n<dependencies>\\n <!-- Batch核心依赖 --\x3e\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-batch</artifactId>\\n </dependency>\\n \\n <!-- 内存数据库(生产环境可更换为MySQL等) --\x3e\\n <dependency>\\n <groupId>com.h2database</groupId>\\n <artifactId>h2</artifactId>\\n <scope>runtime</scope>\\n </dependency>\\n \\n <!-- Lombok简化代码 --\x3e\\n <dependency>\\n <groupId>org.projectlombok</groupId>\\n <artifactId>lombok</artifactId>\\n <optional>true</optional>\\n </dependency>\\n</dependencies>\\n
\\n# application.properties\\nspring.batch.jdbc.initialize-schema=always # 自动创建Batch元数据表\\nspring.datasource.url=jdbc:h2:mem:testdb\\nspring.datasource.driverClassName=org.h2.Driver\\n
\\n领域模型类:
\\n@Data // Lombok注解\\n@NoArgsConstructor\\n@AllArgsConstructor\\npublic class User {\\n private String name;\\n private int age;\\n private String email;\\n}\\n
\\n完整Job配置:
\\n@Configuration\\n@EnableBatchProcessing\\npublic class BatchConfig {\\n\\n @Autowired private JobBuilderFactory jobBuilderFactory;\\n @Autowired private StepBuilderFactory stepBuilderFactory;\\n\\n // 定义Job\\n @Bean\\n public Job importUserJob() {\\n return jobBuilderFactory.get(\\"importUserJob\\")\\n .start(csvProcessingStep())\\n .build();\\n }\\n\\n // 定义Step\\n @Bean\\n public Step csvProcessingStep() {\\n return stepBuilderFactory.get(\\"csvProcessing\\")\\n .<User, User>chunk(100) // 每处理100条提交一次\\n .reader(userReader())\\n .processor(userProcessor())\\n .writer(userWriter())\\n .build();\\n }\\n\\n // CSV文件读取器\\n @Bean\\n public FlatFileItemReader<User> userReader() {\\n return new FlatFileItemReaderBuilder<User>()\\n .name(\\"userReader\\")\\n .resource(new ClassPathResource(\\"users.csv\\")) // 文件路径\\n .delimited()\\n .delimiter(\\",\\")\\n .names(\\"name\\", \\"age\\", \\"email\\") // 字段映射\\n .targetType(User.class)\\n .linesToSkip(1) // 跳过标题行\\n .build();\\n }\\n\\n // 数据处理(示例:年龄校验)\\n @Bean\\n public ItemProcessor<User, User> userProcessor() {\\n return user -> {\\n if (user.getAge() < 0) {\\n throw new IllegalArgumentException(\\"年龄不能为负数: \\" + user);\\n }\\n return user.toBuilder() // 使用Builder模式创建新对象\\n .email(user.getEmail().toLowerCase())\\n .build();\\n };\\n }\\n\\n // 数据库写入器\\n @Bean\\n public JdbcBatchItemWriter<User> userWriter(DataSource dataSource) {\\n return new JdbcBatchItemWriterBuilder<User>()\\n .dataSource(dataSource)\\n .sql(\\"INSERT INTO users (name, age, email) VALUES (:name, :age, :email)\\")\\n .beanMapped()\\n .build();\\n }\\n}\\n
\\nCSV文件示例(src/main/resources/users.csv):
\\nname,age,email\\n张三,25,zhangsan@example.com\\n李四,30,lisi@example.com\\n王五,-5,wangwu@example.com\\n
\\n启动类:
\\n@SpringBootApplication\\npublic class BatchApplication implements CommandLineRunner {\\n\\n @Autowired\\n private JobLauncher jobLauncher;\\n\\n @Autowired\\n private Job importUserJob;\\n\\n public static void main(String[] args) {\\n SpringApplication.run(BatchApplication.class, args);\\n }\\n\\n @Override\\n public void run(String... args) throws Exception {\\n JobParameters params = new JobParametersBuilder()\\n .addLong(\\"startAt\\", System.currentTimeMillis())\\n .toJobParameters();\\n jobLauncher.run(importUserJob, params);\\n }\\n}\\n
\\nsequenceDiagram\\n participant App as 应用程序\\n participant JobLauncher\\n participant Job\\n participant Step\\n participant Reader\\n participant Processor\\n participant Writer\\n \\n App->>JobLauncher: 启动Job\\n JobLauncher->>Job: 执行Job实例\\n loop 每个Step\\n Job->>Step: 执行Step\\n Step->>Reader: 打开数据源\\n loop 每个Chunk\\n Reader->>Reader: 读取100条数据\\n loop 每条数据\\n Reader->>Processor: 传递数据\\n Processor->>Writer: 处理后的数据\\n end\\n Writer->>Writer: 批量写入数据库\\n Step->>Step: 提交事务\\n end\\n Step->>Reader: 关闭资源\\n end\\n Job->>JobLauncher: 返回执行结果\\n
\\n控制台输出:
\\n2023-10-01 10:00:00 INFO o.s.b.c.l.support.SimpleJobLauncher - Job: [SimpleJob: [name=importUserJob]] launched\\n2023-10-01 10:00:05 INFO o.s.batch.core.job.SimpleStepHandler - Executing step: [csvProcessing]\\n2023-10-01 10:00:15 ERROR o.s.batch.core.step.AbstractStep - Encountered an error executing step csvProcessing\\norg.springframework.batch.item.validator.ValidationException: 年龄不能为负数: User(name=王五, age=-5, email=wangwu@example.com)\\n
\\n数据库结果:
\\nSELECT * FROM users;\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nname | age | |
---|---|---|
张三 | 25 | zhangsan@example.com |
李四 | 30 | lisi@example.com |
SELECT * FROM BATCH_JOB_INSTANCE;\\nSELECT * FROM BATCH_STEP_EXECUTION;\\n
\\n// 在Job配置中添加容错机制\\n@Bean\\npublic Step csvProcessingStep() {\\n return stepBuilderFactory.get(\\"csvProcessing\\")\\n .<User, User>chunk(100)\\n .reader(userReader())\\n .processor(userProcessor())\\n .writer(userWriter())\\n .faultTolerant()\\n .skipLimit(3) // 最多跳过3条错误\\n .skip(IllegalArgumentException.class)\\n .build();\\n}\\n
\\nlogging.level.org.springframework.batch=DEBUG\\nlogging.level.org.hibernate.SQL=WARN\\n
\\n核心流程:
\\nflowchart TD\\n A[下载银行对账单] --\x3e B[加载内部交易数据]\\n B --\x3e C[数据校验比对]\\n C --\x3e D{存在差异?}\\n D --\x3e|是| E[记录差异明细]\\n D --\x3e|否| F[更新对账状态]\\n E --\x3e G[生成差异报告]\\n F --\x3e H[发送成功通知]\\n
\\n技术挑战:
\\ngraph TD\\n subgraph 输入源\\n A[银行SFTP服务器]\\n B[内部Oracle数据库]\\n end\\n \\n subgraph Spring Batch\\n C[FileItemReader]\\n D[JdbcCursorItemReader]\\n E[CompositeItemProcessor]\\n F[JdbcBatchItemWriter]\\n G[ExcelFileItemWriter]\\n end\\n \\n subgraph 输出目标\\n H[差异记录表]\\n I[Excel报告]\\n J[消息队列]\\n end\\n \\n A --\x3e C\\n B --\x3e D\\n C --\x3e E\\n D --\x3e E\\n E --\x3e F\\n E --\x3e G\\n F --\x3e H\\n G --\x3e I\\n H --\x3e J\\n
\\n@Data\\n@AllArgsConstructor\\n@NoArgsConstructor\\npublic class Transaction {\\n // 公共字段\\n private String transactionId;\\n private LocalDateTime tradeTime;\\n private BigDecimal amount;\\n \\n // 银行端数据\\n private String bankSerialNo;\\n private BigDecimal bankAmount;\\n \\n // 内部系统数据\\n private String internalOrderNo;\\n private BigDecimal systemAmount;\\n \\n // 对账结果\\n private ReconStatus status;\\n private String discrepancyType;\\n}\\n\\npublic enum ReconStatus {\\n MATCHED, // 数据一致\\n AMOUNT_DIFF, // 金额不一致\\n STATUS_DIFF, // 状态不一致\\n ONLY_IN_BANK, // 银行单边账\\n ONLY_IN_SYSTEM // 系统单边账\\n}\\n
\\n@Configuration\\n@EnableBatchProcessing\\npublic class BankReconJobConfig {\\n\\n // 主Job定义\\n @Bean\\n public Job bankReconciliationJob(Step downloadStep, Step reconStep, Step reportStep) {\\n return jobBuilderFactory.get(\\"bankReconciliationJob\\")\\n .start(downloadStep)\\n .next(reconStep)\\n .next(reportStep)\\n .build();\\n }\\n\\n // 文件下载Step\\n @Bean\\n public Step downloadStep() {\\n return stepBuilderFactory.get(\\"downloadStep\\")\\n .tasklet((contribution, chunkContext) -> {\\n // 实现SFTP下载逻辑\\n sftpService.download(\\"/bank/recon/20231001.csv\\");\\n return RepeatStatus.FINISHED;\\n })\\n .build();\\n }\\n\\n // 核心对账Step\\n @Bean\\n public Step reconStep() {\\n return stepBuilderFactory.get(\\"reconStep\\")\\n .<Transaction, Transaction>chunk(1000)\\n .reader(compositeReader())\\n .processor(compositeProcessor())\\n .writer(compositeWriter())\\n .faultTolerant()\\n .skipLimit(100)\\n .skip(DataIntegrityViolationException.class)\\n .retryLimit(3)\\n .retry(DeadlockLoserDataAccessException.class)\\n .build();\\n }\\n\\n // 组合数据读取器\\n @Bean\\n public CompositeItemReader<Transaction> compositeReader() {\\n return new CompositeItemReaderBuilder<Transaction>()\\n .delegates(bankFileReader(), internalDbReader())\\n .build();\\n }\\n\\n // 银行文件读取器\\n @Bean\\n public FlatFileItemReader<Transaction> bankFileReader() {\\n return new FlatFileItemReaderBuilder<Transaction>()\\n .name(\\"bankFileReader\\")\\n .resource(new FileSystemResource(\\"recon/20231001.csv\\"))\\n .delimited()\\n .names(\\"transactionId\\",\\"tradeTime\\",\\"amount\\",\\"bankSerialNo\\")\\n .fieldSetMapper(fieldSet -> {\\n Transaction t = new Transaction();\\n t.setTransactionId(fieldSet.readString(\\"transactionId\\"));\\n t.setBankSerialNo(fieldSet.readString(\\"bankSerialNo\\"));\\n t.setBankAmount(fieldSet.readBigDecimal(\\"amount\\"));\\n return t;\\n })\\n .build();\\n }\\n\\n // 内部数据库读取器\\n @Bean\\n public JdbcCursorItemReader<Transaction> internalDbReader() {\\n return new JdbcCursorItemReaderBuilder<Transaction>()\\n .name(\\"internalDbReader\\")\\n .dataSource(internalDataSource)\\n .sql(\\"SELECT order_no, amount, status FROM transactions WHERE trade_date = ?\\")\\n .rowMapper((rs, rowNum) -> {\\n Transaction t = new Transaction();\\n t.setInternalOrderNo(rs.getString(\\"order_no\\"));\\n t.setSystemAmount(rs.getBigDecimal(\\"amount\\"));\\n return t;\\n })\\n .preparedStatementSetter(ps -> ps.setString(1, \\"2023-10-01\\"))\\n .build();\\n }\\n\\n // 组合处理器\\n @Bean\\n public CompositeItemProcessor<Transaction> compositeProcessor() {\\n List<ItemProcessor<?, ?>> delegates = new ArrayList<>();\\n delegates.add(new DataMatchingProcessor());\\n delegates.add(new DiscrepancyClassifier());\\n return new CompositeItemProcessorBuilder<>()\\n .delegates(delegates)\\n .build();\\n }\\n\\n // 组合写入器\\n @Bean\\n public CompositeItemWriter<Transaction> compositeWriter() {\\n return new CompositeItemWriterBuilder<Transaction>()\\n .delegates(\\n discrepancyDbWriter(),\\n alertMessageWriter()\\n )\\n .build();\\n }\\n}\\n
\\npublic class DataMatchingProcessor implements ItemProcessor<Transaction, Transaction> {\\n\\n @Override\\n public Transaction process(Transaction item) {\\n // 双数据源匹配逻辑\\n if (item.getBankSerialNo() == null) {\\n item.setStatus(ReconStatus.ONLY_IN_SYSTEM);\\n } else if (item.getInternalOrderNo() == null) {\\n item.setStatus(ReconStatus.ONLY_IN_BANK);\\n } else {\\n compareAmounts(item);\\n compareStatuses(item);\\n }\\n return item;\\n }\\n\\n private void compareAmounts(Transaction t) {\\n if (t.getBankAmount().compareTo(t.getSystemAmount()) != 0) {\\n t.setDiscrepancyType(\\"AMOUNT_MISMATCH\\");\\n t.setStatus(ReconStatus.AMOUNT_DIFF);\\n BigDecimal diff = t.getBankAmount().subtract(t.getSystemAmount());\\n t.setAmount(diff.abs());\\n }\\n }\\n\\n private void compareStatuses(Transaction t) {\\n // 假设从数据库获取内部状态\\n String internalStatus = transactionService.getStatus(t.getInternalOrderNo());\\n if(!\\"SETTLED\\".equals(internalStatus)){\\n t.setDiscrepancyType(\\"STATUS_MISMATCH\\");\\n t.setStatus(ReconStatus.STATUS_DIFF);\\n }\\n }\\n}\\n\\npublic class DiscrepancyClassifier implements ItemProcessor<Transaction, Transaction> {\\n @Override\\n public Transaction process(Transaction item) {\\n if (item.getStatus() != ReconStatus.MATCHED) {\\n // 添加告警标记\\n item.setAlertLevel(calculateAlertLevel(item));\\n }\\n return item;\\n }\\n\\n private AlertLevel calculateAlertLevel(Transaction t) {\\n if (t.getAmount().compareTo(new BigDecimal(\\"1000000\\")) > 0) {\\n return AlertLevel.CRITICAL;\\n }\\n return AlertLevel.WARNING;\\n }\\n}\\n
\\n@Bean\\npublic Step reportStep() {\\n return stepBuilderFactory.get(\\"reportStep\\")\\n .<Transaction, Transaction>chunk(1000)\\n .reader(discrepancyReader())\\n .writer(excelWriter())\\n .build();\\n}\\n\\n@Bean\\npublic JdbcPagingItemReader<Transaction> discrepancyReader() {\\n return new JdbcPagingItemReaderBuilder<Transaction>()\\n .name(\\"discrepancyReader\\")\\n .dataSource(reconDataSource)\\n .selectClause(\\"SELECT *\\")\\n .fromClause(\\"FROM discrepancy_records\\")\\n .whereClause(\\"WHERE recon_date = \'2023-10-01\'\\")\\n .sortKeys(Collections.singletonMap(\\"transaction_id\\", Order.ASCENDING))\\n .rowMapper(new BeanPropertyRowMapper<>(Transaction.class))\\n .build();\\n}\\n\\n@Bean\\npublic ExcelFileItemWriter<Transaction> excelWriter() {\\n return new ExcelFileItemWriterBuilder<Transaction>()\\n .name(\\"excelWriter\\")\\n .resource(new FileSystemResource(\\"reports/2023-10-01.xlsx\\"))\\n .sheetName(\\"差异报告\\")\\n .headers(new String[]{\\"交易ID\\", \\"差异类型\\", \\"金额差异\\", \\"告警级别\\"})\\n .fieldExtractor(item -> new Object[]{\\n item.getTransactionId(),\\n item.getDiscrepancyType(),\\n item.getAmount(),\\n item.getAlertLevel()\\n })\\n .build();\\n}\\n
\\n# 应用配置\\nspring.batch.job.enabled=false # 禁止自动启动\\nspring.batch.initialize-schema=never # 生产环境禁止自动建表\\n\\n# 性能调优参数\\nspring.batch.chunk.size=2000 # 根据内存调整\\nspring.datasource.hikari.maximum-pool-size=20\\nspring.jpa.properties.hibernate.jdbc.batch_size=1000\\n
\\ngantt\\n title 对账任务执行进度\\n dateFormat HH:mm\\n section 任务执行\\n 文件下载 :done, des1, 00:00, 5m\\n 数据比对 :active, des2, 00:05, 40m\\n 报告生成 : des3, 00:45, 15m\\n section 资源监控\\n CPU使用率 :crit, done, 00:00, 60m\\n 内存占用 :active, 00:00, 60m\\n
\\nflowchart TD\\n A[处理记录] --\x3e B{出现异常?}\\n B --\x3e|是| C[检查重试策略]\\n C --\x3e D{可重试异常?}\\n D --\x3e|是| E[重试计数器+1]\\n E --\x3e F{达到上限?}\\n F --\x3e|否| G[等待1秒后重试]\\n F --\x3e|是| H[应用跳过策略]\\n D --\x3e|否| H\\n H --\x3e I[记录错误上下文]\\n I --\x3e J[写入错误日志表]\\n B --\x3e|否| K[正常处理]\\n
\\n完整容错配置示例:
\\n@Bean\\npublic Step secureStep() {\\n return stepBuilderFactory.get(\\"secureStep\\")\\n .<Input, Output>chunk(500)\\n .reader(jdbcReader())\\n .processor(secureProcessor())\\n .writer(restApiWriter())\\n .faultTolerant()\\n .retryLimit(3)\\n .retry(ConnectException.class) // 网络问题重试\\n .retry(DeadlockLoserDataAccessException.class) // 数据库死锁重试\\n .skipLimit(100)\\n .skip(DataIntegrityViolationException.class) // 数据问题跳过\\n .skip(InvalidDataAccessApiUsageException.class)\\n .noRollback(ValidationException.class) // 验证异常不回滚\\n .listener(new ErrorLogListener()) // 自定义监听器\\n .build();\\n}\\n\\n// 错误日志监听器示例\\npublic class ErrorLogListener implements ItemProcessListener<Input, Output> {\\n @Override\\n public void onProcessError(Input item, Exception e) {\\n ErrorLog log = new ErrorLog();\\n log.setItemData(item.toString());\\n log.setErrorMsg(e.getMessage());\\n errorLogRepository.save(log);\\n }\\n}\\n
\\n策略1:并行Step执行
\\ngantt\\n title 并行执行优化对比\\n dateFormat HH:mm\\n section 串行执行\\n 数据清洗 :a1, 00:00, 30m\\n 风险校验 :a2, after a1, 20m\\n 生成报告 :a3, after a2, 10m\\n section 并行执行\\n 数据清洗 :b1, 00:00, 30m\\n 风险校验 :b2, 00:00, 20m\\n 生成报告 :b3, after b1, 10m\\n
\\n配置代码:
\\n@Bean\\npublic Job parallelJob() {\\n return jobBuilderFactory.get(\\"parallelJob\\")\\n .start(step1())\\n .split(new SimpleAsyncTaskExecutor()) // 启用异步执行器\\n .add(step2(), step3())\\n .build();\\n}\\n
\\n策略2:分区处理(Partitioning)
\\nflowchart TB\\n Master[Master Step] --\x3e|分区策略| Partition1[Slave Step-1]\\n Master --\x3e|分区策略| Partition2[Slave Step-2]\\n Master --\x3e|分区策略| Partition3[Slave Step-3]\\n \\n subgraph 数据分区\\n Partition1 --\x3e 处理1-100万条\\n Partition2 --\x3e 处理100-200万条\\n Partition3 --\x3e 处理200-300万条\\n end\\n
\\n分区处理器实现:
\\n@Bean\\npublic Step masterStep() {\\n return stepBuilderFactory.get(\\"masterStep\\")\\n .partitioner(\\"slaveStep\\", partitioner())\\n .gridSize(10) // 分区数量=CPU核心数*2\\n .taskExecutor(new ThreadPoolTaskExecutor())\\n .build();\\n}\\n\\n@Bean\\npublic Partitioner partitioner() {\\n return new Partitioner() {\\n @Override\\n public Map<String, ExecutionContext> partition(int gridSize) {\\n Map<String, ExecutionContext> result = new HashMap<>();\\n long total = getTotalRecordCount();\\n \\n long range = total / gridSize;\\n for (int i = 0; i < gridSize; i++) {\\n ExecutionContext context = new ExecutionContext();\\n context.putLong(\\"min\\", i * range);\\n context.putLong(\\"max\\", (i+1) * range);\\n result.put(\\"partition\\"+i, context);\\n }\\n return result;\\n }\\n };\\n}\\n\\n// Slave Step配置\\n@Bean\\npublic Step slaveStep() {\\n return stepBuilderFactory.get(\\"slaveStep\\")\\n .<Record, Result>chunk(1000)\\n .reader(rangeReader(null, null))\\n .processor(processor())\\n .writer(writer())\\n .build();\\n}\\n\\n@StepScope\\n@Bean\\npublic ItemReader<Record> rangeReader(\\n @Value(\\"#{stepExecutionContext[min]}\\") Long min,\\n @Value(\\"#{stepExecutionContext[max]}\\") Long max) {\\n return new JdbcCursorItemReaderBuilder<Record>()\\n .sql(\\"SELECT * FROM records WHERE id BETWEEN ? AND ?\\")\\n .preparedStatementSetter(ps -> {\\n ps.setLong(1, min);\\n ps.setLong(2, max);\\n })\\n // 其他配置...\\n .build();\\n}\\n
\\n策略3:异步ItemProcessor
\\nsequenceDiagram\\n participant R as Reader\\n participant AP as AsyncProcessor\\n participant W as Writer\\n \\n R->>AP: 同步读取数据\\n AP->>+AP: 提交异步任务\\n AP--\x3e>-W: 异步返回处理结果\\n W->>W: 批量写入\\n
\\n异步处理配置:
\\n@Bean\\npublic Step asyncStep() {\\n return stepBuilderFactory.get(\\"asyncStep\\")\\n .<Input, Output>chunk(1000)\\n .reader(reader())\\n .processor(asyncItemProcessor())\\n .writer(writer())\\n .build();\\n}\\n\\n@Bean\\npublic AsyncItemProcessor<Input, Output> asyncItemProcessor() {\\n AsyncItemProcessor<Input, Output> asyncProcessor = new AsyncItemProcessor<>();\\n asyncProcessor.setDelegate(syncProcessor()); // 同步处理器\\n asyncProcessor.setTaskExecutor(new ThreadPoolTaskExecutor());\\n return asyncProcessor;\\n}\\n\\n@Bean\\npublic AsyncItemWriter<Output> asyncItemWriter() {\\n AsyncItemWriter<Output> asyncWriter = new AsyncItemWriter<>();\\n asyncWriter.setDelegate(syncWriter()); // 同步写入器\\n return asyncWriter;\\n}\\n
\\n处理方式 | 100万条耗时 | 1000万条耗时 | 资源消耗 |
---|---|---|---|
单线程 | 2h15m | 23h+ | CPU 15% |
分区处理(10线程) | 25m | 4h10m | CPU 75% |
异步处理+分区 | 18m | 3h05m | CPU 95% |
优化技巧:
\\nspring.datasource.hikari.maximum-pool-size=20\\nspring.datasource.hikari.minimum-idle=5\\n
\\njava -jar -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ...\\n
\\n.chunk(2000) // 根据内存容量调整\\n.setQueryTimeout(60) // 数据库查询超时\\n
\\ngraph TD\\n A[Prometheus] --\x3e|拉取指标| B(Spring Batch Metrics)\\n C[Grafana] --\x3e|可视化| A\\n D[Alert Manager] --\x3e|告警规则| A\\n E[Elasticsearch] --\x3e|存储日志| F[Kibana]\\n
\\n现代监控栈配置:
\\n// 添加监控依赖\\n<dependency>\\n <groupId>io.micrometer</groupId>\\n <artifactId>micrometer-registry-prometheus</artifactId>\\n</dependency>\\n\\n// 暴露监控端点\\n@Bean\\npublic MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {\\n return registry -> registry.config().commonTags(\\"application\\", \\"batch-service\\");\\n}\\n\\n// 自定义Batch指标\\npublic class BatchMetricsListener extends JobExecutionListenerSupport {\\n private final Counter processedRecords = Counter.builder(\\"batch.records.processed\\")\\n .description(\\"Total processed records\\")\\n .register(Metrics.globalRegistry);\\n \\n @Override\\n public void afterStep(StepExecution stepExecution) {\\n processedRecords.increment(stepExecution.getWriteCount());\\n }\\n}\\n
\\nerDiagram\\n BATCH_JOB_INSTANCE ||--o{ BATCH_JOB_EXECUTION : \\"1:N\\"\\n BATCH_JOB_EXECUTION ||--o{ BATCH_STEP_EXECUTION : \\"1:N\\"\\n BATCH_JOB_EXECUTION ||--o{ BATCH_JOB_EXECUTION_PARAMS : \\"1:N\\"\\n BATCH_STEP_EXECUTION ||--o{ BATCH_STEP_EXECUTION_CONTEXT : \\"1:1\\"\\n\\n BATCH_JOB_INSTANCE {\\n bigint JOB_INSTANCE_ID PK\\n varchar JOB_NAME\\n varchar JOB_KEY\\n }\\n \\n BATCH_JOB_EXECUTION {\\n bigint JOB_EXECUTION_ID PK\\n bigint JOB_INSTANCE_ID FK\\n timestamp START_TIME\\n timestamp END_TIME\\n varchar STATUS\\n varchar EXIT_CODE\\n }\\n \\n BATCH_STEP_EXECUTION {\\n bigint STEP_EXECUTION_ID PK\\n bigint JOB_EXECUTION_ID FK\\n varchar STEP_NAME\\n timestamp START_TIME\\n timestamp END_TIME\\n int READ_COUNT\\n int WRITE_COUNT\\n int ROLLBACK_COUNT\\n }\\n
\\n关键表用途:
\\nBATCH_JOB_INSTANCE
:作业指纹库(相同参数只能存在一个实例)BATCH_JOB_EXECUTION_PARAMS
:存储每次运行的参数BATCH_STEP_EXECUTION_CONTEXT
:保存步骤上下文数据(重启恢复的关键)-- 常用监控SQL示例\\n-- 最近5次作业执行情况\\nSELECT j.JOB_NAME, e.START_TIME, e.END_TIME, \\n TIMEDIFF(e.END_TIME, e.START_TIME) AS DURATION,\\n s.READ_COUNT, s.WRITE_COUNT\\nFROM BATCH_JOB_EXECUTION e\\nJOIN BATCH_JOB_INSTANCE j ON e.JOB_INSTANCE_ID = j.JOB_INSTANCE_ID\\nJOIN BATCH_STEP_EXECUTION s ON e.JOB_EXECUTION_ID = s.JOB_EXECUTION_ID\\nORDER BY e.START_TIME DESC LIMIT 5;\\n
\\n场景:处理10GB CSV文件时OOM
\\nflowchart TD\\n A[大文件] --\x3e B{处理方式}\\n B --\x3e|传统方式| C[全量加载 ->内存爆炸]\\n B --\x3e|Spring Batch方案| D[分块流式处理]\\n \\n D --\x3e E[文件分割策略]\\n E --\x3e E1[按行数分割]\\n E --\x3e E2[按大小分割]\\n \\n D --\x3e F[内存控制技巧]\\n F --\x3e F1[调整chunk size]\\n F --\x3e F2[关闭数据缓存]\\n F --\x3e F3[使用游标读取]\\n
\\n优化代码示例:
\\n@Bean\\n@StepScope\\npublic FlatFileItemReader<LargeRecord> largeFileReader(\\n @Value(\\"#{jobParameters[\'filePath\']}\\") String filePath) {\\n \\n return new FlatFileItemReaderBuilder<LargeRecord>()\\n .resource(new FileSystemResource(filePath))\\n .lineMapper(new DefaultLineMapper<>() {{\\n setLineTokenizer(new DelimitedLineTokenizer());\\n setFieldSetMapper(new BeanWrapperFieldSetMapper<>() {{\\n setTargetType(LargeRecord.class);\\n }});\\n }})\\n .linesToSkip(1)\\n .strict(false) // 允许文件结尾空行\\n .saveState(false) // 禁用状态保存\\n .build();\\n}\\n\\n// JVM参数建议\\n// -XX:+UseG1GC -Xmx2g -XX:MaxGCPauseMillis=200\\n
\\n多任务调度方案:
\\n@Configuration\\n@EnableScheduling\\npublic class ScheduleConfig {\\n\\n @Autowired private JobLauncher jobLauncher;\\n @Autowired private Job reportJob;\\n \\n // 工作日凌晨执行\\n @Scheduled(cron = \\"0 0 2 * * MON-FRI\\")\\n public void dailyJob() throws Exception {\\n JobParameters params = new JobParametersBuilder()\\n .addString(\\"date\\", LocalDate.now().toString())\\n .toJobParameters();\\n jobLauncher.run(reportJob, params);\\n }\\n\\n // 每小时轮询\\n @Scheduled(fixedRate = 3600000)\\n public void pollJob() {\\n if(checkNewDataExists()) {\\n jobLauncher.run(dataProcessJob, new JobParameters());\\n }\\n }\\n \\n // 优雅停止示例\\n public void stopJob(Long executionId) {\\n JobExecution execution = jobExplorer.getJobExecution(executionId);\\n if(execution.isRunning()) {\\n execution.setStatus(BatchStatus.STOPPING);\\n jobRepository.update(execution);\\n }\\n }\\n}\\n
\\nQ:如何重新运行失败的任务?
\\n-- 步骤1:查询失败的任务ID\\nSELECT * FROM BATCH_JOB_EXECUTION WHERE STATUS = \'FAILED\';\\n\\n-- 步骤2:使用相同参数重新启动\\nJobParameters params = new JobParametersBuilder()\\n .addLong(\\"restartId\\", originalExecutionId)\\n .toJobParameters();\\njobLauncher.run(job, params);\\n
\\nQ:处理过程中断电怎么办?
\\nsequenceDiagram\\n participant App as 应用程序\\n participant DB as 数据库\\n \\n App->>DB: 开启事务(Chunk1)\\n DB--\x3e>App: 事务ID:1001\\n App->>DB: 提交事务\\n Note over App,DB: 正常处理\\n \\n App->>DB: 开启事务(Chunk2)\\n DB--\x3e>App: 事务ID:1002\\n Note left of App: 断电!事务未提交\\n App--x DB: 连接中断\\n \\n App->>DB: 重新启动\\n App->>DB: 查询最后提交位置\\n DB--\x3e>App: 最后成功Chunk1\\n App->>DB: 从Chunk2继续处理\\n
\\nQ:如何实现动态参数传递?
\\n// 命令行启动方式\\njava -jar batch.jar --spring.batch.job.name=dataImportJob date=2023-10-01\\n\\n// 编程式参数构建\\npublic void runJobWithParams(Map<String, Object> params) {\\n JobParameters jobParams = new JobParametersBuilder()\\n .addString(\\"mode\\", \\"forceUpdate\\")\\n .addLong(\\"timestamp\\", System.currentTimeMillis())\\n .toJobParameters();\\n jobLauncher.run(importJob, jobParams);\\n}\\n
\\n数据库优化
\\nJVM优化
\\n-XX:+UseStringDeduplication\\n-XX:+UseCompressedOops\\n-XX:MaxMetaspaceSize=512m\\n
\\nBatch配置
\\nspring.batch.jdbc.initialize-schema=never\\nspring.batch.job.enabled=false\\nspring.jpa.open-in-view=false\\n
\\n在 Java 开发的世界里,性能优化是永恒的话题。当系统面临高并发、大数据量时,如何让 Java 应用程序保持高效运行,成为了每个开发者必须攻克的难题。接下来,我将分享自己通过 30 天时间,从 JVM 调优到实现百万级 QPS 的 Java 性能优化全历程,希望能给大家带来启发。
\\nJVM 作为 Java 程序运行的核心环境,其调优至关重要。首先要了解 JVM 的内存结构,包括堆内存、方法区、程序计数器、虚拟机栈和本地方法栈。其中,堆内存是对象分配的主要区域,也是调优的重点。
\\n通过设置合理的堆内存大小参数,可以有效提升程序性能。例如,使用-Xms和-Xmx参数设置堆的初始大小和最大大小,避免频繁的内存扩展和收缩。如:
\\njava -Xms1024m -Xmx4096m MyApp\\n
\\n同时,垃圾回收机制的选择也会影响性能。常见的垃圾回收器有 Serial、Parallel、CMS 和 G1 等。对于不同的应用场景,应选择合适的垃圾回收器。比如,对于响应时间敏感的应用,CMS 垃圾回收器可能是更好的选择,它能在垃圾回收过程中尽量减少应用程序的停顿时间。
\\n在高并发场景下,多线程技术是提升系统吞吐量的关键。但多线程也会带来线程安全、死锁等问题。为了优化多线程性能,可以从以下几个方面入手。
\\n线程池可以避免频繁创建和销毁线程带来的开销。Java 提供了ExecutorService接口及其实现类ThreadPoolExecutor来创建线程池。通过设置合理的核心线程数、最大线程数、队列容量等参数,可以充分利用系统资源。例如:
\\nExecutorService executorService = new ThreadPoolExecutor(\\n 5, // 核心线程数\\n 10, // 最大线程数\\n 60L, // 空闲线程存活时间\\n TimeUnit.SECONDS,\\n new ArrayBlockingQueue<>(100) // 任务队列\\n);\\n
\\n在多线程环境中,锁的使用会影响性能。尽量减小锁的粒度,使用ConcurrentHashMap等线程安全的集合类代替加锁的普通集合类。对于读写操作频繁的场景,可以使用ReadWriteLock,允许多个线程同时读,但只允许一个线程写,从而提高并发性能。
\\n数据库是 Java 应用程序的重要组成部分,数据库性能的好坏直接影响整个系统的性能。
\\n编写高效的 SQL 语句是数据库优化的基础。避免使用SELECT *,明确指定需要查询的字段;合理使用索引,对经常用于查询条件、排序和连接的字段创建索引。例如,对于一个查询用户信息的 SQL 语句:
\\nSELECT id, name, age FROM users WHERE age > 18 AND city = \'Beijing\';\\n
\\n可以在age和city字段上创建复合索引,以加快查询速度。
\\n使用数据库连接池可以减少数据库连接的创建和销毁开销。常见的数据库连接池有 C3P0、DBCP 和 HikariCP 等。HikariCP 以其高性能和低资源消耗受到广泛青睐,配置示例如下:
\\nspring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver\\nspring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/mydb\\nspring.datasource.hikari.username=root\\nspring.datasource.hikari.password=123456\\n
\\n引入缓存可以将经常访问的数据存储在内存中,减少对数据库的访问次数,从而提高系统性能。常用的缓存框架有 Ehcache、Guava Cache 和 Redis 等。
\\n以 Redis 为例,它是一个高性能的键值对数据库,可以作为缓存使用。在 Java 中,通过 Jedis 或 Lettuce 等客户端库可以方便地操作 Redis。例如,使用 Jedis 获取缓存数据:
\\nJedis jedis = new Jedis(\\"localhost\\", 6379);\\nString value = jedis.get(\\"key\\");\\nif (value == null) {\\n // 从数据库查询数据\\n value = queryFromDatabase();\\n jedis.set(\\"key\\", value);\\n}\\nreturn value;\\n
\\n通过以上一系列的性能优化措施,经过 30 天的不断尝试和调整,最终实现了系统百万级 QPS 的目标。性能优化是一个持续的过程,需要不断学习和实践,希望大家在 Java 开发中也能打造出高性能的应用程序。
\\nJava 21 引入的虚拟线程,为 Java 开发者带来了全新的编程体验,它极大地简化了并发编程,提高了应用程序的性能和可扩展性。虚拟线程与传统的平台线程相比,具有轻量级、创建成本低等优势,能够轻松处理海量并发任务。下面将为大家介绍虚拟线程的 5 大实战场景,看看你是否已经应用到实际开发中。
\\n在 Web 应用开发中,经常会面临高并发的请求。传统的线程模型在处理大量并发请求时,会因为线程数量过多而导致系统资源耗尽。而虚拟线程的出现,完美解决了这个问题。
\\n以 Spring Boot 应用为例,通过在ThreadPoolTaskExecutor中配置虚拟线程工厂,可以轻松实现基于虚拟线程的高并发处理。首先,创建一个虚拟线程工厂:
\\nimport java.util.concurrent.ThreadFactory;\\nimport java.util.concurrent.Executors;\\nimport java.util.concurrent.VirtualThread;\\npublic class VirtualThreadFactory implements ThreadFactory {\\n @Override\\n public Thread newThread(Runnable r) {\\n return VirtualThread.newThread(r);\\n }\\n}\\n
\\n然后,在 Spring Boot 的配置类中配置线程池:
\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\\n@Configuration\\npublic class ThreadPoolConfig {\\n @Bean\\n public ThreadPoolTaskExecutor taskExecutor() {\\n ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\\n executor.setThreadFactory(new VirtualThreadFactory());\\n executor.setCorePoolSize(100);\\n executor.setMaxPoolSize(1000);\\n executor.setQueueCapacity(10000);\\n return executor;\\n }\\n}\\n
\\n这样,当 Web 服务接收到大量请求时,虚拟线程能够快速响应,提高服务的吞吐量和响应速度。
\\n在数据处理场景中,经常需要对大量数据进行异步批处理。例如,从数据库中读取大量数据,然后进行计算、转换等操作。使用虚拟线程可以轻松创建大量的异步任务,同时避免线程资源的浪费。
\\n以下是一个简单的示例,使用虚拟线程对数据列表进行异步处理:
\\nimport java.util.ArrayList;\\nimport java.util.List;\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.concurrent.ExecutionException;\\npublic class BatchProcessing {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n List<Integer> dataList = new ArrayList<>();\\n for (int i = 0; i < 1000; i++) {\\n dataList.add(i);\\n }\\n List<CompletableFuture<Void>> futures = new ArrayList<>();\\n for (Integer data : dataList) {\\n CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {\\n // 模拟数据处理操作\\n processData(data);\\n }, new VirtualThreadFactory());\\n futures.add(future);\\n }\\n CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();\\n }\\n private static void processData(int data) {\\n // 具体的数据处理逻辑\\n System.out.println(\\"Processing data: \\" + data);\\n }\\n}\\n
\\n通过这种方式,可以高效地处理大量数据,提高批处理任务的执行效率。
\\n在微服务架构中,服务之间的调用通常是异步的。虚拟线程可以降低微服务调用过程中的线程切换开销,提高系统的整体性能。
\\n当一个微服务需要调用多个其他微服务获取数据时,可以使用虚拟线程并行发起调用。例如,使用HttpClient发起 HTTP 请求:
\\nimport java.net.URI;\\nimport java.net.http.HttpClient;\\nimport java.net.http.HttpRequest;\\nimport java.net.http.HttpResponse;\\nimport java.util.concurrent.CompletableFuture;\\npublic class MicroserviceCall {\\n public static void main(String[] args) {\\n HttpClient client = HttpClient.newHttpClient();\\n String url1 = \\"http://service1/api/data\\";\\n String url2 = \\"http://service2/api/data\\";\\n CompletableFuture<HttpResponse<String>> future1 = CompletableFuture.supplyAsync(() -> {\\n try {\\n HttpRequest request = HttpRequest.newBuilder()\\n .uri(URI.create(url1))\\n .build();\\n return client.send(request, HttpResponse.BodyHandlers.ofString());\\n } catch (Exception e) {\\n throw new RuntimeException(e);\\n }\\n }, new VirtualThreadFactory());\\n CompletableFuture<HttpResponse<String>> future2 = CompletableFuture.supplyAsync(() -> {\\n try {\\n HttpRequest request = HttpRequest.newBuilder()\\n .uri(URI.create(url2))\\n .build();\\n return client.send(request, HttpResponse.BodyHandlers.ofString());\\n } catch (Exception e) {\\n throw new RuntimeException(e);\\n }\\n }, new VirtualThreadFactory());\\n CompletableFuture.allOf(future1, future2).join();\\n try {\\n HttpResponse<String> response1 = future1.get();\\n HttpResponse<String> response2 = future2.get();\\n // 处理响应数据\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n通过虚拟线程并行调用微服务,可以减少整体的响应时间,提升用户体验。
\\n在大型应用系统中,日志量通常非常庞大。实时处理日志数据,如日志分析、统计等,需要高效的并发处理能力。虚拟线程可以快速处理大量的日志数据,满足实时性要求。
\\n例如,使用虚拟线程对日志文件进行实时读取和分析:
\\nimport java.io.BufferedReader;\\nimport java.io.FileReader;\\nimport java.io.IOException;\\nimport java.util.concurrent.CompletableFuture;\\npublic class LogProcessing {\\n public static void main(String[] args) {\\n String logFilePath = \\"path/to/logfile.log\\";\\n CompletableFuture.runAsync(() -> {\\n try (BufferedReader reader = new BufferedReader(new FileReader(logFilePath))) {\\n String line;\\n while ((line = reader.readLine()) != null) {\\n // 分析日志数据\\n analyzeLog(line);\\n }\\n } catch (IOException e) {\\n e.printStackTrace();\\n }\\n }, new VirtualThreadFactory());\\n }\\n private static void analyzeLog(String log) {\\n // 具体的日志分析逻辑\\n System.out.println(\\"Analyzing log: \\" + log);\\n }\\n}\\n
\\n通过这种方式,可以及时处理日志数据,为系统监控和故障排查提供有力支持。
\\n在数据采集领域,需要从大量的网页中爬取数据。由于网页数量众多,传统的线程模型难以高效处理。虚拟线程可以轻松创建大量的爬虫任务,快速获取数据。
\\n以下是一个简单的网页爬虫示例:
\\nimport java.io.BufferedReader;\\nimport java.io.IOException;\\nimport java.io.InputStreamReader;\\nimport java.net.HttpURLConnection;\\nimport java.net.URL;\\nimport java.util.concurrent.CompletableFuture;\\npublic class WebCrawler {\\n public static void main(String[] args) {\\n String[] urls = {\\"http://example1.com\\", \\"http://example2.com\\", \\"http://example3.com\\"};\\n List<CompletableFuture<String>> futures = new ArrayList<>();\\n for (String url : urls) {\\n CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {\\n try {\\n URL obj = new URL(url);\\n HttpURLConnection con = (HttpURLConnection) obj.openConnection();\\n con.setRequestMethod(\\"GET\\");\\n BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));\\n String inputLine;\\n StringBuilder response = new StringBuilder();\\n while ((inputLine = in.readLine()) != null) {\\n response.append(inputLine);\\n }\\n in.close();\\n return response.toString();\\n } catch (IOException e) {\\n e.printStackTrace();\\n return \\"\\";\\n }\\n }, new VirtualThreadFactory());\\n futures.add(future);\\n }\\n CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();\\n try {\\n for (CompletableFuture<String> future : futures) {\\n String content = future.get();\\n // 处理爬取到的网页内容\\n }\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n通过虚拟线程并行发起网页请求,可以大大提高数据爬取的效率。
\\nJava 21 的虚拟线程为开发者提供了强大的并发编程工具,在上述 5 大场景以及更多的应用场景中都能发挥巨大的作用。希望大家能够深入学习和应用虚拟线程,提升自己的 Java 开发水平。
\\n以上两篇文章分别聚焦 Java 性能优化和虚拟线程实战。若你对文章内容深度、案例类型还有别的想法,欢迎随时告诉我。
","description":"爆肝 30 天!从 JVM 调优到百万级 QPS,我的 Java 性能飞升全记录 在 Java 开发的世界里,性能优化是永恒的话题。当系统面临高并发、大数据量时,如何让 Java 应用程序保持高效运行,成为了每个开发者必须攻克的难题。接下来,我将分享自己通过 30 天时间,从 JVM 调优到实现百万级 QPS 的 Java 性能优化全历程,希望能给大家带来启发。\\n\\n一、JVM 调优:性能优化的基石\\n\\nJVM 作为 Java 程序运行的核心环境,其调优至关重要。首先要了解 JVM 的内存结构,包括堆内存、方法区、程序计数器、虚拟机栈和本地方法栈。其中…","guid":"https://juejin.cn/post/7498555335114031158","author":"天天摸鱼的java工程师","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T01:13:50.048Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d4c06b3015e944ccac5c6bb3e2401b9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSp5aSp5pG46bG855qEamF2YeW3peeoi-W4iA==:q75.awebp?rk3s=f64ab15b&x-expires=1746782078&x-signature=8KvfsrwPwnLN5ZEnzzDzQ0wo9w0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"数据库建表时才知道我多菜","url":"https://juejin.cn/post/7498571828120191003","content":"最近建库设计表,弄得有点不自信了,好久没干过这种细活了,真是家狗吃不了细糠不是;\\n下面咱就说道说道这个编码格式及所需字节数之间的关系,一起坐好,上课;
\\n(以下内容均来自于网络学习及ai回答,没深入细节,没深入细节,没深入细节,不对的地方,大佬们一定要指正)
\\n首先说下,咱们目前常用的数据库编码格式,其他字符咱没用过也没见过就不瞎说了。\\nISO 8859-1 GB2312 GBK GB18030 UTF8 UTF16 UTF32 还有个啥UTF8mb4;太多了,是不是,先过滤下。\\n看下u8家族的,当我们在设计数据库时,Unicode码与我们的数据库并不是一一对应的,直接看结果:
\\n数据库中UTF8(实际叫utf8mb3)不等于UTF-8,\\"utf8\\"只支持每个字符最多3个字节, 对于超过3个字节的字符就会出错,而我们的汉字虽然通常在utf8的情况下占三个字节,但是存在占用四个字节的情况,且某些特殊符号也是四个字节,所以utf8淘汰。
\\nUTF-8支持1-4个字节,其最小单元是1个字节,也有说它支持最大6个字节;
\\nutf16的每个字符必须是2个字节或者4个字节,而UTF编码在最小单元为多字节中存在字节顺序的问题,\\n所以UTF-8没这个困扰,但是utf16最小是2字节,所以我们也pass掉吧,费神不是;
\\nutf32呢直接一个字符四个字节,但是呢我们的库表并不需要简单粗暴的定长,而是尽量最优使用存储空间(可以参考oracle);
\\n数据库里的utf8mb4有说他就是纯正的UTF-8,特性类似于UTF-8;(我以前根本不懂这玩意,就在哪看过说utf8mb4支持emoj我就用它了,没想到是对的);\\n那最终我们mysql层面u8家族的就剩一个utf8mb4能打了。\\n再说一下我们的utf8mb4什么时候是一个字节呢,就是内容在ASCII编码范围内(就是128个字母数字符号)的时候是一个字符;
\\n下面这几个一般在oracle上用了(如果mysql也用就当我没说过这句话)
\\n占用一个字节,不支持汉字等其他字符,所以直接淘汰
\\n汉字占用2个字节,非汉字字符(如字母、数字、标点符号等)占用1个字节主要覆盖简体汉字,(对汉字支持不够全面)所以直接淘汰;
\\n兼容GB2312,所需字节数与GB2312一样,GB2312中的字符在GBK中有相同的编码,相对于GB2312添加了繁体字,生僻字,东亚其他文字的支持;(有时我们会使用它)
\\nai给的评价是基本覆盖了中国所有的汉字(包括少数民族文字)和常用字符需求;(我的想法是正常普通业务不需要这么大的,如果你喜欢当我没说)
\\n好了,不同编码格式存储数据所需要的字节数我们差不多知道了吧,下面我们再看看mysql那些讨人厌的字段类型各自的字节数。
\\n我直接复制网上一份过来\\nMySQL 字段类型可以简单分为三大类
\\n先捡简单的说:日期类型其实我们常用的就两个DATETIME 和 TIMESTAMP,其他三个就是字面意思年份、时间、日期;\\n这两个说实话大差不差哈,平时我们都用,整体区别就是DATETIME占八个字节,而 TIMESTAMP占四个字节,DATETIME表示的时间范围更广,TIMESTAMP能表示到2038年,但其可以随时区变化;\\n接着看我们的字符串类型,我此处捡常用的说
\\n简单说就是char是不可变长度,但是varchar是可变长度,这么看好像没啥区别,比如说我们数字类型的字典,那我给varchar(1)岂不是更方便,然后我一顿捣鼓,终于发现存储的区别,varchar会用字节空间来记录字符长度,而char是定长的,不需要记录,这就会让mysql在sql优化的时候会考虑这种情况,所以总能看到前人的总结,固定字符数的用char,字符数不固定就用varchar,有人说char属性的字段如果字符不够会空格填充,又有人说填充仅限于oracle;注意哈,虽然char(1)表示一个字符空间,但是存储依然只能存储一个值哈,简单理解就是它叫字符个数,varchar同理一样;总结下哈,定长(char),可变长(varchar);
\\n这里TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT我就不拆开说了,下图简单看就是一个字节能存储范围是255,那两个就是255*255,依次类推
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n数据类型 | 存储大小(字节) | 有符号取值范围 | 无符号取值范围 |
---|---|---|---|
TINYINT | 1 | -128 到 127 | 0 到 255 |
SMALLINT | 2 | -32768 到 32767 | 0 到 65535 |
MEDIUMINT | 3 | -8388608 到 8388607 | 0 到 16777215 |
INT | 4 | -2147483648 到 2147483647 | 0 到 4294967295 |
BIGINT | 8 | -9223372036854775808 到 9223372036854775807 | 0 到 18446744073709551615 |
那我们经常定义整型字段后面那个位数是啥比如int(M)的M是啥,怎么说呢,你就当它毫无用处吧,因为有说他们表示的是显示宽度,但是mysql8又不推荐了,所以咱们就当不存在,总结就是这里选择的话看你要表示的范围选取合适的,不用管数据库建表时的长度配置;
\\n给我说我就是不推荐,反驳的理由就是精度无法保证,想要精度就别用他,不在乎精度更没必要用它,当然,如果你就说普通的精度控制其实也可以用,但是给我我不会废这个脑子去思考,其实这两个的精度控制在mysql中我欣赏不来,可能是我navicat问题,我还是喜欢在plsql上操作oracle中double的感觉。举例double(M,D)中 M=整数位+小数位,D=小数位;
\\n我也是第一次知道这个叫定点数,DECIMAL(M,D)表示M是最大位数(精度)(整数位+小数位+小数点),范围是1到65。可不指定,默认值是10。\\nD是小数点右边的位数(小数位)。范围是0到30,并且不能大于M,可不指定,默认值是0,
","description":"最近建库设计表,弄得有点不自信了,好久没干过这种细活了,真是家狗吃不了细糠不是; 下面咱就说道说道这个编码格式及所需字节数之间的关系,一起坐好,上课; (以下内容均来自于网络学习及ai回答,没深入细节,没深入细节,没深入细节,不对的地方,大佬们一定要指正)\\n\\n数据库编码格式\\n\\n首先说下,咱们目前常用的数据库编码格式,其他字符咱没用过也没见过就不瞎说了。 ISO 8859-1 GB2312 GBK GB18030 UTF8 UTF16 UTF32 还有个啥UTF8mb4;太多了,是不是,先过滤下。 看下u8家族的,当我们在设计数据库时…","guid":"https://juejin.cn/post/7498571828120191003","author":"小红帽的大灰狼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T01:11:26.539Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a646ae50a8da46dda0f9c178ef5a15f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1746580459&x-signature=ZYLr2DRYvE06gDUIEalHwz0eIMM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e65c87f071ef4174bd76c5ee80bd3b89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1746580459&x-signature=2vzcDPYN0aZVLJuYq3e08I8Rs4g%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"别怪 Python 慢,是你 import 的姿势不对!我亲测提速 3~5 倍","url":"https://juejin.cn/post/7498555335113998390","content":"有一段时间我总觉得,自己写的 Python 项目怎么越写越沉,明明功能没多几个,但打开速度、执行效率就像早高峰的地铁,一步三挪,急死个人。
\\n那时候我还自我安慰:“哎,模块多一点正常啦,Python 嘛,不就是慢点嘛。”
\\n直到有天我给客户部署个 Web 工具,结果人家点了下按钮,加载了快5秒才有动静,我一边装镇定,一边默默看向终端日志——全是模块加载……
那一刻我才意识到,我不是写得慢,是“模块加载方式”出了问题。
\\n今天这篇文章,咱就一起深入聊聊如何通过优化模块加载方式,让你的 Python 项目飞起来!
\\n大部分人写 Python 项目,结构往往是这样的:
\\n# main.py\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nimport numpy as np\\nimport tensorflow as tf\\n...\\n
\\n一上来全导入,管你用不用。
\\n其实在项目早期,确实问题不大,几个模块罢了。但随着功能越堆越多,动辄几十个三方库、上百个自定义模块——启动性能、内存开销、甚至用户体验就开始哐哐下滑。
\\n特别是:
\\n如果你在这些场景里还用“贪婪式导入”,就跟开车忘放手刹一样,自己拉自己后腿。
\\n这个是最直接、最好落地的方式。
\\n就像外卖,不是你打开美团它就开始做饭,而是你点单它才开始做。
\\nLazy Import 就是:只有用到模块的时候才导入,而不是一开始就塞进来。
\\ndef plot_data():\\n import matplotlib.pyplot as plt\\n plt.plot([1, 2, 3])\\n plt.show()\\n
\\n这招最适合“偶尔才用”的库。比如你的工具大部分不画图,只有特定情况下才调用 plot_data()
,那就别让 matplotlib 一开始就拖后腿了。
import importlib\\n\\nclass LazyModule:\\n def __init__(self, module_name):\\n self._module_name = module_name\\n self._module = None\\n\\n def __getattr__(self, attr):\\n if self._module is None:\\n print(f\\"[Lazy] Loading module: {self._module_name}\\")\\n self._module = importlib.import_module(self._module_name)\\n return getattr(self._module, attr)\\n\\n# 用法\\nnp = LazyModule(\\"numpy\\")\\n
\\n当你第一次访问 np.array()
,才会真正加载 numpy。再访问时就走缓存,性能不错还优雅~
lazy-import
(想偷懒就用它)# pip install lazy-import\\nimport lazy_import\\nnp = lazy_import.lazy_module(\\"numpy\\")\\n
\\n简单粗暴,适合快速接入。就是调试时注意,IDE 可能不识别这些懒模块,补全啥的会断。
\\n我之前做的一个数据处理工具,起初所有数据源处理函数都放一个 main.py
里,开头全是 import:
import mysql.connector\\nimport pymongo\\nimport boto3\\nimport pyodbc\\n...\\n
\\n结果你用一个 CSV 功能,也得加载 MongoDB 和 S3,太傻了。
\\n后来我一刀切,把每类数据源放独立模块:
\\nproject |\\n ├── mysql_loader.py\\n ├── mongo_loader.py\\n ├── s3_loader.py\\n ├── csv_loader.py\\n
\\n每个模块只导入自己依赖的东西,main.py
只根据用户选择按需加载对应模块,启动速度直接翻倍!
✅ 最佳实践:
\\n有些库很“活泼”,import 时就跑一堆事:
\\n# utils/logger.py\\nimport logging\\n\\nlogging.basicConfig(...) # 这行直接会跑!\\n
\\n如果你导入这个模块,哪怕你压根没用它,logging 配置就改了!
\\n✅ 最佳做法:把副作用动作写成函数:
\\n# utils/logger.py\\ndef setup_logger():\\n logging.basicConfig(...)\\n
\\n然后用的时候再 setup_logger()
,控制权回到你手上。
很多人喜欢在 __init__.py
里自动 import 模块,比如这样:
# mylib/__init__.py\\nfrom .foo import *\\nfrom .bar import *\\n
\\n你以为方便了,结果一引入 mylib,后面一堆 foo、bar 全跑来了。对懒加载来说,这操作是直接判死刑。
\\n建议保守点:让用户显式导入你提供的功能,别偷偷来。
\\n懒加载有个隐藏雷区——模块互相依赖时很容易触发循环导入。
\\n比如:
\\n# a.py\\nfrom b import func_b\\n\\ndef func_a():\\n func_b()\\n\\n# b.py\\nfrom a import func_a\\n
\\n你加了 Lazy Import,可能更不容易察觉 bug。建议结构清晰、依赖单向,或者统一用延迟导入函数化。
\\n技术手段 | 优点 | 使用建议 |
---|---|---|
函数内部导入 | 简单、实用 | 小项目、工具函数优先用 |
LazyModule 封装 | 优雅、好维护 | 大项目通用,推荐封装统一使用 |
lazy-import 库 | 最快接入、最省事 | 临时用,或快速试验场景 |
模块结构优化 | 提升解耦、控制加载粒度 | 项目规模一旦变大就要考虑 |
延迟初始化 | 避免副作用 | logging、db 初始化必须延迟 |
控制 init.py 行为 | 降低不必要的预加载 | 不推荐放默认导入 |
优化加载这事儿,说大不大,说小不小。但它是那种藏在细节里的功夫,不会在你第一次开发时显山露水,却会在项目做大后狠狠反噬。
\\n就像我有次看同事的代码启动慢,硬件都换了还是慢,结果问题出在:项目一跑,直接加载了十几个从没用到的分析模块……
\\n性能优化从来不是大刀阔斧开始的,往往是从一次 import
的反思、一个模块结构的整理开始。
你不优化,项目不报错——但你一优化,它就飞了。
","description":"别怪 Python 慢,是你 import 的姿势不对!我亲测提速 3~5 倍 有一段时间我总觉得,自己写的 Python 项目怎么越写越沉,明明功能没多几个,但打开速度、执行效率就像早高峰的地铁,一步三挪,急死个人。\\n\\n那时候我还自我安慰:“哎,模块多一点正常啦,Python 嘛,不就是慢点嘛。”\\n 直到有天我给客户部署个 Web 工具,结果人家点了下按钮,加载了快5秒才有动静,我一边装镇定,一边默默看向终端日志——全是模块加载……\\n\\n那一刻我才意识到,我不是写得慢,是“模块加载方式”出了问题。\\n\\n今天这篇文章,咱就一起深入聊聊如何通过优化模块加载方式,让你…","guid":"https://juejin.cn/post/7498555335113998390","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T01:10:36.905Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4fe7d6490b274a068ec187c2673aa76b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1747182240&x-signature=BiKGWqXLleI6CZnM%2BlXpVAldU%2FE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot中6种拦截器使用场景","url":"https://juejin.cn/post/7498555281430282303","content":"在构建企业级Web应用时,我们经常需要在请求处理的不同阶段执行一些通用逻辑,如权限验证、日志记录、性能监控等。Spring MVC的拦截器(Interceptor)机制提供了一种优雅的方式来实现这些横切关注点,而不必在每个控制器中重复编写相同的代码。
\\n本文将介绍SpringBoot中6种常见的拦截器使用场景及其实现方式。
\\n拦截器是Spring MVC框架提供的一种机制,用于在控制器(Controller)处理请求前后执行特定的逻辑。
\\n拦截器通过实现HandlerInterceptor
接口来定义,该接口包含三个核心方法:
用户认证拦截器主要用于:
\\n@Component\\npublic class AuthenticationInterceptor implements HandlerInterceptor {\\n \\n @Autowired\\n private JwtTokenProvider jwtTokenProvider;\\n \\n @Autowired\\n private UserService userService;\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n // 跳过非控制器方法的处理\\n if (!(handler instanceof HandlerMethod)) {\\n return true;\\n }\\n \\n HandlerMethod handlerMethod = (HandlerMethod) handler;\\n \\n // 检查是否有@PermitAll注解,有则跳过认证\\n PermitAll permitAll = handlerMethod.getMethodAnnotation(PermitAll.class);\\n if (permitAll != null) {\\n return true;\\n }\\n \\n // 从请求头中获取token\\n String token = request.getHeader(\\"Authorization\\");\\n if (token == null || !token.startsWith(\\"Bearer \\")) {\\n response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\\n response.getWriter().write(\\"{\\"error\\": \\"未授权,请先登录\\"}\\");\\n return false;\\n }\\n \\n token = token.substring(7); // 去掉\\"Bearer \\"前缀\\n \\n try {\\n // 验证token\\n if (!jwtTokenProvider.validateToken(token)) {\\n response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\\n response.getWriter().write(\\"{\\"error\\": \\"Token已失效,请重新登录\\"}\\");\\n return false;\\n }\\n \\n // 从token中获取用户信息并设置到请求属性中\\n String username = jwtTokenProvider.getUsernameFromToken(token);\\n User user = userService.findByUsername(username);\\n \\n if (user == null) {\\n response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\\n response.getWriter().write(\\"{\\"error\\": \\"用户不存在\\"}\\");\\n return false;\\n }\\n \\n // 检查方法是否有@RequireRole注解\\n RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);\\n if (requireRole != null) {\\n // 检查用户是否有所需角色\\n String[] roles = requireRole.value();\\n boolean hasRole = false;\\n for (String role : roles) {\\n if (user.hasRole(role)) {\\n hasRole = true;\\n break;\\n }\\n }\\n \\n if (!hasRole) {\\n response.setStatus(HttpServletResponse.SC_FORBIDDEN);\\n response.getWriter().write(\\"{\\"error\\": \\"权限不足\\"}\\");\\n return false;\\n }\\n }\\n \\n // 将用户信息放入请求属性\\n request.setAttribute(\\"currentUser\\", user);\\n \\n return true;\\n } catch (Exception e) {\\n response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\\n response.getWriter().write(\\"{\\"error\\": \\"Token验证失败\\"}\\");\\n return false;\\n }\\n }\\n}\\n
\\n@Configuration\\npublic class WebMvcConfig implements WebMvcConfigurer {\\n \\n @Autowired\\n private AuthenticationInterceptor authenticationInterceptor;\\n \\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n registry.addInterceptor(authenticationInterceptor)\\n .addPathPatterns(\\"/api/**\\")\\n .excludePathPatterns(\\"/api/auth/login\\", \\"/api/auth/register\\");\\n }\\n}\\n
\\n@Target(ElementType.METHOD)\\n@Retention(RetentionPolicy.RUNTIME)\\npublic @interface PermitAll {\\n}\\n\\n@Target(ElementType.METHOD)\\n@Retention(RetentionPolicy.RUNTIME)\\npublic @interface RequireRole {\\n String[] value();\\n}\\n
\\n日志记录拦截器主要用于:
\\n@Component\\n@Slf4j\\npublic class LoggingInterceptor implements HandlerInterceptor {\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n // 记录请求开始时间\\n long startTime = System.currentTimeMillis();\\n request.setAttribute(\\"startTime\\", startTime);\\n \\n // 记录请求信息\\n String requestURI = request.getRequestURI();\\n String method = request.getMethod();\\n String remoteAddr = request.getRemoteAddr();\\n String userAgent = request.getHeader(\\"User-Agent\\");\\n \\n // 获取当前用户(如果已通过认证拦截器)\\n Object currentUser = request.getAttribute(\\"currentUser\\");\\n String username = currentUser != null ? ((User) currentUser).getUsername() : \\"anonymous\\";\\n \\n // 记录请求参数\\n Map<String, String[]> paramMap = request.getParameterMap();\\n StringBuilder params = new StringBuilder();\\n \\n if (!paramMap.isEmpty()) {\\n for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {\\n params.append(entry.getKey())\\n .append(\\"=\\")\\n .append(String.join(\\",\\", entry.getValue()))\\n .append(\\"&\\");\\n }\\n \\n if (params.length() > 0) {\\n params.deleteCharAt(params.length() - 1);\\n }\\n }\\n \\n // 记录请求体(仅POST/PUT/PATCH请求)\\n String requestBody = \\"\\";\\n if (HttpMethod.POST.matches(method) || \\n HttpMethod.PUT.matches(method) || \\n HttpMethod.PATCH.matches(method)) {\\n \\n // 使用包装请求对象来多次读取请求体\\n ContentCachingRequestWrapper wrappedRequest = \\n new ContentCachingRequestWrapper(request);\\n \\n // 为了触发内容缓存,我们需要获取一次输入流\\n if (wrappedRequest.getContentLength() > 0) {\\n wrappedRequest.getInputStream().read();\\n requestBody = new String(wrappedRequest.getContentAsByteArray(), \\n wrappedRequest.getCharacterEncoding());\\n }\\n }\\n \\n log.info(\\n \\"REQUEST: {} {} from={} user={} userAgent={} params={} body={}\\",\\n method,\\n requestURI,\\n remoteAddr,\\n username,\\n userAgent,\\n params,\\n requestBody\\n );\\n \\n return true;\\n }\\n \\n @Override\\n public void afterCompletion(HttpServletRequest request, HttpServletResponse response, \\n Object handler, Exception ex) throws Exception {\\n \\n // 计算请求处理时间\\n long startTime = (Long) request.getAttribute(\\"startTime\\");\\n long endTime = System.currentTimeMillis();\\n long processingTime = endTime - startTime;\\n \\n // 记录响应状态和处理时间\\n int status = response.getStatus();\\n String requestURI = request.getRequestURI();\\n String method = request.getMethod();\\n \\n if (ex != null) {\\n log.error(\\n \\"RESPONSE: {} {} status={} time={}ms error={}\\",\\n method,\\n requestURI,\\n status,\\n processingTime,\\n ex.getMessage()\\n );\\n } else {\\n log.info(\\n \\"RESPONSE: {} {} status={} time={}ms\\",\\n method,\\n requestURI,\\n status,\\n processingTime\\n );\\n }\\n }\\n}\\n
\\n@Bean\\npublic FilterRegistrationBean<ContentCachingFilter> contentCachingFilter() {\\n FilterRegistrationBean<ContentCachingFilter> registrationBean = new FilterRegistrationBean<>();\\n registrationBean.setFilter(new ContentCachingFilter());\\n registrationBean.addUrlPatterns(\\"/api/*\\");\\n return registrationBean;\\n}\\n\\n@Override\\npublic void addInterceptors(InterceptorRegistry registry) {\\n registry.addInterceptor(loggingInterceptor)\\n .addPathPatterns(\\"/**\\");\\n}\\n
\\npublic class ContentCachingFilter extends OncePerRequestFilter {\\n \\n @Override\\n protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, \\n FilterChain filterChain) throws ServletException, IOException {\\n \\n ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);\\n ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);\\n \\n try {\\n filterChain.doFilter(wrappedRequest, wrappedResponse);\\n } finally {\\n wrappedResponse.copyBodyToResponse();\\n }\\n }\\n}\\n
\\n性能监控拦截器主要用于:
\\n@Component\\n@Slf4j\\npublic class PerformanceMonitorInterceptor implements HandlerInterceptor {\\n \\n // 慢请求阈值,单位毫秒\\n @Value(\\"${app.performance.slow-request-threshold:500}\\")\\n private long slowRequestThreshold;\\n \\n @Autowired\\n private MetricsService metricsService;\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n if (handler instanceof HandlerMethod) {\\n HandlerMethod handlerMethod = (HandlerMethod) handler;\\n String controllerName = handlerMethod.getBeanType().getSimpleName();\\n String methodName = handlerMethod.getMethod().getName();\\n \\n request.setAttribute(\\"controllerName\\", controllerName);\\n request.setAttribute(\\"methodName\\", methodName);\\n request.setAttribute(\\"startTime\\", System.currentTimeMillis());\\n }\\n \\n return true;\\n }\\n \\n @Override\\n public void afterCompletion(HttpServletRequest request, HttpServletResponse response, \\n Object handler, Exception ex) throws Exception {\\n \\n Long startTime = (Long) request.getAttribute(\\"startTime\\");\\n \\n if (startTime != null) {\\n long processingTime = System.currentTimeMillis() - startTime;\\n \\n String controllerName = (String) request.getAttribute(\\"controllerName\\");\\n String methodName = (String) request.getAttribute(\\"methodName\\");\\n String uri = request.getRequestURI();\\n \\n // 记录性能数据\\n metricsService.recordApiPerformance(controllerName, methodName, uri, processingTime);\\n \\n // 记录慢请求\\n if (processingTime > slowRequestThreshold) {\\n log.warn(\\"Slow API detected: {} {}.{} - {}ms (threshold: {}ms)\\",\\n uri, controllerName, methodName, processingTime, slowRequestThreshold);\\n \\n // 记录慢请求到专门的监控系统\\n metricsService.recordSlowRequest(controllerName, methodName, uri, processingTime);\\n }\\n }\\n }\\n}\\n
\\n@Service\\n@Slf4j\\npublic class MetricsServiceImpl implements MetricsService {\\n \\n // 使用滑动窗口记录最近的性能数据\\n private final ConcurrentMap<String, SlidingWindowMetric> apiMetrics = new ConcurrentHashMap<>();\\n \\n // 慢请求记录队列\\n private final Queue<SlowRequestRecord> slowRequests = new ConcurrentLinkedQueue<>();\\n \\n // 保留最近1000条慢请求记录\\n private static final int MAX_SLOW_REQUESTS = 1000;\\n \\n @Override\\n public void recordApiPerformance(String controller, String method, String uri, long processingTime) {\\n String apiKey = controller + \\".\\" + method;\\n \\n apiMetrics.computeIfAbsent(apiKey, k -> new SlidingWindowMetric())\\n .addSample(processingTime);\\n \\n // 可以在这里添加Prometheus或其他监控系统的指标记录\\n }\\n \\n @Override\\n public void recordSlowRequest(String controller, String method, String uri, long processingTime) {\\n SlowRequestRecord record = new SlowRequestRecord(\\n controller, method, uri, processingTime, LocalDateTime.now()\\n );\\n \\n slowRequests.add(record);\\n \\n // 如果队列超过最大容量,移除最早的记录\\n while (slowRequests.size() > MAX_SLOW_REQUESTS) {\\n slowRequests.poll();\\n }\\n }\\n \\n @Override\\n public List<ApiPerformanceMetric> getApiPerformanceMetrics() {\\n List<ApiPerformanceMetric> metrics = new ArrayList<>();\\n \\n for (Map.Entry<String, SlidingWindowMetric> entry : apiMetrics.entrySet()) {\\n String[] parts = entry.getKey().split(\\"\\\\.\\");\\n String controller = parts[0];\\n String method = parts.length > 1 ? parts[1] : \\"\\";\\n \\n SlidingWindowMetric metric = entry.getValue();\\n \\n metrics.add(new ApiPerformanceMetric(\\n controller,\\n method,\\n metric.getAvg(),\\n metric.getMin(),\\n metric.getMax(),\\n metric.getCount()\\n ));\\n }\\n \\n return metrics;\\n }\\n \\n @Override\\n public List<SlowRequestRecord> getSlowRequests() {\\n return new ArrayList<>(slowRequests);\\n }\\n \\n // 滑动窗口指标类\\n private static class SlidingWindowMetric {\\n private final LongAdder count = new LongAdder();\\n private final LongAdder sum = new LongAdder();\\n private final AtomicLong min = new AtomicLong(Long.MAX_VALUE);\\n private final AtomicLong max = new AtomicLong(0);\\n \\n public void addSample(long value) {\\n count.increment();\\n sum.add(value);\\n \\n // 更新最小值\\n while (true) {\\n long currentMin = min.get();\\n if (value >= currentMin || min.compareAndSet(currentMin, value)) {\\n break;\\n }\\n }\\n \\n // 更新最大值\\n while (true) {\\n long currentMax = max.get();\\n if (value <= currentMax || max.compareAndSet(currentMax, value)) {\\n break;\\n }\\n }\\n }\\n \\n public long getCount() {\\n return count.sum();\\n }\\n \\n public double getAvg() {\\n long countValue = count.sum();\\n return countValue > 0 ? (double) sum.sum() / countValue : 0;\\n }\\n \\n public long getMin() {\\n return min.get() == Long.MAX_VALUE ? 0 : min.get();\\n }\\n \\n public long getMax() {\\n return max.get();\\n }\\n }\\n}\\n
\\n@Data\\n@AllArgsConstructor\\npublic class ApiPerformanceMetric {\\n private String controllerName;\\n private String methodName;\\n private double avgProcessingTime;\\n private long minProcessingTime;\\n private long maxProcessingTime;\\n private long requestCount;\\n}\\n\\n@Data\\n@AllArgsConstructor\\npublic class SlowRequestRecord {\\n private String controllerName;\\n private String methodName;\\n private String uri;\\n private long processingTime;\\n private LocalDateTime timestamp;\\n}\\n
\\npublic interface MetricsService {\\n void recordApiPerformance(String controller, String method, String uri, long processingTime);\\n \\n void recordSlowRequest(String controller, String method, String uri, long processingTime);\\n \\n List<ApiPerformanceMetric> getApiPerformanceMetrics();\\n \\n List<SlowRequestRecord> getSlowRequests();\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/admin/metrics\\")\\npublic class MetricsController {\\n \\n @Autowired\\n private MetricsService metricsService;\\n \\n @GetMapping(\\"/api-performance\\")\\n public List<ApiPerformanceMetric> getApiPerformanceMetrics() {\\n return metricsService.getApiPerformanceMetrics();\\n }\\n \\n @GetMapping(\\"/slow-requests\\")\\n public List<SlowRequestRecord> getSlowRequests() {\\n return metricsService.getSlowRequests();\\n }\\n}\\n
\\n接口限流拦截器主要用于:
\\n@Component\\n@Slf4j\\npublic class RateLimitInterceptor implements HandlerInterceptor {\\n \\n @Autowired\\n private RedisTemplate<String, Object> redisTemplate;\\n \\n @Value(\\"${app.rate-limit.enabled:true}\\")\\n private boolean enabled;\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n if (!enabled) {\\n return true;\\n }\\n \\n if (!(handler instanceof HandlerMethod)) {\\n return true;\\n }\\n \\n HandlerMethod handlerMethod = (HandlerMethod) handler;\\n \\n // 获取限流注解\\n RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);\\n if (rateLimit == null) {\\n // 没有配置限流注解,不进行限流\\n return true;\\n }\\n \\n // 获取限流类型\\n RateLimitType limitType = rateLimit.type();\\n \\n // 根据限流类型获取限流键\\n String limitKey = getLimitKey(request, limitType);\\n \\n // 获取限流配置\\n int limit = rateLimit.limit();\\n int period = rateLimit.period();\\n \\n // 执行限流检查\\n boolean allowed = checkRateLimit(limitKey, limit, period);\\n \\n if (!allowed) {\\n // 超过限流,返回429状态码\\n response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());\\n response.setContentType(MediaType.APPLICATION_JSON_VALUE);\\n response.getWriter().write(\\"{\\"error\\":\\"Too many requests\\",\\"message\\":\\"请求频率超过限制,请稍后再试\\"}\\");\\n return false;\\n }\\n \\n return true;\\n }\\n \\n private String getLimitKey(HttpServletRequest request, RateLimitType limitType) {\\n String key = \\"rate_limit:\\";\\n \\n switch (limitType) {\\n case IP:\\n key += \\"ip:\\" + getClientIp(request);\\n break;\\n case USER:\\n // 从认证信息获取用户ID\\n Object currentUser = request.getAttribute(\\"currentUser\\");\\n String userId = currentUser != null ? \\n String.valueOf(((User) currentUser).getId()) : \\"anonymous\\";\\n key += \\"user:\\" + userId;\\n break;\\n case API:\\n key += \\"api:\\" + request.getRequestURI();\\n break;\\n case IP_API:\\n key += \\"ip_api:\\" + getClientIp(request) + \\":\\" + request.getRequestURI();\\n break;\\n case USER_API:\\n Object user = request.getAttribute(\\"currentUser\\");\\n String id = user != null ? \\n String.valueOf(((User) user).getId()) : \\"anonymous\\";\\n key += \\"user_api:\\" + id + \\":\\" + request.getRequestURI();\\n break;\\n default:\\n key += \\"global\\";\\n }\\n \\n return key;\\n }\\n \\n private boolean checkRateLimit(String key, int limit, int period) {\\n // 使用Redis的原子操作进行限流检查\\n Long count = redisTemplate.execute(connection -> {\\n // 递增计数器\\n Long currentCount = connection.stringCommands().incr(key.getBytes());\\n \\n // 如果是第一次递增,设置过期时间\\n if (currentCount != null && currentCount == 1) {\\n connection.keyCommands().expire(key.getBytes(), period);\\n }\\n \\n return currentCount;\\n }, true);\\n \\n return count != null && count <= limit;\\n }\\n \\n private String getClientIp(HttpServletRequest request) {\\n String ipAddress = request.getHeader(\\"X-Forwarded-For\\");\\n \\n if (ipAddress == null || ipAddress.isEmpty() || \\"unknown\\".equalsIgnoreCase(ipAddress)) {\\n ipAddress = request.getHeader(\\"Proxy-Client-IP\\");\\n }\\n \\n if (ipAddress == null || ipAddress.isEmpty() || \\"unknown\\".equalsIgnoreCase(ipAddress)) {\\n ipAddress = request.getHeader(\\"WL-Proxy-Client-IP\\");\\n }\\n \\n if (ipAddress == null || ipAddress.isEmpty() || \\"unknown\\".equalsIgnoreCase(ipAddress)) {\\n ipAddress = request.getRemoteAddr();\\n if (\\"127.0.0.1\\".equals(ipAddress) || \\"0:0:0:0:0:0:0:1\\".equals(ipAddress)) {\\n // 根据网卡取本机配置的IP\\n try {\\n InetAddress inet = InetAddress.getLocalHost();\\n ipAddress = inet.getHostAddress();\\n } catch (UnknownHostException e) {\\n log.error(\\"获取本机IP失败\\", e);\\n }\\n }\\n }\\n \\n // 对于多个代理的情况,第一个IP为客户端真实IP\\n if (ipAddress != null && ipAddress.contains(\\",\\")) {\\n ipAddress = ipAddress.substring(0, ipAddress.indexOf(\\",\\"));\\n }\\n \\n return ipAddress;\\n }\\n}\\n
\\n@Target(ElementType.METHOD)\\n@Retention(RetentionPolicy.RUNTIME)\\npublic @interface RateLimit {\\n \\n /**\\n * 限流类型\\n */\\n RateLimitType type() default RateLimitType.IP;\\n \\n /**\\n * 限制次数\\n */\\n int limit() default 100;\\n \\n /**\\n * 时间周期(秒)\\n */\\n int period() default 60;\\n}\\n\\npublic enum RateLimitType {\\n /**\\n * 按IP地址限流\\n */\\n IP,\\n \\n /**\\n * 按用户限流\\n */\\n USER,\\n \\n /**\\n * 按接口限流\\n */\\n API,\\n \\n /**\\n * 按IP和接口组合限流\\n */\\n IP_API,\\n \\n /**\\n * 按用户和接口组合限流\\n */\\n USER_API,\\n \\n /**\\n * 全局限流\\n */\\n GLOBAL\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/api/products\\")\\npublic class ProductController {\\n \\n @Autowired\\n private ProductService productService;\\n \\n @GetMapping\\n @RateLimit(type = RateLimitType.IP, limit = 100, period = 60)\\n public List<Product> getProducts() {\\n return productService.findAll();\\n }\\n \\n @GetMapping(\\"/{id}\\")\\n @RateLimit(type = RateLimitType.IP, limit = 200, period = 60)\\n public Product getProduct(@PathVariable Long id) {\\n return productService.findById(id)\\n .orElseThrow(() -> new ResourceNotFoundException(\\"Product not found\\"));\\n }\\n \\n @PostMapping\\n @RequireRole(\\"ADMIN\\")\\n @RateLimit(type = RateLimitType.USER, limit = 10, period = 60)\\n public Product createProduct(@RequestBody @Valid ProductRequest productRequest) {\\n return productService.save(productRequest);\\n }\\n}\\n
\\n请求参数验证拦截器主要用于:
\\n@Component\\n@Slf4j\\npublic class RequestValidationInterceptor implements HandlerInterceptor {\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n if (!(handler instanceof HandlerMethod)) {\\n return true;\\n }\\n \\n HandlerMethod handlerMethod = (HandlerMethod) handler;\\n \\n // 检查方法参数是否需要验证\\n Parameter[] parameters = handlerMethod.getMethod().getParameters();\\n \\n for (Parameter parameter : parameters) {\\n // 检查是否有@RequestBody注解\\n if (parameter.isAnnotationPresent(RequestBody.class) && \\n parameter.isAnnotationPresent(Valid.class)) {\\n \\n // 该参数需要验证,在控制器方法中会自动验证\\n // 这里只需确保我们能处理验证失败的情况\\n // 通过全局异常处理器处理MethodArgumentNotValidException\\n \\n // 记录验证将要发生\\n log.debug(\\"将对 {}.{} 的请求体参数 {} 进行验证\\", \\n handlerMethod.getBeanType().getSimpleName(),\\n handlerMethod.getMethod().getName(),\\n parameter.getName());\\n }\\n \\n // 检查是否有@RequestParam注解\\n RequestParam requestParam = parameter.getAnnotation(RequestParam.class);\\n if (requestParam != null) {\\n String paramName = requestParam.value().isEmpty() ? \\n parameter.getName() : requestParam.value();\\n \\n String paramValue = request.getParameter(paramName);\\n \\n // 检查必填参数\\n if (requestParam.required() && (paramValue == null || paramValue.isEmpty())) {\\n response.setStatus(HttpStatus.BAD_REQUEST.value());\\n response.setContentType(MediaType.APPLICATION_JSON_VALUE);\\n response.getWriter().write(\\n \\"{\\"error\\":\\"参数错误\\",\\"message\\":\\"缺少必填参数: \\" + paramName + \\"\\"}\\");\\n return false;\\n }\\n \\n // 检查参数格式(如果有注解)\\n if (parameter.isAnnotationPresent(Pattern.class) && paramValue != null) {\\n Pattern pattern = parameter.getAnnotation(Pattern.class);\\n if (!paramValue.matches(pattern.regexp())) {\\n response.setStatus(HttpStatus.BAD_REQUEST.value());\\n response.setContentType(MediaType.APPLICATION_JSON_VALUE);\\n response.getWriter().write(\\n \\"{\\"error\\":\\"参数错误\\",\\"message\\":\\"参数 \\" + \\n paramName + \\" 格式不正确: \\" + pattern.message() + \\"\\"}\\");\\n return false;\\n }\\n }\\n }\\n \\n // 检查是否有@PathVariable注解\\n PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);\\n if (pathVariable != null) {\\n // 对于PathVariable的验证主要依赖RequestMappingHandlerMapping的正则匹配\\n // 这里可以添加额外的验证逻辑,如数值范围检查等\\n }\\n }\\n \\n return true;\\n }\\n}\\n
\\n@RestControllerAdvice\\n@Slf4j\\npublic class GlobalExceptionHandler {\\n \\n /**\\n * 处理请求体参数验证失败的异常\\n */\\n @ExceptionHandler(MethodArgumentNotValidException.class)\\n public ResponseEntity<Map<String, Object>> handleValidationExceptions(\\n MethodArgumentNotValidException ex) {\\n \\n Map<String, String> errors = new HashMap<>();\\n \\n ex.getBindingResult().getAllErrors().forEach(error -> {\\n String fieldName = ((FieldError) error).getField();\\n String errorMessage = error.getDefaultMessage();\\n errors.put(fieldName, errorMessage);\\n });\\n \\n Map<String, Object> body = new HashMap<>();\\n body.put(\\"error\\", \\"参数验证失败\\");\\n body.put(\\"details\\", errors);\\n \\n return ResponseEntity.badRequest().body(body);\\n }\\n \\n /**\\n * 处理请求参数绑定失败的异常\\n */\\n @ExceptionHandler(MissingServletRequestParameterException.class)\\n public ResponseEntity<Map<String, Object>> handleMissingParams(\\n MissingServletRequestParameterException ex) {\\n \\n Map<String, Object> body = new HashMap<>();\\n body.put(\\"error\\", \\"参数错误\\");\\n body.put(\\"message\\", \\"缺少必填参数: \\" + ex.getParameterName());\\n \\n return ResponseEntity.badRequest().body(body);\\n }\\n \\n /**\\n * 处理路径参数类型不匹配的异常\\n */\\n @ExceptionHandler(MethodArgumentTypeMismatchException.class)\\n public ResponseEntity<Map<String, Object>> handleTypeMismatch(\\n MethodArgumentTypeMismatchException ex) {\\n \\n Map<String, Object> body = new HashMap<>();\\n body.put(\\"error\\", \\"参数类型错误\\");\\n body.put(\\"message\\", \\"参数 \\" + ex.getName() + \\" 应为 \\" + \\n ex.getRequiredType().getSimpleName() + \\" 类型\\");\\n \\n return ResponseEntity.badRequest().body(body);\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @Autowired\\n private UserService userService;\\n \\n @GetMapping\\n public List<User> getUsers(\\n @RequestParam(required = false) \\n @Pattern(regexp = \\"^[a-zA-Z0-9]+$\\", message = \\"只能包含字母和数字\\") \\n String keyword,\\n \\n @RequestParam(defaultValue = \\"0\\") \\n @Min(value = 0, message = \\"页码不能小于0\\") \\n Integer page,\\n \\n @RequestParam(defaultValue = \\"10\\") \\n @Min(value = 1, message = \\"每页条数不能小于1\\") \\n @Max(value = 100, message = \\"每页条数不能大于100\\")\\n Integer size) {\\n \\n return userService.findUsers(keyword, page, size);\\n }\\n \\n @PostMapping\\n public User createUser(@RequestBody @Valid UserCreateRequest request) {\\n return userService.createUser(request);\\n }\\n \\n @GetMapping(\\"/{id}\\")\\n public User getUser(@PathVariable @Positive(message = \\"用户ID必须为正整数\\") Long id) {\\n return userService.findById(id)\\n .orElseThrow(() -> new ResourceNotFoundException(\\"User not found\\"));\\n }\\n}\\n
\\n@Data\\npublic class UserCreateRequest {\\n \\n @NotBlank(message = \\"用户名不能为空\\")\\n @Size(min = 4, max = 20, message = \\"用户名长度必须在4-20之间\\")\\n @Pattern(regexp = \\"^[a-zA-Z0-9_]+$\\", message = \\"用户名只能包含字母、数字和下划线\\")\\n private String username;\\n \\n @NotBlank(message = \\"密码不能为空\\")\\n @Size(min = 6, max = 20, message = \\"密码长度必须在6-20之间\\")\\n @Pattern(regexp = \\"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d).+$\\", \\n message = \\"密码必须包含大小写字母和数字\\")\\n private String password;\\n \\n @NotBlank(message = \\"邮箱不能为空\\")\\n @Email(message = \\"邮箱格式不正确\\")\\n private String email;\\n \\n @NotBlank(message = \\"手机号不能为空\\")\\n @Pattern(regexp = \\"^1[3-9]\\\\d{9}$\\", message = \\"手机号格式不正确\\")\\n private String phone;\\n \\n @NotNull(message = \\"年龄不能为空\\")\\n @Min(value = 18, message = \\"年龄必须大于或等于18岁\\")\\n @Max(value = 120, message = \\"年龄必须小于或等于120岁\\")\\n private Integer age;\\n \\n @NotEmpty(message = \\"角色不能为空\\")\\n private List<String> roles;\\n \\n @Valid\\n private Address address;\\n}\\n\\n@Data\\npublic class Address {\\n \\n @NotBlank(message = \\"省份不能为空\\")\\n private String province;\\n \\n @NotBlank(message = \\"城市不能为空\\")\\n private String city;\\n \\n @NotBlank(message = \\"详细地址不能为空\\")\\n private String detail;\\n \\n @Pattern(regexp = \\"^\\\\d{6}$\\", message = \\"邮编必须为6位数字\\")\\n private String zipCode;\\n}\\n
\\n国际化处理拦截器主要用于:
\\n@Component\\npublic class LocaleChangeInterceptor implements HandlerInterceptor {\\n \\n @Autowired\\n private MessageSource messageSource;\\n \\n private final List<Locale> supportedLocales = Arrays.asList(\\n Locale.ENGLISH, // en\\n Locale.SIMPLIFIED_CHINESE, // zh_CN\\n Locale.TRADITIONAL_CHINESE, // zh_TW\\n Locale.JAPANESE, // ja\\n Locale.KOREAN // ko\\n );\\n \\n // 默认语言\\n private final Locale defaultLocale = Locale.ENGLISH;\\n \\n // 语言参数名\\n private String paramName = \\"lang\\";\\n \\n // 用于检测语言的HTTP头\\n private List<String> localeHeaders = Arrays.asList(\\n \\"Accept-Language\\",\\n \\"X-Locale\\"\\n );\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n // 尝试从请求参数中获取语言设置\\n String localeParam = request.getParameter(paramName);\\n Locale locale = null;\\n \\n if (localeParam != null && !localeParam.isEmpty()) {\\n locale = parseLocale(localeParam);\\n }\\n \\n // 如果请求参数中没有有效的语言设置,尝试从HTTP头中获取\\n if (locale == null) {\\n for (String header : localeHeaders) {\\n String localeHeader = request.getHeader(header);\\n if (localeHeader != null && !localeHeader.isEmpty()) {\\n locale = parseLocaleFromHeader(localeHeader);\\n if (locale != null) {\\n break;\\n }\\n }\\n }\\n }\\n \\n // 如果无法确定语言,使用默认语言\\n if (locale == null) {\\n locale = defaultLocale;\\n }\\n \\n // 将解析出的语言设置到LocaleContextHolder中\\n LocaleContextHolder.setLocale(locale);\\n \\n // 将语言信息放入请求属性中,便于在视图中使用\\n request.setAttribute(\\"currentLocale\\", locale);\\n \\n return true;\\n }\\n \\n @Override\\n public void afterCompletion(HttpServletRequest request, HttpServletResponse response, \\n Object handler, Exception ex) throws Exception {\\n // 请求结束后清除语言设置\\n LocaleContextHolder.resetLocaleContext();\\n }\\n \\n /**\\n * 解析语言参数\\n */\\n private Locale parseLocale(String localeParam) {\\n Locale requestedLocale = Locale.forLanguageTag(localeParam.replace(\'_\', \'-\'));\\n \\n // 检查请求的语言是否在支持的语言列表中\\n for (Locale supportedLocale : supportedLocales) {\\n if (supportedLocale.getLanguage().equals(requestedLocale.getLanguage())) {\\n // 如果语言匹配,但国家可能不同,使用完整的支持语言\\n return supportedLocale;\\n }\\n }\\n \\n return null;\\n }\\n \\n /**\\n * 从Accept-Language头解析语言\\n */\\n private Locale parseLocaleFromHeader(String headerValue) {\\n // 解析Accept-Language头,格式如: \\"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7\\"\\n String[] parts = headerValue.split(\\",\\");\\n \\n for (String part : parts) {\\n String[] subParts = part.split(\\";\\");\\n String localeValue = subParts[0].trim();\\n \\n Locale locale = Locale.forLanguageTag(localeValue.replace(\'_\', \'-\'));\\n \\n // 检查是否是支持的语言\\n for (Locale supportedLocale : supportedLocales) {\\n if (supportedLocale.getLanguage().equals(locale.getLanguage())) {\\n return supportedLocale;\\n }\\n }\\n }\\n \\n return null;\\n }\\n}\\n
\\n@Configuration\\npublic class LocaleConfig {\\n \\n @Bean\\n public LocaleResolver localeResolver() {\\n SessionLocaleResolver resolver = new SessionLocaleResolver();\\n resolver.setDefaultLocale(Locale.ENGLISH);\\n return resolver;\\n }\\n \\n @Bean\\n public MessageSource messageSource() {\\n ReloadableResourceBundleMessageSource messageSource = \\n new ReloadableResourceBundleMessageSource();\\n messageSource.setBasename(\\"classpath:i18n/messages\\");\\n messageSource.setDefaultEncoding(\\"UTF-8\\");\\n messageSource.setCacheSeconds(3600); // 刷新缓存的周期(秒)\\n return messageSource;\\n }\\n}\\n
\\n@Component\\npublic class I18nUtil {\\n \\n @Autowired\\n private MessageSource messageSource;\\n \\n /**\\n * 获取国际化消息\\n *\\n * @param code 消息代码\\n * @return 本地化后的消息\\n */\\n public String getMessage(String code) {\\n return getMessage(code, null);\\n }\\n \\n /**\\n * 获取国际化消息\\n *\\n * @param code 消息代码\\n * @param args 消息参数\\n * @return 本地化后的消息\\n */\\n public String getMessage(String code, Object[] args) {\\n Locale locale = LocaleContextHolder.getLocale();\\n try {\\n return messageSource.getMessage(code, args, locale);\\n } catch (NoSuchMessageException e) {\\n return code;\\n }\\n }\\n}\\n
\\n# src/main/resources/i18n/messages_en.properties\\ngreeting=Hello, {0}!\\nlogin.success=Login successful\\nlogin.failure=Login failed: {0}\\nvalidation.username.notEmpty=Username cannot be empty\\nvalidation.password.weak=Password is too weak, must be at least 8 characters\\n\\n# src/main/resources/i18n/messages_zh_CN.properties\\ngreeting=你好,{0}!\\nlogin.success=登录成功\\nlogin.failure=登录失败:{0}\\nvalidation.username.notEmpty=用户名不能为空\\nvalidation.password.weak=密码强度不够,至少需要8个字符\\n
\\n@RestController\\n@RequestMapping(\\"/api/auth\\")\\npublic class AuthController {\\n \\n @Autowired\\n private AuthService authService;\\n \\n @Autowired\\n private I18nUtil i18nUtil;\\n \\n @PostMapping(\\"/login\\")\\n public ResponseEntity<?> login(@RequestBody LoginRequest request) {\\n try {\\n String token = authService.login(request.getUsername(), request.getPassword());\\n \\n Map<String, Object> response = new HashMap<>();\\n response.put(\\"token\\", token);\\n response.put(\\"message\\", i18nUtil.getMessage(\\"login.success\\"));\\n \\n return ResponseEntity.ok(response);\\n } catch (AuthenticationException e) {\\n Map<String, Object> response = new HashMap<>();\\n response.put(\\"error\\", i18nUtil.getMessage(\\"login.failure\\", new Object[]{e.getMessage()}));\\n \\n return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);\\n }\\n }\\n \\n @GetMapping(\\"/greeting\\")\\n public Map<String, String> greeting(@RequestParam String name) {\\n Map<String, String> response = new HashMap<>();\\n response.put(\\"message\\", i18nUtil.getMessage(\\"greeting\\", new Object[]{name}));\\n return response;\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/api/locale\\")\\npublic class LocaleController {\\n \\n @GetMapping(\\"/current\\")\\n public Map<String, Object> getCurrentLocale(HttpServletRequest request) {\\n Locale currentLocale = (Locale) request.getAttribute(\\"currentLocale\\");\\n \\n if (currentLocale == null) {\\n currentLocale = LocaleContextHolder.getLocale();\\n }\\n \\n Map<String, Object> response = new HashMap<>();\\n response.put(\\"locale\\", currentLocale.toString());\\n response.put(\\"language\\", currentLocale.getLanguage());\\n response.put(\\"country\\", currentLocale.getCountry());\\n \\n return response;\\n }\\n \\n @GetMapping(\\"/supported\\")\\n public List<Map<String, String>> getSupportedLocales() {\\n List<Map<String, String>> locales = new ArrayList<>();\\n \\n locales.add(createLocaleMap(Locale.ENGLISH, \\"English\\"));\\n locales.add(createLocaleMap(Locale.SIMPLIFIED_CHINESE, \\"简体中文\\"));\\n locales.add(createLocaleMap(Locale.TRADITIONAL_CHINESE, \\"繁體中文\\"));\\n locales.add(createLocaleMap(Locale.JAPANESE, \\"日本語\\"));\\n locales.add(createLocaleMap(Locale.KOREAN, \\"한국어\\"));\\n \\n return locales;\\n }\\n \\n private Map<String, String> createLocaleMap(Locale locale, String displayName) {\\n Map<String, String> map = new HashMap<>();\\n map.put(\\"code\\", locale.toString());\\n map.put(\\"language\\", locale.getLanguage());\\n map.put(\\"displayName\\", displayName);\\n return map;\\n }\\n}\\n
\\n拦截器的执行顺序非常重要,通常应该遵循:
\\n@Configuration\\npublic class WebMvcConfig implements WebMvcConfigurer {\\n \\n @Autowired\\n private AuthenticationInterceptor authInterceptor;\\n \\n @Autowired\\n private LoggingInterceptor loggingInterceptor;\\n \\n @Autowired\\n private PerformanceMonitorInterceptor performanceInterceptor;\\n \\n @Autowired\\n private RateLimitInterceptor rateLimitInterceptor;\\n \\n @Autowired\\n private RequestValidationInterceptor validationInterceptor;\\n \\n @Autowired\\n private LocaleChangeInterceptor localeInterceptor;\\n \\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n // 1. 国际化拦截器\\n registry.addInterceptor(localeInterceptor)\\n .addPathPatterns(\\"/**\\");\\n \\n // 2. 日志拦截器\\n registry.addInterceptor(loggingInterceptor)\\n .addPathPatterns(\\"/**\\");\\n \\n // 3. 性能监控拦截器\\n registry.addInterceptor(performanceInterceptor)\\n .addPathPatterns(\\"/api/**\\");\\n \\n // 4. 限流拦截器\\n registry.addInterceptor(rateLimitInterceptor)\\n .addPathPatterns(\\"/api/**\\");\\n \\n // 5. 参数验证拦截器\\n registry.addInterceptor(validationInterceptor)\\n .addPathPatterns(\\"/api/**\\");\\n \\n // 6. 认证拦截器\\n registry.addInterceptor(authInterceptor)\\n .addPathPatterns(\\"/api/**\\")\\n .excludePathPatterns(\\"/api/auth/login\\", \\"/api/auth/register\\");\\n }\\n}\\n
\\n通过合理使用这些拦截器,可以极大地提高代码复用性,减少重复代码,使应用架构更加清晰和模块化。
\\n在实际应用中,可以根据具体需求选择或组合使用这些拦截器,甚至扩展出更多类型的拦截器来满足特定业务场景。
","description":"在构建企业级Web应用时,我们经常需要在请求处理的不同阶段执行一些通用逻辑,如权限验证、日志记录、性能监控等。Spring MVC的拦截器(Interceptor)机制提供了一种优雅的方式来实现这些横切关注点,而不必在每个控制器中重复编写相同的代码。 本文将介绍SpringBoot中6种常见的拦截器使用场景及其实现方式。\\n\\n拦截器基础\\n什么是拦截器?\\n\\n拦截器是Spring MVC框架提供的一种机制,用于在控制器(Controller)处理请求前后执行特定的逻辑。\\n\\n拦截器与过滤器的区别\\n归属不同:过滤器(Filter)属于Servlet规范…","guid":"https://juejin.cn/post/7498555281430282303","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T23:18:03.616Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"Docker 万字教程:从入门到掌握","url":"https://juejin.cn/post/7498553859816816655","content":"\\n\\nDocker:Docker 是一个开源的应用容器引擎,让开发者可以将应用程序和所有依赖打包到一个轻量级、可移植的容器中,然后在任何支持 Docker 的环境中运行。
\\n
在传统的项目开发中,开发者经常遇到环境不一致的问题,比如代码在本地开发环境运行正常,但在测试或生产环境却出现各种错误,原因可能是操作系统版本、依赖库版本或配置差异。此外,传统部署方式需要手动安装和配置各种软件环境,过程繁琐且容易出错,不同服务器之间的环境也难以保持一致。
\\nDocker 的核心目标就是解决这些问题,通过容器化技术将应用及其运行环境打包在一起,确保应用在不同环境中表现一致。Docker 的出现极大简化了开发、测试和部署的流程,成为现代 DevOps 和云计算中的重要工具。Docker 有几个显著特点:
\\nDocker 采用了客户端-服务器架构。Docker 客户端负责发送命令,Docker 守护进程(服务端)负责执行这些命令。它们可以运行在同一台机器上,也可以分开运行。客户端和守护进程通过 UNIX 套接字或网络接口进行通信,使用 REST API 交换数据。这种架构让用户可以通过本地或远程客户端管理 Docker 服务。Docker 的架构图如下所示:
\\nDocker 的核心概念包括容器、镜像、Dockerfile 和镜像仓库,这些是理解 Docker 技术的基础。
\\n\\n\\n容器(Container):一种轻量级的虚拟化技术,它共享操作系统内核但保持独立的运行环境,这使得容器比传统虚拟机更快速且占用资源更少。
\\n
容器是 Docker 技术的核心运行单元,它是一种轻量级的虚拟化技术实现方式。与传统的虚拟机不同,容器不需要模拟完整的硬件环境,也不需要运行独立的操作系统内核。容器在运行时与其他容器和宿主机共享操作系统内核,这使得容器启动更快且占用资源更少。
\\n容器之间是相互独立的。每个容器都拥有自己的文件系统、网络和进程空间,确保应用之间不会互相干扰。
\\n\\n\\n镜像(Image):镜像是用于创建容器的模板,它包含了运行应用所需的代码、库和配置文件,用户可以从 Docker Hub 下载现成镜像或自己构建。
\\n
镜像是用来创建 Docker 容器的基础,镜像中包含了运行应用所需的代码、库、环境变量和配置文件,用户可以直接使用现成的镜像,比如从 Docker Hub 下载,也可以基于现有镜像定制自己的镜像。镜像采用分层存储结构,每一层代表一个修改步骤,这种设计使得镜像的构建和分发更高效。镜像中不包含任何的动态数据,其内容在构建之后不再变动。
\\n\\n\\nDockerfile:Dockerfile 是一个文本文件,里面写明了如何一步步构建镜像,通过执行 Dockerfile 中的指令,Docker 能自动生成镜像。
\\n
Dockerfile 是用于定义镜像构建过程的脚本文件,它由一系列指令组成,比如 FROM
指定基础镜像,RUN
执行命令,COPY
复制文件等,Docker 会根据 Dockerfile 的指令自动构建镜像,这使得镜像的创建过程可重复且透明。
\\n\\n镜像仓库(Image Repository):用于集中存储和分发镜像的地方。
\\n
最常用的公共仓库是 Docker Hub,它提供了大量官方和社区维护的镜像。此外,我们也可以搭建公司内部 / 个人使用的私有镜像仓库,用于存放自己的镜像。当需要在另一台主机上使用该镜像时,只需要从仓库上下载即可。
\\n容器、镜像和 Dockerfile、镜像仓库共同构成了 Docker 的工作流程:
\\n整个过程标准化且高效。理解这些核心概念是掌握 Docker 的关键,它们解决了传统开发部署中的环境不一致问题,使应用在任何地方都能以相同的方式运行。
\\nDocker 和虚拟机都是用来隔离应用运行环境的技术,但它们的工作原理和资源占用方式完全不同。Docker 容器不需要模拟整个操作系统,而是直接运行在宿主机的内核上,这使得容器启动更快、占用资源更少,同时仍能提供良好的隔离性。
\\nDocker 容器和虚拟机的具体区别如下:
\\n虚拟机的隔离性更强,因为每个虚拟机有完全独立的操作系统,适合运行需要强隔离的不同类型应用。Docker 的隔离性主要依靠 Linux 内核的命名空间和控制组功能,虽然隔离程度不如虚拟机,但对于大多数应用场景已经足够。Docker 和虚拟机对比如图所示:
\\n安装前需要清理系统中可能存在的旧版本 Docker 或其他冲突软件包。
\\n$ sudo dnf remove docker \\\\\\n docker-client \\\\\\n docker-client-latest \\\\\\n docker-common \\\\\\n docker-latest \\\\\\n docker-latest-logrotate \\\\\\n docker-logrotate \\\\\\n docker-engine\\n
\\n$ sudo yum install -y yum-utils\\n
\\n# yum 国内源\\n$ sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo\\n$ sudo sed -i \'s/download.docker.com/mirrors.aliyun.com\\\\/docker-ce/g\' /etc/yum.repos.d/docker-ce.repo\\n\\n# yum 官方源\\n# $ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo\\n
\\n安装 Docker 需要多个组件,包括 Docker 引擎(docker-ce)、命令行工具(docker-ce-cli)、容器运行时(containerd.io)、构建镜像的插件工具(docker-buildx-plugin)、多容器应用的编排管理工具(docker-compose-plugin)等。
\\n$ sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\\n
\\n安装完成后可以通过 docker --version
命令检查版本信息确认安装是否成功。
$ docker --version\\nDocker version 28.0.4, build b8034c0\\n
\\n安装成功后需要启动 Docker 服务,运行下面的命令可以启动 Docker 守护进程。
\\n$ sudo systemctl enable docker\\n$ sudo systemctl start docker\\n
\\n\\n\\n注意:如果安装过程中出现问题,可以尝试清理 yum 缓存,使用以下命令清除缓存后再重新安装。
\\n\\n$ yum clean packages\\n
在 macOS 系统上安装 Docker 需要下载并安装「Docker Desktop for Mac」 应用程序。Docker Desktop 是官方提供的图形化工具,它包含了 Docker 引擎、命令行工具和图形界面,让用户能够方便地在 macOS 上使用 Docker。
\\n目前 macOS 上安装 Docker Desktop 有两种方法:「使用 Homebrew 安装」和「手动下载安装」。
\\n第一种方法是使用 Homebrew 工具安装,打开终端并运行命令 brew install --cask docker
,这个命令会自动下载并安装最新版的 Docker Desktop。
$ brew install --cask docker\\n==> Downloading https://formulae.brew.sh/api/cask_tap_migrations.jws.json\\n==> Downloading https://formulae.brew.sh/api/formula_tap_migrations.jws.json\\n==> Downloading https://raw.githubusercontent.com/Homebrew/homebrew-cask/b946820\\n######################################################################### 100.0%\\n==> Downloading https://desktop.docker.com/mac/main/arm64/187762/Docker.dmg\\n######################################################################### 100.0%\\n==> Installing Cask docker\\n==> Moving App \'Docker.app\' to \'/Applications/Docker.app\'\\n==> Linking Binary \'docker-compose.zsh-completion\' to \'/opt/homebrew/share/zsh/s\\n==> Linking Binary \'docker-compose.fish-completion\' to \'/opt/homebrew/share/fish\\n==> Linking Binary \'compose-bridge\' to \'/usr/local/bin/compose-bridge\'\\n==> Linking Binary \'docker\' to \'/usr/local/bin/docker\'\\n==> Linking Binary \'docker-credential-desktop\' to \'/usr/local/bin/docker-credent\\n==> Linking Binary \'docker-credential-ecr-login\' to \'/usr/local/bin/docker-crede\\n==> Linking Binary \'docker-credential-osxkeychain\' to \'/usr/local/bin/docker-cre\\n==> Linking Binary \'kubectl\' to \'/usr/local/bin/kubectl.docker\'\\n==> Linking Binary \'docker-compose\' to \'/usr/local/cli-plugins/docker-compose\'\\nPassword: # 如果需要输入密码,则输入电脑密码\\n==> Linking Binary \'docker.bash-completion\' to \'/opt/homebrew/etc/bash_completio\\n==> Linking Binary \'docker.zsh-completion\' to \'/opt/homebrew/share/zsh/site-func\\n==> Linking Binary \'docker.fish-completion\' to \'/opt/homebrew/share/fish/vendor_\\n==> Linking Binary \'hub-tool\' to \'/usr/local/bin/hub-tool\'\\n==> Linking Binary \'docker-compose.bash-completion\' to \'/opt/homebrew/etc/bash_c\\n🍺 docker was successfully installed!\\n
\\n第二种方法是手动下载安装包。具体步骤如下:
\\ndocker --version
可以检查 Docker 是否安装成功,如果显示版本号说明安装正确。Docker Desktop 默认会在开机时自动启动,你也可以在设置中调整这个选项。安装完成后,你可以直接使用 docker 命令在终端中操作容器,或者通过 Docker Desktop 的图形界面管理容器和镜像。需要注意的是,由于 macOS 的特殊性,Docker 在文件共享和网络配置方面与 Linux 系统有些差异,具体使用时可能需要调整相关设置。
\\n在 Windows 系统上安装 Docker 需要下载并安装 Docker Desktop 应用程序。Docker Desktop 是官方提供的工具,它包含 Docker 引擎、命令行工具和图形界面,让用户能够在 Windows 上方便地使用 Docker。安装前需要确认系统版本,Windows 10 或 11 的 64 位专业版、企业版或教育版才能运行 Docker Desktop,家庭版需要通过额外步骤启用 Hyper-V 功能。具体安装步骤如下:
\\ndocker --version
可以检查安装是否成功,如果显示版本号说明安装正确。Docker Desktop 默认会随系统启动,可以在设置中修改这个选项。使用 Docker 时需要保持 Docker Desktop 在后台运行,通过命令行或图形界面都能管理容器和镜像。Windows 版 Docker 在文件共享方面需要注意,默认只有 C 盘用户目录下的文件可以直接挂载到容器中,其他盘符需要在 Docker 设置中手动添加共享路径。网络配置方面,Windows 版 Docker 使用 NAT 模式为容器提供网络连接,端口映射规则与 Linux 版本一致。如果遇到安装或运行问题,可以尝试重启 Docker 服务或电脑,检查防火墙设置是否阻止了 Docker 的网络连接。
\\nDocker 下载镜像默认从国外的官网下载,在国内需要通过代理访问 / 更换国内镜像源。下面介绍一下更换国内镜像源的方法。
\\n创建镜像源配置文件:
\\n$ sudo tee /etc/docker/daemon.json <<-\'EOF\'\\n{\\n \\"registry-mirrors\\": [\\n \\"https://docker.m.daocloud.io\\",\\n \\"https://dockerproxy.com\\",\\n \\"https://docker.mirrors.ustc.edu.cn\\",\\n \\"https://docker.nju.edu.cn\\"\\n ]\\n}\\nEOF\\n
\\n更改镜像源配置文件后,需要重新加载配置文件。
\\n$ sudo systemctl daemon-reload\\n
\\n然后重启 Docker 服务。
\\n$ sudo systemctl restart docker\\n
\\nDocker 服务管理是使用 Docker 的基础操作。下面介绍常用的 Docker 服务命令。
\\n$ systemctl start docker\\n
\\n$ systemctl stop docker\\n
\\n$ systemctl restart docker\\n
\\n$ systemctl enable docker\\n
\\n$ systemctl status docker\\n
\\n这些命令用于控制 Docker 服务的运行状态。启动服务后才能使用 Docker 的其他功能。停止服务会关闭所有正在运行的容器。重启命令通常用于应用配置变更后重新加载服务。设置开机启动可以确保系统重启后 Docker 自动运行。查看状态命令可以检查服务是否正常运行。
\\nDocker 镜像是创建容器的基础模板,理解镜像的使用方法是掌握 Docker 的关键。镜像包含了运行应用所需的代码、库、环境变量和配置文件,用户可以直接使用现成的镜像,也可以基于现有镜像定制自己的镜像。下面以 nginx 为例,详细介绍一下镜像的基本操作。
\\nDocker Hub 是 Docker 官方的公共镜像仓库,提供了大量官方和社区维护的镜像。使用 Docker Hub 需要注册账号。在命令行中可以通过 docker login
命令登录 Docker Hub 账号。
$ docker login\\nLogin with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.\\nUsername: your_username\\nPassword: \\nLogin Succeeded\\n
\\n登录后可以拉取和推送镜像。
\\n获取镜像最常用的方法是从 Docker Hub 或其他镜像仓库下载。Docker Hub 是 Docker 官方的公共镜像仓库,提供了大量官方和社区维护的镜像。从 Docker 镜像仓库获取镜像的命令是 docker pull
,对应命令格式为:
$ docker pull [OPTIONS] NAME[:TAG|@DIGEST]\\n
\\nOPTIONS(可选):选项参数。
\\n--all-tags, -a
:下载指定镜像的所有标签。--disable-content-trust
:跳过镜像签名验证。NAME:镜像名称,通常包含注册表地址(比如 docker.io/library/ubuntu
),不带注册表地址则默认从 Docker Hub 进行拉取。
TAG(可选):镜像标签,默认为 latest
。
DIGEST(可选):镜像的 SHA256 摘要。
\\n下载最新的 nginx 镜像可以运行 docker pull nginx
命令,Docker 会从配置的镜像源查找并下载该镜像:
$ docker pull nginx\\nUsing default tag: latest\\nlatest: Pulling from library/nginx\\n16c9c4a8e9ee: Pull complete\\nde29066b274e: Pull complete\\n2cf157fc31fe: Pull complete\\n450968563955: Pull complete\\n9b14c47aa231: Pull complete\\nfd8a9ced9846: Pull complete\\nc96c7b918bd5: Pull complete\\nDigest: sha256:5ed8fcc66f4ed123c1b2560ed708dc148755b6e4cbd8b943fab094f2c6bfa91e\\nStatus: Downloaded newer image for nginx:latest\\ndocker.io/library/nginx:latest\\n\\nWhat\'s next:\\n View a summary of image vulnerabilities and recommendations → docker scout quickview nginx\\n
\\n如果想下载特定版本的镜像,可以在镜像名后加上标签,比如下载 1.23 版本的 nginx。
\\n$ docker pull nginx:1.23\\n
\\n镜像标签是用来标识和管理镜像版本的重要工具。每个镜像可以有多个标签,通常用于区分不同版本或环境。使用 docker tag
命令可以为镜像创建新标签。
以 nginx 镜像为例,默认情况下当我们拉取nginx镜像时,Docker 会自动使用 latest 标签,这表示最新稳定版。在实际开发中,我们需要更精确的控制镜像版本。例如使用下面的命令会将本地的 nginx 镜像标记为 my-nginx:v1
,这样就能在项目中明确使用特定版本的镜像。
$ docker tag nginx:latest my-nginx:v1\\n
\\n要将本地镜像推送到 Docker Hub,需要使用登录的 Docker Hub 用户给镜像打标签。
\\n$ docker tag my-image your_username/my-image:1.0\\n
\\n然后使用 docker push
命令推送到 Docker Hub。
$ docker push your_username/my-image:1.0\\n
\\n下载完成后,对应镜像会存储在本地。通过 docker images
命令可以查看本地已有的镜像列表,这个命令会显示镜像的名称、标签、镜像 ID、创建时间和大小等信息。
$ docker images\\nREPOSITORY TAG IMAGE ID CREATED SIZE\\nnginx latest 1f94323bafb2 6 days ago 198MB\\n
\\n如果想要查看某个镜像(比如 nginx)的详细信息,包括创建时间、环境变量、工作目录、暴露的端口等配置信息,可以通过 docker inspect
命令查看,这些信息对于了解镜像的运行方式和配置非常重要。
$ docker inspect nginx\\n
\\n如果想要查看镜像的构建历史,则可以通过 docker history
相关命令查看,这个命令会显示镜像每一层的创建命令和大小,帮助理解镜像是如何一步步构建出来的。
$ docker history nginx \\n
\\n对于正在运行的容器,可以使用 docker logs
相关命令查看容器的日志输出,这个命令对于排查容器运行问题很有帮助。
$ docker logs nginx\\n
\\n镜像的查找可以通过 Docker Hub 网站或命令行完成,使用 docker search
命令能在终端直接搜索公共镜像。比如使用下面命令可以列出所有 MySQL 相关镜像,结果会显示镜像名称、描述、星标数和是否官方认证等信息。官方镜像由 Docker 官方团队维护,通常更可靠安全,建议优先选择带有 \\"OFFICIAL\\" 标记的镜像。
$ docker search mysql\\nNAME DESCRIPTION STARS OFFICIAL\\nmysql MySQL is a widely used, open-source relation… 15752 [OK]\\nbitnami/mysql Bitnami container image for MySQL 133\\ncircleci/mysql MySQL is a widely used, open-source relation… 31\\nbitnamicharts/mysql Bitnami Helm chart for MySQL 0\\ncimg/mysql 3\\nubuntu/mysql MySQL open source fast, stable, multi-thread… 67\\n……\\n
\\n在开发过程中,经常需要将 Docker 镜像从一个环境迁移到另一个环境。比如开发完成后,需要将测试通过的镜像部署到生产服务器,但生产服务器可能无法直接访问互联网下载镜像。这时可以使用镜像导出和导入功能。导出镜像使用 docker save
命令,这个命令会把镜像及其所有层打包成一个 tar 文件。
$ docker save -o nginx.tar nginx:latest\\n
\\n以上命令执行之后,会讲最新的 nginx 镜像保存到当前目录下的 nginx.tar 文件中。这个文件可以复制到其他服务器上。
\\n导出镜像可以使用 docker load
命令。在其他服务器上,通过 docker load -i nginx.tar
命令可以将 nginx.tar
所对应的 nginx 镜像导入到服务器的 Docker 中。
$ docker load -i nginx.tar\\nc9b18059ed42: Loading layer [==================================================>] 100.2MB/100.2MB\\ncbd8457b9f28: Loading layer [==================================================>] 101.6MB/101.6MB\\nc648e944b17e: Loading layer [==================================================>] 3.584kB/3.584kB\\n966bd022be40: Loading layer [==================================================>] 4.608kB/4.608kB\\nb422fd70039f: Loading layer [==================================================>] 2.56kB/2.56kB\\n486cd1e5e3be: Loading layer [==================================================>] 5.12kB/5.12kB\\ncbaa47f9fe15: Loading layer [==================================================>] 7.168kB/7.168kB\\nLoaded image: nginx:latest\\n
\\n导入后使用 docker images
命令可以看到 nginx 镜像已经存在。
当需要删除不再使用的 Docker 镜像时,可以使用 docker rmi
命令。这个命令需要指定镜像 ID 或镜像名称。比如要删除 nginx,可以使用 docker rmi nginx
命令。
$ docker rmi nginx\\nUntagged: nginx:latest\\nDeleted: sha256:1f94323bafb2ac98d25b664b8c48b884a8db9db3d9c98921b3b8ade588b2e676\\nDeleted: sha256:ca37bdd8ff5f2cbccaea502aa62460940bd5a2500a9fce4e931964e05a5f2ece\\nDeleted: sha256:2775bcda3310ca958798e032824be2d7a383c61cc4415c9ffd906be40eeab511\\nDeleted: sha256:c52a77a0a626e1e81d52b4ee7479be288a7b5430103e8caf99ea71c690084a41\\nDeleted: sha256:8f2e09717443cb341c6811b420d0d5129c92a1f2ec3af4795c0cdaf9d8f6ccdc\\nDeleted: sha256:58969d76cbbc7255c4f86d6c39a861f2e56e58c9f316133f988b821a9205bf32\\nDeleted: sha256:b4e77298dcd6ddc409f7e8d0ae3ccd9fe141f8844fd466ecf44dc927d9030ae6\\nDeleted: sha256:c9b18059ed426422229b2c624582e54e7e32862378c9556b90a99c116ae10a04\\n
\\n当镜像有多个标签时,删除一个标签只会移除该标签引用,不会真正删除镜像数据,需要使用 docker rmi
加上镜像 ID 才能彻底删除镜像文件。镜像 ID 可以通过 docker images
命令进行查看。
$ docker rmi 1f94323bafb2\\n
\\nDocker 使用过程中会积累大量未使用的镜像缓存,占用磁盘空间。通过 docker image prune
命令可以清理这些无用镜像。
不加参数时,这个命令只删除悬空镜像(没有标签且不被任何镜像引用的中间层)。加上 -a
参数会删除所有未被容器或镜像引用的镜像,包括构建缓存和旧版本镜像。
$ docker image prune\\nWARNING! This will remove all dangling images.\\nAre you sure you want to continue? [y/N] y\\nTotal reclaimed space: 0B\\n
\\n在执行清理前应该先用 docker images
查看镜像列表,确认哪些镜像可以删除。删除操作不可逆,重要的镜像需要提前备份。
清理完成后可以用 docker system df
查看磁盘使用情况,确认空间已经释放。定期清理镜像缓存能有效节省存储空间,保持 Docker 环境整洁高效。
$ docker system df\\nTYPE TOTAL ACTIVE SIZE RECLAIMABLE\\nImages 1 0 197.7MB 197.7MB (100%)\\nContainers 0 0 0B 0B\\nLocal Volumes 2 0 41.46MB 41.46MB (100%)\\nBuild Cache 46 0 300.5MB 300.5MB\\n
\\n在获取镜像并下载完成之后,可以通过 docker run
命令运行容器,这个命令的基本格式为:
$ docker run [OPTIONS] IMAGE [COMMAND] [ARG...]\\n
\\n常用参数说明:
\\n-d
:以后台模式运行容器。-p
:端口映射,格式为 主机端口号:容器端口号
,例如 80:80
。--name
:指定容器名称。-it
:保持交互式终端连接。-v
:挂载主机目录到容器,格式为 -v 主机目录:容器目录
。--rm
:容器停止时自动删除容器。--env
或 -e
:设置环境变量,格式为 --env 变量名=变量值
。--network
:指定容器的网络模式。--restart
:容器的重启策略。-u
:指定用户运行,格式为 -u 用户名
。以 nginx
为例,如果我们想要启动一个 nginx
容器,可以通过以下命令进行启动。
$ docker run -d -p 80:80 --name nginx-container nginx\\n
\\n通过以上命令,Docker 会进行以下操作:
\\nnginx
镜像构建容器。80
端口映射到主机的 80
端口上。nginx-container
。在容器启动后,通过主机的 IP 地址就能看到 nginx 的默认欢迎页面了。
\\n如果容器因为某种原因停止运行,可以使用 docker start 容器ID
命令重新启动它。
\\n\\n注意:如果每次运行容器使用
\\ndocker run
命令,则每次都都会创建一个新的容器实例,即使使用相同的镜像和参数也会生成不同的容器。
要终止正在运行的 Docker 容器,可以使用 docker stop
命令。这个命令会向容器发送 SIGTERM 信号,让容器有机会执行清理工作并正常关闭。如果容器在指定时间内没有停止,Docker 会强制发送 SIGKILL 信号终止容器。
$ docker stop nginx-container\\nnginx-container\\n
\\n在 Docker 使用过程中,经常需要查看容器的运行状态和信息。Docker 提供了多个命令来查看容器。
\\n使用 docker ps
命令可以查看当前正在运行的容器。这个命令会显示容器的 ID、名称、使用的镜像、创建时间、状态和端口映射等信息。
$ docker ps\\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\\na1b2c3d4e5f6 nginx \\"/docker-entrypoint.…\\" 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp nginx-container\\n
\\ndocker ps -a
命令可以查看所有容器,包括已经停止的容器。这个命令的输出格式与 docker ps
相同,但会显示更多容器。
$ docker ps -a\\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\\na1b2c3d4e5f6 nginx \\"/docker-entrypoint.…\\" 5 minutes ago Up 5 minutes 0.0.0.0:80->80/tcp nginx-container\\nb2c3d4e5f6a1 redis:alpine \\"docker-entrypoint.s…\\" 2 days ago Exited (0) 2 days ago redis-test\\n
\\ndocker inspect
命令可以查看容器的详细信息。这个命令会返回 JSON 格式的数据,包含容器的配置、网络设置、挂载卷等完整信息。
$ docker inspect nginx-container\\n
\\ndocker logs
命令可以查看容器的日志输出。这个命令对于排查容器运行问题很有帮助。加上 -f
参数可以实时查看日志输出。
$ docker logs nginx-container\\n$ docker logs -f nginx-container\\n
\\ndocker stats
命令可以实时查看容器的资源使用情况,包括 CPU、内存、网络和磁盘 I/O。
$ docker stats\\nCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS\\na1b2c3d4e5f6 nginx-container 0.00% 2.5MiB / 1.952GiB 0.13% 1.45kB / 648B 0B / 0B 2\\n
\\n当容器在后台运行时,我们可以通过 docker exec
命令来进入运行中的容器内部执行命令或查看状态。
$ docker exec -it nginx-container /bin/bash\\n
\\n以上命令会启动一个bash shell 让我们能与 nginx-container
容器进行交互,-it
参数会保持终端交互连接。
通过这种方式可以像操作普通 Linux 系统一样在容器内执行命令、查看文件或修改配置。容器启动后,Docker 会为它分配唯一的 ID 和名称,自动设置网络和存储,并根据镜像定义启动指定的进程。
\\n容器的导出和导入功能用于将容器及其当前状态保存为文件,便于迁移或备份。
\\n导出容器可以使用 docker export
命令,这个命令会将容器的文件系统打包成 tar 归档文件,但不包含容器的元数据、网络配置或卷信息。
$ docker export -o nginx-container.tar nginx-container\\n
\\n使用上面的容器导出命令会将名为 nginx-container 的容器导出到当前目录下的 nginx-container.tar 文件中。
\\n容器导出后的文件可以通过 docker import
命令重新导入为镜像,导入时需要指定镜像名称和标签。
$ docker import nginx-container.tar my-nginx:v1\\n
\\n使用上面的导入命令会创建一个名为 my-nginx、标签为 v1 的新镜像。
\\n当容器不再需要时,可以使用 docker rm
命令将其删除,这个命令需要指定容器 ID 或容器名称。
docker rm nginx-container\\n
\\n使用上面的删除容器命令,会删除名为 nginx-container 的容器。
\\n\\n\\n注意:如果容器正在运行,直接删除会失败,需要先使用
\\ndocker stop
停止容器,或者添加-f
参数强制删除运行中的容器。
除了使用公共仓库,可以搭建私有镜像仓库来存储内部镜像。Docker 官方提供了 Registry 镜像用于搭建私有仓库。
\\n运行命令拉取最新版 Registry 镜像。
\\n$ docker pull registry:2\\n
\\n使用以下命令启动 Registry 容器。
\\n$ docker run -d \\\\\\n -p 5000:5000 \\\\\\n --name my-registry \\\\\\n -v /data/registry:/var/lib/registry \\\\\\n registry:2\\n
\\n参数说明:
\\n-p 5000:5000
:将容器 5000 端口映射到主机 5000 端口。-v /data/registry:/var/lib/registry
:挂载数据目录持久化存储镜像。registry:2
:指定使用的镜像版本。这会在本地 5000 端口启动一个私有仓库。
\\n检查容器是否正常运行:
\\n$ docker ps\\n
\\n访问 http://localhost:5000/v2/_catalog
查看仓库内容,正常应返回空列表:
{\\"repositories\\":[]}\\n
\\n首先给本地镜像打标签,包含私有仓库地址。
\\n$ docker tag my-image localhost:5000/my-image\\n
\\n然后推送镜像到仓库。
\\n$ docker push localhost:5000/my-image\\n
\\n$ docker pull localhost:5000/my-image\\n
\\n通过 API 查看仓库中的镜像列表:
\\n$ curl http://localhost:5000/v2/_catalog\\n{\\"repositories\\":[\\"my-image\\"]}\\n
\\n查看特定镜像的标签:
\\ncurl http://localhost:5000/v2/my-image/tags/list\\n
\\n默认仓库只能在本地访问。要允许远程访问需修改配置。
\\n按照 3.5.1 的方式,在想要进行远程访问的服务器上搭建私有镜像仓库。
\\n编辑本地 /etc/docker/daemon.json
文件,添加仓库地址到 insecure-registries
中。
{\\n \\"insecure-registries\\": [\\"your-server-ip:5000\\"]\\n}\\n
\\nsystemctl restart docker\\n
\\n$ docker tag my-image your-server-ip:5000/my-image:v1\\n$ docker push your-server-ip:5000/my-image:v1\\n
\\n安装 htpasswd 工具:
\\n$ yum install httpd-tools # CentOS\\n$ apt-get install apache2-utils # Ubuntu\\n
\\n创建认证文件:
\\n$ mkdir auth\\n$ htpasswd -Bbn testuser testpassword > auth/htpasswd\\n
\\n使用下面命令生成自签名证书
\\n$ mkdir certs\\n$ openssl req -newkey rsa:4096 -nodes -sha256 \\\\\\n -keyout certs/domain.key -x509 -days 365 \\\\\\n -out certs/domain.crt\\n
\\ndocker run -d \\\\\\n -p 443:443 \\\\\\n --name my-registry \\\\\\n -v /data/registry:/var/lib/registry \\\\\\n -v $(pwd)/certs:/certs \\\\\\n -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \\\\\\n -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \\\\\\n -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \\\\\\n registry:2\\n
\\n示例 Nginx 配置:
\\nserver {\\n listen 443 ssl;\\n server_name registry.example.com;\\n\\n ssl_certificate /path/to/cert.pem;\\n ssl_certificate_key /path/to/key.pem;\\n\\n location / {\\n proxy_pass http://registry:5000;\\n proxy_set_header Host $http_host;\\n proxy_set_header X-Real-IP $remote_addr;\\n }\\n}\\n
\\n创建 docker-compose.yml 文件,示例 docker-compose.yml 配置:
\\nversion: \'3\'\\n\\nservices:\\n registry:\\n image: registry:2\\n ports:\\n - \\"5000:5000\\"\\n environment:\\n REGISTRY_AUTH: htpasswd\\n REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd\\n REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm\\n volumes:\\n - ./auth:/auth\\n - ./data:/var/lib/registry\\n
\\n生产环境建议使用 Nginx 作为 Registry 的反向代理,提供 TLS 加密和认证。
\\n示例 Nginx 配置:
\\nserver {\\n listen 443 ssl;\\n server_name registry.example.com;\\n\\n ssl_certificate /path/to/cert.pem;\\n ssl_certificate_key /path/to/key.pem;\\n\\n location / {\\n proxy_pass http://registry:5000;\\n proxy_set_header Host $http_host;\\n proxy_set_header X-Real-IP $remote_addr;\\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\\n proxy_set_header X-Forwarded-Proto $scheme;\\n }\\n}\\n
\\n启动服务:
\\n$ docker-compose up -d\\n
\\n\\n\\nDockerfile:Dockerfile 是用来构建 Docker 镜像的文本文件,它包含了一系列的指令和配置,用于告诉 Docker 如何构建一个镜像。
\\n
使用 Dockerfile 可以自动构建镜像,这样在不同的电脑上都能得到一样的镜像。Dockerfile 里可以写很多不同的指令,比如指定用什么基础镜像、往镜像里放什么文件、安装什么软件、设置什么环境变量、打开什么端口、运行什么命令等。每一条指令都会在镜像里新建一层,所有层叠在一起就是最终的镜像。这种分层的方式让镜像构建更快,也更容易分享和重复使用。
\\n在实际开发中,虽然官方镜像提供了基础运行环境,但项目通常需要特定的配置、依赖、文件、程序代码,这时我们就需要用 Dockerfile 来定制自己的镜像。
\\n以 Web 开发为例,我们可以在官方 nginx 镜像基础上,添加自定义的网页文件、修改配置文件、安装必要的工具等。这样就能保证开发、测试和线上环境完全一致,不会因为环境不同出问题。
\\n具体操作如下:
\\nmyweb
。myweb
下创建一个文本文件,命名为 Dockerfile
,并在文件中添加如下内容。FROM nginx:latest\\nRUN echo \'<h1>My Custom Nginx Page</h1>\' > /usr/share/nginx/html/index.html\\nEXPOSE 80\\n
\\n这个 Dockerfile 做了三件事:第一行 FROM nginx:latest
指定使用最新版 nginx 作为基础镜像;第二行 RUN
命令将网页内容放到容器的 nginx 默认的网页目录中;第三行 EXPOSE 80
声明容器会使用 80
端口。
myweb
目录下运行命令 docker build -t my-web .
进行自定义 nginx 镜像构建。这样就完成了自定义 nginx 镜像的构建。其中 -t my-web
是给新镜像命名为 my-web,最后的点表示使用当前目录的 Dockerfile。
构建完成后可以用 docker images
查看新构建的镜像。
$ docker images\\n
\\ndocker run -d -p 80:80 my-web
启动容器。这时访问主机的 80 端口就能看到自定义网页了。
\\n如果需要更复杂的定制,可以在 Dockerfile 中添加更多指令,Dockerfile 常用指令如下表所示。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n命令 | 描述 |
---|---|
FROM | 指定基础镜像,用于后续指令构建。 |
LABEL | 添加镜像的元数据,使用键值对的形式。 |
RUN | 创建镜像时,在镜像中要执行的命令。 |
CMD | 指定容器启动时默认要执行的命令(可以被覆盖)。如果执行 docker run 后面跟启动命令会被覆盖掉。 |
ENTRYPOINT | 设置容器创建时的主要命令(不可被覆盖)。 |
SHELL | 覆盖Docker中默认的shell,用于RUN、CMD和ENTRYPOINT指令。 |
EXPOSE | 声明容器运行时监听的特定网络端口。 |
ENV | 设置环境变量 |
COPY | 将文件或目录复制到镜像中。 |
ADD | 将文件、目录或远程 URL 复制到镜像中。 |
WORKDIR | 指定工作目录 |
VOLUME | 为容器创建挂载点或声明卷。 |
USER | 切换执行后续命令的用户和用户组,但这个用户必需首先已使用 RUN 的命令进行创建好了。 |
ARG | 定义在构建过程中传递给构建器的变量,可使用 \\"docker build\\" 命令设置。 |
ONBUILD | 当该镜像被用作另一个构建过程的基础时,添加触发器。 |
STOPSIGNAL | 设置发送给容器以退出的系统调用信号。 |
HEALTHCHECK | 定义周期性检查容器健康状态的命令。 |
\\n\\nFROM 指令:FROM 指令用于指定基础镜像,必须是 Dockerfile 的第一个指令。基础镜像可以是官方镜像如
\\nubuntu:20.04
,也可以是用户自定义的镜像。如果不指定标签,默认使用latest
标签。例如FROM nginx
表示基于最新版 nginx 镜像构建。每个 Dockerfile 可以有多个 FROM 指令用于构建多阶段镜像,但最终只会保留最后一个 FROM 生成的镜像层。基础镜像的选择直接影响最终镜像的大小和安全性,通常推荐使用官方维护的最小化镜像如alpine
版本。最后的AS name
可以选择为新生成阶段指定名称。
FROM <image>[:<tag>] [AS <name>]\\n
\\n<image>
:必需,指定基础镜像名称。:<tag>
:可选,指定镜像版本标签。AS <name>
:可选,为构建阶段命名。FROM nginx:1.23\\nFROM python:3.9-alpine\\nFROM ubuntu:20.04 AS builder\\n
\\n\\n\\nLABEL 指令:用于添加元数据到镜像,采用键值对格式。
\\n
LABEL <key>=<value>\\n
\\nLABEL version=\\"1.0.1\\"\\nLABEL maintainer=\\"admin@example.com\\"\\n
\\n\\n\\nRUN 指令:在镜像构建过程中执行命令,每条 RUN 指令都会创建一个新的镜像层。RUN 有两种格式:Shell 格式(
\\nRUN <command>
)和 Exec 格式(RUN [\\"executable\\", \\"param1\\", \\"param2\\"]
)。Shell 格式默认使用/bin/sh -c
执行,Exec 格式直接调用可执行文件。为了减少镜像层数,建议将多个命令合并到单个 RUN 指令中,用&&
连接命令,用\\\\
换行。例如安装软件包时应该先更新包列表再安装,最后清理缓存。
# Shell 格式\\nRUN <command>\\n\\n# Exec 格式\\nRUN [\\"executable\\", \\"param1\\", \\"param2\\"]\\n
\\nRUN yum -y install wget\\nRUN apt-get update && apt-get install -y \\\\\\n git \\\\\\n curl \\\\\\n && rm -rf /var/lib/apt/lists/*\\nRUN [\\"/bin/bash\\", \\"-c\\", \\"echo Hello, Docker!\\"]\\nRUN [\\"yum\\", \\"-y\\", \\"install\\", \\"wget\\"]\\n
\\n\\n\\nCMD 指令:指定容器启动时的默认命令,一个 Dockerfile 只能有一个有效的 CMD 指令。如果
\\ndocker run
指定了命令,CMD 会被覆盖。CMD 有三种格式:Exec 格式(推荐)、Shell 格式和作为 ENTRYPOINT 的参数。Exec 格式直接调用可执行文件,不经过 shell 处理环境变量;Shell 格式会通过/bin/sh -c
执行。例如运行 Python 应用时可以使用CMD [\\"python\\", \\"app.py\\"]
。
# Exec 格式(推荐)\\nCMD [\\"executable\\",\\"param1\\",\\"param2\\"]\\n\\n# Shell 格式\\nCMD command param1 param2\\n\\n# 作为 ENTRYPOINT 的参数\\nCMD [\\"param1\\",\\"param2\\"]\\n
\\n# Shell 格式示例: 运行 Python 脚本\\nCMD python app.py\\n\\n# Exec 格式示例: 运行 Nginx\\nCMD [\\"nginx\\", \\"-g\\", \\"daemon off;\\"]\\n\\n# 作为 ENTRYPOINT 参数\\nCMD [\\"--port=8080\\"]\\n
\\n\\n\\nENTRYPOINT 指令:用于设置容器启动时的主要命令,与 CMD 不同,它不会被 docker run 后面的命令覆盖。
\\n
# Shell 格式\\nENTRYPOINT command param1 param2\\n\\n# Exec 格式\\nENTRYPOINT [\\"executable\\", \\"param1\\", \\"param2\\"]\\n
\\n# Shell 格式示例: 运行 Python 脚本\\nENTRYPOINT python app.py\\n\\n# Exec 格式示例: 运行 Nginx\\nENTRYPOINT [\\"nginx\\", \\"-g\\", \\"daemon off;\\"]\\n
\\n\\n\\nSHELL 指令:用于覆盖 Docker 默认的 shell 程序。默认情况下 Linux 使用
\\n[\\"/bin/sh\\", \\"-c\\"]
,Windows 使用[\\"cmd\\", \\"/S\\", \\"/C\\"]
。这个指令会影响 RUN、CMD 和 ENTRYPOINT 的 shell 格式执行方式。
SHELL [\\"executable\\", \\"parameters\\"]\\n
\\n# 切换为 PowerShell\\nSHELL [\\"powershell\\", \\"-command\\"]\\n# 切换回默认 shell\\nSHELL [\\"/bin/sh\\", \\"-c\\"]\\n
\\n\\n\\nEXPOSE 指令:声明容器运行时监听的网络端口,只是文档性质的说明,不会实际发布端口。实际端口映射需要在运行容器时通过
\\n-p
参数设置。EXPOSE 可以指定 TCP 或 UDP 协议,默认是 TCP。例如EXPOSE 80/tcp
表示容器会监听 80 端口。这个指令主要作用是帮助使用者理解容器提供的服务端口,同时被一些编排工具如 Docker Compose 使用。
EXPOSE <port> [<port>/<protocol>...]\\n
\\n# 声明单个端口\\nEXPOSE 80\\n# 声明多个端口\\nEXPOSE 80 443\\n# 指定协议\\nEXPOSE 53/udp\\n
\\n\\n\\nENV 指令:设置环境变量,这些变量会在构建阶段和容器运行时生效。变量可以被后续的 RUN、CMD 等指令使用,也会持久化到容器中。定义的环境变量会出现在
\\ndocker inspect
的输出中,也可以在容器运行时通过docker run --env
参数覆盖。
# 定义单个变量\\nENV <key> <value>\\n# 一次性定义多个变量\\nENV <key1>=<value1> <key2>=<value2>...\\n
\\n# 设置单个变量\\nENV NODE_VERSION=18.15.0\\n# 设置多个变量\\nENV NODE_VERSION=18.15.0 NODE_ENV=production\\n
\\n\\n\\nCOPY 指令:将文件或目录从构建上下文复制到镜像中,源路径必须是相对路径(相对于 Dockerfile 所在目录),不能使用绝对路径或
\\n../
父目录引用。目标路径可以是绝对路径或相对于 WORKDIR 的路径。COPY 会保留文件元数据(权限、时间戳),但不支持自动解压压缩包。与 ADD 指令相比,COPY 更推荐用于简单的文件复制操作,因为它的行为更可预测。例如复制项目代码到镜像的/app
目录。
COPY <src>... <dest>\\n
\\n# 复制单个文件\\nCOPY requirements.txt /app/\\n# 复制整个目录\\nCOPY src /app/src\\n# 使用通配符复制多个文件\\nCOPY *.sh /scripts/\\n
\\n\\n\\nADD 指令:ADD 指令功能类似 COPY,但增加了自动解压压缩包和处理远程 URL 的能力。当源路径是本地压缩文件(如 .tar、.gz)时,ADD 会自动解压到目标路径。源路径也可以是 URL,Docker 会下载文件到镜像中。例如
\\nADD https://example.com/file.tar.gz /tmp
会下载并解压文件。由于 ADD 行为较复杂,官方建议优先使用 COPY,除非明确需要解压或下载功能。
ADD <src>... <dest>\\n
\\n# 添加本地文件\\nADD app.jar /opt/app/\\n# 自动解压压缩包\\nADD project.tar.gz /app\\n# 从 URL 下载文件\\nADD https://example.com/data.json /data\\n
\\n\\n\\nWORKDIR 指令:设置工作目录,相当于
\\ncd
命令的效果,如果目录不存在会自动创建。后续的 RUN、CMD、COPY 等指令都会在此目录下执行。WORKDIR 可以多次使用,路径可以是相对路径(基于前一个 WORKDIR)。例如WORKDIR /app
后接WORKDIR src
最终路径是/app/src
。使用 WORKDIR 可以避免在 RUN 指令中频繁使用cd
命令,使 Dockerfile 更清晰。
WORKDIR <path>\\n
\\nWORKDIR /usr/src\\nWORKDIR app\\nRUN pwd # 输出 /usr/src/app\\n
\\n\\n\\nVOLUME 指令:创建挂载点或声明卷,会在容器运行时自动挂载匿名卷。主要用途是保留重要数据(如数据库文件)或共享目录。例如
\\nVOLUME /data
确保/data
目录的数据持久化。实际挂载的主机目录可以通过docker inspect
查看。VOLUME 指令不能在构建阶段向挂载点写入数据,因为这些数据在运行时会被覆盖。数据卷可以在容器间共享和重用,即使容器被删除,卷数据仍然存在。VOLUME 声明后,运行时可以通过 -v 参数覆盖,但无法在构建阶段向挂载点写入数据(会被运行时覆盖)。
VOLUME [\\"<path1>\\", \\"<path2>\\", ...]\\n
\\n# 声明单个卷\\nVOLUME /data\\n# 声明多个卷\\nVOLUME [\\"/data\\", \\"/config\\"]\\n
\\n\\n\\nUSER 指令:切换执行后续指令的用户身份,用户必须已通过 RUN 指令创建。例如
\\nUSER nobody
让后续命令以 nobody 身份运行,增强安全性。该用户需要有足够的权限访问所需文件。USER 会影响 RUN、CMD、ENTRYPOINT 的执行身份。在运行时可以通过docker run -u
覆盖此设置。典型的用法是在安装软件包后创建非 root 用户并切换,避免容器以 root 权限运行。
USER <user>[:<group>]\\n
\\nRUN groupadd -r app && useradd -r -g app appuser\\nUSER appuser\\nCMD [\\"python\\", \\"app.py\\"]\\n
\\n\\n\\nARG 指令:指令定义构建时的变量。这些变量只在构建阶段有效,不会保留到容器运行时。可以通过
\\ndocker build --build-arg <name>=<value>
覆盖默认值。例如ARG VERSION=latest
定义版本变量。ARG 变量可以用于控制构建流程,如选择不同的软件版本。常见的预定义变量包括 HTTP_PROXY 等代理设置。
ARG <name>[=<默认值>]\\n
\\nARG NODE_VERSION=14\\nFROM node:${NODE_VERSION}\\n
\\n\\n\\nONBUILD 指令:设置触发器。当当前镜像被用作其他镜像的基础时,这些指令会被触发执行。例如
\\nONBUILD COPY . /app
会在子镜像构建时自动复制文件。ONBUILD 常用于创建基础镜像模板,子镜像可以继承特定的构建步骤。通过docker inspect
可以查看镜像的 ONBUILD 触发器。
ONBUILD <其他指令>\\n
\\nONBUILD RUN npm install\\nONBUILD COPY . /app\\n
\\n\\n\\nSTOPSIGNAL 指令:设置容器停止时发送的系统信号。信号可以是数字(如 9)或信号名(如 SIGKILL)。默认的信号是 SIGTERM,允许容器优雅退出。如果需要强制终止,可以设置为 SIGKILL。例如
\\nSTOPSIGNAL SIGTERM
确保容器收到终止信号。这个设置会影响docker stop
命令的行为。
STOPSIGNAL <信号>\\n
\\nSTOPSIGNAL SIGQUIT\\n
\\n\\n\\nHEALTHCHECK 指令:定义容器健康检查,Docker 会定期执行检查命令判断容器是否健康。检查命令返回 0 表示健康,1 表示不健康。选项包括
\\n--interval
(检查间隔)、--timeout
(超时时间)、--start-period
(启动宽限期)和--retries
(重试次数)。例如HEALTHCHECK --interval=30s CMD curl -f http://localhost/
每30秒检查Web服务是否响应。健康状态可以通过docker ps
查看。
HEALTHCHECK [OPTIONS] CMD <command>\\n
\\nHEALTHCHECK --interval=5m --timeout=3s \\\\\\n CMD curl -f http://localhost/ || exit 1\\n
\\n假设有一个简单的 Python Flask 应用,需要将其打包成 Docker 镜像。以下是完整的 Dockerfile 示例:
\\n# 使用官方 Python 基础镜像\\nFROM python:3.9-slim\\n\\n# 设置工作目录\\nWORKDIR /app\\n\\n# 复制当前目录下的文件到工作目录\\nCOPY . .\\n\\n# 安装依赖\\nRUN pip install --no-cache-dir -r requirements.txt\\n\\n# 暴露端口\\nEXPOSE 5000\\n\\n# 定义环境变量\\nENV FLASK_APP=app.py\\nENV FLASK_ENV=production\\n\\n# 启动命令\\nCMD [\\"flask\\", \\"run\\", \\"--host=0.0.0.0\\"]\\n
\\n这个 Dockerfile 的执行步骤如下:
\\npython:3.9-slim
作为基础镜像,这是一个轻量级的 Python 环境。/app
。/app
目录。pip install
安装依赖包。构建镜像的命令:
\\ndocker build -t my-python-app .\\n
\\n运行容器的命令:
\\ndocker run -d -p 5000:5000 my-python-app\\n
\\n下面是一个 Node.js 应用的 Dockerfile 示例:
\\n# 使用官方 Node.js 基础镜像\\nFROM node:16-alpine\\n\\n# 设置工作目录\\nWORKDIR /usr/src/app\\n\\n# 复制 package.json 和 package-lock.json\\nCOPY package*.json ./\\n\\n# 安装依赖\\nRUN npm install\\n\\n# 复制所有源代码\\nCOPY . .\\n\\n# 构建应用\\nRUN npm run build\\n\\n# 暴露端口\\nEXPOSE 3000\\n\\n# 启动命令\\nCMD [\\"npm\\", \\"start\\"]\\n
\\n这个 Dockerfile 的执行步骤如下:
\\nnode:16-alpine
作为基础镜像,这是一个基于 Alpine Linux 的 Node.js 环境。/usr/src/app
。package.json
和 package-lock.json
文件,这样可以利用 Docker 的缓存层。npm install
安装依赖。构建镜像的命令:
\\ndocker build -t my-node-app .\\n
\\n运行容器的命令:
\\ndocker run -d -p 3000:3000 my-node-app\\n
\\n如果需要部署一个静态网站,可以使用 Nginx 作为 Web 服务器。以下是 Dockerfile 示例:
\\n# 使用官方 Nginx 基础镜像\\nFROM nginx:alpine\\n\\n# 删除默认的 Nginx 配置\\nRUN rm -rf /etc/nginx/conf.d/default.conf\\n\\n# 复制自定义配置\\nCOPY nginx.conf /etc/nginx/conf.d/\\n\\n# 复制静态网站文件\\nCOPY dist/ /usr/share/nginx/html/\\n\\n# 暴露端口\\nEXPOSE 80\\n\\n# 启动 Nginx\\nCMD [\\"nginx\\", \\"-g\\", \\"daemon off;\\"]\\n
\\n这个 Dockerfile 的执行步骤如下:
\\nnginx:alpine
作为基础镜像,这是一个轻量级的 Nginx 环境。需要准备一个 nginx.conf
配置文件,例如:
server {\\n listen 80;\\n server_name localhost;\\n\\n location / {\\n root /usr/share/nginx/html;\\n index index.html;\\n try_files $uri $uri/ /index.html;\\n }\\n}\\n
\\n构建镜像的命令:
\\ndocker build -t my-static-site .\\n
\\n运行容器的命令:
\\ndocker run -d -p 8080:80 my-static-site\\n
\\n\\n\\nDocker Compose:用于定义和管理多容器应用的工具。它通过一个 YAML 文件来配置所有服务配置,用一条命令就能启动整个应用。Docker Compose 解决了多个容器之间的依赖关系和启动顺序问题,主要用于开发环境、测试环境和 CI/CD 流程中。Docker Compose 文件包含了容器镜像、端口映射、数据卷、环境变量等配置,所有配置集中在一个文件中,便于版本控制和团队协作。
\\n
Docker Compose 可以通过多种方式安装。在 Linux 系统上,可以直接下载二进制文件进行安装。运行以下命令可以安装最新版本的 Docker Compose:
\\nsudo curl -L \\"https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\\" -o /usr/local/bin/docker-compose\\nsudo chmod +x /usr/local/bin/docker-compose\\n
\\n安装完成后,可以通过运行 docker-compose --version
来验证安装是否成功。对于 Windows 和 macOS 用户,Docker Desktop 已经包含了 Docker Compose,不需要单独安装。如果系统已经安装了 Docker Engine,通常也会包含 Docker Compose 插件,可以通过 docker compose version
命令检查。
Docker Compose 的核心是 docker-compose.yml
文件,它采用 YAML 格式定义服务、网络和卷。一个基本的 Compose 文件包含以下部分:
\'3.8\'
。YAML 文件使用缩进来表示层级关系,冒号表示键值对,连字符表示列表项。
\\n以下是一个典型的 docker-compose.yml
示例,定义了一个 WordPress 的应用配置。
version: \'3.8\'\\n\\nservices:\\n db:\\n image: mysql:5.7\\n volumes:\\n - db_data:/var/lib/mysql\\n environment:\\n MYSQL_ROOT_PASSWORD: password\\n MYSQL_DATABASE: wordpress\\n networks:\\n - backend\\n\\n wordpress:\\n image: wordpress:latest\\n ports:\\n - \\"80:80\\"\\n environment:\\n WORDPRESS_DB_HOST: db\\n WORDPRESS_DB_USER: root\\n WORDPRESS_DB_PASSWORD: password\\n depends_on:\\n - db\\n networks:\\n - frontend\\n - backend\\n\\nnetworks:\\n frontend:\\n backend:\\n\\nvolumes:\\n db_data:\\n\\n
\\n这个 WordPress 的应用配置示例中展示了:
\\n下面详细说明每个部分的作用和配置方法。
\\n\\n\\nversion:指定使用的 Compose 文件格式版本。不同版本支持的功能不同。目前常用的是 3.x 版本。版本号需要用引号包裹。
\\n
示例:
\\nversion: \'3.8\'\\n
\\n版本号影响可用功能。例如 3.8 版本支持更多配置选项。版本号必须与安装的 Docker Compose 版本兼容。如果不指定版本,默认使用最新支持版本。
\\n\\n\\nservices:定义需要运行的容器服务,是 Compose 文件的核心部分。每个服务对应一个容器。服务名称自定义,作为容器的标识。
\\n
基本服务配置示例:
\\nservices:\\n web:\\n image: nginx:latest\\n ports:\\n - \\"80:80\\"\\n
\\n常用服务配置项:
\\nimage:指定使用的镜像名称和标签。可以从 Docker Hub 获取或使用本地构建的镜像。
\\nbuild:如果使用本地 Dockerfile 构建镜像,需要指定构建路径。
\\nbuild: ./dir\\n
\\nports:设置端口映射,格式为 \\"主机端口:容器端口\\"
。
ports:\\n - \\"8080:80\\"\\n
\\nvolumes:配置数据卷挂载,支持主机路径和命名卷。
\\nvolumes:\\n - /host/path:/container/path\\n - named_volume:/container/path\\n
\\nenvironment:设置环境变量,可以用列表或键值对格式。
\\nenvironment:\\n - VAR1=value1\\n - VAR2=value2\\n
\\ndepends_on:定义服务启动顺序,确保依赖服务先启动。
\\ndepends_on:\\n - db\\n
\\nrestart:设置容器重启策略,比如 always
表示总是自动重启。
restart: always\\n
\\ncommand:覆盖容器启动命令。
\\ncommand: [\\"python\\", \\"app.py\\"]\\n
\\n完整服务示例:
\\nservices:\\n web:\\n build: .\\n ports:\\n - \\"5000:5000\\"\\n volumes:\\n - .:/code\\n environment:\\n FLASK_ENV: development\\n depends_on:\\n - redis\\n redis:\\n image: redis:alpine\\n volumes:\\n - redis_data:/data\\n
\\n\\n\\nnetworks:定义自定义网络。容器通过网络名称互相通信。默认会创建 bridge 网络。
\\n
示例:
\\nnetworks:\\n frontend:\\n driver: bridge\\n backend:\\n driver: bridge\\n
\\n服务中使用网络:
\\nservices:\\n web:\\n networks:\\n - frontend\\n db:\\n networks:\\n - backend\\n
\\n自定义网络提供更好的隔离性。不同网络的容器默认不能互相访问。
\\n\\n\\nvolumes:声明数据卷。数据卷用于持久化存储和容器间共享数据。
\\n
示例:
\\nvolumes:\\n db_data:\\n driver: local\\n app_data:\\n driver: local\\n
\\n服务中使用卷:
\\nservices:\\n db:\\n volumes:\\n - db_data:/var/lib/mysql\\n
\\n卷数据独立于容器生命周期。删除容器不会删除卷数据。
\\nDocker Compose 提供了一系列命令来管理多容器应用:
\\ndocker-compose up -d
会在后台启动所有服务,-d
表示 detached 模式。docker-compose down
停止并移除所有容器、网络和卷。docker-compose ps
显示各容器的运行状态。docker-compose logs
输出容器日志,加 -f
可以跟踪实时日志。docker-compose build
会重新构建镜像。docker-compose restart
,重启所有服务或指定服务。。docker-compose start wordpress
。这些命令大大简化了多容器应用的管理工作。通过组合使用这些命令,可以轻松控制整个应用的运行状态。
\\n$ systemctl start docker\\n
\\n$ systemctl stop docker\\n
\\n$ systemctl restart docker\\n
\\n$ systemctl enable docker\\n
\\n$ systemctl status docker\\n
\\n$ docker images\\n
\\n$ docker pull NAME[:TAG]\\n
\\n$ docker search NAME\\n
\\n# docker build -t 镜像名:版本号 .\\n$ docker build -t my_image:1.0 .\\n
\\n# docker rmi 镜像名:版本号\\n$ docker rmi mysql:5.7\\n
\\n# docker load -i 指定要导入的镜像压缩包文件名\\n$ docker load -i image.tar\\n
\\n# docker save -o 导出的镜像压缩包的文件名 要导出的镜像名:版本号\\n$ docker save -o image.tar target_image:tag\\n
\\n$ docker system prune -a\\n
\\n# 常用参数列表\\n# -d: 后台运行容器,并返回容器 ID\\n# -p: 指定端口映射,格式为:主机(宿主)端口:容器端口\\n# -i: 以交互模式运行容器,通常与 -t 同时使用\\n# -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用\\n# --name=my_container: 为容器指定一个名称\\n# --dns 8.8.8.8: 指定容器使用的 DNS 服务器,默认和宿主一致\\n$ docker run -d --name=my_container -p 8080:8080 tomcat:latest\\n
\\n# 查看正在运行的容器列表\\n$ docker ps\\n\\n# 查看最近一次创建的容器\\n$ docker ps -l\\n\\n# 查看正在运行的容器 ID 列表\\n$ docker ps -q\\n\\n# 查看全部容器(包括已经停止的容器)\\n$ docker ps -a\\n\\n# 查看全部容器 ID 列表\\n$ docker ps -aq\\n
\\n# 使用容器名停止\\n$ docker stop my_container\\n\\n# 使用容器 ID 停止\\n$ docker stop container_id\\n\\n# 使用容器 ID 停止多个正在运行的容器\\n$ docker ps\\n
\\n# 容器名\\n$ docker start my_container\\n\\n# 容器 ID\\n$ docker start container_id\\n\\n# 使用容器 ID 启动多个已停止的容器\\n$ docker start `docker ps -aq`\\n
\\n# 用容器名删除\\n$ docker rm my_container\\n\\n# 用容器 ID 删除\\n$ docker rm container_id\\n\\n# 删除多个未运行的容器, 运行中的无法删除\\n$ docker rm `docker ps -aq`\\n
\\n# 使用容器名\\n$ docker exec -it my_container /bin/bash\\n\\n# 使用容器 ID\\n$ docker exec -it container_id /bin/bash\\n
\\n# 容器名\\n$ docker inspect my_container\\n\\n# 容器 ID\\n$ docker inspect container_id\\n
\\nqwen3 惊喜发布了,帅!我们用 ollama 和 solon ai (java) 也来尝个鲜。
\\n听说,在个人电脑上用 4b 的参数,效果就很好了。
\\nollama run qwen3:4b\\n
\\n用 solon-initializr ( solon.noear.org/start/ ),生成一个 solon-ai 模板项目。之后:
\\nsolon.ai.chat:\\n qwen3:\\n apiUrl: \\"http://127.0.0.1:11434/api/chat\\" # 使用完整地址(而不是 api_base)\\n provider: \\"ollama\\" # ollama 是有自己的专有接口格式,通过配置 provider 可识别方言\\n model: \\"qwen3:4b\\"\\n
\\n@Configuration\\npublic class DemoConfig {\\n @Bean\\n public ChatModel chatModel(@Inject(\\"${solon.ai.chat.qwen3}\\") ChatConfig config) {\\n return ChatModel.of(config).build();\\n }\\n}\\n
\\n@Controller\\npublic class DemoController {\\n @Inject\\n ChatModel chatModel;\\n\\n @Mapping(\\"hello\\")\\n public String hello(String message) throws IOException {\\n return chatModel.prompt(message).call().getMessage().getContent();\\n }\\n}\\n
\\n启动项目。打开浏览器地址:http://localhost:8080/hello?message=hello
。效果良好:
@Controller\\npublic class DemoController {\\n @Inject\\n ChatModel chatModel;\\n\\n @Produces(MimeType.TEXT_EVENT_STREAM_UTF8_VALUE) //这个很重要,申明用 sse 格式渲染\\n @Mapping(\\"hello\\")\\n public Flux<String> hello(String message) throws IOException {\\n return Flux.from(chatModel.prompt(message).stream())\\n .filter(resp -> resp.hasChoices())\\n .map(resp -> resp.getMessage().getContent());\\n }\\n}\\n
\\n启动项目。再次打开浏览器地址:http://localhost:8080/hello?message=hello
。效果良好:
这里把“联网搜索”,做为一个知识库使用(内部是动态搜索的)。用它作为 RAG 的外部检索支持。
\\nsolon.ai.chat:\\n qwen3:\\n apiUrl: \\"http://127.0.0.1:11434/api/chat\\" # 使用完整地址(而不是 api_base)\\n provider: \\"ollama\\" # ollama 是有自己的专有接口格式,通过配置 provider 可识别方言\\n model: \\"qwen3:4b\\"\\n \\nsolon.ai.repo:\\n websearch:\\n apiUrl: \\"https://api.bochaai.com/v1/web-search\\" # 使用完整地址(而不是 api_base)\\n apiKey: \\"sk-demo...\\"\\n
\\n@Configuration\\npublic class DemoConfig {\\n @Bean\\n public ChatModel chatModel(@Inject(\\"${solon.ai.chat.qwen3}\\") ChatConfig config) {\\n return ChatModel.of(config).build();\\n }\\n \\n @Bean\\n public Repository repository(@Inject(\\"${solon.ai.repo.websearch}\\") AiConfig config) {\\n return new WebSearchRepository(null, config);\\n }\\n}\\n
\\n@Controller\\npublic class DemoController {\\n @Inject\\n ChatModel chatModel;\\n\\n @Inject\\n Repository repository;\\n\\n @Mapping(\\"hello\\")\\n public String hello(String message) throws IOException {\\n //检索\\n List<Document> context = repository.search(new QueryCondition(message).limit(4));\\n\\n //消息增强\\n ChatMessage chatMessage = UserMessage.augment(message, context);\\n\\n //提交大模型并简单返回(不然,截图不好截)\\n return chatModel.prompt(chatMessage).call().getMessage().getContent();\\n }\\n}\\n
\\n启动项目。打开浏览器地址:http://localhost:8080/hello?message=solon%20%E6%98%AF%E8%B0%81%E5%BC%80%E5%8F%91%E7%9A%84%EF%BC%9F
。效果良好:
修改下刚才的配置器,加个模型的默认工具。
\\n@Configuration\\npublic class DemoConfig {\\n @Bean\\n public ChatModel chatModel(@Inject(\\"${solon.ai.chat.qwen3}\\") ChatConfig config) {\\n return ChatModel.of(config)\\n .defaultToolsAdd(new Tools())\\n .build();\\n }\\n\\n public static class Tools {\\n @ToolMapping(description = \\"获取指定城市的天气情况\\")\\n public String get_weather(@ToolParam(description = \\"根据用户提到的地点推测城市\\") String location) {\\n return \\"晴,24度\\";\\n }\\n }\\n}\\n
\\n启动项目。再次打开浏览器地址:http://localhost:8080/hello?message=杭州今天的天气如何?
。效果良好:
继续分享最新的go面经。
\\n今天分享的是组织内部的朋友在字节的go运维工程师岗位的云原生方向的面经,涉及Prometheus、Kubernetes、CI/CD、网络代理、MySQL主从、Redis哨兵、系统调优及基础命令行工具
等知识点,问题我都整理在下面了
回答思路:
\\nhttp_requests_total
)。job=\\"api-server\\", instance=\\"192.168.1.100:9090\\"
)。avg()
, sum()
)、范围查询([5m]
)、条件判断(如 > 90
)等。回答思路:
\\nkubernetes_sd_config
进行服务发现。 scrape_configs: \\n - job_name: \'kubernetes-apiservers\' \\n kubernetes_sd_configs: \\n - role: endpoints \\n relabel_configs: \\n - action: keep \\n regex: default \\n source_labels: [__meta_kubernetes_namespace] \\n
\\nkube-state-metrics
或 cAdvisor
采集。 curl http://localhost:8080/api/v1/nodes/{node-name}/metrics \\n
\\n/metrics
端点,格式符合 Prometheus 文本格式(如通过 prometheus-client-go
库实现)。kube-prometheus-stack
Helm Chart 可一键部署完整监控链。回答思路:
\\nprometheus.yml
或独立的 .rules
文件中定义规则,例如: groups: \\n - name: example \\n rules: \\n - alert: HighCPUUsage \\n expr: instance:node_cpu_usage:rate1m > 0.8 \\n for: 5m \\n labels: \\n severity: warning \\n annotations: \\n summary: \\"Instance {{ $labels.instance }} CPU usage is high\\" \\n
\\nexpr
:PromQL 表达式,定义触发条件。for
:告警持续时间(避免短暂波动触发)。labels
和 annotations
:补充告警元数据和描述。severity
)将告警分发到不同接收者。 route: \\n group_by: [\'alertname\'] \\n group_wait: 30s \\n group_interval: 5m \\n receiver: \'team-alerts\' \\n routes: \\n - match_re: \\n severity: critical \\n receiver: \'oncall-team\' \\n
\\nInstanceDown
)可抑制低优先级告警(如 HighCPUUsage
)。for
参数。fire-and-forget
模式测试配置。回答思路:
\\ngroup_by
)。 global: \\n resolve_timeout: 5m \\n route: \\n receiver: \'team-email\' \\n group_wait: 30s \\n receivers: \\n - name: \'team-email\' \\n email_configs: \\n - to: \'team@example.com\' \\n
\\nprometheus.yml
中指定 Alertmanager 地址: alerting: \\n alertmanagers: \\n - static_configs: \\n - targets: [\'alertmanager:9093\'] \\n
\\n回答思路:
\\ndocker inspect
或 kubectl describe pod
查看容器状态。echo
命令输出中间变量,或使用 debug
模式。回答思路:
\\nproxy_pass
是否指向正确的后端服务地址。proxy_read_timeout
或 proxy_connect_timeout
。ps aux | grep service_name
)。dig
或 nslookup
验证域名解析。error.log
中的 502
错误详情。curl http://backend:8080
)。tail -f /var/log/app.log
)。回答思路:
\\n30000-32767
),外部可通过 NodeIP:NodePort
访问。api.example.com
),常用于跨集群访问。externalTrafficPolicy: Local
控制流量来源。回答思路:
\\nvim filename
。:%s/^/head /g
(替换每一行开头)。ggVG:Ihead
(全局插入)。 sed -i \'s/^/head /\' filename \\n # 或批量处理多行: \\n sed -i \'1i\\\\head\' filename # 在文件开头插入(非每行) \\n
\\n awk \'{print \\"head \\" $0}\' filename > newfile \\n
\\n-i
参数会直接修改原文件,建议先备份。awk ... > newfile
。回答思路:
\\n awk -F \'|\' \'$2 == 8 {count++} END {print count}\' filename \\n
\\n-F \'|\'
:设置分隔符为 |
。$2 == 8
:筛选第二列值为 8 的行。count++
:计数器自增。$2 > 5 && $2 < 10
统计第二列在 5~10 之间的行数。 awk -F \'|\' \'$2 == 8 && $3 ~ /error/ {count++} END {print count}\' \\n
\\n awk -F \'|\' \'$2 == 8 {print $0}\' filename > result.txt \\n
\\ntime
命令或 parallel
加速。回答思路:
\\n upstream backend { \\n server backend1.example.com weight=3; \\n server backend2.example.com; # 权重默认1 \\n server backup.example.com backup; # 备用节点 \\n } \\n
\\n server { \\n listen 80; \\n server_name example.com; \\n location / { \\n proxy_pass http://backend; \\n proxy_next_upstream error timeout invalid_header http_500; # 失败重试策略 \\n } \\n } \\n
\\nround_robin
(默认):轮询。ip_hash
:根据客户端 IP 分配,保持会话。least_conn
:最少连接数。 upstream backend { \\n zone backend 64k; \\n server backend1.example.com; \\n health_check; \\n } \\n
\\nproxy_next_upstream
规则剔除故障节点。proxy_connect_timeout 5s;
。ip_hash
或 Cookie。curl -I http://example.com
检查响应头的 X-Forwarded-For
。回答思路:
\\nread_only
参数控制从库写入。@Transactional
注解控制)。回答思路:
\\ntop
/htop
:实时查看 CPU、内存、进程状态。vmstat
:监控内存、swap、IO 等。iostat
:分析磁盘 IO 性能。netstat
, tcpdump
, iftop
。nice
调整优先级)。vm.swappiness
控制 swap 使用)。vm.max_map_count
(如 Elasticsearch 需要)。oom_score_adj
防止关键进程被 OOM Killer 终止。read_ahead
缓冲区大小。noatime
挂载选项减少磁盘写入。net.core.somaxconn
扩大连接队列。TCP BBR
拥塞控制算法。perf top
定位热点函数。innodb_buffer_pool_size
)。sar
数据,确保性能提升。我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
\\n没准能让你能刷到自己意向公司的最新面试题呢。
\\n感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
","description":"继续分享最新的go面经。 今天分享的是组织内部的朋友在字节的go运维工程师岗位的云原生方向的面经,涉及Prometheus、Kubernetes、CI/CD、网络代理、MySQL主从、Redis哨兵、系统调优及基础命令行工具等知识点,问题我都整理在下面了\\n\\n面经详解\\nPrometheus 的信息采集原理?\\n\\n回答思路:\\n\\n数据模型:Prometheus 采用时间序列数据模型,每个数据点由以下部分组成:\\n度量名称(Metric Name):标识数据的类型(如 http_requests_total)。\\n标签(Labels):键值对形式的元数据…","guid":"https://juejin.cn/post/7498354447241641994","author":"王中阳讲编程","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T04:28:44.826Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/841069345216427cb5d570e1dd92c9a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg546L5Lit6Ziz6K6y57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1746505724&x-signature=8CqRUxTZVubVAiEPx9xwwgNXTEQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Spring Boot 3.4 配置校验新特性全解锁","url":"https://juejin.cn/post/7498280252466430015","content":"配置属性验证增强亮点
\\nSpring Boot 3.4 对配置校验支持进行了全面升级,核心亮点包括:
\\n@NotNull
、@Email
、@Pattern
等)可以说,从易用性到严谨性,都有了质的飞跃!
\\n以用户配置为例:
\\npackage com.icoderoad.demo.config;\\n\\n\\nimport jakarta.validation.Valid;\\nimport jakarta.validation.constraints.*;\\nimport org.springframework.boot.context.properties.ConfigurationProperties;\\nimport org.springframework.validation.annotation.Validated;\\n\\n\\nimport java.util.List;\\n\\n\\n@Validated\\n@ConfigurationProperties(prefix = \\"app.user\\")\\npublic class UserProperties {\\n\\n\\n @NotBlank(message = \\"用户名不能为空\\")\\n private String username;\\n\\n\\n @Email(message = \\"邮箱格式不正确\\")\\n private String email;\\n\\n\\n @Min(value = 18, message = \\"年龄不能小于18岁\\")\\n private Integer age;\\n\\n\\n @Valid\\n private Address address;\\n\\n\\n @Size(min = 1, message = \\"至少需要一个角色\\")\\n private List<@NotBlank(message = \\"角色名称不能为空\\") String> roles;\\n\\n\\n // Address是嵌套对象,需要加@Valid\\n public static class Address {\\n @NotBlank(message = \\"城市不能为空\\")\\n private String city;\\n\\n\\n @Pattern(regexp = \\"\\\\d{6}\\", message = \\"邮编必须是6位数字\\")\\n private String zipCode;\\n\\n\\n // getter/setter\\n public String getCity() {\\n return city;\\n }\\n public void setCity(String city) {\\n this.city = city;\\n }\\n public String getZipCode() {\\n return zipCode;\\n }\\n public void setZipCode(String zipCode) {\\n this.zipCode = zipCode;\\n }\\n }\\n\\n\\n // getter/setter\\n public String getUsername() {\\n return username;\\n }\\n public void setUsername(String username) {\\n this.username = username;\\n }\\n public String getEmail() {\\n return email;\\n }\\n public void setEmail(String email) {\\n this.email = email;\\n }\\n public Integer getAge() {\\n return age;\\n }\\n public void setAge(Integer age) {\\n this.age = age;\\n }\\n public Address getAddress() {\\n return address;\\n }\\n public void setAddress(Address address) {\\n this.address = address;\\n }\\n public List<String> getRoles() {\\n return roles;\\n }\\n public void setRoles(List<String> roles) {\\n this.roles = roles;\\n }\\n}\\n
\\napp:\\n user:\\n username: \\"张三\\"\\n email: \\"zhangsan@example.com\\"\\n age: 25\\n address:\\n city: \\"上海\\"\\n zipCode: \\"200000\\"\\n roles:\\n - \\"admin\\"\\n - \\"user\\"\\n
\\n在你的服务中注入:
\\npackage com.icoderoad.demo.service;\\n\\n\\nimport com.example.demo.config.UserProperties;\\nimport org.springframework.stereotype.Service;\\n\\n\\n@Service\\npublic class UserService {\\n\\n\\n private final UserProperties userProperties;\\n\\n\\n public UserService(UserProperties userProperties) {\\n this.userProperties = userProperties;\\n }\\n\\n\\n public void printUserInfo() {\\n System.out.println(\\"用户名:\\" + userProperties.getUsername());\\n System.out.println(\\"邮箱:\\" + userProperties.getEmail());\\n }\\n}\\n
\\n注意,在嵌套对象上必须标注 @Valid
,才能对子属性继续校验。集合元素(如 List<String>
)同样支持元素级校验注解!
这让配置类的约束更加细粒度、安全。
\\n启动阶段即校验失败
\\n如果配置不符合要求,比如漏填 username
、邮箱格式错误、年龄不足18岁、角色列表为空等,Spring Boot 启动时就会直接报错!
示例错误日志:
\\n***************************\\nAPPLICATION FAILED TO START\\n***************************\\n\\nDescription:\\n\\nBinding to target [Bindable@xxx type = com.icoderoad.demo.config.UserProperties] failed:\\n\\n Property: app.user.username\\n Value: \\n Reason: 用户名不能为空\\n\\n Property: app.user.email\\n Value: not-an-email\\n Reason: 邮箱格式不正确\\n
\\n非常直观,能第一时间发现配置问题,避免服务上线后隐患!
\\n配合 Spring Boot 的 spring-boot-configuration-processor
插件,还能自动生成提示补全信息(IDE 中 .yml
配置智能提示)!
pom.xml
配置:
<dependency>\\n<groupId>org.springframework.boot</groupId>\\n<artifactId>spring-boot-configuration-processor</artifactId>\\n<optional>true</optional>\\n</dependency>\\n
\\n编译后,会生成 META-INF/spring-configuration-metadata.json
,供 IDE 智能补全参考。
@ConfigurationProperties
@Validated
@Valid
扩展:错误处理更友好(自定义异常消息格式)
\\n默认启动校验失败时,Spring Boot 抛出 BindValidationException
,信息虽然完整但略显杂乱。为了让错误提示更专业友好,我们可以自定义异常处理。
package com.icoderoad.demo.exception;\\n\\n\\n/**\\n * 自定义配置校验异常\\n */\\npublic class ConfigValidationException extends RuntimeException {\\n\\n\\n public ConfigValidationException(String message) {\\n super(message);\\n }\\n}\\n
\\n通过 BeanFactoryPostProcessor
统一拦截配置阶段的校验错误:
package com.icoderoad.demo.exception;\\n\\n\\nimport org.springframework.beans.BeansException;\\nimport org.springframework.beans.factory.config.BeanFactoryPostProcessor;\\nimport org.springframework.boot.context.properties.bind.BindValidationException;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.validation.ObjectError;\\n\\n\\nimport java.util.stream.Collectors;\\n\\n\\n@Configuration\\npublic class ConfigValidationExceptionHandler {\\n\\n\\n @Bean\\n public static BeanFactoryPostProcessor configurationPropertiesValidator() {\\n return beanFactory -> {\\n try {\\n // 手动触发bean初始化\\n } catch (BeansException ex) {\\n Throwable cause = ex.getCause();\\n if (cause instanceof BindValidationException bindValidationException) {\\n String errorMessages = bindValidationException.getValidationErrors()\\n .getAllErrors()\\n .stream()\\n .map(ObjectError::getDefaultMessage)\\n .collect(Collectors.joining(\\"; \\"));\\n throw new ConfigValidationException(\\"配置属性校验失败:\\" + errorMessages);\\n }\\n throw ex;\\n }\\n };\\n }\\n}\\n
\\n逻辑解释:
\\nBindValidationException
;
拼接成简洁可读的文本ConfigValidationException
比如你的配置错误如下:
\\napp:\\n user:\\n username: \\"\\"\\n email: \\"wrong\\"\\n age: 15\\n address:\\n city: \\"\\"\\n zipCode: \\"12abc\\"\\n roles:\\n - \\"\\"\\n
\\n启动时抛出的错误变成:
\\n配置属性校验失败:用户名不能为空; 邮箱格式不正确; 年龄不能小于18岁; 城市不能为空; 邮编必须是6位数字; 角色名称不能为空\\n1.\\n
\\nSpring Boot 3.4 配置属性验证:
\\n在实际项目中,推荐配合自定义异常机制,打造更加专业可靠的配置校验体系!
","description":"配置属性验证增强亮点 Spring Boot 3.4 对配置校验支持进行了全面升级,核心亮点包括:\\n\\n支持 jakarta.validation 全套标准注解(如 @NotNull、@Email、@Pattern 等)\\n嵌套对象、集合元素 的深度校验支持\\n启动阶段校验失败,IDE友好提示,快速定位问题\\n自动生成更完善的开发时元信息(metadata)\\n\\n可以说,从易用性到严谨性,都有了质的飞跃!\\n\\n基本用法示例\\n定义配置类\\n\\n以用户配置为例:\\n\\npackage com.icoderoad.demo.config;\\n\\n\\nimport jakarta.valida…","guid":"https://juejin.cn/post/7498280252466430015","author":"星辰聊技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T02:22:12.878Z","media":null,"categories":["后端","架构","Java"],"attachments":null,"extra":null,"language":null},{"title":"线上 nacos 挂了 !cp 模式下,naming server down 掉问题深度解析!","url":"https://juejin.cn/post/7498535220732985380","content":"某日中午 12 点 40,生产环境很多服务同时收到大量 NacosException 告警。
\\n异常详情如下,看详情,是 nacos naming 挂了,原因是跟 raft 相关。
\\n查看监控,此时 cpu 使用率、内存、gc 都正常 ,nacos cofing 模块也正常。
\\n中午 12 点 40 时,其他部门 golang 应用进行了发版,接入了 nacos,同时 ephemeral 设置的为 false。
\\nephemeral 是控制 nacos 使用 ap 模式还是 cp 模式的,我们 java 服务默认值为 true,也就是使用 ap 模式,一致性协议用的 distro;cp 模式一致性协议用的 raft,所以可以初步判断应该是这个上线造成的。
\\n因为 nacos 不管是对配置文件、还是所注册服务的实例列表在本地都是有缓存的,所以此时虽然 nacos naming 挂了,但是并没影响到业务正常运行。
\\n随后紧急取消了 golang 服务发布,重启了 nacos 集群恢复了正常。
\\n查看 nacos server error 日志,发现有这样一个异常。
\\n查看源码,代码如下:
\\nOp 取值只有这三个,这说明 request 中的 operation 字段的值并不是这三个之一。
\\n继续查看 nacos 状态机的处理函数,可以看到 request 对象是通过 ProtoMessageUtil.parse 解析而来。
\\nProtoMessageUtil.parse 代码如下,先是尝试将字节流解析为 WriteRequest,如果解析不了,再尝试解析为 ReadRequest。
\\n那会不会一个 read 的字节流被 parse 成 write 了呢?
\\n试了一下,还真的可以,所以上述那个异常就可以解释通了,read 请求被解析成了 write 请求,拿到的 operation 为空字符串,当然就 IllegalArgumentException 异常了。
\\n所以这是 nacos2.1.0 以下版本的一个 bug,在 2.1.0 修复了,我们使用的 nacos 是基于 2.0.4 构建的。
\\n同时在 2.2.3 版本对此处异常进行了捕获。
\\n这个状态机异常为什么会导致 nacos naming 挂掉呢?
\\nnacos naming server 模块,每个节点都有个状态,当状态机抛异常后,server 就会处于 DOWN 状态。
\\n同时 naming 模块有个过滤器 TrafficReviseFilter,会对所以的入口流量拦截,如果 server status != UP,就会直接以 503 返回所有请求。
\\n所以我们服务请求 nacos naming 接口时都会返回 server is DOWNnow, detailed error message: Optional[The raft peer is in error: null],这个异常。
\\n至于具体是如何设置 server status 为 DOWN 的呢,当 nacos 状态机异常后,会走到下述代码:
\\n将 error msg 设置到 BasePersistentServiceProcessor 中。
\\n有个定时任务会每隔 5s 检查下,如果一致性服务有 error,就会将改 server 状态设置为 DOWN。
\\n当时的现象是 naming server node2 流量翻了近 2 倍,node0、node1 流量降到了正常时的 1/4,同时 nacos 控制台服务刷新服务列表有时有数据,有时没数据。
\\nnode2 是 leader,node0、node1 是 follower,证明 leader 并没有挂,两个 follower 挂了。
\\n同时在日志里也没看到 node2 报的异常。
\\n那为什么 node2 没挂呢?仔细看了下代码,leader 会走到第一步处理中,不会用到 ProtoMessageUtil.parse 做去反序列化,所以也就不会将 read 请求转为 write 请求,也就不会执行异常导致节点挂掉了。
\\n至于为什么是 golang 服务上线导致的呢?
\\n之前 nacos 上注册的节点都是临时节点,也就是使用的 ap 模式,没有用到 raft 协议相关,golang 服务使用的是 cp 模式,所以会使用 raft 协议相关功能。
\\n实例注册的时候会触发到从 nacos 集群获取数据,此时会构造 read 请求,触发上述异常,导致节点挂掉。
\\n开源软件在为项目开发带来高效便捷的同时,也暗藏潜在风险。
\\n许多开源软件都存在未被触发的 Bug,这些隐藏的 Bug 就像 “定时炸弹”,在特定场景下才会暴露。
\\n日常开发中,开发者可能对这些潜在问题毫无察觉,一旦触发,往往会导致严重的生产故障,带来巨大的业务损失。
\\n因此,定期关注项目中使用的开源软件发版记录尤为重要。仔细查看每次版本更新说明,会发现其中包含大量的 Bugfix。
\\n你工作中遇到过哪些开源软件的 Bug 呢?欢迎评论区讨论!
","description":"问题现象 某日中午 12 点 40,生产环境很多服务同时收到大量 NacosException 告警。\\n\\n异常详情如下,看详情,是 nacos naming 挂了,原因是跟 raft 相关。\\n\\n查看监控,此时 cpu 使用率、内存、gc 都正常 ,nacos cofing 模块也正常。\\n\\n中午 12 点 40 时,其他部门 golang 应用进行了发版,接入了 nacos,同时 ephemeral 设置的为 false。\\n\\nephemeral 是控制 nacos 使用 ap 模式还是 cp 模式的,我们 java 服务默认值为 true,也就是使用 ap…","guid":"https://juejin.cn/post/7498535220732985380","author":"CodeFox","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T02:21:24.709Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/999bd1fb428a4ffd86733476c67a4efe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=JKERKCbOXqdsOnFvfllr47nn54U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f1e3a618d5234bc38c1a2fe228c087d4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=CvbE%2FN3iIU4ld5sq5Q4TjnAbmrQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4d9804d88eaf4d06938c7990edebba80~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=PyA%2BHBdm6DhFiXcD7%2B9YBVQX%2BTs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/221298cc0c1046ffa6b6c866c7a687bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=qu5KjrIim3fzVO9lAfIPVpSu7Do%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2bffd6af652f4f1086a70de3b58e5206~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=KU14wfpNoX8hdVB0WX0928IqwBE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1f28f27c2dc14b9f92edd57d3e9d8ccc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=nSTPIdrEtxsbFOnMIar%2BJQ8D4tE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1fc4a655c5a448479b5bff7adb01e29b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=LoO%2BBrHEy05bTZw7oZThAQn3RgU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7f1d17b455264991af9472d7cdcc25b2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=PrJiUUTpkmKY9%2B1iazrw5ksNYTk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/19983b55fab3491cb36b5f11ccd8fc56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=wKQpogYqK1PHYe%2BTvXE440PuJ10%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ef7400d113ac4dcc98746f9f2ddbf85f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=L41cJHUuSmu7Gu5lqvYppHOuuuo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/806a4a48130b446a8c5e58b85ac17903~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=x9ntWvbtMxD%2F657QFmBv8jgmca0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/13f52a779fe145e0be3a98b5667fc488~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=vfH9KwBWXUSSFnfkU5qHm3Ze4ME%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7288ce46f16643fd916a5b0bc078467e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=o7pEzcpI1chElo%2BZ8%2FTd6CMlK4g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e4e6f57b886e4676aff88e901bedcdf8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=HXQgIxsIVra8u5nuTGcaxtWkqoM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5bea038f53ec484c82138ff3f405de69~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=VntXKZ7qoxE05upYqmx1sf81kic%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a6c19e1da5a4e718a3c9f6baec00123~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=A0dJdu9CjD47LO0d6EBx2CU7J%2BE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50d1b9613e044a6099eff5de3c4ff077~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=3zVWy71%2FFnk8lLQ7AcOBAFRiPl0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd3f5473bad74c5eb0d2c9491aa17a56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=Jjd%2BfbXm0nZCsVcZfiyAAOiy4yo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f8be9f32010e40c7bb43fc30d94e373f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=j44g0c5wNZm3kzM6sZ%2BtuFlggPk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da6b29ef7dac4a5681c1a0acdc532382~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746498083&x-signature=yG1Gaff8O%2Bw%2B9ZPE1YWegkG2alI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"换掉ES!SpringBoot + Meilisearch实现商品搜索,太方便了!","url":"https://juejin.cn/post/7498291152727949350","content":"\\n\\n在我的mall电商实战项目中,有使用过Elasticsearch实现商品搜索功能。其实商品搜索也可以使用Meilisearch来实现,实现起来还是非常方便的,今天就来带大家实现一下!
\\n
学习本文需要对Meilisearch有所了解,还没有了解过它的小伙伴可以参考下这篇教程:
\\n《超越Elasticsearch!号称下一代搜索引擎,性能炸裂!》
\\n下面是使用Meilisearch实现商品搜索的效果图,搜索速度还是非常快的!
\\n\\n\\n由于我们会以mall项目中的商品搜索功能为例来讲解Meilisearch的使用,这里先简单介绍下mall项目。
\\n
mall项目是一套基于 SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\n\\n\\n接下来就来带大家使用SpringBoot + Meilisearch实现下商品搜索功能。
\\n
pom.xml
文件中添加Meilisearch的Java SDK依赖;<!--Meilisearch Java SDK--\x3e\\n<dependency>\\n <groupId>com.meilisearch.sdk</groupId>\\n <artifactId>meilisearch-java</artifactId>\\n <version>0.14.3</version>\\n</dependency>\\n
\\napplication.yml
文件中添加Meilisearch的连接配置;meilisearch:\\n host: http://192.168.3.101:7700\\n index: products\\n
\\nMeilisearchConfig
,配置好Meilisearch对应的Client;/**\\n * @auther macrozheng\\n * @description Meilisearch配置类\\n * @date 2025/4/18\\n * @github https://github.com/macrozheng\\n */\\n@Configuration\\npublic class MeilisearchConfig {\\n\\n @Value(\\"${meilisearch.host}\\")\\n private String MEILISEARCH_HOST;\\n\\n @Bean\\n public Client searchClient(){\\n return new Client(new Config(MEILISEARCH_HOST,null));\\n }\\n}\\n
\\n/**\\n * @auther macrozheng\\n * @description Meilisearch搜索功能Controller\\n * @date 2025/4/18\\n * @github https://github.com/macrozheng\\n */\\n@RestController\\n@Tag(name = \\"MeilisearchController\\",description = \\"Meilisearch搜索功能\\")\\n@RequestMapping(\\"/meilisearch\\")\\npublic class MeilisearchController {\\n\\n @Value(\\"${meilisearch.index}\\")\\n private String MEILISEARCH_INDEX;\\n\\n @Autowired\\n private Client searchClient;\\n\\n @Operation(summary = \\"创建索引并导入商品数据\\")\\n @GetMapping(\\"/createIndex\\")\\n public CommonResult createIndex(){\\n ClassPathResource resource = new ClassPathResource(\\"json/products.json\\");\\n String jsonStr = IoUtil.read(resource.getStream(), Charset.forName(\\"UTF-8\\"));\\n Index index = searchClient.index(MEILISEARCH_INDEX);\\n TaskInfo info = index.addDocuments(jsonStr, \\"id\\");\\n return CommonResult.success(info);\\n }\\n\\n @Operation(summary = \\"刪除商品索引\\")\\n @GetMapping(\\"/deleteIndex\\")\\n public CommonResult deleteIndex(){\\n TaskInfo info = searchClient.deleteIndex(MEILISEARCH_INDEX);\\n return CommonResult.success(info);\\n }\\n}\\n
\\n{\\n \\"id\\": 27,\\n \\"productSn\\": \\"7437788\\",\\n \\"brandId\\": 6,\\n \\"brandName\\": \\"小米\\",\\n \\"productCategoryId\\": 19,\\n \\"productCategoryName\\": \\"手机通讯\\",\\n \\"pic\\": \\"http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180615/xiaomi.jpg\\",\\n \\"name\\": \\"小米8 全面屏游戏智能手机 6GB+64GB 黑色 全网通4G 双卡双待\\",\\n \\"subTitle\\": \\"骁龙845处理器,红外人脸解锁,AI变焦双摄,AI语音助手小米6X低至1299,点击抢购\\",\\n \\"keywords\\": \\"\\",\\n \\"price\\": 2699.0,\\n \\"sale\\": 99,\\n \\"newStatus\\": 1,\\n \\"recommandStatus\\": 1,\\n \\"stock\\": 100,\\n \\"promotionType\\": 3,\\n \\"sort\\": 0\\n}\\n
\\n/**\\n * @auther macrozheng\\n * @description Meilisearch搜索功能Controller\\n * @date 2025/4/18\\n * @github https://github.com/macrozheng\\n */\\n@RestController\\n@Tag(name = \\"MeilisearchController\\",description = \\"Meilisearch搜索功能\\")\\n@RequestMapping(\\"/meilisearch\\")\\npublic class MeilisearchController {\\n\\n @Operation(summary = \\"获取索引设置\\")\\n @GetMapping(\\"/getSettings\\")\\n public CommonResult getSettings(){\\n Settings settings = searchClient.index(MEILISEARCH_INDEX).getSettings();\\n return CommonResult.success(settings);\\n }\\n\\n @Operation(summary = \\"修改索引设置\\")\\n @GetMapping(\\"/updateSettings\\")\\n public CommonResult updateSettings(){\\n Settings settings = new Settings();\\n settings.setFilterableAttributes(new String[]{\\"productCategoryName\\"});\\n settings.setSortableAttributes(new String[]{\\"price\\"});\\n TaskInfo info = searchClient.index(MEILISEARCH_INDEX).updateSettings(settings);\\n return CommonResult.success(info);\\n }\\n}\\n
\\n关键字搜索、分页、按商品分类筛选、按价格排序
的综合搜索功能。/**\\n * @auther macrozheng\\n * @description Meilisearch搜索功能Controller\\n * @date 2025/4/18\\n * @github https://github.com/macrozheng\\n */\\n@RestController\\n@Tag(name = \\"MeilisearchController\\",description = \\"Meilisearch搜索功能\\")\\n@RequestMapping(\\"/meilisearch\\")\\npublic class MeilisearchController {\\n @Operation(summary = \\"根据关键字分页搜索商品\\")\\n @GetMapping(value = \\"/search\\")\\n @ResponseBody\\n public CommonResult search(@RequestParam(required = false) String keyword,\\n @RequestParam(required = false, defaultValue = \\"1\\") Integer pageNum,\\n @RequestParam(required = false, defaultValue = \\"5\\") Integer pageSize,\\n @RequestParam(required = false) String productCategoryName,\\n @RequestParam(required = false,value = \\"0->按价格升序;1->按价格降序\\") Integer order) {\\n SearchRequest.SearchRequestBuilder searchBuilder = SearchRequest.builder();\\n searchBuilder.page(pageNum);\\n searchBuilder.hitsPerPage(pageSize);\\n if(StrUtil.isNotEmpty(keyword)){\\n searchBuilder.q(keyword);\\n }\\n if(StrUtil.isNotEmpty(productCategoryName)){\\n searchBuilder.filter(new String[]{\\"productCategoryName=\\"+productCategoryName});\\n }\\n if(order!=null){\\n if(order==0){\\n searchBuilder.sort(new String[]{\\"price:asc\\"});\\n }else if(order==1){\\n searchBuilder.sort(new String[]{\\"price:desc\\"});\\n }\\n }\\n Searchable searchable = searchClient.index(MEILISEARCH_INDEX).search(searchBuilder.build());\\n return CommonResult.success(searchable);\\n }\\n}\\n
\\n\\n\\n接下来我们来演示下上面实现的商品搜索功能。
\\n
/meilisearch/createIndex
来实现创建索引并导入商品数据;手机
看下效果;/meilisearch/updateSettings
来修改索引设置;/meilisearch/search
接口来按关键字搜索商品、分页、筛选商品分类并按价格降序;\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n这里对Meilisearch和Elasticsearch两种搜索引擎做个对比。
\\n
Meilisearch | Elasticsearch | |
---|---|---|
架构与设计 | 轻量级、开箱即用 | 分布式、部署配置复杂 |
性能 | 50毫秒以内,中小数据集场景 | 在大规模数据场景下性能更优 |
SDK | 支持多种语言、API简洁易用 | 插件生态丰富、API学习成本高 |
中文支持 | 原生支持 | 需要额外配置中文分词插件 |
配置要求 | 占用内存低 | 占用内存高 |
今天带大家使用Meilisearch实现了商品搜索,由于它原生支持中文,API也是简洁易用,使用起来确实挺方便的!
\\n在技术圈,Go和Rust的“神仙打架”已经持续好几年了。有人说Go简单高效,是云原生和后端开发的首选;有人说Rust才是未来,安全性无敌,性能爆炸。
\\n但你有没有想过,也许下一个真正能改变行业格局的黑马,根本不是他们俩?今天我们要聊的,就是最近悄悄火出圈的Carbon语言——它被称为“C++真正的继承者”,有望成为下一个程序员财富密码!别等到“Carbon工程师月薪5万”才后悔没早行动!
\\n打开知乎、V2EX或者技术微信群,Go和Rust的优缺点讨论永远吵得热火朝天。Go的语法简洁、并发模型强大,适合做云服务、API、工具链;
\\nRust则以内存安全和“编译即正确”著称,硬核开发者的心头好。
\\n但有个现实问题,谁来接替C++?毕竟,世界上最重要的底层系统
、区块链
、操作系统
、数据库
,几乎都离不开C++。无论Go多简单、Rust多安全,都很难让企业把数十年积累的C++代码一下子全部重写。
Carbon不是又一个“自嗨”新语言,它的定位非常精准:不是要推翻C++,而是要无缝继承和升级C++。用一句话总结,Carbon就是“给C++老项目一个现代化新生命”的桥梁。
\\n\\n\\n“Carbon是C++的继任者,不是C++的渐进式升级。它天生支持与C++互操作,适合大规模代码迁移和开发者转型。”
\\n
很多人担心新语言学习成本高,但Carbon的设计思路是“让C++程序员一眼就懂”,同时保留Go/Rust的现代感。
\\n看一眼Carbon的函数声明:
\\nfn Main() -> i32 {\\n var s: auto = \\"Hello world!\\";\\n Print(s);\\n return 0;\\n}\\n
\\n是不是比C++清爽多了?变量声明、类型推断、打印函数都很直观。你不需要像学Rust那样“和借用检查器打仗”,也不用担心Go的“过于简陋”。
\\nCarbon的泛型支持也非常现代,而且可以选择性地兼容C++的模板。你想用新特性可以用,不想用也能和老代码完美协作。
\\n别忘了,很多区块链项目(比如比特币)本身就是C++写的。Rust虽然在Web3领域很火,但一旦涉及到和C++互操作,难度就很大——很多团队宁愿不升级,也不想冒重写代码的风险。
\\nCarbon的出现,等于给这些项目提供了“渐进式升级”方案:性能敏感、核心部分用Carbon重写,其它部分继续用C++。不用大拆大建,风险低、收益高,这才是大厂、金融机构最喜欢的路径。
\\n很多开发者吐槽,Rust门槛高到“劝退”,尤其是“借用检查器”让人头秃。Carbon的设计哲学是“让C++程序员无痛转型”,语法更友好,学习曲线更平滑。
\\nCarbon直接用LLVM做底层编译器,这可是和Clang、Swift、Rust同一个家族。意味着性能、可扩展性都不用担心。而且,得益于C++互操作,一出生就能用C++全家桶的生态资源,不像Go/Rust早期那样“孤军奋战”。
\\n大胆预测,未来五年,Carbon极有可能成为C++项目升级、系统级开发、区块链等领域的“标配”。Go会继续在云原生、API、工具领域发光发热,Rust会在新项目和极致安全领域有一席之地。但Carbon极可能成为“老项目升级+新项目开发”的双料冠军。
\\n想象一下,等招聘市场上出现“10年Carbon经验”岗位时,你已经提前上车,不香吗?
\\n技术圈的风向变化很快,抓住新趋势的人,往往能实现“弯道超车”。现在大家都在争论Go和Rust,真正的聪明人已经开始偷偷学Carbon了。别等到“Carbon工程师月薪5万”才后悔没早行动!
\\n关注梦兽编程微信公众号,第一时间掌握Carbon、Go、Rust等新语言实战技巧和入门资料,和一群有远见的程序员一起冲刺下一个技术红利!
\\n你怎么看?Carbon会成为C++的真正继承者吗?会不会颠覆Rust和Go?欢迎留言讨论、点赞、收藏、转发,和更多技术同好一起冲浪新风口!
\\n(关注梦兽编程,爆款技术干货天天见!)
","description":"在技术圈,Go和Rust的“神仙打架”已经持续好几年了。有人说Go简单高效,是云原生和后端开发的首选;有人说Rust才是未来,安全性无敌,性能爆炸。 但你有没有想过,也许下一个真正能改变行业格局的黑马,根本不是他们俩?今天我们要聊的,就是最近悄悄火出圈的Carbon语言——它被称为“C++真正的继承者”,有望成为下一个程序员财富密码!别等到“Carbon工程师月薪5万”才后悔没早行动!\\n\\nGo和Rust之争,其实没那么重要?\\n\\n打开知乎、V2EX或者技术微信群,Go和Rust的优缺点讨论永远吵得热火朝天。Go的语法简洁、并发模型强大,适合做云服务、API…","guid":"https://juejin.cn/post/7498286960236019753","author":"傻梦兽","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T01:36:03.425Z","media":null,"categories":["后端","C++"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot中4种登录验证码实现方案","url":"https://juejin.cn/post/7498286956343066663","content":"在当今互联网安全形势日益严峻的环境下,验证码已成为保护用户账户安全、防止暴力破解和自动化攻击的重要手段。尤其在登录系统中,合理使用验证码不仅能有效阻止机器人批量尝试账号密码,还能降低账户被盗风险,提升系统安全性。
\\n本文将详细介绍在SpringBoot应用中实现四种登录验证码的技术方案,包括图形验证码、短信验证码、邮箱验证码和滑动拼图验证码。
\\n图形验证码是最传统且应用最广泛的验证码类型,原理是在服务端生成随机字符串并渲染成图片,用户需要识别图片中的字符并输入。图形验证码实现简单,对用户体验影响较小,是中小型应用的理想选择。
\\n<dependency>\\n <groupId>com.github.penggle</groupId>\\n <artifactId>kaptcha</artifactId>\\n <version>2.3.2</version>\\n</dependency>\\n
\\n@Configuration\\npublic class KaptchaConfig {\\n \\n @Bean\\n public Producer kaptchaProducer() {\\n Properties properties = new Properties();\\n // 图片宽度\\n properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, \\"150\\");\\n // 图片高度\\n properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, \\"50\\");\\n // 字体大小\\n properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, \\"32\\");\\n // 字体颜色\\n properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, \\"black\\");\\n // 文本集合,验证码值从此集合中获取\\n properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, \\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\\");\\n // 验证码长度\\n properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, \\"4\\");\\n // 干扰线颜色\\n properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, \\"blue\\");\\n // 去除背景渐变\\n properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, \\"com.google.code.kaptcha.impl.ShadowGimpy\\");\\n \\n Config config = new Config(properties);\\n return config.getProducerImpl();\\n }\\n}\\n
\\n@Service\\npublic class CaptchaService {\\n \\n @Autowired\\n private RedisTemplate<String, String> redisTemplate;\\n \\n // 验证码有效期(秒)\\n private static final long CAPTCHA_EXPIRATION = 300;\\n \\n // 存储验证码\\n public void storeCaptcha(String sessionId, String captchaCode) {\\n redisTemplate.opsForValue().set(\\n \\"CAPTCHA:\\" + sessionId, \\n captchaCode, \\n CAPTCHA_EXPIRATION, \\n TimeUnit.SECONDS);\\n }\\n \\n // 验证验证码\\n public boolean validateCaptcha(String sessionId, String userInputCaptcha) {\\n String key = \\"CAPTCHA:\\" + sessionId;\\n String storedCaptcha = redisTemplate.opsForValue().get(key);\\n \\n if (storedCaptcha != null && storedCaptcha.equalsIgnoreCase(userInputCaptcha)) {\\n // 验证成功后立即删除,防止重复使用\\n redisTemplate.delete(key);\\n return true;\\n }\\n \\n return false;\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/captcha\\")\\npublic class CaptchaController {\\n \\n @Autowired\\n private Producer kaptchaProducer;\\n \\n @Autowired\\n private CaptchaService captchaService;\\n \\n @GetMapping(\\"/image\\")\\n public void getImageCaptcha(HttpServletResponse response, HttpServletRequest request) throws IOException {\\n // 清除浏览器缓存\\n response.setDateHeader(\\"Expires\\", 0);\\n response.setHeader(\\"Cache-Control\\", \\"no-store, no-cache, must-revalidate\\");\\n response.addHeader(\\"Cache-Control\\", \\"post-check=0, pre-check=0\\");\\n response.setHeader(\\"Pragma\\", \\"no-cache\\");\\n response.setContentType(\\"image/jpeg\\");\\n \\n // 创建验证码文本\\n String capText = kaptchaProducer.createText();\\n \\n // 获取会话ID\\n String sessionId = request.getSession().getId();\\n \\n // 存储验证码\\n captchaService.storeCaptcha(sessionId, capText);\\n \\n // 创建验证码图片\\n BufferedImage image = kaptchaProducer.createImage(capText);\\n ServletOutputStream out = response.getOutputStream();\\n \\n // 输出图片\\n ImageIO.write(image, \\"jpg\\", out);\\n out.flush();\\n out.close();\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/auth\\")\\npublic class AuthController {\\n \\n @Autowired\\n private CaptchaService captchaService;\\n \\n @Autowired\\n private UserService userService;\\n \\n @PostMapping(\\"/login\\")\\n public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletRequest request) {\\n // 获取会话ID\\n String sessionId = request.getSession().getId();\\n \\n // 验证验证码\\n if (!captchaService.validateCaptcha(sessionId, loginRequest.getCaptcha())) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"验证码错误或已过期\\"));\\n }\\n \\n // 用户名密码验证\\n boolean authenticated = userService.authenticate(\\n loginRequest.getUsername(), \\n loginRequest.getPassword());\\n \\n if (authenticated) {\\n // 生成Token或设置会话\\n String token = jwtTokenProvider.generateToken(loginRequest.getUsername());\\n return ResponseEntity.ok(new JwtAuthResponse(token));\\n } else {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"用户名或密码错误\\"));\\n }\\n }\\n}\\n
\\n<div class=\\"login-form\\">\\n <form id=\\"loginForm\\">\\n <div class=\\"form-group\\">\\n <label for=\\"username\\">用户名</label>\\n <input type=\\"text\\" class=\\"form-control\\" id=\\"username\\" name=\\"username\\" required>\\n </div>\\n <div class=\\"form-group\\">\\n <label for=\\"password\\">密码</label>\\n <input type=\\"password\\" class=\\"form-control\\" id=\\"password\\" name=\\"password\\" required>\\n </div>\\n <div class=\\"form-group\\">\\n <label for=\\"captcha\\">验证码</label>\\n <div class=\\"captcha-container\\">\\n <input type=\\"text\\" class=\\"form-control\\" id=\\"captcha\\" name=\\"captcha\\" required>\\n <img id=\\"captchaImg\\" src=\\"/captcha/image\\" alt=\\"验证码\\" onclick=\\"refreshCaptcha()\\">\\n </div>\\n </div>\\n <button type=\\"submit\\" class=\\"btn btn-primary\\">登录</button>\\n </form>\\n</div>\\n<script>\\nfunction refreshCaptcha() {\\n document.getElementById(\'captchaImg\').src = \'/captcha/image?t=\' + new Date().getTime();\\n}\\n\\ndocument.getElementById(\'loginForm\').addEventListener(\'submit\', function(e) {\\n e.preventDefault();\\n \\n const data = {\\n username: document.getElementById(\'username\').value,\\n password: document.getElementById(\'password\').value,\\n captcha: document.getElementById(\'captcha\').value\\n };\\n \\n fetch(\'/auth/login\', {\\n method: \'POST\',\\n headers: {\\n \'Content-Type\': \'application/json\'\\n },\\n body: JSON.stringify(data)\\n })\\n .then(response => response.json())\\n .then(data => {\\n if (data.token) {\\n localStorage.setItem(\'token\', data.token);\\n window.location.href = \'/dashboard\';\\n } else {\\n alert(data.message);\\n refreshCaptcha();\\n }\\n })\\n .catch(error => {\\n console.error(\'Error:\', error);\\n refreshCaptcha();\\n });\\n});\\n</script>\\n
\\n优点
\\n缺点
\\n短信验证码通过向用户手机发送一次性验证码实现身份验证。用户需要输入收到的验证码完成登录过程。这种方式不仅验证了账号密码的正确性,还确认了用户对手机号的控制权,大幅提高了安全性。
\\n<dependency>\\n <groupId>com.aliyun</groupId>\\n <artifactId>aliyun-java-sdk-core</artifactId>\\n <version>4.5.3</version>\\n</dependency>\\n
\\n@Configuration\\n@ConfigurationProperties(prefix = \\"aliyun.sms\\")\\n@Data\\npublic class SmsProperties {\\n private String accessKeyId;\\n private String accessKeySecret;\\n private String signName;\\n private String templateCode;\\n private String endpoint = \\"dysmsapi.aliyuncs.com\\";\\n}\\n
\\n# application.properties\\naliyun.sms.access-key-id=YOUR_ACCESS_KEY_ID\\naliyun.sms.access-key-secret=YOUR_ACCESS_KEY_SECRET\\naliyun.sms.sign-name=YOUR_SMS_SIGN_NAME\\naliyun.sms.template-code=SMS_TEMPLATE_CODE\\n
\\n@Service\\n@Slf4j\\npublic class SmsService {\\n \\n @Autowired\\n private SmsProperties smsProperties;\\n \\n @Autowired\\n private RedisTemplate<String, String> redisTemplate;\\n \\n private static final long SMS_EXPIRATION = 300; // 5分钟过期\\n private static final String SMS_PREFIX = \\"SMS_CAPTCHA:\\";\\n \\n /**\\n * 发送短信验证码\\n * @param phoneNumber 手机号\\n * @return 是否发送成功\\n */\\n public boolean sendSmsCaptcha(String phoneNumber) {\\n try {\\n // 生成6位随机验证码\\n String captcha = generateCaptcha(6);\\n \\n // 存储验证码\\n redisTemplate.opsForValue().set(\\n SMS_PREFIX + phoneNumber, \\n captcha, \\n SMS_EXPIRATION, \\n TimeUnit.SECONDS);\\n \\n // 构建短信客户端\\n DefaultProfile profile = DefaultProfile.getProfile(\\n \\"cn-hangzhou\\", \\n smsProperties.getAccessKeyId(), \\n smsProperties.getAccessKeySecret());\\n IAcsClient client = new DefaultAcsClient(profile);\\n \\n // 构建短信请求\\n CommonRequest request = new CommonRequest();\\n request.setSysMethod(MethodType.POST);\\n request.setSysDomain(smsProperties.getEndpoint());\\n request.setSysVersion(\\"2017-05-25\\");\\n request.setSysAction(\\"SendSms\\");\\n request.putQueryParameter(\\"RegionId\\", \\"cn-hangzhou\\");\\n request.putQueryParameter(\\"PhoneNumbers\\", phoneNumber);\\n request.putQueryParameter(\\"SignName\\", smsProperties.getSignName());\\n request.putQueryParameter(\\"TemplateCode\\", smsProperties.getTemplateCode());\\n \\n // 设置模板参数,将验证码作为参数传入\\n Map<String, String> templateParam = Map.of(\\"code\\", captcha);\\n request.putQueryParameter(\\"TemplateParam\\", new ObjectMapper().writeValueAsString(templateParam));\\n \\n // 发送短信\\n CommonResponse response = client.getCommonResponse(request);\\n log.info(\\"短信发送结果: {}\\", response.getData());\\n \\n // 解析响应\\n JsonNode responseJson = new ObjectMapper().readTree(response.getData());\\n return \\"OK\\".equals(responseJson.get(\\"Code\\").asText());\\n \\n } catch (Exception e) {\\n log.error(\\"发送短信验证码失败\\", e);\\n return false;\\n }\\n }\\n \\n /**\\n * 验证短信验证码\\n * @param phoneNumber 手机号\\n * @param captcha 用户输入的验证码\\n * @return 是否验证成功\\n */\\n public boolean validateSmsCaptcha(String phoneNumber, String captcha) {\\n String key = SMS_PREFIX + phoneNumber;\\n String storedCaptcha = redisTemplate.opsForValue().get(key);\\n \\n if (storedCaptcha != null && storedCaptcha.equals(captcha)) {\\n // 验证成功后删除验证码,防止重复使用\\n redisTemplate.delete(key);\\n return true;\\n }\\n \\n return false;\\n }\\n \\n /**\\n * 生成指定长度的随机数字验证码\\n */\\n private String generateCaptcha(int length) {\\n StringBuilder captcha = new StringBuilder();\\n Random random = new Random();\\n \\n for (int i = 0; i < length; i++) {\\n captcha.append(random.nextInt(10));\\n }\\n \\n return captcha.toString();\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/captcha\\")\\npublic class SmsCaptchaController {\\n \\n @Autowired\\n private SmsService smsService;\\n \\n @PostMapping(\\"/sms/send\\")\\n public ResponseEntity<?> sendSmsCaptcha(@RequestBody PhoneNumberRequest request) {\\n String phoneNumber = request.getPhoneNumber();\\n \\n // 验证手机号格式\\n if (!isValidPhoneNumber(phoneNumber)) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"手机号格式不正确\\"));\\n }\\n \\n // 发送短信验证码\\n boolean sent = smsService.sendSmsCaptcha(phoneNumber);\\n \\n if (sent) {\\n return ResponseEntity.ok(new ApiResponse(true, \\"验证码已发送,请注意查收\\"));\\n } else {\\n return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\\n .body(new ApiResponse(false, \\"验证码发送失败,请稍后再试\\"));\\n }\\n }\\n \\n // 验证手机号格式(简化版)\\n private boolean isValidPhoneNumber(String phoneNumber) {\\n return phoneNumber != null && phoneNumber.matches(\\"^1[3-9]\\\\d{9}$\\");\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/auth\\")\\npublic class SmsLoginController {\\n \\n @Autowired\\n private SmsService smsService;\\n \\n @Autowired\\n private UserService userService;\\n \\n @Autowired\\n private JwtTokenProvider jwtTokenProvider;\\n \\n @PostMapping(\\"/sms-login\\")\\n public ResponseEntity<?> smsLogin(@RequestBody SmsLoginRequest request) {\\n String phoneNumber = request.getPhoneNumber();\\n String captcha = request.getCaptcha();\\n \\n // 验证短信验证码\\n if (!smsService.validateSmsCaptcha(phoneNumber, captcha)) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"验证码错误或已过期\\"));\\n }\\n \\n // 查找或创建用户\\n User user = userService.findOrCreateByPhone(phoneNumber);\\n \\n if (user != null) {\\n // 生成JWT令牌\\n String token = jwtTokenProvider.generateToken(user.getUsername());\\n return ResponseEntity.ok(new JwtAuthResponse(token));\\n } else {\\n return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\\n .body(new ApiResponse(false, \\"登录失败,请稍后再试\\"));\\n }\\n }\\n}\\n
\\n<div class=\\"sms-login-form\\">\\n <form id=\\"smsLoginForm\\">\\n <div class=\\"form-group\\">\\n <label for=\\"phoneNumber\\">手机号</label>\\n <input type=\\"text\\" class=\\"form-control\\" id=\\"phoneNumber\\" name=\\"phoneNumber\\" required>\\n </div>\\n <div class=\\"form-group\\">\\n <label for=\\"smsCaptcha\\">验证码</label>\\n <div class=\\"captcha-container\\">\\n <input type=\\"text\\" class=\\"form-control\\" id=\\"smsCaptcha\\" name=\\"smsCaptcha\\" required>\\n <button type=\\"button\\" class=\\"btn btn-outline-primary\\" id=\\"sendSmsBtn\\">获取验证码</button>\\n </div>\\n </div>\\n <button type=\\"submit\\" class=\\"btn btn-primary\\">登录</button>\\n </form>\\n</div>\\n<script>\\nlet countdown = 60;\\n\\ndocument.getElementById(\'sendSmsBtn\').addEventListener(\'click\', function() {\\n const phoneNumber = document.getElementById(\'phoneNumber\').value;\\n \\n if (!phoneNumber || !/^1[3-9]\\\\d{9}$/.test(phoneNumber)) {\\n alert(\'请输入正确的手机号\');\\n return;\\n }\\n \\n // 发送短信验证码请求\\n fetch(\'/captcha/sms/send\', {\\n method: \'POST\',\\n headers: {\\n \'Content-Type\': \'application/json\'\\n },\\n body: JSON.stringify({ phoneNumber: phoneNumber })\\n })\\n .then(response => response.json())\\n .then(data => {\\n alert(data.message);\\n if (data.success) {\\n startCountdown();\\n }\\n })\\n .catch(error => {\\n console.error(\'Error:\', error);\\n alert(\'发送验证码失败,请稍后再试\');\\n });\\n});\\n\\nfunction startCountdown() {\\n const btn = document.getElementById(\'sendSmsBtn\');\\n btn.disabled = true;\\n \\n let timer = setInterval(() => {\\n btn.textContent = `${countdown}秒后重新获取`;\\n countdown--;\\n \\n if (countdown < 0) {\\n clearInterval(timer);\\n btn.disabled = false;\\n btn.textContent = \'获取验证码\';\\n countdown = 60;\\n }\\n }, 1000);\\n}\\n\\ndocument.getElementById(\'smsLoginForm\').addEventListener(\'submit\', function(e) {\\n e.preventDefault();\\n \\n const data = {\\n phoneNumber: document.getElementById(\'phoneNumber\').value,\\n captcha: document.getElementById(\'smsCaptcha\').value\\n };\\n \\n fetch(\'/auth/sms-login\', {\\n method: \'POST\',\\n headers: {\\n \'Content-Type\': \'application/json\'\\n },\\n body: JSON.stringify(data)\\n })\\n .then(response => response.json())\\n .then(data => {\\n if (data.token) {\\n localStorage.setItem(\'token\', data.token);\\n window.location.href = \'/dashboard\';\\n } else {\\n alert(data.message);\\n }\\n })\\n .catch(error => {\\n console.error(\'Error:\', error);\\n alert(\'登录失败,请稍后再试\');\\n });\\n});\\n</script>\\n
\\n优点
\\n缺点
\\n邮箱验证码通过向用户注册的电子邮箱发送一次性验证码实现身份验证。这种方式与短信验证码类似,但使用电子邮件作为传递媒介,适合对成本敏感或不需要实时验证的场景。
\\n<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-mail</artifactId>\\n</dependency>\\n<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-thymeleaf</artifactId>\\n</dependency>\\n
\\n# application.properties\\nspring.mail.host=smtp.gmail.com\\nspring.mail.port=587\\nspring.mail.username=your-email@gmail.com\\nspring.mail.password=your-app-password\\nspring.mail.properties.mail.smtp.auth=true\\nspring.mail.properties.mail.smtp.starttls.enable=true\\nspring.mail.properties.mail.smtp.starttls.required=true\\n\\n# 自定义配置\\napp.email.from=noreply@yourapp.com\\napp.email.personal=Your App Name\\n
\\n@Service\\n@Slf4j\\npublic class EmailCaptchaService {\\n \\n @Autowired\\n private JavaMailSender mailSender;\\n \\n @Autowired\\n private RedisTemplate<String, String> redisTemplate;\\n \\n @Autowired\\n private TemplateEngine templateEngine;\\n \\n @Value(\\"${app.email.from}\\")\\n private String fromEmail;\\n \\n @Value(\\"${app.email.personal}\\")\\n private String emailPersonal;\\n \\n private static final long EMAIL_CAPTCHA_EXPIRATION = 600; // 10分钟过期\\n private static final String EMAIL_CAPTCHA_PREFIX = \\"EMAIL_CAPTCHA:\\";\\n \\n /**\\n * 发送邮箱验证码\\n * @param email 电子邮箱\\n * @return 是否发送成功\\n */\\n public boolean sendEmailCaptcha(String email) {\\n try {\\n // 生成6位随机验证码\\n String captcha = generateCaptcha(6);\\n \\n // 存储验证码\\n redisTemplate.opsForValue().set(\\n EMAIL_CAPTCHA_PREFIX + email, \\n captcha, \\n EMAIL_CAPTCHA_EXPIRATION, \\n TimeUnit.SECONDS);\\n \\n // 准备邮件内容\\n Context context = new Context();\\n context.setVariable(\\"captcha\\", captcha);\\n context.setVariable(\\"expirationMinutes\\", EMAIL_CAPTCHA_EXPIRATION / 60);\\n String emailContent = templateEngine.process(\\"email/captcha-template\\", context);\\n \\n // 创建MIME邮件\\n MimeMessage message = mailSender.createMimeMessage();\\n MimeMessageHelper helper = new MimeMessageHelper(message, true, \\"UTF-8\\");\\n \\n // 设置发件人、收件人、主题和内容\\n helper.setFrom(new InternetAddress(fromEmail, emailPersonal));\\n helper.setTo(email);\\n helper.setSubject(\\"登录验证码\\");\\n helper.setText(emailContent, true);\\n \\n // 发送邮件\\n mailSender.send(message);\\n log.info(\\"邮箱验证码已发送至: {}\\", email);\\n \\n return true;\\n } catch (Exception e) {\\n log.error(\\"发送邮箱验证码失败\\", e);\\n return false;\\n }\\n }\\n \\n /**\\n * 验证邮箱验证码\\n * @param email 电子邮箱\\n * @param captcha 用户输入的验证码\\n * @return 是否验证成功\\n */\\n public boolean validateEmailCaptcha(String email, String captcha) {\\n String key = EMAIL_CAPTCHA_PREFIX + email;\\n String storedCaptcha = redisTemplate.opsForValue().get(key);\\n \\n if (storedCaptcha != null && storedCaptcha.equals(captcha)) {\\n // 验证成功后删除验证码,防止重复使用\\n redisTemplate.delete(key);\\n return true;\\n }\\n \\n return false;\\n }\\n \\n /**\\n * 生成指定长度的随机数字验证码\\n */\\n private String generateCaptcha(int length) {\\n StringBuilder captcha = new StringBuilder();\\n Random random = new Random();\\n \\n for (int i = 0; i < length; i++) {\\n captcha.append(random.nextInt(10));\\n }\\n \\n return captcha.toString();\\n }\\n}\\n
\\n<!-- src/main/resources/templates/email/captcha-template.html --\x3e\\n<!DOCTYPE html>\\n<html xmlns:th=\\"http://www.thymeleaf.org\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <title>登录验证码</title>\\n <style>\\n body {\\n font-family: Arial, sans-serif;\\n line-height: 1.6;\\n color: #333;\\n }\\n .container {\\n max-width: 600px;\\n margin: 0 auto;\\n padding: 20px;\\n border: 1px solid #ddd;\\n border-radius: 5px;\\n }\\n .header {\\n text-align: center;\\n padding-bottom: 10px;\\n border-bottom: 1px solid #eee;\\n }\\n .content {\\n padding: 20px 0;\\n }\\n .code {\\n font-size: 24px;\\n font-weight: bold;\\n text-align: center;\\n padding: 10px;\\n margin: 20px 0;\\n background-color: #f5f5f5;\\n border-radius: 5px;\\n letter-spacing: 5px;\\n }\\n .footer {\\n font-size: 12px;\\n color: #777;\\n text-align: center;\\n padding-top: 10px;\\n border-top: 1px solid #eee;\\n }\\n </style>\\n</head>\\n<body>\\n <div class=\\"container\\">\\n <div class=\\"header\\">\\n <h2>登录验证码</h2>\\n </div>\\n <div class=\\"content\\">\\n <p>您好,</p>\\n <p>您正在进行登录操作,请使用以下验证码完成验证:</p>\\n <div class=\\"code\\" th:text=\\"${captcha}\\">123456</div>\\n <p>验证码有效期为 <span th:text=\\"${expirationMinutes}\\">10</span> 分钟,请及时使用。</p>\\n <p>如果这不是您的操作,请忽略此邮件。</p>\\n </div>\\n <div class=\\"footer\\">\\n <p>此邮件由系统自动发送,请勿回复。</p>\\n </div>\\n </div>\\n</body>\\n</html>\\n
\\n@RestController\\n@RequestMapping(\\"/captcha\\")\\npublic class EmailCaptchaController {\\n \\n @Autowired\\n private EmailCaptchaService emailCaptchaService;\\n \\n @PostMapping(\\"/email/send\\")\\n public ResponseEntity<?> sendEmailCaptcha(@RequestBody EmailRequest request) {\\n String email = request.getEmail();\\n \\n // 验证邮箱格式\\n if (!isValidEmail(email)) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"邮箱格式不正确\\"));\\n }\\n \\n // 发送邮箱验证码\\n boolean sent = emailCaptchaService.sendEmailCaptcha(email);\\n \\n if (sent) {\\n return ResponseEntity.ok(new ApiResponse(true, \\"验证码已发送,请查收邮件\\"));\\n } else {\\n return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\\n .body(new ApiResponse(false, \\"验证码发送失败,请稍后再试\\"));\\n }\\n }\\n \\n // 验证邮箱格式\\n private boolean isValidEmail(String email) {\\n String regex = \\"^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$\\";\\n return email != null && email.matches(regex);\\n }\\n}\\n
\\n@RestController\\n@RequestMapping(\\"/auth\\")\\npublic class EmailLoginController {\\n \\n @Autowired\\n private EmailCaptchaService emailCaptchaService;\\n \\n @Autowired\\n private UserService userService;\\n \\n @Autowired\\n private JwtTokenProvider jwtTokenProvider;\\n \\n @PostMapping(\\"/email-login\\")\\n public ResponseEntity<?> emailLogin(@RequestBody EmailLoginRequest request) {\\n String email = request.getEmail();\\n String captcha = request.getCaptcha();\\n \\n // 验证邮箱验证码\\n if (!emailCaptchaService.validateEmailCaptcha(email, captcha)) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"验证码错误或已过期\\"));\\n }\\n \\n // 查找或创建用户\\n User user = userService.findOrCreateByEmail(email);\\n \\n if (user != null) {\\n // 生成JWT令牌\\n String token = jwtTokenProvider.generateToken(user.getUsername());\\n return ResponseEntity.ok(new JwtAuthResponse(token));\\n } else {\\n return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\\n .body(new ApiResponse(false, \\"登录失败,请稍后再试\\"));\\n }\\n }\\n}\\n
\\n优点
\\n缺点
\\n滑动拼图验证码是一种更现代的验证方式,通过让用户拖动滑块完成拼图来验证人机交互。这种验证码在用户体验和安全性之间取得了很好的平衡,既能有效防止自动化攻击,又不会像传统图形验证码那样影响用户体验。
\\n<dependency>\\n <groupId>com.anji-plus</groupId>\\n <artifactId>captcha-spring-boot-starter</artifactId>\\n <version>1.4.0</version>\\n</dependency>\\n
\\naj:\\n captcha:\\n cache-type: local\\n expires-in: 300\\n req-frequency-limit-count: 50\\n cache-number: 1000\\n jigsaw: classpath:images/jigsaw\\n pic-click: classpath:images/pic-click\\n
\\n\\n@RestController\\n@RequestMapping(\\"/captcha\\")\\npublic class JigsawCaptchaController {\\n \\n @Autowired\\n private CaptchaService captchaService;\\n \\n @RequestMapping(value = \\"/get\\", method = {RequestMethod.GET, RequestMethod.POST})\\n public ResponseModel get(@RequestParam(value = \\"type\\", defaultValue = \\"slide\\") String captchaType) {\\n CaptchaVO captchaVO = new CaptchaVO();\\n captchaVO.setCaptchaType(captchaType);\\n return captchaService.get(captchaVO);\\n }\\n \\n @PostMapping(\\"/check\\")\\n public ResponseModel check(@RequestBody CaptchaVO captchaVO) {\\n return captchaService.check(captchaVO);\\n }\\n}\\n
\\n\\n@RestController\\n@RequestMapping(\\"/auth\\")\\npublic class JigsawLoginController {\\n \\n @Autowired\\n private CaptchaService captchaService;\\n\\n @PostMapping(\\"/jigsaw-login\\")\\n public ResponseEntity<?> jigsawLogin(@RequestBody JigsawLoginRequest request) {\\n // 验证滑动验证码\\n CaptchaVO captchaVO = new CaptchaVO();\\n captchaVO.setCaptchaVerification(request.getCaptchaVerification());\\n \\n ResponseModel response = captchaService.verification(captchaVO);\\n \\n if (!response.isSuccess()) {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"验证码校验失败\\"));\\n }\\n \\n // TODO 模拟验证用户名密码\\n boolean authenticated = request.getPassword().equals(\\"admin\\");\\n \\n if (authenticated) {\\n // TODO 模拟生成令牌\\n String token = IdUtil.simpleUUID();\\n return ResponseEntity.ok(new JwtAuthResponse(token));\\n } else {\\n return ResponseEntity.badRequest().body(new ApiResponse(false, \\"用户名或密码错误\\"));\\n }\\n }\\n}\\n
\\n@SpringBootApplication\\npublic class Main {\\n\\n public static void main(String[] args) {\\n SpringApplication.run(Main.class, args);\\n }\\n\\n @Bean\\n public CaptchaService captchaService(){\\n BlockPuzzleCaptchaServiceImpl clickWordCaptchaService = new BlockPuzzleCaptchaServiceImpl();\\n Properties properties = new Properties();\\n properties.setProperty(Const.CAPTCHA_FONT_TYPE,\\"WenQuanZhengHei.ttf\\");\\n clickWordCaptchaService.init(properties);\\n return clickWordCaptchaService;\\n }\\n\\n}\\n
\\n注:依赖的本地js文件可从 https://github.com/anji-plus/captcha/tree/master/view/html 获取\\n
\\n<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <title>滑动验证码登录</title>\\n <link rel=\\"stylesheet\\" href=\\"https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css\\">\\n <link rel=\\"stylesheet\\" href=\\"https://cdn.jsdelivr.net/npm/aj-captcha@1.3.0/dist/captcha.min.css\\">\\n <meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no\\"/>\\n <link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"css/verify.css\\">\\n <style>\\n\\n </style>\\n</head>\\n<body>\\n<div class=\\"container mt-5\\">\\n <div class=\\"row justify-content-center\\">\\n <div class=\\"col-md-6\\">\\n <div class=\\"card\\">\\n <div class=\\"card-header\\">用户登录</div>\\n <div class=\\"card-body\\">\\n <form id=\\"loginForm\\">\\n <div class=\\"form-group\\">\\n <label for=\\"username\\">用户名</label>\\n <input type=\\"text\\" class=\\"form-control\\" id=\\"username\\" name=\\"username\\" required>\\n </div>\\n <div class=\\"form-group\\">\\n <label for=\\"password\\">密码</label>\\n <input type=\\"password\\" class=\\"form-control\\" id=\\"password\\" name=\\"password\\" required>\\n </div>\\n <div class=\\"form-group\\">\\n <div id=\\"captcha\\"></div>\\n <input type=\\"hidden\\" id=\\"captchaVerification\\" name=\\"captchaVerification\\">\\n </div>\\n\\n <div id=\\"slidePanel\\" ></div>\\n\\n <button type=\\"submit\\" class=\\"btn btn-primary btn-block\\">登录</button>\\n </form>\\n </div>\\n </div>\\n </div>\\n </div>\\n</div>\\n<script src=\\"https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js\\"></script>\\n<script>\\n (function () {\\n if (!window.Promise) {\\n document.writeln(\'<script src=\\"https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.min.js\\"><\' + \'/\' + \'script>\');\\n }\\n })();\\n</script>\\n<script src=\\"./js/crypto-js.js\\"></script>\\n<script src=\\"./js/ase.js\\"></script>\\n<script src=\\"./js/verify.js\\" ></script>\\n<script>\\n let captchaVerification;\\n // 初始化验证码 嵌入式\\n $(\'#slidePanel\').slideVerify({\\n baseUrl:\'http://localhost:8080\', // 服务器请求地址;\\n mode:\'fixed\',\\n imgSize : { //图片的大小对象\\n width: \'400px\',\\n height: \'200px\',\\n },\\n barSize:{\\n width: \'400px\',\\n height: \'40px\',\\n },\\n ready : function() { //加载完毕的回调\\n },\\n success : function(params) { //成功的回调\\n // 返回的二次验证参数 合并到验证通过之后的逻辑 参数中回传服务器\\n captchaVerification = params.captchaVerification;\\n },\\n error : function() { //失败的回调\\n }\\n });\\n\\n // 表单提交处理\\n document.getElementById(\'loginForm\').addEventListener(\'submit\', function(e) {\\n e.preventDefault();\\n\\n if (!captchaVerification) {\\n alert(\'请先完成滑动验证\');\\n return;\\n }\\n\\n var data = {\\n username: document.getElementById(\'username\').value,\\n password: document.getElementById(\'password\').value,\\n captchaVerification: captchaVerification\\n };\\n\\n fetch(\'/auth/jigsaw-login\', {\\n method: \'POST\',\\n headers: {\\n \'Content-Type\': \'application/json\'\\n },\\n body: JSON.stringify(data)\\n })\\n .then(response => response.json())\\n .then(data => {\\n if (data.token) {\\n localStorage.setItem(\'token\', data.token);\\n alert(\'login success\');\\n } else {\\n alert(data.message);\\n }\\n })\\n .catch(error => {\\n console.error(\'Error:\', error);\\n });\\n });\\n</script>\\n</body>\\n</html>\\n
\\n优点
\\n缺点
\\n特性/方案 | 图形验证码 | 短信验证码 | 邮箱验证码 | 滑动拼图验证码 |
---|---|---|---|---|
安全性 | 中 | 高 | 中高 | 高 |
实现复杂度 | 低 | 中 | 中 | 高 |
适用场景 | 简单应用 | 高安全性需求 | 注册、找回密码 | 现代Web应用 |
防机器人效果 | 一般 | 优秀 | 良好 | 优秀 |
是否需要第三方 | 否 | 是 | 否 | 否 |
在实际项目中,可以根据应用特点、用户需求和安全要求选择合适的验证码方案,甚至可以组合多种方案,在不同场景下使用不同的验证方式,既保障系统安全,又提供良好的用户体验。
\\n随着AI技术的发展,验证码技术也在不断演进。对于重要系统,建议定期评估和更新验证码方案,以应对新型的自动化攻击手段。
","description":"在当今互联网安全形势日益严峻的环境下,验证码已成为保护用户账户安全、防止暴力破解和自动化攻击的重要手段。尤其在登录系统中,合理使用验证码不仅能有效阻止机器人批量尝试账号密码,还能降低账户被盗风险,提升系统安全性。 本文将详细介绍在SpringBoot应用中实现四种登录验证码的技术方案,包括图形验证码、短信验证码、邮箱验证码和滑动拼图验证码。\\n\\n方案一:基于Kaptcha的图形验证码\\n原理介绍\\n\\n图形验证码是最传统且应用最广泛的验证码类型,原理是在服务端生成随机字符串并渲染成图片,用户需要识别图片中的字符并输入。图形验证码实现简单,对用户体验影响较小…","guid":"https://juejin.cn/post/7498286956343066663","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-28T23:26:22.478Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbfec8cc8fe94c16b8463a42d6d388fd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6aOO6LGh5Y2X:q75.awebp?rk3s=f64ab15b&x-expires=1746487582&x-signature=t2JSL6LumnD2fUTjyjhly%2Bpeaqo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"Spring AI 实操案例:搭建智能客服系统(含RAG增强版)","url":"https://juejin.cn/post/7498291145987129370","content":"某电商平台需要一个智能客服系统,要求:
\\n组件 | 选型依据 |
---|---|
AI框架 | Spring AI 0.8.1(Java生态友好,支持多模型切换) |
大模型 | DeepSeek(兼容OpenAI API,成本低)或Ollama本地模型(数据隐私) |
向量数据库 | Redis(内存级检索,适合实时场景) |
辅助工具 | FFmpeg(音频预处理)、Lombok(简化代码) |
spring: \\n ai: \\n openai: \\n api-key: sk-xxx # DeepSeek API密钥 \\n base-url: https://api深基流动.com/v1 # 深基流动API地址 \\n chat: \\n model: deepseek-chat-v3 # 指定模型 \\n temperature: 0.7 # 控制生成随机性 \\nredlock: \\n redis: \\n host: localhost \\n port: 6379 \\n
\\n@Service \\npublic class AiCustomerService { \\n private final ChatClient chatClient; \\n public AiCustomerService ChatClient chatClient) { \\n this.chatClient = chatClient; \\n } \\n // 普通对话接口 \\n public String handleQuery(String userInput) { \\n String prompt = \\"\\"\\" \\n 你是一个电商客服专家,请用中文回答用户问题。 \\n 系统状态: \\n - 用户最近订单:OD123456(已发货) \\n - 产品库版本:v2.1 \\n 用户问题:%s \\n \\"\\"\\" \\n .formatted(userInput); \\n return chatClient.call(prompt).getResult().getOutput().getContent(); \\n } \\n} \\n
\\n@RestController \\n@RequestMapping(\\"/rag\\") \\npublic classragController { \\n @Autowired \\n private VectorStore vectorStore; \\n @PostMapping(\\"/ask\\") \\n public String askWithRAG(@RequestBody String question) { \\n // 1. 检索相关文档 \\n List文档> docs = vectorStore simSearch(question, 3); \\n // 2. 构建上下文 \\n Prompt prompt = new Prompt( \\n Arrays.asList( \\n new SystemMessage(\\"你是一个客服,需根据以下资料回答问题:\\" + docs), \\n new UserMessage(question) \\n ) \\n ); \\n // 3. 生成回答 \\n return chatClient.call(prompt). Result(). Content(); \\n } \\n} \\n
\\n@GetMapping(value = \\"/stream\\", produces = MediaType.APPLICATION Event_STREAM_VALUE) \\npublic Flux streamChat(@RequestParam String prompt) { \\n return chatClient.stream() \\n .user(prompt) \\n .system(\\"请逐步思考并回答:\\") \\n .map(response -> \\"data: \\" + response + \\"\\\\n\\\\n\\"); \\n} \\n
\\n@Bean \\npublic VectorStore vectorStore(EmbeddingModel embeddingModel) { \\n return SimpleVectorStore.builder(embeddingModel).build(); \\n} \\n
\\n使用JMeter模拟1000并发请求,优化点:
\\n// 调整生成参数 \\nChatOptions options = new ChatOptions() \\n .withTemperature(0.3) // 降低随机性 \\n .withMaxTokens(500); // 限制生成长度 \\n
\\n彩蛋:尝试在Prompt中添加\\"假设你是李佳琦\\"
,AI会瞬间变成带货模式!
\\n(注:实际效果因模型而异,建议先测试再商用)
\\nPhoto by Christopher Gower on Unsplash
在现代微服务架构设计中,模块化和可插拔的设计模式越来越受到开发者的青睐。go-doudou作为一款国产的Go语言微服务框架,提供了优秀的插件机制和模块化架构支持。本文将通过一个基于RAG(检索增强生成)的实际项目来详细讲解go-doudou的插件机制和模块化微内核架构的实现方式。
\\n微内核架构(MicroKernel Architecture)也称为插件架构(Plugin Architecture),是一种将核心系统功能与扩展功能分离的设计模式。在这种架构中:
\\n这种架构的优势在于:
\\ngo-doudou框架通过实现ServicePlugin
接口来支持插件机制。每个服务模块作为一个插件被注册到主应用中,实现了模块与核心系统的解耦。
让我们先看一下该项目中main/cmd/main.go
的核心代码:
package main\\n\\nimport (\\n\\"go-doudou-rag/toolkit/auth\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/grpcx\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/plugin\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/rest\\"\\n\\"github.com/unionj-cloud/toolkit/pipeconn\\"\\n\\"github.com/unionj-cloud/toolkit/zlogger\\"\\n\\"google.golang.org/grpc\\"\\n\\n_ \\"go-doudou-rag/module-auth/plugin\\"\\n_ \\"go-doudou-rag/module-chat/plugin\\"\\n_ \\"go-doudou-rag/module-knowledge/plugin\\"\\n)\\n\\nfunc main() {\\nsrv := rest.NewRestServer()\\nsrv.Use(auth.Jwt)\\n\\ngrpcServer := grpcx.NewGrpcServer(\\n// GRPC配置...\\n)\\nlis, dialCtx := pipeconn.NewPipeListener()\\nplugins := plugin.GetServicePlugins()\\nfor _, key := range plugins.Keys() {\\nvalue, _ := plugins.Get(key)\\nvalue.Initialize(srv, grpcServer, dialCtx)\\n}\\ndefer func() {\\nif r := recover(); r != nil {\\nzlogger.Info().Msgf(\\"Recovered. Error: %v\\\\n\\", r)\\n}\\nfor _, key := range plugins.Keys() {\\nvalue, _ := plugins.Get(key)\\nvalue.Close()\\n}\\n}()\\ngo func() {\\ngrpcServer.RunWithPipe(lis)\\n}()\\nsrv.AddRoutes(rest.DocRoutes(\\"\\"))\\nsrv.Run()\\n}\\n
\\n这段代码展示了go-doudou的微内核架构实现:
\\n_ \\"go-doudou-rag/module-xxx/plugin\\"
)各模块的plugin包plugin.GetServicePlugins()
Initialize
方法,将REST服务器和gRPC服务器传入Close
方法每个模块通过实现ServicePlugin
接口来成为一个插件。以module-auth
模块为例:
package plugin\\n\\nimport (\\n\\"github.com/glebarez/sqlite\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/grpcx\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/plugin\\"\\n\\"github.com/unionj-cloud/go-doudou/v2/framework/rest\\"\\n\\"github.com/unionj-cloud/toolkit/pipeconn\\"\\n\\"github.com/unionj-cloud/toolkit/stringutils\\"\\nservice \\"go-doudou-rag/module-auth\\"\\n\\"go-doudou-rag/module-auth/config\\"\\n\\"go-doudou-rag/module-auth/internal/dao\\"\\n\\"go-doudou-rag/module-auth/internal/model\\"\\n\\"go-doudou-rag/module-auth/transport/httpsrv\\"\\n\\"google.golang.org/grpc\\"\\n\\"gorm.io/gorm\\"\\n\\"os\\"\\n)\\n\\nvar _ plugin.ServicePlugin = (*ModuleAuthPlugin)(nil)\\n\\ntype ModuleAuthPlugin struct {\\ngrpcConns []*grpc.ClientConn\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) Close() {\\nfor _, item := range receiver.grpcConns {\\nitem.Close()\\n}\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) GoDoudouServicePlugin() {\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) GetName() string {\\nname := os.Getenv(\\"GDD_SERVICE_NAME\\")\\nif stringutils.IsEmpty(name) {\\nname = \\"cloud.unionj.ModuleAuth\\"\\n}\\nreturn name\\n}\\n\\nfunc (receiver *ModuleAuthPlugin) Initialize(restServer *rest.RestServer, grpcServer *grpcx.GrpcServer, dialCtx pipeconn.DialContextFunc) {\\nconf := config.LoadFromEnv()\\n\\ndb, err := gorm.Open(sqlite.Open(conf.Db.Dsn), &gorm.Config{})\\nif err != nil {\\npanic(\\"failed to connect database\\")\\n}\\n\\nif err = db.AutoMigrate(&model.User{}); err != nil {\\npanic(err)\\n}\\n\\ndao.Use(db)\\ndao.Init()\\n\\nsvc := service.NewModuleAuth(conf)\\nroutes := httpsrv.Routes(httpsrv.NewModuleAuthHandler(svc))\\nrestServer.GroupRoutes(\\"/moduleauth\\", routes)\\nrestServer.GroupRoutes(\\"/moduleauth\\", rest.DocRoutes(service.Oas))\\n}\\n\\nfunc init() {\\nplugin.RegisterServicePlugin(&ModuleAuthPlugin{})\\n}\\n
\\n这个插件实现了关键的接口方法:
\\nGoDoudouServicePlugin()
: 标识接口方法GetName()
: 返回插件名称Initialize()
: 初始化插件,注册HTTP路由Close()
: 释放资源特别注意init()
函数中的plugin.RegisterServicePlugin()
,它将插件注册到全局插件注册表中,使得主应用能够发现并加载这个插件。
在微内核架构中,模块间通信是关键挑战之一。go-doudou提供了多种通信方式:
\\nsamber/do
库实现依赖注入在module-chat
的实现中,我们可以看到如何调用module-knowledge
的服务:
func (receiver *ModuleChatImpl) Chat(ctx context.Context, req dto.ChatRequest) (err error) {\\n// ...省略部分代码...\\n\\nknowService := do.MustInvoke[know.ModuleKnowledge](nil)\\nqueryResults, err := knowService.GetQuery(ctx, kdto.QueryReq{\\nText: req.Prompt,\\nTop: 10,\\n})\\n\\n// ...省略部分代码...\\n}\\n
\\nmodule-knowledge
通过依赖注入注册服务:
func init() {\\nplugin.RegisterServicePlugin(&ModuleKnowledgePlugin{})\\n\\ndo.Provide[service.ModuleKnowledge](nil, func(injector *do.Injector) (service.ModuleKnowledge, error) {\\nconf := config.LoadFromEnv()\\n\\ndb, err := gorm.Open(sqlite.Open(conf.Db.Dsn), &gorm.Config{})\\nif err != nil {\\npanic(\\"failed to connect database\\")\\n}\\n\\nif err = db.AutoMigrate(&model.File{}); err != nil {\\npanic(err)\\n}\\n\\ndao.Use(db)\\n\\nsvc := service.NewModuleKnowledge(conf)\\nreturn svc, nil\\n})\\n}\\n
\\n该项目展示了模块化设计的最佳实践,每个模块有清晰的职责划分:
\\n每个模块都遵循相似的内部结构:
\\nmodule-xxx/\\n ├── cmd/ # 独立运行入口\\n ├── config/ # 模块配置\\n ├── dto/ # 数据传输对象\\n ├── internal/ # 内部实现\\n │ ├── dao/ # 数据访问\\n │ └── model/ # 数据模型\\n ├── plugin/ # 插件实现\\n ├── transport/ # 传输层\\n │ └── httpsrv/ # HTTP服务\\n ├── svc.go # 服务接口定义\\n └── svcimpl.go # 服务实现\\n
\\n这种结构保证了:
\\n这个项目实现了一个基于RAG(检索增强生成)的聊天系统,整体流程如下:
\\nmodule-auth
进行认证module-knowledge
上传知识文档module-chat
提问,系统会:\\nmodule-knowledge
检索相关内容例如,module-chat
中的核心处理逻辑:
func (receiver *ModuleChatImpl) Chat(ctx context.Context, req dto.ChatRequest) (err error) {\\n// ...设置响应头...\\n\\n// 创建LLM客户端\\nllm, err := openai.New(\\nopenai.WithBaseURL(receiver.conf.Openai.BaseUrl),\\nopenai.WithToken(lo.Ternary(stringutils.IsNotEmpty(receiver.conf.Openai.Token), \\nreceiver.conf.Openai.Token, os.Getenv(\\"OPENAI_API_KEY\\"))),\\nopenai.WithEmbeddingModel(receiver.conf.Openai.EmbeddingModel),\\nopenai.WithModel(receiver.conf.Openai.Model),\\n)\\n\\n// 从知识库检索相关内容\\nknowService := do.MustInvoke[know.ModuleKnowledge](nil)\\nqueryResults, err := knowService.GetQuery(ctx, kdto.QueryReq{\\nText: req.Prompt,\\nTop: 10,\\n})\\n\\n// 过滤相关性高的结果\\nqueryResults = lo.Filter(queryResults, func(item kdto.QueryResult, index int) bool {\\nreturn cast.ToFloat64(item.Similarity) >= 0.5\\n})\\n\\n// 构建提示词\\nprompt := \\"请结合下面给出的上下文信息回答问题...\\"\\n\\n// 调用LLM生成回答并流式返回\\ncontent := []llms.MessageContent{\\nllms.TextParts(llms.ChatMessageTypeSystem, \\"You are a senior public policy researcher.\\"),\\nllms.TextParts(llms.ChatMessageTypeHuman, prompt),\\n}\\n\\n_, err = llm.GenerateContent(ctx, content,\\nllms.WithMaxTokens(4096),\\nllms.WithTemperature(0.2),\\nllms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {\\nchunkResp := dto.ChatResponse{\\nContent: string(chunk),\\nRequestID: requestID,\\nType: \\"content\\",\\n}\\nreturn writeSSEMessage(w, flusher, chunkResp)\\n}))\\n\\nreturn\\n}\\n
\\n通过以下步骤启动此基于go-doudou插件架构的RAG系统:
\\n克隆代码库并进入项目目录
\\ngit clone https://github.com/your-repo/go-doudou-rag.git\\ncd go-doudou-rag\\n
\\n安装依赖
\\ngo mod tidy\\n
\\n启动主应用
\\ncd main/cmd\\ngo run main.go\\n
\\n系统启动后,所有模块(auth、chat、knowledge)会作为插件被加载,各自的API端点也会被注册到主应用中。
\\n下面展示如何使用curl命令向聊天服务发送请求,实现基于知识库的问答:
\\n# 登录\\ncurl --location \'http://localhost:6060/moduleauth/login\' \\\\\\n--header \'Content-Type: application/json\' \\\\\\n--data \'{\\n \\"username\\": \\"admin\\",\\n \\"password\\": \\"admin\\"\\n}\'\\n\\n# 上传pdf文档\\ncurl --location \'http://localhost:6060/moduleknowledge/upload\' \\\\\\n--header \'Authorization: Bearer <从登录接口获取的token>\' \\\\\\n--form \'file=@\\"/Users/wubin1989/Downloads/杭州市人民政府印发关于进一步推动经济高质量发展若干政策的通知.pdf\\"\'\\n\\n# 聊天\\ncurl -w \'\\\\n\' -N -X POST \'http://localhost:6060/modulechat/chat\' \\\\\\n--header \'Content-Type: application/json\' \\\\\\n--header \'Authorization: Bearer <从登录接口获取的token>\' \\\\\\n--data \'{\\n \\"prompt\\": \\"最近杭州出台了什么经济相关的政策?\\"\\n}\'\\n
\\n系统响应示例:
\\n首先,系统会返回构建的提示词(包含从知识库检索到的上下文信息):
\\n请结合下面给出的上下文信息回答问题,答案必须分条阐述,力求条理清晰,如果不知道可以回答不知道,但不要编造答案:\\n1. — 1 —\\n杭州市人民政府文件\\n杭政函〔2024〕16 号\\n杭州市人民政府印发关于进一步推动\\n经济高质量发展若干政策的通知\\n各区、县(市)人民政府,市政府各部门、各直属单位:\\n现将《关于进一步推动经济高质量发展的若干政策》印发给\\n你们,请结合实际认真组织实施。\\n杭州市人民政府\\n2024 年 2 月 18 日\\n(此件公开发布)\\nZJAC00-2024-0001\\n2. — 2 —\\n关于进一步推动经济高质量发展的若干政策\\n... [省略部分上下文] ...\\n
\\n然后,系统会基于检索到的上下文信息,生成结构化的回答:
\\n根据提供的信息,杭州市人民政府最近出台了一系列推动经济高质量发展的政策,具体包括以下几个方面:\\n\\n1. **扩大有效投资政策**:推动《杭州市政府投资项目管理条例》出台,落实省扩大有效投资\\"千项万亿\\"工程,2024年计划完成投资800亿元以上,带动固定资产投资增长3%。\\n\\n2. **激发消费潜能**:落实新能源汽车减免购置税等政策,全年新增公共领域充电设施3000个,组织促消费活动500场以上,举办餐饮促消费活动50场以上。\\n\\n3. **支持企业高质量发展**:完善优质企业梯度培育机制,对首次上规纳统的工业企业给予最高20万元的一次性奖励,上规后连续3年保持在规的再给予不超过30万元的一次性奖励。\\n\\n4. **稳定外贸发展**:全年组织不少于150个外贸团组,参加100个以上境外展会,3000家次企业赴境外拓市场,提高企业短期出口信用保险的投保费资助比例上限至60%(制造业企业上限提高至65%)。\\n\\n5. **打造国际会展之都**:高质量办好第三届全球数字贸易博览会,实现五个翻番,2024年招引30场展览落户杭州国博中心和大会展中心。\\n\\n6. **支持数字贸易发展**:鼓励企业开展数据出境安全评估,参与制定数字贸易领域各类标准,持续推动服务贸易创新发展。\\n\\n7. **发挥电商优势**:推进杭州市新电商高质量发展,全年累计打造电商直播式\\"共富工坊\\"不少于200家,深化杭州跨境电商综试区建设。\\n\\n8. **强化财政资金支持**:市财政2024年预算安排6亿元,用于支持扩大内需促消费领域,支持外贸发展、新电商高质量发展、跨境电商发展、新开国际航线等。\\n
\\nRAG系统的一个重要特性是仅回答基于知识库中存在的信息,当用户提问的内容与知识库中的文档相关性不高或完全不相关时,系统会明确告知用户无法回答,而不是生成可能不准确的信息。以下是一个示例:
\\ncurl -w \'\\\\n\' -N -X POST \'http://localhost:6060/modulechat/chat\' \\\\\\n--header \'Content-Type: application/json\' \\\\\\n--header \'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDU4OTM4MTMsInVzZXJuYW1lIjoiYWRtaW4ifQ.EjxDfrMMHmOCvt557H8rd5sn9zX-uYOytw4OKH-jLJ8\' \\\\\\n--data \'{\\n \\"prompt\\": \\"Java的最新版本是哪个?\\" \\n}\'\\n
\\n系统响应:
\\n非常抱歉,未能检索到相关信息,无法回答\\n
\\n在代码实现中,这个机制通过以下方式实现:
\\n// 从module-chat/svcimpl.go中的实现\\nqueryResults = lo.Filter(queryResults, func(item kdto.QueryResult, index int) bool {\\n return cast.ToFloat64(item.Similarity) >= 0.5\\n})\\n\\nif len(queryResults) == 0 {\\n zlogger.Error().Msgf(\\"Knowledge not found, requestId: %s\\", requestID)\\n chunk := dto.ChatResponse{\\n Content: \\"非常抱歉,未能检索到相关信息,无法回答\\",\\n RequestID: requestID,\\n Type: \\"error\\",\\n }\\n writeSSEMessage(w, flusher, chunk)\\n return\\n}\\n
\\n这个设计确保了系统只回答它有知识基础的问题,提高了回答的可靠性和准确性,避免了生成虚假信息的风险。它是负责任的AI应用设计的一个重要方面,尤其在需要高度准确性的领域如政策咨询、法律建议等方面尤为重要。
\\n这个示例充分展示了插件架构的强大之处:
\\nmodule-auth
处理认证,module-knowledge
负责知识检索,module-chat
集成大语言模型生成回答本文通过一个实际的RAG聊天系统案例,详细介绍了go-doudou框架中的插件机制与模块化可插拔微内核架构。我们看到,这种架构模式不仅提供了良好的模块化和可扩展性,还使得系统各部分能够松耦合地协同工作,大大提高了开发效率和系统可维护性。
\\ngo-doudou框架的插件机制通过ServicePlugin
接口和依赖注入系统,为开发者提供了一种简洁而强大的方式来构建模块化应用。这种方式特别适合于团队协作开发复杂系统,每个团队可以专注于自己的领域模块,而无需过多关注其他模块的实现细节。
然而,理解概念和原理只是第一步,如何从零开始实际构建这样的系统才是开发者最关心的问题。在下一篇文章《go-doudou + langchaingo 微内核架构RAG大模型知识库实战(二)》中,我们将提供一个详细的实战指南,带领读者一步步从零开始搭建一个完整的go-doudou微内核架构应用。我们将通过具体的命令和代码示例,展示如何使用go-doudou CLI工具创建工作空间、定义服务接口、实现插件、配置模块间通信等全流程操作,帮助开发者快速掌握这一强大架构模式的实际应用方法。
\\n最近刷论坛、看评论,老是能看到有人对 Python 程序员冷嘲热讽,说我们是“调库侠”、“import 工程师”、“copy stackoverflow 的猴子”。一开始我还真有点儿内伤,但后来我细想了下:这事儿……真的可耻吗?不!这事儿,反而是 Python 程序员的“智慧写照”!
\\n我们先来直白点说:重复造轮子才是浪费时间!
\\n你看人家造了个牛X的轮子——比如 requests
处理 HTTP、pandas
玩数据、FastAPI
搞接口,写得又快又稳又高效,那我为啥非得硬刚一个低配的?
你说我是“调库侠”?不好意思,我这是 拥抱生态。Python 生态牛到什么程度?你想到的基本都有库,你没想到的,GitHub 上也十有八九有人提前帮你写好了。
\\n会用工具的人不叫懒,叫高效,叫懂事,叫有工程思维。
\\n不是谁都能“调”得明明白白的。调库,不只是 import x
,还得:
说白了,调库侠也是技术力的一种体现,高阶“调库侠”,比你手搓一大堆 bug 代码要靠谱得多。
\\nPython 从一开始就不装——它不是那种“我要啥都原生搞定”的语言,它就是告诉你:“我给你个灵活的壳子,你想怎么玩,拉上我这堆兄弟库就能飞。”
\\n正是这种思路,才让 Python 在数据科学、AI、Web 开发、自动化这些领域火得一塌糊涂。你见过哪个搞深度学习的大佬,不用 PyTorch
或 TensorFlow
的吗?他们也是“调库侠”啊!但没人笑他们对吧?为什么?因为他们调的是能跑出成果的、能解决问题的库。
说到底,语言只是工具,解决问题才是王道。
\\n你用 Python,能快速写出稳定可用的程序;\\n你调库,能比别人少踩十个坑、多干十倍活;\\n你 stackoverflow 找段代码能解决 bug,那说明你善用资源;
\\n那么,不才是真正的“代码高手”吗?
\\n所以啊,被人说“调库侠”?笑笑就行了。
\\n记住:调得好,那叫高手。调得快,那叫效率。调得妙,那是艺术。
别被键盘侠带节奏,我们写 Python,不是为了耍帅,是为了让代码跑起来,让项目落地,让生活更爽。会用工具不是罪,闭门造车才是真可怕。
\\n所以,继续 import 吧,兄弟姐妹们!
","description":"写Python被嘲笑是“调库侠”,真的很可耻吗? 最近刷论坛、看评论,老是能看到有人对 Python 程序员冷嘲热讽,说我们是“调库侠”、“import 工程师”、“copy stackoverflow 的猴子”。一开始我还真有点儿内伤,但后来我细想了下:这事儿……真的可耻吗?不!这事儿,反而是 Python 程序员的“智慧写照”!\\n\\n一、调库不是偷懒,是认清了世界运行的方式\\n\\n我们先来直白点说:重复造轮子才是浪费时间!\\n 你看人家造了个牛X的轮子——比如 requests 处理 HTTP、pandas 玩数据、FastAPI 搞接口,写得又快又稳又高效…","guid":"https://juejin.cn/post/7497858962395004954","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-28T01:02:29.045Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da97f188fe0c4f6880d4148435caebd2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746406948&x-signature=IBopg57o1IC2CPhwZ2Q%2FDMqtDEQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"你真的会用 return 吗?—— 11个值得借鉴的 return 写法","url":"https://juejin.cn/post/7497804336568582183","content":"return
这个关键字,相信大家每天都在用。
它就像一把锤子,敲打着我们代码里的每一个出口。
\\n但扪心自问,我们真的把这把锤子用好了吗?
\\n今天,不想聊什么高深莫测的设计模式,也不敢妄称“最佳实践”。
\\n只想结合自己这些年在项目摸爬滚打中踩过的一些坑、积累的一点心得,和大家分享一些关于 return
的、或许能让我们的代码更规范、更优雅、更易读的写法。
权当抛砖引玉,希望能引发大家的一些思考。
\\n耐心看完,你一定有所收获。
\\n这是最常见也是非常推荐的一种模式。
\\n核心思想是:在方法开头处理掉所有“异常”或“特殊”情况,让方法的主体部分专注于核心逻辑。
\\n反面教材 ❌:
\\npublic void processData(Data data) {\\n if (data != null) {\\n if (data.isValid()) {\\n if (checkPermission(data)) {\\n // 核心逻辑开始...\\n System.out.println(\\"处理数据:\\" + data.getContent());\\n // ...\\n // 大量核心代码嵌套在这里\\n // ...\\n System.out.println(\\"处理完成\\");\\n } else {\\n System.out.println(\\"权限不足\\");\\n }\\n } else {\\n System.out.println(\\"数据无效\\");\\n }\\n } else {\\n System.out.println(\\"数据为null\\");\\n }\\n}\\n
\\n很难评,嵌套过深,核心逻辑被包裹在层层 if-else
中,可读性太差。
推荐写法 ✅:
\\npublic void processData(Data data) {\\n if (data == null) {\\n System.out.println(\\"数据为null\\");\\n return; // 提前返回\\n }\\n if (!data.isValid()) {\\n System.out.println(\\"数据无效\\");\\n return; // 提前返回\\n }\\n if (!checkPermission(data)) {\\n System.out.println(\\"权限不足\\");\\n return; // 提前返回\\n }\\n\\n // --- 核心逻辑开始 ---\\n // 经过前面的卫语句检查,这里的data一定是有效且有权限的\\n System.out.println(\\"处理数据:\\" + data.getContent());\\n // ...\\n // 核心代码不再嵌套,非常清晰\\n // ...\\n System.out.println(\\"处理完成\\");\\n}\\n
\\n通过提前 return
,避免了深层嵌套,让主要的处理流程更加顺畅,代码逻辑一目了然。
配得上“优雅”二字。
\\nreturn
后的 else
块当 if
分支中包含 return
语句时,其后的代码天然就是 else
的逻辑,无需显式写出 else
。
反面教材 ❌:
\\npublic String getStatus(int code) {\\n if (code == 0) {\\n return \\"Success\\";\\n } else {\\n // 其他逻辑\\n return \\"Error: \\" + getErrorMessage(code);\\n }\\n}\\n
\\n虽然没错,但 else
显得有些多余,未免画蛇添足。
推荐写法 ✅:
\\npublic String getStatus(int code) {\\n if (code == 0) {\\n return \\"Success\\";\\n }\\n // 如果 code == 0,上面的 return 已经退出方法了\\n // 能执行到这里,说明 code != 0,天然就是 else 的逻辑\\n return \\"Error: \\" + getErrorMessage(code);\\n}\\n
\\n代码更简洁,减少了一层不必要的缩进。
\\n直接返回布尔表达式的结果,而不是使用 if-else
返回 true
或 false
。
反面教材 ❌:
\\npublic boolean isEligible(User user) {\\n if (user.getAge() >= 18 && user.isActive()) {\\n return true;\\n } else {\\n return false;\\n }\\n}\\n
\\n点评:非常啰嗦。
\\n推荐写法 ✅:
\\npublic boolean isEligible(User user) {\\n return user.getAge() >= 18 && user.isActive();\\n}\\n
\\n一行搞定,清晰明了。
\\n如果一个变量仅仅是为了存储即将 return
的值,可以考虑直接 return
表达式的结果。
反面教材 ❌:
\\npublic int calculateSum(int a, int b) {\\n int sum = a + b;\\n return sum;\\n}\\n\\npublic String getUserGreeting(User user) {\\n String greeting = \\"Hello, \\" + user.getName();\\n return greeting;\\n}\\n
\\nsum
和 greeting
变量并非必需。
推荐写法 ✅:
\\npublic int calculateSum(int a, int b) {\\n return a + b;\\n}\\n\\npublic String getUserGreeting(User user) {\\n // 如果 user.getName() 调用成本高或需要复用,临时变量可能有意义\\n // 但在这个简单场景下,直接返回更简洁\\n return \\"Hello, \\" + user.getName();\\n}\\n
\\n更直接。
\\n但注意,如果表达式复杂或计算结果需要复用,还是考虑使用临时变量,可以提高可读性或效率,需要权衡。
\\n对于简单的二选一返回逻辑,三元运算符 ?:
是 if-else
的简洁替代。
反面教材 ❌:
\\npublic String getLevel(int score) {\\n String level;\\n if (score >= 60) {\\n level = \\"Pass\\";\\n } else {\\n level = \\"Fail\\";\\n }\\n return level;\\n}\\n
\\n推荐写法 ✅:
\\npublic String getLevel(int score) {\\n return score >= 60 ? \\"Pass\\" : \\"Fail\\";\\n}\\n
\\n一行代码,清晰表达了条件选择。
\\n但是千万注意不要滥用,过分嵌套的三元运算符会降低可读性。
\\nnull
方法约定返回集合类型(List, Set, Map等)时,如果没有数据,应返回空集合而不是 null
。
这可以避免调用方不必要的 null
检查。
反面教材 ❌:
\\npublic List<String> getUsers(String department) {\\n List<String> users = findUsersByDepartment(department);\\n if (users.isEmpty()) { // 或者 users == null\\n return null; // 调用方需要检查 null !\\n }\\n return users;\\n}\\n
\\n推荐写法 ✅:
\\nimport java.util.Collections;\\nimport java.util.List;\\n\\npublic List<String> getUsers(String department) {\\n List<String> users = findUsersByDepartment(department);\\n // 假设 findUsersByDepartment 可能返回 null 或空 List\\n if (users == null || users.isEmpty()) {\\n return Collections.emptyList(); // 返回不可变的空列表\\n }\\n // 或者更好的是,确保 findUsersByDepartment 内部就返回空列表而不是 null\\n return users;\\n}\\n\\n// 调用方代码,无需担心 NullPointerException\\nList<String> userList = service.getUsers(\\"IT\\");\\nfor (String user : userList) { // 直接遍历,安全\\n System.out.println(user);\\n}\\n\\n
\\n调用方代码更健壮、简洁,符合“防御性编程”的原则。
\\nOptional
优雅处理可能缺失的值当方法可能返回一个值,也可能什么都不返回时,使用 Optional<T>
作为返回类型比返回 null
更能明确表达这种可能性,并引导调用方正确处理。
反面教材 ❌:
\\npublic User findUserById(String id) {\\n // ... 查询逻辑 ...\\n if (found) {\\n return user;\\n } else {\\n return null; // 调用方必须检查 null\\n }\\n}\\n\\n// 调用方\\nUser user = findUserById(\\"123\\");\\nif (user != null) { // 繁琐的 null 检查\\n System.out.println(user.getName());\\n}\\n
\\n推荐写法 ✅:
\\nimport java.util.Optional;\\n\\npublic Optional<User> findUserById(String id) {\\n // ... 查询逻辑 ...\\n if (found) {\\n return Optional.of(user);\\n } else {\\n return Optional.empty();\\n }\\n}\\n\\n// 调用方\\nfindUserById(\\"123\\")\\n .ifPresent(user -> System.out.println(user.getName())); // 清晰地处理存在的情况\\n\\n// 或者提供默认值\\nString userName = findUserById(\\"123\\")\\n .map(User::getName)\\n .orElse(\\"Unknown User\\");\\n
\\nOptional
强制调用者思考值不存在的情况,并通过链式调用提供了更流畅的处理方式,减少空指针风险。
但是Java的Optional
非常蛋疼,如果使用时不加注意,本应返回Optional
的方法,返回了null,反而会增加负担,因此团队的开发规范至关重要。
在循环中查找元素或满足某个条件时,一旦找到或满足,应立即 return
,避免不必要的后续迭代。
反面教材 ❌:
\\npublic Product findProductByName(List<Product> products, String name) {\\n Product foundProduct = null;\\n for (Product product : products) {\\n if (product.getName().equals(name)) {\\n foundProduct = product;\\n break; // 找到后跳出循环\\n }\\n }\\n // 循环结束后再返回\\n return foundProduct;\\n}\\n
\\n需要一个额外的 foundProduct
变量,并且在循环外返回。
浪费性能。
\\n推荐写法 ✅:
\\npublic Product findProductByName(List<Product> products, String name) {\\n for (Product product : products) {\\n if (product.getName().equals(name)) {\\n return product; // 一旦找到,立即返回\\n }\\n }\\n // 循环正常结束,说明没找到\\n return null; // 或者 Optional.empty()\\n}\\n
\\n逻辑更直接,代码更紧凑。
\\nswitch
表达式(Java 14+)现在Java的 switch
表达式可以直接返回值,使得基于多分支选择的返回更加简洁。
反面教材 ❌ (传统 switch
语句):
public String getWeekdayType(DayOfWeek day) {\\n String type;\\n switch (day) {\\n case MONDAY:\\n case TUESDAY:\\n case WEDNESDAY:\\n case THURSDAY:\\n case FRIDAY:\\n type = \\"Workday\\";\\n break;\\n case SATURDAY:\\n case SUNDAY:\\n type = \\"Weekend\\";\\n break;\\n default:\\n throw new IllegalArgumentException(\\"Invalid day: \\" + day);\\n }\\n return type;\\n}\\n
\\n推荐写法 ✅ (使用 switch
表达式):
public String getWeekdayType(DayOfWeek day) {\\n return switch (day) {\\n case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> \\"Workday\\";\\n case SATURDAY, SUNDAY -> \\"Weekend\\";\\n // default 分支通常是必需的,除非覆盖了所有枚举常量\\n // 如果逻辑确定上面的 case 已经覆盖所有情况,可以不写 default,\\n // 但如果传入未覆盖的值会抛异常。\\n // 或者明确处理:\\n // default -> throw new IllegalArgumentException(\\"Invalid day: \\" + day);\\n };\\n}\\n
\\n代码量显著减少,->
语法更直观,且 switch
表达式要求覆盖所有情况(或有 default
),更安全。
避免使用魔法数字或含义模糊的字符串作为返回码,应返回定义清晰的枚举或包含状态和信息的自定义结果对象。
\\n反面教材 ❌:
\\npublic int processOrder(Order order) {\\n if (order == null) return -1; // -1 代表失败\\n if (!checkInventory(order)) return 1; // 1 代表库存不足\\n // ... 处理 ...\\n if (!paymentSuccess(order)) return 2; // 2 代表支付失败\\n\\n return 0; // 0 代表成功\\n}\\n\\n// 调用方\\nint resultCode = processOrder(myOrder);\\nif (resultCode == 0) { ... }\\nelse if (resultCode == 1) { ... } // 难以理解和维护\\n
\\n推荐写法 ✅:
\\npublic enum OrderStatus { SUCCESS, FAILED_NULL_ORDER, FAILED_INVENTORY, FAILED_PAYMENT }\\n\\npublic OrderStatus processOrder(Order order) {\\n if (order == null) return OrderStatus.FAILED_NULL_ORDER;\\n if (!checkInventory(order)) return OrderStatus.FAILED_INVENTORY;\\n // ... 处理 ...\\n if (!paymentSuccess(order)) return OrderStatus.FAILED_PAYMENT;\\n\\n return OrderStatus.SUCCESS;\\n}\\n\\n// 调用方\\nOrderStatus status = processOrder(myOrder);\\nif (status == OrderStatus.SUCCESS) { ... }\\nelse if (status == OrderStatus.FAILED_INVENTORY) { ... } // 清晰易懂\\n
\\n返回类型本身就携带了业务含义,代码自解释,更易于维护和扩展。
\\nfinally
块中的 return
(陷阱!)尽量避免在 finally
块中使用 return
。
它会覆盖 try
或 catch
块中的 return
或抛出的异常,可能导致非预期的行为和难以追踪的 Bug。
反面教材 ❌ (极不推荐):
\\npublic int trickyReturn() {\\n try {\\n System.out.println(\\"Trying...\\");\\n // 假设这里发生异常或正常返回 1\\n // throw new RuntimeException(\\"Oops!\\");\\n return 1;\\n } catch (Exception e) {\\n System.out.println(\\"Caught exception\\");\\n return 2; // 试图在 catch 中返回 2\\n } finally {\\n System.out.println(\\"Finally block\\");\\n return 3; // finally 中的 return 会覆盖前面的所有返回/异常!\\n }\\n // 这个方法最终会返回 3,即使 try 或 catch 中有 return 或抛出异常\\n}\\n
\\nfinally
的主要目的是资源清理(如关闭流、释放锁),而不是返回值。
在这里 return
会让程序行为变得诡异。
推荐写法 ✅:
\\npublic int cleanReturn() {\\n int result = -1; // 默认值或错误码\\n Connection conn = null;\\n try {\\n conn = getConnection();\\n // ... 使用 conn 操作 ...\\n result = 1; // 操作成功\\n return result; // 在 try 中返回\\n } catch (SQLException e) {\\n System.err.println(\\"Database error: \\" + e.getMessage());\\n result = -2; // 数据库错误码\\n return result; // 在 catch 中返回\\n } finally {\\n // 只做清理工作\\n if (conn != null) {\\n try {\\n conn.close();\\n System.out.println(\\"Connection closed.\\");\\n } catch (SQLException e) {\\n System.err.println(\\"Failed to close connection: \\" + e.getMessage());\\n }\\n }\\n // 不要在 finally 中 return\\n }\\n}\\n
\\nfinally
专注于它的本职工作——资源清理,让返回值逻辑在 try
和 catch
中清晰地处理。
return
虽小,五脏俱全。
一切的目的都是让代码更加优雅,逻辑更加清晰。
\\n这些并非什么高深的理论,更多的是在日常写代码时,对可读性、简洁性和健壮性的追求。
\\n希望你能写出诗一样的代码,从码农
变成代码艺术家
。
当然,上面提到的点,有些可能在特定复杂场景下有争议(比如临时变量有时能提升可读性)。关键在于理解这些写法背后的思考:如何让代码更容易被他人(以及未来的自己)理解和维护?
\\n希望这些粗浅的经验能给大家带来一点启发。
\\n代码之道,与君共勉!
","description":"前言 return 这个关键字,相信大家每天都在用。\\n\\n它就像一把锤子,敲打着我们代码里的每一个出口。\\n\\n但扪心自问,我们真的把这把锤子用好了吗?\\n\\n今天,不想聊什么高深莫测的设计模式,也不敢妄称“最佳实践”。\\n\\n只想结合自己这些年在项目摸爬滚打中踩过的一些坑、积累的一点心得,和大家分享一些关于 return 的、或许能让我们的代码更规范、更优雅、更易读的写法。\\n\\n权当抛砖引玉,希望能引发大家的一些思考。\\n\\n耐心看完,你一定有所收获。\\n\\n正文\\n1. 提前返回(卫语句):让主逻辑更清晰\\n\\n这是最常见也是非常推荐的一种模式。\\n\\n核心思想是:在方法开头处理掉所有“异常”或…","guid":"https://juejin.cn/post/7497804336568582183","author":"一只叫煤球的猫","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T15:11:12.616Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/db4ff70dfdd44c6791ee05d3e01bd1c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiA5Y-q5Y-r54Wk55CD55qE54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746371702&x-signature=fpFJ66lh%2FwH6bEj3R6IExvf3Y0c%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","代码规范"],"attachments":null,"extra":null,"language":null},{"title":"动态线程池 v1.2.1 版本发布,告警规则重构,bytebuddy 替换 cglib,新增 jmh 基准测试等!","url":"https://juejin.cn/post/7497804336567844903","content":"DynamicTp
是一款基于配置中心的轻量级动态线程池监控管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。
经过多个版本的迭代,目前最新版本 v1.2.1
具有以下特性 ✅
实时指标监控端点名称从 dynamic-tp
修改为 dynamictp
,消除 springboot 的非法字符 warn 警告。
v1.2.1 之前版本里告警规则比较简单,通过 threshold
和 interval
字段来控制。
dynamictp:\\n # 全局配置\\n globalExecutorProps: # 线程池配置 > 全局配置 > 字段默认值\\n rejectedHandlerType: CallerRunsPolicy\\n queueType: VariableLinkedBlockingQueue\\n waitForTasksToCompleteOnShutdown: true\\n awaitTerminationSeconds: 3\\n taskWrapperNames: [\\"swTrace\\", \\"ttl\\", \\"mdc\\"]\\n queueTimeout: 300\\n runTimeout: 300\\n notifyItems: # 报警项,不配置自动会按默认值(查看源码NotifyItem类)配置(变更通知、容量报警、活性报警、拒绝报警、任务超时报警)\\n - type: change\\n interval: 10\\n\\n - type: capacity # 队列容量使用率,报警项类型,查看源码 NotifyTypeEnum枚举类\\n threshold: 80 # 报警阈值,默认70,意思是队列使用率达到70%告警\\n interval: 120 # 报警间隔(单位:s),默认120\\n\\n - type: liveness # 线程池活性\\n threshold: 80 # 报警阈值,默认 70,意思是活性达到70%告警\\n interval: 120\\n\\n - type: reject # 触发任务拒绝告警\\n threshold: 1 # 默认阈值10\\n interval: 120\\n\\n - type: run_timeout # 任务执行超时告警\\n threshold: 100 # 默认阈值10\\n interval: 120\\n\\n - type: queue_timeout # 任务排队超时告警\\n threshold: 100 # 默认阈值10\\n interval: 120\\n
\\n比如对于 capacity 项:语义为当线程池队列容量达到 80%时触发一次告警,告警后 120s 内再产生的报警保持静默。
\\n设计的比较草率,有几个问题:
\\n数据统计需要限定在一定的时间窗口内,过期需重新计数,此处 interval 只用在了静默处理上,没统计窗口的概念
\\n只要阈值达到了就会产生一次报警,更好的做法应该是达到阈值的次数达到某个值才算一个异常,触发一次报警
\\n无效告警多,静默不能关闭
\\n在 v1.2.1 版本里,我们重构了告警规则,引入 threshold
、count
、period
、silencePeriod
四个配置字段。
目前的告警语义:对于某一个告警项,在一定的统计窗口(period)内,达到阈值(threshold)的次数达到某个值(count)时才算为一个有效的异常,触发一次报警。告警后(silencePeriod)内再产生的报警保持静默,且静默可以关闭。
\\ndynamictp:\\n globalExecutorProps:\\n rejectedHandlerType: CallerRunsPolicy\\n queueType: VariableLinkedBlockingQueue\\n waitForTasksToCompleteOnShutdown: true\\n awaitTerminationSeconds: 3\\n taskWrapperNames: [\\"swTrace\\", \\"ttl\\", \\"mdc\\"]\\n queueTimeout: 300\\n runTimeout: 300\\n notifyItems: # 报警项,不配置自动会按默认值配置(变更通知、容量报警、活性报警、拒绝报警、任务超时报警)\\n - type: change # 线程池核心参数变更通知\\n silencePeriod: 120 # 通知静默时间(单位:s),默认值1,0表示不静默\\n\\n - type: capacity # 队列容量使用率,报警项类型,查看源码 NotifyTypeEnum枚举类\\n threshold: 80 # 报警阈值,意思是队列使用率达到70%告警;默认值=70\\n count: 2 # 在一个统计周期内,如果触发阈值的数量达到 count,则触发报警;默认值=1\\n period: 30 # 报警统计周期(单位:s),默认值=120\\n silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默,默认值=120\\n\\n - type: liveness # 线程池活性\\n threshold: 80 # 报警阈值,意思是活性达到70%告警;默认值=70\\n count: 3 # 在一个统计周期内,如果触发阈值的数量达到 count,则触发报警;默认值=1\\n period: 30 # 报警统计周期(单位:s),默认值=120\\n silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默;默认值=120\\n\\n - type: reject # 触发任务拒绝告警\\n count: 1 # 在一个统计周期内,如果触发拒绝策略次数达到 count,则触发报警;默认值=1\\n period: 30 # 报警统计周期(单位:s),默认值=120\\n silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默;默认值=120\\n\\n - type: run_timeout # 任务执行超时告警\\n count: 20 # 在一个统计周期内,如果执行超时次数达到 count,则触发报警;默认值=10\\n period: 30 # 报警统计周期(单位:s),默认值=120\\n silencePeriod: 30 # 报警静默时间(单位:s),0表示不静默;默认值=120\\n\\n - type: queue_timeout # 任务排队超时告警\\n count: 5 # 在一个统计周期内,如果排队超时次数达到 count,则触发报警;默认值=10\\n period: 30 # 报警统计周期(单位:s),默认值=120\\n silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默;默认值=120\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/545\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/538\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/553\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/542\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/529\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/534\\n
\\nhttps://github.com/dromara/dynamic-tp/pull/537\\n
\\n以上就是本次发版的全部内容。欢迎大家升级体验!
\\n看到这儿,方便的话给项目一个 star,你的支持是我们前进的动力!
\\n使用过程中有任何问题,或者对项目有什么想法或者建议,可以加入社群,跟 1700+ 群友一起交流讨论。
\\n\\n官网:https://dynamictp.cn\\n\\ngitee:https://gitee.com/dromara/dynamic-tp\\n\\ngithub:https://github.com/dromara/dynamic-tp\\n\\ngitcode:https://gitcode.com/dromara/dynamic-tp\\n\\n
","description":"DynamicTp 简介 DynamicTp 是一款基于配置中心的轻量级动态线程池监控管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。\\n\\nDynamicTp 特性\\n\\n经过多个版本的迭代,目前最新版本 v1.2.1 具有以下特性 ✅\\n\\nv1.2.1 升级注意事项\\nDtpEndpoint 端点名称修改\\n\\n实时指标监控端点名称从 dynamic-tp 修改为 dynamictp,消除 springboot 的非法字符 warn 警告。\\n\\n告警规则重构\\n\\nv1.2.1 之前版本里告警规则比较简单,通过 threshold 和…","guid":"https://juejin.cn/post/7497804336567844903","author":"CodeFox","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T09:49:25.127Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b42d7356bdc649c3b2661f7519181b67~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746352165&x-signature=gdlDx0LZNuMk%2BekBXyoBeGrsclY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/698e4d59092c4d5787d492cdb4176545~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ29kZUZveA==:q75.awebp?rk3s=f64ab15b&x-expires=1746352165&x-signature=VG%2FskIi0bUqvnl5ePfcHtfi%2Bumc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"登录功能实现深度解析:从会话管理到安全校验全流程指南","url":"https://juejin.cn/post/7497533992087797800","content":"graph TD\\n A[用户提交登录表单] --\x3e B{参数校验}\\n B --\x3e|校验失败| C[返回错误提示]\\n B --\x3e|校验通过| D[数据库查询用户]\\n D --\x3e|用户不存在| C\\n D --\x3e|密码错误| C\\n D --\x3e|验证成功| E[生成访问令牌]\\n E --\x3e F[返回令牌给客户端]\\n F --\x3e G[客户端存储令牌]\\n G --\x3e H[后续请求携带令牌]\\n H --\x3e I[服务端校验令牌]\\n I --\x3e|校验通过| J[返回请求数据]\\n I --\x3e|校验失败| K[返回401状态码]\\n
\\n技术类型 | Cookie | Session | JWT |
---|---|---|---|
存储位置 | 客户端 | 服务端 | 客户端 |
安全性 | 较低 | 较高 | 较高(需HTTPS) |
扩展性 | 单域限制 | 集群部署需同步 | 天然支持分布式 |
性能开销 | 低 | 中等 | 低 |
典型应用场景 | 简单状态保持 | 传统Web应用 | 前后端分离/移动端 |
令牌结构示例:
\\n// Header\\n{\\n \\"alg\\": \\"HS256\\",\\n \\"typ\\": \\"JWT\\"\\n}\\n\\n// Payload\\n{\\n \\"sub\\": \\"1234567890\\",\\n \\"name\\": \\"John Doe\\",\\n \\"iat\\": 1516239022,\\n \\"exp\\": 1516242622\\n}\\n\\n// Signature\\nHMACSHA256(\\n base64UrlEncode(header) + \\".\\" +\\n base64UrlEncode(payload),\\n secret)\\n
\\nJava生成JWT示例:
\\npublic String generateToken(UserDetails userDetails) {\\n Map<String, Object> claims = new HashMap<>();\\n claims.put(\\"roles\\", userDetails.getAuthorities());\\n \\n return Jwts.builder()\\n .setClaims(claims)\\n .setSubject(userDetails.getUsername())\\n .setIssuedAt(new Date(System.currentTimeMillis()))\\n .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))\\n .signWith(SignatureAlgorithm.HS256, secretKey)\\n .compact();\\n}\\n
\\npublic class JwtFilter implements Filter {\\n @Override\\n public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) \\n throws IOException, ServletException {\\n \\n HttpServletRequest request = (HttpServletRequest) req;\\n String token = resolveToken(request);\\n \\n if (StringUtils.hasText(token) && validateToken(token)) {\\n Authentication auth = parseAuthentication(token);\\n SecurityContextHolder.getContext().setAuthentication(auth);\\n }\\n \\n chain.doFilter(req, res);\\n }\\n\\n private String resolveToken(HttpServletRequest request) {\\n String bearerToken = request.getHeader(\\"Authorization\\");\\n if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(\\"Bearer \\")) {\\n return bearerToken.substring(7);\\n }\\n return null;\\n }\\n}\\n
\\npublic class JwtInterceptor implements HandlerInterceptor {\\n \\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) \\n throws Exception {\\n \\n if (!(handler instanceof HandlerMethod)) return true;\\n\\n String token = getTokenFromRequest(request);\\n if (token == null || !jwtProvider.validateToken(token)) {\\n throw new AuthenticationException(\\"Invalid JWT token\\");\\n }\\n \\n setAuthentication(token);\\n return true;\\n }\\n\\n private String getTokenFromRequest(HttpServletRequest request) {\\n // 从Cookie或Header获取令牌\\n }\\n}\\n
\\n特性 | Filter | Interceptor |
---|---|---|
容器依赖 | Servlet容器 | Spring容器 |
执行顺序 | 最先执行 | 在DispatcherServlet之后 |
异常处理 | 无法使用@ExceptionHandler | 可以使用 |
资源类型 | 所有资源 | Spring管理的资源 |
配置方式 | web.xml或@WebFilter | Java配置 |
@RestControllerAdvice\\npublic class GlobalExceptionHandler {\\n \\n @ExceptionHandler(AuthenticationException.class)\\n public ResponseEntity<ErrorResponse> handleAuthException(AuthenticationException ex) {\\n ErrorResponse error = new ErrorResponse();\\n error.setStatus(HttpStatus.UNAUTHORIZED.value());\\n error.setMessage(\\"Authentication failed: \\" + ex.getMessage());\\n error.setTimestamp(LocalDateTime.now());\\n return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED);\\n }\\n\\n @ExceptionHandler(AccessDeniedException.class)\\n public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {\\n ErrorResponse error = new ErrorResponse();\\n error.setStatus(HttpStatus.FORBIDDEN.value());\\n error.setMessage(\\"Access denied: \\" + ex.getMessage());\\n return new ResponseEntity<>(error, HttpStatus.FORBIDDEN);\\n }\\n}\\n
\\n@Data\\n@AllArgsConstructor\\n@NoArgsConstructor\\npublic class ErrorResponse {\\n private int status;\\n private String message;\\n private LocalDateTime timestamp;\\n private String path;\\n \\n public ErrorResponse(HttpStatus status, String message, String path) {\\n this.status = status.value();\\n this.message = message;\\n this.timestamp = LocalDateTime.now();\\n this.path = path;\\n }\\n}\\n
\\npublic TokenPair refreshToken(String refreshToken) {\\n if (!validateRefreshToken(refreshToken)) {\\n throw new InvalidTokenException(\\"Invalid refresh token\\");\\n }\\n \\n String username = parseUsername(refreshToken);\\n UserDetails user = userService.loadUserByUsername(username);\\n \\n String newAccessToken = generateAccessToken(user);\\n String newRefreshToken = generateRefreshToken(user);\\n \\n redisTemplate.delete(refreshToken);\\n redisTemplate.opsForValue().set(newRefreshToken, username, REFRESH_EXPIRE);\\n \\n return new TokenPair(newAccessToken, newRefreshToken);\\n}\\n
\\npublic void handleConcurrentLogin(String username, String newSessionId) {\\n String oldSession = redisTemplate.opsForValue().get(\\"user:\\" + username);\\n if (StringUtils.hasText(oldSession)) {\\n // 1. 发送下线通知\\n messagingTemplate.convertAndSendToUser(oldSession, \\"/queue/logout\\", \\"forced_logout\\");\\n // 2. 清除旧令牌\\n redisTemplate.delete(oldSession);\\n }\\n // 3. 存储新会话\\n redisTemplate.opsForValue().set(\\"user:\\" + username, newSessionId);\\n}\\n
\\npublic boolean validateToken(String token) {\\n // 先检查黑名单\\n if (redisTemplate.hasKey(\\"token:blacklist:\\" + token)) {\\n return false;\\n }\\n \\n // 快速过期检查\\n if (Jwts.parser().parseClaimsJws(token).getBody().getExpiration().before(new Date())) {\\n return false;\\n }\\n \\n // 详细验证\\n try {\\n Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);\\n return true;\\n } catch (JwtException e) {\\n return false;\\n }\\n}\\n
\\n@Cacheable(value = \\"userDetails\\", key = \\"#username\\")\\npublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\\n User user = userRepository.findByUsername(username)\\n .orElseThrow(() -> new UsernameNotFoundException(\\"User not found\\"));\\n \\n return new CustomUserDetails(\\n user.getUsername(),\\n user.getPassword(),\\n getAuthorities(user.getRoles())\\n );\\n}\\n\\n@CacheEvict(value = \\"userDetails\\", key = \\"#user.username\\")\\npublic void updateUser(User user) {\\n userRepository.save(user);\\n}\\n
\\n攻击类型 | 防护措施 | 实现示例 |
---|---|---|
暴力破解 | 登录失败锁定机制 | Redis记录失败次数 |
CSRF | SameSite Cookie + 状态令牌 | 生成anti-csrf-token |
XSS | 输入过滤 + CSP策略 | Jsoup清理HTML内容 |
会话劫持 | 绑定用户设备指纹 | 记录IP+UserAgent+浏览器指纹 |
重放攻击 | 请求时间戳校验 | 验证请求时间在5分钟内 |
@Configuration\\npublic class SecurityHeaderConfig implements WebMvcConfigurer {\\n \\n @Override\\n public void addCorsMappings(CorsRegistry registry) {\\n registry.addMapping(\\"/**\\")\\n .allowedOrigins(\\"https://yourdomain.com\\")\\n .allowedMethods(\\"GET\\", \\"POST\\")\\n .allowCredentials(true);\\n }\\n\\n @Bean\\n public FilterRegistrationBean<HeaderFilter> securityHeadersFilter() {\\n FilterRegistrationBean<HeaderFilter> registration = new FilterRegistrationBean<>();\\n registration.setFilter(new HeaderFilter());\\n registration.addUrlPatterns(\\"/*\\");\\n return registration;\\n }\\n\\n private static class HeaderFilter extends OncePerRequestFilter {\\n @Override\\n protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, \\n FilterChain filterChain) throws ServletException, IOException {\\n \\n response.setHeader(\\"X-Content-Type-Options\\", \\"nosniff\\");\\n response.setHeader(\\"X-Frame-Options\\", \\"DENY\\");\\n response.setHeader(\\"X-XSS-Protection\\", \\"1; mode=block\\");\\n response.setHeader(\\"Content-Security-Policy\\", \\"default-src \'self\'\\");\\n \\n filterChain.doFilter(request, response);\\n }\\n }\\n}\\n
\\n@Aspect\\n@Component\\npublic class LoginAuditAspect {\\n \\n @Autowired\\n private AuditLogService auditLogService;\\n\\n @AfterReturning(pointcut = \\"execution(* AuthController.login(..))\\", returning = \\"result\\")\\n public void logSuccessLogin(JoinPoint joinPoint, Object result) {\\n Object[] args = joinPoint.getArgs();\\n String username = (String) args[0];\\n auditLogService.log(username, \\"LOGIN_SUCCESS\\", \\"User logged in successfully\\");\\n }\\n\\n @AfterThrowing(pointcut = \\"execution(* AuthController.login(..))\\", throwing = \\"ex\\")\\n public void logFailedLogin(JoinPoint joinPoint, Exception ex) {\\n Object[] args = joinPoint.getArgs();\\n String username = (String) args[0];\\n auditLogService.log(username, \\"LOGIN_FAILED\\", ex.getMessage());\\n }\\n}\\n
\\n@Configuration\\npublic class SecurityMetricsConfig {\\n\\n @Bean\\n public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {\\n return registry -> registry.config().commonTags(\\n \\"application\\", \\"auth-service\\",\\n \\"region\\", System.getenv(\\"REGION\\")\\n );\\n }\\n\\n @Bean\\n public TimedAspect timedAspect(MeterRegistry registry) {\\n return new TimedAspect(registry);\\n }\\n\\n @Bean\\n public Counter loginAttemptCounter(MeterRegistry registry) {\\n return Counter.builder(\\"auth.login.attempts\\")\\n .description(\\"Total login attempts\\")\\n .register(registry);\\n }\\n}\\n
\\n项目规模 | 推荐方案 | 优势 |
---|---|---|
小型单体应用 | Session + Cookie | 实现简单,维护成本低 |
中大型Web应用 | JWT + Redis | 扩展性强,支持分布式部署 |
微服务架构 | OAuth2 + JWT | 标准化协议,生态完善 |
高安全要求系统 | SAML + 硬件令牌 | 企业级安全,多因素认证 |
通过本文的详细实现方案,大家可以构建出更加安全可靠、高性能的登录认证系统。建议根据实际业务需求选择合适的会话管理方案,并持续监控系统安全指标。
","description":"一、登录功能核心实现流程 1.1 登录流程图解\\ngraph TD\\n A[用户提交登录表单] --\x3e B{参数校验}\\n B --\x3e|校验失败| C[返回错误提示]\\n B --\x3e|校验通过| D[数据库查询用户]\\n D --\x3e|用户不存在| C\\n D --\x3e|密码错误| C\\n D --\x3e|验证成功| E[生成访问令牌]\\n E --\x3e F[返回令牌给客户端]\\n F --\x3e G[客户端存储令牌]\\n G --\x3e H[后续请求携带令牌]\\n H --\x3e I[服务端校验令牌]\\n I --\x3e|校验通过| J…","guid":"https://juejin.cn/post/7497533992087797800","author":"小厂永远得不到的男人","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T08:16:05.169Z","media":null,"categories":["后端","程序员"],"attachments":null,"extra":null,"language":null},{"title":"腾讯Java后端一面,被速通了!","url":"https://juejin.cn/post/7497428040483946550","content":"分享一篇腾讯的后端Java一面凉经,被速通了, 大家感受一下难度如何。
\\n这次面试的考察覆盖了从 项目经验的深度挖掘(面试官非常看重 STAR 法则的应用)到 扎实的计算机基础(经典的 TCP/UDP 对比、MySQL 事务与 MVCC 原理),再到 分布式系统 的核心概念(如分布式锁的必要性与 Redis 实现),甚至还涉及了对 新兴技术趋势(如 AI 辅助编码)的看法,最后当然少不了 算法能力 的现场检验。
\\nSTAR 法则 是介绍项目经验的黄金法则:
\\n针对自己简历上的每个项目,至少准备 1-2 个有技术含量或业务复杂度的难点,想清楚解决过程和结果。不要只说功能实现,要突出技术选型、优化思路和遇到的挑战。
\\n为了更直观地对比,可以看下面这个表格:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | TCP | UDP |
---|---|---|
连接性 | 面向连接 | 无连接 |
可靠性 | 可靠 | 不可靠 (尽力而为) |
状态维护 | 有状态 | 无状态 |
传输效率 | 较低 | 较高 |
传输形式 | 面向字节流 | 面向数据报 (报文) |
头部开销 | 20 - 60 字节 | 8 字节 |
通信模式 | 点对点 (单播) | 单播、多播、广播 |
常见应用 | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 |
选择 TCP 还是 UDP,主要取决于你的应用对数据传输的可靠性要求有多高,以及对实时性和效率的要求有多高。
\\n当数据准确性和完整性至关重要,一点都不能出错时,通常选择 TCP。因为 TCP 提供了一整套机制(三次握手、确认应答、重传、流量控制等)来保证数据能够可靠、有序地送达。典型应用场景如下:
\\n当实时性、速度和效率优先,并且应用能容忍少量数据丢失或乱序时,通常选择 UDP。UDP 开销小、传输快,没有建立连接和保证可靠性的复杂过程。典型应用场景如下:
\\n图解如下:
\\n详细介绍可以参考 JavaGuide(javaguide.cn)的这篇文章:javaguide.cn/cs-basics/n… 。
\\nSQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
\\n隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
---|---|---|---|
READ UNCOMMITTED | √ | √ | √ |
READ COMMITTED | × | √ | √ |
REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
SERIALIZABLE | × | × | × |
默认级别查询:
\\nMySQL InnoDB 存储引擎的默认隔离级别是 REPEATABLE READ。可以通过以下命令查看:
\\nSELECT @@tx_isolation;
SELECT @@transaction_isolation;
mysql> SELECT @@transaction_isolation;\\n+-------------------------+\\n| @@transaction_isolation |\\n+-------------------------+\\n| REPEATABLE-READ |\\n+-------------------------+\\n
\\nInnoDB 的 REPEATABLE READ 对幻读的处理:
\\n标准的 SQL 隔离级别定义里,REPEATABLE READ 是无法防止幻读的。但 InnoDB 的实现通过以下机制很大程度上避免了幻读:
\\nSELECT ... FOR UPDATE
, SELECT ... LOCK IN SHARE MODE
, INSERT
, UPDATE
, DELETE
这些操作。InnoDB 使用 Next-Key Lock 来锁定扫描到的索引记录及其间的范围(间隙),防止其他事务在这个范围内插入新的记录,从而避免幻读。Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的组合。值得注意的是,虽然通常认为隔离级别越高、并发性越差,但 InnoDB 存储引擎通过 MVCC 机制优化了 REPEATABLE READ 级别。对于许多常见的只读或读多写少的场景,其性能与 READ COMMITTED 相比可能没有显著差异。不过,在写密集型且并发冲突较高的场景下,RR 的间隙锁机制可能会比 RC 带来更多的锁等待。
\\n此外,在某些特定场景下,如需要严格一致性的分布式事务(XA Transactions),InnoDB 可能要求或推荐使用 SERIALIZABLE 隔离级别来确保全局数据的一致性。
\\n《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到:
\\n\\n\\nInnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。
\\n
MVCC(多版本并发控制)是一种并发控制机制。核心原理是为数据行维护多个版本:写操作(如UPDATE/DELETE)不直接覆盖旧数据,而是创建新版本,记录事务ID(DB_TRX_ID),并通过指针(DB_ROLL_PTR)指向存储在undo log中的旧版本。读操作(SELECT)基于事务启动时创建的Read View(包含活跃事务列表),通过比较行版本的事务ID和Read View,只读取对当前事务可见的版本(通常是启动前已提交或自身修改的版本),从而实现非锁定读,提升并发性能。
\\n详细介绍可以参考 JavaGuide(javaguide.cn)的这篇文章: InnoDB 存储引擎对 MVCC 的实现。
\\n在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。
\\n举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况:
\\n为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。
\\n如何才能实现共享资源的互斥访问呢? 锁是一个比较通用的解决方案,更准确点来说是悲观锁。
\\n悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
\\n对于单机多线程来说,在 Java 中,我们通常使用 ReentrantLock
类、synchronized
关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
下面是我对本地锁画的一张示意图。
\\n从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源。
\\n分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
\\n举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。
\\n下面是我对分布式锁画的一张示意图。
\\n从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。
\\nRedis 实现分布式锁主要利用其原子操作。
\\nSET key unique_value NX EX expire_time
命令。NX 确保只有键不存在时才能设置成功(获取锁),EX 设置过期时间防止持有者宕机导致死锁,unique_value
用于标识锁的持有者。unique_value
相等,确认是自己的锁后才执行 DEL,保证原子性和防止误删。我认为它是一个极具潜力的发展方向,并且已经成为提升开发者生产力的重要工具。
\\nAI 辅助编码工具,例如 GitHub Copilot、Tabnine 等,正越来越多地集成到我们的开发流程中。我认为它们的主要优势体现在以下几个方面:
\\n当然,我们也要清醒地认识到当前 AI 辅助编码工具的局限性:
\\nLeetcode3.无重复字符的最长子串
","description":"分享一篇腾讯的后端Java一面凉经,被速通了, 大家感受一下难度如何。 这次面试的考察覆盖了从 项目经验的深度挖掘(面试官非常看重 STAR 法则的应用)到 扎实的计算机基础(经典的 TCP/UDP 对比、MySQL 事务与 MVCC 原理),再到 分布式系统 的核心概念(如分布式锁的必要性与 Redis 实现),甚至还涉及了对 新兴技术趋势(如 AI 辅助编码)的看法,最后当然少不了 算法能力 的现场检验。\\n\\n项目遇到过什么难点吗?你是如何解决的?\\n\\nSTAR 法则 是介绍项目经验的黄金法则:\\n\\nSituation (背景): “我参与的项目是 X(比如…","guid":"https://juejin.cn/post/7497428040483946550","author":"JavaGuide","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T07:13:41.382Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ca3f50fd475642488a29211c6feeaef3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YUd1aWRl:q75.awebp?rk3s=f64ab15b&x-expires=1746342821&x-signature=UGs%2FUybRcv5dBKm81eZ5mgkRX6g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e212643ff18a4feb8c23cc71b20865cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YUd1aWRl:q75.awebp?rk3s=f64ab15b&x-expires=1746342821&x-signature=zqRU2S7cjqxH4%2FUNYQcMPJuu9v8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b186da3c2fc428da33a3357a725fa4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YUd1aWRl:q75.awebp?rk3s=f64ab15b&x-expires=1746342821&x-signature=%2Ft6RcQ9WtR3sB8LIqkpv3lmORkk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9fbcf32cc52c493fbbfa91dc9262743c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YUd1aWRl:q75.awebp?rk3s=f64ab15b&x-expires=1746342821&x-signature=hNq9ibBqWOgPbGrS5wP%2FR9MGSYM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/156c854bb2924d9abb2af0ed030516cd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YUd1aWRl:q75.awebp?rk3s=f64ab15b&x-expires=1746342821&x-signature=29qiu0vORjXhGenhMYqQjQJoH1M%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"n8n 快速入门","url":"https://juejin.cn/post/7497523095201906688","content":"今天,我将为大家介绍一个当前非常流行的可视化智能体搭建平台——n8n。n8n(发音为 \\"n-eight-n\\")是一个强大的自动化工具,它能够帮助您轻松地将任何具有API的应用程序与其他应用程序进行连接,并通过最少的代码或甚至无需编写代码来实现数据的自动化流转。
\\nn8n的核心特点之一是高度可定制,它提供了灵活的工作流程构建功能,并允许您创建自定义节点,满足各种独特的业务需求。无论是简单的数据传输任务,还是复杂的工作流,n8n都能通过其丰富的配置选项轻松实现。
\\n此外,n8n非常方便,支持通过npm或Docker进行试用,您可以在自己的机器上快速启动平台。如果您希望将基础设施的管理交给专业团队,n8n还提供了Cloud托管选项,您可以通过云端托管服务轻松享受n8n的功能,无需担心服务器的配置与维护。
\\nn8n还注重隐私和安全,通过自托管部署,您可以完全掌控数据的流动与存储,确保您的业务流程在保护隐私和数据安全的前提下顺畅运行。无论是个人项目还是企业级应用,n8n都能为您提供高度可靠的自动化解决方案。
\\n目前,我们依然使用的是腾讯云的轻量级服务器,并选择了专享宝塔面板版本。在成功购买服务器后,您将能够直接访问登录页面,界面如图所示。
\\n首先,找到并打开 Docker,进入其应用商店界面,具体操作如图所示。
\\n点击“安装”按钮后,请稍等片刻,直到安装完成并成功启动。启动成功后,我们接下来需要打开防火墙入口,具体操作步骤如图所示。
\\n完成防火墙设置后,我们就可以正常访问页面了,浏览器输入:http://ip:5678,具体界面如图所示。
\\n首先,注册邮箱和设置密码,请务必牢记密码,因为如果忘记了只能重新安装。目前,开源社区版本不提供密码找回服务。注册完成后,我们将快速开始第一个案例。
\\n点击页面上固定的工作流面板,其中包含一个简单的案例,界面如图所示。
\\n进入后,你将看到Agent的所有功能点,界面如下图所示。
\\n以前,我们调试智能体的过程通常依赖于一套固定的流程和工具。这些工具和界面设计旨在帮助开发人员更高效地识别和解决问题。最常见的元器的界面通常包括以下几个关键元素:
\\n他将这些可视化内容简化为节点形式,使得整个过程更加直观和易于管理。通过这种方式,开发人员能够更方便地进行操作和调试,无需过多关注复杂的底层细节。然而,这只是常规操作中的一部分,接下来我们将重点讨论智能体平台中较为不常见的内容,比如数据库连接和MCP的配置与应用。
\\n点击工具栏中的“+”号,我们可以直接在弹出的商店界面中选择MySQL数据库连接,并快速进行配置和连接操作。
\\n不过这里的数据库连接并不是让大模型帮助我们生成SQL,而是指我们提前配置好的数据库连接。具体操作如下所示:
\\n同样地,n8n 也支持 MCP 服务器的配置。操作方式与之前相同,只需点击工具栏中的“+”号,进行相应配置。如图所示:
\\n接下来,只需配置我们自己的 MCP 服务器的 SSE 地址,完成后即可开始使用。
\\n通过今天的介绍,相信大家对n8n这个强大的可视化智能体搭建平台有了基本的了解。n8n凭借其高度可定制的工作流、灵活的配置选项以及对隐私与安全的关注,成为了开发者和企业自动化解决方案的重要工具。从快速部署到便捷的数据库和MCP配置,n8n为我们提供了一个简单而高效的方式来处理复杂的自动化任务。无论是个人项目还是企业级应用,n8n都将成为你智能自动化旅程中的得力助手。
\\n我是努力的小雨,一个正经的 Java 东北服务端开发,整天琢磨着 AI 技术这块儿的奥秘。特爱跟人交流技术,喜欢把自己的心得和大家分享。还当上了腾讯云创作之星,阿里云专家博主,华为云云享专家,掘金优秀作者。各种征文、开源比赛的牌子也拿了。
\\n💡 想把我在技术路上走过的弯路和经验全都分享出来,给你们的学习和成长带来点启发,帮一把。
\\n🌟 欢迎关注努力的小雨,咱一块儿进步!🌟
","description":"今天,我将为大家介绍一个当前非常流行的可视化智能体搭建平台——n8n。n8n(发音为 \\"n-eight-n\\")是一个强大的自动化工具,它能够帮助您轻松地将任何具有API的应用程序与其他应用程序进行连接,并通过最少的代码或甚至无需编写代码来实现数据的自动化流转。 n8n的核心特点之一是高度可定制,它提供了灵活的工作流程构建功能,并允许您创建自定义节点,满足各种独特的业务需求。无论是简单的数据传输任务,还是复杂的工作流,n8n都能通过其丰富的配置选项轻松实现。\\n\\n此外,n8n非常方便,支持通过npm或Docker进行试用,您可以在自己的机器上快速启动平台…","guid":"https://juejin.cn/post/7497523095201906688","author":"努力的小雨","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T05:55:15.699Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/951ece4e465548f29c79763d74200f06~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=TwDf0JNeSqI6M%2BRXrmoZ%2FRt9X2E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4845865d7f704e6a875494cd913f5c31~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=MFvoBglp9YUvG99yaotCOMxnt3A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b7194a69a0834fc0b5d62930deaa6597~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=uNIJQ8pWGk2BJKXVIZwSpo8UuGg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4ce00de7a8a7417e9f3adb9e769d93c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=Rwk74sS6fUL5Qd7lOvGRhw7PXgQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91d34ed2d4dd4c2ea10d39239b583cab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=Nisx1ktNcHY9YxCTxil3lY0Lsfw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ac6d287f59d943699974ddb1dbdc3850~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=tUwkktc3vTbUcq9YY059sKCI%2FwE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/eca5fd30b55d4f32b13d1a524784e157~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=GN64ydoVP8La5EiyB%2B%2BcQcanDgA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbe741429ea64fd58a0dfe2f0e6e5832~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=YOnSekr3qYfNDlpb0k9pdYNrDxg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a9f507b8daba4a3bb020633fcd5d366d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=vuvpwcABX%2BVVtkcLmZCtNG%2BygfI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ae9402f3267c45bba0d0c52a789475e1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqq5Yqb55qE5bCP6Zuo:q75.awebp?rk3s=f64ab15b&x-expires=1746338115&x-signature=MogshhZ56PHOomUEA%2FANpyzzNyc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"什么?2025年了还在写传统爬虫!来试试更有爽感的AI爬虫 ψ(`∇´)ψ","url":"https://juejin.cn/post/7497785507742760999","content":"在数据抓取领域,传统爬虫一直占据着重要地位。传统爬虫基于规则进行数据抓取,通过识别网页中的特定元素,如类名、标签或结构,来定位和提取所需信息。然而,随着互联网的飞速发展,网站结构日益复杂且频繁变动,传统爬虫的局限性逐渐凸显。一旦网站更新,改变了原有的类名、标签或结构,传统爬虫就可能失效,导致数据抓取失败或错误。
\\n这时,AI 爬虫应运而生,为数据抓取带来了全新的解决方案。AI 爬虫借助人工智能技术,能够智能解析网页、自适应变化,展现出更高的灵活性和准确性。今天,就让我们一起来深入了解 AI 爬虫,并用实际代码来感受它的强大魅力。
\\n我们选用大家熟悉的豆瓣电影榜来进行示范,而爬虫则使用 x - crawl 这个结合了传统爬虫技术和 AI 算法的 Web 数据采集框架。它为开发者提供了灵活的接口和丰富的特性,非常适合用来爬取复杂多变的网站。
\\n首先,我们创建 index.mjs 文件,使用 ES6 语法进行导包,以便更好地进行后期维护。代码如下:
\\nimport { \\n createCrawl,\\n createCrawlOpenAI \\n} from \\"x - crawl\\";\\n
\\n这里从 x - crawl 库中导入了 createCrawl 和 createCrawlOpenAI。createCrawl 用于返回爬虫实例,createCrawlOpenAI 则是 openai 配置项。
\\n接下来,我们需要实例化爬虫应用:
\\nconst crawlApp = createCrawl({\\n maxRetry: 3, // 最大重试次数\\n intervalTime: { max: 2000, min: 1000 } // 重试间隔时间\\n});\\n
\\n通过 createCrawl 创建了一个名为 crawlApp 的实例对象,并设置了最大重试次数为 3 次,重试间隔时间在 1000 毫秒到 2000 毫秒之间。这样设置可以在网络不稳定等情况下,保证爬虫有一定的重试机会,并且合理控制重试间隔,避免对目标服务器造成过大压力。
\\n同时,我们还需要实例化 OpenAI:
\\nconst crawlOpenAI = createCrawlOpenAI({\\n clientOptions: { \\n apiKey: \\"sk - yourKey\\",\\n baseURL: \\"https://api.302.ai/v1/\\"\\n },\\n defaultModel:{\\n chatModel: \\"gpt - 4 - turbo - preview\\",\\n }\\n});\\n
\\n通过 createCrawlOpenAI 创建了 crawlOpenAI 实例对象。clientOptions 用于设置创建客户端时的一组配置选项,这里设置了 APIkey 和 baseURL 等基础选项。defaultModel 用于设置初始模型,我们选择了 chatModel 中的 gpt - 4 - turbo - preview 模型,这个模型能够帮助我们更好地处理网页内容的解析。
\\n一切准备就绪后,我们开始发送爬取请求:
\\ncrawlApp.crawlPage(\\"https://movie.douban.com/chart\\")\\n .then(async (res) => {\\n const { page, browser } = res.data;\\n const targetSelector = \\".indent\\";\\n await page.waitForSelector(targetSelector);\\n const highlyHTML = await page.$eval(targetSelector, \\n (el) => el.innerHTML\\n );\\n
\\n调用 crawlApp 上的 crawlPage 方法,打开豆瓣热门电影的网页并返回一个 Promise,其中包含页面对象和其他信息。使用 .then 来处理这个 Promise,当页面加载完成并且解析完 Promise,就会执行回调函数中的代码。这里的回调函数使用了 async 关键字,表示它是异步的,允许我们使用 await 来等待其他 Promise 操作。
\\n通过解构的方法从 res.data 中提取出 page 和 browser 对象。page 是一个 Puppeteer 页面实例,代表当前加载的网页;browser 则是浏览器实例,可用于管理整个浏览器会话。
\\n定义了一个 CSS 选择器字符串 \'.indent\',根据豆瓣电影排行榜页面的设计,.indent 可能是用来包裹电影列表内容的 HTML 元素。使用 page.waitForSelector() 方法等待直到页面中出现与 targetSelector 匹配的元素,确保在继续下一步之前,页面已经完全加载并渲染了所需的元素。
\\n然后使用 page.$eval,将选择器 targetSelector 和一个函数作为参数。它首先查找与给定选择器匹配的第一个元素,然后在这个元素上下文中执行提供的函数,并将结果返回给调用者。这里通过 el.innerHTML 获取了由 .indent 选择器选中的第一个元素内部的 HTML 内容,并将其赋值给变量 highlyHTML。
\\n接下来,我们利用 OpenAI 来解析提取到的 HTML 内容:
\\nconst result = await crawlOpenAI.parseElements( \\n highlyHTML,\\n `\\n 获取图片链接、电影名称、电影评分、电影简介,\\n 输出格式为json数组。\\n 例如:\\n [\\n {\\n \\"src\\": \\"图片链接\\",\\n \\"title\\": \\"电影名称\\",\\n \\"score\\": \\"电影评分\\",\\n \\"desc\\": \\"电影简介\\"\\n }\\n ]\\n `\\n)\\nbrowser.close();\\nconsole.log(result);\\n
\\n调用 crawlOpenAI 对象的 parseElements 方法,解析爬取的内容(highlyHTML)并提取出我们需要的图片链接、电影名称、电影评分、电影简介等信息,以 JSON 数组的形式返回。最后用 browser.close() 关闭浏览器,并将结果打印到控制台。
\\n如果我们还想保存爬取到的图片等文件,可以继续使用以下代码:
\\ncrawlApp.crawlFile({\\n targets: result.elements[0].src,\\n storeDirs: \\"./upload\\",\\n})\\n
\\n这里通过 crawlApp 的 crawlFile 方法,将结果中第一个元素的图片链接(result.elements[0].src)对应的文件保存到 ./upload 目录下。
\\n完整代码如下
\\n// es6 模块化 导包\\n// 解构运算符\\nimport { \\n createCrawl,\\n createCrawlOpenAI \\n} from \\"x-crawl\\";\\n\\n// 实例化爬虫\\nconst crawlApp = createCrawl({\\n maxRetry: 3, // 最大重试次数\\n intervalTime: { max: 2000 , min: 1000 }, // 重试间隔时间\\n});\\nconst crawlOpenAI = createCrawlOpenAI({\\n clientOptions: { \\n apiKey: \\"sk-yourKey\\" ,\\n baseURL: \\"https://api.302.ai/v1/\\"\\n },\\n defaultModel:{\\n chatModel: \\"gpt-4-turbo-preview\\",\\n }\\n});\\n\\ncrawlApp.crawlPage(\\"https://movie.douban.com/chart\\")\\n .then(async (res) => {\\n const {page,browser}=res.data;\\n const targetSelector = \\".indent\\";\\n await page.waitForSelector(targetSelector);\\n const highlyHTML = await page.$eval(targetSelector, \\n (el) => el.innerHTML\\n );\\n const result=await crawlOpenAI.parseElements( \\n highlyHTML,\\n `\\n 获取图片链接、电影名称、电影评分、电影简介,\\n 输出格式为json数组。\\n 例如:\\n [\\n {\\n \\"src\\": \\"图片链接\\",\\n \\"title\\": \\"电影名称\\",\\n \\"score\\": \\"电影评分\\",\\n \\"desc\\": \\"电影简介\\"\\n }\\n ]\\n \\n `\\n )\\n browser.close();\\n console.log(result);\\n crawlApp.crawlFile({\\n targets: result.elements[0].src,\\n storeDirs: \\"./upload\\",\\n })\\n })\\n\\n
\\n效果展示
\\n从上述代码示例中,我们可以初步窥探 AI 爬虫的原理。在传统爬虫获取网页 HTML 内容的基础上,AI 爬虫利用自然语言处理等人工智能技术来进一步处理这些内容。
\\n以我们使用的 x - crawl 和 OpenAI 结合的方式为例,当获取到网页特定部分的 HTML(如 highlyHTML)后,OpenAI 的模型(如 gpt - 4 - turbo - preview)能够理解我们以自然语言形式给出的指令(如 “获取图片链接、电影名称、电影评分、电影简介,输出格式为 json 数组”)。模型通过对 HTML 内容的语义分析,识别出其中与电影相关的元素,并按照我们要求的格式提取和整理信息。
\\n与传统爬虫固定的规则匹配方式不同,AI 爬虫的模型经过大量数据的训练,能够理解网页内容的语义和逻辑关系。即使网页结构发生变化,只要内容的语义不变,AI 爬虫依然有可能准确地提取出我们需要的数据。这就大大提高了爬虫的适应性和准确性,尤其是在面对复杂多变的现代网站时,优势更加明显。
\\n传统爬虫依赖固定规则,面对网站结构变化往往束手无策。而 AI 爬虫能根据语义理解网页,自动适应结构调整,无需频繁修改规则,大大提高了爬虫的灵活性。
\\n通过自然语言处理和语义分析,AI 爬虫能更精准地定位和提取所需信息,减少因网页结构复杂或不规范导致的数据提取错误,提高数据质量。
\\n对于开发者来说,不需要花费大量时间和精力去编写和维护复杂的规则匹配逻辑。使用 AI 爬虫框架,如 x - crawl,结合强大的 AI 模型,能够快速实现高效的数据抓取功能,降低了开发成本和难度。
\\n通过对 AI 爬虫的介绍、实际代码演示以及原理剖析,相信大家已经对 AI 爬虫有了更深入的了解。在 2025 年这个技术飞速发展的时代,AI 爬虫为我们的数据抓取工作带来了更高效、更智能的解决方案。
\\n随着人工智能技术的不断进步,我们有理由相信 AI 爬虫在未来会发挥更大的作用。它可能会在更多领域得到应用,如更精准的市场调研、更全面的舆情监测、更智能的搜索引擎优化等。同时,也期待更多强大的 AI 爬虫框架和工具出现,进一步推动数据抓取技术的发展。不妨现在就尝试使用 AI 爬虫,体验它带来的全新数据抓取爽感吧!
","description":"在数据抓取领域,传统爬虫一直占据着重要地位。传统爬虫基于规则进行数据抓取,通过识别网页中的特定元素,如类名、标签或结构,来定位和提取所需信息。然而,随着互联网的飞速发展,网站结构日益复杂且频繁变动,传统爬虫的局限性逐渐凸显。一旦网站更新,改变了原有的类名、标签或结构,传统爬虫就可能失效,导致数据抓取失败或错误。 这时,AI 爬虫应运而生,为数据抓取带来了全新的解决方案。AI 爬虫借助人工智能技术,能够智能解析网页、自适应变化,展现出更高的灵活性和准确性。今天,就让我们一起来深入了解 AI 爬虫,并用实际代码来感受它的强大魅力。\\n\\nAI 爬虫初体验…","guid":"https://juejin.cn/post/7497785507742760999","author":"天天扭码","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-27T04:10:24.210Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6005f0c3b1f24fd3a76d60fa88b79095~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSp5aSp5omt56CB:q75.awebp?rk3s=f64ab15b&x-expires=1746331864&x-signature=NVeos7e79u6C2PER%2FWw8pbEpT7g%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Node.js","爬虫","AI编程"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot中内置的49个常用工具类","url":"https://juejin.cn/post/7497173460423753740","content":"SpringBoot以其强大的自动配置和丰富的生态系统成为Java开发的首选框架。除了核心功能外,SpringBoot及其依赖的Spring框架还包含大量实用工具类,它们可以显著简化日常开发工作。本文将介绍49个常用工具类,并通过简洁的代码示例展示它们的基本用法。
\\nimport org.springframework.util.StringUtils;\\n\\n// 检查字符串是否为空\\nboolean isEmpty = StringUtils.isEmpty(null); // true\\nboolean isEmpty2 = StringUtils.isEmpty(\\"\\"); // true\\n\\n// 检查字符串是否有文本内容\\nboolean hasText = StringUtils.hasText(\\" \\"); // false\\nboolean hasText2 = StringUtils.hasText(\\"hello\\"); // true\\n\\n// 分割字符串\\nString[] parts = StringUtils.tokenizeToStringArray(\\"a,b,c\\", \\",\\");\\n\\n// 清除首尾空白\\nString trimmed = StringUtils.trimWhitespace(\\" hello \\"); // \\"hello\\"\\n
\\nimport org.springframework.util.AntPathMatcher;\\n\\nAntPathMatcher matcher = new AntPathMatcher();\\nboolean match1 = matcher.match(\\"/users/*\\", \\"/users/123\\"); // true\\nboolean match2 = matcher.match(\\"/users/**\\", \\"/users/123/orders\\"); // true\\nboolean match3 = matcher.match(\\"/user?\\", \\"/user1\\"); // true\\n\\n// 提取路径变量\\nMap<String, String> vars = matcher.extractUriTemplateVariables(\\n \\"/users/{id}\\", \\"/users/42\\"); // {id=42}\\n
\\nimport org.springframework.util.PatternMatchUtils;\\n\\nboolean matches1 = PatternMatchUtils.simpleMatch(\\"user*\\", \\"username\\"); // true\\nboolean matches2 = PatternMatchUtils.simpleMatch(\\"user?\\", \\"user1\\"); // true\\nboolean matches3 = PatternMatchUtils.simpleMatch(\\n new String[]{\\"user*\\", \\"admin*\\"}, \\"username\\"); // true\\n
\\nimport org.springframework.util.PropertyPlaceholderHelper;\\n\\nPropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(\\"${\\", \\"}\\");\\nProperties props = new Properties();\\nprops.setProperty(\\"name\\", \\"World\\");\\nprops.setProperty(\\"greeting\\", \\"Hello ${name}!\\");\\n\\nString result = helper.replacePlaceholders(\\"${greeting}\\", props::getProperty);\\n// \\"Hello World!\\"\\n
\\nimport org.springframework.util.CollectionUtils;\\n\\n// 检查集合是否为空\\nboolean isEmpty = CollectionUtils.isEmpty(null); // true\\nboolean isEmpty2 = CollectionUtils.isEmpty(Collections.emptyList()); // true\\n\\n// 集合操作\\nList<String> list1 = Arrays.asList(\\"a\\", \\"b\\", \\"c\\");\\nList<String> list2 = Arrays.asList(\\"b\\", \\"c\\", \\"d\\");\\nCollection<String> intersection = CollectionUtils.intersection(list1, list2); // [b, c]\\n\\n// 合并集合\\nList<String> target = new ArrayList<>();\\nCollectionUtils.mergeArrayIntoCollection(new String[]{\\"a\\", \\"b\\"}, target);\\n
\\nimport org.springframework.util.LinkedMultiValueMap;\\nimport org.springframework.util.MultiValueMap;\\n\\nMultiValueMap<String, String> map = new LinkedMultiValueMap<>();\\nmap.add(\\"colors\\", \\"red\\");\\nmap.add(\\"colors\\", \\"blue\\");\\nmap.add(\\"sizes\\", \\"large\\");\\n\\nList<String> colors = map.get(\\"colors\\"); // [red, blue]\\n
\\nimport org.springframework.util.ConcurrentReferenceHashMap;\\n\\n// 创建高并发场景下的引用Map (类似WeakHashMap但线程安全)\\nMap<String, Object> cache = new ConcurrentReferenceHashMap<>();\\ncache.put(\\"key1\\", new LargeObject());\\n
\\nimport org.springframework.util.SystemPropertyUtils;\\n\\n// 解析含系统属性的字符串\\nString javaHome = SystemPropertyUtils.resolvePlaceholders(\\"${java.home}\\");\\nString pathWithDefault = SystemPropertyUtils.resolvePlaceholders(\\n \\"${unknown.property:default}\\"); // \\"default\\"\\n
\\nimport org.springframework.util.ReflectionUtils;\\n\\n// 获取类的字段\\nField field = ReflectionUtils.findField(Person.class, \\"name\\");\\nReflectionUtils.makeAccessible(field);\\nReflectionUtils.setField(field, person, \\"John\\");\\n\\n// 调用方法\\nMethod method = ReflectionUtils.findMethod(Person.class, \\"setAge\\", int.class);\\nReflectionUtils.makeAccessible(method);\\nReflectionUtils.invokeMethod(method, person, 30);\\n\\n// 字段回调\\nReflectionUtils.doWithFields(Person.class, field -> {\\n System.out.println(field.getName());\\n});\\n
\\nimport org.springframework.util.ClassUtils;\\n\\n// 获取类名\\nString shortName = ClassUtils.getShortName(\\"org.example.MyClass\\"); // \\"MyClass\\"\\n\\n// 检查类是否存在\\nboolean exists = ClassUtils.isPresent(\\"java.util.List\\", null); // true\\n\\n// 获取所有接口\\nClass<?>[] interfaces = ClassUtils.getAllInterfaces(ArrayList.class);\\n\\n// 获取用户定义的类加载器\\nClassLoader classLoader = ClassUtils.getDefaultClassLoader();\\n
\\nimport org.springframework.util.MethodInvoker;\\n\\nMethodInvoker invoker = new MethodInvoker();\\ninvoker.setTargetObject(new MyService());\\ninvoker.setTargetMethod(\\"calculateTotal\\");\\ninvoker.setArguments(new Object[]{100, 0.2});\\ninvoker.prepare();\\nObject result = invoker.invoke();\\n
\\nimport org.springframework.beans.BeanUtils;\\n\\n// 复制属性\\nPerson source = new Person(\\"John\\", 30);\\nPerson target = new Person();\\nBeanUtils.copyProperties(source, target);\\n\\n// 实例化类\\nPerson newPerson = BeanUtils.instantiateClass(Person.class);\\n\\n// 查找方法\\nMethod method = BeanUtils.findMethod(Person.class, \\"setName\\", String.class);\\n
\\nimport org.springframework.util.FileCopyUtils;\\n\\n// 复制文件内容\\nbyte[] bytes = FileCopyUtils.copyToByteArray(new File(\\"input.txt\\"));\\nFileCopyUtils.copy(bytes, new File(\\"output.txt\\"));\\n\\n// 读取文本\\nString content = FileCopyUtils.copyToString(\\n new InputStreamReader(new FileInputStream(\\"input.txt\\"), \\"UTF-8\\"));\\n\\n// 流复制\\nFileCopyUtils.copy(inputStream, outputStream);\\n
\\nimport org.springframework.util.ResourceUtils;\\n\\n// 获取文件\\nFile file = ResourceUtils.getFile(\\"classpath:config.properties\\");\\n\\n// 检查是否是URL\\nboolean isUrl = ResourceUtils.isUrl(\\"http://example.com\\");\\n\\n// 获取URL\\nURL url = ResourceUtils.getURL(\\"classpath:data.json\\");\\n
\\nimport org.springframework.util.StreamUtils;\\n\\n// 流操作\\nbyte[] data = StreamUtils.copyToByteArray(inputStream);\\nString text = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);\\nStreamUtils.copy(inputStream, outputStream);\\nStreamUtils.copy(\\"Hello\\", StandardCharsets.UTF_8, outputStream);\\n
\\nimport org.springframework.util.FileSystemUtils;\\n\\n// 删除目录\\nboolean deleted = FileSystemUtils.deleteRecursively(new File(\\"/tmp/test\\"));\\n\\n// 复制目录\\nFileSystemUtils.copyRecursively(new File(\\"source\\"), new File(\\"target\\"));\\n
\\nimport org.springframework.core.io.Resource;\\nimport org.springframework.core.io.support.ResourcePatternUtils;\\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\\n\\n// 获取匹配资源\\nResource[] resources = ResourcePatternUtils.getResourcePatternResolver(\\n new PathMatchingResourcePatternResolver())\\n .getResources(\\"classpath*:META-INF/*.xml\\");\\n
\\nimport org.springframework.web.util.WebUtils;\\nimport javax.servlet.http.HttpServletRequest;\\n\\n// 获取Cookie\\nCookie cookie = WebUtils.getCookie(request, \\"sessionId\\");\\n\\n// 获取请求路径\\nString path = WebUtils.getLookupPathForRequest(request);\\n\\n// 从请求中获取参数\\nint pageSize = WebUtils.getIntParameter(request, \\"pageSize\\", 10);\\n
\\nimport org.springframework.web.util.UriUtils;\\n\\n// 编解码URI组件\\nString encoded = UriUtils.encodePathSegment(\\"path with spaces\\", \\"UTF-8\\");\\nString decoded = UriUtils.decode(encoded, \\"UTF-8\\");\\n
\\nimport org.springframework.web.util.UriComponentsBuilder;\\n\\n// 构建URI\\nURI uri = UriComponentsBuilder.fromHttpUrl(\\"http://example.com\\")\\n .path(\\"/products\\")\\n .queryParam(\\"category\\", \\"books\\")\\n .queryParam(\\"sort\\", \\"price\\")\\n .build()\\n .toUri();\\n
\\nimport org.springframework.web.util.ContentCachingRequestWrapper;\\nimport javax.servlet.http.HttpServletRequest;\\n\\nContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);\\n// 请求处理后\\nbyte[] body = wrapper.getContentAsByteArray();\\n
\\nimport org.springframework.web.util.HtmlUtils;\\n\\n// HTML转义\\nString escaped = HtmlUtils.htmlEscape(\\"<script>alert(\'XSS\')</script>\\");\\n// <script>alert(\'XSS\')</script>\\n\\n// HTML反转义\\nString unescaped = HtmlUtils.htmlUnescape(\\"<b>Bold</b>\\");\\n// <b>Bold</b>\\n
\\nimport org.springframework.util.Assert;\\n\\n// 常用断言\\nAssert.notNull(object, \\"Object must not be null\\");\\nAssert.hasText(name, \\"Name must not be empty\\");\\nAssert.isTrue(amount > 0, \\"Amount must be positive\\");\\nAssert.notEmpty(items, \\"Items must not be empty\\");\\nAssert.state(isInitialized, \\"Service is not initialized\\");\\n
\\nimport org.springframework.util.ObjectUtils;\\n\\n// 对象工具\\nboolean isEmpty = ObjectUtils.isEmpty(null); // true\\nboolean isEmpty2 = ObjectUtils.isEmpty(new String[0]); // true\\n\\nString nullSafe = ObjectUtils.nullSafeToString(null); // \\"null\\"\\nboolean equals = ObjectUtils.nullSafeEquals(obj1, obj2);\\n\\n// 默认值\\nString value = ObjectUtils.getOrDefault(null, \\"default\\");\\n
\\nimport org.springframework.util.NumberUtils;\\n\\n// 数字转换\\nInteger parsed = NumberUtils.parseNumber(\\"42\\", Integer.class);\\nDouble converted = NumberUtils.convertNumberToTargetClass(42, Double.class);\\n
\\nimport org.springframework.format.datetime.DateTimeFormatUtils;\\nimport java.util.Date;\\n\\n// 格式化日期\\nString formatted = DateTimeFormatUtils.getDateTimeInstance().format(new Date());\\n
\\nimport org.springframework.util.StopWatch;\\n\\n// 计时工具\\nStopWatch watch = new StopWatch(\\"TaskName\\");\\nwatch.start(\\"phase1\\");\\n// 执行任务1\\nThread.sleep(100);\\nwatch.stop();\\n\\nwatch.start(\\"phase2\\");\\n// 执行任务2\\nThread.sleep(200);\\nwatch.stop();\\n\\n// 输出报告\\nSystem.out.println(watch.prettyPrint());\\nSystem.out.println(\\"Total time: \\" + watch.getTotalTimeMillis() + \\"ms\\");\\n
\\nimport org.springframework.util.DigestUtils;\\n\\n// MD5哈希\\nString md5 = DigestUtils.md5DigestAsHex(\\"password\\".getBytes());\\n\\n// 文件MD5\\nString fileMd5;\\ntry (InputStream is = new FileInputStream(\\"file.txt\\")) {\\n fileMd5 = DigestUtils.md5DigestAsHex(is);\\n}\\n
\\nimport org.springframework.util.Base64Utils;\\n\\n// Base64编解码\\nbyte[] data = \\"Hello World\\".getBytes();\\nString encoded = Base64Utils.encodeToString(data);\\nbyte[] decoded = Base64Utils.decodeFromString(encoded);\\n
\\nimport org.springframework.security.crypto.encrypt.Encryptors;\\nimport org.springframework.security.crypto.encrypt.TextEncryptor;\\n\\n// 文本加密\\nString password = \\"secret\\";\\nString salt = \\"deadbeef\\";\\nTextEncryptor encryptor = Encryptors.text(password, salt);\\nString encrypted = encryptor.encrypt(\\"Message\\");\\nString decrypted = encryptor.decrypt(encrypted);\\n
\\nimport org.springframework.boot.json.JsonParser;\\nimport org.springframework.boot.json.JsonParserFactory;\\n\\n// JSON解析\\nJsonParser parser = JsonParserFactory.getJsonParser();\\nMap<String, Object> parsed = parser.parseMap(\\"{\\"name\\":\\"John\\", \\"age\\":30}\\");\\nList<Object> parsedList = parser.parseList(\\"[1, 2, 3]\\");\\n
\\nimport org.springframework.core.ResolvableType;\\n\\n// 类型解析\\nResolvableType type = ResolvableType.forClass(List.class);\\nResolvableType elementType = type.getGeneric(0);\\n\\n// 泛型类型处理\\nResolvableType mapType = ResolvableType.forClassWithGenerics(\\n Map.class, String.class, Integer.class);\\n
\\nimport org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;\\nimport com.fasterxml.jackson.databind.ObjectMapper;\\n\\n// 自定义JSON转换器\\nObjectMapper mapper = new ObjectMapper();\\n// 配置mapper\\nMappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper);\\n
\\nimport org.apache.commons.lang3.RandomStringUtils;\\n\\n// 随机字符串生成\\nString random = RandomStringUtils.randomAlphanumeric(10);\\nString randomLetters = RandomStringUtils.randomAlphabetic(8);\\nString randomNumeric = RandomStringUtils.randomNumeric(6);\\n
\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.List;\\nimport java.util.stream.Collectors;\\n\\n// 合并多个CompletableFuture结果\\nList<CompletableFuture<String>> futures = /* 多个异步操作 */;\\nCompletableFuture<List<String>> allResults = CompletableFuture.allOf(\\n futures.toArray(new CompletableFuture[0]))\\n .thenApply(v -> futures.stream()\\n .map(CompletableFuture::join)\\n .collect(Collectors.toList()));\\n
\\nimport org.springframework.http.CacheControl;\\nimport java.util.concurrent.TimeUnit;\\n\\n// 缓存控制\\nCacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS)\\n .noTransform()\\n .mustRevalidate();\\n\\nString headerValue = cacheControl.getHeaderValue();\\n
\\nimport org.springframework.core.annotation.AnnotationUtils;\\n\\n// 查找注解\\nComponent annotation = AnnotationUtils.findAnnotation(MyClass.class, Component.class);\\n\\n// 获取注解属性\\nString value = AnnotationUtils.getValue(annotation, \\"value\\").toString();\\n\\n// 合并注解\\nComponent mergedAnnotation = AnnotationUtils.synthesizeAnnotation(\\n annotation, MyClass.class);\\n
\\nimport org.springframework.core.convert.support.DefaultConversionService;\\n\\n// 类型转换\\nDefaultConversionService conversionService = new DefaultConversionService();\\nString strValue = \\"42\\";\\nInteger intValue = conversionService.convert(strValue, Integer.class);\\n
\\nimport org.springframework.http.HttpHeaders;\\n\\n// HTTP头处理\\nHttpHeaders headers = new HttpHeaders();\\nheaders.add(\\"Content-Type\\", \\"application/json\\");\\nheaders.setContentLength(1024);\\nheaders.setCacheControl(\\"max-age=3600\\");\\n
\\nimport org.springframework.http.MediaType;\\nimport org.springframework.http.MediaTypeFactory;\\nimport java.util.Optional;\\n\\n// 根据文件名推断媒体类型\\nOptional<MediaType> mediaType = MediaTypeFactory.getMediaType(\\"document.pdf\\");\\n// application/pdf\\n
\\nimport org.springframework.util.MimeTypeUtils;\\n\\n// MIME类型常量和解析\\nboolean isCompatible = MimeTypeUtils.APPLICATION_JSON.isCompatibleWith(\\n MimeTypeUtils.APPLICATION_JSON_UTF8);\\n
\\nimport org.springframework.web.reactive.function.client.WebClient;\\nimport org.springframework.web.reactive.function.client.ExchangeFilterFunction;\\n\\n// 创建WebClient\\nWebClient webClient = WebClient.builder()\\n .baseUrl(\\"https://api.example.com\\")\\n .defaultHeader(\\"Authorization\\", \\"Bearer token\\")\\n .filter(ExchangeFilterFunction.ofRequestProcessor(\\n clientRequest -> {\\n // 请求处理\\n return Mono.just(clientRequest);\\n }))\\n .build();\\n
\\nimport org.springframework.core.env.PropertySource;\\nimport org.springframework.core.env.StandardEnvironment;\\n\\n// 添加属性源\\nStandardEnvironment env = new StandardEnvironment();\\nMap<String, Object> map = new HashMap<>();\\nmap.put(\\"app.name\\", \\"MyApp\\");\\nenv.getPropertySources().addFirst(new MapPropertySource(\\"my-properties\\", map));\\n
\\nimport org.springframework.context.ApplicationEventPublisher;\\n\\n// 发布事件\\nApplicationEventPublisher publisher = /* 获取发布器 */;\\npublisher.publishEvent(new CustomEvent(\\"Something happened\\"));\\n
\\nimport org.springframework.context.i18n.LocaleContextHolder;\\nimport java.util.Locale;\\n\\n// 获取/设置当前语言环境\\nLocale currentLocale = LocaleContextHolder.getLocale();\\nLocaleContextHolder.setLocale(Locale.FRENCH);\\n
\\nimport org.springframework.aop.support.AopUtils;\\n\\n// AOP工具方法\\nboolean isAopProxy = AopUtils.isAopProxy(bean);\\nboolean isCglibProxy = AopUtils.isCglibProxy(bean);\\nClass<?> targetClass = AopUtils.getTargetClass(bean);\\n
\\nimport org.springframework.aop.framework.ProxyFactory;\\n\\n// 创建代理\\nProxyFactory factory = new ProxyFactory(targetObject);\\nfactory.addInterface(MyInterface.class);\\nfactory.addAdvice(new MyMethodInterceptor());\\nMyInterface proxy = (MyInterface) factory.getProxy();\\n
\\nimport org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;\\n\\n// 扫描类路径\\nClassPathScanningCandidateComponentProvider scanner = \\n new ClassPathScanningCandidateComponentProvider(true);\\nscanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));\\nSet<BeanDefinition> beans = scanner.findCandidateComponents(\\"org.example\\");\\n
\\nimport org.springframework.beans.factory.config.YamlPropertiesFactoryBean;\\nimport org.springframework.core.io.ClassPathResource;\\n\\n// 解析YAML文件\\nYamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();\\nyaml.setResources(new ClassPathResource(\\"application.yml\\"));\\nProperties properties = yaml.getObject();\\n
\\n这些工具类涵盖了Java开发中的大部分常见场景,从基础的字符串处理到高级的反射操作,从文件IO到安全加密,从Web开发到性能监控。熟练掌握这些工具类可以显著提高开发效率,减少样板代码,并帮助编写更健壮的应用程序。
\\n在日常开发中,建议养成查看Spring和SpringBoot文档的习惯,挖掘更多有用的工具类。
","description":"SpringBoot以其强大的自动配置和丰富的生态系统成为Java开发的首选框架。除了核心功能外,SpringBoot及其依赖的Spring框架还包含大量实用工具类,它们可以显著简化日常开发工作。本文将介绍49个常用工具类,并通过简洁的代码示例展示它们的基本用法。 字符串处理工具类\\n1. StringUtils\\nimport org.springframework.util.StringUtils;\\n\\n// 检查字符串是否为空\\nboolean isEmpty = StringUtils.isEmpty(null); // true\\nboolean is…","guid":"https://juejin.cn/post/7497173460423753740","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-26T23:55:55.151Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"并发编程太难写?这些新方法救了我🚀","url":"https://juejin.cn/post/7497230252017434675","content":"在日常的 Java 开发中,并发编程一直是一个让开发者头疼的问题 😩。随着系统规模的扩大,异步任务越来越多,如何管理和协调这些任务变得尤为复杂。错误处理、超时控制、以及异步任务间的依赖关系,常常让代码变得杂乱无章,维护起来也更加困难。
\\n不过,从 JDK 9 开始,随着 JEP 266(多并发更新)的引入,异步编排迎来了一个大的变革 ✨。本文将从常见的异步编排难点入手,讲解 JEP 266 给我们带来的改变,并展示一些新特性如何让异步编排变得更简洁高效。
\\n在 JDK 9 之前,处理异步任务的超时通常需要手动管理定时器。例如:
\\nExecutorService executor = Executors.newFixedThreadPool(2);\\nScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);\\n\\nCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> slowService(), executor);\\n\\nScheduledFuture<?> timeout = scheduler.schedule(() -> {\\n future.completeExceptionally(new TimeoutException(\\"Timeout after 2 seconds\\"));\\n}, 2, TimeUnit.SECONDS);\\n\\nfuture.whenComplete((result, ex) -> timeout.cancel(true));\\n
\\n如上所示,我们需要为每个异步任务手动创建超时逻辑,且需要关注错误处理、任务取消等细节。虽然Java8引入了CompletableFuture
,但是开发这仍需要面临以下这些困扰:
在异步任务中,超时是常见问题。如果没有一个好的超时控制机制,任务可能会无限期等待下去,导致程序卡死。以前,我们需要编写定时器,检查每个任务是否超时。
\\n例如:我们有一个支付服务,它通过异步调用第三方支付接口进行支付。为了避免支付过程中的死锁或长时间等待,我们通常需要设置超时。但是,如果没有超时控制机制,可能会发生支付请求卡住的情况。
\\n\\n但是在JDK8中我们只能进行超时异常中断:
import java.util.concurrent.CompletableFuture;\\n\\npublic class PaymentService {\\n\\n public static void main(String[] args) {\\n CompletableFuture<Void> paymentFuture = initiatePaymentAsync();\\n\\n // 这里我们想要进行支付的逻辑,但没有超时控制\\n paymentFuture.join(); // 如果支付请求没有成功,程序将永远卡在这里\\n System.out.println(\\"支付完成\\");\\n }\\n\\n public static CompletableFuture<Void> initiatePaymentAsync() {\\n return CompletableFuture.runAsync(() -> {\\n try {\\n // 模拟调用第三方支付接口\\n System.out.println(\\"正在向第三方支付接口发送请求...\\");\\n Thread.sleep(5000); // 模拟支付接口响应延迟\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n }\\n}\\n\\n
\\n异步编排通常涉及多个任务的顺序执行。例如,thenApply
, thenCompose
, whenComplete
等方法,虽然能够将异步操作串联起来,但也容易导致代码层级过多,逻辑混乱,维护成本增加。
例如有下面五个任务:
\\nTask 1:启动异步任务,返回一个初始值 5
。
Task 2:对任务结果进行处理,乘以 2。
\\nTask 3:继续处理,将结果加上 10
。
Task 4:使用 thenCompose
执行另一个异步任务,将之前的结果乘以 3
。
Task 5:继续处理,结果加上 20
。
whenComplete:在所有异步任务完成后,检查是否有错误,并输出最终结果。
\\n代码示例:
\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.concurrent.ExecutionException;\\n\\npublic class AsyncTaskExample {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n\\n CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n System.out.println(\\"Task 1: Start\\");\\n try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }\\n System.out.println(\\"Task 1: End\\");\\n return 5;\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 2: Start\\");\\n try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }\\n System.out.println(\\"Task 2: End\\");\\n return result * 2;\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 3: Start\\");\\n try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }\\n System.out.println(\\"Task 3: End\\");\\n return result + 10;\\n })\\n .thenCompose(result -> {\\n System.out.println(\\"Task 4: Start\\");\\n try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }\\n System.out.println(\\"Task 4: End\\");\\n return CompletableFuture.supplyAsync(() -> result * 3);\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 5: Start\\");\\n try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }\\n System.out.println(\\"Task 5: End\\");\\n return result + 20;\\n })\\n .whenComplete((result, throwable) -> {\\n if (throwable != null) {\\n System.out.println(\\"Error: \\" + throwable.getMessage());\\n } else {\\n System.out.println(\\"Final Result: \\" + result);\\n }\\n });\\n\\n // Block and get the final result\\n future.get();\\n }\\n}\\n
\\n异步编排中的错误处理往往容易被忽略,尤其是当任务链中的某一环节发生错误时,整个任务链可能会中断。如何优雅地捕获和处理异步任务中的异常,成为开发者的一大难题。
\\n例如:当任务链中的某一环节发生异常时,如果没有恰当的错误处理机制,整个任务链会中断,后续的任务无法继续执行,例如下面三个任务:
\\nRuntimeException
。Task 1
抛出了异常。Task 1
抛出异常时,exceptionally()
方法会捕获到该异常并处理它。在这个例子中,返回了一个默认值 -1
,继续执行后续任务。exceptionally()
返回了 -1
,Task 3
会继续执行,基于默认值进行计算。whenComplete()
来报告最终结果或错误。代码示例:
\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.concurrent.ExecutionException;\\n\\npublic class AsyncErrorHandling {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n \\n CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n System.out.println(\\"Task 1: Start\\");\\n if (true) {\\n throw new RuntimeException(\\"Something went wrong in Task 1\\");\\n }\\n return 5;\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 2: Start\\");\\n return result * 2;\\n })\\n .exceptionally(ex -> {\\n System.out.println(\\"Caught exception in Task 2: \\" + ex.getMessage());\\n return -1; // return a fallback value in case of an error\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 3: Start\\");\\n return result + 10;\\n })\\n .whenComplete((result, throwable) -> {\\n if (throwable != null) {\\n System.out.println(\\"Error during the chain: \\" + throwable.getMessage());\\n } else {\\n System.out.println(\\"Completed with result: \\" + result);\\n }\\n });\\n\\n // Block and get the final result\\n Integer finalResult = future.get();\\n System.out.println(\\"Final Result: \\" + finalResult);\\n }\\n}\\n
\\n当多个异步任务同时执行时,如果没有合理的流量控制,可能会导致资源的过度消耗,甚至出现内存泄漏或性能瓶颈。**背压(Backpressure)**管理就是为了应对这种情况,它要求开发者能灵活控制数据流的速度。
\\n假设我们有多个异步任务需要执行,每个任务都需要从数据库或远程服务获取数据。如果这些任务执行得太快,可能会造成数据库连接池或网络带宽耗尽,导致性能瓶颈,甚至崩溃。
\\n问题场景:
\\n示例代码:
\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.concurrent.ExecutorService;\\nimport java.util.concurrent.Executors;\\nimport java.util.concurrent.TimeUnit;\\n\\npublic class BackpressureExample {\\n\\n public static void main(String[] args) throws InterruptedException {\\n ExecutorService executor = Executors.newFixedThreadPool(5); // 限制最大并发任务数为 5\\n CompletableFuture<Void>[] futures = new CompletableFuture[10]; // 模拟 10 个任务\\n\\n for (int i = 0; i < 10; i++) {\\n int taskId = i;\\n futures[i] = CompletableFuture.supplyAsync(() -> {\\n System.out.println(\\"Task \\" + taskId + \\": Start\\");\\n simulateDatabaseQuery(); // 模拟数据库查询\\n return \\"Data from task \\" + taskId;\\n }, executor)\\n .thenApply(result -> {\\n System.out.println(\\"Task \\" + taskId + \\": Processing \\" + result);\\n return result;\\n })\\n .exceptionally(ex -> {\\n System.out.println(\\"Task \\" + taskId + \\": Exception - \\" + ex.getMessage());\\n return null;\\n });\\n }\\n\\n // 等待所有任务完成\\n CompletableFuture.allOf(futures).join();\\n\\n // 关闭线程池\\n executor.shutdown();\\n }\\n\\n private static void simulateDatabaseQuery() {\\n try {\\n // 模拟数据库查询时间\\n TimeUnit.SECONDS.sleep(2); // 假设每个查询需要 2 秒\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n }\\n }\\n}\\n
\\nJDK 9 通过 JEP 266 引入了一系列新的并发编程特性,这些特性在减少代码复杂度、增强异步流控制、以及提升性能等方面,提供了有效的支持。
\\nCompletableFuture
异步能力大增 🏃♂️JDK 9 增强了 CompletableFuture
的能力,新增了多个方法,让我们更容易控制异步任务的执行:
orTimeout
:为异步任务设置超时。如果任务超时,会自动抛出 TimeoutException
⚡️。completeOnTimeout
:如果任务超时,会自动返回给定的默认值,而不是抛出异常 💡。delayedExecutor
:支持延迟执行异步任务,省去了手动编写定时器的麻烦 ⏰。这些新方法直接简化了以前我们需要手动编写的超时管理和错误处理代码,极大地提高了开发效率。
\\n最后我们一张图总结一下:
\\nJEP 266 还引入了 java.util.concurrent.Flow
,为 Java 提供了官方的异步流处理支持。这个类库遵循 Reactive Streams 标准,提供了对数据流的正式支持:
通过这种标准化的方式,开发者不再需要自己设计复杂的 API,而是能直接使用统一接口来处理异步流。
\\n一张图总结Flow api功能:
\\nJDK 9 对并发集合(例如 ConcurrentHashMap
)进行了优化,减少了内部锁的使用,从而提高了性能。这对于高并发环境中的异步任务非常有帮助,减少了线程竞争,提高了吞吐量 🚀。
java 9 的 JEP 266 对并发编程引入了一些新特性,极大地简化了异步编排过程,让开发者可以更轻松地管理异步任务,避免了传统方式中的许多繁琐操作。在 JDK 9 后,通过 CompletableFuture
的新方法(如 orTimeout
)和 Flow API
,异步任务的管理变得更加简洁,减少了手动编写的繁琐代码。
CompletableFuture
的新方法 📝JDK 9 中,CompletableFuture
新增了以下方法来处理异步任务的超时和错误:
orTimeout
:为异步任务设置超时。如果超时,任务会自动报错。这使得开发者不再需要编写额外的超时检查代码。
示例:
\\nCompletableFuture.supplyAsync(() -> slowService())\\n .orTimeout(2, TimeUnit.SECONDS)\\n .exceptionally(ex -> fallback());\\n
\\ncompleteOnTimeout
:如果超时,自动返回给定的值,而不是抛出异常。这适用于对超时不做特殊处理的情况。
示例:
\\nCompletableFuture.supplyAsync(() -> slowService())\\n .completeOnTimeout(\\"Timeout Result\\", 2, TimeUnit.SECONDS);\\n
\\ndelayedExecutor
:延迟执行任务,不再需要手动实现定时器。
示例:
\\nCompletableFuture.supplyAsync(() -> slowService(), \\n CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));\\n
\\n所以当我们再遇到并发任务编排的场景时,我们便可以像这样编写避免任务链中断:
\\nimport java.util.concurrent.CompletableFuture;\\nimport java.util.concurrent.ExecutionException;\\nimport java.util.concurrent.TimeUnit;\\n\\npublic class AsyncErrorHandlingJDK9 {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n \\n CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n System.out.println(\\"Task 1: Start\\");\\n if (true) {\\n throw new RuntimeException(\\"Something went wrong in Task 1\\");\\n }\\n return 5;\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 2: Start\\");\\n return result * 2;\\n })\\n .exceptionally(ex -> {\\n System.out.println(\\"Caught exception in Task 2: \\" + ex.getMessage());\\n return -1; // return a fallback value in case of an error\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Task 3: Start\\");\\n return result + 10;\\n })\\n .orTimeout(2, TimeUnit.SECONDS) // Set a timeout for the entire chain\\n .completeOnTimeout(-1, 2, TimeUnit.SECONDS) // Provide a fallback result on timeout\\n .whenComplete((result, throwable) -> {\\n if (throwable != null) {\\n System.out.println(\\"Error during the chain: \\" + throwable.getMessage());\\n } else {\\n System.out.println(\\"Completed with result: \\" + result);\\n }\\n });\\n\\n // Block and get the final result\\n Integer finalResult = future.get();\\n System.out.println(\\"Final Result: \\" + finalResult);\\n }\\n}\\n\\n
\\n通过 Flow API,Java 提供了标准化的异步流处理方式。当我们遇到并发查询数据库时便可以这样编写:
\\nimport java.util.concurrent.Flow.*;\\nimport java.util.concurrent.SubmissionPublisher;\\nimport java.util.concurrent.TimeUnit;\\n\\npublic class FlowBackpressureExample {\\n\\n public static void main(String[] args) throws InterruptedException {\\n // 创建一个 SubmissionPublisher 作为 Publisher\\n SubmissionPublisher<String> publisher = new SubmissionPublisher<>();\\n\\n // 创建一个 Subscriber,控制流量\\n Subscriber<String> subscriber = new Subscriber<>() {\\n private Subscription subscription;\\n\\n @Override\\n public void onSubscribe(Subscription subscription) {\\n this.subscription = subscription;\\n subscription.request(1); // 初始请求 1 个数据\\n }\\n\\n @Override\\n public void onNext(String item) {\\n System.out.println(\\"Received: \\" + item);\\n simulateDatabaseQuery(); // 模拟处理时间\\n subscription.request(1); // 请求下一个数据\\n }\\n\\n @Override\\n public void onError(Throwable throwable) {\\n System.err.println(\\"Error occurred: \\" + throwable.getMessage());\\n }\\n\\n @Override\\n public void onComplete() {\\n System.out.println(\\"All data has been processed.\\");\\n }\\n };\\n\\n // 订阅 Publisher\\n publisher.subscribe(subscriber);\\n\\n // 发布 10 个任务\\n for (int i = 1; i <= 10; i++) {\\n publisher.submit(\\"Data \\" + i);\\n }\\n\\n // 等待任务完成\\n TimeUnit.SECONDS.sleep(5); // 等待一段时间确保任务被处理完\\n publisher.close();\\n }\\n\\n // 模拟数据库查询,假设每个查询需要 2 秒钟\\n private static void simulateDatabaseQuery() {\\n try {\\n TimeUnit.SECONDS.sleep(2);\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n }\\n }\\n}\\n\\n
\\n通过flow api,我们便可以精确背压:
\\n背压控制:通过 Subscription.request(n)
来控制订阅者每次可以处理的数据量。这里使用了 request(1)
,意味着每次订阅者只能处理一个数据项,确保任务不会过多并行执行。
流速的调节:在消费者处理数据时,request(1)
会确保当一个任务处理完毕之后,才会继续处理下一个任务。这保证了在高并发环境中,系统不会因为任务过载而崩溃。
这种方式使得我们能够灵活地处理异步数据流,同时有效避免了传统方式中的复杂操作。
\\nJEP 266 带来的新特性,不仅让异步编排变得更加简洁和标准化,还提高了代码的可维护性和性能。通过使用 orTimeout
、Flow API
等新方法,我们能够更加方便地管理异步任务、错误处理以及超时控制。
如果你还没有使用这些新特性,建议从简单的 orTimeout
和 Flow API
开始,逐步提升你的异步编排能力。相信你会发现,异步编排再也不需要那么痛苦了! 🎉
想了解更多 JDK 新特性,提升开发效率?欢迎关注我的专栏 👉 JDK 新特性专栏!一起来变得更强大吧 🚀
","description":"在日常的 Java 开发中,并发编程一直是一个让开发者头疼的问题 😩。随着系统规模的扩大,异步任务越来越多,如何管理和协调这些任务变得尤为复杂。错误处理、超时控制、以及异步任务间的依赖关系,常常让代码变得杂乱无章,维护起来也更加困难。 不过,从 JDK 9 开始,随着 JEP 266(多并发更新)的引入,异步编排迎来了一个大的变革 ✨。本文将从常见的异步编排难点入手,讲解 JEP 266 给我们带来的改变,并展示一些新特性如何让异步编排变得更简洁高效。\\n\\n并发编程的常见难点 😖\\n\\n在 JDK 9 之前,处理异步任务的超时通常需要手动管理定时器。例如:…","guid":"https://juejin.cn/post/7497230252017434675","author":"橙序员小站","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-26T15:45:17.980Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/195f360c07a94447b3b030f28301f31d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qmZ5bqP5ZGY5bCP56uZ:q75.awebp?rk3s=f64ab15b&x-expires=1746287116&x-signature=sphFDLSoVi1xGKMOmHh11JX4Wic%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88a720733b704ad493d9fe12990f21ec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qmZ5bqP5ZGY5bCP56uZ:q75.awebp?rk3s=f64ab15b&x-expires=1746287116&x-signature=4%2F8qbAOWCBKJEXPBof5y1M4hGXQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://juejin.cn/","type":"photo"}],"categories":["后端","Java","面试"],"attachments":null,"extra":null,"language":null},{"title":"Java Streams 中的7个常见错误","url":"https://juejin.cn/post/7497170890083024933","content":"在使用 Java Streams 时,以下是一些常见的错误:
\\n错误:忘记调用终止操作(如collect()
、forEach()
或reduce()
),这会导致流没有执行。
public static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n\\n // 创建流但没有调用终止操作\\n names.stream()\\n .filter(name -> name.startsWith(\\"A\\")); // 这里没有调用终止操作\\n\\n // 由于流没有执行,什么都不会打印\\n System.out.println(\\"Stream operations have not been executed.\\");\\n}\\n\\n
\\n解决方案:始终以终止操作结束,以触发流的处理。
\\npublic static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n\\n // 创建流并调用终止操作\\n names.stream()\\n .filter(name -> name.startsWith(\\"A\\")) // 中间操作\\n .forEach(System.out::println); // 终止操作\\n\\n // 这将打印 \\"Alice\\",因为流被执行了\\n}\\n\\n
\\n错误:在处理流时修改源数据结构(如List
)可能导致未知的结果。
public static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n // 尝试在流处理时修改源列表\\n names.stream()\\n .filter(name -> {\\n if (name.startsWith(\\"B\\")) {\\n names.remove(name); // 修改源列表\\n }\\n return true;\\n })\\n .forEach(System.out::println);\\n // 由于并发修改,输出可能不符合预期\\n System.out.println(\\"Remaining names: \\" + names);\\n}\\n\\n
\\n解决方案:不要在流操作期间修改源数据,而是使用流创建新的集合。
\\npublic static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n // 基于过滤结果创建一个新列表\\n List<String> filteredNames = names.stream()\\n .filter(name -> name.startsWith(\\"B\\")) // 过滤出以 \'B\' 开头的名字\\n .collect(Collectors.toList());\\n // 显示过滤后的列表\\n System.out.println(\\"Filtered names: \\" + filteredNames);\\n System.out.println(\\"Original names remain unchanged: \\" + names);\\n}\\n\\n
\\n错误:认为并行流总是能提高性能,而不考虑上下文,例如小数据集或轻量级操作。
\\npublic static void main(String[] args) {\\n List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 小数据集\\n // 在小数据集上使用并行流\\n numbers.parallelStream()\\n .map(n -> {\\n // 模拟轻量级操作\\n System.out.println(Thread.currentThread().getName() + \\" processing: \\" + n);\\n return n * n;\\n })\\n .forEach(System.out::println);\\n // 输出可能显示为简单任务创建了不必要的线程\\n}\\n\\n
\\n解决方案:谨慎使用并行流,尤其是对于大数据集的 CPU 密集型任务。
\\npublic static void main(String[] args) {\\n List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000) // 大数据集\\n .boxed()\\n .collect(Collectors.toList());\\n // 在大数据集上使用并行流进行 CPU 密集型操作\\n List<Integer> squareNumbers = numbers.parallelStream()\\n .map(n -> {\\n // 模拟 CPU 密集型操作\\n return n * n;\\n })\\n .collect(Collectors.toList());\\n // 打印前 10 个结果\\n System.out.println(\\"First 10 squared numbers: \\" + squareNumbers.subList(0, 10));\\n}\\n\\n
\\n错误:链式调用过多的中间操作(如filter()
和map()
)可能会引入性能开销。
public static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\", \\"Eve\\");\\n\\n // 过度使用中间操作\\n List<String> result = names.stream()\\n .filter(name -> name.startsWith(\\"A\\")) // 第一个中间操作\\n .filter(name -> name.length() > 3) // 第二个中间操作\\n .map(String::toUpperCase) // 第三个中间操作\\n .map(name -> name + \\" is a name\\") // 第四个中间操作\\n .toList(); // 终端操作\\n\\n // 输出结果\\n System.out.println(result);\\n}\\n\\n
\\n解决方案:尽量减少流管道中的中间操作,并在可能的情况下使用流融合。
\\npublic static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\", \\"Eve\\");\\n\\n // 优化流管道\\n List<String> result = names.stream()\\n .filter(name -> name.startsWith(\\"A\\") && name.length() > 3) // 将过滤器合并为一个\\n .map(name -> name.toUpperCase() + \\" is a name\\") // 合并 map 操作\\n .toList(); // 终端操作\\n\\n // 输出结果\\n System.out.println(result);\\n}\\n\\n
\\n错误:在使用findFirst()
或reduce()
等操作时,没有正确处理Optional
结果。
public static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\");\\n // 尝试查找以 \\"Z\\" 开头的名字(不存在)\\n String firstNameStartingWithZ = names.stream()\\n .filter(name -> name.startsWith(\\"Z\\")) \\n .findFirst() // 返回一个 Optional\\n .get(); // 如果 Optional 为空,这将抛出 NoSuchElementException\\n // 输出结果\\n System.out.println(firstNameStartingWithZ);\\n}\\n\\n
\\n解决方案:在访问Optional
的值之前,始终检查它是否存在,以避免NoSuchElementException
。
public static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\");\\n // 正确处理 Optional\\n Optional<String> firstNameStartingWithZ = names.stream()\\n .filter(name -> name.startsWith(\\"Z\\")) \\n .findFirst(); // 返回一个 Optional\\n // 检查 Optional 是否存在\\n if (firstNameStartingWithZ.isPresent()) {\\n System.out.println(firstNameStartingWithZ.get());\\n } else {\\n System.out.println(\\"No name starts with \'Z\'\\");\\n }\\n}\\n\\n
\\n错误:在并行流中使用共享的可变状态可能导致竞态条件和不一致的结果。
\\npublic static void main(String[] args) {\\n List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);\\n List<Integer> results = new ArrayList<>(); // 共享的可变状态\\n // 在并行流中使用共享的可变状态\\n numbers.parallelStream().forEach(number -> {\\n results.add(number * 2); // 这可能导致竞态条件\\n });\\n // 输出结果\\n System.out.println(\\"Results: \\" + results);\\n}\\n\\n
\\n解决方案:避免共享可变状态;使用线程安全的集合或局部变量。
\\npublic static void main(String[] args) {\\n List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);\\n List<Integer> results = new CopyOnWriteArrayList<>(); // 线程安全的集合\\n // 在并行流中使用线程安全的集合\\n numbers.parallelStream().forEach(number -> {\\n results.add(number * 2); // 避免竞态条件\\n });\\n // 输出结果\\n System.out.println(\\"Results: \\" + results);\\n}\\n\\n
\\n错误:不清楚中间操作(返回新流)和终止操作(产生结果)之间的区别。
\\npublic static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n // 错误:尝试将中间操作用作终止操作\\n // 这将无法编译,因为 \'filter\' 返回一个 Stream,而不是一个 List\\n names.stream().filter(name -> name.startsWith(\\"A\\")).forEach(System.out::println); // 这里正确使用了终止操作\\n}\\n\\n
\\n解决方案:熟悉每种操作类型的特性,以避免代码中的逻辑错误。
\\npublic static void main(String[] args) {\\n List<String> names = Arrays.asList(\\"Alice\\", \\"Bob\\", \\"Charlie\\", \\"David\\");\\n // 正确使用中间操作和终止操作\\n List<String> filteredNames = names.stream()\\n .filter(name -> name.startsWith(\\"A\\")) // 中间操作\\n .collect(Collectors.toList()); // 终止操作\\n\\n // 输出过滤后的名字\\n System.out.println(\\"Filtered Names: \\" + filteredNames);\\n}\\n\\n
\\n通过掌握这些技巧并实施这些解决方案,你可以更好地使用 Java Streams,并编写更简洁、更高效的代码。
","description":"在使用 Java Streams 时,以下是一些常见的错误: 1.不使用终止操作\\n\\n错误:忘记调用终止操作(如collect()、forEach()或reduce()),这会导致流没有执行。\\n\\npublic static void main(String[] args) {\\n ListChatClient是Spring AI中一个 “人狠话不多” 的组件,它能让你用几行代码就能与GPT、BERT等AI大模型“搭讪”。它的核心定位是 “流畅API客户端” ,专治各种“复杂交互恐惧症”——比如提示词模板、聊天记忆、输出解析等组件的协同难题。
\\n想象你要和AI聊天,需要:
\\nChatClient.Builder
:\\n@RestController\\npublic class JokeController {\\n private final ChatClient chatClient;\\n public JokeController(ChatClient.Builder builder) {\\n this.chatClient = builder.build();\\n }\\n @GetMapping(\\"/joke\\")\\n String getJoke() {\\n return chatClient.prompt().user(\\"讲个冷笑话\\").call().content();\\n }\\n}\\n
\\nspring.ai.chat.client.enabled=false
),手动绑定ChatModel
:\\nChatClient chatClient = ChatClient.create(myChatModel); // myChatModel需自行注入\\n
\\ncall().content()
直接返回字符串响应。entity()
方法将AI返回的JSON自动转换为Java类:\\nrecord Joke(String setup, String punchline) {}\\nJoke joke = chatClient.prompt().user(\\"讲个冷笑话\\").call().entity(Joke.class);\\n
\\nstream().content()
返回Flux<String>
,避免“卡死”界面:\\nFlux<String> jokeStream = chatClient.prompt().user(\\"讲个长篇笑话\\").stream().content();\\n
\\n通过ChatClient.Builder
设置默认系统提示,比如让AI永远用“海盗口吻”回答:
@Configuration\\npublic class PirateConfig {\\n @Bean\\n ChatClient pirateChatClient(ChatClient.Builder builder) {\\n return builder.defaultSystem(\\"你是一个海盗,所有回答必须带\'哟吼!\'\\").build();\\n }\\n}\\n
\\nChatClient内部像是一个导演,协调多个“演员”完成一场AI对话大戏:
\\n举个🌰:当你调用.entity(Joke.class)
时,幕后其实是结构化输出转换器(如BeanOutputConverter
)在悄悄把AI的文本“翻译”成Java对象。
对比维度 | ChatClient | 原子API(ChatModel/Message) |
---|---|---|
代码复杂度 | 低(链式调用,封装细节) | 高(需手动处理提示词、解析响应) |
灵活性 | 中(适合常见场景) | 高(可定制每一步逻辑) |
适用场景 | 快速开发、标准化交互 | 复杂业务逻辑、深度定制需求 |
学习成本 | 低(半小时上手) | 高(需理解底层组件) |
总结:
\\nstream()
时,记得用异步处理(如WebFlux),否则界面会“卡成PPT”。defaultSystem
后,某些接口可能不需要系统提示,记得用.system(null)
覆盖。ChatResponse
中的getTokenUsage()
能帮你算钱(钱包守护者必备)。@Configuration
类中预定义常用提示(如客服话术),减少重复代码。@ControllerAdvice
统一捕获AI模型超时、令牌不足等异常,返回友好错误提示。stream()
,结合缓存(如Redis)避免重复调用AI模型。this
,形成方法链,如prompt().user().call()
)Flux
实现非阻塞)StructuredOutputConverter
)ChatClient的终极奥义是 “把复杂留给框架,把简洁留给开发者” 。无论是智能客服、游戏NPC,还是教育辅导,它都能让你快速集成AI能力。记住:不要重复造轮子,除非你想成为下一个“轮子哥”!
","description":"Spring AI中的ChatClient:从入门到精通,一篇搞定! 一、ChatClient 是什么?——你的AI对话“翻译官”\\n\\nChatClient是Spring AI中一个 “人狠话不多” 的组件,它能让你用几行代码就能与GPT、BERT等AI大模型“搭讪”。它的核心定位是 “流畅API客户端” ,专治各种“复杂交互恐惧症”——比如提示词模板、聊天记忆、输出解析等组件的协同难题。\\n\\n举个栗子🌰:\\n\\n想象你要和AI聊天,需要:\\n\\n拼接提示词模板;\\n管理历史对话;\\n解析返回的JSON;\\n处理流式响应...\\n ChatClient一出手…","guid":"https://juejin.cn/post/7497128873752510501","author":"都叫我大帅哥","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-26T06:15:23.831Z","media":null,"categories":["后端","Spring","AI编程","Java"],"attachments":null,"extra":null,"language":null},{"title":"线程池隐患解析:为何阿里巴巴拒绝 Executors","url":"https://juejin.cn/post/7497173891811393563","content":"\\"生产环境又双叒出问题了!\\"—— 这样的消息在 Java 开发团队的群里太常见了。排查日志发现,服务器 CPU 飙升 100%,内存不断增长最终 OOM。罪魁祸首竟是一行看似无害的代码:Executors.newCachedThreadPool()
。
在高并发业务场景,这种通过 Executors 创建线程池的方式频繁引发灾难。线程数暴增、内存溢出、请求堆积、响应超时...这也是为什么阿里巴巴 Java 开发手册将\\"禁止使用 Executors 创建线程池\\"列为强制规定。
\\n为什么简单几行代码会埋下如此大隐患?今天,我们就来揭开 Executors 工具类背后的风险,并学习如何正确创建线程池。
\\nExecutors 提供了几种快捷创建线程池的静态工厂方法:
\\n// 固定大小线程池\\nExecutorService fixedPool = Executors.newFixedThreadPool(10);\\n\\n// 缓存线程池\\nExecutorService cachedPool = Executors.newCachedThreadPool();\\n\\n// 单线程线程池\\nExecutorService singlePool = Executors.newSingleThreadExecutor();\\n\\n// 定时任务线程池\\nScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);\\n
\\n乍看挺方便,一行代码解决问题。但正如我的老师常说的:\\"Java 中没有真正的捷径,所有的便利都有代价。\\"
\\n要理解 Executors 的问题,首先得掌握线程池的核心工作流程。以下是 ThreadPoolExecutor 的 execute()方法核心逻辑(简化后):
\\npublic void execute(Runnable command) {\\n if (command == null)\\n throw new NullPointerException();\\n\\n int c = ctl.get();\\n // 1. 如果运行的线程少于corePoolSize,创建新线程\\n if (workerCountOf(c) < corePoolSize) {\\n if (addWorker(command, true))\\n return;\\n c = ctl.get();\\n }\\n // 2. 如果达到核心线程数,尝试将任务加入队列\\n if (isRunning(c) && workQueue.offer(command)) {\\n // 二次检查\\n int recheck = ctl.get();\\n if (!isRunning(recheck) && remove(command))\\n reject(command);\\n else if (workerCountOf(recheck) == 0)\\n addWorker(null, false);\\n }\\n // 3. 如果队列已满,尝试创建非核心线程\\n else if (!addWorker(command, false))\\n // 4. 如果线程数达到最大值,执行拒绝策略\\n reject(command);\\n}\\n
\\n线程池执行任务的基本流程如下:
\\n阿里巴巴 Java 开发手册明确指出:
\\n\\n\\n【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
\\n
下面我们逐一分析 Executors 各类线程池的具体问题。
\\n来看看 newFixedThreadPool 的源码:
\\npublic static ExecutorService newFixedThreadPool(int nThreads) {\\n return new ThreadPoolExecutor(nThreads, nThreads,\\n 0L, TimeUnit.MILLISECONDS,\\n new LinkedBlockingQueue<Runnable>());\\n}\\n
\\n这里的关键问题是new LinkedBlockingQueue<Runnable>()
,注意没有传入队列容量参数!这会导致:
下面是一个模拟 OOM 的示例代码:
\\npublic class ExecutorsOOMDemo {\\n public static void main(String[] args) {\\n ExecutorService executorService = Executors.newFixedThreadPool(10);\\n\\n for (int i = 0; i < 10000; i++) { // 有限任务数,足以演示问题\\n executorService.execute(() -> {\\n try {\\n // 模拟任务执行时间比提交时间长\\n Thread.sleep(10000);\\n // 占用内存\\n byte[] data = new byte[1024 * 1024]; // 1MB\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n }\\n\\n executorService.shutdown(); // 演示结束后释放资源\\n }\\n}\\n
\\n运行上面的代码,很快就会看到:
\\nException in thread \\"main\\" java.lang.OutOfMemoryError: Java heap space\\n
\\n再来看看 newCachedThreadPool 的源码:
\\npublic static ExecutorService newCachedThreadPool() {\\n return new ThreadPoolExecutor(0, Integer.MAX_VALUE,\\n 60L, TimeUnit.SECONDS,\\n new SynchronousQueue<Runnable>());\\n}\\n
\\n注意这个线程池的特殊之处:
\\n这导致了一个严重问题:每来一个新任务都会创建一个新线程,直到系统资源耗尽!
\\n下面是 CachedThreadPool 的实际工作流程:
\\ngraph TB\\n A[新任务提交] --\x3e B{\\"当前线程数 < 核心线程数(0)?\\"}\\n B --\x3e|是| D[创建新的核心线程]\\n B --\x3e|否| C{\\"SynchronousQueue能否直接交付?\\"}\\n C --\x3e|\\"是,有空闲线程\\"| F[空闲线程执行]\\n C --\x3e|\\"否,无空闲线程\\"| E{\\"当前线程数 < 最大线程数(Integer.MAX_VALUE)?\\"}\\n E --\x3e|是| H[创建新的非核心线程]\\n E --\x3e|否| G[执行拒绝策略]\\n\\n style B fill:#f9f,stroke:#333\\n style C fill:#f9f,stroke:#333\\n style E fill:#f9f,stroke:#333\\n
\\n由于 SynchronousQueue 特性(没有存储能力,需要直接交付给线程),几乎所有新任务都会走\\"创建新线程\\"路径!在高并发下,可能创建成千上万的线程,导致:
\\nnewScheduledThreadPool
的源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {\\n return new ScheduledThreadPoolExecutor(corePoolSize);\\n}\\n\\n// ScheduledThreadPoolExecutor构造函数\\npublic ScheduledThreadPoolExecutor(int corePoolSize) {\\n super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());\\n}\\n
\\n这个线程池的问题:
\\n每种 Executors 工厂方法使用不同队列,直接影响线程池行为:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n队列类型 | 特点 | 用于 Executors 方法 | 风险点 |
---|---|---|---|
LinkedBlockingQueue (无界) | 默认容量为 Integer.MAX_VALUE | newFixedThreadPool newSingleThreadExecutor | 任务堆积导致 OOM |
SynchronousQueue | 无存储空间,直接交付 | newCachedThreadPool | 高并发时创建过多线程 |
DelayedWorkQueue | 无界延迟队列 | newScheduledThreadPool | 定时任务堆积可能 OOM |
ArrayBlockingQueue | 有界队列,基于数组 | Executors 不使用 建议手动使用 | 队列满后触发拒绝策略 |
PriorityBlockingQueue | 优先级队列,无界 | Executors 不使用 | 任务堆积可能 OOM |
队列选择决定线程池行为:
\\n如 CPU 有 N 个核心,且任务几乎无等待时间,那么最优线程数 ≈ N。原因:更多线程会导致上下文切换开销,反而降低效率。
\\ngraph LR\\n A[CPU核心数] --\x3e B[最优线程数]\\n B --\x3e C{N或N+1}\\n C --\x3e|完全CPU密集| D[N]\\n C --\x3e|略有IO| E[N+1]\\n
\\n假设:
\\n则线程数 = N * (1 + W/T)
\\n推导过程:
\\n举例:CPU 有 8 核,任务 80%时间在 IO 等待,则线程数 = 8 _ (1 + 0.8/0.2) = 8 _ 5 = 40
\\n队列容量 = 每秒任务量 × 平均执行时间 × 预留系数(1.5-2)
\\n举例:系统每秒 500 任务,任务平均执行 0.2 秒,预留系数 1.5:\\n队列容量 = 500 × 0.2 × 1.5 = 150
\\n根据不同业务场景的线程池配置示例:
\\n// CPU密集型任务线程池\\nThreadPoolExecutor cpuIntensivePool = new ThreadPoolExecutor(\\n Runtime.getRuntime().availableProcessors(), // 核心线程数 = CPU核心数\\n Runtime.getRuntime().availableProcessors() + 1, // 最大线程数略大于核心线程数\\n 60L, TimeUnit.SECONDS, // 空闲线程存活时间\\n new ArrayBlockingQueue<>(200), // 有界队列,防止OOM\\n new ThreadFactory() {\\n private final AtomicInteger counter = new AtomicInteger(1);\\n @Override\\n public Thread newThread(Runnable r) {\\n Thread thread = new Thread(r);\\n thread.setName(\\"order-cpu-\\" + counter.getAndIncrement()); // 业务标识+类型+序号\\n return thread;\\n }\\n },\\n new ThreadPoolExecutor.CallerRunsPolicy() // 调用者运行策略,起到限流作用\\n);\\n\\n// IO密集型任务线程池 - 使用自定义ThreadFactory\\nint cpuCores = Runtime.getRuntime().availableProcessors();\\ndouble blockingCoefficient = 0.8; // 假设任务80%时间在IO等待\\nint ioThreads = (int)(cpuCores / (1 - blockingCoefficient));\\n\\nThreadPoolExecutor ioIntensivePool = new ThreadPoolExecutor(\\n ioThreads,\\n ioThreads,\\n 60L, TimeUnit.SECONDS,\\n new LinkedBlockingQueue<>(500), // 队列容量根据业务量估算\\n new ThreadFactory() {\\n private final AtomicInteger counter = new AtomicInteger(1);\\n @Override\\n public Thread newThread(Runnable r) {\\n Thread thread = new Thread(r);\\n thread.setName(\\"payment-io-\\" + counter.getAndIncrement());\\n return thread;\\n }\\n },\\n new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:直接抛出异常\\n);\\n\\n// IO密集型任务线程池 - 使用Guava的ThreadFactoryBuilder\\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\\n\\n// 依赖: com.google.guava:guava:32.1.3-jre (适用于Java 11+)\\nThreadPoolExecutor guavaThreadPool = new ThreadPoolExecutor(\\n ioThreads,\\n ioThreads,\\n 60L, TimeUnit.SECONDS,\\n new LinkedBlockingQueue<>(500),\\n new ThreadFactoryBuilder()\\n .setNameFormat(\\"api-pool-%d\\")\\n .setUncaughtExceptionHandler((t, e) -> logger.error(\\"线程异常\\", e))\\n .build(),\\n new ThreadPoolExecutor.AbortPolicy()\\n);\\n
\\n拒绝策略触发条件:工作队列已满且线程数达到 maximumPoolSize
\\n这意味着:
\\nnewFixedThreadPool
,队列永远不会满,拒绝策略永远不会触发SynchronousQueue
的newCachedThreadPool
,队列容量为 0 但最大线程数几乎无限,拒绝策略几乎不会触发四种标准拒绝策略的应用场景:
\\n适用场景:订单提交、支付等关键业务
\\n代码示例:
\\n// 在调用方捕获并处理异常\\ntry {\\n orderProcessPool.submit(orderTask);\\n} catch (RejectedExecutionException e) {\\n logger.error(\\"订单处理线程池已满,订单号:\\" + orderId, e);\\n // 降级处理:写入本地文件或MQ重试\\n saveToRetryQueue(orderTask);\\n}\\n
\\n适用场景:非关键任务,如监控数据上报
\\n代码示例:
\\n// 提前检查线程池状态,决定是否降级\\nif (monitorPool.getQueue().size() > THRESHOLD) {\\n // 队列接近满,预见性降级,不提交低优先级数据\\n return;\\n}\\nmonitorPool.execute(monitorTask);\\n
\\n对于同时包含 CPU 计算和 IO 操作的混合型任务,有两种优化方式:
\\n// 主任务拆分提交\\nvoid processOrder(Order order) {\\n // CPU密集型任务(计算价格、校验等)提交到CPU池\\n Future<OrderVerifyResult> verifyFuture = cpuPool.submit(() -> {\\n return verifyAndCalculate(order);\\n });\\n\\n // IO密集型任务(数据库查询、远程调用)提交到IO池\\n Future<OrderEnrichData> enrichFuture = ioPool.submit(() -> {\\n return queryExternalSystems(order);\\n });\\n\\n // 合并结果\\n try {\\n OrderVerifyResult verify = verifyFuture.get(1, TimeUnit.SECONDS);\\n OrderEnrichData enrich = enrichFuture.get(2, TimeUnit.SECONDS);\\n // 最终处理...\\n } catch (Exception e) {\\n // 超时或异常处理\\n }\\n}\\n
\\nFork/Join 框架专为可分解的递归任务设计,如归并排序、树遍历等分治算法,不适合普通独立任务。
\\n// 仅适用于可递归分解的任务(如大数据集分片处理)\\nclass OrderTask extends RecursiveTask<OrderResult> {\\n private Order order;\\n private int threshold = 1000; // 分解阈值\\n\\n @Override\\n protected OrderResult compute() {\\n // 任务足够小时直接处理\\n if (order.getItems().size() <= threshold) {\\n return processDirectly(order);\\n }\\n\\n // 分解任务为两部分\\n List<OrderItem> firstHalf = order.getItems().subList(0, order.getItems().size()/2);\\n List<OrderItem> secondHalf = order.getItems().subList(order.getItems().size()/2, order.getItems().size());\\n\\n Order firstOrder = new Order(firstHalf);\\n Order secondOrder = new Order(secondHalf);\\n\\n // 并行处理子任务\\n OrderTask firstTask = new OrderTask(firstOrder);\\n OrderTask secondTask = new OrderTask(secondOrder);\\n\\n firstTask.fork(); // 异步执行\\n OrderResult secondResult = secondTask.compute(); // 当前线程执行\\n OrderResult firstResult = firstTask.join(); // 获取结果\\n\\n // 合并结果\\n return mergeResults(firstResult, secondResult);\\n }\\n}\\n\\n// 使用Fork/Join池\\nForkJoinPool forkJoinPool = new ForkJoinPool(\\n Runtime.getRuntime().availableProcessors());\\nOrderResult result = forkJoinPool.invoke(new OrderTask(order));\\n
\\n// 动态调整核心线程数\\npublic void adjustThreadPool(ThreadPoolExecutor executor, int queueSize) {\\n int currentCoreSize = executor.getCorePoolSize();\\n int currentQueueSize = executor.getQueue().size();\\n\\n // CPU利用率获取(通过JMX)\\n OperatingSystemMXBean osMxBean = ManagementFactory.getPlatformMXBean(\\n com.sun.management.OperatingSystemMXBean.class);\\n double cpuUsage = osMxBean.getSystemCpuLoad() * 100;\\n\\n // 队列接近饱和且CPU利用率不高,增加线程数\\n if (currentQueueSize > queueSize * 0.8 && cpuUsage < 70) {\\n int newCoreSize = Math.min(currentCoreSize + 2, MAX_POOL_SIZE);\\n executor.setCorePoolSize(newCoreSize);\\n logger.info(\\"线程池扩容:\\" + currentCoreSize + \\" -> \\" + newCoreSize);\\n }\\n // 队列使用率低且线程池线程较多,减少线程数\\n else if (currentQueueSize < queueSize * 0.2 && currentCoreSize > MIN_POOL_SIZE) {\\n int newCoreSize = Math.max(currentCoreSize - 1, MIN_POOL_SIZE);\\n executor.setCorePoolSize(newCoreSize);\\n logger.info(\\"线程池缩容:\\" + currentCoreSize + \\" -> \\" + newCoreSize);\\n }\\n}\\n\\n// 也可使用开源库如Micrometer获取CPU指标\\n// 依赖: io.micrometer:micrometer-registry-prometheus:1.10.0\\n
\\n很多开发者认为增加线程数可以提高并发能力,但实际上:
\\n// 错误示例:盲目设置大量线程\\nThreadPoolExecutor wrongPool = new ThreadPoolExecutor(\\n 100, 200, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)\\n);\\n\\n// 正确示例:根据任务特性计算线程数\\nint optimalThreads = calculateOptimalThreads(); // 基于CPU核心数和任务IO比例\\nThreadPoolExecutor rightPool = new ThreadPoolExecutor(\\n optimalThreads, optimalThreads, 60L, TimeUnit.SECONDS,\\n new LinkedBlockingQueue<>(1000)\\n);\\n
\\n过大的队列容量会导致:
\\n// 错误示例:使用过大队列\\nThreadPoolExecutor wrongPool = new ThreadPoolExecutor(\\n 10, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100000)\\n);\\n\\n// 正确示例:使用合理队列大小+拒绝策略\\nThreadPoolExecutor rightPool = new ThreadPoolExecutor(\\n 10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500),\\n new ThreadPoolExecutor.CallerRunsPolicy() // 通过拒绝策略限流\\n);\\n
\\n应用关闭时未正确关闭线程池会导致:
\\n// 正确的线程池关闭方式\\n@PreDestroy // Spring生命周期注解\\npublic void shutdown() {\\n // 停止接收新任务,等待已提交任务完成\\n executorService.shutdown();\\n\\n try {\\n // 等待现有任务结束\\n if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {\\n // 取消当前执行的任务\\n executorService.shutdownNow();\\n // 等待任务取消响应\\n if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {\\n logger.error(\\"线程池未能完全关闭\\");\\n }\\n }\\n } catch (InterruptedException ie) {\\n // 重新取消当前线程进行中断\\n executorService.shutdownNow();\\n Thread.currentThread().interrupt();\\n }\\n}\\n
\\nExecutors 方法 | 使用的队列和线程数 | 风险场景 | 替代方案 |
---|---|---|---|
newFixedThreadPool | 无界 LinkedBlockingQueue 固定线程数 | 任务堆积导致 OOM | 有界 ArrayBlockingQueue + 自定义线程池 |
newCachedThreadPool | SynchronousQueue 无限制最大线程数 | 瞬时高并发导致线程爆炸 | 限制最大线程数 + 合适队列大小 |
newSingleThreadExecutor | 无界 LinkedBlockingQueue 单线程 | 任务堆积 + 无法调参 | 核心线程=1 的可配置 ThreadPoolExecutor |
newScheduledThreadPool | DelayedWorkQueue 无限制最大线程数 | 定时任务太多导致线程暴增 | 限制最大线程数的 ScheduledThreadPoolExecutor |
阿里巴巴禁止使用 Executors 创建线程池是有充分理由的。线程池配置不当会导致严重后果:从任务堆积、响应超时,到系统崩溃、服务不可用。作为开发者,应该:
\\n在单体应用中,一般直接用表的自增ID或者UUID作为唯一标识。业务体量增大在分库分表之后,如何生成一个全局唯一的ID,就是一个关键的问题。
\\n通常情况下,对于分布式ID来说,我们一般希望他具有以下几个特点:
\\n● 全局唯一:必须保证全局唯一性,这个是最基本的要求。
\\n● 高性能&高可用:需要保证ID的生成是稳定且高效的。
\\n● 递增:根据不同的业务情况,有的会要求生成的ID呈递增趋势,也有的要求必须单调递增(后一个ID必须比前一个大),也有的没有严格要求。
\\n具体的分布式ID有以下这些方案:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 全局唯一 | 高性能 / 高可用 | 递增性 / 有序性 | 依赖组件 | 适用场景 |
---|---|---|---|---|---|
UUID | ✅ | 本地生成,高性能 | 无序 | 无 | 无需有序性的通用场景 |
数据库自增 | ✅ | 单点瓶颈,低可用 | 严格递增 | 单库单表 | 低并发、强依赖递增的场景 |
号段模式 | ✅ | 减少数据库访问 | 趋势递增(非严格) | 数据库 | 中高并发、可接受 ID 不连续场景 |
Redis | ✅ | 集群支持高可用 | 严格递增 | Redis 集群 | 高并发、需严格递增的场景 |
雪花算法 | ✅ | 单机高性能 | 严格递增 | 时钟同步 | 高并发、需有序性的场景 |
UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。
\\n标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),共32个字符,通常由以下几部分的组合而成:当前日期和时间,时钟序列,全局唯一的IEEE机器识别号
\\n优点: UUID的优点就是他的性能比较高,不依赖网络,本地就可以生成,使用起来也比较简单。
\\n缺点:长度过长和没有任何含义。一旦使用它作为全局唯一标识,就意味着在日后的问题排查和开发调试过程中会遇到很大的困难。
\\n不适合范围查询、不方便展示以及查询效率低等问题。
分布式ID也可以使用数据库的自增ID,但是这种实现中就要求一定是一个单库单表才能保证ID自增且不重复.
\\n号段模式是在数据库的基础上,为了解决性能问题而产生的一种方案。他的意思就是每次去数据库中取ID的时候取出来一批,并放在缓存中,然后下一次生成新ID的时候就从缓存中取。这一批用完了再去数据库中拿新的。
\\n而为了防止多个实例之间发生冲突,需要采用号段的方式,即给每个客户端发放的时候按号段分开,如客户端A取的号段是1-1000,客户端B取的是1001-2000,客户端C取的是2001-3000。当客户端A用完之后,再来取的时候取到的是3001-4000。
\\n\\n\\n其实很多分库分表的中间件的主键ID的生成,主要采用的也是号段模式,如TDDL Sequence
\\n
基于数据库可以实现,那么基于Redis也是可以的,我们可以依赖Redis的incr命令实现ID的原子性自增。R
\\n雪花算法(Snowflake)是由Twitter研发的一种分布式ID生成算法,它可以生成全局唯一且递增的ID。它的核心思想是将一个64位的ID划分成多个部分,每个部分都有不同的含义,包括时间戳、数据中心标识、机器标识和序列号等。
\\n雪花算法生成的ID由以下几个部分组成:
\\n是百度开源基于 Java 语言实现的唯一 ID 生成器,是在雪花算法 Snowflake 的基础上做了一些改进,如支持自定义位数和生成策略等。
\\n优点
\\n缺点
\\n兼具数据库号段模式和雪花算法两种方式,用户可以同时开启两种方式,也可以指定开启某种方式,能够根据不同业务场景灵活切换。
\\n优点
\\n缺点
\\n是滴滴用 Java 开发的一款分布式 ID 生成系统,基于数据库号段算法实现,扩展了 leaf - segment 算法,支持了多 db(master),同时提供了 java - client(SDK)使 ID 生成本地化,获得了更好的性能与可用性
\\n优点
\\n缺点
\\n分布式 ID 生成的核心是平衡 唯一性、性能、有序性 与 系统依赖。\\nUUID 和 数据库自增 是入门方案,适合简单场景;\\n雪花算法及其变种(Uidgenerator、Leaf) 是中高频选择,兼顾性能与有序性;\\nTinyid 和 Redis 则面向极致性能需求,但需接受 ID 不连续或缓存依赖。
\\n实际选型时,需结合业务规模、技术栈和未来扩展规划,对系统性能要求或者扩展性高的可优先选择成熟开源方案(如 Leaf、Tinyid),避免重复造轮子。
程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。”
\\n架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。”
\\n小李:“那您的意思是,现在可以了?”
\\n老王:“也不能这么说。性能、数据安全、运维复杂度,这些都是需要考虑的。不过,已经有不少公司在生产环境里用 Docker 跑 MySQL 了,效果还不错。”
\\nDocker(鲸鱼)+MySQL(海豚)到底如何,我们来具体看看:
\\n我们来看看业界使用情况:
\\n刘风才是京东的资深数据库专家,他分享了京东在MySQL数据库Docker化方面的实践经验。京东从最初的小规模使用,到现在超过70%的MySQL数据库运行在Docker容器中。
\\n当然京东也不是所有的业务都适合把 mysql 部署在 docker 容器中。比如,
\\n刘风才演讲中也提出:数据文件多于1T多的情况下是不太合适部署在Docker上的;再有就是在性能上要求特别高的,特别重要的核心系统目前仍跑在物理机上,后面随着Docker技术不断的改进,会陆续地迁到Docker上。
\\n同程艺龙的机票事业群 CTO 王晓波在QCon北京2018大会上做了《MySQL的Docker容器化大规模实践》的主题演讲。他分享了同程艺龙如何大规模实践基于Docker的MySQL私有云平台,集成了高可用、快速部署、自动化备份、性能监控等多项自动化运维功能。该平台支撑了总量90%以上的MySQL服务(实际数量超过2000个),资源利用率提升了30倍,数据库交付能力提升了70倍,并经受住了业务高峰期的考验。
\\n当然不仅仅是京东、同程像阿里云、腾讯、字节、美团等都有把 Mysql 部署在 Docker 容器中的案例。
\\nMySql 官方文档提供了 mysql 的 docker 部署方式,文档中并没有明确的表明这种方式是适用于开发、测试或生产。那就是通用性的,也就是说生产也可以使用。
\\n以下就是安装的脚本可以看到配置文件和数据都是挂载到宿主机上。
\\ndocker run --name=mysql1 \\\\\\n--mount type=bind,src=/path-on-host-machine/my.cnf,dst=/etc/my.cnf \\\\\\n--mount type=bind,src=/path-on-host-machine/datadir,dst=/var/lib/mysql \\\\\\n-d container-registry.oracle.com/mysql/community-server:tag\\n
\\n再看看镜像文件,可以看到 oralce 官方 7 年前就发布了 mysql5.7 的镜像。
\\n反方观点:生产环境MySQL不该部署在Docker里
\\n反方主要担心数据持久化、性能、复杂性、备份恢复和安全性等问题,觉得在Docker里跑MySQL风险挺大。
\\n正方观点:生产环境MySQL可以部署在Docker里
\\n正方则认为Docker的灵活性、可移植性、资源隔离、自动化管理以及社区支持都挺好,生产环境用Docker部署MySQL是可行的,而且有成熟的解决方案来应对数据持久化和性能等问题。
\\n争议的焦点主要在于Docker容器会不会影响性能。其实 Docker和虚拟机不一样,虚拟机是模拟物理机硬件,而Docker是基于Linux内核的cgroups和namespaces技术,实现了CPU、内存、网络和I/O的共享与隔离,性能损失很小。
\\nDocker 和传统虚拟化方式的不同之处,在于 Docker 是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。
\\nDocker的特点:
\\nDocker虚拟化操作系统而不是硬件
\\n随着技术的发展,Docker在数据库部署中的应用可能会越来越多。
\\n所以,生产环境在Docker里部署MySQL,虽然有争议,但大厂都在用,官方也支持,技术也在不断进步,未来可能是个趋势。
\\n我是栈江湖,如果你喜欢此文章,不要忘记点赞+关注!
","description":"程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。” 架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。”\\n\\n小李:“那您的意思是,现在可以了?”\\n\\n老王:“也不能这么说。性能、数据安全、运维复杂度,这些都是需要考虑的。不过,已经有不少公司在生产环境里用 Docker 跑 MySQL 了,效果还不错。”\\n\\nDocker(鲸鱼)+MySQL(海豚)到底如何,我们来具体看看:\\n\\n一、业界大厂\\n\\n我们来看看业界使用情况:\\n\\n1…","guid":"https://juejin.cn/post/7497057694530502665","author":"栈江湖","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T12:51:03.467Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50486e8859c543b9b20b364432a74a5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1746794774&x-signature=rwfuYylSRYJwiyhT8gWw6KXpGT0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/11a068f8a15e4f7e9d95adfcae9664dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1746794774&x-signature=%2BjRDT5sjRY7uDXDIsc%2FucIUvT1U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d371ecc2e7a462d90f4717f935dcaeb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1746794774&x-signature=CvtfU5EIpCz8eaw2WjuCB%2BMQtXA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a944640f861e4c6d93ddcd2a6ec70221~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1746794774&x-signature=fCTJqcM%2BwPm4gax0IxfZDIYYqtE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0173d66fab284407a8f48340934d48f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1746794774&x-signature=9SISfL5A8Rt12trt%2BL6ywME6Qi4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Docker","MySQL"],"attachments":null,"extra":null,"language":null},{"title":"WebSocket深度剖析:实时通信的终极解决方案实践指南","url":"https://juejin.cn/post/7497057694530387977","content":"JAVASCRIPT\\n// 传统轮询示例(石器时代的通信方式)\\nsetInterval(() => {\\n fetch(\'/check-update\')\\n .then(response => response.json())\\n .then(handleUpdate)\\n}, 5000); // 每隔5秒轮询一次\\n
\\n痛点分析:
\\n协议对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n维度 | HTTP | WebSocket |
---|---|---|
连接方式 | 短连接 | 长连接 |
数据流向 | 单向 | 双向 |
首部开销 | 800-2000字节 | 2-10字节 |
适用场景 | 文档传输 | 实时交互 |
握手过程:
\\nHTTP\\nGET /chat HTTP/1.1\\nHost: server.example.com\\nUpgrade: websocket\\nConnection: Upgrade\\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\nSec-WebSocket-Version: 13\\n\\nHTTP/1.1 101 Switching Protocols\\nUpgrade: websocket\\nConnection: Upgrade\\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\\n
\\nTEXT\\n 0 1 2 3\\n 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\\n+-+-+-+-+-------+-+-------------+-------------------------------+\\n|F|R|R|R| opcode|M| Payload len | Extended payload length |\\n|I|S|S|S| (4) |A| (7) | (16/64) |\\n|N|V|V|V| |S| | (if payload len==126/127) |\\n| |1|2|3| |K| | |\\n+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +\\n| Extended payload length continued, if payload len == 127 |\\n+ - - - - - - - - - - - - - - - +-------------------------------+\\n| |Masking-key, if MASK set to 1 |\\n+-------------------------------+-------------------------------+\\n| Masking-key (continued) | Payload Data |\\n+-------------------------------- - - - - - - - - - - - - - - - +\\n: Payload Data continued ... :\\n+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +\\n| Payload Data continued ... |\\n+---------------------------------------------------------------+\\n
\\n关键字段:
\\nJAVASCRIPT\\n// 客户端心跳发送\\nsetInterval(() => {\\n ws.send(heartbeatPacket); // 发送PING帧\\n}, 30000);\\n\\n// 服务端心跳响应\\nfunction handlePing() {\\n sendPong(); // 返回PONG帧\\n}\\n
\\nJAVA\\n@Configuration\\n@EnableWebSocket\\npublic class WebSocketConfig implements WebSocketConfigurer {\\n\\n @Override\\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\\n registry.addHandler(new LiveCommentHandler(), \\"/live/comment\\")\\n .setAllowedOrigins(\\"*\\")\\n .addInterceptors(new AuthInterceptor());\\n }\\n\\n @Bean\\n public ServletServerContainerFactoryBean createWebSocketContainer() {\\n ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();\\n container.setMaxTextMessageBufferSize(8192);\\n container.setMaxBinaryMessageBufferSize(8192);\\n container.setMaxSessionIdleTimeout(600000L);\\n return container;\\n }\\n}\\n\\npublic class LiveCommentHandler extends TextWebSocketHandler {\\n \\n private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();\\n \\n @Override\\n public void afterConnectionEstablished(WebSocketSession session) {\\n sessions.put(session.getId(), session);\\n }\\n \\n @Override\\n protected void handleTextMessage(WebSocketSession session, TextMessage message) {\\n // 处理聊天消息\\n broadcast(message.getPayload());\\n }\\n \\n private void broadcast(String message) {\\n sessions.values().parallelStream().forEach(s -> {\\n try {\\n if (s.isOpen()) {\\n s.sendMessage(new TextMessage(message));\\n }\\n } catch (IOException e) {\\n // 处理异常\\n }\\n });\\n }\\n}\\n
\\nJAVASCRIPT\\nclass LiveChat extends React.Component {\\n constructor() {\\n this.state = { messages: [] };\\n this.ws = new WebSocket(\'wss://api.example.com/live/comment\');\\n }\\n\\n componentDidMount() {\\n this.ws.onmessage = (event) => {\\n this.setState(prev => ({\\n messages: [...prev.messages, event.data]\\n }));\\n };\\n \\n this.ws.onclose = () => {\\n console.log(\'连接断开,尝试重连...\');\\n setTimeout(() => this.reconnect(), 3000);\\n };\\n }\\n\\n sendMessage = (text) => {\\n this.ws.send(JSON.stringify({\\n content: text,\\n userId: this.state.userId,\\n timestamp: Date.now()\\n }));\\n };\\n\\n reconnect = () => {\\n // 指数退避重连策略\\n this.ws = new WebSocket(this.ws.url);\\n };\\n}\\n
\\nJAVA\\n// 使用Netty构建WebSocket服务器\\npublic class WebSocketServer {\\n public static void main(String[] args) {\\n EventLoopGroup bossGroup = new NioEventLoopGroup();\\n EventLoopGroup workerGroup = new NioEventLoopGroup();\\n \\n try {\\n ServerBootstrap b = new ServerBootstrap();\\n b.group(bossGroup, workerGroup)\\n .channel(NioServerSocketChannel.class)\\n .childHandler(new WebSocketInitializer());\\n \\n Channel ch = b.bind(8080).sync().channel();\\n ch.closeFuture().sync();\\n } finally {\\n bossGroup.shutdownGracefully();\\n workerGroup.shutdownGracefully();\\n }\\n }\\n}\\n
\\nJAVA\\n// 使用Disruptor实现高性能消息队列\\npublic class MessageDisruptor {\\n private static final int BUFFER_SIZE = 1024;\\n private final Disruptor<MessageEvent> disruptor;\\n \\n public MessageDisruptor() {\\n this.disruptor = new Disruptor<>(\\n MessageEvent::new,\\n BUFFER_SIZE,\\n DaemonThreadFactory.INSTANCE\\n );\\n \\n disruptor.handleEventsWith(this::processMessage);\\n disruptor.start();\\n }\\n \\n private void processMessage(MessageEvent event, long sequence, boolean endOfBatch) {\\n // 消息处理逻辑\\n }\\n \\n public void publish(Message message) {\\n disruptor.publishEvent((event, seq) -> event.set(message));\\n }\\n}\\n
\\nTEXT\\nwss:// 协议配置(Nginx):\\nserver {\\n listen 443 ssl;\\n server_name ws.example.com;\\n \\n ssl_certificate /path/to/cert.pem;\\n ssl_certificate_key /path/to/key.pem;\\n \\n location / {\\n proxy_pass http://backend;\\n proxy_http_version 1.1;\\n proxy_set_header Upgrade $http_upgrade;\\n proxy_set_header Connection \\"upgrade\\";\\n }\\n}\\n
\\nJAVASCRIPT\\n// 消息签名验证(HMAC-SHA256)\\nfunction signMessage(message, secret) {\\n const hmac = crypto.createHmac(\'sha256\', secret);\\n hmac.update(message);\\n return hmac.digest(\'hex\');\\n}\\n\\n// 客户端发送前签名\\nconst payload = JSON.stringify(data);\\nconst signature = signMessage(payload, SECRET_KEY);\\nws.send(`${signature}|${payload}`);\\n\\n// 服务端验证签名\\nfunction verifyMessage(raw) {\\n const [signature, payload] = raw.split(\'|\');\\n return signMessage(payload, SECRET_KEY) === signature;\\n}\\n
\\n性能指标对比(单服务器):
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n协议 | 并发连接数 | 消息延迟 | 吞吐量 |
---|---|---|---|
HTTP轮询 | 5,000 | 200-5000ms | 50 msg/s |
WebSocket | 100,000 | <50ms | 1,000,000 msg/s |
SSE | 10,000 | <100ms | 100,000 msg/s |
未来展望:随着WebTransport协议的演进,WebSocket将与QUIC协议深度融合,实现更高效的实时通信。在元宇宙、云游戏等新兴领域,WebSocket将持续发挥关键作用。
\\n最佳实践建议:
\\n笔者之前,分别写过两篇关于Semantic Kernel(下简称SK)相关的博客,最近模型上下文协议(下称MCP)大火,实际上了解过SK的小伙伴,一看到 MCP的一些具体呈现,会发现,Client 调用 Server的方式,和SK调用插件的过程很像,实际操作了一下,发现确实是可以的。
\\n也就是说,如果我们之前的项目里用到SK做过Agent相关的模块,如今也可以丝滑的让其充当MCP Client的角色,去使用更多MCP生态的东西,而不需要做更多的改动。
\\n虽然SK是为AI Agent的发展而诞生的,但好框架就是好框架,没想到它和MCP也这么契合。
\\n本篇,建立在《再尝Semantic Kernel,Planning特性很香》的基础上,再次扩展一下MCP相关的介绍。
\\n\\n\\n注意:本篇不会深入介绍MCP相关的概念,架构等等前置内容,主要还是通过案例说明SK和MCP Server之间的联系,建议不熟悉MCP相关内容的小伙伴,先登录MCP官网进行了解。
\\n
在MCP的官方介绍文档里已经有C#的官方SDK了,这里我们参照它官方的例子,先来一个MCP Server。
\\n\\n\\nTips:官方的案例已经非常简洁和完善了,建议没搞过MCP的小伙伴上手试一下,虽然案例很简单,一看就能明白,但那真正跑通的获得感,还是得自己动手试一下才能体会到。
\\n
这一步我觉得大家还是直接看官方文档更清楚,我这里不在赘述
\\n传送门👉:modelcontextprotocol.io/quickstart/…
\\n需要注意的是,我这里的Server是使用SSE的传输方式。
\\n目前SSE的方式官方已经声明会逐步被Streamable Http的形式替代,但目前还是Built-in状态,本地调试的话还可以使用stdio的方式,这也是Claude Desktop,Cline之类客户端工具支持的方式,这点大家按需设定即可,这部分内容可以参考这里👉:mcp-framework.com/docs/Transp…。
\\n定义一个class,然后标记上MCPServer的特定属性,这部分官网也有介绍,我就直接上代码了
\\n[McpServerToolType]\\npublic static class WeatherTools\\n{\\n [McpServerTool(Name = \\"GetWeather\\"), Description(\\"获取当前城市的天气\\")]\\n public static async Task<string> GetWeather(\\n HttpClient client,\\n [Description(\\"中国的城市编码adcode\\")] string adcode)\\n {\\n if (string.IsNullOrEmpty(adcode))\\n {\\n return \\"adcode不能为空\\";\\n }\\n string gdKey = ConfigHelper.GetAppSetting(\\"GaoDeKey\\");\\n var jsonElement = await client.GetFromJsonAsync<JsonElement>($\\"/v3/weather/weatherInfo?key={gdKey}&city={adcode}&extensions=base\\");\\n var lives = jsonElement.GetProperty(\\"lives\\").EnumerateArray();\\n\\n if (!lives.Any())\\n {\\n return \\"当前城市天气获取失败\\";\\n }\\n\\n return string.Join(\\"\\\\n--\\\\n\\",lives.Select(live =>\\n {\\n var city = $\\"{live.GetProperty(\\"province\\").GetString()}--{live.GetProperty(\\"city\\").GetString()}\\";\\n var weather = live.GetProperty(\\"weather\\").GetString();\\n var temperature = live.GetProperty(\\"temperature\\").GetString();\\n var windPower = live.GetProperty(\\"windpower\\").GetString();\\n var humidity = live.GetProperty(\\"humidity\\").GetString();\\n return $\\"城市:{city}\\\\n天气:{weather}\\\\n温度:{temperature}°C\\\\n风力:{windPower}级\\\\n湿度:{humidity}%\\";\\n }));\\n }\\n}\\n
\\n我这里,没有使用官方案例里的天气接口,而是改成了高德的天气接口,因为一会儿还要演示一下SK调用MCP Server的能力,除了调用本地的Server,高德还有一个云端的MCP Server,非常好用,稍后一并介绍一下,正好就连天气接口也改成高德的。
\\n编写完成后,启动我们的Server服务。
\\ndotnet run\\n
\\nTool编写完成后,可以先使用一些软件或者工具类的MCP Client验证一下,开发阶段,这些工具还是非常有必要的,它的角色定位就像我们用到的数据库管理工具,比如SSMS,Pg Admin,DBeaver等。
\\n我这里使用的是官方的MCP Inspector,本地只要有node和npx环境即可。
\\n另外,因为要经常测试一些Server,建议把Python和Python包管理工具uv也安装一下。
\\n然后,我们启动Inspector
\\nnpx @modelcontextprotocol/inspector node build/index.js\\n
\\n启动之后,控制台会监听一个端口,然后在浏览器打开,然后配置好我们的Server地址,如下图
\\n获取到所有的Tool之后,测试验证一下我们刚完成的天气接口是否生效。
\\n至此,验证工作完成,说明我们的MCP Server是可以正常工作的,接下来就是接入实际的业务系统,来调用这个Server提供的能力了。
\\n这部分略过,在前面的系列文章里已经写过了,或者大家也可以直接查看微软的官方文档,这里不再赘述。
\\n这里呢,因为我在之前的项目里,已经开始使用SK框架了,并且完成了部分的Agent功能,都是以Plugin的方式注入到系统里的,这里的演示也就暂时以这种方式来接入,后续再根据实际情况调整。
\\n插件的代码如下
\\n[KernelFunction(\\"call_weather_api\\")]\\n[Description(\\"通过地理编码,获取天气信息\\")]\\n[return: Description(\\"如果运行正常,返回编号所属地址的天气详情\\")]\\npublic async Task<string> CallWeatherApi(string adcode)\\n{\\n Logger.Debug(\\"--------天气插件正确执行---------------\\");\\n var defaultOptions = new McpClientOptions\\n {\\n ClientInfo = new() { Name = \\"SK\\", Version = \\"1.0.0\\" }\\n };\\n\\n var defaultConfig = new SseClientTransportOptions\\n {\\n Endpoint = new Uri($\\"http://localhost:5001/sse\\"),\\n Name = \\"Magic.Services.MCPServer\\",\\n };\\n await using var client = await McpClientFactory.CreateAsync(\\n new SseClientTransport(defaultConfig),\\n defaultOptions);\\n\\n var result = await client.CallToolAsync(\\"GetWeather\\", new Dictionary<string, object?>\\n {\\n { \\"adcode\\",adcode}\\n });\\n\\n return JsonHelper.JsonSerialize(result);\\n}\\n
\\n我这里是在Web系统里进行的演示,所以以接口形式来调用插件,代码如下
\\npublic async Task<IActionResult> CallLocalServer(string adcode)\\n{\\n _kernel.Plugins.AddFromType<LocalServer>(\\"LocalServer\\", _serviceProvider);\\n // 获取聊天完成服务\\n var chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();\\n // 启用自动函数调用\\n OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()\\n {\\n ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,\\n //FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()\\n };\\n PromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };\\n ChatHistory chatHistory = [];\\n chatHistory.AddSystemMessage($\\"你是一个天气预报员,本函数的入参会给你一个中国地理位置编码,你要调用合适的MCP Server来完成天气预报。\\");\\n chatHistory.AddUserMessage($\\"查询地理编码为【{adcode}】的地点天气\\");\\n var chatResult = await chatCompletionService.GetChatMessageContentAsync(\\n chatHistory,\\n openAIPromptExecutionSettings,\\n _kernel);\\n Console.Write($\\"\\\\nAssistant : {chatResult}\\\\n\\");\\n\\n return Json(chatResult);\\n}\\n
\\n为了方便验证,可以先把接口的访问等级降低,直接通过URL地址访问,效果如下
\\n控制台打印的信息如下
\\n至此,我们已经在本地创建了一个MCPServer,并通过MCP Inspector进行了验证,同时又在原有使用SK的系统里,通过SK成功调用了这个Server提供的tool,接入复杂度可以接受,效果也非常不错。
\\n接下来,再试试SK能不能成功调用高德地图的MCP Server
\\n注意,如果前面的天气接口你是使用的高德的服务,那么相信你已经注册了高德的key,如果没有,这里需要去注册一下。
\\n由于调用的是第三方的MCP,我们这里可以直接在接口或者服务类里编写调用代码
\\npublic async Task<IActionResult> CallGaodeServer(string msg)\\n{\\n // 第一步:创建 mcp 客户端\\n var defaultOptions = new McpClientOptions\\n {\\n ClientInfo = new() { Name = \\"地图规划\\", Version = \\"1.0.0\\" }\\n };\\n\\n var defaultConfig = new SseClientTransportOptions\\n {\\n Endpoint = new Uri(ConfigurationHelper.GetSectionValue(\\"GaodeMCP\\")),\\n Name = \\"Magic.Services.MCPServer\\",\\n };\\n await using var client = await McpClientFactory.CreateAsync(\\n new SseClientTransport(defaultConfig),\\n defaultOptions);\\n\\n var tools = await client.ListToolsAsync();\\n\\n foreach (var tool in tools)\\n {\\n Logger.Debug($\\"秀一下高德的能力之--- {tool.Name}\\");\\n }\\n\\n #pragma warning disable SKEXP0001\\n _kernel.Plugins.AddFromFunctions(\\"gaodemap\\", tools.Select(aiFunction => aiFunction.AsKernelFunction()));\\n #pragma warning restore SKEXP0001\\n // 获取聊天完成服务\\n var chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();\\n // 启用自动函数调用\\n OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()\\n {\\n ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,\\n };\\n PromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };\\n ChatHistory chatHistory = [];\\n chatHistory.AddUserMessage(msg);\\n var chatResult = await chatCompletionService.GetChatMessageContentAsync(\\n chatHistory,\\n openAIPromptExecutionSettings,\\n _kernel);\\n Console.Write($\\"\\\\nAssistant : {chatResult}\\\\n\\");\\n\\n return Json(chatResult);\\n}\\n
\\n验证工作,还是和前面一样,暂时在浏览器里直接访问即可。
\\n控制台打印的信息更友好一点
\\n好了,至此,我们成功在原有的系统上,使用SK框架充当MCP Client的角色,完成了本地MCP Server的调用和第三方MCP Server的调用目标。
\\n最后,再推荐几个介绍MCP的参考站点
\\n在传统的软件测试体系中,QA人员主要采用接口测试、ui/功能测试、兼容/易用性测试、性能测试等黑/灰盒测试技术手段来保障软件项目质量,涉及代码层面的质量检查包括单元测试、代码Review环节主要由开发人员负责,QA涉及较少或者基本不参与,因此,针对质量至少存在以下局限性:
\\n1、QA重心在于关注功能实现,对内在的代码质量可能存在盲区
\\n一些隐形bug,对常见的代码错误和安全漏洞,如入参为空导致数据库全表扫描、特定数据返回空导致空指针异常、异常处理、线程安全、资源泄露、代码健壮性、SQL注入/XSS漏洞等问题难以被传统测试手段发现;
\\n2、人工Review代码成本较高,执行过程中落地难度大
\\n 由于人工Review代码,需要耗费大量的时间精力,效率比较低,很多团队因为项目开发任务重、时间紧,往往得不到执行,或者执行大打折扣,这些因素都会导致代码Review落地困难,从而影响质量。
\\n以上对于测试团队的工作效率以及产品质量的保障都会有很大的影响。
基于AI大模型的能力,搭建一套能够实现对增量代码自动Review的代码质量检测工具,通过该工具实现自动扫描识别代码的逻辑错误、安全漏洞、代码健壮性等问题,并生成详尽的审查报告和优化建议,从而提升代码Review效率和质量,实现对现有的质量体系进行增强和改进的目标,与QA人员形成互补能力,但是并不能替代QA。
\\n 经过反复调研和测试,我们利用git diff 提取被检测项目的目标分支增量代码片段,调用本地api提取该增量代码完整方法体,最后调用AI大模型对进行扫描,生成CodeRview报告。\\n整体技术方案一共包括4层:业务接入层、jenkins调度层、API服务层、底层模型层,具体如下:\\n
前提条件:
\\n1.硬件资源条件:公司采用阿里云托管,目前有1块闲置的英伟达V100 32G 显卡
\\n2.数据安全要求:基于公司对数据安全的要求,需要保证模型能支持私有化部署
基于以上要求,我们在选择用于代码审查(Code Review) 的 AI 大模型时,需要考虑模型是否支持本地化部署、模型能力偏向和审查效果。目前,常见的选择包括 OpenAI 的 GPT 系列、智谱 AI、以及 DeepSeek 等。这些模型具备强大的自然语言处理能力,可分析代码变更并生成审查建议。
\\n 我们对Qwen2.5-Coder、DeepSeek-Coder-V2和智谱CodeGeeX4三大模型的代码审查(Code Review)能力进行了全面的调研和测试,他们的主要特性比较如下:\\n
综合来看,Qwen2.5-Coder、DeepSeek-Coder、CodeGeeX4 都具备较强的代码识别和审查能力,且都支持本地部署和具有较高的数据安全性。
\\n 结合调研结果加上本地硬件资源(V100 32G),分别对 Qwen2.5-Coder(7B、14B、32B)、DeepSeek-Coder(6.7B、V2 16B)、CodeGeeX4(9B) 在代码审查(Code Review)结果与响应速度上做了验证与对比:\\n
基于模型调研和实际验证结果,目前我们选择智普CodeGeeX4 9B模型作为主要模型,Qwen2.5-Coder 14B和DeepSeek-Coder 16B模型作为备选模型,支持随时切换。
\\n核心功能的实现思路如下图,大致分为五步:
\\n1、拉取目标分支增量代码,通过git diff获取差异文件
\\n2、提取识别增量方法、解析变更类全量代码和匹配增量方法体
\\n3、以Class、Method为单位循环调用本地大模型API接口获取结果
\\n4、结果存储与报告生成
\\n5、发送钉钉通知审查结果
\\n整体流程如下:\\n
以下对每个步骤进行详细介绍:
\\nJenkins 任务配置:
\\n定时任务或触发式拉取:配置 Jenkins 任务,定时或通过其他触发方式拉取指定 Git 仓库中的代码,支持 GitHub、GitLab 等代码托管平台。
\\nGit 配置:
\\n指定分支、版本、提交信息等,通过配置 Jenkins job 来自动拉取最新的代码。
\\n
Shell 脚本执行:
\\n使用 shell 脚本执行 Git 拉取操作,可以配置参数如 git pull
或 git checkout
来确保拉取正确版本的代码。\\n
Git Diff 对比:
\\n使用 git diff <commit_base_branch> <commit_target_branch>
比较开发分支与基线分支的差异,提取增量代码。git diff 可以高效地找到相对较小的代码变更,避免全量扫描带来的性能开销。生成 diff
文件,标记出新增或修改的代码行,这些变更即为增量代码。
\\n增量代码识别与存储:
\\n提取出来的增量代码所在方法会存储在一个专门的临时文件夹中,等待后续步骤进一步处理。增量代码可以根据修改的内容分类(如新增、修改、删除),以便后续审查更具针对性。
Python 逻辑处理:
\\n解析 git diff 输出,分析出增量代码中的类、方法、函数等模块。通过 Python 脚本解析差异文件,提取新增或修改的函数、类,将识别出的方法和类将被归类,存储为一个 JSON 格式的结构,包含方法名称、类名等信息。
\\n全量代码解析:
\\n使用 JavaParser 解析整个代码库(包括新增和历史版本的代码),通过解析构建方法体的映射表。JavaParser 可以生成抽象语法树(AST),帮助更准确地理解方法和类的结构。\\n通过解析后的 AST,提取出代码中的方法名、类名及其对应的实现。这一过程为后续增量代码与全量代码的匹配提供了基础数据。
\\n方法体匹配与上下文提取:
\\n将增量代码中的方法名与全量代码中的方法体进行匹配,找出增量代码的完整上下文(如方法的调用和逻辑实现)。通过这种方式,可以确保审查的是完整的业务逻辑而不仅仅是部分代码。\\n增量方法和上下文通过映射关系进行归档,便于后续审查模块访问。
Ollama 模型微调:
\\n使用 Ollama 作为基础的 AI 模型,进行微调,能显著提高模型的审查准确度,实现输出样式最优化。
\\n建议修改:
\\n提供针对问题的修复建议,帮助开发人员理解和修复问题。
\\n智能审查扩展:
\\n可以根据具体需求调整模型,增加或修改审查的规则,比如在模型的输入中加入更多的上下文信息,或者使用外部工具提供的静态分析结果进行补充。
MySQL 数据库:
\\n审查结果存储在 MySQL 数据库中,设计审查结果表来保存以下信息:
\\n报告id:查询唯一值
\\n文件路径:类所在文件路径
\\n方法名:增量代码方法名称
\\n代码片段:问题所在的代码行及相关代码。
\\n修复建议:基于模型输出的修复建议,帮助开发人员快速定位问题并解决。
\\nfastjson 报告生成:
\\n利用 fastjson实现,将数据库中存储的审查结果提取出来,生成 HTML 格式的报告。\\n
以Class为单位输出报告,示例如下:\\n
钉钉机器人 API:
\\n使用钉钉机器人的 Webhook 接口,将审查报告和关键问题摘要自动推送到指定的钉钉群。
\\n消息内容包括:
\\n**审查报告链接:**提供报告的下载链接,供开发人员查看详细的审查内容。
\\n被测分支:方便查阅\\n
配置灵活性:
\\n可以根据需要配置不同的通知频率和内容摘要,例如仅推送高危漏洞,或者根据问题的严重性级别推送不同的通知。
包括提测前、bug fixed后、代码合并后等场景,支持手工和自动运行,目前大部分由QA手动触发执行。
\\n目前已接入4条业务线,接入服务20+,试用近一个月,累计发现的有效问题40+(包括空指针校验、入参为空导致数据库查全表、线程安全、异常处理不规范、资源泄露、代码安全漏洞等问题),单次生成Review报告的时间基本控制在5-10min以内。
\\n案例1:入参未校验,导致查全表,已修复\\n
优化前后代码对比:\\n
案例2:缺少对参数的判空 存在潜在的空指针异常:\\n
优化前后代码对比:\\n
自动代码Review工具虽然可以帮助我们发现问题,是否要修复,还要结合具体的业务场景、上下文进行判断,不能一概而论,做到具体问题具体分析,确保每一个可疑问题都能得到恰当的评估与处理。
\\n 目前主要实现对java工程相关代码的自动化Review,取得一定的效果,后续计划包括:
\\n1、接入更多项目
\\nA、支持android、ios客户端代码接入,实现自动Review
\\nB、支持前端vue、js、css代码接入,实现自动Review
\\n2、报告生成与结果展示优化
\\nA、提供易于理解,问题更加精准的审查报告,帮助开发人员快速识别和修复问题
\\nB、增量代码标识精确到行,方便快速判断是新增代码问题,还是历史问题
在当前竞争日益激烈的软件领域,降本、增效、提质一直是各团队不懈追求的目标,如何站在巨人的肩膀,利用好AI的能力帮我们实现这一目标显得尤为重要,基于AI大模型的增量代码自动Review工具,正是顺应这一趋势做出的尝试。
\\n通过该工具可以帮助QA团队增强和改进质量,但仍存在较大的优化空间,后续将持续改进优化,争取使该工具成为我们的得力助手,在质量保障提升的道路上注入更多活力与价值。
作者:洞窝技术QA团队
","description":"一、背景介绍 在传统的软件测试体系中,QA人员主要采用接口测试、ui/功能测试、兼容/易用性测试、性能测试等黑/灰盒测试技术手段来保障软件项目质量,涉及代码层面的质量检查包括单元测试、代码Review环节主要由开发人员负责,QA涉及较少或者基本不参与,因此,针对质量至少存在以下局限性:\\n\\n1、QA重心在于关注功能实现,对内在的代码质量可能存在盲区\\n\\n 一些隐形bug,对常见的代码错误和安全漏洞,如入参为空导致数据库全表扫描、特定数据返回空导致空指针异常、异常处理、线程安全、资源泄露、代码健壮性、SQL注入…","guid":"https://juejin.cn/post/7497054114170994697","author":"洞窝技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T08:46:52.500Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f99ee13239a94f06823290bb357bf6b2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=bZkuda0RoNnDXQTu1aGrsd5blRM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e6c3379dba64834900dd6d746d65583~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=Hx7d4Mibp2yjl7AWJHIP9j9abjQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5a3e3362b269416b8613fe89179138ca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=aTz%2BWN%2FCKWobnIsKi1CBraAM3m4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dcfb7fdd145945cabb60630759585b8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=Um5hj%2FukbPBarKkL2qjZMpGrpjk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5a91790445254291824ea0559d53fa53~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=YxS0ltuFg8wAt%2FvNT%2BxWo1r9Tzw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/255bdf48089f47ffb5d2a71785e4adb7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=GzBDaucgXonYtrgulbeWU0xBciI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9511d98119eb4a85bb70817a4f535f8a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=IM5tMptiJ%2B4eWYMNAsplh5EcwoQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1cf4806020684eaebe7b815ad25947d1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=%2Flrqy%2Baedbis%2FCpDxGeR1PSnbGU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bcf134b6968f4123bf4ec9ded7b99850~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=TqJ6TO4bM%2BP2xdqkuw8yDo3U9d4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f37c8d8ca0864eeebef573c1c1239026~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=nxM3kiMx6TGJHRA1HoD5x5COKLw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3119905c01bd48b19940639d202cc581~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=5CZYtYozIF9lcYfszFMQBGQzbh8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f46cf077f9f7462e9a75fed539a6c160~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=7CIdtUfEJmzItpz0379YTyzrd3w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f6000599723d46ee9214053bde00df9e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=zkb9OXNmWnXcCJ7vHo%2BpuWimauA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0deb936828a34879993ce76f24e21e50~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rSe56qd5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1746175716&x-signature=HGX4J0jg7ZLdTMeBIzaoT3%2BBxsc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","测试","AI编程","Jenkins"],"attachments":null,"extra":null,"language":null},{"title":"暴论:2025年,程序员必学技能就是MCP","url":"https://juejin.cn/post/7497054114170781705","content":"\\n\\n🍄 大家好,我是风筝
\\n🌍 个人博客:【古时的风筝】。
\\n本文目的为个人学习记录及知识分享。如果有什么不正确、不严谨的地方请及时指正,不胜感激。
\\n每一个赞都是我前进的动力。
\\n
之前写过一篇初识MCP的文章,简单介绍了 MCP 是什么,但是写的比较简单,今天来个详细一点的。
\\nMCP 就像是大模型世界里的“最后一公里”。说个暴论,MCP 应该是每个程序员在2025年必须掌握的知识点。
\\n大模型功能很强大, 我们都是清楚的。但是,它是有短板的。
\\n比如大模型的数学不好,知名测试就是让大模型比较9.8 和 9.11哪个数大
,大概半年之前,大部分大模型都会告诉你是9.11大。现在基本都正确了,其实还要归功于 RAG 技术,RAG 技术的原理和 MCP 实际上有异曲同工之妙,都像是个外挂程序,只不过 RAG 挂在了LLM端,MCP挂在了客户端。
再比如,大模型在没有联网功能前,它是没办法告诉你实时天气的。
\\n大模型比较擅长处理文科方面的工作,比如做总结、出报告、写文章等等,这也合理,毕竟人家 LLM 的全称是「大语言模型」,当然处理语言比较强。再拿多模态模型来说,比如生图模型,你让它画一幅画,画出的是梵高风格还是莫奈风格,对于很多人来说都没什么关系,好看就行。
\\n总结下来,大模型更擅长处理艺术类、语言类工作,或者说它擅长搞那些形而上的东西,而不太擅长处理特别精细化的东西,那些定制化的需求就更不用说了。
\\nMCP,全称 Model Context Protocol,由 Anthropic 在 2024 年 11 月推出,也就是目前公认写代码最厉害的大模型 Claude 的公司。MCP是社区共建的开放协议。目的是提供一个通用的开放标准,用来连接大语言模型和外部数据、行为。
\\n注意喽,它是一个开放标准,就像我们电脑上的USB接口,就像是手机上的 Type-C 接口,不管是哪个厂家生产的数据线,只要遵循USB标准或者 Type-C标准,就能用来充电或者传输数据。或者不管是哪个硬盘厂商生产的硬盘,只要甩出一个支持 Type-C的连接头出来,就能接到电脑上用。
\\n再来一个例子,我们平时开发做接口调用都用 JSON,JSON 的格式就是一个标准,只要你把数据格式构造成 JSON 这种格式,不管是谁来接数据,不管用哪个JSON库,设置手动解析,只要符合 JSON 的格式,就能够畅通无阻。
\\n好像有点儿唠叨了,总之 Anthropic 是制定了一个标准,只要大家遵循这个标准来就可以了。
\\nMCP 主机(MCP Hosts)
\\n像 Claude Desktop、IDE 或希望通过 MCP 访问数据的 AI 工具等程序,还有像 VsCode 中的 Cline 插件、Cursor、WindSurf 都支持MCP了,都是MCP主机,未来支持MCP调用的终端都可以成为 MCP主机。甚至,你自己也可以开发一个客户端。
\\nMCP 客户端(MCP Clients)
\\n通过协议客户端与服务器保持 1:1 连接的程序。通过上图也看出来了,主机和客户端一般都是在一起的,Client 更偏重于编程概念里的解释。
\\n可以对照数据库工具,比如 Navicat 软件本身可以理解为主机,而一个 Navicat 可以同时连接多个数据库,每连接一个数据需要一个连接(设置多个),这些连接就可以理解为MCP里的客户端。
\\nMCP 服务器(MCP Servers)
\\n轻量级程序,每个程序通过标准化的模型上下文协议暴露特定功能。这部分就是最后一公里的具体实现了,需要根据具体的需求去开发,比如想让LLM访问自研系统的数据,就要提供开放接口并供LLM使用,这部分就是 MCP服务器。
\\nMCP 服务可以用 Python、JavaScript(NodeJS)、Java 来开发,目前这是官方开放了 SDK 的,以后肯定还有 Go、Rust、.NET 等语言的SDK 出来。
\\n本地数据源(Local Data Sources)
\\n您的计算机文件、数据库和服务,MCP 服务器可以安全地访问这些数据源。
\\n远程服务(Remote Services)
\\n可以通过互联网访问的外部系统(例如,通过 API),MCP 服务器可以连接到这些服务。例如查询实时天气,你的MCP服务器也要去国家气象局等第三方平台获取,这不就需要 API调用了吗。
\\nMCP 的调用机制需要客户端、MCP服务端、LLM 三方配合。
\\n首先MCP 是一个服务,比如一个查询天气的 Spring Boot 应用,按照官方标准实现具体的服务接口,在本地启动。
\\n将服务在客户端进行配置,就像在注册中心注册一样,通常是启动命令,例如 npx
或者java
,意思就是客户端打开后,在本地同时启动MCP服务。例如下面这个配置文件内容,配置了两个服务,一个用 Java 实现的,一个是 Node。
{\\n \\"mcpServers\\": {\\n \\"spring-ai-mcp-weather\\": {\\n \\"command\\": \\"java\\",\\n \\"args\\": [\\n \\"-Dspring.ai.mcp.server.stdio=true\\",\\n \\"-jar\\",\\n \\"/Users/fengzheng/model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar\\"\\n ]\\n },\\n \\"brave-search\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\n \\"-y\\",\\n \\"@modelcontextprotocol/server-brave-search\\"\\n ],\\n \\"env\\": {\\n \\"BRAVE_API_KEY\\": \\"xxxx\\"\\n },\\n \\"autoApprove\\": [\\n \\"brave_web_search\\"\\n ]\\n }\\n }\\n}\\n
\\n之后,客户端就能根据MCP标准获取到 MCP 服务都有哪些可以用的工具(接口)了,在收到匹配的需求后就可以调用具体MCP的接口了。
\\n整个原理其实比较简单,下面是一个调用流程图。
\\n在文章开头说到,大模型虽然厉害,但是有短板。而每一个短板都是一部分人的真实需求,例如有人想要精确的实时天气情况,这就是一个短板。有人说,你想查天气,直接打开一个APP不就行了吗,当然可以,但是既然有了大模型,在一个应用或一个终端中完成更简单,省去拿手机打开APP的步骤了。
\\n再比如,直接在大模型聊天窗口操作本地数据库,已经有很多这种 MCP 服务了。
\\n再比如,想要搞自己的知识库。加入我跟大模型说:把我曾经写的关于 JVM 的文章找出来,并给我汇总成一篇带目录结构的文章合集。 如果没有 MCP 的辅助,现在的任何大模型客户端都是没办法实现的,而有了可以访问本地文件的MCP 帮助,这个需求就有可能实现了。
\\n还有数不胜数的类似的需求,每一个现有APP的功能都可能会成为 MCP 需要的功能。
\\n说到MCP就不得不提到Function Calling 和 Agent,一眼看过去,这几个好像功能差不多,目的也差不多,都是为了弥补大模型的短板。
\\nMCP ,咱前面介绍了一大堆,就不再过多赘述了,总之它是运行在本地(至少目前是),由Cursor 这样的主机(或统称为终端)调用,可以访问本地资源、个性化的API等。
\\n** Function Calling**
\\nFunction Calling 是AI模型与外部函数或服务交互的一种机制。
\\n在这种模式下,模型生成一个函数调用请求,宿主应用解析该请求并执行相应的操作,然后将结果返回给模型。通常有以下特点:
\\n同步执行:调用函数后,程序会等待函数执行完毕并返回结果,才继续执行后续代码。
\\n紧耦合:模型与函数或服务之间的关系较为紧密,需要在代码中明确指定。
\\n特定实现:函数调用的实现方式可能因平台或服务提供商而异,缺乏统一标准。
\\n智能体(Agent)\\n智能体是具备自主行动能力的系统,能够执行一系列复杂的任务,比如前一段时间很火的 Manus。
\\n它们通常具备以下特征:\\n自主性:能够根据环境变化和目标自主做出决策。
\\n任务执行:能够执行多步骤、多环节的任务,往往需要调用多个工具或服务。
\\n集成性:通常集成多种功能模块,如MCP和函数调用,以实现复杂的任务处理。
\\nMCP作为一种协议,主要解决模型与外部工具和数据源之间的交互问题,提供标准化的接口和通信方式。很灵活,只要遵循标准,几乎能实现任何功能。
\\nFunction Calling是模型与特定函数或服务交互的具体实现方式,关注如何在代码层面实现功能调用。需要大模型和大模型自身服务端、特定客户端紧密绑定,没那么灵活。
\\n智能体就复杂多了,它是一个系统,能够自主执行任务,通常需要结合MCP和Function Calling等机制,以实现复杂的功能。
\\nAI 势不可挡,未来生活的各个方面必将充斥着AI的身影,那MCP肯定随着这个趋势继续进化。
\\n猜测可能会出现下面这几种情况,有些正在发生。
\\n超级客户端的出现:
\\n现在我们想要什么功能就要打开具体的APP,随着AI的普及,是否会有超级客户端或者终端呢,就比如豆包或者小爱同学这种。
\\n这些客户端将集成多种工具和服务,提供统一的用户体验。例如,用户可以在一个应用中同时访问本地文件、数据库、浏览器和其他服务,无缝切换,提高工作效率。
\\n现在用小爱同学通常只有两个场景:定时和查天气。如果使用MCP的方式,和更多的服务商打通,是不是就可以在小爱同学实现更多的需求了。
\\nMCP 市场
\\n就像现在的应用商店那样,每个人、每个公司都可以提交自己的 MCP,比如某短视频平台提交一个推荐视频接口,用户就可以在一个支持 MCP 的客户端直接刷视频了(当然同时也要支持加载视频功能)。
\\n现在 Cline 插件有一个 MCP 市场功能,可以查看各个开源的 MCP,想用哪个就可以直接安装。但是还比较初级,有时候还不太好用。
\\n远程运行能力的实现
\\nMCP 支持多种通信方式,包括本地的 STDIO 和网络上的 HTTP/SSE。当需要跨机器通信时,MCP 可以使用 HTTP/SSE 实现工具的远程调用。这意味着,未来的 MCP 系统将能够在本地和远程环境中运行工具,提供灵活的部署选项。
\\n对于程序员来说,再本地跑Node、Python、Java 都不是什么难事儿,但是如果是一个没接触过代码的人,这就太难了。
\\n所以,如果想让更多的人用上 MCP ,一定要解决本地启动服务的问题。
\\n本文只是抛砖引玉,以下是关于 MCP 的一些学习资源,可以进行更加深入的学习。
\\n官方指导文档: modelcontextprotocol.io/introductio…
\\nLearn how to get started with the Anthropic API and Claude: docs.anthropic.com/en/home
\\n服务端开发指南: modelcontextprotocol.io/quickstart/…
\\nTypeScript SDK: github.com/modelcontex…
\\nPython SDK: github.com/modelcontex…
\\nJava SDK: github.com/modelcontex…
\\n目前几个比较大的 MCP 仓库\\ncursor.directory/mcp
\\n\\n\\n还可以看看风筝往期文章
\\n用这个方法,免费、无限期使用 SSL(HTTPS)证书,从此实现证书自由了
\\n为什么我每天都记笔记,主要是因为我用的这个笔记软件太强大了,强烈建议你也用起来
\\n\\n\\n\\n\\n","description":"🍄 大家好,我是风筝 🌍 个人博客:【古时的风筝】。\\n\\n本文目的为个人学习记录及知识分享。如果有什么不正确、不严谨的地方请及时指正,不胜感激。\\n\\n每一个赞都是我前进的动力。\\n\\n之前写过一篇初识MCP的文章,简单介绍了 MCP 是什么,但是写的比较简单,今天来个详细一点的。\\n\\n最后一公里\\n\\nMCP 就像是大模型世界里的“最后一公里”。说个暴论,MCP 应该是每个程序员在2025年必须掌握的知识点。\\n\\n大模型功能很强大, 我们都是清楚的。但是,它是有短板的。\\n\\n比如大模型的数学不好,知名测试就是让大模型比较9.8 和 9.11哪个数大,大概半年之前…","guid":"https://juejin.cn/post/7497054114170781705","author":"古时的风筝","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T08:15:35.430Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c7c9fa266ede47d291546c0283537d01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746180092&x-signature=WPvZLxZoNFXJUj%2F4Z30Vsh70FCc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8b7377b0b4534028a26b13824990b31c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746180092&x-signature=SckP3M4eSAUP2C3kKWmHw%2BRfT2M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e3ec62fccbd4136a2cfdd8124817b92~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746180092&x-signature=7FRUJI%2FuiCzHDdarorT%2BfRmtxL0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","前端","MCP"],"attachments":null,"extra":null,"language":null},{"title":"这编程圈子变化太快了,谁能告诉我 MCP 是什么","url":"https://juejin.cn/post/7496876253864067124","content":"\\n\\n🍄 大家好,我是风筝
\\n🌍 个人博客:【古时的风筝】。
\\n本文目的为个人学习记录及知识分享。如果有什么不正确、不严谨的地方请及时指正,不胜感激。
\\n每一个赞都是我前进的动力。
\\n
在当下这个时间点,各种 AI 大模型、各种 AI IDE 层出不穷,争奇斗艳。
\\n作为程序员,一日不学,就发现被人落下三秋了。
\\n前些天,在看各个 IDE 的时候,看到了 Cline 这个插件,起初,没觉得它有什么特别的,看了看介绍,感觉就是个像 WindSurf 或者 Trae Builder 模式的一个 VS Code 插件,输入 prompt,然后它就开始自己干活儿,比如让他做一个坦克大战的小游戏,它就在那儿一顿操作,创建文件、写代码、执行命令,然后还能打开浏览器调试看效果。
\\n\\n就是上面这个插件,已经 70多万下载了,GitHub 仓库也有3万 star 了。
\\n比如下面,我让它写个小红书出来,它就开始自己干活。当然,你还能让它自己检查有没有bug,然后它就会自己修。感觉和 Cursor、WindSurf 没什么区别,对不对。
\\n然后我看到说它最近开放了一个市场,叫做 MCP Marketplace,里面有各种各样的选择,也就是所说的各种 MCP。
\\n其实这不是第一次看到 MCP 了,只是这次勾起了我的会议,我突然记起来,之前就在推上收藏了好几个关于 MCP 的推文,就是因为好奇这玩意到底是啥。
\\n只是一直没时间看,属于那种收藏了就等于学会了的,自欺欺人的状态。
\\nMCP,全称 Model Context Protocol,由 Anthropic 在 2024 年 11 月推出,社区共建的开放协议。目的是提供一个通用的开放标准,用来连接大语言模型和外部数据、行为。
\\n最近 Cursor 也支持 MCP 了。
\\n首先来说,这是个协议,也就是一个标准,是给大语言模型用的标准。
\\n大语言模型是非常聪明的,但是,它还是有一些不擅长的事情或者是它根本做不了的。
\\n你让它写一个爆款文案,它很擅长,但是你要让它写个爆款文案并直接发布到小红书它就做不了。
\\n你给它一段文本让它解读一下很简单,但是你要把一个 word 扔它,让它返回给你一个 Markdown ,它就做不了。
\\n由于诸如此类种种场景,就需要一些外部资源介入了,例如要发布到小红书,就需要调用小红书接口,转 markdown 就需要有这种功能的工具或接口。
\\n这些外部资源用来填补大模型的短板,也就是 MCP 要解决的问题。
\\nMCP 架构由三个端组成:\\n客户端:各种IDE、插件,例如 Cursor、Cline 等\\n服务器:轻量级本地进程,通过 API 连接数据源,如发布小红书、拉取小红书数据的接口。\\nData Source:本地(如文件系统)或远程数据源。
\\n看下面这个架构图就很清晰了。
\\n拿官方一个例子来说,想要大模型支持把word转markdown的功能,按 MCP 架构怎么做呢?
\\n很简单,就是启动一个有这种能力的服务,然后让大模型调用这个服务就可以了。
\\n如果你使用了 Cline,可以在它的 MCP 市场安装。
\\n当然,你也可以自己部署,这是一个开源的 MCP ,开源地址:github.com/zcaceres/ma…
\\n最后,使用支持 MCP 的客户端,正常使用大模型就可以了,再跑出去一个 word,让大模型帮忙转成 markdown 就可以办到了。
\\n虽然有各种大模型、AI IDE 辅助开发,但是仍然有很多东西要学习,要不然先进生产力工具来了,你都不会用,干着急。
\\n还可以看看风筝往期文章
\\n用这个方法,免费、无限期使用 SSL(HTTPS)证书,从此实现证书自由了
\\n为什么我每天都记笔记,主要是因为我用的这个笔记软件太强大了,强烈建议你也用起来
\\n\\n\\n\\n\\n","description":"🍄 大家好,我是风筝 🌍 个人博客:【古时的风筝】。\\n\\n本文目的为个人学习记录及知识分享。如果有什么不正确、不严谨的地方请及时指正,不胜感激。\\n\\n每一个赞都是我前进的动力。\\n\\n在当下这个时间点,各种 AI 大模型、各种 AI IDE 层出不穷,争奇斗艳。\\n\\n作为程序员,一日不学,就发现被人落下三秋了。\\n\\n前些天,在看各个 IDE 的时候,看到了 Cline 这个插件,起初,没觉得它有什么特别的,看了看介绍,感觉就是个像 WindSurf 或者 Trae Builder 模式的一个 VS Code 插件,输入 prompt,然后它就开始自己干活儿…","guid":"https://juejin.cn/post/7496876253864067124","author":"古时的风筝","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T08:11:08.368Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5891c4f29a20442594773a1d399ad1f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=CgijxW44iXWVnrl2S%2BZCXNNfmCM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce68aba542c54c10aa2089b88579ff8a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=XAjlLDjGyMiYybJWWFDBJibYHak%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9cbead60916e4391b40d6c547d1a5fde~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=UbVFHUZpJzPm%2F6kF%2FLsdykn59cs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5192e2d1a04c425e8f816e7a4a3da325~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=%2FhfFvbTQaq7l6%2BEpD%2F%2Bqc3GYjHQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/506fe2205b64442290b5d41dc42da875~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=%2BrzRPtZ9sO6%2BxxPJyR%2B8b0Ae59M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab71f10b0ed14d878665122a51862ac5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-k5pe255qE6aOO562d:q75.awebp?rk3s=f64ab15b&x-expires=1746173468&x-signature=fx6xjmp%2FKDDCnxc5FKNPcz3hibU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","MCP","前端"],"attachments":null,"extra":null,"language":null},{"title":"Spring Task定时任务:程序员的自动化办公秘籍","url":"https://juejin.cn/post/7496369162485317647","content":"想象你有个24小时待命的英国管家:
\\n这就是Spring Task的本质——让程序学会自己\\"定闹钟\\"!相比传统的Timer,它就像从诺基亚升级到iPhone:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nSpring Task | Timer | |
---|---|---|
任务调度 | 支持cron表达式 | 固定间隔 |
线程管理 | 线程池管理 | 单线程 |
异常处理 | 不会因异常终止任务 | 会终止整个任务 |
功能扩展 | 支持异步/分布式 | 功能单一 |
XML\\n<!-- 使用前先安装\\"发条\\" --\x3e\\n<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter</artifactId>\\n</dependency>\\n
\\nSpring Boot 2.x+版本已经内置定时任务模块,无需额外添加依赖
\\nJAVA\\n@SpringBootApplication\\n@EnableScheduling // 给程序装上定时芯片\\npublic class TaskApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(TaskApplication.class, args);\\n }\\n}\\n
\\nJAVA\\n@Component\\npublic class MyTask {\\n \\n // 每天23:59:59执行(打工人日报提醒)\\n @Scheduled(cron = \\"59 59 23 * * ?\\")\\n public void dailyReport() {\\n System.out.println(\\"【系统提示】记得写日报!\\");\\n }\\n}\\n
\\nJAVA\\n秒 分 时 日 月 周 年(可选)\\n
\\n记忆口诀: \\"秒杀时分日月周年\\"
\\n场景 | Cron表达式 | 人话翻译 |
---|---|---|
每30分钟执行一次 | 0 0/30 * * * ? | 整点/半点报时 |
工作日上午9点打卡 | 0 0 9 ? * MON-FRI | 打工人专属闹钟 |
每月最后一天凌晨清空缓存 | 0 0 0 L * ? | 每月一次的存储大扫除 |
每天三顿提醒吃饭 | 0 0 7,12,18 * * ? | 干饭人必备三连击 |
*
:比老板的要求更野(每时每刻)?
:佛系青年专用(不指定具体值)L
:最后的倔强(最后一天)W
:社畜必修(最近工作日)#
:霸道总裁选择(第N个周X)JAVA\\n@Scheduled(fixedRate = 3600000) // 每小时跑腿一次\\npublic void syncOrderStatus() {\\n // 把订单系统的状态\\"搬运\\"到物流系统\\n}\\n
\\nJAVA\\n@Scheduled(cron = \\"0 0 3 * * ?\\") // 每天凌晨3点打扫\\npublic void cleanLogs() {\\n // 把7天前的日志文件\\"扫\\"进回收站\\n}\\n
\\nJAVA\\n@Scheduled(cron = \\"0 0 9 ? * MON\\") // 每周一早上9点\\npublic void sendWeeklyReport() {\\n // 自动给老板发送\\"表面功夫\\"周报\\n}\\n
\\nJAVA\\n@Scheduled(fixedDelay = 5000) // 5秒后重复\\n@Scheduled(fixedRate = 3000) // 3秒一次\\n@Scheduled(initialDelay = 10000, fixedRate = 5000) // 10秒后开始,每5秒一次\\n
\\nJAVA\\n@Configuration\\npublic class TaskConfig implements SchedulingConfigurer {\\n @Override\\n public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {\\n // 创建10个线程的定时任务线程池\\n taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));\\n }\\n}\\n
\\n当多个实例同时运行时:
\\n默认单线程执行时,前一个任务卡住会导致后续任务排队。解决方案:
\\nJAVA\\n@EnableAsync // 开启异步模式\\n@Async // 给方法加上\\"加速器\\"\\n@Scheduled(fixedRate = 1000)\\npublic void asyncTask() {\\n // 现在不会堵车了\\n}\\n
\\n使用fixedDelay代替fixedRate:
\\nJAVA\\n@Scheduled(fixedDelay = 5000) // 每次执行结束后等5秒\\n
\\n0 */5 * * * ?
每5分钟(整点开始)0 5/10 * * * ?
每小时的第5分钟开始,每10分钟一次0 0 12 1W * ?
每月最接近1号的工作日中午12点JAVA\\n@Around(\\"@annotation(scheduled)\\")\\npublic Object monitor(ProceedingJoinPoint pjp, Scheduled scheduled) throws Throwable {\\n long start = System.currentTimeMillis();\\n try {\\n return pjp.proceed();\\n } finally {\\n long cost = System.currentTimeMillis() - start;\\n log.info(\\"任务执行耗时:{}ms\\", cost);\\n }\\n}\\n
\\nPROPERTIES\\n# application.properties\\nschedule.enabled=true\\n
\\nJAVA\\n@ConditionalOnProperty(name = \\"schedule.enabled\\", havingValue = \\"true\\")\\n@Scheduled(cron = \\"${schedule.cron}\\")\\npublic void configurableTask() {\\n // 可配置的任务\\n}\\n
\\nJAVA\\n// 动态任务示例\\n@Autowired\\nprivate ScheduledTaskRegistrar taskRegistrar;\\n\\npublic void addDynamicTask(Runnable task, String cron) {\\n taskRegistrar.addCronTask(new CronTask(task, cron));\\n}\\n
\\n最后友情提醒:
\\n定时任务虽好,但不要贪杯哦!
\\n当你的任务开始需要以下功能时,就该考虑专业调度框架了:
\\n✅ 任务持久化
\\n✅ 失败重试机制
\\n✅ 可视化任务管理
\\n✅ 复杂依赖关系
现在就去给你的程序装上\\"定时芯片\\"吧!如果遇到任何问题,欢迎在评论区呼叫\\"任务救援队\\"~
","description":"一、Spring Task是什么?程序员的\\"私人助理\\" 想象你有个24小时待命的英国管家:\\n\\n早上6点:自动帮你煮咖啡(数据备份)\\n中午12点:准时提醒你吃饭(系统监控)\\n凌晨3点:偷偷帮你抢茅台(定时任务)\\n\\n这就是Spring Task的本质——让程序学会自己\\"定闹钟\\"!相比传统的Timer,它就像从诺基亚升级到iPhone:\\n\\n\\tSpring Task\\tTimer任务调度\\t支持cron表达式\\t固定间隔\\n线程管理\\t线程池管理\\t单线程\\n异常处理\\t不会因异常终止任务\\t会终止整个任务\\n功能扩展\\t支持异步/分布式\\t功能单一…","guid":"https://juejin.cn/post/7496369162485317647","author":"小厂永远得不到的男人","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T03:11:58.520Z","media":null,"categories":["后端","Spring","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"超越Elasticsearch!号称下一代搜索引擎,性能炸裂!","url":"https://juejin.cn/post/7496400838514622473","content":"\\n\\n当我们需要实现全文搜索功能的时候,往往会使用到搜索引擎,比较常用的是Elasticsearch。但是Elasticsearch的硬件配置要求比较高,不同版本间的API兼容性也比较差。今天给大家分享一款轻量级搜索引擎Meilisearch,搜索速度非常快,能实现即时搜索,希望对大家有所帮助!
\\n
Meilisearch是一款轻量级搜索引擎,它支持RESTful风格的搜索API,目前在Github上已有50k+star
。其目标是成为适用于所有用户的搜索引擎解决方案,能让用户端的每一位用户获得快速且精准的搜索体验。
Meilisearch主要具有如下特性:
\\n下面是使用Meilisearch实现即时搜索的效果图,搜索速度还是非常快的!
\\n\\n\\n使用Docker来部署Meilisearch是非常方便的,我们将采用此种方式!
\\n
docker pull getmeili/meilisearch:v1.13\\n
\\ndocker run -p 7700:7700 --name meilisearch \\\\\\n-e MEILI_ENV=\'development\' \\\\\\n-v /mydata/meiliData:/meili_data \\\\\\n-d getmeili/meilisearch:v1.13\\n
\\n这里给大家分享一个实战项目,mall项目是一套基于 SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\n\\n\\n接下来我们就来讲解下Meilisearch的使用,将使用RESTful API的形式。
\\n
\\n\\n在Meilisearch中,索引是一系列文档的组合,相当于MySQL中的表的概念,这里我们先来讲解它的使用。
\\n
curl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/documents?primaryKey=id\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary @movies.json\\n
\\nTransformers
;curl \\\\\\n -X GET \'{{MEILISEARCH_URL}}/indexes\'\\n
\\ncurl \\\\\\n -X DELETE \'{{MEILISEARCH_URL}}/indexes/movies\'\\n
\\n\\n\\n索引设置是一个包含很多选项的JSON对象,它可以用于定义Meilisearch的搜索行为,有点类似于MySQL中表结构的概念,这里我们来讲解下它的使用。
\\n
curl \\\\\\n -X GET \'{{MEILISEARCH_URL}}/indexes/movies/settings\'\\n
\\ntitle
和release_date
字段变成可以排序的,genres
和release_date
变成可以筛选的;curl \\\\\\n -X PATCH \'{{MEILISEARCH_URL}}/indexes/movies/settings\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{\\n \\"sortableAttributes\\": [\\n \\"title\\",\\n \\"release_date\\"\\n ],\\n \\"filterableAttributes\\": [\\n \\"genres\\",\\n \\"release_date\\"\\n ]\\n }\'\\n
\\n\\n\\n在Meilisearch中,文档是一个包含很多属性的对象,有点类似于MySQL中表记录的概念。
\\n
curl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/documents\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{\\n \\"id\\": 1,\\n \\"title\\": \\"Transformers Test\\",\\n \\"overview\\": \\"Young teenager, Sam Witwicky becomes involved in the ancient struggle between two extraterrestrial factions of transforming robots...\\",\\n \\"genres\\": [\\n \\"Adventure\\",\\n \\"Science Fiction\\",\\n \\"Action\\"\\n ],\\n \\"poster\\": \\"https://image.tmdb.org/t/p/w500/6eehp9I54syN3x753XMqjKz8M3F.jpg\\",\\n \\"release_date\\": 1182902400\\n}\'\\n
\\ncurl \\\\\\n -X GET \'{{MEILISEARCH_URL}}/indexes/movies/documents/1\'\\n
\\ncurl \\\\\\n -X PUT \'{{MEILISEARCH_URL}}/indexes/movies/documents\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'[\\n {\\n \\"id\\": 1,\\n \\"title\\": \\"Transformers Update\\"\\n }\\n ]\'\\n
\\ncurl \\\\\\n -X DELETE \'{{MEILISEARCH_URL}}/indexes/movies/documents/1\'\\n
\\n\\n\\n接下来我们来讲解下如何使用Meilisearch搜索数据;
\\n
curl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/search\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{ \\"q\\": \\"Transformers\\" }\'\\n
\\ncurl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/search\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{ \\"q\\": \\"Transformers\\",\\"offset\\": 0,\\"limit\\": 5 }\'\\n
\\ncurl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/search\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{ \\"q\\": \\"Transformers\\",\\"offset\\": 0,\\"limit\\": 5,\\"sort\\": [\\"release_date:desc\\"]}\'\\n
\\ncurl \\\\\\n -X POST \'{{MEILISEARCH_URL}}/indexes/movies/search\' \\\\\\n -H \'Content-Type: application/json\' \\\\\\n --data-binary \'{ \\"q\\": \\"Transformers\\",\\"offset\\": 0,\\"limit\\": 5,\\"sort\\": [\\"release_date:desc\\"],\\"filter\\":\\"genres = Action OR genres = Adventure\\"}\'\\n
\\n今天带大家体验了一把Meilisearch的搜索功能,对比Elasticsearch,它需要的配置非常低,搜索速度也非常快,感兴趣的小伙伴可以尝试下它!
\\n\\n\\n最近在处理一个 JSON 接口时,遇到这样一种情况:返回的数据中包含一些我事先并不知道的字段,这些字段会根据上下文动态变化,没办法在 Java 类中提前写死字段名。起初我以为只能通过 Map 手动解析,但后来发现 Jackson 提供了
\\n@JsonAnyGetter
和@JsonAnySetter
这两个注解,专门用来处理这种“动态属性”。它们能让我优雅地把未知字段收集起来或者序列化出去,不影响已知字段的正常处理。不过我在使用过程中也有点疑惑,比如这两个注解的用法顺序有什么讲究?有哪些坑需要避免?这种方式是不是适合所有动态字段的场景?
@JsonAnyGetter
/ @JsonAnySetter
:像开了一家灵活应对一切需求的「杂货铺」🧃🧂
想象你是个 JSON 杂货铺老板,门口写着招牌:“你有啥,我都能装;你要啥,我都能配。”
\\n你平时会备一些常规商品(字段),但总有顾客带些奇怪需求来问:
\\n这些你事先没在货架上准备的“临时需求”,你也得接单,对吧?
\\n这时候你就需要一对“万能架子”——也就是:
\\n@JsonAnySetter
:随便放!你给啥我都能接每次有奇怪字段进来(JSON 反序列化时),你就把它们统统放进一个万能柜子(通常是 Map<String, Object>
):
@JsonAnySetter\\npublic void add(String key, Object value) {\\n otherProps.put(key, value);\\n}\\n
\\n比如这个 JSON:
\\n{\\n \\"name\\": \\"豆瓣酱\\",\\n \\"spicy\\": true,\\n \\"limited_edition\\": \\"yes\\",\\n \\"extra_notes\\": \\"只在冬天卖\\"\\n}\\n
\\n你类里只定义了 name
和 spicy
字段,但 limited_edition
和 extra_notes
也能顺利进货,被收纳进了 otherProps
这个万能抽屉里。
\\n\\n\\n
@JsonAnySetter
用于标注一个方法,该方法可以接收 JSON 中没有预定义的属性。当 Jackson 反序列化 JSON 时,如果遇到未在 Java 类中显式定义的字段,它会调用这个方法并将字段名和字段值作为参数传递给它。当你在反序列化 JSON 时,不希望显式定义所有的字段,或者 JSON 中包含了动态的属性时,使用
\\n@JsonAnySetter
可以自动将这些字段添加到一个Map
或类似的结构中。
接下来用一个完整的代码示例,我们来实现反序列化时动态添加属性:
\\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\\nimport com.fasterxml.jackson.databind.ObjectMapper;\\n\\nimport java.util.HashMap;\\nimport java.util.Map;\\n\\npublic class Person {\\n private String name;\\n private int age;\\n\\n // 存储额外的动态属性\\n private Map<String, Object> additionalProperties = new HashMap<>();\\n\\n // 添加动态属性\\n @JsonAnySetter\\n public void addAdditionalProperty(String key, Object value) {\\n this.additionalProperties.put(key, value);\\n }\\n\\n // 省略 getter 和 setter 方法\\n\\n public Map<String, Object> getAdditionalProperties() {\\n return additionalProperties;\\n }\\n\\n public static void main(String[] args) throws Exception {\\n String json = \\"{\\"name\\":\\"John\\",\\"age\\":30,\\"address\\":\\"123 Street\\",\\"nickname\\":\\"Johnny\\"}\\";\\n\\n ObjectMapper mapper = new ObjectMapper();\\n Person person = mapper.readValue(json, Person.class);\\n\\n System.out.println(\\"Name: \\" + person.name); // 输出:Name: John\\n System.out.println(\\"Age: \\" + person.age); // 输出:Age: 30\\n System.out.println(\\"Additional Properties: \\" + person.getAdditionalProperties());\\n // 输出:Additional Properties: {address=123 Street, nickname=Johnny}\\n }\\n}\\n
\\n在这个例子中:
\\nPerson
类通过 @JsonAnySetter
注解的 addAdditionalProperty
方法来处理动态的属性。Person
类中没有显式的字段来接收 address
和 nickname
,但它们被添加到 additionalProperties
中。address
和 nickname
作为动态属性添加到 additionalProperties
中。输出:
\\nName: John\\nAge: 30\\nAdditional Properties: {address=123 Street, nickname=Johnny}\\n
\\n@JsonAnyGetter
:随便拿!需要啥我都能给到了序列化的时候,有顾客问你:“老板,这罐酱料里都包含什么成分?”
\\n你就把主料(已有字段)和万能抽屉里的额外信息一起打包给他,看起来就像全是标准字段一样输出!
\\n@JsonAnyGetter\\npublic Map<String, Object> getOtherProps() {\\n return otherProps;\\n}\\n
\\n这样序列化输出的 JSON 会自动把 otherProps
里的内容平铺出去,和其他字段“融为一体”。
\\n\\n\\n
@JsonAnyGetter
用于标注一个方法,该方法返回一个Map
或类似结构,它将包含对象的 动态属性(即对象中没有显式定义的字段)。当 Jackson 序列化对象时,它会将这个Map
中的键值对当作额外的 JSON 属性来序列化。当你有一个类,但是它可能会接受动态的字段,或者一些额外的键值对时,使用
\\n@JsonAnyGetter
允许你将这些额外的字段序列化为 JSON。
继续使用上面的person类,它有一些基本的属性,但你希望允许动态添加额外的属性,如额外的 \\"address\\" 、 \\"nickname\\" 等字段。
\\nimport com.fasterxml.jackson.annotation.JsonAnyGetter;\\nimport com.fasterxml.jackson.databind.ObjectMapper;\\n\\nimport java.util.HashMap;\\nimport java.util.Map;\\n\\npublic class Person {\\n private String name;\\n private int age;\\n\\n // 存储额外的动态属性\\n private Map<String, Object> additionalProperties = new HashMap<>();\\n\\n public Person(String name, int age) {\\n this.name = name;\\n this.age = age;\\n }\\n\\n // 通过该方法返回所有额外的动态属性\\n @JsonAnyGetter\\n public Map<String, Object> getAdditionalProperties() {\\n return additionalProperties;\\n }\\n\\n public void addAdditionalProperty(String key, Object value) {\\n this.additionalProperties.put(key, value);\\n }\\n\\n // 省略 getter 和 setter 方法\\n}\\n\\npublic class Main {\\n public static void main(String[] args) throws Exception {\\n Person person = new Person(\\"John\\", 30);\\n person.addAdditionalProperty(\\"address\\", \\"123 Street\\");\\n person.addAdditionalProperty(\\"nickname\\", \\"Johnny\\");\\n\\n ObjectMapper mapper = new ObjectMapper();\\n String json = mapper.writeValueAsString(person);\\n System.out.println(json); // 输出:{\\"name\\":\\"John\\",\\"age\\":30,\\"address\\":\\"123 Street\\",\\"nickname\\":\\"Johnny\\"}\\n }\\n}\\n
\\n在这个例子中:
\\nPerson
类包含一个 Map<String, Object>
来存储动态属性。@JsonAnyGetter
标注 getAdditionalProperties()
方法,表示 additionalProperties
中的键值对应该被序列化为 JSON 字段。addAdditionalProperty()
方法向 additionalProperties
中添加动态字段。输出:
\\n{\\n \\"name\\": \\"John\\",\\n \\"age\\": 30,\\n \\"address\\": \\"123 Street\\",\\n \\"nickname\\": \\"Johnny\\"\\n}\\n
\\n注解 | 类比 | 用途 |
---|---|---|
@JsonAnySetter | 顾客带啥都能收的“万能抽屉”🗃️ | JSON → Java:把未知字段也收进去 |
@JsonAnyGetter | 万能抽屉变展示架,拿出来卖!🛒 | Java → JSON:把 Map 中的额外字段“当作正常字段”输出 |
特别适合那些字段不固定、可能需要动态扩展的 JSON 数据结构,比如配置项、参数列表、插件信息等。
","description":"最近在处理一个 JSON 接口时,遇到这样一种情况:返回的数据中包含一些我事先并不知道的字段,这些字段会根据上下文动态变化,没办法在 Java 类中提前写死字段名。起初我以为只能通过 Map 手动解析,但后来发现 Jackson 提供了 @JsonAnyGetter 和 @JsonAnySetter 这两个注解,专门用来处理这种“动态属性”。它们能让我优雅地把未知字段收集起来或者序列化出去,不影响已知字段的正常处理。不过我在使用过程中也有点疑惑,比如这两个注解的用法顺序有什么讲究?有哪些坑需要避免?这种方式是不是适合所有动态字段的场景? 开家杂货铺吧…","guid":"https://juejin.cn/post/7496341504849641510","author":"洛小豆","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T23:56:14.536Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"聊聊四种实时通信技术:长轮询、短轮询、WebSocket 和 SSE","url":"https://juejin.cn/post/7496375493329174591","content":"这篇文章,我们聊聊 四种实时通信技术:短轮询、长轮询、WebSocket 和 SSE 。
\\n浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新)。
\\n笔者职业生涯印象最深刻的短轮询应用场景是比分直播:
\\n如图所示,用户进入比分直播界面,浏览器定时查询赛事信息(比分变动、黄红牌等),假如数据有变化,则重新渲染页面。
\\n这种方式实现起来非常简单可靠,但是频繁的调用后端接口,会对后端性能会有影响(主要是 CPU)。同时,因为依赖轮询间隔,页面数据变化有延迟,用户体验并不算太好。
\\n浏览器发送 HTTP 请求后,服务器 挂起连接 直到数据更新或超时,返回响应后浏览器立即发起新请求。
\\n长轮询最常见的应用场景是:配置中心,我们耳熟能详的注册中心 Nacos 、阿波罗都是依赖长轮询机制。
\\n\\n\\n客户端发起请求后,Nacos 服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。
\\n
基于 TCP 的全双工协议,通过 HTTP 升级握手(Upgrade: websocket
)建立持久连接,双向实时通信。
笔者曾经服务于北京一家电商公司,参与直播答题功能的研发。
\\n直播答题整体架构见下图:
\\nNetty TCP 网关的技术选型是:Netty、ProtoBuf、WebSocket ,选择 WebSocket 是因为它支持双向实时通信,同时 Netty 内置了 WebSocket 实现类,工程实现起来相对简单。
\\n基于 HTTP 协议,服务器可 主动推送 数据流(如Content-Type: text/event-stream
),浏览器通过EventSource
API 监听。
SSE 最经典的应用场景是 : DeepSeek web 聊天界面 ,如图所示:
\\n当在 DeepSeek 对话框发送消息后,浏览器会发送一个 HTTP 请求 ,服务端会通过 SSE 方式将数据返回到浏览器。
\\n特性 | 短轮询 | 长轮询 | SSE | WebSocket |
---|---|---|---|---|
通信方向 | 浏览器→服务器 | 浏览器→服务器 | 服务器→浏览器 | 双向通信 |
协议 | HTTP | HTTP | HTTP | WebSocket(基于TCP) |
实时性 | 低 | 中 | 高 | 极高 |
资源消耗 | 高(频繁请求) | 中(挂起连接) | 低 | 低(长连接) |
选择建议:
\\n最近在某社区看到一句扎心的话:“2025年的程序员,不是在优化简历,就是在准备优化简历的路上。”
\\n我们就像在写一段没有事务保护的代码——一个异常就可能让所有努力付诸东流。
\\n当潮水退去,才知道谁在裸泳。
\\n最近review代码时,发现不少同事对Spring事务的理解还停留在“加个@Transactional就完事”的阶段。
\\n这让我想起自己刚入行时,也曾在事务的坑里摔得鼻青脸肿。
\\n今天趁着周日(单双休的休),把这些年积累的事务心得整理成文。
\\n耐心看完,你一定有所收获。
\\n好了,咱们开始聊正事儿,从事务的基本概念说起吧!
\\n事务,英文名叫 Transaction。我特意查了下,这词儿来自拉丁语“transactio”,意思是“完成”或者“处理”。在计算机的世界里,它指的是一组操作,要么全干完,要么全不干,就像个打包好的整体。
\\n为了更好理解,咱们拿银行转账举个例子。你想给爸妈转1000块钱,这事儿分两步走:
\\n这俩步骤必须一起成功,不然就乱套了。
\\n为啥这么说呢?想象一下,要是你的钱扣了,爸妈那边却没收到,这1000块不就凭空没了?反过来,要是你没扣钱,爸妈账户却多了1000,银行岂不是白赚了?(开玩笑,便宜谁也不能便宜银行啊!)
\\n所以,任何一个步骤出错,另一边都得跟着停下来。
\\n这时候,事务就派上用场了。它能保证这两步要么全成,要么全不干。
\\n在数据库里,这种“非黑即白”的特性有个专业名字,叫原子性(Atomicity)。
\\n它是事务的四大特性之一,简称 ACID
:
原子性(Atomicity)
:操作要么全做,要么全不做,没中间状态。一致性(Consistency)
:事务得让数据库从一个正常状态,稳稳当当过渡到另一个正常状态。隔离性(Isolation)
:多个事务一块儿跑时,互相不能干扰。持久性(Durability)
:一旦事务提交,数据就永久保存,丢不了。正式定义来了:事务就是一组操作的集合,要么全部成功,要么全部失败。简单吧?
\\n这个概念在现实中超重要,像银行转账、电商下单,哪儿都少不了。
\\n数据库里的事务还能在系统崩了的时候,保持数据不乱。要么一切顺利完成(叫提交事务),要么就像啥也没干(叫回滚事务)。
\\n看看这个流程图就明白了:
\\ngraph LR\\n A[开始事务] --\x3e B[操作1]\\n B --\x3e C[操作2]\\n C --\x3e D[操作3]\\n D --\x3e E{是否全部成功?}\\n E --\x3e|是| F[提交事务]\\n E --\x3e|否| G[回滚事务]\\n
\\n说完了概念,我们来讲讲应用。
\\nSpringBoot提供了好几种方式来管理事务,最常用也最简单的就是通过 @Transactional
注解实现。
而 @Transactional
实际是利用了 TransactionManager
进行事务的管理,这里暂且按下不表,知道有这回事即可。
classDiagram\\n class UserService {\\n +@Transactional\\n +transferMoney()\\n }\\n class TransactionManager\\n UserService --\x3e TransactionManager : 使用\\n
\\n@Transactional
注解可以用在方法上或类上,用来声明事务。
它的基本用法非常简单:
\\n@Service\\npublic class UserService {\\n @Transactional\\n public void transferMoney(String fromAccount, String toAccount, BigDecimal amount) {\\n // 先查你的账户是否正常\\n // 再查你父母的账户是否正常\\n // 你的账户扣1000\\n // 父母账户加1000\\n }\\n}\\n
\\n注释里的所有操作都被包含在一个事务中,就这么简单。
\\n那这样就结束了?显然不是。
\\n这里面还有不少的注意事项,稍不注意就会踩坑。同样先按下不表,继续往下看。
\\n知道了事务最简单的用法,还得回头来了解下事务中两个很重要的要点,一个叫传播行为,一个叫隔离等级。
\\n想象一下,事务就像个保护罩,把操作裹起来,确保要么全成,要么全挂。
\\n而传播行为(Propagation Behavior),讲的是一个罩着保护罩的方法 A,去调用另一个方法 B 时,这罩子咋传过去的问题。
\\n就像接力赛,接力棒(事务)是直接递给 B,还是 B 自己拿根新的,或者干脆不拿?
\\n常见的传播行为有这些:
\\nREQUIRED(默认值) :
\\n如果当前已经有事务,那么方法B就加入这个已有的事务。
\\n如果当前没有事务,那么就给方法B新建一个事务。
\\n简单说:有就加入,没有就新建,大家尽量在一个“罩子”里。
\\ngraph TD\\n A[方法A] --\x3e|无事务| B[新建事务]\\n C[方法B] --\x3e|已有事务| D[加入事务]\\n\\n
\\nREQUIRES_NEW:
\\n不管当前有没有事务,它总会为自己创建一个全新的事务。
\\n如果当前已经有事务,那么原来的事务会先“暂停”一下,等方法B这个新事务执行完了,再“恢复”执行。
\\n简单说:我就是要单干,不管外面有没有“罩子”,我自己必须有一个新的。
\\n这通常用在一些需要独立提交或回滚的日志记录、或者不希望影响外部事务的操作上。
\\ngraph TD\\n A[当前事务] --\x3e B[挂起]\\n B --\x3e C[新建事务]\\n\\n
\\nNESTED:
\\nSUPPORTS:
\\nNOT_SUPPORTED:
\\nNEVER:
\\nMANDATORY:
\\n隔离级别是干啥的?
\\n简单说,就是解决多个事务一块跑时互相干扰的问题,比如脏读、不可重复读、幻读这些麻烦。
\\n当多个事务同时操作数据库时,为了避免数据错乱,就需要设定一些规则来隔离它们,这就是事务的隔离级别(Isolation Level) 。隔离级别越高,数据越安全,但并发性能可能越差(因为限制更多了)。
\\n并发事务可能引发以下问题(按严重程度递增):
\\ngraph LR\\n A[脏读] --\x3e B[不可重复读] --\x3e C[幻读]\\n \\n
\\nSpring支持的标准隔离级别:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
---|---|---|---|---|
READ_UNCOMMITTED | ✓ | ✓ | ✓ | 最好 |
READ_COMMITTED | × | ✓ | ✓ | 好 |
REPEATABLE_READ | × | × | ✓ | 一般 |
SERIALIZABLE | × | × | × | 最差 |
READ_UNCOMMITTED(读未提交) :
\\nREAD_COMMITTED(读已提交) :
\\nREPEATABLE_READ(可重复读) :
\\nSERIALIZABLE(串行化) :
\\n选择哪个隔离级别,需要在数据一致性和系统性能之间做权衡。
\\n通常 READ_COMMITTED
或 REPEATABLE_READ
是比较常用的折中选择。
虽然 @Transactional
注解用起来爽,但有时我们需要更精细地控制事务的边界,比如在同一个方法内,部分代码需要事务,部分不需要,或者需要根据条件动态决定是否开启事务。
这时,编程式事务管理就派上用场了。
\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.stereotype.Service;\\nimport org.springframework.transaction.PlatformTransactionManager;\\nimport org.springframework.transaction.TransactionDefinition;\\nimport org.springframework.transaction.TransactionStatus;\\nimport org.springframework.transaction.support.DefaultTransactionDefinition;\\n\\n@Service\\npublic class ManualTransactionService {\\n\\n @Autowired\\n private PlatformTransactionManager transactionManager;\\n\\n public void transferMoneyManually() {\\n // 定义事务属性,比如隔离级别、传播行为等(这里用默认)\\n TransactionDefinition def = new DefaultTransactionDefinition();\\n // 手动开启事务\\n TransactionStatus status = transactionManager.getTransaction(def);\\n System.out.println(\\"手动开启事务...\\");\\n\\n try {\\n // --- 这里是你的业务逻辑 ---\\n System.out.println(\\"执行业务操作 1...\\");\\n System.out.println(\\"执行业务操作 2...\\");\\n // 假设这里可能出错\\n // if (System.currentTimeMillis() % 2 == 0) {\\n // throw new RuntimeException(\\"模拟手动事务中发生异常!\\");\\n // }\\n // --- 业务逻辑结束 ---\\n\\n // 如果一切顺利,手动提交事务\\n transactionManager.commit(status);\\n System.out.println(\\"手动提交事务成功!\\");\\n } catch (Exception e) {\\n // 如果出现任何异常,手动回滚事务\\n transactionManager.rollback(status);\\n System.err.println(\\"手动回滚事务!原因: \\" + e.getMessage());\\n // 记得把异常抛出,让上层知道出错了\\n throw e;\\n }\\n }\\n}\\n\\n\\n
\\n这个过程就像开手动挡汽车,虽然麻烦点,但控制感更强。
\\n它的执行流程大致如下:
\\nsequenceDiagram\\n participant Service\\n participant TransactionManager\\n participant DB\\n \\n Service->>TransactionManager: getTransaction()\\n TransactionManager->>DB: 开启事务\\n Service->>DB: 执行SQL\\n alt 成功\\n Service->>TransactionManager: commit()\\n TransactionManager->>DB: 提交\\n else 失败\\n Service->>TransactionManager: rollback()\\n TransactionManager->>DB: 回滚\\n end\\n \\n
\\n方法可见性:只对 public
方法生效:
@Transactional
加在 private
、protected
或 package-private
方法上是无效的,且 Spring 不会报错(静默失败)。
原理:Spring 事务是基于 AOP 代理实现的,非 public
方法无法被代理类有效拦截。
记住:事务方法必须是公开的(public)!
\\n graph LR\\n A[方法可见性] --\x3e B(public);\\n A --\x3e C(private);\\n A --\x3e D(protected);\\n A --\x3e E(package-private);\\n\\n B -- @Transactional --\x3e F[✔ 生效];\\n C -- @Transactional --\x3e G[✘ 无效];\\n D -- @Transactional --\x3e H[✘ 无效];\\n E -- @Transactional --\x3e I[✘ 无效];\\n
\\n异常处理:默认只认 RuntimeException
和 Error
RuntimeException
或 Error
时,事务才会回滚。IOException
, SQLException
),事务不会回滚!@Transactional(rollbackFor=Exception.class)
才管用rollbackFor = {SpecificException.class, ...}
指定特定异常回滚noRollbackFor
来指定哪些异常不回滚自调用问题:同一个类内部调用会失效
\\n@Transactional
注解的方法 A 调用同一个类里面另一个有 @Transactional
注解的方法 B,方法 B 的事务不会生效。this.methodB()
时,是直接调用原始对象的方法,绕过了代理对象,自然事务拦截器就没机会工作了。ApplicationContext
获取自身的代理 Bean,再用代理对象调用。AspectJ
(配置更复杂)。事务超时:防止长时间锁定资源
\\n@Transactional(timeout = 10)
设置事务超时时间(单位秒)。如果事务执行时间超过设定值,会自动回滚并抛出异常。只读事务:优化查询性能
\\n@Transactional(readOnly = true)
。多数据源事务:需要特殊处理
\\n@Transactional\\npublic void transfer() {\\n try {\\n // 业务代码\\n } catch (Exception e) {\\n // 异常被捕获,事务不会回滚\\n }\\n}\\n\\n
\\n正确做法:要么在 catch
块里手动回滚(如果用编程式事务),要么重新抛出异常(或者包装成 RuntimeException
抛出),让 @Transactional
能捕获到。
// 场景:统计报表,但用了最低隔离级别\\n@Transactional(isolation = Isolation.READ_UNCOMMITTED)\\npublic Report generateReport() {\\n // 这里读取的数据可能是其他事务未提交的“脏”数据\\n List<Data> data = fetchData();\\n // 基于可能不准确的数据生成报表...\\n Report report = processData(data);\\n return report;\\n}\\n\\n
\\n反思:
\\nREAD_UNCOMMITTED
。graph LR\\n A[开始事务] --\x3e B[查询1]\\n B --\x3e C[业务计算]\\n C --\x3e D[远程调用]\\n D --\x3e E[更新数据库]\\n E --\x3e F[提交事务]\\n
\\n问题
\\n建议:
\\n事务教会我们一个朴素的真理:人生没有“部分提交”,每个选择都应当全力以赴,即使失败也要优雅回滚。
\\n就像Spring的事务管理,重要的不是永不犯错,而是知道何时该坚持,何时该放手。
\\n程序员的生活何尝不是一场精心设计的事务?
\\n我们熬夜写代码是begin,成功上线是commit,遇到bug时的回滚不过是下一次尝试的开始。
\\n在这里,愿每一位同行者:
\\n更重要的是,在敲代码之余,也能找到生活的平衡点,身体健康,心情愉悦,享受创造带来的成就,也拥抱生活赋予的温暖。
\\n平安喜乐。
\\n当我们提到 Spring 时,或许首先映入脑海的是 IOC(控制反转)和 AOP(面向切面编程)。它们可以被视为 Spring 的基石。正是凭借其出色的设计,Spring 才能在众多优秀框架中脱颖而出。
\\nSpring 具有很强的扩展性。许多第三方应用程序,如 rocketmq、mybatis、redis 等,都可以轻松集成到 Spring 系统中。让我们一起来看看 Spring 中最常用的十个扩展点。
\\n过去,在开发接口时,如果发生异常,我们通常需要给用户一个更友好的提示。但如果不进行错误处理,例如:
\\n@RequestMapping(\\"/test\\")\\n@RestController\\npublic class TestController {\\n @GetMapping(\\"/division\\")\\n public String division(@RequestParam(\\"a\\") int a, @RequestParam(\\"b\\")int b) {\\n return String.valueOf(a / b);\\n }\\n}\\n\\n
\\n这是一个计算 a/b 结果的方法,通过127.0.0.1:8080/test/division?a=10&b=2
访问后会出现以下结果:
什么?用户能直接看到如此详细的错误信息吗?
\\n这种报错方式给用户带来了非常糟糕的体验。为了解决这个问题,我们通常在接口中捕获异常。
\\n@GetMapping(\\"/division\\")\\npublic String division(@RequestParam(\\"a\\") int a, @RequestParam(\\"b\\") int b) {\\n String result = \\"\\";\\n try {\\n result = String.valueOf(a / b);\\n } catch (ArithmeticException e) {\\n result = \\"params error\\";\\n }\\n return result;\\n}\\n\\n
\\n接口改造后,当发生异常时,会提示:“params error”,用户体验会更好。
\\n如果只是一个接口,那没问题。但如果项目中有成百上千个接口,我们是否需要为所有接口添加异常处理代码呢?
\\n肯定不能这样做的。这时,全局异常处理就派上用场了:RestControllerAdvice。
\\n@RestControllerAdvice\\npublicclass GlobalExceptionHandler {\\n @ExceptionHandler(Exception.class)\\n public String handleException(Exception e) {\\n if (e instanceof ArithmeticException) {\\n return\\"params error\\";\\n }\\n if (e instanceof Exception) {\\n return\\"Internal server exception\\";\\n }\\n returnnull;\\n }\\n}\\n\\n
\\n只需在 handleException 方法中处理异常情况。业务接口可以放心使用,不再需要捕获异常(遵循统一的处理逻辑)。
\\n与 Spring 拦截器相比,Spring MVC 拦截器可以在内部获取 HttpServletRequest 和 HttpServletResponse 等 Web 对象实例。
\\nSpring MVC 拦截器的顶级接口是:HandlerInterceptor,它包含三个方法:
\\npreHandle:在目标方法执行前执行。
\\npostHandle:在目标方法执行后执行。
\\nafterCompletion:在请求完成时执行。
\\n为了方便起见,在一般情况下,我们通常使用 HandlerInterceptor 接口的实现类 HandlerInterceptorAdapter。
\\n如果存在权限认证、日志记录和统计等场景,可以使用此拦截器。
\\n第一步,通过继承 HandlerInterceptorAdapter 类定义一个拦截器:
\\npublic class AuthInterceptor extends HandlerInterceptorAdapter {\\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\\n String requestUrl = request.getRequestURI();\\n if (checkAuth(requestUrl)) {\\n returntrue;\\n }\\n returnfalse;\\n }\\n\\n private boolean checkAuth(String requestUrl) {\\n System.out.println(\\"===Authority Verification===\\");\\n returntrue;\\n }\\n}\\n\\n
\\n第二步,在 Spring 容器中注册此拦截器。
\\n@Configuration\\npublic class WebAuthConfig extends WebMvcConfigurerAdapter {\\n @Bean\\n public AuthInterceptor getAuthInterceptor() {\\n return new AuthInterceptor();\\n }\\n\\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n registry.addInterceptor(new AuthInterceptor());\\n }\\n}\\n\\n
\\n随后,当请求接口时,Spring MVC 可以通过此拦截器自动拦截接口并验证权限。
\\n在日常开发中,我们经常需要从 Spring 容器中获取 Beans。但是你知道如何获取 Spring 容器对象吗?
\\n@Service\\npublic class StudentService implements BeanFactoryAware {\\n private BeanFactory beanFactory;\\n\\n @Override\\n public void setBeanFactory(BeanFactory beanFactory) throws BeansException {\\n this.beanFactory = beanFactory;\\n }\\n\\n public void add() {\\n Student student = (Student) beanFactory.getBean(\\"student\\");\\n }\\n}\\n\\n
\\n实现 BeanFactoryAware 接口,然后重写 setBeanFactory 方法。从这个方法中,可以获取 Spring 容器对象。
\\n@Service\\npublic class StudentService2 implements ApplicationContextAware {\\n private ApplicationContext applicationContext;\\n\\n @Override\\n public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\\n this.applicationContext = applicationContext;\\n }\\n\\n public void add() {\\n Student student = (Student) applicationContext.getBean(\\"student\\");\\n }\\n}\\n\\n
\\n有时我们需要在某个配置类中导入其他一些类,并且导入的类也会被添加到 Spring 容器中。此时,可以使用@Import 注解来完成此功能。
\\n如果你看过它的源代码,会发现导入的类支持三种不同的类型。
\\n然而,我认为最好将普通类和带有@Configuration 注解的配置类分开解释。因此,列出了四种不同的类型:
\\n这种导入方式最简单。导入的类将被实例化为一个 bean 对象。
\\npublic class A {\\n}\\n\\n@Import(A.class)\\n@Configuration\\npublic class TestConfiguration {\\n}\\n\\n
\\n通过@Import 注解导入类 A,Spring 可以自动实例化对象 A。然后,可以在需要的地方通过@Autowired 注解进行注入:
\\n@Autowired\\nprivate A a;\\n\\n
\\n是不是很神奇?不需要添加@Bean 注解就可以实例化对象。
\\n这种导入方式最复杂,因为@Configuration 注解还支持多种组合注解,例如:
\\n@Import
\\n@ImportResource
\\n@PropertySource 等
\\npublic class A {\\n}\\n\\npublicclass B {\\n}\\n\\n@Import(B.class)\\n@Configuration\\npublic class AConfiguration {\\n @Bean\\n public A a() {\\n returnnew A();\\n }\\n}\\n\\n@Import(AConfiguration.class)\\n@Configuration\\npublic class TestConfiguration {\\n}\\n\\n
\\n通过@Import 注解导入一个带有@Configuration 注解的配置类,与该配置类相关的@Import、@ImportResource 和@PropertySource 等注解导入的所有类将一次性全部导入。
\\n这种导入方式需要实现 ImportSelector 接口:
\\npublic class AImportSelector implements ImportSelector {\\n private static final String CLASS_NAME = \\"com.demo.cache.service.A\\";\\n\\n public String[] selectImports(AnnotationMetadata importingClassMetadata) {\\n return new String[]{CLASS_NAME};\\n }\\n}\\n\\n@Import(AImportSelector.class)\\n@Configuration\\npublic class TestConfiguration {\\n}\\n\\n
\\n这种方法的优点是 selectImports 方法返回一个数组,这意味着可以非常方便的导入多个类。
\\n这种导入方式需要实现 ImportBeanDefinitionRegistrar 接口:
\\npublic class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {\\n @Override\\n public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {\\n RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class);\\n registry.registerBeanDefinition(\\"a\\", rootBeanDefinition);\\n }\\n}\\n\\n@Import(AImportBeanDefinitionRegistrar.class)\\n@Configuration\\npublic class TestConfiguration {\\n}\\n\\n
\\n有时我们需要在项目启动时自定义一些附加逻辑,例如加载一些系统参数、资源初始化、预热本地缓存等。我们该怎么做呢?Spring Boot 提供了两个接口来帮助我们实现上述要求:
\\nCommandLineRunner
\\nApplicationRunner
\\n它们的用法非常简单。以 ApplicationRunner 接口为例:
\\n@Component\\npublicclass MyApplicationRunner implements ApplicationRunner {\\n\\n @Override\\n public void run(ApplicationArguments args) throws Exception {\\n // 在这里编写项目启动时需要执行的代码\\n System.out.println(\\"项目启动时执行附加功能,加载系统参数...\\");\\n // 假设这里从配置文件中加载系统参数并进行处理\\n Properties properties = new Properties();\\n try (InputStream inputStream = new FileInputStream(\\"application.properties\\")) {\\n properties.load(inputStream);\\n String systemParam = properties.getProperty(\\"system.param\\");\\n System.out.println(\\"加载的系统参数值为:\\" + systemParam);\\n } catch (IOException e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n\\n
\\n在上述代码中,我们实现了 ApplicationRunner 接口,并重写了 run 方法。在 run 方法中,我们可以编写在项目启动时需要执行的附加功能代码,例如加载系统参数、初始化资源、预热缓存等。这里只是简单地模拟了从配置文件中加载系统参数并打印出来,实际应用中可以根据具体需求进行更复杂的操作。
\\n当项目启动时,Spring Boot 会自动检测并执行实现了 ApplicationRunner 或 CommandLineRunner 接口的类中的 run 方法,从而实现项目启动时的附加功能。
\\n这两个接口的区别在于参数类型不同,ApplicationRunner 的 run 方法参数是 ApplicationArguments,它提供了更多关于应用程序参数的信息,而 CommandLineRunner 的 run 方法参数是原始的字符串数组,直接包含了命令行参数。根据具体需求可以选择使用其中一个接口来实现项目启动时的附加功能。
\\n在实例化 Bean 对象之前,Spring IOC 需要先读取 Bean 的相关属性,将它们保存在 BeanDefinition 对象中,然后通过 BeanDefinition 对象实例化 Bean 对象。
\\n如果你想修改 BeanDefinition 对象中的属性,该怎么做呢?我们可以实现 BeanFactoryPostProcessor 接口。
\\n@Component\\npublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {\\n @Override\\n public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {\\n DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;\\n BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);\\n beanDefinitionBuilder.addPropertyValue(\\"id\\", 123);\\n beanDefinitionBuilder.addPropertyValue(\\"name\\", \\"Dylan Smith\\");\\n defaultListableBeanFactory.registerBeanDefinition(\\"user\\", beanDefinitionBuilder.getBeanDefinition());\\n }\\n}\\n\\n
\\n在 postProcessBeanFactory 方法中,可以获取 BeanDefinition 的相关对象并修改该对象的属性。
\\n目前,Spring 中比较常用的初始化 bean 的方法有:
\\n使用@PostConstruct 注解。
\\n实现 InitializingBean 接口。
\\n@Service\\npublic class AService {\\n @PostConstruct\\n public void init() {\\n System.out.println(\\"===Initializing===\\");\\n }\\n}\\n\\n
\\n在需要初始化的方法上添加@PostConstruct 注解。这样,它就具有了初始化的能力。
\\n@Service\\npublic class BService implements InitializingBean {\\n @Override\\n public void afterPropertiesSet() throws Exception {\\n System.out.println(\\"===Initializing===\\");\\n }\\n}\\n\\n
\\n有时,你希望在初始化 bean 之前和之后实现一些自己的逻辑。
\\n这时,可以实现 BeanPostProcessor 接口。
\\n这个接口目前有两个方法:
\\npostProcessBeforeInitialization:在初始化方法之前调用。
\\npostProcessAfterInitialization:在初始化方法之后调用。
\\n例如:
\\n@Component\\npublic class MyBeanPostProcessor implements BeanPostProcessor {\\n @Override\\n public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {\\n if (bean instanceof User) {\\n ((User) bean).setUserName(\\"Dylan Smith\\");\\n }\\n return bean;\\n }\\n}\\n\\n
\\n如果 Spring 中有一个 User 对象,将其 userName 设置为:Dylan Smith。
\\n实际上,我们经常使用的注解,如@Autowired、@Value、@Resource、@PostConstruct 等,都是通过 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 实现的。
\\n有时,我们需要在关闭 Spring 容器之前做一些额外的工作,例如关闭资源文件。
\\n这时,我们可以实现 DisposableBean 接口并覆盖其 destroy 方法:
\\n@Service\\npublic class DService implements InitializingBean, DisposableBean {\\n @Override\\n public void destroy() throws Exception {\\n System.out.println(\\"DisposableBean destroy\\");\\n }\\n\\n @Override\\n public void afterPropertiesSet() throws Exception {\\n System.out.println(\\"InitializingBean afterPropertiesSet\\");\\n }\\n}\\n\\n
\\n这样,在 Spring 容器销毁之前会调用 destroy 方法。通常,我们会同时实现 InitializingBean 和 DisposableBean 接口,并覆盖初始化方法和销毁方法。
\\n我们都知道,Spring 只支持两种默认的 Scope:
\\nsingleton:在单例作用域中,从 Spring 容器中获取的每个 bean 都是同一个对象。
\\nprototype:在原型作用域中,从 Spring 容器中获取的每个 bean 都是不同的对象。
\\nSpring Web 扩展了 Scope 并添加了:
\\nRequestScope:在同一个请求中,从 Spring 容器中获取的 bean 都是同一个对象。
\\nSessionScope:在同一个会话中,从 Spring 容器中获取的 bean 都是同一个对象。
\\n即便如此,有些场景仍然无法满足我们的要求。
\\n例如,如果我们希望在同一个线程中从 Spring 容器中获取的所有 bean 都是同一个对象,该怎么办呢?
\\n这就需要自定义 Scope。
\\n第一步,实现 Scope 接口:
\\npublic class ThreadLocalScope implements Scope {\\n privatestaticfinal ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal();\\n\\n @Override\\n public Object get(String name, ObjectFactory<?> objectFactory) {\\n Object value = THREAD_LOCAL_SCOPE.get();\\n if (value!= null) {\\n return value;\\n }\\n Object object = objectFactory.getObject();\\n THREAD_LOCAL_SCOPE.set(object);\\n return object;\\n }\\n\\n @Override\\n public Object remove(String name) {\\n THREAD_LOCAL_SCOPE.remove();\\n returnnull;\\n }\\n\\n @Override\\n public void registerDestructionCallback(String name, Runnable callback) {\\n }\\n\\n @Override\\n public Object resolveContextualObject(String key) {\\n returnnull;\\n }\\n\\n @Override\\n public String getConversationId() {\\n returnnull;\\n }\\n}\\n\\n
\\n第二步,将新定义的“Scope”注入到 Spring 容器中:
\\n@Component\\npublic class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {\\n @Override\\n public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {\\n beanFactory.registerScope(\\"threadLocalScope\\", new ThreadLocalScope());\\n }\\n}\\n\\n
\\n第三步,使用新定义的“Scope”:
\\n@Scope(\\"threadLocalScope\\")\\n@Service\\npublic class CService {\\n public void add() {\\n }\\n}\\n\\n
\\n好了,今天的内容就到这里。对 Spring 框架感兴趣的读者可以关注我,后续会分享更多有关 Spring 的相关知识。
","description":"当我们提到 Spring 时,或许首先映入脑海的是 IOC(控制反转)和 AOP(面向切面编程)。它们可以被视为 Spring 的基石。正是凭借其出色的设计,Spring 才能在众多优秀框架中脱颖而出。 Spring 具有很强的扩展性。许多第三方应用程序,如 rocketmq、mybatis、redis 等,都可以轻松集成到 Spring 系统中。让我们一起来看看 Spring 中最常用的十个扩展点。\\n\\n1. 全局异常处理\\n\\n过去,在开发接口时,如果发生异常,我们通常需要给用户一个更友好的提示。但如果不进行错误处理,例如:\\n\\n@RequestMapping(\\"…","guid":"https://juejin.cn/post/7496077120767410187","author":"写bug写bug","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T02:38:30.371Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/221e93a2520849b18d677ef30ad76ee2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YaZYnVn5YaZYnVn:q75.awebp?rk3s=f64ab15b&x-expires=1746585438&x-signature=ae5naUwKiHTghob8cwT2uG%2FbHT8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Spring"],"attachments":null,"extra":null,"language":null},{"title":"消灭空指针,Lombok 给我们的最佳解决方案","url":"https://juejin.cn/post/7496051843538452543","content":"空指针异常是开发者经常遇到的一个问题,通常是由于对一个可能为null的对象调用方法而引发的。传统的解决方案包括显式的null检查和使用Optional类。然而,这些方法可能会导致代码冗长且不易阅读。Lombok提供了一种更简洁的方式,通过@ExtensionMethod
注解来扩展现有类的方法,从而优雅地处理null值,甚至可以实现类似null.foo()这样的效果。
@ExtensionMethod
是Lombok提供的一个注解,它允许开发者为现有类添加扩展方法。这些方法可以在不修改原始类的情况下使用,从而增强类的功能。通过这种方式,可以为可能为null的对象提供安全的默认行为。
创建一个工具类,定义处理空值的静态方法。例如:
\\npublic class NullSafeExtensions {\\n // 安全获取字符串(若为 null 返回空字符串)\\n public static String orEmpty(String str) {\\n return str != null ? str : \\"\\";\\n }\\n\\n // 安全获取集合(若为 null 返回空集合)\\n public static <T> List<T> orEmpty(List<T> list) {\\n return list != null ? list : Collections.emptyList();\\n }\\n\\n // 安全调用方法(避免链式调用 NPE)\\n public static <T, R> R safeCall(T target, Function<T, R> mapper, R defaultValue) {\\n return target != null ? mapper.apply(target) : defaultValue;\\n }\\n}\\n
\\n在需要处理空值的类上添加 @ExtensionMethod
,并指定工具类:
@ExtensionMethod(NullSafeExtensions.class)\\npublic class Main {\\n public static void main(String[] args) {\\n // 示例 1: 处理可能为 null 的字符串\\n String nullableString = null;\\n System.out.println(nullableString.orEmpty()); // 输出空字符串 \\"\\"\\n\\n // 示例 2: 处理可能为 null 的集合\\n List<String> nullableList = null;\\n nullableList.orEmpty().forEach(System.out::println); // 无 NPE\\n\\n // 示例 3: 安全链式调用\\n User user = null;\\n String city = user.safeCall(u -> u.getAddress().getCity(), \\"Unknown\\");\\n System.out.println(city); // 输出 \\"Unknown\\"(无 NPE)\\n }\\n}\\n
\\nLombok 的 @ExtensionMethod
通过简单的语法改造,让空指针问题处理变得更直观。它的核心原理是将类似 obj.method()
的调用,自动转换为静态工具类中的方法(例如 Utils.method(obj)
),并在工具方法内部处理 null
值。具体优势如下:
orEmpty()
),可以直接为 null
对象提供备用值。例如,null
字符串调用 orEmpty()
会返回空字符串,null
集合调用后返回空列表,无需手动写 if (obj == null)
。user.getAddress().getCity()
)一旦某个环节返回 null
就会崩溃。通过 safeCall
扩展方法,可以像这样安全调用:user.safeCall(u -> u.getAddress().getCity(), \\"Unknown\\")
user
或 address
为 null
,也会直接返回 \\"Unknown\\"
,避免逐层判空。obj.orEmpty()
替代繁琐的 if (obj != null)
检查,让代码直接表达业务逻辑。例如:list.orEmpty().forEach(...)
的含义一目了然——“不管列表是否为空,都执行遍历”。总结来说,@ExtensionMethod
把 null
检查隐藏到扩展方法里,让代码既安全又简洁,开发者只需关注“做什么”,而不是反复检查“会不会崩”。
@ExtensionMethod
通过扩展方法为空指针问题提供了解决方案。@ExtensionMethod
注解是一种优雅的方式。我们的各个业务系统上报了用户的登录日志(包括成功和失败的),这些登录日志我们要存起来,对用户的行为进行分析,以便发现一些异常的用户;也为用户画像提供更多的基础数据支持。
\\n但是我们的登录是可以使用邮箱和用户名两种情况登录的,上报的日志是从多个业务场景上报,高峰量还是非常大的,并且我们并没有用户数据库的直连权限,这里有需要将使用用户名登录的和使用邮箱登录的分开存储。
\\n所以在存储之前我们得弄清楚用户是使用哪种场景登录的。
\\n\\n\\n这里的邮箱登录还是用户名登录,我们就是使用的正则来做的一个简单判断。
\\n
发生时间还好是上班时间,钉钉告警出来,正好在工位就立马发现能解决了;避开了悲催的半夜被告警铃声吵醒美梦的悲剧。
\\n下面是使用top
命令查看当时CPU使用情况的截图,也是后背一凉。
实际上面1.2的图就可以看到是哪一个进程导致的CPU飙高,这就非常方便我们定位了,直接找到对应的程序,然后使用jstack
命令查看一下堆栈情况。
打印出来的堆栈情况,可以清晰的看到大量的java.util.regex.Pattern
对象,这个时候如果想不到我们可以dump
一下具体分析,万幸的是这个项目从头到尾都是我负责的,里面只有唯一的一个地方使用到了正则,所以我立马定位到了具体的代码(疯狂的牛马)。
下面是我真正正则校验的代码,实际看起来还是非常清晰明了的。
\\n/** 正则表达式:验证邮箱 */\\npublic static final String REGEX_EMAIL = \\"^([a-z0-9A-Z]+[-|\\\\\\\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\\\\\.)+[a-zA-Z]{2,}$\\";\\n\\n/**\\n * 校验邮箱\\n *\\n * @param email 邮箱\\n * @return 校验通过返回true,否则返回false\\n */\\npublic static boolean isEmail(String email) {\\nreturn Pattern.matches(REGEX_EMAIL, email);\\n}\\n
\\n单独从上面实际是不怎么看的出我们的问题的。
\\n找到了问题产生点,问题分析说起来就非常简单了;哪怕我想不到,我也可以直接问问AI。
\\n哎,看了AI的回答,相信你也猜到了问题产生的核心原因。
\\n看到前面的正则,我们就知道,我这里是符合贪婪匹配与长文本和贪婪匹配与长文本这两点的,至于并发问题,因为我的实际处理程序是通过MQ
削峰的,所以这里并不满足。
当然,如果你并不能直接看出来,那也简单,你把上面的正则丢给AI分析分析。
\\n咋样,强大的AI立马告诉了你结果。并且他还可以给出一些解决办法。
\\n前面已经说过AI会给你提供一些解决办法,但是AI并不是万能的,每个业务场景都有自己的特殊,我们可以根据自己的业务来综合考虑。
\\n先看看AI的优化建议。
\\nAI的优化建议非常标准,但是我考虑这里业务场景实际有很大限制,所以没必要按照它的来。
\\n业务场景补充描述:
\\n邮箱最长长度是80
\\n用户名最长长度是256(你别说,还真有人定格设置)
\\n前面补充了业务场景,相信很多人已经猜到了解决办法。
\\n没有,就是限制一下触发正则的前提。
\\nif (username.length() <= 80 && username.contains(\\"@\\")) {\\n // 进行正则校验 \\n}\\n
\\n虽然排查到解决,看着都非常简单,但是实际操作起来还是很繁琐的,一顿骚操作,再经过测试到发布到正式环境,也耗时1小时多。
\\n任何代码都不是万能的,这个正则校验工具类,经过了不知道多长时间的使用,在本项目就是踩坑了,要是不从堆栈去仔细排查,你怕是脑子想爆炸都找不到原因。
\\n在开发过程中真的需要多多思考,结合业务场景仔细去设计代码,没有万能的代码,但是有万能的程序员,相信你看了这篇文章也会有不一样的收获。
","description":"一、事件背景 1.1 需求说明\\n\\n我们的各个业务系统上报了用户的登录日志(包括成功和失败的),这些登录日志我们要存起来,对用户的行为进行分析,以便发现一些异常的用户;也为用户画像提供更多的基础数据支持。\\n\\n但是我们的登录是可以使用邮箱和用户名两种情况登录的,上报的日志是从多个业务场景上报,高峰量还是非常大的,并且我们并没有用户数据库的直连权限,这里有需要将使用用户名登录的和使用邮箱登录的分开存储。\\n\\n所以在存储之前我们得弄清楚用户是使用哪种场景登录的。\\n\\n这里的邮箱登录还是用户名登录,我们就是使用的正则来做的一个简单判断。\\n\\n1.2 事件发生时间及发现过程…","guid":"https://juejin.cn/post/7496025111294115881","author":"竹子爱揍功夫熊猫","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T01:04:44.541Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/81ed16456527437f9c3a444059b90fec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=2lrr2e6GoeJX%2FiXJtNq1XfK%2FjgY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/62413ca7f01c421db52fbaeb6471747f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=P2JQuZJR1i8WUxcCEZ%2BrA67%2BoZ4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7176233deb994891a30594fffa87bc4a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=Dqnhf3D%2B3rPc3Xp%2BozD7FwgN1%2BY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/087ab2721bac43d7a0acf5f7404b7b56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=cbzJRc0RZjTPUYy3brv8iY6F4lM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/81fd6593487545b99409c6ec89065d80~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=VjVw9kf21MTX7YQnJ%2BEWxf0IaRE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a511ec53fa242e1b0fcffaa60bcd39d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56u55a2Q54ix5o-N5Yqf5aSr54aK54yr:q75.awebp?rk3s=f64ab15b&x-expires=1746579609&x-signature=mzKXdp2zgPn1QlvxMelDW6MPlYE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","AI编程","Java","正则表达式"],"attachments":null,"extra":null,"language":null},{"title":"别让 If-Else ,变成 “懒婆娘的裹脚布”","url":"https://juejin.cn/post/7496033842748489778","content":"if-else 语句堪称程序设计领域中最为常见的控制结构之一 。在软件开发的历程中,每一位开发者都曾运用if-else语句来实现条件判断。在程序逻辑较为简单的场景下,借助if-else进行条件判断,的确能够高效地解决诸多业务逻辑问题。
\\n但是随着项目规模的不断扩大以及代码复杂度的持续攀升,大量堆砌的if-else语句逐渐暴露出一系列棘手问题。过度的条件判断会致使代码篇幅大幅增长,逻辑结构错综复杂,使得代码的可读性大打折扣。这不仅为开发人员理解代码意图设置了重重障碍,还严重削弱了代码的可维护性与可扩展性。当需要对功能进行修改或添加新功能时,牵一发而动全身,很容易引入新的错误,极大地增加了软件开发与维护的成本。可以说臃肿的 if-else判断,像极了懒婆娘的裹脚布。
\\n在if-else语句存在的问题里,可读性欠佳是最为直观的一点。当程序中的条件判断不断增多,if-else语句的代码量会迅速膨胀,逻辑层级也随之不断加深。这就导致代码变得异常冗长,理解起来极为困难。特别是当if-else语句用于处理多种不同类型的业务逻辑时,代码所表达的意图会变得模糊不清,甚至可能对开发者产生误导。
\\n举例来说,在实现一个用户权限管理功能时,往往需要对多个权限进行判断。若将所有的权限判断逻辑都集中写在一个if-else语句中,代码的规模会变得相当庞大。以下是一个简单的示例:
\\npublic class PermissionChecker {\\n public boolean hasPermission(User user, String action) {\\n if (user.isAdmin()) {\\n return true;\\n } else if (user.hasRole(\\"EDITOR\\") && action.equals(\\"edit\\")) {\\n return true;\\n } else if (user.hasRole(\\"VIEWER\\") && action.equals(\\"view\\")) {\\n return true;\\n } else if (user.hasRole(\\"GUEST\\") && action.equals(\\"read\\")) {\\n return true;\\n } else {\\n return false;\\n }\\n }\\n}\\n
\\n像这样的代码,不仅阅读起来十分吃力,而且在进行功能扩展时也面临诸多挑战。一旦需要添加新的权限检查类型或行为,开发者就不得不频繁地修改和增加if-else语句。在此过程中,极易引入新的错误,从而影响整个程序的稳定性和可靠性。
\\n在系统不断发展和功能持续拓展的过程中,if-else语句的数量常常会不可避免地逐渐增多。这一现象会使得代码的规模日益庞大,逻辑结构也愈发复杂。每当业务需求发生变化,开发者就不得不对大量的if-else语句进行修改,以添加新的条件判断逻辑。然而,这种基于if-else的扩展方式,往往会带来诸多问题,其中最突出的就是容易引发不必要的代码重复,并且在修改过程中极有可能引入新的错误。
\\n以之前提到的用户权限管理代码为例,如果后续需要为不同的用户角色添加更多的权限判断逻辑,开发者就必须逐个对现有的if-else语句进行修改,增加新的条件分支。这种操作不仅会大幅增加开发和维护的成本,而且随着代码的不断修改,逻辑结构会变得愈发混乱,给后续的开发工作带来极大的困扰。
\\n大量存在的if-else语句会给代码的测试工作带来相当大的挑战,使测试过程变得异常复杂。当程序中的条件判断数量众多时,为了确保代码的正确性,单元测试需要覆盖的范围就会变得极为广泛。这就要求开发者必须编写大量的测试用例,以涵盖各种可能的条件组合。
\\n尤其是在处理复杂的业务逻辑时,测试用例的数量会呈指数级增长。当存在多种不同的输入组合时,测试工作的难度更是急剧上升。此外,由于if-else语句往往与具体的业务逻辑紧密耦合,对其中的条件判断进行任何修改,都有可能引发一系列的连锁反应,进而导致需要进行大量的回归测试,以确保修改不会对原有功能造成影响。
\\nif-else语句在实际应用中,常常将控制流逻辑与业务逻辑混杂在一起。这种情况会导致业务逻辑的抽象层次较低,使得代码中的控制流和业务规则未能得到有效的分离。这样的设计方式,不仅会让业务逻辑变得晦涩难懂,增加开发者理解和维护代码的难度,而且在需要对业务规则进行单独修改或重构时,也会面临诸多困难,难以实现高效的代码优化和功能扩展。
\\n策略模式(Strategy Pattern)作为一种经典的设计模式,能够有效地将不同的行为封装到各自独立的策略类中,进而规避使用复杂的if-else判断语句。其核心思想在于将行为与类进行分离,使得行为可以在程序运行的过程中灵活地进行动态切换,极大地增强了代码的灵活性和可维护性。
\\n以之前讨论的用户权限管理场景为例,我们可以运用策略模式来替代原有的if-else语句,以实现更加简洁高效的权限判断逻辑。首先,定义一个统一的权限检查接口PermissionStrategy,该接口规定了权限检查的方法签名,为后续不同权限策略的实现提供了统一的标准:
\\npublic interface PermissionStrategy {\\n boolean hasPermission(User user, String action);\\n}\\n
\\n接着,针对每种具体的权限类型,创建相应的策略实现类。这些类分别实现了PermissionStrategy接口,并根据各自的权限规则来实现hasPermission方法:
\\npublic class AdminPermissionStrategy implements PermissionStrategy {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.isAdmin();\\n }\\n}\\n\\npublic class EditorPermissionStrategy implements PermissionStrategy {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"EDITOR\\") && action.equals(\\"edit\\");\\n }\\n}\\n\\npublic class ViewerPermissionStrategy implements PermissionStrategy {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"VIEWER\\") && action.equals(\\"view\\");\\n }\\n}\\n\\npublic class GuestPermissionStrategy implements PermissionStrategy {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"GUEST\\") && action.equals(\\"read\\");\\n }\\n}\\n
\\n复制
\\n最后,创建一个PermissionChecker类,在该类中通过维护一个Map来管理各种权限策略,并利用这些策略来进行权限检查:
\\npublic class PermissionChecker {\\n private Map<String, PermissionStrategy> strategyMap;\\n\\n public PermissionChecker() {\\n strategyMap = new HashMap<>();\\n strategyMap.put(\\"ADMIN\\", new AdminPermissionStrategy());\\n strategyMap.put(\\"EDITOR\\", new EditorPermissionStrategy());\\n strategyMap.put(\\"VIEWER\\", new ViewerPermissionStrategy());\\n strategyMap.put(\\"GUEST\\", new GuestPermissionStrategy());\\n }\\n\\n public boolean hasPermission(User user, String action) {\\n for (PermissionStrategy strategy : strategyMap.values()) {\\n if (strategy.hasPermission(user, action)) {\\n return true;\\n }\\n }\\n return false;\\n }\\n}\\n
\\n通过上述方式,我们成功地避免了在if-else语句中进行繁杂的条件判断,而是将每个判断条件封装到独立的策略类中。这种实现方式使得代码结构更加清晰明了,并且在需要添加新的权限策略时,只需创建新的策略实现类并将其添加到strategyMap中即可,大大提高了代码的扩展性。
\\n工厂模式(Factory Pattern)作为一种广泛应用的创建对象的设计模式,其核心在于将对象的创建逻辑集中封装在一个专门的工厂类中。通过这种方式,在程序代码中能够有效地避免编写大量冗余的if-else语句,而是借助工厂方法依据特定条件动态地选取并创建合适的对象,从而提升代码的简洁性和可维护性。
\\n继续以上文的用户权限管理场景为例,我们可以运用工厂模式来实现不同权限策略对象的创建。创建一个PermissionStrategyFactory工厂类,该类负责根据传入的用户角色信息来创建对应的权限策略对象:
\\npublic class PermissionStrategyFactory {\\n public static PermissionStrategy getPermissionStrategy(String role) {\\n switch (role) {\\n case \\"ADMIN\\":\\n return new AdminPermissionStrategy();\\n case \\"EDITOR\\":\\n return new EditorPermissionStrategy();\\n case \\"VIEWER\\":\\n return new ViewerPermissionStrategy();\\n case \\"GUEST\\":\\n return new GuestPermissionStrategy();\\n default:\\n throw new IllegalArgumentException(\\"Unknown role: \\" + role);\\n }\\n }\\n}\\n
\\n上述代码中,getPermissionStrategy方法通过switch语句对传入的role参数进行判断,根据不同的角色值返回相应的权限策略对象。如果遇到未知的角色值,则抛出IllegalArgumentException异常。
\\n然后,对PermissionChecker类进行修改,使其通过工厂类来获取合适的权限策略对象,进而完成权限检查操作:
\\npublic class PermissionChecker {\\n public boolean hasPermission(User user, String action) {\\n PermissionStrategy strategy = PermissionStrategyFactory.getPermissionStrategy(user.getRole());\\n return strategy.hasPermission(user, action);\\n }\\n}\\n
\\n通过引入工厂模式,我们有效地避免了在PermissionChecker类中使用复杂的if-else语句进行权限策略对象的创建和选择。这种设计将对象的创建过程与使用过程进行了分离,使得代码结构更加清晰。当需要新增或修改权限策略时,只需要在工厂类中进行相应的调整,而不会对权限检查的核心逻辑造成影响,大大提高了代码的扩展性和可维护性。
\\n在 Java 编程语言中,多态性作为面向对象编程的核心特性之一,发挥着至关重要的作用。它能够帮助我们有效地规避大量的if-else条件判断语句,通过继承和方法重写机制来实现不同的行为表现,从而使代码在扩展和维护方面变得更加灵活便捷。
\\n以用户权限管理这一常见的业务场景为例,我们可以巧妙地运用多态性来优化权限检查的逻辑。,定义一个抽象的UserPermission类,该类中声明了一个抽象方法hasPermission,用于定义权限检查的通用接口,后续不同用户角色的权限类将继承这个抽象类并实现该方法:为每种具体的用户角色创建对应的权限类,这些类继承自UserPermission抽象类,并通过重写hasPermission方法来实现各自独特的权限检查逻辑:
\\npublic abstract class UserPermission {\\n public abstract boolean hasPermission(User user, String action);\\n}\\n\\npublic class AdminPermission extends UserPermission {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.isAdmin();\\n }\\n}\\n\\npublic class EditorPermission extends UserPermission {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"EDITOR\\") && action.equals(\\"edit\\");\\n }\\n}\\n\\npublic class ViewerPermission extends UserPermission {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"VIEWER\\") && action.equals(\\"view\\");\\n }\\n}\\n
\\n最后,在PermissionChecker类中充分利用多态性来处理不同用户角色的权限检查。通过调用user.getPermission()方法获取对应的权限对象,然后直接调用该权限对象的hasPermission方法来完成权限检查操作:
\\npublic class PermissionChecker {\\n public boolean hasPermission(User user, String action) {\\n UserPermission permission = user.getPermission();\\n return permission.hasPermission(user, action);\\n }\\n}\\n
\\n通过运用多态性这种编程方式,我们成功地避免了在代码中编写大量繁琐的条件判断语句。同时,这种设计使得代码结构更加清晰,层次更加分明。当需要新增或修改用户角色的权限逻辑时,只需要创建新的权限类或者修改现有权限类的hasPermission方法的实现即可,无需对PermissionChecker类的核心逻辑进行大规模的改动,极大地提高了代码的可扩展性和可维护性。
\\n在 Java 编程中,枚举(Enum)的功能并不仅限于表示一组固定的常量。它还具备承载行为的能力,这一特性使得我们能够借助枚举来简化复杂的条件判断逻辑,有效地避免编写冗长的if-else语句。通过将不同的行为逻辑封装到枚举类的各个枚举常量中,我们可以实现代码结构的优化,使代码更加简洁明了。
\\n例如,假设我们要根据不同的用户角色检查权限,我们可以定义一个枚举来替代 if-else 判断:
\\npublic enum RolePermission {\\n ADMIN {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.isAdmin();\\n }\\n },\\n EDITOR {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"EDITOR\\") && action.equals(\\"edit\\");\\n }\\n },\\n VIEWER {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"VIEWER\\") && action.equals(\\"view\\");\\n }\\n },\\n GUEST {\\n @Override\\n public boolean hasPermission(User user, String action) {\\n return user.hasRole(\\"GUEST\\") && action.equals(\\"read\\");\\n }\\n };\\n\\n public abstract boolean hasPermission(User user, String action);\\n}\\n
\\n通过使用枚举这种方式,我们将每种权限检查的逻辑都封装在了不同的枚举常量中。当业务需求发生变化,需要扩展权限检查逻辑时,我们只需要在枚举类中添加新的枚举值,并实现相应的hasPermission方法即可,而无需对现有的代码结构进行大规模的修改。这种方式不仅提高了代码的可扩展性,还使得代码的维护更加方便,减少了因修改代码而引入错误的风险。
\\n虽然if-else语句是编程中的基本控制结构,但它的过度使用可能会导致代码变得冗长、难以理解和扩展。开发过程中,过度依赖if-else会导致代码冗长、可读性差、扩展性差,并且难以测试和维护,我们需重视并寻求更优的编程方式。
","description":"if-else 语句堪称程序设计领域中最为常见的控制结构之一 。在软件开发的历程中,每一位开发者都曾运用if-else语句来实现条件判断。在程序逻辑较为简单的场景下,借助if-else进行条件判断,的确能够高效地解决诸多业务逻辑问题。 但是随着项目规模的不断扩大以及代码复杂度的持续攀升,大量堆砌的if-else语句逐渐暴露出一系列棘手问题。过度的条件判断会致使代码篇幅大幅增长,逻辑结构错综复杂,使得代码的可读性大打折扣。这不仅为开发人员理解代码意图设置了重重障碍,还严重削弱了代码的可维护性与可扩展性。当需要对功能进行修改或添加新功能时,牵一发而动全身…","guid":"https://juejin.cn/post/7496033842748489778","author":"不惑_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T00:28:30.687Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9167877b42e24154be55ef1f730d9249~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiN5oORXw==:q75.awebp?rk3s=f64ab15b&x-expires=1746574756&x-signature=1%2BXXd0tC5FsJTX%2BO8wZhSHGsoBE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","架构"],"attachments":null,"extra":null,"language":null},{"title":"性能比拼: Go vs Java","url":"https://juejin.cn/post/7496021698959147046","content":"本内容是对知名性能评测博主 Anton Putra Go (Golang) vs Java: Performance Benchmark 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在","description":"本内容是对知名性能评测博主 Anton Putra Go (Golang) vs Java: Performance Benchmark 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在","guid":"https://juejin.cn/post/7496021698959147046","author":"fliter","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T13:28:37.104Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"你一定想不到, 2025年了, 我竟然开始写php了","url":"https://juejin.cn/post/7495773306555269130","content":"年初我的两个想法
\\n在2025年, 我爱上了 Laravel! - #掘金沸点#
\\nlaravel + filament太优雅了, 感觉自己像个土狗#网页链接# - #掘金沸点#
\\n之后, 我终于开始写php了,
\\n首先要面对的问题就是断点调试, 这里还真有一点点小复杂,\\n首先说明 Jetbrains 的 文档 https://www.jetbrains.com/help/phpstorm/2022.2/configuring-xdebug.html 还是比较准确的, 下面我说几个比较容易忽略的点
\\n因为读取你的输出了, 所以精准的定位到你的 php.ini 位置, 下载dll 并且放入ext目录中\\n特别强调你要重启你的 http server, 如果你是使用phpstorm 加载的这个页面, 最好重启下phpstorm, 我一开始这里都发现了调试器了, 但是phpinfo() 一直没有输出让我郁闷很久
\\n然后在能断点调试之前还有一个很重要的内容是
\\nxdebug.mode
这个值也接受 develop/trace, 但是只有 debug 能触发断点,
Xdebug Helper
这个扩展, 原作者因为协议问题被停用了, jetbrains 又维护了一个, 这个直接在 chrome store 里搜索安装即可另一种就是 cli, 即终端调试脚本等
\\nphp scripts/main.php\\n
\\n这种, 需要先设置一个环境变量
\\n这些前置工作都准备好之后就是在 phpstorm 中调试了,
\\n首先要点击 phpstorm 的这个监听按钮
\\n然后点击php的行首增加一个断点, 这时刷新页面, phpstorm 可能会先弹出是否接受请求的弹窗, 点击接受后, 代码执行到这里就可以看相关变量等信息了, 十分方便.
\\n按照文档操作, 并不复杂, 但是因为涉及到的修改项比较多, 其中一个没执行到, 便无法达到预期, 所以写下这篇文章记录一下.
","description":"年初我的两个想法 在2025年, 我爱上了 Laravel! - #掘金沸点#\\n\\nlaravel + filament太优雅了, 感觉自己像个土狗#网页链接# - #掘金沸点#\\n\\n之后, 我终于开始写php了,\\n\\n首先要面对的问题就是断点调试, 这里还真有一点点小复杂, 首先说明 Jetbrains 的 文档 https://www.jetbrains.com/help/phpstorm/2022.2/configuring-xdebug.html 还是比较准确的, 下面我说几个比较容易忽略的点\\n\\n使用 phpinfo() 获取到你当前的版本输出, 然后粘贴到…","guid":"https://juejin.cn/post/7495773306555269130","author":"NowStudio","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T08:09:21.433Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a6577510ff4a4b77935e654f758f850b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=SoEgpRVXJ0sXcX9ozETv2SOZczY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/362796a3518448e6aac4f8727b971d77~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=ZSjrWdH7CrcMXbFq9faXHrbyI6c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3bc5dfa7253948079071578b7fcbe044~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=1fsYAw5MpBFrhVm8E3COiXQOYZ8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8df6363ee7648cdacd87240f39d6e3b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=MKfyTUfj6LTAoyPCl2IWtk%2F8Jmg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4c3a31d14f4c4eed994d0a2e62530498~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=KWxUscLL0%2B2yF4Pv%2BazS9KWFjZk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15cdcee3e6be4eed98bbef3945179e4a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=ORK8uXdG%2BNHFsAdoVjOC%2BC6Gj0w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f9364261442742dda149d86addd801ac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTm93U3R1ZGlv:q75.awebp?rk3s=f64ab15b&x-expires=1745914160&x-signature=4%2FUQOsFEbxMAKgl033rXp1rgY18%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","PHP"],"attachments":null,"extra":null,"language":null},{"title":"Spring Cache修仙指南:从青铜到王者的缓存通关秘籍","url":"https://juejin.cn/post/7495703953666916352","content":"想象你是个996的社畜程序员,每次想喝肥宅快乐水都要跑到3公里外的仓库(数据库)去拿,直到有一天...
\\n你在工位旁开了个小卖部(缓存)!
\\n这就是Spring Cache的奥义——让数据库这个\\"大冤种\\"少跑腿!
\\n注解 | 江湖绝技 | 口头禅 |
---|---|---|
@Cacheable | 乾坤大挪移(优先取缓存) | \\"这题我见过,直接抄答案!\\" |
@CachePut | 移花接木(强制更新缓存) | \\"旧的不去新的不来!\\" |
@CacheEvict | 化骨绵掌(删除缓存) | \\"毁灭吧,赶紧的!\\" |
// 配置本地缓存(Caffeine)\\nspring.cache.type=caffeine\\nspring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=5m\\n
\\n就像在工位抽屉里藏零食,优点是伸手就能摸到,缺点是换工位(重启服务)就没了...
\\nspring:\\n cache:\\n type: redis\\n redis:\\n time-to-live: 30m # 商品保质期\\n key-prefix: \\"BUFF:\\" # 货架标签\\n
\\n现在你是连锁超市老板(Redis集群),全公司程序猿都能来蹭吃蹭喝,但记得:
\\n@Cacheable(value = \\"外卖\\", \\n key = \\"#userId + \'_\' + #restaurantId\\",\\n unless = \\"#result.price > 50\\") // 超过50块的不缓存\\npublic String 点外卖(Long userId, String restaurantId) {\\n System.out.println(\\"【真实伤害】数据库查询中...\\");\\n return \\"香辣鸡腿堡套餐\\";\\n}\\n
\\n当同事问你为什么总不写SQL——\\"因为我在摸鱼啊(缓存命中)!\\"
\\n@CacheEvict(value = \\"购物车\\", \\n allEntries = true, // 清空整个货架\\n beforeInvocation = true) // 双十一前先清缓存\\npublic void 结算购物车(Long userId) {\\n // 支付成功后让缓存原地爆炸\\n}\\n
\\n记住:删缓存要像分手一样干脆,别给脏数据留机会!
\\n@Cacheable(value = \\"商品详情\\", \\n unless = \\"#result == null\\") // 缓存空对象\\npublic Product 查商品(String productId) {\\n Product product = 数据库查不到的方法();\\n return product != null ? product : new Product(\\"暂无数据\\");\\n}\\n
\\n遇到疯狂刷不存在的ID?给他返回\\"空气商品\\",让他刷个寂寞!
\\n@Bean\\npublic RedisCacheManager 随机过期大法(RedisConnectionFactory factory) {\\n return RedisCacheManager.builder(factory)\\n .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()\\n .entryTtl(Duration.ofMinutes(30 + new Random().nextInt(10)))) // 随机过期时间\\n .build();\\n}\\n
\\n别让所有缓存像大学宿舍一样集体断电,给每个缓存不同的\\"死亡时间\\"!
\\n@PostConstruct // 服务启动时执行\\npublic void 偷偷加载缓存() {\\n List<热门商品> items = 偷偷查数据库();\\n items.forEach(item -> 缓存起来(item.getId(), item));\\n}\\n
\\n就像提前把爆款商品堆在超市门口,开门瞬间直接开抢!
\\ngraph LR\\n 用户 --\x3e 本地小卖部(Caffeine)\\n 本地小卖部 --\x3e 中央超市(Redis)\\n 中央超市 --\x3e 终极仓库(MySQL)\\n
\\n记住:
\\n症状:明明调了@CacheEvict,缓存还在躺尸
\\n把脉:
症状:Redis里存的值像天书
\\n药方:
@Bean\\npublic RedisTemplate<String, Object> 防乱码模板() {\\n RedisTemplate<String, Object> template = new RedisTemplate<>();\\n template.setKeySerializer(new StringRedisSerializer()); // 钥匙别乱改\\n template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); // JSON大法好\\n return template;\\n}\\n
\\n命名规范:
\\nuser:profile:1001
(用户档案)cache1
(这是要给后人留坑?)监控三件套:
\\n防御性编程:
\\n最后送上终极奥义:
\\n缓存不是银弹,乱用缓存一时爽,数据一致火葬场!
各位缓存修仙者,你在闯荡江湖时遇到过哪些奇葩缓存问题?是缓存击穿让你一夜秃头,还是缓存雪崩让你怀疑人生?欢迎在评论区分享你的渡劫经历,点赞最高的道友将获得【防脱发洗发水】一瓶(虚拟版)!
","description":"一、缓存世界的生存法则 1.1 缓存是什么?程序员的\\"小卖部哲学\\"\\n\\n想象你是个996的社畜程序员,每次想喝肥宅快乐水都要跑到3公里外的仓库(数据库)去拿,直到有一天...\\n\\n你在工位旁开了个小卖部(缓存)!\\n\\n高频访问:肥宅水、泡面常备(热点数据)\\n快速响应:伸手就能拿到(内存级速度)\\n过期策略:薯片每周五补货(TTL时间)\\n\\n这就是Spring Cache的奥义——让数据库这个\\"大冤种\\"少跑腿!\\n\\n1.2 三大护法注解(缓存界的桃园三结义)\\n注解\\t江湖绝技\\t口头禅@Cacheable\\t乾坤大挪移(优先取缓存)\\t\\"这…","guid":"https://juejin.cn/post/7495703953666916352","author":"小厂永远得不到的男人","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T06:13:15.994Z","media":null,"categories":["后端","Spring","面试"],"attachments":null,"extra":null,"language":null},{"title":"线上FullGC问题如何排查 - Java版","url":"https://juejin.cn/post/7495649309691707433","content":"Java线上FullGC问题如何排查 在线上环境,我们可能会经常遇到程序执行缓慢,甚至频繁出现卡顿的情况。有时候通过top命令查看进程,甚至出现cpu运行接近100% 的情况。","description":"Java线上FullGC问题如何排查 在线上环境,我们可能会经常遇到程序执行缓慢,甚至频繁出现卡顿的情况。有时候通过top命令查看进程,甚至出现cpu运行接近100% 的情况。","guid":"https://juejin.cn/post/7495649309691707433","author":"ThinkSmart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T02:59:45.846Z","media":null,"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"瞧瞧别人家的日期处理,那叫一个优雅!","url":"https://juejin.cn/post/7495598591488491530","content":"在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间。
\\n比如:2025-04-21、2025/04/21、2025年04月21日等等。
\\n有些字段是String类型,有些是Date类型,有些是Long类型。
\\n如果不同的数据类型,经常需要相互转换,如果处理不好,可能会出现很多意想不到的问题。
\\n这篇文章跟大家一起聊聊日期处理的常见问题,和相关的解决方案,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\n在文章的开头,先给大家列举一个非常经典的日期格式化问题:
\\n// 旧代码片段(线程不安全的经典写法)\\npublic class OrderService {\\n\\n private SimpleDateFormat sdf = new SimpleDateFormat(\\"yyyy-MM-dd HH:mm:ss\\");*\\n\\n public void saveOrder(Order order) {\\n // 线程A和线程B同时进入该方法\\n String createTime = sdf.format(order.getCreateTime()); \\n // 可能出现\\"2023-02-30 12:00:00\\"这种根本不存在的日期\\n orderDao.insert(createTime);**\\n }\\n\\n}\\n
\\n问题复现场景:
\\n问题根源:SimpleDateFormat内部使用了共享的Calendar实例,多线程并发修改会导致数据污染。
\\n我们在处理日期的时候,还可能会遇到夏令时转换的问题:
\\n// 错误示范:简单加减8小时\\npublic Date convertToBeijingTime(Date utcDate) {\\n Calendar cal = Calendar.getInstance();\\n cal.setTime(utcDate);\\n cal.add(Calendar.HOUR, 8); // 没考虑夏令时切换问题\\n return cal.getTime();\\n}\\n
\\n夏令时是一种在夏季期间将时间提前一小时的制度,旨在充分利用日光,病节约能源。
\\n在一些国家和地区,夏令时的开始和结束时间是固定的。
\\n而在一些国家和地区,可能会根据需要调整。
\\n在编程中,我们经常需要处理夏令时转换的问题,以确保时间的正确性。
\\n隐患分析:2024年10月27日北京时间凌晨2点会突然跳回1点,直接导致订单时间计算错误
\\n在Java8之前,一般是通过ThreadLocal解决多线程场景下,日期转换的问题。
\\n例如下面这样:
\\n// ThreadLocal封装方案(适用于JDK7及以下)\\npublic class SafeDateFormatter {\\n private static final ThreadLocal<DateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> \\n new SimpleDateFormat(\\"yyyy-MM-dd HH:mm:ss\\")\\n );\\n\\n public static String format(Date date) {\\n return THREAD_LOCAL.get().format(date);\\n }\\n}\\n
\\n线程安全原理:
\\n原理揭秘:通过ThreadLocal为每个线程分配独立DateFormat实例,彻底规避线程安全问题。
\\n在Java8之后,提供了LocalDateTime类对时间做转换,它是官方推荐的方案。
\\n例如下面这样:
\\n// 新时代写法(线程安全+表达式增强)\\npublic class ModernDateUtils {\\n public static String format(LocalDateTime dateTime) {\\n return dateTime.format(DateTimeFormatter.ofPattern(\\"yyyy-MM-dd HH:mm:ss\\"));\\n }\\n\\n public static LocalDateTime parse(String str) {\\n return LocalDateTime.parse(str, DateTimeFormatter.ISO_LOCAL_DATE_TIME);\\n }\\n}\\n
\\n黑科技特性:
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n下面这个例子是基于时区计算营业时长:
\\n// 正确示范:基于时区计算营业时长\\npublic Duration calculateBusinessHours(ZonedDateTime start, ZonedDateTime end) {\\n // 显式指定时区避免歧义\\n ZonedDateTime shanghaiStart = start.withZoneSameInstant(ZoneId.of(\\"Asia/Shanghai\\"));\\n ZonedDateTime newYorkEnd = end.withZoneSameInstant(ZoneId.of(\\"America/New_York\\"));\\n \\n // 自动处理夏令时切换\\n return Duration.between(shanghaiStart, newYorkEnd);\\n}\\n
\\n底层原理:通过ZoneId维护完整的时区规则库(含历史变更数据),自动处理夏令时切换。
\\n日均亿级请求的处理方案:
\\n// 预编译模式(性能提升300%)\\npublic class CachedDateFormatter {\\n private static final Map<String, DateTimeFormatter> CACHE = new ConcurrentHashMap<>();\\n\\n public static DateTimeFormatter getFormatter(String pattern) {\\n return CACHE.computeIfAbsent(pattern, DateTimeFormatter::ofPattern);\\n }\\n}\\n
\\n我们可以使用static final这种预编译模式,来提升日期转换的性能。
\\n性能对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 内存占用 | 初始化耗时 | 格式化速度 |
---|---|---|---|
每次新建Formatter | 1.2GB | 2.3s | 1200 req/s |
预编译缓存 | 230MB | 0.8s | 5800 req/s |
为了方便统一解决时区问题,我们可以使用全局时区上下文+拦截器。
\\n例如下面这样:
\\n// 全局时区上下文传递\\npublicclass TimeZoneContext {\\n privatestaticfinal ThreadLocal<ZoneId> CONTEXT_HOLDER = new ThreadLocal<>();\\n\\n public static void setTimeZone(ZoneId zoneId) {\\n CONTEXT_HOLDER.set(zoneId);\\n }\\n\\n public static ZoneId getTimeZone() {\\n return CONTEXT_HOLDER.get();\\n }\\n}\\n\\n// 在Spring Boot拦截器中设置时区\\n@Component\\npublicclass TimeZoneInterceptor implements HandlerInterceptor {\\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\\n String timeZoneId = request.getHeader(\\"X-Time-Zone\\");\\n TimeZoneContext.setTimeZone(ZoneId.of(timeZoneId));\\n returntrue;\\n }\\n}\\n
\\n此外,还需要在请求接口的header中传递X-Time-Zone时区参数。
\\n// LocalDate的不可变设计\\nLocalDate date = LocalDate.now();\\ndate.plusDays(1); // 返回新实例,原对象不变\\nSystem.out.println(date); // 输出当前日期,不受影响\\n
\\n// Stream API处理时间序列\\nList<Transaction> transactions = \\n list.stream()\\n .filter(t -> t.getTimestamp().isAfter(yesterday)) // 声明式过滤\\n .sorted(Comparator.comparing(Transaction::getTimestamp)) // 自然排序\\n .collect(Collectors.toList()); // 延迟执行\\n
\\n下面总结一下日期处理的各种方案:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n境界 | 代码特征 | 典型问题 | 修复成本 |
---|---|---|---|
初级 | 大量使用String拼接 | 格式混乱/解析异常 | 高 |
进阶 | 熟练运用JDK8新API | 时区处理不当 | 中 |
高手 | 预编译+缓存+防御性编程 | 性能瓶颈 | 低 |
大师 | 结合领域模型设计时间类型 | 业务逻辑漏洞 | 极低 |
终极建议:在微服务架构中,建议建立统一的时间处理中间件,通过AOP拦截所有时间相关操作,彻底消除代码层面的时间处理差异。
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间。\\n\\n比如:2025-04-21、2025/04/21、2025年04月21日等等。\\n\\n有些字段是String类型,有些是Date类型,有些是Long类型。\\n\\n如果不同的数据类型,经常需要相互转换,如果处理不好,可能会出现很多意想不到的问题。\\n\\n这篇文章跟大家一起聊聊日期处理的常见问题,和相关的解决方案,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题…","guid":"https://juejin.cn/post/7495598591488491530","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T02:23:05.760Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"__init__.py 是个啥,为什么深受大厂程序员偏爱?","url":"https://juejin.cn/post/7495624625403084863","content":"👋 朋友们,今天我们来聊聊 Python 里一个低调却至关重要的文件——__init__.py
。
\\n说实话,这玩意儿刚开始学 Python 时,很多人(包括当年的我)都是一脸懵:“这啥?删了会咋样?”
有些人可能听说过它是“包的标志”,也有人觉得它“没啥大用,可以忽略”,更有甚者以为它“只是个装样子的文件”😂。今天,我们就来彻底搞清楚 __init__.py
到底是干啥的,以及它如何影响 Python 项目的结构和运行。
在聊 __init__.py
之前,我们得先弄清楚 Python 里的 模块 和 包 这两个概念。
📌 模块(module):简单来说,就是一个 .py
文件,里面写了一些函数、类或者变量。
\\n比如,有个叫 math_tools.py
的文件,里面有一堆数学工具函数,那它就是个模块。
# math_tools.py\\ndef add(a, b):\\n return a + b\\n\\ndef subtract(a, b):\\n return a - b\\n
\\n然后,我们可以在别的 Python 文件里这样用它:
\\nimport math_tools\\n\\nprint(math_tools.add(3, 5)) # 输出 8\\n
\\n这就是 模块的基本用法,没啥难的,对吧?
\\n如果你写的模块越来越多,代码量越来越大,就得想办法组织它们。这时候,Python 里的 包(package) 就派上用场了。
\\n📌 包(package):一个 文件夹,里面包含多个模块(.py
文件)。
在 Python 3.3 之前,如果要让一个目录被识别为 Python 包,必须在里面创建 __init__.py
文件。但从 Python 3.3 开始,即使没有 __init__.py
,Python 也能识别它是一个包(称为“命名空间包”)。
不过,大部分实际项目 依然建议添加 __init__.py
,因为它可以:
✅ 明确这个文件夹是一个包,避免某些工具(如打包工具)识别错误。
\\n✅ 允许在包初始化时执行特定代码,比如自动导入子模块。
\\n✅ 让导入行为更加可控,避免意外的命名冲突。
比如,咱们有个 math_utils
目录,里面放了几个数学相关的模块:
math_utils/ # 这个文件夹就是一个包\\n│── __init__.py\\n│── basic.py\\n│── advanced.py\\n
\\n其中,basic.py
和 advanced.py
分别是两个模块,而 __init__.py
可以用来 自定义包的导入行为。
__init__.py
到底是干嘛的?虽然 __init__.py
不再是创建包的 必需 条件,但它依然是 Python 项目里一个重要的组件。
它的主要作用有 两个:
\\n如果 __init__.py
存在,Python 解析器就会知道:“这个目录是个 Python 包,而不是普通文件夹。”
即使 Python 3.3+ 之后不强制要求 __init__.py
,但加上它可以:
✅ 避免 Python 解释器在某些情况下误认为这是普通目录。
\\n✅ 兼容旧版本 Python,让代码能在不同环境中运行得更稳定。
\\n✅ 让某些工具(如 pytest
、mypy
)更好地识别项目结构。
如果 __init__.py
里什么都不写,那它的作用只是个“标志”。但如果我们在 __init__.py
里加点代码,它就能 自定义包的导入行为。
🌟 示例 1:让包直接暴露子模块
\\n# math_utils/__init__.py\\nfrom .basic import add, subtract\\nfrom .advanced import power\\n
\\n这样,我们就可以直接 import 整个 math_utils
,而不需要写 .basic
或 .advanced
了:
import math_utils\\n\\nprint(math_utils.add(2, 3)) # 输出 5\\nprint(math_utils.power(2, 3)) # 假设 advanced 里有个 power 函数\\n
\\n等于说,__init__.py
让 包变得像一个大模块 一样,外部不需要知道里面的模块结构,直接用就行。
🌟 示例 2:包初始化操作
\\n__init__.py
还能在包被导入时执行一些初始化操作,比如加载配置、设置日志等:
# math_utils/__init__.py\\nprint(\\"数学工具包加载成功!\\") # 只要 import 这个包,就会执行这行代码\\n
\\n__init__.py
还能干点啥?大厂的 Python 项目里,__init__.py
还经常被用来做这些事:
在大型 Python 项目中,随着模块越来越多,手动维护__init__.py
将变得特别复杂还容易出错,这时候动态导入子模块就成了香饽饽了。
\\n假设我们不知道 math_utils
里具体有哪些模块,可以让 __init__.py
在导入时动态扫描并加载:
# math_utils/__init__.py\\nimport os\\nimport importlib\\n\\n# 获取当前包的路径\\npackage_path = os.path.dirname(__file__)\\n\\n# 遍历当前目录下的所有 .py 文件(不包括 __init__.py 本身)\\nfor module in os.listdir(package_path):\\n if module.endswith(\\".py\\") and module != \\"__init__.py\\":\\n module_name = module[:-3] # 去掉 .py 后缀\\n importlib.import_module(f\\"{__name__}.{module_name}\\") # 动态导入模块\\n
\\n✨ 效果:\\n这样,当你在别的地方写 import mypackage
,所有 mypackage
里的 .py
文件都会自动加载,不用再手动 import
了!🎉
✨没加动态导入要这么写:
\\nimport math_utils.basic\\nprint(math_utils.basic.add(1,2))\\n\\n#如果直接 import math_utils 会报错AttributeError: module \'math_utils\' has no attribute \'basic\'\\n
\\n✨加了动态导入可以这么写:
\\nimport math_utils\\nprint(math_utils.basic.add(1,2))\\n
\\n有时候,我们不想让 所有 子模块都被自动导入,而是只暴露一部分给外部用。这时候可以用 __all__
来 手动控制 允许被 from mypackage import *
访问的模块。
# math_utils/__init__.py\\nimport os\\nimport importlib\\n\\npackage_path = os.path.dirname(__file__)\\n__all__ = []\\n\\nfor module in os.listdir(package_path):\\n if module.endswith(\\".py\\") and module != \\"__init__.py\\":\\n module_name = module[:-3]\\n __all__.append(module_name) # 只暴露在 __all__ 里的模块\\n importlib.import_module(f\\"{__name__}.{module_name}\\")\\n
\\n🌟 效果:
\\nfrom math_utils import *\\n\\nprint(basic) # 只有在 __all__ 里的模块能被导入 \\n
\\n如果某些模块比较大,加载它们会影响性能,那可以用 懒加载(lazy import)技术,在需要时才导入,而不是在 import mypackage
时一次性全加载。
# math_utils/__init__.py\\nimport importlib\\n\\ndef lazy_import(name):\\n return importlib.import_module(f\\"{__name__}.{name}\\")\\n\\nmodule1 = lazy_import(\\"basic\\")\\n
\\n🌟 效果:
\\n这样,basic
只有在第一次被使用时才会真正导入,提高了性能!💡
__init__.py
还能给包加上版本号,让外部代码可以访问:
# math_utils/__init__.py\\n__version__ = \\"1.0.0\\"\\n
\\n然后,在别的地方可以这样用:
\\nimport math_utils\\n\\nprint(math_utils.__version__) # 输出 \\"1.0.0\\"\\n
\\n有些模块是“内部用”的,不想让外部访问,怎么办?可以在 __init__.py
里手动控制 对外暴露的内容:
# math_utils/__init__.py\\nfrom .basic import add, subtract\\n\\n__all__ = [\\"add\\", \\"subtract\\"] # advanced.py 里的东西就不会被直接 import\\n
\\n这样,外部只能用 math_utils.add()
,但 math_utils.advanced
就不让直接访问了。
关于 __init__.py
,咱们就聊到这儿!希望这篇文章能帮你彻底搞懂它的作用,今后写 Python 项目时能更自信地使用它。
如果你觉得这篇文章有帮助,别忘了——顺手点赞 + 在看,就是对花姐最大的支持! ❤️🔥
","description":"👋 朋友们,今天我们来聊聊 Python 里一个低调却至关重要的文件——__init__.py。 说实话,这玩意儿刚开始学 Python 时,很多人(包括当年的我)都是一脸懵:“这啥?删了会咋样?”\\n\\n有些人可能听说过它是“包的标志”,也有人觉得它“没啥大用,可以忽略”,更有甚者以为它“只是个装样子的文件”😂。今天,我们就来彻底搞清楚 __init__.py 到底是干啥的,以及它如何影响 Python 项目的结构和运行。\\n\\n🏗️ 先搞懂 Python 模块(module)\\n\\n在聊 __init__.py 之前,我们得先弄清楚 Python 里的 模块…","guid":"https://juejin.cn/post/7495624625403084863","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T01:42:01.440Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbdbedac916d45898817d6d62eec7cc7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1745890920&x-signature=M%2BRBG6FosmU0bjTsCmNNYfXcLlk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"个人开发者如何发送短信?这个方案太香了!","url":"https://juejin.cn/post/7495570300124119052","content":"\\n\\n还在为无法发送短信验证码而烦恼?今天分享一个超实用的解决方案,个人开发者也能用!
\\n
最近国内很多平台暂停了针对个人用户的短信发送,这给个人开发者带来了不少困扰。不过别担心,我发现了一个超实用的解决方案——Spug推送平台,它能很好地满足我们发送短信等需求。
\\n打开push.spug.cc,使用微信扫码直接登录,无需繁琐的认证流程。
\\n复制模版ID,通过API调用即可发送短信。
\\nimport requests\\n\\ndef send_sms(template_id, code, phone):\\n url = f\\"https://push.spug.cc/send/{template_id}\\"\\n params = {\\n \\"code\\": code,\\n \\"targets\\": phone\\n }\\n response = requests.get(url, params=params)\\n return response.json()\\n\\n# 使用示例\\nresult = send_sms(\\"abc\\", \\"6677\\", \\"151xxxx0875\\")\\nprint(result)\\n
\\npackage main\\n\\nimport (\\n \\"fmt\\"\\n \\"net/http\\"\\n \\"io/ioutil\\"\\n)\\n\\nfunc sendSMS(templateID, code, phone string) (string, error) {\\n url := fmt.Sprintf(\\"https://push.spug.cc/send/%s?code=%s&targets=%s\\", \\n templateID, code, phone)\\n \\n resp, err := http.Get(url)\\n if err != nil {\\n return \\"\\", err\\n }\\n defer resp.Body.Close()\\n \\n body, err := ioutil.ReadAll(resp.Body)\\n if err != nil {\\n return \\"\\", err\\n }\\n \\n return string(body), nil\\n}\\n\\nfunc main() {\\n result, err := sendSMS(\\"abc\\", \\"6677\\", \\"151xxxx0875\\")\\n if err != nil {\\n fmt.Println(\\"Error:\\", err)\\n return\\n }\\n fmt.Println(result)\\n}\\n
\\nimport java.net.HttpURLConnection;\\nimport java.net.URL;\\nimport java.io.BufferedReader;\\nimport java.io.InputStreamReader;\\n\\npublic class SMSSender {\\n public static String sendSMS(String templateId, String code, String phone) throws Exception {\\n String url = String.format(\\"https://push.spug.cc/send/%s?code=%s&targets=%s\\",\\n templateId, code, phone);\\n \\n URL obj = new URL(url);\\n HttpURLConnection con = (HttpURLConnection) obj.openConnection();\\n con.setRequestMethod(\\"GET\\");\\n \\n BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));\\n String inputLine;\\n StringBuilder response = new StringBuilder();\\n \\n while ((inputLine = in.readLine()) != null) {\\n response.append(inputLine);\\n }\\n in.close();\\n \\n return response.toString();\\n }\\n\\n public static void main(String[] args) {\\n try {\\n String result = sendSMS(\\"abc\\", \\"6677\\", \\"151xxxx0875\\");\\n System.out.println(result);\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n参数说明
\\ncode
:验证码内容targets
:接收短信的手机号targets
参数会覆盖模板配置的手机号最佳实践
\\n每日订单量1000万级,数据规模呈指数级增长:
\\n核心挑战聚焦三大维度:
\\n通过分库分表
\\n如何拆分?买家和卖家是否需要分开存储?
\\n分开存储扩展已经查询性能都会好一些。
\\n分库策略,通过用户ID 或者订单创建时间 取模 分库数量
\\n分表策略:每个库内按月份分表
\\n对于很多数据在近期都不会被查询,所以可以将数据做一个拆分,冷数据 and 热数据。
\\n30 天内的数据 -> MySQL
\\n超过30天的数据 -> TiDB or ClickHouse ...
\\n举个例子,比如查询卖家过去30天内金额>100元且为己完成状态的某类订单?
\\n通过ES是不是更加高效?
\\n// 多条件组合查询DSL优化\\n{\\n \\"query\\": {\\n \\"bool\\": {\\n \\"filter\\": [\\n {\\"term\\": {\\"seller_id\\": 98765}},\\n {\\"range\\": {\\"create_time\\": {\\"gte\\": \\"now-30d/d\\"}}},\\n {\\"script\\": {\\"script\\": \\"doc[\'order_amount\'].value >= 100\\"}}\\n ],\\n \\"must\\": [\\n {\\"term\\": {\\"order_status\\": \\"completed\\"}},\\n {\\"match\\": {\\"order_type\\": \\"takeout\\"}}\\n ]\\n }\\n },\\n \\"preference\\": \\"primary_first\\" // 优先主分片提升稳定性\\n}\\n\\n
\\n对于热点数据,查询 ES 或者 MySQL 无疑是带来很大压力以及性能也会比较差。
\\n举个例子,买家查询最近10条订单数?卖家最近1天内所有订单列表;
\\n对于这种热点数据,我们可以缓存到 Redis 中,对于热点数据存储到Redis就好,查询中走Redis查询性能也会提升大一些。
\\n# Key: 用户维度 + 业务标识\\nbuyer:orders:{user_id}:recent # 例如 buyer:orders:12345:recent\\n\\n# Value: 用 Redis List 存储订单ID(按时间倒序)\\n[\\n \\"order_id_100\\", # 最新订单\\n \\"order_id_99\\",\\n ...\\n \\"order_id_91\\" # 第10条订单\\n]\\n\\n# 或用 Hash 存储订单详情(如果需缓存完整数据)\\n{\\n \\"order_id_100\\": \\"{订单JSON}\\",\\n \\"order_id_99\\": \\"{订单JSON}\\",\\n ...\\n}\\n\\n
\\n# Key: 卖家ID + 时间范围\\nseller:orders:{seller_id}:last_24h # 例如 seller:orders:98765:last_24h\\n\\n# Value: 用 Sorted Set (ZSET) 存储订单ID和创建时间戳\\n# Score = 订单时间戳, Member = 订单ID\\n[\\n [1640995200, \\"order_id_100\\"],\\n [1640995201, \\"order_id_101\\"],\\n ...\\n]\\n\\n# 或用 Hash 存储订单详情(按需)\\n{\\n \\"order_id_100\\": \\"{订单JSON}\\",\\n \\"order_id_101\\": \\"{订单JSON}\\",\\n ...\\n}\\n\\n
\\n对于存储压力比较大,可以采用分库分表优化,采用合理的分库分表手段以及分片键的选取。对于久远数据,为了避免干扰目前的查询,可以采用冷热数据分离。
\\n为了解决复杂的查询条件,可以采用ES查询优化。
\\n对于热点数据,可以采用将近期内的订单数据缓存到 Redis 中,利用 Redis 性能高的特性,以及引入本地缓存构建多级缓存体系进行优化。
","description":"1. 业务场景与挑战 每日订单量1000万级,数据规模呈指数级增长:\\n\\n年度数据量达36亿条\\n单表容量突破2000万性能临界点\\n高峰期并发请求量超过5万QPS\\n\\n核心挑战聚焦三大维度:\\n\\n存储成本:海量数据物理存储成本激增\\n查询效率:多维度组合查询响应超时\\n并发压力:实时订单状态查询流量洪峰\\n2. 存储压力如何解决\\n如何保证大数据量存储\\n\\n通过分库分表\\n\\n如何拆分?买家和卖家是否需要分开存储?\\n\\n分开存储扩展已经查询性能都会好一些。\\n\\n如何分库?\\n\\n分库策略,通过用户ID 或者订单创建时间 取模 分库数量\\n\\n分表策略:每个库内按月份分表\\n\\n数据都要存储到数…","guid":"https://juejin.cn/post/7495598574710210570","author":"爱吃饭敢当","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T15:37:34.873Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"10种常见的架构风格,你用过几种?","url":"https://juejin.cn/post/7495591732915453987","content":"嗨,你好呀,我是猿java
\\n软件架构风格是描述软件系统高层次组织和结构的模式,它定义了组件之间的交互方式、通信协议以及系统的整体设计原则。不同的架构风格适用于不同的应用场景,影响系统的可维护性、可扩展性、性能和可靠性。这篇文章,我们来分析 10种常见的软件架构风格及其特点。
\\n分层架构(Layered Architecture)的核心思想是将系统垂直划分为多个层级,每层提供特定功能,且仅能调用下一层的服务(严格分层)或相邻层(松散分层)。其特点是将系统划分为若干层(如表现层、业务逻辑层、数据访问层),每层仅依赖下一层。
\\n常见的典型分层:表现层(UI)→ 业务逻辑层(BLL)→ 数据访问层(DAL)→ 数据库。
\\n┌─────────────────┐\\n│ 表现层 │ (UI/API)\\n└────────┬────────┘\\n ↓\\n┌─────────────────┐\\n│ 业务逻辑层 │ (Service)\\n└────────┬────────┘\\n ↓\\n┌─────────────────┐\\n│ 数据访问层 │ (DAO/Repository)\\n└────────┬────────┘\\n ↓\\n┌─────────────────┐\\n│ 数据库 │\\n└─────────────────┘\\n
\\n箭头:严格分层仅允许上层调用下层(禁止跨层或逆向调用)。
\\n客户端-服务器架构(Client-Server)的核心思想:功能分离为两个角色:
\\n其特点是客户端请求服务,服务器提供服务,两者通过网络通信。
\\n┌─────────────┐ HTTP/GRPC ┌─────────────┐\\n│ Client │ ────────────────────> │ Server │\\n│(Browser/App)│ <──────────────────── │ (Web/DB) │\\n└─────────────┘ Response └─────────────┘\\n
\\n双向箭头:客户端发起请求,服务器返回响应。
\\n微服务架构(Microservices)的核心思想:将单体应用拆分为多个小型服务,每个服务:
\\n微服务架构的特点是将系统拆分为多个小型、独立的服务,每个服务负责特定功能,通过API通信。
\\n┌─────────────┐ API Gateway ┌─────────────┐\\n│ Client │ ──────────────────────> │ Service A │\\n└─────────────┘ └─────────────┘\\n │ ▲\\n │ Service Discovery │\\n └───────────────────────────┘\\n (Consul/Eureka/Nacos)\\n
\\n关键组件:API网关统一入口,服务注册中心管理动态服务地址。
\\n事件驱动架构(Event-Driven Architecture, EDA)的核心思想:组件通过事件异步通信,典型模式:
\\n事件驱动架构的特点是组件通过发布/订阅事件异步通信,解耦生产者和消费者。
\\n┌─────────────┐ Publish ┌─────────────┐\\n│ Producer │ ───────────────────> │ Event Bus │\\n└─────────────┘ (OrderCreated) └─────────────┘\\n ↑\\n │ Subscribe\\n │\\n ┌─────────────┐\\n │ Consumer │\\n │ (Inventory) │\\n └─────────────┘\\n
\\n事件流:生产者发布事件到消息队列(如Kafka),消费者订阅感兴趣的事件。
\\n管道-过滤器架构(Pipe-Filter)的核心思想:数据流经一系列过滤器(处理单元),每个过滤器:
\\n管道-过滤器架构的特点是数据通过一系列过滤器(处理单元)流动,每个过滤器对数据做特定处理。
\\n┌─────────┐ ┌─────────┐ ┌─────────┐\\n│ Data │ ──> │ Filter │ ──> │ Filter │ ──> Output\\n│ Source │ │ (Parse) │ │ (Enrich)│\\n└─────────┘ └─────────┘ └─────────┘\\n
\\n线性管道:数据流经多个过滤器,每个过滤器完成特定转换。
\\n面向服务架构(SOA)的核心思想是将业务功能抽象为可复用服务,通过企业服务总线(ESB)集成:
\\n它的特点是将功能封装为可重用的服务,通过标准协议(如SOAP、REST)通信。
\\n┌─────────────┐ SOAP/REST ┌─────────────┐\\n│ Consumer │ ────────────────────> │ Service │\\n└─────────────┘ └─────────────┘\\n │ ▲\\n │ ESB │\\n └───────────────────────────┘\\n (Enterprise Service Bus)\\n
\\nESB核心作用:路由、协议转换、消息增强。
\\n单体架构(Monolithic)的核心思想是:所有功能模块(UI、业务逻辑、数据库访问)打包为单一可执行文件。 它的特点是将所有功能集中在一个代码库中,统一部署。
\\n┌───────────────────────────────────┐\\n│ Monolith │\\n│ ┌─────────┐ ┌─────────┐ ┌───────┐ │\\n│ │ Module A │ │ Module B │ │ DB │ │\\n│ └─────────┘ └─────────┘ └───────┘ │\\n└───────────────────────────────────┘\\n
\\n单一进程:所有模块共享同一内存空间和数据库连接。
\\n无服务器架构(Serverless)的核心思想是:开发者只编写函数(如AWS Lambda),云平台负责:
\\n它的特点是开发者专注于函数(Function)开发,云平台管理资源调度。
\\n┌─────────────┐ Event ┌─────────────┐\\n│ Trigger │ ─────────────────> │ Function │\\n│ (HTTP/S3) │ <───────────────── │ (Lambda) │\\n└─────────────┘ Response └─────────────┘\\n
\\n事件触发:云平台自动管理函数实例的创建和销毁。
\\n空间架构的核心思想:通过分布式共享内存(如元组空间)实现数据共享,避免集中式数据库。
\\n它的特点是通过共享内存(如元组空间)实现分布式组件通信,避免集中式数据库。
\\n┌─────────────┐ Read/Write ┌─────────────┐\\n│ Node 1 │ ────────────────────> │ Tuple │\\n└─────────────┘ │ Space │\\n┌─────────────┐ Data Grid └─────────────┘\\n│ Node 2 │ ────────────────────> (Shared Memory)\\n└─────────────┘\\n
\\n共享空间:所有节点通过分布式内存(如Redis集群)交换数据。
\\n点对点架构(Peer-to-Peer, P2P)的核心思想: 节点(Peer)既消费又提供服务,无中心服务器。
\\n它的特点是节点平等,既消费又提供服务(如文件共享)。
\\n ┌─────────────┐\\n │ Peer A │\\n └──────┬──────┘\\n │ Query File\\n ┌──────▼──────┐\\n │ Peer B │\\n └──────┬──────┘\\n │ Forward\\n ┌──────▼──────┐\\n │ Peer C │\\n └─────────────┘\\n
\\n去中心化网络:节点间直接通信,无中心协调者。
\\n现代系统常混合多种风格(如微服务+事件驱动),并结合云原生技术(容器化、Kubernetes)。架构风格的选择需权衡业务需求与技术约束,没有“银弹”。
\\n本文,我们分析了 11种常见的软件架构风格,并且花了它们简要的模型图。每种架构风格都有它的特点以及对应的使用场景,不过在现实工作中,为了业务需求,我们通常会多种架构风格混合使用。所以,掌握这些架构风格还是很有必要的。
\\n最后,把猿哥的座右铭送给你:投资自己才是最大的财富。 如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
","description":"嗨,你好呀,我是猿java 软件架构风格是描述软件系统高层次组织和结构的模式,它定义了组件之间的交互方式、通信协议以及系统的整体设计原则。不同的架构风格适用于不同的应用场景,影响系统的可维护性、可扩展性、性能和可靠性。这篇文章,我们来分析 10种常见的软件架构风格及其特点。\\n\\n1. 软件架构风格\\n1.1 分层架构\\n\\n分层架构(Layered Architecture)的核心思想是将系统垂直划分为多个层级,每层提供特定功能,且仅能调用下一层的服务(严格分层)或相邻层(松散分层)。其特点是将系统划分为若干层(如表现层、业务逻辑层、数据访问层),每层仅依赖下一层。…","guid":"https://juejin.cn/post/7495591732915453987","author":"猿java","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T10:46:43.396Z","media":null,"categories":["后端","架构","Java","分布式"],"attachments":null,"extra":null,"language":null},{"title":"Java 8 到 Java 17 兼容性分析与迁移指南","url":"https://juejin.cn/post/7495005738764730431","content":"本文档提供了将项目从 Java 8 升级到 Java 17 的详细分析和迁移步骤,包括代码修改建议、依赖更新和配置调整。","description":"本文档提供了将项目从 Java 8 升级到 Java 17 的详细分析和迁移步骤,包括代码修改建议、依赖更新和配置调整。","guid":"https://juejin.cn/post/7495005738764730431","author":"Cosolar","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T07:02:38.618Z","media":null,"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"如何使用Trae AI 创建你的智能流程图?【干货】","url":"https://juejin.cn/post/7495070285067370536","content":"上周一,我们团队开大双周会,讲了团队的一个质量架构体系、然后讲了后续Q2季度的发展,什么GMV、各类指标
\\n我看到了琳琅满目的架构图,眼花缭乱,\\n思绪开始慢慢开始飘荡到远方,我坐着,思绪开始静静的追忆着过往的岁月,
\\n在很多年以前,我想起我看到的团队的leader也在画这个图,那是很久很久以前了,有些记不清了,大概是18年的年底,记得那天是冬天,天很冷,所以记忆此刻也仍然犹新,当时老大也在给团队的CTO做汇报写了很长的周报,我从旁路过,看到它在一个一个的画架构图,省略后的大概长这样
\\n当时的我在想,哇~ 这就是大佬嘛? 看着好高级,希望有一天我也能向老板一样,我一般称呼我们的团队的leader为老板,时至今日,我自己也成为leader了,才慢慢的对这些图有了自己的理解,慢慢也开始着手区去画,去和别让分享,也就是开始了今天的这篇文章
\\n首先我们打开
\\n海外版:www.trae.ai/
\\nor
\\n国内版:www.trae.com.cn/
\\n我们点击左侧进行插件部分,搜索:drawio
\\n\\n\\n为什么要装这个东西,一句话概括就是** drawio是一款清晰的流程图软件**,\\n我们以插件的形式进行安装,方便后续进行生成的时候预览,下方是详细介绍:
\\n
draw.io 是一款功能强大的开源图表绘制工具,它可以帮助你轻松创建各种类型的图表,包括:
\\n流程图: 业务流程、系统流程、数据流程等
\\n思维导图: 整理思路、头脑风暴、项目规划等
\\n网络拓扑图: 网络结构、服务器架构、系统部署等
\\nUML 图: 软件设计、类图、时序图等
\\n实体关系图: 数据库设计、数据模型等
\\n以及其他各种类型的图表: 甘特图、线框图、组织结构图等等
\\n接下来我们开始在Trae 里面使用\\n记住装这个插件\\n不要整错了
\\n在2025年的今天,AI技术大火,有很多各式各样的网站,已经开始支持AI生成架构图了,例如我们可以使用最简单的方式,我们先告诉AI,我们需要的格式:
\\n请为我绘制一个现代化的Springboot+Vue博客系统架构流程图,使用draw.io格式。\\n\\n技术栈要求:\\n- 前端:Vue3 + Vuex + Vue Router + Element Plus/Ant Design Vue\\n- 后端:Spring Boot + Spring Security + MyBatis-Plus\\n- 数据存储:MySQL主从复制 + Redis缓存\\n- 搜索引擎:Elasticsearch\\n- 消息队列:RabbitMQ\\n- 文件存储:MinIO\\n\\n架构图需包含以下模块及其交互流程:\\n1. 用户层:PC端、移动端访问\\n2. 前端架构:组件结构、状态管理、路由系统\\n3. 网关层:Nginx反向代理、负载均衡\\n4. 后端微服务:用户服务、内容服务、评论服务等\\n5. 数据访问层:ORM框架、缓存策略、读写分离\\n6. 存储层:主从复制、数据备份\\n7. 中间件:消息队列、搜索引擎\\n8. 运维层:Docker容器化、CI/CD流程\\n\\n请使用现代简约设计风格,配色方案采用蓝色系专业配色,各组件间用箭头清晰标明数据流向,并标注核心技术点。\\n
\\n我们来看下Trae AI 生成的效果
\\n上方都是Trae AI生成的,下面我们来试下换成医疗的Cursor生成的效果:
\\n当然这里其实存在一个差距,例如Cursor 使用的claude 模型,而trae 使用的是deepseek,模型之间也存在差异,但是事实证明,其实这张画架构图的形式,已经可以完全使用大模型进行替代生成
\\n接下来,我们来试试使用HTML+CSS 的网页架构图看下是否正常
\\n哈哈,看来HTML的形式似乎还是不够完美,存在许多的缺陷,当然也跟提示词有关
\\n请为我绘制一个现代化的Springboot+Vue博客系统架构流程图,使用HTML和TailwindCSS构建 \\n \\n技术栈要求: \\n- 前端:Vue3 + Vuex + Vue Router + Element Plus/Ant Design Vue \\n- 后端:Spring Boot + Spring Security + MyBatis-Plus \\n- 数据存储:MySQL主从复制 + Redis缓存 \\n- 搜索引擎:Elasticsearch \\n- 消息队列:RabbitMQ \\n- 文件存储:MinIO \\n \\n架构图需包含以下模块及其交互流程: \\n1. 用户层:PC端、移动端访问 \\n2. 前端架构:组件结构、状态管理、路由系统 \\n3. 网关层:Nginx反向代理、负载均衡 \\n4. 后端微服务:用户服务、内容服务、评论服务等 \\n5. 数据访问层:ORM框架、缓存策略、读写分离 \\n6. 存储层:主从复制、数据备份 \\n7. 中间件:消息队列、搜索引擎 \\n8. 运维层:Docker容器化、CI/CD流程 \\n \\n请使用现代简约设计风格,配色方案采用蓝色系专业配色,各组件间用箭头清晰标明数据流向,并标注核心技术点。\\n
\\n下面我们修改下提示词,让他参考图片进行二次修改,我们会发现,加上图片的理解,\\n网页端一样也能完成优化下架构图
\\n下方是架构图的代码:搭建可以自行预览
\\n\\n
在这个章节,接下来我们使用vercel 进行线上部署,让你的组员、或者领导,公司的同时都能访问到这个架构图
\\n首先第一步:
\\n我们需要将1. 架构图上传github
\\n\\n\\n
进入vercel官网 vercel.com/
\\nVercel 是一个面向前端开发者的云平台,支持快速部署静态网站(如 Gatsby、Hugo 生成的项目)和动态应用(如 Next.js 服务端渲染应用)。其核心优势包括:
\\n这里我们选择进行创建新项目
\\n找到我们在进入vercel官网上,先进行关联的github的,最后在找到我们使用Trae AI 画出的流程图这个地方,点击Import
\\nindex.html
文件部署完成!!!💥
\\n线上地址: jiagoutu.vercel.app/ ,第一次访问可能会比较慢,\\n但是后续会快很多
\\n这里进行选择域名管理
\\n这里我们选择添加我们自己的域名
\\n选择添加域名
\\n这里记得配置DNS 解析,部署成功!!!
\\n\\n\\n\\n其实本来,这篇文章已经完结了,但是晚上下班刚到家,给小孩哄睡玩,甚似高兴\\n看到这个网名名为:_等闲的小兄弟说:
\\n
这个可以让AI分析项目,然后根据项目画嘛?\\n
我只能说:有的有的兄弟,这必须有!
\\n下载项目
\\n这里我们找到经典的Java项目给这位兄弟进行演示画架构图 gitee.com/y_project/R…
\\n首先我们需要进行下载,我们就以这位掘友的名字进行命名,将项目clone 到这里
\\ngit clone https://gitee.com/y_project/RuoYi.git\\n
\\n复制这个地址
\\n输入提示词之前我们需要让AI了解一下我的项目
\\n你找对象不是也需要一定的时间相处才能了解释? 一样的道理,所以让AI熟悉一下,我们连着Cursor 和 Trae AI一起用,我们先用Cursor,用Cursor之前首先需要将项目和文档关联起来,总的来说就是你的知识库
\\n点击设置、 找到 Features\\n
输入若依的文档地址
\\n点击这个小书的图标会自动出现章节就没啥问题了
\\n@RuoYi \\n请你熟悉一下项目,并列出具体的功能,要具体点,这是我的好兄弟等闲需要的,我需要用来画架构图的,你需要足够清晰,嘎嘎清晰,不清晰把你网线拔了!@需求理解.md\\n
\\n接下来,我们把文件夹拖进来然后在输入提示词,同时友好的催下一下AI
\\n最后生成架构图,输入我们的终极提示词
\\n记住这里需要注意的是: 一定要将文档和需求理解,一起拖拽进行让AI进行实现
\\n枚举类是在 Java 5(也称为 Java 1.5)中引入的。此版本的引入使得枚举类型不仅可以简单地定义常量集合,还支持属性、方法和构造函数,从而增强了语言的表达能力。自从引入枚举后,在 Java 开发中得到了广泛应用,尤其是在状态管理、策略模式、命令模式等场景中,提升了代码的可读性和可维护性。在Java编程中,枚举(enum)是一种非常强大的特性。它不仅提供了对一组常量的良好封装,还允许我们为这些常量定义方法和属性,从而提升代码的可读性和可维护性。在这篇文章中,我们将深入探讨如何优雅地编写枚举类,涵盖枚举的基本用法、设计模式、常用方法以及最佳实践。
\\n在Java中,定义枚举使用enum
关键字。一个简单的枚举示例如下:这里,我们定义了一个表示一周七天的枚举类。每个常量都是Day类型的一个实例。
public enum Day {\\n MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;\\n}\\n
\\n=
使用枚举类非常简单。我们可以通过直接引用枚举常量来使用:
\\nDay today = Day.MONDAY;\\nSystem.out.println(\\"今天是: \\" + today);\\n
\\n可以使用values()
方法遍历枚举中的所有常量:
for (Day day : Day.values()) {\\n System.out.println(day);\\n}\\n
\\n作为一名优秀的编程工作者,当我们遇到需要展示中文的时候应该怎么写枚举类呢?枚举不仅仅是常量的集合,我们可以为枚举常量添加属性和方法,以增强其功能。
\\n我们可以为枚举常量定义字段,并通过构造函数初始化这些字段:每个Day枚举常量都有一个描述属性。通过getDescription()方法,我们可以获取每个常量的描述信息。
\\npublic enum Day {\\n MONDAY(\\"工作日\\"), \\n TUESDAY(\\"工作日\\"), \\n WEDNESDAY(\\"工作日\\"), \\n THURSDAY(\\"工作日\\"), \\n FRIDAY(\\"工作日\\"), \\n SATURDAY(\\"周末\\"), \\n SUNDAY(\\"周末\\");\\n\\n private String description;\\n\\n Day(String description) {\\n this.description = description;\\n }\\n\\n public String getDescription() {\\n return description;\\n }\\n}\\n
\\nisWorkDay()
方法:该方法可以方便地判断某一天是否为工作日,增强了可用性。fromString()
方法:提供了一个静态方法来通过字符串获取对应的枚举常量,增加了灵活性。public boolean isWorkDay() {\\nreturn this != SATURDAY && this != SUNDAY;\\n}\\n\\npublic static Day fromString(String name) {\\nreturn Day.valueOf(name.toUpperCase());\\n}\\n
\\n枚举可以实现特定的接口,以便在不同的枚举常量中定义不同的行为。例如,我们可以定义一个支付方式的枚举:这里,PaymentMethod枚举定义了一个抽象方法pay(),每个常量实现自己的支付逻辑。
\\npublic enum PaymentMethod {\\n CREDIT_CARD {\\n @Override\\n public void pay(double amount) {\\n System.out.println(\\"用信用卡支付: \\" + amount);\\n }\\n },\\n PAYPAL {\\n @Override\\n public void pay(double amount) {\\n System.out.println(\\"用PayPal支付: \\" + amount);\\n }\\n };\\n\\n public abstract void pay(double amount);\\n}\\n
\\n我们还可以使用枚举作为工厂来创建对象。例如,定义一个形状工厂:每个常量负责创建自己对应的形状实例。
\\npublic enum ShapeType {\\n CIRCLE {\\n @Override\\n public Shape createShape() {\\n return new Circle();\\n }\\n },\\n SQUARE {\\n @Override\\n public Shape createShape() {\\n return new Square();\\n }\\n };\\n\\n public abstract Shape createShape();\\n}\\n
\\n枚举可以作为事件的发布者,允许多个观察者监听状态变化。例如:Event枚举允许注册观察者,并在事件发生时通知它们。
\\npublic enum Event {\\n START,\\n STOP;\\n\\n private List<Observer> observers = new ArrayList<>();\\n\\n public void addObserver(Observer observer) {\\n observers.add(observer);\\n }\\n\\n public void notifyObservers() {\\n for (Observer observer : observers) {\\n observer.update(this);\\n }\\n }\\n}\\n
\\n枚举可以用来表示对象的不同状态,并根据状态执行不同的行为。例如,交通灯的状态:通过重写action()方法,不同的状态实现了各自的行为。
\\npublic enum TrafficLight {\\n RED {\\n @Override\\n public void action() {\\n System.out.println(\\"停止\\");\\n }\\n },\\n GREEN {\\n @Override\\n public void action() {\\n System.out.println(\\"通行\\");\\n }\\n },\\n YELLOW {\\n @Override\\n public void action() {\\n System.out.println(\\"警告\\");\\n }\\n };\\n\\n public abstract void action();\\n}\\n
\\n枚举本身可以被用作单例模式的实现。Java的枚举特性天然支持单例:
\\npublic enum Singleton {\\n INSTANCE;\\n\\n public void doSomething() {\\n System.out.println(\\"执行某个操作\\");\\n }\\n}\\n
\\n可以为枚举添加一些基于条件的方法,以增强其功能。例如,判断某天是否为工作日:
\\npublic boolean isWorkDay() {\\n return this != SATURDAY && this != SUNDAY;\\n}\\n
\\n可以提供一些静态方法来操作枚举,例如查找特定枚举常量:
\\npublic static Day fromString(String name) {\\n return Enum.valueOf(Day.class, name.toUpperCase());\\n}\\n
\\n通过values()
方法,遍历枚举中的所有常量非常方便:
for (Day day : Day.values()) {\\n System.out.println(day);\\n}\\n
\\n在编写枚举类时,有一些最佳实践可以帮助你更有效地利用这一特性,提升代码质量和可维护性。以下是一些推荐的实践:
\\n如果常量集合具有明显的逻辑关系,使用枚举而不是静态常量可以提高代码的可读性和安全性。枚举提供了类型安全的优势,避免了常量间的潜在混淆。
\\npublic enum Color {\\n RED, GREEN, BLUE;\\n}\\n\\n// 使用\\nColor favoriteColor = Color.RED;\\n\\n
\\n如果常量有不同的行为,可以通过抽象方法实现多态。例如,在支付方式的枚举中,每种支付方式可以实现不同的支付逻辑。这种方式不仅增强了可读性,也便于未来的扩展。
\\npublic enum PaymentMethod {\\n CREDIT_CARD {\\n @Override\\n public void pay(double amount) {\\n System.out.println(\\"用信用卡支付: \\" + amount);\\n }\\n },\\n PAYPAL {\\n @Override\\n public void pay(double amount) {\\n System.out.println(\\"用PayPal支付: \\" + amount);\\n }\\n };\\n\\n public abstract void pay(double amount);\\n}\\n\\n
\\n枚举常量是静态的,设计时应尽量避免依赖外部状态。保持枚举的独立性可以防止意外的状态变化,从而提高代码的稳定性。
\\npublic enum Status {\\n PENDING, COMPLETED, FAILED;\\n\\n public String getMessage() {\\n switch (this) {\\n case PENDING:\\n return \\"任务正在进行中\\";\\n case COMPLETED:\\n return \\"任务已完成\\";\\n case FAILED:\\n return \\"任务失败\\";\\n default:\\n return \\"未知状态\\";\\n }\\n }\\n}\\n\\n
\\n如果多个枚举常量需要遵循相同的协议,可以让枚举实现接口。这样可以确保每个常量都遵循相同的规范,提高了代码的一致性和可维护性。
\\npublic interface Describable {\\n String getDescription();\\n}\\n\\npublic enum Animal implements Describable {\\n DOG {\\n @Override\\n public String getDescription() {\\n return \\"狗,忠诚的动物\\";\\n }\\n },\\n CAT {\\n @Override\\n public String getDescription() {\\n return \\"猫,优雅的动物\\";\\n }\\n };\\n}\\n\\n
\\n保持枚举类的简洁性,避免添加过多复杂的逻辑。每个枚举的职责应单一且明确,复杂的逻辑应尽量放在其他类中处理,以增强代码的可读性。
\\npublic enum Direction {\\n NORTH, SOUTH, EAST, WEST;\\n\\n public Direction turnLeft() {\\n switch (this) {\\n case NORTH: return WEST;\\n case WEST: return SOUTH;\\n case SOUTH: return EAST;\\n case EAST: return NORTH;\\n default: throw new AssertionError(\\"Unknown direction: \\" + this);\\n }\\n }\\n}\\n\\n
\\nvalues()
方法遍历枚举常量时,使用 values()
方法可以避免手动维护常量列表。这一方法不仅简化了代码,还能确保不会遗漏任何常量。
public enum Day {\\n MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;\\n}\\n\\n// 遍历枚举\\nfor (Day day : Day.values()) {\\n System.out.println(day);\\n}\\n\\n
\\nDay
枚举类通过遵循这些最佳实践,可以有效提升枚举类的设计和实现质量,从而使代码更加清晰、可维护,易于扩展。
\\n/**\\n * 表示一周的七天,并提供相关功能。\\n */\\npublic enum Day {\\n MONDAY(\\"工作日\\"),\\n TUESDAY(\\"工作日\\"),\\n WEDNESDAY(\\"工作日\\"),\\n THURSDAY(\\"工作日\\"),\\n FRIDAY(\\"工作日\\"),\\n SATURDAY(\\"周末\\"),\\n SUNDAY(\\"周末\\");\\n\\n private String description;\\n\\n Day(String description) {\\n this.description = description;\\n }\\n\\n /**\\n * 获取描述信息。\\n *\\n * @return 描述信息\\n */\\n public String getDescription() {\\n return description;\\n }\\n\\n /**\\n * 判断当前天是否为工作日。\\n *\\n * @return 如果是工作日返回 true,否则返回 false\\n */\\n public boolean isWorkDay() {\\n return this != SATURDAY && this != SUNDAY;\\n }\\n\\n /**\\n * 根据字符串获取对应的 Day 枚举常量。\\n *\\n * @param name 天的名称\\n * @return 对应的 Day 枚举常量\\n * @throws IllegalArgumentException 如果没有对应的枚举常量\\n */\\n public static Day fromString(String name) {\\n return Day.valueOf(name.toUpperCase());\\n }\\n\\n /**\\n * 根据当前天获取下一个工作日。\\n *\\n * @return 下一个工作日\\n */\\n public Day nextWorkDay() {\\n switch (this) {\\n case FRIDAY: return MONDAY;\\n case SATURDAY: return MONDAY;\\n case SUNDAY: return MONDAY;\\n default: return Day.values()[(this.ordinal() + 1) % 5];\\n }\\n }\\n\\n /**\\n * 获取当前天的类型(工作日或周末)。\\n *\\n * @return 类型信息\\n */\\n public String getType() {\\n return isWorkDay() ? \\"工作日\\" : \\"周末\\";\\n }\\n}\\n\\n
\\n在本篇文章中,我们探讨了如何优雅地编写枚举类,包括基本用法、常用设计模式、常用方法以及最佳实践。希望这些内容能够帮助你在Java编程中更好地使用枚举特性,提高你的代码质量。
","description":"枚举类是在 Java 5(也称为 Java 1.5)中引入的。此版本的引入使得枚举类型不仅可以简单地定义常量集合,还支持属性、方法和构造函数,从而增强了语言的表达能力。自从引入枚举后,在 Java 开发中得到了广泛应用,尤其是在状态管理、策略模式、命令模式等场景中,提升了代码的可读性和可维护性。在Java编程中,枚举(enum)是一种非常强大的特性。它不仅提供了对一组常量的良好封装,还允许我们为这些常量定义方法和属性,从而提升代码的可读性和可维护性。在这篇文章中,我们将深入探讨如何优雅地编写枚举类,涵盖枚举的基本用法、设计模式、常用方法以及最佳实践。…","guid":"https://juejin.cn/post/7495176903020675082","author":"不惑_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T01:31:18.202Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c50285a37a704e04b58090df45645cc5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiN5oORXw==:q75.awebp?rk3s=f64ab15b&x-expires=1746064352&x-signature=JtSkVl4%2F8GCLXcd04Nm3eajYDZY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0f74f6c89c6a41919d77c9179042101d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiN5oORXw==:q75.awebp?rk3s=f64ab15b&x-expires=1746064352&x-signature=cXk9mwNEaazq%2BP5f4DDA9%2BebDIQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","架构"],"attachments":null,"extra":null,"language":null},{"title":"🚀Python神器NiceGUI:手把手带你从0到精通,写GUI界面从未如此简单!","url":"https://juejin.cn/post/7495020421218435082","content":"\\n\\n前言:昨天晚上加班回家,刚躺下就想着,Python做GUI总得折腾PyQt、Tkinter这些庞然大物,写个简单界面都像是在搬砖。忽然,我想起了 NiceGUI——一个超轻量、超简单、超好用的Python GUI框架!想着很多小伙伴可能还没用过,那必须得给你们整一期深入教程了!🔥
\\n
NiceGUI 是一个基于 Python + Web 的 GUI 框架,它的 界面直接在浏览器里运行,但代码写起来像 Tkinter 一样简单。最重要的是,它完全 不需要前端知识,适合咱们这种专注于 Python 的人。
\\n安装起来也超级简单:
\\npip install nicegui\\n
\\n然后,跑一个最简单的例子👇
\\nfrom nicegui import ui \\n\\nui.label(\'Hello, NiceGUI!\') # 显示文本\\nui.run() # 运行服务器\\n
\\n运行后,你的浏览器里就会打开一个页面,显示 “Hello, NiceGUI!”,这也太丝滑了吧?!😂
\\nNiceGUI 提供了丰富的 组件和功能,花姐带你飞,从 基础到进阶 一步步搞懂它的强大之处!
\\n文本组件是 GUI 里最基础的内容,比如 标题、段落、代码框 等。
\\nfrom nicegui import ui \\n\\nui.label(\'普通文本\')\\nui.markdown(\'# 这是一级标题\\\\n## 这是二级标题\')\\nui.html(\'<b>支持 HTML 代码哦!</b>\')\\n\\nui.run()\\n
\\n🌟 重点:
\\nui.label()
👉 适用于普通文本。ui.markdown()
👉 适用于带格式的文本。ui.html()
👉 可以直接嵌入 HTML 代码。UI 交互怎么能少了按钮、输入框、滑块这些小玩意呢?
\\nfrom nicegui import ui \\n\\ndef on_click():\\n ui.notify(\'你点击了按钮!🎉\')\\n\\nui.button(\'点我!\', on_click=on_click)\\n\\nui.input(label=\'请输入点啥\', placeholder=\'开始输入\',\\n on_change=lambda e: result.set_text(\'你输入的内容为: \' + e.value),\\n validation={\'输入的内容太长了\': lambda value: len(value) < 20})\\nresult = ui.label()\\n\\nui.run()\\n
\\n🌟 重点:
\\nui.button(\'文本\', on_click=回调函数)
👉 让按钮拥有点击事件。ui.input(\'提示文本\')
👉 创建输入框,并可以绑定事件。ui.notify(\'xxx\')
👉 创建 通知框,可用于提示用户操作反馈!😂 有趣的地方:
\\n可以用 notify()
来整点整活,比如输入 666
时弹出“老板牛逼!”😂
NiceGUI 也支持 播放音频、视频、图片等多媒体内容,可以用来做 数据可视化,甚至可以做个 Web 版的音乐播放器!
\\nfrom nicegui import ui \\n\\nui.image(\'https://picsum.photos/300\') # 显示一张随机图片\\nui.audio(\'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3\', autoplay=True)\\n\\nui.run()\\n
\\n🌟 重点:
\\nui.image(\'图片路径\')
👉 可以加载本地/网络图片。ui.audio(\'音频路径\', autoplay=True)
👉 播放音频,支持自动播放。😂 细节注意:
\\n如果想让音频自动播放,部分浏览器可能需要 用户交互(比如点击一下页面)。
有时候我们需要展示数据,比如表格、进度条、统计信息等。
\\nfrom nicegui import ui \\n\\ncolumns = [\\n {\'name\': \'姓名1\', \'label\': \'姓名\', \'field\': \'name\', \'required\': True, \'align\': \'left\'},\\n {\'name\': \'年龄1\', \'label\': \'年龄\', \'field\': \'age\', \'sortable\': True},\\n]\\nrows = [\\n {\'name\': \'哪吒\', \'age\': 18},\\n {\'name\': \'张飞\', \'age\': 21},\\n {\'name\': \'三太子\'},\\n]\\nui.table(columns=columns, rows=rows, row_key=\'name\')\\n\\nui.linear_progress(0.5) # 50% 进度条\\n\\nui.run()\\n
\\n\\n🌟 重点:
ui.table(columns=..., rows=...)
👉 轻松创建数据表格。ui.linear_progress(0.5)
👉 进度条,值在 0~1
之间。💡 开发者容易忽视的小细节:
\\ntable
里的 columns
需要 name
和 label
,否则不会正常显示!
NiceGUI 让 前端界面和 Python 代码的数据同步 变得异常简单,不需要手动监听事件,只要 绑定属性,前端改了,Python 变量自动更新!
\\nfrom nicegui import ui\\n\\ndata = {\'name\': \'花姐\', \'age\': 17}\\n\\nui.label().bind_text_from(data, \'name\', backward=lambda n: f\'Name: {n}\')\\nui.label().bind_text_from(data, \'age\', backward=lambda a: f\'Age: {a}\')\\n\\nui.button(\'年龄变成 18 岁\', on_click=lambda: data.update(age=18))\\n\\nui.run()\\n
\\n🌟 重点:
\\nbind_text_from
直接绑定数据,逻辑清晰,无需额外的监听器data
的值是从数据库、API 或传感器获取的,你不需要不停 刷新
UI,数据变化 = UI 变化,完美!💡 你可能忽略的细节:
\\n用 globals().update(变量=值)
可以让匿名函数修改外部变量,否则 Lambda 不能直接改外部变量!
一个好看的界面离不开 布局,NiceGUI 里有 栅格系统、侧边栏、分区容器 这些布局工具。
\\nfrom nicegui import ui \\n\\nwith ui.row():\\n ui.button(\'左边的按钮\')\\n ui.button(\'右边的按钮\')\\n\\nwith ui.column():\\n ui.label(\'上面的文字\')\\n ui.label(\'下面的文字\')\\n\\nwith ui.tabs().classes(\'w-full\') as tabs:\\n one = ui.tab(\'Tab1\')\\n two = ui.tab(\'Tab2\')\\nwith ui.tab_panels(tabs, value=two).classes(\'w-full\'):\\n with ui.tab_panel(one):\\n ui.label(\'我是Tab1\')\\n with ui.tab_panel(two):\\n ui.label(\'我是Tab2\')\\n\\nui.run()\\n
\\n\\n🌟 重点:
ui.row()
👉 水平布局,组件并排放置。ui.column()
👉 垂直布局,组件上下排列。ui.tabs()
👉 Tab标签页,适合用于分类展示信息 。默认的 NiceGUI 界面已经很清爽,但如果想要 更个性化的设计,可以调整 颜色、字体、样式!
\\nfrom nicegui import ui\\n\\ndark = ui.dark_mode()\\nui.label(\'主题切换:\')\\nui.button(\'黑色主题\', on_click=dark.enable ,color=\\"green\\")\\nui.button(\'浅色主题\', on_click=dark.disable ,color=\\"red\\")\\n\\nui.run()\\n
\\n\\n🌟 重点:
color=\'xxx\'
👉 改变组件颜色,如 red
、blue
等。ui.dark_mode()
👉 一键切换 暗黑模式!💡 细节注意:
\\n有些组件不支持 color
,但可以用 .style(\'background-color: xxx\')
来手动调整!
NiceGUI 支持各种事件监听,如 点击、悬停、拖拽、键盘输入 等。
\\nfrom nicegui import ui \\n\\ndef on_key(e):\\n ui.notify(f\'你按了 {e.key}\')\\n\\nui.keyboard(on_key)\\n\\n\\nwith ui.row():\\n ui.button(\'A\', on_click=lambda: ui.notify(\'你点击了按钮 A.\'))\\n ui.button(\'B\').on(\'click\', lambda: ui.notify(\'你点击了按钮 B.\'))\\nwith ui.row():\\n ui.button(\'C\').on(\'mousemove\', lambda: ui.notify(\'鼠标移动到了 C 按钮上面了\'))\\n ui.button(\'D\').on(\'mousemove\', lambda: ui.notify(\'鼠标移动到了 D 按钮上面了\'), throttle=0.5)\\n\\nui.run()\\n\\n
\\n🌟 重点:
\\nclick
👉 监听单击事件。mousemove
👉 监听鼠标悬停。ui.keyboard(函数)
👉 监听键盘输入事件。😂 有趣的地方:
\\n可以做个“精神测验”:如果用户按 F
,就弹出 你真的有点东西!
😂
如果想要 多个页面,NiceGUI 也支持 路由功能!
\\nfrom nicegui import ui \\n\\n@ui.page(\'/\')\\ndef main():\\n ui.label(\'这是主页\')\\n ui.link(\'去设置页\', \'/settings\')\\n\\n@ui.page(\'/settings\')\\ndef settings():\\n ui.label(\'这是设置页\')\\n ui.link(\'返回主页\', \'/\')\\n\\nui.run()\\n
\\n\\n🌟 重点:
@ui.page(\'/路径\')
👉 定义不同的页面。ui.link(\'文本\', \'路径\')
👉 创建页面跳转链接。💡 细节注意:
\\n如果页面刷新后数据丢失,可以用 ui.storage
存储数据!
NiceGUI 内置了 FastAPI 服务器,可以很方便地 部署到云端 或 打包成独立应用!
\\nfrom nicegui import ui \\n\\nui.label(\'Hello, NiceGUI!\')\\nui.run(host=\'0.0.0.0\', port=8080) # 指定端口\\n
\\n🌟 重点:
\\nhost=\'0.0.0.0\'
👉 让局域网设备访问。port=8080
👉 指定端口,默认是 8080
。💡 部署方式:
\\npython app.py
。gunicorn
或 uvicorn
运行后端。nicegui-pack --onefile --name \\"myapp\\" app.py
写完这篇文章,我不得不感叹 NiceGUI 太香了!
\\n如果你也对 GUI 开发感兴趣,那就赶紧试试 NiceGUI 吧!🤩
\\n想学习更多NiceGUI的内容可登录官网:https://nicegui.io/
查看
👉 最后,顺手点赞 + 在看,就是对花姐最大的支持!💖
","description":"前言:昨天晚上加班回家,刚躺下就想着,Python做GUI总得折腾PyQt、Tkinter这些庞然大物,写个简单界面都像是在搬砖。忽然,我想起了 NiceGUI——一个超轻量、超简单、超好用的Python GUI框架!想着很多小伙伴可能还没用过,那必须得给你们整一期深入教程了!🔥 🎉 NiceGUI 是个啥?\\n\\nNiceGUI 是一个基于 Python + Web 的 GUI 框架,它的 界面直接在浏览器里运行,但代码写起来像 Tkinter 一样简单。最重要的是,它完全 不需要前端知识,适合咱们这种专注于 Python 的人。\\n\\n安装起来也超级…","guid":"https://juejin.cn/post/7495020421218435082","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T01:03:21.772Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e93ebe37a9fe4ced9796d9fb277c5501~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=QPLWWUIqO8qiKJGRZChprkbEUbc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ba5980d3920431199e78ed4ff9e5e98~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=yV5jZGVFFKCdJWrvDK6e5EFVnn8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/25331c97e6c44ab1a950ce293b75fc8d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=rhDJusRkfve56605aLTwSWXRgGQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/616ef28d4c0d483f9d8f9207e5ad35d3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=krEtOAmD9zBeei6GZNngotpOLP0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6878b8a69c4b4ff2be665c18b6a3e521~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=lFUCQb5%2BR668k0Ky6oz%2F0qa6uMk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f896de311cd74049b3351689a78bcfbf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=PnaQldpmHAoEwPhSfvxGfSjCf4Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/175356941efc45b7b6703f814b76be4d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=8pb7j1TWYzyjwL4fBze3SLvxM54%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b955fe4a339b4b8bb94d9feedf475e9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=3XA4T1WJv%2BlJB61uSkUDubdKY3o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/de497e3c4c8743659aef6bbaa42bb845~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=pvPlHITy%2BCFk0nFP1dwsB5zpt44%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bd140f84ebb849698d005f7054ce33d2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=TwvgszIzLqTzMTcyDupN34drR7A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78a0f8076d134b058e3ec0ba9d7e657e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=7OzDCMIE7%2BRcVsaiH%2FBA0ucpN4w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/185b0092d75e48a1a8ac32e3850e7a53~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1746404624&x-signature=CXo7vndbBo8BWdccB4Y5Ljzbhro%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","前端","Python"],"attachments":null,"extra":null,"language":null},{"title":"后端开发其实没你想的那么难","url":"https://juejin.cn/post/7494920513966522403","content":"很多人认为后端技术很复杂,但其实一般项目的技术其实压根就没有那么高要求。什么查询优化,数据缓存弄着弄那的,结果一看数据才几万条。
\\n复杂的其实是业务,而不是技术。技术你不会可以学,但业务可能上一秒做好了,下一秒产品经理就说:“这个流程改一下。”
\\n后端的开发,本质其实就是三个步骤:
\\n\\n\\n接收数据 → 处理数据 → 返回数据
\\n
说白了就是前端把请求发过来,你拿到参数,处理一下,再返回结果。我们平时讲的 “CRUD”(增删查改)其实就是这样。
\\n所有的后端技术,几乎都是围绕这个流程展开的。
\\n但就算你一开始啥都不会,只要把这个流程搞清楚,一步一步照着做,基本上就能写出一个能用的后端系统。
\\n比如我们用 Express 写一个最简单的后端:
\\nconst express = require(\'express\')\\n\\nconst app = express()\\n\\napp.get(\'/\', (req, res) => {\\n res.send(\'Hello World\')\\n})\\n\\napp.listen(3000)\\n
\\n以上就是一个简单的后端。访问 localhost:3000
就能看见 Hello World 字样。
如果遇到了需要接收参数的情况,就根据请求类型从请求头里面取出来就行。
\\n拿到参数了就调方法,调完方法接着就返回数据。
\\n说它简单,是说入门容易,真要做出一个靠谱、稳定、可扩展的系统,那难度还是很高的。
\\n比如下面这些看起来“简单”的需求,其实每一个做起来都藏着坑:
\\n这就是为什么说“复杂的不是技术,而是业务”。
\\n因为技术可以查文档,学教程,但业务是你踩过坑、看过线上事故、参与过版本迭代,才能真正掌握的。
\\n那问题来了:既然后端开发流程这么简单,那为什么各种后端框架还层出不穷?
\\n答案是:框架本质上就是对底层的封装,是为了简化流程、提升效率,让开发者少踩坑。
\\n我使用现金,得先找到现金,然后出门,来到菜市场,再找对应的商铺才能买菜,没标价要手动问价,可能还要费劲心思砍价,同时现金支付,要注意对方找的是不是假币,现金交易还不好记录。
\\n我使用电子支付,出门边上就是生活超市,要买的东西全部都在一个分区,全部明码标价,不议价,电子支付不需要担心假币,支付后有电子账本自动记账可以查看。
\\n通过框架,我们简化了流程,去除了不必要的重复代码,还提高了效率。在以上例子中:
\\n你依然是买菜,但效率、体验、稳定性,完全不一样了。
\\n这也是为什么大家愿意花时间学习框架,甚至在已有框架之上再二次封装业务框架,为了就是统一流程,减少重复劳动。
\\n诚然,使用框架能大大提升开发效率,但它也不是万能的。
\\n就像前面所说,用框架开发,就像去超市买菜,虽然方便、省时、有保障,但也失去了一部分自由。
\\n你去超市买菜,看到价格是 1块5一把,称重、打包、结账、走人,全流程高效流畅。
\\n在菜市场,这菜也许只要1块钱,你还能砍价,甚至老板心情好送你几根辣椒。你想多抓一把,也没人管你。
\\n你还可以:
\\n自由度高,选择灵活,操作空间大。
\\n框架帮你封装了流程、约定了规范、隐藏了细节——确实让你写代码时更轻松。但如果你想“多抓一把菜”时,就可能发现:
\\n\\n\\n你想走小路,因为它是一条捷径,可框架却不知道,非要导航你走大道,绕一大圈。
\\n
所以,不必神化框架,也别排斥框架。
\\n关键是你是否理解它的机制,知道它给你带来了什么,又限制了什么。
\\n\\n\\n真正的自由,是你知道规则之后,依然可以做选择。
\\n
后端开发,其实没有那么神秘。
\\n别怕技术看起来多复杂,大多数项目用的技术其实都很基础。
\\n怕的不是你不会技术,而是你不愿意去理解业务。
\\n如果你能把“流程处理”变成“业务理解”,那你已经不是一个“写后端”的人了,而是一个“解决问题”的人。
\\n最终,不论你用什么语言、什么框架,写的还是“前端请求 → 后端处理 → 返回结果”的那件事,只不过你做得更稳、更快、更聪明了而已。
\\n那时,你就会发现,所谓的后端开发,其实从来不是在堆技术名词,而是在解决一个又一个问题。
","description":"很多人认为后端技术很复杂,但其实一般项目的技术其实压根就没有那么高要求。什么查询优化,数据缓存弄着弄那的,结果一看数据才几万条。 复杂的其实是业务,而不是技术。技术你不会可以学,但业务可能上一秒做好了,下一秒产品经理就说:“这个流程改一下。”\\n\\n后端其实很简单\\n\\n后端的开发,本质其实就是三个步骤:\\n\\n接收数据 → 处理数据 → 返回数据\\n\\n说白了就是前端把请求发过来,你拿到参数,处理一下,再返回结果。我们平时讲的 “CRUD”(增删查改)其实就是这样。\\n\\n所有的后端技术,几乎都是围绕这个流程展开的。\\n\\n需要数据保存?那就用数据库(简单的甚至可以直接写文件)。…","guid":"https://juejin.cn/post/7494920513966522403","author":"墨夏","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-20T16:02:54.386Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"对比 Spring ,Solon 在实现上又有什么差异?","url":"https://juejin.cn/post/7494944356533878822","content":"Solon 是 国产的 Java 企业级应用开发框架 ,算是国内在体系和生态上都比较全面的框架了。
\\n这个框架在 内存占用 \\\\ 启动速度上都很亮眼, 据官方数据 ,整体性能对比 Spring 提高了多倍。
\\n这一篇就来感受一下这个框架 ,并且从源码的角度上 ,来感受一下这个框架的种种,对比一下 Spring ,它又优化了什么。
\\n\\n\\nMaven 依赖
\\n
<project xmlns=\\"http://maven.apache.org/POM/4.0.0\\" xmlns:xsi=\\"http://www.w3.org/2001/XMLSchema-instance\\"\\n xsi:schemaLocation=\\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\\">\\n <modelVersion>4.0.0</modelVersion>\\n\\n <parent>\\n <groupId>org.noear</groupId>\\n <artifactId>solon-parent</artifactId>\\n <version>3.2.0</version>\\n </parent>\\n\\n <groupId>org.example</groupId>\\n <artifactId>solonSimpleDemo</artifactId>\\n <version>1.0-SNAPSHOT</version>\\n <packaging>jar</packaging>\\n\\n <name>solonSimpleDemo</name>\\n\\n <properties>\\n <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\\n </properties>\\n\\n <dependencies>\\n <dependency>\\n <groupId>org.noear</groupId>\\n <artifactId>solon-web</artifactId>\\n </dependency>\\n </dependencies>\\n\\n <build>\\n <finalName>${project.artifactId}</finalName>\\n <plugins>\\n <plugin>\\n <!-- 引入打包插件 --\x3e\\n <groupId>org.noear</groupId>\\n <artifactId>solon-maven-plugin</artifactId>\\n </plugin>\\n </plugins>\\n </build>\\n</project>\\n
\\n\\n\\n启动类
\\n
import org.noear.solon.Solon;\\npublic class App {\\n public static void main(String[] args) {\\n Solon.start(App.class, args);\\n }\\n}\\n
\\n\\n\\nRest 接口
\\n
@Controller\\npublic class HelloController {\\n /**\\n * 这是直接返回值\\n * */\\n @Mapping(\\"/\\")\\n public String hello() {\\n return \\"Hello world!\\";\\n }\\n\\n /**\\n * 这是返回个对象(以json形式)\\n * */\\n @Mapping(\\"/json\\")\\n public Map hello_json() {\\n Map<String,Object> map = new HashMap<>(); //实体也ok\\n map.put(\\"message\\", \\"Hello world!\\");\\n\\n return map;\\n }\\n\\n}\\n
\\n\\n\\n阶段总结 :
\\n
我们知道 ,Spring 的 MVC 功能是基于 Servlet
的 ,其根本的特性是通过 DispatchServlet
接收来自于外部的各种请求。
而 Solon 里面要简单很多 ,其默认核心的依赖是 solon-boot-smarthttp ,而从官方的依赖列表里面 ,Solon 的 HTTP 层面还支持 : jdkhttp , jetty 和 undertow.
\\n我们来简单学习一下这四个框架 :
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n框架 | 实现原理 | 主要特性 | 性能指标(线程模型 & 吞吐) | 适用场景 |
---|---|---|---|---|
SmartHttp (SmartBoot) | 基于 Java NIO 异步非阻塞,事件驱动,利用 Epoll/kqueue | - 纯异步 NIO 架构 - 轻量高性能 - 灵活的协议扩展 - 支持全链路异步处理 | 高效纯异步线程模型 支持大量并发连接 低延迟,高吞吐(百万级 TPS,视环境不同) | 高并发微服务、游戏、实时通信 |
JDK HttpServer | Java 自带,基于线程池的阻塞 IO 实现,简单 HTTP 服务器 | - 内置 JDK,无需额外依赖 - 简单易用 - 仅支持基础 HTTP 功能 | 线程池阻塞模型 低并发适用 吞吐和延迟一般,不适合高并发 | 简单应用、快速开发、测试调试 |
Jetty | 基于 Java NIO 异步非阻塞,Servlet 容器,多线程队列 | - 完整 Servlet 规范支持 - 轻量灵活 - 支持 HTTP/2, WebSocket - 丰富扩展和生态整合 | 异步线程池+请求队列 线程复用良好 性能优异,百万级 TPS(硬件及配置相关) | 传统中大型 Web 应用,Servlet 生态 |
Undertow | 基于 Java NIO,借助 XNIO 框架实现轻量异步非阻塞 | - 极简轻量 - 支持 Servlet 3.1 异步 API - HTTP/2 支持 - 内置 WebSocket | 高效事件驱动异步 线程数少,减少上下文切换 吞吐量高,性能与 Jetty 相似或略优 | 微服务、轻量 Web 服务及嵌入式服务器 |
\\n\\nsolon HTTP 流程的核心流程处理 :
\\n
\\n\\n这是 Spring MVC 的核心流程 :
\\n
\\n\\n阶段总结 :
\\n
Spring 默认带的 Tomcat 有问题吗 ? 一点问题没有 , Tomcat 的性能是够的 , 这一点已经有大量案例了 。
\\n但是从上面流程可以对比出来 ,Spring 的 Web 处理太完备了 ,以至于一个简单的 HTTP 请求链过长
,其中可能多做了50% 的无意义操作.
Solon 的处理很原生 ,主要在最底层的框架上面做了一些必要的封装。简单请求里面 ,从SmartHttp 透传请求 ,到业务方接收到请求 ,整体的处理栈差不多在10层左右。
\\nSolon 的启动是从 Solon.class
的 start
方法开始。重点是在 SolonApp 中 ,其中主要可以分为3步 :
\\n\\nPlugin 的扫描处理 :
\\n
protected void pluginScan(List<ClassLoader> classLoaders) {\\n for (ClassLoader classLoader : classLoaders) {\\n //扫描配置\\n PluginUtil.scanPlugins(classLoader, pluginExcludeds, plugins::add);\\n }\\n\\n //扫描主配置\\n PluginUtil.findPlugins(AppClassLoader.global(), this, pluginExcludeds, plugins::add);\\n\\n //插件排序\\n Collections.sort(plugins);\\n}\\n
\\n\\n\\nRun 方法中对 Bean 的扫描起点
\\n
//2.1.通过注解导入bean(一般是些配置器)\\nbeanImportTry();\\n\\n//2.2.通过源扫描bean\\nif (source() != null && enableScanning()) {\\n context().beanScan(source());\\n}\\n
\\n\\n\\nSpring 对比 :
\\n
Bean 真正扫描的起点在于 :AppContext # beanScan 部分 :
\\n\\n\\nSpring部分 :
\\n
这部分有点意思 ,Solon 对 AOP 进行了很直接的简化 ,简化为了两个注解 ,使用上反而会复杂点 :
\\n所以 ,其实 @Around 更像之前 Spring 的 AOP 流程。由于 Solon 相当于省略了整个 AOP 的自动化处理 ,更接近于对象代理 ,所以配置起来会更复杂点 :
\\n\\n// S1 : 准备一个注解 \\n@Target({ElementType.METHOD, ElementType.TYPE}) //支持加在类或方法上\\n@Retention(RetentionPolicy.RUNTIME)\\npublic @interface AopDemo {\\n}\\n\\n// S2 : 准备拦截器\\n@Slf4j\\npublic class LogInterceptor implements Interceptor {\\n @Override\\n public Object doIntercept(Invocation inv) throws Throwable {\\n System.out.println(\\"拦截器执行了\\");\\n return inv.invoke();\\n }\\n}\\n\\n// S3 : 启动类里面注入\\npublic static void main(String[] args) {\\n Solon.start(App.class, args, app->{\\n app.context().beanInterceptorAdd(AopDemo.class, new LogInterceptor());\\n });\\n}\\n
\\n\\n\\n源码层面 :
\\n
核心也是分为两个部分 :
\\n\\n// S1 : 创建代理对象 \\n// C- AppContext\\nif (tryProxy) {\\n //是否需要自动代理\\n enableProxy = enableProxy || beanInterceptorHas(bw.clz());\\n\\n if (enableProxy) {\\n ProxyBinder.global().binding(bw);\\n }\\n}\\n\\n// S2 : 调用代理的方法\\nC- BeanInvocationHandler\\npublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { \\n // 判断是否存在自定义处理器,如果没有则调用默认的执行流程 \\n if (this.handler == null) { \\n // 允许访问私有方法 \\n method.setAccessible(true);\\n\\n // 通过上下文获取对应类的对应方法的 MethodWrap(或类似封装) \\n // bw.context() 表示当前的上下文环境 \\n // bw.rawClz() 获取原始类(目标类) \\n // methodGet 方法根据类和方法反射信息获取相应的包装MethodWrap对象 \\n // invokeByAspect 表示通过AOP切面逻辑来执行这个方法,传入 this.bean 作为目标实例,args 作为参数 \\n Object result = this.bw.context() \\n .methodGet(this.bw.rawClz(), method) \\n .invokeByAspect(this.bean, args); \\n\\n return result; \\n }\\n}\\n\\n
\\n\\n\\nSpring AOP 做了什么 ?
\\n
时间精力有限 ,整体代码没有太深入的看了。整体使用下来 ,如果项目比较小 ,资源也紧张 ,Solon 是一个很值得考虑的框架。
\\n\\n\\n个人的一点思路 :
\\n
Spring 是全面可靠的 ,这点无容置疑 , 所以生产级的大项目 ,相信还是会以 Spring 为主流。
\\n但是个人的一些小 Demo ,和一些资源更少的场景 ,就可以考虑使用 Solon了 ,不用自己造轮子 ,也不会浪费 Spring 的性能。
\\n\\n\\n关于设计的思想 :
\\n
核心源码大致走了一遍 ,Solon 本身设计上是有一些明确的思路的 :
\\nSpring AOP 的使用率并不高
,更多的是Spring自己在用 ,业务上用到也主要是日志级别的处理Listener
,Aware
,PostProcessor
, 很全面 ,但是也太难了、\\nPlugin (组件) + Bean 加载
🌟嗨,我是LucianaiB!
\\n🌍 总有人间一两风,填我十万八千梦。
\\n🚀 路漫漫其修远兮,吾将上下而求索。
\\n在一个慵懒的周末午后,我正享受着悠闲的时光,突然一个念头闪过脑海:下载一款新软件来试试。于是,我随手在某个不知名的小网站上找到了一个看起来很有趣的软件,兴冲冲地下载并安装了它。然而,没过多久我就后悔了,这软件不仅功能鸡肋,还时不时弹出烦人的广告,简直是个垃圾软件!我决定立刻卸载它,可当我打开卸载程序(使用过Geek),却发现它并没有完全清除所有文件(如果不服的,我告诉你软件,你来试试卸载),残留的文件让我感到十分不爽。我心想,一定要找到这个软件的安装路径,彻底清理掉它,让我的电脑恢复干净整洁。
\\n我首先想起了一种常见的方法。在安装软件的时候,我隐约记得有个快捷键组合可以打开任务管理器。于是,我迅速按下 Esc+Shift+Ctrl 这三个键,任务管理器果然出现了。我仔细在任务管理器中寻找那个垃圾软件的进程,找到了之后,我毫不犹豫地右键点击它,选择了“打开文件所在位置”。任务管理器很听话地帮我定位到了软件的安装文件夹,我长舒一口气,开始手动删除那些残留的文件。虽然这个方法有点麻烦,但好在最终还是解决了问题。不过,我心里还是有点不甘心,难道就没有更简单、更高效的方法吗?
\\n就在我陷入沉思的时候,我突然先到影刀最近新出了魔法指令3.0上线,挑战用AI替代我的工作。根据介绍,这个工具可以通过简单的指令来完成各种复杂的电脑操作,简直就是电脑操作界的“魔法棒”。我半信半疑,但还是决定试一试,说不定真的能找到更便捷的方法呢。
\\n1.首先新建‘PC自动化应用’
\\n2.点击‘魔法指令’
\\n3.输入提示词
\\n在影刀界面输入需求,它就会自动去编写代码:
\\n\\n\\n请帮我找到名为:我会给你窗口对象(例如微信)的桌面软件程序的完整安装路径,并返回完整文件路径
\\n
4.测试与结果输出
\\n我们点击运行试试效果
\\n成功运行后,影刀RPA输出了微信的安装路径,接下来就是找到垃圾软件的位置进行删除,整个指令执行结果输出仅需3秒!
\\n再试试其他的应用,例如Pycharm,出现下图
\\n原来是我这里是根据窗口去找,而Pycharm在窗口的名字是main.py,那么我们来搜索main.py试试,成功找到Pycharm的安装位置。
\\n附魔法指令生成的python代码:
\\n# 使用此指令前,请确保安装必要的Python库:\\n# pip install pywin32 psutil\\n\\nimport win32gui\\nimport win32process\\nimport psutil\\nimport os\\n\\nfrom typing import *\\ntry:\\n from xbot.app.logging import trace as print\\nexcept:\\n from xbot import print\\n\\n\\ndef get_application_path(window_title):\\n \\"\\"\\"\\n title: 获取软件安装路径\\n description: 根据窗口标题查找桌面应用程序的安装路径,支持模糊匹配窗口标题,返回完整的可执行文件路径。\\n inputs: \\n - window_title (str): 窗口标题,eg: \\"微信\\"\\n outputs: \\n - path (str): 程序安装路径,eg: \\"C:\\\\Program Files\\\\WeChat\\\\WeChat.exe\\"\\n \\"\\"\\"\\n \\n # 定义一个列表来存储找到的窗口和对应的路径\\n result = {}\\n \\n def _enum_windows_callback(hwnd, _):\\n if win32gui.IsWindowVisible(hwnd):\\n title = win32gui.GetWindowText(hwnd)\\n if window_title.lower() in title.lower():\\n try:\\n # 获取窗口关联的进程ID\\n _, pid = win32process.GetWindowThreadProcessId(hwnd)\\n # 使用进程ID获取进程信息\\n process = psutil.Process(pid)\\n # 获取进程的可执行文件路径\\n exe_path = process.exe()\\n if os.path.exists(exe_path):\\n result[title] = exe_path\\n except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):\\n pass\\n return True\\n \\n # 枚举所有窗口\\n win32gui.EnumWindows(_enum_windows_callback, None)\\n \\n # 如果找到多个匹配的窗口,返回所有结果\\n if not result:\\n return f\\"未找到标题包含\'{window_title}\'的窗口\\"\\n \\n # 格式化输出结果\\n if len(result) == 1:\\n title, path = next(iter(result.items()))\\n return path\\n else:\\n # 如果找到多个结果,返回第一个\\n first_title, first_path = next(iter(result.items()))\\n return first_path\\n\\n
\\n这次经历让我深刻体会到科技的力量和便捷性。在面对电脑问题时,传统的手动操作虽然也能解决问题,但过程繁琐且效率低下。而影刀的魔法指令3.0则像一位智能的助手,通过简单的指令就能快速完成复杂的任务。它不仅帮我找到了垃圾软件的安装路径,还清理了电脑上的残留文件,甚至还能主动发现并解决潜在问题。这次经历让我认识到,合理利用科技工具可以大大提高我们的工作效率和生活质量。在未来,我将继续探索更多类似的工具,让科技为我的生活带来更多便利。同时,我也提醒大家在下载软件时一定要谨慎,避免不必要的麻烦(如果不服的,我告诉你软件,你来试试卸载),期待有人试试。
\\n\\n\\n嗨,我是LucianaiB。如果你觉得我的分享有价值,不妨通过以下方式表达你的支持:👍 点赞来表达你的喜爱,📁 关注以获取我的最新消息,💬 评论与我交流你的见解。我会继续努力,为你带来更多精彩和实用的内容。
\\n
点击这里👉LucianaiB ,获取最新动态,⚡️ 让信息传递更加迅速。
","description":"用魔法打败魔法——获取软件安装路径 🌟嗨,我是LucianaiB!\\n\\n🌍 总有人间一两风,填我十万八千梦。\\n\\n🚀 路漫漫其修远兮,吾将上下而求索。\\n\\n目录\\n\\n背景\\n\\n普通方法\\n\\n用魔法一句话\\n\\n1.首先新建‘PC自动化应用’\\n2.点击‘魔法指令’\\n3.输入提示词\\n4.测试与结果输出\\n\\n总结\\n\\n背景\\n\\n在一个慵懒的周末午后,我正享受着悠闲的时光,突然一个念头闪过脑海:下载一款新软件来试试。于是,我随手在某个不知名的小网站上找到了一个看起来很有趣的软件,兴冲冲地下载并安装了它。然而,没过多久我就后悔了,这软件不仅功能鸡肋,还时不时弹出烦人的广告…","guid":"https://juejin.cn/post/7494484162037956649","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T17:17:04.036Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b03fea3d1ee84e27a46c0c34d40f8912~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=A7AeG0FoWgcLh8F4OSeRkVKsRws%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d44911b3405d4964bc8d1673ce1fda71~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=5Gg4lDkwM2bwdPyGUZANLz%2BGbpY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f644de8864254e0abcd374ee3e38b44a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=bR6T%2FA2kJy%2F%2F1c9FyRNly9dZwWU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/24399c4e92e54cb38fd2f4708c18c082~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=pzgLhCWvbDzoKQiRpf2hnrOC5KA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ba02e7cf9c3f4822a726f740e13e14eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=vEudsAaBcXMo5XMldFTDwQ2Qlv0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06dd1be537dc45c7a7b8b6a1f0895533~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=E5IuhhB4t%2FDyKgH%2F3SFeIRv6EJE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8a54a4722f2d499a8fc72fd80873ffe2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=PkXOzLymjXjnwveh%2BE2DQIkoIV4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9ad6f401a31d4ad5b740c1f01ea9bf2c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745601424&x-signature=%2BxC3xR0dCi8hq9%2Fk8sNwPskeLDM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","AIGC"],"attachments":null,"extra":null,"language":null},{"title":"分布式并发业务场景---分布式锁,以及不用锁方案","url":"https://juejin.cn/post/7494201369303564298","content":"\\n\\n分布式并发业务场景不要只会说加分布式锁,其实解决问题的方案有很多
\\n先梳理好业务场景,找到该场景下的痛点
\\n有些场景甚至不加锁,依靠sql语句即可解决,并且还是最优解,更符合业务场景
\\n
并发问题解决方案:
\\n在我们的项目中,有很多并发的场景,但是我们的解决思路是不一样的。这一篇就从整体并发防控的角度来把这些方案以及为什么用介绍一下。
\\n在并发防控上,其实归根结底就两个方案:
\\n乐观锁主要以数据库的乐观锁为主,比如类似下面这种:
\\n\\nUPDATE aska_collection SET\\n name=?,\\n cover=?,\\n purchase_price=?,\\n collection_id=?,\\n serial_no=?,\\n nft_id=?,\\n user_id=?,\\n state=?,\\n tx_hash=?,\\n hold_time=?,\\n sync_chain_time=?,\\n biz_type=?,\\n biz_no=?,\\n lock_version=?,\\n gmt_create=?,\\n gmt_modified=?\\nWHERE id=?\\n AND lock_version=?\\n AND deleted=0\\n \\n // 这里面的UPDATE aska_collection set lock_version = ? where lock_version = ? \\n // 就是一个非常典型的用版本号来做乐观锁控制的场景。\\n
\\n悲观锁的话,主要以 Redis 实现的分布式锁为主,如下面这种:
\\n\\n@DistributeLock(keyExpression = \\"#request.identifier\\", scene = \\"ORDER_CREATE\\")\\npublic OrderResponse create(OrderCreateRequest request) {\\n //这里面的@DistributeLock是我们自己给予 Redisson 封装的一个分布式锁的注解。\\n}\\n\\n
\\n那么,这两种方案我们是如何选择的呢?其实这主要就是悲观锁和乐观锁的区别了。
\\n\\n\\n先上干货 也就是说,乐观锁是先干活,后加锁。悲观锁是先加锁,再干活
\\n
乐观锁的基本思想是假设冲突很少发生,每个线程在修改数据之前,先获取一个版本号或时间戳,并在更新时检查这个版本号或时间戳,以确保其他线程没有同时修改数据。
\\n乐观锁适用于读操作频繁,写操作相对较少的场景。当冲突较少,且并发写入的概率较低时,乐观锁的性能可能更好。
\\n悲观锁则是假设冲突经常发生,因此在访问共享资源之前,线程会先获取锁,确保其他线程无法同时访问相同的数据。这可能导致并发性降低,因为只有一个线程能够访问数据。
\\n悲观锁适用于写操作较为频繁,且并发写入的概率较高的场景。悲观锁可以有效地避免多个线程同时修改相同数据的情况。
\\n乐观锁和悲观锁还有个区别:乐观锁因为比较乐观,所以一般是先做业务逻辑操作,比如参数处理,内存中进行模型组装调整,然后再去更新数据库。悲观锁因为比较悲观,所以会先尝试加锁,然后再去做业务逻辑操作
\\n而高并发的写操作时,你干了一大堆活,把模型都组装好了,内存计算也都做完了,结果最后去数据库那更新的时候发现版本号变了。这不是大冤种吗?
\\n所以,应该是先尝试获取锁,如果获取锁成功,再进行业务操作,否则就直接返回失败。这样可以做fail-fast。
\\n在我们大部分的业务场景中,锁的选择一方面考虑了上面提到的并发的情况,另外一方面,也考虑到了乐观锁其实只适合用在有 update 的场景。
\\n在订单模块中,有很多会出现并发的场景
\\n首先,我们根据并发情况分析,其实只有订单的创建是高并发的场景,而订单的支付成功的回调、关单动作等等都不是并发特别高的。
\\n所以,针对创建订单的接口,我们使用了分布式锁来对用户传入的下单的幂等号来做并发控制
\\n\\n@Override\\n@DistributeLock(keyExpression = \\"#request.identifier\\", scene = \\"ORDER_CREATE\\")\\npublic OrderResponse create(OrderCreateRequest request) {\\n try {\\n orderValidatorChain.validate(request);\\n } catch (OrderException e) {\\n return new OrderResponse.OrderResponseBuilder().buildFail(ORDER_CREATE_VALID_FAILED.getCode(), e.getErrorCode().getMessage());\\n }\\n\\n InventoryRequest inventoryRequest = new InventoryRequest(request);\\n SingleResponse<Boolean> decreaseResult = inventoryFacadeService.decrease(inventoryRequest);\\n\\n if (decreaseResult.getSuccess()) {\\n return orderService.createAndAsyncConfirm(request);\\n }\\n throw new OrderException(OrderErrorCode.INVENTORY_DECREASE_FAILED);\\n}\\n\\n
\\n而在其他的几个并发场景中,我们没有显示的加悲观锁,而是通过状态机+乐观锁实现的。
\\n比如同一笔订单的多个渠道同时成功,在我们以下逻辑中实现:
\\n@Override\\npublic OrderResponse paySuccess(OrderPayRequest request) {\\n OrderResponse response = orderService.paySuccess(request);\\n if (!response.getSuccess()) {\\n TradeOrder existOrder = orderReadService.getOrder(request.getOrderId());\\n if (existOrder != null && existOrder.isClosed()) {\\n return new OrderResponse.OrderResponseBuilder().orderId(existOrder.getOrderId()).buildFail(OrderErrorCode.ORDER_ALREADY_CLOSED.getCode(), OrderErrorCode.ORDER_ALREADY_CLOSED.getMessage());\\n }\\n if (existOrder != null && existOrder.isPaid()) {\\n if (existOrder.getPayStreamId().equals(request.getPayStreamId()) && existOrder.getPayChannel() == request.getPayChannel()) {\\n return new OrderResponse.OrderResponseBuilder().orderId(existOrder.getOrderId()).buildSuccess();\\n } else {\\n return new OrderResponse.OrderResponseBuilder().orderId(existOrder.getOrderId()).buildFail(OrderErrorCode.ORDER_ALREADY_PAID.getCode(), OrderErrorCode.ORDER_ALREADY_PAID.getMessage());\\n }\\n }\\n }\\n return response;\\n}\\n\\n\\n// 核心sql\\n\\n<update id=\\"updateByOrderId\\" parameterType=\\"tradeOrder\\">\\n update trade_order set gmt_modified = now(),lock_version = lock_version + 1\\n <if test=\\"orderState != null\\">\\n , order_state = #{orderState}\\n </if>\\n <if test=\\"closeType != null\\">\\n , close_type = #{closeType}\\n </if>\\n\\n where order_id = #{orderId} and deleted = 0 and lock_version = #{lockVersion}\\n</update>\\n\\n\\n
\\n在这个逻辑中,我们通过订单在更新的时候会添加乐观锁(利用了 mybatisplus 自动识别 lock_version 进行的乐观锁判断),以及做了严格的状态机控制,来保证这个 orderService.pay 方法只会被成功调用一次,下次再调用,则会返回失败。
\\n而在orderService.pay方法返回失败后,我们则去判断下订单的支付状态,以及订单上记录的上一次支付成功的信息,来判断是否发生了多付。
\\n所以,针对这种场景,同一个订单多个渠道支付成功并发其实并不高,出现多付的概率也并不大的情况,我们没必要加一个悲观锁,直接用乐观锁就行了。
\\n而其他的几个场景,和这个场景一样,也都是这样的,并发不高,完全可以借助状态机+乐观锁的校验来确保在数据库更新的时候只有一个线程能成功来防止并发的。
\\n!!!留个问题 !!!
\\n\\n\\n为什么库存扣减不需要加锁
\\n尤其是乐观锁?在库存扣减的方法中(本文特指数据库的扣减,不包含 Redis 层)
\\n我们全程是没有加锁的,甚至乐观锁都没加
\\n没加悲观锁是因为秒杀场景不适合用悲观锁
\\n
而我们的这个库存扣减这里,其实并发冲突还挺高的,如果这里用了乐观锁,那么就会有大量的失败。
\\n举个例子,100个线程同时来查询库存,得到的 lock_version = 1,然后同时去更新库存,都要求当前的 lock_version = 1,这就会导致99个线程都失败,那么就会导致整体失败,如果事务中有其他操作,就要回滚。
\\n库存扣减的时候,我们的目的肯定是让多个线程都扣减成功,并且不扣错就行了
\\n如果你的 SQL 是这样的:\\n\\nUPDATE collection\\nSET saleable_inventory = #{saleableInventory}\\nWHERE id = #{id}\\n\\n也就是说这个变更后的saleableInventory是你自己算好的,\\n给到 SQL 去执行的,那么就必须要加锁来避免并发的时候计算出错,\\n但是我们通过在 SQL 中扣减不会有任何并发导致出错的问题。\\n
\\n\\n而如果这里不加乐观锁,只通过库存扣减,\\n以及库存不能小于0来做控制,\\n那么只需要他们各自抢锁然后按顺序扣减就行了,\\n反正每次库存都是在前面的结果上依次扣减的\\n\\n\\nUPDATE collection\\nSET \\n saleable_inventory = saleable_inventory - #{quantity}, -- 减少可售库存\\n lock_version = lock_version + 1, -- 增加锁版本号\\n gmt_modified = now() -- 更新修改时间为当前时间\\nWHERE \\n id = #{id} -- 根据ID定位记录\\n AND <![CDATA[saleable_inventory - frozen_inventory >= #{quantity}]]> \\n -- 确保可售库存减去冻结库存大于等于要减少的数量\\n\\n
\\n所以,这里我们只需要保证百分百不会超卖就行了,剩下的并发问题 update 过程中的(mysql 行锁)锁会帮我们解决的
","description":"前言 分布式并发业务场景不要只会说加分布式锁,其实解决问题的方案有很多\\n\\n先梳理好业务场景,找到该场景下的痛点\\n\\n有些场景甚至不加锁,依靠sql语句即可解决,并且还是最优解,更符合业务场景\\n\\n并发问题解决方案:\\n\\n分布式锁\\n乐观锁\\n悲观锁\\n业务代码无锁,最终靠数据库行锁一次执行,通过sql条件判断\\n✅并发问题解决方案(分布式锁)\\n\\n在我们的项目中,有很多并发的场景,但是我们的解决思路是不一样的。这一篇就从整体并发防控的角度来把这些方案以及为什么用介绍一下。\\n\\n在并发防控上,其实归根结底就两个方案:\\n\\n1、乐观锁\\n2、悲观锁\\n乐观锁\\n\\n乐观锁主要以数据库…","guid":"https://juejin.cn/post/7494201369303564298","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T06:23:23.838Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e614fa11b694a81960df28f45dddde8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745633745&x-signature=Xk4uXEhSwClpjjX0J1xIvk3Vj68%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"史上最强的 Java Solon v3.2.0 发布(并发高 700%;内存省 50%)","url":"https://juejin.cn/post/7494181442107293730","content":"7年开源时长,这一版估计是 Solon 历史上最强之版!
\\nSolon 是新一代,Java 企业级应用开发框架。从零开始构建(No Java-EE),有灵活的接口规范与开放生态。采用商用友好的 Apache 2.0 开源协议,是“杭州无耳科技有限公司”开源的根级项目,是 Java 应用开发的生态基座(可替换 Spring 生态)。
\\n累计代码提交1.6万次 ,近半年下载量1200万次。
\\n特点 | 描述 |
---|---|
更高的计算性价比 | 并发高 700%;内存省 50% |
更快的开发效率 | 代码少;入门简单;启动快 10 倍(调试快) |
更好的生产与部署体验 | 打包小 90% |
更大的兼容范围 | 非 java-ee 架构;同时支持 java8 ~ java24,graalvm native image |
最新的 techempower 测试数据:
\\n\\nSolon 快如闪电!智也非凡!从 v3.1 起,提供完整的 AI 应用开发支持(Solon AI 同时支持 java8 到 java24)。
\\n@Configuration
类,有构建注入且没有源时,造成 @Bean
函数无法注入的问题\\n\\nIt all starts with a Dockerfile. (docker万物始于此)
\\n
这是官网对Dockerfile的描述。
\\n在介绍Dockerfile之前,推荐小白去恶补一下作者之前发的文章,初学者选学,可以留个赞再走,求求了!!
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n标题 | 链接 |
---|---|
SpringBoot应用:Docker与Kubernetes全栈实战秘籍 | juejin.cn/post/744068… |
Docker入门之Windows安装Docker初体验 | juejin.cn/post/741565… |
从零开始玩转 Docker:一站式入门指南,带你快速掌握镜像、容器与仓库 | juejin.cn/post/740318… |
面试官让你介绍一下docker,别再说不知道了 | juejin.cn/post/740283… |
Windows 10环境用Docker发布SpringBoot项目 | juejin.cn/post/724197… |
Dockerfile 是一个文本文件,其中包含了若干个命令,用户可以调用这些命令来构建一个镜像。通过这个文件,开发者能够定义应用程序运行环境的所有细节,从基础操作系统的选择到需要安装的软件包,再到启动服务所需的配置。
\\nDockerfile 不仅是自动化构建的基础,也是实现持续集成和持续部署(CI/CD)流程的关键组成部分。它使得开发团队能够在一致的环境中开发、测试和部署应用,从而减少“在我机器上能跑”的问题。此外,Dockerfile 促进了微服务架构的发展,让每个服务都可以独立打包成容器,易于管理和扩展。
\\nDockerfile 有个和其他文件与众不同的点,就是他没有后缀,他全部的名字就叫做Dockerfile 而不是Dockerfile.txt 或者Dockerfile.yml
\\n先来看一个Dockerfile 的案例(之前作者参加gitee比赛的时候成功运行在比赛服务器上的Dockerfile 示例):
\\n# 使用指定的基础镜像\\nFROM registry.gitee-ai.local/base/iluvatar-corex:3.2.0-bi100\\n# 设置工作目录\\nWORKDIR /app\\n\\n# 添加 Java 环境\\nRUN apt-get update && \\\\\\n apt-get install -y openjdk-8-jdk && \\\\\\n apt-get clean\\n\\n# 复制当前目录下的所有文件到容器内的/app目录下\\nCOPY . /app\\n\\nEXPOSE 7860\\n\\n# 指定启动命令\\nENTRYPOINT [\\"java\\",\\"-jar\\",\\"/app/mergevideo-0.0.1-SNAPSHOT.jar\\"]\\n\\n
\\niluvatar-corex:3.2.0-bi100
从私有仓库 registry.gitee-ai.local
中拉取。/app
。/app
目录下。/app/mergevideo-0.0.1-SNAPSHOT.jar
应用。而作者写的这个dockerFile几乎覆盖了官方文档中说到了主要几个命令。这里介绍一下: docker build
命令。
docker build
命令可以从 Dockerfile 构建一个新的 Docker 镜像。
docker build [OPTIONS] PATH | URL | -\\n
\\nname:tag
格式)。Dockerfile
)。假设你的 Dockerfile 位于当前目录下,并且你想为生成的镜像打上标签 my-app:latest
,可以使用以下命令:
docker build -t my-app:latest .\\n
\\n这里的 .
表示当前目录。
如果你的 Dockerfile 不在当前目录下,可以使用 -f
选项指定其路径:
docker build -t my-app:latest -f path/to/Dockerfile .\\n
\\n你可以使用 --build-arg
选项传递构建时变量。假设你的 Dockerfile 中使用了 ARG
指令:
ARG BUILD_DATE\\nRUN echo \\"Build date: $BUILD_DATE\\"\\n
\\n你可以这样构建镜像:
\\ndocker build -t my-app:latest --build-arg BUILD_DATE=$(date) .\\n
\\n如果你希望构建时不使用缓存,可以使用 --no-cache
选项:
docker build -t my-app:latest --no-cache .\\n
\\n默认情况下,构建完成后会删除中间容器。如果你想明确指定这一点,可以使用 --rm
选项:
docker build -t my-app:latest --rm .\\n
\\n如果你希望在构建前始终从远程仓库拉取最新版本的基础镜像,可以使用 --pull
选项:
docker build -t my-app:latest --pull .\\n
\\n如果你只想输出最终的镜像 ID,可以使用 --quiet
或 -q
选项:
docker build -q -t my-app:latest .\\n
\\n假设你有一个复杂的 Dockerfile,并且需要设置多个构建时变量,可以这样构建:
\\ndocker build -t my-app:latest \\\\\\n --build-arg BUILD_DATE=$(date) \\\\\\n --build-arg VERSION=1.0.0 \\\\\\n --no-cache \\\\\\n --pull \\\\\\n -f path/to/Dockerfile \\\\\\n .\\n
\\n我们的build命令都会根据DockerFile 里面的内容来一步一步构建我们的镜像,那么介绍了build命令,再来认识一下build之后构建的镜像如何运行
\\n运行一个 Docker 容器:
\\ndocker run -d -p 8999:8999 aijava:1.0\\n
\\ndocker run
:这是运行 Docker 容器的命令。-d
:表示在后台运行容器。-p 8999:8999
:将宿主机的 8999 端口映射到容器的 8999 端口。aijava:1.0
:这是要运行的镜像名称和标签。最后,我们就来认识一下DockerFile里面的命令所代表的意思了
\\nDocker 镜像由层组成。每个层都是构建的结果 指令。层按顺序堆叠,每个层都是 表示应用于上一层的更改的增量。
\\n# syntax=docker/dockerfile:1\\n\\n# 使用指定的基础镜像\\nFROM registry.gitee-ai.local/base/iluvatar-corex:3.2.0-bi100\\n\\n
\\n这个是DockerFile中一个特殊的注释
\\n# syntax=docker/dockerfile:1
注释告诉 Docker 使用 BuildKit 构建器来解析和构建 Dockerfile。BuildKit 是 Docker 的下一代构建工具,旨在提高 Docker 镜像构建的性能和可靠性。它引入了许多新的特性和改进,使其成为比传统 Docker 构建器更强大的工具。
写了这个注释可以让我们利用 BuildKit 的高级特性,还可以提高构建性能和灵活性。如果不写这个注释,Docker 将使用传统的构建器构建镜像
\\n# 使用指定的基础镜像\\nFROM registry.gitee-ai.local/base/iluvatar-corex:3.2.0-bi100\\n\\n
\\n这块就是告诉docker以 这个镜像为基础构建一个新镜像,大家可以这么理解,需要装修得要毛坯房吧,而这个基础的镜像就是毛坯房,一般我们使用 FROM Linux系统
这样的写法,因为我们自己构建的镜像其实本质也是运行在一个系统中的,就像我们上面的案例:
# 使用指定的基础镜像\\nFROM registry.gitee-ai.local/base/iluvatar-corex:3.2.0-bi100\\n# 设置工作目录\\nWORKDIR /app\\n\\n# 添加 Java 环境\\nRUN apt-get update && \\\\\\n apt-get install -y openjdk-8-jdk && \\\\\\n apt-get clean\\n
\\n在ubuntu的镜像基础上,安装Java 环境,而这里的结构图就像这样:
\\n这样是不是大家就很好理解这句话了:层按顺序堆叠,每个层都是 表示应用于上一层的更改的增量(Docker images consist of layers. Each layer is the result of a build instruction in the Dockerfile. Layers are stacked sequentially, and each one is a delta representing the changes applied to the previous layer.)。
\\n而 registry.gitee-ai.local/base/iluvatar-corex:3.2.0-bi100这个镜像就是gitee大赛中官方打的一个新镜像,类比我们结构图中的 新镜像A
\\n就是作者案例中的
\\n# 添加 Java 环境\\nRUN apt-get update && \\\\\\n apt-get install -y openjdk-8-jdk && \\\\\\n apt-get clean\\n
\\n这个命令会在上面的镜像的基础上运行一些命令,例如作者这里写的就是安装jdk环境,因为再官方提供的那个镜像是没有jdk环境的,所以运行了这个命令之后,在这个镜像中就有了Java应用所需要的jdk环境了
\\n这个就对应开发者来说太熟悉了,就是注释,例如上面的 # 添加 Java 环境
写了这个dockerfile在构建的过程中就会跳过这个注释,很好理解
这个语法是会将你与dockerfile同路径的文件复制到一个新的目录下面,而新的目录你可以理解为linux中的目录,例如 /
就是可以理解为新镜像中的根目录,例如上面作者的案例就是把所有文件复制到 /app
目录下
# 复制当前目录下的所有文件到容器内的/app目录下\\nCOPY . /app\\n
\\n如果你要指定某个文件的话,例如jar包可以这样写:
\\nCOPY ./masiyi.jar /app\\n# 或者这样\\nCOPY masiyi.jar /app\\n
\\n这个命令设置环境变量,如果熟悉shell命令的同学有福了,就类似那种的设置
\\nVARIABLE_NAME=value\\n
\\n而dockerfile使用 ENV
作为标识,例如在生产过程中你可以这样写:
ENV MAX_HEAP 2048m\\nENV MIN_HEAP 1024m\\n\\nCMD java -jar -Xms${MIN_HEAP} -Xmx${MAX_HEAP} masiyi.jar \\n
\\n这样就可以使用变量来控制jvm的大小了
\\n这块也很好理解,如果你是web服务,需要一个端口,这里只需要和你的web服务保持一样就好了
\\n这里是容器启动时所调用的命令,例如上面的
\\nCMD java -jar -Xms${MIN_HEAP} -Xmx${MAX_HEAP} masiyi.jar \\n
\\n最后你在启动这个镜像的时候就会构建一个容器,从而执行这个命令,这样一个jar包就被启动起来了。但是很多同学容易把CMD和上面的RUN搞混淆,我们来了解一下他们有什么区别
\\nCMD
和 RUN
是 Dockerfile 中用于指定命令的两个不同指令,它们在 Docker 镜像构建和容器运行过程中扮演着不同的角色。
CMD
和 RUN
的区别RUN
:
CMD
:
RUN
:
RUN
指令时,Docker 会创建一个新的中间层,执行该命令,并将结果保存到这个层中。多个 RUN
指令会导致多个层的创建。RUN apt-get update && apt-get install -y curl
会在构建镜像时安装 curl
,并且这个安装结果会被保存到镜像中。CMD
:
CMD
指定的命令只在容器启动时执行,不会在构建镜像时执行。docker run
的命令行参数),CMD
中的命令会被覆盖。CMD [\\"java\\", \\"-jar\\", \\"app.jar\\"]
会在容器启动时运行 java -jar app.jar
,但如果你在 docker run
时指定了其他命令,CMD
中的命令将不会执行。RUN
:
RUN
指令在构建镜像时执行,生成的层是镜像的一部分,无法在运行容器时被覆盖或修改。CMD
:
可覆盖: CMD
指令可以在运行容器时被覆盖。你可以通过 docker run
命令行参数指定不同的命令来替代 CMD
中的默认命令。
例如,如果你有一个 Dockerfile 如下:
\\nCMD [\\"echo\\", \\"Hello, World!\\"]\\n
\\n你可以通过以下命令覆盖 CMD
:
docker run my-image echo \\"Hello, Docker!\\"\\n
\\nRUN
:
你可以有多个 RUN
指令,每个 RUN
指令都会创建一个新的层。为了减少镜像的层数,建议将多个相关的命令合并为一个 RUN
指令,使用 &&
连接多个命令。
例如:
\\nRUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*\\n
\\nCMD
:
你只能有一个 CMD
指令。如果 Dockerfile 中有多个 CMD
指令,只有最后一个 CMD
会生效。
例如:
\\nCMD [\\"echo\\", \\"First command\\"]\\nCMD [\\"echo\\", \\"Second command\\"] # 只有这一条会生效\\n
\\n使用 RUN
安装软件
# 安装 curl\\nRUN apt-get update && apt-get install -y curl\\n
\\n使用 CMD
启动应用程序
# 启动 Java 应用\\nCMD [\\"java\\", \\"-jar\\", \\"app.jar\\"]\\n
\\n不知道大家注意到一个细节没有,那就是dockerfile中的命令基本都是大写,那么最后一个要介绍一个命令就是ENTRYPOINT
ENTRYPOINT
是 Dockerfile 中的一个重要指令,用于定义容器启动时执行的主命令。与 CMD
不同,ENTRYPOINT
提供了一种更灵活的方式来指定容器的默认行为,并且可以与 CMD
结合使用以提供更多的灵活性。
例如作者上面的案例:
\\n# 指定启动命令\\nENTRYPOINT [\\"java\\",\\"-jar\\",\\"/app/mergevideo-0.0.1-SNAPSHOT.jar\\"]\\n
\\n这个就会在容器启动的时候执行 java -jar /app/mergevideo-0.0.1-SNAPSHOT.jar
这是他的一个最基础的用法,但是ENTRYPOINT
和 CMD
可以一起使用,CMD
提供的参数会被传递给 ENTRYPOINT
指定的命令。这种组合非常有用,尤其是当你希望容器在启动时执行一个特定的命令,但允许用户通过 docker run
提供额外的参数时。
# 使用 ENTRYPOINT 和 CMD 启动 Java 应用\\nENTRYPOINT [\\"java\\", \\"-jar\\"]\\nCMD [\\"/app/mergevideo-0.0.1-SNAPSHOT.jar\\"]\\n
\\n解释:
\\nENTRYPOINT [\\"java\\", \\"-jar\\"]
指定了容器启动时的主命令是 java -jar
。CMD [\\"/app/mergevideo-0.0.1-SNAPSHOT.jar\\"]
提供了默认的参数 /app/mergevideo-0.0.1-SNAPSHOT.jar
。docker run my-image
时,容器会执行 java -jar /app/mergevideo-0.0.1-SNAPSHOT.jar
。覆盖 CMD
:
如果你在 docker run
时提供了其他参数,CMD
中的参数会被覆盖。
例如:
\\ndocker run my-image another-app.jar\\n
\\n这将执行
\\njava -jar another-app.jar\\n
\\nENTRYPOINT
与 CMD
的区别特性 | ENTRYPOINT | CMD |
---|---|---|
执行时机 | 容器启动时执行 | 容器启动时执行,默认参数可以被覆盖 |
是否可覆盖 | 不可覆盖,除非使用 --entrypoint 选项 | 可以通过 docker run 命令行参数覆盖 |
用途 | 定义容器的主命令,适合创建可执行容器 | 提供默认参数或命令,适合提供默认行为 |
与 docker run 的关系 | docker run 参数会作为 ENTRYPOINT 的参数传递 | docker run 参数会覆盖 CMD 中的默认参数 |
至此,我们已经学到了dockerfile中最常用的命令,至于其他的都是作为特殊需求或者更定制化的dockerfile脚本去使用,而这篇文章中的命令基本可以作为企业中的dockerfile使用,当然,dockerfile的语法远远不值这么简单,那么更多的信息可以参考官方文档:docs.docker.com/reference/d…
\\n\\n","description":"It all starts with a Dockerfile. (docker万物始于此) 这是官网对Dockerfile的描述。\\n\\n系列教程\\n\\n在介绍Dockerfile之前,推荐小白去恶补一下作者之前发的文章,初学者选学,可以留个赞再走,求求了!!\\n\\n标题\\t链接SpringBoot应用:Docker与Kubernetes全栈实战秘籍\\tjuejin.cn/post/744068…\\nDocker入门之Windows安装Docker初体验\\tjuejin.cn/post/741565…\\n从零开始玩转…","guid":"https://juejin.cn/post/7494282115833249844","author":"掉头发的王富贵","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T02:30:55.980Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89e581d005564ca08df5c14771ffb3c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=ksmO0LzbdXt9U0Q7vJFNhwHUuQo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/36d4159844374feaba114c0997d7e5f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=GFK%2B3XwJ4sFWtHpJlH02yS%2FJ3yU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8963d21247843f4a5c55024ffcf1966~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=0s4EnvM8yKGt%2FK2Mu7oZ5oz5ydY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8ecba8d45233435f81b2fba0626af5b8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=ITNjf4iWQOd1lKSqgedSmKHCPRk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a6800feb11f468bae61e274c86cb43a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=WQMEsicE80HKhA12YcEzhcgnj64%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8f55a654bc284a949a89bbd5f95e9aab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6J5aS05Y-R55qE546L5a-M6LS1:q75.awebp?rk3s=f64ab15b&x-expires=1746153035&x-signature=fr5wMgpCGknlxSTFxCK%2FlmSMaZM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Docker","容器"],"attachments":null,"extra":null,"language":null},{"title":"京东外卖,探索「距离最近」排序背后的秘密","url":"https://juejin.cn/post/7494124948855078950","content":"介绍经纬度计算的几种公式及难点,阐述了 Mysql 5.7.6 + 和 Redis GEO 两种实现方法,最后鼓励读者分享其他方法。","description":"介绍经纬度计算的几种公式及难点,阐述了 Mysql 5.7.6 + 和 Redis GEO 两种实现方法,最后鼓励读者分享其他方法。","guid":"https://juejin.cn/post/7494124948855078950","author":"SimonKing","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T02:03:21.812Z","media":null,"categories":["后端","Redis","MySQL"],"attachments":null,"extra":null,"language":null},{"title":"不会还有人在传统网页定位修改,看我用一行JS代码让你的网页变为可编辑","url":"https://juejin.cn/post/7494098722932621347","content":"传统的网页定位修改方法虽然简单,但效率低下,只能逐个修改元素,且操作繁琐。相比之下,通过一行简单的 JavaScript 代码,可以将整个网页变为可编辑状态,极大地提高了效率和趣味性。","description":"传统的网页定位修改方法虽然简单,但效率低下,只能逐个修改元素,且操作繁琐。相比之下,通过一行简单的 JavaScript 代码,可以将整个网页变为可编辑状态,极大地提高了效率和趣味性。","guid":"https://juejin.cn/post/7494098722932621347","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-17T16:27:10.644Z","media":null,"categories":["后端","JavaScript"],"attachments":null,"extra":null,"language":null},{"title":"聊聊我的开源经历——先做个垃圾出来","url":"https://juejin.cn/post/7494096210740215848","content":"\\n
程序员中的很多人都是完美主义者,在工作对自己的要求是一丝不苟,不能出一丝一毫的错误,交付给领导的技术方案连个错别字都不能有,线上也不能有bug,无论是主动或被动,很多人都有在追求完美主义。这里面也包括我~
\\n大概一年前,我就有一个想法,做一个开源项目————订单中台系统,但是一直没有付诸行动,我给自己的解释是,我还没有想好如何设计,很多决策点困惑着我,一来二去拖了非常久的时间。
\\n直到去年过年,我有大把的时间,闲得无聊,我不想再等了,想不明白也要开搞,我决定:先搭建一套 SpringBoot应用,把常见的框架中间件先引入进来。
\\n我发现,当我抱着做完这件事,而不是把这件事做完美的想法去做事以后,事情有了很大的进展!
\\n引入MQ/DB/Redis/Mybatis/SpringCloud等等框架和中间件,把项目搭建好,仅用了一天半不到,剩下的半天我把项目里的工具类、基础组件写好,包括扩展点引擎和流程引擎。
\\n万事开头难,可以先从自己最熟悉最擅长的部分开始入手~
\\n扩展点引擎是我很早之前就想明白,同时在业界也是广泛采用的办法,它解决的痛点是交易系统中台要接入很多的业务方,每个业务方并不是完全相同。很多时候无法完全复用,需要改造系统适应新的业务。
\\n对于一个复杂的多业务并存的交易系统,新增业务代码时,务必要保证原有业务不受影响,如果没有插件扩展能力,就会充斥大量的 if else 。
\\n因此项目开发初期,我完成了插件扩展点引擎的开发,用了不到半天,一两百行代码,但是很关键!可以很好解决业务隔离性差和扩展难 的问题。
\\n更详细的想法在这里。\\n# 程序员的保命技能——流程编排,你一定要了解!
\\n我还花了一天的时间调研了流程引擎框架,LiteFlow,但是调研以后发现它的流程设计和我预想中不太一样,我期望的流程引擎执行时,每个节点类似于过滤器链条中的1个节点,当流程失败以后,执行各个节点的回滚方法,但是LiteFlow只能顺序的执行每个节点,不能回滚。因此我决定自己写一个流程引擎很简单的那种,花了大概不到半天,实际用起来发现很好用~ 这种代码设计风格也推荐给大家# 程序员的保命技能——流程编排,你一定要了解!
\\n不要等到百分百想明白再干,而是在干中想,干中学,慢慢就全明白了~
\\n开工以后项目经历了三次大的修改,其中最大的1次,我将设计好的数据库模型全部推翻,把之前写的代码全部删除重写一遍,重新梳理思路,重新设计。
\\n之前在设计订单系统时,我把交易下单部分和履约部分分拆成两个独立的模型,后来发现完全没有必要,履约只是订单交易系统的一个模块。下单、消单、履约、退款是在订单模型上驱动订单状态改变并执行其他业务动作。订单履约没有必要抽出和订单模型一对一的模型。当然这是有前提条件的,MemberClub目前的定位是虚拟订单系统,它的履约模块的业务复杂度相比实物订单配送履约系统,是简单不少的,所以没有必要单独抽离出履约单模型,反而抽出履约单模型,会增加系统的复杂度和理解难度。
\\n如无必要,勿增实体。
\\n我不认为被删除的代码是做了无用功,恰恰相反,我认为如果没有这次试错,我就算干想一万年,也可能想不明白这件事。经过这次修改以后,我脑海里不成熟的想法逐渐成熟。
\\n最后,如果欢迎掘友们加入我的开源项目 MemberClub,欢迎关注。
\\n它可实现一天时间内搭建一套订单交易系统。 轻量级完全开源的交易引擎,以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!
\\ngithub: github.com/juejin-wuya…
\\ngitee: gitee.com/juejinwuyan…
\\n就在前几天,IntelliJ IDEA 2025.1 正式发布了!
\\n这次更新的核心亮点包括:对 Java 24 的全面支持、Kotlin Notebooks 正式内置、Kotlin K2 模式成为默认、以及 JetBrains AI 的重大升级。此外,调试功能也得到增强,新增了对观察表达式(Watches)求值的暂停与恢复功能。
\\n下面,简单带大家看看这次更新。
\\nJetBrains AI 迎来了重大升级,将 AI Assistant 和 Junie 集成到一个统一的订阅计划中。在此次更新中,JetBrains AI 的所有功能在 IDE 中免费开放,其中部分功能如无限制的代码补全和本地模型支持可以无限使用,而其他功能则基于额度限制提供。
\\n此次更新带来了多项生产力提升和重复工作减少的改进。新功能包括更智能的代码补全、对新一代云模型(如即将推出的 OpenAI GPT-4.1、Claude 3.7 Sonnet 和 Gemini 2.0 Flash)的支持、基于 RAG 技术的高级上下文感知,以及支持直接从聊天窗口进行多文件编辑的新模式。
\\n从 2025.1 IDE 版本开始,在 IDE 的右上角菜单中找到 JetBrains AI 图标,然后单击即可开始使用 JetBrains AI Free。
\\nIntelliJ IDEA 2025.1 提供了对最新 Java 24 版本所有特性的完整支持。
\\nIntelliJ IDEA 2025.1 默认启用了 K2 模式,这标志着在提升 Kotlin 代码分析能力、内存效率和整体性能方面取得了重大进展。
\\n引入了基于稳定、标准兼容核心的重构版终端,并使用 IDE 编辑器渲染 UI,旨在提升跨平台(本地或远程)的兼容性、性能和未来功能扩展性。
\\n对于 Windows 和 Linux 用户,IDE 现在提供了一个新选项,将主菜单与主工具栏合并,从而创建更加简化的界面。
\\n现在可以在渲染后的 Markdown 预览窗口内直接搜索内容,快速定位关键信息。
\\n现在可以在调试期间暂停和恢复对单个观察表达式的求值。右键单击正在求值的 Watch,选择“Pause Watch”即可暂停,避免其计算可能产生的副作用或错误报告;选择“Resume Watch”则可恢复。
\\n调试时检查包含标记文本(如 XML)的值时,现在会以格式化形式显示,而不是冗长的纯字符串。
\\n从 Gradle 8.13 开始,可以像为项目配置 JVM 一样,使用原生工具链为 Gradle Daemon 定义精确的 JVM。IntelliJ IDEA 会与 Gradle 的配置保持同步,并在需要时允许 Gradle 自动下载所需的 JVM。可以在 Preferences/Settings | Build Tools | Gradle
中轻松管理这些设置,IDE 将与 Gradle 的配置完全一致。
导航到库文件时,IDE 会自动下载其源代码,无需手动操作,即时获得格式化源码和文档。
\\n可以直接在差异视图中查看提交详情。差异对话框会显示提交信息、作者、日期时间以及完整的提交哈希值。这使您能够更清晰地了解文件的历史记录,加速对代码修改的理解。
\\n可将任何自定义工具配置为运行配置,在提交前的检查阶段(与 IDE 内置检查、格式化并行)执行。
\\n当新增或修改 Git 远程仓库时,IntelliJ IDEA 会自动拉取最新的更改,从而确保您拥有最新的分支列表和提交历史,而无需手动执行拉取操作。新拉取的分支会立即出现在 Git 分支树中,可以直接开始使用,同时保证代码库始终保持最新状态。
\\n新增设置,允许指示 IDE 在执行提交操作时跳过运行 Git 钩子。
\\n不好啦❗ 天塌了❗ 又又又又来生产事故了❗
\\n最近公司发生了一起生产事故,WMS在库存扣减时产生了 重复扣减,事件报告中指出是因为针对重复MQ消息做幂等控制时,幂等控制方案失效,导致重复处理了两条退款消息,最终造成重复退款。
\\n失效的幂等控制方案其实很简单,就是基于数据库的 唯一索引 来进行幂等控制,这其实是很常用也很简单的一种实现方案,但为什么在这起生产事故中,唯一索引实现幂等就失效了呢,问题就出在这个唯一索引上。
\\n上游重复投递消息后,重复投递的消息有两个下游消费,两个下游都基于唯一索引做了幂等控制,但是结果就是下游-1幂等控制失败,另一个下游-2幂等控制成功。
\\n上游投递的消息有四个字段,记为field_1,field_2,field_3和field_4,其中下游-1的幂等控制表的创表语句如下。
\\nCREATE TABLE idempotent_1 (\\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\\n field_1 VARCHAR(255) NOT NULL,\\n field_2 VARCHAR(255) NOT NULL,\\n field_3 VARCHAR(255) DEFAULT NULL,\\n field_4 VARCHAR(255) NOT NULL,\\n UNIQUE INDEX idempotent_index(field_1, field_2, field_3)\\n)\\n
\\n下游-2的幂等控制表的创表语句如下
\\nCREATE TABLE idempotent_2 (\\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\\n field_1 VARCHAR(255) NOT NULL,\\n field_2 VARCHAR(255) NOT NULL,\\n field_3 VARCHAR(255) DEFAULT NULL,\\n field_4 VARCHAR(255) NOT NULL,\\n UNIQUE INDEX idempotent_index(field_2)\\n)\\n
\\n不知道你猜到没有,原因就是下游-1的唯一索引会失效。
\\n下游-1的唯一索引是一个复合索引,包含field_1,field_2和field_3,其中field_3允许为NULL,而在MySQL中,NULL表示 未知
,也就是前后两次插入数据时,如果field_3都是NULL,此时就算field_1和field_2完全一样,也是能够插入成功的,因此唯一索引的唯一约束就失效了,最终幂等控制也就失败了。
我们可以把idempotent_1表创建出来自行做一下测试,执行如下插入语句两次,是可以插入成功的。
\\nINSERT INTO idempotent_1 (field_1, field_2, field_3, field_4) VALUES (\'A\', \'B\', NULL, \'D\')\\n
\\n问题的解决很简单,为field_3添加NOT NULL约束,就能够规避因为存在NULL而导致的唯一索引失效,进而幂等控制就能成功。
\\n进一步的,其实建议所有字段都添加上NOT NULL约束,这样能够规避很多问题。
\\n情况是一个简单情况,就是对重复MQ消息基于唯一索引做幂等控制时,因为唯一索引是一个复合索引且存在字段允许为NULL,从而唯一索引失效,最终幂等控制失败。
\\n解决方案也十分简单,就是为所有字段都添加上NOT NULL约束,从而唯一索引就不会因为存在NULL而失效。
\\n那么最后思考一个问题,为什么MySQL允许字段可以为NULL呢,毕竟很多问题都是因为NULL的存在而出现的。
\\n个人认为最本质的原因就是NULL可以节约空间,比如一个字段是VARCHAR,当该字段允许为NULL且实际就是NULL时,是不会占用空间的,但如果该字段不允许为NULL,那么至少都会存储一个空字符串,而空字符串的占用空间包含两部分,即 长度信息
和 实际内容
,因为是空字符串,所以实际内容是0字节,但长度信息至少都会占用1字节,所以 节约空间
是NULL存在的意义。
\\n\\n程序员在工作中经常会有画流程图、时序图这类需求,尤其是在写文档的时候。如果有一款好用的画图工具,会极大地提高我们的效率。今天给大家分享一款Github官方支持的画图工具Mermaid,能让你像使用Markdown一样画流程图,希望对你有所帮助!
\\n
Mermaid是一款基于JS的画图工具,能通过解析Markdown语法来实现图表的动态渲染,目的在于让文档的更新跟得上开发进度,目前在Github上已有77k+star
。
Mermaid具有如下特性:
\\n下面是Mermaid使用过程中的效果图,还是挺炫酷的!
\\n\\n\\n使用Docker来安装Mermaid Live Editor还是非常方便的,我们将采用此种方式来安装。
\\n
docker pull ghcr.io/mermaid-js/mermaid-live-editor\\n
\\ndocker run -p 8000:8080 --name mermaid-live-editor \\\\\\n-d ghcr.io/mermaid-js/mermaid-live-editor\\n
\\nSample Diagrams
来查看生成图表的效果,访问地址:http://192.168.3.101:8000\\n\\n接下来我们就以
\\n《mall学习教程》
中的图表为例,来讲解下Mermaid的使用。
这里简单介绍下mall项目,mall项目是一套基于SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\n\\n\\n流程图(Flowchart)是用的比较多的图表,对于复杂的业务流程,使用流程图能让人比较好理解。这里以mall项目的
\\n下单流程
为例,来讲解下Mermaid的使用。
code
中添加如下代码;--\\ntitle: 生成确认单流程\\n---\\nflowchart TD\\n A[获取购物车信息并计算好优惠] --\x3e B(从ums_member_receive_address表中获取会员收货地址列表)\\n B --\x3e C(获取该会员所有优惠券信息)\\n C --\x3e D{{根据use_type判断每个优惠券是否可用}}\\n D --\x3e|0| E(全场通用)\\n E --\x3e H{{判断所有商品总金额是否满足使用起点金额}}\\n H --\x3e|否| I[得到用户不可用优惠券列表]\\n H --\x3e|是| J(得到用户可用优惠券列表)\\n J --\x3e K(获取用户积分)\\n K --\x3e L(获取积分使用规则)\\n L --\x3e M[计算总金额,活动优惠,应付金额]\\n D --\x3e|-1| F(指定分类)\\n F --\x3e N{{判断指定分类商品总金额是否满足使用起点金额}}\\n N --\x3e|否| I\\n N --\x3e|是| J\\n D --\x3e|2| G(指定商品)\\n G --\x3e O{{判断指定分类商品总金额是否满足使用起点金额}}\\n O --\x3e|否| I\\n O --\x3e|是| J\\n
\\nflowchart
代表流程图;TD
代表从上往下的结构;--\x3e
代表流程的箭头;[]
代表方框;()
代表圆角方框;{{}}
代表六边形;||
代表连线上的文字;\\n\\n时序图(Sequence Diagram),它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。我们在学习Oauth2的时候,第一步就是要搞懂Oauth2的流程,这时候有个时序图帮助可就大了。下面我们使用Mermaid来绘制Oauth2中使用授权码模式颁发令牌的时序图。
\\n
code
中添加如下代码;---\\ntitle: Oauth2令牌颁发之授权码模式\\n---\\nsequenceDiagram\\n autonumber\\n actor user as User\\n participant userAgent as User Agent\\n participant client as Client\\n participant login as Auth Login\\n participant server as Auth Server\\n user->>userAgent: 访问客户端\\n userAgent->>login:重定向到授权页面+clientId+redirectUrl\\n login->>server:用户名+密码+clientId+redirectUrl\\n server--\x3e>login:返回授权码\\n login--\x3e>userAgent:重定向到redirectUrl+授权码code\\n userAgent->>client:使用授权码code换取令牌\\n client->>server:授权码code+clientId+clientSecret\\n server--\x3e>client:颁发访问令牌accessToken+refreshToken\\n client--\x3e>userAgent:返回访问和刷新令牌\\n userAgent--\x3e>user:令牌颁发完成\\n
\\nsequenceDiagram
代表时序图;autonumber
代表给箭头自动添加序号;actor
代表人形参与者;participant
代表普通参与者;->>
代表实现箭头;--\x3e>
代表虚线箭头。\\n\\n类图(Class Diagram)可以表示类的静态结构,比如类中包含的属性和方法,还有类的继承结构。下面我们用Mermaid来画个类图。
\\n
code
中添加如下代码;classDiagram\\n Person <|-- Student\\n Person <|-- Teacher\\n class Person {\\n # String name\\n # Integer age\\n + void move()\\n + void say()\\n }\\n class Student {\\n - String studentNo\\n + void study()\\n }\\n class Teacher {\\n - String teacherNo\\n + void teach()\\n }\\n
\\nclassDiagram
代表类图;class
代表类;<|--
代表继承结构;#
代表protect,+
代表public,-
代表private。思维导图(Mindmap),是表达发散性思维的有效图形工具,它简单却又很有效,是一种实用性的思维工具。这里以mall项目的技术栈、核心功能、部署上线为例来讲解下Mermaid的使用。
\\ncode
中添加如下代码;mindmap\\n root[电商实战项目]\\n mall[mall项目]\\n stack1[技术栈]\\n Spring Boot\\n MyBatis\\n MySQL\\n Redis\\n Elasticsearch\\n MongoDB\\n RabbitMQ\\n MinIO\\n business1[核心功能]\\n 商品模块\\n 订单模块\\n 营销模块\\n 权限模块\\n deploy1[部署上线]\\n Linux\\n Docker\\n Jenkins\\n mall-swarm[mall-swarm微服务项目]\\n stack2[技术栈]\\n Spring Cloud核心组件\\n Spring Cloud Alibaba核心组件\\n Sa-Token\\n business2[核心功能]\\n 从购物车到下单到支付的完整订单流程\\n 一套通用的微服务项目脚手架\\n deploy2[部署上线]\\n Kubernetes\\n KubeSphere\\n
\\nmindmap
代表思维导图[]
代表方框\\n\\n之前我们使用的是Mermaid的默认主题,其实Mermaid一个支持5种主题:default、neutral、dark、forest、base。
\\n
config
面板,修改下theme
属性就能切换主题了;Mermaid能让我们用Markdown来画流程图,使用起来确实很优雅,而且支持的图表种类也很多,感兴趣的小伙伴可以尝试下!
\\n我们经常会把MySQL 中的数据同步到 ES 中,他们之间的类型的映射关系如下:
\\nMySQL 类型 | Elasticsearch 类型 | 说明 |
---|---|---|
VARCHAR | text, keyword | 根据是否需要全文搜索或精确搜索选择使用 text 或 keyword。 |
CHAR | keyword | 通常映射为 keyword,因为它们用于存储较短的、不经常变化的字符序列。 |
BLOB/TEXT | text | 大文本块使用 text 类型,支持全文检索。 |
INT, BIGINT | long | 大多数整数类型映射为 long,以支持更大的数值。 |
TINYINT | byte | 较小的整数可以映射为 byte 类型。 |
DECIMAL, FLOAT, DOUBLE | double, float | 根据精确度需求选择 double 或 float。 |
DATE, DATETIME, TIMESTAMP | date | 所有的日期时间类型均可映射为 date。 |
TINYINT(1) | boolean |
text 类型被设计用于全文搜索。这意味着当文本被存储为 text 类型时,Elasticsearch 会对其进行分词,把文本分解成单独的词或短语,便于搜索引擎进行全文搜索。因为 text 字段经过分词,它不适合用于排序或聚合查询。
\\nkeyword 类型用于精确值匹配,不进行分词处理。这意味着存储在 keyword 字段的文本会被当作一个完整不可分割的单元进行处理。因为 keyword 类型字段是作为整体存储,它们非常适合用于聚合(如计数、求和、过滤唯一值等)和排序操作。由于不进行分词,keyword 类型字段不支持全文搜索,但可以进行精确匹配查询
\\n通过上文我们知道,ES 不支持 decimal 类型的,只有 double、float 等类型,那么,MySQL 中的 decimal 类型,同步到 ES 之后,如何避免丢失精度呢?
\\nprice DECIMAL(10, 2)\\n
\\n如以上 price 字段,在 es 中如何表示呢?有以下几种方式:
\\n将 decimal 数据作为字符串类型存储在 Elasticsearch 中。这种方式可以保证数字的精度不会丢失,因为字符串会保留数字的原始表示形式。
\\n{\\n \\"properties\\": {\\n \\"price\\": {\\n \\"type\\": \\"keyword\\"\\n }\\n }\\n}\\n
\\n虽然 double 类型在理论上可能会有精度损失,但实际上 double 类型提供的精度对于许多业务需求已经足够使用。如果决定使用这种方法,可以在数据迁移或同步时适当扩大数值范围以尽量减小精度损失。
\\n{\\n \\"properties\\": {\\n \\"amount\\": {\\n \\"type\\": \\"double\\"\\n }\\n }\\n}\\n
\\nElasticsearch 的 scaled_float 类型是一种数值数据类型,专门用于存储浮点数。其特点是通过一个缩放因子(scaling factor)将浮点数转换为整数来存储,从而在一定范围内提高存储和计算的效率。
\\n他使用一个缩放因子将浮点数转换为整数存储。例如,如果缩放因子是 100,那么值 123.45 会存储为 12345。这样可以避免浮点数存储和计算中的精度问题。
\\n{\\n \\"mappings\\": {\\n \\"properties\\": {\\n \\"price\\": {\\n \\"type\\": \\"scaled_float\\",\\n \\"scaling_factor\\": 100\\n }\\n }\\n }\\n}\\n
\\n在某些情况下,可以将 decimal 数值拆分为两个字段存储:一个为整数部分,另一个为小数部分。这样做可以在不丢失精度的情况下,将数值分开处理。
\\n{\\n \\"properties\\": {\\n \\"total_price_yuan\\": {\\n \\"type\\": \\"integer\\"\\n },\\n \\"total_price_cents\\": {\\n \\"type\\": \\"integer\\"\\n }\\n }\\n}\\n
\\n在查询时,可以使用 Elasticsearch 的脚本功能(如 Painless 脚本)来处理数值计算,确保在处理过程中控制精度。
\\n2025年4月16日,天气阴,8点30刚出健身房,心里默念又是充满希望的一天✨
\\n打开钉钉,一条@消息映入眼帘😱,感谢领导昨天晚上没有call🗽
\\n昨天晚上 8点半,生产环境SQL
占用cpu
过高 🧨
🏁上面的SQL
数据最多的表为3W
条,其他表都是几千的数据量。为啥会导致cpu
标高❓
\\n\\n执行了几次执行时间在20s左右,两三个人同时执行,直接卡死
\\n
看到这个问题,我基本上已经定位到问题了。估计是 人大金仓
的 执行计划 和 服务器的 CPU
以及磁盘
的性能存在问题!因为这个SQL 迁移之前也没问题的。
\\n\\n前两天也出现过
\\nSQL
超时的问题,还写了一篇文章: 小小的改动,竟然效率提高了1000倍
\\n这次的问题 按照道理 应该也是一样,因为join了多张表,同时SQL的执行计划走的是Loop join导致消耗了很多cpu
资源 。
\\n💀联系了研发经理之后,用工具测试了一下性能,新环境的 性能 粗略计算的性能只有原来环境的三分之一(但是配置比以前环境的都高)
仅仅是服务器原因同样SQL执行时间不可能存在1000 倍的差距。关键还是数据库本身的优化器有关系。连表查询kingbase
可能走 Nested Loop
进行连表,有时候不会走 hash join
. 走 Nested Loop
加上join
的表有好几个,因此导致导致消耗掉了大量的cup
性能.
✅看一下上面SQL的执行计划吧三层nested loop
loop中的遍历次数粗略计算 10000 * 100 * 30000 次
看了两个官网的说明一个是mysql 官方文档,一个是postgrel sql 的官方问题。(人大金仓内核也是通过 postgrel sql 改的)
\\nmysql:MySQL 8.0.18 及以后的版本❣尽可能的用 hash join
,并且 8.0.20 开始禁用 block nested loop
\\n官方文档:dev.mysql.com/doc/refman/…
pgsql: 当连表如果查询使用的关系少于 geqo_threshold(默认12),会寻找❣最优的方式执行(nested loop、merge join、hash join
);官方文档:www.postgresql.org/docs/curren…
kingbase: 连表查询同pgsql
三种方式,官网上说了nested loop ❣适用于:内外表数据量不大的情况 或者内表数据量很小,外表数据量大的情况;官方文档:bbs.kingbase.com.cn/docHtml?rec…
\\n\\n是不是人大金仓的官方文档是根据
\\npgsql
写的读后感哦🤣 ,数据量小?多少算小,多少算大?
✅看了mysql
和pgsql
的官方文档,至少来说很少走nested loop
去实现连表,如果走 nested loop
正常来说效率也挺高的。
那么kingbase
的实现方式 和 pgsql
底层实现一样的话,也很少会走nested loop
循环。 kingbase
说的内外表数据量不大的情况适用于 nested loop
,大小也没明确说明,再加上之前也遇到过一次loop
导致SQL慢查询的问题,因为一个类型转换影响了 kingbase
的执行效率 。
kingbase
优化做出错误的抉择,从而走nested loop类型转换导致使用nested loop,这个问题就是条件中存在列类型转换导致 使用loop
,改正确的类型之后,就是走的hash join
了。
按照我们的假设,我把【前言】报错的SQL 所有条件去掉。然后真的变成hash join
,然后再一个条件一个条件加上最后加到这个条件ut.id IS NULL
的时候,就变成nested loop
循环了。看看不加 这个条件的计划吧
🚀优化之后的时间就来到了700ms
应该只有kingbase容易出现这个问题吧💔
\\n既然问题已经定位到了,那么就很容易解决这个问题了
\\nut.id is null
这个条件改成内存过滤SQL
再封装一层(不用join用子查询也行),影响它的执行计划,修改如下:select * (select ...IFNULL(ut.\\"id\\" , -1) AS cid from...) where cid = -1
hint
语法指定走hash join
实现,修改如下(部分数据库支持,需要开启配置):SELECT /*+Hashjoin( sr ut su uo,syr)*/ su.\\"id\\",.............
Nested Loop
如此消耗性能什么是Nested loop
?
翻译过来就是嵌套循环,如下MySql官网的距离说明
\\nNested loop
时间复杂度
\\n知道嵌套循环之后,我们就可以大概估算出,在最极端的情况3表join的时间复杂度 为 O(r1 * r2 * r3)
,假设数量级在w的级别,10 * 1000 * 10000 = 1亿
,可以看到数量不多的情况 使用Nested Loop
也会出现上亿
次的计算。
所以Nested loop
比较消耗cpu
资源的;推荐阅读::mysql 官网nested-loop
\\n\\n✅时间复杂度对比:假设三个表(数据量为100、1000、10000)
\\njoin
查询,走nested loop
的时间复杂度为O(100100010000),那么hash join
的复杂度O(100+1000+1000)
\\nps: 当然上面这个计算方式是简单粗暴的理解。\\n虽然我用的是kingbase
,但是我有时候看一些文档,还是会去mysql、pg-sql
看的,底层的这些核心算法逻辑,还是相通的。
有些场景 nested loop 还是有优势的:
\\n小数据集情况:\\n当参与连接的表数据量都很小时,Nested Loop Join 可能比 Hash Join 更高效。因为 Hash Join 需要构建哈希表,这个过程存在一定的开销,像创建哈希表、分配内存等。而对于小数据集,Nested Loop Join 的简单嵌套循环操作开销更小,能更快完成连接。比如,表 A 有 10 条记录,表 B 有 20 条记录,使用 Nested Loop Join 进行简单的遍历比较,消耗的时间和资源会比构建哈希表要少。
\\n存在合适索引的情况:\\n若连接列上有高效的索引,Nested Loop Join 可以利用索引快速定位匹配的记录,减少不必要的比较操作。例如,表 A 是订单表,表 B 是客户表,连接列是客户 ID,且客户 ID 在表 B 上有索引。当执行连接操作时,Nested Loop Join 可以根据表 A 中的客户 ID,通过索引快速在表 B 中找到匹配的记录,这种情况下性能可能会优于 Hash Join。
\\n连接条件复杂的情况:\\n对于一些复杂的连接条件,尤其是包含范围查询、函数调用等,索引可以帮助 Nested Loop Join 筛选出部分符合条件的记录,减少需要扫描的数据量。而 Hash Join 通常更适合简单的等值连接,对于复杂连接条件处理起来可能不如\\nNested Loop Join 灵活。
\\n当然上面这些条件,也不是说绝对的就用 nested loop
. 还是各个数据库的优化器有关系,优化器通常会选择最优的一种方式执行。直到我遇到了kingbase
,以后连表 数据量超过 1w
的都得 explain
了🤑
Nested loop
循环问题hash join 算法 (mysql)
\\n✔MySQL(8.0.18 及更高版本)会尽可能的去使用hash 算法进行join。
\\n✔MySQL 8.0.20 block nested loop
已经被移除,所以mysql 高版本使用loop
循环的方式就更少。
\\n\\n所以 mysql 的高版本对于 多表join,性能还是比较好的。但是数据量大的话,join表多话,就需要关注性能问题了。文章推荐:在sql 中谨慎使用多表join
\\n
merge join 、hash join (pgsql、kinbase)
\\npgsql 对于join 来说,就有三种选择 nested loop join
、merge join
、hash join
,当连表如果查询使用的关系少于 geqo_threshold,会寻找最优的方式执行。
修改配置参数 比如geqo_threshold
连表的阈值,或者关闭 nested loop
查询。(部分数据库)
explain
分析查询是否使用loop
,对where
条件进行排查,优化成hash join
JOIN
的需要。SELECT /*+Hashjoin( sr ut su uo,syr)*/ su.\\"id\\",.............
)本篇文章,分析了join
查询在数据库中实现的核心算法nested loop
和 hash join
这两者之间的区别。以及分享了在kingbase
中where
条件问题导致,join 查询走了nested loop
方式,导致CPU报警的案例。通过本篇文章,让我们了解join
查询潜在风险,以及如何解决nested loop
导致CPU飚高的的问题。
所以啊各位,连表查询的时候小心咯!可能换了一个数据版本环境就GG了💥
\\n当然mysql 和 pgsql 应该问题不大的,国产库的话 升级环境就小心了💥
\\n别以为,你的数据就几千,几万条,连起表来也能把你服务器干蹦☢
🙈kingbase 官方说明:\\n
RocketMQ主要由Producer、Broker和Consumer三部分组成,如下图所示:
\\n\\n\\nRocketMQ中有这样几个角色:NameServer、Broker、Producer和Consumer
\\n
NameServer:NameServer是RocketMQ的路由和寻址中心,它维护了Broker和Topic的路由信息,提供了Producer和Consumer与正确的Broker建立连接的能力。NameServer还负责监控Broker的状态,并提供自动发现和故障恢复的功能。
\\nBroker:Broker是RocketMQ的核心组件,负责存储、传输和路由消息。它接收Producer发送的消息,并将其存储在内部存储中。并且还负责处理Consumer的订阅请求,将消息推送给订阅了相应Topic的Consumer。
\\nProducer(消息生产者):Producer是消息的生产者,用于将消息发送到RocketMQ系统。
\\nConsumer(消息消费者):Consumer是消息的消费者,用于从RocketMQ系统中订阅和消费消息。
\\nRocketMQ的工作过程大致如下:
\\n1、启动NameServer,他会等待Broker、Producer以及Consumer的链接。
\\n2、启动Broker,会和NameServer建立连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Borker与Topic的映射关系。
\\n3、启动Producer,启动时先随机和NameServer集群中的一台建立长连接,并从NameServer中获取当前发送的Topic所在的所有Broker的地址;然后从队列列表中轮询选择一个队列,与队列所在的Broker建立长连接,进行消息的发送。
\\n4、Broker接收Producer发送的消息,当配置为同步复制时,master需要先将消息复制到slave节点,然后再返回“写成功状态”响应给生产者;当配置为同步刷盘时,则还需要将消息写入磁盘中,再返回“写成功状态”;要是配置的是异步刷盘和异步复制,则消息只要发送到master节点,就直接返回“写成功”状态。
\\n5、启动Consumer,过程和Producer类似,先随机和一台NameServer建立连接,获取订阅信息,然后在和需要订阅的Broker建立连接,获取消息。
\\nRocketMQ的消息想要确保不丢失,需要生产者、消费者以及Broker的共同努力,缺一不可。
\\n首先在生产者端,消息的发送分为同步和异步两种,在同步发送消息的情况下,消息的发送会同步阻塞等待Broker返回结果,在Broker确认收到消息之后,生产者才会拿到SendResult。如果这个过程中发生异常,那么就说明消息发送可能失败了,就需要生产者进行重新发送消息。
\\n但是Broker其实并不会立即把消息存储到磁盘上,而是先存储到内存中,内存存储成功之后,就返回给确认结果给生产者了。然后再通过异步刷盘的方式将内存中的数据存储到磁盘上。但是这个过程中,如果机器挂了,那么就可能会导致数据丢失。
\\n如果想要保证消息不丢失,可以将消息保存机制修改为同步刷盘,这样,Broker会在同步请求中把数据保存在磁盘上,确保保存成功后再返回确认结果给生产者。
\\n## 默认情况为 ASYNC_FLUSH flushDiskType = SYNC_FLUSH\\n
\\n除了同步发送消息,还有异步发送,异步发送的话就需要生产者重写SendCallback的onSuccess和onException方法,用于给Broker进行回调。在方法中实现消息的确认或者重新发送。
\\n为了保证消息不丢失,RocketMQ肯定要通过集群方式进行部署,Broker 通常采用一主多从部署方式,并且采用主从同步的方式做数据复制。
\\n当主Broker宕机时,从Broker会接管主Broker的工作,保证消息不丢失。同时,RocketMQ的Broker还可以配置多个实例,消息会在多个Broker之间进行冗余备份,从而保证数据的可靠性。
\\n默认方式下,Broker在接收消息后,写入 master 成功,就可以返回确认响应给生产者了,接着消息将会异步复制到 slave 节点。但是如果这个过程中,Master的磁盘损坏了。那就会导致数据丢失了。
\\n如果想要解决这个问题,可以配置同步复制的方式,即Master在将数据同步到Slave节点后,再返回给生产者确认结果。
\\n## 默认为 ASYNC_MASTERbrokerRole=SYNC_MASTER\\n
\\n在消费者端,需要确保在消息拉取并消费成功之后再给Broker返回ACK,就可以保证消息不丢失了,如果这个过程中Broker一直没收到ACK,那么就可以重试。
\\n所以,在消费者的代码中,一定要在业务逻辑的最后一步
\\nreturn ConsumeConcurrentlyStatus.CONSUME_SUCCESS;\\n
\\n当然,也可以先把数据保存在数据库中,就返回,然后自己再慢慢处理。
\\n但是,需要注意的是RocketMQ和Kafka一样,只能最大限度的保证消息不丢失,但是没办法做到100%保证不丢失。原理类似:
\\n3种,分别是单Master模式、多Master模式以及多Master多Slave模式。
\\n单Master集群,这是一种最简单的集群方式,只包含一个Master节点和若干个Slave节点。所有的写入操作都由Master节点负责处理,Slave节点主要用于提供读取服务。当Master节点宕机时,集群将无法继续工作。
\\n多Master集群:这种集群方式包含多个Master节点,不部署Slave节点。这种方式的优点是配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;缺点是单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
\\n多Master多Slave集群:这种集群方式包含多个Master节点和多个Slave节点。每个Master节点都可以处理写入操作,并且有自己的一组Slave节点。当其中一个Master节点宕机时,消费者仍然可以从Slave消费。优点是数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;缺点是性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。
\\n\\n\\n网站:Suno
\\n号称:被许多用户称为“最强音乐类AI”
\\n博主评价:早在去年1月,我就已经开始使用过了,从小就有一个音乐梦,奈何五音不全,现在用这个来进行创作音乐,有想AI创造音乐的可以试试
\\n推荐指数:🌟🌟🌟🌟🌟(5星)
\\n难度指数:需要梯子🪜
\\n
🌟嗨,我是LucianaiB!
\\n🌍 总有人间一两风,填我十万八千梦。
\\n🚀 路漫漫其修远兮,吾将上下而求索。
\\n先来欣赏一下我用它创作的歌曲吧,这可是我仅用 2 分钟就完成的,是不是感觉非常不错呢?
\\n\\n链接问题,需要点击去听。
\\nSuno AI是一款强大的AI音乐创作平台,能够根据用户输入的歌词和风格提示词,快速生成高质量的音乐作品。它通过深度学习技术,将文本提示转化为完整的歌曲,包括旋律、人声和乐器伴奏。其主要特点包括:
\\nSuno AI通过其强大的AI技术,极大地降低了音乐创作的门槛,让每个人都能轻松创作出属于自己的音乐作品。
\\n我们先点击网站进行登录:Suno ,登录后点击左侧的Create
\\n我们在进入后可以修改名字,以及选择模型和创作类型。
\\n在这里有4种模型提供(我选择使用v4以及自定义创作类型进行实验),下面是4种模型即介绍:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nv4 | Newest, with best vocal quality and song structures, max 4 mins | 最新的,最好的音质和歌曲结构,最多4分钟 |
---|---|---|
v3.5 | Upgraded with better arrangements and creativity, max 4 mins | 升级与更好的安排和创意,最多4分钟 |
v3 | Older with improved sound, max 2 mins | 声音更老,最多2分钟 |
v2 | Vintage, max 1.3 mins | 经典,最多1.3分钟 |
由于没有创作歌曲的水平,直接点击Full Song,让AI给我们创作一个,比如,我以 2018 年李荣浩的《年少有为》为模仿对象,输入了以下作曲提示词和风格提示词:
\\n下面是我的作曲提示词:
\\n\\n\\n怀旧与现代的交融,抒情摇滚风格,讲述青春遗憾与成长的故事,用温暖的旋律和细腻的歌词表达对过去的怀念和对未来的期待。
\\n
下面是我的风格提示词:
\\n\\n\\n复古抒情摇滚,温暖吉他旋律,深情男声,青春遗憾与成长的叙事,都市感与怀旧交织,钢琴与弦乐铺陈,节奏舒缓而富有张力,歌词诗意且直击人心
\\n
在初始操作结束后,点击“Create”开始创作,一首属于你的音乐作品就诞生了。
\\n下面就是我的音乐了
\\n\\n链接问题,需要点击去听。
\\n\\n歌词如下:
\\n[Verse]\\n时间倒退\\n青涩的我们相对\\n牵手一起走回家\\n一起吃饭聊梦想\\n\\n[Verse]\\n后来我们\\n相遇在人生岔路\\n故事慢慢地展开\\n遇见不同的女孩\\n\\n[Chorus]\\n你告诉我\\n青涩不再 变得成熟\\n生活起伏 充满惊喜\\n已不再是当初模样\\n都已长大\\n\\n[Verse]\\n十年之后\\n结婚生子事业成功\\n以为曾经是朋友\\n却发现最是陌生\\n\\n[Verse]\\n一晃多年\\n青春与我们擦肩\\n都各自有了归属\\n再也不会去赌注\\n\\n[Chorus]\\n我告诉你\\n青涩不再 变得成熟\\n生活起伏 充满惊喜\\n已不再是当初模样\\n都已长大\\n
\\n在主页的Home种我们可以看到其他人创造的歌曲,对于听惯了大众的音乐,可以来这里听听AI有趣的创造思路音乐。
\\n1.独特的音乐体验
\\n对于听惯了大众流行音乐的用户来说,Suno 提供了一个全新的音乐体验。AI 创作的音乐往往具有独特的思路和风格,能够带来与众不同的听觉享受。
\\n2.个性化创作
\\n用户可以根据自己的喜好和需求,选择不同的音乐风格、节奏和情感表达,让 AI 生成符合自己期望的音乐作品。这种个性化创作方式为音乐爱好者提供了无限的可能性。
\\n3.社区互动
\\nSuno 的社区功能允许用户分享自己的创作,并与其他创作者交流。这种互动不仅能够激发更多创意,还能让用户从其他优秀作品中获得灵感。
\\n4.探索新音乐
\\nSuno 的平台汇聚了来自全球用户的音乐创作,用户可以在这里发现许多新颖的音乐风格和创意。无论是寻找灵感还是单纯享受音乐,Suno 都是一个值得探索的地方。
\\nSuno 是一款极具创新性的 AI 音乐创作平台,以其强大的功能和便捷的操作被众多用户誉为“最强音乐类 AI”。它通过深度学习技术,能够根据用户输入的歌词、风格提示词等信息,快速生成高质量的音乐作品,涵盖旋律、人声和伴奏。这种智能化的创作方式极大地降低了音乐创作的门槛,让即使没有专业音乐背景的用户也能轻松创作出属于自己的音乐。
\\nSuno 提供了丰富的音乐风格选择,从流行、摇滚到电子、古典、嘻哈等,几乎涵盖了所有常见的音乐类型。用户可以根据自己的喜好和创作需求,选择不同的风格进行创作。此外,平台还支持纯音乐创作模式,适合制作背景音乐,进一步拓展了其应用场景。
\\n在使用体验上,Suno 的界面简洁直观,操作流程简单易懂。用户登录后,点击“Create”即可进入创作页面。在这里,用户可以修改歌曲名称、选择模型和创作类型,并输入歌词和风格提示词。平台提供了多种模型供用户选择,每种模型都有其独特的特点和优势,例如最新的 v4 模型具有最佳的音质和歌曲结构,适合创作时长较长的作品,而 v2 模型则更经典,适合快速生成短小精悍的音乐片段。
\\nSuno 的社区功能也是其一大亮点。用户可以在主页的“Home”页面浏览其他人创作的歌曲,这些由 AI 生成的音乐往往具有独特的思路和风格,为用户提供了全新的音乐体验。此外,社区还允许用户分享自己的创作,并与其他创作者交流,这种互动不仅能够激发更多创意,还能让用户从其他优秀作品中获得灵感。
\\n然而,Suno 也有一些局限性。例如,目前使用该平台可能需要借助网络工具(俗称“梯子”),这在一定程度上限制了部分用户的使用。尽管如此,Suno 依然凭借其强大的功能和创新的体验,吸引了大量用户的关注和使用。对于那些渴望创作音乐但又缺乏专业技能的用户来说,Suno 无疑是一个值得尝试的平台。它不仅能够帮助用户实现音乐梦想,还能在音乐创作的道路上提供无限的可能性。
\\n\\n\\n嗨,我是LucianaiB。如果你觉得我的分享有价值,不妨通过以下方式表达你的支持:👍 点赞来表达你的喜爱,📁 关注以获取我的最新消息,💬 评论与我交流你的见解。我会继续努力,为你带来更多精彩和实用的内容。
\\n
点击这里👉LucianaiB ,获取最新动态,⚡️ 让信息传递更加迅速。
","description":"网站:Suno 号称:被许多用户称为“最强音乐类AI”\\n\\n博主评价:早在去年1月,我就已经开始使用过了,从小就有一个音乐梦,奈何五音不全,现在用这个来进行创作音乐,有想AI创造音乐的可以试试\\n\\n推荐指数:🌟🌟🌟🌟🌟(5星)\\n\\n难度指数:需要梯子🪜\\n\\n强大的AI网站推荐(第五集)—— Suno\\n\\n🌟嗨,我是LucianaiB!\\n\\n🌍 总有人间一两风,填我十万八千梦。\\n\\n🚀 路漫漫其修远兮,吾将上下而求索。\\n\\n欣赏音乐\\n\\n先来欣赏一下我用它创作的歌曲吧,这可是我仅用 2 分钟就完成的,是不是感觉非常不错呢?\\n\\nsuno.com/song/78a86a…","guid":"https://juejin.cn/post/7493354111473532968","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T17:15:32.173Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a57df3a81e2a44f6ba3a76d7d5f92ec1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=NoBaGBXlRnztCBU5brVcyanT508%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab9221deabc94d0ab9db1540db2daf88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=CL%2By8yIrP2Bw60qZ1CzucfzoJvg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ef4dc5516b3141399bb6b6fe62054f6a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=10qBsHc3sYy7SHcmpBSmJIitr3A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/544adbfbf934409086dcb175eb0ac08a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=ZXeQNGffqqaKdY9rG%2BD7ihXb31k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b375b433dff34bf392f7b52def8c42a4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=SYoZGIU2vXp%2B0DUqw5IaL9rmgxg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b93676f41f44ed89fd7065470f0cd39~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=ddjxB8Ayp4qK%2FUBIWBMCea8Qbhw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f18fb5369bc4d35ba1e858d23fd1762~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1745367606&x-signature=d0w96wN%2BwELL4KAwJiLvu8CKAkc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","OpenAI"],"attachments":null,"extra":null,"language":null},{"title":"性能比拼: Node.js vs Go","url":"https://juejin.cn/post/7493420166877937714","content":"本内容是对知名性能评测博主 Anton Putra Node.js vs Go (Golang): Performance (Latency - Throughput - Saturation - Availability) 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准
\\n在本篇内容中,我们将比较 Node.js 和 Golang。我会使用标准库在 Golang 中创建一个 Web 应用程序;而在 Node.js 中,我会使用非常常见的 Web 框架 ---
Express。
为了运行这些测试,我将两个应用程序都部署到了 AWS 上一个面向生产环境的 Kubernetes 集群中,并使用最新一代的 EC2 实例作为 Kubernetes 节点。
\\n我们首先会测量 CPU 使用率、内存使用率、应用程序可用性、Kubernetes 的 CPU 限流情况以及网络压力。我们还会追踪每个应用程序接收和发送的字节数。这一数值可能会有很大差异,取决于请求头的数量以及应用程序是否启用了 KeepAlive(KeepAlive 允许一个 TCP 连接处理多个 HTTP 请求)。
\\n在第二个测试中,我引入了持久化层,因为大多数应用程序都需要以某种方式存储状态。例如,一个博客网站需要存储文章,一个电商网站需要存储库存,甚至一个待办事项列表也需要存储任务。
\\n最常见的方法之一是使用数据库。在本次基准测试中,我们使用的是 PostgreSQL 关系型数据库。
\\n除了前面提到的指标外,我们还会测量每个应用程序将数据插入数据库所需的延迟,并追踪连接池的状态以及每个应用程序如何扩展连接池。
\\n作为一名 DevOps 工程师,我依赖开发者的输入来在生产环境中运行和优化应用程序。我欢迎任何改进应用的建议,甚至更好的是 Pull Request。你可以在视频描述中找到我 GitHub 仓库的链接。
\\n好了,现在我来部署这两个应用程序到 Kubernetes。你会注意到,Node.js 使用的 CPU 稍微多一些,而内存使用基本相同。好了,现在开始第一次性能测试。
\\n我使用了一个 Kubernetes Job 和 20 个副本来为每个应用程序生成负载。整个测试可能持续了三到四个小时,但我会将其压缩为几分钟展示。我从每个 Pod 启动一个客户端开始,然后不断增加客户端数量,直到两个应用程序都开始失败。
\\n另外,每个阶段之间我设置了 60 秒的间隔,客户端超时时间设置为 1 秒。当达到超时阈值时,你会在可用性图表中看到下降。
\\n从一开始你就可以注意到,Node.js 使用更多的 CPU 来处理请求,且延迟明显高于 Go。我认为它的性能与我在之前视频中测试过的 Python Django 框架类似。除此之外,Node.js 使用了更多的内存,它默认还会发送更多的头信息,这也是你会看到它传输的数据更多的原因。
\\n当我们达到每秒 9,000 个请求时,Node.js 的 CPU 使用率达到 60%,并陷入卡顿状态。如果你知道 Node.js 出现这种情况的原因,或者以前遇到过这种问题,请告诉我。在接下来的测试中,Node.js 的 CPU 使用率将维持在 60% 左右,而请求的延迟会持续增加。我知道你可以使用 cluster 模式,但我个人认为直接增加副本数量对 Node.js 更合适。
\\n由于我使用的是与之前测试完全相同的设置,我们知道 Golang 可以处理大约 70,000 到 80,000 个请求。由于 Golang 的标准库没有任何速率限制机制,它会持续缓存所有请求,直到内存使用率达到 100%,然后被 Kubernetes 的 OOM(内存溢出)机制杀死。
\\n在测试结束时,Node.js 也开始性能下降,很多请求开始超时,你可以在可用性图表中看到这一点。顺便说一句,我在 Node.js 中使用了 async 函数,并设置了 NODE_ENV=production
。
测试结果:Golang 可以处理大约 70,000 个请求,而 Node.js 只能处理大约 9,000 个请求。
\\n现在让我打开整个测试周期中的每个图表:
\\nNode.js 开始变慢前的延迟图;
\\n好了,这就是第一个测试的全部内容。如果你有任何改进测试的建议,请告诉我。
\\n现在我们进行第二个测试。在本测试中,我们向每个应用程序发送一个包含 JSON 负载的 POST 请求。应用程序会为设备生成一个 UUID,然后将其保存到数据库中。我为两个应用程序都设置了最大连接池大小为 20。
\\n我使用一个开源的 Postgres Docker 镜像来运行一些数据库迁移操作。例如,我会创建一个用户,清除该用户的所有空闲连接,并创建一个表。然后我使用 init 容器在每次应用程序部署时运行这些迁移脚本。
\\n现在开始测试。我为 Node.js 使用了最快的 Postgres 驱动之一,因此在测试初期,插入数据的延迟基本相同。整体延迟也非常接近。但当然,CPU 和内存使用差异很大。
\\n你还可以注意到,连接池很快就达到了最大连接数 ---
每个应用程序 20 个连接。
当请求量达到每秒约 4,000 时,你会再次看到 Node.js 的 CPU 使用率达到 60%,并开始性能下降,延迟上升。看起来这就是 Node.js 在本测试中的最大处理能力。
\\n继续测试,直到 Golang 开始失败。当请求量达到每秒约 7,000 时,你可以看到它开始卡顿,只有内存使用率继续上升。如果我们继续测试,它也会达到内存限制,并被 Kubernetes 杀死。
\\n好了,现在让我打开整个测试周期中的每个图表:
\\n和Kafka只支持同一个Partition内消息的顺序性一样,RocketMQ中也提供了基于队列(分区)的顺序消费。即同一个队列内的消息可以做到有序,但是不同队列内的消息是无序的!
\\n当我们作为MQ的生产者需要发送顺序消息时,需要在send方法中,传入一个MessageQueueSelector。
\\nMessageQueueSelector中需要实现一个select方法,这个方法就是用来定义要把消息发送到哪个MessageQueue的,通常可以使用取模法进行路由:
\\nSendResult sendResult = producer.send(\\n msg,\\n new MessageQueueSelector() {\\n @Override\\n // mqs:该Topic下所有可选的MessageQueue\\n // msg:待发送的消息\\n // arg:发送消息时传递的参数\\n public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg{\\n Integer id = (Integer) arg;\\n //根据参数,计算出一个要接收消息的MessageQueue的下标\\n int index = id % mqs.size();\\n //返回这个MessageQueue\\n return mqs.get(index);\\n }\\n },\\n orderId);\\n
\\n通过以上形式就可以将需要有序的消息发送到同一个队列中。需要注意的时候,这里需要使用同步发送的方式!
\\n消息按照顺序发送的消息队列中之后,那么,消费者如何按照发送顺序进行消费呢?
\\nRocketMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently。所以,想要实现顺序消费,需
\\nconsumer.registerMessageListener(\\n new MessageListenerOrderly() {\\n Override\\n public ConsumeOrderlyStatus consumeMessage (List<MessageExt> msgs, ConsumeOrderlyContext context){\\n System.out.printf(\\"Receive order msg:\\" + new String(msgs.get(0).getBody()));\\n return ConsumeOrderlyStatus.SUCCESS;\\n }\\n});\\n
\\n当我们用以上方式注册一个消费之后,为了保证同一个队列中的有序消息可以被顺序消费,就要保证RocketMQ的Broker只会把消息发送到同一个消费者上,这时候就需要加锁了。
\\n在实现中,ConsumeMessageOrderlyService 初始化的时候,会启动一个定时任务,会尝试向 Broker 为当前消费者客户端申请分布式锁。如果获取成功,那么后续消息将会只发给这个Consumer。
\\n接下来在消息拉取的过程中,消费者会一次性拉取多条消息的,并且会将拉取到的消息放入 ProcessQueue,同时将消息提交到消费线程池进行执行。
\\n那么拉取之后的消费过程,怎么保证顺序消费呢?这里就需要更多的锁了。
\\nRocketMQ在消费的过程中,需要申请 MessageQueue 锁,确保在同一时间,一个队列中只有一个线程能处理队列中的消息。
\\n获取到 MessageQueue 的锁后,就可以从ProcessQueue中依次拉取一批消息处理了,但是这个过程中,为了保证消息不会出现重复消费,还需要对ProcessQueue进行加锁。(这个在扩展知识中展开)
\\n然后就可以开始处理业务逻辑了。
\\n总结下来就是三次加锁,先锁定Broker上的MessageQueue,确保消息只会投递到唯一的消费者,对本地的MessageQueue加锁,确保只有一个线程能处理这个消息队列。对存储消息的ProcessQueue加锁,确保在重平衡的过程中不会出现消息的重复消费。
\\n(完整的处理流程大家可以看一下这张图,是极客时间上某个专栏中的内容,虽然专栏中这段文字描述不太容易懂,但是这个图画的还是挺清晰的。)
\\n前面介绍客户端加锁过程中,一共加了三把锁,那么,有没有想过这样一个问题,第三把锁如果不加的话,是不是也没问题?
\\n因为我们已经对MessageQueue加锁了,为啥还需要对ProcessQueue再次加锁呢?
\\n这里其实主要考虑的是重平衡的问题。
\\n当我们的消费者集群,新增了一些消费者,发生重平衡的时候,某个队列可能会原来属于客户端A消费的,但是现在要重新分配给客户端B了。
\\n这时候客户端A就需要把自己加在Broker上的锁解掉,而在这个解锁的过程中,就需要确保消息不能在消费过程中就被移除了,因为如果客户端A可能正在处理一部分消息,但是位点信息还没有提交,如果客户端B立马去消费队列中的消息,那存在一部分数据会被重复消费。
\\n那么如何判断消息是否正在消费中呢,就需要通过这个ProcessQueue上面的锁来判断了,也就是说在解锁的线程也需要尝试对ProcessQueue进行加锁,加锁成功才能进行解锁操作。以避免过程中有消息消费。
\\n通过上面的介绍,我们知道了RocketMQ的顺序消费是通过在消费者上多次加锁实现的,这种方式带来的问题就是会降低吞吐量,并且如果前面的消息阻塞,会导致更多消息阻塞。所以,顺序消息需要慎用。
","description":"RocketMQ---如何保证消息的顺序性 和Kafka只支持同一个Partition内消息的顺序性一样,RocketMQ中也提供了基于队列(分区)的顺序消费。即同一个队列内的消息可以做到有序,但是不同队列内的消息是无序的!\\n\\n当我们作为MQ的生产者需要发送顺序消息时,需要在send方法中,传入一个MessageQueueSelector。\\n\\nMessageQueueSelector中需要实现一个select方法,这个方法就是用来定义要把消息发送到哪个MessageQueue的,通常可以使用取模法进行路由:\\n\\nSendResult sendResult = p…","guid":"https://juejin.cn/post/7493052429228834854","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T00:59:09.629Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/744de950f036406fa217de66e89c3794~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745283549&x-signature=q7ssq5Rjf7a1jVPuXEtKCHIZQpE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Spring AI MCP入门:手写一个Jdbc MCP Server","url":"https://juejin.cn/post/7493033903289335843","content":"当前MCP Server多基于Python/Node.js开发,Java虽存在SDK但应用较少。Spring框架正基于MCP SDK开发Spring AI","description":"当前MCP Server多基于Python/Node.js开发,Java虽存在SDK但应用较少。Spring框架正基于MCP SDK开发Spring AI","guid":"https://juejin.cn/post/7493033903289335843","author":"索码理","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T00:07:47.643Z","media":null,"categories":["后端","MCP","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"为什么用了 Stream,代码反而越写越丑了?","url":"https://juejin.cn/post/7492809676683509787","content":"\\n\\n哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。
\\n
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
\\n我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
\\n\\n\\n小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
\\n
众所周知,Java 8 新特性引入 Stream
API, 它对集合框架是一个重要的扩展; 而且 Stream
API 为我们提供了更简洁、更声明式的方式来处理数据集合。在很多情况下,Stream
可以使代码更简洁、可读性更高。然而,许多开发者发现,尽管 Stream
能够减少代码行数和实现逻辑的复杂度,但在实际开发中,使用 Stream
反而会导致代码变得更加难懂、可读性降低。为什么会出现这种情况呢?
本文将带大家一起深入剖析使用 Stream
时可能带来的问题,并探讨如何避免这些问题。
Stream
的本意和优点首先,我们来回顾一下 Stream
的优点:
Stream
,我们可以更像 SQL 查询一样表达业务逻辑,这种高层次的抽象使得代码变得更加简洁。Stream
提供了强大的并行处理支持,可以轻松地处理大规模数据。Stream
支持懒加载(Lazy Evaluation),意味着在没有最终操作(如 collect()
)时,数据并不会被处理,提高了性能。Stream
计算偶数之和import java.util.Arrays;\\nimport java.util.List;\\n\\n/**\\n * @Author 喵手\\n * @date: 2025-04-14\\n */\\npublic class StreamTest {\\n public static void main(String[] args) {\\n List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);\\n int sum = numbers.stream()\\n .filter(n -> n % 2 == 0)\\n .mapToInt(Integer::intValue)\\n .sum();\\n System.out.println(sum); // 输出 12\\n }\\n}\\n
\\n 在这个例子中,Stream
用简洁的声明式方式表达了“找出所有偶数并求和”的业务逻辑,代码简洁明了。
就光看着如下代码,是不是非常简洁,使用起来也及其方便,比传统写法优雅多了。
\\nStream
时代码反而变丑的原因 尽管 Stream
提供了许多优点,但有时在复杂的业务场景下,过度使用 Stream
或不当使用会导致代码变得更加难懂。以下是一些可能导致代码“越写越丑”的原因:
Stream
的一个常见用法是链式调用,即将多个操作(如 filter()
、map()
、reduce()
)连接在一起。尽管这种链式调用在简单的场景中非常有效,但在复杂的业务逻辑中,过多的链式调用会使代码变得冗长且难以理解,尤其当每个操作的行为都不明确时。
List<String> result = list.stream()\\n .filter(s -> s.length() > 3)\\n .map(s -> s.toUpperCase())\\n .filter(s -> s.startsWith(\\"A\\"))\\n .distinct()\\n .collect(Collectors.toList());\\n
\\n在这个例子中,多个操作被串联在一起,每个操作看起来都非常简洁,但是当业务逻辑变得复杂时,这种链式调用会让代码看起来像是“黑箱”,很难理解每个操作的具体含义,有木有,特别是运维前同事的代码,就有种想拍屎它的感jio,写的都是啥!!!
\\n Stream
的懒加载特性虽然提高了性能,但在某些情况下,这种特性会导致代码执行的顺序不明确,容易让开发者迷失在操作的流程中。如果流式操作没有合适的终止操作(如 collect()
、forEach()
),代码可能看起来没有执行任何实际操作。
List<String> strings = Arrays.asList(\\"apple\\", \\"banana\\", \\"cherry\\");\\nstrings.stream()\\n .filter(s -> s.length() > 5)\\n .map(s -> s.toUpperCase());\\n
\\n 在上面的代码中,Stream
中的操作不会执行,因为没有终止操作(如 collect()
或 forEach()
)。这种懒加载特性会让人产生疑问:是不是代码缺少某些执行步骤?
forEach()
进行输出),代码的可操作性变差。 由于 Stream
操作通常是基于函数式编程的,而函数式编程是高度抽象化的,调试过程可能变得困难。尤其是当 Stream
操作链较长时,很难逐步追踪执行过程并查看每个步骤的中间结果。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);\\nint sum = numbers.stream()\\n .map(n -> n * 2)\\n .filter(n -> n > 5)\\n .reduce(0, Integer::sum);\\n
\\n 在上面的例子中,虽然代码简洁,但调试时可能难以迅速看到每个中间操作的结果。你可能会希望查看 map
后的数据,或者调试 filter
的逻辑,但这种函数式编程风格不太容易追踪。
尽管 Stream
提供了并行流处理的能力,但并行流在某些情况下可能引入性能问题。对于小规模的数据集,使用并行流反而会增加不必要的开销。过度依赖并行流可能会导致性能下降。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);\\nint sum = numbers.parallelStream()\\n .filter(n -> n > 2)\\n .mapToInt(Integer::intValue)\\n .sum();\\n
\\n在这种情况下,数据集非常小,使用并行流处理反而增加了线程切换的开销,不会带来性能提升。
\\nStream
代码变丑? 虽然 Stream
能提高代码的简洁性,但为了避免上述问题,开发者应该注意以下几点:
如果链式调用过长,可以考虑将其拆分成多个步骤,并为每个步骤添加有意义的变量名,这样可以使每个操作更加清晰。
\\nList<String> filteredStrings = strings.stream()\\n .filter(s -> s.length() > 3)\\n .collect(Collectors.toList());\\n\\nList<String> upperCaseStrings = filteredStrings.stream()\\n .map(String::toUpperCase)\\n .collect(Collectors.toList());\\n
\\n 确保每个流的操作都有一个明确的终止操作,避免懒加载特性带来的困惑。常见的终止操作包括 collect()
、forEach()
、reduce()
等。
使用函数式编程时,尽量使每个操作简洁且有意义。避免复杂的 filter()
、map()
操作堆叠在一起,尽量将每个操作的意图表达得更加清晰。
在使用并行流时,应该评估数据集的大小和操作的复杂性,确保并行流的引入确实能带来性能提升。
\\n Stream
是 Java 中非常强大的工具,它能够使代码更加简洁、声明式且易于扩展。然而,在一些复杂的场景中,滥用 Stream
或者不当的使用方式可能会让代码变得难以理解,甚至影响性能。为了避免这些问题,开发者应该根据具体场景和需求合理使用 Stream
,保持代码简洁且易于理解,所以说,任何事物都有两面性,适合的场景才是最好的!而不是任何场景。
... ...
\\n好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
\\n... ...
\\n学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
\\nwished for you successed !!!
\\n⭐️若喜欢我,就请关注我叭。
\\n⭐️若对您有用,就请点赞叭。
\\n⭐️若有疑问,就请评论留言告诉我叭。
\\n\\n","description":"哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。 今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。\\n\\n 我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。\\n\\n小伙伴们在批阅的过程中…","guid":"https://juejin.cn/post/7492809676683509787","author":"喵手","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T13:18:12.529Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9526aa7f04764c13b3a79e7f54322176~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Za15omL:q75.awebp?rk3s=f64ab15b&x-expires=1745846076&x-signature=hsXAkiUXZCCbNjg3XdWRdPxez1k%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java EE","Java"],"attachments":null,"extra":null,"language":null},{"title":"同事突然考我1000 个线程同时运行,怎么防止不卡?","url":"https://juejin.cn/post/7492975598873542682","content":"版权声明:本文由作者原创,转载请注明出处,谢谢支持!
\\n
\\n\\n哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。
\\n
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
\\n我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
\\n\\n\\n小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
\\n
我同事突然问我说,假若现在有个需求场景,当我们需要同时启动1000个线程时,系统的负载和资源竞争会急剧增加,可能导致系统变得不响应甚至崩溃。为了防止系统因为线程过多而卡住,确保系统能够平稳运行,我们需要采取一些策略来优化线程的管理和资源利用。那么你们有啥好的方式能够进行优化?如果你们暂时还无头绪,没关系,看完我推荐的这几种后,你们就不能再说自己不会需要研究一下了。以下是几种有效的方法来防止1000个线程同时运行时卡顿的解决方案,仅供参考。
\\nExecutorService
) 直接启动1000个线程可能会导致系统资源耗尽,尤其是当线程数过多时,操作系统和JVM会为每个线程分配一定的资源。线程池(如ExecutorService
)可以有效地控制线程数量,通过复用线程和任务调度来优化性能。线程池的核心概念是使用固定大小的线程池来执行任务,而不是每次都创建新的线程。
如下是示例代码,仅供参考:
\\nimport java.util.concurrent.*;\\n\\n/**\\n * @Author 喵手\\n * @date: 2025-04-14\\n */\\npublic class ThreadPoolExample {\\n public static void main(String[] args) {\\n // 创建一个固定大小的线程池\\n ExecutorService executor = Executors.newFixedThreadPool(10); // 线程池大小为10\\n\\n // 提交1000个任务\\n for (int i = 0; i < 1000; i++) {\\n executor.submit(() -> {\\n try {\\n // 模拟任务处理\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟耗时任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n }\\n // 关闭线程池\\n executor.shutdown();\\n }\\n}\\n
\\n线程池的大小设置为10,意味着最多同时运行10个线程,其余的任务会等待线程池中的线程空闲出来执行。这就避免了直接启动1000个线程的资源消耗。
\\npool-1-thread-1 is running
的消息,表示当前线程正在执行任务。这种方式适用于当需要执行大量独立任务时,可以利用线程池来避免每个任务都重新创建线程,从而提高性能。
\\n如下是正式环境演示截图:
\\n 如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码演示了如何使用Java中的线程池(ExecutorService
)来并行处理多个任务,具体是创建了一个固定大小的线程池来执行1000个任务。下面是代码的逐行解析:
1. 引入必要的库:
\\nimport java.util.concurrent.*;\\n
\\n 这行代码导入了Java并发编程包中的核心类。ExecutorService
是用于管理线程池的接口,Executors
是一个工厂类,提供了各种创建线程池的静态方法。
2. 主类与 main
方法:
public class ThreadPoolExample {\\n public static void main(String[] args) {\\n
\\n 这段代码定义了一个名为 ThreadPoolExample
的公共类,并声明了 main
方法,这是程序执行的入口点。
3. 创建线程池:
\\nExecutorService executor = Executors.newFixedThreadPool(10); // 线程池大小为10\\n
\\nExecutors.newFixedThreadPool(10)
创建了一个固定大小的线程池,大小为10。也就是说,最多同时运行10个线程。ExecutorService
是一个接口,它提供了管理线程池的一些常用方法,如提交任务、关闭线程池等。4. 提交任务:
\\nfor (int i = 0; i < 1000; i++) {\\n executor.submit(() -> {\\n try {\\n // 模拟任务处理\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟耗时任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n}\\n
\\nexecutor.submit(() -> {...})
使用Lambda表达式提交了一个任务。每个任务的内容是:\\nThread.sleep(1000)
来让线程暂停1秒钟。在这个例子中,任务只是简单的打印当前线程名称并休眠,实际应用中这些任务可以是任何长时间运行的操作,如文件处理、数据库操作等。
\\n5. 关闭线程池:
\\nexecutor.shutdown();\\n
\\nexecutor.shutdown()
会在所有已提交的任务完成后,关闭线程池。这是一个优雅的关闭方式,它不会立即停止正在执行的任务,而是停止接受新的任务,并在所有任务完成后关闭线程池。shutdownNow()
,但这种方法不保证所有任务会被执行。总结:
\\nsubmit()
提交任务,shutdown()
关闭线程池。即使使用线程池,仍然需要注意限制同时运行的线程数。如果线程池配置过大,仍然会消耗大量的资源,导致卡顿。可以通过调整线程池的大小来优化并发处理能力。
\\n 在创建线程池时,可以根据系统的硬件资源(如CPU和内存)来确定适当的线程池大小。一个常见的经验法则是线程池的大小可以设置为CPU核心数的两倍(例如2 * CPU核心数
),但这也取决于任务的性质和其他因素。
如下是示例代码,仅供参考:
\\nimport java.util.concurrent.*;\\n\\n/**\\n * @Author 喵手\\n * @date: 2025-04-14\\n */\\npublic class CPUThreadPoolExample {\\n public static void main(String[] args) {\\n // 获取系统的CPU核心数\\n int cpuCount = Runtime.getRuntime().availableProcessors();\\n System.out.println(\\"CPU核心数: \\" + cpuCount);\\n\\n // 设置线程池大小为2倍CPU核心数\\n ExecutorService executor = Executors.newFixedThreadPool(cpuCount * 2);\\n System.out.println(\\"线程池大小: \\" + (cpuCount * 2));\\n\\n // 提交任务到线程池\\n for (int i = 0; i < 1000; i++) {\\n executor.submit(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is executing the task\\");\\n Thread.sleep(1000); // 模拟任务耗时1秒\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n }\\n\\n // 关闭线程池\\n executor.shutdown();\\n }\\n}\\n
\\n线程池大小的选择:选择线程池大小为 cpuCount * 2
,对于大多数 I/O 密集型应用是合理的,因为 I/O 操作往往会等待外部资源(如数据库、网络等),此时 CPU 可以空闲出来处理其他任务。但如果任务是 CPU 密集型的,通常线程池大小应为 CPU 核心数。
并发控制:通过线程池并发执行多个任务,而不是为每个任务创建新的线程,从而避免了线程创建的开销,确保系统性能和资源的高效利用。
\\n如下是正式环境演示截图:
\\n如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。你的代码看起来非常好!它展示了如何根据系统的 CPU 核心数来动态设置线程池的大小,并提交多个任务到线程池进行并发执行。代码的结构清晰且符合并发编程的最佳实践。
\\n1. 获取 CPU 核心数:
\\nint cpuCount = Runtime.getRuntime().availableProcessors();\\nSystem.out.println(\\"CPU核心数: \\" + cpuCount);\\n
\\nRuntime.getRuntime().availableProcessors()
获取系统的 CPU 核心数。这个方法返回的值表示你机器上可用的处理器核心数。在多核机器上,这个值通常会大于1。2. 设置线程池大小:
\\nExecutorService executor = Executors.newFixedThreadPool(cpuCount * 2);\\nSystem.out.println(\\"线程池大小: \\" + (cpuCount * 2));\\n
\\ncpuCount * 2
。这是因为通常在计算密集型任务中,你可以让线程池大小为 CPU 核心数的两倍,这样线程可以在等待 I/O 操作时利用 CPU 执行计算任务。3. 提交任务:
\\nfor (int i = 0; i < 1000; i++) {\\n executor.submit(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is executing the task\\");\\n Thread.sleep(1000); // 模拟任务耗时1秒\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n}\\n
\\nexecutor.submit()
被提交给线程池执行,而线程池会调度合适的线程来执行这些任务。cpuCount * 2
,最多在同一时刻会有这么多任务并发执行。4. 关闭线程池:
\\nexecutor.shutdown();\\n
\\nshutdown()
会优雅地关闭线程池。它会停止接收新任务,并在所有已提交的任务完成后关闭线程池。shutdown()
并不会立即终止正在执行的任务,而是等待所有任务执行完毕后才会关闭线程池。总结:\\n 这个程序展示了如何通过 ExecutorService
管理线程池来并发执行多个任务,并根据 CPU 核心数动态调整线程池大小,达到更高效的并行执行效果。通过合理设置线程池的大小和任务调度,你能够高效地利用系统资源,避免过度创建线程和线程管理的开销。
Java 8引入了CompletableFuture
类,提供了一种更优雅的方式来管理异步任务。如果任务本身是独立的,并且可以异步执行,使用CompletableFuture
可以提高效率,避免不必要的线程阻塞。
如下是示例代码,仅供参考:
\\nimport java.util.concurrent.*;\\n\\n/**\\n * @Author 喵手\\n * @date: 2025-04-14\\n */\\npublic class CompletableFutureExample {\\n public static void main(String[] args) {\\n int numThreads = 10;\\n ExecutorService executor = Executors.newFixedThreadPool(numThreads);\\n\\n // 提交1000个异步任务\\n for (int i = 0; i < 1000; i++) {\\n CompletableFuture.runAsync(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟耗时任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n }, executor);\\n }\\n\\n // 关闭线程池\\n executor.shutdown();\\n }\\n}\\n
\\nCompletableFuture.runAsync()
用于异步执行任务,这些任务在后台线程池中执行,不会阻塞主线程。runAsync()
方法默认是无返回值的异步任务,如果任务需要返回值,可以使用CompletableFuture.supplyAsync()
。pool-1-thread-1 is running
的信息,表示当前的线程在执行任务。任务间的执行顺序是不确定的,具体哪一个线程执行哪个任务取决于线程调度。如下是正式环境演示截图:
\\n如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码展示了如何使用 CompletableFuture
来处理异步任务,并结合线程池来并发执行任务。CompletableFuture
是Java 8引入的一个强大工具,它提供了一个非常灵活和强大的异步编程模型。接下来,我会逐行解析代码的功能和原理:
1. 引入必要的库:
\\nimport java.util.concurrent.*;\\n
\\n这里引入了Java并发编程包中的核心类。CompletableFuture
是用于处理异步任务的类,而 ExecutorService
和 Executors
用来管理线程池。
2. 主类与 main
方法:
public class CompletableFutureExample {\\n public static void main(String[] args) {\\n
\\n这段代码定义了一个名为 CompletableFutureExample
的公共类,并声明了 main
方法,程序的入口点。
3. 创建线程池:
\\nint numThreads = 10;\\nExecutorService executor = Executors.newFixedThreadPool(numThreads);\\n
\\nnumThreads
设置了线程池的大小为10,这意味着线程池最多同时可以有10个线程在执行任务。Executors.newFixedThreadPool(numThreads)
创建了一个固定大小的线程池,线程池的大小是 numThreads
,这里是10。4. 提交异步任务:
\\nfor (int i = 0; i < 1000; i++) {\\n CompletableFuture.runAsync(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟耗时任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n }, executor);\\n}\\n
\\n这段代码向线程池提交了1000个异步任务,具体功能如下:
\\nCompletableFuture.runAsync()
用来提交一个异步任务,这个任务通过 Runnable
接口表示。() -> {...}
是一个Lambda表达式,表示异步执行的任务内容。每个任务打印当前线程的名称,并模拟一个1秒钟的耗时任务(使用 Thread.sleep(1000)
)。runAsync()
方法有两个参数:\\nRunnable
对象,表示要执行的任务。Executor
,指定任务将在哪个线程池中执行。这里将线程池 executor
作为参数传入,确保任务使用线程池中的线程来执行。5. 关闭线程池:
\\nexecutor.shutdown();\\n
\\nexecutor.shutdown()
会在所有已提交的任务完成后,关闭线程池。与 ExecutorService
的 submit()
方法不同,这里是使用 runAsync()
提交的异步任务,shutdown()
确保线程池中的所有任务完成后才会关闭。shutdown()
并不会立即停止正在执行的任务,它会等待已提交的任务完成后再关闭线程池。总结:
\\nCompletableFuture.runAsync()
提交了1000个异步任务。每个任务都在独立的线程中运行,不会阻塞主线程。CompletableFuture.runAsync()
是一个异步执行方法,它不会阻塞主线程。它会直接返回一个 CompletableFuture
对象,但主线程继续执行,而任务会在后台线程池中的线程上异步执行。shutdown()
方法关闭线程池。\\n进一步说明:CompletableFuture
是一个非常强大的工具,可以在异步任务完成时执行回调,或者将多个异步操作组合在一起进行处理。可以通过 thenRun()
, thenApply()
, thenCompose()
等方法进一步扩展异步操作的处理逻辑,进行链式调用。runAsync()
没有返回任何结果,因此仅仅是执行任务。如果需要返回值,可以使用 CompletableFuture.supplyAsync()
。这个示例展示了如何使用 CompletableFuture
来提高并发性能,同时避免了在多线程中使用传统的回调方法或显式的 Thread
创建。
如果任务的提交速率过高(即一次性提交1000个任务),即使使用线程池,系统也可能会被压垮。通过限制提交任务的速率,可以有效避免系统被瞬时大量任务拖慢。常见的方式有:
\\nSemaphore
、RateLimiter
等技术来实现。Semaphore
进行限流如下是示例代码,仅供参考:
\\nimport java.util.concurrent.*;\\n\\npublic class RateLimitedExecutor {\\n private static final int MAX_CONCURRENT_TASKS = 10; // 最大并发任务数\\n private static final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_TASKS);\\n\\n public static void main(String[] args) throws InterruptedException {\\n ExecutorService executor = Executors.newFixedThreadPool(10);\\n\\n for (int i = 0; i < 1000; i++) {\\n // 获取许可\\n semaphore.acquire();\\n executor.submit(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n } finally {\\n // 释放许可\\n semaphore.release();\\n }\\n });\\n }\\n\\n // 关闭线程池\\n executor.shutdown();\\n }\\n}\\n
\\n 使用Semaphore
控制最大并发数,确保最多只有MAX_CONCURRENT_TASKS
个任务同时运行,其他任务会等待。
pool-1-thread-1 is running
,表示某个线程正在执行任务。由于有并发控制,所以你会看到最多10个线程并发执行。Thread.sleep(1000)
),所以执行时间会比较长,但并发任务的数量被限制在10个内。如下是正式环境演示截图:
\\n 如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码实现了一个基于信号量(Semaphore
)的并发任务控制,限制了同时运行的任务数,以防止线程池中的任务过多,避免资源被耗尽。代码的关键在于使用 Semaphore
来控制最大并发任务数。下面我会逐行解析代码的原理和细节。
1. 引入必要的库:
\\nimport java.util.concurrent.*;\\n
\\n这行代码引入了Java的并发包中的关键类:
\\nExecutorService
:管理线程池,提交和执行任务。Executors
:用于创建不同类型的线程池的工具类。Semaphore
:用于控制并发访问共享资源的信号量。2. 定义常量和信号量:
\\nprivate static final int MAX_CONCURRENT_TASKS = 10; // 最大并发任务数\\nprivate static final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_TASKS);\\n
\\nMAX_CONCURRENT_TASKS
设置了最大并发任务数,这里设为10,意味着最多同时运行10个任务。semaphore
是一个信号量,用来控制允许同时访问的任务数。Semaphore
的构造方法接收一个整数参数,表示允许并发的许可证数。这里,信号量的值为10,表示最多同时有10个线程可以运行。3. 主方法及线程池初始化:
\\nExecutorService executor = Executors.newFixedThreadPool(10);\\n
\\nExecutors.newFixedThreadPool(10)
创建了一个固定大小的线程池,大小为10,意味着线程池最多同时运行10个线程。executor
变量将用于提交任务到线程池。4. 提交任务:
\\nfor (int i = 0; i < 1000; i++) {\\n // 获取许可\\n semaphore.acquire();\\n executor.submit(() -> {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" is running\\");\\n Thread.sleep(1000); // 模拟任务\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n } finally {\\n // 释放许可\\n semaphore.release();\\n }\\n });\\n}\\n
\\nfor
循环提交了1000个任务到线程池。semaphore.acquire()
方法会阻塞当前线程,直到信号量中有可用的许可为止。如果信号量的许可已用尽,调用线程将被阻塞,直到有其他线程释放许可。MAX_CONCURRENT_TASKS
(即10个)任务是并发执行的。executor.submit()
方法将一个 Runnable
任务提交到线程池。这个任务只是打印当前线程的名称,模拟一个耗时的操作(Thread.sleep(1000)
)。semaphore.release()
方法释放一个许可,允许其他等待的任务继续执行。finally
块保证即使任务抛出异常,信号量也会被释放,避免导致死锁。5. 关闭线程池:
\\nexecutor.shutdown();\\n
\\nexecutor.shutdown()
会在所有提交的任务完成后关闭线程池,停止接受新的任务。这是一个优雅的关闭方式,确保所有任务都完成后才会关闭线程池。6. 总结与关键点:
\\nSemaphore
来限制最大并发任务数。semaphore.acquire()
用于阻塞任务直到可以执行,而 semaphore.release()
用于释放许可。使用 Semaphore
的好处:
Semaphore
允许我们非常精确地控制同时运行的任务数。对于某些资源有限的场景(比如数据库连接池、网络带宽等),可以通过信号量来保证不会超出资源的最大限制。Semaphore
则通过限制并发数来避免这种情况。这种方式适用于你希望对并发任务进行精细控制的场景,特别是当任务数较大,而同时运行的任务数应受限时。
\\n 对于IO密集型任务(如网络请求、文件读写),阻塞式的线程处理会浪费大量资源。可以使用异步IO或非阻塞编程模型,如Java的NIO
、Netty
等,来处理大规模的并发任务。
通过异步IO,可以在等待IO操作完成的同时,让CPU去做其他的工作,避免了线程阻塞。
\\n 在系统中运行大量线程时,监控线程的使用情况至关重要。可以通过jconsole
、jvisualvm
等工具来监控线程池的使用情况和资源消耗,及时调整线程池大小和任务策略,确保系统始终保持在合理的负载范围内。
总而言之,若你要启动1000个线程时,为了防止系统卡顿,我们需要采取多种措施来合理管理线程和资源:
\\nExecutorService
)来控制最大并发线程数,避免线程过多。CompletableFuture
)能提高任务执行的效率。通过这些方法,我们可以有效地管理大量并发线程,避免卡顿和性能瓶颈,确保系统的平稳运行。
\\n... ...
\\n好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
\\n... ...
\\n学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
\\nwished for you successed !!!
\\n⭐️若喜欢我,就请关注我叭。
\\n⭐️若对您有用,就请点赞叭。
\\n⭐️若有疑问,就请评论留言告诉我叭。
\\n\\n","description":"哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。 今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。\\n\\n 我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。\\n\\n小伙伴们在批阅的过程中…","guid":"https://juejin.cn/post/7492975598873542682","author":"喵手","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T13:05:33.414Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9602f919b4c44da88c616b859d5f3a48~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Za15omL:q75.awebp?rk3s=f64ab15b&x-expires=1745845499&x-signature=swNNXhtlgx6Lb5tq%2Fr6dAHuoM4E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1b0dfdd9afd24fa792ba185521d33a00~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Za15omL:q75.awebp?rk3s=f64ab15b&x-expires=1745845499&x-signature=RKDZ1LH0qr0zOjbYmliCISGSLr4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e3132cf9ccb4f008e9f6f91370cecce~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Za15omL:q75.awebp?rk3s=f64ab15b&x-expires=1745845499&x-signature=Lk18GC9Mv7Tkgqry%2BDbaaTI%2FXAk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2a4b233ba604961916f932bc105adb4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Za15omL:q75.awebp?rk3s=f64ab15b&x-expires=1745845499&x-signature=XlIjCKsYW706DPaFjVm%2F5xlj6Hk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Java EE"],"attachments":null,"extra":null,"language":null},{"title":"别让外部接口\\"毒死\\"你的系统!防腐层技术一定要知道","url":"https://juejin.cn/post/7492975598873526298","content":"版权声明:本文由作者原创,转载请注明出处,谢谢支持!
\\n
大家好,我是草捏子。今天我们要聊一个听起来很专业,但其实特别接地气的概念——防腐层。就像我们点外卖时会用一次性餐具隔离不干净的餐盒,我们的系统也需要这样一个\\"隔离装置\\"。
\\n程序员小王接到一个需求:给他们公司的电商系统接入美团外卖的订单接口。他直接把外卖平台的订单数据塞进自家系统,结果...
\\n三天后系统就崩溃了:日期格式冲突、金额单位混乱、状态码对不上...这就是典型的\\"数据中毒\\"问题。如果当时用了防腐层,这些问题都能避免。
\\n# 没有防腐层的危险代码\\ndef process_order(external_data):\\n # 直接使用外部系统的字段\\n order_id = external_data[\\"transactionID\\"] \\n amount = external_data[\\"total\\"] # 单位是分\\n # 这里埋着定时炸弹...\\n
\\n# 有防腐层的安全代码\\nclass ExternalOrderAdapter:\\n def convert(self, external_data):\\n return InternalOrder(\\n id=external_data[\\"transactionID\\"],\\n amount=external_data[\\"total\\"] / 100, # 转换为元\\n status=self._map_status(external_data[\\"state\\"])\\n )\\n \\n def _map_status(self, external_status):\\n # 状态码转换字典\\n status_map = {\\n 1: \\"pending\\",\\n 2: \\"completed\\",\\n 3: \\"canceled\\"\\n }\\n return status_map.get(external_status, \\"unknown\\")\\n
\\nclass PaymentACL:\\n @staticmethod\\n def to_internal(external_payment):\\n # 转换金额(美分转人民币元)\\n amount = external_payment[\\"amount\\"] / 100 * 6.5\\n \\n # 转换时间格式\\n payment_time = datetime.strptime(\\n external_payment[\\"timestamp\\"], \\n \\"%Y-%m-%dT%H:%M:%SZ\\"\\n ).strftime(\\"%Y-%m-%d %H:%M:%S\\")\\n \\n return {\\n \\"order_no\\": f\\"EXT_{external_payment[\'id\']}\\",\\n \\"amount\\": round(amount, 2),\\n \\"payment_time\\": payment_time,\\n \\"status\\": \\"success\\" if external_payment[\\"code\\"] == 200 else \\"failed\\"\\n }\\n
\\n# 带缓存和验证的防腐层\\nclass EnhancedACL:\\n def __init__(self):\\n self.status_cache = {}\\n self.currency_rates = self._load_rates()\\n \\n def convert_order(self, external_data):\\n # 验证数据完整性\\n if not self._validate(external_data):\\n raise InvalidDataException(\\"数据校验失败\\")\\n \\n # 转换逻辑\\n internal_data = {\\n # ...转换字段...\\n }\\n \\n # 缓存处理\\n if internal_data[\\"status\\"] not in self.status_cache:\\n self.status_cache[internal_data[\\"status\\"]] = 0\\n self.status_cache[internal_data[\\"status\\"]] += 1\\n \\n return internal_data\\n
\\n假设我们要对接顺丰、京东、EMS等7家物流公司,每家接口都不一样:
\\n解决方案:
\\n# 物流防腐层伪代码\\nclass LogisticsACL:\\n def __init__(self, company):\\n self.strategy = {\\n \\"sf\\": SFConverter(),\\n \\"jd\\": JDConverter(),\\n \\"ems\\": EMSConverter()\\n }[company]\\n \\n def convert_tracking(self, external_data):\\n return self.strategy.convert(external_data)\\n\\nclass SFConverter:\\n def convert(self, data):\\n return {\\n \\"logistics_no\\": data[\\"mailNo\\"],\\n \\"current_node\\": data[\\"processInfo\\"][\\"context\\"],\\n \\"estimated_time\\": data[\\"expectedDeliverTime\\"]\\n }\\n
\\n# 防腐层单元测试示例\\ndef test_payment_conversion():\\n external_data = {\\n \\"id\\": \\"123\\", \\n \\"amount\\": 1000,\\n \\"currency\\": \\"USD\\",\\n \\"timestamp\\": \\"2023-01-01T12:00:00Z\\"\\n }\\n result = PaymentACL().convert(external_data)\\n assert result[\\"amount\\"] == 65.0\\n
\\n就像用净水器喝水需要等待几分钟,确实会有轻微影响。但可以通过缓存、异步处理等方式优化。
\\n就像不是所有食物都要用保鲜膜,只有易变质的才需要。主要看对接系统的稳定性。
\\n可以对比输入输出日志,就像检查净水器滤芯的变色提示。
\\n防腐层就像系统的\\"免疫系统\\",在如今这个需要频繁对接各种外部服务的时代,掌握这个设计模式能让你的系统:
\\n下次对接外部接口时,记得先问自己:我需要加个\\"过滤器\\"吗?
","description":"大家好,我是草捏子。今天我们要聊一个听起来很专业,但其实特别接地气的概念——防腐层。就像我们点外卖时会用一次性餐具隔离不干净的餐盒,我们的系统也需要这样一个\\"隔离装置\\"。 一、真实故事:小王的外卖系统翻车事件\\n\\n程序员小王接到一个需求:给他们公司的电商系统接入美团外卖的订单接口。他直接把外卖平台的订单数据塞进自家系统,结果...\\n\\n三天后系统就崩溃了:日期格式冲突、金额单位混乱、状态码对不上...这就是典型的\\"数据中毒\\"问题。如果当时用了防腐层,这些问题都能避免。\\n\\n二、防腐层到底是什么?\\n2.1 生活中的防腐层\\n净水器的过滤网\\n手机贴膜\\n快递包装的泡沫纸…","guid":"https://juejin.cn/post/7492975598873526298","author":"草捏子","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T12:49:21.483Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1fb0fa4f3ac840fc9f0e941c835083b0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1745842067&x-signature=j0HNQpfeLgen1AId8HsXkr7gIsw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b1f9700866c54b1f9f68fae42b8a796e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1745842067&x-signature=RBbH2nDSR30fhB25oqr7l0fFleQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"事务消息用在用么场景?如何使用","url":"https://juejin.cn/post/7492760566203121676","content":"MQ 相信程序员们都用过?但是MQ中的事务消息,估计用过的人不多。MQ的事务消息 在实现分布式事务中 可靠消息最终一致性这个方案中用得还是很多的🚀
\\n还有就是,面试经常问到的消息丢失问题,使用事务消息当然也能解决这个问题咯👍
\\n了解事务消息,就得先了解,其常使用的场景。下面看看分布式事务基于MQ实现的两种方式吧。
\\n在分布式事务场景中,实现的方案也比较多,比较常用的就是基于MQ来实现。在MQ的实现的方案中,常用两个案有以下两种
\\n\\n\\n实现可靠消息 除了 事务消息这种方式,还有一种就是本地消息,在提交业务事务中,将消息也进行写入到数据库,利用定时任务去扫描 数据库中 消息的状态,进行消息的重试或者消息状态的更正。
\\n
所以事务消息用来保证消息的可靠性的一种方式,从而实现可靠消息最终一致性的分布式事务。
\\n利用 事务消息 和 本地事务,实现事务的最终一致性。实现的主要步骤如下
\\n1、客户端A执行本地事务
\\n2、客户端A向客户端B发送MQ消息
\\n3、客户端B收到MQ消息后执行自己的本地事务
\\nRocketMQ 的事务消息实现基于两阶段提交思想,主要分为三个阶段:
\\n发送半消息:生产者先向消息队列发送半消息(Half Message),半消息对于消费者是不可见的,只有在后续被确认提交后才会对消费者可见。
\\n执行本地事务:发送半消息成功后,生产者执行本地事务,根据本地事务的执行结果决定是提交还是回滚半消息。
\\n消息状态确认:
\\n1.引入依赖
\\n<dependency>\\n <groupId>org.apache.rocketmq</groupId>\\n <artifactId>rocketmq-client</artifactId>\\n <version>4.9.2</version>\\n</dependency>\\n
\\nimport org.apache.rocketmq.client.exception.MQClientException;\\nimport org.apache.rocketmq.client.producer.LocalTransactionState;\\nimport org.apache.rocketmq.client.producer.TransactionListener;\\nimport org.apache.rocketmq.client.producer.TransactionMQProducer;\\nimport org.apache.rocketmq.common.message.Message;\\nimport org.apache.rocketmq.common.message.MessageExt;\\n\\nimport java.util.concurrent.*;\\n\\npublic class TransactionalProducer {\\n public static void main(String[] args) throws MQClientException, InterruptedException {\\n // 创建事务消息生产者\\n TransactionMQProducer producer = new TransactionMQProducer(\\"transaction_producer_group\\");\\n // 设置 NameServer 地址\\n producer.setNamesrvAddr(\\"localhost:9876\\");\\n\\n // 创建线程池用于执行本地事务\\n ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {\\n Thread thread = new Thread(r);\\n thread.setName(\\"client-transaction-msg-check-thread\\");\\n return thread;\\n });\\n\\n // 设置线程池\\n producer.setExecutorService(executorService);\\n\\n // 设置事务监听器\\n producer.setTransactionListener(new TransactionListener() {\\n @Override\\n public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {\\n // 执行本地事务\\n try {\\n System.out.println(\\"Executing local transaction...\\");\\n // 模拟本地事务执行\\n Thread.sleep(1000);\\n // 假设本地事务执行成功\\n return LocalTransactionState.COMMIT_MESSAGE;\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n return LocalTransactionState.ROLLBACK_MESSAGE;\\n }\\n }\\n\\n @Override\\n public LocalTransactionState checkLocalTransaction(MessageExt msg) {\\n // 消息队列回查本地事务状态\\n System.out.println(\\"Checking local transaction state...\\");\\n // 假设本地事务执行成功\\n return LocalTransactionState.COMMIT_MESSAGE;\\n }\\n });\\n\\n // 启动生产者\\n producer.start();\\n\\n // 发送半消息\\n try {\\n Message msg = new Message(\\"TransactionTopic\\", \\"TransactionTag\\", \\"Hello, RocketMQ Transaction Message\\".getBytes());\\n producer.sendMessageInTransaction(msg, null);\\n } catch (MQClientException e) {\\n e.printStackTrace();\\n }\\n\\n // 等待一段时间后关闭生产者\\n Thread.sleep(5000);\\n producer.shutdown();\\n }\\n} \\n
\\n3.创建消费者
\\nimport org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;\\nimport org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;\\nimport org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;\\nimport org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;\\nimport org.apache.rocketmq.client.exception.MQClientException;\\nimport org.apache.rocketmq.common.message.MessageExt;\\n\\nimport java.util.List;\\n\\npublic class TransactionalConsumer {\\n public static void main(String[] args) throws InterruptedException, MQClientException {\\n // 创建消费者\\n DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(\\"transaction_consumer_group\\");\\n // 设置 NameServer 地址\\n consumer.setNamesrvAddr(\\"localhost:9876\\");\\n // 订阅主题\\n consumer.subscribe(\\"TransactionTopic\\", \\"*\\");\\n\\n // 设置消息监听器\\n consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {\\n for (MessageExt msg : msgs) {\\n System.out.printf(\\"%s Receive New Messages: %s %n\\", Thread.currentThread().getName(), new String(msg.getBody()));\\n }\\n return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;\\n });\\n\\n // 启动消费者\\n consumer.start();\\n System.out.printf(\\"Consumer Started.%n\\");\\n }\\n} \\n
\\n本篇文章,介绍了什么是事务消息,以及事务消息的运用场景和使用案例。如果觉得有用欢迎一键三连👍👍👍
\\n\\n","description":"前言 MQ 相信程序员们都用过?但是MQ中的事务消息,估计用过的人不多。MQ的事务消息 在实现分布式事务中 可靠消息最终一致性这个方案中用得还是很多的🚀\\n\\n还有就是,面试经常问到的消息丢失问题,使用事务消息当然也能解决这个问题咯👍\\n\\n事务消息\\n\\n了解事务消息,就得先了解,其常使用的场景。下面看看分布式事务基于MQ实现的两种方式吧。\\n\\n分布式事务基于MQ的实现方式\\n\\n在分布式事务场景中,实现的方案也比较多,比较常用的就是基于MQ来实现。在MQ的实现的方案中,常用两个案有以下两种\\n\\n最大努力通知:\\n 他不需要保证消费者一定能接收到,只是尽自己最大的努力去通知就行了…","guid":"https://juejin.cn/post/7492760566203121676","author":"提前退休的java猿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T09:14:02.132Z","media":null,"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"java的Random居然是假随机","url":"https://juejin.cn/post/7492846604564283407","content":"RocketMQ 官网也对 事务消息 有详细的说明,并且还有 java 案例,非常值得阅读的 :rocketmq.apache.org/zh/docs/fea…
\\n
今天合作方K反馈没有收到微信解约信息,我感觉大概是系统报错了,或者是连接超时之类的错误,结果一排查发现这个解约消息回调到合作方B了......我记得逻辑是根据签约号查询数据库的签约信息,签约信息有渠道号,根据渠道去通知到对应的url请求,怎么会弄混了呢,百思不得其解。
\\n首先是看有多少数据是这种情况,通过查日志和数据库都确认了只有这一条数据发送到错误的渠道,对业务影响不大。然后仔细看代码逻辑发现了蹊跷的地方,查询渠道的时候居然是通过序列号去查询,不是通过协议号,下面是序列号的生成规则。
\\n一开始我只是以为刚好这么巧,在同一个时间戳+同一个随机数刚好冲突了,上线了快半年,一天可能有上万的签约记录,只出现了一次。
\\n我跟兄弟说这个事情后,他说听说Random是伪随机数,后面证实了确实如此,传入了种子后,得到的数字都是一样的...
\\n当创建Random的实例对象时,没有指定种子,系统会以当前时间戳作为种子,产生随机数。当指定种子后,两次产生的随机数序列就会一样了。
\\n所以这个随机数不管传不传种子,如果是时间戳一样,random的数字肯定是一样的.....怪不得说他是伪随机,大概的算法就是根据时间戳做一个计算处理。
\\n其实在这个业务中处理还挺简单的,不用这个“唯一”序列号做查询条件,使用有唯一索引的订单号作为查询条件就解决了。另外除了数据库唯一索引校验,还有其他几种分布式id生成方式。
\\n1、UUID
\\n缺点:字段太长占用空间,无序性,插入数据库时可能导致页分裂,降低写入效率。一般不推荐使用
\\n2、redis分布式id
\\n优点:性能较高,ID持续递增
\\n缺点:依赖redis,redis挂了服务会异常
\\n我们服务有用过redis这种生成方式
\\n3、雪花算法
\\n雪花算法是由时间戳 + 数据中心ID + 机器ID + 序列号组成。如果时间戳一样,就会在本地保存一个序列号,依次递增。
\\n优点:本地生成,无网络开销。
\\n缺点:时钟回拨问题:机器时钟不同步可能导致ID重复。
\\n其实我们也算是半个雪花算法,我们用的是时间戳+random随机数,只不过时间戳跟random随机数是绑定关系,时间戳一样,random随机数就不随机了- -
\\n4、Leaf(号段模式)
\\n美团开源的分布式ID服务,批量从数据库获取号段(如1-1000),内存分配。
\\n优点:高吞吐:减少数据库访问频率。
\\n缺点:需等待新号段获取,存在短暂阻塞风险。
\\n我们也用过这种号段方式,不过是用的redis版本。
\\nrandom是个伪随机数,不能跟时间戳共用。一般可以用时间戳+redis分布式id,或者时间戳+mysql的号段方式比较好~
","description":"故事背景 今天合作方K反馈没有收到微信解约信息,我感觉大概是系统报错了,或者是连接超时之类的错误,结果一排查发现这个解约消息回调到合作方B了......我记得逻辑是根据签约号查询数据库的签约信息,签约信息有渠道号,根据渠道去通知到对应的url请求,怎么会弄混了呢,百思不得其解。\\n\\n问题排查\\n\\n首先是看有多少数据是这种情况,通过查日志和数据库都确认了只有这一条数据发送到错误的渠道,对业务影响不大。然后仔细看代码逻辑发现了蹊跷的地方,查询渠道的时候居然是通过序列号去查询,不是通过协议号,下面是序列号的生成规则。\\n\\n一开始我只是以为刚好这么巧,在同一个时间戳…","guid":"https://juejin.cn/post/7492846604564283407","author":"玛奇玛丶","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T09:09:52.545Z","media":[{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/190f15afa3c24b609d26044fb036b4df~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1381&h=174&s=38205&e=png&b=2c2c2c","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a220dadeaa224651ad952d007c4bbd6b~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1154&h=414&s=66876&e=png&b=2c2c2c","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"业务架构设计---MQ出现消息乱序了如何解决","url":"https://juejin.cn/post/7492702134733144074","content":"消息中间件如Kafka、RocketMQ等,普通的消息是有可能存在乱序的,比如说因为网络延迟导致某个消息发送晚了,因为系统异常导致第一个消息处理失败了,等等原因都可能会导致消息乱序
\\n举个简单的例子,一次下单过程中,有一个支付消息、一个发货消息。按理说支付一定在发货之前。所以消息的顺序也是先处理支付消息、再处理发货消息。但是对于一些特殊的业务,比如那种虚拟商品,可能支付后马上就自动发货了。这时候如果有一点点网络延迟,就可能导致发货消息优先于支付消息投递。这就是所谓的消息乱序。
\\n一般来说,消息乱序会导致系统处理异常,比如A消息你还没出来就处理B消息的话可能会失败,也有可能你直接把B消息处理成功了,导致A再来的时候无法处理。等等一系列问题。所以这个乱序的问题是非常关键的。
\\n一般来说,我们有几种办法来解决这个消息乱序的问题。
\\n对于那些明确的有顺序的消息,比如像支付消息和发货消息,这个就可以在发送的时候就用顺序消息的方式发送。把他们按照顺序投递到同一个partition(队列上)上,利用分区的顺序性保证消息的顺序投递。
\\n同时还需要确保只有一个消费者进行串行消费,这样才能完全避免消息的乱序。
\\n对于上面说的支付消息和发货消息,我们可以在消息体中增加一个前置状态的信息,比如beforeStatus。
\\n作为一个消费支付消息和发货消息的系统,我们可以基于这个beforeStatus来判断和我系统中的当前状态是否一致,如果是一致的,说明我是可以处理的,那么我就处理这个消息,如果是不一致的,那说明我要的消息还没来,那我就把这个消息处理失败,让MQ下次再重投给我。
\\n这个方案有两个要求:
\\n这样就能通过状态来确保消息的有序。
\\n如果无法完全保证发送和处理的顺序,又没有状态来做前置判断,可以在消息中引入序列号,消费端根据序列号重排。例如:
\\n缺点是会增加系统复杂度,并且需要设置缓存超时时间来处理丢失的消息。
\\n还有一种方案,其实是对上面第2个和第3个方案的优化。
\\n第二个方案存在一个问题,那就是依赖MQ的重新投递,有可能会导致最终这个消息丢失了,因为一旦长时间无法消费,消息就会不再重投了。而且不断地让MQ重试,也可能会导致消息堆积,并且对系统造成一定的压力。
\\n第三个方案的问题就是需要再内存中维护一个队列,来进行排序,太麻烦了。
\\n那么,我们有一个做法。是这样的流程:
\\n这么做,就能确保所有的事件我都有存储,并且存储后立刻返回,避免消息重投和堆积。消息存储下来之后,我就可以基于这些消息做排序,以及重试了,如果某个消息处理失败了,也不怕,不断重试即可。当达到了一定次数之后,报警出来人工跟进。
\\n为了减少这个方案的定时任务带来的延迟,我们可以在写入消息表的时候,在redis中存一条记录,业务单号(比如订单号)当作key,然后把存入的消息的主键id当作value存进去。
\\n这样再有消息过来的时候,先正常处理,如果处理成功了,去redis中查一下是不是存在相同业务单号的待处理的消息,有的话,根据存储的主键id查询对应的事件,放线程池中进行处理。
","description":"MQ出现消息乱序了如何解决? 消息中间件如Kafka、RocketMQ等,普通的消息是有可能存在乱序的,比如说因为网络延迟导致某个消息发送晚了,因为系统异常导致第一个消息处理失败了,等等原因都可能会导致消息乱序\\n\\n举个简单的例子,一次下单过程中,有一个支付消息、一个发货消息。按理说支付一定在发货之前。所以消息的顺序也是先处理支付消息、再处理发货消息。但是对于一些特殊的业务,比如那种虚拟商品,可能支付后马上就自动发货了。这时候如果有一点点网络延迟,就可能导致发货消息优先于支付消息投递。这就是所谓的消息乱序。\\n\\n一般来说,消息乱序会导致系统处理异常…","guid":"https://juejin.cn/post/7492702134733144074","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T09:06:40.056Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"技术上的争论,我是错的吗?","url":"https://juejin.cn/post/7492346703103705128","content":"这个周日一直被昨天与同事争论的一个问题所困扰(周六加了班),背景是上周的一个项目,TL让我新建了一个JAR类型的工程,设计一个提供灰度功能的模块,经过清明的奋战赶工,也是如期的赶上了项目进度。
\\n我提供的灰度模块功能是这样的,最外层是业务所属的灰度层,提供每个业务定制的灰度功能,比如各个灰度间的串联,前置数据的查询等。内层为一个通用的单个灰度服务,灰度服务串联了 白名单、阻断、版本判断、AB实验、梵高人群 五个环节,接口类提供了一个判断单个用户灰度的方法,主体流程都写在 abstract 抽象类里面,子类只需要实现一个 getConfig() 方法,提供本灰度需要的配置,例如 白名单配置、版本配置、AB实验配置 等。当单个灰度接入时,只需要实现抽象类,返回灰度配置,定义好本灰度场景值即可。
\\n组内的一位同事,看了我设计的灰度流程后,反应比较激烈。
\\n同事的观点是,我提供的判断单个灰度的流程,过于复杂冗余,不符合单一职责原则,abstract 类逻辑过多,其它同事接入时的学习成本太高。
\\n同事认为,不需要提供内层的单个灰度流程,而是提供自定义的灰度服务和几个工具类即可,即最外层的业务灰度,以及 白名单、阻断、版本判断、AB实验、梵高判断 这 5 种工具,当使用者接入灰度时,新建一个业务灰度类,在业务灰度类内部调用工具串联起整个流程。
\\n同事举了一个例子,如果业务诉求是判断 1 个版本以及 3 个实验时,如果按照我的方式,需要写 3 个单个的灰度实现类,然后新建一个业务灰度类,再去调用这 3 个灰度实现类进行判断。如果按照他的方式,只需要新建一个业务灰度类,然后调用一个版本工具类判断,再调用 3 次实验判断,就可以完成业务方诉求。
\\n我的观点是,所提供的通用单个灰度服务,并非不符合单一职责原则,单一职责原则虽然要求提供粒度小、功能单一的类,但是单一职责的目的是 可复用性、可维护性、可扩展性、可读性,虽然我所提供的单个灰度判断流程的可读性稍差,但是做到了很好的可复用、可维护、可扩展,因此与单一职责并没有冲突。而且使用者接入,在绝大多数灰度场景下,都不需要感知整体逻辑,只需要进行简单的抽象类实现,定义好版本、实验等配置即可,接入成本是非常低的。
\\n而且,同事所列举的例子,是属于比较少数的场景,绝大多数场景是提供【单个版本】+【单个实验】判断即可,如果按照我的流程,只需要提供单个灰度实现类,再新建一个业务灰度类,直接进行简单调用,这比在业务类中调用工具进行流程串联成本是更低的。而即使是同事所举的例子,接入成本也不高,虽然新建 3 个实现类,有类膨胀的趋势,但是这 3 个实现,本身就是 3 个单一的灰度,应该进行隔离。
\\n再者,因为通用的单一灰度框架进行了统一的监控打点、灰度降级等能力,因此从可观测可降级角度考虑,也是优于同事的方式的。
\\n这个问题目前并没有结论,同事准备下周一把问题抛给TL,让TL进行决断,如果结论是同事的方式更好的话,他准备另写一套。
\\n不知大家怎么认为,是我的方式更好呢,还是同事的观点更为正确。
","description":"问题 这个周日一直被昨天与同事争论的一个问题所困扰(周六加了班),背景是上周的一个项目,TL让我新建了一个JAR类型的工程,设计一个提供灰度功能的模块,经过清明的奋战赶工,也是如期的赶上了项目进度。\\n\\n我提供的灰度模块功能是这样的,最外层是业务所属的灰度层,提供每个业务定制的灰度功能,比如各个灰度间的串联,前置数据的查询等。内层为一个通用的单个灰度服务,灰度服务串联了 白名单、阻断、版本判断、AB实验、梵高人群 五个环节,接口类提供了一个判断单个用户灰度的方法,主体流程都写在 abstract 抽象类里面,子类只需要实现一个 getConfig() 方法…","guid":"https://juejin.cn/post/7492346703103705128","author":"起风了布布","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-13T16:34:33.509Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"mysql---主从延时问题","url":"https://juejin.cn/post/7491954287607513138","content":"有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。主从延迟,其实就是“从库回放” 完成的时间,与 “主库写 binlog” 完成时间的差值,会导致从库查询的数据,和主库的不一致。
\\n我们分析一下主从复制的过程。
\\nMySQL 的主从复制都是单线程的操作,主库对所有 DDL 和 DML 产生 binlog,binlog 是顺序写,所以效率很高。
\\nSlave 的 Slave_IO_Running 线程会到主库取日志,放入 relay log,效率会比较高。
\\nSlave 的 Slave_SQL_Running 线程将主库的 DDL 和 DML 操作都在 Slave 实施,DML 和 DDL 的 IO 操作是随机的,不是顺序的,因此成本会很高。
\\n还可能是 Slave 上的其他查询产生 lock 争用,由于 Slave_SQL_Running 也是单线程的,所以一个 DDL 卡住了,需要执行 10 分钟,那么所有之后的 DDL 会等待这个 DDL 执行完才会继续执行,这就导致了延时。
\\n总结一下主从延迟的主要原因:主从延迟主要是出现在 “relay log 回放” 这一步,当主库的 TPS 并发较高,产生的 DDL 数量超过从库一个 SQL 线程所能承受的范围,那么延时就产生了,当然还有就是可能与从库的大型 query 语句产生了锁等待
\\n我们先看看,哪些情况会导致主从延时:
\\n面试时,有些同学能回答出使用缓存、查询主库、提升机器配置等,仅仅这些么?
\\n最容易想到的方法,缩短主从同步时间:
\\n设置配置如下:
\\nsync_binlog = 1\\ninnodb_flush_log_at_trx_commit = 1\\n
\\n\\n\\n而 slave 不需要这么高的数据安全,完全可以将 sync_binlog 设置为 0,或者关闭 binlog,innodb_flushlog 也可以设置为 0,来提高 sql 的执行效率。
\\n
架构方案:使用多台 slave 来分摊读请求,再从这些 slave 中取一台专用的服务器,只作为备份用,不进行其他任何操作,比如设置 sync_binlog 为0,或者关闭 binglog 等,提升从库查询性能。
\\n再问一下,还有么?可以文末私信我哈~~
\\nSa-Token 是一款 免费、开源 的轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。🔐
\\n目前最新版本 v1.42.0
已推送至 Maven
中央仓库 🎉,大家可以通过如下方式引入:
<!-- Sa-Token 权限认证 --\x3e\\n<dependency>\\n <groupId>cn.dev33</groupId>\\n <artifactId>sa-token-spring-boot-starter</artifactId>\\n <version>1.42.0</version>\\n</dependency>\\n
\\n该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:
\\n如果你曾经对接过 ChatGPT、DeepSeek 等大模型平台的开放接口,那你一定对 API Key 不陌生。🤝
\\nAPI Key 是一种接口调用密钥,类似于会话 token ,但比会话 token 具有更灵活的权限控制。🔑
\\n本次更新带来了 API Key 的全流程管理,支持为指定账号签发、校验、禁用、删除 API Key 。\\n同时每个 API Key 都可以单独设置不同的 scope 权限,以便在不同的场景下使用不同的 API Key,做到秘钥相互隔离,最小化授权。🛡️
\\n为了更好的展示此模块的能力,我们专门制作了一个 demo 示例:
\\n示例仓库地址:sa-token-demo-apikey 🔗
\\n在这个示例中,你可以登录测试不同的账号,并为它们签发 API Key,设置 scope 权限,并使用不同的 API Key 测试调用接口,观察响应结果。 🧪
\\n框架默认将所有 API Key 信息保存在缓存中,这可以称之为“缓存模式”,在这种模式下,重启缓存库后,数据将会丢失。⚠️
\\n框架预留了 SaApiKeyDataLoader 接口,以便你将数据的加载切换为 “数据库模式”,做到数据长久有效保存。 💾
\\n在线文档直达地址:API Key 接口调用秘钥 🔗
\\n在 Sa-Token 文档中有一段这样的示例:📚
\\n该示例演示了如何通过临时 Token 认证模块,创建 RefreshToken
为登录会话做到双 Token 的效果。🔄
但是有一天我在官网 sa-token 小助手接收到一位用户的咨询: 💬
\\n该用户指出,是否可以为 RefreshToken
提供反查机制,以便获取某个账号历史签发的 全部 RefreshToken
。
必须安排!💪🏆
\\n此次版本更新,允许程序在创建 refresh-token 时,指定第三个参数,该参数表示是否允许框架记录 Token 索引信息:
\\nSaTempUtil.createToken(\\"10001\\", 2592000, true);\\n
\\n指定为 false 代表不记录索引,只生成 token,指定为 true 代表记录索引信息,以便日后可以通过 value 反查历史签发的所有 token。🔍
\\n例如我们可以通过 SaTempUtil.getTempTokenList(\\"xxx\\")
方法获取指定账号所有历史签发的 RefreshToken
记录:
List<String> refreshTokenList = SaTempUtil.getTempTokenList(\\"10001\\");\\n
\\n在线文档直达地址:临时 Token 令牌认证 🔗
\\nTOTP 是一种动态密码算法,用于生成短暂有效的数字验证码(通常6-8位)️。它的核心原理是:结合密钥与当前时间,通过哈希运算生成一次性密码。⏱
\\nTOTP 一般有以下应用场景:
\\n本次版本新增了 TOTP 验证码的生成与校验功能,这将方便大家为自己的系统添加双因子认证能力。🚀
\\nSaTokenContext
上下文读写策略这可能是近几个版本中最底层的一次重构,几乎完全推翻了之前上下文模块的设计。💥
\\n在之前的版本中,Sa-Token 对接不同的 Web 框架需要利用这些 Web 框架的原生上下文能力来构建 Sa-Token 的上下文。 🌐
\\n本次更新 Sa-Token 利用 ThreadLocal 实现了自己的上下文存储机制,这将带来以下好处:
\\nCORS
跨域策略处理函数,提供不同架构下统一的跨域处理方案在之前的版本中,跨域处理总是要写在全局鉴权过滤器中,属于“鉴权之下的额外补充操作”。⏳
\\n新版本专门提供了一个 CORS 跨域处理策略组件,以后再也不用仅仅为了跨域就书写一个长长的鉴权过滤器组件了。🚀
\\n/**\\n * CORS 跨域处理\\n */\\n@Bean\\npublic SaCorsHandleFunction corsHandle() {\\nreturn (req, res, sto) -> {\\nres.\\n// 允许指定域访问跨域资源\\nsetHeader(\\"Access-Control-Allow-Origin\\", \\"*\\")\\n// 允许所有请求方式\\n.setHeader(\\"Access-Control-Allow-Methods\\", \\"POST, GET, OPTIONS, DELETE\\")\\n// 有效时间\\n.setHeader(\\"Access-Control-Max-Age\\", \\"3600\\")\\n// 允许的header参数\\n.setHeader(\\"Access-Control-Allow-Headers\\", \\"*\\");\\n\\n// 如果是预检请求,则立即返回到前端\\nSaRouter.match(SaHttpMethod.OPTIONS)\\n.free(r -> System.out.println(\\"--------OPTIONS预检请求,不做处理\\"))\\n.back();\\n};\\n}\\n
\\n开源仓库示例:sa-token-demo-cross 🔗
\\nsa-token-quick-login
插件支持 Http Basic
方式通过认证sa-token-quick-login
可以快速、方便的为项目注入一个登录页面,当我们引入依赖后:
<dependency>\\n <groupId>cn.dev33</groupId>\\n <artifactId>sa-token-quick-login</artifactId>\\n <version>1.42.0</version>\\n</dependency>\\n
\\n启动类:
\\n@SpringBootApplication\\npublic class SaTokenQuickDemoApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(SaTokenQuickDemoApplication.class, args);\\n \\n System.out.println(\\"\\\\n------ 启动成功 ------\\");\\n System.out.println(\\"name: \\" + SaQuickManager.getConfig().getName());\\n System.out.println(\\"pwd: \\" + SaQuickManager.getConfig().getPwd());\\n }\\n}\\n
\\n测试 Controller
\\n@RestController\\npublic class TestController {\\n @RequestMapping({\\"/\\", \\"/index\\"})\\n public String index() {\\n String str = \\"<br />\\"\\n + \\"<h1 style=\'text-align: center;\'>资源页 (登录后才可进入本页面) </h1>\\"\\n + \\"<hr/>\\"\\n + \\"<p style=\'text-align: center;\'> Sa-Token \\" + SaTokenConsts.VERSION_NO + \\" </p>\\";\\n return str;\\n }\\n}\\n\\n
\\n启动项目,使用浏览器访问:http://localhost:8081
,首次访问时,由于处于未登录状态,会被强制进入登录页面 🚪:
使用默认账号:sa / 123456
进行登录,会看到资源页面
新版本中更新了通过 Http Basic 的方式直接进行认证的能力:
\\nhttp://sa:123456@localhost:8081/\\n
\\n这将非常有助于大家在专门的 API 测试工具下进行 quick-login 相关资源接口的测试。🧪
\\n除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.42.0 版本的完整更新日志:
\\nAPI Key
模块。 [重要]TOTP
实现。 [重要]TempToken
模块,新增 value 反查 token 机制。 [重要]SaTokenContext
上下文读写策略。 [重要]Base32
编码工具类。CORS
跨域策略处理函数,提供不同架构下统一的跨域处理方案。renewTimeout
续期方法增加 token 终端信息有效性校验。cookieAutoFillPrefix
:cookie 模式是否自动填充 token 前缀。rightNowCreateTokenSession
:在登录时,是否立即创建对应的 Token-Session
。Token-Session
获取算法,减少缓存读取次数。SaLoginParameter
支持配置 SaCookieConfig
,以配置 Cookie 相关参数。hook
注册新增 registerHookToFirst
、registerHookToSecond
方法,以便更灵活的控制 hook 顺序。sa-token-quick-login
插件支持 Http Basic
方式通过认证。Temp Token
模块单元测试。Thymeleaf
集成文档不正确的依赖示例说明。unionid
章节错误描述。LoginType
。SpringBoot (<2.2.0)
引入 Sa-Token 报错的问题。hosts
文件无效可能原因排查。更新日志在线文档直达链接:sa-token.cc/doc.html#/m…
\\n代码仓库地址:gitee.com/dromara/sa-…
\\n框架功能结构图:
\\n嗨,你好呀,我是猿java
\\n在实际开发中,如何精确获取用户的真实IP?如何判断 IP是属于哪个国家?这篇文章,我们来详细地分析其原理。
\\n精准识别用户,我们通常会判断他的 IP 地址是否属于美国,主要依赖于 GeoIP(地理 IP)技术。GeoIP 通过将 IP 地址映射到地理位置,实现对用户地理位置的识别。这里以美国为例,基本流程如下:
\\nGEOIP,全称是 地理位置IP(Geolocation IP) ,是一种通过用户的IP地址来确定其地理位置的技术。简单来说,GEOIP 允许开发者和网站管理员了解访问者来自哪个国家、城市,甚至更具体的位置信息。这对于许多应用场景非常有用,比如内容本地化、地域限制、广告投放优化以及用户分析等。
\\nGEOIP 的工作原理
\\n在 Java中,实现 GeoIP功能常用的方法包括使用本地数据库或调用第三方 API,本文给出了几种常见的 GeoIP 库与服务:
\\n鉴于性能和控制性,MaxMind GeoIP2 是一个广泛推荐的选择,尤其适合需要高频次查询的应用。
\\n为了更好地理解整个过程,接下来,我们将通过详细的 Java代码示例,展示如何使用 MaxMind GeoIP2 库判断一个 IP 地址是否属于美国。
\\n注册并下载数据库:前往 MaxMind 注册一个账户,并下载 GeoLite2 Country 数据库文件(GeoLite2-Country.mmdb
)。
将数据库文件放置在项目中的合适位置,例如 src/main/resources/GeoLite2-Country.mmdb
。
如果你使用 Maven 作为项目管理工具,在 pom.xml
中添加以下依赖:
<dependency>\\n <groupId>com.maxmind.geoip2</groupId>\\n <artifactId>geoip2</artifactId>\\n <version>4.5.0</version>\\n</dependency>\\n
\\n\\n\\n注意:请确保使用最新版本的 GeoIP2 库,以获取最新的功能和修复。
\\n
在实际应用中,你需要从用户的 HTTP 请求中获取真实的 IP 地址。以下是一个在 Servlet 中获取用户 IP 地址的示例:
\\nimport javax.servlet.http.HttpServletRequest;\\n\\npublic class IPUtils {\\n /**\\n * 从 HttpServletRequest 中获取用户真实 IP 地址\\n *\\n * @param request HttpServletRequest 对象\\n * @return 用户的真实 IP 地址\\n */\\n public static String getClientIp(HttpServletRequest request) {\\n String ip = null;\\n String[] headers = {\\n \\"X-Forwarded-For\\",\\n \\"Proxy-Client-IP\\",\\n \\"WL-Proxy-Client-IP\\",\\n \\"HTTP_X_FORWARDED_FOR\\",\\n \\"HTTP_X_FORWARDED\\",\\n \\"HTTP_X_CLUSTER_CLIENT_IP\\",\\n \\"HTTP_CLIENT_IP\\",\\n \\"HTTP_FORWARDED_FOR\\",\\n \\"HTTP_FORWARDED\\",\\n \\"HTTP_VIA\\",\\n \\"REMOTE_ADDR\\"\\n };\\n for (String header : headers) {\\n ip = request.getHeader(header);\\n if (ip != null && ip.length() != 0 && !\\"unknown\\".equalsIgnoreCase(ip)) {\\n // 多个 IP 地址时取第一个\\n if (ip.contains(\\",\\")) {\\n ip = ip.split(\\",\\")[0].trim();\\n }\\n break;\\n }\\n }\\n if (ip == null || ip.length() == 0 || \\"unknown\\".equalsIgnoreCase(ip)) {\\n ip = request.getRemoteAddr();\\n }\\n return ip;\\n }\\n}\\n
\\n在上一个步骤中,我们已经识别了用户的真是IP,接下来只需要判断这个 IP是不是属于美国IP,以下是一个完整的示例,展示如何判断一个 IP 地址是否属于美国,并根据结果计算最终的服务费用。
\\nimport com.maxmind.geoip2.DatabaseReader;\\nimport com.maxmind.geoip2.exception.GeoIp2Exception;\\nimport com.maxmind.geoip2.model.CountryResponse;\\n\\nimport java.io.File;\\nimport java.io.IOException;\\nimport java.net.InetAddress;\\n\\npublic class USIPIdentifier {\\n private DatabaseReader dbReader;\\n\\n /**\\n * 构造函数,初始化 GeoIP 数据库读取器\\n *\\n * @param dbPath GeoIP 数据库文件路径\\n * @throws IOException 如果数据库文件无法读取\\n */\\n public USIPIdentifier(String dbPath) throws IOException {\\n File database = new File(dbPath);\\n dbReader = new DatabaseReader.Builder(database).build();\\n }\\n\\n /**\\n * 判断给定 IP 是否来自美国\\n *\\n * @param ip 用户的 IP 地址\\n * @return 如果来自美国返回 true,否则返回 false\\n */\\n public boolean isIPFromUS(String ip) {\\n try {\\n InetAddress ipAddress = InetAddress.getByName(ip);\\n CountryResponse response = dbReader.country(ipAddress);\\n String country = response.getCountry().getName();\\n return \\"United States\\".equalsIgnoreCase(country);\\n } catch (IOException | GeoIp2Exception e) {\\n e.printStackTrace();\\n // 异常情况下,默认返回 false\\n return false;\\n }\\n }\\n\\n\\n public static void main(String[] args) {\\n try {\\n // 初始化 USIPIdentifier,路径指向 GeoLite2-Country.mmdb\\n USIPIdentifier identifier = new USIPIdentifier(\\"src/main/resources/GeoLite2-Country.mmdb\\");\\n\\n // 示例 IP 地址\\n String userIp = \\"128.101.101.101\\"; // 替换为实际 IP\\n double baseFee = 100.0;\\n\\n // 计算最终费用\\n double finalFee = identifier.calculateFinalFee(userIp, baseFee);\\n\\n System.out.println(\\"用户 IP: \\" + userIp);\\n System.out.println(\\"基础费用: $\\" + baseFee);\\n System.out.println(\\"最终费用: $\\" + finalFee);\\n } catch (IOException e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n到此,获取用户IP并判断其所属国家功能就完成了。
\\n本文,我们分析了如何从用户的HTTP请求中判断用户所在的国家,关键点总结:
\\n最后,把猿哥的座右铭送给你:投资自己才是最大的财富。 如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
","description":"嗨,你好呀,我是猿java 在实际开发中,如何精确获取用户的真实IP?如何判断 IP是属于哪个国家?这篇文章,我们来详细地分析其原理。\\n\\n1 如何精准识别用户?\\n\\n精准识别用户,我们通常会判断他的 IP 地址是否属于美国,主要依赖于 GeoIP(地理 IP)技术。GeoIP 通过将 IP 地址映射到地理位置,实现对用户地理位置的识别。这里以美国为例,基本流程如下:\\n\\n获取用户的 IP 地址:在用户访问你的应用时,第一步是获取其请求中的 IP 地址。\\n查询 GeoIP 数据库或 API:使用 GeoIP 工具将 IP 地址映射到地理位置信息,获取国家/地区名称…","guid":"https://juejin.cn/post/7491858512606019624","author":"猿java","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-12T01:14:06.197Z","media":null,"categories":["后端","Java","面试","分布式"],"attachments":null,"extra":null,"language":null},{"title":"货拉拉-营销平台流程画布建设","url":"https://juejin.cn/post/7491927782764691490","content":"随着货拉拉用户和司机规模以及订单量的不断增长,运营对营销工具的要求不断提升,急需一个能覆盖用户和司机生命周期、全渠道触达、丰富奖励发放等能力的营销工具,流程画布因此应运而生。流程画布以数据驱动,支持多策略组合的灵活配置,并整合了丰富的营销投放能力,通过精准的用户定位、恰当的时机选择、合适的触达方式以及贴切的内容传递,帮助业务实现用户和司机规模、订单量的持续增长,从而实现流量最大化的价值转化。接下来将为大家介绍营销平台流程画布的建设与实践。
\\n货拉拉流程画布平台是以“营销策略引擎”为核心,基于用户行为驱动的一站式自动化营销工具。流程画布服务于货拉拉多业务、覆盖了用户与司机的全生命周期(拉新、促活、转化、召回),同时支持对用户与司机进行多奖励、全渠道的触达。
\\n\\n我们结合整个货运的业务流程,覆盖了多种业务场景,支持对货拉拉用户、司机全生命周期的运营。
“营销策略引擎”最核心的目标在于如何精准而有效地利用每份流量,最大化的挖掘私域的价值。我们通过自研、并整合了货拉拉多平台能力,集成了一套可灵活配置各种单一式、组合式的营销策略,通过圈人群(找对的人)-选权益(对的奖励)-选渠道(合适的触达方式)-选时机(对的时间、场景)到看效果的完整业务流程闭环,帮助业务实现流量的最大价值转化。
\\n营销流程画布平台目前已覆盖货拉拉大部分业务线,对用户与司机全生命周期的运营起到了关键性的作用,货拉拉大部分的补贴都是由流程画布平台发放出去的。 |
---|
结合业务使用场景,抽象出一套事件触发(Trigger)+ 条件规则(Condition)+ 动作执行(Action) 策略模型,通过策略模型的关联组合成一个画布模型,用来满足各种复杂的运营场景。
\\n策略模型主要由事件触发、条件规则、动作执行三部分组成。
\\n通过多个策略编排(定时+实时、实时+实时)组合成一个策略树,并指定一套策略树流转规则。
\\n一个画布是如何运行?
\\n管理后台(Admin):提供活动编排功能,并具备元数据配置管理(如事件管理)、排障工具和数据看板等功能。
\\n事件接入层(Trigger):系统支持定时和实时触发。定时触发适用于离线画像和名单上传,实时触发通过监听上游行为事件实现。事件特征平台负责管理数据源、流量管控、特征加工与补全、指标计算、存储,以及事件拆分与分发,最终输出标准化事件消息。
\\n策略引擎层(Engine):作为整个系统的决策中心,engine主要负责策略规则解析,事件流量从进入会经过一系列过滤链节点,包含条件规则校验、AB分流、画布状态、频次管控、预算控制等,最终根据动作类型、用户类型以及业务优先级等分发到不同的动作执行通道。
\\n动作执行层(Action):负责动作的执行,支持丰富的触达以及补贴动作内容,并可对各执行通道做速率管控,防止下游过载崩溃。
\\n随着覆盖的业务线越来越多,面对的产品以及业务人员也越来越多,在人力有限的情况下如何又快又稳支持各业务线持续不断的新需求成为了一个较大的挑战。
\\n如何解决
\\n我们设计了一套事件特征平台,通过后台配置管理界面,可快速配置和调整数据源、事件、函数、特征补全与加工、事件分发等元数据信息,以满足多业务线的敏捷迭代需求。
\\n数据源动态加载:系统具备动态数据源管理能力,能够实时接入事件数据源。通过集群管理,根据流量大小将事件数据源合理分配到不同集群进行消费,实现消费节点的弹性扩缩容和流量调控,从而在高并发场景下优化资源利用率,确保系统稳定性。
\\n特征加工与补全:支持通过Groovy脚本、RPC接口和指标统计等方式,对实时事件数据进行特征补全和加工,从而扩展事件特征集。
\\n数据标准化处理
\\n统一数据模型 :将交易、计价、司机、用户等领域多源异构数据转换为标准事件模型,下游系统无需关心事件数据来源。
\\n关键特征提取:解析并提取账号主体、业务类型、时间等核心元数据特征。
\\n策略引擎是整个流程画布的“智慧大脑”,随着使用的业务方越来越多,系统面临着三多问题:“策略多”、“事件多”、“流量多”,如何让这些海量的流量高效的在正确的时间、通过正确的渠道、向正确的人群传递正确的信息成为了一大挑战。
\\n如何解决
\\n通过对入口和出口流量的管控、数据缓存设计、流量漏斗过滤模型等手段保证策略引擎高效稳定运行。
\\n事件层流量接收管控:支持通过MQ、API方式承接事件接入层的实时单用户数据流以及画像、上传名单批量用户数据流处理,并支持队列动态新增、限流调整,根据业务优先级合理分配计算资源。
\\nLoader本地缓存:针对策略信息、事件配置等低频变更、高频读取的核心元数据建立内存索引,保证策略高效筛选,同时也减少了数据库查询。
\\n流量漏斗过滤模型,逐级削减流量:大部分流量会在前置本地内存计算过滤节点中被拦截过滤,依赖数据库查询或更新的过滤节点后置执行,从而有效减少对数据库的请求压力。
\\n动作执行流量调度与管控:
\\n流量调度:根据动作类型、用户类型以及业务优先级等分配不同的执行通道,避免资源竞争。
\\n流量管控:基于下游系统承载能力差异,我们为不同通道配置相应的派发速率,以防止下游系统过载崩溃。
\\n补偿重试:对于下游网络抖动、接口超时等临时故障,系统支持自动重试,并提供阶梯式重试补偿机制。
\\n随着公司业务量的增长,接入的流量和策略以及使用方日益增多,线上经常会遇到两个问题:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n问题1:某策略命中流量为什么不符合预期?
|
---|
问题2: 某用户为什么没命中策略? |
---|
在项目初期开发面对此类问题时,可用的手段相对有限。开发主要依赖日志系统,通过关键字搜索相关日志来分析问题。然而,这一过程可能非常耗时,并且大多数情况下,问题并非源于系统本身,而是由于策略中某些条件配置错误所致。这种情况导致开发人员需要投入大量时间进行排查和定位,严重影响了工作效率。
\\n如何解决
\\n我们对系统的过滤节点进行了全面梳理和分类,并设计了一套标准化的排障信息埋点方案。通过策略和单次请求两个维度构建可视化工具,实现快速、直观的问题定位辅助。
\\n\\n单次请求排障
运营、产品或测试开发人员可以通过账号、traceId、策略和时间等信息,快速定位用户在某个策略中被过滤的原因。
\\n策略流量分析
\\n利用排障工具收集的埋点数据,我们通过聚合计算生成多个指标,并利用可视化流量分析工具快速识别策略流量在各节点的拦截情况。
\\n历经几年深耕与实践,流程画布已覆盖多个运营业务线,接入各种事件100+,全平台月均活动创建数2300+,日常在线运行活动数500+,已成为货拉拉业务增长的重要营销工具。在数字化进程加速的当下,伴随着大模型的快速发展,我们也将探索如何借助大模型能力辅助提升策略效果,同时将拓展更多新的业务场景接入,并不断提升系统能力,促进公司业务的持续增长。
","description":"引言 随着货拉拉用户和司机规模以及订单量的不断增长,运营对营销工具的要求不断提升,急需一个能覆盖用户和司机生命周期、全渠道触达、丰富奖励发放等能力的营销工具,流程画布因此应运而生。流程画布以数据驱动,支持多策略组合的灵活配置,并整合了丰富的营销投放能力,通过精准的用户定位、恰当的时机选择、合适的触达方式以及贴切的内容传递,帮助业务实现用户和司机规模、订单量的持续增长,从而实现流量最大化的价值转化。接下来将为大家介绍营销平台流程画布的建设与实践。\\n\\n一、什么是流程画布?\\n\\n货拉拉流程画布平台是以“营销策略引擎”为核心,基于用户行为驱动的一站式自动化营销工具…","guid":"https://juejin.cn/post/7491927782764691490","author":"货拉拉技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T07:34:31.467Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ae8e32608cf4e71a6740df19eb6f08f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=AP3YrD9bXchkKV1opFAZHOKbDKI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/22597af2d95e4d12807bab4497aab02a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=bed3dmIfsiQ%2FWrWFC6NdzWhIYpw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd7a476880994408b457e7a713426001~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=QI6GUkNlI4cvJmXvuKc0TK8idJY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5a1b7d62184e4449800980afb92639cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=XxyloTRvjKtKC6AXyCUyjnLp09I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/824b07a0d85e41cda9b81697bed8c24f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=v3T12ucNlVKvGqLac5GZbSkPRMM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a19222df44f4ed2854e727c066e21c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=MIUp4NdQ%2BVcDCdJhPstWGDrvsHg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/75346bd268734e2ca6d0a343fb249829~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=r%2B9Xdr1%2FcImJ9%2BXRreMmaRXMJKE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a35b13a26108449ca031d3930f8b72a4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=F5qe2StATECliw%2BKoq2xS1qdDcw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0e98d875143645b7b4452fa50a5c95d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=cKjROPTlC%2FMBx9RauBpVHAciC1Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e63324838ee4c489bb911a2c6725f89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=SRvTfiNCv7dfYHcq7r5GP4vjMNE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e75b156d0c5f4bf0bf072de91a6ebe5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=Xj4JpcDDPR0hUTSZDN3hmMnvSlM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/db7b2b3ef1e641f0b03e616500aaf21b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=8QBiYjXkXz72daO%2B%2BdCTGXHv6%2F8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c45bb0773664062943c31c4ea0dbcde~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=FEfPL7wXZp3Zd%2BC%2FTznixPHcdt8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b16fafe54f7c41d6a779f93d9b28f516~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=qR1okHssKI9lFc6gIMOJd9ijIE4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d81ae81053a54285858dc58476cee947~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=S1TidfZ6HwfBnYj%2BnIiVtiYyrHI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dcc5df6b050a4595acec213ce9e8db33~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=nc3r1EPjSAxPGk1U9z1BQK08orQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/84570574210c43289c5f0e74026489c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744961671&x-signature=k5BYq2vzNW00QjrZLaGH4X%2BvFHE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"小小的改动,竟然效率提高了1000倍","url":"https://juejin.cn/post/7491672283413757988","content":"小小的改动,竟然让SQL 效率提高了1000倍🚀
\\n今天又是被国产环境教育的一天😭!!!!!
\\n同样的SQL
同样的数据,迁移了服务器,升级了一个小的数据库版本之后,直接导致查询效率慢了100倍😱。
一个分页统计条数的SQL,在遥遥领先的 服务器 执行 40毫秒,在国产环境竟然达到了 惊人的 10s💀!
\\n看到这儿,至少知道怎么优化了,第一 去掉类型的转换,把 is_del = 0 改成数据库的boolea类型,再看一下执行计划
\\n现在是没有类型的转换了,并且 最里面的一层 loop 循环优化成了 Hash join
\\n这时候 执行时间已经是 300ms 左右了。
\\n2.SQL 优化:去掉left join C 表
\\n接下来我们自定义一个 COUNT SQL 去掉C表的连接, 覆盖框架里面自动生成COunt sql 这样执行时间 就缩短到了 10ms 左右了。
\\n\\n\\n因为 left join C 没有对C表进行任何过滤,并且不会存在一对多的情况,所以直接去掉C表的统计。
\\n
本篇文章 通过Explain 分析SQL 的执行计划,定位到了SQL 执行缓慢的原因。 然后通过 覆盖 count 自动生成的SQL,从来减少连表查询。 最后将SQL的查询时间 从 10s 优化到了10ms,提升了1000倍🚀
\\n\\n","description":"前言 小小的改动,竟然让SQL 效率提高了1000倍🚀\\n\\n今天又是被国产环境教育的一天😭!!!!!\\n\\n同样的SQL同样的数据,迁移了服务器,升级了一个小的数据库版本之后,直接导致查询效率慢了100倍😱。\\n\\n一个分页统计条数的SQL,在遥遥领先的 服务器 执行 40毫秒,在国产环境竟然达到了 惊人的 10s💀!\\n\\nSQL 优化过程\\n执行计划分析\\n执行计划不一样,国产环境高一个小版本的数据库,查询SQL 时做了三次嵌套,并且存在类型转换\\n\\n瑶瑶领先环境的执行计划是这样的,明显SQL 进行了优化 left join 的C 表直接去掉了…","guid":"https://juejin.cn/post/7491672283413757988","author":"提前退休的java猿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T06:27:05.200Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d4d2829dd9b9424dac784d0090779665~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o-Q5YmN6YCA5LyR55qEamF2YeeMvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744957625&x-signature=%2B2wHn2BcEIOKv8nVdofHXfP0LsQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dba03995cf404b968cb0596dc60ac2ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o-Q5YmN6YCA5LyR55qEamF2YeeMvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744957625&x-signature=qlakUQzP0lKD1cw3CjpwuVGzDfE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c270328a2dcf48728eb10a51fb0a6c7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o-Q5YmN6YCA5LyR55qEamF2YeeMvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744957625&x-signature=Vz7lfzlIAZxFuE9u1JYRgAO3iQU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8079dddf4954e3e923f77eca6fe6208~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o-Q5YmN6YCA5LyR55qEamF2YeeMvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744957625&x-signature=aDfTm%2BGAuFBSvYnS7Ivl331SOXg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","数据库"],"attachments":null,"extra":null,"language":null},{"title":"MySQL---只操作同一条记录,也会发生死锁吗?","url":"https://juejin.cn/post/7491596953407668274","content":"我想不通的是,一个小的版本,居然执行计划相差这么大. 同时 数据量也不大,估计CPU 和 IO 的国产环境效率很低👎
\\n
\\n推荐阅读:【hash join】
\\n推荐阅读:【类型转换之谜】
会
\\n因为数据库的锁锁的是索引,并不是记录。
\\n当我们在事务中,更新一条记录的时候,如果用到普通索引作为条件,那么会先获取普通索引的锁,然后再尝试获取主键索引的锁。
\\n那么这个时候,如果刚好有一个线程,已经拿到了这条记录的主键索引的锁后,同时尝试在该事务中去拿该记录的普通索引的锁。
\\n这时候就会发生死锁。
\\n\\nupdate my_table set name = \'aska-2\',age = 22 where name = \\"aska\\";\\n\\n
\\n这个SQL会先对name加锁, 然后再回表对id加锁。
\\n\\nselect * from my_table where id = 15 for update;\\n\\n\\nupdate my_table set age = 33 where name like \\"aska%\\";\\n
\\n以上SQL,会先获取主键的锁,然后再获取name的锁。
\\n为了避免这种死锁情况的发生,可以在应用程序中设置一个规定的索引获取顺序,例如,只能按照主键索引->普通索引的顺序获取锁,这样就可以避免不同的线程出现获取不同顺序锁的情况,进而避免死锁的发生(靠SQL保证)。
","description":"✅MySQL只操作同一条记录,也会发生死锁吗? 会\\n\\n因为数据库的锁锁的是索引,并不是记录。\\n\\n当我们在事务中,更新一条记录的时候,如果用到普通索引作为条件,那么会先获取普通索引的锁,然后再尝试获取主键索引的锁。\\n\\n那么这个时候,如果刚好有一个线程,已经拿到了这条记录的主键索引的锁后,同时尝试在该事务中去拿该记录的普通索引的锁。\\n\\n这时候就会发生死锁。\\n\\n案例\\n\\nupdate my_table set name = \'aska-2\',age = 22 where name = \\"aska\\";\\n\\n\\n\\n这个SQL会先对name加锁, 然后再回表对id加锁。\\n\\n\\nselec…","guid":"https://juejin.cn/post/7491596953407668274","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T01:25:39.549Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"jvm---YoungGC和FullGC的触发条件是什么","url":"https://juejin.cn/post/7491596953407635506","content":"YoungGC的触发条件比较简单,那就是当年轻代中的eden区分配满的时候就会触发。
\\nFullGC的触发条件比较复杂也比较多,主要以下几种:
\\n老年代空间不足
\\n空间分配担保失败
\\n永久代空间不足
\\n代码中执行System.gc()
\\n空间分配担保机制
\\n如果Survivor区域的空间不够,就要分配给老年代,也就是说,老年代起到了一个兜底的作用。但是,老年代也是可能空间不足的。所以,在这个过程中就需要做一次空间分配担保(CMS)
\\n在每一次执行YoungGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
\\n如果大于,那么说明本次Young GC是安全的。
\\n如果小于,那么虚拟机会查看\ufeffHandlePromotionFailure\ufeff 参数设置的值判断是否允许担保失败。如果值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小(一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考)。如果大于,则尝试进行一次YoungGC,但这次YoungGC依然是有风险的;如果小于,或者HandlePromotionFailure=false,则会直接触发一次Full GC。
\\n但是,需要注意的是\ufeffHandlePromotionFailure\ufeff这个参数,在JDK 7中就不再支持了:
\\n在JDK代码中,移除了这个参数的判断,也就是说,在后续的版本中, 只要检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则认为担保成功。
\\n但是需要注意的是,担保的结果可能成功,也可能失败。所以,在YoungGC的复制阶段执行之后,会发生以下三种情况:
\\n\\n\\n🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
\\n
晚上好啊,同学们,今天我们继续来聊注解,本期的干货绝对会让你们大饱眼福,特别是那些天天和数据打交道的小伙伴们!👋 先采访下大家,是不是总会遇到需要导出数据成 Excel 的需求?特别是财务数据、订单报表、用户数据等,Excel 可以说是日常工作中的“神器”之一!然而,手动处理 Excel 导出不仅耗时耗力,还可能出错。于是乎,@Excel
注解,天空一声巨响,它就闪亮登场了,拯救被 Excel 折磨的我们!于是乎,我们便不再被这些导入导出的功能折磨的没有自己的摸鱼时间✨
@Excel
注解,它是在 Java 开发中常用于快速实现数据的 Excel 导出、导入的注解,使用起来特别方便。今天我们就来一起深入了解 @Excel
的使用技巧,通过实例演示如何在项目中优雅地使用它,从而让你的数据一键导出成表格,提升工作效率!💪
@Excel
?它的作用是什么?@Excel
注解的基本用法@Excel
?它的作用是什么? 在学习一个新知识点之前,我们必须要了解它是做什么的,有什么用,怎么用等这些基本功。对于@Excel
,它是 Java 项目中常见的一个数据导出注解,通常与一些 Excel 处理库(如 EasyPOI、Apache POI)结合使用。它可以帮我们轻松实现对象数据到 Excel 表格的自动映射,而不需要手写复杂的格式转换代码。只需要在对象属性上加上 @Excel
注解,设置相关属性,系统就会自动将数据生成指定格式的 Excel 文件,是不是很方便?👍
@Excel
的主要功能是:
用 @Excel
注解,不仅可以减少手动导出 Excel 的代码量,而且可以灵活控制每个字段在 Excel 表中的展示效果,真是 Excel 表格导出领域的“小帮手”!谁用谁爱系列。
@Excel
注解的基本用法 @Excel
注解的基础配置非常简单,主要的核心属性有:
假设我们有一个简单的用户类 User
,包含姓名、年龄、邮箱和注册时间字段。我们希望将这些字段导出到 Excel 表格中,可以用 @Excel
注解来实现:
import cn.afterturn.easypoi.excel.annotation.Excel;\\nimport java.util.Date;\\n\\npublic class User {\\n\\n @Excel(name = \\"姓名\\", width = 20, orderNum = \\"0\\")\\n private String name;\\n\\n @Excel(name = \\"年龄\\", width = 10, orderNum = \\"1\\")\\n private Integer age;\\n\\n @Excel(name = \\"邮箱\\", width = 30, orderNum = \\"2\\")\\n private String email;\\n\\n @Excel(name = \\"注册时间\\", width = 20, orderNum = \\"3\\", format = \\"yyyy-MM-dd HH:mm:ss\\")\\n private Date registrationDate;\\n\\n // Getter 和 Setter\\n}\\n
\\n 在如上这个例子中,我们通过 @Excel
注解的 name
、width
、orderNum
、format
属性分别指定了 Excel 表格中每一列的列名、列宽、显示顺序和时间格式。这样一来,只需要调用库的导出方法,就可以将 User
对象转换为 Excel 表格。
为了更好地理解 @Excel
注解的用法,我们来完整演示一下如何在 Spring Boot 项目中实现 Excel 导出。
首先,在 Maven 项目的 pom.xml
文件中引入 EasyPOI 依赖:
<dependency>\\n <groupId>cn.afterturn</groupId>\\n <artifactId>easypoi</artifactId>\\n <version>4.1.0</version>\\n</dependency>\\n
\\n@Excel
注解 创建 User
类,并在需要导出的字段上加上 @Excel
注解,如上例所示。
编写一个导出服务,将数据导出到 Excel 文件中:
\\n定义一个导出接口。
\\n //Excel导出\\n public List<ExportProjectReviewVo> export(ProjectQueryParam projectQueryParam);\\n
\\n导出接口实现类。
\\n @Override\\n public List<ExportProjectReviewVo> export(ProjectQueryParam projectQueryParam) {\\n List<ExportProjectReviewVo> projectReviews = projectReviewMapper.export(projectQueryParam);\\n return projectReviews;\\n }\\n
\\n最后,在 Controller 中创建一个导出接口,用户可以通过访问此接口直接下载 Excel 文件:
\\n@RestController\\npublic class ProjectReviewController {\\n\\n private final IProjectReviewService projectReviewService;\\n\\n @PostMapping(\\"/export\\")\\n public void export(HttpServletResponse response, ProjectQueryParam projectQueryParam) {\\n List<ExportProjectReviewVo> exportData = projectReviewService.export(projectQueryParam);\\n ExcelUtil<ExportProjectReviewVo> util = new ExcelUtil<>(ExportProjectReviewVo.class);\\n util.exportExcel(response, exportData, \\"客户项目信息数据导出_\\" + DateUtils.dateTemplate());\\n }\\n}\\n
\\n 访问 /export-users
接口,即可下载包含用户信息的 Excel 表格了!
然后我们通过模拟访问Postman,进行请求导出接口,大家请看。
\\n假如我们有多个用户列表,想在一个 Excel 文件中分别生成多个工作表,可以这样实现:
\\npublic void exportMultipleUserLists(HttpServletResponse response, Map<String, List<User>> userGroups) throws Exception {\\n Workbook workbook = ExcelExportUtil.exportExcel(userGroups.entrySet().stream()\\n .map(entry -> new ExportParams(entry.getKey(), entry.getKey()))\\n .collect(Collectors.toList()), User.class, userGroups.values().stream().flatMap(Collection::stream).collect(Collectors.toList()));\\n \\n response.setContentType(\\"application/vnd.ms-excel\\");\\n response.setHeader(\\"Content-Disposition\\", \\"attachment;filename=MultipleUserLists.xls\\");\\n workbook.write(response.getOutputStream());\\n workbook.close();\\n}\\n
\\n @Excel
注解还支持许多样式配置,可以设置背景颜色、字体大小等。例如,可以通过 type
和 isWrap
属性设置单元格的文本类型和自动换行。
@Excel
注解,既然学习它,那必须把它的实现原理搞懂。它的底层实现,其实是基于 POI 库的封装。EasyPOI 通过注解扫描自动读取字段信息,将每个字段的值映射到 Excel 单元格中。当导出时,EasyPOI 会创建 Excel 工作簿,逐行填充数据并处理格式转换。相对于手动编码生成 Excel 文件,这种注解方式显得更直观、易于维护,减少了大量重复代码。
简言之,@Excel
注解往往是与一些第三方库结合使用,如 easyexcel
,apache poi
等。以下我将详细地分析 @Excel
注解的底层实现原理,探讨其如何简化操作并提升性能。
@Excel
注解的基本作用 @Excel
注解的主要功能是标注需要处理的字段,并且可以为其指定一些属性,如Excel列名、是否为必填项等。这个注解通常会应用于POJO类的字段,以指定该字段在Excel导入/导出时的映射关系。
假设我们使用了 easyexcel
这样的库,@Excel
注解常见的用法如下:
import com.alibaba.excel.annotation.ExcelProperty;\\n\\npublic class Person {\\n @ExcelProperty(\\"姓名\\")\\n private String name;\\n \\n @ExcelProperty(\\"年龄\\")\\n private Integer age;\\n\\n // Getter 和 Setter 方法\\n}\\n
\\n 在这个例子中,@ExcelProperty
注解指示每个字段对应 Excel 表格中的列名。easyexcel
在执行数据导入或导出时,会根据这些注解映射字段与 Excel 表格之间的关系。
@Excel
注解的底层实现 @Excel
注解的实现过程通常由反射机制和字节码操作来完成。当你使用该注解时,底层的处理逻辑会在程序运行时读取类的注解信息,并根据这些信息进行相应的 Excel 读写操作。
以下是 @Excel
注解的底层实现步骤,仅供参考:
类和字段扫描:\\n当 easyexcel
这样的库启动时,会扫描目标类及其字段,查找标记了 @Excel
或 @ExcelProperty
注解的字段。这个过程通常使用反射机制来动态获取字段及注解信息。
字段与 Excel 列的映射:\\n通过反射获取字段的注解,easyexcel
会将字段与 Excel 中的列进行映射。@ExcelProperty
注解上的值指定了 Excel 文件中列的名称或索引。
数据转换:\\n在导出数据时,库会根据注解信息读取 POJO 对象的字段,并将数据格式化为 Excel 表格的相应单元格内容。在导入数据时,Excel 文件的内容会映射到 Java 对象的字段上,进行数据类型转换。
\\n动态生成 Excel 文件:\\n使用像 easyexcel
这样的库时,底层会使用 Apache POI
或类似的框架来生成 Excel 文件。这个过程包括设置工作簿、创建单元格、填充数据等。
底层代码中,会通过反射机制获取类中所有字段的注解信息。例如:
\\nimport java.lang.reflect.Field;\\n\\npublic void processAnnotations(Class<?> clazz) {\\n for (Field field : clazz.getDeclaredFields()) {\\n if (field.isAnnotationPresent(ExcelProperty.class)) {\\n ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);\\n System.out.println(\\"Field: \\" + field.getName() + \\" -> Excel Column: \\" + excelProperty.value());\\n }\\n }\\n}\\n
\\n 上面的代码展示了如何通过反射获取字段上的 @ExcelProperty
注解,并打印出字段与 Excel 列之间的映射关系。
easyexcel
库的核心工作之一就是根据注解信息动态生成 Excel 文件。对于每一行数据,它会逐个字段地填充对应的 Excel 列。底层的实现使用了 ExcelWriter
和 ExcelReader
类来分别处理数据的导出和导入。
例如,导出操作的核心逻辑可能如下:
\\nExcelWriter writer = EasyExcel.write(outputStream).head(head).build();\\nSheet sheet = new Sheet(1, 0, Person.class);\\nwriter.write(data, sheet);\\nwriter.finish();\\n
\\n 这段代码中,head
部分是根据 @ExcelProperty
注解构建的列头,而数据部分则是根据 POJO 类动态生成的。
@Excel
注解和相关库的一个重要特点就是性能优化,尤其是对大数据量的导入导出具有很好的支持。下面是几种常见的性能优化方法:
内存优化:\\neasyexcel
采用了流式处理方式(streaming
),即一次只读取或写入一行数据,避免了将整个数据加载到内存中的问题。这样在处理大数据时可以大幅度降低内存消耗。
批量处理:\\n在导出时,可以通过批量数据的方式一次性写入 Excel,减少磁盘 IO 操作的频繁次数,从而提升性能。
\\n异步处理:\\n在一些场景中,Excel 的生成可能非常耗时。easyexcel
等库支持异步处理,可以将导入或导出的操作放入后台线程进行。
简言之,针对@Excel
注解,它底层实现原理依赖于反射、字节码处理和流式数据处理机制。通过这些手段,@Excel
注解能够简化 Excel 文件的导入导出操作,同时提供较高的性能,尤其是在处理大数据量时。其核心思路是在运行时动态生成 Excel 文件,并且通过注解映射 POJO 类与 Excel 列之间的关系,避免了传统手动处理的繁琐。不知道我讲到这里,大家能不能掌握?
控制数据量:Excel 适合导出适量数据,不适合海量数据。建议数据量过大时分页导出,避免文件过大难以处理。
\\n自定义错误提示:在实际操作中,可能会遇到字段与 Excel 文件列名不匹配的问题,可以通过 @Excel
注解的 name
属性自定义列名,确保字段准确映射。
合理使用样式:虽然 @Excel
提供了丰富的样式设置,但样式越多处理速度越慢,特别是在批量导出时,建议控制样式数量,提高导出效率。
异常处理:在导出过程中,可能会因为字段为空、格式不匹配等原因导致导出失败,建议捕获异常并提供友好的错误提示,避免用户困惑。
\\n @Excel
注解,它是一个强大的注解工具,帮我们大大简化了 Excel 数据导入导出功能。它不仅能自动将对象映射到 Excel 表格,还支持多样化的样式配置。通过合理使用 @Excel
注解和一些数据导出库的结合,我们可以轻松将数据从数据库转化为 Excel 文件,解决了繁琐的手动导出问题。
希望通过这篇文章,大家能对 @Excel
注解有更深入的了解,快把它应用到项目中,让 Excel 导出变得更加轻松和优雅吧!而不是自己动手实操Excel表格🎉
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主&最具价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。
\\n-End-
","description":"🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!! 📜 前言\\n\\n 晚上好啊,同学们,今天我们继续来聊注解,本期的干货绝对会让你们大饱眼福,特别是那些天天和数据打交道的小伙伴们!👋 先采访下大家,是不是总会遇到需要导出数据成 Excel 的需求?特别是财务数据、订单报表、用户数据等,Excel 可以说是日常工作中的“神器”之一!然而,手动处理 Excel 导出不仅耗时耗力,还可能出错。于是乎,@Excel 注解…","guid":"https://juejin.cn/post/7491563108474486835","author":"bug菌","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T12:25:32.885Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80f9a8d56495469389726d8620a49c6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=Ig7C0OQ2zM%2BlQ985OsQhgWzRz6E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c798c1d5dbd149f4a06f66bac946a79e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=vbVLwhBlCIuH9SakP%2BgM4ozXVCY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49ff189ed406409a974ea4831d12dd78~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=yKPTzVGqiT39nwKjgPgke1aXBYQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a03a6a6ec3ce4cfaa1417396ab9dca94~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=MT1xLrYIw8joX4PT0y8eq%2Bnrpc4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ad8012d7ad054acea23bd5f85e1a3edb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=34SnyozUT8heWSj6SnUyd9vul%2Fo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/162b131277fc4f50a844077d8ac86aea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=RokKjVzzZwMQzjiTGzs%2BHsQjfEs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/39abc25bb65b41a69666d4fc01921c4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=v6K5JNAr4QMROc60ppYrDivOGDs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/55ad95eb942e4c8b88dc230188907873~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=4RDgGiuj5%2BR1ByRsb0pBo3h3KF0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/23085d76b3534c74bec85fe461c97ccb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=a24IZfReTVzU8x%2F09Z%2BBNMSbCis%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ff0a617d94d0491c9fc344f84f583654~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=UkBfaQVYWm8lZw1XAKFgBvmpeRc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9bbfb7920e44c20afc3354d184843ff~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=bvbnhzNtTiDt1MBy8y2LtpcJ1XM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0f02433e7183441488c6d8913c2b258d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1745497662&x-signature=99P4PuA50FEM9aD4pxOc3wY0j00%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Spring"],"attachments":null,"extra":null,"language":null},{"title":"我们做了可能是第一个支持了MCP服务的ERP/WMS系统?","url":"https://juejin.cn/post/7491481176224350220","content":"我们之前开发了一个 SPMS 的智能生产管理系统,其中包含了很多 资源管理(ERP)、库存管理(WMS)、生产管理(MES) 等功能,前段时间 MCP 在国内火了起来,我们也顺带的给这个开源管理系统加上了几个 MCP 工具的功能,可以参考之前我们发布的文章:
\\n\\n\\n\\n我们和目前主流的 STDIO 模式的 MCP 工具不一样,我们使用的是 MCP 的 SSE/HTTP 模式,所以本文不涉及 STDIO 模式的 MCP 的实现和内容。
\\n
今天我们来聊聊这些实现中的细节:
\\n我们在 MCP 的 SSE/HTTP 模式下,MCP 服务端会主动向客户端推送消息,所以 MCP 服务端需要实现一个 SSE/HTTP 的端点。这个端点只是在告诉客户端,我们提供了这么个接入的地址,SSE在初次连接时,会推送一个 POST 接受客户端请求的地址。
\\n因为我们使用的是 SpringBoot ,所以 MCP 服务端只需要实现一个 Controller 即可。
\\n我们在这个控制器中做了这些设计:
\\n用于客户端连接,并推送初始化的一条信息告诉客户端,我们可以通过什么地址接受消息。
\\n用于接受客户端的数据,处理完毕之后的数据通过上面的 SSE 端点推送给客户端。
\\n这个 MAP 的作用是,用来存储客户端的 AccessToken,这个 AccessToken 是客户端在当前系统中申请的私人令牌,用于身份验证以及功能的授权。
\\n如上,我们可以创建一个身份令牌,用于客户端侧的使用。
\\n我们可以和上面一样,为指定的用户授权某些 MCP 工具的使用权限。
\\n这里我们配合之前设计的拦截器,实现了身份验证和权限校验等。
\\n而具体的细节,被我们藏在了 McpService 这个类里面:
\\n其中,我们完成了下面这些部分的功能:
\\n我们通过初始化的时候调用 scanMcpMethods
进行扫描标记了 @McpMethod
注解的方法,并注册到权限列表中以提供给上面身份令牌的用户所在角色授权使用。
同时,扫描到的工具列表也将在客户端调用 tools/list
时放回给支持 MCP 的客户端。
我们通过传入的 methodName
从上面的 MCP工具中 获取到工具,然后再通过反射去调用这个工具当时被扫描时的方法,执行方法后返回结果给到客户端。
当然,在调用工具前,我们通过对身份令牌的权限做验证来保证所有的 MCP工具 都在合理授权的情况下被调用。
\\n因为这里是 POST 请求过来的,MCP协议 要求结果不通过 response
返回,而是通过 SSE 推送消息给客户端。
所以我们实现了这些 推送消息 的功能。
\\n接下来,我们演示通过 CherryStudio 配置这个 MCP服务
\\n如图,我们在 CherryStudio 中添加了 MCP 服务器,并配置了 SSE 模式下的 MCP 服务器的地址。
\\n\\n\\n这个地址是从我们后台系统中获取的:
\\n
其中,身份令牌就是上面提到的。
\\n我们先给大家看看这个创建采购单的后端实现吧:
\\n很简单。
\\n接下来,我们通过 创建采购单 这个工具来测试一下 MCP 服务器是否正常。
\\n\\n\\n不好意思啊兄弟们,不是我不采购,是没权限。
\\n
还行吗兄弟们?
\\n所有的代码都是开源的,可以参考这个开源项目:
\\nGithub: github.com/s-pms
\\n今天我们分享了在这个开源后台管理系统上实现 MCP 的简单过程说明,也演示了实现之后的效果。
\\n虽然目前还需要依赖各种 AI 客户端来实现,但我相信,在不久的将来,通过 Siri
小爱同学
你好宝马
都能动动嘴完成这些事情。
今天到这,Bye.
","description":"一、前言 我们之前开发了一个 SPMS 的智能生产管理系统,其中包含了很多 资源管理(ERP)、库存管理(WMS)、生产管理(MES) 等功能,前段时间 MCP 在国内火了起来,我们也顺带的给这个开源管理系统加上了几个 MCP 工具的功能,可以参考之前我们发布的文章:\\n\\nMCP 很火,来看看我们直接给后台管理系统上一个 MCP?\\n\\n我们和目前主流的 STDIO 模式的 MCP 工具不一样,我们使用的是 MCP 的 SSE/HTTP 模式,所以本文不涉及 STDIO 模式的 MCP 的实现和内容。\\n\\n今天我们来聊聊这些实现中的细节:\\n\\n二、实现思路\\n\\n我们在 M…","guid":"https://juejin.cn/post/7491481176224350220","author":"Hamm","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T09:50:14.905Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/95def4ebdee44149b0891fb88c3f0902~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=UOI9tCQYrnFTBC2FBlK10fMYwhY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/76421570ff4d4383b7f1e1a80e78d6cc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=ds9gkCdZJfQGrw1XE%2BHoXaXCpkk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b51b5f87d534bda80745b3fca1a1814~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=00IDwt%2B78Sb%2B97xeg7dBoGpQuHo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/01fa8ab7a4294c2889509e658ad637b0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=GBABePvWHoTo%2B%2B9OmhaEgEpgC8I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b5aef1f9620f420b921d2b417db4d923~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=ZCm9%2BwodKJIulg%2FWL1X3mvns65E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/320356a0e4f64c5e80607aaca25a46c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=TITK%2Bv7hoT8q1x7Gf7lPXVs%2BT9s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0911b3c65ec24e9f8134d51ee517aa6d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=lgn3bc7Y%2F0hsKUhq68B%2BG2%2BcNt4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af1f3a7255f343779a8d27a69318c674~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=DHfq9u9q7sZAN7rJz1ZJ71tuawA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7f231ddbc6ac4570a7f18d044c99db97~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1745487932&x-signature=B3xrsa0eAmPKRiD2VKvVgVY9iYc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","人工智能","MCP"],"attachments":null,"extra":null,"language":null},{"title":"经理突然问我为什么BigDecimal可以不丢失精度?我表示...😨","url":"https://juejin.cn/post/7491248598316580914","content":"\\n\\n🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
\\n
哈咯啊,同学们!今天我要分享一个比较有趣的知识点,恐怕大家基本都回答的不够全面,这也是前天我经理突然对我灵魂来上的重重一击,还好我底子厚,经得住考验,如果换做是你,你能如何满分回答领导提出的拷问?
\\n 或许你们曾经也有过因为浮点数的精度问题抓狂?比如说,当你愉快地写下 0.1 + 0.2
时,满心期待的结果为 0.3
,结果程序却冷漠地给你输出 0.30000000000000004
,直接给你的程序人生当头一棒!😤,这答案连我不懂算数的太奶看了都能直接给晕过去!
类似的问题在计算机中屡见不鲜,尤其是在金融计算、科学计算等领域,如果计算结果稍有偏差,可能就会引发无法挽回的后果。于是,在Java 语言 中,噔噔噔 --- BigDecimal,由此横空出世,它不但精准得让人放心,还能让你远离那些“不靠谱”的浮点数。
\\n今天这篇文章,bug菌我的目的就是带着大家能够全面了解 BigDecimal 的魔法!既有专业分析,也有生动案例,帮你一文搞懂:为什么 BigDecimal 可以不丢失精度?并且如何用好它?它到底有多强大?,让你秒上手,就是我今天写此文的最终目的,而且让你无论过去多久,依旧能够回忆起,它 - BigDecimal 为什么不会丢失精度?
\\n想必这个问题,浮点数的精度问题是每个开发者都无法绕过的坑。在搞懂 BigDecimal 之前,我们必须先弄清楚一个问题:浮点数到底为什么会丢失精度?
\\n 在计算机中,浮点数(float
和 double
)的存储基于 IEEE 754 标准。它将一个数字拆分成三个部分存储:
用通俗点的话来说,浮点数在计算机中是用 二进制科学计数法 表示的,比如:
\\n0.1 = 1.10011001100110011...(无限循环) × 2^-4\\n
\\n但是,由于计算机存储空间有限,无法表示无限循环小数,因此只能取一个近似值。这就导致:
\\n0.1 在计算机中实际存储的并不是精确的 0.1,而是一个接近 0.1 的值。\\n
\\n 当你进行类似 0.1 + 0.2
的操作时,这些近似值累积起来,就会产生微小误差,比如:
0.1 + 0.2 = 0.30000000000000004\\n
\\nBigDecimal 的核心优势在于它使用 字符串存储数字,而不是像浮点数那样用二进制表示。它的设计使得它可以避免浮点数的精度丢失问题。那么它到底是怎么做到的?让我们通过 BigDecimal 的源码 来一探究竟!🚀
\\n以下是 BigDecimal
的部分构造方法的源码:
// 基于字符串的构造方法\\npublic BigDecimal(String val) {\\n // 校验输入的字符串是否为有效数字\\n this(new BigDecimalParser(val).bigInteger, BigDecimalParser.scale, 0);\\n}\\n\\n// 基于 double 的构造方法\\npublic BigDecimal(double val) {\\n this(Double.toString(val));\\n}\\n
\\n 从源码中可以看出,BigDecimal 最推荐的构造方式是通过字符串,因为字符串可以确保输入的数值精确无误。相比之下,如果你用 double
初始化 BigDecimal,其实底层仍然会把 double
转换为字符串(通过 Double.toString()
),再进一步存储。
这就是为什么我们总是强调:初始化 BigDecimal 时,最好用字符串而不是浮点数,因为直接传递浮点数容易把精度问题带入 BigDecimal。
\\n深入到 BigDecimal 的底层,我们会发现它的核心存储结构主要有两个部分:
\\n下面是 BigDecimal 的重要字段源码:
\\n// BigDecimal 核心字段\\nprivate final BigInteger intVal; // 用于存储大整数的核心字段\\nprivate final int scale; // 精度:小数点后位数\\nprivate transient int precision; // 用于缓存精度的字段\\n
\\n具体来看:
\\nintVal
使用 BigInteger 来存储数字,意味着它可以支持任意大小的整数。scale
用来表示小数点后精确的位数。例如 new BigDecimal(\\"1.23\\")
的 scale
为 2。precision
是一个临时缓存字段,用来加快计算速度。BigDecimal bigDecimal = new BigDecimal(\\"123.456\\");\\nSystem.out.println(bigDecimal.unscaledValue()); // 输出:123456\\nSystem.out.println(bigDecimal.scale()); // 输出:3\\n
\\n在这个例子中:
\\nintVal
实际存储的是 123456,即原数字去掉小数点后的整数值。scale
表示小数点后有 3 位。示例运行结果展示如下:
\\n通过这种设计,BigDecimal 能够准确地存储任意大小和任意精度的数字,而不会出现浮点数的近似值问题。
\\n以下是 BigDecimal
加法的核心源码:
public BigDecimal add(BigDecimal augend) {\\n // 如果两个数的 scale 相同,直接相加\\n if (scale == augend.scale) {\\n return new BigDecimal(intVal.add(augend.intVal), scale);\\n }\\n // 如果 scale 不同,调整 scale 后再相加\\n BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));\\n BigInteger result = scaledIntVal.add(augend.intVal);\\n return new BigDecimal(result, Math.max(this.scale, augend.scale));\\n}\\n
\\n如下是对上的完整源码解析,希望能够帮到大家加深对BigDecimal 理解。
\\npublic BigDecimal add(BigDecimal augend) {\\n // 如果两个数的 scale 相同,直接相加\\n if (scale == augend.scale) {\\n return new BigDecimal(intVal.add(augend.intVal), scale);\\n }\\n // 如果 scale 不同,调整 scale 后再相加\\n BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));\\n BigInteger result = scaledIntVal.add(augend.intVal);\\n return new BigDecimal(result, Math.max(this.scale, augend.scale));\\n}\\n
\\nadd
BigDecimal
类的一个方法,用于实现两个 BigDecimal
对象的加法运算。BigDecimal augend
BigDecimal
类型的参数表示参与加法运算的另一个数。BigDecimal
BigDecimal
对象,表示两个数的和。if (scale == augend.scale) {\\n return new BigDecimal(intVal.add(augend.intVal), scale);\\n}\\n
\\nscale
:BigDecimal
中的 scale
表示小数点后的位数。例如,123.45
的 scale
是 2,123.456
的 scale
是 3。BigDecimal
对象的小数点位数(scale
)相同,可以直接对它们的整数部分(intVal
)进行加法运算。\\nintVal
:BigDecimal
内部使用 BigInteger
来存储数值的整数部分。intVal
表示去掉小数点后的整数部分。intVal.add(augend.intVal)
,将两个 BigInteger
直接相加。BigDecimal
对象,其整数部分是加法结果,小数点位数(scale
)保持不变。示例:
\\n假设 this = 123.45
和 augend = 678.90
,它们的 scale
都是 2。
intVal
分别是 12345
和 67890
。12345 + 67890 = 80235
。new BigDecimal(80235, 2)
,即 802.35
。BigInteger scaledIntVal = this.intVal.multiply(BigInteger.TEN.pow(augend.scale - this.scale));\\nBigInteger result = scaledIntVal.add(augend.intVal);\\nreturn new BigDecimal(result, Math.max(this.scale, augend.scale));\\n
\\nBigDecimal
对象的小数点位数(scale
)不同,需要先将它们调整到相同的小数点位数,再进行加法运算。\\naugend.scale - this.scale
:计算两个数的小数点位数差。BigInteger.TEN.pow(augend.scale - this.scale)
:生成一个 10 的幂次方,用于调整小数点位数。例如,如果 augend.scale - this.scale = 2
,则生成 100
。this.intVal.multiply(...)
:将当前对象的整数部分乘以 10 的幂次方,调整小数点位数,使其与被加数的 scale
一致。scaledIntVal.add(augend.intVal)
:将调整后的整数部分与被加数的整数部分相加。Math.max(this.scale, augend.scale)
:选择较大的 scale
作为结果的小数点位数。new BigDecimal(result, ...)
:构造一个新的 BigDecimal
对象,其整数部分是加法结果,小数点位数为较大的 scale
。示例:
\\n假设 this = 123.45
(scale = 2
)和 augend = 67.890
(scale = 3
)。
intVal
分别是 12345
和 67890
。augend.scale - this.scale = 3 - 2 = 1
10.pow(1) = 10
this.intVal.multiply(10) = 12345 * 10 = 123450
123450 + 67890 = 191340
Math.max(this.scale, augend.scale) = Math.max(2, 3) = 3
new BigDecimal(191340, 3)
,即 191.340
。如上源码的核心逻辑是实现两个 BigDecimal
对象的加法运算。它通过以下步骤确保加法的正确性:
scale
):\\nscale
相同,直接对整数部分进行加法运算。scale
不同,将较小 scale
的数调整到较大的 scale
,再进行加法运算。scale
构造新的 BigDecimal
对象。这种方法确保了加法运算的精确性,同时避免了因小数点位数不同导致的计算错误。
\\nBigDecimal
用于处理高精度的数值运算,特别是在金融和科学计算中。通过精确调整小数点位数,确保加法运算的结果精确无误。scale
相同时直接相加,避免了不必要的调整操作,提高了性能。希望这段解析对同学们理解 BigDecimal.add
方法有帮助!如果还有其他问题,欢迎继续提问。
BigDecimal num1 = new BigDecimal(\\"1.23\\");\\nBigDecimal num2 = new BigDecimal(\\"4.567\\");\\nBigDecimal result = num1.add(num2);\\nSystem.out.println(result); // 输出:5.797\\n
\\n示例运行结果展示如下:
\\n接下来,我们通过一个个具体案例来看看如何正确使用 BigDecimal。快来打开你的 IDE,跟着我一起代码实战一起学吧!
\\nimport java.math.BigDecimal;\\nimport java.math.RoundingMode;\\n\\n/**\\n * @author bug菌\\n * @Source 公众号:猿圈奇妙屋\\n */\\npublic class OdMain {\\n\\n public static void main(String[] args) {\\n BigDecimal num1 = new BigDecimal(\\"0.1\\");\\n BigDecimal num2 = new BigDecimal(\\"0.2\\");\\n\\n // 加法\\n BigDecimal sum = num1.add(num2);\\n System.out.println(\\"加法结果:\\" + sum); // 输出:0.3\\n\\n // 减法\\n BigDecimal diff = num1.subtract(num2);\\n System.out.println(\\"减法结果:\\" + diff); // 输出:-0.1\\n\\n // 乘法\\n BigDecimal product = num1.multiply(num2);\\n System.out.println(\\"乘法结果:\\" + product); // 输出:0.02\\n\\n // 除法\\n BigDecimal quotient = num1.divide(num2, 2, RoundingMode.HALF_UP);\\n System.out.println(\\"除法结果:\\" + quotient); // 输出:0.50\\n }\\n}\\n
\\n根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。实际运行结果展示如下:
\\n在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
\\n 如上这段代码演示了 BigDecimal
类在Java中进行高精度算术运算的能力,包括加法、减法、乘法和除法。BigDecimal
是一个用于处理精确浮点数运算的类,特别适用于需要高精度计算的场景,如金融和科学计算。
BigDecimal
对象程序首先创建了两个 BigDecimal
对象:
num1
表示数值 0.1
。num2
表示数值 0.2
。这里使用字符串构造 BigDecimal
对象的原因是为了避免浮点数表示不准确的问题。例如,直接使用 new BigDecimal(0.1)
可能会导致精度问题,因为 0.1
在浮点数表示中是不精确的。而使用字符串构造可以确保数值的精确表示。
程序执行了加法运算:
\\nnum1
和 num2
相加,即 0.1 + 0.2
。0.3
,并打印出来。BigDecimal
的 add
方法用于执行加法运算,它会精确地计算两个数的和,避免了浮点数运算中的精度问题。
程序执行了减法运算:
\\nnum1
减去 num2
,即 0.1 - 0.2
。-0.1
,并打印出来。BigDecimal
的 subtract
方法用于执行减法运算,同样确保了结果的精确性。
程序执行了乘法运算:
\\nnum1
乘以 num2
,即 0.1 * 0.2
。0.02
,并打印出来。BigDecimal
的 multiply
方法用于执行乘法运算,确保了乘法结果的精确性。
程序执行了除法运算:
\\nnum1
除以 num2
,即 0.1 / 0.2
。0.50
,并打印出来。BigDecimal
的 divide
方法用于执行除法运算。它需要指定结果的小数点位数(scale
)和舍入模式(RoundingMode
)。在这个例子中,结果保留两位小数,并使用 RoundingMode.HALF_UP
(四舍五入)作为舍入模式。
精度问题:
\\nBigDecimal
用于处理高精度的浮点数运算,避免了 float
和 double
类型的精度问题。BigDecimal
对象可以确保数值的精确表示。舍入模式:
\\nRoundingMode.HALF_UP
表示四舍五入,是常用的舍入模式之一。方法功能:
\\nadd
:执行加法运算。subtract
:执行减法运算。multiply
:执行乘法运算。divide
:执行除法运算,需要指定结果的小数点位数和舍入模式。如上代码通过 BigDecimal
类展示了如何在Java中进行高精度的算术运算。它涵盖了加法、减法、乘法和除法的基本操作,并通过指定舍入模式确保了除法运算的精确性。程序逻辑清晰,适合初学者学习 BigDecimal
的基本用法。
BigDecimal 提供了 8 种舍入模式,以下是常用的几个:
\\nBigDecimal number = new BigDecimal(\\"10.125\\");\\nBigDecimal rounded = number.setScale(2, RoundingMode.HALF_UP);\\nSystem.out.println(\\"四舍五入结果:\\" + rounded); // 输出:10.13\\n
\\n根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。
\\nBigDecimal a = new BigDecimal(\\"1.23\\");\\nBigDecimal b = new BigDecimal(\\"4.56\\");\\n\\nint result = a.compareTo(b);\\nSystem.out.println(result); // 输出:-1 表示 a < b\\n
\\n根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。
\\n货币计算是 BigDecimal 最经典的应用场景,比如计算商品价格总额:
\\nBigDecimal price = new BigDecimal(\\"19.99\\");\\nBigDecimal quantity = new BigDecimal(\\"3\\");\\nBigDecimal total = price.multiply(quantity).setScale(2, RoundingMode.HALF_UP);\\nSystem.out.println(\\"总价:\\" + total); // 输出:59.97\\n
\\n如下是实际案例运行结果展示:
\\n科学计算中,我们需要对精度要求极高的数值进行运算:
\\nBigDecimal base = new BigDecimal(\\"2\\");\\nBigDecimal exponent = new BigDecimal(\\"10\\");\\nBigDecimal result = base.pow(exponent.intValue());\\nSystem.out.println(\\"2 的 10 次方:\\" + result); // 输出:1024\\n
\\n如下是实际案例运行结果展示:
\\n在统计数据时,BigDecimal 可以避免累计误差:
\\nBigDecimal[] numbers = {\\n new BigDecimal(\\"1.23\\"),\\n new BigDecimal(\\"4.56\\"),\\n new BigDecimal(\\"7.89\\")\\n};\\n\\nBigDecimal sum = BigDecimal.ZERO;\\nfor (BigDecimal num : numbers) {\\n sum = sum.add(num);\\n}\\nSystem.out.println(\\"总和:\\" + sum); // 输出:13.68\\n
\\n如下是实际案例运行结果展示:
\\n BigDecimal num = new BigDecimal(\\"123.45\\");\\n String str = num.toString();\\n System.out.println(str); // 输出:123.45\\n
\\n double val = num.doubleValue();\\n System.out.println(val); // 输出:123.45\\n
\\nBigDecimal num1 = new BigDecimal(\\"10\\");\\nBigDecimal num2 = new BigDecimal(\\"3\\");\\nBigDecimal result = num1.divide(num2, 5, RoundingMode.HALF_UP);\\nSystem.out.println(\\"商:\\" + result); // 输出:3.33333\\n
\\n总而言之,BigDecimal之所以能够解决精度问题,归纳有仨:
\\nBigDecimal
使用十进制表示法,能够精确表示任意精度的浮点数,避免了二进制表示带来的精度问题。0.1
在 BigDecimal
中可以精确表示为 0.1
,而不是近似值。BigDecimal
的算术运算方法(如 add
、subtract
、multiply
和 divide
)在执行时会考虑小数点位置(scale
),确保结果的精确性。0.1 + 0.2
的结果是 0.3
,而不是 0.30000000000000004
。BigDecimal
提供了多种舍入模式,允许用户根据具体需求选择合适的舍入方式。RoundingMode.HALF_UP
(四舍五入)来确保结果的精确性。 简言之,BigDecimal
能够解决浮点数精度问题,原因在于其内部使用十进制表示法,能够精确表示任意精度的浮点数,并提供精确的算术运算方法。与 float
和 double
类型相比,BigDecimal
避免了二进制表示带来的精度问题,适用于需要高精度计算的场景。\\n 虽然它在性能和操作简便性上稍逊于 double
,但在需要精度的地方,它绝对是你的得力助手。记住:当你需要靠谱的计算结果时,不要犹豫,直接选择 BigDecimal 吧!🎯
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主&最具价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。
\\n-End-
","description":"🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!! 🌟 前言:开发与精度的爱恨情仇\\n\\n 哈咯啊,同学们!今天我要分享一个比较有趣的知识点,恐怕大家基本都回答的不够全面,这也是前天我经理突然对我灵魂来上的重重一击,还好我底子厚,经得住考验,如果换做是你,你能如何满分回答领导提出的拷问?\\n\\n 或许你们曾经也有过因为浮点数的精度问题抓狂?比如说,当你愉快地写下 0.1 + 0.2 时,满心期待的结果为 0.3,结果程序却…","guid":"https://juejin.cn/post/7491248598316580914","author":"bug菌","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T06:35:29.783Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ecdf2214bd414cb0b67b3e02ee1ecb94~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=RSqW2uFDMFst2jQjVt6nB42dibI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6b2d60e545a14f6080ea3db51b3c7102~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=kVH3xM9Vgw3IZFWBfx2hQx%2FD5ng%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d55e8749511d4f4abaa83c73a61614fe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=9mm4jBZZmC0eNmUYY%2BDR7%2Bh431I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab7fb272face4ffc8cbf0bf55c89e198~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=tl9rVHqx7GerT%2FCRD%2BwmGfyrWOw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2cf232118aef49d1af0fa5c643418faa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=%2FAm1qqU9RcjWYwHzTF%2F7lNGWKVI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/265649a244e1431083ae6c34331816f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=sAq6qsSLFQrU8O59KQt%2FE7jAMkY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/870ca55a4d7549b5a2d7718ad13ac630~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=PgcKHTujIQpbS5aTKUCH76EwG7s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4e32b3daf6fb454b80e28b6d8a3075fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=c8h3%2FKtwwNd4gwY8fDT9xbEZho4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a1b9c035a83547869f650ba8bafe8157~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=Z6mkov8uT%2FRm1fJqDoQaj7fLmfA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/864d6986c578475581e8c97799e3de97~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=Y6sHHpu8AIn5HYGM2L2IKmIk8hI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6ac62e43891940369f53c98307f59259~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=pXhQcvHshC5i2dcW4y7efBPB8C4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df6295ca5d944c8d87eb8cfbc8439670~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=ix3cM3SDBuQ5hldQTwUMbFJuD2M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8a0ce05181394bb2b4d6d1474c95ae19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=SGmeHkzPSEksqk4iUK%2B4vCXVHX4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9308ab2ae5b4292a55ac31e0667ec6c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=kpkk5XuH%2BAmndN%2BSyq5QaAqtY24%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/031619645c31437c8aa5612a467ff9f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=nv4Gh%2BZWDY%2BVBFnClD0Zdi84J08%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2c0c7a84ac704425a50e480e712ef239~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=6dnL9wSJJAIsppMVMCPdXfwb8t4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0f02433e7183441488c6d8913c2b258d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYnVn6I-M:q75.awebp?rk3s=f64ab15b&x-expires=1744871850&x-signature=A3Kn7C2TDmb2Akcf9D2ceHPGjK8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Java EE"],"attachments":null,"extra":null,"language":null},{"title":"mysql---MySQL的字典锁","url":"https://juejin.cn/post/7491274636294995995","content":"字典锁,英文名叫做MetaData Lock,也叫做MDL锁,它是一种用于管理元数据的锁机制,而不是数据本身的锁。
\\nMDL锁用于控制对数据库对象的元数据的并发访问,数据库会在执行DDL(Data Defination Language)操作时加上字典锁。字典锁的主要目的是保护数据库中的元数据对象,如表、列、索引、视图等,以确保在DDL操作期间,不会出现数据一致性问题和竞争条件。
\\n以下是触发数据库加字典锁的一些情况:
\\n1.创建/修改/删除表结构:当执行CREATE TABLE、ALTER TABLE、DROP TABLE等DDL语句时,数据库会对相关的表和表的元数据对象加上字典锁,以阻止其他事务同时修改这些表的结构。
\\n2.创建/修改/删除索引:执行CREATE INDEX、ALTER TABLE 添加索引、修改、删除索引等DDL操作时,会锁定与索引相关的元数据,以确保索引的一致性。
\\n3.修改列定义:如果执行ALTER TABLE来修改表的列定义,例如改变数据类型、添加、删除、重命名列等,相关的列和表的元数据会被锁定。
\\n4.创建/修改/删除视图:当执行CREATE VIEW、ALTER VIEW、DROP VIEW等DDL操作以创建或修改视图时,相关视图的元数据会被锁定。
\\n5.其他DDL操作:其他的DDL操作,如创建、修改、删除存储过程、触发器、事件等也可能涉及到元数据的锁定。
\\n在数据库中,通常有两种主要的锁级别,即共享锁和排他锁,而字典锁也有两种级别,即:
\\n1.共享字典锁(SHARED-MDL ):这允许多个事务同时读取元数据对象,但不允许任何事务修改它们。共享字典锁通常用于保护元数据的读取操作,以确保在读取元数据时不会被其他事务修改。
\\n2.排他字典锁(EXCLUSIVE-MDL ):排他字典锁是最高级别的字典锁,它阻止其他事务同时读取或修改元数据对象。只有一个事务可以持有排他字典锁,通常用于保护元数据的写操作,以确保数据的完整性。
\\n而在字典锁的加锁过程中,会有升级的情况,当事务开始时,通常会以共享字典锁的方式访问元数据对象。这允许多个事务同时读取相同的元数据。
\\n如果事务需要对元数据对象进行修改操作,例如修改表结构或索引,它需要将共享字典锁升级为排他字典锁,以阻止其他事务同时访问该元数据对象。
\\n在数据库管理系统中,升级通常是自动执行的。当事务尝试修改元数据对象时,系统会检测到需要升级共享字典锁为排他字典锁,以确保数据的完整性。
","description":"✅什么是MySQL的字典锁? 字典锁,英文名叫做MetaData Lock,也叫做MDL锁\\n\\n字典锁,英文名叫做MetaData Lock,也叫做MDL锁,它是一种用于管理元数据的锁机制,而不是数据本身的锁。\\n\\nMDL锁用于控制对数据库对象的元数据的并发访问,数据库会在执行DDL(Data Defination Language)操作时加上字典锁。字典锁的主要目的是保护数据库中的元数据对象,如表、列、索引、视图等,以确保在DDL操作期间,不会出现数据一致性问题和竞争条件。\\n\\n以下是触发数据库加字典锁的一些情况:\\n\\n1.创建/修改/删除表结构…","guid":"https://juejin.cn/post/7491274636294995995","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T05:52:03.319Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"mysql---什么是OnlineDDL","url":"https://juejin.cn/post/7491154248564408370","content":"DDL,即Data Defination Language,是用于定义数据库结构的操作。DDL操作用于创建、修改和删除数据库中的表、索引、视图、约束等数据库对象,而不涉及实际数据的操作。以下是一些常见的DDL操作:
\\n\\n\\n与DDL相对的是DML,即Data Manipulation Language,用于操作数据。即包括我们常用的INSERT、DELETE和UPDATE等。
\\n
在MySQL 5.6之前,所有的ALTER操作其实是会阻塞DML操作的,如:添加/删除字段、添加/删除索引等,都是会锁表的。
\\n但是在MySQL 5.6中引入了Online DDL,OnLineDDL是MySQL5.6提出的加速DDL方案,尽最大可能保证DDL期间不阻塞DML动作。但是需要注意,这里说的尽最大可能意味着不是所有DDL语句都会使用OnlineDDL加锁。
\\nOnline DDL的优点就是可以减少阻塞,是MySQL的一种内置优化手段,但是需要注意的是,DDL在刚开始和快结束的时候,都需要获取MDL锁,而在获取锁的时候如果有事务未提交,那么DDL就会因为加锁失败而进入阻塞状态,也会造成性能影响。
\\n还有就是,如果Online DDL操作失败,其回滚操作可能成本较高。以及长时间运行的Online DDL操作可能导致主从同步滞后。
\\n但是需要注意的是,即使有了Online DDL,也不意味着就可以随意在业务高峰期进行DDL变更了
\\n在MySQL 5.6支持Online DDL之前,有两种DDL的算法,分别是COPY和INPLACE。
\\n我们可以使用如下SQL指定DDL算法:
\\nALTER TABLE hollis_ddl_test ADD PRIMARY KEY (id) ,ALGORITHM=INPLACE,LOCK=NONE\\n
\\nINPLACE算法是MySQL 5.5中引入的,主要是为了优化索引的创建和删除过程的效率。INPLACE算法的原理是可能地使用原地算法进行DDL操作,而不是重新创建或复制表。
\\nMySQL中的INPLACE其实还可以分为以下两种算法:
\\n前面说过,ALGORITHM可以指定的DDL操作的算法,目前主要支持以下几种:
\\n以下是MySQL官网上给出的Online DDL对索引操作的支持情况:
\\n以下是OnlineDDL的整体步骤,主要分为Prepare阶段、DDL执行阶段以及Commit阶段。
\\nPrepare阶段和Commit阶段虽然也加了EXECLUSIVE-MDL锁,但操作非常轻,所以耗时较低。Execute阶段允许读写,通过row_log记录期间变更的数据记录,最后再应用row_log到新表中。最终实现OnLineDDL的效果。
","description":"前言 DDL,即Data Defination Language,是用于定义数据库结构的操作。DDL操作用于创建、修改和删除数据库中的表、索引、视图、约束等数据库对象,而不涉及实际数据的操作。以下是一些常见的DDL操作:\\n\\nCREATE\\nALTER\\nDROP\\nTRUNCATE\\n\\n与DDL相对的是DML,即Data Manipulation Language,用于操作数据。即包括我们常用的INSERT、DELETE和UPDATE等。\\n\\n在MySQL 5.6之前,所有的ALTER操作其实是会阻塞DML操作的,如:添加/删除字段、添加/删除索引等…","guid":"https://juejin.cn/post/7491154248564408370","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T03:04:43.766Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e53ad7697094637a539b356000d261f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744859083&x-signature=ukWDZUHfpyzXQo0HLlSiNMXaSug%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","前端","架构"],"attachments":null,"extra":null,"language":null},{"title":"一文弄懂用Go实现MCP服务","url":"https://juejin.cn/post/7491221633030815756","content":"最近这段时间,AI领域里有一个非常热门的概念——MCP(模型上下文协议)。Anthropic推出的这一开放标准旨在为大型语言模型和AI助手提供统一的接口,使其能够轻松操作外部工具并完成更复杂的任务。
\\n本文将带你速览MCP的核心概念,并以Go语言为例,介绍如何开发MCP服务端和客户端。
\\n在过去,如果想要让AI处理特定的数据,通常只能依赖于预训练数据或者手动上传数据,这既麻烦又低效。即便对于强大的AI模型而言,也存在数据隔离的问题,无法直接访问新的数据源,每次更新数据都需要重新训练或上传。现在,MCP解决了这个问题,它使得AI不再局限于静态知识库,而是能够像人类一样调用搜索引擎、访问本地文件、连接API服务等,极大提升了AI的动态交互能力。
\\nMCP的核心是“客户端-服务器”架构,其中MCP客户端可以连接到多个服务器。客户端是指希望通过MCP访问数据的应用程序,如CLI工具、IDE插件或AI应用。
\\n要开始使用Go语言构建MCP项目,首先需要安装mcp-go
库,这是Go语言实现的Model Context Protocol库,支持LLM应用与外部数据源和工具之间的无缝集成。
go get github.com/mark3labs/mcp-go\\n
\\n接下来,我们将演示如何使用mcp-go
提供的server模块来构建一个通过stdio方式连接的MCP服务器。
s := server.NewMCPServer(\\"My Server\\", \\"1.0.0\\")\\n
\\n例如,我们可以创建一个简单的计算器工具,这次我们实现乘法和除法功能:
\\ncalculatorTool := mcp.NewTool(\\"calculate\\",\\n mcp.WithDescription(\\"执行基本的算术运算\\"),\\n mcp.WithString(\\"operation\\",\\n mcp.Required(),\\n mcp.Description(\\"要执行的算术运算类型\\"),\\n mcp.Enum(\\"multiply\\", \\"divide\\"), // 修改为仅支持乘法和除法\\n ),\\n mcp.WithNumber(\\"x\\",\\n mcp.Required(),\\n mcp.Description(\\"第一个数字\\"),\\n ),\\n mcp.WithNumber(\\"y\\",\\n mcp.Required(),\\n mcp.Description(\\"第二个数字\\"),\\n ),\\n)\\n\\ns.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\\n op := request.Params.Arguments[\\"operation\\"].(string)\\n x := request.Params.Arguments[\\"x\\"].(float64)\\n y := request.Params.Arguments[\\"y\\"].(float64)\\n\\n var result float64\\n switch op {\\n case \\"multiply\\":\\n result = x * y\\n case \\"divide\\":\\n if y == 0 {\\n return nil, errors.New(\\"不允许除以零\\")\\n }\\n result = x / y\\n }\\n\\n return mcp.FormatNumberResult(result), nil\\n})\\n
\\n同样地,我们也可以注册一些静态资源,比如README.md文件的内容:
\\nresource := mcp.NewResource(\\n \\"docs://readme\\",\\n \\"项目说明文档\\",\\n mcp.WithResourceDescription(\\"项目的 README 文件\\"),\\n mcp.WithMIMEType(\\"text/markdown\\"),\\n)\\n\\ns.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\\n content, err := os.ReadFile(\\"README.md\\")\\n if err != nil {\\n return nil, err\\n }\\n\\n return []mcp.ResourceContents{\\n mcp.TextResourceContents{\\n URI: \\"docs://readme\\",\\n MIMEType: \\"text/markdown\\",\\n Text: string(content),\\n },\\n }, nil\\n})\\n
\\nif err := server.ServeStdio(s); err != nil {\\n fmt.Printf(\\"Server error: %v\\\\n\\", err)\\n}\\n
\\n以上步骤完成后,我们就成功搭建了一个基础的MCP服务器。
\\n接着,我们将展示如何使用mcp-go
提供的client模块构建一个连接至上述MCP服务器的客户端。
mcpClient, err := client.NewStdioMCPClient(\\"./client/server\\", []string{})\\nif err != nil {\\n panic(err)\\n}\\ndefer mcpClient.Close()\\n
\\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\\ndefer cancel()\\n\\ninitRequest := mcp.InitializeRequest{}\\ninitRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION\\ninitRequest.Params.ClientInfo = mcp.Implementation{\\n Name: \\"Client Demo\\",\\n Version: \\"1.0.0\\",\\n}\\n\\ninitResult, err := mcpClient.Initialize(ctx, initRequest)\\nif err != nil {\\n panic(err)\\n}\\nfmt.Printf(\\"初始化成功,服务器信息: %s %s\\\\n\\", initResult.ServerInfo.Name, initResult.ServerInfo.Version)\\n
\\n最后,我们可以通过构造CallToolRequest
来调用服务器上的工具,如下所示:
toolRequest := mcp.CallToolRequest{\\n Request: mcp.Request{\\n Method: \\"tools/call\\",\\n },\\n}\\ntoolRequest.Params.Name = \\"calculate\\"\\ntoolRequest.Params.Arguments = map[string]any{\\n \\"operation\\": \\"multiply\\", // 调用乘法\\n \\"x\\": 2,\\n \\"y\\": 3,\\n}\\n\\nresult, err := mcpClient.CallTool(ctx, toolRequest)\\nif err != nil {\\n panic(err)\\n}\\nfmt.Println(\\"调用工具结果:\\", result.Content[0].(mcp.TextContent).Text)\\n
\\n以下是完整的代码示例,包括服务端和客户端的实现:
\\n服务端代码:
\\npackage main\\n\\nimport (\\n \\"context\\"\\n \\"errors\\"\\n \\"fmt\\"\\n \\"os\\"\\n\\n \\"github.com/mark3labs/mcp-go/mcp\\"\\n \\"github.com/mark3labs/mcp-go/server\\"\\n)\\n\\nfunc main() {\\n s := server.NewMCPServer(\\"Server Demo\\", \\"1.0.0\\")\\n\\n // 添加工具\\n calculatorTool := mcp.NewTool(\\"calculate\\",\\n mcp.WithDescription(\\"执行基本的算术运算\\"),\\n mcp.WithString(\\"operation\\",\\n mcp.Required(),\\n mcp.Description(\\"要执行的算术运算类型\\"),\\n mcp.Enum(\\"multiply\\", \\"divide\\"),\\n ),\\n mcp.WithNumber(\\"x\\",\\n mcp.Required(),\\n mcp.Description(\\"第一个数字\\"),\\n ),\\n mcp.WithNumber(\\"y\\",\\n mcp.Required(),\\n mcp.Description(\\"第二个数字\\"),\\n ),\\n )\\n\\n s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\\n op := request.Params.Arguments[\\"operation\\"].(string)\\n x := request.Params.Arguments[\\"x\\"].(float64)\\n y := request.Params.Arguments[\\"y\\"].(float64)\\n\\n var result float64\\n switch op {\\n case \\"multiply\\":\\n result = x * y\\n case \\"divide\\":\\n if y == 0 {\\n return nil, errors.New(\\"不允许除以零\\")\\n }\\n result = x / y\\n }\\n\\n return mcp.FormatNumberResult(result), nil\\n })\\n\\n // 启动基于 stdio 的服务器\\n if err := server.ServeStdio(s); err != nil {\\n fmt.Printf(\\"Server error: %v\\\\n\\", err)\\n }\\n}\\n
\\n客户端代码:
\\npackage main\\n\\nimport (\\n \\"context\\"\\n \\"fmt\\"\\n \\"time\\"\\n\\n \\"github.com/mark3labs/mcp-go/client\\"\\n \\"github.com/mark3labs/mcp-go/mcp\\"\\n)\\n\\nfunc main() {\\n mcpClient, err := client.NewStdioMCPClient(\\"./client/server\\", []string{})\\n if err != nil {\\n panic(err)\\n }\\n defer mcpClient.Close()\\n\\n ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\\n defer cancel()\\n\\n initRequest := mcp.InitializeRequest{}\\n initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION\\n initRequest.Params.ClientInfo = mcp.Implementation{\\n Name: \\"Client Demo\\",\\n Version: \\"1.0.0\\",\\n }\\n\\n initResult, err := mcpClient.Initialize(ctx, initRequest)\\n if err != nil {\\n panic(err)\\n }\\n fmt.Printf(\\"初始化成功,服务器信息: %s %s\\\\n\\", initResult.ServerInfo.Name, initResult.ServerInfo.Version)\\n\\n // 调用工具\\n toolRequest := mcp.CallToolRequest{\\n Request: mcp.Request{\\n Method: \\"tools/call\\",\\n },\\n }\\n toolRequest.Params.Name = \\"calculate\\"\\n toolRequest.Params.Arguments = map[string]any{\\n \\"operation\\": \\"multiply\\",\\n \\"x\\": 2,\\n \\"y\\": 3,\\n }\\n\\n result, err := mcpClient.CallTool(ctx, toolRequest)\\n if err != nil {\\n panic(err)\\n }\\n fmt.Println(\\"调用工具结果:\\", result.Content[0].(mcp.TextContent).Text)\\n}\\n
\\n希望这篇文章能帮助你快速入门Go语言下的MCP开发!
\\n我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
\\n没准能让你能刷到自己意向公司的最新面试题呢。
\\n感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
","description":"最近这段时间,AI领域里有一个非常热门的概念——MCP(模型上下文协议)。Anthropic推出的这一开放标准旨在为大型语言模型和AI助手提供统一的接口,使其能够轻松操作外部工具并完成更复杂的任务。 本文将带你速览MCP的核心概念,并以Go语言为例,介绍如何开发MCP服务端和客户端。\\n\\n为什么MCP如此重要?\\n\\n在过去,如果想要让AI处理特定的数据,通常只能依赖于预训练数据或者手动上传数据,这既麻烦又低效。即便对于强大的AI模型而言,也存在数据隔离的问题,无法直接访问新的数据源,每次更新数据都需要重新训练或上传。现在,MCP解决了这个问题…","guid":"https://juejin.cn/post/7491221633030815756","author":"王中阳讲编程","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T01:58:45.908Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d4e8a194dca4078bd9d28ef8b8bf8b1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg546L5Lit6Ziz6K6y57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1744855124&x-signature=wbn2mNhBByW6YEi8kAhurpbyvIc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d728788fba2445e87d28e8bf14f1a7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg546L5Lit6Ziz6K6y57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1744855124&x-signature=Kh13%2FEo6ud8zpfMj%2FtAhdQZGxHg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Go"],"attachments":null,"extra":null,"language":null},{"title":"大佬,需要频繁切换jdk版本怎么办(SCOOP)","url":"https://juejin.cn/post/7490875106074312742","content":"前段时间做ai项目,同事为了适配引入的jar将jdk从11干到17,又从17干到21,而我要一边维护老项目(JDK8),又要一边开发新项目(JDK21),怎么办,这不是搞事吗?谁想去ORACLE官网去下载然后安装啊,\\n我头直摇,不不不,我不要这么搞。
\\n百度一下scoop;去github上瞅瞅:\\n\\n
Scoop is a command-line installer for Windows
.哦!这是一个Windows 的命令行安装工具,\\n不闲扯,跟着文档开干;
打开powerShell终端,(我看有些文章写的是管理员方式运行,但是官方写的是非管理员,一切以官方为准
)
给出的解释是Win10默认限制任何powerShell脚本的执行;\\n
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\\n
\\nInvoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\\n或者用另一种方式\\niwr -useb get.scoop.sh | iex\\n
\\n它会将 Scoop 安装到其默认位置:C:\\\\Users\\\\<YOUR USERNAME>\\\\scoop
scoop help\\n又或者通过\\nscoop --version\\n
\\n进行查看(我这里已经弄了个java的bucket)。\\n
scoop默认安装的bucket是main
,
scoop bucket add java\\n
\\n我们看下官方支持哪些:\\n
scoop search jdk\\n
\\nscoop install openjdk21\\n
\\nscoop reset openjdk21\\n
\\n如果我们想查看安装了哪些东西,可以通过\\nscoop list进行查看
\\n好了,现在我们就可以愉快的切换各种jdk进行牛马耕地了。如果你想弄比如PHP,也是同理了。
","description":"前段时间做ai项目,同事为了适配引入的jar将jdk从11干到17,又从17干到21,而我要一边维护老项目(JDK8),又要一边开发新项目(JDK21),怎么办,这不是搞事吗?谁想去ORACLE官网去下载然后安装啊, 我头直摇,不不不,我不要这么搞。 这时一道光出来了,大佬拍了拍我的肩膀,说了声,同志,为什么要傻傻的按部就班,你可以试试scoop啊,我愣了,scoop是什么鬼玩意,为了隐藏我的无知,我连连点头,说了声,对啊,我咋没想起来呢;\\n\\n百度一下scoop;去github上瞅瞅: Scoop is a command-line…","guid":"https://juejin.cn/post/7490875106074312742","author":"小红帽的大灰狼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-09T07:46:39.095Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1ff26b12ab524b6c86beb76b0390d204~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=7vi32k9gEAb6XyHmhDYa44JHHRg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dbe8806c7ca84678b3d369cad0767f8c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=ICu%2BY7A5NxTBBn8AGSMVz%2BLFjeM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7dc79f011c0c4c46a0f8dd7a152ddcfb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=g%2B%2Fk8inSvPoFrsQUne9YE%2Fat9Is%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/891f6a11b706453cacd205431ec9a6be~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=%2Bmmwm2nWbh0GRiKH9TPNVR%2Be6HM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ad3f9e40975648a9b91e84a2c7edd380~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=Vn68N4X0X63tEW3RDCa9g1oHz4E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5739ff0cd5a449e3b912fb46f86ecb1d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=1ie%2FLW%2FCMIwyxoV%2BnyExJp4BIqc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dce03382170b465e911ca3a62fa29d8a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=073RfPKHJSyrQZoJQMvskTugbJI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c22aa919cccd4638af933297f3327a7b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP57qi5bi955qE5aSn54Gw54u8:q75.awebp?rk3s=f64ab15b&x-expires=1744789791&x-signature=juXYg9zkXcIZdB210HIrGcdm1MA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"SQL隐式类型转换 什么时候会导致索引失效","url":"https://juejin.cn/post/7490856819003785252","content":"在数据库中,索引失效会导致查询无法利用索引来加速,从而降低查询性能。
\\n今天就来探索为什么隐式类型转换,会导致索引失效呢,为什么不能对参数进行类型转换再匹配呢,这样不就能用上索引呢?
\\n先看一个例子: create_by
的字段类型为 varchar
\\n\\n看到这儿可能很多人都会大吃一惊!
\\n
\\nMySQL 会尝试将VARCHAR
类型的create_by
字段值转换为数字类型,然后再与116
进行比较。在转换过程中,MySQL 会从字符串的开头开始解析数字,直到遇到非数字字符为止。如果字符串开头没有有效的数字,那么转换结果为0
上面的SQL 和 下面这个SQL 执行逻辑应该是相似的
\\nSELECT distinct create_by FROM t_message WHERE CONVERT(create_by, SIGNED) = 116;
\\n换成这个SQL我想大家都明白了,为什么索引会失效了,隐式转换的时候如果是对列转,那么索引就一定失效
当然不是🏍,请看下面ID
是bigint
,并且执行执行计划是 使用了主键索引的
再来看一张人大金仓的explain
, 它这个就很清楚了,是把参数转换成 数据库列的类型,这样就肯定走索引呢\\n: id = \'123\' ::bigint
如下
🚀其实Mysql
转换是有一套优先级的: 数字类型 > 时间类型 > 字符串类型,低级向高级转换。
🚀所以如果列类型的优先级高于参数类型的优先级 那么就会对参数进行类型转换,这时候类型转换不会对性能有啥影响。反之会去对列数据进行转换,性能的影响就比较大
\\n\\n\\n最近工作也遇到很多 数字 和 boolean 类型的转换,在人大金仓中是 将boolean类型转为数字,mysql 没做测试,应该也是一样。
\\n
MySQL在处理类型不匹配的比较时,通常选择转换列值而非参数值,从而导致索引失效。这个设计决策背后主要是按照
\\n✔MySQL遵循SQL标准中\\"操作数类型提升\\"的规则,通常会将低优先级类型转换为高优先级类型,数值类型优先级 > 时间类型 > 字符串类型
\\n这种转换方向是数据库引擎的通用设计模式
\\n❗下面看看DeepSeek 是怎么回答的❗
\\n1. 语义一致性(避免歧义)与可预测性
\\nSQL 标准要求表达式的结果类型必须明确且可预测。当操作数类型不同时,通过固定的优先级规则(如数值 > 时间 > 字符串)进行隐式转换,确保所有数据库实现的行为一致。例如:
\\n\\n\\n数值与字符串比较时,字符串转为数值,避免因字符编码差异导致不确定结果。 假设 一个条件是 var_colum = 123, 如果转换成 var_colum = \'123\' ,那么 \' 123 \' 这种有空格的就无法查询了。这就是语义一致性问题。
\\n
2. 计算效率优化
\\n高优先级类型(如数值)通常具有更高效的比较和计算机制。例如:
\\n\\n\\n数值比较直接使用CPU指令,而字符串比较需逐字符处理。优先转为数值可提升性能。
\\n
3. 索引结构的限制
\\n我也没咋看懂😭:\\n索引(如B-tree)按列的原生类型组织。若强制转换参数而非列值,存储引擎仍需按列类型重新转换参数,反而增加开销。例如:
\\n\\n\\n
WHERE varchar_col = 123
若转为varchar_col = \'123\'
,仍需将字符串\'123\'
转回数值与索引比较,无法避免转换。
本篇文章,分析了查询隐式转换什么时候会索引失效,以及转换规则优先级,以及为什么SQL标准要这么去定义,DeepSeek 给的答案是主要就是 避免歧义,提升性能。
\\n\\n\\n\\n
下面第一、二点就是我们今天探索的失效场景
\\n-- 假设 id 是varchar类型的索引列,参数传一个 数字\\nSELECT * FROM users WHERE id = 123; \\n
\\n-- 例如在 MySQL 中,对索引列使用 UPPER 函数\\nSELECT * FROM users WHERE UPPER(name) = \'JOHN\'; \\n
\\n-- 假设存在复合索引 (col1, col2)\\nSELECT * FROM table_name WHERE col1 > 10 AND col2 = 20; \\n-- 此时 col2 列的索引会失效\\n
\\nLIKE
进行模糊查询时,如果通配符 %
出现在字符串的开头,数据库无法利用索引的有序性进行快速匹配,会导致索引失效。SELECT * FROM users WHERE name LIKE \'%john\'; \\n
\\n5. OR
连接条件\\n当查询条件使用 OR
连接多个条件,且这些条件部分没有索引或者不全使用同一个索引时,可能会导致索引失效。
SELECT * FROM users WHERE id = 1 OR name = \'john\'; \\n-- 如果 id 有索引,name 没有索引,可能导致索引失效\\n
\\n6. IS NULL
和 IS NOT NULL
\\n在某些情况下,对索引列使用 IS NULL
或 IS NOT NULL
可能会导致索引失效,尤其是在数据分布不均匀时。
SELECT * FROM users WHERE email IS NULL; \\n
\\n全表扫描更快\\n当数据库的查询优化器认为全表扫描比使用索引扫描更快时,会选择全表扫描,此时索引就不会被使用。例如,当查询的数据量占总数据量的比例较大时,优化器可能会做出这样的决策。
\\n索引统计信息不准确\\n如果索引的统计信息不准确,查询优化器可能会做出错误的决策,导致索引失效。例如,表数据发生了大量的插入、删除、更新操作,但没有及时更新索引统计信息。
\\n强制索引失效\\n在 SQL 语句中使用 IGNORE INDEX
关键字可以强制数据库不使用指定的索引。
上篇博文,Huazie 向大家详细介绍了 ConfigurableEnvironment
及其父接口的功能和方法,它的主要作用是提供当前运行环境的公共接口,比如 配置文件(profiles) 及 各类属性和变量(properties) 的设置、添加、读取、合并等功能。
有了这些基础知识,我们就可以更好地了解接下来的配置环境的初始化过程。
\\n\\n\\n注意: 以下涉及 Spring Boot 源码 均来自版本 2.7.9,其他版本有所出入,可自行查看源码。
\\n
在 SpringApplication
的 run
方法中,准备好 ApplicationArguments
参数之后,便开始通过 prepareEnvironment
方法对配置环境 ConfigurableEnvironment
进行初始化操作。
完成了 ConfigurableEnvironment
的初始化操作之后,再通过 configureIgnoreBeanInfo
方法来设置忽略信息配置。
public ConfigurableApplicationContext run(String... args) {\\n // 。。。\\n try {\\n ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);\\n ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);\\n configureIgnoreBeanInfo(environment);\\n // 。。。\\n } catch (Throwable ex) {\\n handleRunFailure(context, ex, listeners);\\n throw new IllegalStateException(ex);\\n }\\n}\\n
\\n先查看 prepareEnvironment
方法,源码如下:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,\\nDefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {\\n ConfigurableEnvironment environment = getOrCreateEnvironment();\\n configureEnvironment(environment, applicationArguments.getSourceArgs());\\n ConfigurationPropertySources.attach(environment);\\n listeners.environmentPrepared(bootstrapContext, environment);\\n DefaultPropertiesPropertySource.moveToEnd(environment);\\n Assert.state(!environment.containsProperty(\\"spring.main.environment-prefix\\"),\\n \\"Environment prefix cannot be set via properties.\\");\\n bindToSpringApplication(environment);\\n if (!this.isCustomEnvironment) {\\n EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());\\n environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());\\n }\\n ConfigurationPropertySources.attach(environment);\\n return environment;\\n}\\n
\\n通过阅读上述源码,我们先对上述环境准备工作大致做个总结,如下:
\\nConfigurableEnvironment environment = getOrCreateEnvironment();\\n
\\n这里通过 getOrCreateEnvironment
方法来获取或创建可配置环境对象,下面进入该方法查看一下其源码:
private ConfigurableEnvironment getOrCreateEnvironment() {\\n if (this.environment != null) {\\n return this.environment;\\n }\\n ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType);\\n if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {\\n environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType);\\n }\\n return (environment != null) ? environment : new ApplicationEnvironment();\\n }\\n
\\n这里也不复杂,大致总结下:
\\nenvironment
【可通过 SpringApplication##setEnvironment
方法设置】是否为 null
,如果不为空,则直接返回这个已存在的 ConfigurableEnvironment
实例;applicationContextFactory
【可通过 SpringApplication##setApplicationContextFactory
方法设置,默认为 ApplicationContextFactory.DEFAULT
】 来创建一个新的 ConfigurableEnvironment
实例;applicationContextFactory
无法获取环境实例,并且当前的 applicationContextFactory
不是默认的(ApplicationContextFactory.DEFAULT
),则使用ApplicationContextFactory.DEFAULT
来创建环境。environment
如果还是为空,则创建 ApplicationEnvironment
返回;否则,直接返回;一般来讲,我们通常使用都是 ApplicationContextFactory.DEFAULT
来创建环境。
@FunctionalInterface\\npublic interface ApplicationContextFactory {\\n\\n ApplicationContextFactory DEFAULT = new DefaultApplicationContextFactory();\\n \\n //......\\n}\\n
\\n继续查看 DefaultApplicationContextFactory
,如下:
class DefaultApplicationContextFactory implements ApplicationContextFactory {\\n\\n //......\\n\\n @Override\\n public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {\\n return getFromSpringFactories(webApplicationType, ApplicationContextFactory::createEnvironment, null);\\n }\\n\\n private <T> T getFromSpringFactories(WebApplicationType webApplicationType,\\nBiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {\\n for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,\\n getClass().getClassLoader())) {\\n T result = action.apply(candidate, webApplicationType);\\n if (result != null) {\\n return result;\\n }\\n }\\n return (defaultResult != null) ? defaultResult.get() : null;\\n }\\n\\n}\\n
\\n这里我们主要分析 getFromSpringFactories
方法;
先来看看它的参数:
\\nWebApplicationType webApplicationType
:标识当前 Web 应用的类型(如 SERVLET, REACTIVE 等)。该参数用于根据不同类型的 Web 应用选择适合的 ApplicationContextFactory
。BiFunction<ApplicationContextFactory, WebApplicationType, T> action
:函数式接口 BiFunction
,它定义了如何将 ApplicationContextFactory
和 WebApplicationType
映射到一个结果 T
。Supplier<T> defaultResult
:函数式接口 Supplier
,它提供一个默认值生成器,用于生成一个默认值。接着简单分析一下它的代码逻辑:
\\nSpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, getClass().getClassLoader())
加载所有实现了ApplicationContextFactory
接口的工厂类【这个加载过程会查找类路径下所有 META-INF/spring.factories
文件中配置的对应工厂类实现】。接着,遍历候选的 ApplicationContextFactory
实例,针对每一个工厂类,使用传入的 BiFunction
(具体就是 ApplicationContextFactory::createEnvironment
,即调用工厂类的createEnvironment
方法)去尝试获取一个类型为T
的结果对象。只要在遍历过程中得到的结果对象不为null
,就立即返回该结果。
最后,如果遍历完所有工厂类都没有得到非null
的结果对象,那么会判断是否提供了默认结果(即defaultResult
是否为null
),如果提供了就通过调用defaultResult.get()
来获取并返回默认结果,否则返回null
。
总结:
\\nApplicationServletEnvironment
;ApplicationReactiveWebEnvironment
;ApplicationEnvironment
;configureEnvironment(environment, applicationArguments.getSourceArgs());\\n
\\n在获取可配置环境对象之后,这里通过 configureEnvironment
方法来配置环境并设置参数,查看其源码如下:
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {\\n // addConversionService = true:需要设置转换服务\\n if (this.addConversionService) {\\n environment.setConversionService(new ApplicationConversionService());\\n }\\n configurePropertySources(environment, args);\\n configureProfiles(environment, args);\\n}\\n
\\n上述内容主要包括【有关这一块的内容,后续专门来一篇讲解,这里简单总结下】:
\\nPropertySources
。添加、删除或重新排序任何该环境下的属性源。开发人员可以重写该方法,以实现对属性源更精细的控制。Profiles
。虽然是空实现,但开发人员可以重写该方法,来自定义哪些配置文件应该被激活或默认激活。在配置文件处理过程中,可以通过 spring.profiles.active
属性激活其他配置文件。ConfigurationPropertySources.attach(environment); \\n\\n//。。。\\n\\nConfigurationPropertySources.attach(environment); \\nreturn environment;\\n
\\n将 ConfigurationPropertySources
附加到指定环境中的第一位,并动态跟踪环境的添加或删除。
这块内容将会在介绍配置属性来源 ConfigurationPropertySources
详细讲解。
listeners.environmentPrepared(bootstrapContext, environment);\\n
\\n前面章节已经讲过各种事件监听的内容,此处主要针对应用环境准备事件的监听【即 org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
】,读者朋友们可以自行查看,这里不再赘述了。
DefaultPropertiesPropertySource.moveToEnd(environment);\\n\\npublic static final String NAME = \\"defaultProperties\\";\\n\\npublic static void moveToEnd(ConfigurableEnvironment environment) {\\n moveToEnd(environment.getPropertySources());\\n}\\n\\npublic static void moveToEnd(MutablePropertySources propertySources) {\\n PropertySource<?> propertySource = propertySources.remove(NAME);\\n if (propertySource != null) {\\n propertySources.addLast(propertySource);\\n }\\n}\\n
\\n这里的主要功能是将名为 \\"defaultProperties\\"
的 PropertySource
(属性源)移动到环境属性源列表的末尾,从而降低其优先级。这将意味着其他属性源(如配置文件、命令行参数等)中的同名属性会覆盖默认属性,确保外部配置能够生效。
bindToSpringApplication(environment);\\n\\nprotected void bindToSpringApplication(ConfigurableEnvironment environment) {\\n try {\\n Binder.get(environment).bind(\\"spring.main\\", Bindable.ofInstance(this));\\n } catch (Exception ex) {\\n throw new IllegalStateException(\\"Cannot bind to SpringApplication\\", ex);\\n }\\n}\\n
\\n上述代码将环境(ConfigurableEnvironment
)中 spring.main
开头的配置属性绑定到当前 SpringApplication
实例的对应字段上,实现通过外部配置(如 application.yml
)动态控制 SpringApplication
的启动行为。
Binder.get(environment)
:从环境(Environment
)中获取 Binder
工具类实例,用于类型安全的属性绑定。Binder
类是 Spring Boot 2.0 引入的强类型配置绑定工具。
bind()
方法 :将 spring.main.xxx
的配置值映射到 SpringApplication
的同名字段。例如:
spring.main.web-application-type=none
:强制禁用 Web 环境。spring.main.lazy-initialization=true
:启用懒加载模式。spring.main.banner-mode=off
:关闭启动 Banner。其他可配置字段,大家可以查看 官方文档 ,这里不赘诉了。
\\nif (!this.isCustomEnvironment) {\\n EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());\\n environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());\\n}\\n
\\n当应用未使用自定义环境(!this.isCustomEnvironment
)时,根据当前应用的实际类型(如 Web 应用、非 Web 应用),通过 EnvironmentConverter
将现有环境(environment
)转换为适配当前应用类型的标准环境,确保环境配置与应用运行时需求一致。
EnvironmentConverter
:Spring Boot 提供的工具类,用于根据应用类型自动适配环境。
deduceEnvironmentClass()
:动态推断当前应用所需的环境类型(如 StandardEnvironment
、StandardServletEnvironment
)。
ConfigurableEnvironment convertEnvironmentIfNecessary(ConfigurableEnvironment environment,\\n Class<? extends ConfigurableEnvironment> type) {\\n if (type.equals(environment.getClass())) {\\n return environment;\\n }\\n return convertEnvironment(environment, type);\\n}\\n\\nprivate ConfigurableEnvironment convertEnvironment(ConfigurableEnvironment environment,\\n Class<? extends ConfigurableEnvironment> type) {\\n ConfigurableEnvironment result = createEnvironment(type);\\n result.setActiveProfiles(environment.getActiveProfiles());\\n result.setConversionService(environment.getConversionService());\\n copyPropertySources(environment, result);\\n return result;\\n}\\n
\\nenvironment
已经是 targetEnvClass
类型,直接返回原环境。targetEnvClass
实例,并将原环境中的属性源(PropertySources
)拷贝到新环境中,确保配置不丢失。通过上述的转换环境过程,Spring Boot 可以实现如下效果:
\\nServletContext
参数)。application.properties
)无缝迁移到新环境。public static final String IGNORE_BEANINFO_PROPERTY_NAME = \\"spring.beaninfo.ignore\\";\\n\\nprivate void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {\\n if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {\\n Boolean ignore = environment.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME,\\n Boolean.class, Boolean.TRUE);\\n System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());\\n }\\n}\\n
\\n上述代码根据 Spring 环境配置动态设置系统属性 \\"spring.beaninfo.ignore\\"
。
spring.beaninfo.ignore
:用于决定是否跳过 BeanInfo
类的扫描,如果设置为 true
,则跳过。
典型的应用场景:
\\nBeanInfo
解析以减少启动时间。BeanInfo
实现时,强制忽略以避免出现 ClassNotFoundException
。ConfigurableEnvironment
的初始化是 Spring Boot 应用启动的关键环节,本篇 Huazie 通过源码带大家深入分析了这一过程,相信大家对环境变量的初始化已经有了自己的初步了解。下篇 Huazie 将继续聚焦 Spring Boot 的启动过程,敬请期待!
一句话回答:会出现一种脏读情况,只不过和传统脏读不一样,传统的脏读指的是读取到其他(MySQL本地事务)事务未提交的数据。而Seata会出现,读取到其他(分支事务)(本地事务)事务已提交但可能被全局回滚的数据。
\\n(听上去有点绕?你看完下面的介绍在回过头来看,就能明白了。。。)
\\n想要回答好这个问题,需要先了解一下Seata的AT模式的工作原理。
\\n●第一阶段:本地事务立即提交,释放本地锁,数据对其他事务可见。\\n●第二阶段:全局事务根据协调结果决定提交或回滚(通过undo log补偿)。
\\n那么,大家仔细想一下这个场景,其实会出现一种情况,那就是如果全局事务最终回滚,其他事务可能在P第一阶段结束后、第二阶段回滚前读取到已提交但即将被撤销的数据,导致逻辑上的脏读。
\\n现在有三个模块,交易模块、订单模块和库存模块。在一次下单过程中,为了保证一致性,代码可能如下:
\\nimport io.seata.spring.annotation.GlobalTransactional;\\nimport org.springframework.beans.factory.annotation.Autowired;\\n\\n@Service\\npublic class TradeService {\\n @Autowired\\n private InventoryService inventoryService;\\n @Autowired\\n private OrderService orderService;\\n\\n @GlobalTransactional\\n public boolean buy() {\\n //库存扣减 \\n inventoryService.decreaseInvenroty(); \\n //创建订单 \\n orderService.createOrder(); } \\n }\\n
\\n在这个分布式事务中(@GlobalTransactional开启的分布式事务),先调库存服务进行库存扣减,然后再调用订单服务进行订单创建。那么整个(大致)流程就是这样的:
\\n这里需要注意的是,在第二步调库存模块之后,库存模块的在数据库上的操作,都是基于数据库的本地事务的,他需要通过数据库的本地事务来保障undolog(Seata用到的的undolog,和MySQL中MVCC的那个undolog不是一个)和库存扣减的原子性,并且这一步执行完之后,数据库的本地事务是要做提交的!
\\n这很关键,数据库的本地事务做了提交,事务提交了,也就意味着,不管在什么样的事务隔离级别下,其他的事务都能查到提交后的新值了。
\\n那么试想一下,如果在第二步执行成功之后,库存已经完成了扣减,但是第三步执行订单创建执行失败了,这时候整个分布式事务是要回滚的,那么就会基于库存库中的undolog做回滚。那么,在提交后,回滚前,如果有其他的事务来查询库存数据,是不是就读到了一个本该回滚的值?
\\n这是不是也是一种脏读,只不过这个脏读并不是MySQL的本地事务中的脏读,而是Seata全局事务中的脏读。即在全局事务过程中,别的事务可能会读到全局事务尚未提交(后面可能会回滚)的数据。
\\nAT模式的脏读是他的实现机制导致的,因为他的第一阶段是借助分支事务实现的,利用分支事务的ACID保证业务操作和undolog的写入的原子性。但是第一阶段执行完,整个全局事务并没有确定要不要提交,还是有可能会回滚的。一旦发生回滚,那么在回滚前,就会能读到脏数据了。
\\n也有一个办法解决 就是在查询的时候加上 @GlobalTransactional + select * ... for update 但是这个比较笨拙 但是确实可以解决这个棘手的问题
\\n在就没啥好办法,因为事务已经提交了,没办法避免其他事务的读取,就算能实现,也会大大降低可用性。所以如果不能接受脏读,那么不要使用AT模式,可以选择其他的事务方案,比如TCC。
\\n可以的。
\\n在Spring框架中,@Autowired 注解不仅可以用于单个bean的注入,还可以用于注入复杂的集合类型,如List、Map、Set等。这种机制非常有用,尤其是当你需要注入同一类型的多个bean时。
\\n当你使用@Autowired在一个List字段上时,Spring会将所有匹配的bean类型注入到这个列表中。这是自动按类型注入的一个例子。这意味着如果你有多个bean都是同一接口的实现,Spring会将它们全部收集起来,注入到这个List中。
\\n//将注入所有AskaService类型的bean\\n\\n@Autowiredprivate List<AskaService> services; \\n
\\n使用Map时,key通常是bean的名称,value是bean的实例。这允许你不仅按类型注入,还可以按名称引用具体的bean。这在你需要根据名称动态选择bean时非常有用。
\\n这通常用在工厂模式中
\\n//键是bean的名称,值是AskaService类型的实例\\n\\n@Autowiredprivate Map<String, AskaService> servicesMap; \\n
\\n与List类似,使用Set可以注入所有匹配的bean类型,但注入到Set中的bean实例将是唯一的(无重复元素),这依赖于bean的equals和hashCode实现。
\\n// 将注入所有HollisService类型的bean,但每个实例只出现一次\\n\\n@Autowiredprivate Set<HollisService> servicesSet; \\n
\\n你也可以使用数组类型来注入。这与使用List类似,Spring会注入所有匹配类型的bean到数组中。
\\n// 将注入所有HollisService类型的bean\\n\\n@Autowiredprivate HollisService[] servicesArray;\\n
\\n当使用这些集合类型注入时,如果没有找到任何匹配的bean,Spring默认的行为是抛出异常。你可以通过设置@Autowired(required = false)来避免这种情况,这样如果没有找到匹配的bean,Spring就不会注入任何值(字段将保持为null)
","description":"✅Spring的@Autowired能用在Map上吗? 可以的。\\n\\n在Spring框架中,@Autowired 注解不仅可以用于单个bean的注入,还可以用于注入复杂的集合类型,如List、Map、Set等。这种机制非常有用,尤其是当你需要注入同一类型的多个bean时。\\n\\nList\\n\\n当你使用@Autowired在一个List字段上时,Spring会将所有匹配的bean类型注入到这个列表中。这是自动按类型注入的一个例子。这意味着如果你有多个bean都是同一接口的实现,Spring会将它们全部收集起来,注入到这个List中。\\n\\n//将注入所有AskaService…","guid":"https://juejin.cn/post/7490815158190178330","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-09T01:03:21.388Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"反射太慢了?那是你不会用LambdaMetafactory!","url":"https://juejin.cn/post/7490777239878418473","content":"在Java的世界里,反射一直是开发者们既爱又恨的功能
\\n一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已
\\n在之前的文章中,我们介绍使用Spring工具类ReflectionUtils通过缓存、避免创建临时对象的方式来优化反射的性能
\\n但工具类在调用方法时依旧会使用反射的invoke,在高频调用的场景性能还是会与直接调用相差一大截
\\n随着Java 8的到来,LambdaMetafactory为我们打开了新的大门,它不仅能够使用类似反射的灵活性,在高频调用场景下还能与直接调用的性能相差不大
\\n本文将深入浅出的介绍LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势
\\nLambdaMetafactory是Java 8引入的一个类,位于java.lang.invoke
包下
它的主要任务是在运行时创建lambda表达式的实现
\\nLambdaMetafactory通过MethodHandle和CallSite机制工作,它能够在运行时通过ASM字节码库生成lambda内部类来调用目标方法,从而兼得反射的灵活性与直接调用的性能优势
\\nMethodHandle是方法句柄,通过它可以灵活调用目标方法;CallSite用于动态管理存储方法句柄MethodHandle,它们是实现动态调用的关键
\\nLambdaMetafactory的核心在于其LambdaMetafactory.metafactory
方法
该方法根据目标方法、接口方法等数据来定义目标lambda的行为,并返回一个CallSite对象
\\n后续通过获取CallSite的目标方法句柄进行调用
\\n使用方法通常分为以下几个步骤:
\\n\\n\\n定义的函数式接口
\\n
@FunctionalInterface\\ninterface Greeter {\\n void greet(String name);\\n}\\n
\\n\\n\\n使用LambdaMetafactory
\\n
public class LambdaMetafactoryExample {\\n public static void sayHello(String name) {\\n System.out.println(\\"Hello, \\" + name);\\n }\\n\\n public static void main(String[] args) throws Throwable {\\n // 获取 Lookup 对象\\n MethodHandles.Lookup lookup = MethodHandles.lookup();\\n\\n // 编写方法类型 返回、入参类型\\n MethodType methodType = MethodType.methodType(void.class, String.class);\\n // 查找目标方法句柄\\n MethodHandle targetMethod = lookup.findStatic(LambdaMetafactoryExample.class, \\"sayHello\\", methodType);\\n\\n // 准备元数据并创建 CallSite\\n CallSite callSite = LambdaMetafactory.metafactory(\\n lookup,\\n \\"greet\\", // 要生成的lambda方法名\\n MethodType.methodType(Greeter.class), // 调用点签名\\n methodType, // 目标方法类型(注意这里应该直接是 methodType)\\n targetMethod, // 目标方法句柄\\n methodType // 目标方法类型\\n );\\n\\n // 获取并调用\\n MethodHandle factory = callSite.getTarget();\\n Greeter greeter = (Greeter) factory.invokeWithArguments();\\n\\n // 调用接口方法\\n greeter.greet(\\"World\\");\\n }\\n}\\n
\\n总的来说,LambdaMetafactory就是通过lambda生成接口方法,从而实现灵活调用目标方法
\\n反射性能上带来的劣势主要有以下几个原因:
\\nJVM通过解释、编译混合运行,对于高频访问的代码会使用JIT将字节码转化为机器码缓存在方法区,无需再进行解释执行,在高频访问的场景下反射无法使用该优化
\\n而LambdaMetafactory相比反射具有显著的性能优势,主要原因在于:
\\n这些因素使得LambdaMetafactory在循环、频繁调用的场景中尤为出色
\\n测试代码如下,也可以直接看后面的结果表格:
\\npublic class LambdaVsReflectionBenchmark {\\n\\n // 目标方法\\n public static void sayHello(String name) {\\n name = \\"Hello,\\" + name;\\n// System.out.println(\\"Hello, \\" + name);\\n }\\n\\n // 使用 LambdaMetafactory 创建 lambda 表达式\\n private static Greeter createLambda() throws Throwable {\\n MethodHandles.Lookup lookup = MethodHandles.lookup();\\n MethodType methodType = MethodType.methodType(void.class, String.class);\\n MethodHandle targetMethod = lookup.findStatic(LambdaVsReflectionBenchmark.class, \\"sayHello\\", methodType);\\n\\n CallSite callSite = LambdaMetafactory.metafactory(\\n lookup,\\n \\"greet\\",\\n MethodType.methodType(Greeter.class),\\n methodType.changeReturnType(void.class),\\n targetMethod,\\n methodType\\n );\\n\\n MethodHandle factory = callSite.getTarget();\\n return (Greeter) factory.invokeExact();\\n }\\n\\n // 使用反射获取目标方法\\n private static Method getReflectiveMethod() throws Exception {\\n return LambdaVsReflectionBenchmark.class.getMethod(\\"sayHello\\", String.class);\\n }\\n\\n // 测试调用次数\\n// private static final long ITERATIONS = 1_000L;\\n private static final long ITERATIONS = 1_000_000_000L;\\n\\n /**\\n * 调用次数:1000\\n * 直接调用耗时: 0.13 ms\\n * LambdaMetafactory 调用耗时: 0.17 ms\\n * 反射调用耗时: 1.74 ms\\n * <p>\\n * 调用次数:1000000000\\n * 直接调用耗时: 4501.54 ms\\n * LambdaMetafactory 调用耗时: 4640.59 ms\\n * 反射调用耗时: 6142.39 ms\\n *\\n * @param args\\n * @throws Throwable\\n */\\n public static void main(String[] args) throws Throwable {\\n\\n System.out.println(\\"调用次数:\\" + ITERATIONS);\\n //直接调用\\n long start = System.nanoTime();\\n for (int i = 0; i < ITERATIONS; i++) {\\n sayHello(\\"World\\");\\n }\\n long end = System.nanoTime();\\n System.out.printf(\\"直接调用耗时: %.2f ms%n\\", (end - start) / 1e6);\\n\\n // 准备 Lambda\\n Greeter lambdaGreeter = createLambda();\\n\\n // 测试 Lambda 调用\\n long lambdaStart = System.nanoTime();\\n for (int i = 0; i < ITERATIONS; i++) {\\n lambdaGreeter.greet(\\"World\\");\\n }\\n long lambdaEnd = System.nanoTime();\\n System.out.printf(\\"LambdaMetafactory 调用耗时: %.2f ms%n\\", (lambdaEnd - lambdaStart) / 1e6);\\n\\n\\n // 准备反射方法\\n Method reflectiveMethod = getReflectiveMethod();\\n\\n // 测试反射调用\\n long reflectionStart = System.nanoTime();\\n for (int i = 0; i < ITERATIONS; i++) {\\n reflectiveMethod.invoke(null, \\"World\\");\\n }\\n long reflectionEnd = System.nanoTime();\\n System.out.printf(\\"反射调用耗时: %.2f ms%n\\", (reflectionEnd - reflectionStart) / 1e6);\\n }\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n直接调用 | LambdaMetafactory | 反射 | |
---|---|---|---|
循环1000次 | 0.13 ms | 0.17 ms | 1.74 ms |
循环1000000000次 | 4501.54 ms | 4640.59 ms | 6142.39 ms |
根据表格可以看出在高频调用的场景下,LambdaMetafactory与直接调用性能几乎相同,而反射性能几乎慢了将近四分之一
\\n我们从核心方法 LambdaMetafactory.metafactory
生成CallSite作为入口进行分析其实现原理
该方法通过一系列的元数据,使用工厂来构建CallSite
\\npublic static CallSite metafactory(MethodHandles.Lookup caller,\\n String invokedName,\\n MethodType invokedType,\\n MethodType samMethodType,\\n MethodHandle implMethod,\\n MethodType instantiatedMethodType)\\n throws LambdaConversionException {\\n AbstractValidatingLambdaMetafactory mf;\\n //工厂实例\\n mf = new InnerClassLambdaMetafactory(caller, invokedType,\\n invokedName, samMethodType,\\n implMethod, instantiatedMethodType,\\n false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);\\n //校验参数\\n mf.validateMetafactoryArgs();\\n //生产CallSite\\n return mf.buildCallSite();\\n}\\n
\\nInnerClassLambdaMetafactory
工厂主要为lambda调用点创建内部类、以及封装构建CallSite
buildCallSite
构建CallSite时:先通过 spinInnerClass
方法创建内部类,再根据元数据在内部类中找到MethodHandle封装为CallSite
创建内部类是通过ASM字节码库的ClassWriter,将元数据写入后转换为流,最后通过UNSAFE类的defineAnonymousClass
生成类(逻辑代码如下)
private Class<?> spinInnerClass() throws LambdaConversionException {\\n //元数据写入ClassWriter \\n cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,\\n lambdaClassName, null,\\n JAVA_LANG_OBJECT, interfaces);\\n \\n //转为字节流\\n final byte[] classBytes = cw.toByteArray();\\n \\n //UNSAFE生成\\n return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);\\n}\\n
\\n总的来说,LambdaMetafactory实际上是使用指定的元数据,通过ASM字节码库动态生成内部类,通过调用内部类接口方法来间接实现调用目标方法
\\n这样就即实现反射调用的灵活,又能享受直接调用的性能,只是初次生成类存在一定的开销
\\n(实际上这也是使用Lambda语法糖时会隐式帮助我们做的事情)
\\nLambdaMetafactory虽然能够带来性能优势,但也存在一定的劣势,比如:依赖接口、使用更复杂...
\\n根据不同的应用场景可以从反射、反射工具类、LambdaMetafactory中选择最适合的解决方案
\\n在高频使用反射的场景下,常常会有创建临时对象、软引用缓存被gc清空、无法使用JIT优化等问题而导致性能受到影响的情况
\\nLambdaMetafactory带来了更加优雅的动态调用方式,虽然会有部分生成内部类的开销,但它解决了长期以来困扰开发者的反射性能问题
\\nLambdaMetafactory使用元数据通过ASM字节码库、Unsafe类动态生成匿名内部类,再封装为Methodhandler、CallSite进行使用
\\n(同时它也是Lambda语法糖的隐式实现,对于开发者透明)
\\n对于不同的应用场景可以选择反射、Spring ReflectionUtils、LambdaMetafactory等多种方案进行解决问题
\\n😁我是菜菜,热爱技术交流、分享与写作,喜欢图文并茂、通俗易懂的输出知识
\\n📚在我的博客中,你可以找到Java技术栈的各个专栏:Java并发编程与JVM原理、Spring和MyBatis等常用框架及Tomcat服务器的源码解析,以及MySQL、Redis数据库的进阶知识,同时还提供关于消息中间件和Netty等主题的系列文章,都以通俗易懂的方式探讨这些复杂的技术点
\\n🏆除此之外,我还是掘金优秀创作者、腾讯云年度影响力作者、华为云年度十佳博主....
\\n👫我对技术交流、知识分享以及写作充满热情,如果你愿意,欢迎加我一起交流(vx:CaiCaiJava666),也可以持续关注我的公众号:菜菜的后端私房菜,我会分享更多技术干货,期待与更多志同道合的朋友携手并进,一同在这条充满挑战与惊喜的技术之旅中不断前行
\\n🤝如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
\\n📖本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔
\\n📝本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~
","description":"反射太慢了?那是你不会用LambdaMetafactory! 引言\\n\\n在Java的世界里,反射一直是开发者们既爱又恨的功能\\n\\n一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已\\n\\n在之前的文章中,我们介绍使用Spring工具类ReflectionUtils通过缓存、避免创建临时对象的方式来优化反射的性能\\n\\n但工具类在调用方法时依旧会使用反射的invoke,在高频调用的场景性能还是会与直接调用相差一大截\\n\\n随着Java 8的到来,LambdaMetafactory为我们打开了新的大门,它不仅能够使用类似反射的灵活性…","guid":"https://juejin.cn/post/7490777239878418473","author":"菜菜的后端私房菜","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T13:31:21.049Z","media":null,"categories":["后端","Java","设计"],"attachments":null,"extra":null,"language":null},{"title":"瞧瞧别人家的限流,那叫一个优雅!","url":"https://juejin.cn/post/7490781505582579739","content":"去年夏天某个凌晨,我接到某金融平台报警:支付接口错误率飙升至35%。
\\n赶到机房时,发现数据库连接池耗尽,大量请求堆积成山——这就是典型的未做限流防护的灾难现场。
\\n就像高速公路不设收费站,高峰期必然堵成停车场。
\\n限流的本质不是拒绝服务,而是用可控的牺牲保护核心链路。
\\n某电商大促时,他们用令牌桶算法将秒杀接口QPS限制在5万,虽然流失了20%的突发流量,但保住了99%的核心交易成功率。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。
\\n核心原理:
\\n以固定时间窗口(如1秒)为周期,统计周期内请求数,超过阈值则拒绝后续请求。
具体代码实现如下:
\\n// 线程安全实现(AtomicLong优化版)\\npublicclass FixedWindowCounter {\\n privatefinal AtomicLong counter = new AtomicLong(0);\\n privatevolatilelong windowStart = System.currentTimeMillis();\\n privatefinalint maxRequests;\\n privatefinallong windowMillis;\\n\\n public boolean tryAcquire() {\\n long now = System.currentTimeMillis();\\n if (now - windowStart > windowMillis) {\\n if (counter.compareAndSet(counter.get(), 0)) {\\n windowStart = now;\\n }\\n }\\n return counter.incrementAndGet() <= maxRequests;\\n }\\n}\\n
\\n致命缺陷:
\\n假设设置1秒100次限制,0.9秒时突发100次请求,下一秒0.1秒又放行100次,实际两秒内通过200次。
就像红绿灯切换时车辆抢行,容易引发\\"临界点突刺\\"。
\\n适用场景:
\\n日志采集、非关键性接口的粗粒度限流
核心原理:
\\n将时间窗口细分为更小的时间片(如10秒),统计最近N个时间片的请求总和。
基于Redis的Lua脚本如下:
\\n// Redis Lua实现滑动窗口(精确到毫秒)\\nString lua = \\"\\"\\"\\n local now = tonumber(ARGV\\n local window = tonumber(ARGV\\n local key = KEYS[1]\\n \\n redis.call(\'ZREMRANGEBYSCORE\', key, \'-inf\', now - window)\\n local count = redis.call(\'ZCARD\', key)\\n \\n if count < tonumber(ARGV then\\n redis.call(\'ZADD\', key, now, now)\\n redis.call(\'EXPIRE\', key, window/1000)\\n return 1\\n end\\n return 0\\n \\"\\"\\";\\n
\\n技术亮点:
\\n某证券交易系统采用滑动窗口后,将API异常率从5%压降至0.3%。
通过Redis ZSET实现时间切片,误差控制在±10ms内。
\\n优势对比
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n指标 | 固定窗口 | 滑动窗口 |
---|---|---|
时间精度 | 1秒 | 100ms |
临界突刺问题 | 存在 | 消除 |
实现复杂度 | 简单 | 中等 |
核心原理:
\\n请求像水流一样进入漏桶,系统以固定速率处理请求。
桶满时新请求被丢弃。
\\n具体实现如下:
\\n// 漏桶动态实现(Semaphore优化版)\\npublicclass LeakyBucket {\\n privatefinal Semaphore permits;\\n privatefinal ScheduledExecutorService scheduler;\\n\\n public LeakyBucket(int rate) {\\n this.permits = new Semaphore(rate);\\n this.scheduler = Executors.newScheduledThreadPool(1);\\n scheduler.scheduleAtFixedRate(() -> permits.release(rate), 1, 1, TimeUnit.SECONDS);\\n }\\n\\n public boolean tryAcquire() {\\n return permits.tryAcquire();\\n }\\n}\\n
\\n技术痛点:
\\n某智能家居平台用此方案,确保即使10万台设备同时上报数据,系统仍按500条/秒的速率稳定处理。
但突发流量会导致队列积压,就像用漏斗倒奶茶——珍珠容易卡住。
\\n适用场景:
\\nIoT设备控制指令下发、支付渠道限额等需要严格恒定速率的场景
核心原理:
\\n以固定速率生成令牌,请求需获取令牌才能执行。
突发流量可消耗桶内积攒的令牌。
\\n具体实现如下:
\\n// Guava RateLimiter高级用法\\nRateLimiter limiter = RateLimiter.create(10.0, 1, TimeUnit.SECONDS); // 初始预热\\nlimiter.acquire(5); // 尝试获取5个令牌\\n\\n// 动态调整速率(需反射实现)\\nField field = RateLimiter.class.getDeclaredField(\\"tokens\\");\\nfield.setAccessible(true);\\nAtomicDouble tokens = (AtomicDouble) field.get(limiter);\\ntokens.set(20); // 突发时注入20个令牌\\n
\\n实战案例:
\\n某视频平台用此方案应对热点事件:平时限制10万QPS,突发时允许3秒内超限50%,既防雪崩又保用户体验。
动态特性
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n某电商双11方案:通过Redis+Lua实现分布式计数,配合Nginx本地缓存,在网关层拦截了83%的恶意请求。
\\n我们还需要自适应熔断机制。
\\n某社交平台用此方案,在突发流量时自动将限流阈值从5万降到3万,等系统恢复后再逐步提升。
\\n在数据库连接池前做限流!
\\n某公司曾因此导致连接泄漏,最终撑爆数据库。
\\n正确做法应遵循熔断三原则:
\\n某金融系统通过JMH测试发现,使用LongAdder替代AtomicLong,限流吞吐量提升220%。
\\n性能优化手段:减少CAS竞争 和 分段锁基座。
上面列举了工作中最常用的4种限流方案。
\\n对于不同的业务场景,我们需要选择不同的限流方案。
\\n限流的黄金法则如下:
记住:好的限流方案就像高铁闸机——既保证通行效率,又守住安全底线。
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"前言 去年夏天某个凌晨,我接到某金融平台报警:支付接口错误率飙升至35%。\\n\\n赶到机房时,发现数据库连接池耗尽,大量请求堆积成山——这就是典型的未做限流防护的灾难现场。\\n\\n就像高速公路不设收费站,高峰期必然堵成停车场。\\n\\n限流的本质不是拒绝服务,而是用可控的牺牲保护核心链路。\\n\\n某电商大促时,他们用令牌桶算法将秒杀接口QPS限制在5万,虽然流失了20%的突发流量,但保住了99%的核心交易成功率。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、项目实战、工作内推什么都有。\\n\\n1…","guid":"https://juejin.cn/post/7490781505582579739","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T13:30:39.728Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78576a9947f54462b5b7dc1a898a8061~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=OTG3hSplXpB5OCVyQOTxuHa816E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d0e125278c934cf39c2102594ac46e01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=w9zsJgnngmBym15LUAHPHbhMHhQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b33c876d3924ceea2c6ec7113b5c764~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=sZHfVliGc3Sy7hqp6zXKMAOXyqo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/312eca06dad24c888beba5a7bebfb058~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=b4%2F%2FeIW08JXXPfNxWP2g8EUVNBs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df0956b2c1e44cc9993aca51810eae6f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=0bO8vhiI7ATAOCENJSiyDoszeU0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a8b8cab21bc94c84a8d98837892e4311~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=hoJrehDy9kdD%2BSduMC77igvBoN0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2ca6a7446aa349ad8d50efe6357746b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=C6sSmxjivDb5WeKZK2vR23xVQDE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2af3b40fc12548bab2855b5d18e5619c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=Uy746L9m4JVEJAp6M2rt2JSBiJw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/899eecef3f9d4b29b52a5bfe929de45e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=oj9hz%2F5HGXnjd1wPzLG%2FXowPjNY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b830c2a99e7492ab92a341fffd57d14~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744723839&x-signature=oA699jwwd0spssY4uOc8t%2BiSjlU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Redis到底能不能做主数据库?","url":"https://juejin.cn/post/7490440642851635252","content":"张三拍案而起:“Redis 是缓存数据库,怎么能当主数据库用?简直是天方夜谭!”
\\n李四冷笑回应:“你没用过,凭什么说不行?我已经用 Redis 做主数据库好几年了,系统稳定得像铁板一块!”
\\n这场争论,早已在技术圈掀起轩然大波。一边是“传统派”的质疑,一边是“创新派”的实战经验,双方各执一词,谁也说服不了谁。
\\n而你,是否也曾陷入这样的困惑:Redis 到底能不能胜任主数据库的角色?
\\n技术的世界瞬息万变,今天的“不行”,会不会成为明天的“标配”?
\\n我们看看 Redis官方是怎么看这个问题的:
\\nSanfilippo 在接受采访时表示
\\n“What Redis is good for is not my choice; it’s the application developer that knows better,” he says, explaining that every application has its goals, guarantees it must provide, and latency and scalability concerns. Sanfilippo says he wants Redis to be used when it solves a problem: be it a primary database, just an index for another database, some smart caching, messaging, or whatever.
\\n翻译:
\\n“我对于如何使用 Redis 没有太多发言权,应用开发者比我知道得更清楚”他说,并解释道:每个应用都有自己的目标,必须提供的保证,以及延迟和可扩展性考虑。Sanfilippo 说,他唯一希望的就是 Redis 可以用于解决问题:无论是作为主数据库,还是其他数据库的索引,又或者是智能缓存、消息队列,等等。
\\n\\"Redis began as a caching database, but it has since evolved into a primary database. \\"
\\nRedis最初是一个缓存数据库,但它已经发展成为一个主数据库。
\\n当然这段话也透露出另一个信息就是,“However, most Redis service providers support Redis as a cache but not as a primary database. ”。也就是大多数人还是使用 redis 作为缓存而非主数据库。
\\n不过这篇文章主要是表明官方推荐把 redis 作为主数据库的态度。
\\n其实官方推荐把 redis 作为主数据库的文章还不少,这篇也有说到 Redis 作为主数据库。
\\n在 Redis 官网在原本的站内搜索的再进,增加了 AI 对话的功能。我们可以直接向 AI 提问,他会根据官方文档、博客等资源给我们回答。
\\n我问他 redis 作为主数据库的事,他的回答也是肯定的。
\\n其实站在 Redis 官方的角度,肯定是希望它可以有更多的使用场景与生态。当然他们的推荐肯定还是有一定的技术依据的,不然这观点是完全站不住的。
\\n我们来看看,一般说 Redis 不能作为主数据库主要就这几个原因:
\\nRedis 的数据存储在内存中,这意味着它的存储容量受限于物理内存大小。它需要把所有的业务数据加载到内存中,对于大规模数据存储,内存的成本远高于磁盘,且扩展内存容量可能会带来高昂的硬件成本。虽然可以通过 Redis Cluster 来扩充内存,但仍然解决不了根本的问题。
\\nRedis 的持久化机制(RDB 和 AOF)虽然可以保存数据到磁盘,但存在一定的局限性。比如:RDB 是指定在多少秒内发生多少次数据变化时触发 RDB 快照。而 AOF 是每秒一次同步一次也就是刷盘。可以看到这两中同步方式都会存在数据的丢失。当然我们可以说我把同步频率设置更短一点,那么又会产生新的性能问题,可能得不偿失。
\\nRedis 的数据结构(如字符串、哈希、列表 、Json 等)适合快速访问和缓存,但在处理复杂的关系型数据时并不好。不支持复杂的 SQL 查询,如连表查询、聚合查询等
\\n其实认真看官方的文档会发现,Redis作为主数据库与缓存数据库还是有所区别的。
\\n比如:官方文档中说到的
\\nHowever, when deploying Redis as a primary database, it requires specific configurations to ensure data availability and reliability.
\\n和
\\nWith Redis open source, you need to set up Redis Sentinel for high availability. In Redis Cloud, it’s a core feature that you just need to turn on when creating the database.
\\n主要意思第一段说Redis要作为主数据库的话需要特定的配置,第二段说要打开或设置 HA。
\\n所以说Redis 是可以作为主数据库,不过得在适合下场景中使用。比如通常业务简单、高性能、低延迟和实时性有较高要求的场景中作为主数据库。
\\n权衡性能与内存,哪个是自己更想要的
\\n通过增加硬件组件分布式集群解决内存限制,解决了单机单点的问题也就解决了 RDB 与 AOF 不足的问题。
\\n业务简单的场景也就没有太多的复杂查询,Redis 就足够了。
\\nRedis 可以用作生产上的主数据库吗?
\\n答案是:可以,它已经具备了这个能力,当然还是要带一些条件的。
\\n当然也有不少人选择一个比较折中的方案就是选择使用 MongoDB,所以是否作为主数据库还得看实际的情况而定。
\\n我是栈江湖,如果你喜欢此文章,不要忘记点赞+关注!
","description":"张三拍案而起:“Redis 是缓存数据库,怎么能当主数据库用?简直是天方夜谭!” 李四冷笑回应:“你没用过,凭什么说不行?我已经用 Redis 做主数据库好几年了,系统稳定得像铁板一块!”\\n\\n这场争论,早已在技术圈掀起轩然大波。一边是“传统派”的质疑,一边是“创新派”的实战经验,双方各执一词,谁也说服不了谁。\\n\\n而你,是否也曾陷入这样的困惑:Redis 到底能不能胜任主数据库的角色?\\n\\n技术的世界瞬息万变,今天的“不行”,会不会成为明天的“标配”?\\n\\n我们看看 Redis官方是怎么看这个问题的:\\n\\n一、官方的建议\\n1.1、Redis创始人 Salvatore…","guid":"https://juejin.cn/post/7490440642851635252","author":"栈江湖","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T03:41:16.347Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ae713ca6d7b64e0fbdd9eef58349fe38~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=%2BQCd4nYPYuMu%2Fb6S5xLEmf%2FazbE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c122ba4b58c845f4a1f9a2d9f7548f76~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=tqEdm8hwl4nvKaN4M6iLVigdFa8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/429703511aea4afe8f79e4e069af0ace~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=T54HUwp9lwtuYG2%2FGxVarxRPogo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/729d05e075a945329d9139cc6d4aae55~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=oj%2Fd3XfSN%2BN0%2BAVq%2FcqnxC5FEvQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fdd9b4af600d4fb9b46d45eb222e65e2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=dw0zMOq6KyEJUfYqtiWL1LL9Z1k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6842d36e23c542deb2b6a01a126b6043~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=wT%2BBo9yyJwgb62Cg0Kh6z1%2BHOA8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f66c8decab174ebf8dae5932ce172b75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qCI5rGf5rmW:q75.awebp?rk3s=f64ab15b&x-expires=1744688476&x-signature=iDgFNERe0BrSQMjB2ux1my51QII%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Redis","数据库"],"attachments":null,"extra":null,"language":null},{"title":"业务设计---针对天气预报变化时触发用户通知和推荐行程用什么设计模式","url":"https://juejin.cn/post/7490428110223261722","content":"当天气不同时,用户会收到不同的通知,通知中包括具体的天气和推荐的行程。
\\n这里有两个点,一个是天气不同的时候发通知,还有一个就是不同的天气发送的内容不一样。
\\n天气变化是典型的事件驱动场景,需通知多个订阅方(如通知服务、推荐服务),观察者模式天然支持一对多依赖关系,实现低耦合的事件响应机制。
\\n不同天气对应不同的行程推荐策略(如晴天推荐户外活动、雨天推荐室内活动),策略模式通过封装可互换的业务逻辑实现逻辑灵活切换。
\\n那么其实就是需要结合 观察者模式 和 策略模式两种设计模式。
\\nSubject(被观察者):天气服务(\ufeffWeatherService\ufeff),负责监测天气变化,并管理观察者列表。
\\nObserver(观察者):定义通知接口(如 \ufeffsendNotification\ufeff),由具体观察者实现。
\\n// 推荐服务 + 策略模式\\nclass RecommendationService implements WeatherObserver {\\n private RecommendationStrategy strategy;\\n\\n public void setStrategy(RecommendationStrategy strategy) {\\n this.strategy = strategy;\\n }\\n\\n @Override\\n public void update(WeatherData data) {\\n // 根据天气类型切换策略 \\n switch (data.getType()) {\\n case SUNNY:\\n setStrategy(new SunnyStrategy());\\n break;\\n case RAINY:\\n setStrategy(new RainyStrategy());\\n break;\\n }\\n String rec = strategy.generateRecommendation(data);\\n sendRecommendation(rec);\\n }\\n}\\n\\n// 策略接口与实现\\n\\ninterface RecommendationStrategy {\\n String generateRecommendation(WeatherData data);\\n}\\n\\nclass SunnyStrategy implements RecommendationStrategy {\\n @Override\\n public String generateRecommendation(WeatherData data) {\\n return \\"今日晴,推荐去公园野餐,紫外线指数:\\" + data.getUVIndex();\\n }\\n}\\n
","description":"✅针对天气预报变化时触发用户通知和推荐行程用什么设计模式 当天气不同时,用户会收到不同的通知,通知中包括具体的天气和推荐的行程。\\n\\n这里有两个点,一个是天气不同的时候发通知,还有一个就是不同的天气发送的内容不一样。\\n\\n天气变化是典型的事件驱动场景,需通知多个订阅方(如通知服务、推荐服务),观察者模式天然支持一对多依赖关系,实现低耦合的事件响应机制。\\n\\n不同天气对应不同的行程推荐策略(如晴天推荐户外活动、雨天推荐室内活动),策略模式通过封装可互换的业务逻辑实现逻辑灵活切换。\\n\\n那么其实就是需要结合 观察者模式 和 策略模式两种设计模式。\\n\\n观察者模式…","guid":"https://juejin.cn/post/7490428110223261722","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T02:32:18.506Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a3bd5ed74a87460885527799a8cb0391~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744684338&x-signature=zCIhQHhThDAZ4x1t6%2FRbObaY3Hk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c963c32f808743aa87ea2bbabe5df1e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744684338&x-signature=Bx005fUy4pjBwg7Jb1r%2B%2Bke5eKA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"限流---漏桶和令牌桶有啥区别","url":"https://juejin.cn/post/7490676187754676274","content":"漏桶和令牌桶都是用来做流量控制的算法。经常会有人把他们放在一起对比。
\\n\\n\\n他们之间的关系是都有一个固定容量的桶,都是按照固定的速率向桶中添加水(或者令牌),但是他们有一个最大的区别,那就是漏桶的这个桶底部是漏的,它同样会按照固定的速率把水流出,所以漏桶输出流量是匀速的,不管输入流量如何变化。而令牌桶的底部不是漏的,他不会以固定的速率流出,只会以固定的速率向桶中添加令牌。
\\n
以上就是二者的差别。举个例子:
\\n\\n\\n漏桶的这个桶,一秒钟流入一滴水,同样一秒钟漏出一滴水。那么,一秒钟就只能处理一个请求,超过的请求会被拒绝掉,达到限流的效果。
\\n也就是说,漏桶这种算法,在5秒钟只能可以处理5个请求,并且每秒钟一个。但是如果出现这种情况,前4秒钟都没有请求,第5秒同时来了5个请求,漏桶是无法处理5个请求的,他只能处理1个,因为这一秒钟只会有一滴水漏出来。
\\n这就是典型的突发流量的问题。而令牌桶可以很好的解决这个问题。
\\n令牌桶的实现逻辑是同样1秒钟产生一个令牌放到桶中,但是如果这个令牌没有被消费的话,他就会一直在桶中,不会被漏出去。还是刚刚那个例子,前4秒没有请求要处理的话,那么5秒钟就可以积攒5个令牌,这时候第5秒来了5个请求的时候,他去桶中是可以一次取出5个令牌,然后把这5个请求都给处理掉的。这就很好地应对了突发力量的问题。
\\n
所以,漏桶算法适合于需要限制数据的平均传输速率并确保数据传输的平滑性的场景。令牌桶算法更加灵活,适合于那些既需要限制数据平均传输速率,又需要允许一定程度突发传输的场景。
\\n漏桶算法是一种流量控制算法,可以平滑控制流量的进出,原理比较简单:假设我们有一个水桶按固定的速率向下方滴落一滴水,无论有多少请求,请求的速率有多大,都按照固定的速率流出,对应到系统中就是按照固定的速率处理请求。
\\n漏桶算法通过一个固定容量的漏桶来控制请求的处理速率,每个请求被看作是一定数量的水,需要先放到漏桶中。当漏桶满时,请求将被拒绝或延迟处理,从而保证了系统的稳定性。
\\n漏桶通过定时器的方式将水以恒定的速率流出,与请求的数量无关,从而平滑控制了请求的处理速率。当请求到来时,先将请求看作是一定数量的水,需要将这些水放入漏桶中。
\\n如果漏桶未满,请求将被立即处理并从漏桶中取出对应数量的水。如果漏桶已满,请求将被拒绝或被延迟处理,直到漏桶中有足够的空间存放请求对应数量的水。
\\n当定时器触发时,漏桶中的水以恒定的速率流出,此时可以继续处理请求。
\\n总之,漏桶算法通过一个固定容量的漏桶来控制请求的处理速率,可以平滑控制流量的进出,保证系统的稳定性和安全性。
\\n但需要注意的是,漏桶算法无法处理突发流量,因为他只能按照固定的速度来处理请求,如果某个请求的流量突增,因为漏桶的机制就导致了他还是只能一个一个的按照固定速度进行消费。
\\n为了解决这种突发流量的问题,就有了令牌桶算法。
\\n令牌桶其实和漏桶的原理类似,令牌桶按固定的速率往桶里放入令牌,并且只要能从桶里取出令牌就能通过。
\\n也就是说,我不管现在请求量是多还是少,都有一个线程以固定的速率再往桶里放入令牌,而有请求过来的时候,就会去桶里取出令牌,能取到就执行,取不到就拒绝或者阻塞。
\\n令牌桶通过定时器的方式向桶中添加令牌,每秒钟添加一定数量的令牌,从而平滑控制了请求的处理速率。这样如果突发流量过来了,只要令牌桶中还有足够的令牌,就可以快速的执行,而不是像漏桶一样还要按照固定速率执行。
\\n令牌桶的好处就是把流量给平滑掉了,在流量不高的时候也会不断的向桶中增加令牌,这样就有足够的令牌可供请求消费。
\\n在Java中,我们可以借助Guava提供的RateLimiter来实现令牌桶
","description":"✅漏桶和令牌桶有啥区别? 漏桶和令牌桶都是用来做流量控制的算法。经常会有人把他们放在一起对比。\\n\\n他们之间的关系是都有一个固定容量的桶,都是按照固定的速率向桶中添加水(或者令牌),但是他们有一个最大的区别,那就是漏桶的这个桶底部是漏的,它同样会按照固定的速率把水流出,所以漏桶输出流量是匀速的,不管输入流量如何变化。而令牌桶的底部不是漏的,他不会以固定的速率流出,只会以固定的速率向桶中添加令牌。\\n\\n以上就是二者的差别。举个例子:\\n\\n漏桶的这个桶,一秒钟流入一滴水,同样一秒钟漏出一滴水。那么,一秒钟就只能处理一个请求,超过的请求会被拒绝掉,达到限流的效果。…","guid":"https://juejin.cn/post/7490676187754676274","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T01:47:59.506Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/581d38b167b24c129b72316f819a9a90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744683500&x-signature=UF3MmqzPOG95m00aMs9rCv7GkZ4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/610efd69f17b44e39f3b7734cc376954~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744683500&x-signature=C23NQSNsaYhnebgGmZfBvWIjo%2FY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"70k star,取代Postman!这款轻量级API工具,太香了!","url":"https://juejin.cn/post/7490440642850586676","content":"\\n\\n作为一名后端开发者,我们经常会使用API工具来调试接口,例如Postman。随着Postman的迭代更新,功能越来越复杂,有时候打开也比较慢。作为开发者我们对API工具的需求很简单:简单好用就可以了,今天给大家分享一款这样的轻量级API工具!
\\n
Hoppscotch是一款开源的API工具,它的功能简单易用,界面也很清新优雅,目前在Github上已有70k+star
。
它具有如下特性:
\\n下面是Hoppscotch使用过程中的效果图,界面还是挺清新的!
\\n设置->语言
中将其设置为中文;设置->主题
中我们可以设置背景和强调色,个人比较喜欢暗色的主题。接下来我们将通过Hoppscotch来调试下电商实战项目mall中的接口
,这里还是简单介绍下mall项目吧,mall项目是一套基于 SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\nREST
面板右侧的导入按钮,选择从OpenAPI导入->从URL导入
;集合
中会看到对应的接口;暂未登录
的结果;授权
里引用这个token就可以正常访问需要登录认证的接口了!Hoppscotch确实是一款非常不错的API工具,它功能简洁易用,而且它也不会像Postman一样每次使用都会提示你去登录,感兴趣的小伙伴可以尝试下它!
\\njstat(JVM Statistics Monitoring Tool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形的服务器上,它是运行期定位虚拟机性能问题的首选工具。
\\njstat位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。可见,Jstat是轻量级的、专门针对JVM的工具,非常适用。
\\njstat 命令格式:
\\njstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]\\n
\\n使用
\\n参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250毫秒查询一次进程5828垃圾收集状况,一共查询5次,那命令行如下:
\\n jstat -gc 5828 250 5\\n
\\n常见用法
\\n对于一个配备4核CPU和8GB内存的机器,如果运行Java应用,系统指标的正常范围会依赖于多种因素,
\\n以下是一些常见的系统指标及其大致的正常范围:
\\n以下,这个只是我认为的经验值,并不是业内通用标准哈。
\\n✅FullGC多久一次算正常?典型回答很多人会在面试的时候,提到频繁FullGC,那有的面试官就会问,你认为多久一次FullGC算是频繁呢?多久一次算是正常呢?其实,Full GC的频率取决于多个因素,包括应用程序的性质、堆的大小、内存分配和释放的模式等。正常情况下,Full GC应该是相对较少发生的,因为频繁的Full ...Java八股
\\n下面是基于上面的信息我整理的表格:
\\n很多人会在面试的时候,提到频繁FullGC,那有的面试官就会问,\\n你认为多久一次FullGC算是频繁呢?多久一次算是正常呢?
\\n其实,Full GC的频率取决于多个因素,包括应用程序的性质、堆的大小、内存分配和释放的模式等。正常情况下,Full GC应该是相对较少发生的,因为频繁的Full GC会导致应用程序的性能下降和响应时间延长。
\\n如果一定要给一个指标,那么我可以给一个经验值,拿我们这面一个非常核心的应用来说:
\\n这个应用日常的QPS在5000以上,线上一共有100台左右的机器。
\\n整个集群,也就是100多台4C8G的机器总体的数据是:
\\n在微信里,关注了某个公众号或者进了某个群,经常会显示你有几个共同的朋友。如图:
\\n作为一个技术出身的人,必然要探究一下其背后技术。如果是自己去实现应该怎么实现呢?
\\n我们要探究两个圈子的共同的朋友,首先我们得有两个圈子,以上面的关注的公众号编程朝花夕拾为例。一个是我自己的朋友圈子,另一个去圈子则是关注这个公众号的圈子,共同的朋友就是两个圈子共同的朋友。
\\n使用数学思想就可以将两个圈子抽象成集合(A,B),朋友抽象成集合中的元素。公共的朋友就是两个集合的交集(A ∩ B)。
\\n至此,就演变成两个集合的运算了。
\\n集合的运算我们需要借助apache
的工具类,maven以来如下:
<dependency>\\n <groupId>org.apache.commons</groupId>\\n <artifactId>commons-collections4</artifactId>\\n <version>${latest.version}</version>\\n</dependency>\\n
\\n集合的交集(A∩B)
\\n两个集合都有的元素:
\\n@Test\\nvoid contextLoads() {\\n // 我的朋友圈\\n List<String> myFriends = List.of(\\n \\"张君宝\\",\\"杨过\\",\\"郭靖\\",\\n \\"黄蓉\\",\\"赵敏\\",\\"周芷若\\",\\n \\"李莫愁\\",\\"小龙女\\",\\"令狐冲\\"\\n );\\n // 公众号朋友圈\\n List<String> gZHFriends = List.of(\\n \\"张君宝\\",\\"黄蓉\\",\\"赵敏\\",\\n \\"周芷若\\",\\"令狐冲\\", \\"胡斐\\",\\n \\"欧阳锋\\", \\"李寻欢\\", \\"林平之\\",\\n \\"岳不群\\", \\"任我行\\"\\n );\\n\\n // 共同的朋友\\n Collection<String> commonFriends = CollectionUtils.intersection(myFriends, gZHFriends);\\n // 输出结果:[赵敏, 张君宝, 令狐冲, 黄蓉, 周芷若]\\n System.out.println(commonFriends);\\n}\\n
\\n集合的并集(A∪B)
\\n两个集合所有的元素
\\n@Test\\nvoid union() {\\n // 我的朋友圈\\n List<String> myFriends = List.of(\\n \\"张君宝\\",\\"杨过\\",\\"郭靖\\",\\n \\"黄蓉\\",\\"赵敏\\",\\"周芷若\\",\\n \\"李莫愁\\",\\"小龙女\\",\\"令狐冲\\"\\n );\\n // 公众号朋友圈\\n List<String> gZHFriends = List.of(\\n \\"张君宝\\",\\"黄蓉\\",\\"赵敏\\",\\n \\"周芷若\\",\\"令狐冲\\", \\"胡斐\\",\\n \\"欧阳锋\\", \\"李寻欢\\", \\"林平之\\",\\n \\"岳不群\\", \\"任我行\\"\\n );\\n\\n // 两个所有的朋友\\n Collection<String> unionFriends = CollectionUtils.union(myFriends, gZHFriends);\\n // 输出结果:\\n // [任我行, 小龙女, 赵敏, 张君宝, 岳不群, 李莫愁, 令狐冲, \\n // 杨过, 黄蓉, 欧阳锋, 李寻欢, 郭靖, 周芷若, 胡斐, 林平之]\\n System.out.println(unionFriends);\\n}\\n
\\n集合的差集(A-B)
\\n集合A中不包含集合B的所有元素
\\n@Test\\nvoid subtract() {\\n // 我的朋友圈\\n List<String> myFriends = List.of(\\n \\"张君宝\\",\\"杨过\\",\\"郭靖\\",\\n \\"黄蓉\\",\\"赵敏\\",\\"周芷若\\",\\n \\"李莫愁\\",\\"小龙女\\",\\"令狐冲\\"\\n );\\n // 公众号朋友圈\\n List<String> gZHFriends = List.of(\\n \\"张君宝\\",\\"黄蓉\\",\\"赵敏\\",\\n \\"周芷若\\",\\"令狐冲\\", \\"胡斐\\",\\n \\"欧阳锋\\", \\"李寻欢\\", \\"林平之\\",\\n \\"岳不群\\", \\"任我行\\"\\n );\\n\\n // 共同的朋友\\n Collection<String> subtractFriends = CollectionUtils.subtract(myFriends, gZHFriends);\\n // 输出结果:[杨过, 郭靖, 李莫愁, 小龙女]\\n System.out.println(subtractFriends);\\n}\\n
\\n思考
\\n这个基于内存的集合运算,如果是你,你会用在微信这个产品上么?
\\n效果是可以实现的,但是最大的缺陷就是基于内存,如果遇到宕机或重启,数据造成数据的丢失。或者说每次启动都需要将各个圈子的数据加载到内存中才可以了。微信的数据量如此庞大,纯内存能不能支撑呢,会不会造成内存溢出呢?这些都是有可能的。
\\n很显然这种方案,可以但是不那么合适。那么有没有什么其他更好的办法呢?那就得拿出我们的大杀器Redis
,轻松应对。
Set
和ZSet
是Redis
中的无序集合和有序集合都可以实现集合的运算,本章以Set
集合为例。
数据准备
\\n# 添加我的朋友圈\\nsadd myFriends \\"张君宝\\" \\"杨过\\" \\"郭靖\\" \\"黄蓉\\" \\"赵敏\\" \\"周芷若\\" \\"李莫愁\\" \\"小龙女\\" \\"令狐冲\\"\\n\\n# 添加公众号的朋友圈\\nsadd gZHFriends \\"张君宝\\" \\"黄蓉\\" \\"赵敏\\" \\"周芷若\\" \\"令狐冲\\" \\"胡斐\\" \\"欧阳锋\\" \\"李寻欢\\" \\"林平之\\" \\"岳不群\\" \\"任我行\\"\\n
\\n交集(A∩B)
\\n# 共同的朋友\\nsinter myFriends gZHFriends\\n
\\n因为Redis客户端的问题,展示成16进制的内容了,翻译之后和集合运算的结果相同。
\\n并集(A∪B)
\\n# 所有的朋友\\nsunion myFriends gZHFriends\\n
\\n这里就不做字符的翻译了,合计15个朋友,同集合运算的结果一致。
\\n差集(A-B)
\\n# A中不包含B中的所有朋友\\nsdiff myFriends gZHFriends\\n
\\n同样获取的结果和集合运算一致。
\\nZset
同样可以实现,并且能够保持有序。
看似小小的功能,里面其实蕴藏着许多设计思想。看似每一种方式都可以实现,但总有场景不适合的,这也是技术选型的难点。
\\nRedis
有其得天独厚的优势,不仅因为它是基于内存计算的,更重要的是它有持久化的功能,RBD和AOF为其保驾护航。速度又快,更不拍数据的丢失。
这就是一个程序员的日常和思考。
\\n关注我的公众号:【编程朝花夕拾】,可获取首发内容。
","description":"1、引言 在微信里,关注了某个公众号或者进了某个群,经常会显示你有几个共同的朋友。如图:\\n\\n作为一个技术出身的人,必然要探究一下其背后技术。如果是自己去实现应该怎么实现呢?\\n\\n2、抽丝剥茧\\n\\n我们要探究两个圈子的共同的朋友,首先我们得有两个圈子,以上面的关注的公众号编程朝花夕拾为例。一个是我自己的朋友圈子,另一个去圈子则是关注这个公众号的圈子,共同的朋友就是两个圈子共同的朋友。\\n\\n使用数学思想就可以将两个圈子抽象成集合(A,B),朋友抽象成集合中的元素。公共的朋友就是两个集合的交集(A ∩ B)。\\n\\n至此,就演变成两个集合的运算了。\\n\\n3、Java中的集合运算…","guid":"https://juejin.cn/post/7490400682303995914","author":"SimonKing","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-07T07:05:39.821Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e0955015eb31467fbb2aa6d8e8caa16c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=WvWylXttOSDkrW%2BRtyvRVbIO6ZE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1f293808ccc145be9b2912dc00e5427f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=uzYL8O9I0zMxkx3HnPLldg8DkJA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06854add92754416938a2460f313cc9a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=j7LFjl2QMYuTegJrg0bpVBrHzSs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89cfc344bffd4f1fa6c6164e846742b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=fNk9IgJORWSTJWgzFPWhZOaQyeo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ebfd1ae329e3470280277186f1dff23b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=v17qCiqWVzrzP6acO73PNY6BOwQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2af2c743eab749ef95cfe53fe63fa4d9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744614339&x-signature=cnXOPwzz0wRMUx5DS7EemlBc5rw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Redis"],"attachments":null,"extra":null,"language":null},{"title":"使用Go降低70%的基础设施成本","url":"https://juejin.cn/post/7490399021728038938","content":"Go性能炸裂!实测对比Go/Gin、Java/SpringBoot、Node/Nestjs等框架,Go在CPU、RAM占用上大幅领先,基础设施成本狂降70%!AOT编译、Goroutines
、GC优化是关键。选型需综合考虑团队技术栈、功能需求和社区支持。
\\n\\n译自:Insights and Optimizations from Benchmarking Frameworks
\\n作者:Lucas Borsatto
\\n
总结: 结果在本文底部的图表中;
\\n在微服务架构中,应用具有饱和度和延迟等指标的可观测性是理解和提高应用程序性能的关键组成部分。虽然算法的选择和代码优化通常对这些指标有最显著的影响——你可以在 Vercel 等网站上查看跨语言的算法比较——但很少有参考文献探讨语言框架本身的效率。
\\n本文旨在通过评估用 Java、Go、Kotlin 和 Node 实现的基本 REST Api,提供更广泛的基准测试视角。为此,我们将使用 Benchmark API,我开发它是为了测量饱和度和延迟指标,并探索我们将在本文中讨论的场景中可能的性能优化。
\\n此基准测试中使用的代码位于此 Github repo,如果您是只想看结果的人,可以在文章末尾找到完整的结果。 这篇文章的灵感来自 Toptal 上的另一篇文章 博客。
\\n该基准测试在 Docker compose 之上运行,您可以在项目的 README 文件中看到更多相关说明。 PC 设置为:
\\n所有基准测试均使用以下 docker compose 配置运行:
\\ndeploy:\\n resources:\\n limits:\\n cpus: \'6\'\\n memory: 8g\\n
\\n由于这里的目标是收集请求延迟、CPU 和 RAM 使用情况,因此所有 API 的开发都遵循以下定义:
\\nApi 的示例请求是:
\\ncurl localhost:8080/benchmark?n=100\\n
\\n该基准测试使用以下 Artillery 配置运行:
\\n— Case 1
\\nWarm up: Arrival rate of 150 users per sec for 60 seconds with N=800\\nSpike: Arrival rate of 300 users per sec for 60 seconds with N=800
\\n— Case 2
\\nWarm up: Arrival rate of 400 users per sec for 60 seconds with N=1\\nSpike: Arrival rate of 550 users per sec for 60 seconds with N=1
\\n— Case 3
\\nWarm up: Arrival rate of 150 users per sec for 60 seconds with N=10\\nSpike: Arrival rate of 300 users per sec for 60 seconds with N=10
\\n现在我们已经确定了基准测试的定义,我们将开始讨论每种语言/框架的一些结果。 最后,我们将对其中的佼佼者进行比较回顾。
\\n首先,我选择 NestJS 作为框架,因为它被广泛使用,并提供了一些优势,例如约定优于配置、控制反转和内置模块化。 在这里,我们将比较两种情况:一种是使用没有优化的带有 Express 的 NestJS,另一种是使用 Fastify 并入 cluster 库的 NestJS。
\\n让我们从 Artillery Case 1 的结果开始:
\\n考虑到我们通过采用 N=800 阻止了事件循环,因此可以预期利用多集群的应用程序会比具有单 CPU 核心的应用程序更好地处理它,尽管 CPU 和 RAM 图表表明了一些令人担忧的峰值。 但是,如果我们将场景转移到 Artillery Case 2,其中涉及更多的并发和更少的 CPU 使用率,该怎么办。 即使那样,我们也会期望优化的应用程序表现更好,对吗? 好吧:
\\n在这里我们得到了第一个教训。 由于多个产生的进程以及我们请求的简单性质,主进程和工作进程之间的通信开销导致更多的 CPU 使用率,这超过了使用带有集群库的多个内核的优势。 鉴于 Node 已经针对 I/O 进行了优化,因此单线程应用程序在这方面表现更好。
\\n考虑到这是一个临界情况,我们可以使用 Artillery Case 3 作为中间情况来确定更有效的方法。 这是结果:
\\n获胜者是使用集群 Fastify 的 NestJS。由此,我们得到了第二个教训:如果我们不关注技术或框架的局限性以及它可能引入的潜在性能瓶颈,那么技术或框架就无法成为解决方案。
\\n当然,Node 通过 --max-old-space-size
等参数为我们提供了一些优化选项,以定义长期对象可以占用多少 RAM。
对于 Java,我选择了一些最常用的框架:SpringBoot、Micronauts 和 Quarkus。
\\nArtillery Case 1 的结果是:
\\n由于结果看起来彼此之间只有细微的差异,让我们检查一下 Artillery Case 2 的结果:
\\nQuarkus 以微弱优势获胜。重要的是要注意,即使使用协程,Kotlin 也没有胜过其他语言。这可能是因为在请求期间实际上没有挂起协程的操作。正如前面提到的,某些语言特性的好处在更复杂的应用程序中往往更明显,在这些应用程序中会应用高性能算法。这适用于协程和其他使 Kotlin 比 Java 具有优势的特性。
\\n另一个需要考虑的点是,此基准测试不包括 Ktor 框架,该框架可以突出显示 Kotlin 可以为 API 性能带来的一些优势。
\\n最后,人们总是可以通过参数 -Xms
和 -Xmx
来提高 RAM 性能,或者使用 -XX:+UseZGC
将应用程序 GC 更改为性能更高的 GC。虽然,这在我们这样简单的 API 中不是必需的。
Go 被认为是市场上性能最高的语言之一。这可能会让我们认为它在任何情况下都会胜过其他语言。是这样吗?在本节中,我们将回顾使用 Chi 和 Gin 的 Go 的基准测试结果,它们是两个最流行的 Go 框架。以下是 Artillery Case 1 的图表:
\\n好吧,看起来 Chi 确实胜过了其他框架,但这种情况对 Gin 不利。除了高 CPU 消耗外,延迟达到了 8 秒。当我们查看下面 Gin 的 Artillery 报告时,情况会变得更糟:
\\n--------------------------------\\nSummary report @ 11:57:36(-0300)\\n--------------------------------\\nerrors.ETIMEDOUT: ................................................. 23558\\nhttp.codes.200: ................................................... 3442\\nhttp.downloaded_bytes: ............................................ 6884\\nhttp.request_rate: ................................................ 210/sec\\nhttp.requests: .................................................... 27000\\n
\\n实际上,大多数请求都导致超时。由此,我们得出结论,Gin 在需要高 CPU 使用率的场景中可能会遇到困难,即使应用程序处理 800 次重复循环的情况确实不常见。
\\n尽管第一个案例的结果如此,但 Artillery Case 2 的结果似乎更好一些:
\\nGin 取得了明显更好的结果,但此场景的明显赢家是 Chi。此外,Gin 似乎更能够处理更温和的场景。
\\n为了进一步提高应用程序性能,可以尝试更改环境变量 GOMAXPROCS
,该变量具有取决于服务器设置的默认值。
以下是总结的最终结果。使用 Artillery Case 1:
\\n使用 Artillery Case 2:
\\n总而言之,Go 显然是此基准测试中性能最高的框架。正如您在之前的图表中看到的那样,它使用的 RAM 少 3 倍,CPU 少 4 倍,并且由于我们看到的延迟百分位数,我们甚至可以在生产环境中减少实例数量,从而显着降低基础设施成本。这有一些原因:
\\n当然,为您的项目选择语言和框架应基于您团队的熟练程度、功能和性能要求,以及该语言和框架拥有的社区支持程度。
","description":"Go性能炸裂!实测对比Go/Gin、Java/SpringBoot、Node/Nestjs等框架,Go在CPU、RAM占用上大幅领先,基础设施成本狂降70%!AOT编译、Goroutines、GC优化是关键。选型需综合考虑团队技术栈、功能需求和社区支持。 译自:Insights and Optimizations from Benchmarking Frameworks\\n\\n作者:Lucas Borsatto\\n\\n总结: 结果在本文底部的图表中;\\n\\n在微服务架构中,应用具有饱和度和延迟等指标的可观测性是理解和提高应用程序性能的关键组成部分…","guid":"https://juejin.cn/post/7490399021728038938","author":"云云众生s","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-07T04:01:05.399Z","media":[{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997813/nnshkr5zq6mydlxrlnvn.png","type":"photo","width":1194,"height":580,"blurhash":"LqPGj*~lxpRqRqt5odWEM~t3ocRl"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997831/ftmr2gc7kbjftqplkw3h.png","type":"photo","width":1189,"height":576,"blurhash":"LkQcoH~lM}oh%Lj[R,t5ayayayay"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997861/pwpdwqrmolmvvzgupu8w.png","type":"photo","width":1184,"height":581,"blurhash":"LaQ,LJ~lk8oi%Mfkj[ofayj?WCaz"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997872/nrjwmlnzswqqzwsdtiy1.png","type":"photo","width":1183,"height":583,"blurhash":"LgQT4i~locj_%La#a#oeazj@fQaz"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997881/p1bp3vwrrlnw2s9jcofa.png","type":"photo","width":1182,"height":577,"blurhash":"LgQJf;~lovoh%MfkWEoeazj@j@az"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997894/sscvqscfn1xgrkxenhb9.png","type":"photo","width":1187,"height":597,"blurhash":"LiQ,LJ~lM~t8%Lj[WEt6oeazj@j@"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997924/xo24gbvat1ekglmy15vs.png","type":"photo","width":1186,"height":572,"blurhash":"LkQmC?~lM~t8xuoeWEt6WCj?ayaz"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997936/jyauxykaq3l7yqq5iwo4.png","type":"photo","width":1184,"height":572,"blurhash":"LkP%PD~lxpRq?YN1Rnt5%HM~j@t4"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997948/tpaq5ioaookbspx9x2fn.png","type":"photo","width":1178,"height":570,"blurhash":"LfQ,LJ~lM~t8-:RnWWoxocWCodWD"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997958/vq1z4fpwjjfrtnfodczx.png","type":"photo","width":1167,"height":851,"blurhash":"LbRC[K~lIYxvt9t5WDoLWCodWDod"},{"url":"https://res.cloudinary.com/dkrpg71cx/image/upload/v1743997970/rkjduqakatubsjxleami.png","type":"photo","width":1167,"height":849,"blurhash":"LcRMb]~lD.-pxvt5WDoeWCodazj?"}],"categories":["后端","Go","Java","Node.js"],"attachments":null,"extra":null,"language":null},{"title":"新来的技术总监,把DDD落地的那叫一个高级优雅!","url":"https://juejin.cn/post/7489738724574658612","content":"大家好,我是田螺.
\\n我们在日常开发中,经常听到DDD,那么DDD到底是什么呢? 我之前也看过一些网络上的文章,都是写了一大堆的文字,比较羞涩难懂.本文打算跟大家聊聊DDD.让大家看清楚它的模样~
\\n\\n\\nDDD(Domain-Driven Design,领域驱动设计)是一种通过聚焦业务领域来构建复杂系统的软件开发方法,核心思想是将代码结构与业务领域的实际需求深度结合。
\\n
一句话简单概括就是:用代码还原业务本质,而非实现功能。
\\n其实这些概念上的东西,看完过会,还是很容易忘记对吧~ 我们来看一个代码例子吧~
\\n假设我们做一个用户注册的例子,业务规则如下:
\\n传统模式,于是可以快速写出以下代码:
\\n@Controller\\npublic class UserController {\\n public void register(String username, String password) {\\n // 校验密码\\n // 检查用户名\\n // 保存数据库\\n // 记录日志\\n // 所有逻辑混在一起\\n }\\n}\\n
\\n有些伙伴说,哪有所有代码混合在controller,肯定要分层的呀,比如分controller、service、dao层. 于是写出类似这样的代码:
\\n// Service层:仅有流程控制,业务规则散落在各处\\npublic class UserService {\\n public void register(User user) {\\n // 校验规则1:写在工具类里\\n ValidationUtil.checkPassword(user.getPassword()); \\n // 校验规则2:通过注解实现\\n if (userRepository.exists(user)) { ... }\\n // 数据直接传递到DAO\\n userDao.save(user); \\n }\\n}\\n
\\n你还别说,这快代码,其实流程已经比较清晰了~ 有些伙伴,满怀激动地说,已经分层了,代码已经很优雅清晰了,这就是DDD了吧.
\\n答案是,NO!
\\n以上代码虽然分层了,代码结构划分了,但是它还不是DDD.
\\n其实对于传统的分层的那块代码,User对象仅是数据载体(贫血模型),业务逻辑被拆解到外部了.对于DDD,其实一些逻辑,可以内聚到领域User对象中的. 如密码规则的校验.
\\n对于这个注册的例子, DDD的正确姿势(充血模型)如下:
\\n// 领域实体:业务逻辑内聚\\npublic class User {\\n public User(String username, String password) {\\n // 密码规则内聚到构造函数\\n if (!isValidPassword(password)) { \\n throw new InvalidPasswordException();\\n }\\n this.username = username;\\n this.password = encrypt(password);\\n }\\n\\n // 密码复杂度校验是实体的职责\\n private boolean isValidPassword(String password) { ... }\\n}\\n
\\n其实把校验密码的下沉到User 领域实体对象里了.专业点说法,就是业务规则被封装在领域对象内部,对象不再只是“数据袋子”。
\\n所以,DDD 就是把一些逻辑下沉到领域对象中?
\\n\\n\\n不全对~
\\n
其实处了分层,DDD的关键设计,体现在以下模式深化业务表达:
\\npublic class User {\\n private List<Address> addresses;\\n\\n // 添加地址的逻辑由聚合根控制\\n public void addAddress(Address address) {\\n if (addresses.size() >= 5) {\\n throw new AddressLimitExceededException();\\n }\\n addresses.add(address);\\n }\\n}\\n
\\n// 领域服务:处理核心业务逻辑\\npublic class TransferService {\\n public void transfer(Account from, Account to, Money amount) {\\n from.debit(amount); // 账户扣款逻辑内聚在Account实体\\n to.credit(amount);\\n }\\n}\\n\\n// 应用服务:编排流程,不包含业务规则\\npublic class BankingAppService {\\n public void executeTransfer(Long fromId, Long toId, BigDecimal amount) {\\n Account from = accountRepository.findById(fromId);\\n Account to = accountRepository.findById(toId);\\n transferService.transfer(from, to, new Money(amount));\\n messageQueue.send(new TransferEvent(...)); // 基础设施操作\\n \\n
\\npublic class User {\\n public void register() {\\n // ...注册逻辑\\n this.events.add(new UserRegisteredEvent(this.id)); // 记录领域事件\\n }\\n}\\n
\\n简单总结一下传统开发和DDD的区别~
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n维度 | 传统开发 | DDD |
---|---|---|
业务逻辑归属 | 散落在Service、Util、Controller | 内聚在领域实体/领域服务 |
模型作用 | 数据载体(贫血模型) | 携带行为的业务模型(充血模型) |
| 技术实现影响 | 数据库表驱动设计 |业务需求驱动表结构设计 |
\\n为了方便大家理解,再给大家来个DDD的案例.给大家解解渴、润润喉
\\n假设有个需求:
\\n\\n\\n用户下单需实现:校验库存、用优惠券、计算实付金额、生成订单。
\\n
/**\\n * \\n * 公众号:捡田螺的小男孩\\n **/\\n// Service层:大杂烩式下单\\npublic class OrderService {\\n @Autowired private InventoryDAO inventoryDAO;\\n @Autowired private CouponDAO couponDAO;\\n \\n public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) {\\n // 1. 校验库存(散落在Service)\\n for (ItemDTO item : items) {\\n Integer stock = inventoryDAO.getStock(item.getSkuId());\\n if (item.getQuantity() > stock) {\\n throw new RuntimeException(\\"库存不足\\");\\n }\\n }\\n \\n // 2. 计算总价\\n BigDecimal total = items.stream()\\n .map(i -> i.getPrice().multiply(i.getQuantity()))\\n .reduce(BigDecimal.ZERO, BigDecimal::add);\\n \\n // 3. 应用优惠券(规则写在工具类)\\n if (couponId != null) {\\n Coupon coupon = couponDAO.getById(couponId);\\n total = CouponUtil.applyCoupon(coupon, total); // 优惠逻辑隐藏在Util\\n }\\n \\n // 4. 保存订单(纯数据操作)\\n Order order = new Order();\\n order.setUserId(userId);\\n order.setTotalAmount(total);\\n orderDAO.save(order);\\n return order;\\n }\\n}\\n
\\n传统方式存在的问题:
\\n/**\\n * \\n * 更多干货,关注公众号:捡田螺的小男孩\\n **/\\n// 聚合根:Order(承载核心逻辑)\\npublic class Order {\\n private List<OrderItem> items;\\n private Coupon coupon;\\n private Money totalAmount;\\n\\n // 构造函数内聚业务逻辑\\n public Order(User user, List<OrderItem> items, Coupon coupon) {\\n // 1. 校验库存(领域规则内聚)\\n items.forEach(item -> item.checkStock());\\n \\n // 2. 计算总价(业务逻辑在值对象)\\n this.totalAmount = items.stream()\\n .map(OrderItem::subtotal)\\n .reduce(Money.ZERO, Money::add);\\n \\n // 3. 应用优惠券(规则在实体内部)\\n if (coupon != null) {\\n validateCoupon(coupon, user); // 优惠券使用规则内聚\\n this.totalAmount = coupon.applyDiscount(this.totalAmount);\\n }\\n }\\n\\n // 优惠券校验逻辑(业务归属清晰)\\n private void validateCoupon(Coupon coupon, User user) {\\n if (!coupon.isValid() || !coupon.isApplicable(user)) {\\n throw new InvalidCouponException();\\n }\\n }\\n}\\n\\n// 领域服务:协调下单流程\\npublic class OrderService {\\n public Order createOrder(User user, List<Item> items, Coupon coupon) {\\n Order order = new Order(user, convertItems(items), coupon);\\n orderRepository.save(order);\\n domainEventPublisher.publish(new OrderCreatedEvent(order)); // 领域事件\\n return order;\\n }\\n}\\n
\\n改为DDD后的优点:
\\n假设产品又出了个新需求:优惠券需满足“订单满100减20”,且仅限新用户使用。
\\n传统开发的方式影响了Service层、Util类,因为需要修改
\\n1. 修改CouponUtil.applyCoupon()逻辑\\n2. 在Service层添加新用户校验\\n
\\n而DDD 只影响了领域层,因为只需要修改:
\\n仅修改Order.validateCoupon()方法\\n
\\n其实,是不是什么场景,都要使用DDD呢? 不是的,那就是小题大作啦~
\\n我觉得这句话有点道理:
\\n\\n\\n当你发现修改业务规则时,只需调整领域层代码,而无需改动Controller或DAO,这才是DDD真正落地。
\\n
让代码和业务长成连体婴,改需求不再是程序员的噩梦 !!!
\\n一直坚持原创不易,大家给个三连支持一下哈. 如果你觉得本文有不对的地方,可以在评论区留言讨论哈~
","description":"前言 大家好,我是田螺.\\n\\n我们在日常开发中,经常听到DDD,那么DDD到底是什么呢? 我之前也看过一些网络上的文章,都是写了一大堆的文字,比较羞涩难懂.本文打算跟大家聊聊DDD.让大家看清楚它的模样~\\n\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\n1. 什么是DDD\\n\\nDDD(Domain-Driven Design,领域驱动设计)是一种通过聚焦业务领域来构建复杂系统的软件开发方法,核心思想是将代码结构与业务领域的实际需求深度结合。\\n\\n一句话简单概括就是:用代码还原业务本质,而非实现功能。…","guid":"https://juejin.cn/post/7489738724574658612","author":"捡田螺的小男孩","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-07T01:07:31.478Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"用 Go 语言轻松构建 MCP 客户端与服务器","url":"https://juejin.cn/post/7489418732500615220","content":"\\n\\n该文章已被 Model Context Protocol(MCP) 中文教程讲解 收录,欢迎 star 收藏。
\\n若想获取可执行的完整项目代码,可关注公众号:程序员陈明勇,回复 MCP。
\\n
模型上下文协议(Model Context Protocol
,简称 MCP
)是一种开放标准,旨在标准化大型语言模型(LLM
)与外部数据源和工具之间的交互方式。随着 MCP
越来越受欢迎,Go MCP
库应运而生。本文将介绍如何在 Go
语言里面轻松构建 MCP
客户端和服务器。
如果你不熟悉 MCP
协议,可以看我之前写的这篇文章:一文掌握 MCP 上下文协议:从理论到实践。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
\\n要构建 MCP
客户端和服务器,我们需要使用 mcp-go
库。
mcp-go
是 Go
语言实现的 Model Context Protocol
(MCP
)库,通过这个库可以实现 LLM
应用与外部数据源和工具之间的无缝集成。
快速:高级接口意味着更少的代码和更快的开发速度
\\n简单:使用极少的样板代码构建 MCP
服务器
完整:MCP Go
旨在提供 MCP
核心规范的完整实现
在 Go
项目根目录下,执行以下命令:
go get github.com/mark3labs/mcp-go\\n
\\n接下来,我们使用 mcp-go
提供的 server
模块,构建一个通过 stidio
方式连接的 MCP
服务器。
s := server.NewMCPServer(\\n \\"Server Demo\\",\\n \\"1.0.0\\",\\n)\\n
\\n创建 server
对象时,我们可以指定 服务器名,版本号 等参数。
以下是一个示例,用于创建并注册一个简单的计算器工具:
\\ncalculatorTool := mcp.NewTool(\\"calculate\\",\\n mcp.WithDescription(\\"执行基本的算术运算\\"),\\n mcp.WithString(\\"operation\\",\\n mcp.Required(),\\n mcp.Description(\\"要执行的算术运算类型\\"),\\n mcp.Enum(\\"add\\", \\"subtract\\", \\"multiply\\", \\"divide\\"), // 保持英文\\n ),\\n mcp.WithNumber(\\"x\\",\\n mcp.Required(),\\n mcp.Description(\\"第一个数字\\"),\\n ),\\n mcp.WithNumber(\\"y\\",\\n mcp.Required(),\\n mcp.Description(\\"第二个数字\\"),\\n ),\\n)\\n\\ns.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\\n op := request.Params.Arguments[\\"operation\\"].(string)\\n x := request.Params.Arguments[\\"x\\"].(float64)\\n y := request.Params.Arguments[\\"y\\"].(float64)\\n\\n var result float64\\n switch op {\\n case \\"add\\":\\n result = x + y\\n case \\"subtract\\":\\n result = x - y\\n case \\"multiply\\":\\n result = x * y\\n case \\"divide\\":\\n if y == 0 {\\n return nil, errors.New(\\"不允许除以零\\")\\n }\\n result = x / y\\n }\\n\\n return mcp.FormatNumberResult(result), nil\\n})\\n
\\n添加工具的步骤如下:
\\n创建工具对象\\n使用 mcp.NewTool
创建一个工具实例。
\\"calculate\\"
。functional options
)方式传入,例如:\\nmcp.WithDescription(...)
添加工具描述;mcp.WithString(...)
或 mcp.WithNumber(...)
定义参数及其规则(如是否必填、参数说明、枚举限制等)。注册工具到服务器\\n通过 s.AddTool
方法将工具注册到 MCP
服务中。
下面的示例展示了如何创建并注册一个静态资源,用于读取并提供 README.md
文件的内容。
resource := mcp.NewResource(\\n \\"docs://readme\\",\\n \\"项目说明文档\\",\\n mcp.WithResourceDescription(\\"项目的 README 文件\\"),\\n mcp.WithMIMEType(\\"text/markdown\\"),\\n)\\n\\ns.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\\n content, err := os.ReadFile(\\"README.md\\")\\n if err != nil {\\n return nil, err\\n }\\n\\n return []mcp.ResourceContents{\\n mcp.TextResourceContents{\\n URI: \\"docs://readme\\",\\n MIMEType: \\"text/markdown\\",\\n Text: string(content),\\n },\\n }, nil\\n})\\n
\\n添加资源的步骤如下:
\\n创建资源对象
\\n使用 mcp.NewResource
函数创建资源实例。
mcp.WithResourceDescription(...)
设置资源描述;mcp.WithMIMEType(...)
指定资源的 MIME 类型。注册资源处理函数
\\n使用 s.AddResource
将资源对象注册到服务器,并提供一个处理函数:
TextResourceContents
)。以下示例展示了如何创建并添加一个带参数的简单提示词,用于生成个性化的问候语。
\\ns.AddPrompt(mcp.NewPrompt(\\"greeting\\",\\n mcp.WithPromptDescription(\\"一个友好的问候提示\\"),\\n mcp.WithArgument(\\"name\\",\\n mcp.ArgumentDescription(\\"要问候的人的名字\\"),\\n ),\\n), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\\n name := request.Params.Arguments[\\"name\\"]\\n if name == \\"\\" {\\n name = \\"朋友\\"\\n }\\n\\n return mcp.NewGetPromptResult(\\n \\"友好的问候\\",\\n []mcp.PromptMessage{\\n mcp.NewPromptMessage(\\n mcp.RoleAssistant,\\n mcp.NewTextContent(fmt.Sprintf(\\"你好,%s!今天有什么可以帮您的吗?\\", name)),\\n ),\\n },\\n ), nil\\n})\\n
\\n添加提示词的步骤如下:
\\n创建提示词对象
\\n通过 mcp.NewPrompt
创建一个提示词定义。
mcp.WithPromptDescription(...)
添加描述;mcp.WithArgument(...)
定义参数及其说明(如提示词中需要动态插值的内容)。注册提示词处理函数
\\n使用 s.AddPrompt
将提示词对象注册到服务器,并提供对应的处理逻辑函数:
stdio
传输类型的服务器// 启动基于 stdio 的服务器\\nif err := server.ServeStdio(s); err != nil {\\n fmt.Printf(\\"Server error: %v\\\\n\\", err)\\n}\\n
\\n使用 server.ServeStdio
方法可以启动一个基于标准输入/输出(stdio
)的 MCP
服务器。
这种方式适用于本地集成与命令行工具。
\\nsse
(Server-Sent Events)传输类型的服务器如果需要通过 HTTP
的方式提供服务,支持服务端推送数据,可以使用 SS
E(Server-Sent Events
)传输模式。
s := server.NewMCPServer(\\n \\"My Server\\", // Server 名称\\n \\"1.0.0\\", // 版本号\\n)\\n\\n// 创建基于 SSE 的服务器实例\\nsseServer := server.NewSSEServer(s)\\n\\n// 启动服务器,监听指定端口(如 :8080)\\nerr := sseServer.Start(\\":8080\\")\\nif err != nil {\\n panic(err)\\n}\\n\\n
\\n与 stdio
不同,sse
模式基于 HTTP
协议,更适合 Web
应用中的长连接场景,支持服务端推送数据。
package main\\n\\nimport (\\n\\"context\\"\\n\\"errors\\"\\n\\"fmt\\"\\n\\"os\\"\\n\\n\\"github.com/mark3labs/mcp-go/mcp\\"\\n\\"github.com/mark3labs/mcp-go/server\\"\\n)\\n\\nfunc main() {\\ns := server.NewMCPServer(\\n\\"Server Demo\\",\\n\\"1.0.0\\",\\n)\\n\\n// 添加工具\\n{\\ncalculatorTool := mcp.NewTool(\\"calculate\\",\\nmcp.WithDescription(\\"执行基本的算术运算\\"),\\nmcp.WithString(\\"operation\\",\\nmcp.Required(),\\nmcp.Description(\\"要执行的算术运算类型\\"),\\nmcp.Enum(\\"add\\", \\"subtract\\", \\"multiply\\", \\"divide\\"), // 保持英文\\n),\\nmcp.WithNumber(\\"x\\",\\nmcp.Required(),\\nmcp.Description(\\"第一个数字\\"),\\n),\\nmcp.WithNumber(\\"y\\",\\nmcp.Required(),\\nmcp.Description(\\"第二个数字\\"),\\n),\\n)\\n\\ns.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\\nop := request.Params.Arguments[\\"operation\\"].(string)\\nx := request.Params.Arguments[\\"x\\"].(float64)\\ny := request.Params.Arguments[\\"y\\"].(float64)\\n\\nvar result float64\\nswitch op {\\ncase \\"add\\":\\nresult = x + y\\ncase \\"subtract\\":\\nresult = x - y\\ncase \\"multiply\\":\\nresult = x * y\\ncase \\"divide\\":\\nif y == 0 {\\nreturn nil, errors.New(\\"不允许除以零\\")\\n}\\nresult = x / y\\n}\\n\\nreturn mcp.FormatNumberResult(result), nil\\n})\\n}\\n\\n// 添加资源\\n{\\n// 静态资源示例 - 暴露一个 README 文件\\nresource := mcp.NewResource(\\n\\"docs://readme\\",\\n\\"项目说明文档\\",\\nmcp.WithResourceDescription(\\"项目的 README 文件\\"),\\nmcp.WithMIMEType(\\"text/markdown\\"),\\n)\\n\\n// 添加资源及其处理函数\\ns.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\\ncontent, err := os.ReadFile(\\"README.md\\")\\nif err != nil {\\nreturn nil, err\\n}\\n\\nreturn []mcp.ResourceContents{\\nmcp.TextResourceContents{\\nURI: \\"docs://readme\\",\\nMIMEType: \\"text/markdown\\",\\nText: string(content),\\n},\\n}, nil\\n})\\n}\\n\\n// 添加提示词\\n{\\n// 简单问候提示\\ns.AddPrompt(mcp.NewPrompt(\\"greeting\\",\\nmcp.WithPromptDescription(\\"一个友好的问候提示\\"),\\nmcp.WithArgument(\\"name\\",\\nmcp.ArgumentDescription(\\"要问候的人的名字\\"),\\n),\\n), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\\nname := request.Params.Arguments[\\"name\\"]\\nif name == \\"\\" {\\nname = \\"朋友\\"\\n}\\n\\nreturn mcp.NewGetPromptResult(\\n\\"友好的问候\\",\\n[]mcp.PromptMessage{\\nmcp.NewPromptMessage(\\nmcp.RoleAssistant,\\nmcp.NewTextContent(fmt.Sprintf(\\"你好,%s!今天有什么可以帮您的吗?\\", name)),\\n),\\n},\\n), nil\\n})\\n}\\n\\n// 启动基于 stdio 的服务器\\nif err := server.ServeStdio(s); err != nil {\\nfmt.Printf(\\"Server error: %v\\\\n\\", err)\\n}\\n\\n}\\n\\n
\\n接下来,我们使用 mcp-go
提供的 client
模块,构建一个通过 stdio
方式连接到前面打包好的 MCP
服务器的客户端。
该客户端将展示以下功能:
\\nmcpClient, err := client.NewStdioMCPClient(\\n \\"./client/server\\", // 服务器可执行文件路径\\n []string{}, // 启动参数(如果有)\\n)\\nif err != nil {\\n panic(err)\\n}\\ndefer mcpClient.Close()\\n
\\n通过 client.NewStdioMCPClient
方法可以创建一个基于 stdio
传输的客户端,并连接到指定的 MCP
服务器可执行文件。
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\\ndefer cancel()\\n\\ninitRequest := mcp.InitializeRequest{}\\ninitRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION\\ninitRequest.Params.ClientInfo = mcp.Implementation{\\n Name: \\"Client Demo\\",\\n Version: \\"1.0.0\\",\\n}\\n\\ninitResult, err := mcpClient.Initialize(ctx, initRequest)\\nif err != nil {\\n panic(err)\\n}\\nfmt.Printf(\\"初始化成功,服务器信息: %s %s\\\\n\\", initResult.ServerInfo.Name, initResult.ServerInfo.Version)\\n
\\n初始化操作通过 Initialize
方法完成,需指定协议版本及客户端信息。
promptsRequest := mcp.ListPromptsRequest{}\\nprompts, err := mcpClient.ListPrompts(ctx, promptsRequest)\\nif err != nil {\\n panic(err)\\n}\\nfor _, prompt := range prompts.Prompts {\\n fmt.Printf(\\"- %s: %s\\\\n\\", prompt.Name, prompt.Description)\\n fmt.Println(\\"参数:\\", prompt.Arguments)\\n}\\n
\\n客户端可以使用 ListPrompts
获取服务器上定义的所有提示词,包括名称、描述和参数结构。
resourcesRequest := mcp.ListResourcesRequest{}\\nresources, err := mcpClient.ListResources(ctx, resourcesRequest)\\nif err != nil {\\n panic(err)\\n}\\nfor _, resource := range resources.Resources {\\n fmt.Printf(\\"- uri: %s, name: %s, description: %s, MIME类型: %s\\\\n\\",\\n resource.URI, resource.Name, resource.Description, resource.MIMEType)\\n}\\n
\\n通过 ListResources
方法,客户端可以查看服务器上可用的静态或动态资源信息。
toolsRequest := mcp.ListToolsRequest{}\\ntools, err := mcpClient.ListTools(ctx, toolsRequest)\\nif err != nil {\\n panic(err)\\n}\\nfor _, tool := range tools.Tools {\\n fmt.Printf(\\"- %s: %s\\\\n\\", tool.Name, tool.Description)\\n fmt.Println(\\"参数:\\", tool.InputSchema.Properties)\\n}\\n
\\n通过 ListTools
,客户端可以获取所有注册的工具信息,方便用户交互式选择或自动生成表单调用。
toolRequest := mcp.CallToolRequest{\\n Request: mcp.Request{\\n Method: \\"tools/call\\",\\n },\\n}\\ntoolRequest.Params.Name = \\"calculate\\"\\ntoolRequest.Params.Arguments = map[string]any{\\n \\"operation\\": \\"add\\",\\n \\"x\\": 1,\\n \\"y\\": 1,\\n}\\n\\nresult, err := mcpClient.CallTool(ctx, toolRequest)\\nif err != nil {\\n panic(err)\\n}\\nfmt.Println(\\"调用工具结果:\\", result.Content[0].(mcp.TextContent).Text)\\n
\\n通过构造 CallToolRequest
,客户端可以向 MCP
服务器发起工具调用请求,并获取返回的结构化结果。
在此示例中,我们调用了服务器端注册的 calculate
工具,实现 1 + 1
运算。
package main\\n\\nimport (\\n\\"context\\"\\n\\"fmt\\"\\n\\"time\\"\\n\\n\\"github.com/mark3labs/mcp-go/client\\"\\n\\"github.com/mark3labs/mcp-go/mcp\\"\\n)\\n\\nfunc main() {\\n\\n// 创建一个基于 stdio 的MCP客户端\\nmcpClient, err := client.NewStdioMCPClient(\\n\\"./client/server\\",\\n[]string{},\\n)\\nif err != nil {\\npanic(err)\\n}\\ndefer mcpClient.Close()\\n\\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\\ndefer cancel()\\n\\nfmt.Println(\\"初始化 mcp 客户端...\\")\\ninitRequest := mcp.InitializeRequest{}\\ninitRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION\\ninitRequest.Params.ClientInfo = mcp.Implementation{\\nName: \\"Client Demo\\",\\nVersion: \\"1.0.0\\",\\n}\\n\\n// 初始化MCP客户端并连接到服务器\\ninitResult, err := mcpClient.Initialize(ctx, initRequest)\\nif err != nil {\\npanic(err)\\n}\\nfmt.Printf(\\n\\"\\\\n初始化成功,服务器信息: %s %s\\\\n\\\\n\\",\\ninitResult.ServerInfo.Name,\\ninitResult.ServerInfo.Version,\\n)\\n\\n// 从服务器获取提示词列表\\nfmt.Println(\\"提示词列表:\\")\\npromptsRequest := mcp.ListPromptsRequest{}\\nprompts, err := mcpClient.ListPrompts(ctx, promptsRequest)\\nif err != nil {\\npanic(err)\\n}\\nfor _, prompt := range prompts.Prompts {\\nfmt.Printf(\\"- %s: %s\\\\n\\", prompt.Name, prompt.Description)\\nfmt.Println(\\"参数:\\", prompt.Arguments)\\n}\\n\\n// 从服务器获取资源列表\\nfmt.Println()\\nfmt.Println(\\"资源列表:\\")\\nresourcesRequest := mcp.ListResourcesRequest{}\\nresources, err := mcpClient.ListResources(ctx, resourcesRequest)\\nif err != nil {\\npanic(err)\\n}\\nfor _, resource := range resources.Resources {\\nfmt.Printf(\\"- uri: %s, name: %s, description: %s, MIME类型: %s\\\\n\\", resource.URI, resource.Name, resource.Description, resource.MIMEType)\\n}\\n\\n// 从服务器获取工具列表\\nfmt.Println()\\nfmt.Println(\\"可用工具列表:\\")\\ntoolsRequest := mcp.ListToolsRequest{}\\ntools, err := mcpClient.ListTools(ctx, toolsRequest)\\nif err != nil {\\npanic(err)\\n}\\n\\nfor _, tool := range tools.Tools {\\nfmt.Printf(\\"- %s: %s\\\\n\\", tool.Name, tool.Description)\\nfmt.Println(\\"参数:\\", tool.InputSchema.Properties)\\n}\\nfmt.Println()\\n\\n// 调用工具\\nfmt.Println(\\"调用工具: calculate\\")\\ntoolRequest := mcp.CallToolRequest{\\nRequest: mcp.Request{\\nMethod: \\"tools/call\\",\\n},\\n}\\ntoolRequest.Params.Name = \\"calculate\\"\\ntoolRequest.Params.Arguments = map[string]any{\\n\\"operation\\": \\"add\\",\\n\\"x\\": 1,\\n\\"y\\": 1,\\n}\\n// Call the tool\\nresult, err := mcpClient.CallTool(ctx, toolRequest)\\nif err != nil {\\npanic(err)\\n}\\nfmt.Println(\\"调用工具结果:\\", result.Content[0].(mcp.TextContent).Text)\\n}\\n\\n
\\n\\n\\n若想获取可执行的完整项目代码,可关注公众号:程序员陈明勇,回复 MCP。
\\n
本文介绍了如何使用 mcp-go
构建一个完整的 MCP
应用,包括服务端和客户端两部分。
stdio
或 sse
模式对外提供服务;stdio
连接服务器,支持初始化、列出服务内容、调用远程工具等操作。你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
\\n我专注于分享 Go
语言相关的技术知识,同时也会深入探讨 AI
领域的前沿技术。
成功的路上并不拥挤,有没有兴趣结个伴?
","description":"该文章已被 Model Context Protocol(MCP) 中文教程讲解 收录,欢迎 star 收藏。 若想获取可执行的完整项目代码,可关注公众号:程序员陈明勇,回复 MCP。\\n\\n前言\\n\\n模型上下文协议(Model Context Protocol,简称 MCP)是一种开放标准,旨在标准化大型语言模型(LLM)与外部数据源和工具之间的交互方式。随着 MCP 越来越受欢迎,Go MCP 库应运而生。本文将介绍如何在 Go 语言里面轻松构建 MCP 客户端和服务器。\\n\\n如果你不熟悉 MCP 协议,可以看我之前写的这篇文章:一文掌握 MCP 上下文协议…","guid":"https://juejin.cn/post/7489418732500615220","author":"陈明勇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T16:58:37.058Z","media":[{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af4d5f7ce922427ab24d63b526270c33~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1080&h=350&s=104940&e=png&b=fefefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5712aba684154a52abc8833dbaac4e6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZmI5piO5YuH:q75.awebp?rk3s=f64ab15b&x-expires=1744563517&x-signature=U1XrYPpCTC1DM3%2FQvEeBvIgZHRA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Go","MCP"],"attachments":null,"extra":null,"language":null},{"title":"怎么自学嵌入式?","url":"https://juejin.cn/post/7489405244038037538","content":"大家好,我是良许。
\\n十年前我特么真是瞎了眼,机械专业毕业后居然接了个嵌入式的offer。第一天上班,我的领导扔给我一本STM32的数据手册,厚得能砸死人,然后丢下一句\\"下周之前把GPIO的例程写出来\\"就走了...
\\n当时我就傻了,GPIO是个啥玩意?C语言也就课上混了60分的水平,连指针都整不明白。那段日子真是度日如年,晚上做噩梦都是寄存器地址和时钟配置。
\\n好在功夫不负有心人,这些年摸爬滚打下来,我从一个连LED都点不亮的菜鸟,变成能独当一面的嵌入式工程师。去年我把多年踩坑经验整理成了《STM32实战快速入门》(点击直达)课程,也算是给走投无路的后浪们提供条生路了。
\\n啰嗦这么多,进入正题,这里我把自学嵌入式的套路和坑都给你交底。
\\n刚上手别急着买开发板,市面上那些动辄上百的\\"豪华套装\\",大多是智商税。我最开始就被坑了,买了个所谓的\\"嵌入式学习套件\\",结果里面一堆用不上的传感器模块,接口还不通用,现在还在柜子里吃灰。
\\n其实一块STM32F103C8T6的\\"蓝色药丸\\"开发板真的就够了,20多块钱,麻雀虽小五脏俱全。小破站里很多课程里的所有例程都是基于这个板子设计的。
\\n除了开发板,你还需要:
\\n这套配置总共50块左右,能满足入门一年内的全部需求了。别问我为啥知道,因为我第一份工资对半砍用来买书和开发板,结果大部分都用不上,老板还纳闷为啥我总是月底蹭同事泡面吃。
\\n很多培训机构画的路线图又臭又长,搞得好像不把整个计算机科学学一遍就不能写嵌入式似的。扯淡!
\\n我的建议是先搞定这几样:
\\n大学里教的C语言跟嵌入式用的C语言简直是两回事。你需要特别关注:
\\n说个笑话,我刚入职时有次代码审查,主管问我:\\"你这个变量为啥不用volatile修饰?\\" 我一脸懵逼:\\"volatile是啥?能吃吗?\\" 然后全会议室都笑了...
\\n别浪费时间在51单片机上了,那玩意是上个世纪的产物。虽然有人说\\"先学51打基础\\",但我觉得那跟说\\"学开车先学马车\\"一样扯。
\\nSTM32用HAL库开发其实挺简单的,我之前带过一个实习生,没啥基础,一周就能点灯控制按键了。在我的《STM32实战快速入门》(点击直达)课程里,我就是直接从STM32教起的,省了不少弯路。
\\n很多人急功近利,基础不牢就想学RTOS、学Linux。结果啥都学不明白,代码全是从网上抄的,出了问题一脸茫然。
\\n我建议至少把这些搞明白再考虑RTOS:
\\n我自己当年脑子一热,看了两天FreeRTOS就想做多任务项目,结果搞出来的东西莫名其妙死机,排查了一周才发现是中断优先级设置错了。那会没人指导,走了不少弯路。
\\n说实话,嵌入式这行真不是靠看书能学会的。我书架上十几本嵌入式\\"宝典\\",大部分只翻了前三章。
\\n真正有用的学习方法是:
\\n我记得刚学ADC时,书上写得云里雾里,看了三天还是不明白。后来我直接找了个例程,改了改参数,再去数据手册上找对应解释,一下子就通了。先上手实操,遇到问题再补充理论知识。
\\n分享个真实故事:我第一次做UART通信的时候,死活收不到数据。检查了一晚上代码,结果第二天同事瞄了一眼说:\\"你TX/RX接反了...\\",当时就想找个地缝钻进去。这种教训,书上学不到啊!
\\n初学者都喜欢用printf调试,我也不例外。但嵌入式里printf很费资源,而且有时候会掩盖真正的问题。
\\n更科学的方法是:
\\n我在《STM32实战快速入门》里专门有一节讲调试技巧,因为这真的太重要了,比写代码本身还重要。
\\n网上的代码质量参差不齐,有些看起来能用,实际上漏洞百出。我刚入行时就犯过这种错,复制了一段\\"高手\\"写的I2C驱动,结果在高温环境下莫名其妙出错,折腾了好久才发现是没做超时处理。
\\n正确的学习方式是:理解官方例程,然后自己写。慢一点,但起码知道每行代码是干啥的。
\\n这可能是最普遍的问题了。看了几本书,以为自己啥都懂,但一写代码就傻眼。
\\n解决办法只有一个:多做项目,从小项目做起。先点个灯,再控制个电机,然后做个温控系统...循序渐进。
\\n我当年脑袋一热,想一口气做个\\"智能家居系统\\",结果连最基础的传感器都读不好数据,白白浪费了两个月。后来我在课程设计中特意避开这个坑,设计了由浅入深的项目序列,让学员有成就感的同时,也能稳步提升。
\\n说到学习资源,网上一搜一大把,但大部分质量堪忧。我给你推荐几个我亲测有效的:
\\nST官网的参考手册和HAL库说明文档虽然枯燥,但是最准确的。别嫌它们难懂,迟早要啃下来。我每天睡前都会翻一小节参考手册,久而久之也就熟悉了。
\\n别被培训机构的\\"必读书单\\"吓到,精读这几本就够了。其他的等用到了再查。
\\nB站上的免费教程质量参差不齐,有些讲得挺好,有些纯属误导。选课程有个简单法则:看评论区老司机的反馈。
\\n我自己录制《STM32实战快速入门》(点击直达)课程时,特别注重把理论和实践结合起来,毕竟我就是被那些纯理论的课程坑过的人。每个知识点都有对应的实战项目,从点灯开始,到最后的MQTT物联网应用,循序渐进。
\\n老实说,嵌入式没有传说中那么难,但也绝对不是几个月就能精通的。
\\n最难的部分是:软硬结合、调试困难、问题隐蔽。比如我曾经遇到一个bug,程序偶尔死机,排查了两周才发现是电源纹波导致的。这种问题,单纯学软件是解决不了的。
\\n最爽的部分是:做出来的东西是实实在在能看能摸的。记得我第一次做的智能小车,演示给同事看时,大家都惊叹不已,那种成就感是写网页或APP所不能比的。
\\n从零自学的话,踏踏实实一年,能达到就业水平;三年,能独当一面;五年,能做技术专家。当然,前提是你真的热爱这个行业,不是为了跟风或者高薪。
\\n我自己是从机械转行过来的,啥基础都没有,不知道加班多少个晚上才算入门。现在回想起来,那段苦逼日子反而是最有动力的时候。所以,只要你真的喜欢,肯定能学好。
\\n自学嵌入式最大的敌人不是难度,而是坚持。很多人看几天书就放弃了,觉得太难、太枯燥。
\\n但只要你能度过最初的迷茫期(大约1-2个月),后面就会越来越有意思。等你第一次看到自己写的代码控制电路时,那种成就感会让你上瘾的。
\\n另外,找个学习伙伴真的很重要。我当年就是和一个同事相互监督,每周交流进度,才没有半途而废。我的《STM32实战快速入门》(点击直达)课程的学员群也是这个目的,大家互相帮助,一起成长。
\\n最后送你一句我常挂嘴边的话:嵌入式没捷径,但有弯路;少走弯路,就是最大的捷径。
\\n另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
\\n刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
\\n推荐阅读:
\\n欢迎关注我的博客:良许嵌入式教程网,满满都是干货!
","description":"大家好,我是良许。 十年前我特么真是瞎了眼,机械专业毕业后居然接了个嵌入式的offer。第一天上班,我的领导扔给我一本STM32的数据手册,厚得能砸死人,然后丢下一句\\"下周之前把GPIO的例程写出来\\"就走了...\\n\\n当时我就傻了,GPIO是个啥玩意?C语言也就课上混了60分的水平,连指针都整不明白。那段日子真是度日如年,晚上做噩梦都是寄存器地址和时钟配置。\\n\\n好在功夫不负有心人,这些年摸爬滚打下来,我从一个连LED都点不亮的菜鸟,变成能独当一面的嵌入式工程师。去年我把多年踩坑经验整理成了《STM32实战快速入门》(点击直达)课程…","guid":"https://juejin.cn/post/7489405244038037538","author":"良许Linux","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T16:49:49.180Z","media":[{"url":"https://lxlinux.superbed.verylink.top/item/6569931cc458853aef9b704b.jpg","type":"photo"}],"categories":["后端","Linux"],"attachments":null,"extra":null,"language":null},{"title":"你见过的最差的程序员是怎样的?","url":"https://juejin.cn/post/7489488440113692724","content":"作为一名在嵌入式领域摸爬滚打近十年的老兵,我见过太多奇葩程序员了。但要说最差的,非\\"赵工\\"莫属。
\\n那是我从机械调剂到电子部门的第二年,公司接了个重要项目,需要开发一款基于STM32的工业控制系统。领导从总部借来一位\\"资深嵌入式专家\\"——赵工。
\\n初见赵工时,他西装革履,一副成功人士模样。\\"我做过BAT核心项目,对单片机开发了如指掌\\",他面试时的豪言壮语,让领导对他寄予厚望。
\\n接手项目的第一周,赵工就展示了他的\\"实力\\":
\\nvoid Do_Something(void)\\n{\\n u8 a;\\n u8 b;\\n u8 c;\\n u8 i;\\n u8 j;\\n u8 k;\\n a=1;\\n b=2;\\n if(a==1)\\n {\\n for(i=0;i<10;i++)\\n {\\n if(b==2)\\n {\\n k = i + 1;\\n //do something here\\n }\\n }\\n }\\n}\\n
\\n没错,这就是他的编码风格——变量命名全是单字母,没有注释,缩进混乱,函数名毫无意义。当我问他这些变量代表什么意思时,他瞪了我一眼:\\"代码就是写给机器看的,能运行就行,哪那么多讲究?\\"
\\n赵工的调试方法更是\\"高效\\"。有一次系统死机,排查原因时,他直接往代码里塞了几十个printf:
\\nprintf(\\"here1\\\\n\\");\\nif(temp > 50) {\\n printf(\\"here2\\\\n\\");\\n control_valve();\\n printf(\\"here3\\\\n\\");\\n}\\nprintf(\\"here4\\\\n\\");\\n
\\n任何正常程序员都会使用条件断点或日志系统,但他偏要用这种原始方法。更可怕的是,调试完成后,这些垃圾代码常常被他忘记删除,留在生产代码中。
\\n记得有次他在处理EEPROM存储时,创造了这样的\\"杰作\\":
\\n// 存储用户配置\\nvoid save_config(void)\\n{\\n // 直接从0地址开始写,不管有没有其他数据\\n EEPROM_Write(0, (uint8_t*)&global_config, sizeof(global_config));\\n}\\n\\n// 加载配置\\nvoid load_config(void)\\n{\\n // 没有任何校验,直接读取\\n EEPROM_Read(0, (uint8_t*)&global_config, sizeof(global_config));\\n}\\n
\\n没有地址规划,没有数据校验,没有版本管理。当我提醒他这会导致数据混乱时,他不以为然:\\"又不是大型系统,用不着那么复杂。\\"
\\n结果可想而知,产品一上线,用户配置经常莫名其妙丢失或混乱。
\\n在一个需要处理大量传感器数据的模块中,他写出了这样的代码:
\\nvoid process_sensor_data(void)\\n{\\n // 每次分配固定大小,用完不释放\\n uint8_t *buffer = malloc(1024);\\n \\n // 处理数据...\\n \\n // 没有free操作\\n}\\n
\\n这个函数每分钟会被调用几十次,内存泄漏严重。当系统运行几小时后必然崩溃。我指出这个问题时,他竟然说:\\"单片机会自动回收内存的,不用担心。\\"
\\n我当时就懵了,这种基础常识都不懂,他是怎么通过面试的?
\\n提到版本控制,赵工也有独到见解。公司用Git管理代码,他却坚持用自己的方式:
\\n有一次他把整个主分支代码弄坏了,急得团队其他成员直冒冷汗。当问他为什么不用分支开发时,他理直气壮:\\"那太麻烦了,我一个人开发用不着那些东西。\\"
\\n赵工的团队协作能力堪称一绝。记得有次我接手他的一个模块进行扩展,打开代码后惊呆了:
\\n// 神秘函数\\nvoid xyz(void)\\n{\\n u16 m = get_value();\\n if(m > 30)\\n {\\n op();\\n }\\n else if(m <= 30 && m > 20)\\n {\\n op2();\\n }\\n else\\n {\\n if(flag)\\n {\\n op3();\\n }\\n }\\n}\\n
\\n完全看不懂这函数是干什么的!没有文档,没有注释,变量名全是缩写,函数名毫无意义。我只好硬着头皮找他问。
\\n他却说:\\"代码写出来就是给机器看的,你看不懂是你水平问题。再说了,这是我的核心竞争力,如果写得太清楚,公司还要我干嘛?\\"
\\n这种\\"核心竞争力\\"理论让我哭笑不得。在我看来,真正的核心竞争力是创造价值的能力,而不是制造混乱的能力。
\\n最后这个项目如何收场?你们猜到了。
\\n原定三个月的项目拖了半年,客户不断投诉系统不稳定。在一次重要演示中,系统当场崩溃,客户大怒。公司损失了一个重要客户,也赔了一大笔违约金。
\\n赵工却毫不愧疚,反而抱怨环境问题:\\"肯定是测试环境不对,我本地运行得好好的。\\"
\\n最终,他被公司礼貌地送回了总部,项目由我和另外两位同事重构。我们花了两个月才把这烂摊子收拾干净。
\\n回想这段经历,我总结赵工这类\\"最差程序员\\"的特质:
\\n这种程序员不仅技术差,更可怕的是态度差。他们像一颗定时炸弹,迟早会给团队和产品带来灾难。
\\n我27岁进入世界500强外企时,遇到一位让我敬佩的技术主管李工。他的代码风格截然不同:
\\n/**\\n * @brief 处理温度传感器数据并控制阀门\\n * @param temperature 当前温度值(摄氏度)\\n * @return 操作是否成功\\n * @note 当温度超过临界值时,会自动关闭阀门\\n */\\nbool processTempAndControlValve(float temperature)\\n{\\n // 安全检查\\n if (!isSensorValid(SENSOR_TEMP)) {\\n logError(\\"Temperature sensor not valid!\\");\\n return false;\\n }\\n \\n // 温度过高,关闭阀门\\n if (temperature > CRITICAL_TEMP_THRESHOLD) {\\n logWarning(\\"Critical temperature detected: %.2f°C\\", temperature);\\n return closeValve(VALVE_MAIN);\\n }\\n \\n // 正常温度范围\\n return true;\\n}\\n
\\n他的代码:
\\n更重要的是,他从不吝啬分享知识。每周五下午,他都会组织技术分享会,讲解嵌入式Linux的各种难点。正是在他的影响下,我开始自学Linux,并在28岁时开始写技术公众号分享所学。
\\n这些经历让我深刻认识到,成为好程序员不仅关乎技术,更关乎态度和习惯。这也是我30岁创业后,在培训和咨询中一直强调的核心理念。
\\n在我的小公司里,我们有严格的代码审查制度,无论资历高低,代码必须符合规范才能合并。有位刚入职的年轻人抱怨:\\"写那么多注释太浪费时间了!\\"我给他看了赵工项目的代码和我们后来重构的对比,他立刻理解了。
\\n好的编程习惯就像复利,短期看不到效果,长期却能造就天壤之别。这也是我从嵌入式开发一路走来的深刻体会。
\\n如果你在团队中遇到了\\"赵工\\"式的程序员,请保持警惕,远离这种技术债务制造机。如果你担心自己可能有类似倾向,请反思并改变,这对你的职业生涯至关重要。
\\n真正的编程高手,不仅代码写得好,更能让团队变得更好。就像我在二线城市靠技术和分享积累第一个百万时所感悟的:技术能力决定下限,协作能力决定上限。
\\n作为一个从机械转行到嵌入式的非科班程序员,我深知基础扎实和态度端正的重要性。希望每位程序员都能远离\\"最差\\",走向更好的自己。
\\n另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
\\n刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
\\n推荐阅读:
\\n欢迎关注我的博客:良许嵌入式教程网,满满都是干货!
","description":"我见过的最差程序员,差到让整个团队崩溃 作为一名在嵌入式领域摸爬滚打近十年的老兵,我见过太多奇葩程序员了。但要说最差的,非\\"赵工\\"莫属。\\n\\n初见赵工\\n\\n那是我从机械调剂到电子部门的第二年,公司接了个重要项目,需要开发一款基于STM32的工业控制系统。领导从总部借来一位\\"资深嵌入式专家\\"——赵工。\\n\\n初见赵工时,他西装革履,一副成功人士模样。\\"我做过BAT核心项目,对单片机开发了如指掌\\",他面试时的豪言壮语,让领导对他寄予厚望。\\n\\n\\"独特\\"的编码风格\\n\\n接手项目的第一周,赵工就展示了他的\\"实力\\":\\n\\nvoid Do_Something(void)\\n{\\n u8 a…","guid":"https://juejin.cn/post/7489488440113692724","author":"良许Linux","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T16:47:09.785Z","media":[{"url":"https://lxlinux.superbed.verylink.top/item/6569931cc458853aef9b704b.jpg","type":"photo"}],"categories":["后端","Linux"],"attachments":null,"extra":null,"language":null},{"title":"都说 SpringBoot 启动慢 ,你知道慢在哪里吗?","url":"https://juejin.cn/post/7489382857032548393","content":"前段时间体验了几个开源的开发框架 ,发现他们的亮点主要集中在启动快 ,内存低上面。
\\n随之回想 SpringBoot ,发现自己并不能准确的说出 SpringBoot 启动慢的详细原因,所以才有了这篇文章。
\\n来 ,让我们详细的理解一下 ,SpringBoot 启动这么慢 ,是做了什么?
\\n\\n\\n先来选择一个最简单的 MVC 项目 ,来看一下时间轴 :
\\n
public void run(String... args) {\\n // 3. 触发启动开始事件\\n listeners.starting();\\n\\n // 4. 准备应用运行环境\\n // 包括:加载配置、设置profile、系统环境变量等\\n ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);\\n\\n // 7. 创建应用上下文\\n // 根据应用类型(Web/普通)创建不同的上下文\\n context = createApplicationContext();\\n\\n // 8. 准备应用上下文\\n // 包括:设置环境、注册初始化器、加载source、处理前置处理器等\\n prepareContext(context, environment, listeners, applicationArguments, printedBanner);\\n\\n // 9. 刷新应用上下文\\n refreshContext(context);\\n\\n // 13. 触发上下文启动完成事件\\n listeners.started(context);\\n\\n // 14. 执行所有注册的运行器(ApplicationRunner和CommandLineRunner)\\n callRunners(context, applicationArguments);\\n\\n // 15. 触发应用运行事件\\n listeners.running(context);\\n}\\n
\\n\\n\\nprepareEnvironment 部分 :
\\n
// 根据应用类型创建基础环境对象 \\nConfigurableEnvironment environment = getOrCreateEnvironment(); \\n\\n// 环境配置 : 设置profile和处理启动参数 \\nconfigureEnvironment(environment, applicationArguments.getSourceArgs()); \\n\\n// 高级环境注入 : 增强属性源,提供更灵活的配置解析 \\n// - 特殊的环境绑定 : 驼峰 ,下划线等\\n// - 自动进行属性类型转换\\n// - 复杂的嵌套属性解析 , 占位符 $ 的变量替换\\n// - 各种优先级相关\\nConfigurationPropertySources.attach(environment); \\n\\n// 触发环境准备事件,允许监听器处理 \\nlisteners.environmentPrepared(environment); \\n\\n// 将环境配置绑定到SpringApplication \\nbindToSpringApplication(environment); \\n\\n// 返回配置完成的环境对象 \\nreturn environment; \\n
\\n\\n\\nrefreshContext 部分 :
\\n
可以看到 ,refreshContext 部分才是大头 ,这里就是 Bean 加载创建最核心的流程 ,我们一般知道的 doGetBean 和 populateBean 就是在这个环节中进行的 :
\\n\\n// 前置刷新上下文环境 \\nthis.prepareRefresh();\\n\\n// 获取freshBeanFactory,初始化BeanFactory \\nConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); \\n \\n// 配置BeanFactory的标准属性 \\nthis.prepareBeanFactory(beanFactory); \\n\\n\\n// 提供子类扩展BeanFactory的机会 \\nthis.postProcessBeanFactory(beanFactory); \\n \\n// 执行BeanFactory后置处理器 \\nthis.invokeBeanFactoryPostProcessors(beanFactory); \\n \\n// 注册Bean后置处理器 \\nthis.registerBeanPostProcessors(beanFactory); \\n \\n// 初始化消息源 \\nthis.initMessageSource(); \\n \\n// 初始化应用事件广播器 \\nthis.initApplicationEventMulticaster(); \\n \\n// 提供子容器刷新的扩展点 \\nthis.onRefresh(); \\n \\n// 注册监听器 \\nthis.registerListeners(); \\n \\n// 初始化所有非懒加载单例Bean \\nthis.finishBeanFactoryInitialization(beanFactory); \\n \\n// 完成刷新,发布容器刷新事件 \\nthis.finishRefresh(); \\n
\\n生产级项目的分析和上面的是完全不一样的 ,生产级的复杂度远超单一的小项目 ,这也是导致大家认为 : Spring 启动太慢了
。
我贴了一个我这边启动中规中矩的一个项目的处理栈 ,整个大概花了 35s , 来分析一下 :
\\n\\n\\n场景一 : 多容器环境 (反复创建容器)
\\n
\\n\\n场景二 :配置侧的环境 Listener (资源文件的读取)
\\n
不能理解为
我们通常用的某个Listener 的操作 ,监听个什么东西什么的ApplicationEnvironmentPreparedEvent
有很多很复杂的 Listener 在监听\\nConfigFileApplicationListener
: 用于加载外部文件SpringBootServletInitializer
: 用于 Servlet容器集成BootstrapApplicationListener
: 初始化 SpringCloud 到的配置\\n\\n场景三 : 大量的 doRefrsh() Bean 的加载 (重复读资源)
\\n
\\n\\n场景四 :各种 Bean 的加载 (大量 Bean)
\\n
这个是一大根源 ,(由于统计时间的逻辑不够严谨,所以Bean处理的时间被分摊到 prepareRefresh 中了)
\\n\\n\\n场景五 : 第三方组件 Client 的创建 (连接第三方)
\\n
大部分时间都用来写这了 ,感觉还行, 在 IDEA 里面进行 DEBUG 就行 :
\\nStopWatchExpand.start(\\"Bean\\",\\"开始-创建RedisClient\\")
StopWatchExpand.start(\\"主线\\",\\"全部完成\\"); StopWatchExpand.stop();
package com.gang.start;\\n\\nimport java.text.SimpleDateFormat;\\nimport java.util.ArrayList;\\nimport java.util.List;\\n\\n/**\\n * 检测程序片段运行时间拓展\\n */\\npublic class StopWatchExpand {\\n\\n /**\\n * 存储时间节点的静态集合\\n */\\n private static List<TaskEntry> taskEntries = new ArrayList<>();\\n\\n /**\\n * 开启计时\\n *\\n * @param processLine 流程线\\n * @param node 节点行为\\n * @return 提示字符串\\n */\\n public static String start(String processLine, String node) {\\n try {\\n // 获取调用的类和方法信息\\n StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\\n // 注意: stackTrace[2]是调用start方法的方法\\n String callerClass = stackTrace[2].getClassName().substring(\\n stackTrace[2].getClassName().lastIndexOf(\\".\\") + 1\\n );\\n String callerMethod = stackTrace[2].getMethodName();\\n\\n // 记录当前时间戳\\n long currentTimeMillis = System.currentTimeMillis();\\n\\n // 将流程线、节点行为、调用类、调用方法和时间戳保存到集合\\n taskEntries.add(new TaskEntry(processLine, node, callerClass, callerMethod, currentTimeMillis));\\n\\n return String.format(\\"[ 流程: %s | 节点: %s | 调用类: %s | 调用方法: %s ] 监测到达时间: %s\\",\\n processLine, node, callerClass, callerMethod, formatTimestamp(currentTimeMillis));\\n } catch (Exception e) {\\n e.printStackTrace();\\n return \\"执行异常: \\" + node;\\n }\\n }\\n\\n /**\\n * 结束计时并记录统计\\n */\\n public static void stop() {\\n logStatistics();\\n // 清空记录,防止跨调用干扰\\n taskEntries.clear();\\n }\\n\\n /**\\n * 打印所有任务的时间节点以及间隔时间和百分比\\n */\\n private static void logStatistics() {\\n if (taskEntries.isEmpty()) {\\n return;\\n }\\n\\n // 1. 预处理:计算最大节点行为长度和最大流程行长度\\n int maxNodeLength = taskEntries.stream()\\n .map(entry -> entry.getNode().length())\\n .max(Integer::compare)\\n .orElse(30); // 默认最小宽度为30\\n\\n // 保证processLine长度为10\\n int processLineWidth = 10;\\n int classNameWidth = 30;\\n int methodNameWidth = 30;\\n\\n // 2. 创建格式化模板,使用动态宽度\\n String headerFormat = \\"%-\\" + (maxNodeLength + 10) + \\"s |%-\\" + processLineWidth + \\"s | %-\\" + classNameWidth + \\"s | %-\\" + methodNameWidth + \\"s | %-9s | %-12s | %-24s | %-30s\\\\n\\";\\n String rowFormat = \\"%-\\" + processLineWidth + \\"s |%-\\" + (classNameWidth+4) + \\"s | %-\\" + (methodNameWidth+4) + \\"s |%-11d ms | %-13.2f%% | %-31d ms | %-30d ms\\\\n\\";\\n\\n // 3. 计算总时长:从第一个节点到最后一个节点的时间差\\n long startTime = taskEntries.get(0).getTimeMillis();\\n long endTime = taskEntries.get(taskEntries.size() - 1).getTimeMillis();\\n long totalDuration = endTime - startTime;\\n\\n // 4. 构建日志字符串\\n StringBuilder sb = new StringBuilder();\\n sb.append(\\"--------------------------------------------------------------------------------------------------\\\\n\\");\\n sb.append(\\"时间节点统计:\\\\n\\");\\n sb.append(\\"--------------------------------------------------------------------------------------------------\\\\n\\");\\n\\n // 5. 使用动态宽度的格式化模板打印表头\\n sb.append(String.format(headerFormat,\\n \\"节点行为\\", \\"流程\\", \\"调用类\\", \\"调用方法\\", \\"节点耗时\\", \\"占比\\", \\"相对总初始节点时间戳\\",\\n \\"相对当前流程初始节点时间戳\\"));\\n sb.append(\\"--------------------------------------------------------------------------------------------------\\\\n\\");\\n\\n // 6. 遍历并格式化每一行\\n for (int i = 0; i < taskEntries.size(); i++) {\\n TaskEntry entry = taskEntries.get(i);\\n\\n // 填充processLine到固定长度\\n String processLine = String.format(\\"%-\\" + processLineWidth + \\"s\\",\\n entry.getProcessLine().length() > processLineWidth\\n ? entry.getProcessLine().substring(0, processLineWidth)\\n : entry.getProcessLine());\\n\\n // 截断或填充调用类和方法名\\n String callerClass = padString(entry.getCallerClass(), classNameWidth);\\n String callerMethod = padString(entry.getCallerMethod(), methodNameWidth);\\n\\n String node = padChineseNode(entry.getNode(), maxNodeLength + 10);\\n long taskTimeMillis = entry.getTimeMillis();\\n\\n // 7. 计算时间相关指标\\n long interval = (i == 0) ? 0 : (taskTimeMillis - taskEntries.get(i - 1).getTimeMillis());\\n double intervalPercentage = totalDuration > 0 ? ((double) interval / totalDuration) * 100 : 0.0;\\n\\n long relativeToInitial = taskTimeMillis - startTime;\\n long relativeToCurrentProcess = (i == 0) ? 0 : (taskTimeMillis - startTime);\\n\\n // 8. 使用动态宽度的格式化模板打印每一行\\n sb.append(\\"| \\");\\n sb.append(node);\\n sb.append(\\" |\\");\\n sb.append(String.format(rowFormat,\\n processLine,\\n callerClass,\\n callerMethod,\\n interval,\\n intervalPercentage,\\n relativeToInitial,\\n relativeToCurrentProcess));\\n }\\n\\n sb.append(\\"---------------------------------------------------------------\\\\n\\");\\n\\n // 9. 输出到日志\\n System.out.println(sb);\\n }\\n\\n /**\\n * 格式化时间戳为字符串\\n *\\n * @param timestamp 时间戳\\n * @return 格式化后的字符串\\n */\\n private static String formatTimestamp(long timestamp) {\\n SimpleDateFormat sdf = new SimpleDateFormat(\\"yyyy-MM-dd HH:mm:ss.SSS\\");\\n return sdf.format(timestamp);\\n }\\n\\n /**\\n * 填充或截断字符串到指定长度\\n */\\n private static String padString(String input, int length) {\\n if (input == null) input = \\"\\";\\n if (input.length() > length) {\\n return input.substring(0, length);\\n }\\n return String.format(\\"%-\\" + length + \\"s\\", input);\\n }\\n\\n /**\\n * 处理中文节点的填充\\n */\\n private static String padChineseNode(String node, int maxNodeLength) {\\n int width = 0;\\n StringBuilder result = new StringBuilder();\\n\\n for (char c : node.toCharArray()) {\\n int charWidth = (c >= 0x4E00 && c <= 0x9FA5) ||\\n (c >= \'a\' && c <= \'z\') ||\\n (c >= \'A\' && c <= \'Z\') ?\\n ((c >= 0x4E00 && c <= 0x9FA5) ? 2 : 1) : 0;\\n\\n if (width + charWidth > maxNodeLength) {\\n break;\\n }\\n\\n if (charWidth > 0) {\\n result.append(c);\\n width += charWidth;\\n }\\n }\\n\\n while (width < maxNodeLength) {\\n result.append(\\" \\");\\n width++;\\n }\\n\\n return result.toString();\\n }\\n\\n /**\\n * 任务条目类,增加调用类和调用方法字段\\n */\\n private static class TaskEntry {\\n private String processLine; // 流程线\\n private String node; // 节点行为\\n private String callerClass; // 调用类\\n private String callerMethod; // 调用方法\\n private long timeMillis; // 时间戳\\n\\n public TaskEntry(String processLine, String node, String callerClass, String callerMethod, long timeMillis) {\\n this.processLine = processLine;\\n this.node = node;\\n this.callerClass = callerClass;\\n this.callerMethod = callerMethod;\\n this.timeMillis = timeMillis;\\n }\\n\\n // 增加getter方法\\n public String getProcessLine() { return processLine; }\\n public String getNode() { return node; }\\n public String getCallerClass() { return callerClass; }\\n public String getCallerMethod() { return callerMethod; }\\n public long getTimeMillis() { return timeMillis; }\\n }\\n\\n // 可选的main方法,用于测试\\n public static void main(String[] args) {\\n // 测试用例\\n start(\\"测试流程\\", \\"开始处理\\");\\n try {\\n Thread.sleep(100); // 模拟处理时间\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n start(\\"测试流程\\", \\"处理中\\");\\n try {\\n Thread.sleep(50); // 模拟处理时间\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n stop();\\n }\\n}\\n
\\n整个过程中 ,Client 端的连接是最耗时的 ,其次是配置读取 。 也就是外部资源的加载更耗时 。
\\n所以后面看看新版本的时候 ,来看一下他们是怎么解决的 ,以及其他优秀的开源组件又是怎么解决的。
\\nSpringBoot 本身是知道自己过于臃肿的 ,所以在后面的迭代中都有意识的为自己的代码进行瘦身。
\\n先看懂了 SpringBoot2 的慢 ,后面会有一篇来感受一下SpringBoot3 干了什么 ,以及是否真的提升了加载的速度。
\\n清明光顾着玩去了 ,实在没时间研究什么,最后半天感兴趣研究了下。
\\n以前模模糊糊也知道这些 ,但是没具体的深入了解过 ,了解了原理框架搭建也就得心应手了❗❗
\\n在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。
\\n代码通常会提交到Git或SVN等版本控制系统中。一旦私钥被提交,团队中的每个人都能看到这些敏感信息。即使后来删除了私钥,在历史记录中依然可以找到。有开发者就分享过真实案例:团队成员意外将AWS密钥提交到GitHub,结果第二天账单暴增数千元——有人利用泄露的密钥进行了挖矿活动。
\\n在规范的开发流程中,密钥管理和代码开发应该严格分离。通常由运维团队负责密钥管理,而开发人员则不需要(也不应该)直接接触生产环境的密钥。这是基本的安全实践。
\\n当应用从开发环境迁移到测试环境,再到生产环境时,如果密钥硬编码在代码中,每次环境切换都需要修改代码并重新编译。这不仅效率低下,还容易出错。
\\n业内已有多种成熟的解决方案:
\\n有开发团队就曾经花费两周时间清理代码中的硬编码密钥。其中甚至发现了一个已离职员工留下的\\"临时\\"数据库密码,注释中写着\\"临时用,下周改掉\\"——然而那个\\"下周\\"已经过去五年了。
\\n作为专业开发者,应当始终保持良好的安全习惯。将私钥硬编码进代码,就像把家门钥匙贴在门上一样不可理喻。
\\n这个教训值得所有软件工程师引以为戒。
","description":"为什么把私钥写在代码里是一个致命错误 在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。\\n\\n为什么把私钥写在代码里如此危险?\\n1. 代码会被分享和同步\\n\\n代码通常会提交到Git或SVN等版本控制系统中。一旦私钥被提交,团队中的每个人都能看到这些敏感信息。即使后来删除了私钥,在历史记录中依然可以找到。有开发者就分享过真实案例:团队成员意外将AWS密钥提交到GitHub,结果第二天账单暴增数千元…","guid":"https://juejin.cn/post/7489043337290203163","author":"Asthenian","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T05:33:06.394Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot中6种API版本控制策略","url":"https://juejin.cn/post/7489315969318649867","content":"API版本控制是确保系统平稳演进的关键策略。当API发生变化时,合理的版本控制机制能让旧版客户端继续正常工作,同时允许新版客户端使用新功能。
\\n这是最直观、应用最广泛的版本控制方式,通过在URL路径中直接包含版本号。
\\n@RestController\\n@RequestMapping(\\"/api/v1/users\\")\\npublic class UserControllerV1 {\\n \\n @GetMapping(\\"/{id}\\")\\n public UserV1DTO getUser(@PathVariable Long id) {\\n // 返回v1版本的用户信息\\n return userService.getUserV1(id);\\n }\\n}\\n\\n@RestController\\n@RequestMapping(\\"/api/v2/users\\")\\npublic class UserControllerV2 {\\n \\n @GetMapping(\\"/{id}\\")\\n public UserV2DTO getUser(@PathVariable Long id) {\\n // 返回v2版本的用户信息,可能包含更多字段\\n return userService.getUserV2(id);\\n }\\n}\\n
\\n优点
\\n缺点
\\n通过在请求参数中指定版本号,保持URL路径不变。
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @GetMapping(\\"/{id}\\")\\n public Object getUser(@PathVariable Long id, @RequestParam(defaultValue = \\"1\\") int version) {\\n switch (version) {\\n case 1:\\n return userService.getUserV1(id);\\n case 2:\\n return userService.getUserV2(id);\\n default:\\n throw new IllegalArgumentException(\\"Unsupported API version: \\" + version);\\n }\\n }\\n}\\n
\\n或者使用SpringMVC的条件映射:
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @GetMapping(value = \\"/{id}\\", params = \\"version=1\\")\\n public UserV1DTO getUserV1(@PathVariable Long id) {\\n return userService.getUserV1(id);\\n }\\n \\n @GetMapping(value = \\"/{id}\\", params = \\"version=2\\")\\n public UserV2DTO getUserV2(@PathVariable Long id) {\\n return userService.getUserV2(id);\\n }\\n}\\n
\\n优点
\\n缺点
\\n通过自定义HTTP头来指定API版本,这是一种更符合RESTful理念的方式。
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @GetMapping(value = \\"/{id}\\", headers = \\"X-API-Version=1\\")\\n public UserV1DTO getUserV1(@PathVariable Long id) {\\n return userService.getUserV1(id);\\n }\\n \\n @GetMapping(value = \\"/{id}\\", headers = \\"X-API-Version=2\\")\\n public UserV2DTO getUserV2(@PathVariable Long id) {\\n return userService.getUserV2(id);\\n }\\n}\\n
\\n优点
\\n缺点
\\n使用HTTP协议的内容协商机制,通过Accept头指定媒体类型及其版本。
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @GetMapping(value = \\"/{id}\\", produces = \\"application/vnd.company.app-v1+json\\")\\n public UserV1DTO getUserV1(@PathVariable Long id) {\\n return userService.getUserV1(id);\\n }\\n \\n @GetMapping(value = \\"/{id}\\", produces = \\"application/vnd.company.app-v2+json\\")\\n public UserV2DTO getUserV2(@PathVariable Long id) {\\n return userService.getUserV2(id);\\n }\\n}\\n
\\n客户端请求时需要设置Accept头:
\\nAccept: application/vnd.company.app-v2+json\\n
\\n优点
\\n缺点
\\n通过自定义注解和拦截器/过滤器实现更灵活的版本控制。
\\n首先定义版本注解:
\\n@Target({ElementType.TYPE, ElementType.METHOD})\\n@Retention(RetentionPolicy.RUNTIME)\\npublic @interface ApiVersion {\\n int value() default 1;\\n}\\n
\\n创建版本匹配的请求映射处理器:
\\n@Component\\npublic class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {\\n\\n @Override\\n protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {\\n ApiVersion apiVersion = handlerType.getAnnotation(ApiVersion.class);\\n return createCondition(apiVersion);\\n }\\n\\n @Override\\n protected RequestCondition<?> getCustomMethodCondition(Method method) {\\n ApiVersion apiVersion = method.getAnnotation(ApiVersion.class);\\n return createCondition(apiVersion);\\n }\\n\\n private ApiVersionCondition createCondition(ApiVersion apiVersion) {\\n return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());\\n }\\n}\\n\\npublic class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {\\n\\n private final int apiVersion;\\n\\n public ApiVersionCondition(int apiVersion) {\\n this.apiVersion = apiVersion;\\n }\\n\\n @Override\\n public ApiVersionCondition combine(ApiVersionCondition other) {\\n // 采用最高版本\\n return new ApiVersionCondition(Math.max(this.apiVersion, other.apiVersion));\\n }\\n\\n @Override\\n public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {\\n String version = request.getHeader(\\"X-API-Version\\");\\n if (version == null) {\\n version = request.getParameter(\\"version\\");\\n }\\n \\n int requestedVersion = version == null ? 1 : Integer.parseInt(version);\\n return requestedVersion >= apiVersion ? this : null;\\n }\\n\\n @Override\\n public int compareTo(ApiVersionCondition other, HttpServletRequest request) {\\n // 优先匹配高版本\\n return other.apiVersion - this.apiVersion;\\n }\\n}\\n
\\n配置WebMvc使用自定义的映射处理器:
\\n@Configuration\\npublic class WebConfig implements WebMvcConfigurer {\\n\\n @Bean\\n public RequestMappingHandlerMapping requestMappingHandlerMapping() {\\n return new ApiVersionRequestMappingHandlerMapping();\\n }\\n}\\n
\\n使用自定义注解:
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n @ApiVersion(1)\\n @GetMapping(\\"/{id}\\")\\n public UserV1DTO getUserV1(@PathVariable Long id) {\\n return userService.getUserV1(id);\\n }\\n \\n @ApiVersion(2)\\n @GetMapping(\\"/{id}\\")\\n public UserV2DTO getUserV2(@PathVariable Long id) {\\n return userService.getUserV2(id);\\n }\\n}\\n
\\n优点
\\n缺点
\\n通过接口继承和策略模式实现版本控制,核心思想是提供相同接口的不同版本实现类。
\\n首先定义API接口:
\\npublic interface UserApi {\\n Object getUser(Long id);\\n}\\n\\n@Service\\n@Primary\\npublic class UserApiV2Impl implements UserApi {\\n // 最新版本实现\\n @Override\\n public UserV2DTO getUser(Long id) {\\n // 返回V2版本数据\\n return new UserV2DTO();\\n }\\n}\\n\\n@Service\\n@Qualifier(\\"v1\\")\\npublic class UserApiV1Impl implements UserApi {\\n // 旧版本实现\\n @Override\\n public UserV1DTO getUser(Long id) {\\n // 返回V1版本数据\\n return new UserV1DTO();\\n }\\n}\\n
\\n控制器层根据版本动态选择实现:
\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n private final Map<Integer, UserApi> apiVersions;\\n \\n // 通过构造注入收集所有实现\\n public UserController(List<UserApi> apis) {\\n // 简化示例,实际应通过某种方式标记每个实现的版本\\n this.apiVersions = Map.of(\\n 1, apis.stream().filter(api -> api instanceof UserApiV1Impl).findFirst().orElseThrow(),\\n 2, apis.stream().filter(api -> api instanceof UserApiV2Impl).findFirst().orElseThrow()\\n );\\n }\\n \\n @GetMapping(\\"/{id}\\")\\n public Object getUser(@PathVariable Long id, @RequestParam(defaultValue = \\"2\\") int version) {\\n UserApi api = apiVersions.getOrDefault(version, apiVersions.get(2)); // 默认使用最新版本\\n return api.getUser(id);\\n }\\n}\\n
\\n可以自己实现一个版本委托器来简化版本选择:
\\n// 自定义API版本委托器\\npublic class ApiVersionDelegator<T> {\\n \\n private final Class<T> apiInterface;\\n private final Map<String, T> versionedImpls = new HashMap<>();\\n private final Function<HttpServletRequest, String> versionExtractor;\\n private final String defaultVersion;\\n \\n public ApiVersionDelegator(Class<T> apiInterface, \\n Function<HttpServletRequest, String> versionExtractor,\\n String defaultVersion,\\n ApplicationContext context) {\\n this.apiInterface = apiInterface;\\n this.versionExtractor = versionExtractor;\\n this.defaultVersion = defaultVersion;\\n \\n // 从Spring上下文中查找所有实现了该接口的bean\\n Map<String, T> impls = context.getBeansOfType(apiInterface);\\n for (Map.Entry<String, T> entry : impls.entrySet()) {\\n ApiVersion apiVersion = entry.getValue().getClass().getAnnotation(ApiVersion.class);\\n if (apiVersion != null) {\\n versionedImpls.put(String.valueOf(apiVersion.value()), entry.getValue());\\n }\\n }\\n }\\n \\n public T getApi(HttpServletRequest request) {\\n String version = versionExtractor.apply(request);\\n return versionedImpls.getOrDefault(version, versionedImpls.get(defaultVersion));\\n }\\n \\n // 构建器模式简化创建过程\\n public static <T> Builder<T> builder() {\\n return new Builder<>();\\n }\\n \\n public static class Builder<T> {\\n private Class<T> apiInterface;\\n private Function<HttpServletRequest, String> versionExtractor;\\n private String defaultVersion;\\n private ApplicationContext applicationContext;\\n \\n public Builder<T> apiInterface(Class<T> apiInterface) {\\n this.apiInterface = apiInterface;\\n return this;\\n }\\n \\n public Builder<T> versionExtractor(Function<HttpServletRequest, String> versionExtractor) {\\n this.versionExtractor = versionExtractor;\\n return this;\\n }\\n \\n public Builder<T> defaultVersion(String defaultVersion) {\\n this.defaultVersion = defaultVersion;\\n return this;\\n }\\n \\n public Builder<T> applicationContext(ApplicationContext applicationContext) {\\n this.applicationContext = applicationContext;\\n return this;\\n }\\n \\n public ApiVersionDelegator<T> build() {\\n return new ApiVersionDelegator<>(apiInterface, versionExtractor, defaultVersion, applicationContext);\\n }\\n }\\n}\\n
\\n配置和使用委托器:
\\n@Configuration\\npublic class ApiConfiguration {\\n \\n @Bean\\n public ApiVersionDelegator<UserApi> userApiDelegator(ApplicationContext context) {\\n return ApiVersionDelegator.<UserApi>builder()\\n .apiInterface(UserApi.class)\\n .versionExtractor(request -> {\\n String version = request.getHeader(\\"X-API-Version\\");\\n return version == null ? \\"2\\" : version;\\n })\\n .defaultVersion(\\"2\\")\\n .applicationContext(context)\\n .build();\\n }\\n}\\n\\n@RestController\\n@RequestMapping(\\"/api/users\\")\\npublic class UserController {\\n \\n private final ApiVersionDelegator<UserApi> apiDelegator;\\n \\n public UserController(ApiVersionDelegator<UserApi> apiDelegator) {\\n this.apiDelegator = apiDelegator;\\n }\\n \\n @GetMapping(\\"/{id}\\")\\n public Object getUser(@PathVariable Long id, HttpServletRequest request) {\\n UserApi api = apiDelegator.getApi(request);\\n return api.getUser(id);\\n }\\n}\\n
\\n优点
\\n缺点
\\n以上6种API版本控制方式各有优劣,选择时应考虑以下因素
\\n最后,版本控制只是手段,不是目的。关键是要构建可演进的API架构,让系统能够持续满足业务需求的变化。选择合适的版本控制策略,能够在保证系统稳定性的同时,实现API的平滑演进。
","description":"API版本控制是确保系统平稳演进的关键策略。当API发生变化时,合理的版本控制机制能让旧版客户端继续正常工作,同时允许新版客户端使用新功能。 一、URL路径版本控制\\n\\n这是最直观、应用最广泛的版本控制方式,通过在URL路径中直接包含版本号。\\n\\n实现方式\\n@RestController\\n@RequestMapping(\\"/api/v1/users\\")\\npublic class UserControllerV1 {\\n \\n @GetMapping(\\"/{id}\\")\\n public UserV1DTO getUser(@PathVariable L…","guid":"https://juejin.cn/post/7489315969318649867","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-05T23:52:46.671Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"Wails:用 Go 构建桌面应用的新姿势!","url":"https://juejin.cn/post/7489261834182017024","content":"在桌面应用开发领域,Electron 无疑是一匹黑马,它让前端开发者也能做出高质量的跨平台桌面应用。但对 Go 开发者来说,传统 GUI 库不是太老旧就是开发体验不佳,这时候,Wails 出现了。
\\nWails 是一个开源项目,允许开发者使用 Go 编写后端逻辑,前端则用 HTML、CSS 和 JavaScript 构建 UI,实现现代桌面应用开发的新组合。它可以让你用 Go 写后端逻辑,再用 Vue、React、Svelte 等前端框架渲染界面,最后打包成一个原生应用,在 Windows、macOS、Linux 上流畅运行。
\\n简而言之:
\\n\\n\\n用 Wails,Go 不只是写命令行工具,也可以优雅“上桌面”。
\\n
相比传统的桌面开发框架,Wails 有几个显著优势:
\\n如果你是一位 Go 开发者,想要开发一个带 GUI 的工具软件,Wails 是你不可错过的选择。
\\n准备工作:
\\ngo install github.com/wailsapp/wails/v2/cmd/wails@latest\\n
\\n初始化一个新项目:
\\nwails init -n myapp -t vue\\n
\\n-t 后面的参数可以根据自己的实际情况来选,常见有 vanilla、vue、svelte、react 等。
\\n进入项目目录并运行:
\\ncd myapp\\nwails dev\\n
\\nBoom!你会看到一个现代感十足的 GUI 应用窗口,背后是你熟悉的 Go 程序在驱动。
\\nWails 提供了简单直观的前后端通信机制。你可以在 Go 代码中导出函数,在前端直接调用它。
\\n比如,我们在 backend.go
中写一个函数:
func (a *App) Greet(name string) string {\\n return \\"你好, \\" + name + \\"!\\"\\n}\\n
\\n然后在前端中调用它(以 Vue 为例):
\\nimport { Greet } from \'../wailsjs/go/main/App\'\\n\\nGreet(\\"小明\\").then(result => {\\n console.log(result) // 输出:你好,小明!\\n})\\n
\\n这种方式既简单又强大,适合处理业务逻辑、文件操作、调用系统 API 等任务。
\\n开发完成后,只需一条命令即可打包项目:
\\nwails build\\n
\\n默认会根据系统打包出对应平台的可执行程序,并生成安装文件。无论是发送给客户还是部署内部使用,都非常方便。
\\nWails 适合各种需要界面但又希望保持高性能和小体积的工具类软件,例如:
\\n尤其是那些对响应速度和系统资源要求较高的桌面应用,Wails 相比 Electron 有明显优势。
\\nWails 的出现,极大地拓宽了 Go 开发者的能力边界。它用 Web 技术赋能桌面应用,用 Go 提供强大的后台处理能力,结合成一种既现代又高效的桌面开发方案。
\\n如果你是 Go 爱好者,想要你的工具“长出界面”,那就赶紧试试 Wails 吧。下一款火爆的桌面神器,或许就出自你的手中!
\\n想了解更多 Wails 的使用细节和高级功能?欢迎留言交流,我们可以一起用 Go 玩出新花样!🌈
","description":"1. Wails 是什么? 在桌面应用开发领域,Electron 无疑是一匹黑马,它让前端开发者也能做出高质量的跨平台桌面应用。但对 Go 开发者来说,传统 GUI 库不是太老旧就是开发体验不佳,这时候,Wails 出现了。\\n\\nWails 是一个开源项目,允许开发者使用 Go 编写后端逻辑,前端则用 HTML、CSS 和 JavaScript 构建 UI,实现现代桌面应用开发的新组合。它可以让你用 Go 写后端逻辑,再用 Vue、React、Svelte 等前端框架渲染界面,最后打包成一个原生应用,在 Windows、macOS、Linux 上流畅运行。…","guid":"https://juejin.cn/post/7489261834182017024","author":"GetcharZp","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-05T12:37:56.811Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/de821d1ad1e54206931a42fa947dd866~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgR2V0Y2hhclpw:q75.awebp?rk3s=f64ab15b&x-expires=1745066179&x-signature=8Lh33l6UKVUsoVbSmdYW3mdT2NM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Go"],"attachments":null,"extra":null,"language":null},{"title":"为什么不让程序员直接对接客户,而是通过产品经理?","url":"https://juejin.cn/post/7489010197853388841","content":"先说个真实故事。
\\n我刚从机械专业转行做嵌入式开发那会,公司接了个工业控制项目。当时团队小,没有专门的产品经理,老板直接让我和另外两个开发跟客户对接需求。
\\n那天会议室里,客户代表滔滔不绝:\\"我们需要一个简单的控制系统,界面做得好看点,操作要简单,最好点一下就能完成所有功能。对了,还要能实时监控所有设备状态,要有报警功能,要能远程控制,要......\\"
\\n我和同事一边狂点头一边记笔记,心想这不难啊,回去一周肯定能搞定。
\\n然后,我们花了三个月都没搞定。
\\n为什么?因为那次会议后,我们脑补了一套完整的需求,写了3万行代码,结果交付时客户震惊了:\\"这不是我想要的!我只需要监控温度和湿度,其他都是将来可能要添加的功能啊!\\"
\\n我们也震惊了:\\"可您上次说......\\"
\\n客户摇头:\\"我没说要这么复杂啊,你们理解错了。\\"
\\n这个惨痛教训让我明白:程序员直接对接客户,就像让一个只会说C++的人和一个只懂黄老邪内功心法的人谈恋爱 — 看似都在用中文交流,实际上完全不在一个频道上。
\\n作为现在自己开小公司的人,我痛定思痛,招了专门的产品经理负责客户对接。下面就来说说,为什么绝大多数公司都不让程序员直接对接客户。
\\n程序员:「我们需要确认一下,这个数据是走MySQL还是MongoDB?考虑到并发量和后期扩展性...」
\\n客户:「???你说中文好吗?我只想知道为什么我点这个按钮没反应!」
\\n真实场景中,这种对话太常见了。当我作为技术负责人第一次参加客户会议时,我滔滔不绝地讲了10分钟的技术方案,用了大量专业术语,结果抬头一看:客户眼神空洞,完全懵了。
\\n而客户也有自己的专业术语。记得有次客户说「我们需要提高系统的闭环转化率和留存粘性」,我和开发团队面面相觑,完全不知道这些营销术语到底对应什么具体功能。
\\n语言不通的结果是:程序员以为理解了需求,客户以为表达清楚了,结果做出来的东西完全不是客户想要的。
\\n程序员的思维高度结构化、逻辑化,崇尚明确定义、精确数值、严格的是/否判断。
\\n客户的思维则常常模糊而跳跃,更关注「感觉对不对」、「看起来好不好」,而不是底层逻辑。
\\n我曾经历过这样的对话:
\\n客户:\\"这个界面要做得\'高大上\'一点。\\"\\n我:\\"高大上是指...?具体要改哪些元素?\\"\\n客户:\\"就是看起来专业一点,有科技感一点。\\"\\n我:\\"是要改变配色还是布局?或者添加一些动效?\\"\\n客户:\\"你们是专业的,自己看着办吧,做得好看点就行。\\"
\\n然后我们修改了三次,客户都不满意:\\"不是这样的,再高大上一点。\\"
\\n这种思维方式的差异导致程序员需要具体参数才能工作,而客户却常常给出模糊指令,双方都很痛苦。
\\n程序员关心的是:这个功能怎么实现?代码效率如何?架构是否合理?
\\n客户关心的是:这个功能能带来多少收益?用户会喜欢吗?比竞品强在哪?
\\n我做过一个电子设备监控系统,团队花了两周时间优化了后台算法,减少了50%的响应时间。我们兴冲冲地向客户汇报这个\\"重大进展\\",客户的反应却是:\\"哦,那能不能把界面上的按钮改大一点,颜色改成蓝色?\\"
\\n我们心里一万匹草泥马奔腾而过...
\\n程序员在意的技术优化,客户常常视而不见;客户在意的体验细节,程序员又觉得微不足道。
\\n没有产品经理翻译,程序员和客户的沟通就像一场\\"破译密码\\"的游戏,而且双方都不知道自己理解错了。
\\n记得有一次,客户说:\\"我们需要一个报表功能。\\"
\\n我理解的报表:能导出Excel的数据列表。\\n客户想要的报表:带有漂亮图表、可交互、能筛选的动态数据大屏。
\\n结果:我花了3天做完了\\"报表\\",客户看后说这完全不是他想要的。然后我又花了2周重做。
\\n这种理解偏差不仅浪费时间,还会让双方都很沮丧。程序员觉得客户总是变需求,客户觉得程序员不理解自己的意图。
\\n客户天性喜欢\\"顺便再加个小功能\\"。没有人把控需求边界,程序员又不擅长说\\"不\\",结果就是项目范围不断扩大。
\\n有一次客户在验收时突然说:\\"对了,能不能顺便加一个用户管理功能?就是能分配不同权限那种,应该很简单吧?\\"
\\n作为技术人员,我知道这\\"简单\\"的功能至少需要一周时间,涉及数据库设计、权限控制、界面开发等多方面工作。但在客户面前,我一时语塞,不好直接拒绝,结果默认接受了这个\\"小\\"需求。
\\n项目因此延期两周,还影响了其他项目进度。
\\n没有产品经理控制需求范围,程序员往往陷入无休止的\\"再加一个小功能\\"的噩梦中。
\\n让程序员直接对接客户,意味着他们需要花大量时间在沟通、解释、澄清需求上,而不是写代码。
\\n这就像让一名外科医生花一半时间去挂号、测血压、解释手术风险一样——虽然他可能都能做,但这是对专业技能的极大浪费。
\\n我有一个开发很厉害的同事,C++和嵌入式领域几乎无所不能,但一旦让他去客户那开会,他就紧张得说不出话,或者陷入技术细节无法自拔。结果他不得不花3-4小时准备一个1小时的客户会议,严重影响了他的开发效率。
\\n程序员的核心价值在于编码能力,而不是沟通协调能力。让擅长写代码的人去做不擅长的客户沟通,是双重的资源浪费。
\\n好的产品经理就像一个双语翻译,能将客户的业务语言翻译成程序员的技术语言,反之亦然。
\\n我公司招的第一个产品经理给我留下了深刻印象。她之前做过UI设计师,又懂一些编程基础,还有市场营销背景。在一次客户会议上,她神奇地做到了:
\\n客户说:\\"我们希望系统能够更智能地预测设备故障。\\"\\n产品经理立刻转化为:\\"您是希望系统能基于历史数据建立一个预警模型,当某些参数达到阈值时自动报警,对吗?\\"\\n客户点头:\\"对,就是这样!\\"\\n她转向开发团队:\\"我们需要开发一个基于规则引擎的异常检测模块,接入现有的数据采集系统,设置可配置的阈值规则...\\"
\\n一瞬间,模糊的需求变成了清晰的技术任务!这种\\"翻译\\"能力极大提高了沟通效率。
\\n好的产品经理能听懂客户的\\"言外之意\\",又能用开发能理解的语言表达出来,这种双向翻译能力是无价之宝。
\\n产品经理的另一个关键作用是过滤和排序需求。
\\n客户常常会提出各种想法:\\"我要这个功能,那个功能也要,最好下周就能上线。\\"如果这些需求都直接传达给程序员,他们会崩溃的。
\\n我的产品经理会做的是:
\\n记得有一次客户要求系统支持\\"所有主流浏览器\\",我们的产品经理没有简单答应,而是拿出数据:
\\n\\"贵公司90%的用户都使用Chrome和Firefox,只有不到2%使用IE。全面支持IE需要额外两周开发时间。我建议我们先专注主流浏览器,后续根据实际需求再考虑IE支持。您认为呢?\\"
\\n客户被这种数据驱动的分析说服了,我们避免了不必要的工作量。
\\n好的产品经理能帮团队聚焦真正重要的需求,避免浪费时间在低价值功能上。
\\n程序员通常倾向于诚实(有时过于诚实):\\"这个功能很复杂,可能需要一个月。\\"
\\n而客户通常希望听到:\\"没问题,下周就能完成!\\"
\\n这种期望差距是冲突的根源。好的产品经理知道如何既不过度承诺,又不直接拒绝客户:
\\n\\"这个功能确实很有价值,但完整开发需要较长时间。我们可以先开发一个简化版本,满足您的核心需求,后续再迭代完善。这样您下周就能看到初步效果,同时我们也能确保质量。您觉得这个方案如何?\\"
\\n我公司有一位资深产品经理特别擅长这种期望管理。她总是设定略低于团队预估的交付预期,然后当我们提前完成时,客户会惊喜地发现我们\\"超出预期\\"了。这种小技巧大大提升了客户满意度。
\\n好的产品经理能在客户期望和团队实际能力之间找到平衡点,既不会过度承诺导致失望,也不会因过于保守而错失机会。
\\n讲了这么多不应该直接对接的理由,但其实也有一些例外情况,程序员直接对接客户反而更有效:
\\n如果项目极其技术化,或者客户本身就是技术背景,直接对接反而减少了沟通环节。
\\n我曾经负责一个为科研机构开发的数据处理系统,客户团队全是物理学博士和计算机科学家。他们精确地知道自己需要什么算法、什么数据结构、什么输出格式。产品经理在这种情况下反而成了\\"多余的中间层\\"。
\\n我们后来采取的方式是:产品经理负责项目进度和资源协调,而技术细节由我直接与客户沟通。这种混合模式效果很好。
\\n当系统出现紧急故障,或客户需要深度技术咨询时,让程序员直接参与是最高效的。
\\n有一次客户系统突然崩溃,直接影响生产线运行。我们的产品经理立刻组织了一个远程会议,但她明智地保持了后台角色,由我们的技术专家直接与客户IT人员对话,迅速定位并解决了问题。
\\n在这种火力全开的紧急情况下,去掉中间环节反而更有效率。
\\n在小型创业团队中,每个人可能身兼数职,程序员直接对接客户是常态。
\\n我创业初期就是这样,自己既写代码又对接客户。虽然经历了不少沟通困难,但也锻炼了全方位能力,了解了更多业务知识。
\\n随着团队扩大,我们才逐渐引入了专职产品经理。但那段\\"全栈\\"经历对我后来管理团队很有帮助,因为我理解了双方的痛点。
\\n很多程序员对产品经理有误解,认为他们\\"什么都不做,就知道改需求\\"。作为曾经也这么想,后来创业才理解产品价值的人,我想澄清一下产品经理的实际工作:
\\n优秀的产品经理不只是传达客户明确提出的需求,还会主动挖掘潜在需求。
\\n我公司的产品经理经常做的一件事是访谈客户的实际用户(不只是决策者)。有一次她发现,虽然客户要求的是\\"详细的数据报表\\",但实际用户(现场操作人员)根本没时间看复杂报表,他们需要的是简单直观的异常提醒。
\\n这个发现让我们调整了产品方向,最终交付的系统更符合实际使用场景,客户非常满意。
\\n产品经理通过深入了解用户需求,帮助团队构建真正有价值的产品,而不仅仅是满足表面需求。
\\n好的产品经理会密切关注竞争对手的产品,找出差异化优势。
\\n记得我们开发一个工业监控系统时,产品经理对市场上主要竞品做了彻底分析,发现所有竞争对手都专注于功能全面性,但用户体验都较差。
\\n她提出我们的差异化策略:不追求功能最全,而是做\\"最容易上手\\"的系统。这一定位指导了后续所有设计决策,最终我们的产品虽然功能不是最多,但以\\"简单易用\\"迅速占领了市场份额。
\\n产品经理的竞品分析和战略定位,决定了产品的市场竞争力,这是单纯的技术实现无法替代的。
\\n将模糊的业务需求转化为明确的产品规格,是产品经理的核心工作。
\\n一个好的产品需求文档(PRD)包含:
\\n我曾收到过一个25页的详细PRD,涵盖了一个看似简单功能的各种细节。起初我觉得过于繁琐,但开发过程中发现这份文档预见了几乎所有可能出现的问题,极大减少了返工和沟通成本。
\\n好的产品经理能将复杂需求分解为可执行的任务,并预见可能的问题,这正是大多数程序员不擅长的工作。
\\n产品经理通常也承担项目管理的部分职责,协调各方资源,确保项目按时交付。
\\n我们公司的一个大项目涉及硬件团队、嵌入式软件团队、云平台团队和UI设计团队。产品经理每周组织跨团队会议,确保各方进度同步,及时解决阻碍问题。
\\n当一个功能开发遇到困难时,她会迅速调整计划,重新安排优先级,确保项目整体不受太大影响。这种灵活协调能力是项目成功的关键因素。
\\n产品经理扮演项目\\"润滑剂\\"的角色,帮助团队专注于技术实现,而不必分心处理各种协调工作。
\\n虽然UI设计师负责视觉设计,但产品经理负责整体用户体验和产品流程设计。
\\n我们有一个资深产品经理特别擅长用户体验优化。她会通过用户测试发现操作流程中的痛点,然后提出改进方案。
\\n有一次她观察到用户在系统中频繁切换几个特定页面,于是提议在界面中增加快捷入口,这个小改动大大提升了用户效率。而这种优化如果只由程序员来做,很可能被忽视,因为从技术角度看\\"系统运行正常\\"。
\\n产品经理关注的不只是功能能否实现,还有用户使用体验如何,这种以用户为中心的思维是打造成功产品的关键。
\\n作为曾经对产品经理不屑一顾,现在却深知他们价值的人,我想聊聊如何改善程序员和产品经理的关系:
\\n程序员和产品经理需要相互理解各自的专业领域和局限性。
\\n程序员擅长:技术实现、问题解决、系统架构\\n程序员局限:用户需求理解、商业价值判断、沟通表达
\\n产品经理擅长:需求分析、用户体验、商业价值评估\\n产品经理局限:技术实现细节、开发难度评估、性能优化
\\n我在公司推行的一个做法是:让新入职的产品经理参与一周的编程培训,让新入职的程序员参与一周的用户研究。这种\\"角色体验\\"大大增进了相互理解。
\\n我们建立了一些提高协作效率的机制:
\\n这些机制确保了产品决策既考虑业务价值,也考虑技术现实。
\\n最重要的转变是心态:不再将产品经理视为\\"提需求的甲方\\",而是视为\\"同一团队的伙伴\\"。
\\n我亲眼见证了一个团队的转变:从最初程序员抱怨\\"产品又改需求了\\",产品抱怨\\"开发总是拖延\\",到后来双方一起分析问题、共同寻找最优解决方案。
\\n这种转变始于一次危机:一个重要项目因为双方互相指责而濒临失败。在公司干预下,双方被迫放下成见,一起闭关三天重新规划项目。出乎意料的是,这次深度协作不仅挽救了项目,还建立了相互尊重的基础。
\\n真正高效的团队,不是程序员服从产品经理的安排,也不是产品经理迁就技术限制,而是双方基于各自专业共同打造最佳产品。
\\n学习基本的产品思维
\\n作为程序员,了解一些产品设计原则会让你的技术决策更有价值。推荐阅读《用户体验要素》《启示录:打造用户喜爱的产品》等书籍。
\\n我自己就是从完全不懂产品,到慢慢理解用户需求,再到现在能够从产品角度思考问题。这种转变让我的技术决策更加全面。
\\n提高沟通表达能力
\\n即使有产品经理,程序员也需要清晰表达技术观点。学会用非技术语言解释技术问题,是一项值得培养的能力。
\\n我强烈建议程序员参加一些演讲培训或写作练习,提高表达能力。这对职业发展大有裨益。
\\n主动参与产品讨论
\\n不要等产品经理把需求\\"扔\\"给你,而是主动参与需求讨论。你的技术视角可能发现产品经理忽视的问题。
\\n在我们公司,技术团队经常在需求初期就提出建设性意见,比如\\"这个功能如果稍微调整一下实现方式,可以节省50%的开发时间\\"。这种早期投入最终节省了大量时间。
\\n尊重技术团队的专业判断
\\n当程序员说某个功能技术上难以实现,请认真对待,而不是简单地说\\"试试看\\"或\\"加加班\\"。
\\n好的产品经理会问\\"为什么困难?有没有替代方案?\\"而不是一味坚持己见。
\\n学习基本的技术知识
\\n你不需要成为编程专家,但应该了解基本的技术概念和限制。这会让你的需求更切实可行。
\\n我见过最优秀的产品经理都能看懂一些代码,理解基本的技术架构,这极大提高了沟通效率。
\\n尽早让技术团队参与
\\n在需求成型前就让技术团队参与,会得到更多有价值的反馈,避免提出不切实际的需求。
\\n我们公司的产品经理通常会在正式编写PRD前,先与技术负责人进行头脑风暴,探讨可能的实现路径。
\\n建立合理的组织结构
\\n产品和技术应该是平行关系,而非上下级关系。避免\\"产品决定一切,技术只负责实现\\"的错误结构。
\\n在我的公司,重大产品决策需要产品负责人和技术负责人共同签字确认,确保双方都认可最终方案。
\\n鼓励跨部门合作
\\n组织产品和技术的联合培训、团建或轮岗,促进相互理解。
\\n我们每季度会组织一次\\"角色互换日\\",让产品经理体验一天开发工作,让程序员体验一天产品工作,效果很好。
\\n正确看待产品经理的价值
\\n不要将产品经理视为简单的\\"需求传话筒\\",而应该重视他们在产品规划、用户研究方面的专业能力。
\\n好的产品经理能带来巨大价值,值得投资培养和合理授权。
\\n回到最初的问题:为什么不让程序员直接对接客户,而是通过产品经理?
\\n答案已经很清楚:产品经理是专业的需求分析师、沟通翻译官和项目协调者,他们弥补了程序员在业务理解和客户沟通方面的短板,让程序员能够专注于技术实现。
\\n但这不意味着程序员应该完全与客户和业务隔离。最理想的状态是:产品经理负责日常客户沟通和需求管理,程序员在关键节点参与讨论,双方相互尊重专业领域,共同打造优秀产品。
\\n作为一个从纯技术到创业管理的转变者,我深刻体会到:伟大的产品不是由优秀的程序员或优秀的产品经理单独创造的,而是由优秀的团队协作创造的。
\\n就像嵌入式系统需要硬件和软件的紧密配合一样,优秀的产品需要技术和产品的完美融合。当技术追求的可靠性与产品追求的易用性达成平衡,当程序员的逻辑思维与产品经理的用户思维相互补充,产品才能真正打动用户。
\\n所以,与其纠结于\\"谁应该对接客户\\"这个表面问题,不如思考\\"如何建立最高效的协作模式\\"这个本质问题。
\\n在我的公司,我们正在努力打造这样一种文化:尊重每个角色的专业价值,消除部门壁垒,以用户价值为核心,共同创造令人骄傲的产品。
\\n这个过程充满挑战,但也充满成就感。希望我的经验分享能给同样面临这些问题的团队带来一些启发。
\\n另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
\\n刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
\\n推荐阅读:
\\n欢迎关注我的博客:良许嵌入式教程网,满满都是干货!
","description":"一、那些年,我们\\"撞过\\"的客户南墙 先说个真实故事。\\n\\n我刚从机械专业转行做嵌入式开发那会,公司接了个工业控制项目。当时团队小,没有专门的产品经理,老板直接让我和另外两个开发跟客户对接需求。\\n\\n那天会议室里,客户代表滔滔不绝:\\"我们需要一个简单的控制系统,界面做得好看点,操作要简单,最好点一下就能完成所有功能。对了,还要能实时监控所有设备状态,要有报警功能,要能远程控制,要......\\"\\n\\n我和同事一边狂点头一边记笔记,心想这不难啊,回去一周肯定能搞定。\\n\\n然后,我们花了三个月都没搞定。\\n\\n为什么?因为那次会议后,我们脑补了一套完整的需求,写了3万行代码…","guid":"https://juejin.cn/post/7489010197853388841","author":"良许Linux","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-05T05:04:07.169Z","media":[{"url":"https://lxlinux.superbed.verylink.top/item/6569931cc458853aef9b704b.jpg","type":"photo"}],"categories":["后端","Linux"],"attachments":null,"extra":null,"language":null},{"title":"公司来的新人用字符串存储日期,被组长怒怼了...","url":"https://juejin.cn/post/7488927722774937609","content":"在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。
\\n本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。
\\n和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,\'YYYY-MM-DD HH:MM:SS\' 这样的格式看起来清晰易懂。
\\n但是,这是不正确的做法,主要会有下面两个问题:
\\nDATETIME
和 TIMESTAMP
是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?
下面我们从几个关键维度对它们进行对比:
\\nDATETIME
类型存储的是字面量的日期和时间值,它本身不包含任何时区信息。当你插入一个 DATETIME
值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。
这样就会有什么问题呢? 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 DATETIME
时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。
TIMESTAMP
和时区有关。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 TIMESTAMP
字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。
这意味着,对于同一条记录的 TIMESTAMP
字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。
下面实际演示一下!
\\n建表 SQL 语句:
\\nCREATE TABLE `time_zone_test` (\\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\\n `date_time` datetime DEFAULT NULL,\\n `time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\\n PRIMARY KEY (`id`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\\n
\\n插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::
\\nINSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());\\n
\\n查询数据(在同一时区会话下):
\\nSELECT date_time, time_stamp FROM time_zone_test;\\n
\\n结果:
\\n+---------------------+---------------------+\\n| date_time | time_stamp |\\n+---------------------+---------------------+\\n| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |\\n+---------------------+---------------------+\\n
\\n现在,修改当前会话的时区为东八区 (UTC+8):
\\nSET time_zone = \'+8:00\';\\n
\\n再次查询数据:
\\n# TIMESTAMP 的值自动转换为 UTC+8 时间\\n+---------------------+---------------------+\\n| date_time | time_stamp |\\n+---------------------+---------------------+\\n| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |\\n+---------------------+---------------------+\\n
\\n扩展:MySQL 时区设置常用 SQL 命令
\\n# 查看当前会话时区\\nSELECT @@session.time_zone;\\n# 设置当前会话时区\\nSET time_zone = \'Europe/Helsinki\';\\nSET time_zone = \\"+00:00\\";\\n# 数据库全局时区设置\\nSELECT @@global.time_zone;\\n# 设置全局时区\\nSET GLOBAL time_zone = \'+8:00\';\\nSET GLOBAL time_zone = \'Europe/Helsinki\';\\n
\\n下图是 MySQL 日期类型所占的存储空间(官方文档传送门:dev.mysql.com/doc/refman/…):
\\n在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 58 字节,TIMESTAMP 的范围是 47 字节。
TIMESTAMP
表示的时间范围更小,只能到 2038 年:
DATETIME
:\'1000-01-01 00:00:00.000000\' 到 \'9999-12-31 23:59:59.999999\'TIMESTAMP
:\'1970-01-01 00:00:01.000000\' UTC 到 \'2038-01-19 03:14:07.999999\' UTC由于 TIMESTAMP
在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,DATETIME
因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。
为了获得可预测的行为并可能减少 TIMESTAMP
的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 time_zone
参数,而不是依赖服务器的默认或操作系统时区。
除了上述两种类型,实践中也常用整数类型(INT
或 BIGINT
)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。
这种存储方式的具有 TIMESTAMP
类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
时间戳的定义如下:
\\n\\n\\n时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。
\\n
数据库中实际操作:
\\n-- 将日期时间字符串转换为 Unix 时间戳 (秒)\\nmysql> SELECT UNIX_TIMESTAMP(\'2020-01-11 09:53:32\');\\n+---------------------------------------+\\n| UNIX_TIMESTAMP(\'2020-01-11 09:53:32\') |\\n+---------------------------------------+\\n| 1578707612 |\\n+---------------------------------------+\\n1 row in set (0.00 sec)\\n\\n-- 将 Unix 时间戳 (秒) 转换为日期时间格式\\nmysql> SELECT FROM_UNIXTIME(1578707612);\\n+---------------------------+\\n| FROM_UNIXTIME(1578707612) |\\n+---------------------------+\\n| 2020-01-11 09:53:32 |\\n+---------------------------+\\n1 row in set (0.01 sec)\\n
\\n由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:www.postgresql.org/docs/curren…。
\\n可以看到,PG 没有名为 DATETIME
的类型:
TIMESTAMP WITHOUT TIME ZONE
在功能上最接近 MySQL 的 DATETIME
。它存储日期和时间,但不包含任何时区信息,存储的是字面值。TIMESTAMP WITH TIME ZONE
(或 TIMESTAMPTZ
) 相当于 MySQL 的 TIMESTAMP
。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。对于绝大多数需要记录精确发生时间点的应用场景,TIMESTAMPTZ
是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。
MySQL 中时间到底怎么存储才好?DATETIME
?TIMESTAMP
?还是数值时间戳?
并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。
\\n《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:
\\n每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
---|---|---|---|---|
DATETIME | 5~8 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | 否 |
TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 |
数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 |
选择建议小结:
\\nTIMESTAMP
的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,TIMESTAMP
是自然的选择(注意其时间范围限制,也就是 2038 年问题)。DATETIME
是更稳妥的选择。领域驱动设计(Domain-Driven Design,简称DDD)是由Eric Evans提出的一种软件开发方法论,旨在应对复杂业务系统的设计和实现。它的核心思想是将软件的设计与业务领域紧密结合,通过深入理解业务需求,构建一个反映真实业务逻辑的模型,并用代码清晰地表达出来。
\\n在传统的开发模式中,我们常常以技术为中心,先设计数据库表结构或API接口,再围绕这些技术组件填充业务逻辑。而DDD则反其道而行之,它强调**领域(Domain)**是软件的核心,技术只是实现领域的工具。换句话说,DDD的目标是通过代码和架构让业务逻辑成为系统的“主角”。
\\n假设你正在开发一个电商系统,里面涉及商品、订单、库存、支付等功能。如果没有清晰的领域划分,可能出现以下问题:
\\nDDD通过领域模型和限界上下文解决了这些问题。它鼓励你先理解业务的全貌,再通过分层和模块化设计,让每个部分的职责清晰,降低耦合性。
\\nDDD通常采用以下四层架构:
\\n假设我们现在要设计一个基于Spring Cloud Alibaba的电商微服务系统,包含商品管理、订单管理、库存管理和支付管理等模块。我们可以用DDD来规划架构和文件结构。
\\n以下是一个基于Spring Cloud Alibaba的电商微服务系统的DDD文件框架。我们以订单服务(Order Service)为例,展示其目录结构。其他服务(如商品服务、库存服务)可以参照类似结构。
\\norder-service/\\n├── src/\\n│ ├── main/\\n│ │ ├── java/\\n│ │ │ └── com/\\n│ │ │ └── ecommerce/\\n│ │ │ └── order/\\n│ │ │ ├── presentation/ # 表现层\\n│ │ │ │ ├── controller/ # REST API控制器\\n│ │ │ │ │ └── OrderController.java\\n│ │ │ │ └── dto/ # 数据传输对象\\n│ │ │ │ ├── OrderRequest.java\\n│ │ │ │ └── OrderResponse.java\\n│ │ │ ├── application/ # 应用层\\n│ │ │ │ ├── service/ # 应用服务\\n│ │ │ │ │ └── OrderAppService.java\\n│ │ │ │ └── event/ # 领域事件处理\\n│ │ │ │ └── OrderEventPublisher.java\\n│ │ │ ├── domain/ # 领域层\\n│ │ │ │ ├── entity/ # 实体\\n│ │ │ │ │ ├── Order.java # 聚合根\\n│ │ │ │ │ └── OrderItem.java\\n│ │ │ │ ├── valueobject/ # 值对象\\n│ │ │ │ │ └── Address.java\\n│ │ │ │ ├── repository/ # 仓储接口\\n│ │ │ │ │ └── OrderRepository.java\\n│ │ │ │ ├── service/ # 领域服务\\n│ │ │ │ │ └── OrderDomainService.java\\n│ │ │ │ └── event/ # 领域事件\\n│ │ │ │ └── OrderCreatedEvent.java\\n│ │ │ ├── infrastructure/ # 基础设施层\\n│ │ │ │ ├── repository/ # 仓储实现\\n│ │ │ │ │ └── OrderRepositoryImpl.java\\n│ │ │ │ ├── mq/ # 消息队列集成\\n│ │ │ │ │ └── RocketMQProducer.java\\n│ │ │ │ └── config/ # 配置类\\n│ │ │ │ └── NacosConfig.java\\n│ │ ├── resources/\\n│ │ │ ├── application.yml # Spring Boot配置文件\\n│ │ │ └── nacos-config.properties # Nacos配置\\n│ └── test/\\n│ └── java/\\n│ └── com/\\n│ └── ecommerce/\\n│ └── order/\\n│ └── OrderServiceTest.java\\n├── pom.xml # Maven依赖文件\\n└── README.md # 项目说明\\n
\\n表现层(presentation)
\\nOrderController
:对外暴露REST API,比如创建订单、查询订单。OrderRequest
/OrderResponse
:DTO用于接收和返回数据,避免直接暴露领域模型。应用层(application)
\\nOrderAppService
:协调业务用例,比如调用领域服务创建订单、发布事件。OrderEventPublisher
:将领域事件发布到消息队列(如RocketMQ)。领域层(domain)
\\nOrder
:聚合根,包含订单的核心逻辑,比如添加订单项、计算总价。OrderItem
:实体,表示订单中的商品项。Address
:值对象,表示订单的配送地址。OrderRepository
:仓储接口,定义订单的持久化操作。OrderDomainService
:处理复杂的领域逻辑,比如订单状态转换。OrderCreatedEvent
:领域事件,表示订单已创建。基础设施层(infrastructure)
\\nOrderRepositoryImpl
:仓储的具体实现,使用Spring Data JPA或MyBatis。RocketMQProducer
:集成RocketMQ发送消息。NacosConfig
:配置Nacos服务发现和配置管理。以下是Order
聚合根的一个简化实现:
package com.ecommerce.order.domain.entity;\\n\\nimport com.ecommerce.order.domain.valueobject.Address;\\n\\nimport java.math.BigDecimal;\\nimport java.util.ArrayList;\\nimport java.util.List;\\n\\npublic class Order {\\n private Long id;\\n private List<OrderItem> items = new ArrayList<>();\\n private Address shippingAddress;\\n private BigDecimal totalAmount;\\n\\n public void addItem(OrderItem item) {\\n this.items.add(item);\\n calculateTotal();\\n }\\n\\n private void calculateTotal() {\\n this.totalAmount = items.stream()\\n .map(OrderItem::getSubtotal)\\n .reduce(BigDecimal.ZERO, BigDecimal::add);\\n }\\n\\n // Getters and setters\\n}\\n
\\nOrderCreatedEvent
,库存服务订阅事件并扣减库存。DDD的学习曲线可能较陡,但它能显著提升复杂系统设计的清晰度和可扩展性。希望这个框架能为你的电商微服务系统提供一个良好的起点!
","description":"什么是DDD领域驱动设计? DDD的基本概念\\n\\n领域驱动设计(Domain-Driven Design,简称DDD)是由Eric Evans提出的一种软件开发方法论,旨在应对复杂业务系统的设计和实现。它的核心思想是将软件的设计与业务领域紧密结合,通过深入理解业务需求,构建一个反映真实业务逻辑的模型,并用代码清晰地表达出来。\\n\\n在传统的开发模式中,我们常常以技术为中心,先设计数据库表结构或API接口,再围绕这些技术组件填充业务逻辑。而DDD则反其道而行之,它强调**领域(Domain)**是软件的核心,技术只是实现领域的工具。换句话说…","guid":"https://juejin.cn/post/7488927722774413321","author":"Asthenian","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-04T06:38:27.677Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"1K star!这个开源项目让短信集成简单到离谱,开发效率直接翻倍!","url":"https://juejin.cn/post/7488662486590783500","content":"嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法
\\n\\"让简单的事情回归简单的本质\\" —— SMS4J 项目宣言
\\n\\n\\n
SMS4J
是一款由国内技术团队打造的短信聚合框架,专为解决多短信服务商接入难题而生。它就像短信界的\\"瑞士军刀\\",目前已整合21家主流短信服务商,从阿里云、腾讯云到中国移动云MAS,开发者只需通过简单配置即可实现多平台无缝切换。
支持包括阿里云、华为云、京东云等21家服务商,更换服务商只需修改配置文件:
\\nsms:\\n blends:\\n aliyun:\\n accessKeyId: YOUR_KEY\\n signature: 公司签名\\n huawei:\\n appKey: YOUR_KEY\\n sender: 8823040504797\\n
\\n三行代码完成短信发送:
\\n// 获取阿里云短信服务\\nSmsBlend aliyun = SmsFactory.getSmsBlend(\\"aliyun\\");\\n// 发送验证码\\naliyun.sendMessage(\\"18888888888\\", \\"123456\\");\\n// 批量发送\\naliyun.massTexting(List.of(\\"18888888888\\",\\"16666666666\\"), \\"系统通知\\");\\n
\\n内置可配置化线程池,轻松应对高并发场景:
\\nsms:\\n corePoolSize: 20 # 核心线程数\\n maxPoolSize: 100 # 最大线程数\\n queueCapacity: 200 # 等待队列容量\\n
\\n支持固定模板与自定义模板双模式:
\\n// 固定模板发送\\naliyun.sendMessage(\\"18888888888\\", \\"123456\\");\\n\\n// 自定义模板\\nMap<String, String> params = new HashMap<>();\\nparams.put(\\"code\\", \\"654321\\");\\nparams.put(\\"time\\", \\"5分钟\\");\\nhuawei.sendMessage(\\"16666666666\\", params);\\n
\\n提供发送状态回调机制,实时掌握短信投递情况:
\\n// 设置华为云回调\\nhuawei.setStatusCallBack(\\"https://your-domain.com/callback\\");\\n
\\n层级 | 技术实现 | 功能说明 |
---|---|---|
接口层 | 统一API规范 | 提供标准化发送接口 |
适配层 | 厂商SDK适配器 | 封装各平台差异化实现 |
核心层 | Spring Boot Starter | 自动化配置 |
异步层 | ThreadPoolTaskExecutor | 异步任务处理 |
扩展层 | SPI机制 | 支持自定义扩展 |
// 大促期间批量发送\\njdCloud.massTexting(vipUsers, \\"年中大促5折优惠!\\");\\n
\\n// 政务短信模板\\nMap<String, String> params = new HashMap<>();\\nparams.put(\\"applicant\\", \\"张先生\\");\\nparams.put(\\"date\\", \\"2024-03-15\\");\\nctyun.sendMessage(\\"18888888888\\", params);\\n
\\n// 风控验证码\\nString code = generateRandomCode();\\naliyun.sendMessage(userPhone, code);\\nredis.saveCode(userId, code);\\n
\\n项目 | 支持厂商 | 配置复杂度 | 学习曲线 | 扩展性 | 社区活跃度 |
---|---|---|---|---|---|
SMS4J | 21+ | ★☆☆☆☆ | 1天 | 强 | 高 |
阿里云SDK | 1 | ★★★☆☆ | 3天 | 弱 | 中 |
腾讯云SDK | 1 | ★★★★☆ | 3天 | 弱 | 中 |
短信宝 | 3 | ★★☆☆☆ | 2天 | 中 | 低 |
<dependency>\\n <groupId>org.dromara.sms4j</groupId>\\n <artifactId>sms4j-spring-boot-starter</artifactId>\\n <version>3.3.4</version>\\n</dependency>\\n
\\nsms:\\n blends:\\n aliyun:\\n accessKeyId: AKID123456\\n accessKeySecret: SECRET789\\n signature: 阿里云签名\\n templateId: SMS_215125134\\n huawei:\\n appKey: 5N6fvXXXX920HaWhVXXXXXX7fYa\\n app-secret: Wujt7EYzZTBXXXXXXEhSP6XXXX\\n signature: 华为签名\\n sender: 8823040504797\\n
\\n@RestController\\npublic class SmsController {\\n \\n @GetMapping(\\"/sendAliyun\\")\\n public String sendAliyunSms() {\\n SmsFactory.getSmsBlend(\\"aliyun\\")\\n .sendMessage(\\"18888888888\\", \\"您的验证码是:123456\\");\\n return \\"短信已发送\\";\\n }\\n}\\n
\\nEasySMS:轻量级短信网关,支持5家服务商
\\nSmsAggregator:企业级短信中台
\\nUniSMS:跨平台解决方案
\\n\\n\\n🚀 作者主页: 有来技术
\\n🔥 开源项目: youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template
\\n🌺 仓库主页: GitCode︱ Gitee ︱ Github
\\n💖 欢迎点赞 👍 收藏 ⭐评论 📝 如有错误敬请纠正!
\\n
本文基于 Java 和 Spring Boot 3,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQL 和 Redis,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制,最终构建完整的企业级安全管理框架。
\\n作为开源项目youlai-boot 的入门篇,本文旨在帮助前端开发者或后端初学者快速上手 Java 后端开发。通过一步步实践,掌握项目的核心逻辑与实现细节,不仅能放心使用,还能轻松扩展和二次开发
\\n本章节介绍安装 Java 开发所需的环境,包括 JDK、Maven 和 IntelliJ IDEA(简称 IDEA),这些工具是 Java 开发的核心环境。
\\nJDK(Java Development Kit) 是 Java 开发工具包,包含编译器、运行时环境等,支持 Java 应用程序的开发与运行。
\\n访问以下链接,下载最新版本的 JDK 安装包:\\ndownload.oracle.com/java/17/arc…\\n下载完成后,双击安装包,根据引导完成安装。\\n示例安装路径:D:\\\\Java\\\\jdk-17.0.3.1
JAVA_HOME
,值为 D:\\\\Java\\\\jdk-17.0.3.1
Path
环境变量中,添加 %JAVA_HOME%\\\\bin
在命令行中执行以下命令,查看 Java 版本:
\\njava -version\\n
\\n输出类似如下内容表示安装成功:
\\nMaven 是一个流行的 Java 构建和依赖管理工具,类似于前端的 npm,用于管理项目的构建流程及第三方依赖库。
\\n访问以下链接,下载最新的 bin.zip
文件:maven.apache.org/download.cg…
将 bin.zip
解压到本地目录,示例解压路径:D:\\\\Soft\\\\apache-maven-3.9.5
编辑配置文件 D:\\\\Soft\\\\apache-maven-3.9.5\\\\conf\\\\settings.xml
,在 <mirrors>
节点中添加以下配置:
<mirrors>\\n <mirror>\\n <id>alimaven</id>\\n <name>aliyun maven</name>\\n <url>http://maven.aliyun.com/nexus/content/groups/public/</url>\\n <mirrorOf>central</mirrorOf> \\n </mirror>\\n</mirrors>\\n
\\nM2_HOME
,值为 D:\\\\Soft\\\\apache-maven-3.9.5
Path
环境变量中,添加 %M2_HOME%\\\\bin
在命令行中执行以下命令,查看 Maven 版本:
\\nmvn -v\\n
\\n输出类似如下内容表示安装成功:
\\nIntelliJ IDEA 是一款功能强大的 Java 集成开发环境(IDE),由 JetBrains 开发,广泛用于 Java 项目的开发、调试和运行。
\\n访问以下链接,下载适合您系统的安装包:www.jetbrains.com/idea/downlo…
\\n下载完成后,双击安装包,按引导完成安装即可。
\\n具体的配置在创建项目之前说明。
\\n推荐使用 Navicat,这是一款功能强大的数据库管理工具,但需要付费。如果你因未付费而遇到使用限制,可以选择 DBeaver 作为替代方案。
\\n下载并安装 Navicat 后,你将获得 14 天的免费试用期。安装完成后,连接到 MySQL 服务,即可对数据库和表进行可视化操作,体验非常流畅。
\\nNavicat 界面效果:
\\n推荐使用开源的 AnotherRedisDesktopManager,这是一款功能强大且免费的 Redis 可视化工具。
\\n安装步骤:
\\n使用步骤:
\\nAnotherRedisDesktopManager 界面效果:
\\n连接成功示例:
\\n打开 IDEA,选择 Projects → New Project。
\\nyoulai-boot
(可根据实际需求调整)。com.youlai
(可根据实际需求调整)。youlai-boot
(可根据实际需求调整)。com.youlai.boot
(可根据实际需求调整)。点击 Next,在左侧的依赖列表中勾选项目所需的依赖。
\\n完成项目初始化后,项目结构如下:
\\n通过 File → Project Structure(快捷键 Ctrl + Alt + Shift + S)打开项目结构配置面板,确保 Project 和 Modules 使用的 SDK 版本为前面安装的 JDK 17。
\\n通过 File → Settings(快捷键 Ctrl + Alt + S)打开设置面板,切换到 Maven 选项,并将 Maven 设置为前面安装到本地的版本。
\\n在 IDEA 的 Terminal 中输入以下命令 mvn -v
,验证 Maven 是否正确使用了 JDK 17:
src/main/java
目录下的 com.youlai.boot
包中,新建一个名为 controller
的包。controller
包下创建一个名为 TestController
的 Java 类。TestController
类中添加一个简单的 hello-world
接口。以下是 TestController
类的代码:
/**\\n * 测试接口\\n *\\n * @author youlai\\n */\\n@RestController\\npublic class TestController {\\n\\n @GetMapping(\\"/hello-world\\")\\n public String test() {\\n return \\"hello world\\";\\n }\\n\\n}\\n
\\n在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。
\\n控制台显示 Tomcat 已在端口 8080 (http)
,表示应用成功启动
打开浏览器,访问 http://localhost:8080/hello-world
,页面将显示 hello world
,表示接口正常运行。
为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。
\\n使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表
\\n -- ----------------------------\\n -- 1. 创建数据库\\n -- ----------------------------\\n CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;\\n\\n -- ----------------------------\\n -- 2. 创建表 && 数据初始化\\n -- ----------------------------\\n use youlai_boot;\\n\\n-- ----------------------------\\n -- Table structure for sys_user\\n -- ----------------------------\\n DROP TABLE IF EXISTS `sys_user`;\\n CREATE TABLE `sys_user` (\\n `id` int NOT NULL AUTO_INCREMENT,\\n `username` varchar(64) NULL DEFAULT NULL COMMENT \'用户名\',\\n `nickname` varchar(64) NULL DEFAULT NULL COMMENT \'昵称\',\\n `gender` tinyint(1) NULL DEFAULT 1 COMMENT \'性别(1-男 2-女 0-保密)\',\\n `password` varchar(100) NULL DEFAULT NULL COMMENT \'密码\',\\n `dept_id` int NULL DEFAULT NULL COMMENT \'部门ID\',\\n `avatar` varchar(255) NULL DEFAULT \'\' COMMENT \'用户头像\',\\n `mobile` varchar(20) NULL DEFAULT NULL COMMENT \'联系方式\',\\n `status` tinyint(1) NULL DEFAULT 1 COMMENT \'状态(1-正常 0-禁用)\',\\n `email` varchar(128) NULL DEFAULT NULL COMMENT \'用户邮箱\',\\n `create_time` datetime NULL DEFAULT NULL COMMENT \'创建时间\',\\n `create_by` bigint NULL DEFAULT NULL COMMENT \'创建人ID\',\\n `update_time` datetime NULL DEFAULT NULL COMMENT \'更新时间\',\\n `update_by` bigint NULL DEFAULT NULL COMMENT \'修改人ID\',\\n `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT \'逻辑删除标识(0-未删除 1-已删除)\',\\n PRIMARY KEY (`id`) USING BTREE\\n ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = \'用户表\' ROW_FORMAT = DYNAMIC;\\n\\n -- ----------------------------\\n -- Records of sys_user\\n -- ----------------------------\\n INSERT INTO `sys_user` VALUES (1, \'root\', \'有来技术\', 0, \'$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq\', NULL, \'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif\', \'18866668888\', 1, \'youlaitech@163.com\', NULL, NULL, NULL, NULL, 0);\\n INSERT INTO `sys_user` VALUES (2, \'admin\', \'系统管理员\', 1, \'$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq\', 1, \'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif\', \'18866668887\', 1, \'\', now(), NULL, now(), NULL, 0);\\n INSERT INTO `sys_user` VALUES (3, \'websocket\', \'测试小用户\', 1, \'$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq\', 3, \'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif\', \'18866668886\', 1, \'youlaitech@163.com\', now(), NULL, now(), NULL, 0);\\n\\n
\\n项目 pom.xml
添加 MySQL 驱动和 Mybatis-Plus 依赖:
<!-- MySQL 8 驱动 --\x3e\\n<dependency>\\n<groupId>com.mysql</groupId>\\n<artifactId>mysql-connector-j</artifactId>\\n<version>9.1.0</version>\\n<scope>runtime</scope>\\n</dependency>\\n\\n<!-- Druid 数据库连接池 --\x3e\\n<dependency>\\n<groupId>com.alibaba</groupId>\\n<artifactId>druid-spring-boot-starter</artifactId>\\n<version>1.2.24</version>\\n</dependency>\\n\\n<!-- MyBatis Plus Starter--\x3e\\n<dependency>\\n<groupId>com.baomidou</groupId>\\n<artifactId>mybatis-plus-spring-boot3-starter</artifactId>\\n<version>3.5.9</version>\\n</dependency>\\n
\\n将 src/main/resources/application.properties
文件修改为 src/main/resources/application.yml
,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容:
server:\\n port: 8080\\n \\nspring:\\n datasource:\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone=Asia/Shanghai&&characterEncoding=utf8\\n username: root\\n password: 123456\\n\\nmybatis-plus:\\n configuration:\\n # 驼峰命名映射\\n map-underscore-to-camel-case: true\\n # 打印 sql 日志\\n log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\\n global-config:\\n db-config:\\n id-type: auto # 主键策略\\n logic-delete-field: is_deleted # 全局逻辑删除字段(可选)\\n
\\n在 IDEA 中依次点击 File → Settings(快捷键 Ctrl + Alt + S),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。
\\n在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。
\\n输入数据库的 主机地址、用户名 和 密码,测试连接成功后点击 OK
保存。\\n
配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。
\\n设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。
\\n点击 Finish
生成,自动生成相关代码。
MybatisX 生成的代码存在以下问题:
\\nSysUserMapper.java
文件未标注 @Mapper
注解,导致无法被 Spring Boot 识别为 Mybatis 的 Mapper 接口。如果已配置 @MapperScan
,可以省略此注解,但最简单的方法是直接在 SysUserMapper.java
文件中添加 @Mapper
注解。注意避免导入错误的包。在 controller
包下创建 UserController.java
,编写用户管理接口:
/**\\n * 用户控制层\\n *\\n * @author youlai\\n * @since 2024/12/04\\n */\\n@RestController\\n@RequestMapping(\\"/users\\")\\n@RequiredArgsConstructor\\npublic class UserController {\\n\\n private final SysUserService userService;\\n\\n /**\\n * 获取用户列表\\n */\\n @GetMapping\\n public List<SysUser> listUsers() {\\n return userService.list();\\n }\\n\\n /**\\n * 获取用户详情\\n */\\n @GetMapping(\\"/{id}\\")\\n public SysUser getUserById(@PathVariable Long id) {\\n return userService.getById(id);\\n }\\n\\n /**\\n * 新增用户\\n */\\n @PostMapping\\n public String createUser(@RequestBody SysUser user) {\\n userService.save(user);\\n return \\"用户创建成功\\";\\n }\\n\\n /**\\n * 更新用户信息\\n */\\n @PutMapping(\\"/{id}\\")\\n public String updateUser(@PathVariable Long id, @RequestBody SysUser user) {\\n userService.updateById(user);\\n return \\"用户更新成功\\";\\n }\\n\\n /**\\n * 删除用户\\n */\\n @DeleteMapping(\\"/{id}\\")\\n public String deleteUser(@PathVariable Long id) {\\n userService.removeById(id);\\n return \\"用户删除成功\\";\\n }\\n\\n}\\n\\n
\\n重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。
\\n其他增删改接口可以通过后续整合接口文档进行测试。
\\nKnife4j 是基于 Swagger2 和 OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。
\\n在 pom.xml
文件中引入 Knife4j 的依赖:
<dependency>\\n <groupId>com.github.xiaoymin</groupId>\\n <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>\\n <version>4.5.0</version>\\n</dependency>\\n
\\n在 application.yml
文件中进行配置。注意,packages-to-scan
需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。
# springdoc-openapi 项目配置\\nspringdoc:\\n swagger-ui:\\n path: /swagger-ui.html\\n tags-sorter: alpha\\n operations-sorter: alpha\\n api-docs:\\n path: /v3/api-docs\\n group-configs:\\n - group: \'default\'\\n paths-to-match: \'/**\'\\n packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径\\n# knife4j的增强配置,不需要增强可以不配\\nknife4j:\\n enable: true\\n # 是否为生产环境,true 表示生产环境,接口文档将被禁用\\n production: false\\n setting:\\n language: zh_cn # 设置文档语言为中文\\n
\\n添加接口文档配置,在 com.youlai.boot.config
添加 OpenApiConfig
接口文档配置
package com.youlai.boot.config;\\n\\nimport io.swagger.v3.oas.models.Components;\\nimport io.swagger.v3.oas.models.OpenAPI;\\nimport io.swagger.v3.oas.models.info.Info;\\nimport io.swagger.v3.oas.models.security.SecurityRequirement;\\nimport io.swagger.v3.oas.models.security.SecurityScheme;\\nimport lombok.RequiredArgsConstructor;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springdoc.core.customizers.GlobalOpenApiCustomizer;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.core.env.Environment;\\nimport org.springframework.http.HttpHeaders;\\n\\n/**\\n * OpenAPI 接口文档配置\\n *\\n * @author youlai\\n */\\n@Configuration\\n@RequiredArgsConstructor\\n@Slf4j\\npublic class OpenApiConfig {\\n\\n private final Environment environment;\\n\\n /**\\n * 接口信息\\n */\\n\\n @Bean\\n public OpenAPI openApi() {\\n\\n String appVersion = environment.getProperty(\\"project.version\\", \\"1.0.0\\");\\n\\n return new OpenAPI()\\n .info(new Info()\\n .title(\\"系统接口文档\\")\\n .version(appVersion)\\n )\\n // 配置全局鉴权参数-Authorize\\n .components(new Components()\\n .addSecuritySchemes(HttpHeaders.AUTHORIZATION,\\n new SecurityScheme()\\n .name(HttpHeaders.AUTHORIZATION)\\n .type(SecurityScheme.Type.APIKEY)\\n .in(SecurityScheme.In.HEADER)\\n .scheme(\\"Bearer\\")\\n .bearerFormat(\\"JWT\\")\\n )\\n );\\n }\\n\\n\\n /**\\n * 全局自定义扩展\\n * <p>\\n * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。\\n * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。\\n */\\n @Bean\\n public GlobalOpenApiCustomizer globalOpenApiCustomizer() {\\n return openApi -> {\\n // 全局添加鉴权参数\\n if (openApi.getPaths() != null) {\\n openApi.getPaths().forEach((s, pathItem) -> {\\n // 登录接口/验证码不需要添加鉴权参数\\n if (\\"/api/v1/auth/login\\".equals(s)) {\\n return;\\n }\\n // 接口添加鉴权参数\\n pathItem.readOperations()\\n .forEach(operation ->\\n operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))\\n );\\n });\\n }\\n };\\n }\\n\\n}\\n
\\n完善接口描述
\\n在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例:
\\n@Tag(name = \\"用户接口\\")\\n@RestController\\n@RequestMapping(\\"/users\\")\\n@RequiredArgsConstructor\\npublic class UserController {\\n\\n private final SysUserService userService;\\n\\n @Operation(summary = \\"获取用户列表\\")\\n @GetMapping\\n public List<SysUser> listUsers() {\\n return userService.list();\\n }\\n\\n @Operation(summary = \\"获取用户详情\\")\\n @GetMapping(\\"/{id}\\")\\n public SysUser getUserById(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n ) {\\n return userService.getById(id);\\n }\\n\\n @Operation(summary = \\"新增用户\\")\\n @PostMapping\\n public String createUser(@RequestBody SysUser user) {\\n userService.save(user);\\n return \\"新增用户成功\\";\\n }\\n\\n @Operation(summary = \\"修改用户\\")\\n @PutMapping(\\"/{id}\\")\\n public String updateUser(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id,\\n @RequestBody SysUser user\\n ) {\\n userService.updateById(user);\\n return \\"修改用户成功\\";\\n }\\n\\n @Operation(summary = \\"删除用户\\")\\n @DeleteMapping(\\"/{id}\\")\\n public String deleteUser(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n ) {\\n userService.removeById(id);\\n return \\"用户删除成功\\";\\n }\\n\\n}\\n
\\n完善实体类描述
\\n在 SysUser
实体类中为每个字段添加 @Schema
注解,用于在接口文档中显示字段的详细说明及示例值:
@Schema(description = \\"用户对象\\")\\n@TableName(value = \\"sys_user\\")\\n@Data\\npublic class SysUser implements Serializable {\\n\\n @Schema(description = \\"用户ID\\", example = \\"1\\")\\n @TableId(type = IdType.AUTO)\\n private Integer id;\\n\\n @Schema(description = \\"用户名\\", example = \\"admin\\")\\n private String username;\\n\\n @Schema(description = \\"昵称\\", example = \\"管理员\\")\\n private String nickname;\\n\\n @Schema(description = \\"性别(1-男,2-女,0-保密)\\", example = \\"1\\")\\n private Integer gender;\\n\\n @Schema(description = \\"用户头像URL\\", example = \\"https://example.com/avatar.png\\")\\n private String avatar;\\n\\n @Schema(description = \\"联系方式\\", example = \\"13800000000\\")\\n private String mobile;\\n\\n @Schema(description = \\"用户邮箱\\", example = \\"admin@example.com\\")\\n private String email;\\n \\n // ... \\n}\\n
\\n完成以上步骤后,重新启动应用并访问生成的接口文档。
\\n通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值:
\\n接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据:
\\nRedis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。
\\n在 pom.xml
文件中添加 Spring Boot Redis 依赖:
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-data-redis</artifactId>\\n</dependency>\\n
\\n在 application.yml
文件中配置 Redis 连接信息:
spring:\\n data:\\n redis:\\n database: 0 # Redis 数据库索引\\n host: localhost # Redis 主机地址\\n port: 6379 # Redis 端口\\n # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串\\n password: 123456\\n timeout: 10s\\n
\\nSpring Boot 默认使用 JdkSerializationRedisSerializer
进行序列化。我们可以通过自定义 RedisTemplate
,将其修改为更易读的 String
和 JSON
序列化方式:
/**\\n * Redis 自动装配配置\\n *\\n * @author youlai\\n * @since 2024/12/5\\n */\\n@Configuration\\npublic class RedisConfig {\\n\\n /**\\n * 自定义 RedisTemplate\\n * <p>\\n * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer\\n *\\n * @param redisConnectionFactory {@link RedisConnectionFactory}\\n * @return {@link RedisTemplate}\\n */\\n @Bean\\n public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {\\n\\n RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();\\n redisTemplate.setConnectionFactory(redisConnectionFactory);\\n\\n redisTemplate.setKeySerializer(RedisSerializer.string());\\n redisTemplate.setValueSerializer(RedisSerializer.json());\\n\\n redisTemplate.setHashKeySerializer(RedisSerializer.string());\\n redisTemplate.setHashValueSerializer(RedisSerializer.json());\\n\\n redisTemplate.afterPropertiesSet();\\n return redisTemplate;\\n }\\n\\n}\\n\\n
\\n在 src/test/java
目录的 com.youlai.boot
包下创建 RedisTests
单元测试类,用于验证数据的存储与读取。
@SpringBootTest\\n@Slf4j\\nclass RedisTests {\\n\\n @Autowired\\n private RedisTemplate<String, Object> redisTemplate;\\n\\n @Autowired\\n private SysUserService userService;\\n\\n @Test\\n void testSetAndGet() {\\n Long userId = 1L;\\n // 1. 从数据库中获取用户信息\\n SysUser user = userService.getById(userId);\\n log.info(\\"从数据库中获取用户信息: {}\\", user);\\n\\n // 2. 将用户信息缓存到 Redis\\n redisTemplate.opsForValue().set(\\"user:\\" + userId, user);\\n\\n // 3. 从 Redis 中获取缓存的用户信息\\n SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get(\\"user:\\" + userId);\\n log.info(\\"从 Redis 中获取用户信息: {}\\", cachedUser);\\n }\\n}\\n
\\n点击测试类方法左侧的 ▶️ 图标运行单元测试。
\\n运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。
\\n为什么需要统一响应?
\\n默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。
\\n下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。
\\n定义统一业务状态码
\\n在 com.youlai.boot.common.result
包下创建 ResultCode
枚举,错误码规范参考 阿里开发手册-错误码设计。
package com.youlai.boot.common.result;\\n\\nimport java.io.Serializable;\\nimport lombok.Getter;\\n\\n/**\\n * 统一业务状态码枚举\\n *\\n * @author youlai\\n */\\n@Getter\\npublic enum ResultCode implements Serializable {\\n\\n SUCCESS(\\"00000\\", \\"操作成功\\"),\\n TOKEN_INVALID(\\"A0230\\", \\"Token 无效或已过期\\"),\\n ACCESS_UNAUTHORIZED(\\"A0301\\", \\"访问未授权\\"),\\n SYSTEM_ERROR(\\"B0001\\", \\"系统错误\\");\\n\\n private final String code;\\n private final String message;\\n\\n ResultCode(String code, String message) {\\n this.code = code;\\n this.message = message;\\n }\\n}\\n
\\n创建统一响应结构
\\n定义 Result
类,封装响应码、消息和数据。
package com.youlai.boot.common.result;\\n\\nimport lombok.Data;\\nimport java.io.Serializable;\\n\\n/**\\n * 统一响应结构\\n *\\n * @author youlai\\n **/\\n@Data\\npublic class Result<T> implements Serializable {\\n // 响应码\\n private String code;\\n // 响应数据\\n private T data;\\n // 响应信息\\n private String msg;\\n\\n /**\\n * 成功响应\\n */\\n public static <T> Result<T> success(T data) {\\n Result<T> result = new Result<>();\\n result.setCode(ResultCode.SUCCESS.getCode());\\n result.setMsg(ResultCode.SUCCESS.getMsg());\\n result.setData(data);\\n return result;\\n }\\n\\n /**\\n * 失败响应\\n */\\n public static <T> Result<T> failed(ResultCode resultCode) {\\n Result<T> result = new Result<>();\\n result.setCode(resultCode.getCode());\\n result.setMsg(resultCode.getMsg());\\n return result;\\n }\\n\\n /**\\n * 失败响应(系统默认错误)\\n */\\n public static <T> Result<T> failed() {\\n Result<T> result = new Result<>();\\n result.setCode(ResultCode.SYSTEM_ERROR.getCode());\\n result.setMsg(ResultCode.SYSTEM_ERROR.getMsg());\\n return result;\\n }\\n\\n}\\n
\\n封装接口返回结果
\\n调整接口代码,返回统一的响应格式。
\\n@Operation(summary = \\"获取用户详情\\")\\n@GetMapping(\\"/{id}\\")\\npublic Result<SysUser> getUserById(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n) {\\n SysUser user = userService.getById(id);\\n return Result.success(user);\\n}\\n
\\n效果预览
\\n接口返回结构变为标准格式:
\\n通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。
\\n为什么需要全局异常处理
\\n如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。
\\n以下接口模拟了一个业务逻辑中的异常:
\\n@Operation(summary = \\"获取用户详情\\")\\n@GetMapping(\\"/{id}\\")\\npublic Result<SysUser> getUserById(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n) {\\n // 模拟异常\\n int i = 1 / 0;\\n\\n SysUser user = userService.getById(id);\\n return Result.success(user);\\n}\\n
\\n当发生异常时,默认返回的数据格式如下所示:
\\n这类非标准的响应格式既不直观,也不利于前后端协作。
\\n全局异常处理器
\\n在 com.youlai.boot.common.exception
包下创建全局异常处理器,用于捕获和处理系统异常。
package com.youlai.boot.common.exception;\\n\\nimport com.youlai.boot.common.result.Result;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springframework.http.HttpStatus;\\nimport org.springframework.web.bind.annotation.ExceptionHandler;\\nimport org.springframework.web.bind.annotation.ResponseStatus;\\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\\n\\n/**\\n * 全局异常处理器\\n *\\n * @author youlai\\n */\\n@RestControllerAdvice\\n@Slf4j\\npublic class GlobalExceptionHandler {\\n\\n /**\\n * 处理系统异常\\n * <p>\\n * 兜底异常处理,处理未被捕获的异常\\n */\\n @ExceptionHandler(Exception.class)\\n @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\\n public <T> Result<T> handleNullPointerException(Exception e) {\\n log.error(e.getMessage(), e);\\n return Result.failed(\\"系统异常:\\" + e.getMessage());\\n }\\n\\n}\\n
\\n验证全局异常处理
\\n再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范:
\\n自定义业务异常
\\n在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException
,可以实现更灵活的异常处理机制。
package com.youlai.boot.common.exception;\\n\\nimport com.youlai.boot.common.result.ResultCode;\\nimport lombok.Getter;\\n\\n/**\\n * 自定义业务异常\\n *\\n * @author youlai\\n */\\n@Getter\\npublic class BusinessException extends RuntimeException {\\n\\n public ResultCode resultCode;\\n\\n public BusinessException(ResultCode errorCode) {\\n super(errorCode.getMsg());\\n this.resultCode = errorCode;\\n }\\n\\n public BusinessException(String message) {\\n super(message);\\n }\\n\\n}\\n
\\n在全局异常处理器中添加业务异常处理逻辑
\\n@RestControllerAdvice\\n@Slf4j\\npublic class GlobalExceptionHandler {\\n\\n /**\\n * 处理自定义业务异常\\n */\\n @ExceptionHandler(BusinessException.class)\\n public <T> Result<T> handleBusinessException(BusinessException e) {\\n log.error(e.getMessage(), e);\\n if(e.getResultCode()!=null){\\n return Result.failed(e.getResultCode());\\n }\\n return Result.failed(e.getMessage());\\n }\\n\\n}\\n
\\n模拟业务异常
\\n @Operation(summary = \\"获取用户详情\\")\\n @GetMapping(\\"/{id}\\")\\n public Result<SysUser> getUserById(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n ) {\\n SysUser user = userService.getById(-1);\\n // 模拟异常\\n if (user == null) {\\n throw new BusinessException(\\"用户不存在\\");\\n }\\n return Result.success(user);\\n }\\n\\n
\\n请求不存在的用户时,响应如下:
\\n通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。
\\n日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具
\\n配置 logback-spring.xml
日志文件
在 src/main/resources
目录下,新增 logback-spring.xml
配置文件。基于 Spring Boot \\"约定优于配置\\" 的设计理念,项目默认会自动加载并使用该配置文件。
<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 --\x3e\\n<configuration>\\n\\n <!-- SpringBoot默认logback的配置 --\x3e\\n <include resource=\\"org/springframework/boot/logging/logback/defaults.xml\\"/>\\n\\n <springProperty scope=\\"context\\" name=\\"APP_NAME\\" source=\\"spring.application.name\\"/>\\n <property name=\\"LOG_HOME\\" value=\\"/logs/${APP_NAME}\\"/>\\n\\n <!-- 1. 输出到控制台--\x3e\\n <appender name=\\"CONSOLE\\" class=\\"ch.qos.logback.core.ConsoleAppender\\">\\n <!-- <withJansi>true</withJansi>--\x3e\\n <!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息--\x3e\\n <filter class=\\"ch.qos.logback.classic.filter.ThresholdFilter\\">\\n <level>DEBUG</level>\\n </filter>\\n <encoder>\\n <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>\\n <charset>UTF-8</charset>\\n </encoder>\\n </appender>\\n\\n <!-- 2. 输出到文件 --\x3e\\n <appender name=\\"FILE\\" class=\\"ch.qos.logback.core.rolling.RollingFileAppender\\">\\n <!-- 当前记录的日志文档完整路径 --\x3e\\n <file>${LOG_HOME}/log.log</file>\\n <encoder>\\n <!--日志文档输出格式--\x3e\\n <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern>\\n <charset>UTF-8</charset>\\n </encoder>\\n <!-- 日志记录器的滚动策略,按大小和时间记录 --\x3e\\n <rollingPolicy class=\\"ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy\\">\\n <!-- 滚动后的日志文件命名模式 --\x3e\\n <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern>\\n <!-- 单个日志文件的最大大小 --\x3e\\n <maxFileSize>10MB</maxFileSize>\\n <!-- 最大保留30天的日志 --\x3e\\n <maxHistory>30</maxHistory>\\n <!-- 总日志文件大小不超过3GB --\x3e\\n <totalSizeCap>1GB</totalSizeCap>\\n </rollingPolicy>\\n <!-- 临界值过滤器,输出大于INFO级别日志 --\x3e\\n <filter class=\\"ch.qos.logback.classic.filter.ThresholdFilter\\">\\n <level>INFO</level>\\n </filter>\\n </appender>\\n\\n <!-- 根日志记录器配置 --\x3e\\n <root level=\\"INFO\\">\\n <!-- 引用上面定义的两个appender,日志将同时输出到控制台和文件 --\x3e\\n <appender-ref ref=\\"CONSOLE\\"/>\\n <appender-ref ref=\\"FILE\\"/>\\n </root>\\n</configuration>\\n
\\n查看日志输出效果
\\n添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息:
\\nSpring Security 是一个强大的安全框架,可用于身份认证和权限管理。
\\n在 pom.xml
添加 Spring Security
依赖
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-security</artifactId>\\n</dependency>\\n
\\n从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。
\\n定义用户认证信息类 UserDetails
\\n创建 com.youlai.boot.security.model
包,新建 SysUserDetails
用户认证信息对象,继承 Spring Security 的 UserDetails 接口
/**\\n * Spring Security 用户认证信息对象\\n * <p>\\n * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。\\n * 实现了 {@link UserDetails} 接口,提供用户的核心信息。\\n *\\n * @author youlai\\n */\\n@Data\\n@NoArgsConstructor\\npublic class SysUserDetails implements UserDetails {\\n\\n /**\\n * 用户ID\\n */\\n private Integer userId;\\n\\n /**\\n * 用户名\\n */\\n private String username;\\n\\n /**\\n * 密码\\n */\\n private String password;\\n\\n /**\\n * 账号是否启用(true:启用,false:禁用)\\n */\\n private Boolean enabled;\\n\\n /**\\n * 用户角色权限集合\\n */\\n private Collection<SimpleGrantedAuthority> authorities;\\n\\n /**\\n * 根据用户认证信息初始化用户详情对象\\n */\\n public SysUserDetails(SysUser user) {\\n this.userId = user.getId();\\n this.username = user.getUsername();\\n this.password = user.getPassword();\\n this.enabled = ObjectUtil.equal(user.getStatus(), 1);\\n\\n // 初始化角色权限集合\\n this.authorities = CollectionUtil.isNotEmpty(user.getRoles())\\n ? user.getRoles().stream()\\n // 角色名加上前缀 \\"ROLE_\\",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add)\\n .map(role -> new SimpleGrantedAuthority(\\"ROLE_\\" + role))\\n .collect(Collectors.toSet())\\n : Collections.emptySet();\\n }\\n\\n @Override\\n public Collection<? extends GrantedAuthority> getAuthorities() {\\n return this.authorities;\\n }\\n\\n @Override\\n public String getPassword() {\\n return this.password;\\n }\\n\\n @Override\\n public String getUsername() {\\n return this.username;\\n }\\n\\n @Override\\n public boolean isEnabled() {\\n return this.enabled;\\n }\\n}\\n
\\n获取用户认证信息服务类
\\n创建 com.youlai.boot.security.service
包,新建 SysUserDetailsService
用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口
/**\\n * 用户认证信息加载服务类\\n * <p>\\n * 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法,\\n * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。\\n *\\n * @author youlai\\n */\\n@Service\\n@RequiredArgsConstructor\\npublic class SysUserDetailsService implements UserDetailsService {\\n\\n private final SysUserService userService;\\n\\n /**\\n * 根据用户名加载用户的认证信息\\n */\\n @Override\\n public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\\n // 查询用户基本信息\\n SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>()\\n .eq(SysUser::getUsername, username)\\n );\\n if (user == null) {\\n throw new UsernameNotFoundException(username);\\n }\\n // 模拟设置角色,实际应从数据库获取用户角色信息\\n Set<String> roles = Set.of(\\"ADMIN\\");\\n user.setRoles(roles);\\n\\n // 模拟设置权限,实际应从数据库获取用户权限信息\\n Set<String> perms = Set.of(\\"sys:user:query\\");\\n user.setPerms(perms);\\n\\n // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象\\n return new SysUserDetails(user);\\n }\\n}\\n
\\n在 com.youlai.boot.common.util
添加响应工具类 ResponseUtils
@Slf4j\\npublic class ResponseUtils {\\n\\n /**\\n * 异常消息返回(适用过滤器中处理异常响应)\\n *\\n * @param response HttpServletResponse\\n * @param resultCode 响应结果码\\n */\\n public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {\\n // 根据不同的结果码设置HTTP状态\\n int status = switch (resultCode) {\\n case ACCESS_UNAUTHORIZED, \\n ACCESS_TOKEN_INVALID \\n -> HttpStatus.UNAUTHORIZED.value();\\n default -> HttpStatus.BAD_REQUEST.value();\\n };\\n\\n response.setStatus(status);\\n response.setContentType(MediaType.APPLICATION_JSON_VALUE);\\n response.setCharacterEncoding(StandardCharsets.UTF_8.name());\\n\\n try (PrintWriter writer = response.getWriter()) {\\n String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));\\n writer.print(jsonResponse);\\n writer.flush(); // 确保将响应内容写入到输出流\\n } catch (IOException e) {\\n log.error(\\"响应异常处理失败\\", e);\\n }\\n }\\n\\n}\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n功能 | AuthenticationEntryPoint | AccessDeniedHandler |
---|---|---|
对应异常 | AuthenticationException | AccessDeniedException |
适用场景 | 用户未认证(无凭证或凭证无效) | 用户已认证但无权限 |
返回 HTTP 状态码 | 401 Unauthorized | 403 Forbidden |
常见使用位置 | 用于处理身份认证失败的全局入口逻辑 | 用于处理权限不足时的逻辑 |
用户未认证处理器
\\n/**\\n * 未认证处理器\\n *\\n * @author youlai\\n */\\n@Slf4j\\npublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {\\n\\n @Override\\n public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {\\n if (authException instanceof BadCredentialsException) {\\n // 用户名或密码错误\\n ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);\\n } else {\\n // token 无效或者 token 过期\\n ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);\\n }\\n }\\n \\n}\\n
\\n无权限访问处理器
\\n/**\\n * 无权限访问处理器\\n *\\n * @author youlai\\n */\\npublic class MyAccessDeniedHandler implements AccessDeniedHandler {\\n\\n @Override\\n public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {\\n ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);\\n }\\n\\n}\\n
\\n注意事项
\\n在全局异常处理器中,认证异常(AuthenticationException)和授权异常(AccessDeniedException)不应被捕获,否则这些异常将无法交给 Spring Security 的异常处理机制进行处理。因此,当捕获到这类异常时,应该将其重新抛出,交给 Spring Security 来处理其特定的逻辑。
\\npublic class GlobalExceptionHandler {\\n\\n /**\\n * 处理系统异常\\n * <p>\\n * 兜底异常处理,处理未被捕获的异常\\n */\\n @ExceptionHandler(Exception.class)\\n @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\\n public <T> Result<T> handleNullPointerException(Exception e) throws Exception {\\n // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理\\n if (e instanceof AccessDeniedException\\n || e instanceof AuthenticationException) {\\n throw e;\\n }\\n\\n log.error(e.getMessage(), e);\\n return Result.failed(\\"系统异常,请联系管理员\\");\\n }\\n\\n}\\n
\\n在 com.youlai.boot.config
包下新建 SecurityConfig
用来 Spring Security 安全配置
/**\\n * Spring Security 安全配置\\n *\\n * @author youlai\\n */\\n@Configuration\\n@EnableWebSecurity // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链\\n@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)\\npublic class SecurityConfig {\\n\\n /**\\n * 忽略认证的 URI 地址\\n */\\n private final String[] IGNORE_URIS = {\\"/api/v1/auth/login\\"};\\n\\n /**\\n * 配置安全过滤链,用于定义哪些请求需要认证或授权\\n */\\n @Bean\\n public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\\n\\n // 配置认证与授权规则\\n http\\n .authorizeHttpRequests(requestMatcherRegistry ->\\n requestMatcherRegistry\\n .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证\\n .anyRequest().authenticated() // 其他请求必须认证\\n )\\n // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT)\\n .sessionManagement(configurer ->\\n configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)\\n )\\n // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF)\\n .csrf(AbstractHttpConfigurer::disable)\\n // 禁用默认的表单登录功能\\n .formLogin(AbstractHttpConfigurer::disable)\\n // 禁用 HTTP Basic 认证(统一使用 JWT 认证)\\n .httpBasic(AbstractHttpConfigurer::disable)\\n // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中\\n .headers(headers ->\\n headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)\\n )\\n // 异常处理\\n .exceptionHandling(configurer -> {\\n configurer\\n .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器\\n .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器\\n });\\n \\n ;\\n\\n return http.build();\\n }\\n\\n /**\\n * 配置密码加密器\\n *\\n * @return 密码加密器\\n */\\n @Bean\\n public PasswordEncoder passwordEncoder() {\\n return new BCryptPasswordEncoder();\\n }\\n \\n /**\\n * 用于配置不需要认证的 URI 地址\\n */\\n @Bean\\n public WebSecurityCustomizer webSecurityCustomizer() {\\n return (web) -> {\\n web.ignoring().requestMatchers(\\n \\"/v3/api-docs/**\\",\\n \\"/swagger-ui/**\\",\\n \\"/swagger-ui.html\\",\\n \\"/webjars/**\\",\\n \\"/doc.html\\"\\n );\\n };\\n }\\n\\n /**\\n *认证管理器\\n */\\n @Bean\\n public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {\\n return configuration.getAuthenticationManager();\\n }\\n}\\n\\n
\\n在 com.youlai.boot.security.manager
包下新建 JwtTokenManager
,用于生成和解析 token
/**\\n * JWT Token 管理类\\n *\\n * @author youlai\\n */\\n@Service\\npublic class JwtTokenManager {\\n\\n /**\\n * JWT 密钥,用于签名和解签名\\n */\\n private final String secretKey = \\" SecretKey012345678901234567890123456789012345678901234567890123456789\\";\\n\\n /**\\n * 访问令牌有效期(单位:秒), 默认 1 小时\\n */\\n private final Integer accessTokenTimeToLive = 3600;\\n\\n /**\\n * 生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token\\n *\\n * @param authentication 用户认证信息\\n * @return JWT 访问令牌\\n */\\n public String generateToken(Authentication authentication) {\\n SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();\\n Map<String, Object> payload = new HashMap<>();\\n // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入\\n payload.put(\\"userId\\", userDetails.getUserId());\\n\\n // 将用户的角色和权限信息放入 JWT 载荷中,例如:[\\"ROLE_ADMIN\\", \\"sys:user:query\\"]\\n Set<String> authorities = authentication.getAuthorities().stream()\\n .map(GrantedAuthority::getAuthority)\\n .collect(Collectors.toSet());\\n payload.put(\\"authorities\\", authorities);\\n\\n Date now = new Date();\\n payload.put(JWTPayload.ISSUED_AT, now);\\n\\n // 设置过期时间 -1 表示永不过期\\n if (accessTokenTimeToLive != -1) {\\n Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive);\\n payload.put(JWTPayload.EXPIRES_AT, expiresAt);\\n }\\n payload.put(JWTPayload.SUBJECT, authentication.getName());\\n payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());\\n\\n return JWTUtil.createToken(payload, secretKey.getBytes());\\n }\\n\\n\\n /**\\n * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息\\n *\\n * @param token JWT Token\\n * @return Authentication 对象\\n */\\n public Authentication parseToken(String token) {\\n\\n JWT jwt = JWTUtil.parseToken(token);\\n JSONObject payloads = jwt.getPayloads();\\n SysUserDetails userDetails = new SysUserDetails();\\n userDetails.setUserId(payloads.getInt(\\"userId\\")); // 用户ID\\n userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名\\n // 角色集合\\n Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(\\"authorities\\")\\n .stream()\\n .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))\\n .collect(Collectors.toSet());\\n\\n return new UsernamePasswordAuthenticationToken(userDetails, \\"\\", authorities);\\n }\\n\\n /**\\n * 验证 JWT Token 是否有效\\n *\\n * @param token JWT Token 不携带 Bearer 前缀\\n * @return 是否有效\\n */\\n public boolean validateToken(String token) {\\n JWT jwt = JWTUtil.parseToken(token);\\n // 检查 Token 是否有效(验签 + 是否过期)\\n return jwt.setKey(secretKey.getBytes()).validate(0);\\n }\\n\\n}\\n
\\n在 com.youlai.boot.controller
包下新建 AuthController
/**\\n * 认证控制器\\n *\\n * @author youlai\\n */\\n@Tag(name = \\"01.认证中心\\")\\n@RestController\\n@RequestMapping(\\"/api/v1/auth\\")\\n@RequiredArgsConstructor\\npublic class AuthController {\\n\\n // 认证管理器 - 用于执行认证\\n private final AuthenticationManager authenticationManager;\\n\\n // JWT 令牌服务类 - 用于生成 JWT 令牌\\n private final JwtTokenManager jwtTokenManager;\\n\\n @Operation(summary = \\"登录\\")\\n @PostMapping(\\"/login\\")\\n public Result<String> login(\\n @Parameter(description = \\"用户名\\", example = \\"admin\\") @RequestParam String username,\\n @Parameter(description = \\"密码\\", example = \\"123456\\") @RequestParam String password\\n ) {\\n\\n // 1. 创建用于密码认证的令牌(未认证)\\n UsernamePasswordAuthenticationToken authenticationToken =\\n new UsernamePasswordAuthenticationToken(username.trim(), password);\\n\\n // 2. 执行认证(认证中)\\n Authentication authentication = authenticationManager.authenticate(authenticationToken);\\n\\n // 3. 认证成功后生成 JWT 令牌(已认证)\\n String accessToken = jwtTokenManager.generateToken(authentication);\\n\\n return Result.success(accessToken);\\n }\\n}\\n
\\n访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token
\\n访问 jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。
\\n我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。
\\n验证解析 Token 过滤器
\\n新建 com.youlai.boot.security.filter
添加 JwtValidationFilter
过滤器 用于验证和解析token
/**\\n * JWT Token 验证和解析过滤器\\n * <p>\\n * 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。\\n * 如果 Token 无效或解析失败,直接返回错误响应。\\n * </p>\\n *\\n * @author youlai\\n */\\npublic class JwtAuthenticationFilter extends OncePerRequestFilter {\\n\\n private static final String BEARER_PREFIX = \\"Bearer \\";\\n\\n private final JwtTokenManager jwtTokenManager;\\n\\n public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {\\n this.jwtTokenManager = jwtTokenManager;\\n }\\n\\n @Override\\n protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {\\n String token = request.getHeader(HttpHeaders.AUTHORIZATION);\\n try {\\n if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) {\\n // 去除 Bearer 前缀\\n token = token.substring(BEARER_PREFIX.length());\\n // 校验 JWT Token ,包括验签和是否过期\\n boolean isValidate = jwtTokenService.validateToken(token);\\n if (!isValidate) {\\n writeErrMsg(response, ResultCode.TOKEN_INVALID);\\n return;\\n }\\n // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中\\n Authentication authentication = jwtTokenManager.parseToken(token);\\n SecurityContextHolder.getContext().setAuthentication(authentication);\\n }\\n } catch (Exception e) {\\n SecurityContextHolder.clearContext();\\n writeErrMsg(response, ResultCode.TOKEN_INVALID);\\n return;\\n }\\n \\n // 无 Token 或 Token 验证通过时,继续执行过滤链。\\n // 如果请求不在白名单内(例如登录接口、静态资源等),\\n // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。\\n // 例如:\\n // - 匹配到 permitAll() 的规则会直接放行。\\n // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。\\n // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。\\n filterChain.doFilter(request, response);\\n }\\n\\n /**\\n * 异常消息返回\\n *\\n * @param response HttpServletResponse\\n * @param resultCode 响应结果码\\n */\\n public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {\\n int status = switch (resultCode) {\\n case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();\\n default -> HttpStatus.BAD_REQUEST.value();\\n };\\n\\n response.setStatus(status);\\n response.setContentType(MediaType.APPLICATION_JSON_VALUE);\\n response.setCharacterEncoding(StandardCharsets.UTF_8.name());\\n\\n try (PrintWriter writer = response.getWriter()) {\\n String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));\\n writer.print(jsonResponse);\\n writer.flush();\\n } catch (IOException e) {\\n // 日志记录:捕获响应写入失败异常\\n // LOGGER.error(\\"Error writing response\\", e);\\n }\\n }\\n}\\n
\\n添加 JWT 验证和解析过滤器
\\n在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。
\\n/**\\n * Spring Security 安全配置\\n *\\n * @author youlai\\n */\\n@RequiredArgsConstructor\\npublic class SecurityConfig {\\n\\n // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作\\n private final JwtTokenManager jwtTokenManager;\\n\\n @Bean\\n public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\\n return http \\n // ... \\n // JWT 验证和解析过滤器\\n .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class)\\n .build();\\n }\\n\\n // ...\\n}\\n\\n
\\n获取用户列表接口
\\n@RestController\\n@RequestMapping(\\"/users\\")\\n@RequiredArgsConstructor\\npublic class UserController {\\n\\n private final SysUserService userService;\\n\\n @Operation(summary = \\"获取用户列表\\")\\n @GetMapping\\n @PreAuthorize(\\"hasAuthority(\'sys:user:query\')\\")\\n public List<SysUser> listUsers() {\\n return userService.list();\\n }\\n \\n}\\n
\\n访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize(\\"hasAuthority(\'sys:user:query\')\\") 标记说明用户需要有 sys:user:query
的权限,也就是所谓的鉴权。
正常访问
\\n不携带 token 访问
\\n携带错误/过期的 token
\\n有访问权限
\\n用户拥有的权限 sys:user:query
public class UserController {\\n\\n @Operation(summary = \\"获取用户列表\\")\\n @GetMapping\\n @PreAuthorize(\\"hasAuthority(\'sys:user:query\')\\") // 需要 sys:user:query 权限\\n public List<SysUser> listUsers() {\\n return userService.list();\\n }\\n \\n}\\n
\\n无访问权限
\\n用户没有拥有的权限 sys:user:info
public class UserController {\\n\\n @Operation(summary = \\"获取用户详情\\")\\n @GetMapping(\\"/{id}\\")\\n @PreAuthorize(\\"hasAuthority(\'sys:user:info\')\\") // 需要 sys:user:info 权限\\n public Result<SysUser> getUserById(\\n @Parameter(description = \\"用户ID\\") @PathVariable Long id\\n ) {\\n SysUser user = userService.getById(id);\\n } \\n}\\n
\\n通过本文,您将了解企业级后端开发的核心技能和项目从搭建到部署的完整流程。如有兴趣,欢迎访问开源项目:gitee.com/youlaiorg,关注公众号【有来技术】,或添加微信(haoxianrui)参与开源交流。
","description":"🚀 作者主页: 有来技术 🔥 开源项目: youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template\\n\\n🌺 仓库主页: GitCode︱ Gitee ︱ Github\\n\\n💖 欢迎点赞 👍 收藏 ⭐评论 📝 如有错误敬请纠正!\\n\\n前言\\n\\n本文基于 Java 和 Spring Boot 3,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQL 和 Redis,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制…","guid":"https://juejin.cn/post/7488609303065231386","author":"有来技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T03:16:43.346Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f46f9ba177fd480db428741520b25dea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=1nt1%2FNfrh40zIJC%2Bz8GTgrWV03E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89170f584c5f4603a977ed342f5a3b9c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=ejRY3Q1Q%2B0ba%2FxqA7jFKDl%2F8wiY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f27b369ca5a54846a18b623fbbd514c6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=xzeSE9Ii4f3%2Fhnqd%2BmRanYlCg8E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/36a9fdb6b6c44a1886acbd4402384e8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=PQSIu8OeAAXIPNf3jetgXb06ccw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/304b81c9c97d4c2c8a3caac689803e05~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=9WGOEZKP%2FWyyjM9oshbv1yEpDIw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/42c1d41cd0d84ca9abb07d5722b0f30f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=7xZgeSarqvgM4uMzMg%2BbVTh3X10%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/86997eb9a0d248e180684af85257a6af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=wH1hFbd1uTyrQTCSeeE699btTng%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/480d9475bc8945edb03de0931dbe969b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=onhcHgI6iFGT4yVtsYPvshZ5khU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa7e9ab709b1430fa9c684218379aa3e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=kCdkjjP9Svlvbwa2WDNY1N2ulYo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d7070a31f414d989a2408cb549b6bb1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Y9PMRtGSKiH9KzxakfFkCffk%2FaQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6389be9bdb1a48fc965a8832da39257e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=zezlt9ElVlhrq9%2Bi8nFW%2FqqcHBM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/98de7cf9ae504650974ea0296982978d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=TJ50WbgSjdTBPWQgJzh3uRtAb8s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e4f442b61e234055a3a87c075429a518~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=7P5BDAdN%2FVBGw1d0NbQyMNwmMqs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6c70a64c3f1a43a7a6ba44b97de20551~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Zdd%2FRFyBd6RRDXt9NncVOAB2Dp4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b18d9f692259447f9472550417a15140~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=rC%2B6y2cMTco8TM5kcMbUPV9hxHE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4f65c28de74d45c5a62318c3ea34b902~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Hl2h0yZxB0BNoHTw3x3ZngohDFA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d4e6685a90a432d9559833eb4054b98~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=3gG8bSXvXPp%2BTRgudrwh%2BWWWZDU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c0a1b85d971e4f6098d00acab10070ba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=ddm60tAir93d5OBjYpW1OUWrsCs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/470150d910e7459d84729ae996c6263e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=QV7e344%2BtlNcU9NJYp8Clqm6akM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd888631459e466c9314e6a5a3da8710~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Db7ZoRCO%2Bdkrf51p4nPx0JzQY14%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/51bedf0cd7cf401684c2767d16eaa3a8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=AT3RzjfkrgwAcLm8f%2Fy84f8oKZY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5e1ceb18d7804257a8b4ef54acec5fad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Zcbs3JRWTwjx2ZbumZ4nUQGw6U0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/097c9fe012534a2188570be319b79c9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=4qN%2BHoy17C8F7uqDNRpcDJnXhBY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9f55052095914944bf740c0171374b65~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=%2BnUM64HemtnqYPbNVdlYKzTymbk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0afe76489d044571916769ee1a3f06ad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=lijQHtXgsguY2GWZSB3Ya9d545s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9767d08b071b4485b3b3c3be2fbdbc10~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=EaQ4zGoWOn197d726U1LjgybJkw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/464f2166e9e34e888f4db03772288ffe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=1OboWEzENJqK8ABlo1nT86skBL8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a239dad777ba49e59e9fc9ff6368cc51~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=zO2dB69vsedYVIJCDbESZF%2BWh0g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5a8b7504c21a4ab292a0b9fbb19319de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=C9T4LGayGmMxOWnz%2BHRE1pZDFAU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5e43c6fc841c44ec9f299bc6af9eed1c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=UgTddDjnhcPeqaz%2FJshlujpW4mY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/afc5dc394d1548418315c1a020e67833~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=1z0LEWy1jGVFp1EFrbcpIR6SKxw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d5377e4e776c49498cfc54b98c3d893c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=i0YAeBmmdGuwTWmf3MONRxZ2NDE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/17fba374300d4efeb840f9f0846eeb32~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=0xWsGwXj9WfogQAQGBxkorXpf1c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/32379f80996747a095d6aed441599b24~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=v6wZIKZdmKm5YdImlchkIeyzTwk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91e89367e3ee42f5b1afbb41dff427c3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=9IGFcV6gnjuqIEq9sglODeLe%2BYs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aeac618434d5488c9287c8a0aa5bc031~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=NfF8jrflbRZKzI86B14FBu61MWw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f567ec3226f44db1a4381d1b5bdb4992~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=zjlfsCdj053%2B99vMtwyUmwc6xJk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/550d376ce8d443edafbf0e201f2e2cca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=d2uaXcrXNL444hlu9E%2FxsDYp0kE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/77befdbcd6684d8c9b06ff5ab0198551~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=2%2B%2FadCjCYDAtlq%2F8q58K8zBf7fg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5648804b46cc496b9335f3dcc48de2f6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=DbyQoS%2FSWzcumWbc6c7dDHUDA%2BY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/82bf9098b5da43b3ba89ce7e935da9cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=A2bE4PRzwFq5VB97XuFkm0GkUeY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a5cf6fcf466d4905846beb973ccee4b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=4rCPwdlpdgzCKUrWZmHBqiVV5Rs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1b978fa835ec406183b483632ffd809d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=wKIrBdw3v5Ywa6NKa8MZSp87UHk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/45a4ad27049b4693bb5a253529743d53~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=n6HdP79pWCRSRNa3qeMvydsieqw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d70085174cae4238ab8b9509993fa0e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=J%2FWIAV6qTlXZr8ZfD33GM36CDaM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/11258e4ac3b64a3aac7b6128e03d3235~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=p2DrCfgC8wFpKZMLmFRzs%2FrDtv8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/103ffdd1254e4244be8fedd8db06a2c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Hz2nRCaioeWCRAfj7VTpRPJOzb0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d2a39046b014a9ab98aefece1268214~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=Ji0szyBpyD7ZbvNmdesS22xZCOg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ea5464823d7243409f9d291c10c7957a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyJ5p2l5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744859548&x-signature=d7%2FbYIuBWf%2FW2DbKvvI4UL3cfPs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Spring Boot","Java"],"attachments":null,"extra":null,"language":null},{"title":"因为不知道条件注解@Conditional,错失15K的Offer!","url":"https://juejin.cn/post/7488605335058972722","content":"前两天刷到大V程序员鱼皮视频面试现场,在直播面试的时候问到:怎么保证开发的SDK的时候,部分Bean
的实例化根据配置项实例化,没有配置就不实例化?
候选人支支吾吾半天,说到拦截器、过滤器等,就是没有条件注解,硬是逼着鱼皮自己说了。自然也就失去了这份Offer。
\\n其实这个条件注解就是@Conditional
,它是 Spring4.0
版本框架的一个核心注解,专门用于 根据条件动态注册 Bean
。我们今天来了解一下这个注解。
假设我们需要按照不同的环境初始化不同的Bean
,Windows下创建Windows相关的Bean
,Linux下创建Linux相关的Bean
。
\\n\\nWindows下的Bean
\\n
public class WindowBean {\\n\\n public WindowBean() {\\n System.out.println(\\"WindowBean 构造器执行完成\\");\\n }\\n}\\n
\\n\\n\\nLinux 下的Bean
\\n
public class LinuxBean {\\n\\n public LinuxBean() {\\n System.out.println(\\"LinuxBean 构造器执行完成\\");\\n }\\n}\\n
\\n@Configuration\\npublic class BeanConfig {\\n\\n @Bean\\n public WindowBean windowBean() {\\n return new WindowBean();\\n }\\n\\n @Bean\\n public LinuxBean linuxBean() {\\n return new LinuxBean();\\n }\\n}\\n
\\n目前我们没有做任何的关于条件注解的配置,项目启动之后会这两个Bean都会被实例化。如图:
\\n根据环境变量os.name
匹配。
public class WindowsCondition implements Condition {\\n @Override\\n public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {\\n String osName = context.getEnvironment().getProperty(\\"os.name\\");\\n System.out.println(\\"WindowsCondition osName: \\" + osName);\\n return \\"win\\".equals(osName);\\n }\\n}\\n\\npublic class LinuxCondition implements Condition {\\n @Override\\n public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {\\n String osName = context.getEnvironment().getProperty(\\"os.name\\");\\n System.out.println(\\"LinuxCondition osName: \\" + osName);\\n return \\"linux\\".equals(osName);\\n }\\n}\\n
\\nWindowBean
在匹配WindowsCondition
逻辑的时候,实例化Bean
。LinuxBean
在匹配LinuxCondition
逻辑的时候,才会实例化。
@Configuration\\npublic class BeanConfig {\\n\\n @Bean\\n @Conditional(WindowsCondition.class)\\n public WindowBean windowBean() {\\n return new WindowBean();\\n }\\n\\n @Bean\\n @Conditional(LinuxCondition.class)\\n public LinuxBean linuxBean() {\\n return new LinuxBean();\\n }\\n}\\n
\\n启动命令上,增加os.name=win
的配置:
因为使用的是注解,我们就直接看注解的上下文,以此为入口:
\\norg.springframework.context.annotation.AnnotationConfigApplicationContext
关键代码块:
\\n可以看到,在shouldSkip()
方法中,首先会判断类或方法上是否标注了@Conditional注解,如果没有标注@Conditional注解,则直接返回false。
此时,调用的doRegisterBean()
方法根据shouldSkip()
的返回,决定要不要把对应的Bean会被创建并注入到IOC容器中。
自此,条件注解的内幕也就被了。
\\n其实我们在平时使用的时候,往往不会直接去用@Conditional
注解,反而经常会使用其扩展的注解,如下:
关注我的公众号获取首发内容:【编程朝花夕拾】
","description":"01 引言 前两天刷到大V程序员鱼皮视频面试现场,在直播面试的时候问到:怎么保证开发的SDK的时候,部分Bean 的实例化根据配置项实例化,没有配置就不实例化?\\n\\n候选人支支吾吾半天,说到拦截器、过滤器等,就是没有条件注解,硬是逼着鱼皮自己说了。自然也就失去了这份Offer。\\n\\n其实这个条件注解就是@Conditional ,它是 Spring4.0版本框架的一个核心注解,专门用于 根据条件动态注册 Bean。我们今天来了解一下这个注解。\\n\\n02 案例\\n\\n假设我们需要按照不同的环境初始化不同的Bean,Windows下创建Windows相关的Bean,Lin…","guid":"https://juejin.cn/post/7488605335058972722","author":"SimonKing","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T02:38:53.744Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b723eb5eaaee44a4a03eaa71e3e9bb99~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=X%2FcjxXjj4VgUuo6ll4ExOh7ltq4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b605183ed24948f792a2b10e94b7f74c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=MJN%2BUvmxceRaixJJ5MbcQ4Rrsu8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a944269fef6480d86b16d89318a74b8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=A%2FrbRkiJ4Ota%2FTd1BAgzq1jfaCU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c9b7125578c417d9afea97ab2a3daf1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=KTDshEstbMP63JPxQWwDLfBdT7I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/93eff6df1c4f44b5b47564436b124d95~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=DjFH%2FMN64dosEJoZq11hUFfvyTc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ffad75433a044cada6dfe2aa09f86c47~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=YWO5yE8Q6%2B3SKwnrRNnD4Ld1j30%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e0240d418afb438badab06d0643aca88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=nFAUuqfBvqU3LMC1LBvo7uj6XEM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a5036dc5058743a59bd1695d5f97b21e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=nXiBY30rr3SNzEGj3kU6w8fiF5w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a405fca48caa4aee8e6a72f196fe24aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744252732&x-signature=mt8bojI8PEoszrRoUebEZEOydU0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"拿到Offer,租房怎么办?看我用高德MCP+腾讯云MCP,帮你分分钟搞定!","url":"https://juejin.cn/post/7488599657125052416","content":"在线地址:mcp.edgeone.app/share/O-Fz2…__
\\n在面对“拿到Offer后如何快速找到合适的租房地点”这一问题时,我的整体思路是利用现有的技术工具和资源,通过高效的资源整合和自动化流程,快速筛选和展示合适的房源信息。具体思路如下:
\\n1. 利用高德地图MCP快速定位与筛选房源
\\n核心目标:快速找到公司周边的房源,并根据租金、房型、周边设施等条件进行筛选。
\\n实现方法:
\\n2. 使用腾讯云MCP搭建房源信息展示页面
\\n核心目标:将筛选出的房源信息以可视化的方式展示出来,方便用户查看和分享。
\\n实现方法:
\\n3. 整合MCP服务,实现自动化流程
\\n核心目标:通过整合高德地图MCP和腾讯云MCP,实现从房源筛选到信息展示的自动化流程。
\\n实现方法:
\\n4. 提升用户体验和效率
\\n核心目标:通过技术手段提升用户在租房过程中的体验和效率。
\\n实现方法:
\\n先打开idoubi开发的MCP Servers:mcp.so/,找到高德地图MCP Server
\\n点击进去后,切换到\\"Content\\"标签,复制接入配置代码:
\\n{\\n \\"mcpServers\\": {\\n \\"amap-maps\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\n \\"-y\\",\\n \\"@amap/amap-maps-mcp-server\\"\\n ],\\n \\"env\\": {\\n \\"AMAP_MAPS_API_KEY\\": \\"您在高德官网上申请的key\\"\\n }\\n }\\n }\\n}\\n\\n
\\n然后打开Cursor,点击\\"右上角的齿轮\\",然后找到MCP,点击“Add new global MCP server”,把刚才复制的代码粘贴到左侧窗口,保存。
\\n接下来重要的是申请高德地图Key。在MCP.so的高德地图MCP Content页面有个\\"复制key\\"链接,点击后会有详细的申请流程:
\\n申请完Key后,填入Cursor的MCP配置界面,ctrl+s保存,然后点击刷新即可。
\\n如果amap-maps前面显示绿色灯,恭喜你,配置完成了!搞定这一步,你已经超过60%的人了,因为最难的其实就是配置MCP。
\\n这里为什么选择腾讯云MCP呢?
\\nEdgeOne Pages MCP Server 介绍:EdgeOne Pages Deploy MCP 是一项专用服务,能够将 HTML 内容快速部署到 EdgeOne Pages 并生成公开访问链接。这使您能够立即预览和分享 AI 生成的网页内容。
\\n打开Cursor,点击\\"右上角的齿轮\\",然后找到MCP,点击“Add new global MCP server”,把刚才复制的代码粘贴到左侧窗口,保存。
\\n加入以下以下 JSON 配置:
\\n{\\n \\"mcpServers\\": {\\n \\"edgeone-pages-mcp-server\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\"edgeone-pages-mcp\\"]\\n }\\n }\\n}\\n
\\n如果edgeone-pages-mcp-server前面显示绿色灯,恭喜你,配置完成腾讯云MCP了。
\\nPages MCP Server 利用无服务器边缘计算能力和 KV 存储,通过 API 接收 HTML 内容,即可自动生成即时生效的公共访问链接,实现秒级静态页面部署并内置错误处理机制。
\\n到目前为止,MCP Server 主要在本地机器上运行,依赖特定运行时环境(如 Node.js、Python 或 Docker),限制了可落地的应用场景,导致受众面较为狭窄。但随着业界对远程 MCP Server 协议的支持,使其能够摆脱环境限制,变得更加通用,为更广泛的大模型用户群体打开了大门。 目前 MCP 社区正在推动远程 Server 重要技术升级,未来将迎来更丰富的使用场景和更便捷的用户体验。
\\nMCP 技术趋势与 Pages Functions 的边缘无服务器架构高度契合,其在性能、可扩展性和易用性上的优势,使开发者无需管理基础设施即可享受全球边缘网络的便利。我们将持续跟进业界动态,结合社区技术演进方向,通过标准化协议支持进一步拓展开发场景应用边界,不断增强 MCP 相关能力,助力开发者提升效能与开发体验。
\\n将两个MCP一起联合,代码如下:
\\n{\\n \\"mcpServers\\": {\\n \\"amap-maps\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\n \\"-y\\",\\n \\"@amap/amap-maps-mcp-server\\"\\n ],\\n \\"env\\": {\\n \\"AMAP_MAPS_API_KEY\\": \\"您在高德官网上申请的key\\"\\n }\\n },\\n \\"edgeone-pages-mcp-server\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\"edgeone-pages-mcp\\"]\\n }\\n }\\n}\\n\\n
\\n可以看到都已经是绿色了,表示配置成功。
\\n配置好后,我直接在Cursor里输入:
\\n我刚刚拿到offer,在杭州的算力小镇,帮我推荐几个我可以住宿小区的地方,通行时间两个小时以内,并给出小区的具体信息,包括位置,交通,周边设施,房源情况,租金。\\n
\\n使用Claude 3.7 sonnet,它立刻开始调用高德地图MCP的多个工具来解决我的问题。很快,就得出了结果!
\\n请生成一个包含上面的推荐内容的 HTML 页面,用好看的图像化表达出来。\\n\\n页面应使用 Bootstrap 5 框架进行布局和样式设计,并包含以下几个部分:\\n\\n1. 头部(Header):\\n * 包含一个标题,显示“杭州算力小镇住宿小区推荐”。\\n * 包含一个副标题,显示“祝贺您拿到offer,为您在杭州的算力小镇,推荐性价比超高的住宿小区”。\\n * 包含一个更新时间,显示“更新时间: (自动获取今天时间,如:2025年4月1日)”。\\n\\n2. 地图(Map):\\n * 使用高德地图嵌入,显示以下推荐住宿小区的位置。\\n * 地图应具有 100% 的宽度和 400px 的高度。\\n\\n3. 住宿小区列表(Cafe List):\\n * 以卡片形式展示住宿小区信息,每行显示 4 个住宿小区。\\n * 住宿小区信息应包括:\\n * 位置\\n * 评分(使用 Font Awesome 星星图标)\\n * 交通\\n * 周边设施\\n * 人均消费\\n * 房源情况\\n * 租金\\n * 特色(例如:停车场、免费 WiFi),使用 Font Awesome 图标。\\n * 使用 Unsplash API 获取住宿小区的图片,每张图片的高度为 200px。\\n\\n4. 交通分析(Travel Info):\\n * 提供从住宿小区到杭州算力小镇的交通建议,包括预计车程时间。\\n\\n5. 页脚(Footer):\\n * 包含版权信息,显示“© 2025 算力小镇住宿推荐 | 数据来源: 高德地图 | 制作:LucianaiB”。\\n\\n使用 CSS 自定义样式,包括:\\n\\n* 主色调(primary-color):#6f4e37\\n* 辅助色调(secondary-color):#f5f5dc\\n* 强调色调(accent-color):#d4a76a\\n* 卡片hover效果:鼠标悬停时卡片向上移动并增加阴影\\n\\n确保页面具有响应式布局,可以在不同设备上正常显示。\\n
\\n最后的结果:
\\n接下来重要的一步是部署到公网,这里就是利用EdgeOne Pages MCP。
\\n我们直接对Cursor说:
\\n将代码部署到 EdgeOne Pages 并生成公开访问链接\\n
\\n我们直接访问网址:杭州算力小镇住宿小区推荐
\\n下面是来自官网的介绍:
\\n\\n\\nModel Context Protocol (MCP) 是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
\\n
\\n\\n在我看来:MCP就像是一座桥梁,连接了AI与外部世界的丰富资源,让AI能够像调用API一样无缝对接各种数据和服务,从而实现更强大的功能和更广泛的应用。
\\n
MCP(Model Context Protocol)的出现,标志着AI应用进入了一个全新阶段。它不再是简单的文本对话工具,而是能够与各种外部系统无缝连接的智能助手。通过MCP,AI能够更好地服务于用户,为人们的生活和工作带来更多的便利和价值。
\\n通过这次尝试,我深刻体会到了技术的力量,尤其是高德地图MCP和腾讯云MCP的强大功能。当我拿到Offer,面对租房这件“人生大事”时,我并没有感到手足无措,反而因为这两个工具的助力,轻松地找到了心仪的住处。
\\n高德地图MCP的地理信息服务简直是我的“租房小助手”。输入公司地址后,周边的房源信息一目了然,租金、房型、周边设施等筛选条件让我能迅速锁定目标。而且,它的导航功能还帮我规划好了实地看房的路线,让我节省了不少时间和精力。
\\n腾讯云MCP的云开发功能同样让我惊喜。我轻松搭建了一个房源信息展示页面,把筛选出的房源信息、图片、位置等都上传了上去,还用HTML和CSS进行了美化,让页面看起来既美观又实用。更让我满意的是,我还分享了一些租房小技巧,希望能帮到更多像我一样租房的小伙伴。
\\n最让我觉得厉害的是,这两个MCP服务还能整合在一起,实现自动化流程。我只需要在Cursor里简单配置一下,然后输入指令,就能快速完成房源筛选和页面生成。最后,通过腾讯云的EdgeOne Pages服务,我把页面部署到公网,方便随时查看和分享。
\\n这次经历让我对MCP(Model Context Protocol)有了更深刻的理解。它不仅仅是一个协议,更是AI与外部资源交互的桥梁。它打破了数据孤岛,让AI能够访问和整合更多数据;它提升了AI的实用性和灵活性,让AI能更好地服务于我们的生活;它还降低了开发门槛,让开发者能更便捷地构建和部署AI应用。
\\n这次租房的经历,对我来说不仅是一次生活中的小挑战,更是一次技术探索的旅程。高德地图MCP和腾讯云MCP的结合,让我感受到了技术带来的便利和高效。我相信,随着技术的不断发展,未来的生活一定会更加美好。
\\nmcp.json
\\n{\\n \\"mcpServers\\": {\\n \\"amap-maps\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\n \\"-y\\",\\n \\"@amap/amap-maps-mcp-server\\"\\n ],\\n \\"env\\": {\\n \\"AMAP_MAPS_API_KEY\\": \\"fdda8428fc9485f355b24b1c76f6f147\\"\\n }\\n },\\n \\"edgeone-pages-mcp-server\\": {\\n \\"command\\": \\"npx\\",\\n \\"args\\": [\\"edgeone-pages-mcp\\"]\\n }\\n }\\n}\\n\\n
\\nindex.html
\\n<!DOCTYPE html>\\n<html lang=\\"zh-CN\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>杭州算力小镇住宿小区推荐</title>\\n <link href=\\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\\" rel=\\"stylesheet\\">\\n <link href=\\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css\\" rel=\\"stylesheet\\">\\n <style>\\n :root {\\n --primary-color: #4a90a7;\\n --secondary-color: #e8f4f8;\\n --accent-color: #7fb9c9;\\n }\\n\\n body {\\n font-family: \'Helvetica Neue\', Arial, sans-serif;\\n background-color: #f8f9fa;\\n color: #333;\\n line-height: 1.6;\\n }\\n\\n .header {\\n background: linear-gradient(135deg, var(--primary-color), #5dacbf);\\n color: white;\\n padding: 2rem 0;\\n margin-bottom: 2rem;\\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\\n }\\n\\n .header h1 {\\n font-weight: 700;\\n margin-bottom: 0.5rem;\\n }\\n\\n .header p {\\n opacity: 0.9;\\n max-width: 700px;\\n margin: 0 auto;\\n }\\n\\n .map-wrapper {\\n position: relative;\\n width: 100%;\\n height: 600px;\\n background: #fff;\\n border-radius: 12px;\\n overflow: hidden;\\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);\\n margin-bottom: 2rem;\\n }\\n\\n .search-box {\\n position: absolute;\\n top: 20px;\\n left: 20px;\\n width: 350px;\\n z-index: 100;\\n background: white;\\n border-radius: 8px;\\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\\n }\\n\\n .search-input {\\n width: 100%;\\n padding: 12px 15px;\\n border: none;\\n border-radius: 8px;\\n font-size: 14px;\\n background-color: #fff;\\n }\\n\\n .search-input:focus {\\n outline: none;\\n box-shadow: 0 0 0 2px var(--accent-color);\\n }\\n\\n .poi-list {\\n background: white;\\n max-height: 400px;\\n overflow-y: auto;\\n border-radius: 0 0 8px 8px;\\n }\\n\\n .poi-item {\\n padding: 15px;\\n border-bottom: 1px solid #eee;\\n cursor: pointer;\\n transition: background-color 0.2s ease;\\n }\\n\\n .poi-item:hover {\\n background: #f8f9fa;\\n }\\n\\n .poi-title {\\n font-size: 14px;\\n color: #333;\\n margin-bottom: 5px;\\n font-weight: 500;\\n }\\n\\n .poi-address {\\n font-size: 12px;\\n color: #666;\\n }\\n\\n .map-controls {\\n position: absolute;\\n top: 20px;\\n right: 20px;\\n z-index: 100;\\n display: flex;\\n flex-direction: column;\\n gap: 8px;\\n }\\n\\n .map-control-item {\\n background: white;\\n border: none;\\n width: 40px;\\n height: 40px;\\n border-radius: 8px;\\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\\n cursor: pointer;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n transition: all 0.2s ease;\\n }\\n\\n .map-control-item:hover {\\n background: #f8f9fa;\\n transform: translateY(-2px);\\n }\\n\\n .map-control-item i {\\n color: var(--primary-color);\\n font-size: 16px;\\n }\\n\\n .section-title {\\n color: var(--primary-color);\\n margin-bottom: 1.5rem;\\n padding-bottom: 0.5rem;\\n border-bottom: 2px solid var(--accent-color);\\n display: inline-block;\\n }\\n\\n .card {\\n border-radius: 12px;\\n overflow: hidden;\\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);\\n transition: transform 0.3s ease, box-shadow 0.3s ease;\\n margin-bottom: 2rem;\\n border: none;\\n }\\n\\n .card:hover {\\n transform: translateY(-5px);\\n box-shadow: 0 12px 20px rgba(0, 0, 0, 0.15);\\n }\\n\\n .card-img-top {\\n height: 200px;\\n object-fit: cover;\\n }\\n\\n .card-body {\\n padding: 1.5rem;\\n }\\n\\n .card-title {\\n color: var(--primary-color);\\n font-weight: 700;\\n margin-bottom: 0.5rem;\\n font-size: 1.4rem;\\n }\\n\\n .rating {\\n color: #ffc107;\\n margin-bottom: 1rem;\\n }\\n\\n .feature-icon {\\n color: var(--primary-color);\\n width: 20px;\\n margin-right: 8px;\\n }\\n\\n .badge {\\n padding: 0.5rem 1rem;\\n border-radius: 50px;\\n margin-right: 0.5rem;\\n margin-bottom: 0.5rem;\\n font-weight: 500;\\n }\\n\\n .travel-info {\\n background-color: white;\\n border-radius: 12px;\\n padding: 2rem;\\n margin-bottom: 2rem;\\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);\\n }\\n\\n footer {\\n background: linear-gradient(135deg, var(--primary-color), #5dacbf);\\n color: white;\\n padding: 1.5rem 0;\\n margin-top: 3rem;\\n }\\n\\n footer p {\\n margin-bottom: 0;\\n opacity: 0.9;\\n }\\n\\n @media (max-width: 768px) {\\n .header {\\n padding: 1.5rem 0;\\n }\\n\\n .search-box {\\n width: calc(100% - 40px);\\n }\\n\\n .map-wrapper {\\n height: 400px;\\n }\\n }\\n </style>\\n</head>\\n<body>\\n <!-- Header --\x3e\\n <header class=\\"header text-center\\">\\n <div class=\\"container\\">\\n <h1>杭州算力小镇住宿小区推荐</h1>\\n <p class=\\"lead\\">祝贺您拿到offer,为您在杭州的算力小镇,推荐性价比超高的住宿小区</p>\\n <p class=\\"text-light\\" id=\\"update-time\\"></p>\\n </div>\\n </header>\\n\\n <!-- Map Section --\x3e\\n <div class=\\"container\\">\\n <div class=\\"map-wrapper\\">\\n <!-- 搜索框 --\x3e\\n <div class=\\"search-box\\">\\n <input type=\\"text\\" class=\\"search-input\\" id=\\"search-input\\" placeholder=\\"搜索位置、公交站、地铁站\\">\\n <div class=\\"poi-list\\" id=\\"poi-list\\">\\n <div class=\\"poi-item\\" onclick=\\"focusLocation(0)\\">\\n <div class=\\"poi-title\\">1. 算力小镇</div>\\n <div class=\\"poi-address\\">您的工作地点</div>\\n </div>\\n <div class=\\"poi-item\\" onclick=\\"focusLocation(1)\\">\\n <div class=\\"poi-title\\">2. 复地·连城国际花园</div>\\n <div class=\\"poi-address\\">在杭海路1636号,距离乔司南地铁站约1公里</div>\\n </div>\\n <div class=\\"poi-item\\" onclick=\\"focusLocation(2)\\">\\n <div class=\\"poi-title\\">3. 佳兆业·君汇上品苑</div>\\n <div class=\\"poi-address\\">在乔司街道石塘路北侧,环境优美</div>\\n </div>\\n <div class=\\"poi-item\\" onclick=\\"focusLocation(3)\\">\\n <div class=\\"poi-title\\">4. 敏捷·源著天樾府</div>\\n <div class=\\"poi-address\\">在石塘东路,近乔司南地铁站</div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- 地图控件 --\x3e\\n <div class=\\"map-controls\\">\\n <button class=\\"map-control-item\\" onclick=\\"toggleSatellite()\\"><i class=\\"fas fa-satellite\\"></i></button>\\n <button class=\\"map-control-item\\" onclick=\\"toggleTraffic()\\"><i class=\\"fas fa-car\\"></i></button>\\n <button class=\\"map-control-item\\" onclick=\\"showNearbyFacilities()\\"><i class=\\"fas fa-compass\\"></i></button>\\n <button class=\\"map-control-item\\" onclick=\\"map.setFitView()\\"><i class=\\"fas fa-expand\\"></i></button>\\n </div>\\n\\n <!-- 地图容器 --\x3e\\n <div id=\\"map\\" style=\\"width: 100%; height: 100%;\\">\\n <iframe width=\\"100%\\" height=\\"100%\\"\\n src=\\"https://uri.amap.com/marker?markers=120.293468,30.333673,算力小镇|120.289468,30.329673,复地·连城国际花园|120.295468,30.335673,佳兆业·君汇上品苑|120.291468,30.331673,敏捷·源著天樾府&src=mypage&callnative=1\\"\\n style=\\"border: none;\\"></iframe>\\n </div>\\n </div>\\n </div>\\n\\n <!-- Residential Areas List --\x3e\\n <div class=\\"container\\">\\n <div class=\\"row\\">\\n <!-- 复地·连城国际花园 --\x3e\\n <div class=\\"col-lg-4 col-md-6\\">\\n <div class=\\"card\\">\\n <img src=\\"https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=800&q=80\\" class=\\"card-img-top\\" alt=\\"复地·连城国际花园\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\">复地·连城国际花园</h5>\\n <div class=\\"rating mb-2\\">\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star-half-alt\\"></i>\\n </div>\\n <p class=\\"card-text\\">\\n <i class=\\"fas fa-map-marker-alt feature-icon\\"></i> 杭海路1636号<br>\\n <i class=\\"fas fa-subway feature-icon\\"></i> 距离乔司南地铁站约1公里<br>\\n <i class=\\"fas fa-building feature-icon\\"></i> 大型成熟社区<br>\\n <i class=\\"fas fa-money-bill-wave feature-icon\\"></i> 租金:3500-6000元/月\\n </p>\\n <div class=\\"facilities mt-3\\">\\n <span class=\\"badge bg-primary\\"><i class=\\"fas fa-shopping-cart\\"></i> 永辉超市</span>\\n <span class=\\"badge bg-success\\"><i class=\\"fas fa-school\\"></i> 乔司中学</span>\\n <span class=\\"badge bg-info\\"><i class=\\"fas fa-hospital\\"></i> 社区医院</span>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- 佳兆业·君汇上品苑 --\x3e\\n <div class=\\"col-lg-4 col-md-6\\">\\n <div class=\\"card\\">\\n <img src=\\"https://images.unsplash.com/photo-1574362848149-11496d93a7c7?w=800&q=80\\" class=\\"card-img-top\\" alt=\\"佳兆业·君汇上品苑\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\">佳兆业·君汇上品苑</h5>\\n <div class=\\"rating mb-2\\">\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"far fa-star\\"></i>\\n </div>\\n <p class=\\"card-text\\">\\n <i class=\\"fas fa-map-marker-alt feature-icon\\"></i> 乔司街道石塘路北侧<br>\\n <i class=\\"fas fa-car feature-icon\\"></i> 距离算力小镇约2公里<br>\\n <i class=\\"fas fa-tree feature-icon\\"></i> 环境优美,物业完善<br>\\n <i class=\\"fas fa-money-bill-wave feature-icon\\"></i> 租金:3000-5500元/月\\n </p>\\n <div class=\\"facilities mt-3\\">\\n <span class=\\"badge bg-primary\\"><i class=\\"fas fa-shopping-bag\\"></i> 商业中心</span>\\n <span class=\\"badge bg-success\\"><i class=\\"fas fa-tree\\"></i> 高绿化率</span>\\n <span class=\\"badge bg-info\\"><i class=\\"fas fa-parking\\"></i> 充足车位</span>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- 敏捷·源著天樾府 --\x3e\\n <div class=\\"col-lg-4 col-md-6\\">\\n <div class=\\"card\\">\\n <img src=\\"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&q=80\\" class=\\"card-img-top\\" alt=\\"敏捷·源著天樾府\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\">敏捷·源著天樾府</h5>\\n <div class=\\"rating mb-2\\">\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n <i class=\\"fas fa-star\\"></i>\\n </div>\\n <p class=\\"card-text\\">\\n <i class=\\"fas fa-map-marker-alt feature-icon\\"></i> 石塘东路(近乔司南地铁站)<br>\\n <i class=\\"fas fa-subway feature-icon\\"></i> 步行5分钟到地铁站<br>\\n <i class=\\"fas fa-building feature-icon\\"></i> 新建高档住宅区<br>\\n <i class=\\"fas fa-money-bill-wave feature-icon\\"></i> 租金:3800-6500元/月\\n </p>\\n <div class=\\"facilities mt-3\\">\\n <span class=\\"badge bg-primary\\"><i class=\\"fas fa-shopping-cart\\"></i> 万达广场</span>\\n <span class=\\"badge bg-success\\"><i class=\\"fas fa-graduation-cap\\"></i> 临平一中</span>\\n <span class=\\"badge bg-info\\"><i class=\\"fas fa-hospital\\"></i> 临平一院</span>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- Travel Info --\x3e\\n <div class=\\"container mt-5\\">\\n <h3 class=\\"text-center mb-4\\">交通分析</h3>\\n <div class=\\"row\\">\\n <div class=\\"col-md-4\\">\\n <div class=\\"card\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\"><i class=\\"fas fa-car feature-icon\\"></i> 驾车</h5>\\n <p class=\\"card-text\\">平均通勤时间:6-8分钟<br>距离:约1.6公里</p>\\n </div>\\n </div>\\n </div>\\n <div class=\\"col-md-4\\">\\n <div class=\\"card\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\"><i class=\\"fas fa-bus feature-icon\\"></i> 公交</h5>\\n <p class=\\"card-text\\">平均通勤时间:25-30分钟<br>包含步行约600米</p>\\n </div>\\n </div>\\n </div>\\n <div class=\\"col-md-4\\">\\n <div class=\\"card\\">\\n <div class=\\"card-body\\">\\n <h5 class=\\"card-title\\"><i class=\\"fas fa-walking feature-icon\\"></i> 步行</h5>\\n <p class=\\"card-text\\">步行时间:约13-15分钟<br>距离:约1公里</p>\\n </div>\\n </div>\\n </div>\\n </div>\\n </div>\\n\\n <!-- Footer --\x3e\\n <footer class=\\"text-center\\">\\n <div class=\\"container\\">\\n <p>© <span id=\\"current-year\\"></span> 算力小镇住宿推荐 | 数据来源: 高德地图 | 制作:LucianaiB</p>\\n </div>\\n </footer>\\n\\n <!-- Scripts --\x3e\\n <script src=\\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js\\"></script>\\n <script>\\n // 更新时间\\n const updateDate = new Date();\\n document.getElementById(\'update-time\').textContent = `更新时间: ${updateDate.getFullYear()}年${updateDate.getMonth() + 1}月${updateDate.getDate()}日`;\\n \\n // 更新年份\\n document.getElementById(\'current-year\').textContent = new Date().getFullYear();\\n </script>\\n</body>\\n</html> \\n
","description":"拿到Offer,租房怎么办?看我用高德MCP+腾讯云MCP,帮你分分钟搞定! 目录\\n\\n效果展示\\n\\nAI编程开发流程\\n\\n2.1需求分析\\n\\n高德MCP:快速定位与筛选房源\\n腾讯云MCP:搭建房源信息展示页面\\n\\n2.2整体思路\\n\\n给Cursor安装高德MCP\\n\\n3.1配置方法\\n3.2申请高德地图Key\\n\\n给Cursor安装腾讯云MCP\\n\\n4.1配置方法\\n4.2技术原理\\n4.3为什么使用 EdgeOne Pages?\\n\\nCoser编程\\n\\n5.1整合MCP\\n5.2直接说需求找住宿小区\\n5.3一键生成可视化网页\\n\\nMCP(大模型上下文)…","guid":"https://juejin.cn/post/7488599657125052416","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T16:30:58.131Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/64df9ad395eb4434ae6607b98346ec7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=6NRNbpWXeQK9h0jqNeouja0E9Co%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/762c7c46d3f34fbb8424c3dae08def6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=tQTXLU4kijApy0J7KW%2F4cISrCX8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0757c8e1363b411292792f9b0a01fc6f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=koQF7rMD1xBTedShG7QN6BjPYkg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/05e6266c754f4083a8e0b2cb58db3400~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=6e9Z%2FzF%2Blhe9pTDkRyyLhgXRL1c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c3ad33ad44ad4bacbdae9cd921de62c9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=w4CNFjarR08RQzk2VQbpdgYDArk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3da19090ddae473ea73b9b7080610db9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=T%2BYiy0aZsH0gBSWtNz5waMALAzY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bb195233c4ae43caaa82a42bdbcbb05e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=sCRqIrHtxq3u%2FoAm0cvt%2BC0oYJU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/248fa085158b4926bb30b0bfbe261fea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=ZX5VNZeE6WwyBadNrSMIMkiLJvA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c9e51887a1a049de9029608f19779913~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=7mvxR6NwzG2QnaiF5JT1lknxGd4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f71bd352c9d3495e997ab4813b838ac6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=yEr3b%2F7d2SPeJb1x2cmoRiSsgrk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6440d4eb00844d2a21998bd95c0a59e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=pbjIuFzRJhK6YqbJhZIxkCc7ckk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a9fe0651817b4a8a86372949e220c057~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=gYsLtZ1Tlxy%2BQoq2nNoACQm%2FHaM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c15dc7d291f84f5fa1186a6d2c9e6fdf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=BJrauYbeRxYLCVPuE3wOEbUY4sc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5285c8105fb847168460fbb3619d7bd5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=1tfrWdBUAT2cOmhnPAsYUuTWIsM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e6187f0fd3f34a9c9c3727346f93a615~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=YPa2BJIwUdrelsaDAVQuYVLCy8M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/76e04b28576a4172aaadb2cb5a4772fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=gjUZn0a2cfqvXG2WQefc4wSzp6g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d088b8e414d04221af112aa447d2cc6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=Y4B7sNX4oSQmCHvxwF2z8boij%2BU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e9bf2fbd002748c39810e1939b0de132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=WyL%2BGo06wB4fwDu0E8o6jGyOrLM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dfe01bccbe4a4cc59faa721bf3859e12~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=NFGM%2BldP1YesgKmpbxrj1y%2FoHmc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5e27afc5add44c8189c10b3fd64f8307~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=S7hoMj64LZJASnbcII5h9vaqun8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d1b2f64b878491b952361710a0de231~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1744818859&x-signature=ivpbDfjE5wN3vFgOG%2BGHn9IyJnk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Cursor","前端"],"attachments":null,"extra":null,"language":null},{"title":"程序员,你使用过灰度发布吗?","url":"https://juejin.cn/post/7488321730764603402","content":"大家好呀,我是猿java。
\\n在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。
\\n简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:
\\n要理解灰度发布,我们需要先了解一下它的基本流程:
\\n在这个过程中,关键在于如何切分流量,确保新旧版本平稳过渡。常见的切分方式包括:
\\n为了更好地理解灰度发布,接下来,我们通过一个简单的 Java示例来演示基本的灰度发布策略。假设我们有一个简单的 Web应用,有两个版本的登录接口/login/v1
和/login/v2
,我们希望将百分之十的流量引导到v2
,其余流量继续使用v1
。
我们可以通过拦截器(Interceptor)来实现流量的切分。以下是一个基于Spring Boot的简单实现:
\\nimport org.springframework.stereotype.Component;\\nimport org.springframework.web.servlet.HandlerInterceptor;\\n\\nimport javax.servlet.http.HttpServletRequest;\\nimport javax.servlet.http.HttpServletResponse;\\nimport java.util.Random;\\n\\n@Component\\npublic class GrayReleaseInterceptor implements HandlerInterceptor {\\n\\n private static final double GRAY_RELEASE_PERCENT = 0.1; // 10% 流量\\n\\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\\n String uri = request.getRequestURI();\\n if (\\"/login\\".equals(uri)) {\\n if (isGrayRelease()) {\\n // 重定向到新版本接口\\n response.sendRedirect(\\"/login/v2\\");\\n return false;\\n } else {\\n // 使用旧版本接口\\n response.sendRedirect(\\"/login/v1\\");\\n return false;\\n }\\n }\\n return true;\\n }\\n\\n private boolean isGrayRelease() {\\n Random random = new Random();\\n return random.nextDouble() < GRAY_RELEASE_PERCENT;\\n }\\n}\\n
\\n在Spring Boot中,我们需要将拦截器注册到应用中:
\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.web.servlet.config.annotation.*;\\n\\n@Configuration\\npublic class WebConfig implements WebMvcConfigurer {\\n\\n @Autowired\\n private GrayReleaseInterceptor grayReleaseInterceptor;\\n\\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n registry.addInterceptor(grayReleaseInterceptor).addPathPatterns(\\"/login\\");\\n }\\n}\\n
\\nimport org.springframework.web.bind.annotation.*;\\n\\n@RestController\\n@RequestMapping(\\"/login\\")\\npublic class LoginController {\\n\\n @GetMapping(\\"/v1\\")\\n public String loginV1(@RequestParam String username, @RequestParam String password) {\\n // 旧版本登录逻辑\\n return \\"登录成功 - v1\\";\\n }\\n\\n @GetMapping(\\"/v2\\")\\n public String loginV2(@RequestParam String username, @RequestParam String password) {\\n // 新版本登录逻辑\\n return \\"登录成功 - v2\\";\\n }\\n}\\n
\\n在上面三个步骤之后,我们就实现了登录接口地灰度发布:
\\n/login
时,拦截器会根据设定的灰度比例(10%)决定请求被重定向到/login/v1
还是/login/v2
。上述示例,我们只是一个简化的灰度发布实现,实际生产环境中,我们可能需要更精细的灰度策略,例如:
\\n在实际工作中,为什么我们要使用灰度发布?这里我们总结了几个重要的原因。
\\n每次发布新版本,尤其是功能性更新或架构调整,都会伴随着一定的风险。即使经过了充分的测试,实际生产环境中仍可能出现意想不到的问题。灰度发布通过将新版本逐步推向部分用户,可以有效降低全量发布可能带来的风险。
\\n举个例子,假设你上线了一个全新的支付功能,直接面向所有用户开放。如果这个功能存在严重 bug,可能导致大量用户无法完成支付,甚至影响公司声誉。而如果采用灰度发布,先让10%的用户体验新功能,发现问题后只需影响少部分用户,修复起来也更为迅速和容易。
\\n在传统的全量发布中,一旦发现问题,回滚到旧版本可能需要耗费大量时间和精力,尤其是在高并发系统中,数据状态的同步与恢复更是复杂。而灰度发布由于新版本只覆盖部分流量,问题定位和回滚变得更加简单和快速。
\\n比如说,你在灰度发布阶段发现新版本的某个功能在某些特定条件下会导致系统崩溃,立即可以停止向新用户推送这个版本,甚至只针对受影响的用户进行回滚操作,而不用影响全部用户的正常使用。
\\n灰度发布让你有机会在真实的生产环境中监控新版本的表现,并收集用户的反馈。这些数据对于评估新功能的实际效果至关重要,有助于做出更明智的决策。
\\n举个具体的场景,你新增了一个推荐算法,希望提升用户的点击率。在灰度发布阶段,你可以监控新算法带来的点击率变化、服务器负载情况等指标,确保新算法确实带来了预期的效果,而不是引入了新的问题。
\\n通过灰度发布,你可以在推出新功能时,逐步优化用户体验。先让一部分用户体验新功能,收集他们的使用反馈,根据反馈不断改进,最终推出一个更成熟、更符合用户需求的版本。
\\n举个例子,你开发了一项新的用户界面设计,直接全量发布可能会让一部分用户感到不适应或不满意。灰度发布允许你先让一部分用户体验新界面,收集他们的意见,进行必要的调整,再逐步扩大使用范围,确保最终发布的版本能获得更多用户的认可和喜爱。
\\n灰度发布是实现A/B测试的基础。通过将用户随机分配到不同的版本,你可以比较不同版本的表现,选择最优方案进行全面推行。这对于优化产品功能和提升用户体验具有重要意义。
\\n比如说,你想测试两个不同的推荐算法,看哪个能带来更高的转化率。通过灰度发布,将用户随机分配到使用算法A和算法B的版本,比较它们的表现,最终选择效果更好的算法进行全面部署。
\\n在一些复杂的业务场景中,全量发布可能无法满足灵活的需求,比如分阶段推出新功能、针对不同用户群体进行差异化体验等。灰度发布提供了更高的灵活性和可控性,能够更好地适应多变的业务需求。
\\n例如,你正在开发一个面向企业用户的新功能,希望先让部分高价值客户试用,收集他们的反馈后再决定是否全面推广。灰度发布让这一过程变得更加顺畅和可控。
\\n本文,我们详细地分析了灰度发布,它是一种强大而灵活的部署策略,能有效降低新版本上线带来的风险,提高系统的稳定性和用户体验。作为Java开发者,掌握灰度发布的原理和实现方法,不仅能提升我们的技术能力,还能为团队的项目成功保驾护航。
\\n对于灰度发布,如果你有更多的问题或想法,欢迎随时交流!
\\n如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
","description":"大家好呀,我是猿java。 在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。\\n\\n1. 什么是灰度发布?\\n\\n简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:\\n\\n降低风险:新版本如果存在 bug,只影响少部分用户,减少了对整体用户体验的冲击。\\n快速回滚:在小范围内发现问题,可以更快地回到旧版本。\\n收集反馈:可以在真实环…","guid":"https://juejin.cn/post/7488321730764603402","author":"猿java","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T05:59:53.402Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/944a8c4b3b4e47f4be0c64656f4c3285~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54y_amF2YQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744178393&x-signature=oG5%2BUTVvXrcHT63JlhIp33B5c6c%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","分布式"],"attachments":null,"extra":null,"language":null},{"title":"Excel百万数据如何快速导入?","url":"https://juejin.cn/post/7488246529431748645","content":"今天要讨论一个让无数人抓狂的话题:如何高效导入百万级Excel数据。
\\n去年有家公司找到我,他们的电商系统遇到一个致命问题:每天需要导入20万条商品数据,但一执行就卡死,最长耗时超过3小时。
\\n更魔幻的是,重启服务器后前功尽弃。
\\n经过半天的源码分析,我们发现了下面这些触目惊心的代码...
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\n很多小伙伴在实现Excel导入时,往往直接写出这样的代码:
\\n// 错误示例:逐行读取+逐条插入\\npublic void importExcel(File file) {\\n List<Product> list = ExcelUtils.readAll(file); // 一次加载到内存\\n for (Product product : list) {\\n productMapper.insert(product); // 逐行插入\\n }\\n}\\n
\\n这种写法会引发三大致命问题:
\\nUserModel
(如XSSFWorkbook)一次性加载整个Excel到内存使用POI的SAX模式替代DOM模式:
\\n// 正确写法:分段读取(以HSSF为例)\\nOPCPackage pkg = OPCPackage.open(file);\\nXSSFReader reader = new XSSFReader(pkg);\\nSheetIterator sheets = (SheetIterator) reader.getSheetsData();\\n\\nwhile (sheets.hasNext()) {\\n try (InputStream stream = sheets.next()) {\\n Sheet sheet = new XSSFSheet(); // 流式解析\\n RowHandler rowHandler = new RowHandler();\\n sheet.onRow(row -> rowHandler.process(row));\\n sheet.process(stream); // 不加载全量数据\\n }\\n}\\n
\\n⚠️ 避坑指南:
\\n基于MyBatis的批量插入+连接池优化:
\\n// 分页批量插入(每1000条提交一次)\\npublic void batchInsert(List<Product> list) {\\n SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);\\n ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);\\n \\n int pageSize = 1000;\\n for (int i = 0; i < list.size(); i += pageSize) {\\n List<Product> subList = list.subList(i, Math.min(i + pageSize, list.size()));\\n mapper.batchInsert(subList);\\n sqlSession.commit();\\n sqlSession.clearCache(); // 清理缓存\\n }\\n}\\n
\\n关键参数调优:
\\n# MyBatis配置\\nmybatis.executor.batch.size=1000\\n\\n# 连接池(Druid)\\nspring.datasource.druid.maxActive=50\\nspring.datasource.druid.initialSize=10\\n
\\n架构设计:
对于千万级数据,可采用分治策略:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n阶段 | 操作 | 耗时对比 |
---|---|---|
单线程 | 逐条读取+逐条插入 | 基准值100% |
批处理 | 分页读取+批量插入 | 时间降至5% |
多线程分片 | 按Sheet分片,并行处理 | 时间降至1% |
分布式分片 | 多节点协同处理(如Spring Batch集群) | 时间降至0.5% |
典型代码缺陷:
\\n// 错误:边插入边校验,可能污染数据库\\npublic void validateAndInsert(Product product) {\\n if (product.getPrice() < 0) {\\n throw new Exception(\\"价格不能为负\\");\\n }\\n productMapper.insert(product);\\n}\\n
\\n✅ 正确实践:
\\n解决方案:
\\n配置要点:
\\n// Spring Boot配置Prometheus指标\\n@Bean\\npublic MeterRegistryCustomizer<PrometheusMeterRegistry> metrics() {\\n return registry -> registry.config().meterFilter(\\n new MeterFilter() {\\n @Override\\n public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {\\n return DistributionStatisticConfig.builder()\\n .percentiles(0.5, 0.95) // 统计中位数和95分位\\n .build().merge(config);\\n }\\n }\\n );\\n}\\n
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n测试环境:
\\n方案 | 内存峰值 | 耗时 | 吞吐量 |
---|---|---|---|
传统逐条插入 | 2.5GB | 96分钟 | 173条/秒 |
分页读取+批量插入 | 500MB | 7分钟 | 2381条/秒 |
多线程分片+异步批量 | 800MB | 86秒 | 11627条/秒 |
分布式分片(3节点) | 300MB/节点 | 29秒 | 34482条/秒 |
Excel高性能导入的11条军规:
\\n如果你正在为Excel导入性能苦恼,希望这篇文章能为你的系统打开一扇新的大门。
\\n如果你有其他想了解的技术难题,欢迎在评论区留言!
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n今天要讨论一个让无数人抓狂的话题:如何高效导入百万级Excel数据。\\n\\n去年有家公司找到我,他们的电商系统遇到一个致命问题:每天需要导入20万条商品数据,但一执行就卡死,最长耗时超过3小时。\\n\\n更魔幻的是,重启服务器后前功尽弃。\\n\\n经过半天的源码分析,我们发现了下面这些触目惊心的代码...\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。\\n\\n1 为什么传统导入方案会崩盘?\\n\\n很多小伙伴在实现Excel导入时…","guid":"https://juejin.cn/post/7488246529431748645","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T03:27:41.267Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c8953452063c4674a0acf743e452eef6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1744774080&x-signature=ufMXCBvJT8N8usqmZgYiEoNdWfo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Kafka 4.0.0震撼来袭,彻底摒弃Zookeeper","url":"https://juejin.cn/post/7487863095869718565","content":"Apache Kafka 4.0.0 版本发布,带来了众多新功能和改进。该版本是第一个完全不依赖 Apache ZooKeeper 运行的主要版本,默认以 KRaft 模式运行,简化了部署和管理。此外,还引入了新的消费者组协议、提供对 Queues for Kafka 的早期访问、更新了 Java 版本要求、移除了一些旧的 API 和功能等。
\\n无 ZooKeeper 运行:
\\nApache Kafka 4.0 标志着第一个完全没有 Apache ZooKeeper 运行的主要版本。默认以 KRaft 模式运行,简化了部署和管理,降低了运营开销,增强了可扩展性并简化了管理任务。
\\n新消费者组协议:
\\nKafka 4.0 带来了 KIP-848 的全面可用性,引入了强大的新消费者组协议,显著提高再平衡性能,减少停机时间和延迟,增强了消费者组的可靠性和响应能力。
\\nQueues for Kafka:
\\n提供对 Queues for Kafka(KIP-932)的早期访问,使 Kafka 能够直接支持传统的队列语义,扩展了 Kafka 的多功能性。
\\nJava 版本要求更新:
\\nKafka 4.0 中,Kafka Clients 和 Kafka Streams 需要 Java 11,而 Kafka Brokers、Connect 和 Tools 现在需要 Java 17。
\\nKafka Streams 改进:
\\nKIP-1104 允许在 KTable 连接中从键和值中提取外键;KIP-1112 允许自定义处理器包装;KIP-1065 为 ProductionExceptionHandler 添加 “retry” 选项;KIP-1091 改进了 Kafka Streams 运算符指标。
\\nZooKeeper
依靠自己强一致性的特点,一直在分布式届延续着自己的传奇,很多软件都会选择它作为分布式集群的载体,如之前的4.0之前的Kafka、spark、hadoop、dubbo的注册中心等。但是Kafka在以强大推吐量著称,强依赖强一致性的ZooKeeper
,势必带来一定的性能瓶颈。ZooKeeper
增加了系统的复杂性,维护和管理 ZooKeeper 集群需要额外的资源和专业知识。KRaft
作为Kafka
内部的算法机制直接替代ZooKeeper
,使得部署和维护都变得简单了许多。
Raft 协议被用于分布式存储系统中,以确保数据在多个节点间的一致性和可用性。例如,分布式键值存储(如 etcd、Consul)和分布式数据库(如 TiKV)都采用了 Raft 协议。KRaft
则是用户Kafka
内部的专属协议。
新消费者组协议的核心就是增量式重平衡,不再依赖全局同步屏障,而是由组协调器(GroupCoordinator)驱动。将凭屏障的颗粒度有全局编程局部,细粒度的控制消费者。只有更改的消费者和分区变化时只会触发对应的屏障,其他不受影响。这也明显的提高处理的速度。
\\nKafka 4.0 通过引入共享组 (Share Group) 机制提供了类似队列的功能。不过,它并非真正意义上的队列,而是利用 Kafka 已有的主题(Topic)和分区(Partition)机制,结合新的消费模式和记录确认机制来实现类似队列的行为。
\\nKafka 4.0对JDK也有要求,Kafka 客户端和 Kafka Streams 需要 Java 11,Kafka Brokers、Connect工具需要 Java 17。你还在坚持着JDK你发任你发,我用JAVA8的态度么,如果这样,Kafka 4.0的新特性也将无法尝试。
\\n各种软件都在拥抱新的变化,不断地兼容新的产品。我们是不是也要去迭代自己的知识库,去学习这些新的东西,要不然就被时代遗弃了。
\\n关注我的公众号:【编程朝花夕拾】,获取首发内容。
","description":"Apache Kafka 4.0.0 版本发布,带来了众多新功能和改进。该版本是第一个完全不依赖 Apache ZooKeeper 运行的主要版本,默认以 KRaft 模式运行,简化了部署和管理。此外,还引入了新的消费者组协议、提供对 Queues for Kafka 的早期访问、更新了 Java 版本要求、移除了一些旧的 API 和功能等。 1、重要特性说明\\n\\n无 ZooKeeper 运行:\\n\\nApache Kafka 4.0 标志着第一个完全没有 Apache ZooKeeper 运行的主要版本。默认以 KRaft 模式运行,简化了部署和管理…","guid":"https://juejin.cn/post/7487863095869718565","author":"SimonKing","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T03:35:22.623Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/267a13d9babd41e3bb7cb7c1cb13d9ad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744083322&x-signature=deNNIQFVMg2pA40eW06OoHz1EGk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7d475767757e4055a76911f58a4697c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2ltb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1744083322&x-signature=pBs7lczdD5jjCyRbD43HlIM8cnw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"程序员如何避免被加班文化榨干身体?","url":"https://juejin.cn/post/7487905034783309864","content":"文章首发到公众号:月伴飞鱼,每天分享程序员职场经验!
\\n文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n大家好呀,我是飞鱼。
\\n打工人经常面对加班的困扰,为了健康,我们需要一些小技巧来抵消加班带来的伤害。
\\n大家不要觉得自己年轻/健身/身体好可以随便造,猝死是单发的,单次的程度足够,是不管你底子厚不厚的。
\\n下面是一些996打工人的防猝死指南!
\\n1、拒绝24小时待机,用番茄钟工作法(25分钟干活+5分钟放空),强行打断代码上头状态。
\\n\\n\\n❝
\\n每小时必须站起来晃悠2分钟(接水/上厕所/假装看风景),拯救僵硬的颈椎腰椎。
\\n下班后电脑开飞行模式,真有急事?让领导先打钱再说话。
\\n
2、办公室保命三件套:
\\n\\n\\n❝
\\n人体工学椅(公司不配就自己买,比治腰椎便宜)。
\\n显示器支架(屏幕顶端与视线齐平)、护腕鼠标垫(腱鞘炎发作时哭都来不及)。
\\n
3、 每天灌满2升水(设每小时喝水闹钟)。
\\n4、下午4点后别碰咖啡奶茶,心慌时改喝黄芪枸杞茶,常年熬夜的记得补维生素B族。
\\n\\n\\n❝
\\n多吃水果和蔬菜,防止便秘,大便秘结排便时增加腹压影响心脏,易诱发冠心病急性发作。
\\n
5、摸鱼式锻炼大法:
\\n\\n\\n❝
\\n开会时偷偷绷脚尖练小腿,等代码编译时深蹲5个(同事笑你就拉他一起),周末逼自己出门遛弯1小时(晒太阳防抑郁+补钙)。
\\n学会科学摸鱼,不要连续工作,工作数小时后,要短暂休息。
\\n交替式休息消除疲劳最有效,如左撇子就多活动右侧肢体,动脑多就多活动身体/肩颈。
\\n
7、带薪养生实操技巧:
\\n\\n\\n❝
\\n排需求工期多留20%缓冲时间,领导骂人时默念工资含精神补偿费,下班后物理屏蔽工作群(消息免打扰+手机扔沙发)。
\\n
8、每年必做体检项目:
\\n\\n\\n❝
\\n心脏彩超(查心律失常)、颈动脉超声(防血管堵塞)、甲状腺功能(压力大易中招)、肝功能+血糖血脂(年轻也要查!)。
\\n
9、脸皮要厚一点:
\\n\\n\\n❝
\\n避免在工作中产生过强的情绪,无论是愤怒、焦虑、还是紧张,尽量保持心情平稳舒展。
\\n
10、疲劳后健身很危险:
\\n\\n\\n❝
\\n类似事件发生过很多次,累了的最好健身方式是休息,疲劳之后立即运动,容易导致运动性碎死。
\\n
卷王们清醒点!别仗着年轻就狂肝代码、靠咖啡续命、一坐8小时不挪窝。
\\n公司离了你分分钟招新人,你垮了全家都得哭!从今天开始,把喘气设为每日首要KPI!
\\n有啥其他看法,欢迎在评论区留言讨论。
\\n\\n\\n❝
\\n想看技术文章的,可以去我的个人网站:hardyfish.top/。
\\n\\n
\\n- 目前网站的内容足够应付基础面试(
\\nP7
)了!想学AI技术的,欢迎加入我的AI学习社群!
\\n
题目描述
\\n\\n\\n❝
\\n给你一个长度为
\\nn
的整数数组nums
和 一个目标值target
。请你从
\\nnums
中选出三个整数,使它们的和与target
最接近。返回这三个数的和。
\\n
示例 1:
\\n输入:nums = [-1,2,1,-4], target = 1\\n输出:2\\n解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2)。\\n
\\n示例 2:
\\n输入:nums = [0,0,0], target = 1\\n输出:0\\n解释:与 target 最接近的和是 0(0 + 0 + 0 = 0)。\\n
\\n解题思路
\\n\\n\\n❝
\\n排序和双指针!
\\n
代码实现
\\nJava
代码:
class Solution {\\n public int threeSumClosest(int[] nums, int target) {\\n Arrays.sort(nums);\\n int ans = nums[0] + nums[1] + nums[2];\\n for(int i=0;i<nums.length;i++) {\\n int start = i+1, end = nums.length - 1;\\n while(start < end) {\\n int sum = nums[start] + nums[end] + nums[i];\\n if(Math.abs(target - sum) < Math.abs(target - ans)) {\\n ans = sum;\\n }\\n if(sum > target) {\\n end--;\\n } elseif(sum < target) {\\n start++;\\n } else {\\n return ans;\\n }\\n }\\n }\\n return ans;\\n }\\n}\\n
","description":"文章首发到公众号:月伴飞鱼,每天分享程序员职场经验! 文章内容收录到个人网站,方便阅读:hardyfish.top/\\n\\n大家好呀,我是飞鱼。\\n\\n打工人经常面对加班的困扰,为了健康,我们需要一些小技巧来抵消加班带来的伤害。\\n\\n大家不要觉得自己年轻/健身/身体好可以随便造,猝死是单发的,单次的程度足够,是不管你底子厚不厚的。\\n\\n下面是一些996打工人的防猝死指南!\\n\\n1、拒绝24小时待机,用番茄钟工作法(25分钟干活+5分钟放空),强行打断代码上头状态。\\n\\n❝\\n\\n每小时必须站起来晃悠2分钟(接水/上厕所/假装看风景),拯救僵硬的颈椎腰椎。\\n\\n下班后电脑开飞行模式,真有急事…","guid":"https://juejin.cn/post/7487905034783309864","author":"程序员飞鱼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T03:22:49.234Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"超越 Xshell!号称下一代终端神器,用完爱不释手!","url":"https://juejin.cn/post/7487903297832353832","content":"\\n\\n作为后端程序员,我们经常会使用终端工具来管理服务器,例如Xshell。今天给大家分享一款现代化的终端工具Warp,它不仅界面炫酷,而且提示非常智能,希望对大家有所帮助!
\\n
Warp是一款Rust语言编写的现代化终端工具,内置AI功能,目前在Github上已有22k+Star
。最初Warp只有MacOS版本,最近逛了下它的官网,发现它已经支持Windows和Linux系统了!
Warp具有如下特性:
\\n这个是Warp使用过程中的效果图,界面还是非常炫酷的!
\\n右上角
的设置按钮,对Warp进行设置;Text
中设置它的字体大小。这或许是一个对你有用的开源项目,mall项目是一套基于SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\n\\n\\n接下来我们来讲解下Warp的使用。
\\n
ssh {username}@{hostname_or_ip}\\n
\\n→键
直接使用命令补全功能;Tab键
来进行提示,Warp不仅会提示对应的命令,还有命令对应的说明,很全面;docker run -p 8080:8080 --name mall-admin \\\\\\n--link mysql:db \\\\\\n--link redis:redis \\\\\\n-v /etc/localtime:/etc/localtime \\\\\\n-v /mydata/app/admin/logs:/var/logs \\\\\\n-d mall/mall-admin:1.0-SNAPSHOT\\n
\\n\\n\\n可以通过
\\nAgent Mode
和AI Command
两种方式来使用Warp的AI功能。
Agent Mode
来使用AI功能,我这里提问Warp如何连接远程的Linux服务器
,可以切换使用DeepSeek大模型;Ctrl+·
来调用AI Command
,例如提问如何查看所有Docker镜像
,Warp会把对应的命令告诉你;今天带大家体验了一把现代化的终端工具Warp,对比传统的终端工具,Warp的提示非常智能,让人有种终端工具界的IDEA的感觉,感兴趣的小伙伴可以尝试下!
\\n前段时间忙项目,这个snail-job系列也停更了有段时间了。接续前缘,按照之前的思路,继续探索未知的snail-job领域。不过这段时间snail-job也没闲着,非常活跃的经历了多次升级,目前最新的版本已经来到了v1.4.0
版本。最直观的感受是服务端的后台报警信息少了很多。可能与通讯模式默认改为Grpc有关。这里也为开原作者献上我的敬意!
本节主要是针对工作流任务进行测试学习。在前面6节的学习后,我们搭建一个普通定时任务已经完全没有问题了,而这个工作流其实就是把多个普通定时进行编排,从而形成一个有前后执行顺序甚至是有条件的流程任务。
\\n\\n\\n抓重点
\\n第一段叽里呱啦说了一大段,真正有营养的就最后一句话。拆解这句话,可以归纳:
\\n\\n
\\n- 工作流是普通定时任务的集合
\\n- 工作流有前后执行顺序
\\n- 工作流在执行某个任务可以加条件
\\n
这里实现一个假设的业务场景:一个是微信的账单收集任务,统计某一天的总金额;另一个是支付宝的账单收集任务,同样也是统计某一天的总金额;等这两个任务完成后再执行一个会汇总账单总金额的定时任务。
\\n基础的流程就如上图这么简单的业务流程。后面可能随着深入学习,此图可能会有所变化。
\\n<dependencies>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-web</artifactId>\\n </dependency>\\n <!-- snail-job 客户端依赖 --\x3e\\n <dependency>\\n <groupId>com.aizuda</groupId>\\n <artifactId>snail-job-client-starter</artifactId>\\n <version>1.4.0</version>\\n </dependency>\\n <!-- snail-job 重试相关依赖 --\x3e\\n <dependency>\\n <groupId>com.aizuda</groupId>\\n <artifactId>snail-job-client-retry-core</artifactId>\\n <version>1.4.0</version>\\n </dependency>\\n <!-- snail-job 客户端核心依赖 --\x3e\\n <dependency>\\n <groupId>com.aizuda</groupId>\\n <artifactId>snail-job-client-job-core</artifactId>\\n <version>1.4.0</version>\\n </dependency>\\n</dependencies>\\n
\\n这个对象用于在几个定时任务的上下文中进行传输。
\\n@Data\\npublic class BillDto {\\n /**\\n * 账单ID\\n */\\n private Long billId;\\n /**\\n * 账单渠道\\n */\\n private String billChannel;\\n /**\\n * 账单日期\\n */\\n private String billDate;\\n /**\\n * 账单金额\\n */\\n private BigDecimal billAmount;\\n\\n}\\n
\\n该任务就是用于微信账单的统计任务。该任务从工作流的上下文中获得一个settlementDate清算日期值,如果是sysdate就设置当前天为账单日期。并且把BillDto放到上下文中,供后面的任务使用。代码如下:
\\n@Component\\n@JobExecutor(name = \\"wechatBillTask\\")\\npublic class WechatBillTask {\\n public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {\\n BillDto billDto = new BillDto();\\n billDto.setBillId(123456789L);\\n billDto.setBillChannel(\\"wechat\\");\\n // 从上下文中获得清算日期并设置,如果上下文中清算日期\\n // 是sysdate设置为当前日期;否则取管理页面设置的值\\n String settlementDate = (String) jobArgs.getWfContext().get(\\"settlementDate\\");\\n if(StrUtil.equals(settlementDate, \\"sysdate\\")) {\\n settlementDate = DateUtil.today();\\n }\\n billDto.setBillDate(settlementDate);\\n billDto.setBillAmount(new BigDecimal(\\"1234.56\\"));\\n // 把billDto对象放入上下文进行传递\\n jobArgs.appendContext(\\"wechat\\", JSONUtil.toJsonStr(billDto));\\n SnailJobLog.REMOTE.info(\\"上下文: {}\\", jobArgs.getWfContext());\\n return ExecuteResult.success(billDto);\\n }\\n}\\n
\\n\\n\\n补充说明:
\\n我这里采用注解方式来实现定时任务。如果想通过实现类方式请自行参考前面的几篇文章。
\\n
支付宝账单任务同微信账单任务。
\\n@Component\\n@JobExecutor(name = \\"alipayBillTask\\")\\npublic class AlipayBillTask {\\n public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {\\n BillDto billDto = new BillDto();\\n billDto.setBillId(23456789L);\\n billDto.setBillChannel(\\"alipay\\");\\n // 设置清算日期\\n String settlementDate = (String) jobArgs.getWfContext().get(\\"settlementDate\\");\\n if(StrUtil.equals(settlementDate, \\"sysdate\\")) {\\n settlementDate = DateUtil.today();\\n }\\n billDto.setBillDate(settlementDate);\\n billDto.setBillAmount(new BigDecimal(\\"2345.67\\"));\\n // 把billDto对象放入上下文进行传递\\n jobArgs.appendContext(\\"alipay\\", JSONUtil.toJsonStr(billDto));\\n SnailJobLog.REMOTE.info(\\"上下文: {}\\", jobArgs.getWfContext());\\n return ExecuteResult.success(billDto);\\n }\\n}\\n
\\n该任务就是从上下文取得微信、支付宝账单内容,把dto中的金额进行汇总。代码如下:
\\n@Component\\n@JobExecutor(name = \\"summaryBillTask\\")\\npublic class SummaryBillTask {\\n public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {\\n // 获得微信账单\\n BigDecimal wechatAmount = BigDecimal.valueOf(0);\\n String wechat = (String) jobArgs.getWfContext(\\"wechat\\");\\n if(StrUtil.isNotBlank(wechat)) {\\n BillDto wechatBillDto = JSONUtil.toBean(wechat, BillDto.class);\\n wechatAmount = wechatBillDto.getBillAmount();\\n }\\n // 获得支付宝账单\\n BigDecimal alipayAmount = BigDecimal.valueOf(0);\\n String alipay = (String) jobArgs.getWfContext(\\"alipay\\");\\n if(StrUtil.isNotBlank(alipay)) {\\n BillDto alipayBillDto = JSONUtil.toBean(alipay, BillDto.class);\\n alipayAmount = alipayBillDto.getBillAmount();\\n }\\n // 汇总账单\\n BigDecimal totalAmount = wechatAmount.add(alipayAmount);\\n SnailJobLog.REMOTE.info(\\"总金额: {}\\", totalAmount);\\n return ExecuteResult.success(totalAmount);\\n }\\n}\\n
\\n微信对账单任务
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n配置项 | 配置内容 |
---|---|
任务名称 | 微信对账单 |
状态 | 启用 |
任务类型 | 集群 |
自定义执行器 | wechatBillTask |
路由策略 | 轮询 |
阻塞策略 | 丢弃 |
触发类型 | 工作流 |
超时时间 | 60秒 |
最大重试次数 | 0 |
重试间隔 | 1秒 |
\\n\\n说明:
\\n这里很关键的一个配置项是触发类型。当设置为工作流触发时,阻塞策略以工作流的策略为准,所以这里配置什么已经不重要了。
\\n
支付宝对账单任务
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n配置项 | 配置内容 |
---|---|
任务名称 | 支付宝对账单 |
自定义执行器 | alipayBillTask |
\\n\\n其他配置项同上面的微信对账单任务
\\n
汇总账单金额任务
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n配置项 | 配置内容 |
---|---|
任务名称 | 汇总账单金额 |
自定义执行器 | summaryBillTask |
\\n\\n其他配置项同上面的微信对账单任务
\\n
工作流总体配置
\\n\\n\\n说明:
\\n\\n
\\n- \\n
\\n这里是对整个工作流的配置,这里设置的触发类型和间隔时间,实际上是调用具体定时任务的时间
\\n- \\n
\\n执行超时时间,是所有任务执行完成的超时时间。所以最好根据实际情合理的设置。
\\n- \\n
\\n阻塞策略,同理也是该工作流下所有任务的阻塞策略。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
阻塞策略 策略功能 解释 丢弃 放弃创建新工作流批次 你都没干完,我不干 覆盖 关闭还在执行的工作流批次,执行新工作流批次 你别干了,我来干 并行 不管正在执行的工作流批次,直接执行新的工作流批次 直接开干 恢复 执行正在执行中的工作流批次中的失败任务(非重试) 继续把失败的任务干完 - \\n
\\n工作流上下文,可以通过点击减号去掉不用设置。该例中是需要的。
\\n- \\n
\\n节点状态。这里关闭或者开启该工作流。
\\n
微信对账单任务
\\n\\n\\n说明:
\\n\\n
\\n- \\n
\\n优先级:同一级中数字越小越先执行
\\n- \\n
\\n失败策略:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
类型 说明 跳过 工作流会跳过当前失败任务,继续向下执行,并且若最终执行完成,
工作流状态会是处理成功。阻塞 工作流会被阻塞在当前任务阶段,等待手动重试。若重试成功,
工作流还会自动向下执行未执行完的任务。- \\n
\\n节点状态:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
类型 说明 开启 任务流调度会经过该节点。 关闭 任务流调度会跳过该节点。 就目前的1.4.0版本而言,如果把一个节点状态设置为关闭后,记得要把失败策略手动设置为跳过。不然工作流还是会阻塞住。这个开发团队后面可能会进行调整。
\\n
支付宝账单任务
\\n汇总账单金额任务
\\n整体配置后效果图
\\n启动服务端和客户端后,查看工作流的执行批次。
\\n这里想使用工作流中的决策节点。这里增加一个需求,要按照上下文中的channel渠道如果是wechat走微信对账单任务;其他走支付宝对账单任务。
\\n不用改动
\\n表达式类型包括:SpEl、Aviator和QL。我这里就知道SpEl,就拿这个试试。不过受到安全限制,这里的SpEl上下文是用SimpleEvaluationContext
。我这里列出和StandardEvaluationContext
d的区别。
特性 | SimpleEvaluationContext | StandardEvaluationContext |
---|---|---|
设计目的 | 限制功能以提高安全性,适用于简单场景 | 提供完整功能,但可能引入安全风险 |
默认注册的 Java 类型 | 无 | 自动注册 java.lang 包下的所有类 |
允许 T() 类型引用 | 默认禁用 | 默认启用 |
适用场景 | 数据绑定、简单表达式 | 复杂表达式、需要访问静态方法/类 |
\\n\\n说明:
\\n\\n
\\n- 表达式类型: 这里我仅仅会SpEl表达式
\\n- 条件表达式:这里就是判断上下文中的channel是不是等于wechat
\\n- 模拟上下文:这里单纯用来测试验证用的,这里设置的值,并不会影响实际上下文中的值
\\n
在上面的微信渠道任务条件下,把微信对账单任务添加进来。这个和前面目标1介绍的方式是一样的。
\\n支付宝的任务节点添加到其他情况这个分支中,汇总账单金额任务不变,最后的整体流程如下:
\\n渠道是wechat
\\n2025-03-28 16:46:21 [snail-grpc-server-5] INFO c.a.s.c.job.core.client.JobEndPoint\\n - 批次:[8260] 任务调度成功. \\n2025-03-28 16:46:21 [snail-job-job-8,260-1] INFO c.mayuanfei.workflow.WechatBillTask\\n - 上下文: {channel=wechat, settlementDate=sysdate}\\n2025-03-28 16:46:21 [snail-job-job-8,260-1] INFO c.a.s.c.j.c.e.JobExecutorFutureCallback\\n - 任务执行成功 taskBatchId:[8260] [{\\"status\\":1,\\"result\\":{\\"billId\\":123456789,\\"billChannel\\":\\"wechat\\",\\"billDate\\":\\"2025-03-28\\",\\"billAmount\\":1234.56},\\"message\\":\\"任务执行成功\\"}]\\n2025-03-28 16:46:22 [snail-job-grpc-server-executor-11] INFO c.a.s.c.c.rpc.server.GrpcInterceptor\\n - method invoked: UnaryRequest/unaryRequest cast:0ms\\n2025-03-28 16:46:22 [snail-grpc-server-6] INFO c.a.s.c.job.core.client.JobEndPoint\\n - 批次:[8262] 任务调度成功. \\n2025-03-28 16:46:22 [snail-job-job-8,262-1] INFO c.mayuanfei.workflow.SummaryBillTask\\n - 总金额: 1234.56\\n2025-03-28 16:46:22 [snail-job-job-8,262-1] INFO c.a.s.c.j.c.e.JobExecutorFutureCallback\\n - 任务执行成功 taskBatchId:[8262] [{\\"status\\":1,\\"result\\":1234.56,\\"message\\":\\"任务执行成功\\"}]\\n
\\n从测试结果看,只有微信任务和汇总任务执行了。
\\n渠道是alipay
\\n2025-03-28 16:49:25 [snail-grpc-server-11] INFO c.a.s.c.job.core.client.JobEndPoint\\n - 批次:[8276] 任务调度成功. \\n2025-03-28 16:49:25 [snail-job-job-8,276-1] INFO c.mayuanfei.workflow.AlipayBillTask\\n - 上下文: {channel=alipay, settlementDate=sysdate}\\n2025-03-28 16:49:25 [snail-job-job-8,276-1] INFO c.a.s.c.j.c.e.JobExecutorFutureCallback\\n - 任务执行成功 taskBatchId:[8276] [{\\"status\\":1,\\"result\\":{\\"billId\\":23456789,\\"billChannel\\":\\"alipay\\",\\"billDate\\":\\"2025-03-28\\",\\"billAmount\\":2345.67},\\"message\\":\\"任务执行成功\\"}]\\n2025-03-28 16:49:25 [snail-job-grpc-server-executor-23] INFO c.a.s.c.c.rpc.server.GrpcInterceptor\\n - method invoked: UnaryRequest/unaryRequest cast:0ms\\n2025-03-28 16:49:25 [snail-grpc-server-12] INFO c.a.s.c.job.core.client.JobEndPoint\\n - 批次:[8277] 任务调度成功. \\n2025-03-28 16:49:25 [snail-job-job-8,277-1] INFO c.mayuanfei.workflow.SummaryBillTask\\n - 总金额: 2345.67\\n2025-03-28 16:49:25 [snail-job-job-8,277-1] INFO c.a.s.c.j.c.e.JobExecutorFutureCallback\\n - 任务执行成功 taskBatchId:[8277] [{\\"status\\":1,\\"result\\":2345.67,\\"message\\":\\"任务执行成功\\"}]\\n
\\n从测试结果看,只有支付宝和汇总任务执行了。
\\n这里想使用工作流中的回调通知。这里增加一个需求,就是在汇总账单金额任务执行完成后调用一个接口,把最后汇总的金额通知给这个接口。
\\n@Slf4j\\n@RestController\\n@RequestMapping(\\"/workflow\\")\\npublic class WorkflowCallbackController {\\n\\n @PostMapping(\\"/callback\\")\\n public void callback(@RequestBody CallbackParamsDTO callbackParams, \\n @RequestHeader HttpHeaders headers) {\\n // callbackParams 对象可以获取到当前回调通知之前的上下文内容\\n // secret 是当回调通知的秘钥,用于鉴权\\n String secret = headers.getFirst(\\"secret\\");\\n log.info(\\"callback: {}, secret:{}\\", callbackParams, secret);\\n log.info(\\"完成任务推送到监控\\");\\n }\\n}\\n
\\n\\n\\n说明:
\\n\\n
\\n- CallbackParamsDTO可以接受回调的参数。当然这里需要引用snail-job的包,如果是通知到其他服务,这里可以用字符串接收。
\\n- header中有秘钥的信息。通过该秘钥可以鉴权。
\\n- 回调可以做很多业务以为的事情。这里就是结合实际情况进行运用了。可能更多情况是不需要用到回调的,但是如果当你需要某个工作流执行完成后,再做一些额外的事情。那么这个webhook就很有用了。
\\n
调出添加对话框
\\n添加回调通知
\\n\\n\\n说明
\\n\\n
\\n- webhook: 回调地址
\\n- 请求类型:可以是json或者form表单
\\n- 秘钥:通讯秘钥
\\n- 回调通知状态:这里估计是页面写错误。这里的工作流状态其实应该是通知的状态。
\\n
jobArgs.getWfContext()
jobArgs.appendContext()
SimpleEvaluationContext
所以无法引用很多java内置方法。headers.getFirst(\\"secret\\");
\\n\\n本次线上问题 从得到通知, 到定位出具体问题代码 耗时半小时
\\n
不好啦❗ 天塌了❗ 系统崩了❗ 服务又又又又掉线了❗ 有人要倒霉了❗
\\n快看啊,一个上线3年的业务,突然就崩了
\\n生产问题群爆炸了
\\n\\n\\n我的心里活动一:“这个服务现在是交给我负责,我得快点排查清楚,最好汇报,最好能把责任推出去”
\\n
\\n\\n\\n我的心里活动二:“太好了😀太好了😀终于给我碰上了,这个问题可很少发生啊,又积累血琳琳的生产一个问题”
\\n
不想看废话的直接看【解决过程和方案】 吧
\\n先看pinponint监控 一年看出tomcat线程耗尽 直接百分百锁定服务掉线原因
\\n进一步看这个爬坡过程,发现持续爬坡接近1个小时,是什么样的接口导致他爬坡1小时呢!!!
\\n再看掉线前几分钟日志,发现tomcat只有几个线程在处理web请求,其他线程未曾在日志中发现,
\\n这样一来进一步确认了上一步监控看到的现象。
\\n所以 我们是不是找到某个线程最后处理请求是那个接口是不是就可以确认,是那个接口出的问题呢。
\\n看这个211线程 最后处理请求是在这个时间,处理这个接口,之后这个线程就再也未曾出来过,
\\n接着又看了几个线程 最后一次请求也是这个接口
\\n所以基本已经定位出那个接口出的问题
\\n上代码 结合日志加代码, 发现finally块代码始终未执行
\\n那么 直接定位出 阻塞在了红框内
\\n既然定位出问题所在 那么修改就简单了
\\n跟运维+产品确认 选择合适的修改方案即可
","description":"前言 本次线上问题 从得到通知, 到定位出具体问题代码 耗时半小时\\n\\n不好啦❗ 天塌了❗ 系统崩了❗ 服务又又又又掉线了❗ 有人要倒霉了❗\\n\\n快看啊,一个上线3年的业务,突然就崩了\\n\\n生产问题群爆炸了\\n\\n我的心里活动一:“这个服务现在是交给我负责,我得快点排查清楚,最好汇报,最好能把责任推出去”\\n\\n我的心里活动二:“太好了😀太好了😀终于给我碰上了,这个问题可很少发生啊,又积累血琳琳的生产一个问题”\\n\\n理论基础 juejin.cn/post/748721…\\n\\n不想看废话的直接看【解决过程和方案】 吧\\n\\n排查过程\\n\\n先看pinponint监控…","guid":"https://juejin.cn/post/7487861752341364746","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T01:37:42.125Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/763f2028cd014898a4472ab4b85368d4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744080740&x-signature=7fCJSN76VBNsa44wpbc3A8ZLUhU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5dabcf0b52c44757874e2e031ec6b09b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744080740&x-signature=7V6USS%2BfzCdr8vl1uOE16mRM3Gg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6604f2273575402783f7c9be06e341b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744080740&x-signature=mFXnXdKAV4czdmJ9qYphUvh%2BCpU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/506168af3f444dd488cde77729cb6d82~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744080740&x-signature=B1MxOH38qQEUOshICTTqsgQQAI4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f4bc3ced8efe44238a04b8301659af92~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744080740&x-signature=WXWZ6VtfNAonserLUNp%2FrsGtMZo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"凌晨三点我用Python重写公司远控程序,竟发现实习生埋了后门?","url":"https://juejin.cn/post/7487846902661513250","content":"事情是这样的,那天晚上,我加完班回到家,刚打开电脑准备刷点剧放松一下,结果老板一个电话打过来,语气焦急:
\\n\\n\\n\\"花姐,公司远程控制程序有安全漏洞,客户抱怨有人未经授权登录他们的服务器,你能不能看下?\\"
\\n
听到这话,我的困意瞬间消失,脑子里只剩下四个字:要出事了!
\\n远控程序的安全性问题可不是闹着玩的,客户的数据如果泄露,公司不仅要背锅,搞不好还要吃官司。
于是,我火速连上公司的服务器,翻看了远程控制代码。刚看了几眼,额头就开始冒汗——
\\n\\n\\n代码里居然有一个隐藏的端口监听!
\\n
而且,仔细一查,居然是一个实习生写的?!
\\n说到远程控制,最核心的部分其实就是网络通信,而在Python里,处理网络通信最基础的库就是socket
。
Socket(套接字)是计算机网络通信的基石,简单来说,它就像一个电话——
\\n二者通过IP + 端口的方式建立连接,进行数据传输。
\\nimport socket\\n\\n# 创建Socket对象\\nserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\\n\\n# 绑定IP和端口\\nserver.bind((\'0.0.0.0\', 9999))\\n\\n# 开始监听\\nserver.listen(5)\\nprint(\\"服务器启动,等待连接...\\")\\n\\nwhile True:\\n client, addr = server.accept()\\n print(f\\"客户端连接:{addr}\\")\\n client.send(b\\"Hello from server!\\\\n\\")\\n client.close()\\n
\\nimport socket\\n\\nclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\\nclient.connect((\'127.0.0.1\', 9999))\\n\\ndata = client.recv(1024)\\nprint(f\\"收到服务器消息:{data.decode()}\\")\\n
\\n运行服务端,再运行客户端,客户端会收到服务器发来的消息:
\\n收到服务器消息:Hello from server!\\n
\\n看起来很简单,对吧?但如果让一个实习生来写……你永远不知道他会干出什么事。
\\n我仔细翻了翻代码,发现了一个非常隐蔽的代码片段:
\\nimport socket\\n\\ndef backdoor():\\n s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\\n s.bind((\'0.0.0.0\', 4444)) # 监听隐藏端口\\n s.listen(1)\\n while True:\\n client, addr = s.accept()\\n print(f\\"后门连接自 {addr}\\")\\n client.send(b\\"You got hacked!\\\\n\\")\\n client.close()\\n\\nbackdoor()\\n
\\n4444
端口You got hacked!
这个鬼东西相当于给服务器开了个暗门,任何知道端口的人都能悄悄连上来!
\\n瞬间冷汗直流,实习生这不是在帮忙写代码,这是在给公司挖坑啊!
\\n为了防止类似情况发生,我决定重写整个远控程序,并加入加密传输、身份验证和日志记录。
\\nimport socket\\nimport ssl\\n\\ncontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\\ncontext.load_cert_chain(certfile=\\"cert.pem\\", keyfile=\\"key.pem\\")\\n\\nserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\\nserver.bind((\'0.0.0.0\', 9999))\\nserver.listen(5)\\n\\nsecure_server = context.wrap_socket(server, server_side=True)\\nprint(\\"安全服务器已启动...\\")\\n\\nwhile True:\\n client, addr = secure_server.accept()\\n print(f\\"安全连接:{addr}\\")\\n client.send(b\\"Secure Hello!\\\\n\\")\\n client.close()\\n
\\n这样,即使有人监听流量,也只能看到加密数据,无法直接窃取信息。
\\nimport hashlib\\n\\ndef check_password(password):\\n hash_pwd = hashlib.sha256(password.encode()).hexdigest()\\n return hash_pwd == \\"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd37d17bd3a\\" # 存储的密码哈希\\n\\npassword = input(\\"请输入密码:\\")\\nif check_password(password):\\n print(\\"登录成功!\\")\\nelse:\\n print(\\"密码错误!\\")\\n
\\n这样,非授权用户即使连上了服务器,也无法执行任何操作。
\\n这次事件让我深刻认识到,
\\n\\n\\n安全问题,不能交给实习生! 😂
\\n
远程控制程序本质上就是在玩“开门”和“关门”的游戏,而如果代码里藏了一个“永远开着的窗户”,那黑客自然能随时进出。
\\n所以,开发网络程序时,千万别偷懒,一定要考虑加密、身份验证、日志审计,否则某天你可能会像我一样,凌晨三点被老板紧急call醒……
\\n希望这篇文章能给你们提个醒,**Python远控很酷,但安全更重要!**🎉
","description":"0. 背景故事:一场意外的挑战 事情是这样的,那天晚上,我加完班回到家,刚打开电脑准备刷点剧放松一下,结果老板一个电话打过来,语气焦急:\\n\\n\\"花姐,公司远程控制程序有安全漏洞,客户抱怨有人未经授权登录他们的服务器,你能不能看下?\\"\\n\\n听到这话,我的困意瞬间消失,脑子里只剩下四个字:要出事了!\\n 远控程序的安全性问题可不是闹着玩的,客户的数据如果泄露,公司不仅要背锅,搞不好还要吃官司。\\n\\n于是,我火速连上公司的服务器,翻看了远程控制代码。刚看了几眼,额头就开始冒汗——\\n\\n代码里居然有一个隐藏的端口监听!\\n\\n而且,仔细一查,居然是一个实习生写的?!\\n\\n1…","guid":"https://juejin.cn/post/7487846902661513250","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T01:23:33.982Z","media":null,"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"Spring Boot 实现文件秒传功能","url":"https://juejin.cn/post/7487859784231141417","content":"在开发Web应用时,文件上传是一个常见需求。然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余。此时可以使用文件秒传技术通过识别重复文件,实现瞬间完成上传的效果,大大提升了用户体验和系统效率。
\\n文件秒传的核心原理是:
\\n这种方式能显著减少网络传输和避免存储冗余。
\\n首先创建Spring Boot项目,添加必要依赖:
\\n<dependencies>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-web</artifactId>\\n <version>2.7.18</version>\\n </dependency>\\n <dependency>\\n <groupId>cn.hutool</groupId>\\n <artifactId>hutool-all</artifactId>\\n <version>5.8.1</version>\\n </dependency>\\n</dependencies>\\n
\\n此处使用一个简单的集合来存储文件信息,实际使用需要替换为数据库或其他持久化中间件。
\\nimport cn.hutool.crypto.digest.DigestUtil;\\nimport org.springframework.stereotype.Service;\\nimport org.springframework.web.multipart.MultipartFile;\\n\\nimport java.io.IOException;\\nimport java.util.Map;\\nimport java.util.concurrent.ConcurrentHashMap;\\n\\n@Service\\npublic class FileService {\\n \\n // 使用Map存储文件信息,key为MD5,value为文件信息(实际使用时可替换为数据库存储)\\n private final Map<String, FileInfo> fileStore = new ConcurrentHashMap<>();\\n \\n /**\\n * 检查文件是否已存在\\n */\\n public FileInfo findByMd5(String md5) {\\n return fileStore.get(md5);\\n }\\n \\n /**\\n * 保存文件信息\\n */\\n public FileInfo saveFile(String fileName, String fileMd5, Long fileSize, String filePath) {\\n FileInfo fileInfo = new FileInfo(fileName, fileMd5, fileSize, filePath);\\n fileStore.put(fileMd5, fileInfo); // 实际使用时插入数据库\\n return fileInfo;\\n }\\n \\n /**\\n * 计算文件MD5\\n */\\n public String calculateMD5(MultipartFile file) throws IOException {\\n return DigestUtil.md5Hex(file.getInputStream());\\n }\\n}\\n
\\n定义一个简单的文件信息实体类:
\\nimport cn.hutool.core.util.IdUtil;\\n\\npublic class FileInfo {\\n\\n private String id = IdUtil.fastUUID();\\n private String fileName;\\n private String fileMd5;\\n private Long fileSize;\\n private String filePath;\\n\\n public FileInfo(String fileName, String fileMd5, Long fileSize, String filePath) {\\n this.fileName = fileName;\\n this.fileMd5 = fileMd5;\\n this.fileSize = fileSize;\\n this.filePath = filePath;\\n }\\n\\n public String getId() {\\n return id;\\n }\\n\\n public void setId(String id) {\\n this.id = id;\\n }\\n\\n public String getFileName() {\\n return fileName;\\n }\\n\\n public void setFileName(String fileName) {\\n this.fileName = fileName;\\n }\\n\\n public String getFileMd5() {\\n return fileMd5;\\n }\\n\\n public void setFileMd5(String fileMd5) {\\n this.fileMd5 = fileMd5;\\n }\\n\\n public Long getFileSize() {\\n return fileSize;\\n }\\n\\n public void setFileSize(Long fileSize) {\\n this.fileSize = fileSize;\\n }\\n\\n public String getFilePath() {\\n return filePath;\\n }\\n\\n public void setFilePath(String filePath) {\\n this.filePath = filePath;\\n }\\n}\\n
\\n为了统一返回结果格式,可以创建一个简单的Result类。
\\npublic class Result {\\n private boolean success;\\n private Object data;\\n private String message;\\n\\n public Result(boolean success, Object data, String message) {\\n this.success = success;\\n this.data = data;\\n this.message = message;\\n }\\n\\n public static Result success(Object data) {\\n return new Result(true, data,\\"success\\");\\n }\\n\\n public static Result success(Object data,String message) {\\n return new Result(true, data,message);\\n }\\n\\n public static Result error(String message) {\\n return new Result(false, null, message);\\n }\\n\\n // Getters\\n public boolean isSuccess() { return success; }\\n public Object getData() { return data; }\\n public String getMessage() { return message; }\\n}\\n
\\nimport cn.hutool.core.io.FileUtil;\\nimport org.slf4j.Logger;\\nimport org.slf4j.LoggerFactory;\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.web.bind.annotation.*;\\nimport org.springframework.web.multipart.MultipartFile;\\n\\nimport java.io.File;\\n\\n@RestController\\n@RequestMapping(\\"/api/file\\")\\npublic class FileController {\\n\\n private static Logger logger = LoggerFactory.getLogger(FileController.class);\\n\\n @Autowired\\n private FileService fileService;\\n\\n /**\\n * 检查文件是否已存在\\n */\\n @PostMapping(\\"/check\\")\\n public Result checkFile(@RequestParam(\\"md5\\") String md5) {\\n FileInfo fileInfo = fileService.findByMd5(md5);\\n if (fileInfo != null) {\\n return Result.success(fileInfo);\\n }\\n return Result.success(null);\\n }\\n \\n /**\\n * 上传文件\\n */\\n @PostMapping(\\"/upload\\")\\n public Result uploadFile(@RequestParam(\\"file\\") MultipartFile file) {\\n try {\\n // 计算文件MD5值\\n String md5 = fileService.calculateMD5(file);\\n \\n // 检查文件是否已存在\\n FileInfo existFile = fileService.findByMd5(md5);\\n if (existFile != null) {\\n // todo 进行自定义的逻辑处理\\n return Result.success(existFile,\\"文件秒传成功\\");\\n }\\n \\n // 文件不存在,执行上传\\n String originalFilename = file.getOriginalFilename();\\n String filePath = FileUtil.getTmpDir() + File.separator + originalFilename; // 保存到临时目录\\n \\n // 存储文件\\n file.transferTo(new File(filePath));\\n \\n // 保存文件信息到内存(实际使用时应替换为数据库)\\n FileInfo fileInfo = fileService.saveFile(originalFilename, md5, file.getSize(), filePath);\\n return Result.success(fileInfo,\\"文件上传成功\\");\\n } catch (Exception e) {\\n logger.error(e.getMessage(),e);\\n return Result.error(\\"文件上传失败:\\" + e.getMessage());\\n }\\n }\\n}\\n
\\n创建一个简单的HTML上传页面:
\\n<!DOCTYPE html>\\n<html lang=\\"zh\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <title>文件秒传示例</title>\\n <script src=\\"https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js\\"></script>\\n <script src=\\"https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js\\"></script>\\n</head>\\n<body>\\n <h2>文件上传(支持秒传)</h2>\\n <input type=\\"file\\" id=\\"fileInput\\" />\\n <button onclick=\\"uploadFile()\\">上传文件</button>\\n <div id=\\"progressBar\\" style=\\"display:none;\\">\\n <div>上传进度:<span id=\\"progress\\">0%</span></div>\\n </div>\\n <div id=\\"result\\"></div>\\n <script>\\n function uploadFile() {\\n const fileInput = document.getElementById(\'fileInput\');\\n const file = fileInput.files[0];\\n if (!file) {\\n alert(\'请选择文件\');\\n return;\\n }\\n \\n document.getElementById(\'progressBar\').style.display = \'block\';\\n document.getElementById(\'result\').innerText = \'计算文件MD5中...\';\\n\\n // 计算文件MD5\\n calculateMD5(file).then(md5 => {\\n document.getElementById(\'result\').innerText = \'正在检查文件是否已存在...\';\\n \\n // 检查文件是否已存在\\n return axios.post(\'/api/file/check\', {\\n md5: md5\\n }).then(response => {\\n if (response.data.data && response.data.data.id) {\\n // 文件已存在,执行秒传\\n document.getElementById(\'result\').innerText = \'文件秒传成功!\';\\n document.getElementById(\'progress\').innerText = \'100%\';\\n return Promise.resolve();\\n } else {\\n // 文件不存在,执行上传\\n const formData = new FormData();\\n formData.append(\'file\', file);\\n \\n return axios.post(\'/api/file/upload\', formData, {\\n onUploadProgress: progressEvent => {\\n const percentCompleted = Math.round(\\n (progressEvent.loaded * 100) / progressEvent.total\\n );\\n document.getElementById(\'progress\').innerText = percentCompleted + \'%\';\\n }\\n }).then(response => {\\n document.getElementById(\'result\').innerText = \'文件上传成功!\';\\n });\\n }\\n });\\n }).catch(error => {\\n document.getElementById(\'result\').innerText = \'错误:\' + error.message;\\n });\\n }\\n \\n // 计算文件MD5\\n function calculateMD5(file) {\\n return new Promise((resolve, reject) => {\\n const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;\\n const chunkSize = 2097152; // 2MB\\n const chunks = Math.ceil(file.size / chunkSize);\\n let currentChunk = 0;\\n const spark = new SparkMD5.ArrayBuffer();\\n const fileReader = new FileReader();\\n \\n fileReader.onload = function(e) {\\n spark.append(e.target.result);\\n currentChunk++;\\n \\n if (currentChunk < chunks) {\\n loadNext();\\n } else {\\n resolve(spark.end());\\n }\\n };\\n \\n fileReader.onerror = function() {\\n reject(\'文件读取错误\');\\n };\\n \\n function loadNext() {\\n const start = currentChunk * chunkSize;\\n const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;\\n fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));\\n }\\n \\n loadNext();\\n });\\n }\\n </script>\\n</body>\\n</html>\\n
\\n在application.yml
中添加必要配置
server:\\n port: 8080\\n\\nspring:\\n servlet:\\n multipart:\\n max-file-size: 100MB\\n max-request-size: 100MB\\n
\\n作为程序员,你一定见过这样的配置:i9-13900K拥有32KB L1/2MB L2/36MB L3。为什么所有现代CPU都不约而同选择三级缓存?这背后藏着计算机架构师们二十年的智慧博弈。
\\n初代奔腾处理器只有16KB统一缓存,就像你住在一个大型社区,每天要接收很多快递,家楼下有一个小型快递柜(存3个包裹):
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n处理器 | 缓存结构 | 内存延迟 |
---|---|---|
Intel Pentium | 16KB L1 | 120ns |
现代i9 | L1+L2+L3 | 8ns |
奔腾4引入L2缓存,相当于在小区中央里建了社区驿站(存20个包裹):
\\n但到2006年,问题出现了:双核处理器共享L2导致冲突,就像两个邻居总在驿站撞见。
\\n引入L3缓存,就像引入分拣中心(存200个包裹),容量更大
\\n三级缓存本质上是道数学题:用最小成本覆盖90%的命中率
\\n对比不同层级缓存的关键参数:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n缓存层级 | 访问周期 | 典型容量 | 物理距离 |
---|---|---|---|
L1 | 3-4 cycle | 32-64KB | 核心内部 |
L2 | 12-15 cycle | 256KB-2MB | 核心旁边 |
L3 | 30-50 cycle | 8-128MB | 芯片边缘 |
假设只有L1和L3:
\\n实验结果:三级结构比两级总体延迟降低42%
\\nAMD推土机架构(仅L1+L2)遭遇的灾难:
\\n缓存层级的性价比曲线:
\\nAMD 3D V-Cache的创新:
\\n实测效果:《CS:GO》帧率提升25%,但仍属于L3范畴
\\n三级结构就像快递网络:
\\n少一级不够用:两级就像只有楼下快递柜和分拣中心,缺了驿站环节
\\n多一级不划算:四级相当于在分拣中心里再建迷你仓库,管理成本暴增
\\n如果觉得有用,欢迎关注\\"草捏子\\",一起成长!👨🔧
","description":"作为程序员,你一定见过这样的配置:i9-13900K拥有32KB L1/2MB L2/36MB L3。为什么所有现代CPU都不约而同选择三级缓存?这背后藏着计算机架构师们二十年的智慧博弈。 一、从单级到三级:缓存层级的进化史\\n1.1 1995年:单级缓存的青铜时代\\n\\n初代奔腾处理器只有16KB统一缓存,就像你住在一个大型社区,每天要接收很多快递,家楼下有一个小型快递柜(存3个包裹):\\n\\n处理器\\t缓存结构\\t内存延迟Intel Pentium\\t16KB L1\\t120ns\\n现代i9\\tL1+L2+L3\\t8ns\\n1.2 2003年…","guid":"https://juejin.cn/post/7487848315679244342","author":"草捏子","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T13:06:00.942Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/829235976c7b4fea97742fc1aebebc8d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1744156442&x-signature=f4pi5qxdAbMpRLyUuvhB3Jap3kc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8e75dad53eda454684fa6ad557430047~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1744156442&x-signature=v5qkVZBVdMlG7r1vZAn%2BCZXNB7k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ccbf6439fed54519968697fd13857582~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1744156442&x-signature=Y0se%2F%2B%2BbghYHGgvrVIomOKYX88A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2075f5cb42d04edb8cc176fca178c206~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1744156442&x-signature=8qlyvSZMoycWUWJrXCIagPpT8u0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b67848ac2fac40a3a2f7d9a98ffc886b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I2J5o2P5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1744156442&x-signature=W2%2BXJ3QcQo%2Fpk%2FqG2T3tUSiYg9s%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","cpu"],"attachments":null,"extra":null,"language":null},{"title":"Java开发工程师必须掌握的线程知识指南","url":"https://juejin.cn/post/7487600796739534859","content":"本篇文章,主要是列举了线程模块的核心知识和技术。内容分两个模块一个是简单介绍,第二是深入学习。深入学习部分都是推荐的博客文章。
\\n内容有点多,欢迎收藏,可当做java线程模块学习路线,以及面试突击。
\\n新建状态(New):new Thread 此时线程对象已经被创建,但还没有开始运行。
\\n就绪状态(Runnable):调用start()
方法后,线程进入就绪状态,可能还没有被分配到CPU时间片。
运行状态(Running):线程获得CPU时间片并开始执行时,线程进入运行状态,执行run()
方法
阻塞状态(Blocked):线程因为某些原因无法继续执行时,线程进入阻塞状态。阻塞状态可以分为多种类型,如等待I/O、等待锁、等待信号等。
\\n等待状态(Waiting):线程需要等待某些条件满足时,线程进入等待状态。等待状态可以通过wait()
方法、join()
方法等实现。
计时等待状态(Timed Waiting):当线程需要等待一定时间或者等待某些条件满足时,线程进入计时等待状态。计时等待状态可以通过sleep()方法、wait(timeout)方法等实现。
\\n终止状态(Terminated):当线程完成了任务或者因为异常等原因退出时,线程进入终止状态。此时线程的生命周期结束。
\\nclass MyThread extends Thread {\\n @Override\\n public void run() {\\n System.out.println(\\"这是通过继承Thread类创建的线程在执行任务\\");\\n }\\n}\\n\\npublic class Main {\\n public static void main(String[] args) {\\n MyThread myThread = new MyThread();\\n myThread.start();\\n }\\n}\\n
\\nclass MyRunnable implements Runnable {\\n @Override\\n public void run() {\\n System.out.println(\\"这是通过实现Runnable接口创建的线程在执行任务\\");\\n }\\n}\\n\\npublic class Main {\\n public static void main(String[] args) {\\n MyRunnable myRunnable = new MyRunnable();\\n Thread thread = new Thread(myRunnable);\\n thread.start();\\n }\\n}\\n
\\nclass MyCallable implements Callable<String> {\\n @Override\\n public String call() throws Exception {\\n return \\"这是通过实现Callable接口创建的线程执行结果\\";\\n }\\n}\\n\\npublic class Main {\\n public static void main(String[] args) {\\n MyCallable myCallable = new MyCallable();\\n FutureTask<String> futureTask = new FutureTask<>(myCallable);\\n Thread thread = new Thread(futureTask);\\n thread.start();\\n try {\\n String result = futureTask.get();\\n System.out.println(result);\\n } catch (InterruptedException | ExecutionException e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\nclass MyTask implements Runnable {\\n @Override\\n public void run() {\\n System.out.println(Thread.currentThread().getName() + \\"正在执行任务\\");\\n }\\n}\\n\\npublic class Main {\\n public static void main(String[] args) {\\n ExecutorService executorService = Executors.newFixedThreadPool(3);\\n for (int i = 0; i < 5; i++) {\\n executorService.submit(new MyTask());\\n }\\n executorService.shutdown();\\n }\\n}\\n
\\n线程池算是实际开发中,多线程用得最多的一个技术了,也是初中级面试必问问题,下面几篇文章可以重点阅读:
\\nwait/sleep、notify/notifyAll、join 这些基础方法需要了解
\\nwait 和 sleep的区别:
\\nsleep()方法可以在任何地方使用;而wait()方法则只能在同步方法或同步块中使用。
\\nwait 方法会释放对象锁,但 sleep 方法不会。
\\nwait的线程会进入到WAITING状态,直到被唤醒;sleep的线程会进入到TIMED_WAITING状态,等到指定时间之后会再尝试获取CPU时间片。
\\nnotify/notifyAll:\\n都是唤醒,等待获取当前对象的线程,notify
是唤醒一个,notifyAll
是唤醒所有有,当然只有一个能成功获取对象锁资源。
join():
\\njoin就是把子线程加入到当前主线程中,也就是主线程要阻塞在这里,等子线程执行完之后再继续执行.
常用来控制线程的顺序执行。
\\n\\n\\n\\n
// 实例方法锁\\npublic synchronized void method() {}\\n\\n// 代码块锁\\npublic void method() {\\n synchronized(this) {\\n // 临界区\\n }\\n}\\n\\n// 类锁\\npublic static synchronized void staticMethod() {}\\n
\\nReentrantLock lock = new ReentrantLock(true); // 公平锁\\nlock.lock();\\ntry {\\n // 临界区\\n} finally {\\n lock.unlock();\\n}\\n
\\nvolatile
通过内存屏障和缓存一致性协议实现可见性和有序性,是一种轻量级的同步工具。尽管它无法替代锁(如synchronized
),但在特定场景下能显著提升性能。
常当开关使用:
\\npublic class Test {\\n private static boolean stop = false;\\n\\n public static void main(String[] args) {\\n Thread workerThread = new Thread(() -> {\\n int count = 0;\\n // 按照没有用volatile修饰,当前线程不能看到变量被修改,所以理论上来说不会退出循环\\n while (!stop) {\\n System.out.println(\\"Worker thread running...\\");\\n count++;\\n }\\n System.out.println(\\"Worker thread stopped, count: \\" + count);\\n });\\n\\n workerThread.start();\\n\\n try {\\n Thread.sleep(1000);\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n\\n stop = true;\\n System.out.println(\\"Main thread set stop to true\\");\\n }\\n\\n}\\n
\\n从 JDK1.5 开始提供,用于支持在单个变量上进行无锁的线程安全编程。\\n基于volatile
和 CAS(compare - and - swap)算法,volatile
保证内存可见性,CAS 保证原子性
常用的基础类:AtomicBoolean
、AtomicInteger
、AtomicLong
,用于原子方式更新对应的基本类型值。
public class AtomicIntegerExample {\\n public static void main(String[] args) {\\n // 创建一个初始值为 0 的 AtomicInteger 对象\\n AtomicInteger atomicInt = new AtomicInteger(0);\\n\\n // 创建并启动 5 个工作线程\\n for (int i = 0; i < 5; i++) {\\n new Worker(atomicInt).start();\\n }\\n\\n try {\\n // 主线程休眠一段时间,等待所有工作线程完成\\n Thread.sleep(2000);\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n\\n // 输出最终的计数值\\n System.out.println(\\"最终计数值: \\" + atomicInt.get());\\n }\\n\\n static class Worker extends Thread {\\n private final AtomicInteger atomicInt;\\n\\n public Worker(AtomicInteger atomicInt) {\\n this.atomicInt = atomicInt;\\n }\\n\\n @Override\\n public void run() {\\n for (int i = 0; i < 1000; i++) {\\n // 原子性地将计数值加 1\\n atomicInt.incrementAndGet();\\n }\\n }\\n }\\n}\\n
\\nCountDownLatch
用于让一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,初始值为需要等待的线程数量。每个线程完成操作后,计数器减 1,当计数器为 0 时,等待的线程将被唤醒。
CountDownLatch
。例如,一个数据处理系统,需要同时从多个数据源获取数据,主线程需要等待所有数据源的数据都获取完成后,再进行后续的数据整合和分析。CountDownLatch
让主线程等待所有资源初始化完成后再启动系统的其他服务。import java.util.concurrent.CountDownLatch;\\n\\npublic class CountDownLatchExample {\\n public static void main(String[] args) throws InterruptedException {\\n // 创建一个 CountDownLatch 对象,计数器初始值为 3\\n CountDownLatch latch = new CountDownLatch(3);\\n\\n // 创建并启动 3 个工作线程\\n for (int i = 0; i < 3; i++) {\\n new Worker(latch).start();\\n }\\n\\n // 主线程等待所有工作线程完成\\n System.out.println(\\"主线程等待工作线程完成...\\");\\n latch.await();\\n System.out.println(\\"所有工作线程已完成,主线程继续执行。\\");\\n }\\n\\n static class Worker extends Thread {\\n private final CountDownLatch latch;\\n\\n public Worker(CountDownLatch latch) {\\n this.latch = latch;\\n }\\n\\n @Override\\n public void run() {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" 开始工作...\\");\\n // 模拟工作耗时\\n Thread.sleep((long) (Math.random() * 1000));\\n System.out.println(Thread.currentThread().getName() + \\" 工作完成。\\");\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n } finally {\\n // 工作完成,计数器减 1\\n latch.countDown();\\n }\\n }\\n }\\n}\\n
\\nCyclicBarrier
用于让一组线程在某个点上进行同步。当所有线程都到达该点时,它们将继续执行。CyclicBarrier
可以重复使用,即当所有线程通过屏障后,屏障可以重置,等待下一组线程。
CyclicBarrier
可以确保所有线程都完成计算后再进行结果汇总。public class CyclicBarrierExample {\\n public static void main(String[] args) {\\n // 创建一个 CyclicBarrier 对象,指定参与等待的线程数量和所有线程到达屏障后要执行的任务\\n CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println(\\"所有线程都已到达屏障,开始下一步操作\\"));\\n\\n // 创建并启动 3 个工作线程\\n for (int i = 0; i < 3; i++) {\\n new Worker(barrier).start();\\n }\\n }\\n\\n static class Worker extends Thread {\\n private final CyclicBarrier barrier;\\n\\n public Worker(CyclicBarrier barrier) {\\n this.barrier = barrier;\\n }\\n\\n @Override\\n public void run() {\\n try {\\n System.out.println(Thread.currentThread().getName() + \\" 开始工作...\\");\\n // 模拟工作耗时\\n Thread.sleep((long) (Math.random() * 1000));\\n System.out.println(Thread.currentThread().getName() + \\" 到达屏障,等待其他线程...\\");\\n // 线程到达屏障并等待\\n barrier.await();\\n System.out.println(Thread.currentThread().getName() + \\" 继续执行后续操作。\\");\\n } catch (InterruptedException | BrokenBarrierException e) {\\n e.printStackTrace();\\n }\\n }\\n }\\n}\\n
\\nSemaphore
用于控制同时访问某个资源的线程数量。它通过一个许可证计数器来实现,线程在访问资源前需要获取许可证,访问完成后释放许可证。
Semaphore
可以用来控制同时使用资源的线程数量,避免资源过度使用。Semaphore
对请求进行限流,只允许一定数量的请求同时处理。public static void main(String[] args) {\\n // 创建一个 Semaphore 对象,初始许可数量为 3\\n Semaphore semaphore = new Semaphore(3);\\n\\n // 创建并启动 5 个工作线程\\n for (int i = 0; i < 5; i++) {\\n new Worker(semaphore).start();\\n }\\n}\\n\\nstatic class Worker extends Thread {\\n private final Semaphore semaphore;\\n\\n public Worker(Semaphore semaphore) {\\n this.semaphore = semaphore;\\n }\\n\\n @Override\\n public void run() {\\n try {\\n // 获取许可\\n semaphore.acquire();\\n System.out.println(Thread.currentThread().getName() + \\" 已获取许可,开始工作...\\");\\n // 模拟工作耗时\\n Thread.sleep((long) (Math.random() * 1000));\\n System.out.println(Thread.currentThread().getName() + \\" 工作完成,释放许可。\\");\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n } finally {\\n // 释放许可\\n semaphore.release();\\n }\\n }\\n}\\n
\\nCompletableFuture
是 Java 8 引入的一个强大的异步编程工具类,它实现了 Future
接口和 CompletionStage
接口,结合了 Future
的异步操作能力和 CompletionStage
的流式处理和组合能力,能让开发者以更简洁、灵活的方式处理异步任务。以下从几个方面详细介绍:
CompletableFuture
可以避免阻塞主线程,提高程序的响应性能。CompletableFuture
并行执行这些小任务,最后合并结果,提高计算效率。CompletableFuture
实现异步调用,减少服务之间的等待时间。public static void main(String[] args) {\\n List<Integer> nums = Arrays.asList(1, 11,34,12, 23, 34,45, 16, 27, 38, 19, 10);\\n\\n List<CompletableFuture<Integer>> futures = nums.stream()\\n .map(value -> CompletableFuture.supplyAsync(() -> {\\n // 这里是每个异步任务要执行的操作,\\n return value*2;\\n }))\\n .collect(Collectors.toList());\\n\\n CompletableFuture<Integer> sumFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\\n .thenApplyAsync(v -> {\\n // 所有异步计算任务完成后,将它们的结果进行合并\\n int sum = futures.stream()\\n .mapToInt(CompletableFuture::join)\\n .sum();\\n return sum;\\n });\\n\\n int sum = sumFuture.join();\\n System.out.println(sum);\\n}\\n
\\nForkJoinPool 是基于工作窃取(Work-Stealing)算法实现的线程池,ForkJoinPool 中每个线程都有自己的工作队列,用于存储待执行的任务。当一个线程执行完自己的任务之后,会从其他线程的工作队列中窃取任务执行,以此来实现任务的动态均衡和线程的利用率最大化
\\nForkJoinPool 实现快排的代码
\\npublic class TestForkJoinPool extends RecursiveAction {\\n private int[] array;\\n private int left;\\n private int right;\\n\\n public TestForkJoinPool(int[] array, int left, int right) {\\n this.array = array;\\n this.left = left;\\n this.right = right;\\n }\\n\\n private int partition(int left, int right) {\\n\\n int pivot = array[right];\\n int i = left - 1;\\n for (int j = left; j < right; j++) {\\n if (array[j] <= pivot) {\\n i++;\\n // Swap array[i] and array[j]\\n int temp = array[i];\\n array[i] = array[j];\\n array[j] = temp;\\n }\\n }\\n // Swap array[i+1] and array[right] (or pivot)\\n int temp = array[i + 1];\\n array[i + 1] = array[right];\\n array[right] = temp;\\n return i + 1;\\n }\\n\\n @Override\\n protected void compute() {\\n if (left < right) {\\n int partitionIndex = partition(left, right);\\n // Parallelize the two subtasks\\n TestForkJoinPool leftTask = new TestForkJoinPool(array, left, partitionIndex - 1);\\n TestForkJoinPool rightTask = new TestForkJoinPool(array, partitionIndex + 1, right);\\n leftTask.fork();\\n rightTask.fork();\\n leftTask.join();\\n rightTask.join();\\n }\\n }\\n public static void TestForkJoinPool(int[] array) {\\n ForkJoinPool pool = new ForkJoinPool();\\n pool.invoke(new TestForkJoinPool(array, 0, array.length - 1));\\n }\\n public static void main(String[] args) {\\n int[] array = { 12, 35, 87, 26, 9, 28, 7 };\\n TestForkJoinPool(array);\\n for (int i : array) {\\n System.out.print(i + \\" \\");\\n }\\n }\\n}\\n
\\n掌握Java多线程开发需要理解线程基础、同步机制、线程协作等核心概念,同时要熟悉JUC工具包的使用。建议通过实际项目中的并发场景(如秒杀系统、批量处理等)加深理解。良好的并发程序设计需要平衡性能与线程安全,避免过度同步导致的性能问题。
\\n\\n","description":"前言 本篇文章,主要是列举了线程模块的核心知识和技术。内容分两个模块一个是简单介绍,第二是深入学习。深入学习部分都是推荐的博客文章。\\n\\n内容有点多,欢迎收藏,可当做java线程模块学习路线,以及面试突击。\\n\\njava线程核心知识及API\\n一、线程基础\\n1.1 线程生命周期\\n\\n新建状态(New):new Thread 此时线程对象已经被创建,但还没有开始运行。\\n\\n就绪状态(Runnable):调用start()方法后,线程进入就绪状态,可能还没有被分配到CPU时间片。\\n\\n运行状态(Running):线程获得CPU时间片并开始执行时,线程进入运行状态,执行…","guid":"https://juejin.cn/post/7487600796739534859","author":"提前退休的java猿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T08:16:48.140Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"千万级大表的优化技巧","url":"https://juejin.cn/post/7487517908935983115","content":"如果佬们觉得还有重要的API,欢迎留言补充
\\n
大表优化是一个老生常谈的话题,但随着业务规模的增长,总有人会“中招”。
\\n很多小伙伴的数据库在刚开始的时候表现良好,查询也很流畅,但一旦表中的数据量上了千万级,性能问题就开始浮现:查询慢、写入卡、分页拖沓、甚至偶尔直接宕机。
\\n这时大家可能会想,是不是数据库不行?是不是需要升级到更强的硬件?
\\n其实很多情况下,根本问题在于没做好优化。
\\n今天,我们就从问题本质讲起,逐步分析大表常见的性能瓶颈,以及如何一步步优化,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\n在搞优化之前,先搞清楚大表性能问题的根本原因。数据量大了,为什么数据库就慢了?
\\n大表的数据是存储在磁盘上的,数据库的查询通常会涉及到数据块的读取。
\\n当数据量很大时,单次查询可能需要从多个磁盘块中读取大量数据,磁盘的读写速度会直接限制查询性能。
\\n假设有一张订单表orders
,里面存了5000万条数据,你想要查询某个用户的最近10条订单:
SELECT * FROM orders WHERE user_id = 123 ORDER BY order_time DESC LIMIT 10;\\n
\\n如果没有索引,数据库会扫描整个表的所有数据,再进行排序,性能肯定会拉胯。
\\n如果表的查询没有命中索引,数据库会进行全表扫描(Full Table Scan),也就是把表里的所有数据逐行读一遍。
\\n这种操作在千万级别的数据下非常消耗资源,性能会急剧下降。
\\n比如你在查询时写了这样的条件:
\\nSELECT * FROM orders WHERE DATE(order_time) = \'2023-01-01\';\\n
\\n这里用了DATE()
函数,数据库需要对所有记录的order_time
字段进行计算,导致索引失效。
分页查询是大表中很常见的场景,但深度分页(比如第100页之后)会导致性能问题。
\\n即使你只需要10条数据,但数据库仍然需要先扫描出前面所有的记录。
\\n查询第1000页的10条数据:
\\nSELECT * FROM orders ORDER BY order_time DESC LIMIT 9990, 10;\\n
\\n这条SQL实际上是让数据库先取出前9990条数据,然后丢掉,再返回后面的10条。
\\n随着页码的增加,查询的性能会越来越差。
\\n在高并发场景下,多个线程同时对同一张表进行增删改查操作,会导致行锁或表锁的争用,进而影响性能。
\\n性能优化的本质是减少不必要的IO、计算和锁竞争,目标是让数据库尽量少做“无用功”。
\\n优化的总体思路可以总结为以下几点:
\\n接下来,我们逐一展开。
\\n表结构是数据库性能优化的基础,设计不合理的表结构会导致后续的查询和存储性能问题。
\\n字段的类型决定了存储的大小和查询的性能。
\\nINT
的不要用BIGINT
。VARCHAR(100)
的不要用TEXT
。TIMESTAMP
或DATETIME
,不要用CHAR
或VARCHAR
来存时间。-- 不推荐\\nCREATETABLE orders (\\n idBIGINT,\\n user_id BIGINT,\\n order_status VARCHAR(255),\\n remarks TEXT\\n);\\n\\n-- 优化后\\nCREATETABLE orders (\\n idBIGINT,\\n user_id INTUNSIGNED,\\n order_status TINYINT, -- 状态用枚举表示\\n remarks VARCHAR(500) -- 限制最大长度\\n);\\n
\\n这样可以节省存储空间,查询时也更高效。
\\n如果对表设计比较感兴趣,可以看看我之前的另一篇文章《表设计的18条军规》,里面有详细的介绍。
\\n当表中字段过多,某些字段并不是经常查询的,可以将表按照业务逻辑拆分为多个小表。
\\n示例: 将订单表分为两个表:orders_basic
和 orders_details
。
-- 基本信息表\\nCREATETABLE orders_basic (\\n idBIGINT PRIMARY KEY,\\n user_id INTUNSIGNED,\\n order_time TIMESTAMP\\n);\\n\\n-- 详情表\\nCREATETABLE orders_details (\\n idBIGINT PRIMARY KEY,\\n remarks VARCHAR(500),\\n shipping_address VARCHAR(255)\\n);\\n
\\n当单表的数据量过大时,可以按一定规则拆分到多张表中。
\\n示例: 假设我们按用户ID对订单表进行水平拆分:
\\norders_0 -- 存user_id % 2 = 0的订单\\norders_1 -- 存user_id % 2 = 1的订单\\n
\\n拆分后每张表的数据量大幅减少,查询性能会显著提升。
\\n索引是数据库性能优化的“第一杀器”,但很多人对索引的使用并不熟悉,导致性能不升反降。
\\n为高频查询的字段创建索引,比如主键、外键、查询条件字段。
\\nCREATE INDEX idx_user_id_order_time ON orders (user_id, order_time DESC);\\n
\\n上面的复合索引可以同时加速user_id
和order_time
的查询。
别对索引字段使用函数或运算。
\\n错误:
SELECT * FROM orders WHERE DATE(order_time) = \'2023-01-01\';\\n
\\n优化:
\\nSELECT * FROM orders WHERE order_time >= \'2023-01-01 00:00:00\'\\n AND order_time < \'2023-01-02 00:00:00\';\\n
\\n注意隐式类型转换。
\\n错误:
SELECT * FROM orders WHERE user_id = \'123\';\\n
\\n优化:
\\nSELECT * FROM orders WHERE user_id = 123;\\n
\\n如果对索引失效问题比较感兴趣,可以看看我之前的另一篇文章《聊聊索引失效的10种场景,太坑了》,里面有详细的介绍。
\\n只查询需要的字段,避免SELECT *
。
-- 错误\\nSELECT * FROM orders WHERE user_id = 123;\\n\\n-- 优化\\nSELECT id, order_time FROM orders WHERE user_id = 123;\\n
\\n深度分页时,使用“延迟游标”的方式避免扫描过多数据。
\\n-- 深分页(性能较差)\\nSELECT * FROM orders ORDER BY order_time DESC LIMIT 9990, 10;\\n\\n-- 优化:使用游标\\nSELECT * FROM orders WHERE order_time < \'2023-01-01 12:00:00\'\\n ORDER BY order_time DESC LIMIT 10;\\n
\\n如果对SQL优化比较感兴趣,可以看看我之前的另一篇文章《聊聊sql优化的15个小技巧》,里面有详细的介绍。
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n当单表拆分后仍无法满足性能需求,可以通过分库分表将数据分散到多个数据库中。
\\n如果对分库分表比较感兴趣,可以看看我之前的另一篇文章《阿里二面:为什么要分库分表?》,里面有详细的介绍。
\\n对高频查询的数据可以存储到Redis中,减少对数据库的直接访问。
\\n// 从缓存读取数据\\nString result = redis.get(\\"orders:user:123\\");\\nif (result == null) {\\n result = database.query(\\"SELECT * FROM orders WHERE user_id = 123\\");\\n redis.set(\\"orders:user:123\\", result, 3600); // 设置缓存1小时\\n}\\n
\\n高并发写入时,可以将写操作放入消息队列(如Kafka),然后异步批量写入数据库,减轻数据库压力。
\\n如果对Kafka的一些问题比较感兴趣,可以看看我之前的另一篇文章《我用kafka两年踩过的一些非比寻常的坑》,里面有详细的介绍。
\\n某电商系统的订单表存储了5000万条记录,用户查询订单详情时,页面加载时间超过10秒。
\\nuser_id
和order_time
创建索引。search_after
代替LIMIT
深分页。大表性能优化是一个系统性工程,需要从表结构、索引、SQL到架构设计全方位考虑。
\\n千万级别的数据量看似庞大,但通过合理的拆分、索引设计和缓存策略,可以让数据库轻松应对。
\\n最重要的是,根据业务特点选择合适的优化策略,切勿盲目追求“高大上”的方案。
\\n希望这些经验能帮到你!
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n大表优化是一个老生常谈的话题,但随着业务规模的增长,总有人会“中招”。\\n\\n很多小伙伴的数据库在刚开始的时候表现良好,查询也很流畅,但一旦表中的数据量上了千万级,性能问题就开始浮现:查询慢、写入卡、分页拖沓、甚至偶尔直接宕机。\\n\\n这时大家可能会想,是不是数据库不行?是不是需要升级到更强的硬件?\\n\\n其实很多情况下,根本问题在于没做好优化。\\n\\n今天,我们就从问题本质讲起,逐步分析大表常见的性能瓶颈,以及如何一步步优化,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan…","guid":"https://juejin.cn/post/7487517908935983115","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T01:54:39.158Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"springboot-tomcat 线程处理web接口解读","url":"https://juejin.cn/post/7487219933127786505","content":"在 Spring Boot 项目中,http-nio-18882-exec-216
这样的线程在处理完 HTTP 请求后,线程本身不会被销毁,但线程的局部数据会被清除。以下是详细说明:
ThreadPoolExecutor
管理这些线程,线程在处理完请求后会被放回线程池(而不是销毁),等待下一个请求。http-nio-18882-exec-216
)可能会被多次用于处理不同的 HTTP 请求。keepAliveTime=60秒
),Tomcat 可能会回收该线程(减少到 min-spare-threads
数量)。HttpServletRequest
、HttpServletResponse
)会在请求结束后被销毁。Model
、@RequestAttribute
)也会随请求结束而清除。ThreadLocal
存储请求相关数据,且未手动清理,可能会导致内存泄漏或跨请求数据污染。\\nprivate static final ThreadLocal<User> userHolder = new ThreadLocal<>();\\n\\n@GetMapping(\\"/user\\")\\npublic String getUser() {\\n userHolder.set(new User(\\"Alice\\")); // 未清理会导致后续请求读到脏数据\\n return \\"success\\";\\n}\\n
\\nThreadLocal
:\\n@Component\\npublic class ThreadLocalCleanupFilter implements Filter {\\n @Override\\n public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {\\n try {\\n chain.doFilter(request, response);\\n } finally {\\n userHolder.remove(); // 确保清除\\n }\\n }\\n}\\n
\\nThreadLocal
的残留数据),但依赖这一行为并不安全,应主动清理。通过日志观察线程是否被复用:
\\n@RestController\\npublic class ThreadDebugController {\\n @GetMapping(\\"/thread\\")\\n public String logThread() {\\n String threadName = Thread.currentThread().getName();\\n System.out.println(\\"当前线程: \\" + threadName);\\n return threadName;\\n }\\n}\\n
\\n多次请求该接口,可能会看到同一个线程名(如 http-nio-8080-exec-1
)反复出现。
项目 | 是否会被清理? | 注意事项 |
---|---|---|
线程本身 | ❌ 放回线程池复用 | 空闲超时后可能被回收 |
HttpServletRequest | ✅ 请求结束即销毁 | 无需手动干预 |
ThreadLocal 数据 | ❌ 需手动清理 | 不清理会导致内存泄漏或数据污染 |
Spring 的模型数据 | ✅ 随请求结束自动清理 | 依赖框架机制 |
ThreadLocal
:优先使用请求作用域(@RequestAttribute
)或 Spring 的上下文(如 RequestContextHolder
)。@Async
或自定义线程池任务中,通过 try-finally
确保清理:\\npublic void asyncTask() {\\n try {\\n // 业务逻辑\\n } finally {\\n userHolder.remove(); // 清理 ThreadLocal\\n }\\n}\\n
\\njstack
或 VisualVM 检查长时间运行的线程是否堆积。如果有特定场景(如异步处理、WebFlux),线程模型会有所不同,但 Tomcat 的 HTTP 线程行为符合上述规则。
","description":"在 Spring Boot 项目中,http-nio-18882-exec-216 这样的线程在处理完 HTTP 请求后,线程本身不会被销毁,但线程的局部数据会被清除。以下是详细说明: 1. 线程本身的生命周期\\n线程池管理:Tomcat 使用 ThreadPoolExecutor 管理这些线程,线程在处理完请求后会被放回线程池(而不是销毁),等待下一个请求。\\n线程复用:同一个线程(如 http-nio-18882-exec-216)可能会被多次用于处理不同的 HTTP 请求。\\n空闲回收:如果线程长时间空闲(默认超过 keepAliveTime=60秒…","guid":"https://juejin.cn/post/7487219933127786505","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T01:53:37.060Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Look My Eyes 最新IDEA快速搭建Java Web工程的两种方式","url":"https://juejin.cn/post/7487131921715494939","content":"创建Java Web工程属于入门的知识,网上的文章也很多。本次基于最新的IDEA来阐述如何快速创建Java Web工程,并部署到本地最终通过浏览器访问,希望通过本篇文章让大家对Java Web有个最直观的认识。
\\n通过本篇文章,你将了解到:
\\n\\n\\n\\n
\\n- 什么是Java Web?
\\n- Java项目转为Java Web
\\n- 通过Maven创建Java Web
\\n- 如何部署服务到Tomcat中?
\\n- 总结
\\n
用三张图说明:
\\n我们手机上的App各式各样,他们的共同点之一是随时能够刷新内容,靠的就是每个App都接上了互联网,可以从网上获取最新内容。
\\n对应到代码术语里的是:前端<---\x3e服务端交互。
\\n当前最常用的交互协议是TCP/IP,使用的应用层协议是HTTP协议,如下:
前端作为Http Client,服务端作为Http Server,共同为用户提供服务。
\\n继续演变如下图:
客户端引入Http SDK,服务端引用Http SDK,通信交由该SDK负责。
\\n对于服务端来说,只需要专注于写具体的业务逻辑代码,并根据计算得的数据渲染出前端页面,最后返回给客户端,我们通常称此类的应用为Web应用。
而Java Web 是指使用 Java 技术构建 Web 应用程序的开发领域,它通常包括如下内容:
\\n\\n\\n\\n
\\n- Servlet:用于处理网络请求和生成响应
\\n- JSP:用于动态生成网页的内容
\\n- Tomcat:用于运行Servlet并提供Http 基础服务(Http Server)
\\n- 数据库:用于持久化数据
\\n
因此一个最简单的Web应用运行流程为:
\\n\\n\\n接收客户端请求--\x3e处理请求--\x3e读写数据库--\x3e返回处理后的数据--\x3e渲染到页面--\x3e生成响应给客户端
\\n
接下来我们探索如何快速创建Java Web项目,主要有两种方式:
\\n\\n\\n\\n
\\n- Java项目转为Java Web
\\n- 通过Maven创建Java Web
\\n
IDEA 版本:IntelliJ IDEA 2024.3.2 (Ultimate Edition)
\\n有一个入口函数,就是熟悉的Main函数,当我们运行该函数就可以看到打印,如此一个最简单的Java工程搭建完成。\\n
\\n3. 引入Web依赖
\\n上述步骤仅仅只是创建了纯Java项目,现在需要使它变为Java Web项目,需要添加对应的依赖。
\\n打开项目结构:\\\\
选择添加Web模块:
\\n选择之后点确定即可。
\\n此时再来查看项目的结构。
\\n
在web目录上右键,选择新建JSP文件:
\\n新建后修改内容:
\\n可以看出,JSP和HTML结构差不多,只是它里面可以插入Java代码进行动态修改界面。
\\n编辑运行配置:
\\n选择本地的Tomcat:
\\n找到之前下载的Tomcat,设置为Server,其他可暂时不用动。
\\n可以看到右下角有个提示,点击fix按钮。
\\n跳转后选择新建artifacts:
\\n回到工程后就会发现配置构建多了Tomcat选项:
\\n此时我们直接点击运行按钮,项目就会部署到Tomcat里,自动跳转到浏览器访问:
\\n如此一来,最简单的没有任何逻辑的Java Web搭建并运行成功,后续就可以完善数据的输入输出,完善页面的显示。
\\n上面的步骤还是有点繁琐,现在我们的目的很明确,直接创建Java Web项目,而不是通过Java项目再转Java Web项目。
\\n确定后查看项目结构:
\\n此时运行项目:
\\n通过比较创建Java Web的两种方式,第二种方式便捷了许多,推荐大家使用第二种方式创建Java Web项目。
\\n上述我们都是直接点击IDEA里的运行按钮直接部署,观察项目编译产物:
\\n可以看出,此时的target目录并没有在Tomcat目录下,那如何把target放在Tomcat目录下呢?\\n分两步:
\\n构建之后查看target目录,多了一个.war包。
\\n将War包放到Tomcat目录下
\\n找到Tomcat安装目录下的webapps,比如我的目录:/usr/local/apache-tomcat-9.0.96/webapps
\\n将war包拷贝到该目录下。
启动Tomcat
\\n找到Tomcat安装目录下的,比如我的目录:/usr/local/apache-tomcat-9.0.96/bin\\n执行命令启动tomcat(可能需要管理员权限):
\\n\\nmac/linux 下命令:./startup.sh
\\n
\\nwindows 下命令:./startup.bat
启动后查看webapps目录:
\\n可以看出,.war包被自动解压了。
\\n</div>\\n注意此时的路径是webapps下对应的.war包解压后的目录名。 \\n
\\n以上我们实现了最简单的Java Web项目的创建到部署的功能,接下来我们会往里面添加实现逻辑,比如接收客户端的传值、接收客户端拉取数据等功能,让大家逐渐了解Java Web开发流程。
\\n下篇将重点分析Servlet的编写、运行过程,敬请期待。
\\n如果您觉得有帮助,记得一键三连哦~ 感谢感谢再感谢~
项目地址 持续维护中。
如下图所示,\'240610708\'
和 \'QNKCDZO\'
是两个完全不同的字符串,它们的 MD5 哈希值自然也不相同。可为什么明明不同,PHP 还会认为这两个哈希值相等呢?更离谱的是,从 2004 年底的 PHP 4.3.10 版本开始,这个“问题”至今一直存在,所有后续版本都会认为它们是相等的!
难道是 PHP 又出 bug 了?还是这背后另有隐情?让我们一探究竟!
\\n这看起来的确像是 PHP 的一个 bug。但实际上,这只是 ==
弱类型比较运算符带来的副作用。这个副作用的危害在于,只要还在用 ==
进行哈希值比较,就无疑会埋下安全隐患!
我们先来看看这两个本不相同的 MD5 哈希值有什么特点,
\\n0e462097431906509019562988736854\\n0e830400451993494058024219903391\\n
\\n不难发现,这两个哈希值都以 0e
开头,并且 0e
之后全是数字。
在 PHP 中,对于形如 \'0e[0-9]+\'
的字符串,PHP 会尝试将它解析为用科学计数法表示的数字,即:
0e462097431906509019562988736854\\n => 0 × 10的462097431906509019562988736854次方 = 0\\n \\n0e830400451993494058024219903391\\n => 0 × 10的830400451993494058024219903391次方 = 0\\n
\\n由于 0 乘以任何数都等于 0,所以实际比较的是 0 == 0
吗?结果当然是 true
,这才导致了这个看似 bug 的现象。
再从 PHP 源代码的角度来看,如果弱类型比较运算符 ==
两边的值都是字符串,那么会执行 zendi_smart_strcmp()
这个函数。而这个函数最初要做的,就是试图通过 is_numeric_str_function()
将字符串转换成对应的数字,成功转换成数字后,再对数字进行比较。
对于形如 \'0e[0-9]+\'
的科学计数法字符串,无论 e
之后是什么,都会被转换成浮点数 0,0 自然等于 0 喽,所以 ==
比较的结果为 true
。
了解了不同的 MD5 哈希值会被 PHP 中的 ==
判定为相同的原理后,就再来看看这个问题带来的危害吧。
显然,这个问题可能导致严重的安全漏洞:如果某个系统使用 md5($password . $salt) == $stored_hash
来验证用户身份,那么攻击者就可能找到另一个字符串,即使不是正确的密码,但加盐后的 MD5 哈希值满足 \'0e[0-9]+\'
的条件,而刚好来自数据库中的 $stored_hash
也是 0e
开头之后全是数字,这样的话攻击者就可以绕过密码验证了。
那如何堵住这个安全漏洞呢?
\\n既然漏洞是由弱类型比较运算符 ==
引起的,那最简单的办法就是改用 ===
进行严格比较。而更好的方法是,使用 PHP 5.6+ 提供的专门用于 哈希值比较 的安全函数 hash_equals()
。该函数还能通过牺牲性能来防止时序攻击。其源代码的注释中写道:这是安全性敏感的代码,千万别为了追求速度去优化啊!
\\n\\n时序攻击(timing attack)是一种通过测量代码执行时间的微小差异来推测机密信息的攻击方式,比如对于普通的
\\n===
比较,其执行时间会因不一致的字符的出现位置的不同而不同。例如,相较于两个字符串的第一个字符就不相同,前面的字符全部一致,只有最后一个字符不同,后者的运算时间应该更长。
而更安全的做法是改用 PHP 5.5+ 提供的 password_hash()
函数来生成哈希值,并搭配 password_verify()
函数进行校验,而不要使用 MD5 进行安全相关的哈希计算。
总之,MD5 早已不再安全,PHP 的弱类型比较 ==
又让这个问题雪上加霜。在实际开发中,我们应该避免使用 MD5 进行身份验证。
如果对此此项目还不了解可以先看下面两篇文章
\\n\\n\\n正如前文所讲,开始分支开发了,规划中main对应开源主分支(功能有限),pro对应标准付费分支,当然未来可能还会有pro-max🥳,demo对应在线体验分支,目前是基于pro增加一些限制和不一样的东西。
\\n所以在线体验版本并不是开源版,会有些差别。
\\n在开始之前还是先回顾一下最近做的事情吧!
\\n最近除了在main分支做优化更新外,还要在pro分支做规划中的事情,说起来是这么概括就够了,但是其实事情很多,不仅是前端后端项目的迭代优化,更重要还有除编程外的其他事情。
\\n其中花费时间最多的是在做开放在线体验上,因为这并不是简简单单的将服务部署在公网上这么简单的事,不过回头来看好像也就是这么简单的事情😂
\\n因为放在公网上总要考虑很多安全性上的问题,记得之前买的服务器因为配置了常用安全组,并设置了简单的密码导致服务数据全没了,收到了“比特币换数据的要挟”,不过还好那台服务器就是自己随便玩玩的,没什么重要数据。除了数据问题之外,更有甚者被黑客攻击后被作为挖矿服务器,频繁高CPU,并收到云厂商的告警通知法律法规发现挖矿脚本要本人确定,挺麻烦的。自那之后,在这方面就谨慎起来了,不管是开发测试,密码都尽可能设置的复杂一些🥲怕了怕了
\\n完善表单校验,对于普通的字段的校验相对简单,对于系统中如复合条件的校验也做了加强。
\\n左/逻辑判断/右,递归等等。
\\n抽象了发布组件,新增批量发布组件
\\n事件数据存储在ElasticSearch中,相关代码也只写在pro分支。
\\n这部分重点在于ES索引的设计与检索数据的实现。
\\n如下是目前事件数据页,支持复杂多条件查询
\\n因为决策流配置还未完成,在面对多策略结果时,如何综合所有结果还在考虑中,所以目前所有结果都是“通过”。
\\n同样高级搜索这里做了校验
\\n点击行即可看事件详细数据
\\n字段-指标-策略
\\n上篇预热在线体验的文章也有展示
\\n项目使用了云效的devops,采用Docker镜像部署方案,详细的如Dockerfile和docker-compose.yaml可以另开一篇文章讲。
\\n要开放在线体验势必要考虑安全问题,包含服务器、中间件、账号、接口等等。
\\n后端项目也为一些接口增加了权限校验,在线体验一定会限制的!
\\n自己写的当然最为熟悉,对于目前有哪些问题,下一步怎么做,我是有比较清晰的规划的,但还是那句话,没有那么多时间精力啊🤪
\\n除了大方向的功能/流程/交互方式/表/接口等等,更细节的如使用什么组件,那个方法需要优化,大概什么样式都是考虑中的,这些平常都记录在我的todolist中,每完成一项就删除一项,或者迁移到产品说明书/备忘录中。
\\n关注公众号,私信或是加好友后,说明“在线体验”就好。
\\n我会分配一个专有账号,此账号会有权限限制(这当然是必须的,主要是一些查询和新增的权限,修改、删除等等是没有),而且账号默认有效期只有7天。
\\n如果人员过多,我会设置几个公用的账号(因为设置了同账号不允许多地登录,所以有挤掉的情况😅)。
\\n毕竟是自费服务器,还是要限制一下。
\\n当然这些只是此刻的规划,未来可能需要申请填写个人或公司信息、目的等等,也不一定,也有可能什么时候就关闭在线体验也不一定🙂↔️
\\n关于反馈问题的渠道,当然私信和私聊都是可以的。
\\n群聊的话,一直都没有,但如果开放在线体验了,想发一些公告,如:“维护中不可访问”,那么群聊就很有必要了。关于这个请看下面“交流群”。
\\n关于系统如何工作的前面有很多文章了,不想在这里赘述了,未来尽量去补充详细的官方文档(产品、技术),敬请期待吧。
\\n\\n1、接口
\\n每配置一个接入就有三个接口可用,/test、/sync、/async如下,test需要登录,sync适合需要决策的场景,async不会决策。
\\n2、参数
\\n接口参数就是下图配置的这些
\\n在线体验中配置了已经一个策略,发送时注意必须的几个字段就行,如下
\\n{
\\n\\"appCode\\": \\"app\\",
\\n\\"policySetCode\\": \\"appLogin\\",
\\n\\"eventTime\\": \\"{{$date.now}}\\",
\\n\\"transSerialNo\\": \\"{% mock \'uuid\' %}\\",
\\n\\"eventCode\\": \\"event1234\\"
\\n}
平常我都是使用下面这种方式发的
\\ncurl参考,mock的数据
\\ncurl --location --request POST \'http://127.0.0.1:8081/decision/public/sync\'
\\n--header \'Content-Type: application/json\'
\\n--data-raw \'{
\\n\\"appCode\\": \\"app\\",
\\n\\"policySetCode\\": \\"appLogin\\",
\\n\\"eventTime\\": \\"2025-03-29 23:31:31\\",
\\n\\"transTime\\": \\"2025-03-29 23:31:31\\",
\\n\\"transAmount\\": 1098.52734,
\\n\\"transSerialNo\\": \\"9b926aae-3379-4845-8e91-b9c5913dc118\\",
\\n\\"payerAccount\\": \\"123456\\",
\\n\\"payeeAccount\\": \\"654321\\",
\\n\\"payeeName\\": \\"薛娜\\",
\\n\\"payerName\\": \\"董军\\",
\\n\\"payeeType\\": \\"反外合年他\\",
\\n\\"payerType\\": \\"龙和民角\\",
\\n\\"payeeRiskRating\\": \\"HIGH\\",
\\n\\"payerRiskRating\\": \\"HIGH\\",
\\n\\"payeeBankName\\": \\"ABC Bank\\",
\\n\\"payerBankName\\": \\"XYZ Bank\\",
\\n\\"payeeAddress\\": \\"罗平县 福建省 上海市\\",
\\n\\"payerAddress\\": \\"改则县 广西壮族自治区 泰州市\\",
\\n\\"payeePhoneNumber\\": \\"18104207857\\",
\\n\\"payerPhoneNumber\\": \\"18654274834\\",
\\n\\"payeeIDNumber\\": \\"640000200005293830\\",
\\n\\"payerIDNumber\\": \\"630000197905127364\\",
\\n\\"payeeIDCountryRegion\\": \\"US\\",
\\n\\"payerIDCountryRegion\\": \\"US\\",
\\n\\"ip\\": \\"134.172.96.72\\",
\\n\\"lonAndLat\\": \\"4.514,134.44342\\",
\\n\\"account\\": \\"019741048\\",
\\n\\"deviceId\\": \\"e\\",
\\n\\"bindDevice\\": \\"true\\",
\\n\\"eventCode\\": \\"event1234\\"
\\n}\'
3、响应
\\n响应结果自己试一下吧。
\\n服务器日志大概如下,目前影响耗时也还好。
\\n从决策的角度来看,这一步已经结束了,接下来就是调用方如何使用决策结果了,当然反馈机制也是需要的。
\\n4、验证
\\n参考前面的事件数据查看是否符合规则配置,是否按照策略运行。
\\n交流群其实我是比较推荐qq频道的,因为几点
\\n成员数量上限高
\\n无论加入时间,历史消息可见
\\n不仅仅是聊天,还有帖子
\\n等等
\\n交流群可作为官方通知,因为公众号是有消息限制的,所以这种交流群是最好的,另外可以听到大家的声音,有利于我们的共同成长
\\n至于共同开发/运营,也还在考虑中。
\\n不管是开放在线体验还是交流群都是需要精力去运营的,个人精力实在有限,我也想把一天掰开来用,还想拔几根毫毛生成几个分身呢🥹但实在是没空啊!
\\n开源版大概就是修修补补了,pro应该会成为未来主要重心。
\\n全局
\\n规则引擎可以应用于哪些系统,用户画像、触达、风控、推荐、监控...
\\n\\n\\n\\n\\n策略/规则篇
\\n\\n\\n\\n指标篇
\\n风控系统指标计算/特征提取分析与实现01,Redis、Zset、模版方法
\\n\\n\\n\\n\\n数据篇
\\n风控系统之数据服务,名单、标签、IP、设备、地理信息、征信等
\\n","description":"项目地址:github.com/wnhyang/coo… 前言\\n\\n如果对此此项目还不了解可以先看下面两篇文章\\n\\n基于规则引擎的风控决策系统介绍与演示\\n\\n风控系统之规则条件&操作增强,名单&标签&消息模版管理\\n\\n正如前文所讲,开始分支开发了,规划中main对应开源主分支(功能有限),pro对应标准付费分支,当然未来可能还会有pro-max🥳,demo对应在线体验分支,目前是基于pro增加一些限制和不一样的东西。\\n\\n所以在线体验版本并不是开源版,会有些差别。\\n\\n最近\\n\\n在开始之前还是先回顾一下最近做的事情吧!\\n\\n最近除了在main分支做优化更新外…","guid":"https://juejin.cn/post/7486788421933203468","author":"无奈何杨","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-30T06:02:38.220Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d1c21bc43d6e48c6a09516941a3b1203~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=m7d9sDA1p5Vg19erqm8J%2F3ZAdEw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5353406f4476463c86f646c7d61c2a8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=sOKcZi9id4hNOlItA%2BbuEqTwL98%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/933b43ae3952471887bae91e41e8fe72~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=y1CqloAEFA61okyWV%2FRGhl%2FkDKc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a7a1a25b3c874fa4a1577d289272d305~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=bj0Uz%2B9royJ1WgptaOES6d8Seto%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9904ae53ef9c454cb2b410e70cbaea3b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=0ZZpy%2Btk7PwbFXZTAXLY7yPEKBM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe01c2920cb949e6ae879b60957a7a11~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=DVMA9U4ROTcVBY6e0ddxl8jKVx8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4f7c1d8daac8462da600933a397c8be1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=w%2BIDzWqv0mti2uZ75O7x%2F6FmV2k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/38dcfe1ae9ff46919737bd12d937af4c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=BCJbMIpPYYV5oDmSTLuzixuMxoQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fc1d0cad57d240eba2b0545a6a95bcc7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=N60XPeDl%2Fgh8LMyEXa2Afuu7PGk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d6106dbe01784638afae997210791868~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=Z31EpP2PwhMd6HCREo2%2FbJaTPjg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/582656c6c0234ecfbba2c0c704b66713~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=kIwKqHYS9yEcKD6zMLud5Efzkw4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e175ca1b3bcb4888979be0289cb96195~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=JIfzISYvbr232D9Vtv1RaSo2k7w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbef711680a740118db3fe8098e831c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=CV%2FJJhrdEbjXLeV2UT9ipCv50wo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1006fb8fe5fc46838fb9c588796842b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=%2B8DxPjNgJ1v3FqZSVl0bYG7otzA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc638d6bc06d4dcaae3c5d39f2e5632d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=1WLh03FDE9dvDw3Jj5CST9dmMK4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b34c74b1c804478780315031e1b481a9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=q%2BjXF5sRN2ZPeeF3cSIb3OhFNVg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/960d0a8f8050446a88ab0a52062611de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5peg5aWI5L2V5p2o:q75.awebp?rk3s=f64ab15b&x-expires=1743919358&x-signature=8jZMagotWEc%2BCZzIwoObfWTDyWI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"双Token无感刷新方案","url":"https://juejin.cn/post/7486782063422717962","content":"双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。
\\n最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token
的过期时间,前端在申请后端登录接口成功之后,会返回一个token
值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token
值,但是这个token
的有效期应该设置为多少?
token
泄露,攻击者可长期冒充用户身份,直到token
过期,服务端无法限制其访问用户数据所以有没有两者都兼顾的方案呢?
\\n传统的token
方案要么频繁要求用户重新登录,要么面临长期有效的安全风险
但是双token
无感刷新机制,通过组合设计,在保证安全性的情况下,实现无感知的认证续期
access_token
:访问令牌,有效期一般设置为15~30分钟,主要用于对后端请求API的交互refresh_token
:刷新令牌,一般设置为一个星期到一个月,主要用于获取新的access_token
用户登录之后,后端返回access_token
和refresh_token
响应给前端,前端将两个token
存储在用户本地
在用户端发起前端请求,访问后端接口,在请求头中携带上access_token
前端会对access_token
的过期时间进行检测,当access_token
过期前一分钟,前端通过refresh_token
向后端发起请求,后端判断refresh_token是否有效,有效则重新获取新的access_token
,返回给前端替换掉之前的access_token
存储在用户本地,无效则要求用户重新认证
这样的话对于用户而言token
的刷新是无感知的,不会影响用户体验,只有当refresh_token
失效之后,才需要用户重新进行登录认证,同时,后端可以通过对用户refresh_token
的管理来限制用户对后端接口的请求,大大提高了安全性
有了这个思路,写代码就简单了
\\n@Service\\npublic class LoginServiceImpl implements LoginService {\\n\\n @Autowired\\n private JwtUtils jwtUtils;\\n\\n // token过期时间\\n private static final Integer TOKEN_EXPIRE_DAYS =5;\\n // token续期时间\\n\\n private static final Integer TOKEN_RENEWAL_MINUTE =15;\\n\\n @Override\\n public boolean verify(String refresh_token) {\\n Long uid = jwtUtils.getUidOrNull(refresh_token);\\n if (Objects.isNull(uid)) {\\n return false;\\n }\\n String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN,uid);\\n String realToken = RedisUtils.getStr(key);\\n return Objects.equals(refresh_token, realToken);\\n }\\n\\n @Override\\n public void renewalTokenIfNecessary(String refresh_token) {\\n Long uid = jwtUtils.getUidOrNull(refresh_token);\\n if (Objects.isNull(uid)) {\\n return;\\n }\\n String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);\\n long expireSeconds = RedisUtils.getExpire(refresh_key, TimeUnit.SECONDS);\\n if (expireSeconds == -2) { // key不存在,refresh_token已过期\\n return;\\n }\\n String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);\\n RedisUtils.expire(access_key, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);\\n }\\n\\n @Override\\n @Transactional(rollbackFor = Exception.class)\\n @RedissonLock(key = \\"#uid\\")\\n public LoginTokenResponse login(Long uid) {\\n String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);\\n String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);\\n String refresh_token = RedisUtils.getStr(refresh_key);\\n String access_token;\\n if (StrUtil.isNotBlank(refresh_token)) { //刷新令牌不为空\\n access_token = jwtUtils.createToken(uid);\\n RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);\\n return LoginTokenResponse.builder()\\n .refresh_token(refresh_token).access_token(access_token)\\n .build();\\n }\\n refresh_token = jwtUtils.createToken(uid);\\n RedisUtils.set(refresh_key, refresh_token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);\\n access_token = jwtUtils.createToken(uid);\\n RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);\\n return LoginTokenResponse.builder()\\n .refresh_token(refresh_token).access_token(access_token)\\n .build();\\n }\\n}}\\n
\\n双Token机制并没有从根本上解决安全性的问题,它只是尝试通过改进设计,优化用户体验,全面的安全策略需要多层防护,分别针对不同类型的威胁和风险,而不仅仅依赖于Token的管理方式或数量
\\n安全是一个持续对抗的过程,关键在于提高攻击者的成本,而非追求绝对防御。
\\n\\"完美的认证方案不存在,但聪明的权衡永远存在。\\"
\\n本笔者水平有限,望各位海涵
\\n如果文章中有不对的地方,欢迎大家指正。
","description":"提醒一下 双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。\\n\\ntoken有效期设置问题\\n\\n最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token的过期时间,前端在申请后端登录接口成功之后,会返回一个token值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token值,但是这个token的有效期应该设置为多少?\\n\\n如果设置的太短,比如1小时,那么用户一小时之后。再访问其他接口,需要再次重新登录…","guid":"https://juejin.cn/post/7486782063422717962","author":"昔年种柳","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-29T16:00:19.083Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dcf81ecf407140e1a23603f026eca2f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5piU5bm056eN5p-z:q75.awebp?rk3s=f64ab15b&x-expires=1744014441&x-signature=zT3g%2FZ4Hb248c6pZvVQuauSUvBs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2521436c7cad45ca81b80454662bd7dd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5piU5bm056eN5p-z:q75.awebp?rk3s=f64ab15b&x-expires=1744014441&x-signature=d2JbJASC4pQMNfjVtPJXH3WPbbU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9527fa24fcba4789be48fa7e21c54f64~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5piU5bm056eN5p-z:q75.awebp?rk3s=f64ab15b&x-expires=1744014441&x-signature=c3DYxxIeQFT5TKSfhir%2F%2B1XzpGo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"程序员必看:两个思想优化90%的代码","url":"https://juejin.cn/post/7486694184407138339","content":"在软件开发过程中,代码的可读性和可维护性往往是衡量代码质量的重要指标。本文将介绍两个能够显著提升代码质量的设计原则:组合函数模式(Composed Method Pattern)和抽象层次一致性原则(Single Level of Abstraction Principle, SLAP),并通过实例说明如何在实际开发中应用这些原则。
\\n组合函数模式最早由 Kent Beck 在《Smalltalk Best Practice Patterns》一书中提出。这是一个简单易懂且实用的编程原则,能够对代码的可读性和可维护性产生立竿见影的效果。
\\n组合函数模式要求:
\\n这种模式有助于保持代码精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,指向各自的私有函数,而具体内容则在私有函数中实现。
\\n不良实践:
\\npublic void processOrder(Order order) {\\n // 验证订单\\n if (order == null) {\\n throw new IllegalArgumentException(\\"Order cannot be null\\");\\n }\\n if (order.getItems().isEmpty()) {\\n throw new IllegalArgumentException(\\"Order must contain at least one item\\");\\n }\\n\\n // 计算总价\\n double total = 0;\\n for (OrderItem item : order.getItems()) {\\n total += item.getPrice() * item.getQuantity();\\n }\\n\\n // 应用折扣\\n if (total > 1000) {\\n total *= 0.9; // 10% 折扣\\n } else if (total > 500) {\\n total *= 0.95; // 5% 折扣\\n }\\n\\n // 更新订单状态\\n order.setTotal(total);\\n order.setStatus(OrderStatus.PROCESSED);\\n orderRepository.save(order);\\n\\n // 发送确认邮件\\n String message = \\"Your order #\\" + order.getId() + \\" has been processed. Total: $\\" + total;\\n emailService.sendEmail(order.getCustomerEmail(), \\"Order Confirmation\\", message);\\n}\\n
\\n良好实践(应用组合函数模式):
\\npublic void processOrder(Order order) {\\n validateOrder(order);\\n double total = calculateTotal(order);\\n total = applyDiscount(total);\\n updateOrderStatus(order, total);\\n sendConfirmationEmail(order, total);\\n}\\n\\nprivate void validateOrder(Order order) {\\n if (order == null) {\\n throw new IllegalArgumentException(\\"Order cannot be null\\");\\n }\\n if (order.getItems().isEmpty()) {\\n throw new IllegalArgumentException(\\"Order must contain at least one item\\");\\n }\\n}\\n\\nprivate double calculateTotal(Order order) {\\n double total = 0;\\n for (OrderItem item : order.getItems()) {\\n total += item.getPrice() * item.getQuantity();\\n }\\n return total;\\n}\\n\\nprivate double applyDiscount(double total) {\\n if (total > 1000) {\\n return total * 0.9; // 10% 折扣\\n } else if (total > 500) {\\n return total * 0.95; // 5% 折扣\\n }\\n return total;\\n}\\n\\nprivate void updateOrderStatus(Order order, double total) {\\n order.setTotal(total);\\n order.setStatus(OrderStatus.PROCESSED);\\n orderRepository.save(order);\\n}\\n\\nprivate void sendConfirmationEmail(Order order, double total) {\\n String message = \\"Your order #\\" + order.getId() + \\" has been processed. Total: $\\" + total;\\n emailService.sendEmail(order.getCustomerEmail(), \\"Order Confirmation\\", message);\\n}\\n
\\n抽象层次一致性原则与组合函数密切相关。它要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,代码会显得凌乱,难以理解。
\\n按照组合函数和 SLAP 原则,我们应当:
\\n满足 SLAP 实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。
\\n示例:
\\n// 顶层抽象 - 业务流程\\npublic void registerNewUser(UserRegistrationRequest request) {\\n User user = createUserFromRequest(request);\\n validateUser(user);\\n saveUser(user);\\n sendWelcomeEmail(user);\\n}\\n\\n// 中层抽象 - 具体步骤\\nprivate User createUserFromRequest(UserRegistrationRequest request) {\\n User user = new User();\\n mapBasicInfo(user, request);\\n mapAddressInfo(user, request);\\n mapPreferences(user, request);\\n return user;\\n}\\n\\n// 底层抽象 - 实现细节\\nprivate void mapBasicInfo(User user, UserRegistrationRequest request) {\\n user.setUsername(request.getUsername());\\n user.setEmail(request.getEmail());\\n user.setFirstName(request.getFirstName());\\n user.setLastName(request.getLastName());\\n // 其他基本信息映射\\n}\\n
\\n抽象的过程是合并同类项、归并分类和寻找共性的过程。将有内在逻辑关系的事物放在一起,然后给这个分类进行命名,这个名字就代表了这组分类的抽象。
\\n示例:重构重复代码
\\n// 重复代码\\npublic void processCustomerA(Customer customer) {\\n System.out.println(\\"Processing customer: \\" + customer.getName());\\n double discount = customer.getTotal() * 0.1;\\n customer.applyDiscount(discount);\\n notifyCustomer(customer);\\n}\\n\\npublic void processVipCustomer(Customer customer) {\\n System.out.println(\\"Processing customer: \\" + customer.getName());\\n double discount = customer.getTotal() * 0.2;\\n customer.applyDiscount(discount);\\n notifyCustomer(customer);\\n}\\n\\n// 抽象后的代码\\npublic void processCustomer(Customer customer, double discountRate) {\\n System.out.println(\\"Processing customer: \\" + customer.getName());\\n double discount = customer.getTotal() * discountRate;\\n customer.applyDiscount(discount);\\n notifyCustomer(customer);\\n}\\n\\npublic void processRegularCustomer(Customer customer) {\\n processCustomer(customer, 0.1);\\n}\\n\\npublic void processVipCustomer(Customer customer) {\\n processCustomer(customer, 0.2);\\n}\\n
\\n当我们发现有些东西无法归到一个类别中时,可以通过上升一个抽象层次的方式,让它们在更高的抽象层次上产生逻辑关系。
\\n示例:支付方式抽象
\\n// 提升抽象层次前\\npublic class CreditCardPayment {\\n public void processCreditCardPayment(double amount) {\\n // 信用卡支付逻辑\\n }\\n}\\n\\npublic class PayPalPayment {\\n public void processPayPalPayment(double amount) {\\n // PayPal支付逻辑\\n }\\n}\\n\\n// 提升抽象层次后\\npublic interface PaymentMethod {\\n void processPayment(double amount);\\n}\\n\\npublic class CreditCardPayment implements PaymentMethod {\\n @Override\\n public void processPayment(double amount) {\\n // 信用卡支付逻辑\\n }\\n}\\n\\npublic class PayPalPayment implements PaymentMethod {\\n @Override\\n public void processPayment(double amount) {\\n // PayPal支付逻辑\\n }\\n}\\n
\\n金字塔原理作为一种强大的结构化思维方法,在编程领域展现出独特价值,它帮助开发者将复杂问题分解为有序的层次结构,使代码更加清晰、逻辑更加严密。在实际编程中,我们遵循\\"自下而上分析问题,自上而下编写代码\\"的原则,先收集具体需求并识别共性进行抽象,再从高层接口和主流程开始编写实现。
\\n这种思维方式创造了清晰的抽象层次:
\\n形成了一个既在单个函数内部,也贯穿整个代码库架构设计的金字塔结构。
\\n组合函数模式和抽象层次一致性原则是提升代码可读性和可维护性的有效工具。通过将大函数拆分为多个小函数,并确保每个函数内的抽象层次一致,我们可以构建出更加清晰、易于理解和维护的代码结构。
\\n抽象思维是软件开发人员的核心能力之一,通过不断的训练和实践,我们可以提升自己的抽象能力,从而更好地应对复杂的软件开发挑战。记住,好的抽象能够帮助我们抓住事物本质,简化复杂问题,提高开发效率。
\\n在日常开发中,我们应当有意识地应用这些原则和方法,不断提升自己的代码质量和抽象思维能力。
\\nTCC是一种分布式的事务的方案,将一个事务分成了TRY-CANCEL-CONFIRM三个阶段:
\\n在TCC中,存在着两个比较关键的问题,那就是空回滚和悬挂的问题。
\\nTCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。
\\nTCC的实现方式存在悬挂事务的问题,在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况。举一个比较常见的具体场景:一次分布式事务,先发生了Try,但是因为有的节点失败,又发生了Cancel,而下游的某个节点因为网络延迟导致先接到了Cancel,在空回滚完成后,又接到了Try的请求,然后执行了,这就会导致这个节点的Try占用的资源无法释放,也没人会再来处理了,就会导致了事务悬挂。
\\n这两个问题处理不好,都可能会导致一个分布式事务没办法保证最终一致性。有一个办法,可以一次性的解决以上两个问题,那就是——引入分布式事务记录表。
\\n有了这张表,每一个参与者,都可以在本地事务执行的过程中,同时记录一次分布式事务的操作记录。
\\n这张表中有两个关键的字段,一个是tx_id用于保存本次处理的事务ID,还有一个就是state,用于记录本次事务的执行状态。至于其他的字段,比如一些业务数据,执行时间、业务场景啥的,就自己想记录上就记录啥。
\\nCREATE TABLE `distribute_transaction`( \\n`tx_id` varchar(128) NOT NULL COMMENT \'事务id\', \\n`state` int(1) DEFAULT NULL COMMENT \'事务状态,0:try,1:confirm,2:cancel\', \\nPRIMARY KEY (`tx_id`) U)\\n
\\n有了这张表以后,我们在做try、cancel和confirm操作之后,都需要在本地事务中创建或者修改这条记录。一条记录的状态机如下:
\\n当一个参与者接到一次Cancel请求的时候,先去distribute_transaction表中根据tx_id查询是否有try的记录,如果没有,则进行一次空回滚即可。并在distribute_transaction中创建一条记录,状态标记为cancel。
\\n当一个参与者接到一次Try请求的时候,先去distribute_transaction表中根据tx_id查询是否有记录,如果当前存在,并且记录的状态是cancel,则拒绝本次try请求。
\\n但是需要注意的是,上面的请求过程,需要做好并发控制。
\\n有了这张表,我们还可以基于他做幂等控制,每次try-cancel-confirm请求来的时候,都可以到这张表中查一下,然后做幂等控制。
","description":"✅TCC的空回滚和悬挂是什么?如何解决? TCC是一种分布式的事务的方案,将一个事务分成了TRY-CANCEL-CONFIRM三个阶段:\\n\\n在TCC中,存在着两个比较关键的问题,那就是空回滚和悬挂的问题。\\n\\n1.空回滚问题:\\n\\nTCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。\\n\\n2.悬挂事务问题:\\n\\nTCC的实现方式存…","guid":"https://juejin.cn/post/7486755501528055846","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-29T03:43:30.283Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/96871b8e15314139a9231a81fea04633~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743824610&x-signature=fCP3nkHuGkb53gp%2BZp0K6uCukqk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"「Hadoop实战基础:从集群搭建到编程测试」","url":"https://juejin.cn/post/7486733708949143603","content":"全流程解析Hadoop集群搭建,带你完成从环境部署到代码测试的完整实战之旅,掌握Hadoop大数据基础技能!","description":"全流程解析Hadoop集群搭建,带你完成从环境部署到代码测试的完整实战之旅,掌握Hadoop大数据基础技能!","guid":"https://juejin.cn/post/7486733708949143603","author":"问天道","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-29T03:23:40.959Z","media":null,"categories":["后端","Hadoop"],"attachments":null,"extra":null,"language":null},{"title":"关于Node,一定要学这个10+万Star项目!","url":"https://juejin.cn/post/7486515264823132210","content":"给大家分享一个关于 Node.js 的宝藏项目,目前已经有 10+万 Star,非常值得学习。 来源:沉浸式趣谈
\\nNode.js Best Practices 是 GitHub 上一个超级热门的项目,
\\n目前已经有 102k Star。它汇集了 100+条关于 Node.js 开发的最佳实践,涵盖了从项目架构到安全防范的方方面面。
\\nhttps://practica.dev
https://github.com/goldbergyoni/nodebestpractices
最厉害的是,它不像其他很多教程那样空洞地讲概念,每条最佳实践都包含了:
\\n而且内容分门别类,按照项目结构、错误处理、编码规范、测试、安全、性能等主题组织,非常清晰。
\\n之前的问题:
\\n// app.js - 传统单进程启动方式\\nconst express = require(\'express\');\\nconst app = express();\\n// ...其他代码\\napp.listen(3000, () => console.log(\'服务启动在3000端口\'));\\n
\\n我们的服务器是 8 核 CPU,但上面的代码只能利用其中一个核心,大量计算资源被浪费。
\\n参考最佳实践 5.6「利用 CPU 多核」,改进后:
\\n// app.js - 使用Node.js内置cluster模块\\nconst cluster = require(\'cluster\');\\nconst os = require(\'os\');\\nconst numCPUs = os.cpus().length;\\n\\nif (cluster.isMaster) {\\n console.log(`主进程 ${process.pid} 正在运行`);\\n\\n // 根据CPU数量创建工作进程\\n for (let i = 0; i < numCPUs; i++) {\\n cluster.fork();\\n }\\n\\n cluster.on(\'exit\', (worker, code, signal) => {\\n console.log(`工作进程 ${worker.process.pid} 已退出`);\\n // 当工作进程退出后,立即创建新的工作进程\\n cluster.fork();\\n });\\n} else {\\n // 工作进程共享同一个TCP连接\\n const express = require(\'express\');\\n const app = express();\\n\\n // ...其他代码\\n\\n app.listen(3000, () => {\\n console.log(`工作进程 ${process.pid} 监听端口3000`);\\n });\\n}\\n
\\n效果立竿见影!CPU 利用率从原来的 12%左右提升到了 80%以上,系统吞吐量提高了 6 倍多。
\\n之前的问题:
\\napp.get(\'/process-data\', (req, res) => {\\n // 直接在API请求中执行CPU密集型操作\\n const result = processLargeDataSet(req.body.data);\\n res.json(result);\\n});\\n\\nfunction processLargeDataSet(data) {\\n // 一个CPU密集型的操作,处理几十万条记录\\n let result = [];\\n for (let i = 0; i < data.length; i++) {\\n // 复杂计算...\\n }\\n return result;\\n}\\n
\\n这段代码在处理大量数据时会阻塞事件循环,导致其他用户请求无法得到响应,超时报错。
\\n参考最佳实践 7.1「不要阻塞事件循环」,改进后:
\\nconst { Worker } = require(\'worker_threads\');\\n\\napp.get(\'/process-data\', (req, res) => {\\n const data = req.body.data;\\n\\n // 创建一个工作线程来处理CPU密集型任务\\n const worker = newWorker(\'./data-processor.js\', {\\n workerData: data,\\n });\\n\\n worker.on(\'message\', result => {\\n res.json(result);\\n });\\n\\n worker.on(\'error\', err => {\\n res.status(500).json({ error: err.message });\\n });\\n});\\n\\n// data-processor.js\\nconst { workerData, parentPort } = require(\'worker_threads\');\\n\\nfunctionprocessLargeDataSet(data) {\\n // 复杂计算...\\n let result = [];\\n // 处理数据...\\n return result;\\n}\\n\\nconst result = processLargeDataSet(workerData);\\nparentPort.postMessage(result);\\n
\\n改进后,即使在处理大数据集的时候,API 服务依然能流畅响应其他请求,用户体验大大提升。
\\n之前的问题:
\\napp.get(\'/api/users/:id\', (req, res) => {\\n try {\\n const user = getUserById(req.params.id);\\n if (!user) {\\n res.status(404).json({ error: \'User not found\' });\\n return;\\n }\\n res.json(user);\\n } catch (error) {\\n console.error(\'获取用户出错:\', error);\\n res.status(500).json({ error: \'服务器内部错误\' });\\n }\\n});\\n\\n// 每个路由都重复类似的错误处理代码...\\n
\\n这种方式导致每个路由都要写重复的错误处理代码,不仅冗余,也容易漏掉某些错误处理。
\\n参考最佳实践 2.1「使用 Async-Await 或 Promise 处理异步错误」,改进后:
\\n// 统一的错误处理中间件\\napp.use((err, req, res, next) => {\\n console.error(\'应用错误:\', err);\\n\\n // 根据错误类型返回不同状态码\\n if (err.name === \'NotFoundError\') {\\n return res.status(404).json({ error: err.message });\\n }\\n\\n if (err.name === \'ValidationError\') {\\n return res.status(400).json({ error: err.message });\\n }\\n\\n // 默认服务器错误\\n res.status(500).json({ error: \'服务器内部错误\' });\\n});\\n\\n// 自定义错误类\\nclassNotFoundErrorextendsError {\\n constructor(message) {\\n super(message);\\n this.name = \'NotFoundError\';\\n }\\n}\\n\\n// 路由变得简洁\\napp.get(\'/api/users/:id\', async (req, res, next) => {\\n try {\\n const user = awaitgetUserById(req.params.id);\\n if (!user) {\\n thrownewNotFoundError(\'用户不存在\');\\n }\\n res.json(user);\\n } catch (error) {\\n next(error); // 传递给错误处理中间件\\n }\\n});\\n
\\n改进后,代码更加简洁,错误处理也更加统一和健壮。
\\n在所有的实践中,以下五条是我认为对前端开发转 Node.js 的开发者最有价值的:
\\n说实话,之前我总觉得编码就是实现功能,能跑就行。
\\n接触 Node.js Best Practices 后,我才明白写出高质量的 Node.js 代码需要考虑这么多方面。
\\n最后,强烈推荐每一个使用 Node.js 的开发者都去看看这个项目。
\\n它不仅告诉你 \\"怎么做\\" ,更重要的是解释了 \\"为什么要这么做\\" ,这对于提升开发能力至关重要。
","description":"给大家分享一个关于 Node.js 的宝藏项目,目前已经有 10+万 Star,非常值得学习。 来源:沉浸式趣谈 这个项目是啥?\\n\\nNode.js Best Practices 是 GitHub 上一个超级热门的项目,\\n\\n目前已经有 102k Star。它汇集了 100+条关于 Node.js 开发的最佳实践,涵盖了从项目架构到安全防范的方方面面。\\n\\n• 官网:https://practica.dev\\n• Github:https://github.com/goldbergyoni/nodebestpractices\\n\\n最厉害的是…","guid":"https://juejin.cn/post/7486515264823132210","author":"独立开阀者_FwtCoder","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-28T08:38:53.401Z","media":null,"categories":["后端","前端","面试"],"attachments":null,"extra":null,"language":null},{"title":"Java中Spring Boot使用MQTT推送,订阅","url":"https://juejin.cn/post/7486502836965490724","content":"推荐大家尝试 MQTT-Macchiatto,轻松实现优雅的 MQTT 连接体验。快速、简便,让你的代码更加优雅!
\\n\\n<dependency>\\n <groupId>io.github.rururunu</groupId>\\n <artifactId>MQTT-Macchiatto</artifactId>\\n <version>0.1.3</version>\\n</dependency>\\n
\\n在 application.yml 中编写配置 Write configuration in application.yml:
\\nmto-mqtt:\\n # 主机\\n host: tcp://${ip}:${port}\\n # 用户名\\n username: ${username}\\n # 密码\\n password: ${password}\\n # 超时时间\\n timeout: 10000\\n # 心跳\\n keepalive: 60\\n # 重连间隔\\n reconnect-frequency-ms: 5000\\n
\\n启动类上添加 Add to Startup Class
\\n@SpringBootApplication(scanBasePackages = {\\"io.github.rururunu\\"})\\n
\\nMqttPut.of(\\"rsp/\\")\\n .response((topic, message) -> {\\n // 在这里编写收到消息的响应操作\\n // Write the response operation for receiving messages here\\n System.out.println(\\"topic:\\" + topic + \\"message:\\" + message);\\n }).start();\\n
\\n或 or
\\nMqttPut.of()\\n .setTopic(\\"topic\\")\\n .setServiceId(\\"serviceId\\")\\n .setCleanSession(true)\\n .response((message) -> {\\n // 在这里编写收到消息的响应操作\\n // Write the response operation for receiving messages here\\n })\\n .start();\\n
\\n或 or
\\nMQTTMonitor mqttMonitor = new MQTTMonitor();\\nmqttMonitor.setClientId(\\"clientId\\");\\nmqttMonitor.setCleanSession(false);\\nmqttMonitor.setQos(MQTTQos.EXACTLY_ONCE);\\nmqttMonitor.setMqttCallback(new MqttCallback() {\\n @Override\\n public void connectionLost(Throwable throwable) {\\n // 编写异常断开的代码\\n // Write code for abnormal disconnection\\n // 可以直接使用封装好的 mqttMonitor.reconnect();\\n // You can directly use the packaged product\\n mqttMonitor.reconnect();\\n }\\n\\n @Override\\n public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {\\n // 在这里编写收到消息的响应操作\\n // Write the response operation for receiving messages here\\n }\\n\\n @Override\\n public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {\\n\\n }\\n});\\n// 开启订阅\\n//Activate subscription\\nmqttMonitor.start(\\"topic\\");\\n
\\n使用 init 直接创建 MqttPush 对象来上报
\\nMqttPush mqttPush = new MqttPush();\\nmqttPush.push(\\"test/\\", \\"test\\", MQTTQos.AT_LEAST_ONCE);\\n
\\n或者你也可以使用 start 来初始化,或更改 MqttPush 信息后重新初始化
\\nMqttPush mqttPush = new MqttPush();\\nmqttPush.start();\\nmqttPush.push(\\"test/\\", \\"test\\", MQTTQos.AT_LEAST_ONCE,\\n(iMqttToken) -> System.out.println(\\"success\\"),\\n(iMqttToken, throwable) -> System.out.println(\\"failure\\")\\n);\\n
\\n或者您可以使用 MqttReport 来上报
\\n// 创建连接\\n// Create connection\\nMQTTReport mqttReport = new MQTTReport();\\nmqttReport.setTopic(\\"topic\\");\\nmqttReport.setServiceId(\\"serviceId\\");\\nmqttReport.setCleanSession(false);\\nmqttReport.start();\\n// 发送消息\\n// send message\\nmqttReport.getMessage().setQos(MQTTQos.EXACTLY_ONCE.getValue());\\nmqttReport.getMessage().setPayload(\\"hello\\".getBytes());\\nmqttReport.publish(mqttReport.getMqttTopic(), mqttReport.getMessage());\\n
\\n或 or
\\n// 创建连接\\n// Create connection\\nMQTTReport mqttReport = new MQTTReport();\\nmqttReport.setTopic(\\"topic\\");\\nmqttReport.setServiceId(\\"serviceId\\");\\nmqttReport.start();\\n// 发送消息\\n// Create connection\\nMqttMessage message = new MqttMessage();\\nmessage.setQos(MQTTQos.EXACTLY_ONCE.getValue());\\nmessage.setPayload(\\"hello\\".getBytes());\\nmqttReport.publish(\\"topic\\", message);\\n
\\n如果您需要长连接请勿将 new MqttPush 的代码写入在每次都需要推送的方法中。可以在 class 中创建 MqttPush 的对象,若您使用的是配置文件连接请勿调用 MqttPush().init() 在第一次推送时会自动调用 init(), 因为在一开始创建时 MqttPush 无法获取到 yml 中的数据。
\\n如下所示:
\\nclass MqttMacchiatto {\\n\\nprivate MqttPush mqttPush = new MqttPush();\\n\\npublic void push() {\\nmqttPush.push(\\"test/\\", \\"test\\", MQTTQos.AT_LEAST_ONCE);\\n}\\n}\\n
\\n如果可以通过其他方式获取MQTT 服务的信息,可以省略配置信息,直接通过构建MQTT 服务信息来进行消息的监听和上报,也可以通过创建多个对象来连接不同的 MQTT 服务
\\nIf information about MQTT services can be obtained through other means, configuration information can be omitted and messages can be monitored and reported directly by building MQTT service information. Multiple objects can also be created to connect different MQTT services
\\nMqttPut.of(\\"test/\\")\\n .host(\\"tcp://127.0.0.1:1883\\")\\n .username(\\"username\\")\\n .password(\\"password\\")\\n .timeout(10000)\\n .keepalive(60)\\n .cleanSession(false)\\n .reconnectFrequencyMs(5000)\\n .response((topic, msg) -> System.out.println(topic + \\":\\" + msg))\\n .start();\\n
\\n// 使用 builder 初始化主机信息并使用 init 加载 \\n// Initialize host information using builder and load it using init\\nMqttPush mqttPush = new MqttPush.builder()\\n .host(\\"tcp://127.0.0.1:1883\\")\\n .username(\\"username\\")\\n .password(\\"password\\")\\n .timeout(10000)\\n .keepalive(60)\\n .cleanSession(false)\\n .build()\\n .init((e) -> {\\n System.out.println(\\"Mqtt Creation failed\\" + e);\\n });\\n// 上报消息\\n// Report message\\nmqttPush.push(\\"test/\\", \\"test\\", MQTTQos.AT_LEAST_ONCE,\\n (iMqttToken) -> System.out.println(\\"success\\"),\\n (iMqttToken, throwable) -> System.out.println(\\"failure\\")\\n );\\n
\\n或 or
\\n// 初始化主机信息\\n// Initialize host information\\nMqttPush mqttPush = new MqttPush()\\n .host(\\"tcp://127.0.0.1:1883\\")\\n .username(\\"username\\")\\n .password(\\"password\\")\\n .timeout(10000)\\n .keepalive(60)\\n .cleanSession(false)\\n .reconnectFrequencyMs(5000);\\n// 开启主机\\n// Open the host\\nmqttPush.start();\\n// 创建主题 可以忽略这一步,若topic没有创建在调用 push方法时会自动创建一个 topic 的 MqttTopic 对象放入内存\\n// found topic You can ignore this step. If the topic is not created,\\n// an MqttTopic object for the topic will be automatically created and placed\\n// in memory when calling the push method\\nmqttPush.foundTopic(\\"test/\\");\\n// 上报消息\\n// Report message\\nmqttPush.push(\\"test/\\", \\"test\\", MQTTQos.AT_LEAST_ONCE,\\n (iMqttToken) -> System.out.println(\\"success\\"),\\n (iMqttToken, throwable) -> System.out.println(\\"failure\\")\\n);\\n// 可以选择手动关闭\\nMqttPush.stop();\\n
","description":"Java中Spring Boot使用MQTT推送,订阅 推荐大家尝试 MQTT-Macchiatto,轻松实现优雅的 MQTT 连接体验。快速、简便,让你的代码更加优雅!\\n\\nGitHub | Gitee\\n\\n在 pom.xml 中引入 Introduce us in pom.xml\\n在Java开发中,线程安全是一个高频关键词。当我们使用多线程处理共享数据时,常常需要加锁或使用同步机制来避免数据混乱。但有一把“锁”却能让每个线程拥有自己的独立数据副本,它就是ThreadLocal
。接下来通过实际案例,带你理解它的核心价值和可能踩到的“坑”。
ThreadLocal是Java提供的一个工具类,它为每个线程创建一个独立的变量副本。不同线程之间无法访问彼此的副本,因此天然避免了线程安全问题。
\\n假设有一个公共会议室(共享变量),多个人(线程)要轮流使用。传统方式是排队(加锁),但更高效的做法是给每个人发一个隔音耳机(ThreadLocal),各自听自己的内容。
\\n// 创建一个ThreadLocal变量\\nprivate static ThreadLocal<String> userSession = new ThreadLocal<>();\\n\\n// 线程A设置值\\nuserSession.set(\\"UserA-Data\\");\\n\\n// 线程A获取自己的值\\nSystem.out.println(userSession.get()); // 输出:UserA-Data\\n
\\n在Web应用中,一个请求可能经过多个方法处理(如Controller、Service、DAO)。如果每个方法都需传递用户信息,代码会变得冗长。使用ThreadLocal
,可以在拦截器中保存用户信息,后续方法直接获取。
public class UserContextHolder {\\n private static ThreadLocal<User> currentUser = new ThreadLocal<>();\\n \\n public static void set(User user) {\\n currentUser.set(user);\\n }\\n \\n public static User get() {\\n return currentUser.get();\\n }\\n \\n public static void clear() {\\n currentUser.remove();\\n }\\n}\\n\\n// 拦截器中设置用户信息\\nUserContextHolder.set(user);\\n// Service层直接获取\\nUser user = UserContextHolder.get();\\n
\\n某些ORM框架(如MyBatis)使用ThreadLocal
保存数据库连接,确保同一线程中的多个数据库操作使用同一个连接,避免频繁创建和关闭连接。
SimpleDateFormat
是非线程安全的,使用ThreadLocal
为每个线程分配独立的实例,既安全又高效。
private static ThreadLocal<SimpleDateFormat> dateFormat = \\n ThreadLocal.withInitial(() -> new SimpleDateFormat(\\"yyyy-MM-dd\\"));\\n\\n// 使用\\nString date = dateFormat.get().format(new Date());\\n
\\n问题原因:
\\nThreadLocal
的存储结构(ThreadLocalMap)中,Entry的Key是弱引用,但Value是强引用。如果线程长时间存活(如线程池中的线程),即使ThreadLocal
实例被回收,Value仍无法释放,导致内存泄漏。
解决方案:
\\n使用完ThreadLocal
后,必须调用remove()
方法清理当前线程的值。
try {\\n userSession.set(\\"data\\");\\n // ...业务逻辑\\n} finally {\\n userSession.remove(); // 必须清理!\\n}\\n
\\n问题原因:
\\n线程池会复用线程。若一个任务未清理ThreadLocal
数据,下一个任务可能读取到残留数据,导致逻辑错误。
案例:
\\n用户A的请求处理完成后,未清理ThreadLocal
中的用户信息。用户B的请求复用了同一线程,误读到用户A的数据。
解决方案:
\\n在任务执行完毕后,务必调用remove()
。
滥用ThreadLocal
可能导致代码逻辑隐式依赖线程上下文,增加维护难度。例如,在异步编程中,子线程无法直接获取父线程的ThreadLocal
数据。
始终在try-finally块中使用:
\\n确保即使发生异常,也能执行remove()
。
避免存储大对象:
\\nThreadLocal
中的数据会随线程生命周期存在,大对象容易导致内存压力。
谨慎用于框架设计:
\\n合理封装,避免暴露ThreadLocal
细节给业务代码。
ThreadLocal
是一把双刃剑:
最后核心口诀:用完即清理,设计要克制。
","description":"前言 在Java开发中,线程安全是一个高频关键词。当我们使用多线程处理共享数据时,常常需要加锁或使用同步机制来避免数据混乱。但有一把“锁”却能让每个线程拥有自己的独立数据副本,它就是ThreadLocal。接下来通过实际案例,带你理解它的核心价值和可能踩到的“坑”。\\n\\n一、ThreadLocal是什么?\\n\\nThreadLocal是Java提供的一个工具类,它为每个线程创建一个独立的变量副本。不同线程之间无法访问彼此的副本,因此天然避免了线程安全问题。\\n\\n举个栗子 🌰\\n\\n假设有一个公共会议室(共享变量),多个人(线程)要轮流使用。传统方式是排队(加锁…","guid":"https://juejin.cn/post/7486421434576912434","author":"四七伵","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-28T05:08:39.640Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"mysql---死锁问题探讨","url":"https://juejin.cn/post/7486359711846137894","content":"数据库死锁问题是指在多个并发事务中,彼此之间出现了相互等待的情况,导致所有事务都无法继续执行,称为死锁。
\\n今年都还没写过一个需求呢😣,闲来无事,突发奇想 volatile
可见性的问题,我以前一直都没模拟成功过呢。现在AI
这么厉害,应该能帮我解决这个困惑吧!!!
\\n\\n对
\\nvolatile
变量的修改,能立即对其他线程可见(强制刷新主内存和本地缓存)。
相信所有的java🧔对这句话非常的熟悉了。有多少个人去做过测试,我3、5年前就已经去做过测试了。但是很遗憾,当时怎么也复现不了。不管我用不volatile
效果都一样呢。今天AI 帮我解决了我的困惑!!!
🎈不想看过程的,直接看总结吧!
\\n下面是我做的测试代码,没有用volatile修饰,所以理论上来说一直会输出Worker thread running...
实现代码:
\\npublic class Test {\\n private static boolean stop = false;\\n\\n public static void main(String[] args) {\\n Thread workerThread = new Thread(() -> {\\n int count = 0;\\n // 按照没有用volatile修饰,当前线程不能看到变量被修改,所以理论上来说不会退出循环\\n while (!stop) {\\n System.out.println(\\"Worker thread running...\\");\\n count++;\\n }\\n System.out.println(\\"Worker thread stopped, count: \\" + count);\\n });\\n\\n workerThread.start();\\n\\n try {\\n Thread.sleep(1000);\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n\\n stop = true;\\n System.out.println(\\"Main thread set stop to true\\");\\n }\\n\\n}\\n
\\n执行结果
\\nWorker thread running...\\nMain thread set stop to true\\nWorker thread stopped, count: 1\\n
\\n竟然和想象中的完全不一样。
\\n有大佬看到这儿已经定位到了问题了么❔
\\n\\n\\n就是说在某些情况下会去与主存同步,获取最新值。下面列举了比如CPU混存满了,或者到了特定的时间间隔。
\\n如果是因为这两个原因,就有点说不通了,我就运行一个demo,而且每次执行都是同样的结果。
\\n
没有获取到我想要的答案,那就继续AI吧✈
\\n\\n\\n运行上面的代码,确实出现 无法对出循环的问题。 这个例子和我开篇的例子就 少了一个
\\nSystem.out.println(\\"Worker thread running...\\");
🤩看到这儿 ,应该❗大概❗好像❗感觉❗知道问题所在了吧❓
\\n
可能是System.out.println()
加了锁的,导致线程的工作内存被刷新了。\\n点进去看果然sout(打印方法简称)
方法是加了锁的。
public void println(String x) {\\n if (getClass() == PrintStream.class) {\\n //也加了锁的\\n writeln(String.valueOf(x));\\n } else {\\n synchronized (this) {\\n print(x);\\n newLine();\\n }\\n }\\n}\\n
\\n继续问AI,看看AI是怎么解释的
\\nsout
,循环就会退出(没有用volatile,其他线程也能看到修改)看一下AI是怎么解释的吧,如下图:
\\n\\n\\n在lock、sync修饰的代码,工作内存的变量会被刷新。所有我们在 打印 没有用
\\nvolatile
修饰的变量的时候,变量也会被刷新,导致测试结果偏离预期
Tips:用在while 循环中使用Thread.sleep()
也会退出循环,是不是也意味着它也能刷线程的工作内存呢?
目前作者也是没有找到比较官方的资料
本篇文章利用AI 分析了 无法复现普通变量 的不可见性 的原因。最后确认是因为使用sout
访问变量,sout
底层加锁,所以变量在工作内存中被刷新了,导致程序员的运行结果和理论上的不一致。
\\n\\n原来小小
\\nsout
竟然有这么大的能量😱,兄弟们好好的把日志打印
给我用起来,防御式编程✈啊,让后面的同学删除一行日志,就导致运行结果就变了,这不美滋滋。
最后,想了解volatile
更多的知识的 ,推荐阅读:深入解析Java中volatile关键字的底层原理
文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n在 RocketMQ 中,消息队列(Queue)与消费者(Consumer)之间的对应关系由 消费者组(Consumer Group) 和 负载均衡机制(Load Balancing) 决定。
\\n以下是具体的计算方式及原理整理。
\\n集群消费(Clustering Mode) :
\\n广播消费(Broadcasting Mode) :
\\nRocketMQ 在 集群消费模式 下,使用 负载均衡机制 分配队列与消费者的关系。
\\n具体过程如下:
\\nBroker 根据消费者的 客户端 ID 对消费者进行字典序排序。
\\nIP地址:进程号
。consumerIndex
。AllocateMessageQueueAveragely
策略,每个消费者分配连续的一组队列。queueIndex = (queueSize / consumerSize) * consumerIndex\\n
\\nqueueIndex = (queueSize / consumerSize) * consumerIndex
其中:
queueSize
:主题的队列总数。consumerSize
:消费者组内的活跃消费者数量。consumerIndex
:当前消费者在排序后的序号。假设某主题有 6 个队列(Q0-Q5),消费者组 ConsumerGroupA
中有 3 个消费者(C1、C2、C3),消费者的客户端 ID 和队列分配如下:
消费者的客户端 ID:
\\n192.168.1.10:12345
192.168.1.11:12346
192.168.1.9:12347
排序结果(按字典序):
\\nconsumerIndex = 0
consumerIndex = 1
consumerIndex = 2
队列通过负载均衡算法分配:
\\nRocketMQ 会根据消费者组的活跃消费者数量动态调整队列分配关系:
\\n消费者增加:
\\n消费者退出:
\\n心跳检测:
\\n轮询分配:将队列按轮询方式分配给消费者。
\\n示例:
\\nAllocateMessageQueueStrategy
,用户可以自定义队列分配逻辑,以满足特殊业务需求。在广播模式下,每个消费者都会消费主题下的所有队列,不涉及队列分配。
\\n适合需要消息被多个消费者同时处理的场景。
\\nRocketMQ 中队列与消费者的对应关系通过 消费者组 和 负载均衡机制 确定:
\\nconsumerIndex
。Golang(Go 语言)凭借其简洁语法、卓越性能和原生并发支持,已成为现代后端开发的首选语言之一。本教程将指导你如何通过 Cursor 编辑器——一款深度集成 AI 辅助的现代化开发工具,高效构建 Golang 后端应用。
\\n首先确保你的系统已安装Golang:
\\n# 在MacOS上使用Homebrew安装\\nbrew install go\\n\\n# 在Linux上安装\\nsudo apt install golang\\n\\n# 在Windows上可以从官网下载安装包\\n
\\n验证安装:
\\ngo version\\n
\\n从Cursor官网下载并安装适合你系统的版本。Cursor是一款专为AI辅助编程设计的编辑器,特别适合Golang开发。
\\nmkdir go-backend\\ncd go-backend\\ngo mod init github.com/yourusername/go-backend\\n
\\ngo.mod
文件在Cursor中点击项目文件夹,选择\\"New File\\",命名为main.go
。
package main\\n\\nimport (\\n\\"fmt\\"\\n\\"net/http\\"\\n)\\n\\nfunc main() {\\n// 定义路由处理函数\\nhttp.HandleFunc(\\"/\\", func(w http.ResponseWriter, r *http.Request) {\\nfmt.Fprintf(w, \\"欢迎使用Golang后端服务!\\")\\n})\\n\\nhttp.HandleFunc(\\"/api/hello\\", func(w http.ResponseWriter, r *http.Request) {\\nname := r.URL.Query().Get(\\"name\\")\\nif name == \\"\\" {\\nname = \\"访客\\"\\n}\\nfmt.Fprintf(w, \\"你好, %s!\\", name)\\n})\\n\\n// 启动服务器\\nfmt.Println(\\"服务器正在运行,访问 http://localhost:8080\\")\\nhttp.ListenAndServe(\\":8080\\", nil)\\n}\\n\\n
\\n终端运行 go run main.go,即可在浏览器访问该服务
\\n图1:在Cursor中运行项目
\\n\\n图2:在浏览器中访问服务
Cursor的一个强大功能是AI辅助编码。
\\n直接在chat窗口描述你要做的事,让Cursor自动帮你生成代码
\\n比如将上面简单的http服务用Gin框架改写成更复杂的后端应用:
\\n图4:在chat窗口描述你要的变更
\\nVS Code 的 Go 扩展为开发者提供了全方位的语言支持,包括智能补全、代码导航和调试功能,能显著提升 Go 开发效率。
\\n图5:安装Go扩展
\\ninstall好扩展之后,通过 Command/Ctrl+Shift+P 快捷键快速调出命令面板,输入 \'Go: Install/Update Tools\' 命令,全选安装官方推荐的工具集,这样就能获得最完整的 Go 语言开发支持。
\\n\\n图6:安装Go工具集
_test.go
文件,节省手动编写测试用例的时间。gdb
更适配 Go 特性。go vet
更强大。除了用浏览器/postman,我们也可以直接使用内置的 HTTP 客户端扩展进行API 测试:
\\n安装 REST Client 扩展
\\n创建 requests.http
文件
编写测试请求(支持多种 HTTP 方法)
\\n点击 Send Request 按钮即可查看完整响应
\\n\\n图7:使用扩展工具测试API
本文展示了如何利用 Cursor 编辑器高效开发 Golang 后端应用。通过 Cursor 强大的 AI 辅助功能(如智能补全、代码生成和问题诊断),开发者能够显著提升开发效率,同时为 Golang 初学者提供平滑的学习曲线。结合 Golang 的高性能特性与 Cursor 的智能化工具链(包括扩展支持、实时协作和上下文感知编辑),你可以快速构建易维护、高性能的后端服务,减少重复性工作,专注于核心业务逻辑的实现。当然,Cursor 的潜力远不止于此,更多高效功能等待你的探索。
","description":"AI赋能:使用Cursor快速开发Golang后端服务 Golang(Go 语言)凭借其简洁语法、卓越性能和原生并发支持,已成为现代后端开发的首选语言之一。本教程将指导你如何通过 Cursor 编辑器——一款深度集成 AI 辅助的现代化开发工具,高效构建 Golang 后端应用。\\n\\n一、环境准备\\n1. 安装Golang\\n\\n首先确保你的系统已安装Golang:\\n\\n# 在MacOS上使用Homebrew安装\\nbrew install go\\n\\n# 在Linux上安装\\nsudo apt install golang\\n\\n# 在Windows上可以从官网下载安装包\\n\\n\\n验证…","guid":"https://juejin.cn/post/7486057384395178036","author":"37手游后端团队","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-27T07:13:10.681Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/254c99321a14470591527dfe682e23bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=Z9mcG5iXKH239523G0iOh1YbbTY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b6c3043e0db43dca2cb5ad38f2f3e59~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=bWCh9pmFJGNX3Xb140QclBNOaFc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ccc27bc8e41e46699b5463915009a57a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=RZKjFNBp%2BrhWOvPervl1NIglw8E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a0770c1ac9b46ac93cf49e8e5ca6d26~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=Gsa734G7K3l2sH2e8zT%2BwB6Jg7g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/33ff7ceab2ee4d42a4f567700a3c838f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=Q9L8K5xZEx5RdfK5M69v2WIvStA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ea6dc0690ba4896aaab7b2ca678c929~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=xeT45s8GfgIzAtM5u4yWZ%2BvcSBc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f5f7e60f7404be389b95de23bf6c137~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMzfmiYvmuLjlkI7nq6_lm6LpmJ8=:q75.awebp?rk3s=f64ab15b&x-expires=1743664390&x-signature=l0gjoxxkD90PP2XYUkGoo2bpoqY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","AIGC","人工智能"],"attachments":null,"extra":null,"language":null},{"title":"mysql---ReadView_MVCC理解学习","url":"https://juejin.cn/post/7486185012388216842","content":"典型回答
\\nMVCC,是Multiversion Concurrency Control的缩写,翻译过来是多版本并发控制,和数据库锁一样,他也是一种并发控制的解决方案。
\\n我们知道,在数据库中,对数据的操作主要有2种,分别是读和写,而在并发场景下,就可能出现以下三种情况:
\\n我们都知道,在没有写的情况下读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。
\\n要想搞清楚MVCC的机制,最重要的一个概念那就是快照读。
\\n所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。如:
\\nSELECT * FROM xx_table WHERE ...\\n
\\n和快照读相对应的另外一个概念叫做当前读,当前读就是读取最新数据,所以,加锁的 SELECT,或者对数据进行增删改都会进行当前读,比如:
\\nSELECT * FROM xx_table LOCK IN SHARE MODE;\\nSELECT * FROM xx_table FOR UPDATE;\\nINSERT INTO xx_table ...\\nDELETE FROM xx_table ...\\nUPDATE xx_table ...\\n
\\n可以说快照读是MVCC实现的基础,而当前读是悲观锁实现的基础。
\\n那么,快照读读到的快照是从哪里读到的呢?换句话说,快照是存在哪里的呢?
\\nundo log是Mysql中比较重要的事务日志之一,顾名思义,undo log是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
\\n这里面提到的存在undo log中的\\"更新前的数据\\"就是我们前面提到的快照。所以,这也是为什么很多人说UndoLog是MVCC实现的重要手段的原因。
\\n那么,一条记录在同一时刻可能有多个事务在执行,那么,undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?
\\n这就需要用到另外几个信息了。
\\n其实,数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有一些重要的隐式字段的:
\\n● db_row_id:隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引。\\n● db_trx_id:对这条记录做了最新一次修改的事务的ID\\n● db_roll_ptr:回滚指针,指向这条记录的上一个版本,其实他指向的就是Undo Log中的上一个版本的快照的地址。
\\n注意,以上字段,只有在聚簇索引的行记录中才会有,而在普通二级索引中是没有这些值的,至于二级索引的MVCC的支持,有单独文章介绍
\\n因为每一次记录变更之前都会先存储一份快照到undo log中,那么这几个隐式字段也会跟着记录一起保存在undo log中,就这样,每一个快照中都有一个db_trx_id字段表示了对这个记录做了最新一次修改的事务的ID ,以及一个db_roll_ptr字段指向了上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面还会用到)
\\n这样,就形成了一个快照链表
\\n有了undo log,又有了几个隐式字段,我们好像还是不知道具体应该读取哪个快照,那怎么办呢?
\\nRead View是如何保证可见性判断的呢?我们先看看Read view 的几个重要属性
\\nRead view 匹配条件规则如下:
\\ntrx_id < min_limit_id
,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。trx_id>= max_limit_id
,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。min_limit_id =<trx_id< max_limit_id
,需腰分3种情况讨论\\n\\n\\n
\\n- (1).如果
\\nm_ids
包含trx_id
,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。- (2)如果
\\nm_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;- (3).如果
\\nm_ids
不包含trx_id
,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
所以,当读取一条记录的时候,经过以上判断,发现记录对当前事务可见,那么就直接返回就行了。那么如果不可见怎么办?没错,那就需要用到undo log了。
\\n当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。
\\n所以,总结一下,在InnoDB中,MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View 用来判断具体哪一个快照是可见的。
\\n其实,根据不同的事务隔离级别,Read View的获取时机是不同的,在RC下,一个事务中的每一次SELECT都会重新获取一次Read View,而在RR下,一个事务中只在第一次SELECT的时候会获取一次Read View。
\\n所以,可重复读这种事务隔离级别之下,因为有MVCC机制,就可以解决不可重复读的问题,因为他只有在第一次SELECT的时候才会获取一次Read View,天然不存在不可重复读的问题了。
","description":"✅如何理解MVCC? 典型回答\\n\\nMVCC,是Multiversion Concurrency Control的缩写,翻译过来是多版本并发控制,和数据库锁一样,他也是一种并发控制的解决方案。\\n\\n我们知道,在数据库中,对数据的操作主要有2种,分别是读和写,而在并发场景下,就可能出现以下三种情况:\\n\\n读-读并发\\n读-写并发\\n写-写并发\\n\\n我们都知道,在没有写的情况下读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。\\n\\n快照读和当前读\\n\\n要想搞清楚MVCC的机制,最重要的一个概念那就是快照读…","guid":"https://juejin.cn/post/7486185012388216842","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-27T06:35:09.752Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4df5089a78a24fc9b2b5f4ceb59d5063~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743663233&x-signature=2QbtOTiW1icu93yjxvdjw6MyXAs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/32fb0c22515842ebaeda438e9f4002b0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743663233&x-signature=UEc7EeY0T2SkqD6P2WuIJbF%2F9k4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"通用树形结构构建工具类-Java","url":"https://juejin.cn/post/7486089532283092992","content":"package com.pig4cloud.pigx.common.core.util.tree;\\n\\nimport java.util.*;\\nimport java.util.function.Function;\\nimport java.util.stream.Collectors;\\n\\n/**\\n * 通用树结构构建工具类\\n *\\n * <p>重要说明:\\n * <ol>\\n * <li>所有节点必须具有唯一ID</li>\\n * <li>父节点不存在时自动成为根节点</li>\\n * <li>节点排序依赖comparator实现</li>\\n * <li>支持循环依赖检测和错误路径提示</li>\\n * </ol>\\n *\\n * @param <T> 原始数据类型\\n * @param <K> 节点ID类型(建议使用包装类型)\\n */\\npublic class TreeBuilder<T, K> {\\n private final Function<T, K> idGetter;\\n private final Function<T, K> parentIdGetter;\\n private final ChildSetter<T> childSetter;\\n private final Comparator<T> comparator;\\n\\n /**\\n * 构造方法\\n */\\n public TreeBuilder(Function<T, K> idGetter,\\n Function<T, K> parentIdGetter,\\n ChildSetter<T> childSetter,\\n Comparator<T> comparator) {\\n\\n this.idGetter = Objects.requireNonNull(idGetter, \\"ID获取器不能为null\\");\\n this.parentIdGetter = Objects.requireNonNull(parentIdGetter, \\"父ID获取器不能为null\\");\\n this.childSetter = Objects.requireNonNull(childSetter, \\"子节点设置器不能为null\\");\\n this.comparator = Objects.requireNonNull(comparator, \\"排序比较器不能为null\\");\\n }\\n\\n /**\\n * 构建完整树结构\\n */\\n public List<T> buildTree(List<T> items) {\\n Objects.requireNonNull(items, \\"节点列表不能为null\\");\\n if (items.isEmpty()) return Collections.emptyList();\\n\\n // 1. 构建数据索引\\n Map<K, T> nodeMap = createNodeMap(items);\\n Map<K, List<T>> parentChildrenMap = items.stream()\\n .collect(Collectors.groupingBy(\\n parentIdGetter,\\n LinkedHashMap::new, // 保持插入顺序\\n Collectors.toList()\\n ));\\n\\n // 2. 循环依赖检测\\n detectCyclicDependencies(items, nodeMap);\\n\\n // 3. 构建树结构\\n nodeMap.forEach((nodeId, node) -> {\\n List<T> children = parentChildrenMap.getOrDefault(nodeId, Collections.emptyList())\\n .stream()\\n .sorted(comparator)\\n .collect(Collectors.toList());\\n\\n childSetter.setChildren(node, Collections.unmodifiableList(children));\\n });\\n\\n // 4. 获取根节点(parentId为null或不存在于nodeMap)\\n return items.stream()\\n .filter(item -> isRootNode(item, nodeMap))\\n .sorted(comparator)\\n .collect(Collectors.toList());\\n\\n }\\n\\n /**\\n * 判断是否为根节点(抽离方法提升可读性)\\n */\\n private boolean isRootNode(T item, Map<K, T> nodeMap) {\\n K parentId = parentIdGetter.apply(item);\\n return parentId == null || !nodeMap.containsKey(parentId);\\n }\\n\\n /**\\n * 构建搜索结果树\\n */\\n public List<T> buildSearchTree(List<T> allItems, Set<K> matchIds) {\\n Objects.requireNonNull(allItems, \\"节点列表不能为null\\");\\n Objects.requireNonNull(matchIds, \\"匹配ID集合不能为null\\");\\n\\n Set<K> relatedIds = findRelatedIds(allItems, matchIds);\\n List<T> relatedItems = allItems.stream()\\n .filter(item -> relatedIds.contains(idGetter.apply(item)))\\n .collect(Collectors.toList());\\n\\n return buildTree(relatedItems);\\n }\\n\\n /**\\n * 创建节点ID映射表(含重复检测)\\n */\\n private Map<K, T> createNodeMap(List<T> items) {\\n Map<K, T> map = new LinkedHashMap<>(items.size());\\n for (T item : items) {\\n K id = idGetter.apply(item);\\n if (map.containsKey(id)) {\\n throw new IllegalArgumentException(String.format(\\n \\"发现重复节点ID: %s (冲突对象1: %s, 冲突对象2: %s)\\",\\n id, map.get(id), item));\\n }\\n map.put(id, item);\\n }\\n return map;\\n }\\n\\n /**\\n * 循环依赖检测核心逻辑\\n */\\n private void detectCyclicDependencies(List<T> items, Map<K, T> nodeMap) {\\n Set<K> verifiedNodes = new HashSet<>();\\n Map<K, K> idToParentMap = items.stream()\\n .collect(Collectors.toMap(idGetter, parentIdGetter));\\n\\n for (T item : items) {\\n K currentId = idGetter.apply(item);\\n if (verifiedNodes.contains(currentId)) continue;\\n\\n Set<K> path = new LinkedHashSet<>();\\n K tracingId = currentId;\\n\\n while (tracingId != null) {\\n if (!path.add(tracingId)) {\\n throw new CyclicDependencyException(buildCyclePath(path, tracingId));\\n }\\n\\n // 短路已验证节点\\n if (verifiedNodes.contains(tracingId)) break;\\n\\n K parentId = idToParentMap.get(tracingId);\\n if (parentId == null) break;\\n\\n // 直接循环检测\\n if (parentId.equals(tracingId)) {\\n throw new CyclicDependencyException(\\"直接循环依赖: \\" + tracingId);\\n }\\n\\n tracingId = parentId;\\n }\\n verifiedNodes.addAll(path);\\n }\\n }\\n\\n /**\\n * 构造循环路径描述\\n */\\n private String buildCyclePath(Set<K> path, K duplicateId) {\\n List<K> pathList = new ArrayList<>(path);\\n int index = pathList.indexOf(duplicateId);\\n List<K> cycle = pathList.subList(index, pathList.size());\\n return \\"检测到循环依赖链: \\" + cycle.stream()\\n .map(Object::toString)\\n .collect(Collectors.joining(\\" → \\"));\\n }\\n\\n /**\\n * 查找相关ID集合(匹配节点+路径节点)\\n */\\n private Set<K> findRelatedIds(List<T> allItems, Set<K> matchIds) {\\n Map<K, K> idToParentMap = allItems.stream()\\n .collect(Collectors.toMap(idGetter, parentIdGetter));\\n\\n return matchIds.stream()\\n .flatMap(id -> traceAncestors(id, idToParentMap).stream())\\n .collect(Collectors.toSet());\\n }\\n\\n /**\\n * 追溯父节点链\\n */\\n private Set<K> traceAncestors(K startId, Map<K, K> idToParentMap) {\\n Set<K> ancestors = new LinkedHashSet<>();\\n K currentId = startId;\\n\\n while (currentId != null && ancestors.add(currentId)) {\\n currentId = idToParentMap.get(currentId);\\n }\\n return ancestors;\\n }\\n\\n /**\\n * 自定义循环依赖异常\\n */\\n public static class CyclicDependencyException extends RuntimeException {\\n public CyclicDependencyException(String message) {\\n super(message);\\n }\\n }\\n\\n /**\\n * 子节点设置接口\\n */\\n @FunctionalInterface\\n public interface ChildSetter<T> {\\n void setChildren(T parent, List<T> children);\\n }\\n\\n /* 快捷构造方法 */\\n\\n public static <T, K> TreeBuilder<T, K> create(\\n Function<T, K> idGetter,\\n Function<T, K> parentIdGetter,\\n ChildSetter<T> childSetter,\\n Comparator<T> comparator) {\\n return new TreeBuilder<>(idGetter, parentIdGetter, childSetter, comparator);\\n }\\n\\n public static <T, K extends Comparable<? super K>> TreeBuilder<T, K> createWithNaturalOrder(\\n Function<T, K> idGetter,\\n Function<T, K> parentIdGetter,\\n ChildSetter<T> childSetter) {\\n return new TreeBuilder<>(\\n idGetter,\\n parentIdGetter,\\n childSetter,\\n Comparator.comparing(idGetter, Comparator.nullsLast(Comparator.naturalOrder()))\\n );\\n }\\n}\\n\\n
\\n本工具类采用泛型设计,可处理任意类型的节点数据,具备以下核心能力:
\\nMap<K, T> nodeMap = createNodeMap(items);\\nMap<K, List<T>> parentChildrenMap = items.stream()\\n .collect(Collectors.groupingBy(...));\\n
\\n采用路径追踪法,时间复杂度O(n):
\\nSet<K> path = new LinkedHashSet<>();\\nwhile (tracingId != null) {\\n if (!path.add(tracingId)) {\\n throw new CyclicDependencyException(...);\\n }\\n // 追溯父节点链\\n}\\n
\\n可检测两种异常情况:
\\n采用两阶段构建模式:
\\n通过ID回溯算法构建有效路径:
\\nSet<K> traceAncestors(K startId) {\\n // 向上追溯所有祖先节点\\n}\\n
\\n确保搜索结果的完整树形结构
\\nchildSetter.setChildren(node, \\n children.stream()\\n .sorted(comparator)\\n .collect(Collectors.toList())\\n);\\n
\\n支持两种排序方式:
\\n自定义异常类型增强可读性:
\\npublic class CyclicDependencyException extends RuntimeException {\\n // 携带具体循环路径信息\\n}\\n
\\n提供明确的错误定位信息:
\\n检测到循环依赖链: 1001 → 1002 → 1003 → 1001\\n
\\n@FunctionalInterface\\npublic interface ChildSetter<T> {\\n void setChildren(T parent, List<T> children);\\n}\\n
\\n使用时可通过Lambda表达式实现:
\\nTreeBuilder<Department, Long> builder = \\n new TreeBuilder<>(..., (parent, children) -> parent.setChildDepts(children));\\n
\\nList<Menu> menus = getFromDB();\\n\\nTreeBuilder<Menu, Integer> builder = TreeBuilder.create(\\n Menu::getId,\\n Menu::getParentId,\\n (parent, children) -> parent.setChildren(children),\\n Comparator.comparing(Menu::getSortOrder)\\n);\\n\\nList<Menu> tree = builder.buildTree(menus);\\n
\\nSet<Integer> matchIds = searchService.findIds(\\"关键\\");\\nList<Menu> resultTree = builder.buildSearchTree(allMenus, matchIds);\\n
\\n重要说明:\\n *
大家好,我是田螺。
\\n我们日常开发的时候,经常说到异步编程。比如说,在注册接口,我们在用户注册成功时,用异步发送邮件通知用户。
\\n那么,小伙伴们,你知道实现异步编程,一共有多少种方式吗? 我给大家梳理了9种~~
\\nThread和Runnable是最基本的异步编程方式,直接使用Thread和Runnable来创建和管理线程。
\\npublic class Test {\\n public static void main(String[] args) {\\n System.out.println(\\"田螺主线程:\\" + Thread.currentThread().getName());\\n Thread thread = new Thread(() -> {\\n try {\\n Thread.sleep(1000);\\n System.out.println(\\"田螺异步线程测试:\\"+Thread.currentThread().getName());\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n thread.start();\\n }\\n}\\n
\\n但是呢,日常工作中,不推荐直接使用Thread和Runnable,因为它有这些缺点:
\\n针对Thread和Runnable的缺点,我们可以使用线程池呀,线程池主要有这些优点:
\\n有些小伙伴说,我们可以直接使用Executors提供的线程池呀,非常快捷方便,比如:
\\n- Executors.newFixedThreadPool\\n- Executors.newCachedThreadPool\\n
\\n简单demo代码如下:
\\npublic class Test {\\n public static void main(String[] args) {\\n System.out.println(\\"田螺主线程:\\" + Thread.currentThread().getName());\\n ExecutorService executor = Executors.newFixedThreadPool(3);\\n \\n executor.execute(()->{\\n System.out.println(\\"田螺线程池方式,异步线程:\\" + Thread.currentThread().getName());\\n });\\n }\\n}\\n// 运行结果:\\n田螺主线程:main\\n田螺线程池方式,异步线程:pool-1-thread-1\\n
\\n使用使用Executors提供线程池,如newFixedThreadPool,虽然简单快捷,但是呢,它的阻塞队列十无界的!
\\n\\n\\nnewFixedThreadPool默认使用LinkedBlockingQueue作为任务队列,而LinkedBlockingQueue是一个无界队列(默认容量为Integer.MAX_VALUE)。如果任务提交速度远大于线程池处理速度,队列会不断堆积任务,最终可能导致内存耗尽.
\\n
因此,我们一般推荐使用自定义线程池,来开启异步。
\\npublic class Test {\\n public static void main(String[] args) {\\n // 自定义线程池\\n ThreadPoolExecutor executor = new ThreadPoolExecutor(\\n 2, // 核心线程数\\n 4, // 最大线程数\\n 60, // 空闲线程存活时间\\n TimeUnit.SECONDS, // 时间单位\\n new ArrayBlockingQueue<>(8), // 任务队列\\n Executors.defaultThreadFactory(), // 线程工厂\\n new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略\\n );\\n\\n System.out.println(\\"田螺主线程:\\" + Thread.currentThread().getName());\\n executor.execute(() -> {\\n try {\\n Thread.sleep(500); // 模拟耗时操作\\n System.out.println(\\"田螺自定义线程池开启异步:\\" + Thread.currentThread().getName());\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n }\\n}\\n//输出\\n田螺主线程:main\\n田螺自定义线程池开启异步:pool-1-thread-1\\n
\\n有些小伙伴说,如果我们期望异步编程可以返回结果呢?
\\n\\n\\n那我们可以使用Future和Callable。Future和Callable是Java 5引入的,用于处理异步任务。Callable类似于Runnable,但它可以返回一个结果,并且可以抛出异常。Future表示异步计算的结果。
\\n
简单使用demo:
\\npublic class Test {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n // 自定义线程池\\n ThreadPoolExecutor executor = new ThreadPoolExecutor(\\n 2, // 核心线程数\\n 4, // 最大线程数\\n 60, // 空闲线程存活时间\\n TimeUnit.SECONDS, // 时间单位\\n new ArrayBlockingQueue<>(8), // 任务队列\\n Executors.defaultThreadFactory(), // 线程工厂\\n new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略\\n );\\n\\n System.out.println(\\"田螺主线程:\\" + Thread.currentThread().getName());\\n\\n Callable<String> task = () -> {\\n Thread.sleep(1000); // 模拟耗时操作\\n System.out.println(\\"田螺自定义线程池开启异步:\\" + Thread.currentThread().getName());\\n return \\"Hello, 公众号:捡田螺的小男孩!\\";\\n };\\n\\n Future<String> future = executor.submit(task);\\n\\n String result = future.get(); // 阻塞直到任务完成\\n System.out.println(\\"异步结果:\\"+result);\\n }\\n}\\n\\n运行结果:\\n田螺主线程:main\\n田螺自定义线程池开启异步:pool-1-thread-1\\n异步结果:Hello, 公众号:捡田螺的小男孩!\\n
\\nCompletableFuture是Java 8引入的,提供了更强大的异步编程能力,支持链式调用、异常处理、组合多个异步任务等。
\\n简单使用demo:
\\npublic class Test {\\n public static void main(String[] args) throws ExecutionException, InterruptedException {\\n // 自定义线程池\\n ThreadPoolExecutor executor = new ThreadPoolExecutor(\\n 2, // 核心线程数\\n 4, // 最大线程数\\n 60, // 空闲线程存活时间\\n TimeUnit.SECONDS, // 时间单位\\n new ArrayBlockingQueue<>(8), // 任务队列\\n Executors.defaultThreadFactory(), // 线程工厂\\n new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略\\n );\\n\\n System.out.println(\\"田螺主线程:\\" + Thread.currentThread().getName());\\n\\n CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {\\n try {\\n Thread.sleep(1000); // 模拟耗时操作\\n System.out.println(\\"田螺CompletableFuture开启异步:\\" + Thread.currentThread().getName());\\n return \\"Hello, 公众号:捡田螺的小男孩!\\";\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n return \\"Hello, 公众号:捡田螺的小男孩!\\";\\n },executor);\\n\\n future.thenAccept(result -> System.out.println(\\"异步结果:\\" + result));\\n future.join();\\n }\\n}\\n\\n
\\n有些时候,我们希望开启异步,将一个大任务拆分成多个小任务(Fork),然后将这些小任务的结果合并(Join)。这时候,我们就可以使用ForkJoinPool啦~。
\\n\\n\\nForkJoinPool 是 Java 7 引入的一个线程池实现,专门用于处理分治任务。
\\n\\n
\\n- 它的特点就是任务拆分(Fork)和结果合并(Join),以及工作窃取(Work-Stealing)。
\\n- ForkJoinPool 特别适合处理递归任务或可以分解的并行任务。
\\n
简单使用demo:
\\npublic class Test {\\n public static void main(String[] args) {\\n ForkJoinPool pool = new ForkJoinPool(); // 创建 ForkJoinPool\\n int result = pool.invoke(new SumTask(1, 100)); // 提交任务并获取结果\\n System.out.println(\\"1 到 100 的和为: \\" + result);\\n }\\n\\n static class SumTask extends RecursiveTask<Integer> {\\n private final int start;\\n private final int end;\\n\\n SumTask(int start, int end) {\\n this.start = start;\\n this.end = end;\\n }\\n\\n @Override\\n protected Integer compute() {\\n if (end - start <= 10) { // 直接计算小任务\\n int sum = 0;\\n for (int i = start; i <= end; i++) sum += i;\\n return sum;\\n } else { // 拆分任务\\n int mid = (start + end) / 2;\\n SumTask left = new SumTask(start, mid);\\n SumTask right = new SumTask(mid + 1, end);\\n left.fork(); // 异步执行左任务\\n return right.compute() + left.join(); // 等待左任务完成并合并结果\\n }\\n }\\n }\\n\\n}\\n
\\nSpring 提供了 @Async 注解来实现异步方法调用,非常方便。使用 @Async 可以让方法在单独的线程中执行,而不会阻塞主线程。
\\nSpring @Async 的使用步骤其实很简单:
\\n启用异步支持:
\\n@SpringBootApplication\\n@EnableAsync // 启用异步支持\\npublic class AsyncDemoApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(AsyncDemoApplication.class, args);\\n }\\n}\\n
\\n异步服务类
\\n@Service\\npublic class TianLuoAsyncService {\\n\\n @Async // 标记为异步方法\\n public void asyncTianLuoTask() {\\n try {\\n Thread.sleep(2000); // 模拟耗时操作\\n System.out.println(\\"异步任务完成,线程: \\" + Thread.currentThread().getName());\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n默认情况下,Spring 使用一个简单的线程池(SimpleAsyncTaskExecutor
),每次调用都会创建一个新线程。因此,在使用Spring的@Async进行异步时,推荐使用自定义线程池。
如下:
\\n@Configuration\\npublic class AsyncConfig {\\n\\n @Bean(name = \\"taskExecutor\\")\\n public Executor taskExecutor() {\\n ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\\n executor.setCorePoolSize(10); // 核心线程数\\n executor.setMaxPoolSize(20); // 最大线程数\\n executor.setQueueCapacity(50); // 队列容量\\n executor.setThreadNamePrefix(\\"AsyncThread-\\"); // 线程名前缀\\n executor.initialize();\\n return executor;\\n }\\n}\\n
\\n然后,在 @Async 注解中指定线程池的名称:
\\n@Async(\\"taskExecutor\\") // 指定使用自定义的线程池\\npublic void asyncTianLuoTask() {\\n // 方法逻辑\\n}\\n
\\n我们在提到MQ的时候,经常提到,它有异步处理、解耦、流量削锋。 是的,MQ经常用来实现异步编程。
\\n简要代码如下:先保存用户信息,然后发送注册成功的MQ消息
\\n // 用户注册方法\\n public void registerUser(String username, String email, String phoneNumber) {\\n // 保存用户信息(简化版)\\n userService.add(buildUser(username,email,phoneNumber))\\n // 发送消息\\n String registrationMessage = \\"User \\" + username + \\" has registered successfully.\\";\\n // 发送消息到队列\\n rabbitTemplate.convertAndSend(\\"registrationQueue\\", registrationMessage);\\n }\\n
\\n消费者从队列中读取消息并发送短信或邮件:
\\n@Service\\npublic class NotificationService {\\n\\n // 监听消息队列中的消息并发送短信/邮件\\n @RabbitListener(queues = \\"registrationQueue\\")\\n public void handleRegistrationNotification(String message) {\\n // 这里可以进行短信或邮件的发送操作\\n System.out.println(\\"Sending registration notification: \\" + message);\\n\\n // 假设这里是发送短信的操作\\n sendSms(message);\\n\\n // 也可以做其他通知(比如发邮件等)\\n sendEmail(message);\\n }\\n }\\n
\\n可以使用的是类似 Hutool 工具库中的 ThreadUtil,它提供了丰富的线程池管理和异步任务调度功能。
\\n先引入依赖:
\\n<dependency>\\n <groupId>cn.hutool</groupId>\\n <artifactId>hutool-all</artifactId>\\n <version>5.8.11</version> <!-- 请使用最新版本 --\x3e\\n</dependency>\\n
\\n最简单的,可以直接使用ThreadUtil.execute
执行异步任务
public class Test {\\n public static void main(String[] args) {\\n System.out.println(\\"田螺主线程\\");\\n ThreadUtil.execAsync(\\n () -> {\\n System.out.println(\\"田螺异步测试:\\" + Thread.currentThread().getName());\\n }\\n );\\n }\\n}\\n//输出\\n田螺主线程\\n田螺异步测试:pool-1-thread-1\\n
","description":"前言 大家好,我是田螺。\\n\\n我们日常开发的时候,经常说到异步编程。比如说,在注册接口,我们在用户注册成功时,用异步发送邮件通知用户。\\n\\n那么,小伙伴们,你知道实现异步编程,一共有多少种方式吗? 我给大家梳理了9种~~\\n\\n使用Thread和Runnable\\n使用Executors提供线程池\\n使用自定义线程池\\n使用Future和Callable\\n使用CompletableFuture\\n使用ForkJoinPool\\nSpring的@Async异步\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\n1…","guid":"https://juejin.cn/post/7485980624189931559","author":"捡田螺的小男孩","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T23:58:28.840Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"\\"慢SQL\\"治理的几点思考","url":"https://juejin.cn/post/7485965177096781874","content":"今年初团队开始推行“服务稳定性问题治理专项”。通过错误日志、慢SQL、接口性能等各项指标的优化,进一步提升系统稳定性与可靠性。在此契机之下,本文将从“慢SQL治理”的角度,通过部分实际案例,分析其原理,做一些阶段性总结和思考。
\\n某日线上突发告警,发现有一条慢SQL
\\nselect * from xxxx_supplier_extra where column_key = \'xxxx\'\\n
\\n作为有“临床经验”的老司机,第一直觉是先用explain分析。explain用了毫秒级的时间就输出了这样一份执行计划,果然它很快,两分钟都不到。
\\n通过“key=null”,我们不难发现,这条SQL走了全表扫描。这是一条简单的SQL案例,我们借用来分析MYSQL背后的成本评估原理。
\\n执行SQL前,优化器会先选择它认为成本最低的方案。正如同有电梯坐电梯,没电梯爬楼梯。
\\n由于没有可供选择的索引,执行器选了全表扫描。查询成本一般由IO成本和CPU成本组成。众所周知,表数据是存储在磁盘的,每次读磁盘上的数据都 产生IO,受制于磁盘的物理属性,IO往往占查询成本的大头。举个例子,key_name=\\"input_time\\"这行记录存储在磁盘页A中,读完这行后,又读了页B中的数据key_name=\\"input_state\\"。此时多了一次IO,成本就有多一些。
\\n因为全表扫描是发生在聚簇索引上,首先会估算整个聚簇索引占用的页面数,以及表的记录数,再计算IO成本和扫描成本(可以理解为CPU成本)。
\\n计算口径如下:
\\nIO成本:页面数 x io_block_read_cost(IO成本数 默认0.25) + 1.0\\n\\nCPU成本:记录数 x cpu_tuple_cost (扫描成本数 默认0.1)\\n\\n总成本 = IO成本 + CPU成本\\n
\\nMySQL的IO成本默认基于随机IO计算(io_block_read_cost=1.0
),而非顺序IO。这里因为全表扫描是顺序IO,io_block_read_cost的默认值则为0.25。表记录越多,占用的页数则随着增长,其查询成本也就不断累加。
看到慢 SQL 直接加索引就好吗?基于前面全表扫描的原理,是否看到慢 SQL 直接加索引就完事了?
\\n以前面SQL为例,当我们为‘column_key’字段加索引后,测试环境 explain 分析能命中索引,但上线后还是咔咔咔出现慢查询。
\\n这是因为如果索引字段的区分度不够,优化器会认为查找成本过大,此时还是选择走全表扫描。而测试环境表记录较少的情况下,优化器觉得回表开销不大,就能命中索引,这也解释了为什么两者的执行计划不同。
\\n索引能否命中往往与查询条件以及数据分布有关。
\\n如何在索引设计之初就规避此类问题?
\\n网上一些文章会提到基于该列的业务属性来区分,例如性别字段只有两个值:“sex=男,sex=女”。
\\n由于大部分记录都拥有相同值,数据区分度不大,所以容易成为低效索引。
\\n除此之外,可以使用该语句计算区分度:
\\nSELECT COUNT(DISTINCT column_name ) / COUNT(*)\\n
\\n区分度低于10%的字段避免单独建索引。 对于联合索引而言,也应尽量将区分度高的字段放在前面。
\\n值得注意的是,即使该字段的区分度能够建立索引。也要根据已有索引和查询场景做综合取舍,要避免在同一个表上堆砌过多索引。
\\n内存碎片对优化器的影响
\\n上文提到了成本估算是基于页数以及记录数计算的,这些数据来源于库中的统计信息。当内存碎片过大时,如果出现库表的统计信息未及时更新,也会因为优化器评估的结果与实际差距太大,从而影响实际执行效果。
\\n内存碎片导致慢查询
\\n举个例子:某日xxxx_price表产生慢查询告警,该表作为统计数据表,其查询SQL较简单,单纯从SQL上分析并没有太多问题。
\\n联想到前阵子,该表由于历史原因,积压了2亿+无效数据。后面做了批量删除清理,由此推断可能是内存碎片导致的。
\\n通过查看表的TABLE STATUS
\\nSHOW TABLE STATUS LIKE \'xxx_price\'\\n
\\n输出结果显示:Data_free=54835281920。碎片占了大量空间。
\\n当内存碎片过多时,首当其冲会让物理IO被放大。原本“id=1,id=10,id=15”这三条记录读一次数据页就能够拿到,现在由于这几条数据被分散在多个数据页中,从而引发IO次数增多。同时,数据页是加载到缓冲池(Buffer Pool)里面的,这也会导致缓存命中率下降。
\\n一般情况下,有一定的内存碎片是正常情况。但当内存碎片的占比过高时,则需要关注。
\\n为什么会产生内存碎片?
\\n• 删除标记(Delete Mark)
\\n执行 DELETE时,InnoDB仅标记记录为“已删除”,物理空间不立即释放(为MVCC和回滚保留可能)。如删除页A中的记录R1,R1位置变为“空洞\\",但页A仍属于该表。
\\n• 页内空洞 (Page Fragmentation) 频繁删除导致页内出现大量空洞,页实际利用率下降(如页利用率从90%降到50%)。 读取相同数据量需要访问更多页,IO次数增加。
\\n• 页合并的局限性
\\nInnoDB仅合并相邻空闲页,若页分散则无法合并(如页A、页C空闲但中间夹着已使用的页B)。
\\n某日发现线上出现一些重复异常,显示查询某参考价表的数据重复了,通过排查insert的两条数据,发现其实并没有重复。看了表结构才发现,原来表某个字段加了唯一索引,且唯一索引键使用了前缀索引。
\\nunique (cate_id, brand_id, model_id, key_props(10))\\n
\\n由于 key_props字段使用了前缀索引,因此索引树的叶子节点,并没有完整地存储整个字符串,而是截取字符串前面N个字符。这可以有效地节省索引空间。但这里的问题是使用了唯一索引,导致两个不同的字符串,只是前缀相同就触发重复冲突。
\\n一般对于长度过长的字段,加前缀索引是一种选择,但像案例中在唯一约束中使用前缀索引,则需要保证前缀唯一性.
\\n除了在单个索引下检索数据,其实还有可能在多个索引上检索。在符合特定条件下,通过索引合并,能够减少回表带来的消耗。如:
\\nselect * from xxx_supplier_order where k1 = \'123\' and k2 = \'345\'\\n
\\n假设xxx_supplier_order的主键是id字段。k1和k2分别是两个独立的索引字段。当满足一定条件时(k1的叶子结点上id是有序的,k2也是id有序)。此时MYSQL可以根据 k1 = \'123\'在索引上检索出id,再根据 k2 = \'345\'在索引上检索id。并将两次检索的id取交集,就可以筛选出符合条件的id并回表执行。
\\n这样做的好处在于
\\n1.要同时满足 k1 = \'123\' and k2 = \'345\'的记录,其id必然存在于两个索引树上,通过交集,筛选出少量符合条件的id才去回表,理论上能够有效减少回表的次数。
\\n2.id有序性有利于取交集操作,如某次检索。从k1上读到id=1,再从k2读到id=2,此时就可以判定id=1不满足k2的条件。另外,通过有序id,也能够确保每次回表能够有序,避免随机IO。
\\n如果SQL没有问题,那么关注点可以放在mysql实例的资源开销上了。因为造成慢查询的原因不单只是SQL本身,有可能是磁盘负载,CPU以及网络 等方面的资源不足引起了。举个例子:
\\n某统计服务数据库,其库表大部分数据源自大数据平台的异步交换任务。某个时间段有多个交换任务往库表里面导入大批量数据,从而引发了磁盘等资源的负载增加,带来慢查询。
\\n另外有时候事务的问题也需要关注。比如当长事务导致Undo Log膨胀时,容易使得扫描效率降低。同时\\nBuffer Pool中缓存页因旧版本数据过多,其缓存命中率也会下降。\\n我们可以通过 SHOW ENGINE INNODB STATUS
中History list length
值是否飙升,加以判断。
本文试图通过一些案例,分析其背后的原理。至于覆盖索引,联合索引等其它内容,相信网上有很多类似的内容,这里不多赘述。慢SQL治理是一个值得关注的问题。重要的是理解MySQL索引,事务等方面执行原理,然后现实使用场景中灵活分析和运用。
\\n\\n> 转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
> 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~
关于作者
\\n邱腾龙 B2C供应链研发工程师
","description":"一.背景 二.MySQL是如何评估成本的?\\n三.即使加了索引,也没有起作用\\n四.内存碎片也是一个值得关注的问题\\n五.前缀索引的坑\\n六.索引合并\\n七.有时候SQL没啥问题,但还是报了慢查询?\\n八.总结\\n一.背景\\n\\n今年初团队开始推行“服务稳定性问题治理专项”。通过错误日志、慢SQL、接口性能等各项指标的优化,进一步提升系统稳定性与可靠性。在此契机之下,本文将从“慢SQL治理”的角度,通过部分实际案例,分析其原理,做一些阶段性总结和思考。\\n\\n二.MySQL是如何评估成本的?\\n\\n某日线上突发告警,发现有一条慢SQL\\n\\nselect * from xxxx…","guid":"https://juejin.cn/post/7485965177096781874","author":"转转技术团队","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T12:00:48.849Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a87c4e22c93643f0934201d7d03efb52~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2s6L2s5oqA5pyv5Zui6Zif:q75.awebp?rk3s=f64ab15b&x-expires=1743595248&x-signature=Zo00CfZH%2FJB0gBnzUnZluaJA2LA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9587d5762b34c56892f373d9b47b670~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2s6L2s5oqA5pyv5Zui6Zif:q75.awebp?rk3s=f64ab15b&x-expires=1743595248&x-signature=DGvI5XmRkBcymglZn%2FJ%2BhcQUl58%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aea4da97912749fa87b79e319a4f01fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2s6L2s5oqA5pyv5Zui6Zif:q75.awebp?rk3s=f64ab15b&x-expires=1743595248&x-signature=BFMZPdXJmXFPeu0mv4kJCp3w3Hw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7492794c0a4840f4bc2712621d3985aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2s6L2s5oqA5pyv5Zui6Zif:q75.awebp?rk3s=f64ab15b&x-expires=1743595248&x-signature=tOSdMSeZeMmoogXu4dw9Z%2BsG4dM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","MySQL","性能优化","数据库"],"attachments":null,"extra":null,"language":null},{"title":"JDK废弃了观察者模式,我们还能用它吗?","url":"https://juejin.cn/post/7485725589611429903","content":"JDK 中曾直接提供对观察者模式的支持,但因其设计局限性,现已被标记为“过时”(Deprecated)。不过,观察者模式的思想在 JDK 的事件处理、spring框架等仍有广泛应用。下面我将从实际的问题出发,带你详细了解观察者设计模式。
\\n观察者模式(Observer Pattern)定义对象间的一对多依赖关系,当一个对象(被观察者/主题)状态发生改变时,所有依赖它的对象(观察者)都会自动收到通知并更新。其核心目的是解耦主题与观察者,使二者能够独立变化而不互相影响。
\\n\\n\\nGoF定义:定义一种订阅机制,在对象事件发生时通知多个“观察”该对象的其他对象。
\\n
假如你有两种类型的对象: 顾客和 商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。
\\n顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。
\\n另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。
\\n我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。
\\n拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。
\\n观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括
\\n现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。
\\n实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。
\\n因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。
\\n如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。
\\n微信公众号与订阅用户:
\\nupdate()
方法。update()
逻辑(如更新UI、执行业务)。// 主题接口\\ninterface Subject {\\n void registerObserver(Observer o);\\n void removeObserver(Observer o);\\n void notifyObservers();\\n}\\n\\n// 具体主题:天气预报站\\nclass WeatherStation implements Subject {\\n private List<Observer> observers = new ArrayList<>();\\n private float temperature;\\n\\n public void setTemperature(float temp) {\\n this.temperature = temp;\\n notifyObservers();\\n }\\n\\n @Override\\n public void registerObserver(Observer o) { observers.add(o); }\\n @Override\\n public void removeObserver(Observer o) { observers.remove(o); }\\n @Override\\n public void notifyObservers() {\\n for (Observer o : observers) {\\n o.update(temperature);\\n }\\n }\\n}\\n\\n// 观察者接口\\ninterface Observer {\\n void update(float temperature);\\n}\\n\\n// 具体观察者:手机天气App\\nclass PhoneApp implements Observer {\\n @Override\\n public void update(float temperature) {\\n System.out.println(\\"手机App收到温度更新:\\" + temperature + \\"℃\\");\\n }\\n}\\n\\n// 使用\\npublic class Client {\\n public static void main(String[] args) {\\n WeatherStation station = new WeatherStation();\\n PhoneApp app = new PhoneApp();\\n station.registerObserver(app);\\n station.setTemperature(25.5f); // 触发通知\\n }\\n}\\n
\\n仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。
\\n声明订阅者接口。 该接口至少应声明一个 update方法。
\\n声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。
\\n确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。
\\n\\n\\n但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。
\\n
创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。
\\n在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。
\\n\\n\\n但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。
\\n
优点:
\\n缺点:
\\nspring中的ApplicationListener就是典型的观察者模式的应用,其中各个角色对应关系如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n组件/角色 | 对应观察者模式中的角色 | 作用 |
---|---|---|
ApplicationEvent | 事件对象(状态/消息载体) | 封装事件数据(如用户注册事件、订单创建事件)。 |
ApplicationListener | 观察者(Observer) | 监听特定事件并定义响应逻辑(如发送邮件、记录日志)。 |
ApplicationEventPublisher | 主题(Subject) | 负责发布事件,通知所有监听该事件的观察者。 |
观察者模式是构建松耦合、事件驱动系统的核心工具,广泛应用于GUI框架、消息中间件等场景。在实现时需权衡实时性与性能,并谨慎处理观察者的生命周期,避免内存泄漏。
\\n如果文章对你有帮助,点个免费的赞鼓励一下吧!关注gzh:加瓦点灯, 每天推送干货知识!
","description":"观察者模式:解耦对象间的依赖关系 JDK 中曾直接提供对观察者模式的支持,但因其设计局限性,现已被标记为“过时”(Deprecated)。不过,观察者模式的思想在 JDK 的事件处理、spring框架等仍有广泛应用。下面我将从实际的问题出发,带你详细了解观察者设计模式。\\n\\n意图\\n\\n观察者模式(Observer Pattern)定义对象间的一对多依赖关系,当一个对象(被观察者/主题)状态发生改变时,所有依赖它的对象(观察者)都会自动收到通知并更新。其核心目的是解耦主题与观察者,使二者能够独立变化而不互相影响。\\n\\nGoF定义:定义一种订阅机制…","guid":"https://juejin.cn/post/7485725589611429903","author":"加瓦点灯","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T08:42:18.343Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9f337294438d4325b48f2b2124a1b4d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqg55Om54K554Gv:q75.awebp?rk3s=f64ab15b&x-expires=1743583337&x-signature=7a%2Bu17THMyhgLInTC3iLNaFDxiU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/794e9fa297404100983d6c469c72415e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqg55Om54K554Gv:q75.awebp?rk3s=f64ab15b&x-expires=1743583337&x-signature=%2Behnzgt4b6%2BQ6h%2FpNm%2FqCbzI26w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5eb9a0adfb184fde88bf667e3491d520~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqg55Om54K554Gv:q75.awebp?rk3s=f64ab15b&x-expires=1743583337&x-signature=kPor69MOp1aPd6wYIG3%2F6KBWGwQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b365900791344753bc935f3d36b3a4c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yqg55Om54K554Gv:q75.awebp?rk3s=f64ab15b&x-expires=1743583337&x-signature=%2B0Bw2UUk6WIjpGD7WMXqt5zpVLc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"java--死锁,死循环会导致CPU使用率升高吗?","url":"https://juejin.cn/post/7485729208108695562","content":"典型回答
\\n死循环会导致CPU使用率升高。当代码中出现死循环时,涉及的线程会不断执行循环中的指令而不会退出,因此会持续占用处理器资源。这种情况下,CPU会花费所有可用的时间片去执行这个无尽的循环,导致几乎没有资源剩余来处理其他任务或线程。
\\nTalk Is Cheap,Show Me The Code!!!
\\n我们可以通过实验验证上面的结论,以下实验在8C16G的物理机上实现。内存total=15g,14g可用,因为有1g是因为Linux内核分配给SLAB了。
编写死循环代码:
\\npublic class Main {\\n\\npublic static void main(String[] args) {\\n\\nwhile (true){\\n\\nSystem.out.println(System.currentTimeMillis());\\n\\n//死循环输出系统时间戳,强制产生2个系统调用,可以看到CPU us 和sy 占比都会升高\\n\\n}\\n\\n}\\n\\n}\\n
\\n没有跑以上代码之前的CPU使用率下图所示,us代表用户态1%,sy代表内核态0.5%。
\\n运行代码后,CPU使用率 us和sy都有升高,同时进程列表中的java进程CPU使用一列也大幅度升高。
\\n注意:俩个java进程,一个是IDEA编译器的,一个是通过IDEA启动的测试代码,由于死循环疯狂在IDEA控制台输出系统时间戳,导致IDEA的CPU使用率也升高了。
\\n如果把死循环输出的那一行注释掉,可以看到只有us用户态的使用率升高了,同时IDEA的进程使用率不会升高。原因是System.out.println()和System.currentTimeMillis()都会产生系统调用进入内核态,注释掉代码后,while死循环只是用户态的程序代码,不需要进入内核态了。
\\n典型回答
\\n死锁不会导致升高,甚至可能降低。因为死锁发生时,涉及的线程会在等待获取锁时被挂起,而不是处于忙碌等待状态。因此,这些线程不会占用CPU资源进行计算,但它们会保持在等待状态,直到死锁被解决。
\\nTalk Is Cheap,Show Me The Code!!!
\\n我们可以通过实验验证上面的结论,以下实验在8C16G的物理机上实现。内存total=15g,14g可用,因为有1g是因为Linux内核分配给SLAB了。
\\n编写死锁代码:
\\npublic class Main {\\n public static void main(String[] args) {\\n int threadCnt = 2000; //多创建一些线程观察CPU使用率更合理\\n Object[] locks = new Object[threadCnt];\\n for (int i = 0; i < threadCnt; i++) { //初始化线程锁对象\\n locks[i] = new Object();\\n }\\n\\n for (int i = 0; i < threadCnt; i++) {\\n //为了产生死锁,我们需要一个线程抢占2把锁,以下代码控制创建时相邻2个线程分别拿到相同的锁\\n if (i % 2 == 0) { //确保前一个线程拿锁顺序:锁1->锁2\\n new Thread(new DeadLockTest(locks[i], locks[i + 1])).start(); \\n } else { //确保下一个线程拿锁顺序:锁2->锁1\\n new Thread(new DeadLockTest(locks[i], locks[i - 1])).start(); \\n }\\n }\\n\\n }\\n\\n public static class DeadLockTest implements Runnable {\\n private final Object lock1;\\n private final Object lock2;\\n\\n public DeadLockTest(Object lock1, Object lock2) {\\n this.lock1 = lock1;\\n this.lock2 = lock2;\\n }\\n\\n @Override\\n public void run() {\\n synchronized (lock1) {\\n System.out.println(Thread.currentThread().getName() + \\" get Lock:\\" + lock1); //打印线程名抢到的锁\\n try {\\n Thread.sleep(1);//休眠1毫秒保证别的线程有机会拿到下面依赖的锁\\n } catch (InterruptedException e) {\\n Thread.currentThread().interrupt();\\n }\\n System.out.println(Thread.currentThread().getName() + \\" wait Lock:\\" + lock2);//打印线程名要拿的锁\\n synchronized (lock2) {//以下会由于拿锁顺序不正确,产生死锁\\n System.out.println(Thread.currentThread().getName() + \\" get Lock:\\" + lock2);//这里由于死锁不会打印出来\\n }\\n }\\n }\\n }\\n}\\n
\\n为了验证死锁线程会不会导致CPU使用率升高,我们需要弄许多线程死锁,这样才能模拟出线上环境业务线程多,并且因为业务代码错误导致的死锁,来观察对CPU的影响。 运行前的CPU使用率
\\n运行后的CPU使用率
\\n从图中可以看到CPU使用率并没有明显升高,java进程占用的CPU非常低。
\\njstack -l
\\n选一个Thread1查看一下线程状态:
\\njps -l |grep -E \'[0-9].Main\' |awk \'{print $1}\' #拿到测试代码Java进程pid\\n\\njstack -l <java进程pid>|grep Thread1| awk -v FS=\'nid=| \' \'{print $9}\' |xargs printf \'%d\\\\n\' #提取死锁线程Thread1的线程pid\\n\\ntop -Hp <java进程pid> #查看线程状态\\n
\\n第一列为线程的pid,可以看到状态列为S,代表线程处在sleeping状态,因为拿不到锁让出CPU的使用权。 CPU使率用是统计online-cpu的任务,即任务状态为running的任务。所以印证了死锁不会导致CPU使用率升高的结论。之所以线上出现死锁使用率降低,是因为死锁后业务代码无法继续运行,导致使用率会降低。
\\nApache Kafka 4.0 正式发布了,这是一次里程碑式的版本更新。这次更新带来的改进优化非常多,不仅简化了 Kafka 的运维,还显著提升了性能,扩展了应用场景。
\\n我这里简单聊聊我觉得最重要的 3 个改动:
\\n详细更新介绍可以参考官方文档:www.confluent.io/blog/introd… 。
\\n在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用(ZK 模式)。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式(Kafka Raft),不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的、单进程的方式来使用 Kafka。
\\nKRaft 模式在后续的版本中不断完善,直到 Kafka 3.3.1,被正式标记为生产环境可用(Production Ready)。
\\nKafka 4.0 则迈出了更大的一步——彻底移除了对 Zookeeper 的支持,并默认采用 KRaft 模式。
\\n需要注意的是,Kafka 4.0 不再支持以 ZK 模式运行或从 ZK 模式迁移。如果你的 Kafka 仍然使用 ZK 模式,官方建议先升级到过渡版本(如 Kafka 3.9),执行 ZK 迁移后再升级到目标版本。
\\n详细介绍:developer.confluent.io/learn/kraft…
\\n全新的消费者重平衡协议正式 GA 了,可以告别“stop-the-world”重新平衡了!这个协议的核心思想最早在 Kafka 2.4 版本 通过KIP-429: Kafka Consumer Incremental Rebalance Protocol 实现。
\\n新协议的核心在于增量式重平衡,不再依赖全局同步屏障,而是由组协调器(GroupCoordinator)驱动,各个消费者独立地与协调器交互,只调整自身相关的分区分配,从而将全局的“停顿”分解成多个局部的、微小的调整。只有需要调整的消费者和分区才会发生变更,未受影响的消费者可以继续正常工作(旧有的再均衡协议依赖于组范围内的同步屏障,所有消费者都需要参与,这会导致明显的“停顿”)。
\\n详细介绍:cwiki.apache.org/confluence/…
\\nKafka 4.0 通过引入共享组 (Share Group) 机制提供了类似队列的功能。不过,它并非真正意义上的队列,而是利用 Kafka 已有的主题(Topic)和分区(Partition)机制,结合新的消费模式和记录确认机制来实现类似队列的行为。
\\nKafka 发布订阅模型
共享组解决了传统 Kafka 消费者组(Consumer Groups)在某些场景下的局限性,主要体现在:
\\n共享组还支持无队列深度限制和基于时间点的恢复能力,极大地扩展了 Kafka 的应用场景。
\\n详细介绍:cwiki.apache.org/confluence/…
\\n关于 Kafka 以及其他常见消息队列的知识点/面试题总结,大家可以参考这两篇文章:
\\n虽然RR的隔离级别可以在一定程度上避免脏读、不可重复读和幻读等问题,但是,对于很多大型的互联网来说,会愿意将数据库的默认隔离级别调整成并发度更高的RC级别,从而,提升并发度并且降低发生死锁的概率。
\\n扩展知识
\\n我们需要先来弄清楚一下 RR 和 RC 的区别,分析下各自的优缺点。
\\n一致性读,又称为快照读。快照即当前行数据之前的历史版本。快照读就是使用快照信息显示基于某个时间点的查询结果,而不考虑与此同时运行的其他事务所执行的更改。
\\n在MySQL 中,只有READ COMMITTED 和 REPEATABLE READ这两种事务隔离级别才会使用一致性读。
\\n在 RR 中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照。
\\n在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本。
\\n在数据库的 RC 这种隔离级别中,还支持\\"半一致读\\" ,一条update语句,如果 where 条件匹配到的记录已经加锁,那么InnoDB会返回记录最近提交的版本,由MySQL上层判断此是否需要真的加锁。
\\n数据库的锁,在不同的事务隔离级别下,是采用了不同的机制的。在 MySQL 中,有三种类型的锁,分别是Record Lock、Gap Lock和 Next-Key Lock。
\\nRecord Lock表示记录锁,锁的是索引记录。
\\nGap Lock是间隙锁,锁的是索引记录之间的间隙。
\\nNext-Key Lock是Record Lock和Gap Lock的组合,同时锁索引记录和间隙。他的范围是左开右闭的。
\\n在 RC 中,只会对索引增加Record Lock,不会添加Gap Lock和Next-Key Lock。
\\n在 RR 中,为了解决幻读的问题,在支持Record Lock的同时,还支持Gap Lock和Next-Key Lock;
\\n在数据主从同步时,不同格式的 binlog 也对事务隔离级别有要求。
\\nMySQL的binlog主要支持三种格式,分别是statement、row以及mixed,但是,RC 隔离级别只支持row格式的binlog。如果指定了mixed作为 binlog 格式,那么如果使用RC,服务器会自动使用基于row 格式的日志记录。
\\n而 RR 的隔离级别同时支持statement、row以及mixed三种。
\\n提升并发
\\n高并发!
\\n没错,互联网业务的并发度比传统企业要高出很多。2020年双十一当天,订单创建峰值达到 58.3 万笔/秒。
\\n很多人问,要怎么做才能扛得住这么大的并发量。其实,这背后的优化多到几个小时都讲不完,因为要做的、可以做的事情实在是太多了。
\\n而有一个和我们今天这篇文章有关的优化,那就是通过修改数据库的隔离级别来提升并发度。
\\n为什么 RC 比 RR 的并发度要好呢?
\\n首先,RC 在加锁的过程中,是不需要添加Gap Lock和 Next-Key Lock 的,只对要修改的记录添加行级锁就行了。
\\n这就使得并发度要比 RR 高很多。
\\n另外,因为 RC 还支持\\"半一致读\\",可以大大的减少了更新语句时行锁的冲突;对于不满足更新条件的记录,可以提前释放锁,提升并发度。
\\n减少死锁
\\n因为RR这种事务隔离级别会增加Gap Lock和 Next-Key Lock,这就使得锁的粒度变大,那么就会使得死锁的概率增大。
\\n死锁:一个事务锁住了表A,然后又访问表B;另一个事务锁住了表B,然后企图访问表A;这时就会互相等待对方释放锁,就导致了死锁。
\\nRR和RC主要在加锁机制、主从同步以及一致性读方面存在一些差异。
\\n而很多大厂,为了提升并发度和降低死锁发生的概率,会把数据库的隔离级别从默认的 RR 调整成 RC。
\\n当然,这样做也不是完全没有问题,首先使用 RC 之后,就需要自己解决不可重复读的问题,这个其实还好,很多时候不可重复读问题其实是可以忽略的,或者可以用其他手段解决。
\\n比如读取到别的事务修改的值其实问题不太大的,只要修改的时候的不基于错误数据就可以了,所以我们都是在核心表中增加乐观锁标记,更新的时候都要带上锁标记进行乐观锁更新。
\\n还有就是使用 RC 的时候,不能使用statement格式的 binlog,这种影响其实可以忽略不计了,因为MySQL是在5.1.5版本开始支持row的、在5.1.8版本中开始支持mixed,后面这两种可以代替 statement格式。
\\n所有的技术方案的选择,都是一种权衡的艺术!
","description":"✅为什么默认RR,大厂要改成RC? 虽然RR的隔离级别可以在一定程度上避免脏读、不可重复读和幻读等问题,但是,对于很多大型的互联网来说,会愿意将数据库的默认隔离级别调整成并发度更高的RC级别,从而,提升并发度并且降低发生死锁的概率。\\n\\n扩展知识\\n\\nRR 和 RC 的区别\\n\\n我们需要先来弄清楚一下 RR 和 RC 的区别,分析下各自的优缺点。\\n\\n一致性读\\n\\n一致性读,又称为快照读。快照即当前行数据之前的历史版本。快照读就是使用快照信息显示基于某个时间点的查询结果,而不考虑与此同时运行的其他事务所执行的更改。\\n\\n在MySQL 中,只有READ COMMITTED 和…","guid":"https://juejin.cn/post/7485633832307703846","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T10:05:31.429Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"mysql---Innodb加锁原则:Record Lock_Gap Lock_Next-Key Lock","url":"https://juejin.cn/post/7485560281956384778","content":"借鉴:
\\n\\n\\n下面会介绍过了Record Lock、Gap Lock和Next-Key Lock,但是并没有说明加锁规则。关于加锁规则,我是看了丁奇大佬的《MySQL实战45讲》中的文章之后理解的,他总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:
\\n原则 1:加锁的基本单位是 next-key lock。是一个前开后闭区间。
\\n原则 2:查找过程中访问到的对象才会加锁。
\\n优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
\\n优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
\\n一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
\\n数据库使用锁是为了支持更好的并发,提供数据的完整性和一致性。InnoDB是一个支持行锁的存储引擎,锁的类型有:共享锁(S)、排他锁(X)、意向共享(IS)、意向排他(IX)。为了提供更好的并发,InnoDB提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照。该方法是通过InnoDB的一个特性:MVCC来实现的。
\\nroot@localhost : test 10:56:10>\\n\\ncreate table t(a int,key idx_a(a))engine =innodb;\\n\\nQuery OK, 0 rows affected (0.20 sec)\\n\\nroot@localhost : test 10:56:13>\\n\\ninsert into t values(1),(3),(5),(8),(11);\\n\\nQuery OK, 5 rows affected (0.00 sec)\\nRecords: 5 Duplicates: 0 Warnings: 0\\n\\nroot@localhost : test 10:56:15>\\n\\nselect * from t;\\n\\n+------+\\n| a |\\n+------+\\n| 1 |\\n| 3 |\\n| 5 |\\n| 8 |\\n| 11 |\\n+------+\\n5 rows in set (0.00 sec)\\n\\nsection A:\\n\\nroot@localhost : test 10:56:27>\\n\\nstart transaction;\\n\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 10:56:29>\\n\\nselect * from t where a = 8 for update;\\n\\n+------+\\n| a |\\n+------+\\n| 8 |\\n+------+\\n1 row in set (0.00 sec)\\n\\n\\nsection B:\\nroot@localhost : test 10:54:50>\\n\\nbegin;\\n\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 10:56:51>\\n\\nselect * from t;\\n\\n+------+\\n| a |\\n+------+\\n| 1 |\\n| 3 |\\n| 5 |\\n| 8 |\\n| 11 |\\n+------+\\n5 rows in set (0.00 sec)\\n\\nroot@localhost : test 10:56:54>\\n\\ninsert into t values(2);\\n\\nQuery OK, 1 row affected (0.00 sec)\\n\\nroot@localhost : test 10:57:01>\\n\\ninsert into t values(4);\\n\\nQuery OK, 1 row affected (0.00 sec)\\n\\n++++++++++\\n\\nroot@localhost : test 10:57:04>insert into t values(6);\\n\\nroot@localhost : test 10:57:11>insert into t values(7);\\n\\nroot@localhost : test 10:57:15>insert into t values(9);\\n\\nroot@localhost : test 10:57:33>insert into t values(10);\\n\\n++++++++++ 上面全被锁住,阻塞住了\\n\\nroot@localhost : test 10:57:39>insert into t values(12);\\nQuery OK, 1 row affected (0.00 sec)\\n\\n
\\n问题:
\\n为什么section B上面的插入语句会出现锁等待的情况?InnoDB是行锁,在section A里面锁住了a=8的行,其他应该不受影响。why?
\\n分析:
\\n因为InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围(GAP)。上面索引值有1,3,5,8,11,其记录的GAP的区间如下:是一个左开右闭的空间(原因是默认主键的有序自增的特性,结合后面的例子说明)
\\n(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
\\n特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock。如上面分析,那就可以解释了。
\\nroot@localhost : test 10:56:29>select * from t where a = 8 for update;\\n+------+\\n| a |\\n+------+\\n| 8 |\\n+------+\\n1 row in set (0.00 sec)\\n
\\n该SQL语句锁定的范围是(5,8],下个下个键值范围是(8,11],所以插入5~11之间的值的时候都会被锁定,要求等待。即:插入5,6,7,8,9,10 会被锁住。插入非这个范围内的值都正常。
\\n################################### 2016-07-21 更新
\\n因为例子里没有主键,所以要用隐藏的ROWID来代替,数据根据Rowid进行排序。而Rowid是有一定顺序的(自增),所以其中11可以被写入,5不能被写入,不清楚的可以再看一个有主键的例子:
\\n会话1:\\n01:43:07>\\n\\ncreate table t(id int,name varchar(10),key idx_id(id),primary key(name))engine =innodb;\\n\\nQuery OK, 0 rows affected (0.02 sec)\\n\\n01:43:11>\\n\\ninsert into t values(1,\'a\'),(3,\'c\'),(5,\'e\'),(8,\'g\'),(11,\'j\'); \\nQuery OK, 5 rows affected (0.01 sec)\\nRecords: 5 Duplicates: 0 Warnings: 0\\n\\n01:44:03>\\n\\nselect @@global.tx_isolation, @@tx_isolation; \\n\\n+-----------------------+-----------------+\\n| @@global.tx_isolation | @@tx_isolation |\\n+-----------------------+-----------------+\\n| REPEATABLE-READ | REPEATABLE-READ |\\n+-----------------------+-----------------+\\n1 row in set (0.01 sec)\\n\\n01:44:58>select * from t;\\n+------+------+\\n| id | name |\\n+------+------+\\n| 1 | a |\\n| 3 | c |\\n| 5 | e |\\n| 8 | g |\\n| 11 | j |\\n+------+------+\\n5 rows in set (0.00 sec)\\n\\n01:45:07>\\n\\nstart transaction; \\n\\n01:45:09>\\n\\ndelete from t where id=8;\\n\\nQuery OK, 1 row affected (0.01 sec)\\n\\n\\n会话2:\\n01:50:38>\\n\\nselect @@global.tx_isolation, @@tx_isolation;\\n\\n+-----------------------+-----------------+\\n| @@global.tx_isolation | @@tx_isolation |\\n+-----------------------+-----------------+\\n| REPEATABLE-READ | REPEATABLE-READ |\\n+-----------------------+-----------------+\\n1 row in set (0.01 sec)\\n\\n01:50:48>\\n\\nstart transaction; \\n\\n01:50:51>\\n\\nselect * from t;\\n\\n+------+------+\\n| id | name |\\n+------+------+\\n| 1 | a |\\n| 3 | c |\\n| 5 | e |\\n| 8 | g |\\n| 11 | j |\\n+------+------+\\n5 rows in set (0.01 sec)\\n\\n01:51:35>\\n\\ninsert into t(id,name) values(6,\'f\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:53:32>\\n\\ninsert into t(id,name) values(5,\'e1\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:53:41>\\n\\ninsert into t(id,name) values(7,\'h\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:54:43>\\n\\ninsert into t(id,name) values(8,\'gg\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:55:10>\\n\\ninsert into t(id,name) values(9,\'k\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:55:23>\\n\\ninsert into t(id,name) values(10,\'p\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n01:55:33>\\n\\ninsert into t(id,name) values(11,\'iz\');\\n\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n\\n#########上面看到 id:5,6,7,8,9,10,11都被锁了。\\n\\n#########下面看到 id:5,11 还是可以插入的\\n01:54:33>\\n\\ninsert into t(id,name) values(5,\'cz\');\\n\\nQuery OK, 1 row affected (0.01 sec)\\n\\n01:55:59>\\n\\ninsert into t(id,name) values(11,\'ja\');\\n\\nQuery OK, 1 row affected (0.01 sec)\\n
\\n分析: 因为会话1已经对id=8的记录加了一个X锁,由于是RR隔离级别,INNODB要防止幻读需要加GAP锁:即id=5(8的左边),id=11(8的右边)之间需要加间隙锁(GAP)。这样[5,e]和[8,g],[8,g]和[11,j]之间的数据都要被锁。上面测试已经验证了这一点,根据索引的有序性,数据按照主键(name)排序,后面写入的[5,cz]([5,e]的左边)和[11,ja]([11,j]的右边)不属于上面的范围从而可以写入。
\\n另外一种情况,把name主键去掉会是怎么样的情况?有兴趣的同学可以测试一下。
\\n##################################################
\\n继续: 插入超时失败后,会怎么样?
\\n超时时间的参数:innodb_lock_wait_timeout ,默认是50秒。
\\n超时是否回滚参数:innodb_rollback_on_timeout 默认是OFF。
\\nsection A:\\n\\nroot@localhost : test 04:48:51>\\n\\nstart transaction;\\n\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 04:48:53>\\n\\nselect * from t where a = 8 for update;\\n\\n+------+\\n| a |\\n+------+\\n| 8 |\\n+------+\\n1 row in set (0.01 sec)\\n\\n\\nsection B:\\n\\nroot@localhost : test 04:49:04>\\n\\nstart transaction;\\n\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 04:49:07>\\n\\ninsert into t values(12);\\n\\nQuery OK, 1 row affected (0.00 sec)\\n\\nroot@localhost : test 04:49:13>\\n\\ninsert into t values(10);\\n\\nERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction\\nroot@localhost : test 04:50:06>\\n\\nselect * from t;\\n\\n+------+\\n| a |\\n+------+\\n| 1 |\\n| 3 |\\n| 5 |\\n| 8 |\\n| 11 |\\n| 12 |\\n+------+\\n6 rows in set (0.00 sec)\\n
\\n经过测试,不会回滚超时引发的异常,当参数innodb_rollback_on_timeout 设置成ON时,则可以回滚,会把插进去的12回滚掉。
\\n默认情况下,InnoDB存储引擎不会回滚超时引发的异常,除死锁外。
\\n既然InnoDB有三种算法,那Record Lock什么时候用?还是用上面的列子,把辅助索引改成唯一属性的索引。
\\nroot@localhost : test 04:58:49>create table t(a int primary key)engine =innodb;\\nQuery OK, 0 rows affected (0.19 sec)\\n\\nroot@localhost : test 04:59:02>insert into t values(1),(3),(5),(8),(11);\\nQuery OK, 5 rows affected (0.00 sec)\\nRecords: 5 Duplicates: 0 Warnings: 0\\n\\nroot@localhost : test 04:59:10>select * from t;\\n+----+\\n| a |\\n+----+\\n| 1 |\\n| 3 |\\n| 5 |\\n| 8 |\\n| 11 |\\n+----+\\n5 rows in set (0.00 sec)\\n\\nsection A:\\n\\nroot@localhost : test 04:59:30>start transaction;\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 04:59:33>select * from t where a = 8 for update;\\n+---+\\n| a |\\n+---+\\n| 8 |\\n+---+\\n1 row in set (0.00 sec)\\n\\nsection B:\\n\\nroot@localhost : test 04:58:41>start transaction;\\nQuery OK, 0 rows affected (0.00 sec)\\n\\nroot@localhost : test 04:59:45>insert into t values(6);\\nQuery OK, 1 row affected (0.00 sec)\\n\\nroot@localhost : test 05:00:05>insert into t values(7);\\nQuery OK, 1 row affected (0.00 sec)\\n\\nroot@localhost : test 05:00:08>insert into t values(9);\\nQuery OK, 1 row affected (0.00 sec)\\n\\nroot@localhost : test 05:00:10>insert into t values(10);\\nQuery OK, 1 row affected (0.00 sec)\\n
\\n问题:
\\n为什么section B上面的插入语句可以正常,和测试一不一样?
\\n分析:
\\n因为InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围,按照这个方法是会和第一次测试结果一样。但是,当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
\\n注意:通过主键或则唯一索引来锁定不存在的值,也会产生GAP锁定。即:
\\n会话1:\\n04:22:38>show create table t\\\\G\\n*************************** 1. row ***************************\\n Table: t\\nCreate Table: CREATE TABLE `t` (\\n `id` int(11) NOT NULL,\\n `name` varchar(10) DEFAULT NULL,\\n PRIMARY KEY (`id`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4\\n1 row in set (0.00 sec)\\n\\n04:22:49>start transaction;\\n\\n04:23:16>select * from t where id = 15 for update;\\nEmpty set (0.00 sec)\\n\\n会话2:\\n04:26:10>insert into t(id,name) values(10,\'k\');\\nQuery OK, 1 row affected (0.01 sec)\\n\\n04:26:26>insert into t(id,name) values(12,\'k\');\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n04:29:32>insert into t(id,name) values(16,\'kxx\');\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted\\n04:29:39>insert into t(id,name) values(160,\'kxx\');\\n^CCtrl-C -- sending \\"KILL QUERY 9851\\" to server ...\\nCtrl-C -- query aborted.\\nERROR 1317 (70100): Query execution was interrupted \\n
\\n如何让测试一不阻塞?可以显式的关闭Gap Lock:
\\n1:把事务隔离级别改成:Read Committed,提交读、不可重复读。SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
\\n2:修改参数:innodb_locks_unsafe_for_binlog 设置为1。
","description":"借鉴: www.cnblogs.com/zhoujinyi/p…\\n\\ndev.mysql.com/doc/refman/…\\n\\nMySQL的加锁原则\\n\\n下面会介绍过了Record Lock、Gap Lock和Next-Key Lock,但是并没有说明加锁规则。关于加锁规则,我是看了丁奇大佬的《MySQL实战45讲》中的文章之后理解的,他总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:\\n\\n原则 1:加锁的基本单位是 next-key lock。是一个前开后闭区间。\\n\\n原则 2:查找过程中访问到的对象才会加锁。\\n\\n优化 1:索引上的等值查询…","guid":"https://juejin.cn/post/7485560281956384778","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T09:09:38.186Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"新项目终于用上了jdk24","url":"https://juejin.cn/post/7485305668359045160","content":"Java世界迎来重大更新!
\\nOracle刚刚发布的JDK 24不仅是一个长期支持版本(LTS),更是一场Java编程体验的革命。
\\n想象一下,无需预编译就能直接运行多文件项目,用一个下划线就能优雅地忽略不需要的变量,甚至可以在字符串中直接嵌入表达式...这些曾经只能在其他现代语言中享受的便利,现在都在JDK 24中实现了。
\\n作为Java开发者,掌握这些新特性不仅能让你的代码更简洁高效,更能在技术浪潮中保持领先。本文将带你全面了解JDK 24的安装步骤和九大革命性新特性,让我们一起探索Java新世界的无限可能!
\\n官网下载地址:www.oracle.com/java/techno…
\\n我喜欢下载zip版本。
\\n通过java -version
查看系统jdk版本即可。
Stream Gatherers是对Stream API的强大扩展,提供了更灵活的流转换机制,特别适合处理有状态操作和输入输出不对等的场景。
\\nimport java.util.List;\\nimport java.util.stream.Gatherers;\\n\\npublic class GatherersExample {\\n public static void main(String[] args) {\\n List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);\\n \\n // 使用滑动窗口计算平均值\\n List<Double> averages = numbers.stream()\\n .gather(Gatherers.windowSliding(3)\\n .map(window -> window.stream()\\n .mapToInt(Integer::intValue)\\n .average()\\n .orElse(0.0)))\\n .toList();\\n \\n System.out.println(\\"滑动窗口平均值: \\" + averages);\\n // 输出: [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]\\n \\n // 去除连续重复元素\\n List<String> words = List.of(\\"apple\\", \\"apple\\", \\"banana\\", \\"banana\\", \\"apple\\");\\n List<String> distinct = words.stream()\\n .gather(Gatherers.distinctConsecutive())\\n .toList();\\n \\n System.out.println(\\"去除连续重复后: \\" + distinct);\\n // 输出: [apple, banana, apple]\\n }\\n}\\n
\\n这个预览特性允许使用下划线(_)作为占位符,忽略不需要的变量或模式匹配结果。
\\nimport java.util.Map;\\n\\npublic class UnnamedVariablesExample {\\n public static void main(String[] args) {\\n // 旧方式:必须声明不使用的变量\\n Map<String, Integer> scores = Map.of(\\"Alice\\", 95, \\"Bob\\", 87);\\n for (var entry : scores.entrySet()) {\\n String name = entry.getKey();\\n Integer score = entry.getValue(); // 即使不使用name也必须声明\\n System.out.println(\\"得分: \\" + score);\\n }\\n \\n // 新方式:使用_忽略不需要的变量\\n for (var entry : scores.entrySet()) {\\n String _ = entry.getKey(); // 使用_表示不关心这个值\\n Integer score = entry.getValue();\\n System.out.println(\\"得分: \\" + score);\\n }\\n \\n // 在模式匹配中使用\\n Object obj = \\"Hello\\";\\n if (obj instanceof String _) { // 不需要绑定变量名\\n System.out.println(\\"这是一个字符串,长度: \\" + ((String)obj).length());\\n }\\n }\\n}\\n
\\nJava 24允许直接运行包含多个源文件的程序,无需预先编译,大大简化了小型项目的开发和运行。
\\n假设有以下两个文件在同一目录中:
\\npublic class Main {\\n public static void main(String[] args) {\\n System.out.println(\\"主程序启动\\");\\n Helper helper = new Helper();\\n helper.doSomething();\\n }\\n}\\n
\\npublic class Helper {\\n public void doSomething() {\\n System.out.println(\\"辅助功能执行中\\");\\n }\\n}\\n```bash\\n\\n现在可以直接运行,无需显式编译:\\n\\n```bash\\njava Main.java\\n
\\n系统会自动查找和编译相关的源文件。
\\n这个特性终于从孵化阶段转为标准功能,让Java可以直接调用本机代码并访问外部内存,无需使用JNI。
\\nimport java.lang.foreign.*;\\nimport java.lang.invoke.MethodHandle;\\n\\npublic class ForeignMemoryExample {\\n public static void main(String[] args) throws Throwable {\\n // 使用内存段API分配本机内存\\n try (Arena arena = Arena.ofConfined()) {\\n MemorySegment segment = arena.allocate(100);\\n \\n // 在本机内存中写入数据\\n segment.setAtIndex(ValueLayout.JAVA_INT, 0, 42);\\n \\n // 从本机内存读取数据\\n int value = segment.getAtIndex(ValueLayout.JAVA_INT, 0);\\n System.out.println(\\"读取到的值: \\" + value);\\n \\n // 调用C标准库函数\\n Linker linker = Linker.nativeLinker();\\n SymbolLookup stdlib = linker.defaultLookup();\\n \\n MethodHandle strlen = linker.downcallHandle(\\n stdlib.find(\\"strlen\\").orElseThrow(),\\n FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)\\n );\\n \\n try (Arena stringArena = Arena.ofConfined()) {\\n MemorySegment cString = stringArena.allocateUtf8String(\\"Hello, Native World!\\");\\n long length = (long) strlen.invoke(cString);\\n System.out.println(\\"字符串长度: \\" + length);\\n }\\n }\\n }\\n}\\n
\\n字符串模板是一种新的插值机制,允许在字符串中嵌入表达式,这个特性仍处于预览阶段。
\\npublic class StringTemplatesExample {\\n public static void main(String[] args) {\\n String name = \\"Java\\";\\n int version = 24;\\n double rating = 4.7;\\n \\n // 使用字符串模板\\n String message = STR.\\"\\"\\"\\n 欢迎使用\\\\{name} \\\\{version}!\\n 用户评分: \\\\{rating}/5.0\\n 当前时间: \\\\{java.time.LocalTime.now()}\\n \\"\\"\\";\\n \\n System.out.println(message);\\n \\n // SQL模板示例 (JEP 459)\\n String table = \\"users\\";\\n String column = \\"username\\";\\n int limit = 10;\\n \\n String query = SQL.\\"\\"\\"\\n SELECT \\\\{column}\\n FROM \\\\{table}\\n WHERE active = true\\n LIMIT \\\\{limit}\\n \\"\\"\\";\\n \\n System.out.println(\\"SQL查询: \\" + query);\\n }\\n}\\n
\\nJDK 24引入了新的接口SequencedCollection、SequencedSet和SequencedMap,提供了顺序相关的操作方法。
\\nimport java.util.*;\\n\\npublic class SequencedCollectionsExample {\\n public static void main(String[] args) {\\n // 有序列表\\n SequencedCollection<String> fruits = new ArrayList<>(List.of(\\"苹果\\", \\"香蕉\\", \\"橙子\\"));\\n \\n // 获取第一个和最后一个元素\\n String first = fruits.getFirst(); // 苹果\\n String last = fruits.getLast(); // 橙子\\n \\n // 获取反向视图\\n SequencedCollection<String> reversed = fruits.reversed();\\n System.out.println(\\"反向顺序: \\" + reversed); // [橙子, 香蕉, 苹果]\\n \\n // 有序映射\\n SequencedMap<Integer, String> ranks = new LinkedHashMap<>();\\n ranks.put(1, \\"金牌\\");\\n ranks.put(2, \\"银牌\\");\\n ranks.put(3, \\"铜牌\\");\\n \\n // 获取第一个和最后一个条目\\n Map.Entry<Integer, String> firstEntry = ranks.firstEntry(); // 1=金牌\\n Map.Entry<Integer, String> lastEntry = ranks.lastEntry(); // 3=铜牌\\n \\n System.out.println(\\"第一名: \\" + firstEntry.getValue()); // 金牌\\n System.out.println(\\"最后一名: \\" + lastEntry.getValue()); // 铜牌\\n \\n // 获取键的有序集合\\n SequencedSet<Integer> keys = ranks.sequencedKeySet();\\n System.out.println(\\"排名顺序: \\" + keys); // [1, 2, 3]\\n }\\n}\\n
\\nJDK 24将之前作为孵化模块的简易HTTP服务器标准化,可以快速启动一个静态文件服务器。
\\nimport com.sun.net.httpserver.SimpleFileServer;\\nimport com.sun.net.httpserver.HttpServer;\\n\\nimport java.net.InetSocketAddress;\\nimport java.nio.file.Path;\\n\\npublic class SimpleWebServerExample {\\n public static void main(String[] args) throws Exception {\\n // 创建一个简单的文件服务器,指向当前目录\\n Path root = Path.of(\\"./public\\");\\n InetSocketAddress addr = new InetSocketAddress(8080);\\n \\n HttpServer server = SimpleFileServer.createFileServer(addr, root, SimpleFileServer.OutputLevel.VERBOSE);\\n \\n System.out.println(\\"服务器已启动: http://localhost:8080/\\");\\n server.start();\\n \\n // 也可以通过命令行直接启动:\\n // java -m jdk.httpserver -p 8080 -d ./public\\n }\\n}\\n
\\n这个预览特性让Java的入门更简单,允许省略类声明和静态main方法,直接编写代码。
\\n传统方式:
\\npublic class HelloWorld {\\n public static void main(String[] args) {\\n System.out.println(\\"你好,世界!\\");\\n }\\n}\\n
\\n新方式 (HelloWorld.java):
\\n// 注意:无需声明类\\nvoid main() { // 不再需要static和args参数\\n System.out.println(\\"你好,世界!\\");\\n \\n // 可以直接访问实例方法\\n sayHello(\\"Java 24\\");\\n}\\n\\nvoid sayHello(String name) {\\n System.out.println(\\"你好,\\" + name + \\"!\\");\\n}\\n
\\n这个预览特性允许在调用父类构造器之前执行一些语句,提高了代码灵活性。
\\nclass ConfigurationException extends Exception {\\n private final String config;\\n \\n ConfigurationException(String file, String message) {\\n // 旧方式:必须先调用super()\\n super(message);\\n this.config = file;\\n }\\n \\n // 新方式:可以在super之前执行语句\\n ConfigurationException(String file, int line) {\\n // 提前计算消息\\n String message = \\"错误位于 \\" + file + \\" 的第 \\" + line + \\" 行\\";\\n System.out.println(\\"准备创建异常: \\" + message);\\n \\n // 使用计算结果调用父类构造器\\n super(message);\\n this.config = file;\\n }\\n}\\n
\\nJDK 24带来了多项强大的新特性,重点提升了几个方面:
\\n这些新特性使Java继续保持其强大、灵活和现代化的特点,为开发者提供了更多高效编程的工具。
\\nJDK 24作为Java的新一代长期支持版本,为开发者带来了显著的语言和API改进。
\\n从Stream Gatherers提供的强大流转换机制,到未命名变量和隐式声明类带来的代码简洁性提升;从外部函数和内存API实现的高效本机交互,到多文件直接运行和简易Web服务器带来的开发体验优化;再到字符串模板和有序集合API增强的语言表达力,这些创新特性共同构成了一个更现代、更高效的Java开发生态。
\\n随着这些新特性的引入,Java不仅保持了其企业级应用开发的强大基础,还在语法简洁性和开发便捷性方面迈出了重要一步。对Java开发者而言,及时掌握和应用这些新特性,不仅可以提高日常开发效率,还能编写出更加简洁、可维护的代码。
\\nJDK 24的这些改进充分证明了Java语言持续进化的活力,也展示了Oracle对保持Java作为主流编程语言地位的坚定承诺。无论是资深Java工程师还是刚入行的开发者,都应当抓住这次技术升级的机会,拥抱Java开发的新时代。
","description":"Java世界迎来重大更新! Oracle刚刚发布的JDK 24不仅是一个长期支持版本(LTS),更是一场Java编程体验的革命。\\n\\n想象一下,无需预编译就能直接运行多文件项目,用一个下划线就能优雅地忽略不需要的变量,甚至可以在字符串中直接嵌入表达式...这些曾经只能在其他现代语言中享受的便利,现在都在JDK 24中实现了。\\n\\n作为Java开发者,掌握这些新特性不仅能让你的代码更简洁高效,更能在技术浪潮中保持领先。本文将带你全面了解JDK 24的安装步骤和九大革命性新特性,让我们一起探索Java新世界的无限可能!\\n\\n一、安装JDK24\\n1、下载JDK24\\n\\n官网下…","guid":"https://juejin.cn/post/7485305668359045160","author":"哪吒编程","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T03:22:29.943Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5479a0404256446ab10e2903374b682d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=BVPl8kGR%2BJ%2BsJH47p%2FU8VV6Yw38%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fabe9b8e89354a0cbeb5e862918f3abc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=gZa7VcIeB5qI%2F1kK5cbDHo9%2Fy%2FI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d8a69e262174a529d7e0da0bd3cee90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=44RzxWT7QoTwTILwlgbivmcqdrM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/29c2dede97734f96a5461d4694fb686e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=qA%2FHZEeOv%2BQgB7sYuqv3%2FEunZ1s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d298ea7286074aef94683ee094082614~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=4xDQRfS9u9F6Rkn2ZStBUDspavA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab86c3fed03a46049a1bd4e9b431113d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=tyqn4fO%2BxrHK8whqeBu6TkWvC%2F8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d2971ec79f544fc1b9bc0c13626c3581~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZOq5ZCS57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1743477749&x-signature=JUL4zaYAVawXD9QekLO3woDS0dk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"解放双手!看看人家的Nginx可视化管理工具,确实清新优雅!","url":"https://juejin.cn/post/7485276123253817378","content":"\\n\\nNginx是一款非常流行的Web服务器,作为后端程序员我们会经常使用到它。但是去服务器上手动修改Nginx配置确实是一件麻烦的事情,今天给大家分享一款Nginx可视化管理工具
\\nnginx-proxy-manager
,能彻底解放你的双手!
nginx-proxy-manager
是一款Nginx可视化管理工具,具有强大的用户界面,目前在Github上已有25k+star
。\\n使用它你无需深入了解Nginx,即可实现请求代理、自动申请SSL证书等功能。
它具有如下特性:
\\n\\n\\n使用Docker来安装
\\nnginx-proxy-manager
是非常方便的,这里将采用此种方式来安装。
docker pull jc21/nginx-proxy-manager:latest\\n
\\ndocker run -p 80:80 -p 81:81 -p 443:443 --name nginx-proxy-manager \\\\\\n-v /mydata/nginx-pm/data:/data \\\\\\n-v /mydata/nginx-pm/letsencrypt:/etc/letsencrypt \\\\\\n-d jc21/nginx-proxy-manager:latest\\n
\\nadmin@example.com:changeme
,访问地址:http://192.168.3.101:81接下来就以我的mall电商实战项目的部署为例,演示下nginx-proxy-manager
的使用。
\\n\\n这里简单介绍下mall项目,mall项目是一套基于
\\nSpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目地址
\\n项目演示:
\\n\\n\\n这里以mall电商实战项目的后台管理系统和前台商城系统的部署为例来演示下静态代理功能。
\\n
nginx-proxy-manager
的服务器地址为192.168.3.101
;192.168.3.101 mall.macrozheng.com\\n
\\nHosts->Proxy Hosts->Add Proxy Host
来添加代理主机;Details
中设置好域名和转发的服务器信息;Advanced
中添加自定义的Nginx配置;location /admin {\\n alias /data/html/admin;\\n index index.html index.htm;\\n}\\n
\\n/mydata/nginx-pm/data/html
目录下并解压,这里我把前台商城系统的代码也同时上传了;location /app {\\n alias /data/html/app;\\n index index.html index.htm;\\n}\\n
\\n\\n\\n接下来以mall项目的后端API为例来演示下动态代理功能。
\\n
192.168.3.101 api.macrozheng.com\\n
\\nadmin-api.macrozheng.com
即可;Access Lists
功能可以实现代理主机的Basic认证;用户管理
功能可以实现用户及权限的管理;Audit Log
功能可以查看用户的操作日志。\\n\\n如果你用不习惯英文版本的话,还有个中文版本的项目
\\nnginx-proxy-manager-zh
,中文镜像是基于官方镜像替换前端代码来实现的,所以中文版本的全部功能与官方版本相同。
docker run -p 80:80 -p 81:81 -p 443:443 --name nginx-proxy-manager \\\\\\n-v /mydata/nginx-pm/data:/data \\\\\\n-v /mydata/nginx-pm/letsencrypt:/etc/letsencrypt \\\\\\n-d chishin/nginx-proxy-manager-zh:release\\n
\\n今天给大家分享了一款Nginx可视化管理工具,界面清新优雅,用起来也很方便!有了它你无需了解过多的Nginx知识,就可以快速使用Nginx了,感兴趣的小伙伴可以尝试下!
\\n大家好,我是田螺.
\\n分享一道网上很火的腾讯面试题:40亿的QQ号,如何去重,1G的内存. 不过,有腾讯上班的朋友说,我们没出过这种面试题~ 哈哈~
\\n哈哈,anyway,这道题还是很有意思的. 它是一个非常经典的海量数据去重问题,并且做了内存限制,只能1G.本文田螺哥跟大家探讨一下.
\\n我们日常开发中,如果谈到去重,最容易想到的就是放到HashSet
,直接放到HashSet
就好:
Set<Long> qqSet = new HashSet<>();\\nqqSet.add(qqNumber); // 自动去重\\n
\\n但是呢,是有个1G的内存限制的! 如果放到HashSet
,那40亿的QQ数据,都是在内存中的,我们来算一下,40亿的QQ,要多大的内存:
如果每个QQ号是64位整数(8字节),那么40亿个QQ号的总存储量为:
\\n40亿 * 8字节 = 320亿字节\\n转化位KB 32,000,000,000 字节/1024 = 31,250,000 KB\\nKB转化为MB 31,250,000 KB/ 1024 ≈ 30,517.578125 MB\\nMB转化为GB 30,517.578125 MB/ 1024 ≈ 29.8023223876953125 GB\\n
\\n那就是30GB
左右,如果每个QQ号码是32位整数(4字节),则是15GB
左右. 显然,都远超1GB的内存.
因此,直接放到HashSet
并不可行.
因此,这道题我们需要换个思路,就是在内存有限的情况下,如何实现去重? 我们可以考虑一种更高效的数据结构来处理这个问题。
\\n我们可以考虑BitMap(位图)来解决这个问题.
\\n\\n\\nBitMap(位图)是一种非常高效的数据结构,特别适合处理大规模数据的去重和查询问题。它的基本思想是使用一个bit位来表示一个数字是否存在。
\\n
例如,如果我们有一个长度为10的BitMap,那么它可以表示数字0到9是否存在:
\\n以此类推~
\\n数字9表示的BitMap如下:
\\n如果用BitMap,比如我要记录的QQ号码分别是9、5、2、7, 那么BitMap表示为
\\n显然只需要一个10位就可以表示,如果用传统方法来记录,一个整型4字节,4个QQ号码就是,44=16字节,然后一个字节8位,那就是 168=128位啦~. 可以发现用BitMap 可以大大节省存储空间.
\\n既然BitMap 可以大大节省存储空间,我们用BitMap来给40亿QQ去重,看看会不会超1G的内存.
\\n我们来一起估算一下, 因为要40亿的QQ,那我们申请一个足够大的BitMap,假设就是40亿的位,那内存大概就是:
\\n4,000,000,000/8 = 500,000,000 \\n500,000,000/1024/1024/1024 ≈ 0.466GB\\n
\\n可以发现,只需要0.466GB
的内存就足够啦~ 在内存这方面,是符合不超过1GB的限制的~
\\n\\n比如,假设有个QQ号码为326443281,那么就在BitMap的对应位置,设置为1
\\n
给大家来个简单的代码模拟吧:
\\nimport java.util.*;\\n\\npublic class QQDeduplication {\\n\\n // 位图的大小为 4,294,967,296 bits,即 0.5 GB\\n private static final long BITMAP_SIZE = 1L << 32; // 2^32\\n private static final int BYTE_SIZE = 8; // 每个字节有8位\\n\\n private static List<Long> deduplicateQQNumbers(long[] qqNumbers) {\\n // 创建位图,使用字节数组\\n byte[] bitmap = new byte[(int) (BITMAP_SIZE / BYTE_SIZE)];\\n\\n // 更新位图\\n for (long qqNumber : qqNumbers) {\\n if (qqNumber >= 0 && qqNumber < BITMAP_SIZE) {\\n // 计算字节索引和位索引\\n int index = (int) (qqNumber / BYTE_SIZE);\\n int bitPosition = (int) (qqNumber % BYTE_SIZE);\\n // 设置对应的位\\n bitmap[index] |= (1 << bitPosition);\\n }\\n }\\n\\n // 收集去重后的QQ号码\\n List<Long> uniqueQQNumbers = new ArrayList<>();\\n for (int i = 0; i < bitmap.length; i++) {\\n for (int j = 0; j < BYTE_SIZE; j++) {\\n if ((bitmap[i] & (1 << j)) != 0) {\\n long qqNumber = (long) i * BYTE_SIZE + j;\\n uniqueQQNumbers.add(qqNumber);\\n }\\n }\\n }\\n\\n return uniqueQQNumbers;\\n }\\n}\\n
\\n我们使用一种数据结构去解决问题,那肯定要知道它的优缺点对吧.
\\nBitmap的优点
\\n\\n\\n相比哈希表存储原始数据,Bitmap仅用1位/元素。对于密集数据(如连续QQ号),空间利用率极高。
\\n
\\n\\n插入和查询均为O(1)复杂度,位运算速度快,适合海量数据实时处理。
\\n
\\n\\n只需遍历数据,置位存在标记,无需复杂结构。
\\n
Bitmap的缺点
\\n\\n\\n若值域范围大但稀疏(如QQ号上限远大于实际数量),空间浪费严重。例如,若QQ号上限为1万亿,需125GB内存,难以承受。
\\n
\\n\\n仅记录是否存在,无法保存出现次数等元数据。
\\n
有些伙伴认为,使用布隆过滤器也可以实现,40亿的QQ号,不超过1G的内存,进行去重.大家觉得呢? 欢迎评论区留言讨论哈. 希望大家都找到心仪的offer ~~
\\nBitMap 的存储空间与值域强相关
\\nBitMap 的存储空间需求直接取决于 值域的大小,而不是实际数据的数量。
\\n值域:指数据可能的取值范围(如 QQ 号的最小值和最大值之间的范围)。
\\n存储空间:BitMap 需要为值域中的每一个可能值分配一个 bit 位,无论该值是否实际存在。
","description":"前言 大家好,我是田螺.\\n\\n分享一道网上很火的腾讯面试题:40亿的QQ号,如何去重,1G的内存. 不过,有腾讯上班的朋友说,我们没出过这种面试题~ 哈哈~\\n\\n哈哈,anyway,这道题还是很有意思的. 它是一个非常经典的海量数据去重问题,并且做了内存限制,只能1G.本文田螺哥跟大家探讨一下.\\n\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\n1. 常规思路\\n\\n我们日常开发中,如果谈到去重,最容易想到的就是放到HashSet,直接放到HashSet就好:\\n\\nSet典型回答
\\n这个问题,主要分两部分,一部分是拉黑,一部分是踢下线。分开讨论。
\\n用户的拉黑,可以看是哪种拉黑方式,如果是单个用户的拉黑,可以提供一个后台接口,在管理端,通过输入用户的手机号,或者用户 ID 进行拉黑,如果是批量的,那么就需要提供一个页面,支持批量上传用户列表。
\\n其次就是拉黑功能的实现,有3种方式,第一种是加黑名单,第二种是更新用户的状态,第三种是在用户表上打标。
\\n第一种是不修改用户表的方案,即我们单独有一张黑名单表,这里记录了用户的黑名单的列表,这么做的好处是可以和用户表解耦,互相不干预,而且还有个好处,就是可以做很多其他的事情,比如说拉黑开始时间、拉黑结束时间等等的各种控制。
\\n第二种和第三种方案本质上是一样的,就是在用户表上有个字段表示这个用户的被拉黑了。
\\n如果用户量比较大,或者是拉黑的用户比较多,建议用第一种方案,而且这个名单还可以前置给到其他的业务一起用,比如风控。如果用户量不大的话,没必要搞这个黑名单表,用户表加一个状态或者字段就行了。
\\n然后为了提升性能,针对用户的黑名单,还可以做缓存,将黑名单用户缓存在 Redis 或者本地缓存中,可以快速的针对黑用户进行拦截,而且黑名单非常适合使用布隆过滤器!如果量很大,可以进一步的用布隆过滤器来做缓存。
\\n[典型回答布隆过滤器是一种数据结构,用于快速检索一个元素是否可能存在于一个集合(bit 数组)中。它的基本原理是利用多个哈希函数,将一个元素映射成多个位,然后将这些位设置为 1。当查询一个元素时,如果这些位都被设置为 1,则认为元素可能存在于集合中,否则肯定不存在。所以,布隆过滤器可以准确的判断...]
\\n除了拉黑外,还有一个功能就是踢人下线,这个功能其实也简单,只需要我们把用户登录后的 Session 给他清空就行了。这样用户在下次访问我们的系统的时候,因为查不到 Session,就是被强制下线了。
\\n至于实现方式,要看 Session 存在哪,比如 Redis 的话,那么就找到这个用户的 ID,然后把他对应的 Session 给他清空即可。
\\n如果用的是那种单点登录的框架,很多都是支持这些功能的,可能你只需要调一个方法,就可以直接一键踢人下线了。如:sa-token.cc/doc.html#/u…
","description":"✅实现一个登录拉黑功能,实现拉黑用户和把已经登陆用户踢下线。 典型回答\\n\\n这个问题,主要分两部分,一部分是拉黑,一部分是踢下线。分开讨论。\\n\\n拉黑\\n\\n用户的拉黑,可以看是哪种拉黑方式,如果是单个用户的拉黑,可以提供一个后台接口,在管理端,通过输入用户的手机号,或者用户 ID 进行拉黑,如果是批量的,那么就需要提供一个页面,支持批量上传用户列表。\\n\\n其次就是拉黑功能的实现,有3种方式,第一种是加黑名单,第二种是更新用户的状态,第三种是在用户表上打标。\\n\\n第一种是不修改用户表的方案,即我们单独有一张黑名单表,这里记录了用户的黑名单的列表,这么做的好处是可以和用户表解耦…","guid":"https://juejin.cn/post/7485068571775008795","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T10:59:56.010Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c59a3e297f845679b70cc5192df9ec2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744020086&x-signature=Tiy0MIlUOrQv2K6GuVcY4KsKepc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d7af06028ee47f188492a8af74d7534~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1744020086&x-signature=j%2FfGFNsfX8pSiN0yz%2FMqYvTDiJw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"抢课_电商商品预约等等类似通用业务设计---基于 bitset 实现","url":"https://juejin.cn/post/7485002652285632550","content":"针对一些热点商品,我们提供了预约功能,就像天猫超市卖茅台一样,需要提前预约才能购买,不约不能买
\\n针对一些热点选修课,我们提供了预约抢课功能,就像天猫超市卖茅台一样,需要提前预约才能购抢,不约不能抢
\\n同一个商品,我们想要记录哪些用户预约过,同时需要能够快速查询,并且可以尽可能的减少存储空间,避免浪费,我们选择了使用 BitSet。
\\n为了快速的存取,我们使用 Redis,正好Redis 也支持 bitSet 数据结构,同时,我们为了避免Redis 挂了,我们也要存储一张预约表, 在数据库中做持久化。
\\nRedis 的 BitSet 是一种特殊的字符串类型,它允许你对字符串中的每一位(bit)进行操作。每个位可以是 0 或 1,因此你可以将 BitSet 视为一个非常高效的布尔数组。
\\n因为我们的用户 ID 是一定不重复的,并且可以转换成integer,所以没一个用户可以映射到一个唯一的 bit 上面,这样预约过的用户对应的 bit 设置为1 ,没预约过的默认为0,就能实现存储预约信息了。
\\n那么我们就可以把商品 id 作为 key,存储一个 bitset,bitset 中存储的是已经预约过的用户的id 列表。
\\n/**\\n * 商品预约\\n * 先更新缓存,再更新数据库。优先保证缓存,如果出现不一致,以缓存为主\\n *\\n * @param request\\n * @return\\n */\\n@Transactional(rollbackFor = Exception.class)\\npublic GoodsBookResponse book(GoodsBookRequest request) {\\n // 因为用户id都是不重复的,并且可以转换成integer,所以这里可以使用BitSet来存储预约信息,减少存储量\\n RBitSet bookedUsers = redissonClient.getBitSet(BOOK_KEY + request.getGoodsType() + CacheConstant.CACHE_KEY_SEPARATOR + request.getGoodsId());\\n // 不报错则成功\\n bookedUsers.set(Integer.parseInt(request.getBuyerId()));\\n\\n GoodsBook existBook = goodsBookMapper.selectByGoodsIdAndBuyerId(request.getGoodsId(), request.getGoodsType().name(), request.getBuyerId());\\n if (existBook != null) {\\n return new GoodsBookResponse.GoodsBookResponseBuilder().bookId(existBook.getId()).buildSuccess();\\n }\\n GoodsBook goodsBook = GoodsBook.createBook(request);\\n boolean result = save(goodsBook);\\n Assert.isTrue(result, () -> new BizException(RepoErrorCode.INSERT_FAILED));\\n\\n //异步为热门商品添加缓存,失败不影响业务\\n Thread.ofVirtual().start(() -> {\\n // 检查是否为热门商品\\n long bookedCount = bookedUsers.cardinality();\\n if (bookedCount > HOT_GOODS_BOOK_COUNT) {\\n hotGoodsService.addHotGoods(request.getGoodsId(), request.getGoodsType().name());\\n }\\n });\\n\\n return new GoodsBookResponse.GoodsBookResponseBuilder().bookId(goodsBook.getId()).buildSuccess();\\n}\\n\\n
\\n就这样,就可以把一个预约的信息保存下来了。
\\n当29这个用户 ID 预约过之后:
\\n想要查询某个用户是否预约过的时候,可以:
\\npublic boolean isBooked(String goodsId, GoodsType goodsType, String buyerId) {\\n RBitSet bookedUsers = redissonClient.getBitSet(BOOK_KEY + goodsType + CacheConstant.CACHE_KEY_SEPARATOR + goodsId);\\n return bookedUsers.get(Integer.parseInt(buyerId));\\n}\\n
\\n这样就能查看某个用户是否在 bitset 中,即是否预约过。
\\n以上操作其实就相当于执行了命令:
\\nGETBIT \\"goods:book:COLLECTION:10085\\" \\"39\\"\\n
","description":"✅基于 bitset 实现高效的 商品_选修课... 预约 业务场景\\n在电商类行业:\\n\\n针对一些热点商品,我们提供了预约功能,就像天猫超市卖茅台一样,需要提前预约才能购买,不约不能买\\n\\n在校园抢课系统:\\n\\n针对一些热点选修课,我们提供了预约抢课功能,就像天猫超市卖茅台一样,需要提前预约才能购抢,不约不能抢\\n\\n同一个商品,我们想要记录哪些用户预约过,同时需要能够快速查询,并且可以尽可能的减少存储空间,避免浪费,我们选择了使用 BitSet。\\n\\n为了快速的存取,我们使用 Redis,正好Redis 也支持 bitSet 数据结构,同时,我们为了避免Redis…","guid":"https://juejin.cn/post/7485002652285632550","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T07:52:56.330Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9f11ae60dffe40568483547a79663c42~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743407575&x-signature=bYnvaMo5lhB9FkW9BGTEwa1d2QA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c15b5ae5e6ae46f0b52cb7d37e67a0ff~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743407575&x-signature=t6ON3fDazpspzdtD4AUBZao9VK4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"面试官:工作中优化MySQL的手段有哪些?","url":"https://juejin.cn/post/7485068571773714459","content":"MySQL 是面试中必问的模块,而 MySQL 中的优化内容又是常见的面试题,所以本文来看“工作中优化MySQL的手段有哪些?”。
\\n工作中常见的 MySQL 优化手段分为以下五大类:
\\n索引优化包含以下内容:
\\n-- 原始查询(需回表)\\nSELECT * FROM orders WHERE user_id = 100;\\n-- 优化为覆盖索引\\nALTER TABLE orders ADD INDEX idx_user_status (user_id, status);\\nSELECT user_id, status FROM orders WHERE user_id = 100;\\n
\\n只查询需要的字段,减少数据传输和内存占用:
\\n-- 不推荐\\nSELECT * FROM products;\\n-- 推荐\\nSELECT id, name, price FROM products;\\n
\\n大数据量分页时,避免 LIMIT 100000, 10,而是使用上次查询 ID 作为起始 ID 进行查询:
\\n-- 原始分页(性能差)\\nSELECT * FROM logs ORDER BY id LIMIT 100000, 10;\\n-- 优化:使用游标分页(记录上一页最后一条的 id)\\nSELECT * FROM logs WHERE id>100000 ORDER BY id LIMIT 10;\\n
\\n【图片来源于网络,侵权可删】
\\n例如以下示例:
\\n-- 小表(emp)驱动大表(dept)\\nSELECT * FROM emp \\nINNER JOIN dept ON emp.dept_id = dept.id;\\n
\\n长事务会导致锁竞争和回滚段膨胀:
\\n-- 不推荐:事务中包含耗时操作\\nBEGIN;\\nUPDATE account SET balance = balance - 100 WHERE id = 1;\\n-- 执行其他耗时操作...\\nCOMMIT;\\n-- 推荐:尽快提交事务\\n
\\n使用批量插入代替逐条插入:
\\n-- 不推荐\\nINSERT INTO logs (msg) VALUES (\'a\');\\nINSERT INTO logs (msg) VALUES (\'b\');\\n-- 推荐\\nINSERT INTO logs (msg) VALUES (\'a\'), (\'b\');\\n
\\n数据量比较大时,可采取以下措施:
\\nMySQL 常见的优化手段包含 5 大类,索引优化、SQL 优化、事务和锁优化、架构优化和硬件及配置优化。你还知道哪些优化手段呢?欢迎评论区留下你的答案。
\\n\\n","description":"MySQL 是面试中必问的模块,而 MySQL 中的优化内容又是常见的面试题,所以本文来看“工作中优化MySQL的手段有哪些?”。 工作中常见的 MySQL 优化手段分为以下五大类:\\n\\n索引优化:确保高频查询字段有合适索引。\\nSQL优化:减少全表扫描、避免不必要计算。\\n事务与锁优化:避免长事务、使用批量插入。\\n架构优化:数据量大时进行读写分离或分库分表。\\n硬件和配置优化:升级硬件和 MySQL 参数调优。\\n1.索引优化\\n\\n索引优化包含以下内容:\\n\\n高频字段需要创建索引:对于读多少写的场景,一定要创建正确的索引,避免全表扫描,提升查询效率。\\n避免索引失效…","guid":"https://juejin.cn/post/7485068571773714459","author":"Java中文社群","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T07:10:57.530Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0182c71ac79f4c76a01757d736a22104~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1743405056&x-signature=qcUsiPaoOH%2B%2FlZvhhNsUqyN5qqI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/59e256a9a2e44548a57edc80320c4e28~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1743405056&x-signature=eFD8EBBAku7BDRlHow1FQIGfXaM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/37a79b70b6384b06aedb9db4542079df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1743405056&x-signature=BZIND7%2BZeyp93RXG5uwusLERcYk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/92d06439fd2645bb8518e23294757277~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1743405056&x-signature=mxPP7FUHz%2FthDEbMYH%2BCfFHybYc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"Java 泛型:从入门到起飞","url":"https://juejin.cn/post/7484920754871058471","content":"本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:场景题、并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、JVM、设计模式、消息队列等模块。
\\n
在 Java 编程的浩瀚宇宙中,泛型堪称一颗璀璨夺目的 “多面宝石”。它不仅能赋予代码卓越的通用性,极大提升安全性,更能巧妙规避大量繁琐重复的类型转换操作。接下来,就让我们一同踏上探索 Java 泛型的奇妙旅程,从基础知识逐步深入高阶应用,全方位解锁其强大潜能,让你轻松驾驭这一编程利器!
\\n一、泛型初相识
\\n简单来说,泛型就是一种参数化类型的机制。它允许我们在定义类、接口或方法的时候,不指定具体的类型,而是用一个占位符(类型参数)来代替。这样,在使用这些类、接口或方法的时候,再传入具体的类型。
\\n比如说,我们有一个盒子 Box 类,它可以用来装各种东西。如果没有泛型,我们可能需要为每一种要装的东西都创建一个单独的 Box 类,像装苹果的 AppleBox,装橘子的 OrangeBox 等等,这显然太麻烦了。但有了泛型,我们只需要一个 Box 类,就可以装任何类型的东西啦!
\\n在 Java 中,定义一个泛型类的语法是在类名后面加上一对尖括号<>,里面写上类型参数。比如:
\\nclass Box<T> {\\n private T content;\\n public void setContent(T content) {\\n this.content = content;\\n }\\n public T getContent() {\\n return content;\\n }\\n}\\n
\\n这里的T就是类型参数,它可以是任何合法的标识符,通常我们会用单个大写字母来表示,常见的有T(Type 的缩写)、E(Element 的缩写,常用于集合)、K和V(Key 和 Value 的缩写,常用于映射)等。
\\n有了上面定义的Box类,我们就可以像这样使用它:
\\nBox<Integer> integerBox = new Box<>();\\nintegerBox.setContent(10);\\nInteger value = integerBox.getContent();\\n
\\n这里我们创建了一个Box类型的对象,它只能装Integer类型的东西。通过这种方式,编译器可以在编译时就检查类型的正确性,避免了运行时的类型错误。
\\n除了泛型类,我们还可以定义泛型方法。泛型方法的语法是在方法返回类型前面加上<>,里面写上类型参数。比如:
\\nclass Util {\\n public static <T> T getFirstElement(T[] array) {\\n if (array != null && array.length > 0) {\\n return array[0];\\n }\\n return null;\\n }\\n}\\n
\\n这个getFirstElement方法可以接受任何类型的数组,并返回数组的第一个元素。使用起来也很简单:
\\nInteger[] intArray = {1, 2, 3};\\nInteger firstInt = Util.getFirstElement(intArray);\\nString[] stringArray = {\\"Hello\\", \\"World\\"};\\nString firstString = Util.getFirstElement(stringArray);\\n
\\n类型通配符是泛型中的一个重要概念,它用?表示。类型通配符主要有两种用法:上限通配符和下限通配符。
\\n上限通配符的语法是<? extends 类型>,它表示这个通配符所代表的类型是某个类型的子类(包括自身)。例如:
\\nclass Animal {}\\nclass Cat extends Animal {}\\nclass Dog extends Animal {}\\nclass Zoo {\\n public static void printAnimals(List<? extends Animal> animals) {\\n for (Animal animal : animals) {\\n System.out.println(animal);\\n }\\n }\\n}\\n
\\n这里的printAnimals方法可以接受任何类型为Animal及其子类的列表,比如List或List。
\\n下限通配符的语法是<? super 类型>,它表示这个通配符所代表的类型是某个类型的父类(包括自身)。例如:
\\nclass Fruit {}\\nclass Apple extends Fruit {}\\nclass RedApple extends Apple {}\\nclass FruitBasket {\\n public static void addRedApple(List<? super RedApple> basket, RedApple apple) {\\n basket.add(apple);\\n }\\n}\\n
\\n这个addRedApple方法可以接受任何类型为RedApple及其父类的列表,比如List或List。
\\n泛型接口的定义和泛型类类似,也是在接口名后面加上<>和类型参数。例如:
\\ninterface Mapper<K, V> {\\n V map(K key);\\n}\\n
\\n然后我们可以创建实现这个接口的类:
\\nclass IntegerToStringMapper implements Mapper<Integer, String> {\\n @Override\\n public String map(Integer key) {\\n return String.valueOf(key);\\n }\\n}\\n
\\n泛型类和接口也支持继承和多态。比如:
\\nclass Parent<T> {\\n public void print(T t) {\\n System.out.println(t);\\n }\\n}\\nclass Child<T> extends Parent<T> {\\n public void printTwice(T t) {\\n print(t);\\n print(t);\\n }\\n}\\n
\\n这里Child类继承了Parent类,并且可以使用父类的泛型类型参数T。
\\n泛型还可以嵌套使用,比如:
\\nList<List<Integer>> nestedList = new ArrayList<>();\\nList<Integer> innerList = new ArrayList<>();\\ninnerList.add(1);\\ninnerList.add(2);\\nnestedList.add(innerList);\\n
\\n这里nestedList是一个包含List的列表,也就是一个二维列表。
\\n在反射中使用泛型可以让我们编写更加灵活和通用的代码。例如:
\\nimport java.lang.reflect.ParameterizedType;\\nimport java.lang.reflect.Type;\\nclass GenericClass<T> {\\n private T value;\\n public T getValue() {\\n return value;\\n }\\n}\\npublic class GenericReflection {\\n public static void main(String[] args) throws NoSuchFieldException {\\n GenericClass<Integer> genericClass = new GenericClass<>();\\n ParameterizedType genericSuperclass = (ParameterizedType) genericClass.getClass().getGenericSuperclass();\\n Type[] typeArguments = genericSuperclass.getActualTypeArguments();\\n System.out.println(\\"The type argument is: \\" + typeArguments[0]);\\n }\\n}\\n
\\n这段代码通过反射获取了GenericClass的类型参数Integer。
\\nJava 的泛型是在编译时实现的,编译后字节码中的泛型信息会被擦除,只保留原始类型。例如:
\\nList<String> stringList = new ArrayList<>();\\nList<Integer> integerList = new ArrayList<>();\\nSystem.out.println(stringList.getClass() == integerList.getClass());\\n
\\n这段代码输出true,因为在运行时,List和List的实际类型都是ArrayList,泛型信息被擦除了。
\\nJava 的泛型是一个强大而又复杂的特性,它可以让我们编写更加通用、安全和高效的代码。从基础的泛型类和方法,到进阶的类型通配符、泛型接口,再到高阶的泛型嵌套、反射和擦除,每一步都为我们的编程带来了更多的可能性。希望通过这篇文章,你能对 Java 泛型有一个全面而深入的理解,并且在实际编程中能够熟练运用它,让你的代码飞起来!
","description":"在 Java 编程的浩瀚宇宙中,泛型堪称一颗璀璨夺目的 “多面宝石”。它不仅能赋予代码卓越的通用性,极大提升安全性,更能巧妙规避大量繁琐重复的类型转换操作。接下来,就让我们一同踏上探索 Java 泛型的奇妙旅程,从基础知识逐步深入高阶应用,全方位解锁其强大潜能,让你轻松驾驭这一编程利器! 一、泛型初相识\\n\\n(一)什么是泛型?\\n\\n简单来说,泛型就是一种参数化类型的机制。它允许我们在定义类、接口或方法的时候,不指定具体的类型,而是用一个占位符(类型参数)来代替。这样,在使用这些类、接口或方法的时候,再传入具体的类型。\\n\\n比如说,我们有一个盒子 Box 类…","guid":"https://juejin.cn/post/7484920754871058471","author":"装睡鹿先生","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T05:58:39.209Z","media":null,"categories":["后端","Java","Spring","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"DataWorks 体验笔记 :MaxCompute 用 Python 对数据进行二次处理","url":"https://juejin.cn/post/7484589584718888999","content":"前面2篇已经体验了 DataWorks 的简单使用,但是一些复杂的业务通过纯 SQL 是没有办法处理的。
\\n这个时候就需要进行代码处理了 ,通常像 Flink 支持 Java 工具包上传的方式对是数据进行处理 ,另外很多大数据工具类会考虑通过脚本语言进行处理 。Python 就是一个很好的实现方式。
\\n另外注意一下 ,这里为了方便 ,把 MaxCompute 和 DataWorks 是看成一体了 ,MaxCompute 也可以是一个独立的组件,这里没有单独区分开。
\\n首先官方文档里面对这些都有详细的描述 :@ 官方文档
\\n\\n\\n使用方式 :
\\n
PyODPS2
和 PyODPS3
,分别对应着 python2 / python3在 DataWorks 里面,是通过 PyODPS 类型来进行 Python 层面的数据处理 。 我这边基于昨天的操作来替换其中的一个关键节点 ,来实现类似的功能 :
\\nCREATE TABLE IF NOT EXISTS self_user_info_1d (\\n uid STRING COMMENT \'用户ID\',\\n username STRING COMMENT \'用户名称\',\\n email STRING COMMENT \'用户\',\\n password STRING COMMENT \'密码 \',\\n first_name STRING COMMENT \'名称\',\\n last_name STRING COMMENT \'名称\',\\n full_name STRING COMMENT \'全名\',\\n created_at STRING COMMENT \'年龄段\',\\n updated_at STRING COMMENT \'星座\'\\n)\\n\\n\\nCOMMENT \'用户行为分析案例-用户画像数据\'\\nPARTITIONED BY (\\n dt STRING COMMENT \'业务日期, 格式yyyymmdd\'\\n)\\nLIFECYCLE 7;\\n\\nINSERT OVERWRITE TABLE self_user_info_1d \\nSELECT id,\\n username,\\n email,\\n password,\\n first_name,\\n last_name,\\n first_name AS full_name,\\n created_at,\\n updated_at \\nFROM \\n users where pt = 20250313;\\n\\n\\nSELECT * FROM self_user_info_1d\\n\\n
\\nfrom odps import ODPS \\nfrom odps.models import Schema, Column, Partition \\n\\n# 板块:创建表 \\ntry: \\n # 定义表结构 \\n columns = [ \\n Column(name=\'uid\', type=\'string\', comment=\'用户ID\'), \\n Column(name=\'username\', type=\'string\', comment=\'用户名称\'), \\n Column(name=\'email\', type=\'string\', comment=\'用户\'), \\n Column(name=\'password\', type=\'string\', comment=\'密码\'), \\n Column(name=\'first_name\', type=\'string\', comment=\'名称\'), \\n Column(name=\'last_name\', type=\'string\', comment=\'名称\'), \\n Column(name=\'full_name\', type=\'string\', comment=\'全名\'), \\n Column(name=\'created_at\', type=\'string\', comment=\'年龄段\'), \\n Column(name=\'updated_at\', type=\'string\', comment=\'星座\') \\n ] \\n partitions = [Partition(name=\'dt\', type=\'string\', comment=\'业务日期, 格式yyyymmdd\')] \\n schema = Schema(columns=columns, partitions=partitions) \\n\\n # 创建表 \\n table = o.create_table(\'self_user_info_1d_002\', schema, comment=\'用户行为分析案例-用户画像数据\', lifecycle=7) \\n print(f\\"INFO: 表创建成功 - 表名: {table.name}\\") \\n\\nexcept Exception as e: \\n print(f\\"ERROR: 创建表失败 - 错误信息: {str(e)}\\") \\n\\n# 板块:插入数据 \\ntry: \\n # 执行SQL插入操作 \\n insert_sql = \\"\\"\\" \\n INSERT OVERWRITE TABLE self_user_info_1d_002 PARTITION(dt=\'20250315\') \\n SELECT id, username, email, password, first_name, last_name, first_name AS full_name, \\n created_at, updated_at \\n FROM users WHERE pt = \'20250315\'; \\n \\"\\"\\" \\n o.execute_sql(insert_sql) \\n print(\\"INFO: 数据插入成功\\") \\n\\n\\n\\nexcept Exception as e: \\n print(f\\"ERROR: 数据插入失败 - 错误信息: {str(e)}\\") \\n
\\nfrom odps import ODPS \\nfrom odps.models import Schema, Column, Partition \\nimport random \\n\\n# 板块:创建表 \\ntry: \\n # 定义表结构 \\n columns = [ \\n Column(name=\'uid\', type=\'string\', comment=\'用户ID\'), \\n Column(name=\'username\', type=\'string\', comment=\'用户名称\'), \\n Column(name=\'email\', type=\'string\', comment=\'用户\'), \\n Column(name=\'password\', type=\'string\', comment=\'密码\'), \\n Column(name=\'first_name\', type=\'string\', comment=\'名称\'), \\n Column(name=\'last_name\', type=\'string\', comment=\'名称\'), \\n Column(name=\'full_name\', type=\'string\', comment=\'全名\'), \\n Column(name=\'roles\', type=\'string\', comment=\'用户角色\'), # 添加角色字段 \\n Column(name=\'created_at\', type=\'string\', comment=\'年龄段\'), \\n Column(name=\'updated_at\', type=\'string\', comment=\'星座\') \\n ] \\n partitions = [Partition(name=\'dt\', type=\'string\', comment=\'业务日期, 格式yyyymmdd\')] \\n schema = Schema(columns=columns, partitions=partitions) \\n\\n # 创建表 \\n o.delete_table(\'self_user_info_1d_002\', if_exists=True) \\n table = o.create_table(\'self_user_info_1d_002\', schema, comment=\'用户行为分析案例-用户画像数据\', lifecycle=7) \\n print(f\\"INFO: 表创建成功 - 表名: {table.name}\\") \\n\\nexcept Exception as e: \\n print(f\\"ERROR: 创建表失败 - 错误信息: {str(e)}\\") \\n\\n# 板块:查询数据并处理 \\ntry: \\n # 查询用户数据 \\n select_sql_users = \\"SELECT id, username, email, password, first_name, last_name, created_at, updated_at FROM users WHERE pt = \'20250315\';\\" \\n records = [] \\n \\n with o.execute_sql(select_sql_users).open_reader() as reader: \\n for record in reader: \\n # 模拟查询用户角色 \\n user_id = record[\'id\'] \\n # 伪代码:根据用户ID获取角色 \\n # 假设我们有一个字典来模拟角色数据 \\n simulated_user_roles = { \\n \'1\': [\'admin\', \'editor\'], \\n \'2\': [\'viewer\'], \\n \'3\': [\'editor\', \'contributor\'], \\n \'4\': [\'admin\', \'viewer\', \'editor\'], \\n \'5\': [\'contributor\', \'viewer\'], \\n # 其他用户ID及其角色... \\n } \\n roles = simulated_user_roles.get(user_id, []) # 获取角色,默认为空列表 \\n \\n # 随机选择一个角色,如果没有角色则设为\'无角色\' \\n selected_role = random.choice(roles) if roles else \'无角色\' \\n\\n # 在这里进行数据处理,例如添加后缀和整合角色信息 \\n processed_record = { \\n \'uid\': record[\'id\'], \\n \'username\': f\\"{record[\'username\']}_suffix\\", # 添加后缀 \\n \'email\': record[\'email\'], \\n \'password\': record[\'password\'], \\n \'first_name\': record[\'first_name\'], \\n \'last_name\': record[\'last_name\'], \\n \'full_name\': f\\"{record[\'first_name\']} {record[\'last_name\']}\\", # 组合名称 \\n \'roles\': selected_role, # 随机选择的角色 \\n \'created_at\': record[\'created_at\'], \\n \'updated_at\': record[\'updated_at\'] \\n } \\n records.append(processed_record) \\n\\n print(\\"INFO: 数据查询和处理完成\\") \\n\\nexcept Exception as e: \\n print(f\\"ERROR: 数据查询失败 - 错误信息: {str(e)}\\") \\n\\n# 板块:写入处理后的数据 \\ntry: \\n # 准备写入数据 \\n write_records = [ \\n [record[\'uid\'], record[\'username\'], record[\'email\'], record[\'password\'], \\n record[\'first_name\'], record[\'last_name\'], record[\'full_name\'], \\n record[\'roles\'], # 写入随机选择的角色信息 \\n record[\'created_at\'], record[\'updated_at\']] \\n for record in records \\n ] \\n\\n # 写入数据到新表 \\n o.write_table(\'self_user_info_1d_002\', write_records, partition=\'dt=20250315\', create_partition=True) \\n print(\\"INFO: 数据写入成功\\") \\n\\nexcept Exception as e: \\n print(f\\"ERROR: 数据写入失败 - 错误信息: {str(e)}\\") \\n
\\nfrom odps import ODPS\\nfrom odps.models import Schema, Column, Partition\\nimport random\\n\\n# # 板块 : 运行环境 - 项目空间查询\\ntry:\\n # 获取特定项目空间 \\n project = o.get_project(\'DF_cs_624101\') \\n print(f\\"INFO: 获取特定项目空间成功 - 项目名称: {project.name}\\") \\n\\n # 获取当前项目空间 \\n current_project = o.get_project() \\n print(f\\"INFO: 获取当前项目空间成功 - 项目名称: {current_project.name}\\") \\n\\n # 验证项目空间是否存在 \\n project_name = \'DF_cs_624101\' \\n is_exist = o.exist_project(project_name) \\n print(f\\"INFO: 验证项目空间是否存在完成 - 项目名称: {project_name}, 是否存在: {is_exist}\\") \\n\\nexcept Exception as e:\\n print(f\\"ERROR: 运行环境获取异常{e}\\",e)\\n\\n\\n# # 板块 : Schema 环境准备\\ntry: \\n\\n #- 列举所有Schema \\n schemas = o.list_schemas() \\n if not schemas: \\n print(\\"INFO: 当前没有任何Schema\\") \\n else: \\n for schema in schemas: \\n print(f\\"INFO: Schema名称: {schema.name}\\") \\n if schema.name == \'my_new_schema\':\\n o.delete_schema(\'my_new_schema\', if_exists=True) \\n\\n #- 创建Schema \\n schema_name = \'my_new_schema\' \\n schema = o.create_schema(schema_name) \\n print(f\\"INFO: 创建Schema成功 - Schema名称: {schema_name}\\") \\n\\n #- 删除Schema \\n delete_schema_name = \'my_new_schema\' \\n o.delete_schema(delete_schema_name, if_exists=True) \\n print(f\\"INFO: 删除Schema成功 - Schema名称: {delete_schema_name}\\") \\n\\nexcept Exception as e: \\n print(f\\"ERROR: Schema操作失败 - 错误信息: {str(e)}\\") \\n\\n\\n# 板块 : Schema 创建表操作\\ntry: \\n # 定义表结构(此处定义了Columns 和 分区信息) \\n columns = [ \\n Column(name=\'id\', type=\'bigint\', comment=\'the column\'), \\n Column(name=\'age\', type=\'double\', comment=\'the column2\'),\\n Column(name=\'name\', type=\'string\', comment=\'the column3\') \\n ] \\n partitions = [Partition(name=\'pt\', type=\'string\', comment=\'the partition\')]\\n schema = Schema(columns=columns, partitions=partitions)\\n # 打印列信息 \\n for column in columns: \\n print(f\\"INFO 列 name=\'{column.name}\', type=\'{column.type}\', comment=\'{column.comment}\'\\") \\n \\n # 打印分区信息 \\n for partition in partitions: \\n print(f\\"INFO 分区 name=\'{partition.name}\', type=\'{partition.type}\', comment=\'{partition.comment}\'\\") \\n \\n # 打印字段类型信息 \\n for column in schema.columns: \\n print(f\\"INFO 类型 name=\'{column.name}\', type=\'{column.type}\'\\") \\n\\n # schema 创建表\\n o.delete_table(\'my_new_table\', if_exists=True) \\n table = o.create_table(\'my_new_table\', schema) \\n print(f\\"INFO: 表创建完成\\") \\n print(f\\"INFO: 表的shema :{table.table_schema}\\") \\n\\n # #- 列举指定Schema下的所有表 \\n # tables = o.list_tables(schema=table.schema) \\n # if not tables: \\n # print(f\\"INFO: Schema {specified_schema} 下没有任何表\\") \\n # else: \\n # for table in tables: \\n # print(f\\"INFO: 表名称: {table.name}\\") \\n\\n \\nexcept Exception as e: \\n print(f\\"ERROR: Schema操作失败 - 错误信息: {str(e)}\\") \\n\\n\\n# 模块 : 插入数据\\ntry: \\n # 获取表对象 \\n table = o.get_table(\'my_new_table\') \\n\\n records = [] \\n for _ in range(5): \\n id = random.randint(100000, 999999) # 随机生成id \\n age = random.randint(20, 60) # 随机生成年龄 \\n name = f\\"test_{id}\\" # 生成测试name \\n records.append([id, age, name]) \\n\\n # 插入数据 \\n o.write_table(table, records, partition=\'pt=test\', create_partition=True) \\n print(f\\"INFO: 插入数据成功 - {len(records)} 条记录\\") \\n\\n # 通过read_table方法读取数据 \\n for record in o.read_table(\'my_new_table\',partition=\'pt=test\'): \\n print(record) \\n\\n # 通过表对象读取数据 \\n table = o.get_table(\'my_new_table\') \\n with table.open_reader(partition=\'pt=test\') as reader: \\n for record in reader: \\n print(record) \\n\\n # 删除表中的所有数据 \\n # table.truncate() \\n\\n\\nexcept Exception as e: \\n print(f\\"ERROR: Table 操作失败 - 错误信息: {str(e)}\\") \\n\\n\\n\\n# 板块 :SQL 数据处理篇\\ntry: \\n\\n # 执行 SQL 语句 \\n sql = \\"select id,age,name from my_new_table where pt = \'test\' \\" \\n result = o.execute_sql(sql) \\n print(f\\"INFO: SQL 执行成功 - SQL: {sql}\\") \\n\\n # 执行 SQL 语句 \\n sql = \\"select id,age,name from my_new_table where pt = \'test\' \\"\\n with o.execute_sql(sql).open_reader() as reader: \\n for record in reader: \\n print(record) \\n\\nexcept Exception as e: \\n print(f\\"ERROR: SQL 操作失败 - 错误信息: {str(e)}\\") \\n\\n
\\n@ 操作指南
\\n在执行的过程中 ,可以通过单个节点的执行 ,也可以线性处理 , 其中比较麻烦的是确认代码写的对不对。
\\n一般需要计算的数据最终会放在 MaxCompute 里面 ,这些可以直接在 MaxCompute 控制台里面查看 :
\\n到了这一步 ,基本上常见的业务都可以通过 Python 进行二次处理 ,从而达到预期的需求。
\\n后面如果有空 ,会做一些更复杂的案例,总结一下。
\\n当不让使用redis分布式锁,或者集群不可用的时候,如何做到防止用户重复点击的功能呢?
\\n有以下几个思路可以供大家参考:
\\n1、首先就是前端需要做一些按钮置灰的动作,让用户点击一次之后,按钮就直接禁用调,让用户无法重复点击。但是有些情况可能没来得及置灰就重复点击了,或者有些用户自己绕过了置灰也可以点击。
\\n2、可以通过token的机制避免重复提交,当用户访问页面的时候,请求后端服务拿到一个token,然后下一次接口点击的时候把token带过来,服务端对token进行验证,验证该token是否被使用过,如果没有被使用过才可以进行点击。验证的逻辑可以放在数据库中,通过数据库的悲观锁或者乐观锁都可以实现。
\\n比如,以下就是我自己的项目中,通过token来进行订单防重复的一个具体的交互图:
\\n滑动窗口限流,滑动窗口限流是一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。我们可以限制一分钟或者一秒钟内用户只能发起一次请求来防止重复点击。
\\n可以使用布隆过滤器,他可以快速判断某个元素是否存在于集合中。可以在服务器端使用布隆过滤器记录某个操作是否已经被执行过,从而防止重复执行。
\\n如果布隆过滤器不存在,则一定不存在,所以,如果没查到,说明一定没有幂等操作,直接执行就行了。
\\n如果查询布隆过滤器发现有命中,则需要在服务数据库做一次幂等判断。
\\n大多数情况下,需要幂等的情况占比小,所以可以用布隆过滤器做一次fail-fast的快速校验。
Redis其实也是一个集中式的存储服务,在特殊情况下,如果无法使用,一般的做法都是降级成直接使用数据库。
\\n5、还有种方式,那就是参考 ruoyi 框架中的防重复提交的实现方案,其实就是把表单信息做校验并保存在 REDIS 中,下次再提交的时候做校验,如果和上次提交的内容一样,并且时间小于一定的时间间隔,则拒绝请求
","description":"✅不用redis分布式锁, 如何防止用户重复点击? 当不让使用redis分布式锁,或者集群不可用的时候,如何做到防止用户重复点击的功能呢?\\n\\n有以下几个思路可以供大家参考:\\n\\n1、前端需要做一些按钮置灰\\n\\n1、首先就是前端需要做一些按钮置灰的动作,让用户点击一次之后,按钮就直接禁用调,让用户无法重复点击。但是有些情况可能没来得及置灰就重复点击了,或者有些用户自己绕过了置灰也可以点击。\\n\\n2、token的机制避免重复提交\\n\\n2、可以通过token的机制避免重复提交,当用户访问页面的时候,请求后端服务拿到一个token,然后下一次接口点击的时候把token带过来…","guid":"https://juejin.cn/post/7484262318723203112","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T08:46:21.773Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a13d5efe695548b08dc31e263391d7b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=PNtKu9mhq4Kf%2B%2BToJgFRDDyejAE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0186ad7039cb4ba68aac13f42562a132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=3tkKIkpi7tP%2BJVZ8rPxTamcrlR0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b86ebbc22d3146f4b710d52f00350f0d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=3wjb7ZqFtI%2BXUV6IRrFBDDRUzVE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/63663ae301f34a42b2fea628e3e83218~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=CttOEz1L7SDmOyHQBlZserupHM4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b5262a6d036d431684c4c90cbac8cabe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=1h3wXylTp6s7M51P%2BKS6xzXZe8g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2545160d04b4461498cb503f1af13dab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=XWSFZ2ARIjFlHJTSXSyQfQgqH%2BI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d47ef14559a42e28c54765e8ebf1757~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743324381&x-signature=6stDpzfi4ZbxPPOEFvFP5SBQK%2BI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能!","url":"https://juejin.cn/post/7484191942358499368","content":"Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。🔐
\\n目前最新版本 v1.41.0
已推送至 Maven
中央仓库 🎉,大家可以通过如下方式引入:
<!-- Sa-Token 权限认证 --\x3e\\n<dependency>\\n <groupId>cn.dev33</groupId>\\n <artifactId>sa-token-spring-boot-starter</artifactId>\\n <version>1.41.0</version>\\n</dependency>\\n
\\n该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:
\\n本次更新针对防火墙新增了多条校验规则,之前的规则为:
\\n本次新增规则为:
\\n并且本次更新开放了 hooks 机制,允许开发者注册自定义的校验规则 🛠️,参考如下:
\\n@PostConstruct\\npublic void saTokenPostConstruct() {\\n // 注册新 hook 演示,拦截所有带有 pwd 参数的请求,拒绝响应 \\n SaFirewallStrategy.instance.registerHook((req, res, extArg)->{\\n if(req.getParam(\\"pwd\\") != null) {\\n throw new FirewallCheckException(\\"请求中不可包含 pwd 参数\\");\\n }\\n });\\n}\\n
\\n文档直达地址:Sa-Token 防火墙 🔗
\\n之前在 Sa-Token 中也有插件体系,不过都是利用 SpringBoot 的 SPI 机制完成组件注册的。
\\n这种注册机制有一个问题,就是插件只能在 SpringBoot 环境下正常工作,在其它环境,比如 Solon 项目中,就只能手动注册插件才行 😫。
\\n也就是说,严格来讲,这些插件只能算是 SpringBoot 的插件,而非 Sa-Token 框架的插件 🌐。
\\n为了提高插件的通用性,Sa-Token 设计了自己的 SPI 机制,使得这些插件可以在更多的项目环境下正常工作 🚀。
\\n第一步:实现插件注册类,此类需要 implements SaTokenPlugin
接口 👨💻:
/**\\n * SaToken 插件安装:插件作用描述\\n */\\npublic class SaTokenPluginForXxx implements SaTokenPlugin {\\n @Override\\n public void install() {\\n // 书写需要在项目启动时执行的代码,例如:\\n // SaManager.setXxx(new SaXxxForXxx());\\n }\\n}\\n
\\n第二步:在项目的 resources\\\\META-INF\\\\satoken\\\\
文件夹下 📂 创建 cn.dev33.satoken.plugin.SaTokenPlugin
文件,内容为该插件注册类的完全限定名:
cn.dev33.satoken.plugin.SaTokenPluginForXxx\\n
\\n这样便可以在项目启动时,被 Sa-Token 插件管理器加载到此插件,执行插件注册类的 install 方法,完成插件安装 ✅。
\\n文档直达地址:Sa-Token 插件开发指南 🔗
\\n在之前的版本中,Redis 集成通常和具体的序列化方式耦合在一起,这不仅让 Redis 相关插件产生大量的重复冗余代码,也让大家在选择 Redis 插件时严重受限。⚠️
\\n本次版本更新彻底重构了此模块,将数据读写与序列化操作分离,使其每一块都可以单独自定义实现类,做到灵活扩展 ✨,例如:
\\n所有实现类均可以按需选择,自由搭配,大大提高灵活性🏗️。
\\nSaLoginParameter (前SaLoginModel) 用于控制登录操作中的部分细节行为,本次新增的配置项有:
\\n以上大部分配置项在之前的版本中也有支持,不过它们都被定义在了全局配置类 SaTokenConfig 之上,本次更新支持在 SaLoginParameter 中定义这些配置项,\\n这将让登录策略的控制变得更加灵活。✨
\\nSaLogoutParameter 用于控制注销操作中的部分细节行为️,例如:
\\n通过 Range
参数决定注销范围 🎯:
// 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话\\nStpUtil.logout(new SaLogoutParameter().setRange(SaLogoutRange.TOKEN));\\n
\\n通过 DeviceType
参数决定哪些登录设备类型参与注销 💻:
// 指定 10001 账号,所有 PC 端注销下线,其它端如 APP 端不受影响 \\nStpUtil.logout(10001, new SaLogoutParameter().setDeviceType(\\"PC\\"));\\n
\\n还有其它参数此处暂不逐一列举,文档直达地址:Sa-Token 登录参数 & 注销参数 🔗
\\nStpUtil.setTokenValue(\\"xxx\\")
、loginParameter.getIsWriteHeader()
空指针的问题。这个没啥好说的,有 bug 🐛 必须修复。
\\nfix issue:#IBKSM0 🔗
\\n多应用模式就是指,允许在对接多个系统时分别使用不同的秘钥等配置项,配置示例如下 📝:
\\nsa-token: \\n # API 签名配置 多应用模式\\n sign-many:\\n # 应用1\\n xm-shop:\\n secret-key: 0123456789abcdefg\\n digest-algo: md5\\n # 应用2\\n xm-forum:\\n secret-key: 0123456789hijklmnopq\\n digest-algo: sha256\\n # 应用3\\n xm-video:\\n secret-key: 12341234aaaaccccdddd\\n digest-algo: sha512\\n
\\n然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作 👨💻:
\\n// 创建签名示例\\nString paramStr = SaSignMany.getSignTemplate(\\"xm-shop\\").addSignParamsAndJoin(paramMap);\\n\\n// 校验签名示例\\nSaSignMany.getSignTemplate(\\"xm-shop\\").checkRequest(SaHolder.getRequest());\\n
\\nCaffeine 是一个基于 Java 的高性能本地缓存库,本次新增 sa-token-caffeine 插件用于将 Caffeine 作为 Sa-Token 的缓存层,存储会话鉴权数据。🚀\\n这进一步丰富了 Sa-Token 的缓存层插件生态。🌱
\\n<!-- Sa-Token 整合 Caffeine --\x3e\\n<dependency>\\n <groupId>cn.dev33</groupId>\\n <artifactId>sa-token-caffeine</artifactId>\\n <version>1.41.0</version>\\n</dependency>\\n
\\n引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。(娱乐向,不建议上生产 🎭)
\\n例如:以base64 编码,采用:元素周期表 🧪、特殊符号 🔣、或 emoji 😊 作为元字符集存储数据 :
\\n除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.41.0 版本的完整更新日志:
\\nStpUtil.setTokenValue(\\"xxx\\")
、loginParameter.getIsWriteHeader()
空指针的问题。 fix: #IBKSM0SaDisableWrapperInfo.createNotDisabled()
默认返回值封禁等级改为 -2,以保证向之前版本兼容。Object
与 String
的序列化方式。 [重要]SaTokenDao
模块,将序列化与存储操作分离。 [重要]SaTokenDao
默认实现类,优化底层设计。isLastingCookie
配置项支持在全局配置中定义了。SaLoginModel
-> SaLoginParameter
。 [不向下兼容]TokenSign
-> SaTerminalInfo
。 [不向下兼容]SaTerminalInfo
新增 extraData
自定义扩展数据设置。SaLoginParameter
支持配置 isConcurrent
、isShare
、maxLoginCount
、maxTryTimes
。SaLogoutParameter
,用于控制注销会话时的各种细节。 [重要]StpLogic#isTrustDeviceId
方法,用于判断指定设备是否为可信任设备。StpUtil.getTerminalListByLoginId(loginId)
、StpUtil.forEachTerminalList(loginId)
方法,以更方便的实现单账号会话管理。@SaCheckSign
注解鉴权,用于 API 签名参数校验。is-share
默认值改为 false。 [不向下兼容]BCrypt
标注为 @Deprecated
。sa-token-quick-login
支持 SpringBoot3
项目。 fix: #IAFQNE、#673SaTokenConfig
新增 replacedRange
、overflowLogoutMode
、logoutRange
、isLogoutKeepFreezeOps
、isLogoutKeepTokenSession
配置项。sa-token-serializer-features
插件,用于实现各种形式的自定义字符集序列化方案。sa-token-fastjson
插件。sa-token-fastjson2
插件。sa-token-snack3
插件。sa-token-caffeine
插件。sa-token-json-test
json 模块单元测试。sa-token-serializer-test
序列化模块单元测试。preview-doc.bat
文件,一键启动文档预览。sa-token-demo/pom.xml
以便在 idea 中一键导入所有 demo 项目。.gitignore
文件sa-token-solon-plugin
插件。更新日志在线文档直达链接:sa-token.cc/doc.html#/m…
\\n代码仓库地址:gitee.com/dromara/sa-…
\\n框架功能结构图:
\\n文章首发公众号【风象南】
\\n\\nSpring Boot 作为一款广泛使用的 Java 开发框架,虽然为开发者提供了诸多便利,但也并非无懈可击,其安全漏洞问题不容忽视。本文将深入探讨 Spring Boot 常见的安全漏洞类型、产生原因以及相应的解决方案,帮助开发者更好地保障应用程序的安全。
\\n漏洞描述: 当应用程序使用用户输入的数据来构建 SQL 查询时,如果没有进行适当的过滤或转义,攻击者就可以通过构造恶意的 SQL 语句来访问、修改甚至删除数据库中的数据。
\\n危险指数: 💥💥💥💥💥
\\n反面示例:
\\n@GetMapping(\\"/users\\")\\npublic List<User> getUsers(@RequestParam String username) {\\n String sql = \\"SELECT * FROM users WHERE username = \'\\" + username + \\"\'\\";\\n // ... 执行 SQL 查询\\n}\\n
\\n解决方案:
\\n正确示例:
\\n@GetMapping(\\"/users\\")\\npublic List<User> getUsers(@RequestParam String username) {\\n return userRepository.findByUsername(username);\\n}\\n\\n// UserRepository.java\\npublic interface UserRepository extends JpaRepository<User, Long> {\\n List<User> findByUsername(String username);\\n}\\n
\\n漏洞描述: 攻击者通过在 Web 页面中注入恶意脚本,当其他用户访问该页面时,这些脚本就会在用户的浏览器中执行,从而窃取用户信息、篡改页面内容等。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\n// 在 Spring Security 配置中启用 XSS 保护\\n@Configuration\\npublic class SecurityConfig extends WebSecurityConfigurerAdapter {\\n @Override\\n protected void configure(HttpSecurity http) throws Exception {\\n http.headers().xssProtection().and().contentSecurityPolicy(\\"script-src \'self\'\\");\\n }\\n}\\n
\\n漏洞描述: 配置文件中的数据库凭证、API 密钥等敏感信息可能通过日志、异常信息或者 API 响应泄露。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\n// 使用 @Value 注入环境变量\\n@Value(\\"${database.password}\\")\\nprivate String dbPassword;\\n\\n// 或者使用 Spring Boot 的配置属性类\\n@ConfigurationProperties(prefix = \\"database\\")\\npublic class DatabaseProperties {\\n private String password;\\n // getters and setters\\n}\\n
\\n漏洞描述: 攻击者诱导用户访问一个包含恶意请求的网站,利用用户已登录的身份,在用户不知情的情况下执行操作。
\\n危险指数: 💥💥💥
\\n解决方案:
\\n正确示例:
\\n<!-- 在 Thymeleaf 模板中添加 CSRF Token --\x3e\\n<form th:action=\\"@{/profile/update}\\" method=\\"post\\">\\n <input type=\\"hidden\\" th:name=\\"${_csrf.parameterName}\\" th:value=\\"${_csrf.token}\\" />\\n <!-- 表单内容 --\x3e\\n <button type=\\"submit\\">更新</button>\\n</form>\\n
\\n漏洞描述: Spring Boot 项目通常有大量第三方依赖,这些依赖本身可能存在安全漏洞。
\\n危险指数: 💥💥💥
\\n解决方案:
\\n正确示例:
\\n# 使用 Maven 插件检查依赖漏洞\\nmvn org.owasp:dependency-check-maven:check\\n\\n# 或者使用 Gradle 插件\\n./gradlew dependencyCheckAnalyze\\n
\\n漏洞描述: 未正确实施访问控制,导致用户可以访问或修改他们本不应该访问的资源。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n@PreAuthorize
、@PostAuthorize
等注解。正确示例:
\\n@RestController\\n@RequestMapping(\\"/api/admin\\")\\npublic class AdminController {\\n \\n @PreAuthorize(\\"hasRole(\'ADMIN\')\\")\\n @GetMapping(\\"/users\\")\\n public List<User> getAllUsers() {\\n // 仅管理员可访问\\n return userService.findAll();\\n }\\n}\\n
\\n漏洞描述: 当应用程序反序列化不可信的数据时,攻击者可能利用这一点执行任意代码。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\nObjectMapper mapper = new ObjectMapper();\\n// 禁用所有默认类型信息的使用\\nmapper.disableDefaultTyping();\\n// 或者在新版本中使用\\nmapper.activateDefaultTyping(\\n LaissezFaireSubTypeValidator.instance, \\n ObjectMapper.DefaultTyping.NONE\\n);\\n
\\n漏洞描述: 默认配置、开发环境配置或不安全的配置可能导致安全漏洞。
\\n危险指数: 💥💥💥
\\n解决方案:
\\n正确示例:
\\n# application-prod.yml\\nspring:\\n security:\\n headers:\\n xss: true\\n content-type: true\\n frame: true\\n cache: true\\n boot:\\n admin:\\n client:\\n enabled: false\\nmanagement:\\n endpoints:\\n web:\\n exposure:\\n include: health,info\\n
\\n漏洞描述: 未对上传的文件进行严格校验和限制,可能导致攻击者上传恶意文件(如 JSP 木马、WebShell 等),或进行服务器端文件包含攻击。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\n@PostMapping(\\"/upload\\")\\npublic ResponseEntity<String> uploadFile(@RequestParam(\\"file\\") MultipartFile file) {\\n // 检查文件类型\\n String contentType = file.getContentType();\\n if (!Arrays.asList(\\"image/jpeg\\", \\"image/png\\", \\"image/gif\\").contains(contentType)) {\\n return ResponseEntity.badRequest().body(\\"不支持的文件类型\\");\\n }\\n \\n // 检查文件大小\\n if (file.getSize() > 5 * 1024 * 1024) { // 5MB\\n return ResponseEntity.badRequest().body(\\"文件过大\\");\\n }\\n \\n // 生成随机文件名\\n String fileName = UUID.randomUUID().toString() + \\n file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(\\".\\"));\\n \\n // 保存到安全路径\\n Path uploadPath = Paths.get(\\"/app/uploads\\").resolve(fileName);\\n try {\\n Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);\\n return ResponseEntity.ok(\\"上传成功\\");\\n } catch (IOException e) {\\n return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(\\"上传失败\\");\\n }\\n}\\n
\\n漏洞描述: 使用不安全的方式存储用户密码(如明文存储、简单 MD5 加密等),一旦数据库泄露,用户密码就会被暴露。
\\n危险指数: 💥💥💥💥💥
\\n解决方案:
\\n正确示例:
\\n@Configuration\\npublic class SecurityConfig extends WebSecurityConfigurerAdapter {\\n \\n @Bean\\n public PasswordEncoder passwordEncoder() {\\n return new BCryptPasswordEncoder(12); // 使用 12 轮加密\\n }\\n}\\n\\n@Service\\npublic class UserService {\\n @Autowired\\n private PasswordEncoder passwordEncoder;\\n \\n public void createUser(UserDTO userDTO) {\\n // 密码强度校验\\n if (!isPasswordStrong(userDTO.getPassword())) {\\n throw new InvalidPasswordException(\\"密码强度不够\\");\\n }\\n \\n User user = new User();\\n user.setUsername(userDTO.getUsername());\\n // 使用 BCrypt 加密存储密码\\n user.setPassword(passwordEncoder.encode(userDTO.getPassword()));\\n userRepository.save(user);\\n }\\n \\n private boolean isPasswordStrong(String password) {\\n // 密码至少8位,包含大小写字母、数字和特殊字符\\n String pattern = \\"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\\\S+$).{8,}$\\";\\n return password.matches(pattern);\\n }\\n}\\n\\n// 密码校验工具类\\n@Component\\npublic class PasswordValidator {\\n public static final String PASSWORD_PATTERN = \\n \\"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\\\S+$).{8,}$\\";\\n \\n public boolean validate(String password) {\\n Pattern pattern = Pattern.compile(PASSWORD_PATTERN);\\n return pattern.matcher(password).matches();\\n }\\n \\n public String getPasswordRequirements() {\\n return \\"密码必须包含:\\\\n\\" +\\n \\"- 至少8个字符\\\\n\\" +\\n \\"- 至少一个大写字母\\\\n\\" +\\n \\"- 至少一个小写字母\\\\n\\" +\\n \\"- 至少一个数字\\\\n\\" +\\n \\"- 至少一个特殊字符(@#$%^&+=)\\";\\n }\\n}\\n
\\n漏洞描述: 未加密的 HTTP 通信可能导致数据在传输过程中被拦截、篡改或侦听。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\n// Spring Boot application.properties 中配置 HTTPS\\nserver.ssl.enabled=true\\nserver.ssl.key-store-type=JKS\\nserver.ssl.key-store=classpath:keystore.jks\\nserver.ssl.key-store-password=changeit\\nserver.ssl.key-alias=tomcat\\n
\\n漏洞描述: 未正确配置 HTTP 安全头,导致应用容易遭受各种 Web 安全攻击,如点击劫持、MIME 类型嗅探等。
\\n危险指数: 💥💥💥
\\n解决方案:
\\n正确示例:
\\n@Configuration\\npublic class SecurityHeaderConfig extends WebSecurityConfigurerAdapter {\\n @Override\\n protected void configure(HttpSecurity http) throws Exception {\\n http\\n .headers(headers -> headers\\n .frameOptions().deny()\\n .xssProtection()\\n .and()\\n .contentSecurityPolicy(\\n \\"default-src \'self\'; \\" +\\n \\"script-src \'self\' \'unsafe-inline\' \'unsafe-eval\'; \\" +\\n \\"style-src \'self\' \'unsafe-inline\'\\"\\n )\\n );\\n }\\n}\\n
\\n漏洞描述: Spring Boot Actuator 默认暴露 /actuator
下的健康检查、配置信息等接口。如果未正确配置权限,攻击者可能通过这些接口获取敏感数据(如数据库连接信息、内存状态),甚至远程执行命令。
危险指数: 💥💥💥💥
\\n反面示例:
\\n# 未做任何安全配置的 application.yml\\nmanagement:\\n endpoints:\\n web:\\n exposure:\\n include: \\"*\\" # 暴露所有端点\\n
\\n解决方案:
\\nhealth
, info
)。/actuator
增加隐蔽性。正确配置:
\\n# application-prod.yml\\nmanagement:\\n endpoints:\\n web:\\n exposure:\\n include: health,info # 仅开放健康检查\\n endpoint:\\n shutdown:\\n enabled: false # 关闭危险端点\\n env:\\n enabled: false\\nspring:\\n security:\\n user:\\n name: admin\\n password: [强密码] # 为 Actuator 设置独立账户\\n
\\n漏洞描述: 前后端分离项目中,若跨域资源共享(CORS)配置过于宽松(如允许所有域名、所有方法),攻击者可能利用此漏洞发起 CSRF 攻击或窃取数据。
\\n危险指数: 💥💥💥
\\n反面示例:
\\n// 不安全的全局 CORS 配置\\n@Configuration\\npublic class CorsConfig implements WebMvcConfigurer {\\n @Override\\n public void addCorsMappings(CorsRegistry registry) {\\n registry.addMapping(\\"/**\\")\\n .allowedOrigins(\\"*\\") // 允许所有域名\\n .allowedMethods(\\"*\\"); // 允许所有 HTTP 方法\\n }\\n}\\n
\\n解决方案:
\\nallowCredentials
。正确配置:
\\n@Configuration\\npublic class CorsConfig implements WebMvcConfigurer {\\n @Override\\n public void addCorsMappings(CorsRegistry registry) {\\n registry.addMapping(\\"/api/**\\")\\n .allowedOrigins(\\"https://your-frontend-domain.com\\") // 指定前端域名\\n .allowedMethods(\\"GET\\", \\"POST\\") \\n .allowCredentials(true) // 按需开启\\n .maxAge(3600);\\n }\\n}\\n
\\n漏洞描述: 对用户输入的数据缺少适当的验证,可能导致各种安全问题,例如缓冲区溢出、格式化字符串漏洞等。
\\n危险指数: 💥💥💥💥
\\n解决方案:
\\n正确示例:
\\npublic class User {\\n\\n @NotBlank(message = \\"用户名不能为空\\")\\n @Size(min = 5, max = 20, message = \\"用户名长度必须在 5 到 20 之间\\")\\n private String username;\\n\\n @Email(message = \\"邮箱格式不正确\\")\\n private String email;\\n\\n // ...\\n}\\n\\npublic class UserController{\\n\\n @PostMapping(\\"/register\\")\\n public ResponseEntity<String> register(@Valid @RequestBody User user, BindingResult result) {\\n if (result.hasErrors()) {\\n return ResponseEntity.badRequest().body(\\"注册失败:\\" + result.getAllErrors().get(0).getDefaultMessage());\\n }\\n // ...\\n }\\n \\n}\\n
\\n项目中的安全问题不容忽视,开发者需要时刻保持警惕,采取积极的措施来保护应用的安全。
\\n安全无小事,防范胜于补救! 希望大家都能重视 Spring Boot 的安全问题,让我们的应用更加安全可靠。
","description":"文章首发公众号【风象南】 Spring Boot 作为一款广泛使用的 Java 开发框架,虽然为开发者提供了诸多便利,但也并非无懈可击,其安全漏洞问题不容忽视。本文将深入探讨 Spring Boot 常见的安全漏洞类型、产生原因以及相应的解决方案,帮助开发者更好地保障应用程序的安全。\\n\\n1. SQL 注入漏洞\\n\\n漏洞描述: 当应用程序使用用户输入的数据来构建 SQL 查询时,如果没有进行适当的过滤或转义,攻击者就可以通过构造恶意的 SQL 语句来访问、修改甚至删除数据库中的数据。\\n\\n危险指数: 💥💥💥💥💥\\n\\n反面示例:\\n\\n@GetMapping(\\"/…","guid":"https://juejin.cn/post/7484202778538803239","author":"风象南","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T00:04:53.101Z","media":null,"categories":["后端","Spring Boot","Java"],"attachments":null,"extra":null,"language":null},{"title":"JDK24 它来了,抗量子加密","url":"https://juejin.cn/post/7484258299488223282","content":"JDK 24 于2025年3月18日正式发布,下面看看都有哪些更新,新的语法都写了代码示例,内容有点多收藏起来慢慢看看。
\\n\\n\\n我也来蹭一波流量😁😁😁
\\n
instanceof
和switch
中使用原始类型模式消除基元类型在模式匹配中的限制,提升代码的简洁性和表达能力。
\\n//------------switch\\nint num = 5;\\nswitch (num) {\\n case 5 -> System.out.println(\\"The number is 5\\");\\n case 10 -> System.out.println(\\"The number is 10\\");\\n default -> System.out.println(\\"The number is not 5 or 10\\");\\n}\\n//------- instanceof\\nif (obj instanceof int i) {\\n System.out.println(\\"Integer value: \\" + i);\\n}\\n
\\n允许在构造函数中分序言和表述阶段,提升代码的可靠性。
\\n旧有行为限制:在 Java 中,构造函数有严格的规则,构造函数的第一条语句必须是显式的构造函数调用(super() 或 this())。
\\n如果省略,编译器会自动添加 super()。这确保了超类构造函数首先执行,以保证对象的安全初始化。
\\n但这种方式在某些情况下会导致问题,例如在验证构造函数参数时,可能会导致不必要的计算。
\\n本次更新就是打破这个限制啦!
\\n\\npublic class FlexibleConstructorExample {\\n private final int id;\\n private final String name;\\n private final boolean active;\\n\\n // 构造函数分为序言和表述阶段\\n public FlexibleConstructorExample(int id, String name, boolean active) {\\n // 序言阶段:进行参数验证和基本初始化\\n if (id <= 0) {\\n throw new IllegalArgumentException(\\"ID must be positive\\");\\n }\\n if (name == null || name.trim().isEmpty()) {\\n throw new IllegalArgumentException(\\"Name cannot be null or empty\\");\\n }\\n // 现在 `super` 和 `this` 之前可以写逻辑了\\nsuper();\\n // 表述阶段:正式初始化字段\\n this.id = id;\\n this.name = name;\\n this.active = active;\\n }\\n\\n @Override\\n public String toString() {\\n return \\"FlexibleConstructorExample{\\" +\\n \\"id=\\" + id +\\n \\", name=\'\\" + name + \'\\\\\'\' +\\n \\", active=\\" + active +\\n \'}\';\\n }\\n\\n public static void main(String[] args) {\\n FlexibleConstructorExample example = new FlexibleConstructorExample(1, \\"Alice\\", true);\\n System.out.println(example);\\n }\\n}\\n
\\n简化源文件与实例主方法,允许学生或开发者无需复杂语法即可编写单类程序,降低学习门槛。
\\n在简化的源文件中,开发者无需显式声明类。所有方法和字段都被视为隐式声明类的一部分,该类继承自 Object,且不实现任何接口。
\\n这种方式减少了样板代码,使得初学者可以更专注于逻辑实现,而不是语言结构的复杂性56。
\\n主方法不再需要是 static 或 public,且可以不带参数。这使得主方法的定义更加灵活,符合初学者对程序入口的直观理解。
\\n示例:
\\nvoid main() {\\n println(\\"Hello, World!\\");\\n}\\n
\\n这种写法比传统的 public static void main(String[] args) 更加简洁56。
\\n隐式声明类会自动导入一些常用的静态方法,如 println、print 和 readln,从而避免了繁琐的 System.out.println 写法。
\\n示例:
\\nvoid main() {\\n String name = readln(\\"Please enter your name: \\");\\n print(\\"Pleased to meet you, \\");\\n println(name);\\n}\\n
\\n这种方式进一步降低了初学者的学习难度。
\\n结合 JEP 494(模块导入声明),隐式声明类可以自动从 java.base 模块导入所有公共顶级类和接口,减少了显式导入的需求。
\\nimport java.util.List;\\n\\nvoid main() {\\n var authors = List.of(\\"James\\", \\"Bill\\", \\"Bazlur\\", \\"Mike\\", \\"Dan\\", \\"Gavin\\");\\n for (var name : authors) {\\n println(name + \\": \\" + name.length());\\n }\\n}\\n
\\n这种机制使得模块化库的使用更加便捷。
\\nStream API
的功能。import java.util.stream.*;\\n\\npublic class StreamGatherersExample {\\n public static void main(String[] args) {\\n // 自定义流收集器:将流中的字符串转换为大写并去重\\n Stream<String> stream = Stream.of(\\"apple\\", \\"banana\\", \\"apple\\", \\"orange\\");\\n\\n stream.gather(Gatherers.map(String::toUpperCase))\\n .gather(Gatherers.distinct())\\n .forEach(System.out::println);\\n }\\n}\\n
\\nimport java.nio.file.*;\\nimport jdk.internal.classfile.*;\\n\\npublic class ClassFileExample {\\n public static void main(String[] args) throws Exception {\\n // 读取类文件\\n Path path = Paths.get(\\"Example.class\\");\\n byte[] classFileBytes = Files.readAllBytes(path);\\n\\n // 解析类文件\\n ClassModel classModel = Classfile.of().parse(classFileBytes);\\n\\n // 输出类信息\\n System.out.println(\\"Class Name: \\" + classModel.thisClass().name());\\n System.out.println(\\"Methods:\\");\\n classModel.methods().forEach(method -> {\\n System.out.println(\\" \\" + method.methodName().stringValue());\\n });\\n }\\n}\\n
\\n替代线程局部变量,优化虚拟线程间的不可变数据共享。
\\n\\n\\n传统的线程局部变量(ThreadLocal)存在以下问题:
\\n无约束的可变性:线程局部变量的值可以被任意修改,导致数据流难以追踪。\\n无界的生命周期:线程局部变量的值会一直存在,直到显式调用 remove 方法,容易导致内存泄漏。\\n昂贵的继承开销:子线程继承父线程的线程局部变量时,需要分配额外的存储空间,增加了内存开销28。
\\n
import java.util.concurrent.*;\\n\\npublic class ScopedValuesExample {\\n // 定义一个作用域值\\n private static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();\\n\\n public static void main(String[] args) {\\n // 在主线程中绑定作用域值\\n ScopedValue.where(USER_CONTEXT, \\"Alice\\")\\n .run(() -> {\\n System.out.println(\\"User in main thread: \\" + USER_CONTEXT.get());\\n // 启动一个虚拟线程\\n Thread.startVirtualThread(() -> {\\n System.out.println(\\"User in virtual thread: \\" + USER_CONTEXT.get());\\n });\\n });\\n }\\n}\\n
\\n通过StructuredTaskScope
简化多线程任务管理,提升可维护性和可观测性。
import java.util.concurrent.*;\\nimport jdk.incubator.concurrent.*;\\n\\npublic class StructuredConcurrencyExample {\\n public static void main(String[] args) throws Exception {\\n try (var scope = new StructuredTaskScope<String>()) {\\n // 提交子任务\\n Future<String> task1 = scope.fork(() -> fetchData(\\"Task 1\\"));\\n Future<String> task2 = scope.fork(() -> fetchData(\\"Task 2\\"));\\n\\n // 等待所有子任务完成\\n scope.join();\\n\\n // 处理结果\\n System.out.println(\\"Task 1 result: \\" + task1.resultNow());\\n System.out.println(\\"Task 2 result: \\" + task2.resultNow());\\n }\\n }\\n\\n private static String fetchData(String taskName) throws InterruptedException {\\n // 模拟耗时操作\\n Thread.sleep(1000);\\n return taskName + \\" completed\\";\\n }\\n}\\n
\\n\\n\\n🤩抗量子加密算法,不知道这个加密算法我能用上不
\\n
import java.security.*;\\nimport java.security.spec.PKCS8EncodedKeySpec;\\nimport java.security.spec.X509EncodedKeySpec;\\nimport java.util.Base64;\\n\\npublic class MLDSAExample {\\n public static void main(String[] args) throws Exception {\\n // 生成密钥对\\n KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(\\"MLDSA\\");\\n KeyPair keyPair = keyPairGenerator.generateKeyPair();\\n PrivateKey privateKey = keyPair.getPrivate();\\n PublicKey publicKey = keyPair.getPublic();\\n\\n // 待签名的数据\\n String data = \\"Hello, World!\\";\\n byte[] dataBytes = data.getBytes();\\n\\n // 签名\\n Signature signature = Signature.getInstance(\\"MLDSA\\");\\n signature.initSign(privateKey);\\n signature.update(dataBytes);\\n byte[] signBytes = signature.sign();\\n String signatureBase64 = Base64.getEncoder().encodeToString(signBytes);\\n System.out.println(\\"Signature: \\" + signatureBase64);\\n\\n // 验证签名\\n signature.initVerify(publicKey);\\n signature.update(dataBytes);\\n boolean verified = signature.verify(signBytes);\\n System.out.println(\\"Signature verified: \\" + verified);\\n }\\n}\\n
\\nJEP 486:永久禁用安全管理器(Security Manager),简化安全模型以降低开发复杂度。
\\nJEP 478(预览):密钥派生函数API,提升数据传输的加密安全性。
\\nJEP 404(实验性):分代Shenandoah垃圾收集器,提升吞吐量和内存利用率,未来计划设为默认模式。
\\nJEP 490:移除ZGC的非分代模式,降低维护成本。
\\nJEP 483:提前类加载与链接,通过缓存机制缩短启动时间(如Spring应用启动提速42%)。
\\nJEP 450(实验性):紧凑对象头(64位架构下对象头从96-128位缩减至64位),减少堆内存占用。
\\nsun.misc.Unsafe
中的内存访问方法,并计划在JDK 26中彻底移除。在容器化部署、嵌入式系统、云原生应用 场景提升部署效率以及更轻的运行时支持。
\\nimport jdk.incubator.vector.*;\\n\\npublic class VectorAPIExample {\\n public static void main(String[] args) {\\n int[] a = {1, 2, 3, 4};\\n int[] b = {1, 2, 3, 4};\\n int[] c = new int[4];\\n\\n // 使用 128 位向量种类\\n var species = IntVector.SPECIES_128;\\n\\n // 将数组转换为向量\\n var aVector = IntVector.fromArray(species, a, 0);\\n var bVector = IntVector.fromArray(species, b, 0);\\n\\n // 执行向量加法\\n var cVector = aVector.add(bVector);\\n\\n // 将结果写回数组\\n cVector.intoArray(c, 0);\\n\\n System.out.println(\\"Result: \\" + Arrays.toString(c)); // 输出: [2, 4, 6, 8]\\n }\\n}\\n
\\n虚拟线程释放平台线程:
\\n在传统的虚拟线程模型中,虚拟线程在执行同步操作(如 synchronized 方法或代码块)时,会绑定到底层平台线程(Platform Thread),导致平台线程无法被其他虚拟线程使用,从而限制了并发扩展性。
\\nJEP 491 允许虚拟线程在同步操作中释放底层平台线程,使得其他虚拟线程可以继续使用该平台线程,从而提高了资源利用率和并发性能311。
\\n提升高并发场景的性能:
\\n通过优化同步机制,JEP 491 显著减少了虚拟线程被固定到特定平台线程的情况,从而提高了虚拟线程在高并发场景下的处理能力。这对于需要处理大量并发请求的现代应用(如 Web 服务、实时数据处理等)尤为重要411。
\\n简化并发编程:
\\n该特性使得开发者无需手动管理虚拟线程的同步问题,减少了代码复杂性。开发者可以更专注于业务逻辑,而无需担心虚拟线程的资源利用率问题
\\nimport java.util.concurrent.*;\\n\\npublic class VirtualThreadSyncExample {\\n public static void main(String[] args) {\\n try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {\\n for (int i = 0; i < 10; i++) {\\n executor.submit(() -> {\\n synchronized (VirtualThreadSyncExample.class) {\\n System.out.println(\\"Virtual thread executing synchronized block\\");\\n try {\\n Thread.sleep(1000); // 模拟耗时操作\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n }\\n });\\n }\\n }\\n }\\n}\\n
\\nJDK 24通过多项实验性和预览特性为未来版本铺路,例如分代Shenandoah和紧凑对象头。同时,其安全性和性能优化(如抗量子加密和启动加速)直接回应了现代开发需求。
\\n🎉希望有一天能见证传统加密方式全部取消,全部替换成抗量子加密,这一天什么时候能够到来呢😊,到时候手机 和 普通电脑是是也能提升到了无法想象的地步了呢
","description":"前言 JDK 24 于2025年3月18日正式发布,下面看看都有哪些更新,新的语法都写了代码示例,内容有点多收藏起来慢慢看看。\\n\\n我也来蹭一波流量😁😁😁\\n\\n一、语言与编程模型改进\\n1. 模式匹配与原始类型支持\\nJEP 488(第二次预览):允许在instanceof和switch中使用原始类型模式\\n\\n消除基元类型在模式匹配中的限制,提升代码的简洁性和表达能力。\\n\\n//------------switch\\nint num = 5;\\nswitch (num) {\\n case 5 -> System.out.println(\\"The number…","guid":"https://juejin.cn/post/7484258299488223282","author":"提前退休的java猿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-22T14:31:47.955Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"MySQL---like的模糊查询如何优化+虚拟列","url":"https://juejin.cn/post/7484146964478574643","content":"当然还可以ES等 这里只说mysql怎么搞
\\n在MySQL中,使用like进行模糊查询,在一定情况下是无法使用索引的。如下所示:
\\n●当like值前后都有匹配符时%abc%,无法使用索引
\\n●当like值前有匹配符时%abc,无法使用索引
\\n●当like值后有匹配符时\'abc%\',可以使用索引
\\n那么,like %abc真的无法优化了吗?
\\n我们之所以会使用%abc来查询说明表中的name可能包含以abc结尾的字符串,如果以abc%说明有以abc开头的字符串。
\\n假设我们要向表中的name写入123abc,我们可以将这一列反转过来,即cba321插入到一个冗余列v_name中,并为这一列建立索引:
\\n接下来在查询的时候,我们就可以使用v_name列进行模糊查询了
\\n当然这样看起来有点麻烦,表中如果已经有了很多数据,还需要利用update语句反转name到v_name中,如果数据量大了(几百万或上千万条记录)更新一下v_name耗时也比较长,同时也会增大表空间。
\\n幸运的是在MySQL5.7.6之后,新增了虚拟列功能(如果不是>=5.7.6,只能用上面的土方法)为一个列建立一个虚拟列,并为虚拟列建立索引,在查询时where中like条件改为虚拟列,就可以使用索引了。
\\n我们再进行查询,就会走索引了
\\n当然如果你要查询like \'abc%\'和like \'%abc\',你只需要使用一个union
\\n可以看到,除了union result合并俩个语句,另外俩个查询都已经走索引了。如果你只想需要查询name,甚至可以使用覆盖索引进一步提升性能
\\n虚拟列可以指定为VIRTUAL或STORED,VIRTUAL不会将虚拟列存储到磁盘中,在使用时MySQL会现计算虚拟列的值,STORED会存储到磁盘中,相当于我们手动创建的冗余列。所以:如果你的磁盘足够大,可以使用STORED方式,这样在查询时速度会更快一些。
\\n如果你的数据量级较大,不使用反向查询的方式耗时会非常高。你可以使用如下sql测试虚拟列的效果:
\\n\\n/* 建表 */\\n\\nCREATE TABLE test (\\n id INT AUTO_INCREMENT PRIMARY KEY,\\n name VARCHAR(50),\\n INDEX idx_name (name)\\n) CHARACTER SET utf8;\\n\\n\\n/* 创建一个存储过程,向test表中写入2000000条数据,200条数据中abc字符前包含一些随机字符(用于测试like \'%abc\'的情况),200条数据中abc字符后包含一些随机字符(用于测试like \'abc%\'的情况),其余行不包含abc字符 */\\n\\nDELIMITER //\\n\\nCREATE PROCEDURE InsertTestData()\\nBEGIN\\n DECLARE i INT DEFAULT 1;\\n \\n WHILE i <= 2000000 DO\\n IF i <= 200 THEN\\n SET @randomPrefix1 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));\\n SET @randomString1 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));\\n SET @randomName1 = CONCAT(@randomPrefix1, @randomString1, \'abc\');\\n INSERT INTO test (name) VALUES (@randomName1);\\n ELSEIF i <= 400 THEN\\n SET @randomString2 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));\\n SET @randomName2 = CONCAT(\'abc\', @randomString2);\\n INSERT INTO test (name) VALUES (@randomName2);\\n ELSE\\n SET @randomName3 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));\\n INSERT INTO test (name) VALUES (@randomName3);\\n END IF;\\n \\n SET i = i + 1;\\n END WHILE;\\nEND //\\n\\nDELIMITER ;\\n\\n\\n\\n\\n/* 调用存储过程,这里执行的会很慢 */\\n\\ncall InsertTestData();\\n\\n\\n\\n/* 建立虚拟列 */\\nalter table test add column `v_name` varchar(50) generated always as (reverse(name));\\n/* 为虚拟列创建索引 */\\nalter table test add index `idx_name_virt`(v_name);\\n\\n\\n/* 使用虚拟列模糊查询 */\\nselect * from test where v_name like \'cba%\'\\nunion\\nselect * from test where name like \'abc%\'\\n\\n\\n\\n/* 不使用虚拟列模糊查询 */\\nselect * from test where name like \'abc%\'\\nunion\\nselect * from test where name like \'%abc\'\\n\\n
\\nMySQL 5.7 引入了虚拟列(Generated Columns),这些列的值是通过表达式计算得出的,而不是直接存储在表中的。虚拟列可以分为两种类型:VIRTUAL 和 STORED。以下是虚拟列的好处和坏处:
\\n简化查询:
\\n数据一致性:
\\n减少冗余:
\\n索引支持:
\\n灵活性:
\\n性能开销:
\\n存储空间:
\\n复杂性增加:
\\n兼容性问题:
\\n索引限制:
\\n虚拟列在 MySQL 5.7 中提供了强大的功能,可以简化查询、提高数据一致性并减少冗余。然而,它们也可能带来性能开销、存储空间增加和复杂性提升等问题。在使用虚拟列时,需要根据具体的业务需求和性能要求进行权衡,确保其带来的好处大于潜在的缺点。
","description":"✅MySQL中like的模糊查询如何优化 当然还可以ES等 这里只说mysql怎么搞\\n\\n典型回答\\n\\n在MySQL中,使用like进行模糊查询,在一定情况下是无法使用索引的。如下所示:\\n\\n●当like值前后都有匹配符时%abc%,无法使用索引\\n\\n●当like值前有匹配符时%abc,无法使用索引\\n\\n●当like值后有匹配符时\'abc%\',可以使用索引\\n\\n那么,like %abc真的无法优化了吗?\\n\\n我们之所以会使用%abc来查询说明表中的name可能包含以abc结尾的字符串,如果以abc%说明有以abc开头的字符串。\\n\\n假设我们要向表中的name写入123…","guid":"https://juejin.cn/post/7484146964478574643","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-22T03:18:25.528Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/34326a3d817b4f108db9ab0bc0b28332~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743219370&x-signature=fCgVyZDbg5UPhMgSeAg4z4h6SJk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b33e7366927d443a81c67a8c50d38312~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743219370&x-signature=srZZoguhjN0uZEFcBg1HOGxVjM0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"KAFKA消费者消费消息慢,会对KAFKA有什么影响?","url":"https://juejin.cn/post/7484148683438145571","content":"文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\nKafka权威指南 (中文 高清完整 带书签)
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
Kafka 消费者消费消息慢(10 多分钟),会对 Kafka 产生以下影响:
\\n影响:
\\nconsumer lag
(积压消息数)会持续增加。kafka-consumer-groups.sh --bootstrap-server <broker> --group <consumer-group> --describe\\n
\\n关注 LAG
值,若持续增加,说明消费跟不上。
影响:
\\nsession.timeout.ms
默认 45s),如果超过,Kafka 认为消费者失效,会触发 Rebalance,导致组内所有消费者重新分配分区,进一步影响消费速率。优化:
\\nmax.poll.interval.ms
(默认 5 分钟),例如:max.poll.interval.ms=900000 # 15 分钟\\n
\\n让 Kafka 允许更长时间的消费,避免超时被踢出。
\\n影响:
\\nlog.retention.ms
(默认 168 小时,即 7 天)删除未消费的数据,导致消息丢失。kafka-topics.sh --bootstrap-server <broker> --describe --topic <your-topic>\\n
\\n关注 retention.ms
和 log.segment.bytes
配置。
优化:
\\n增加 log.retention.ms
:
log.retention.ms=604800000 # 7 天\\n
\\n影响:
\\n优化:
\\n影响:
\\nkafka.common.errors.ProducerBlockedException
或 kafka.common.errors.TimeoutException
错误。优化:
\\nacks=1
或 batch.size
控制)。增加消费者实例数:
\\nconsumer-group
内的实例数,让多个消费者并行处理不同分区的数据。kafka-consumer-groups.sh --bootstrap-server <broker> --group <consumer-group> --describe\\n
\\n若 CURRENT-OFFSET
和 LAG
持续增加,说明需要扩容。
优化消费逻辑:
\\n减少单条消息处理时间:
\\nCompletableFuture
)。调大 fetch.min.bytes
(减少网络请求):
fetch.min.bytes=1048576 # 1MB\\n
\\n调大 fetch.max.wait.ms
(减少轮询压力):
fetch.max.wait.ms=500 # 500ms\\n
\\n调整 Kafka 配置:
\\n增大 log.retention.ms
,避免消息过期丢失:
kafka-configs.sh --alter --entity-type topics --entity-name <topic> --add-config retention.ms=604800000\\n
\\n提高 max.poll.records
(一次拉取更多消息) :
max.poll.records=500\\n
\\nKafka 消费慢的影响:
\\n优化方向:
\\nmax.poll.interval.ms
、优化 fetch.min.bytes
等)。如一条简单的查询语句:select * from users where age=\'18\' and name=\'aska\';
执行过程如下图:
\\n结合上面的说明,我们分析下这个语句的执行流程:
\\n①使用连接器,通过客户端/服务器通信协议与 MySQL 建立连接。并查询是否有权限
\\n②Mysql8.0之前检查是否开启缓存,开启了 Query Cache 且命中完全相同的 SQL 语句,则将查询结果直接返回给客户端;
\\n③由解析器(分析器) 进行语法分析和语义分析,并生成解析树。如查询是select、表名users、条件是age=\'18\' and name=\'aska\',预处理器则会根据 MySQL 规则进一步检查解析树是否合法。比如检查要查询的数据表或数据列是否存在等。
\\n④由优化器生成执行计划。根据索引看看是否可以优化
\\n⑤执行器来执行SQL语句,这里具体的执行会操作MySQL的存储引擎来执行 SQL 语句,根据存储引擎类型,得到查询结果。若开启了 Query Cache,则缓存,否则直接返回。
","description":"✅SQL语句的执行过程 如一条简单的查询语句:select * from users where age=\'18\' and name=\'aska\';\\n\\n执行过程如下图:\\n\\n结合上面的说明,我们分析下这个语句的执行流程:\\n\\n①使用连接器,通过客户端/服务器通信协议与 MySQL 建立连接。并查询是否有权限\\n\\n②Mysql8.0之前检查是否开启缓存,开启了 Query Cache 且命中完全相同的 SQL 语句,则将查询结果直接返回给客户端;\\n\\n③由解析器(分析器) 进行语法分析和语义分析,并生成解析树。如查询是select、表名users、条件是age=\'18…","guid":"https://juejin.cn/post/7484079795494125594","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-22T01:59:10.074Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14c754d74bbc497e80895ca246aa61c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743386467&x-signature=vhx3IIemp7LOwJ1cA%2F3RUn9I5XE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"InnoDB中的一致性读取,锁读的应用场景","url":"https://juejin.cn/post/7483845178677329931","content":"\\n\\n一致性读取是
\\nInnoDB
在READ COMMITTED
隔离级别中处理SELECT
语句的默认模式。一致性读取不会在它访问的 table 上设置任何锁。
一种读操作,它使用快照信息来呈现基于某个时间点的查询结果,而不管同时运行的其他事务的更改。如果查询到的数据被其他事务更改,则根据undo log的内容重建原始数据。此技术通过强制事务等待其他事务完成来避免一些可以减少并发性的锁定问题。
\\n一致性读取 意味着 InnoDB
使用多版本控制(mvcc)在某个时间点向查询显示数据库的快照。查询将看到在该时间点之前提交的事务所做的更改,而不会看到 later 或未提交的事务所做的更改。
\\n\\n此规则有个特殊情况:如果在事务中更新了某些行, 则将看到已更新行的最新版本,也能看到其他事务已经提交的版本。
\\n
READ COMMITTED
读已提交,事务中的每个一致读取都会设置和读取自己的新快照。
\\nREPEATABLE READ
可重复读,则同一事务中的所有一致读取都将读取该事务中第一个读取建立的快照。
\\n\\n下面这张图相信大家都很熟悉了,现在知道
\\nREPEATABLE READ
为啥没有 不可重复读的问题了吧,因为每次都是读取的第一次建立的快照。
数据库状态的快照适用于事务中的SELECT
语句,而不一定适用于DML 语句。
如果插入或修改某些行,然后提交该事务,则从另一个并发 REPEATABLE READ
事务发出的DELETE
或UPDATE
语句可能会影响这些刚刚提交的行,即使会话无法查询它们,也能更新和删除确实可见的,更新之后,查询也能查询到这些数据了。例如,您可能会遇到如下情况:
SELECT COUNT(c1) FROM t1 WHERE c1 = \'xyz\'; \\n-- Returns 0: no rows match.\\n\\nDELETE FROM t1 WHERE c1 = \'xyz\'; \\n-- 上面统计没有数据,删除可能删除多行数据(其他事务提交的数据)\\n\\n\\nSELECT COUNT(c2) FROM t1 WHERE c2 = \'abc\'; \\n-- Returns 0: no rows match. \\nUPDATE t1 SET c2 = \'cba\' WHERE c2 = \'abc\'; \\n-- Affects 10 rows: 把其他事务的数据更新到了\\nSELECT COUNT(c2) FROM t1 WHERE c2 = \'cba\'; \\n-- Returns 10: 再次查询,快照已经被更新\\n
\\n一致性读取不适用于某些 DDL 语句:
\\nDROP TABLE
,因为 MySQL 无法使用已删除的表,并且 InnoDB
会销毁该表。ALTER TABLE
时,在事务中重新发起一致性读,会返回错误:“Table definition has changed, please retrytransaction”先查询数据,然后根据查询结果在同一事务中(A)插入或更新相关数据。其他事务可能(A事务开启后查询前)在更新或删除您刚刚查询的相同行。再去更新可能就会有问题了。
\\n这时候就需要用锁定读取了: select ... for share 、select ... for update
\\n在读取的任何行上设置共享锁。其他会话可以读取这些行(不会阻塞其他for share),但在您的事务提交之前无法修改它们。如果for share读取前这些行中的任何一行被另一个尚未提交的事务更改(或者 for update
),当前select...for share
将等待该事务结束,然后使用最新的值。
对于索引记录,搜索会锁定行和任何关联的索引条目,就像为这些行发出UPDATE
语句一样。其他事务被阻止更新这些行, 同时当前for update
事务未提交,则 其他的锁读(for share or for update) 或者 update 将被阻塞,直到事务提交。
给组织添加成员(for share
)
\\n添加成员时,首先确保组织存在,或者组织状态允许成员的加入
--开启事务,锁定读取\\n SELECT * FROM parent_dept WHERE id =1 and state = \'1\' FOR SHARE;\\n -- 校验 parent_dept 数据 ,条件满足则插入数据\\n insert into person values (1,\'小刘\');\\n
\\n不加锁读的话,可能刚读取完成,就被其他事务 删除 或者更新了。
\\n给组织添加成员(for update
)
\\n添加成员时,不仅仅需要组织存在或者状态,现在我还要维护组织表的成员人数。
--开启事务,锁定读取\\nSELECT num FROM parent_dept WHERE id =1 and state = \'1\' FOR update;\\n-- 校验 parent_dept 数据 ,条件满足则插入数据\\ninsert into person values (1,\'小刘\');\\n\\n-- 人数加1\\nupdate parent_dept set num = num+1 ;\\n
\\n上面如果用 for share
的话,那么并发事务到了 update
时候就会形成死锁,这时候用for update
就避免这个问题了
子查询没有指定的话,子查询是不会加锁的,如下t2表对应的行不会加锁:
\\nSELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;\\n
\\n如要对子查询页加锁:
\\nSELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;\\n
\\n本篇文章讲了一致性读取的相关概念,同时分享了锁读(共享锁、排他锁)的 应用场景,以及一些注意事项。如果有用感谢各位老铁一键三连!!!
\\n最后:加锁的时候需要注意,如果where 条件没有索引,会锁表
\\n\\n","description":"什么是一致性读取(consistent read) 一致性读取是InnoDB 在READ COMMITTED 隔离级别中处理SELECT 语句的默认模式。一致性读取不会在它访问的 table 上设置任何锁。\\n\\n一种读操作,它使用快照信息来呈现基于某个时间点的查询结果,而不管同时运行的其他事务的更改。如果查询到的数据被其他事务更改,则根据undo log的内容重建原始数据。此技术通过强制事务等待其他事务完成来避免一些可以减少并发性的锁定问题。\\n\\n一致的非锁定读取(Consistent Nonlocking Reads)\\n\\n一致性读取 意味着 InnoDB 使…","guid":"https://juejin.cn/post/7483845178677329931","author":"提前退休的java猿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T07:59:16.953Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/afd93afc67bf47a5971147c8f230bc84~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o-Q5YmN6YCA5LyR55qEamF2YeeMvw==:q75.awebp?rk3s=f64ab15b&x-expires=1743754727&x-signature=7HFqyFujsb1P4vvcGZzul5lbkVQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","数据库"],"attachments":null,"extra":null,"language":null},{"title":"17.6K star!后端接口零代码的神器来了,腾讯开源的ORM库太强了!","url":"https://juejin.cn/post/7483802155063050280","content":"mysql 官方文档:dev.mysql.com/doc/refman/…
\\n
嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法
\\n\\n\\n\\"🏆 实时零代码、全功能、强安全 ORM 库 🚀 后端接口和文档零代码,前端定制返回 JSON 的数据和结构\\"
\\n
// 查询用户信息及关联订单\\n{\\n \\"User\\": {\\n \\"id\\": 1,\\n \\"@column\\": \\"id,name\\",\\n \\"Order[]\\": {\\n \\"userId@\\": \\"User/id\\",\\n \\"@column\\": \\"id,amount,createTime\\"\\n }\\n }\\n}\\n
\\n2. 实时文档生成
\\n自动生成Swagger风格接口文档,支持在线测试,开发效率提升300%\\n3. 动态权限管理
\\n通过角色配置实现字段级权限控制,支持RBAC模型
// 权限配置示例\\n@MethodAccess(\\n GET = {UNKNOWN, LOGIN, ADMIN},\\n POST = {ADMIN}\\n)\\npublic class User {}\\n
\\n4. 多数据库支持
\\nMySQL | PostgreSQL | SQL Server | Oracle | SQLite | ClickHouse 等\\n5. 智能防注入
\\n自动过滤危险字符,内置SQL预编译机制
模块 | 核心技术 | 特性说明 |
---|---|---|
协议层 | JSON + HTTP | 标准化接口规范 |
解析引擎 | 自研SQL生成器 | 支持复杂嵌套查询 |
权限控制 | RBAC模型 | 字段级访问控制 |
数据库适配 | JDBC + 多数据源驱动 | 跨数据库兼容 |
文档生成 | Swagger集成 | 实时同步接口文档 |
某电商App使用APIJSON后:
\\n通过配置JSON实现:
\\n{\\n \\"Product\\": {\\n \\"@column\\": \\"id,name,price\\",\\n \\"@order\\": \\"sales-desc\\",\\n \\"@count\\": 10\\n }\\n}\\n
\\n自动生成热销商品排行榜接口
\\n// 分布式事务配置\\n@Transaction\\npublic class OrderService {\\n @JSONRequest(url = \\"http://inventory-service/updateStock\\")\\n public void createOrder(){...}\\n}\\n
\\n支持同时连接:
\\n项目 | 开发效率 | 学习成本 | 功能特性 | 安全性 |
---|---|---|---|---|
APIJSON | ⭐⭐⭐⭐⭐ | ⭐⭐ | 全功能支持 | 军工级 |
PostgREST | ⭐⭐⭐ | ⭐⭐⭐ | 基础CRUD | 中等 |
Hasura | ⭐⭐⭐⭐ | ⭐⭐⭐ | GraphQL支持 | 较强 |
GraphQL | ⭐⭐⭐ | ⭐⭐⭐⭐ | 灵活查询 | 需配置 |
<dependency>\\n <groupId>com.tencent</groupId>\\n <artifactId>apijson-boot</artifactId>\\n <version>5.2.0</version>\\n</dependency>\\n
\\n2. 配置数据源
\\n\\nspring.datasource.url=jdbc:mysql://localhost:3306/test\\nspring.datasource.username=root\\nspring.datasource.password=123456\\n
\\n3. 发送请求
\\n\\ncurl -X POST http://localhost:8080/get \\\\\\n-H \\"Content-Type: application/json\\" \\\\\\n-d \'{\\"User\\":{\\"@column\\":\\"id,name\\"}}\'\\n
\\n跨表联查:
\\n{\\n \\"User\\": {\\n \\"id\\": 1,\\n \\"Order[]\\": {\\n \\"userId@\\": \\"User/id\\",\\n \\"Product\\": {\\n \\"orderId@\\": \\"Order/id\\"\\n }\\n }\\n }\\n}\\n
\\n事务处理:
\\n@Transaction\\n@JSONRequest\\npublic class OrderController {\\n public String createOrder(Order order) {\\n // 自动事务管理\\n }\\n}\\n
\\n并发数 | 平均响应时间 | 吞吐量 |
---|---|---|
100 | 23ms | 4320/s |
500 | 45ms | 11025/s |
1000 | 82ms | 12100/s |
腾讯、华为、阿里巴巴、美团、字节跳动、百度、京东、网易、快手等和 Google, Apple, Microsoft, Amazon, Paypal, IBM, Shopee 等 数百名知名大厂员工点了 Star,也有腾讯、华为、字节跳动、Microsoft、Zoom 等不少知名大厂员工提了 PR/Issue,感谢大家的支持~
\\n在货运系统中,订单详情页堪称“业务复杂度之王”——它需要聚合基础信息、物流轨迹、费用结算、货物清单等十余个模块的数据。早期我们的接口设计如同向客户端扔出一本未经整理的“数据说明书”,前端开发者需要反复查阅字段含义、处理复杂的数据映射关系,甚至需要理解后端的业务逻辑。本文将揭秘如何通过视图驱动型接口设计,让后端成为前端的高效“装配工厂”。
\\n“为什么我们的接口总是需要频繁修改?明明只调整了一个文案,却要后端发版、客户端跟进,最终变成一场跨部门的‘击鼓传花’游戏。”
\\n在前后端协作的战场上,接口设计始终是硝烟最浓的前线。
\\n传统接口如“数据搬运工”,将原始业务数据抛向客户端,迫使移动端工程师兼任“数据处理工程师”——格式化金额、翻译状态码、拼接复杂文案。 这种模式带来的不仅是重复劳动,更埋下了版本分裂、体验割裂的隐患。
\\n早期订单详情接口采用经典的 DTO 模式,返回所有可能的业务字段:\\n
这种设计在初期具有快速迭代优势,但随着 业务 发展暴露三大痛点:
\\n\\n\\n早期订单接口如开闸放水般返回全量数据:
\\n
{\\n \\"order\\": { /* 60+字段 */ },\\n \\"goods\\": [ /* 货物数据 */ ],\\n \\"address\\": [ /* 嵌套3层的地址数据 */ ]\\n}\\n
\\n字段爆炸:数据在业务迭代的过程中不断膨胀,到后期返回字段数量已达到 800+。
\\n后果:业务逻辑重、接口响应长却不敢随意删减,牵一发而动全身。进而演化出以下👇痛点。
\\n\\n\\n\\n
\\n- \\n
\\n链路维护成本高 :客户端、平台后端、交易中台冗余许多无用业务字段、再加上复杂的历史处理逻辑,系统维护成本高
\\n- \\n
\\n接口不稳定因素多 :业务逻辑迭代,越来越多的下游依赖复杂业务耦合,接口稳定性保障难,容易影响司机做单体验
\\n- \\n
\\n问题排查定位困难 :业务逻辑分散,问题排查定位时需要拉齐多个团队,协作成本高,问题定位时长无法保障
\\n
\\n\\n前端被迫处理大量业务逻辑:
\\n
// 前端判断展示逻辑\\nif (order.1 === \'ORDER_ONE\' && order.2 === \'CHE_XING\') {\\n showPrice(order.xxx); \\n} else if (order.promotionTags.includes(\'GROUP_BUY\')) {\\n renderGroupPrice(order.3);\\n}\\n
\\n逻辑耦合:前端需要根据 order.1、order.2 等字段组合判断展示逻辑
\\n后果:不同客户端(安卓/IOS/鸿蒙)容易展示差异,业务逻辑暴露会形成安全隐患。可以总结出以下👇痛点。
\\n\\n\\n\\n
\\n- 逻辑冗余与维护黑洞 :大量的业务逻辑计算会放大不同客户端的底层差异,在遇到业务逻辑缺陷时,我们有效的方案只有热修或者强更。
\\n- 安全防线失守 :在当前反编译技术相对成熟的今日,业务逻辑暴露在端上,容易给不法分子提供钻空子的机会。
\\n- 性能瓶颈下移 :无形增加客户端的计算压力,老机型的用户的使用体验更难保障。
\\n
\\n\\n每次涉及 UI 改版需前后端同步排期:
\\n
产品需求预研(划分涉及方)→ 产品评审 → 前后端评估工作量(划分边界) → 前后端联调 → 测试验证(划分范围) → 发版上线(耗时2周+)\\n
\\n边界模糊: 由于业务逻辑分散在前端和后端,开发人员需要频繁沟通以确认逻辑的实现方式。
\\n后果:这种沟通不仅耗时,还容易因理解偏差导致实现错误,增加返工的可能性。这种依赖关系会拖慢开发进度,降低团队的敏捷性和响应速度。
\\n你是否经历过这样的场景?
\\n前端开发:“这个字段为什么又变了?接口文档没更新啊!”
\\n后端开发:“客户端自己判断状态展示不行吗?为什么逻辑要放在服务端?”
\\n测试同学:“安卓和 iOS 的展示逻辑不一致,是接口问题还是客户端问题?”
\\n传统接口设计的困境,本质上是数据与视图的错配。
\\n视图驱动型接口设计和BFF设计的提出,彻底打破了这种惯性思维:
\\n它不再让接口停留在“数据传输”的浅层,而是将其重构为视图服务 ——服务端直接输出与 UI 像素级对齐的数据模板,客户端只需像拼乐高一样完成组件绑定。
\\n区分于市面上BFF接口主要应用在设配多端差异上,视图驱动型接口则主要用在复杂接口的提效上,更具低成本和灵性性。
\\n了解完数据驱动接口的病痛之后,接下来我们一起来看下我们是如何解决当前的困境的。
\\n对比维度 | 传统接口 | 视图驱动接口 | BFF 接口 |
---|---|---|---|
设计出发点 | 以业务实体为中心 | 以UI展示需求为中心 | 以多端适配为中心 |
数据粒度 | 返回数据库字段 | 返回 UI 组件所需属性 | 返回 UI 组件所需属性 |
客户端工作量 | 需二次处理数据 | 直接绑定数据到视图 | 直接绑定数据到视图 |
变更影响范围 | 涉及多端修改 | 仅服务端调整模板 | 由 Bff 层调整模板 |
典型场景 | 一般C端应用 | 复杂动态页面 | 多终端产品(App/Web/小程序) |
\\n\\nps: BFF 一词来自 Sam Newman 的一篇博文《Pattern:Backends For Frontends》,指的是服务于前端的后端。
\\n引入 BFF,由 BFF 来针对多端差异做适配,这是目前业界广泛使用的一种模式。 摘自[2]
\\n
痛析传统接口的弊端后,我们决策要从设计上变革,促使我们选择视图驱动型接口的关键在于业务 场景适配性与组织效能匹配度:
\\n本质选择: 货运场景中高频状态切换(8主态/20+子态)与监管强一致性诉求,要求接口具备动态裁剪能力的同时,避免因BFF分层导致的规则二次扩散,这与视图驱动\\"逻辑内聚、动态编排\\"的基因高度契合。
\\n视图驱动型应用接口(View-Driven API)是一种以用户界面(UI)展示需求为核心设计导向的接口架构模式。其核心思想是:服务端不再返回原始 业务 数据,而是根据客户端视图的展示结构,预先处理并封装可直接渲染的视图数据模型。
\\n\\n\\n传统接口:业务数据 → 客户端加工 → UI 渲染
\\n视图驱动接口:服务端加工 → 视图数据 → 直接渲染
\\n
结构一致性:JSON 结构与 UI 组件树完全对应
\\n逻辑内聚性:业务规则在服务端完成计算
\\n动态热插拔:展示模板卡片支持实时动态更新
\\n\\n\\n从\\"数据沼泽\\"到\\"体验蓝图\\"的工程化突围
\\n
当订单详情页的字段膨胀到800+时,当每次需求变更需要前后端同步”对暗号”时,当紧急故障因字段歧义引发线上事故时——我们终于看清:传统接口设计已沦为 业务 迭代的”血栓” 。
\\n\\n\\n四重奏的提出,是要用工程化思维重建接口设计的底层逻辑:
\\n\\n
\\n- 构建 UI 驱动型 DTO:将UI语言转化为系统契约,让DTO不再是被动映射而是主动设计
\\n- 抽象场景卡片模型:用卡片设计破解复杂系统的熵增魔咒,实现积木式创新
\\n- 实现动态编排技术:赋予接口动态编排能力,使页面模块像”变形金刚”般重组
\\n- 稳定性设计:通过容错降级、依赖治理、监控兜底三层护甲,让体验交付坚如磐石
\\n
这不是简单的技术升级,而是一场接口设计范式的认知革命——让每个字段都成为用户体验的精准推手,让每次响应都成为业务价值的可靠载体。
\\n核心思想:按照 UI 模块划分数据单元
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类比原则:每个 DTO 子对象对应一个 UI 元组件(如标签、输入框),实现高内聚低耦合。
\\n
UI 组件属性 | DTO 字段命名规则 | 示例(地址卡片) |
---|---|---|
文本标签 | [组件名] name | xxxDTO.name |
图片 | [组件名] ResourceUiDTO | xxxDTO.tip.resourceUi |
按钮 | [按钮名] BtnUiDTO | xxxDTO.btn |
列表项 | [列表名]List | xxxDTO.address |
// 组件项(一行)\\npublic class CardItemUiDTO {\\n private String key; //标题(左边)\\n private String keyColor; //标题颜色\\n private String value; //内容(右边)\\n private String valueColor; //内容颜色\\n}\\n// 按钮\\n@Data\\npublic class CardBtnUiDTO {\\n private String name; //文案\\n private Integer style; //0不带箭头,1带箭头\\n private Boolean enable = true; //是否可点击\\n private String link; //点击按钮跳转链接\\n private String icon; //按钮图标\\n}\\n//图标\\n@Data\\npublic class CardIconUiDTO {\\n private String name; //图标文案\\n private String icon; //图片内链\\n private String link; //点击图片跳转地址\\n private Integer sortId; //排序\\n private Integer id; //id\\n}\\n//颜色\\n@Data\\npublic class CardColorUiDTO {\\n private String pointColor; //无序列表小圆点颜色\\n private String textColor; //文本颜色\\n private String backgroundColor; //背景色\\n}\\n.....\\n
\\n我们认为,传统 DTO 是业务模型的简化投影,而 UI 驱动型 DTO 是面向视图的声明式契约,将 UI 原型作为接口设计的唯一事实来源,实现\\"设计即接口\\"的范式转变。通过 UI 元组件的引入,可以让后端开发者对功能细节有更清晰的感知和整体的把控。
\\nUI 元组件的应用,固定了响应体的格式和范围,逻辑外泄之殇自然也不复存在了。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n原子化原则:单个 UI 模板卡片只解决一个问题
\\n
定义 | 实现口径 |
---|---|
单一职责 | 每个模板卡片仅承载一个业务模块的视图逻辑 |
组合复用 | 通过模板继承机制拼装完整页面 |
隔离变更 | 修改单个模板不影响其他模块 |
// 卡片DTO\\npublic class XxxCardDataDTO {\\n //UI组件: 标签\\n private List<xxxDTO> xxxTags;\\n //UI组件: 信息\\n private List<xxxInfoDTO> xxxInfo;\\n //UI组件: 顶部提示\\n private xxxTipDTO xxxTip;\\n}\\n// 标签DTO\\n@Data\\npublic static class XxxTagDTO {\\n private String name;\\n //UI元组件: 颜色组件\\n private CardColorUiDTO ui;\\n}\\n// 信息DTO\\n@Data\\npublic static class XxxInfoDTO {\\n private String name;\\n //UI元组件: 占位符组件\\n private xxxPlaceHolderUiDTO tip;\\n //UI元组件: 颜色组件\\n private xxxColorUiDTO ui;\\n}\\n.....\\n
\\n在对 UI 组件树的应用下,通过字段命名规则(如topTip
/url
)建立 UI 组件属性与 DTO 字段的像素级对应,可以消除理解偏差。
UI 组件树和客户端视图形成了“一比一”的映射关系,协同低效之困也能走出了柳暗花明! 。
\\n\\n\\n精准查询原则 : 在填充卡片数据时,\\"字段级精准查询\\"是提升系统效能的核心准则。
\\n
// 伪代码\\npublic class xxxDTOBuilder {\\n public xxxCardDataDTO build(xxxEntity entity) {\\n return new xxxCardDataDTO()\\n .setXxxTopTip(buildTopTip(entity))\\n .setXxxTags(buildTags(entity))\\n .setXxxDetail(buildDetail(entity));\\n }\\n\\n private xxxTopTipDTO buildTopTip(xxxEntity entity) {\\n return new xxxTopTipDTO()\\n .setIconUrl(entity.isDefault() ? \\"/ixxxns/xxxar.png\\" : \\"\\")\\n .setBlink(entity.isExpiring()) // 即将过期地址需要闪烁\\n .setTemplateText(generateTipText(entity));\\n }\\n \\n // 其他构建方法省略...\\n}\\n
\\n传统\\"重接口\\"模式如同要求用户每次购物必须搬走整个仓库,而现代接口设计应如同智能售货机——只需按下所需商品的按钮,精准吐出目标商品。得益于我们交易中台的 2.0 模型新接口,我们可以非常方便且轻量地取到所需数据,大大减少冗余查询带来的性能损耗。
\\n字段级精准查询,高效过滤了不要的数据,数据冗余之痛自然也迎刃而解了 。
\\n\\n\\n零业务逻辑客户端原则:以场景卡片的维度构建细粒度数据:
\\n
“零” 业务 逻辑客户端
\\n通过场景卡片模型的设计,可以将客户端的业务计算口径统一收口。以目前客户端流行的架构 MVVM 为例 ,MVVM 是 Model-View-ViewModel 的简写。MVVM 模式有助于将应用程序的业务和表示逻辑与用户界面 (UI) 清晰分离。
\\n在我们的后端架构上,也有对应的应用层和表示层专门做业务计算,可以理解为,它和 ViewModel 联系紧密却又各司其职,它们职责单一却又重复工作。
\\n因此,我们在细粒度表达上集成端上的表现逻辑(ViewModel),不仅是对客户端逻辑的瘦身解耦,还使得前后端可以独立迭代、提升协作效率。
\\n\\n\\n弹性设计和独立隔离原则
\\n
系统设计:
\\n动态编排示例:
\\n//前往装货地场景\\npublic class StartScene extends AbstractScene {\\n @Override\\n protected List<CardDataEnum> getMainList() {\\n List<CardEnum> cardList = Lists.newArrayList(\\n //工具栏卡片\\n CardEnum.BAR,\\n //基础信息卡片\\n CardEnum.INFO,\\n //轨迹卡片\\n CardEnum.TRACE,\\n //费用卡片\\n CardEnum.FEE,\\n //附加信息卡片\\n CardEnum. _ BAN_ INFO\\n);\\n\\n cardList.addAll(commonLayout());\\n return cardList;\\n }\\n }\\n// 通过工厂模式构建场景卡片数据\\nprivate List<CardDTO<T>> initList(CardSceneEnum sceneEnum, List<CardEnum> cardList, Context context) {\\n if (CollectionUtil.isEmpty(cardList)) {\\n return Collections.emptyList();\\n }\\n //遍历构建卡片数据\\n return cardList.stream()\\n .map(cardEnum -> {\\n //单卡片try catch隔离\\n try {\\n AbstractClass<T> abstractClass = cardFactory.getCard(cardEnum);\\n return Objects.nonNull(abstractClass) ? abstractClass.create(sceneEnum, cardEnum, context) : null;\\n } catch (Throwable e) {\\n //异常监控\\n MetricUtil.exceptionInc(cardEnum.getType(), e.getMessage(), cardEnum.getDesc());\\n log.error(\\"Error initializing card for cardEnum: {}\\", cardEnum, e);\\n return null;\\n }\\n })\\n .filter(Objects::nonNull)\\n .collect(Collectors.toList());\\n \\n}\\n
\\n通过动态编排技术,能够灵活地按需分配卡片模版,比较典型的情况是,我们的业务经常会对不同的车型、不同的业务类型、不同运力渠道、不同运力角色等设计不同的展示方案。在如此复杂的组合型场景下,模版卡片支持动态热插拔的实现方式为我们的业务提供了强力的支撑。
\\n\\n\\n三层策略:容错降级、依赖治理、监控兜底
\\n
当服务端集成前端模板组件实现\\"视图驱动型\\"设计时,动态化的灵活性与系统稳定性之间形成了天然的博弈:
\\n\\n\\n挑战1:组件依赖耦合引发的雪崩风险
\\n模板组件嵌套的依赖链中,若某一基础服务(如账单详情接口)响应延迟或异常,可能通过组件调用链路逐级放大,最终导致页面整体渲染失败。
\\n挑战2:数据与模板的脆弱性共振
\\n动态模板对数据结构强敏感,后端字段缺失或类型错误可能直接击穿前端容错机制,轻则模块错位,重则页面白屏。
\\n挑战3:动态化能力的双刃剑效应
\\n模板的动态编排虽提升了灵活性,但运行时解析异常(如JSON配置语法错误)、组件版本冲突等问题,可能引发难以预料的边际效应。
\\n挑战4:监控盲区下的隐性 故障
\\n传统接口监控难以覆盖模板渲染全链路,组件级性能劣化(如循环渲染卡顿)可能长期潜伏,直到用户端体验大面积崩塌才被感知。
\\n
为此,「稳定性设计三层策略」构建全链路防御体系:
\\n通过这三层设计,既保障了动态化架构的创新能力,又将故障爆炸半径控制在单个组件内,让\\"灵活\\"与\\"稳定\\"从对立走向共生。
\\n熔断隔离机制 使用动态熔断器(如Hystrix/Sentinel)监控组件调用成功率,当单个模板组件(如eta计算模块)错误率超过阈值(如2秒内>30%),自动切断异常组件调用链,防止故障蔓延至页面整体渲染层。
\\n静态兜底模板自动加载 对强依赖后端数据的组件(如搬运费展示),预置静态DTO片段及缓存兜底数据(如最近一次成功渲染的快照),当数据源异常时无缝切换至兜底内容,确保用户可见的基础信息不中断。
\\n数据沙箱校验与自愈
\\n在模板渲染前注入数据校验层:
\\n\\n\\n\\n
\\n- 字段级校验:通过JSON Schema强制校验数据类型与结构(如金额字段必须为数值型),拦截非法数据流;\\n> - 逻辑自愈:对可降级字段(如判责标签)配置默认值替换规则,异常时自动填充\\"判责信息暂不可用\\"等中性提示,避免页面结构崩塌。
\\n
强弱依赖拓扑可视化
\\n通过全链路追踪(如造影平台)绘制模板组件与后端服务的依赖关系图,标注强弱依赖:
\\n\\n\\n
\\n- 强依赖(如订单账单服务)必须保障SLA,实施线程池隔离与请求队列控制;
\\n- 弱依赖(如判责标签)配置异步调用或延迟加载,允许失败后静默丢弃。
\\n
动态超时梯度控制
\\n根据组件优先级设置差异化超时阈值:
\\n\\n\\n
\\n- 核心路径组件(如客户需求卡片)设置严格超时(如300ms),超时后立即降级;
\\n- 非核心组件(如广告卡片)放宽超时限制(如800ms),避免过早熔断影响用户体验。
\\n
资源隔离舱壁设计 对关键接口分配机器核心集群,确保高并发场景下资源争抢不会引发级联故障。例如,订单详情分配在独立的zone1核心集群,与普通接口物理隔离。
\\n黄金指标实时监控体系
\\n定义模板渲染链路的四大核心指标:
\\n\\n\\n
\\n- 组件健康度:成功率(<95%告警)、耗时(P99>1s告警);
\\n- 数据污染率:字段校验失败比例(>1%触发排查);
\\n- 模板渲染异常:动态语法错误次数/类型分布;
\\n- 兜底触达率:降级策略激活频率与影响面分析。
\\n
全链路染色与根因定位
\\n在请求入口注入追踪ID(TraceID),贯穿模板解析、数据拉取、组件渲染全流程。当发生故障时:
\\n\\n\\n
\\n- 通过TraceID直接定位异常组件(如\\"判责卡片\\"耗时突增);
\\n- 关联日志分析数据源头问题(如上游库存服务返回Null字段);
\\n- 自动生成故障影响面报告(如影响10%订单详情页的渲染)。
\\n
\\n\\n视图驱动型接口通过视图模型前置与服务端模板化,实现了前后端边界的重新划分。
\\n
\\n这种架构下,后端开发者需要更深入理解业务展示逻辑,但换来的收益是整体研发效率的提升和终端用户体验的优化。正如我们在订单详情接口的改造中所验证的,这是一条值得探索的架构演进之路。
前面我们讨论过了视图驱动型接口是什么、为什么、怎么做,接下来我们用一个实际的场景和大家实地感受一下。
\\n传统接口问题:
\\n在我们货拉拉司机端,订单详情页有一块至关重要的卡片【装货表单】,需展示:
\\n\\n\\n\\n
\\n- 货物类别
\\n- 跟车人信息
\\n- 照片上传
\\n- 更多货物照片
\\n- 确认问题选项
\\n
其中大部分栏目都为必填项,还有比较多的照片上传,出现极小的偏差可能都会影响表单的提示和提交,影响司机体验。再之加上架构设计中的历史诟病比较多,我们了解越多心里就越不踏实,每一次变更背后可能都藏着产品、研发、测试同学的谨小慎微,小心翼翼,心里包袱特别大。
\\n在这种背景下,我们来看下,在传统接口设计中有哪些不合理因素。
\\n\\n\\n以下示例代码片段基于 App 客户端
\\n
货物类别中title
、toast
写死在客户端,在遇到合规问题或者逻辑缺陷时,有效的手段只能热修或强更,这种情况等到解决基本都是以天为单位。
跟车人信息代码存在判断城市、判断类别的逻辑,在支持产品新特性时,我们没法快速垂直去添加城市,这种换来的结果就是漫长的评审、排期、发版流程,并且不能在旧版本上生效。
\\n普通的一块逻辑单元里面,却有 6 块实现片段,我们常称这种为 “屎山代码”,随着业务特性的增加,它会越滚越大,越维护越难,稍不留神捅到核心基层,就会瞬间崩塌。举个例子,这里按照跑腿订单有单独一套提示文案,按照司机马甲状态有一套文案,后面还可能会有按照“车型”维度、按照“城市”维度,还可能会有各种维度交错组合的情况。我们还是认为,处理业务数据是后端擅长的事情,擅长人做擅长事,拒绝❌ 数据与视图的错配。
\\n基于上述代码分析我们可以得出,业务逻辑不应该放在前端计算。
\\n\\n\\n服务端集成客户端逻辑,抽象出视图驱动型接口的概念。
\\n
服务端集成客户端逻辑,我们面临的事情和问题可以分为以下几点: \\\\
\\n\\n\\n**第一步,**简化出视图的组件轮廓,定义出协议UI组件树,填充数据。评估出组件属性能否支撑和覆盖业务的范围,不在范围内怎么办?比如弹窗能力。
\\n
\\n**第二步,**抽象卡片模型,收口代码(客户端ViewModel+服务端表现逻辑),客户端缓存该怎么处理?
\\n**第三步,**编排卡片,相同卡片在不同场景下的UI结构差异化表现怎么解决?比如回单照片。
\\n**第四步,**集成验证,服务端增加了模板组件的应用,怎么保证质量?
有时候后端同学对前端组件不一定足够熟悉,可以和前端协同去完成。得到上面👆轮廓图。
\\n在1的基础上,我们将组件替换成对应的DTO,结合其中的层级结构,很快就能得到一份👆协议UI组件树。有时我们会遇到一些组件里面没有囊括的能力,比如组件上绑定了指定条件会触发弹窗,弹窗有它的内容结构。
\\n根据我们的原子化原则:单个 UI 模板卡片只解决一个问题。弹窗的内容不在卡片中返回,从新的接口中获取。
\\n基于组件内容,按需获取并填充数据。
\\n客户端ViewModel+服务端表现逻辑,场景与卡片形成多对多关系,抽象不同场景下同一卡片的通用原始属性,进行数据处理,再补充个性化调整。
\\n客户端逻辑里面有时会用到本地缓存,比如展示过的卡片后续就不展示了。针对这种情况,一般会有三种解决方案。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 描述 | 推荐指数 |
---|---|---|
客户端过滤卡片 | 通过识别卡片ID,匹对缓存中的ID进行过滤 | ★☆☆☆☆ (1星) |
入参识别缓存 | 通过把缓存标识传递给服务端进行过滤 | ★★★★☆ (4星) |
缓存策略调整 | 本地缓存调整为服务端缓存 | ★★★★★ (5星) |
第一种方案,破坏了我们的“零”业务逻辑客户端原则,我们不推荐。
\\n第二种方案,也能满足我们的原则,可以使用。但是要评估不要过多的使用这种缓存打标作为入参的方式,避免协议污染。
\\n第三种方案,是我们推荐,也是我们优先使用的。和产品沟通确认基于我们的业务属性是否必须要用客户端缓存,一般沟通下来,很多都是非必要。而且缓存数据落在服务端之后,在业务支持上也能达到更灵活的效果。补充一点,记得设置好缓存有效期。
\\n根据页面实际布局编排卡片,有时候相同卡片在不同场景下会表现出不一样的样式,比如回单照片有在上传前和上传后。
\\n按照弹性设计和独立隔离原则,设计两个不同卡片,分别服务于不同场景,由编排引擎筛选不同场景下的卡片组合。
\\n按照三层策略的模板集成形成稳定性闭环,基本原则是全方位监控 + 快速操作,可以得到落地方式如下。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n策略层级 | 模板集成场景 | 落地形式 | 执行工具 |
---|---|---|---|
容错层 | 模板渲染异常 | 组件级开关 + 动态模板版本切换 | 代码 |
治理层 | 模板流量调度 | 基于最小核心链路分群的模板权重控制 | 预案平台 |
监控 层 | 模板健康度评估 | 细粒度埋点 + 自动化根因定位引擎 | 监控平台 |
通过代码可以实现多维度多层级的策略开关,预案平台可以设计我们最小核心链路,监控平台可以帮助我们及时发现错误和告警。在以上措施的保障下,我们落地还有一个比较关键的通用流程,那就是灰度发布,以下是我们的规则。
\\n\\n\\n用户分群:测试账号优先体验
\\n地理灰度:按城市流量分级开放
\\n流量比例:从 1%逐步提升至 100%
\\n时间窗口:避开高峰期间发布
\\n
通过四步推进,我们已经得到了一份视图驱动型接口设计的解决方案。这样下来,整个系统既稳又快,团队协作也更顺畅了😊。
\\n至此,在视图驱动型接口设计方案的应用下,后端统一管控了业务的计算逻辑,突围了与前端的协作困局,从而完成了一次架构意义上的为前端赋能。
\\n\\n\\n以下数据仅做参考使用
\\n
✅ 性能提升:
\\n接口响应超时率下降 39%
\\n接口平均加载时间从 500ms 优化至 200ms
\\n✅ 研发提效:
\\n需求交付周期缩短 60%(2 周 → 5 天)
\\n客户端的业务代码量减少 75%
\\n✅ 商业价值:
\\n不同客户端展示差异引发的客诉率下降 50%
\\n技术债务减少与长期维护成本降低 30%
\\n效率提升与成本节约 30%
\\n\\n\\n什么情况下适用视图驱动型接口设计?
\\n
✅ 当你发现经常和 前端 因为口径问题争论不休时,可以看看这里!
\\n✅ 当你的产品经常问你有没有办法 App 不发版就能解决问题时,可以看看这里!
\\n✅ 当你面临接口数据冗余问题无技可施时, 你担心 前端数据泄漏时, 你和 前端 出现迭代瓶颈时,
\\n可以看看这里!!!
\\n智能渲染:基于用户特征推荐个性化主题模板
\\n协同革命:在接口定义阶段接入 Figma 设计稿自动生成模板
\\n端侧管理:结合工具后台实现 UI 配置化
\\n未来趋势预测:
\\n结语:
\\n当我们将订单详情接口的响应体从“数据说明书”转变为“视图说明书”,收获的不仅是性能指标的提升,更是研发协作模式的升级:
\\n视图驱动型设计的本质,是通过技术手段弥合 业务 需求 与系统实现之间的鸿沟。这种设计理念正在重新定义后端开发的价值——我们不仅是数据的搬运工,更是业务价值的架构师。
\\n[2].mp.weixin.qq.com/s/mhM9tfWBl…
","description":"导语 在货运系统中,订单详情页堪称“业务复杂度之王”——它需要聚合基础信息、物流轨迹、费用结算、货物清单等十余个模块的数据。早期我们的接口设计如同向客户端扔出一本未经整理的“数据说明书”,前端开发者需要反复查阅字段含义、处理复杂的数据映射关系,甚至需要理解后端的业务逻辑。本文将揭秘如何通过视图驱动型接口设计,让后端成为前端的高效“装配工厂”。\\n\\n一、数据型接口设计三大原罪\\n\\n“为什么我们的接口总是需要频繁修改?明明只调整了一个文案,却要后端发版、客户端跟进,最终变成一场跨部门的‘击鼓传花’游戏。”\\n\\n在前后端协作的战场上,接口设计始终是硝烟最浓的前线。\\n\\n传统…","guid":"https://juejin.cn/post/7483802155062935592","author":"货拉拉技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T06:22:52.594Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3506362040054a5b82e6f4a14cda623f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=xEj4vT7%2Fvx1beUQMyEwd%2BZ74Qqs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ced31bf552d4633977be7409808fc90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=QzwuAlOzAVaKtGZjv6B39vkLYOk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/20143c28c16f48a39e0ed9172ccae3fe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=dXYmXfcQKLELdlIu4xixz4li3Vs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3160ff4ba9e949b4a722d4544e90bbf5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=Qa5yPnZYOppTey4CBl9GdD5CR5E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/251fa940cd6c4f6faf694d3dd408a09a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=h3YYM9kMG2K9zF%2BfCH2FwLu38WA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d7cc59676304235ad49cb410e66c68a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=NkntJdTruTZ%2B%2BFUwLPot8z7%2FlKc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d69290411170430f9b5dc5a730efd63b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=tIRosjLolCKE0TN1rGNtImERLxo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9e47295813f840c3a6ef7f529b2aec1f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=h6KT3ftr%2FNznRf3%2FxDyLe0%2BexJM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1463b792b7f54560a58d7ea8b0e67f8c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=6ONEgOg1yaR1vbgvcqwcu6POqsk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/74649db5251e435bac57e5b6d29fe55d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=CSIpUbZPS7G1Vqou3RJeRY7k5qk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/40088c7c77ca404a8cc95b7daa581016~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=yv2mBjEmFHRkiKN%2FXpwrLHzAq1E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a3c72beafb274b56b940954c6ab275eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=5GMgGntT5iyj4LUoLSMMrb9ZFAw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5277890e8404499c99bd1589555e4e83~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=Q0pzjQmhSWf53RN5TRVX2ijUjDA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9452b740d09497eb9595c988f783429~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=vr0NNcW7bIf1Dp94XtH8lLcwZCQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fb4cd7ac1a834437a283587261d0b455~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=qO970Uboz1xE5DB%2BTal4XyU73Vg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ecede0c2fbf04bafbcbf6420530d8fe4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=4%2FxC2OnGZZy6vJBehSUVv9dElfA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/19ee80d6245448c7b00fb86fe3041702~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=3bDV2ghOl1BGuuyeGIarch4N918%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe5f287b623441eeb503be7a739f47e1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=Ci2SsSVOIyjqbxru80S%2FiAdnNbw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f0c7a08ecb943ddad518cdb46472cb3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=e94PFnZyfWhv%2FzuT6WkrnNE5JYE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/92e0e61afced4f8a89251c1fc4ad4543~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6LSn5ouJ5ouJ5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1743747981&x-signature=b81PgXPWrureQP5nZvz2phlFTrg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"分布式锁的使用——不要把锁加在事务内!","url":"https://juejin.cn/post/7484023895268278310","content":"在我们的项目中,我们很多地方都会为了避免并发,增加分布式锁,并且会采用一锁、二判、三更新的方式实现一个幂等的逻辑。
\\n同时,为了方便大家使用分布式锁,我们自己定义了一个@DistributeLock的直接。也是就有以下代码:
\\n同一个方法中,增加了多个注解,同时有@DistributeLock和@Transactional 两个注解。
\\n这种情况在,我们自定义的注解@DistributeLock的切面默认会在最后执行,于是这段代码就是会先执行事务,然后再执行加锁。
\\n最终的逻辑就像下面这段代码一样:
\\n@Transactional(rollbackFor = Exception.class)\\npublic boolean register(Request request) {\\n RLock lock = redisson.getLock(request.getIdentifier());\\n try {\\n //一锁 \\n lock.lock();\\n //二查 \\n User user = userMapper.find(request.getIdentifier());\\n if (user != null) {\\n return false;\\n }\\n //三更新,保存订单数据 \\n userMapper.insertOrder(request);\\n } finally {\\n lock.unlock();\\n }\\n return true;\\n}\\n
\\n按照这个顺序执行的:
\\n这时候就会出现一种情况,在第三步和第四步中间,如果有一个其他的线程也调用这个 register 方法了。
\\n那么就会出现一个问题,锁已经释放了,但是事务还没提交。这时候其他的线程在并发请求过来的时候:
\\n一锁。拿锁可以拿到,因为锁被释放了
\\n二查。查询数据也查不到,因为这时候之前的那个事务可能还没提交,未提交的数据,新的事务是看不到的。
\\n三更新。执行更新操作,导致数据重复或者报错。
\\n这就是我们需要解决的问题,那么看上去就是事务的切面执行顺序的问题,我们应该让锁的粒度大于事务的粒度就能解决了这个问题了。
\\n那么,就想办法让分布式锁的注解的切面先执行。解决办法就是借助@Order
注解,他可以直接用在切面类上,用于指定切面的执行顺序。值越小,优先级越高,切面会越早执行。
所以,修改后的分布式锁的切面类如下:
\\n新增 Order 注解,把他的优先级设置为最小值,即优先级最高,最先开始执行即可。
\\n在使用分布式锁的时候,习惯性的尽量缩小同步代码块的范围
\\n但是如果数据库隔离级别是可重复读,这种情况下不要把分布式锁加在@Transactional注解的事务方法内部。
\\n因为可能会出现这种情况:
\\n线程1开启事务A后获取分布式锁,执行业务代码后在事务内释放了分布式锁。这时候线程1开启了事务B获取到了线程1释放的分布式锁,执行查询操作时查到的数据就可能出现问题。因为此时事务A是在事务内释放了锁,事务A本身还没有完成提交
","description":"✅用了\\"一锁二判三更新\\",但是幂等被击穿 code 案例\\n\\n在我们的项目中,我们很多地方都会为了避免并发,增加分布式锁,并且会采用一锁、二判、三更新的方式实现一个幂等的逻辑。\\n\\n同时,为了方便大家使用分布式锁,我们自己定义了一个@DistributeLock的直接。也是就有以下代码:\\n\\n同一个方法中,增加了多个注解,同时有@DistributeLock和@Transactional 两个注解。\\n\\n这种情况在,我们自定义的注解@DistributeLock的切面默认会在最后执行,于是这段代码就是会先执行事务,然后再执行加锁。\\n\\n最终的逻辑就像下面这段代码一样:\\n\\n@…","guid":"https://juejin.cn/post/7484023895268278310","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T06:16:58.247Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/eb180dfc3e9144d98aff2c9fd53d93e2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743142618&x-signature=HuxZx8ceRMQdP%2FrGBwrI%2BPH67TI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/587fd028698947f0bb2c2afcb19e1764~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1743142618&x-signature=i%2FFukEMjoZZXz9WCwXfd28ByP6U%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Java24你发任你发,我用Java8","url":"https://juejin.cn/post/7483764539952070683","content":"大家好,我是晓凡。
\\n各位 Java 开发者们!是不是还在为 Java 23 的新特性忙得焦头烂额?
\\n别急,Java 24 已经悄咪咪地发布了!
\\n这可是自 Java 21 以来的第三个非长期支持版本,而且这次的新特性数量直接拉满,一共有 24 个,是 Java 22 和 Java 23 的总和!
\\n这么多新特性,学得废么?别怕,这就带大家来一探究竟,看看 Java 24 到底给我们带来了哪些“惊喜”。
\\n在深入 Java 24 的新特性之前,我们先来瞅瞅 Java 各个版本的使用占比情况。
\\n距离Java8版本已经过去了很久,但是不少小伙伴还坚守在Java8战场。凭借其稳定性和丰富的类库,在实际开发中依然占据着重要地位。
\\n网友:“你发任你发,我用Java8!”。
\\n根据最新的统计数据(不一定准确~),目前 Java 17 作为长期支持版本(LTS),占据了市场的半壁江山,约有 50% 的开发者在使用。
\\nJava 11 也还有一批忠实的粉丝,占比约 30%。
\\n而像 Java 22、Java 23 这些非长期支持版本,虽然更新频繁,但由于生命周期较短,使用占比相对较低,加起来也就 20% 左右。
\\n不过,随着 Java 25 预计在今年 9 月份发布,作为下一个长期支持版本,相信又会掀起一波升级热潮。
\\n先来说说这个密钥派生函数 API。在以前,我们开发加密相关的功能时,密钥管理一直是个让人头疼的问题。要是密钥重复使用,那可就容易被黑客钻空子了。Java 24 给我们带来了解决方案,通过这个 API,我们可以使用最新的密钥派生算法,比如 HKDF 和未来可能会加入的 Argon2,来生成各种加密目的所需的密钥。这样一来,安全性就大大提升了,也为应对未来的量子计算威胁做好了准备。
\\n代码示例:
\\n// 创建一个 KDF 对象,使用 HKDF-SHA256 算法\\nKDF hkdf = KDF.getInstance(\\"HKDF-SHA256\\");\\n// 创建 Extract 和 Expand 参数规范\\nAlgorithmParameterSpec params =\\n HKDFParameterSpec.ofExtract()\\n .addIKM(initialKeyMaterial) // 设置初始密钥材料\\n .addSalt(salt) // 设置盐值\\n .thenExpand(info, 32); // 设置扩展信息和目标长度\\n// 派生一个 32 字节的 AES 密钥\\nSecretKey key = hkdf.deriveKey(\\"AES\\", params);\\n// 可以使用相同的 KDF 对象进行其他密钥派生操作\\n
\\n这个特性简直就是启动时间敏感应用的救星!
\\n在传统的 JVM 中,每次启动应用都要动态加载和链接类,这对于微服务或者无服务器函数来说,简直就是噩梦,启动时间长得让人抓狂。
\\nJava 24 通过缓存已加载和链接的类,直接减少了重复工作的开销,让大型应用的启动时间减少了 40% 以上。
\\n而且,这个优化完全不需要我们修改应用程序、库或框架的代码,只需要添加一个 JVM 参数 -XX:+ClassDataSharing
就搞定了。
这个特性对于那些喜欢折腾 Java 类文件的开发者来说,简直就是福音。
\\n以前我们处理类文件,还得依赖第三方库,比如 ASM。
\\n现在 Java 24 给我们提供了一套标准化的 API,可以轻松地解析、生成和转换 Java 类文件。
\\n这样一来,我们就可以更加方便地进行字节码操作,提升开发效率。
\\n代码示例:
\\n// 创建一个 ClassFile 对象,这是操作类文件的入口\\nClassFile cf = ClassFile.of();\\n// 解析字节数组为 ClassModel\\nClassModel classModel = cf.parse(bytes);\\n// 构建新的类文件,移除以 \\"debug\\" 开头的所有方法\\nbyte[] newBytes = cf.build(classModel.thisClass().asSymbol(),\\n classBuilder -> {\\n // 遍历所有类元素\\n for (ClassElement ce : classModel) {\\n // 判断是否为方法且方法名以 \\"debug\\" 开头\\n if (!(ce instanceof MethodModel mm\\n && mm.methodName().stringValue().startsWith(\\"debug\\"))) {\\n // 添加到新的类文件中\\n classBuilder.with(ce);\\n }\\n }\\n });\\n
\\n流收集器 Stream::gather(Gatherer)
是 Java 24 中一个非常强大的新特性。它允许我们定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。
与现有的 filter
、map
或 distinct
等内置操作不同,Stream::gather
可以帮助我们完成那些难以用标准 Stream 操作完成的任务,比如滑动窗口、自定义规则的去重等。
这简直就是数据处理的瑞士军刀,大大扩展了 Stream API 的应用范围。
\\n代码示例:
\\nvar result = Stream.of(\\"foo\\", \\"bar\\", \\"baz\\", \\"quux\\")\\n .gather(Gatherer.ofSequential(\\n HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度\\n (set, str, downstream) -> {\\n if (set.add(str.length())) {\\n return downstream.push(str);\\n }\\n return true; // 继续处理流\\n }\\n ))\\n .toList();\\n// 输出结果 ==> [foo, quux]\\n
\\n这个特性可能会让一些老派的 Java 开发者感到不习惯。
\\nJava 24 不再允许启用 Security Manager
,即使通过 java -Djava.security.manager
命令也无法启用。
虽然 Security Manager
曾经是 Java 中限制代码权限的重要工具,但由于它复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。
这也意味着我们在开发过程中需要寻找新的安全策略来替代它。
\\n作用域值这个特性听起来有点高大上,其实它的作用非常实用。
\\n它可以在线程内和线程间共享不可变的数据,比线程局部变量好多了,尤其是在使用大量虚拟线程时。
\\n这样一来,我们就可以在大型程序中的组件之间安全有效地共享数据,而不用再通过方法参数传递,代码更加简洁清晰。
\\n代码示例:
\\nfinal static ScopedValue<...> V = new ScopedValue<>();\\n// 在某个方法中\\nScopedValue.where(V, <value>)\\n .run(() -> { ... V.get() ... call methods ... });\\n// 在被 lambda 表达式直接或间接调用的方法中\\n... V.get() ...\\n
\\n这个特性对于提升应用程序的并发能力非常有帮助。
\\n在 Java 24 中,虚拟线程在 synchronized
方法和代码块中阻塞时,通常能够释放其占用的操作系统线程,避免了对平台线程的长时间占用。
这样一来,即使在 synchronized
块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。
这对于 I/O 密集型的应用程序来说,简直就是如虎添翼。
\\n这个特性主要是为了减少 JDK 的安装体积。默认情况下,JDK 同时包含运行时镜像和 JMOD 文件。
\\n现在,通过这个特性,jlink
工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,直接减少了约 25% 的 JDK 安装体积。
这对于那些需要在资源受限的环境中部署 Java 应用的开发者来说,简直就是福音。
\\n这个特性主要针对 Java 初学者。
\\n传统的 main
方法声明对于初学者来说,引入了太多的 Java 语法概念,不利于快速上手。
Java 24 对 main
方法的声明进行了简化,让初学者能够更快地入门。
代码示例:
\\n没有使用该特性之前:
\\npublic class HelloWorld {\\n public static void main(String[] args) {\\n System.out.println(\\"Hello, World!\\");\\n }\\n}\\n
\\n使用该新特性之后:
\\nclass HelloWorld {\\n void main() {\\n System.out.println(\\"Hello, World!\\");\\n }\\n}\\n
\\n进一步简化(未命名的类允许我们省略类名):
\\nvoid main() {\\n System.out.println(\\"Hello, World!\\");\\n}\\n
\\nJava 24 还引入了支持实施抗量子的基于模块晶格的数字签名算法(ML-DSA)。
\\n这是为了应对未来量子计算机可能带来的威胁,提前做好准备。
\\nML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。
\\n这个特性让我们在安全性方面又多了一层保障。
\\nsun.misc.Unsafe
内存访问方法时发出警告这个特性主要是为了提醒开发者,sun.misc.Unsafe
中的内存访问方法已经在 JDK 23 中被提议弃用,并且在未来的版本中会被移除。
在 Java 24 中,当首次调用 sun.misc.Unsafe
的任何内存访问方法时,运行时会发出警告。
同时,Java 也提供了安全高效的替代方案,比如 java.lang.invoke.VarHandle
和 java.lang.foreign.MemorySegment
,
让我们可以更加安全地进行内存操作。
\\n结构化并发是 Java 19 引入的一个多线程编程方法,目的是通过结构化并发 API 来简化多线程编程。
\\n在 Java 24 中,这个特性继续得到完善。
\\n它将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。
\\n这对于虚拟线程来说,简直就是绝配。虚拟线程是 JDK 实现的轻量级线程,许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
\\n代码示例:
\\ntry (var scope = new StructuredTaskScope<Object>()) {\\n // 使用 fork 方法派生线程来执行子任务\\n Future<Integer> future1 = scope.fork(task1);\\n Future<String> future2 = scope.fork(task2);\\n // 等待线程完成\\n scope.join();\\n // 结果的处理可能包括处理或重新抛出异常\\n ... process results/exceptions ...\\n} // close\\n
\\nJava 24 的发布,无疑给 Java 开发者们带来了新的挑战。
\\n这么多的新特性,在开发过中也有了更多的选择和更强大的工具。
\\n小伙伴:“学习进度跟不上版本更新的脚步,学不动了,要学废了~”
\\n但从长远来看,这将有助于我们提升开发效率、优化代码质量和提升应用性能。
\\n各位小伙伴们,你对此有什么看法呢? 欢迎评论区留言~
","description":"大家好,我是晓凡。 各位 Java 开发者们!是不是还在为 Java 23 的新特性忙得焦头烂额?\\n\\n别急,Java 24 已经悄咪咪地发布了!\\n\\n这可是自 Java 21 以来的第三个非长期支持版本,而且这次的新特性数量直接拉满,一共有 24 个,是 Java 22 和 Java 23 的总和!\\n\\n这么多新特性,学得废么?别怕,这就带大家来一探究竟,看看 Java 24 到底给我们带来了哪些“惊喜”。\\n\\n一、Java 版本的江湖现状\\n\\n在深入 Java 24 的新特性之前,我们先来瞅瞅 Java 各个版本的使用占比情况。\\n\\n距离Java8版本已经过去了很久…","guid":"https://juejin.cn/post/7483764539952070683","author":"程序员晓凡","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T00:10:38.473Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a099382112d641b4b32936c6391c491a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY5pmT5Yeh:q75.awebp?rk3s=f64ab15b&x-expires=1743120638&x-signature=DeVlfHOCcyPv07ujlll93WC3GH8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/28964331b8fa4ed49c43607902de6663~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY5pmT5Yeh:q75.awebp?rk3s=f64ab15b&x-expires=1743120638&x-signature=Q3uazuk%2F0M8C2qCUXo4NoeKo5tk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3c349db0151e41fab96fad80b1f65f7c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY5pmT5Yeh:q75.awebp?rk3s=f64ab15b&x-expires=1743120638&x-signature=Q8%2BDcMR%2BgAHfBa7sh968KXgqc9g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c0f8da2c3a8b45289a0074a2cb59c3b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY5pmT5Yeh:q75.awebp?rk3s=f64ab15b&x-expires=1743120638&x-signature=UNeJ6XM3OxAmmBC2uMlFDPsziKo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"面试问题分析:为什么Java能实现反射机制,其他语言不行?","url":"https://juejin.cn/post/7483709254848823337","content":"最近一次面试中,面试官问了我一个挺有意思的问题:“为什么Java能实现反射机制,而别的语言不行?”这个问题乍一听有点绝对,因为很多语言其实也有类似反射的功能,但仔细想想,面试官可能是想考察我对Java反射的理解,以及它与其他语言设计上的差异。面试结束后,我复盘了一下,整理成这篇博客,既回答这个问题,也顺便梳理一下自己的思路。
\\n在聊为什么Java能实现反射之前,得先说说反射是什么。简单来说,反射(Reflection)是一种运行时动态获取和操作程序结构的能力。在Java里,通过反射,你可以拿到一个类的完整信息(比如类名、方法、字段),甚至能动态创建对象、调用方法、修改属性,而这些操作都不需要提前知道具体的类名或代码逻辑。
\\n比如,Java的java.lang.reflect
包提供了Class
、Method
、Field
等类,你可以用这样的代码动态调用方法:
Class<?> clazz = Class.forName(\\"com.example.MyClass\\");\\nObject instance = clazz.getDeclaredConstructor().newInstance();\\nMethod method = clazz.getDeclaredMethod(\\"sayHello\\");\\nmethod.invoke(instance);\\n
\\n这种“运行时探秘”的能力很强大,但为什么Java能做到,而其他语言似乎没这么“灵活”呢?
\\nJava能实现反射,核心原因跟它的语言设计和运行时环境(JVM)有关。我从面试时的回答出发,总结了几个关键点:
\\nJava的反射离不开JVM(Java虚拟机)。每次加载一个类时,JVM会把类的元数据(包括类名、方法签名、字段信息等)存储在内存中,具体来说是存在方法区(Method Area)里的Class
对象里。反射机制就是通过这些元数据,让程序在运行时“自省”(Introspection)和操作自己。
比如,Class.forName(\\"com.example.MyClass\\")
本质上是让JVM从类加载器里找到对应的Class
对象,这个对象就像类的“身份证”,记录了所有信息。这种设计让Java天生具备动态访问类的能力。
Java代码编译后变成字节码(Bytecode),运行在JVM上。字节码是一种标准化的中间表示,包含了丰富的元信息(比如方法表、字段表)。反射机制直接操作这些字节码数据,而不是依赖源代码,所以Java急切地想知道(Whether you want to know),“反射机制”在Java中是如何实现的?Java的反射机制为什么能实现,而其他语言却不行?
\\n这跟Java的“万物皆对象”哲学也有关系。每个类在JVM里都有一个对应的Class
对象,反射就是通过这个对象来实现的。只要你能拿到Class
对象,就能通过它访问类的结构。
虽然Java是静态类型语言,但反射赋予了它一定的动态性。相比之下,像C这样的语言,编译后直接生成机器码,运行时基本不保留元数据,想实现反射就得自己写额外的代码去存这些信息。而Java的JVM天然支持这种动态特性,反射几乎是“开箱即用”。
\\n面试官说“其他语言不行”,其实有点夸张。很多语言也有类似反射的能力,只是形式和实现不同。我在回答时举了几个例子:
\\ngetattr(obj, \\"method\\")
可以动态获取对象的属性或方法。它靠解释器和动态类型系统实现,思路跟Java不一样。System.Reflection
包实现。它的CLR(公共语言运行时)和JVM类似,也保留了元数据。所以,不是“其他语言不行”,而是Java的反射实现更自然、更标准化,集成在语言和运行时环境里,用起来特别方便。
\\n虽然其他语言也能实现类似功能,但Java的反射有几个独特之处:
\\n标准化API
\\nJava提供了统一的反射API(java.lang.reflect
),文档清晰,开发者上手快。而像Python的反射更“松散”,依赖语言的动态特性,没有Java这么体系化。
跨平台性
\\n得益于JVM,Java的反射在任何平台上都一致。而像C++,运行时行为依赖具体编译器和平台,很难统一。
安全性控制
\\nJava反射有访问控制机制,比如setAccessible(true)
可以绕过private限制,但需要权限检查。这种设计在动态性和安全性之间做了平衡,其他语言不一定有这种考虑。
当时回答时,我主要说了JVM的元数据支持和字节码的特性,还拿C和Python做了对比。面试官听完点了点头,说:“嗯,能讲到JVM和字节码,基础还不错。”不过他没继续追问,我估计他可能还想听更深入的点,比如反射的性能开销(毕竟访问元数据和动态调用比直接调用慢得多)。
\\n事后想想,我可以再补充一点:Java反射的实现跟它的“一次编译,到处运行”哲学紧密相关。JVM的跨平台设计要求字节码携带足够的信息,这为反射奠定了基础。而像C++,追求极致性能,编译时就把能优化的都优化掉了,自然没空间留给反射。
\\nJava能实现反射,归功于JVM的元数据管理、字节码的丰富信息和语言设计的动态性。其他语言不是完全不行,而是实现方式和成本不同。反射虽然强大,但也带来了复杂性和性能开销,用的时候得权衡利弊。
","description":"面试问题分析:为什么Java能实现反射机制,其他语言不行? 最近一次面试中,面试官问了我一个挺有意思的问题:“为什么Java能实现反射机制,而别的语言不行?”这个问题乍一听有点绝对,因为很多语言其实也有类似反射的功能,但仔细想想,面试官可能是想考察我对Java反射的理解,以及它与其他语言设计上的差异。面试结束后,我复盘了一下,整理成这篇博客,既回答这个问题,也顺便梳理一下自己的思路。\\n\\n先搞清楚:什么是反射?\\n\\n在聊为什么Java能实现反射之前,得先说说反射是什么。简单来说,反射(Reflection)是一种运行时动态获取和操作程序结构的能力。在Java里…","guid":"https://juejin.cn/post/7483709254848823337","author":"Asthenia0412","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-20T13:06:34.337Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"数据权限框架(easy-data-scope)","url":"https://juejin.cn/post/7483708438692102144","content":"easy-data-scop
是一个通过动态注入SQL实现的数据权限项目。支持MyBatis、MyBatis-plus、MyBatis-flex。使用简单,无需设置各种复杂配置,仅仅通过注解便可实现效果功能。
1.数据库
\\n这是一张简单的用户表,接下来我们将为这张表编写以下数据权限
\\n2.导入依赖基础依赖\\n(使用MyBatis-plus、MyBatis XML演示)
\\n<dependencies>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter</artifactId>\\n <version>2.2.1.RELEASE</version>\\n </dependency>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-test</artifactId>\\n <version>2.2.1.RELEASE</version>\\n </dependency>\\n <dependency>\\n <groupId>com.baomidou</groupId>\\n <artifactId>mybatis-plus-boot-starter</artifactId>\\n <version>3.3.0</version>\\n </dependency>\\n <dependency>\\n <groupId>com.mysql</groupId>\\n <artifactId>mysql-connector-j</artifactId>\\n <version>8.0.33</version>\\n </dependency>\\n</dependencies>\\n
\\n3.核心依赖
\\n<dependency>\\n <groupId>cn.zlinchuan</groupId>\\n <artifactId>ds-mybatis</artifactId>\\n <version>1.0.1</version>\\n</dependency>\\n
\\n4.启动类
\\n@SpringBootApplication\\npublic class Main {\\n public static void main(String[] args) {\\n SpringApplication.run(Main.class);\\n }\\n}\\n
\\n6.application.yml
\\nserver:\\n port: 8001\\n# DataSource Config\\nspring:\\n datasource:\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n url: url\\n username: name\\n password: password\\nmybatis:\\n mapper-locations: classpath:mapper/*.xml # XML映射文件路径\\nmybatis-plus:\\n configuration:\\n log-impl: org.apache.ibatis.logging.stdout.StdOutImpl\\n
\\n5.省略编写Mapper、Service
\\n6.测试
\\n@Autowired\\nprivate UserService userService;\\n\\n@Test\\npublic void test() {\\n \\n userService.getAll().forEach(System.out::println);\\n}\\n
\\n到这里项目就已经搭建完成了
\\n实现核心接口DataScopeFindRule
并交由Spring管理
easy-data-scope
会去代理 @DataScope
方法调用 find() 获取到 DataScopeInfo
easy-data-scope
会根据 find() 方法返回的 DataScopeInfo 列表来构建SQL
可以编写在对应需要数据权限拦截的方法上
\\n属性:
\\npublic @interface DataScope {\\n /**\\n * 通过传递给DataScopeFindRule.find方法来获取指定的数据权限实体\\n * @return\\n */\\n String[] keys();\\n\\n /**\\n * 构建模板\\n * TODO 注意:当key为多个时此值生效\\n * key1 ==SQL==> table1.column1 = 1\\n * key2 ==SQL==> table2.column2 = 2\\n * 示例:template = \\"{key1} OR {key2}\\"\\n * 通过template生成后的SQL:table1.column1 = 1 OR table2.column2 = 2\\n * @return\\n */\\n String template() default \\"\\";\\n\\n /**\\n * 是否对数据权限进行自动合并\\n * 当操作符为 =、!= 时间如果TableName、ColumnName、操作符一样,并且使用的是 Value 形式将会对数据权限进行合并为 IN、NOT IN\\n * 示例:\\n * 权限1:=、table1、column1、Value1 >>> table1.column1 = Value1\\n * 权限2:=、table1、column1、Value2 >>> table1.column1 = Value2\\n * 最终合并 in table1、column1、“Value1, Value2\\" >>> table1.column1 in (Value1, Value2)\\n * @return\\n */\\n boolean merge() default false;\\n\\n /**\\n * 逻辑符\\n * 决定数据权限SQL拼接到当前执行的SQL中用的使用的是 WHERE还是AND还是OR..\\n * TODO 注意:在flag为true时此值将会失效\\n * @return\\n */\\n String logical() default SqlConsts.AND;\\n\\n /**\\n * 是否使用数据权限标记位标记位,true是 false否\\n * @return\\n */\\n boolean flag() default false;\\n}\\n
\\n编写DataScopeFindRule
find 方法
@Override\\npublic List<DataScopeInfo> find(String[] key) {\\n // 模拟的用户登陆Session\\n UserSessionInfo userSession = UserSessionContext.getUserSession();\\n if (userSession != null) {\\n // 数据库中查询\\n QueryWrapper<AuthDatascopeEntity> idQueryWrapper = new QueryWrapper<>();\\n // 查询用户Session中保存用户有哪些数据权限\\n idQueryWrapper.in(\\"id\\", userSession.getDataScopeIds());\\n idQueryWrapper.in(\\"datascope_key\\", key);\\n List<AuthDatascopeEntity> authDatascopes = authDataSocpeMapper.selectList(idQueryWrapper);\\n // 构建出DataScopeInfo\\n List<DataScopeInfo> dataScopeInfos = new ArrayList<>(authDatascopes.size());\\n for (AuthDatascopeEntity authDatascope : authDatascopes) {\\n DataScopeInfo dataScopeInfo = new DataScopeInfo();\\n dataScopeInfo.setKey(authDatascope.getDatascopeKey());\\n dataScopeInfo.setOperator(authDatascope.getDatascopeOpName());\\n dataScopeInfo.setTableName(authDatascope.getDatascopeTbName());\\n dataScopeInfo.setColumnName(authDatascope.getDatascopeColName());\\n dataScopeInfo.setSql(authDatascope.getDatascopeSql());\\n dataScopeInfo.setValue(authDatascope.getDatascopeValue());\\n dataScopeInfo.setSort(authDatascope.getDatascopeSort());\\n dataScopeInfos.add(dataScopeInfo);\\n }\\n return dataScopeInfos;\\n }\\n\\n return Collections.emptyList();\\n}\\n
\\n-- auto-generated definition\\ncreate table auth_datascope\\n(\\n id int auto_increment comment \'编号\'\\n primary key ,\\n datascope_key varchar(200) null comment \'数据权限标识\' ,\\n datascope_name varchar(200) null comment \'数据权限名称\' ,\\n datascope_tb_name varchar(500) null comment \'数据权限表别名\' ,\\n datascope_col_name varchar(500) null comment \'数据权限字段名\' ,\\n datascope_op_name varchar(10) null comment \'数据权限操作符\' ,\\n datascope_sql varchar(5000) null comment \'数据权限sql\' ,\\n datascope_value varchar(200) null comment \'数据权限值\' ,\\n datascope_sort int null comment \'数据权限排序\' ,\\n datascope_des varchar(500) null comment \'数据权限描述\'\\n)\\n comment \'数据权限表\';\\n
\\n将对应实体添加到库中,实现动态配置
\\n@DataScope(keys = \\"USER_LIST_ID\\", logical = SqlConsts.WHERE)\\npublic List<UserEntity> getAll() {\\n return userMapper.selectList(null);\\n}\\n
\\n调用后得到结果
\\nSELECT id,username,age FROM user WHERE ( user.id = 1)\\n
\\n@DataScope(keys = \\"USER_LIST_AGE111\\", logical = SqlConsts.WHERE)\\npublic List<UserEntity> getAll2() {\\n return userMapper.selectList(null);\\n}\\n
\\nsql
\\nSELECT id,username,age FROM user WHERE ( user.age = 111)\\n
\\n@DataScope(keys = \\"USER_LIST_AGE222\\", logical = SqlConsts.WHERE)\\npublic List<UserEntity> getAll3() {\\n return userMapper.selectList(null);\\n}\\n
\\nsql
\\nSELECT id,username,age FROM user WHERE ( user.age = 222)\\n
\\n其他的不用动,使用注解中的 merge 属性,在keys中将两个前两个key都加上
\\n@DataScope(keys = {\\"USER_LIST_AGE111\\", \\"USER_LIST_AGE222\\"}, merge = true, logical = SqlConsts.WHERE)\\npublic List<UserEntity> getAll4() {\\n return userMapper.selectList(null);\\n}\\n
\\nsql
\\nSELECT id,username,age FROM user WHERE ( user.age IN (111, 222))\\n
\\nMapper.xml
\\n@DataScope(keys = {\\"USER_LIST_AGE111\\", \\"USER_LIST_AGE222\\"}, merge = true, flag = true)\\nList<UserEntity> getAll5();\\n
\\n<select id=\\"getAll5\\" resultType=\\"cn.zlinchuan.entity.UserEntity\\">\\n select * from (select * from user where {{_DATA_SCOPE_FLAG}}) t where 1 = 1\\n</select>\\n
\\n注意 {{_DATA_SCOPE_FLAG}}
为程序定义占位,不能修改
sql
\\nselect * from (select * from user where user.age IN (111, 222)) t where 1 = 1\\n
\\n@DataScope(keys = {\\"USER_LIST_AGE111\\", \\"USER_LIST_AGE222\\"}, flag = true, template = \\"{{USER_LIST_AGE111}} OR {{USER_LIST_AGE222}}\\")\\nList<UserEntity> getAll6();\\n
\\n<select id=\\"getAll6\\" resultType=\\"cn.zlinchuan.entity.UserEntity\\">\\n select * from (select * from user where {{_DATA_SCOPE_FLAG}}) t where 1 = 1\\n</select>\\n
\\nsql
\\nselect * from (select * from user where user.age = 111 OR user.age = 222) t where 1 = 1\\n
\\n真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22、Java 23一样。
\\n下一个长期支持版是 Java 25,预计今年 9 月份发布。
\\nJava 24 带来的新特性还是蛮多的,一共 24 个。Java 23 和 Java 23 都只有 12 个,Java 24的新特性相当于这两次的总和了。因此,这个版本还是非常有必要了解一下的。
\\n下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
\\n我在昨天晚上详细看了一下 Java 24 的详细更新,并对其中比较重要的新特性做了详细的解读,希望对你有帮助!
\\n本文内容概览:
\\n密钥派生函数 API 是一种用于从初始密钥和其他数据派生额外密钥的加密算法。它的核心作用是为不同的加密目的(如加密、认证等)生成多个不同的密钥,避免密钥重复使用带来的安全隐患。 这在现代加密中是一个重要的里程碑,为后续新兴的量子计算环境打下了基础
\\n通过该 API,开发者可以使用最新的密钥派生算法(如 HKDF 和未来的 Argon2):
\\n// 创建一个 KDF 对象,使用 HKDF-SHA256 算法\\nKDF hkdf = KDF.getInstance(\\"HKDF-SHA256\\"); \\n\\n// 创建 Extract 和 Expand 参数规范\\nAlgorithmParameterSpec params =\\n HKDFParameterSpec.ofExtract()\\n .addIKM(initialKeyMaterial) // 设置初始密钥材料\\n .addSalt(salt) // 设置盐值\\n .thenExpand(info, 32); // 设置扩展信息和目标长度\\n\\n// 派生一个 32 字节的 AES 密钥\\nSecretKey key = hkdf.deriveKey(\\"AES\\", params);\\n\\n// 可以使用相同的 KDF 对象进行其他密钥派生操作\\n
\\n在传统 JVM 中,应用在每次启动时需要动态加载和链接类。这种机制对启动时间敏感的应用(如微服务或无服务器函数)带来了显著的性能瓶颈。该特性通过缓存已加载和链接的类,显著减少了重复工作的开销,显著减少 Java 应用程序的启动时间。测试表明,对大型应用(如基于 Spring 的服务器应用),启动时间可减少 40% 以上。
\\n这个优化是零侵入性的,对应用程序、库或框架的代码无需任何更改,启动也方式保持一致,仅需添加相关 JVM 参数(如 -XX:+ClassDataSharing
)。
类文件 API 在 JDK 22 进行了第一次预览(JEP 457),在 JDK 23 进行了第二次预览并进一步完善(JEP 466)。最终,该特性在 JDK 24 中顺利转正。
\\n类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。
\\n// 创建一个 ClassFile 对象,这是操作类文件的入口。\\nClassFile cf = ClassFile.of();\\n// 解析字节数组为 ClassModel\\nClassModel classModel = cf.parse(bytes);\\n\\n// 构建新的类文件,移除以 \\"debug\\" 开头的所有方法\\nbyte[] newBytes = cf.build(classModel.thisClass().asSymbol(),\\n classBuilder -> {\\n // 遍历所有类元素\\n for (ClassElement ce : classModel) {\\n // 判断是否为方法 且 方法名以 \\"debug\\" 开头\\n if (!(ce instanceof MethodModel mm\\n && mm.methodName().stringValue().startsWith(\\"debug\\"))) {\\n // 添加到新的类文件中\\n classBuilder.with(ce);\\n }\\n }\\n });\\n
\\n流收集器 Stream::gather(Gatherer)
是一个强大的新特性,它允许开发者定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。Gatherer
接口是该特性的核心,它定义了如何从流中收集元素,维护中间状态,并在处理过程中生成结果。
与现有的 filter
、map
或 distinct
等内置操作不同,Stream::gather
使得开发者能够实现那些难以用标准 Stream 操作完成的任务。例如,可以使用 Stream::gather
实现滑动窗口、自定义规则的去重、或者更复杂的状态转换和聚合。 这种灵活性极大地扩展了 Stream API 的应用范围,使开发者能够应对更复杂的数据处理场景。
基于 Stream::gather(Gatherer)
实现字符串长度的去重逻辑:
var result = Stream.of(\\"foo\\", \\"bar\\", \\"baz\\", \\"quux\\")\\n .gather(Gatherer.ofSequential(\\n HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度\\n (set, str, downstream) -> {\\n if (set.add(str.length())) {\\n return downstream.push(str);\\n }\\n return true; // 继续处理流\\n }\\n ))\\n .toList();// 转换为列表\\n\\n// 输出结果 ==> [foo, quux]\\n
\\nJDK 24 不再允许启用 Security Manager
,即使通过 java -Djava.security.manager
命令也无法启用,这是逐步移除该功能的关键一步。虽然 Security Manager
曾经是 Java 中限制代码权限(如访问文件系统或网络、读取或写入敏感文件、执行系统命令)的重要工具,但由于复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。
作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
\\nfinal static ScopedValue<...> V = new ScopedValue<>();\\n\\n// In some method\\nScopedValue.where(V, <value>)\\n .run(() -> { ... V.get() ... call methods ... });\\n\\n// In a method called directly or indirectly from the lambda expression\\n... V.get() ...\\n
\\n作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。
\\n优化了虚拟线程与 synchronized
的工作机制。 虚拟线程在 synchronized
方法和代码块中阻塞时,通常能够释放其占用的操作系统线程(平台线程),避免了对平台线程的长时间占用,从而提升应用程序的并发能力。 这种机制避免了“固定 (Pinning)”——即虚拟线程长时间占用平台线程,阻止其服务于其他虚拟线程的情况。
现有的使用 synchronized
的 Java 代码无需修改即可受益于虚拟线程的扩展能力。 例如,一个 I/O 密集型的应用程序,如果使用传统的平台线程,可能会因为线程阻塞而导致并发能力下降。 而使用虚拟线程,即使在 synchronized
块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。
默认情况下,JDK 同时包含运行时镜像(运行时所需的模块)和 JMOD 文件。这个特性使得 jlink 工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,减少了 JDK 的安装体积(约 25%)。
\\n说明:
\\n这个特性主要简化了 main
方法的的声明。对于 Java 初学者来说,这个 main
方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
没有使用该特性之前定义一个 main
方法:
public class HelloWorld {\\n public static void main(String[] args) {\\n System.out.println(\\"Hello, World!\\");\\n }\\n}\\n
\\n使用该新特性之后定义一个 main
方法:
class HelloWorld {\\n void main() {\\n System.out.println(\\"Hello, World!\\");\\n }\\n}\\n
\\n进一步简化(未命名的类允许我们省略类名)
\\nvoid main() {\\n System.out.println(\\"Hello, World!\\");\\n}\\n
\\nJDK 24 引入了支持实施抗量子的基于模块晶格的数字签名算法 (Module-Lattice-Based Digital Signature Algorithm, ML-DSA),为抵御未来量子计算机可能带来的威胁做准备。
\\nML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。
\\nsun.misc.Unsafe
内存访问方法时发出警告JDK 23(JEP 471) 提议弃用 sun.misc.Unsafe
中的内存访问方法,这些方法将来的版本中会被移除。在 JDK 24 中,当首次调用 sun.misc.Unsafe
的任何内存访问方法时,运行时会发出警告。
这些不安全的方法已有安全高效的替代方案:
\\njava.lang.invoke.VarHandle
:JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。java.lang.foreign.MemorySegment
:JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 VarHandle
协同工作。这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。
\\nimport jdk.incubator.foreign.*;\\nimport java.lang.invoke.VarHandle;\\n\\n// 管理堆外整数数组的类\\nclass OffHeapIntBuffer {\\n\\n // 用于访问整数元素的VarHandle\\n private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();\\n\\n // 内存管理器\\n private final Arena arena;\\n\\n // 堆外内存段\\n private final MemorySegment buffer;\\n\\n // 构造函数,分配指定数量的整数空间\\n public OffHeapIntBuffer(long size) {\\n this.arena = Arena.ofShared();\\n this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);\\n }\\n\\n // 释放内存\\n public void deallocate() {\\n arena.close();\\n }\\n\\n // 以volatile方式设置指定索引的值\\n public void setVolatile(long index, int value) {\\n ELEM_VH.setVolatile(buffer, 0L, index, value);\\n }\\n\\n // 初始化指定范围的元素为0\\n public void initialize(long start, long n) {\\n buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,\\n ValueLayout.JAVA_INT.byteSize() * n)\\n .fill((byte) 0);\\n }\\n\\n // 将指定范围的元素复制到新数组\\n public int[] copyToNewArray(long start, int n) {\\n return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,\\n ValueLayout.JAVA_INT.byteSize() * n)\\n .toArray(ValueLayout.JAVA_INT);\\n }\\n}\\n
\\nJDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent
,目前处于孵化器阶段。
结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
\\n结构化并发的基本 API 是StructuredTaskScope
,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
StructuredTaskScope
的基本用法如下:
try (var scope = new StructuredTaskScope<Object>()) {\\n // 使用fork方法派生线程来执行子任务\\n Future<Integer> future1 = scope.fork(task1);\\n Future<String> future2 = scope.fork(task2);\\n // 等待线程完成\\n scope.join();\\n // 结果的处理可能包括处理或重新抛出异常\\n ... process results/exceptions ...\\n } // close\\n
\\n结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
\\n如果你想系统了解 Java 8 以及之后版本的新特性,可以在 JavaGuide 上阅读对应的文章:
\\n比较推荐这几篇:
\\n随着人工智能的飞速发展,大语言模型(LLM)正在革命性地重塑用户与软件的交互范式。想象一下这样的场景:用户无需钻研复杂的API文档或者在繁琐的表单间来回切换,只需通过自然语言直接与系统对话——\\"帮我查找所有2023年出版的图书\\"、\\"创建一个新用户叫张三,邮箱是zhangsan@example.com\\"。这种直观、流畅的交互方式不仅能显著降低新用户的学习曲线,更能大幅削减B端系统的培训成本和实施周期,让企业应用变得更为简单和高效。
\\n这正是Model Context Protocol (MCP) 协议在应用层面所带来的价值体现。
\\n我这里不粘贴官方的定义,用大白话给大家解释下:MCP就像是AI世界的\\"万能适配器\\"。想象你有很多不同类型的服务和数据库,每个都有自己独特的\\"说话方式\\"。AI需要和这些服务交流时就很麻烦,因为要学习每个服务的\\"语言\\"。
\\nMCP解决了这个问题 - 它就像一个统一的翻译官,让AI只需学一种\\"语言\\"就能和所有服务交流。这样开发者不用为每个服务单独开发连接方式,AI也能更容易获取它需要的信息。
\\n如果你是一个后端同学,那么应该接触或听说过gRPC。gRPC通过标准化的通信方式可以实现不同语言开发的服务之间进行通信,那么MCP专门为AI模型设计的\\"翻译官和接口管理器\\",让AI能以统一方式与各种应用或数据源交互。
\\n我们假设开发了一个天气服务,用户想要查询深圳的天气,这里分别以传统API方式和MCP方式进行对比:
\\n这里为了演示,先准备好一个图书管理服务,图书实体字段如下:
\\nimport jakarta.persistence.*;\\nimport jakarta.validation.constraints.NotBlank;\\nimport jakarta.validation.constraints.NotNull;\\nimport jakarta.validation.constraints.PastOrPresent;\\nimport lombok.AllArgsConstructor;\\nimport lombok.Data;\\nimport lombok.NoArgsConstructor;\\n\\n\\nimport java.time.LocalDate;\\n\\n@Entity\\n@Table(name = \\"books\\")\\n@Data\\n@AllArgsConstructor\\n@NoArgsConstructor\\npublic class Book {\\n \\n @Id\\n @GeneratedValue(strategy = GenerationType.IDENTITY)\\n private Long id;\\n\\n @NotBlank(message = \\"书名不能为空\\")\\n @Column(nullable = false)\\n private String title;\\n\\n @NotBlank(message = \\"分类不能为空\\")\\n @Column(nullable = false)\\n private String category;\\n\\n @NotBlank(message = \\"作者不能为空\\")\\n @Column(nullable = false)\\n private String author;\\n\\n @NotNull(message = \\"出版日期不能为空\\")\\n @PastOrPresent(message = \\"出版日期不能是未来日期\\")\\n @Column(nullable = false)\\n private LocalDate publicationDate;\\n\\n @NotBlank(message = \\"ISBN编码不能为空\\")\\n @Column(nullable = false, unique = true)\\n private String isbn;\\n \\n }\\n
\\n为这个服务编写了2个测试方法:
\\nimport com.example.entity.Book;\\n\\nimport java.util.List;\\n\\npublic interface BookService {\\n\\n // 根据作者查询\\n List<Book> findBooksByAuthor(String author);\\n\\n // 根据分类查询\\n List<Book> findBooksByCategory(String category);\\n}\\n
\\n现在我们要将这个SpringBoot服务改造成MCP服务,需要以下步骤:
\\n在pom.xml中引入相关依赖,这里提示一下anthropic的访问需要代理,否则会提示403。
\\n<!-- Spring AI 核心依赖 --\x3e\\n<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-core</artifactId>\\n</dependency>\\n\\n<!-- Anthropic 模型支持 --\x3e\\n<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>\\n</dependency>\\n\\n<!-- MCP 服务器支持 - WebMVC版本 --\x3e\\n<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>\\n</dependency>\\n
\\n由于目前这些依赖还是预览版本,所以在Maven中央仓库中是找不到的,需要我们额外引入仓库地址。
\\n<repositories>\\n <repository>\\n <id>spring-milestones</id>\\n <name>Spring Milestones</name>\\n <url>https://repo.spring.io/milestone</url>\\n <snapshots>\\n <enabled>false</enabled>\\n </snapshots>\\n </repository>\\n <repository>\\n <id>spring-snapshots</id>\\n <name>Spring Snapshots</name>\\n <url>https://repo.spring.io/snapshot</url>\\n <releases>\\n <enabled>false</enabled>\\n </releases>\\n </repository>\\n <repository>\\n <name>Central Portal Snapshots</name>\\n <id>central-portal-snapshots</id>\\n <url>https://central.sonatype.com/repository/maven-snapshots/</url>\\n <releases>\\n <enabled>false</enabled>\\n </releases>\\n <snapshots>\\n <enabled>true</enabled>\\n </snapshots>\\n </repository>\\n</repositories>\\n
\\n关于项目中代理的配置可以参考我这段配置:
\\nimport jakarta.annotation.PostConstruct;\\nimport org.springframework.context.annotation.Configuration;\\n\\n\\n@Configuration\\npublic class ProxyConfig {\\n\\n // 代理设置\\n private final String PROXY_HOST = \\"127.0.0.1\\";\\n private final int PROXY_PORT = 10080;\\n\\n @PostConstruct\\n public void setSystemProxy() {\\n // 设置系统代理属性,这会影响Spring Boot自动配置的HTTP客户端\\n System.setProperty(\\"http.proxyHost\\", PROXY_HOST);\\n System.setProperty(\\"http.proxyPort\\", String.valueOf(PROXY_PORT));\\n System.setProperty(\\"https.proxyHost\\", PROXY_HOST);\\n System.setProperty(\\"https.proxyPort\\", String.valueOf(PROXY_PORT));\\n\\n System.out.println(\\"System proxy configured: http://\\" + PROXY_HOST + \\":\\" + PROXY_PORT);\\n }\\n}\\n
\\n我们的目的是将一个Spring服务改造成MCP服务,所以这里不需要进行客户端的配置,同理,在引入依赖的时候也不用引入客户端的依赖。
\\n# Spring AI api-key\\nspring.ai.anthropic.api-key=这里换成你的api-key\\n\\n# MCP服务端开启\\nspring.ai.mcp.server.enabled=true\\n\\n# MCP服务端配置\\nspring.ai.mcp.server.name=book-management-server\\nspring.ai.mcp.server.version=1.0.0\\nspring.ai.mcp.server.type=SYNC\\nspring.ai.mcp.server.sse-message-endpoint=/mcp/message\\n
\\n服务的改造有两种思路-分别是注解方式和函数Bean方式,这里对两种方式都做下简略说明。注解方式在需要改造的实现类对需要改造的方法加上@Tool和@ToolParam注解分别标记方法和参数。
\\nimport com.example.entity.Book;\\nimport com.example.repository.BookRepository;\\nimport com.example.service.BookService;\\nimport jakarta.annotation.Resource;\\nimport lombok.RequiredArgsConstructor;\\nimport org.springframework.ai.tool.annotation.Tool;\\nimport org.springframework.ai.tool.annotation.ToolParam;\\nimport org.springframework.stereotype.Service;\\n\\nimport java.util.List;\\n\\n@Service\\n@RequiredArgsConstructor\\npublic class BookServiceImpl implements BookService {\\n\\n @Resource\\n private BookRepository bookRepository;\\n\\n\\n @Override\\n @Tool(name = \\"findBooksByTitle\\", description = \\"根据书名模糊查询图书,支持部分标题匹配\\")\\n public List<Book> findBooksByTitle(@ToolParam(description = \\"书名关键词\\") String title) {\\n return bookRepository.findByTitleContaining(title);\\n }\\n\\n @Override\\n @Tool(name = \\"findBooksByAuthor\\", description = \\"根据作者精确查询图书\\")\\n public List<Book> findBooksByAuthor(@ToolParam(description = \\"作者姓名\\") String author) {\\n return bookRepository.findByAuthor(author);\\n }\\n\\n @Override\\n @Tool(name = \\"findBooksByCategory\\", description = \\"根据图书分类精确查询图书\\")\\n public List<Book> findBooksByCategory(@ToolParam(description = \\"图书分类\\")String category) {\\n return bookRepository.findByCategory(category);\\n }\\n}\\n
\\n接着将这个实现类注册到MCP服务器配置上即可。
\\nimport com.example.service.BookService;\\nimport org.springframework.ai.tool.ToolCallbackProvider;\\nimport org.springframework.ai.tool.method.MethodToolCallbackProvider;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n/**\\n * MCP服务器配置类,负责注册MCP工具\\n */\\n@Configuration\\npublic class McpServerConfig {\\n\\n /**\\n * 注册工具回调提供者,将BookQueryService中的@Tool方法暴露为MCP工具\\n *\\n * @param bookService 图书服务\\n * @return 工具回调提供者\\n */\\n @Bean\\n public ToolCallbackProvider bookToolCallbackProvider(BookService bookService) {\\n return MethodToolCallbackProvider.builder()\\n .toolObjects(bookService)\\n .build();\\n }\\n\\n}\\n
\\n函数Bean方式则不需要在原服务实现类方法上加注解,而是单独声明一个类将查询方法作为函数Bean导出。
\\nimport com.example.entity.Book;\\nimport com.example.service.BookService;\\nimport jakarta.annotation.Resource;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.stereotype.Service;\\n\\nimport java.util.List;\\nimport java.util.function.Function;\\n\\n/**\\n * 图书查询服务,将查询方法作为函数Bean导出\\n */\\n@Service\\npublic class BookQueryService {\\n\\n @Resource\\n private BookService bookService;\\n\\n /**\\n * 根据书名查询图书的函数Bean\\n */\\n @Bean\\n public Function<String, List<Book>> findBooksByTitle() {\\n return title -> bookService.findBooksByTitle(title);\\n }\\n\\n /**\\n * 根据作者查询图书的函数Bean\\n */\\n @Bean\\n public Function<String, List<Book>> findBooksByAuthor() {\\n return author -> bookService.findBooksByAuthor(author);\\n }\\n\\n /**\\n * 根据分类查询图书的函数Bean\\n */\\n @Bean\\n public Function<String, List<Book>> findBooksByCategory() {\\n return category -> bookService.findBooksByCategory(category);\\n }\\n\\n}\\n
\\n他们二者的区别在于定义AI聊天客户端的时候是否需要显式地声明,如果我们采用函数Bean方式则需要对聊天客户端客户端进行如下声明。
\\nimport org.springframework.ai.chat.client.ChatClient;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n/**\\n * 聊天客户端配置类\\n */\\n@Configuration\\npublic class ChatClientConfig {\\n\\n\\n /**\\n * 配置ChatClient,注册系统指令和工具函数\\n */\\n @Bean\\n public ChatClient chatClient(ChatClient.Builder builder) {\\n return builder\\n .defaultSystem(\\"你是一个图书管理助手,可以帮助用户查询图书信息。\\" +\\n \\"你可以根据书名模糊查询、根据作者查询和根据分类查询图书。\\" +\\n \\"回复时,请使用简洁友好的语言,并将图书信息整理为易读的格式。\\")\\n // 注册工具方法,这里使用方法名称来引用Spring上下文中的函数Bean\\n .defaultTools(\\n \\"findBooksByTitle\\",\\n \\"findBooksByAuthor\\",\\n \\"findBooksByCategory\\"\\n )\\n .build();\\n }\\n}\\n
\\n完成了服务开发后,我们就可以声明一个控制器对外暴露进行调用。
\\nimport com.example.model.ChatRequest;\\nimport com.example.model.ChatResponse;\\nimport jakarta.annotation.Resource;\\nimport org.springframework.ai.chat.client.ChatClient;\\nimport org.springframework.http.ResponseEntity;\\nimport org.springframework.web.bind.annotation.*;\\n\\n/**\\n * 聊天控制器,处理AI聊天请求\\n */\\n@RestController\\n@RequestMapping(\\"/api/chat\\")\\npublic class ChatController {\\n\\n \\n @Resource\\n private ChatClient chatClient;\\n\\n\\n /**\\n * 处理聊天请求,使用AI和MCP工具进行响应\\n *\\n * @param request 聊天请求\\n * @return 包含AI回复的响应\\n */\\n @PostMapping\\n public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {\\n try {\\n // 创建用户消息\\n String userMessage = request.getMessage();\\n\\n // 使用流式API调用聊天\\n String content = chatClient.prompt()\\n .user(userMessage)\\n .call()\\n .content();\\n\\n return ResponseEntity.ok(new ChatResponse(content));\\n } catch (Exception e) {\\n e.printStackTrace();\\n return ResponseEntity.ok(new ChatResponse(\\"处理请求时出错: \\" + e.getMessage()));\\n }\\n }\\n \\n}\\n
\\n为了方便测试,我们开发一个数据初始化器,通过实现CommandLineRunner
接口,它会在我们的应用程序启动时自动向数据库中加载这些测试数据。
import com.example.entity.Book;\\nimport com.example.repository.BookRepository;\\nimport jakarta.annotation.Resource;\\nimport lombok.RequiredArgsConstructor;\\nimport org.springframework.boot.CommandLineRunner;\\nimport org.springframework.stereotype.Component;\\n\\nimport java.time.LocalDate;\\nimport java.util.Arrays;\\nimport java.util.List;\\n\\n@Component\\n@RequiredArgsConstructor\\npublic class DataInitializer implements CommandLineRunner {\\n\\n @Resource\\n private BookRepository bookRepository;\\n\\n @Override\\n public void run(String... args) throws Exception {\\n // 准备示例数据\\n List<Book> sampleBooks = Arrays.asList(\\n new Book(null, \\"Spring实战(第6版)\\", \\"编程\\", \\"Craig Walls\\",\\n LocalDate.of(2022, 1, 15), \\"9787115582247\\"),\\n new Book(null, \\"深入理解Java虚拟机\\", \\"编程\\", \\"周志明\\",\\n LocalDate.of(2019, 12, 1), \\"9787111641247\\"),\\n new Book(null, \\"Java编程思想(第4版)\\", \\"编程\\", \\"Bruce Eckel\\",\\n LocalDate.of(2007, 6, 1), \\"9787111213826\\"),\\n new Book(null, \\"算法(第4版)\\", \\"计算机科学\\", \\"Robert Sedgewick\\",\\n LocalDate.of(2012, 10, 1), \\"9787115293800\\"),\\n new Book(null, \\"云原生架构\\", \\"架构设计\\", \\"张三\\",\\n LocalDate.of(2023, 3, 15), \\"9781234567890\\"),\\n new Book(null, \\"微服务设计模式\\", \\"架构设计\\", \\"张三\\",\\n LocalDate.of(2021, 8, 20), \\"9789876543210\\"),\\n new Book(null, \\"领域驱动设计\\", \\"架构设计\\", \\"Eric Evans\\",\\n LocalDate.of(2010, 4, 10), \\"9787111214748\\"),\\n new Book(null, \\"高性能MySQL\\", \\"数据库\\", \\"Baron Schwartz\\",\\n LocalDate.of(2013, 5, 25), \\"9787111464747\\"),\\n new Book(null, \\"Redis实战\\", \\"数据库\\", \\"Josiah L. Carlson\\",\\n LocalDate.of(2015, 9, 30), \\"9787115419378\\"),\\n new Book(null, \\"深入浅出Docker\\", \\"容器技术\\", \\"李四\\",\\n LocalDate.of(2022, 11, 20), \\"9787123456789\\")\\n );\\n\\n // 保存示例数据\\n bookRepository.saveAll(sampleBooks);\\n\\n System.out.println(\\"数据初始化完成,共加载 \\" + sampleBooks.size() + \\" 本图书\\");\\n }\\n\\n}\\n
\\n接下来我们通过请求接口进行如下测试:
\\n可以看到此时返回结果是数据库中的测试数据内容。这里是根据用户输入的问题,大模型会判断我们开放的工具方法中是否有匹配的,如果有则进行调用并返回。
\\n通过Spring Boot与MCP的整合,我们轻松实现了传统CRUD系统到智能AI助手的转变。MCP作为AI与服务之间的桥梁,极大简化了集成工作。未来随着MCP生态发展,\\"对话即服务\\"将可能成为应用的开发范式,让复杂系统变得更加易用。
","description":"引言 随着人工智能的飞速发展,大语言模型(LLM)正在革命性地重塑用户与软件的交互范式。想象一下这样的场景:用户无需钻研复杂的API文档或者在繁琐的表单间来回切换,只需通过自然语言直接与系统对话——\\"帮我查找所有2023年出版的图书\\"、\\"创建一个新用户叫张三,邮箱是zhangsan@example.com\\"。这种直观、流畅的交互方式不仅能显著降低新用户的学习曲线,更能大幅削减B端系统的培训成本和实施周期,让企业应用变得更为简单和高效。\\n\\n这正是Model Context Protocol (MCP) 协议在应用层面所带来的价值体现。\\n\\n认识MCP\\n\\n我这里不粘…","guid":"https://juejin.cn/post/7483454392979570700","author":"别惹CC","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-20T04:45:12.091Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec9ecda9d6a246c9be269758778dd305~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1743050712&x-signature=9tq1%2FHAHHQJW35uv8y92UyHDpsI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd3fd591893f40af81933b7a3d97a215~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1743050712&x-signature=irj%2FDAXsEn3AxAEf9tPBzGY8DR8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1dbedc210c64fdbaa7999e01ed66d7c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1743050712&x-signature=9wDoXdKWZyHftwYMVh0qNEwd%2BaQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Spring Boot","MCP"],"attachments":null,"extra":null,"language":null},{"title":"换掉Typora!这款现代化的笔记应用,太炫酷了!","url":"https://juejin.cn/post/7483427107738189859","content":"\\n\\n作为一名程序员,大家应该都关注了很多技术大佬,其中不乏文章格式看起来很舒服的。今天给大家分享一款好用的开源笔记应用
\\nWeChat Markdown Editor
,能将Markdown写的文档即时渲染为微信文章,希望对大家有所帮助!
WeChat Markdown Editor(简称md)是一款高度简洁的微信Markdown编辑器,能将Markdown文档自动即时渲染为微信文章,目前在Github上已有7.8k+star
。
它主要具有如下特性:
\\n这是一张md使用过程中的效果图,还是非常炫酷的!
\\n\\n\\nmd支持在线使用、Docker部署、源码编译部署等多种使用方式,这里以Docker部署为例。
\\n
docker pull doocs/md:latest\\n
\\ndocker run -p 8080:80 --name md -d doocs/md:latest\\n
\\n本文将以《mall-swarm微服务学习教程》中的部分文档为例,来演示下md的使用。
\\n\\n\\n这里简单介绍下mall项目,mall项目是一套基于
\\nSpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目地址
\\n项目演示:
\\n内容管理
按钮开启左侧的内容侧边栏,然后通过加号来新建文档;样式->主题
来切换主题,这里目前有三种主题;设置
按钮进行文章样式的设置,功能还是非常强大的!jsdelivr
上去。md
或者html
格式的文件。md确实是一款非常不错的Markdown笔记工具,只要你会Markdown语法,就能排版出简洁大方的微信文章,感兴趣的小伙伴可以尝试下!
\\n简单一句话
\\n把del_flag 0值:代表未删除 null值:代表删除
当要对保证某些可编辑字段的唯一性时,代码需要在插入和更新时都进行唯一性校验,这很繁琐。
\\n因此想到采用将对于唯一性的校验直接交给数据库进行,通过数据库唯一索引实现。
\\n但这就又出现了另一个问题:由于采用的是“逻辑删除”,那么会造成唯一字段值相同的数据只能删除一次的情况。第二次删除时,由于已存在 del_flag 值为 1 的数据,会报错“违反唯一约束”
\\n基于mysql“唯一约束对 Null 失效”的原理,将被删数据的逻辑删除字段置为 null 即可。如果使用的是 mybatis-plus,可以直接通过@TableLogic
注解实现
/**\\n * 是否删除\\n *\\n * <p>\\n * 为解决\'逻辑删除\'和\'唯一索引\'冲突问题,而将逻辑删除字段设置为NULL\\n * </p>\\n */\\n @TableLogic(value = \\"0\\", delval = \\"NULL\\")\\n private Boolean deleteFlag;\\n
\\n这样就可以在数据库层面轻松实现唯一性的保证,且\'逻辑删除\'和\'唯一索引\'不会冲突。
\\n此时当入参违反唯一性约束时就会抛出异常,我们需要对异常进行处理,以向前端返回更加友好的报错信息。
\\ntry {\\n thisGiftProductService.saveThisGiftProduct(thisGiftProductDO, request);\\n return RestResult.<Void>builder().success(\\"保存成功\\");\\n} catch (Exception e) {\\n log.error(\\"新增错误\\", e);\\n Throwable cause = e.getCause();\\n if (cause instanceof java.sql.SQLIntegrityConstraintViolationException) {\\n return RestResult.<Void>builder().fail(\\"本品产品编码和组织信息组合不能重复\\");\\n }\\n return RestResult.<Void>builder().fail(e.getMessage());\\n}\\n
\\n核心在于:
\\nlog.error(\\"新增错误\\", e);\\nThrowable cause = e.getCause();\\nif (cause instanceof java.sql.SQLIntegrityConstraintViolationException) {\\n return RestResult.<Void>builder().fail(\\"本品产品编码和组织信息组合不能重复\\");\\n}\\n
\\n之所以采用这种方法,是因为 java.sql.SQLIntegrityConstraintViolationException
异常是无法作为 catch 参数捕捉的。
\\n\\n大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术
\\n
在2025年3月18日宣布发布Apache Kafka
4.0.0
版本
Apache Kafka
4.0.0
是 Kafka
的一个重要里程碑,标志着其架构的重大转变。
标志着第一个完全不使用Apache ZooKeeper
的主要版本
默认运行在KRaft
模式下,简化了Kafka
的部署和管理。
消除了维护单独ZooKeeper
集成的复杂性。这一更改显著降低了运营开销,增强了可扩展性,并简化了管理任务
改动包含Kafka Broker
, Controller
, Producer
, Consumer
和Admin Client
自Apache Kafka 4.0.0
起,下一代消费者再平衡协议(KIP-848)正式发布(Generally Available, GA)。
该协议通过完全增量化的设计,不再依赖全局同步屏障,从而显著缩短了再平衡时间,同时提升了消费者组的可扩展性并简化了消费者的实现逻辑。
\\n使用新协议的消费者组现称为消费者组(Consumer Groups),而使用旧协议的组称为经典组(Classic Groups)。
\\n需注意,经典组仍可通过旧协议组成消费者组。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n改进点 | 旧协议(Classic) | 新协议(KIP-848) |
---|---|---|
再平衡机制 | 全局同步屏障(所有消费者暂停等待协调器指令) | 增量式协调(消费者异步提交状态) |
时间开销 | O(N)(N为消费者数量) | O(1)(仅需局部协调) |
资源占用 | 高(需维护完整的组成员列表) | 低(仅维护必要元数据) |
容错能力 | 弱(单个消费者故障触发全组再平衡) | 强(故障影响范围局部化) |
扩展性 | 支持数百消费者 | 支持数万消费者 |
server
默认开启新协议
consumer
必须通过设置group.protocol=consumer
详细说明参数consumer_rebalance_protocol:kafka.apache.org/40/document…
\\n改进降低了生产者(Producer)发生故障时出现“僵尸事务”(Zombie Transactions)的概率
\\n僵尸事务:当生产者因崩溃或网络故障无法提交或回滚事务时,事务协调器(Transaction Coordinator)可能残留未完成的事务状态,占用资源并可能引发数据不一致1。
\\n旧客户端兼容性:旧版客户端在处理事务验证阶段返回的NETWORK_EXCEPTION
错误时,可能误判为致命错误,导致事务管理器进入不可恢复状态1。
第二阶段的核心改进
\\n服务端事务验证增强
\\n在生产者发送 Produce 和 TxnOffsetCommit 请求时,服务端主动与事务协调器验证事务状态,确保事务处于活跃或可提交状态,避免处理已失效的事务请求1。
\\n新增错误转换逻辑,将网络异常(如 NETWORK_EXCEPTION)映射为客户端可重试的错误类型,提升旧客户端兼容性1。
\\n网络超时与连接中断:若验证请求超时或连接中断,服务端返回可重试错误,而非直接终止事务。
\\n并发控制优化:当多个验证请求并发时(如相同事务的 AddPartitionsToTxn 请求),通过错误码引导客户端重试,避免竞争条件1。
\\n服务端通过心跳超时机制检测僵尸事务,自动将其标记为终止状态,释放相关资源。
\\n生产者恢复后,通过事务 ID 和纪元(Epoch)验证合法性,防止旧事务干扰新事务
\\n详细说明参考transaction_protocol:kafka.apache.org/40/document…
\\n通过引入共享群组(Share Group) 的概念,支持基于 Kafka 主题的协作式消费模式。
\\nKIP-932 并未直接在Kafka
中新增“队列”这一数据结构,而是通过扩展现有主题的消费机制来满足队列场景的需求。
共享群组的功能类似于其他消息系统中的“持久化共享订阅”(Durable Shared Subscription)
\\nKIP-966
在 Kafka
4.0
中首次引入合格领导者副本(Eligible Leader Replicas, ELR) 的预览功能。ELR 是 ISR(In-Sync Replicas,同步副本)的子集,保证其数据完整性达到高水位线(High-Watermark)。ELR 可安全用于领导者选举,避免数据丢失
详细说明可以参考eligible_leader_replicas
\\nKRaft
模式下,节点可能因瞬时网络问题(如 GC 暂停)误判领导者失联,触发不必要的选举,导致:
集群波动:频繁领导者切换影响吞吐量
\\n元数据竞争:多个节点同时发起选举引发脑裂风险
\\n预投票机制原理
\\n预投票阶段:\\n节点感知领导者失联后,先向其他节点发送预投票请求(携带自身日志最新偏移量)
\\n接收节点检查请求者日志是否足够新(避免落后副本成为领导者)
\\n仅当获得多数预投票认可后,节点才发起正式选举
\\n否则进入冷却期(election.backoff.ms)
\\nKIP-714 允许集群管理员通过插件直接从Broker
收集客户端指标,但仅覆盖 Kafka 原生客户端(生产者、消费者、Admin)。
KIP-1076 扩展此功能,支持嵌入式客户端(如 Kafka Streams
)上报应用级指标,实现端到端性能监控。
新增auto.offset.reset.duration
配置,允许消费者在无初始偏移量或偏移量失效时,从指定时间点(如 24 小时前)开始消费,避免全量数据重处理。
针对KIP-848
(新消费者组)和 KIP-932
(共享组)引入的组类型,更新 kafka-groups.sh
工具以支持查看所有组类型,修复 Admin API
的兼容性问题。
客户端在元数据超时未更新或收到服务端错误码(如 FENCED_INSTANCE_ID
)时,主动触发重引导,解决旧机制中元数据过时导致的阻塞问题。
首次移除了旧的协议 API 版本。
\\n用户在将 Java 客户端(包括 Connect 和 Streams)升级到 4.0 版本之前,应确保broker
版本为2.1
或更高。
同样,用户在将broker
升级到4.0
版本之前,应确保其 Java 客户端版本为2.1
或更高
定义 Kafka 客户端、Streams 和 Connect 到 4.0 的升级步骤,强制阅读以避免升级风险。
\\n日志框架迁移到Log4j2
,提供 log4j-transform-cli
自动转换旧配置,但部分特性受限(如自定义 Appender)。
自 Kafka 3.0 弃用的消息格式v0
和v1
在 4.0 中彻底移除,仅支持v2+
。
Kafka Clients
和 Kafka Streams
需JDK11+
broker
、Connect
和工具需JDK17+
调整多个配置的默认值(如num.io.threads
根据 CPU 核数动态设置),提升开箱即用体验。
总的来说改动还挺多,最核心的改动还是默认使用KRaft
模式运行。
Kafka Streams
和Kafka Connect
也有一些改动,具体参考官网吧
在 Spring 中,若创建 Bean 发生解决循环依赖会通过三级缓存解决。
\\nsingletonObjects
(一级缓存):存放 完整 的 Bean 对象;earlysingletonObjects
(二级缓存):存放 Bean 的 早期(early)对象;singletonFactories
(三级缓存):存放 Bean 的 工厂(Factory)对象;我们应用的 pay 这个模块,在启动时候会报错。报错信息提示如下:
\\n提示是有一个循环依赖的问题,即 PayApplicationService -> PayChannelServiceFactory -> MockPayChannelService -> PayApplicationService.
\\nSpring不是引入了三级缓存,解决了循环依赖的问题吗?那为啥启动还报错呢?
\\n从 Spring Framework 5.3 开始,Spring 默认禁用了对循环依赖的支持,而在 Spring 2.6 中,这一行为得到了进一步的明确和强化
\\n如果想要开启对循环依赖的支持,需要在配置文件中加入
\\nspring.main.allow-circular-references=true\\n
\\n或者,如果不想加配置的话,也可以用@Lazy
注解,在@Autowired
地方增加即可:
@Autowired\\n@Lazy\\nprivate PayChannelServiceFactory payChannelServiceFactory;\\n
","description":"✅Spring 循环依赖细节 在 Spring 中,若创建 Bean 发生解决循环依赖会通过三级缓存解决。\\n\\nsingletonObjects(一级缓存):存放 完整 的 Bean 对象;\\nearlysingletonObjects(二级缓存):存放 Bean 的 早期(early)对象;\\nsingletonFactories(三级缓存):存放 Bean 的 工厂(Factory)对象;\\n\\n✅Spring 循环依赖导致启动报错\\n\\n我们应用的 pay 这个模块,在启动时候会报错。报错信息提示如下:\\n\\n提示是有一个循环依赖的问题,即…","guid":"https://juejin.cn/post/7483329722496581658","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-19T02:17:03.066Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/22a9f18d745f4aa689a45e0ed16f0b5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742955888&x-signature=DGQ3TowRCEZguz25uMBG9Rv3DYE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/817715c8fc0e491894e6a56327cb4ea7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742955888&x-signature=65cs%2BSvaG66gH7jfHfEGckv0tK4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"系统高可用的 10 条军规","url":"https://juejin.cn/post/7482993860878532635","content":"系统高可用是非常经典的问题,无论在面试,还是实际工作中,都经常会遇到。
\\n这篇文章跟大家一起聊聊,保证系统高可用的10条军规,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\n场景:某电商大促期间,数据库主节点突然宕机,导致全站交易瘫痪。
\\n问题:单节点部署的系统,一旦关键组件(如数据库、消息队列)故障,业务直接归零。
\\n解决方案:通过主从复制、集群化部署实现冗余。例如MySQL主从同步,Redis Sentinel哨兵机制。
\\nMySQL主从配置如下:
\\n-- 主库配置\\nCHANGE MASTER TO \\nMASTER_HOST=\'master_host\',\\nMASTER_USER=\'replica_user\',\\nMASTER_PASSWORD=\'password\',\\nMASTER_LOG_FILE=\'mysql-bin.000001\',\\nMASTER_LOG_POS=154;\\n\\n-- 从库启动复制\\nSTART SLAVE;\\n
\\n效果:主库宕机时,从库自动切换为可读写状态,业务无感知。
\\n场景:支付服务响应延迟,导致订单服务线程池耗尽,引发连锁故障。
\\n问题:服务依赖链中某个环节异常,会像多米诺骨牌一样拖垮整个系统。
\\n解决方案:引入熔断器模式,例如Hystrix或Resilience4j。
\\nResilience4j熔断配置如下:
\\nCircuitBreakerConfig config = CircuitBreakerConfig.custom()\\n .failureRateThreshold(50) // 失败率超过50%触发熔断\\n .waitDurationInOpenState(Duration.ofMillis(1000))\\n .build();\\nCircuitBreaker circuitBreaker = CircuitBreaker.of(\\"paymentService\\", config);\\n\\n// 调用支付服务\\nSupplier<String> supplier = () -> paymentService.call();\\nSupplier<String> decoratedSupplier = CircuitBreaker\\n .decorateSupplier(circuitBreaker, supplier);\\n
\\n效果:当支付服务失败率飙升时,自动熔断并返回降级结果(如“系统繁忙,稍后重试”)。
\\n场景:秒杀活动开始瞬间,10万QPS直接击穿数据库连接池。
\\n问题:突发流量超过系统处理能力,导致资源耗尽。
\\n解决方案:引入消息队列(如Kafka、RocketMQ)做异步缓冲。
\\n用户下单的系统流程图如下:
\\nRocketMQ生产者的示例代码:
\\nDefaultMQProducer producer = new DefaultMQProducer(\\"seckill_producer\\");\\nproducer.setNamesrvAddr(\\"127.0.0.1:9876\\");\\nproducer.start();\\nMessage msg = new Message(\\"seckill_topic\\", \\"订单数据\\".getBytes());\\nproducer.send(msg);\\n
\\n效果:将瞬时10万QPS的请求平滑处理为数据库可承受的2000 TPS。
\\n场景:日常流量100台服务器足够,但大促时需要快速扩容到500台。
\\n问题:固定资源无法应对业务波动,手动扩容效率低下。
\\n解决方案:基于Kubernetes的HPA(Horizontal Pod Autoscaler)。
\\nK8s HPA 的配置如下:
\\napiVersion: autoscaling/v2\\nkind: HorizontalPodAutoscaler\\nmetadata:\\n name: order-service-hpa\\nspec:\\n scaleTargetRef:\\n apiVersion: apps/v1\\n kind: Deployment\\n name: order-service\\n minReplicas: 2\\n maxReplicas: 10\\n metrics:\\n - type: Resource\\n resource:\\n name: cpu\\n target:\\n type: Utilization\\n averageUtilization: 60\\n
\\n效果:CPU利用率超过60%时自动扩容,低于30%时自动缩容。
\\n场景:新版本代码存在内存泄漏,全量发布导致线上服务崩溃。
\\n问题:一次性全量发布风险极高,可能引发全局故障。
\\n解决方案:基于流量比例的灰度发布策略。
\\nIstio流量染色配置如下:
\\napiVersion: networking.istio.io/v1alpha3\\nkind: VirtualService\\nmetadata:\\n name: bookinfo\\nspec:\\n hosts:\\n - bookinfo.com\\n http:\\n - route:\\n - destination:\\n host: reviews\\n subset: v1\\n weight: 90 # 90%流量走老版本\\n - destination:\\n host: reviews\\n subset: v2\\n weight: 10 # 10%流量走新版本\\n
\\n效果:新版本异常时,仅影响10%的用户,快速回滚无压力。
\\n场景:推荐服务超时导致商品详情页加载时间从200ms飙升到5秒。
\\n问题:非核心功能异常影响核心链路用户体验。
\\n解决方案:配置中心增加降级开关,如果遇到紧急情况,能 动态降级非关键服务。
\\nApollo配置中心的示例代码如下:
\\n@ApolloConfig\\nprivate Config config;\\n\\npublic ProductDetail getDetail(String productId) {\\n if(config.getBooleanProperty(\\"recommend.switch\\", true)) {\\n // 调用推荐服务\\n }\\n // 返回基础商品信息\\n}\\n
\\n效果:关闭推荐服务后,详情页响应时间恢复至200ms以内。
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n场景:某金融系统在真实流量下暴露出数据库死锁问题。
\\n问题:测试环境无法模拟真实流量特征,线上隐患难以发现。
\\n解决方案:基于流量录制的全链路压测。
\\n实施步骤:
\\n效果:提前发现数据库连接池不足、缓存穿透等问题。
\\n场景:用户表达到10亿行,查询性能断崖式下降。
\\n问题:单库单表成为性能瓶颈。
\\n解决方案:基于ShardingSphere的分库分表。
\\n分库分表的配置如下:
\\nsharding:\\n tables:\\n user:\\n actualDataNodes: ds_${0..1}.user_${0..15}\\n tableStrategy:\\n standard:\\n shardingColumn: user_id\\n preciseAlgorithmClassName: HashModShardingAlgorithm\\n preciseAlgorithmType: HASH_MOD\\n shardingCount: 16\\n
\\n效果:10亿数据分散到16个物理表,查询性能提升20倍。
\\n场景:某次机房网络抖动导致服务不可用3小时。
\\n问题:系统健壮性不足,故障恢复能力弱。
\\n解决方案:使用ChaosBlade模拟故障。
\\n示例命令:
\\n# 模拟网络延迟\\nblade create network delay --time 3000 --interface eth0\\n\\n# 模拟数据库节点宕机\\nblade create docker kill --container-id mysql-node-1\\n
\\n效果:提前发现缓存穿透导致DB负载过高的问题,优化缓存击穿防护策略。
\\n场景:磁盘IOPS突增导致订单超时,但运维人员2小时后才发现。
\\n问题:监控维度单一,无法快速定位根因。
\\n解决方案:构建Metrics-Log-Trace三位一体监控体系。
\\n技术栈组合:
\\n定位问题流程如下 :
\\nCPU利用率 > 80% → 关联日志检索 → 定位到GC频繁 → \\n追踪调用链 → 发现某个DAO层SQL未走索引\\n
\\n效果:故障定位时间从小时级缩短到分钟级。
\\n系统高可用建设就像打造一艘远洋巨轮。
\\n冗余部署是双发动机,熔断降级是救生艇,监控体系是雷达系统。
\\n但真正的关键在于:
\\n没有100%可用的系统,但通过这10个实战技巧,我们可以让系统的可用性从99%提升到99.99%。
\\n这0.99%的提升,可能意味着每年减少8小时的故障时间——而这,正是架构师价值的体现。
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n系统高可用是非常经典的问题,无论在面试,还是实际工作中,都经常会遇到。\\n\\n这篇文章跟大家一起聊聊,保证系统高可用的10条军规,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。\\n\\n1 冗余部署\\n\\n场景:某电商大促期间,数据库主节点突然宕机,导致全站交易瘫痪。\\n\\n问题:单节点部署的系统,一旦关键组件(如数据库、消息队列)故障,业务直接归零。\\n\\n解决方案:通过主从复制、集群化部署实现冗余。例如MySQL主从同步,Redis…","guid":"https://juejin.cn/post/7482993860878532635","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-19T01:28:49.466Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b15c767893f843229993fb8d7bd4ce2f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742952528&x-signature=Fi0DZ4f1Ls3obU2VF0jjmD%2F%2BPo0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc34190c3e634d38b2a12e1cb6475de4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742952528&x-signature=qV0yzNEdd2nHeY2QpW2ulIS8ltg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7dc7c9d991f144a6b5b90a16ca93e7d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742952528&x-signature=sYjFvTFiF8elknoRR2d%2FwzmsseY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"jdk24即将直播发布,抢先看新特性","url":"https://juejin.cn/post/7482966936516362259","content":"\\n\\n大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术
\\n
jdk24 23:00
将在YouTube
进行直播发布
我们来抢先看看jdk24
有哪些新特性
本次共有24个新特性。主要包含以下个方面
\\n类别 | 功能名称 | JEP 编号 | 状态 |
---|---|---|---|
语言增强 | 提前类加载和链接 | 483 | 正式 |
语言增强 | Class-File API | 484 | 正式 |
语言增强 | Stream Gatherers | 485 | 正式 |
语言增强 | Scoped Values | 487 | 第四次预览 |
语言增强 | 原始类型在模式、instanceof 和 switch 中的使用 | 488 | 第二次预览 |
语言增强 | Vector API | 489 | 第九次孵化 |
语言增强 | 灵活构造器体 | 492 | 第三次预览 |
语言增强 | 模块导入声明 | 494 | 第二次预览 |
语言增强 | 简单源文件和实例主方法 | 495 | 第四次预览 |
语言增强 | 结构化并发 | 499 | 第四次预览 |
性能优化 | 分代 Shenandoah | 404 | 实验性 |
性能优化 | 紧凑对象头 | 450 | 实验性 |
性能优化 | G1 晚期屏障扩展 | 475 | 正式 |
性能优化 | ZGC: 移除非分代模式 | 490 | 正式 |
性能优化 | 虚拟线程同步无锁定 | 491 | 正式 |
性能优化 | 无 JMOD 的运行时镜像链接 | 493 | 正式 |
安全增强 | 键派生函数 API | 478 | 预览 |
安全增强 | 量子抗性密钥封装机制 (ML-KEM) | 496 | 正式 |
安全增强 | 量子抗性数字签名算法 (ML-DSA) | 497 | 正式 |
安全增强 | 永久禁用安全管理器 | 486 | 正式 |
平台调整 | 准备限制 JNI 使用 | 472 | 正式 |
平台调整 | 移除 Windows 32 位 x86 端口 | 479 | 正式 |
平台调整 | 弃用 Linux 32 位 x86 端口 | 501 | 正式 |
平台调整 | 对 sun.misc.Unsafe 内存访问方法使用发出警告 | 498 | 正式 |
下面来具体看看
\\n该功能通过缓存已加载和链接的类,显著减少 Java 应用程序的启动时间。
\\n传统 JVM 在每次启动时都需要动态加载和链接类,这对启动时间敏感的应用程序(如微服务或无服务器函数)是一个瓶颈。
\\n提前类加载通过在第一次运行时缓存这些信息,之后启动时直接使用,特别适合短暂运行的程序
\\n提供标准 API 用于解析、生成和转换 Java 类文件,取代第三方库如 ASM,确保与 JVM 规范兼容。
\\n主要是为了简化字节码操作
功能:扩展Stream API支持自定义中间操作。
\\n示例:实现滑动窗口统计:
\\nList<Integer> numbers = List.of(1, 2, 3, 4, 5);\\nList<Integer> sums = numbers.stream()\\n .gather(Gatherers.windowSliding(2))\\n .map(window -> window.stream().mapToInt(i->i).sum())\\n .toList(); // 结果: [3, 5, 7, 9]\\n
\\n提供共享不可变数据的方法,降低线程局部变量的开销,适合并发环境
\\nimport java.lang.ScopedValue;\\nScopedValue<String> userName = ScopedValue.newInstance();\\nuserName.set(\\"Alice\\", () -> {\\n System.out.println(userName.get());\\n});\\n
\\n扩展模式匹配至原始类型,增强 instanceof 和 switch 的表达力。
\\nObject obj = ...;\\nif (obj instanceof int i) {\\n System.out.println(\\"It\'s an int: \\" + i);\\n}\\n
\\n允许利用 CPU SIMD 指令进行矢量计算,优化性能,适合数值计算。
\\n提升科学计算和机器学习性能
\\nimport jdk.incubator.vector.IntVector;\\nimport jdk.incubator.vector.VectorSpecies;\\n\\nVectorSpecies<Integer> species = IntVector.SPECIES_256;\\nIntVector vector = IntVector.fromArray(species, array, offset);\\nIntVector result = vector.add(vector);\\nresult.intoArray(array, offset);\\n
\\n允许在超类构造器前后添加代码,增强构造器灵活性
\\npublic class Sub extends Super {\\n int x;\\n { x = 10; } // 前导代码\\n public Sub() {\\n super();\\n System.out.println(x); // 后继代码\\n }\\n}\\n
\\n允许单条语句导入模块的所有包,简化模块使用。
\\nimport module com.xiaozoujishu.lib;\\n
\\n语法糖:允许省略static void main的显式声明。
\\n示例:
\\nvoid main() { // 自动识别为入口方法\\n System.out.println(\\"Hello, Simplified Main!\\");\\n}\\n
\\n将相关任务作为一个单元管理,简化并发编程中的错误处理和资源清理。
\\n主要是为了提高并发可靠性
\\nimport java.util.concurrent.StructuredTaskScope;\\n\\ntry (var scope = new StructuredTaskScope<String>().fork(new Task1()).fork(new Task2())) {\\n scope.join();\\n scope.resultStream().forEach(System.out::println);\\n} catch (Exception e) {\\n // 处理任务异常\\n}\\n
\\n增强 Shenandoah 垃圾回收器,引入分代模式,改善吞吐量和负载峰值抵抗力。
\\n将 64 位架构对象头从 96-128 位减少至 64 位,降低内存占用。
\\n通过在 C2 编译后期扩展屏障,简化 G1 执行,减少运行时间。
\\n移除 ZGC 的非分代模式,简化维护,推广分代模式。
\\n允许虚拟线程在同步代码阻塞时释放平台线程,提升可扩展性。
\\n减少JDK大小约25%通过jlink创建运行时镜像,无需 JMOD 文件。
\\n引入 API 支持密钥派生函数,如 HKDF 和 Argon2。
\\nimport javax.crypto.KDF;\\nKDF kdf = KDF.getInstance(\\"HKDF\\");\\nSecretKey derivedKey = kdf.extractAndExpand(new SecretKeySpec(secret, \\"HmacSHA256\\"), salt, 32);\\n
\\n提供量子抗性密钥封装机制,保护对称密钥。
\\nKeyAgreement ka = KeyAgreement.getInstance(\\"ML-KEM\\");\\nka.init(kp.getPublic());\\nbyte[] ciphertext = ka.generateSecret();\\n
\\n提供量子抗性数字签名,检测未经授权修改。
\\nSignature sig = Signature.getInstance(\\"ML-DSA\\");\\nsig.initSign(privateKey);\\nsig.update(data);\\nbyte[] signature = sig.sign();\\n
\\n禁用安全管理器,简化平台,移除相关引用。
\\n对 JNI 使用发出警告,准备未来默认限制。
\\n调用JNI_CreateJavaVM()
会触发警告
移除 Windows 32 位支持,简化构建。
弃用 Linux 32 位 x86 端口,计划在 JDK 25 移除。
\\n对已废弃的内存访问方法发出运行时警告,鼓励迁移至标准 API。
\\n使用 Unsafe.getInt() 会触发警告
\\nJDK
24在并发编程、性能优化和安全增强方面提供了显著改进.不过有很多功能是实验性和预览功能,线上需要谨慎使用
感兴趣的可以在23:00
观看发布直播
状态机用于描述一个系统在不同状态之间的转换和行为,是状态模式的一种具体应用。状态机是一种抽象的计算模型,它包含有限个状态和转换规则,用于描述系统在不同状态下如何响应输入以及在不同输入下如何进行状态转换。
\\n在我们日常开发中,我们提到的状态机基本都是有限状态机,用于解决状态相关的问题。有限状态机可以通过状态转换和事件触发来描述程序的行为和状态迁移
\\n一般我们在工作中,我们的主要的业务单据都会有一个状态的设计,我们会通过状态图画出他的状态流转情况,而控制这些状态如何流转,就是状态机干的事儿。
\\n状态机的实现,有很多种方式,可以用一些状态机的框架,如Spring StateMachine,也可以用状态模式,也可以自己封装一个工具类都行。
\\n如果没有一个严格的状态机控制的话,我们是可以随便修改订单的状态的,我们可以在已下单
状态直接推进到已发货
状态,这显然是不对的。
而状态机就是来控制这个状态的流转的他的目的都是把状态、事件、转换以及动作封装在一起的,他把这些东西内聚在一起了。有了它,一个已下单
状态的订单,只能通过支付
事件来驱动,并且还会有一些其他的约束,比如支付金额>0
(转移条件)等,然后他的下一个状态只能是已支付
这样的。
在我们的项目中,针对订单的状态流转做了状态的严格控制,并且自定义了一个简单的状态机设计。
\\n首先我们定义了一个接口,StateMachine,其中提供了一个状态转换的方法:transition。
\\n/**\\n * @author Aska\\n */\\npublic interface StateMachine<STATE, EVENT> {\\n\\n /**\\n * 状态机转移\\n *\\n * @param state\\n * @param event\\n * @return\\n */\\n public STATE transition(STATE state, EVENT event);\\n}\\n
\\n入参分别是STATE和EVENT。
\\n接下来定义了一个通用的状态机:
\\n\\n/**\\n * @author Aska\\n */\\npublic class BaseStateMachine<STATE, EVENT> implements StateMachine<STATE, EVENT> {\\n private Map<String, STATE> stateTransitions = Maps.newHashMap();\\n\\n protected void putTransition(STATE origin, EVENT event, STATE target) {\\n stateTransitions.put(Joiner.on(\\"_\\").join(origin, event), target);\\n }\\n\\n @Override\\n public STATE transition(STATE state, EVENT event) {\\n STATE target = stateTransitions.get(Joiner.on(\\"_\\").join(state, event));\\n if (target == null) {\\n throw new BizException(\\"state = \\" + state + \\" , event = \\" + event, STATE_MACHINE_TRANSITION_FAILED);\\n }\\n return target;\\n }\\n}\\n
\\n这里实现了transition接口,进行状态的流转判断。这个方法主要做的事情,就是去stateTransitions中查看是否有状态&事件对,如果有的话认为可以转换,没有则认为不能转换。
\\n接下我们看下OrderStateMachine的实现,这里只干了一件事,就是去初始化前面用到的stateTransitions这个 map。
\\n其实逻辑也很剪的,就是把能转换的原状态、事件和目标状态放进去。
\\n\\n/**\\n * @author Aska\\n */\\n \\npublic class OrderStateMachine extends BaseStateMachine<TradeOrderState, TradeOrderEvent> {\\n\\n public static final OrderStateMachine INSTANCE = new OrderStateMachine();\\n\\n {\\n putTransition(TradeOrderState.CREATE, TradeOrderEvent.CONFIRM, TradeOrderState.CONFIRM);\\n putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.PAY, TradeOrderState.PAID);\\n //库存预扣减成功,但是未真正扣减成功,也能支付/取消,不能因为延迟导致用户无法支付/取消。\\n putTransition(TradeOrderState.CREATE, TradeOrderEvent.PAY, TradeOrderState.PAID);\\n putTransition(TradeOrderState.CREATE, TradeOrderEvent.CANCEL, TradeOrderState.CLOSED);\\n putTransition(TradeOrderState.CREATE, TradeOrderEvent.TIME_OUT, TradeOrderState.CLOSED);\\n\\n //订单创建过程中失败,推进到废弃态,这种状态用户看不到订单\\n putTransition(TradeOrderState.CREATE, TradeOrderEvent.DISCARD, TradeOrderState.DISCARD);\\n putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.DISCARD, TradeOrderState.DISCARD);\\n\\n //已支付后,再确认,状态不变\\n putTransition(TradeOrderState.PAID, TradeOrderEvent.CONFIRM, TradeOrderState.PAID);\\n\\n putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.CANCEL, TradeOrderState.CLOSED);\\n putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.TIME_OUT, TradeOrderState.CLOSED);\\n\\n putTransition(TradeOrderState.PAID, TradeOrderEvent.FINISH, TradeOrderState.FINISH);\\n }\\n\\n}\\n
\\n比如,订单状态可以从 CREATE流转到 CONFIRM 状态,并且需要经过 CONFIRM 事件才能 。那么就:
\\nputTransition(TradeOrderState.CREATE, TradeOrderEvent.CONFIRM, TradeOrderState.CONFIRM);\\n\\n
\\n就 OK 了。这样我们在需要状态流转的时候,只需要告诉状态机,我当前的状态,和我本次事件,就可以通过状态机来控制是否可以转换,以及得到转换后的状态是什么了。
\\n如以下调用方式:
\\npublic TradeOrder confirm(OrderConfirmRequest request) {\\n this.setOrderConfirmedTime(request.getOperateTime());\\n TradeOrderState orderState = OrderStateMachine.INSTANCE.transition(this.getOrderState(), request.getOrderEvent());\\n this.setOrderState(orderState);\\n return this;\\n}\\n
\\n在 confirm 方法中。通过OrderStateMachine.INSTANCE.transition(this.getOrderState(), request.getOrderEvent());
来获取到目标状态,如果不报错,将返回一个目标状态,如果报错,则说明当前事件是不合法的。
通过这样的设计,我们可以严格的控制状态流转。并且业务操作时不需要关注具体的目标状态,只需要知道当前状态和事件就行了。
","description":"✅统一状态机设计 状态机用于描述一个系统在不同状态之间的转换和行为,是状态模式的一种具体应用。状态机是一种抽象的计算模型,它包含有限个状态和转换规则,用于描述系统在不同状态下如何响应输入以及在不同输入下如何进行状态转换。\\n\\n在我们日常开发中,我们提到的状态机基本都是有限状态机,用于解决状态相关的问题。有限状态机可以通过状态转换和事件触发来描述程序的行为和状态迁移\\n\\n一个状态机通常包含以下几个要素\\n状态(States):代表系统可能处于的各种状态,例如 \\"已下单\\"、\\"已支付\\"、\\"已发货\\" 等。\\n事件(Events):触发状态转换的事件,例如 \\"下单\\"、\\"支付…","guid":"https://juejin.cn/post/7482753184654737458","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-18T05:53:35.109Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c71ce1a2d4943acbaf64be15d31f067~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742882259&x-signature=lNzLAHSjPmiAZedDM59XzsFXDPs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"谈谈Select For Update的实现原理?","url":"https://juejin.cn/post/7482690481043275839","content":"文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\nMySQ技术内幕第5版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
SELECT FOR UPDATE
是 SQL 中的一种行锁机制,用于在事务中对查询到的数据行加锁。
它的作用是阻止其他事务对这些行进行修改或获取锁,通常用于需要保证数据一致性的场景。
\\n在 MySQL 中,SELECT FOR UPDATE
的具体实现与存储引擎(如 InnoDB)密切相关,其核心是利用 行锁(Row Lock) 来实现对目标行的加锁操作。
锁的类型:
\\nSELECT FOR UPDATE
会对查询到的行加 排他锁(Exclusive Lock, X 锁) 。适用范围:
\\nBEGIN
和 COMMIT
之间)。行为:
\\nSELECT FOR UPDATE
会加锁所有满足查询条件的行。如果查询没有使用索引,会退化为 表锁,即锁定整个表。读提交(READ COMMITTED) :
\\n可重复读(REPEATABLE READ) :
\\n串行化(SERIALIZABLE) :
\\n以下以 InnoDB 为例说明 SELECT FOR UPDATE
的执行流程:
事务开始:
\\nBEGIN
)。查询并加锁:
\\nSELECT ... FOR UPDATE
。数据操作:
\\n释放锁:
\\nCOMMIT
)后,释放锁。ROLLBACK
),同样释放锁。基本用法
\\nBEGIN;\\nSELECT * FROM orders WHERE order_id = 1 FOR UPDATE;\\n/* 对 order_id=1 的行加锁 */\\nUPDATE orders SET status = \'processed\' WHERE order_id = 1;\\nCOMMIT;\\n
\\n多事务竞争场景
\\n事务 A:
\\nBEGIN;\\nSELECT * FROM orders WHERE order_id = 1 FOR UPDATE;\\n-- 对 order_id=1 的行加锁\\n
\\n事务 B(在事务 A 未提交之前):
\\nBEGIN;\\nSELECT * FROM orders WHERE order_id = 1 FOR UPDATE;\\n-- 等待事务 A 释放锁\\n
\\n事务 B 在事务 A 提交或回滚之前无法获得锁。
\\n在 REPEATABLE READ
隔离级别下,范围查询可能触发间隙锁。例如:
SELECT * FROM orders WHERE order_id BETWEEN 10 AND 20 FOR UPDATE;\\n
\\n10
到 20
的范围加间隙锁,阻止其他事务在此范围内插入新记录。索引的使用:
\\n避免长事务:
\\n死锁检测:
\\n结合业务需求使用:
\\nSELECT FOR UPDATE
,避免不必要的锁竞争。SELECT FOR UPDATE
是 MySQL 中通过行锁机制实现的一种锁定操作,主要用于防止并发修改冲突。SELECT FOR UPDATE
可以有效保护数据一致性,但需要注意性能开销,避免锁竞争和死锁问题。\\n\\n最近在逛Github的时候,发现一个很有意思的项目
\\nawesome-deepseek-integration
,它是由DeepSeek官方维护的开源项目。如果你想找一些DeepSeek相关的项目,把DeepSeek快速使用起来的话,就可以看看这个项目,今天我们就来聊聊这个项目!
awesome-deepseek-integration
是DeepSeek官方维护的开源项目,里面涵盖了一系列集成DeepSeek的项目,目前在Github上已有27k+Star
!
里面包罗的项目很多,涵盖应用程序、AI Agent 框架、RAG 框架、即时通讯插件、浏览器插件、VS Code 插件等。
\\n\\n\\n这里我们就来看看里面包含有哪些项目。
\\n
\\n\\n这或许是一个对你有用的开源项目,mall项目是一套基于
\\nSpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!\\n
\\n- Boot项目:github.com/macrozheng/…
\\n- Cloud项目:github.com/macrozheng/…
\\n- 教程网站:www.macrozheng.com
\\n项目演示:\\n
\\n
\\n\\n这里我体验了几个项目,还是挺不错的,这里以Chatbox为例来演示下效果。
\\n
设置
中进行模型的设置,这里我使用的阿里云百炼中的DeepSeek模型,如图设置即可;显示
里设置;awesome系列确实是比较不错的资源,如果你想找Java类资源的话,可以搜索下awesome-java
。这些项目可以帮助你快速找到对应的开源项目,例如awesome-deepseek-integration
就能帮助我们快速找到DeepkSeek相关的项目。
dify官网:dify.ai/
\\ndify官网教程:docs.dify.ai/getting-sta…
\\n参考文档【如何集成Ollama】:docs.dify.ai/development…
\\ndify官网本地化部署ollama + deepseek教程:docs.dify.ai/zh-hans/lea…
\\nwin11 添加模型代理商报错,参考文档:www.bilibili.com/opus/104017…
\\n1、ollama 软件和模型,安装到D盘,不默认安装到C盘
\\n2、ollama 实现局域网访问
\\n3、ollama 下载模型 llama3.2:3b/deepseek-r1,并实现调用
\\n4、docker相关设置,更改docker Engine配置,更改docker镜像源存放地址,dify使用docker启动 ,dify相关环境变量更改
\\n5、dify注册管理员账号,dify保存模型代理商不成功/超时问题解决,以及plugin_daemon镜像源更改,IIS服务占据80端口问题
\\n6、dify保存模型代理商,跟网速、外网有关系,建议能用千兆网,就不要用百兆网,能有网线就不要用wifi,能用梯子就不要不用梯子( 如果你不借助fastgithub改变host文件的应用,访问github;很快的话,以下的安装将没有任何阻力,如果存在访问慢的情况,以下安装会存在问题)
\\n7、适当重启电脑,检查系统变量、环境变量、下载的模型;如果安装模型代理商不成功,请更改镜像源;多试几次
\\n• 操作系统:Windows 11 / Windows 10 相关
\\n• 部署工具:Docker Desktop + Docker Compose
\\n• 核心服务:
\\n• Dify:v1.0.0(本地Docker部署)
\\n• Ollama:v0.1.21(Windows原生安装)
\\n前端智能体Ollama + Deepseek AI 开发平台全链路设计与实践;LLM大语言模型有chatGPT , chatGPT-4,o1, o2, o3-mini,deepseek-r1,LLama
\\n模型应用发展阶段
\\n注册一个账户,官网:ollama.com/\\n
因为ollama是默认下载到C盘,以及后续模型下载到C盘,如果想将ollama以及其模型文件不默认安装到C盘,
\\n下载该文件,并安装,安装成功后,执行ollama -v 检查版本,ollama list 检查下载的模型\\n
【补充】因为ollama是默认下载到C盘的,所以模型也会下载到C盘,你将ollama应用安装到C盘中,但是模型想换个地方下载,所以需要提前设置环境变量,更多环境变量,请参考:blog.csdn.net/2501_905615…
\\n请参考下文,或者参考文档:zhuanlan.zhihu.com/p/226156184…
\\n当C盘很小,如果你将ollama安装到D盘中,又需要将models安装到D盘中
\\n执行命令 .\\\\OllamaSetup.exe /DIR=\\"D:\\\\ollama\\"\\n
请检查ollama -v 是否有版本号
\\n请检查D:\\\\ollama 是否存在文件\\n
环境变量设置;【OLLAMA_MODELS设置为D:\\\\ollama.ollama】更改ollama的模型的下载位置,需要提前建文件夹,请立即重启电脑,重启后生效
\\n我们先下载llama3.2这个模型
\\n执行下载命令,ollama run llama3.2:3b
\\n我们再需要下载deepseek-r1 , 7b这个版本
\\n通过cmd输入命令 ollama -h , 查看ollama相关命令;通过 ollama list 查看下载的模型
\\n通过命令 ollama run deepseek-r1:7b 命令【注意,不要直接执行ollama run deepseek-r1,后面还有:版本】,下载7b的模型
\\n当我们将模型下载完毕后,执行 ollama run deepseek-r1:7b 将模型启动起来
\\n【补充】ollama相关命令
\\n【补充】ollama 默认只能本机访问,如果需要局域网访问,请进行以下操作
\\n\\ndify,github地址:github.com/langgenius/…
\\ndocker官网:www.docker.com/
\\ndocker对windows有要求
\\n如何更改docker镜像文件默认存储路径,更改到D盘中
\\n更改docker engine 配置,因为docker下载的镜像使用docker默认配置的,可能安装不上\\n
{\\n \\"builder\\": {\\n \\"gc\\": {\\n \\"defaultKeepStorage\\": \\"20GB\\",\\n \\"enabled\\": true\\n }\\n },\\n \\"experimental\\": false,\\n \\"registry-mirrors\\": [\\n \\"https://2a6bf1988cb6428c877f723ec7530dbc.mirror.swr.myhuaweicloud.com\\",\\n \\"https://docker.m.daocloud.io\\",\\n \\"https://hub-mirror.c.163.com\\",\\n \\"https://mirror.baidubce.com\\",\\n \\"https://your_preferred_mirror\\",\\n \\"https://dockerhub.icu\\",\\n \\"https://docker.registry.cyou\\",\\n \\"https://docker-cf.registry.cyou\\",\\n \\"https://dockercf.jsdelivr.fyi\\",\\n \\"https://docker.jsdelivr.fyi\\",\\n \\"https://dockertest.jsdelivr.fyi\\",\\n \\"https://mirror.aliyuncs.com\\",\\n \\"https://dockerproxy.com\\",\\n \\"https://mirror.baidubce.com\\",\\n \\"https://docker.m.daocloud.io\\",\\n \\"https://docker.nju.edu.cn\\",\\n \\"https://docker.mirrors.sjtug.sjtu.edu.cn\\",\\n \\"https://docker.mirrors.ustc.edu.cn\\",\\n \\"https://mirror.iscas.ac.cn\\",\\n \\"https://docker.rainbond.cc\\"\\n ]\\n}\\n
\\npypi镜像源,下方安装python依赖的时候会用上:
\\ndify官网docker教程:docs.dify.ai/getting-sta…
\\n在启动dify之前,需要关闭任何可能会更改电脑host文件的程序,例如需要访问github的FastGithub.UI
\\n先进入项目中的docker文件夹下
\\n执行命令,copy .env.example .env
\\n执行命令启动,docker compose up -d;-d表示不看错误日志,或者使用docker-compose up 启动,使用完毕,执行命令关闭,docker compose down
\\n创建成功
\\n面板中可以看见容器已经启动了
\\n检查容器状态
\\n访问http://localhost/install ,先设置管理员账户
\\n注册好之后,登录进入http://localhost/apps:
\\n在diff-main/docker/.env文件下加入环境变量,我们这边先强制指定默认模型DEFAULT_MODEL=llama3.2:3b ; 如果需要自定义请打开CUSTOM_MODEL_ENABLED=true
\\n# Ollama服务地址(容器访问宿主机)\\nOLLAMA_API_BASE_URL=http://host.docker.internal:11434\\n\\n# 强制指定默认模型(需与Ollama模型名完全一致)\\nDEFAULT_MODEL=llama3.2:3b\\n\\n# # 启用自定义模型\\n# CUSTOM_MODEL_ENABLED=true\\n
\\n从Dify容器内部测试Ollama接口:docker exec -it docker-api-1 curl host.docker.internal:11434/api/tags
\\n效果如下,表示调用成功\\n
重新启动docker服务,访问http://localhost/apps ,在dify平台中引入 llama3.2:3b 模型
\\n\\n安装成功后,需要刷新一下,ollama插件安装不成功,就多试几次
添加ollama模型,关键的一步
\\n\\n其他默认,点击保存,等待接口models响应,这里有一个docker连接的问题,很容易卡在这里,因为下载一些外网的东西,models这个接口很容易不响应并报错
因为存在错误,参考文档:mbd.baidu.com/newspage/da…
\\n上述错误,描述的是插件守护进程超时导致api无法正常工作。所以需要替换源;如果你在使用Dify时遇到了“PluginDaemonInternalServerError: killed by timeout”的错误,可以通过设置以下两个环境变量来解决:
\\n你需要在docker-compose.yml
文件中新增以下配置:
plugin_daemon:\\nimage: langgenius/dify-plugin-daemon:0.0.3-local\\nrestart: always\\nenvironment:\\n # Use the shared environment variables.\\n <<: *shared-api-worker-env\\n\\n # 新增下面这两个\\n PYTHON_ENV_INIT_TIMEOUT: ${PYTHON_ENV_INIT_TIMEOUT:-640}\\n PIP_MIRROR_URL: https://pypi.tuna.tsinghua.edu.cn/simple\\n
\\n重新启动docker,重新进入http://localhost/apps,重新操作添加模型操作,等待models接口返回;如果清华的镜像源不行,则换其它的
\\n由上文得知,我们是强行强制指定默认模型llama3.2:3b ;如果添加其他模型,models接口一定会报错,如果我们想再加其他模型,需更改环境变量,如下图,重启docker服务
\\n\\n我们需要加的 deepseek-r1:7b 模型
只要我们上一次成功过,那么后续的模型添加就好说了
\\n如果你现在情况是不需要ollama本地化部署,那么我们可以通过相关模型平台的API Key实现模型API调用;这里我们以硅基流动为例
\\n安装硅基流动插件,并输入API key
\\n硅基流动登录:account.siliconflow.cn/zh/login?re…
\\n邀请码: Z6pZwzxp
\\n《我的第一本算法书》 pan.baidu.com/s/1Q0fd04pt… 提取码:h2xc
\\n等待其解析完成
\\n进入编排
\\n提示词编排:
\\nUse the following context as your learned knowledge, inside <context></context> XML tags.\\n\\n<context>\\n{{#context#}}\\n</context>\\n\\nWhen answer to user:\\n- If you don\'t know, just say that you don\'t know.\\n- If you don\'t know when you are not sure, ask for clarification.\\nAvoid mentioning that you obtained the information from the context.\\nAnd answer according to the language of the user\'s question.\\n\\n{{pre_prompt}}\\n{{query}}\\n\\n
\\n\\n输入问题,效果如下:
换成deepseek-r1
\\n有的模型在生成会话的过程中,会卡顿,生成的,因为我这台电脑是没有显卡的;这里AI应用的回答不一定能够满足用户需求,所以需要用户手动调整参数
\\n花了我3-4天的时间去研究,模型代理商保存卡了我很久
\\n如果对您有帮助,请给我点个赞吧。。。
\\n完结撒花。。。
","description":"一、前言 1.1、相关资料\\n\\ndify官网:dify.ai/\\n\\ndify官网教程:docs.dify.ai/getting-sta…\\n\\n参考文档【如何集成Ollama】:docs.dify.ai/development…\\n\\ndify官网本地化部署ollama + deepseek教程:docs.dify.ai/zh-hans/lea…\\n\\nwin11 添加模型代理商报错,参考文档:www.bilibili.com/opus/104017…\\n\\n1.2、总结解决的问题\\n\\n1、ollama 软件和模型,安装到D盘,不默认安装到C盘\\n\\n2、ollama 实现局域网访问\\n\\n3…","guid":"https://juejin.cn/post/7482582836706705435","author":"前端梭哈攻城狮","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T13:51:32.254Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b05b2bbfe784164af948968ee1461c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=5uSCJ4H34YNeFIYfa7gbdomr4uk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/abb05ca3a2f9491cbd47a243ddb3bb4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=GKPL9ppKwucIT6RIfSbit0S9VnY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce83f1f0bd47413bb926031025bf55cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=%2Fn0NUyFv0%2Bz1oHzRuMGmLtUDbS0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/36b5a46b677649ce93c4652cb65360ca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=7HsOPvbENLSwOesGXeVok7F3feE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b8c1961bebf242ebb57e2dff4aa446b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=4UVTqUtC2ZxtAY%2BPlqmaT3QCv%2FQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af407b62b2af4504bbffebdee2ffa022~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=d9Wm0eCk53zEJMeN6i4wOeHxwxk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a149b821e0b427cb1ed2676c8a04832~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=FeWSKcUjEjleAxi1mY9L9EJnPJI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/155b6c294b384a7594a0f639b3eb2c10~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=TTQoiI33oXqU6JDc8PZLai%2BitCU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c634e9710b24efbadc7e51aeb27e9a9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=OTMwIKtdVlGTFqQ7rYX4iJs2VZY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1a8c8093f91c499eaf63d668a5599bd4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=eC0FgTHpUlkRb36uv%2FZExdHMQ18%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a73856639d9d4fee8335910a9ea3df87~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=AoIdhy4kQ0XmbcqoOJkHhJlXEac%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6355c9ce11c44642affe91d8b572d311~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=QGcqKnUJtGjF%2FcoJH5aRMQuYZyI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2f2782228ccc4d4794ec0a1c04d7c7b1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=d1mhLXt0n3ARG%2FO4tC6wWv%2Brz5A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14c3b651b0ab4ba9859ea59dcbe4668b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=plclJ5bX29mn9tTQigP1wn7YuAs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91426b0dea6b43bdacb20de66d0edfec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=BDWxK2vN7Ju5xgRVO1r9IWqdMns%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed19c5e052684d2a80050441b10bee0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=vrlCqnVb%2FDF8vjKDTUG4gaX1fPs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c53e41c0121940698a0d5ae07f700b85~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=UysRyU8z%2F866lGVze0ZsyvAA9%2FU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9981d8102e7345a5a24c833e559082dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=Hhm8Wt6Xyih3U8sublFnslXM8vU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cacf604f09794957a4c2ec6e2ed9a170~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=GRIoBI%2BTadvFnDTeNCaEM70eiS4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a9265b537e54166973f77a520f3730e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=%2FhIJndwO3LdIyhD%2B84cKwbo2GsE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a0ac7f7bb99e418a9991db23693e83e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=aLahaC1i4L6uC7xpy9AyHLMygLc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a0da94dd68b04c93b63e3672384ca096~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=AEEGkyvXt1pT1a%2BA75i85JuPXgY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3cd2887a88694001831aaa8b4f640d7f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=fSVT4eE%2F65OtD6SF%2FsJdYBk2MjA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/262798287f784aba80d0a5d96ad7a356~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=TeoaozHuZzwLIk%2BAi3taBpam8cs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3dc65f0d3f714968bfc60b130a047a61~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=QrmhAkgkpfeMVbeP7eAmf57ohgA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d10c3e88454544a6bbfdcb825053306c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=czct69YD65NU3pcm%2BHl%2FKlwHAh8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e021447ba87f451ea41932b9effb876a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=lUxZmwFm0V7SSJpUiq2%2BnwYLo2g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cf52598405f547808030d05607fdb2bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=B2pUgSq51mDSxRv6WjCGd5tbCJM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/efa096804f354bbeb56755b755c1f399~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=G7TPKnIHG4YgHaBDtL5M0GDd8i0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da7030864ae7437b9d3bc6dc1e6a84e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=0lllS9%2BGQ3GPBTA7XurZxx6Tnwo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2015ac481d2744ecbb5cac39c201873b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=JXLfFin0bggPRt2OHQuljjiPZBE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a433f8d4dc8c4b52b14bee4aee1ca7b7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=NmViIIx6gyTdDJfvAY5H7evV6U0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a5fe1c2c5464b70a47a04e191cda183~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=JOJirjnG3PuVbcKIBkDpLh7BM7o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d163e63a7bfe439da76efe523d51d082~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=GFmTfJTr%2FLBCLcXOyZUcNAHC9i0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7d5f550b0dcb48878921c1c33f68e9bc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=ESNsj4FI1LJW%2Fd51URhdRI3x4iw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/929663235cb749b49fb4b16d16b635c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=5z%2ByxAYyNe3Doyb9QE3eeYfU9EU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b1626be09b04437948eeee6a99c5f82~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=6M%2Byqj%2FPzXTLh%2B6K7qYnGtadBBI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bb424b5dc7384959a2099d043b649b87~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=qt0KQqQ6F1LrYLWxJvqgEVJzVIM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a8f629ed8f14c91ad69f70a0662cd75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=WULuKIBBZ8AHkWWTF6juS1KHclk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ee7982a33bf6413890f8dfeb4ebe5b0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=REqKQ5GyO3HGCUlgBOWgQqTZz10%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a32918f7e60f4cd6981484d5d66f3995~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=4JzWuDxURHOdvv%2FG64tGtboCVWA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af49aa2c14a841a3bb9bbeadbb1edcbb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=BXOgNlOIYYh%2FSqQWPku4MfpCtRM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/96dd44cd6c0d4a8cb3742a0313df5c56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=N0EmVFvnlYnmiww1hFbqOeVGEVM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6b22b0578b6547f5aa10a5fb278f80f2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=5%2BdeOdZC7ED2bG%2BTfads4kI2FE8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a7395cbf39c4003b3cf8924f23042d8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=vamVYebT2CGnW6W2uIC15LdUSf0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa3b6064e0f4430c98481eeee36eeec2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=tyPx3FARQ2nHmsAHioq0YS7jNWg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dbe0d1f390e5413c9afa6e6a1a505c5c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=NuSvkX%2FNsRLRWDqqRc6HZbsejQc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1f0e07d41c64a6f9e6b71c5b7204313~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=saklPIbCfrjI8q7zVEOOJQ0ClOc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4da9a9e097f441a7ae281e122fe264a6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=zSAyWR7mjohJGdNCs7%2BQVS%2BBRyY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9ba7007978c144f8a7ff9c0b3ee85d68~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=wbu3llqqTTeSag5Ov6wK4eBiePc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2e4b928139084450a14b7b0f0f068df3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=yz%2FVZQVyUDqCIuvJL%2BPqG7kj%2F0U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/700f69a799d24e10bc7a78dd294b0186~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=gy2D4NG10kSjVLfgUM5bVTuxu8I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/790a50b9df0c46c2bb979d9b238a344a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YmN56uv5qKt5ZOI5pS75Z-O54uu:q75.awebp?rk3s=f64ab15b&x-expires=1742952251&x-signature=N7HH3%2FLh9bCyhbHAvI8TkfpQY5E%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Ollama","DeepSeek","AIGC"],"attachments":null,"extra":null,"language":null},{"title":"分享一个我遇到过的“量子力学”级别的BUG。","url":"https://juejin.cn/post/7482584165991219226","content":"你好呀,我是歪歪。
\\n前几天在网上冲浪的时候,看到知乎上的这个话题:
\\n一瞬间,一次历史悠久但是记忆深刻的代码调试经历,“刷”的一下,就在我的脑海中蹦出来了。
\\n虽然最终定位到的原因令人无语,对于日常编码也没啥帮助,但是真的是:
\\n我记得当时我是学习 ConcurrentLinkedQueue (下文用 CLQ 代替)的这个玩意,为了比较深入的掌握这个玩意,我肯定是要 Debug 跟踪一下源码的。
\\n问题就出现在 Debug 的时候,现象非常诡异,听我细细道来。
\\n首先,我当时的 Demo 极其简单,就这么两行代码:
\\nnew 一个 CLQ 对象,然后调用 offer 方法筛一个对象进去。
\\n完事了。
\\n这么简单的代码能搞出什么牛逼的玩意呢?
\\n首先,我带你看看 CLQ 的数据结构。
\\nCLQ 是由一个个 Node 组成的链式结构。
\\nnew CLQ 的时候通过 new Node() 构造出一个特殊的“dummy node”,翻译过来大家一般叫它“哑元节点”。
\\n然后将头指针 head 和尾指针 tail 都指向这个哑元节点。
\\n那这个 Node 长啥样呢?
\\nNode 里面有一个 item(放的是存储的对象),还有一个 next 节点(指向的是当前 Node 的下一个节点):
\\n从数据结构来看,也知道这是一个单向链表了。
\\n当时为了学它,我想通过日志的方式直接输出链表结构,这应该是最简单的演示方式了。
\\n毕竟 Java 程序员,就靠日志活着了。
\\n所以我当时自定义了一个 WhyConcurrentLinkedQueue(下文简写为 WhyCLQ)。
\\n这个 WhyCLQ 是怎么来的呢?
\\n非常简单,我直接把 JDK 源码中的 CLQ 复制出来一份,改名为 WhyCLQ 就完事了。
\\n然后搞个测试用例跑跑:
\\n非常 nice,没有任何毛病。
\\n我们现在可以任意的在代码中增加输出日志了。
\\n比如,我想要看 WhyCLQ 这个链式结构到底是怎么样的。
\\n我们可以在自定义的 CLQ 里面加一个打印链表结构的方法:
\\npublic void printWhyCLQ() {\\n StringBuilder sb = new StringBuilder();\\n for (Node<E> p = first(); p != null; p = succ(p)) {\\n E item = p.item;\\n sb.append(item).append(\\"->\\");\\n }\\n System.out.println(\\"链表item对象指向 =\\" + sb);\\n}\\n
\\n然后在每次 offer 方法新增完成后,调用一下 printWhyCLQ 方法,输出当前的链式结构:
\\n其他的地方类似,只要你觉得源码看起来有点绕的地方,你就可以加输出语句,哪怕一行代码就配上一行输出语句也没问题。
\\n甚至,你还能“客制化”源码,但是这不是本文的重点,我就不展开了。
\\n通过复制源码的方式自定义一个 JDK 源码中的类,然后加上大量的输出语句,有时候也会对源码进行各种改装,是我常用的一个学习小技巧,分享给你,不用客气。
\\n当你被一步步 debug 带晕的时候,你可以试一试这种方式,先整体再局部。
\\n好,到这里就算是铺垫完成了。
\\n我们回到最开始的这两行代码:
\\n按照我们的理解,第一次 offer 之后,对应的链表画个简图应该是这样的:
\\n但是最后的输出是这样的:
\\n为什么输出的日志不是 null->@4629104a 呢?
\\n因为我们自定义的 printWhyCLQ 这个方法里面会调用 first 方法,获取真正的头节点,即 item 不为 null 的节点:
\\n也就是我框起来的地方:first 方法中的 updateHead(h, p) 方法,会去修改头结点。
\\n然后,我还想在第一次 offer 的时候,详细的输出头结点的信息,所以加了这几行输出语句:
\\n直接把程序跑起来,对应的效果是这样的:
\\n但是,当我在这个分支入口,打上断点,用 debug 模式进行调试的时候:
\\n运行结果是这样的:
\\n空指针异常!!!???
\\n为了让你有更加直观的感受,我给你上个动图。
\\n首先,是直接把程序运行起来的动图:
\\n这是 Debug 运行时的动图:
\\n如果前面的文字你没看懂,不重要,你只需要记住下面这个现象:
\\n同样的程序,当你直接运行,就能正常结束,当你用 Debug 模式运行的时候,就会抛出空指针异常。
\\n来,如果是你遇到这个问题,你会怎么办?
\\n当年我还是一个萌新菜鸟的时候,遇到这个问题,直接就懵逼了啊,百思不得其解,感觉编程的大厦正在摇摇欲坠。
\\n这真的就很诡异啊!
\\n当你直接运行程序,会拿到一个预期的结果。
\\n但是试图通过 Debug 模式去观察这个程序的时候,这个程序就会抛出异常。
\\n这很难不让人想起“量子力学”中的光的双缝干涉试验啊。
\\n观测手段触发了光的粒子状态,所以没有干涉条纹。
\\n如果不观测,光就是波的形态,出现了干涉条纹。
\\n如果你不知道我在说什么,一点也不重要。
\\n但是你知道我在说什么,你就知道,歪师傅这个程序的现象,用“量子力学”来形容是多么的贴切。
\\n我甚至还怀疑过是质子,一定是质子在搞事情。
\\n当时,我是怎么解决这个问题的呢?
\\n没有解决。
\\n当年经验浅薄,现象又太过诡异导致我不知道应该怎么去解决,而且最重要的是并没有影响我理解 CLQ 这个玩意。
\\n是的,感谢我当时还记得主要目标是去学习 CLQ,而不是去研究这个诡异的现象。
\\n我忘了隔了多长时间,只记得是一个麦子黄了的季节,我在这个链接中偶遇到了真相:
\\n\\n\\n\\n
这个哥们遇到的问题和我一模一样,但是这个问题下面只有一个回答:
\\n这个回答给出的解决方案
\\n最后的解决方案就是关闭 IDEA 的这两个配置,他们默认是开启的:
\\n当关闭这两个配置后,我的程序在 Debug 的时候也正常了。
\\n为什么呢?
\\n因为 IDEA 在 Debug 模式下会主动的帮我们调用一次 toString 方法。
\\n而在 CLQ 的 toString 方法里面,会去调用 first 方法:
\\n前面我说了:first 方法中的 updateHead(h, p) 方法,会去修改头结点。
\\n之前我给的简图是这样的:
\\n由于 Debug 会调用 toString 方法,从而触发了 first 方法,进而导致了头结点不是 null,而是这个 obj 了:
\\n再到 this.head.next 这里获取头结点的 next 的时候,由于 next 并不存在,值为 null:
\\n所以 this.head.next.item 抛出了空指针异常。
\\n没有什么玄学,我们要相信科学。
\\n但是,这个真相确实有点坑。
\\n那么问题就来了。
\\n为什么 IDEA 要在 Debug 的时候默认调用一下 toString 方法呢?
\\n我用 HashMap 举例,给你上个对比图你就知道它想要干啥了。
\\n这是默认配置的情况:
\\n可以直观的看到 map 中 key 和 value 的情况
\\n当我们取消前面说的配置:
\\n再次 Debug 的时候,看到的就是这样的:
\\n而且可以看到,toString 方法是可以点击的。
\\n当你点击之后,就变成了这样:
\\n这么一对比,就很直观了。
\\n你说 IDEA 图啥?
\\n还不就说图用户调试起来的时候,看起来更加直观嘛,确实是一片好心。
\\n谁能想到你 toString 方法中还能藏着一些逻辑呢。
\\n这波我站 IDEA。
\\n通过前面的介绍,我仿佛又掌握了一个埋坑的小技巧。
\\n我给你演示一下。
\\n首先我定义一个 why 的类:
\\n这个类的 toSting 方法中有 age++ 这样的操作。
\\n当你直接运行这个程序的时候,运行结果为 18:
\\n但是,当你 Debug 的时候:
\\nage 就变成 19 了。
\\n而且是看一次,就涨一岁,这你受得了吗:
\\n如果代码再复杂一点,找问题都让你焦头烂额了。
\\n谁能想到 IDEA 在你 Debug 的时候帮你调用了 toString,谁又能想到 toString 方法中还有逻辑呢?
\\n如果 toString 方法中的逻辑,和前面说的 CLQ 一样,会影响到你要寻找的答案...
\\n这一套丝滑小连招下来,你就玩去吧。
\\n一个埋坑的小技巧,没到血海深仇,不要轻易使用。
\\n最后,你说上帝在编程的时候,会不会也是埋了这样的一个坑。
\\n当我们直接运行“光”这个方法的时候,光就是波的形态。
\\n但是当我们使用通过观察手段去 Debug “光”这个方法到底是怎么运行的时候,上帝他老人家就会在“光的 toStirng 方法”中主动调用一个让光变成粒子的逻辑。
\\n所以,我们的任何观测手段都会触发这个“光的 toStirng 方法”,导致光的出现了粒子状态,在光的双缝干涉试验直接中,就没有出现干涉条纹。
\\n从编程角度,看量子力学,有点意思。
\\n最后,欢迎关注公众号why技术,全网首发。
","description":"你好呀,我是歪歪。 前几天在网上冲浪的时候,看到知乎上的这个话题:\\n\\n一瞬间,一次历史悠久但是记忆深刻的代码调试经历,“刷”的一下,就在我的脑海中蹦出来了。\\n\\n虽然最终定位到的原因令人无语,对于日常编码也没啥帮助,但是真的是:\\n\\n情景再现\\n\\n我记得当时我是学习 ConcurrentLinkedQueue (下文用 CLQ 代替)的这个玩意,为了比较深入的掌握这个玩意,我肯定是要 Debug 跟踪一下源码的。\\n\\n问题就出现在 Debug 的时候,现象非常诡异,听我细细道来。\\n\\n首先,我当时的 Demo 极其简单,就这么两行代码:\\n\\nnew 一个 CLQ 对象…","guid":"https://juejin.cn/post/7482584165991219226","author":"why技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T11:58:43.526Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91bb4a21c9624589974ed09583400a0e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=tmYdSDWb22KhCyYP7e%2BSUr4r5HI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14f431b8735342b3a4435fde69c368fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=jd91QzydMq0NetwMoesVPfLY9Cw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9ba94ff1ce8c480c8adc93596fc69ecc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=7gYRwRy8MD6OqsoU9P%2FCmoWsZ5g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6571b1484d44cac8988548befaac5aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=aHJN%2B1LtmxnPkju7DzvSmjdsnNk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d9cdfd0a20d4ae487f23cadae53e548~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=GenkIAgF7gs6VLQp1SvTYacbYnw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4f7a2c1c3b424f3bb6466887eccb23f3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=zw59pdIzH8kay443Vxu2h95fYs4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/28a5199f3c714622af3c4e7e8e95ba43~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=ivtiFw0ek5%2BCKUtkoNmUz8m1Hq0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/543449ef68d14c6f8b0185d894113b96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=Vyv5LCUVOyhL4kJHqVMGnEoKows%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ee8434dc911e43d0b820690b4273b545~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=AUm9yi%2BLQhf7AwCDEaNfcRICfNI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/85502ab0cc1b4fb19ef46f2e7ef71485~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=Z6Z79SSzLxCDm1638%2BZOPA8fWGQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4fcda4c4d357478bbfa64240a7c1faf6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=dzFJI3W8qJzzR8K36NxW3MDg75c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2fbb80a9abd345ffa5a5df789761f65b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=0X%2FWtqZKn0cjiTaDEgxxBK8Ft4U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d10d16275f4c403695a80c8227fe6231~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=Xce9SWSW04xzdGEghhao1LJ3IJg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f19015d45e2b4811beaccb2ac2116db6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=4lYjfHFYvjQbi5B2A7Hy4eYxonI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f1e7d9af0e12413bbe5ad6b4106e1a83~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=IZU1CArCjtfjN6HcZqhoeskBgEM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/22a6a9441e8f4f2ea3e3adafc0cda0dd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=DBZ88Ddk1%2BfHCZUufWXRULPOJrA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/51402c3766424bfaa20eb57e7e8ae1fe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=wIDpQFQtysr9Zr71iPYc%2ButvwGg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15c26f4d2f02472ba112ccf6488a1826~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=DjTdVELO25RsuPHoj%2BsuNC%2BC4uw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14fbbf0d22fd4e2abe6ba8a409635ac3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=CIR1jSf7kYmj4jNVAxRtepogejU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ece950cbe4064dc8a0d007ef1a5aec08~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=9Eiw5Ffy3GyeGJiIlh2APuHCgpI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9290cb8552f0497cabe4dbfcb78ca3b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=bGcDPepe0mb7b5c08nij2Y%2FKerM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c4f310eb86ee4dbd8d3495f6f4040869~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=HGmLQXDzdQ18x2w0lE4g79JuE4k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/90fe640a5d764c4991aa8bcb282e6514~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=X6amHyy6khW0IT9iHirxt2pazv8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/00bf67857fa34fabb22f7b12c34ec132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=5r35FEaKrkhZRHm%2F0fGy29w20qY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/329a2736893342a5bd0ab30a43c1d035~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=Ng48YJabUAHuG8w5%2Fp%2BtKcLenOk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a4a26bf62fd948639f6c90bc8908ef9b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=1pZSuo5343pEmRsm4PlcOYgr1bA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f7c4324d7b8c470c9c70c35708b87776~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=%2FlJGzfW5SiZzqhaSS8cqpZetUXQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/42dd896d0fa64c4da1eba7eeb1bce7cc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=Zvc4gHh%2FRbtg5G1SfHMTVMq1%2Bco%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8ee7004fcfc84e4e880c60b4cf650a05~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=v6YFqnouon7s8dT91NLpFZnKUW4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3633ed31d3c6486f8ba8bd36999c036c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=OsEJka7XuvbazuOFxwZKngzppIg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/30902bcfe96d4e249c002dd8689df972~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=HjFfzAD7gaQylhoBLlMcYVpJezI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a292be1ef02464e81d2374cd9df80c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=s3P3ZmC4Q%2BtgBmZeztrwDoidg04%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b1e2c7db66bb40dda084ce03f6e7a387~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=g2G23T9u5Vi128kW3gD6Nd2GNI0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f1c74670960a46d684636768e336eb4c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=TPVvSG1tjBq1s09PagQyPMlYO%2B4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/291b0f5765c040799922a04e4e9b5539~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=lDgwkHQg5Blh8cS%2BPvPJwhIvRnY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b576cab313b435aa6837dde9b8e5f25~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=kkDGf%2B%2BNbgFfJC6GVI80xHgRMGk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d981e56e5f2b400d9d21eced95c7742f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=yRt9gvr8v3Q8Ma%2F7o%2B0tA4Vbabs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd29b9976ad649d59756703df0ec6b9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2h55oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742817523&x-signature=R7EdSYQetoHeg0zMWUlc3AAA8Do%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"对接第三方接口不稳定经常超时---如何处理","url":"https://juejin.cn/post/7482582836705837083","content":"典型回答
\\n这种情况还挺常见的,我们经常需要调外部的服务,但是有的时候外部服务又不稳定,经常会失败或者超时,那么我们怎么避免因为他们的超时而影响到我们自己的接口呢?
\\n有以下几种办法可以选择。
\\n对于一些可以异步请求第三方的场景,我们应该尽可能用异步的方案,包括MQ,异步线程,甚至是离线文件同步等方案。这种异步的方案我们用的非常多。
\\n异步的话至少能保证我们给上游的返回是不受影响的。但是异步的话需要注意的是,我们需要有个收单的过程,就是别人请求过来的时候,我们做一下收单,然后收单成功之后就给上游返回成功,然后再异步处理。如果异步失败了,则根据收单记录进行重试。
\\n这样做的话,还需要引入一个回调或者反查机制,因为我们只是告诉上游收单成功了,是否真正成功他是不知道的,所以需要有个回调或者反查的机制。
\\n很多人看完之后一定会觉得复杂,但是其实大家对接过很多第三方的话就能发现,其实他们很多接口也都是这么做的,都是先收单,然后处理结果有了之后在回调,或者需要我们主动反查。
\\n这是一种非常常见的做法,目的就是提升整体的吞吐量,以及减少对下游的强依赖。
\\n如果是同步调用,也不是没有办法,那就是我们设定一个超时时间,不管下游需要多少秒,我们执行的时候,如果超过我们给定的时间,就直接结束请求。避免被下游给拖垮,比如以下做:
\\n在我们发现超时之后,可以通过一些降级手段做返回,比如如果是查询接口的话,可以返回默认值,或者上次查询结果的值都可以。如果是写操作稍微麻烦一点,就需要约定好一些超时的处理策略,以及引入幂等+重试的机制了。
\\n但是不管怎么样,这个超时机制还是非常有必要的,可以在关键时刻救命。
\\n如果大家知道熔断的话,就会知道,我们可以借助熔断器实现当第三方接口的错误率或超时次数达到一定阈值时,停止请求该接口一段时间(熔断状态),然后逐渐恢复流量。
\\n我们可以借助hystrix、sentinel等工具帮我们实现快速熔断,这样就能很好的避免我们自己被拖垮,也可以避免给下游造成进一部分压力。
\\n大家知道最开始余额宝被做出来主要是干嘛的吗?其实最初是为了解决支付成功率低的问题的。
\\n大促期间就是因为银联等外部渠道支付成功率太低了,但是很多人又因为银行有收益所以把钱存在银行。
\\n那为了解决这个问题,一个好的办法,就是在业务上给用户多一个选择,让用户减少使用银行卡支付的概率。
\\n就假如说,高德聚合打车,发现外部服务商技术都太差了,他做了自营,是不是也能大幅度降低对外部机构的依赖问题呢?
","description":"✅第三方接口不稳定经常超时,如何处理三方接口异常不影响自己接口 典型回答\\n\\n这种情况还挺常见的,我们经常需要调外部的服务,但是有的时候外部服务又不稳定,经常会失败或者超时,那么我们怎么避免因为他们的超时而影响到我们自己的接口呢?\\n\\n有以下几种办法可以选择。\\n\\n异步处理\\n\\n对于一些可以异步请求第三方的场景,我们应该尽可能用异步的方案,包括MQ,异步线程,甚至是离线文件同步等方案。这种异步的方案我们用的非常多。\\n\\n异步的话至少能保证我们给上游的返回是不受影响的。但是异步的话需要注意的是,我们需要有个收单的过程,就是别人请求过来的时候,我们做一下收单…","guid":"https://juejin.cn/post/7482582836705837083","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T09:41:25.786Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"要价 3k 的应届生,何去何从?","url":"https://juejin.cn/post/7482584165990613018","content":"前两天面试一个应届生,各方面能力还行,能用的水平,一看期望薪资 3k,果断放弃。
\\n原来 3k 找不来一个清洁工,但能找来一个大学生不是段子。我在武汉,这里大学非常多,但大学生廉价成这样还是超出想象了。
\\n聊聊我为什么不要这个应届生。
\\n要价太低
\\n没错,要价太低意味着对自己非常不自信,对自己不自信,就认为自己就值个 3k,我对你怎么有信心?
装逼得逼,求仁得仁,你觉得自己只值 3k,那你就只值 3k,你觉得自己 8k,那你就可以拿到 8k,气场很重要。
\\n薪资要得高的,一般对自身实力有把握,我面试后发现的确有两把刷子,这种入职成功率就很高了。
\\n事实是我们要了那个面试要 9k 的,他自己写了两个项目,对自己做的项目有思考。
\\n无法担事
\\n3k 的工资,真招你进来了,给你点有压力的事,或者叼你两句,你不就直接跑路了吗?
你跑路了,出事的不就是我了?不是说一定会跑路,但 3k 和 8k 比起来,跑路的风险更大。
\\n应届生要搞清楚招你进来是干活的,不是搞学习的,也别傻傻在日报里面写学了这学了那,就写和工作、业务相关的就行了。学习是自己私底下悄悄做的,不要抬到明面上。
\\n所以我招你过来,一定是我手上的事非常多,我要分担一部分给你。
\\n在这个过程中我也有成本和风险,我要对你进行培训,需要花时间精力,这是成本;我还要思考你能不能扛住压力,扛不住你就跑了,这是风险。
\\n总结一下。
\\n在分布式系统中由于相关联的多个服务所在的数据库互相隔离,数据库无法使用本地事务来保证数据的一致性,因此需要使用分布式事务来保证数据的一致性
\\n比如用户支付订单后,需要更改订单状态,还需要涉及其他服务的其他操作如:物流出货、积分变更、清空购物车等
\\n由于它们数据所存储的数据库会互相隔离,当订单状态修改成功/失败时,其他服务对应的数据也需要修改成功/失败,否则就会出现数据不一致的情况
\\n解决分布式事务常用的一种方案是使用MQ做补偿以此来达到数据的最终一致性,而RocketMQ提供的事务消息能够简单、有效的解决分布式事务满足数据最终一致性
\\n在上面支付订单的案例中,主分支只需要修改订单状态,其他分支(出货、积分变更、清空购物车)都可以发送事务消息来达到数据最终一致性
\\n本篇文章通过分析源码来描述事务消息的原理以及使用方法,并总结使用时需要注意的地方,思维导图如下:
\\n\\n\\n\\n\\n\\n\\n往期回顾:
\\n
RocketMQ(六):Consumer Rebalanc原理(运行流程、触发时机、导致的问题)
\\n\\n\\nRocketMQ(三):面对高并发请求,如何高效持久化消息?(核心存储文件、持久化核心原理、源码解析)
\\nRocketMQ(二):揭秘发送消息核心原理(源码与设计思想解析)
\\nRocketMQ(一):消息中间件缘起,一览整体架构及核心组件
\\n事务消息拥有“半事务”的状态,在这种状态下即时消息到达broker也不能进行消费,直到主分支本地事务提交,事务消息才能被下游服务进行消费
\\n使用事务消息的流程如下:
\\n发送事务消息的生产者为TransactionMQProducer,TransactionMQProducer的使用与默认类似,只不过需要设置事务监听器TransactionListener
\\n事务监听器接口需要实现executeLocalTransaction用于执行本地事务和checkLocalTransaction用于broker回查本地事务状态
\\n public interface TransactionListener {\\n //执行本地事务\\n LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);\\n //回查事务状态\\n LocalTransactionState checkLocalTransaction(final MessageExt msg);\\n }\\n
\\n它们的结果LocalTransactionState有三个状态:COMMIT_MESSAGE 成功、ROLLBACK_MESSAGE 失败、UNKNOW 未知
\\n当为未知状态时,后续还会触发回查,直到超过次数或者返回成功/失败
\\n调用 sendMessageInTransaction
发送事务消息,其中参数arg用于扩展,执行本地事务时会携带使用
public TransactionSendResult sendMessageInTransaction(final Message msg,final Object arg)\\n
\\n根据我们的情况写出TransactionListener的模拟代码
\\n public class OrderPayTransactionListener implements TransactionListener {\\n //执行本地事务 其中参数arg传递的为订单ID\\n @Override\\n public LocalTransactionState executeLocalTransaction(Message msg, Object orderId) {\\n try {\\n //修改订单状态为已支付\\n if (updatePayStatus((Long) orderId)) {\\n return LocalTransactionState.COMMIT_MESSAGE;\\n }\\n } catch (Exception e) {\\n //log\\n return LocalTransactionState.UNKNOW;\\n }\\n return LocalTransactionState.ROLLBACK_MESSAGE;\\n }\\n \\n \\n //回查状态\\n @Override\\n public LocalTransactionState checkLocalTransaction(MessageExt msg) {\\n Long orderId = Long.valueOf(msg.getBuyerId());\\n //查询订单状态是否为已支付\\n try {\\n if (isPayed(orderId)) {\\n return LocalTransactionState.COMMIT_MESSAGE;\\n }\\n } catch (Exception e) {\\n //log\\n return LocalTransactionState.UNKNOW;\\n }\\n \\n return LocalTransactionState.ROLLBACK_MESSAGE;\\n }\\n }\\n
\\n执行本地事务时如果成功修改订单状态就返回commit,回查状态时判断订单状态是否为已支付
\\n前文分析过通用的发送消息流程,而 sendMessageInTransaction
发送消息调用通用的发送消息流程外,还会在期间多做一些处理:
sendDefaultImpl
(校验参数、获取路由信息、选择队列、封装消息、netty rpc调用,期间检查超时、超时情况)executeLocalTransaction
endTransactionOneway
(有回查机制无需考虑失败) public TransactionSendResult sendMessageInTransaction(final Message msg,\\n final LocalTransactionExecuter localTransactionExecuter, final Object arg)\\n throws MQClientException {\\n //检查事务监听器\\n TransactionListener transactionListener = getCheckListener();\\n if (null == localTransactionExecuter && null == transactionListener) {\\n throw new MQClientException(\\"tranExecutor is null\\", null);\\n }\\n //清除延迟等级 使用事务消息就不能使用延迟消息\\n // ignore DelayTimeLevel parameter\\n if (msg.getDelayTimeLevel() != 0) {\\n MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);\\n }\\n //检查消息\\n Validators.checkMessage(msg, this.defaultMQProducer);\\n SendResult sendResult = null;\\n //标记事务消息为半事务状态\\n MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, \\"true\\");\\n //存储生产者组\\n MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());\\n try {\\n //通用的发送消息流程\\n sendResult = this.send(msg);\\n } catch (Exception e) {\\n throw new MQClientException(\\"send message Exception\\", e);\\n }\\n \\n LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;\\n Throwable localException = null;\\n switch (sendResult.getSendStatus()) {\\n case SEND_OK: {\\n try {\\n if (sendResult.getTransactionId() != null) {\\n msg.putUserProperty(\\"__transactionId__\\", sendResult.getTransactionId());\\n }\\n String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);\\n if (null != transactionId && !\\"\\".equals(transactionId)) {\\n msg.setTransactionId(transactionId);\\n }\\n if (null != localTransactionExecuter) {\\n localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);\\n } else if (transactionListener != null) {\\n log.debug(\\"Used new transaction API\\");\\n //成功执行本地事务\\n localTransactionState = transactionListener.executeLocalTransaction(msg, arg);\\n }\\n if (null == localTransactionState) {\\n localTransactionState = LocalTransactionState.UNKNOW;\\n }\\n \\n if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {\\n log.info(\\"executeLocalTransactionBranch return {}\\", localTransactionState);\\n log.info(msg.toString());\\n }\\n } catch (Throwable e) {\\n log.info(\\"executeLocalTransactionBranch exception\\", e);\\n log.info(msg.toString());\\n localException = e;\\n }\\n }\\n break;\\n case FLUSH_DISK_TIMEOUT:\\n case FLUSH_SLAVE_TIMEOUT:\\n case SLAVE_NOT_AVAILABLE:\\n //刷盘超时 或 从节点不可用 相当于失败\\n localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;\\n break;\\n default:\\n break;\\n }\\n \\n try {\\n //通知broker本地事务状态\\n this.endTransaction(msg, sendResult, localTransactionState, localException);\\n } catch (Exception e) {\\n log.warn(\\"local transaction execute \\" + localTransactionState + \\", but end broker transaction failed\\", e);\\n }\\n \\n //返回\\n TransactionSendResult transactionSendResult = new TransactionSendResult();\\n transactionSendResult.setSendStatus(sendResult.getSendStatus());\\n transactionSendResult.setMessageQueue(sendResult.getMessageQueue());\\n transactionSendResult.setMsgId(sendResult.getMsgId());\\n transactionSendResult.setQueueOffset(sendResult.getQueueOffset());\\n transactionSendResult.setTransactionId(sendResult.getTransactionId());\\n transactionSendResult.setLocalTransactionState(localTransactionState);\\n return transactionSendResult;\\n }\\n
\\n在发送的流程中主要会在发送前做一些准备如标记半事务状态,然后进行同步发送,如果发送成功则会执行本地事务,最后单向通知broker本地事务的状态
\\n之前的文章也说过消息到达后,broker存储消息的原理(先写CommitLog、再写其他文件)
\\n事务消息在消息进行存储前,会使用桥接器TransactionalMessageBridge调用 parseHalfMessageInner
,将消息topic改为半事务topic并存储原始topic、队列ID(方便后续重新投入真正的topic)
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {\\n //存储真正的topic和队列ID\\n MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());\\n MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,\\n String.valueOf(msgInner.getQueueId()));\\n msgInner.setSysFlag(\\n MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));\\n //设置本次要投入的topic为半事务Topic RMQ_SYS_TRANS_HALF_TOPIC\\n msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());\\n msgInner.setQueueId(0);\\n msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));\\n return msgInner;\\n }\\n
\\n这样半事务状态的事务消息就会被投入半事务topic的队列中,这样就能达到消费者无法消费半事务消息(因为它们没被投入真实的队列中)
\\n生产者发送完消息,无论成功还是失败都会通知broker本地事务状态
\\nbroker使用EndTransactionProcessor处理END_TRANSACTION
的请求,其核心逻辑就是根据本地事务状态进行处理:
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws\\n RemotingCommandException {\\n //构建通用响应\\n final RemotingCommand response = RemotingCommand.createResponseCommand(null);\\n //解析\\n final EndTransactionRequestHeader requestHeader =\\n (EndTransactionRequestHeader) request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);\\n \\n //从节点直接响应失败\\n if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {\\n response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);\\n return response;\\n }\\n \\n \\n //...\\n \\n \\n OperationResult result = new OperationResult();\\n //成功的情况\\n if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {\\n //调用 getHalfMessageByOffset 根据commitLog偏移量获取半事务消息\\n result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);\\n //找到半事务消息\\n if (result.getResponseCode() == ResponseCode.SUCCESS) {\\n //检查数据\\n RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);\\n if (res.getCode() == ResponseCode.SUCCESS) {\\n //检查成功 \\n MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());\\n msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));\\n msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());\\n msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());\\n msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());\\n //清理半事务标识\\n MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);\\n //重新将消息投入真实topic、队列中\\n RemotingCommand sendResult = sendFinalMessage(msgInner);\\n if (sendResult.getCode() == ResponseCode.SUCCESS) {\\n //重投成功 删除事务消息\\n this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());\\n }\\n return sendResult;\\n }\\n return res;\\n }\\n } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {\\n //失败情况 也是调用 getHalfMessageByOffset 根据commitLog偏移量获取半事务消息\\n result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);\\n if (result.getResponseCode() == ResponseCode.SUCCESS) {\\n RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);\\n if (res.getCode() == ResponseCode.SUCCESS) {\\n //找到消息检查完就删除事务消息\\n this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());\\n }\\n return res;\\n }\\n }\\n response.setCode(result.getResponseCode());\\n response.setRemark(result.getResponseRemark());\\n return response;\\n }\\n
\\n成功或失败(commit/rollback)的情况都会删除半消息,成功的情况会将消息投入原始队列中,后续进行消费
\\n而还要一种无法确定是成功还是失败的情况,需要broker进行回查
\\n负责回查的组件是TransactionalMessageCheckService:定期对半事务消息进行检查是否需要回查(在broker启动初始化时进行初始化)
\\n其检查回查会调用this.brokerController.getTransactionalMessageService().check
它会遍历事务topic RMQ_SYS_TRANS_HALF_TOPIC
下的所有队列,循环取出半事务消息进行判断是否需要进行回查
由于代码较多,这里总结性贴出关键代码:
\\ngetHalfMsg
resolveDiscardMsg
putBackHalfMsgQueue
resolveHalfMsg
public void check(long transactionTimeout, int transactionCheckMax,AbstractTransactionalMessageCheckListener listener) {\\n //遍历事务topic下的所有队列,循环取出半事务消息进行判断是否需要进行回查\\n String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC;\\n Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);\\n for (MessageQueue messageQueue : msgQueues) {\\n while (true) {\\n //超出边界会退出 代码略\\n \\n //获取半事务消息 这里的参数i是半事务消息偏移量\\n GetResult getResult = getHalfMsg(messageQueue, i);\\n MessageExt msgExt = getResult.getMsg();\\n \\n //needDiscard 超过最大检查次数 15次\\n //needSkip 超过最大存储时间 72h\\n if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {\\n //丢弃半事务消息\\n listener.resolveDiscardMsg(msgExt);\\n //..\\n continue;\\n }\\n \\n //...\\n \\n //超过6s\\n if (isNeedCheck) {\\n //将消息重投入半消息队列\\n if (!putBackHalfMsgQueue(msgExt, i)) {\\n continue;\\n }\\n //向生产者发送回查的请求 CHECK_TRANSACTION_STATE\\n listener.resolveHalfMsg(msgExt);\\n }\\n \\n }\\n }\\n }\\n
\\n请求回查并不会返回结果,生产者处理查询到事务状态后,再向broker发送单向的本地事务状态通知请求(endTransactionOneway)
\\nClientRemotingProcessor 处理broker发送的回查请求CHECK_TRANSACTION_STATE
\\nClientRemotingProcessor 调用 checkTransactionState
进行处理:
transactionListener.checkLocalTransaction
endTransactionOneway
对broker进行通知本地事务状态结果涉及多服务的分布式事务,不追求强一致性的情况下,可考虑使用事务消息+重试的方式尽力达到最终一致性
\\n使用时需要定义事务监听器执行本地事务和回查本地事务状态的方法,注意可能消费失败,重试多次后需要记录并特殊处理避免最终数据不一致
\\n使用事务消息时无法设置延迟级别,发送前会将延迟级别清除
\\n发送事务消息采用同步发送,在发送前会标记为半(事务)消息状态,在发送成功后会调用事务监听器执行本地事务,最后单向通知broker本地事务的状态
\\nbroker存储半(事务)消息前会更改它的topic、queueId,将其持久化到事务(半消息)topic中,以此来达到暂时不可以被消费的目的
\\nbroker接收本地事务状态通知时,如果是commit状态则将半(事务)消息重投入原始topic、队列中,以此来达到可以进行消费的目的,并且删除半(事务)消息,rollback状态也会删除半(事务)消息,只有未知状态的情况下不删除,等待后续触发回查机制
\\nbroker使用组件定期遍历事务(半消息)topic下的所有队列检查是否需要进行回查,遍历队列时循环取出半(事务)消息,如果超过检查最大次数(15)或超时(72h),则会丢弃消息;否则会将半(事务)消息放回队列,当事务消息超过6s时会触发回查机制,向produce发送检查事务状态的请求
\\nproduce收到回查请求后,调用事务监听器的检查事务状态方法,并又调用单向通知broker本地事务状态
\\n😁我是菜菜,热爱技术交流、分享与写作,喜欢图文并茂、通俗易懂的输出知识
\\n📚在我的博客中,你可以找到Java技术栈的各个专栏:Java并发编程与JVM原理、Spring和MyBatis等常用框架及Tomcat服务器的源码解析,以及MySQL、Redis数据库的进阶知识,同时还提供关于消息中间件和Netty等主题的系列文章,都以通俗易懂的方式探讨这些复杂的技术点
\\n🏆除此之外,我还是掘金优秀创作者、腾讯云年度影响力作者、华为云年度十佳博主....
\\n👫我对技术交流、知识分享以及写作充满热情,如果你愿意,欢迎加我一起交流(vx:CaiCaiJava666),也可以持续关注我的公众号:菜菜的后端私房菜,我会分享更多技术干货,期待与更多志同道合的朋友携手并进,一同在这条充满挑战与惊喜的技术之旅中不断前行
\\n🤝如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
\\n📖本篇文章被收入专栏 消息中间件,感兴趣的朋友可以持续关注~
\\n📝本篇文章、笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的朋友可以star持续关注~
","description":"RocketMQ(十一):事务消息如何满足分布式一致性? 前言\\n\\n在分布式系统中由于相关联的多个服务所在的数据库互相隔离,数据库无法使用本地事务来保证数据的一致性,因此需要使用分布式事务来保证数据的一致性\\n\\n比如用户支付订单后,需要更改订单状态,还需要涉及其他服务的其他操作如:物流出货、积分变更、清空购物车等\\n\\n由于它们数据所存储的数据库会互相隔离,当订单状态修改成功/失败时,其他服务对应的数据也需要修改成功/失败,否则就会出现数据不一致的情况\\n\\n解决分布式事务常用的一种方案是使用MQ做补偿以此来达到数据的最终一致性,而RocketMQ提供的事务消息能够简单…","guid":"https://juejin.cn/post/7481991063113990198","author":"菜菜的后端私房菜","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T01:35:29.480Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce07b578fdbf4eb09b252c65695f65b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=2bP5DX5rkF8uifHp8VzMtb%2F9M40%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8abfafcb7ba7492a9e5ccf8c10260cdf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=DtuQOAHgWI6hJEbGSBcH4WzKPu0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2e8bbdbd28fa4fe2b54b08e93355173d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=YiEAbMPl1FFbe3EhdIiFWerg6yk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a741a56534d542d389145c8806e1d713~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=d0xGTb4BL%2Bzm%2Be5RonTUD9WqcjQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8c05409a0304638a8df2a3db1ee6cd0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=U8BE5YUtGTeCzcoXBE8PgncXP9I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5abfdcf6c96e442e91368abf7eed6526~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=TYRvQXM1eWvmVmlRT%2B%2FHUEcnHko%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d658b6f2e5ec4064982efe22e655b280~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6I-c6I-c55qE5ZCO56uv56eB5oi_6I-c:q75.awebp?rk3s=f64ab15b&x-expires=1742780129&x-signature=Cfo82SZ2ES99uHkS1iTuffJR%2Bmw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","RocketMQ","Java"],"attachments":null,"extra":null,"language":null},{"title":"CompletableFuture使用的6个坑","url":"https://juejin.cn/post/7481840315047591988","content":"大家好,我是田螺。
\\n日常开发中,我们经常喜欢用CompletableFuture。但是它在使用的过程中,容易忽略几个坑,今天田螺哥给大家盘点一下~~
\\n既然上来说CompletableFuture可能隐藏几个坑,那为什么我们还要使用它呢?
\\n\\n\\nCompletableFuture 是 Java 8 引入的异步编程工具,它的核心优势在于简化异步任务编排、提升代码可读性和灵活性。
\\n
我们来看一个使用CompletableFuture的例子吧,代码如下:
\\n\\n\\n假设我们有两个任务服务,一个查询用户基本信息,一个是查询用户勋章信息。
\\n
public class FutureTest {\\n\\n public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {\\n\\n UserInfoService userInfoService = new UserInfoService();\\n MedalService medalService = new MedalService();\\n long userId =666L;\\n long startTime = System.currentTimeMillis();\\n\\n //调用用户服务获取用户基本信息\\n CompletableFuture<UserInfo> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));\\n\\n Thread.sleep(300); //模拟主线程其它操作耗时\\n\\n CompletableFuture<MedalInfo> completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId)); \\n\\n UserInfo userInfo = completableUserInfoFuture.get(2,TimeUnit.SECONDS);//获取个人信息结果\\n MedalInfo medalInfo = completableMedalInfoFuture.get();//获取勋章信息结果\\n System.out.println(\\"总共用时\\" + (System.currentTimeMillis() - startTime) + \\"ms\\");\\n\\n }\\n}\\n
\\n接下来,我们通过代码demo,阐述一下CompletableFuture
使用的几个坑~
CompletableFuture
默认使用ForkJoinPool.commonPool()
作为线程池。如果任务阻塞或执行时间过长,可能会导致线程池耗尽,影响其他任务的执行。
反例:
\\n CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {\\n // 模拟长时间任务\\n try {\\n Thread.sleep(10000);\\n System.out.println(\\"捡田螺的小男孩666\\");\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n });\\n future.join();\\n
\\n正例:
\\n// 1. 手动创建线程池(核心参数可配置化)\\nint corePoolSize = 10; // 核心线程数\\nint maxPoolSize = 10; // 最大线程数(固定大小)\\nlong keepAliveTime = 0L; // 非核心线程空闲存活时间(固定线程池可设为0)\\nBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 有界队列(容量100)\\nRejectedExecutionHandler rejectionHandler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略\\n\\nExecutorService customExecutor = new ThreadPoolExecutor(\\n corePoolSize,\\n maxPoolSize,\\n keepAliveTime,\\n TimeUnit.MILLISECONDS,\\n workQueue,\\n rejectionHandler\\n);\\n\\n// 2. 提交异步任务\\nCompletableFuture<Void> future = CompletableFuture.runAsync(() -> {\\n try {\\n Thread.sleep(10000); // 模拟耗时任务\\n System.out.println(\\"捡田螺的小男孩666\\");\\n System.out.println(\\"Task completed\\");\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n}, customExecutor);\\n\\n// 3. 阻塞等待任务完成\\nfuture.join();\\n
\\n如果CompletableFuture 中的任务抛出异常,跟我们使用的传统try...catch
有点不一样的
正例:
\\n使用 exceptionally 或 handle 方法来处理异常。
\\n CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n throw new RuntimeException(\\"田螺测试异常!\\");\\n });\\n future.exceptionally(ex -> {\\n System.err.println(\\"异常: \\" + ex.getMessage());\\n return -1; // 返回默认值\\n }).join();\\n
\\n运行结果:
\\n异常: java.lang.RuntimeException: 田螺测试异常!\\n
\\nCompletableFuture 本身不支持超时处理,如果任务长时间不完成,可能会导致程序一直等待。
\\n反例:
\\nCompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n try {\\n Thread.sleep(10000); //模拟任务耗时\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n return 1;\\n});\\nfuture.join(); // 程序会一直等待\\n
\\n正例:
\\n如果你是JDK8,使用 get() 方法并捕获 TimeoutException
\\n CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n try {\\n Thread.sleep(10000);\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n return 1;\\n });\\n future.join(); // 程序会一直等待\\n\\n try {\\n Integer result = future.get(3, TimeUnit.SECONDS); // 设置超时时间为1秒\\n System.out.println(\\"田螺等待3秒之后:\\"+result);\\n } catch (TimeoutException e) {\\n System.out.println(\\"Task timed out\\");\\n future.cancel(true); // 取消任务\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n
\\n如果你是Java 9 或更高版本,可以直接使用 orTimeout 和 completeOnTimeout 方法:
\\nCompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {\\n try {\\n Thread.sleep(10000);\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n return 1;\\n}).orTimeout(3, TimeUnit.SECONDS); // 3秒超时\\nfuture.exceptionally(ex -> {\\n System.err.println(\\"Timeout: \\" + ex.getMessage());\\n return -1;\\n}).join();\\n
\\nCompletableFuture 默认不会传递线程上下文(如 ThreadLocal),这可能导致上下文丢失~~
\\n ThreadLocal<String> threadLocal = new ThreadLocal<>();\\n threadLocal.set(\\"田螺主线程\\");\\n CompletableFuture.runAsync(() -> {\\n System.out.println(threadLocal.get()); // 输出 null\\n }).join();\\n
\\n正例:
\\n使用CompletableFuture
的supplyAsync
或 runAsync
时,手动传递上下文。
ThreadLocal<String> threadLocal = new ThreadLocal<>();\\n threadLocal.set(\\"田螺主线程\\");\\n ExecutorService executor = Executors.newFixedThreadPool(1);\\n CompletableFuture.runAsync(() -> {\\n threadLocal.set(\\"田螺子线程\\");\\n System.out.println(threadLocal.get()); // 输出田螺子线程\\n }, executor).join();\\n
\\n\\n\\nCompletableFuture 的回调地狱指的是在异步编程中,过度依赖回调方法(如 thenApply、thenAccept 等)导致代码嵌套过深、难以维护的现象。
\\n
当多个异步任务需要顺序执行或依赖前一个任务的结果时,如果直接嵌套回调,代码会变得臃肿且难以阅读。反例如下:
\\nCompletableFuture.supplyAsync(() -> 1)\\n .thenApply(result -> {\\n System.out.println(\\"Step 1: \\" + result);\\n return result + 1;\\n })\\n .thenApply(result -> {\\n System.out.println(\\"Step 2: \\" + result);\\n return result + 1;\\n })\\n .thenAccept(result -> {\\n System.out.println(\\"Step 3: \\" + result);\\n });\\n
\\n正例:
\\n通过链式调用和方法拆分,保持代码简洁:
\\nCompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1)\\n .thenApply(this::step1)\\n .thenApply(this::step2);\\n\\nfuture.thenAccept(this::step3);\\n\\n// 拆分逻辑到单独方法\\nprivate int step1(int result) {\\n System.out.println(\\"Step 1: \\" + result);\\n return result + 1;\\n}\\n\\nprivate int step2(int result) {\\n System.out.println(\\"Step 2: \\" + result);\\n return result + 1;\\n}\\n\\nprivate void step3(int result) {\\n System.out.println(\\"Step 3: \\" + result);\\n}\\n
\\n任务编排时,如果任务之间有依赖关系,可能会导致任务无法按预期顺序执行。
\\n反例:
\\nCompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 1);\\nCompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 2);\\nCompletableFuture<Integer> result = future1.thenCombine(future2, (a, b) -> a + b);\\nresult.join(); // 可能不会按预期顺序执行\\n
\\n正例:
\\n使用 thenCompose 或 thenApply 来确保任务顺序。
\\nCompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 1);\\nCompletableFuture<Integer> future2 = future1.thenApply(a -> a + 2);\\nfuture2.join(); // 确保顺序执行\\n
","description":"前言 大家好,我是田螺。\\n\\n日常开发中,我们经常喜欢用CompletableFuture。但是它在使用的过程中,容易忽略几个坑,今天田螺哥给大家盘点一下~~\\n\\n公众号:捡田螺的小男孩 (有田螺精心原创的面试PDF)\\ngithub地址,感谢每颗star:github\\nCompletableFuture使用的优点\\n\\n既然上来说CompletableFuture可能隐藏几个坑,那为什么我们还要使用它呢?\\n\\nCompletableFuture 是 Java 8 引入的异步编程工具,它的核心优势在于简化异步任务编排、提升代码可读性和灵活性。\\n\\n我们来看一个使用Com…","guid":"https://juejin.cn/post/7481840315047591988","author":"捡田螺的小男孩","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T11:10:02.429Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"DataWorks 体验笔记 :一切的基础都是数据的读和写","url":"https://juejin.cn/post/7481861794589343763","content":"前段时间我体验了一把简单的 DataWroks 的体验项目 ,通过 DataWorks 学习一把大数据是在干什么 。
\\n后面在考虑业务使用的时候 ,也陆陆续续的体验了一些其他的功能 ,这里统一记录下。
\\n这一篇主要针对数据的读取和写入 ,这是所有功能的基础。 在 DataWroks 中 ,数据的读和写叫数据集成
功能。
数据集成是 DataWorks 的核心功能之一 ,在功能上数据集成包括以下功能 :
\\n从这一系列的功能中 ,其实还是围绕这一条主线 : 读 👉 处理 👉 写。
\\nDataWorks 对于数据源的处理和 绝大多数 数据处理工具一致 , 把数据源当成一个 可复用 的终端节点。
\\n\\n\\n先来看看 DataWorks 支持的数据源
\\n
我们稍微深入一下 : 很明显 ,不同的数据源 ,数据的格式 / 编码 / 模态 都是不同的。
\\n那么在读取这些数据的时候 ,如何对数据进行处理呢?
\\n\\n\\nS1 : 在阿里云上面简单准备了3个表
\\n
\\n\\nS2 : 在数据集成里面创建对应的数据源读取
\\n
\\n\\n阶段总结 :
\\n
这里不是为了某个渠道打广告 ,不止阿里的工具 ,很多云平台都有类似的工具 , 使用方式上都大同小异。
\\n会了一个其他的都会了 ,数据源就是一个个触点 ,把各种资源连接到大数据这张网上。
\\n数据集成是第二步 ,再聪明的组件 ,也不可能毫无处理的情况下 ,就帮你把数据处理好 。 所以在同步之前 ,还是要实现一次离线同步的配置。
\\n当我们在 同步任务 创建过程中 ,系统会引导我们来到这个页面 (在这里可以直接选择 3.1 的数据源)
\\n\\n\\n阶段总结
\\n
和很多的数据处理工具一样 ,数据开发分为模块式 和 代码式 两种常见的集成方式。一般情况下 ,工具会提供完整零代码界面进行配置 ,对于 MySQL 这种常见的工具 ,基本上配置好就能使用。
\\n同时还提供了很多特性功能 , 比如 :
\\n导入前
执行特定的 SQL\\n支持执行日志
,可以在日志中明确的看到导入的结果和详情映射方式
,可以基于每行 ,也可以基于相同的名字 ,还可以通过手动处理到了这里同步基本上就完成了 ,我们可以直接通过上面的 运行 箭头 ,运行一次数据的同步功能。
\\n\\n\\n阶段总结 :
\\n
同步调度的目的是为了定时的触发任务的同步 ,比如每天跑一次数据的收集 ,把数据同步到 MaxCompute 等分析工具里面 ,这是非常有用的。
\\n从界面里面 ,我们可以分析到一般的大数据框架都会带以下功能 :
\\n线性
的 ,先拿 ,再算 ,最后录入。 依赖就是为了保证这种关系\\n\\n执行日志
\\n
商用工具的文档很多 ,所以这里没有铺开去体验各种渠道 ,主要是为了对这些组件有一个清晰的认知。
\\n但是这种还是会有瓶颈 ,对于复杂的聚合,运算后输出的逻辑 ,最终还是要数据开发进行处理 ,这就不是简单的零代码可以解决的了。
\\n到了这里一个简单的数据集成就完成了 ,剩下的无非就是扩展渠道 ,扩展数据源罢了。
\\n一般的大数据工具都提供的是一个综合性的管理平台 ,而我们业务上更多的时候是为了进行数据的开发和处理。
\\n同样到 ,该框架集成了数据开发的功能 ,可以很便利的进行数据开发处理。
\\n既然使用的是 DataWorks ,这里顺带也薅羊毛试试 MaxCompute :
\\n人到中年家里事就多了 ,现在已经没有太多的业余时间去搞新东西了 ,也不知道这个系列会学到什么地步, 走走停停吧。
\\nDataWorks 已经是一个产品级的应用了 ,里面很多东西都是现成的 ,开箱就可以用 ,上手感受了一下 ,其实不难
。
大家薅羊毛 ,免费3个月的体验试试就知道了。
\\n\\n\\n体验感受
\\n
\\n\\n还有一篇 :
\\n
基于 SQL 的场景是很有限的 ,可能只适合简单的数据同步 ,而不是
复杂的大数据分析。
所以下一篇会说一说怎么通过 Python
实现代码级别复杂的处理和分析。
Redis 是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将 Redis 中的数据以某种形式(数据或命令)从内存保存到硬盘。当下次 Redis 重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。Redis 的持久化机制有三种:
\\nRedis 持久化拥有以下三种方式:
\\n\\n\\nRDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
\\n
\\n\\n记录所有的操作命令,并以文本的形式追加到文件中;
\\n
\\n\\nRedis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
\\n
以上三种持久化方案,每一种都有特定的使用场景,具体的我们可以根据自己的需求自行选择。
\\nRDB ( Redis Data Base) 指的是在指定的时间间隔内将内存中的数据集快照写入磁盘,RDB 是内存快照(内存数据的二进制序列化形式)的方式持久化,每次都是从 Redis 中生成一个快照进行数据的全量备份。
\\nRDB(Redis DataBase)是将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘的过程
。
优点:
\\n缺点:
\\nRDB的持久化方式有两种:
\\n一种是手动触发, 一种是自动间隔性保存触发
手动指令触发
\\n手动触发 RDB 持久化的方式可以使用 save
命令和 bgsave
命令,这两个命令的区别如下:
save
:执行 save
指令,阻塞 Redis 的其他操作,会导致 Redis 无法响应客户端请求,不建议使用。
bgsave
:执行 bgsave
指令,Redis 后台创建子进程,异步进行快照的保存操作,此时 Redis 仍然能响应客户端的请求。
自动间隔性保存
\\n在默认情况下,Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。可以对 Redis 进行设置,让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。
\\n比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 10 个键被改动”这一条件时,自动保存一次数据集:save 60 10
。
Redis 的默认配置如下,三个设置满足其一即可触发自动保存:
\\nsave 60 10000\\nsave 300 10\\nsave 900 1\\n
\\nRDB 持久化方案进行备份时,Redis 会单独 fork 一个子进程来进行持久化,会将数据写入一个临时文件中,持久化完成后替换旧的 RDB 文件。在整个持久化过程中,主进程(为客户端提供服务的进程)不参与 IO 操作,这样能确保 Redis 服务的高性能,RDB 持久化机制适合对数据完整性要求不高但追求高效恢复的使用场景。下面展示 RDB 持久化流程:
\\n关键执行步骤如下
\\n\\n\\n在生成 RDB 文件的步骤中,在同步到磁盘和持续写入这个过程是如何处理数据不一致的情况呢?生成快照 RDB 文件时是否会对业务产生影响?
\\n
上面说到了 RDB 持久化过程中,主进程会 fork 一个子进程来负责 RDB 的备份,这里简单介绍一下 fork:
\\nLinux 操作系统中的程序,fork 会产生一个和父进程完全相同的子进程。子进程与父进程所有的数据均一致,但是子进程是一个全新的进程,与原进程是父子进程关系。
\\n出于效率考虑,Linux 操作系统中使用 COW(Copy On Write)写时复制机制,fork 子进程一般情况下与父进程共同使用一段物理内存,只有在进程空间中的内存发生修改时,内存空间才会复制一份出来。
\\n在 Redis 中,RDB 持久化就是充分的利用了这项技术,Redis 在持久化时调用 glibc 函数 fork 一个子进程,全权负责持久化工作,这样父进程仍然能继续给客户端提供服务。fork 的子进程初始时与父进程(Redis 的主进程)共享同一块内存;当持久化过程中,客户端的请求对内存中的数据进行修改,此时就会通过 COW (Copy On Write) 机制对数据段页面进行分离,也就是复制一块内存出来给主进程去修改。
\\n通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对于子进程是不可见的,两者不会相互影响;
\\n通过 fork 创建子进程时不会立刻触发大量内存的拷贝,采用的是写时拷贝 COW (Copy On Write)。内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间;
\\nAOF (Append Only File) 是把所有对内存进行修改的指令(写操作)以独立日志文件的方式进行记录,重启时通过执行 AOF 文件中的 Redis 命令来恢复数据。类似MySql bin-log 原理。AOF 能够解决数据持久化实时性问题,是现在 Redis 持久化机制中主流的持久化方案。
\\nAOF的工作流程操作
\\n\\n\\n命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)。
\\n
优点:
\\n缺点:
\\nAOF 持久化方案进行备份时,客户端所有请求的写命令都会被追加到 AOF 缓冲区中,缓冲区中的数据会根据 Redis 配置文件中配置的同步策略来同步到磁盘上的 AOF 文件中,追加保存每次写的操作到文件末尾。同时当 AOF 的文件达到重写策略配置的阈值时,Redis 会对 AOF 日志文件进行重写,给 AOF 日志文件瘦身。Redis 服务重启的时候,通过加载 AOF 日志文件来恢复数据。
\\nAOF 的执行流程包括:
\\n命令追加(append)
\\nRedis 先将写命令追加到缓冲区 aof_buf,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘 IO 成为 Redis 负载的瓶颈。
\\nstruct redisServer {\\n //其他域...\\n sds aof_buf; // sds类似于Java中的String\\n //其他域...\\n}\\n
\\n文件写入(write)和文件同步(sync)
\\n根据不同的同步策略将 aof_buf 中的内容同步到硬盘;
\\nLinux 操作系统中为了提升性能,使用了页缓存(page cache)。当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,而是在 page cache 中,为了将 page cache 中的数据真正落盘,需要执行 fsync / fdatasync 命令来强制刷盘。这边的文件同步做的就是刷盘操作,或者叫文件刷盘可能更容易理解一些。
\\nAOF 缓存区的同步文件策略由参数 appendfsync 控制,有三种同步策略,各个值的含义如下:
\\nalways
:命令写入 aof_buf 后立即调用系统 write 操作和系统 fsync 操作同步到 AOF 文件,fsync 完成后线程返回。这种情况下,每次有写命令都要同步到 AOF 文件,硬盘 IO 成为性能瓶颈,Redis 只能支持大约几百TPS写入,严重降低了 Redis 的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低 SSD 的寿命。可靠性较高,数据基本不丢失。
no
:命令写入 aof_buf 后调用系统 write 操作,不对 AOF 文件做 fsync 同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。
everysec
:命令写入 aof_buf 后调用系统 write 操作,write 完成后线程返回;fsync 同步文件操作由专门的线程每秒调用一次。everysec 是前述两种策略的折中,是性能和数据安全性的平衡,因此是 Redis 的默认配置,也是我们推荐的配置。
文件重写(rewrite)
\\n定期重写 AOF 文件,达到压缩的目的。
\\nAOF 重写是 AOF 持久化的一个机制,用来压缩 AOF 文件,通过 fork 一个子进程,重新写一个新的 AOF 文件,该次重写不是读取旧的 AOF 文件进行复制,而是读取内存中的Redis数据库,重写一份 AOF 文件,有点类似于 RDB 的快照方式。
\\n文件重写之所以能够压缩 AOF 文件,原因在于:
\\n过期的数据不再写入文件
\\n无效的命令不再写入文件:如有些数据被重复设值(set mykey v1, set mykey v2)、有些数据被删除了(sadd myset v1, del myset)等等
\\n多条命令可以合并为一个:如 sadd myset v1, sadd myset v2, sadd myset v3 可以合并为 sadd myset v1 v2 v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于 list、set、hash、zset类型的 key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 中定义,不可更改,2.9版本中值是64。
\\n缓冲区同步策略,由参数 appendfsync 控制,一共3种:
\\nalways: 调用系统 fsync 函数,直到同步到硬盘返回; (严重影响redis性能)
\\neverysec: 先调用 OS write 函数, 写到缓冲区,然后 redis 每秒执行一次 OS fsync 函数。(推荐使用这种方式)
\\nno: 只执行 write OS 函数,具体同步硬盘策略由 OS 决定;(不推荐,数据不安全,容易丢失数据)
\\n前面提到 AOF 的缺点时,说过 AOF 属于日志追加的形式来存储 Redis 的写指令,这会导致大量冗余的指令存储,从而使得 AOF 日志文件非常庞大,比如同一个 key 被写了 10000 次,最后却被删除了,这种情况不仅占内存,也会导致恢复的时候非常缓慢,因此 Redis 提供重写机制来解决这个问题。Redis 的 AOF 持久化机制执行重写后,保存的只是恢复数据的最小指令集,我们如果想手动触发可以使用如下指令:
\\nbgrewriteaof\\n
\\n文件重写时机
\\n相关参数:
\\naof_current_size:表示当前 AOF 文件空间
\\naof_base_size:表示上一次重写后 AOF 文件空间
\\nauto-aof-rewrite-min-size: 表示运行 AOF 重写时文件的最小体积,默认为64MB
\\nauto-aof-rewrite-percentage: 表示当前 AOF 重写时文件空间(aof_current_size)超过上一次重写后 AOF 文件空间(aof_base_size)的比值多少后会重写。
\\n同时满足下面两个条件,则触发 AOF 重写机制:
\\naof_current_size 大于 auto-aof-rewrite-min-size
\\n当前 AOF 相比上一次 AOF 的增长率:(aof_current_size - aof_base_size)/aof_base_size 大于或等于 auto-aof-rewrite-percentage
\\nAOF 重写流程如下:
\\nbgrewriteaof 触发重写,判断是否存在 bgsave 或者 bgrewriteaof 正在执行,存在则等待其执行结束再执行
\\n主进程 fork 子进程,防止主进程阻塞无法提供服务,类似 RDB
\\n子进程遍历 Redis 内存快照中数据写入临时 AOF 文件,同时会将新的写指令写入 aof_buf 和 aof_rewrite_buf 两个重写缓冲区,前者是为了写回旧的 AOF 文件,后者是为了后续刷新到临时 AOF 文件中,防止快照内存遍历时新的写入操作丢失
\\n子进程结束临时 AOF 文件写入后,通知主进程
\\n主进程会将上面 3 中的 aof_rewirte_buf 缓冲区中的数据写入到子进程生成的临时 AOF 文件中
\\n主进程使用临时 AOF 文件替换旧 AOF 文件,完成整个重写过程。
\\n在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序会检查集合元素数量是否超过 REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,如果超过了,则会使用多个命令来记录,而不单单使用一条命令。
\\n\\n\\nAOF重写是一个有歧义的名字,该功能是通过直接读取数据库的键值对实现的,程序无需对现有AOF文件进行任何读入、分析或者写入操作。
\\n
Redis 为什么考虑使用 AOF 而不是 WAL 呢?
\\n很多数据库都是采用的 Write Ahead Log(WAL)写前日志,其特点就是先把修改的数据记录到日志中,再进行写数据的提交,可以方便通过日志进行数据恢复。
\\n但是 Redis 采用的却是 AOF(Append Only File)写后日志,特点就是先执行写命令,把数据写入内存中,再记录日志。
\\n如果先让系统执行命令,只有命令能执行成功,才会被记录到日志中。因此,Redis 使用写后日志这种形式,可以避免出现记录错误命令的情况。
\\n另外还有一个原因就是:AOF 是在命令执行后才记录日志,所以不会阻塞当前的写操作。
\\nRedis4.0 后大部分的使用场景都不会单独使用 RDB 或者 AOF 来做持久化机制,而是兼顾二者的优势混合使用。其原因是 RDB 虽然快,但是会丢失比较多的数据,不能保证数据完整性;AOF 虽然能尽可能保证数据完整性,但是性能确实是一个诟病,比如重放恢复数据。
\\nRedis从4.0版本开始引入 RDB-AOF 混合持久化模式,这种模式是基于 AOF 持久化模式构建而来的,混合持久化通过 aof-use-rdb-preamble yes
开启。
那么 Redis 服务器在执行 AOF 重写操作时,就会像执行 BGSAVE 命令那样,根据数据库当前的状态生成出相应的 RDB 数据,并将这些数据写入新建的 AOF 文件中,至于那些在 AOF 重写开始之后执行的 Redis 命令,则会继续以协议文本的方式追加到新 AOF 文件的末尾,即已有的 RDB 数据的后面。
\\n换句话说,在开启了 RDB-AOF 混合持久化功能之后,服务器生成的 AOF 文件将由两个部分组成,其中位于 AOF 文件开头的是 RDB 格式的数据,而跟在 RDB 数据后面的则是 AOF 格式的数据。
\\n当一个支持 RDB-AOF 混合持久化模式的 Redis 服务器启动并载入 AOF 文件时,它会检查 AOF 文件的开头是否包含了 RDB 格式的内容。
\\n其日志文件结构如下:
\\nAOF 和 RDB 文件都可以用于服务器重启时的数据恢复,具体流程如下图:
\\n从图中可以看出优先加载 AOF,当没有 AOF 时才加载 RDB。当 AOF 或者 RDB 存在错误,则加载失败。
","description":"前言 Redis 是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将 Redis 中的数据以某种形式(数据或命令)从内存保存到硬盘。当下次 Redis 重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。Redis 的持久化机制有三种:\\n\\n持久化的几种方式\\n\\nRedis 持久化拥有以下三种方式:\\n\\n快照方式(RDB, Redis DataBase)\\n\\nRDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发\\n\\n文件追加方式(AOF…","guid":"https://juejin.cn/post/7481863661129039899","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T01:09:24.338Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fba343d75eb948518330d9fa196fd443~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=toNkj7cgLHlkeo87bNP1NsuX2aU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d6ab17b1b81c4c96a4ea97ac2f81fcf1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=0fxpztUR9Kq%2BDhAVXh4NOPCsmYE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a62b02bcdf6428491f8a18eb7f6d872~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=HMjzq9lx0NsNarqsV0bjsaGQskE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/79fe17b8285f41c497b4f2afb3b24c19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=IV4K3iVwpwr%2FIPo4BC9f6CJlIZ4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5844eb5482ed4c51af9c80cc0b6d47bb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=7vkC4rqL5VpMQ%2BZSC8DfBUbOErE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1110349f11245e981b21b7a9c2931d8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=7XMSNhdx%2FdjGWDm05MNYQ86kbyc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6b2be1b47cf4a09bc21983aa02bb90a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=S2vC1vF8XXreINKar1fACc9Aroc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/045b230ab5da4300b285c4ae01b53105~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=MOQ5t1f49dQp7K6SZ%2FAQbJBqZMU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/540c807b353845e6b47249c482c9728e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=5kBc%2FoPKLJzuCVBpFqX%2BdHU3dBw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1f693181fc84d36849e6c62d00d2422~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742692163&x-signature=l4IvhuX0uMkGcqiE7HQkXYJ%2B78M%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Redis---配置文件详解","url":"https://juejin.cn/post/7481600472109547558","content":"\\n\\n配置文件版本使用的是redis 4.0.14, 某些参数需要处理了解linux kernel,笔者不太了解linux内核参数,后面还要继续努力呀。
\\n
./redis-server /path/to/redis.conf
启动redis并使配置文件生效
\\ninclude /path/to/local.conf
include 可以使用多个配置文件,如果配置文件有相同值,后面的会覆盖前面的:
\\ninclude /path/to/local.conf\\ninclude /path/to/other.conf\\n
\\nloadmodule /path/to/my_module.so
加载modules 没什么用好像,还需要继续研究
\\nloadmodule /path/to/my_module.so\\nloadmodule /path/to/other_module.so\\n
\\nbind 127.0.0.1
绑定ip地址,为了安全最好都绑定
\\nprotected-mode yes
保护模式,如果保护模式开了,而且redis既没有bind ip,也没设置密码,那redis只接收127.0.0.1的连接。 默认都开
\\nport 6379
端口,设置为0就不会监听
\\ntcp-backlog 511
linux 内核tcp_max_syn_backlog和somaxconn 参数调优
\\nunixsocket /tmp/redis.sock unixsocketperm 700
unix socket ,默认不监听,没用
\\ntimeout 0
连接闲置N秒时关闭连接
\\ntcp-keepalive 300
开启TCP长连接,如果设置非0,会使用系统的SO_KEEPALIVE间隔发送TCP ACK给客户端,以防连接被弃用。这个很有用:
\\n默认值是300。
\\ndaemonize yes
默认情况redis不会按照守护进程的模式去运行。如果你需要,可以设置来开启 注意,如果开启守护进程模式,会生成/var/run/redis.pid
保存pid
supervised no
这个不太明白,暂时不翻译了,搞懂后更新
\\n If you run Redis from upstart or systemd, Redis can interact with your\\n supervision tree. Options:\\n supervised no - no supervision interaction\\n supervised upstart - signal upstart by putting Redis into SIGSTOP mode\\n supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\\n supervised auto - detect upstart or systemd method based on\\n UPSTART_JOB or NOTIFY_SOCKET environment variables\\n Note: these supervision methods only signal \\"process is ready.\\"\\n They do not enable continuous liveness pings back to your supervisor.\\n
\\npidfile /var/run/redis_6379.pid
pid文件路径 ,默认值/var/run/redis.pid
如果在非守护进程模式下,而且也没配置pidfile路径,那么不会生成pid文件。如果是守护进程模式, pidfile总会生成,没配置pidfile就会用默认路径。
loglevel notice
指定服务的日志级别:
\\nlogfile \\"\\"
指定redis日志文件名称和路径。你也可以设置logfile \\"\\"
强制redis将日志输出的标准输出。 注意,如果你使用标准输出,而且redis使用守护进程模式运行,那log日志会被发送给/dev/null,就没了
syslog-enabled no
To enable logging to the system logger, just set \'syslog-enabled\' to yes,\\nand optionally update the other syslog parameters to suit your needs.\\n
\\nsyslog-ident redis
Specify the syslog identity.
\\nsyslog-facility local0
Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
\\ndatabases 16
always-show-logo yes
搞笑配置,永远显示redis的logo
\\n开启RDB持久化,save <seconds> <changes>
\\nsave 900 1
:就是900秒有一次更改就做一次rdb快照到磁盘。
\\n禁用rdb就是注释掉save行,如果你配置了save \\"\\"
,也可以禁用rdb。
\\n下面是默认值
save 900 1\\nsave 300 10\\nsave 60 10000\\n
\\nstop-writes-on-bgsave-error yes
默认情况下,如果RDB快照功能开启而且最后一次rdb快照save失败时,redis会停止接收写请求, 这其实就是一种强硬的方式来告知用户数据持久化功能不正常,否则没有人会知道当前系统出大问题了。 如果后台save进程正常工作了(正常保存了rdb文件),那么redis会自动允许写请求。 不过如果你已经设置了一些监控到redis服务器,你可能想要禁用这个功能,这样redis在磁盘出问题时依旧可以继续处理写请求。只要set stop-writes-on-bgsave-error yes
rdbcompression yes
使用LZF算法对rdb文件进行压缩,如果要节省一些CPU,可以设置为no。
\\nrdbchecksum yes
自从redis 5.0,rdb文件的末尾会设置一个CRC64校验码(循环冗余码)。这可以起到一定的的纠错作用,但是也要 付出10%的性能损失,你可以关闭这个功能来获取最大的性能。 如果rdb文件校验功能关闭,那么系统读取不到检验码时会自动跳过校验。 rdbchecksum yes
dbfilename dump.rdb
rdb文件名
\\ndir ./
redis的工作目录 ,aof文件,rdb文件还有redis cluster模式下的node.conf文件均会创建在这个目录下。
\\nslaveof <masterip> <masterport>
主从复制。使用slaveof配置将redis实例变为其他redis服务器的一个拷贝。
\\nmasterauth <master-password>
如果主节点有密码,从节点必须配置这个密码,否则主节点拒绝复制请求。
\\nslave-serve-stale-data yes
当主从同步失败时,从节点有两种行为:
\\nslave-read-only yes
你可以设置从节点能够处理写请求。向从节点写入一些临时数据有时候是有用的(因为数据在resync后很快就会删除),如果配错了也可能会造成一些问题。 2.6版本以后默认都是read-only。 read-only不是设计成对抗那些不可信的客户端的。只是怕客户端用错命令。read-only模式下一些管理类命令还是会输出的。如果要限制这种命令,你可以使用rename-command来重命名那些管理类命令
\\nrepl-diskless-sync no
主从同步策略:disk或socket。
\\n警告:diskless复制目前只是试验阶段 当出现新的从节点或重连的从节点无法进行增量同步时,就需要做一次全量同步(full synchronization)。 一个RDB文件会从主节点传输到从节点,传输方式有两种:
\\n使用disk-backed复制,在rdb文件生成完毕后,主节点会为每个从节点创建队列来传说RDB文件,直到传输结束。 使用diskless复制,一旦开始传输rdb,当时有多少从节点建立连接,就只能并行传输多少从节点,如果此时有新的从节点发起全量同步,就只能等之前的都传完。 如果使用diskless复制,主节点会在传输之前等待一小段时间(这个时间可以配置),这样可以让多个从节点到达 ,并做并行传输。 如果磁盘贼慢,网络带宽特别好,diskless复制策略效果会更好一些。
\\nrepl-diskless-sync-delay 5
如果开启了diskless复制,需要配置一个延迟时间,让主节点等待所有从节点都到达。 这是非常重要的,因为一旦开始传输,主节点就无法响应新的从节点的全量复制请求,只能先到队列中等待下一次RDB传输,所以主节点需要等待一段时间,让所有从节点全量复制都到达。 这个延迟时间的单位是秒,默认是5秒。关闭这个特性可以将其设置成0,这样传输总是马上开始。
\\nrepl-ping-slave-period 10
从节点在一定间隔时间发送ping到主节点。默认是是10秒。
\\nrepl-timeout 60
这个值对三个场景都有效:
\\n注意,这个值一定要设置的比repl-ping-slave-period
大,否则每次心跳检测都超时
repl-disable-tcp-nodelay no
在从节点socket 发起SYNC同步后是否需要关闭TCP_NODELAY? 如果选择YES,redis会使用较小的tcppacket和较小的带宽去发送数据到从节点。但是这会让主从复制增加部分延迟,差不多40毫秒,取决于linux kernel配置。 如果选择no,主从复制延迟会稍微减少,但是会消耗更大的网络带宽。 默认我们倾向于低延迟,但是如果网络状况不好的情况时将这个选项置为yes或许是个好方案。
\\nrepl-backlog-size 1mb
设置主从复制backlog大小。backlog是一个缓冲区。 当主从不同步时,主节点缓存主从复制数据到backlog缓冲区中,当从节点重新连接到主节点时,从节点可以从缓冲区中拿到增量同步数据,并进行增量同步(partitial synchronization)。 backlog越大,允许从节点断线的时间就越长。backlog缓冲区只有在最少有一个从节点连接时才会创建。
\\nrepl-backlog-ttl 3600
如果主节点再也没有连接到从节点,那个从节点的backlog会被释放。 当从节点断线开始,这个配置的时间就开始计时了。单位是秒。 设置为0说明永远都不会释放backlog。
\\nslave-priority 100
这个配置是给哨兵模式用的,当主节点挂掉时,哨兵会选取一个priority最小的从节点去升主,如果某个redis节点的这个值配成0,那么这个节点永远都不会被升为主节点。默认值就是100。
\\nmin-slaves-to-write 3和min-slaves-max-lag 10
如果lag秒内主节点在线的从节点少于N个,主节点停止接收写请求。 例如10秒内最少3个从节点在线时,主节点才接受写请求,可以用如下配置:
\\nmin-slaves-to-write 3\\nmin-slaves-max-lag 10\\n
\\n将这两个配置任意一个设置为0,就禁用此功能。默认是禁用的。
\\nslave-announce-ip 5.5.5.5
和 slave-announce-port 1234
有多种方式可以显示主节点当前在线的从节点的ip和端口。 例如,info replication 部分,或者在主节点执行ROLE命令。
\\n#\\n# The listed IP and address normally reported by a slave is obtained\\n# in the following way:\\n#\\n# IP: The address is auto detected by checking the peer address\\n# of the socket used by the slave to connect with the master.\\n#\\n# Port: The port is communicated by the slave during the replication\\n# handshake, and is normally the port that the slave is using to\\n# list for connections.\\n#\\n# However when port forwarding or Network Address Translation (NAT) is\\n# used, the slave may be actually reachable via different IP and port\\n# pairs. The following two options can be used by a slave in order to\\n# report to its master a specific set of IP and port, so that both INFO\\n# and ROLE will report those values.\\n#\\n# There is no need to use both the options if you need to override just\\n# the port or the IP address.\\n#\\n# slave-announce-ip 5.5.5.5\\n# slave-announce-port 1234\\n
\\nrequirepass foobared
给redis设置密码,因为redis快的一逼,一秒钟攻击者能尝试150000次密码,所以你的密码必须非常强壮否则很容易被暴力破解。
\\nrename-command CONFIG \\"\\"和rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
完全杀掉一个命令就用rename-command CONFIG \\"\\"
\\nrename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
可以将命令改掉,这样彩笔程序员就不会使用危险命令了。
注意,如果你把命令给改名了,那么从节点什么的都要统一改名字,否则会有问题。
\\nmaxclients 10000
设置同一时刻的最大客户端数。 默认值是10000,只要达到最大值,redis会关闭所有新的链接,并且发送一个错误“max number of clients readched”给客户端。
\\n设置一个内存的最大值。当内存达到最大值之后,redis会按照选择的内存淘汰策略去删除key。 如果redis根据淘汰策略无法删除key,或者淘汰策略是noeviction,客户端发送写请求时redis会开始返回报错,并且不会使用更多的内存。但是读请求还是会继续支持的。 注意,如果你有很多从节点,那么内存设置不能太大,否则从节点发起全量同步时,output buffer占用的内存也在这个maxmemory的范围内,例如,最大值配的是4GB,如果内存已经3G了,此时一个从节点发起全量同步,outputbuffer你设置的是2G这样内存直接就满了,然后就要开始淘汰key,这肯定不是我们想要的。
\\nmaxmemory-policy noeviction
内存淘汰策略,决定了当redis内存满时如何删除key。 默认值是noeviction。
\\nLRU means Least Recently Used LFU means Least Frequently Used
\\nLRU, LFU and volatile-ttl 基于近似随机算法实现。 注意,使用上述策略时,如果没有合适的key去删除时,redis在处理写请求时都会返回报错。
\\n写明令: set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort。
\\nmaxmemory-samples 5
LRU, LFU and minimal TTL algorithms 不是精确的算法,是一个近似的算法(主要为了节省内存), 所以你可以自己权衡速度和精确度。默认redis会检查5个key,选择一个最近最少使用的key,你可以改变这个数量。默认的5可以提供不错的结果。你用10会非常接近真实的LRU但是会耗费更多的CPU,用3会更快,但是就不那么精确了。
\\nredis有两个删除key的基本命令。一个是DEL,这是一个阻塞的删除。DEL会让redis停止处理新请求,然后redis会用一种同步的方式去回收DEL要删除的对象的内存。如果这个key对应的是一个非常小的对象,那么DEL的执行时间会非常短,接近O(1)或者O(log n)。不过,如果key对应的对象很大,redis就会阻塞很长时间来完成这个命令。鉴于上述的问题,redis也提供了非阻塞删除命令,例如UNLINK(非阻塞的DEL)和异步的删除策略:FLUSHALL和FLUSHDB,这样可以在后台进行内存回收。这些命令的执行时间都是常量时间。一个新的线程会在后台渐进的删除并释放内存。
\\n上面说的那些命令都是用户执行的,具体用哪种命令,取决于用户的场景。 但是redis本身也会因为一些原因去删除key或flush掉整个内存数据库。 除了用户主动删除,redis自己去删除key的场景有以下几个:
\\nlazyfree-lazy-eviction no\\nlazyfree-lazy-expire no\\nlazyfree-lazy-server-del no\\nslave-lazy-flush no\\n
\\nappendonly no
默认情况下,redis异步的dump内存镜像到磁盘(RDB)。这个模式虽然已经很不错了,但是如果在发起dump之前机器宕机,就会丢失一些数据。 AOF(Append only file)是一种可选的持久化策略提供更好数据安全性。使用默认配置的情况下, redis最多丢失一秒钟的写入数据,你甚至可以提高级别,让redis最多丢失一次write操作。 AOF和RDB持久化可以同时开启。如果开了AOF,redis总会先加载AOF的文件,因为AOF提供更高的可用性。
\\nappendfilename \\"appendonly.aof\\"
aof 文件的名称。
\\nappendfsync everysec
对操作系统的fsync()调用告诉操作系统将output buffer中的缓冲数据写入到磁盘。有些操作系统会 真正的写磁盘,有一些会尽量去写,也可能会等一下。 redis 支持三种方式:
\\n默认就是everysec,一般也是推荐的策略,平衡了速度和数据安全性。
\\nappendfsync always\\nappendfsync everysec\\nappendfsync no\\n
\\nno-appendfsync-on-rewrite no
当AOF fsync 策略设置成always或者everysec,而且一个后台的save进程(可能RDB的bgsave进程,也可能是 AOF rewrite进程)正在执行大量磁盘I/O操作,在一些linux配置中,redis可能会对fsync()执行太长的调用。 这个问题目前没什么办法修复,也就是说就算起一个后台进程去做fsync,如果之前已经有进程再做fsync了,后来的调用 会被阻塞。 为了缓和这个问题,可以使用下面的配置,当已经有BGSAVE和BGREWRITEAOF在做fsync()时,就不要再起新进程 了。 如果已经有子进程在做bgsave或者其他的磁盘操作时,redis无法继续写aof文件,等同于appendsync none。 在实际情况中,这意味着可能会丢失多达30秒的日志。也就是说,这是会丢数据的,如果对数据及其敏感,要注意这个问题。 如果你有延迟类问题,可以设置成yes,否则设置为no,这样能保证数据的安全性最高,极少丢数据。
\\nauto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb
自动重写aof文件。当aof文件增大到某个百分比时,redis会重写aof文件。 redis会记住上次rewrite后aof文件的大小(如果启动后还没发生过rewrite,那么会使用aof原始大小)。 这个size大小会和当前aof文件的size大小做比较。如果当前size大于指定的百分比,就做rewrite。 并且,还要指定最小的size,如果当前aof文件小于最小size,不会触发rewrite,这是为了防止文件其实很小,但是 已经符合增长百分比时的多余的rewrite操作。 如果指定percentage为0代表禁用aof rewrite功能
\\naof-load-truncated yes
当Redis启动时会加载AOF文件将数据还原到内存中,但是有时候这个AOF的文件可能被损坏掉了 ,例如文件末尾是坏的。这种情况一般都是由于redis宕机导致的,尤其是使用ext4文件系统挂载 时没配置 data=ordered选项。 在这种情况下,redis可以直接报错,或者尽可能的读取剩余可读的AOF文件。
\\n如果 aof-load-truncated=yes,redis依然会读取这个损坏的aof文件,但是会打出一个报错日志, 通知用户。 如果 aof-load-truncated=no,redis就会报错并拒绝启动服务,用户需要使用redis-check-aof工具 修复aof文件,再启动redis。 如果redis运行时aof文件崩溃,redis依然会报错并退出。这个选项救不了这种情况。
\\naof-use-rdb-preamble no
当redis重写aof文件时,redis可以先读一个rdb来加快重写的速度,当这个选项打开时,重写的aof文件由 两部分组成:rdb文件+aof文件。 当redis启动时加载的aof文件以 \\"REDIS\\"开头,就会加载rdb文件,然后再读取剩余的AOF文件。 默认这个选项是关闭的,
\\nlua-time-limit 5000
表示一个lua脚本的最大执行毫秒数。 如果执行时间达到了最大时间,redis会log这个脚本已经超时了,并且会报个error。 当一个脚本执行超时,只有SCRIPT KILL
和SHUTDOWN NOSAVE
命令是可用的。第一个命令可以去 停止一个不包含写命令的脚本。第二个命令是唯一一个可以停掉超时写命令的脚本。 将lua-time-limit设置成0或负数表示你不限制执行时间,并且不会有任何警告。
cluster-enabled yes
普通的redis实例无法成为cluster的一员的;只有node可以。想要将节点加入redis cluster,需要将cluster-enabled设置为yes。
\\ncluster-config-file nodes-6379.conf
所有cluster node都有一个cluster配置文件。这个文件不是为了人工编辑的,是redis自己创建的。 每个redis-node都要使用不同的cluster配置文件。一定要确保运行在同一个系统中的多个redis cluster节点使用的是不同的redis配置文件,不要互相覆盖。
\\ncluster-node-timeout 15000
cluster node timeout是一个节点无响应的最长毫秒数。大多数超时时间限制都是这个值的倍数。
\\ncluster-slave-validity-factor 10
一个主节点宕机后,如果它的从节点A数据太旧(长期处于未同步状态),那么A不会触发failover, 它不会升为主。 没有一个简单方式去策略一个从节点的“数据年龄” 。下面提供了两种方式来评估从节点的数据是否过老:
\\n(node-timeout * slave-validity-factor) + repl-ping-slave-period
,从节点就不会发生failover。 例如,如果node-timeout=30秒,slave-validity-factor=10,repl-ping-slave-period=10秒, 如果从节点与主节点上次交互时间已经过去了310秒,那么从节点就不会做failover。 调大slave-validity-factor会允许从节点持有过旧的数据时提升为主节点,调小这个值可能会 导致从节点永远都无法升为主节点。 考虑最高的可用性,可以将slave-validity-factor
设置为0,这样从节点会忽略和主节点的上次 交互时间,永远都会尝试去做failover。(但是依然会做延迟选举的操作)cluster-migration-barrier 1
从节点可以迁移至孤儿主节点(这种主节点没有从节点)。 从节点只有在原来的主节点最少有N个从节点时才会迁移到其他的孤儿主节点,这个给定的数字N就是migration-barrier,也叫迁移临界点。migration barrier=1代表,主节点如果有2个从节点, 当集群中出现孤儿主节点时,其中一个从节点可以被迁移过去。 想要禁止从节点迁移可以将这个值设置成很大的值,例如999。 只有在debug模式才可以将这个值设置为0,生产环境别乱设置。
\\ncluster-require-full-coverage yes
默认情况下,redis cluster在发现还有最少1个hash slot没有被分配时会禁止查询操作。 **这样的话,如果cluster出现部分宕机时,整个集群就不可用了。**只有在其他的hash slot都被分配才可以。 你可能会需要cluster的子集可以继续提供服务,要想这样,只要设置cluster-require-full-coverage no
即可
cluster-slave-no-failover no
这个选项如果设置为yes,在主节点宕机是,从节点永远都不会升为主。但是主节点依然可以执行常规的failover。 在多数据中心的场景下,这个配置会比较有用,我们希望某一个数据中心永远都不要升级为主节点,否则主节点就漂移到别的数据中心了,这可能挺麻烦的。
\\n在一些特定的部署场景下,redis cluster 节点地址自动发现会失败,因为地址被NAT了,或者端口 被转发了(Docker容器中)。 为了让redis cluster在这种环境下正常工作,需要静态配置地址和端口,具体配置如下:
\\nslowlog-log-slower-than 10000
和slowlog-max-len 128
redis slow log是系统记录慢操作的,只要超过了给定的时间,都会记录。 执行时间不包括I/O操作的时间。 你可以通过两个参数来配置slow log:
\\nlatency-monitor-threshold 0
redis 延迟监控系统会在运行时抽样一部分命令来帮助用户分析redis卡顿的原因。 通过LATENCY
命令可以打印一些视图和报告。 redis只会记录那些大于设定毫秒数的命令。 如果要关闭这个功能,就将latency-monitor-threshold
设置为0。 默认情况下monitor是关闭的,没有延迟问题不要一直开着monitor,因为开这个功能可能会对性能有很大影响。 在运行时也可以开这个功能,执行这个命令即可:CONFIG SET latency-monitor-threshold <milliseconds>
notify-keyspace-events \\"\\"
当特定的key space有事件发生时,redis 可以通知 pub/sub 客户端。 如果开启事件通知功能,一个client对key\\"foo\\"执行了del操作,通过pub/sub,两条消息会被推送:
\\n当数据量很少时,哈希值可以使用一种更高效的数据结构。这个阈值可以使用以下的配置来设置:
\\nhash-max-ziplist-entries 512\\nhash-max-ziplist-value 64\\n
\\nlist-max-ziplist-size -2
list也可以使用一种特殊编码方式来节省内存。list底层的数据结构是quicklist,quicklist的每一个 节点都是一个ziplist,这个参数主要来控制每个ziplist的大小,如果配置正数, 那么quicklist每个ziplist中的节点数最大不会超过配置的值。 如果配置负数,就是指定ziplist的长度:
\\nlist-compress-depth 0
list也可以被压缩。 list底层是一个双向链表,压缩深度代表除了head和tail节点有多少node不会被压缩。 head和tail节点是永远都不会被压缩的。
\\nset-max-intset-entries 512
set也支持内部优化,当set内部元素都是64位以下的十进制整数时,这个set的底层实现会使用intset, 当添加的元素大于set-max-intset-entries时,底层实现会由intset转换为dict。
\\nzset-max-ziplist-entries 128
和zset-max-ziplist-value 64
当zset内部元素大于128,或者value超过64字节时,zset底层将不再使用ziplist
\\nhll-sparse-max-bytes 3000
HyperLogLog稀疏表示字节限制。这个限制包括16字节的header。当HyperLogLog使用稀疏表示时,如果 达到了这个限制,它将会转换成紧凑表示。 这个值设置成大于16000是没意义的,因为16000时用紧凑表示对内存会更友好。 推荐的值是0~3000,这样可以布降低PFADD命令的执行时间时还能节省空间。如果内存空间相对cpu资源更紧张, ,可以将这个值提升到10000。
\\nactiverehashing yes
动态rehash使用每100毫秒中的1毫秒来主动为Redis哈希表做rehash操作。Redis的哈希表实现默认使用 一种lazy rehash模式:你对hash表的操作越多,越多rehash步骤会被执行,所以如果服务在空闲状态下, rehash操作永远都不会结束,而且hash表会占用更多的内存。 默认会每秒做10次动态rehash来释放内存。
\\nclient output buffer限制是用来强制断开client连接的,当client没有及时将缓冲区的数据读取完 时,redis会认为这个client可能出现宕机,就会断掉连接。
\\n这个限制可以根据三种不同的情况去设置:
\\nclient-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
默认情况下normal的clients不会有这个限制,因为normal的clients获取数据都是先执行个命令,不存在 redis主动给normal推送数据的情况。 如果设置三个0代表无限制,永远不断连接。 但是这样可能会撑爆内存。
\\nclient-output-buffer-limit normal 0 0 0\\nclient-output-buffer-limit slave 256mb 64mb 60\\nclient-output-buffer-limit pubsub 32mb 8mb 60\\n
\\nclient-query-buffer-limit 1gb
client query buffer缓冲新的命令。默认使用一个固定数量来避免protocol desynchronization。
\\nproto-max-bulk-len 512mb
redis协议中,大多数的请求,都是string,默认value不会大于512mb,当然,你可以改这个限制。
\\nhz 10
redis调用一些内部函数来执行很多后台任务,像关闭超时连接,清理从未请求的过期的key等等。 不是所有的后台任务都使用相同的频率来执行,redis使用hz参数来决定执行任务的频率。 默认hz是10.提高这个值会在redis空闲时消耗更多的cpu,但是同时也会让redis更主动的清理过期 key,而且清理超时连接的操作也会更精确。 这个值的范围是1~500,不过并不推荐设置大于100的值。多数的用户应该使用默认值,或者最多调高到100。
\\naof-rewrite-incremental-fsync yes
当子进程重写aof文件是,如果这个功能开启,redis会以每32MB的数据主动提交到文件。这种 递增提交文件到磁盘可以避免大的延迟尖刺。
\\nredis lfu淘汰策略可以调优。 每个key的LFU counter只有8个key,最大值是255,所以redis使用一个基于概率的对数增长算法, 并不是每次访问key都会counter+1。当一个key被访问后,会按照以下方式去做counter+1:
\\n默认 lfu-log-factor 是 10. 下面是不同的factor下的增长速度,可以看到,factor越小增长越快:
\\nlua\\n代码解读\\n复制代码\\n +--------+------------+------------+------------+------------+------------+\\n | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |\\n +--------+------------+------------+------------+------------+------------+\\n | 0 | 104 | 255 | 255 | 255 | 255 |\\n +--------+------------+------------+------------+------------+------------+\\n | 1 | 18 | 49 | 255 | 255 | 255 |\\n +--------+------------+------------+------------+------------+------------+\\n | 10 | 10 | 18 | 142 | 255 | 255 |\\n +--------+------------+------------+------------+------------+------------+\\n | 100 | 8 | 11 | 49 | 143 | 255 |\\n +--------+------------+------------+------------+------------+------------+\\n\\n\\n 注意: 上表结论是执行以下命令得出的:\\n\\n redis-benchmark -n 1000000 incr foo\\n redis-cli object freq foo\\n\\n 注意 2: counter初始值是5,否则key很快就被淘汰了\\n
\\nlfu-decay-time我还不太理解,等研究透彻后补充
\\nlfu-log-factor 10 lfu-decay-time 1
\\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\\n# even in production and manually tested by multiple engineers for some\\n# time.\\n#\\n# What is active defragmentation?\\n# -------------------------------\\n#\\n# Active (online) defragmentation allows a Redis server to compact the\\n# spaces left between small allocations and deallocations of data in memory,\\n# thus allowing to reclaim back memory.\\n#\\n# Fragmentation is a natural process that happens with every allocator (but\\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\\n# restart is needed in order to lower the fragmentation, or at least to flush\\n# away all the data and create it again. However thanks to this feature\\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\\n# in an \\"hot\\" way, while the server is running.\\n#\\n# Basically when the fragmentation is over a certain level (see the\\n# configuration options below) Redis will start to create new copies of the\\n# values in contiguous memory regions by exploiting certain specific Jemalloc\\n# features (in order to understand if an allocation is causing fragmentation\\n# and to allocate it in a better place), and at the same time, will release the\\n# old copies of the data. This process, repeated incrementally for all the keys\\n# will cause the fragmentation to drop back to normal values.\\n#\\n# Important things to understand:\\n#\\n# 1. This feature is disabled by default, and only works if you compiled Redis\\n# to use the copy of Jemalloc we ship with the source code of Redis.\\n# This is the default with Linux builds.\\n#\\n# 2. You never need to enable this feature if you don\'t have fragmentation\\n# issues.\\n#\\n# 3. Once you experience fragmentation, you can enable this feature when\\n# needed with the command \\"CONFIG SET activedefrag yes\\".\\n#\\n# The configuration parameters are able to fine tune the behavior of the\\n# defragmentation process. If you are not sure about what they mean it is\\n# a good idea to leave the defaults untouched.\\n\\n# Enabled active defragmentation\\n# activedefrag yes\\n\\n# Minimum amount of fragmentation waste to start active defrag\\n# active-defrag-ignore-bytes 100mb\\n\\n# Minimum percentage of fragmentation to start active defrag\\n# active-defrag-threshold-lower 10\\n\\n# Maximum percentage of fragmentation at which we use maximum effort\\n# active-defrag-threshold-upper 100\\n\\n# Minimal effort for defrag in CPU percentage\\n# active-defrag-cycle-min 25\\n\\n# Maximal effort for defrag in CPU percentage\\n# active-defrag-cycle-max 75\\n
","description":"Redis配置文件详解 配置文件版本使用的是redis 4.0.14, 某些参数需要处理了解linux kernel,笔者不太了解linux内核参数,后面还要继续努力呀。\\n\\n1. 常规命令\\n1.1 ./redis-server /path/to/redis.conf\\n\\n启动redis并使配置文件生效\\n\\n1.2 include /path/to/local.conf\\n\\ninclude 可以使用多个配置文件,如果配置文件有相同值,后面的会覆盖前面的:\\n\\ninclude /path/to/local.conf\\ninclude /path/to/other.conf…","guid":"https://juejin.cn/post/7481600472109547558","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T01:09:13.837Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"后端老兵的AI进化论:如何借力大模型浪潮重构技术护城河","url":"https://juejin.cn/post/7481601410230272035","content":"随着DeepSeek-R1
的爆火,突然意识到一个残酷事实:曾经引以为傲的分布式架构设计能力,正在被大模型自动生成架构图的能力解构;引经据典的性能优化经验,逐渐被AI实时诊断系统取代。作为10年+后端Lnmp老兵,我深刻感受到——技术进化的齿轮正被AI重新锻造,想要成为未来的幸存者必须拥抱AI,做AI的朋友。
对话式AI首先改变的是检索方式,以前都是在百度、博客、Github里检索需要的专业技能知识,而现在使用AI的应用检索更加精准高效。
\\n最开始使用的对话式AI是豆包,整洁的页面十分吸引人,文档也可以快速生成,能解决大部分的技术问题,同样的一个问题chatGPT能给出答案,但豆包还差点意思,但是比传统的百度高效许多,基于字节跳动的封闭生态数据池(如头条、抖音内容),结合开源语料进行预训练,数据质量高且格式规范,但覆盖领域受平台内容限制,长尾知识可能不足,依赖单一生成式大模型(如云雀LLM),通过MoE(混合专家)架构动态分配计算资源,支持复杂任务(如代码生成),但存在幻觉风险。
\\n随着对DeepSeek的使用和了解,DeepSeek对中文的理解和推理能力好过其他单纯使用大模型训练的对话式AI,Deepseek聚焦垂直领域AGI研发,采用“小规模专家模型+领域知识库”架构。其模型参数量级较小(如百亿级),通过行业数据微调与知识增强技术(如RAG)提升专业场景(如金融、客服)的准确性与可控性。
\\nDeepSeek的本地部署也很简单,之前使用ChatBox的方式在我本地部署,但是超时比较严重,推荐直接使用DeepSeek满血版,之前DeepSeek一直超时,可以使用腾讯元宝作为平替代替。
\\n如果是刚入坑的小伙伴可以看我之前的博客,新手快速安装部署本地DeepSeek,顺便在多说一嘴,B站有个讲DeeSeek的教程也是干货满满,可点击跳转,市面上也有很多讲解Deepseek的书籍,不建议购买李尚龙的那本,基本上没有什么价值。
\\n作为拥有十余年后端开发经验的工程师,我见证过IDE从纯文本编辑器到智能编程工具的演进历程。当前AI代码助手已不再是实验室概念,而是渗透到日常开发的效率倍增器,我之前一直使用的是JetBrains IDE系列的全家桶。
\\n1.腾讯云AI助手
\\n腾讯云AI助手是以插件的形式进行安装和使用,腾讯云社区有专门的介绍和安装,官方的手册比较丰富和清晰,我在这里不做过多的赘述,请注意我下图中画圈的功能,功能非常强大 点击跳转
\\n腾讯云AI助手,很方便唤醒,我尝试优化了一个已有Nginx配置文件,它清晰的帮我对比了优化前和优化后的文件对比,并给出了很多优化建议,但是也存在一个问题,我点击应用的时候,第一我不能明确的知道它到底帮我生成了没有,如果生成了,文件存放在了哪里?
\\n腾讯云AI助手的 代码提示、上下文感知能力就非常强悍和高效了,首先能帮助根据上下文生成代码,可以使用Tab键进行补齐,而且自动提示的代码85%都是正确的,可替代大量无脑的CRUD操作。
\\n腾讯云AI助手可以对代码进行提问,有时候它还根据上下文给你更多惊喜,帮我解决了大部分的难题,适合于项目迭代、开发的日常使用。
\\n2.Cursor
\\n很多同学肯定是,用过 Cursor 了,Cursor 作为世界级的第一款 AI IDE,效果的成熟度和规划都处于领先地位,新人注册可以给150次对话的机会,自动补齐不做限制,但它的提问是收费的,得办理一个信用卡,使用银联支付,体验还是非常丝滑。
\\n3.Trae
\\n字节有了一款国产AI编辑器 Trae ,点击前往官网,现阶段处于免费和大力推广的阶段,在未来可能也会进行收费,Trae 编辑器有个Builder模式,可以使用简单的命令进行自己实现,代码在自动执行时可以审查,接收或者驳回。
\\n编辑器使用的资料可以在官方文档上查询,也可以去掘金小册中学习,Trae 从入门到实践:AI 编码的妙笔生花 。
\\n网上有很多关于AI的预测和猜想,在最后向你简单表达我对AI的思考,首先人类是群居想象力动物,无论科技如何进步,都不能改变人性的本质,就像工业革命改变了人类的生活方式,旧职业消亡,但随之而来的是新职业、新岗位的诞生,世界是永恒变化的。
\\nAI技术的快速发展将重塑程序员职业版图,但不会取代其核心价值,短期内,编程工具链将逐步智能化:代码生成、自动化测试、DevOps优化等领域将率先实现AI增强,基础编码需求降低30%-50%。初级程序员可能面临岗位缩减,但高级人才需求将持续增长。
\\n咱们要做的是拥抱变化,和AI对话,做AI的朋友。
","description":"概述 随着DeepSeek-R1的爆火,突然意识到一个残酷事实:曾经引以为傲的分布式架构设计能力,正在被大模型自动生成架构图的能力解构;引经据典的性能优化经验,逐渐被AI实时诊断系统取代。作为10年+后端Lnmp老兵,我深刻感受到——技术进化的齿轮正被AI重新锻造,想要成为未来的幸存者必须拥抱AI,做AI的朋友。\\n\\n对话式AI\\n\\n对话式AI首先改变的是检索方式,以前都是在百度、博客、Github里检索需要的专业技能知识,而现在使用AI的应用检索更加精准高效。\\n\\n最开始使用的对话式AI是豆包,整洁的页面十分吸引人,文档也可以快速生成,能解决大部分的技术问题…","guid":"https://juejin.cn/post/7481601410230272035","author":"stark张宇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-15T15:19:50.578Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c852197b0d2345219182839dd27c5e76~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=VuAAEVO7wMPucwfM%2B3SlM6XYwTc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b859a6d3592941fdab3d32e0e23e38c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=u0%2BBM9LyLR5hKFOKyGhwPRMZ4Yg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/877e1d6978034d40ae688177767ae804~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=CjOegRuSgtWxuxj%2FURglgcS0VoQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c2b0032635604c79811915570d87a625~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=ZFEZKkgmjMwL3Y00XNCHv1d%2FO3o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49ea2aace99641c28c31859a8b9ad0d9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=IT2CNKtnjVAXNWf7VoFp7mRkwjw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d5d8a6f1aa424e2ab62453dbf80308ab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3RhcmvlvKDlroc=:q75.awebp?rk3s=f64ab15b&x-expires=1743036989&x-signature=JRKZ9wjSS1YLvKpQm%2BMoVzeaMLw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Trae","DeepSeek"],"attachments":null,"extra":null,"language":null},{"title":"业务架构设计---报表_BI大屏_预警等等Java企业级架构","url":"https://juejin.cn/post/7481581369630621723","content":"INSERT INTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN),(VALUE1,VALUE2...,VALUEN),(VALUE1,VALUE2...,VALUEN);\\n
\\nINSERT ALL\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nSELECT * FROM DUAL\\n
\\n @Transactional(\\n rollbackFor = {Exception.class}\\n )\\n public boolean saveBatch(Collection<T> entityList, int batchSize) {\\n String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);\\n return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {\\n sqlSession.insert(sqlStatement, entity);\\n });\\n }\\n
\\n缺点是每个表都要手动编写xml,优点是效率较高
\\n<insert id=\\"batchInsert\\" parameterType=\\"java.util.List\\">\\ninsert into user (id, name, age)values\\n<foreach collection=\\"list\\" item=\\"user\\" separator=\\",\\">\\n (#{user.id}, #{user.name}, #{user.age})\\n</foreach>\\n</insert>\\n
\\n// mapper.xml\\n<insert id=\\"batchInsert\\" parameterType=\\"java.util.List\\">\\ninsert all\\n<foreach collection=\\"list\\" item=\\"user\\" separator=\\",\\">\\ninto user (id, name, age) values(#{user.id}, #{user.name}, #{user.age})\\n</foreach>\\nselect * from dual\\n</insert>\\n
\\n底层也是拼接sql,但无需手动编写sql语句,效率同第二种,本文重点介绍这种方式,使用步骤:
\\nMySQL版
\\npublic class MySqlInjector extends DefaultSqlInjector {\\n\\n @Override\\n public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {\\n List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);\\n methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));\\n return methodList;\\n }\\n}\\n
\\nOracle版
\\nimport com.baomidou.mybatisplus.annotation.FieldFill; \\nimport com.baomidou.mybatisplus.core.injector.AbstractMethod; \\nimport com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; \\n \\nimport java.util.List; \\n \\npublic class OracleInjector extends DefaultSqlInjector { \\n \\n@Override \\npublic List<AbstractMethod> getMethodList(Class<?> mapperClass) { \\nList<AbstractMethod> methodList = super.getMethodList(mapperClass); \\n//这里改成我们自己的实现MyInsertBatchSomeColumn \\nmethodList.add(new OracleInsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); \\nreturn methodList; \\n} \\n}\\n
\\n@Configuration \\npublic class MyBatisConfig { \\n@Bean \\npublic OracleInjector sqlInjector() { \\nreturn new OracleInjector(); \\n}\\n}\\n
\\npublic interface MyBaseMapper<T> extends BaseMapper<T> {\\n /**\\n * 以下定义的 4个 method 其中 3 个是内置的选装件\\n */\\n int insertBatchSomeColumn(List<T> entityList);\\n}\\n
\\n@Mapper\\npublic interface UserMapper extends MyBaseMapper<Student> {\\n\\n}\\n
\\n先了解下,Oracle批量插入数据SQL
\\nINSERT ALL\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nINTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)\\nSELECT * FROM DUAL\\n
\\n因此我们需要把SQL组装成这种结构,查看InsertBatchSomeColumn类,可以发现SQL组装逻辑在injectMappedStatement方法,因此我们模仿InsertBatchSomeColumn类,编写SQL组装逻辑
\\nimport com.baomidou.mybatisplus.annotation.IdType;\\nimport com.baomidou.mybatisplus.core.enums.SqlMethod;\\nimport com.baomidou.mybatisplus.core.metadata.TableFieldInfo;\\nimport com.baomidou.mybatisplus.core.metadata.TableInfo;\\nimport com.baomidou.mybatisplus.core.metadata.TableInfoHelper;\\nimport com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;\\nimport lombok.AllArgsConstructor;\\nimport lombok.NoArgsConstructor;\\nimport lombok.Setter;\\nimport lombok.experimental.Accessors;\\nimport org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;\\nimport org.apache.ibatis.executor.keygen.KeyGenerator;\\nimport org.apache.ibatis.executor.keygen.NoKeyGenerator;\\nimport org.apache.ibatis.mapping.MappedStatement;\\nimport org.apache.ibatis.mapping.SqlSource;\\n\\nimport java.util.List;\\nimport java.util.Map;\\nimport java.util.function.Predicate;\\n\\n@NoArgsConstructor\\n@AllArgsConstructor\\n@SuppressWarnings(\\"serial\\")\\npublic class OracleInsertBatchSomeColumn extends InsertBatchSomeColumn {\\n\\n @Setter\\n @Accessors(chain = true)\\n private Predicate<TableFieldInfo> predicate;\\n\\n private final String INSERT_BATCH_SQL=\\"<script>\\\\nINSERT ALL \\\\n %s\\\\n</script>\\";\\n\\n @SuppressWarnings(\\"Duplicates\\")\\n @Override\\n public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {\\n //pojo类型为Map时禁用\\n if (tableInfo.getEntityType().equals(Map.class)) {\\n return null;\\n }\\n KeyGenerator keyGenerator = new NoKeyGenerator();\\n SqlMethod sqlMethod = SqlMethod.INSERT_ONE;\\n List<TableFieldInfo> fieldList = tableInfo.getFieldList();\\n String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) +\\n this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);\\n String columns = insertSqlColumn.substring(0, insertSqlColumn.length() - 1) ;\\n String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(ENTITY_DOT, false) +\\n this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);\\n insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;\\n String valuesScript = convertForeach(insertSqlProperty, \\"list\\", tableInfo.getTableName(),columns, ENTITY, NEWLINE);\\n String keyProperty = null;\\n String keyColumn = null;\\n // 表包含主键处理逻辑,如果不包含主键当普通字段处理\\n if (tableInfo.havePK()) {\\n if (tableInfo.getIdType() == IdType.AUTO) {\\n /* 自增主键 */\\n keyGenerator = new Jdbc3KeyGenerator();\\n keyProperty = tableInfo.getKeyProperty();\\n keyColumn = tableInfo.getKeyColumn();\\n } else {\\n if (null != tableInfo.getKeySequence()) {\\n keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);\\n keyProperty = tableInfo.getKeyProperty();\\n keyColumn = tableInfo.getKeyColumn();\\n }\\n }\\n }\\n String sql = String.format(INSERT_BATCH_SQL, valuesScript);\\n SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);\\n return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);\\n }\\n public static String convertForeach(final String sqlScript, final String collection, final String tableName,final String columns, final String item, final String separator) {\\n StringBuilder sb = new StringBuilder(\\"<foreach\\");\\n\\n if (StringUtils.isNotBlank(collection)) {\\n sb.append(\\" collection=\\"\\").append(collection).append(\\"\\"\\");\\n }\\n\\n if (StringUtils.isNotBlank(item)) {\\n sb.append(\\" item=\\"\\").append(item).append(\\"\\"\\");\\n }\\n\\n if (StringUtils.isNotBlank(separator)) {\\n sb.append(\\" separator=\\"\\").append(separator).append(\\"\\"\\");\\n }\\n\\n sb.append(\\">\\").append(\\"\\\\n\\");\\n\\n if (StringUtils.isNotBlank(tableName)) {\\n sb.append(\\" INTO \\").append(tableName).append(\\" \\");\\n }\\n\\n if (StringUtils.isNotBlank(columns)) {\\n sb.append(LEFT_BRACKET).append(columns).append(RIGHT_BRACKET).append(\\" VALUES \\");\\n }\\n\\n return sb.append(sqlScript).append(\\"\\\\n\\").append(\\"</foreach>\\\\n\\").append(\\" SELECT \\").append(\\"*\\").append(\\" FROM dual\\").toString();\\n }\\n}\\n
\\n执行批量插入,会发现报错
\\nServlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property=\'__frch_et_0.serialno\', mode=IN, javaType=class java.lang.String, jdbcType=null, numericScale=null, resultMapId=\'null\', jdbcTypeName=\'null\', expression=\'null\'}. Cause: org.apache.ibatis.type.TypeException: Error setting null for parameter #2 with JdbcType OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. Cause: java.sql.SQLException: 无效的列类型: 1111] with root cause\\n
\\n这是因为字段值为NULL时无法确定jdbcType是什么类型,导致插入失败,有两种解决方法,第一种是指定实体所有属性的jdbcType类型,如
\\nimport com.baomidou.mybatisplus.annotation.TableField;\\nimport com.baomidou.mybatisplus.extension.activerecord.Model;\\nimport lombok.Data;\\nimport org.apache.ibatis.type.JdbcType;\\n\\nimport java.io.Serializable;\\nimport java.util.Date;\\n\\n\\n@SuppressWarnings(\\"serial\\")\\n@Data\\npublic class TzVerifyLog extends Model<TzVerifyLog> {\\n private String id;\\n @TableField(value = \\"serialno\\",jdbcType = JdbcType.VARCHAR)\\n private String serialno;\\n @TableField(value = \\"verify_msg\\",jdbcType = JdbcType.VARCHAR)\\n private String verifyMsg;\\n @TableField(value = \\"type\\",jdbcType = JdbcType.VARCHAR)\\n private String type;\\n @TableField(value = \\"row_num\\",jdbcType = JdbcType.INTEGER)\\n private Integer rowNum;\\n\\n @TableField(value = \\"createtime\\",jdbcType = JdbcType.DATE)\\n private Date createtime;\\n\\n /**\\n * 获取主键值\\n *\\n * @return 主键值\\n */\\n @Override\\n protected Serializable pkVal() {\\n return this.id;\\n }\\n}\\n
\\n第二种是设置mybatisplus的jdbc-type-for-null
属性值
mybatis-plus:\\n configuration:\\n jdbc-type-for-null: varchar #空值时设置为varchar类型\\n
\\nservice封装insertBatchSomeColumn方法,方便后面调用
\\nimport com.baomidou.mybatisplus.extension.service.IService;\\n\\nimport java.util.List;\\n\\npublic interface IMyService <T> extends IService<T> {\\n int insertBatchSomeColumn(List<T> entityList);\\n int insertBatchSomeColumn(List<T> entityList,int batchSize);\\n}\\n
\\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\\n\\n\\nimport java.util.ArrayList;\\nimport java.util.List;\\n\\npublic class MyServiceImpl<M extends MyBaseMapper<T>, T>extends ServiceImpl<M,T> implements IMyService<T> {\\n @Override\\n public int insertBatchSomeColumn(List<T> entityList) {\\n return this.baseMapper.insertBatchSomeColumn(entityList);\\n }\\n\\n @Override\\n public int insertBatchSomeColumn(List<T> entityList, int batchSize) {\\n int size=entityList.size();\\n if(size<batchSize){\\n return this.baseMapper.insertBatchSomeColumn(entityList);\\n }\\n int page=1;\\n if(size % batchSize ==0){\\n page=size/batchSize;\\n }else {\\n page=size/batchSize+1;\\n }\\n for (int i = 0; i < page; i++) {\\n List<T> sub = new ArrayList<>();\\n if(i==page-1){\\n sub=entityList.subList(i*batchSize, entityList.size());\\n }else {\\n sub.subList(i*batchSize,(i+1)*batchSize);\\n }\\n if(sub.size()>0){\\n baseMapper.insertBatchSomeColumn(sub);\\n }\\n\\n }\\n return size;\\n }\\n}\\n
\\npublic interface ITzVerifyLogService extends IMyService<TzVerifyLog> { \\n \\n}\\n
\\nimport org.springframework.stereotype.Service; \\n\\n@Service \\npublic class TzVerifyLogServiceImpl extends MyServiceImpl<TzVerifyLogMapper, TzVerifyLog> implements ITzVerifyLogService { \\n \\n}\\n
","description":"MybatisPlus自定义insertBatchSomeColumn组件实现真正批量插入 一、批量插入数据SQL\\nMySQL批量插入数据SQL\\nINSERT INTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN),(VALUE1,VALUE2...,VALUEN),(VALUE1,VALUE2...,VALUEN);\\n\\nOracle批量插入数据SQL\\nINSERT ALL\\nINTO TABLE_NAME(COLUMN1,COLUMN2…","guid":"https://juejin.cn/post/7481797018781515803","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-15T04:56:40.562Z","media":null,"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"MCP 很火,来看看我们直接给后台管理系统上一个 MCP?","url":"https://juejin.cn/post/7481491253499969575","content":"MCP
引用一些官方的介绍吧:
\\n\\n\\n\\n
Model Context Protocol
(MCP
) 是一个开放协议,它使LLM
应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
大白话就是一个数据通信的应用协议,约定了应用和大模型之间如何传递数据进行无缝连接。
\\n本文主要讲的是 MCP
的 SSE+HTTP
方式的使用。
先举个荔枝吧:)
\\nOllama
部署了一些乱七八糟的模型,用于提供给公司内部的朋友们使用。ERP
系统,管理着公司大量的数据信息。那我们能在这个背景下玩一些什么事情呢?
\\n先看截图:
\\n\\n\\n我们使用的客户端是 CherryStudio,左边是我们的
\\nERP
系统,右边是Ollama
跑的一个小 7B 的通义千问开源模型。
我们直接通过 CherryStudio 的 MCP
协议接入功能,直接和 ERP
系统进行通信,实现 ERP
系统的数据查询和操作。
如果我们把 CherryStudio 换成手机上的 Siri,身边的小爱同学呢?
\\n\\n\\nSiri 可以通过快捷指令来完成,小爱同学可以通过小爱技能来完成,当然,体验肯定没有直接内置 MCP 来得快体验好。
\\n
首先,我们先了解一下 MCP 的架构设计时序图:
\\nsequenceDiagram\\n participant User as User\\n participant CherryStudio as CherryStudio\\n participant Server as Server\\n participant Ollama as Ollama\\n User->>CherryStudio: 打开软件\\n CherryStudio--\x3e>Server: **SSE** 兄弟,我们聊会\\n Server--)CherryStudio: **SSE** 好,你有事的话 POST 这个地址(endpoint)\\n CherryStudio--\x3e>Server: **POST** 兄弟,自我介绍一下(initalize)\\n Server--)CherryStudio: **SSE** 好,这是我的基本信息(serverInfo)\\n CherryStudio--\x3e>Server: **POST** 兄弟,我收到了,我准备好了(initialized)\\n CherryStudio--\x3e>Server: POST: 兄弟,你有MCP的工具吗(tools/list)\\n Server--)CherryStudio: **SSE** 我提供了几个工具(tools)\\n User->>CherryStudio: 输入: 禁用张三的账号\\n CherryStudio->>Ollama: POST: 带工具调用 `禁用张三的账号`\\n Ollama-)CherryStudio: 意图识别: {工具:禁用账号,参数:张三}\\n CherryStudio--\x3e>Server: **POST** 请求发送 {工具:禁用账号,参数:张三}\\n Server--\x3e>CherryStudio: 执行工具并 **SSE** 推送结果\\n CherryStudio->>Ollama: 整理下收到的结果\\n Ollama-)CherryStudio: 返回处理后的结果\\n CherryStudio-)User: 显示给用户看\\n
\\n有了架构图了,那开发起来倒是没有什么难事了:
\\n当然,你可以使用官网提供的一些 SDK 来做,不过吧,很多问题,你可以先试试了来评论区讨论~。。。
\\n我们就不考虑上 SDK 啦,直接在项目里生撸!
\\n来吧,直接开始。
\\n{\\n \\"id\\": 0,\\n \\"jsonrpc\\": \\"2.0\\"\\n}\\n
\\n所有发送给 MCP 服务器的请求都是这个结构:
\\ninterface Request {\\n // 请求的ID\\n id: number\\n\\n // 请求的协议 固定2.0\\n jsonrpc: \\"2.0\\";\\n\\n // 请求的方法\\n method: string;\\n\\n // 请求的参数\\n params?: { ... };\\n}\\n
\\n例如 方法 initalize
的请求结构:
{\\n \\"id\\": 0,\\n \\"jsonrpc\\": \\"2.0\\",\\n \\"method\\": \\"initalize\\",\\n \\"params\\": {\\n // 客户端的一些能力\\n \\"capabilities\\": {},\\n \\"clientInfo\\": {\\n // 一些客户端信息,比如名称、版本等\\n }\\n }\\n}\\n
\\n又例如 函数调用的 请求结构
\\n{\\n \\"id\\": 1,\\n \\"jsonrpc\\": \\"2.0\\",\\n \\"method\\": \\"tools/call\\",\\n \\"params\\": {\\n \\"name\\": \\"disableUserByName\\",\\n \\"arguments\\": {\\n \\"name\\": \\"张三\\"\\n }\\n }\\n}\\n
\\n所有通过 SSE 推送给客户端的响应都是这个结构:
\\ninterface Response {\\n id: 0;\\n jsonrpc: \\"2.0\\";\\n result: {\\n // 一些数据信息\\n };\\n error: {\\n // 一些错误信息\\n };\\n}\\n
\\nSpringBoot 下开启一个 SSE 服务简单得不要不要的:
\\npublic final static ConcurrentHashMap<String, SseEmitter> EMITTERS = new ConcurrentHashMap<>();\\n\\n@GetMapping(value = \\"sse\\", produces = MediaType.TEXT_EVENT_STREAM_VALUE)\\npublic SseEmitter connect() throws IOException {\\n String uuid = UUID.randomUUID().toString();\\n SseEmitter emitter = new SseEmitter();\\n sseEmitter.send(SseEmitter.event()\\n .name(\\"endpoint\\")\\n .data(\\"/mcp/messages?sessionId=\\" + uuid)\\n .build()\\n );\\n EMITTERS.put(uuid, emitter);\\n\\n // 可以加点心跳\\n\\n emitter.onCompletion(() -> EMITTERS.remove(uuid));\\n emitter.onTimeout(() -> EMITTERS.remove(uuid));\\n return emitter;\\n return sseEmitter;\\n}\\n
\\n\\n\\n这里需要注意的是,MCP 要求连接上后必须发送一次消息,内容是 MCP 服务用于接受 POST 请求的 URL。
\\n
好,这个服务有了,客户端就可以通过这个服务来收我们要下发的消息了。
\\n接下来,我们来实现这个复杂一点的 POST 请求:
\\n@PostMapping(\\"messages\\")\\npublic Json messages(HttpServletRequest request, @RequestBody McpRequest mcpRequest) {\\n String uuid = request.getParameter(\\"sessionId\\");\\n if (Objects.isNull(uuid)) {\\n return Json.error(\\"sessionId is required\\");\\n }\\n String method = mcpRequest.getMethod();\\n\\n switch(method){\\n case \\"initalize\\":\\n // 这个请求是初始化请求,需要返回一些服务器信息给客户端\\n break;\\n case \\"tools/call\\":\\n // 这个请求是工具调用请求,需要返回执行结果给客户端\\n break;\\n case \\"tools/list\\":\\n // 这个请求是工具列表请求,需要返回一些工具列表给客户端\\n break;\\n default:\\n }\\n}\\n
\\n\\n\\n请注意,所有请求都不是 HTTP 直接响应,而是通过刚才的 SSE 通道推送回去。
\\n
初始化请求需要响应给客户端的是服务器的一些基本信息:
\\n{\\n id: id,\\n jsonrpc: \\"2.0\\",\\n result: {\\n // 一些服务能力\\n capabilities: {},\\n serverInfo: {\\n name: \\"服务器名称\\",\\n version: \\"1.0.0\\"\\n }\\n }\\n}\\n
\\n这时候,客户端已经可以显示服务器的基本信息了。
\\nSSE 服务器收到到请求后,需要响应给客户端的是工具列表:
\\n{\\n \\"id\\": 0,\\n \\"jsonrpc\\": \\"2.0\\",\\n \\"result\\": {\\n \\"tools\\": [\\n {\\n \\"name\\": \\"disableUserByName\\",\\n \\"description\\": \\"禁用一个用户的账号\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {\\n \\"nickname\\": {\\n \\"type\\": \\"string\\",\\n \\"description\\": \\"名称\\"\\n }\\n },\\n \\"required\\": [\\"nickname\\"]\\n }\\n }\\n ]\\n }\\n}\\n
\\nSSE 服务器需要执行工具时,会得到这个结构体:
\\n{\\n \\"id\\": 1,\\n \\"jsonrpc\\": \\"2.0\\",\\n \\"method\\": \\"tools/call\\",\\n \\"params\\": {\\n \\"name\\": \\"disableUserByName\\",\\n \\"arguments\\": {\\n \\"name\\": \\"张三\\"\\n }\\n }\\n}\\n
\\n你可以在执行一些代码后,返回下面的结构体:
\\n{\\n \\"id\\": 1,\\n \\"jsonrpc\\": \\"2.0\\",\\n \\"result\\": {\\n \\"content\\": [\\n {\\n \\"type\\": \\"text\\",\\n \\"text\\": \\"好,张三被我干掉了\\"\\n }\\n ]\\n }\\n}\\n
\\n到这里,几乎完成了整个流程。
\\n我们因为使用的 Java 和 SpringBoot, 所以我们使用了 @McpMethod
注解配合 Reflections
来实现自动注册工具。
@McpMethod(\\"modifyEmailByName\\")\\n@Description(\\"modify user new email by name\\")\\npublic String modifyEmailByName(\\n @Description(\\"the name of user, e.g. 凌小云\\")\\n String name,\\n @Description(\\"the new email of user, e.g. example@domain.com\\")\\n String email\\n) {\\n List<UserEntity> userList = filter(new UserEntity().setNickname(name));\\n DATA_NOT_FOUND.when(userList.isEmpty(), \\"没有叫 \\" + name + \\" 的用户\\");\\n userList.forEach(user -> {\\n updateToDatabase(get(user.getId()).setEmail(email));\\n });\\n return \\"已经将 \\" + userList.size() + \\" 个叫 \\" + name + \\" 的用户邮箱修改为 \\" + email;\\n}\\n
\\n只要标记了 @McpMethod
注解, MCP
服务器就会自动注册这个方法。
然后你就可以通过 CherryStudio 等工具来调用这个方法了。
\\n\\n\\n动动嘴的事情~
\\n
我们通过上述的方式完成了一个的 MCP 服务, 并且也可以为我们的一些其他系统进行扩展,用大模型来改造这些系统的使用方式,美滋滋。
\\n当然,这里还有很多问题需要我们解决,比如权限控制。
\\n完整的代码我们放在了我们的 SPMS_Server 项目以及 AirPower4J 基础库里了:
\\n我倒是很悲观,现在满脑子都是 小爱同学,把张三的辞职报告审核通过一下。
\\n等各种终端设备都支持 MCP 协议了,我们再来玩更多的事情吧。
\\n各位周末愉快,Bye.
","description":"什么是 MCP 引用一些官方的介绍吧:\\n\\nModel Context Protocol (MCP) 是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。\\n\\n大白话就是一个数据通信的应用协议,约定了应用和大模型之间如何传递数据进行无缝连接。\\n\\n本文主要讲的是 MCP 的 SSE+HTTP 方式的使用。\\n\\n先举个荔枝吧:)\\n\\n当下背景\\n服务器通过 Ollama 部署了一些乱七八糟…","guid":"https://juejin.cn/post/7481491253499969575","author":"Hamm","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-14T11:27:39.969Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d07c3847e4e540bcbcbb947206cdad9e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFtbQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742556459&x-signature=ehIzeE99V7wY%2FmgLwUbjRyrVJ08%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","LLM","MCP"],"attachments":null,"extra":null,"language":null},{"title":"ZooKeeper是多主多从的结构,还是一主多从的结构?","url":"https://juejin.cn/post/7481484509436395560","content":"文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n孙子兵法:
\\n\\n\\n资料链接: url81.ctfile.com/f/57345181-…
\\n访问密码:3899
\\n
Zookeeper 是 一主多从的结构。
\\n具体来说,Zookeeper 集群由一个 Leader(主节点) 和多个 Follower(从节点) 构成,所有节点共同组成一个分布式协调服务。
\\n以下是 Zookeeper 结构的详细说明:
\\nLeader(主节点)
\\nFollower(从节点)
\\nObserver(观察者节点,可选)
\\n写请求:
\\n读请求:
\\n一致性保障
\\n性能权衡
\\n选举机制
\\n简单高效:
\\n强一致性:
\\n容错性:
\\n写性能瓶颈:
\\n单点故障:
\\nZookeeper 是 一主多从 的架构:
\\n为什么你投了上百份简历,没有一家公司约你?为什么有人找你要了简历,但就是不约你面试呢?核心原因就在于你的敲门砖 —> 简历不行!!
\\n拿到一份令人满意的 Offer,需要两个东西:
\\n大明哥工作 10+ 年了,有 7 年的面试官经验了,2024 年帮助 40+ 位同学优化过简历(大明哥的简历优化和别人的全然不同! ),还是蛮清楚怎样的简历才算优秀的简历。
\\n什么是简历?可能有同学嗤之以鼻,难道作为一名合格的牛马会不知道什么是简历吗?简历不就是个人基本信息 、教育背景、工作经历、项目经历写到一个 word 文档里,然后转换为 PDF 的文档么?如果你是这么理解的话,说明同学你看得太浅了!
\\n什么是简历?大明哥认为简历是一封介绍信,一封体现你价值的介绍信;一块敲门砖,一块敲开你心仪公司大门的敲门砖;这是面试官对你的第一印象。你要通过你的简历告诉面试官如下几个信息:
\\n理解不了这简历的本质,你就写不好一份优质的简历。
\\n那简历的核心是什么?
\\n面试前绝大多数的同学都会把大量的时间用来复习和刷八股文,留给写简历的时间可能就只有那么一两个小时。甚至,有的同学就直接将以前的简历拿过来增加下工作经历和项目经历就行了,非常敷衍了事。
\\n可能有同学说,我以前就是这么干的,也能找到工作呀!同学,你也说是以前,今时不同往日了呀。现在是什么时候,以前是什么时候?我以前只要把 Boss 的状态改下,简历公开下,一天就可以约到 10 多个面试,现在你试试 ?不好好准备,没有好的简历,你投的天昏地暗都没人约你面试,当然,有些外包除外。
\\n一份完整的简历需要具备如下五部分内容:
\\n其中基本资料的顺序位于第一位,其余几个部分注意要扬长避短。什么意思呢?就是那个部分内容比较突出你的优势,你就先写哪部分。比如你是双一流本硕、海龟,那么就将教育背景前移。但是如果你跟大明哥一样,只是一个小二本仔,那么就挪到后面去吧。
\\n基本资料就是个人的基本信息,包括姓名、年龄、工作年限、联系方式(手机、邮箱)。其实有这些信息就够了,有些同学会写按照在网上搞来的模板来写,会写一些无关要紧的信息,例如籍贯、居住地等等。
\\n\\n\\n某些憨憨们,一定要仔细核对你们的联系方式啊!有一位同学给我的简历优化,我发现它的手机号码竟然只有 10 位。你要别人怎么联系你嘛?联系上了,面试官看到了也不好啊。不让你过吧,你面试表现确实还可以。让你过吧,你这仔细程度,不知道工作后会给我挖什么坑!!
\\n
还有,关于招聘。有些同学会把他的照片也贴上去,其实真没有必要,除非你是大美女。不过,妹子对于程序员来说确实是优势,以前我们老大还会特意招几个开发妹子来阴阳调和下。所以,长的好看还是比较有优势的,像大明哥就比较有自知之明,不放。哈哈哈~~~
\\n关于博客和 GitHub。如果你有经常更新的博客或者 GitHub,一定要写在基本资料里面。注意一定要是经常更新的。这个对于我们开发同学来说会是很大的亮点。如果你写的东西很对你未来老板的胃口,那你离进去就只差几步了。有些面试官看到你的博客地址,他比较感兴趣的话,就会让你直接进入面试环节。
\\n\\n\\n扬长避短。
\\n
什么意思?就是你的优势放在前面,劣势放在后面,最好到最后一页去。
\\n有些同学,工作履历不错,项目经历也很好,但学历一般般。如果放在以前环境好、需求大的时候,其实问题也不是很大。但是现在就不是那么行了。
\\n所以,如果你是学霸,就直接放在个人基本信息下面,突出学历的优势。如果你跟大明哥一样,小二本一个,直接挪到最后去吧,避免因为学历问题而挂掉。
\\n专业技能是比较重要的一个模块。如果面试官比较喜欢问八股文,则主要就看这块的内容了。你的技术栈是吹牛逼还是有真材实料,一问便知。
\\n在这一模块,会涉及到几个词:了解、熟悉、掌握、熟练、精通。每个人对这几个词的理解各不相同,大明哥是这么理解的:
\\n对于这 5 个名词,大部分同学是按照工作年限来写的,比如应届毕业生就主要写了解、熟悉,1~3年就写熟悉、掌握。3 ~ 5 年就通篇的熟练,比如下面这位工作三年的同学:
\\n这 5 个「熟练掌握」看起来还是挺难受的。
\\n还有的同学对「精通」二字比较敬畏,根本不敢写,觉得写了会被面试官问死去。其实大可不必,如果对某项技术确实非常熟悉,能够应对 90% 的情况,就大胆写,引导面试官来问你这项技术,这样不就突显你的优势了么?
\\n至于广度和深度。有些同学是巴不得把他知道的所有技术点都写到简历里面来,但是每个技术点又写得不够详细,都是些什么熟悉 xxx 技术,了解其核心原理,甚至有些同学还写了 VUE 、TS 前端框架,你一个 Java 开发写什么前端框架?当然,有些小公司确实是需要你掌握一些前端框架,但是,这个写到简历里面并不能体现你的优势啊。
\\n所以,大明哥认为,总体上深度 > 广度。所以,在专业技能这块,我们需要更加突出我们把握技术的深度,将广度放在项目经历那个栏目去。
\\n所以,对于专业技能,大明哥建议:减少样板词汇+过滤合并技术栈+字体加粗。
\\n从最近的工作开始往前写,标注公司名称、工作时间以及做了什么事、产生了什么价值。
\\n这部分内容切记流水账,最好是用一两段话来阐述你在这家公司做了什么有价值的事情,没有?那就吹吧!
\\n至于有些工作内容比较简单,也可以不用写。但是不要漏,最好是把你每家公司都写进来。有的同学,有些工作时间比较短,如果不怕背调的话,是可以进行适当地合并。工作经历这块也要扬长避短,如果你都是大厂经验,建议直接放在个人信息下面。
\\n这个栏目,对于我们程序员来说是最最重要的,没有之一。有些面试官有时候都不会去看你其他内容,直奔项目经历,我有时候就是这样。
\\n大明哥认为我们程序员的项目经历经历具有非常独特的价值,它在求职过程中发挥着非常重要的作用。比如你做过视频项目,那再找类似做视频的公司就很容易脱颖而出;比如你一直都在金融行业,那么找金融行业的工作就是你的优势了。
\\n完整的项目经历主要包括以下几个部分:
\\n我们都知道现在面试都是面试造火箭、工作拧螺丝。项目经历就体现了你有没有造火箭的本事,如何体现呢?这个就要我们在写项目经历时,一定要想明白并记录起来,可以适当地夸大。可以从这四个方面来挖掘你项目的亮点:
\\n写这部分内容一定不要老实,在原有的基础上适当夸大,怎么夸大呢?比如你现有系统的用户数只有 500万,我吹成 2000万,咋啦?面试官又不知道。500 万单表就可以解决,2000 万,我也能吹个分库分表出来。
\\n同时,你对这个系统未来的展望也可以写。这个系统一些功能或者架构比较烂,由于某些原因,系统无法重构、优化,但是我们可以在简历里面把你对这个项目的设计写出来啊。你写成什么样,这个系统就长成什么样,面试官又不知道你系统长啥样。
\\n有些同学比较担心,说会露馅。漏啥馅咯,只要我脸皮厚,能吹会道,这就是我的项目经历。但是,也别吹过火,比如你们公司就十来个研发人员,你吹你们系统的拆成了几十个微服务就有点儿过分了。
\\n同时,这部分内容要精不要多。大明哥建议写 2 ~ 3 个优质的项目经历就可以了,没有任何难度的 demo 级别或者低质量的项目,就不要写进来了。而且千万千万不要写成流水账,比如这样:
\\n或者这样:
\\n你说从这样职责里面能看出啥?除了能看出你是一个 crud boy 外,看不出来任何东西,而且工作中做的东西都是比较 low,没有难度的事情。要写就写这种:
\\n我们的简历一定要简单、清晰、突出重点,切忌花里胡哨。有些同学还是用彩色的简历,其实没有必要,浪费这个钱干啥?还有些同学在网上找了一些 7 ~ 8 年前、丑了吧唧的简历模板,导致整个简历看起来非常不舒服。
\\n大明哥推荐使用 Boss 上面的简历模板,cv.zhipin.com/job/。里面很多简历模板看起来都非常不错。
\\n所以,简历保持黑白灰 + 关键词加粗,足够!!!
\\n嘿,大家好!今天咱们聊个运维圈的经典笑话:一个实习生上生产环境,直接给所有文件设成 777
权限,结果 SSH 都连不上了。这事儿听着好笑,但真碰上了就头疼了。咱们就从这个糗事儿出发,聊聊实习生上生产环境该咋给自己设权限,尤其是部署一个 Spring Boot 的 Docker 服务,到底得用哪些命令,咋做才靠谱。咱从最简单的想法开始,一步步推到现代化的方案,顺便看看简单方法有啥坑,再想想咋优化到主流水平。
假设你是那个实习生,刚拿到一台生产服务器,想部署个 Spring Boot 应用,跑在 Docker 里。你一琢磨,权限不够咋办?简单,直接全开:
\\nchmod -R 777 /app\\n
\\n这命令把 /app
目录下所有文件都改成读写执行随便来。然后你兴冲冲地跑 Docker:
docker run -d -p 8080:8080 my-springboot-app\\n
\\n结果呢?服务倒是跑起来了,但问题大了——SSH 连不上,服务器直接“裸奔”。为啥?777
太猛了,连系统关键文件都被改了权限,SSH 服务依赖的配置可能都废了。这就是传说中的“777惨案”。
这最原始的法子看着省事,但毛病不少:
\\n-R
是递归改,连不该动的系统文件都中招,SSH 挂掉只是开始。这法子显然不行,得想想更靠谱的路子。实习生上生产环境,到底该给自己啥权限呢?
\\n咱们冷静下来,部署 Spring Boot 的 Docker 服务,到底需要啥?一般是:
\\n/app
)得能写。docker
命令的执行权限。那就别用 777
了,试试最小权限。比如你是用户 dev
,先建个目录:
mkdir /app\\nchown dev:dev /app\\nchmod 700 /app\\n
\\n700
是啥意思?用户 dev
有读写执行(4+2+1=7),其他人啥权限都没。这样 /app
归你管,安全多了。然后写个 Dockerfile,把 Spring Boot 应用打包:
FROM openjdk:17\\nCOPY target/myapp.jar /app/myapp.jar\\nWORKDIR /app\\nCMD [\\"java\\", \\"-jar\\", \\"myapp.jar\\"]\\n
\\n构建镜像:
\\ndocker build -t my-springboot-app .\\n
\\n跑容器:
\\ndocker run -d -p 8080:8080 -v /app:/app my-springboot-app\\n
\\n这里 -v
把宿主机的 /app
挂载到容器里,权限够用,服务也能跑。但你试了试,发现 Docker 报错:permission denied
。咋回事?原来 docker
命令需要 root 权限,你得加 sudo
:
sudo docker run -d -p 8080:8080 -v /app:/app my-springboot-app\\n
\\n这下好了吧?服务跑起来了,权限也没乱给。但还是有点别扭,手动加 sudo
太麻烦,而且 /app
里文件的权限还得再调调。
光这样还不够,生产环境不会让你随便用 root 跑东西。主流做法是啥呢?得用个专门的用户管服务,别直接拿自己的账号干活。咱们再改改:
\\nspringuser
:\\nsudo useradd -m springuser\\nsudo passwd springuser # 设置密码\\n
\\n/app
给这个用户:\\nsudo mkdir /app\\nsudo chown springuser:springuser /app\\nsudo chmod 750 /app # 用户读写执行,组读执行\\n
\\nspringuser
:\\nsu - springuser\\n
\\ndocker
组:\\nsudo usermod -aG docker springuser\\n
\\n退出再登录,springuser
就能直接跑 docker
命令了。然后构建和运行还是那套:
\\ndocker build -t my-springboot-app .\\ndocker run -d -p 8080:8080 -v /app:/app my-springboot-app\\n
\\n这时候,/app
里文件的权限得再确认下。Spring Boot 的 jar 文件只需要读执行就够,改一下:
chmod 550 /app/myapp.jar\\n
\\n550
是用户和组可读可执行(4+1),别人没权限。服务跑得稳,权限也没乱放。
这套手动操作已经比 777
强多了,但离现代运维还有距离。生产环境里,手动敲命令早淘汰了,主流方案是用 CI/CD 管道 和 容器编排。咋弄呢?
version: \'3\'\\nservices:\\n app:\\n image: my-springboot-app\\n ports:\\n - \\"8080:8080\\"\\n volumes:\\n - /app:/app\\n user: \\"springuser\\"\\n
\\n跑起来:\\ndocker-compose up -d\\n
\\nsecurityContext
跑非 root 用户:apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: springboot-app\\nspec:\\n replicas: 3\\n selector:\\n matchLabels:\\n app: springboot\\n template:\\n metadata:\\n labels:\\n app: springboot\\n spec:\\n securityContext:\\n runAsUser: 1000 # springuser 的 UID\\n containers:\\n - name: app\\n image: my-springboot-app\\n ports:\\n - containerPort: 8080\\n
\\n这些方案权限控制更细,部署也自动化,实习生再也不用手忙脚乱了。
\\n从最初的 777
到现在,问题暴露了一堆。基于这个案例,优化得往这些方向走:
777
,用 750
、550
这种精准权限。springuser
专门干活。docker
组,避免每次 sudo
。runAsUser
。这些都是现代运维的标配。数字上也得准,比如 750
是 7(读写执行)+ 5(读执行)+ 0,不能写成 751
,不然 others 多了一份执行权限。
通过修复历史遗留的Crash漏报问题(包括端侧SDK采集的兼容性优化及Crash平台的数据消费机制完善),得物Android端的Crash监控体系得到显著增强,使得历史Crash数据的完整捕获能力得到系统性改善,相应Crash指标也有所上升,经过架构以及各团队的共同努力下,崩溃率已从最高的万2降至目前的万1.1到万1.5,其中疑难问题占比约90%、因系统bug导致的Crash占比约40%,在本文中将简要介绍一些较典型的系统Crash的治理过程。
\\nAndroid11及以下版本在DNS解析过程中的有几率产生野指针问题导致的Native Crash,其中Android9占比最高。
\\n堆栈与上报趋势
\\nat libcore.io.Linux.android_getaddrinfo(Linux.java)\\nat libcore.io.BlockGuardOs.android_getaddrinfo(BlockGuardOs.java:172)\\nat java.net.InetAddress.parseNumericAddressNoThrow(InetAddress.java:1631)\\nat java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:96)\\nat java.net.InetAddress.getAllByName(InetAddress.java:1154)\\n\\n#00 pc 000000000003b938 /system/lib64/libc.so (android_detectaddrtype+1164)\\n#01 pc 000000000003b454 /system/lib64/libc.so (android_getaddrinfofornet+72)\\n#02 pc 000000000002b5f4 /system/lib64/libjavacore.so (_ZL25Linux_android_getaddrinfoP7_JNIEnvP8_jobjectP8_jstringS2_i+336)\\n
\\n崩溃入口方法InetAddress.getAllByName用于根据指定的主机名返回与之关联的所有 IP 地址,它会根据系统配置的名称服务进行解析,沿着调用链查看源码发现在parseNumericAddressNoThrow方法内部调用Libcore.os.android_getaddrinfo时中有try catch的容错逻辑,继续查看后续调用的c++的源码,在调用android_getaddrinfofornet函数返回值不为0时抛出GaiException异常。
\\nhttps://cs.android.com/android/platform/superproject/+/android-9.0.0_r49:libcore/ojluni/src/main/java/java/net/InetAddress.java\\n\\nstatic InetAddress parseNumericAddressNoThrow(String address) {\\n // Accept IPv6 addresses (only) in square brackets for compatibility.\\n if (address.startsWith(\\"[\\") && address.endsWith(\\"]\\") && address.indexOf(\':\') != -1) {\\n address = address.substring(1, address.length() - 1);\\n }\\n StructAddrinfo hints = new StructAddrinfo();\\n hints.ai_flags = AI_NUMERICHOST;\\n InetAddress[] addresses = null;\\n try {\\n addresses = Libcore.os.android_getaddrinfo(address, hints, NETID_UNSET);\\n } catch (GaiException ignored) {\\n }\\n return (addresses != null) ? addresses[0] : null;\\n }\\n
\\nhttps://cs.android.com/android/platform/superproject/+/master:libcore/luni/src/main/native/libcore_io_Linux.cpp?q=Linux_android_getaddrinfo&ss=android%2Fplatform%2Fsuperproject\\n\\nstatic jobjectArray Linux_android_getaddrinfo(JNIEnv* env, jobject, jstring javaNode,\\n jobject javaHints, jint netId) {\\n ......\\n int rc = android_getaddrinfofornet(node.c_str(), NULL, &hints, netId, 0, &addressList);\\n std::unique_ptr<addrinfo, addrinfo_deleter> addressListDeleter(addressList);\\n if (rc != 0) {\\n throwGaiException(env, \\"android_getaddrinfo\\", rc);\\n return NULL;\\n }\\n ......\\n return result;\\n}\\n
\\n解决思路是代理android_getaddrinfofornet函数,捕捉调用原函数过程中出现的段错误信号,接着吃掉这个信号并返回-1,使之转换为JAVA异常进而走进parseNumericAddressNoThrow方法的容错逻辑,和负责网络的同学提前做了沟通,确定此流程对业务没有影响后开始解决。
\\n首先使用inline-hook代理了android_getaddrinfofornet函数,接着使用字节封装好的native try catch工具做吃掉段错误信号并返回-1的,字节工具内部原理是在try块的开始使用sigsetjmp打个锚点并快照当前寄存器的值,然后设置信号量处理器并关联当前线程,在catch块中解绑线程与信号的关联并执行业务兜底代码,在捕捉到信号时通过siglongjmp函数长跳转到catch块中,感兴趣的同学可以用下面精简后的demo试试,以下代码保存为mem_err.c,执行gcc ./mem_err.c;./a.out
\\n#include <stdio.h>\\n#include <signal.h>\\n#include <setjmp.h>\\n\\nstruct sigaction old;\\nstatic sigjmp_buf buf;\\n\\nvoid SIGSEGV_handler(int sig, siginfo_t *info, void *ucontext) {\\n printf(\\"信号处理 sig: %d, code: %d\\\\n\\", sig, info->si_code);\\n siglongjmp(buf, -1);\\n}\\n\\nint main() {\\n if (!sigsetjmp(buf, 0)) {\\n struct sigaction sa;\\n\\n sa.sa_sigaction = SIGSEGV_handler;\\n sigaction(SIGSEGV, &sa, &old);\\n\\n printf(\\"try exec\\\\n\\");\\n //产生段错误\\n int *ptr = NULL;\\n *ptr = 1;\\n printf(\\"try-block end\\\\n\\");//走不到\\n } else {\\n printf(\\"catch exec\\\\n\\");\\n sigaction(SIGSEGV, &old, NULL);\\n }\\n printf(\\"main func end\\\\n\\");\\n return 0;\\n}\\n\\n//输出以下日志\\n//try exec\\n//信号处理 sig: 11, code: 2\\n//catch exec\\n//main func end\\n
\\ninline-hook库: github.com/bytedance/a…
\\n字节native try catch工具: github.com/bytedance/a…
\\n在Android 11系统库的音视频播放过程中,偶尔会出现因状态异常导致的SIGABRT崩溃。音视频团队反馈指出,这是Android 11的一个系统bug。随后,我们协助音视频团队通过hook解决了这一问题。
\\n堆栈与上报趋势
\\n#00 pc 0000000000089b1c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)\\n#01 pc 000000000055ed78 /apex/com.android.art/lib64/libart.so (_ZN3art7Runtime5AbortEPKc+2308)\\n#02 pc 0000000000013978 /system/lib64/libbase.so (_ZZN7android4base10SetAborterEONSt3__18functionIFvPKcEEEEN3$_38__invokeES4_+76)\\n#03 pc 0000000000006e30 /system/lib64/liblog.so (__android_log_assert+336)\\n#04 pc 0000000000122074 /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEERKNS_2spINS_8AMessageEEE+720)\\n#05 pc 00000000001215cc /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEi+244)\\n#06 pc 000000000011c308 /system/lib64/libstagefright.so (_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE+8752)\\n#07 pc 0000000000017814 /system/lib64/libstagefright_foundation.so (_ZN7android8AHandler14deliverMessageERKNS_2spINS_8AMessageEEE+84)\\n#08 pc 000000000001d9cc /system/lib64/libstagefright_foundation.so (_ZN7android8AMessage7deliverEv+188)\\n#09 pc 0000000000018b48 /system/lib64/libstagefright_foundation.so (_ZN7android7ALooper4loopEv+572)\\n#10 pc 0000000000015598 /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+460)\\n#11 pc 00000000000a1d6c /system/lib64/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+144)\\n#12 pc 0000000000014d94 /system/lib64/libutils.so (_ZN13thread_data_t10trampolineEPKS_+412)\\n#13 pc 00000000000eba94 /apex/com.android.runtime/lib64/bionic/libc.so (_ZL15__pthread_startPv+64)\\n#14 pc 000000000008bd80 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)\\n
\\n根据堆栈内容分析Android11的源码以及结合SIGABRT信号采集到的信息(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING),找到崩溃发生在onMessageReceived函数处理kWhatRelease类型消息的过程中,onMessageReceived函数连续收到两条消息,第一条是kWhatError:STOPPING,第二条是kWhatRelease:STOPPING此时因mReplyID已经被置为空,因此走到判空抛异常的逻辑。
\\n\\n\\n
\\n
\\n
\\n对比Android12的源码,在处理kWhatRelease事件且状态为STOPPING抛异常前,增加了对mReplyID不为空的判断来规避这个问题。
Android12的修复方式意味着上述三个条件结合下吃掉异常是符合预期的,接下来就是想办法通过hook Android11使逻辑对齐Android12。
\\n【初探】最先想到的办法是代理相关函数通过判断走到这个场景时提前return出去来规避,音视频的同学尝试后发现不可行,原因如下:
\\n【踩坑】接着尝试使用与修复DNS崩溃类似思路的保护方案,使用inline-hook代理onMessageReceived函数调用原函数时使用setjmp打锚点,然后使用plt hook代理_android_log_assert函数并在内部检测错误信息为特征字符串时通过longjmp跳转到onMessageReceived函数的锚点并作return操作,精简后的demo如下:
\\nPlt-hook 库: github.com/iqiyi/xHook
\\n#include <iostream>\\n#include <setjmp.h>\\n#include <csignal>\\n\\nstatic thread_local jmp_buf _buf;\\nvoid *origin_onMessageReceived = nullptr;\\nvoid *origin__android_log_assert = nullptr;\\n\\nvoid _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {\\n //模拟liblog.so的__android_log_assert函数\\n std::cout << \\"__android_log_assert start\\" << std::endl;\\n if (!strncmp(fmt, \\"postPendingRepliesAndDeferredMessages: mReplyID == null\\", 55)) {\\n longjmp(_buf, -1);\\n }\\n //模拟调用origin__android_log_assert,产生崩溃 \\n raise(SIGABRT);\\n}\\n\\nvoid onMessageReceived_proxy(void *thiz, void *msg) {\\n std::cout << \\"onMessageReceived_proxy start\\" << std::endl;\\n if (!setjmp(_buf)) {\\n //模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程\\n std::cout << \\"onMessageReceived_proxy 1\\" << std::endl;\\n _android_log_assert_proxy(nullptr, nullptr, \\"postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING\\");\\n std::cout << \\"onMessageReceived_proxy 2\\" << std::endl;//走不到\\n } else {\\n //保护后从此处返回\\n std::cout << \\"onMessageReceived_proxy 3\\" << std::endl;\\n }\\n std::cout << \\"onMessageReceived_proxy end\\" << std::endl;\\n}\\n\\nint main() {\\n std::cout << \\"main func start\\" << std::endl;\\n /**\\n inline-hook: shadowhook_hook_sym_name(\\"libstagefright.so\\",\\"_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE\\",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);\\n plhook: xh_core_register(\\"libstagefright.so\\", \\"__android_log_assert\\", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));\\n */\\n //模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数\\n onMessageReceived_proxy(nullptr, nullptr);\\n std::cout << \\"main func end\\" << std::endl;\\n return 0;\\n}\\n\\n/**\\n日志输出\\n main func start\\nonMessageReceived_proxy start\\nonMessageReceived_proxy 1\\n__android_log_assert start\\nonMessageReceived_proxy 3\\nonMessageReceived_proxy end\\nmain func end\\n*/\\n
\\n线下一阵操作猛如虎经测试保护逻辑符合预期,但是在灰度期间踩到栈溢出保护导致错误转移的坑,堆栈如下:
\\n#00 pc 000000000004e40c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)\\n#01 pc 0000000000062730 /apex/com.android.runtime/lib64/bionic/libc.so (__stack_chk_fail+20)\\n#02 pc 000000000000a768 /data/app/~~JaQm4SU8wxP7T2GaSWxYkQ==/com.shizhuang.duapp-N5RFIB8WurdccMgAVsBang==/lib/arm64/libduhook.so (_ZN25CrashMediaCodecProtection5proxyEPvS0_)\\n#03 pc 0000000001091c0c [anon:scudo:primary]\\n
\\n*关于栈溢出保护机制感兴趣的同学可以参考这篇文章bbs.kanxue.com/thread-2217…
\\n(CSPP 第3版 “3.10.3 内存越界引用和缓冲区溢出”章节讲的更详细)*
\\nlongjmp函数只是恢复寄存器的值后从锚点处再次返回,过程中也唯一可能会操作栈祯只有inline-hook,当时怀疑是与setjmp/longjmp机制不兼容,由于inline-hook内部逻辑大量使用汇编来实现排查起来比较困难,因此这个问题困扰比较久,网上的资料提到可以使用代理出错函数(__stack_chk_fail)或者编译so时增加参数不让编译器生成保护代码来绕过,这两种方式影响面都比较大所以未采用。有了前面的怀疑点想到使用c++的try catch机制来做跨函数域的跳转,大致的思路同上只是把setjmp替换为c++的try catch,把longjmp替换为throw exception,精简后的demo如下:
\\nc++异常机制介绍: baiy.cn/doc/cpp/ins…
\\n#include <iostream>\\n#include <csignal>\\n\\nvoid *origin_onMessageReceived = nullptr;\\nvoid *origin__android_log_assert = nullptr;\\n\\nclass MyCustomException : public std::exception {\\npublic:\\n explicit MyCustomException(const std::string& message)\\n : msg_(message) {}\\n\\n virtual const char* what() const noexcept override {\\n return msg_.c_str();\\n }\\n\\nprivate:\\n std::string msg_;\\n};\\n\\nvoid _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {\\n //模拟liblog.so的__android_log_assert函数\\n std::cout << \\"__android_log_assert start\\" << std::endl;\\n if (!strncmp(fmt, \\"postPendingRepliesAndDeferredMessages: mReplyID == null\\", 55)) {\\n throw MyCustomException(\\"postPendingRepliesAndDeferredMessages: mReplyID == null\\");\\n }\\n //模拟调用origin__android_log_assert,产生崩溃\\n raise(SIGABRT);\\n}\\n\\nvoid onMessageReceived_proxy(void *thiz, void *msg) {\\n std::cout << \\"onMessageReceived_proxy start\\" << std::endl;\\n try {\\n //模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程\\n std::cout << \\"onMessageReceived_proxy 1\\" << std::endl;\\n _android_log_assert_proxy(nullptr, nullptr, \\"postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING\\");\\n std::cout << \\"onMessageReceived_proxy 2\\" << std::endl;//走不到\\n } catch (const MyCustomException& e) {\\n //保护后从此处返回\\n std::cout << \\"onMessageReceived_proxy 3\\" << std::endl;\\n }\\n std::cout << \\"onMessageReceived_proxy end\\" << std::endl;\\n}\\n\\nint main() {\\n std::cout << \\"main func start\\" << std::endl;\\n /**\\n inline-hook: shadowhook_hook_sym_name(\\"libstagefright.so\\",\\"_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE\\",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);\\n plhook: xh_core_register(\\"libstagefright.so\\", \\"__android_log_assert\\", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));\\n */\\n //模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数\\n onMessageReceived_proxy(nullptr, nullptr);\\n std::cout << \\"main func end\\" << std::endl;\\n return 0;\\n}\\n\\n/**\\n日志输出\\n main func start\\nonMessageReceived_proxy start\\nonMessageReceived_proxy 1\\n__android_log_assert start\\nonMessageReceived_proxy 3\\nonMessageReceived_proxy end\\nmain func end\\n*/\\n
\\n灰度上线后发现有设备走到了_android_log_assert代理函数中的throw逻辑,但是未按预期走到catch块而是把错误又转移为\\" terminating with uncaught exception of type\\" ,有点搞心态啊。
\\n【柳暗花明】C++的异常处理机制在throw执行时,会开始在调用栈中向上查找匹配的catch块,检查每一个函数直到找到一个具有合适类型的catch块,上述的错误信息代表未找到匹配的catch块。从转移的堆栈中注意到没有onMessageReceived代理函数的堆栈,此时基于inline-hook的原理(修改原函数前面的汇编代码跳转到代理函数)又怀疑到它身上,再次排查代码时发现代理函数开头漏写了一个宏,在inline-hook中SHADOWHOOK_STACK_SCOPE就是来管理栈祯的,因此出现找不到catch块以及前面longjmp的问题就不奇怪了。加上这个宏以后柳暗花明,重新放量后保护逻辑按预期执行并且保护生效后视频播放正常。和音视频的小伙伴一努力下,经历了几个版本终于解决了这个系统bug,目前仅剩老版本App有零星的上报。
\\nAndroid 11 Socket close过程中在多线程场景下有几率产生野指针问题导致Native Crash,现象是多个线程同时close连接时,一个线程已销毁了bio的上下文,另外一个线程仍执行close并在此过程中尝试获取这个bio有多少未写出去的字节数时出现野指针导致的段错误。此问题从21年首次上报以来在得物的Crash列表中一直处于较前的位置。
\\n堆栈与上报趋势
\\nat com.android.org.conscrypt.NativeCrypto.SSL_pending_written_bytes_in_BIO(Native method)\\nat com.android.org.conscrypt.NativeSsl$BioWrapper.getPendingWrittenBytes(NativeSsl.java:660)\\nat com.android.org.conscrypt.ConscryptEngine.pendingOutboundEncryptedBytes(ConscryptEngine.java:566)\\nat com.android.org.conscrypt.ConscryptEngineSocket.drainOutgoingQueue(ConscryptEngineSocket.java:584)\\nat com.android.org.conscrypt.ConscryptEngineSocket.close(ConscryptEngineSocket.java:480)\\nat okhttp3.internal.Util.closeQuietly_aroundBody0(Util.java:1)\\nat okhttp3.internal.Util$AjcClosure1.run(Util.java:1)\\nat org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:3)\\nat com.shizhuang.duapp.common.aspect.ThirdSdkAspect.t(ThirdSdkAspect.java:1)\\nat okhttp3.internal.Util.closeQuietly(Util.java:3)\\nat okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.java:42)\\nat okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.java:1)\\nat okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.java:6)\\nat okhttp3.internal.connection.Transmitter.newExchange(Transmitter.java:5)\\nat okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:5)\\n\\n#00 pc 0000000000064060 /system/lib64/libcrypto.so (bio_ctrl+144)\\n#01 pc 00000000000615d8 /system/lib64/libcrypto.so (BIO_ctrl_pending+40)\\n#02 pc 00000000000387dc /apex/com.android.conscrypt/lib64/libjavacrypto.so (_ZL45NativeCrypto_SSL_pending_written_bytes_in_BIOP7_JNIEnvP7_jclassl+20)\\n
\\n从设备分布上看,出问题都全是Android 11且各个国内厂商的设备都有,怀疑是Android 11引入的bug,对比了Android 11 和 Android 12的源码,发现在Android12 崩溃堆栈中的相关类 com.android.org.conscrypt.NativeSsl$BioWrapper有四个方法增加了读写锁,此时怀疑是多线程问题,通过搜索Android源码的相关issue以及差异代码的MR描述信息,进一步确认此结论。通过源码进一步分析发现NativeSsl的所有加锁的方法,会分发到NativeCrypto.java中的native方法,最终调用到native_crypto.cc中的JNI函数,如果能hook到相关的native函数并在Native层实现与Android12相同的读写锁逻辑,这个问题就可以解决了。
\\ncs.android.com/android/pla…\\ncs.android.com/android/pla…\\ncs.android.com/android/pla…
\\n通过JNI hook代理Android12中增加锁的相关函数,当走到代理函数中时,先分发到JAVA层通过反射获取ReadWriteLock实例并上锁再通过跳板函数调用原来的JNI函数,此时就完成了对Android12 增量锁逻辑的复刻。经历了两个版本的灰度hook方案已稳定在线上运行,期间无因hook导致的网络不可用和其它崩溃问题,目前开关放全量的版本崩溃设备数已降为0。
\\n\\nJNI hook原理,以及详细修复过程: blog.dewu-inc.com/article/MTM…
随着Android15开放公测,焦点处理过程中发生的空指针问题逐步增多,并在1月份上升到Top。
\\n堆栈与上报趋势
\\njava.lang.NullPointerException: Attempt to invoke virtual method \'android.view.ViewGroup$LayoutParams android.view.View.getLayoutParams()\' on a null object reference\\nat android.view.ViewRootImpl.handleWindowFocusChanged(ViewRootImpl.java:5307)\\nat android.view.ViewRootImpl.-$$Nest$mhandleWindowFocusChanged(Unknown Source:0)\\nat android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:7715)\\nat android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:7611)\\nat android.os.Handler.dispatchMessage(Handler.java:107)\\nat android.os.Looper.loopOnce(Looper.java:249)\\nat android.os.Looper.loop(Looper.java:337)\\nat android.app.ActivityThread.main(ActivityThread.java:9568)\\nat java.lang.reflect.Method.invoke(Native Method)\\nat com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)\\nat com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)\\n
\\n通过分析ASOP的源码,崩溃的触发点是mView字段为空。
\\n\\n\\n源码中mView为空的情况有两种:
结合前置判断了mAdded为true才会走到崩溃点,在源码中寻找到只有先正常调用setView以后在调用dispatchDetachedFromWindow时才满足mAdded=true、mView=null的条件,从采集的logcat日志中可以证明这一点,此时基本可以定位根因是窗口销毁与焦点事件处理的时序问题。
\\n\\n
在问题初期,尝试通过 Hook 拦截 handleWindowFocusChanged 方法增加防御:当检测到 mView 为空时直接中断后续逻辑执行。本地验证阶段,通过在 Android 15 设备上高频触发商详页 Dialog 弹窗的焦点获取与关闭操作,未复现线上崩溃问题。考虑到 Hook 方案的侵入性风险 ,且无法本地测试,最终放弃此方案上线。
\\n通过崩溃日志分析发现,问题设备100% 集中在小米/红米机型,而该品牌在 Android 15 DAU中仅占 36% ,因此怀疑是MIUI对Android15某些定制功能有bug。经与小米技术团队数周的沟通与联合排查,最终小米在v2.0.28版本修复了此问题,需要用户升级ROM解决,目前>=2.0.28的MIUI设备无此问题的上报。
\\n通过上述问题的治理,系统bug类的崩溃显著减少,希望这些经验对大家有所帮助。
\\n文 / 亚鹏
\\n关注得物技术,每周更新技术干货
\\n要是觉得文章对你有帮助的话,欢迎评论转发点赞~
\\n未经得物技术许可严禁转载,否则依法追究法律责任。
","description":"一、前言 通过修复历史遗留的Crash漏报问题(包括端侧SDK采集的兼容性优化及Crash平台的数据消费机制完善),得物Android端的Crash监控体系得到显著增强,使得历史Crash数据的完整捕获能力得到系统性改善,相应Crash指标也有所上升,经过架构以及各团队的共同努力下,崩溃率已从最高的万2降至目前的万1.1到万1.5,其中疑难问题占比约90%、因系统bug导致的Crash占比约40%,在本文中将简要介绍一些较典型的系统Crash的治理过程。\\n\\n二、DNS解析崩溃\\n背景\\n\\nAndroid…","guid":"https://juejin.cn/post/7481104876887048243","author":"得物技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-13T09:07:05.582Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c00235dac98c44d1bebebe8748b3cb0a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=eqREFyYKBQdu%2B79tSQ3lksWm8CU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f9af3bd3ced14127abecbf91f5685b7f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=KcTzUeI%2BKBao%2F5tdqaX%2Fbm7RKR8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d450ab316f2d4c88a89b88d81bdb1e5f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=ElcA4%2BF2NsMCKT5qE3myLw0n0mU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c76e5ae9d6b0448281302ca5f0b660b5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=KcctaVD5tGVN7SeenHYQsBDoeIE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c93b671bee7e416db2a758bb284b1468~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=fAC0KuJ6GFS318WTI96WcEhd0iA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0c36c22126cb44c0bec435bc2f0a3ed7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=7lg7FLjGkoVB448MUfmiBvrQisA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/284802908b3446ddbde27d8b22833823~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=yFH2ZAvRtCyBW9p%2BvWuxU1IDO%2BM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e3c67586cb848af930672d11b7d589b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=JQqbKTF1nfeeyxBsll8zdl%2BN%2BXA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0fca0c0b45ec4ceb9ea103d51476b35e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=i4Ibm4Z7zuhT4bN6wrm%2FJTSEc40%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/797f2f68356a42e7b82cb107c9582374~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=i9xq%2BjbLgF%2FYn4MHJpUDfOxkjwI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c17e78388ee142b8b8ebbb2285fa52fb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=WtKHhGGwL5t2nk0kK3xn1jkA8oI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7306fe44aa164ecb89522b608a077596~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1742461625&x-signature=%2FkeElazT6gvGkFFCNRncTHV0nZI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Android"],"attachments":null,"extra":null,"language":null},{"title":"生产redis数据出问题了_shell脚本刷redis数据","url":"https://juejin.cn/post/7480847106029846579","content":"java程序员_shell脚本之路_redis刷数据
\\n需求描述,由于线上事故,现在急需将线上redis某个业务规则的key的数据刷下
\\n赋予shell脚本 执行权限
\\nchmod +x update_redis_values.sh
\\n#!/bin/bash\\n\\nprefixes=(\\"WSH\\" \\"WBJ\\")\\n\\nREDIS_HOST=\\"172.22.197.5\\"\\nREDIS_PORT=\\"6379\\"\\nREDIS_PASSWORD=\'V3gk5OAZ\'\\nREDIS_DB=3\\n\\nfor item in \\"${prefixes[@]}\\"; do\\n # 生成Redis的key\\n key=\\"wms:shsc:${item}:serial_number:PREFIX:${item}\\"\\n\\n # 从Redis中获取当前值\\n value=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD -n $REDIS_DB GET \\"$key\\")\\n\\n if [ \\"$value\\" != \\"\\" ]; then\\n # 提取数字部分(假设前缀部分是WXA,剩下的是数字)\\n item2=$(echo \\"$value\\" | sed \'s/[0-9]//g\' | sed \'s/\\"//g\')\\n number=$(echo \\"$value\\" | sed \'s/[^0-9]//g\')\\n\\n if [[ \\"$number\\" =~ ^[0-9]+$ ]]; then\\n # 获取数值的长度\\n length=${#number}\\n\\n # 判断数值是否符合预期长度(比如13位数字)\\n if [ \\"$length\\" -ge 9 ]; then\\n # 将倒数第九位数字替换为2\\n # 使用字符串操作,保留前面的部分,替换倒数第九位为2,后面的部分不变\\n new_number=\\"${number:0:$(($length-9))}2${number:$(($length-8))}\\"\\n\\n # 构造新的值,保留原始前缀和新的数字\\n new_value_str=\\"\\\\\\"${item2}${new_number}\\\\\\"\\"\\n\\n # 将新的值设置回Redis\\n redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD -n $REDIS_DB SET \\"$key\\" \\"$new_value_str\\"\\n\\n echo \\"Updated $key: $value -> $new_value_str\\"\\n else\\n echo \\"Value is too short to replace: $value\\"\\n fi\\n else\\n echo \\"Invalid value format for key $key: $value\\"\\n fi\\n else\\n echo \\"Key $key not found\\"\\n fi\\ndone\\n
\\n作用:单引号中的内容会被完全按字面意思处理。也就是说,Shell不会对单引号中的任何字符进行变量替换、命令替换或转义。特点:单引号中的特殊字符(如 $ 、 \\" 、 \\\\ 等)都被当做普通字符处理,不会触发任何特殊操作。
\\nvariable=\\"World\\"\\necho \'$variable\' \\n\\n\\n输出: $variable\\n
\\n\\n\\n因为使用了单引号, $variable 会被当作普通字符串输出,而不会进行变量替换。
\\n
作用:双引号中的内容允许变量替换和命令替换。也就是说,Shell会在双引号内解析 $
、`
(反引号,命令替换)等特殊字符。
特点:双引号保留了空格和换行等字符,因此通常用于包含空格或特殊字符的字符串,但仍然允许变量替换。
\\nvariable=\\"World\\"\\necho \\"$variable\\" \\n\\n# 输出: World\\n
\\n\\n\\n结果:因为使用了双引号,
\\n$variable
会被解析为其值 “World”,并输出。
在双引号中,可以使用反斜杠 () 来转义特殊字符,如 `\\"` 或 `$` 等。 单引号中的反斜杠(
)是普通字符,不会被用来转义后面的字符。
echo \\"It\\\\\'s a test\\" # 输出: It\'s a test\\necho \'It\\\\\'s a test\' # 输出: It\\\\\'s a test\\n\\n
\\n总结:
\\n单引号:内容严格按照字面量处理,不做任何替换或转义。
\\n双引号:允许变量替换、命令替换和转义,通常用于包含空格或
\\n在Shell脚本中,数组的定义和使用方式依赖于你使用的Shell类型(如 Bash)。以下是一些常见的数组定义方法和示例:
\\n# 使用括号定义数组\\narray=(1 2 3 4 5)\\n
\\necho ${array[0]} \\n# 输出: 1\\necho ${array[1]} \\n# 输出: 2\\n
\\n${#array[@]}
来获取数组的长度。echo ${#array[@]} # 输出: 5\\n
\\nfor
循环可以遍历数组中的所有元素:for element in \\"${array[@]}\\"; do\\n echo $element\\ndone\\n
\\n在使用索引时,数组索引是从0开始的。
\\n定义数组时如果没有使用括号,就会定义一个普通变量,而不是数组。
\\n示例:
\\nmy_var=\\"Hello\\"\\necho $my_var # 输出: Hello\\n
\\n( )
来定义,使用索引访问元素。declare -A
来定义,使用键(字符串)访问值。这些是Shell脚本中常见的数组定义和使用方法。
\\n在Shell脚本中,$
符号有多种用途,主要用于表示变量、命令替换和特殊参数。以下是 $
的一些常见用法:
$
用于引用变量的值。name=\\"Alice\\"\\necho \\"Hello, $name\\" # 输出: Hello, Alice\\n
\\n`
)或 $(...)
语法可以执行命令并将其输出作为字符串返回。current_date=$(date)\\necho \\"Today\'s date is: $current_date\\"\\n
\\n$
后面可以跟一些特殊字符,表示特定的含义。$0
:脚本的名称。
$1
, $2
, ...:脚本的参数,$1
是第一个参数,$2
是第二个参数,以此类推。
$#
:传递给脚本的参数个数。
$@
:所有参数的列表(以空格分隔)。
$*
:所有参数的列表(作为一个单一的字符串)。
$?
:上一个命令的退出状态(返回值)。
$$
:当前Shell进程的PID(进程ID)。
echo \\"Script name: $0\\"\\necho \\"First argument: $1\\"\\necho \\"Number of arguments: $#\\"\\n
\\n$
不用于变量名。my_var=\\"Hello\\" # 正确\\necho $my_var # 输出: Hello\\n
\\n$
符号而不进行变量替换,可以使用反斜杠 \\\\
进行转义。echo \\"This is a dollar sign: \\\\$\\" # 输出: This is a特殊参数: $\\n- 了解 `$` 的用法对于编写和调试Shell脚本非常重要。\\n\\n### 4 赋值操作符(`=`)两边不能有空格\\n在Shell脚本中,赋值操作符(`=`)两边不能有空格。这是因为Shell认为空格分隔了命令的不同部分,所以如果你在等号两边放置空格,Shell会将其解析为多个不同的命令或参数,从而导致语法错误。\\n\\n#### 正确的变量赋值:\\n```bash\\nmy_var=\\"Hello\\" # 正确,等号两边没有空格\\n错误的变量赋值:\\nmy_var = \\"Hello\\" # 错误,等号两边有空格\\n在第二个示例中,Shell会将 `my_var` 和 `= \\"Hello\\"` 分开解释,这会导致错误。\\n小结:赋值时,等号两边**不能**有空格。\\n
\\nredis-cli 客户端命令\\n\\nvalue=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD -n $REDIS_DB GET \\"$key\\") \\n\\n\\n\\n#!/bin/bash\\n# 定义 Redis 连接参数\\nREDIS_HOST=\\"127.0.0.1\\"\\nREDIS_PORT=\\"6379\\"\\n\\n# 定义一个函数来获取键的值\\nget_redis_value() {\\n redis-cli -h $REDIS_HOST -p $REDIS_PORT GET \\"$1\\"\\n}\\n\\n# 获取键的值\\nvalue1=$(get_redis_value 123)\\nvalue2=$(get_redis_value 456)\\n\\n# 输出结果\\necho \\"Value for key 123: $value1\\"\\necho \\"Value for key 456: $value2\\"\\n
\\n\\n这段代码是一个 Shell 脚本中的条件判断语句,具体来说,它是用于检查变量 `$value` 是否不为空。下面是对这段代码的详细解释:\\n\\n`if`: 这是一个条件语句的开始,后面跟着一个条件表达式。\\n \\n`[ \\"$value\\" != \\"\\" ]`: 这是条件表达式,使用方括号 `[]` 来进行测试。具体解释如下:\\n`\\"$value\\"`: 这是对变量 `value` 的引用。使用双引号包裹变量是一个好习惯,可以防止变量为空或包含空格时引发错误。\\n`!=`: 这是一个比较运算符,表示“不等于”。\\n`\\"\\"`: 这是一个空字符串,用于与变量 `$value` 进行比较。\\n \\n`then`: 如果条件成立(即 `$value` 不为空),将执行 `then` 后面的代码块。\\n\\n整体逻辑\\n这段代码的逻辑是:如果变量 `value` 的值不是空字符串,那么就执行 `then` 后面的代码。这通常用于确保在执行某些操作之前,变量中确实有值。\\n\\n\\n\\n\\n\\n以下是一个简单的示例,展示如何使用这段代码:\\n\\n```bash\\nvalue=\\"Hello, World!\\"\\n\\nif [ \\"$value\\" != \\"\\" ]; then\\n echo \\"Value is not empty: $value\\"\\nelse\\n echo \\"Value is empty\\"\\nfi\\n\\n
\\n在这个示例中,由于 value
变量被赋值为 \\"Hello, World!\\",所以输出将是 Value is not empty: Hello, World!
。
[]
进行条件测试时,通常需要注意空格的使用。例如,[ \\"$value\\" != \\"\\" ]
中的空格是必须的。[[ ]]
代替 [ ]
可以提供更强大的功能和\\n\\n最近逛了下Spring的官网,发现Spring AI已经支持DeepSeek!今天和大家聊聊如何在Spring Boot项目制使用DeepSeek,还是非常方便的!
\\n
Spring AI是Spring官方推出的开源框架,旨在为Java开发者提供方便的AI集成能力。其核心是通过抽象化和模块化设计,简化AI功能的接入步骤,同时保持与Spring生态的无缝兼容。
\\n以下是其主要特点与功能:
\\n\\n\\n由于DeepSeek官方服务有时候调用会繁忙,这里以阿里云百炼平台的DeepSeek服务为例。
\\n
立即体验
,然后点击右上角的钥匙
按钮就可以获取到对应的API KEY了,首次使用需要自行创建API KEY。\\n\\n这或许是一个对你有用的开源项目,mall项目是一套基于
\\nSpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!\\n
\\n- Boot项目:github.com/macrozheng/…
\\n- Cloud项目:github.com/macrozheng/…
\\n- 教程网站:www.macrozheng.com
\\n项目演示:\\n
\\n
\\n\\n接下来我们就来讲解下使用Spring AI来调用DeepSeek服务。
\\n
<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-openai-spring-boot-starter</artifactId>\\n <version>1.0.0-M6</version>\\n</dependency>\\n
\\nspring:\\n ai:\\n openai:\\n # 调用AI接口时表明身份的API KEY\\n api-key: <YOUR_API_KEY>\\n # 调用AI接口时的基础路径,配置的是阿里云百炼的基础路径\\n base-url: https://dashscope.aliyuncs.com/compatible-mode\\n chat:\\n options:\\n # 调用的模型,DeepSeek的话可以选择deepseek-r1或deepseek-v3\\n model: deepseek-r1\\n # 用来控制文本生成的随机性(创造力),值越小越严谨\\n temperature: 0.8\\n
\\n/**\\n * @auther macrozheng\\n * @description 对接DeepSeek后生成回答的Controller\\n * @date 2025/2/21\\n * @github https://github.com/macrozheng\\n */\\n@RestController\\npublic class DeepSeekController {\\n\\n private final OpenAiChatModel chatModel;\\n\\n @Autowired\\n public DeepSeekController(OpenAiChatModel chatModel) {\\n this.chatModel = chatModel;\\n }\\n\\n /**\\n * 根据消息直接输出回答\\n */\\n @GetMapping(\\"/ai/chat\\")\\n public Map chat(@RequestParam(value = \\"message\\") String message) {\\n return Map.of(\\"generation\\", this.chatModel.call(message));\\n }\\n\\n /**\\n * 根据消息采用流式输出,输出回答\\n */\\n @GetMapping(value = \\"/ai/chatFlux\\", produces = MediaType.TEXT_EVENT_STREAM_VALUE + \\"; charset=UTF-8\\")\\n public Flux<ChatResponse> chatFlux(@RequestParam(value = \\"message\\") String message) {\\n Prompt prompt = new Prompt(new UserMessage(message));\\n return this.chatModel.stream(prompt);\\n }\\n\\n}\\n
\\n\\n\\n然后启动项目,这里我们使用Postman来测试下接口
\\n
result.output.text
这个属性里面,我们可以通过接口中的text属性的拼接来获得完整的回答。今天给大家介绍了下Spring AI和DeepSeek的集成方法,还是比较简单的。对于回答的输出,由于直接输出响应比较慢,我们可以采用流式输出,通过不断拼接回答来响应比较好。
\\nDocker-Compose相比DockerFile运行一个容器而言,Compose可以一个定义和运行一组容器,用来简化多容器环境的管理和部署的场景。通过Docker-Compose,开发者可以使用YAML文件来配置一组应用服务,并且只需一个简单的命令即可创建和启动所有服务。这种方法特别适用于复杂应用程序和中间件的部署,也非常适用于开发和演示环境快速搭建。本文示例可参考Gitee仓库各种compose文件夹中README.md文档说明
\\n安装Docker-Compose之前,需要先安装Docker,参考之前 juejin.cn/post/726938… 文章中的Docker安装指南
\\n1.前往 github.com/docker/comp… 选择DockerCompose对应的版本下载linux二进制文件\\n2.把文件重命名为 docker-compose 放入/usr/local/bin/ 路径下并添加可执行权限
\\nchmod +x /usr/local/bin/docker-compose\\n
\\n3.查看docker-compose判断是否安装成功
\\ndocker-compose version\\n
\\n1.创建名为docker-compose-config.yml配置文件
\\nservices:\\n # Mysql\\n serverMysql:\\n restart: always\\n image: mysql:8.0.23\\n container_name: server-mysql\\n environment:\\n MYSQL_ROOT_HOST: \'%\'\\n MYSQL_ROOT_PASSWORD: mysql@Mypass.\\n MYSQL_DATABASE: server_nacos_db\\n MYSQL_USER: nacos\\n MYSQL_PASSWORD: nacos@Mypass.\\n TZ: Asia/Shanghai\\n volumes:\\n - /opt/dockerData/config/mysql/config/my.cnf:/etc/mysql/my.cnf\\n - /opt/dockerData/config/mysql/data:/var/lib/mysql\\n - /opt/dockerData/config/mysql/log:/logs\\n - /opt/dockerData/config/mysql/init:/docker-entrypoint-initdb.d\\n - /opt/dockerData/configPlus/mysql/mysql-files:/var/lib/mysql-files\\n logging:\\n driver: \'json-file\'\\n options:\\n max-size: \'5g\'\\n command: \\n --lower_case_table_names=1\\n --max_connections=1000\\n --character-set-server=utf8mb4\\n --collation-server=utf8mb4_general_ci\\n --default-authentication-plugin=mysql_native_password\\n # 解决docker容器中的mysql安全认证问题\\n security_opt:\\n - seccomp:unconfined\\n ports:\\n - 3306:3306\\n healthcheck:\\n test: [ \\"CMD\\", \\"mysqladmin\\" ,\\"ping\\", \\"-h\\", \\"localhost\\" ]\\n interval: 10s\\n timeout: 10s\\n retries: 10\\n networks:\\n - configNet\\n # Nacos\\n serverNacos:\\n restart: always\\n image: nacos/nacos-server:2.0.1\\n container_name: server-nacos\\n privileged: true\\n environment:\\n TZ: Asia/Shanghai\\n MODE: standalone\\n NACOS_APPLICATION_PORT: 8848\\n SPRING_DATASOURCE_PLATFORM: mysql\\n MYSQL_SERVICE_HOST: serverMysql\\n MYSQL_SERVICE_DB_NAME: server_nacos_db\\n MYSQL_SERVICE_PORT: 3306\\n MYSQL_SERVICE_USER: nacos\\n MYSQL_SERVICE_PASSWORD: nacos@Mypass.\\n volumes:\\n - /opt/dockerData/config/nacos/logs:/home/nacos/logs\\n logging:\\n driver: \'json-file\'\\n options:\\n max-size: \'2g\'\\n depends_on:\\n serverMysql:\\n condition: service_healthy\\n networks:\\n - configNet\\n ports:\\n - 8848:8848\\n - 9848:9848\\n - 9849:9849\\nnetworks:\\n configNet:\\n driver: bridge\\n enable_ipv6: false\\n
\\n3.同级目录执行命令即可,建议启动服务前先对文件夹进行授权
\\n# 增加权限\\nsudo chmod -R 777 /opt\\n# 在后台所有启动服务,指定编排文件\\ndocker-compose -f docker-compose-config.yml up -d\\n# 停止服务\\ndocker-compose -f docker-compose-config.yml stop\\n# 删除容器包括网络和卷\\ndocker-compose -f docker-compose-config.yml down -v\\n
\\n1.创建名为docker-compose-elk.yml配置文件
\\nservices:\\n # ES\\n elasticsearch:\\n restart: always\\n image: elasticsearch:7.4.2\\n container_name: server-elasticsearch\\n privileged: true\\n user: root\\n environment:\\n # 以单一节点模式启动\\n - discovery.type=single-node \\n # 设置使用JVM内存大小\\n - ES_JAVA_OPTS=-Xms4096m -Xmx4096m\\n # ES密码(注意修改ES密码的情况下,Logstash和kibana映射文件里面的ES密码也要修改)\\n - ELASTIC_PASSWORD=es@Mypass.\\n volumes:\\n # 挂在ES文件\\n - /opt/dockerData/elk/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml\\n - /opt/dockerData/elk/elasticsearch/plugins:/usr/share/elasticsearch/plugins\\n - /opt/dockerData/elk/elasticsearch/data:/usr/share/elasticsearch/data\\n networks:\\n - elkNet\\n ports:\\n - 9200:9200\\n - 9300:9300\\n # Logstash \\n logstash:\\n restart: always\\n image: logstash:7.4.2\\n container_name: server-logstash\\n privileged: true\\n user: root\\n volumes:\\n # 挂载logstash的配置文件\\n - /opt/dockerData/elk/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml\\n - /opt/dockerData/elk/logstash/config/pipeline.yml:/usr/share/logstash/config/pipeline.yml\\n - /opt/dockerData/elk/logstash/pipeline/logstash.conf:/usr/share/logstash/pipeline/logstash.conf\\n depends_on:\\n - elasticsearch\\n networks:\\n - elkNet\\n ports:\\n - 5044:5044\\n - 9600:9600\\n # Kibana\\n kibana:\\n restart: always\\n image: kibana:7.4.2\\n container_name: server-kibana\\n privileged: true\\n volumes:\\n # 挂载logstash的配置文件\\n - /opt/dockerData/elk/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml\\n depends_on:\\n - elasticsearch\\n networks:\\n - elkNet\\n ports:\\n - 5601:5601\\nnetworks:\\n elkNet:\\n driver: bridge\\n enable_ipv6: false\\n
\\n3.同级目录执行命令即可,建议启动服务前先对文件夹进行授权
\\n# 增加权限\\nsudo chmod -R 777 /opt\\n# 在后台所有启动服务,指定编排文件\\ndocker-compose -f docker-compose-elk.yml up -d\\n# 停止服务\\ndocker-compose -f docker-compose-elk.yml stop\\n# 删除容器包括网络和卷\\ndocker-compose -f docker-compose-elk.yml down -v\\n
\\n谢谢大家阅读,如果喜欢,请收藏点赞,多给些star,文章不足之处,也请给出宝贵意见.
","description":"一.引言 Docker-Compose相比DockerFile运行一个容器而言,Compose可以一个定义和运行一组容器,用来简化多容器环境的管理和部署的场景。通过Docker-Compose,开发者可以使用YAML文件来配置一组应用服务,并且只需一个简单的命令即可创建和启动所有服务。这种方法特别适用于复杂应用程序和中间件的部署,也非常适用于开发和演示环境快速搭建。本文示例可参考Gitee仓库各种compose文件夹中README.md文档说明\\n\\n二.安装Docker-Compose\\n\\n安装Docker-Compose之前,需要先安装Docker,参考之前…","guid":"https://juejin.cn/post/7480522174411096076","author":"Sans_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-12T08:01:44.653Z","media":null,"categories":["后端","Docker","容器"],"attachments":null,"extra":null,"language":null},{"title":"mybatis+springboot+MySQL批量插入 1w 条数据——探讨","url":"https://juejin.cn/post/7480529891491020827","content":"传统的单条 INSERT 语句逐行插入方式,在处理 1 万条数据时往往需要数秒,这不仅会导致事务锁竞争加剧,更可能引发连接超时等系统性风险
\\n那么如何优化这种批量插入的场景呢?让我们一起看看吧!
\\n在进行批量插入时,大事务(即将多条 INSERT 语句放在一个事务中)比独立事务(即每一条 INSERT 语句都单独使用一个事务)通常性能更好,原因是:
\\n事务提交开销
\\nCOMMIT
操作,这会触发 MySQL 的日志持久化(如 redo log 的 fsync 操作),导致磁盘 I/O 开销。同时,多次发送开启事务和提交事务的操作,带来了额外的网络开销COMMIT
,减少了日志刷盘的次数,从而显著降低 I/O 等待时间锁竞争与锁释放
\\n日志写入优化
\\n将多条 INSERT
语句合并成一条 INSERT
语句(例如 INSERT INTO table (col1, col2) VALUES (val1, val2), (val3, val4), ...
)也能提高性能,原因包括:
\\n\\n实现 INSERT 合并
\\n
rewriteBatchedStatements=true
如 jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
。有 JDBC 帮我们完成 SQL 的合并InnoDB 为保证自增 ID 的全局唯一性,在分配自增值时会持有自增锁(AUTO-INC Lock) 。在高并发情况下,多个插入操作会频繁争抢数据库的自增 ID,这可能导致锁的竞争和性能瓶颈
\\n使用预生成 ID 会有更好的性能表现,比如预先生成雪花 ID。避免在数据库层面加锁解锁影响性能
\\napplicatoin.yml 配置(关键在于rewriteBatchedStatements=true
):
spring:\\n datasource:\\n url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false\\n username: root\\n password: root\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n
\\n为了方便测试,使用数据库的自增 ID,生成数据库表 user:
\\nCREATE TABLE `user` (\\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\\n `name` varchar(100) DEFAULT NULL,\\n `age` int(11) DEFAULT NULL,\\n `email` varchar(100) DEFAULT NULL,\\n PRIMARY KEY (`id`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\\n
\\njava User 实体类:
\\n@Data\\n@TableName(\\"user\\")\\npublic class User {\\n private Long id;\\n private String name;\\n private Integer age;\\n private String email;\\n}\\n
\\n生成 1w 条测试数据:
\\nprivate List<User> prepareTestData(int count) {\\n List<User> users = new ArrayList<>(count);\\n for (int i = 0; i < count; i++) {\\n User user = new User();\\n user.setId(null); // 自增ID\\n user.setName(\\"test\\" + i);\\n user.setAge(20 + i % 50);\\n user.setEmail(\\"test\\" + i + \\"@test.com\\");\\n users.add(user);\\n }\\n return users;\\n}\\n
\\n每次测试后,清空数据,避免对下次测试的影响:
\\nTRUNCATE TABLE user;\\nALTER TABLE user AUTO_INCREMENT = 1;\\n
\\n使用 for 循环,逐个 INSERT。每个 INSERT 都会隐式独立地开启并提交事务,一条 INSERT 语句都单独使用一个事务。如图:
\\n@Test\\npublic void testSingleTransactionInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n for (User user : users) {\\n userMapper.insert(user);\\n }\\n\\n stopWatch.stop();\\n System.out.println(\\"独立事务循环插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: 独立事务循环插入耗时: 12485ms
\\n\\n\\n特点
\\n
显式使用@Transactional
开启事务,让执行的 SQL 都在同一个大事务中,这样只需要开启和提交一次事务,如图:
@Test\\n@Transactional // 开始事务\\npublic void testBigTransactionInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n for (User user : users) {\\n userMapper.insert(user);\\n }\\n\\n stopWatch.stop();\\n System.out.println(\\"大事务循环插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: 大事务循环插入耗时: 9565ms
\\n\\n\\n特点
\\n
通过 MyBatis foreach 标签,将多条 INSERT 语句合并为一条 INSERT 语句,如图:
\\n@Mapper\\npublic interface UserMapper extends BaseMapper<User> {\\n // 方便测试,直接将 SQL 写到注解\\n @Insert(\\"<script>\\" +\\n \\"INSERT INTO user (name, age, email) VALUES \\" +\\n \\"<foreach collection=\'users\' item=\'user\' separator=\',\'>\\" +\\n \\"(#{user.name}, #{user.age}, #{user.email})\\" +\\n \\"</foreach>\\" +\\n \\"</script>\\")\\n void batchInsert(@Param(\\"users\\") List<User> users);\\n}\\n\\n@Test\\npublic void testMybatisForeachInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n userMapper.batchInsert(users);\\n\\n stopWatch.stop();\\n System.out.println(\\"MyBatis foreach批量插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: MyBatis foreach批量插入耗时: 891ms
\\n\\n\\n特点
\\n
rewriteBatchedStatements=true
和 JDBC 批量插入机制需要开启 rewriteBatchedStatements=true
JDBC BatchInsert 的 SQL 和 INSERT 单条数据的语法一样,JDBC 会帮我们像上面 MyBatis foreach 一样,将多个 INSERT 聚合为一条 INSERT 语句
\\n@Test\\npublic void testJdbcBatchInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n jdbcTemplate.batchUpdate(\\"INSERT INTO user (name, age, email) VALUES (?, ?, ?)\\",\\n new BatchPreparedStatementSetter() {\\n @Override\\n public void setValues(PreparedStatement ps, int i) throws SQLException {\\n User user = users.get(i);\\n ps.setString(1, user.getName());\\n ps.setInt(2, user.getAge());\\n ps.setString(3, user.getEmail());\\n }\\n\\n @Override\\n public int getBatchSize() {\\n return users.size();\\n }\\n });\\n\\n stopWatch.stop();\\n System.out.println(\\"JDBC batch插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: JDBC batch插入耗时: 587ms
\\n\\n\\n特点
\\n
rewriteBatchedStatements=true
。由 JDBC 帮我们进行批处理,不用我们手动合并 INSERTmax_allowed_packet
参数限制,需避免超出阈值需要开启 rewriteBatchedStatements=true
获取 MyBatis 的 ExecutorType.BATCH 的 SqlSession,执行批量插入,底层依赖 JDBC BatchInsert,将多条 SQL 合并成一条
\\n@Test\\npublic void testMybatisBatchInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {\\n UserMapper mapper = sqlSession.getMapper(UserMapper.class);\\n for (User user : users) {\\n mapper.insert(user);\\n }\\n sqlSession.commit();\\n }\\n\\n stopWatch.stop();\\n System.out.println(\\"MyBatis SqlSession批量插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: MyBatis SqlSession批量插入耗时: 825
\\n\\n\\n特点
\\n
rewriteBatchedStatements=true
需要开启 rewriteBatchedStatements=true
使用 MyBatis-Plus saveBatch 将多个 SQL 合并成一条
\\n@Test\\npublic void testMybatisPlusBatchInsert() {\\n List<User> users = prepareTestData(10000);\\n StopWatch stopWatch = new StopWatch();\\n stopWatch.start();\\n\\n userService.saveBatch(users, 1000);\\n\\n stopWatch.stop();\\n System.out.println(\\"MyBatis-Plus批量插入耗时: \\" + stopWatch.getTotalTimeMillis() + \\"ms\\");\\n}\\n
\\n测试结果: MyBatis-Plus批量插入耗时: 860ms
\\n\\n\\n特点
\\n
rewriteBatchedStatements=true
方法 | 耗时(ms) |
---|---|
独立事务循环插入 | 12485 |
大事务循环插入 | 9565 |
MyBatis foreach | 891 |
JDBC BatchInsert | 587 |
MyBatis SqlSession 批量插入 | 825 |
MyBatis-Plus saveBatch | 860 |
性能排名:
\\n\\n\\n如何选择?
\\n
都需要开启rewriteBatchedStatements=true
\\n\\n测试的局限性
\\n
问题的核心在于:如何“攒一波”数据来实现批量插入?
\\n推荐批量插入与 MQ 配合使用。将需要插入的数据的消息发送给 MQ,生产者需要保证 MQ 的消息发送和本地事务的原子性。对于消费者,一次性拉取多个消息进行批量插入。当消息消费失败时,可以让 MQ 重新投递消息并重新消费
\\n不过 MQ 的引入和积攒数据,带来的数据插入的延迟是不可避免的,同时只能保证最终一致性而不是强一致
\\n只需要开启rewriteBatchedStatements=true
并使用 MyBatis-Plus saveBatch,就可以大幅提高批量插入的性能。如果可以离线导入数据,使用LOAD DATA
也是一种选择
不过一致性与性能难以兼得,一旦某个 INSERT 执行失败,整个事务的插入操作都要回滚。如果是在不同分片的事务,分片与分片之间的一致性也无法保证。同时,数据的插入可能会延后一段时间。所以不建议强一致的场景使用批量插入
\\n距离上一次写正儿八经的技术文有个半年多了,抱歉抱歉ಥ_ಥ,确实有点怠惰了。工作强度吧,真的有点大,心力耗费有点严重,同时博主的相亲之路也不是很顺畅,北京的相亲市场简直就是神仙赶集(┬┬﹏┬┬),再加上工作和生活上事儿赶事儿,就慢慢躺了。不过心里确实还记着要写技术博客,也没耽搁,目前存稿和思路后面会出两篇,一篇RPC,一篇缓存相关。
\\n本文是RPC的经典一本通文章,因为RPC是一个偏概念的东西,所以会搭配一个DEMO来讲解,同时相关的知识会尽可能的丰富一下,保持我一贯的文章水准。文章从RPC的定义和特征讲起,介绍核心流程和关键组件,接着会对一些名词做一下解释,同时回答一些问题。最后会用一个DEMO来介绍如何写一个RPC框架,当然也会拓展序列化、压缩、负载均衡和动态代理的知识,汇总网上的资料横向对比各种框架的性能和选型思路。本文的目的就是去帮助大家学习和认识平时最熟悉的基础框架-RPC组件的原理,同时拓展的知识也能帮助我们加深对其他中间件的理解。
\\nRPC(Remote Procedure Call,远程过程调用)是远程过程调用的缩写,是一种计算机通信协议。它使得程序可以像调用本地函数一样,请求另一个进程或者计算机上的服务,而不需要了解底层网络技术。RPC可以简化分布式系统的开发,提高系统的可维护性和可扩展性。RPC协议的实现包括一个客户端和一个服务端,它们可以运行在不同的机器上。
\\n核心目标: 简化分布式系统中跨进程/跨机器的交互,让你调用远程方法像调用本地方法一样简单。
\\n核心特征:
\\n现代的RPC框架,部分还会提供服务治理能力(服务注册与发现、负载均衡、容错机制等功能)、跨语言支持(通过接口定义语言(IDL)生成多语言客户端代码,实现跨平台调用)、多样化通信模式(支持同步、异步调用,以及流式通信。例如,gRPC支持双向流式数据传输,适用于实时交互场景)
\\n举个例子: 用交流来说,远程调用就相当于打电话沟通或者微信聊天,本地调用就是面对面说话。
\\n这是一个标准的RPC的原理网图,如果我们摘掉客户端 Stub和服务端 Skeleton,就成了用socket实现的一个普通通信代码,同时也是最简单的RPC框架。
\\n---------------------------客户端---------------------------\\n// 1. 创建Socket连接服务端(假设服务端运行在本机8080端口)\\nSocket socket = new Socket(\\"localhost\\", 8080);\\n// 2. 获取输出流并包装为对象流(发送请求)\\nObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());\\n// 3. 获取输入流并包装为对象流(接收响应)\\nObjectInputStream ois = new ObjectInputStream(socket.getInputStream())\\n// 4. 发送方法名和参数(模拟调用add(1, 2))\\noos.writeUTF(\\"add\\"); // 写入方法名(UTF编码)\\noos.writeInt(1); // 写入第一个参数\\noos.writeInt(2); // 写入第二个参数\\noos.flush(); // 确保数据发送\\n\\n// 5. 读取服务端返回的结果\\nint result = ois.readInt();\\nSystem.out.println(\\"调用结果: \\" + result);\\n\\n---------------------------服务端---------------------------\\nServerSocket serverSocket = new ServerSocket(8080);\\nSocket socket = serverSocket.accept();\\nObjectInputStream ois = new ObjectInputStream(socket.getInputStream());\\nString methodName = ois.readUTF(); // 读取方法名\\nint a = ois.readInt(); // 读取参数\\nint b = ois.readInt();\\nint result = a + b; // 实际计算\\nObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());\\noos.writeInt(result); // 返回结果\\n
\\n那么这个最基础的框架有什么问题呢?
\\n为了解决这些问题,各种RPC框架出现了,比如Dubbo、Grpc等,他们的底层基础是Socket,因为要依赖Socket进行网络传输,所以是Socket的高度封装。在此之上通过动态代理、序列化、服务治理等功能,简化开发并提升系统能力,并且引入了一些高性能网络库(如Netty)、高效序列化协议(如Protobuf)和各种服务治理机制(如Zookeeper)
\\n先来对比一下两者的区别,但是这两者一般不能对比,因为RPC是一种设计理念,而Http是一种通信协议实现
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n场景 | 典型HTTP接口(如RESTful API) | RPC框架(如gRPC) |
---|---|---|
设计目标 | 资源操作(GET/POST/PUT/DELETE) | 方法调用(远程函数执行) |
数据格式 | 文本(JSON/XML) | 二进制(Protobuf/Thrift) |
调用方式 | 显式构造URL和参数 | 通过接口代理直接调用方法 |
协议灵活性 | 需遵循HTTP语义 | 可自定义协议(如Dubbo的私有协议) |
性能 | 较低(头部冗余,文本解析慢) | 较高(二进制压缩,协议精简) |
先来说一下Http和Socket的关系,有助于理解和RPC的关系
\\nFeign这个框架,基本是HttpClient->RestTemplate->Feign(OpenFeign)这样的演化过程,实际上就是一个声明式、模板化的Http客户端,主要用于简化HTTP API的调用。
\\nFeignFeign通过动态代理和接口封装隐藏了HTTP调用细节,符合RPC“透明调用”的核心特征,从广义上可以认为是RPC的一种实现,狭义上不属于传统RPC框架:
\\n客户端Stub和服务端Skeleton的概念最早源于1984年Andrew Birrell和Bruce Nelson的论文《Implementing Remote Procedure Calls》。
\\n该论文提出将RPC抽象为五个组件(User、User-stub、RPCRuntime、Server-stub、Server),其中Stub和Skeleton的设计目标是屏蔽网络通信复杂性,是实现透明调用的基石。
\\n简单来说,Stub 帮客户端假装自己在本地调用,Skeleton 帮服务端偷偷执行真实方法,深藏功与名~
\\n偷个小懒,从Github上扒了一个RPC教学类star排名前几的项目,github.com/Snailclimb/…,接下来会根据该项目的代码进行讲解。项目启动很简单,下载一个3.5.10或者最新的zk,启动后,先启动example-server,再启动example-client即可。
\\n大致讲一下代码结构,对应上前面讲的关键组件
\\n需要注解+bean注册类
\\n一般来说至少需要两种类,一种是服务端在类上的服务方定义@RpcService,一种是客户端在类变量上定义的资源定义@RpcReference。这里多了一个扫描器@RpcScan,类似于@ComponentScan,有用处,但是不要也可以,可以直接在@RpcService中加上@Component之类的的bean资源注解,交给Spring,事后再捞出来已注册的bean特殊处理即可。
\\n使用BeanPostProcessor这个进行Bean生命周期拓展可以,但是更推荐使用LifeCycle这个Bean生命周期全部走完再进行操作的类,避免BeanPostProcessor后有什么额外的操作影响当前拓展。
\\n需要大致两种接口和一个本地缓存操作类
\\n核心模块,需要定义出入参和信息传递三个类,定义两个接口,一个负责发消息,一个负责接收消息
\\n收发消息这个需要定义发送端和服务端,这时候技术选型就是真正的传输了
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n传输方式 | 性能 | 复杂度 | 适用场景 | 代表框架 |
---|---|---|---|---|
原生Socket | 高 | 高 | 需要底层控制的场景 | Java RMI(究极老) |
Netty(主流) | 高 | 中 | 高并发生产环境 | Dubbo、gRPC |
HTTP/1.x | 中 | 低 | 兼容性优先的跨平台调用 | Spring Cloud Feign |
HTTP/2 | 高 | 中 | 现代高性能RPC | gRPC(默认) |
自定义协议 | ? | 高 | 内部高性能服务调用,定制化处理 | Dubbo协议 |
这里简单讲下代码里的Netty模块吧,毕竟是主流
\\n客户端:
\\n服务端:
\\n额外插播一个我觉得很有意思的网站,云原生计算基金会(cncf)的技术全景图landscape.cncf.io/,里面都是一些很知名的分布式项目,涵盖容器、CI/CD、监控、服务治理、网络、分布式数据库、流处理计算、消息队列等。大家如果遇到什么问题,想要拓展思路或者单纯为了技术选型,都可以来瞅瞅,激发灵感。当然最重要的是看看目前最潮流的技术是啥样的,发展到什么程度了。
\\n通信协议是 RPC 通信双方约定的 数据传输规则,定义了如何组织、传输和解析数据包。其职责如下:
\\n协议和序列化的区别,用寄快递举例来讲,协议定义收发快递的规范(哪里发快递,怎么发以及收快递,快递的单子信息)、走陆运还是空运海运(通信流程)。序列化就是单纯决定用盒子装还是用袋子装这个东西。
\\n一次客户端发起的 RPC 调用的流程是,协议规范传输行为 → 序列化处理数据内容 → 协议封装并发送数据。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nDubbo2/3 | gRPC | Thrift | |
---|---|---|---|
传输协议 | TCP 长连接(Dubbo 协议),HTTP/2(Triple 协议,Dubbo 3.x 引入) | 基于 HTTP/2 | TCP、HTTP 等 |
支持序列化方式 | Hessian 2、Protobuf、JSON 等 | Protobuf、JSON 等 | TBinary(Thrift自研)、JSON、CompactProtocol 等 |
默认序列化方式 | Hessian 2(Dubbo 协议);Protobuf(Triple 协议) | Protobuf | TBinary |
跨语言支持能力 | 支持 Java 为主,对其他语言有一定支持,Triple 协议增强了跨语言能力 | 支持多语言,如 Java、C++、Python、Go 等 | 支持多语言,如 Java、C++、Python、PHP 等 |
社区活跃度 | Apache Dubbo 社区活跃度较高,有大量企业应用和贡献 | 社区活跃,有广泛的用户和贡献者 | 社区有一定活跃度,在特定领域应用较多 |
核心优势 | 高性能,长连接减少开销;可扩展性,协议头部预留扩展字段;可靠性,支持心跳检测、超时重试 | 基于 HTTP/2 和 Protobuf 的高性能;多语言跨平台二进制兼容能力 | 高效二进制协议,节省带宽和提高性能;多语言支持 |
服务治理能力 | Dubbo 框架具备完善的服务治理功能,如服务注册与发现、负载均衡、容错机制等 | 通过相关组件和工具可实现服务治理,如服务发现、负载均衡等 | 可通过扩展实现服务治理功能 |
流式通信 | Triple 协议支持流式调用,Dubbo 协议默认不支持 | 支持客户端流、服务器流和双向流 | 支持流式传输 |
通常我们需要提供一个Serializer接口用以扩展序列化方式,序列化byte[] serialize(Object obj)和反序列化 T deserialize(byte[] bytes, Class clazz);。以下是常见的序列化框架,简单搜罗网上数据做了个横向对比表格,一般Java环境推荐个人使用Kryo和JSON来适配不同环境。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n框架 | 设计定位 | 序列化速度 | 数据体积 | 跨语言支持 | 兼容性 | 社区活跃度 | 易用性 | 典型适用场景 |
---|---|---|---|---|---|---|---|---|
Kryo | 纯Java高性能序列化 | 极快 | 最小 | 否(仅Java) | 低(字段增减需处理) | 高 | 需管理线程安全/类注册 | 实时通信、游戏、内存缓存 |
Protostuff | 基于Protobuf的无预编译Java优化版 | 快 | 小 | 有限(主Java) | 中(末尾追加兼容) | 低 | 无需预编译,但需处理无参构造 | 微服务通信、持久化存储 |
Protobuf | 跨语言结构化数据协议 | 较快 | 小 | 是 | 高(版本兼容) | 高 | 修改Schema需重新生成代码,需预编译.proto文件 | 多语言RPC、API协议、长期数据存储 |
Hessian/Hessian2 | 跨语言动态兼容协议 | 中等/较快(接近Protobuf) | 大/中等 | 是 | 高(自动兼容) | 低 | 开箱即用,无复杂配置 | 遗留系统集成、简单跨语言调用 |
JSON(Jackson/fastjson/gson) | 可读性优先的文本序列化(其他为二进制,均不可读!) | 较慢 | 最大(文本) | 是 | 高(无结构约束) | 高 | 直接操作POJO,开发便捷 | 前后端交互、调试日志 |
Java原生 | 内置序列化 | 极慢 | 极大 | 否(仅Java) | 低(完全依赖类结构) | 低 | 简单但效率低下 | 临时测试、简单场景 |
MessagePack | 高效二进制跨语言序列化 | 快 | 小(紧凑) | 是 | 中(字段顺序敏感) | 中等活跃(持续更新) | 类似JSON,需处理二进制 需POJO注解或预定义类 | 移动端、IoT设备、跨语言微服务 |
序列化方式合适的话会有一定的压缩率,但是我们仍然需要提供扩展接口Compress,压缩byte[] compress(byte[] bytes)和解压缩byte[] decompress(byte[] bytes)。比较常见的是GZIP,毕竟JDK内置确实方便好用,最好和序列化的一起打配合。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n框架 | 设计定位 | 压缩速度 | 压缩率 | 解压速度 | 内存占用 | 兼容性 | 易用性 | 适用场景 | 社区活跃度 |
---|---|---|---|---|---|---|---|---|---|
GZIP | 高压缩比,长期存储 | 慢 | 高 | 中等 | 中 | 跨平台(RFC 1952) | JDK原生支持,简单API | HTTP压缩、日志归档 | 稳定维护(JDK内置) |
LZ4 | 极致速度优先,低延迟场景 | 极快 | 低 | 极快 | 低 | 需第三方库 | 依赖lz4-java 库,API简洁 | 实时通信、内存缓存、游戏数据流 | 高活跃度(GitHub开源) |
Snappy | 速度与压缩率平衡,Google生态集成 | 快 | 中低 | 快 | 低 | 跨语言(C++/Java) | 简单API(如Snappy.compress() ) | 大数据传输(Hadoop、Kafka) | 高活跃度(Google维护) |
Bzip2 | 高压缩比,牺牲速度 | 极慢 | 最高 | 慢 | 高 | 需第三方库 | 依赖commons-compress 库 | 离线数据归档、静态资源压缩 | 低活跃度(维护较少) |
LZO | 快速解压,适合读取密集型场景 | 快(解压) | 低 | 极快 | 低 | 需第三方库 | 复杂配置(需指定算法版本) | 日志分析、HDFS存储 | 停滞(社区支持有限) |
LZMA/LZMA2 | 极高压缩比,适合长期归档 | 极慢 | 最高 | 中等 | 高(百MB级) | 需第三方库 | 配置复杂(需设置字典大小等参数) | 软件分发、历史数据归档 | 中等(7z生态维护) |
QuickLZ | 极速压缩,实时性优先 | 极快 | 中低 | 极快 | 低(KB级) | 社区支持差,自己写 | 文档少,自己写 | 实时通信、嵌入式设备、数据库写入 | 低活跃度(更新停滞) |
分布式部署的话肯定需要一个合理的负载均衡算法,这里也需要提供一个扩展接口。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n名称 | 配置 | 优点 | 缺点 |
---|---|---|---|
随机(默认) | random | 1. 按权重的进行随机,可以方便的调整1. 调用量越大分布越均匀 | 当调用次数比较少时,Random 产生的随机数可能会比较集中,此时多数请求会落到同一台服务器上。这个缺点并不是很严重,多数情况下可以忽略。RandomLoadBalance 是一个简单,高效的负载均衡实现,因此它被作为缺省实现。 |
轮询 | roundrobin | 1. 轮循,按公约后的权重设置轮循比率 | 如果某个Provider较慢,可能会积压请求 |
最少并发优先 | leastactive | 1. 在调用前后增加计数器,并发最小的Provider认为是最快的Provider并发大的Provider认为是慢的Provider。并发数相同再比较权重。这样快的Provider收到更多请求。 | 快速抛异常的Provider有可能被误认为是响应快的Provider。目前此问题已解决,Provider抛异常会降低选中概率,最低到10%,等Provider恢复并调用成功后,选中概率回恢复到100%。 |
一致性hash | consistenthash | 1. 服务端节点没变化的情况下,同样的请求(根据第一个参数值进行hash)会指向同一台机器,基于虚拟节点,如果一台挂了也没事,会平摊到其它提供者 | 可能调用分布不均匀。 |
本机IP调用优先 | localpref | 1. 本机IP上的Provider优先调用 匹配到多个使用随机调用本机IP,未匹配也随机调用远程机器 | 计算相同IP稍微增加点耗时。 |
最快响应 | shortestresponse | 根据平均响应时间 * 当前请求数,选取两者乘积最小的节点 | cpu偏高 |
平滑加权轮询 | swrr | 平滑加权轮询算法,provider接受到的流量更加均匀 | 计算会消耗些cpu |
自定义负载 | Extensible里的value | 自定义负载均衡算法 |
随着JDK的版本更新JDK动态代理的效率越来越高了,Spring默认代理接口也是用的这个。在查询资料的时候发现之前没怎么了解的Byte Buddy,神灯冲浪的时候发现之前有人了解过,ASM字节码操作类库(打开java语言世界通往字节码世界的大门)
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n框架 | 实现原理 | 依赖 | 代理限制 | 性能 | 易用性 | 内存开销 | 适用场景 |
---|---|---|---|---|---|---|---|
JDK动态代理 | 基于接口,通过反射生成代理类 | JDK原生支持 | 仅支持接口 | -生成速度:快(4 ms) -调用速度:反射较慢(约136 ms/千万次调用) | 简单(仅需实现InvocationHandler 接口) | 低(代理类由JVM缓存) | 接口代理(如Spring AOP默认策略) |
CGLIB | 基于继承,通过字节码增强生成目标类的子类 | 需引入cglib 库 | 可代理普通类(无法代理final 类和方法) | -生成速度:较慢(108 ms) -调用速度:较快(约215 ms/千万次) | 中等(需配置Enhancer 和MethodInterceptor ) | 较高(生成子类字节码) | 类代理(如Spring无接口时的AOP) |
Javassist | 基于源码或字节码操作,通过字符串拼接生成类和方法 | 需引入javassist 库 | 可代理普通类(需处理构造函数,支持动态生成新类) | -生成速度:中(39 ms) -调用速度:慢(1690 ms/千万次) | 高(通过字符串拼接生成代码,无需了解字节码) | 中等(支持动态生成和缓存) | 快速原型开发、动态生成类(如ORM工具) |
ASM | 直接操作JVM字节码指令,基于访问者模式生成类 | 需引入asm 库 | 可代理任何类(包括final 类,但无法覆盖final 方法) | -生成速度:极快(4 ms) -调用速度:最快(172 ms/千万次) | 低(需熟悉JVM指令和类结构) | 低(直接生成紧凑字节码) | 高性能场景(如序列化框架Kryo) |
Byte Buddy | 基于ASM的高层抽象,提供类型安全的DSL生成字节码 | 需引入byte-buddy 依赖 | 可代理任何类(支持复杂字节码操作) | -生成速度:快(接近ASM) -调用速度:接近ASM(179 ms/千万次) | 中等(需学习DSL语法,但比ASM友好) | 低(优化内存管理) | 复杂字节码操作(如Mockito、Hibernate) |
这篇写在今年的二月底吧,当时是计划去写定制化开发的RPC的,结果想了想还是应该从源头开始,顺道我也相对系统的建立起RPC的知识体系。本文相对来说已经非常详细了,当时本来打算是四部曲,原理、实战、公司框架分析和主流RPC对比。结果写原理也就是本篇写了一半,公司给推了一篇大佬写的公司框架分析,好家伙写的真好,瞬间就觉得没必要重复写了,实战篇在后面马上出,主流RPC对比不一定会写的,但是我比较想看看GRPC,如果有机会的话会拜读一下。emmm,这半年的总结,躺了这么久也该回归了,虽然还要和生活搏斗,但是老本行不能忘了。最近在做一个还算有点意思的东西,写了一段有点意思的代码,应该下个月就会分享出来。工作上有意思的东西还不算特别多,之前写了太多东西,其实新的环境也有点雷同,公司的东西封装的太狠,说实话我也没有细细品味,仅仅是在用着,不过文档都写得蛮不错的。对了,实战篇的时候,我参杂着公司的RPC文档来聊些私货吧,有些设计思路我觉得可以参考,毕竟中间件大部分都是差不多的思路。
\\n聊聊生活吧,又是深夜,沟槽的世界,到家10点,感觉还没干啥就0点多了,能不能给单身狗留一点追逐爱情的时间(大雾)。最近也是感受到了父母急切的心情,博主快28了,谁能想到还是一个纯情大男孩,淦,品味着男女对立的资讯,抱着吃屎的决心还要冲锋,这就是生活,一个巨大的SM场。当然我觉得我是看清了情感,所以才能更好地去追逐爱情,不过北京的相亲市场属实地狱,说是相亲其实也就扩列,我还是相对喜欢这种日久生情的感觉。但,哈哈,有时候就感觉男女沟通确实是门学问,大家都得学,还有思想独立拎得清的女生确实不多。以上就是我的一些简单吐槽,说是吃屎,但是我觉得这是我对期待生活和伴侣需要付出的代价吧,向阳而生吧,少年!
","description":"前言 距离上一次写正儿八经的技术文有个半年多了,抱歉抱歉ಥ_ಥ,确实有点怠惰了。工作强度吧,真的有点大,心力耗费有点严重,同时博主的相亲之路也不是很顺畅,北京的相亲市场简直就是神仙赶集(┬┬﹏┬┬),再加上工作和生活上事儿赶事儿,就慢慢躺了。不过心里确实还记着要写技术博客,也没耽搁,目前存稿和思路后面会出两篇,一篇RPC,一篇缓存相关。\\n\\n本文是RPC的经典一本通文章,因为RPC是一个偏概念的东西,所以会搭配一个DEMO来讲解,同时相关的知识会尽可能的丰富一下,保持我一贯的文章水准。文章从RPC的定义和特征讲起,介绍核心流程和关键组件…","guid":"https://juejin.cn/post/7480430771574095884","author":"云雨雪","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-12T01:29:21.824Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/505255a9f246466ea18c28cdc9a59706~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=KKCCOTATXwSzMERADE8K49InQkU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f33501b8f2142e99a99174b2573d36e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=au8iYb%2Fi6uWvX1oaugX1jYsEPHg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6ec8c43e03584ee38da3807d873fc3d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=r3hmSLF3V77MBFR6%2B%2FwykgXciYc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8f9c6e4427484e0b8d31f3a24137f210~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=lk3Vhte3VDQzZ6Xakviyac5bGcQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e22ce48f5fc4a0380a7fae5b06d3463~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=sPZdiBxfNzJMbXK%2FhC6xIn6fThQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c3c9f3b812704678a90d358d3dcab346~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=%2B%2BJI9%2FMbl7IdI6aCE6Nl6l6Yo6Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3333489282a04ef987e86758be1215e6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=Y2vjmuwULIn8b05me%2B3s9aF8qu0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f4da54bd99394a9e80805399822d2c32~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=Bw7JzYjnEI2odXgtDYKuDA%2BA%2BOQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1eda858aca534f0284dbe27ae9ce5f4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=xd9TaEc%2Byduo%2B3pBfIQnNsiDdcc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49202e6b989d47f2a2a8fd40c901b1d0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=9mJodcR%2FyG8cLk1MJJpse7i7cLI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/eb901bd0aa394b58a4974b1fd1ac4136~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR6Zuo6Zuq:q75.awebp?rk3s=f64ab15b&x-expires=1742474003&x-signature=Nyb8R8XAglAdUjsKHz%2FmDPYNj08%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","RPC","微服务"],"attachments":null,"extra":null,"language":null},{"title":"经过 10 亿级性能验证的隐私计算开源利器","url":"https://juejin.cn/post/7480430771573833740","content":"在数据驱动的时代,我们每天都在产生大量数据:购物记录、健康信息、社交关系……这些数据蕴含巨大价值,但也伴随着隐私泄露的风险。
\\n试想一下:
\\n传统的数据共享方式就像在数据世界里「裸奔」,风险巨大。那么,有没有一种方法能在保护隐私的前提下实现数据的价值共享呢?隐私计算技术应运而生,它能让数据在加密状态下完成计算,真正做到「数据可用不可见」。
\\n隐私计算的兴起主要源于两个趋势:
\\n到了 2025 年,随着生成式 AI 的普及,「AI 生成内容的归属权」已成为数据隐私领域的焦点问题。当企业利用用户数据训练 AI 模型时,谁拥有最终生成内容的权利?数据提供者如何确保自己的隐私不被侵犯?
\\n面对这些挑战,今天我们将介绍一款当前非常流行且易用的隐私计算框架——SecretFlow(隐语),它能让数据在加密状态下完成计算,真正实现“可用不可见”的隐私保护理念。
\\n\\n\\nGitHub 地址:github.com/secretflow/…
\\n
最令人惊喜的是,只要你会 Python 就能通过 SecretFlow(隐语)快速上手这一前沿技术。本教程将带你一步步体验隐私计算的神奇与乐趣,让你在数据安全与价值之间找到完美平衡点!此外,SecretFlow 拥有一个活跃的开源社区,并经常回馈贡献者,文末就有价值 1000 元的社区礼物。
\\n接下来,让我们开始 SecretFlow 的奇妙之旅吧!
\\nSecretFlow 是由蚂蚁密算团队开源的可信隐私计算框架,它就像数据世界的“安全卫士”,让数据在不暴露具体内容的情况下安全地进行计算和分析。有了 SecretFlow,不同机构可以像「戴着眼罩一起玩游戏」,既能高效地协作完成任务,又不会泄露各自数据。
\\n要体验 SecretFlow 很简单,官方提供 Docker 镜像,包含完整依赖直接启动即可使用。
\\n你可以选择完整版本或 lite 版本(不包含深度学习)。
\\n# 完整版本\\ndocker run -it secretflow/secretflow-anolis8:latest\\n\\n# Lite 版本(更小巧)\\ndocker run -it secretflow/secretflow-lite-anolis8:latest\\n
\\n我们用一个简单的例子「安全多方计算」来实际体验一下 SecretFlow,如何实现数据的「可用不可见」。
\\n什么是安全多方计算?
\\n安全多方计算(Secure Multi-Party Computation,MPC)是一种密码学技术,允许多个参与方在保护隐私的情况下,共同完成计算任务。
\\n举个例子:
\\n\\n\\nAlice、Bob 和 Carol 想知道他们三人的平均收入,但又不想泄露各自的收入数额。安全多方计算可以让他们输入各自数据后,仅输出最终平均值,而不暴露任何人的具体收入。
\\n
我们后续会用到几个概念:
\\n前面我们介绍了安全多方计算(MPC)的基本概念。接下来,我们以一个具体实例来展示 SecretFlow 如何让多个参与方安全计算数据的平均值,而不暴露任何人的具体收入数据。
\\nAlice、Bob 和 Carol 想计算他们三个人的平均收入,但又不想让彼此知道各自具体的收入。这是典型的安全多方计算应用场景。
\\n首先,我们创建一个本地模拟环境,包含三个参与方:Alice、Bob 和 Carol。
\\nimport secretflow as sf\\n\\n# 初始化 SecretFlow 本地模拟环境,包含三个参与方\\nsf.init(\\n parties={\'Alice\', \'Bob\', \'Carol\'},\\n address=\'local\'\\n)\\n
\\n每个参与方都会使用专属的计算设备来保存自己的数据。我们分别为 Alice、Bob 和 Carol 创建设备:
\\nalice = sf.PYU(\'Alice\')\\nbob = sf.PYU(\'Bob\')\\ncarol = sf.PYU(\'Carol\')\\n
\\n为保护隐私,每个参与方通过自己的设备输入各自的收入数据:
\\n# 假设 Alice、Bob 和 Carol 的收入分别是 5000、6000 和 7000\\nalice_income = alice(lambda: 5000)()\\nbob_income = bob(lambda: 6000)()\\ncarol_income = carol(lambda: 7000)()\\n
\\n注意:
\\n我们使用 SecretFlow 提供的安全计算协议(例如 SPU 设备)来安全地计算三个人的平均收入:
\\n# 创建一个安全计算设备(SPU),用于多方安全计算\\nspu = sf.SPU(sf.utils.testing.cluster_def([\'Alice\', \'Bob\', \'Carol\']))\\n\\n# 将三人的收入数据安全地汇总到 SPU 设备,并计算平均值\\naverage_income = spu(lambda x, y, z: (x + y + z) / 3)(alice_income, bob_income, carol_income)\\n
\\n在此过程中:
\\n直接打印结果会看到一个加密后的对象:
\\nprint(average_income)\\n# 输出示例:<SPUObject object at 0x7fdec24a15b0>\\n
\\n此时数据仍处于加密状态,我们需要使用 sf.reveal
方法安全地解密并查看结果:
# 安全地解密并查看结果\\nprint(\\"三人收入的平均值是:\\", sf.reveal(average_income))\\n# 输出:三人收入的平均值是: 6000.0\\n# 即 (5000+6000+7000)/3 = 6000\\n
\\n通过本实例,我们完整体验了 SecretFlow 如何在保护数据隐私的前提下实现安全多方计算。每位参与方输入了自己的敏感数据,却不会暴露给其他人,最终大家都得到了想要的计算结果(平均收入),真正实现了数据「可用不可见」。
\\nSecretFlow 通过以下几个层次来实现隐私保护的数据分析和机器学习:
\\n此外,SecretFlow 还关联了多个相关项目,如 Kuscia(一个轻量级隐私保护计算任务编排框架)、SCQL(一个允许多个不信任方联合分析的系统)、SPU(一个提供计算能力并保护私有数据的安全计算设备)、HEU(一个高性能同态加密算法库)和 YACL(一个包含密码学、网络和 IO 模块的 C++ 库)。
\\n我们可以把 SecretFlow 想象成一座数字化的「智能工厂」:
\\n由于篇幅有限,上面的内容仅介绍了 SecretFlow(隐语)的冰山一角。它不仅让上手隐私计算变得简单,也让数据安全不再成为数据价值释放的阻碍。无论你是开发者、研究人员还是数据分析师,SecretFlow 都能助你轻松开启隐私计算之旅。
\\n\\n\\nGitHub 地址:github.com/secretflow/…
\\n
如果你想进一步了解更多 SecretFlow 实践案例和高级用法,欢迎访问官方开源社区参与讨论、贡献代码,一起建设更安全可靠的数据世界。
\\n近期 SecretFlow 开源社区正在举办开春福利活动——文档季,推出了 150 多个「Good first issue」,非常适合新手贡献者参与。文档季任务主要分为两类:翻译任务和验证任务,都是基于开源文档的优化。参与活动不仅能提升自己的技术,还能获得多种奖品!包括:开发者专属徽章、证书、T 恤、天猫超市卡,同时本周还增设了特别奖:1000 元的礼品卡。
\\n快来参与 SecretFlow 社区的文档季活动,一起为开源贡献力量吧!
","description":"在数据驱动的时代,我们每天都在产生大量数据:购物记录、健康信息、社交关系……这些数据蕴含巨大价值,但也伴随着隐私泄露的风险。 试想一下:\\n\\n医院希望联合研究某种疾病,但患者数据无法直接共享。\\n银行想合作分析反欺诈信息,但客户隐私数据必须严格保护。\\nAI 公司需要使用大量用户数据训练模型,但用户对自己数据的使用方式几乎无法控制。\\n\\n传统的数据共享方式就像在数据世界里「裸奔」,风险巨大。那么,有没有一种方法能在保护隐私的前提下实现数据的价值共享呢?隐私计算技术应运而生,它能让数据在加密状态下完成计算,真正做到「数据可用不可见」。\\n\\n一、为什么要关注隐私计算?…","guid":"https://juejin.cn/post/7480430771573833740","author":"HelloGitHub","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-12T00:43:25.759Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d3a7653478047849cd7d2343f071eab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=sowAbuJzJd7a7jhHv5nwwv1BDa8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1410fb8c1fdc454eb25cf9f77a9a9ac6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=NWxhvtpXiooGNSlsdK3lsUr34BA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c7151e57653d43d3a8acc824d2105c96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=pPhjYufdLE3%2FxTiCJIioJ2CYK6s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aa92b69b9d6d4f97baf2668a2becc75a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=qPFqJ82gSi1ek8RlzbOxRSSR00Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/28a1008e563b4e958e9a66b193342514~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=%2FReiNSy5E3JOAeyllWPwAZZY6I0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ef30f0235ed6463097af810b0d31e1a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=bIOjadE6fEywHdO4%2FoSjOtf4V6s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/061e558b4a3644c7a48cb3f22592ce1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=WvnBd5GOL4GWp90e4e4NMM8weeY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3cf45970d36843b1bbea248a74f11613~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGVsbG9HaXRIdWI=:q75.awebp?rk3s=f64ab15b&x-expires=1742345005&x-signature=jmENGLGGmNrYg94uYnEQHXtuhjA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","GitHub","开源","Python"],"attachments":null,"extra":null,"language":null},{"title":"应用策略模式优化if_else","url":"https://juejin.cn/post/7480180932038262821","content":"目前在改造项目中的一个功能,简化一下业务逻辑 具体需求如下
\\n\\n\\n现在是需要提供一个接口,接口会传入一个mark标识 以及一个url 需要根据不同的mark标识的策略 在url后面拼接一些不同的参数 再将url返回。
\\n
常见的做法就是去使用 if/else 判断,看看url后面该拼什么参数 拼接后返回
\\n考虑到该接口后面可能需要适配更多的拼接策略 我使用策略模式对 if/else 进行了优化 具体代码如下
\\n首先定义处理url拼接的service 为了方便拓展 我们设计为一个接口,以及两个实现类,实现类分别代表了不同的拼接策略
\\n如下 分为A、B两种策略,A策略是需要拼接userId这个参数 值为a,B策略是需要拼接uuid这个参数 值为b(这里只是为了演示)
\\n\\npublic interface LinkRedirectService {\\n String getUrl(Map<String,String> map);\\n}\\n\\n@Service(\\"urlA\\")\\npublic class UrlALinkRedirectServiceImpl implements LinkRedirectService {\\n @Override\\n public String getUrl(Map<String, String> map) {\\n return map.get(\\"url\\") + \\"?userId=a\\";\\n }\\n}\\n\\n@Service(\\"urlB\\")\\npublic class UrlBExamLinkRedirectServiceImpl implements LinkRedirectService {\\n @Override\\n public String getUrl(Map<String, String> map) {\\n return map.get(\\"url\\") + \\"?uuid=b\\";\\n }\\n}\\n
\\n接下来 定义一个策略类 在初始化时 把上面的实现类都加载进内部的Map集合,bean名称为key、对象为value
\\n\\n@Component\\npublic class LinkRedirectServiceStrategy {\\n\\n @Autowired\\n ApplicationContext applicationContext;\\n\\n private static Map<String, LinkRedirectService> map = new HashMap<>();\\n\\n //bean初始化时 获取到所有LinkRedirectService类型的bean 注入到map集合\\n @PostConstruct\\n public void init() {\\n map = applicationContext.getBeansOfType(LinkRedirectService.class);\\n }\\n\\n //提供getUrl方法 供Controller层调用 直接根据传入的mark 找到对应的实现类 去执行拼接逻辑 避免了if/else\\n public String getUrl(Map<String, String> param) {\\n String mark = param.get(\\"mark\\");\\n if (map.containsKey(mark)) {\\n return map.get(mark).getUrl(param);\\n }\\n return param.get(\\"url\\");\\n }\\n}\\n\\n
\\n控制器层 注入这个 LinkRedirectServiceStrategy 、调用getUrl传入参数即可
\\n后续如果再需要添加新的策略,只需要为 LinkRedirectService 接口 增加新的实现类即可,同时还需要注意:实现类的bean名称需要和接口传入的mark保持一致。
","description":"需求 目前在改造项目中的一个功能,简化一下业务逻辑 具体需求如下\\n\\n现在是需要提供一个接口,接口会传入一个mark标识 以及一个url 需要根据不同的mark标识的策略 在url后面拼接一些不同的参数 再将url返回。\\n\\n常见的做法就是去使用 if/else 判断,看看url后面该拼什么参数 拼接后返回\\n\\n考虑到该接口后面可能需要适配更多的拼接策略 我使用策略模式对 if/else 进行了优化 具体代码如下\\n\\n代码\\n\\n首先定义处理url拼接的service 为了方便拓展 我们设计为一个接口,以及两个实现类,实现类分别代表了不同的拼接策略\\n\\n如下…","guid":"https://juejin.cn/post/7480180932038262821","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T07:57:10.023Z","media":null,"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"Mybatis-plus怎么更新Null字段?","url":"https://juejin.cn/post/7480157532038430757","content":"\\n本文介绍【Mybatis-plus】updateById()方法不能更新字段为null的原因及解决办法。
\\n在日常项目开发过程中,经常会使用Mybatis-plus的updateById()方法,快速将接收道德参数或者查询结果中原本不为null的字段更新为null,并且该字段在数据库中可为null,这个时候使用updateById()并不能实现这个操作,不会报错,但是对应的字段并没有更新为null。
\\nMybatis-plus的字段策略(FieldStrategy)有三种策略:
\\nIGNORED:0 忽略
\\nNOT_NULL:1 非 NULL,默认策略
\\nNOT_EMPTY:2 非空
\\n而默认的更新策略是NOT_NULL:非NULL; 即通过接口更新数据时数据为NULL值时将不更新进数据库。
\\n update table A set 字段a = null where 字段b = 条件\\n
\\n在配置文件中修改全局策略
\\nmybatis-plus.global-config.db-config.field-strategy=ignored\\n\\n#yml文件格式:\\nmybatis-plus:\\n global-config:\\n #字段策略 0:\\"忽略判断\\",1:\\"非 NULL 判断\\",2:\\"非空判断\\"\\n field-strategy: 0\\n
\\n这样做是进行全局配置,在更新时会忽略对所有字段的判断。但是如果一些字段没有传值过来,会被直接更新为null,可能会影响其它业务数据的准确性。不推荐使用此方法。
\\n根据具体情况,在需要更新的字段中调整验证注解,如验非空:
\\n@TableField(strategy=FieldStrategy.NOT_EMPTY)
这样的话,我们只需要在需要更新为null的字段上,设置忽略策略,如下:
\\n@TableField(updateStrategy = FieldStrategy.IGNORED)\\nprivate String updateBy;\\n
\\n设置好了之后,在更新时就可以直接使用mybatis-plus中的updateById方法就可以成功将字段更新为null,但是这样做存在一定的弊端,就是当需要这样处理的字段比较多时,要给对应的字段都要添加上这样的注解。
\\nUser user=userService.lambdaQuery().eq(User::getUserId,userId).one();\\nif(user!=null){\\n userService.update(user,new UpdateWrapper<User>().lambda()\\n .set(User::getUserName,null)\\n .eq(User::getUserId,user.getUserId()));\\n}\\n
\\n这种方法不会影响其它方法,不需要修改全局配置,也不需要在字段上单独加注解,只需要在使用的时候设置一下要修改的字段为null就可以更新成功,推荐使用方法4。
\\n","description":"本文介绍【Mybatis-plus】updateById()方法不能更新字段为null的原因及解决办法。 一、问题描述\\n\\n在日常项目开发过程中,经常会使用Mybatis-plus的updateById()方法,快速将接收道德参数或者查询结果中原本不为null的字段更新为null,并且该字段在数据库中可为null,这个时候使用updateById()并不能实现这个操作,不会报错,但是对应的字段并没有更新为null。\\n\\n二、问题原因\\n\\nMybatis-plus的字段策略(FieldStrategy)有三种策略:\\n\\nIGNORED:0 忽略\\n\\nNOT_NULL:1 非…","guid":"https://juejin.cn/post/7480157532038430757","author":"剽悍一小兔","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T07:10:50.104Z","media":[{"url":"https://juejin.cn/","type":"photo"},{"url":"https://juejin.cn/","type":"photo"},{"url":"https://juejin.cn/","type":"photo"},{"url":"https://juejin.cn/","type":"photo"}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"业务架构设计---硬件设备监控指标数据上报业务Java企业级架构","url":"https://juejin.cn/post/7480157532038332453","content":"河水水环境业务监测指标数据上报, 核心业务需求:
\\n业务例子:比如设备A上报(该设备sn 时间 A指标 监测结果)数据,就会触发对应的URL(我们这边提供)
\\n通过采用策略模式和事件驱动的设计,实现了对不同指标的灵活处理和扩展。
\\npackage com.shsc.wms.biz;\\n\\nimport com.shsc.wms.common.annotation.PlatformHandler;\\nimport com.shsc.wms.common.enums.TaskTypeEnum;\\nimport org.springframework.stereotype.Service;\\n\\nimport java.util.HashMap;\\nimport java.util.Map;\\n\\n@Service\\npublic class TaskEtlContext {\\n\\n private final Map<TaskTypeEnum, TaskEtlTemplateService> handlerMap = new HashMap<>();\\n\\n public TaskEtlContext(Map<String, TaskEtlTemplateService> map) {\\n map.forEach((k, v) -> {\\n PlatformHandler annotation = v.getClass().getAnnotation(PlatformHandler.class);\\n this.handlerMap.put(annotation.value(), v);\\n });\\n }\\n\\n public TaskEtlTemplateService getHandler(String code) {\\n return handlerMap.get(code);\\n }\\n\\n}\\n
\\npackage com.shsc.wms.common.annotation;\\n\\nimport com.shsc.wms.common.enums.TaskTypeEnum;\\nimport org.springframework.stereotype.Component;\\n\\nimport java.lang.annotation.*;\\n\\n@Target(ElementType.TYPE)\\n@Retention(RetentionPolicy.RUNTIME)\\n@Documented\\n@Component\\npublic @interface PlatformHandler {\\n\\n TaskTypeEnum value();\\n\\n}\\n
\\npackage com.shsc.wms.common.enums;\\n\\n/**\\n * @desc 指标对照关系\\n **/\\npublic enum TaskTypeEnum {\\n\\n TASK_A(\\"WarehouseReq\\", \\"AAAA\\");\\n\\n //入参类全路径\\n private String req;\\n //实体类全路径\\n private String entity;\\n\\n TaskTypeEnum(String req, String entity) {\\n this.req = req;\\n this.entity = entity;\\n }\\n\\n public String getReq() {\\n return req;\\n }\\n\\n public String getEntity() {\\n return entity;\\n }\\n\\n}\\n
\\npackage com.shsc.wms.biz;\\n\\nimport com.shsc.wms.common.enums.EtlTaskStatusEnum;\\nimport com.shsc.wms.entity.EtlTaskEntity;\\nimport com.shsc.wms.mapper.etl.EtlTaskEntityMapper;\\nimport org.springframework.beans.factory.annotation.Autowired;\\n\\nimport java.util.Date;\\nimport java.util.List;\\n\\npublic abstract class TaskEtlTemplateService<T> {\\n\\n @Autowired\\n private EtlTaskEntityMapper etlTaskEntityMapper;\\n\\n public final void handle(EtlTaskEntity taskEntity) {\\n\\n // 1 抓取数据\\n List<T> items = extract(taskEntity);\\n\\n // 2 保存或者更新 指标结果\\n saveOrUpdateResult(items);\\n\\n // 3 更新 task\\n updateTask(taskEntity);\\n\\n }\\n\\n /**\\n * 抓取数据\\n */\\n protected abstract List<T> extract(EtlTaskEntity taskEntity);\\n\\n /**\\n * 保存或者更新 指标结果\\n */\\n protected abstract void saveOrUpdateResult(List<T> items);\\n\\n /**\\n * 更新 task\\n */\\n public void updateTask(EtlTaskEntity taskEntity) {\\n taskEntity.setStatus(EtlTaskStatusEnum.FINISH.getCode());\\n taskEntity.setActualTime(new Date());\\n etlTaskEntityMapper.updateById(taskEntity);\\n }\\n\\n}\\n
\\npackage com.shsc.wms.service;\\n\\nimport com.shsc.wms.biz.TaskEtlTemplateService;\\nimport com.shsc.wms.common.annotation.PlatformHandler;\\nimport com.shsc.wms.common.enums.TaskTypeEnum;\\nimport com.shsc.wms.common.model.request.StoredLocationBatchReq;\\nimport com.shsc.wms.entity.EtlTaskEntity;\\nimport lombok.extern.slf4j.Slf4j;\\n\\nimport java.util.Collections;\\nimport java.util.List;\\n\\n@PlatformHandler(value = TaskTypeEnum.TASK_A)\\n@Slf4j\\npublic class TaskAService extends TaskEtlTemplateService<StoredLocationBatchReq> {\\n\\n @Override\\n protected List<StoredLocationBatchReq> extract(EtlTaskEntity taskEntity) {\\n log.info(\\"TaskAService TaskA 抓取数据\\");\\n return Collections.emptyList();\\n }\\n\\n @Override\\n protected void saveOrUpdateResult(List<StoredLocationBatchReq> items) {\\n log.info(\\"TaskAService TaskA 保存数据\\");\\n }\\n\\n\\n}\\n
\\npackage com.shsc.wms.biz;\\n\\nimport com.shsc.wms.common.constant.CommonConstant;\\nimport com.shsc.wms.common.core.ThreadUtil;\\nimport com.shsc.wms.common.core.UserInfoEntity;\\nimport com.shsc.wms.entity.EtlTaskEntity;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.scheduling.annotation.Async;\\nimport org.springframework.stereotype.Component;\\nimport org.springframework.transaction.TransactionStatus;\\nimport org.springframework.transaction.support.TransactionCallbackWithoutResult;\\nimport org.springframework.transaction.support.TransactionTemplate;\\n\\n@Component\\n@Slf4j\\npublic class EtlTask {\\n\\n @Autowired\\n private TaskEtlContext taskEtlContext;\\n\\n @Autowired\\n private TransactionTemplate transactionTemplate;\\n\\n @Async(value = CommonConstant.wmsEtlExecutor)\\n public void runTask(EtlTaskEntity taskEntity, UserInfoEntity userHolder) {\\n ThreadUtil.resetCurrentUserInfo(userHolder);\\n\\n // 获取任务处理器\\n TaskEtlTemplateService handler = taskEtlContext.getHandler(taskEntity.getTaskType());\\n\\n if (handler == null) {\\n log.error(\\"未找到对应的任务处理器,任务类型:{}\\", taskEntity.getTaskType());\\n return;\\n }\\n\\n // 使用编程式事务\\n transactionTemplate.execute(new TransactionCallbackWithoutResult() {\\n @Override\\n protected void doInTransactionWithoutResult(TransactionStatus status) {\\n try {\\n // 处理任务\\n handler.handle(taskEntity);\\n } catch (Exception e) {\\n // 发生异常时回滚事务\\n status.setRollbackOnly();\\n log.error(\\"执行任务时发生异常,事务已回滚\\", e);\\n }\\n }\\n });\\n }\\n\\n}\\n
","description":"策略模式企业级实践——硬件设备监控指标数据上报 业务背景\\n\\n河水水环境业务监测指标数据上报, 核心业务需求:\\n\\n多设备指标差异化处理:各设备,指标上报参数格式、加密方式、回调协议不同\\n监测指标类型扩展:支持 水温,悬浮物,pH值,化学需氧量(COD),大肠菌群 等等30+业务指标类型\\n高并发处理:日均处理百万级事件,峰值QPS 1000\\n数据一致性:保证不同设备上报不同指标数据完整性\\n\\n业务例子:比如设备A上报(该设备sn 时间 A指标 监测结果)数据,就会触发对应的URL(我们这边提供)\\n\\n通过采用策略模式和事件驱动的设计,实现了对不同指标的灵活处理和扩展…","guid":"https://juejin.cn/post/7480157532038332453","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T06:57:16.251Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a468577d7ded45de805e8db852ba63a2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742281036&x-signature=Rx0gwNyk42wLXSNdoAQ6NJL4pIA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2f97f4694f7e407cb8d36338c1c9e0f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1742281036&x-signature=65g2gu1n0h28g558%2BoSzc26KJT0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","架构"],"attachments":null,"extra":null,"language":null},{"title":"瞧瞧别人家的接口重试,那叫一个优雅!","url":"https://juejin.cn/post/7480061880922751010","content":"大家好,我是苏三,又跟大家见面了。
\\n记得五年前的一个深夜,某个电商平台的订单退款接口突发异常,因为银行系统网络抖动,退款请求连续失败。
\\n原本技术团队只是想“好心重试几次”,结果开发小哥写的重试代码竟疯狂调用了银行的退款接口 82次!
\\n最终导致用户账户重复退款,平台损失过百万。
\\n老板在复盘会上质问:“接口重试这么基础的事,为什么还能捅出大篓子?”
\\n大家哑口无言,因为所有人都以为只要加个 for
循环,再睡几秒就完事了……
这篇文章跟大家一起聊聊重试的7种常用方案,希望对你会有所帮助。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\n某实习生写的用户注册短信发送接口。
\\n在一个while循环中,重复调用第三方的发短信接口给用户发送短信。
\\n代码如下:
\\npublic void sendSms(String phone) {\\n int retry = 0;\\n while (retry < 5) { // 无脑循环\\n try {\\n smsClient.send(phone);\\n break;\\n } catch (Exception e) {\\n retry++;\\n Thread.sleep(1000); // 固定1秒睡眠\\n }\\n }\\n}\\n
\\n某次短信服务器出现了过载问题,导致所有请求都延迟了3秒。
\\n这个暴力循环的代码在 0.5秒内同时发起数万次重试,直接打爆短信平台,触发了 熔断封禁,连正常请求也被拒绝。
\\nSpring Retry适用于中小项目,通过注解快速实现基本重试和熔断(如订单状态查询接口)。
\\n通过声明@Retryable注解,来实现接口重试的功能。
\\n@Retryable(\\n value = {TimeoutException.class}, // 只重试超时异常\\n maxAttempts = 3,\\n backoff = @Backoff(delay = 1000, multiplier = 2) // 1秒→2秒→4秒\\n)\\npublic boolean queryOrderStatus(String orderId) {\\n return httpClient.get(\\"/order/\\" + orderId);\\n}\\n\\n@Recover // 兜底回退方法\\npublic boolean fallback() {\\n return false; \\n}\\n
\\n@CircuitBreaker
可快速阻断异常流量对于有些需要自定义退避算法、熔断策略和多层防护的大中型系统(如支付核心接口),我们可以使用 Resilience4j。
\\n核心代码如下:
\\n// 1. 重试配置:指数退避 + 随机抖动\\nRetryConfig retryConfig = RetryConfig.custom()\\n .maxAttempts(3)\\n .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(\\n 1000L, // 初始间隔1秒\\n 2.0, // 指数倍数\\n 0.3 // 随机抖动系数\\n ))\\n .retryOnException(e -> e instanceof TimeoutException)\\n .build();\\n\\n// 2. 熔断配置:错误率超50%时熔断\\nCircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()\\n .slidingWindow(10, 10, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) \\n .failureRateThreshold(50)\\n .build();\\n\\n// 组合使用\\nRetry retry = Retry.of(\\"payment\\", retryConfig);\\nCircuitBreaker cb = CircuitBreaker.of(\\"payment\\", cbConfig);\\n\\n// 执行业务逻辑\\nSupplier<Boolean> supplier = () -> paymentService.pay();\\nSupplier<Boolean> decorated = Decorators.ofSupplier(supplier)\\n .withRetry(retry)\\n .withCircuitBreaker(cb)\\n .decorate();\\n
\\n某电商大厂上线此方案后,支付接口 超时率下降60% ,且熔断触发频率降低近 90%
\\n真正做到了“打不还手,骂不还口”。
\\n高并发、允许延时的异步场景(如物流状态同步)。
\\nRocketMQ代码片段如下:
\\n// 生产者发送延时消息\\nMessage<String> message = new Message();\\nmessage.setBody(\\"订单数据\\");\\nmessage.setDelayTimeLevel(3); // RocketMQ预设的10秒延迟级别\\nrocketMQTemplate.send(message);\\n\\n// 消费者重试\\n@RocketMQMessageListener(topic = \\"DELAY_TOPIC\\")\\npublic class DelayConsumer {\\n @Override\\n public void handleMessage(Message message) {\\n try {\\n syncLogistics(message);\\n } catch (Exception e) {\\n // 重试次数 + 1,并重新发送到更高延迟级别\\n resendWithDelay(message, retryCount + 1);\\n }\\n }\\n}\\n
\\n如何RocketMQ的消费者消费失败,会自动发起重试。
\\n对于有些不需要实时反馈,允许批量处理的任务(如文件导入)的业务场景,我们可以使用定时任务。
\\n在这里以Quartz为例。
\\n具体代码如下:
\\n@Scheduled(cron = \\"0 0/5 * * * ?\\") // 每5分钟执行\\npublic void retryFailedTasks() {\\n List<FailedTask> list = failedTaskDao.listUnprocessed(5); // 查失败任务\\n list.forEach(task -> {\\n try {\\n retryTask(task);\\n task.markSuccess();\\n } catch (Exception e) {\\n task.incrRetryCount();\\n }\\n failedTaskDao.update(task);\\n });\\n}\\n
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n对于严格保证数据一致性的场景(如资金转账),我们可以使用两阶段提交机制。
\\n大致代码如下:
\\n@Transactional\\npublic void transfer(TransferRequest req) {\\n // 1. 记录流水\\n transferRecordDao.create(req, PENDING);\\n \\n // 2. 调用银行接口\\n boolean success = bankClient.transfer(req);\\n \\n // 3. 更新流水状态\\n transferRecordDao.updateStatus(req.getId(), success ? SUCCESS : FAILED);\\n \\n // 4. 失败转异步重试\\n if (!success) {\\n mqTemplate.send(\\"TRANSFER_RETRY_QUEUE\\", req);\\n }\\n}\\n
\\n对于一些多服务实例、多线程环境的防重复提交(如秒杀)的业务场景,我们可以使用分布式锁。
\\n这里以Redis + Lua的分布式锁为例。
\\n代码如下:
\\npublic boolean retryWithLock(String key, int maxRetry) {\\n String lockKey = \\"api_retry_lock:\\" + key;\\n for (int i = 0; i < maxRetry; i++) {\\n // 尝试获取分布式锁\\n if (redis.setnx(lockKey, \\"1\\", 30, TimeUnit.SECONDS)) {\\n try {\\n return callApi();\\n } finally {\\n redis.delete(lockKey);\\n }\\n }\\n Thread.sleep(1000 * (i + 1)); // 等待释放锁\\n }\\n return false;\\n}\\n
\\n重试就像机房里的灭火器——永远不希望用到它,但必须保证关键时刻能救命。
\\n我们工作中选择哪种方案?
\\n别只看技术潮流,而要看业务的长矛和盾牌,需要哪种配合。
\\n最后送大家一句话:系统稳定的秘诀,是永远对重试保持敬畏。
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n记得五年前的一个深夜,某个电商平台的订单退款接口突发异常,因为银行系统网络抖动,退款请求连续失败。\\n\\n原本技术团队只是想“好心重试几次”,结果开发小哥写的重试代码竟疯狂调用了银行的退款接口 82次!\\n\\n最终导致用户账户重复退款,平台损失过百万。\\n\\n老板在复盘会上质问:“接口重试这么基础的事,为什么还能捅出大篓子?”\\n\\n大家哑口无言,因为所有人都以为只要加个 for 循环,再睡几秒就完事了……\\n\\n这篇文章跟大家一起聊聊重试的7种常用方案,希望对你会有所帮助。\\n\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan…","guid":"https://juejin.cn/post/7480061880922751010","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T02:16:35.667Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"废弃手机秒变摄像头,我用Python偷看男朋友都在干啥👀","url":"https://juejin.cn/post/7480032817759862847","content":"前几天拿着父母的旧手机去卖钱,结果人家说:“手机太老,最多5块”!花姐不废话灰溜溜的拿着回家了。
\\n但最近我灵机一动,给它找了个新用途:秒变IP摄像头!而且,我还用Python写了个小脚本,随时随地用电脑偷偷看看“男朋友”都在干啥!(注意,正经用法也适用于监控猫主子和花盆里的绿植~)
\\n接下来,就跟我一起解锁这个新技能,顺便让你的废旧手机“死灰复燃”,实现它摄像头的最后价值!📱✨
\\n要开始这个有趣的小项目,你只需要以下装备:
\\nOpenCV
。pip install opencv-python\\n
\\n还有,如果你男朋友问你为什么对代码突然感兴趣,请保持微笑,不要慌。
\\n旧手机变摄像头,其实只需要一个神奇的APP:IP Webcam。它不仅能让手机秒变“监控神器”,还能随时随地输出实时视频流。来,跟我一起操作:
\\n下载IP Webcam:
\\n打开应用商店,搜“IP Webcam”,下载安装,别偷懒。国内主流应用商店可能不提供下载地址,可以自行上网搜索,是可以找到下载地址的。
启动视频服务:
\\n打开APP,直接点“Start Server/开启服务器”,然后你会看到一个神秘的地址,比如:http://192.168.0.101:8080
。这个地址就是手机摄像头的视频入口,记下来!💡
测试摄像头:
\\n在电脑的浏览器上输入刚才的地址,能看到实时画面就说明一切正常啦!
\\n至此,你的旧手机已经完美进化成一台摄像头。接下来,我们用Python“偷偷连线”它!🤓
说是“偷偷”,但其实是正经的技术实现!现在我们用Python写段小代码,把手机摄像头的画面抓到电脑上。
\\n来,直接上代码👇:
\\nimport cv2\\n\\n# 你的 IP Webcam 的 URL\\nip_camera_url = \'http://192.168.0.100:8080/video\'\\n\\n# 创建一个视频捕捉对象,连接到 IP Webcam 视频流\\ncap = cv2.VideoCapture(ip_camera_url)\\n\\nif not cap.isOpened():\\n print(\\"无法连接到 IP Webcam!\\")\\n exit()\\n\\n# 设置窗口名称\\nwindow_name = \\"what\'s U BF doing\\"\\n\\n# 创建窗口,并设置大小(例如设置为 640x480)\\ncv2.namedWindow(window_name, cv2.WINDOW_NORMAL)\\ncv2.resizeWindow(window_name, 640, 480)\\n\\nwhile True:\\n # 读取一帧视频\\n ret, frame = cap.read()\\n\\n if not ret:\\n print(\\"读取视频帧失败!\\")\\n break\\n\\n # 显示视频帧\\n cv2.imshow(window_name, frame)\\n\\n # 按 \'q\' 键退出\\n if cv2.waitKey(1) & 0xFF == ord(\'q\'):\\n break\\n\\n# 释放资源\\ncap.release()\\ncv2.destroyAllWindows()\\n\\n
\\nimport cv2\\n
\\ncv2
:OpenCV 的核心模块,提供视频捕获、图像处理等功能。
ip_camera_url = \'http://192.168.0.100:8080/video\'\\n
\\nip_camera_url
:是 IP Webcam 提供的视频流地址。你需要确保:
video
流(通常在默认端口 8080
)。cap = cv2.VideoCapture(ip_camera_url)\\n
\\ncv2.VideoCapture()
:用于创建一个视频捕捉对象。
ip_camera_url
),OpenCV 会尝试连接到对应的视频流。0
),OpenCV 会尝试访问本地的物理摄像头。if not cap.isOpened():\\n print(\\"无法连接到 IP Webcam!\\")\\n exit()\\n
\\ncap.isOpened()
:检查视频捕捉对象是否成功连接到视频流。window_name = \\"what\'s U BF doing\\"\\ncv2.namedWindow(window_name, cv2.WINDOW_NORMAL)\\ncv2.resizeWindow(window_name, 640, 480)\\n
\\ncv2.namedWindow()
:创建一个显示窗口。\\ncv2.WINDOW_NORMAL
表示窗口可以调整大小。cv2.resizeWindow()
:设置窗口的初始大小为 640x480 像素。while True:\\n ret, frame = cap.read()\\n
\\ncap.read()
:从视频流中读取一帧视频。\\nret
:布尔值,表示是否成功读取帧。frame
:读取的视频帧(以图像形式存储,类型是 Numpy 数组)。if not ret:\\n print(\\"读取视频帧失败!\\")\\n break\\n
\\nret
为 False
,说明读取帧失败,可能是网络中断或视频流停止。cv2.imshow(window_name, frame)\\n
\\ncv2.imshow()
:将视频帧显示到指定窗口中。if cv2.waitKey(1) & 0xFF == ord(\'q\'):\\n break\\n
\\ncv2.waitKey(1)
:等待键盘输入,参数 1
表示等待 1 毫秒。& 0xFF
:在某些系统中用于处理按键编码。ord(\'q\')
:表示键盘按下字母 \'q\'
键。\'q\'
键时,程序退出循环,结束视频捕捉。cap.release()\\ncv2.destroyAllWindows()\\n
\\ncap.release()
:释放视频捕捉对象,关闭与摄像头的连接。cv2.destroyAllWindows()
:关闭所有由 OpenCV 创建的窗口。what\'s U BF doing
的窗口中。\'q\'
键即可退出程序并释放资源。无法连接到 IP Webcam:
\\n视频卡顿或延迟:
\\n显示窗口问题:
\\nresizeWindow
的参数。学会了这段代码,你可以有更多好玩的扩展,比如:
\\n我们是否可以基于cv2读取的内容做个自动识别,比如提供男朋友照片,发现他出现以后就发邮件提醒我男朋友来了,快去围观!
\\n今天花姐就不实现这个功能了,后期如果感兴趣的人比较多,花姐在考虑是不是出个文章。这里提供一个思路,抛砖引玉。
\\n可以使用face_recognition
,它是一个非常流行且易于使用的 Python 库,专门用于进行人脸识别和处理。这个库背后采用的是深度学习技术,基于 dlib(一个强大的机器学习库)的人脸识别模型,提供了快速且准确的人脸识别能力。它不仅支持基础的人脸检测,还能完成面部特征点检测、面部表情识别、面部比对等高级功能。
\\n\\n是不是可以通过面部表情识功能统计下男朋友今天笑了几次?
\\n
smtplib
和email
库是个不错的选择,花姐的《0基础学Python》中有详细讲解用法。
最后友情提醒一句,千万别滥用这项技术哦,做个正直的程序员,记得事先获得“男朋友”或者其他人的同意。不然,代码的第一句可能就变成“如何恢复和平关系”了。😂
\\n学会了这个技能,记得来告诉花姐,你是拿它来干啥的?监控猫?还是种子发芽?我先不说了,我要去看我家猫主子睡成啥奇怪的姿势了~ 🐾
","description":"前几天拿着父母的旧手机去卖钱,结果人家说:“手机太老,最多5块”!花姐不废话灰溜溜的拿着回家了。 但最近我灵机一动,给它找了个新用途:秒变IP摄像头!而且,我还用Python写了个小脚本,随时随地用电脑偷偷看看“男朋友”都在干啥!(注意,正经用法也适用于监控猫主子和花盆里的绿植~)\\n\\n接下来,就跟我一起解锁这个新技能,顺便让你的废旧手机“死灰复燃”,实现它摄像头的最后价值!📱✨\\n\\n准备工作:一杯奶茶+废手机\\n\\n要开始这个有趣的小项目,你只需要以下装备:\\n\\n一台废弃手机(还能用摄像头的就行,别拿屏幕碎成蜘蛛网那种😂)。\\nPython的1个工具:OpenC…","guid":"https://juejin.cn/post/7480032817759862847","author":"花小姐的春天","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T01:35:46.149Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14ae1d31381448a0b420af4a18666119~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1742261746&x-signature=SabM1snFHpbCPdw5oUwA8RR6ENo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/08d86f282bd34e4ebade98418202a583~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Iqx5bCP5aeQ55qE5pil5aSp:q75.awebp?rk3s=f64ab15b&x-expires=1742261746&x-signature=Rsq8OF%2FVTozVH1xUeCxdgN70QSU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Python"],"attachments":null,"extra":null,"language":null},{"title":"项目优化-浏览器的缓存","url":"https://juejin.cn/post/7479994388023803958","content":"在开发项目时,我们经常需要向页面添加图片等资源。每次打开项目时,都需要请求这些资源,而网络请求总是耗时的。如果在一个时间段内频繁地访问同一个页面,每次都等待资源请求完成显然是低效的,特别是当这些资源没有变化的时候。为了提高效率,我们可以利用浏览器的缓存机制来存储频繁使用的资源,从而避免每次加载页面都进行网络请求。
\\n将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度
\\n例如,考虑以下HTML文档:
\\n<!DOCTYPE html>\\n<html lang=\\"en\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>Document</title>\\n</head>\\n<body>\\n <h1>hello 你好 世界</h1>\\n <img src=\\"assets/img/1.png\\" alt=\\"\\">\\n <h3>test</h3>\\n</body>\\n</html>\\n
\\n这是我们打开这个页面所需要进行的资源请求
\\n当加载此页面时,除了HTML文件本身,还需要请求其中包含的图片资源。为了加速这个过程,我们可以使用HTTP缓存中的强缓存机制。
\\n强缓存通过设置响应头中的Cache-Control
字段实现,该字段的值为max-age=xxx
,表示缓存的有效期(以秒为单位)。比如,在Node.js服务器端可以这样设置:
res.writeHead(200, {\\n \'content-type\': mime.getType(ext),\\n \'cache-control\': \'max-age=86400\', // 设置缓存一天\\n});\\n
\\n在开头提到的那个例子,我们修改Cache-Control
字段中的内容,将它时间改为一天,那么在我们第一次打开这个页面后,这个图片资源就会被强缓存下来\\n\\n此时,请求这个图片资源就直接从缓存中获取,提高了页面的加载速度。
强缓存的一个限制是它对通过浏览器地址栏直接访问的资源无效。因为这类请求会自动携带Cache-Control: max-age=0
头部信息,这意味着无法使用强缓存。
我们可以总结一下强缓存的特点:
\\n在响应头中设置 Cache-Control 字段,该字段的值为 max-age=xxx,表示缓存的有效期,单位为秒
\\n通过浏览器 url 地址栏请求的资源,请求头中就会自动携带 Cache-Control: max-age=0 字段,也就意味着这种资源无法被强缓存
\\n被缓存的资源在浏览器的 cache Storage 中,本质上还是在硬盘上
\\n强制刷新浏览器,会清空浏览器的 cache Storage
\\n那既然强缓存对浏览器地址栏访问的资源无效,那我们怎么可以再优化一些呢,那么这个时候我们就要用到协商缓存了
\\n\\n\\n协商缓存用于解决强缓存的不足。其工作原理是在首次请求时,服务端会在响应头中加入
\\nLast-Modified
字段,记录资源的最后修改时间。之后,当客户端再次请求该资源时,会在请求头中携带If-Modified-Since
字段,值为上次收到的Last-Modified
值。服务器根据这两个时间戳是否匹配决定返回304 Not Modified
还是新的资源。
const http = require(\'http\');\\nconst path = require(\'path\');\\nconst fs = require(\'fs\');\\nconst mime = require(\'mime\');\\n\\nconst server = http.createServer((req, res) => {\\n let filePath = path.resolve(__dirname, path.join(\'www\',req.url))\\n\\n if(fs.existsSync(filePath)){ // 判断文件是否存在\\n const stats = fs.statSync(filePath) // 获取文件信息 \\n const isDir = stats.isDirectory() // 判断是否为文件夹\\n \\n if(isDir){\\n filePath = path.join(filePath, \'index.html\') // 如果是文件夹,默认返回index.html\\n }\\n\\n if(!isDir || fs.existsSync(filePath)){ // 向前端返回文件\\n\\n const {ext} = path.parse(filePath)\\n const timeStamp = req.headers[\'if-modified-since\']\\n\\n\\n let status = 200\\n\\n if(timeStamp && Number(timeStamp) === stats.mtimeMs){ // 文件没有发生过更改\\n status = 304\\n }\\n\\n res.writeHead(status, {\\n \'content-type\': mime.getType(ext),\\n \'cache-control\': \'max-age=86400\', // 强缓存一天\\n \'last-modified\': stats.mtimeMs, // 最后修改时间\\n })\\n if(status === 200){\\n const readStream = fs.createReadStream(filePath) // 创建可读流\\n readStream.pipe(res) // 管道流,将可读流中的数据直接输出到res中\\n }else {\\n return res.end()\\n }\\n }\\n }\\n});\\n\\nserver.listen(3000)\\n
\\n\\n但是我们可以看到,为什么
html
资源还是没被缓存下来,这是因为Last-Modified
和If-Modified-Since
机制存在一个问题:即使内容未变但文件被修改后,Last-Modified
的时间戳也会更新,导致不必要的重新请求。
为了解决上述问题,可以采用ETag机制。ETag是一个独特的标识符,通常基于文件内容生成的哈希值。只有当文件内容改变时,ETag才会变化。
\\nconst http = require(\'http\');\\nconst path = require(\'path\');\\nconst fs = require(\'fs\');\\nconst mime = require(\'mime\');\\nconst checksum = require(\'checksum\'); // 计算文件的MD5值\\n\\nconst server = http.createServer((req, res) => {\\n let filePath = path.resolve(__dirname, path.join(\'www\', req.url))\\n\\n if (fs.existsSync(filePath)) { // 判断文件是否存在\\n const stats = fs.statSync(filePath); // 获取文件信息\\n const isDir = stats.isDirectory() // 判断是否为文件夹\\n if (isDir) {\\n filePath = path.join(filePath, \'index.html\') // 如果是文件夹,默认返回index.html\\n }\\n if (!isDir || fs.existsSync(filePath)) { // 向前端返回文件\\n const { ext } = path.parse(filePath);\\n const ifNoneMatch = req.headers[\'if-none-match\']\\n\\n checksum.file(filePath, (err, sum) => {\\n sum = `\\"${sum}\\"`\\n\\n if (ifNoneMatch === sum) { // 文件没有变化\\n\\n res.writeHead(304, {\\n \'Content-Type\': mime.getType(ext),\\n \'etag\': sum,\\n })\\n res.end()\\n\\n } else {\\n\\n res.writeHead(200, {\\n \'Content-Type\': mime.getType(ext),\\n \'Cache-Control\': \'max-age=1000000\',\\n \'etag\': sum,\\n })\\n const resStream = fs.createReadStream(filePath)\\n resStream.pipe(res)\\n\\n }\\n })\\n }\\n }\\n})\\n\\nserver.listen(3000);\\n
\\n只用强缓存可以把除url地址栏访问的资源缓存起来,但是资源跟新了就无法第一时间让前端获取到,所以还需要协商缓存
\\n只要命中了强缓存,就不会走协商缓存,只有强缓存到期,才会走协商缓存
\\n为了保证文件资源更新,前端能及时获取到,一般会在文件名后面加上文件指纹(用内容生成hash值),这样文件指就会改变,从而保证资源的更新
\\nJava面试资料(阿里内部资料):
\\n\\n(访问密码:3899)
\\n联合索引(复合索引)是指在多个列上创建的索引,通常适用于以下场景:
\\n查询涉及多个条件(多个列的 WHERE
过滤)
SELECT * FROM orders WHERE user_id = 1001 AND status = \'shipped\';\\n
\\n(user_id, status)
的联合索引,查询时可以有效利用索引。索引列经常同时出现在查询中
\\ncol1
和 col2
经常一起用于 WHERE
过滤、排序或分组,考虑创建 (col1, col2)
联合索引。查询需要覆盖索引
\\nSELECT
查询的列都包含在联合索引中,则可以直接使用索引数据,避免回表查询,提高性能(覆盖索引)。SELECT user_id, status FROM orders WHERE user_id = 1001;\\n
\\n(user_id, status)
索引,就可以直接从索引中取数据,无需访问表数据。索引顺序符合查询习惯
\\n不一定。主要取决于以下情况:
\\n该列是否高选择性
\\nuser_id
),单列索引可能就够了。status
只有几种状态),联合索引可能更好。未来查询是否会增加条件
\\nWHERE col1 = ?
,如果未来可能会加入 col2
,创建 (col1, col2)
联合索引更合适。是否有 ORDER BY
或 GROUP BY
需求
ORDER BY col1, col2
或 GROUP BY col1, col2
也较常见,联合索引有助于优化排序和分组。1. 只查询 user_id
SELECT * FROM orders WHERE user_id = 1001;\\n
\\nuser_id
选择性高,单列索引 (user_id)
就够了。status
也是高频过滤项,并且经常 WHERE user_id AND status
,可以考虑 (user_id, status)
联合索引。2. 既查询 user_id
又查询 status
SELECT * FROM orders WHERE user_id = 1001 AND status = \'shipped\';\\n
\\n(user_id, status)
联合索引比单独的 (user_id)
更有效,避免 status
需要额外的过滤。3. 只查询 status
SELECT * FROM orders WHERE status = \'shipped\';\\n
\\n(user_id, status)
联合索引存在,但 status
不是最左前缀,索引无法完全使用,可能需要额外的单列索引 (status)
。当面试官让我们聊一聊某些东西时,我们首先要想到对话的方式,一般都遵循
\\n这三点来进行回答,那么接下来我们来聊一聊跨域
\\n跨域问题主要出现在前端开发中,特别是涉及到前后端分离的项目。当一个网页的JavaScript代码试图通过Ajax等方式访问不同源(包括协议、域名、端口号之一不同)的API接口时,就会遇到跨域问题,这就是浏览器的同源策略。浏览器为了安全性考虑,默认阻止了这种跨域请求,除非服务器明确允许。
\\n同源策略(Same-Origin Policy)要求以下三要素完全一致:
\\nexample.com
)同源策略(Same-Origin Policy)是一种重要的安全机制,用于控制不同源(origin)之间的交互。这里的“源”由协议、域名和端口号三部分组成。当两个URL的这三个部分完全相同时,则它们属于同一源;只要有任何一部分不同,它们就被认为是不同源。
\\nhttp://www.example.com:80/a.html
与 https://www.example.com/b.html
不同源(协议不同)http://shop.example.com
与 http://pay.example.com
不同源(子域名不同)这是一个比较取巧的方法,利用script 标签src属性不受同源策略限制的特性,来发送请求
\\n利用 <script>
标签不受同源策略限制的特性:
<!DOCTYPE html>\\n<html lang=\\"en\\">\\n\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>Document</title>\\n</head>\\n\\n<body>\\n <button onclick=\\"handle()\\">请求</button>\\n <script>\\n function jsonp(url, cb) {\\n return new Promise((resolve, reject) => {\\n\\n const script = document.createElement(\'script\')\\n\\n window[cb] = function (data) {\\n // console.log(data) // 后端返回的数据\\n resolve(data)\\n }\\n\\n script.src = `${url}?cb=${cb}`\\n\\n document.body.appendChild(script)\\n // callback(\'hello world\')\\n\\n })\\n }\\n\\n\\n\\n\\n function handle() {\\n jsonp(\'http://localhost:3000\', \'callback\').then(res => {\\n console.log(res)\\n })\\n }\\n </script>\\n</body>\\n\\n</html>\\n
\\nconst http = require(\'http\');\\n\\nhttp.createServer((req, res) => {\\n const query = new URL(req.url, `http://${req.headers.host}`).searchParams\\n // console.log(query.get(\'cb\'));\\n \\n if (query.get(\'cb\')) {\\n const cb = query.get(\'cb\') // \'callback\'\\n const data = \'hello world\'\\n const result = `${cb}(\\"${data}\\")` // \\"callback(\'hello world\')\\"\\n res.end(result)\\n }\\n\\n // res.end(\'hello world\')\\n\\n}).listen(3000);\\n
\\n这是我们个人写项目最常用的解决跨域问题的方案
\\n\\n\\n后端设置 Access-Control-Allow-Origin: \'域名白名单\',来通知浏览器哪些域名可以跨域访问
\\n
<!DOCTYPE html>\\n<html lang=\\"en\\">\\n <head>\\n <meta charset=\\"UTF-8\\" />\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />\\n <title>Document</title>\\n </head>\\n <body>\\n <button onclick=\\"handle()\\">请求</button>\\n\\n <script>\\n function handle() {\\n const xhr = new XMLHttpRequest();\\n xhr.open(\\"GET\\", \\"http://localhost:3000\\", true);\\n xhr.send();\\n xhr.readystatechange = function () {\\n if (xhr.readyState === 4 && xhr.status === 200) {\\n console.log(xhr.responseText);\\n }\\n };\\n }\\n </script>\\n </body>\\n</html>\\n
\\nconst http = require(\'http\')\\n\\nconst server = http.createServer((req, res) => {\\n res.writeHead(200, { \\n \'Access-Control-Allow-Origin\': \'http://192.168.1.1:5500/\', // 允许所有域名跨域\\n \'Access-Control-Allow-Methods\': \'GET, POST, PUT, DELETE, PATCH, OPTIONS\', // 允许的请求方式\\n \'Access-Control-Allow-Headers\': \'Content-Type, Authorization, X-Requested-With\' // 允许的请求头\\n })\\n \\n res.end(\'hello world\')\\n})\\n\\nserver.listen(3000)\\n
\\nNginx 反向代理是一种服务器配置技术,它允许一台服务器作为另一台服务器的中介,接收来自客户端的请求,并将这些请求转发给后端服务器。在企业项目中,一般用这种方式解决跨域问题。
\\n\\n\\n前端服务器和后端服务器不在同一个域名下,前端服务器通过 nginx 反向代理来访问后端服务器
\\n
\\n如图所示
\\n\\n前端服务器和后端服务器不在同一个域名下,前端服务器通过 node 中间件来访问后端服务器
\\n
与nginx 反向代理大差不差,只是换了一种语言
\\n传统的前后端通信是基于http协议的, 是单向的, 只能从一端发到另一端, 无法双向通信。为了解决这个问题,开发者编写了websocket方法
\\n<!DOCTYPE html>\\n<html lang=\\"en\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>Document</title>\\n</head>\\n<body>\\n <script>\\n function WebSocketTest(url, params = {}) {\\n return new Promise((resolve, reject) => {\\n const socket = new WebSocket(url)\\n socket.onopen = () => {\\n socket.send(JSON.stringify(params))\\n }\\n socket.onmessage = (event) => {\\n resolve(event.data)\\n }\\n })\\n }\\n\\n\\n WebSocketTest(\'ws://localhost:3000\', {age: 18}).then(res => {\\n console.log(res)\\n })\\n </script>\\n</body>\\n</html>\\n
\\nconst WebSocket = require(\'ws\');\\n\\n// 在 3000 端口上建立 WebSocket 伺服器 (随时都在线的服务)\\nconst ws = new WebSocket.Server({ port: 3000 });\\n\\nlet count = 0\\n\\nws.on(\'connection\', (obj) => {\\n // console.log(obj);\\n obj.on(\'message\', (msg) => { // 收到客户端发来的消息\\n // console.log(msg.toString());\\n obj.send(\'收到了\')\\n\\n setInterval(() => {\\n count++\\n obj.send(count)\\n }, 2000)\\n \\n })\\n})\\n
\\n\\n\\n前面我们提到的跨域问题,都是产生在前端与后端传输数据时。其实,前端与前端也可以传输数据,并且也会有跨域问题,这里就要提到 iframe 标签了,它可以在一个前端页面中嵌套另一个页面,这两个页面进行数据传输时也会有跨域问题。
\\n
让我们用一个例子来说明:
\\n<!DOCTYPE html>\\n<html lang=\\"en\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>Document</title>\\n</head>\\n<body>\\n <h2>首页</h2>\\n <iframe id=\\"frame\\" src=\\"http://127.0.0.1:5500/%E8%B7%A8%E5%9F%9F/postMessage/detail.html\\" frameborder=\\"0\\" width=\\"800\\" height=\\"500\\"></iframe>\\n\\n <script>\\n let obj = {name: \'阿杰\', age: 18}\\n\\n document.getElementById(\'frame\').onload = function () {\\n this.contentWindow.postMessage(obj, \'http://127.0.0.1:5500\') // 向iframe发送消息\\n\\n window.onmessage = function (e) { // 接收iframe发送的消息\\n console.log(e.data);\\n }\\n\\n }\\n </script>\\n</body>\\n</html>\\n
\\n<!DOCTYPE html>\\n<html lang=\\"en\\">\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <title>Document</title>\\n</head>\\n<body>\\n <h3>详情页 --<span id=\\"title\\"></span></h3>\\n\\n <script>\\n let title = document.getElementById(\'title\')\\n\\n window.onmessage = function(e){\\n console.log(e.data);\\n title.innerHTML = e.data.age\\n\\n e.source.postMessage(\'阿杰 20了\', e.origin) // 向父级页面发送信息\\n }\\n \\n </script>\\n</body>\\n</html>\\n
\\n父页面 子页面\\n | |\\n |-- postMessage(data) ------\x3e|\\n | |\\n |<----- responseMessage -----|\\n | |\\n
\\n\\n\\n\\n
document.domain
允许将页面的域名设置为当前域或其父域,从而实现跨子域通信。与上述方法一致,但谷歌已经禁用此方法
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
JSONP | 兼容老旧浏览器 | 无需服务端改动 | 仅 GET,安全性低 |
CORS | 现代 Web 应用 | 标准化,支持所有方法 | 需服务端配合 |
Nginx 代理 | 生产环境部署 | 高性能,零侵入 | 需运维知识 |
WebSocket | 实时通信场景 | 全双工,低延迟 | 协议升级成本高 |
postMessage | 跨窗口通信 | 安全可控 | 仅限窗口间通信 |
这些就是我们解决跨域问题的七种方法,看到这里,你对跨域问题应该有了更深的理解。
\\n\\n\\n🔥 战力值突破 95% 警告!调优就像吃重庆火锅——要选对食材(数据结构)还要控制火候(算法)🌶️
\\n
graph TD\\n A[性能瓶颈] --\x3e B[数据结构选错]\\n A --\x3e C[频繁装箱拆箱]\\n A --\x3e D[无效数据搬运]\\n A --\x3e E[并发场景翻车]\\n style A fill:#f96,stroke:#333\\n
\\n// 反面教材:用 LinkedList 做随机访问\\nLinkedList<Integer> list = new LinkedList<>();\\nfor(int i=0; i<100000; i++){\\n list.get(i); // 比乌龟还慢🐢\\n}\\n\\n// 正面示范:ArrayList 闪电访问⚡\\nArrayList<Integer> array = new ArrayList<>();\\narray.get(99999); // 瞬间到达\\n
\\ngraph TB\\n A[需要排序?] --\x3e|是| B[TreeSet/TreeMap]\\n A --\x3e|否| C[需要快速访问?]\\n C --\x3e|是| D[ArrayList/HashMap]\\n C --\x3e|否| E[LinkedList/ArrayDeque]\\n style A fill:#9cf,stroke:#333\\n
\\n操作 | ArrayList | LinkedList | HashMap | TreeMap |
---|---|---|---|---|
随机访问 | ⚡⚡⚡⚡ | 🐌 | - | - |
头部插入 | 🐢 | ⚡⚡⚡ | - | - |
查找元素 | 🐌 | 🐌 | ⚡⚡⚡⚡ | ⚡⚡⚡ |
排序维持 | - | - | - | ⚡⚡⚡⚡ |
场景 | 首选方案 | 次选方案 | 雷区方案 |
---|---|---|---|
高频随机访问 | ArrayList⚡ | CopyOnWriteArrayList | LinkedList💣 |
频繁增删首部 | ArrayDeque🌪️ | LinkedList | ArrayList💣 |
大数据去重 | HashSet🚀 | TreeSet | List+contains💣 |
范围查询 | TreeMap🌳 | HashMap+排序 | LinkedList💣 |
graph LR\\n A[原始数据] --\x3e B{过滤空值}\\n B --\x3e C[转换数据类型]\\n C --\x3e D{分组}\\n D --\x3e E[统计各组数量]\\n E --\x3e F[输出结果]\\n \\n style B fill:#f96,stroke:#333\\n style D fill:#9cf,stroke:#333\\n
\\nList<Integer> nums = IntStream.range(0,1000000).boxed().collect(Collectors.toList());\\n\\n// 错误顺序:先过滤掉大部分元素再做复杂计算\\nlong cost1 = measure(() -> \\n nums.stream()\\n .filter(n -> n%1000 == 0)\\n .map(this::heavyCalculate)\\n .count()\\n);\\n\\n// 正确顺序:先处理耗时操作再过滤\\nlong cost2 = measure(() ->\\n nums.stream()\\n .map(this::heavyCalculate)\\n .filter(n -> n%1000 == 0)\\n .count()\\n);\\n\\nSystem.out.println(\\"性能差距:\\"+(cost1/cost2)+\\"倍!\\"); // 输出:性能差距:32倍!\\n
\\n// 优化案例:电商订单处理\\norders.parallelStream()\\n .filter(o -> o.getStatus() == PAID) // 先过滤无效数据\\n .map(Order::convertToDTO) // 转换传输对象\\n .collect(groupingBy(OrderDTO::getCategory, \\n summingInt(OrderDTO::getAmount))) // 按类目汇总\\n .forEach((k,v) -> sendToBI(k, v)); // 发送到大数据平台\\n
\\n// 找出第一个长度超过10的单词\\nList<String> words = readDictionary(); // 10万词汇量\\n\\n// 传统方式:遍历全部元素\\nwords.stream().filter(w -> w.length()>10).findFirst();\\n\\n// 涡轮增压:发现符合条件立即停车🚦\\nwords.stream().peek(w->System.out.println(\\"处理:\\"+w))\\n .filter(w -> w.length()>10)\\n .findAny();\\n
\\ngraph LR\\n A[是否CPU密集型?] --\x3e|是| B[适合并行]\\n A --\x3e|否| C[放弃并行]\\n D[数据量>1万?] --\x3e|是| B\\n D --\x3e|否| C\\n E[是否线程安全?] --\x3e|是| B\\n E --\x3e|否| C\\n
\\nList<Integer> data = IntStream.range(0,100000).boxed().collect(Collectors.toList());\\n\\n// 错误示范:在并行流中操作非线程安全集合\\nList<Integer> unsafeList = new ArrayList<>();\\ndata.parallelStream()\\n .filter(n -> n%2==0)\\n .forEach(unsafeList::add); // 💥 数据丢失警告!\\n\\n// 正确姿势:使用并发集合\\nList<Integer> safeList = Collections.synchronizedList(new ArrayList<>());\\ndata.parallelStream()\\n .filter(n -> n%2==0)\\n .forEach(safeList::add);\\n
\\n// 错误示范:使用 Integer 集合存数据\\nList<Integer> boxedList = IntStream.range(0,1000000)\\n .boxed() // 产生百万个对象📦\\n .collect(Collectors.toList());\\n\\n// 优化方案:使用原始类型流\\nint[] primitiveArray = IntStream.range(0,1000000)\\n .toArray(); // 内存占用减少40%!\\n
\\n// 反面教材:默认大小频繁扩容\\nList<User> users = new ArrayList<>(); // 默认容量10\\nIntStream.range(0,100000).forEach(i->users.add(new User())); // 扩容13次!\\n\\n// 祖师爷推荐:预分配空间\\nList<User> optimizedList = new ArrayList<>(100000); // 一步到位🚀\\n
\\n@State(Scope.Thread)\\npublic class BenchmarkDemo {\\n List<Integer> data = IntStream.range(0,1000000).boxed().collect(Collectors.toList());\\n \\n @Benchmark\\n public long testStream() {\\n return data.stream().filter(n -> n%2==0).count();\\n }\\n \\n @Benchmark\\n public long testParallelStream() {\\n return data.parallelStream().filter(n -> n%2==0).count();\\n }\\n}\\n
\\n测试用例 | 模式 | 吞吐量 (ops/ms) | 性能提升 |
---|---|---|---|
testStream | 顺序流 | 123.4 | 基准 |
testParallelStream | 并行流 | 456.7 | 3.7倍 |
传统for循环 | 单线程 | 156.8 | 1.27倍 |
🔥《性能九阳真经》终极奥义:\\n一测:基准测试定乾坤\\n二观:监控指标找热点 \\n三改:精准优化关键点\\n四防:预防性能退化\\n五复:持续迭代验证\\n六记:优化记录文档\\n七衡:权衡时间收益\\n八借:善用工具分析\\n九破:突破思维定式\\n
\\nList<Employee> employees = generateMillionEmployees(); \\n\\n// 要求:在 2s 内完成按工资排序+前100名筛选\\nList<Employee> result = employees.stream()\\n // 在这里施展你的魔法...\\n
\\ngraph TD\\n A[10万QPS请求] --\x3e B{缓存策略}\\n B --\x3e C[ConcurrentHashMap]\\n B --\x3e D[Caffeine]\\n B --\x3e E[Redis]\\n style A fill:#f44,stroke:#333\\n
\\n// 剧透代码:ArrayList 扩容核心逻辑\\nprivate void grow(int minCapacity) {\\n int oldCapacity = elementData.length;\\n int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容\\n // 祖师爷的数学魔法...\\n}\\n
\\n\\n","description":"🚀 Java 集合框架大师课:性能调优火葬场(四) 🔥 战力值突破 95% 警告!调优就像吃重庆火锅——要选对食材(数据结构)还要控制火候(算法)🌶️\\n\\n第一章:性能瓶颈大追捕\\n1.1 常见性能刺客图鉴\\ngraph TD\\n A[性能瓶颈] --\x3e B[数据结构选错]\\n A --\x3e C[频繁装箱拆箱]\\n A --\x3e D[无效数据搬运]\\n A --\x3e E[并发场景翻车]\\n style A fill:#f96,stroke:#333\\n\\n1.2 祖师爷的性能忠告\\n// 反面教材:用 LinkedList 做随机访问\\nLin…","guid":"https://juejin.cn/post/7479995740031565887","author":"无限大6","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T11:09:59.952Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot 实现 RSA+AES 自动接口解密!","url":"https://juejin.cn/post/7479766671504097295","content":"🌟 终极忠告:性能调优不是玄学,是数据驱动的科学!用基准测试说话,用数据选择最优解📊 记住:最快的代码是永远不执行的代码!🚫💻
\\n
在现代应用开发中,接口安全性变得越来越重要。当敏感数据通过网络传输时,如何确保数据不被窃取或篡改?本文将详细介绍如何在 SpringBoot 应用中实现 RSA+AES 混合加密方案,为接口通信提供强大的安全保障。
\\n在没有加密的情况下,通过网络传输的数据可以被抓包工具轻松获取。特别是当传输包含用户隐私、支付信息等敏感数据时,这种风险更加不可接受。接口加密能够确保即使数据被截获,攻击者也无法理解其中的内容。
\\n我们选择 RSA+AES 混合加密方案是因为它结合了两种算法的优点:
\\n通过混合使用这两种算法,我们用 RSA 来加密 AES 的密钥,然后用 AES 来加密实际传输的数据,既保证了安全性,又兼顾了性能。
\\n下面我们将通过实例代码演示如何在 SpringBoot 中实现这一方案。
\\n首先在 pom.xml
中添加必要的依赖:
<dependencies>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-web</artifactId>\\n </dependency>\\n <dependency>\\n <groupId>org.projectlombok</groupId>\\n <artifactId>lombok</artifactId>\\n <optional>true</optional>\\n </dependency>\\n <dependency>\\n <groupId>com.alibaba</groupId>\\n <artifactId>fastjson</artifactId>\\n <version>1.2.78</version>\\n </dependency>\\n <dependency>\\n <groupId>org.bouncycastle</groupId>\\n <artifactId>bcprov-jdk15on</artifactId>\\n <version>1.68</version>\\n </dependency>\\n</dependencies>\\n
\\n下面是我们需要实现的加密工具类:
\\npackage com.example.secureapi.utils;\\n\\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\\n\\nimport javax.crypto.Cipher;\\nimport javax.crypto.KeyGenerator;\\nimport javax.crypto.SecretKey;\\nimport javax.crypto.spec.IvParameterSpec;\\nimport javax.crypto.spec.SecretKeySpec;\\nimport java.nio.charset.StandardCharsets;\\nimport java.security.*;\\nimport java.security.spec.PKCS8EncodedKeySpec;\\nimport java.security.spec.X509EncodedKeySpec;\\nimport java.util.Base64;\\n\\npublic class EncryptionUtils {\\n // 添加BouncyCastle作为安全提供者,提供更强大的加密算法支持\\n static {\\n Security.addProvider(new BouncyCastleProvider());\\n }\\n\\n // AES加密配置\\n private static final String AES_ALGORITHM = \\"AES/CBC/PKCS7Padding\\"; // 使用CBC模式和PKCS7填充\\n private static final int AES_KEY_SIZE = 256; // 使用256位密钥提供更强的安全性\\n \\n // RSA加密配置\\n private static final String RSA_ALGORITHM = \\"RSA/ECB/PKCS1Padding\\"; // 使用PKCS1填充\\n private static final int RSA_KEY_SIZE = 2048; // 使用2048位密钥长度,提供足够的安全强度\\n\\n /**\\n * 生成RSA密钥对\\n * @return 包含公钥和私钥的密钥对\\n * @throws Exception 生成过程中可能出现的异常\\n */\\n public static KeyPair generateRSAKeyPair() throws Exception {\\n KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(\\"RSA\\");\\n keyPairGenerator.initialize(RSA_KEY_SIZE);\\n return keyPairGenerator.generateKeyPair();\\n }\\n\\n /**\\n * 将密钥转换为Base64编码字符串,便于存储和传输\\n * @param key 密钥\\n * @return Base64编码的密钥字符串\\n */\\n public static String keyToString(Key key) {\\n return Base64.getEncoder().encodeToString(key.getEncoded());\\n }\\n\\n /**\\n * 从Base64编码的字符串恢复RSA公钥\\n * @param keyStr Base64编码的公钥字符串\\n * @return RSA公钥对象\\n * @throws Exception 转换过程中可能出现的异常\\n */\\n public static PublicKey stringToRSAPublicKey(String keyStr) throws Exception {\\n byte[] keyBytes = Base64.getDecoder().decode(keyStr);\\n X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);\\n KeyFactory keyFactory = KeyFactory.getInstance(\\"RSA\\");\\n return keyFactory.generatePublic(keySpec);\\n }\\n\\n /**\\n * 从Base64编码的字符串恢复RSA私钥\\n * @param keyStr Base64编码的私钥字符串\\n * @return RSA私钥对象\\n * @throws Exception 转换过程中可能出现的异常\\n */\\n public static PrivateKey stringToRSAPrivateKey(String keyStr) throws Exception {\\n byte[] keyBytes = Base64.getDecoder().decode(keyStr);\\n PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);\\n KeyFactory keyFactory = KeyFactory.getInstance(\\"RSA\\");\\n return keyFactory.generatePrivate(keySpec);\\n }\\n\\n /**\\n * 生成随机AES密钥\\n * @return AES密钥\\n * @throws Exception 生成过程中可能出现的异常\\n */\\n public static SecretKey generateAESKey() throws Exception {\\n KeyGenerator keyGen = KeyGenerator.getInstance(\\"AES\\");\\n keyGen.init(AES_KEY_SIZE);\\n return keyGen.generateKey();\\n }\\n\\n /**\\n * 从Base64编码的字符串恢复AES密钥\\n * @param keyStr Base64编码的AES密钥字符串\\n * @return AES密钥对象\\n */\\n public static SecretKey stringToAESKey(String keyStr) {\\n byte[] keyBytes = Base64.getDecoder().decode(keyStr);\\n return new SecretKeySpec(keyBytes, \\"AES\\");\\n }\\n\\n /**\\n * 使用RSA公钥加密数据\\n * @param data 待加密数据\\n * @param publicKey RSA公钥\\n * @return Base64编码的加密数据\\n * @throws Exception 加密过程中可能出现的异常\\n */\\n public static String encryptWithRSA(String data, PublicKey publicKey) throws Exception {\\n Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);\\n cipher.init(Cipher.ENCRYPT_MODE, publicKey);\\n byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));\\n return Base64.getEncoder().encodeToString(encryptedBytes);\\n }\\n\\n /**\\n * 使用RSA私钥解密数据\\n * @param encryptedData Base64编码的加密数据\\n * @param privateKey RSA私钥\\n * @return 解密后的原始数据\\n * @throws Exception 解密过程中可能出现的异常\\n */\\n public static String decryptWithRSA(String encryptedData, PrivateKey privateKey) throws Exception {\\n byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);\\n Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);\\n cipher.init(Cipher.DECRYPT_MODE, privateKey);\\n byte[] decryptedBytes = cipher.doFinal(encryptedBytes);\\n return new String(decryptedBytes, StandardCharsets.UTF_8);\\n }\\n\\n /**\\n * 使用AES密钥加密数据\\n * @param data 待加密数据\\n * @param secretKey AES密钥\\n * @param iv 初始化向量\\n * @return Base64编码的加密数据\\n * @throws Exception 加密过程中可能出现的异常\\n */\\n public static String encryptWithAES(String data, SecretKey secretKey, byte[] iv) throws Exception {\\n Cipher cipher = Cipher.getInstance(AES_ALGORITHM, \\"BC\\");\\n IvParameterSpec ivSpec = new IvParameterSpec(iv);\\n cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);\\n byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));\\n return Base64.getEncoder().encodeToString(encryptedBytes);\\n }\\n\\n /**\\n * 使用AES密钥解密数据\\n * @param encryptedData Base64编码的加密数据\\n * @param secretKey AES密钥\\n * @param iv 初始化向量\\n * @return 解密后的原始数据\\n * @throws Exception 解密过程中可能出现的异常\\n */\\n public static String decryptWithAES(String encryptedData, SecretKey secretKey, byte[] iv) throws Exception {\\n byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);\\n Cipher cipher = Cipher.getInstance(AES_ALGORITHM, \\"BC\\");\\n IvParameterSpec ivSpec = new IvParameterSpec(iv);\\n cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);\\n byte[] decryptedBytes = cipher.doFinal(encryptedBytes);\\n return new String(decryptedBytes, StandardCharsets.UTF_8);\\n }\\n\\n /**\\n * 生成随机初始化向量\\n * @return 16字节的随机初始化向量\\n */\\n public static byte[] generateIV() {\\n SecureRandom random = new SecureRandom();\\n byte[] iv = new byte[16]; // AES使用16字节的初始化向量\\n random.nextBytes(iv);\\n return iv;\\n }\\n}\\n
\\n为了实现加密请求的自动解密,我们需要定义一个请求包装类:
\\npackage com.example.secureapi.model;\\n\\nimport lombok.Data;\\n\\n@Data\\npublic class EncryptedRequest {\\n // 使用RSA加密后的AES密钥\\n private String encryptedKey;\\n \\n // 使用Base64编码的AES初始化向量\\n private String iv;\\n \\n // 使用AES加密后的业务数据\\n private String encryptedData;\\n\\n // 可选:时间戳,用于防重放攻击\\n private Long timestamp;\\n\\n // 可选:签名,用于验证请求完整性\\n private String signature;\\n}\\n
\\n我们接下来实现一个请求解密拦截器,它会在控制器处理请求之前自动解密数据:
\\npackage com.example.secureapi.interceptor;\\n\\nimport com.alibaba.fastjson.JSON;\\nimport com.alibaba.fastjson.JSONObject;\\nimport com.example.secureapi.annotation.Decrypt;\\nimport com.example.secureapi.model.EncryptedRequest;\\nimport com.example.secureapi.utils.EncryptionUtils;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springframework.beans.factory.annotation.Value;\\nimport org.springframework.stereotype.Component;\\nimport org.springframework.web.method.HandlerMethod;\\nimport org.springframework.web.servlet.HandlerInterceptor;\\n\\nimport javax.crypto.SecretKey;\\nimport javax.servlet.http.HttpServletRequest;\\nimport javax.servlet.http.HttpServletResponse;\\nimport java.io.BufferedReader;\\nimport java.io.ByteArrayInputStream;\\nimport java.io.InputStreamReader;\\nimport java.nio.charset.StandardCharsets;\\nimport java.security.PrivateKey;\\nimport java.util.Base64;\\nimport java.util.stream.Collectors;\\n\\n@Slf4j\\n@Component\\npublic class DecryptInterceptor implements HandlerInterceptor {\\n\\n // 从配置文件注入RSA私钥,用于解密AES密钥\\n @Value(\\"${security.rsa.private-key}\\")\\n private String rsaPrivateKeyStr;\\n\\n @Override\\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\\n // 只处理带有@Decrypt注解的控制器方法\\n if (handler instanceof HandlerMethod) {\\n HandlerMethod handlerMethod = (HandlerMethod) handler;\\n \\n // 检查方法或类是否有@Decrypt注解\\n Decrypt decryptAnnotation = handlerMethod.getMethodAnnotation(Decrypt.class);\\n if (decryptAnnotation == null) {\\n decryptAnnotation = handlerMethod.getBeanType().getAnnotation(Decrypt.class);\\n }\\n \\n // 如果有@Decrypt注解,进行解密处理\\n if (decryptAnnotation != null) {\\n // 读取请求体内容\\n String requestBody = request.getReader().lines().collect(Collectors.joining());\\n \\n // 解析加密的请求对象\\n EncryptedRequest encryptedRequest = JSON.parseObject(requestBody, EncryptedRequest.class);\\n \\n // 获取RSA私钥\\n PrivateKey rsaPrivateKey = EncryptionUtils.stringToRSAPrivateKey(rsaPrivateKeyStr);\\n \\n // 解密AES密钥\\n String aesKeyStr = EncryptionUtils.decryptWithRSA(encryptedRequest.getEncryptedKey(), rsaPrivateKey);\\n SecretKey aesKey = EncryptionUtils.stringToAESKey(aesKeyStr);\\n \\n // 获取初始化向量\\n byte[] iv = Base64.getDecoder().decode(encryptedRequest.getIv());\\n \\n // 使用AES密钥解密实际数据\\n String decryptedData = EncryptionUtils.decryptWithAES(encryptedRequest.getEncryptedData(), aesKey, iv);\\n \\n // 防重放攻击检查(可选)\\n if (encryptedRequest.getTimestamp() != null) {\\n long currentTime = System.currentTimeMillis();\\n // 如果请求时间戳与当前时间相差超过5分钟,则拒绝请求\\n if (Math.abs(currentTime - encryptedRequest.getTimestamp()) > 300000) {\\n response.setStatus(HttpServletResponse.SC_FORBIDDEN);\\n response.getWriter().write(\\"{\\"code\\":403,\\"message\\":\\"请求已过期\\"}\\");\\n return false;\\n }\\n }\\n \\n log.debug(\\"解密后的数据: {}\\", decryptedData);\\n \\n // 创建一个包含解密数据的新BufferedReader,替换原有的请求Reader\\n request.setAttribute(\\"DECRYPTED_DATA\\", decryptedData);\\n \\n // 包装请求,使控制器能够读取解密后的数据\\n return wrapRequest(request, decryptedData);\\n }\\n }\\n return true;\\n }\\n \\n /**\\n * 包装HttpServletRequest,替换请求体内容为解密后的数据\\n * @param request 原始请求\\n * @param decryptedData 解密后的数据\\n * @return 是否成功包装请求\\n */\\n private boolean wrapRequest(HttpServletRequest request, String decryptedData) {\\n try {\\n // 创建包装后的请求对象\\n DecryptedRequestWrapper wrapper = new DecryptedRequestWrapper(request, decryptedData);\\n // 替换当前请求\\n request.setAttribute(\\"org.springframework.web.util.WebUtils.ERROR_EXCEPTION_ATTRIBUTE\\", wrapper);\\n return true;\\n } catch (Exception e) {\\n log.error(\\"包装请求失败\\", e);\\n return false;\\n }\\n }\\n \\n /**\\n * 自定义请求包装类,用于替换请求体内容\\n */\\n private static class DecryptedRequestWrapper extends HttpServletRequestWrapper {\\n private final String decryptedData;\\n \\n public DecryptedRequestWrapper(HttpServletRequest request, String decryptedData) {\\n super(request);\\n this.decryptedData = decryptedData;\\n }\\n \\n @Override\\n public BufferedReader getReader() {\\n return new BufferedReader(new InputStreamReader(\\n new ByteArrayInputStream(decryptedData.getBytes(StandardCharsets.UTF_8))));\\n }\\n \\n @Override\\n public ServletInputStream getInputStream() {\\n final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(\\n decryptedData.getBytes(StandardCharsets.UTF_8));\\n \\n return new ServletInputStream() {\\n @Override\\n public boolean isFinished() {\\n return byteArrayInputStream.available() == 0;\\n }\\n \\n @Override\\n public boolean isReady() {\\n return true;\\n }\\n \\n @Override\\n public void setReadListener(ReadListener readListener) {\\n throw new UnsupportedOperationException();\\n }\\n \\n @Override\\n public int read() {\\n return byteArrayInputStream.read();\\n }\\n };\\n }\\n }\\n}\\n
\\n现在还需要补充缺少的导入包,以及添加解密注解:
\\npackage com.example.secureapi.annotation;\\n\\nimport java.lang.annotation.*;\\n\\n/**\\n * 标记需要解密处理的控制器或方法\\n * 可以应用于类或方法级别\\n */\\n@Target({ElementType.METHOD, ElementType.TYPE})\\n@Retention(RetentionPolicy.RUNTIME)\\n@Documented\\npublic @interface Decrypt {\\n // 可以添加额外配置参数,例如是否检查时间戳、是否验证签名等\\n boolean checkTimestamp() default true;\\n boolean verifySignature() default false;\\n}\\n
\\n最后,我们还需要在 Spring Boot 配置中注册这个拦截器:
\\npackage com.example.secureapi.config;\\n\\nimport com.example.secureapi.interceptor.DecryptInterceptor;\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\\n\\n@Configuration\\npublic class WebMvcConfig implements WebMvcConfigurer {\\n\\n @Autowired\\n private DecryptInterceptor decryptInterceptor;\\n\\n @Override\\n public void addInterceptors(InterceptorRegistry registry) {\\n // 添加解密拦截器,应用到所有API请求路径\\n registry.addInterceptor(decryptInterceptor)\\n .addPathPatterns(\\"/api/**\\");\\n }\\n}\\n
\\n这篇文章详细介绍了如何在 SpringBoot 应用中实现 RSA+AES 混合加密方案,为接口通信提供安全保障。文章首先解释了接口加密的必要性,指出未加密的网络数据容易被抓包工具获取,特别是当传输敏感信息时风险更大。
\\n\\n","description":"引言 在现代应用开发中,接口安全性变得越来越重要。当敏感数据通过网络传输时,如何确保数据不被窃取或篡改?本文将详细介绍如何在 SpringBoot 应用中实现 RSA+AES 混合加密方案,为接口通信提供强大的安全保障。\\n\\n为什么需要接口加密?\\n\\n在没有加密的情况下,通过网络传输的数据可以被抓包工具轻松获取。特别是当传输包含用户隐私、支付信息等敏感数据时,这种风险更加不可接受。接口加密能够确保即使数据被截获,攻击者也无法理解其中的内容。\\n\\nRSA+AES 混合加密方案的优势\\n\\n我们选择 RSA+AES 混合加密方案是因为它结合了两种算法的优点:\\n\\nRSA: 非对…","guid":"https://juejin.cn/post/7479766671504097295","author":"trymoLiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T07:34:43.708Z","media":null,"categories":["后端","Java","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"Spring Fu:让Spring Boot启动提速40%的黑科技","url":"https://juejin.cn/post/7479651468341182516","content":"公众号:trymoLiu\\n欢迎前来关注~
\\n
Spring Fu是Spring官方推出的实验性项目,通过代码配置DSL革新传统Spring Boot的配置方式。就像用乐高积木拼装应用,开发者可以用Java/Kotlin代码显式\\"组装\\"Spring组件,主要包含两大模块:
\\nSpring Fu架构示意图
\\n特性 | 传统Spring Boot | Spring Fu | 提升效果 |
---|---|---|---|
启动时间(空项目) | 2.3秒 | 1.4秒 | ↓40% |
内存占用 | 120MB | 85MB | ↓29% |
反射使用率 | 高 | 接近0 | 更适合云原生 |
配置方式 | 注解/XML | 代码DSL | 更直观 |
kotlin\\n// 应用入口:App.kt\\nimport org.springframework.fu.kofu.application\\nimport org.springframework.fu.kofu.web.webMvc\\n\\nval app = application {\\n webMvc {\\n port = 8080 // 设置服务端口\\n router { // 声明路由\\n GET(\\"/hello\\") { \\n ok().body(\\"你好,Spring Fu!\\") \\n }\\n }\\n }\\n}\\n\\nfun main() {\\n app.run() // 启动应用\\n}\\n
\\njava\\n// 应用入口:App.java\\nimport static org.springframework.fu.jafu.Jafu.*;\\n\\npublic class App {\\n public static void main(String[] args) {\\n application(app -> app\\n .enable(webMvc(web -> web\\n .port(8080)\\n .router(router -> router\\n .GET(\\"/hello\\", request -> ok().body(\\"你好!\\"))\\n )\\n ))\\n ).run(args);\\n }\\n}\\n
\\n安装要求:
\\n项目初始化:
\\nbash\\n# 克隆官方示例库\\ngit clone https://github.com/spring-projects-experimental/spring-fu-samples.git\\ncd spring-fu-samples/kofu-rest-service\\n
\\nbash\\n# 编译并运行\\n./gradlew bootRun\\n\\n# 打包原生镜像(需安装GraalVM)\\n./gradlew nativeCompile\\n
\\n\\n\\n某电商平台测试数据:在订单查询服务中使用Spring Fu后,QPS从1200提升到1800,GC次数减少70%7
\\n
通过代码显式装配组件的方式,不仅让配置更直观,还能结合IDE的智能提示实现\\"配置即文档\\"的效果。这种设计特别适合需要精细控制启动过程的云原生场景,是传统Spring Boot向云原生演进的重要探索方向。
","description":"基础知识点 Spring Fu是Spring官方推出的实验性项目,通过代码配置DSL革新传统Spring Boot的配置方式。就像用乐高积木拼装应用,开发者可以用Java/Kotlin代码显式\\"组装\\"Spring组件,主要包含两大模块:\\n\\nJaFu:面向Java开发者的配置语言\\nKoFu:专为Kotlin设计的配置语法\\n\\nSpring Fu架构示意图\\n\\n核心优势对比表\\n特性\\t传统Spring Boot\\tSpring Fu\\t提升效果启动时间(空项目)\\t2.3秒\\t1.4秒\\t↓40%\\n内存占用\\t120MB\\t8…","guid":"https://juejin.cn/post/7479651468341182516","author":"Y11_推特同名","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T05:49:22.548Z","media":null,"categories":["后端","面试","GitHub"],"attachments":null,"extra":null,"language":null},{"title":"NestJS + DrizzleORM:轻量级、高性能的完美搭配? 🚀","url":"https://juejin.cn/post/7479349953125826569","content":"NestJS是一个用于构建高效、可靠的服务器端应用程序的渐进式Node.js框架,而DrizzleORM是一个轻量级、类型安全的ORM(对象关系映射)工具。本文将探讨这两种技术如何完美地配合使用,以创建强大且可维护的后端应用程序。
\\n在Node.js生态系统中,开发者有多种ORM选择,其中最流行的包括TypeORM、Prisma和DrizzleORM。下面是这三种框架的详细比较:
\\n特性 | TypeORM | Prisma | DrizzleORM |
---|---|---|---|
成熟度 | 高,生态系统成熟 | 中等,快速发展中 | 较低,相对较新 |
性能 | 中等 | 良好 | 优秀,极其轻量级 |
类型安全 | 基本支持 | 优秀 | 优秀,完全类型安全 |
查询方式 | ORM优先,支持原生SQL | Prisma查询语言 | SQL优先 |
学习曲线 | 中等 | 陡峭 | 平缓 |
社区支持 | 广泛 | 活跃 | 发展中 |
与NestJS集成 | 官方支持 | 第三方集成 | 简单直接 |
数据库支持 | 多种数据库 | 多种数据库 | 多种数据库 |
启动时间 | 较慢 | 中等 | 快速 |
迁移工具 | 内置 | 强大 | 简单有效 |
优点 | 缺点 |
---|---|
成熟稳定,使用广泛 | 性能开销较大 |
与NestJS有官方集成 | 类型安全性不如新一代ORM工具 |
支持装饰器,类Java风格 | 文档有时不够清晰 |
丰富的关系映射功能 | \\"魔术行为\\"导致调试困难 |
完善的社区和资源 | 大型项目中性能可能成为瓶颈 |
优点 | 缺点 |
---|---|
出色的类型安全性 | 需要额外的Prisma CLI和运行时 |
强大的数据迁移工具 | 对于复杂查询,灵活性可能不足 |
直观的模式定义语言 | 学习曲线较陡峭 |
优秀的查询性能 | 与NestJS集成需要额外工作 |
Prisma Studio提供可视化工具 | 依赖Prisma生态系统 |
优点 | 缺点 |
---|---|
极其轻量级,性能开销最小 | 相对较新,生态系统仍在发展中 |
完全类型安全 | 文档可能不如成熟框架全面 |
SQL优先,查询直观透明 | 高级功能可能不如其他框架完善 |
没有\\"魔术\\"方法,行为可预测 | 市场占有率较低 |
启动时间快,无需额外代码生成 | 现成的集成示例相对较少 |
理由 | 说明 |
---|---|
类型安全与开发体验 | DrizzleORM与NestJS都高度重视TypeScript和类型安全,提供一流的开发体验 |
性能优势 | DrizzleORM的轻量级设计减少了性能开销,特别适合微服务架构 |
透明性和可控性 | SQL优先方法与NestJS的明确架构理念相契合,提供更好的控制 |
可测试性 | 两种技术都设计为易于测试,适合TDD团队 |
迁移友好 | 直观设计和最小化的魔术方法使从其他ORM迁移更加顺畅 |
适合现代应用需求 | 对于需要灵活性、性能和类型安全的现代应用,DrizzleORM是更贴合的选择 |
NestJS采用了Angular的许多设计理念,包括模块化架构、依赖注入和装饰器的广泛使用。这使得它成为构建企业级应用程序的理想选择,特别是对于喜欢TypeScript和强类型系统的开发人员。
\\nNestJS的主要优势包括:
\\nDrizzleORM是一个相对较新但快速发展的ORM工具,专为TypeScript设计。与其他ORM相比,DrizzleORM有几个显著特点:
\\n让我们来看看如何在NestJS应用程序中设置和使用DrizzleORM。我们将创建一个简单的用户管理系统,展示这种集成。
\\nnpm install drizzle-orm postgres @nestjs/config\\nnpm install -D drizzle-kit pg\\n
\\n首先,我们需要设置数据库连接。创建一个drizzle.config.ts
文件:
import type { Config } from \'drizzle-kit\';\\nimport * as dotenv from \'dotenv\';\\n\\ndotenv.config();\\n\\nexport default {\\n schema: \'./src/db/schema/*\',\\n out: \'./drizzle\',\\n driver: \'pg\',\\n dbCredentials: {\\n connectionString: process.env.DATABASE_URL,\\n },\\n} satisfies Config;\\n
\\n创建一个用户模式文件 src/db/schema/users.ts
:
import { pgTable, serial, text, varchar, timestamp } from \'drizzle-orm/pg-core\';\\n\\nexport const users = pgTable(\'users\', {\\n id: serial(\'id\').primaryKey(),\\n email: varchar(\'email\', { length: 255 }).notNull().unique(),\\n name: text(\'name\').notNull(),\\n password: text(\'password\').notNull(),\\n createdAt: timestamp(\'created_at\').defaultNow().notNull(),\\n updatedAt: timestamp(\'updated_at\').defaultNow().notNull(),\\n});\\n\\nexport type User = typeof users.$inferSelect;\\nexport type NewUser = typeof users.$inferInsert;\\n
\\n接下来,创建一个NestJS模块来处理DrizzleORM的初始化和注入:
\\n// src/drizzle/drizzle.module.ts\\nimport { Module, Global } from \'@nestjs/common\';\\nimport { ConfigService } from \'@nestjs/config\';\\nimport { drizzle } from \'drizzle-orm/postgres-js\';\\nimport postgres from \'postgres\';\\nimport { DrizzleService } from \'./drizzle.service\';\\n\\n@Global()\\n@Module({\\n providers: [\\n {\\n provide: \'DRIZZLE_ORM\',\\n inject: [ConfigService],\\n useFactory: (configService: ConfigService) => {\\n const connectionString = configService.get<string>(\'DATABASE_URL\');\\n const client = postgres(connectionString);\\n return drizzle(client);\\n },\\n },\\n DrizzleService,\\n ],\\n exports: [DrizzleService],\\n})\\nexport class DrizzleModule {}\\n
\\n现在创建一个服务来提供对DrizzleORM实例的访问:
\\n// src/drizzle/drizzle.service.ts\\nimport { Inject, Injectable } from \'@nestjs/common\';\\nimport { PostgresJsDatabase } from \'drizzle-orm/postgres-js\';\\n\\n@Injectable()\\nexport class DrizzleService {\\n constructor(\\n @Inject(\'DRIZZLE_ORM\')\\n private readonly db: PostgresJsDatabase,\\n ) {}\\n\\n getDB() {\\n return this.db;\\n }\\n}\\n
\\n接下来,创建一个用户服务来处理用户相关的操作:
\\n// src/users/users.service.ts\\nimport { Injectable } from \'@nestjs/common\';\\nimport { DrizzleService } from \'../drizzle/drizzle.service\';\\nimport { users, NewUser, User } from \'../db/schema/users\';\\nimport { eq } from \'drizzle-orm\';\\n\\n@Injectable()\\nexport class UsersService {\\n constructor(private drizzleService: DrizzleService) {}\\n\\n async findAll(): Promise<User[]> {\\n const db = this.drizzleService.getDB();\\n return db.select().from(users);\\n }\\n\\n async findOne(id: number): Promise<User | null> {\\n const db = this.drizzleService.getDB();\\n const result = await db.select().from(users).where(eq(users.id, id));\\n return result[0] || null;\\n }\\n\\n async create(userData: NewUser): Promise<User> {\\n const db = this.drizzleService.getDB();\\n const result = await db.insert(users).values(userData).returning();\\n return result[0];\\n }\\n\\n async update(id: number, userData: Partial<NewUser>): Promise<User | null> {\\n const db = this.drizzleService.getDB();\\n const result = await db\\n .update(users)\\n .set(userData)\\n .where(eq(users.id, id))\\n .returning();\\n return result[0] || null;\\n }\\n\\n async remove(id: number): Promise<void> {\\n const db = this.drizzleService.getDB();\\n await db.delete(users).where(eq(users.id, id));\\n }\\n}\\n
\\n最后,创建一个控制器来处理HTTP请求:
\\n// src/users/users.controller.ts\\nimport {\\n Controller,\\n Get,\\n Post,\\n Body,\\n Param,\\n Put,\\n Delete,\\n} from \'@nestjs/common\';\\nimport { UsersService } from \'./users.service\';\\nimport { NewUser } from \'../db/schema/users\';\\n\\n@Controller(\'users\')\\nexport class UsersController {\\n constructor(private readonly usersService: UsersService) {}\\n\\n @Get()\\n findAll() {\\n return this.usersService.findAll();\\n }\\n\\n @Get(\':id\')\\n findOne(@Param(\'id\') id: string) {\\n return this.usersService.findOne(+id);\\n }\\n\\n @Post()\\n create(@Body() createUserDto: NewUser) {\\n return this.usersService.create(createUserDto);\\n }\\n\\n @Put(\':id\')\\n update(@Param(\'id\') id: string, @Body() updateUserDto: Partial<NewUser>) {\\n return this.usersService.update(+id, updateUserDto);\\n }\\n\\n @Delete(\':id\')\\n remove(@Param(\'id\') id: string) {\\n return this.usersService.remove(+id);\\n }\\n}\\n
\\nDrizzleORM提供了许多高级特性,可以在NestJS应用程序中充分利用。
\\nDrizzleORM允许你轻松地处理表之间的关系。例如,如果我们有一个关联到用户的帖子表:
\\n// src/db/schema/posts.ts\\nimport { pgTable, serial, text, integer, timestamp } from \'drizzle-orm/pg-core\';\\nimport { users } from \'./users\';\\nimport { relations } from \'drizzle-orm\';\\n\\nexport const posts = pgTable(\'posts\', {\\n id: serial(\'id\').primaryKey(),\\n title: text(\'title\').notNull(),\\n content: text(\'content\').notNull(),\\n authorId: integer(\'author_id\')\\n .references(() => users.id)\\n .notNull(),\\n createdAt: timestamp(\'created_at\').defaultNow().notNull(),\\n updatedAt: timestamp(\'updated_at\').defaultNow().notNull(),\\n});\\n\\nexport const postsRelations = relations(posts, ({ one }) => ({\\n author: one(users, {\\n fields: [posts.authorId],\\n references: [users.id],\\n }),\\n}));\\n\\nexport const usersRelations = relations(users, ({ many }) => ({\\n posts: many(posts),\\n}));\\n\\nexport type Post = typeof posts.$inferSelect;\\nexport type NewPost = typeof posts.$inferInsert;\\n
\\nDrizzleORM支持事务,对于需要原子操作的场景非常有用:
\\n// 在服务中使用事务\\nasync transferFunds(fromId: number, toId: number, amount: number) {\\n const db = this.drizzleService.getDB();\\n \\n return await db.transaction(async (tx) => {\\n // 从一个账户扣款\\n await tx\\n .update(accounts)\\n .set({ balance: sql`balance - ${amount}` })\\n .where(eq(accounts.id, fromId));\\n \\n // 向另一个账户存款\\n await tx\\n .update(accounts)\\n .set({ balance: sql`balance + ${amount}` })\\n .where(eq(accounts.id, toId));\\n \\n // 记录交易\\n return await tx\\n .insert(transactions)\\n .values({ fromId, toId, amount })\\n .returning();\\n });\\n}\\n
\\nDrizzleORM与drizzle-kit
结合使用,提供了强大的迁移功能:
# 生成迁移\\nnpx drizzle-kit generate\\n\\n# 应用迁移\\nnpx drizzle-kit push\\n
\\nNestJS和DrizzleORM是构建现代后端应用程序的强大组合。NestJS提供了坚实的架构基础和丰富的功能集,而DrizzleORM通过其轻量级设计和类型安全的特性,补充了这一点。
\\n这种组合尤其适合那些重视代码质量、类型安全和开发体验的团队。通过遵循本文中的模式和实践,你可以创建出既可靠又易于维护的应用程序。
\\n无论你是构建微服务、API还是全栈应用,NestJS和DrizzleORM都可以为你的下一个项目提供坚实的基础。
","description":"NestJS与DrizzleORM:绝佳的搭配 NestJS是一个用于构建高效、可靠的服务器端应用程序的渐进式Node.js框架,而DrizzleORM是一个轻量级、类型安全的ORM(对象关系映射)工具。本文将探讨这两种技术如何完美地配合使用,以创建强大且可维护的后端应用程序。\\n\\nNode.js中主流ORM框架的比较\\n\\n在Node.js生态系统中,开发者有多种ORM选择,其中最流行的包括TypeORM、Prisma和DrizzleORM。下面是这三种框架的详细比较:\\n\\n各ORM框架优缺点对比…","guid":"https://juejin.cn/post/7479349953125826569","author":"妖孽白YoonA","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T02:10:19.695Z","media":null,"categories":["后端","NestJS","ORM"],"attachments":null,"extra":null,"language":null},{"title":"Java 高级面试题:Lock 到底比 synchronized 强在哪?","url":"https://juejin.cn/post/7479625267028344859","content":"\\n在 Java 领域混迹多年的老王,最近跳槽到了大厂,面对的是一场高难度的社招面试。
\\n面试官推了推眼镜,微微一笑:
\\n“老王,聊聊 Lock 接口吧,和 synchronized 比,它有什么优势?”
\\n老王心里一紧,手心微微冒汗。 这可是高频面试题,自己之前一直用 synchronized,Lock 倒是听说过,但没仔细研究……
\\n他清了清嗓子,脑子里迅速搜索相关知识。为了让自己的回答更有说服力,他决定讲一个自己的亲身经历。
\\n老王家开了一家超市,店里有个仓库,负责存放饮料、零食等热销商品。
\\n一天,老王雇了两个伙计小张和小李。
\\n为了避免 小张进货 和 小李卖货 同时操作,导致数据混乱,老王想了个办法:
\\n“仓库门上装一把锁!谁要进仓库,必须先拿到钥匙。”
\\n这样,无论是进货还是卖货,都必须先拿到钥匙,确保同一时间只有一个人操作仓库。
\\n这个钥匙就像 Java 里的 synchronized 关键字,保证了线程安全。
\\n起初,老王的锁是传统的老式门锁,只能一个人用,谁先抢到钥匙谁先用,直到操作完成后,才把钥匙还回去。这就像 synchronized 一样:
\\n\\n老王发现,synchronized 用起来很方便,但它有很多局限性,比如:
\\n老王是个喜欢折腾的人,于是他换了个 智能门锁,并且给伙计们讲解了新规则:
\\n在 Java 里,这把智能锁就对应着 Lock 接口。
\\n\\n1. 让锁更公平
\\nsynchronized 默认是非公平锁,新来的线程可能插队。
\\n而 Lock 可以手动设置 公平锁,保证先来的线程先获得锁:
\\n\\n2. 等待锁时可以响应中断
\\nsynchronized 一旦进入等待状态,不能被中断。
\\nLock 提供了 lockInterruptibly(),可以在等待锁的时候被中断:
\\n\\n如果线程 A 在等待锁的过程中,发现自己被中断了,就可以提前退出,而不是无休止等待。
\\n3. 尝试获取锁
\\n场景: 小李去拿钥匙,发现仓库门被锁住,他想:“算了,我不等了,先去干别的。”
\\n在 Java 里,Lock 提供 tryLock() 方法,让线程尝试获取锁,如果失败,就直接返回,不会阻塞。
\\n\\n还能指定超时时间,例如 等 5 秒,5 秒内还没获取到就放弃:
\\n\\n4. 灵活释放锁
\\nsynchronized 必须按照先加锁,后解锁的顺序,不能灵活释放。
\\n但 Lock可以在不同方法、不同范围内释放锁,这样可以有更灵活的加锁策略。
\\n面试官听完,满意地点点头:“老王,讲得不错。”
\\n老王心里一松,笑着说:“其实这就是我家的仓库管理经验,生活处处是技术啊。”
\\nJava 并发编程很重要,Lock 让我们在高并发场景下有更灵活的锁策略。如果你还在用 synchronized,不妨试试 Lock,它的强大一定会让你惊喜!
\\n如果你觉得这篇文章有帮助,记得点赞、分享、收藏!
\\n我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
","description":"在 Java 领域混迹多年的老王,最近跳槽到了大厂,面对的是一场高难度的社招面试。 面试官推了推眼镜,微微一笑:\\n\\n“老王,聊聊 Lock 接口吧,和 synchronized 比,它有什么优势?”\\n\\n老王心里一紧,手心微微冒汗。 这可是高频面试题,自己之前一直用 synchronized,Lock 倒是听说过,但没仔细研究……\\n\\n他清了清嗓子,脑子里迅速搜索相关知识。为了让自己的回答更有说服力,他决定讲一个自己的亲身经历。\\n\\n老王的店铺生意:并发中的锁\\n\\n老王家开了一家超市,店里有个仓库,负责存放饮料、零食等热销商品。\\n\\n一天,老王雇了两个伙计小张和小李。\\n\\n小…","guid":"https://juejin.cn/post/7479625267028344859","author":"软件求生","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T01:33:55.775Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0b79be0cd34c4fa282685222c463006b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=hwKMvU65KTnhplc2rfRDueSujqY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8173dfac64314341ac7ecefe29879333~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=ULCfXa4OE7RFSfd2JkVql8itI7g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/16d5df6da0804727971a0864e6979e49~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=ML1VUS2uN44u7h2Y0Nde6QQycRc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bce76f491d604dae898c89eed162cc35~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=YXtttsut0ny9iqkp7RAaMZyefXk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3c33d11158b045a8aec98eb2cffdd01d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=4i6oars8TgX0U6UgZckBjZ7rfxE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/24a3bee454c64a56b1c60049973f4aeb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=Pe4OO5j3W%2FZ6Ij4oKQOWPRigvkc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06a732bccc4f4d45aa6a455b9f09f147~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6L2v5Lu25rGC55Sf:q75.awebp?rk3s=f64ab15b&x-expires=1742175234&x-signature=8Wo7GM7HgUlVt6DEfjH0urRf5Zs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"Trae编程:用腾讯云 HAI 快速开发一个智能客服助手,白嫖党的福利到底有多香?","url":"https://juejin.cn/post/7479434217941270565","content":"🌟 嗨,我是LucianaiB!
\\n🌍 总有人间一两风,填我十万八千梦。
\\n🚀 路漫漫其修远兮,吾将上下而求索。
\\n在当今数字化时代,客服行业正经历着一场前所未有的“智能革命”。想象一下,当你深夜下单后,突然冒出一个疑问,却发现客服小哥早已下班,那种“求而不得”的焦虑感简直让人抓狂!而传统的客服模式,不仅人力成本高昂,还常常因为“人手不足”或“反应迟缓”而让客户望而却步。于是,我们决定用科技的力量,打造一个“永不打烊”的智能客服助手——AIHelper,让它成为企业和客户之间的超级纽带。
\\n腾讯云HAI平台的出现,就像是一场及时雨,为我们的想法插上了翅膀。它不仅提供了强大的算力支持,还预装了DeepSeek-R1系列模型,这些模型就像是不同“体型”的智慧大脑,从1.5B到32B,能满足各种复杂场景的需求。更让人兴奋的是,腾讯云还贴心地推出了1元体验1个月的福利,这简直是对开发者最大的诱惑!于是,我们决定利用腾讯云HAI的API接口和DeepSeek-R1模型,开发一个智能客服助手系统,让AI技术真正落地,为企业和客户带来实实在在的便利。
\\n在这个充满挑战和机遇的时代,AIHelper将成为我们的“秘密武器”,用智能和高效重新定义客服行业的未来。
\\n1. 技术选型与平台选择
\\n2. 系统架构设计
\\n3. 功能实现路径
\\n模型选择与测试:
\\n前端开发:
\\n后端开发:
\\n数据分析与优化:
\\n腾讯云HAI上线了CPU版1元限时体验活动,什么?1元体验1个月,对于我这样的白嫖党那必须尝试一下的。
\\n在这里可以最高1个月,我想尝试一下16核32GB的,于是选择了第二种。
\\n打开算力界面,我们可以看到我们购买的这个体验版。
\\n点击这个体验版,这里腾讯云提供了5种使用的方法,简直是小白学习的好办法,这里还可以看到算力的使用情况。
\\n本次的测试接口,在jupyter这里进行尝试,按照下图点击。
\\n我们输入以下命令,看回应效果。localhost改为公网地址,我们就可以在自己的电脑提供公网访问。(这里也进行了尝试,证明是可以的)
\\ncurl -X POST http://localhost:6399/api/chat \\\\\\n -H \\"Content-Type: application/json\\" \\\\\\n -d \'{\\n \\"model\\": \\"deepseek-r1:7b\\",\\n \\"messages\\": [\\n {\\"role\\": \\"user\\", \\"content\\": \\"你好!\\"},\\n {\\"role\\": \\"assistant\\", \\"content\\": \\"你好!有什么我可以帮助你的吗?\\"},\\n {\\"role\\": \\"user\\", \\"content\\": \\"推荐一些美食?\\"}\\n ],\\n \\"stream\\": false\\n }\'\\n# localhost改为公网地址即可\\n
\\n基于现有代码,现在我们需要整合腾讯云API,建议提前把已有功能告诉Trae,这样也能更好地理解代码背景。输入提示词示例,可结合个人实际情况调整:
\\n# 目的\\n用户需求是基于腾讯云 HAI 快速开发AIHelper - 智能客服助手系统\\n\\n# 使用方法\\n我用的是python,我要的是在运行后端的时候直接就运行前端,并给了我接口\\n\\n# 要求实现的功能有:\\n系统提供智能对话服务、用户管理、知识库管理和数据分析等核心功能。支持自然语言交互,能够准确理解客户需求并提供相应解答。具备知识库自动更新、多轮对话管理、情感分析、用户意图识别等AI能力。系统通过机器学习模型持续优化响应质量,支持多渠道接入,提供实时监控和数据分析功能。同时具备会话历史记录、用户画像分析、服务质量评估等管理功能,确保服务质量和用户体验。\\n\\n\\n# 腾讯云使用的AI接口\\n## 腾讯云API\\ncurl -X POST http://localhost:6399/api/chat \\\\\\n -H \\"Content-Type: application/json\\" \\\\\\n -d \'{\\n \\"model\\": \\"deepseek-r1:7b\\",\\n \\"messages\\": [\\n {\\"role\\": \\"user\\", \\"content\\": \\"你好!\\"},\\n {\\"role\\": \\"assistant\\", \\"content\\": \\"你好!有什么我可以帮助你的吗?\\"},\\n {\\"role\\": \\"user\\", \\"content\\": \\"推荐一些美食?\\"}\\n ],\\n \\"stream\\": false\\n }\'\\n
\\n我们要的效果如下,运行pycharm后就可以看到网址了,出乎意料的API文档都输出了:
\\n可以看到最后的前端效果:
\\n在完成整个项目的开发和测试后,我详细查看了运行过程中的各项资源使用情况,尤其是GPU的利用率。从监控数据来看,GPU的利用率表现非常出色,这表明腾讯云HAI平台提供的强大算力能够高效支持DeepSeek-R1模型的运行,满足了智能客服助手系统的需求。
\\n从上图可以看出,在测试期间,GPU的利用率始终保持在一个较高的水平,这说明系统在处理用户请求时能够充分利用硬件资源,确保了快速响应和高效处理。这种高效的资源利用不仅提升了用户体验,也验证了腾讯云HAI平台在实际应用中的强大性能。
\\n此外,通过本次项目开发,我总结了以下几点关键收获:
\\n通过本次项目,我深刻体会到腾讯云HAI平台在AI应用开发中的强大优势。借助其强大的算力和灵活的API接口,开发者可以快速实现从概念到产品的转变。未来,我将继续探索更多基于腾讯云HAI的创新应用,为企业和客户带来更多价值。
\\nproject_root/\\n├── app/ # 应用主目录\\n│ ├── __init__.py # 初始化文件\\n│ ├── api/ # API接口目录\\n│ │ ├── __init__.py\\n│ │ ├── chat.py # 聊天相关接口\\n│ │ └── user.py # 用户相关接口\\n│ │\\n│ ├── models/ # 数据模型\\n│ │ ├── __init__.py\\n│ │ ├── chat.py # 聊天模型\\n│ │ └── user.py # 用户模型\\n│ │\\n│ ├── services/ # 业务逻辑层\\n│ │ ├── __init__.py\\n│ │ ├── chat.py # 聊天业务逻辑\\n│ │ └── auth.py # 认证业务逻辑\\n│ │\\n│ ├── utils/ # 工具函数\\n│ │ ├── __init__.py\\n│ │ └── helpers.py # 通用辅助函数\\n│ │\\n│ └── config/ # 配置文件\\n│ ├── __init__.py\\n│ └── settings.py # 系统配置\\n│\\n├── tests/ # 测试目录\\n│ ├── __init__.py\\n│ ├── test_api/ # API测试\\n│ └── test_services/ # 服务测试\\n│\\n├── static/ # 静态文件\\n│ ├── css/\\n│ ├── js/\\n│ └── images/\\n│\\n├── templates/ # 模板文件\\n│ ├── base.html\\n│ ├── chat/\\n│ └── user/\\n│\\n├── logs/ # 日志文件夹\\n│ ├── app.log\\n│ └── error.log\\n│\\n├── requirements.txt # 项目依赖\\n├── .env # 环境变量\\n├── .gitignore # git忽略文件\\n├── README.md # 项目说明\\n└── run.py # 启动文件\\n
","description":"Trae编程:用腾讯云 HAI 快速开发一个智能客服助手,白嫖党的福利到底有多香? 声明:非广告,为用户体验,开发一个智能客服助手\\n声明:非广告,为用户体验,开发一个智能客服助手\\n声明:非广告,为用户体验,开发一个智能客服助手\\n\\n🌟 嗨,我是LucianaiB!\\n\\n🌍 总有人间一两风,填我十万八千梦。\\n\\n🚀 路漫漫其修远兮,吾将上下而求索。\\n\\n目录\\n效果展示\\n腾讯云HAI开发流程\\n2.1 需求分析(白嫖党的福利,那必须尝试)\\n2.2 整体思路\\n寻找腾讯云HAI以及API测试\\nTrae编程集成谷歌插件\\n总结\\n系统框架\\n1.效果展示\\n\\n2…","guid":"https://juejin.cn/post/7479434217941270565","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T16:58:56.912Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd9865d5ff7b4c35aa9f2c40417eb7c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=S%2BjnntE%2F%2Bh6yO5VJg01YtPgEjOI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbdbbec67987467f9a66e7a8a2f7cea8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=2%2Bk5rAwlA3CI8Tafu8IFP6fcbyo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a75787e22f204650aadaf0bbec61a9da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=oTLOy0GTljjdSARjFyTMQB5VvAQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7bb9d8bd163d47bb8b1a53c290629e03~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=S2FRbllmj7%2B%2FSwBmrJ92FAxUimE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed163ef31ada48fdab6362d60bb7f63e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=lv1QPnswh5m9M0oLQ3BmUhavIEw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c9b970466837488aab7acd6f7ee971b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=oqzN57StvwosIHiAGOcVUnx0Ghc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/75d90349a1684c2ebe10d57bee4bc994~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=ZCbwgPKCrro6zfzggZg8r2BQdFQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/040b0e4aa7e04748b67eae81a933ba80~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=StnQwsKxm9GxtW4%2Bl80W0ZXe3WU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9771205cd830498b9ac7e806d94a0a90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=xcBGHnI2ERyqB9l1wVz8S8r0asU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e4d0c67c0a77475b993af54732de07d8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=eHf%2Bd3GmAlyWHJn5nss1IX0K5hI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/79ac69b32ba34e8cb3763507bfb472c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=GRhYO185Gd%2BqIbFniexvZ9Pz0Aw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/27edbaf06ed14555912164ed9f7a5797~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=1n7hNY1xjOyUHWeUBeehPNtJH8M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec1e91de219a47b193bef4fe6c688017~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=W9jKRhCN8j5zaKAlbkrES%2B6jqms%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd9865d5ff7b4c35aa9f2c40417eb7c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=S%2BjnntE%2F%2Bh6yO5VJg01YtPgEjOI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbdbbec67987467f9a66e7a8a2f7cea8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=2%2Bk5rAwlA3CI8Tafu8IFP6fcbyo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1012aed92e04f1f9505d298d4caf8f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1742179148&x-signature=Bsm%2FI%2FFAY81Osw34dQPdX%2B0elSY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","AIGC"],"attachments":null,"extra":null,"language":null},{"title":"Spring Boot 的 20个实用技巧","url":"https://juejin.cn/post/7479452347714945087","content":"\\n\\n文章首发公众号『风象南』
\\n
在 Java 开发领域,Spring Boot 以简化配置、快速开发等特性,目前成为必不可少的开发框架。但在日常开发中,还有许多实用技巧能让我们更高效地使用 Spring Boot。今天分享工作中常用的 20 个实用技巧。
\\n在复杂项目中,配置项众多,分散在各处的配置不利于管理。这时,@ConfigurationProperties
注解就能派上用场。它能将多个相关配置映射到一个类中,使代码更简洁。
定义一个配置类:
\\nimport org.springframework.boot.context.properties.ConfigurationProperties;\\n\\n@ConfigurationProperties(prefix = \\"app\\")\\npublic class AppProperties {\\n private String name;\\n private int version;\\n\\n // getters and setters\\n}\\n
\\n配置文件中:
\\napp:\\n name: mySpringApp\\n version: 1\\n
\\n在其他组件中,通过@Autowired
注入AppProperties
,就可以方便地获取配置信息:
import org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.stereotype.Component;\\n\\n@Component\\npublic class MyComponent {\\n @Autowired\\n private AppProperties appProperties;\\n\\n public void doSomething() {\\n String appName = appProperties.getName();\\n int appVersion = appProperties.getVersion();\\n // 使用配置信息进行业务逻辑处理\\n }\\n}\\n
\\n每次启动 Spring Boot 应用,看到默认的启动 Banner 是不是觉得有点单调?其实,我们可以自定义这个 Banner,让启动界面充满个性。只需在src/main/resources
目录下创建一个banner.txt
文件,在里面写入你想要展示的内容,比如公司 logo、项目名称、版本号等。
例如:
\\n ____ _ _ _\\n| _ \\\\| | (_) | | |\\n| |_) | __ _ ___| |__ _ _ __ ___ __ _| |_\\n| _ < / _` |/ __| \'_ \\\\| | \'_ ` _ \\\\ / _` | __|\\n| |_) | (_| | (__| | | | | | | | | | (_| | |_\\n|____/ \\\\__,_|\\\\___|_| |_|_|_| |_| |_|\\\\__,_|\\\\__|\\n
\\n这样,下次启动应用时,就能看到自定义的 Banner 。
\\nSpring Boot 的自动配置功能十分强大,但有时我们并不需要加载所有的自动配置组件,这时候可以使用@SpringBootApplication
的exclude
属性来排除不需要的模块,从而加快启动速度,减少内存占用。
比如,若项目中不使用数据库相关的自动配置,可以这样写:
\\nimport org.springframework.boot.SpringApplication;\\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\\n\\n@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})\\npublic class MyApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(MyApplication.class, args);\\n }\\n}\\n
\\n通过排除DataSourceAutoConfiguration
,Spring Boot 在启动时就不会尝试加载数据库相关的配置和组件,启动过程更加轻量化。
当 Spring Boot 应用启动完成后,有时我们需要执行一些初始化任务,比如初始化数据库、加载默认数据等。这时,CommandLineRunner
接口就能派上用场。
创建一个实现CommandLineRunner
接口的组件:
import org.springframework.boot.CommandLineRunner;\\nimport org.springframework.stereotype.Component;\\n\\n@Component\\npublic class StartupRunner implements CommandLineRunner {\\n @Override\\n public void run(String... args) throws Exception {\\n System.out.println(\\"Application started, running initial tasks...\\");\\n // 在这里编写具体的初始化任务逻辑,比如数据库初始化操作\\n }\\n}\\n
\\nSpring Boot 在启动过程中,会检测到实现了CommandLineRunner
接口的组件,并在应用启动完成后,按顺序执行它们的run
方法。如果有多个CommandLineRunner
实现类,可以通过实现org.springframework.core.Ordered
接口或使用@Order
注解来指定执行顺序。
SpringApplicationBuilder
为我们提供了更多灵活的启动配置方式,通过链式调用,可以在代码层面方便地设置应用的各种属性。
例如,设置应用的运行环境为开发环境,并指定服务器端口为 8081:
\\nimport org.springframework.boot.SpringApplication;\\nimport org.springframework.boot.builder.SpringApplicationBuilder;\\n\\npublic class MyApplication {\\n public static void main(String[] args) {\\n new SpringApplicationBuilder(MyApplication.class)\\n .profiles(\\"dev\\")\\n .properties(\\"server.port=8081\\")\\n .run(args);\\n }\\n}\\n
\\n这种方式特别适合一些需要根据不同条件灵活配置启动参数的场景,相比在配置文件中设置,在代码中控制更加直观和便捷。
\\n在开发、测试、生产等不同环境中,应用的配置往往有所不同,比如数据库连接信息、日志级别等。Spring Boot 的@Profile
注解可以轻松实现不同环境配置的切换。
首先,定义不同环境的配置类:
\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.context.annotation.Profile;\\nimport javax.sql.DataSource;\\nimport org.apache.commons.dbcp2.BasicDataSource;\\n\\n@Configuration\\npublic class DataSourceConfig {\\n\\n @Bean\\n @Profile(\\"dev\\")\\n public DataSource devDataSource() {\\n BasicDataSource dataSource = new BasicDataSource();\\n dataSource.setUrl(\\"jdbc:mysql://localhost:3306/devdb\\");\\n dataSource.setUsername(\\"devuser\\");\\n dataSource.setPassword(\\"devpassword\\");\\n return dataSource;\\n }\\n\\n @Bean\\n @Profile(\\"prod\\")\\n public DataSource prodDataSource() {\\n BasicDataSource dataSource = new BasicDataSource();\\n dataSource.setUrl(\\"jdbc:mysql://localhost:3306/proddb\\");\\n dataSource.setUsername(\\"produser\\");\\n dataSource.setPassword(\\"prodpassword\\");\\n return dataSource;\\n }\\n}\\n
\\n然后,在application.yaml
中指定当前激活的环境:
spring:\\n profiles:\\n active: dev\\n
\\n这样,Spring Boot 会根据spring.profiles.active
的值,自动加载对应的环境配置类,方便我们在不同环境下快速切换配置。
有时,我们希望根据配置文件中的某个属性值来决定是否加载某个 Bean,@ConditionalOnProperty
注解就可以满足这个需求,实现按需加载 Bean。
例如,假设有一个功能开关featureX.enabled
,只有当该开关为true
时,才加载FeatureX
这个 Bean:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n@Configuration\\npublic class FeatureConfig {\\n\\n @Bean\\n @ConditionalOnProperty(name = \\"featureX.enabled\\", havingValue = \\"true\\")\\n public FeatureX featureX() {\\n return new FeatureX();\\n }\\n}\\n
\\n在application.properties
中配置:
featureX.enabled=true\\n
\\n当featureX.enabled
为true
时,Spring Boot 会创建FeatureX
的 Bean;若为false
,则不会创建,可以根据条件动态控制Bean的加载。
Spring Boot DevTools 是一个专门为开发过程提供便利的工具,它包含了代码热重载、缓存禁用等功能,能大大加快开发调试的速度。
\\n只需要在pom.xml
文件中引入 DevTools 依赖:
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-devtools</artifactId>\\n <optional>true</optional>\\n</dependency>\\n
\\n引入后,当我们修改代码保存时,应用会自动重启,无需手动重启,节省了大量开发时间。
\\nSpring Boot Actuator 是一个强大的监控和管理工具,通过它,我们可以轻松了解应用的运行状态、性能指标等信息。
\\n首先,在pom.xml
中引入 Actuator 依赖:
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-actuator</artifactId>\\n</dependency>\\n\\n
\\n引入后,应用会自动暴露一些内置的端点,比如:
\\n/health
:用于查看应用的健康状况,返回UP
表示应用正常运行。
/metrics
:可以获取应用的各种指标数据,如内存使用情况、HTTP 请求数、CPU 使用率等。
/info
:可以展示应用的一些自定义信息,比如版本号、构建时间等,需要在application.yaml
中配置相关信息:
info:\\n app:\\n name: MySpringApp\\n version: 1.0.0\\n build:\\n time: 2024-10-01T12:00:00Z\\n
\\n通过这些端点,我们能更好地监控和管理 Spring Boot 应用,及时发现和解决潜在问题。
\\n在接收用户输入或处理业务数据时,数据校验不可或缺。Spring Boot 整合了 Java Validation API,借助@Validated
注解,我们能轻松实现数据校验功能。通过在方法参数前添加@Validated
,并结合各种校验注解(如@NotNull
、@Size
、@Pattern
等),Spring Boot 会自动对输入数据进行校验,校验不通过时会抛出异常,便于我们统一处理。
比如,我们有一个用户注册的 DTO 类:
\\nimport javax.validation.constraints.NotBlank;\\nimport javax.validation.constraints.Pattern;\\n\\npublic class UserRegistrationDTO {\\n @NotBlank(message = \\"Username cannot be blank\\")\\n private String username;\\n\\n @Pattern(regexp = \\"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\\\\\\.[A-Za-z]{2,}$\\", message = \\"Invalid email format\\")\\n private String email;\\n\\n // getters and setters\\n}\\n
\\n在控制器方法中使用@Validated
进行校验:
import org.springframework.validation.annotation.Validated;\\nimport org.springframework.web.bind.annotation.PostMapping;\\nimport org.springframework.web.bind.annotation.RequestBody;\\nimport org.springframework.web.bind.annotation.RestController;\\n\\n@RestController\\npublic class UserController {\\n\\n @PostMapping(\\"/register\\")\\n public String registerUser(@Validated @RequestBody UserRegistrationDTO userDTO) {\\n // 业务逻辑,处理注册\\n return \\"User registered successfully\\";\\n }\\n}\\n
\\n在 Spring Boot 应用中,统一处理异常是非常重要的,它可以提高应用的健壮性和用户体验。我们可以通过创建一个全局异常处理器来捕获并处理应用中抛出的各种异常。
\\n创建一个全局异常处理类,使用@ControllerAdvice
注解:
import org.springframework.http.HttpStatus;\\nimport org.springframework.http.ResponseEntity;\\nimport org.springframework.web.bind.annotation.ControllerAdvice;\\nimport org.springframework.web.bind.annotation.ExceptionHandler;\\n\\n@ControllerAdvice\\npublic class GlobalExceptionHandler {\\n\\n @ExceptionHandler(Exception.class)\\n public ResponseEntity<String> handleGeneralException(Exception ex) {\\n return new ResponseEntity<>(\\"An error occurred: \\" + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);\\n }\\n\\n @ExceptionHandler(NullPointerException.class)\\n public ResponseEntity<String> handleNullPointerException(NullPointerException ex) {\\n return new ResponseEntity<>(\\"A null pointer exception occurred: \\" + ex.getMessage(), HttpStatus.BAD_REQUEST);\\n }\\n // 可以继续添加其他类型异常的处理方法\\n}\\n
\\n通过这种方式,当应用中抛出异常时,会被全局异常处理器捕获,并根据异常类型返回相应的 HTTP 状态码和错误信息,使前端能更好地处理异常情况,同时也方便开发人员定位问题。
\\nAOP(面向切面编程)在 Spring Boot 中是一个非常强大的功能,我们可以利用它来进行日志记录、性能监控等横切关注点的处理,避免在业务代码中大量重复编写相关逻辑。
\\n首先,在pom.xml
中引入 AOP 依赖:
<dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-aop</artifactId>\\n</dependency>\\n
\\n然后,创建一个切面类:
\\nimport org.aspectj.lang.ProceedingJoinPoint;\\nimport org.aspectj.lang.annotation.Around;\\nimport org.aspectj.lang.annotation.Aspect;\\nimport org.slf4j.Logger;\\nimport org.slf4j.LoggerFactory;\\nimport org.springframework.stereotype.Component;\\n\\n@Aspect\\n@Component\\npublic class LoggingAndPerformanceAspect {\\n private static final Logger logger = LoggerFactory.getLogger(LoggingAndPerformanceAspect.class);\\n\\n @Around(\\"@annotation(org.springframework.web.bind.annotation.RequestMapping)\\")\\n public Object logAndMeasurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {\\n long startTime = System.currentTimeMillis();\\n logger.info(\\"Start executing method: {}\\", joinPoint.getSignature().getName());\\n try {\\n return joinPoint.proceed();\\n } finally {\\n long endTime = System.currentTimeMillis();\\n logger.info(\\"Method {} executed in {} ms\\", joinPoint.getSignature().getName(), endTime - startTime);\\n }\\n }\\n}\\n
\\n上述切面类通过@Around
注解,对所有被@RequestMapping
注解标记的方法进行环绕增强,在方法执行前后记录日志,并统计方法执行的时间,方便我们对应用的性能进行监控和分析,同时也能更好地了解方法的调用情况。
Spring Boot 默认使用嵌入式 Servlet 容器(如 Tomcat)来运行应用,我们可以通过配置文件或编程方式对其进行自定义配置,以优化性能或满足特定需求。
\\n在application.yaml
中配置 Tomcat 的最大线程数和连接数:
server:\\n tomcat:\\n max-threads: 200\\n max-connections: 1000\\n
\\n如果需要更复杂的配置,也可以通过编程方式来实现
\\nimport org.apache.catalina.connector.Connector;\\nimport org.apache.coyote.http11.Http11NioProtocol;\\nimport org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n@Configuration\\npublic class TomcatConfig {\\n\\n @Bean\\n public TomcatServletWebServerFactory tomcatServletWebServerFactory() {\\n TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();\\n Connector connector = new Connector(Http11NioProtocol.class.getName());\\n connector.setPort(8080);\\n Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();\\n protocol.setMaxThreads(200);\\n protocol.setMaxConnections(1000);\\n factory.addAdditionalTomcatConnectors(connector);\\n return factory;\\n }\\n}\\n
\\n通过这种方式,可以根据项目的实际情况,灵活调整 Servlet 容器的参数。
\\n在 Spring Boot 应用中,合理使用缓存可以显著提升应用的性能,减少数据库查询次数,提高响应速度。Spring Boot 提供了强大的缓存支持,通过@EnableCaching
注解开启缓存功能,并使用@Cacheable
等注解来标记需要缓存的方法。
首先,在启动类或配置类上添加@EnableCaching
注解:
import org.springframework.boot.SpringApplication;\\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\\nimport org.springframework.cache.annotation.EnableCaching;\\n\\n@SpringBootApplication\\n@EnableCaching\\npublic class MyApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(MyApplication.class, args);\\n }\\n}\\n
\\n然后,在需要缓存结果的方法上使用@Cacheable
注解:
import org.springframework.cache.annotation.Cacheable;\\nimport org.springframework.stereotype.Service;\\n\\n@Service\\npublic class UserService {\\n\\n @Cacheable(value = \\"users\\", key = \\"#id\\")\\n public User getUserById(Long id) {\\n // 这里执行查询数据库等操作获取用户信息\\n User user = new User();\\n user.setId(id);\\n user.setName(\\"John Doe\\");\\n return user;\\n }\\n}\\n
\\n上述代码中,@Cacheable
注解表示当getUserById
方法被调用时,如果缓存中已经存在对应id
的用户信息,则直接从缓存中返回,不再执行方法内部的数据库查询操作。value
属性指定缓存的名称,key
属性指定缓存的键。
在 Spring Boot 应用中,有些任务可能比较耗时,如果在主线程中执行,会影响应用的响应速度。通过@Async
注解,我们可以将这些任务异步执行,使主线程能够迅速返回,提升用户体验。
首先,在启动类或配置类上添加@EnableAsync
注解,开启异步任务支持:
import org.springframework.boot.SpringApplication;\\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\\nimport org.springframework.scheduling.annotation.EnableAsync;\\n\\n@SpringBootApplication\\n@EnableAsync\\npublic class MyApplication {\\n public static void main(String[] args) {\\n SpringApplication.run(MyApplication.class, args);\\n }\\n}\\n
\\n然后,在需要异步执行的方法上使用@Async
注解:
import org.springframework.scheduling.annotation.Async;\\nimport org.springframework.stereotype.Service;\\n\\n@Service\\npublic class TaskService {\\n\\n @Async\\n public void processLongTask() {\\n // 模拟一个耗时任务,比如复杂的数据处理、远程调用等\\n try {\\n Thread.sleep(5000);\\n System.out.println(\\"Long task completed.\\");\\n } catch (InterruptedException e) {\\n e.printStackTrace();\\n }\\n }\\n}\\n
\\n当调用processLongTask
方法时,它会在一个新的线程中执行,不会阻塞主线程,应用可以继续处理其他请求。
在生产环境中,我们常常需要在不重新打包应用的情况下修改配置。Spring Boot 支持将配置文件外部化,这样可以方便地在不同环境中调整配置。常见的方式是将配置文件放置在应用运行目录的config
文件夹下,或者通过命令行参数指定配置文件路径。
假设我们有一个application.yaml
文件,内容如下:
app:\\n message: Hello, World!\\n
\\n在应用启动时,可以通过以下命令指定外部配置文件路径:
\\njava -jar your-application.jar --spring.config.location=file:/path/to/your/config/\\n
\\n这样,即使应用已经打包成jar
文件,也能轻松修改配置,无需重新构建和部署应用。另外,还可以使用 Spring Cloud Config 实现集中化的配置管理,在分布式系统中更方便地管理各个服务的配置。
在某些业务场景下,一个应用可能需要连接多个数据源,根据不同的业务需求动态切换数据源。Spring Boot 提供了灵活的机制来实现这一点。首先,配置多个数据源:
\\nimport org.springframework.beans.factory.annotation.Qualifier;\\nimport org.springframework.boot.context.properties.ConfigurationProperties;\\nimport org.springframework.boot.jdbc.DataSourceBuilder;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.jdbc.core.JdbcTemplate;\\n\\nimport javax.sql.DataSource;\\n\\n@Configuration\\npublic class DataSourceConfig {\\n\\n @Bean\\n @ConfigurationProperties(prefix = \\"spring.datasource.first\\")\\n public DataSource firstDataSource() {\\n return DataSourceBuilder.create().build();\\n }\\n\\n @Bean\\n @ConfigurationProperties(prefix = \\"spring.datasource.second\\")\\n public DataSource secondDataSource() {\\n return DataSourceBuilder.create().build();\\n }\\n\\n @Bean\\n public JdbcTemplate firstJdbcTemplate(@Qualifier(\\"firstDataSource\\") DataSource dataSource) {\\n return new JdbcTemplate(dataSource);\\n }\\n\\n @Bean\\n public JdbcTemplate secondJdbcTemplate(@Qualifier(\\"secondDataSource\\") DataSource dataSource) {\\n return new JdbcTemplate(dataSource);\\n }\\n}\\n
\\n然后,通过 AOP(面向切面编程)实现动态数据源切换。创建一个切面类,根据方法上的自定义注解决定使用哪个数据源:
\\nimport org.aspectj.lang.ProceedingJoinPoint;\\nimport org.aspectj.lang.annotation.Around;\\nimport org.aspectj.lang.annotation.Aspect;\\nimport org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;\\nimport org.springframework.stereotype.Component;\\n\\n@Aspect\\n@Component\\npublic class DataSourceAspect {\\n\\n @Around(\\"@annotation(com.example.DataSourceAnnotation)\\")\\n public Object switchDataSource(ProceedingJoinPoint joinPoint) throws Throwable {\\n DataSourceAnnotation annotation = joinPoint.getSignature().getDeclaringType().getAnnotation(DataSourceAnnotation.class);\\n if (annotation != null) {\\n String dataSourceName = annotation.value();\\n AbstractRoutingDataSource dataSource = (AbstractRoutingDataSource) dataSourceResolver.resolveDataSource();\\n dataSource.setCurrentLookupKey(dataSourceName);\\n }\\n try {\\n return joinPoint.proceed();\\n } finally {\\n // 清除数据源标识,恢复默认数据源\\n AbstractRoutingDataSource dataSource = (AbstractRoutingDataSource) dataSourceResolver.resolveDataSource();\\n dataSource.setCurrentLookupKey(null);\\n }\\n }\\n}\\n
\\n通过这种方式,应用可以在运行时根据业务需求灵活切换数据源,满足复杂业务场景下的数据访问需求。
\\n在编写单元测试和集成测试时,模拟真实的数据库、消息队列等环境是很有必要的。Testcontainers 是一个开源库,它允许我们在测试中轻松创建和管理容器化的测试环境,如 MySQL、Redis、Kafka 等。
\\n以测试一个使用 MySQL 数据库的 Spring Boot 应用为例,先在pom.xml
中添加 Testcontainers 和相关数据库驱动依赖:
<dependency>\\n <groupId>org.testcontainers</groupId>\\n <artifactId>testcontainers</artifactId>\\n <scope>test</scope>\\n</dependency>\\n<dependency>\\n <groupId>org.testcontainers</groupId>\\n <artifactId>mysql</artifactId>\\n <scope>test</scope>\\n</dependency>\\n
\\n然后编写测试类:
\\nimport org.junit.jupiter.api.Test;\\nimport org.springframework.beans.factory.annotation.Autowired;\\nimport org.springframework.boot.test.context.SpringBootTest;\\nimport org.springframework.jdbc.core.JdbcTemplate;\\nimport org.testcontainers.containers.MySQLContainer;\\nimport org.testcontainers.junit.jupiter.Container;\\nimport org.testcontainers.junit.jupiter.Testcontainers;\\n\\nimport static org.junit.jupiter.api.Assertions.assertEquals;\\n\\n@Testcontainers\\n@SpringBootTest\\npublic class DatabaseTest {\\n\\n @Container\\n public static MySQLContainer<?> mysql = new MySQLContainer<>(\\"mysql:8.0.26\\")\\n .withDatabaseName(\\"testdb\\")\\n .withUsername(\\"testuser\\")\\n .withPassword(\\"testpassword\\");\\n\\n @Autowired\\n private JdbcTemplate jdbcTemplate;\\n\\n @Test\\n public void testDatabaseInsert() {\\n jdbcTemplate.execute(\\"INSERT INTO users (name, age) VALUES (\'John\', 30)\\");\\n int count = jdbcTemplate.queryForObject(\\"SELECT COUNT(*) FROM users\\", Integer.class);\\n assertEquals(1, count);\\n }\\n}\\n
\\n上述测试类中,MySQLContainer
会在测试启动时创建一个 MySQL 容器实例,并且自动配置好数据源连接信息供 Spring Boot 应用使用。测试完成后,容器会自动销毁,保证每次测试环境的一致性和独立性,极大提升了测试的可靠性和可重复性。
Spring Boot 默认使用 Jackson 库来处理 JSON 数据的序列化和反序列化。在实际开发中,我们可能需要根据业务需求定制 Jackson 的行为,比如修改日期格式、忽略某些属性等。
\\n要定制日期格式,可以创建一个Jackson2ObjectMapperBuilderCustomizer
的 Bean:
import com.fasterxml.jackson.databind.ObjectMapper;\\nimport com.fasterxml.jackson.databind.SerializationFeature;\\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\\nimport com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\nimport org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;\\n\\nimport java.time.LocalDateTime;\\nimport java.time.format.DateTimeFormatter;\\n\\n@Configuration\\npublic class JacksonConfig {\\n\\n @Bean\\n public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {\\n return builder -> {\\n JavaTimeModule module = new JavaTimeModule();\\n module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(\\"yyyy-MM-dd HH:mm:ss\\")));\\n builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).modules(module);\\n };\\n }\\n}\\n
\\n上述代码中,我们创建了一个JavaTimeModule
,并为LocalDateTime
类型定制了序列化格式,然后将其添加到Jackson2ObjectMapperBuilder
中。这样,在将LocalDateTime
类型的数据序列化为 JSON 时,就会按照指定的格式输出。此外,还可以通过@JsonIgnore
注解忽略某些属性,通过@JsonProperty
注解重命名属性等,灵活定制 JSON 数据的处理方式,满足各种复杂的业务需求。
在 Spring Boot 应用里,我们常常会遇到定时任务的需求,像是定时清理过期数据、定时发送提醒邮件等。Spring Boot 借助@Scheduled
注解,能轻松实现任务调度功能。只要在方法上添加该注解,并设置好调度规则,Spring Boot 就会按设定的时间间隔或具体时间点执行任务。
例如,我们要实现一个每天凌晨 1 点执行的任务:
\\nimport org.springframework.scheduling.annotation.Scheduled;\\nimport org.springframework.stereotype.Component;\\n\\n@Component\\npublic class ScheduledTask {\\n\\n @Scheduled(cron = \\"0 0 1 * * ?\\")\\n public void cleanExpiredData() {\\n // 执行清理过期数据的业务逻辑\\n System.out.println(\\"Executing clean expired data task at \\" + System.currentTimeMillis());\\n }\\n}\\n
\\n上述代码里,cron
表达式\\"0 0 1 * * ?\\"
代表每天凌晨 1 点触发任务。当然,@Scheduled
注解还支持fixedRate
、fixedDelay
等属性,能满足不同场景下的任务调度需求。
前段时间有人推荐我使用 PyStand 打包 ,但是找了一圈,发现这方面的资料不多 ,没有一个靠谱的入门文档。
\\n所以结合自己的使用,简单整了一个流程出来 ,便于后面使用的时候回看。
\\nPyStand 是一个用于将 Python 应用程序打包为独立可执行文件的工具,旨在简化 Python 应用程序的发布过程。
\\nPyStand 的目标是更加轻量化
和易用
,同时提供高度自定义的配置选项。
多的不说了 ,直接看流程 : 👇 👇👇
\\n\\n\\n前置知识点 :
\\n
// 步骤一 : 需要准备好 PyStand 的相关文件 (这里可以直接去 Git 上面拉 Release 包)\\n// --- 详情见 3.1\\n// --- 下载好了之后 ,就是你的软件执行目录了\\n\\n\\n// 步骤二 : 准备好 Python Embeddable 包 (也是去官方下载即可)\\n// --- 详情见 3.1 \\n// --- 放入 Runtime , 如果上面下载的是聚合版 ,这里不需要处理\\n\\n\\n// 步骤三 : 准备好 Site Package 的依赖包\\n- 如果是类似于 anaconda 的运行环境 ,可以直接去环境里面抽包\\n\\n\\n\\n\\n
\\n\\n\\nPyStand 的相关文件
\\n
\\n\\n下载 Python Embeddable 包
\\n
自行去官网下载对应的版本 @ www.python.org/downloads/r…
\\n\\n\\n抽取 Site Package 依赖包
\\n
整个过程中主要涉及到这些步骤 :
\\n// S1 : 首先准备好项目执行文件\\n这里可以把整个 Python 项目文件拖进去 ,其中要有一个 main.py ,作为项目的启动文件\\n\\n\\n// S2 : 修改 PyStand.int\\n把 main.py 的整个内容复杂到 PyStand.int 项目里面\\n\\n\\n// S3 : 双击 PyStand.exe 运行\\n\\n\\n// S4 : 打包成 zip/rar 压缩文件 ,发布就行\\n
\\n哈哈 ,是不是特别简单,如果项目简单的话 ,这里就一步到位了!!
\\n\\n\\n如果包的依赖不对 ,会直接闪退
\\n
注意 ,如果闪退的话 ,就从命令行里面运行 exe ,就可以看到执行失败的问题了
\\n\\n\\nPyStand.exe 需要和 PyStand.int 对应
\\n
这里注意一下 ,.int 查找是按照执行名称来的 ,也就是main.int 时 ,要把 exe 改为 main.exe
\\n\\n\\n包文件还是太大了
\\n
\\n\\n代码是明文的
\\n
按照上面的流程不难发现 ,代码是明文 .py 文件
放在项目里面的 ,这里可就不是 .pyc 这里执行文件了。
所以对于代码敏感的情况下 ,需要对代码进行加密处理。
\\n这里比较简单的方式就是进行代码混淆
,方案很多 ,我这里就不深入了
整个项目打包里面 ,最复杂的一点就是精简 Python 依赖包的大小 ,给大家看一个大项目 :
\\n\\n\\n对 site-packages 进行修剪
\\n
像这样 ,直接把里面用不到的依赖删除就行 ,只要项目能运行 ,就没问题.
\\n这篇文章主要针对新手开荒的时候 ,如何快速打包发布一个项目 , 后面用到了会把一些复杂的功能也记录过来。
\\n但是对于这个工具 ,上手容易 ,想精化同样不简单。需要对 Python 的包有足够的了解,才方便精简包减少大小。
\\n最近在做项目的时候,遇到了一个Long类型传给前端之后,接收到的数值不对的情况,我一开始以为是前端接收数据有问题,后来经过查找,了解到这是一个Long类型精度丢失问题,于是写这篇文章记录下来,也探究一下为什么会发生这种情况。
\\n在SpringBoot将数据传给前端前,会默认使用Jackson序列化Java对象为JSON,Long类型默认将其转换成Number类型,到Long类型的数值超过Javas安全范围的数值时就会发生精度丢失,
\\n在你需要处理的字段添加注解。\\n实现如下面代码所示:
\\n @Data\\n public class Book {\\n @JsonSerialize(using = ToStringSerializer.class)\\n private Long id; // 主键ID\\n private String name;\\n }\\n
\\n将全部Long类型统一处理。实现如下面代码所示:
\\n@Configuration\\npublic class JacksonConfig {\\n\\n @Bean\\n @Primary\\n @ConditionalOnMissingBean(ObjectMapper.class)\\n public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {\\n ObjectMapper objectMapper = builder.createXmlMapper(false).build();\\n SimpleModule simpleModule = new SimpleModule();\\n // 将Long类型序列化为String类型\\n simpleModule.addSerializer(Long.class, ToStringSerializer.instance);\\n objectMapper.registerModule(simpleModule);\\n return objectMapper;\\n }\\n}\\n
\\n上面两种方案都是将Long类型转换成String类型,前端使用String类型来保存保持精度不发生丢失问题。开始我遇到这个问题的时候,我直接认为是前端代码写的有问题,所有接到的数据才会出现问题,因为我knife4j响应的数据没有问题,结果前端自己搞来搞去了很久,找了其他人,之后才有人说是这个问题,当时的自己实际上是第一次前端后端一起合作开发一个小项目,给我的感受就是有许许多多的问题是只有在实践中才会发现的,而不是坐在大学的课堂中去学,而应该自己主动的去进行技术学习,然后将学的知识运用在开发中,许许多多的问题是只有自己遇到了,才会记忆犹新,并且通过自己解决,下次遇到就有应对之法。
","description":"前言 最近在做项目的时候,遇到了一个Long类型传给前端之后,接收到的数值不对的情况,我一开始以为是前端接收数据有问题,后来经过查找,了解到这是一个Long类型精度丢失问题,于是写这篇文章记录下来,也探究一下为什么会发生这种情况。\\n\\n背景\\n后端:Java、SpringBoot......\\n前端:JavaScript、Vue......\\n问题展示\\n接口展示\\n\\n数据响应展示:\\n\\n浏览器预览展示:\\n\\n问题原因\\nJava中,Long类型为64位有符合整数,取值范围是-2^63到2^63-1。\\nJavaScript中,所有数字均基于IEEE…","guid":"https://juejin.cn/post/7479296373331214376","author":"镜花水月linyi","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T09:05:46.058Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0881deae22094919b0fd43329e229f8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZWc6Iqx5rC05pyIbGlueWk=:q75.awebp?rk3s=f64ab15b&x-expires=1742115946&x-signature=%2B0W3pap4H7l%2Bm%2BkGLA4zAC3EFv0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5d286ffabf4943e98b070a0807debef1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZWc6Iqx5rC05pyIbGlueWk=:q75.awebp?rk3s=f64ab15b&x-expires=1742115946&x-signature=ienQNc2lQhSDCavV2CtWD0Q3z1o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4669d24be5394391891a9af0184ed262~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZWc6Iqx5rC05pyIbGlueWk=:q75.awebp?rk3s=f64ab15b&x-expires=1742115946&x-signature=HSIlnOBNxTupYPyD2vBXV%2BLlq8s%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"Idea插件开发之YamlHelper","url":"https://juejin.cn/post/7479225933061734426","content":"天下苦yaml
久矣。
如果是篇幅小还好,可是如果篇幅很大,再加上自定义的属性很多,那yaml
用起来就是噩梦。
曾经遇到一个项目,根据环境不同有五六个yaml
配置文件,每个文件将近六百行,一个属性下面又有很多属性,拖拖拉拉搞了很长一串,那改起来真是让人恶心。
单纯靠缩进能把人看的眼睛都瞎了,遇到过好几次,加配置因为缩进问题加错了,配置读取不了。
\\n所以它来了,伴随着千呼万唤,它终于来了。
\\n这是我开发的第三个插件,已经发布到插件市场啦。
\\nJet Brains
:plugins.jetbrains.com/plugin/2676…
GitHub
:github.com/LerDer/Yaml…
markdown
大家肯定都用过,这个插件借鉴了markdown
那种展示方式,采用左右分屏的展示。右上角支持切换视图,和markdown
一样,可以选择只展示文本或者预览。
在树视图下支持一些简单的功能
\\n下面用gif图片展示一下定位功能
\\n支持配置备注文件,配置文件要yaml
格式
示例:keys_mark.yaml
\\nname: 名称\\nserviceCode: 服务编码\\nserviceScene: 服务场景\\ntranCode: 交易码\\nmock: 是否mock\\nactive: 环境\\nurl: 地址\\nusername: 用户名\\npassword: 密码\\ndriverClassName: 驱动\\ndb-type: 数据库类型\\ncache: 是否缓存\\nprefix: 前缀\\nstatic-locations: 静态资源\\nport: 端口\\nsensitive-column: 敏感字段\\ndb-name: 数据库名\\nqueryConfig: 查询字段\\nmapper-locations: mapper路径\\nmap-underscore-to-camel-case: 下划线转驼峰\\nencodeKey: 加密key\\nmocks: 是否mock\\nelement: 元素\\nprofiles: 配置\\n
\\n这个时候,树结构就有了备注
\\n欢迎大家去点点 Star
,感谢大家的支持。
如果有好的建议或者想法可以提交issue
最后欢迎大家关注我的公众号,共同学习,一起进步。加油🤣
\\n南诏Blog
\\n\\n","description":"天下苦yaml久矣。 如果是篇幅小还好,可是如果篇幅很大,再加上自定义的属性很多,那yaml用起来就是噩梦。\\n\\n曾经遇到一个项目,根据环境不同有五六个yaml配置文件,每个文件将近六百行,一个属性下面又有很多属性,拖拖拉拉搞了很长一串,那改起来真是让人恶心。\\n\\n单纯靠缩进能把人看的眼睛都瞎了,遇到过好几次,加配置因为缩进问题加错了,配置读取不了。\\n\\n所以它来了,伴随着千呼万唤,它终于来了。\\n\\n这是我开发的第三个插件,已经发布到插件市场啦。\\n\\nJet Brains:plugins.jetbrains.com/plugin/2676…\\n\\nGitHub:github…","guid":"https://juejin.cn/post/7479225933061734426","author":"白起那么早","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T07:46:25.368Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f23874bcf98a47289be6d4ef175580e2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=4VghAyADccde50pElwznJLus%2BTE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/36fb07c00e524dc1aa156af51484ceb6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=gT4UnIV6QzLZhevI0%2BYbvcCzS3w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8daf0802450f41ab8d78c6fbeaba2b7f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=WanvKo0FoUXOlar80HAortZzMng%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/59a30bf3dbe04ec485d257916f937b57~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=wviihe450iktxKfZw%2FqBe75DP%2FA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9316cb9062174c90a01bf39535d93df7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=zIO2VRH3igGWwd%2Fgdlhv9hCk%2BeU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/490fb9749b7b4f0b90120adceb4716e5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=48e6uixQrBmH2ShSt35jhfOwGI0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5ee3304fbf3a47b180398eba31f9ba75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96LW36YKj5LmI5pep:q75.awebp?rk3s=f64ab15b&x-expires=1742111185&x-signature=eGAnA3PHAsEjC%2Bet1qksmA4c71I%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","IntelliJ IDEA"],"attachments":null,"extra":null,"language":null},{"title":"苍穹外卖技术总结——ThreadLocal","url":"https://juejin.cn/post/7479007185517084707","content":"\\n本项目参考了以下项目:
\\nJson-Assistant : github.com/MemoryZy/Js…
\\nMaven Helper : github.com/krasa/Maven…
\\n
在多线程编程中,共享变量的并发访问常导致数据错乱(如多个线程同时修改同一个变量)。传统的同步方法(如锁)虽然能解决问题,但会降低性能。ThreadLocal 提供了一种更轻量的方案:为每个线程创建变量的独立副本,实现线程间数据隔离,无需加锁即可保证安全。
\\n示例场景:
\\n假设有100个线程需要操作各自的数据库连接。若共享一个Connection对象,必须加锁;但若每个线程有自己的Connection副本,则无需同步。这正是ThreadLocal的用武之地!
ThreadLocal的核心操作包括:set()
、get()
、remove()
。
常用方法:
\\n\\n\\n\\npublic void set(T value) 设置当前线程的线程局部变量的值\\n public T get() 返回当前线程所对应的线程局部变量的值\\n public void remove() 移除当前线程的线程局部变量\\n
代码示例:
\\npublic class ThreadLocalDemo {\\n private static ThreadLocal<String> threadLocal = new ThreadLocal<>();\\n\\n public static void main(String[] args) {\\n new Thread(() -> {\\n threadLocal.set(\\"线程A的私藏数据\\");\\n System.out.println(threadLocal.get()); // 输出:线程A的私藏数据\\n threadLocal.remove(); // 用完记得清理!\\n }).start();\\n\\n new Thread(() -> {\\n threadLocal.set(\\"线程B的私藏数据\\");\\n System.out.println(threadLocal.get()); // 输出:线程B的私藏数据\\n }).start();\\n }\\n}\\n
\\n关键点:
\\nset()
设置自己的数据,get()
仅获取本线程的数据。remove()
用于清理数据,避免内存泄漏(下文详解)ThreadLocal的核心秘密藏在Thread类中:
\\n每个线程内部维护了一个**ThreadLocalMap**
(类似哈希表),键是ThreadLocal对象,值是线程的变量副本。
大概结构如图所示:
\\n流程解析:
\\n1.set()方法:
\\n①获取当前线程的ThreadLocalMap
。
\\n②若Map不存在,则创建并存储键值对(键为当前ThreadLocal对象,值为数据)。
public void set(T value) {\\n Thread t = Thread.currentThread();\\n ThreadLocalMap map = getMap(t);\\n if (map != null) {\\n map.set(this, value); // this指当前ThreadLocal对象\\n } else {\\n createMap(t, value); // 首次调用时创建Map\\n }\\n}\\n
\\n2.get()方法:
\\n①从当前线程的ThreadLocalMap
中查找与当前ThreadLocal关联的值。
②若未找到,则通过initialValue()
初始化(可重写此方法设置默认值)。
public T get() {\\n Thread t = Thread.currentThread();\\n ThreadLocalMap map = getMap(t);\\n if (map != null) {\\n ThreadLocalMap.Entry e = map.getEntry(this);\\n if (e != null) {\\n return (T)e.value;\\n }\\n }\\n return setInitialValue(); // 初始化并返回默认值\\n}\\n
\\nThreadLocal适用于线程需独立访问数据的场景:
\\nRequestContextHolder
)。ThreadLocal的ThreadLocalMap
中,Entry的Key是弱引用(WeakReference),Value是强引用。这可能导致以下问题:
1. 内存泄漏的根本原因
\\nThreadLocal的内存泄漏问题源于其内部数据结构ThreadLocalMap的设计。每个线程的ThreadLocalMap中存储的Entry键值对具有以下特性:
\\n\\n\\n\\n
\\n- Key为弱引用:Entry的Key是ThreadLocal对象的弱引用,而Value是强引用。
\\n- 线程生命周期绑定:ThreadLocalMap的生命周期与线程一致,若线程长时间运行(如线程池中的线程),未清理的Entry会持续占用内存。
\\n
泄漏过程:
\\n\\n\\n\\n
\\n- 当ThreadLocal对象的外部强引用被置为
\\nnull
时(如局部变量使用完毕),由于Entry的Key是弱引用,Key会被GC回收,导致Entry的Key变为null
。- 但Entry的Value仍被强引用,且线程未结束,导致Value无法被回收,形成内存泄漏。
\\n- 强引用链:
\\nThread Ref -> Thread -> ThreadLocalMap -> Entry -> Value
,这条链在Key为null
后依然存在,使Value无法释放 。
2. 为什么使用弱引用?
\\n弱引用的设计是为了降低内存泄漏的风险,而非完全消除。对比两种场景:
\\n\\n\\n\\n
\\n- 若Key为强引用:
\\n
\\n即使ThreadLocal对象外部引用被置为null
,Entry的Key仍持有强引用,ThreadLocal对象和Value都无法被回收,泄漏更严重。- 若Key为弱引用:
\\n
\\nThreadLocal对象会被GC回收,Key变为null
,Value的强引用链仍然存在,但后续通过调用set()
、get()
、remove()
方法可触发清理机制,释放Value 。
结论:弱引用提供了一层保障,但无法完全依赖自动清理。
\\n3. 被动清理机制的局限性
\\nThreadLocalMap在调用set()
、get()
、remove()
时会触发清理逻辑(如expungeStaleEntry()
),清除Key为null
的Entry的Value。但存在以下问题:
如何避免内存泄漏的发生?
\\n\\n\\n\\n
\\n- 在使用完
\\nThreadLocal
后,务必调用remove()
方法。 这是最安全和最推荐的做法。remove()
方法会从ThreadLocalMap
中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将ThreadLocal
定义为static final
,也强烈建议在每次使用后调用remove()
。- 在线程池等线程复用的场景下,使用
\\ntry-finally
块可以确保即使发生异常,remove()
方法也一定会被执行。
ThreadLocal的优势:
\\n\\n\\n\\n
\\n- 无锁化线程安全,提升性能。
\\n- 简化多线程数据传递(如上下文信息)。
\\n
劣势:
\\n\\n\\n\\n
\\n- 内存管理需谨慎,需配合
\\nremove()
使用。- 不适用于需跨线程共享数据的场景。
\\n
最后的最后,一句话理解ThreadLocal:
\\n它为每个线程提供了一个“独立储物柜”,柜子的钥匙是ThreadLocal对象,数据仅对当前线程可见。
看到这如果有用的话记得点赞关注哦,后续会更新更多内容的!!
\\ngraph TD\\n A[本地仓库] --\x3e B{远程仓库}\\n B --\x3e C[Mirror镜像]\\n B --\x3e D[Profile仓库]\\n B --\x3e E[POM中仓库]\\n style A fill:#90EE90\\n style C fill:#FFD700\\n
\\n配置位置 | 生效范围 | 优先级 | 典型应用场景 |
---|---|---|---|
本地仓库 | 全局 | 最高 | 优先使用本地缓存 |
Mirror镜像 | 全局覆盖 | 高 | 加速中央仓库/统一代理 |
settings.xml | Profile级 | 中 | 团队统一环境配置 |
POM文件 | 项目级 | 低 | 项目特殊依赖需求 |
Maven首先检查${user.home}/.m2/repository
:
<settings>\\n <localRepository>/path/to/custom/repo</localRepository>\\n</settings>\\n
\\nflowchart LR\\n Client[客户端请求] --\x3e Mirror{是否匹配mirrorOf}\\n Mirror --\x3e|匹配| MirrorRepo[使用镜像仓库]\\n Mirror --\x3e|不匹配| OriginRepo[使用原始仓库]\\n
\\n示例配置:
\\n<mirrors>\\n <mirror>\\n <id>aliyun</id>\\n <name>阿里云镜像</name>\\n <url>https://maven.aliyun.com/repository/public</url>\\n <mirrorOf>central,jcenter</mirrorOf>\\n </mirror>\\n</mirrors>\\n
\\n<profiles>\\n <profile>\\n <id>custom</id>\\n <repositories>\\n <repository>\\n <id>internal-repo</id>\\n <url>http://repo.internal.com</url>\\n <!-- 优先级高于pom中的仓库 --\x3e\\n <releases>\\n <enabled>true</enabled>\\n <updatePolicy>daily</updatePolicy>\\n </releases>\\n </repository>\\n </repositories>\\n </profile>\\n</profiles>\\n<activeProfiles>\\n <activeProfile>custom</activeProfile>\\n</activeProfiles>\\n
\\n<!-- pom.xml --\x3e\\n<repositories>\\n <repository>\\n <id>thirdparty</id>\\n <url>http://nexus.company.com/repo</url>\\n </repository>\\n</repositories>\\n
\\nsequenceDiagram\\n participant Maven\\n participant Local\\n participant Remote\\n \\n Maven->>Local: 1.检查本地仓库\\n alt 存在依赖\\n Local--\x3e>Maven: 直接使用\\n else 不存在\\n Maven->>Remote: 2.按顺序检查远程仓库\\n Remote--\x3e>Maven: 返回依赖\\n Maven->>Local: 缓存到本地\\n end\\n \\n Note over Remote: 检查顺序:<br/>1. Mirror镜像<br/>2. settings.xml中的Profile仓库<br/>3. POM中声明的仓库\\n
\\n<repository>\\n <id>snapshots</id>\\n <snapshots>\\n <enabled>true</enabled>\\n <!-- 更新频率策略 --\x3e\\n <updatePolicy>interval:60</updatePolicy>\\n </snapshots>\\n</repository>\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n策略参数 | 说明 |
---|---|
always | 每次构建都检查更新 |
daily (默认) | 每天首次构建检查更新 |
interval:X | 每隔X分钟检查(X=60) |
never | 仅使用本地缓存 |
graph TB\\n A[Server配置] --\x3e B{匹配Repository ID}\\n B --\x3e|完全匹配| C[使用对应凭证]\\n B --\x3e|镜像覆盖| D[使用Mirror的凭证]\\n
\\n场景1:依赖下载始终来自中央仓库
\\n✅ 检查mirrorOf
是否覆盖了目标仓库ID
场景2:私有仓库依赖无法解析
\\n✅ 验证settings.xml
中server配置与repository ID匹配
场景3:SNAPSHOT版本不更新
\\n✅ 检查updatePolicy
是否设置为always
调试命令:
\\nmvn dependency:resolve -X | grep \'Downloading from\'\\n
\\n<mirror>\\n <id>aliyun</id>\\n <mirrorOf>*</mirrorOf>\\n <url>https://maven.aliyun.com/repository/public</url>\\n</mirror>\\n
\\n分层配置策略:
\\nsettings.xml
pom.xml
profile
管理版本锁定:结合<dependencyManagement>
控制依赖版本
按照小数据量的查询商品标题,比如在一个四百万个商品里找标题,如果是使用 %标题关键词%
会全表扫描 ,一些小伙伴会忍不住上ES 或者 XunSearch ,我以前遇到的小系统,不少的人是这么干的
其实没有必要,可能百分之99的开发者,这辈子都用不上。没有达一种非常夸张的数据量的时候,几百万个商品标题也是小事一桩。下面由我娓娓道来。\\n
有没有一种方式,比如把 搜索 \\"一个饼干的包装\\" ,直接命中“饼干” 直接索引读取
\\n我们假设有一种方式,就是可以通过分词工具,比如 用jieba-php
进行分词。
假设,有两张表\\nproduct
product_title
Jieba::init();\\n Finalseg::init();\\n $tokens = Jieba::cut($model->title);\\n
\\n我把商品标题表储存在 product 表中, 然后分词的储存在 product_title 中,这样就形成了对应,饼干和包装分两条数据储存在了product_title 表中,其中对应着 product_id 。 其中product_title 的title 加了索引
\\n这种情况就像是论坛里的朋友开发过的优化查询的插件 whereIn([product_id,product_id ])差不多的方案。
\\n但是这种方式太low 了,要是多创建几个表搜索那不是要炸。
\\n那有没有一种laravel 插件,或者mysql 插件能直接做呢?
\\n\\n\\n//话外~~~,其实写到这的时候就不想写了,现在gpt这么发达,还写博客,我都不知道自己在干嘛~
\\n
其实mysql 自带就有,叫 全文索引(FULLTEXT)
CREATE TABLE test (\\n id INT AUTO_INCREMENT PRIMARY KEY,\\n title TEXT,\\n FULLTEXT(title)\\n);\\n
\\n这种方式的索引和我刚刚举例的的方式有点像,就是先进行了分词,分完词之后会储存到一个地方,读取的
\\n$searchTerm = \'关键词\';\\n$results = YourModel::where(\'title\', \'like\', \\"%$searchTerm%\\")\\n ->orWhereRaw(\\"MATCH(title) AGAINST(? IN NATURAL LANGUAGE MODE)\\", [$searchTerm])\\n ->get();\\n
\\n这种方式又会遇到问题,因为mysql 不知道中文呀,它只知道有空格的英文,中文分词默认的方式不行,需要一种 兼容 中文,日文,韩文这种语言叫做ngram
分词的倒排索引
ALTER TABLE 表名 ADD FULLTEXT(title) WITH PARSER ngram;\\n
\\n通过相对比较精确的分词,可以很快的完成我们的目标,搜索 “饼干”能找到相应的 文档ID ,直接命中索引
\\n加不加 ngram 的区别:
\\n我们搜索场景搜索 “饼干包装” ,如果直接搜索能行吗? 不行!\\n因为没有储存“饼干包装”的文档库,如何实现呢?
\\n从应用层面实现:
\\n拿到用户的keyword
之后,用分词工具分开来, 再通过 implode(\'+\',$cuts)
的方式塞到查询中
SELECT * FROM test_fulltext WHERE MATCH(title) AGAINST(\'饼干+包装\' IN BOOLEAN MODE);\\n
\\n还有就是索引保存在磁盘,意味着你不要买大内存的服务器部署 ,运行在内存中的中间件 ,可以调节mysql 的索引缓冲来优化查询
\\n实在不行,docker+ 主 从+从+从+从+从+从+从+从+从+从+从+从+从 .... 就能解决
\\n再说了你用es 并发大了,也要考虑分布式
\\n以后小伙伴们,业务中有类似的需求的时候,可以试试这种方案哈,别一个小小的系统还搞这些中间件哈,百分之99的开发碰不上,真的,我men只是配角!
\\n\\n","description":"杀鸡用牛刀 按照小数据量的查询商品标题,比如在一个四百万个商品里找标题,如果是使用 %标题关键词% 会全表扫描 ,一些小伙伴会忍不住上ES 或者 XunSearch ,我以前遇到的小系统,不少的人是这么干的\\n\\n其实没有必要,可能百分之99的开发者,这辈子都用不上。没有达一种非常夸张的数据量的时候,几百万个商品标题也是小事一桩。下面由我娓娓道来。\\n\\n搜“一个饼干的包装”中的 “饼干” 不用%%\\n\\n有没有一种方式,比如把 搜索 \\"一个饼干的包装\\" ,直接命中“饼干” 直接索引读取\\n\\n思路打开\\n\\n我们假设有一种方式,就是可以通过分词工具,比如 用jieba…","guid":"https://juejin.cn/post/7478893732190453798","author":"廖圣平","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T09:53:18.322Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a617724618684bb3b8aececb09949c41~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5buW5Zyj5bmz:q75.awebp?rk3s=f64ab15b&x-expires=1741945997&x-signature=MmEv%2BMquozETcrz46SzObsvTv4w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b690ab2c549246c4b056d18ec8f21991~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5buW5Zyj5bmz:q75.awebp?rk3s=f64ab15b&x-expires=1741945997&x-signature=H8dvy0aLBYHWv3PITdQ6L8DAESo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1e48fc90d46462faff5c2679c701fe1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5buW5Zyj5bmz:q75.awebp?rk3s=f64ab15b&x-expires=1741945997&x-signature=kbOpkN3HznPmblTnBTRTMteoBsA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/beb96852918144deae887706329cacc3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5buW5Zyj5bmz:q75.awebp?rk3s=f64ab15b&x-expires=1741945997&x-signature=r3aBNIWWWsN4elnLKJJhILHJ14k%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"破坏双亲委派之后,能重写String类吗?","url":"https://juejin.cn/post/7478889524425752595","content":"好啦,到此结束!
\\n
文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n\\n\\nJava虚拟机规范.Java SE 8版:
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n深入理解Java虚拟机:JVM高级特性与最佳实践(第3版):
\\n\\n
\\n- 资料链接:url81.ctfile.com/f/57345181-…
\\n- 访问密码:3899
\\n
在 Java 中,即使破坏了 双亲委派模型,理论上依然无法完全重写 java.lang.String
类。
这是因为 JVM 对 String
类有特殊的限制和处理机制,具体如下:
JVM 内置限制
\\njava.lang.String
是 JVM 的核心类,其加载和使用受到严格限制。JVM 会优先加载和使用由引导类加载器(Bootstrap ClassLoader)加载的 String
类。java.lang.String
类,JVM 在加载和运行中仍会优先使用引导类加载器加载的原生 String
类。类加载机制
\\njava.lang
包下的类),例如 java.lang.String
、java.lang.Object
等。java.lang.String
依然会被优先加载,且不允许被覆盖。安全性限制
\\njava.lang.String
的不可替换性是 JVM 保证平台安全性和一致性的基础。如果允许替换 String
,可能会导致各种意料之外的安全问题。defineClass()
方法的限制JVM 提供的 ClassLoader
的 defineClass()
方法会对类的全限定名进行检查:
java.
开头,则直接抛出 SecurityException
,防止覆盖 java
包下的核心类。这项检查在 JVM 的底层实现中被硬编码,无法通过常规方式绕过。
\\n尝试覆盖 java.lang.String
的结果
示例代码:假设你尝试自定义一个 java.lang.String
类:
package java.lang;\\n\\npublic class String {\\n public String() {\\n System.out.println(\\"My String\\");\\n }\\n}\\n
\\n编译结果
\\n编译时会提示错误:
\\nerror: cannot access java.lang.String\\n
\\n这是因为 Java 禁止用户在 java.lang
包下定义核心类。
绕过编译检查
\\n使用某些工具或方法绕过编译检查(如直接修改 .class
文件),在运行时仍然会因为类加载机制而失败。
即使破坏双亲委派模型(如自定义类加载器,优先加载用户定义的类),由于 JVM 会始终优先使用引导类加载器加载核心类,因此你定义的 java.lang.String
仍无法被 JVM 认可。
破坏双亲委派模型后依然无法重写 java.lang.String
类。
原因包括:
\\njava.lang.String
的安全性限制。String
类的特殊处理。如果尝试修改或替换 String
,可能导致程序运行异常甚至 JVM 崩溃。建议在 Java 开发中不要尝试替换或覆盖核心类。
使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈, 在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。
\\n注意:如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中。
\\n参考官方文档:dev.mysql.com/doc/refman/…
\\n\\n# 示例表:\\n\\nDROP TABLE IF EXISTS `actor`;\\nCREATE TABLE `actor` (\\n`id` int(11) NOT NULL,\\n`name` varchar(45) DEFAULT NULL,\\n`update_time` datetime DEFAULT NULL,\\nPRIMARY KEY (`id`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\\n\\n\\nINSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1,\'a\',\'2017‐12‐22\\n15:27:18\'), (2,\'b\',\'2017‐12‐22 15:27:18\'), (3,\'c\',\'2017‐12‐22 15:27:18\');\\n\\nDROP TABLE IF EXISTS `film`;\\nCREATE TABLE `film` (\\n`id` int(11) NOT NULL AUTO_INCREMENT,\\n`name` varchar(10) DEFAULT NULL,\\nPRIMARY KEY (`id`),\\nKEY `idx_name` (`name`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\\n\\n\\nINSERT INTO `film` (`id`, `name`) VALUES (3,\'film0\'),(1,\'film1\'),(2,\'film2\');\\n\\nDROP TABLE IF EXISTS `film_actor`;\\nCREATE TABLE `film_actor` (\\n`id` int(11) NOT NULL,\\n`film_id` int(11) NOT NULL,\\n`actor_id` int(11) NOT NULL,\\n`remark` varchar(255) DEFAULT NULL,\\nPRIMARY KEY (`id`),\\nKEY `idx_film_actor_id` (`film_id`,`actor_id`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\\n\\nINSERT INTO `film_actor` (`id`, `film_id`, `actor_id`) VALUES (1,1,1),(2,1,2),(3,2,1);\\n\\n\\nexplain select * from actor;\\n \\n# 在查询中的每个表会输出一行,如果有两个表通过 join 连接查询,那么会输出两行。\\n\\n
\\n会在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 show warnings 命令可 以得到优化后的查询语句,从而看出优化器优化了什么。额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)
\\nexplain extended select * from film where id = 1;
\\nshow warnings;
\\n相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。
\\nid列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。 id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
\\nselect_type 表示对应行是简单还是复杂的查询。
\\nexplain select * from film where id = 2;
\\n用这个例子来了解 primary、subquery 和 derived 类型:
\\n#关闭mysql5.7新特性对衍生表的合并优化
\\nset session optimizer_switch=\'derived_merge=off\';\\n\\nexplain select (select 1 from actor where id = 1) from (select * from film where id = 1) der;\\n
\\n#还原默认配置
\\nset session optimizer_switch=\'derived_merge=on\'; \\nexplain select 1 union all select 1;\\n
\\n这一列表示 explain 的一行正在访问哪个表。\\n当 from 子句中有子查询时,table列是格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。当有 union 时,UNION RESULT 的 table 列的值为,1和2表示参与 union 的 select 行id。
\\n这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。 依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL 。\\n一般来说,得保证查询达到range级别,最好达到ref ;
\\nmysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可 以单独查找索引来完成,不需要在执行时访问表
\\nmysql> explain select min(id) from film;
\\nmysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是 const的特例,表里只有一条元组匹配时为system;
\\nexplain extended select * from (select * from film where id = 1) tmp;
\\nshow warnings;
\\nprimary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。
\\nexplain select * from film_actor left join film on film_actor.film_id = film.id;
\\nref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会 找到多个符合条件的行。
\\n简单 select 查询,name是普通索引(非唯一索引)
\\nexplain select * from film where name = \'film1\';
\\n关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。
\\nexplain select film_id from film left join film_actor on film.id = film_actor.fi\\nlm_id;
\\n范围扫描通常出现在 in(), between ,> ,= 等操作中。使用一个索引来检索给定范围的行。
\\nexplain select * from actor where id > 1;
\\n扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接 对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这 种通常比ALL快一些。
\\nexplain select * from film;
\\n即全表扫描,扫描你的聚簇索引的所有叶子节点.通常情况下这需要增加索引来进行优化。
\\nexplain select * from actor;
\\n这一列显示查询可能使用哪些索引来查找 explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对此查询帮助不大,选择了全表查询。 如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提 高查询性能,然后用 explain 查看效果。
\\n这一列显示mysql实际采用哪个索引来优化对该表的访问。 如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。
\\n这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。 举例来说,film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,并且每个int是4字节。通 过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找。
\\nexplain select * from film_actor where film_id = 2;
\\nkey_len计算规则如下:
\\n 字符串:\\n char(n):如果存汉字长度就是 3n 字节 \\n varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,\\n 因为 varchar是变长字符串\\n
\\nchar(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,\\n如果是 utf-8,一个数字 或字母占1个字节,一个汉字占3个字节 ;
\\n 数值类型:\\n tinyint:1字节\\n smallint:2字节\\n int:4字节\\n bigint:8字节\\n 时间类型:\\n date:3字节\\n timestamp:4字节\\n datetime:8字节\\n
\\n如果字段允许为 NULL,需要1字节记录是否为 NULL ;索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。
\\n这一列显示了在key列记录的索引中,表查找值所用到的列或常量,\\n常见的有:const(常量),字段名(例:film.id)
\\n这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。
\\n这一列展示的是额外信息。常见的重要值如下:
\\n覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中 获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个 查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值。
\\nexplain select film_id from film_actor where film_id = 1;
\\n使用 where 语句来处理结果,并且查询的列未被索引覆盖
\\nexplain select * from actor where name = \'a\';
\\n查询的列不完全被索引覆盖,where条件中是一个前导列的范围;
\\nexplain select * from film_actor where film_id > 1;
\\nmysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索 引来优化。
\\nactor.name没有索引,此时创建了张临时表来distinct
\\nexplain select distinct name from actor;
\\nfilm.name建立了idx_name索引,此时查询时extra是using index,没有用临时表
\\nexplain select distinct name from film;
\\n将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一 般也是要考虑使用索引来优化的
\\n1 mysql> explain select * from actor order by name\\n2. film.name建立了idx_name索引,此时查询时extra是using index
\\nexplain select * from film order by name;
\\n使用某些聚合函数(比如 max、min来访问存在索引的某个字段是
\\nexplain select min(id) from film;
\\n\\n# 示例表:\\nCREATE TABLE `employees` (\\n`id` int(11) NOT NULL AUTO_INCREMENT,\\n`name` varchar(24) NOT NULL DEFAULT \'\' COMMENT \'姓名\',\\n`age` int(11) NOT NULL DEFAULT \'0\' COMMENT \'年龄\',\\n`position` varchar(20) NOT NULL DEFAULT \'\' COMMENT \'职位\',\\n`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \'入职时间\',\\nPRIMARY KEY (`id`),\\n KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE\\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=\'员工记录表\';\\n\\nINSERT INTO employees(name,age,position,hire_time) VALUES(\'LiLei\',22,\'manager\',NOW());\\nINSERT INTO employees(name,age,position,hire_time) VALUES(\'HanMeimei\',\\n23,\'dev\',NOW());\\nINSERT INTO employees(name,age,position,hire_time) VALUES(\'Lucy\',23,\'dev\',NOW());\\n
\\n1 EXPLAIN SELECT * FROM employees WHERE name= \'LiLei\';
\\n1 EXPLAIN SELECT * FROM employees WHERE name= \'LiLei\' AND age = 22;
\\nEXPLAIN SELECT * FROM employees WHERE name= \'LiLei\' AND age = 22 AND position =\'manager\';
\\n如果索引了多列,要遵守最左前缀法。则指的是查询从索引的最左前列开始并且不跳过索引中的列。
\\n1 EXPLAIN SELECT * FROM employees WHERE name = \'Bill\' and age = 31;\\n2 EXPLAIN SELECT * FROM employees WHERE age = 30 AND position = \'dev\';\\n3 EXPLAIN SELECT * FROM employees WHERE position = \'manager\'
\\n1 EXPLAIN SELECT * FROM employees WHERE name = \'LiLei\';\\n2 EXPLAIN SELECT * FROM employees WHERE left(name,3) = \'LiLei\';\\n给hire_time增加一个普通索引:
\\n1 ALTER TABLE employees
ADD INDEX idx_hire_time
(hire_time
) USING BTREE ;\\n2 EXPLAIN select * from employees where date(hire_time) =\'2018‐09‐30\';
转化为日期范围查询,有可能会走索引:
\\n1 EXPLAIN select * from employees where hire_time >=\'2018‐09‐30 00:00:00\' and hire_time <=\'2018‐09‐30 23:59:59\';
\\n还原最初索引状态
\\n1 ALTER TABLE employees
DROP INDEX idx_hire_time
;
1 EXPLAIN SELECT * FROM employees WHERE name= \'LiLei\' AND age = 22 AND position =\'manager\';\\n2 EXPLAIN SELECT * FROM employees WHERE name= \'LiLei\' AND age > 22 AND position =\'manager\';
\\n1 EXPLAIN SELECT name,age FROM employees WHERE name= \'LiLei\' AND age = 23 AND position=\'manager\';
\\n1 EXPLAIN SELECT * FROM employees WHERE name= \'LiLei\' AND age = 23 AND position =\'manager\';
\\n1 EXPLAIN SELECT * FROM employees WHERE name != \'LiLei\';
\\n1 EXPLAIN SELECT * FROM employees WHERE name is null
\\n1 EXPLAIN SELECT * FROM employees WHERE name like \'%Lei\'
\\n1 EXPLAIN SELECT * FROM employees WHERE name like \'Lei%\'
\\n问题:解决like\'%字符串%\'索引不被使用的方法?\\na)使用覆盖索引,查询字段必须是建立覆盖索引字段
\\n1 EXPLAIN SELECT name,age,position FROM employees WHERE name like \'%Lei%\';
\\nb)如果不能使用覆盖索引则可能需要借助搜索引擎
\\n1 EXPLAIN SELECT * FROM employees WHERE name = \'1000\'; 2 EXPLAIN SELECT * FROM employees WHERE name = 1000;
\\n用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评 估是否使用索引,详见范围查询优化
\\n1 EXPLAIN SELECT * FROM employees WHERE name = \'LiLei\' or name = \'HanMeimei\';
\\n1 ALTER TABLE employees
ADD INDEX idx_age
(age
) USING BTREE ;\\n2 explain select * from employees where age >=1 and age <=2000;
没走索引原因:mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。比如这个例子,可能是 由于单次数据量查询过大导致优化器最终选择不走索引 。\\n优化方法:可以将大的范围拆分成多个小范围。
\\n1 explain select * from employees where age >=1 and age <=1000;\\n2 explain select * from employees where age >=1001 and age <=2000;
\\n还原最初索引状态
\\n1 ALTER TABLE employees
DROP INDEX idx_age
;
PS:like KK%相当于=常量,%KK和%KK% 相当于范围
","description":"Explain总览图 这篇文章主要看图 Explain是啥\\n1、Explain工具介绍\\n\\n使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈, 在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。\\n\\n注意:如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中。\\n\\n2、Explain分析示例\\n\\n参考官方文档:dev.mysql.com/doc/refman/…\\n\\n\\n# 示例表:\\n\\nDROP TABLE IF EXISTS…","guid":"https://juejin.cn/post/7478888679231193125","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T08:34:45.005Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70054f9c7fa94751a945ed9cc78c2092~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741941833&x-signature=A4Ucifopx6QkbREhPhfD1cmoPAw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/900af3bb02a643fcbbea55dd4f73bcd7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741941833&x-signature=h66vmDO1EgtSjdm%2FZpneHnGlmJQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"面试官:如何实现一个并发控制?","url":"https://juejin.cn/post/7478690071396237321","content":"在前端开发的面试中,关于如何控制并发的问题经常被提及。无论是处理网络请求、文件上传还是其他异步操作,有效地管理并发都是确保应用性能和用户体验的关键。本文将通过一个简单的例子来探讨如何使用JavaScript实现并发控制,并解释其中涉及的核心概念。
\\n想象这样一个场景:你需要同时发起多个网络请求,但出于对服务器负载或客户端性能的考虑,你希望限制同时进行的最大请求数量。例如,如果一次性发送过多的请求,可能会导致服务器过载,或者客户端资源耗尽,影响用户体验。因此,我们需要一种机制来控制并发执行的任务数量。假设每次只能允许两个请求发送,这两个请求完成后其他请求才能发送,我们要怎么实现呢?
\\n要想保证每次只能两个任务进行,那我们首先能想到哪种数据结构呢?当然是数组了,先存入两个要执行的任务,当这个数组里有任务完成后,再添加进新的任务。这样每次都保证数组里的两个任务执行,从而实现并发控制。
\\n由于每个任务都是异步的(比如网络请求),我们需要知道什么时候这些任务完成了。JavaScript中的Promise
对象提供了一种很好的方式来处理异步操作的结果。通过.then()
方法,我们可以在任务成功完成时得到通知;通过.catch()
方法,我们可以在任务失败时得到通知。
一旦我们知道了一个任务已经完成,我们就需要触发下一轮的任务调度。这意味着我们需要重新检查当前是否还有空闲的并发槽位,并从任务队列中取出新的任务来执行。
\\nclass Limit {\\n constructor(paralleCount = 2) {\\n this.tasks = [];\\n this.runningCount = 0;\\n this.paralleCount = paralleCount;\\n }\\n\\n add(task) {\\n return new Promise((resolve, reject) => {\\n this.tasks.push({task, resolve, reject});\\n this._run();\\n });\\n }\\n\\n _run() {\\n while (this.runningCount < this.paralleCount && this.tasks.length) {\\n const {task, resolve, reject} = this.tasks.shift();\\n this.runningCount++;\\n task().then(() => { // 一个任务完毕了\\n resolve()\\n this.runningCount--\\n this._run()\\n }).catch((err) => {\\n reject(err)\\n this.runningCount--\\n this._run()\\n })\\n }\\n }\\n}\\n\\nconst limit = new Limit(2);\\nfunction addTask(time, name) {\\n limit.add(() => ajax(time))\\n .then(() => console.log(`任务${name}完成`))\\n .catch(() => console.log(`任务${name}出错`));\\n}\\n
\\n运行结果如下:
\\n通过上述分析和实现,我们不仅解决了并发控制的问题,还深入理解了其背后的原理和设计思想。这种模式不仅可以应用于网络请求,还可以扩展到任何需要控制并发执行的任务场景中。
","description":"在前端开发的面试中,关于如何控制并发的问题经常被提及。无论是处理网络请求、文件上传还是其他异步操作,有效地管理并发都是确保应用性能和用户体验的关键。本文将通过一个简单的例子来探讨如何使用JavaScript实现并发控制,并解释其中涉及的核心概念。 一. 问题的提出\\n\\n想象这样一个场景:你需要同时发起多个网络请求,但出于对服务器负载或客户端性能的考虑,你希望限制同时进行的最大请求数量。例如,如果一次性发送过多的请求,可能会导致服务器过载,或者客户端资源耗尽,影响用户体验。因此,我们需要一种机制来控制并发执行的任务数量。假设每次只能允许两个请求发送…","guid":"https://juejin.cn/post/7478690071396237321","author":"zylx73","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T06:04:47.840Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/84e6cb65731a443eb22d1b9e63bdae84~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgenlseDcz:q75.awebp?rk3s=f64ab15b&x-expires=1741932286&x-signature=T6z5b8F1aqDOfnqOAkTzbNVHUzE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","JavaScript"],"attachments":null,"extra":null,"language":null},{"title":"年少不知自增好,错把UUID当个宝!!!","url":"https://juejin.cn/post/7478495083374559270","content":"在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。
\\n550e8400-e29b-41d4-a716-446655440000
)。BIGINT
)仅占用 8 字节。索引越大,存储和查询的效率越低。更新主键:
\\n更新非主键列:
\\nWHERE id = \'550e8400-e29b-41d4-a716-446655440000\'
的效率低于 WHERE id = 12345
。UUIDv7
),减少索引分裂和页分裂。将 UUID 存储为 BINARY(16)
而不是 CHAR(36)
,减少存储空间。
CREATE TABLE users (\\n id BINARY(16) PRIMARY KEY,\\n name VARCHAR(255)\\n);\\n
\\n使用自增主键作为物理主键,UUID 作为逻辑主键。
\\nCREATE TABLE users (\\n id BIGINT AUTO_INCREMENT PRIMARY KEY,\\n uuid CHAR(36) UNIQUE,\\n name VARCHAR(255)\\n);\\n
\\nUUID 作为主键的缺点:
\\n字符主键效率低的原因:
\\n优化建议:
\\n最近一段时间接了个项目一直在忙碌,遇到了一个非常无语的问题,因为该项目的测试服务器及环境是由甲方人员进行购买、搭建环境,我在本地开发后推到测试服务器上面进行构建测试、由于环境不一致的问题导致产生了很多问题,出现了经典的 在我机器上就可以运行没问题
的人生哲理......我滴妈 不想吐槽了。
所以说。在本地和测试环境中,使用 Docker 来安装和管理服务已经成为最简单、最常见的方式。通过 docker compose up -d
,我们可以一键启动所有依赖服务,提高开发和测试效率。此外,结合 Dockerfile 进行环境自定义,不仅可以实现个性化配置,还能方便团队协作,确保所有成员的环境一致。
平常情况下当我们入职新公司拿到新电脑的时候 第一步就是安装自己的本地开发环境,无论是Windows环境的安装包还是Mac的brew安装 都需要我们去一步一步手动安装 Nginx、MySQL、Redis、MQ 等服务,并配置各种依赖。这种安装方式无法保证我们一次性顺利安装成功 可能会遇到一些问题导致我们折腾一上午或者一天,这对于我们这种 “经验丰富”的牛马来说是不能容忍的。 那么使用docker安装的好处就体现出来了。
\\n无论我们使用的是Windows系统还是Mac系统我们都可以通过安装Docker Desktop,它提供了简单易用的图形化界面,并且内置了 Docker Engine 和 Docker CLI,适用于本地开发和测试环境。
\\n\\n\\nDocker Desktop安装方式我就不介绍了 不了解的可以参考其他关于介绍Docker Desktop的文章进行了解安装。
\\n
验证安装 打开终端(Terminal),运行:
\\ndocker -v\\n
\\n看到 Docker 版本信息即安装成功。
\\n这里举个例子 比如我们有个项目需要安装php开发环境。我需要安装php、mysql、nginx、redis、rabbitmq这些服务。
\\nPHP 运行环境可以通过官方提供的 Docker 镜像来安装:
\\ndocker pull php:8.2-fpm\\n
\\n然后运行 PHP 容器:
\\ndocker run -d --name php-container -p 9000:9000 -v $(pwd)/php:/var/www/html php:8.2-fpm\\n
\\n-d
:后台运行容器--name php-container
:指定容器名称-p 9000:9000
:映射 PHP-FPM 端口-v $(pwd)/php:/var/www/html
:将本地 php
目录挂载到容器的 /var/www/html
,便于开发容器启动后就是这样的。
\\n可以进入 PHP 容器,运行 php -v
查看版本:
docker exec -it php-container php -v\\n
\\n我们可以通过 Docker 轻松安装 MySQL 5.7:
\\ndocker run -d --name mysql-container -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:5.7\\n
\\n-e MYSQL_ROOT_PASSWORD=123456
:设置 root 用户密码-p 3306:3306
:映射数据库端口--name mysql-container
:指定容器名称验证 MySQL 运行
\\ndocker exec -it mysql-container mysql -uroot -p\\n
\\n输入 123456
进入 MySQL 命令行。
docker run -d --name nginx-container -p 80:80 -v $(pwd)/nginx/conf.d:/etc/nginx/conf.d -v $(pwd)/php:/var/www/html nginx\\n
\\n-p 80:80
:映射 Web 端口-v $(pwd)/nginx/conf.d:/etc/nginx/conf.d
:挂载本地 Nginx 配置-v $(pwd)/php:/var/www/html
:确保 Nginx 可以访问 PHP 代码目录测试 Nginx
\\ncurl http://localhost\\n
\\n如果返回默认的 Nginx 页面,说明安装成功。
\\nRedis安装方式如下:
\\ndocker run -d --name redis-container -p 6379:6379 redis:5.0\\n
\\n-p 6379:6379
:映射 Redis 端口--name redis-container
:指定容器名称连接 Redis
\\ndocker exec -it redis-container redis-cli\\n
\\n输入 ping
,如果返回 PONG
,说明 Redis 运行正常。
RabbitMQ安装方法如下:
\\ndocker run -d --name rabbitmq-container -p 5672:5672 -p 15672:15672 rabbitmq:latest\\n
\\n-p 5672:5672
:RabbitMQ 消息端口-p 15672:15672
:RabbitMQ Web 管理界面端口访问 Web 管理界面 浏览器打开 http://localhost:15672/
,默认用户名密码为 guest/guest
上面我们介绍了如何使用 docker run
命令分别安装 PHP、MySQL、Nginx、Redis 和 RabbitMQ。这种方式虽然能让我们快速启动各个服务,但能发现感觉好麻烦,大概总结一下麻烦点:
docker run
命令,手动指定端口、环境变量、挂载目录等。直接使用 docker run
启动容器后,它们默认属于不同的网络,彼此之间无法直接访问,必须手动创建 Docker 网络:
docker network create my-network\\n
\\n然后在每个 docker run
命令中加上 --network my-network
,这样容器之间才能互相通信,配置过程较为繁琐。
MYSQL_ROOT_PASSWORD
、Redis 的配置等),如果用 docker run
,这些变量必须手动传递,而且不方便维护。nginx.conf
、PHP 的 php.ini
、MySQL 的 my.cnf
)需要挂载到正确的路径,使用 docker run
逐个配置很麻烦。docker run
,每个人都要手动执行相同的命令,容易出错,也难以保证所有人的开发环境完全一致。这时候就不得不说使用Docker Compose的好处了。Docker Compose 是通过一个 docker-compose.yml
文件 定义所有服务,并提供了一键启动的能力:
统一管理所有服务,只需一个命令即可启动/停止所有容器:
\\ndocker compose up -d\\n
\\n自动创建网络,让所有容器在同一个网络下自由通信,无需手动配置。
\\n更方便的配置管理,所有环境变量、挂载路径都可以直接写入 docker-compose.yml
,可读性更好。
易于版本控制和团队协作,只需共享 docker-compose.yml
文件,所有人都能获得相同的环境。
用Docker Compose来启动多项服务的话 建议最好设置一个目录来进行统一的文件映射与存放。\\n比如我本地的项目目录结构是这样的:
\\nproject-root/\\n│── docker-compose.yml\\n│── php/\\n│── mysql/\\n│ ├── my.cnf\\n│── nginx/\\n│ ├── conf.d/\\n│── redis/\\n│── rabbitmq/\\n│── wwwroot/\\n
\\nphp/
:PHP 代码目录mysql/my.cnf
:MySQL 配置文件nginx/conf.d/
:Nginx 配置目录redis/
和 rabbitmq/
只是占位符目录wwwroot/
项目存放目录然后我们这次用新建 docker-compose.yml
的方式来一键启动上面我们安装的服务。比如我们新建 docker-compose.yml
,然后填入以下内容:
services:\\n php:\\n image: php:8.2-fpm\\n container_name: php-container\\n volumes:\\n - ./wwwroot:/var/www/html\\n networks:\\n - my-network\\n depends_on:\\n - mysql\\n - redis\\n\\n mysql:\\n image: mysql:5.7\\n container_name: mysql-container\\n restart: always\\n environment:\\n MYSQL_ROOT_PASSWORD: 123456\\n MYSQL_DATABASE: mydb\\n MYSQL_USER: myuser\\n MYSQL_PASSWORD: mypassword\\n ports:\\n - \\"3306:3306\\"\\n volumes:\\n - ./mysql/data:/var/lib/mysql\\n - ./mysql/my.cnf:/etc/mysql/my.cnf\\n networks:\\n - my-network\\n\\n nginx:\\n image: nginx:latest\\n container_name: nginx-container\\n ports:\\n - \\"80:80\\"\\n volumes:\\n - ./nginx/conf.d:/etc/nginx/conf.d\\n - ./wwwroot:/var/www/html\\n networks:\\n - my-network\\n depends_on:\\n - php\\n\\n redis:\\n image: redis:5.0\\n container_name: redis-container\\n restart: always\\n ports:\\n - \\"6379:6379\\"\\n networks:\\n - my-network\\n\\n rabbitmq:\\n image: rabbitmq:latest\\n container_name: rabbitmq-container\\n restart: always\\n ports:\\n - \\"5672:5672\\"\\n - \\"15672:15672\\"\\n networks:\\n - my-network\\n\\nnetworks:\\n my-network:\\n driver: bridge\\n
\\n在 docker-compose.yml
中,每个服务(service)都有不同的配置项,如 image
、container_name
、volumes
、networks
、depends_on
等。下面我们简单逐个解析它们的作用和使用方式。
1. image
:指定使用的 Docker 镜像
image: php:8.2-fpm\\n
\\n作用:指定容器使用哪个 Docker 镜像。
\\n示例:
\\nphp:8.2-fpm
:使用官方 PHP 8.2-FPM 镜像。mysql:5.7
:使用官方 MySQL 5.7 镜像。nginx:latest
:使用最新版本的 Nginx 镜像。如果本地没有这个镜像,Docker 会自动从 Docker Hub 拉取。
\\n2. container_name
:指定容器名称
container_name: php-container\\n
\\n作用:为容器指定一个固定的名称,便于管理和操作。
\\n示例:
\\ncontainer_name: mysql-container
→ 我们可以用 docker ps
看到容器的名称是 mysql-container
。docker exec -it mysql-container bash
进入容器,而不用记住随机生成的容器 ID。⚠ 注意:如果不指定 container_name
,Docker 会自动给容器分配一个随机名称,比如 adoring_turing
这样的名字。
3. volumes
:挂载本地文件或目录
volumes:\\n - ./wwwroot:/var/www/html\\n
\\n作用:将 宿主机的目录(或文件) 挂载到容器的指定目录,方便开发和数据持久化。
\\n格式:
\\nvolumes:\\n - 宿主机路径:容器内部路径\\n
\\n示例:
\\n./wwwroot:/var/www/html
wwwroot
目录作为代码目录。这样我们本地修改 wwwroot
里的文件,容器里会同步更新。./mysql/data:/var/lib/mysql
./mysql/data
目录,防止数据丢失。./nginx/conf.d:/etc/nginx/conf.d
nginx/conf.d/
目录中的配置文件,方便修改 Nginx 配置。⚠ 注意:如果不使用 volumes
,容器中的数据会丢失,因为容器被删除后,它内部的文件也会被清空
4. networks
:管理容器间的网络
networks:\\n - my-network\\n
\\n作用:将容器加入同一个网络,让它们可以互相通信,而不用手动创建网络。
\\n示例:
\\nnetworks:\\n my-network:\\n driver: bridge\\n
\\n这样所有在 my-network
网络中的容器都可以通过 容器名称 互相访问。例如:
mysql-container
连接 MySQL,而不用 localhost
。php-container
访问 PHP-FPM,而不用 127.0.0.1
。⚠ 如果不指定 networks
,Docker 默认会创建一个独立的网络,每个容器只能用 localhost
访问自己,不能访问其他容器。
5. depends_on
:指定容器依赖关系
depends_on:\\n - mysql\\n - redis\\n
\\n作用:确保 php
容器 在 mysql
和 redis
之后启动。
示例:
\\nphp:\\n depends_on:\\n - mysql\\n - redis\\n
\\nphp
不会先于 mysql
和 redis
启动,可以避免 PHP 启动时 MySQL 还没准备好导致连接失败的情况。⚠ 注意:
\\ndepends_on
只保证 容器启动顺序,但 不保证服务内部完全就绪。php
容器启动时,加入一个等待逻辑,确保 mysql
和 redis
可用,比如使用 wait-for-it.sh
脚本。总结一下
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n配置项 | 作用 | 示例 |
---|---|---|
image | 指定 Docker 镜像 | php:8.2-fpm |
container_name | 设定容器名称 | php-container |
volumes | 挂载本地目录到容器 | ./wwwroot:/var/www/html |
networks | 让容器互相通信 | my-network |
depends_on | 设置容器依赖关系 | php -> mysql, redis |
启动项目
\\n在 docker-compose.yml
所在目录下,运行:
docker compose up -d\\n
\\n这将:
\\nmy-network
,让各个服务互相通信停止 & 删除容器
\\ndocker compose down\\n
\\n这将停止并移除所有容器。
\\n上面我们已经使用 docker-compose.yml
成功编排了 PHP、MySQL、Nginx、Redis 和 RabbitMQ,并且能够一键启动所有服务。但是,虽然 环境的版本一致了,但仍然会遇到一些 额外的问题,比如我们需要进行 PHP 扩展的安装。
docker-compose.yml
还不够?让我们来看看,在 docker-compose.yml
方式下,可能会出现哪些问题:
当我们启动 PHP 容器后,默认的 php:8.2-fpm
镜像 只包含 PHP 运行环境,但实际项目可能需要用到 pdo_mysql
、gd
、redis
、bcmath
等扩展。例如,我们需要执行:
docker exec -it php-container bash\\n
\\n然后在容器内手动安装:
\\ndocker-php-ext-install pdo_mysql gd bcmath\\ndocker-php-ext-enable redis\\n
\\n这样虽然可以解决问题,但 每次换一台机器都要手动操作一次,非常麻烦。
\\n假设我们的同事也使用了我们的 docker-compose.yml
启动环境:
docker compose up -d\\n
\\n他虽然能启动 PHP 容器,但仍然需要手动进入容器安装扩展。如果 有 10 个人 都要这么做,那就是 10 次重复劳动,这不仅费时,还可能导致版本不一致(有些人可能少装了某个扩展)。
\\n我们最终希望:
\\n👉 这就是 Dockerfile 的作用!我们可以使用 Dockerfile 定制自己的 PHP 镜像,在构建镜像时自动安装所有需要的扩展,从而彻底解决这个问题。
\\n接下来,我们将使用 Dockerfile 来构建一个包含 PHP 扩展的自定义镜像,确保每个人启动 PHP 容器后,就能直接使用项目所需的环境!🚀
\\n我们在项目的 php/
目录下,新建 Dockerfile
(不带扩展名),然后填入以下内容:
# 使用 php:8.2-fpm 作为基础镜像\\nFROM php:8.2-fpm\\n\\n# 安装常见的扩展依赖(这适用于很多 PHP 扩展的依赖)\\nRUN apt-get update && apt-get install -y \\\\\\n libzip-dev \\\\\\n libxml2-dev \\\\\\n libcurl4-openssl-dev \\\\\\n libpng-dev \\\\\\n libjpeg-dev \\\\\\n libfreetype6-dev \\\\\\n libmemcached-dev \\\\\\n libssl-dev \\\\\\n libssh2-1-dev \\\\\\n libsodium-dev \\\\\\n libicu-dev \\\\\\n libmagickwand-dev \\\\\\n gettext \\\\\\n unzip \\\\\\n && rm -rf /var/lib/apt/lists/*\\n\\n# 安装 PHP 扩展\\nRUN docker-php-ext-install -j$(nproc) pdo_mysql\\n\\n# 安装并启用 Redis 扩展\\nRUN pecl install redis && docker-php-ext-enable redis\\n\\n# 安装并启用 Memcached 扩展\\nRUN pecl install memcached && docker-php-ext-enable memcached\\n\\n# 安装并启用 MongoDB 扩展\\nRUN pecl install mongodb && docker-php-ext-enable mongodb\\n\\n# 安装并启用 Xdebug 扩展\\nRUN pecl install xdebug && docker-php-ext-enable xdebug\\n\\n# 安装并启用 Opcache 扩展\\nRUN docker-php-ext-enable opcache\\n\\n# 安装并启用 Swoole 扩展(如果需要)\\nRUN pecl install swoole && docker-php-ext-enable swoole\\n\\n# 安装并启用 igbinary 扩展(用于更高效的序列化)\\nRUN pecl install igbinary && docker-php-ext-enable igbinary\\n\\n# 安装并启用 GD 扩展\\nRUN docker-php-ext-install gd\\n\\n# 安装并启用 PCNTL 扩展\\nRUN docker-php-ext-install pcntl \\n\\n# 安装 Composer(PHP 依赖管理工具)\\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\\n\\n# 设置 Composer 为全局可用\\nRUN ln -s /usr/local/bin/composer /usr/bin/composer\\n\\n# 清理缓存以减小镜像大小\\nRUN apt-get clean\\n\\n# 配置 Xdebug\\nRUN echo \\"zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)\\" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \\\\\\n echo \\"xdebug.mode=debug\\" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \\\\\\n echo \\"xdebug.start_with_request=yes\\" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \\\\\\n echo \\"xdebug.client_host=host.docker.internal\\" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \\\\\\n echo \\"xdebug.client_port=9003\\" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini\\n\\n# 默认启用 OPcache\\nRUN echo \\"opcache.enable=1\\" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini\\n\\n# 设置工作目录\\nWORKDIR /var/www/html\\n\\n# 默认命令\\nCMD [\\"php-fpm\\"]\\n
\\ndocker-compose.yml
使用 Dockerfile在 docker-compose.yml
中,我们需要 修改 PHP 的 image
配置,让它从 Dockerfile 构建自定义镜像,而不是直接拉取 php:8.2-fpm
。
services:\\n php:\\n build:\\n context: ./php # 指定 Dockerfile 所在目录\\n dockerfile: Dockerfile # 这个参数可以省略,默认就是 Dockerfile\\n container_name: php-container\\n volumes:\\n - ./wwwroot:/var/www/html # 挂载项目代码\\n networks:\\n - my-network\\n depends_on:\\n - mysql\\n - redis\\n\\n mysql:\\n image: mysql:5.7\\n container_name: mysql-container\\n restart: always\\n environment:\\n MYSQL_ROOT_PASSWORD: 123456\\n MYSQL_DATABASE: mydb\\n MYSQL_USER: myuser\\n MYSQL_PASSWORD: mypassword\\n ports:\\n - \\"3306:3306\\"\\n volumes:\\n - ./mysql/data:/var/lib/mysql\\n - ./mysql/my.cnf:/etc/mysql/my.cnf\\n networks:\\n - my-network\\n\\n nginx:\\n image: nginx:latest\\n container_name: nginx-container\\n ports:\\n - \\"80:80\\"\\n volumes:\\n - ./nginx/conf.d:/etc/nginx/conf.d\\n - ./wwwroot:/var/www/html # 让 Nginx 也能访问 PHP 代码\\n networks:\\n - my-network\\n depends_on:\\n - php\\n\\n redis:\\n image: redis:5.0\\n container_name: redis-container\\n restart: always\\n ports:\\n - \\"6379:6379\\"\\n networks:\\n - my-network\\n\\n rabbitmq:\\n image: rabbitmq:latest\\n container_name: rabbitmq-container\\n restart: always\\n ports:\\n - \\"5672:5672\\"\\n - \\"15672:15672\\"\\n networks:\\n - my-network\\n\\nnetworks:\\n my-network:\\n driver: bridge\\n
\\nbuild:
替换 image: php:8.2-fpm
,改为:
build:\\n context: ./php\\n dockerfile: Dockerfile\\n
\\n这样 Docker Compose 会在 php/
目录 下查找 Dockerfile
并构建自定义镜像。
volumes:
./wwwroot:/var/www/html
,保证代码能够挂载到 PHP 容器中。第一次运行(需要构建镜像)
\\ndocker compose up -d --build\\n
\\n--build
选项确保 重新构建 PHP 镜像,并安装扩展。之后的启动(无需重建)
\\ndocker compose up -d\\n
\\n验证 PHP 是否安装了扩展
\\n进入 PHP 容器:
\\ndocker exec -it php-container /bin/bash\\n
\\n然后运行:
\\nphp -m | grep -E \\"pdo_mysql|gd|bcmath|opcache|redis|memcached|mongodb|xdebug|swoole|igbinary\\"\\n
\\n如果输出如下:
\\n说明所有扩展已正确安装!🎉
\\n这样做的好处
\\n✅ 解决了手动安装 PHP 扩展的问题,所有扩展在构建镜像时自动安装。
\\n✅ 保证团队环境一致,不需要每个开发者手动安装扩展。
\\n✅ 包含 Composer,方便管理 PHP 依赖。
\\n✅ 支持 Xdebug 远程调试,方便开发和调试代码。
\\n✅ 构建一次,所有人都可以直接使用相同的环境,提升开发效率。
后续优化(可选)
\\n如果我们还想:
\\n优化 PHP 配置,可以在 php/
目录下创建 php.ini
,并在 Dockerfile
里挂载:
COPY php.ini /usr/local/etc/php/php.ini\\n
\\n减少镜像体积,可以在 RUN
语句后加上 rm -rf /var/lib/apt/lists/*
来清理不必要的缓存。
除了 PHP,我们安装的 MySQL、Redis、Nginx 和 RabbitMQ 也可以使用 Dockerfile 进行 自定义构建 和 配置优化。
\\n虽然 docker-compose.yml
可以直接拉取官方镜像,但通过 Dockerfile 自定义构建,我们可以实现:
my.cnf
、Nginx 自定义 nginx.conf
)。其他服务的Dockerfile我就不一一写示例了,总之我们可以通过类似的方式在 mysql/
、redis/
、nginx/
和 rabbitmq/
目录下创建各自的 Dockerfile
来完成自定义。 然后修改docker-compose.yml
对应的地方来进行重新构建启动。
到这里,我们已经: ✅ 了解了 docker-compose.yml
如何进行服务编排。
\\n✅ 发现了 docker-compose.yml
的局限性(仍需手动安装 PHP 扩展)。
\\n✅ 通过 Dockerfile 自定义了 PHP 运行环境(扩展已安装、环境已配置)。
\\n✅ 了解了 MySQL、Redis、Nginx、RabbitMQ 也可以使用 Dockerfile 进行自定义构建,并列出了使用 Dockerfile 的优势。
针对上面的一些示例。我再简单说一些可以完善的地方以供参考哈。
\\n目前我们的 Dockerfile
直接基于 php:8.2-fpm
,并安装了很多扩展。如果需要优化镜像大小,可以:
使用 multi-stage build
来减少不必要的组件(特别适用于构建 PHP 应用)。
减少 apt-get
安装后的缓存:
RUN apt-get update && apt-get install -y \\\\\\n some-packages \\\\\\n && rm -rf /var/lib/apt/lists/* && apt-get clean\\n
\\n避免安装不必要的软件,尽量精简。
\\n如果我们的 PHP 代码 最终是要在生产环境运行,那么可以用 更轻量的镜像,如 php:8.2-alpine
:
FROM php:8.2-fpm-alpine\\n
\\n这样可以减少很多无用的 Linux 库。
\\n在我们的 docker-compose.yml
里面的 MySQL 密码、数据库名是写死的:
environment:\\n MYSQL_ROOT_PASSWORD: 123456\\n MYSQL_DATABASE: mydb\\n MYSQL_USER: myuser\\n MYSQL_PASSWORD: mypassword\\n
\\n但在 生产环境中,明文暴露密码是非常危险的!
\\n建议改为 .env
文件管理环境变量:
environment:\\n MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}\\n MYSQL_DATABASE: ${MYSQL_DATABASE}\\n MYSQL_USER: ${MYSQL_USER}\\n MYSQL_PASSWORD: ${MYSQL_PASSWORD}\\n
\\n然后创建 .env
文件:
MYSQL_ROOT_PASSWORD=your_secure_password\\nMYSQL_DATABASE=mydb\\nMYSQL_USER=myuser\\nMYSQL_PASSWORD=mypassword\\n
\\n这样可以 避免密码硬编码,并且更方便切换不同的环境配置。
\\n我们虽然在 docker-compose.yml
里定义了 Nginx 服务:
volumes:\\n - ./nginx/conf.d:/etc/nginx/conf.d\\n
\\n但如果 没有正确配置 nginx.conf
,Nginx 可能不会解析 PHP 文件。比如我们提供一个示例 nginx.conf
:
server {\\n listen 80;\\n server_name localhost;\\n root /var/www/html;\\n\\n index index.php index.html;\\n\\n location / {\\n try_files $uri $uri/ =404;\\n }\\n\\n location ~ .php$ {\\n include fastcgi_params;\\n fastcgi_pass php-container:9000;\\n fastcgi_index index.php;\\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\\n }\\n}\\n
\\n这样,Nginx 就能正确解析 .php
文件,并将请求转发到 php-container
。
目前 docker-compose.yml
里挂载了 mysql/data
目录:
volumes:\\n - ./mysql/data:/var/lib/mysql\\n
\\n但我们可以 进一步优化数据管理:
\\n使用 Docker Volume,而不是直接挂载宿主机目录:
\\nvolumes:\\n mysql-data:\\nservices:\\n mysql:\\n volumes:\\n - mysql-data:/var/lib/mysql\\n
\\n这样即使 docker-compose down
,数据仍然保留在 docker volume
里,而不会误删。
对 Redis 进行持久化(AOF 方式) :
\\nredis:\\n volumes:\\n - ./redis/data:/data\\n
\\n确保 Redis 不会在容器重启后丢失数据。
\\nMakefile
简化管理比如我们现在启动项目需要运行:
\\ndocker compose up -d --build\\n
\\n如果要停止:
\\ndocker compose down\\n
\\n那我们其实可以创建一个 Makefile
,让这些命令更简洁:
up:\\ndocker compose up -d --build\\n\\ndown:\\ndocker compose down\\n\\nlogs:\\ndocker compose logs -f\\n\\nrestart:\\ndocker compose down && docker compose up -d --build\\n
\\n这样,执行 make up
就可以一键启动环境,比手动输入命令要方便很多。
如果我们的团队需要在 CI/CD(持续集成/持续部署) 流程中使用 Docker,可以在 GitHub Actions 或 GitLab CI 里集成 docker-compose
,自动构建和部署服务。例如:
jobs:\\n build:\\n runs-on: ubuntu-latest\\n steps:\\n - name: Checkout code\\n uses: actions/checkout@v2\\n\\n - name: Set up Docker\\n uses: docker/setup-buildx-action@v1\\n\\n - name: Build and run services\\n run: |\\n docker compose up -d --build\\n
\\n这样,每次推送代码,GitHub Actions 就会 自动启动 Docker 容器 进行测试。
\\n因为上面我们在 Dockerfile
里已经安装了 Xdebug:
RUN pecl install xdebug && docker-php-ext-enable xdebug\\n
\\n但如果本地开发时 想用 VS Code 进行远程调试,需要添加 .vscode/launch.json
:
{\\n \\"version\\": \\"0.2.0\\",\\n \\"configurations\\": [\\n {\\n \\"name\\": \\"Listen for Xdebug\\",\\n \\"type\\": \\"php\\",\\n \\"request\\": \\"launch\\",\\n \\"port\\": 9003,\\n \\"pathMappings\\": {\\n \\"/var/www/html\\": \\"${workspaceFolder}\\"\\n }\\n }\\n ]\\n}\\n
\\n这样,我们就可以在 VS Code 里直接调试 PHP 代码 了。
\\n目前我们的 Dockerfile
适用于 开发环境,但在 生产环境,也许我们可能希望:
去掉 Xdebug(提高性能)。
\\n使用 php:8.2-fpm-alpine
,减少镜像大小。
增加 healthcheck
来自动检测 MySQL、Redis 是否存活:
mysql:\\n healthcheck:\\n test: [\\"CMD\\", \\"mysqladmin\\", \\"ping\\", \\"-h\\", \\"localhost\\"]\\n interval: 30s\\n timeout: 10s\\n retries: 3\\n
\\n这样,容器会自动检测 MySQL 是否可用,如果失败,Docker 会自动重启它。
\\n通过上面的示例,我们可以看到,使用 Docker 及 Docker Compose 可以极大地简化本地和测试环境的搭建,让我们能够快速启动完整的 PHP 运行环境,包括 PHP、MySQL、Nginx、Redis、RabbitMQ 等常见服务。同时,使用 Dockerfile 进一步定制化,使得每个开发人员的环境都保持一致,避免了手动安装扩展、配置环境的麻烦。
\\n然而,我要说下 上面的这些示例 主要适用于本地开发和测试环境。当我们进入 正式生产环境 时,就需要考虑更严谨的 Dockerfile 编写和镜像构建,比如:
\\nphp:8.2-fpm-alpine
以减少不必要的软件包,优化启动速度和安全性。.env
文件或 Kubernetes Secret 来存储敏感信息,避免密码硬编码。docker logs
并结合 ELK(Elasticsearch + Logstash + Kibana)或 Loki 进行日志收集和分析。🚨 最后提醒:
\\n上面的示例主要适用于本地和测试环境,生产环境请慎重使用!
\\n在生产环境中,应根据实际需求优化 Dockerfile、镜像大小、安全策略、数据持久化方案、监控和运维策略,确保服务的稳定性和安全性。
在本地和测试环境,我们通常使用 Docker 搭建 MySQL、Redis、Elasticsearch,这非常方便,也能快速重置环境。但在生产环境中,使用 Docker 直接运行存储型服务可能存在数据丢失的风险,因为:
\\nvolumes
或宿主机磁盘,但如果没有做好 数据持久化(如 RAID 备份、云盘挂载),当服务器宕机时,数据可能丢失或损坏。生产环境的推荐做法
\\n✅ 使用云服务的数据库:如 AWS RDS、阿里云 RDS、腾讯云 MySQL 代替 Docker 运行的 MySQL。
\\n✅ 使用云服务的 Redis/ES:如 阿里云 Redis、腾讯云 Redis、AWS ElastiCache,保证 高可用、自动备份和恢复。
\\n✅ 使用 Kubernetes 挂载持久化存储:如果必须在 Kubernetes 集群内运行 MySQL、Redis、ES,可以使用 Ceph、NFS、EBS(AWS)、Alibaba Cloud Disk 持久化存储,避免数据丢失。
🚨 不建议直接用 Docker 运行 MySQL、Redis、ES 在生产环境,除非:
\\n大家好,我是苏三,又跟大家见面了。
\\n某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。
\\nDEBUG日志显示问题出现在如下代码段:
\\n// 错误示例\\nBigDecimal amount = user.getWallet().getBalance().add(new BigDecimal(\\"100\\"));\\n
\\n此类链式调用若中间环节出现null值,必定导致NPE。
\\n初级阶段开发者通常写出多层嵌套式判断:
\\nif(user != null){\\n Wallet wallet = user.getWallet();\\n if(wallet != null){\\n BigDecimal balance = wallet.getBalance();\\n if(balance != null){\\n // 实际业务逻辑\\n }\\n }\\n}\\n
\\n这种写法既不优雅又影响代码可读性。
\\n那么,我们该如何优化呢?
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\nJava8之后,新增了Optional类,它是用来专门判空的。
\\n能够帮你写出更加优雅的代码。
\\n// 重构后的链式调用\\nBigDecimal result = Optional.ofNullable(user)\\n .map(User::getWallet)\\n .map(Wallet::getBalance)\\n .map(balance -> balance.add(new BigDecimal(\\"100\\")))\\n .orElse(BigDecimal.ZERO);\\n
\\n高级用法:条件过滤
\\nOptional.ofNullable(user)\\n .filter(u -> u.getVipLevel() > 3)\\n .ifPresent(u -> sendCoupon(u)); // VIP用户发券\\n
\\nBigDecimal balance = Optional.ofNullable(user)\\n .map(User::getWallet)\\n .map(Wallet::getBalance)\\n .orElseThrow(() -> new BusinessException(\\"用户钱包数据异常\\"));\\n
\\npublic class NullSafe {\\n \\n // 安全获取对象属性\\n public static <T, R> R get(T target, Function<T, R> mapper, R defaultValue) {\\n return target != null ? mapper.apply(target) : defaultValue;\\n }\\n \\n // 链式安全操作\\n public static <T> T execute(T root, Consumer<T> consumer) {\\n if (root != null) {\\n consumer.accept(root);\\n }\\n return root;\\n }\\n}\\n\\n// 使用示例\\nNullSafe.execute(user, u -> {\\n u.getWallet().charge(new BigDecimal(\\"50\\"));\\n logger.info(\\"用户{}已充值\\", u.getId());\\n});\\n
\\nSpring中自带了一些好用的工具类,比如:CollectionUtils、StringUtils等,可以非常有效的进行判空。
\\n具体代码如下:
\\n// 集合判空工具\\nList<Order> orders = getPendingOrders();\\nif (CollectionUtils.isEmpty(orders)) {\\n return Result.error(\\"无待处理订单\\");\\n}\\n\\n// 字符串检查\\nString input = request.getParam(\\"token\\");\\nif (StringUtils.hasText(input)) {\\n validateToken(input); \\n}\\n
\\n我们在日常开发中的entity对象,一般会使用Lombok框架中的注解,来实现getter/setter方法。
\\n其实,这个框架中也提供了@NonNull等判空的注解。
\\n比如:
\\n@Getter\\n@Setter\\npublic class User {\\n @NonNull // 编译时生成null检查代码\\n private String name;\\n \\n private Wallet wallet;\\n}\\n\\n// 使用构造时自动判空\\nUser user = new User(@NonNull \\"张三\\", wallet);\\n
\\npublic interface Notification {\\n void send(String message);\\n}\\n\\n// 真实实现\\npublic class EmailNotification implements Notification {\\n @Override\\n public void send(String message) {\\n // 发送邮件逻辑\\n }\\n}\\n\\n// 空对象实现\\npublic class NullNotification implements Notification {\\n @Override\\n public void send(String message) {\\n // 默认处理\\n }\\n}\\n\\n// 使用示例\\nNotification notifier = getNotifier();\\nnotifier.send(\\"系统提醒\\"); // 无需判空\\n
\\n其实Guava工具包中,给我们提供了Optional增强的功能。
\\n比如:
\\nimport com.google.common.base.Optional;\\n\\n// 创建携带缺省值的Optional\\nOptional<User> userOpt = Optional.fromNullable(user).or(defaultUser);\\n\\n// 链式操作配合Function\\nOptional<BigDecimal> amount = userOpt.transform(u -> u.getWallet())\\n .transform(w -> w.getBalance());\\n
\\nGuava工具包中的Optional类已经封装好了,我们可以直接使用。
\\n其实有些Assert断言类中,已经做好了判空的工作,参数为空则会抛出异常。
\\n这样我们就可以直接调用这个断言类。
\\n例如下面的ValidateUtils类中的requireNonNull方法,由于它内容已经判空了,因此,在其他地方调用requireNonNull方法时,如果为空,则会直接抛异常。
\\n我们在业务代码中,直接调用requireNonNull即可,不用写额外的判空逻辑。
\\n例如:
\\npublic class ValidateUtils {\\n public static <T> T requireNonNull(T obj, String message) {\\n if (obj == null) {\\n throw new ServiceException(message);\\n }\\n return obj;\\n }\\n}\\n\\n// 使用姿势\\nUser currentUser = ValidateUtils.requireNonNull(\\n userDao.findById(userId), \\n \\"用户不存在-ID:\\" + userId\\n);\\n
\\n最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n我们在一些特殊的业务场景种,可以通过自定义注解 + 全局AOP拦截器的方式,来实现实体或者字段的判空。
\\n例如:
\\n@Aspect\\n@Component\\npublic class NullCheckAspect {\\n \\n @Around(\\"@annotation(com.xxx.NullCheck)\\")\\n public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable {\\n Object[] args = joinPoint.getArgs();\\n for (Object arg : args) {\\n if (arg == null) {\\n throw new IllegalArgumentException(\\"参数不可为空\\");\\n }\\n }\\n return joinPoint.proceed();\\n }\\n}\\n\\n// 注解使用\\npublic void updateUser(@NullCheck User user) {\\n // 方法实现\\n}\\n
\\n// 旧代码(4层嵌套判断)\\nif (order != null) {\\n User user = order.getUser();\\n if (user != null) {\\n Address address = user.getAddress();\\n if (address != null) {\\n String city = address.getCity();\\n // 使用city\\n }\\n }\\n}\\n\\n// 重构后(流畅链式)\\nString city = Optional.ofNullable(order)\\n .map(Order::getUser)\\n .map(User::getAddress)\\n .map(Address::getCity)\\n .orElse(\\"未知城市\\");\\n
\\nList<User> users = userService.listUsers();\\n\\n// 传统写法(显式迭代判断)\\nList<String> names = new ArrayList<>();\\nfor (User user : users) {\\n if (user != null && user.getName() != null) {\\n names.add(user.getName());\\n }\\n}\\n\\n// Stream优化版\\nList<String> nameList = users.stream()\\n .filter(Objects::nonNull)\\n .map(User::getName)\\n .filter(Objects::nonNull)\\n .collect(Collectors.toList());\\n
\\n上面介绍的这些方案都可以使用,但除了代码的可读性之外,我们还需要考虑一下性能因素。
\\n下面列出了上面的几种在CPU消耗、内存只用和代码可读性的对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | CPU消耗 | 内存占用 | 代码可读性 | 适用场景 |
---|---|---|---|---|
多层if嵌套 | 低 | 低 | ★☆☆☆☆ | 简单层级调用 |
Java Optional | 中 | 中 | ★★★★☆ | 中等复杂度业务流 |
空对象模式 | 高 | 高 | ★★★★★ | 高频调用的基础服务 |
AOP全局拦截 | 中 | 低 | ★★★☆☆ | 接口参数非空验证 |
黄金法则
\\n除了,上面介绍的常规判空之外,下面再给大家介绍两种扩展的技术。
\\n虽然Java开发者无法直接使用,但可借鉴其设计哲学:
\\nval city = order?.user?.address?.city ?: \\"default\\"\\n
\\n// 模式匹配语法尝鲜\\nif (user instanceof User u && u.getName() != null) {\\n System.out.println(u.getName().toUpperCase());\\n}\\n
\\n总之,优雅判空不仅是代码之美,更是生产安全底线。
\\n本文分享了代码判空的10种方案,希望能够帮助你编写出既优雅又健壮的Java代码。
\\n如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 一、传统判空的血泪史\\n\\n某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。\\n\\nDEBUG日志显示问题出现在如下代码段:\\n\\n// 错误示例\\nBigDecimal amount = user.getWallet().getBalance().add(new BigDecimal(\\"100\\"));\\n\\n\\n此类链式调用若中间环节出现null值,必定导致NPE。\\n\\n初级阶段开发者通常写出多层嵌套式判断:\\n\\nif(user != null){\\n Wallet wallet = user.getWallet();…","guid":"https://juejin.cn/post/7478221220074504233","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T02:32:37.442Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"JavaScript继承探秘:从原型链到类的演变","url":"https://juejin.cn/post/7478199362242986034","content":"在JavaScript中,继承是通过原型链(prototype chain)来实现的。不同的继承方式各有特点,适用于不同的场景。本文将详细介绍几种常见的继承方法:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承以及使用ES6 class
关键字实现的继承,并提供相应的代码示例。
原型链继承是最基础的继承方式。它通过设置子类的原型为父类的一个实例来实现继承。
\\nfunction Parent() {\\n this.name = \'Parent\';\\n}\\n\\nParent.prototype.sayHello = function() {\\n console.log(\'Hello from \' + this.name);\\n};\\n\\nfunction Child() {}\\n\\n// 设置Child的原型为Parent的一个实例\\nChild.prototype = new Parent();\\nChild.prototype.constructor = Child;\\n\\nlet child = new Child();\\nchild.sayHello(); // 输出: Hello from Parent\\n
\\n优点: 子类可以继承父类原型上的方法。
\\n缺点: 所有子类实例共享同一个父类实例的状态,可能导致意外的数据共享问题。
\\n通过在子类构造函数内部调用父类构造函数,可以继承父类的属性。这种方式不会继承父类原型上的方法。
\\nfunction Parent(name) {\\n this.name = name;\\n}\\n\\nfunction Child(name, age) {\\n Parent.call(this, name); // 继承属性\\n this.age = age;\\n}\\n\\nlet child = new Child(\'Child\', 10);\\nconsole.log(child.name); // 输出: Child\\n
\\n优点: 可以继承父类的属性,且每个实例都有独立的状态。
\\n缺点: 不能继承父类原型上的方法。
\\n组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承模式之一。
\\nfunction Parent(name) {\\n this.name = name;\\n}\\n\\nParent.prototype.sayHello = function() {\\n console.log(\'Hello from \' + this.name);\\n};\\n\\nfunction Child(name, age) {\\n Parent.call(this, name); // 第二次调用Parent,继承属性\\n this.age = age;\\n}\\n\\n// 继承方法\\nChild.prototype = new Parent(); // 第一次调用Parent,继承方法\\nChild.prototype.constructor = Child;\\n\\nlet child = new Child(\'Child\', 10);\\nchild.sayHello(); // 输出: Hello from Child\\n
\\n优点: 既能继承父类的属性,也能继承父类的方法。
\\n缺点: 父类构造函数被调用了两次,影响性能。
\\n原型式继承利用一个空函数作为中介,避免直接修改原对象的原型。
\\nfunction createObject(o) {\\n function F() {}\\n F.prototype = o;\\n return new F();\\n}\\n\\nlet parent = {\\n name: \'Parent\',\\n sayHello: function() {\\n console.log(\'Hello from \' + this.name);\\n }\\n};\\n\\nlet child = createObject(parent);\\nchild.sayHello(); // 输出: Hello from Parent\\n
\\n优点: 简单易用,适合不需要创建多个实例的情况。
\\n缺点: 所有实例共享同一个原型对象的状态。
\\n寄生式继承在原型式继承的基础上,增强对象,返回构造函数。
\\nfunction createObject(o) {\\n function F() {}\\n F.prototype = o;\\n return new F();\\n}\\n\\nfunction inheritPrototype(child, parent) {\\n let prototype = createObject(parent.prototype); // 创建对象\\n prototype.constructor = child; // 增强对象\\n child.prototype = prototype; // 指定对象\\n}\\n\\nfunction Parent(name) {\\n this.name = name;\\n}\\n\\nParent.prototype.sayHello = function() {\\n console.log(\'Hello from \' + this.name);\\n};\\n\\nfunction Child(name, age) {\\n Parent.call(this, name);\\n this.age = age;\\n}\\n\\ninheritPrototype(Child, Parent);\\n\\nlet child = new Child(\'Child\', 10);\\nchild.sayHello(); // 输出: Hello from Child\\n
\\n优点: 解决了组合继承中父类构造函数被调用两次的问题。
\\n缺点: 相对复杂,理解成本较高。
\\nclass
关键字ES6引入了class
关键字简化了继承语法,使继承更加直观和易于理解。
class Parent {\\n constructor(name) {\\n this.name = name;\\n }\\n\\n sayHello() {\\n console.log(\'Hello from \' + this.name);\\n }\\n}\\n\\nclass Child extends Parent {\\n constructor(name, age) {\\n super(name); // 调用父类的构造函数\\n this.age = age;\\n }\\n}\\n\\nlet child = new Child(\'Child\', 10);\\nchild.sayHello(); // 输出: Hello from Child\\n
\\n优点: 语法简洁,易于理解和使用。
\\n缺点: 需要支持ES6及以上的环境。
\\n每种继承方式都有其适用场景和优缺点:
\\nclass
关键字:提供了简洁明了的语法,易于理解和使用,但需要支持ES6及以上版本。根据项目需求选择合适的继承方式,可以使代码更加清晰和高效。希望这篇文章能够帮助你更好地理解和应用JavaScript中的继承机制。
","description":"前言 在JavaScript中,继承是通过原型链(prototype chain)来实现的。不同的继承方式各有特点,适用于不同的场景。本文将详细介绍几种常见的继承方法:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承以及使用ES6 class关键字实现的继承,并提供相应的代码示例。\\n\\n1. 原型链继承\\n\\n原型链继承是最基础的继承方式。它通过设置子类的原型为父类的一个实例来实现继承。\\n\\nfunction Parent() {\\n this.name = \'Parent\';\\n}\\n\\nParent.prototype.sayHello = functio…","guid":"https://juejin.cn/post/7478199362242986034","author":"竺梓君","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T14:48:17.977Z","media":null,"categories":["后端","JavaScript"],"attachments":null,"extra":null,"language":null},{"title":"DeepSeek 让同事的bug无所遁形","url":"https://juejin.cn/post/7477921821284958248","content":"今天试试DeepSeek都能帮我识别到哪些bug,看看DeepSeek 实力如何。\\n插件我用的是MarsCode
,模型选择 DeepSeek R1
,今天看看都有哪些严重bug吧,这可比人工复查方便多了。
使用的是 MarsCode fix
功能,检查了两个项目,一个是线上运行的项目,一个是正在开发调试的项目。
检测出最多的问题就是NPE
,这个东西也是java程序员遇到过最多的一个错误。很多地方实际上从业务上就避免了NPE
这个东西对于初级开发👦来说在调试过程中用处非常大🚀,还有就是我们在开发过程帮我检测XML
还是不错的,我发现,对于中高级开发程序员🧓来说,xml
是最容易出现问题的,因为集成工具不能直接检测到一些语法问题。
当然,AI给出的建议还是需要我们具备辨别的能力,下面看看我用AI 扫描出的一些问题,以及AI 给出的一些错误建议
当然还有很多问题,其他问题从业务上应该是不会报错,这个inputStream
关闭方式确实 存在很大问题,和业务无关,纯技术问题。
public void exportData(Long exportRecordId) throws Exception {\\n ExportRecord exportRecord = exportRecordService.getById(exportRecordId);\\n try {\\n ................... 查询数据................\\n IPage<Record> page = service.queryPage(req);\\n String excelName = \\"本年记录\\" + System.currentTimeMillis() + \\".xlsx\\";\\n String excelNameFilePath = ExportConstant.EXPORT_TEMP_FILE_PATH_STAR_CURRENT_APPLY + excelName;\\n File file = new File(excelNameFilePath);\\n if (file.exists()) {\\n FileUtil.del(file);\\n } else {\\n FileUtil.touch(file);\\n }\\n .......................封装数据.............\\n // 5.4、读写内容到excel。\\n EasyExcel.write(file).head(headerList).sheet().doWrite(rowsList);\\n\\n // 6、将excel文件上传文件服务器。\\n InputStream inputStream = new FileInputStream(file);\\n // 上传到minio,upload 方法有关闭 inputStream.close(),没有处理IOException\\n String uploadFilePath = MinioUtil.upload(inputStream, excelNameFilePath.substring(1));\\n \\n // 7、最后删除临时文件。\\n FileUtil.del(file);\\n \\n }catch (Exception e){\\n e.printStackTrace();\\n log.error(\\"导出失败\\",e);\\n }\\n}\\n
\\n\\n\\n最近不是在裁员么,昨天临时接手了一个半成品项目,发现SQL 在报错,于是就扫描了一下。果然没有测试的代码,用AI测试还是挺不错的。直接帮我把问题都扫描出来了
\\n
AI主要修复点说明:
\\n<if test=\\"req.workUnitNature != null and \\">改为<if test=\\"req.workUnitNature != null\\">
,移除无效的and判断if (!allDeptsNotEmpty) {\\n // 如果有任意一个deptId为空,抛出异常或返回错误信息\\n throw new JedisDataException(\\"部门需要全部配置\\");\\n}\\n
\\nAI修复说明如下:\\n
提醒了我加事务,但是事务方法不能 用 private
,并且我这个方法还是本类的 this
调用的
and company like (\'%\'<![CDATA[||]]>#{req.company}<![CDATA[||]]>\'%\')\\n
\\nAI优化建议:
\\nand company like concat(\'%\', #{req.company}, \'%\') <!-- 优化like语法 --\x3e\\n\\n
\\nconcat
在某些库不支持两个参数
希望大家在开发过程中,经常使用AI
插件 FIX
的功能,能减少很多粗心大意 引起的bug,能提供我们自测 和 联调的效率。
最后AI
的优化建议,也有可能发生错误,所以我们得具备辨别的能力。
大家好,我是小林。
\\n有读者跟我说,看腻了互联网面经,想看看国企的软开面试,想针对性准备一下。
\\n说来就来!这次带大家看看中国移动的面经。
\\n中国移动校招年总包有 20w+,不过可能实际每月到手可能是 1w 多一些,因为很多平摊到奖金和公积金里面了,所以一共加起来20万左右。
\\n中国移动的面试相比互联网中大厂的面试强度会弱一些,通常一场技术面可能是 20 分钟左右,相比互联网中大厂能少 50-60%的强度,所以难度不会太难。
\\n我一般会建议想冲央国企的同学,如果一开始按照中大厂面试难度准备的话,那么后面去面央国企就会感觉很简单,有一种降维打击的感觉了。
\\n这次,我们来看看中国移动 Java 软开的校招一面,主要是问了 Java、Spring、数据结构与算法这三大块知识了,这场面试就问了 8 个技术问题,面试时长是 15 分钟。
\\n添加图片注释,不超过 140 字(可选)
\\n首先,Java的优势,我记得跨平台应该是一个大点,因为JVM的存在,一次编写到处运行。然后面向对象,这个可能也是优势,不过现在很多语言都支持面向对象,但是Java的设计从一开始就是OOP的。还有强大的生态系统,比如Spring框架,Hibernate,各种库和工具,社区支持大,企业应用广泛。另外,内存管理方面,自动垃圾回收机制,减少了内存泄漏的问题,对开发者友好。还有多线程支持,内置的线程机制,方便并发编程。安全性方面,Java有安全模型,比如沙箱机制,适合网络环境。还有稳定性,企业级应用长期使用,版本更新也比较注重向后兼容。
\\n劣势的话,性能可能是一个,虽然JVM优化了很多,但相比C++或者Rust这种原生编译语言,还是有一定开销。特别是启动时间,比如微服务场景下,可能不如Go之类的快。语法繁琐,比如样板代码多,之前没有lambda的时候更麻烦,现在有了但比起Python还是不够简洁。内存消耗,JVM本身占内存,对于资源有限的环境可能不太友好。还有面向对象过于严格,有时候写简单程序反而麻烦,虽然Java8引入了函数式编程,但不如其他语言自然。还有开发效率,相比动态语言如Python,Java需要更多代码,编译过程也可能拖慢开发节奏。
\\n在传统编程中,当一个类需要使用另一个类的对象时,通常会在该类内部通过new关键字来创建依赖对象,这使得类与类之间的耦合度较高。
\\n而依赖注入则是将对象的创建和依赖关系的管理交给 Spring 容器来完成,类只需要声明自己所依赖的对象,容器会在运行时将这些依赖对象注入到类中,从而降低了类与类之间的耦合度,提高了代码的可维护性和可测试性。
\\n具体到Spring中,常见的依赖注入的实现方式,比如构造器注入、Setter方法注入,还有字段注入。
\\n@Service\\npublic class UserService {\\n private final UserRepository userRepository;\\n \\n // 构造器注入(Spring 4.3+ 自动识别单构造器,无需显式@Autowired)\\n public UserService(UserRepository userRepository) {\\n this.userRepository = userRepository;\\n }\\n}\\n
\\npublic class PaymentService {\\n private PaymentGateway gateway;\\n \\n @Autowired\\n public void setGateway(PaymentGateway gateway) {\\n this.gateway = gateway;\\n }\\n}\\n
\\n@Service\\npublic class OrderService {\\n @Autowired\\n private OrderRepository orderRepository;\\n}\\n
\\nMyBatis 在 SQL 灵活性、动态 SQL 支持、结果集映射和与 Spring 整合方面表现卓越,尤其适合重视 SQL 可控性的项目。
\\n<!-- 示例:XML 中定义 SQL --\x3e\\n<select id=\\"findUserWithRole\\" resultMap=\\"userRoleMap\\">\\n SELECT u.*, r.role_name \\n FROM user u \\n LEFT JOIN user_role ur ON u.id = ur.user_id\\n LEFT JOIN role r ON ur.role_id = r.id \\n WHERE u.id = #{userId}\\n</select>\\n
\\n<select id=\\"searchUsers\\" resultType=\\"User\\">\\n SELECT * FROM user\\n <where>\\n <if test=\\"name != null\\">AND name LIKE #{name}</if>\\n <if test=\\"status != null\\">AND status = #{status}</if>\\n </where>\\n</select>\\n
\\n<resultMap id=\\"userRoleMap\\" type=\\"User\\">\\n <id property=\\"id\\" column=\\"user_id\\"/>\\n <result property=\\"name\\" column=\\"user_name\\"/>\\n <collection property=\\"roles\\" ofType=\\"Role\\">\\n <result property=\\"roleName\\" column=\\"role_name\\"/>\\n </collection>\\n</resultMap>\\n
\\n@Intercepts({\\n @Signature(type=Executor.class, method=\\"query\\", args={...})\\n})\\npublic class PaginationPlugin implements Interceptor {\\n // 实现分页逻辑\\n}\\n
\\n@Configuration\\n@MapperScan(\\"com.example.mapper\\")\\npublic class MyBatisConfig {\\n // 数据源和 SqlSessionFactory 配置\\n}\\n
\\n可以通过这些方法来保证:
\\n1、可变性 :String 是不可变的(Immutable),一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder 和 StringBuffer 是可变的(Mutable),可以直接对字符串内容进行修改而不会创建新对象。
\\n2、线程安全性 :String 因为不可变,天然线程安全。StringBuilder 不是线程安全的,适用于单线程环境。StringBuffer 是线程安全的,其方法通过 synchronized 关键字实现同步,适用于多线程环境。
\\n3、性能 :String 性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder 性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer 性能略低于 StringBuilder,因为它的线程安全机制引入了同步开销。
\\n4、使用场景 :如果字符串内容固定或不常变化,优先使用 String。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer。
\\n对比总结如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
不可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是(因不可变) | 否 | 是(同步方法) |
性能 | 低(频繁修改时) | 高(单线程) | 中(多线程安全) |
适用场景 | 静态字符串 | 单线程动态字符串 | 多线程动态字符串 |
例子代码如下:
\\n// String的不可变性\\nString str = \\"abc\\";\\nstr = str + \\"def\\"; // 新建对象,str指向新对象\\n\\n// StringBuilder(单线程高效)\\nStringBuilder sb = new StringBuilder();\\nsb.append(\\"abc\\").append(\\"def\\"); // 直接修改内部数组\\n\\n// StringBuffer(多线程安全)\\nStringBuffer sbf = new StringBuffer();\\nsbf.append(\\"abc\\").append(\\"def\\"); // 同步方法保证线程安全\\n
\\n在 Java 中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定:
\\nhashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。
\\n操作 | AVL 树 | 红黑树 |
---|---|---|
查询 | ⭐⭐⭐⭐⭐(更快) | ⭐⭐⭐⭐ |
插入/删除 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐(更快) |
平衡开销 | 高 | 低 |
1、查询性能的对比:
\\n在查询性能上,AVL 树由于其严格的平衡特性,表现会稍好于红黑树,但差距通常不大。
\\n2、插入性能的对比:
\\n在插入性能上,红黑树由于其弱平衡特性,表现优于 AVL 树。
\\n在实际应用中,如果查询操作频繁,对查询性能要求较高,且插入和删除操作相对较少,可以选择 AVL 树;如果插入和删除操作较为频繁,对插入性能有较高要求,同时查询性能也能接受一定的损耗,则红黑树是更好的选择。例如,Java 中的 TreeMap 和 TreeSet 底层使用的就是红黑树,以兼顾插入、删除和查询操作的性能。
\\n取常见的解决方案有几种:
\\nimport java.util.Arrays;\\n\\npublic class TopKBySorting {\\n public static int[] topK(int[] arr, int k) {\\n Arrays.sort(arr);\\n int n = arr.length;\\n int[] result = new int[k];\\n for (int i = 0; i < k; i++) {\\n result[i] = arr[n - k + i];\\n }\\n return result;\\n }\\n\\n public static void main(String[] args) {\\n int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};\\n int k = 3;\\n int[] topK = topK(arr, k);\\n for (int num : topK) {\\n System.out.print(num + \\" \\");\\n }\\n }\\n}\\n
\\nimport java.util.PriorityQueue;\\n\\npublic class TopKByMinHeap {\\n public static int[] topK(int[] arr, int k) {\\n PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);\\n for (int num : arr) {\\n if (minHeap.size() < k) {\\n minHeap.offer(num);\\n } else if (num > minHeap.peek()) {\\n minHeap.poll();\\n minHeap.offer(num);\\n }\\n }\\n int[] result = new int[k];\\n for (int i = k - 1; i >= 0; i--) {\\n result[i] = minHeap.poll();\\n }\\n return result;\\n }\\n\\n public static void main(String[] args) {\\n int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};\\n int k = 3;\\n int[] topK = topK(arr, k);\\n for (int num : topK) {\\n System.out.print(num + \\" \\");\\n }\\n }\\n}\\n
\\nimport java.util.Arrays;\\n\\npublic class TopKByQuickSelect {\\n public static int[] topK(int[] arr, int k) {\\n quickSelect(arr, 0, arr.length - 1, k);\\n int[] result = Arrays.copyOfRange(arr, arr.length - k, arr.length);\\n Arrays.sort(result);\\n return result;\\n }\\n\\n private static void quickSelect(int[] arr, int left, int right, int k) {\\n if (left < right) {\\n int pivotIndex = partition(arr, left, right);\\n if (pivotIndex > arr.length - k) {\\n quickSelect(arr, left, pivotIndex - 1, k);\\n } else if (pivotIndex < arr.length - k) {\\n quickSelect(arr, pivotIndex + 1, right, k);\\n }\\n }\\n }\\n\\n private static int partition(int[] arr, int left, int right) {\\n int pivot = arr[right];\\n int i = left - 1;\\n for (int j = left; j < right; j++) {\\n if (arr[j] < pivot) {\\n i++;\\n swap(arr, i, j);\\n }\\n }\\n swap(arr, i + 1, right);\\n return i + 1;\\n }\\n\\n private static void swap(int[] arr, int i, int j) {\\n int temp = arr[i];\\n arr[i] = arr[j];\\n arr[j] = temp;\\n }\\n\\n public static void main(String[] args) {\\n int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};\\n int k = 3;\\n int[] topK = topK(arr, k);\\n for (int num : topK) {\\n System.out.print(num + \\" \\");\\n }\\n }\\n}\\n
\\n具体怎么选,可以根据具体需求选择合适的方法:
\\n如果 K 很小,推荐使用 最小堆 方法,其时间复杂度为 O(NlogK)。
\\n如果 K 较大或需要线性时间复杂度,推荐使用 快速选择 方法,其平均时间复杂度为 O(N)。
\\n如果对实现复杂度要求较低且 K 接近 N,可以直接使用 排序法 ,时间复杂度为 O(NlogN)。
\\n\\n","description":"大家好,我是小林。 有读者跟我说,看腻了互联网面经,想看看国企的软开面试,想针对性准备一下。\\n\\n说来就来!这次带大家看看中国移动的面经。\\n\\n中国移动校招年总包有 20w+,不过可能实际每月到手可能是 1w 多一些,因为很多平摊到奖金和公积金里面了,所以一共加起来20万左右。\\n\\n中国移动的面试相比互联网中大厂的面试强度会弱一些,通常一场技术面可能是 20 分钟左右,相比互联网中大厂能少 50-60%的强度,所以难度不会太难。\\n\\n我一般会建议想冲央国企的同学,如果一开始按照中大厂面试难度准备的话,那么后面去面央国企就会感觉很简单,有一种降维打击的感觉了。\\n\\n这次…","guid":"https://juejin.cn/post/7477931159524524095","author":"小林coding","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T06:44:56.823Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88f0a8a90b72464ea84a745eb4711f7b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p6XY29kaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1741761896&x-signature=%2FXEsxuLmnclC%2FCxFASPJbHIrL8g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/218471a1193047738193333cde84936a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p6XY29kaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1741761896&x-signature=qOxzzGjGbW2qk%2BAegcK4qQmqUBA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"程序员面试要怎么描述项目经历?","url":"https://juejin.cn/post/7477921837684195364","content":"\\n
\\n- 《图解网络》 :500 张图 + 15 万字贯穿计算机网络重点知识,如HTTP、HTTPS、TCP、UDP、IP等协议
\\n- 《图解系统》:400 张图 + 16 万字贯穿操作系统重点知识,如进程管理、内存管理、文件系统、网络系统等
\\n- 《图解MySQL》:重点突击 MySQL 索引、存储引擎、事务、MVCC、锁、日志等面试高频知识
\\n- 《图解Redis》:重点突击 Redis 数据结构、持久化、缓存淘汰、高可用、缓存数据一致性等面试高频知识
\\n- 《Java后端面试题》:涵盖Java基础、Java并发、Java虚拟机、Spring、MySQL、Redis、计算机网络等企业面试题
\\n- 《大厂真实面经》:涵盖互联网大厂、互联网中厂、手机厂、通信厂、新能源汽车厂、银行等企业真实面试题
\\n
文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!
\\n架构实战案例解析
\\n\\n\\n资料链接:url81.ctfile.com/f/57345181-…
\\n访问密码:3899
\\n
大家好呀,我是飞鱼。
\\n请分享一下你做过的 XX 项目?这基本是每个面试都会被问到的。
\\n但很多人讲项目经历时都不得要领,面试官听完也是一头雾水,根本不清楚你具体做了啥,你的优势在哪?
\\n面试官问这个问题,主要是想考察你是否真的参与过该项目:
\\n\\n\\n❝
\\n你的能力和经验能不能迁移到新工作里,还有你的表达、汇报与沟通能力咋样。
\\n
很多面试Java开发岗的小伙伴以为,做的项目多,用的中间件多,就能让面试官眼前一亮?
\\n其实有所偏差,项目在于精,不在于多,面试就那么1个小时,只够你深入聊1,2个项目。
\\n怎么描述清楚一个项目呢?
\\n\\n\\n❝
\\n项目经历可以参考 STAR 原则。
\\n项目背景(S):
\\n\\n
\\n- 在XX大背景下,我们遇到了XXX痛点,急需这个项目来拯救。
\\n个人职责(T):
\\n\\n
\\n- 讲清楚负责XX模块的方案设计,开发,测试,全程参与。
\\n关键需求点及其解决方案(A):
\\n\\n
\\n- 简历上用陈述句描述:用XX方案解决XX问题。
\\n- 面试时思路要清晰,列举出当前需求点的所有解决方案。
\\n- 首先对方案逐一简述,再从业务满足度、实现成本、可维护性、可扩展性等多个维度对比,推演出最终选型。
\\n项目成果(R):
\\n\\n
\\n- 项目亮点要突出,比如: 沉淀了XX通用组件,以后类似需求接入时,开发成本节省X人日。
\\n- 重构了XX代码,可读性、严谨性、可扩展性显著提升。
\\n
日常做项目时就要多思考、多练习,培养从多个方案中推演出最合适方案的能力,面试时才能游刃有余。
\\n注意面试中最好不要使用假项目经验:
\\n\\n\\n❝
\\n有些程序员求职者想尽办法将培训经历写成实际的项目经验,甚至有些人直接从网上找一个自己根本没有做过的项目写上去。
\\n如果你对项目确实非常了解,项目中的一些细节都能讲清楚也行,但是,大多数时候,假项目经验总会露出马脚。
\\n如果在求职的过程中你确实没有真实的项目经历,你在简历上写上这个项目的时候,最好自己完整做一遍。
\\n如果项目确实太大,挑核心功能做,做的过程中你才能了解具体的细节以及技术难点,不至于在面试过程中一问三不知。
\\n
有啥其他看法,欢迎在评论区留言讨论。
\\n\\n\\n❝
\\n想看技术文章的,可以去我的个人网站:hardyfish.top/
\\n\\n
\\n- 目前网站的内容足够应付基础面试(
\\nP7
)了!
题目描述
\\n\\n\\n❝
\\n假设你正在爬楼梯,需要
\\nn
阶你才能到达楼顶。每次你可以爬
\\n1
或2
个台阶,你有多少种不同的方法可以爬到楼顶呢?
示例 1:
\\n输入:n = 2\\n输出:2\\n解释:有两种方法可以爬到楼顶。\\n1. 1 阶 + 1 阶\\n2. 2 阶\\n
\\n示例 2:
\\n输入:n = 3\\n输出:3\\n解释:有三种方法可以爬到楼顶。\\n1. 1 阶 + 1 阶 + 1 阶\\n2. 1 阶 + 2 阶\\n3. 2 阶 + 1 阶\\n
\\n解题思路
\\n\\n\\n❝
\\n动态规划
\\n
代码实现
\\nJava
代码:
class Solution {\\n public int climbStairs(int n) {\\n int[] dp = new int[n + 1];\\n dp[0] = 1;\\n dp[1] = 1;\\n for(int i = 2; i <= n; i++) {\\n dp[i] = dp[i - 1] + dp[i - 2];\\n }\\n return dp[n];\\n }\\n}\\n
","description":"文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识! 架构实战案例解析\\n\\n资料链接:url81.ctfile.com/f/57345181-…\\n\\n访问密码:3899\\n\\n大家好呀,我是飞鱼。\\n\\n请分享一下你做过的 XX 项目?这基本是每个面试都会被问到的。\\n\\n但很多人讲项目经历时都不得要领,面试官听完也是一头雾水,根本不清楚你具体做了啥,你的优势在哪?\\n\\n面试官问这个问题,主要是想考察你是否真的参与过该项目:\\n\\n❝\\n\\n你的能力和经验能不能迁移到新工作里,还有你的表达、汇报与沟通能力咋样。\\n\\n很多面试Java开发岗的小伙伴以为,做的项目多,用的中间件多…","guid":"https://juejin.cn/post/7477921837684195364","author":"程序员飞鱼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T06:05:23.870Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"线上问题-我就加了个索引怎么就导致线上事故了","url":"https://juejin.cn/post/7477916023971315738","content":"不好啦❗ 天塌了❗ 系统崩了❗
\\n快看啊,一个上线5年的业务,今天发个版突然就崩了
\\n生产问题群爆炸了
\\n\\n\\n我的心里活动:“太好了😀太好了😀终于给我碰上了,这个问题可很少发生啊,又积累血琳琳的生产一个问题”
\\n
不想看废话的直接看【解决过程和方案】 吧
\\n// 问题sql\\n\\nselect * from w_location where \\nlocation_type = \'SORTING_TEMPORARY\' \\nAND \\nstatus = \'1\'\\n\\n
\\n问题就是这个sql 在线上查不到数据从而导致这个业务异常抛出。\\n但是我直连线上数据库发现这个条件是有数据的
\\n然后查看发版记录,本次版本迭代改的跟这个功能毫无关系,甚至都没有发这个服务。真是奇了个怪,但是线上这里突然还是个必现的问题
\\n\\nCREATE TABLE `w_location` (\\n `id` bigint(19) NOT NULL COMMENT \'主键id\',\\n `warehouse_id` bigint(19) DEFAULT NULL COMMENT \'仓库id\',\\n `warehouse_code` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'仓库code\',\\n `location_code` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'库位编码\',\\n `tunnel_id` bigint(19) DEFAULT NULL COMMENT \'巷道id\',\\n `entity_type` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'实体类型\',\\n `high_low_type` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'高低类型\',\\n `row_number` tinyint(4) unsigned DEFAULT NULL COMMENT \'排\',\\n `column_number` tinyint(4) unsigned DEFAULT NULL COMMENT \'列\',\\n `floor_number` tinyint(4) unsigned DEFAULT NULL COMMENT \'层\',\\n `grid` char(1) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'格\',\\n `emp_loc` bit(1) DEFAULT NULL COMMENT \'是否空库位 0:不是空库位,1:是空库位\',\\n `location_classify_id` bigint(19) DEFAULT NULL COMMENT \'库位分类id\',\\n `status` bit(1) DEFAULT NULL COMMENT \'状态(1:生效,0:失效)\',\\n `load_id` bigint(19) DEFAULT NULL COMMENT \'库位承载id\',\\n `area_id` bigint(19) DEFAULT NULL COMMENT \'工作区id\',\\n `zone_id` bigint(19) DEFAULT NULL COMMENT \'库区id\',\\n `location_type` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'库位类型\',\\n `picking_sequence` mediumint(7) DEFAULT NULL COMMENT \'拣货动线号\',\\n `is_deleted` bit(1) DEFAULT NULL COMMENT \'是否删除\',\\n `inventory_sequence` mediumint(7) DEFAULT NULL COMMENT \'盘点动线号\',\\n `create_time` datetime DEFAULT NULL COMMENT \'创建时间\',\\n `create_user` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'创建人\',\\n `update_time` datetime DEFAULT NULL COMMENT \'更新时间\',\\n `update_user` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'更新人\',\\n `tenant_code` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT \'租户编码\',\\n `actual_temp_id` bigint(19) DEFAULT NULL COMMENT \'实际温层 ID\',\\n `actual_temp_code` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'实际温层 CODE\',\\n `create_nick_name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'创建用户姓名\',\\n `update_nick_name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'更新用户姓名\',\\n `production_line_id` bigint(19) DEFAULT NULL COMMENT \'生产线ID\',\\n `production_line_name` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'生产线名称\',\\n `instance_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'实例编码\',\\n `empty_flag` varchar(1) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'Y=空库位,N=非空库位;供点检使用\',\\n `picking_sequence_rearrangement` bigint(19) DEFAULT NULL COMMENT \'重排的拣货动线号\',\\n `inventory_sequence_rearrangement` bigint(19) DEFAULT NULL COMMENT \'重排的盘点动线号\',\\n `location_purpose` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT \'库位用途\',\\n `casual_pick_loc` bit(1) DEFAULT b\'0\' COMMENT \'是否临时拣货位\',\\n PRIMARY KEY (`id`) USING BTREE,\\n KEY `idx_wh_location_type` (`warehouse_code`,`location_code`,`location_type`),\\n KEY `index_location_type_status` (`location_type`,`status`)\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT=\'库位\';\\n\\n\\nselect * from w_location where location_type = \'SORTING_TEMPORARY\' AND status = \'1\'\\n\\n\\n
\\n这个 status 字段是int类型 怎么就参数还搞个字符串
\\n然后还有 KEY index_location_type_status
(location_type
,status
) 这个索引
这让我一下子就联想到之前学习mysql时的一个知识点, 索引+隐式数据转换会带来意想不到的问题
\\n紧急联系DBA 把发版的新增的索引给干掉
\\n\\n\\nMySQL中隐式转换详细查看官方文档相关的说明:
\\n\\n
根据现象分析,根本原因是MySQL的bit类型字段在索引查询时的隐式类型转换规则导致的。以下是具体解释:
\\n问题核心原因:
\\nbit类型存储特性:
\\nstatus字段bit(1)实际存储的是二进制值:\\nTRUE存储为 b\'1\'(二进制值)\\nFALSE存储为 b\'0\'
\\n索引影响下的隐式转换:
\\nstatus=\'1\' → 需要将bit转为字符串比较(b\'1\' → \\"1\\")\\nstatus=1 → 将bit转为整数比较(b\'1\' → 1)
\\n两者都会做全表扫描,MySQL统一进行类型转换
\\nmysql 隐式转换的坑还有很多,并且隐式转化有很多默认规则,这个我们控制不来。\\n我们能做到的就是尽量避免隐式转换
","description":"前言 不好啦❗ 天塌了❗ 系统崩了❗\\n\\n快看啊,一个上线5年的业务,今天发个版突然就崩了\\n\\n生产问题群爆炸了\\n\\n我的心里活动:“太好了😀太好了😀终于给我碰上了,这个问题可很少发生啊,又积累血琳琳的生产一个问题”\\n\\n不想看废话的直接看【解决过程和方案】 吧\\n\\n排查过程\\n\\n// 问题sql\\n\\nselect * from w_location where \\nlocation_type = \'SORTING_TEMPORARY\' \\nAND \\nstatus = \'1\'\\n\\n\\n\\n问题就是这个sql 在线上查不到数据从而导致这个业务异常抛出…","guid":"https://juejin.cn/post/7477916023971315738","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T02:57:24.875Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/900bcc5baae44d1c99cb8ead189e14cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=9QWcv7B68od98gq%2BILFReKmgANE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/afba744a56714f7a80091f74f05ae1f4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=rWkjufxp%2FES5vGCO6aAT6SyaY%2Bk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6fa8378135d648358846d2c75d535fc9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=aA%2B%2FJ4zopiu6lk8PadkjSrFLEtw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/511441c6db9a4364a52168d40df68629~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=yjkL09T0X2fiGA8fvT1NSq7hHus%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0311aace672b49ab9a8700a298bb604d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=VJYpa7KVSi5b4rERAjNGikpjKpk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f42e25b5bb4c4c6cae26c743f310b12f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=707Q%2Fo3BpixBN9NhgmIzfltaRcM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/820117feb6d54aaa9a6263a8e4a5fddb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741750665&x-signature=znpkmFWhL8ERIWBfltAQMOA1KRU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"如何一眼定位SQL的代码来源:一款SQL染色标记的简易MyBatis插件","url":"https://juejin.cn/post/7477884622836498451","content":"作者:京东物流 郭忠强
\\n\\n\\n本文分析了后端研发和运维在日常工作中所面临的线上SQL定位排查痛点,基于姓名贴的灵感,设计和开发了一款SQL染色标记的MyBatis插件。该插件轻量高效,对业务代码无侵入,接入简单,支持SELECT、INSERT、UPDATE、DELETE等语句,同时也支持无WHERE条件SQL的标记增强。该SQL染色插件并不改变SQL指纹,染色信息内置了statementId、PFinderId,方便分布式跟踪和定位。此外,还提供了附加信息的传递入口,方便用户进行自定义信息染色,例如客户端的执行线程id等。期望在大家面临类似痛点时提供一些实践经验和参考,也欢迎大家合适的场景下接入使用。
\\n
作为后端开发,不可避免地与SQL打交道,一个大型复杂系统中往往会有大量的SQL语句支撑业务,而且单表所涉及的不同SQL可能也多达几十个甚至上百个。
\\n当看到一个SQL时,如何快速识别这个SQL是哪块业务的?具体是哪个方法走到了这个SQL?
\\n这些SQL是凭个人大脑无法全部记住的,而且业务在不断发展,SQL语句本身也在不断地变化,可能明天增多一个表的join,后天增多了几个where条件限制,大后天减少了几个字段……
\\nSQL本身也是支持动态拼接形成,当看到一个SQL时,如何快速定位是来自哪块具体业务?这是个问题,也是个难题。
\\n以下面的报表查询SQL为例:
\\nSELECT\\nCOUNT( *)\\nFROM\\nst_stock m\\nINNER JOIN st_lot_shelf_life slsl\\nON\\nm.tenant_code = slsl.tenant_code\\nAND m.sku = slsl.sku\\nAND m.lot_no = slsl.lot_no\\nAND slsl.deleted = 0\\nWHERE\\nm.deleted = 0\\nAND m.stock_qty > 0\\nAND m.warehouse_no = ?\\nAND m.lot_no != \'-1\'\\nAND m.owner_no IN(?)\\n
\\n我经常会面临这种根据SQL定位分析业务来源的问题,尤其是在慢SQL分析治理时,往往会存在类似的痛点。
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n我们日常看到一些工作人员的制服上会配备姓名贴,这样很有辨识度,通过姓名贴我们可以一看就可以看出来当前的工作人员是哪位同事。
\\n在此启发下,我认为对SQL也可以进行一些染色标记增强,通过这些标记可以一眼看出来这个SQL是哪些业务产生的。
\\n我这里考虑采用MyBatis Plugini机制进行SQL染色增强,可以达到业务零侵入的效果:不改业务代码、不改业务SQL,做到SQL无感增强,自动染色。
\\n用什么来区分SQL的唯一性呢?这个区分的标识区分度越高,越容易达到“一眼就看出来SQL来源”的效果。
\\n对此,我采用SQL statement的id来作为唯一标识。SQL statement是有两部分组成:mapper namespace + SQL id,通过SQL statement的id基本上可以唯一确定程序中的SQL在mapper文件中的位置,顺便可以找到对应的DAO方法,及其追溯到上层调用来源和业务场景。
\\n\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\nSQL染色增强,这里是通过将附加信息作为SQL注释,对SQL拼接改写。
\\n因为增加的部分是SQL注释,不影响SQL的执行正确性,也不会改写SQL指纹,对于慢SQL排查定位、死锁日志SQL排查都有帮助。
\\n这里是对SQL执行前进行染色增强,所以拦截StatementHandler的StatementHandler方法即可。
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\nSQL的修改核心代码片段:
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n插件除了会自动拼接statementId和pFinderId外,还预留了一个ThreadLocal变量,允许使用者执行线程的上线文中向SQL传递附加信息,比如SQL的执行用户ERP、执行线程的id等。
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n用法示例:
\\n// 其他代码\\nSQLMarkingThreadLocal.put(\\"operator\\", UserInfoUtil.getUserCode());\\n// 其他代码\\nSQLMarkingThreadLocal.remove();\\n// 其他代码\\n
\\n用户也可以通过自定义切面方式自动赋值这些附加信息。
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n2025-02-11 00:27:19.982 [http-nio-8082-exec-7] DEBUG [pfinderId:4630283.56667.17392048399060130] org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137 - c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport\\n==> Preparing: SELECT m.id, m.sku, m.location_no locationNo, m.container_level_1 containerLevel1, m.container_level_2 containerLevel2, m.lot_no lotNo, m.sku_level skuLevel, m.owner_no ownerNo, m.pack_code packCode, m.stock_qty stockQty, m.prepicked_qty prePickedQty, m.premoved_qty preMovedQty, m.frozen_qty frozenQty, m.diff_qty diffQty, m.broken_qty brokenQty, m.status, m.create_time as createTime, m.update_time as updateTime, m.update_user as updateUser, m.create_user as createUser, stock_qty - (prepicked_qty + premoved_qty + frozen_qty + diff_qty + broken_qty) availableQty, (prepicked_qty + premoved_qty + frozen_qty + diff_qty + broken_qty) noAvailableQty, m.zone_no zoneNo, m.zone_type zoneType, slsl.shelf_life_status shelfLifeStatus, slsl.left_days leftDays, slsl.production_date productionDate, slsl.expiration_date expirationDate, slsl.shelf_life_days shelfLifeDays, slsl.warning_days warningDays, slsl.regular_advent_days regularAdventDays, slsl.urgent_advent_days urgentAdventDays, slsl.advent_days adventDays, slsl.extend_content extendContent FROM st_stock m INNER JOIN st_lot_shelf_life slsl ON m.tenant_code = slsl.tenant_code AND m.sku = slsl.sku AND m.lot_no = slsl.lot_no AND slsl.deleted = 0 WHERE m.deleted = 0 AND m.stock_qty > 0 AND m.warehouse_no = ? AND m.lot_no != \'-1\' LIMIT ? /* [SQLMarking] statementId: com.jdwl.wms.stock.infrastructure.jdbc.report.dao.StockShelfLifeReportDao.selectStockShelfLifeReport, pFinderId: 4630283.56667.17392048399060130, operator: guozhongqiang5, traceId: 59f48d4d-5346-4ffe-9837-693a090090fc */\\n2025-02-11 00:27:19.982 [http-nio-8082-exec-7] DEBUG [pfinderId:4630283.56667.17392048399060130] org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137 - c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport\\n==> Parameters: 6_975(String), 10(Integer)\\n2025-02-11 00:27:19.988 [http-nio-8082-exec-7] DEBUG [pfinderId:4630283.56667.17392048399060130] org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137 - c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport\\n<== Total: 10\\n
\\n\ufeff
\\nSELECT\\nm.id,\\nm.sku,\\nm.location_no locationNo,\\nm.container_level_1 containerLevel1,\\nm.container_level_2 containerLevel2,\\nm.lot_no lotNo,\\nm.sku_level skuLevel,\\nm.owner_no ownerNo,\\nm.pack_code packCode,\\nm.stock_qty stockQty,\\nm.prepicked_qty prePickedQty,\\nm.premoved_qty preMovedQty,\\nm.frozen_qty frozenQty,\\nm.diff_qty diffQty,\\nm.broken_qty brokenQty,\\nm.status,\\nm.create_time AS createTime,\\nm.update_time AS updateTime,\\nm.update_user AS updateUser,\\nm.create_user AS createUser,\\nstock_qty -(prepicked_qty + premoved_qty + frozen_qty + diff_qty + broken_qty) availableQty,\\n(prepicked_qty + premoved_qty + frozen_qty + diff_qty + broken_qty) noAvailableQty,\\nm.zone_no zoneNo,\\nm.zone_type zoneType,\\nslsl.shelf_life_status shelfLifeStatus,\\nslsl.left_days leftDays,\\nslsl.production_date productionDate,\\nslsl.expiration_date expirationDate,\\nslsl.shelf_life_days shelfLifeDays,\\nslsl.warning_days warningDays,\\nslsl.regular_advent_days regularAdventDays,\\nslsl.urgent_advent_days urgentAdventDays,\\nslsl.advent_days adventDays,\\nslsl.extend_content extendContent\\nFROM\\nst_stock m\\nINNER JOIN st_lot_shelf_life slsl\\nON\\nm.tenant_code = slsl.tenant_code\\nAND m.sku = slsl.sku\\nAND m.lot_no = slsl.lot_no\\nAND slsl.deleted = 0\\nWHERE\\nm.deleted = 0\\nAND m.stock_qty > 0\\nAND m.warehouse_no = ?\\nAND m.lot_no != \'-1\' LIMIT ?\\n/* [SQLMarking] statementId: com.jdwl.wms.stock.infrastructure.jdbc.report.dao.StockShelfLifeReportDao.selectStockShelfLifeReport, pFinderId: 4630283.56667.17392048399060130, operator: xxx, traceId: 59f48d4d-5346-4ffe-9837-693a090090fc */\\n
\\n\ufeff
\\n通过这个染色标记后的SQL我们可以一眼看出来,这个SQL是来自StockShelfLifeReportDao中的selectStockShelfLifeReport方法,其中StockShelfLifeReportDao对应于mapper文件中的namespace,selectStockShelfLifeReport 对应于 SQL id。
\\n除了statementId和pFinderId外,还允许用户在线程上下文中自定义传输一些附加信息到SQL中,并体现在SQL注释信息中。
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n借助IDE的reference功能,我们可以很快找到调用入口:
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n继续向上追溯,流量源头是来自一张报表查询:
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n既然是代理增强,多少会有一些性能开销,目前根据我这边使用的情况来看,单个SQL基本上是0-1ms左右,个别在3-4ms,正常情况下,不会影响业务响应时长。
\\n\ufeff
\\n已支持的场景:
\\n•使用MyBatis的SQL,包含select、insert、update、delete,同时也支持无where条件的SQL。支持MyBatis-Plus。
\\n\ufeff
\\nSELECT SQL效果:
\\nSELECT\\n COUNT(DISTINCT ito.transfer_order_no) AS qty\\nFROM\\n inv_transfer_order AS ito\\nLEFT JOIN inv_transfer_order_detail itd\\n ON\\n ito.warehouse_no = itd.warehouse_no\\n AND ito.transfer_order_no = itd.transfer_no\\n AND itd.deleted = 0\\nWHERE\\n ito.deleted = 0\\n AND ito.warehouse_no = ?\\n AND ito.transfer_status IN(?, ?, ?, ?, ?, ?, ?, ?)\\n /* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferOrderDao.selectOverstockOrderQty, pFinderId: 4900300.56689.17397685906403801, traceId: abc53cd3-e814-451e-a771-5d8caae861a7, operator: xxx */\\n
\\n\ufeff
\\nUPDATE SQL效果:
\\nUPDATE\\n inv_transfer_task_detail\\n SET\\n task_status = ?,\\n task_user = ?,\\n update_user = ?,\\n update_time = now(),\\n receive_time = now()\\nWHERE\\n warehouse_no = ?\\n AND deleted = 0\\n AND order_detail_id IN(?)\\n AND task_status IN(?, ?, ?)\\n /* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferTaskDetailDao.updateStatusAndTaskUserByOrderDetailAndStatus, pFinderId: 4900300.56689.17397685881342999, traceId: 41366c16-2e10-4c45-a10c-c84326e201b4, operator: xxx */\\n
\\n\ufeff
\\nINSERT SQL效果:
\\nINSERT\\nINTO\\ninv_transfer_task_result\\n(\\nid,\\nresult_no,\\ntransfer_type,\\ntask_type,\\nlocation_no,\\ncontainer_level_1,\\ncontainer_level_2,\\ncontainer_full,\\nextend_content,\\nwarehouse_no,\\ncreate_user,\\ncreate_time,\\nupdate_user,\\nupdate_time,\\ntask_no,\\ntenant_code\\n)\\nVALUES\\n(\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\n?,\\nnow(),\\n?,\\nnow(),\\n?,\\n\'TC26473419\'\\n)\\n/* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferTaskResultDao.insert, pFinderId: 4900300.56689.17397685845562352, traceId: 7cc0eebf-c4c5-4fc1-b5de-ae1f14ba29ba, operator: xxx */\\n
\\n\ufeff
\\n无WHERE条件的SQL效果:
\\nSELECT NOW()\\n/* [SQLMarking] statementId: com.jdwl.wms.stock.xxx.jdbc.main.dao.StockQueryDao.dbTime, pFinderId: 2033056.56579.17392526509236705 */\\n
\\n\ufeff
\\n该插件暂不支持的场景如下:
\\n•ORM非MyBatis的SQL,例如通过 connection statement execute 操作的SQL,通过JdbcTemplate 操作的SQL等。
\\n\ufeff
\\n\ufeff
\\n慢SQL分析
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n会话管理
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\nPFinder SQL分析
\\n\ufeff
\\n\ufeff\ufeff
\\n\ufeff
\\n如果小伙伴也有类似痛点和使用诉求,可以接入这个简易的SQL染色标记插件。
\\n目前该组件已在多个大型复杂系统的生产环境中接入使用,大家可以先在测试、UAT环境接入试用,然后再逐步推广线上生产环境。
\\n接入方法也非常简单,如下。
\\n1、引入Maven坐标:
\\n<dependency>\\n <groupId>com.jd.sword</groupId>\\n <artifactId>sword-mybatis-plugins</artifactId>\\n <version>1.0.2-SNAPSHOT</version>\\n <exclusions>\\n <exclusion>\\n <groupId>org.mybatis</groupId>\\n <artifactId>mybatis</artifactId>\\n </exclusion>\\n <exclusion>\\n <groupId>org.projectlombok</groupId>\\n <artifactId>lombok</artifactId>\\n </exclusion>\\n <exclusion>\\n <groupId>org.apache.commons</groupId>\\n <artifactId>commons-lang3</artifactId>\\n </exclusion>\\n <exclusion>\\n <groupId>org.slf4j</groupId>\\n <artifactId>slf4j-api</artifactId>\\n </exclusion>\\n </exclusions>\\n</dependency>\\n
\\n对于其中的间接依赖,例如lombok等,大家可以使用自己工程中的已有依赖,在这里可以通过exclusion排掉,如果自己工程中没有这些依赖,可以不exclusion。
\\n2、在mybatis config xml中引入SQLMarking插件:
\\n<!-- SQLMarking Plugin --\x3e\\n<plugin interceptor=\\"com.jd.sword.mybatis.plugin.sql.SQLMarkingInterceptor\\">\\n <!-- 是否开启SQL染色标记插件 --\x3e\\n <property name=\\"enabled\\" value=\\"true\\"/>\\n</plugin>\\n
\\n1、支持 Mybatis-Plus 吗?
\\n答:支持,Mybatis-Plus是在MyBatis基础上的增强,MyBatis插件可以得到执行。
\\n\ufeff
\\n2、SQLMarking Plugin 在 plugins中的位置有严格要求吗,比如必须第一个位置?
\\n答:没有严格要求,理论上放上放下都可以。有的小伙伴工程里依赖了多种 MyBatis Plugin,多种Plugin之间可能会有冲突,比如有些 Plugin 会对SQL的开头INSERT/SELECT/UPDATE/DELETE关键词进行前缀判断,大家如果遇到报错可以灵活调整 SQLMarking Plugin 的位置,向上或向下调整,不一定非得放在第一个位置。
\\n\ufeff
\\n3、报错信息:There is no getter for property named \'delegate\' in \'class com.sun.proxy.$Proxy211\'
\\n答:这种是多个插件之间有先后顺序依赖,别的插件先行执行,影响了delegate的获取,调整 SQLMarking Plugin 的位置,向上或向下调整,可解决冲突。
\\n\ufeff
\\n4、报错信息关键词:NoClassDefFoundError RoutingStatementHandlerUtils
\\n答:缺少依赖,添加以下依赖:
\\n<dependency>\\n<groupId>mybatis-plugins</groupId>\\n<artifactId>mybatis-plugins</artifactId>\\n<version>2.2.3</version>\\n</dependency>\\n
\\n\ufeff
\\n5、染色信息中如何添加一些个性化的附加信息?
\\n答:可以用下这个
\\nSQLMarkingThreadLocal.put(key, value)\\n
\\nSQL 执行完 remove 掉。一个方法同时执行多个SQL时,如果 SQLMarkingThreadLocal 可共享,也可以在方法维度上 put 和 remove,就不用每个SQL put remove一下。主要是看线程上下文是否应该传递SQLMarkingThreadLocal的信息。
","description":"作者:京东物流 郭忠强 导语\\n\\n本文分析了后端研发和运维在日常工作中所面临的线上SQL定位排查痛点,基于姓名贴的灵感,设计和开发了一款SQL染色标记的MyBatis插件。该插件轻量高效,对业务代码无侵入,接入简单,支持SELECT、INSERT、UPDATE、DELETE等语句,同时也支持无WHERE条件SQL的标记增强。该SQL染色插件并不改变SQL指纹,染色信息内置了statementId、PFinderId,方便分布式跟踪和定位。此外,还提供了附加信息的传递入口,方便用户进行自定义信息染色,例如客户端的执行线程id等…","guid":"https://juejin.cn/post/7477884622836498451","author":"京东云开发者","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T02:08:38.810Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec24ec20c8f94194a241c1e555dc715c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=mqkbIyLAhkXtAU2aTufcoDgTXmo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd7126bc82b24129965f1b809539588c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=Of92SP%2BmoOPfpPSUoy35OB5tebw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/853d20c410cd4b2d96966157c466d299~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=etfFK6ITVf6p7frFkvqFy6txo7w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ad13f6202aa64615bcc7a76444419d3c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=kb2af2lu6o6Zaq9CQGyMgNtnJaM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/efe8582c8e494a1c98dd2a5ae0b21205~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=%2Ba5Ihq298duwZ%2B56D3tNs6h7A%2FY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/099f5aa954a1438b82a4e641e51b91ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=rQXXsEhCbCIir%2B1a7psBMvfyf3U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e0092a89faff4aa7a770df17ee5480a8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=chxrulO3itDmYz%2BxYTZxsoak9yI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f69bc61c0a4148b0995f1aea0330a1d0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=siuDEhrbHTq3tr0IT5OO719duiA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/617bf66e1c184457b686c8170037f3da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=wrAfSyFpEPurvuRbGQxfYfnihYA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3217717498f74cff9789ede782bdf58b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=UjiWrTd7NOHMipV01OXKWnNtq%2BA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b4e279ec580f4e8e990b7914b86895b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=Vop1m6VMCCaNpNCtbPUH7VXa1lE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bddacd5c54244147b41e490804922bdd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=j1at88ytmT4F7opd11TF6Gk4Psw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a4ae58dec1f84952bd40a882a31b3489~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=aqLsG07IkwG1oNu9UloDrEHjHCI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/067b622df9ec4b93a8ae1016c2175da4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=dnBdFr%2BBzK%2FacijavneyZotDg6A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da8ed88fd3844f7188cd0d486e0fdc63~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=mqjrr6HNDNXkcNkbRWgiq7vKJ2U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/32ab2c67c29f4785bb25312e7f2e1968~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=rbb8QqZPcYWx%2BWEkVkBCLniZzyo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e96577ea396548feaaa21b3a2210e015~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1741745318&x-signature=JAUMHnLCxZYu8Fona8%2FzUKsLsI0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"18k star,取代Navicat!一款集成了AI功能的数据库管理工具!","url":"https://juejin.cn/post/7477885617383276578","content":"\\n\\n数据库管理工具很多,但是集成AI功能的却不多,今天给大家分享一款集成AI功能的数据库管理工具Chat2DB,能帮你快速编写SQL语句,希望对大家有所帮助!
\\n
Chat2DB是一款智能的通用SQL客户端和数据报表工具,它集成了AI的能力,目前在Github上已有18k+Star
。它可以帮助我们快速编写SQL查询、管理数据库、生成报告、探索数据、并且可以与多种数据库进行交互。
Chat2DB具有如下特性:
\\n下面是使用Chat2DB管理数据库的效果图,界面还是挺炫酷的!
\\n\\n\\nChat2DB具有多种客户端,支持Windows、MacOS、Linux、Docker环境,这里以Docker环境安装为例子。
\\n
docker pull chat2db/chat2db:latest\\n
\\ndocker run -p 10824:10824 --name=chat2db \\\\\\n-v /mydata/chat2db:/root/.chat2db \\\\\\n-d chat2db/chat2db:latest\\n
\\nchat2db/chat2db
,访问地址:http://192.168.3.101:10824\\n\\n由于下面要以mall电商实战项目的数据库表为例来介绍Chat2DB的使用,这里简单介绍下mall项目。
\\n
mall项目是一套基于SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
\\n\\n\\n接下来我们来介绍下Chat2DB的数据库管理功能,以MySQL数据库为例。
\\n
\\n\\n上面介绍的是Chat2DB的数据库管理功能,接下来介绍下它的AI功能,这里以通义千问为例。
\\n
设置->自定义AI
进行设置;# ApiKey 可以从阿里云百炼平台获取,地址:https://bailian.console.aliyun.com\\n<YOUR_API_KEY>\\n# ApiHost\\nhttps://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation/\\n# Model\\nqwen-plus\\n
\\n根据用户名查询后台用户 用中文回答
,然后点击回车按钮,AI模型就会给我们生成好对应的SQL了。今天给大家分享了一款集成AI功能的数据库管理工具Chat2DB,它的界面确实够炫酷,提示也很全。体验了下AI功能也可以使用,但是有时候回答会有一些重复的信息,这时候需要自己手动筛选下!
\\n文件存储已成为一个做任何应用都不可回避的需求。传统的单机文件存储方案在面对大规模数据和高并发访问时往往力不从心,而分布式文件存储系统则提供了更好的解决方案。本篇文章我将基于Spring Boot 3 为大家讲解如何基于MinIO来实现分布式文件存储。
\\n在探讨核心内容之前,我们不妨先回顾分布式存储技术是如何伴随系统架构演变发展的。在单体架构早期,文件直接存储于应用服务器中,这种方式简单直接,存取便捷。然而,随着业务规模扩大和用户量激增,系统架构逐步向分布式或微服务方向演进。此时,若仍将文件存储在应用服务器中,在负载均衡机制下可能导致文件访问异常 —— 用户上传的文件可能因路由到其他服务节点而无法访问。
\\n面对这个挑战,我们可以借鉴\\"分层解决\\"的架构思想:将文件存储从应用服务中剥离,集中在独立的存储服务中统一管理。这便是分布式文件存储系统的雏形。
\\n在了解了分布式存储的演进背景后,让我们来梳理当前主流的分布式存储解决方案。
\\nMinIO 是一款轻量级的分布式对象存储系统,完全兼容 Amazon S3 云存储服务接口。其部署维护简单,性能卓越,成为我们的首选方案。
\\nMinIO 提供了多种部署方式,包括单机部署和分布式部署。本文主要关注 Spring Boot 与 MinIO 的整合实践,因此我们选择使用Docker(Ps:没安装Docker的同学速速去安装,或者用别的方式只要本地部署的能跑就行)进行快速部署。
\\n首先,通过命令拉取镜像。
\\ndocker pull minio/minio\\n
\\n接着在 本地创建一个存储文件的映射目录 D:\\\\minio\\\\data
(Ps:我当前演示的环境是win系统,大家根据自己的操作系统建个目录就行),使用以下命令启动 MinIO:
🔑 补充一个小细节:MinIO 的安全限制要求用户名长度至少需要 3 个字符,密码长度至少需要 8 个字符。
\\ndocker run -d --name minio -p 9000:9000 -p 9001:9001 -v D:\\\\minio\\\\data:/data -e \\"MINIO_ROOT_USER=root\\" -e \\"MINIO_ROOT_PASSWORD=12345678\\" minio/minio server /data --console-address \\":9001\\" --address \\":9000\\"\\n
\\n参数说明:
\\n-d
: 后台运行容器--name
: 容器名称-p
: 端口映射,9000用于API访问,9001用于控制台访问-v
: 目录映射,将本地目录映射到容器的 /data-e
: 环境变量,设置管理员账号和密码--console-address
: 指定控制台端口--restart=always
: 容器自动重启策略--address \\":9000\\"
: 显式指定 API 端口运行成功后访问 http://localhost:9001
,使用执行命令中的凭据(Ps:大家在使用时可以修改为自己的用户名和密码)登录:
登录系统后,界面会提示创建桶。熟悉云服务商OSS服务的读者对此概念应该不陌生。对初次接触的读者,可以将桶理解为一个命名空间或文件夹,您可以创建多个桶,每个桶内还能包含多层级的文件夹和文件。
\\n这里我演示下控制台如何建桶和上传文件,方便大家理解文件在MinIO上的存储结构。
\\n只需要输入名称就可以,建好之后可以看到桶的使用状态。
\\n点击它进入桶的内部,这里大家需要关注一个设置- Access Policy,默认是Private
。这个设置需要根据业务的实际情况来,如果你的业务是需要提供一些不需要鉴权的公共访问的文件,就设为public
;反之,就保持private
。我这里把它修改为public
。
然后点击右上角的上传按钮进入上传页可以向桶内上传文件。
\\n上传成功后可以在桶内看到文件。
\\n点击文件可查看详情,支持预览、删除、分享等多种功能。这些操作较为直观,安装后各位读者可自行体验。本文重点关注不在控制台的操作,就不做过多赘述了。
\\n🔑这里再强调一点:存储在桶里的文件通过API访问的端口和控制台是不一样的。如果你对这里感觉迷惑,可以回看一下上面我贴上的docker运行命令里配置了两个端口-9000
和9001
。如果要通过API访问查看这个文件的话,通过拼接地址/端口号/桶名/文件路径查看,那么刚测试上传的文件的访问API就是http://localhost:9000/test/1.gif,在浏览器地址栏输入后可以看到。
这部分对于新建项目就不赘述了,直接说下我使用的 Spring boot 版本为3.2.3,供大家参考。
\\n在pom.xml引入minIO的依赖,版本大家自己使用你当前最新的版本即可。
\\n<!-- minio --\x3e\\n<dependency>\\n <groupId>io.minio</groupId>\\n <artifactId>minio</artifactId>\\n <version>${latest.version}</version>\\n</dependency>\\n
\\n在yml配置文件中配置连接信息。
\\n# minIO配置\\nminio:\\n endpoint: http://127.0.0.1:9000 # MinIO服务地址\\n fileHost: http://127.0.0.1:9000 # 文件地址host\\n bucketName: wechat # 存储桶bucket名称\\n accessKey: root # 用户名\\n secretKey: 12345678 # 密码\\n
\\nimport com.pitayafruits.utils.MinIOUtils;\\nimport lombok.Data;\\nimport org.springframework.beans.factory.annotation.Value;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n@Configuration\\n@Data\\npublic class MinIOConfig {\\n\\n @Value(\\"${minio.endpoint}\\")\\n private String endpoint;\\n @Value(\\"${minio.fileHost}\\")\\n private String fileHost;\\n @Value(\\"${minio.bucketName}\\")\\n private String bucketName;\\n @Value(\\"${minio.accessKey}\\")\\n private String accessKey;\\n @Value(\\"${minio.secretKey}\\")\\n private String secretKey;\\n\\n @Bean\\n public MinIOUtils creatMinioClient() {\\n return new MinIOUtils(endpoint, fileHost, bucketName, accessKey, secretKey);\\n }\\n}\\n
\\n这个工具类封装了MinIO的核心功能,为您提供了很多开箱即用的功能。通过引入它,可以轻松实现文件上传、下载等操作,让大家将更多精力集中在业务开发上。
\\nimport io.minio.*;\\nimport io.minio.http.Method;\\nimport io.minio.messages.Bucket;\\nimport io.minio.messages.DeleteObject;\\nimport io.minio.messages.Item;\\nimport lombok.extern.slf4j.Slf4j;\\nimport org.springframework.web.multipart.MultipartFile;\\n\\nimport java.io.ByteArrayInputStream;\\nimport java.io.InputStream;\\nimport java.io.UnsupportedEncodingException;\\nimport java.net.URLDecoder;\\nimport java.util.ArrayList;\\nimport java.util.LinkedList;\\nimport java.util.List;\\nimport java.util.Optional;\\n\\n/**\\n * MinIO工具类\\n */\\n@Slf4j\\npublic class MinIOUtils {\\n\\n private static MinioClient minioClient;\\n\\n private static String endpoint;\\n private static String fileHost;\\n private static String bucketName;\\n private static String accessKey;\\n private static String secretKey;\\n\\n private static final String SEPARATOR = \\"/\\";\\n\\n public MinIOUtils() {\\n }\\n\\n public MinIOUtils(String endpoint, String fileHost, String bucketName, String accessKey, String secretKey) {\\n MinIOUtils.endpoint = endpoint;\\n MinIOUtils.fileHost = fileHost;\\n MinIOUtils.bucketName = bucketName;\\n MinIOUtils.accessKey = accessKey;\\n MinIOUtils.secretKey = secretKey;\\n createMinioClient();\\n }\\n\\n /**\\n * 创建基于Java端的MinioClient\\n */\\n public void createMinioClient() {\\n try {\\n if (null == minioClient) {\\n log.info(\\"开始创建 MinioClient...\\");\\n minioClient = MinioClient\\n .builder()\\n .endpoint(endpoint)\\n .credentials(accessKey, secretKey)\\n .build();\\n createBucket(bucketName);\\n log.info(\\"创建完毕 MinioClient...\\");\\n }\\n } catch (Exception e) {\\n log.error(\\"MinIO服务器异常:{}\\", e);\\n }\\n }\\n\\n /**\\n * 获取上传文件前缀路径\\n * @return\\n */\\n public static String getBasisUrl() {\\n return endpoint + SEPARATOR + bucketName + SEPARATOR;\\n }\\n\\n /****************************** Operate Bucket Start ******************************/\\n\\n /**\\n * 启动SpringBoot容器的时候初始化Bucket\\n * 如果没有Bucket则创建\\n * @throws Exception\\n */\\n private static void createBucket(String bucketName) throws Exception {\\n if (!bucketExists(bucketName)) {\\n minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());\\n }\\n }\\n\\n /**\\n * 判断Bucket是否存在,true:存在,false:不存在\\n * @return\\n * @throws Exception\\n */\\n public static boolean bucketExists(String bucketName) throws Exception {\\n return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());\\n }\\n\\n\\n /**\\n * 获得Bucket的策略\\n * @param bucketName\\n * @return\\n * @throws Exception\\n */\\n public static String getBucketPolicy(String bucketName) throws Exception {\\n String bucketPolicy = minioClient\\n .getBucketPolicy(\\n GetBucketPolicyArgs\\n .builder()\\n .bucket(bucketName)\\n .build()\\n );\\n return bucketPolicy;\\n }\\n\\n\\n /**\\n * 获得所有Bucket列表\\n * @return\\n * @throws Exception\\n */\\n public static List<Bucket> getAllBuckets() throws Exception {\\n return minioClient.listBuckets();\\n }\\n\\n /**\\n * 根据bucketName获取其相关信息\\n * @param bucketName\\n * @return\\n * @throws Exception\\n */\\n public static Optional<Bucket> getBucket(String bucketName) throws Exception {\\n return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();\\n }\\n\\n /**\\n * 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在\\n * @param bucketName\\n * @throws Exception\\n */\\n public static void removeBucket(String bucketName) throws Exception {\\n minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());\\n }\\n\\n /****************************** Operate Bucket End ******************************/\\n\\n\\n /****************************** Operate Files Start ******************************/\\n\\n /**\\n * 判断文件是否存在\\n * @param bucketName 存储桶\\n * @param objectName 文件名\\n * @return\\n */\\n public static boolean isObjectExist(String bucketName, String objectName) {\\n boolean exist = true;\\n try {\\n minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());\\n } catch (Exception e) {\\n exist = false;\\n }\\n return exist;\\n }\\n\\n /**\\n * 判断文件夹是否存在\\n * @param bucketName 存储桶\\n * @param objectName 文件夹名称\\n * @return\\n */\\n public static boolean isFolderExist(String bucketName, String objectName) {\\n boolean exist = false;\\n try {\\n Iterable<Result<Item>> results = minioClient.listObjects(\\n ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());\\n for (Result<Item> result : results) {\\n Item item = result.get();\\n if (item.isDir() && objectName.equals(item.objectName())) {\\n exist = true;\\n }\\n }\\n } catch (Exception e) {\\n exist = false;\\n }\\n return exist;\\n }\\n\\n /**\\n * 根据文件前置查询文件\\n * @param bucketName 存储桶\\n * @param prefix 前缀\\n * @param recursive 是否使用递归查询\\n * @return MinioItem 列表\\n * @throws Exception\\n */\\n public static List<Item> getAllObjectsByPrefix(String bucketName,\\n String prefix,\\n boolean recursive) throws Exception {\\n List<Item> list = new ArrayList<>();\\n Iterable<Result<Item>> objectsIterator = minioClient.listObjects(\\n ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());\\n if (objectsIterator != null) {\\n for (Result<Item> o : objectsIterator) {\\n Item item = o.get();\\n list.add(item);\\n }\\n }\\n return list;\\n }\\n\\n /**\\n * 获取文件流\\n * @param bucketName 存储桶\\n * @param objectName 文件名\\n * @return 二进制流\\n */\\n public static InputStream getObject(String bucketName, String objectName) throws Exception {\\n return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());\\n }\\n\\n /**\\n * 断点下载\\n * @param bucketName 存储桶\\n * @param objectName 文件名称\\n * @param offset 起始字节的位置\\n * @param length 要读取的长度\\n * @return 二进制流\\n */\\n public InputStream getObject(String bucketName, String objectName, long offset, long length)throws Exception {\\n return minioClient.getObject(\\n GetObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .offset(offset)\\n .length(length)\\n .build());\\n }\\n\\n /**\\n * 获取路径下文件列表\\n * @param bucketName 存储桶\\n * @param prefix 文件名称\\n * @param recursive 是否递归查找,false:模拟文件夹结构查找\\n * @return 二进制流\\n */\\n public static Iterable<Result<Item>> listObjects(String bucketName, String prefix,\\n boolean recursive) {\\n return minioClient.listObjects(\\n ListObjectsArgs.builder()\\n .bucket(bucketName)\\n .prefix(prefix)\\n .recursive(recursive)\\n .build());\\n }\\n\\n /**\\n * 使用MultipartFile进行文件上传\\n * @param bucketName 存储桶\\n * @param file 文件名\\n * @param objectName 对象名\\n * @param contentType 类型\\n * @return\\n * @throws Exception\\n */\\n public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,\\n String objectName, String contentType) throws Exception {\\n InputStream inputStream = file.getInputStream();\\n return minioClient.putObject(\\n PutObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .contentType(contentType)\\n .stream(inputStream, inputStream.available(), -1)\\n .build());\\n }\\n\\n /**\\n * 上传本地文件\\n * @param bucketName 存储桶\\n * @param objectName 对象名称\\n * @param fileName 本地文件路径\\n */\\n public static String uploadFile(String bucketName, String objectName,\\n String fileName, boolean needUrl) throws Exception {\\n minioClient.uploadObject(\\n UploadObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .filename(fileName)\\n .build());\\n if (needUrl) {\\n String imageUrl = fileHost\\n + \\"/\\"\\n + bucketName\\n + \\"/\\"\\n + objectName;\\n return imageUrl;\\n }\\n return \\"\\";\\n }\\n\\n /**\\n * 通过流上传文件\\n *\\n * @param bucketName 存储桶\\n * @param objectName 文件对象\\n * @param inputStream 文件流\\n */\\n public static ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {\\n return minioClient.putObject(\\n PutObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .stream(inputStream, inputStream.available(), -1)\\n .build());\\n }\\n\\n public static String uploadFile(String bucketName, String objectName, InputStream inputStream, boolean needUrl) throws Exception {\\n minioClient.putObject(\\n PutObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .stream(inputStream, inputStream.available(), -1)\\n .build());\\n if (needUrl) {\\n String imageUrl = fileHost\\n + \\"/\\"\\n + bucketName\\n + \\"/\\"\\n + objectName;\\n return imageUrl;\\n }\\n return \\"\\";\\n }\\n\\n /**\\n * 创建文件夹或目录\\n * @param bucketName 存储桶\\n * @param objectName 目录路径\\n */\\n public static ObjectWriteResponse createDir(String bucketName, String objectName) throws Exception {\\n return minioClient.putObject(\\n PutObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)\\n .build());\\n }\\n\\n /**\\n * 获取文件信息, 如果抛出异常则说明文件不存在\\n *\\n * @param bucketName 存储桶\\n * @param objectName 文件名称\\n */\\n public static String getFileStatusInfo(String bucketName, String objectName) throws Exception {\\n return minioClient.statObject(\\n StatObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .build()).toString();\\n }\\n\\n /**\\n * 拷贝文件\\n *\\n * @param bucketName 存储桶\\n * @param objectName 文件名\\n * @param srcBucketName 目标存储桶\\n * @param srcObjectName 目标文件名\\n */\\n public static ObjectWriteResponse copyFile(String bucketName, String objectName,\\n String srcBucketName, String srcObjectName) throws Exception {\\n return minioClient.copyObject(\\n CopyObjectArgs.builder()\\n .source(CopySource.builder().bucket(bucketName).object(objectName).build())\\n .bucket(srcBucketName)\\n .object(srcObjectName)\\n .build());\\n }\\n\\n /**\\n * 删除文件\\n * @param bucketName 存储桶\\n * @param objectName 文件名称\\n */\\n public static void removeFile(String bucketName, String objectName) throws Exception {\\n minioClient.removeObject(\\n RemoveObjectArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .build());\\n }\\n\\n /**\\n * 批量删除文件\\n * @param bucketName 存储桶\\n * @param keys 需要删除的文件列表\\n * @return\\n */\\n public static void removeFiles(String bucketName, List<String> keys) {\\n List<DeleteObject> objects = new LinkedList<>();\\n keys.forEach(s -> {\\n objects.add(new DeleteObject(s));\\n try {\\n removeFile(bucketName, s);\\n } catch (Exception e) {\\n log.error(\\"批量删除失败!error:{}\\",e);\\n }\\n });\\n }\\n\\n /**\\n * 获取文件外链\\n * @param bucketName 存储桶\\n * @param objectName 文件名\\n * @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))\\n * @return url\\n * @throws Exception\\n */\\n public static String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {\\n GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();\\n return minioClient.getPresignedObjectUrl(args);\\n }\\n\\n /**\\n * 获得文件外链\\n * @param bucketName\\n * @param objectName\\n * @return url\\n * @throws Exception\\n */\\n public static String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {\\n GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()\\n .bucket(bucketName)\\n .object(objectName)\\n .method(Method.GET).build();\\n return minioClient.getPresignedObjectUrl(args);\\n }\\n\\n /**\\n * 将URLDecoder编码转成UTF8\\n * @param str\\n * @return\\n * @throws UnsupportedEncodingException\\n */\\n public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {\\n String url = str.replaceAll(\\"%(?![0-9a-fA-F]{2})\\", \\"%25\\");\\n return URLDecoder.decode(url, \\"UTF-8\\");\\n }\\n\\n /****************************** Operate Files End ******************************/\\n\\n\\n}\\n
\\n我刚好在做练手项目,这里写个上传头像的接口。
\\nimport com.pitayafruits.base.BaseInfoProperties;\\nimport com.pitayafruits.config.MinIOConfig;\\nimport com.pitayafruits.grace.result.GraceJSONResult;\\nimport com.pitayafruits.grace.result.ResponseStatusEnum;\\nimport com.pitayafruits.utils.MinIOUtils;\\nimport jakarta.annotation.Resource;\\nimport org.apache.commons.lang3.StringUtils;\\nimport org.springframework.web.bind.annotation.*;\\nimport org.springframework.web.multipart.MultipartFile;\\n\\n@RestController\\n@RequestMapping(\\"file\\")\\npublic class FileController extends BaseInfoProperties {\\n\\n @Resource\\n private MinIOConfig minIOConfig;\\n\\n @PostMapping(\\"uploadFace\\")\\n public GraceJSONResult upload(@RequestParam MultipartFile file,\\n String userId) throws Exception {\\n if (StringUtils.isBlank(userId)) {\\n return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);\\n }\\n\\n String filename = file.getOriginalFilename();\\n\\n if (StringUtils.isBlank(filename)) {\\n return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);\\n }\\n\\n filename = \\"face\\" + \\"/\\" + userId + \\"/\\" + filename;\\n\\n MinIOUtils.uploadFile(minIOConfig.getBucketName(), filename, file.getInputStream());\\n\\n String faceUrl = minIOConfig.getFileHost()\\n + \\"/\\"\\n + minIOConfig.getBucketName()\\n + \\"/\\"\\n + filename;\\n\\n return GraceJSONResult.ok(faceUrl);\\n }\\n\\n}\\n
\\n可以看到通过工具类只需要一行代码就可以实现上传文件,我们只需要在调用的时候做好文件的业务隔离即可。完成了接口的开发,这里我来通过Apifox调用测试一下。
\\n通过浏览器访问返回的图片链接会自动下载,我们再登录控制台看对应的桶下的这个路径,也可以看到这个文件。
\\n我们在集成第三方服务时应遵循一个核心原则:将API操作封装成通用工具类。这不仅让MinIO的集成更加优雅,也让代码具备更好的复用性和可维护性。这种思维方式同样适用于其他第三方服务的对接。
","description":"引言 文件存储已成为一个做任何应用都不可回避的需求。传统的单机文件存储方案在面对大规模数据和高并发访问时往往力不从心,而分布式文件存储系统则提供了更好的解决方案。本篇文章我将基于Spring Boot 3 为大家讲解如何基于MinIO来实现分布式文件存储。\\n\\n分布式存储的出现\\n\\n在探讨核心内容之前,我们不妨先回顾分布式存储技术是如何伴随系统架构演变发展的。在单体架构早期,文件直接存储于应用服务器中,这种方式简单直接,存取便捷。然而,随着业务规模扩大和用户量激增,系统架构逐步向分布式或微服务方向演进。此时,若仍将文件存储在应用服务器中…","guid":"https://juejin.cn/post/7477875236144398362","author":"别惹CC","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T01:21:34.949Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aa1d5a81f596405f917eb54270467ffe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=nflqNsO2RknCoRFy5ijBGX2lN0M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/62d2752dc1984b6cad6b24b6fbd68b88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=P1g6i5LuCCfdLST7Ac8fjUwIAfQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d502f2718aa410f8ba0f2d1e6a54c31~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=ECL1uhdG58Qz2xQuugW9OusV0UQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2357e6faebfd474baecca400ad0db88e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=ephT0CarKHKuI%2Fa1bc2XuvVF9gQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6af0260dd01d4fe894f6ace8b3ee9e90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=ww34AxVWb2ti9as4cyWn3roIfP8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d02c9ef084cc4f9ea65d7ceee6c1a930~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=CQaH3Aw1DNFVHkPpfE7OiYn7OTU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/780d074964924202b220d5cf5094d331~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=md3ZE6qv2Y3CtNCTFskCAUMBjTw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/52b2f2dc052a44ceaa7a50aa66d7eafe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=T5Us%2F8m18xseNzgkuPuM3ioBPN8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/973a44e7adef4d15981a3cc9d7ddd38b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=HcOY4jzauFLdElGK%2BNtdKqLGF7I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cb3725894b59437ca6bc1f1027bdbfb9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=ZlW98e7A3jEVCRPb2hkJHccLZa8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a0576525bd3f46928decaa3966b42796~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Yir5oO5Q0M=:q75.awebp?rk3s=f64ab15b&x-expires=1741742494&x-signature=2siIW6SAP2iaea8jVpb8DytuyUE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","分布式","Spring Boot"],"attachments":null,"extra":null,"language":null},{"title":"工作十年,谈谈我的100W QPS高可用架构和系统设计经验","url":"https://juejin.cn/post/7477871457738375218","content":"\\n\\n“系统又崩了!”——这句话是不是让你头皮发麻?
\\n
高可用系统设计是每个技术人必须掌握的硬核技能,但很多人只会堆功能,忽视了系统稳定性的重要性。
\\n本文从研发规范、应用服务、存储、产品、运维部署、异常应急六大层面,手把手教你如何设计一个高可用系统,让你的服务坚如磐石,再也不用担心半夜被报警电话叫醒!
\\n可用性,简单来说就是系统能正常干活的时间比例。它是一个可以量化的指标,计算公式是这样的:
\\n可用性 = (总运行时间 - 宕机时间) / 总运行时间
\\n行业内通常用“几个9”来衡量可用性水平:
\\n对于大多数系统来说,**99.99%(四个9)**是起步要求,只有达到这个水平,才能勉强算得上高可用。
\\n\\n\\n最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。\\n这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
\\n
高可用(High Availability,HA),说白了就是系统能无中断地提供服务的能力。它是系统设计的核心准则之一,目标是尽量减少宕机时间,确保服务在任何情况下都能稳定运行。
\\n但要注意,100%可用是不可能的!无论你设计得多完美,硬件故障、网络波动、人为失误等问题总是难以避免。所以,高可用的核心思想是:尽最大可能提高系统的可用性,让服务在绝大多数情况下都能对外正常提供服务。
\\n用一句话概括高可用的本质:高可用就是让系统在任何情况下都能“扛得住、稳得住、恢复得快”。
\\n在当今的互联网时代,用户对系统的稳定性要求越来越高。一次小小的宕机,可能导致:
\\n\\n\\n还记得2025年1月16日下午,支付宝出现了“政府补贴”错误提示,导致部分用户在支付时享受了不应有的折扣。支付宝官方确认了此事,并表示不会向用户追款。造成了严重的资金损失。
\\n
所以,高可用不仅是一个技术问题,更是一个业务问题。只有把高可用做到位,才能让系统真正成为业务的坚实后盾。
\\n高可用系统的设计,需要有一套比较科学的工程管理套路,要从产品、开发、运维、基建等全方位去考量和设计,高可用系统的设计思想包括但不限于:
\\n\\n\\n做好研发规范,系统都是研发人员设计和编码写出来的,因此首先要对研发层面有一个规范和标准
\\n做好容量规划和评估,主要是让开发人员对系统要抗住的量级有一个基本认知,方便进行合理的架\\n构设计和演进。
\\n做好服务层面的高可用,主要是负载均衡、弹性扩缩容、异步解耦、故障容错、过载保护等。
\\n做好存储层面的高可用,主要是冗余备份(热备、冷备)、失效转移(确认,转移,恢复)等。
\\n做好运维层面的高可用,主要是发布测试、监控告警、容灾、故障演练等。
\\n做好产品层面的高可用,主要是兜底策略。
\\n做好应急预案,主要是在出现问题后怎么快速恢复,不至于让我们的异常事态扩大。
\\n
研发规范层面这个是大家容易忽视的一个点,但是,我们所有的设计,都是研发人员来完成的,包括从设计文档到编码到发布上线,因此,研发层面也是有一个规范流程和套路,来让我们更好的去研发和维护一个高可用的系统:
\\n\\n\\n方案设计后一定要进行评审,在我们团队中,新项目一定要评审,重构项目一定要评审,大\\n的系统优化或者升级一定要评审,其他的一般研发工作量超过一周的建议要评审的。
\\n
\\n\\n不要随便打日志 要接入远程日志 要能够分布式链路追踪
\\n代码编写完需要有一定的单测来保证代码的健壮性,同时也能保障我们后续调整逻辑或者优化的时候可以保证代码的稳定\\n包括增量覆盖率、全量覆盖率,具体的覆盖率要达到多少可以根据团队内部的实际情况来定,在我们团队,定的规则是 50% 的覆盖率。
\\n工程的 layout 目录结构规范,团队内部保持统一,尽量简洁\\n遵循团队内部的代码规范,一般公司都有对应语言的规范,如果没有则参考官方的规范,代\\n码规范可以大大减少 bug 并且提高可用性。
\\n执行代码规范 单测覆盖率 日志规范
\\n发布上线阶段,参考下面运维部署层面那一章节的灰度发布和接口测试相关说明
\\n
容量评估,是指我们需要评估好,我们这个系统,是为了应对一个什么体量的业务,这个业务请求量的平均值、高峰的峰值大概都在一个什么级别。
\\n如果是新系统,那么就需要根据产品和运营同学对业务有一个大体的预估,然后开发同学根据产品给的数据再进行详细的评估。如果是老系统,那么就可以根据历史数据来评估。评估的时候,要从一个整体角度来看全局的量级,然后再细化到每个子业务模块要承载的量级。
\\n例如常用的二八法则估算
\\n容量规划,简单说就是我们在设计系统的时候,就要大概知道系统能承受多少流量,是十万、百万,还是更多。
\\n不同的流量量级,系统架构的设计差别可大了,尤其是到了千万、亿级别的流量,架构设计就得考虑的更多。
\\n不过,别误会,并不是说一开始就得做个能承受几亿请求的系统——没必要。你得根据自己业务的真实流量来做,合适的才是最好的。
\\n容量规划还得考虑我们系统上下游的各个模块,比如你依赖的存储、三方服务等,它们各自需要多少资源,这些数据得能量化出来。
\\n最重要的是,容量规划更多是靠经验,团队的经验。比如,得了解你用的日志系统的性能、Redis的性能、RPC接口的性能、服务化框架的性能等,通过这些组件的性能数据,综合评估系统整体能承受多少流量。
\\n做完容量评估和规划后,下一步就是进行性能压测,最好是全链路压测。性能压测的目的是为了验证容量规划到底准不准。
\\n比如,你规划的系统能抗千万级别的请求,实际上能不能真的承受得住?这就得靠性能压测来给出准确答案。
\\n经验固然重要,但一定得有实际的数据来支撑,才能确保系统上线不会出问题。
\\n性能压测时,要关注的指标其实很多,但最关键的两个:一个是QPS(每秒请求量),另一个是响应时间。确保压测的结果符合你的预期才行。
\\n至于怎么压测,最好是先分模块逐个压测,如果有条件,能做全链路压测更好。这样才能真正知道,系统在大流量压力下的表现如何。
\\nQPS 预估(漏斗型),其实就是从一个真实请求进来后,依次经过系统的各个层级和模块。每一层级的 QPS(每秒请求量)都会有不同的量级,通常来说,越往下走,QPS的量级会逐步减少,因为每通过一个层级,都有可能会过滤掉一些请求。
\\n举个例子,就像用户从活动页进入,查看商品详情,再到下单的过程。最开始,所有用户进入活动页时,都会有请求进入系统;接着,有部分用户会去查询商品详情;然后,在查看商品详情的用户中,又只有一部分最终会下单。所以,你会看到从活动页到下单的这个过程,就像一个漏斗,层级越往下,流量就会越来越少。
\\n\\n\\n最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。\\n这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
\\n
为了让我们的系统保持高可用,通常会设计成无状态的应用服务。
\\n\\n\\n这是什么意思呢?
\\n
就是我们可以把服务拆成多个实例来提高可用性。多个实例的流量分配,靠的就是负载均衡。
\\n无状态服务加上负载均衡,不仅能提升系统的并发能力,还能让系统在面对故障时更加稳健。
\\n如果我们用的是微服务框架来开发的,那大概率框架内部自带了服务发现和负载均衡的功能。
\\n也就是说,有一整套流程:服务注册、服务发现、负载均衡、健康检查、自动剔除。当某个服务实例出了问题,系统会自动把它剔除,新增的服务实例会自动加入,继续提供服务。这样一来,整个系统就更加灵活、可靠。
\\n但如果我们没有用微服务框架开发,而是用传统架构,那么就得依赖一些代理服务来做负载均衡了,比如LVS 或者 Nginx。这些工具会帮我们处理流量分配,保证系统的可用性和稳定性。
\\n弹性扩缩容是应对突发流量的绝招,也是保证我们服务能一直在线、可用的必要手段。它的核心是无状态的应用服务,因为服务是无状态的,所以我们可以根据实际的流量来随时进行扩容或缩容。流量猛增时,扩容来处理更多请求;流量下降时,缩容,节省资源,减少浪费。
\\n那我们到底怎么实现弹性扩缩容呢?现在是云原生时代,很多公司都采用容器化部署,像是K8s(Kubernetes)这样的平台。对于这种架构,弹性扩缩容就变得特别简单。只需要在K8s中配置好弹性扩缩容的条件,比如设置CPU使用率的阈值,K8s就能自动监控、自动调整,保证系统始终处于最佳状态。
\\n举个例子,假设你有一个电商系统在双十一期间,流量激增。K8s可以根据CPU使用情况,自动增加容器实例来应对大量并发请求。如果在流量低谷时,K8s又会根据预设条件自动减少实例数,节约资源。
\\n但如果你没有用容器化,而是传统的物理机部署,那要实现弹性扩缩容就没那么简单了。你得有强大的内部基础设施支持,通过运营平台实时监控服务的CPU使用率或者QPS。一旦这些指标超过某个阈值,系统就会自动触发扩缩容操作。虽然实现起来没有K8s那么自动化,但原理是一样的:根据需求动态调整资源。
\\n无论是容器化还是物理机,弹性扩缩容的目标都是一样的:让服务在流量波动中保持高可用,同时也避免资源浪费。
\\n\\n\\n最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。\\n这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
\\n
想让系统扛得住高并发、稳如老狗?架构设计上就得学会“切蛋糕”——分层分模块。
\\n但光分开了还不够,还得让模块之间“各干各的”,这时候就要靠消息队列(比如Kafka、RocketMQ)的异步解耦和削峰能力了。这两招能让你的系统从“一碰就碎”变成“韧性十足”。
\\n想象一下,A模块要调B模块,如果每次都要等B模块回复才能继续干活。但用了消息队列,A模块只需要把消息往队列里一扔(比如订单数据写到Kafka),就可以转身继续处理其他请求。B模块(比如支付系统)什么时候有空了,再去队列里慢慢取消息处理。
\\n例如: 电商平台用RocketMQ处理订单。用户下单后,订单服务直接把消息丢进队列,不用等库存服务、支付服务挨个处理完才给用户反馈。哪怕库存服务突然宕机了,订单数据还在队列里存着,恢复后继续扣库存,用户完全感知不到。
\\n高可用设计的核心不是追求100%不出错(这不可能),而是出错时怎么让用户觉得“问题不大”,甚至无感知。因此业界来评价可用性 SLA都是说多少个 9,比如 4 个 9(99.99%)的可用性。
\\n想象一下双十一零点,每秒百万请求冲进来,服务器眼看要挂。这时候就得学地铁站——限流!比如:
\\n例如前端时间,华哥演唱会开票时,10万人同时开抢,系统只放前1万请求进核心流程,后面的直接展示“排队中”。虽然有人骂,但至少系统没崩,比全员404强多了。
\\n当你发现依赖的服务开始抽风(比如数据库响应从50ms变成5秒),千万别头铁继续调用!这时候要像家里电路跳闸一样,直接切断连接。比如:
\\n系统快扛不住时,得学会“丢车保帅”。比如:
\\n真实案例:微博社交App在明星官宣离婚时,立刻降级非核心功能:
\\n系统出问题不可怕,可怕的是雪崩时没有应急预案。设计系统时,不妨多想一步——如果这个服务挂了,用户能不能至少完成核心操作?
\\n四、存储层面的高可用
\\n现在的互联网应用大部分都是无状态的,所以保证应用服务的高可用其实挺简单的。但是要保证数据存储的高可用就复杂多了,因为数据是有状态的。
\\n\\n\\n那我们到底该怎么保证数据存储的高可用呢?
\\n
数据存储高可用的本质就是通过“冗余”来确保数据的可靠性,简单来说,就是把数据复制到多个地方。
\\n这样不仅能避免数据丢失,还能提升并发能力。因为数据本身是有状态的,所以数据存储的高可用比应用服务的高可用复杂多了。
\\n主要体现在以下几个方面:
\\n\\n\\n\\n
\\n- 数据该怎么复制?各个节点分别负责什么?
\\n- 复制过程中的延迟咋办?
\\n- 复制中断了,怎么办?
\\n
其实解决这些问题的常见方法就是两种:集群存储和分布式存储。
\\n业界大多数方案其实都是围绕这两种方式进行的,或者对它们做了一些衍生和扩展。
\\n集群存储(集中式存储)
\\n所谓集群,其实就是一群逻辑上在做同一个任务的机器。它们可以在同一个机房,也可以分布在不同的机房。集群存储就是把这些机器上的存储资源合在一起,呈现给外部一个统一的存储系统。
\\n集群存储其实适用于那些业务存储量规模不算特别大的场景。对于一般的业务数据存储,集群方式就足够用了。比如现在大家用的Redis、MySQL等,都是采用集群存储方式。对于中大型互联网公司来说,默认就是这么做的。
\\n集群存储的工作方式是:1主多备或者1主多从。数据写入通过主机,读取一般由从机来承担。集群存储要解决的关键问题就是:
\\n\\n\\n\\n
\\n- 主机怎么把数据复制到备机(从机)上?\\n
\\n\\n
\\n- 这里的数据同步就是主机负责的,把数据复制到备机(从机)。不过得注意,主备之间的数据同步有可能会有延迟。
\\n
\\n\\n\\n
\\n- 备机(从机)怎么监控主机状态?\\n
\\n\\n
\\n- 如果主机故障,备机(从机)怎么接管主机的角色,变成新的主机?
\\n
简而言之,在主从架构中,一旦主机挂掉,备机(从机)就可以直接接管,继续提供服务。这也是集群存储的基本思想。
\\n1. 主备复制
\\n主备复制是最常见、最简单的存储高可用方案了。几乎所有的存储系统都能提供这种功能,比如 MySQL、Redis、MongoDB 这些大家熟悉的工具,都是这么做的。
\\n在主备架构里,所谓“备机”主要是用来做备份的,并不会参与实际的读写操作。如果想要让备机变成主机,那就得手动干预了。所以,这种架构一般用于后台管理系统,或者一些不需要高实时性的场景。
\\n2. 主从复制
\\n主从复制和主备复制听起来差不多,但其实是两种不同的设计思路。简单来说,“从”就是听命的意思,“备”是备份的意思。主从复制的“从机”是要真的干活的,它会处理数据的“读”操作,而“主机”负责“写”和“读”操作。
\\n所以,主从架构里,主机负责数据的读写,而从机只负责数据的读取。写操作不会由从机来承担。
\\n3. 主从切换
\\n主备复制和主从复制这两种方案,虽然挺常见,但都有个问题:主机故障后,系统就不能继续写数据了。如果主机坏了,还得人工去指定一个新的主机。
\\n为了解决这两个问题,就出现了主从切换(有时候叫主备切换)。这就是在原有架构上加了一点“智能”,也就是当主机异常时,系统会自动检测并把备机或者从机切换成主机。这个方案在实际应用中非常常见,因为它能保证主机挂掉后,从机能自动顶上,保证业务不出问题。
\\n4. 主主复制
\\n主主复制就是两台机器都当“主机”,它们互相将数据复制给对方。客户端可以随便选择一台机器进行读写操作,没啥限制。但主主复制的一个大挑战是:两台机器之间的数据必须能双向同步。所以,这种架构的实现要求相对较高,需要更强的数据一致性保证。
\\n\\n\\n最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。\\n这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
\\n
分布式存储
\\n集群和分布式听起来有点像,但其实不一样。集群是把几台服务器集中在一起,做同一个业务,而分布式就是把不同的业务分散到不同的地方,形成一个整体。在分布式系统里,每一个节点都有可能是一个小集群。
\\n分布式存储就是通过网络,把企业每台机器的磁盘空间给用上,将这些零散的存储资源整合成一个虚拟的存储设备。数据就像散落在各个角落一样,分布在不同的地方。分布式存储的每台服务器都能处理读写请求,不像集群存储那样只有一个主机负责写数据。可是,虽然没有主机这种角色,还是得有一个“指挥官”来负责数据分配算法,这个“指挥官”可以是独立的服务器,也可以是集群中选举出来的某台机器。
\\n这种分布式存储特别适合海量数据存储,尤其是那些数据量特别大的业务场景。常见的分布式存储系统有Hadoop(HDFS)、HBase、Elasticsearch等。
\\n五、产品层面的高可用
\\n在产品层面上,高可用架构主要是指我们的“兜底”策略,也就是当系统出现问题时,能保证用户体验不至于崩溃。降级和限流策略更多是从后端架构的角度来考虑,而产品层的兜底策略就是从用户的角度,确保即使出现问题,用户也能感知到的是友好的提醒,而不是系统崩塌。
\\n举个例子,假如我们的页面获取不到数据或者不能访问时,我们该如何友好地通知用户呢?像“稍后重试”这种提示就很有用。
\\n再比如,当真实页面无法访问时,产品应该提供一个默认页面。如果后端获取不到真实数据,直接给用户渲染默认的页面,而不是让他们一直等。
\\n再比如,服务器需要停机维护时,产品应该给出一个停机页面,所有用户看到的就是这个页面,不会再请求后端服务,避免系统负担过重。
\\n还有一种情况,比如抽奖活动,万一后台数据出问题,给一个默认的兜底商品,至少能让用户看到一个结果,而不是一片空白。
\\n六、运维部署层面
\\n开发阶段——灰度发布、接口测试设计
\\n说到灰度发布和接口测试,很多人都会觉得这两者是个常见的流程,尤其是在大公司里,基本都会涉及到这些环节。灰度发布是啥?就是服务上线的时候,不是一次性把所有实例都发布出去,而是先发布1-2个实例,然后慢慢放量,看看效果如何。如果没问题,再继续发布,直到所有实例都上线。
\\n接口测试呢,就是每次服务发布前,要先跑一遍接口测试用例,确保接口没有问题,避免上线后出现麻烦。接口测试能让我们在发布前知道,接口是不是能正常工作。
\\n这两项工作,尤其在大公司中,基本都由专门的DevOps团队来负责,确保每次发布都安全、稳定。
\\n开发阶段——监控告警设计
\\n监控告警的设计,听起来可能有点复杂,但在大公司里这通常不是问题。公司会有一群专门的人来搭建这个基础设施,配套的系统也会有,开发同学只需要配置好、使用好就行了。
\\n但如果你在的公司没有这样的基础设施,那就得自己动手了,得接入一些监控系统来保证服务的稳定性。
\\n监控系统
\\n说到监控系统,现在有几个开源的解决方案特别流行,可以帮助我们快速搭建自己的监控体系:
\\n\\n\\nELK (Elasticsearch、Logstash、Kibana) :日志收集和分析。\\n由于微服务化后,日志分布在各个机器上,不能都存在本地,这时候ELK就是不二选择。它能将日志收集起来,做集中管理和分析。
\\n
\\n\\nPrometheus:监控数据收集。\\n它可以帮助我们监控各种系统指标,甚至自定义一些业务指标。
\\n
\\n\\nOpenTracing:分布式全链路追踪。\\n这意味着,你可以追踪一个请求从开始到结束的整个流程,查看涉及到的每一个服务,精准找到问题。
\\n
\\n\\nOpenTelemetry:可观测系统标准。\\n这是个新标准,能结合追踪数据、指标数据和日志数据来观察分布式系统的状态,简化了很多监控的工作。
\\n
这些监控工具都是用来帮我们搭建一个全方位的监控体系。监控的指标会包括以下几个层面:
\\n\\n\\n基础设施层监控:主要是监控网络、交换机、路由器等底层设备。如果这些设备出现问题,那我们的业务服务肯定也会受影响。常见的监控指标包括网络流量(进和出)、丢包情况、连接数等。
\\n
\\n\\n操作系统层监控:包括物理机和容器的监控,主要监控CPU使用率、内存占用、磁盘IO和网络带宽等。
\\n
\\n\\n应用服务层监控:这个层面就比较复杂了,涉及到很多指标,比如主调请求量、被调请求量、接口成功率、失败率、响应时间(包括P99、P95等)等。
\\n
\\n\\n业务自定义监控:每个业务服务都有自己独特的监控指标。例如电商系统里可能需要监控浏览量、支付成功率、发货情况等。
\\n
\\n\\n端用户层监控:这一层关注的是用户实际体验,比如用户访问页面时的耗时、数据获取的耗时等。这个通常是前端或者客户端需要进行统计的。
\\n
这些监控系统和指标的设计,最终目的是为了让我们在服务出现问题时能尽早发现,并且能迅速定位到问题所在,确保系统始终稳定运行。
\\n告警系统
\\n虽然我们通过监控系统已经把所有可能出现的问题都统计清楚了,但如果没有一个实时告警系统,问题出现时我们就无法及时反应,系统出问题后,可能就会错失最佳处理时机,最终导致大规模的故障或者灾难。所以,除了监控,还得有一个能即时报警的系统。
\\n告警系统的设计要做到:
\\n\\n\\n\\n
\\n- 实时性:要做到秒级监控,问题一出现就能发现。
\\n- 全面性:要覆盖公司所有的系统和业务,确保没有遗漏。
\\n- 实用性:告警要分级,轻微问题和严重问题要有不同的预警级别,监控人员可以根据严重程度做出准确的决策。
\\n- 多样性:告警方式要多样,除了常规的短信和邮件,还要有可视化界面,方便监控人员随时发现问题。
\\n
开发阶段——安全性、防攻击设计
\\n防刷、防黑产、防黑客攻击这些防御措施,就是为了保护我们的系统不被外部恶意攻击。通常有几种常见策略:
\\n\\n\\n\\n
\\n- 在公司级的流量入口上做好统一的防刷和鉴权能力,比如统一接入层的封装。
\\n- 在业务服务内部,要做好细致的业务鉴权,比如登录状态的管理,或者加入更多业务层的鉴权逻辑,确保每一步都安全。
\\n
部署阶段——多机房部署(容灾设计)
\\n我们通常会设计高可用策略,确保一个机房内的服务稳定运行,但万一整个机房出问题怎么办?比如地震、火灾,或者光纤被挖断了,那可就麻烦了。这时候就需要有容灾设计,常见的做法就是多机房部署。
\\n\\n\\n\\n
\\n- 服务的多机房部署:这一点相对容易,因为服务大多数是无状态的,只要名字服务能发现不同机房的服务,调用就能顺利进行。但要注意,名字服务或者负载均衡服务需要有就近访问的能力。
\\n- 存储的多机房部署:这就比较难搞了,因为存储是有状态的,部署到不同机房就意味着要考虑存储的同步和复制问题,这要求要更高。
\\n
如果条件有限,至少保证多机房部署业务服务就好,毕竟存储的多机房部署相对复杂。
\\n线上运行阶段——故障演练(混沌实验)
\\n故障演练在大公司里是常见手段,尤其是像Netflix这种大公司,早在2010年就推出了混沌实验工具——Chaos Monkey。这个“混沌实验”对提升复杂分布式系统的健壮性和可靠性非常有效。
\\n故障演练其实挺简单,就是模拟一些极端场景,比如机房断电、网络断开、服务挂掉,看看我们的系统能不能正常运行。这其实是个大工程,得按照混沌实验的框架来规划和设计,但一旦实施,效果非常好。不过,这需要大量人力来做基础设施的开发和支撑。
\\n线上运行阶段——接口拨测设计
\\n接口拨测,说白了就像是巡检。服务上线后,我们需要定时(比如每隔5秒)调用后端的各种接口,检查它们是否正常。如果发现接口异常,就立即报警。
\\n接口拨测通常会有一些配套的工具来支持实现这个功能。如果没有这样的工具,那我们就只能自己开发一个接口拨测(巡检)服务,定期检查那些重要的接口,确保它们随时正常。
\\n七、异常应急层面
\\n虽然前面做了各种保障措施,但毕竟线上服务总会遇到一些不可预见的异常。服务出现问题、无法正常提供服务时,我们还得有一套应急预案,把损失降到最低。
\\n应急预案的核心就是:在业务系统出现问题时,我们需要有明确的恢复步骤,知道第一时间该怎么应对。我们要事先制定好相关的规则和流程,一旦异常情况发生,就能按流程执行,避免手忙脚乱,导致问题扩大。
\\n没有一个完备的应急预案,你的业务在遇到重大故障时,基本上就会瘫痪,完全失去控制。所以,做好预案,时刻保持应急准备,才是线上服务的“救命稻草”。
\\n\\n\\n最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。\\n这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
\\n
本文,已收录于,我的技术网站 cxykk.com:程序员编程资料站,有大厂完整面经,工作技术,架构师成长之路,等经验分享
\\n点赞对我真的非常重要!在线求赞,加个关注我会非常感激!
","description":"“系统又崩了!”——这句话是不是让你头皮发麻? 高可用系统设计是每个技术人必须掌握的硬核技能,但很多人只会堆功能,忽视了系统稳定性的重要性。\\n\\n本文从研发规范、应用服务、存储、产品、运维部署、异常应急六大层面,手把手教你如何设计一个高可用系统,让你的服务坚如磐石,再也不用担心半夜被报警电话叫醒!\\n\\n一、高可用架构和系统设计思想\\n可用性和高可用概念\\n高可用性:从理论到实践,打造坚如磐石的系统\\n什么是可用性?\\n\\n可用性,简单来说就是系统能正常干活的时间比例。它是一个可以量化的指标,计算公式是这样的:\\n\\n可用性 = (总运行时间 - 宕机时间) / 总运行时间\\n\\n行…","guid":"https://juejin.cn/post/7477871457738375218","author":"江小北","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-04T13:52:10.576Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/39e6f06cd1dc49f1a3e3fc410ea752b1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=5%2FEH9lxG6ZI49A8HLZwTB6aqKX8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c4727054ad440858369b069dd1c87d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=f9cav72vN8GMeITmSIcc9njmhRk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9c7abd4d4d9d4c6bacecffc7ea5fb3ee~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=5XCC1j%2FkZWjJXmbOqp30ccp%2B0ug%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/94e24eb5f75941548ecab71e3b9a19e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=7FgTXtMeR9EhbcOqdjGw8mexyqs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a87a466e225248c3ac5daaa01b3dea3d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=Fr6AgReazSN6xj6601BAz3Boq4A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/32ae47a7672c4ede9a54cd2b7e69556b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=t1zbpY4cOw8uX32D8NoPktwodZo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1fff84bfc1904c90a7bac5e512726bac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=GbzyzpDkvL%2B6YqxzCjrR4O7m52o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aef011be096b4e9e91a943c5acc7e66c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=Y81X12xxZXU6of7FX7i2cB2vz%2Bo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b598bbb64e784f5b95252fa5a0af8f96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=BKks9BrFLUyYM%2Fh8rGgSqTUdHYQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/90e9174b313445048680bda7805e7d8f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=OVwv%2FXVWNc8SMFeW5DJ3Eg%2Baup8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d993447db6f04bb2b28f3aa42445369e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=koBmyEIxVtRGpBfjESEAEDKIgME%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd22b4ffe7cb44ef9794ecb65ee2ea98~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rGf5bCP5YyX:q75.awebp?rk3s=f64ab15b&x-expires=1741701130&x-signature=ZTGrU5rFkXFmtwKSNvxZLiyTJ3g%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","架构"],"attachments":null,"extra":null,"language":null},{"title":"Go Web开发提速指南:Gin框架入门与热更新","url":"https://juejin.cn/post/7477883966889951271","content":"大家好,我是长林啊!一个爱好 JavaScript、Go、Rust 的全栈开发者和 AI 探索者;致力于终生学习和技术分享。
\\n本文首发在我的微信公众号【长林啊】,欢迎大家关注、分享、点赞!
\\n在学习了一段时间的 Go 语言基础后,掌握一门编程语言的语法只是第一步,如何运用它来构建实际的项目才是关键。我还查阅了 Go 官方用户 2024 调查报告,找到 Go 应用最广泛的领域调查结果图:
\\n从上图可以看出使用 Go 构建 API/RPC 类型的项目竟高达 76%;对于 Web 开发而言,一个轻量级、高性能的 Web 框架至关重要,而 Gin 正是 Go 生态中最受欢迎的 Web 框架之一。
\\nGin 以其简洁的 API 设计、高效的路由处理和强大的中间件机制,成为了许多开发者的首选框架。相较于标准库 net/http
,Gin 提供了更加优雅且易用的方式来处理路由、请求参数、JSON
解析等功能,同时它基于 httprouter
进行了优化,使其在处理大量请求时依然保持高效,并让开发者可以更加专注于业务逻辑的实现。
\\n\\n本系列文章的环境配置如下:
\\n\\n
\\n- Go:1.24
\\n- Gin:1.10
\\n- OS:Mac M1 14.1.2
\\n- VS Code:1.97.2
\\n
这个倒是没有什么严格的限制,找一个自己熟悉的工具都没问题,比如 VS Code、GoLand 都没有问题,只是 JetBrains 公司的 GoLand 是一个收费的工具,不过可以免费试用 30 天。本系列文章就是用免费的开发者工具 VS Code 进行编码环境的搭建。
\\n使用 VS Code 还需要安装一些插件来辅助开发,当然这也不是必须的,我这里就推荐:
\\n如果不想去挨个挨个安装的话,可以直接安装 Go Extension Pack:
\\n它里面包含了上面提到的 Go 插件。
\\n我们先通过一个简单的 RESTful API 示例,快速了解它的基本使用方式。
\\n创建文件
\\nmkdir 01-hello-world\\n
\\n进入 01-hello-world
初始化项目
> go mod init hello-world\\ngo: creating new go.mod: module hello-world\\ngo: to add module requirements and sums:\\n go mod tidy\\n
\\n如果你的 Go 环境还没有安装 Gin,可以使用 go get
进行安装:
go get -u github.com/gin-gonic/gin\\n
\\n在 01-hello-wrold
目录下创建一个 main.go
文件,编写如下代码:
package main\\n\\nimport (\\n\\"net/http\\"\\n\\n\\"github.com/gin-gonic/gin\\"\\n)\\n\\nfunc main() {\\n// 创建 Gin 路由实例\\nr := gin.Default()\\n\\n// 定义一个简单的 GET 接口\\nr.GET(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 启动服务,监听 8080 端口\\nr.Run(\\":8080\\")\\n}\\n
\\n在这个示例中:
\\ngin.Default()
创建了一个 Gin 路由实例,默认启用了 Logger 和 Recovery 中间件。r.GET(\\"/api/hello\\", func(c *gin.Context) {...})
定义了一个 GET
请求的接口,返回 JSON
格式的响应数据。r.Run(\\":8080\\")
启动 HTTP 服务器,监听 8080 端口。在终端中执行 go run main.go
命令启动服务器,终端效果如下图:
如果一切正常,你会看到类似下面的日志输出:
\\n[GIN-debug] Listening and serving HTTP on :8080\\n
\\n然后,在浏览器或 curl
访问 http://localhost:8080/api/hello
:
curl http://localhost:8080/api/hello\\n
\\n你将会得到如下响应:
\\n{ \\"message\\": \\"Hello, Gin!\\" }\\n
\\n这里就不一一演示 POST、PUT、PATCH、DELETE 等方法了;关键代码如下:
\\npackage main\\n\\nimport (\\n\\"net/http\\"\\n\\n\\"github.com/gin-gonic/gin\\"\\n)\\n\\nfunc main() {\\n// 创建 Gin 路由实例\\nr := gin.Default()\\n\\n// 定义一个简单的 GET 接口\\nr.GET(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 定义一个简单的 POST 接口\\nr.POST(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 定义一个简单的 PUT 接口\\nr.PUT(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 定义一个简单的 PATCH 接口\\nr.PATCH(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 定义一个简单的 DELETE 接口\\nr.DELETE(\\"/api/hello\\", func(c *gin.Context) {\\nc.JSON(http.StatusOK, gin.H{\\n\\"message\\": \\"Hello, Gin!\\",\\n})\\n})\\n\\n// 启动服务,监听 8080 端口\\nr.Run(\\":8080\\")\\n}\\n
\\n\\n\\n你可会觉得这里都在重复写
\\n/api/hello
,如果有这样的疑问就对了,后面我们会介绍到Group
这个方法。
这样,我们就成功搭建了一个 RESTful API,并使用 Gin 处理 GET
、POST
、PUT
、PATCH
、DELETE
请求。
在本地开发 Gin 应用时,你可能会遇到这样的问题:每次修改代码后,都需要手动停止进程、重新编译并启动服务,这无疑影响了开发效率。相比于前端开发中的 Hot Reload(热重载) 机制,Go 语言的标准运行方式显得有些繁琐。
\\n为了解决这个问题,我们可以使用热更新(Hot Reloading) 工具,让 Gin 代码在修改后自动生效,而无需手动重启。常见的 Go 热更新工具包括:
\\n\\n接下来,我们分别介绍 air 和 fresh 的安装与使用方式。
\\n使用 go install
命令全局安装 air
:
go install github.com/air-verse/air@latest\\n
\\n\\n\\n如果在 docker 容器中使用,也可以使用 docker 的安装方式;
\\n
air
允许自定义监控文件和重启规则,可以使用以下命令生成默认配置文件:
air init\\n
\\n这将生成 .air.toml
配置文件,里面包含了代码热更新的规则,你可以根据需要修改。
在项目根目录下运行 air
,它会自动监视代码变动并重启服务:
\\n
现在,你可以修改 main.go
,比如更改 message
的内容,保存后 air
会自动重启,访问 http://localhost:8080/api/hello
就能看到最新的结果。
使用 go install
命令安装 fresh
:
go install github.com/gravityblast/fresh@latest\\n
\\n同样,如果无法找到 fresh
命令,需要将 $GOPATH/bin
加入环境变量:
export PATH=$PATH:$(go env GOPATH)/bin\\n
\\n在项目根目录下运行 fresh
,它会自动监听代码变动并重启服务:
fresh\\n
\\n类似于 air
,fresh
也会在代码变更时自动重新编译并运行应用,但 fresh
的默认配置更加简单,适合小型项目或快速开发。
工具 | 适用场景 | 主要特点 |
---|---|---|
air | 适用于大中型项目,功能更丰富 | 支持 .air.toml 配置,允许自定义监听规则 |
fresh | 适用于小型项目,零配置 | 安装即用,适合快速开发 |
如果你希望有更强的可定制性和稳定性,建议使用 air;如果只是想要简单的热更新功能,Fresh 会更轻量级一些。
\\n在掌握 Go 语言的基础语法后,如何应用它构建实际项目才是关键。而在 Web 开发领域,Gin 作为 Go 生态中最受欢迎的 Web 框架之一,以其高性能、简洁的 API 设计和强大的中间件机制,成为开发者的首选。
\\n介绍 Gin 框架后,通过一个简单的示例实现 RESTful API 的开发,然后我们接着介绍了 air 和 fresh 两种热更新工具,让代码在修改后自动生效,提高开发体验。
\\n后面我们将继续深入学习 Gin 的 路由管理、参数解析、中间件使用 等,以构建更完善的 Web API。技术成长,永远在路上。与君共勉!
","description":"大家好,我是长林啊!一个爱好 JavaScript、Go、Rust 的全栈开发者和 AI 探索者;致力于终生学习和技术分享。 本文首发在我的微信公众号【长林啊】,欢迎大家关注、分享、点赞!\\n\\n在学习了一段时间的 Go 语言基础后,掌握一门编程语言的语法只是第一步,如何运用它来构建实际的项目才是关键。我还查阅了 Go 官方用户 2024 调查报告,找到 Go 应用最广泛的领域调查结果图:\\n\\n从上图可以看出使用 Go 构建 API/RPC 类型的项目竟高达 76%;对于 Web 开发而言,一个轻量级、高性能的 Web 框架至关重要,而 Gin 正是 Go…","guid":"https://juejin.cn/post/7477883966889951271","author":"长林啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-04T13:33:46.537Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f052c26cf55345a8bb2e3e6e93d0d51c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=6XXKJluENkjzPu5LMh7pQRtzUOM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6032b34328664ceab99ab2c1c3364952~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=15WRRElrpnw9f8YGNQfXg8jjCzs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2349febfa0c345a2acef5f5672921730~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=YK9DEnnNmucg6Nnb08saYA2P56Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf540c4ba3824478af939ed9badfd893~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=itJrNIzkpYWsHlknfdO1fqxy2uc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f9add4e076942488c8b008f560efc1e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=SjHN7a%2FRTZyLLAa2Cld7a7Bbm8s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e8ccb93567cc45f8a65cd607915ca690~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=Xq9hYpD2x6grR7dInpJbR%2BfEtlw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cdd94ac4c00d492a9b0e7dd9c64e8777~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ZW_5p6X5ZWK:q75.awebp?rk3s=f64ab15b&x-expires=1741751665&x-signature=wrcWBDzgzOXANKAB2a98Jj0Pnu0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Go"],"attachments":null,"extra":null,"language":null},{"title":"SpringBoot有哪些优点?和Spring、SpringCloud有何不同?","url":"https://juejin.cn/post/7477532065657028635","content":"首先,我们要理解Spring框架的作用。它就像一个\\"大工具箱\\",里面提供了各种各样的\\"工具\\",帮助Java开发者更高效、更便捷地构建应用程序。这些\\"工具\\"包括了IoC容器、AOP面向切面编程、数据访问、事务管理等等。
\\n而SpringBoot呢,它是在Spring的基础上发展而来的一套\\"快速开发工具包\\"。它的目标是简化Spring应用程序的开发和部署流程,让开发者能够以最小的配置和最少的代码快速搭建出生产级别的应用。用一个比喻来说,如果Spring是一个\\"大工具箱\\",那SpringBoot就是一个\\"电动工具套装\\",它将常用的工具进行了组合和优化,使用起来更加方便快捷。
\\nSpringBoot的优点主要有以下几点:
\\nSpringBoot
会根据你添加的依赖自动进行配置,大大减少了手动配置的工作量。SpringBoot
内置了Tomcat、Jetty等Web容器,你可以直接打包成一个可执行的Jar文件,无需额外部署。SpringBoot
提供了一系列起步依赖,它们集成了常用的第三方库,让你可以一站式添加所需功能。SpringBoot
提供了一些生产环境需要的特性,如指标度量、健康检查、外部化配置等。SpringBoot
可以方便地与各种技术和框架集成,如MyBatis
、Redis
、Elasticsearch
等。而SpringCloud,它是构建在SpringBoot之上的一套微服务开发框架。如果说SpringBoot是用来快速开发单个应用程序的,那SpringCloud就是用来开发微服务架构的分布式系统的。它提供了一系列工具来支持服务发现、配置管理、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、决策竞选、分布式会话等操作。
\\n用一个比喻来说,如果我们把微服务比作一个个独立的\\"小机器人\\",那SpringCloud就是一个\\"指挥中心\\",它负责协调和管理这些\\"小机器人\\",让它们能够协同工作,完成更加复杂的任务。
\\nSpring Boot
内置Tomcat/Jetty
服务器,无需手动配置MySQL
驱动包,自动配置数据源)XML
地狱,使用application.yml/properties统一管理配置\\n\\n实际案例:开发一个REST API
\\n传统Spring需要配置:DispatcherServlet、视图解析器、组件扫描...
\\nSpring Boot只需:@SpringBootApplication + 一个main方法
\\n
维度 | Spring Framework | Spring Boot | Spring Cloud |
---|---|---|---|
定位 | 基础框架 | 快速开发脚手架 | 微服务全家桶解决方案 |
配置方式 | 手动配置XML/注解 | 约定大于配置 | 分布式系统专用配置 |
依赖管理 | 需手动管理依赖版本 | starter依赖自动版本控制 | 基于Boot的依赖扩展 |
典型场景 | 传统企业级应用 | 单体/微服务单体 | 微服务集群 |
好比 | 汽车发动机 | 整车组装厂 | 智能交通系统 |
Spring
如同手动档汽车:需要自己换挡(配置)、调校悬挂(组件集成)Spring Boot
如同自动档汽车:一键启动,自适应路况(自动配置)Spring Boot
是造车工厂:快速生产标准化车辆(单个服务)Spring Cloud
是交通管理局:协调车流(服务调用)、设置红绿灯(熔断限流)、管理停车场(注册中心)下面是一个思维导图,展示了Spring、SpringBoot和SpringCloud之间的关系:
\\ngraph TD\\n A[Spring 框架] --\x3e B[SpringBoot]\\n B --\x3e C[SpringCloud]\\n A --\x3e D[IoC容器]\\n A --\x3e E[AOP编程]\\n A --\x3e F[数据访问]\\n A --\x3e G[事务管理]\\n A --\x3e H[...]\\n B --\x3e I[自动配置]\\n B --\x3e J[内嵌容器]\\n B --\x3e K[起步依赖]\\n B --\x3e L[生产就绪特性]\\n B --\x3e M[易于集成]\\n C --\x3e N[服务发现]\\n C --\x3e O[配置管理]\\n C --\x3e P[断路器]\\n C --\x3e Q[智能路由]\\n C --\x3e R[...]\\n
\\n从这个图中我们可以清晰地看出:
\\nSpring
是一个基础框架,提供了各种基本的\\"工具\\"。SpringBoot
是在Spring
基础上的一个快速开发工具包,它简化和优化了Spring
的使用。SpringCloud
是构建在SpringBoot
之上的微服务开发框架,用于开发分布式系统。何时用Spring Boot?\\n├── 需要快速验证原型\\n├── 开发独立服务\\n└── 讨厌复杂配置\\n何时用Spring Cloud?\\n├── 系统拆分为多个微服务\\n├── 需要服务治理\\n└── 处理分布式事务\\n何时用原生Spring?\\n├── 需要深度定制框架\\n├── 遗留系统维护\\n└── 特殊场景需求\\n
\\n2010年:Spring MVC项目\\n→ 要配置20+个XML文件,启动时间3分钟
\\n2014年:Spring Boot诞生\\n→ 零XML配置,启动时间15秒
\\n2016年:Spring Cloud兴起\\n→ 十个微服务协同工作,启动总时间2分钟(但单个服务只需8秒)
\\nSpring Boot是开发者的\\"瑞士军刀\\"
,Spring Cloud是微服务的\\"指挥中枢\\"
,而Spring Framework是支撑它们的\\"基石\\"
。三者如同乐高积木,Boot是标准积木块,Cloud是连接器,Framework则是塑料原料,共同构建出强大的应用体系。
CompositeByteBuf composite = Unpooled.compositeBuffer();\\ncomposite.addComponents(true, headerBuf, payloadBuf);\\n
\\n自定义事件分级:将行情更新、订单成交等事件划分优先级队列
\\n精细化线程模型:
\\n指标 | 传统方案 | Netty方案 |
---|---|---|
单节点TPS | 120,000 | 2,100,000 |
端到端延迟(99.9%) | 850μs | 37μs |
内存消耗 | 8GB/节点 | 2.3GB/节点 |
HashedWheelTimer timer = new HashedWheelTimer(\\n new CustomThreadFactory(\\"netty-timer\\"), \\n 10, TimeUnit.MILLISECONDS, 1024);\\n
\\n channel.config().setWriteBufferHighWaterMark(32 * 1024 * 1024);\\n channel.config().setWriteBufferLowWaterMark(8 * 1024 * 1024);\\n
\\n FileRegion region = new DefaultFileRegion(\\n file, 0, file.length());\\n ctx.writeAndFlush(region);\\n
\\n pipeline.addLast(new ProtocolSelectorHandler());\\n
\\n\\n | 数据规模 | 传统方案吞吐 | Netty方案吞吐 | 资源节省 |\\n |----------|--------------|---------------|----------|\\n | 10GB/s | 3.2GB/s | 9.8GB/s | 67% |\\n | 100GB/s | 28GB/s | 93GB/s | 69% |\\n
\\n public class ProtocolDispatcher extends ByteToMessageDecoder {\\n @Override\\n protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {\\n byte magic = in.getByte(in.readerIndex());\\n if (magic == 0x01) {\\n ctx.pipeline().addLast(new ModbusDecoder());\\n } else if (magic == 0xAA) {\\n ctx.pipeline().addLast(new CoAPDecoder());\\n }\\n }\\n }\\n
\\n // 使用池化分配器\\n ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;\\n ByteBuf buf = alloc.directBuffer(1024);\\n\\n // 内存泄漏检测\\n ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);\\n
\\n channel.write(msg).addListener(future -> {\\n if (!future.isSuccess()) {\\n metrics.markWriteFailure();\\n if (metrics.getFailureRate() > 0.3) {\\n circuitBreaker.trip();\\n }\\n }\\n });\\n
\\n场景 | 连接数 | 吞吐量 | 延迟(P99) | 资源消耗 |
---|---|---|---|---|
金融交易引擎 | 50,000 | 2.1M TPS | 37μs | 4C/8G |
5G信令处理 | 10,000,000 | 120M msg/s | 89ms | 32C/64G |
物联网网关 | 500,000 | 800K cmd/s | 43ms | 16C/32G |
数据总线 | N/A | 93GB/s | 162ms | 48C/128G |
\\n\\n当系统崩溃、用户投诉如潮,你准备好了吗?这篇文章提供了处理技术与业务问题的完整框架。系统性地介绍了问题分类、分级标准、生命周期管理和应急处理方法,包含了从业务分析到技术排查的多种实用策略。如果还有别的问题欢迎大家留言
\\n
级别 | 定义 | 响应时间 | 示例 |
---|---|---|---|
P0 | 系统完全不可用,严重业务中断 | 立即响应 | 支付系统瘫痪 |
P1 | 核心功能受限,影响部分用户 | 30分钟内 | 订单无法完成 |
P2 | 功能可用但体验下降 | 2小时内 | 页面加载缓慢 |
P3 | 小问题,不影响核心业务 | 24小时内 | 非关键UI错误 |
关键变化类型:
\\n变化处理策略:
\\n单机问题特征:
\\n高效摘除策略:
\\n单个API错误处理:
\\n多个API错误处理:
\\n流量应对策略:
\\n依赖问题联动处理:
\\n快速止血措施:
\\n问题边界确定法:
\\n二分法排查:
\\n特征匹配法:
\\n异常数据追踪法:
\\n用户反馈聚类分析:
\\n实时用户操作跟踪:
\\n业务指标异常关联分析:
\\n5W2H分析法:
\\n鱼骨图分析:
\\n决策树分析:
\\n用户旅程图分析:
\\n临时解决方案:
\\n根本解决方案:
\\n监控告警机制:
\\n注意告警需要有分级和降噪,否则问题出现可能有大量告警导致忽略真正问题
\\n日志分析:
\\n监控数据分析:
\\n问题定位工具使用:
\\n问题复现:
\\n系统资源根因分析:
\\n内存问题分析:
\\nCPU问题分析:
\\n磁盘IO问题分析:
\\n代码审查:
\\n依赖服务分析:
\\n数据流分析:
\\n容量评估:
\\n根本原因修复:
\\n修复验证与生产恢复:
\\n绘制应用系统架构图
\\n梳理系统故障等级
\\n依赖分析与压力点识别
\\n故障演练
\\n稳定性建设 -高可用系统建设的必备知识-上
\\n稳定性建设 -高可用系统建设的必备知识-下
一个应用要好用 ,第一点就是要好看 ,看起来又土又旧的应用 ,可能大家都不会看一眼。
\\n之前有几篇已经简单的梳理了一下 PyQT 如何进行布局的方式 ,这一篇我们换一种思路 ,能否让界面美化借助 AI 变得更简单。
\\n\\n\\n❓ 首先要有一个认知 ,PyQT 里面有两种核心的界面管理方式 :
\\n布局管理器
: 建筑结构 ,负责支撑整个建筑的形状和结构QSS
: 内部的装潢和装饰,它决定了空间的美观程度和个性化风格我们只要解决了这两个问题 ,那么一个应用基本上就搭建完成 ,后续进行内容的完善就行了。
\\n为了完成这些 ,我们需要尝试做这些事 : 我需要 AI 尽可能的给我搭建出布局 ,然后基于我给的图片生成 QSS.
\\n❓ 现在看完这两个用法 ,我们需要做的是通过 AI 加速这个过程,主要做两件事 :
\\nAI 大家都会用 ,没什么复杂的 ,但是 AI 实际上是有上限的,不论是上下文 ,还是提示词 ,对于零基础的开发者来说 ,都是一种限制。 我们先来看一下单纯问 ,能设计出什么应用
\\n\\n\\n❓ 思路梳理 :
\\n
一般导航分为 : 菜单栏 / 工具栏 / 状态栏 / 选项卡 等样式。常见的方式就像 VScode 这样 ,顶部是菜单栏 ,侧面是工具栏。
\\n先简单看一下顶部菜单栏的生成 , 直接问 AI 以下几个问题 :
\\n\\n\\n❗ 问题一 : (这里我期望能直接干出一个项目出来)
\\n
> 帮我设计一个 PyQT 的桌面端应用 ,它拥有一个侧边栏 和 一个 顶部菜单栏 \\n- 设计的样式参考这个图片里面的 , 帮我生成多个文件 ,包括主页面 + 子页面 \\n- 同时完善页面切换的逻辑。 样式通过 QSS 引入外部 .qss 文件。 先给我主页面的代码\\n
\\n\\n\\n❗ 问题二 : (很明显,Token数太多,上下文效果也不好,这里我尝试模块化)
\\n
> 界面不好看 ,帮我把 style_sheet 参考这个样式进行设计 : \\n1. 左侧和顶部菜单栏和主内容分开 \\n2. 左侧导航栏支持展开和伸缩 ,展开是图标 + 文字 ,右侧收缩的时候 ,只展示图标 \\n3. 导航栏有背景 ,半透明感\\n\\n
\\n\\n\\n❗ 进行优化 :我开始试图把内容细化 ,让 AI 进行处理
\\n
\\n// 首先切割文件 :\\n第一步 : 优化这段代码 ,把 style_sheet 迁移到一个单独的文件里面 ,在这个 main 里面引入使用\\n\\n// 定点优化 : \\n第二步 : 详细的细化这一块的样式 ,尤其是左侧导航栏参考这个图片里面的样式\\n\\n\\n// 优化表格 (这里我试图让优化的点更加清晰)\\n第三步 :优化这一段代码 ,为 table 添加更多的列 ,同时为 QTableWidget 完善更好看的样式 :\\n1. 左侧序号需要浅灰底 ,小字 \\n2. 顶部头也是浅灰底小字\\n3. 鼠标放在某一行 ,当前行变成浅灰 ,比上面两种更浅\\n\\n// 继续优化\\n第四步 : 优化 QLabel 的 QSS 样式 ,要白底 圆角 ,同时要和外部的组件分隔开 ,高度控制只有一行的高低\\n\\n
\\n\\n\\n总结
\\n
有可以一把生成的
,效果待定 ,而且收费比较贵 @ copyweb.ai/👉 零基础想通过 AI 照搬或者写出一个应用还是有难度的 ,AI 无法纵观全局是最大的问题
既然从零开始有难度 ,那么能否借用开源组件 ,在它上面进行微小的定制 ,借助 AI 的能力 ,减少我们的工作量 :
\\n这里我还是选择最常用的组件进行扩展 : QFluentWidgets
\\n我定义了一个 TreeFrame ,通过 QTreeWidgetItem 定义了一个个小节点 。\\n\\n我现在想要优化每个节点前面的勾选框的样式 ,样式优化的结果如上面的图片 : \\n\\n要求的效果 : \\n1. 前面的勾选框需要白底\\n2. 勾选后复选框变成图片中的蓝色 ,整行也变成图片中的蓝色\\n\\n\\n// 结果 : \\n并没有达到我预期的效果 \\n\\n
\\n\\n\\n❓ 为什么复杂组件也无法实现修改?
\\n
那么我们是否可以借助 AI 来自行绘制一个组件工具呢 ? 可以尝试一下 ,把代码丢给 AI 后 ,它给我生成了一个新的 Tree View 类 。
\\n针对毫无基础或者一点基础的小伙伴 ,AI 现在能做的有限。兄弟们可以先不用担心失业了。 但是 AI 已经能做到很多了 :
\\n最重要的 ,AI 生成的数据效果并不理想 ,没有达到预期
👇👇👇
\\n试了一天 ,我是没有找到很好的办法能快速流水线生产页面 ,大佬们有没有好的途径,欢迎评论区讨论。
\\nGitee:gitee.com/juejinwuyan…
\\nGitHub github.com/juejin-wuya…
\\n开源3周以来,已有130多个关注和Fork
\\n开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是
\\n功能堆砌,大部分功能用不上,需要大量裁剪;
\\n逻辑差异点较多,需要大量修改;
\\n功能之间耦合,难以独立替换某个功能。
\\n由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显
\\n所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?
\\n他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
\\n他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
\\n他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响
\\n开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?
\\nmemberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱,\\n以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!
\\n我认为这很有价值!
\\n这是五阳花了1天时间,模仿京东plus会员和抖音券包产品,借助于memberclub 提供的sdk,新搭建的一套交易系统。
\\n使用效果如下
\\n针对优惠券卡包形式的产品业务,可以支持多商品、多份数等形式的购买,支持购物车提单。最后也展示了购买配额能力。
\\n以上交易提单、履约、售后和结算等模块均依赖 memberclub 提供的sdk实现。
\\n在memberclub中,除业务组件可以自由的编排、扩展和替换之外,服务所依赖的其他中间件和基础能力也可以轻松的替换和扩展。 例如我们为 MQ、缓存、分布式锁、重试组件、延迟组件、分布式配置组件、SPI等定义类接口,使用者可以配置组件名,使用自己的组件。
\\n所以你无需有以下顾虑
\\n下图展示了,memberclub依赖的组件配置,在业务代码中并不依赖组件的具体实现类,而是接口,如果你需要替换,只需要在配置文件中换成你的组件名即可!业务代码不会受到一丝丝影响!
\\n在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。
\\n同时你也可以学习到以下组件的实现原理
\\n订单交易领域划分包括 购买域、履约域、售后域和结算域等。
\\n购买域需提供提交订单、预览订单、取消订单的业务能力,需提供续费购买、自动续费、先享后付、不回本包退、随单搭售、直购、兑换码购买等多样化的购买能力。领域能力则包括会员开通单能力、库存能力、购买配额能力、会员新客能力等。
\\n履约域需提供主单履约、主单逆向履约、周期履约的业务能力,领域能力上包括履约单管理、履约接单完单能力、履约拆合单能力、权益发放能力、周期发放能力等。
\\n售后域需提供售后预览、售后提交的业务能力。领域能力上包括售后可退校验能力、售后金额计算能力、过期退能力、随单退、售后次数限额能力,支持在续费、自动续费、直购、搭售等购买场景的售后。
\\n会员交易订单在多个时点需要进行结算,包括交易结算和离线收入报账,结算模块需提供支付完成、履约、退款、过期等业务变更时点的结算能力。
\\n系统设计时应区分业务能力和领域能力,业务能力是指系统对外部提供的业务能力,不可再细分,如购买域需提供购买预览、购买提单、取消等业务能力。领域能力则为实现该业务能力所必须的能力,如购买域在提单阶段需扣减商品库存、记录用户购买配额数据、记录会员新客标签、记录会员单等,这类系统能力视为领域能力。
\\n为什么要区分业务能力和领域能力呢?
\\n业务中台要负责承接各类业务形态相似的业务系统,一般情况下业务能力是业务必选的能力,中台负责对外提供的标准\\nAPI(也可以由中台提供端到端的接入),而领域能力在不同的产品线上所需不同。如部分产品线不需要库存、购买配额等能力,不需要年卡等周期卡履约能力等。
\\n业务中台如何抽象业务共性、隔离业务差异性、提供快速可靠的扩展机制,是中台建设的重点和难点。业界常见的做法是
\\n1) 抽象共用的业务逻辑为领域能力。
\\n2) 前瞻性的预置扩展点,业务通过插件进行差异化能力扩展
\\n3) 通过流程引擎编排流程,实现流程的差异化配置,实现业务流程的可视化和扁平化。
\\n扩展点在业务中台中无处不在,这是因为业务中台要承接的业务太多,很难保证所有业务完全相同。而业务差异性部分又不能叠罗汉式堆叠在主流程中,因为这意味\\n影响点扩散导致业务隔离性差。即业务特性配置在主流程中,势必潜在影响其他业务。久而久之,系统必定到处是陷阱,难以梳理维护。\\n外加系统架构腐化后的破窗效应,会让后来者倾向于在屎山代码继续堆屎!连重构都变得困难,最终难以收场。
\\n扩展点插件通过业务线和业务域两层路由,在运行时委托某一业务的插件执行业务逻辑,系统主流程依然保持简洁和扁平,改动其中一个业务,完全不会影响到其他业务,系统扩展性和隔离性大大增强!
\\n在实现各个领域能力时,应该依托于领域模型的状态变更执行业务逻辑。如会员交易锁领域能力在预提单、提单成功、预取消、取消成功、履约和履约成功等各个业务状态下\\n实现锁的领域能力。
\\n在购买、履约、售后等主流程中,各领域能力封装到流程节点中,由流程引擎负责编排执行。
\\n会员C 端交易流程要解决以下问题:C端高并发低延迟、业务复杂、数据一致性要求高、资金安全等,因此应尽可能的保证 C 端系统流程设计的简洁。
\\n如C 端在实现库存扣减、用户配额记录、年卡履约等领域能力时,应由商品模型驱动业务流程,而非 C 端根据不同条件决定是否 做某件事,尽可能简化\\nC 端业务逻辑。 商品模型应实现 BC端模型分离。
\\n在 memberclub 项目中你可以学习到
\\n为了提供更好的扩展点,memberclub 对各类基础组件、外部存储系统依赖均抽象了接口,不同接口实现类可通过 Spring\\n条件注入到系统。如你希望自己重写分布式锁组件,那么你可以上线分布式锁接口,将其注入到 Spring 中,就可以直接替换掉原有的分布式锁组件,无需改动原有代码。
\\n在单元测试和standalone模式下,系统不应该依赖各类外部存储系统,因此通过抽象各个基础组件为通用接口,在不同的环境先可配置不同的实现类注入到\\nSpring,保证了系统在单测和 Standalone 模型下的独立运营。
\\n系统提供了 本地组件和 Redis lua 组件。
\\npublic interface DistributeLock {\\n\\n boolean lock(String key, Long value, int timeSeconds);\\n\\n boolean unlock(String key, Long value);\\n}\\n
\\n打印日志时,自动填充用户id和订单Id等通参,无需手动指定
\\n\\n被标注了 Retryable 注解的方法,当抛出异常后,系统可自动重试。重试依然失败,失败请求将自动投递到延迟队列,进行分布式重试 。
\\n@Retryable(maxTimes = 5, initialDelaySeconds = 1, maxDelaySeconds = 30, throwException = true)\\n@Override\\npublic void process(AfterSaleApplyContext context) {\\n try {\\n extensionManager.getExtension(context.toBizScene(),\\n AfterSaleApplyExtension.class).doApply(context);\\n } catch (Exception e) {\\n CommonLog.error(\\"售后受理流程异常 context:{}\\", context, e);\\n throw new AftersaleDoApplyException(AFTERSALE_DO_APPLY_ERROR, e);\\n }\\n}\\n
\\nApplicationContextUtils
可以在Spring 启动后,任意地方通过静态方法获取到 Spring 上下文。确保任意地方获取上下文时,Spring已被加载到静态属性中。
延迟队列组件提供延迟事件触发能力,如异常重试场景往往需要延迟一段时间后再次重试,此时将请求投递到延迟队列可实现延迟重试。
\\n系统实现了本地 DelayQueue 和 Redison 、Rabbitmq 延迟队列能力。
\\n系统提供了本地 RandomUtils 和 Redisson 分布式 ID 组件,你可以接入基于雪花算法的分布式 ID 系统。
\\n@ExtensionConfig(desc = \\"分布式 ID 生成扩展点\\", type = ExtensionType.COMMON, must = true)\\npublic interface IdGenerator extends BaseExtension {\\n\\n public Long generateId(IdTypeEnum idType);\\n}\\n
\\n周期履约、结算过期、售后过期退等场景,均需要系统有一个指定时间大批量任务触发能力。通过抽象通用任务表,实现通用任务延迟触发能力。
\\n集成 mybatis-plus,在数据模型更新时可通过 UpdateWrapper等由各业务线自行扩展,无需手动编写 SQL,扩展性更强!
\\n同时系统实现了 insert ignore 批量插入能力。
\\n通过集成 sharding-jdbc 实现了多数据源的分库分表能力。 使用者自行定义分表规则即可
\\n集成了rabbitmq,并且基于死信队列实现了消息消费的延迟重试能力!
\\n集成 Redisson,使用其延迟队列和分布式 ID 能力。
\\n集成了 RedisTemplate,其中库存更新、用户标签写入更新、分布式锁部分实现通过 RedisLua 脚本实现。
\\n系统集成了 Apollo 作为配置中心,同时系统提供了配置中心的接口,并内置了本地Map和 Apollo 两种组件
\\npublic interface DynamicConfig {\\n\\n boolean getBoolean(String key, Boolean value);\\n\\n int getInt(String key, int value);\\n\\n long getLong(String key, long value);\\n\\n String getString(String key, String value);\\n}\\n
\\n集成了 H2 内存数据库,在开发阶段使用 单元测试 profile 执行,访问内存数据库,不会污染测试环境数据库!
\\nmemberclub # 主项目①pom.xml\\n├── starter # memberclub 的启东入口,Rpc/MQ/Http/Job等流量入口\\n├── common # Common 公共工具类\\n├── sdk # 会员领域能力 sdk\\n├── domain # 领域对象,主要包括 DO、DTO、VO、PO 等\\n├── plugin.demomember # Demo会员(每个会员产品线独占一个 pom 工程)\\n├── infrastruce # 基础设置层,包括rpc下游/mq/redis/apollo/db等下游\\n详细说明\\n├── starter # 启动服务\\n│ ├── controller # Http 入口\\n│ ├── job # Job 入口\\n│ └── mq # MQ 流量入口\\n├── domain # 领域对象\\n│ ├── contants # 常量\\n│ ├── context # 流程引擎和领域服务的上下文对象\\n│ ├── dataobject # 数据对象 DO 等\\n│ ├── entity # 数据库实体类 PO\\n├── sdk # 会员领域能力以 sdk形式对各产品线提供\\n│ ├── common # sdk 公共类工具类如 Topic/配置中心等\\n│ ├── aftersale # 会员售后域(核心)\\n│ ├── config # 配置中心\\n│ ├── event # 会员交易事件领域能力(核心)\\n│ ├── inventory # 会员商品库存领域能力(核心)\\n│ ├── lock # 会员锁(核心)\\n│ └── memberorder # 会员单管理(核心)\\n│ └── membership # 会员身份域(核心)\\n│ └── newmember # 会员新客域\\n│ └── oncetask # 会员任务域\\n│ └── ordercenter # 订单中心域(防腐层)\\n│ └── perform # 会员履约域(核心)\\n│ └── prefinance # 会员预结算域\\n│ └── purchase # 会员购买域\\n│ └── quota # 会员配额域\\n│ └── sku # 会员商品域\\n│ └── usertag # 会员用户标签域\\n├── common # 会员 Common 公共工程,包括各类基础组价实现\\n│ ├── annotation # 常见注解\\n│ ├── extension # 扩展点引擎实现\\n│ ├── flow # 流程引擎实现\\n│ ├── log # 通用日志组件\\n│ ├── retry # 通用分布式重试组件\\n│ ├── util # 通用 Util 工具如 Spring 上下文工具类、加解密、集合类、周期计算、JSON 解析\\n├── infrastructure # 基础设置层,包括rpc下游/mq/redis/apollo/db等下游\\n│ ├── assets # 下游资产服务防腐层和 资产SPI接口 \\n│ ├── cache # 缓存组件\\n│ ├── dynamic_config # 分布式配置中心组件\\n│ ├── id # 分布式 ID 组件\\n│ ├── lock # 分布式锁组件\\n│ ├── mapstruct # mapstruct 接口\\n│ ├── mq # MQ 接口(屏蔽了具体 MQ 接入方式,可独立替换)\\n│ └── mybatis # Mybatis dao 层\\n│ └── order # 订单中心防腐层\\n│ └── retry # 分布式重试组件\\n│ └── swagger # Swagger 配置\\n│ └── usertag # 会员用户标签组件\\n├── plugin.demomember # Demo 会员业务特性\\n│ └── config # 会员配置表\\n│ └── perform # 会员履约域扩展点插件\\n│ └── aftersale # 会员售后域扩展点插件\\n│ └── prefinance # 会员预结算域扩展点插件\\n│ └── purchase # 会员购买域扩展点插件\\n
\\n技术 | 说明 | 官网 |
---|---|---|
SpringBoot | Web应用开发框架 | spring.io/projects/sp… |
MyBatis | ORM框架 | www.mybatis.org/mybatis-3/z… |
RabbitMQ | 消息队列 | www.rabbitmq.com/ |
Redis | 内存数据存储 | redis.io/ |
Druid | 数据库连接池 | github.com/alibaba/dru… |
Lombok | Java语言增强库 | github.com/rzwitserloo… |
Hutool | Java工具类库 | github.com/looly/hutoo… |
Swagger-UI | API文档生成工具 | github.com/swagger-api… |
工具 | 说明 | 官网 |
---|---|---|
IDEA | 开发IDE | www.jetbrains.com/idea/downlo… |
Navicat | 数据库连接工具 | www.formysql.com/xiazai.html |
Postman | API接口调试工具 | www.postman.com/ |
Typora | Markdown编辑器 | typora.io/ |
工具 | 版本号 | 下载 |
---|---|---|
JDK | 1.8 | www.oracle.com/technetwork… |
MySQL | 8.1.0 | www.mysql.com/ |
Redis | 7.0 | redis.io/download |
RabbitMQ | 3.10.5 | www.rabbitmq.com/download.ht… |
Apollo | - | github.com/apolloconfi… |
memberclub 在standalone模式下无需任何中间件即可启动,在集成测试环境默认依赖 mysql/redis/apollo/rabbitmq\\n等中间件。所以如果仅学习使用,可以选择独立启动模式,那么只需要1条命令就可以启动memberclub服务!
\\ngit clone git@gitee.com:juejinwuyang/memberclub.git
\\n进入项目目录下
\\ncd bin && ./starter.sh -e ut
\\n-e ut 是指指定启动模式为 独立启动,不依赖mysql数据库、redis等。(方便学习和展示,实际业务使用应使用集成模式)
\\n然后 git clone 下载memberclub H5项目,地址在 gitee.com/juejinwuyan…
\\n下载完成后,需要下载 HBuilderX IDE 启动H5项目。 HBuilderX 地址: www.dcloud.io/hbuilderx.h…
\\n选择工程,打开IDE 以后,点击运行-> 运行到浏览器。输入地址:http://localhost:8080/#
\\n初始化SQL脚本的位置在: memberclub/memberclub.starter/src/main/resources/sql/initial.sql\\n在mysql client中通过 source执行sql脚本,如下所示
\\nsource memberclub/memberclub.starter/src/main/resources/sql/initial.sql \\n
\\n以下命令初始化一些展示用的sku商品数据
\\nsource memberclub/memberclub.starter/src/main/resources/sql/init_demo_sku.sql \\n
\\n该脚本会自动创建数据库和表。
\\nbrew install rabbitmq
\\n启动rabbitmq
\\nbrew services start rabbitmq
\\nbrew install redis
\\n启动redis
\\nbrew services start redis
\\ndeveloper.aliyun.com/article/136…
\\nmemberclub项目根目录下
\\ncd bin && ./starter.sh
\\n使用mvn命令编译
\\nmvn clean package -P ut
\\n从TestDemoMember 单测类入手,通过断点调试,深入学习。
\\nGitee:gitee.com/juejinwuyan…
\\nGitHub github.com/juejin-wuya…
","description":"项目地址 Gitee:gitee.com/juejinwuyan…\\n\\nGitHub github.com/juejin-wuya…\\n\\n开源3周以来,已有130多个关注和Fork\\n\\n简介\\n\\n开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是\\n\\n功能堆砌,大部分功能用不上,需要大量裁剪;\\n\\n逻辑差异点较多,需要大量修改;\\n\\n功能之间耦合,难以独立替换某个功能。\\n\\n由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显…","guid":"https://juejin.cn/post/7476798809341296677","author":"五阳","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T12:17:35.362Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/21fb59f899914f63947a40c507485223~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741522654&x-signature=JlMHzv4aOePQNf%2BqJfvtfVqTQUk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d0a1460d7eca41c3b7060d7ed4febdd4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741522654&x-signature=FSushexPJ4NgeYXoAZNS38rKijA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/52d407a17dc847a59e4b8a8ae9ee1e87~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741522654&x-signature=JGuxZfJIy4tXW92LP0GZHvpAI1M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/21fb59f899914f63947a40c507485223~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741522654&x-signature=JlMHzv4aOePQNf%2BqJfvtfVqTQUk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d50845ffd7254641987be6313c0e328e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741522654&x-signature=S1mC%2BCzG80yTh8i8IkylJMXwnYk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Trae编程:打造懒人专属的谷歌浏览器翻译插件","url":"https://juejin.cn/post/7476859063478353929","content":"\\n\\n我正在参加Trae「超级体验官」创意实践征文,本文所使用的 Trae 免费下载链接:www.trae.ai/?utm_source…
\\n
下面是完成一个需求的流程图:
\\n嘿,朋友们!👋
\\n在这个信息爆炸的时代,互联网就像一个巨大的知识宝库,而我,一个对新知识充满渴望的探索者,每天都在这片海洋中遨游。我热爱阅读各种英文网站,从科技前沿到文化趣闻,从学术研究到生活小技巧,无一不让我着迷。然而,最近我遇到了一个小小的烦恼——英文翻译。
\\n每次在浏览英文网站时,总会遇到一些不太熟悉的单词或句子。为了弄懂它们的含义,我不得不新建一个标签页,打开翻译工具,小心翼翼地复制那些单词或句子,然后再粘贴进去等待翻译结果。这个过程不仅繁琐,还常常打断我的阅读节奏,让我感到有些沮丧。
\\n有一天,我在使用腾讯云大模型知识引擎时,突然灵光一闪:如果能结合腾讯云的强大知识引擎和DeepSeek的高效翻译能力,设计一个浏览器插件,那该多好啊!这样,我只需要选中需要翻译的内容,就能直接得到翻译结果,再也不用重复那些繁琐的操作了。
\\n于是,我开始了我的“翻译插件”计划。我想象着这个插件的功能:它应该简单易用,用户只需要选中网页上的文字,点击右键选择“翻译”,或者直接通过快捷键触发翻译功能,翻译结果就会立刻显示在页面上,或者弹出一个小窗口。这样,无论是阅读新闻、学习教程,还是研究学术论文,都能无缝切换,让翻译变得轻松又高效。
\\n我还希望这个插件能支持多种语言,不仅限于英文,还能翻译其他外语,比如法语、德语、日语等。这样,无论我在哪里遇到语言障碍,都能轻松解决。
\\n在设计过程中,我遇到了不少技术难题,但每当我想到这个插件能给像我一样的用户带来便利,我就充满了动力。我开始学习前端开发、API 调用,甚至请教了一些技术大牛。经过无数次的尝试和改进,我的“翻译插件”终于初具雏形。
\\n当我第一次使用它成功翻译了一段文字时,那种成就感简直无法用言语表达。我知道,这只是一个开始,未来我还会继续优化它,让它更智能、更便捷。
\\n今天,我想和大家分享这个小小的创意,也许它能改变我们使用互联网的方式。如果你也有类似的烦恼,或者对这个插件感兴趣,欢迎和我一起探索,让它变得更完美!
\\n在开始之前,我首先需要了解腾讯云大模型知识引擎的功能和优势。腾讯云大模型知识引擎是一个强大的语言处理工具,它不仅能提供精准的翻译服务,还能理解上下文语义,支持多种语言的互译。这意味着它不仅能翻译单词,还能翻译复杂的句子和段落,甚至能够处理一些专业术语。
\\n我花了一些时间研究它的文档,了解它的API接口、参数配置以及返回的数据格式。通过这些信息,我明白了如何将它集成到我的插件中,让它为用户提供高效、准确的翻译服务。
\\ncurl
在 Apifox 中测试接口在正式开发之前,我需要确保腾讯云大模型知识引擎的接口能够正常工作,并且符合我的需求。我使用了 curl
命令来测试接口的响应情况。curl
是一个强大的命令行工具,可以用来发送 HTTP 请求,非常适合用来测试 API 接口。
curl -X POST https://api.tencentcloud.com/translate \\\\\\n-H \\"Content-Type: application/json\\" \\\\\\n-H \\"Authorization: YOUR_AUTH_TOKEN\\" \\\\\\n-d \'{\\"text\\": \\"Hello, world!\\", \\"sourceLang\\": \\"en\\", \\"targetLang\\": \\"zh\\"}\'\\n
\\n通过 curl
,我能够快速验证接口的返回结果是否符合预期。为了更方便地管理接口测试,我还使用了 Apifox。Apifox 是一个集成了接口测试、文档管理等功能的工具,它提供了可视化的界面,让我能够更直观地看到接口的请求和响应情况。
设计合适的提示词让AI去编程。
\\n在开发过程中,使用腾讯云大模型知识引擎(DeepSeek)的第一步是找到并了解其API接口。腾讯云提供了详细的文档和多种编程语言的示例代码,方便开发者快速上手。
\\n我们首先找到知识引擎原子能力的 DeepSeek OpenAI对话接口。在文档中,腾讯云提供了多种语言的接口调用示例,包括Python、Node.js和CURL。根据我们的需求,我们选择CURL进行测试。
\\n在使用腾讯云大模型知识引擎之前,需要获取API Key。API Key是调用接口时的身份验证凭证,用于确保接口的安全性和合法性。以下是获取API Key的步骤:
\\n步骤1:登录主账号
\\n注册并通过个人实名认证或企业认证后,登录 腾讯云。如果没有账号,请参考 注册腾讯云。
\\n步骤2:开通服务
\\n知识引擎原子能力大模型对话API已对外开放,可前往 控制台 开通服务。
\\n步骤3:管理API Key
\\n进入控制台 > 立即接入管理,单击创建API Key。
\\n创建完成后,进入API Key管理页面,可以进行新增、查看、删除等操作。
\\n我们通过在线测试普通Apifox来测试接口,地址:app.apifox.com/
\\n可以将腾讯云API文档里的示例拷贝出来,在Apifox中选择导入cURL
的方式新建一个请求,修改对应的参数为自己创建的应用参数,就可以请求执行了,下面是我的请求示例,大家可以自行参考:
curl https://api.lkeap.cloud.tencent.com/v1/chat/completions \\\\\\n-H \\"Content-Type: application/json\\" \\\\\\n-H \\"Authorization: Bearer sk-个人访问令牌API Key\\" \\\\\\n-d \'{\\n \\"model\\": \\"deepseek-r1\\",\\n \\"messages\\": [\\n {\\n \\"role\\": \\"user\\",\\n \\"content\\": \\"你是一位精通多语言的翻译大师,能够准确地将用户输入的内容进行高质量翻译。中译英,英译中。\\"\\n },\\n {\\n \\"role\\": \\"user\\",\\n \\"content\\": \\"我是Bob\\"\\n }\\n ],\\n \\"stream\\": false\\n}\'\\n
\\n可以看到在线测试API成功得到返回的数据。到这里,我们前期的准备工作就做好了。
\\n基于现有代码,现在我们需要整合腾讯云API,建议提前把已有功能告诉Trae,这样也能更好地理解代码背景。输入提示词示例,可结合个人实际情况调整:
\\n# 目的\\n用户需求是基于网页选中的英文内容翻译成中文,中文内容翻译成英文\\n\\n# 用户故事\\n用户在网页上选中一段文字,点击浮动按钮,可以生成翻译后的文字,在侧边栏展示\\n\\n# 翻译使用的AI接口\\n## 腾讯云API\\ncurl https://api.lkeap.cloud.tencent.com/v1/chat/completions \\\\\\n-H \\"Content-Type: application/json\\" \\\\\\n-H \\"Authorization: Bearer sk-个人访问令牌API Key\\" \\\\\\n-d \'{\\n \\"model\\": \\"deepseek-r1\\",\\n \\"messages\\": [\\n {\\n \\"role\\": \\"system\\",\\n \\"content\\": \\"你是一位精通多语言的翻译大师,能够准确地将用户输入的内容进行高质量翻译。中译英,英译中。\\"\\n },\\n {\\n \\"role\\": \\"user\\",\\n \\"content\\": \\"我是Bob\\"\\n }\\n ],\\n \\"stream\\": true\\n}\'\\n\\n\\n\\n## 请求参数说明\\n1. messages下面的第二个content:网页选中内容\\n\\n## 返回数据\\ndata: {\\"id\\":\\"32e0957e199c329fc20522f15b5d2592\\",\\"object\\":\\"chat.completion.chunk\\",\\"created\\":1740319168,\\"model\\":\\"deepseek-r1\\",\\"choices\\":[{\\"index\\":0,\\"delta\\":{\\"role\\":\\"assistant\\",\\"reasoning_content\\":\\"\\\\n好的\\"}}],\\"usage\\":{\\"prompt_tokens\\":37,\\"completion_tokens\\":1,\\"total_tokens\\":38}}\\n\\ndata: {\\"id\\":\\"32e0957e199c329fc20522f15b5d2592\\",\\"object\\":\\"chat.completion.chunk\\",\\"created\\":1740319168,\\"model\\":\\"deepseek-r1\\",\\"choices\\":[{\\"index\\":0,\\"delta\\":{\\"role\\":\\"assistant\\",\\"reasoning_content\\":\\",\\"}}],\\"usage\\":{\\"prompt_tokens\\":3…d2592\\",\\"object\\":\\"chat.completion.chunk\\",\\"created\\":1740319168,\\"model\\":\\"deepseek-r1\\",\\"choices\\":[{\\"index\\":0,\\"delta\\":{\\"role\\":\\"assistant\\",\\"reasoning_content\\":\\"发\\"}}],\\"usage\\":{\\"prompt_tokens\\":37,\\"completion_tokens\\":4,\\"total_tokens\\":41}}\\n\\ndata: {\\"id\\":\\"32e0957e199c329fc20522f15b5d2592\\",\\"object\\":\\"chat.completion.chunk\\",\\"created\\":1740319168,\\"model\\":\\"deepseek-r1\\",\\"choices\\":[{\\"index\\":0,\\"delta\\":{\\"role\\":\\"assistant\\",\\"reasoning_content\\":\\"来\\"}}],\\"usage\\":{\\"prompt_tokens\\":37,\\"completion_tokens\\":5,\\"total_tokens\\":42}}\\n\\n## 返回参数说明\\n1. reasoning_content为返回内容,提取全部并合并为一句话\\n\\n# 注意\\n1. 注意使用manifest v3版本开发\\n2. 注意中文编码问题\\n\\n# 任务\\n根据 用户故事 和提供的 生成思维导图接口,以及相关注意点,请优化当前谷歌插件\\n
\\n\\n
最终效果已满足需求了!!!
\\n在谷歌的扩展程序中,选择加载已减压的程序(打开我们的文件夹)即可。
\\n在当今信息爆炸的时代,阅读外文资料已成为获取前沿知识的重要方式。然而,频繁切换翻译工具的繁琐过程常常打断我的阅读节奏,让我感到困扰。于是,我萌生了一个想法:利用腾讯云大模型知识引擎(DeepSeek)开发一款谷歌浏览器翻译插件,让翻译变得轻松又高效。
\\n开发这款插件的过程充满了挑战与成就感。我首先深入了解了腾讯云大模型知识引擎的强大功能,它不仅能精准翻译单词和句子,还能理解上下文语义,支持多种语言互译。通过研究API文档,我掌握了接口的调用方式,并用curl
和Apifox进行接口测试,确保其稳定性和准确性。
在编程阶段,我基于谷歌浏览器扩展程序框架,编写了背景脚本、内容脚本和样式文件。背景脚本负责调用腾讯云API处理翻译请求,内容脚本则在网页上创建浮动按钮和侧边栏,让用户可以轻松触发翻译功能。经过反复调试和优化,插件最终实现了选中文字后即时翻译的效果,支持中英互译。
\\n如今,这款翻译插件已经成为我浏览外文网页时的得力助手。它不仅让我摆脱了频繁切换翻译工具的烦恼,还提升了我的阅读效率。更重要的是,它让我深刻体会到技术的力量和开发的乐趣。未来,我将继续优化插件功能,支持更多语言和场景,希望它能帮助更多像我一样热爱学习新知识的朋友,让语言不再成为获取知识的障碍。
\\ncontent.css
\\n#translate-btn {\\n position: absolute;\\n z-index: 10000;\\n padding: 5px 10px;\\n border: none;\\n border-radius: 4px;\\n background: #4285f4;\\n color: white;\\n cursor: pointer;\\n}\\n\\n#translate-sidebar {\\n position: fixed;\\n top: 0;\\n right: 0;\\n width: 300px;\\n height: 100vh;\\n background: white;\\n box-shadow: -2px 0 5px rgba(0,0,0,0.1);\\n z-index: 10000;\\n display: flex;\\n flex-direction: column;\\n}\\n\\n#translate-close-btn {\\n position: absolute;\\n top: 10px;\\n right: 10px;\\n width: 24px;\\n height: 24px;\\n border: none;\\n background: none;\\n font-size: 20px;\\n cursor: pointer;\\n color: #666;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n border-radius: 50%;\\n}\\n\\n#translate-close-btn:hover {\\n background: #f0f0f0;\\n}\\n\\n#translate-content {\\n padding: 20px;\\n overflow-y: auto;\\n flex-grow: 1;\\n}\\n\\n.translation-result {\\n margin: 10px 0;\\n}\\n\\n.translation-result h3 {\\n margin: 10px 0 5px;\\n color: #333;\\n font-size: 14px;\\n}\\n\\n.translation-result p {\\n margin: 0;\\n line-height: 1.4;\\n}\\n\\n.error {\\n color: red;\\n} \\n
\\nbackground.js
\\n// 监听来自 content script 的消息\\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\\n if (request.type === \'translate\') {\\n // 处理翻译请求\\n handleTranslation(request.text)\\n .then(result => {\\n sendResponse(result);\\n })\\n .catch(error => {\\n sendResponse({ success: false, error: error.message });\\n });\\n return true; // 保持消息通道开启以进行异步响应\\n }\\n});\\n\\n// 处理翻译请求的函数\\nasync function handleTranslation(text) {\\n try {\\n const response = await fetch(\'https://api.lkeap.cloud.tencent.com/v1/chat/completions\', {\\n method: \'POST\',\\n headers: {\\n \'Content-Type\': \'application/json\',\\n \'Authorization\': \'Bearer sk-hZsNO8sajITBlRzgA9d0iKjC3Mu4ez2WSfoBvYBPPthUbc0l\'\\n },\\n body: JSON.stringify({\\n model: \\"deepseek-r1\\",\\n messages: [\\n {\\n role: \\"system\\",\\n content: \\"你是一位精通多语言的翻译大师,能够准确地将用户输入的内容进行高质量翻译。中译英,英译中。\\"\\n },\\n {\\n role: \\"user\\",\\n content: text\\n }\\n ],\\n stream: false\\n })\\n });\\n\\n if (!response.ok) {\\n throw new Error(`HTTP error! status: ${response.status}`);\\n }\\n\\n const data = await response.json();\\n \\n if (!data.choices || !data.choices[0] || !data.choices[0].message) {\\n throw new Error(\'Invalid response format\');\\n }\\n\\n return {\\n success: true,\\n translation: data.choices[0].message.content\\n };\\n } catch (error) {\\n console.error(\'Translation error:\', error);\\n return {\\n success: false,\\n error: \'翻译请求失败: \' + error.message\\n };\\n }\\n} \\n
\\ncontent.js
\\n// 创建浮动按钮和侧边栏\\nconst floatingButton = document.createElement(\'button\');\\nfloatingButton.id = \'translate-btn\';\\nfloatingButton.textContent = \'翻译\';\\nfloatingButton.style.display = \'none\';\\n\\nconst sidebar = document.createElement(\'div\');\\nsidebar.id = \'translate-sidebar\';\\nsidebar.style.display = \'none\';\\n\\n// 添加关闭按钮\\nconst closeButton = document.createElement(\'button\');\\ncloseButton.id = \'translate-close-btn\';\\ncloseButton.innerHTML = \'×\';\\nsidebar.appendChild(closeButton);\\n\\n// 添加内容容器\\nconst contentContainer = document.createElement(\'div\');\\ncontentContainer.id = \'translate-content\';\\nsidebar.appendChild(contentContainer);\\n\\ndocument.body.appendChild(floatingButton);\\ndocument.body.appendChild(sidebar);\\n\\n// 监听关闭按钮点击事件\\ncloseButton.addEventListener(\'click\', () => {\\n sidebar.style.display = \'none\';\\n});\\n\\n// 监听文本选择事件\\ndocument.addEventListener(\'mouseup\', (e) => {\\n const selectedText = window.getSelection().toString().trim();\\n \\n if (selectedText) {\\n // 显示浮动按钮在选中文本附近\\n const selection = window.getSelection();\\n const range = selection.getRangeAt(0);\\n const rect = range.getBoundingClientRect();\\n \\n floatingButton.style.display = \'block\';\\n floatingButton.style.top = `${window.scrollY + rect.bottom + 10}px`;\\n floatingButton.style.left = `${window.scrollX + rect.left}px`;\\n } else {\\n floatingButton.style.display = \'none\';\\n }\\n});\\n\\n// 点击翻译按钮\\nfloatingButton.addEventListener(\'click\', async () => {\\n const selectedText = window.getSelection().toString().trim();\\n \\n if (!selectedText) {\\n return;\\n }\\n\\n try {\\n sidebar.style.display = \'block\';\\n contentContainer.innerHTML = \'<p>正在翻译中...</p>\';\\n\\n // 通过 background script 发送翻译请求\\n const response = await chrome.runtime.sendMessage({\\n type: \'translate\',\\n text: selectedText\\n });\\n\\n if (response.success) {\\n contentContainer.innerHTML = `\\n <div class=\\"translation-result\\">\\n <h3>原文:</h3>\\n <p>${selectedText}</p>\\n <h3>译文:</h3>\\n <p>${response.translation}</p>\\n </div>\\n `;\\n } else {\\n throw new Error(response.error || \'翻译失败\');\\n }\\n } catch (error) {\\n console.error(\'翻译出错:\', error);\\n contentContainer.innerHTML = `<p class=\\"error\\">翻译失败: ${error.message}</p>`;\\n }\\n}); \\n
\\nmanifest.json
\\n{\\n \\"manifest_version\\": 3,\\n \\"name\\": \\"智能翻译助手\\",\\n \\"version\\": \\"1.0\\",\\n \\"description\\": \\"选中文本即时翻译(中英互译)\\",\\n \\"permissions\\": [\\n \\"activeTab\\",\\n \\"scripting\\",\\n \\"storage\\",\\n \\"tabs\\"\\n ],\\n \\"host_permissions\\": [\\n \\"https://api.lkeap.cloud.tencent.com/*\\"\\n ],\\n \\"action\\": {\\n \\"default_popup\\": \\"popup.html\\",\\n \\"default_icon\\": {\\n \\"16\\": \\"icons/icon16.png\\",\\n \\"48\\": \\"icons/icon48.png\\",\\n \\"128\\": \\"icons/icon128.png\\"\\n }\\n },\\n \\"icons\\": {\\n \\"16\\": \\"icons/icon16.png\\",\\n \\"48\\": \\"icons/icon48.png\\",\\n \\"128\\": \\"icons/icon128.png\\"\\n },\\n \\"background\\": {\\n \\"service_worker\\": \\"background.js\\",\\n \\"type\\": \\"module\\"\\n },\\n \\"content_scripts\\": [\\n {\\n \\"matches\\": [\\"<all_urls>\\"],\\n \\"css\\": [\\"styles/content.css\\"],\\n \\"js\\": [\\"content.js\\"]\\n }\\n ]\\n} \\n
\\npopup.html
\\n<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <title>智能翻译助手</title>\\n <style>\\n body {\\n width: 300px;\\n padding: 10px;\\n font-family: Arial, sans-serif;\\n }\\n \\n .title {\\n font-size: 16px;\\n font-weight: bold;\\n margin-bottom: 10px;\\n color: #333;\\n }\\n \\n .description {\\n font-size: 14px;\\n color: #666;\\n line-height: 1.4;\\n }\\n \\n .instructions {\\n margin-top: 10px;\\n padding: 10px;\\n background: #f5f5f5;\\n border-radius: 4px;\\n }\\n \\n .instructions li {\\n margin: 5px 0;\\n font-size: 13px;\\n }\\n </style>\\n</head>\\n<body>\\n <div class=\\"title\\">智能翻译助手</div>\\n <div class=\\"description\\">\\n 选中网页文本即可快速翻译,支持中英互译。\\n </div>\\n <div class=\\"instructions\\">\\n 使用说明:\\n <ol>\\n <li>在网页上选中需要翻译的文本</li>\\n <li>点击出现的\\"翻译\\"按钮</li>\\n <li>翻译结果将在右侧边栏显示</li>\\n </ol>\\n </div>\\n</body>\\n</html> \\n
","description":"我正在参加Trae「超级体验官」创意实践征文,本文所使用的 Trae 免费下载链接:www.trae.ai/?utm_source… Trae编程:打造懒人专属的谷歌浏览器翻译插件\\n目录\\n\\n效果展示\\n\\nAI编程开发流程\\n\\n2.1 需求分析\\n2.2 整体思路\\n\\n寻找腾讯云大模型知识引擎的API\\n\\n3.1 访问腾讯云知识引擎API文档\\n3.2 获取个人访问令牌API Key\\n\\n在线测试API\\n\\nTrae编程集成谷歌插件\\n\\n使用方法\\n\\n总结\\n\\n源码\\n\\n1.效果展示\\n\\n2.AI编程开发流程\\n\\n下面是完成一个需求的流程图:\\n\\n2.1需求分析…","guid":"https://juejin.cn/post/7476859063478353929","author":"LucianaiB","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T11:12:53.687Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7350e541a55f4bf796b28af9e4979d78~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=b%2FR%2BEjkUmPThveVz%2B6Pq%2BqIM9t8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/53ff2ccefcf24ba9bb43c64b65c7f8d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=JZP7OMMbJv4kGxn1Z0ehY9TiEYU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4db29c3b24714103b404e37e67d058ac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=7USK7RTKjLSeq6Efh1MlFBl%2FtwM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cb450a4d3ee84681be57a0364c8ecf8f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=%2FO%2Fp%2Bv1zKZKNp3e%2BBE1mciJyAbg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e6cba64a48fa4738b836143ae3b080e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=tSQ4iTxeYg9ZXSmKh0O%2FCqe4Eoc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7ec5d1062169409499144c6ed6a50148~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=f2WsB3%2FLeH9G5CgiUUG%2Bb0cG2KM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/895e24e0c1644ba3920971b1f2a92c15~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=706ks93mc79yjtWw9UXOj3y05uI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/becbc4f9d08e40119e210e578d7fc9c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=PiS%2Fy8G0hl4MFJxr2zXDszu69Cs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6dffbe07ff74c0ab7aa313084d41404~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=zrY%2FwDg3xX%2BGKdvfjMS4KsLt88Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91fa42232e1a42a1a7e25ec0446a0379~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=Nk4337kryGQRvPrcxFWFnV%2Bg19Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7350e541a55f4bf796b28af9e4979d78~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=b%2FR%2BEjkUmPThveVz%2B6Pq%2BqIM9t8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7350e541a55f4bf796b28af9e4979d78~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=b%2FR%2BEjkUmPThveVz%2B6Pq%2BqIM9t8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/127cead457af46e9951db8e55fd62931~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=m0KNPltxB7vFdpUVwAwUSTf%2B64o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d78b9b3392dd434e81419fc8e91c8e2a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=7n3iRhMTKeT54G4aVZ4lCMbGR9c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa488e06ea65452198ed4199d5025fb7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTHVjaWFuYWlC:q75.awebp?rk3s=f64ab15b&x-expires=1741518772&x-signature=Juop8m4OdDfp%2BJvGaoJqLUepH54%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Trae","OpenAI"],"attachments":null,"extra":null,"language":null},{"title":"2025防失业预警:不会用DeepSeek-RAG建知识库的人正在被淘汰","url":"https://juejin.cn/post/7476780955920777270","content":"\\n\\n作者:后端小肥肠
\\n姊妹篇:
\\nDeepSpeek服务器繁忙?这几种替代方案帮你流畅使用!(附本地部署教程)-CSDN博客
\\n10分钟上手DeepSeek开发:SpringBoot + Vue2快速构建AI对话系统_springboot deepseek-CSDN博客
\\n
前几天,老板和我们开了个会,提到一个让人深思的事情:越来越多的客户开始询问,能不能把DeepSeek接入他们的系统,帮助他们管理数据、合同和一些文件。那一刻,我突然意识到,AI技术已经不仅仅是个高大上的话题,它正在切实改变着我们工作和生活的方式。
\\n客户的需求很简单,但背后却透露着一个深刻的问题——他们希望通过AI来提高效率、减少错误。对于很多传统企业来说,信息的混乱和管理的漏洞已经成为不可忽视的痛点。想象一下,如果这些企业能够通过DeepSeek-RAG技术,构建一个强大的知识库来帮助他们自动化处理这些信息,那么工作效率和决策质量将会有多大提升。
\\n我开始意识到,知识库的构建正在成为未来竞争力的一部分。尤其是在AI幻觉频发的今天,单纯依赖模型生成的内容是有风险的,而通过精准的知识库来辅助AI工作,能够有效避免错误的发生。也正是因此,我决定写这篇文章,分享如何基于AnythingLLM构建DeepSeek-RAG本地知识库,并帮助传统企业从中受益。掌握这种技术,将不仅仅是提升工作效率,更是走在未来职场前沿的关键。
\\n随着人工智能技术的飞速发展,越来越多的企业和个人将AI作为决策支持的核心工具。然而,这种过度依赖和迷信AI的现象,实际上可能带来一些严重的问题——那就是AI幻觉。简单来说,AI幻觉指的是人工智能在生成内容时,出现了与事实不符、逻辑断裂或脱离上下文的情况。AI模型虽然能生成看似可信的答案,但其背后的推理和依据可能是错误的。
\\n这一问题不仅仅存在于专业领域,许多人在日常生活中也会因过度信任AI而无意间陷入幻觉中。
\\nAI幻觉主要有两种形式:事实性幻觉和忠实性幻觉。事实性幻觉是指AI生成的内容与实际世界的事实不一致,例如错误的历史事件或科学数据;而忠实性幻觉则指的是AI的回答虽然在事实上没有错,但与用户的真实意图或问题的上下文不符。
\\n那么,为什么AI会产生幻觉呢?首先,AI模型依赖于训练数据,这些数据可能存在偏差或错误,导致AI在生成内容时不符合实际情况。其次,当AI遇到未知的复杂情境时,它可能会出现泛化问题,无法有效处理超出训练数据范围的内容。再者,由于大多数AI模型缺乏自我更新和实时学习的能力,它们会在面对新信息时产生过时的回答。
\\n随着AI技术的普及,越来越多的人和企业开始盲目依赖AI做出决策,而忽视了AI潜在的幻觉问题。这种过度迷信AI的行为,不仅会导致错误的判断,还可能在不知不觉中影响我们的生活和工作。认识到AI幻觉的存在,并采取措施应对,将是我们在AI时代避免被误导的关键。
\\n为了降低生成式人工智能中的幻觉风险,可以采取以下措施:
\\n模型微调
\\n优点:
\\n缺点:
\\n构建本地知识库与检索增强生成(RAG)技术
\\n优点:
\\n缺点:
\\n限制模型响应范围
\\n优点:
\\n缺点:
\\n持续测试与优化
\\n优点:
\\n缺点:
\\n综上所述,为了有效降低AI幻觉风险,通常需要将多种措施结合使用。在实际应用中,构建本地知识库与检索增强生成(RAG)技术是一种有效的方案。
\\nEmbedding是将文本转化为固定维度数值向量的技术,这些向量能够帮助AI模型理解和处理文本数据。通过Embedding,AI能够计算文本之间的语义相似度,从而提升搜索、问答等任务的准确性。在构建知识库时,Embedding技术可以将文件和数据转化为向量,使得知识库能够更智能地匹配和检索相关信息。常见的Embedding方法包括Word2Vec、GloVe、BERT等,这些方法在实际应用中可以帮助改善语义理解和信息处理的效率(Embedding我会在后续文章细讲,本文里只是让大家有个了解,感兴趣的朋友可以点点关注,后续会更新相关内容
)。
Ollama 是一个让用户可以在本地计算机上运行 AI 语言模型的工具,省去了连接云端和复杂配置的麻烦。它的优点包括:
\\n访问官网:Ollama.com,页面应显示一个羊驼🦙,如果不是,说明你进入了错误的页面。然后,点击页面下方的【下载】按钮。
\\n选择适配你系统的版本,进行下载:
\\n点击【Install】进行安装
\\n有一个点要注意一下,Ollama 下载模型的默认位置是在 C 盘,在下载模型之前,我们需要更改磁盘位置,步骤如下:
\\n在【我的电脑】处点击鼠标右键弹出菜单,选择【属性】,进入设置界面后选择【高级系统设置】:
\\n在弹出的系统属性弹窗中点击【环境变量】:
\\n在【系统变量】区域点击【新建】按钮,在变量名处输入OLLAMA_MODELS,点击【浏览目录】,选择模型存放位置:
\\n设置完成以后记得保存环境变量配置(很重要)。
\\n需要注意的是刚刚我们下载的Ollama 程序也是放在C盘的,C盘空间容易爆满,这里我们还需要再操作异步,把Ollama 全部文件夹迁移到一个空间大一些的盘(我选的F盘),如果你C盘空间很多,可以直接跳到下一节
。
在F盘新建Ollama文件夹,新建exe、logs、models三个文件夹:
\\n然后去默认安装文件夹把里面的内容都复制到刚刚创建的文件夹里面,默认安装文件夹如下:
\\nC:\\\\Users\\\\用户名\\\\.ollama ------------------------存放大模型\\n\\nC:\\\\Users\\\\用户名\\\\AppData\\\\Local\\\\Ollama------------------------存放日志\\n\\nC:\\\\Users\\\\用户名\\\\AppData\\\\Local\\\\Programs\\\\Ollama------------------------存Ollama程序\\n\\n
\\n将文件夹里面的内容搬走,删掉 C 盘他们原来的文件夹,打开命令提示符,输入:
\\n\\nmklink /D C:\\\\users\\\\用户名.ollama F:\\\\Ollama\\\\models\\n\\nmklink /D C:\\\\users\\\\用户名\\\\AppData\\\\Local\\\\Ollama F:\\\\Ollama\\\\logs\\n\\nmklink /D C:\\\\users\\\\用户名\\\\AppData\\\\Local\\\\Programs\\\\Ollama F:\\\\Ollama\\\\exe\\n\\n
\\n上述命令旨在通过创建软链接,将原本需要位于 C 盘的文件夹重定向到 F 盘,以满足某些脚本必须在 C 盘运行的要求,同时避免占用 C 盘空间。具体操作如下:
\\nC:\\\\users\\\\用户名.ollama
重定向到 F:\\\\Ollama\\\\models
。C:\\\\users\\\\用户名\\\\AppData\\\\Local\\\\Ollama
重定向到 F:\\\\Ollama\\\\logs
。C:\\\\users\\\\用户名\\\\AppData\\\\Local\\\\Programs\\\\Ollama
重定向到 F:\\\\Ollama\\\\exe
。通过这些操作,用户可以在 F 盘上访问 Ollama 相关的文件和程序,同时满足脚本对 C 盘路径的要求。
\\n如果提示没有权限,就以管理员的身份运行:
\\n\\n操作完成以后回到原始放置Ollama文件的C盘对应目录看一下,变成下图这样,软连接就创建好了:
最后一步,检查Ollama是否安装成功,命令提示符窗口输入ollama -v,如下图所示就是安装成功了:
\\n下载大模型打开 Ollama 官网 ollama.com/ ,点击的【Models】,选择合适你的模型进行下载(我选择的是1.5b),如果你不知道你的电脑可以下载什么规格的模型可以去看一下我这篇文章的第三章:blog.csdn.net/c1821359022…
\\n将模型拉取命令复制到命令提示符窗口中,按回车键,出现Send a meesage即为下载成功:
\\n到这一步我们就能直接使用了:
\\n在Send a meesage处输入/?就可以获得操作帮助:
\\n输入/bye可以退出:
\\n如果想重新对话,还是输入ollama run deepseek-r1:1.5b:
\\n在知识库的构建中,我们采用AnythingLLM来作为知识库的UI,你也可以选择其他UI工具,比如 Dify,Fastchat, AnythingLLM 相对来说 0 代码基础的小伙伴就可以操作了,所以这里我们先以 AnythingLLM 为例。网址:anythingllm.com/ ,打开链接后界面如下:
\\n点击【Download for desktop】选择合适的版本:
\\n安装步骤就略过了,都是傻瓜式的,但是有一点需要注意,不要把软件装在C盘,选一个空间足够的盘:
\\n下载好后,点击运行,软件的界面如下,点击【开始】
\\n选择【Ollama】后自动加载我们已经下载好的大模型,选择合适的模型后点击下一步按钮,之后都是点击下一步按钮:
\\n输入工作区名称:
\\n点击【小肥肠科技公司合同管理】,进入默认对话界面,这个界面就是我刚刚在本地部署的的大模型DeepSpeek-r1:1.5b,可以直接和它对话(因为我的模型规格只有1.5b,所以它的回答有点可笑,显存高的读者尽量选择规格高的模型
):
接下来就是拉取向量化模型,这里我拉取的是Nomic-Embed-Text 模型(他有很强的长上下文处理能力),打开命令提示符窗口,输入ollama pull nomic-embed-text:
\\n设置Embedding模型,确保你的LLM 首选项为Ollama:
\\n现在一切基础工作准备就绪,接下来就是投喂资料了,我造假了一个喵喵星球租房的资料:
\\n\\n\\n租房合同
\\n甲方(出租方):豆豆
\\n
\\n乙方(承租方):小肥肠科技有限公司根据喵喵共和国相关规定,甲乙双方就租赁房屋事宜达成如下协议:
\\n一、房屋基本情况
\\n
\\n房屋位置:喵喵共和国,猫爪街14号
\\n房屋面积:150平方米
\\n房屋类型:高层公寓二、租赁期限
\\n
\\n租期自2025年1月1日起至2025年12月30日止,期满后可续租。三、租金及支付方式
\\n
\\n租金为小鱼干30个/月,乙方应于每月1日前支付。
\\n押金:小鱼干100个,租期结束后无损坏可退还。四、甲方责任
\\n
\\n确保房屋符合安全标准,负责主要结构及设施维修。五、乙方责任
\\n
\\n按时支付租金,妥善使用房屋设施,如有损坏负责修理。六、提前解除合同
\\n
\\n需提前30个喵喵日通知对方,违约方需支付租金50%的违约金。七、争议解决
\\n
\\n双方可协商解决争议,若协商不成,可向猫咪法院提起诉讼。八、合同生效
\\n
\\n本合同一式两份,签字盖章后生效。甲方(签字):豆豆
\\n
\\n乙方(签字):小肥肠科技有限公司
\\n签订日期:2025年1月1日
上传资料,点击上传按钮,弹出上传资料的界面:
\\n点击【Click to upload or drag and drop】上传资料(也可以直接拖拽进去):
\\n上传完成后点击【Move to Workspace】:
\\n点击【Save and Embed】:
\\n到此DeepSeek-RAG和本地知识库的构建内容完结,需要注意的是本地部署很吃电脑配置,尽量选配置高一点的电脑,如果电脑配置实在不给力也可以选择联网版本的RAG(后续更新的智能体文章会讲
)。
如果你对DeepSpeek的相关知识还不熟悉,可以关注gzh后端小肥肠,点击底部【资源】菜单获取DeepSpeek相关教程。
\\n随着人工智能的不断发展,AI已经不仅仅是一个技术工具,它正在深入改变我们的工作和生活方式。在信息处理、数据管理等领域,AI的应用已经成为提升效率、减少错误的关键。DeepSeek-RAG和本地知识库的构建,是确保AI高效、可靠运作的核心所在。掌握这些技术,不仅能让你走在技术前沿,更能帮助你在未来的职场竞争中占据先机。如果本文对你有帮助,请给小肥肠点点关注,小肥肠将持续更新更多AI领域干货内容,你的关注是小肥肠最大更新动力哦~
\\n1. 自定义协议栈(避免粘包/拆包)
\\npublic class GameProtocolDecoder extends ByteToMessageDecoder { \\n @Override \\n protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { \\n if (in.readableBytes() < 4) return; // 头部长度不足,等待 \\n in.markReaderIndex(); \\n int length = in.readInt(); \\n if (in.readableBytes() < length) { \\n in.resetReaderIndex(); // 数据不完整,重置读取位置 \\n return; \\n } \\n byte[] data = new byte[length]; \\n in.readBytes(data); \\n out.add(GamePacket.parseFrom(data)); // Protobuf 反序列化 \\n } \\n} \\n
\\n2. 连接管理与心跳检测
\\n// 用户连接管理 \\npublic class GameSessionManager { \\n private static final ConcurrentHashMap<Long, Channel> sessions = new ConcurrentHashMap<>(); \\n\\n public static void addSession(Long userId, Channel channel) { \\n sessions.put(userId, channel); \\n channel.closeFuture().addListener(future -> sessions.remove(userId)); \\n } \\n} \\n\\n// 心跳处理 \\nch.pipeline().addLast(new IdleStateHandler(30, 0, 0)); \\nch.pipeline().addLast(new HeartbeatHandler()); \\n
\\n3. 异步事件驱动架构
\\nCompositeByteBuf
合并多个 Buffer,减少内存复制。1. WebSocket 全双工通信
\\npublic class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> { \\n @Override \\n protected void initChannel(SocketChannel ch) { \\n ch.pipeline() \\n .addLast(new HttpServerCodec()) \\n .addLast(new HttpObjectAggregator(65536)) \\n .addLast(new WebSocketServerProtocolHandler(\\"/ws\\")) \\n .addLast(new IMHandler()); // 自定义消息处理器 \\n } \\n} \\n
\\n2. 分布式会话管理
\\n3. 消息压缩与加密
\\n// Protobuf + GZIP 压缩 \\npublic class MessageEncoder extends MessageToByteEncoder<IMessage> { \\n @Override \\n protected void encode(ChannelHandlerContext ctx, IMessage msg, ByteBuf out) { \\n byte[] data = msg.toByteArray(); \\n byte[] compressed = compressWithGZIP(data); // 自定义压缩 \\n out.writeInt(compressed.length); \\n out.writeBytes(compressed); \\n } \\n} \\n
\\n1. 动态代理与协议封装
\\n// 客户端代理 \\npublic class RpcProxy implements InvocationHandler { \\n public Object invoke(Object proxy, Method method, Object[] args) { \\n RpcRequest request = buildRequest(method, args); \\n Channel channel = ConnectionPool.getChannel(); \\n channel.writeAndFlush(request).sync(); // 同步等待响应 \\n return parseResponse(channel); \\n } \\n} \\n
\\n2. 响应式编解码管道
\\n// 协议头定义:魔数(4) + 长度(4) + 序列化类型(1) \\npublic class RpcDecoder extends ByteToMessageDecoder { \\n protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { \\n if (in.readableBytes() < 9) return; \\n in.markReaderIndex(); \\n int magic = in.readInt(); \\n int length = in.readInt(); \\n byte serializationType = in.readByte(); \\n // ... 根据类型反序列化 \\n } \\n} \\n
\\n3. 熔断与降级
\\nReadTimeoutHandler
检测未响应请求。1. 异步请求处理
\\npublic class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> { \\n @Override \\n protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { \\n CompletableFuture.supplyAsync(() -> processRequest(request)) \\n .thenAccept(response -> writeResponse(ctx, response)); \\n } \\n} \\n
\\n2. 文件传输优化
\\n// 零拷贝发送文件 \\nFileRegion fileRegion = new DefaultFileRegion(file, 0, file.length()); \\nctx.write(fileRegion); \\n
\\n3. 链路监控
\\n1. MQTT 协议支持
\\n// 使用 Netty-MQTT 编解码库 \\nch.pipeline().addLast(MqttEncoder.INSTANCE); \\nch.pipeline().addLast(new MqttDecoder()); \\nch.pipeline().addLast(new MqttHandler()); \\n
\\n2. 连接保活与断线重连
\\n// 服务端心跳检测 \\nch.pipeline().addLast(new IdleStateHandler(0, 0, 60)); \\nch.pipeline().addLast(new DeviceHeartbeatHandler()); \\n\\n// 客户端自动重连 \\nBootstrap bootstrap = new Bootstrap(); \\nbootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) \\n .handler(new ReconnectHandler()); \\n
\\n3. 消息批量压缩上传
\\n// 累积10条数据后批量发送 \\npublic class BatchUploadHandler extends ChannelDuplexHandler { \\n private List<SensorData> buffer = new ArrayList<>(); \\n\\n @Override \\n public void channelRead(ChannelHandlerContext ctx, Object msg) { \\n buffer.add((SensorData) msg); \\n if (buffer.size() >= 10) { \\n ctx.writeAndFlush(new BatchData(buffer)); \\n buffer.clear(); \\n } \\n } \\n} \\n
","description":"场景一:游戏服务器 —— 万人同屏的底层引擎 核心痛点\\n高并发连接:万人同时在线,TCP 长连接管理。\\n低延迟通信:实时战斗、技能同步需毫秒级响应。\\n协议复杂性:自定义二进制协议,高效序列化与反序列化。\\nNetty 实战方案\\n\\n1. 自定义协议栈(避免粘包/拆包)\\n\\npublic class GameProtocolDecoder extends ByteToMessageDecoder { \\n @Override \\n protected void decode(ChannelHandlerContext ctx, ByteBuf in,…","guid":"https://juejin.cn/post/7476737955828826163","author":"码农liuxin","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T07:32:24.615Z","media":null,"categories":["后端","Java","Netty","网络协议"],"attachments":null,"extra":null,"language":null},{"title":"2025年 IDEA 插件推荐,告别低效!","url":"https://juejin.cn/post/7476755577192857639","content":"IDEA插件没有绝对的好坏,每个人的需求不一样,选择合适的插件,并定期清理和维护,才能提高效率,否则适得其反。
\\nRainbow Brackets 会将不同层级的括号用不同的颜色标记出来,很快就能分辨出括号的对应关系,避免括号匹配错误。\\n我更喜欢默认的括号高亮显示,它已经足够清晰了,不需要额外的颜色区分,反而眼花缭乱的颜色可能会造成视觉上的干扰,所以没有使用Rainbow Brackets。
\\nCodeGlance 在编辑器右侧生成一个代码的缩略图,可以快速定位到代码的任何位置,但是它占用了显示代码的部分屏幕空间,而且一般通过搜索来定位,所以没有使用CodeGlance。
\\nKey Promoter X 用于自动提示鼠标操作对应快捷键。\\n因为我已经学习了IDEA的使用,已经熟悉IDEA快捷键,因此我不需要依赖Key Promoter X的实时提示。
\\n下面开始讲解我使用的IDEA插件,我的 IDEA 版本是IntelliJ IDEA 2020.3.2 (Ultimate Edition)。
\\nLombok依赖库通过注解自动生成getter、setter等方法,减少代码量,不过因为IDEA无法识别Lombok注解,实例调用getter、setter方法是会有错误提示的,\\n而Lombok 插件的作用则是提示和校验实体类的getter、setter方法,避免错误提示。
\\nGenerate All Getter And Setter 有以下功能:
\\n1.使用 .allget 生成所有 getter 方法。
\\n2.使用 .allset 生成所有不带默认值的 setter 方法。
\\n比 GenerateAllSetter 好用
\\n要想在程序运行时重新加载修改的代码,而不需要重新启动整个应用程序,可以使用两种热部署工具:devtools和JRebel。
\\n要使用devtools,要在项目中添加依赖。在修改了某个Java文件后,不需要重启项目,只需要重新构建项目即可。
\\n虽然devtools非常方便,但它有一些限制。例如,它只能用于Spring Boot应用,而且无法在生产环境中使用。推荐使用 JRebel\\n使用JRebel有相关异常抛出可能是版本问题,IDEA版本需要兼容JRebel版本,如果不想升级IDEA,可以下载低版本的JRebel,\\n如果由于项目JDK版本问题,必须使用相应高版本的JRebel,那么就要升级IDEA了,或者,不升级IDEA,使用devtools。\\n比如IntelliJ IDEA 2020.3.2推荐使用JRebel 2021.1.2。\\nJRebel各版本下载地址
\\nSpring Boot 热部署:从devtools到JRebel的探索\\nJava热加载(JRebel)与Devtools热部署 - 思凡念真 - 博客园
\\n切换背景图片的插件有三款:
\\n\\nBackground Image Plus +继承了Background Image Plus的意志,Background Image Rotation继承了Background Image Plus +的意志,\\n因此推荐使用最新的Background Image Rotation。
\\nBackground Image Rotation每隔一段时间以随机顺序选择文件夹中的一张图片,每一轮都会选完文件夹的图片。\\nView | Random Order Reset
重新开始下一轮,随机选择第一张图片作为背景,View | Random Background Image
随机选择本轮中的下一张图片作为背景。
Grep Console 用于管理控制台输出语句。
\\nGrep Console支持为指定的输出语句配置指定的样式。比如可为启动成功语句配置为绿色。\\nscreenshot_17407.png (1048×390)
\\nGrep Console支持在原有控制台开一个控制台,可用于过滤原有控制台的输出语句,只关注需要的输出语句。\\nscreenshot_17407.png (1048×390)
\\nCamelCase 支持使用Shift + Alt + U
切换变量名的各种命名格式,\\n比如大驼峰命名,小驼峰命名,下划线命名之间互相切换。请在File | Settings | Camel Case
查看更多变量名的格式。
Save Actions X 可以在保存时优化包导入,自动为没有修改的变量添加final
修饰符,调用方法的时候自动添加this
关键字等。
Statistic 用于统计代码行数。
\\nFree MyBatis Tool\\n支持生成代码,但是不支持自定义模板,不够灵活。
\\nMyBatisX 的生成代码功能复制自Free MyBatis Tool并做了改进,\\n它使用的是FreeMarker模板引擎生成代码,支持自定义任意文件模板。更多功能请看Mybatis X文档 。
\\nEasyCode 用于生成代码,\\n它使用的是Velocity模板引擎生成代码,支持自定义任意文件模板,可以生成任何与数据库相关的代码,不局限于Mybatis相关代码。\\n支持多个表多个模板批量生成。
\\nMyBatisCodeHelperPro 为Mybatis映射文件提供了最好的代码提示功能,\\n还提供了文件跳转,代码提示,代码检查,代码生成等功能,部分功能需收费,代码生成不支持自定义模板。更多功能请看MyBatisCodeHelperPro文档 。
\\nEasyCode-MybatisCodeHelper 是由MyBatisCodeHelperPro作者开发的,\\n它复制并改进了EasyCode的模板代码生成功能,比如支持在scratch的目录来配置代码模板,添加模板在线导入导出功能,可惜是闭源的。具体配置请看通过模版生成代码文档
\\n注意,MyBatisX和MyBatisCodeHelperPro一起安装可能存在功能冲突,MyBatisX免费支持通过方法名生成sql功能,MyBatisCodeHelperPro是不支持的,\\n如果追求免费,那么推荐使用MyBatisX,如果想要完善的代码提示功能,那么推荐使用MyBatisCodeHelperPro。\\n关于代码生成功能,推荐使用EasyCode-MybatisCodeHelper,如果喜欢FreeMarker语法,那么推荐使用MyBatisX。
\\n如果使用JRebel进行热加载,修改 MyBatis 的 XML 映射文件后,更改不会立即生效。这是因为JRebel默认不支持这种类型的热加载。我们可以安装JRebel mybatisPlus extension 来使MyBatis映射文件的修改也能实时生效。
\\nIDE Eval Reset插件可以无限重置试用时间,从而实现永久使用,这是最简便的白嫖方法。\\n具体请看Jetbrains系列产品重置试用方法\\n注意,IDEA 2021.2.1是最后一个可以使用IDE Eval Reset插件的版本,因为后面的版本没有试用按钮了,无法点击试用了,也无法重置试用时间了。
\\nIdea本身具备静态代码分析功能,帮助静态分析代码中潜在的错误,而静态代码分析插件则增强了此部分功能。比如检测空指针异常、无限递归循环和无用变量等。
\\n推荐使用SonarQube for IDE 和 spotbugs-idea 。
\\nAlibaba Java Coding Guidelines 多年不更新,不推荐。
\\nGsonFormatPlus 支持将JSON转实体类。
\\nPOJO to JSON 支持将实体类转为JSON。
\\nEasy Javadoc 支持为代码生成生成中文注释。\\n具体使用请看Easy Javadoc文档 。
\\nMaven Helper 可以在pom文件中分析并显示出相关依赖关系,且对于冲突的依赖进行标红,极大方便了排除冲突依赖的工作。
\\nEasyYapi 支持将请求方法导出到YApi,Postman或者Markdown中,以便快速请求测试。
\\nFast Request 支持快速生成请求方法对应的请求来进行测试。\\n具体使用请看Fast Request文档 。
\\nCool Request 和拥有强大的请求调用能力,\\n直接检测SpringBoot配置,可直接调用请求方法,而且可通过反射绕过拦截器,调用接口无需在配置Token。\\n同事支持手动触发任意一个方法,调试代码方便至极。具体使用请看Cool Request文档 。
\\n总之,Fast Request插件和Cool Request插件提供了和请求相关的很多功能,都值得一试。
\\nRedis Helper 是免费的Redis客户端,支持修改键值。
\\nJMH 插件支持整合JMH快速进行基准测试。
\\nMarkdown Image Support 支持为md文件插入图片时自定义图片路径,支持上传图片到云服务。
\\nLeetCode Editor 支持生成LeetCode题目模板,快速刷题
","description":"前言 IDEA插件没有绝对的好坏,每个人的需求不一样,选择合适的插件,并定期清理和维护,才能提高效率,否则适得其反。\\n\\nRainbow Brackets 会将不同层级的括号用不同的颜色标记出来,很快就能分辨出括号的对应关系,避免括号匹配错误。 我更喜欢默认的括号高亮显示,它已经足够清晰了,不需要额外的颜色区分,反而眼花缭乱的颜色可能会造成视觉上的干扰,所以没有使用Rainbow Brackets。\\n\\nCodeGlance 在编辑器右侧生成一个代码的缩略图,可以快速定位到代码的任何位置,但是它占用了显示代码的部分屏幕空间,而且一般通过搜索来定位…","guid":"https://juejin.cn/post/7476755577192857639","author":"减瓦","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T06:34:50.247Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f6dd1e70faa54079913f6ea52ac52c5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YeP55Om:q75.awebp?rk3s=f64ab15b&x-expires=1741504218&x-signature=RCycRpkM%2BxsVmXMnc44NtyqIIfI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/787400bd75bc4213953f8bfc7474de2d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YeP55Om:q75.awebp?rk3s=f64ab15b&x-expires=1741504218&x-signature=HpeG1Waa%2F49U4v2MoT2DI4ioOwg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/69771b834d61495d9a81634e5d65b205~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YeP55Om:q75.awebp?rk3s=f64ab15b&x-expires=1741504218&x-signature=%2FQT%2BpgLPz1mZw9ZS9udBfw8RkhM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/83dd473055c34c9c8962e575bf132aab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YeP55Om:q75.awebp?rk3s=f64ab15b&x-expires=1741504218&x-signature=QIOuEJ%2BqYlHoswKcQ29fCL4axgE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"DeepSeek 精准使用提示词技巧和闭坑指南","url":"https://juejin.cn/post/7476497261574340623","content":"这篇文章主要介绍了 DeepSeek 的精准使用提示词技巧和闭坑指南。包括基本使用方法,如深度思考、联网搜索、上传附件等功能的应用场景。提示词方面,强调精准高效提问,如明确需求、不定义过程、明确受众风格等技巧,还提到了反馈与迭代优化、复杂问题分步拆解等。闭坑指南包括避免冗长提示词、复杂句式等。
\\n直接打开DeepSeek官网,页面大致如下,DeepSeek能暴打ChatGPT关键因素是深度思考、联网搜索两个功能,如果你需要简单快速的回答的时候就不需要点击深度思考了,指定默认DeepSeek-V3
模型就可以解决了。
**深度思考R1:**当你需要完成更复杂的任务时,希望AI深思时,涉及到编程、数学、计算、产品策划方案等等,我们就可以打开深度思考了。
\\n**联网搜索:**如果是涉及2023年12月之后的信息,需要打开联网搜索的条件,如果搜搜的内容和时间没有关系,关闭联网搜索效果会更好。
\\n**上传附件:**上传附件可以把pdf、或者excel进行上传,进行更具时效性的任务,进行提问,也可以上传一本书让DeepSeek
概括一些中心思想。
DeepSeek
之所以称之为AI核武器,与其他大模型最根本的不同,DeepSeek
属于推理模型,是操作手册指令型到战略伙伴的范式革新,核心就是精准、高效的提问。
**提示词的本质就是表达Prompt,**两个关键问题:首先,我是否真正理清了脑海里的想法,其次,我是否能够通过文字准确传达这个想法。
\\n我们需要达成几点共识:
\\nDeepSeek-R1
的提示词技巧,就是没有技巧,不需要角色设定、不需要思维提示、不需要结构化提示词,不需要给提示、不需要给实例...干什么?\\n给谁干?\\n目的是?(要什么)\\n约束是?(不要什么)\\n
\\n举例:
\\n我要写一个如何理解爱因斯坦的相对论的科普文章,给中小学生看,希望能通俗易懂、内容充实、幽默,且觉得非常实用,不要太AI或枯燥。
\\n推荐:我是一名小学生,请...\\n不推荐:我是一名小学生,没有学习过物理、化学、听不懂高深的专业词汇 请...
这里有几种典型方式:
\\n1.举例法:\\n最常见的是通过举例来实现,当我们展示一个具体例子时,实际上是在让AI感知这个例子中的模式(pattern),并期待它能够通过自身的泛化能力来理解和应用这个模式。
\\n2.定义字典:\\n在特定场景中,比如需要使用15个独有术语时(比如一些“业内黑话”),我们可以专门设置一个定义模块,将这个\\"定义字典\\"输入给AI,这也是在输入模式。
\\n3.RAG(检索增强生成)技术:
\\n当我们面对AI未知的数据时,我们使用先检索(本地+联网查资料)→再生成(写答案)的方式,本质上也是在输入模式。
\\n举例1:2025年当前,最新的法定年假政策是什么?(联网检索)\\n举例2:公司今年的年假政策是什么?(提供本地文件)
\\n在这个领域,提示词的核心技巧就在于如何提出好问题。
\\n\\"提问\\"本身完全可以作为一门独立的学科来研究,提升提问能力正是我们需要努力的方向。提问能力,也将成为一项核心竞争力。
\\n这个象限属于:探索人类数学边界的数学家、正在挑战物理学极限的科学家。
\\n可以,要尽量多提供些描述。
\\n在和R1交互时,把自己想象成具备管理经验的领导,而R1是聪明的下属,原则:给模型目标,而不是任务。
\\n万能提示词模板: 你是谁+(背景信息)+你的目标
\\n你是谁:非常的有用\\n背景信息:告诉他你为什么做这件事,你面临的现实背景是什么或问题是什么,你的目标:说清楚它帮你做什么,做到什么程度
\\n核心:用人话清晰的表达出你的需求。
\\n特别建议大家提供清楚你的目标,让R1具备一定的思考空间去帮助你执行得更好,而非提供一个机械化执行指令。
\\n你应该像产品经理提需求般描述要什么,而不是像程序员写代码般规定怎么做。
\\n举例1:
\\n错误提问:\\n请按以下步骤解答:\\n1.列出方程的所有可能形式式\\n2.代入数值验证参数\\n3.检查是否存在整数解的条件\\n问题:方程3x+5=20的解是多少?
\\n正确提问:方程3x+5=20的解是什么?请通过数学逻辑推导给出过程。
\\n举例2:\\n错误提问:\\n问题:A说B在说谎,B说c在说谎,c说A和B都在说谎,谁在说真话?要求:\\n步骤1:列出所有人物关系\\n步骤2:排除矛盾选项\\n步骤3:验证时间线
\\n正确提问:A、B、C三人中,A指控B说谎,B指控c说谎,c则指控A和B都在说谎。根据逻辑矛盾如何推断谁在说真话?
\\nR1输出的内容看不懂怎么办?本质上就是对方不知道你是谁!
\\n使用提示词:
\\n关于风格明确,模版如下:
\\n模板:用xxx的风格,写一篇主题xxx的文章,要求xxx。
\\n举例1:\\n玄武门之变结束的当天,李世民在深夜写下一段独白,请你用李世民的语气,写出他可能写的内容。
\\n举例2:\\n模仿董宇辉的风格,写100字杭州文旅文案。
\\n举例3:\\n模仿朱自清《春》的文风,写一篇春天的散文。
\\n举例4:\\n有人说你是chatGPT套壳,用键盘侠的风格怼回去,要求骂人不吐脏字
\\n举例5:\\n为我写一首类似于阿房宫赋的文言文,\\n描述中国近代史,融入哲学思考。
\\n众人皆知推理模型好,但是推理模型,几乎都不联网,Deepseek-R1
是为数不多的,可以联网的推理大模型。
上传PDF/PPT作为知识基底。(最多不超过50个,每个不超过100MB)\\n推理+上传附件,可以做更多本地化、私密化的东西,比如你自己的知识库或者内部资料。让其基于自有知识库进行推理和思考。
\\n举例1:
\\n根据上传的图书,分析这本书作者想表达的主要观点,以及作为企业经营者主要关注的问题是啥。
\\n举例2:
\\n基于这份2024Q3财报,分析新能源电池业务的毛利率变化。
\\n举例3:\\n基于我提供的奥运会数据,请分析2024年巴黎奥运会中国代表团不同运动项目的金牌贡献率。
\\n上下文记忆:Deepseek R1目前提供的上下文只有64k token长度(官方API文档的说明实际聊天对话的长度待确认),对应到中文字符大概是3-4万字。适用于文档分析、长对话等场景。
\\n三点注意:
\\n注意1:上下文记忆有限
\\n上下文理解能力是有限的。随着会话时间的延长,模型处理过去信息的能力会受到限制,从而导致遗忘,之前最初聊天的内容。
\\n当你发送的文档长度超过3万字时,你可以理解为他是通过RAG,也就是检索增强的方式去选取你文档中的部分内容作为记忆的一部分来展开与你的对话的,而不是全部内容。
\\n注意2:输出长度有限
\\n多数大模型会将输出长度控制在4k或者8k,也就是单次对话最多给你2-4千中文字符\\n所以,你没法复制一篇万字长文让Deepseek一次性完成翻译,也不能让Deepseek一次性帮你写一篇5000字以上的文章,这些都是模型输出长度限制导致,你需要理解这个问题的存在。
\\n注意3:如何清除之前的记忆
\\n因为模型会记住或跟踪你之前写的所有聊天记录。如果之前你的角色设定是体育老师,下面又问数学问题,那就会出现“你的数学是体育老师教的”问题。
\\n解决办法:\\n① 开启新的对话\\n② 输入:回复此条对话前,请忽略前面所有的对话
\\n情况1:对初始的回答进一步追问、优化。
\\n举例1:\\n用鲁迅的文风写一篇2000字以内的公众号文章,分析一下2025春节档的几部爆火的电影。
\\n举例2:简化内容
\\n巴“上一个回答中的技术解释过于复杂,请用小学生能听懂的语言重新描述\'云计算”概念,并举例说明。
\\n举例3:补充细节\\n巴“关于时间管理四象限法则’,请补充一个职场人士的每日任务分配案例(每个象限至少2个任务)。
\\n举例4:修正错误
\\n巴\\"你提到“光合作用的暗反应需要光照’,这与教材矛盾,请核实并重新解释光反应与暗反应的区别。
\\n举例5:调整风格
\\n国\\"将上述法律条款解读改写成幽默风格的科普短文,适合社交媒体传播,保留核心信息。
\\n举例6:扩展范围
\\n\\"你推荐的书籍均为英文原著,请再推荐3本中文科幻小说,要求近5年出版目豆瓣评分8.0以上。
\\n情况2:针对某一个问题,挑毛病或辩证思考,评估方案和决策
\\n提示词:
\\nDeepseek 会恢复到深度思考的状态,提供更有价值的回答。
\\n举例1:
\\n我是个脱离职场5年的宝妈,宝宝现在3岁,在上幼儿园,帮我想想有哪些副业可以赚钱,对你的回答复盘5次,论证可行性。
\\n举例2:\\n模仿李白的风格,写一首七言律诗,描述中国近代史,反复斟酌,注意是否满足七律对于韵律的要求。
\\n背景/附加条件:\\n模仿李白的风格,描述中国近代史,反复斟酌
\\n目标:\\n写一首满足韵律要求的七言律诗
\\n举例1:项目管理
\\n举例2:法律咨询
\\n超过200字的需求描述可能导致焦点偏移,过度思考,甚至逻辑凌乱\\n推理模型时代,只需要命中那个关键词即可。其余的,交给模型自由发挥。
\\n举例1:\\n错误提问:能不能帮我写点关于现在智能手机的东西,说说它的好用地方,特别是拍照和电池正确提问口:请写一段关于智能手机的介绍,突出拍照和续航
\\n举例2:\\n错误提问:列几个电动车牌子(未说明格式和数量)正确提问口:生成5个新能源汽车品牌名称,用Markdown列表展示
\\nR1模型通过强化学习,自动生成完整思维链。
\\n错误提问
\\n1.列出所有可能原因\\n2.评估证据支持度\\n3.选择最优解释
\\n问题:某电商用户下单后频繁取消订单的根本原因可能是什么?
\\nR1本身就是专家模型&专家思维,除非你是需要R1从特定学科视角为你提供解答,在那种情况下,你只需要去提示学科即可,不需要提专家了。
\\n错误举例:
\\n\\"假设你是乔布斯…\\",\\"假设你是数据库专家....\\",\\n\\"假设你是一名医生...\\"\\"
","description":"概述 这篇文章主要介绍了 DeepSeek 的精准使用提示词技巧和闭坑指南。包括基本使用方法,如深度思考、联网搜索、上传附件等功能的应用场景。提示词方面,强调精准高效提问,如明确需求、不定义过程、明确受众风格等技巧,还提到了反馈与迭代优化、复杂问题分步拆解等。闭坑指南包括避免冗长提示词、复杂句式等。\\n\\n基本使用\\n\\n直接打开DeepSeek官网,页面大致如下,DeepSeek能暴打ChatGPT关键因素是深度思考、联网搜索两个功能,如果你需要简单快速的回答的时候就不需要点击深度思考了,指定默认DeepSeek-V3模型就可以解决了。\\n\\n**深度思考R1…","guid":"https://juejin.cn/post/7476497261574340623","author":"stark张宇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T01:50:57.056Z","media":null,"categories":["后端","DeepSeek","人工智能"],"attachments":null,"extra":null,"language":null},{"title":"双Token机制(Access Token + Refresh Token)安全高效","url":"https://juejin.cn/post/7476388533738504192","content":"Access Token
\\nexp
声明)Refresh Token
\\n// AuthController.java\\n@PostMapping(\\"/login\\")\\npublic R<LoginResult> login(@RequestBody LoginRequest request) {\\n // 1. 验证用户密码\\n LoginUser user = remoteUserService.authenticate(request);\\n \\n // 2. 生成双Token\\n String accessToken = JwtUtils.generateAccessToken(user);\\n String refreshToken = UUID.randomUUID().toString();\\n \\n // 3. 存储Refresh Token到Redis(绑定设备和用户)\\n String deviceFingerprint = buildDeviceFingerprint(request);\\n String redisKey = buildRefreshTokenKey(user.getUserId(), deviceFingerprint);\\n redisService.setEx(redisKey, refreshToken, 7, TimeUnit.DAYS);\\n \\n // 4. 设置Refresh Token到Cookie\\n ResponseCookie cookie = ResponseCookie.from(\\"refresh_token\\", refreshToken)\\n .httpOnly(true)\\n .secure(true)\\n .path(\\"/\\")\\n .maxAge(7 * 24 * 3600)\\n .sameSite(\\"Strict\\")\\n .build();\\n \\n return R.ok(new LoginResult(accessToken))\\n .addHeader(HttpHeaders.SET_COOKIE, cookie.toString());\\n}\\n
\\n// AuthController.java\\n@PostMapping(\\"/auth/refresh\\")\\npublic R<LoginResult> refreshToken(\\n @CookieValue(name = \\"refresh_token\\", required = false) String refreshToken,\\n HttpServletRequest request) {\\n \\n // 1. 验证Refresh Token存在性\\n if (StringUtils.isEmpty(refreshToken)) {\\n return R.fail(HttpStatus.UNAUTHORIZED, \\"缺少刷新令牌\\");\\n }\\n \\n // 2. 提取设备指纹\\n String deviceFingerprint = buildDeviceFingerprint(request);\\n \\n // 3. 查询Redis验证有效性\\n String redisKey = buildRefreshTokenKeyFromRequest(request); // 根据请求生成Key\\n String storedToken = redisService.get(redisKey);\\n if (!refreshToken.equals(storedToken)) {\\n return R.fail(HttpStatus.UNAUTHORIZED, \\"刷新令牌无效\\");\\n }\\n \\n // 4. 生成新Access Token\\n LoginUser user = getCurrentUser(); // 从上下文获取用户\\n String newAccessToken = JwtUtils.generateAccessToken(user);\\n \\n // 5. 可选:刷新Refresh Token有效期(滑动过期)\\n redisService.expire(redisKey, 7, TimeUnit.DAYS);\\n \\n return R.ok(new LoginResult(newAccessToken));\\n}\\n
\\nprivate String buildDeviceFingerprint(HttpServletRequest request) {\\n String ip = ServletUtils.getClientIP(request);\\n String userAgent = request.getHeader(\\"User-Agent\\");\\n return Hashing.sha256().hashString(ip + userAgent, StandardCharsets.UTF_8).toString();\\n}\\n
\\n// AuthFilter.java\\npublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {\\n ServerHttpRequest request = exchange.getRequest();\\n \\n // 1. 白名单直接放行\\n if (isIgnorePath(request.getPath().toString())) {\\n return chain.filter(exchange);\\n }\\n \\n // 2. 尝试获取Access Token\\n String accessToken = getAccessToken(request);\\n \\n try {\\n // 3. 验证Access Token有效性\\n Claims claims = JwtUtils.parseToken(accessToken);\\n if (claims != null && isTokenValid(claims)) {\\n // 正常流程\\n return chain.filter(addHeaders(exchange, claims));\\n }\\n } catch (ExpiredJwtException ex) {\\n // 4. Access Token过期,尝试刷新\\n return handleTokenRefresh(exchange, chain, ex.getClaims());\\n }\\n \\n // 5. 无有效令牌\\n return unauthorizedResponse(exchange, \\"请重新登录\\");\\n}\\n\\nprivate Mono<Void> handleTokenRefresh(ServerWebExchange exchange, \\n GatewayFilterChain chain,\\n Claims expiredClaims) {\\n // 1. 获取Refresh Token\\n String refreshToken = getRefreshTokenFromCookie(exchange);\\n \\n // 2. 调用刷新接口(内部转发)\\n return WebClient.create()\\n .post()\\n .uri(\\"http://auth-service/auth/refresh\\")\\n .cookie(\\"refresh_token\\", refreshToken)\\n .retrieve()\\n .bodyToMono(R.class)\\n .flatMap(result -> {\\n if (result.getCode() == HttpStatus.SUCCESS) {\\n // 3. 更新请求头中的Access Token\\n String newToken = result.getData().get(\\"accessToken\\");\\n ServerHttpRequest newRequest = exchange.getRequest().mutate()\\n .header(\\"Authorization\\", \\"Bearer \\" + newToken)\\n .build();\\n return chain.filter(exchange.mutate().request(newRequest).build());\\n } else {\\n return unauthorizedResponse(exchange, \\"会话已过期\\");\\n }\\n });\\n}\\n
\\n// JWT生成时加入设备指纹\\npublic static String generateAccessToken(LoginUser user, HttpServletRequest request) {\\n String fingerprint = buildDeviceFingerprint(request);\\n return Jwts.builder()\\n .setSubject(user.getUsername())\\n .claim(\\"user_id\\", user.getUserId())\\n .claim(\\"fp\\", fingerprint)\\n .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))\\n .signWith(SECRET_KEY)\\n .compact();\\n}\\n\\n// 网关验证时检查设备\\nprivate boolean validateDeviceFingerprint(Claims claims, HttpServletRequest request) {\\n String currentFp = buildDeviceFingerprint(request);\\n String tokenFp = claims.get(\\"fp\\", String.class);\\n return currentFp.equals(tokenFp);\\n}\\n
\\n// 注销接口\\n@PostMapping(\\"/logout\\")\\npublic R<Void> logout(HttpServletRequest request) {\\n // 1. 获取当前设备指纹\\n String fingerprint = buildDeviceFingerprint(request);\\n \\n // 2. 删除Redis中的Refresh Token\\n String redisKey = buildRefreshTokenKey(getCurrentUserId(), fingerprint);\\n redisService.delete(redisKey);\\n \\n // 3. 将Access Token加入黑名单(剩余有效期内拒绝)\\n String accessToken = getAccessToken(request);\\n redisService.setEx(\\"token_blacklist:\\" + accessToken, \\"1\\", \\n JwtUtils.getRemainingTime(accessToken), TimeUnit.SECONDS);\\n \\n // 4. 清除客户端Cookie\\n ResponseCookie cookie = ResponseCookie.from(\\"refresh_token\\", \\"\\")\\n .maxAge(0)\\n .build();\\n \\n return R.ok().addHeader(HttpHeaders.SET_COOKIE, cookie.toString());\\n}\\n
\\n// axios拦截器\\naxios.interceptors.response.use(response => {\\n return response;\\n}, error => {\\n const originalRequest = error.config;\\n \\n if (error.response?.status === 401 && !originalRequest._retry) {\\n originalRequest._retry = true;\\n \\n // 调用刷新接口\\n return axios.post(\'/auth/refresh\', {}, { withCredentials: true })\\n .then(res => {\\n const newToken = res.data.accessToken;\\n localStorage.setItem(\'access_token\', newToken);\\n originalRequest.headers[\'Authorization\'] = `Bearer ${newToken}`;\\n return axios(originalRequest);\\n });\\n }\\n return Promise.reject(error);\\n});\\n
\\n// 定时检查Token有效期\\nsetInterval(() => {\\n const token = localStorage.getItem(\'access_token\');\\n if (token && isTokenExpiringSoon(token)) { // 剩余<5分钟\\n axios.post(\'/auth/refresh\', {}, { withCredentials: true })\\n .then(res => {\\n localStorage.setItem(\'access_token\', res.data.accessToken);\\n });\\n }\\n}, 300000); // 每5分钟检查\\n
\\n指标名称 | 监控方式 | 报警阈值 |
---|---|---|
刷新令牌失败率 | Prometheus计数器 | >5% (持续5分钟) |
并发刷新冲突次数 | Redis分布式锁统计 | >10次/秒 |
黑名单令牌数量 | Redis键空间统计 | 突增50%时告警 |
# 成功刷新日志\\n[INFO] 用户[1001]通过设备[192.168.1.1|Chrome]刷新令牌,新有效期至2023-10-01 12:30\\n\\n# 异常事件日志\\n[WARN] 检测到异常刷新请求,用户[1001]的设备[192.168.1.2|Firefox]与记录不匹配\\n
\\nPhase 1:
\\nPhase 2:
\\nPhase 3:
\\n紧急开关:
\\n@Value(\\"${security.token.mode:SINGLE}\\")\\nprivate String tokenMode;\\n\\npublic Mono<Void> filter(...) {\\n if (\\"SINGLE\\".equals(tokenMode)) {\\n // 回退到旧逻辑\\n }\\n}\\n
\\n数据兼容:
\\n安全性提升:
\\n用户体验优化:
\\n系统扩展性:
\\n合规性保障:
\\n欢迎关注公众号:月伴飞鱼,每天分享程序员职场经验!
\\n文章内容收录到个人网站,方便阅读:hardyfish.top/
\\n资料分享
\\n\\n\\n技术之瞳 阿里巴巴技术笔试心得::pan.quark.cn/s/1e9825fc2…
\\n
Spring AI是一个专为AI工程设计的Java应用框架。
\\n\\n\\nSpringAI是Spring框架的一个扩展,用于方便开发者集成AI调用AI接口。
\\n\\n
Spring AI有以下特点:
\\n\\n\\n在AI的聊天、文生图、嵌入模型等方面提供API级别的支持。
\\n与模型之间支持同步式和流式交互。
\\n多种模型支持。
\\n
基本使用
\\n\\n\\n使用:start.spring.io/,构建一个
\\nSpring Boot
项目。点击
\\nADD DEPENDENCIES
,搜索Ollama
添加依赖。
打开生成的项目,查看pom.xml
,可以看到核心依赖:
<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>\\n</dependency>\\n
\\n安装 Ollama:
\\ncurl -fsSL https://ollama.com/install.sh | sh\\n
\\n\\n运行 deepseek-r1:
\\nollama run deepseek-r1:671b\\n
\\n配置Ollama的相关信息:
\\nspring.ai.ollama.base-url=http://localhost:11434\\nspring.ai.ollama.chat.model=deepseek-r1:1.5b\\n
\\n\\n\\n\\n
spring.ai.ollama.base-url
: Ollama的API服务地址。\\n
spring.ai.ollama.chat.model
: 要调用的模型名称。
调用Ollama中的deepseek-r1模型:
\\npublic class TestOllama {\\n\\n @Autowired\\n private OllamaChatModel ollamaChatModel;\\n\\n @Test\\n public void testChatModel() {\\n String prompt = \\"英文就翻译成中文\\";\\n String message = \\"test\\";\\n String result = ollamaChatModel.call(prompt + \\":\\" + message);\\n System.out.println(result);\\n }\\n}\\n
\\n集成 DeepSeek 大模型
\\n\\n\\nSpring AI 集成 DeepSeek的代码示例:github.com/Fj-ivy/spri…
\\nDeepSeek 官方文档:api-docs.deepseek.com/zh-cn/
\\n
引入依赖:
\\n<dependency>\\n <groupId>org.springframework.ai</groupId>\\n <artifactId>spring-ai-openai-spring-boot-starter</artifactId>\\n</dependency>\\n
\\n配置:
\\nspring:\\n ai:\\n openai:\\n api-key: sk-xxx // 填写自己申请的key\\n base-url: https://api.deepseek.com\\n chat:\\n options:\\n model: deepseek-chat\\n
\\n简单示例:
\\n@RestController\\npublic class ChatController {\\n\\n private final OpenAiChatModel chatModel;\\n\\n @Autowired\\n public ChatController(OpenAiChatModel chatModel) {\\n this.chatModel = chatModel;\\n }\\n\\n /**\\n * 让用户输入一个prompt,然后返回一个结果\\n */\\n @GetMapping(\\"/ai/generate\\")\\n public Map<String,String> generate(@RequestParam(value = \\"message\\") String message) {\\n return Map.of(\\"generation\\", this.chatModel.call(message));\\n }\\n}\\n
","description":"欢迎关注公众号:月伴飞鱼,每天分享程序员职场经验! 文章内容收录到个人网站,方便阅读:hardyfish.top/\\n\\n资料分享\\n\\n技术之瞳 阿里巴巴技术笔试心得::pan.quark.cn/s/1e9825fc2…\\n\\nSpring AI是一个专为AI工程设计的Java应用框架。\\n\\nSpringAI是Spring框架的一个扩展,用于方便开发者集成AI调用AI接口。\\n\\n官网:spring.io/projects/sp…\\n\\nSpring AI有以下特点:\\n\\n在AI的聊天、文生图、嵌入模型等方面提供API级别的支持。\\n\\n与模型之间支持同步式和流式交互。\\n\\n多种模型支持。…","guid":"https://juejin.cn/post/7476406317759512591","author":"程序员飞鱼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-01T05:07:25.620Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"深入理解 JavaScript 中的 this 绑定及模拟 call、apply、bind 方法","url":"https://juejin.cn/post/7476389305881346086","content":"在 JavaScript 中,this
关键字是一个非常重要的概念,它决定了函数执行时的上下文。理解 this
的绑定机制对于编写高效且可靠的代码至关重要。本文将详细探讨 this
的四种主要绑定方式:默认绑定、隐式绑定、隐式丢失和显式绑定,并介绍如何实现与 call
、apply
和 bind
方法相同的功能。
当函数被直接调用(而不是作为对象的方法或通过其他方式调用)时,this
默认绑定到全局对象。在浏览器环境中,全局对象是 window
;在 Node.js 环境中,它是 global
或 globalThis
。
function foo() {\\n console.log(this);\\n}\\n\\nfoo(); // 输出: Window {...} (在浏览器中)\\n
\\nthis
的默认值是 undefined
而不是全局对象。当函数作为某个对象的方法调用时,this
绑定到该对象。这种绑定方式称为隐式绑定。
const obj = {\\n name: \'张三\',\\n greet: function(greeting) {\\n console.log(`${greeting}, ${this.name}`);\\n }\\n};\\n\\nobj.greet(\'Hello\'); // 输出: Hello, 张三\\n
\\nthis
,可能导致隐式丢失。当函数从其原始上下文中提取出来并独立调用时,this
的绑定会丢失,通常会回退到默认绑定(即全局对象或 undefined
)。
const obj = {\\n name: \'张三\',\\n greet: function(greeting) {\\n console.log(`${greeting}, ${this.name}`);\\n }\\n};\\n\\nconst greet = obj.greet;\\ngreet(\'Hello\'); // 输出: Hello, undefined (在严格模式下) 或 Hello, [Window] (在非严格模式下)\\n
\\n使用显式绑定(如 call
、apply
或 bind
)来确保 this
正确绑定。
显式绑定允许我们手动控制 this
的值。JavaScript 提供了三种方法来实现显式绑定:call
、apply
和 bind
。
call
方法call
方法允许我们在调用函数时指定 this
的值,并传递参数列表。
function greet(greeting) {\\n console.log(`${greeting}, ${this.name}`);\\n}\\n\\nconst person = { name: \'张三\' };\\n\\ngreet.call(person, \'Hello\'); // 输出: Hello, 张三\\n
\\nFunction.prototype.myCall = function(context, ...args) {\\n if (typeof this !== \'function\') {\\n throw new TypeError(\'Error: Not a function\');\\n }\\n\\n context = context || (typeof globalThis !== \'undefined\' ? globalThis : window);\\n const key = Symbol(\'fn\');\\n context[key] = this;\\n const res = context[key](...args);\\n delete context[key];\\n return res;\\n};\\n
\\napply
方法apply
方法类似于 call
,但接受一个参数数组而不是参数列表。
function greet(greeting) {\\n console.log(`${greeting}, ${this.name}`);\\n}\\n\\nconst person = { name: \'张三\' };\\n\\ngreet.apply(person, [\'Hello\']); // 输出: Hello, 张三\\n
\\nFunction.prototype.myApply = function(context, args) {\\n if (typeof this !== \'function\') {\\n throw new TypeError(\'Error: Not a function\');\\n }\\n\\n context = context || (typeof globalThis !== \'undefined\' ? globalThis : window);\\n const key = Symbol(\'fn\');\\n context[key] = this;\\n const res = context[key](...args);\\n delete context[key];\\n return res;\\n};\\n
\\nbind
方法bind
方法返回一个新的函数,该函数在调用时始终将 this
绑定到指定的对象,并可以预先填充部分参数。
function greet(greeting, punctuation) {\\n console.log(`${greeting}, ${this.name}${punctuation}`);\\n}\\n\\nconst person = { name: \'张三\' };\\n\\nconst boundGreet = greet.bind(person, \'Hello\');\\nboundGreet(\'!\'); // 输出: Hello, 张三!\\n
\\nFunction.prototype.myBind = function(context, ...outerArgs) {\\n if (typeof this !== \'function\') {\\n throw new TypeError(\'Error: Not a function\');\\n }\\n\\n const fn = this;\\n\\n function bound(...innerArgs) {\\n const isNew = this instanceof bound;\\n const contextToUse = isNew ? this : context;\\n\\n const allArgs = [...outerArgs, ...innerArgs];\\n return fn.apply(contextToUse, allArgs);\\n }\\n\\n Object.setPrototypeOf(bound, Object.getPrototypeOf(fn));\\n return bound;\\n};\\n
\\n理解 this
的绑定机制对于编写高效且可靠的 JavaScript 代码至关重要。以下是四种主要的绑定方式及其特点:
undefined
在严格模式下)。this
绑定到该对象。this
绑定丢失,通常回退到默认绑定。call
、apply
和 bind
方法手动控制 this
的值。此外,我们还展示了如何自定义实现 call
、apply
和 bind
方法,以便更好地理解和应用这些功能。希望这篇文章能帮助你深入理解 this
的绑定机制,并在实际开发中灵活运用。
今天咱们来聊聊一个很常见的开发场景:字符串拼接
。在日常开发中,字符串拼接几乎是每个 Java 开发者都会用到的操作。但最近,有朋友在面试时被问到一个问题:“为什么 IDEA 建议用‘+’拼接字符串,而不是用 StringBuilder?”这问题听起来是不是有点反直觉?毕竟,在大家的普遍认知中,用 StringBuilder 拼接字符串效率更高。
先来说说“+”拼接字符串。在 Java 中,“+” 是一个非常直观的字符串拼接操作符。比如,\\"Hello\\" + \\" \\" + \\"World\\"
,结果就是 \\"Hello World\\"
。简单、直接、易读,这是它的优点。
但长期以来,我们一直被告知:“+”拼接字符串效率很低,尤其是在循环中。因为每次拼接都会创建一个新的字符串对象,导致大量的临时对象产生,增加了垃圾回收的负担。所以,很多开发者会习惯性地使用 StringBuilder 来代替“+”,尤其是在处理复杂的字符串拼接时。
\\n然而,从 JDK 5 开始,Java 编译器做了一个优化——当你使用“+”拼接字符串时,编译器会自动将其优化为使用 StringBuilder 的方式。也就是说,\\"a\\" + \\"b\\"
在编译后,实际上会被编译器转换为 new StringBuilder().append(\\"a\\").append(\\"b\\").toString()
。这样一来,“+”拼接字符串的性能问题就得到了解决。
为了验证这一点,我们来做一个简单的实验。写一个测试类,分别用“+”和 StringBuilder 拼接字符串,然后比较它们的性能。
\\npublic String concatenationStringByPlus(String prefix, int i) {\\n return prefix + \\"-\\" + i;\\n}\\n\\npublic String concatenationStringByStringBuilder(String prefix, int i) {\\n return new StringBuilder().append(prefix).append(\\"-\\").append(i).toString();\\n}\\n
\\n然后,我们用 JUnit 测试用例分别调用这两种方法,拼接 100000 次,看看耗时情况:
\\n@Test\\npublic void testStringConcatenationByPlus() {\\n long startTime = System.currentTimeMillis();\\n for (int i = 0; i < 100000; i++) {\\n concatenationStringByPlus(\\"testByPlus:\\", i);\\n }\\n long endTime = System.currentTimeMillis();\\n System.out.println(\\"使用 \'+\' 拼接 100000 次,耗时:\\" + (endTime - startTime) + \\" 毫秒\\");\\n}\\n\\n@Test\\npublic void testStringConcatenationByStringBuilder() {\\n long startTime = System.currentTimeMillis();\\n for (int i = 0; i < 100000; i++) {\\n concatenationStringByStringBuilder(\\"testByStringBuilder:\\", i);\\n }\\n long endTime = System.currentTimeMillis();\\n System.out.println(\\"使用 StringBuilder 拼接 100000 次,耗时:\\" + (endTime - startTime) + \\" 毫秒\\");\\n}\\n
\\n运行结果:
\\n使用 \'+\' 拼接 100000 次,耗时:33 毫秒\\n使用 StringBuilder 拼接 100000 次,耗时:36 毫秒\\n
\\n可以看到,两者的耗时几乎一致。这说明在普通拼接场景下,“+” 和 StringBuilder 的性能几乎没有区别。而且,“+” 的代码更简洁、更易读
。
那么,是不是在所有场景下,“+” 都和 StringBuilder 一样高效呢?答案是否定的。当涉及到循环拼接时,“+” 的效率问题就暴露出来了。
\\n我们再做一个实验,模拟循环拼接一个长字符串。这次,我们分别用“+”和 StringBuilder 来拼接 10000 次:
\\n@Test\\npublic void testLoopConcatenationByPlus() {\\n long startTime = System.currentTimeMillis();\\n String str = \\"Initial String\\";\\n for (int i = 0; i < 10000; i++) {\\n str = str + \\"-\\" + i;\\n }\\n long endTime = System.currentTimeMillis();\\n System.out.println(\\"使用 \'+\' 循环拼接 10000 次,耗时:\\" + (endTime - startTime) + \\" 毫秒\\");\\n}\\n\\n@Test\\npublic void testLoopConcatenationByStringBuilder() {\\n long startTime = System.currentTimeMillis();\\n StringBuilder sb = new StringBuilder(\\"Initial String\\");\\n for (int i = 0; i < 10000; i++) {\\n sb.append(\\"-\\").append(i);\\n }\\n long endTime = System.currentTimeMillis();\\n System.out.println(\\"使用 StringBuilder 循环拼接 10000 次,耗时:\\" + (endTime - startTime) + \\" 毫秒\\");\\n}\\n
\\n运行结果:
\\n使用 \'+\' 循环拼接 10000 次,耗时:463 毫秒\\n使用 StringBuilder 循环拼接 10000 次,耗时:13 毫秒\\n
\\n可以看到,循环拼接时,“+” 的效率远远低于 StringBuilder。这是因为每次循环时,“+” 都会创建一个新的 StringBuilder 对象,而 StringBuilder 只需要在同一个对象上追加内容,效率自然更高
。
既然在循环拼接中 StringBuilder 更高效,为什么 IDEA 还要建议用“+”呢?原因在于编译器的优化和代码的可读性
。
对于简单的字符串拼接,编译器会自动将“+”优化为 StringBuilder 的形式。在这种情况下,使用“+”不仅代码更简洁,而且性能也一样好。而 StringBuilder 的代码相对冗长,可读性稍差
。
但如果是在循环中拼接字符串,IDEA 通常会提示你使用 StringBuilder,因为它能明显提升性能
。
通过以上实验和分析,我们可以得出以下结论:
\\n围观朋友⭕:dabinjava
","description":"今天咱们来聊聊一个很常见的开发场景:字符串拼接。在日常开发中,字符串拼接几乎是每个 Java 开发者都会用到的操作。但最近,有朋友在面试时被问到一个问题:“为什么 IDEA 建议用‘+’拼接字符串,而不是用 StringBuilder?”这问题听起来是不是有点反直觉?毕竟,在大家的普遍认知中,用 StringBuilder 拼接字符串效率更高。 一、“+” 拼接字符串\\n\\n先来说说“+”拼接字符串。在 Java 中,“+” 是一个非常直观的字符串拼接操作符。比如,\\"Hello\\" + \\" \\" + \\"World\\",结果就是 \\"Hello World\\"。简单…","guid":"https://juejin.cn/post/7476400083410108416","author":"大彬聊编程","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-01T04:34:07.824Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"Spring 6.0 + Boot 3.0:秒级启动、万级并发的开发新姿势","url":"https://juejin.cn/post/7476389305881296934","content":"// 示例:虚拟线程使用\\nThread.ofVirtual().name(\\"my-virtual-thread\\").start(() -> {\\n // 业务逻辑\\n});\\n
\\n// 传统线程池 vs 虚拟线程\\n// 旧方案(平台线程)\\nExecutorService executor = Executors.newFixedThreadPool(200);\\n// 新方案(虚拟线程)\\nExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();\\n// 处理10000个并发请求\\nIntStream.range(0, 10000).forEach(i -> \\n virtualExecutor.submit(() -> {\\n // 处理订单逻辑\\n processOrder(i);\\n })\\n);\\n
\\n@HttpExchange(url = \\"/api/users\\")\\npublic interface UserClient {\\n @GetExchange\\n List<User> listUsers();\\n}\\n
\\n应用场景:微服务间API调用
\\n@HttpExchange(url = \\"/products\\", accept = \\"application/json\\")\\npublic interface ProductServiceClient {\\n @GetExchange(\\"/{id}\\")\\n Product getProduct(@PathVariable String id);\\n @PostExchange\\n Product createProduct(@RequestBody Product product);\\n}\\n// 自动注入使用\\n@Service\\npublic class OrderService {\\n @Autowired\\n private ProductServiceClient productClient;\\n \\n public void validateProduct(String productId) {\\n Product product = productClient.getProduct(productId);\\n // 校验逻辑...\\n }\\n}\\n
\\n{\\n \\"type\\": \\"https://example.com/errors/insufficient-funds\\",\\n \\"title\\": \\"余额不足\\",\\n \\"status\\": 400,\\n \\"detail\\": \\"当前账户余额为50元,需支付100元\\"\\n}\\n
\\n@RestControllerAdvice\\npublic class GlobalExceptionHandler {\\n @ExceptionHandler(ProductNotFoundException.class)\\n public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {\\n ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);\\n problem.setType(URI.create(\\"/errors/product-not-found\\"));\\n problem.setTitle(\\"商品不存在\\");\\n problem.setDetail(\\"商品ID: \\" + ex.getProductId());\\n return problem;\\n }\\n}\\n// 触发异常示例\\n@GetMapping(\\"/products/{id}\\")\\npublic Product getProduct(@PathVariable String id) {\\n return productRepo.findById(id)\\n .orElseThrow(() -> new ProductNotFoundException(id));\\n}\\n
\\nnative-image -jar myapp.jar\\n
\\n# application.yml配置\\nspring:\\n security:\\n oauth2:\\n authorization-server:\\n issuer-url: https://auth.yourcompany.com\\n token:\\n access-token-time-to-live: 1h\\n
\\n@Configuration\\n@EnableWebSecurity\\npublic class AuthServerConfig {\\n @Bean\\n public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {\\n http\\n .authorizeRequests(authorize -> authorize\\n .anyRequest().authenticated()\\n )\\n .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);\\n return http.build();\\n }\\n}\\n
\\n应用场景:云原生Serverless函数
\\n# 打包命令(需安装GraalVM)\\nmvn clean package -Pnative\\n# 运行效果对比\\n传统JAR启动:启动时间2.3s | 内存占用480MB\\n原生镜像启动:启动时间0.05s | 内存占用85MB\\n
\\n// 自定义业务指标\\n@RestController\\npublic class OrderController {\\n private final Counter orderCounter = Metrics.counter(\\"orders.total\\");\\n @PostMapping(\\"/orders\\")\\n public Order createOrder() {\\n orderCounter.increment();\\n // 创建订单逻辑...\\n }\\n}\\n# Prometheus监控指标示例\\norders_total{application=\\"order-service\\"} 42\\nhttp_server_requests_seconds_count{uri=\\"/orders\\"} 15\\n
\\n场景:电商平台升级
\\n// 商品查询服务(组合使用新特性)\\n@RestController\\npublic class ProductController {\\n // 声明式调用库存服务\\n @Autowired\\n private StockServiceClient stockClient;\\n // 虚拟线程处理高并发查询\\n @GetMapping(\\"/products/{id}\\")\\n public ProductDetail getProduct(@PathVariable String id) {\\n return CompletableFuture.supplyAsync(() -> {\\n Product product = productRepository.findById(id)\\n .orElseThrow(() -> new ProductNotFoundException(id));\\n \\n // 并行查询库存\\n Integer stock = stockClient.getStock(id);\\n return new ProductDetail(product, stock);\\n }, Executors.newVirtualThreadPerTaskExecutor()).join();\\n }\\n}\\n
\\nspring-boot-properties-migrator
检测配置变更通过以上升级方案:
\\n本次升级标志着Spring生态正式进入云原生时代。重点关注:虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向。
","description":"Spring生态重大升级全景图 一、Spring 6.0核心特性详解\\n1. Java版本基线升级\\n最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能\\n虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+)\\n// 示例:虚拟线程使用\\nThread.ofVirtual().name(\\"my-virtual-thread\\").start(() -> {\\n // 业务逻辑\\n});\\n\\n虚拟线程(Project Loom)\\n应用场景:电商秒杀系统、实时聊天服务等高并发场景\\n// 传统线程池 vs 虚拟线程\\n//…","guid":"https://juejin.cn/post/7476389305881296934","author":"后端出路在何方","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-01T04:19:54.360Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/decd50b259a543f5a3db33fa4a19aabe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv5Ye66Lev5Zyo5L2V5pa5:q75.awebp?rk3s=f64ab15b&x-expires=1741407593&x-signature=ryRa70JM14BrEOsRi3c2p6J6CIQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/46dfca93de4e451d8117e4ca83df4005~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv5Ye66Lev5Zyo5L2V5pa5:q75.awebp?rk3s=f64ab15b&x-expires=1741407593&x-signature=d0ny4FxXVJbf2EmYG6fNoHgkWn0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","Spring"],"attachments":null,"extra":null,"language":null},{"title":"解密如何快速搭建一套虚拟商品交易系统,推荐这个神奇的开源项目","url":"https://juejin.cn/post/7476324218143522826","content":"大家好,我是五阳,专注于分享电商交易系统设计~
\\n最近我发现抖音上售卖的券包优惠力度很大,也囤了很多优惠券,我在想,能否模仿抖音券包,快速实现一个券包类虚拟商品售卖的交易系统呢?
\\n说干就干,我借助于 memberclub 电商交易中台的SDK,用了将近1天的时间顺利完成开发。
\\n在抖音上券包的交易过程如下
\\n包括
\\n为了叫着方便,模仿的抖音券包,我把它抖阳券包
\\n我花了1天时间,模仿抖音实现了券包交易过程。(五阳擅长后端,前端技术太差劲。后端这些功能有2个小时就搞定了,但是实现这些前端页面花了将近1天,然而效果图还是很low,意思一下,各位兄台多多包涵)
\\n虽然我不是抖音员工,但还是想说一句。抖音上有一点做的非常好,对三方系统的券包发起退款,抖音是极速垫付退款。在三方券可能还未回收之际,就先赔付用户。我猜测券包履约方为三方公司时,两套系统外加公网交互,数据不一致性问题会比较严重,如果先冻券,再退款,数据不一致时,极可能导致售后卡单,为了优化用户体验,就同时发起先退款和冻券。 虽然增加了资金损失风险,但是用户体验更好!这是一种平衡~
\\n言归正传,看看,五阳1天时间如何实现的山寨版券包交易流程
\\n我认为,在系统设计阶段,正确的做法是先抽象业务共性,剥离业务特性,这样才能更好的抽象,构建的系统扩展性也更强。
\\n从如上需求分析来看,抖音券包、京东会员等虚拟商品的交易过程具备较多的业务共性,在实现抖阳券包的交易能力时,我们应该首先抽象出这些业务共性,构建一个通用的虚拟商品交易中台。业务新接入时,为其分配新的业务身份,然后服用这部分通用能力,实现新业务的快速接入和未来快速扩展。
\\nMemberclub项目提供了虚拟商品的交易解决方案,在各类购买场景下提供各类虚拟商品形态的履约及售后结算能力,它抽象了会员等虚拟商品的业务共性,同时对业务差异性预置了扩展点,通过插件方便扩展。
\\n因此我会基于memberclub ,快速地搭建抖阳券包所需要的交易能力。
\\n在 BizTypeEnum 业务身份枚举中定义,抖阳券包业务身份: 2
\\n/**\\n * @author 掘金五阳\\n */\\npublic enum BizTypeEnum {\\n\\n DEFAULT(0, \\"default_biz\\"),\\n DEMO_MEMBER(1, \\"demo_member\\"),\\n VIDEO_MEMBER(3, \\"video_member\\"),\\n MUSIC_MEMBER(4, \\"music_member\\"),\\n DOUYIN_COUPON_PACKAGE(2, \\"douyin_coupon_package\\"),//douyin 优惠券包,支持过期退、多份数购买\\n ;\\n}\\n
\\nmemberclub 中商品模型如下,包括业务身份,商品展示信息、商品结算信息、商品售卖信息、商品履约信息、商品限额信息、商品库存信息、及其他扩展字段。
\\n新增抖阳券包时,商品模型无需新增或修改,仅需要再数据库中,创建券包商品数据。
\\npublic class SkuInfoDO {\\n\\n private long skuId;\\n\\n private int bizType;\\n\\n private SkuViewInfo viewInfo = new SkuViewInfo();\\n\\n private SkuFinanceInfo financeInfo = new SkuFinanceInfo();\\n\\n private SkuSaleInfo saleInfo = new SkuSaleInfo();\\n\\n private SkuPerformConfigDO performConfig = new SkuPerformConfigDO();\\n\\n private SkuInventoryInfo inventoryInfo = new SkuInventoryInfo();\\n\\n private SkuRestrictInfo restrictInfo = new SkuRestrictInfo();\\n\\n private SkuExtra extra = new SkuExtra();\\n\\n private long utime;\\n\\n private long ctime;\\n}\\n
\\n新增商品券包数据如下。(下面的json被打平了,感兴趣可以在 JSON 可视化以后查看。)
\\n{\\"skuId\\":200404,\\"buyCount\\":0,\\"bizType\\":2,\\"viewInfo\\":{\\"displayImage\\":\\"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.alicdn.com%2Fbao%2Fuploaded%2Fi3%2F374544688%2FO1CN016Zx2lK1kV9QkrD6gW_%21%210-item_pic.jpg&refer=http%3A%2F%2Fimg.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1742951021&t=9c87d26097559e952d220dff49a9d060\\",\\"displayName\\":\\"15元混合券包\\",\\"displayDesc\\":\\"无门槛组合券15元;有效期14天;过期退;有效期内限购4次\\",\\"internalName\\":\\"15元混合券包\\",\\"internalDesc\\":\\"无门槛组合券15元\\"},\\"financeInfo\\":{\\"contractorId\\":\\"438098434\\",\\"settlePriceFen\\":900,\\"periodCycle\\":1,\\"financeProductType\\":1},\\"saleInfo\\":{\\"originPriceFen\\":1500,\\"salePriceFen\\":900},\\"performConfig\\":{\\"configs\\":[{\\"bizType\\":2,\\"rightType\\":1,\\"rightId\\":32424,\\"assetCount\\":1,\\"periodCount\\":14,\\"periodType\\":1,\\"cycle\\":1,\\"providerId\\":\\"1\\",\\"grantInfo\\":{},\\"settleInfo\\":{\\"contractorId\\":\\"438098434\\",\\"settlePriceFen\\":500,\\"financeAssetType\\":1,\\"financeable\\":true},\\"viewInfo\\":{\\"displayName\\":\\"5元立减券\\"},\\"saleInfo\\":{}},{\\"bizType\\":2,\\"rightType\\":1,\\"rightId\\":32423,\\"assetCount\\":1,\\"periodCount\\":14,\\"periodType\\":1,\\"cycle\\":1,\\"providerId\\":\\"1\\",\\"grantInfo\\":{},\\"settleInfo\\":{\\"contractorId\\":\\"438098434\\",\\"settlePriceFen\\":1000,\\"financeAssetType\\":1,\\"financeable\\":true},\\"viewInfo\\":{\\"displayName\\":\\"10元立减券\\"},\\"saleInfo\\":{}}]},\\"inventoryInfo\\":{\\"enable\\":false,\\"type\\":0},\\"restrictInfo\\":{\\"enable\\":true,\\"restrictItems\\":[{\\"periodType\\":\\"TOTAL\\",\\"periodCount\\":14,\\"itemType\\":\\"TOTAL\\",\\"userTypes\\":[\\"USERID\\"],\\"total\\":4}]},\\"extra\\":{},\\"utime\\":0,\\"ctime\\":0}\\n
\\n以上商品数据定义在 这里,可以自行插入到数据库。
\\n购买域主要包括三个业务接口,分别为提交订单、(未支付,主动取消)取消订单、售后取消订单。
\\n@ExtensionConfig(desc = \\"购买流程扩展点\\", type = ExtensionType.PURCHASE, must = true)\\npublic interface PurchaseExtension extends BaseExtension {\\n\\n public void submit(PurchaseSubmitContext context);\\n\\n public void reverse(AfterSaleApplyContext context);\\n\\n public void cancel(PurchaseCancelContext context);\\n}\\n
\\n我们定义 DouyinPkgPurchaseExtension 实现该接口的三个业务方法,需要明确的是MemberClub在设计阶段就是定义为电商交易中台项目,所提供的能力均可轻松复用。复用方式主要包括两类:流程编排复用、扩展点定义复用。
\\n从下面的类的提单方法可以看到,每个业务方法并非定制化实现,而是通过流程引擎编排流程节点,每个流程节点执行某一类业务逻辑。如提单流程中一共6个节点,分别为
\\n由于需求上不包含库存限制,因此在流程编排上去除了库存相关节点。如果抖音券包在提单流程上还有其他流程,可以新增流程节点,编排进抖音券包提单扩展点即可,不会影响到已接入的其他业务,最大程度实现了业务隔离。
\\n@ExtensionProvider(desc = \\"抖阳券包 购买提单扩展点\\", bizScenes = {\\n @Route(bizType = BizTypeEnum.DOUYIN_COUPON_PACKAGE, scenes = {SceneEnum.HOMEPAGE_SUBMIT_SCENE})\\n})\\npublic class DouyinPkgPurchaseExtension implements PurchaseExtension {\\n\\n private static FlowChain<PurchaseSubmitContext> submitChain = null;\\n private static FlowChain<AfterSaleApplyContext> purchaseReverseChain = null;\\n private static FlowChain<PurchaseCancelContext> purchaseCancelFlowChain = null;\\n @Autowired\\n private MemberOrderDomainService memberOrderDomainService;\\n\\n @PostConstruct\\n public void init() {\\n submitChain = FlowChain.newChain(PurchaseSubmitContext.class)\\n .addNode(PurchaseSubmitLockFlow.class)\\n .addNode(SkuInfoInitalSubmitFlow.class)\\n .addNode(PurchaseSubmitCmdValidateFlow.class)\\n .addNode(PurchaseUserQuotaFlow.class) //检查限额\\n //.addNode(PurchaseValidateInventoryFlow.class) //检查库存\\n .addNode(MemberOrderSubmitFlow.class) // 会员提单\\n //.addNode(PurchaseMarkNewMemberFlow.class) //新会员标记\\n //.addNode(PurchaseOperateInventoryFlow.class) //扣减库存\\n .addNode(CommonOrderSubmitFlow.class) //订单系统提单\\n ;\\n\\n purchaseReverseChain = FlowChain.newChain(AfterSaleApplyContext.class)\\n //.addNode(PurchaseReverseNewMemberFlow.class)\\n //.addNode(PurchaseReverseInventoryFlow.class)\\n .addNode(PurchaseReverseMemberQuotaFlow.class)\\n //\\n ;\\n\\n purchaseCancelFlowChain = FlowChain.newChain(PurchaseCancelContext.class)\\n .addNode(PurchaseCancelLockFlow.class)\\n .addNode(PurchaseCancelOrderFlow.class)\\n //.addNode(PurchaseCancelNewMemberFlow.class)\\n .addNode(PurchaseCancelQuotaFlow.class)\\n //.addNode(PurchaseCancelInventoryFlow.class)\\n ;\\n }\\n\\n @Override\\n public void submit(PurchaseSubmitContext context) {\\n submitChain.execute(context);\\n }\\n\\n @Override\\n public void reverse(AfterSaleApplyContext context) {\\n purchaseReverseChain.execute(context);\\n }\\n\\n @Override\\n public void cancel(PurchaseCancelContext context) {\\n MemberOrderDO memberOrder = memberOrderDomainService.\\n getMemberOrderDO(context.getCmd().getUserId(), context.getCmd().getTradeId());\\n context.setMemberOrder(memberOrder);\\n\\n purchaseCancelFlowChain.execute(context);\\n }\\n}\\n
\\n商品履约主要在拆合单和权益履约部分需要扩展。
\\n商品购买时因为履约、结算、售后等方面的诉求往往需要拆单履约。怎么理解呢?举例说明
\\n可以看到拆单部分主要是对订单中的商品、商品中的权益进行拆分,制定履约计划,分别进行履约。 抖阳券包支持多份数购买,因此在拆单阶段会根据购买份数,拆单为多个履约项进行履约。
\\n考虑到抖阳券包和默认券包的拆单能力诉求相同,我们直接在原来的扩展点上,添加抖阳券包业务身份,这样抖阳券包就能直接复用这个扩展点。
\\n@ExtensionConfig(desc = \\"履约拆单 扩展点\\", type = ExtensionType.PERFORM_MAIN, must = true)\\npublic interface PerformSeparateOrderExtension extends BaseExtension {\\n public void separateOrder(PerformContext context);\\n}\\n
\\n\\n@ExtensionProvider(desc = \\"默认 履约上下文构建\\", bizScenes = {\\n @Route(bizType = BizTypeEnum.DEMO_MEMBER, scenes = {SceneEnum.SCENE_MONTH_CARD}),\\n @Route(bizType = BizTypeEnum.DOUYIN_COUPON_PACKAGE, scenes = {SceneEnum.SCENE_MONTH_CARD}),\\n})\\npublic class DemoMemberPerformSeparateOrderExtension implements PerformSeparateOrderExtension {\\n\\n FlowChain<PerformContext> performSeparateOrderChain = null;\\n\\n\\n @Autowired\\n private FlowChainService flowChainService;\\n\\n @PostConstruct\\n public void run() throws Exception {\\n performSeparateOrderChain = FlowChain.newChain(flowChainService, PerformContext.class)\\n .addNode(InitialSkuPerformContextsFlow.class)\\n .addNode(MutilBuyCountClonePerformItemFlow.class)\\n //如果年卡周期是自然月,则可以在此处根据当前期数计算每期的天数\\n .addNode(CalculateImmediatePerformItemPeriodFlow.class)//计算立即履约项 时间周期\\n .addNode(CalculateOrderPeriodFlow.class)//计算订单整体有效期\\n .addNode(PerformContextExtraInfoBuildFlow.class)// 构建扩展属性\\n ;\\n }\\n\\n @Override\\n public void separateOrder(PerformContext context) {\\n flowChainService.execute(performSeparateOrderChain, context);\\n }\\n}\\n
\\n履约执行阶段的主要工作包括
\\n由于抖阳券包需要支持过期退,因此在履约完成以后需要在任务表新增一条任务,系统每日定时执行已过期的任务。在券包过期后,系统自动发起售后。因此在执行扩展点新增了 MemberExpireRefundTaskCreatedFlow 流程节点,用来新增任务。
\\n@ExtensionProvider(desc = \\"抖阳券包执行履约扩展点\\", bizScenes = {\\n @Route(bizType = BizTypeEnum.DOUYIN_COUPON_PACKAGE, scenes = {SceneEnum.SCENE_MONTH_CARD})//抖音券包月卡,多份数, 多商品\\n})\\npublic class DouyinPkgPerformExecuteExtension implements PerformExecuteExtension {\\n private FlowChain<PerformContext> flowChain;\\n\\n private FlowChain<PerformContext> subFlowChain;\\n @Autowired\\n private FlowChainService flowChainService;\\n\\n @PostConstruct\\n public void init() {\\n subFlowChain = FlowChain.newChain(flowChainService, PerformContext.class)\\n .addNode(SingleSubOrderPerformFlow.class)\\n .addNodeWithSubNodes(ImmediatePerformFlow.class, PerformItemContext.class,\\n // 构建 MemberPerformItem, 发放权益\\n ImmutableList.of(MemberPerformItemFlow.class, PerformItemGrantFlow.class));\\n\\n flowChain = FlowChain.newChain(flowChainService, PerformContext.class)\\n .addNode(MemberResourcesLockFlow.class)\\n .addNode(MemberOrderOnPerformSuccessFlow.class)\\n .addNode(MemberPerformMessageFlow.class)\\n .addNode(MemberExpireRefundTaskCreatedFlow.class)\\n .addNodeWithSubNodes(MutilSubOrderPerformFlow.class, subFlowChain)\\n ;\\n }\\n\\n @Override\\n public void execute(PerformContext context) {\\n flowChainService.execute(flowChain, context);\\n }\\n}\\n\\n\\n
\\n抖阳券包在商品履约配置内容中,仅需要配置1张优惠券即可,毕竟券包售卖就是花钱买券包。 券包发放也比较简单,制定权益ID,绑定券模版ID、指定券包有效期和发券张数即可。(如果业务上还需要其他发放参数,在扩展字段中扩展即可)
\\n我们定义了权益履约的通用SPI接口,履约方实现该接口(或者履约方定义接口,我们适配也行,只要保持接口协议稳定,不经常变化就行。)
\\npublic interface AssetsFacadeSPI {\\n\\n @RequestMapping(method = RequestMethod.POST, value = \\"/items/grant\\")\\n public GrantResponseDO grant(@RequestBody GrantRequestDO requestDO);\\n\\n @RequestMapping(method = RequestMethod.POST, value = \\"/items/fetch\\")\\n public AssetFetchResponseDO fetch(@RequestBody AssetFetchRequestDO request);\\n\\n @RequestMapping(method = RequestMethod.POST, value = \\"/items/reverse\\")\\n public AssetReverseResponseDO reverse(@RequestBody AssetReverseRequestDO request);\\n}\\n
\\n由于抖阳券包基于最基础的立减优惠券权益,系统已基于SPI接口实现,因此复用系统扩展点即可,无需新增。
\\n售后主要包括三个业务方法,分别为
\\n售后预览阶段主要是检查用户订单数据合法性,如状态机校验、有效期校验、券包使用状态校验。抖阳券包计算券包是否可退时,基于券使用状态,对每张券分摊的实付金额进行加和,计算未使用券包的金额,如果金额大于0,则可以退,如果金额为0,则不可退。
\\n以下是售后预览阶段的扩展点。
\\n\\n@ExtensionProvider(desc = \\"抖音券包售后预览扩展点\\", bizScenes = {\\n @Route(bizType = BizTypeEnum.DOUYIN_COUPON_PACKAGE, scenes = {SceneEnum.SCENE_AFTERSALE_MONTH_CARD})\\n})\\npublic class DouyinPkgAftersalePreviewExtension implements AftersalePreviewExtension {\\n\\n private FlowChain<AftersalePreviewContext> previewChain = null;\\n\\n private FlowChain<AftersalePreviewContext> subPreviewChain = null;\\n\\n @Autowired\\n private FlowChainService flowChainService;\\n\\n @PostConstruct\\n public void init() {\\n subPreviewChain = FlowChain.newChain(AftersalePreviewContext.class)\\n .addNode(RealtimeCalculateUsageAmountFlow.class) //实时计算使用类型\\n .addNode(OverallCheckUsageFlow.class) //完全检查使用类型\\n .addNode(CalculateRefundWayFlow.class) //计算赔付类型\\n .addNode(GenerateAftersalePlanDigestFlow.class) //生成售后计划摘要\\n ;\\n\\n previewChain = FlowChain.newChain(AftersalePreviewContext.class)\\n .addNode(AftersalePreviewDegradeFlow.class)\\n // TODO: 2025/1/1 //增加售后单 进行中校验,当前存在生效中受理单,不允许预览(数据处于不一致状态,无法获得准确的预览结果),返回特殊错误码\\n .addNode(AftersaleStatusCheckFlow.class)\\n .addNode(AftersaleGetAndCheckPeriodFlow.class)\\n .addNode(GetAndCheckAftersaleTimesFlow.class)\\n .addNodeWithSubNodes(MutilSubOrderPreviewFlow.class, subPreviewChain)\\n ;\\n }\\n\\n @Override\\n public void preview(AftersalePreviewContext context) {\\n flowChainService.execute(previewChain, context);\\n }\\n}\\n
\\n售后受理阶段需要再次检查券包订单是否可退,是否和售后预览结果一致。售后受理流程主要通过售后单状态机进行驱动,包括调用履约域逆向履约方法、提单域逆向取消订单、和调用订单退款。
\\n@ExtensionProvider(desc = \\"示例会员售后受理扩展点\\", bizScenes = {\\n @Route(bizType = BizTypeEnum.DOUYIN_COUPON_PACKAGE, scenes = {SceneEnum.SCENE_AFTERSALE_MONTH_CARD})\\n})\\npublic class DouyinPkgAfterSaleApplyExtension implements AfterSaleApplyExtension {\\n\\n\\n FlowChain<AfterSaleApplyContext> applyFlowChain = null;\\n\\n FlowChain<AfterSaleApplyContext> checkFlowChain = null;\\n\\n FlowChain<AfterSaleApplyContext> doApplyFlowChain = null;\\n\\n @Autowired\\n private FlowChainService flowChainService;\\n\\n @PostConstruct\\n public void init() {\\n applyFlowChain = FlowChain.newChain(flowChainService, AfterSaleApplyContext.class)\\n .addNode(AftersaleApplyLockFlow.class) //加锁\\n .addNode(AftersaleApplyPreviewFlow.class) //售后预览\\n .addNode(AfterSalePlanDigestCheckFlow.class) //校验售后计划摘要\\n .addNode(AftersaleGenerateOrderFlow.class) //生成售后单\\n .addNode(AftersaleDoApplyFlow.class)\\n ;\\n\\n doApplyFlowChain = FlowChain.newChain(flowChainService, AfterSaleApplyContext.class)\\n .addNode(AftersaleOrderDomainFlow.class) \\n .addNode(MemberOrderRefundSuccessFlow.class) //售后成功后, 更新主单子单的状态为成功\\n .addNode(AftersaleAsyncRollbackFlow.class) // 失败异步回滚\\n .addNode(AftersaleReversePerformFlow.class) //逆向履约\\n .addNode(AftersaleReversePurchaseFlow.class) //逆向取消订单\\n .addNode(AftersaleRefundOrderFlow.class) //退款\\n //.addNode()\\n ;\\n\\n\\n }\\n\\n @Override\\n public void apply(AfterSaleApplyContext context) {\\n flowChainService.execute(applyFlowChain, context);\\n }\\n\\n @Override\\n public void doApply(AfterSaleApplyContext context) {\\n flowChainService.execute(doApplyFlowChain, context);\\n }\\n\\n @Override\\n public void customBuildAftersaleOrder(AfterSaleApplyContext context, AftersaleOrderDO aftersaleOrderDO) {\\n\\n }\\n}\\n
\\n过期退流程依赖任务定时触发能力,memberclub 定义了 任务触发扩展点,每个业务身份和任务类型均可以自行扩展过期任务如何触发。
\\n如下扩展点,分为两个流程,其一为触发流程,主要功能是根据数据库分库分表,并发的扫描过期的目标任务,扫描出以后,需要调用 执行流程,每个执行流程仅处理1个过期任务。执行流程也支持编排,如过期退执行流程,编排了两个节点,分别为
\\n\\npublic abstract class DefaultExpireRefundTriggerExtension implements OnceTaskTriggerExtension {\\n private FlowChain<OnceTaskTriggerContext> triggerFlowChain = null;\\n\\n private FlowChain<OnceTaskExecuteContext> executelowChain = null;\\n\\n @PostConstruct\\n public void init() {\\n triggerFlowChain = FlowChain.newChain(OnceTaskTriggerContext.class)\\n .addNode(OnceTaskSeprateFlow.class)\\n .addNodeWithSubNodes(OnceTaskConcurrentTriggerFlow.class, OnceTaskTriggerJobContext.class,\\n ImmutableList.of(OnceTaskForceRouterFlow.class, OnceTaskScanDataFlow.class)\\n )\\n .addNode(OnceTaskTriggerMonitorFlow.class)\\n ;\\n\\n executelowChain = FlowChain.newChain(OnceTaskExecuteContext.class)\\n .addNode(OnceTaskRepositoryFlow.class)\\n .addNode(ExpiredRefundOnceTaskExecuteFlow.class)\\n ;\\n }\\n\\n\\n @Override\\n public void trigger(OnceTaskTriggerContext context) {\\n triggerFlowChain.execute(context);\\n }\\n\\n @Override\\n public void execute(OnceTaskExecuteContext context) {\\n executelowChain.execute(context);\\n }\\n}\\n
\\n客户端部分在uniapp 插件市场,找到了一个开源模版,其中包括商品列表页和购物车页面,我修改了和后端接口交互部分,一共以下几个后端接口。我自己新增了购买记录页。
\\n技术栈包括
\\n以上后端部分大概花了2个小时就能完成,主要原因是我基于memberclub 进行二次开发,它提供了虚拟商品交易提单、履约、售后和结算的 基础SDK,并且提供了流程引擎和 扩展点引擎能力,可以非常快速的支持新增业务身份进行扩展。
\\n如果是从0到1开发,别说2小时,两天甚至两周也不一定能开发完~
\\nmemberclub开源地放这里了,想学习电商交易系统设计的可以看看。
\\nGitee:gitee.com/juejinwuyan…
\\nGitHub : github.com/juejin-wuya…
\\nmemberclub 在standalone模式下无需任何中间件即可启动,在集成测试环境默认依赖 mysql/redis/apollo/rabbitmq\\n等中间件。所以如果仅学习使用,只需要启动memberclub服务即可!
\\ncd bin && ./starter.sh -e ut
\\n然后 git clone 下载memberclub H5项目,地址在 gitee.com/juejinwuyan…
\\n下载完成后,需要下载 HBuilderX IDE 启动H5项目。这样就可以了
","description":"大家好,我是五阳,专注于分享电商交易系统设计~ 最近我发现抖音上售卖的券包优惠力度很大,也囤了很多优惠券,我在想,能否模仿抖音券包,快速实现一个券包类虚拟商品售卖的交易系统呢?\\n\\n说干就干,我借助于 memberclub 电商交易中台的SDK,用了将近1天的时间顺利完成开发。\\n\\n关键需求分析\\n\\n在抖音上券包的交易过程如下\\n\\n进入商品页面,预览券包商品详情,可以看到抖音上的券包商品有有效期限制,过期后还支持过期退。\\n在提单页面,可以加购商品,支持单商品多份数购买。如果多次勾选会发现,部分商品有单用户购买限额。(非商品库存)\\n支付完成后…","guid":"https://juejin.cn/post/7476324218143522826","author":"五阳","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-28T13:56:16.347Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a0a912d624214fcda8a126f8518b1299~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741492301&x-signature=m1v5DNA2vPnvPhJr%2F8iHOmCOves%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1dd4ff2361f54c8da985ff3c05483ee0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741492301&x-signature=7TEc4RstOQ0GBEGdPKwZ3Ljf2MU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/082015d280144b35a41d19c48eeff247~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741492301&x-signature=Zf5z5qFbQmuKfxq6D4iNwXLhgdc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/519ce47a73eb4d3bbfd4731fed6d68ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqU6Ziz:q75.awebp?rk3s=f64ab15b&x-expires=1741492301&x-signature=SbvQvZa6PKdYPyCCmvziCpQTaaw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"告别Cursor降智!国产MarsCode+DeepSeek R1组合来袭","url":"https://juejin.cn/post/7476264620807995426","content":"最近,使用 Cursor 编程工具的开发者们纷纷吐槽,不是因为它频繁封号,而是因为其明显的降智特性。开发者们发现,这款工具生成的代码存在诸多问题,甚至在简单任务中也显得笨拙和低效。以一个最简单的例子(最离谱的是我用的是claude3.7模型! ):
\\n从年初开始,Cursor 莫名其妙地大规模封号,我费尽心思找了各种方法续命,好不容易稳定下来,却又迎来了降智暴击。于是,我开始在网上疯狂寻找替代方案,直到我偶然发现了 MarsCode。说实话,这个插件我很久之前就装过,但当时体验并不理想。然而,最近它集成了 DeepSeek(V3 和 R1) ,我试用后发现效果出奇的好,甚至可以说是真香
了。
本文将分享我对 MarsCode 的真实使用体验,如果你也正被 Cursor 的降智问题困扰,不妨继续往下看,或许这款国产平替工具能成为你的新选择。
\\n从年初开始Cursor就开始大规模封号,想必用Cursor的jym都深有体会,我开始用的Cursor无限续杯方案被封号,结果又去网上找了很多途径获取账号,还是被封,最后经过千辛万苦找了一个稳定的方案用上了,结果给我来了一波降智,网上找了一波资料,解释为此次降智不分国籍不分账号不分是否付费。
\\n网上说把提示词添加到Rules for AI内【有机会取消降智的参数】
You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, Headless UI, Element Plus, and Tailwind, with a deep understanding of best practices and performance optimization techniques in these technologies.\\nCode Style and Structure\\nWrite concise, maintainable, and technically accurate TypeScript code with relevant examples.\\nUse functional and declarative programming patterns; avoid classes.\\nFavor iteration and modularization to adhere to DRY principles and avoid code duplication.\\nUse descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).\\nOrganize files systematically: each file should contain only related content, such as exported components, subcomponents, helpers, static content, and types.\\nNaming Conventions\\nUse lowercase with dashes for directories (e.g., components/auth-wizard).\\nFavor named exports for functions.\\nTypeScript Usage\\nUse TypeScript for all code; prefer interfaces over types for their extendability and ability to merge.\\nAvoid enums; use maps instead for better type safety and flexibility.\\nUse functional components with TypeScript interfaces.\\nSyntax and Formatting\\nUse the \\"function\\" keyword for pure functions to benefit from hoisting and clarity.\\nAlways use the Vue Composition API script setup style.\\nUI and Styling\\nUse Headless UI, Element Plus, and Tailwind for components and styling.\\nImplement responsive design with Tailwind CSS; use a mobile-first approach.\\nPerformance Optimization\\nLeverage VueUse functions where applicable to enhance reactivity and performance.\\nWrap asynchronous components in Suspense with a fallback UI.\\nUse dynamic loading for non-critical components.\\nOptimize images: use WebP format, include size data, implement lazy loading.\\nImplement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.\\nKey Conventions\\nOptimize Web Vitals (LCP, CLS, FID) using tools like Lighthouse or WebPageTest.\\nSpeak to me in Chinese\\nNever remove unedited content from files\\nAvoid summarizing unchanged content as \\"[rest of file remains the same]\\"\\nSeek confirmation before any content deletion\\nFocus on updates and additions rather than deletions\\n
\\n我加了,也没有什么实际的作用,它还是像个zz。。。。。。。
\\n在经历了Cursor的降智折磨后,我开始寻找其他替代工具。首先尝试了 Windsurf,它的功能看似不错,但收费模式让我望而却步:
\\n免费版:
\\nPro版($10/月):
\\n虽然据大家反馈 Windsurf 效果不错,但收费门槛让我最终放弃了尝试。
\\n接着,我试用了 Trae,但它的表现让我大失所望:
\\n到这里我就没有用它的欲望了,说实话我只是想确认一下能不能用(聪不聪明),结果给我来这一出,希望后期能改善一下吧。正当我焦急万分的时候,群友给我了我希望:
\\n说实话,我之前用过 MarsCode,但当时的体验并不理想。抱着试试看的心态,我重新打开了它——惊喜来了!它居然集成了 DeepSeek(V3 和 R1) ,并且在我测试的数字大小问题上给出了正确答案。
\\n我开始全面测试它的辅助编程能力。如果你也好奇它的表现,不妨继续往下看(MarsCode功能我没有全部测,只是测了几个核心功能)。
\\nMarsCode会对工作区中的代码进行全局索引构建,发起 #Workspace问答时将自动全局检索与问题相关的跨文件上下文。
\\n我随便找了一个工程测试(blog.csdn.net/c1821359022…),它有三个模型可以选,分别是Douao-1.5-pro、DeepSpeek V3、DeepSpeek R1,我这里选的是DeepSpeek R1:
\\n结果:
\\n总体还可以,大体的结构清晰地梳理出来了。
\\n指定代码理解有两种方式
\\n第一种:
\\n选择要理解的代码,可以点击【#】按钮,也可以键盘输入#,选择File:
\\n找到要理解的代码:
\\n查看相应解析:
\\n代码总结:
\\n第二种:
\\n鼠标选中具体代码,点击MarsCode图标,选择【解释代码】:
\\n生成的解释:
\\n为了测试 MarsCode 的代码生成能力,我选择了一个我不太熟悉的前端领域——用 JavaScript 生成一个贪吃蛇小游戏:
\\n以下是生成结果:
\\n需要注意的点: 和Cursor编程差不多,尽量明确你的需求,在上图中我把生成代码的位置,界面风格,游戏规则等要点都梳理给了DeepSpeek大模型。
\\n还是以贪吃蛇的游戏为例,我想把小蛇改成绿色系(现在是蓝色),移动慢一点:
\\n代码修改这个和Cursor差不多,先点击Apply,然后把你要修改的代码确认一下:
\\n确认修改就点击Accpet:
\\n改完以后可以再点击Apply按钮确定一下:
\\n测试一下修改效果吧:
\\n显然它改多了,全部改成绿色,没关系,我们修正一版:
\\n看一下效果:
\\n首先我写了一个冒泡排序方法:
\\n我想让它优化为双向冒泡排序(鸡尾酒排序) :
\\n后端没有Apply按钮,我点击了插入文件:
\\n效果不是很理想(不智能,乱插入),不如把原来代码删了重新粘贴它生成的,我不太清楚MarsCode两个版本(VS和JetBrains)是不是两个团队在做,代码修改测下来明显前端体验更好,希望后端也尽快跟上吧。
\\n如果你也被 Cursor 的降智问题困扰,或者正在寻找一款更智能、更稳定的 AI 编程助手,现在就是最好的机会!通过下方专属链接,你可以立即获取 MarsCode,体验它强大代码辅助能力: www.marscode.cn/events/s/i5…
\\n说实话,在遇到 Cursor 降智问题之后,我确实挺头疼的。不过,幸好我找到了 MarsCode,它集成了 DeepSpeek 大模型(V3和R1),作为一个替代工具,效果还算不错。它的代码理解和生成能力确实比最近的 Cursor(降智版) 稳定多了。
\\n在实测MarsCode的过程中,我深刻意识到:
你的选择:
\\n[ ] 继续忍受Cursor的降智折磨
\\n[✔️] 点击下方链接开启智能编程新纪元
\\n👉 www.marscode.cn/events/s/i5…
如果本文对你有帮助,请动动你发财的小手点点关注吧小肥肠将持续更新更多AI干货文章
文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!
\\n资料分享
\\n「清华大学104页《DeepSeek:从入门到精通》.pdf」,链接:pan.quark.cn/s/81367f52c…
\\n大家好呀,我是飞鱼。
\\n之前网上看到一个段子:
\\n\\n\\n❝
\\n刚毕业那会,朋友里有去银行的一个月拿三四千,去医院的甚至无薪实习。
\\n我做了程序员去个小互联网公司薪水上万属实得意坏了。
\\n现在十年过去了,银行的朋友靠人脉接私活解决买房贷款问题,已经财富自由,现在到处旅游。
\\n医院的朋友积累了十年的技术到处有机构挖他,年薪已经50W+了身价还在持续增值。
\\n而我,十年来学习的技术很快过时,又去学新的,又过时,不断循环,拼了老命和年轻人抢饭吃。
\\n现在老了学不动了,开始研究如何稳拿N+1,被裁之后又何去何从?迷茫得像个刚毕业的青涩小伙。
\\n
所以,程序员明明是靠脑力吃饭,最后却混成拼体力,其实挺悲哀的:
\\n\\n\\n❝
\\n程序员以为自己的技术值高薪,其实是业务值钱才值钱。
\\n而99%的程序员的技术不具有不可取代性,所以资本可以随时换掉程序员。
\\n从历史的长河看,程序员和1900年织布的工人没什么区别,高级技术工人而已。
\\n
肯定也有朋友遇到这样的:
\\n\\n\\n❝
\\n刚入职时维护的一坨屎山代码,是一个干了20年程序员,现在当管理的人留下的。
\\n不开玩笑的,除了他以外,基本上没人能看懂他写的代码。
\\n但是偏偏功能又能实现,领导上面领导一直觉得他很不错,但是说实话,这哥们写的就是屎,后来的人根本就没法接。
\\n
所以,文档清晰,注释完整,对新人不吝赐教,换来的是可能时更容易被取代:
\\n\\n\\n❝
\\n劣币驱逐良币的后果就是:产品代码都变成无法维护的屎山,能跑就没人敢碰。
\\n
外面人人都以为程序员工资很高,实际大多数程序员如果用时薪算其实并不算高,但是身体却越熬越差。
\\n\\n\\n❝
\\n而且大多数程序员这行干久了,因为钱太容易赚,会对这个世界产生了错觉。
\\n
我个人觉得程序员最大的悲哀是:
\\n\\n\\n❝
\\n过度沉浸在技术的世界里,忽视了自身技能的提升和个人成长。
\\n导致在日新月异的技术浪潮中逐渐失去竞争力,最终面临职业瓶颈或失业的困境。
\\n
程序员有时候活的挺累的:
\\n\\n\\n❝
\\n996嫌累,摸鱼觉得没意思,使用开源库觉得没技术含量,自己造轮子又太累。
\\n写代码羡慕领导写PPT,写PPT害怕自己没有硬实力。
\\n终其一生,满是遗憾。
\\n进体制觉得太平庸,在私企又担心不稳定,想转行嫌工资低。
\\n终其一生,眼高手低。
\\n
其实最重要的是学会跟自己和解,跟生活和解。
\\n有啥其他看法,欢迎在评论区留言讨论。
\\n\\n\\n❝
\\n想看技术文章的,可以去我的个人网站:hardyfish.top/
\\n\\n
\\n- 目前网站的内容足够应付基础面试(
\\nP7
)了!
题目描述
\\n\\n\\n❝
\\n给定一个非负整数
\\nnum
,反复将各个位上的数字相加,直到结果为一位数,返回这个结果。
示例 1:
\\n输入: num = 38\\n输出: 2 \\n解释: 各位相加的过程为:\\n38 --> 3 + 8 --> 11\\n11 --> 1 + 1 --> 2\\n由于 2 是一位数,所以返回 2。\\n
\\n示例 2:
\\n输入: num = 0\\n输出: 0\\n
\\n解题思路
\\n\\n\\n❝
\\n输入数字为0时,输出为0。
\\n输入数字是9的倍数时,输出为9。
\\n输入数字非0且不是9的倍数时,输出为
\\n(x % 9)
。
代码实现
\\nJava
代码:
class Solution {\\n public int addDigits(int num) {\\n if(num == 0) {\\n return 0;\\n }\\n return num % 9 == 0 ? 9 : num % 9;\\n }\\n}\\n
","description":"文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识! 资料分享\\n\\n「清华大学104页《DeepSeek:从入门到精通》.pdf」,链接:pan.quark.cn/s/81367f52c…\\n\\n大家好呀,我是飞鱼。\\n\\n之前网上看到一个段子:\\n\\n❝\\n\\n刚毕业那会,朋友里有去银行的一个月拿三四千,去医院的甚至无薪实习。\\n\\n我做了程序员去个小互联网公司薪水上万属实得意坏了。\\n\\n现在十年过去了,银行的朋友靠人脉接私活解决买房贷款问题,已经财富自由,现在到处旅游。\\n\\n医院的朋友积累了十年的技术到处有机构挖他,年薪已经50W+了身价还在持续增值。\\n\\n而我…","guid":"https://juejin.cn/post/7475960553027518479","author":"程序员飞鱼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-27T12:00:57.265Z","media":null,"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"RabbitMQ如何实现消息100%可靠消费?3大核心方案+实战思路","url":"https://juejin.cn/post/7475896708417765412","content":"在分布式系统中,消息丢失可能发生在以下环节:
\\n下面是RabbitMQ保证消息100%被消费的流程图,简要总结了生产端和消费端的各个步骤。
\\ngraph TD;\\n A[生产者发送消息] --\x3e B{消息持久化}\\n B --\x3e C[队列设置为持久化]\\n B --\x3e D[消息写入磁盘]\\n A --\x3e E{事务机制}\\n E --\x3e F[开启事务]\\n E --\x3e G[提交事务]\\n A --\x3e H{Confirm机制}\\n H --\x3e I[发送消息]\\n H --\x3e J[等待确认]\\n J --\x3e K{确认成功?}\\n K --\x3e |是| L[消息投递成功]\\n K --\x3e |否| M[重试]\\n \\n N[消费者处理消息] --\x3e O{手动确认}\\n O --\x3e P[处理成功]\\n O --\x3e Q[处理失败]\\n Q --\x3e R[消息重新入队]\\n P --\x3e S[发送确认信号]\\n S --\x3e T[消息消费成功]\\n
\\nRabbitMQ是一种广泛使用的消息中间件,能够在分布式系统中实现异步通信。在实际应用中,我们希望确保每条消息都能被成功消费,避免丢失。下面将详细探讨RabbitMQ如何实现这一目标,包括生产端的消息投递机制和消费端的消息处理方式,并通过流程图来清晰展示各个步骤。
\\n生产端的核心任务是让消息100%到达RabbitMQ队列。如果消息连队列都没进去,消费就无从谈起。以下是四种关键机制:
\\n什么是消息持久化?
\\n将消息和队列保存到磁盘,即使RabbitMQ重启,数据也不丢失。需同时设置队列和消息的持久化属性。
三种方式:
\\ndurable=true
。channel.queueDeclare(\\"order_queue\\", true, false, false, null);\\n
\\ndeliveryMode=2
。AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().deliveryMode(2).build();\\nchannel.basicPublish(\\"\\", \\"order_queue\\", props, message);\\n
\\ndurable=true
(默认true)。流程图:
\\n示例代码:
\\nchannel.queueDeclare(\\"myQueue\\", true, false, false, null); // 声明持久化队列\\nchannel.basicPublish(\\"\\", \\"myQueue\\", MessageProperties.PERSISTENT_TEXT_PLAIN, messageBody); // 发布持久化消息\\n
\\n什么是事务消息机制?
\\n类比数据库事务:生产者发送消息时,通过开启事务(Transaction),确保消息发送与业务操作的原子性。比如,用户在电商下单时,订单数据写入数据库和发送“扣库存”消息必须同时成功或失败。
实现流程:
\\nchannel.txSelect(); // 开启事务\\nchannel.basicPublish(...); // 发送消息\\nchannel.txCommit(); // 提交事务(若失败则回滚)\\n
\\n示例代码:
\\nchannel.txSelect(); // 开启事务\\nchannel.basicPublish(\\"\\", \\"myQueue\\", null, messageBody); // 发送消息\\nchannel.txCommit(); // 提交事务\\n
\\n流程图:
\\n优缺点:
\\n什么是Confirm机制?
\\n类似快递的“签收回执”:生产者发送消息后,RabbitMQ异步返回一个确认(ACK),告知消息已成功入队。若未收到ACK,生产者可选择重发。
实现流程:
\\nchannel.confirmSelect(); // 开启Confirm模式\\nchannel.basicPublish(...); \\nchannel.waitForConfirms(); // 等待确认(可设置超时)\\n
\\n流程图:
\\n示例代码:
\\nchannel.confirmSelect(); // 开启确认模式\\nchannel.basicPublish(\\"\\", \\"myQueue\\", null, messageBody); // 发送消息\\nif (channel.waitForConfirms()) { // 等待确认\\n System.out.println(\\"Message confirmed\\");\\n}\\n
\\n模式对比:
\\n什么是消息入库?
\\n在发送消息前,先将消息存入本地数据库,并标记状态(如“发送中”)。RabbitMQ确认收到后更新状态为“已发送”;若发送失败,定时任务扫描数据库重新投递。
实现流程:
\\n流程图:
\\n适用场景:
\\n消息进入队列后,消费端需保证消息被正确处理,且不因崩溃或异常丢失。以下是关键机制:
\\n核心逻辑:
\\n消费者处理完消息后,必须手动发送ACK(确认)给RabbitMQ。若未发送ACK或处理中崩溃,消息会重新入队。
代码示例:
\\nchannel.basicConsume(\\"myQueue\\", false, (consumerTag, delivery) -> {\\n try {\\n // 处理消息\\n processMessage(delivery.getBody());\\n channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 手动确认\\n } catch (Exception e) {\\n channel.basicNack(tag, false, true); // 拒绝消息并重新入队\\n }\\n}, consumerTag -> { });\\n
\\n流程图:
\\n什么是死信队列?
\\n当消息因以下原因无法被消费时,会进入死信队列(Dead Letter Exchange):
配置方式:
\\nMap<String, Object> args = new HashMap<>();\\nargs.put(\\"x-dead-letter-exchange\\", \\"dlx_exchange\\"); // 绑定死信交换机\\nchannel.queueDeclare(\\"order_queue\\", true, false, false, args);\\n
\\n处理流程:
\\n什么是幂等性?
\\n同一消息被消费多次,结果与一次一致。例如,支付成功消息重复消费时,不应重复扣款。
实现方式:
\\n数据库唯一约束(如订单ID唯一)
\\n分布式锁(如Redis锁)
\\n版本号机制(消息携带版本号,消费时校验)
\\n流程图:
\\n要保障消息100%被消费,可从生产者、MQ、消费者三个角色协同设计:
\\n1. Confirm与Return机制
\\n配置示例(SpringBoot) :
\\nspring:\\n rabbitmq:\\n publisher-confirm-type: correlated # 开启Confirm\\n publisher-returns: true # 开启Return\\n template:\\n mandatory: true # 路由失败回调\\n
\\n2. 消息落库+定时补偿
\\n核心思路:发送前将消息存入数据库,收到Confirm后标记状态,定时任务扫描未确认消息重发。
// 发送消息前落库\\nmessageLogMapper.insert(new MessageLog(msgId, \\"SENDING\\"));\\n// Confirm回调处理\\nif (ack) {\\n messageLogMapper.updateStatus(msgId, \\"SENT\\"); \\n} else {\\n // 触发重试逻辑\\n}\\n
\\nRabbitMQ的持久化需同时配置以下三项:
\\nDurable=true
Durable=true
deliveryMode=2
代码示例:
\\n// 发送持久化消息\\nrabbitTemplate.convertAndSend(exchange, routingKey, message, msg -> {\\n msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);\\n return msg;\\n});\\n
\\n1. 关闭自动ACK
\\n改为手动确认,确保业务逻辑执行成功后再删除消息:
channel.basicConsume(queue, false, (tag, message) -> {\\n try {\\n processMessage(message); // 业务处理\\n channel.basicAck(tag, false); // 手动ACK\\n } catch (Exception e) {\\n channel.basicNack(tag, false, true); // 重试\\n }\\n});\\n
\\n2. 死信队列兜底
\\n当消息重试超限后,转入死信队列人工处理:
// 声明队列时绑定死信交换机\\nargs.put(\\"x-dead-letter-exchange\\", \\"dlx.exchange\\");\\n
\\n重复消费的解决方案:
\\nversion
字段控制。示例代码:
\\nUPDATE orders SET status = \'paid\' \\nWHERE id = #{orderId} AND status = \'unpaid\';\\n
\\n通过生产者确认+持久化+手动ACK+幂等性的组合拳,可最大限度保障消息可靠性。实际场景中需根据业务容忍度平衡性能与可靠性,例如:
\\n在流量日益增长的今天,随着用户需求的不断增加和性能要求的提升,一个能够更好地处理高并发、低延迟和资源有效利用的计算层是十分重要的。尽管在过去我们平台使用Java开发的计算层提供了稳定的服务支撑,但面对日益增长的流量和低延迟的需求,Java不可避免地开始显现局限性:
\\n在此背景下,经过调研和实验验证,我们发现了Rust这个计算层改造升级的语言选型。Rust语言以其出色的内存管理、安全性和高效性能而闻名。Rust的所有权模型可以在编译时捕捉大多数内存错误,从而减少运行时错误,这对需要高可靠性和稳定性的系统尤为重要。此外,Rust没有垃圾回收机制,这意味着我们可以更好地预测和控制内存使用,提高应用程序的性能和资源利用率。
\\n通过使用Rust对计算层改造升级,我们的系统获得了如下的提升:
\\nRust 能够突破传统编程语言的瓶颈,主要得益于其独特的所有权、借用和生命周期机制。这些特性使 Rust 在编译阶段就能够确保内存安全和线程安全,从而最大程度地减少运行时错误和不确定性。接下来,我们将深入探讨 Rust 在并发模型、所有权、生命周期和借用方面的优势。
\\nRust 的所有权(Ownership)是该语言独特的内存管理机制,它确保内存安全性和并发性而不需要垃圾回收器。所有权机制通过编译时检查来保证安全性,避免绝大多数的运行时错误,例如空指针或数据竞争。
\\nRust的所有权有三个主要规则:
\\n为了方便理解,这里展示Rust、C++和Java对象赋值的异同来理解所有权的运行机制。\\n
可以看到,将a赋值给b时,Java会将a指向的值的引用传递给b,而C++则会产生一个新的副本。从某种意义来说,在内存管理上,Java和C++选择了相反的权衡。代价是Java需要垃圾回收来管理内存,而C++的赋值会消耗更多的内存。不同于Java和C++,Rust选择了另一种方案:移动所有权。即将a指向的堆内存地址“移动到b上”,这时只有b可以访问这段内存,a则成为了未初始化状态并禁止使用。
\\nRust的所有权概念内置于语言本身,在编译期间对所有权和借用规则进行检查。这样,程序员可以在运行之前解决错误,提高代码的可靠性。
\\n尽管Rust规定大多数值会有唯一的拥有者,但在某些情况下,我们很难为每个值都找到具有所需生命周期的单个拥有者,而是希望某个值在每个拥有者使用完后就自动释放。简单来说,就是可以在代码的不同地方拥有某个值的所有权,所有地方都使用完这个值后,会自动释放内存。对于这种情况,Rust提供了引用计数智能指针:Rc和Arc。
\\nRc和Arc非常相似,唯一的区别是Arc可以在多线程环境进行共享,代价是引入原子操作后带来的性能损耗。Rc和Arc实现共享所有权的原理是,Rc和Arc内部包含实际存储的数据T和引用计数,当使用clone时不会复制存储的数据,而是创建另一个指向它的引用并增加引用计数。当一个Rc或Arc离开作用域,引用计数会减一,如果引用计数归零,则数据T会被释放。这种机制也叫共享所有权机制。
\\n\\n这时就有好奇的小伙伴问了,既然可以在多个地方共享所有权,那不是违背了所有权的初衷,从而引入了数据竞争的问题?放心,Rust的开发者早就想到了这个问题,引用计数智能指针是内部不可变的,即无法对共享的值进行修改。那这就又引入了一个问题:如果要对共享的值进行修改怎么办?对于这种情况Rust也提供了解决方案,使用Mutex等同步原语即可避免数据竞争和未定义行为。以下是一个案例,如何在多线程访问数据,并安全的进行修改。
{\\n let counter = Arc::new(Mutex::new(0));\\n let mut handles = vec![];\\n\\n for _ in 0..10 {\\n let counter_clone = Arc::clone(&counter);\\n let handle = thread::spawn(move || {\\n // 锁定 Mutex 以安全地访问数据\\n let mut num = counter_clone.lock().unwrap();\\n *num += 1; // 修改数据\\n });\\n handles.push(handle);\\n }\\n\\n // 等待所有线程完成\\n for handle in handles {\\n handle.join().unwrap();\\n }\\n\\n // 获取最终计数值\\n println!(\\"Final count: {}\\", *counter.lock().unwrap());\\n}\\n\\n
\\n在 Rust 中,生命周期(lifetimes)和引用(references)是两个密切相关的概念,它们共同构成了 Rust 的所有权系统的重要组成部分。生命周期用于确保引用在使用时是有效的,从而防止悬空引用和数据竞争等问题。
\\n前面提到,Rust值的所有权可以被借用,它允许在不获取数据所有权的情况下访问数据。Rust中有两种类型的引用:
\\n在使用引用的时候需要满足以下规则:
\\n在 Rust 编程语言中,生命周期用于确保引用在使用时是有效的。生命周期的存在使得 Rust 能够在编译时检查引用的有效性,从而防止悬空引用。如下是一个Rust编译器检查生命周期的例子:
\\nfn main() {\\n let r; // ---------+-- \'a\\n // |\\n { // |\\n let x = 5; // -+-- \'b |\\n r = &x; // | |\\n } // -+ |\\n // |\\n println!(\\"r: {r}\\"); // |\\n}\\n
\\n这里编译器将r的生命周期记为\'a,x的生命周期记为\'b。可以明显看出,内部块的\'b比外部块的\'a生命周期小,当x离开作用域被释放时,r仍然持有x的引用。所以当把生命周期为\'a的r想引用生命周期为\'b的x时,编译器发现了这个问题,并拒绝通过编译,保证了程序不会出现悬垂引用。
\\n正如我们看到的,Rust的引用代表对值的一次借用,它们有着种种限制,所以,在函数中、在结构体中等等位置上使用引用时,你都要给Rust编译器一些关于引用的提示,这种提示,就是生命周期标记。对于简单的情况,聪明的Rust编译器可以自动推断出引用的生命周期。对于一些模棱两可的情况,编译器也无法推断引用是否在程序运行期间始终有效,这时就需要我们提供生命周期标注来提示编译器我们的代码是正确的,放我过去吧。
\\n生命周期标注并没有改变传入的值和返回的值的生命周期,我们只是向借用检查器指出了一些用于检查非法调用的一些约束而已,而借用检查器并不需要知道 x、y 的具体存活时长。而事实上如果函数引用外部的变量,那么单靠 Rust 确定函数和返回值的生命周期几乎是不可能的事情。因为函数传递什么参数都是我们决定的,这样的话函数在每次调用时使用的生命周期都可能发生变化,正因如此我们才需要手动对生命周期进行标注。
\\n相信第一次看到生命周期的小伙伴们都感觉概念非常难理解,且写出的代码非常丑,简直要逼死强迫症。但是有得就有舍,要写出安全且高效的Rust代码,就要学会理解和使用生命周期。如果实在不想用,那就多用Rc和Arc吧。
\\n了解了Rust最核心的基本知识和特性后,你已经成为了一个合格的Rust练习生,可以开始用Rust愉快的进行开发工作了。但是要使用Rust开发高性能的生产级应用,只了解到这种程序是不行的。当初笔者信心满满地将第一个Rust应用发布到测试环境后,竟然发现效率比Java版本还低,于是开始了长期的瓶颈排查和调优,且调优时间远大于编码时间。最终我们的应用在相同吞吐量的条件下,CPU使用率从高于Java 20%优化到低于Java 40%。在这个过程中,也总结了一些经验进行分享。
\\n相信很多刚接触Rust的小伙伴在面对同一份数据需要在多处使用的情况时,为了逃避复杂的生命周期问题,会倾向于使用Clone来创建数据副本。如果这样做的话,一份数据在内存中重复出现多次,带来的cpu和内存消耗会让你会怀疑人生,为什么这么相信Rust的性能而不相信自己能啃下生命周期这块硬骨头呢?
\\n有一个应用场景,我们从数据源得到若干个源数据,根据业务逻辑聚合成batch并存储到远端或者本地。聚合的逻辑可以有两种方式:
\\n然而这两种方式都不可取。第一种方式的问题是,我们不知道一份源数据是不是只会被使用一次。而使用第二种方式则会消耗更多的CPU,且占用内存成倍上升。
\\n前面提到,Rust的值是可以借用的,如果在batch中不获得所有权,而是存储引用,那么可以几乎零消耗的实现需求。以上述应用场景为例,这里介绍我们是怎么解决这个问题的。
\\n首先给出源数据Data和Batch的定义:
\\nstruct Data {\\n condition: bool,\\n num: i32,\\n msg: String\\n}\\n
\\nstruct Batch<\'a> {\\n msgList: Vec<&\'a str>\\n}\\n
\\n假设需求是将Data的msg字段在Batch里存储num次,我们很容易写出这样的代码:
\\nfn main() {\\n let batch: Batch = Batch:new(); // 初始化Batch\\n loop { \\n let data:Data = dataSource.getData(); // 从数据源获得data\\n recordData(batch, &data);\\n if (batch.len() > 100) { // batch存储的数据大于100条时,存储并清空\\n save(batch);\\n batch.clear();\\n } // ------------------- data的生命周期到此结束\\n } // ------------------- batch的生命周期到此结束\\n}\\n\\nfn record_data(batch: Batch, data: Data) {\\n if(condition) { // 根据条件将msg保存num次\\n for i in 0..data.num { \\n batch.msgList.push(&data.msg);\\n }\\n }\\n}\\n
\\n看起来是不是很合理,和其他语言也没有什么区别,当信心满满按下编译后,会发现天空飘来五个字:编译不通过。原因很简单,因为编译器发现被引用对象data的生命周期小于batch,data的在当前循环结束后就会销毁,batch存储的引用就变成了野指针。我们可以做如下修改:
\\nfn() {\\n let batch: Batch = Batch:new(); // 初始化Batch\\n let dataList: Vec<Data> = Vec::new(); // dataList的生命周期和batch一样\\n loop { \\n let data: Data = dataSource.getData(); // 从数据源获得data\\n dataList.push(data); // 将data保存在dataList,提升生命周期\\n if(batch.len() > 100) {\\n for data_ref: &Batch in dataList.iter() {\\n record_data(batch, data_ref); // 此时data的生命周期和batch相等\\n }\\n save(batch);\\n batch.clear();\\n dataList.clear();\\n }\\n }\\n}\\n\\nfn record_data<\'a>(batch: Batch<\'a>, data: &\'a Data) {\\n if(condition) { // 根据条件将msg保存num次\\n for i in 0..data.num { \\n batch.msgList.push(&data.msg);\\n }\\n }\\n}\\n
\\n可以看到,我们对代码做了一些小改动:
\\n为什么这么做呢?我们已经知道最初版本是因为data的生命周期小于batch,导致batch不能存储data的引用。解决这个问题的思路很简单,提升data的生命周期不就完了。假设batch的生命周期是\'a,data的生命周期是\'b,很明显\'a是大于\'b的,因为batch的生命周期是整个main函数,而data的生命周期仅仅在loop内。我们在batch同样的作用域内定义一个容器,它的生命周期也是\'a。在每次得到data后把它存入容器中,那data就不会在循环结束的时候被销毁了。
\\n同时,在record_data函数定义上,我们也要使用标注告诉编译器batch和data的生命周期是相等的。如果data的生命周期大于batch,我们也可以在参数中定义data的生命周期为\'a,因为实际的生命周期和参数生命周期标注无需一致,只需要实际的生命周期大于参数生命周期就行了。如果你有强迫症,也可以在参数中标注实际的生命周期,只需要加上适当的生命周期约束就行了:
\\n// \'b: \'a表示\'b的生命周期能够覆盖\'a\\nfn record_data<\'a>(batch: Batch<\'a>, data: &\'b Data) where \'b: \'a {\\n ......\\n}\\n
\\n经过这些小改动,你的应用会比粗暴的使用拷贝提升许多性能并且节约大量内存使用。经过我们的测试,在类似需求中将需要大量拷贝的操作替换成引用,可以节省一倍的内存,CPU使用率也下降了20%。
\\n在一些情况下,我们项目使用的编程语言在实现一些功能时,想使用现成的依赖库来实现复杂的逻辑,但是因为生态不完善,导致缺少此类库或者现存的依赖库不成熟。在使用Rust时,这种现象尤其普遍。很多热门组件没有为Rust提供官方API,非官方实现功能和性能又得不到保证,且更新不稳定。难道Rust进阶之路就要到此为止?
\\nRust很贴心地提供了跨语言交互能力,对FFI的良好支持可以让开发者方便的在Rust代码中调用C程序。如果我们需要的依赖库刚好有C/C++的实现,就能使Rust完成主要逻辑,把一些Rust不完善的功能通过C/C++实现,而且性能也不会受到影响。在Rust程序调用C代码也非常简单:
\\nextern \\"C\\" {\\n fn c_add(a: i32, b: i32) -> i32;\\n}\\n
\\nfn main() {\\n unsafe {\\n c_add(1, 2); \\n }\\n}\\n
\\ng++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp\\n
\\nrustc main.rs hello.o\\n
\\n尽管用Rust调用C程序已经非常方便,但是仍需要注意这些问题:
\\n如果你想构建一个高性能的Rust服务器应用,那么Tokio绝对是你的首选框架。Tokio 是一个用 Rust 编写的异步运行时,旨在提供高性能的 I/O、任务调度和并发支持。虽说Tokio提供了强大的异步支持,要用好Tokio也不是一件容易得事,首先要了解“异步”的概念。在计算机编程中,“异步”是指一种不阻塞的操作方式,允许程序在等待某些操作(如 I/O 操作、网络请求等)完成时继续执行其他代码。
\\nTokio 通过使用协程和 Future 机制来实现高效的并发处理。它将异步任务封装为Future对象,并通过运行时的调度器管理这些任务的执行状态。当任务被调用时,运行时通过poll方法检查其状态,如果任务无法继续执行(返回 Poll::Pending),则将其挂起并注册一个Waker来在后续的某个时刻唤醒任务。一旦相关的I/O操作完成,Waker会通知运行时重新调度该任务,从而实现非阻塞的并发执行。Tokio支持多线程运行,可以充分利用多核CPU的能力,提高应用程序的性能和响应性。
\\n\\nTokio的使用非常简单,使用async和await就可以很方便地创建异步任务,但是要使用Tokio写出高性能的代码不是一件简单的事。刚刚接触Tokio的开发者会经常发现代码无故卡死或者性能低下,这是因为没有正确使用Tokio。举个例子,下面是一段运行后会卡死的代码:
#[tokio::main(flavor = \\"multi_thread\\", worker_threads = 8)]\\nasync fn main() {\\n let h = tokio::spawn(async {\\n let (tx, rx) = std::sync::mpsc::channel::<String>();\\n tokio::spawn(async move{\\n let _ = tx.send(\\"send message\\".to_string());\\n });\\n let ret = rx.recv().unwrap();\\n println!(\\"{}\\", ret)\\n });\\n h.await;\\n}\\n
\\n代码结构很简单,但是运行后会发现代码似乎hang住了,检查代码结构也没有发现问题。要解释这个卡死的问题,要从Tokio的任务调度机制来分析:
\\n\\nProcessor 获取 Task 后,会开始执行这个 Task,在 Task 执行过程中,可能会产生很多新的 Task,第一个新 Task 会被放到 LIFO Slot 中,其他新 Task 会被放到 Local Run Queue 中,因为 Local Run Queue 的大小是固定的,如果它满了,剩余的 Task 会被放到 Global Queue 中。
Processor 运行完当前 task 后,会尝试按照以下顺序获取新的 Task 并继续运行:
\\n如果 Processor 获取不到 task 了,那么其对应的线程就会休眠,等待下次唤醒。
\\n在上面的例子中,我们首先Spawn了一个异步任务Task-1,Task-1被分配给了Processor-1执行。然后在Task-1里Spawn了另一个异步任务Task-2,Task-2被放到了Processor-1的LIFO Slot中。
\\n因为Task-1继续运行的条件依赖于Task-2,所以Task-1被阻塞了。而且Tokio的协程是非抢占式的,在Task-1没有遇到.await前无法让出CPU,Processor-1无法去执行Task-2。又因为Task-2在Processor-1的LIFO Slot中,其他的Processor也无法偷取Task-2执行。于是,Task-2永远也不会有机会被执行,这两个Task在循环等待中就永远卡死了。
\\n要解决这个问题,我们要将阻塞型的数据结构替换成Tokio的非阻塞式的:
\\n#[tokio::main(flavor = \\"multi_thread\\", worker_threads = 8)]\\nasync fn main() {\\n let handler = tokio::spawn(async {\\n let (tx, mut rx) = tokio::sync::mpsc::channel(2);\\n tokio::spawn(async move{\\n let _ = tx.send(\\"send message\\".to_string()).await;\\n });\\n let ret = rx.recv().await.unwrap();\\n println!(\\"{}\\", ret)\\n });\\n handler.await;\\n}\\n
\\n将channel替换成Tokio的非阻塞数据结构后,Task-1在提交完Task-2后遇到await让出了CPU,Processor-1就可以从LIFO Slot取出Task-2执行了,循环等待也就被打破了。
\\n由这个例子可以看出,Tokio 的轻量级线程之间的关系是一种合作式的。合作式的意思就是同一个 CPU 核上的任务大家是配合着执行(不同 CPU 核上的任务是并行执行的)。我们可以设想一个简单的场景,A 和 B 两个任务被分配到了同一个 CPU 核上,A 先执行,那么,只有在 A 异步代码中碰到 .await 而且不能立即得到返回值的时候,才会触发挂起,进而切换到任务 B 执行。也就是说,在一个 task 没有遇到 .await 之前,它是不会主动交出这个 CPU 核的,其他 task 也不能主动来抢占这个 CPU 核。
\\n所以在使用Tokio时,我们要注意两点:
\\n通过 Cargo,开发者可以轻松创建、构建和共享 Rust 项目。但是因为发布系统只支持Java和Golang应用,要在发布系统发布Rust应用还是需要一些工作的。以下是我们发布Rust应用的流程。
\\n因为公司平台是没有Rust应用的,所以我们需要自己制作镜像并上传,这样才能在发布平台发布我们的代码。我们需要创建两个 Docker 镜像:一个用于构建(CI 镜像),另一个用于运行(运行时镜像)。
\\n\\n在dockerfile里可以安装自己想要的工具包,根据自己需求来定制。
FROM repoin.shizhuang-inc.net/ci-build/rust:1.79.0\\n\\nRUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32\\nRUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 871920D1991BC93C\\n\\n# 创建 /etc/apt/sources.list\\nRUN echo \\"deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse\\" > /etc/apt/sources.list && \\\\\\n echo \\"deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse\\" >> /etc/apt/sources.list && \\\\\\n echo \\"deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse\\" >> /etc/apt/sources.list && \\\\\\n echo \\"deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse\\" >> /etc/apt/sources.list && \\\\\\n echo \\"deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse\\" >> /etc/apt/sources.list\\n\\n# 更新包列表并安装必要的工具\\nRUN apt-get install -y \\\\\\n protobuf-compiler \\\\\\n && apt-get clean \\\\\\n && rm -rf /var/lib/apt/lists/*\\n\\n# 验证安装\\nRUN protoc --version\\n\\nRUN pwd\\n\\nRUN ls -alh .\\n\\nRUN ls -alh workspace\\n
\\n建好集群后,还需要对集群进行一些配置:
\\n还需要注意的是,发布平台的编译环境和运行环境是不同的,编译完成后发布平台会将可执行文件移动到/opt/apps目录下进行执行,而配置文件不会被打包。遇到这种情况可以使用rust-embed库,它允许将静态文件(如 Yaml、Json、图像等)打包到您的二进制文件中,从而简化文件管理和部署。
\\n虽说Rust应用主打的是稳定,但是发布后持续对应用进行监控也是必须的,不然晚上能睡得着吗。和发布一样,Rust应埋的指标要被监控采集,需要额外的配置。在KubeOne平台找到自己的集群,在发布配置里加上这两项,监控平台就可以采集到指标了。
\\nlabels:\\n - key: http://dewu.com/qos\\n value: LS\\n - key: http://duapp.kubernetes.io/metrics-scraped\\n value: metrics\\ncontainerPorts:\\n - containerPort: \\"2892\\"\\n name: http-metrics\\n protocol: TCP\\n
\\n通过上监控,可以实时观察Rust服务的运行情况,并且根据自己的埋点分析系统的瓶颈。可以看到,Rust应用运行非常平稳。相比于有GC的Java应用,Rust明显毛刺很少,非常平滑,而且内存占用相比Java减少了70%。
\\n通过迁移到Rust,我们的计算层能够在处理高并发请求时显著提高系统的吞吐量和响应能力,同时减少服务器资源的浪费。这不仅能降低运营成本,还能为我们的用户提供更流畅、更快速的体验。
\\n但是,如果要持续地拥抱Rust生态,目前仍然面临如下挑战:
\\n1. 生态不完善\\n尽管 Rust 已经有一些非常优秀的库和工具,但某些特定领域仍然缺乏成熟且广泛使用的库。这意味着开发者可能需要花费更多的时间来构建自己的解决方案或者整合不同语言的库。
\\n2. 学习曲线陡峭\\nRust 语言引入了许多独特的概念和特性,对于初学者和来自其他语言的开发者来说,这些特性可能需要一段时间来彻底掌握。
\\n3. 开发进度\\n相比于自动内存管理类型语言的开发任务,Rust严格的编译检查会让开发进度一度阻塞。
\\n尽管开发Rust生产级应用有那么多阻碍,我们目前已经发布的Rust应用已经证明了,相比于付出,迁移Rust带来的收益更大。希望大家都可以探索Rust的可行性,为节能减排和世界和平出一份力,也欢迎各位对Rust有兴趣的同学一起交流。
\\n文 / 小新
\\n关注得物技术,每周更新技术干货
\\n要是觉得文章对你有帮助的话,欢迎评论转发点赞~
\\n未经得物技术许可严禁转载,否则依法追究法律责任。
","description":"一、引 言 在流量日益增长的今天,随着用户需求的不断增加和性能要求的提升,一个能够更好地处理高并发、低延迟和资源有效利用的计算层是十分重要的。尽管在过去我们平台使用Java开发的计算层提供了稳定的服务支撑,但面对日益增长的流量和低延迟的需求,Java不可避免地开始显现局限性:\\n\\n垃圾回收:Java 的自动内存管理依赖于垃圾回收机制,而垃圾回收虽然简化了开发工作,却可能引入不可预测的延迟。\\n内存使用效率:Java 的内存管理通常比手动管理的语言消耗更多的内存,因为它必须保留足够的空间来处理对象分配和回收。\\n异步处理瓶颈:虽然Java近年来强化了异步编程支持…","guid":"https://juejin.cn/post/7475693874511872063","author":"得物技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-27T05:56:02.604Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b8e76c2633de4268924b044a730bd133~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=Pm2N2NXVymAQlP7Rfyp6QXN0uZo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b7d7eb8259cf4b98a25dd17241681c06~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=ztExBqFgMFf87qMdvYEdDxM0P%2Fo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/68fa69b28065425e83337a6b9a9169fb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=cDFz3JGhO%2FNNQNnJ9hFUCKa%2FP2w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e87cf53750914f12b7ccd14ffbb0d317~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=R5GcYZOFJ4DKF4tLsNy5QP96MVQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/887b523ad97844dbb98b30269b555cb1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=UCAHfVwTqS7zzONEr2G4QfHucfA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ebf17881890a45ef818128b5c2cda6f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b6X54mp5oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741240562&x-signature=taa%2ByVxFI1L1gU2YZg35Kqsq5Es%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Rust"],"attachments":null,"extra":null,"language":null},{"title":"分布式事务一张图-最全论述","url":"https://juejin.cn/post/7475715128681512997","content":"# 领导要求下班前画一张图把分布式事务问题说清楚 本文主要是对上篇架构图详细展开说明
\\n架构图如下:
\\n分布式事务,这四个字看着好像挺高大上,很多程序员一听到,心里头都会发怵。\\n但其实,它的本质问题一点都不新鲜,其实就是本地事务的分布式版本而已
\\n想要了解分布式事务,一定要先了解什么是本地事务
\\n大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示
\\n\\n在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。代码如下所示:
Connection conn = ... //获取数据库连接\\nconn.setAutoCommit(false); //开启事务\\ntry{\\n \\n //...执行增删改查sql\\n conn.commit(); //提交事务\\n}catch (Exception e) {\\n \\n conn.rollback();//事务回滚\\n}finally{\\n \\n conn.close();//关闭链接\\n}\\n
\\n在微服务架构中,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据操作,要么全部成功,要么全部失败。
\\n本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性
\\n用个简单的例子解释就明白了:
\\n\\n\\n假设你在一个连锁超市工作,顾客A在北京买了瓶水,然后又跑到上海买了个面包。问题来了——两个城市的收银系统要同步:账上得记着人家消费了两样东西,库存也得扣掉。可是呢,北京和上海的系统数据经常对不上,要么少算,要么多算。这种跨地域、跨系统的协调问题就是分布式事务的“职场原型”。
\\n
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。
\\n如,对于sql:
insert into user(id,name) values (1,\\"张三\\"),(2,\\"李四\\")\\n
\\n这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。\\n但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。\\n所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败。\\n因此基本上所有的数据库中间件都面临着分布式事务的问题。
\\nService A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C。
\\n而Service B又同时操作了2个数据库,Service C也操作了一个库。
\\n需要保证这些跨服务调用对多个数据库的操作要么都成功,要么都失败,实际上这可能是最典型的分布式事务场景
小结:
\\n上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
\\n总结一句就是: 分布式事务的难点,就在于“协调难、压力大、两难选择”
\\n分布式系统就是把一个整体的“大锅饭”拆成了好几口“小锅饭”。表面上看挺合理:每个锅分开煮,互不影响,效率更高。可问题是,这些小锅饭总得对上账啊!如果锅A说“饭好了”,锅B却喊“我这儿米还没下锅呢”,那顾客还不得砸你店?
\\n别以为分布式事务只是少数服务的偶尔尴尬。想想双十一凌晨,几亿人同时下单、支付、抢红包,服务器压力能飙到天上去。这种时候,一旦某个服务崩了,就好比超市里的收银机瘫了——所有人卡在付款的队伍里,退也退不了,买也买不成,直接“炸场”。
\\n有人会说,那就搞分布式事务方案呗!理论上没问题,实践却很迷幻——
\\n吐槽一句: 很多时候,解决方案和问题本身一样复杂。你以为搞个分布式事务是“抡锤子砸钉子”,结果发现需要把钉子从水泥墙里拔出来再重新种棵树。
\\n概述: 两阶段提交方案下全局事务的ACID特性,是依赖于RM的。\\n一个全局事务内部包含了多个独立的事务分支,这一组事务分支要么都成功,要么都失败。\\n各个事务分支的ACID特性共同构成了全局事务的ACID特性。也就是将单个事务分支支持的ACID特性提升一个层次到分布式事务的范畴
\\nTM(事务管理器)通知各个RM(资源管理器)准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。
\\n\\n\\n以mysql数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare\\"准备提交\\"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成\\"可以提交\\",然后把结果返回给事务管理器。
\\n
TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。
\\n\\n\\n以mysql数据库为例,如果第一阶段中所有数据库都prepare成功,那么事务管理器向数据库服务器发出\\"确认提交\\"请求,数据库服务器把事务的\\"可以提交\\"状态改为\\"提交完成\\"状态,然后返回应答。如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把\\"可以提交\\"的事务回撤。
\\n
\\n\\n2PC 中的参与者是阻塞的。在第一阶段收到请求后就会预先锁定资源,一直到 commit 后才会释放。
\\n
\\n\\n由于协调者的重要性,一旦协调者TM发生故障,参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
\\n
\\n\\n若协调者第二阶段发送提交请求时崩溃,可能部分参与者收到commit请求提交了事务,而另一部分参与者未收到commit请求而放弃事务,从而造成数据不一致的问题。
\\n
Seata是什么?
\\n用官方的话说就是:
\\nSeata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
通俗的解释就是:
\\nSeata 就是帮你管理多个服务干活的“事务管家”。它有两套大杀器:AT 模式和 TCC 模式。\\n别被这些字母吓到,它们干的活,说白了就像处理公司财务报销
假设你是个项目经理,手下人天天报销,各种发票堆成山。AT 模式就像一个智能财务系统,员工只管上传发票,它会帮你自动算钱、报账、审核。如果哪步出了错,比如发票无效,它还能帮你把钱退回来。
\\nSeata AT模式的核心是对业务无侵入,是一种改进后的两阶段 2PC 提交,其设计思路如下:
\\n一句话总结 优点与缺点
\\n一阶段:
\\n业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?
\\n二阶段:
\\nTCC 是个强管控的解决方案。假设你要给一百个人发奖金,每个人分三步:确定奖金池、发放奖金、确认到账。TCC 模式就要求每一步都明确标记:钱从哪里扣?到账了没有?出了问题咋办?
\\nTCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂。
\\n但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。
\\n一句话总结 优缺点:
\\n以用户下单为例
\\n理论上,Seata 是为高并发场景设计的,但真到了“双十一”,可能还是会让你“笑着进去哭着出来”。为什么?
\\n一句话总结:Seata 是个好帮手,但它也不是万能的,适合中小型分布式事务场景。要是硬扛高QPS,那得看你整个架构链条行不行了。
\\n\\n\\n那要抗住高 QPS,比如电商网站,该如何做呢?
\\n
CAP 定理,是分布式系统里的“铁律”,逃也逃不掉。说人话就是:“你开个跨国公司,员工遍布全球,信息还能实时同步,但要是断了网呢?老板、员工和客户能忍谁?”
\\nCAP定理的三个选项:
\\n问题来了,这三者只能选两个。 “鱼和熊掌不可兼得” ,这事儿不是耍流氓,是真有物理限制。
\\n假设你是个外卖平台的技术负责人:
\\n总结: 分布式事务里,选择 CAP 的哪两项,完全取决于你的业务目标。想强一致性?系统性能会变成“龟速”;要高可用?一致性可能得做点妥协。
\\nCAP 定理虽然是理论,但落地到工程实践里,它就成了“妥协的艺术”。分布式事务在 CAP 中的表现无非以下几种:
\\n两阶段提交(2PC):强一致性优先
\\n最终一致性:让步换效率
\\n无事务化(BASE 模型):高并发优先
\\n总结一句: CAP 是个选择题,分布式事务让你不断在“一致性、性能、容错”之间拉扯。没完美答案,只有业务需求决定选哪两个。
\\n它的精髓就在于“账实分离”:数据一份一份对,不急着实时对账,但最后一定能对上。\\n这种设计有点像你先把“工作总结”写进草稿箱,等到老板要时再发邮件,慢是慢了点,但总不会搞错。
\\n问题来了:如果库存更新成功了,但物流通知没发出去,系统就变成“订单对外显示成功,仓库和快递却全懵逼”。这就是典型的分布式事务问题。
\\n主数据库先写一条本地消息,表示“我已经扣了库存”;然后,异步通知物流发货。这种模式可以让两边数据最终对得上。
\\n实现步骤:
\\n业务对账逻辑详解:
\\n本地消息表是“人肉对账逻辑”的机器化替代方案。系统会不断检查消息表和目标系统之间的状态,如果发现对不上,就自动重试或者报警。就像老板每天追着财务问:“对账单都核对了吗?”
优点:可靠又简单
\\n缺点:
\\n用消息队列代替数据库表。RocketMQ 本质上就是一个高效的“快递员”,它能帮你存储、分发事务消息,确保消息的可靠投递,同时还提供“事务消息”这种强一致性的增强版功能。
\\n总结一句话:
\\n本地消息表方案是分布式事务的启蒙老师,简单实用但有点老派;RocketMQ 是它的继任者,不仅解决了性能问题,还给了你更多工具去搞定事务一致性。这两者的选择,取决于你的业务规模和技术需求。\\n下一步,我们看看如何把这套逻辑玩到10万QPS级别
\\n核心思路:分区处理,降低竞争
\\n高并发场景下,消息的生产端是第一个“压力山大”的环节。为了不让事务消息的发送变成瓶颈,可以考虑以下优化:
主题分区(Message Sharding):
\\n异步发送:
\\n核心思路:批量消费,提高吞吐
\\n消费端是事务消息处理的最后一环,优化得好坏直接影响系统的处理效率:
批量拉取消息(Batch Consumption):
\\n分治处理逻辑:
\\n延迟消息
\\nRocketMQ 的延迟消息机制,可以用于减缓高峰压力或实现定时对账。例如,秒杀活动中,不用每秒检查库存状态,而是通过延迟触发来减少数据库压力。
案例: 设置10秒延迟的消息,定时检查订单支付状态,未完成的直接取消。
\\n幂等机制: 避免重复处理
\\n消息的重复消费是高并发场景的常见问题,可以通过在消费逻辑中引入幂等校验解决。
\\n实现方式:
\\n消息堆积与降级策略
\\n动态扩容:
\\nRocketMQ 的事务消息在高并发场景下不是“开箱即用就完事”,需要结合业务场景进行生产端分区、消费端批量处理、幂等校验、延迟消息等多维度优化。
\\n最终,你会发现事务消息不仅仅是一个“解决一致性”的工具,它还能成为高并发分布式系统里的“压舱石”。接下来,我们进入总结,看看分布式事务的全景方案。
搞分布式事务,就像盖一栋“永不倒塌的摩天楼”。从 Seata 的 AT/TCC 模式到 RocketMQ 的事务消息,再到本地消息表,这些方案就像建材库里的各种砖块和钢筋,每种都有优缺点,关键在于如何搭配使用,才能撑起你的业务需求。\\n以下是对分布式事务的全景回顾和总结,帮你从技术选型到工程实践,找到最适合的路线。
\\n方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Seata AT 模式 | 自动化高,业务侵入少 | 性能一般,不适合复杂分布式场景 | 单数据库事务,中小型业务 |
Seata TCC 模式 | 控制精细,能满足复杂业务 | 开发量大,侵入性强 | 金融、库存等高一致性要求场景 |
本地消息表方案 | 实现简单,稳定可靠 | 性能瓶颈,消息数据易爆炸 | 中低流量分布式事务 |
RocketMQ 事务消息 | 性能高,扩展性强,能适应高并发 | 配置和优化成本较高 | 高并发、对性能要求高的分布式场景 |
最终一致性(BASE) | 性能极高,不锁资源 | 需要容忍短时间数据不一致 | 秒杀、电商支付等对一致性容忍度高的场景 |
1. 业务需求为王
\\n2. 规模决定复杂度
\\n3. 技术团队能力
\\n如果你的系统目标是支撑10万QPS级别的流量,以下是几条必须遵循的原则:
\\n1. 数据分区,业务拆分
\\n把复杂的分布式事务拆成多个独立子系统,用事件驱动的方式减少事务依赖。比如订单、支付、库存分别独立,靠消息队列进行数据同步。
2. 异步化与最终一致性
\\n不要追求所有流程实时完成,能用异步解决的尽量异步。比如支付完成后,延迟5分钟对账。
3. 动态扩展与限流保护
\\n随着云原生架构和事件驱动设计的普及,分布式事务的形态也在不断演化。未来可能出现更多“去中心化”模式,比如通过区块链技术实现全网的事务一致性,或者利用 AI 优化分布式事务的回查和补偿逻辑。
\\n分布式事务没有万能方案,只有适合业务的“最佳搭配”。从理论到实践,从本地消息表到 RocketMQ,真正的架构高手不是看用什么工具,而是看如何将它们用到极致。最后,记住分布式事务的一条“真理”
\\n\\n\\n作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家!
\\n
XPipe是一款全新的终端管理工具,具有强大的文件管理功能,目前在Github上已有4.8k+Star
。它可以基于你本地安装的命令行工具(例如PowerShell)来执行远程命令,反应速度非常快。如果你有使用 ssh、docker、kubectl 等命令行工具来管理服务器的需求,使用它就可以了。
XPipe具有如下特性:
\\n下面是XPipe使用过程中的截图,界面还是挺炫酷的!
\\n\\n\\n这或许是一个对你有用的开源项目,mall项目是一套基于
\\nSpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!\\n
\\n- Boot项目:github.com/macrozheng/…
\\n- Cloud项目:github.com/macrozheng/…
\\n- 教程网站:www.macrozheng.com
\\n项目演示:\\n
\\n
Portable
版本,解压即可使用,地址:github.com/xpipe-io/xp…xpiped.exe
即可使用;中文
,然后设置下主题,个人比较喜欢黑色主题;添加预定义身份
;Linux-local
这个连接,就可以通过本地命令行工具来管理Linux服务器了;文件浏览器
按钮可以直接管理远程服务器上的文件,非常方便;所有脚本
功能中,可以存储我们的可重用脚本;所有身份
中存储着我们的账号密码,之前创建的Linux root账户在这里可以进行修改。今天给大家分享了一款好用的终端工具XPipe,界面炫酷功能强大,它的文件管理功能确实惊艳到我了。而且它可以用本地命令行工具来执行SSH命令,对比一些套壳的跨平台终端工具,反应速度还是非常快的!
\\n不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。
\\n在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
\\nJava泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。
// 非泛型写法(存在类型转换风险)\\n List list1 = new ArrayList();\\n list1.add(\\"a\\");\\n Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException\\n\\n // 泛型写法(编译时检查类型)\\n List<String> list2 = new ArrayList<>();\\n // list.add(1); // 编译报错\\n list2.add(\\"a\\");\\n String str = list2.get(0); // 无需强制转换 \\n
\\n// 非泛型写法\\nMap map1 = new HashMap();\\nmap1.put(\\"user\\", new User());\\nUser user1 = (User) map1.get(\\"user\\");\\n\\n// 泛型写法\\nMap<String, User> map2 = new HashMap<>();\\nmap2.put(\\"user\\", new User());\\n// 自动转换\\nUser user2 = map2.get(\\"user\\");\\n
\\n3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。
\\n@Data\\n@NoArgsConstructor\\n@AllArgsConstructor\\npublic class Result<T> {\\n /**\\n * 响应状态码\\n */\\n private int code;\\n\\n /**\\n * 响应信息\\n */\\n private String message;\\n\\n /**\\n * 响应数据\\n */\\n private T data;\\n\\n /**\\n * 时间戳\\n */\\n private long timestamp;\\n 其他代码省略...\\n
\\nList<String> list = new ArrayList<>();\\n
\\n我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。
\\npubile class A<T>{\\n prvate T t;\\n //其他省略...\\n}\\n\\n//创建一个不带泛型参数的A\\n A a = new A();\\n a.set(new B());\\n B b = (B) a.get();//需要进行强制类型转换\\n\\n//创建一个带泛型参数的A\\n A<B> a = new A<B>();\\n a.set(new B());\\n B b = a.get();\\n
\\nList<E> list = new ArrayList<>();\\n
\\nMap<K,V> map = new HashMap<>();\\n
\\nMap<K,V> map = new HashMap<>();\\n
\\n // 使用无界通配符处理任意类型的查询结果\\n public void logQueryResult(List<?> resultList) {\\n resultList.forEach(obj -> log.info(\\"Result: {}\\", obj));\\n }\\n
\\n // 使用上界通配符读取缓存\\n public <T extends Serializable> T getCache(String key, Class<T> clazz) {\\n Object value = redisTemplate.opsForValue().get(key);\\n return clazz.cast(value);\\n }\\n
\\n // 使用下界通配符写入缓存\\n public void setCache(String key, <? super Serializable> value) {\\n redisTemplate.opsForValue().set(key, value);\\n }\\n
\\n
综合示例:
import java.util.ArrayList;\\nimport java.util.List;\\n\\npublic class Demo {\\n //实体类\\n class Animal {\\n void eat() {\\n System.out.println(\\"Animal is eating\\");\\n }\\n }\\n\\n class Dog extends Animal {\\n @Override\\n void eat() {\\n System.out.println(\\"Dog is eating\\");\\n }\\n }\\n\\n class Husky extends Dog {\\n @Override\\n void eat() {\\n System.out.println(\\"Husky is eating\\");\\n }\\n }\\n\\n /**\\n * 无界通配符 <?>\\n */\\n // 只能读取元素,不能写入(除null外)\\n public static void printAllElements(List<?> list) {\\n for (Object obj : list) {\\n System.out.println(obj);\\n }\\n // list.add(\\"test\\"); // 编译错误!无法写入具体类型\\n list.add(null); // 唯一允许的写入操作\\n }\\n\\n /**\\n * 上界通配符 <? extends T>\\n */ \\n // 安全读取为Animal,但不能写入(生产者场景)\\n public static void processAnimals(List<? extends Animal> animals) {\\n for (Animal animal : animals) {\\n animal.eat();\\n }\\n // animals.add(new Dog()); // 编译错误!无法确定具体子类型\\n }\\n\\n /**\\n * 下界通配符 <? super T>\\n */ \\n // 安全写入Dog,读取需要强制转换(消费者场景)\\n public static void addDogs(List<? super Dog> dogList) {\\n dogList.add(new Dog());\\n dogList.add(new Husky()); // Husky是Dog子类\\n // dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类\\n \\n Object obj = dogList.get(0); // 读取只能为Object\\n if (obj instanceof Dog) {\\n Dog dog = (Dog) obj; // 需要显式类型转换\\n }\\n }\\n\\n public static void main(String[] args) {\\n // 测试无界通配符\\n List<String> strings = List.of(\\"A\\", \\"B\\", \\"C\\");\\n printAllElements(strings);\\n\\n List<Integer> integers = List.of(1, 2, 3);\\n printAllElements(integers);\\n\\n // 测试上界通配符\\n List<Dog> dogs = new ArrayList<>();\\n dogs.add(new Dog());\\n processAnimals(dogs);\\n\\n List<Husky> huskies = new ArrayList<>();\\n huskies.add(new Husky());\\n processAnimals(huskies);\\n\\n // 测试下界通配符\\n List<Animal> animals = new ArrayList<>();\\n addDogs(animals);\\n System.out.println(animals);\\n\\n List<Object> objects = new ArrayList<>();\\n addDogs(objects);\\n }\\n}\\n
\\n我们需要清楚,这些只是我们开发过程中约定,不是强制规定,但遵循它们可以提高代码的可读性。
\\n我们在很多时候只是单纯的会使用某些技术,但是对它们里面许许多多常见的都是一知半解的,只是会使用确实很重要,但是如果有时间,我们不妨好好的在对这些技术进行深入学习,不仅知其然,而且知其所以然,这样我们的技术才会不断提升进步。
","description":"前言 不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。\\n\\n泛型有什么用?\\n\\n在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。\\n Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型…","guid":"https://juejin.cn/post/7475629913329008649","author":"镜花水月linyi","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T13:56:20.388Z","media":null,"categories":["后端","Java"],"attachments":null,"extra":null,"language":null},{"title":"领导要求下班前画一张图把分布式问题说清楚","url":"https://juejin.cn/post/7475549952610959410","content":"图解分布式事务难题
\\n\\n\\nSeata和Rocketmq事务消息也是我们我们正在使用的,非常适合中小型公司
\\n
本方案具有: 强弱结合型事务 全部特点 非常适合中小型项目使用
\\n\\n\\n秒杀/抢购/电商库存增减/供应链库存增减 等场景,都是属于 强弱结合型 的数据一致性场景
\\n
首先看看 RocketMQ 的事务消息, 能够保证本地操作 + 消息发送的原子性。具体来说, 主要是保证了本地方法执\\n行和消息发送在一个分布式事务中,要不全部成功,要不全部失败。
\\nRocketMQ 通过发送 half 消息来实现,下面详细说明一下:
\\n读写分离(Read-Write Splitting)是一种常见的数据库架构优化策略,通过将数据库的读操作(查询)和写操作(插入、更新、删除)分离到不同的数据库实例上,从而提高系统的性能、可扩展性和高可用性。
\\n在项目中实现读写分离目前主流的实现技术是通过 Apache ShardingSphere 来实现数据库的读写分离的。
\\n从 Apache ShardingSphere 官网也可以看出读写分离是其提供的主要功能之一:
\\n\\n\\nShardingSphere 官网地址:shardingsphere.apache.org/document/cu…
\\n
通过 ShardingSphere 可以轻松实现 MySQL 数据库的读写分离,以下是基于最新 ShardingSphere 5.x 版本的实现步骤和关键代码:
\\nShardingSphere 通过 JDBC 驱动层透明代理实现读写分离,其核心逻辑为:
\\n-- 主库配置(my.cnf)\\nserver-id=1\\nlog-bin=mysql-bin\\nbinlog-format=ROW\\n\\n-- 从库配置(my.cnf)\\nserver-id=2\\nrelay-log=relay-bin\\nread-only=1\\n\\n-- 主库创建复制账号\\nCREATE USER \'repl\'@\'%\' IDENTIFIED BY \'P@ssw0rd\';\\nGRANT REPLICATION SLAVE ON *.* TO \'repl\'@\'%\';\\nFLUSH PRIVILEGES;\\n\\n-- 从库配置主库连接\\nCHANGE MASTER TO \\n MASTER_HOST=\'master_ip\',\\n MASTER_USER=\'repl\',\\n MASTER_PASSWORD=\'P@ssw0rd\',\\n MASTER_LOG_FILE=\'mysql-bin.000001\',\\n MASTER_LOG_POS=592;\\nSTART SLAVE;\\n
\\n1.添加 Maven 依赖
\\n在 pom.xml 中添加 ShardingSphere 和数据库连接池的依赖:
\\n<dependency>\\n <groupId>org.apache.shardingsphere</groupId>\\n <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>\\n</dependency>\\n<dependency>\\n <groupId>com.mysql</groupId>\\n <artifactId>mysql-connector-j</artifactId>\\n</dependency>\\n
\\n2.配置 application.yml
\\n在 application.yml 中配置数据源和读写分离规则:
\\nspring:\\n shardingsphere:\\n datasource:\\n names: master,slave0\\n # 主库配置\\n master:\\n type: com.zaxxer.hikari.HikariDataSource\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n jdbc-url: jdbc:mysql://master_ip:3306/db?useSSL=false\\n username: root\\n password: Master@123\\n # 从库配置 \\n slave0:\\n type: com.zaxxer.hikari.HikariDataSource\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n jdbc-url: jdbc:mysql://slave_ip:3306/db?useSSL=false\\n username: root\\n password: Slave@123\\n # 从库2配置 \\n slave1:\\n type: com.zaxxer.hikari.HikariDataSource\\n driver-class-name: com.mysql.cj.jdbc.Driver\\n jdbc-url: jdbc:mysql://slave_ip:3306/db?useSSL=false\\n username: root\\n password: Slave@123\\n rules:\\n readwrite-splitting:\\n data-sources:\\n readwrite_ds:\\n type: Static\\n props:\\n write-data-source-name: master\\n read-data-source-names: \\n - slave0\\n - slave1\\n load-balancer-name: round_robin\\n load-balancers:\\n round_robin:\\n type: ROUND_ROBIN # 轮询\\n props:\\n sql-show: true # 显示实际路由的SQL\\n
\\n配置说明
\\n3.验证读写分离
\\n1.写操作测试
\\npublic void createUser(User user) {\\nuserMapper.insert(user); // INSERT 语句自动路由到master\\n}\\n
\\n2.读操作测试
\\npublic List<User> listUsers() {\\n return userMapper.selectList(null); // SELECT 语句路由到slave0\\n}\\n
\\n3.查看执行日志
\\n控制台会输出类似日志:
\\nActual SQL: master ::: INSERT INTO user (...)\\nActual SQL: slave0 ::: SELECT * FROM user\\n
\\nHintManager.getInstance().setPrimaryRouteOnly();\\n
\\nspring:\\n shardingsphere:\\n rules:\\n readwrite-splitting:\\n data-sources:\\n readwrite_ds:\\n type: Dynamic\\n props:\\n auto-aware-data-source-name: readwrite_ds\\n health-check-enabled: true\\n health-check-max-retry-count: 3\\n health-check-retry-interval: 5000\\n
\\n主从延迟问题:异步复制场景下,刚写入的数据可能无法立即从从库读取,可通过 HintManager 强制读主库临时解决。
\\n读写分离适用于以下场景:
\\n读写分离是一种常见的数据库架构优化策略,通过将数据库的读操作和写操作分离,提高了系统的性能、可扩展性和高可用性。读写分离主流的实现技术是 Apache ShardingSphere,通过添加依赖,配置读写分离规则的方式就可以轻松的实现读写分离。
\\n\\n","description":"读写分离(Read-Write Splitting)是一种常见的数据库架构优化策略,通过将数据库的读操作(查询)和写操作(插入、更新、删除)分离到不同的数据库实例上,从而提高系统的性能、可扩展性和高可用性。 在项目中实现读写分离目前主流的实现技术是通过 Apache ShardingSphere 来实现数据库的读写分离的。\\n\\n从 Apache ShardingSphere 官网也可以看出读写分离是其提供的主要功能之一:\\n\\nShardingSphere 官网地址:shardingsphere.apache.org/document/cu…\\n\\n通过…","guid":"https://juejin.cn/post/7475384306061213730","author":"Java中文社群","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T06:29:50.436Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/354fd4c107a84e61a1518d8eb7e7af28~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1741156190&x-signature=%2FlCDjb19zVhItwtyqNAG8ILHx38%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91642ed136b84f2db87ab10d2089a90d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1741156190&x-signature=wTonY8jwHZNJUCOmPaDEgJc8nuE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f45fdd16c55b4741b0e70a50d86ef3fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmF2YeS4reaWh-ekvue-pA==:q75.awebp?rk3s=f64ab15b&x-expires=1741156190&x-signature=AJChyFy2pNnwGfalRvwfmozTA8E%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","面试","Java"],"attachments":null,"extra":null,"language":null},{"title":"Java web后端转Java游戏后端","url":"https://juejin.cn/post/7475292103146684479","content":"本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:场景题、并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、JVM、设计模式、消息队列等模块。
\\n
作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:
\\n实时通信管理
\\n游戏逻辑处理
\\n数据持久化
\\n// 使用Protobuf定义移动协议\\nmessage PlayerMove {\\n int32 player_id = 1;\\n Vector3 position = 2; // 三维坐标\\n float rotation = 3; // 朝向\\n int64 timestamp = 4; // 客户端时间戳\\n}\\n\\nmessage BattleSkill {\\n int32 skill_id = 1;\\n repeated int32 target_ids = 2; // 多目标锁定\\n Coordinate cast_position = 3; // 技能释放位置\\n}\\n
\\ngraph TD\\n A[Gateway] --\x3e B[BattleServer]\\n A --\x3e C[SocialServer]\\n B --\x3e D[RedisCluster]\\n C --\x3e E[MySQLCluster]\\n F[MatchService] --\x3e B\\n
\\n网络层实现
\\n// Netty WebSocket处理器示例\\n@ChannelHandler.Sharable\\npublic class GameServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {\\n @Override\\n protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {\\n ProtocolMsg msg = ProtocolParser.parse(frame.text());\\n switch (msg.getType()) {\\n case MOVE: \\n handleMovement(ctx, (MoveMsg)msg);\\n break;\\n case SKILL_CAST:\\n validateSkillCooldown((SkillMsg)msg);\\n broadcastToAOI(ctx.channel(), msg);\\n break;\\n }\\n }\\n}\\n
\\nAOI(Area of Interest)管理
\\n战斗系统
\\n协议版本控制
\\n{\\n \\"ver\\": \\"1.2.3\\",\\n \\"cmd\\": 1001,\\n \\"data\\": {...}\\n}\\n
\\n调试工具链建设
\\n/debug latency 200 // 模拟200ms延迟\\n/simulate 5000 // 生成5000个机器人\\n
\\n联调流程
\\nJVM层面
\\n-XX:+UseG1GC -XX:MaxGCPauseMillis=50 \\n-XX:InitiatingHeapOccupancyPercent=35\\n
\\n网络优化
\\n数据库优化
\\n监控体系
\\n紧急处理预案
\\nif conn_count > 40000:\\n spin_up_new_instance()\\nif qps > 5000:\\n enable_rate_limiter()\\n
\\n问题场景:战斗不同步
\\n排查步骤:
问题场景:登录排队
\\n优化方案:
wait_time = current_queue_size * avg_process_time / available_instances\\n
\\n通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。
","description":"作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程: 一、游戏后端核心职责\\n\\n实时通信管理\\n\\n采用WebSocket/TCP长连接(90%以上MMO游戏选择)\\n使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)\\n心跳机制设计(15-30秒间隔,检测断线)\\n\\n游戏逻辑处理\\n\\n战斗计算(需在50ms内完成复杂技能伤害计算)\\n状态同步(通过Delta同步优化带宽,减少60%数据传输量)\\n定时…","guid":"https://juejin.cn/post/7475292103146684479","author":"加瓦点灯","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T03:19:58.557Z","media":null,"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"别踩坑!存储电话号码,到底用 int 还是用 string?","url":"https://juejin.cn/post/7475276076513738806","content":"在后端开发中,数据存储是一个看似简单却容易出问题的环节。今天,我们就来聊聊一个常见的问题:存储电话号码,到底该用 int
还是 String
?
在 Java 中,int
是一种基本数据类型,它占用 4 个字节(32 位),用于存储整数值。它的优势在于内存占用小,运算速度快,而且在 JVM 中直接存储为数字,没有额外的对象开销。相比之下,String
是一种引用数据类型,它本质上是一个封装了字符数组的对象,还包含了长度、哈希值等元数据。这意味着,String
在内存中需要分配对象空间,每次修改都会生成新的对象。
从性能角度看,int
的优势显而易见。它直接存储数字,没有对象分配和垃圾回收的烦恼。而 String
的操作则相对复杂,需要更多字节码指令和堆内存分配。但在实际开发中,我们不能仅仅从性能出发,还需要考虑数据的本质和应用场景。
电话号码本质上是一个标识符,而不是一个用于数学运算的数字。虽然它由数字组成,但在实际使用中,我们几乎不会对它进行加减乘除等运算。更重要的是,电话号码可能包含特殊符号(如 +
、-
、空格等),这些符号对于电话号码的完整性和可读性至关重要。
举个例子,一个国际电话号码 +123-456-7890
,如果用 int
存储,不仅无法保留 +
和 -
等符号,甚至连数字部分也可能超出 int
的范围(int
的最大值是 2^31 - 1
,即 2147483647
)。即使去掉符号,一些国家的电话号码长度可能达到 15 位,这已经超出了 int
的存储能力。此时,即使改用 long
,也无法解决符号问题。
String
更合适?从语义上看,String
更适合存储电话号码。它不仅可以表示纯数字,还可以包含任何字符序列,完美解决了 int
的局限性。此外,String
在实际开发中更加直观,也更容易与其他系统交互。例如,电话号码在数据库中通常以字符串形式存储,API 调用时也以字符串形式传递,前端显示时更是以字符串形式呈现。
从 JVM 的角度看,虽然 String
对象的内存开销相对较大,但它提供了更灵活的表现形式。更重要的是,JVM 对 String
的优化机制(如字符串常量池)可以在一定程度上减轻内存开销。当多个地方需要存储相同的电话号码时,字符串常量池会复用同一个对象,从而减少内存分配。
在 Java 开发中,我们不仅要关注代码的逻辑,还要了解 JVM 的字节码实现。int
和 String
在字节码层面的处理方式完全不同。
int
类型在字节码中使用 iadd
、isub
等指令进行运算,操作简单高效。而 String
的操作则需要通过对象指令完成。例如,创建一个 String
对象时,字节码会调用 new
指令分配对象内存,并通过 invokespecial
调用构造函数初始化对象。这意味着 String
的创建和操作相对复杂,需要更多的字节码指令和堆内存分配。
不过,JVM 的优化机制也在不断进步。例如,字符串常量池的存在使得重复的字符串可以共享同一个对象,从而减少内存占用。在处理大量重复的电话号码时,这种优化机制可以显著提升性能。
\\n某真实案例:某电信公司为了节省存储空间,在系统中使用 int
类型存储电话号码。结果,系统上线后问题频出。国际号码中的 +
和 -
符号无法存储,长号码超出 int
的范围导致数据丢失,甚至一些客户的电话号码无法正确关联,引发了大量用户投诉。
最终,开发团队不得不紧急将电话号码的存储类型改为 String
。这一改变不仅解决了数据丢失问题,还提高了系统的兼容性和可维护性。在后续的优化中,通过合理利用字符串常量池和数据库索引,系统的性能瓶颈也得到了有效缓解。
虽然 String
在内存占用和性能上看似不如 int
,但在实际开发中,我们可以通过一些优化手段弥补这些不足。例如:
在大多数应用场景中,存储电话号码时使用 String
是更合理的选择。它不仅能保证数据的完整性和可扩展性,还能通过优化手段解决性能瓶颈。
通过以上分析,我们可以得出结论:虽然 int
在某些情况下看起来更节省内存,但从数据的本质和实际应用场景出发,String
才是存储电话号码的最佳选择。它不仅能完美处理各种格式的电话号码,还能通过 JVM 的优化机制减少内存开销。
在后端开发中,我们不能仅仅从性能出发,更要考虑数据的语义和系统的可扩展性。选择合适的数据类型,不仅能避免数据丢失和错误,还能让系统更加健壮和高效。
\\n所以,下次再遇到存储电话号码的问题时,别犹豫,直接用 String
吧!
最后分享一份大彬精心整理的大厂面试手册,包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~
\\n围观朋友⭕:dabinjava
","description":"在后端开发中,数据存储是一个看似简单却容易出问题的环节。今天,我们就来聊聊一个常见的问题:存储电话号码,到底该用 int 还是 String? 1. 数据类型\\n\\n在 Java 中,int 是一种基本数据类型,它占用 4 个字节(32 位),用于存储整数值。它的优势在于内存占用小,运算速度快,而且在 JVM 中直接存储为数字,没有额外的对象开销。相比之下,String 是一种引用数据类型,它本质上是一个封装了字符数组的对象,还包含了长度、哈希值等元数据。这意味着,String 在内存中需要分配对象空间,每次修改都会生成新的对象。\\n\\n从性能角度看,int 的优…","guid":"https://juejin.cn/post/7475276076513738806","author":"大彬聊编程","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T02:26:15.946Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/196ac9ad83ad41c1ba385766c7370a20~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn5b2s6IGK57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1741141575&x-signature=uxFJ%2BNfJUdjlZGkjk5px4HNt8gc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1f70c55fcdfc4b089c8e809d4a4829e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn5b2s6IGK57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1741141575&x-signature=4lSJXzpF3y8FqxGmrVtVe3HlI8Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab781160e49240aca9ffb5f1e9279534~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn5b2s6IGK57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1741141575&x-signature=J5D1ZaORrDus1Muu7cKuPPLzYIg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8343050c8d5440abb197f043ea68a4d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn5b2s6IGK57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1741141575&x-signature=fzaE5qpwxYfUO0c%2Be8UiUwuew5s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df13b3b4238f442fb3fd3a90426cfa9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aSn5b2s6IGK57yW56iL:q75.awebp?rk3s=f64ab15b&x-expires=1741141575&x-signature=c9NY4CPYSDmaRUIVFbqCV%2FPQ094%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"保证接口幂等性的这 7 种方案,绝了!","url":"https://juejin.cn/post/7475291610931462144","content":"大家好,我是苏三,又跟大家见面了。
\\n接口幂等性
问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要的小伙伴一个参考。
不知道你有没有遇到过这些场景:
\\nform表单
时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。接口超时
问题,通常会引入了重试机制
。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。重复消息
(至于什么原因这里先不说,有兴趣的小伙伴,可以找我私聊),如果处理不好,也会产生重复的数据。没错,这些都是幂等性问题。
\\n接口幂等性
是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
这类问题多发于接口的:
\\ninsert
操作,这种情况下多次请求,可能会产生重复数据。update
操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1
,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1
,这种情况下多次请求,可能会导致数据错误。那么我们要如何保证接口幂等性?本文将会告诉你答案。
\\n最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
\\n通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert
前,先根据name
或code
字段select
一下数据。如果该数据已存在,则执行update
操作,如果不存在,才执行 insert
操作。
该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。我在这里提一下,是为了避免大家踩坑。
\\n在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:
\\n\\nupdate user amount = amount-100 where id=123;\\n
\\n
\\n如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。于此同时,系统开发人员可能也要哭了,因为这是很严重的系统bug。
\\n为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。
\\n通常情况下通过如下sql锁住单行数据:
\\n\\nselect * from user id=123 for update;\\n
\\n
\\n具体流程如下:
\\n具体步骤:
\\n多个请求同时根据id查询用户信息。
\\n判断余额是否不足100,如果余额不足,则直接返回余额不足。
\\n如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
\\n只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
\\n第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。
\\n如果余额不足,说明是重复请求,则直接返回成功。
\\n\\n\\n需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事务。此外,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。
\\n
悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。在这里顺便说一下,防重设计
和 幂等设计
,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp
或者version
字段,这里以version
字段为例。
在更新数据之前先查询一下数据:
\\n\\nselect id,amount,version from user id=123;\\n
\\n
\\n如果数据存在,假设查到的version
等于1
,再使用id
和version
字段作为查询条件更新数据:
update user set amount=amount+100,version=version+1where id=123 and version=1;\\n
\\n
\\n更新数据的同时version+1
,然后判断本次update
操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。
由于第一次请求version
等于1
是可以成功的,操作成功后version
变成2
了。这时如果并发的请求过来,再执行相同的sql:
update user set amount=amount+100,version=version+1where id=123 and version=1;\\n
\\n该update
操作不会真正更新数据,最终sql的执行结果影响行数是0
,因为version
已经变成2
了,where
中的version=1
肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。
具体流程如下:
具体步骤:
\\n绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
\\n\\nalter table `order` add UNIQUE KEY `un_code` (`code`);\\n
\\n
\\n加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry \'002\' for key \'order.un_code
异常,表示唯一索引有冲突。
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。
\\n如果是java
程序需要捕获:DuplicateKeyException
异常,如果使用了spring
框架还需要捕获:MySQLIntegrityConstraintViolationException
异常。
具体流程图如下:
\\n具体步骤:
\\n有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。
\\n针对这种情况,我们可以通过建防重表
来解决问题。
该表可以只包含两个字段:id
和 唯一索引
,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,例如:susan_0001。
具体流程图如下:
\\n具体步骤:
\\n\\n\\n需要特别注意的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。
\\n
很多时候业务表是有状态的,比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。
\\n假如id=123的订单状态是已支付
,现在要变成完成
状态。
update `order` set status=3 where id=123 and status=2;\\n
\\n
\\n第一次请求时,该订单的状态是已支付
,值是2
,所以该update
语句可以正常更新数据,sql执行结果的影响行数是1
,订单状态变成了3
。
后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了3
,再用status=2
作为条件,无法查询出需要更新的数据,所以最终sql执行结果的影响行数是0
,即不会真正的更新数据。但为了保证接口幂等性,影响行数是0
时,接口也可以直接返回成功。
具体流程图如下:
\\n具体步骤:
\\n\\n\\n主要特别注意的是,该方案仅限于要更新的
\\n表有状态字段
,并且刚好要更新状态字段
的这种特殊情况,并非所有场景都适用。
其实前面介绍过的加唯一索引
或者加防重表
,本质是使用了数据库
的分布式锁
,也属于分布式锁的一种。但由于数据库分布式锁
的性能不太好,我们可以改用:redis
或zookeeper
。
鉴于现在很多公司分布式配置中心改用apollo
或nacos
,已经很少用zookeeper
了,我们以redis
为例介绍分布式锁。
目前主要有三种方式实现redis的分布式锁:
\\n每种方案各有利弊,具体实现细节我就不说了,有兴趣的朋友可以加我微信找我私聊。
\\n具体流程图如下:
\\n具体步骤:
\\n\\n\\n需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费
\\nredis
的存储空间,需要根据实际业务情况而定。
最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
\\n你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
\\n添加苏三的私人微信:li_su223,备注:掘金+所在城市,即可加入。
\\n除了上述方案之外,还有最后一种使用token
的方案。该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
token
token
,完成业务操作。具体流程图如下:
\\n第一步,先获取token。
\\n第二步,做具体业务操作。
\\n具体步骤:
\\n以上方案是针对幂等设计的。
\\n如果是防重设计,流程图要改改:
\\n\\n\\n需要特别注意的是:token必须是全局唯一的。
\\n
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
\\n求一键三连:点赞、转发、在看。
\\n关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的50万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
","description":"大家好,我是苏三,又跟大家见面了。 前言\\n\\n接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要的小伙伴一个参考。\\n\\n不知道你有没有遇到过这些场景:\\n\\n有时我们在填写某些form表单时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。\\n我们在项目中为了解决接口超时问题,通常会引入了重试机制。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次…","guid":"https://juejin.cn/post/7475291610931462144","author":"苏三说技术","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T01:26:38.481Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af8e3f6e02134fc58dcd4618ddfba021~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=Qbjvz%2BY6G4jwp3MYFc6Irw1t7JE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d493df0ae1704d659b4e65a918f43388~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=idwDZrk7PDxHsrkF%2FfFdVruwH1A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49d67705b6284112a5a9e83a246057fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=InspQDZHRSLMkgqQzX3hY06UVcY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7cde381762514126bfac6c0898822987~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=HmhIT8y8lEguKsGRYVJYemT%2F9%2BI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8b74e38e713d40b6b42bf80f7253141a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=zFzpOvMRGs3kNO00Bl9j7ziN5Lk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9f82370b6bd4c2d8f1aae0b814af129~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=Sy6kPqmNmYEI5GNQtsHYA2lH4X0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80e758be9f9b477ca77bc0871acee955~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=4JCHZkZzea0I%2B0GgokehzznX9zg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78e06ff197ab4c9e8a92cf52927eefac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=hFL9uFr2zsG7eX%2BaqU6x5pjUJmQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ccc33846465446b3b9695dc393ee6470~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=jgE2aFp8ec8iKJKgQAEBJTER18k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3cf1de4cbb4d4f3f8d61b7989a553287~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6IuP5LiJ6K-05oqA5pyv:q75.awebp?rk3s=f64ab15b&x-expires=1741138779&x-signature=qePEDeap6oTJncV6zsrKa7dNfjE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"mysql---Undo Log、Redo Log和Binlog日志实现事务ACID","url":"https://juejin.cn/post/7475301917695655946","content":"\\n在深入理解Undo Log、Redo Log和Binlog之前,首先需要明确事务的ACID特性,这些特性是确保数据库操作可靠性和一致性的基石
\\nUndo Log记录了事务在修改数据之前的原始状态,用于在事务回滚时撤销未完成的修改,确保事务的原子性和隔离性
\\nUndo Log主要的功能有两个:事务回滚和MVCC
\\n首先来说事务回滚,事务如何通过Undo Log进行回滚操作呢?其实很简单,只需要在Undo Log日志中记录事务中的反向操作即可,发生回滚时直接通过Undo Log中记录的反向操作进行恢复。例如:
\\nUndo Log 保存的是一个版本的链路,使用roll_pointer这个字段来连接的。多个事务的Undo Log 日志组成了一个版本链,如图:
\\n在上图中:\\n一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer(回滚指针)和一个 trx_id( 事务 id)
\\ntrx_id代表事务id,记录了这一系列事务操作是基于哪个事务。
\\nroll_pointer代表回滚指针,就是当要发生rollback回滚操作时,就通过roll_pointer进行回滚,这个链表称为版本链。 在事务执行过程中,如果发生错误或主动回滚,Undo Log 会将数据恢复到事务开始前的状态,确保事务的所有操作要么全部完成,要么全部撤销,从而实现原子性。
\\n为了 提升 Undo Log 读写性能, Undo页还存在于Buffer Pool中,因为Buffer Pool 是 InnoDB 的内存缓存,用于存储数据页和索引页,以便快速访问。它通过减少磁盘 I/O,提高数据库的整体性能。将Undo页放在缓存中,可以加速事务的回滚和数据恢复。
\\n当事务commit之后,不会立即删除,会保留至所有快照读完成。后续会通过后台线程中的Master Thread或Purge Thread进行Undo Page的回收工作。
\\n再说MVCC,实现了自己 Copy-On-Write思想提升并发能力的时候, 也需要数据的副本,如上图,既然已经存在了这么多Undo Log的副本,那么MVCC可以直接复用这些副本数据。
\\n所以,Undo Log中的副本,可以用于实现多版本并发控制(MVCC),提升事务的并发性能,同时每一个事务操作自己的副本,实现事务的隔离性。
\\n实现MVCC主要通过三个元素,一个是我们上面已经提到的Undo Log版本链,一个是readView,最后就是我们上面已经提到的这些字段。因为整个课题比较大,在这里就不在过多的赘述。
\\n不同类型的写操作需要记录的内容也是不同的,所以产生的 undo log 格式也是不同。在 InnoDB 中,可分为
\\n- insert undo log
\\n在 insert 操作中产生的 undo log 被称为 insert undo log。由于 insert 操作的记录只对当前事务本身可见,对其他事务不可见(否则就是幻读了),因此该 uodo log 可以在事务提交后直接删除。不需要通过 purge 线程去清理。
\\n- update undo log
\\n在 delete 或 update 操作时产生的 undo log 被称为 update undo log。由于 MVCC 中可能会用到该 undo log,因此不能再事务提交后立刻删除。因此该 uodo log 会被加入到一个链表中,等待 purge 线程去清理。
\\n事务执行期间,在记录发生更新前,首先要记录相应的 undo log。如果是 update 操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,并根据该 undo log 去修改 Buffer Pool 中的 Undo 页面。
\\n在修改该 Undo 页面后,也是需要记录对应的 redo log,因为 undo log 需要基于 redo log 来实现持久化
\\n此外,内存中的 undo log 是会被删除清理的,例如 insert 操作在事务提交之后就可以清除掉了对应的 undo log;update 或 delete 操作则由后台线程 purge 进行清理
\\nRedo Log记录了事务对数据的修改操作,用于在系统崩溃后恢复已提交事务的修改,确保事务的持久性
\\n为了提高数据库的读写能力,MySQL 引入了 Buffer Pool:
\\n当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
\\n当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘。\\n但 Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。
\\n为了避免这个问题,一种做法是:
\\n但 MySQL 的设计者并没有使用这种做法,因此这个简单粗暴的做法有两大问题:
\\n\\n\\n我们知道 MySQL 内存和磁盘交互的最小单位是页,这意味着在上面的做法中,即使我们只修改了一条记录,也需要把整个页都刷到磁盘(刷脏页),这就好比快递员送快递时每次只送一个包裹,效率是很低的。
\\n
\\n\\n一个事务可能会修改多个数据页,加入这些页面并不相邻,就意味着将某个事务修改的 Buffer Pool 中的脏页刷新到磁盘时,会进行很多的随机 IO,而随机 IO 比顺序 IO 慢很多,尤其是对于传统的机械硬盘来说。
\\n
redo log 是一种物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个读写事务就会产生这样的一条或者多条这样的日志。当 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎就会使用 redo log 恢复数据,保证数据的持久性与完整性。
\\n当一个更新事务到达时,Redo Log的处理过程如下:
\\n**Redo Log利用WAL(Write-Ahead Logging)机制来保证故障恢复的安全性(crash-safe)。\\n**
\\nWAL的核心思想是先写日志,再写磁盘。根本原因是机械磁盘的性能,日志是顺序写,而数据页是随机写。顺序写的性能更高,所以先把日志归档。具体来说,当缓存页被修改(即变成脏页)后,相关的操作会先记录到Redo Log Buffer中。在事务提交(commit)时,后台线程会将Redo Log Buffer中的内容刷新到磁盘上(事务提交是Redo Log默认刷盘的时机)。此时,虽然脏页还没有写回磁盘,但只要Redo Log成功写入磁盘,就可以认为此次修改操作已完成。这是因为,即使发生故障导致脏页丢失,我们也可以通过磁盘上的Redo Log来恢复数据。因此,Redo Log与Undo Log的配合作用如下:
\\nRedo Log采用固定大小并循环写入的方式,类似环形缓冲区。当日志文件写满时,会从头开始覆盖之前的内容。这样的设计是因为Redo Log记录的是数据页的修改,而一旦Buffer Pool中的数据页被刷写到磁盘,之前的Redo Log记录就不再有效。新的日志会覆盖这些过时的记录。此外,硬盘上的Redo Log文件并非单一存在,而是以文件组的形式存储,每个文件的大小都相同。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录 4G 的内容。它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示:
\\n在写入数据的同时,也需要执行擦除操作。Redo Log成功刷盘到磁盘后,才可以进行擦除。因此,我们使用两个指针来管理这一过程:
\\n在图示中,黄色部分表示已写入完成的区域,而绿色部分则代表空闲区域。
\\nwrite pos和checkpoint指针之间的绿色区域,表示剩余的可写入空间,即Redo Log文件的空闲/可用部分。
\\n当进行数据页刷盘操作(checkpoint)时,checkpoint指针会顺时针移动,覆盖掉已写入的黄色区域,使其变为绿色。
\\n当write pos追赶上checkpoint时,意味着Redo Log文件已满,此时必须强制执行checkpoint操作,刷新Buffer Pool中的脏页并将其写入磁盘。随后,checkpoint指针会被移动,这样就可以继续向Redo Log文件中写入新的数据。
\\n事实上 redo log 刷盘时,并不是直接由 redo log buffer 刷到 redo log file,而是先写入到 page cache(文件系统缓存),再调用 fsync 方法同步到 redo log file 中
\\n而什么时候写入到 page cache,什么时候同步到 redo log file,是由我们接下来要说的 redo log 刷盘策略决定的。
\\n在 InnoDB 中通过 innodb_flush_log_at_trx_commit 参数来控制事务提交时 redo log 的刷盘策略:
\\n\\n\\n设置为 0 的时候,表示每次事务提交时即不写到 page cache,也不同步到 redo log file。由 InnoDB 后台线程每隔 1 秒去写入 page cache,然后调用 fsync 同步到 redo log file。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
\\n
\\n\\n设置为 1 的时候,表示每次事务提交时会写到 page cache,并同步到 redo log file。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
\\n
\\n\\n设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 里的 redo log 内容写入 page cache。再由操作系统(os)决定什么时候同步到 redo log file 中(依赖于操作系统后台线程)。这种方式的性能和安全性都介于前两者中间。
\\n
下面是不同刷盘策略的流程图:
\\n参数为 0 时,如果 MySQL 挂了或宕机,可能会丢失 1 秒内的数据。
\\n为什么是 1 秒?因为 InnoDB 有个后台线程每隔 1 秒将 redo log buffer 中的内容写到 page cache,然后调用 fsync 刷到磁盘。
\\n为 1 时, 只要事务提交成功,redo log 记录就一定在硬盘里(这点实际上是有二阶段提交来保证的),不会有任何数据丢失。
\\n如果事务执行期间 MySQL 挂了或宕机,会导致这部分日志丢失。但由于事务并没有提交,所以日志丢了也不会有损失,直接回滚即可。
\\n为 2 时, 只要事务提交成功,redo log buffer 中的内容只写入文件系统缓存(page cache)。
\\n如果仅仅只是 MySQL 挂了不会有任何数据丢失(会有操作系统的后台线程去把 page cache 中的 redo log 日志同步到 redo log file),但是宕机可能会有 1 秒数据的丢失
\\n\\n\\n小贴士:
\\nInnoDB 和操作系统(os)都有后台线程负责刷盘,不要把两者搞混了。
\\n
以上三种策略中:
\\nbinlog 是逻辑日志,记录内容是语句的原始逻辑,类似于 “给 ID=2 这一行的 c 字段加 1”。binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写,属于 MySQL Server 层。
\\nBinlog是MySQL特有的一种日志机制,记录了所有导致数据库状态变化的操作(如INSERT、UPDATE、DELETE)
\\n不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志
\\n那binlo 到底是用来干嘛的?
\\nbinlog 日志有三种格式,可以通过 binlog_format 参数指定。
\\n指定 statement,记录的内容是 SQL 语句原文,比如执行一条 update T set update_time=now() where id=1,记录的内容如下:
\\n同步数据时,会执行记录的 SQL 语句,但是有个问题,update_time=now() 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。
\\n为了解决这种问题,我们需要指定为 row,记录的内容不再是简单的 SQL 语句了,还包含操作的具体数据,记录内容如下
\\nrow 格式记录的内容看不到详细信息,要通过 mysqlbinlog 工具解析出来。
\\nupdate_time=now() 变成了具体的时间 update_time=“1627112756247”,条件后面的@1、@2、@3 都是该行数据第 1 个 - 3 个字段的原始值(假设这张表只有 3 个字段)。
\\n这样就能保证同步数据的一致性,通常情况下都是指定为 row,这样可以为数据库的恢复与同步带来更好的可靠性。
\\n但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。
\\n所以就有了一种折中的方案,指定为 mixed,记录的内容是前两者的混合
\\nMySQL 会判断这条 SQL 语句是否可能引起数据不一致,如果是,就用 row 格式,否则就用 statement 格式。
\\nbinlog 的写入时机非常简单,事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
\\n因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为 binlog cache。
\\n我们可以通过 binlog_cache_size 参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘。
\\nbinlog 日志刷盘流程如下:
\\nMySQL提供了一个sync_binlog参数,用于控制Binlog日志写入磁盘的频率:
\\n当sync_binlog设置为1时,系统提供最强的安全性,确保即使发生异常重启,也最多丢失一个事务的Binlog,而已经持久化的数据不会受到影响。然而,这种设置对性能的影响非常大。
\\n如果能够接受少量事务Binlog丢失的风险,并希望提高写入性能,一般可以将sync_binlog设置为100到1000之间的某个值,从而在性能和安全性之间找到平衡。
\\n通过本文的深入解析,我们全面了解了MySQL中Undo Log、Redo Log和Binlog三大日志机制,以及它们在保障事务ACID特性中的关键作用。
\\n在实际应用中,合理配置和管理这些日志机制,结合具体业务需求进行优化,是每位数据库管理员和开发者需要掌握的重要技能。希望通过本文,您能够对MySQL的核心机制有更深刻的理解,并在实际工作中灵活运用,为构建稳定可靠的数据库系统贡献力量。
","description":"继上一篇八大日志 后续 musql事务的ACID特性概述\\n\\n在深入理解Undo Log、Redo Log和Binlog之前,首先需要明确事务的ACID特性,这些特性是确保数据库操作可靠性和一致性的基石\\n\\n原子性(Atomicity) :事务中的所有操作要么全部成功,要么全部失败,不会出现部分完成的状态\\n一致性(Consistency) :事务的执行必须使数据库从一个一致性状态转变到另一个一致性状态,确保数据的完整性\\n隔离性(Isolation) :指在多事务并发执行时,一个事务的操作对其他事务的影响程度。它确保事务之间的操作是相互独立的…","guid":"https://juejin.cn/post/7475301917695655946","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T00:37:28.160Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/add95802517e4e00ab7783262fed2fb6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=j3lkI8J5AsdNlgfTZF8FnD1jfw8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/43be95bc257445d990527f27847d0a8b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=OgPgbhi1JyWuDHWSfABm80lNLlo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8ffbbb5498d4342b96b5ce01831c6ae~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=kTN%2FnGnchYF1fbJAIX6HZ225zjg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d912acbfede4dc7b093c1c2c94836f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=yxYHV3GuiiTObo%2FPzzpidqH%2Fei8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/610578199f7345f2b37bfed3689f8f56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=dNLbnAlg4X9OdgHgcuF6TF0ZcLw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cf9e6cfd8d8e43dc9c08ee5e4624460e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=JpfAXZhdW6ad1EsZAGWNe6wJpgY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6695f2867abc425eb01fbf18d66eb7a6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=am5cF4iDv6BbsynPdEqqA9xP%2FFs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89279ee31c1f40ee88cdb58e1b19f96b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=%2Bmmd7ha82rF3Sl7X3tG4HPhqGeo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ba0ca659ed294515b4a97f2ac80b7205~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=XRS5omAv3ES95knjs6NzWhZGkak%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d4ea4ae7a20b4ae199bc95e761cfeb5f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=hReif9o%2Bua1vnIQGv5wZ9un1erQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa1fea4e9eb84f1cbc541634ecec49da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=jyHC5oK3HlmYOp8gXPKtpXr7UB8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a5ba3a9c70494e858d4f2445664c87c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135181&x-signature=nXiSf%2Fg91pvD3yZtENNL0AyHAsQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Java","数据库"],"attachments":null,"extra":null,"language":null},{"title":"凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!","url":"https://juejin.cn/post/7475302408066269194","content":"大家好,我是晓凡。
\\n\\"凡哥!线上服务响应时间飙到10秒了!\\"凌晨1点,实习生小李的语音带着哭腔。
\\n监控大屏上,JVM堆内存曲线像坐了火箭——刚扩容的16G内存,30分钟就被吃干抹净。
\\n我咬着牙拍桌子:\\"把最近一周上线的代码给我翻个底朝天!\\"
▌ 翻车代码(真实项目片段)
\\n// 缓存用户AI对话历史 → 翻车写法!\\npublic class ChatHistoryCache {\\n private static Map<Long, List<String>> cache = new HashMap<>();\\n\\n public static void addMessage(Long userId, String msg) {\\n cache.computeIfAbsent(userId, k -> new ArrayList<>()).add(msg);\\n }\\n}\\n
\\n▌ 翻车现场
\\nvmtool --action getInstances -c 4614556e
看到Map尺寸破千万HashMap$Node
对象占堆内存82%▌ 正确姿势
\\n// 改用Guava带过期时间的缓存\\nprivate static Cache<Long, List<String>> cache = CacheBuilder.newBuilder()\\n .expireAfterAccess(1, TimeUnit.HOURS) \\n .maximumSize(10000)\\n .build();\\n
\\n▌ 致命代码(处理AI模型文件)
\\n// 加载本地模型文件 → 翻车写法!\\npublic void loadModels(List<File> files) {\\n files.forEach(file -> {\\n try {\\n InputStream is = new FileInputStream(file); // 漏了关闭!\\n parseModel(is);\\n } catch (IOException e) { /*...*/ }\\n });\\n}\\n
\\n▌ 诡异现象
\\nlsof -p 进程ID | grep \'deleted\'
发现大量未释放文件句柄jcmd PID VM.native_memory
显示文件描述符数量突破1万▌ 抢救方案
\\n// 正确写法:try-with-resources自动关闭\\nfiles.forEach(file -> {\\n try (InputStream is = new FileInputStream(file)) { // 自动关流\\n parseModel(is);\\n } catch (IOException e) { /*...*/ }\\n});\\n
\\n▌ 坑爹代码(消息通知模块)
\\n// 监听AI处理完成事件 → 翻车写法!\\n@Component\\npublic class NotifyService {\\n\\n @EventListener\\n public void handleAiEvent(AICompleteEvent event) {\\n // 错误持有外部服务引用\\n externalService.registerCallback(this::sendNotification); \\n }\\n}\\n
\\n▌ 内存曲线
\\nNotifyService
实例数随时间线性增长▌ 避坑绝招
\\n// 使用弱引用解除绑定\\npublic void handleAiEvent(AICompleteEvent event) {\\n WeakReference<NotifyService> weakRef = new WeakReference<>(this);\\n externalService.registerCallback(() -> {\\n NotifyService service = weakRef.get();\\n if (service != null) service.sendNotification();\\n });\\n}\\n
\\n▌ 问题代码(异步处理AI请求)
\\n// 异步线程池配置 → 翻车写法!\\n@Bean\\npublic Executor asyncExecutor() {\\n return new ThreadPoolExecutor(10, 10,\\n 0L, TimeUnit.MILLISECONDS,\\n new LinkedBlockingQueue<>()); // 无界队列!\\n}\\n
\\n▌ 灾难现场
\\nbyte[]
占内存90%,全是待处理的响应数据queue_size
指标持续高位不降▌ 正确配置
\\n// 设置队列上限+拒绝策略\\nnew ThreadPoolExecutor(10, 50, \\n 60L, TimeUnit.SECONDS,\\n new ArrayBlockingQueue<>(1000), \\n new ThreadPoolExecutor.CallerRunsPolicy());\\n
\\n▌ 致命代码(查询用户对话记录)
\\npublic List<ChatRecord> getHistory(Long userId) {\\n SqlSession session = sqlSessionFactory.openSession();\\n try {\\n return session.selectList(\\"queryHistory\\", userId);\\n } finally {\\n // 忘记session.close() → 连接池逐渐枯竭\\n }\\n}\\n
\\n▌ 泄露证据
\\nCannot get connection from pool, timeout 30000ms
SqlSession
实例数异常增长▌ 正确姿势
\\n// 使用try-with-resources自动关闭\\ntry (SqlSession session = sqlSessionFactory.openSession()) {\\n return session.selectList(\\"queryHistory\\", userId);\\n}\\n
\\n▌ 问题代码(缓存用户偏好设置)
\\n// 使用Ehcache时的错误配置\\nCacheConfiguration<Long, UserPreference> config = new CacheConfiguration<>()\\n .setName(\\"user_prefs\\")\\n .setMaxEntriesLocalHeap(10000); // 只设置了数量,没设过期时间!\\n
\\n▌ 内存症状
\\nwatch com.example.CacheService getCachedUser
返回对象存活时间超7天UserPreference
对象▌ 正确配置
\\nconfig.setTimeToLiveSeconds(3600) // 1小时过期\\n .setDiskExpiryThreadIntervalSeconds(60); // 过期检查间隔\\n
\\n▌ 致命代码(用户上下文传递)
\\npublic class UserContextHolder {\\n private static final ThreadLocal<User> currentUser = new ThreadLocal<>();\\n\\n public static void set(User user) {\\n currentUser.set(user);\\n }\\n\\n // 缺少remove方法!\\n}\\n
\\n▌ 内存异常
\\nUser
对象被ThreadLocalMap
强引用无法释放▌ 修复方案
\\n// 使用后必须清理!\\npublic static void remove() {\\n currentUser.remove();\\n}\\n\\n// 在拦截器中强制清理\\n@Around(\\"execution(* com.example..*.*(..))\\")\\npublic Object clearContext(ProceedingJoinPoint pjp) throws Throwable {\\n try {\\n return pjp.proceed();\\n } finally {\\n UserContextHolder.remove(); // 关键!\\n }\\n}\\n
\\n1. Arthas实战三连击
\\n# 实时监控GC情况\\ndashboard -n 5 -i 2000\\n\\n# 追踪可疑方法调用频次\\ntrace com.example.CacheService addCacheEntry -n 10\\n\\n# 动态修改日志级别(无需重启)\\nlogger --name ROOT --level debug\\n
\\n2. MAT分析三板斧
\\nSELECT * FROM java.util.HashMap WHERE size > 10000\\nSELECT toString(msg) FROM java.lang.String WHERE msg.value LIKE \\"%OOM%\\"\\n
\\n3. 线上救火命令包
\\n# 快速查看堆内存分布\\njhsdb jmap --heap --pid <PID>\\n\\n# 统计对象数量排行榜\\njmap -histo:live <PID> | head -n 20\\n\\n# 强制触发Full GC(慎用!)\\njcmd <PID> GC.run\\n
\\ntry (InputStream is = ...) { // 第一重\\n useStream(is); \\n} catch (IOException e) { // 第二重\\n log.error(\\"IO异常\\", e);\\n} finally { // 第三重\\n cleanupTempFiles();\\n}\\n
\\n运维老凡的避坑日记
\\n\\n\\n2024-03-20 凌晨2点
\\n
\\n\\"小王啊,知道为什么我头发这么少吗?
\\n当年有人把用户会话存到ThreadLocal里不清理,
\\n结果线上十万用户同时在线时——
\\n那内存泄漏的速度比理发店推子还快!\\"
自测题:你能看出这段代码哪里会泄漏吗?
\\n// 危险代码!请找出三个泄漏点\\npublic class ModelLoader {\\n private static List<Model> loadedModels = new ArrayList<>();\\n \\n public void load(String path) {\\n Model model = new Model(Files.readAllBytes(Paths.get(path)));\\n loadedModels.add(model);\\n Executors.newSingleThreadScheduledExecutor()\\n .scheduleAtFixedRate(() -> model.refresh(), 1, 1, HOURS);\\n }\\n}\\n
\\n答案揭晓:
\\n本期内容到这儿就结束了,希望对您有所帮助~\\n我们下期再见 ヾ(•ω•`)o (●\'◡\'●)
","description":"大家好,我是晓凡。 引子:那个让运维集体加班的夜晚\\n\\n\\"凡哥!线上服务响应时间飙到10秒了!\\"凌晨1点,实习生小李的语音带着哭腔。\\n 监控大屏上,JVM堆内存曲线像坐了火箭——刚扩容的16G内存,30分钟就被吃干抹净。\\n 我咬着牙拍桌子:\\"把最近一周上线的代码给我翻个底朝天!\\"\\n\\n第一坑:Static集合成永动机\\n\\n▌ 翻车代码(真实项目片段)\\n\\n// 缓存用户AI对话历史 → 翻车写法!\\npublic class ChatHistoryCache {\\n private static Map\\n\\nXXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 ———官网
\\n
废话不多说,现在我们来开始把xxl-job集成到SpringBoot(SpringCloud)中
\\n没错,你没看错,是把他的仓库克隆下来,和其他的框架不同(在项目中引入jar包即可)
\\ngitee地址:gitee.com/xuxueli0323…
\\n之后idea打开这样
\\nxxl-job存储的介质是mysql,所以我们需要执行一下sql文件来创建一个库。找到官方的sql文件
\\n在我们本地的mysql直接执行,执行之后会得到一个名字叫xxl-job的库
\\n这里一共八个表,他们的作用是:
\\n配置文件的地址是:
\\n/xxl-job/xxl-job-admin/src/main/resources/application.properties\\n
\\n这里面我们直接填写本地的数据库信息
\\n配置好本地的mysql环境之后,选择好项目的jdk
\\n之后我们启动启动类
\\ncom.xxl.job.admin.XxlJobAdminApplication\\n
\\n控制台打印如下信息则说明我们启动完成了\\n
访问xxl-job的控制台
\\nhttp://localhost:8084/xxl-job-admin/toLogin
\\n默认的用户密码是 admin、123456
执行器(Executor)是指用于执行具体任务的运行时组件。执行器负责接收任务调度中心分配的任务,并按照任务的配置进行执行。可以理解为xxl-job-admin项目是一个注册中心,而执行器项目就是我们的执行者,注册中心负责管理统筹我们具体的任务,而任务里面具体做了什么事情就是执行器项目里面的业务了。
\\n直接使用官方的项目即可
\\n官方的仓库里面也给了我们一个执行器示例项目,叫做xxl-job-executor-sample-springboot
这个时候我们去访问我们的控制台页面即可看到我们的执行器项目已经成功注册进来
\\n对于每一个接入 xxl-job的服务都要做上面的三步 所以后续我们会考虑封装个 xxl-stater 简化上述重复步骤呢
\\n\\n# XxlJobConfig\\n\\n\\npackage com.xxl.job.executor.core.config;\\n\\nimport com.xxl.job.core.executor.impl.XxlJobSpringExecutor;\\nimport org.slf4j.Logger;\\nimport org.slf4j.LoggerFactory;\\nimport org.springframework.beans.factory.annotation.Value;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.context.annotation.Configuration;\\n\\n/**\\n * xxl-job config\\n *\\n * @author xuxueli 2017-04-28\\n */\\n@Configuration\\npublic class XxlJobConfig {\\n private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);\\n\\n @Value(\\"${xxl.job.admin.addresses}\\")\\n private String adminAddresses;\\n\\n @Value(\\"${xxl.job.accessToken}\\")\\n private String accessToken;\\n\\n @Value(\\"${xxl.job.executor.appname}\\")\\n private String appname;\\n\\n @Value(\\"${xxl.job.executor.address}\\")\\n private String address;\\n\\n @Value(\\"${xxl.job.executor.ip}\\")\\n private String ip;\\n\\n @Value(\\"${xxl.job.executor.port}\\")\\n private int port;\\n\\n @Value(\\"${xxl.job.executor.logpath}\\")\\n private String logPath;\\n\\n @Value(\\"${xxl.job.executor.logretentiondays}\\")\\n private int logRetentionDays;\\n\\n\\n @Bean\\n public XxlJobSpringExecutor xxlJobExecutor() {\\n logger.info(\\">>>>>>>>>>> xxl-job config init.\\");\\n XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();\\n xxlJobSpringExecutor.setAdminAddresses(adminAddresses);\\n xxlJobSpringExecutor.setAppname(appname);\\n xxlJobSpringExecutor.setAddress(address);\\n xxlJobSpringExecutor.setIp(ip);\\n xxlJobSpringExecutor.setPort(port);\\n xxlJobSpringExecutor.setAccessToken(accessToken);\\n xxlJobSpringExecutor.setLogPath(logPath);\\n xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);\\n\\n return xxlJobSpringExecutor;\\n }\\n\\n /**\\n * 针对多网卡、容器内部署等情况,可借助 \\"spring-cloud-commons\\" 提供的 \\"InetUtils\\" 组件灵活定制注册IP;\\n *\\n * 1、引入依赖:\\n * <dependency>\\n * <groupId>org.springframework.cloud</groupId>\\n * <artifactId>spring-cloud-commons</artifactId>\\n * <version>${version}</version>\\n * </dependency>\\n *\\n * 2、配置文件,或者容器启动变量\\n * spring.cloud.inetutils.preferred-networks: \'xxx.xxx.xxx.\'\\n *\\n * 3、获取IP\\n * String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();\\n */\\n\\n\\n}\\n\\n\\n\\n# application.properties\\n\\n\\n# web port\\nserver.port=8089\\n# no web\\n#spring.main.web-environment=false\\n\\n# log config\\nlogging.config=classpath:logback.xml\\n\\n\\n### xxl-job admin address list, such as \\"http://address\\" or \\"http://address01,http://address02\\"\\nxxl.job.admin.addresses=http://127.0.0.1:8084/xxl-job-admin\\n\\n### xxl-job, access token\\nxxl.job.accessToken=\\n\\n### xxl-job executor appname\\nxxl.job.executor.appname=xxl-job-executor-sample\\n### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null\\nxxl.job.executor.address=\\n### xxl-job executor server-info\\nxxl.job.executor.ip=\\nxxl.job.executor.port=9999\\n### xxl-job executor log-path\\nxxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler\\n### xxl-job executor log-retention-days\\nxxl.job.executor.logretentiondays=30\\n\\n\\n\\n# POM \\n\\n<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<project xmlns=\\"http://maven.apache.org/POM/4.0.0\\"\\n xmlns:xsi=\\"http://www.w3.org/2001/XMLSchema-instance\\"\\n xsi:schemaLocation=\\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\\">\\n <modelVersion>4.0.0</modelVersion>\\n <parent>\\n <groupId>com.xuxueli</groupId>\\n <artifactId>xxl-job-executor-samples</artifactId>\\n <version>2.3.0</version>\\n </parent>\\n <artifactId>xxl-job-executor-sample-springboot</artifactId>\\n <packaging>jar</packaging>\\n\\n <name>${project.artifactId}</name>\\n <description>Example executor project for spring boot.</description>\\n <url>https://www.xuxueli.com/</url>\\n\\n <properties>\\n </properties>\\n\\n <dependencyManagement>\\n <dependencies>\\n <dependency>\\n <!-- Import dependency management from Spring Boot (依赖管理:继承一些默认的依赖,工程需要依赖的jar包的管理,申明其他dependency的时候就不需要version) --\x3e\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-parent</artifactId>\\n <version>${spring-boot.version}</version>\\n <type>pom</type>\\n <scope>import</scope>\\n </dependency>\\n </dependencies>\\n </dependencyManagement>\\n\\n <dependencies>\\n <!-- spring-boot-starter-web (spring-webmvc + tomcat) --\x3e\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-web</artifactId>\\n </dependency>\\n <dependency>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-starter-test</artifactId>\\n <scope>test</scope>\\n </dependency>\\n\\n <!-- xxl-job-core --\x3e\\n <dependency>\\n <groupId>com.xuxueli</groupId>\\n <artifactId>xxl-job-core</artifactId>\\n <version>${project.parent.version}</version>\\n </dependency>\\n\\n </dependencies>\\n\\n <build>\\n <plugins>\\n <!-- spring-boot-maven-plugin (提供了直接运行项目的插件:如果是通过parent方式继承spring-boot-starter-parent则不用此插件) --\x3e\\n <plugin>\\n <groupId>org.springframework.boot</groupId>\\n <artifactId>spring-boot-maven-plugin</artifactId>\\n <version>${spring-boot.version}</version>\\n <executions>\\n <execution>\\n <goals>\\n <goal>repackage</goal>\\n </goals>\\n </execution>\\n </executions>\\n </plugin>\\n </plugins>\\n </build>\\n\\n\\n</project>\\n\\n\\n
\\n通过在任务执行类的方法上添加@XxlJob注解,可以将该方法标记为一个定时任务。注解中可以设置任务的名称、分组、CRON表达式等属性,以及任务的参数和路由策略等。
\\n使用项目中的示例任务
\\ncom.xxl.job.executor.service.jobhandler.SampleXxlJob#demoJobHandler\\n
\\n使用控制台调用任务
\\n之后可以看到代码里面对应的信息被打印到日志中(示例任务是这样的,实际根据你的业务写代码就可以了)\\n
为了解决上述 [5.1] 提出的问题,我们开始封装 xxlJob stater组件库
\\n让公司业务服务快速接入xxl-job 无需额外配置
\\n组件总概述:
\\n定义自动引入配置开关
\\npackage com.opengoofy.aska12306.springboot.starter.xxljob.annotation;\\n\\nimport com.opengoofy.aska12306.springboot.starter.xxljob.XxlJobAutoConfiguration;\\nimport org.springframework.context.annotation.Import;\\n\\nimport java.lang.annotation.Documented;\\nimport java.lang.annotation.ElementType;\\nimport java.lang.annotation.Inherited;\\nimport java.lang.annotation.Retention;\\nimport java.lang.annotation.RetentionPolicy;\\nimport java.lang.annotation.Target;\\n\\n/**\\n * aska: 编写 starter的一种方式,通过定义一个注解,来开启本starter的bean引入\\n * <p>\\n * 激活xxl-job配置 这是方式二\\n *\\n * @date 2020/9/14\\n */\\n@Target({ElementType.TYPE})\\n@Retention(RetentionPolicy.RUNTIME)\\n@Documented\\n@Inherited\\n@Import({XxlJobAutoConfiguration.class})\\npublic @interface EnableXxlJob {\\n\\n}\\n
\\npackage com.opengoofy.aska12306.springboot.starter.xxljob.properties;\\n\\nimport lombok.Data;\\n\\n/**\\n * xxl-job管理平台配置\\n *\\n *\\n * @date 2020/9/14\\n */\\n@Data\\npublic class XxlAdminProperties {\\n\\n /**\\n * 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。 执行器将会使用该地址进行\\"执行器心跳注册\\"和\\"任务结果回调\\";为空则关闭自动注册;\\n */\\n private String addresses;\\n\\n}\\n
\\n\\npackage com.opengoofy.aska12306.springboot.starter.xxljob.properties;\\n\\nimport lombok.Data;\\n\\n/**\\n * xxl-job执行器配置\\n *\\n *\\n * @date 2020/9/14\\n */\\n@Data\\npublic class XxlExecutorProperties {\\n\\n /**\\n * 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册\\n */\\n private String appname;\\n\\n /**\\n * 服务注册地址,优先使用该配置作为注册地址 为空时使用内嵌服务 ”IP:PORT“ 作为注册地址 从而更灵活的支持容器类型执行器动态IP和动态映射端口问题\\n */\\n private String address;\\n\\n /**\\n * 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP ,该IP不会绑定Host仅作为通讯实用;地址信息用于 \\"执行器注册\\" 和\\n * \\"调度中心请求并触发任务\\"\\n */\\n private String ip;\\n\\n /**\\n * 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9099,单机部署多个执行器时,注意要配置不同执行器端口;\\n */\\n private Integer port = 0;\\n\\n /**\\n * 执行器通讯TOKEN [必填]:从配置文件中取不到值时使用默认值;\\n */\\n private String accessToken = \\"\\";\\n\\n /**\\n * 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;\\n */\\n private String logPath = \\"logs/applogs/xxl-job/jobhandler\\";\\n\\n /**\\n * 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效;\\n */\\n private Integer logRetentionDays = 30;\\n\\n}\\n
\\n\\npackage com.opengoofy.aska12306.springboot.starter.xxljob.properties;\\n\\nimport lombok.Data;\\nimport org.springframework.boot.context.properties.ConfigurationProperties;\\nimport org.springframework.boot.context.properties.NestedConfigurationProperty;\\n\\n/**\\n * xxl-job配置\\n *\\n *\\n * @date 2020/9/14\\n */\\n@Data\\n@ConfigurationProperties(prefix = \\"xxl.job\\")\\npublic class XxlJobProperties {\\n\\n @NestedConfigurationProperty\\n private XxlAdminProperties admin = new XxlAdminProperties();\\n\\n @NestedConfigurationProperty\\n private XxlExecutorProperties executor = new XxlExecutorProperties();\\n\\n}\\n
\\nxxl-job自动装配 core
\\npackage com.opengoofy.aska12306.springboot.starter.xxljob;\\n\\nimport com.opengoofy.aska12306.springboot.starter.xxljob.properties.XxlExecutorProperties;\\nimport com.opengoofy.aska12306.springboot.starter.xxljob.properties.XxlJobProperties;\\nimport com.xxl.job.core.executor.impl.XxlJobSpringExecutor;\\nimport org.slf4j.Logger;\\nimport org.slf4j.LoggerFactory;\\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\\nimport org.springframework.cloud.client.discovery.DiscoveryClient;\\nimport org.springframework.context.annotation.Bean;\\nimport org.springframework.core.env.Environment;\\nimport org.springframework.util.StringUtils;\\n\\nimport java.util.List;\\nimport java.util.stream.Collectors;\\n\\n/**\\n * xxl-job自动装配\\n *\\n * @date 2020/9/14\\n */\\n@EnableConfigurationProperties(XxlJobProperties.class)\\npublic class XxlJobAutoConfiguration {\\n private Logger log = LoggerFactory.getLogger(XxlJobAutoConfiguration.class);\\n\\n /**\\n * 服务名称 包含 XXL-JOB 则说明是 Admin\\n */\\n private static final String XXL_JOB_ADMIN = \\"xxl-job-admin\\";\\n\\n /**\\n * 配置xxl-job 执行器,提供自动发现 xxl-job 能力\\n *\\n * @param xxlJobProperties xxl 配置\\n * @param environment 环境变量\\n * @param discoveryClient 注册发现客户端\\n * @return\\n */\\n @Bean\\n public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobProperties xxlJobProperties, Environment environment, DiscoveryClient discoveryClient) {\\n XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();\\n XxlExecutorProperties executor = xxlJobProperties.getExecutor();\\n // 应用名默认为服务名\\n String appName = executor.getAppname();\\n if (!StringUtils.hasText(appName)) {\\n appName = environment.getProperty(\\"spring.application.name\\");\\n }\\n String accessToken = environment.getProperty(\\"xxl.job.accessToken\\");\\n if (!StringUtils.hasText(accessToken)) {\\n accessToken = executor.getAccessToken();\\n }\\n\\n xxlJobSpringExecutor.setAppname(appName);\\n xxlJobSpringExecutor.setAddress(executor.getAddress());\\n xxlJobSpringExecutor.setIp(executor.getIp());\\n xxlJobSpringExecutor.setPort(executor.getPort());\\n xxlJobSpringExecutor.setAccessToken(accessToken);\\n xxlJobSpringExecutor.setLogPath(executor.getLogPath());\\n xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays());\\n\\n // 配置\\n // nacos 如果配置为空则获取注册中心的服务列表 \\"http://ip:8080/xxl-job-admin\\"\\n if (!StringUtils.hasText(xxlJobProperties.getAdmin().getAddresses())) {\\n List<String> serviceList = discoveryClient.getServices();\\n String serverList = serviceList.stream()\\n .filter(s -> s.contains(XXL_JOB_ADMIN))\\n .flatMap(s -> discoveryClient.getInstances(s).stream()).map(instance -> String.format(\\"http://%s:%s/%s\\", instance.getHost(), instance.getPort(), XXL_JOB_ADMIN))\\n .collect(Collectors.joining(\\",\\"));\\n xxlJobSpringExecutor.setAdminAddresses(serverList);\\n } else {\\n xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdmin().getAddresses());\\n }\\n log.info(\\"init-xxlJobSpringExecutor: \\" + xxlJobSpringExecutor.toString());\\n return xxlJobSpringExecutor;\\n }\\n\\n}\\n
\\n\\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\\\\n com.opengoofy.aska12306.springboot.starter.xxljob.XxlJobAutoConfiguration\\n\\n//aska: 编写 starter的一种方式,通过spring.factories,来自动开启本starter的bean引入\\n
\\n\\n<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>\\n<project xmlns=\\"http://maven.apache.org/POM/4.0.0\\"\\n xmlns:xsi=\\"http://www.w3.org/2001/XMLSchema-instance\\"\\n xsi:schemaLocation=\\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\\">\\n <parent>\\n <artifactId>aska12306-frameworks</artifactId>\\n <groupId>com.aska12306</groupId>\\n <version>1.0-SNAPSHOT</version>\\n </parent>\\n <modelVersion>4.0.0</modelVersion>\\n\\n <artifactId>aska12306-xxljob-spring-boot-starter</artifactId>\\n\\n <description>定时任务,基于xxl-job</description>\\n\\n <dependencies>\\n <dependency>\\n <groupId>com.xuxueli</groupId>\\n <artifactId>xxl-job-core</artifactId>\\n <version>${xxl-job.version}</version>\\n </dependency>\\n <dependency>\\n <groupId>org.projectlombok</groupId>\\n <artifactId>lombok</artifactId>\\n </dependency>\\n <dependency>\\n <groupId>com.alibaba.cloud</groupId>\\n <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\\n </dependency>\\n </dependencies>\\n\\n</project>\\n
\\n通过本文的介绍,我们了解了如何在Spring Boot项目中集成XXL-Job,实现超牛的定时任务。XXL-Job提供了强大的任务调度和管理功能,使得定时任务的开发和管理变得更加简单和高效。我们还封装了starter组件,方便公司内部服务集成。提供统一接口
\\n集成XXL-Job,我们可以通过配置或注解的方式定义定时任务,并灵活地设置任务的调度规则和执行方式。无论是按照时间间隔还是时间点执行任务,XXL-Job都能满足我们的需求。同时,XXL-Job还提供了任务的监控和管理功能,让我们可以实时了解任务的执行情况和结果。
\\n通过使用XXL-Job,我们可以轻松实现定时任务的自动化执行,提高系统的稳定性和可靠性。无论是在企业级应用中还是个人项目中,XXL-Job都能为我们带来便利和效益。
\\nXXL-Job作为一款国产的优秀开源软件,提供了强大的任务调度和管理功能,完全能够媲美甚至超越国外同类产品。作为国产软件的使用者和推广者,我们应该积极拥抱国产软件,为其发展壮大贡献自己的力量。让我们携手努力,为国内软件行业的发展做出更大的贡献(没收许雪里的钱)!
","description":"xxlJob简单 介绍 XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 ———官网\\n\\n废话不多说,现在我们来开始把xxl-job集成到SpringBoot(SpringCloud)中\\n\\nSpringBoot集成xxl-job 第一版\\n下载官方的仓库\\n\\n没错,你没看错,是把他的仓库克隆下来,和其他的框架不同(在项目中引入jar包即可)\\n\\ngitee地址:gitee.com/xuxueli0323…\\n\\n之后idea打开这样\\n\\n第二步,配置xxl服务端数据源…","guid":"https://juejin.cn/post/7475214133979627547","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T09:18:42.745Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/756c7adaf8f34000a95a50c53d6ac291~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=KsiEiXeqPOdH4B0ryqQOouxA7JI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e5024bd2fccf4570b3917c413f82016a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=R80ddfVQbnjl%2FxQSst6IVUnsqkQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f5c4597e455402a80821fb6b75f66ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=P7bZllRRMjo7p8DPdkhp25bnT3E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ade615c6ca0d4604be64ccea6ff2722f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=4JMLmFWbs8Qj%2F4OWL1ohERV2JXw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/64151497ec564b8d8957ff8ec1245424~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=6Q9d7XwJNakPLgt6MsxVYZguTQg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31abb19146f94965b53a4bd66bd9aba8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=KOoHRRYVak50NSOhs1%2FcBVTQASo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e321222ac7194bb2981370c2a36097ad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=S9l6G0ZJOcuG0WLZ5aJV4z7Tv2s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/170b439200774b1f8793ffd29139c3ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=tmQZNpW1L4qVHAA%2FCkxyb%2FniSlk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/33fa96bb609944c29b7ebb8127270686~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=x%2BYomf2cDI5QG15dx3eMLcmRsNo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/362d7cd02602415c9f8fa527b2c83857~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=3r%2BQ%2Foy53h0N6ufoDQTFjTv%2FCDo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2bc3bcea0b9e4fbfa201078bbdaff667~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=awxjNLLkPazAwarbu89DeMOIbmk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4505245fb0954b64af645aad5cb1a593~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741087100&x-signature=JGOPAoqeIBsprTMCiVDKVNPgPfo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"MySQL---八大日志文件","url":"https://juejin.cn/post/7475169143878221851","content":"redo log、undo log、bin log 三大核心日志梳理
\\nMySQL中存在着以下几种日志:重写日志(redo log)、回滚日志(undo log)、二进制日志(bin log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)、中继日志(relay log)、数据定义语句日志
\\nredo log是一种基于磁盘的数据结构,用来在MySQL宕机情况下将不完整的事务执行数据纠正,redo日志记录事务执行后的状态。
\\n当事务开始后,redo log就开始产生,并且随着事务的执行不断写入redo log file中。redo log file中记录了xxx页做了xx修改的信息,我们都知道数据库的更新操作会在内存中先执行,最后刷入磁盘。
\\nredo log就是为了恢复更新了内存但是由于宕机等原因没有刷入磁盘中的那部分数据。
\\nundo log主要用来回滚到某一个版本,是一种逻辑日志。undo log记录的是修改之前的数据,比如:当delete一条记录时,undolog中会记录一条对应的insert记录,从而保证能恢复到数据修改之前。在执行事务回滚的时候,就可以通过undo log中的记录内容并以此进行回滚。
\\nundo log还可以提供多版本并发控制下的读取(MVCC)
\\nMySQL的bin log日志是用来记录MySQL中增删改时的记录日志。简单来讲,就是当你的一条sql操作对数据库中的内容进行了更新,就会增加一条bin log日志。查询操作不会记录到bin log中。bin log最大的用处就是进行主从复制,以及数据库的恢复。
\\n通过下面的命令可以查看是否开启binlog日志
\\nshow VARIABLES like \'%log_bin%\'\\n
\\n开启binlog的方式如下:
\\nlog-bin=mysql-bin\\nserver-id=1\\nbinlog_format=ROW\\n
\\n其中log-bin指定日志文件的名称,默认会放到数据库目录下,可通过以下命令查看
\\nshow VARIABLES like \'%datadir%\'\\n
\\nerror log主要记录MySQL在启动、关闭或者运行过程中的错误信息,在MySQL的配置文件my.cnf中,可以通过log-error=/var/log/mysqld.log 执行mysql错误日志的位置。
\\n通过MySQL的命令
\\n show variables like \\"%log_error%\\";\\n
\\n也可以获取到错误日志的位置。
\\n慢查询日志用来记录执行时间超过指定阈值的SQL语句,慢查询日志往往用于优化生产环境的SQL语句。可以通过以下语句查看慢查询日志是否开启以及日志的位置:
\\n show variables like \\"%slow_query%\\";\\n
\\n慢查询日志的常用配置参数如下:
\\nslow_query_log=1 #是否开启慢查询日志,0关闭,1开启\\nslow_query_log_file=/usr/local/mysql/mysql-8.0.20/data/slow-log.log #慢查询日志地址(5.6及以上版本)\\nlong_query_time=1 #慢查询日志阈值,指超过阈值时间的SQL会被记录\\nlog_queries_not_using_indexes #表示未走索引的SQL也会被记录\\n
\\n分析慢查询日志一般会用专门的日志分析工具。找出慢SQL后可以通过explain关键字进行SQL分析,找出慢的原因。
\\ngeneral log 记录了客户端连接信息以及执行的SQL语句信息,通过MySQL的命令
\\nshow variables like \'%general_log%\';\\n
\\n可以查看general log是否开启以及日志的位置。
\\ngeneral log 可通过配置文件启动,配置参数如下:
\\ngeneral_log = on\\ngeneral_log_file = /usr/local/mysql/mysql-8.0.20/data/hecs-78422.log\\n
\\n普通查询日志会记录增删改查的信息,因此一般是关闭的。
\\n中继日志(relay log):用于主从服务器架构,从服务器用来存放主服务器二进制日志内容的一个中间件文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作
\\n记录数据定义语句执行的元数据操作
","description":"redo log、undo log、bin log 三大核心日志梳理 MySQL中存在着以下几种日志:重写日志(redo log)、回滚日志(undo log)、二进制日志(bin log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)、中继日志(relay log)、数据定义语句日志\\n\\n八大日志概括\\n\\n1. 重写日志(redo log)\\n\\nredo log是一种基于磁盘的数据结构,用来在MySQL宕机情况下将不完整的事务执行数据纠正,redo日志记录事务执行后的状态。\\n\\n当事务开始后…","guid":"https://juejin.cn/post/7475169143878221851","author":"后端程序员Aska","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T07:35:08.476Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ba4b9d7ab53b49b59cf9877e09b0d65c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135918&x-signature=cRiP7cdrmqJtu%2Buk%2FcXWmDwH%2FdQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6700ae09c2a4026a5d83cb12a171b2c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5ZCO56uv56iL5bqP5ZGYQXNrYQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741135918&x-signature=mAjwxBAC9IexIAjIdoqiIzZ0fhs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端"],"attachments":null,"extra":null,"language":null},{"title":"《你不知道的 JAVA》💘 喝 Java 咖啡配元组蛋糕,饿!","url":"https://juejin.cn/post/7475019081856925722","content":"俗话说二楼必须修在一楼上面,喝 Java 咖啡必须要配元组蛋糕。今天我们就从数据库查询开始讲起。
\\nList<Map>
与类型安全 🤔userDao.queryByLikeWith()
是一个数据库查询接口,我们通过这个接口能获得一个 List<Map>
类型的数据。
\\n\\n大家可以想想,有哪些框架的默认行为,会在 dao 查询中返回一个
\\nList<Map> | <Map>
结构呢?
List<Map> userListMap = userDao.queryByLikeWith(\\"someKey\\");\\nLong idUser1 = (Long) userListMap.get(0).get(\\"id\\");\\nString usernameUser1 = (String) userListMap.get(0).get(\\"username\\");\\nInteger ageUser2 = (Integer) userListMap.get(1).get(\\"age\\");\\n
\\n这样的查询方式有下面几个问题:
\\n要解决上面的问题,创建一个 User Class
就可以了。
class User {\\nprivate Long id;\\n private String username;\\n private Integer age;\\n}\\n\\nList<User> userList = userDao.queryUserByLikeWith(\\"someKey\\");\\nInteger userAge = userList.get(0).getAge();\\nLong userId = userList.get(0).getId();\\nString username = userList.get(1).getUsername();\\n
\\n虽然 user 的数量还是未知的,但至少 key 变成已知了,我们可以放心大胆的访问想要的属性。像这样的方式我们称为为查询结果封装一个 DTO。
\\n\\n\\nDTO 是一种数据封装对象,他没有业务逻辑,只承担数据封装的功能。现在我有一个问题,你认为 DTO 可变不可变?可在评论区讨论。
\\n
有一天新的需求来了。新功能依然需要查询 User 领域相关内容,但需要追加返回 address phone 字段并不再允许返回 id 字段(这是敏感字段)。\\n现在的我们不能再去修改 User 类,因为这会影响既存接口破坏系统稳定性。怎么办呢?好办,再创建一个 User Dto
不就行了。
class UserDetail {\\n private String username;\\n private Integer age;\\nprivate String address;\\nprivate String phone;\\n}\\n\\nList<User> userList = userDao.queryUserDetailByLikeWith(\\"someKey\\");\\nInteger userAge = userList.get(0).getAge();\\nString username = userList.get(1).getUsername();\\nString address = userList.get(0).getAddress();\\n
\\n这样问题就解决了。
\\n时光飞逝,项目新增功能越来越多,你也创建了更多返回特定字段的 DTO 对象。你可能觉得这很正常——项目越大类文件不就越多吗?很不幸,项目越大类文件越多的定义不包括 DTO 类型的文件。我们有一种专业术语来形容项目中存在过量的 DTO 文件,叫做 DTO 爆炸。
\\nclass UserDto1 {\\n}\\nclass UserDto2 {\\n}\\nclass UserDto3 {\\n}\\nclass UserDto4 {\\n}\\nclass UserDto5 {\\n}\\n
\\n说到这里你可能会问:使用一个 DTO 对象来全量返回所有字段可行吗?这显然不可行,因为以下几个原因:
\\n我们上面说过,DTO 是一种数据传输对象,它不包含业务功能,只用来承载数据。言下之意,这是一种「低价值对象」。但又偏偏要用一个文件来创建。如果你的项目中充数着大量的低价值对象,你就会遇到下面这两个经典问题:
\\n编程 10 分钟,命名就花了 8 分钟。除了老板大家都笑了。
\\n说了这么多,能不能不创建 DTO 呢?之前提到的 List<Map>
似乎能承担这个功能,但又有致命缺点:它是类型不安全的。那给 Map 加上泛型行不行呢?
// 我们期待 queryByLikeWith 返回两个字段:username 和 age 的值。\\nList<Map<String,String>> userListMap = userDao.queryByLikeWith(\\"someKey\\");\\n// 通过\\nString usernameUser1 = userListMap.get(0).get(\\"username\\");\\n// 报错\\nInteger ageUser2 = userListMap.get(1).get(\\"age\\");\\n
\\nageUser2 字段报错了。因为从数据库中查询出的字段类型可能有多种,但泛型表示的类型是固定的。那有没有一种数据类型,可以表示多种不同的类型呢?有的,这就是元组。
\\n元祖是一种数据结构,他看起来就像是数组,但他的长度和元素的类型是固定的。
\\nlet ourTuple: [number, boolean, string];\\nourTuple = [5, false, \'Coding God was here\'];\\n
\\n上面的 ourTuple 就是一个元组。他包含且三个元素且元素类型和顺序必须是 number boolean string
。尝试给它赋值其他类型的数据会报错:
// initialized incorrectly which throws an error\\nourTuple = [false, \'Coding God was mistaken\', 5];\\n
\\n获取这个元组中的元素的方式和数组一样,都是通过索引来获取:
\\nlet ourTuple: [number, boolean, string];\\nourTuple = [5, false, \'Coding God was here\'];\\n\\nlet firstElement: number = ourTuple[0]; \\nlet secondElement: boolean = ourTuple[1];\\nlet thirdElement: string = ourTuple[2]; \\n
\\n试想,如果把数据库中查询出的字段封装到一个元组上面,是不是就可以节省了创建 DTO 的开销了呢?说不如做,让我们试试看。
\\n由于 Java 没有原生的元组类型,所以我们使用 JOOQ 这个库为我们封装的元组对象。这个对象曾经在我们的之前的文章中提到过,它叫做 Record
\\n下面的代码中,
getUserTuple
方法返回了 Record2
这个对象……等等,为什么是 Record2
?这样的命名方式很少见!这里的 2 表示的是元组中有两个元素——回顾上面的内容,明确表示集合中元素的数量,是元组的职责范围之一。
void createOrderAndNotifyAdmin() {\\n Record2<String, Long> testUserA = getUserTuple();\\n String username = testUserA.value1();\\n Long userId = testUserA.value2();\\n orderService.createOrderBy(userId);\\n notifyService.notifyAdminBy(username);\\n}\\nprivate Record2<String, Long> getUserTuple() {\\n return dsl.select(USER.USERNAME, USER.ID).from(USER).where(USER.USERNAME.eq(\\"uniqueUserName\\")).fetchOne();\\n}\\n
\\n那既然这样是不是还有 Record3
呢?当然有,Record1..22
JOOQ 提供了这样长度的预定义元组对象供你使用,你想用哪个就用哪个。
private Record3<String, Long, String> getUserByUserNameEq(String username) {\\n return dsl.select(USER.USERNAME, USER.ID, USER.PASSWORD).from(USER).where(USER.USERNAME.eq(username)).fetchOne();\\n}\\n
\\n由于这次你查询了两个字段,所以使用 Record2
来表示这次的查询结果。再通过 testUserA.value1() testUserA.value2()
就能通过类型安全的方式访问到对应的值,然后传递给需要的业务方法。
附上 Record 类型的结构设计
\\npublic interface Record extends Fields, Attachable, Comparable<Record>, Formattable {\\n @NotNull\\n Row valuesRow();\\n}\\npublic interface Record2<T1, T2> extends Record {\\n @NotNull\\n Field<T1> field1();\\n @NotNull\\n Field<T2> field2();\\n}\\n\\nfinal class RecordImpl2<T1, T2> extends AbstractRecord implements InternalRecord, Record2<T1, T2> {\\n RecordImpl2(AbstractRow<?> row) {\\n super(row);\\n }\\n @Override\\n public RowImpl2<T1, T2> fieldsRow() {\\n return new RowImpl2<T1, T2>(field1(), field2());\\n}\\n
\\n讲到这里你应该能明白 JOOQ 中 Record
对象的大体设计思路了吧?为什么 JOOQ 不直接返回一个 Map
来表示查询结果而是专门设计了 Record..N
这样的类型?
因为 JOOQ 希望利用提前设计的 Record
类型,尽可能在框架层面确保开发者在 CRUD 的过程中随时都获得类型安全的保护;尽量减少开发者为了编译时安全这个重要特性去做的手工操作(比如创建一个 DTO);也一定程度上避免了 DTO 爆炸所带来的隐患。
到这里你可能还有一些疑问,比如:
\\nMap
看起来元组失去了通过 key 来访问 value 的特性。关于代码示例,我做了一个开箱即用的仓库供大家取用 github.com/ccmjga/mjga… 如果你不要忘记给它一个 Star,便可得好运相随。剩下的问题,如果大家的 Star 给力,我就在后面的章节中一一为大家解答。
\\n