diff --git a/.gitignore b/.gitignore index 670cd025a..58330892c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,12 @@ docs/java/*.md docs/java/*.java deploy.sh package.json -spring-cloud/* \ No newline at end of file +spring-cloud/* +/java/*.java +/java/nacos/*.* +#目录下java文件夹 +/java/*.java +#目录下java文件夹所有内容 +/java/*.* +.temp +.cache diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..10b731c51 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/JavaPlusDoc.iml b/.idea/JavaPlusDoc.iml new file mode 100644 index 000000000..24643cc37 --- /dev/null +++ b/.idea/JavaPlusDoc.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..cd8384564 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..58918f503 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..e8d7c949f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 300f711af..b217128c5 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,394 @@ -# JavaPlusDoc +# JavaPlus 技术文档平台 -> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),感谢大家的阅读 +## 项目概述 -```js -yarn install // 安装依赖 -``` +本平台是一个战略性的Java技术知识体系,构建了从基础到高级的微服务分布式系统完整架构图谱。平台采用企业级文档标准,支持全文检索、多端适配,为组织内不同层级的技术人员(初学者、中级开发者、高级架构师、技术决策者)提供系统化的技术参考和能力提升路径。 -```js -yarn serve // 预览 -``` +**平台价值**:提升团队技术能力,加速项目交付,降低技术决策风险,支持业务创新 + +**网站地址**:[https://webvueblog.github.io/JavaPlusDoc/](https://webvueblog.github.io/JavaPlusDoc/) + +## 为爱发电 + +
+

🙏 支持JavaPlus技术文档平台

+

如果您觉得本文档对您的学习和工作有所帮助,欢迎扫描下方二维码进行打赏支持。您的每一份鼓励都是我们持续创作优质内容的动力!

+
+ 微信收款码 +
+

感谢您的支持与鼓励!

+

您的赞助将用于平台维护、内容更新与技术研究

+
+ Spring + Java + Redis + MySQL + Docker + K8s + Vue +
+
+ +## 项目亮点 + +- **分层知识体系**:按照初级、中级、高级三个层次组织内容,满足不同水平读者需求 +- **全面技术覆盖**:从Java基础到分布式架构,全面覆盖企业级开发所需技术栈 +- **实用案例导向**:结合实际项目案例,提供可落地的最佳实践和解决方案 +- **图文并茂**:通过图表、代码示例和流程图直观展示复杂概念 +- **持续更新迭代**:定期更新技术内容,保持与行业最新发展同步 +- **企业级应用**:提供企业级架构设计方案和最佳实践,支持技术决策与创新 + +## 战略价值与业务贡献 + +- **技术能力建设**:系统化培养团队从初级到高级的技术梯队,支撑业务持续发展 +- **降低技术风险**:提供经过验证的架构方案和最佳实践,减少技术决策失误 +- **加速项目交付**:通过标准化技术方案和组件复用,显著提高研发效率 +- **促进技术创新**:整合行业前沿技术,为业务创新提供技术支撑 +- **知识资产沉淀**:将团队核心技术经验转化为可持续的组织知识资产 + +## 核心技术体系 + +### 后端技术架构 + +

+ Spring  + Spring Boot  + Go  + MySQL  + Redis  + MongoDB  + RabbitMQ  + ElasticSearch  + Kafka  + Spring Security  + Java  +

+ +### 前端技术架构 + +

+ Vue3  + TypeScript  + Ant Design  + Node.js  + Vite  +

+ +### 云原生 & DevOps 体系 + +

+ Docker  + Kubernetes  + Jenkins  + Nginx  + Prometheus  + Grafana  + Git  +

+ +## 精选技术文章 + +
+ + +
+

+ Redis Redis核心技术 +

+ +
+ + +
+

+ 微服务 微服务架构 +

+ +
+ + +
+

+ 高并发 高并发设计 +

+ +
+ + +
+

+ 分布式 分布式架构 +

+ +
+ + +
+

+ 云原生 云原生技术 +

+ +
+ + +
+

+ 安全 系统安全 +

+ +
+ + +
+

+ 微信小程序 微信小程序开发 +

+ +
+ + +
+

+ Go语言 GO语言开发 +

+ +
+ +
+ +## 文档内容 + +本文档库按照技术深度和应用场景,将内容分为三个层次: + +| 层次 | 适合人群 | 主要内容 | 学习目标 | 应用场景 | +|------|---------|----------|---------|----------| +| **基础篇** | 初学者、转行人员 | Java基础语法、面向对象、集合框架、异常处理 | 掌握Java开发基础,能够独立完成简单功能开发 | 新员工培训、技术栈转型 | +| **进阶篇** | 中级开发者 | 多线程并发、JVM原理、设计模式、框架应用 | 深入理解Java核心机制,能够设计复杂业务模块 | 业务系统开发、性能优化 | +| **高级篇** | 高级开发者、架构师、技术管理者 | 分布式架构、高并发设计、性能优化、微服务治理 | 掌握系统架构设计,解决企业级技术难题 | 架构升级、系统重构、技术创新 | + +核心技术领域及其业务价值: + +|现有技术 | 主要内容 | 业务价值 | 应用场景 | +|---------|----------|----------|----------| +| **Java核心** | JVM原理、多线程并发、内存模型、性能调优 | 提升系统性能,降低资源成本 | 核心业务系统、高性能服务 | +| **GO语言** | 并发编程、网络服务、微服务架构、云原生应用 | 简化并发开发,提高开发效率 | 微服务、API网关、云原生应用 | +| **分布式架构** | 微服务设计、服务注册发现、负载均衡、熔断降级 | 支持业务快速扩展,提高系统可用性 | 大型业务平台、多区域部署 | +| **数据存储** | 关系型数据库、NoSQL、分库分表、数据一致性 | 保障数据安全,支持海量数据处理 | 用户数据分析、交易系统 | +| **中间件技术** | 消息队列、缓存、搜索引擎、任务调度 | 提高系统集成能力,增强业务弹性 | 跨系统集成、峰值流量应对 | +| **云原生** | 容器化、服务网格、Kubernetes、云平台 | 降低运维成本,提高资源利用率 | 弹性伸缩、混合云部署 | +| **DevOps** | CI/CD、自动化测试、监控告警、日志管理 | 加速交付周期,提高发布质量 | 持续集成/部署、自动化运维 | + +## 文档结构 + +文档采用多层次结构组织,便于不同层级技术人员查阅: -```js -yarn docs:dev ``` +├── 基础技术体系 +│ ├── Java核心技术 +│ │ ├── 基本数据类型 +│ │ ├── 面向对象编程 +│ │ ├── 集合框架 +│ │ └── 异常处理 +│ ├── GO语言技术 +│ │ ├── 基础语法 +│ │ ├── 并发编程 +│ │ ├── 标准库应用 +│ │ └── Web服务开发 +│ ├── 开发环境搭建 +│ └── 编程规范与最佳实践 +├── 企业级应用框架 +│ ├── 微服务架构 +│ │ ├── Spring Cloud生态 +│ │ ├── 服务注册与发现 +│ │ ├── 配置中心 +│ │ └── API网关 +│ ├── 数据访问与存储 +│ │ ├── MySQL优化 +│ │ ├── Redis缓存 +│ │ └── 分库分表 +│ ├── 安全架构 +│ └── 性能优化 +└── 技术战略与创新 + ├── 架构演进路线 + │ ├── 单体到微服务 + │ ├── 传统部署到云原生 + │ └── 技术栈升级策略 + ├── 技术风险管控 + ├── 创新技术应用 + └── 技术团队建设 +``` + +## 使用指南 + +### 领导/管理者 + +1. **战略决策参考** + - 通过「战略价值与业务贡献」了解平台对业务的支撑作用 + - 参考「技术选型指南」进行技术投资决策 + - 利用「架构演进路线」规划技术发展方向 + +2. **团队建设工具** + - 基于「分层知识体系」制定团队培训计划 + - 使用「技术评估标准」进行人才评估和梯队建设 + - 通过「知识管理策略」促进团队知识沉淀和共享 + +3. **项目管理辅助** + - 利用「技术评审标准」提高项目质量 + - 参考「质量保障体系」降低项目风险 + - 通过「技术债务管理」优化长期技术投资 + +### 初学者 + +1. **入门学习路径** + - 从「基础篇」开始,按顺序学习Java基础知识 + - 通过实践案例巩固基础概念 + - 利用文档中的图表辅助理解抽象概念 + +2. **实践指导** + - 参考「编程规范」养成良好编码习惯 + - 学习「开发环境搭建」快速进入开发状态 + - 通过「常见问题解答」解决入门障碍 + +3. **进阶准备** + - 完成基础学习后,了解「进阶篇」知识图谱 + - 制定个人学习计划,为技术进阶做准备 + - 参与实践项目,应用所学知识 + +### 中级开发者 + +1. **深入学习** + - 重点关注「进阶篇」内容,深入理解Java高级特性 + - 学习主流框架的原理和最佳实践 + - 通过项目案例提升实际应用能力 + +2. **技术拓展** + - 学习「设计模式」提升代码设计能力 + - 掌握「性能优化」技巧提高系统效率 + - 了解「微服务基础」为架构升级做准备 + +3. **实践提升** + - 参与复杂业务模块的设计和开发 + - 解决实际项目中的技术难题 + - 尝试理解「高级篇」中的架构设计思想 + +### 高级开发者/架构师 + +1. **架构设计** + - 专注「高级篇」内容,掌握分布式系统设计原则 + - 研究性能优化和高可用架构方案 + - 学习「技术战略」内容,提升技术决策能力 + +2. **技术创新** + - 探索前沿技术在企业中的应用价值 + - 设计创新架构解决方案 + - 参与技术选型和架构评审 + +3. **团队引领** + - 指导团队成员技术成长 + - 推动技术最佳实践在团队中的应用 + - 参与文档内容的贡献和完善 + +## 团队协作与知识管理 + +本平台支持企业级知识管理: -## 学前必读 +- **知识资产化**:将团队经验转化为可复用的知识资产 +- **版本化管理**:基于Git的文档版本控制,支持变更追踪 +- **质量保障**:技术内容经过专家评审,确保准确性和实用性 +- **定制化支持**:可根据组织需求定制专属知识库 +- **持续演进**:建立反馈机制,持续优化内容质量 -哪吒希望能为开发人员提供最大程度的愉悦开发体验。提供便捷的阅读文档,帮助开发小团体高效率的工作进度,并维护本站架构文档。 +## 维护与更新策略 -## 留言评论 +平台由专业技术团队维护,采用企业级内容管理流程: -因为目前没有留言功能,请拉到文章底部,跳转到对应的 Github Issues,在 Issues 留言回复。 +- **定期更新计划**:每季度进行内容审核和更新 +- **技术趋势跟踪**:持续整合行业前沿技术和最佳实践 +- **用户反馈闭环**:建立反馈收集和内容优化的闭环机制 +- **版本发布规划**:重大更新按版本发布,提供变更说明 -## 感谢指正 +### 版本管理机制 -指正不胜感激,无以回报。 +- **主版本**:重大内容架构调整或技术体系更新(如v2.0、v3.0) +- **次版本**:新增技术领域或大量内容更新(如v1.1、v1.2) +- **修订版本**:内容修正和小规模更新(如v1.0.1、v1.0.2) +- **变更日志**:每次更新都提供详细的变更说明 -## 声明 +### 内容定制与扩展 -文档仅适合本人食用!!! +企业可以基于本平台进行定制化扩展: +- **行业特化**:增加特定行业的技术应用案例 +- **企业实践**:整合企业内部最佳实践和技术标准 +- **团队培训**:定制化的培训课程和学习路径 +- **评估体系**:与企业人才评估体系对接 +欢迎通过[Issues](https://github.com/webVueBlog/JavaPlusDoc/issues)提交反馈和建议,帮助我们持续提升平台价值。 -## 勘误及提问 +### 技术理念 -如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。 +> 进一寸有一寸的欣喜 —— 持续学习,每天进步一点点! -如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 +作者致力于技术的学习与分享,通过开源项目和技术文章帮助更多开发者成长。欢迎通过GitHub、掘金等与作者交流,共同进步! -## License -所有文章采用[知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/)进行许可。 +| 输出信息 | 含义 | | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------- | +| `Enumerating objects: 1324, done.` | 正在枚举(统计)一共 1324 个对象(包括文件、文件夹、提交等)。 | | +| `Counting objects: 100% (1324/1324), done.` | 统计完毕。 | | +| `Delta compression using up to 32 threads` | 使用最多 32 个线程进行增量压缩(优化数据上传体积)。 | | +| `Compressing objects: 100% (1293/1293), done.` | 完成压缩 1293 个对象。 | | +| \`Writing objects: 100% (1324/1324), 106.13 MiB | 1.49 MiB/s, done.\` | 将所有对象写入 Git 仓库,合计 106MB,速度为 1.49MB/s。 | +| `Total 1324 (delta 559), reused 0 (delta 0), pack-reused 0 (from 0)` | 一共上传了 1324 个对象,其中有 559 个是差异对象(delta)。 | | +| `remote: Resolving deltas: 100% (559/559), done.` | GitHub 远端解析并合并了 559 个差异对象。 | | +| `To https://github.com/webVueBlog/JavaPlusDoc.git` | 上传目标仓库为 `webVueBlog/JavaPlusDoc`。 | | +| `+ cda33eb...cf5b0b4 dist -> gh-pages (forced update)` | 将本地 `dist` 内容强制推送(force push)到了远程的 `gh-pages` 分支,旧的提交是 `cda33eb`,新的是 `cf5b0b4`。 | | +| ✅ `dist目录上传成功,部署完成!` | 你部署成功了!dist 目录已上传并生效,一般这是构建后的静态页面,用于 GitHub Pages 展示。 | | diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7276543cf..c7fa6985e 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,275 +1,1152 @@ module.exports = { - title: 'Jeskson文档', - description: '架构师', - base: '/JavaPlusDoc/', - theme: 'reco', - head: [ - ['meta', { - name: 'viewport', - content: 'width=device-width,initial-scale=1,user-scalable=no' - }] - ], - plugins: [ - '@vuepress/medium-zoom', - // 平滑滚动 - ["vuepress-plugin-smooth-scroll"], - // 页面加载进度条 - ["vuepress-plugin-nprogress"], - // 动态标题 - [ - "vuepress-plugin-dynamic-title", - { - showIcon: "/favicon.ico", - showText: "😃 欢迎回来!", - hideIcon: "/favicon.ico", - hideText: "👋 再见了!", - recoverTime: 2000 - } - ], - // SEO - [ - "vuepress-plugin-seo", - { - siteTitle: (_, $site) => $site.title, - description: (_, $site) => $site.description, - author: (_, $site) => $site.themeConfig.author || $site.title, - tags: (_, $page) => $page.frontmatter.tags, - twitterCard: _ => "summary_large_image", - type: $page => ($page.regularPath === "/" ? "website" : "article") - } - ], - // 阅读时间 - [ - "vuepress-plugin-reading-time", - { - excludes: ["/exclude-page.html"], - wordPerMinute: 300 - } - ], - // Google Analytics - [ - "vuepress-plugin-google-analytics", - { - ga: "UA-XXXXXXXXX-X" - } - ], - '@vuepress-reco/vuepress-plugin-loading-page', - [ - 'dynamic-title', - { - showIcon: '/favicon.ico', - showText: '(/≧▽≦/)咦!又好了!', - hideIcon: '/failure.ico', - hideText: '(●—●)喔哟,崩溃啦!', - recoverTime: 2000, - }, - ], - // 看板娘 - [ - "@vuepress-reco/vuepress-plugin-kan-ban-niang", - { - theme: ["blackCat"], - clean: true, - height: 260, - modelStyle: { - width: '100px', - position: "fixed", - right: "0px", - bottom: "0px", - opacity: "0.9", - zIndex: 99999, - objectFit: 'cover', - } - } - ], - // Medium Zoom 图片缩放 - ["@vuepress/plugin-medium-zoom"], - // 返回顶部 - ["vuepress-plugin-back-to-top"], - ['vuepress-plugin-code-copy', true] - ], - locales: { - '/': { - lang: 'zh-CN' - } - }, - themeConfig: { - lastUpdated: '上次更新', - subSidebar: 'auto', - nav: [{ - text: '首页', - link: '/' - }, - { - text: '网站', - link: 'https://webvueblog.github.io/JavaPlusDoc/' - }, - { - text: '星星', - link: 'https://github.com/webVueBlog/JavaPlusDoc' - }, - { - text: '作者', - items: [{ - text: 'Github', - link: 'https://github.com/webVueBlog' - }] - } - ], - sidebar: [{ - title: '架构师', - path: '/', - collapsable: false, // 不折叠 - children: [{ - title: "学前必读", - path: "/" - }] - }, - { - title: '消息队列', - path: '/messagequeue/why-mq', - collapsable: false, // 不折叠 - children: [{ - title: "为什么使用消息队列", - path: "/messagequeue/why-mq" - }, { - title: "如何保证消息队列的高可用", - path: "/messagequeue/how-to-ensure-high-availability-of-message-queues" - }, { - title: "如何保证消息不被重复消费", - path: "/messagequeue/how-to-ensure-that-messages-are-not-repeatedly-consumed" - }, { - title: "如何保证消息的可靠性传输", - path: "/messagequeue/how-to-ensure-the-reliable-transmission-of-messages" - }] - }, - { - title: 'Redis', - path: '/redis/rumen', - collapsable: false, // 不折叠 - children: [{ - title: "入门教程", - path: "/redis/rumen" - }, { - title: "缓存雪崩、穿透、击穿", - path: "/redis/xuebeng-chuantou-jichuan" - }] - }, - { - title: '操作系统', - path: '/cs/os', - collapsable: false, // 不折叠 - children: [{ - title: "计算机操作系统", - path: "/cs/os" - }, { - title: "计算机网络", - path: "/cs/wangluo" - }] - }, - { - title: 'Java进阶', - path: '/java-up/nginx', - collapsable: false, // 不折叠 - children: [{ - title: "浅出搞懂Nginx", - path: "/java-up/nginx" - }, { - title: "Nginx服务器SSL证书安装部署", - path: "/java-up/ssl" - }, { - title: "SpringAOP扫盲", - path: "/java-up/aop-log" - }, { - title: "SpringIoC扫盲", - path: "/java-up/ioc" - }, { - title: "超详细Netty入门", - path: "/java-up/netty" - }] - }, - { - title: "Java基础", - path: '/basic-grammar/basic-data-type', - collapsable: false, // 不折叠 - children: [{ - title: "Java基本数据类型", - path: "/basic-grammar/basic-data-type" - }, - { - title: "基本数据类型的转换", - path: "/basic-grammar/type-cast" - }, - { - title: "基本数据类型缓存池剖析", - path: "/basic-grammar/int-cache" - }, - { - title: "掌握运算符", - path: "/basic-grammar/operator" - }, - { - title: "流程控制语句", - path: "/basic-grammar/flow-control" - }, - { - title: "深入解读String类", - path: "/basic-grammar/string-source" - }, - { - title: "字符串常量池", - path: "/basic-grammar/constant-pool" - }, - { - title: "StringBuilder和StringBuffer", - path: "/basic-grammar/builder-buffer" - }, - { - title: "如何比较两个字符串相等", - path: "/basic-grammar/equals" - }, - { - title: "万物皆对象", - path: "/basic-grammar/object-class" - }, - { - title: "Java中的包", - path: "/basic-grammar/package" - }, - { - title: "Java变量", - path: "/basic-grammar/var" - }, - { - title: "Java方法", - path: "/basic-grammar/method" - }, - { - title: "构造方法", - path: "/basic-grammar/construct" - }, - { - title: "抽象类", - path: "/basic-grammar/abstract" - }, - { - title: "接口和内部类", - path: "/basic-grammar/interface" - }, - { - title: "封装继承多态", - path: "/basic-grammar/encapsulation-inheritance-polymorphism" - }, - { - title: "this与super关键字", - path: "/basic-grammar/this-super" - }, - { - title: "不可变对象", - path: "/basic-grammar/immutable" - }, - ], - }, - ] - } -} \ No newline at end of file + title: "Jeskson文档-微服务分布式系统架构", + description: "Jeskson文档-架构师", + base: "/JavaPlusDoc/", + theme: "reco", + head: [ + [ + "meta", + { + name: "viewport", + content: "width=device-width,initial-scale=1,user-scalable=no", + }, + ], + ["script", { src: "/JavaPlusDoc/js/sidebar-scroll.js" }], + ["script", { src: "/JavaPlusDoc/js/sidebar-enhance.js" }], + ["script", { src: "/JavaPlusDoc/js/report-site-enhance.js" }], + ], + plugins: [ + ['"@vuepress/plugin-medium-zoom"'], // 图片放大 + ["vuepress-plugin-smooth-scroll"], // 平滑滚动 + ["vuepress-plugin-nprogress"], // 加载进度条 + ["vuepress-plugin-mermaidjs"], // Mermaid 图表 + ["@vuepress-reco/vuepress-plugin-loading-page"], // 页面加载动画 + [ + "dynamic-title", + { + showIcon: "/favicon.ico", + showText: "(/≧▽≦/)咦!又好了!", + hideIcon: "/failure.ico", + hideText: "(●—●)喔哟,崩溃啦!", + recoverTime: 2000, + }, + ], + [ + "@vuepress-reco/vuepress-plugin-kan-ban-niang", + { + theme: ["blackCat"], + clean: true, + height: 260, + modelStyle: { + width: "100px", + position: "fixed", + right: "0px", + bottom: "0px", + opacity: "0.9", + zIndex: 99999, + objectFit: "cover", + }, + }, + ], + ["vuepress-plugin-back-to-top"], + ["vuepress-plugin-code-copy", true], + ], + locales: { + "/": { + lang: "zh-CN", + }, + }, + themeConfig: { + lastUpdated: "上次更新", + subSidebar: "auto", + nav: [ + { + text: "首页", + link: "/", + }, + { + text: "学习路线", + items: [ + { text: "入门指南", link: "/" }, + { text: "基础到进阶", link: "/basic-grammar/basic-data-type" }, + { text: "架构师成长", link: "/high-concurrency/why-cache" }, + ], + }, + { + text: "技术分类", + items: [ + { text: "Java基础", link: "/basic-grammar/basic-data-type" }, + { text: "Java进阶", link: "/java-up/nginx" }, + { text: "分布式架构", link: "/high-concurrency/why-cache" }, + { text: "微服务", link: "/worker/1" }, + { text: "数据库", link: "/mysql/mysql" }, + { text: "运维与部署", link: "/linux/linux" }, + { text: "Docker", link: "/docker/docker-tutorial" }, + { text: "产品系列", link: "/products/" }, + ], + }, + { + text: "实战案例", + items: [ + { text: "系统设计", link: "/worker/1" }, + { text: "性能优化", link: "/high-concurrency/why-cache" }, + { text: "项目实践", link: "/java-up/nginx" }, + ], + }, + { + text: "资源", + items: [ + { + text: "官方文档", + link: "https://webvueblog.github.io/JavaPlusDoc/", + }, + { + text: "GitHub仓库", + link: "https://github.com/webVueBlog/JavaPlusDoc", + }, + { text: "作者主页", link: "https://github.com/webVueBlog" }, + ], + }, + ], + sidebar: [ + { + title: "文档指南", + path: "/", + collapsable: false, // 不折叠 + children: [ + { + title: "学前必读", + path: "/", + }, + ], + }, + { + title: "Java核心技术", + collapsable: false, // 不折叠 + children: [ + { + title: "基础篇", + collapsable: true, + children: [ + { title: "JVM深入解析", path: "/e/JVM深入解析" }, + { title: "JDK开发工具详解", path: "/e/JDK开发工具详解" }, + { title: "Java实战演示", path: "/e/Java实战演示" }, + { + title: "Java、JDK、JRE、JVM详解", + path: "/basic-grammar/java-jdk-jre-jvm", + }, + { + title: "Java基本数据类型", + path: "/basic-grammar/basic-data-type", + }, + { + title: "数据类型转换", + path: "/basic-grammar/type-cast", + }, + { + title: "数据类型缓存池", + path: "/basic-grammar/int-cache", + }, + { + title: "运算符", + path: "/basic-grammar/operator", + }, + { + title: "流程控制", + path: "/basic-grammar/flow-control", + }, + { + title: "String类详解", + path: "/basic-grammar/string-source", + }, + { + title: "字符串常量池", + path: "/basic-grammar/constant-pool", + }, + { + title: "StringBuilder和Buffer", + path: "/basic-grammar/builder-buffer", + }, + { + title: "字符串比较", + path: "/basic-grammar/equals", + }, + { + title: "面向对象基础", + path: "/basic-grammar/object-class", + }, + { + title: "Java包机制", + path: "/basic-grammar/package", + }, + { + title: "变量与方法", + path: "/basic-grammar/var", + }, + { + title: "构造方法", + path: "/basic-grammar/construct", + }, + { + title: "抽象类", + path: "/basic-grammar/abstract", + }, + { + title: "接口和内部类", + path: "/basic-grammar/interface", + }, + { + title: "封装继承多态", + path: "/basic-grammar/encapsulation-inheritance-polymorphism", + }, + { + title: "this与super关键字", + path: "/basic-grammar/this-super", + }, + { + title: "不可变对象", + path: "/basic-grammar/immutable", + }, + ], + }, + { + title: "集合框架", + collapsable: true, + children: [ + { + title: "集合框架概览", + path: "/basic-grammar/gailan", + }, + { + title: "ArrayList详解", + path: "/basic-grammar/arraylist", + }, + { + title: "LinkedList详解", + path: "/basic-grammar/linkedlist", + }, + { + title: "栈Stack详解", + path: "/basic-grammar/stack", + }, + { + title: "HashMap详解", + path: "/basic-grammar/hashmap", + }, + { + title: "LinkedHashMap详解", + path: "/basic-grammar/linkedhashmap", + }, + { + title: "TreeMap详解", + path: "/basic-grammar/treemap", + }, + { + title: "ArrayDeque详解", + path: "/basic-grammar/ArrayDeque", + }, + { + title: "PriorityQueue详解", + path: "/basic-grammar/PriorityQueue", + }, + { + title: "ArrayList vs LinkedList", + path: "/basic-grammar/array-linked-list", + }, + { + title: "Java泛型", + path: "/basic-grammar/generic", + }, + { + title: "Iterator和Iterable", + path: "/basic-grammar/iterator-iterable", + }, + { + title: "foreach循环陷阱", + path: "/basic-grammar/fail-fast", + }, + { + title: "Comparable和Comparator", + path: "/basic-grammar/comparable-omparator", + }, + { + title: "WeakHashMap详解", + path: "/basic-grammar/WeakHashMap", + }, + ], + }, + { + title: "JVM与性能", + collapsable: true, + children: [ + { + title: "JVM基础", + path: "/aJava/jvm", + }, + { + title: "JVM内存区域", + path: "/aJava/JVM内存区域", + }, + { + title: "程序计数器", + path: "/aJava/程序计数器", + }, + { + title: "线程", + path: "/aJava/线程", + }, + ], + }, + { + title: "并发编程", + collapsable: true, + children: [ + { + title: "多线程入门", + path: "/thread/thread", + }, + { + title: "线程执行结果获取", + path: "/thread/callable-future-futuretask", + }, + { + title: "线程的6种状态", + path: "/thread/thread-state-and-method", + }, + { + title: "深入浅出Java线程池ThreadPoolExecutor", + path: "/thread/threadpool-executor", + }, + { + title: "synchronized和lock", + path: "/jobPro/synchronized", + }, + ], + }, + { + title: "IO与网络", + collapsable: true, + children: [ + { + title: "JavaIO知识体系", + path: "/java-up/shangtou", + }, + { + title: "文件流", + path: "/java-up/file-path", + }, + { + title: "字节流与字符流", + path: "/java-up/reader-writer", + }, + { + title: "转换流", + path: "/java-up/char-byte", + }, + { + title: "序列化和反序列化", + path: "/java-up/serialize", + }, + { + title: "NIO基础", + path: "/nio/nio", + }, + { + title: "NIO vs BIO vs AIO", + path: "/nio/BIONIOAIO", + }, + { + title: "Buffer和Channel", + path: "/nio/buffer-channel", + }, + { + title: "网络编程实践", + path: "/nio/network-connect", + }, + { + title: "IO模型", + path: "/nio/moxing", + }, + { + title: "IO多路复用", + path: "/jobPro/IO", + }, + { + title: "Netty入门", + path: "/java-up/netty", + }, + ], + }, + ], + }, + { + title: "分布式架构", + collapsable: false, + children: [ + { + title: "缓存系统", + collapsable: true, + children: [ + { + title: "缓存使用场景", + path: "/high-concurrency/why-cache", + }, + { + title: "Redis vs Memcached", + path: "/high-concurrency/redis-single-thread-model", + }, + { + title: "Redis数据类型", + path: "/high-concurrency/redis-data-types", + }, + { + title: "Redis过期策略", + path: "/high-concurrency/redis-expiration-policies-and-lru", + }, + { + title: "Redis高并发与高可用", + path: "/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis", + }, + { + title: "Redis主从架构", + path: "/high-concurrency/redis-master-slave", + }, + { + title: "Redis持久化", + path: "/high-concurrency/redis-persistence", + }, + { + title: "Redis哨兵集群", + path: "/high-concurrency/redis-sentinel", + }, + { + title: "Redis应用场景", + path: "/jobPro/redis", + }, + { + title: "Redis key过期问题解决方案", + path: "/redis/redis-key-expiration", + }, + { + title: "SpringBoot整合Redis", + path: "/java-up/redis-springboot", + }, + { + title: "缓存雪崩、穿透、击穿", + path: "/redis/xuebeng-chuantou-jichuan", + }, + { + title: "深入分析Redis Lua脚本运行原理", + path: "/redis/redis-lua", + }, + ], + }, + { + title: "消息队列", + collapsable: true, + children: [ + { + title: "为什么使用消息队列", + path: "/messagequeue/why-mq", + }, + { + title: "消息队列高可用", + path: "/messagequeue/how-to-ensure-high-availability-of-message-queues", + }, + { + title: "消息不重复消费", + path: "/messagequeue/how-to-ensure-that-messages-are-not-repeatedly-consumed", + }, + { + title: "消息可靠传输", + path: "/messagequeue/how-to-ensure-the-reliable-transmission-of-messages", + }, + { + title: "消息顺序性", + path: "/messagequeue/how-to-ensure-the-order-of-messages", + }, + { + title: "消息队列延时问题", + path: "/messagequeue/mq-time-delay-and-expired-failure", + }, + { + title: "消息队列设计", + path: "/messagequeue/mq-design", + }, + { + title: "RabbitMQ入门", + path: "/java-up/rabbitmq", + }, + { + title: "Kafka基础", + path: "/power/kafka", + }, + { + title: "Kafka集群", + path: "/power/kafkas", + }, + { + title: "Kafka消费者", + path: "/power/comsumer", + }, + ], + }, + { + title: "搜索引擎", + collapsable: true, + children: [ + { + title: "ES分布式架构原理", + path: "/searchEngine/es-architecture", + }, + { + title: "ES写入数据原理", + path: "/searchEngine/es-write-query-search", + }, + { + title: "ES查询效率优化", + path: "/searchEngine/es-optimizing-query-performance", + }, + { + title: "ES生产集群部署", + path: "/searchEngine/es-production-cluster", + }, + ], + }, + { + title: "微服务架构", + collapsable: true, + children: [ + { + title: "微服务优雅上下线", + path: "/worker/3", + }, + { + title: "滚动部署", + path: "/worker/1", + }, + { + title: "Nacos优雅停机", + path: "/worker/2", + }, + { + title: "Dubbo基础", + path: "/dubbo/dubbo", + }, + ], + }, + { + title: "数据库技术", + collapsable: true, + children: [ + { + title: "SQL执行过程", + path: "/mysql/mysql", + }, + { + title: "MySQL事务实现", + path: "/mysql/shiwu-shixia", + }, + { + title: "深入理解MySQL事务", + path: "/mysql/lijie-shiwu", + }, + { + title: "MySQL锁机制详解", + path: "/mysql/mysql-locks", + }, + { + title: "MySQL和Redis数据一致性", + path: "/mysql/redis-shuju-yizhixing", + }, + { + title: "MyISAM vs InnoDB", + path: "/jobPro/InnoDB", + }, + { + title: "MongoDB基础", + path: "/java-up/mongodb", + }, + { + title: "MongoDB备份与恢复", + path: "/java-up/mongodb-backup-restore", + }, + { + title: "MyBatis-Plus入门", + path: "/MyBatis-Plus/getting-started", + }, + { + title: "MyBatis-Plus接口", + path: "/MyBatis-Plus/service-interface", + }, + ], + }, + ], + }, + { + title: "运维与部署", + collapsable: false, + children: [ + { + title: "Docker容器技术", + collapsable: true, + children: [ + { + title: "Docker教程", + path: "/docker/docker-tutorial", + }, + { + title: "docker架构", + path: "/docker/docker架构", + }, + { + title: "Docker安装指南", + path: "/docker/docker-install", + }, + { + title: "Docker配置国内源", + path: "/docker/docker-mirror", + }, + { + title: "Docker镜像管理", + path: "/docker/docker-images", + }, + { + title: "Docker容器管理", + path: "/docker/docker-containers", + }, + { + title: "Docker网络配置", + path: "/docker/docker-network", + }, + { + title: "Docker数据卷管理", + path: "/docker/docker-volumes", + }, + { + title: "Dockerfile最佳实践", + path: "/docker/docker-dockerfile", + }, + { + title: "Docker Compose详解", + path: "/docker/docker-compose", + }, + { + title: "Docker Swarm集群", + path: "/docker/docker-swarm", + }, + { + title: "Docker安全最佳实践", + path: "/docker/docker-security", + }, + { + title: "Docker生产环境部署", + path: "/docker/docker-production", + }, + ], + }, + { + title: "基础设施", + collapsable: true, + children: [ + { + title: "云计算基础", + path: "/aJava/云计算是什么", + }, + { + title: "mysql是什么", + path: "/aJava/mysql是什么", + }, + { + title: "mogodb是什么", + path: "/aJava/mogodb是什么", + }, + { + title: "安全组是什么", + path: "/aJava/安全组是什么", + }, + { + title: "CDN是什么", + path: "/aJava/CDN是什么", + }, + { + title: "nginx是什么", + path: "/aJava/nginx是什么", + }, + { + title: "tomcat是什么", + path: "/aJava/tomcat是什么", + }, + { + title: "Eureka是什么", + path: "/aJava/Eureka是什么", + }, + { + title: "nacos是什么", + path: "/aJava/nacos是什么", + }, + { + title: "Zookeeper是什么", + path: "/aJava/Zookeeper是什么", + }, + { + title: "ElasticSearch是什么", + path: "/aJava/ElasticSearch是什么", + }, + { + title: "微服务基础", + path: "/aJava/微服务是什么", + }, + { + title: "HDFS基础", + path: "/aJava/HDFS是什么", + }, + { + title: "块存储", + path: "/aJava/块存储是什么", + }, + { + title: "对象存储", + path: "/aJava/对象存储是什么", + }, + { + title: "存储快照", + path: "/aJava/存储快照是什么", + }, + { + title: "负载均衡", + path: "/aJava/负载均衡是什么", + }, + { + title: "灰度发布", + path: "/aJava/什么是灰度发布", + }, + ], + }, + { + title: "DevOps工具链", + collapsable: true, + children: [ + { + title: "Jenkins基础", + path: "/aJava/Jenkins是什么", + }, + { + title: "Ansible基础", + path: "/aJava/Ansible是什么", + }, + { + title: "DevOps概念", + path: "/aJava/DevOps是什么", + }, + { + title: "WAF和DDOS防护", + path: "/aJava/WAF和DDOS的区别是什么", + }, + { + title: "Linux常用命令", + path: "/linux/linux", + }, + { + title: "Nginx环境配置", + path: "/linux/nginx-env", + }, + { + title: "Kibana安装与使用", + path: "/linux/kibana-install", + }, + { + title: "Nginx入门", + path: "/java-up/nginx", + }, + { + title: "Nginx SSL配置", + path: "/java-up/ssl", + }, + { + title: "Nginx优化与防盗链", + path: "/worker/nginx-optimization", + }, + ], + }, + { + title: "部署与监控", + collapsable: true, + children: [ + { + title: "ELK日志系统", + path: "/worker/elk", + }, + { + title: "ELFK生产集群搭建", + path: "/worker/elfk-cluster", + }, + { + title: "Jenkins部署SpringBoot", + path: "/worker/jenkins", + }, + { + title: "RocketMQ安装", + path: "/worker/rocketmq", + }, + { + title: "Grafana监控", + path: "/worker/grafana", + }, + { + title: "Prometheus单机部署", + path: "/worker/prometheus", + }, + { + title: "Grafana监控MySQL、Redis和MongoDB", + path: "/worker/grafana-database-monitoring", + }, + { + title: "K8s监控", + path: "/worker/k8s-monitoring", + }, + { + title: "CPU使用率100%异常排查", + path: "/worker/cpu-troubleshooting", + }, + { + title: "SpringBoot启动脚本", + path: "/worker/springboot", + }, + { + title: "SkyWalking监控", + path: "/worker/skywalking", + }, + { + title: "前端自动化部署", + path: "/worker/7", + }, + { + title: "SRE实践", + path: "/sre/sre", + }, + { + title: "分布式监控", + path: "/sre/monitor", + }, + { + title: "服务质量目标", + path: "/sre/serviceQuality", + }, + { + title: "错误预算", + path: "/sre/errorBudget", + }, + { + title: "系统性能指标", + path: "/sre/system", + }, + { + title: "服务器挖矿病毒清除", + path: "/sre/server-mining-virus-removal", + }, + ], + }, + { + title: "ElasticSearch", + collapsable: true, + children: [ + { + title: "ElasticSearch架构", + path: "/es/es-architecture", + }, + { + title: "查询性能优化", + path: "/es/es-optimizing-query-performance", + }, + { + title: "Linux搭建ES集群与Kibana", + path: "/es/es-cluster-setup", + }, + ], + }, + ], + }, + { + title: "产品系列", + collapsable: false, + children: [ + { + title: "产品概述", + path: "/products/", + }, + { + title: "【溜溜梅】官方商城小程序", + path: "/products/product-gallery", + }, + { + title: "小区充电桩小程序", + path: "/products/productChange", + }, + { + title: "分销小程序", + path: "/products/productShare", + }, + { + title: "电动车充电桩小程序", + path: "/products/product-car", + }, + { + title: "无人岛商业计划书", + path: "/products/无人岛商业计划书", + }, + { + title: "蜗牛睡眠高嵩", + path: "/products/蜗牛睡眠高嵩", + }, + { + title: "优质行业报告网站", + path: "/products/优质行业报告网站", + }, + { + title: "篮球场如何赚钱", + path: "/products/篮球场如何赚钱", + }, + { + title: "企业级应用平台", + path: "/products/enterprise-platform", + }, + { + title: "微服务框架", + path: "/products/microservice-framework", + }, + { + title: "数据分析套件", + path: "/products/data-analytics-suite", + }, + ], + }, + { + title: "实战案例", + collapsable: false, + children: [ + { + title: "Web应用开发", + collapsable: true, + children: [ + { + title: "用户注册登录系统", + path: "/jobPro/login", + }, + { + title: "多语言国际化", + path: "/jobPro/lan", + }, + { + title: "PC网站微信扫码登录", + path: "/jobPro/jobPro", + }, + { + title: "微信小程序开发完全指南", + path: "/jobPro/wechat-miniprogram-guide", + }, + { + title: "微信小程序API客服消息", + path: "/jobPro/wechat-customer-service", + }, + { + title: "微信小程序开放接口", + path: "/jobPro/wechat-open-api", + }, + { + title: "微信小程序运维中心", + path: "/jobPro/wechat-operation-center", + }, + { + title: "微信小程序消息推送", + path: "/jobPro/appMsg", + }, + { + title: "微信小程序自动化部署", + path: "/jobPro/miniprogram", + }, + { + title: "微信小程序支付功能", + path: "/jobPro/appPay", + }, + { + title: "iOS/Android打包发布", + path: "/jobPro/uniapp", + }, + ], + }, + { + title: "技术架构", + collapsable: true, + children: [ + { + title: "500万日订单下的高可用拼购系统", + path: "/tech/high-availability-group-buying", + }, + { + title: "2000万日订单背后的技术架构", + path: "/tech/twenty-million-orders-architecture", + }, + ], + }, + { + title: "企业级应用", + collapsable: true, + children: [ + { + title: "SpringAOP实践", + path: "/java-up/aop-log", + }, + { + title: "SpringIoC实践", + path: "/java-up/ioc", + }, + { + title: "事务支持", + path: "/java-up/transaction", + }, + { + title: "过滤器与拦截器", + path: "/java-up/Filter-Interceptor-Listener", + }, + { + title: "SpringBoot整合Quartz", + path: "/java-up/quartz", + }, + { + title: "SpringBoot整合MyBatis", + path: "/java-up/mybatis", + }, + { + title: "数据校验", + path: "/java-up/validator", + }, + { + title: "API数据加密", + path: "/worker/6", + }, + { + title: "大数据开发实践", + path: "/data/data", + }, + { + title: "订单流程设计", + path: "/order/order-BizOrderService", + }, + { + title: "数据导入导出优化", + path: "/linuxPrometheus/linux", + }, + ], + }, + { + title: "物联网应用", + collapsable: true, + children: [ + { + title: "物联网基础", + path: "/iot/iot", + }, + { + title: "物联网Kafka应用", + path: "/iot/kafka", + }, + { + title: "物联网Redis应用", + path: "/iot/redis", + }, + { + title: "物联网Cassandra应用", + path: "/iot/cassandra", + }, + { + title: "物联网设备管理", + path: "/iot/server", + }, + { + title: "字节数组处理", + path: "/iot/byteArray", + }, + { + title: "物联网业务流程", + path: "/iot/iotJob", + }, + ], + }, + ], + }, + { + title: "Java线程", + collapsable: false, + children: [ + { + title: "linux线程基础", + path: "/aThread/linux", + }, + { + title: "jvm基础知识", + path: "/aThread/jvm", + }, + { + title: "jvm线程", + path: "/aThread/jvmThread", + }, + { + title: "jvm线程通信原理", + path: "/aThread/jvmThreadEvent", + }, + { + title: "jvm线程同步机制", + path: "/aThread/jvmThreadSync", + }, + { + title: "java锁实现原理", + path: "/aThread/jvmLock", + }, + { + title: "java原子操作类实现原理", + path: "/aThread/jvmAtomic", + }, + { + title: "java并发容器实现原理", + path: "/aThread/jvmConcurrent", + }, + { + title: "java线程池实现原理", + path: "/aThread/javaThreadPool", + }, + { + title: "java线程池使用", + path: "/aThread/javaThreadPoolUse", + }, + { + title: "java多线程编程技巧", + path: "/aThread/javaThreadSkill", + }, + ], + }, + { + title: "计算机基础", + collapsable: false, + children: [ + { + title: "操作系统", + path: "/cs/os", + }, + { + title: "计算机网络", + path: "/cs/wangluo", + }, + { + title: "TCP/IP协议详解", + path: "/cs/tcp-ip", + }, + { + title: "Cookie/Session/Token", + path: "/aJava/cookie-session-token", + }, + ], + }, + ], + }, +}; diff --git a/docs/.vuepress/public/failure.ico b/docs/.vuepress/public/failure.ico new file mode 100644 index 000000000..552e5fa47 --- /dev/null +++ b/docs/.vuepress/public/failure.ico @@ -0,0 +1,10 @@ + + + Failure + + + + + + + \ No newline at end of file diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 000000000..a447da281 --- /dev/null +++ b/docs/.vuepress/public/favicon.ico @@ -0,0 +1,13 @@ + + + Java Plus Doc + + + + + J + + + + + \ No newline at end of file diff --git a/docs/.vuepress/public/js/report-site-enhance.js b/docs/.vuepress/public/js/report-site-enhance.js new file mode 100644 index 000000000..c7e190efb --- /dev/null +++ b/docs/.vuepress/public/js/report-site-enhance.js @@ -0,0 +1,270 @@ +/** + * 行业报告网站交互增强脚本 + */ + +// 在文档加载完成后执行 +document.addEventListener('DOMContentLoaded', function() { + // 检查是否在行业报告网站页面 + if (window.location.pathname.includes('/products/优质行业报告网站.html')) { + enhanceReportSite(); + } +}); + +/** + * 增强行业报告网站的交互体验 + */ +function enhanceReportSite() { + // 添加表格行悬停效果 + addTableHoverEffect(); + + // 添加平滑滚动到锚点 + addSmoothScrollToAnchors(); + + // 添加返回顶部按钮 + addBackToTopButton(); + + // 添加链接点击跟踪 + trackExternalLinks(); + + // 添加表格搜索功能 + addTableSearch(); +} + +/** + * 添加表格行悬停效果 + */ +function addTableHoverEffect() { + const tables = document.querySelectorAll('.products-report-site table'); + + tables.forEach(table => { + const rows = table.querySelectorAll('tr'); + + rows.forEach(row => { + row.addEventListener('mouseenter', () => { + row.style.backgroundColor = '#f0f7ff'; + }); + + row.addEventListener('mouseleave', () => { + row.style.backgroundColor = ''; + }); + }); + }); +} + +/** + * 添加平滑滚动到锚点 + */ +function addSmoothScrollToAnchors() { + const links = document.querySelectorAll('.products-report-site a[href^="#"]'); + + links.forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + + const targetId = this.getAttribute('href').substring(1); + const targetElement = document.getElementById(targetId); + + if (targetElement) { + window.scrollTo({ + top: targetElement.offsetTop - 80, + behavior: 'smooth' + }); + } + }); + }); +} + +/** + * 添加返回顶部按钮 + */ +function addBackToTopButton() { + // 创建按钮元素 + const backToTopButton = document.createElement('button'); + backToTopButton.innerHTML = '↑'; + backToTopButton.className = 'back-to-top-button'; + backToTopButton.title = '返回顶部'; + + // 添加样式 + backToTopButton.style.position = 'fixed'; + backToTopButton.style.bottom = '20px'; + backToTopButton.style.right = '20px'; + backToTopButton.style.width = '40px'; + backToTopButton.style.height = '40px'; + backToTopButton.style.borderRadius = '50%'; + backToTopButton.style.backgroundColor = '#3178c6'; + backToTopButton.style.color = 'white'; + backToTopButton.style.border = 'none'; + backToTopButton.style.fontSize = '20px'; + backToTopButton.style.cursor = 'pointer'; + backToTopButton.style.display = 'none'; + backToTopButton.style.zIndex = '1000'; + backToTopButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)'; + + // 添加到文档 + document.body.appendChild(backToTopButton); + + // 添加点击事件 + backToTopButton.addEventListener('click', () => { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }); + + // 控制按钮显示/隐藏 + window.addEventListener('scroll', () => { + if (window.pageYOffset > 300) { + backToTopButton.style.display = 'block'; + } else { + backToTopButton.style.display = 'none'; + } + }); +} + +/** + * 跟踪外部链接点击 + */ +function trackExternalLinks() { + const externalLinks = document.querySelectorAll('.products-report-site a[href^="http"]'); + + externalLinks.forEach(link => { + // 添加新窗口打开属性 + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + + // 添加点击事件 + link.addEventListener('click', function(e) { + // 如果有分析工具,可以在这里添加跟踪代码 + console.log('External link clicked:', this.href); + }); + }); +} + +/** + * 添加表格搜索功能 + */ +function addTableSearch() { + const reportSite = document.querySelector('.products-report-site'); + if (!reportSite) return; + + // 创建搜索框 + const searchContainer = document.createElement('div'); + searchContainer.className = 'report-search-container'; + searchContainer.style.margin = '20px 0'; + searchContainer.style.padding = '15px'; + searchContainer.style.backgroundColor = '#f8f9fa'; + searchContainer.style.borderRadius = '8px'; + searchContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)'; + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = '搜索行业报告网站...'; + searchInput.className = 'report-search-input'; + searchInput.style.width = '100%'; + searchInput.style.padding = '10px 15px'; + searchInput.style.border = '1px solid #ddd'; + searchInput.style.borderRadius = '4px'; + searchInput.style.fontSize = '16px'; + searchInput.style.boxSizing = 'border-box'; + + searchContainer.appendChild(searchInput); + + // 添加搜索结果计数 + const searchResults = document.createElement('div'); + searchResults.className = 'report-search-results'; + searchResults.style.marginTop = '10px'; + searchResults.style.fontSize = '14px'; + searchResults.style.color = '#666'; + searchContainer.appendChild(searchResults); + + // 插入到第一个标题后面 + const firstHeading = reportSite.querySelector('h1'); + if (firstHeading && firstHeading.nextElementSibling) { + reportSite.insertBefore(searchContainer, firstHeading.nextElementSibling.nextElementSibling); + } else { + reportSite.prepend(searchContainer); + } + + // 添加搜索功能 + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + const tables = reportSite.querySelectorAll('table'); + let totalMatches = 0; + + tables.forEach(table => { + const rows = table.querySelectorAll('tbody tr'); + let tableMatches = 0; + + rows.forEach(row => { + const text = row.textContent.toLowerCase(); + const match = text.includes(searchTerm); + + if (match) { + row.style.display = ''; + tableMatches++; + totalMatches++; + + // 高亮匹配文本 + if (searchTerm.length > 0) { + highlightText(row, searchTerm); + } else { + // 移除高亮 + removeHighlight(row); + } + } else { + row.style.display = 'none'; + } + }); + + // 显示/隐藏表格标题 + const tableHeading = table.closest('section').querySelector('h2, h3'); + if (tableHeading) { + tableHeading.style.display = (tableMatches > 0 || searchTerm === '') ? '' : 'none'; + } + }); + + // 更新搜索结果 + if (searchTerm === '') { + searchResults.textContent = ''; + } else { + searchResults.textContent = `找到 ${totalMatches} 个匹配结果`; + } + }); +} + +/** + * 高亮文本 + */ +function highlightText(element, searchTerm) { + removeHighlight(element); + + const cells = element.querySelectorAll('td'); + cells.forEach(cell => { + const originalText = cell.textContent; + const lowerText = originalText.toLowerCase(); + const index = lowerText.indexOf(searchTerm.toLowerCase()); + + if (index >= 0) { + const before = originalText.substring(0, index); + const match = originalText.substring(index, index + searchTerm.length); + const after = originalText.substring(index + searchTerm.length); + + cell.innerHTML = before + + `${match}` + + after; + } + }); +} + +/** + * 移除高亮 + */ +function removeHighlight(element) { + const cells = element.querySelectorAll('td'); + cells.forEach(cell => { + const highlightedSpans = cell.querySelectorAll('span[style*="background-color"]'); + if (highlightedSpans.length > 0) { + cell.textContent = cell.textContent; + } + }); +} \ No newline at end of file diff --git a/docs/.vuepress/public/js/sidebar-enhance.js b/docs/.vuepress/public/js/sidebar-enhance.js new file mode 100644 index 000000000..e69de29bb diff --git a/docs/.vuepress/public/js/sidebar-scroll.js b/docs/.vuepress/public/js/sidebar-scroll.js new file mode 100644 index 000000000..8d06948c0 --- /dev/null +++ b/docs/.vuepress/public/js/sidebar-scroll.js @@ -0,0 +1,523 @@ +// 导航栏选中项滚动居中的JavaScript实现 + +// 防抖函数,用于优化频繁触发的事件 +function debounce(func, wait) { + let timeout; + return function() { + const context = this; + const args = arguments; + clearTimeout(timeout); + timeout = setTimeout(() => { + func.apply(context, args); + }, wait); + }; +} + +document.addEventListener('DOMContentLoaded', function() { + // 初始化侧边栏滚动功能 + initSidebarScroll(); + + // 添加侧边栏搜索框 + addSidebarSearch(); + + // 检查是否需要搜索定位 + checkSearchPosition(); + + // 监听Vue路由变化 + waitForVue(); + + // 等待Vue加载完成并监听路由变化 + function waitForVue() { + const checkVue = setInterval(() => { + if (typeof Vue !== 'undefined') { + clearInterval(checkVue); + // 监听Vue路由变化 + if (window.$vuePress && window.$vuePress.$router) { + window.$vuePress.$router.afterEach(() => { + setTimeout(() => { + const sidebar = document.querySelector('.sidebar-links'); + const activeLink = document.querySelector('.sidebar-link.active'); + if (sidebar && activeLink) { + scrollActiveToCenter(sidebar, activeLink); + // 路由变化后重新添加点击监听器 + addClickListeners(); + } + }, 100); + }); + } + } + }, 100); + } + + // 监听DOM变化,重新添加点击监听器 + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + // 当DOM发生变化时,重新添加点击监听器 + addClickListeners(); + } + }); + }); + + // 开始观察文档变化 + observer.observe(document.body, { childList: true, subtree: true }); + + function initSidebarScroll() { + // 等待侧边栏DOM元素加载完成 + const checkSidebar = setInterval(() => { + const sidebar = document.querySelector('.sidebar-links'); + const activeLink = document.querySelector('.sidebar-link.active'); + + if (sidebar && activeLink) { + clearInterval(checkSidebar); + scrollActiveToCenter(sidebar, activeLink); + + // 添加点击事件监听器到所有导航链接 + addClickListeners(); + + // 监听路由变化,当页面切换时重新滚动 + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' || mutation.type === 'childList') { + const newActiveLink = document.querySelector('.sidebar-link.active'); + if (newActiveLink) { + scrollActiveToCenter(sidebar, newActiveLink); + } + // 当DOM变化时,重新添加点击事件监听器 + addClickListeners(); + } + }); + }); + + observer.observe(sidebar, { + attributes: true, + childList: true, + subtree: true + }); + } + }, 100); + } + + // 添加侧边栏搜索框 + function addSidebarSearch() { + // 等待侧边栏加载完成 + const checkSidebar = setInterval(() => { + const sidebar = document.querySelector('.sidebar'); + if (sidebar) { + clearInterval(checkSidebar); + + // 创建搜索容器 + const searchContainer = document.createElement('div'); + searchContainer.className = 'sidebar-search-container'; + searchContainer.style.cssText = ` + padding: 0.5rem 1rem; + margin-bottom: 1rem; + position: sticky; + top: 0; + background: white; + z-index: 10; + border-bottom: 1px solid #eaecef; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); + `; + + // 创建搜索框 + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = '搜索文档...'; + searchInput.className = 'sidebar-search-input'; + searchInput.style.cssText = ` + width: 100%; + padding: 0.5rem 0.7rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + outline: none; + transition: all 0.3s; + background: #f8f8f8 url('data:image/svg+xml;utf8,') no-repeat; + background-position: calc(100% - 8px) center; + background-size: 16px; + padding-right: 30px; + `; + + // 添加搜索框聚焦样式 + searchInput.addEventListener('focus', () => { + searchInput.style.borderColor = '#42b983'; + searchInput.style.boxShadow = '0 0 0 2px rgba(66, 185, 131, 0.2)'; + searchInput.style.background = '#fff url(\'data:image/svg+xml;utf8,\') no-repeat'; + searchInput.style.backgroundPosition = 'calc(100% - 8px) center'; + searchInput.style.backgroundSize = '16px'; + }); + + searchInput.addEventListener('blur', () => { + searchInput.style.borderColor = '#ddd'; + searchInput.style.boxShadow = 'none'; + searchInput.style.background = '#f8f8f8 url(\'data:image/svg+xml;utf8,\') no-repeat'; + searchInput.style.backgroundPosition = 'calc(100% - 8px) center'; + searchInput.style.backgroundSize = '16px'; + }); + + // 创建搜索结果容器 + const searchResults = document.createElement('div'); + searchResults.className = 'sidebar-search-results'; + searchResults.style.cssText = ` + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #eaecef; + border-top: none; + border-radius: 0 0 4px 4px; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 100; + display: none; + `; + + // 添加搜索功能 + searchInput.addEventListener('input', debounce(function() { + const query = this.value.toLowerCase().trim(); + if (query.length < 2) { + searchResults.style.display = 'none'; + return; + } + + // 获取所有导航链接 + const links = document.querySelectorAll('.sidebar-link'); + let results = []; + + // 搜索匹配项 + links.forEach(link => { + const text = link.textContent.toLowerCase(); + if (text.includes(query)) { + results.push({ + element: link, + text: link.textContent, + url: link.getAttribute('href') + }); + } + }); + + // 显示搜索结果 + if (results.length > 0) { + searchResults.innerHTML = ''; + results.forEach(result => { + const resultItem = document.createElement('div'); + resultItem.className = 'sidebar-search-result-item'; + resultItem.style.cssText = ` + padding: 0.5rem 1rem; + border-bottom: 1px solid #eaecef; + cursor: pointer; + transition: background 0.2s; + `; + + // 高亮匹配文本 + const highlightedText = result.text.replace( + new RegExp(query, 'gi'), + match => `${match}` + ); + + resultItem.innerHTML = highlightedText; + + // 点击结果项导航到对应页面并自动定位 + resultItem.addEventListener('click', () => { + // 先隐藏搜索结果 + searchResults.style.display = 'none'; + searchInput.value = ''; + + // 如果是当前页面的链接,直接滚动定位 + if (result.url === window.location.pathname || + result.url.replace(/\.html$/, '') === window.location.pathname.replace(/\.html$/, '')) { + // 找到对应的侧边栏链接并滚动定位 + setTimeout(() => { + const sidebar = document.querySelector('.sidebar-links'); + if (sidebar && result.element) { + // 移除所有active类 + document.querySelectorAll('.sidebar-link.active').forEach(link => { + link.classList.remove('active'); + }); + // 添加active类到选中项 + result.element.classList.add('active'); + // 滚动到选中项 + scrollActiveToCenter(sidebar, result.element); + } + }, 100); + } else { + // 跳转到其他页面,并添加搜索定位参数 + const targetUrl = new URL(result.url, window.location.origin); + targetUrl.searchParams.set('search_target', result.text); + window.location.href = targetUrl.toString(); + } + });}]}} + + // 鼠标悬停效果 + resultItem.addEventListener('mouseenter', () => { + resultItem.style.backgroundColor = '#f6f8fa'; + }); + + resultItem.addEventListener('mouseleave', () => { + resultItem.style.backgroundColor = 'white'; + }); + + searchResults.appendChild(resultItem); + }); + + searchResults.style.display = 'block'; + } else { + searchResults.innerHTML = '
没有找到匹配项
'; + searchResults.style.display = 'block'; + } + }, 300)); + + // 点击外部关闭搜索结果 + document.addEventListener('click', (e) => { + if (!searchContainer.contains(e.target)) { + searchResults.style.display = 'none'; + } + }); + + // 添加到DOM + searchContainer.appendChild(searchInput); + searchContainer.appendChild(searchResults); + sidebar.insertBefore(searchContainer, sidebar.firstChild); + } + }, 100); + } + + // 为所有导航链接添加点击事件监听器 + function addClickListeners() { + const sidebarLinks = document.querySelectorAll('.sidebar-link'); + sidebarLinks.forEach(link => { + // 移除旧的事件监听器,避免重复 + link.removeEventListener('click', handleLinkClick); + // 添加新的事件监听器 + link.addEventListener('click', handleLinkClick); + }); + } + + // 处理链接点击事件 + function handleLinkClick(event) { + // 使用setTimeout确保在路由变化后执行 + setTimeout(() => { + const sidebar = document.querySelector('.sidebar-links'); + const activeLink = document.querySelector('.sidebar-link.active'); + if (sidebar && activeLink) { + scrollActiveToCenter(sidebar, activeLink); + } + }, 50); + } + + // 监听页面滚动事件,确保在页面滚动时也能触发导航栏的滚动居中 + let scrollTimer; + window.addEventListener('scroll', function() { + // 使用防抖技术,避免频繁触发 + clearTimeout(scrollTimer); + scrollTimer = setTimeout(() => { + const sidebar = document.querySelector('.sidebar-links'); + const activeLink = document.querySelector('.sidebar-link.active'); + if (sidebar && activeLink) { + scrollActiveToCenter(sidebar, activeLink); + } + }, 200); + }); + + function scrollActiveToCenter(sidebar, activeLink) { + // 计算需要滚动的位置,使活动链接居中 + const sidebarHeight = sidebar.clientHeight; + const activeLinkTop = activeLink.offsetTop; + const activeLinkHeight = activeLink.clientHeight; + + // 计算滚动位置,使活动链接垂直居中 + const scrollTop = activeLinkTop - (sidebarHeight / 2) + (activeLinkHeight / 2); + + // 确保滚动位置在有效范围内 + const maxScrollTop = sidebar.scrollHeight - sidebar.clientHeight; + const safeScrollTop = Math.max(0, Math.min(scrollTop, maxScrollTop)); + + // 使用平滑滚动效果 + sidebar.scrollTo({ + top: safeScrollTop, + behavior: 'smooth' + }); + + // 添加呼吸灯效果,使选中项更醒目 + addPulseEffect(activeLink); + + // 添加面包屑导航提示 + updateBreadcrumb(activeLink); + } + + // 添加呼吸灯效果 + function addPulseEffect(element) { + // 移除所有现有的脉冲效果 + const existingPulses = document.querySelectorAll('.sidebar-pulse-effect'); + existingPulses.forEach(pulse => pulse.remove()); + + // 创建新的脉冲效果元素 + const pulse = document.createElement('span'); + pulse.className = 'sidebar-pulse-effect'; + pulse.style.cssText = ` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(66, 185, 131, 0.2); + border-radius: 0 4px 4px 0; + pointer-events: none; + z-index: -1; + animation: pulse 1.5s ease-in-out; + `; + + // 添加脉冲动画样式 + if (!document.getElementById('pulse-animation')) { + const style = document.createElement('style'); + style.id = 'pulse-animation'; + style.textContent = ` + @keyframes pulse { + 0% { opacity: 0.8; transform: scale(0.95); } + 50% { opacity: 0.2; transform: scale(1.02); } + 100% { opacity: 0; transform: scale(1); } + } + `; + document.head.appendChild(style); + } + + // 添加到活动链接 + element.style.position = 'relative'; + element.appendChild(pulse); + + // 动画结束后移除 + setTimeout(() => { + pulse.remove(); + }, 1500); + } + + // 更新面包屑导航 + function updateBreadcrumb(activeLink) { + // 检查是否已存在面包屑容器 + let breadcrumb = document.querySelector('.sidebar-breadcrumb'); + + // 如果不存在,创建一个 + if (!breadcrumb) { + breadcrumb = document.createElement('div'); + breadcrumb.className = 'sidebar-breadcrumb'; + breadcrumb.style.cssText = ` + position: fixed; + top: 3.6rem; + left: 0; + width: 100%; + padding: 0.5rem 1rem 0.5rem 320px; + background-color: rgba(255, 255, 255, 0.9); + border-bottom: 1px solid #eaecef; + font-size: 0.9rem; + color: #999; + z-index: 10; + backdrop-filter: blur(5px); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: opacity 0.3s; + `; + document.body.appendChild(breadcrumb); + + // 添加面包屑样式 + if (!document.getElementById('breadcrumb-style')) { + const style = document.createElement('style'); + style.id = 'breadcrumb-style'; + style.textContent = ` + .sidebar-breadcrumb a { + color: #42b983; + text-decoration: none; + margin: 0 0.3rem; + } + .sidebar-breadcrumb a:hover { + text-decoration: underline; + } + .sidebar-breadcrumb .separator { + margin: 0 0.3rem; + color: #ccc; + } + `; + document.head.appendChild(style); + } + } + + // 构建面包屑路径 + let path = []; + let current = activeLink; + let text = current.textContent.trim(); + path.unshift({ text, link: current.getAttribute('href') }); + + // 查找父级分组 + let parent = current.closest('.sidebar-group'); + while (parent) { + const heading = parent.querySelector('.sidebar-heading'); + if (heading) { + path.unshift({ text: heading.textContent.trim(), link: '#' }); + } + parent = parent.parentElement.closest('.sidebar-group'); + } + + // 生成面包屑HTML + breadcrumb.innerHTML = '当前位置:'; + path.forEach((item, index) => { + if (index > 0) { + breadcrumb.innerHTML += ''; + } + if (index === path.length - 1) { + breadcrumb.innerHTML += `${item.text}`; + } else { + breadcrumb.innerHTML += `${item.text}`; + } + }); + + // 显示面包屑 + breadcrumb.style.opacity = '1'; + + // 3秒后淡出 + setTimeout(() => { + breadcrumb.style.opacity = '0'; + }, 3000); + } +} + +// 检查搜索定位参数 +function checkSearchPosition() { + // 检查URL中是否有搜索定位标识 + const urlParams = new URLSearchParams(window.location.search); + const searchTarget = urlParams.get('search_target'); + + if (searchTarget) { + // 延迟执行,确保页面完全加载 + setTimeout(() => { + const sidebar = document.querySelector('.sidebar-links'); + if (sidebar) { + // 查找匹配的侧边栏链接 + const links = document.querySelectorAll('.sidebar-link'); + let targetLink = null; + + links.forEach(link => { + const linkText = link.textContent.toLowerCase(); + if (linkText.includes(searchTarget.toLowerCase())) { + targetLink = link; + } + }); + + if (targetLink) { + // 移除所有active类 + document.querySelectorAll('.sidebar-link.active').forEach(link => { + link.classList.remove('active'); + }); + // 添加active类到目标项 + targetLink.classList.add('active'); + // 滚动到目标项 + scrollActiveToCenter(sidebar, targetLink); + + // 清除URL参数 + const newUrl = window.location.pathname + window.location.hash; + window.history.replaceState({}, document.title, newUrl); + } + } + }, 500); + } +}); \ No newline at end of file diff --git a/docs/.vuepress/public/resume.html b/docs/.vuepress/public/resume.html new file mode 100644 index 000000000..87622cc91 --- /dev/null +++ b/docs/.vuepress/public/resume.html @@ -0,0 +1,782 @@ + + + + + + 黄广达 - Java后端&AI全栈架构师简历 + + + + + + +
+
+
+
黄广达
+
Java后端 & AI全栈架构师
+
男 / 软件技术 / 7年+经验 / 04.23
+
+ 📞 13119585432  |  ✉️ 2397923107@qq.com +
+ +
+
+
GitHub & 码云活跃者,累积 700+ Stars
+
敏捷项目管理 & 产品运营 & 营销经验
+
0到1项目孵化经验 & 团队管理
+
+
+
+

个人优势

+
    +
  1. 开发经验丰富的0到1项目开发经验:拥有6年以上独立开发经验,主导大型前后端开发重构项目(换电,画本妖鸡AI效率工具),具备敏捷项目管理经验和多人团队协作能力,能够独立进行全栈开发。
  2. +
  3. 技术栈与架构: +
      +
    1. 前端技术栈与架构 +
        +
      • 微前端:熟练 Qiankun 微前端架构,打造多应用独立部署、灰度发布与热更新能力,支持团队各产品模块化协同开发。
      • +
      • 主流框架与生态:熟练 React、Vue3/2、Taro、uniapp、Flutter 及 Node/Express;熟悉 TypeScript 全流程开发,善用 Ant Design Vue、Element Plus、Vue Router、Pinia、Axios、VueUse、VueRequest、vue-i18n、TailwindCSS 等。
      • +
      • 跨平台能力:具备 Android/iOS 、小程序端(微信/支付宝系)开发实战,能在多端保持体验一致、代码复用率高。
      • +
      +
    2. +
    3. 后端技术栈与架构 +
        +
      • 微服务与分布式:熟练 Spring Cloud(Spring Boot 2.7、Spring MVC、Spring Security、OAuth2、JWT)、Maven 多模块管理;有基于 Nacos/Zookeeper 的服务发现与配置管理、OpenFeign/Ribbon 负载均衡、Traefik/Nginx 反向代理落地经验。
      • +
      • 高并发与消息:熟练使用 Netty、MQTT、TCP/UDP 协议栈;擅长 Kafka 流式处理、Sentinel 熔断限流、XXL-Job 定时调度、SkyWalking 链路追踪,保障系统亿级并发下的稳定与可观测性。
      • +
      • 数据存储与检索:熟练关系型与 NoSQL 数据库:MySQL(主从+分库分表)、MongoDB、Redis(缓存、分布式锁)、Cassandra、Elasticsearch;熟练 MyBatis/MyBatis-Plus 快速开发、Log4j 全面日志方案。
      • +
      • 云原生与存储:熟练 MinIO 对象存储、Nginx/Tomcat/Docker 容器化部署;具备一键化脚本与 CI/CD 流水线落地实践。
      • +
      +
    4. +
    +
  4. +
  5. 运维与工具能力: +
      +
    • 容器化与编排:熟练 Docker 镜像构建,熟练使用 Kubernetes(K8s)完成集群部署。
    • +
    • CI/CD 与自动化:熟练基于 Jenkins/GitLab CI 搭建流水线,实现代码打包、蓝绿/灰度/滚动发布;能编写 Shell 自动化运维脚本,提升交付速度与稳定性。
    • +
    • 监控与诊断:利用 Arthas 在线诊断 JVM 问题,结合 SkyWalking 链路追踪与 Sentinel 熔断限流,实现从应用到中台的全链条可观测与服务保护;熟悉 Prometheus+Grafana 指标采集与可视化。
    • +
    • 系统运维与故障排查:精通 Linux(CentOS)系统管理,熟练配置 Nginx、Redis、Kafka、MySQL 等常用中间件,并能快速定位网络、进程、磁盘 I/O、内存泄露等故障根因。
    • +
    +
  6. +
  7. 管理与架构经验: +
      +
    • 技术管理(4+ 年):负责 5+ 人研发团队的技术选型、规范制订与执行,主导规划、项目进度监控与质量评审;建立文档化流程与绩效考核机制,保证项目按期交付。
    • +
    • 高并发系统设计:主导电商秒杀、物联网车联网与 AI 工具平台的千万级流量系统架构,采用微服务 + 异步消息(Kafka/MQTT)+ 分布式缓存(Redis)+ 多级限流(Sentinel)策略,保障系统在高并发场景下的可用性与稳定性。
    • +
    • 团队成长与分享:定期组织技术分享会、故障复盘,推动最佳实践在团队内落地;指导新人快速上手,持续提升团队整体交付能力与技术深度。
    • +
    +
  8. +
  9. 高并发项目设计与落地: +
      +
    1. 二手珠宝 & 购车服务系统 +
        +
      • 构建基于 Spring Cloud 的微服务架构,使用 Nginx + Spring Cloud Gateway 作 API 网关,Kafka 异步解耦业务流;
      • +
      • 引入 Redis 分布式缓存、布隆过滤器防击穿;
      • +
      • 支撑日均百万级请求,秒级峰值超5 万 QPS,P99 响应时间稳定在 <300ms(压测)。
      • +
      +
    2. +
    3. 新能源物联网·换电柜系统 +
        +
      • 基于 Kafka + Netty 双协议网关,高性能接入50 万+ 设备;
      • +
      • 后端微服务使用 Cassandra 存储设备上报,写入延迟 <50ms
      • +
      • 前端结合 WebSocket 实时监控架构,实现设备状态秒级感知与指令下发。
      • +
      +
    4. +
    5. 效率 AI 工具平台(有声小说行业)
    6. +
    +
  10. +
  11. 微服务平台能力亮点: +
      +
    • 稳定性与可靠性:平台上线运行超过 3 年,承载核心业务持续稳定无故障;
    • +
    • SaaS 多租户:设计多租户数据隔离服务,按需扩展子系统,满足隔离、安全、可审计要求。
    • +
    +
  12. +
  13. 算法与数据分析实力: +
      +
    • 算法功底深厚:深入剖析过 Vue 源码,完成 LeetCode 500+ 题目刷题,擅长常见数据结构与并发算法优化;
    • +
    • Python 自动化与数据洞察:基于 Pandas 开发运营数据分析脚本,定期产出自动化报表,提升决策效率。
    • +
    +
  14. +
  15. 产品运营与营销经验: +
      +
    • 精益创业思维:驱动从需求调研、MVP 迭代到上线反馈的全生命周期管理;
    • +
    • 跨部门协作:与产品、市场、客服等团队紧密配合,推动专项营销活动。
    • +
    +
  16. +
+
+ + +
+

项目经验

+ +

项目名称:骑手换电平台(B端 & C端,2023.4–至今)|SaaS + 车联网全栈核心开发 [https://www.fnjkj.cn/]

+
一、项目背景
+
菲尼基科技专注于新能源锂电池全生命周期管理,为企业和个人提供换电柜、充电桩、租车与设备管理等一站式 IoT SaaS 平台,已覆盖 300+ 城市、500+ 运营商。
+
二、核心职责
+
    +
  1. 前端0→1 架构设计: +
      +
    • 框架与工具:Vue2、Vue3、TypeScript、Ant-Design-Vue、Element-Plus、Vue-i18n、Mitt、Axios、Pinia、Vue-Router、TailwindCSS、VueRequest、VueUse、WangEditor
    • +
    • 小程序&H5开发:Taro (React、Taro-UI、Dva-Core、Dva-Loading、UtilsCore、Promise-Polyfill)
    • +
    +
    核心亮点(前端):
    +
      +
    • 全栈后台与小程序:独立从零搭建 Vue3 + TypeScript 管理后台及 Taro/uniapp 小程序,实现生产管理、资产(押金、企业客户、E签宝、免押扣款)、运营(现金活动、邀新、提现)、售后、优惠券、积分兑换、推广、分账、派单/获客、计费/会员与叠加套餐、消息推送等完整功能模块。
    • +
    • 组件化与工程化:设计并落地内部 Gd UI Components 组件库,基于 Ant-Design-Vue、Element-Plus、Naive-UI,高效复用;结合 ESlint、Prettier、Git Hooks、CI/CD 打造统一开发规范与自动化流程。
    • +
    • 微前端接入:基于 qiankun 搭建 Vue3-Vite-React 子应用体系,实现各业务模块独立部署、灰度发布与热更新。
    • +
    • 可视化与地图:Vue3 + ECharts + scale() 构建自适应运营大屏;高德地图 3D 轨迹回放、视野跟随、纠偏、资产地图与附近查询功能,支持海量点位下的高性能渲染。
    • +
    +
  2. +
  3. 后端0→1 架构设计: +
      +
    • 技术栈:Spring Cloud Alibaba + Nacos + Sentinel + Spring Cloud Gateway + redis + kafka + ribbon + Knife4j + netty + Skywalking + 熔断机制 Hystrix + OpenFeign,SkyWalking,MongoDB,MySql,Cassandra,Redis,clickhouse等。
    • +
    • 架构:随着流量的增加,后端的可扩展性和高可用性变得尤为重要。
    • +
    +
    核心亮点(后端):
    +
      +
    • 0→1 微服务集群:使用 Spring Cloud Alibaba + Nacos + Spring Cloud Gateway 构建十余服务(sys-service、order-service、finance-service、user-center、statistics-service、admin/user-gateway、sku-service、send-service 等)与四大设备网关(fcom-battery, fcom-cabinets, fcom-service, fcom-monitor)。
    • +
    • 多存储融合: +
        +
      • MongoDB 主库:高并发读写;
      • +
      • MySQL:事务与核心业务;
      • +
      • Elasticsearch:列表/全文检索查询;
      • +
      • Cassandra:历史/冷数据;
      • +
      • Redis:缓存与实时热数据;
      • +
      • ClickHouse:实时统计与大表关联分析。
      • +
      +
    • +
    • 高可用与弹性:Netty + Kafka 双协议消息网关,Hystrix 熔断、Sentinel 限流,SkyWalking 链路追踪,保证千万级 QPS 下的稳定;Kubernetes + Jenkins 自动扩缩容与滚动发布。
    • +
    • 链路与监控:统一接入 SkyWalking、Prometheus/Grafana、ELK 日志,实时监控服务健康、调用拓扑与关键指标;延迟队列(Kafka-Redisson + XXL-Job)管理定时任务与重试;EasyExcel 分批导出百万级数据(80s→8s)。
    • +
    +
  4. +
  5. 数据分析 & 可视化: +
      +
    • 交付实时运营报表与大屏(ECharts/Vue3),帮助运营决策从"日度"缩短到"分钟级",整体效率提升 60%。
    • +
    • 用户行为监控:从前端埋点→Cassandra,构建用户数据收集(接口请求,设备终端,页面流转,行为采集等)。
    • +
    +
  6. +
  7. DevOps & 监控体系: +
      +
    • CI/CD 自动化:搭建 GitLab + Jenkins + 自动化流水线,前端后端服务部署时间由 15 分钟缩短至 2 分钟,发布失败率下降 80%。
    • +
    • 可观测性:部署 ELK + Prometheus/Grafana + SkyWalking 全链路监控。
    • +
    +
  8. +
  9. 性能优化: +
      +
    • 接口压测 & 调优:使用 JMeter 进行千并发压力测试,精准定位瓶颈。
    • +
    • 大数据导出:基于 EasyExcel 多线程分批导出百万级记录,耗时从 80 秒降至 8 秒。
    • +
    +
  10. +
  11. 异步 & 高可用: +
      +
    • 延迟队列:结合 Kafka + Redisson + XXL-JOB 实现订单超时、状态变更等场景的异步补偿,任务吞吐量 > 10k TPS。
    • +
    • 断连重连策略:在 Netty+Kafka 网关层做心跳检测与离线指令缓存,设备在线率从 85% 提升至 98%,指令丢失率 < 0.1%。
    • +
    +
  12. +
  13. 架构演进: +
      +
    • 缓存 & 搜索:集群化 Redis 做高性能缓存,Elasticsearch 做全文检索,数据库压力峰值下降 80%。
    • +
    • 消息解耦:全链路引入 Kafka,服务间解耦率 100%,容错率提升 50%。
    • +
    • API 网关:升级至 Spring Cloud Gateway,支持 JWT 细粒度鉴权与蓝绿/滚动发布,零停机上线。
    • +
    +
  14. +
  15. 安全 & 实时通信: +
      +
    • 安全防护:AJ-Captcha 滑块验证码+jsencrypt 加密+Redis 分布式限流,恶意请求减少 90%。
    • +
    • 实时心跳:整合 WebSocket 实现指令下发与心跳检测,确保秒级状态感知。
    • +
    +
  16. +
+
解决方案:
+
    +
  1. 物联网设备断连重连策略 +
      +
    • 场景:在换电柜系统中,部分柜体因网络波动导致 MQTT 连接不稳定,设备状态无法实时上报。
    • +
    • 难点:设备掉线后无法自动恢复连接,导致指令丢失和状态不同步。
    • +
    • 解决方案: +
        +
      • 在 Netty + Kafka 双协议网关层实现 心跳检测,对长时间未上报的客户端进行主动断线并重连;
      • +
      • 后端使用 离线消息队列(Kafka)缓存指令,重连成功后统一下发。
      • +
      +
    • +
    • 效果:设备在线率从 85% 提升至 98%,指令丢失率降至 <0.1%。
    • +
    +
  2. +
  3. 分布式事务与幂等保障 +
      +
    • 场景:在换电柜系统中,订单服务(order-service)与库存服务(sku-service)跨微服务调用,需要保证"扣减库存–下单"操作的强一致性,且在高并发下不允许重复下单或库存超卖。
    • +
    • 难点:多服务间分布式事务实现困难;网络抖动或服务降级时可能导致库存扣减成功但订单创建失败,或重复请求导致库存多扣。
    • +
    • 解决方案: +
        +
      • 消息驱动补偿:拆分下单流程为:tryReserveStock → createOrder → confirmStock 三阶段,失败时触发补偿;
      • +
      • 接口幂等设计:在库存扣减与订单创建接口都使用唯一 idempotencyKey,并在数据库做唯一索引;重复请求自动短路返回上次结果。
      • +
      +
    • +
    • 效果:订单—库存强一致性达到 100%;幂等请求下单无重复,库存从无超扣;
    • +
    +
  4. +
  5. 多级缓存与服务降级 +
      +
    • 场景:热点查询接口(如资产地图、运营概况)在短时间内出现突增并发,数据库压力骤增,曾出现超时和雪崩。
    • +
    • 难点:单层 Redis 缓存在失效瞬间仍会击穿,导致后端 DB 瞬时爆发式请求, 需要在缓存或 DB 不可用时保证接口的基本可用性。
    • +
    • 解决方案: +
        +
      • 本地缓存+Redis 分布式缓存,先查本地,再查 Redis,最后回落 DB。
      • +
      • 缓存预热与主动更新:应用启动、业务变更或定时任务时,主动将热点数据写入二级缓存,保证高命中。
      • +
      • Sentinel 降级策略:当 Redis/DB 调用失败或超时,触发降级,返回灰度数据或静态兜底信息,保护核心服务可用。
      • +
      • 异步刷新:缓存过期后,首个请求触发异步刷新,其他请求继续返回旧数据,避免"击穿 + 群体失效"。
      • +
      +
    • +
    • 效果:缓存命中率由 ~70% 提升至 >98%;数据库 QPS 峰值下降 80%,无单点雪崩;Redis 故障或网络抖动期间,API 成功率仍保持 >90% 。
    • +
    +
  6. +
  7. 多区域多活部署与灾备演练 +
      +
    • 场景:为保障核心业务在单个机房或区域故障时持续可用,需要在不同地域同时运行服务,并能在故障时快速切换。
    • +
    • 难点:数据一致性:跨地域数据库主从或多主复制易出现延迟或冲突;DNS 切换与流量切换:需在毫秒级完成切流,避免用户感知。
    • +
    • 解决方案: +
        +
      • 多活集群:在华东、华北、华南等区域各部署一套微服务集群;
      • +
      • 跨区域数据库复制:MySQL 主主双写;Cassandra 跨地域多活复制;ClickHouse 利用 Kafka 同步数据。
      • +
      • 定期灾备演练:注入区域级故障(断网、节点宕机),验证多活切换与数据恢复流程。
      • +
      +
    • +
    • 效果:区域单点故障恢复时间(RTO)< 60s,数据丢失(RPO)< 5s;故障切换透明度高,用户无感知服务中断。
    • +
    +
  8. +
  9. 统一 RPC + gRPC 双通道降级 +
      +
    • 场景:微服务间大量使用 HTTP/JSON 调用,接口调用频次和数据量骤增,出现序列化开销高、链路阻塞、请求超时等问题。
    • +
    • 难点:JSON 序列化/反序列化开销大,网络带宽占用高;接口阻塞无法及时降级,影响链路整体可用性。
    • +
    • 解决方案: +
        +
      • 引入 gRPC:在 Spring Cloud 基础上接入 gRPC(protobuf)作为微服务内部通信协议,替换高频调用的 REST 接口,显著降低序列化成本。
      • +
      • 统一调用封装:在公共库中封装 RpcClient,根据配置动态选择 gRPC 或 HTTP,并自动埋点监控调用时延、错误率。
      • +
      • 双通道灰度切换:失败回退逻辑:当 gRPC 调用失败或延迟超阈值,自动切回 HTTP 通道。
      • +
      +
    • +
    • 效果:服务调用带宽消耗降低 60%,CPU 序列化开销下降 50%;链路可用率提升至 99.99%,再无"单接口阻塞全链路"现象
    • +
    +
  10. +
+ +

骑手商城项目(电商)

+
一、项目背景
+
支持多运营商入驻的骑手商城服务平台,包含积分兑换、商品运营平台。
+
技术栈:
+ +
解决方案:
+
    +
  1. 高并发秒杀——缓存击穿与库存超卖 +
      +
    • 场景:在"积分 & 优惠券微商城"的秒杀中,瞬间并发峰值超 10 万 QPS,库存一度出现超卖。
    • +
    • 难点:高并发下直接打数据库导致锁竞争严重,Redis 缓存在失效瞬间遭受击穿。
    • +
    • 解决方案: +
        +
      • 引入 布隆过滤器预热缓存,于秒杀开始前将商品 ID 加入布隆过滤器并填满缓存;
      • +
      • 基于 Redis 的 分布式锁(Redisson)控制同一商品的并发扣减;
      • +
      • 异步化库存更新:先用本地内存队列削峰,批量异步写 MySQL,并结合 消息队列(Kafka)重试机制保障最终一致性。
      • +
      +
    • +
    • 效果:接口响应从 800ms 降至 150ms,秒杀期间库存无超卖,系统稳定支撑百万级并发。
    • +
    +
  2. +
+ +

埋点系统

+ + +

应急模式

+ + +

有声书制作软件项目(画本妖鸡等,2021.6–2023.4)[https://huabenyaoji.com/]

+
项目背景
+
喜马拉雅一站式有声书制作平台,涵盖"画本妖鸡""对轨妖鸡""云妖鸡""主播市场""剧社管理"等模块,月活突破 100 万+。
+
技术栈:Vue-TypeScript-Admin-Template、UEditor、Electron-Vue-Admin、Spring Boot、MySQL、Redis、JWT、七牛云
+
前端核心亮点:
+
    +
  1. 主要负责开发核心产品:画本妖鸡、对轨妖鸡、云妖鸡、主播市场、剧社管理等应用程序的开发和搭建。同时,我也负责协调团队成员工作,进行代码分配和任务分配
  2. +
  3. TypeScript 转型:1 周内完成 JS→TS 重构,搭建全新组件化脚手架,团队开发效率提升 300%。
  4. +
  5. 富文本定制:在UEditor富文本编辑器的基础上,封装了自己的API,包括文本自动解析、自动分章处理、角色管理中心RoleBus、定位系统ScrollTo、自动匹配并高亮台词对话、选中对话自动判断角色、章节与对话定位等功能。
  6. +
  7. 高级交互组件:灵活自定义plugin($xxx)和指令(v-),抽离项目中复用且复杂的功能,比如当前选中元素切换时自动滚动局中、元素展示更多与收回内容的切换、权限控制等。
  8. +
  9. 暗黑主题 & 断点续传:魔改 ElementUI 为 CSS Variables 黑暗模式;WebSocket 大文件上传+断点续传,任务成功率 99.5%。
  10. +
  11. 桌面客户端:基于 Electron-Vue-Admin 打包 Windows/Mac 双端安装包
  12. +
+
后端核心亮点:
+
    +
  1. 安全与鉴权:JWT 鉴权+Redis 黑名单;自定义注解拦截重复提交。
  2. +
  3. 高效文件存储:七牛云 + MinIO 混合方案,支持海量音频/文本上传;日均存储请求 10k+。
  4. +
  5. 配置与网关:整合 Nacos 实现多环境配置管理,服务注册发现;Knife4j 生成 API 文档,减少 40% 沟通成本。
  6. +
+ +

充电宝·充电线·充电桩一体化平台(2019.6–2021.6)

+
1. 充电桩、充电宝项目
+
背景:自研共享充电宝/充电线与充电桩软硬件一体化服务,面向 C 端用户与 B 端运营商,支持多端(H5 管理后台、小程序、原生 App)全面覆盖。
+
技术栈:单体服务、Nacos 服务发现/配置、MySQL 主从+分库分表、Redis 缓存、XXL-JOB 定时任务、Uniapp 小程序、ElementUI 管理端
+
核心亮点:
+
    +
  1. 端到端全链路:前端 H5 与小程序实现充电站/桩管理、资产监控、故障报警、订单/押金/分账/优惠券全流程;后端用户服务、设备服务、充电运营服务、网关鉴权、模拟桩联调均独立部署、解耦弹性扩容。
  2. +
  3. 高可用与高并发:Redis + MySQL 双写一致性保证;XXL-JOB 与 Kafka-Redisson 延迟队列处理"押金退还"、"订单超时"等场景,峰值并发稳定支撑百 QPS。
  4. +
  5. 全面安全体系:JWT+OAuth2 账户鉴权、RBAC 细粒度权限、AES+RSA 混合加密、操作审计日志、运维堡垒机接入。
  6. +
+
2. 推广内容平台(企业官网+微信公众号+小程序)
+
项目背景:为多品牌客户提供统一的内容发布与运营推广解决方案,覆盖 PC 后台、手机 H5、微信小程序与公众号服务号。
+
技术栈:Spring Boot、MyBatis-Plus、MySQL、Redis、Elasticsearch、MongoDB;Vue + ECharts
+
核心亮点:
+
    +
  1. 通过验证码和前端保持半长链接映射关系,当用户扫码关注公众号并输入验证码后,发起回调,识别用户信息并找到对应半长链接,实现系统自动登录。
  2. +
  3. 高吞吐热点缓存:MySQL + Redis 缓存预热+双层一致性策略;Elasticsearch 支持全文检索与推荐。
  4. +
  5. 富媒体编辑与托管:集成开源 Markdown 编辑器+OSS 图片自动转链上传,编辑效率提升 3×,存储成本下降 40%。
  6. +
+ +

物联网

+
IOT数据存储开发
+
项目:对物联网数据的管理
+
技术栈:SwaggerUI,jwt+Shiro实现登录管理和权限管理,Redis key-value存储系统,Mybatis持久层框架+分页插件PageHelper,Mysql数据库,linux+Tomcat+Nginx部署
+
亮点:
+
    +
  1. 对传感器,系统,设备,图像数据,经纬度数据,账户,数值数据点进行统一权限管理
  2. +
  3. 后台过滤器Filter,拦截器Interceptor,Controller层接收参数,Service服务,Dao持久层,数据交换
  4. +
+ +

大模型

+
基于讯飞星火大模型的智能BI平台
+
项目:用户只需导入原始数据,并输入分析需求,就能一键式生成可视化图表和结论
+
技术栈:SpringBoot,MySQL,MyBatis Plus,Easy Excel,Redission的RateLimiter,星火大模型SDK,Hutool工具库
+
亮点:
+
    +
  1. 用户输入图表名称,图表类型,分析目标后上传Excel文件进行多重校验
  2. +
  3. 校验完成后对图表压缩数据进行处理
  4. +
  5. 支持同步/异步两种分析模式,异步可批量分析多个文件
  6. +
  7. 分析完成后的图表可在我的图表界面查看
  8. +
+ +

Python-fastapi

+
基于full-stack-fastapi-template 全栈 FastAPI
+
项目:仓储物料管理平台
+
技术栈:使用 FastAPI、React、SQLModel、PostgreSQL、Docker 自动 HTTPS 等, 包含DB、Redis、MongoDB、JSON等工具和基础服务类。
+
+ + +
+

开源 & 教育

+
    +
  1. 学校:广东机电职业技术学院 软件技术专业 (2016 – 2019)
  2. +
  3. 开源项目 +
      +
    • UUI 组件库:自主设计并开源了一套高复用前端组件库,覆盖表单、表格、模态框等 20+ 组件
    • +
    • 开源项目托管在 GitHub,具体内容可通过 webVueBlog 获取更多信息。
    • +
    +
  4. +
  5. 个人博客与社交平台开发:个人技术博客平台,支持 Markdown 编辑、SEO 优化、评论与分享,月访问量 10K+:Jeskson文档-微服务分布式系统架构
  6. +
  7. 语雀知识库:维护 200+ 篇技术文档,体系化沉淀前端、后端、DevOps、算法笔记,累计阅读 5K+。
  8. +
  9. 具备 企业级 SaaS 多租户微服务分布式平台架构能力,能根据业务需求设计高可扩展、高可用的微服务架构,并实现高效的多租户支持和资源隔离。
  10. +
+
+
+ + \ No newline at end of file diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl index d454ea473..054f90bc0 100644 --- a/docs/.vuepress/styles/index.styl +++ b/docs/.vuepress/styles/index.styl @@ -10,6 +10,8 @@ .content__default { max-width: 85% !important; + line-height: 1.7; + font-size: 16px; } .theme-container { @@ -38,3 +40,228 @@ div > pre { overflow-wrap: break-word; /* 新版标准,兼容更多浏览器 */ } +// 全局阅读体验优化 +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #2c3e50; +} + +// 阅读进度条样式 +$readingBgColor = transparent +$readingZIndex = 1000 +$readingSize = 4px +$readingProgressColor = #3eaf7c +$readingProgressImage = linear-gradient(90deg, #3eaf7c 0%, #42b883 50%, #35495e 100%) + +.theme-default-content p { + margin: 1.2em 0; + line-height: 1.7; +} + +.theme-default-content h1, .theme-default-content h2, .theme-default-content h3, .theme-default-content h4, .theme-default-content h5, .theme-default-content h6 { + margin-top: 2rem; + margin-bottom: 1.2rem; + font-weight: 600; + line-height: 1.25; +} + +.theme-default-content blockquote { + border-left: 4px solid #3178c6; + padding: 0.8rem 1.5rem; + color: #666; + background-color: #f8f9fa; + border-radius: 0 4px 4px 0; + margin: 1.5rem 0; +} + +.theme-default-content img { + max-width: 100%; + border-radius: 5px; + box-shadow: 0 3px 8px rgba(0,0,0,0.1); + margin: 1.5rem 0; +} + +.theme-default-content code { + padding: 0.25rem 0.5rem; + margin: 0 0.2rem; + border-radius: 3px; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 0.85em; + background-color: #f6f8fa; +} + +// 平板设备布局优化 +@media (min-width: 768px) and (max-width: 1024px) { + .theme-container { + .sidebar { + width: 280px !important; + transform: translateX(-100%); + transition: transform 0.3s ease; + position: fixed; + top: 0; + left: 0; + height: 100vh; + z-index: 1000; + background: #fff; + box-shadow: 2px 0 8px rgba(0,0,0,0.1); + } + + .sidebar.open { + transform: translateX(0); + } + + .page { + padding-left: 0 !important; + margin-left: 0 !important; + } + + .navbar { + padding-left: 4rem !important; + } + + .navbar .sidebar-button { + display: block !important; + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + z-index: 1001; + } + } + + .content__default { + max-width: 95% !important; + padding: 0 1.5rem; + margin: 0 auto; + } + + .theme-default-content { + padding: 1rem 1.5rem; + } + + // 导航栏优化 + .navbar .links { + background-color: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + border-radius: 4px; + padding: 0.5rem; + } + + // 代码块优化 + .theme-default-content div[class*="language-"] { + margin: 1rem -1.5rem; + border-radius: 0; + } + + .theme-default-content div[class*="language-"] pre { + padding: 1.25rem 1.5rem; + overflow-x: auto; + } + + // 表格优化 + .theme-default-content table { + display: block; + width: 100%; + overflow-x: auto; + white-space: nowrap; + } + + // 图片优化 + .theme-default-content img { + max-width: 100%; + height: auto; + margin: 1rem auto; + display: block; + } +} + +// 小平板设备优化 (iPad mini等) +@media (min-width: 768px) and (max-width: 820px) { + .content__default { + max-width: 98% !important; + font-size: 15px; + } + + .theme-default-content h1 { + font-size: 1.8rem; + } + + .theme-default-content h2 { + font-size: 1.5rem; + } + + .theme-default-content h3 { + font-size: 1.3rem; + } +} + +// 大平板设备优化 (iPad Pro等) +@media (min-width: 1024px) and (max-width: 1366px) { + .theme-container .sidebar { + width: 320px !important; + } + + .content__default { + max-width: 90% !important; + margin-left: 340px; + } + + .theme-default-content { + padding: 2rem 2.5rem; + } +} + +// 触摸设备优化 +@media (pointer: coarse) { + .sidebar-links a { + padding: 0.8rem 1rem; + min-height: 44px; + display: flex; + align-items: center; + } + + .navbar .links .nav-item { + margin-left: 1.5rem; + } + + .navbar .links .nav-item > a { + padding: 0.8rem 1rem; + min-height: 44px; + display: flex; + align-items: center; + } + + // 按钮优化 + button, .btn { + min-height: 44px; + padding: 0.8rem 1.5rem; + } +} + +// 横屏模式优化 +@media (orientation: landscape) and (max-height: 768px) { + .theme-container .sidebar { + width: 260px !important; + } + + .content__default { + font-size: 14px; + line-height: 1.6; + } + + .theme-default-content h1, + .theme-default-content h2, + .theme-default-content h3 { + margin-top: 1.5rem; + margin-bottom: 1rem; + } +} + +// 引入侧边栏增强样式 +@import "./sidebar-enhanced.styl"; + +// 引入行业报告网站专用样式 +@import "./report-site.styl"; + diff --git a/docs/.vuepress/styles/report-site.styl b/docs/.vuepress/styles/report-site.styl new file mode 100644 index 000000000..76ae9fd6f --- /dev/null +++ b/docs/.vuepress/styles/report-site.styl @@ -0,0 +1,131 @@ +/* 行业报告网站专用样式 */ + +/* 表格样式优化 */ +.theme-default-content table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 1.5rem 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); +} + +.theme-default-content table th { + background-color: #f6f8fa; + padding: 12px 16px; + font-weight: 600; + text-align: left; + border-bottom: 2px solid #e1e4e8; + color: #24292e; +} + +.theme-default-content table td { + padding: 12px 16px; + border-bottom: 1px solid #eaecef; + transition: background-color 0.2s ease; +} + +.theme-default-content table tr:last-child td { + border-bottom: none; +} + +.theme-default-content table tr:hover td { + background-color: #f6f8fa; +} + +/* 链接样式优化 */ +.theme-default-content a { + position: relative; + text-decoration: none; + color: #3178c6; + font-weight: 500; + transition: all 0.2s ease; + border-bottom: 1px solid transparent; +} + +.theme-default-content a:hover { + border-bottom: 1px solid #3178c6; +} + +/* 提示框样式优化 */ +.custom-block.tip, +.custom-block.warning { + border-radius: 8px; + padding: 16px 20px; + margin: 1.5rem 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.custom-block.tip { + background-color: #f2f7ff; + border-left: 5px solid #3178c6; +} + +.custom-block.warning { + background-color: #fff7f0; + border-left: 5px solid #e67e22; +} + +.custom-block.tip p:first-child, +.custom-block.warning p:first-child { + font-weight: 600; +} + +/* 标题样式优化 */ +.theme-default-content h2 { + border-bottom: 2px solid #eaecef; + padding-bottom: 0.5rem; + margin-top: 2.5rem; + margin-bottom: 1.5rem; + position: relative; +} + +.theme-default-content h2::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 100px; + height: 2px; + background-color: #3178c6; +} + +/* 列表样式优化 */ +.theme-default-content ol, +.theme-default-content ul { + padding-left: 1.5rem; + margin: 1rem 0; +} + +.theme-default-content ol li, +.theme-default-content ul li { + margin-bottom: 0.5rem; + line-height: 1.6; +} + +/* 行业报告网站页面专用样式 */ +.products-report-site { + max-width: 1000px; + margin: 0 auto; + padding: 0 20px; +} + +.products-report-site h1 { + text-align: center; + margin-bottom: 2rem; + font-size: 2.2rem; + color: #2c3e50; +} + +/* 响应式调整 */ +@media (max-width: 719px) { + .theme-default-content table { + display: block; + overflow-x: auto; + } + + .products-report-site { + padding: 0 15px; + } +} \ No newline at end of file diff --git a/docs/.vuepress/styles/sidebar-enhanced.styl b/docs/.vuepress/styles/sidebar-enhanced.styl new file mode 100644 index 000000000..684fb643f --- /dev/null +++ b/docs/.vuepress/styles/sidebar-enhanced.styl @@ -0,0 +1,202 @@ +/** + * 侧边栏增强样式 + * 专门针对平板设备的侧边栏优化 + */ + +// 平板设备侧边栏遮罩层 +.tablet-device.sidebar-open::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + z-index: 999; + transition: opacity 0.3s ease; +} + +// 侧边栏滚动条优化 +.sidebar .sidebar-links { + scrollbar-width: thin; + scrollbar-color: #c1c1c1 transparent; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; + + &:hover { + background: #a1a1a1; + } + } +} + +// 侧边栏链接触摸优化 +.tablet-device .sidebar-links a { + position: relative; + transition: all 0.2s ease; + + &:hover, &:focus { + background-color: #f5f5f5; + transform: translateX(2px); + } + + &:active { + background-color: #e8e8e8; + transform: translateX(1px); + } +} + +// 侧边栏分组标题优化 +.tablet-device .sidebar-group .sidebar-heading { + padding: 1rem 1rem 0.5rem; + font-weight: 600; + font-size: 1.1em; + color: #2c3e50; + border-bottom: 1px solid #eaecef; + margin-bottom: 0.5rem; + + &:not(.clickable) { + cursor: default; + } +} + +// 侧边栏子项缩进优化 +.tablet-device .sidebar-links .sidebar-sub-headers { + padding-left: 1rem; + + a { + padding-left: 2rem; + font-size: 0.9em; + color: #666; + + &.active { + color: #3178c6; + font-weight: 500; + } + } +} + +// 侧边栏按钮样式增强 +.tablet-device .navbar .sidebar-button { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + border-radius: 4px; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } + + &:focus { + outline: 2px solid #3178c6; + outline-offset: 2px; + } + + // 汉堡菜单图标 + .icon { + width: 20px; + height: 20px; + display: block; + + &::before, + &::after { + content: ''; + display: block; + width: 100%; + height: 2px; + background: #2c3e50; + margin: 4px 0; + transition: all 0.3s ease; + } + } +} + +// 侧边栏搜索框优化(如果存在) +.tablet-device .sidebar .search-box { + margin: 1rem; + + input { + width: 100%; + padding: 0.8rem 1rem; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 16px; // 防止iOS缩放 + + &:focus { + border-color: #3178c6; + outline: none; + box-shadow: 0 0 0 2px rgba(49, 120, 198, 0.2); + } + } +} + +// 代码块滚动指示器 +.scrollable { + position: relative; + + &::after { + content: '→'; + position: absolute; + right: 10px; + top: 10px; + color: #999; + font-size: 12px; + opacity: 0.7; + pointer-events: none; + } +} + +// 平板横屏模式特殊优化 +@media (orientation: landscape) and (max-height: 768px) { + .tablet-device .sidebar { + .sidebar-links { + padding-top: 0.5rem; + + a { + padding: 0.6rem 1rem; + font-size: 0.9em; + } + } + + .sidebar-group .sidebar-heading { + padding: 0.8rem 1rem 0.3rem; + font-size: 1em; + } + } +} + +// 暗色主题适配 +.dark .tablet-device { + .sidebar { + background: #1e1e1e; + color: #fff; + + .sidebar-links a { + color: #ccc; + + &:hover, &:focus { + background-color: #333; + color: #fff; + } + + &.active { + color: #3178c6; + } + } + } + + &.sidebar-open::before { + background: rgba(0, 0, 0, 0.6); + } +} \ No newline at end of file diff --git a/docs/.vuepress/styles/sre-page.styl b/docs/.vuepress/styles/sre-page.styl new file mode 100644 index 000000000..6a99a04a0 --- /dev/null +++ b/docs/.vuepress/styles/sre-page.styl @@ -0,0 +1,126 @@ +// SRE页面专用样式 +.sre-page { + // 文章信息区域样式 + .article-info { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: 2rem; + padding: 0.8rem 1.5rem; + background-color: #f8f9fa; + border-radius: 5px; + font-size: 0.9rem; + color: #666; + + .info-item { + margin-right: 1.5rem; + display: flex; + align-items: center; + + .icon { + margin-right: 0.3rem; + } + } + } + + // 标签样式 + .article-tags { + display: flex; + flex-wrap: wrap; + margin: 1rem 0; + + .tag { + padding: 0.2rem 0.8rem; + margin-right: 0.5rem; + margin-bottom: 0.5rem; + background-color: #e9f5fe; + color: #3178c6; + border-radius: 3px; + font-size: 0.85rem; + transition: all 0.3s ease; + + &:hover { + background-color: #3178c6; + color: white; + } + } + } + + // 文章元数据样式 + .article-meta { + display: flex; + align-items: center; + margin-bottom: 1rem; + font-size: 0.9rem; + color: #666; + + .meta-item { + margin-right: 1.5rem; + display: flex; + align-items: center; + + .icon { + margin-right: 0.3rem; + } + } + } + + // 目录样式优化 + .table-of-contents { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 5px; + margin-bottom: 2rem; + + &:before { + content: "目录"; + font-weight: bold; + display: block; + margin-bottom: 0.5rem; + color: #3178c6; + } + + ul { + padding-left: 1rem; + } + + a { + color: #2c3e50; + + &:hover { + color: #3178c6; + } + } + } + + // 文章内容样式优化 + h2 { + border-bottom: 2px solid #eaecef; + padding-bottom: 0.5rem; + } + + h3 { + color: #3178c6; + } + + // 代码块样式优化 + div[class*="language-"] { + margin: 1.5rem 0; + border-radius: 6px; + box-shadow: 0 2px 12px rgba(0,0,0,0.1); + + &:before { + font-size: 0.8rem; + font-weight: bold; + } + } + + // 引用块样式优化 + blockquote { + border-left: 4px solid #3178c6; + background-color: rgba(49, 120, 198, 0.05); + } +} + +// 添加到index.styl中 +@import "./sre-page.styl"; \ No newline at end of file diff --git a/docs/Python/1.md b/docs/Python/1.md new file mode 100644 index 000000000..f6de4e55a --- /dev/null +++ b/docs/Python/1.md @@ -0,0 +1,120 @@ +--- +title: 第1天-Python历史 +author: 哪吒 +date: '2023-06-15' +--- + +# 第1天-Python历史 + +## Python的诞生故事 + +Python是著名的"龟叔"Guido van Rossum在1989年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。 + +> **小白注解**:Guido van Rossum被称为"龟叔",是因为他喜欢英国喜剧团体Monty Python,而不是因为Python这个名字来源于蟒蛇。 + +## Python vs C语言的区别 + +C语言是可以用来编写操作系统的贴近硬件的语言,所以,C语言适合开发那些追求运行速度、充分发挥硬件性能的程序。而Python是用来编写应用程序的高级编程语言。 + +> **通俗理解**: +> - C语言就像手动挡汽车,你需要自己控制每个细节,速度快但操作复杂 +> - Python就像自动挡汽车,操作简单,虽然速度稍慢但足够日常使用 + +## Python的"内置电池"特性 + +Python为我们提供了非常完善的基础代码库,覆盖了网络、文件、GUI、数据库、文本等大量内容,被形象地称作"内置电池(batteries included)"。用Python开发,许多功能不必从零编写,直接使用现成的即可。 + +> **小白示例**:想要下载网页内容?Python内置的urllib库就能做到,不需要自己写复杂的网络代码。 + +## 丰富的第三方库生态 + +除了内置的库外,Python还有大量的第三方库,也就是别人开发的,供你直接使用的东西。当然,如果你开发的代码通过很好的封装,也可以作为第三方库给别人使用。 + +> **扩展知识**: +> - **PyPI**(Python Package Index)是Python的官方第三方库仓库 +> - 截至2024年,PyPI上有超过50万个项目 +> - 常用的第三方库:requests(网络请求)、pandas(数据分析)、numpy(科学计算) + +## Python的应用领域 + +许多大型网站就是用Python开发的: + +> **知名案例**: +> - **YouTube** - 世界最大的视频分享网站 +> - **Instagram** - 知名图片社交平台 +> - **Dropbox** - 云存储服务 +> - **Reddit** - 知名社区论坛 +> - **Netflix** - 流媒体巨头(推荐算法) +> - **Spotify** - 音乐流媒体(数据分析) + +> **应用领域总结**: +> - 🌐 **Web开发**:Django、Flask框架 +> - 📊 **数据科学**:pandas、numpy、matplotlib +> - 🤖 **人工智能**:TensorFlow、PyTorch +> - 🔬 **科学计算**:SciPy、SymPy +> - 🎮 **游戏开发**:Pygame +> - 📱 **移动开发**:Kivy、BeeWare +> - 🔧 **自动化脚本**:系统管理、测试自动化 + +## Python的缺点分析 + +### 缺点一:运行速度相对较慢 + +第一个缺点就是运行速度慢,和C程序相比非常慢,因为Python是解释型语言,你的代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时,所以很慢。而C程序是运行前直接编译成CPU能执行的机器码,所以非常快。 + +> **小白理解**: +> - **解释型语言**:像同声传译,边读边翻译执行 +> - **编译型语言**:像提前翻译好的书,直接阅读 + +但是大量的应用程序不需要这么快的运行速度,因为用户根本感觉不出来。例如开发一个下载MP3的网络应用程序,C程序的运行时间需要0.001秒,而Python程序的运行时间需要0.1秒,慢了100倍,但由于网络更慢,需要等待1秒,你想,用户能感觉到1.001秒和1.1秒的区别吗? + +> **生动比喻**:这就好比F1赛车和普通出租车在北京三环路上行驶,虽然F1赛车理论时速高达400公里,但由于三环路堵车的时速只有20公里,因此,作为乘客,你感觉的时速永远是20公里。 + +![img.png](./img.png) + +> **性能优化方案**: +> - 使用**PyPy**解释器(比CPython快2-10倍) +> - 使用**Cython**编写性能关键部分 +> - 使用**NumPy**进行数值计算 +> - 合理使用**多线程/多进程** + +### 缺点二:源代码无法完全加密 + +第二个缺点就是代码不能加密。如果要发布你的Python程序,实际上就是发布源代码,这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是你在Windows上常见的xxx.exe文件)发布出去。要从机器码反推出C代码是不可能的,所以,凡是编译型的语言,都没有这个问题,而解释型的语言,则必须把源码发布出去。 + +> **解决方案**: +> - 使用**PyInstaller**打包成exe文件 +> - 使用**py2exe**或**cx_Freeze** +> - 核心算法用C/C++编写,Python调用 +> - 使用**代码混淆**工具 + +这个缺点仅限于你要编写的软件需要卖给别人挣钱的时候。好消息是目前的互联网时代,靠卖软件授权的商业模式越来越少了,靠网站和移动应用卖服务的模式越来越多了,后一种模式不需要把源码给别人。 + +![img_1.png](./img_1.png) + +## 总结 + +> **Python适合你吗?** +> +> ✅ **适合的场景**: +> - 初学编程(语法简单) +> - 数据分析和科学计算 +> - Web开发 +> - 自动化脚本 +> - 人工智能和机器学习 +> +> ❌ **不太适合的场景**: +> - 对性能要求极高的系统 +> - 移动应用开发(虽然可以但不是主流) +> - 系统底层开发 +> - 游戏引擎开发 + +> **学习建议**:Python是一门非常适合初学者的语言,即使你完全没有编程基础,也能在短时间内上手。它的设计哲学是"优雅、明确、简单",让你能专注于解决问题而不是纠结于语法细节。 + + + + + + + + diff --git a/docs/Python/10.md b/docs/Python/10.md new file mode 100644 index 000000000..d31ed1715 --- /dev/null +++ b/docs/Python/10.md @@ -0,0 +1,2302 @@ +--- +title: 第10天-高级编程 +author: 哪吒 +date: '2023-06-15' +--- + +> 宝贝,学习是一个过程,不要给自己太大压力哦~ + +# 第10天-高级编程 + +## 学习目标 + +通过本章学习,你将掌握: +- 元类(Metaclass)的概念和使用 +- 描述符(Descriptor)协议 +- 上下文管理器的深入应用 +- 协程和异步编程基础 +- 内存管理和性能优化 +- 设计模式在Python中的实现 +- 代码调试和性能分析技巧 + +--- + +## 一、元类(Metaclass) + +### 1.1 什么是元类 + +元类是"创建类的类"。在Python中,类也是对象,而元类就是创建这些类对象的"类"。 + +```python +# 理解类和对象的关系 +class MyClass: + pass + +# MyClass是一个类,同时也是一个对象 +print(f"MyClass的类型:{type(MyClass)}") # +print(f"MyClass是否是对象:{isinstance(MyClass, object)}") # True + +# 创建实例 +obj = MyClass() +print(f"obj的类型:{type(obj)}") # +print(f"obj的类:{obj.__class__}") # + +# type既是类型,也是元类 +print(f"type的类型:{type(type)}") # +``` + +### 1.2 使用type动态创建类 + +```python +# 使用type动态创建类 +# type(name, bases, dict) + +# 方法1:传统方式定义类 +class Person: + def __init__(self, name): + self.name = name + + def say_hello(self): + return f"Hello, I'm {self.name}" + +# 方法2:使用type动态创建相同的类 +def init_method(self, name): + self.name = name + +def say_hello_method(self): + return f"Hello, I'm {self.name}" + +# 动态创建Person类 +DynamicPerson = type( + 'DynamicPerson', # 类名 + (object,), # 基类元组 + { # 类字典 + '__init__': init_method, + 'say_hello': say_hello_method, + 'class_var': 'I am a dynamic class' + } +) + +# 使用动态创建的类 +person1 = Person("张三") +person2 = DynamicPerson("李四") + +print(person1.say_hello()) # Hello, I'm 张三 +print(person2.say_hello()) # Hello, I'm 李四 +print(person2.class_var) # I am a dynamic class + +print(f"Person类型:{type(Person)}") +print(f"DynamicPerson类型:{type(DynamicPerson)}") +``` + +### 1.3 自定义元类 + +```python +class SingletonMeta(type): + """单例模式元类""" + _instances = {} + + def __call__(cls, *args, **kwargs): + """控制类的实例化过程""" + if cls not in cls._instances: + # 如果还没有实例,创建一个 + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + +class Database(metaclass=SingletonMeta): + """数据库连接类(单例模式)""" + + def __init__(self, host="localhost", port=3306): + self.host = host + self.port = port + self.connected = False + print(f"创建数据库连接:{host}:{port}") + + def connect(self): + if not self.connected: + print(f"连接到数据库 {self.host}:{self.port}") + self.connected = True + else: + print("数据库已连接") + + def disconnect(self): + if self.connected: + print(f"断开数据库连接 {self.host}:{self.port}") + self.connected = False + +class ValidatedMeta(type): + """属性验证元类""" + + def __new__(mcs, name, bases, namespace): + # 在创建类之前进行验证 + if 'required_methods' in namespace: + required = namespace['required_methods'] + for method in required: + if method not in namespace: + raise TypeError(f"类 {name} 必须实现方法 {method}") + + # 为所有方法添加日志装饰器 + for key, value in namespace.items(): + if callable(value) and not key.startswith('_'): + namespace[key] = mcs.add_logging(value, key) + + return super().__new__(mcs, name, bases, namespace) + + @staticmethod + def add_logging(func, func_name): + """为方法添加日志""" + def wrapper(*args, **kwargs): + print(f"调用方法: {func_name}") + result = func(*args, **kwargs) + print(f"方法 {func_name} 执行完成") + return result + return wrapper + +class APIClient(metaclass=ValidatedMeta): + """API客户端类""" + required_methods = ['get', 'post'] # 必须实现的方法 + + def __init__(self, base_url): + self.base_url = base_url + + def get(self, endpoint): + return f"GET {self.base_url}/{endpoint}" + + def post(self, endpoint, data): + return f"POST {self.base_url}/{endpoint} with {data}" + + def delete(self, endpoint): + return f"DELETE {self.base_url}/{endpoint}" + +# 使用示例 +print("=== 元类示例 ===") + +# 单例模式测试 +print("\n=== 单例模式测试 ===") +db1 = Database("localhost", 3306) +db2 = Database("remote", 5432) # 参数会被忽略,返回同一个实例 + +print(f"db1 is db2: {db1 is db2}") # True +print(f"db1.host: {db1.host}, db2.host: {db2.host}") # 都是localhost + +db1.connect() +db2.connect() # 显示已连接 + +# 验证元类测试 +print("\n=== 验证元类测试 ===") +client = APIClient("https://api.example.com") +print(client.get("users")) +print(client.post("users", {"name": "张三"})) +print(client.delete("users/1")) +``` + +### 1.4 元类的实际应用 + +```python +class ORMMeta(type): + """简单的ORM元类""" + + def __new__(mcs, name, bases, namespace): + # 收集字段信息 + fields = {} + for key, value in namespace.items(): + if isinstance(value, Field): + fields[key] = value + value.name = key + + namespace['_fields'] = fields + namespace['_table_name'] = namespace.get('_table_name', name.lower()) + + return super().__new__(mcs, name, bases, namespace) + +class Field: + """字段基类""" + + def __init__(self, field_type, required=True, default=None): + self.field_type = field_type + self.required = required + self.default = default + self.name = None + + def validate(self, value): + if value is None and self.required: + raise ValueError(f"字段 {self.name} 是必需的") + + if value is not None and not isinstance(value, self.field_type): + raise TypeError(f"字段 {self.name} 必须是 {self.field_type.__name__} 类型") + + return value + +class StringField(Field): + def __init__(self, max_length=255, **kwargs): + super().__init__(str, **kwargs) + self.max_length = max_length + + def validate(self, value): + value = super().validate(value) + if value and len(value) > self.max_length: + raise ValueError(f"字段 {self.name} 长度不能超过 {self.max_length}") + return value + +class IntegerField(Field): + def __init__(self, **kwargs): + super().__init__(int, **kwargs) + +class Model(metaclass=ORMMeta): + """模型基类""" + + def __init__(self, **kwargs): + for field_name, field in self._fields.items(): + value = kwargs.get(field_name, field.default) + setattr(self, field_name, field.validate(value)) + + def save(self): + """保存到数据库(模拟)""" + fields = [] + values = [] + for field_name, field in self._fields.items(): + fields.append(field_name) + values.append(getattr(self, field_name)) + + sql = f"INSERT INTO {self._table_name} ({', '.join(fields)}) VALUES ({', '.join(map(repr, values))})" + print(f"执行SQL: {sql}") + + def __repr__(self): + attrs = [] + for field_name in self._fields: + value = getattr(self, field_name) + attrs.append(f"{field_name}={value!r}") + return f"{self.__class__.__name__}({', '.join(attrs)})" + +# 定义用户模型 +class User(Model): + _table_name = 'users' + + id = IntegerField(required=False) + name = StringField(max_length=50) + email = StringField(max_length=100) + age = IntegerField(required=False, default=0) + +# 使用ORM +print("\n=== ORM元类示例 ===") + +try: + user = User(name="张三", email="zhangsan@example.com", age=25) + print(user) + user.save() + + # 测试验证 + invalid_user = User(name="李四", email="lisi@example.com", age="invalid") +except (ValueError, TypeError) as e: + print(f"验证错误: {e}") +``` + +--- + +## 二、描述符(Descriptor) + +### 2.1 描述符协议 + +描述符是定义了`__get__`、`__set__`或`__delete__`方法的对象。 + +```python +class Descriptor: + """基本描述符示例""" + + def __init__(self, name=None): + self.name = name + self.value = None + + def __get__(self, obj, objtype=None): + """获取属性值时调用""" + print(f"获取属性 {self.name}") + if obj is None: + return self + return self.value + + def __set__(self, obj, value): + """设置属性值时调用""" + print(f"设置属性 {self.name} = {value}") + self.value = value + + def __delete__(self, obj): + """删除属性时调用""" + print(f"删除属性 {self.name}") + self.value = None + +class MyClass: + attr = Descriptor("attr") + + def __init__(self): + self.normal_attr = "normal" + +# 使用描述符 +print("=== 描述符基础示例 ===") +obj = MyClass() + +# 访问描述符属性 +print(f"obj.attr = {obj.attr}") # 调用 __get__ + +# 设置描述符属性 +obj.attr = "new value" # 调用 __set__ +print(f"obj.attr = {obj.attr}") # 调用 __get__ + +# 删除描述符属性 +del obj.attr # 调用 __delete__ +print(f"obj.attr = {obj.attr}") # 调用 __get__ +``` + +### 2.2 实用描述符示例 + +```python +class ValidatedAttribute: + """验证属性描述符""" + + def __init__(self, validator=None, default=None): + self.validator = validator + self.default = default + self.name = None + + def __set_name__(self, owner, name): + """Python 3.6+ 新特性,自动设置属性名""" + self.name = name + + def __get__(self, obj, objtype=None): + if obj is None: + return self + return obj.__dict__.get(self.name, self.default) + + def __set__(self, obj, value): + if self.validator: + value = self.validator(value) + obj.__dict__[self.name] = value + +class TypedAttribute(ValidatedAttribute): + """类型验证描述符""" + + def __init__(self, expected_type, **kwargs): + def type_validator(value): + if not isinstance(value, expected_type): + raise TypeError(f"{self.name} 必须是 {expected_type.__name__} 类型") + return value + super().__init__(type_validator, **kwargs) + +class RangeAttribute(ValidatedAttribute): + """范围验证描述符""" + + def __init__(self, min_value=None, max_value=None, **kwargs): + def range_validator(value): + if min_value is not None and value < min_value: + raise ValueError(f"{self.name} 不能小于 {min_value}") + if max_value is not None and value > max_value: + raise ValueError(f"{self.name} 不能大于 {max_value}") + return value + super().__init__(range_validator, **kwargs) + +class EmailAttribute(ValidatedAttribute): + """邮箱验证描述符""" + + def __init__(self, **kwargs): + def email_validator(value): + if not isinstance(value, str) or '@' not in value: + raise ValueError(f"{self.name} 必须是有效的邮箱地址") + return value.lower() + super().__init__(email_validator, **kwargs) + +class CachedProperty: + """缓存属性描述符""" + + def __init__(self, func): + self.func = func + self.name = func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, obj, objtype=None): + if obj is None: + return self + + # 检查是否已缓存 + cache_name = f'_cached_{self.name}' + if hasattr(obj, cache_name): + print(f"从缓存获取 {self.name}") + return getattr(obj, cache_name) + + # 计算并缓存结果 + print(f"计算 {self.name}") + result = self.func(obj) + setattr(obj, cache_name, result) + return result + + def __set__(self, obj, value): + # 清除缓存 + cache_name = f'_cached_{self.name}' + if hasattr(obj, cache_name): + delattr(obj, cache_name) + # 设置新值 + setattr(obj, f'_{self.name}', value) + +# 使用描述符的类 +class Person: + """使用各种描述符的人员类""" + + name = TypedAttribute(str) + age = RangeAttribute(0, 150) + email = EmailAttribute() + + def __init__(self, name, age, email): + self.name = name + self.age = age + self.email = email + + @CachedProperty + def full_info(self): + """完整信息(计算密集型操作模拟)""" + import time + time.sleep(0.1) # 模拟耗时操作 + return f"{self.name} ({self.age}岁) - {self.email}" + + def __repr__(self): + return f"Person(name='{self.name}', age={self.age}, email='{self.email}')" + +# 使用示例 +print("\n=== 描述符实用示例 ===") + +try: + person = Person("张三", 25, "ZhangSan@Example.Com") + print(person) + print(f"邮箱已转换为小写: {person.email}") + + # 测试缓存属性 + print("\n=== 缓存属性测试 ===") + print(f"第一次访问: {person.full_info}") # 计算 + print(f"第二次访问: {person.full_info}") # 从缓存获取 + print(f"第三次访问: {person.full_info}") # 从缓存获取 + + # 测试验证 + print("\n=== 验证测试 ===") + person.age = 30 # 正常 + print(f"修改年龄后: {person.age}") + + # 尝试设置无效值 + person.age = -5 # 应该抛出异常 + +except (TypeError, ValueError) as e: + print(f"验证错误: {e}") + +try: + # 测试类型验证 + person.name = 123 # 应该抛出异常 +except TypeError as e: + print(f"类型错误: {e}") + +try: + # 测试邮箱验证 + person.email = "invalid-email" # 应该抛出异常 +except ValueError as e: + print(f"邮箱错误: {e}") +``` + +--- + +## 三、协程和异步编程 + +### 3.1 生成器和协程基础 + +```python +import asyncio +import time +from typing import AsyncGenerator + +# 传统生成器 +def simple_generator(): + """简单生成器""" + print("生成器开始") + yield 1 + print("生成器继续") + yield 2 + print("生成器结束") + yield 3 + +# 协程生成器 +def coroutine_example(): + """协程示例""" + print("协程启动") + value = None + while True: + received = yield value + print(f"协程接收到: {received}") + value = f"处理了: {received}" + +# 异步生成器 +async def async_generator(): + """异步生成器""" + for i in range(5): + print(f"异步生成 {i}") + await asyncio.sleep(0.1) # 模拟异步操作 + yield i + +# 异步函数 +async def async_function(name: str, delay: float) -> str: + """异步函数示例""" + print(f"{name} 开始执行") + await asyncio.sleep(delay) # 异步等待 + print(f"{name} 执行完成") + return f"{name} 的结果" + +async def fetch_data(url: str, delay: float) -> dict: + """模拟异步获取数据""" + print(f"开始获取数据: {url}") + await asyncio.sleep(delay) # 模拟网络请求 + data = { + "url": url, + "data": f"来自 {url} 的数据", + "timestamp": time.time() + } + print(f"数据获取完成: {url}") + return data + +# 使用示例 +print("=== 生成器和协程示例 ===") + +# 使用生成器 +print("\n=== 简单生成器 ===") +gen = simple_generator() +for value in gen: + print(f"获得值: {value}") + +# 使用协程 +print("\n=== 协程示例 ===") +coro = coroutine_example() +next(coro) # 启动协程 +result1 = coro.send("Hello") +print(f"协程返回: {result1}") +result2 = coro.send("World") +print(f"协程返回: {result2}") +coro.close() # 关闭协程 + +# 异步函数示例 +async def main_async_example(): + """异步主函数""" + print("\n=== 异步函数示例 ===") + + # 顺序执行 + start_time = time.time() + result1 = await async_function("任务1", 1.0) + result2 = await async_function("任务2", 0.5) + end_time = time.time() + print(f"顺序执行结果: {result1}, {result2}") + print(f"顺序执行耗时: {end_time - start_time:.2f}秒") + + # 并发执行 + start_time = time.time() + task1 = async_function("并发任务1", 1.0) + task2 = async_function("并发任务2", 0.5) + results = await asyncio.gather(task1, task2) + end_time = time.time() + print(f"并发执行结果: {results}") + print(f"并发执行耗时: {end_time - start_time:.2f}秒") + + # 使用异步生成器 + print("\n=== 异步生成器 ===") + async for value in async_generator(): + print(f"异步获得值: {value}") + + # 批量数据获取 + print("\n=== 批量异步数据获取 ===") + urls = [ + "https://api1.example.com", + "https://api2.example.com", + "https://api3.example.com" + ] + + # 创建任务 + tasks = [fetch_data(url, 0.5) for url in urls] + + # 等待所有任务完成 + start_time = time.time() + results = await asyncio.gather(*tasks) + end_time = time.time() + + print(f"获取到 {len(results)} 个结果") + for result in results: + print(f" {result['url']}: {result['data']}") + print(f"总耗时: {end_time - start_time:.2f}秒") + +# 运行异步示例 +if __name__ == "__main__": + asyncio.run(main_async_example()) +``` + +### 3.2 异步上下文管理器和迭代器 + +```python +import asyncio +import aiofiles # 需要安装: pip install aiofiles +from contextlib import asynccontextmanager + +class AsyncDatabaseConnection: + """异步数据库连接""" + + def __init__(self, host: str, database: str): + self.host = host + self.database = database + self.connection = None + + async def __aenter__(self): + """异步进入上下文""" + print(f"异步连接到数据库: {self.host}/{self.database}") + await asyncio.sleep(0.1) # 模拟连接延迟 + self.connection = f"Connection to {self.database}" + return self.connection + + async def __aexit__(self, exc_type, exc_value, traceback): + """异步退出上下文""" + if self.connection: + print(f"异步关闭数据库连接: {self.database}") + await asyncio.sleep(0.05) # 模拟关闭延迟 + self.connection = None + return False + +class AsyncFileProcessor: + """异步文件处理器""" + + def __init__(self, filename: str): + self.filename = filename + self.file = None + + def __aiter__(self): + return self + + async def __anext__(self): + if self.file is None: + # 第一次调用时打开文件 + try: + self.file = await aiofiles.open(self.filename, 'r', encoding='utf-8') + except FileNotFoundError: + # 如果文件不存在,创建一个模拟的内容 + self.lines = ["第一行内容", "第二行内容", "第三行内容"] + self.index = 0 + + if hasattr(self, 'lines'): + # 使用模拟内容 + if self.index >= len(self.lines): + raise StopAsyncIteration + line = self.lines[self.index] + self.index += 1 + await asyncio.sleep(0.1) # 模拟处理延迟 + return line.strip() + else: + # 使用真实文件 + line = await self.file.readline() + if not line: + await self.file.close() + raise StopAsyncIteration + await asyncio.sleep(0.1) # 模拟处理延迟 + return line.strip() + +@asynccontextmanager +async def async_timer(name: str): + """异步计时器上下文管理器""" + start_time = time.time() + print(f"{name} 开始") + try: + yield start_time + finally: + end_time = time.time() + print(f"{name} 完成,耗时: {end_time - start_time:.2f}秒") + +async def async_context_examples(): + """异步上下文管理器示例""" + print("=== 异步上下文管理器示例 ===") + + # 使用异步数据库连接 + async with AsyncDatabaseConnection("localhost", "myapp") as conn: + print(f"使用连接: {conn}") + await asyncio.sleep(0.2) # 模拟数据库操作 + print("数据库操作完成") + + # 使用异步计时器 + async with async_timer("数据处理任务"): + await asyncio.sleep(1.0) # 模拟耗时任务 + print("任务执行中...") + + # 使用异步迭代器 + print("\n=== 异步迭代器示例 ===") + processor = AsyncFileProcessor("example.txt") + async for line in processor: + print(f"处理行: {line}") + +# 运行异步上下文示例 +if __name__ == "__main__": + asyncio.run(async_context_examples()) +``` + +--- + +## 四、内存管理和性能优化 + +### 4.1 内存管理基础 + +```python +import gc +import sys +import weakref +from memory_profiler import profile # 需要安装: pip install memory-profiler + +class MemoryExample: + """内存管理示例类""" + + def __init__(self, data): + self.data = data + self.refs = [] + + def add_reference(self, obj): + self.refs.append(obj) + + def __del__(self): + print(f"MemoryExample 对象被销毁: {id(self)}") + +def memory_management_examples(): + """内存管理示例""" + print("=== 内存管理示例 ===") + + # 查看对象引用计数 + obj = MemoryExample("test data") + print(f"对象引用计数: {sys.getrefcount(obj)}") + + # 创建引用 + ref1 = obj + print(f"创建引用后计数: {sys.getrefcount(obj)}") + + # 删除引用 + del ref1 + print(f"删除引用后计数: {sys.getrefcount(obj)}") + + # 循环引用问题 + print("\n=== 循环引用示例 ===") + obj1 = MemoryExample("obj1") + obj2 = MemoryExample("obj2") + + # 创建循环引用 + obj1.add_reference(obj2) + obj2.add_reference(obj1) + + print(f"obj1 引用计数: {sys.getrefcount(obj1)}") + print(f"obj2 引用计数: {sys.getrefcount(obj2)}") + + # 删除主引用 + del obj1, obj2 + + # 手动触发垃圾回收 + print("触发垃圾回收前...") + collected = gc.collect() + print(f"垃圾回收清理了 {collected} 个对象") + + # 弱引用示例 + print("\n=== 弱引用示例 ===") + + class Observer: + def __init__(self, name): + self.name = name + + def notify(self, message): + print(f"{self.name} 收到通知: {message}") + + def __del__(self): + print(f"Observer {self.name} 被销毁") + + class Subject: + def __init__(self): + self._observers = set() + + def attach(self, observer): + # 使用弱引用避免循环引用 + self._observers.add(weakref.ref(observer)) + + def notify(self, message): + # 清理已失效的弱引用 + dead_refs = set() + for obs_ref in self._observers: + observer = obs_ref() + if observer is None: + dead_refs.add(obs_ref) + else: + observer.notify(message) + + # 移除失效引用 + self._observers -= dead_refs + + subject = Subject() + + # 创建观察者 + obs1 = Observer("观察者1") + obs2 = Observer("观察者2") + + subject.attach(obs1) + subject.attach(obs2) + + subject.notify("第一条消息") + + # 删除一个观察者 + del obs1 + + subject.notify("第二条消息") + + del obs2 + subject.notify("第三条消息") + +# @profile # 取消注释以启用内存分析 +def memory_intensive_function(): + """内存密集型函数示例""" + # 创建大量对象 + data = [] + for i in range(100000): + data.append({"id": i, "value": f"item_{i}"}) + + # 处理数据 + processed = [] + for item in data: + if item["id"] % 2 == 0: + processed.append(item["value"].upper()) + + return len(processed) + +def performance_optimization_examples(): + """性能优化示例""" + print("\n=== 性能优化示例 ===") + + import time + from functools import lru_cache + + # 缓存装饰器示例 + @lru_cache(maxsize=128) + def fibonacci_cached(n): + """带缓存的斐波那契数列""" + if n < 2: + return n + return fibonacci_cached(n-1) + fibonacci_cached(n-2) + + def fibonacci_normal(n): + """普通斐波那契数列""" + if n < 2: + return n + return fibonacci_normal(n-1) + fibonacci_normal(n-2) + + # 性能对比 + n = 30 + + # 普通版本 + start_time = time.time() + result_normal = fibonacci_normal(n) + normal_time = time.time() - start_time + + # 缓存版本 + start_time = time.time() + result_cached = fibonacci_cached(n) + cached_time = time.time() - start_time + + print(f"普通版本: 结果={result_normal}, 耗时={normal_time:.4f}秒") + print(f"缓存版本: 结果={result_cached}, 耗时={cached_time:.4f}秒") + print(f"性能提升: {normal_time/cached_time:.2f}倍") + + # 查看缓存信息 + print(f"缓存信息: {fibonacci_cached.cache_info()}") + + # 列表推导式 vs 循环 + print("\n=== 列表推导式性能对比 ===") + + data = range(100000) + + # 使用循环 + start_time = time.time() + result_loop = [] + for x in data: + if x % 2 == 0: + result_loop.append(x * 2) + loop_time = time.time() - start_time + + # 使用列表推导式 + start_time = time.time() + result_comprehension = [x * 2 for x in data if x % 2 == 0] + comp_time = time.time() - start_time + + print(f"循环方式: 耗时={loop_time:.4f}秒") + print(f"列表推导式: 耗时={comp_time:.4f}秒") + print(f"性能提升: {loop_time/comp_time:.2f}倍") + +# 运行示例 +if __name__ == "__main__": + memory_management_examples() + performance_optimization_examples() + + # 运行内存密集型函数 + print("\n=== 内存密集型函数 ===") + result = memory_intensive_function() + print(f"处理结果: {result}") +``` + +--- + +## 五、设计模式 + +### 5.1 创建型模式 + +```python +from abc import ABC, abstractmethod +from typing import Dict, Any +import copy + +# 单例模式 +class Singleton: + """单例模式实现""" + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + self.data = {} + self._initialized = True + + def set_data(self, key, value): + self.data[key] = value + + def get_data(self, key): + return self.data.get(key) + +# 工厂模式 +class Animal(ABC): + """动物抽象基类""" + + @abstractmethod + def make_sound(self) -> str: + pass + + @abstractmethod + def get_type(self) -> str: + pass + +class Dog(Animal): + def make_sound(self) -> str: + return "汪汪" + + def get_type(self) -> str: + return "狗" + +class Cat(Animal): + def make_sound(self) -> str: + return "喵喵" + + def get_type(self) -> str: + return "猫" + +class Bird(Animal): + def make_sound(self) -> str: + return "啾啾" + + def get_type(self) -> str: + return "鸟" + +class AnimalFactory: + """动物工厂""" + + _animals = { + 'dog': Dog, + 'cat': Cat, + 'bird': Bird + } + + @classmethod + def create_animal(cls, animal_type: str) -> Animal: + animal_class = cls._animals.get(animal_type.lower()) + if animal_class is None: + raise ValueError(f"不支持的动物类型: {animal_type}") + return animal_class() + + @classmethod + def get_available_types(cls): + return list(cls._animals.keys()) + +# 建造者模式 +class Computer: + """计算机类""" + + def __init__(self): + self.cpu = None + self.memory = None + self.storage = None + self.graphics = None + self.price = 0 + + def __str__(self): + return f"Computer(CPU: {self.cpu}, Memory: {self.memory}, Storage: {self.storage}, Graphics: {self.graphics}, Price: ¥{self.price})" + +class ComputerBuilder: + """计算机建造者""" + + def __init__(self): + self.computer = Computer() + + def set_cpu(self, cpu: str, price: int): + self.computer.cpu = cpu + self.computer.price += price + return self + + def set_memory(self, memory: str, price: int): + self.computer.memory = memory + self.computer.price += price + return self + + def set_storage(self, storage: str, price: int): + self.computer.storage = storage + self.computer.price += price + return self + + def set_graphics(self, graphics: str, price: int): + self.computer.graphics = graphics + self.computer.price += price + return self + + def build(self) -> Computer: + return self.computer + +class ComputerDirector: + """计算机构建指导者""" + + @staticmethod + def build_gaming_computer(): + """构建游戏电脑""" + return (ComputerBuilder() + .set_cpu("Intel i7-12700K", 3000) + .set_memory("32GB DDR4", 1200) + .set_storage("1TB NVMe SSD", 800) + .set_graphics("RTX 4070", 4500) + .build()) + + @staticmethod + def build_office_computer(): + """构建办公电脑""" + return (ComputerBuilder() + .set_cpu("Intel i5-12400", 1500) + .set_memory("16GB DDR4", 600) + .set_storage("512GB SSD", 400) + .set_graphics("集成显卡", 0) + .build()) + +# 原型模式 +class Prototype(ABC): + """原型抽象基类""" + + @abstractmethod + def clone(self): + pass + +class Document(Prototype): + """文档类""" + + def __init__(self, title: str, content: str, metadata: Dict[str, Any] = None): + self.title = title + self.content = content + self.metadata = metadata or {} + self.created_at = time.time() + + def clone(self): + """深拷贝克隆""" + return copy.deepcopy(self) + + def shallow_clone(self): + """浅拷贝克隆""" + return copy.copy(self) + + def __str__(self): + return f"Document(title='{self.title}', content_length={len(self.content)}, metadata={self.metadata})" + +# 使用示例 +def design_patterns_examples(): + """设计模式示例""" + print("=== 设计模式示例 ===") + + # 单例模式 + print("\n=== 单例模式 ===") + singleton1 = Singleton() + singleton2 = Singleton() + + print(f"singleton1 is singleton2: {singleton1 is singleton2}") + + singleton1.set_data("key1", "value1") + print(f"singleton2.get_data('key1'): {singleton2.get_data('key1')}") + + # 工厂模式 + print("\n=== 工厂模式 ===") + print(f"可用动物类型: {AnimalFactory.get_available_types()}") + + animals = [] + for animal_type in ['dog', 'cat', 'bird']: + animal = AnimalFactory.create_animal(animal_type) + animals.append(animal) + print(f"{animal.get_type()}: {animal.make_sound()}") + + # 建造者模式 + print("\n=== 建造者模式 ===") + gaming_pc = ComputerDirector.build_gaming_computer() + office_pc = ComputerDirector.build_office_computer() + + print(f"游戏电脑: {gaming_pc}") + print(f"办公电脑: {office_pc}") + + # 自定义配置 + custom_pc = (ComputerBuilder() + .set_cpu("AMD Ryzen 7", 2500) + .set_memory("64GB DDR4", 2000) + .set_storage("2TB NVMe SSD", 1500) + .set_graphics("RTX 4080", 6000) + .build()) + print(f"自定义电脑: {custom_pc}") + + # 原型模式 + print("\n=== 原型模式 ===") + original_doc = Document( + "原始文档", + "这是原始文档的内容", + {"author": "张三", "version": 1.0} + ) + + # 深拷贝 + cloned_doc = original_doc.clone() + cloned_doc.title = "克隆文档" + cloned_doc.metadata["author"] = "李四" + + print(f"原始文档: {original_doc}") + print(f"克隆文档: {cloned_doc}") + print(f"元数据是否相同对象: {original_doc.metadata is cloned_doc.metadata}") + + # 浅拷贝 + shallow_doc = original_doc.shallow_clone() + shallow_doc.title = "浅拷贝文档" + shallow_doc.metadata["version"] = 2.0 # 会影响原始文档 + + print(f"浅拷贝后原始文档: {original_doc}") + print(f"浅拷贝文档: {shallow_doc}") + +# 运行示例 +if __name__ == "__main__": + design_patterns_examples() +``` + +### 5.2 结构型和行为型模式 + +```python +from abc import ABC, abstractmethod +from typing import List, Dict, Any +import time + +# 装饰器模式 +class Coffee(ABC): + """咖啡抽象基类""" + + @abstractmethod + def get_description(self) -> str: + pass + + @abstractmethod + def get_cost(self) -> float: + pass + +class SimpleCoffee(Coffee): + """简单咖啡""" + + def get_description(self) -> str: + return "简单咖啡" + + def get_cost(self) -> float: + return 10.0 + +class CoffeeDecorator(Coffee): + """咖啡装饰器基类""" + + def __init__(self, coffee: Coffee): + self._coffee = coffee + + def get_description(self) -> str: + return self._coffee.get_description() + + def get_cost(self) -> float: + return self._coffee.get_cost() + +class MilkDecorator(CoffeeDecorator): + """牛奶装饰器""" + + def get_description(self) -> str: + return self._coffee.get_description() + ", 牛奶" + + def get_cost(self) -> float: + return self._coffee.get_cost() + 2.0 + +class SugarDecorator(CoffeeDecorator): + """糖装饰器""" + + def get_description(self) -> str: + return self._coffee.get_description() + ", 糖" + + def get_cost(self) -> float: + return self._coffee.get_cost() + 1.0 + +class WhipDecorator(CoffeeDecorator): + """奶泡装饰器""" + + def get_description(self) -> str: + return self._coffee.get_description() + ", 奶泡" + + def get_cost(self) -> float: + return self._coffee.get_cost() + 3.0 + +# 观察者模式 +class Observer(ABC): + """观察者接口""" + + @abstractmethod + def update(self, subject, data: Any): + pass + +class Subject: + """主题类""" + + def __init__(self): + self._observers: List[Observer] = [] + self._state = None + + def attach(self, observer: Observer): + """添加观察者""" + if observer not in self._observers: + self._observers.append(observer) + print(f"观察者 {observer.__class__.__name__} 已添加") + + def detach(self, observer: Observer): + """移除观察者""" + if observer in self._observers: + self._observers.remove(observer) + print(f"观察者 {observer.__class__.__name__} 已移除") + + def notify(self, data: Any = None): + """通知所有观察者""" + print(f"通知 {len(self._observers)} 个观察者") + for observer in self._observers: + observer.update(self, data) + + def set_state(self, state: Any): + """设置状态并通知观察者""" + self._state = state + self.notify(state) + + def get_state(self): + return self._state + +class EmailNotifier(Observer): + """邮件通知观察者""" + + def __init__(self, email: str): + self.email = email + + def update(self, subject, data: Any): + print(f"邮件通知 ({self.email}): 收到更新 - {data}") + +class SMSNotifier(Observer): + """短信通知观察者""" + + def __init__(self, phone: str): + self.phone = phone + + def update(self, subject, data: Any): + print(f"短信通知 ({self.phone}): 收到更新 - {data}") + +class LogObserver(Observer): + """日志观察者""" + + def update(self, subject, data: Any): + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + print(f"日志记录 [{timestamp}]: 状态变更为 {data}") + +# 策略模式 +class PaymentStrategy(ABC): + """支付策略接口""" + + @abstractmethod + def pay(self, amount: float) -> str: + pass + +class CreditCardPayment(PaymentStrategy): + """信用卡支付""" + + def __init__(self, card_number: str, holder_name: str): + self.card_number = card_number[-4:] # 只保留后4位 + self.holder_name = holder_name + + def pay(self, amount: float) -> str: + return f"使用信用卡支付 ¥{amount:.2f} (卡号: ****{self.card_number}, 持卡人: {self.holder_name})" + +class AlipayPayment(PaymentStrategy): + """支付宝支付""" + + def __init__(self, account: str): + self.account = account + + def pay(self, amount: float) -> str: + return f"使用支付宝支付 ¥{amount:.2f} (账户: {self.account})" + +class WeChatPayment(PaymentStrategy): + """微信支付""" + + def __init__(self, account: str): + self.account = account + + def pay(self, amount: float) -> str: + return f"使用微信支付 ¥{amount:.2f} (账户: {self.account})" + +class PaymentContext: + """支付上下文""" + + def __init__(self, strategy: PaymentStrategy): + self._strategy = strategy + + def set_strategy(self, strategy: PaymentStrategy): + self._strategy = strategy + + def execute_payment(self, amount: float) -> str: + return self._strategy.pay(amount) + +# 命令模式 +class Command(ABC): + """命令接口""" + + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + +class Light: + """灯具类""" + + def __init__(self, location: str): + self.location = location + self.is_on = False + + def turn_on(self): + self.is_on = True + print(f"{self.location}的灯已打开") + + def turn_off(self): + self.is_on = False + print(f"{self.location}的灯已关闭") + +class LightOnCommand(Command): + """开灯命令""" + + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.turn_on() + + def undo(self): + self.light.turn_off() + +class LightOffCommand(Command): + """关灯命令""" + + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.turn_off() + + def undo(self): + self.light.turn_on() + +class RemoteControl: + """遥控器""" + + def __init__(self): + self.commands: Dict[str, Command] = {} + self.last_command: Command = None + + def set_command(self, slot: str, command: Command): + self.commands[slot] = command + + def press_button(self, slot: str): + if slot in self.commands: + command = self.commands[slot] + command.execute() + self.last_command = command + else: + print(f"按钮 {slot} 未设置命令") + + def press_undo(self): + if self.last_command: + self.last_command.undo() + else: + print("没有可撤销的命令") + +# 使用示例 +def advanced_patterns_examples(): + """高级设计模式示例""" + print("=== 高级设计模式示例 ===") + + # 装饰器模式 + print("\n=== 装饰器模式 - 咖啡订制 ===") + coffee = SimpleCoffee() + print(f"{coffee.get_description()}: ¥{coffee.get_cost():.2f}") + + # 添加牛奶 + coffee = MilkDecorator(coffee) + print(f"{coffee.get_description()}: ¥{coffee.get_cost():.2f}") + + # 添加糖 + coffee = SugarDecorator(coffee) + print(f"{coffee.get_description()}: ¥{coffee.get_cost():.2f}") + + # 添加奶泡 + coffee = WhipDecorator(coffee) + print(f"{coffee.get_description()}: ¥{coffee.get_cost():.2f}") + + # 观察者模式 + print("\n=== 观察者模式 - 消息通知 ===") + news_agency = Subject() + + # 创建观察者 + email_notifier = EmailNotifier("user@example.com") + sms_notifier = SMSNotifier("13800138000") + log_observer = LogObserver() + + # 添加观察者 + news_agency.attach(email_notifier) + news_agency.attach(sms_notifier) + news_agency.attach(log_observer) + + # 发布新闻 + news_agency.set_state("Python 3.12 正式发布!") + + print() + # 移除一个观察者 + news_agency.detach(sms_notifier) + news_agency.set_state("新的Python教程上线了!") + + # 策略模式 + print("\n=== 策略模式 - 支付方式 ===") + + # 创建不同的支付策略 + credit_card = CreditCardPayment("1234567890123456", "张三") + alipay = AlipayPayment("zhangsan@example.com") + wechat = WeChatPayment("zhangsan_wx") + + # 使用支付上下文 + payment_context = PaymentContext(credit_card) + print(payment_context.execute_payment(100.0)) + + # 切换支付策略 + payment_context.set_strategy(alipay) + print(payment_context.execute_payment(200.0)) + + payment_context.set_strategy(wechat) + print(payment_context.execute_payment(50.0)) + + # 命令模式 + print("\n=== 命令模式 - 智能家居 ===") + + # 创建设备 + living_room_light = Light("客厅") + bedroom_light = Light("卧室") + + # 创建命令 + living_room_on = LightOnCommand(living_room_light) + living_room_off = LightOffCommand(living_room_light) + bedroom_on = LightOnCommand(bedroom_light) + bedroom_off = LightOffCommand(bedroom_light) + + # 创建遥控器 + remote = RemoteControl() + + # 设置命令 + remote.set_command("living_room_on", living_room_on) + remote.set_command("living_room_off", living_room_off) + remote.set_command("bedroom_on", bedroom_on) + remote.set_command("bedroom_off", bedroom_off) + + # 使用遥控器 + remote.press_button("living_room_on") + remote.press_button("bedroom_on") + remote.press_button("living_room_off") + + # 撤销最后一个命令 + remote.press_undo() + + remote.press_button("bedroom_off") + remote.press_undo() + +# 运行示例 +if __name__ == "__main__": + advanced_patterns_examples() +``` + +--- + +## 六、调试和性能分析 + +### 6.1 调试技巧 + +```python +import pdb +import traceback +import logging +from functools import wraps +import time + +# 配置日志 +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def debug_decorator(func): + """调试装饰器""" + @wraps(func) + def wrapper(*args, **kwargs): + logger.debug(f"调用函数 {func.__name__},参数: args={args}, kwargs={kwargs}") + try: + result = func(*args, **kwargs) + logger.debug(f"函数 {func.__name__} 返回: {result}") + return result + except Exception as e: + logger.error(f"函数 {func.__name__} 发生异常: {e}") + logger.error(f"异常详情:\n{traceback.format_exc()}") + raise + return wrapper + +def timing_decorator(func): + """计时装饰器""" + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + print(f"函数 {func.__name__} 执行时间: {end_time - start_time:.4f}秒") + return result + return wrapper + +class DebugContext: + """调试上下文管理器""" + + def __init__(self, name): + self.name = name + + def __enter__(self): + print(f"开始调试: {self.name}") + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: + print(f"调试 {self.name} 时发生异常: {exc_type.__name__}: {exc_value}") + return False # 不抑制异常 + print(f"调试 {self.name} 完成") + +@debug_decorator +@timing_decorator +def complex_calculation(n): + """复杂计算示例""" + if n < 0: + raise ValueError("n 必须是非负数") + + result = 0 + for i in range(n): + result += i ** 2 + if i == n // 2: + # 在中间设置断点进行调试 + # pdb.set_trace() # 取消注释以启用断点 + pass + + return result + +def debugging_examples(): + """调试示例""" + print("=== 调试技巧示例 ===") + + # 使用调试上下文 + with DebugContext("计算测试"): + try: + result = complex_calculation(1000) + print(f"计算结果: {result}") + except ValueError as e: + print(f"捕获到值错误: {e}") + + # 测试异常情况 + with DebugContext("异常测试"): + try: + result = complex_calculation(-5) + except ValueError as e: + print(f"预期的异常: {e}") + + # 使用断言进行调试 + def test_function(x, y): + assert isinstance(x, (int, float)), f"x 必须是数字,实际类型: {type(x)}" + assert isinstance(y, (int, float)), f"y 必须是数字,实际类型: {type(y)}" + assert y != 0, "y 不能为零" + + result = x / y + assert result > 0, f"结果必须为正数,实际值: {result}" + return result + + print("\n=== 断言测试 ===") + try: + print(f"10 / 2 = {test_function(10, 2)}") + print(f"10 / -2 = {test_function(10, -2)}") + except AssertionError as e: + print(f"断言失败: {e}") +``` + +### 6.2 性能分析 + +```python +import cProfile +import pstats +import io +from line_profiler import LineProfiler # 需要安装: pip install line_profiler +from memory_profiler import profile as memory_profile # 需要安装: pip install memory-profiler + +def profile_function(func): + """性能分析装饰器""" + @wraps(func) + def wrapper(*args, **kwargs): + pr = cProfile.Profile() + pr.enable() + + result = func(*args, **kwargs) + + pr.disable() + + # 创建统计信息 + s = io.StringIO() + ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') + ps.print_stats(10) # 显示前10个最耗时的函数 + + print(f"\n=== {func.__name__} 性能分析 ===") + print(s.getvalue()) + + return result + return wrapper + +@profile_function +def inefficient_function(): + """低效函数示例""" + # 低效的字符串拼接 + result = "" + for i in range(10000): + result += str(i) + return result + +@profile_function +def efficient_function(): + """高效函数示例""" + # 高效的字符串拼接 + parts = [] + for i in range(10000): + parts.append(str(i)) + return ''.join(parts) + +# @memory_profile # 取消注释以启用内存分析 +def memory_intensive_task(): + """内存密集型任务""" + # 创建大量数据 + data = [] + for i in range(100000): + data.append([j for j in range(100)]) + + # 处理数据 + processed = [] + for item in data: + processed.append(sum(item)) + + return len(processed) + +def performance_analysis_examples(): + """性能分析示例""" + print("=== 性能分析示例 ===") + + # 比较低效和高效的实现 + print("\n=== 字符串拼接性能对比 ===") + + start_time = time.time() + result1 = inefficient_function() + inefficient_time = time.time() - start_time + + start_time = time.time() + result2 = efficient_function() + efficient_time = time.time() - start_time + + print(f"低效方法耗时: {inefficient_time:.4f}秒") + print(f"高效方法耗时: {efficient_time:.4f}秒") + print(f"性能提升: {inefficient_time/efficient_time:.2f}倍") + + # 内存使用分析 + print("\n=== 内存使用分析 ===") + result = memory_intensive_task() + print(f"处理了 {result} 个数据项") + +# 运行示例 +if __name__ == "__main__": + debugging_examples() + performance_analysis_examples() +``` + +--- + +## 七、实战项目:高级任务管理系统 + +```python +import asyncio +import json +import sqlite3 +from abc import ABC, abstractmethod +from dataclasses import dataclass, asdict +from datetime import datetime, timedelta +from enum import Enum +from typing import List, Optional, Dict, Any +from contextlib import asynccontextmanager +import weakref + +class Priority(Enum): + """任务优先级""" + LOW = 1 + MEDIUM = 2 + HIGH = 3 + URGENT = 4 + +class TaskStatus(Enum): + """任务状态""" + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + +@dataclass +class Task: + """任务数据类""" + id: int + title: str + description: str + priority: Priority + status: TaskStatus + created_at: datetime + due_date: Optional[datetime] = None + completed_at: Optional[datetime] = None + tags: List[str] = None + + def __post_init__(self): + if self.tags is None: + self.tags = [] + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + data = asdict(self) + data['priority'] = self.priority.value + data['status'] = self.status.value + data['created_at'] = self.created_at.isoformat() + if self.due_date: + data['due_date'] = self.due_date.isoformat() + if self.completed_at: + data['completed_at'] = self.completed_at.isoformat() + return data + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Task': + """从字典创建任务""" + data['priority'] = Priority(data['priority']) + data['status'] = TaskStatus(data['status']) + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('due_date'): + data['due_date'] = datetime.fromisoformat(data['due_date']) + if data.get('completed_at'): + data['completed_at'] = datetime.fromisoformat(data['completed_at']) + return cls(**data) + +class Observer(ABC): + """观察者接口""" + + @abstractmethod + async def on_task_created(self, task: Task): + pass + + @abstractmethod + async def on_task_updated(self, task: Task): + pass + + @abstractmethod + async def on_task_deleted(self, task_id: int): + pass + +class TaskRepository(ABC): + """任务仓库接口""" + + @abstractmethod + async def create(self, task: Task) -> Task: + pass + + @abstractmethod + async def get_by_id(self, task_id: int) -> Optional[Task]: + pass + + @abstractmethod + async def get_all(self) -> List[Task]: + pass + + @abstractmethod + async def update(self, task: Task) -> Task: + pass + + @abstractmethod + async def delete(self, task_id: int) -> bool: + pass + +class SQLiteTaskRepository(TaskRepository): + """SQLite任务仓库实现""" + + def __init__(self, db_path: str = "tasks.db"): + self.db_path = db_path + self._init_db() + + def _init_db(self): + """初始化数据库""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + priority INTEGER NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + due_date TEXT, + completed_at TEXT, + tags TEXT + ) + """) + conn.commit() + conn.close() + + async def create(self, task: Task) -> Task: + """创建任务""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO tasks (title, description, priority, status, created_at, due_date, completed_at, tags) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + task.title, + task.description, + task.priority.value, + task.status.value, + task.created_at.isoformat(), + task.due_date.isoformat() if task.due_date else None, + task.completed_at.isoformat() if task.completed_at else None, + json.dumps(task.tags) + )) + + task.id = cursor.lastrowid + conn.commit() + conn.close() + + return task + + async def get_by_id(self, task_id: int) -> Optional[Task]: + """根据ID获取任务""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) + row = cursor.fetchone() + conn.close() + + if row: + return self._row_to_task(row) + return None + + async def get_all(self) -> List[Task]: + """获取所有任务""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM tasks ORDER BY created_at DESC") + rows = cursor.fetchall() + conn.close() + + return [self._row_to_task(row) for row in rows] + + async def update(self, task: Task) -> Task: + """更新任务""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + UPDATE tasks SET + title = ?, description = ?, priority = ?, status = ?, + due_date = ?, completed_at = ?, tags = ? + WHERE id = ? + """, ( + task.title, + task.description, + task.priority.value, + task.status.value, + task.due_date.isoformat() if task.due_date else None, + task.completed_at.isoformat() if task.completed_at else None, + json.dumps(task.tags), + task.id + )) + + conn.commit() + conn.close() + + return task + + async def delete(self, task_id: int) -> bool: + """删除任务""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,)) + deleted = cursor.rowcount > 0 + + conn.commit() + conn.close() + + return deleted + + def _row_to_task(self, row) -> Task: + """将数据库行转换为Task对象""" + return Task( + id=row[0], + title=row[1], + description=row[2], + priority=Priority(row[3]), + status=TaskStatus(row[4]), + created_at=datetime.fromisoformat(row[5]), + due_date=datetime.fromisoformat(row[6]) if row[6] else None, + completed_at=datetime.fromisoformat(row[7]) if row[7] else None, + tags=json.loads(row[8]) if row[8] else [] + ) + +class NotificationObserver(Observer): + """通知观察者""" + + async def on_task_created(self, task: Task): + print(f"📝 新任务创建: {task.title}") + + async def on_task_updated(self, task: Task): + print(f"✏️ 任务更新: {task.title} - 状态: {task.status.value}") + + async def on_task_deleted(self, task_id: int): + print(f"🗑️ 任务删除: ID {task_id}") + +class TaskManager: + """任务管理器""" + + def __init__(self, repository: TaskRepository): + self.repository = repository + self._observers: List[weakref.ReferenceType] = [] + self._next_id = 1 + + def add_observer(self, observer: Observer): + """添加观察者""" + self._observers.append(weakref.ref(observer)) + + def remove_observer(self, observer: Observer): + """移除观察者""" + self._observers = [ref for ref in self._observers if ref() is not observer] + + async def _notify_observers(self, method_name: str, *args): + """通知观察者""" + dead_refs = [] + for ref in self._observers: + observer = ref() + if observer is None: + dead_refs.append(ref) + else: + method = getattr(observer, method_name) + await method(*args) + + # 清理失效的引用 + for ref in dead_refs: + self._observers.remove(ref) + + async def create_task( + self, + title: str, + description: str = "", + priority: Priority = Priority.MEDIUM, + due_date: Optional[datetime] = None, + tags: List[str] = None + ) -> Task: + """创建任务""" + task = Task( + id=0, # 将由仓库设置 + title=title, + description=description, + priority=priority, + status=TaskStatus.PENDING, + created_at=datetime.now(), + due_date=due_date, + tags=tags or [] + ) + + task = await self.repository.create(task) + await self._notify_observers('on_task_created', task) + return task + + async def get_task(self, task_id: int) -> Optional[Task]: + """获取任务""" + return await self.repository.get_by_id(task_id) + + async def get_all_tasks(self) -> List[Task]: + """获取所有任务""" + return await self.repository.get_all() + + async def update_task_status(self, task_id: int, status: TaskStatus) -> Optional[Task]: + """更新任务状态""" + task = await self.repository.get_by_id(task_id) + if not task: + return None + + task.status = status + if status == TaskStatus.COMPLETED: + task.completed_at = datetime.now() + + task = await self.repository.update(task) + await self._notify_observers('on_task_updated', task) + return task + + async def delete_task(self, task_id: int) -> bool: + """删除任务""" + success = await self.repository.delete(task_id) + if success: + await self._notify_observers('on_task_deleted', task_id) + return success + + async def get_overdue_tasks(self) -> List[Task]: + """获取过期任务""" + all_tasks = await self.repository.get_all() + now = datetime.now() + + return [ + task for task in all_tasks + if task.due_date and task.due_date < now and task.status != TaskStatus.COMPLETED + ] + + async def get_tasks_by_priority(self, priority: Priority) -> List[Task]: + """根据优先级获取任务""" + all_tasks = await self.repository.get_all() + return [task for task in all_tasks if task.priority == priority] + +@asynccontextmanager +async def task_manager_context(db_path: str = "tasks.db"): + """任务管理器上下文管理器""" + repository = SQLiteTaskRepository(db_path) + manager = TaskManager(repository) + + # 添加通知观察者 + notifier = NotificationObserver() + manager.add_observer(notifier) + + try: + yield manager + finally: + print("任务管理器已关闭") + +async def demo_task_management_system(): + """演示任务管理系统""" + print("=== 高级任务管理系统演示 ===") + + async with task_manager_context() as manager: + # 创建任务 + task1 = await manager.create_task( + "学习Python高级编程", + "深入学习元类、描述符、协程等高级特性", + Priority.HIGH, + datetime.now() + timedelta(days=7), + ["学习", "编程"] + ) + + task2 = await manager.create_task( + "完成项目文档", + "编写项目的技术文档和用户手册", + Priority.MEDIUM, + datetime.now() + timedelta(days=3), + ["文档", "项目"] + ) + + task3 = await manager.create_task( + "代码审查", + "审查团队成员提交的代码", + Priority.URGENT, + datetime.now() + timedelta(hours=2), + ["代码审查", "团队"] + ) + + # 获取所有任务 + print("\n=== 所有任务 ===") + all_tasks = await manager.get_all_tasks() + for task in all_tasks: + print(f"ID: {task.id}, 标题: {task.title}, 优先级: {task.priority.name}, 状态: {task.status.value}") + + # 更新任务状态 + print("\n=== 更新任务状态 ===") + await manager.update_task_status(task2.id, TaskStatus.IN_PROGRESS) + await manager.update_task_status(task3.id, TaskStatus.COMPLETED) + + # 获取高优先级任务 + print("\n=== 高优先级任务 ===") + high_priority_tasks = await manager.get_tasks_by_priority(Priority.HIGH) + for task in high_priority_tasks: + print(f"- {task.title}") + + # 检查过期任务 + print("\n=== 过期任务检查 ===") + overdue_tasks = await manager.get_overdue_tasks() + if overdue_tasks: + for task in overdue_tasks: + print(f"⚠️ 过期任务: {task.title} (截止日期: {task.due_date})") + else: + print("✅ 没有过期任务") + +# 运行演示 +if __name__ == "__main__": + asyncio.run(demo_task_management_system()) +``` + +--- + +## 总结 + +通过本章的学习,你已经掌握了Python的高级编程技巧: + +### 核心概念 + +1. **元类(Metaclass)** + - 理解"类的类"概念 + - 使用`type`动态创建类 + - 自定义元类实现特殊功能 + - 实际应用:ORM、单例模式、验证等 + +2. **描述符(Descriptor)** + - 掌握描述符协议 + - 实现属性验证和缓存 + - 理解Python内置描述符的工作原理 + +3. **协程和异步编程** + - 理解生成器、协程和异步函数的区别 + - 掌握`async/await`语法 + - 使用异步上下文管理器和迭代器 + - 并发编程最佳实践 + +4. **内存管理和性能优化** + - 理解Python的内存管理机制 + - 避免内存泄漏和循环引用 + - 使用性能分析工具 + - 优化代码性能 + +5. **设计模式** + - 创建型模式:单例、工厂、建造者、原型 + - 结构型模式:装饰器 + - 行为型模式:观察者、策略、命令 + +6. **调试和性能分析** + - 使用调试工具和技巧 + - 性能分析和优化方法 + - 日志和错误处理 + +### 最佳实践 + +1. **代码质量** + - 使用类型注解提高代码可读性 + - 编写单元测试和文档 + - 遵循PEP 8编码规范 + +2. **性能优化** + - 选择合适的数据结构 + - 使用生成器处理大数据 + - 合理使用缓存 + - 避免过早优化 + +3. **异步编程** + - 理解何时使用异步 + - 避免阻塞操作 + - 正确处理异常 + - 使用连接池等资源管理 + +### 下一步学习方向 + +1. **Web开发框架** + - FastAPI(异步Web框架) + - Django(全功能Web框架) + - Flask(轻量级Web框架) + +2. **数据科学和机器学习** + - NumPy、Pandas(数据处理) + - Scikit-learn(机器学习) + - TensorFlow、PyTorch(深度学习) + +3. **系统编程** + - 多进程和多线程 + - 网络编程 + - 系统监控和自动化 + +4. **DevOps和部署** + - Docker容器化 + - CI/CD流水线 + - 云平台部署 + +### 练习建议 + +1. **实现一个简单的ORM框架** +2. **创建一个异步Web爬虫** +3. **设计一个插件系统** +4. **开发一个性能监控工具** +5. **构建一个分布式任务队列** + +继续保持学习的热情,Python的高级特性将帮助你编写更优雅、更高效的代码! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/11.md b/docs/Python/11.md new file mode 100644 index 000000000..f15e36cff --- /dev/null +++ b/docs/Python/11.md @@ -0,0 +1,1988 @@ +--- +title: 第11天-错误和调试 +author: 哪吒 +date: '2023-06-15' +--- + +# 第11天-错误和调试 + +## 学习目标 + +通过本章学习,你将掌握: + +- Python中常见的错误类型和异常处理 +- 使用try-except语句处理异常 +- 自定义异常类 +- 调试技巧和工具 +- 日志记录和错误追踪 +- 单元测试和测试驱动开发 +- 性能分析和优化 + +--- + +## 一、Python异常处理基础 + +### 1.1 什么是异常? + +异常是程序运行时发生的错误,它会中断程序的正常执行流程。Python使用异常机制来处理运行时错误。 + +```python +# 常见的异常示例 + +# 1. ZeroDivisionError - 除零错误 +try: + result = 10 / 0 +except ZeroDivisionError: + print("不能除以零!") + +# 2. IndexError - 索引错误 +try: + my_list = [1, 2, 3] + print(my_list[10]) # 索引超出范围 +except IndexError: + print("列表索引超出范围!") + +# 3. KeyError - 键错误 +try: + my_dict = {'name': '张三', 'age': 25} + print(my_dict['salary']) # 键不存在 +except KeyError: + print("字典中不存在该键!") + +# 4. TypeError - 类型错误 +try: + result = "hello" + 5 # 字符串和数字不能直接相加 +except TypeError: + print("类型不匹配!") + +# 5. ValueError - 值错误 +try: + number = int("abc") # 无法将字符串转换为整数 +except ValueError: + print("值转换错误!") + +# 6. FileNotFoundError - 文件未找到错误 +try: + with open("不存在的文件.txt", "r") as file: + content = file.read() +except FileNotFoundError: + print("文件未找到!") +``` + +### 1.2 异常处理语法 + +```python +# 基本的try-except语法 +try: + # 可能出现异常的代码 + risky_code() +except ExceptionType: + # 处理特定类型的异常 + handle_exception() + +# 完整的异常处理语法 +try: + # 可能出现异常的代码 + risky_code() +except SpecificException as e: + # 处理特定异常 + print(f"发生特定异常: {e}") +except (Exception1, Exception2) as e: + # 处理多种异常 + print(f"发生多种异常之一: {e}") +except Exception as e: + # 处理所有其他异常 + print(f"发生未知异常: {e}") +else: + # 没有异常时执行 + print("代码执行成功") +finally: + # 无论是否有异常都会执行 + print("清理资源") +``` + +### 1.3 实际应用示例 + +```python +def safe_divide(a, b): + """安全的除法函数""" + try: + result = a / b + return result + except ZeroDivisionError: + print("错误:除数不能为零") + return None + except TypeError: + print("错误:参数必须是数字") + return None + except Exception as e: + print(f"未知错误:{e}") + return None + +def safe_file_read(filename): + """安全的文件读取函数""" + try: + with open(filename, 'r', encoding='utf-8') as file: + content = file.read() + return content + except FileNotFoundError: + print(f"文件 {filename} 不存在") + return None + except PermissionError: + print(f"没有权限读取文件 {filename}") + return None + except UnicodeDecodeError: + print(f"文件 {filename} 编码格式不正确") + return None + except Exception as e: + print(f"读取文件时发生未知错误:{e}") + return None + finally: + print(f"文件读取操作完成") + +def safe_user_input(): + """安全的用户输入处理""" + while True: + try: + age = int(input("请输入您的年龄:")) + if age < 0: + raise ValueError("年龄不能为负数") + if age > 150: + raise ValueError("年龄不能超过150岁") + return age + except ValueError as e: + if "invalid literal" in str(e): + print("请输入有效的数字") + else: + print(f"输入错误:{e}") + except KeyboardInterrupt: + print("\n用户取消输入") + return None + +# 使用示例 +print("=== 异常处理示例 ===") + +# 测试安全除法 +print("\n--- 安全除法测试 ---") +print(f"10 / 2 = {safe_divide(10, 2)}") +print(f"10 / 0 = {safe_divide(10, 0)}") +print(f"'10' / 2 = {safe_divide('10', 2)}") + +# 测试安全文件读取 +print("\n--- 安全文件读取测试 ---") +content = safe_file_read("test.txt") +if content: + print(f"文件内容:{content[:50]}...") + +# 测试安全用户输入 +print("\n--- 安全用户输入测试 ---") +# age = safe_user_input() # 取消注释以测试 +# if age is not None: +# print(f"您的年龄是:{age}岁") +``` + +--- + +## 二、自定义异常 + +### 2.1 创建自定义异常类 + +```python +# 基本自定义异常 +class CustomError(Exception): + """自定义异常基类""" + pass + +class ValidationError(CustomError): + """数据验证异常""" + def __init__(self, message, field=None): + super().__init__(message) + self.field = field + self.message = message + + def __str__(self): + if self.field: + return f"验证错误 [{self.field}]: {self.message}" + return f"验证错误: {self.message}" + +class BusinessLogicError(CustomError): + """业务逻辑异常""" + def __init__(self, message, error_code=None): + super().__init__(message) + self.error_code = error_code + self.message = message + + def __str__(self): + if self.error_code: + return f"业务错误 [{self.error_code}]: {self.message}" + return f"业务错误: {self.message}" + +class DatabaseError(CustomError): + """数据库操作异常""" + def __init__(self, message, query=None, params=None): + super().__init__(message) + self.query = query + self.params = params + self.message = message + + def __str__(self): + base_msg = f"数据库错误: {self.message}" + if self.query: + base_msg += f"\n查询语句: {self.query}" + if self.params: + base_msg += f"\n参数: {self.params}" + return base_msg +``` + +### 2.2 使用自定义异常的实际案例 + +```python +class User: + """用户类示例""" + + def __init__(self, username, email, age): + self.username = self._validate_username(username) + self.email = self._validate_email(email) + self.age = self._validate_age(age) + + def _validate_username(self, username): + """验证用户名""" + if not username: + raise ValidationError("用户名不能为空", "username") + if len(username) < 3: + raise ValidationError("用户名长度不能少于3个字符", "username") + if len(username) > 20: + raise ValidationError("用户名长度不能超过20个字符", "username") + if not username.isalnum(): + raise ValidationError("用户名只能包含字母和数字", "username") + return username + + def _validate_email(self, email): + """验证邮箱""" + import re + if not email: + raise ValidationError("邮箱不能为空", "email") + + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_pattern, email): + raise ValidationError("邮箱格式不正确", "email") + return email + + def _validate_age(self, age): + """验证年龄""" + if not isinstance(age, int): + raise ValidationError("年龄必须是整数", "age") + if age < 0: + raise ValidationError("年龄不能为负数", "age") + if age > 150: + raise ValidationError("年龄不能超过150岁", "age") + return age + + def update_age(self, new_age): + """更新年龄""" + old_age = self.age + try: + self.age = self._validate_age(new_age) + print(f"年龄已从 {old_age} 更新为 {new_age}") + except ValidationError as e: + print(f"年龄更新失败:{e}") + # 保持原来的年龄不变 + + def __str__(self): + return f"User(username='{self.username}', email='{self.email}', age={self.age})" + +class BankAccount: + """银行账户类示例""" + + def __init__(self, account_number, initial_balance=0): + self.account_number = account_number + self.balance = initial_balance + self.transaction_history = [] + + def deposit(self, amount): + """存款""" + try: + if amount <= 0: + raise BusinessLogicError("存款金额必须大于0", "INVALID_AMOUNT") + + self.balance += amount + self.transaction_history.append(f"存款: +{amount}") + print(f"存款成功,当前余额:{self.balance}") + + except BusinessLogicError as e: + print(f"存款失败:{e}") + + def withdraw(self, amount): + """取款""" + try: + if amount <= 0: + raise BusinessLogicError("取款金额必须大于0", "INVALID_AMOUNT") + + if amount > self.balance: + raise BusinessLogicError( + f"余额不足,当前余额:{self.balance},尝试取款:{amount}", + "INSUFFICIENT_FUNDS" + ) + + if amount > 10000: + raise BusinessLogicError( + "单次取款金额不能超过10000元", + "AMOUNT_LIMIT_EXCEEDED" + ) + + self.balance -= amount + self.transaction_history.append(f"取款: -{amount}") + print(f"取款成功,当前余额:{self.balance}") + + except BusinessLogicError as e: + print(f"取款失败:{e}") + + def transfer(self, target_account, amount): + """转账""" + try: + if not isinstance(target_account, BankAccount): + raise BusinessLogicError("目标账户无效", "INVALID_TARGET") + + if target_account.account_number == self.account_number: + raise BusinessLogicError("不能向自己转账", "SELF_TRANSFER") + + # 先从当前账户取款 + if amount > self.balance: + raise BusinessLogicError( + f"余额不足,无法转账 {amount} 元", + "INSUFFICIENT_FUNDS" + ) + + # 执行转账 + self.balance -= amount + target_account.balance += amount + + # 记录交易历史 + self.transaction_history.append(f"转出: -{amount} (到账户 {target_account.account_number})") + target_account.transaction_history.append(f"转入: +{amount} (从账户 {self.account_number})") + + print(f"转账成功:{amount} 元已转至账户 {target_account.account_number}") + + except BusinessLogicError as e: + print(f"转账失败:{e}") + + def get_balance(self): + """获取余额""" + return self.balance + + def get_transaction_history(self): + """获取交易历史""" + return self.transaction_history.copy() + +def custom_exception_examples(): + """自定义异常示例""" + print("=== 自定义异常示例 ===") + + # 用户验证示例 + print("\n--- 用户验证示例 ---") + + # 正确的用户创建 + try: + user1 = User("zhangsan", "zhangsan@example.com", 25) + print(f"用户创建成功:{user1}") + except ValidationError as e: + print(f"用户创建失败:{e}") + + # 错误的用户创建 + test_cases = [ + ("", "test@example.com", 25), # 空用户名 + ("ab", "test@example.com", 25), # 用户名太短 + ("user@123", "test@example.com", 25), # 用户名包含特殊字符 + ("validuser", "invalid-email", 25), # 邮箱格式错误 + ("validuser", "test@example.com", -5), # 年龄为负数 + ("validuser", "test@example.com", 200), # 年龄过大 + ] + + for username, email, age in test_cases: + try: + user = User(username, email, age) + print(f"用户创建成功:{user}") + except ValidationError as e: + print(f"用户创建失败:{e}") + + # 银行账户示例 + print("\n--- 银行账户示例 ---") + + # 创建账户 + account1 = BankAccount("123456", 1000) + account2 = BankAccount("789012", 500) + + print(f"账户1余额:{account1.get_balance()}") + print(f"账户2余额:{account2.get_balance()}") + + # 测试各种操作 + account1.deposit(500) # 正常存款 + account1.deposit(-100) # 错误存款 + + account1.withdraw(200) # 正常取款 + account1.withdraw(5000) # 余额不足 + account1.withdraw(15000) # 超过限额 + + account1.transfer(account2, 300) # 正常转账 + account1.transfer(account1, 100) # 向自己转账 + account1.transfer(account2, 2000) # 余额不足转账 + + print(f"\n最终余额:") + print(f"账户1:{account1.get_balance()}") + print(f"账户2:{account2.get_balance()}") + + print(f"\n账户1交易历史:") + for transaction in account1.get_transaction_history(): + print(f" {transaction}") + +# 运行示例 +if __name__ == "__main__": + custom_exception_examples() +``` + +--- + +## 三、调试技巧和工具 + +### 3.1 使用print调试 + +```python +def debug_with_print(): + """使用print进行调试""" + print("=== 使用print调试 ===") + + def calculate_average(numbers): + """计算平均值""" + print(f"DEBUG: 输入的数字列表: {numbers}") # 调试信息 + + if not numbers: + print("DEBUG: 列表为空,返回0") # 调试信息 + return 0 + + total = sum(numbers) + count = len(numbers) + + print(f"DEBUG: 总和 = {total}, 数量 = {count}") # 调试信息 + + average = total / count + print(f"DEBUG: 平均值 = {average}") # 调试信息 + + return average + + # 测试函数 + test_data = [ + [1, 2, 3, 4, 5], + [], + [10, 20, 30] + ] + + for data in test_data: + print(f"\n测试数据: {data}") + result = calculate_average(data) + print(f"结果: {result}") + print("-" * 30) + +def debug_with_assert(): + """使用断言进行调试""" + print("\n=== 使用断言调试 ===") + + def factorial(n): + """计算阶乘""" + # 前置条件断言 + assert isinstance(n, int), f"参数必须是整数,实际类型: {type(n)}" + assert n >= 0, f"参数必须是非负数,实际值: {n}" + + if n == 0 or n == 1: + result = 1 + else: + result = n * factorial(n - 1) + + # 后置条件断言 + assert result > 0, f"阶乘结果必须为正数,实际值: {result}" + + return result + + # 测试断言 + test_cases = [5, 0, 1, -1, "abc"] + + for test_case in test_cases: + try: + result = factorial(test_case) + print(f"{test_case}! = {result}") + except AssertionError as e: + print(f"断言失败: {e}") + except Exception as e: + print(f"其他错误: {e}") +``` + +### 3.2 使用logging模块 + +```python +import logging +from datetime import datetime + +# 配置日志 +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('debug.log', encoding='utf-8'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger(__name__) + +def debug_with_logging(): + """使用logging进行调试""" + print("\n=== 使用logging调试 ===") + + def process_user_data(user_data): + """处理用户数据""" + logger.info(f"开始处理用户数据: {user_data}") + + try: + # 验证必需字段 + required_fields = ['name', 'email', 'age'] + for field in required_fields: + if field not in user_data: + logger.error(f"缺少必需字段: {field}") + raise ValueError(f"缺少必需字段: {field}") + + logger.debug(f"字段 {field} 验证通过: {user_data[field]}") + + # 验证数据类型 + if not isinstance(user_data['age'], int): + logger.warning(f"年龄字段类型不正确,尝试转换: {user_data['age']}") + user_data['age'] = int(user_data['age']) + logger.info(f"年龄转换成功: {user_data['age']}") + + # 验证数据范围 + if user_data['age'] < 0 or user_data['age'] > 150: + logger.error(f"年龄超出有效范围: {user_data['age']}") + raise ValueError(f"年龄超出有效范围: {user_data['age']}") + + # 处理成功 + logger.info(f"用户数据处理成功: {user_data['name']}") + return { + 'status': 'success', + 'data': user_data, + 'processed_at': datetime.now().isoformat() + } + + except ValueError as e: + logger.error(f"数据验证失败: {e}") + return { + 'status': 'error', + 'error': str(e), + 'processed_at': datetime.now().isoformat() + } + except Exception as e: + logger.critical(f"处理用户数据时发生未知错误: {e}") + return { + 'status': 'critical_error', + 'error': str(e), + 'processed_at': datetime.now().isoformat() + } + + # 测试数据 + test_users = [ + {'name': '张三', 'email': 'zhangsan@example.com', 'age': 25}, + {'name': '李四', 'email': 'lisi@example.com'}, # 缺少age字段 + {'name': '王五', 'email': 'wangwu@example.com', 'age': '30'}, # age是字符串 + {'name': '赵六', 'email': 'zhaoliu@example.com', 'age': -5}, # age为负数 + {'name': '钱七', 'email': 'qianqi@example.com', 'age': 200}, # age过大 + ] + + for user in test_users: + print(f"\n处理用户: {user}") + result = process_user_data(user) + print(f"处理结果: {result['status']}") + if result['status'] != 'success': + print(f"错误信息: {result.get('error', 'Unknown error')}") + +def create_custom_logger(): + """创建自定义日志记录器""" + print("\n=== 自定义日志记录器 ===") + + # 创建自定义格式化器 + class ColoredFormatter(logging.Formatter): + """彩色日志格式化器""" + + # ANSI颜色代码 + COLORS = { + 'DEBUG': '\033[36m', # 青色 + 'INFO': '\033[32m', # 绿色 + 'WARNING': '\033[33m', # 黄色 + 'ERROR': '\033[31m', # 红色 + 'CRITICAL': '\033[35m', # 紫色 + 'RESET': '\033[0m' # 重置 + } + + def format(self, record): + # 添加颜色 + color = self.COLORS.get(record.levelname, self.COLORS['RESET']) + record.levelname = f"{color}{record.levelname}{self.COLORS['RESET']}" + return super().format(record) + + # 创建自定义日志记录器 + custom_logger = logging.getLogger('custom_app') + custom_logger.setLevel(logging.DEBUG) + + # 清除现有处理器 + custom_logger.handlers.clear() + + # 创建控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_formatter = ColoredFormatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + console_handler.setFormatter(console_formatter) + + # 创建文件处理器 + file_handler = logging.FileHandler('app.log', encoding='utf-8') + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' + ) + file_handler.setFormatter(file_formatter) + + # 添加处理器 + custom_logger.addHandler(console_handler) + custom_logger.addHandler(file_handler) + + # 测试自定义日志记录器 + custom_logger.debug("这是调试信息") + custom_logger.info("这是普通信息") + custom_logger.warning("这是警告信息") + custom_logger.error("这是错误信息") + custom_logger.critical("这是严重错误信息") + + return custom_logger +``` + +### 3.3 使用pdb调试器 + +```python +import pdb + +def debug_with_pdb(): + """使用pdb调试器""" + print("\n=== 使用pdb调试器 ===") + + def complex_calculation(data): + """复杂计算函数""" + result = [] + + for i, item in enumerate(data): + # 设置断点 - 取消注释以启用 + # pdb.set_trace() + + if isinstance(item, (int, float)): + # 计算平方 + square = item ** 2 + result.append(square) + elif isinstance(item, str): + # 计算字符串长度 + length = len(item) + result.append(length) + else: + # 其他类型设为0 + result.append(0) + + return result + + # 测试数据 + test_data = [1, 2, "hello", 3.5, [1, 2, 3], "world", None, 4] + + print(f"输入数据: {test_data}") + result = complex_calculation(test_data) + print(f"计算结果: {result}") + + # pdb调试器常用命令说明 + print("\npdb调试器常用命令:") + print(" l(ist) - 显示当前代码") + print(" n(ext) - 执行下一行") + print(" s(tep) - 进入函数内部") + print(" c(ontinue) - 继续执行") + print(" p <变量名> - 打印变量值") + print(" pp <变量名> - 美化打印变量值") + print(" w(here) - 显示调用栈") + print(" u(p) - 上移一层调用栈") + print(" d(own) - 下移一层调用栈") + print(" q(uit) - 退出调试器") + +def debug_with_breakpoint(): + """使用breakpoint()函数调试(Python 3.7+)""" + print("\n=== 使用breakpoint()调试 ===") + + def fibonacci(n): + """计算斐波那契数列""" + if n <= 0: + return [] + elif n == 1: + return [0] + elif n == 2: + return [0, 1] + + fib_sequence = [0, 1] + + for i in range(2, n): + # 使用breakpoint()设置断点 - 取消注释以启用 + # breakpoint() + + next_fib = fib_sequence[i-1] + fib_sequence[i-2] + fib_sequence.append(next_fib) + + return fib_sequence + + # 测试斐波那契函数 + n = 10 + print(f"计算前{n}个斐波那契数:") + result = fibonacci(n) + print(f"结果: {result}") +``` + +--- + +## 四、日志记录和错误追踪 + +### 4.1 高级日志配置 + +```python +import logging +import logging.config +import json +from datetime import datetime +import traceback +import sys + +# 日志配置字典 +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + }, + 'detailed': { + 'format': '%(asctime)s [%(levelname)s] %(name)s:%(funcName)s:%(lineno)d: %(message)s' + }, + 'json': { + 'format': '%(asctime)s %(name)s %(levelname)s %(message)s', + 'class': 'pythonjsonlogger.jsonlogger.JsonFormatter' + } + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'standard', + 'stream': 'ext://sys.stdout' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'detailed', + 'filename': 'app.log', + 'maxBytes': 1024*1024*5, # 5MB + 'backupCount': 3, + 'encoding': 'utf-8' + }, + 'error_file': { + 'level': 'ERROR', + 'class': 'logging.FileHandler', + 'formatter': 'detailed', + 'filename': 'error.log', + 'encoding': 'utf-8' + } + }, + 'loggers': { + '': { # root logger + 'handlers': ['console', 'file', 'error_file'], + 'level': 'DEBUG', + 'propagate': False + } + } +} + +def setup_logging(): + """设置日志配置""" + logging.config.dictConfig(LOGGING_CONFIG) + return logging.getLogger(__name__) + +class ErrorTracker: + """错误追踪器""" + + def __init__(self, logger): + self.logger = logger + self.error_count = 0 + self.error_history = [] + + def log_error(self, error, context=None): + """记录错误""" + self.error_count += 1 + + error_info = { + 'timestamp': datetime.now().isoformat(), + 'error_type': type(error).__name__, + 'error_message': str(error), + 'traceback': traceback.format_exc(), + 'context': context or {}, + 'error_id': self.error_count + } + + self.error_history.append(error_info) + + # 记录到日志 + self.logger.error( + f"错误 #{self.error_count}: {error_info['error_type']} - {error_info['error_message']}", + extra={'error_info': error_info} + ) + + return error_info['error_id'] + + def get_error_summary(self): + """获取错误摘要""" + if not self.error_history: + return "没有记录到错误" + + error_types = {} + for error in self.error_history: + error_type = error['error_type'] + error_types[error_type] = error_types.get(error_type, 0) + 1 + + summary = f"总错误数: {self.error_count}\n" + summary += "错误类型分布:\n" + for error_type, count in error_types.items(): + summary += f" {error_type}: {count}\n" + + return summary + + def get_recent_errors(self, count=5): + """获取最近的错误""" + return self.error_history[-count:] + +def advanced_logging_example(): + """高级日志记录示例""" + print("=== 高级日志记录示例 ===") + + # 设置日志 + logger = setup_logging() + error_tracker = ErrorTracker(logger) + + # 模拟应用程序 + class DataProcessor: + """数据处理器""" + + def __init__(self, logger, error_tracker): + self.logger = logger + self.error_tracker = error_tracker + + def process_data(self, data): + """处理数据""" + self.logger.info(f"开始处理数据,数据量: {len(data)}") + + processed_items = [] + failed_items = [] + + for i, item in enumerate(data): + try: + self.logger.debug(f"处理第 {i+1} 项: {item}") + + # 模拟数据处理 + if item is None: + raise ValueError("数据项不能为None") + + if isinstance(item, str) and len(item) == 0: + raise ValueError("字符串不能为空") + + if isinstance(item, (int, float)) and item < 0: + raise ValueError("数字不能为负数") + + # 处理成功 + processed_item = self._transform_item(item) + processed_items.append(processed_item) + + self.logger.debug(f"第 {i+1} 项处理成功: {item} -> {processed_item}") + + except Exception as e: + error_id = self.error_tracker.log_error( + e, + context={ + 'item_index': i, + 'item_value': item, + 'function': 'process_data' + } + ) + + failed_items.append({ + 'index': i, + 'item': item, + 'error_id': error_id, + 'error': str(e) + }) + + # 记录处理结果 + self.logger.info( + f"数据处理完成 - 成功: {len(processed_items)}, 失败: {len(failed_items)}" + ) + + if failed_items: + self.logger.warning(f"有 {len(failed_items)} 项数据处理失败") + + return { + 'processed': processed_items, + 'failed': failed_items, + 'summary': { + 'total': len(data), + 'success': len(processed_items), + 'failed': len(failed_items) + } + } + + def _transform_item(self, item): + """转换数据项""" + if isinstance(item, str): + return item.upper() + elif isinstance(item, (int, float)): + return item * 2 + elif isinstance(item, list): + return len(item) + else: + return str(item) + + # 测试数据处理器 + processor = DataProcessor(logger, error_tracker) + + test_data = [ + "hello", + 42, + None, # 会引发错误 + "", # 会引发错误 + -10, # 会引发错误 + [1, 2, 3], + 3.14, + "world" + ] + + logger.info("开始数据处理任务") + result = processor.process_data(test_data) + + print(f"\n处理结果:") + print(f"成功处理: {result['summary']['success']} 项") + print(f"处理失败: {result['summary']['failed']} 项") + + if result['failed']: + print("\n失败的项目:") + for failed in result['failed']: + print(f" 索引 {failed['index']}: {failed['item']} - {failed['error']}") + + print(f"\n错误追踪摘要:") + print(error_tracker.get_error_summary()) + + logger.info("数据处理任务完成") +``` + +### 4.2 性能监控和分析 + +```python +import time +import functools +import cProfile +import pstats +import io +from memory_profiler import profile as memory_profile + +def performance_monitor(func): + """性能监控装饰器""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + # 记录开始时间 + start_time = time.time() + start_cpu = time.process_time() + + try: + # 执行函数 + result = func(*args, **kwargs) + + # 记录结束时间 + end_time = time.time() + end_cpu = time.process_time() + + # 计算性能指标 + wall_time = end_time - start_time + cpu_time = end_cpu - start_cpu + + # 记录性能信息 + logger = logging.getLogger(__name__) + logger.info( + f"函数 {func.__name__} 性能统计 - " + f"墙钟时间: {wall_time:.4f}s, " + f"CPU时间: {cpu_time:.4f}s" + ) + + return result + + except Exception as e: + # 记录异常和性能信息 + end_time = time.time() + wall_time = end_time - start_time + + logger = logging.getLogger(__name__) + logger.error( + f"函数 {func.__name__} 执行失败 - " + f"执行时间: {wall_time:.4f}s, " + f"错误: {e}" + ) + raise + + return wrapper + +def profile_code(func): + """代码性能分析装饰器""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + pr = cProfile.Profile() + pr.enable() + + result = func(*args, **kwargs) + + pr.disable() + + # 创建统计报告 + s = io.StringIO() + ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') + ps.print_stats(10) # 显示前10个最耗时的函数 + + print(f"\n=== {func.__name__} 性能分析报告 ===") + print(s.getvalue()) + + return result + + return wrapper + +@performance_monitor +@profile_code +def cpu_intensive_task(n): + """CPU密集型任务""" + result = 0 + for i in range(n): + result += i ** 2 + return result + +@performance_monitor +def io_intensive_task(): + """IO密集型任务模拟""" + import tempfile + import os + + # 创建临时文件 + with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: + temp_filename = f.name + + # 写入大量数据 + for i in range(10000): + f.write(f"这是第 {i} 行数据\n") + + # 读取数据 + line_count = 0 + with open(temp_filename, 'r') as f: + for line in f: + line_count += 1 + + # 清理临时文件 + os.unlink(temp_filename) + + return line_count + +# @memory_profile # 取消注释以启用内存分析 +def memory_intensive_task(): + """内存密集型任务""" + # 创建大量数据 + data = [] + for i in range(100000): + data.append([j for j in range(100)]) + + # 处理数据 + processed = [] + for item in data: + processed.append(sum(item)) + + return len(processed) + +def performance_analysis_examples(): + """性能分析示例""" + print("=== 性能分析示例 ===") + + # 设置日志 + logger = setup_logging() + + # 测试CPU密集型任务 + print("\n--- CPU密集型任务测试 ---") + result1 = cpu_intensive_task(100000) + print(f"CPU任务结果: {result1}") + + # 测试IO密集型任务 + print("\n--- IO密集型任务测试 ---") + result2 = io_intensive_task() + print(f"IO任务结果: {result2} 行") + + # 测试内存密集型任务 + print("\n--- 内存密集型任务测试 ---") + result3 = memory_intensive_task() + print(f"内存任务结果: {result3} 项") + +# 运行示例 +if __name__ == "__main__": + debug_with_print() + debug_with_assert() + debug_with_logging() + create_custom_logger() + debug_with_pdb() + debug_with_breakpoint() + advanced_logging_example() + performance_analysis_examples() +``` + +--- + +## 五、单元测试和测试驱动开发 + +### 5.1 使用unittest模块 + +```python +import unittest +from unittest.mock import Mock, patch, MagicMock +import tempfile +import os + +# 被测试的类和函数 +class Calculator: + """计算器类""" + + def add(self, a, b): + """加法""" + return a + b + + def subtract(self, a, b): + """减法""" + return a - b + + def multiply(self, a, b): + """乘法""" + return a * b + + def divide(self, a, b): + """除法""" + if b == 0: + raise ValueError("除数不能为零") + return a / b + + def power(self, base, exponent): + """幂运算""" + if not isinstance(base, (int, float)) or not isinstance(exponent, (int, float)): + raise TypeError("参数必须是数字") + return base ** exponent + +class FileManager: + """文件管理器类""" + + def read_file(self, filename): + """读取文件""" + try: + with open(filename, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + raise FileNotFoundError(f"文件 {filename} 不存在") + + def write_file(self, filename, content): + """写入文件""" + with open(filename, 'w', encoding='utf-8') as f: + f.write(content) + + def file_exists(self, filename): + """检查文件是否存在""" + return os.path.exists(filename) + + def get_file_size(self, filename): + """获取文件大小""" + if not self.file_exists(filename): + raise FileNotFoundError(f"文件 {filename} 不存在") + return os.path.getsize(filename) + +# 测试类 +class TestCalculator(unittest.TestCase): + """计算器测试类""" + + def setUp(self): + """测试前的设置""" + self.calc = Calculator() + + def tearDown(self): + """测试后的清理""" + # 这里可以进行清理工作 + pass + + def test_add(self): + """测试加法""" + self.assertEqual(self.calc.add(2, 3), 5) + self.assertEqual(self.calc.add(-1, 1), 0) + self.assertEqual(self.calc.add(0, 0), 0) + self.assertEqual(self.calc.add(1.5, 2.5), 4.0) + + def test_subtract(self): + """测试减法""" + self.assertEqual(self.calc.subtract(5, 3), 2) + self.assertEqual(self.calc.subtract(1, 1), 0) + self.assertEqual(self.calc.subtract(-1, -1), 0) + self.assertEqual(self.calc.subtract(3.5, 1.5), 2.0) + + def test_multiply(self): + """测试乘法""" + self.assertEqual(self.calc.multiply(3, 4), 12) + self.assertEqual(self.calc.multiply(0, 5), 0) + self.assertEqual(self.calc.multiply(-2, 3), -6) + self.assertAlmostEqual(self.calc.multiply(1.5, 2.0), 3.0) + + def test_divide(self): + """测试除法""" + self.assertEqual(self.calc.divide(10, 2), 5) + self.assertEqual(self.calc.divide(7, 2), 3.5) + self.assertAlmostEqual(self.calc.divide(1, 3), 0.3333333333333333) + + # 测试除零异常 + with self.assertRaises(ValueError): + self.calc.divide(10, 0) + + with self.assertRaisesRegex(ValueError, "除数不能为零"): + self.calc.divide(5, 0) + + def test_power(self): + """测试幂运算""" + self.assertEqual(self.calc.power(2, 3), 8) + self.assertEqual(self.calc.power(5, 0), 1) + self.assertEqual(self.calc.power(4, 0.5), 2.0) + + # 测试类型错误 + with self.assertRaises(TypeError): + self.calc.power("2", 3) + + with self.assertRaises(TypeError): + self.calc.power(2, "3") + + def test_edge_cases(self): + """测试边界情况""" + # 测试大数 + large_num = 10**10 + self.assertEqual(self.calc.add(large_num, 1), large_num + 1) + + # 测试小数精度 + result = self.calc.add(0.1, 0.2) + self.assertAlmostEqual(result, 0.3, places=7) + +class TestFileManager(unittest.TestCase): + """文件管理器测试类""" + + def setUp(self): + """测试前的设置""" + self.file_manager = FileManager() + self.test_dir = tempfile.mkdtemp() + self.test_file = os.path.join(self.test_dir, "test.txt") + + def tearDown(self): + """测试后的清理""" + # 清理测试文件 + if os.path.exists(self.test_file): + os.remove(self.test_file) + os.rmdir(self.test_dir) + + def test_write_and_read_file(self): + """测试文件写入和读取""" + content = "这是测试内容\n第二行" + + # 写入文件 + self.file_manager.write_file(self.test_file, content) + + # 读取文件 + read_content = self.file_manager.read_file(self.test_file) + + self.assertEqual(content, read_content) + + def test_read_nonexistent_file(self): + """测试读取不存在的文件""" + nonexistent_file = os.path.join(self.test_dir, "nonexistent.txt") + + with self.assertRaises(FileNotFoundError): + self.file_manager.read_file(nonexistent_file) + + def test_file_exists(self): + """测试文件存在检查""" + # 文件不存在 + self.assertFalse(self.file_manager.file_exists(self.test_file)) + + # 创建文件 + self.file_manager.write_file(self.test_file, "test") + + # 文件存在 + self.assertTrue(self.file_manager.file_exists(self.test_file)) + + def test_get_file_size(self): + """测试获取文件大小""" + content = "Hello, World!" + self.file_manager.write_file(self.test_file, content) + + size = self.file_manager.get_file_size(self.test_file) + expected_size = len(content.encode('utf-8')) + + self.assertEqual(size, expected_size) + + def test_get_size_nonexistent_file(self): + """测试获取不存在文件的大小""" + nonexistent_file = os.path.join(self.test_dir, "nonexistent.txt") + + with self.assertRaises(FileNotFoundError): + self.file_manager.get_file_size(nonexistent_file) + +class TestWithMocks(unittest.TestCase): + """使用Mock的测试类""" + + def test_mock_basic(self): + """基本Mock使用""" + # 创建Mock对象 + mock_obj = Mock() + + # 设置返回值 + mock_obj.method.return_value = "mocked result" + + # 调用方法 + result = mock_obj.method() + + # 验证结果 + self.assertEqual(result, "mocked result") + + # 验证方法被调用 + mock_obj.method.assert_called_once() + + def test_mock_with_side_effect(self): + """使用side_effect的Mock""" + mock_obj = Mock() + + # 设置副作用(异常) + mock_obj.method.side_effect = ValueError("模拟错误") + + # 验证异常 + with self.assertRaises(ValueError): + mock_obj.method() + + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="file content") + def test_file_reading_with_patch(self, mock_file): + """使用patch模拟文件操作""" + file_manager = FileManager() + + # 调用读取文件方法 + content = file_manager.read_file("any_file.txt") + + # 验证结果 + self.assertEqual(content, "file content") + + # 验证文件被正确打开 + mock_file.assert_called_once_with("any_file.txt", 'r', encoding='utf-8') + + @patch('os.path.exists') + def test_file_exists_with_patch(self, mock_exists): + """使用patch模拟os.path.exists""" + # 设置Mock返回值 + mock_exists.return_value = True + + file_manager = FileManager() + result = file_manager.file_exists("any_file.txt") + + # 验证结果 + self.assertTrue(result) + + # 验证os.path.exists被调用 + mock_exists.assert_called_once_with("any_file.txt") + +def run_tests(): + """运行测试""" + print("=== 运行单元测试 ===") + + # 创建测试套件 + test_suite = unittest.TestSuite() + + # 添加测试类 + test_suite.addTest(unittest.makeSuite(TestCalculator)) + test_suite.addTest(unittest.makeSuite(TestFileManager)) + test_suite.addTest(unittest.makeSuite(TestWithMocks)) + + # 运行测试 + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(test_suite) + + # 打印测试结果摘要 + print(f"\n=== 测试结果摘要 ===") + print(f"运行测试数: {result.testsRun}") + print(f"失败数: {len(result.failures)}") + print(f"错误数: {len(result.errors)}") + print(f"跳过数: {len(result.skipped)}") + + if result.failures: + print("\n失败的测试:") + for test, traceback in result.failures: + print(f" {test}: {traceback}") + + if result.errors: + print("\n错误的测试:") + for test, traceback in result.errors: + print(f" {test}: {traceback}") + +def test_driven_development_example(): + """测试驱动开发示例""" + print("\n=== 测试驱动开发示例 ===") + + # 第一步:编写失败的测试 + class TestStringUtils(unittest.TestCase): + """字符串工具测试类""" + + def test_reverse_string(self): + """测试字符串反转""" + from string_utils import reverse_string + + self.assertEqual(reverse_string("hello"), "olleh") + self.assertEqual(reverse_string(""), "") + self.assertEqual(reverse_string("a"), "a") + self.assertEqual(reverse_string("12345"), "54321") + + def test_is_palindrome(self): + """测试回文检查""" + from string_utils import is_palindrome + + self.assertTrue(is_palindrome("racecar")) + self.assertTrue(is_palindrome("A man a plan a canal Panama")) + self.assertTrue(is_palindrome("")) + self.assertFalse(is_palindrome("hello")) + self.assertFalse(is_palindrome("Python")) + + def test_word_count(self): + """测试单词计数""" + from string_utils import word_count + + self.assertEqual(word_count("hello world"), 2) + self.assertEqual(word_count(""), 0) + self.assertEqual(word_count(" "), 0) + self.assertEqual(word_count("Python is awesome"), 3) + self.assertEqual(word_count("one"), 1) + + # 第二步:实现功能使测试通过 + class StringUtils: + """字符串工具类""" + + @staticmethod + def reverse_string(s): + """反转字符串""" + return s[::-1] + + @staticmethod + def is_palindrome(s): + """检查是否为回文""" + # 移除空格并转换为小写 + cleaned = ''.join(s.split()).lower() + return cleaned == cleaned[::-1] + + @staticmethod + def word_count(s): + """计算单词数量""" + if not s or s.isspace(): + return 0 + return len(s.split()) + + # 模拟string_utils模块 + import sys + import types + + string_utils_module = types.ModuleType('string_utils') + string_utils_module.reverse_string = StringUtils.reverse_string + string_utils_module.is_palindrome = StringUtils.is_palindrome + string_utils_module.word_count = StringUtils.word_count + sys.modules['string_utils'] = string_utils_module + + # 运行测试 + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.makeSuite(TestStringUtils)) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(test_suite) + + print(f"\nTDD测试结果: {result.testsRun} 个测试,{len(result.failures)} 个失败,{len(result.errors)} 个错误") + +# 运行示例 +if __name__ == "__main__": + run_tests() + test_driven_development_example() +``` + +--- + +## 六、实战练习:错误处理和调试系统 + +### 6.1 构建一个完整的错误处理系统 + +```python +import logging +import traceback +import json +import time +from datetime import datetime +from typing import Dict, List, Any, Optional +from dataclasses import dataclass, asdict +from enum import Enum + +class ErrorLevel(Enum): + """错误级别枚举""" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + +@dataclass +class ErrorReport: + """错误报告数据类""" + error_id: str + timestamp: str + error_type: str + error_message: str + level: ErrorLevel + context: Dict[str, Any] + traceback_info: str + user_id: Optional[str] = None + session_id: Optional[str] = None + resolved: bool = False + resolution_notes: Optional[str] = None + +class ErrorHandlingSystem: + """错误处理系统""" + + def __init__(self, log_file="error_system.log"): + self.error_reports: List[ErrorReport] = [] + self.error_count = 0 + self.setup_logging(log_file) + + def setup_logging(self, log_file): + """设置日志系统""" + self.logger = logging.getLogger("ErrorHandlingSystem") + self.logger.setLevel(logging.DEBUG) + + # 清除现有处理器 + self.logger.handlers.clear() + + # 文件处理器 + file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_handler.setLevel(logging.DEBUG) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + # 格式化器 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # 添加处理器 + self.logger.addHandler(file_handler) + self.logger.addHandler(console_handler) + + def generate_error_id(self) -> str: + """生成错误ID""" + self.error_count += 1 + timestamp = int(time.time() * 1000) + return f"ERR_{timestamp}_{self.error_count:04d}" + + def determine_error_level(self, error: Exception) -> ErrorLevel: + """确定错误级别""" + if isinstance(error, (SystemExit, KeyboardInterrupt)): + return ErrorLevel.CRITICAL + elif isinstance(error, (MemoryError, OSError)): + return ErrorLevel.HIGH + elif isinstance(error, (ValueError, TypeError, AttributeError)): + return ErrorLevel.MEDIUM + else: + return ErrorLevel.LOW + + def capture_error(self, error: Exception, context: Dict[str, Any] = None, + user_id: str = None, session_id: str = None) -> str: + """捕获错误""" + error_id = self.generate_error_id() + level = self.determine_error_level(error) + + error_report = ErrorReport( + error_id=error_id, + timestamp=datetime.now().isoformat(), + error_type=type(error).__name__, + error_message=str(error), + level=level, + context=context or {}, + traceback_info=traceback.format_exc(), + user_id=user_id, + session_id=session_id + ) + + self.error_reports.append(error_report) + + # 记录到日志 + log_level = { + ErrorLevel.LOW: logging.INFO, + ErrorLevel.MEDIUM: logging.WARNING, + ErrorLevel.HIGH: logging.ERROR, + ErrorLevel.CRITICAL: logging.CRITICAL + }[level] + + self.logger.log( + log_level, + f"错误捕获 [{error_id}] {error_report.error_type}: {error_report.error_message}" + ) + + # 如果是严重错误,立即通知 + if level == ErrorLevel.CRITICAL: + self._send_critical_alert(error_report) + + return error_id + + def _send_critical_alert(self, error_report: ErrorReport): + """发送严重错误警报""" + print(f"\n🚨 严重错误警报 🚨") + print(f"错误ID: {error_report.error_id}") + print(f"错误类型: {error_report.error_type}") + print(f"错误信息: {error_report.error_message}") + print(f"时间: {error_report.timestamp}") + if error_report.user_id: + print(f"用户ID: {error_report.user_id}") + print("请立即处理!\n") + + def get_error_report(self, error_id: str) -> Optional[ErrorReport]: + """获取错误报告""" + for report in self.error_reports: + if report.error_id == error_id: + return report + return None + + def resolve_error(self, error_id: str, resolution_notes: str) -> bool: + """解决错误""" + report = self.get_error_report(error_id) + if report: + report.resolved = True + report.resolution_notes = resolution_notes + self.logger.info(f"错误 {error_id} 已解决: {resolution_notes}") + return True + return False + + def get_error_statistics(self) -> Dict[str, Any]: + """获取错误统计""" + total_errors = len(self.error_reports) + if total_errors == 0: + return {"total_errors": 0} + + # 按级别统计 + level_stats = {} + for level in ErrorLevel: + level_stats[level.value] = sum( + 1 for report in self.error_reports + if report.level == level + ) + + # 按类型统计 + type_stats = {} + for report in self.error_reports: + error_type = report.error_type + type_stats[error_type] = type_stats.get(error_type, 0) + 1 + + # 解决率 + resolved_count = sum(1 for report in self.error_reports if report.resolved) + resolution_rate = (resolved_count / total_errors) * 100 + + return { + "total_errors": total_errors, + "resolved_errors": resolved_count, + "unresolved_errors": total_errors - resolved_count, + "resolution_rate": f"{resolution_rate:.1f}%", + "level_distribution": level_stats, + "type_distribution": type_stats + } + + def export_error_reports(self, filename: str = None) -> str: + """导出错误报告""" + if filename is None: + filename = f"error_reports_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + reports_data = [asdict(report) for report in self.error_reports] + + # 转换枚举为字符串 + for report_data in reports_data: + report_data['level'] = report_data['level'].value + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(reports_data, f, ensure_ascii=False, indent=2) + + self.logger.info(f"错误报告已导出到 {filename}") + return filename + + def get_recent_errors(self, count: int = 10) -> List[ErrorReport]: + """获取最近的错误""" + return self.error_reports[-count:] + + def get_unresolved_errors(self) -> List[ErrorReport]: + """获取未解决的错误""" + return [report for report in self.error_reports if not report.resolved] + +class MonitoredApplication: + """被监控的应用程序示例""" + + def __init__(self, error_system: ErrorHandlingSystem): + self.error_system = error_system + self.user_sessions = {} + + def safe_execute(self, func, *args, user_id=None, session_id=None, **kwargs): + """安全执行函数""" + try: + return func(*args, **kwargs) + except Exception as e: + context = { + 'function_name': func.__name__, + 'args': str(args), + 'kwargs': str(kwargs) + } + + error_id = self.error_system.capture_error( + e, context=context, user_id=user_id, session_id=session_id + ) + + return {"error": True, "error_id": error_id, "message": str(e)} + + def divide_numbers(self, a, b): + """除法运算""" + if b == 0: + raise ZeroDivisionError("除数不能为零") + return a / b + + def access_list_item(self, lst, index): + """访问列表项""" + if not isinstance(lst, list): + raise TypeError("参数必须是列表") + return lst[index] + + def process_user_data(self, user_data): + """处理用户数据""" + if not isinstance(user_data, dict): + raise TypeError("用户数据必须是字典") + + required_fields = ['name', 'email'] + for field in required_fields: + if field not in user_data: + raise ValueError(f"缺少必需字段: {field}") + + return {"status": "success", "processed_data": user_data} + + def simulate_memory_error(self): + """模拟内存错误""" + # 这只是模拟,实际不会真的耗尽内存 + raise MemoryError("模拟内存不足错误") + + def simulate_critical_error(self): + """模拟严重错误""" + raise SystemExit("模拟系统退出错误") + +def error_handling_system_demo(): + """错误处理系统演示""" + print("=== 错误处理系统演示 ===") + + # 创建错误处理系统 + error_system = ErrorHandlingSystem() + app = MonitoredApplication(error_system) + + # 模拟各种错误场景 + test_scenarios = [ + # 正常操作 + ("正常除法", lambda: app.divide_numbers(10, 2), "user1", "session1"), + + # 除零错误 + ("除零错误", lambda: app.divide_numbers(10, 0), "user1", "session1"), + + # 索引错误 + ("索引错误", lambda: app.access_list_item([1, 2, 3], 10), "user2", "session2"), + + # 类型错误 + ("类型错误", lambda: app.access_list_item("not a list", 0), "user2", "session2"), + + # 数据验证错误 + ("数据验证错误", lambda: app.process_user_data({"name": "张三"}), "user3", "session3"), + + # 内存错误 + ("内存错误", lambda: app.simulate_memory_error(), "user4", "session4"), + + # 严重错误 + ("严重错误", lambda: app.simulate_critical_error(), "user5", "session5"), + ] + + error_ids = [] + + for scenario_name, operation, user_id, session_id in test_scenarios: + print(f"\n--- 测试场景: {scenario_name} ---") + + result = app.safe_execute( + operation, + user_id=user_id, + session_id=session_id + ) + + if isinstance(result, dict) and result.get("error"): + error_ids.append(result["error_id"]) + print(f"错误已捕获,错误ID: {result['error_id']}") + else: + print(f"操作成功,结果: {result}") + + # 解决一些错误 + print("\n--- 解决错误 ---") + if error_ids: + # 解决第一个错误 + error_system.resolve_error( + error_ids[0], + "已修复除零检查逻辑,添加了输入验证" + ) + + # 解决第二个错误 + if len(error_ids) > 1: + error_system.resolve_error( + error_ids[1], + "已添加边界检查,防止索引越界" + ) + + # 显示错误统计 + print("\n--- 错误统计 ---") + stats = error_system.get_error_statistics() + print(json.dumps(stats, ensure_ascii=False, indent=2)) + + # 显示未解决的错误 + print("\n--- 未解决的错误 ---") + unresolved = error_system.get_unresolved_errors() + for report in unresolved: + print(f"错误ID: {report.error_id}") + print(f"类型: {report.error_type}") + print(f"级别: {report.level.value}") + print(f"消息: {report.error_message}") + print("-" * 40) + + # 导出错误报告 + print("\n--- 导出错误报告 ---") + export_file = error_system.export_error_reports() + print(f"错误报告已导出到: {export_file}") + +# 运行演示 +if __name__ == "__main__": + error_handling_system_demo() +``` + +--- + +## 七、总结和最佳实践 + +### 7.1 错误处理最佳实践 + +1. **具体化异常处理** + - 捕获具体的异常类型,而不是使用通用的Exception + - 为不同的错误情况提供不同的处理逻辑 + +2. **提供有意义的错误信息** + - 错误信息应该清晰地描述问题 + - 包含足够的上下文信息帮助调试 + +3. **使用自定义异常** + - 为业务逻辑创建专门的异常类 + - 异常类应该包含相关的错误信息和上下文 + +4. **记录错误日志** + - 使用logging模块记录错误信息 + - 包含时间戳、错误级别、上下文信息 + +5. **优雅降级** + - 当发生错误时,程序应该能够优雅地处理 + - 提供备选方案或默认行为 + +### 7.2 调试技巧总结 + +1. **分层调试** + - 从简单的print语句开始 + - 使用logging进行结构化日志记录 + - 使用调试器进行深入分析 + +2. **测试驱动调试** + - 编写测试用例重现问题 + - 使用单元测试验证修复 + +3. **性能分析** + - 使用性能分析工具识别瓶颈 + - 监控内存使用和CPU时间 + +### 7.3 下一步学习方向 + +1. **高级调试工具** + - 学习使用IDE的调试功能 + - 掌握远程调试技术 + +2. **监控和告警** + - 学习应用程序监控 + - 设置错误告警系统 + +3. **错误追踪服务** + - 了解Sentry等错误追踪服务 + - 学习分布式系统的错误追踪 + +### 7.4 练习建议 + +1. **创建一个个人项目**,实践本章学到的错误处理技巧 +2. **为现有代码添加异常处理**,提高代码的健壮性 +3. **编写单元测试**,覆盖各种错误场景 +4. **设置日志系统**,记录应用程序的运行状态 +5. **模拟各种错误情况**,测试错误处理的有效性 + +通过本章的学习,你应该能够: +- 理解Python的异常处理机制 +- 创建和使用自定义异常 +- 使用各种调试技巧和工具 +- 建立完整的错误处理和日志系统 +- 编写健壮的、易于调试的Python代码 + +错误处理和调试是编程中的重要技能,需要在实践中不断提高。记住,好的错误处理不仅能让程序更稳定,还能大大提高开发和维护的效率。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/12.md b/docs/Python/12.md new file mode 100644 index 000000000..7db986264 --- /dev/null +++ b/docs/Python/12.md @@ -0,0 +1,1781 @@ +--- +title: 第12天-IO编程 +author: 哪吒 +date: '2023-06-15' +--- + +# 第12天-IO编程 + +## 一、IO编程概述 + +### 1.1 什么是IO编程 + +IO(Input/Output)编程是指程序与外部世界进行数据交换的编程技术。在Python中,IO编程主要包括: + +- **文件IO**:读写文件 +- **网络IO**:网络通信 +- **标准IO**:控制台输入输出 +- **内存IO**:内存中的数据流操作 + +### 1.2 IO编程的重要性 + +```python +# IO编程的应用场景 +print("=== IO编程的应用场景 ===") + +# 1. 数据持久化 +print("1. 数据持久化:将程序数据保存到文件") +print(" - 配置文件读写") +print(" - 日志记录") +print(" - 数据备份") + +# 2. 数据交换 +print("\n2. 数据交换:与其他程序或系统交换数据") +print(" - 网络通信") +print(" - API调用") +print(" - 数据库操作") + +# 3. 用户交互 +print("\n3. 用户交互:与用户进行输入输出") +print(" - 命令行界面") +print(" - 图形界面") +print(" - Web界面") + +# 4. 系统集成 +print("\n4. 系统集成:与操作系统和其他程序集成") +print(" - 进程间通信") +print(" - 系统调用") +print(" - 外部程序调用") +``` + +--- + +## 二、文件IO操作 + +### 2.1 文件操作基础 + +```python +import os +import shutil +from pathlib import Path + +def file_operations_demo(): + """文件操作基础演示""" + print("=== 文件操作基础 ===") + + # 1. 创建文件 + print("\n1. 创建和写入文件") + + # 使用open函数创建文件 + with open('demo.txt', 'w', encoding='utf-8') as f: + f.write('Hello, Python IO!\n') + f.write('这是第二行\n') + f.write('这是第三行\n') + print("文件 demo.txt 已创建") + + # 2. 读取文件 + print("\n2. 读取文件内容") + + # 读取全部内容 + with open('demo.txt', 'r', encoding='utf-8') as f: + content = f.read() + print("全部内容:") + print(content) + + # 按行读取 + with open('demo.txt', 'r', encoding='utf-8') as f: + print("按行读取:") + for line_num, line in enumerate(f, 1): + print(f"第{line_num}行: {line.strip()}") + + # 3. 追加内容 + print("\n3. 追加内容") + with open('demo.txt', 'a', encoding='utf-8') as f: + f.write('这是追加的内容\n') + + # 验证追加结果 + with open('demo.txt', 'r', encoding='utf-8') as f: + print("追加后的内容:") + print(f.read()) + + # 4. 文件信息 + print("\n4. 文件信息") + file_path = Path('demo.txt') + if file_path.exists(): + stat = file_path.stat() + print(f"文件大小: {stat.st_size} 字节") + print(f"创建时间: {stat.st_ctime}") + print(f"修改时间: {stat.st_mtime}") + print(f"是否为文件: {file_path.is_file()}") + print(f"是否为目录: {file_path.is_dir()}") + + # 5. 清理 + if file_path.exists(): + file_path.unlink() + print("\n文件已删除") + +# 运行演示 +file_operations_demo() +``` + +### 2.2 文件读写模式详解 + +```python +def file_modes_demo(): + """文件读写模式演示""" + print("=== 文件读写模式详解 ===") + + # 准备测试数据 + test_data = "Hello, World!\n这是测试数据\n第三行内容" + + # 1. 文本模式 + print("\n1. 文本模式操作") + + # 写入模式 'w' - 覆盖写入 + with open('test_w.txt', 'w', encoding='utf-8') as f: + f.write(test_data) + print("'w' 模式:覆盖写入完成") + + # 读取模式 'r' - 只读 + with open('test_w.txt', 'r', encoding='utf-8') as f: + content = f.read() + print(f"'r' 模式读取:\n{content}") + + # 追加模式 'a' - 追加写入 + with open('test_w.txt', 'a', encoding='utf-8') as f: + f.write("\n追加的内容") + print("'a' 模式:追加写入完成") + + # 读写模式 'r+' - 读写 + with open('test_w.txt', 'r+', encoding='utf-8') as f: + content = f.read() + print(f"'r+' 模式读取:\n{content}") + f.write("\n通过r+模式追加") + + # 2. 二进制模式 + print("\n2. 二进制模式操作") + + # 二进制写入 + binary_data = b"\x48\x65\x6c\x6c\x6f" # "Hello" 的字节表示 + with open('test_binary.bin', 'wb') as f: + f.write(binary_data) + print("二进制数据写入完成") + + # 二进制读取 + with open('test_binary.bin', 'rb') as f: + data = f.read() + print(f"二进制数据读取: {data}") + print(f"转换为字符串: {data.decode('utf-8')}") + + # 3. 文件模式组合 + print("\n3. 常用文件模式总结") + modes = { + 'r': '只读模式(默认)', + 'w': '写入模式(覆盖)', + 'a': '追加模式', + 'r+': '读写模式', + 'w+': '写读模式(覆盖)', + 'a+': '追加读写模式', + 'rb': '二进制只读', + 'wb': '二进制写入', + 'ab': '二进制追加', + 'rb+': '二进制读写', + 'wb+': '二进制写读', + 'ab+': '二进制追加读写' + } + + for mode, description in modes.items(): + print(f" {mode:4s}: {description}") + + # 清理文件 + for filename in ['test_w.txt', 'test_binary.bin']: + if os.path.exists(filename): + os.remove(filename) + print("\n测试文件已清理") + +# 运行演示 +file_modes_demo() +``` + +### 2.3 高级文件操作 + +```python +import json +import csv +import pickle +from datetime import datetime + +def advanced_file_operations(): + """高级文件操作演示""" + print("=== 高级文件操作 ===") + + # 1. JSON文件操作 + print("\n1. JSON文件操作") + + # 准备JSON数据 + data = { + 'name': '张三', + 'age': 25, + 'skills': ['Python', 'JavaScript', 'SQL'], + 'address': { + 'city': '北京', + 'district': '朝阳区' + }, + 'timestamp': datetime.now().isoformat() + } + + # 写入JSON文件 + with open('data.json', 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("JSON数据已写入 data.json") + + # 读取JSON文件 + with open('data.json', 'r', encoding='utf-8') as f: + loaded_data = json.load(f) + print(f"读取的JSON数据: {loaded_data}") + + # 2. CSV文件操作 + print("\n2. CSV文件操作") + + # 写入CSV文件 + csv_data = [ + ['姓名', '年龄', '城市', '薪资'], + ['张三', 25, '北京', 8000], + ['李四', 30, '上海', 12000], + ['王五', 28, '深圳', 10000], + ['赵六', 32, '广州', 9500] + ] + + with open('employees.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerows(csv_data) + print("CSV数据已写入 employees.csv") + + # 读取CSV文件 + with open('employees.csv', 'r', encoding='utf-8') as f: + reader = csv.reader(f) + print("CSV数据读取:") + for row_num, row in enumerate(reader): + print(f" 第{row_num + 1}行: {row}") + + # 使用DictReader读取CSV + with open('employees.csv', 'r', encoding='utf-8') as f: + dict_reader = csv.DictReader(f) + print("\n使用DictReader读取:") + for row in dict_reader: + print(f" {row}") + + # 3. Pickle序列化 + print("\n3. Pickle序列化操作") + + # 复杂数据结构 + complex_data = { + 'list': [1, 2, 3, [4, 5]], + 'dict': {'a': 1, 'b': 2}, + 'tuple': (1, 2, 3), + 'set': {1, 2, 3}, + 'datetime': datetime.now(), + 'function': lambda x: x * 2 + } + + # 序列化到文件 + with open('data.pickle', 'wb') as f: + pickle.dump(complex_data, f) + print("复杂数据已序列化到 data.pickle") + + # 从文件反序列化 + with open('data.pickle', 'rb') as f: + loaded_complex_data = pickle.load(f) + print(f"反序列化的数据: {loaded_complex_data}") + # 测试函数是否正常 + func = loaded_complex_data['function'] + print(f"函数测试: func(5) = {func(5)}") + + # 4. 大文件处理 + print("\n4. 大文件处理技巧") + + # 创建一个较大的测试文件 + with open('large_file.txt', 'w', encoding='utf-8') as f: + for i in range(10000): + f.write(f"这是第{i+1}行数据,包含一些测试内容\n") + print("大文件 large_file.txt 已创建") + + # 逐行读取大文件(内存友好) + line_count = 0 + with open('large_file.txt', 'r', encoding='utf-8') as f: + for line in f: + line_count += 1 + if line_count <= 5: # 只显示前5行 + print(f" {line.strip()}") + print(f"大文件总行数: {line_count}") + + # 分块读取大文件 + chunk_size = 1024 # 1KB + with open('large_file.txt', 'r', encoding='utf-8') as f: + chunk_count = 0 + while True: + chunk = f.read(chunk_size) + if not chunk: + break + chunk_count += 1 + if chunk_count == 1: # 只显示第一个块的部分内容 + print(f"第一个块的前100个字符: {chunk[:100]}...") + print(f"文件被分为 {chunk_count} 个块读取") + + # 清理文件 + files_to_clean = ['data.json', 'employees.csv', 'data.pickle', 'large_file.txt'] + for filename in files_to_clean: + if os.path.exists(filename): + os.remove(filename) + print("\n测试文件已清理") + +# 运行演示 +advanced_file_operations() +``` + +--- + +## 三、目录操作 + +### 3.1 目录基础操作 + +```python +import os +import shutil +from pathlib import Path +import tempfile + +def directory_operations_demo(): + """目录操作演示""" + print("=== 目录操作演示 ===") + + # 1. 获取当前目录信息 + print("\n1. 当前目录信息") + current_dir = os.getcwd() + print(f"当前工作目录: {current_dir}") + print(f"目录内容: {os.listdir('.')}") + + # 使用pathlib + current_path = Path.cwd() + print(f"使用pathlib获取当前目录: {current_path}") + + # 2. 创建目录 + print("\n2. 创建目录") + + # 创建单个目录 + test_dir = Path('test_directory') + test_dir.mkdir(exist_ok=True) + print(f"目录 {test_dir} 已创建") + + # 创建多级目录 + nested_dir = Path('parent/child/grandchild') + nested_dir.mkdir(parents=True, exist_ok=True) + print(f"多级目录 {nested_dir} 已创建") + + # 3. 遍历目录 + print("\n3. 遍历目录") + + # 创建一些测试文件 + (test_dir / 'file1.txt').write_text('内容1', encoding='utf-8') + (test_dir / 'file2.txt').write_text('内容2', encoding='utf-8') + (test_dir / 'subdir').mkdir(exist_ok=True) + (test_dir / 'subdir' / 'file3.txt').write_text('内容3', encoding='utf-8') + + # 使用os.walk遍历 + print("使用os.walk遍历:") + for root, dirs, files in os.walk(test_dir): + level = root.replace(str(test_dir), '').count(os.sep) + indent = ' ' * 2 * level + print(f"{indent}{os.path.basename(root)}/") + subindent = ' ' * 2 * (level + 1) + for file in files: + print(f"{subindent}{file}") + + # 使用pathlib遍历 + print("\n使用pathlib遍历:") + for item in test_dir.rglob('*'): + if item.is_file(): + print(f"文件: {item}") + elif item.is_dir(): + print(f"目录: {item}") + + # 4. 目录信息 + print("\n4. 目录信息") + for item in test_dir.iterdir(): + stat = item.stat() + item_type = "目录" if item.is_dir() else "文件" + print(f"{item_type}: {item.name}, 大小: {stat.st_size} 字节") + + # 5. 复制和移动目录 + print("\n5. 复制和移动目录") + + # 复制目录 + backup_dir = Path('test_directory_backup') + if backup_dir.exists(): + shutil.rmtree(backup_dir) + shutil.copytree(test_dir, backup_dir) + print(f"目录已复制到 {backup_dir}") + + # 移动目录 + moved_dir = Path('moved_directory') + if moved_dir.exists(): + shutil.rmtree(moved_dir) + shutil.move(str(backup_dir), str(moved_dir)) + print(f"目录已移动到 {moved_dir}") + + # 6. 删除目录 + print("\n6. 删除目录") + + # 删除空目录 + empty_dir = Path('empty_dir') + empty_dir.mkdir(exist_ok=True) + empty_dir.rmdir() + print("空目录已删除") + + # 删除非空目录 + shutil.rmtree(test_dir) + shutil.rmtree(moved_dir) + shutil.rmtree(nested_dir.parent.parent) # 删除parent目录 + print("非空目录已删除") + +# 运行演示 +directory_operations_demo() +``` + +### 3.2 路径操作 + +```python +from pathlib import Path +import os + +def path_operations_demo(): + """路径操作演示""" + print("=== 路径操作演示 ===") + + # 1. 路径构建 + print("\n1. 路径构建") + + # 使用pathlib构建路径 + path1 = Path('documents') / 'projects' / 'python' / 'main.py' + print(f"pathlib构建路径: {path1}") + + # 使用os.path构建路径 + path2 = os.path.join('documents', 'projects', 'python', 'main.py') + print(f"os.path构建路径: {path2}") + + # 2. 路径解析 + print("\n2. 路径解析") + + sample_path = Path('/home/user/documents/project/main.py') + print(f"完整路径: {sample_path}") + print(f"父目录: {sample_path.parent}") + print(f"文件名: {sample_path.name}") + print(f"文件名(无扩展名): {sample_path.stem}") + print(f"扩展名: {sample_path.suffix}") + print(f"所有扩展名: {sample_path.suffixes}") + print(f"路径部分: {sample_path.parts}") + + # 3. 路径判断 + print("\n3. 路径判断") + + current_file = Path(__file__) if '__file__' in globals() else Path('demo.py') + print(f"测试路径: {current_file}") + print(f"是否存在: {current_file.exists()}") + print(f"是否为文件: {current_file.is_file()}") + print(f"是否为目录: {current_file.is_dir()}") + print(f"是否为绝对路径: {current_file.is_absolute()}") + + # 4. 路径转换 + print("\n4. 路径转换") + + relative_path = Path('documents/project/main.py') + print(f"相对路径: {relative_path}") + + # 转换为绝对路径 + absolute_path = relative_path.resolve() + print(f"绝对路径: {absolute_path}") + + # 获取相对路径 + try: + current_dir = Path.cwd() + relative_to_current = absolute_path.relative_to(current_dir) + print(f"相对于当前目录: {relative_to_current}") + except ValueError: + print("路径不在当前目录下") + + # 5. 路径匹配 + print("\n5. 路径匹配") + + test_paths = [ + Path('documents/project1/main.py'), + Path('documents/project2/test.py'), + Path('documents/project1/utils.py'), + Path('images/photo.jpg'), + Path('documents/readme.txt') + ] + + print("Python文件:") + for path in test_paths: + if path.match('*.py'): + print(f" {path}") + + print("\nproject1目录下的文件:") + for path in test_paths: + if path.match('*/project1/*'): + print(f" {path}") + + print("\ndocuments目录下的所有文件:") + for path in test_paths: + if path.match('documents/**/*'): + print(f" {path}") + + # 6. 路径操作实用函数 + print("\n6. 路径操作实用函数") + + def safe_path_join(*parts): + """安全的路径连接""" + return Path(*parts) + + def get_file_info(file_path): + """获取文件信息""" + path = Path(file_path) + if not path.exists(): + return None + + stat = path.stat() + return { + 'name': path.name, + 'size': stat.st_size, + 'modified': stat.st_mtime, + 'is_file': path.is_file(), + 'is_dir': path.is_dir(), + 'parent': str(path.parent), + 'extension': path.suffix + } + + def find_files_by_extension(directory, extension): + """按扩展名查找文件""" + path = Path(directory) + if not path.exists() or not path.is_dir(): + return [] + + pattern = f"**/*{extension}" + return list(path.glob(pattern)) + + # 测试实用函数 + test_path = safe_path_join('documents', 'project', 'main.py') + print(f"安全路径连接: {test_path}") + + # 创建测试文件来演示 + demo_file = Path('demo_file.txt') + demo_file.write_text('测试内容', encoding='utf-8') + + file_info = get_file_info(demo_file) + if file_info: + print(f"文件信息: {file_info}") + + # 清理 + demo_file.unlink() + print("\n演示文件已清理") + +# 运行演示 +path_operations_demo() +``` + +--- + +## 四、标准输入输出 + +### 4.1 控制台输入输出 + +```python +import sys +import getpass +from datetime import datetime + +def console_io_demo(): + """控制台输入输出演示""" + print("=== 控制台输入输出演示 ===") + + # 1. 基本输出 + print("\n1. 基本输出方式") + + # 普通输出 + print("这是普通输出") + + # 格式化输出 + name = "张三" + age = 25 + print(f"姓名: {name}, 年龄: {age}") + print("姓名: {}, 年龄: {}".format(name, age)) + print("姓名: %s, 年龄: %d" % (name, age)) + + # 输出到不同流 + print("正常信息", file=sys.stdout) + print("错误信息", file=sys.stderr) + + # 控制输出结束符 + print("第一部分", end=" ") + print("第二部分", end=" ") + print("第三部分") # 默认换行 + + # 2. 高级输出格式 + print("\n2. 高级输出格式") + + # 表格输出 + data = [ + ['姓名', '年龄', '城市'], + ['张三', 25, '北京'], + ['李四', 30, '上海'], + ['王五', 28, '深圳'] + ] + + print("表格输出:") + for row in data: + print(f"{row[0]:8s} {row[1]:>3} {row[2]:6s}") + + # 进度条输出 + print("\n进度条演示:") + import time + for i in range(21): + percent = i * 5 + bar = '█' * i + '░' * (20 - i) + print(f"\r进度: [{bar}] {percent}%", end='', flush=True) + time.sleep(0.1) + print() # 换行 + + # 3. 彩色输出 + print("\n3. 彩色输出") + + # ANSI颜色代码 + colors = { + 'red': '\033[31m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'blue': '\033[34m', + 'magenta': '\033[35m', + 'cyan': '\033[36m', + 'white': '\033[37m', + 'reset': '\033[0m' + } + + for color_name, color_code in colors.items(): + if color_name != 'reset': + print(f"{color_code}这是{color_name}颜色的文本{colors['reset']}") + + # 4. 输入演示(注释掉以避免阻塞) + print("\n4. 输入方式(演示代码)") + + # 基本输入 + print("# 基本输入") + print("# user_input = input('请输入您的姓名: ')") + print("# print(f'您好, {user_input}!')") + + # 数字输入 + print("\n# 数字输入") + print("# try:") + print("# age = int(input('请输入您的年龄: '))") + print("# print(f'您的年龄是: {age}')") + print("# except ValueError:") + print("# print('请输入有效的数字')") + + # 密码输入 + print("\n# 密码输入") + print("# password = getpass.getpass('请输入密码: ')") + print("# print('密码已输入(不显示)')") + + # 5. 命令行参数 + print("\n5. 命令行参数") + print(f"脚本名称: {sys.argv[0] if sys.argv else 'unknown'}") + print(f"参数列表: {sys.argv[1:] if len(sys.argv) > 1 else '无参数'}") + print(f"参数数量: {len(sys.argv) - 1}") + +def interactive_menu_demo(): + """交互式菜单演示""" + print("\n=== 交互式菜单演示 ===") + + def show_menu(): + """显示菜单""" + print("\n" + "="*30) + print(" 主菜单") + print("="*30) + print("1. 查看当前时间") + print("2. 计算器") + print("3. 文件操作") + print("4. 系统信息") + print("0. 退出") + print("="*30) + + def get_current_time(): + """获取当前时间""" + now = datetime.now() + print(f"当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + + def calculator(): + """简单计算器""" + print("简单计算器(输入 'q' 退出)") + while True: + try: + expr = input("请输入表达式: ").strip() + if expr.lower() == 'q': + break + result = eval(expr) # 注意:实际应用中应该使用更安全的方法 + print(f"结果: {result}") + except Exception as e: + print(f"错误: {e}") + + def file_operations(): + """文件操作""" + print("当前目录文件列表:") + import os + files = os.listdir('.') + for i, file in enumerate(files[:10], 1): # 只显示前10个 + print(f" {i}. {file}") + if len(files) > 10: + print(f" ... 还有 {len(files) - 10} 个文件") + + def system_info(): + """系统信息""" + import platform + print(f"操作系统: {platform.system()}") + print(f"系统版本: {platform.version()}") + print(f"处理器: {platform.processor()}") + print(f"Python版本: {platform.python_version()}") + + # 菜单功能映射 + menu_functions = { + '1': get_current_time, + '2': calculator, + '3': file_operations, + '4': system_info + } + + print("交互式菜单演示(模拟)") + print("在实际应用中,这里会有真正的用户交互") + + # 模拟用户选择 + demo_choices = ['1', '3', '4'] + for choice in demo_choices: + print(f"\n模拟用户选择: {choice}") + if choice in menu_functions: + menu_functions[choice]() + elif choice == '0': + print("退出程序") + break + else: + print("无效选择") + +# 运行演示 +console_io_demo() +interactive_menu_demo() +``` + +### 4.2 格式化输出进阶 + +```python +from datetime import datetime +import locale + +def advanced_formatting_demo(): + """高级格式化输出演示""" + print("=== 高级格式化输出演示 ===") + + # 1. 数字格式化 + print("\n1. 数字格式化") + + number = 1234567.89 + print(f"原始数字: {number}") + print(f"千分位分隔: {number:,}") + print(f"保留2位小数: {number:.2f}") + print(f"科学计数法: {number:.2e}") + print(f"百分比: {0.1234:.2%}") + print(f"填充零: {42:08d}") + print(f"右对齐: {42:>10d}") + print(f"左对齐: {42:<10d}") + print(f"居中对齐: {42:^10d}") + + # 2. 字符串格式化 + print("\n2. 字符串格式化") + + text = "Python" + print(f"原始字符串: '{text}'") + print(f"右对齐(15): '{text:>15s}'") + print(f"左对齐(15): '{text:<15s}'") + print(f"居中对齐(15): '{text:^15s}'") + print(f"填充字符: '{text:*^15s}'") + print(f"截断: '{text:.3s}'") + + # 3. 日期时间格式化 + print("\n3. 日期时间格式化") + + now = datetime.now() + print(f"当前时间: {now}") + print(f"日期: {now:%Y-%m-%d}") + print(f"时间: {now:%H:%M:%S}") + print(f"完整格式: {now:%Y年%m月%d日 %H时%M分%S秒}") + print(f"星期: {now:%A}") + print(f"月份: {now:%B}") + + # 4. 自定义格式化类 + print("\n4. 自定义格式化类") + + class Person: + def __init__(self, name, age, salary): + self.name = name + self.age = age + self.salary = salary + + def __format__(self, format_spec): + if format_spec == 'short': + return f"{self.name}({self.age})" + elif format_spec == 'long': + return f"{self.name}, {self.age}岁, 薪资{self.salary:,}元" + elif format_spec == 'salary': + return f"{self.salary:,.2f}" + else: + return str(self) + + def __str__(self): + return f"Person(name='{self.name}', age={self.age}, salary={self.salary})" + + person = Person("张三", 30, 8500.50) + print(f"默认格式: {person}") + print(f"短格式: {person:short}") + print(f"长格式: {person:long}") + print(f"薪资格式: {person:salary}") + + # 5. 表格格式化 + print("\n5. 表格格式化") + + def print_table(data, headers, widths=None): + """打印格式化表格""" + if widths is None: + widths = [max(len(str(row[i])) for row in [headers] + data) + 2 + for i in range(len(headers))] + + # 打印分隔线 + separator = '+' + '+'.join('-' * width for width in widths) + '+' + print(separator) + + # 打印表头 + header_row = '|' + '|'.join(f"{headers[i]:^{widths[i]}}" for i in range(len(headers))) + '|' + print(header_row) + print(separator) + + # 打印数据行 + for row in data: + data_row = '|' + '|'.join(f"{str(row[i]):^{widths[i]}}" for i in range(len(row))) + '|' + print(data_row) + + print(separator) + + # 示例数据 + headers = ['姓名', '年龄', '城市', '薪资'] + table_data = [ + ['张三', 25, '北京', '8,500'], + ['李四', 30, '上海', '12,000'], + ['王五', 28, '深圳', '10,500'], + ['赵六', 32, '广州', '9,800'] + ] + + print_table(table_data, headers) + + # 6. 进度条和状态显示 + print("\n6. 进度条和状态显示") + + def progress_bar(current, total, width=50, prefix='进度', suffix='完成'): + """显示进度条""" + percent = current / total + filled_width = int(width * percent) + bar = '█' * filled_width + '░' * (width - filled_width) + return f"{prefix}: [{bar}] {percent:.1%} {suffix}" + + # 模拟进度 + import time + total_steps = 20 + for i in range(total_steps + 1): + progress = progress_bar(i, total_steps) + print(f"\r{progress}", end='', flush=True) + time.sleep(0.05) + print() # 换行 + + # 7. 多行格式化 + print("\n7. 多行格式化") + + template = """ + ╔══════════════════════════════════════╗ + ║ 用户信息 ║ + ╠══════════════════════════════════════╣ + ║ 姓名: {name:<30} ║ + ║ 年龄: {age:<30} ║ + ║ 邮箱: {email:<30} ║ + ║ 注册时间: {reg_time:<26} ║ + ╚══════════════════════════════════════╝ + """ + + user_info = { + 'name': '张三', + 'age': 25, + 'email': 'zhangsan@example.com', + 'reg_time': '2023-01-15 10:30:00' + } + + print(template.format(**user_info)) + +# 运行演示 +advanced_formatting_demo() +``` + +--- + +## 五、内存IO操作 + +### 5.1 StringIO和BytesIO + +```python +from io import StringIO, BytesIO +import json + +def memory_io_demo(): + """内存IO操作演示""" + print("=== 内存IO操作演示 ===") + + # 1. StringIO - 字符串流 + print("\n1. StringIO操作") + + # 创建StringIO对象 + string_buffer = StringIO() + + # 写入数据 + string_buffer.write("Hello, ") + string_buffer.write("StringIO!\n") + string_buffer.write("这是第二行\n") + + # 获取当前位置 + print(f"当前位置: {string_buffer.tell()}") + + # 获取全部内容 + content = string_buffer.getvalue() + print(f"StringIO内容:\n{content}") + + # 重置位置并读取 + string_buffer.seek(0) + line1 = string_buffer.readline() + line2 = string_buffer.readline() + print(f"第一行: {line1.strip()}") + print(f"第二行: {line2.strip()}") + + # 关闭StringIO + string_buffer.close() + + # 2. BytesIO - 字节流 + print("\n2. BytesIO操作") + + # 创建BytesIO对象 + bytes_buffer = BytesIO() + + # 写入字节数据 + bytes_buffer.write(b"Hello, ") + bytes_buffer.write(b"BytesIO!\n") + bytes_buffer.write("这是中文".encode('utf-8')) + + # 获取字节内容 + byte_content = bytes_buffer.getvalue() + print(f"BytesIO内容: {byte_content}") + print(f"解码后: {byte_content.decode('utf-8')}") + + # 重置位置并读取 + bytes_buffer.seek(0) + chunk1 = bytes_buffer.read(7) + chunk2 = bytes_buffer.read(8) + print(f"第一块: {chunk1}") + print(f"第二块: {chunk2}") + + bytes_buffer.close() + + # 3. 实际应用示例 + print("\n3. 实际应用示例") + + def create_csv_in_memory(data): + """在内存中创建CSV""" + import csv + + output = StringIO() + writer = csv.writer(output) + + # 写入表头 + if data: + writer.writerow(data[0].keys()) + + # 写入数据 + for row in data: + writer.writerow(row.values()) + + csv_content = output.getvalue() + output.close() + return csv_content + + # 测试数据 + sample_data = [ + {'name': '张三', 'age': 25, 'city': '北京'}, + {'name': '李四', 'age': 30, 'city': '上海'}, + {'name': '王五', 'age': 28, 'city': '深圳'} + ] + + csv_result = create_csv_in_memory(sample_data) + print("内存中生成的CSV:") + print(csv_result) + + def process_json_in_memory(data): + """在内存中处理JSON""" + # 序列化到内存 + json_buffer = StringIO() + json.dump(data, json_buffer, ensure_ascii=False, indent=2) + + # 获取JSON字符串 + json_str = json_buffer.getvalue() + json_buffer.close() + + # 从字符串反序列化 + json_input = StringIO(json_str) + loaded_data = json.load(json_input) + json_input.close() + + return json_str, loaded_data + + json_str, loaded_data = process_json_in_memory(sample_data) + print("\n内存中处理的JSON:") + print(json_str) + print(f"\n反序列化结果: {loaded_data}") + +# 运行演示 +memory_io_demo() +``` + +### 5.2 临时文件操作 + +```python +import tempfile +import os +from pathlib import Path + +def temp_file_demo(): + """临时文件操作演示""" + print("=== 临时文件操作演示 ===") + + # 1. 临时文件 + print("\n1. 临时文件操作") + + # 创建临时文件 + with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False) as temp_file: + temp_file_path = temp_file.name + print(f"临时文件路径: {temp_file_path}") + + # 写入数据 + temp_file.write("这是临时文件的内容\n") + temp_file.write("第二行内容\n") + + # 重置位置并读取 + temp_file.seek(0) + content = temp_file.read() + print(f"临时文件内容:\n{content}") + + # 手动删除临时文件 + os.unlink(temp_file_path) + print("临时文件已删除") + + # 2. 自动删除的临时文件 + print("\n2. 自动删除的临时文件") + + with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8') as auto_temp: + print(f"自动删除临时文件: {auto_temp.name}") + auto_temp.write("这个文件会自动删除") + auto_temp.seek(0) + print(f"内容: {auto_temp.read()}") + # 文件在这里自动删除 + print("临时文件已自动删除") + + # 3. 临时目录 + print("\n3. 临时目录操作") + + with tempfile.TemporaryDirectory() as temp_dir: + print(f"临时目录: {temp_dir}") + + # 在临时目录中创建文件 + temp_file_path = Path(temp_dir) / 'test.txt' + temp_file_path.write_text('临时目录中的文件', encoding='utf-8') + + # 创建子目录 + sub_dir = Path(temp_dir) / 'subdir' + sub_dir.mkdir() + (sub_dir / 'subfile.txt').write_text('子目录文件', encoding='utf-8') + + # 列出临时目录内容 + print("临时目录内容:") + for item in Path(temp_dir).rglob('*'): + print(f" {item}") + # 临时目录及其内容在这里自动删除 + print("临时目录已自动删除") + + # 4. 自定义临时文件 + print("\n4. 自定义临时文件") + + # 指定前缀和后缀 + with tempfile.NamedTemporaryFile( + mode='w+', + prefix='myapp_', + suffix='.log', + encoding='utf-8' + ) as custom_temp: + print(f"自定义临时文件: {custom_temp.name}") + custom_temp.write("自定义前缀和后缀的临时文件") + + # 5. 获取临时目录信息 + print("\n5. 临时目录信息") + + temp_dir = tempfile.gettempdir() + print(f"系统临时目录: {temp_dir}") + + # 创建唯一的临时文件名 + temp_name = tempfile.mktemp(suffix='.txt', prefix='unique_') + print(f"唯一临时文件名: {temp_name}") + + # 注意:mktemp只生成名称,不创建文件,需要手动创建和删除 + + # 6. 临时文件的实际应用 + print("\n6. 临时文件实际应用") + + def process_large_data_with_temp(data_generator): + """使用临时文件处理大量数据""" + with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8') as temp_file: + # 写入大量数据到临时文件 + for i, data in enumerate(data_generator): + temp_file.write(f"{i}: {data}\n") + + # 重置位置并处理数据 + temp_file.seek(0) + line_count = 0 + total_length = 0 + + for line in temp_file: + line_count += 1 + total_length += len(line) + + return { + 'lines': line_count, + 'total_chars': total_length, + 'temp_file': temp_file.name + } + + # 模拟大量数据 + def data_generator(): + for i in range(1000): + yield f"数据项 {i} - 这是一些测试数据" + + result = process_large_data_with_temp(data_generator()) + print(f"处理结果: {result}") + + def create_temp_config(config_data): + """创建临时配置文件""" + import json + + with tempfile.NamedTemporaryFile( + mode='w', + suffix='.json', + delete=False, + encoding='utf-8' + ) as config_file: + json.dump(config_data, config_file, ensure_ascii=False, indent=2) + return config_file.name + + # 创建临时配置 + config = { + 'database': { + 'host': 'localhost', + 'port': 5432, + 'name': 'testdb' + }, + 'logging': { + 'level': 'INFO', + 'file': '/tmp/app.log' + } + } + + config_file_path = create_temp_config(config) + print(f"\n临时配置文件: {config_file_path}") + + # 读取配置文件 + with open(config_file_path, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + print(f"加载的配置: {loaded_config}") + + # 清理临时配置文件 + os.unlink(config_file_path) + print("临时配置文件已删除") + +# 运行演示 +temp_file_demo() +``` + +--- + +## 六、实战练习:文件管理系统 + +### 6.1 构建一个完整的文件管理系统 + +```python +import os +import shutil +import json +import hashlib +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional +from dataclasses import dataclass, asdict +import tempfile + +@dataclass +class FileInfo: + """文件信息数据类""" + path: str + name: str + size: int + modified: float + created: float + is_directory: bool + extension: str + hash_md5: Optional[str] = None + +class FileManager: + """文件管理系统""" + + def __init__(self, base_directory: str = None): + self.base_directory = Path(base_directory) if base_directory else Path.cwd() + self.file_index: Dict[str, FileInfo] = {} + self.operation_log: List[Dict] = [] + + def scan_directory(self, directory: str = None) -> List[FileInfo]: + """扫描目录并建立文件索引""" + scan_dir = Path(directory) if directory else self.base_directory + + if not scan_dir.exists() or not scan_dir.is_dir(): + raise ValueError(f"目录不存在或不是有效目录: {scan_dir}") + + file_list = [] + + for item in scan_dir.rglob('*'): + try: + stat = item.stat() + + file_info = FileInfo( + path=str(item), + name=item.name, + size=stat.st_size, + modified=stat.st_mtime, + created=stat.st_ctime, + is_directory=item.is_dir(), + extension=item.suffix.lower() if item.suffix else '' + ) + + # 为文件计算MD5哈希 + if not file_info.is_directory and file_info.size < 10 * 1024 * 1024: # 小于10MB + try: + file_info.hash_md5 = self._calculate_md5(item) + except Exception: + pass # 忽略无法读取的文件 + + file_list.append(file_info) + self.file_index[str(item)] = file_info + + except (OSError, PermissionError): + continue # 跳过无法访问的文件 + + self._log_operation('scan_directory', {'directory': str(scan_dir), 'files_found': len(file_list)}) + return file_list + + def _calculate_md5(self, file_path: Path) -> str: + """计算文件MD5哈希值""" + hash_md5 = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + def find_files(self, **criteria) -> List[FileInfo]: + """根据条件查找文件""" + results = [] + + for file_info in self.file_index.values(): + match = True + + # 按名称模式匹配 + if 'name_pattern' in criteria: + import fnmatch + if not fnmatch.fnmatch(file_info.name.lower(), criteria['name_pattern'].lower()): + match = False + + # 按扩展名匹配 + if 'extension' in criteria: + if file_info.extension != criteria['extension'].lower(): + match = False + + # 按大小范围匹配 + if 'min_size' in criteria and file_info.size < criteria['min_size']: + match = False + if 'max_size' in criteria and file_info.size > criteria['max_size']: + match = False + + # 按修改时间匹配 + if 'modified_after' in criteria and file_info.modified < criteria['modified_after']: + match = False + if 'modified_before' in criteria and file_info.modified > criteria['modified_before']: + match = False + + # 按文件类型匹配 + if 'is_directory' in criteria and file_info.is_directory != criteria['is_directory']: + match = False + + if match: + results.append(file_info) + + self._log_operation('find_files', {'criteria': criteria, 'results_count': len(results)}) + return results + + def find_duplicates(self) -> Dict[str, List[FileInfo]]: + """查找重复文件""" + hash_groups = {} + + for file_info in self.file_index.values(): + if not file_info.is_directory and file_info.hash_md5: + if file_info.hash_md5 not in hash_groups: + hash_groups[file_info.hash_md5] = [] + hash_groups[file_info.hash_md5].append(file_info) + + # 只返回有重复的组 + duplicates = {hash_val: files for hash_val, files in hash_groups.items() if len(files) > 1} + + self._log_operation('find_duplicates', {'duplicate_groups': len(duplicates)}) + return duplicates + + def organize_files(self, source_dir: str, target_dir: str, organize_by: str = 'extension'): + """按指定规则整理文件""" + source_path = Path(source_dir) + target_path = Path(target_dir) + + if not source_path.exists(): + raise ValueError(f"源目录不存在: {source_path}") + + target_path.mkdir(parents=True, exist_ok=True) + moved_files = 0 + + for file_path in source_path.rglob('*'): + if file_path.is_file(): + # 确定目标子目录 + if organize_by == 'extension': + ext = file_path.suffix.lower() or 'no_extension' + sub_dir = target_path / ext.lstrip('.') + elif organize_by == 'date': + modified_date = datetime.fromtimestamp(file_path.stat().st_mtime) + sub_dir = target_path / modified_date.strftime('%Y') / modified_date.strftime('%m') + elif organize_by == 'size': + size = file_path.stat().st_size + if size < 1024 * 1024: # < 1MB + sub_dir = target_path / 'small' + elif size < 10 * 1024 * 1024: # < 10MB + sub_dir = target_path / 'medium' + else: + sub_dir = target_path / 'large' + else: + sub_dir = target_path / 'others' + + # 创建目标目录 + sub_dir.mkdir(parents=True, exist_ok=True) + + # 移动文件 + target_file = sub_dir / file_path.name + + # 处理文件名冲突 + counter = 1 + while target_file.exists(): + stem = file_path.stem + suffix = file_path.suffix + target_file = sub_dir / f"{stem}_{counter}{suffix}" + counter += 1 + + shutil.move(str(file_path), str(target_file)) + moved_files += 1 + + self._log_operation('organize_files', { + 'source': str(source_path), + 'target': str(target_path), + 'organize_by': organize_by, + 'moved_files': moved_files + }) + + return moved_files + + def backup_files(self, source_paths: List[str], backup_dir: str, + compression: bool = True) -> str: + """备份文件""" + backup_path = Path(backup_dir) + backup_path.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + if compression: + import zipfile + backup_file = backup_path / f"backup_{timestamp}.zip" + + with zipfile.ZipFile(backup_file, 'w', zipfile.ZIP_DEFLATED) as zipf: + for source_path in source_paths: + source = Path(source_path) + if source.exists(): + if source.is_file(): + zipf.write(source, source.name) + else: + for file_path in source.rglob('*'): + if file_path.is_file(): + arcname = file_path.relative_to(source.parent) + zipf.write(file_path, arcname) + else: + backup_file = backup_path / f"backup_{timestamp}" + backup_file.mkdir(exist_ok=True) + + for source_path in source_paths: + source = Path(source_path) + if source.exists(): + target = backup_file / source.name + if source.is_file(): + shutil.copy2(source, target) + else: + shutil.copytree(source, target, dirs_exist_ok=True) + + self._log_operation('backup_files', { + 'sources': source_paths, + 'backup_file': str(backup_file), + 'compression': compression + }) + + return str(backup_file) + + def clean_empty_directories(self, directory: str = None) -> int: + """清理空目录""" + clean_dir = Path(directory) if directory else self.base_directory + removed_count = 0 + + # 从最深层开始清理 + for dir_path in sorted(clean_dir.rglob('*'), key=lambda p: len(p.parts), reverse=True): + if dir_path.is_dir(): + try: + if not any(dir_path.iterdir()): # 目录为空 + dir_path.rmdir() + removed_count += 1 + except OSError: + continue # 跳过无法删除的目录 + + self._log_operation('clean_empty_directories', { + 'directory': str(clean_dir), + 'removed_count': removed_count + }) + + return removed_count + + def get_directory_stats(self, directory: str = None) -> Dict: + """获取目录统计信息""" + stats_dir = Path(directory) if directory else self.base_directory + + stats = { + 'total_files': 0, + 'total_directories': 0, + 'total_size': 0, + 'file_types': {}, + 'size_distribution': {'small': 0, 'medium': 0, 'large': 0}, + 'largest_files': [], + 'newest_files': [], + 'oldest_files': [] + } + + all_files = [] + + for item in stats_dir.rglob('*'): + try: + if item.is_file(): + stats['total_files'] += 1 + size = item.stat().st_size + stats['total_size'] += size + + # 文件类型统计 + ext = item.suffix.lower() or 'no_extension' + stats['file_types'][ext] = stats['file_types'].get(ext, 0) + 1 + + # 大小分布 + if size < 1024 * 1024: # < 1MB + stats['size_distribution']['small'] += 1 + elif size < 10 * 1024 * 1024: # < 10MB + stats['size_distribution']['medium'] += 1 + else: + stats['size_distribution']['large'] += 1 + + # 收集文件信息用于排序 + file_stat = item.stat() + all_files.append({ + 'path': str(item), + 'size': size, + 'modified': file_stat.st_mtime + }) + + elif item.is_dir(): + stats['total_directories'] += 1 + + except (OSError, PermissionError): + continue + + # 最大文件(前10个) + stats['largest_files'] = sorted(all_files, key=lambda x: x['size'], reverse=True)[:10] + + # 最新文件(前10个) + stats['newest_files'] = sorted(all_files, key=lambda x: x['modified'], reverse=True)[:10] + + # 最旧文件(前10个) + stats['oldest_files'] = sorted(all_files, key=lambda x: x['modified'])[:10] + + return stats + + def _log_operation(self, operation: str, details: Dict): + """记录操作日志""" + log_entry = { + 'timestamp': datetime.now().isoformat(), + 'operation': operation, + 'details': details + } + self.operation_log.append(log_entry) + + def export_index(self, filename: str): + """导出文件索引""" + export_data = { + 'timestamp': datetime.now().isoformat(), + 'base_directory': str(self.base_directory), + 'file_count': len(self.file_index), + 'files': [asdict(file_info) for file_info in self.file_index.values()] + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(export_data, f, ensure_ascii=False, indent=2) + + self._log_operation('export_index', {'filename': filename}) + + def get_operation_log(self) -> List[Dict]: + """获取操作日志""" + return self.operation_log.copy() + +def file_manager_demo(): + """文件管理系统演示""" + print("=== 文件管理系统演示 ===") + + # 创建临时测试环境 + with tempfile.TemporaryDirectory() as temp_dir: + print(f"\n使用临时目录: {temp_dir}") + + # 创建测试文件结构 + test_dir = Path(temp_dir) + + # 创建不同类型的文件 + (test_dir / 'documents').mkdir() + (test_dir / 'images').mkdir() + (test_dir / 'code').mkdir() + (test_dir / 'archive').mkdir() + + # 创建测试文件 + test_files = [ + ('documents/report.txt', '这是一个报告文件'), + ('documents/notes.md', '# 笔记\n\n这是一些笔记'), + ('images/photo1.jpg', b'\xFF\xD8\xFF\xE0'), # JPEG文件头 + ('images/photo2.png', b'\x89PNG\r\n\x1a\n'), # PNG文件头 + ('code/main.py', 'print("Hello, World!")'), + ('code/utils.py', 'def helper(): pass'), + ('archive/data.zip', b'PK\x03\x04'), # ZIP文件头 + ('readme.txt', '项目说明文件'), + ] + + for file_path, content in test_files: + full_path = test_dir / file_path + if isinstance(content, str): + full_path.write_text(content, encoding='utf-8') + else: + full_path.write_bytes(content) + + # 创建一些重复文件 + (test_dir / 'documents' / 'copy_report.txt').write_text('这是一个报告文件', encoding='utf-8') + + print("测试文件结构已创建") + + # 初始化文件管理器 + fm = FileManager(temp_dir) + + # 1. 扫描目录 + print("\n1. 扫描目录") + files = fm.scan_directory() + print(f"发现 {len(files)} 个文件和目录") + + # 2. 查找文件 + print("\n2. 查找文件") + + # 查找Python文件 + python_files = fm.find_files(extension='.py') + print(f"Python文件: {len(python_files)} 个") + for file_info in python_files: + print(f" {file_info.name} ({file_info.size} 字节)") + + # 查找文本文件 + text_files = fm.find_files(name_pattern='*.txt') + print(f"\n文本文件: {len(text_files)} 个") + for file_info in text_files: + print(f" {file_info.name}") + + # 3. 查找重复文件 + print("\n3. 查找重复文件") + duplicates = fm.find_duplicates() + if duplicates: + for hash_val, files in duplicates.items(): + print(f"重复文件组 (MD5: {hash_val[:8]}...):") + for file_info in files: + print(f" {file_info.path}") + else: + print("未发现重复文件") + + # 4. 获取目录统计 + print("\n4. 目录统计") + stats = fm.get_directory_stats() + print(f"总文件数: {stats['total_files']}") + print(f"总目录数: {stats['total_directories']}") + print(f"总大小: {stats['total_size']} 字节") + print("文件类型分布:") + for ext, count in stats['file_types'].items(): + print(f" {ext}: {count} 个") + + # 5. 整理文件 + print("\n5. 整理文件") + organize_source = test_dir / 'documents' + organize_target = test_dir / 'organized' + + moved_count = fm.organize_files( + str(organize_source), + str(organize_target), + organize_by='extension' + ) + print(f"已整理 {moved_count} 个文件") + + # 6. 备份文件 + print("\n6. 备份文件") + backup_sources = [str(test_dir / 'code'), str(test_dir / 'readme.txt')] + backup_dir = test_dir / 'backups' + + backup_file = fm.backup_files(backup_sources, str(backup_dir), compression=True) + print(f"备份已创建: {backup_file}") + + # 7. 清理空目录 + print("\n7. 清理空目录") + removed_count = fm.clean_empty_directories() + print(f"已删除 {removed_count} 个空目录") + + # 8. 导出索引 + print("\n8. 导出文件索引") + index_file = test_dir / 'file_index.json' + fm.export_index(str(index_file)) + print(f"文件索引已导出到: {index_file}") + + # 9. 查看操作日志 + print("\n9. 操作日志") + log = fm.get_operation_log() + print(f"共记录 {len(log)} 个操作:") + for entry in log[-3:]: # 显示最后3个操作 + print(f" {entry['timestamp']}: {entry['operation']}") + + print("\n文件管理系统演示完成") + +# 运行演示 +file_manager_demo() +``` + +--- + +## 七、总结和最佳实践 + +### 7.1 IO编程最佳实践 + +1. **使用上下文管理器** + - 始终使用`with`语句处理文件操作 + - 确保资源正确释放 + +2. **选择合适的文件模式** + - 明确区分文本模式和二进制模式 + - 根据需求选择读写模式 + +3. **处理编码问题** + - 明确指定文件编码(通常使用UTF-8) + - 处理编码错误 + +4. **异常处理** + - 捕获和处理IO相关异常 + - 提供有意义的错误信息 + +5. **性能优化** + - 对大文件使用分块读取 + - 使用缓冲区提高效率 + - 考虑使用内存映射文件 + +### 7.2 文件操作安全性 + +1. **路径安全** + - 验证文件路径的合法性 + - 防止路径遍历攻击 + +2. **权限检查** + - 检查文件读写权限 + - 处理权限不足的情况 + +3. **数据完整性** + - 使用校验和验证文件完整性 + - 实现原子操作避免数据损坏 + +### 7.3 下一步学习方向 + +1. **网络IO编程** + - 学习socket编程 + - 掌握HTTP客户端和服务器 + +2. **异步IO** + - 学习asyncio模块 + - 掌握异步文件操作 + +3. **数据库IO** + - 学习数据库连接和操作 + - 掌握ORM框架 + +4. **高级文件格式** + - 学习处理Excel、PDF等格式 + - 掌握图像和音视频文件处理 + +### 7.4 练习建议 + +1. **创建文件处理工具**,实践各种文件操作 +2. **编写日志系统**,掌握文件写入和轮转 +3. **实现配置管理**,练习JSON/YAML文件处理 +4. **开发备份工具**,综合运用文件和目录操作 +5. **构建文件监控系统**,学习文件系统事件处理 + +通过本章的学习,你应该能够: +- 熟练进行各种文件IO操作 +- 掌握目录和路径操作技巧 +- 理解标准输入输出的使用 +- 运用内存IO和临时文件 +- 构建完整的文件管理系统 +- 遵循IO编程的最佳实践 + +IO编程是Python应用开发的基础技能,需要在实践中不断提高。记住,良好的IO处理不仅能提高程序性能,还能增强程序的稳定性和用户体验。 + + + + + + diff --git a/docs/Python/13.md b/docs/Python/13.md new file mode 100644 index 000000000..119cb872c --- /dev/null +++ b/docs/Python/13.md @@ -0,0 +1,1953 @@ +--- +title: 第13天-进程和线程 +author: 哪吒 +date: '2023-06-15' +--- + +# 第13天-进程和线程 + +## 学习目标 + +通过本章学习,你将掌握: +- 理解进程和线程的概念与区别 +- 掌握Python多线程编程 +- 学会使用多进程处理CPU密集型任务 +- 理解GIL(全局解释器锁)的影响 +- 掌握线程同步和进程间通信 +- 学会使用线程池和进程池 +- 理解异步编程的基本概念 + +--- + +## 一、进程和线程基础概念 + +### 1.1 基本概念 + +```python +import threading +import multiprocessing +import time +import os + +def concept_demo(): + """进程和线程概念演示""" + print("=== 进程和线程概念演示 ===") + + # 1. 进程信息 + print("\n1. 进程信息") + print(f"当前进程ID: {os.getpid()}") + print(f"父进程ID: {os.getppid()}") + print(f"CPU核心数: {multiprocessing.cpu_count()}") + + # 2. 线程信息 + print("\n2. 线程信息") + print(f"当前线程名称: {threading.current_thread().name}") + print(f"当前线程ID: {threading.get_ident()}") + print(f"活跃线程数: {threading.active_count()}") + + # 3. 简单的线程示例 + print("\n3. 简单线程示例") + + def worker_function(name, delay): + """工作线程函数""" + print(f"线程 {name} 开始工作 (线程ID: {threading.get_ident()})") + time.sleep(delay) + print(f"线程 {name} 工作完成") + + # 创建并启动线程 + thread1 = threading.Thread(target=worker_function, args=("Worker-1", 2)) + thread2 = threading.Thread(target=worker_function, args=("Worker-2", 1)) + + print("启动线程...") + thread1.start() + thread2.start() + + # 等待线程完成 + thread1.join() + thread2.join() + + print("所有线程完成") + + # 4. 进程vs线程的区别 + print("\n4. 进程vs线程的区别") + print(""" + 进程 (Process): + - 独立的内存空间 + - 进程间通信需要特殊机制 + - 创建开销大 + - 适合CPU密集型任务 + - 真正的并行执行 + + 线程 (Thread): + - 共享进程内存空间 + - 线程间通信简单 + - 创建开销小 + - 适合IO密集型任务 + - 受GIL限制(CPython) + """) + +# 运行演示 +concept_demo() +``` + +### 1.2 GIL(全局解释器锁) + +```python +import threading +import time +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor + +def gil_demo(): + """GIL影响演示""" + print("=== GIL影响演示 ===") + + def cpu_intensive_task(n): + """CPU密集型任务""" + result = 0 + for i in range(n): + result += i * i + return result + + def io_intensive_task(duration): + """IO密集型任务""" + time.sleep(duration) + return f"IO任务完成,耗时{duration}秒" + + # 1. CPU密集型任务对比 + print("\n1. CPU密集型任务对比") + + # 单线程执行 + start_time = time.time() + results = [] + for i in range(4): + result = cpu_intensive_task(1000000) + results.append(result) + single_thread_time = time.time() - start_time + print(f"单线程执行时间: {single_thread_time:.2f}秒") + + # 多线程执行(受GIL限制) + start_time = time.time() + with ThreadPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(cpu_intensive_task, 1000000) for _ in range(4)] + results = [future.result() for future in futures] + multi_thread_time = time.time() - start_time + print(f"多线程执行时间: {multi_thread_time:.2f}秒") + + # 多进程执行(不受GIL限制) + start_time = time.time() + with ProcessPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(cpu_intensive_task, 1000000) for _ in range(4)] + results = [future.result() for future in futures] + multi_process_time = time.time() - start_time + print(f"多进程执行时间: {multi_process_time:.2f}秒") + + print(f"\n性能对比:") + print(f"多线程相对单线程: {single_thread_time/multi_thread_time:.2f}x") + print(f"多进程相对单线程: {single_thread_time/multi_process_time:.2f}x") + + # 2. IO密集型任务对比 + print("\n2. IO密集型任务对比") + + # 单线程执行 + start_time = time.time() + results = [] + for i in range(4): + result = io_intensive_task(1) + results.append(result) + single_thread_io_time = time.time() - start_time + print(f"单线程IO执行时间: {single_thread_io_time:.2f}秒") + + # 多线程执行(IO密集型受益于多线程) + start_time = time.time() + with ThreadPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(io_intensive_task, 1) for _ in range(4)] + results = [future.result() for future in futures] + multi_thread_io_time = time.time() - start_time + print(f"多线程IO执行时间: {multi_thread_io_time:.2f}秒") + + print(f"\nIO任务性能对比:") + print(f"多线程相对单线程: {single_thread_io_time/multi_thread_io_time:.2f}x") + + # 3. GIL的影响总结 + print("\n3. GIL影响总结") + print(""" + GIL(全局解释器锁)的影响: + + CPU密集型任务: + - 多线程无法提升性能,甚至可能更慢 + - 多进程可以充分利用多核CPU + - 推荐使用multiprocessing + + IO密集型任务: + - 多线程可以显著提升性能 + - 线程在等待IO时会释放GIL + - 推荐使用threading + + 混合型任务: + - 根据任务特点选择合适的并发方式 + - 可以考虑使用asyncio异步编程 + """) + +# 运行演示 +gil_demo() +``` + +--- + +## 二、多线程编程 + +### 2.1 线程基础操作 + +```python +import threading +import time +import random +from queue import Queue + +def threading_basics(): + """线程基础操作演示""" + print("=== 线程基础操作演示 ===") + + # 1. 创建线程的不同方式 + print("\n1. 创建线程的不同方式") + + # 方式1:使用函数创建线程 + def simple_task(name, count): + for i in range(count): + print(f"线程 {name}: 执行第 {i+1} 次") + time.sleep(0.5) + + thread1 = threading.Thread(target=simple_task, args=("Function-Thread", 3)) + + # 方式2:继承Thread类 + class CustomThread(threading.Thread): + def __init__(self, name, count): + super().__init__() + self.name = name + self.count = count + + def run(self): + for i in range(self.count): + print(f"自定义线程 {self.name}: 执行第 {i+1} 次") + time.sleep(0.5) + + thread2 = CustomThread("Custom-Thread", 3) + + # 启动线程 + print("启动线程...") + thread1.start() + thread2.start() + + # 等待线程完成 + thread1.join() + thread2.join() + print("所有线程完成") + + # 2. 线程属性和方法 + print("\n2. 线程属性和方法") + + def demo_thread_properties(): + current = threading.current_thread() + print(f"线程名称: {current.name}") + print(f"线程ID: {current.ident}") + print(f"是否存活: {current.is_alive()}") + print(f"是否为守护线程: {current.daemon}") + + # 创建普通线程 + normal_thread = threading.Thread(target=demo_thread_properties, name="NormalThread") + + # 创建守护线程 + daemon_thread = threading.Thread(target=demo_thread_properties, name="DaemonThread") + daemon_thread.daemon = True + + print("普通线程属性:") + normal_thread.start() + normal_thread.join() + + print("\n守护线程属性:") + daemon_thread.start() + daemon_thread.join() + + # 3. 线程间数据传递 + print("\n3. 线程间数据传递") + + # 使用Queue进行线程间通信 + data_queue = Queue() + result_queue = Queue() + + def producer(queue, count): + """生产者线程""" + for i in range(count): + item = f"数据-{i}" + queue.put(item) + print(f"生产者: 生产了 {item}") + time.sleep(0.1) + queue.put(None) # 结束标志 + + def consumer(data_queue, result_queue): + """消费者线程""" + while True: + item = data_queue.get() + if item is None: + break + + # 处理数据 + processed = f"处理后的-{item}" + result_queue.put(processed) + print(f"消费者: 处理了 {item} -> {processed}") + time.sleep(0.2) + + data_queue.task_done() + + # 启动生产者和消费者线程 + producer_thread = threading.Thread(target=producer, args=(data_queue, 5)) + consumer_thread = threading.Thread(target=consumer, args=(data_queue, result_queue)) + + producer_thread.start() + consumer_thread.start() + + producer_thread.join() + consumer_thread.join() + + # 获取结果 + print("\n处理结果:") + while not result_queue.empty(): + result = result_queue.get() + print(f" {result}") + +# 运行演示 +threading_basics() +``` + +### 2.2 线程同步 + +```python +import threading +import time +import random + +def thread_synchronization(): + """线程同步演示""" + print("=== 线程同步演示 ===") + + # 1. 不使用锁的问题演示 + print("\n1. 不使用锁的问题") + + shared_counter = 0 + + def unsafe_increment(count): + global shared_counter + for _ in range(count): + temp = shared_counter + time.sleep(0.0001) # 模拟处理时间 + shared_counter = temp + 1 + + # 创建多个线程同时修改共享变量 + threads = [] + for i in range(5): + thread = threading.Thread(target=unsafe_increment, args=(100,)) + threads.append(thread) + + start_time = time.time() + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + print(f"不安全的计数结果: {shared_counter} (期望: 500)") + print(f"执行时间: {time.time() - start_time:.2f}秒") + + # 2. 使用Lock锁 + print("\n2. 使用Lock锁") + + shared_counter = 0 + counter_lock = threading.Lock() + + def safe_increment(count, lock): + global shared_counter + for _ in range(count): + with lock: # 使用with语句自动获取和释放锁 + temp = shared_counter + time.sleep(0.0001) + shared_counter = temp + 1 + + threads = [] + for i in range(5): + thread = threading.Thread(target=safe_increment, args=(100, counter_lock)) + threads.append(thread) + + start_time = time.time() + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + print(f"安全的计数结果: {shared_counter} (期望: 500)") + print(f"执行时间: {time.time() - start_time:.2f}秒") + + # 3. 使用RLock可重入锁 + print("\n3. 使用RLock可重入锁") + + rlock = threading.RLock() + + def recursive_function(n, lock): + with lock: + print(f"递归调用 {n}") + if n > 0: + recursive_function(n-1, lock) # 同一线程可以多次获取RLock + + thread = threading.Thread(target=recursive_function, args=(3, rlock)) + thread.start() + thread.join() + + # 4. 使用Condition条件变量 + print("\n4. 使用Condition条件变量") + + condition = threading.Condition() + items = [] + + def consumer(name): + with condition: + while len(items) == 0: + print(f"消费者 {name} 等待商品...") + condition.wait() # 等待条件满足 + + item = items.pop(0) + print(f"消费者 {name} 消费了 {item}") + + def producer(): + for i in range(3): + with condition: + item = f"商品-{i}" + items.append(item) + print(f"生产者生产了 {item}") + condition.notify_all() # 通知所有等待的线程 + time.sleep(1) + + # 启动消费者线程 + consumer_threads = [] + for i in range(2): + thread = threading.Thread(target=consumer, args=(f"Consumer-{i}",)) + consumer_threads.append(thread) + thread.start() + + # 启动生产者线程 + producer_thread = threading.Thread(target=producer) + producer_thread.start() + + # 等待所有线程完成 + producer_thread.join() + for thread in consumer_threads: + thread.join() + + # 5. 使用Semaphore信号量 + print("\n5. 使用Semaphore信号量") + + # 限制同时访问资源的线程数量 + semaphore = threading.Semaphore(2) # 最多2个线程同时访问 + + def access_resource(name): + with semaphore: + print(f"{name} 获得资源访问权限") + time.sleep(2) # 模拟使用资源 + print(f"{name} 释放资源") + + threads = [] + for i in range(5): + thread = threading.Thread(target=access_resource, args=(f"Thread-{i}",)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join() + + # 6. 使用Event事件 + print("\n6. 使用Event事件") + + event = threading.Event() + + def waiter(name): + print(f"{name} 等待事件...") + event.wait() # 等待事件被设置 + print(f"{name} 收到事件,开始工作") + + def setter(): + time.sleep(2) + print("设置事件") + event.set() # 设置事件 + + # 启动等待线程 + waiter_threads = [] + for i in range(3): + thread = threading.Thread(target=waiter, args=(f"Waiter-{i}",)) + waiter_threads.append(thread) + thread.start() + + # 启动设置线程 + setter_thread = threading.Thread(target=setter) + setter_thread.start() + + # 等待所有线程完成 + setter_thread.join() + for thread in waiter_threads: + thread.join() + +# 运行演示 +thread_synchronization() +``` + +### 2.3 线程池 + +```python +from concurrent.futures import ThreadPoolExecutor, as_completed +import threading +import time +import requests +from queue import Queue + +def thread_pool_demo(): + """线程池演示""" + print("=== 线程池演示 ===") + + # 1. 基本线程池使用 + print("\n1. 基本线程池使用") + + def worker_task(name, duration): + print(f"任务 {name} 开始执行 (线程: {threading.current_thread().name})") + time.sleep(duration) + result = f"任务 {name} 完成,耗时 {duration} 秒" + print(result) + return result + + # 使用ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=3, thread_name_prefix="Worker") as executor: + # 提交任务 + futures = [] + for i in range(5): + future = executor.submit(worker_task, f"Task-{i}", random.uniform(1, 3)) + futures.append(future) + + # 获取结果 + for future in as_completed(futures): + result = future.result() + print(f"收到结果: {result}") + + # 2. 使用map方法 + print("\n2. 使用map方法") + + def square_number(n): + time.sleep(0.1) # 模拟计算时间 + return n * n + + numbers = list(range(1, 11)) + + with ThreadPoolExecutor(max_workers=4) as executor: + results = list(executor.map(square_number, numbers)) + print(f"平方结果: {results}") + + # 3. 网络请求示例 + print("\n3. 并发网络请求示例") + + def fetch_url(url): + """获取URL内容""" + try: + # 注意:这里使用模拟的网络请求 + print(f"正在请求: {url}") + time.sleep(random.uniform(0.5, 2)) # 模拟网络延迟 + return { + 'url': url, + 'status': 200, + 'content_length': random.randint(1000, 5000) + } + except Exception as e: + return { + 'url': url, + 'error': str(e) + } + + urls = [ + 'http://example.com/page1', + 'http://example.com/page2', + 'http://example.com/page3', + 'http://example.com/page4', + 'http://example.com/page5' + ] + + # 串行请求 + start_time = time.time() + serial_results = [] + for url in urls: + result = fetch_url(url) + serial_results.append(result) + serial_time = time.time() - start_time + print(f"串行请求耗时: {serial_time:.2f}秒") + + # 并行请求 + start_time = time.time() + with ThreadPoolExecutor(max_workers=5) as executor: + parallel_results = list(executor.map(fetch_url, urls)) + parallel_time = time.time() - start_time + print(f"并行请求耗时: {parallel_time:.2f}秒") + print(f"性能提升: {serial_time/parallel_time:.2f}x") + + # 4. 任务队列处理 + print("\n4. 任务队列处理") + + task_queue = Queue() + result_queue = Queue() + + # 添加任务到队列 + for i in range(10): + task_queue.put(f"任务-{i}") + + def process_task_queue(task_queue, result_queue): + """处理任务队列""" + while True: + try: + task = task_queue.get(timeout=1) + # 处理任务 + time.sleep(0.5) + result = f"处理完成: {task}" + result_queue.put(result) + print(f"线程 {threading.current_thread().name} 完成 {task}") + task_queue.task_done() + except: + break # 队列为空,退出 + + with ThreadPoolExecutor(max_workers=3) as executor: + # 启动工作线程 + futures = [] + for i in range(3): + future = executor.submit(process_task_queue, task_queue, result_queue) + futures.append(future) + + # 等待所有任务完成 + task_queue.join() + + # 收集结果 + results = [] + while not result_queue.empty(): + results.append(result_queue.get()) + + print(f"\n处理完成,共 {len(results)} 个结果") + + # 5. 异常处理 + print("\n5. 线程池异常处理") + + def risky_task(n): + if n % 3 == 0: + raise ValueError(f"任务 {n} 出现错误") + return f"任务 {n} 成功完成" + + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(risky_task, i) for i in range(6)] + + for i, future in enumerate(futures): + try: + result = future.result() + print(f"任务 {i}: {result}") + except Exception as e: + print(f"任务 {i} 异常: {e}") + +# 运行演示 +thread_pool_demo() +``` + +--- + +## 三、多进程编程 + +### 3.1 进程基础操作 + +```python +import multiprocessing +import os +import time +from multiprocessing import Process, Queue, Pipe, Value, Array, Lock + +def process_basics(): + """进程基础操作演示""" + print("=== 进程基础操作演示 ===") + + # 1. 创建进程的不同方式 + print("\n1. 创建进程的不同方式") + + def worker_process(name, duration): + """工作进程函数""" + print(f"进程 {name} 开始工作 (PID: {os.getpid()})") + time.sleep(duration) + print(f"进程 {name} 工作完成") + + # 方式1:使用函数创建进程 + process1 = Process(target=worker_process, args=("Process-1", 2)) + + # 方式2:继承Process类 + class CustomProcess(Process): + def __init__(self, name, duration): + super().__init__() + self.process_name = name + self.duration = duration + + def run(self): + print(f"自定义进程 {self.process_name} 开始工作 (PID: {os.getpid()})") + time.sleep(self.duration) + print(f"自定义进程 {self.process_name} 工作完成") + + process2 = CustomProcess("Custom-Process", 1) + + # 启动进程 + print(f"主进程 PID: {os.getpid()}") + print("启动子进程...") + + process1.start() + process2.start() + + # 等待进程完成 + process1.join() + process2.join() + + print("所有子进程完成") + + # 2. 进程属性和方法 + print("\n2. 进程属性和方法") + + def demo_process_properties(): + current = multiprocessing.current_process() + print(f"进程名称: {current.name}") + print(f"进程PID: {current.pid}") + print(f"是否存活: {current.is_alive()}") + print(f"是否为守护进程: {current.daemon}") + + process = Process(target=demo_process_properties, name="DemoProcess") + process.start() + process.join() + + # 3. 守护进程 + print("\n3. 守护进程") + + def daemon_worker(): + while True: + print(f"守护进程工作中... (PID: {os.getpid()})") + time.sleep(1) + + daemon_process = Process(target=daemon_worker) + daemon_process.daemon = True # 设置为守护进程 + daemon_process.start() + + time.sleep(3) # 主进程工作3秒 + print("主进程结束,守护进程也会自动结束") + # 守护进程会随主进程结束而结束 + +# 运行演示(注意:某些功能在Windows上可能需要特殊处理) +if __name__ == '__main__': + process_basics() +``` + +### 3.2 进程间通信 + +```python +import multiprocessing +from multiprocessing import Process, Queue, Pipe, Value, Array, Lock, Manager +import time +import os + +def inter_process_communication(): + """进程间通信演示""" + print("=== 进程间通信演示 ===") + + # 1. 使用Queue队列 + print("\n1. 使用Queue队列") + + def producer(queue, name, count): + """生产者进程""" + for i in range(count): + item = f"{name}-数据-{i}" + queue.put(item) + print(f"生产者 {name} (PID: {os.getpid()}) 生产: {item}") + time.sleep(0.5) + queue.put(None) # 结束标志 + + def consumer(queue, name): + """消费者进程""" + while True: + item = queue.get() + if item is None: + break + print(f"消费者 {name} (PID: {os.getpid()}) 消费: {item}") + time.sleep(0.3) + + # 创建队列 + queue = Queue() + + # 创建进程 + producer_process = Process(target=producer, args=(queue, "Producer-1", 5)) + consumer_process = Process(target=consumer, args=(queue, "Consumer-1")) + + # 启动进程 + producer_process.start() + consumer_process.start() + + # 等待生产者完成 + producer_process.join() + consumer_process.join() + + # 2. 使用Pipe管道 + print("\n2. 使用Pipe管道") + + def sender(conn, messages): + """发送者进程""" + for msg in messages: + conn.send(msg) + print(f"发送者 (PID: {os.getpid()}) 发送: {msg}") + time.sleep(0.5) + conn.close() + + def receiver(conn): + """接收者进程""" + while True: + try: + msg = conn.recv() + print(f"接收者 (PID: {os.getpid()}) 接收: {msg}") + except EOFError: + break + conn.close() + + # 创建管道 + parent_conn, child_conn = Pipe() + + messages = ["消息1", "消息2", "消息3"] + + # 创建进程 + sender_process = Process(target=sender, args=(child_conn, messages)) + receiver_process = Process(target=receiver, args=(parent_conn,)) + + # 启动进程 + sender_process.start() + receiver_process.start() + + # 等待进程完成 + sender_process.join() + receiver_process.join() + + # 3. 使用共享内存 + print("\n3. 使用共享内存") + + def worker_with_shared_data(shared_value, shared_array, lock, worker_id): + """使用共享数据的工作进程""" + for i in range(5): + with lock: + # 修改共享值 + shared_value.value += 1 + # 修改共享数组 + shared_array[worker_id] += 1 + print(f"工作进程 {worker_id} (PID: {os.getpid()}): " + f"共享值={shared_value.value}, 数组[{worker_id}]={shared_array[worker_id]}") + time.sleep(0.1) + + # 创建共享数据 + shared_value = Value('i', 0) # 共享整数 + shared_array = Array('i', [0, 0, 0]) # 共享数组 + lock = Lock() # 锁 + + # 创建多个工作进程 + processes = [] + for i in range(3): + p = Process(target=worker_with_shared_data, + args=(shared_value, shared_array, lock, i)) + processes.append(p) + p.start() + + # 等待所有进程完成 + for p in processes: + p.join() + + print(f"最终结果: 共享值={shared_value.value}, 共享数组={list(shared_array)}") + + # 4. 使用Manager + print("\n4. 使用Manager") + + def worker_with_manager(shared_dict, shared_list, worker_id): + """使用Manager的工作进程""" + # 修改共享字典 + shared_dict[f'worker_{worker_id}'] = os.getpid() + + # 修改共享列表 + shared_list.append(f'来自进程{worker_id}的数据') + + print(f"工作进程 {worker_id} (PID: {os.getpid()}) 完成数据更新") + + # 创建Manager + with Manager() as manager: + # 创建共享数据结构 + shared_dict = manager.dict() + shared_list = manager.list() + + # 创建进程 + processes = [] + for i in range(3): + p = Process(target=worker_with_manager, + args=(shared_dict, shared_list, i)) + processes.append(p) + p.start() + + # 等待所有进程完成 + for p in processes: + p.join() + + print(f"共享字典: {dict(shared_dict)}") + print(f"共享列表: {list(shared_list)}") + +# 运行演示 +if __name__ == '__main__': + inter_process_communication() +``` + +### 3.3 进程池 + +```python +from multiprocessing import Pool, cpu_count +import time +import os + +def process_pool_demo(): + """进程池演示""" + print("=== 进程池演示 ===") + + # 1. 基本进程池使用 + print("\n1. 基本进程池使用") + + def cpu_intensive_task(n): + """CPU密集型任务""" + print(f"进程 {os.getpid()} 开始处理任务 {n}") + result = sum(i * i for i in range(n)) + print(f"进程 {os.getpid()} 完成任务 {n},结果: {result}") + return result + + # 使用进程池 + with Pool(processes=cpu_count()) as pool: + tasks = [100000, 200000, 300000, 400000] + + # 方式1:使用map + print("使用map方法:") + start_time = time.time() + results = pool.map(cpu_intensive_task, tasks) + end_time = time.time() + + print(f"结果: {results}") + print(f"执行时间: {end_time - start_time:.2f}秒") + + # 2. 异步执行 + print("\n2. 异步执行") + + def long_running_task(name, duration): + print(f"任务 {name} 开始 (PID: {os.getpid()})") + time.sleep(duration) + return f"任务 {name} 完成,耗时 {duration} 秒" + + with Pool(processes=3) as pool: + # 异步提交任务 + async_results = [] + tasks = [("Task-A", 2), ("Task-B", 1), ("Task-C", 3), ("Task-D", 1)] + + for name, duration in tasks: + async_result = pool.apply_async(long_running_task, (name, duration)) + async_results.append((name, async_result)) + + # 获取结果 + for name, async_result in async_results: + try: + result = async_result.get(timeout=5) # 设置超时 + print(f"收到结果: {result}") + except Exception as e: + print(f"任务 {name} 出现异常: {e}") + + # 3. 使用回调函数 + print("\n3. 使用回调函数") + + def callback_function(result): + print(f"回调函数收到结果: {result}") + + def error_callback(error): + print(f"回调函数收到错误: {error}") + + def task_with_callback(x): + if x == 3: + raise ValueError("任务3出现错误") + return x * x + + with Pool(processes=2) as pool: + for i in range(1, 5): + pool.apply_async( + task_with_callback, + (i,), + callback=callback_function, + error_callback=error_callback + ) + + pool.close() # 不再接受新任务 + pool.join() # 等待所有任务完成 + + # 4. 进程池vs线程池性能对比 + print("\n4. 进程池vs线程池性能对比") + + def cpu_bound_task(n): + """CPU密集型任务""" + return sum(i * i for i in range(n)) + + tasks = [500000] * 4 + + # 串行执行 + start_time = time.time() + serial_results = [cpu_bound_task(task) for task in tasks] + serial_time = time.time() - start_time + print(f"串行执行时间: {serial_time:.2f}秒") + + # 进程池执行 + start_time = time.time() + with Pool(processes=4) as pool: + process_results = pool.map(cpu_bound_task, tasks) + process_time = time.time() - start_time + print(f"进程池执行时间: {process_time:.2f}秒") + + # 线程池执行(用于对比) + from concurrent.futures import ThreadPoolExecutor + start_time = time.time() + with ThreadPoolExecutor(max_workers=4) as executor: + thread_results = list(executor.map(cpu_bound_task, tasks)) + thread_time = time.time() - start_time + print(f"线程池执行时间: {thread_time:.2f}秒") + + print(f"\n性能对比:") + print(f"进程池相对串行: {serial_time/process_time:.2f}x") + print(f"线程池相对串行: {serial_time/thread_time:.2f}x") + print(f"进程池相对线程池: {thread_time/process_time:.2f}x") + +# 运行演示 +if __name__ == '__main__': + process_pool_demo() +``` + +--- + +## 四、异步编程基础 + +### 4.1 异步编程概念 + +```python +import asyncio +import time +import aiohttp +import aiofiles + +async def async_programming_basics(): + """异步编程基础演示""" + print("=== 异步编程基础演示 ===") + + # 1. 基本异步函数 + print("\n1. 基本异步函数") + + async def simple_async_task(name, delay): + print(f"任务 {name} 开始") + await asyncio.sleep(delay) # 异步等待 + print(f"任务 {name} 完成") + return f"任务 {name} 结果" + + # 运行单个异步任务 + result = await simple_async_task("Task-1", 1) + print(f"结果: {result}") + + # 2. 并发执行多个异步任务 + print("\n2. 并发执行多个异步任务") + + async def concurrent_tasks(): + # 创建多个任务 + tasks = [ + simple_async_task("Concurrent-A", 2), + simple_async_task("Concurrent-B", 1), + simple_async_task("Concurrent-C", 3) + ] + + # 并发执行 + start_time = time.time() + results = await asyncio.gather(*tasks) + end_time = time.time() + + print(f"并发执行结果: {results}") + print(f"总耗时: {end_time - start_time:.2f}秒") + + await concurrent_tasks() + + # 3. 异步生成器 + print("\n3. 异步生成器") + + async def async_generator(count): + for i in range(count): + await asyncio.sleep(0.5) + yield f"数据-{i}" + + async def consume_async_generator(): + async for item in async_generator(5): + print(f"接收到: {item}") + + await consume_async_generator() + + # 4. 异步上下文管理器 + print("\n4. 异步上下文管理器") + + class AsyncContextManager: + async def __aenter__(self): + print("进入异步上下文") + await asyncio.sleep(0.1) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + print("退出异步上下文") + await asyncio.sleep(0.1) + + async with AsyncContextManager() as acm: + print("在异步上下文中工作") + await asyncio.sleep(0.5) + + # 5. 异步迭代器 + print("\n5. 异步迭代器") + + class AsyncIterator: + def __init__(self, count): + self.count = count + self.current = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.current >= self.count: + raise StopAsyncIteration + + await asyncio.sleep(0.2) + self.current += 1 + return f"异步项-{self.current}" + + async for item in AsyncIterator(3): + print(f"迭代得到: {item}") + +# 运行异步演示 +async def run_async_demo(): + await async_programming_basics() + +# 如果直接运行此脚本 +if __name__ == '__main__': + asyncio.run(run_async_demo()) +``` + +### 4.2 异步IO操作 + +```python +import asyncio +import aiofiles +import aiohttp +import time +from pathlib import Path + +async def async_io_demo(): + """异步IO操作演示""" + print("=== 异步IO操作演示 ===") + + # 1. 异步文件操作 + print("\n1. 异步文件操作") + + async def async_file_operations(): + # 异步写入文件 + async with aiofiles.open('async_test.txt', 'w', encoding='utf-8') as f: + await f.write('这是异步写入的内容\n') + await f.write('第二行内容\n') + + # 异步读取文件 + async with aiofiles.open('async_test.txt', 'r', encoding='utf-8') as f: + content = await f.read() + print(f"异步读取的内容:\n{content}") + + # 清理文件 + Path('async_test.txt').unlink() + + await async_file_operations() + + # 2. 模拟异步网络请求 + print("\n2. 模拟异步网络请求") + + async def fetch_data(url, delay): + """模拟异步网络请求""" + print(f"开始请求: {url}") + await asyncio.sleep(delay) # 模拟网络延迟 + return { + 'url': url, + 'status': 200, + 'data': f'来自 {url} 的数据' + } + + async def fetch_multiple_urls(): + urls = [ + ('http://api1.example.com', 1), + ('http://api2.example.com', 2), + ('http://api3.example.com', 1.5), + ('http://api4.example.com', 0.5) + ] + + # 串行请求 + start_time = time.time() + serial_results = [] + for url, delay in urls: + result = await fetch_data(url, delay) + serial_results.append(result) + serial_time = time.time() - start_time + print(f"串行请求耗时: {serial_time:.2f}秒") + + # 并发请求 + start_time = time.time() + tasks = [fetch_data(url, delay) for url, delay in urls] + concurrent_results = await asyncio.gather(*tasks) + concurrent_time = time.time() - start_time + print(f"并发请求耗时: {concurrent_time:.2f}秒") + print(f"性能提升: {serial_time/concurrent_time:.2f}x") + + return concurrent_results + + results = await fetch_multiple_urls() + print(f"请求结果数量: {len(results)}") + + # 3. 异步任务控制 + print("\n3. 异步任务控制") + + async def task_control_demo(): + # 任务超时控制 + async def slow_task(): + await asyncio.sleep(3) + return "慢任务完成" + + try: + result = await asyncio.wait_for(slow_task(), timeout=2) + print(f"任务结果: {result}") + except asyncio.TimeoutError: + print("任务超时") + + # 任务取消 + async def cancellable_task(): + try: + for i in range(10): + print(f"可取消任务执行中: {i}") + await asyncio.sleep(0.5) + return "任务完成" + except asyncio.CancelledError: + print("任务被取消") + raise + + task = asyncio.create_task(cancellable_task()) + await asyncio.sleep(2) # 让任务运行一段时间 + task.cancel() # 取消任务 + + try: + await task + except asyncio.CancelledError: + print("确认任务已取消") + + await task_control_demo() + + # 4. 异步队列 + print("\n4. 异步队列") + + async def async_queue_demo(): + queue = asyncio.Queue(maxsize=3) + + async def producer(queue, name, count): + for i in range(count): + item = f"{name}-项目-{i}" + await queue.put(item) + print(f"生产者 {name} 生产: {item}") + await asyncio.sleep(0.5) + + async def consumer(queue, name): + while True: + try: + item = await asyncio.wait_for(queue.get(), timeout=3) + print(f"消费者 {name} 消费: {item}") + queue.task_done() + await asyncio.sleep(0.3) + except asyncio.TimeoutError: + print(f"消费者 {name} 超时退出") + break + + # 创建生产者和消费者任务 + producer_task = asyncio.create_task(producer(queue, "Producer-1", 5)) + consumer_task1 = asyncio.create_task(consumer(queue, "Consumer-1")) + consumer_task2 = asyncio.create_task(consumer(queue, "Consumer-2")) + + # 等待生产者完成 + await producer_task + + # 等待队列清空 + await queue.join() + + # 取消消费者任务 + consumer_task1.cancel() + consumer_task2.cancel() + + try: + await asyncio.gather(consumer_task1, consumer_task2) + except asyncio.CancelledError: + pass + + await async_queue_demo() + +# 运行异步IO演示 +if __name__ == '__main__': + asyncio.run(async_io_demo()) +``` + +--- + +## 五、实战练习:并发下载器 + +### 5.1 构建一个完整的并发下载系统 + +```python +import asyncio +import threading +import multiprocessing +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed +import time +import os +from pathlib import Path +import hashlib +from dataclasses import dataclass +from typing import List, Optional +from queue import Queue +import json + +@dataclass +class DownloadTask: + """下载任务数据类""" + url: str + filename: str + size: Optional[int] = None + checksum: Optional[str] = None + +@dataclass +class DownloadResult: + """下载结果数据类""" + task: DownloadTask + success: bool + duration: float + error: Optional[str] = None + actual_size: Optional[int] = None + +class ConcurrentDownloader: + """并发下载器""" + + def __init__(self, download_dir: str = "downloads"): + self.download_dir = Path(download_dir) + self.download_dir.mkdir(exist_ok=True) + self.results: List[DownloadResult] = [] + self.progress_queue = Queue() + + def simulate_download(self, task: DownloadTask) -> DownloadResult: + """模拟下载文件""" + start_time = time.time() + + try: + # 模拟下载过程 + print(f"开始下载: {task.url} -> {task.filename}") + + # 模拟网络延迟和下载时间 + download_time = len(task.filename) * 0.1 + 1 # 基于文件名长度模拟下载时间 + time.sleep(download_time) + + # 创建模拟文件 + file_path = self.download_dir / task.filename + content = f"模拟下载的文件内容: {task.url}\n" * 100 + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + actual_size = len(content.encode('utf-8')) + duration = time.time() - start_time + + # 验证文件大小(如果提供了预期大小) + if task.size and abs(actual_size - task.size) > 100: + raise ValueError(f"文件大小不匹配: 期望 {task.size}, 实际 {actual_size}") + + print(f"下载完成: {task.filename} ({actual_size} 字节, {duration:.2f}秒)") + + return DownloadResult( + task=task, + success=True, + duration=duration, + actual_size=actual_size + ) + + except Exception as e: + duration = time.time() - start_time + print(f"下载失败: {task.filename} - {str(e)}") + + return DownloadResult( + task=task, + success=False, + duration=duration, + error=str(e) + ) + + def download_with_threads(self, tasks: List[DownloadTask], max_workers: int = 4) -> List[DownloadResult]: + """使用线程池下载""" + print(f"\n=== 使用线程池下载 (工作线程数: {max_workers}) ===") + + start_time = time.time() + results = [] + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # 提交所有任务 + future_to_task = {executor.submit(self.simulate_download, task): task for task in tasks} + + # 收集结果 + for future in as_completed(future_to_task): + result = future.result() + results.append(result) + + # 显示进度 + completed = len(results) + total = len(tasks) + print(f"进度: {completed}/{total} ({completed/total*100:.1f}%)") + + total_time = time.time() - start_time + print(f"线程池下载完成,总耗时: {total_time:.2f}秒") + + return results + + def download_with_processes(self, tasks: List[DownloadTask], max_workers: int = None) -> List[DownloadResult]: + """使用进程池下载""" + if max_workers is None: + max_workers = multiprocessing.cpu_count() + + print(f"\n=== 使用进程池下载 (工作进程数: {max_workers}) ===") + + start_time = time.time() + results = [] + + with ProcessPoolExecutor(max_workers=max_workers) as executor: + # 提交所有任务 + future_to_task = {executor.submit(self.simulate_download, task): task for task in tasks} + + # 收集结果 + for future in as_completed(future_to_task): + result = future.result() + results.append(result) + + # 显示进度 + completed = len(results) + total = len(tasks) + print(f"进度: {completed}/{total} ({completed/total*100:.1f}%)") + + total_time = time.time() - start_time + print(f"进程池下载完成,总耗时: {total_time:.2f}秒") + + return results + + async def download_with_asyncio(self, tasks: List[DownloadTask], max_concurrent: int = 10) -> List[DownloadResult]: + """使用异步IO下载""" + print(f"\n=== 使用异步IO下载 (最大并发数: {max_concurrent}) ===") + + async def async_download(task: DownloadTask) -> DownloadResult: + """异步下载单个文件""" + start_time = time.time() + + try: + print(f"开始异步下载: {task.url} -> {task.filename}") + + # 模拟异步网络请求 + download_time = len(task.filename) * 0.1 + 1 + await asyncio.sleep(download_time) + + # 创建模拟文件 + file_path = self.download_dir / task.filename + content = f"异步下载的文件内容: {task.url}\n" * 100 + + # 异步写入文件(这里简化为同步写入) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + actual_size = len(content.encode('utf-8')) + duration = time.time() - start_time + + print(f"异步下载完成: {task.filename} ({actual_size} 字节, {duration:.2f}秒)") + + return DownloadResult( + task=task, + success=True, + duration=duration, + actual_size=actual_size + ) + + except Exception as e: + duration = time.time() - start_time + print(f"异步下载失败: {task.filename} - {str(e)}") + + return DownloadResult( + task=task, + success=False, + duration=duration, + error=str(e) + ) + + start_time = time.time() + + # 使用信号量限制并发数 + semaphore = asyncio.Semaphore(max_concurrent) + + async def download_with_semaphore(task): + async with semaphore: + return await async_download(task) + + # 创建所有任务 + download_tasks = [download_with_semaphore(task) for task in tasks] + + # 并发执行所有下载任务 + results = await asyncio.gather(*download_tasks) + + total_time = time.time() - start_time + print(f"异步下载完成,总耗时: {total_time:.2f}秒") + + return results + + def generate_report(self, results: List[DownloadResult]) -> dict: + """生成下载报告""" + successful = [r for r in results if r.success] + failed = [r for r in results if not r.success] + + total_size = sum(r.actual_size or 0 for r in successful) + total_time = sum(r.duration for r in results) + avg_speed = total_size / total_time if total_time > 0 else 0 + + report = { + 'total_tasks': len(results), + 'successful': len(successful), + 'failed': len(failed), + 'success_rate': len(successful) / len(results) * 100 if results else 0, + 'total_size_bytes': total_size, + 'total_time_seconds': total_time, + 'average_speed_bps': avg_speed, + 'failed_tasks': [{'filename': r.task.filename, 'error': r.error} for r in failed] + } + + return report + + def save_report(self, report: dict, filename: str = "download_report.json"): + """保存下载报告""" + report_path = self.download_dir / filename + with open(report_path, 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2, ensure_ascii=False) + print(f"下载报告已保存到: {report_path}") + +# 演示函数 +def demo_concurrent_downloader(): + """演示并发下载器""" + print("=== 并发下载器演示 ===") + + # 创建下载任务 + tasks = [ + DownloadTask("http://example.com/file1.txt", "file1.txt", 5000), + DownloadTask("http://example.com/file2.pdf", "file2.pdf", 8000), + DownloadTask("http://example.com/file3.jpg", "file3.jpg", 3000), + DownloadTask("http://example.com/file4.zip", "file4.zip", 12000), + DownloadTask("http://example.com/file5.doc", "file5.doc", 6000), + DownloadTask("http://example.com/file6.mp3", "file6.mp3", 9000), + ] + + downloader = ConcurrentDownloader() + + # 1. 线程池下载 + thread_results = downloader.download_with_threads(tasks, max_workers=3) + thread_report = downloader.generate_report(thread_results) + print(f"\n线程池下载报告:") + print(f"成功: {thread_report['successful']}/{thread_report['total_tasks']}") + print(f"成功率: {thread_report['success_rate']:.1f}%") + print(f"总耗时: {thread_report['total_time_seconds']:.2f}秒") + + # 2. 进程池下载 + process_results = downloader.download_with_processes(tasks, max_workers=2) + process_report = downloader.generate_report(process_results) + print(f"\n进程池下载报告:") + print(f"成功: {process_report['successful']}/{process_report['total_tasks']}") + print(f"成功率: {process_report['success_rate']:.1f}%") + print(f"总耗时: {process_report['total_time_seconds']:.2f}秒") + + # 3. 异步下载 + async def async_download_demo(): + async_results = await downloader.download_with_asyncio(tasks, max_concurrent=4) + async_report = downloader.generate_report(async_results) + print(f"\n异步下载报告:") + print(f"成功: {async_report['successful']}/{async_report['total_tasks']}") + print(f"成功率: {async_report['success_rate']:.1f}%") + print(f"总耗时: {async_report['total_time_seconds']:.2f}秒") + + # 保存报告 + downloader.save_report(async_report, "async_download_report.json") + + return async_report + + # 运行异步下载 + async_report = asyncio.run(async_download_demo()) + + # 4. 性能对比 + print(f"\n=== 性能对比 ===") + print(f"线程池总耗时: {thread_report['total_time_seconds']:.2f}秒") + print(f"进程池总耗时: {process_report['total_time_seconds']:.2f}秒") + print(f"异步IO总耗时: {async_report['total_time_seconds']:.2f}秒") + + # 清理下载的文件 + import shutil + if downloader.download_dir.exists(): + shutil.rmtree(downloader.download_dir) + print(f"\n已清理下载目录: {downloader.download_dir}") + +# 运行演示 +if __name__ == '__main__': + demo_concurrent_downloader() +``` + +--- + +## 六、总结与最佳实践 + +### 6.1 选择合适的并发方式 + +```python +def choose_concurrency_method(): + """选择合适的并发方式指南""" + print("=== 并发方式选择指南 ===") + + guidelines = { + "CPU密集型任务": { + "推荐方式": "多进程 (multiprocessing)", + "原因": "不受GIL限制,可以充分利用多核CPU", + "适用场景": [ + "数学计算", + "图像处理", + "数据分析", + "加密解密", + "科学计算" + ], + "示例代码": """ +from multiprocessing import Pool + +def cpu_task(n): + return sum(i*i for i in range(n)) + +with Pool() as pool: + results = pool.map(cpu_task, [100000, 200000, 300000]) + """ + }, + + "IO密集型任务": { + "推荐方式": "多线程 (threading) 或 异步IO (asyncio)", + "原因": "IO等待时会释放GIL,线程切换开销小", + "适用场景": [ + "文件读写", + "网络请求", + "数据库操作", + "API调用", + "爬虫" + ], + "示例代码": """ +# 线程方式 +from concurrent.futures import ThreadPoolExecutor + +def io_task(url): + # 模拟网络请求 + time.sleep(1) + return f"数据来自 {url}" + +with ThreadPoolExecutor(max_workers=5) as executor: + results = executor.map(io_task, urls) + +# 异步方式 +import asyncio + +async def async_io_task(url): + await asyncio.sleep(1) # 模拟异步IO + return f"数据来自 {url}" + +async def main(): + tasks = [async_io_task(url) for url in urls] + results = await asyncio.gather(*tasks) + """ + }, + + "混合型任务": { + "推荐方式": "根据主要瓶颈选择,或使用混合方案", + "原因": "需要根据具体情况分析", + "适用场景": [ + "Web服务器", + "数据处理管道", + "实时系统", + "游戏服务器" + ], + "示例代码": """ +# 混合方案:异步IO + 进程池 +import asyncio +from concurrent.futures import ProcessPoolExecutor + +async def hybrid_task(): + # IO密集型部分用异步 + data = await fetch_data_async() + + # CPU密集型部分用进程池 + with ProcessPoolExecutor() as executor: + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(executor, cpu_intensive_func, data) + + return result + """ + } + } + + for task_type, info in guidelines.items(): + print(f"\n{task_type}:") + print(f" 推荐方式: {info['推荐方式']}") + print(f" 原因: {info['原因']}") + print(f" 适用场景: {', '.join(info['适用场景'])}") + print(f" 示例代码:{info['示例代码']}") + +# 运行指南 +choose_concurrency_method() +``` + +### 6.2 最佳实践 + +```python +def best_practices(): + """并发编程最佳实践""" + print("=== 并发编程最佳实践 ===") + + practices = { + "1. 避免共享状态": { + "说明": "尽量避免多个线程/进程共享可变状态", + "建议": [ + "使用不可变数据结构", + "通过消息传递而非共享内存通信", + "使用函数式编程思想" + ] + }, + + "2. 正确使用锁": { + "说明": "必须共享状态时,正确使用同步原语", + "建议": [ + "使用with语句自动管理锁", + "避免死锁(按固定顺序获取锁)", + "尽量减少锁的持有时间", + "考虑使用无锁数据结构" + ] + }, + + "3. 异常处理": { + "说明": "并发程序中的异常处理更加重要", + "建议": [ + "为每个任务添加异常处理", + "使用日志记录异常信息", + "实现优雅的错误恢复机制", + "避免异常导致整个程序崩溃" + ] + }, + + "4. 资源管理": { + "说明": "正确管理线程、进程和其他资源", + "建议": [ + "使用上下文管理器", + "及时释放不需要的资源", + "设置合理的超时时间", + "监控资源使用情况" + ] + }, + + "5. 性能调优": { + "说明": "根据实际情况调优并发参数", + "建议": [ + "测试不同的工作线程/进程数量", + "监控CPU和内存使用率", + "使用性能分析工具", + "避免过度并发" + ] + } + } + + for practice, details in practices.items(): + print(f"\n{practice}") + print(f" {details['说明']}") + for suggestion in details['建议']: + print(f" • {suggestion}") + +# 运行最佳实践 +best_practices() +``` + +### 6.3 常见陷阱和解决方案 + +```python +def common_pitfalls(): + """常见陷阱和解决方案""" + print("=== 常见陷阱和解决方案 ===") + + pitfalls = { + "竞态条件 (Race Condition)": { + "问题": "多个线程同时访问共享资源导致不可预期的结果", + "解决方案": "使用锁、原子操作或无锁数据结构", + "示例": """ +# 错误示例 +counter = 0 +def increment(): + global counter + counter += 1 # 不是原子操作 + +# 正确示例 +import threading +counter = 0 +lock = threading.Lock() +def safe_increment(): + global counter + with lock: + counter += 1 + """ + }, + + "死锁 (Deadlock)": { + "问题": "多个线程相互等待对方释放资源", + "解决方案": "按固定顺序获取锁,使用超时,避免嵌套锁", + "示例": """ +# 可能死锁的代码 +lock1 = threading.Lock() +lock2 = threading.Lock() + +def task1(): + with lock1: + with lock2: # 可能死锁 + pass + +def task2(): + with lock2: + with lock1: # 可能死锁 + pass + +# 避免死锁 +def safe_task1(): + with lock1: + with lock2: # 固定顺序 + pass + +def safe_task2(): + with lock1: # 相同顺序 + with lock2: + pass + """ + }, + + "GIL限制": { + "问题": "Python的GIL限制了多线程的CPU密集型任务性能", + "解决方案": "对CPU密集型任务使用多进程", + "示例": """ +# CPU密集型任务应该使用多进程 +from multiprocessing import Pool + +def cpu_task(n): + return sum(i*i for i in range(n)) + +# 使用进程池而不是线程池 +with Pool() as pool: + results = pool.map(cpu_task, [100000, 200000]) + """ + }, + + "内存泄漏": { + "问题": "线程或进程没有正确清理导致内存泄漏", + "解决方案": "使用上下文管理器,及时join线程/进程", + "示例": """ +# 错误示例 +thread = threading.Thread(target=worker) +thread.start() +# 忘记join,可能导致资源泄漏 + +# 正确示例 +with ThreadPoolExecutor() as executor: + future = executor.submit(worker) + result = future.result() # 自动清理 + """ + } + } + + for pitfall, details in pitfalls.items(): + print(f"\n{pitfall}:") + print(f" 问题: {details['问题']}") + print(f" 解决方案: {details['解决方案']}") + print(f" 示例:{details['示例']}") + +# 运行陷阱说明 +common_pitfalls() +``` + +--- + +## 七、下一步学习 + +### 7.1 进阶主题 + +- **高级异步编程**:深入学习asyncio、aiohttp、aiofiles等异步库 +- **分布式计算**:学习Celery、Dask等分布式计算框架 +- **并发数据结构**:学习无锁数据结构和并发安全的容器 +- **性能分析**:使用cProfile、line_profiler等工具分析并发程序性能 +- **网络编程**:学习socket编程、网络协议和服务器架构 + +### 7.2 实践项目建议 + +1. **Web爬虫系统**:结合多线程和异步IO实现高效爬虫 +2. **文件处理工具**:批量处理大量文件的并发程序 +3. **API服务器**:使用FastAPI或aiohttp构建高并发API服务 +4. **数据处理管道**:实现ETL流程的并发数据处理系统 +5. **实时监控系统**:监控系统资源和应用状态的并发程序 + +### 7.3 推荐资源 + +- **官方文档**:Python threading、multiprocessing、asyncio模块文档 +- **书籍推荐**:《Python并发编程》、《流畅的Python》 +- **在线资源**:Real Python的并发编程教程 +- **实践平台**:GitHub上的开源并发项目 + +--- + +通过本章的学习,你应该已经掌握了Python中进程和线程的基本概念、使用方法和最佳实践。记住,并发编程需要大量的实践来掌握,建议多写代码、多做实验,逐步提高并发编程的技能。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/14.md b/docs/Python/14.md new file mode 100644 index 000000000..b1399a291 --- /dev/null +++ b/docs/Python/14.md @@ -0,0 +1,1120 @@ +--- +title: 第14天-正则表达式 +author: 哪吒 +date: '2023-06-15' +--- + +# 第14天-正则表达式 + +## 学习目标 + +通过本章学习,你将掌握: +- 理解正则表达式的基本概念和语法 +- 掌握Python中re模块的使用 +- 学会编写常用的正则表达式模式 +- 掌握正则表达式的匹配、搜索、替换和分割操作 +- 理解正则表达式的高级特性(分组、前瞻、后顾等) +- 学会在实际项目中应用正则表达式 +- 掌握正则表达式的性能优化技巧 + +--- + +## 一、正则表达式基础 + +### 1.1 什么是正则表达式 + +```python +import re + +def regex_introduction(): + """正则表达式介绍""" + print("=== 正则表达式介绍 ===") + + # 正则表达式的定义 + print("\n1. 正则表达式的定义") + print(""" + 正则表达式(Regular Expression,简称regex或regexp)是一种强大的文本处理工具, + 用于描述字符串的模式。它可以用来: + + • 验证输入格式(如邮箱、电话号码) + • 搜索和提取特定内容 + • 替换文本 + • 分割字符串 + • 数据清洗和预处理 + """) + + # 简单示例 + print("\n2. 简单示例") + + # 示例1:查找数字 + text = "我有3个苹果和5个橙子" + numbers = re.findall(r'\d+', text) + print(f"文本: {text}") + print(f"找到的数字: {numbers}") + + # 示例2:验证邮箱格式 + emails = [ + "user@example.com", + "invalid-email", + "test.email@domain.org", + "@invalid.com" + ] + + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + + print("\n邮箱验证:") + for email in emails: + is_valid = bool(re.match(email_pattern, email)) + print(f"{email}: {'有效' if is_valid else '无效'}") + + # 示例3:提取URL + text = "访问我们的网站 https://www.example.com 或 http://blog.test.org" + url_pattern = r'https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' + urls = re.findall(url_pattern, text) + print(f"\n文本: {text}") + print(f"找到的URL: {urls}") + + # 示例4:替换敏感词 + text = "这个产品很垃圾,完全是骗子公司" + sensitive_words = ["垃圾", "骗子"] + + for word in sensitive_words: + text = re.sub(word, "*" * len(word), text) + + print(f"\n过滤后的文本: {text}") + +# 运行介绍 +regex_introduction() +``` + +### 1.2 基本语法和元字符 + +```python +def regex_basic_syntax(): + """正则表达式基本语法""" + print("=== 正则表达式基本语法 ===") + + # 1. 字面字符 + print("\n1. 字面字符") + text = "Hello World" + pattern = "Hello" + match = re.search(pattern, text) + print(f"文本: {text}") + print(f"模式: {pattern}") + print(f"匹配结果: {match.group() if match else '未匹配'}") + + # 2. 元字符 + print("\n2. 元字符") + + metacharacters = { + ".": "匹配任意字符(除换行符)", + "^": "匹配字符串开始", + "$": "匹配字符串结束", + "*": "匹配前面的字符0次或多次", + "+": "匹配前面的字符1次或多次", + "?": "匹配前面的字符0次或1次", + "|": "或操作符", + "[]": "字符类,匹配方括号内的任意字符", + "()": "分组", + "{}": "指定匹配次数", + "\\": "转义字符" + } + + for char, description in metacharacters.items(): + print(f"{char:3} - {description}") + + # 3. 元字符示例 + print("\n3. 元字符示例") + + examples = [ + ("a.c", "abc", "匹配a和c之间有任意字符"), + ("^Hello", "Hello World", "匹配以Hello开头的字符串"), + ("World$", "Hello World", "匹配以World结尾的字符串"), + ("ab*c", "ac", "匹配a后面跟0个或多个b,然后是c"), + ("ab+c", "abc", "匹配a后面跟1个或多个b,然后是c"), + ("ab?c", "ac", "匹配a后面跟0个或1个b,然后是c"), + ("cat|dog", "I have a cat", "匹配cat或dog"), + ("[aeiou]", "hello", "匹配任意元音字母"), + ("[0-9]", "abc123", "匹配任意数字"), + ("[^0-9]", "abc123", "匹配任意非数字字符") + ] + + for pattern, text, description in examples: + match = re.search(pattern, text) + result = match.group() if match else "未匹配" + print(f"模式: {pattern:10} 文本: {text:15} 结果: {result:10} - {description}") + + # 4. 预定义字符类 + print("\n4. 预定义字符类") + + predefined_classes = { + r"\d": "匹配数字 [0-9]", + r"\D": "匹配非数字 [^0-9]", + r"\w": "匹配单词字符 [a-zA-Z0-9_]", + r"\W": "匹配非单词字符 [^a-zA-Z0-9_]", + r"\s": "匹配空白字符(空格、制表符、换行符等)", + r"\S": "匹配非空白字符", + r"\b": "匹配单词边界", + r"\B": "匹配非单词边界" + } + + for char_class, description in predefined_classes.items(): + print(f"{char_class:3} - {description}") + + # 5. 预定义字符类示例 + print("\n5. 预定义字符类示例") + + text = "Hello123 World_456!" + + class_examples = [ + (r"\d+", "匹配连续数字"), + (r"\w+", "匹配单词字符"), + (r"\s+", "匹配空白字符"), + (r"\b\w+\b", "匹配完整单词") + ] + + for pattern, description in class_examples: + matches = re.findall(pattern, text) + print(f"模式: {pattern:10} 匹配: {matches} - {description}") + +# 运行基本语法演示 +regex_basic_syntax() +``` + +### 1.3 量词和重复 + +```python +def regex_quantifiers(): + """正则表达式量词""" + print("=== 正则表达式量词 ===") + + # 1. 基本量词 + print("\n1. 基本量词") + + quantifiers = { + "*": "0次或多次(贪婪)", + "+": "1次或多次(贪婪)", + "?": "0次或1次(贪婪)", + "{n}": "恰好n次", + "{n,}": "至少n次", + "{n,m}": "n到m次", + "*?": "0次或多次(非贪婪)", + "+?": "1次或多次(非贪婪)", + "??": "0次或1次(非贪婪)" + } + + for quantifier, description in quantifiers.items(): + print(f"{quantifier:6} - {description}") + + # 2. 量词示例 + print("\n2. 量词示例") + + text = "aaabbbcccc" + + quantifier_examples = [ + ("a*", "匹配0个或多个a"), + ("a+", "匹配1个或多个a"), + ("a?", "匹配0个或1个a"), + ("a{3}", "匹配恰好3个a"), + ("b{2,}", "匹配至少2个b"), + ("c{2,4}", "匹配2到4个c") + ] + + for pattern, description in quantifier_examples: + match = re.search(pattern, text) + result = match.group() if match else "未匹配" + print(f"模式: {pattern:8} 结果: {result:8} - {description}") + + # 3. 贪婪vs非贪婪 + print("\n3. 贪婪vs非贪婪") + + html_text = "
内容1
内容2
" + + # 贪婪匹配 + greedy_pattern = r"
.*
" + greedy_match = re.search(greedy_pattern, html_text) + print(f"贪婪匹配: {greedy_match.group() if greedy_match else '未匹配'}") + + # 非贪婪匹配 + non_greedy_pattern = r"
.*?
" + non_greedy_matches = re.findall(non_greedy_pattern, html_text) + print(f"非贪婪匹配: {non_greedy_matches}") + + # 4. 实际应用示例 + print("\n4. 实际应用示例") + + # 提取HTML标签内容 + html = "

标题

段落内容

链接" + + # 提取所有标签内容 + tag_content_pattern = r"<[^>]+>(.*?)]+>" + contents = re.findall(tag_content_pattern, html) + print(f"HTML内容: {contents}") + + # 提取特定标签 + h1_pattern = r"

(.*?)

" + h1_content = re.search(h1_pattern, html) + print(f"H1内容: {h1_content.group(1) if h1_content else '未找到'}") + + # 验证密码强度 + passwords = [ + "123456", + "password", + "Password123", + "P@ssw0rd123", + "Weak" + ] + + # 密码要求:至少8位,包含大小写字母、数字和特殊字符 + strong_password_pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$" + + print("\n密码强度验证:") + for password in passwords: + is_strong = bool(re.match(strong_password_pattern, password)) + print(f"{password:12} - {'强密码' if is_strong else '弱密码'}") + +# 运行量词演示 +regex_quantifiers() +``` + +--- + +## 二、Python re模块详解 + +### 2.1 re模块基本函数 + +```python +import re + +def re_module_basics(): + """re模块基本函数""" + print("=== re模块基本函数 ===") + + text = "Python是一种编程语言,Python很强大。联系方式:email@example.com,电话:138-1234-5678" + + # 1. re.match() - 从字符串开头匹配 + print("\n1. re.match() - 从字符串开头匹配") + + match_result = re.match(r"Python", text) + print(f"匹配结果: {match_result.group() if match_result else '未匹配'}") + + # 不从开头匹配的情况 + match_result2 = re.match(r"编程", text) + print(f"匹配'编程': {match_result2.group() if match_result2 else '未匹配'}") + + # 2. re.search() - 搜索整个字符串 + print("\n2. re.search() - 搜索整个字符串") + + search_result = re.search(r"编程", text) + print(f"搜索'编程': {search_result.group() if search_result else '未找到'}") + + # 获取匹配位置 + if search_result: + print(f"匹配位置: {search_result.span()}") + print(f"开始位置: {search_result.start()}") + print(f"结束位置: {search_result.end()}") + + # 3. re.findall() - 找到所有匹配 + print("\n3. re.findall() - 找到所有匹配") + + # 找到所有"Python" + python_matches = re.findall(r"Python", text) + print(f"所有'Python': {python_matches}") + + # 找到所有数字 + numbers = re.findall(r"\d+", text) + print(f"所有数字: {numbers}") + + # 找到邮箱 + emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text) + print(f"邮箱地址: {emails}") + + # 4. re.finditer() - 返回匹配对象的迭代器 + print("\n4. re.finditer() - 返回匹配对象的迭代器") + + for match in re.finditer(r"\d+", text): + print(f"数字: {match.group()}, 位置: {match.span()}") + + # 5. re.sub() - 替换 + print("\n5. re.sub() - 替换") + + # 简单替换 + new_text = re.sub(r"Python", "Java", text) + print(f"替换后: {new_text}") + + # 使用函数进行替换 + def upper_replace(match): + return match.group().upper() + + upper_text = re.sub(r"python", upper_replace, text, flags=re.IGNORECASE) + print(f"大写替换: {upper_text}") + + # 限制替换次数 + limited_replace = re.sub(r"Python", "Java", text, count=1) + print(f"限制替换: {limited_replace}") + + # 6. re.split() - 分割 + print("\n6. re.split() - 分割") + + # 按标点符号分割 + parts = re.split(r"[,。:]", text) + print(f"分割结果: {[part.strip() for part in parts if part.strip()]}") + + # 保留分隔符 + parts_with_sep = re.split(r"([,。:])", text) + print(f"保留分隔符: {[part for part in parts_with_sep if part]}") + + # 7. re.compile() - 编译正则表达式 + print("\n7. re.compile() - 编译正则表达式") + + # 编译常用模式 + email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") + phone_pattern = re.compile(r"\d{3}-\d{4}-\d{4}") + + # 使用编译后的模式 + email_matches = email_pattern.findall(text) + phone_matches = phone_pattern.findall(text) + + print(f"邮箱匹配: {email_matches}") + print(f"电话匹配: {phone_matches}") + + # 8. 标志参数 + print("\n8. 标志参数") + + flags_demo_text = "Hello WORLD\nPython Programming" + + # re.IGNORECASE - 忽略大小写 + ignore_case = re.findall(r"hello", flags_demo_text, re.IGNORECASE) + print(f"忽略大小写: {ignore_case}") + + # re.MULTILINE - 多行模式 + multiline = re.findall(r"^\w+", flags_demo_text, re.MULTILINE) + print(f"多行模式: {multiline}") + + # re.DOTALL - 点号匹配换行符 + dotall = re.findall(r"Hello.*Programming", flags_demo_text, re.DOTALL) + print(f"点号匹配换行: {dotall}") + + # 组合标志 + combined = re.findall(r"hello.*python", flags_demo_text, re.IGNORECASE | re.DOTALL) + print(f"组合标志: {combined}") + +# 运行re模块基础演示 +re_module_basics() +``` + +### 2.2 分组和捕获 + +```python +def regex_groups(): + """正则表达式分组""" + print("=== 正则表达式分组 ===") + + # 1. 基本分组 + print("\n1. 基本分组") + + text = "姓名:张三,年龄:25,电话:138-1234-5678" + + # 使用分组提取信息 + pattern = r"姓名:(\w+),年龄:(\d+),电话:([\d-]+)" + match = re.search(pattern, text) + + if match: + print(f"完整匹配: {match.group(0)}") + print(f"姓名: {match.group(1)}") + print(f"年龄: {match.group(2)}") + print(f"电话: {match.group(3)}") + print(f"所有分组: {match.groups()}") + + # 2. 命名分组 + print("\n2. 命名分组") + + # 使用命名分组 + named_pattern = r"姓名:(?P\w+),年龄:(?P\d+),电话:(?P[\d-]+)" + named_match = re.search(named_pattern, text) + + if named_match: + print(f"姓名: {named_match.group('name')}") + print(f"年龄: {named_match.group('age')}") + print(f"电话: {named_match.group('phone')}") + print(f"分组字典: {named_match.groupdict()}") + + # 3. 非捕获分组 + print("\n3. 非捕获分组") + + # 普通分组 + normal_pattern = r"(https?)://(\w+\.\w+)" + # 非捕获分组 + non_capture_pattern = r"(?:https?)://(\w+\.\w+)" + + url = "https://www.example.com" + + normal_match = re.search(normal_pattern, url) + non_capture_match = re.search(non_capture_pattern, url) + + print(f"普通分组: {normal_match.groups() if normal_match else '未匹配'}") + print(f"非捕获分组: {non_capture_match.groups() if non_capture_match else '未匹配'}") + + # 4. 分组引用 + print("\n4. 分组引用") + + # 查找重复的单词 + text_with_duplicates = "这是是一个测试测试文本" + duplicate_pattern = r"(\w+)\1" + duplicates = re.findall(duplicate_pattern, text_with_duplicates) + print(f"重复的字符: {duplicates}") + + # 在替换中使用分组引用 + html_text = "粗体 斜体" + # 将HTML标签转换为Markdown + markdown_text = re.sub(r"(.*?)", r"**\1**", html_text) + markdown_text = re.sub(r"(.*?)", r"*\1*", markdown_text) + print(f"转换为Markdown: {markdown_text}") + + # 5. 条件分组 + print("\n5. 条件分组") + + # 匹配不同格式的日期 + dates = [ + "2023-12-25", + "2023/12/25", + "25-12-2023", + "25/12/2023" + ] + + # 使用选择操作符匹配多种格式 + date_pattern = r"(\d{4}[-/]\d{2}[-/]\d{2})|(\d{2}[-/]\d{2}[-/]\d{4})" + + for date in dates: + match = re.search(date_pattern, date) + if match: + if match.group(1): + print(f"{date} - 年-月-日格式") + elif match.group(2): + print(f"{date} - 日-月-年格式") + + # 6. 实际应用:解析日志 + print("\n6. 实际应用:解析日志") + + log_entries = [ + "2023-12-25 10:30:15 [INFO] 用户登录成功 - 用户ID: 12345", + "2023-12-25 10:31:22 [ERROR] 数据库连接失败 - 错误代码: 500", + "2023-12-25 10:32:10 [WARNING] 内存使用率过高 - 使用率: 85%" + ] + + log_pattern = r"(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(?P\w+)\] (?P.*?) - (?P
.*)" + + for log in log_entries: + match = re.search(log_pattern, log) + if match: + log_data = match.groupdict() + print(f"时间: {log_data['timestamp']}") + print(f"级别: {log_data['level']}") + print(f"消息: {log_data['message']}") + print(f"详情: {log_data['details']}") + print("-" * 40) + +# 运行分组演示 +regex_groups() +``` + +### 2.3 前瞻和后顾断言 + +```python +def regex_lookahead_lookbehind(): + """前瞻和后顾断言""" + print("=== 前瞻和后顾断言 ===") + + # 1. 正向前瞻 (?=...) + print("\n1. 正向前瞻 (?=...)") + + text = "password123 username456 email789" + + # 匹配后面跟着数字的单词 + positive_lookahead = re.findall(r"\w+(?=\d+)", text) + print(f"后面跟数字的单词: {positive_lookahead}") + + # 2. 负向前瞻 (?!...) + print("\n2. 负向前瞻 (?!...)") + + # 匹配后面不跟数字的单词 + negative_lookahead = re.findall(r"\w+(?!\d+)", text) + print(f"后面不跟数字的单词: {negative_lookahead}") + + # 3. 正向后顾 (?<=...) + print("\n3. 正向后顾 (?<=...)") + + # 匹配前面有字母的数字 + positive_lookbehind = re.findall(r"(?<=\w)\d+", text) + print(f"前面有字母的数字: {positive_lookbehind}") + + # 4. 负向后顾 (? 链接' + + # 提取src属性值 + src_values = re.findall(r'(?<=src=")[^"]*(?=")', html) + print(f"src属性值: {src_values}") + + # 提取href属性值 + href_values = re.findall(r'(?<=href=")[^"]*(?=")', html) + print(f"href属性值: {href_values}") + + # 7. 复杂的验证示例 + print("\n7. 复杂的验证示例") + + # 验证中国手机号码 + phone_numbers = [ + "13812345678", + "15987654321", + "12345678901", + "1381234567", + "138123456789" + ] + + # 中国手机号规则:1开头,第二位是3-9,总共11位 + china_mobile_pattern = r"^1[3-9]\d{9}$" + + print("中国手机号验证:") + for phone in phone_numbers: + is_valid = bool(re.match(china_mobile_pattern, phone)) + print(f"{phone:12} - {'有效' if is_valid else '无效'}") + + # 8. 提取嵌套结构 + print("\n8. 提取嵌套结构") + + # 提取函数调用 + code = "print('Hello') + len('World') + max(1, 2, 3)" + + # 匹配函数名(后面跟着括号) + function_names = re.findall(r"\w+(?=\()", code) + print(f"函数名: {function_names}") + + # 匹配括号内的内容 + parentheses_content = re.findall(r"(?<=\()[^)]*(?=\))", code) + print(f"括号内容: {parentheses_content}") + +# 运行前瞻后顾演示 +regex_lookahead_lookbehind() +``` + +--- + +## 三、常用正则表达式模式 + +### 3.1 数据验证模式 + +```python +def common_validation_patterns(): + """常用数据验证模式""" + print("=== 常用数据验证模式 ===") + + # 1. 邮箱验证 + print("\n1. 邮箱验证") + + emails = [ + "user@example.com", + "test.email@domain.org", + "invalid-email", + "user@", + "@domain.com", + "user.name+tag@example.co.uk" + ] + + # 简单邮箱模式 + simple_email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + + # 更严格的邮箱模式 + strict_email_pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$" + + print("邮箱验证结果:") + for email in emails: + simple_valid = bool(re.match(simple_email_pattern, email)) + strict_valid = bool(re.match(strict_email_pattern, email)) + print(f"{email:25} 简单: {'✓' if simple_valid else '✗'} 严格: {'✓' if strict_valid else '✗'}") + + # 2. 手机号验证 + print("\n2. 手机号验证") + + phone_numbers = [ + "13812345678", + "138-1234-5678", + "138 1234 5678", + "+86 138 1234 5678", + "12345678901", + "1381234567" + ] + + # 中国手机号模式 + china_mobile_patterns = { + "基本格式": r"^1[3-9]\d{9}$", + "带连字符": r"^1[3-9]\d-\d{4}-\d{4}$", + "带空格": r"^1[3-9]\d \d{4} \d{4}$", + "国际格式": r"^\+86 1[3-9]\d \d{4} \d{4}$" + } + + print("手机号验证结果:") + for phone in phone_numbers: + print(f"{phone:20}", end=" ") + for pattern_name, pattern in china_mobile_patterns.items(): + is_valid = bool(re.match(pattern, phone)) + print(f"{pattern_name}: {'✓' if is_valid else '✗'}", end=" ") + print() + + # 3. 身份证号验证 + print("\n3. 身份证号验证") + + id_numbers = [ + "110101199001011234", + "11010119900101123X", + "110101199013011234", # 无效月份 + "110101199001321234", # 无效日期 + "12345678901234567", # 长度不对 + ] + + # 身份证号模式(简化版) + id_pattern = r"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$" + + print("身份证号验证:") + for id_num in id_numbers: + is_valid = bool(re.match(id_pattern, id_num)) + print(f"{id_num:20} - {'有效' if is_valid else '无效'}") + + # 4. URL验证 + print("\n4. URL验证") + + urls = [ + "https://www.example.com", + "http://example.com/path?param=value", + "ftp://files.example.com", + "www.example.com", + "invalid-url", + "https://sub.domain.example.com:8080/path" + ] + + # URL模式 + url_pattern = r"^(https?|ftp)://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:[0-9]+)?(/.*)?$" + + print("URL验证:") + for url in urls: + is_valid = bool(re.match(url_pattern, url)) + print(f"{url:40} - {'有效' if is_valid else '无效'}") + + # 5. IP地址验证 + print("\n5. IP地址验证") + + ip_addresses = [ + "192.168.1.1", + "255.255.255.255", + "0.0.0.0", + "256.1.1.1", + "192.168.1", + "192.168.1.1.1" + ] + + # IPv4地址模式 + ipv4_pattern = r"^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$" + + print("IPv4地址验证:") + for ip in ip_addresses: + is_valid = bool(re.match(ipv4_pattern, ip)) + print(f"{ip:15} - {'有效' if is_valid else '无效'}") + + # 6. 密码强度验证 + print("\n6. 密码强度验证") + + passwords = [ + "123456", + "password", + "Password123", + "P@ssw0rd", + "MySecureP@ss123", + "weak" + ] + + # 不同强度的密码模式 + password_patterns = { + "弱密码": r"^.{6,}$", # 至少6位 + "中等密码": r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$", # 8位,包含大小写和数字 + "强密码": r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$" # 8位,包含大小写、数字和特殊字符 + } + + print("密码强度验证:") + for password in passwords: + print(f"{password:15}", end=" ") + for strength, pattern in password_patterns.items(): + is_valid = bool(re.match(pattern, password)) + if is_valid: + print(f"- {strength}") + break + else: + print("- 太弱") + +# 运行验证模式演示 +common_validation_patterns() +``` + +### 3.2 文本提取模式 + +```python +def text_extraction_patterns(): + """文本提取模式""" + print("=== 文本提取模式 ===") + + # 1. 提取HTML标签和内容 + print("\n1. 提取HTML标签和内容") + + html_content = """ + + 网页标题 + +

主标题

+

这是一个段落。

+ 链接文本 + 图片描述 + + + """ + + # 提取所有标签 + all_tags = re.findall(r"<[^>]+>", html_content) + print(f"所有标签: {all_tags[:5]}...") # 只显示前5个 + + # 提取标签内容 + tag_contents = re.findall(r"<(\w+)[^>]*>(.*?)", html_content, re.DOTALL) + print("标签内容:") + for tag, content in tag_contents: + clean_content = content.strip() + if clean_content and not clean_content.startswith('<'): + print(f" {tag}: {clean_content}") + + # 提取链接 + links = re.findall(r']+href="([^"]+)"[^>]*>(.*?)', html_content) + print(f"链接: {links}") + + # 提取图片信息 + images = re.findall(r']+src="([^"]+)"[^>]*alt="([^"]+)"[^>]*>', html_content) + print(f"图片: {images}") + + # 2. 提取日期和时间 + print("\n2. 提取日期和时间") + + text_with_dates = """ + 会议安排: + - 2023年12月25日 上午10:00 圣诞节庆祝 + - 2023-12-31 23:59 新年倒计时 + - 12/25/2023 下午2:30 PM 项目讨论 + - 25/12/2023 晚上8点 聚餐 + """ + + # 不同格式的日期模式 + date_patterns = { + "中文日期": r"\d{4}年\d{1,2}月\d{1,2}日", + "ISO日期": r"\d{4}-\d{2}-\d{2}", + "美式日期": r"\d{1,2}/\d{1,2}/\d{4}", + "欧式日期": r"\d{1,2}/\d{1,2}/\d{4}" + } + + # 时间模式 + time_patterns = { + "24小时制": r"\d{1,2}:\d{2}", + "12小时制": r"\d{1,2}:\d{2}\s*[AP]M", + "中文时间": r"[上下]午\d{1,2}[::]\d{2}|晚上\d{1,2}点" + } + + print("提取的日期:") + for pattern_name, pattern in date_patterns.items(): + dates = re.findall(pattern, text_with_dates) + if dates: + print(f" {pattern_name}: {dates}") + + print("提取的时间:") + for pattern_name, pattern in time_patterns.items(): + times = re.findall(pattern, text_with_dates) + if times: + print(f" {pattern_name}: {times}") + + # 3. 提取货币和数字 + print("\n3. 提取货币和数字") + + financial_text = """ + 商品价格: + - iPhone 15: ¥7999 + - MacBook Pro: $2,399.00 + - 咖啡: 25.50元 + - 汽车: €45,000 + - 房价: 1,200,000 RMB + """ + + # 货币模式 + currency_patterns = { + "人民币符号": r"¥[\d,]+(?:\.\d{2})?", + "美元": r"\$[\d,]+(?:\.\d{2})?", + "欧元": r"€[\d,]+(?:\.\d{2})?", + "人民币文字": r"[\d,]+(?:\.\d{2})?元", + "RMB": r"[\d,]+(?:\.\d{2})?\s*RMB" + } + + print("提取的货币:") + for currency_name, pattern in currency_patterns.items(): + amounts = re.findall(pattern, financial_text) + if amounts: + print(f" {currency_name}: {amounts}") + + # 4. 提取联系信息 + print("\n4. 提取联系信息") + + contact_text = """ + 联系方式: + 电话:138-1234-5678, 010-12345678 + 邮箱:contact@example.com, support@company.org + QQ:123456789 + 微信:wechat_id_123 + 地址:北京市朝阳区某某街道123号 + """ + + # 联系信息模式 + contact_patterns = { + "手机号": r"1[3-9]\d-\d{4}-\d{4}", + "固定电话": r"\d{3,4}-\d{7,8}", + "邮箱": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", + "QQ号": r"QQ[::]\s*(\d{5,12})", + "微信号": r"微信[::]\s*([a-zA-Z0-9_-]+)" + } + + print("提取的联系信息:") + for info_type, pattern in contact_patterns.items(): + contacts = re.findall(pattern, contact_text) + if contacts: + print(f" {info_type}: {contacts}") + + # 5. 提取代码片段 + print("\n5. 提取代码片段") + + code_text = """ + Python代码示例: + ```python + def hello_world(): + print("Hello, World!") + ``` + + JavaScript代码: + ```javascript + function greet(name) { + console.log(`Hello, ${name}!`); + } + ``` + + 内联代码:使用 `print()` 函数输出内容。 + """ + + # 代码块模式 + code_block_pattern = r"```(\w+)\n(.*?)```" + code_blocks = re.findall(code_block_pattern, code_text, re.DOTALL) + + print("代码块:") + for language, code in code_blocks: + print(f" 语言: {language}") + print(f" 代码: {code.strip()[:50]}...") # 只显示前50个字符 + + # 内联代码模式 + inline_code_pattern = r"`([^`]+)`" + inline_codes = re.findall(inline_code_pattern, code_text) + print(f"内联代码: {inline_codes}") + +# 运行文本提取演示 +text_extraction_patterns() +``` + +--- + +## 四、正则表达式实战应用 + +### 4.1 日志分析器 + +```python +import re +from datetime import datetime +from collections import defaultdict, Counter +from typing import Dict, List, Tuple + +class LogAnalyzer: + """日志分析器""" + + def __init__(self): + # 常见日志格式的正则表达式 + self.log_patterns = { + 'apache_common': r'(?P\d+\.\d+\.\d+\.\d+) - - \[(?P[^\]]+)\] "(?P\w+) (?P[^"]+) HTTP/[^"]+" (?P\d+) (?P\d+)', + 'apache_combined': r'(?P\d+\.\d+\.\d+\.\d+) - - \[(?P[^\]]+)\] "(?P\w+) (?P[^"]+) HTTP/[^"]+" (?P\d+) (?P\d+) "(?P[^"]*)" "(?P[^"]*)"', + 'nginx': r'(?P\d+\.\d+\.\d+\.\d+) - - \[(?P[^\]]+)\] "(?P\w+) (?P[^"]+) HTTP/[^"]+" (?P\d+) (?P\d+) "(?P[^"]*)" "(?P[^"]*)"', + 'python_logging': r'(?P\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?P\w+) - (?P\w+) - (?P.*)', + 'syslog': r'(?P\w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2}) (?P\w+) (?P\w+)\[(?P\d+)\]: (?P.*)' + } + + self.compiled_patterns = {name: re.compile(pattern) for name, pattern in self.log_patterns.items()} + + def parse_log_line(self, line: str, log_format: str = 'auto') -> Dict: + """解析单行日志""" + line = line.strip() + if not line: + return None + + if log_format == 'auto': + # 自动检测日志格式 + for format_name, pattern in self.compiled_patterns.items(): + match = pattern.match(line) + if match: + result = match.groupdict() + result['format'] = format_name + return result + return {'raw': line, 'format': 'unknown'} + else: + pattern = self.compiled_patterns.get(log_format) + if pattern: + match = pattern.match(line) + if match: + result = match.groupdict() + result['format'] = log_format + return result + + return None + + def analyze_logs(self, log_lines: List[str]) -> Dict: + """分析日志""" + stats = { + 'total_lines': 0, + 'parsed_lines': 0, + 'formats': Counter(), + 'status_codes': Counter(), + 'methods': Counter(), + 'ips': Counter(), + 'user_agents': Counter(), + 'error_logs': [], + 'top_urls': Counter(), + 'hourly_traffic': defaultdict(int) + } + + for line in log_lines: + stats['total_lines'] += 1 + parsed = self.parse_log_line(line) + + if parsed: + stats['parsed_lines'] += 1 + stats['formats'][parsed.get('format', 'unknown')] += 1 + + # 分析Web服务器日志 + if 'status' in parsed: + stats['status_codes'][parsed['status']] += 1 + + if 'method' in parsed: + stats['methods'][parsed['method']] += 1 + + if 'ip' in parsed: + stats['ips'][parsed['ip']] += 1 + + if 'user_agent' in parsed: + stats['user_agents'][parsed['user_agent']] += 1 + + if 'url' in parsed: + stats['top_urls'][parsed['url']] += 1 + + # 分析时间分布 + if 'timestamp' in parsed: + try: + # 尝试解析时间戳 + timestamp = parsed['timestamp'] + if '/' in timestamp: # Apache格式 + dt = datetime.strptime(timestamp.split()[0], '%d/%b/%Y:%H:%M:%S') + else: # Python logging格式 + dt = datetime.strptime(timestamp.split(',')[0], '%Y-%m-%d %H:%M:%S') + + hour_key = dt.strftime('%Y-%m-%d %H:00') + stats['hourly_traffic'][hour_key] += 1 + except: + pass + + # 收集错误日志 + if ('status' in parsed and parsed['status'].startswith(('4', '5'))) or \ + ('level' in parsed and parsed['level'] in ['ERROR', 'CRITICAL']): + stats['error_logs'].append(parsed) + + return stats + + def generate_report(self, stats: Dict) -> str: + """生成分析报告""" + report = [] + report.append("=== 日志分析报告 ===") + report.append(f"总行数: {stats['total_lines']}") + report.append(f"成功解析: {stats['parsed_lines']} ({stats['parsed_lines']/stats['total_lines']*100:.1f}%)") + + # 日志格式分布 + report.append("\n日志格式分布:") + for format_name, count in stats['formats'].most_common(): + report.append(f" {format_name}: {count}") + + # HTTP状态码分布 + if stats['status_codes']: + report.append("\nHTTP状态码分布:") + for status, count in stats['status_codes'].most_common(10): + report.append(f" {status}: {count}") + + # 请求方法分布 + if stats['methods']: + report.append("\n请求方法分布:") + for method, count in stats['methods'].most_common(): + report.append(f" {method}: {count}") + + # 访问最多的IP + if stats['ips']: + report.append("\n访问最多的IP (Top 10):") + for ip, count in stats['ips'].most_common(10): + report.append(f" {ip}: {count}") + + # 访问最多的URL + if stats['top_urls']: + report.append("\n访问最多的URL (Top 10):") + for url, count in stats['top_urls'].most_common(10): + report.append(f" {url}: {count}") + + # 错误日志 + if stats['error_logs']: + report.append(f"\n错误日志 (显示前5条):") + for i, error in enumerate(stats['error_logs'][:5]): + report.append(f" {i+1}. {error.get('timestamp', 'N/A')} - {error.get('status', error.get('level', 'N/A'))} - {error.get('url', error.get('message', 'N/A'))}") + + return "\n".join(report) + + +``` diff --git a/docs/Python/15.md b/docs/Python/15.md new file mode 100644 index 000000000..f40268707 --- /dev/null +++ b/docs/Python/15.md @@ -0,0 +1,3988 @@ +--- +title: 第15天-常用内建模块 +author: 哪吒 +date: '2023-06-15' +--- + +# 第15天-常用内建模块 + +## 学习目标 + +通过本章学习,你将掌握: +- 理解Python内建模块的概念和作用 +- 掌握常用内建模块的使用方法 +- 学会使用os、sys、datetime、json等核心模块 +- 掌握collections、itertools、functools等高级模块 +- 学会使用random、math、statistics等数学模块 +- 理解模块的导入机制和最佳实践 +- 学会在实际项目中合理选择和使用内建模块 + +--- + +## 一、内建模块概述 + +### 1.1 什么是内建模块 + +```python +import sys +import os + +def builtin_modules_introduction(): + """内建模块介绍""" + print("=== Python内建模块介绍 ===") + + print(""" + 内建模块(Built-in Modules)是Python标准库的一部分, + 随Python解释器一起安装,无需额外安装即可使用。 + + 主要特点: + • 官方维护,稳定可靠 + • 性能优化,通常用C语言实现 + • 功能丰富,覆盖各种常见需求 + • 跨平台兼容 + • 文档完善 + + 常用分类: + • 系统交互:os, sys, platform + • 时间处理:datetime, time, calendar + • 数据处理:json, csv, pickle + • 数学计算:math, statistics, random + • 集合工具:collections, itertools + • 函数工具:functools, operator + • 文件处理:pathlib, glob, shutil + • 网络通信:urllib, http, socket + • 并发编程:threading, multiprocessing, asyncio + """) + + # 查看已加载的模块 + print("\n当前已加载的模块数量:", len(sys.modules)) + + # 查看内建模块列表(部分) + builtin_module_names = [ + 'os', 'sys', 'datetime', 'json', 'math', 'random', + 'collections', 'itertools', 'functools', 'pathlib' + ] + + print("\n常用内建模块:") + for module_name in builtin_module_names: + try: + module = __import__(module_name) + print(f" {module_name}: {module.__doc__.split('.')[0] if module.__doc__ else '系统模块'}") + except ImportError: + print(f" {module_name}: 模块未找到") + + # 模块导入方式 + print("\n模块导入方式:") + import_examples = [ + "import os # 导入整个模块", + "import os.path # 导入子模块", + "from os import getcwd # 导入特定函数", + "from os import * # 导入所有(不推荐)", + "import os as operating_sys # 使用别名", + "from os import getcwd as pwd # 函数别名" + ] + + for example in import_examples: + print(f" {example}") + +# 运行介绍 +builtin_modules_introduction() +``` + +### 1.2 模块导入机制 + +```python +import sys +import importlib +from types import ModuleType + +def module_import_mechanism(): + """模块导入机制详解""" + print("=== 模块导入机制详解 ===") + + # 1. 模块搜索路径 + print("\n1. 模块搜索路径:") + print("Python按以下顺序搜索模块:") + for i, path in enumerate(sys.path, 1): + print(f" {i}. {path}") + + # 2. 导入过程 + print("\n2. 模块导入过程:") + import_process = [ + "1. 检查sys.modules缓存", + "2. 在sys.path中搜索模块文件", + "3. 编译模块代码(如果需要)", + "4. 执行模块代码", + "5. 将模块对象添加到sys.modules", + "6. 将模块绑定到当前命名空间" + ] + + for step in import_process: + print(f" {step}") + + # 3. 动态导入 + print("\n3. 动态导入示例:") + + # 使用importlib动态导入 + module_name = 'math' + math_module = importlib.import_module(module_name) + print(f"动态导入{module_name}模块: {math_module}") + print(f"计算π的值: {math_module.pi}") + + # 使用__import__ + os_module = __import__('os') + print(f"使用__import__导入os: {os_module}") + + # 4. 模块重新加载 + print("\n4. 模块重新加载:") + print("注意:模块只会被导入一次,后续import会使用缓存") + + # 检查模块是否已加载 + if 'json' in sys.modules: + print("json模块已在缓存中") + # 重新加载模块 + json_module = importlib.reload(sys.modules['json']) + print("json模块已重新加载") + + # 5. 模块属性检查 + print("\n5. 模块属性检查:") + import json + + module_info = { + '模块名称': json.__name__, + '模块文件': getattr(json, '__file__', '内建模块'), + '模块文档': json.__doc__[:50] + '...' if json.__doc__ else 'None', + '模块版本': getattr(json, '__version__', '未知') + } + + for key, value in module_info.items(): + print(f" {key}: {value}") + + # 6. 查看模块内容 + print("\n6. json模块的主要函数:") + json_functions = [name for name in dir(json) if not name.startswith('_')] + print(f" 公共函数/属性: {json_functions}") + +# 运行导入机制演示 +module_import_mechanism() +``` + +--- + +## 二、系统交互模块 + +### 2.1 os模块 - 操作系统接口 + +```python +import os +import platform +from pathlib import Path + +def os_module_demo(): + """os模块演示""" + print("=== os模块演示 ===") + + # 1. 系统信息 + print("\n1. 系统信息:") + system_info = { + '操作系统': os.name, + '平台': platform.system(), + '架构': platform.architecture()[0], + '处理器': platform.processor(), + '主机名': platform.node(), + 'Python版本': platform.python_version() + } + + for key, value in system_info.items(): + print(f" {key}: {value}") + + # 2. 路径操作 + print("\n2. 路径操作:") + current_dir = os.getcwd() + print(f" 当前工作目录: {current_dir}") + + # 路径拼接 + file_path = os.path.join(current_dir, 'test', 'example.txt') + print(f" 拼接路径: {file_path}") + + # 路径分解 + dir_name = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + name, ext = os.path.splitext(base_name) + + print(f" 目录名: {dir_name}") + print(f" 文件名: {base_name}") + print(f" 文件名(无扩展名): {name}") + print(f" 扩展名: {ext}") + + # 路径判断 + path_checks = { + '是否存在': os.path.exists(current_dir), + '是否为文件': os.path.isfile(current_dir), + '是否为目录': os.path.isdir(current_dir), + '是否为绝对路径': os.path.isabs(current_dir) + } + + print(f"\n 路径检查 ({current_dir}):") + for check, result in path_checks.items(): + print(f" {check}: {result}") + + # 3. 目录操作 + print("\n3. 目录操作:") + + # 列出目录内容 + try: + files = os.listdir('.') + print(f" 当前目录文件数量: {len(files)}") + print(f" 前5个文件: {files[:5]}") + except PermissionError: + print(" 无权限访问目录") + + # 递归遍历目录 + print("\n 递归遍历目录结构:") + for root, dirs, files in os.walk('.'): + level = root.replace('.', '').count(os.sep) + indent = ' ' * 2 * level + print(f"{indent}{os.path.basename(root)}/") + subindent = ' ' * 2 * (level + 1) + for file in files[:3]: # 只显示前3个文件 + print(f"{subindent}{file}") + if len(files) > 3: + print(f"{subindent}... 还有{len(files) - 3}个文件") + if level >= 2: # 限制遍历深度 + break + + # 4. 环境变量 + print("\n4. 环境变量:") + + # 获取环境变量 + important_env_vars = ['PATH', 'HOME', 'USER', 'PYTHONPATH'] + for var in important_env_vars: + value = os.environ.get(var, '未设置') + if len(str(value)) > 50: + value = str(value)[:50] + '...' + print(f" {var}: {value}") + + # 设置环境变量 + os.environ['MY_APP_CONFIG'] = 'production' + print(f" 设置自定义环境变量: {os.environ.get('MY_APP_CONFIG')}") + + # 5. 进程操作 + print("\n5. 进程信息:") + process_info = { + '进程ID': os.getpid(), + '父进程ID': os.getppid() if hasattr(os, 'getppid') else '不支持', + '用户ID': os.getuid() if hasattr(os, 'getuid') else '不支持', + '组ID': os.getgid() if hasattr(os, 'getgid') else '不支持' + } + + for key, value in process_info.items(): + print(f" {key}: {value}") + +# 运行os模块演示 +os_module_demo() +``` + +### 2.2 sys模块 - 系统特定参数 + +```python +import sys +import gc + +def sys_module_demo(): + """sys模块演示""" + print("=== sys模块演示 ===") + + # 1. Python解释器信息 + print("\n1. Python解释器信息:") + interpreter_info = { + 'Python版本': sys.version, + '版本信息': sys.version_info, + '平台': sys.platform, + '可执行文件路径': sys.executable, + '字节序': sys.byteorder, + '默认编码': sys.getdefaultencoding(), + '文件系统编码': sys.getfilesystemencoding() + } + + for key, value in interpreter_info.items(): + if isinstance(value, str) and len(value) > 60: + value = value[:60] + '...' + print(f" {key}: {value}") + + # 2. 命令行参数 + print("\n2. 命令行参数:") + print(f" 脚本名称: {sys.argv[0] if sys.argv else 'None'}") + print(f" 参数列表: {sys.argv}") + print(f" 参数数量: {len(sys.argv)}") + + # 3. 模块路径 + print("\n3. 模块搜索路径:") + print(f" 路径数量: {len(sys.path)}") + for i, path in enumerate(sys.path[:5], 1): + print(f" {i}. {path}") + if len(sys.path) > 5: + print(f" ... 还有{len(sys.path) - 5}个路径") + + # 4. 内存和性能信息 + print("\n4. 内存和性能信息:") + + # 引用计数 + test_list = [1, 2, 3] + print(f" test_list的引用计数: {sys.getrefcount(test_list)}") + + # 对象大小 + objects_size = { + '整数1': sys.getsizeof(1), + '字符串"hello"': sys.getsizeof("hello"), + '列表[1,2,3]': sys.getsizeof([1, 2, 3]), + '字典{"a":1}': sys.getsizeof({"a": 1}) + } + + for obj, size in objects_size.items(): + print(f" {obj}: {size} 字节") + + # 递归限制 + print(f" 递归限制: {sys.getrecursionlimit()}") + + # 5. 标准输入输出 + print("\n5. 标准输入输出:") + + # 标准流信息 + streams = { + 'stdin': sys.stdin, + 'stdout': sys.stdout, + 'stderr': sys.stderr + } + + for name, stream in streams.items(): + print(f" {name}: {type(stream).__name__}") + if hasattr(stream, 'encoding'): + print(f" 编码: {stream.encoding}") + + # 重定向示例 + print("\n 输出重定向示例:") + original_stdout = sys.stdout + + # 创建字符串缓冲区 + from io import StringIO + string_buffer = StringIO() + + # 重定向stdout + sys.stdout = string_buffer + print("这条消息被重定向到缓冲区") + print("这是第二条消息") + + # 恢复stdout + sys.stdout = original_stdout + + # 获取重定向的内容 + captured_output = string_buffer.getvalue() + print(f" 捕获的输出: {repr(captured_output)}") + + # 6. 程序退出 + print("\n6. 程序退出控制:") + + # 退出钩子 + def cleanup_function(): + print(" 执行清理操作...") + + import atexit + atexit.register(cleanup_function) + print(" 已注册退出钩子函数") + + # 异常钩子 + def exception_handler(exc_type, exc_value, exc_traceback): + print(f" 捕获未处理异常: {exc_type.__name__}: {exc_value}") + + # 设置异常钩子(仅作演示,实际使用需谨慎) + original_excepthook = sys.excepthook + sys.excepthook = exception_handler + + print(" 已设置异常钩子") + + # 恢复原始异常钩子 + sys.excepthook = original_excepthook + + # 7. 模块管理 + print("\n7. 已加载模块:") + loaded_modules = list(sys.modules.keys()) + print(f" 已加载模块数量: {len(loaded_modules)}") + + # 显示一些常见模块 + common_modules = ['os', 'sys', 'json', 'datetime', 'math'] + loaded_common = [m for m in common_modules if m in sys.modules] + print(f" 常见已加载模块: {loaded_common}") + +# 运行sys模块演示 +sys_module_demo() +``` + +### 2.3 platform模块 - 平台信息 + +```python +import platform +import sys + +def platform_module_demo(): + """platform模块演示""" + print("=== platform模块演示 ===") + + # 1. 系统信息 + print("\n1. 系统基本信息:") + basic_info = { + '系统名称': platform.system(), + '系统版本': platform.release(), + '系统详细版本': platform.version(), + '平台标识': platform.platform(), + '架构': platform.architecture(), + '机器类型': platform.machine(), + '处理器': platform.processor(), + '网络名称': platform.node() + } + + for key, value in basic_info.items(): + if isinstance(value, tuple): + value = ' | '.join(str(v) for v in value) + print(f" {key}: {value}") + + # 2. Python信息 + print("\n2. Python解释器信息:") + python_info = { + 'Python版本': platform.python_version(), + 'Python版本元组': platform.python_version_tuple(), + 'Python分支': platform.python_branch(), + 'Python修订版': platform.python_revision(), + 'Python实现': platform.python_implementation(), + 'Python编译器': platform.python_compiler() + } + + for key, value in python_info.items(): + if isinstance(value, tuple): + value = '.'.join(value) + print(f" {key}: {value}") + + # 3. 特定系统信息 + print("\n3. 特定系统信息:") + + system = platform.system() + + if system == 'Windows': + try: + win_info = { + 'Windows版本': platform.win32_ver(), + 'Windows版本(详细)': platform.win32_edition() if hasattr(platform, 'win32_edition') else '不支持' + } + + for key, value in win_info.items(): + if isinstance(value, tuple): + value = ' | '.join(str(v) for v in value if v) + print(f" {key}: {value}") + except: + print(" 无法获取Windows特定信息") + + elif system == 'Linux': + try: + linux_info = { + 'Linux发行版': platform.linux_distribution() if hasattr(platform, 'linux_distribution') else '已弃用', + 'Libc版本': platform.libc_ver() + } + + for key, value in linux_info.items(): + if isinstance(value, tuple): + value = ' | '.join(str(v) for v in value if v) + print(f" {key}: {value}") + except: + print(" 无法获取Linux特定信息") + + elif system == 'Darwin': # macOS + try: + mac_info = { + 'macOS版本': platform.mac_ver() + } + + for key, value in mac_info.items(): + if isinstance(value, tuple): + value = ' | '.join(str(v) for v in value if v) + print(f" {key}: {value}") + except: + print(" 无法获取macOS特定信息") + + # 4. 硬件信息 + print("\n4. 硬件信息:") + + # CPU信息 + try: + import multiprocessing + cpu_count = multiprocessing.cpu_count() + print(f" CPU核心数: {cpu_count}") + except: + print(" 无法获取CPU信息") + + # 内存信息(需要psutil库,这里只做演示) + try: + import psutil + memory = psutil.virtual_memory() + print(f" 总内存: {memory.total // (1024**3)} GB") + print(f" 可用内存: {memory.available // (1024**3)} GB") + print(f" 内存使用率: {memory.percent}%") + except ImportError: + print(" 内存信息需要psutil库") + + # 5. 系统兼容性检查 + print("\n5. 系统兼容性检查:") + + compatibility_checks = { + '是否为Windows': system == 'Windows', + '是否为Linux': system == 'Linux', + '是否为macOS': system == 'Darwin', + '是否为64位': platform.architecture()[0] == '64bit', + 'Python版本>=3.6': sys.version_info >= (3, 6), + 'Python版本>=3.8': sys.version_info >= (3, 8) + } + + for check, result in compatibility_checks.items(): + status = "✓" if result else "✗" + print(f" {status} {check}") + + # 6. 环境报告 + print("\n6. 完整环境报告:") + + def generate_environment_report(): + """生成环境报告""" + report = [] + report.append("=== 系统环境报告 ===") + report.append(f"操作系统: {platform.platform()}") + report.append(f"Python版本: {platform.python_version()}") + report.append(f"Python实现: {platform.python_implementation()}") + report.append(f"架构: {platform.architecture()[0]}") + report.append(f"处理器: {platform.processor()}") + report.append(f"主机名: {platform.node()}") + report.append(f"生成时间: {platform.python_build()}") + + return "\n".join(report) + + report = generate_environment_report() + print(report) + +# 运行platform模块演示 +platform_module_demo() +``` + +--- + +## 三、时间处理模块 + +### 3.1 datetime模块 - 日期时间处理 + +```python +import datetime +from datetime import date, time, datetime as dt, timedelta, timezone +import calendar + +def datetime_module_demo(): + """datetime模块演示""" + print("=== datetime模块演示 ===") + + # 1. 基本日期时间对象 + print("\n1. 基本日期时间对象:") + + # 当前时间 + now = dt.now() + today = date.today() + current_time = dt.now().time() + + print(f" 当前日期时间: {now}") + print(f" 当前日期: {today}") + print(f" 当前时间: {current_time}") + + # 创建特定日期时间 + specific_date = date(2023, 12, 25) + specific_time = time(14, 30, 0) + specific_datetime = dt(2023, 12, 25, 14, 30, 0) + + print(f" 特定日期: {specific_date}") + print(f" 特定时间: {specific_time}") + print(f" 特定日期时间: {specific_datetime}") + + # 2. 日期时间格式化 + print("\n2. 日期时间格式化:") + + format_examples = [ + ("%Y-%m-%d", "年-月-日"), + ("%Y/%m/%d", "年/月/日"), + ("%d/%m/%Y", "日/月/年"), + ("%Y-%m-%d %H:%M:%S", "完整日期时间"), + ("%A, %B %d, %Y", "英文格式"), + ("%Y年%m月%d日", "中文格式"), + ("%H:%M:%S", "时:分:秒"), + ("%I:%M %p", "12小时制") + ] + + for fmt, description in format_examples: + formatted = now.strftime(fmt) + print(f" {description}: {formatted} ({fmt})") + + # 3. 字符串解析 + print("\n3. 字符串解析:") + + date_strings = [ + ("2023-12-25", "%Y-%m-%d"), + ("25/12/2023", "%d/%m/%Y"), + ("2023-12-25 14:30:00", "%Y-%m-%d %H:%M:%S"), + ("December 25, 2023", "%B %d, %Y") + ] + + for date_str, fmt in date_strings: + try: + parsed = dt.strptime(date_str, fmt) + print(f" '{date_str}' -> {parsed}") + except ValueError as e: + print(f" '{date_str}' 解析失败: {e}") + + # 4. 时间计算 + print("\n4. 时间计算:") + + # 时间差 + start_date = dt(2023, 1, 1) + end_date = dt(2023, 12, 31) + time_diff = end_date - start_date + + print(f" 开始日期: {start_date.date()}") + print(f" 结束日期: {end_date.date()}") + print(f" 时间差: {time_diff.days} 天") + + # 时间增减 + base_date = dt.now() + + time_operations = [ + (timedelta(days=7), "7天后"), + (timedelta(days=-7), "7天前"), + (timedelta(hours=3), "3小时后"), + (timedelta(weeks=2), "2周后"), + (timedelta(days=30), "30天后") + ] + + for delta, description in time_operations: + result_date = base_date + delta + print(f" {description}: {result_date.strftime('%Y-%m-%d %H:%M')}") + + # 5. 时区处理 + print("\n5. 时区处理:") + + # UTC时间 + utc_now = dt.now(timezone.utc) + print(f" UTC时间: {utc_now}") + + # 本地时间 + local_now = dt.now() + print(f" 本地时间: {local_now}") + + # 时区转换 + try: + import zoneinfo # Python 3.9+ + + # 创建不同时区的时间 + tokyo_tz = zoneinfo.ZoneInfo("Asia/Tokyo") + london_tz = zoneinfo.ZoneInfo("Europe/London") + + tokyo_time = utc_now.astimezone(tokyo_tz) + london_time = utc_now.astimezone(london_tz) + + print(f" 东京时间: {tokyo_time}") + print(f" 伦敦时间: {london_time}") + + except ImportError: + print(" 时区转换需要Python 3.9+或pytz库") + + # 6. 日期时间属性 + print("\n6. 日期时间属性:") + + sample_dt = dt(2023, 12, 25, 14, 30, 45, 123456) + + attributes = { + '年': sample_dt.year, + '月': sample_dt.month, + '日': sample_dt.day, + '时': sample_dt.hour, + '分': sample_dt.minute, + '秒': sample_dt.second, + '微秒': sample_dt.microsecond, + '星期几': sample_dt.weekday(), # 0=Monday + 'ISO星期几': sample_dt.isoweekday(), # 1=Monday + '年中第几天': sample_dt.timetuple().tm_yday + } + + for attr, value in attributes.items(): + print(f" {attr}: {value}") + + # 7. 实用函数 + print("\n7. 实用日期时间函数:") + + def get_age(birth_date): + """计算年龄""" + today = date.today() + age = today.year - birth_date.year + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + return age + + def get_days_until(target_date): + """计算距离目标日期的天数""" + today = date.today() + if isinstance(target_date, dt): + target_date = target_date.date() + return (target_date - today).days + + def get_quarter(date_obj): + """获取季度""" + return (date_obj.month - 1) // 3 + 1 + + def is_weekend(date_obj): + """判断是否为周末""" + return date_obj.weekday() >= 5 + + # 测试实用函数 + birth_date = date(1990, 5, 15) + new_year = date(2024, 1, 1) + + print(f" 年龄计算: 生于{birth_date},现在{get_age(birth_date)}岁") + print(f" 距离新年: {get_days_until(new_year)}天") + print(f" 当前季度: 第{get_quarter(today)}季度") + print(f" 今天是否周末: {is_weekend(today)}") + + # 8. 日历操作 + print("\n8. 日历操作:") + + # 获取月份信息 + year, month = 2023, 12 + + # 月份天数 + days_in_month = calendar.monthrange(year, month)[1] + print(f" {year}年{month}月有{days_in_month}天") + + # 是否闰年 + is_leap = calendar.isleap(year) + print(f" {year}年是否闰年: {is_leap}") + + # 月份第一天是星期几 + first_weekday = calendar.monthrange(year, month)[0] + weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] + print(f" {year}年{month}月1日是{weekdays[first_weekday]}") + + # 显示月历 + print(f"\n {year}年{month}月日历:") + month_calendar = calendar.month(year, month) + print(month_calendar) + +# 运行datetime模块演示 +datetime_module_demo() +``` + +### 3.2 time模块 - 时间处理 + +```python +import time +import datetime + +def time_module_demo(): + """time模块演示""" + print("=== time模块演示 ===") + + # 1. 时间戳 + print("\n1. 时间戳操作:") + + # 当前时间戳 + current_timestamp = time.time() + print(f" 当前时间戳: {current_timestamp}") + print(f" 时间戳(整数): {int(current_timestamp)}") + + # 时间戳转换 + timestamp_to_struct = time.localtime(current_timestamp) + timestamp_to_string = time.ctime(current_timestamp) + + print(f" 时间戳转结构: {timestamp_to_struct}") + print(f" 时间戳转字符串: {timestamp_to_string}") + + # 字符串转时间戳 + time_string = "2023-12-25 14:30:00" + time_struct = time.strptime(time_string, "%Y-%m-%d %H:%M:%S") + timestamp = time.mktime(time_struct) + + print(f" 字符串'{time_string}'转时间戳: {timestamp}") + + # 2. 时间结构 + print("\n2. 时间结构操作:") + + # 当前时间结构 + local_time = time.localtime() + utc_time = time.gmtime() + + print(f" 本地时间结构: {local_time}") + print(f" UTC时间结构: {utc_time}") + + # 时间结构属性 + time_attributes = { + '年': local_time.tm_year, + '月': local_time.tm_mon, + '日': local_time.tm_mday, + '时': local_time.tm_hour, + '分': local_time.tm_min, + '秒': local_time.tm_sec, + '星期几': local_time.tm_wday, # 0=Monday + '年中第几天': local_time.tm_yday, + '是否夏令时': local_time.tm_isdst + } + + for attr, value in time_attributes.items(): + print(f" {attr}: {value}") + + # 3. 时间格式化 + print("\n3. 时间格式化:") + + format_examples = [ + ("%Y-%m-%d %H:%M:%S", "标准格式"), + ("%Y/%m/%d", "日期格式"), + ("%H:%M:%S", "时间格式"), + ("%A, %B %d, %Y", "完整英文格式"), + ("%c", "本地完整格式"), + ("%x", "本地日期格式"), + ("%X", "本地时间格式") + ] + + for fmt, description in format_examples: + formatted = time.strftime(fmt, local_time) + print(f" {description}: {formatted}") + + # 4. 性能测量 + print("\n4. 性能测量:") + + # 使用time.time()测量 + start_time = time.time() + + # 模拟一些计算 + total = sum(i * i for i in range(100000)) + + end_time = time.time() + execution_time = end_time - start_time + + print(f" 计算结果: {total}") + print(f" 执行时间(time): {execution_time:.6f}秒") + + # 使用time.perf_counter()测量(更精确) + start_perf = time.perf_counter() + + # 同样的计算 + total = sum(i * i for i in range(100000)) + + end_perf = time.perf_counter() + perf_time = end_perf - start_perf + + print(f" 执行时间(perf_counter): {perf_time:.6f}秒") + + # 使用time.process_time()测量CPU时间 + start_process = time.process_time() + + # 同样的计算 + total = sum(i * i for i in range(100000)) + + end_process = time.process_time() + process_time = end_process - start_process + + print(f" CPU时间(process_time): {process_time:.6f}秒") + + # 5. 睡眠和延迟 + print("\n5. 睡眠和延迟:") + + print(" 开始睡眠测试...") + + # 短暂睡眠 + sleep_start = time.perf_counter() + time.sleep(0.1) # 睡眠0.1秒 + sleep_end = time.perf_counter() + actual_sleep = sleep_end - sleep_start + + print(f" 预期睡眠: 0.1秒") + print(f" 实际睡眠: {actual_sleep:.6f}秒") + print(f" 误差: {abs(actual_sleep - 0.1):.6f}秒") + + # 6. 时区信息 + print("\n6. 时区信息:") + + # 时区偏移 + timezone_offset = time.timezone + daylight_offset = time.altzone if time.daylight else time.timezone + + print(f" 时区偏移: {timezone_offset}秒 ({timezone_offset/3600}小时)") + print(f" 夏令时偏移: {daylight_offset}秒 ({daylight_offset/3600}小时)") + print(f" 是否支持夏令时: {bool(time.daylight)}") + + # 时区名称 + if hasattr(time, 'tzname'): + print(f" 时区名称: {time.tzname}") + + # 7. 实用时间函数 + print("\n7. 实用时间函数:") + + def format_duration(seconds): + """格式化持续时间""" + if seconds < 60: + return f"{seconds:.2f}秒" + elif seconds < 3600: + minutes = seconds // 60 + secs = seconds % 60 + return f"{int(minutes)}分{secs:.2f}秒" + else: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + return f"{int(hours)}小时{int(minutes)}分{secs:.2f}秒" + + def time_since(timestamp): + """计算距离某个时间戳的时间""" + now = time.time() + diff = now - timestamp + + if diff < 60: + return f"{int(diff)}秒前" + elif diff < 3600: + return f"{int(diff//60)}分钟前" + elif diff < 86400: + return f"{int(diff//3600)}小时前" + else: + return f"{int(diff//86400)}天前" + + def benchmark_function(func, *args, **kwargs): + """函数性能基准测试""" + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + return result, end - start + + # 测试实用函数 + durations = [0.5, 65, 3665, 7325] + for duration in durations: + print(f" {duration}秒 = {format_duration(duration)}") + + # 测试时间距离 + past_timestamp = current_timestamp - 3665 # 1小时前 + print(f" 时间距离: {time_since(past_timestamp)}") + + # 基准测试示例 + def test_function(): + return sum(range(10000)) + + result, duration = benchmark_function(test_function) + print(f" 基准测试: 结果={result}, 耗时={duration:.6f}秒") + +# 运行time模块演示 +time_module_demo() +``` + +--- + +## 四、数据处理模块 + +### 4.1 json模块 - JSON数据处理 + +```python +import json +from datetime import datetime +from decimal import Decimal + +def json_module_demo(): + """json模块演示""" + print("=== json模块演示 ===") + + # 1. 基本序列化和反序列化 + print("\n1. 基本JSON操作:") + + # Python对象转JSON + python_data = { + "name": "张三", + "age": 25, + "is_student": True, + "scores": [85, 92, 78], + "address": { + "city": "北京", + "district": "朝阳区" + }, + "phone": None + } + + # 转换为JSON字符串 + json_string = json.dumps(python_data, ensure_ascii=False, indent=2) + print(f" Python对象转JSON:") + print(json_string) + + # JSON字符串转Python对象 + parsed_data = json.loads(json_string) + print(f"\n JSON转Python对象: {parsed_data}") + print(f" 数据类型: {type(parsed_data)}") + + # 2. 文件操作 + print("\n2. JSON文件操作:") + + # 写入JSON文件 + filename = "test_data.json" + + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(python_data, f, ensure_ascii=False, indent=2) + print(f" 数据已写入文件: {filename}") + + # 读取JSON文件 + with open(filename, 'r', encoding='utf-8') as f: + loaded_data = json.load(f) + print(f" 从文件读取数据: {loaded_data['name']}") + + except Exception as e: + print(f" 文件操作错误: {e}") + + # 3. JSON格式化选项 + print("\n3. JSON格式化选项:") + + sample_data = {"中文": "测试", "numbers": [1, 2, 3], "nested": {"key": "value"}} + + format_options = [ + ({}, "默认格式"), + ({"indent": 2}, "缩进格式"), + ({"indent": 2, "ensure_ascii": False}, "支持中文"), + ({"separators": (',', ':')}, "紧凑格式"), + ({"sort_keys": True}, "键排序"), + ({"indent": 2, "ensure_ascii": False, "sort_keys": True}, "完整格式") + ] + + for options, description in format_options: + formatted = json.dumps(sample_data, **options) + print(f" {description}: {formatted}") + + # 4. 自定义序列化 + print("\n4. 自定义序列化:") + + # 包含不可序列化对象的数据 + complex_data = { + "datetime": datetime.now(), + "decimal": Decimal('10.50'), + "set": {1, 2, 3}, + "bytes": b"hello" + } + + # 自定义JSON编码器 + class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, Decimal): + return float(obj) + elif isinstance(obj, set): + return list(obj) + elif isinstance(obj, bytes): + return obj.decode('utf-8') + return super().default(obj) + + # 使用自定义编码器 + try: + custom_json = json.dumps(complex_data, cls=CustomJSONEncoder, indent=2) + print(f" 自定义序列化结果:") + print(custom_json) + except Exception as e: + print(f" 序列化错误: {e}") + + # 使用default参数 + def json_serializer(obj): + """自定义序列化函数""" + if isinstance(obj, datetime): + return obj.strftime('%Y-%m-%d %H:%M:%S') + elif isinstance(obj, Decimal): + return str(obj) + elif isinstance(obj, set): + return list(obj) + elif isinstance(obj, bytes): + return obj.decode('utf-8') + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + try: + default_json = json.dumps(complex_data, default=json_serializer, indent=2) + print(f"\n 使用default参数:") + print(default_json) + except Exception as e: + print(f" 序列化错误: {e}") + + # 5. JSON验证和错误处理 + print("\n5. JSON验证和错误处理:") + + # 有效和无效的JSON字符串 + json_samples = [ + ('{"name": "张三", "age": 25}', "有效JSON"), + ('{"name": "张三", "age": 25,}', "尾随逗号(无效)"), + ('{name: "张三", age: 25}', "无引号键(无效)"), + ('[1, 2, 3]', "数组格式"), + ('"simple string"', "简单字符串"), + ('123', "数字"), + ('true', "布尔值"), + ('null', "空值"), + ('{"nested": {"deep": {"value": 42}}}', "嵌套对象") + ] + + for json_str, description in json_samples: + try: + parsed = json.loads(json_str) + print(f" ✓ {description}: {parsed}") + except json.JSONDecodeError as e: + print(f" ✗ {description}: 解析错误 - {e.msg} (位置: {e.pos})") + + # 6. JSON Schema验证(概念演示) + print("\n6. JSON数据验证:") + + def validate_user_data(data): + """简单的用户数据验证""" + required_fields = ['name', 'age', 'email'] + errors = [] + + # 检查必需字段 + for field in required_fields: + if field not in data: + errors.append(f"缺少必需字段: {field}") + + # 类型检查 + if 'age' in data and not isinstance(data['age'], int): + errors.append("age字段必须是整数") + + if 'age' in data and data['age'] < 0: + errors.append("age字段必须是正数") + + if 'email' in data and '@' not in str(data['email']): + errors.append("email字段格式无效") + + return len(errors) == 0, errors + + # 测试数据验证 + test_users = [ + {"name": "张三", "age": 25, "email": "zhangsan@example.com"}, + {"name": "李四", "age": "25", "email": "lisi@example.com"}, # age类型错误 + {"name": "王五", "age": -5, "email": "wangwu@example.com"}, # age负数 + {"name": "赵六", "age": 30}, # 缺少email + {"name": "钱七", "age": 28, "email": "qianqi.example.com"} # email格式错误 + ] + + for i, user in enumerate(test_users, 1): + is_valid, errors = validate_user_data(user) + status = "✓" if is_valid else "✗" + print(f" {status} 用户{i}: {user.get('name', '未知')}") + if errors: + for error in errors: + print(f" - {error}") + + # 7. JSON性能优化 + print("\n7. JSON性能测试:") + + import time + + # 创建大量数据 + large_data = { + "users": [ + {"id": i, "name": f"用户{i}", "score": i * 10} + for i in range(1000) + ] + } + + # 测试序列化性能 + start_time = time.perf_counter() + json_str = json.dumps(large_data) + serialize_time = time.perf_counter() - start_time + + # 测试反序列化性能 + start_time = time.perf_counter() + parsed_data = json.loads(json_str) + deserialize_time = time.perf_counter() - start_time + + print(f" 数据大小: {len(large_data['users'])} 个用户") + print(f" JSON字符串长度: {len(json_str)} 字符") + print(f" 序列化时间: {serialize_time:.6f}秒") + print(f" 反序列化时间: {deserialize_time:.6f}秒") + + # 8. 实用JSON工具函数 + print("\n8. 实用JSON工具函数:") + + def pretty_print_json(data): + """美化打印JSON数据""" + return json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + + def json_diff(json1, json2): + """比较两个JSON对象的差异""" + def get_differences(obj1, obj2, path=""): + differences = [] + + if type(obj1) != type(obj2): + differences.append(f"{path}: 类型不同 ({type(obj1).__name__} vs {type(obj2).__name__})") + return differences + + if isinstance(obj1, dict): + all_keys = set(obj1.keys()) | set(obj2.keys()) + for key in all_keys: + new_path = f"{path}.{key}" if path else key + if key not in obj1: + differences.append(f"{new_path}: 仅在第二个对象中存在") + elif key not in obj2: + differences.append(f"{new_path}: 仅在第一个对象中存在") + else: + differences.extend(get_differences(obj1[key], obj2[key], new_path)) + elif isinstance(obj1, list): + if len(obj1) != len(obj2): + differences.append(f"{path}: 列表长度不同 ({len(obj1)} vs {len(obj2)})") + for i in range(min(len(obj1), len(obj2))): + differences.extend(get_differences(obj1[i], obj2[i], f"{path}[{i}]")) + else: + if obj1 != obj2: + differences.append(f"{path}: 值不同 ({obj1} vs {obj2})") + + return differences + + return get_differences(json1, json2) + + def flatten_json(data, separator='.'): + """扁平化JSON对象""" + def _flatten(obj, parent_key=''): + items = [] + if isinstance(obj, dict): + for key, value in obj.items(): + new_key = f"{parent_key}{separator}{key}" if parent_key else key + items.extend(_flatten(value, new_key).items()) + elif isinstance(obj, list): + for i, value in enumerate(obj): + new_key = f"{parent_key}{separator}{i}" if parent_key else str(i) + items.extend(_flatten(value, new_key).items()) + else: + return {parent_key: obj} + return dict(items) + + return _flatten(data) + + # 测试工具函数 + test_data = {"user": {"name": "张三", "details": {"age": 25, "city": "北京"}}} + + print(f" 美化打印:") + print(pretty_print_json(test_data)) + + # JSON差异比较 + data1 = {"name": "张三", "age": 25} + data2 = {"name": "李四", "age": 25, "city": "北京"} + differences = json_diff(data1, data2) + print(f"\n JSON差异:") + for diff in differences: + print(f" {diff}") + + # JSON扁平化 + flattened = flatten_json(test_data) + print(f"\n 扁平化结果: {flattened}") + + # 清理测试文件 + try: + import os + if os.path.exists(filename): + os.remove(filename) + print(f" 已删除测试文件: {filename}") + except Exception as e: + print(f" 删除文件失败: {e}") + +# 运行json模块演示 +json_module_demo() +``` + +### 4.2 csv模块 - CSV文件处理 + +```python +import csv +import io +from datetime import datetime + +def csv_module_demo(): + """csv模块演示""" + print("=== csv模块演示 ===") + + # 1. 基本CSV读写 + print("\n1. 基本CSV操作:") + + # 准备测试数据 + test_data = [ + ['姓名', '年龄', '城市', '薪资'], + ['张三', '25', '北京', '8000'], + ['李四', '30', '上海', '12000'], + ['王五', '28', '广州', '9500'], + ['赵六', '35', '深圳', '15000'] + ] + + # 写入CSV文件 + filename = 'employees.csv' + + try: + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerows(test_data) + print(f" 数据已写入文件: {filename}") + + # 读取CSV文件 + with open(filename, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + print(" 读取的数据:") + for i, row in enumerate(reader): + print(f" 行{i+1}: {row}") + + except Exception as e: + print(f" 文件操作错误: {e}") + + # 2. 字典格式读写 + print("\n2. 字典格式CSV操作:") + + # 字典数据 + dict_data = [ + {'姓名': '张三', '年龄': 25, '城市': '北京', '薪资': 8000}, + {'姓名': '李四', '年龄': 30, '城市': '上海', '薪资': 12000}, + {'姓名': '王五', '年龄': 28, '城市': '广州', '薪资': 9500} + ] + + dict_filename = 'employees_dict.csv' + + try: + # 写入字典格式CSV + with open(dict_filename, 'w', newline='', encoding='utf-8') as f: + fieldnames = ['姓名', '年龄', '城市', '薪资'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + + writer.writeheader() # 写入表头 + writer.writerows(dict_data) + + print(f" 字典数据已写入: {dict_filename}") + + # 读取字典格式CSV + with open(dict_filename, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + print(" 读取的字典数据:") + for i, row in enumerate(reader, 1): + print(f" 员工{i}: {dict(row)}") + + except Exception as e: + print(f" 字典操作错误: {e}") + + # 3. CSV方言和格式化选项 + print("\n3. CSV方言和格式化:") + + # 自定义CSV方言 + csv.register_dialect('custom', + delimiter='|', + quotechar='"', + quoting=csv.QUOTE_MINIMAL, + lineterminator='\n') + + # 使用自定义方言 + custom_filename = 'custom_format.csv' + + try: + with open(custom_filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, dialect='custom') + writer.writerows(test_data) + + print(f" 自定义格式文件: {custom_filename}") + + # 读取自定义格式 + with open(custom_filename, 'r', encoding='utf-8') as f: + reader = csv.reader(f, dialect='custom') + print(" 自定义格式数据:") + for row in reader: + print(f" {row}") + + except Exception as e: + print(f" 自定义格式错误: {e}") + + # 4. 处理特殊字符和引号 + print("\n4. 特殊字符处理:") + + special_data = [ + ['产品名称', '描述', '价格'], + ['iPhone 15', 'Apple最新款手机,配备"A17 Pro"芯片', '7999'], + ['MacBook Pro', '专业级笔记本电脑\n适合开发者使用', '12999'], + ['AirPods Pro', '降噪耳机,支持"空间音频"功能', '1999'] + ] + + special_filename = 'special_chars.csv' + + try: + # 写入包含特殊字符的数据 + with open(special_filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, quoting=csv.QUOTE_ALL) + writer.writerows(special_data) + + print(f" 特殊字符文件: {special_filename}") + + # 读取并显示 + with open(special_filename, 'r', encoding='utf-8') as f: + content = f.read() + print(" 文件内容:") + print(content) + + except Exception as e: + print(f" 特殊字符处理错误: {e}") + + # 5. 内存中的CSV操作 + print("\n5. 内存中CSV操作:") + + # 使用StringIO在内存中处理CSV + csv_string = """姓名,年龄,部门 +张三,25,技术部 +李四,30,销售部 +王五,28,市场部""" + + # 从字符串读取CSV + string_io = io.StringIO(csv_string) + reader = csv.DictReader(string_io) + + print(" 从字符串读取CSV:") + employees = list(reader) + for emp in employees: + print(f" {emp}") + + # 写入到字符串 + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=['姓名', '年龄', '部门', '薪资']) + writer.writeheader() + + # 添加薪资信息 + for emp in employees: + emp['薪资'] = str(int(emp['年龄']) * 500) # 简单计算 + writer.writerow(emp) + + result_csv = output.getvalue() + print("\n 生成的CSV字符串:") + print(result_csv) + + # 6. CSV数据分析 + print("\n6. CSV数据分析:") + + def analyze_csv_data(filename): + """分析CSV文件数据""" + try: + with open(filename, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + data = list(reader) + + if not data: + return "文件为空" + + analysis = { + '总行数': len(data), + '列数': len(data[0]) if data else 0, + '列名': list(data[0].keys()) if data else [], + '数值列分析': {} + } + + # 分析数值列 + for column in analysis['列名']: + values = [row[column] for row in data] + + # 尝试转换为数字 + numeric_values = [] + for value in values: + try: + numeric_values.append(float(value)) + except ValueError: + continue + + if numeric_values: + analysis['数值列分析'][column] = { + '最小值': min(numeric_values), + '最大值': max(numeric_values), + '平均值': sum(numeric_values) / len(numeric_values), + '数值个数': len(numeric_values) + } + + return analysis + + except Exception as e: + return f"分析错误: {e}" + + # 分析员工数据 + analysis = analyze_csv_data(dict_filename) + print(f" 数据分析结果:") + if isinstance(analysis, dict): + for key, value in analysis.items(): + if key == '数值列分析': + print(f" {key}:") + for col, stats in value.items(): + print(f" {col}: {stats}") + else: + print(f" {key}: {value}") + else: + print(f" {analysis}") + + # 7. CSV转换工具 + print("\n7. CSV转换工具:") + + def csv_to_json(csv_filename, json_filename=None): + """CSV转JSON""" + try: + with open(csv_filename, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + data = list(reader) + + if json_filename: + import json + with open(json_filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return f"已转换为: {json_filename}" + else: + return data + + except Exception as e: + return f"转换错误: {e}" + + def filter_csv(input_filename, output_filename, filter_func): + """过滤CSV数据""" + try: + with open(input_filename, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + data = list(reader) + + filtered_data = [row for row in data if filter_func(row)] + + if filtered_data: + with open(output_filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=data[0].keys()) + writer.writeheader() + writer.writerows(filtered_data) + + return f"过滤后数据已保存到: {output_filename}" + else: + return "没有符合条件的数据" + + except Exception as e: + return f"过滤错误: {e}" + + # 测试转换工具 + json_result = csv_to_json(dict_filename) + print(f" CSV转JSON结果: {len(json_result)}条记录") + + # 过滤高薪员工 + def high_salary_filter(row): + try: + return int(row['薪资']) > 10000 + except: + return False + + filter_result = filter_csv(dict_filename, 'high_salary.csv', high_salary_filter) + print(f" 过滤结果: {filter_result}") + + # 清理测试文件 + test_files = [filename, dict_filename, custom_filename, special_filename, 'high_salary.csv'] + for file in test_files: + try: + import os + if os.path.exists(file): + os.remove(file) + except: + pass + + print(" 已清理测试文件") + +# 运行csv模块演示 +csv_module_demo() +``` + +--- + +## 五、数学计算模块 + +### 5.1 math模块 - 数学函数 + +```python +import math +import cmath # 复数数学函数 + +def math_module_demo(): + """math模块演示""" + print("=== math模块演示 ===") + + # 1. 数学常数 + print("\n1. 数学常数:") + constants = { + 'π (pi)': math.pi, + 'e (自然对数底)': math.e, + 'τ (tau = 2π)': math.tau, + '∞ (无穷大)': math.inf, + 'NaN (非数字)': math.nan + } + + for name, value in constants.items(): + print(f" {name}: {value}") + + # 2. 基本数学运算 + print("\n2. 基本数学运算:") + + test_numbers = [16, 25, 100, 2.5, -3.7] + + for num in test_numbers: + operations = { + f'sqrt({num})': math.sqrt(abs(num)), # 平方根 + f'pow({num}, 2)': math.pow(num, 2), # 幂运算 + f'abs({num})': abs(num), # 绝对值 + f'ceil({num})': math.ceil(num), # 向上取整 + f'floor({num})': math.floor(num), # 向下取整 + f'trunc({num})': math.trunc(num) # 截断小数部分 + } + + print(f"\n 数字 {num}:") + for op, result in operations.items(): + print(f" {op} = {result}") + + # 3. 三角函数 + print("\n3. 三角函数:") + + angles_degrees = [0, 30, 45, 60, 90, 180, 270, 360] + + print(" 角度(度) | 弧度 | sin | cos | tan") + print(" " + "-" * 50) + + for angle_deg in angles_degrees: + angle_rad = math.radians(angle_deg) # 度转弧度 + + sin_val = math.sin(angle_rad) + cos_val = math.cos(angle_rad) + + # 处理tan在90度和270度时的无穷大 + try: + tan_val = math.tan(angle_rad) + if abs(tan_val) > 1e10: # 很大的数视为无穷大 + tan_str = "∞" + else: + tan_str = f"{tan_val:.6f}" + except: + tan_str = "∞" + + print(f" {angle_deg:7d} | {angle_rad:8.4f} | {sin_val:8.4f} | {cos_val:8.4f} | {tan_str}") + + # 反三角函数 + print("\n 反三角函数示例:") + test_values = [0, 0.5, 0.707, 0.866, 1] + + for val in test_values: + try: + asin_deg = math.degrees(math.asin(val)) + acos_deg = math.degrees(math.acos(val)) + atan_deg = math.degrees(math.atan(val)) + + print(f" 值 {val}: arcsin={asin_deg:.1f}°, arccos={acos_deg:.1f}°, arctan={atan_deg:.1f}°") + except ValueError as e: + print(f" 值 {val}: 计算错误 - {e}") + + # 4. 对数函数 + print("\n4. 对数函数:") + + log_test_values = [1, 2, 10, 100, math.e, math.pi] + + for val in log_test_values: + log_results = { + f'ln({val})': math.log(val), # 自然对数 + f'log10({val})': math.log10(val), # 常用对数 + f'log2({val})': math.log2(val), # 二进制对数 + f'log({val}, 3)': math.log(val, 3) # 以3为底的对数 + } + + print(f"\n 数值 {val:.4f}:") + for op, result in log_results.items(): + print(f" {op} = {result:.6f}") + + # 5. 指数函数 + print("\n5. 指数函数:") + + exp_test_values = [0, 1, 2, -1, 0.5, math.pi] + + for val in exp_test_values: + exp_results = { + f'e^{val}': math.exp(val), # e的x次方 + f'2^{val}': math.pow(2, val), # 2的x次方 + f'10^{val}': math.pow(10, val), # 10的x次方 + f'e^{val}-1': math.expm1(val) # e^x - 1 (更精确) + } + + print(f"\n 指数 {val}:") + for op, result in exp_results.items(): + print(f" {op} = {result:.6f}") + + # 6. 双曲函数 + print("\n6. 双曲函数:") + + hyperbolic_values = [0, 1, 2, -1] + + for val in hyperbolic_values: + hyp_results = { + f'sinh({val})': math.sinh(val), # 双曲正弦 + f'cosh({val})': math.cosh(val), # 双曲余弦 + f'tanh({val})': math.tanh(val) # 双曲正切 + } + + print(f"\n 值 {val}:") + for op, result in hyp_results.items(): + print(f" {op} = {result:.6f}") + + # 7. 特殊函数 + print("\n7. 特殊函数:") + + # 阶乘 + factorial_values = [0, 1, 5, 10] + print(" 阶乘函数:") + for val in factorial_values: + result = math.factorial(val) + print(f" {val}! = {result}") + + # 最大公约数和最小公倍数 + print("\n 最大公约数和最小公倍数:") + number_pairs = [(12, 18), (24, 36), (17, 19), (100, 75)] + + for a, b in number_pairs: + gcd_val = math.gcd(a, b) + lcm_val = abs(a * b) // gcd_val # 最小公倍数公式 + print(f" gcd({a}, {b}) = {gcd_val}, lcm({a}, {b}) = {lcm_val}") + + # 8. 数值判断函数 + print("\n8. 数值判断函数:") + + test_special_values = [0, 1, -1, math.inf, -math.inf, math.nan, 3.14] + + print(" 值 | 有限 | 无穷 | NaN | 整数") + print(" " + "-" * 40) + + for val in test_special_values: + is_finite = math.isfinite(val) + is_inf = math.isinf(val) + is_nan = math.isnan(val) + + # 检查是否为整数 + try: + is_integer = val.is_integer() if isinstance(val, float) else isinstance(val, int) + except: + is_integer = False + + print(f" {str(val):8} | {str(is_finite):4} | {str(is_inf):4} | {str(is_nan):4} | {str(is_integer):4}") + + # 9. 复数数学函数 + print("\n9. 复数数学函数:") + + complex_numbers = [1+2j, 3-4j, 5j, -2+3j] + + for c in complex_numbers: + complex_results = { + f'abs({c})': abs(c), # 模长 + f'phase({c})': cmath.phase(c), # 相位角 + f'sqrt({c})': cmath.sqrt(c), # 复数平方根 + f'exp({c})': cmath.exp(c), # 复数指数 + f'log({c})': cmath.log(c) # 复数对数 + } + + print(f"\n 复数 {c}:") + for op, result in complex_results.items(): + if isinstance(result, complex): + print(f" {op} = {result:.4f}") + else: + print(f" {op} = {result:.6f}") + + # 10. 实用数学工具函数 + print("\n10. 实用数学工具函数:") + + def distance_2d(x1, y1, x2, y2): + """计算二维平面上两点间距离""" + return math.sqrt((x2 - x1)**2 + (y2 - y1)**2) + + def angle_between_vectors(x1, y1, x2, y2): + """计算两个向量间的夹角(弧度)""" + dot_product = x1 * x2 + y1 * y2 + magnitude1 = math.sqrt(x1**2 + y1**2) + magnitude2 = math.sqrt(x2**2 + y2**2) + + if magnitude1 == 0 or magnitude2 == 0: + return 0 + + cos_angle = dot_product / (magnitude1 * magnitude2) + # 处理浮点数精度问题 + cos_angle = max(-1, min(1, cos_angle)) + return math.acos(cos_angle) + + def circle_area_circumference(radius): + """计算圆的面积和周长""" + area = math.pi * radius**2 + circumference = 2 * math.pi * radius + return area, circumference + + def sphere_volume_surface(radius): + """计算球的体积和表面积""" + volume = (4/3) * math.pi * radius**3 + surface = 4 * math.pi * radius**2 + return volume, surface + + # 测试工具函数 + print(" 几何计算示例:") + + # 两点间距离 + dist = distance_2d(0, 0, 3, 4) + print(f" 点(0,0)到点(3,4)的距离: {dist}") + + # 向量夹角 + angle_rad = angle_between_vectors(1, 0, 0, 1) + angle_deg = math.degrees(angle_rad) + print(f" 向量(1,0)和(0,1)的夹角: {angle_deg}度") + + # 圆的计算 + radius = 5 + area, circumference = circle_area_circumference(radius) + print(f" 半径{radius}的圆: 面积={area:.2f}, 周长={circumference:.2f}") + + # 球的计算 + volume, surface = sphere_volume_surface(radius) + print(f" 半径{radius}的球: 体积={volume:.2f}, 表面积={surface:.2f}") + +# 运行math模块演示 +math_module_demo() +``` + +### 5.2 random模块 - 随机数生成 + +```python +import random +import string +from collections import Counter + +def random_module_demo(): + """random模块演示""" + print("=== random模块演示 ===") + + # 1. 基本随机数生成 + print("\n1. 基本随机数生成:") + + # 设置随机种子(确保结果可重现) + random.seed(42) + print(f" 设置随机种子: 42") + + # 生成随机浮点数 + print("\n 随机浮点数 [0.0, 1.0):") + for i in range(5): + print(f" random(): {random.random():.6f}") + + # 生成指定范围的随机浮点数 + print("\n 指定范围的随机浮点数:") + for i in range(5): + val = random.uniform(1.5, 10.5) + print(f" uniform(1.5, 10.5): {val:.4f}") + + # 生成随机整数 + print("\n 随机整数:") + for i in range(5): + val = random.randint(1, 100) # 包含两端 + print(f" randint(1, 100): {val}") + + # 生成随机整数(不包含上界) + print("\n 随机整数(不包含上界):") + for i in range(5): + val = random.randrange(1, 100, 2) # 1到99的奇数 + print(f" randrange(1, 100, 2): {val}") + + # 2. 序列随机操作 + print("\n2. 序列随机操作:") + + # 随机选择 + colors = ['红色', '绿色', '蓝色', '黄色', '紫色'] + print(f" 颜色列表: {colors}") + + print("\n 随机选择:") + for i in range(5): + color = random.choice(colors) + print(f" choice(): {color}") + + # 带权重的随机选择 + weights = [1, 2, 3, 4, 5] # 权重越大,被选中概率越高 + print(f"\n 带权重随机选择 (权重: {weights}):") + for i in range(5): + color = random.choices(colors, weights=weights, k=1)[0] + print(f" choices(): {color}") + + # 随机抽样(不重复) + print("\n 随机抽样(不重复):") + sample_size = 3 + sample = random.sample(colors, sample_size) + print(f" sample({sample_size}): {sample}") + + # 随机打乱 + numbers = list(range(1, 11)) + print(f"\n 原始列表: {numbers}") + random.shuffle(numbers) + print(f" 打乱后: {numbers}") + + # 3. 概率分布 + print("\n3. 概率分布:") + + # 正态分布(高斯分布) + print("\n 正态分布 N(μ=100, σ=15):") + normal_samples = [] + for i in range(10): + val = random.normalvariate(100, 15) # 均值100,标准差15 + normal_samples.append(val) + print(f" 样本{i+1}: {val:.2f}") + + print(f" 样本均值: {sum(normal_samples)/len(normal_samples):.2f}") + + # 指数分布 + print("\n 指数分布 (λ=1.5):") + for i in range(5): + val = random.expovariate(1.5) + print(f" 样本{i+1}: {val:.4f}") + + # 伽马分布 + print("\n 伽马分布 (α=2, β=1):") + for i in range(5): + val = random.gammavariate(2, 1) + print(f" 样本{i+1}: {val:.4f}") + + # 4. 随机字符串生成 + print("\n4. 随机字符串生成:") + + def generate_random_string(length, chars=None): + """生成随机字符串""" + if chars is None: + chars = string.ascii_letters + string.digits + return ''.join(random.choice(chars) for _ in range(length)) + + def generate_password(length=12): + """生成随机密码""" + chars = string.ascii_letters + string.digits + '!@#$%^&*' + # 确保包含各种字符类型 + password = [ + random.choice(string.ascii_lowercase), + random.choice(string.ascii_uppercase), + random.choice(string.digits), + random.choice('!@#$%^&*') + ] + + # 填充剩余长度 + for _ in range(length - 4): + password.append(random.choice(chars)) + + # 打乱顺序 + random.shuffle(password) + return ''.join(password) + + def generate_uuid_like(): + """生成类似UUID的字符串""" + chars = string.hexdigits.lower() + parts = [ + ''.join(random.choice(chars) for _ in range(8)), + ''.join(random.choice(chars) for _ in range(4)), + ''.join(random.choice(chars) for _ in range(4)), + ''.join(random.choice(chars) for _ in range(4)), + ''.join(random.choice(chars) for _ in range(12)) + ] + return '-'.join(parts) + + # 测试字符串生成 + print(" 随机字符串示例:") + print(f" 字母数字(8位): {generate_random_string(8)}") + print(f" 纯字母(10位): {generate_random_string(10, string.ascii_letters)}") + print(f" 随机密码: {generate_password()}") + print(f" UUID样式: {generate_uuid_like()}") + + # 5. 随机数据生成 + print("\n5. 随机数据生成:") + + def generate_random_person(): + """生成随机人员信息""" + first_names = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴'] + last_names = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋'] + cities = ['北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都'] + + return { + '姓名': random.choice(first_names) + random.choice(last_names), + '年龄': random.randint(18, 65), + '城市': random.choice(cities), + '薪资': random.randint(5000, 50000), + '邮箱': f"{generate_random_string(8).lower()}@example.com" + } + + def generate_test_data(count=5): + """生成测试数据集""" + return [generate_random_person() for _ in range(count)] + + # 生成测试数据 + test_people = generate_test_data(5) + print(" 随机人员数据:") + for i, person in enumerate(test_people, 1): + print(f" 人员{i}: {person}") + + # 6. 随机采样和统计 + print("\n6. 随机采样和统计:") + + # 模拟投硬币 + def simulate_coin_flips(n): + """模拟投硬币""" + results = [random.choice(['正面', '反面']) for _ in range(n)] + counter = Counter(results) + return counter + + # 模拟掷骰子 + def simulate_dice_rolls(n, sides=6): + """模拟掷骰子""" + results = [random.randint(1, sides) for _ in range(n)] + counter = Counter(results) + return counter + + # 蒙特卡洛估算π + def estimate_pi(n): + """用蒙特卡洛方法估算π""" + inside_circle = 0 + for _ in range(n): + x = random.uniform(-1, 1) + y = random.uniform(-1, 1) + if x*x + y*y <= 1: + inside_circle += 1 + return 4 * inside_circle / n + + # 测试模拟 + coin_results = simulate_coin_flips(1000) + print(f" 投硬币1000次: {dict(coin_results)}") + + dice_results = simulate_dice_rolls(600) + print(f" 掷骰子600次: {dict(dice_results)}") + + pi_estimate = estimate_pi(100000) + print(f" 蒙特卡洛估算π (10万次): {pi_estimate:.6f}") + print(f" 真实π值: {3.141592653589793:.6f}") + print(f" 误差: {abs(pi_estimate - 3.141592653589793):.6f}") + + # 7. 随机状态管理 + print("\n7. 随机状态管理:") + + # 保存当前状态 + state = random.getstate() + print(" 保存随机状态") + + # 生成一些随机数 + nums1 = [random.randint(1, 100) for _ in range(5)] + print(f" 第一组随机数: {nums1}") + + # 恢复状态 + random.setstate(state) + print(" 恢复随机状态") + + # 再次生成随机数(应该相同) + nums2 = [random.randint(1, 100) for _ in range(5)] + print(f" 第二组随机数: {nums2}") + print(f" 两组是否相同: {nums1 == nums2}") + + # 8. 实用随机工具 + print("\n8. 实用随机工具:") + + def random_date(start_year=2020, end_year=2024): + """生成随机日期""" + import datetime + start_date = datetime.date(start_year, 1, 1) + end_date = datetime.date(end_year, 12, 31) + + time_between = end_date - start_date + days_between = time_between.days + random_days = random.randrange(days_between) + + return start_date + datetime.timedelta(days=random_days) + + def weighted_choice(choices, weights): + """带权重的选择(自定义实现)""" + total = sum(weights) + r = random.uniform(0, total) + upto = 0 + for choice, weight in zip(choices, weights): + if upto + weight >= r: + return choice + upto += weight + return choices[-1] + + def random_color(): + """生成随机颜色(RGB)""" + return { + 'r': random.randint(0, 255), + 'g': random.randint(0, 255), + 'b': random.randint(0, 255), + 'hex': f"#{random.randint(0, 255):02x}{random.randint(0, 255):02x}{random.randint(0, 255):02x}" + } + + # 测试工具函数 + print(" 实用工具示例:") + print(f" 随机日期: {random_date()}") + + choices = ['A', 'B', 'C', 'D'] + weights = [1, 2, 3, 4] + print(f" 带权重选择: {weighted_choice(choices, weights)}") + + color = random_color() + print(f" 随机颜色: RGB({color['r']}, {color['g']}, {color['b']}) = {color['hex']}") + +# 运行random模块演示 +random_module_demo() +``` + +--- + +## 六、网络编程模块 + +### 6.1 urllib模块 - URL处理 + +```python +import urllib.request +import urllib.parse +import urllib.error +from urllib.robotparser import RobotFileParser + +def urllib_module_demo(): + """urllib模块演示""" + print("=== urllib模块演示 ===") + + # 1. URL解析和构建 + print("\n1. URL解析和构建:") + + # 解析URL + test_urls = [ + 'https://www.example.com:8080/path/to/page?param1=value1¶m2=value2#section1', + 'http://user:pass@localhost:3000/api/data', + 'ftp://files.example.com/downloads/file.zip' + ] + + for url in test_urls: + parsed = urllib.parse.urlparse(url) + print(f"\n URL: {url}") + print(f" 协议: {parsed.scheme}") + print(f" 主机: {parsed.netloc}") + print(f" 路径: {parsed.path}") + print(f" 查询: {parsed.query}") + print(f" 片段: {parsed.fragment}") + + # 分解netloc + if parsed.netloc: + netloc_parts = urllib.parse.urlsplit(url) + print(f" 用户名: {netloc_parts.username}") + print(f" 密码: {netloc_parts.password}") + print(f" 主机名: {netloc_parts.hostname}") + print(f" 端口: {netloc_parts.port}") + + # 构建URL + print("\n 构建URL:") + url_parts = { + 'scheme': 'https', + 'netloc': 'api.example.com', + 'path': '/v1/users', + 'params': '', + 'query': 'page=1&limit=10', + 'fragment': '' + } + + constructed_url = urllib.parse.urlunparse(url_parts.values()) + print(f" 构建的URL: {constructed_url}") + + # 2. 查询字符串处理 + print("\n2. 查询字符串处理:") + + # 解析查询字符串 + query_string = "name=张三&age=25&city=北京&hobby=编程&hobby=阅读" + parsed_query = urllib.parse.parse_qs(query_string) + print(f" 查询字符串: {query_string}") + print(f" 解析结果: {parsed_query}") + + # 构建查询字符串 + query_dict = { + 'search': '机器学习', + 'category': 'technology', + 'sort': 'date', + 'page': 1 + } + + encoded_query = urllib.parse.urlencode(query_dict) + print(f"\n 查询字典: {query_dict}") + print(f" 编码结果: {encoded_query}") + + # 3. URL编码和解码 + print("\n3. URL编码和解码:") + + # URL编码 + test_strings = [ + '你好世界', + 'hello world!', + 'user@example.com', + 'path/to/file with spaces.txt' + ] + + for text in test_strings: + encoded = urllib.parse.quote(text) + decoded = urllib.parse.unquote(encoded) + print(f" 原文: {text}") + print(f" 编码: {encoded}") + print(f" 解码: {decoded}") + print() + + # 4. HTTP请求(基础) + print("\n4. HTTP请求示例:") + + def safe_request(url, timeout=10): + """安全的HTTP请求""" + try: + # 创建请求对象 + req = urllib.request.Request(url) + req.add_header('User-Agent', 'Python-urllib/3.x') + + # 发送请求 + with urllib.request.urlopen(req, timeout=timeout) as response: + # 获取响应信息 + info = { + 'status': response.getcode(), + 'headers': dict(response.headers), + 'url': response.geturl(), + 'content_length': len(response.read()) + } + return info + + except urllib.error.HTTPError as e: + return {'error': f'HTTP错误: {e.code} - {e.reason}'} + except urllib.error.URLError as e: + return {'error': f'URL错误: {e.reason}'} + except Exception as e: + return {'error': f'其他错误: {e}'} + + # 测试请求(使用公共API) + test_api_url = "https://httpbin.org/get" + print(f" 测试URL: {test_api_url}") + + result = safe_request(test_api_url) + if 'error' in result: + print(f" 请求失败: {result['error']}") + else: + print(f" 状态码: {result['status']}") + print(f" 内容长度: {result['content_length']} 字节") + print(f" 部分响应头:") + for key, value in list(result['headers'].items())[:3]: + print(f" {key}: {value}") + + # 5. 实用URL工具函数 + print("\n5. 实用URL工具函数:") + + def is_valid_url(url): + """检查URL是否有效""" + try: + result = urllib.parse.urlparse(url) + return all([result.scheme, result.netloc]) + except: + return False + + def join_url(base, path): + """连接URL路径""" + return urllib.parse.urljoin(base, path) + + def extract_domain(url): + """提取域名""" + try: + parsed = urllib.parse.urlparse(url) + return parsed.netloc.split(':')[0] # 移除端口 + except: + return None + + def add_query_params(url, params): + """向URL添加查询参数""" + parsed = urllib.parse.urlparse(url) + query_dict = urllib.parse.parse_qs(parsed.query) + query_dict.update(params) + + # 重新构建查询字符串 + new_query = urllib.parse.urlencode(query_dict, doseq=True) + + # 重新构建URL + new_parsed = parsed._replace(query=new_query) + return urllib.parse.urlunparse(new_parsed) + + # 测试工具函数 + test_cases = [ + 'https://www.example.com', + 'invalid-url', + 'http://localhost:8080/api' + ] + + print(" URL有效性检查:") + for url in test_cases: + valid = is_valid_url(url) + print(f" {url}: {'有效' if valid else '无效'}") + + print("\n URL路径连接:") + base_url = "https://api.example.com/v1" + paths = ["/users", "posts/123", "../admin/settings"] + + for path in paths: + joined = join_url(base_url, path) + print(f" {base_url} + {path} = {joined}") + + print("\n 域名提取:") + for url in test_urls: + domain = extract_domain(url) + print(f" {url} -> {domain}") + + print("\n 添加查询参数:") + original_url = "https://search.example.com?q=python" + new_params = {'page': 2, 'sort': 'date'} + modified_url = add_query_params(original_url, new_params) + print(f" 原URL: {original_url}") + print(f" 新参数: {new_params}") + print(f" 修改后: {modified_url}") + +# 运行urllib模块演示 +urllib_module_demo() +``` + +--- + +## 七、文件处理模块 + +### 7.1 pathlib模块 - 现代路径处理 + +```python +from pathlib import Path +import os +import tempfile +import shutil + +def pathlib_module_demo(): + """pathlib模块演示""" + print("=== pathlib模块演示 ===") + + # 1. 路径创建和基本操作 + print("\n1. 路径创建和基本操作:") + + # 创建路径对象 + current_dir = Path.cwd() # 当前工作目录 + home_dir = Path.home() # 用户主目录 + + print(f" 当前目录: {current_dir}") + print(f" 用户主目录: {home_dir}") + + # 路径构建 + project_path = Path("projects") / "my_app" / "src" / "main.py" + print(f" 构建的路径: {project_path}") + + # 绝对路径和相对路径 + abs_path = project_path.resolve() + print(f" 绝对路径: {abs_path}") + print(f" 是否绝对路径: {project_path.is_absolute()}") + + # 2. 路径属性和信息 + print("\n2. 路径属性和信息:") + + test_path = Path("documents/reports/annual_report_2024.pdf") + + path_info = { + '完整路径': str(test_path), + '文件名': test_path.name, + '文件名(无扩展名)': test_path.stem, + '扩展名': test_path.suffix, + '所有扩展名': test_path.suffixes, + '父目录': test_path.parent, + '所有父目录': list(test_path.parents), + '路径部分': test_path.parts, + '锚点': test_path.anchor + } + + for key, value in path_info.items(): + print(f" {key}: {value}") + + # 3. 路径操作方法 + print("\n3. 路径操作方法:") + + # 路径连接 + base = Path("data") + sub_paths = ["users", "profiles", "user_123.json"] + + full_path = base + for part in sub_paths: + full_path = full_path / part + + print(f" 路径连接: {full_path}") + + # 路径替换 + original = Path("backup/2023/data.txt") + new_year = original.with_name("2024").parent / "data.txt" + new_ext = original.with_suffix(".json") + new_stem = original.with_stem("backup_data") + + print(f" 原路径: {original}") + print(f" 替换年份: {new_year}") + print(f" 替换扩展名: {new_ext}") + print(f" 替换文件名: {new_stem}") + + # 4. 文件系统操作 + print("\n4. 文件系统操作:") + + # 创建临时目录进行演示 + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + print(f" 临时目录: {temp_path}") + + # 创建目录结构 + project_dir = temp_path / "test_project" + src_dir = project_dir / "src" + docs_dir = project_dir / "docs" + + # 创建目录 + src_dir.mkdir(parents=True, exist_ok=True) + docs_dir.mkdir(parents=True, exist_ok=True) + + print(f" 创建目录: {src_dir}") + print(f" 创建目录: {docs_dir}") + + # 创建文件 + main_file = src_dir / "main.py" + readme_file = project_dir / "README.md" + + main_file.write_text("print('Hello, World!')\n", encoding='utf-8') + readme_file.write_text("# Test Project\n\nThis is a test project.\n", encoding='utf-8') + + print(f" 创建文件: {main_file}") + print(f" 创建文件: {readme_file}") + + # 文件信息 + if main_file.exists(): + stat = main_file.stat() + print(f"\n 文件信息 ({main_file.name}):") + print(f" 大小: {stat.st_size} 字节") + print(f" 修改时间: {stat.st_mtime}") + print(f" 是否文件: {main_file.is_file()}") + print(f" 是否目录: {main_file.is_dir()}") + + # 读取文件内容 + content = main_file.read_text(encoding='utf-8') + print(f"\n 文件内容: {repr(content)}") + + # 5. 目录遍历 + print("\n5. 目录遍历:") + + # 列出目录内容 + print(f" 项目目录内容:") + for item in project_dir.iterdir(): + item_type = "目录" if item.is_dir() else "文件" + print(f" {item_type}: {item.name}") + + # 递归查找文件 + print(f"\n 递归查找所有.py文件:") + for py_file in project_dir.rglob("*.py"): + print(f" {py_file.relative_to(project_dir)}") + + # 模式匹配 + print(f"\n 模式匹配示例:") + test_files = [ + "main.py", + "test_main.py", + "utils.py", + "README.md", + "config.json" + ] + + patterns = ["*.py", "test_*", "*.md"] + + for pattern in patterns: + matches = [f for f in test_files if Path(f).match(pattern)] + print(f" 模式 '{pattern}': {matches}") + + # 6. 实用路径工具函数 + print("\n6. 实用路径工具函数:") + + def ensure_directory(path): + """确保目录存在""" + path = Path(path) + path.mkdir(parents=True, exist_ok=True) + return path + + def safe_file_name(name): + """生成安全的文件名""" + # 移除或替换不安全字符 + unsafe_chars = '<>:"/\\|?*' + safe_name = name + for char in unsafe_chars: + safe_name = safe_name.replace(char, '_') + return safe_name + + def get_unique_filename(directory, base_name, extension): + """获取唯一文件名""" + directory = Path(directory) + counter = 0 + + while True: + if counter == 0: + filename = f"{base_name}{extension}" + else: + filename = f"{base_name}_{counter}{extension}" + + full_path = directory / filename + if not full_path.exists(): + return full_path + counter += 1 + + def calculate_directory_size(directory): + """计算目录大小""" + directory = Path(directory) + total_size = 0 + + for file_path in directory.rglob('*'): + if file_path.is_file(): + total_size += file_path.stat().st_size + + return total_size + + def find_files_by_extension(directory, extension): + """按扩展名查找文件""" + directory = Path(directory) + pattern = f"*.{extension.lstrip('.')}" + return list(directory.rglob(pattern)) + + # 测试工具函数 + print(" 路径工具函数测试:") + + # 安全文件名 + unsafe_name = "report<2024>:data/analysis.txt" + safe_name = safe_file_name(unsafe_name) + print(f" 不安全文件名: {unsafe_name}") + print(f" 安全文件名: {safe_name}") + + # 当前目录大小 + current_size = calculate_directory_size(Path.cwd()) + print(f" 当前目录大小: {current_size:,} 字节") + + # 查找Python文件 + py_files = find_files_by_extension(Path.cwd(), '.py') + print(f" 找到 {len(py_files)} 个Python文件") + + # 7. 跨平台路径处理 + print("\n7. 跨平台路径处理:") + + # 路径分隔符 + print(f" 当前系统路径分隔符: {os.sep}") + print(f" pathlib自动处理分隔符") + + # 不同平台的路径示例 + unix_style = "home/user/documents/file.txt" + windows_style = "C:\\Users\\User\\Documents\\file.txt" + + unix_path = Path(unix_style) + windows_path = Path(windows_style) + + print(f" Unix风格路径: {unix_path}") + print(f" Windows风格路径: {windows_path}") + print(f" pathlib统一处理: {unix_path.parts}") + +# 运行pathlib模块演示 +pathlib_module_demo() +``` + +### 7.2 shutil模块 - 高级文件操作 + +```python +import shutil +import tempfile +from pathlib import Path +import zipfile +import tarfile + +def shutil_module_demo(): + """shutil模块演示""" + print("=== shutil模块演示 ===") + + # 创建临时目录进行演示 + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + print(f"临时目录: {temp_path}") + + # 1. 文件复制操作 + print("\n1. 文件复制操作:") + + # 创建源文件 + source_file = temp_path / "source.txt" + source_file.write_text("这是源文件的内容\n包含多行文本\n用于测试复制功能", encoding='utf-8') + + # 复制文件(保留元数据) + dest1 = temp_path / "copy1.txt" + shutil.copy2(source_file, dest1) + print(f" copy2复制: {source_file} -> {dest1}") + + # 复制文件(不保留元数据) + dest2 = temp_path / "copy2.txt" + shutil.copy(source_file, dest2) + print(f" copy复制: {source_file} -> {dest2}") + + # 复制文件内容(不复制权限) + dest3 = temp_path / "copy3.txt" + shutil.copyfile(source_file, dest3) + print(f" copyfile复制: {source_file} -> {dest3}") + + # 验证复制结果 + for dest in [dest1, dest2, dest3]: + if dest.exists(): + size = dest.stat().st_size + print(f" {dest.name}: {size} 字节") + + # 2. 目录复制操作 + print("\n2. 目录复制操作:") + + # 创建源目录结构 + source_dir = temp_path / "source_project" + (source_dir / "src").mkdir(parents=True) + (source_dir / "docs").mkdir(parents=True) + (source_dir / "tests").mkdir(parents=True) + + # 创建一些文件 + (source_dir / "README.md").write_text("# 项目说明", encoding='utf-8') + (source_dir / "src" / "main.py").write_text("print('Hello')", encoding='utf-8') + (source_dir / "tests" / "test_main.py").write_text("# 测试文件", encoding='utf-8') + + print(f" 创建源目录: {source_dir}") + + # 复制整个目录树 + dest_dir = temp_path / "copied_project" + shutil.copytree(source_dir, dest_dir) + print(f" copytree复制: {source_dir} -> {dest_dir}") + + # 验证目录复制 + def count_files(directory): + return len(list(Path(directory).rglob('*'))) + + source_count = count_files(source_dir) + dest_count = count_files(dest_dir) + print(f" 源目录文件数: {source_count}") + print(f" 目标目录文件数: {dest_count}") + + # 3. 文件移动操作 + print("\n3. 文件移动操作:") + + # 创建要移动的文件 + move_source = temp_path / "to_move.txt" + move_source.write_text("要移动的文件", encoding='utf-8') + + # 移动文件 + move_dest = temp_path / "moved" / "moved_file.txt" + move_dest.parent.mkdir(exist_ok=True) + + shutil.move(move_source, move_dest) + print(f" 移动文件: {move_source} -> {move_dest}") + print(f" 源文件存在: {move_source.exists()}") + print(f" 目标文件存在: {move_dest.exists()}") + + # 4. 文件删除操作 + print("\n4. 文件删除操作:") + + # 创建要删除的目录 + delete_dir = temp_path / "to_delete" + delete_dir.mkdir() + (delete_dir / "file1.txt").write_text("文件1", encoding='utf-8') + (delete_dir / "file2.txt").write_text("文件2", encoding='utf-8') + + print(f" 创建目录: {delete_dir}") + print(f" 目录存在: {delete_dir.exists()}") + + # 删除整个目录树 + shutil.rmtree(delete_dir) + print(f" 删除目录: {delete_dir}") + print(f" 目录存在: {delete_dir.exists()}") + + # 5. 磁盘使用情况 + print("\n5. 磁盘使用情况:") + + # 获取磁盘使用情况 + disk_usage = shutil.disk_usage(temp_path) + + def format_bytes(bytes_value): + """格式化字节数""" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if bytes_value < 1024.0: + return f"{bytes_value:.2f} {unit}" + bytes_value /= 1024.0 + return f"{bytes_value:.2f} PB" + + print(f" 总空间: {format_bytes(disk_usage.total)}") + print(f" 已使用: {format_bytes(disk_usage.used)}") + print(f" 可用空间: {format_bytes(disk_usage.free)}") + print(f" 使用率: {(disk_usage.used / disk_usage.total * 100):.1f}%") + + # 6. 文件查找和过滤 + print("\n6. 文件查找和过滤:") + + def find_files_by_size(directory, min_size=0, max_size=float('inf')): + """按大小查找文件""" + found_files = [] + for file_path in Path(directory).rglob('*'): + if file_path.is_file(): + size = file_path.stat().st_size + if min_size <= size <= max_size: + found_files.append((file_path, size)) + return found_files + + def find_large_files(directory, threshold_mb=1): + """查找大文件""" + threshold_bytes = threshold_mb * 1024 * 1024 + return find_files_by_size(directory, min_size=threshold_bytes) + + def find_empty_files(directory): + """查找空文件""" + return find_files_by_size(directory, max_size=0) + + # 创建不同大小的测试文件 + test_files_dir = temp_path / "test_files" + test_files_dir.mkdir() + + (test_files_dir / "empty.txt").write_text("", encoding='utf-8') + (test_files_dir / "small.txt").write_text("小文件", encoding='utf-8') + (test_files_dir / "medium.txt").write_text("中等文件" * 100, encoding='utf-8') + + # 查找文件 + all_files = find_files_by_size(test_files_dir) + empty_files = find_empty_files(test_files_dir) + + print(f" 测试目录中的所有文件:") + for file_path, size in all_files: + print(f" {file_path.name}: {size} 字节") + + print(f" 空文件: {len(empty_files)} 个") + + # 7. 压缩和解压缩 + print("\n7. 压缩和解压缩:") + + # 创建要压缩的目录 + archive_source = temp_path / "to_archive" + archive_source.mkdir() + + for i in range(3): + file_path = archive_source / f"file_{i}.txt" + file_path.write_text(f"这是文件 {i} 的内容\n" * 10, encoding='utf-8') + + # 创建ZIP压缩包 + zip_path = temp_path / "archive.zip" + shutil.make_archive(str(zip_path.with_suffix('')), 'zip', archive_source) + print(f" 创建ZIP压缩包: {zip_path}") + + # 创建TAR压缩包 + tar_path = temp_path / "archive.tar.gz" + shutil.make_archive(str(tar_path.with_suffix('').with_suffix('')), 'gztar', archive_source) + print(f" 创建TAR压缩包: {tar_path}") + + # 解压缩 + extract_dir = temp_path / "extracted" + extract_dir.mkdir() + + shutil.unpack_archive(zip_path, extract_dir / "from_zip") + print(f" 解压ZIP到: {extract_dir / 'from_zip'}") + + # 验证解压结果 + extracted_files = list((extract_dir / "from_zip").rglob('*')) + print(f" 解压后文件数: {len(extracted_files)}") + + # 8. 实用文件操作工具 + print("\n8. 实用文件操作工具:") + + def backup_file(file_path, backup_dir=None): + """备份文件""" + file_path = Path(file_path) + if not file_path.exists(): + return None + + if backup_dir is None: + backup_dir = file_path.parent / "backup" + else: + backup_dir = Path(backup_dir) + + backup_dir.mkdir(exist_ok=True) + + # 生成备份文件名(包含时间戳) + import datetime + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_name = f"{file_path.stem}_{timestamp}{file_path.suffix}" + backup_path = backup_dir / backup_name + + shutil.copy2(file_path, backup_path) + return backup_path + + def sync_directories(source, destination, delete_extra=False): + """同步目录""" + source = Path(source) + destination = Path(destination) + + if not source.exists(): + return False + + # 确保目标目录存在 + destination.mkdir(parents=True, exist_ok=True) + + # 复制新文件和更新的文件 + for src_file in source.rglob('*'): + if src_file.is_file(): + rel_path = src_file.relative_to(source) + dst_file = destination / rel_path + + # 确保目标目录存在 + dst_file.parent.mkdir(parents=True, exist_ok=True) + + # 检查是否需要复制 + need_copy = True + if dst_file.exists(): + src_stat = src_file.stat() + dst_stat = dst_file.stat() + # 比较修改时间和大小 + if (src_stat.st_mtime <= dst_stat.st_mtime and + src_stat.st_size == dst_stat.st_size): + need_copy = False + + if need_copy: + shutil.copy2(src_file, dst_file) + + return True + + # 测试工具函数 + test_file = temp_path / "test_backup.txt" + test_file.write_text("测试备份功能", encoding='utf-8') + + backup_path = backup_file(test_file) + print(f" 备份文件: {test_file} -> {backup_path}") + + # 测试目录同步 + sync_source = temp_path / "sync_source" + sync_dest = temp_path / "sync_dest" + + sync_source.mkdir() + (sync_source / "sync_test.txt").write_text("同步测试", encoding='utf-8') + + sync_result = sync_directories(sync_source, sync_dest) + print(f" 目录同步结果: {sync_result}") + print(f" 同步后目标目录存在: {(sync_dest / 'sync_test.txt').exists()}") + +# 运行shutil模块演示 +shutil_module_demo() +``` + +--- + +## 八、实用工具模块 + +### 8.1 collections模块 - 特殊容器 + +```python +from collections import ( + Counter, defaultdict, OrderedDict, deque, + namedtuple, ChainMap, UserDict, UserList +) +import heapq + +def collections_module_demo(): + """collections模块演示""" + print("=== collections模块演示 ===") + + # 1. Counter - 计数器 + print("\n1. Counter - 计数器:") + + # 基本计数 + text = "hello world python programming" + char_count = Counter(text) + word_count = Counter(text.split()) + + print(f" 文本: {text}") + print(f" 字符计数: {dict(char_count.most_common(5))}") + print(f" 单词计数: {dict(word_count)}") + + # 列表计数 + numbers = [1, 2, 3, 2, 1, 3, 1, 4, 5, 1] + num_count = Counter(numbers) + + print(f"\n 数字列表: {numbers}") + print(f" 数字计数: {dict(num_count)}") + print(f" 最常见的3个: {num_count.most_common(3)}") + print(f" 总计数: {sum(num_count.values())}") + + # Counter运算 + counter1 = Counter(['a', 'b', 'c', 'a', 'b']) + counter2 = Counter(['a', 'b', 'b', 'd']) + + print(f"\n Counter1: {dict(counter1)}") + print(f" Counter2: {dict(counter2)}") + print(f" 相加: {dict(counter1 + counter2)}") + print(f" 相减: {dict(counter1 - counter2)}") + print(f" 交集: {dict(counter1 & counter2)}") + print(f" 并集: {dict(counter1 | counter2)}") + + # 2. defaultdict - 默认字典 + print("\n2. defaultdict - 默认字典:") + + # 基本使用 + dd_list = defaultdict(list) + dd_int = defaultdict(int) + dd_set = defaultdict(set) + + # 分组数据 + students = [ + ('张三', '数学', 95), + ('李四', '数学', 87), + ('张三', '英语', 92), + ('王五', '数学', 78), + ('李四', '英语', 89) + ] + + # 按学科分组 + subject_scores = defaultdict(list) + # 按学生分组 + student_scores = defaultdict(dict) + + for name, subject, score in students: + subject_scores[subject].append((name, score)) + student_scores[name][subject] = score + + print(f" 按学科分组:") + for subject, scores in subject_scores.items(): + print(f" {subject}: {scores}") + + print(f"\n 按学生分组:") + for name, scores in student_scores.items(): + print(f" {name}: {dict(scores)}") + + # 计数应用 + word_positions = defaultdict(list) + sentence = "the quick brown fox jumps over the lazy dog" + + for i, word in enumerate(sentence.split()): + word_positions[word].append(i) + + print(f"\n 单词位置索引:") + for word, positions in word_positions.items(): + print(f" '{word}': {positions}") + + # 3. OrderedDict - 有序字典 + print("\n3. OrderedDict - 有序字典:") + + # 创建有序字典 + od = OrderedDict() + od['first'] = 1 + od['second'] = 2 + od['third'] = 3 + + print(f" 有序字典: {list(od.items())}") + + # 移动到末尾 + od.move_to_end('first') + print(f" 移动'first'到末尾: {list(od.items())}") + + # 移动到开头 + od.move_to_end('third', last=False) + print(f" 移动'third'到开头: {list(od.items())}") + + # LRU缓存实现 + class LRUCache: + def __init__(self, capacity): + self.capacity = capacity + self.cache = OrderedDict() + + def get(self, key): + if key in self.cache: + # 移动到末尾(最近使用) + self.cache.move_to_end(key) + return self.cache[key] + return None + + def put(self, key, value): + if key in self.cache: + # 更新并移动到末尾 + self.cache[key] = value + self.cache.move_to_end(key) + else: + # 检查容量 + if len(self.cache) >= self.capacity: + # 删除最久未使用的(第一个) + self.cache.popitem(last=False) + self.cache[key] = value + + def items(self): + return list(self.cache.items()) + + # 测试LRU缓存 + lru = LRUCache(3) + operations = [ + ('put', 'a', 1), + ('put', 'b', 2), + ('put', 'c', 3), + ('get', 'a', None), + ('put', 'd', 4), # 应该删除'b' + ('get', 'b', None), + ('items', None, None) + ] + + print(f"\n LRU缓存测试:") + for op, key, value in operations: + if op == 'put': + lru.put(key, value) + print(f" put({key}, {value}): {lru.items()}") + elif op == 'get': + result = lru.get(key) + print(f" get({key}): {result}, cache: {lru.items()}") + elif op == 'items': + print(f" 最终缓存: {lru.items()}") + + # 4. deque - 双端队列 + print("\n4. deque - 双端队列:") + + # 基本操作 + dq = deque(['a', 'b', 'c']) + print(f" 初始队列: {list(dq)}") + + # 两端添加 + dq.appendleft('left') + dq.append('right') + print(f" 两端添加后: {list(dq)}") + + # 两端删除 + left_item = dq.popleft() + right_item = dq.pop() + print(f" 删除的元素: 左={left_item}, 右={right_item}") + print(f" 删除后: {list(dq)}") + + # 旋转 + dq.rotate(1) # 向右旋转 + print(f" 向右旋转1位: {list(dq)}") + + dq.rotate(-2) # 向左旋转 + print(f" 向左旋转2位: {list(dq)}") + + # 限制长度的队列 + limited_dq = deque(maxlen=3) + for i in range(5): + limited_dq.append(i) + print(f" 添加{i}: {list(limited_dq)}") + + # 滑动窗口应用 + def moving_average(data, window_size): + """计算移动平均值""" + window = deque(maxlen=window_size) + averages = [] + + for value in data: + window.append(value) + if len(window) == window_size: + avg = sum(window) / window_size + averages.append(avg) + + return averages + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + window_size = 3 + averages = moving_average(data, window_size) + + print(f"\n 数据: {data}") + print(f" 窗口大小: {window_size}") + print(f" 移动平均: {averages}") + + # 5. namedtuple - 命名元组 + print("\n5. namedtuple - 命名元组:") + + # 创建命名元组类 + Point = namedtuple('Point', ['x', 'y']) + Person = namedtuple('Person', 'name age city') + + # 创建实例 + p1 = Point(1, 2) + p2 = Point(x=3, y=4) + + person1 = Person('张三', 25, '北京') + person2 = Person(name='李四', age=30, city='上海') + + print(f" 点1: {p1}, x={p1.x}, y={p1.y}") + print(f" 点2: {p2}, x={p2.x}, y={p2.y}") + print(f" 人员1: {person1}") + print(f" 人员2: {person2}") + + # 命名元组方法 + print(f"\n 命名元组方法:") + print(f" 字段名: {Person._fields}") + print(f" 转为字典: {person1._asdict()}") + + # 替换字段 + person1_updated = person1._replace(age=26) + print(f" 替换年龄: {person1_updated}") + + # 从可迭代对象创建 + data = ['王五', 28, '广州'] + person3 = Person._make(data) + print(f" 从列表创建: {person3}") + + # 6. ChainMap - 链式映射 + print("\n6. ChainMap - 链式映射:") + + # 创建多个字典 + dict1 = {'a': 1, 'b': 2} + dict2 = {'b': 3, 'c': 4} + dict3 = {'c': 5, 'd': 6} + + # 创建链式映射 + cm = ChainMap(dict1, dict2, dict3) + + print(f" 字典1: {dict1}") + print(f" 字典2: {dict2}") + print(f" 字典3: {dict3}") + print(f" 链式映射: {dict(cm)}") + + # 查找顺序(从第一个开始) + print(f"\n 查找顺序测试:") + for key in ['a', 'b', 'c', 'd', 'e']: + value = cm.get(key, 'Not Found') + print(f" {key}: {value}") + + # 添加新映射 + dict4 = {'e': 7, 'f': 8} + cm = cm.new_child(dict4) + print(f"\n 添加新映射后: {dict(cm)}") + + # 配置管理应用 + default_config = { + 'host': 'localhost', + 'port': 8080, + 'debug': False, + 'timeout': 30 + } + + user_config = { + 'host': '192.168.1.100', + 'debug': True + } + + env_config = { + 'port': 9000 + } + + # 配置优先级: 环境变量 > 用户配置 > 默认配置 + config = ChainMap(env_config, user_config, default_config) + + print(f"\n 配置管理示例:") + print(f" 默认配置: {default_config}") + print(f" 用户配置: {user_config}") + print(f" 环境配置: {env_config}") + print(f" 最终配置: {dict(config)}") + +# 运行collections模块演示 +collections_module_demo() +``` + +### 8.2 itertools模块 - 迭代器工具 + +```python +import itertools +from itertools import ( + count, cycle, repeat, chain, compress, dropwhile, + filterfalse, groupby, islice, starmap, takewhile, + zip_longest, product, permutations, combinations, + combinations_with_replacement, accumulate +) + +def itertools_module_demo(): + """itertools模块演示""" + print("=== itertools模块演示 ===") + + # 1. 无限迭代器 + print("\n1. 无限迭代器:") + + # count - 计数器 + print(" count - 计数器:") + counter = count(10, 2) # 从10开始,步长为2 + count_result = [next(counter) for _ in range(5)] + print(f" 从10开始步长2: {count_result}") + + # cycle - 循环 + print("\n cycle - 循环:") + colors = cycle(['red', 'green', 'blue']) + cycle_result = [next(colors) for _ in range(8)] + print(f" 循环颜色: {cycle_result}") + + # repeat - 重复 + print("\n repeat - 重复:") + repeat_result = list(repeat('hello', 3)) + print(f" 重复'hello'3次: {repeat_result}") + + # 2. 终止迭代器 + print("\n2. 终止迭代器:") + + # chain - 链接 + print(" chain - 链接:") + list1 = [1, 2, 3] + list2 = ['a', 'b', 'c'] + list3 = [10, 20] + chained = list(chain(list1, list2, list3)) + print(f" 链接列表: {chained}") + + # compress - 压缩过滤 + print("\n compress - 压缩过滤:") + data = ['a', 'b', 'c', 'd', 'e'] + selectors = [1, 0, 1, 0, 1] + compressed = list(compress(data, selectors)) + print(f" 数据: {data}") + print(f" 选择器: {selectors}") + print(f" 压缩结果: {compressed}") + + # dropwhile - 丢弃直到条件为假 + print("\n dropwhile - 丢弃直到条件为假:") + numbers = [1, 3, 5, 2, 4, 6, 7, 8] + dropped = list(dropwhile(lambda x: x % 2 == 1, numbers)) + print(f" 原数据: {numbers}") + print(f" 丢弃奇数直到遇到偶数: {dropped}") + + # takewhile - 取值直到条件为假 + print("\n takewhile - 取值直到条件为假:") + taken = list(takewhile(lambda x: x < 5, numbers)) + print(f" 取值直到>=5: {taken}") + + # filterfalse - 过滤假值 + print("\n filterfalse - 过滤假值:") + mixed_data = [0, 1, '', 'hello', [], [1, 2], None, 42] + filtered = list(filterfalse(bool, mixed_data)) + print(f" 原数据: {mixed_data}") + print(f" 假值: {filtered}") + + # islice - 切片 + print("\n islice - 切片:") + data = range(20) + slice1 = list(islice(data, 5)) # 前5个 + slice2 = list(islice(data, 5, 10)) # 5到10 + slice3 = list(islice(data, 0, 20, 3)) # 步长为3 + print(f" 前5个: {slice1}") + print(f" 5到10: {slice2}") + print(f" 步长3: {slice3}") + + # groupby - 分组 + print("\n groupby - 分组:") + + # 按值分组 + data = [1, 1, 2, 2, 2, 3, 1, 1] + groups = [(k, list(g)) for k, g in groupby(data)] + print(f" 按值分组: {groups}") + + # 按条件分组 + words = ['apple', 'banana', 'cherry', 'date', 'elderberry'] + by_length = [(k, list(g)) for k, g in groupby(words, key=len)] + print(f" 按长度分组: {by_length}") + + # 学生成绩分组 + students = [ + ('张三', 'A'), + ('李四', 'B'), + ('王五', 'A'), + ('赵六', 'B'), + ('钱七', 'A') + ] + + # 按成绩分组(需要先排序) + students_sorted = sorted(students, key=lambda x: x[1]) + grade_groups = {k: [name for name, grade in g] + for k, g in groupby(students_sorted, key=lambda x: x[1])} + + print(f" 学生按成绩分组: {grade_groups}") + + # 3. 组合迭代器 + print("\n3. 组合迭代器:") + + # product - 笛卡尔积 + print(" product - 笛卡尔积:") + colors = ['red', 'blue'] + sizes = ['S', 'M', 'L'] + products = list(product(colors, sizes)) + print(f" 颜色×尺寸: {products}") + + # 自身笛卡尔积 + coords = list(product(range(3), repeat=2)) + print(f" 坐标组合: {coords}") + + # permutations - 排列 + print("\n permutations - 排列:") + letters = ['A', 'B', 'C'] + perms2 = list(permutations(letters, 2)) + perms3 = list(permutations(letters)) + print(f" 2个字母排列: {perms2}") + print(f" 3个字母排列: {perms3}") + + # combinations - 组合 + print("\n combinations - 组合:") + numbers = [1, 2, 3, 4] + combs2 = list(combinations(numbers, 2)) + combs3 = list(combinations(numbers, 3)) + print(f" 2个数字组合: {combs2}") + print(f" 3个数字组合: {combs3}") + + # combinations_with_replacement - 可重复组合 + print("\n combinations_with_replacement - 可重复组合:") + combs_rep = list(combinations_with_replacement([1, 2, 3], 2)) + print(f" 可重复2组合: {combs_rep}") + + # 4. 实用应用示例 + print("\n4. 实用应用示例:") + + # 分批处理 + def batch_process(iterable, batch_size): + """分批处理数据""" + iterator = iter(iterable) + while True: + batch = list(islice(iterator, batch_size)) + if not batch: + break + yield batch + + data = range(23) + batches = list(batch_process(data, 5)) + print(f" 分批处理(每批5个): {batches}") + + # 滑动窗口 + def sliding_window(iterable, window_size): + """滑动窗口""" + iterators = [islice(iterable, i, None) for i in range(window_size)] + return zip(*iterators) + + data = [1, 2, 3, 4, 5, 6, 7] + windows = list(sliding_window(data, 3)) + print(f" 滑动窗口(大小3): {windows}") + + # 扁平化嵌套列表 + def flatten(nested_list): + """扁平化嵌套列表""" + return chain.from_iterable(nested_list) + + nested = [[1, 2], [3, 4, 5], [6], [7, 8, 9]] + flattened = list(flatten(nested)) + print(f" 嵌套列表: {nested}") + print(f" 扁平化: {flattened}") + + # 累积计算 + print("\n accumulate - 累积计算:") + numbers = [1, 2, 3, 4, 5] + + # 累积和 + cumsum = list(accumulate(numbers)) + print(f" 累积和: {cumsum}") + + # 累积乘积 + import operator + cumproduct = list(accumulate(numbers, operator.mul)) + print(f" 累积乘积: {cumproduct}") + + # 累积最大值 + data = [3, 1, 4, 1, 5, 9, 2, 6] + cummax = list(accumulate(data, max)) + print(f" 数据: {data}") + print(f" 累积最大值: {cummax}") + + # 5. 性能优化示例 + print("\n5. 性能优化示例:") + + # 使用itertools优化内存 + def memory_efficient_processing(data): + """内存高效的数据处理""" + # 只处理偶数,取前10个,每个乘以2 + result = islice( + map(lambda x: x * 2, + filter(lambda x: x % 2 == 0, data) + ), + 10 + ) + return list(result) + + large_data = range(1000000) # 模拟大数据 + processed = memory_efficient_processing(large_data) + print(f" 处理大数据前10个偶数×2: {processed}") + + # 生成测试数据 + def generate_test_data(): + """生成测试数据""" + # 生成用户ID和随机分数的组合 + user_ids = cycle(['user1', 'user2', 'user3']) + scores = cycle([85, 92, 78, 95, 88]) + + for i, (user_id, score) in enumerate(zip(user_ids, scores)): + if i >= 10: # 只生成10条 + break + yield f"{user_id}_{i//3}", score + + test_data = list(generate_test_data()) + print(f" 生成的测试数据: {test_data}") + +# 运行itertools模块演示 +itertools_module_demo() +``` + +--- + +## 九、总结与最佳实践 + +### 9.1 模块选择指南 + +```python +def module_selection_guide(): + """模块选择指南""" + print("=== Python内建模块选择指南 ===") + + guide = { + "系统交互": { + "文件操作": ["os", "pathlib", "shutil"], + "系统信息": ["sys", "platform"], + "环境变量": ["os.environ"] + }, + "时间处理": { + "日期时间": ["datetime"], + "时间戳": ["time"], + "性能测量": ["time.perf_counter"] + }, + "数据处理": { + "JSON": ["json"], + "CSV": ["csv"], + "配置文件": ["configparser"] + }, + "数学计算": { + "基础数学": ["math"], + "随机数": ["random"], + "统计": ["statistics"] + }, + "网络编程": { + "URL处理": ["urllib.parse"], + "HTTP请求": ["urllib.request"], + "网络工具": ["socket"] + }, + "数据结构": { + "特殊容器": ["collections"], + "堆队列": ["heapq"], + "双端队列": ["collections.deque"] + }, + "迭代工具": { + "迭代器": ["itertools"], + "函数式编程": ["functools"], + "操作符": ["operator"] + } + } + + for category, subcategories in guide.items(): + print(f"\n{category}:") + for task, modules in subcategories.items(): + print(f" {task}: {', '.join(modules)}") + +# 运行模块选择指南 +module_selection_guide() +``` + +### 9.2 最佳实践 + +```python +def best_practices_demo(): + """最佳实践演示""" + print("=== Python内建模块最佳实践 ===") + + print("\n1. 导入最佳实践:") + print(""" + # ✅ 推荐的导入方式 + import os + import sys + from pathlib import Path + from collections import defaultdict, Counter + from datetime import datetime, timedelta + + # ❌ 避免的导入方式 + from os import * # 污染命名空间 + import datetime as dt # 不必要的别名 + """) + + print("\n2. 错误处理最佳实践:") + + def safe_file_operation(filename): + """安全的文件操作""" + try: + with open(filename, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + print(f"文件 {filename} 不存在") + return None + except PermissionError: + print(f"没有权限访问文件 {filename}") + return None + except UnicodeDecodeError: + print(f"文件 {filename} 编码错误") + return None + except Exception as e: + print(f"读取文件时发生未知错误: {e}") + return None + + def safe_json_operation(data): + """安全的JSON操作""" + import json + try: + return json.dumps(data, ensure_ascii=False, indent=2) + except TypeError as e: + print(f"JSON序列化错误: {e}") + return None + + print(" ✅ 具体异常处理") + print(" ✅ 资源自动清理(with语句)") + print(" ✅ 编码明确指定") + + print("\n3. 性能优化最佳实践:") + + # 使用生成器而不是列表 + def process_large_file_good(filename): + """内存友好的文件处理""" + try: + with open(filename, 'r', encoding='utf-8') as f: + for line in f: # 逐行读取,不加载整个文件 + yield line.strip() + except FileNotFoundError: + return + + def process_large_file_bad(filename): + """内存不友好的文件处理""" + try: + with open(filename, 'r', encoding='utf-8') as f: + return f.readlines() # 一次性加载所有行 + except FileNotFoundError: + return [] + + # 使用适当的数据结构 + from collections import defaultdict, Counter + + def count_words_good(text): + """高效的单词计数""" + return Counter(text.split()) + + def count_words_bad(text): + """低效的单词计数""" + word_count = {} + for word in text.split(): + if word in word_count: + word_count[word] += 1 + else: + word_count[word] = 1 + return word_count + + print(" ✅ 使用生成器处理大数据") + print(" ✅ 选择合适的数据结构") + print(" ✅ 避免重复计算") + + print("\n4. 代码组织最佳实践:") + + class ConfigManager: + """配置管理器""" + def __init__(self, config_file=None): + self.config = {} + if config_file: + self.load_config(config_file) + + def load_config(self, config_file): + """加载配置文件""" + import json + from pathlib import Path + + config_path = Path(config_file) + if not config_path.exists(): + raise FileNotFoundError(f"配置文件不存在: {config_file}") + + try: + with open(config_path, 'r', encoding='utf-8') as f: + self.config = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"配置文件格式错误: {e}") + + def get(self, key, default=None): + """获取配置值""" + return self.config.get(key, default) + + def set(self, key, value): + """设置配置值""" + self.config[key] = value + + def save_config(self, config_file): + """保存配置文件""" + import json + from pathlib import Path + + config_path = Path(config_file) + config_path.parent.mkdir(parents=True, exist_ok=True) + + try: + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + except Exception as e: + raise IOError(f"保存配置文件失败: {e}") + + print(" ✅ 类封装相关功能") + print(" ✅ 明确的方法职责") + print(" ✅ 完善的错误处理") + + print("\n5. 测试友好的代码:") + + def calculate_file_stats(file_path): + """计算文件统计信息""" + from pathlib import Path + + path = Path(file_path) + if not path.exists(): + return None + + stat = path.stat() + return { + 'size': stat.st_size, + 'modified': stat.st_mtime, + 'is_file': path.is_file(), + 'extension': path.suffix + } + + def process_data_with_logging(data, logger=None): + """带日志的数据处理""" + if logger is None: + import logging + logger = logging.getLogger(__name__) + + logger.info(f"开始处理 {len(data)} 条数据") + + processed = [] + for item in data: + try: + # 处理逻辑 + result = item * 2 # 示例处理 + processed.append(result) + except Exception as e: + logger.error(f"处理数据项失败: {item}, 错误: {e}") + + logger.info(f"处理完成,成功 {len(processed)} 条") + return processed + + print(" ✅ 函数职责单一") + print(" ✅ 依赖注入(如logger)") + print(" ✅ 返回值明确") + +# 运行最佳实践演示 +best_practices_demo() +``` + +### 9.3 常见陷阱和解决方案 + +```python +def common_pitfalls_demo(): + """常见陷阱和解决方案""" + print("=== 常见陷阱和解决方案 ===") + + print("\n1. 时间处理陷阱:") + + # ❌ 错误:使用可变默认参数 + def bad_timestamp_function(timestamp=None): + from datetime import datetime + if timestamp is None: + timestamp = datetime.now() # 每次调用都会重新计算 + return timestamp + + # ✅ 正确:避免可变默认参数 + def good_timestamp_function(timestamp=None): + from datetime import datetime + if timestamp is None: + timestamp = datetime.now() + return timestamp + + print(" ✅ 避免在默认参数中使用可变对象") + + # 时区处理 + from datetime import datetime, timezone, timedelta + + # ❌ 错误:忽略时区 + naive_time = datetime.now() + + # ✅ 正确:明确时区 + aware_time = datetime.now(timezone.utc) + local_time = datetime.now(timezone(timedelta(hours=8))) # 北京时间 + + print(" ✅ 明确处理时区信息") + + print("\n2. 文件处理陷阱:") + + # ❌ 错误:不处理编码 + def bad_file_read(filename): + try: + with open(filename, 'r') as f: # 使用系统默认编码 + return f.read() + except: + return None + + # ✅ 正确:明确指定编码 + def good_file_read(filename): + try: + with open(filename, 'r', encoding='utf-8') as f: + return f.read() + except UnicodeDecodeError: + # 尝试其他编码 + try: + with open(filename, 'r', encoding='gbk') as f: + return f.read() + except UnicodeDecodeError: + return None + except FileNotFoundError: + return None + + print(" ✅ 明确指定文件编码") + print(" ✅ 处理编码错误") + + print("\n3. JSON处理陷阱:") + + import json + from datetime import datetime + from decimal import Decimal + + # ❌ 错误:不处理特殊类型 + def bad_json_serialize(data): + return json.dumps(data) # datetime等类型会报错 + + # ✅ 正确:自定义序列化 + def good_json_serialize(data): + def json_serializer(obj): + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, Decimal): + return float(obj) + elif hasattr(obj, '__dict__'): + return obj.__dict__ + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + return json.dumps(data, default=json_serializer, ensure_ascii=False, indent=2) + + # 测试数据 + test_data = { + 'name': '张三', + 'timestamp': datetime.now(), + 'price': Decimal('99.99') + } + + try: + result = good_json_serialize(test_data) + print(f" ✅ 成功序列化: {result[:50]}...") + except Exception as e: + print(f" ❌ 序列化失败: {e}") + + print("\n4. 路径处理陷阱:") + + import os + from pathlib import Path + + # ❌ 错误:硬编码路径分隔符 + def bad_path_join(base, *parts): + return base + '/' + '/'.join(parts) # 在Windows上会有问题 + + # ✅ 正确:使用pathlib或os.path + def good_path_join(base, *parts): + return str(Path(base).joinpath(*parts)) + + # 或者使用os.path + def good_path_join_os(base, *parts): + return os.path.join(base, *parts) + + print(" ✅ 使用pathlib或os.path处理路径") + print(" ✅ 避免硬编码路径分隔符") + + print("\n5. 迭代器陷阱:") + + # ❌ 错误:重复使用迭代器 + def bad_iterator_usage(): + data = iter([1, 2, 3, 4, 5]) + first_sum = sum(data) # 消耗了迭代器 + second_sum = sum(data) # 迭代器已空,结果为0 + return first_sum, second_sum + + # ✅ 正确:重新创建迭代器或使用列表 + def good_iterator_usage(): + data = [1, 2, 3, 4, 5] # 使用列表 + first_sum = sum(data) + second_sum = sum(data) + return first_sum, second_sum + + bad_result = bad_iterator_usage() + good_result = good_iterator_usage() + + print(f" ❌ 错误用法结果: {bad_result}") + print(f" ✅ 正确用法结果: {good_result}") + + print("\n6. 内存使用陷阱:") + + # ❌ 错误:一次性加载大量数据 + def bad_large_data_processing(filename): + with open(filename, 'r', encoding='utf-8') as f: + lines = f.readlines() # 全部加载到内存 + + processed = [] + for line in lines: + if line.strip(): # 处理非空行 + processed.append(line.strip().upper()) + + return processed + + # ✅ 正确:流式处理 + def good_large_data_processing(filename): + def process_lines(): + with open(filename, 'r', encoding='utf-8') as f: + for line in f: # 逐行处理 + if line.strip(): + yield line.strip().upper() + + return list(process_lines()) # 只在需要时转换为列表 + + print(" ✅ 使用生成器处理大数据") + print(" ✅ 避免一次性加载大量数据") + +# 运行常见陷阱演示 +common_pitfalls_demo() +``` + +### 9.4 学习建议 + +```python +def learning_suggestions(): + """学习建议""" + print("=== Python内建模块学习建议 ===") + + suggestions = { + "初学者阶段": { + "必学模块": ["os", "sys", "datetime", "json", "math", "random"], + "学习重点": [ + "掌握基本的文件操作", + "理解时间日期处理", + "学会JSON数据处理", + "熟悉数学和随机数操作" + ], + "实践项目": [ + "文件管理工具", + "日志分析器", + "数据转换工具", + "简单的配置管理" + ] + }, + "进阶阶段": { + "必学模块": ["pathlib", "collections", "itertools", "functools", "urllib"], + "学习重点": [ + "现代化的路径处理", + "高效的数据结构使用", + "迭代器和生成器优化", + "网络编程基础" + ], + "实践项目": [ + "数据处理管道", + "网络爬虫", + "性能监控工具", + "批处理系统" + ] + }, + "高级阶段": { + "必学模块": ["asyncio", "multiprocessing", "threading", "logging", "unittest"], + "学习重点": [ + "异步编程模式", + "并发和并行处理", + "完善的日志系统", + "测试驱动开发" + ], + "实践项目": [ + "高并发服务器", + "分布式系统", + "微服务架构", + "企业级应用" + ] + } + } + + for stage, content in suggestions.items(): + print(f"\n{stage}:") + for category, items in content.items(): + print(f" {category}:") + for item in items: + print(f" • {item}") + + print("\n学习方法建议:") + methods = [ + "📚 阅读官方文档 - 最权威的学习资源", + "💻 动手实践 - 编写小项目巩固知识", + "🔍 源码阅读 - 理解模块内部实现", + "🤝 社区交流 - 参与开源项目和讨论", + "📝 总结记录 - 建立个人知识库", + "🎯 项目驱动 - 通过实际需求学习", + "⚡ 性能测试 - 了解不同方法的效率", + "🐛 错误调试 - 从错误中学习经验" + ] + + for method in methods: + print(f" {method}") + + print("\n推荐资源:") + resources = { + "官方文档": "https://docs.python.org/3/library/", + "在线教程": "Real Python, Python.org Tutorial", + "书籍推荐": "《Python标准库》、《Effective Python》", + "实践平台": "LeetCode, HackerRank, Codewars", + "开源项目": "GitHub上的Python项目" + } + + for category, resource in resources.items(): + print(f" {category}: {resource}") + +# 运行学习建议 +learning_suggestions() +``` + +--- + +## 总结 + +通过本章的学习,我们深入了解了Python的常用内建模块,包括: + +### 核心收获 + +1. **系统交互模块** - 掌握了`os`、`sys`、`platform`模块的使用 +2. **时间处理模块** - 学会了`datetime`和`time`模块的时间操作 +3. **数据处理模块** - 熟悉了`json`、`csv`模块的数据处理 +4. **数学计算模块** - 了解了`math`、`random`模块的数学运算 +5. **网络编程模块** - 掌握了`urllib`模块的URL处理 +6. **文件处理模块** - 学会了`pathlib`、`shutil`模块的文件操作 +7. **实用工具模块** - 熟悉了`collections`、`itertools`模块的高级功能 + +### 关键技能 + +- ✅ 能够选择合适的模块解决具体问题 +- ✅ 掌握模块的核心功能和最佳实践 +- ✅ 了解常见陷阱和解决方案 +- ✅ 具备编写高质量、可维护代码的能力 + +### 下一步学习 + +1. **第三方库学习** - requests、pandas、numpy等 +2. **框架学习** - Django、Flask、FastAPI等 +3. **专业领域** - 数据科学、机器学习、Web开发等 +4. **高级主题** - 异步编程、并发处理、性能优化等 + +掌握Python内建模块是成为Python高手的重要基础,继续保持学习和实践的热情! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/16.md b/docs/Python/16.md new file mode 100644 index 000000000..e7b2bb0b7 --- /dev/null +++ b/docs/Python/16.md @@ -0,0 +1,3068 @@ +--- +title: 第16天-常用第三方模块 +author: 哪吒 +date: '2023-06-15' +--- + +# 第16天-常用第三方模块 + +## 学习目标 + +通过本章学习,你将掌握: + +- 第三方模块的安装和管理 +- requests模块进行HTTP请求 +- pandas进行数据分析和处理 +- numpy进行数值计算 +- matplotlib进行数据可视化 +- 其他常用第三方模块的使用 +- 虚拟环境的创建和管理 +- 包管理的最佳实践 + +--- + +## 一、第三方模块概述 + +### 1.1 什么是第三方模块 + +```python +def third_party_modules_intro(): + """第三方模块介绍""" + print("=== 第三方模块概述 ===") + + concepts = { + "定义": "由Python社区开发的扩展库,不包含在Python标准库中", + "特点": [ + "功能强大且专业化", + "活跃的社区支持", + "持续更新和维护", + "丰富的文档和示例" + ], + "优势": [ + "避免重复造轮子", + "提高开发效率", + "获得专业级功能", + "学习最佳实践" + ], + "分类": { + "网络请求": ["requests", "urllib3", "httpx"], + "数据分析": ["pandas", "numpy", "scipy"], + "数据可视化": ["matplotlib", "seaborn", "plotly"], + "Web框架": ["Django", "Flask", "FastAPI"], + "机器学习": ["scikit-learn", "tensorflow", "pytorch"], + "图像处理": ["Pillow", "opencv-python", "imageio"], + "数据库": ["SQLAlchemy", "pymongo", "redis"], + "测试工具": ["pytest", "unittest2", "mock"] + } + } + + print(f"\n定义: {concepts['定义']}") + + print("\n特点:") + for feature in concepts['特点']: + print(f" • {feature}") + + print("\n优势:") + for advantage in concepts['优势']: + print(f" • {advantage}") + + print("\n常用分类:") + for category, modules in concepts['分类'].items(): + print(f" {category}: {', '.join(modules)}") + +# 运行第三方模块介绍 +third_party_modules_intro() +``` + +### 1.2 包管理工具 + +```python +def package_management_demo(): + """包管理工具演示""" + print("=== 包管理工具 ===") + + # pip基本命令 + pip_commands = { + "安装包": [ + "pip install package_name", + "pip install package_name==1.0.0 # 指定版本", + "pip install package_name>=1.0.0 # 最低版本", + "pip install -r requirements.txt # 从文件安装" + ], + "查看包": [ + "pip list # 列出所有已安装的包", + "pip show package_name # 显示包详细信息", + "pip search package_name # 搜索包(已废弃)" + ], + "升级包": [ + "pip install --upgrade package_name", + "pip install -U package_name # 简写", + "pip list --outdated # 查看过期包" + ], + "卸载包": [ + "pip uninstall package_name", + "pip uninstall -r requirements.txt" + ], + "导出依赖": [ + "pip freeze > requirements.txt", + "pip freeze --local > requirements.txt # 只导出本地包" + ] + } + + for category, commands in pip_commands.items(): + print(f"\n{category}:") + for cmd in commands: + print(f" {cmd}") + + # requirements.txt示例 + print("\nrequirements.txt示例:") + requirements_example = """ +# Web开发 +Django==4.2.0 +Flask>=2.0.0 +requests==2.31.0 + +# 数据分析 +pandas>=1.5.0 +numpy>=1.24.0 +matplotlib>=3.6.0 + +# 机器学习 +scikit-learn>=1.2.0 +tensorflow>=2.12.0 + +# 开发工具 +pytest>=7.0.0 +black>=23.0.0 +flake8>=6.0.0 + """ + print(requirements_example) + + # 虚拟环境管理 + print("\n虚拟环境管理:") + venv_commands = [ + "# 创建虚拟环境", + "python -m venv myenv", + "python -m venv --system-site-packages myenv # 继承系统包", + "", + "# 激活虚拟环境", + "# Windows:", + "myenv\\Scripts\\activate", + "# Linux/Mac:", + "source myenv/bin/activate", + "", + "# 停用虚拟环境", + "deactivate", + "", + "# 删除虚拟环境", + "rm -rf myenv # Linux/Mac", + "rmdir /s myenv # Windows" + ] + + for cmd in venv_commands: + print(f" {cmd}") + +# 运行包管理演示 +package_management_demo() +``` + +--- + +## 二、requests模块 - HTTP请求 + +### 2.1 基本HTTP请求 + +```python +# 首先需要安装: pip install requests +import requests +import json +from urllib.parse import urljoin + +def requests_basic_demo(): + """requests基本使用演示""" + print("=== requests基本HTTP请求 ===") + + # 1. GET请求 + print("\n1. GET请求:") + + try: + # 基本GET请求 + response = requests.get('https://httpbin.org/get') + print(f" 状态码: {response.status_code}") + print(f" 响应头: {dict(list(response.headers.items())[:3])}...") + print(f" 响应内容类型: {response.headers.get('content-type')}") + + # 带参数的GET请求 + params = { + 'name': '张三', + 'age': 25, + 'city': '北京' + } + response = requests.get('https://httpbin.org/get', params=params) + data = response.json() + print(f" 请求URL: {data['url']}") + print(f" 查询参数: {data['args']}") + + except requests.exceptions.RequestException as e: + print(f" 请求失败: {e}") + + # 2. POST请求 + print("\n2. POST请求:") + + try: + # 发送JSON数据 + json_data = { + 'username': 'testuser', + 'password': 'testpass', + 'email': 'test@example.com' + } + + response = requests.post( + 'https://httpbin.org/post', + json=json_data, + headers={'Content-Type': 'application/json'} + ) + + result = response.json() + print(f" 发送的JSON: {result['json']}") + print(f" 请求头: {result['headers']['Content-Type']}") + + # 发送表单数据 + form_data = { + 'name': '李四', + 'message': '这是一条测试消息' + } + + response = requests.post( + 'https://httpbin.org/post', + data=form_data + ) + + result = response.json() + print(f" 表单数据: {result['form']}") + + except requests.exceptions.RequestException as e: + print(f" POST请求失败: {e}") + + # 3. 其他HTTP方法 + print("\n3. 其他HTTP方法:") + + methods = { + 'PUT': lambda: requests.put('https://httpbin.org/put', json={'data': 'updated'}), + 'DELETE': lambda: requests.delete('https://httpbin.org/delete'), + 'PATCH': lambda: requests.patch('https://httpbin.org/patch', json={'field': 'patched'}), + 'HEAD': lambda: requests.head('https://httpbin.org/get'), + 'OPTIONS': lambda: requests.options('https://httpbin.org/get') + } + + for method_name, method_func in methods.items(): + try: + response = method_func() + print(f" {method_name}: 状态码 {response.status_code}") + except requests.exceptions.RequestException as e: + print(f" {method_name}: 请求失败 {e}") + +# 运行requests基本演示 +requests_basic_demo() +``` + +### 2.2 高级功能 + +```python +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry +import time + +def requests_advanced_demo(): + """requests高级功能演示""" + print("=== requests高级功能 ===") + + # 1. 会话管理 + print("\n1. 会话管理:") + + # 创建会话 + session = requests.Session() + + # 设置默认头部 + session.headers.update({ + 'User-Agent': 'MyApp/1.0', + 'Accept': 'application/json' + }) + + try: + # 使用会话发送请求 + response = session.get('https://httpbin.org/headers') + headers_info = response.json() + print(f" 会话头部: {headers_info['headers']['User-Agent']}") + + # 会话中的Cookie会自动保持 + session.get('https://httpbin.org/cookies/set/session_id/12345') + response = session.get('https://httpbin.org/cookies') + cookies_info = response.json() + print(f" 会话Cookie: {cookies_info['cookies']}") + + except requests.exceptions.RequestException as e: + print(f" 会话请求失败: {e}") + finally: + session.close() + + # 2. 超时和重试 + print("\n2. 超时和重试:") + + # 设置超时 + try: + # 连接超时5秒,读取超时10秒 + response = requests.get( + 'https://httpbin.org/delay/2', + timeout=(5, 10) + ) + print(f" 超时请求成功: {response.status_code}") + except requests.exceptions.Timeout: + print(" 请求超时") + except requests.exceptions.RequestException as e: + print(f" 请求失败: {e}") + + # 配置重试策略 + def create_session_with_retry(): + session = requests.Session() + + # 重试策略 + retry_strategy = Retry( + total=3, # 总重试次数 + backoff_factor=1, # 重试间隔 + status_forcelist=[429, 500, 502, 503, 504], # 需要重试的状态码 + ) + + # 添加重试适配器 + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount('http://', adapter) + session.mount('https://', adapter) + + return session + + retry_session = create_session_with_retry() + + try: + response = retry_session.get( + 'https://httpbin.org/status/500', + timeout=10 + ) + print(f" 重试请求结果: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f" 重试后仍失败: {e}") + finally: + retry_session.close() + + # 3. 文件上传和下载 + print("\n3. 文件上传和下载:") + + # 模拟文件上传 + try: + # 创建测试文件内容 + files = { + 'file': ('test.txt', 'Hello, World!', 'text/plain') + } + + response = requests.post( + 'https://httpbin.org/post', + files=files + ) + + result = response.json() + print(f" 上传文件信息: {result['files']}") + + except requests.exceptions.RequestException as e: + print(f" 文件上传失败: {e}") + + # 流式下载 + def download_file_stream(url, filename): + """流式下载文件""" + try: + with requests.get(url, stream=True) as response: + response.raise_for_status() + + total_size = int(response.headers.get('content-length', 0)) + downloaded = 0 + + with open(filename, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded += len(chunk) + + if total_size > 0: + progress = (downloaded / total_size) * 100 + print(f"\r 下载进度: {progress:.1f}%", end='') + + print(f"\n 文件下载完成: {filename}") + return True + + except requests.exceptions.RequestException as e: + print(f" 下载失败: {e}") + return False + + # 示例:下载小文件 + print("\n 开始下载示例文件...") + success = download_file_stream( + 'https://httpbin.org/json', + 'example.json' + ) + + if success: + try: + with open('example.json', 'r') as f: + content = f.read() + print(f" 下载内容预览: {content[:100]}...") + except Exception as e: + print(f" 读取文件失败: {e}") + + # 4. 认证和代理 + print("\n4. 认证和代理:") + + # HTTP基本认证 + try: + response = requests.get( + 'https://httpbin.org/basic-auth/user/pass', + auth=('user', 'pass') + ) + print(f" 基本认证: {response.status_code}") + + auth_info = response.json() + print(f" 认证用户: {auth_info['user']}") + + except requests.exceptions.RequestException as e: + print(f" 认证失败: {e}") + + # 代理设置示例(不实际使用) + proxy_config = { + 'http': 'http://proxy.example.com:8080', + 'https': 'https://proxy.example.com:8080' + } + + print(f" 代理配置示例: {proxy_config}") + + # 5. 错误处理 + print("\n5. 错误处理:") + + def safe_request(url, **kwargs): + """安全的请求函数""" + try: + response = requests.get(url, **kwargs) + response.raise_for_status() # 检查HTTP错误 + return response + + except requests.exceptions.ConnectionError: + print(f" 连接错误: 无法连接到 {url}") + except requests.exceptions.Timeout: + print(f" 超时错误: 请求 {url} 超时") + except requests.exceptions.HTTPError as e: + print(f" HTTP错误: {e}") + except requests.exceptions.RequestException as e: + print(f" 请求异常: {e}") + + return None + + # 测试错误处理 + test_urls = [ + 'https://httpbin.org/status/404', # 404错误 + 'https://httpbin.org/delay/1', # 正常请求 + 'https://nonexistent.example.com' # 连接错误 + ] + + for url in test_urls: + print(f"\n 测试URL: {url}") + response = safe_request(url, timeout=5) + if response: + print(f" 成功: 状态码 {response.status_code}") + else: + print(f" 失败") + +# 运行requests高级演示 +requests_advanced_demo() +``` + +### 2.3 实际应用示例 + +```python +import requests +import json +import time +from datetime import datetime + +class APIClient: + """API客户端封装""" + + def __init__(self, base_url, api_key=None, timeout=30): + self.base_url = base_url.rstrip('/') + self.session = requests.Session() + self.timeout = timeout + + # 设置默认头部 + self.session.headers.update({ + 'User-Agent': 'APIClient/1.0', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }) + + # 设置API密钥 + if api_key: + self.session.headers['Authorization'] = f'Bearer {api_key}' + + def _make_request(self, method, endpoint, **kwargs): + """发送请求的内部方法""" + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + # 设置默认超时 + kwargs.setdefault('timeout', self.timeout) + + try: + response = self.session.request(method, url, **kwargs) + response.raise_for_status() + + # 记录请求日志 + print(f"[{datetime.now()}] {method} {url} -> {response.status_code}") + + return response + + except requests.exceptions.RequestException as e: + print(f"[{datetime.now()}] 请求失败: {method} {url} -> {e}") + raise + + def get(self, endpoint, params=None): + """GET请求""" + return self._make_request('GET', endpoint, params=params) + + def post(self, endpoint, data=None, json_data=None): + """POST请求""" + kwargs = {} + if json_data: + kwargs['json'] = json_data + elif data: + kwargs['data'] = data + + return self._make_request('POST', endpoint, **kwargs) + + def put(self, endpoint, data=None, json_data=None): + """PUT请求""" + kwargs = {} + if json_data: + kwargs['json'] = json_data + elif data: + kwargs['data'] = data + + return self._make_request('PUT', endpoint, **kwargs) + + def delete(self, endpoint): + """DELETE请求""" + return self._make_request('DELETE', endpoint) + + def close(self): + """关闭会话""" + self.session.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + +def api_client_demo(): + """API客户端演示""" + print("=== API客户端应用示例 ===") + + # 使用上下文管理器 + with APIClient('https://httpbin.org') as client: + + # 1. 获取数据 + print("\n1. 获取数据:") + try: + response = client.get('/get', params={'page': 1, 'limit': 10}) + data = response.json() + print(f" 请求参数: {data['args']}") + except Exception as e: + print(f" 获取数据失败: {e}") + + # 2. 提交数据 + print("\n2. 提交数据:") + try: + user_data = { + 'name': '张三', + 'email': 'zhangsan@example.com', + 'age': 25 + } + + response = client.post('/post', json_data=user_data) + result = response.json() + print(f" 提交的数据: {result['json']}") + except Exception as e: + print(f" 提交数据失败: {e}") + + # 3. 更新数据 + print("\n3. 更新数据:") + try: + update_data = { + 'name': '张三', + 'age': 26 # 更新年龄 + } + + response = client.put('/put', json_data=update_data) + result = response.json() + print(f" 更新的数据: {result['json']}") + except Exception as e: + print(f" 更新数据失败: {e}") + + # 4. 删除数据 + print("\n4. 删除数据:") + try: + response = client.delete('/delete') + print(f" 删除操作状态: {response.status_code}") + except Exception as e: + print(f" 删除数据失败: {e}") + +# 运行API客户端演示 +api_client_demo() +``` + +--- + +## 三、pandas模块 - 数据分析 + +### 3.1 基础数据结构 + +```python +# 首先需要安装: pip install pandas numpy +import pandas as pd +import numpy as np +from datetime import datetime, timedelta + +def pandas_basic_demo(): + """pandas基础数据结构演示""" + print("=== pandas基础数据结构 ===") + + # 1. Series - 一维数据 + print("\n1. Series - 一维数据:") + + # 创建Series + numbers = pd.Series([1, 2, 3, 4, 5]) + print(f" 数字Series:\n{numbers}") + + # 带索引的Series + scores = pd.Series( + [85, 92, 78, 95, 88], + index=['数学', '英语', '物理', '化学', '生物'] + ) + print(f"\n 成绩Series:\n{scores}") + + # 从字典创建Series + student_info = pd.Series({ + '姓名': '张三', + '年龄': 20, + '专业': '计算机科学', + '年级': '大二' + }) + print(f"\n 学生信息Series:\n{student_info}") + + # Series基本操作 + print(f"\n Series基本操作:") + print(f" 数据类型: {scores.dtype}") + print(f" 形状: {scores.shape}") + print(f" 大小: {scores.size}") + print(f" 索引: {list(scores.index)}") + print(f" 值: {list(scores.values)}") + print(f" 最大值: {scores.max()}") + print(f" 最小值: {scores.min()}") + print(f" 平均值: {scores.mean():.2f}") + print(f" 标准差: {scores.std():.2f}") + + # 2. DataFrame - 二维数据 + print("\n2. DataFrame - 二维数据:") + + # 从字典创建DataFrame + students_data = { + '姓名': ['张三', '李四', '王五', '赵六', '钱七'], + '年龄': [20, 21, 19, 22, 20], + '专业': ['计算机', '数学', '物理', '化学', '生物'], + '成绩': [85, 92, 78, 95, 88] + } + + df = pd.DataFrame(students_data) + print(f" 学生DataFrame:\n{df}") + + # DataFrame基本信息 + print(f"\n DataFrame基本信息:") + print(f" 形状: {df.shape}") + print(f" 列名: {list(df.columns)}") + print(f" 索引: {list(df.index)}") + print(f" 数据类型:\n{df.dtypes}") + + # 查看数据概览 + print(f"\n 数据概览:") + print(f" 前3行:\n{df.head(3)}") + print(f"\n 后2行:\n{df.tail(2)}") + print(f"\n 统计信息:\n{df.describe()}") + print(f"\n 基本信息:") + df.info() + + # 3. 索引和选择 + print("\n3. 索引和选择:") + + # 选择列 + print(f" 选择单列 - 姓名:\n{df['姓名']}") + print(f"\n 选择多列:\n{df[['姓名', '成绩']]}") + + # 选择行 + print(f"\n 选择行 - 第2行:\n{df.iloc[1]}") + print(f"\n 选择多行:\n{df.iloc[1:4]}") + + # 条件选择 + high_scores = df[df['成绩'] >= 90] + print(f"\n 高分学生 (成绩>=90):\n{high_scores}") + + # 复合条件 + young_high_scores = df[(df['年龄'] <= 20) & (df['成绩'] >= 85)] + print(f"\n 年轻高分学生:\n{young_high_scores}") + + # 4. 数据操作 + print("\n4. 数据操作:") + + # 添加新列 + df['等级'] = df['成绩'].apply(lambda x: 'A' if x >= 90 else 'B' if x >= 80 else 'C') + print(f" 添加等级列:\n{df}") + + # 排序 + df_sorted = df.sort_values('成绩', ascending=False) + print(f"\n 按成绩降序排列:\n{df_sorted}") + + # 分组统计 + grade_stats = df.groupby('等级')['成绩'].agg(['count', 'mean', 'min', 'max']) + print(f"\n 按等级分组统计:\n{grade_stats}") + +# 运行pandas基础演示 +pandas_basic_demo() +``` + +### 3.2 数据处理和清洗 + +```python +import pandas as pd +import numpy as np +from datetime import datetime, timedelta + +def pandas_data_processing_demo(): + """pandas数据处理和清洗演示""" + print("=== pandas数据处理和清洗 ===") + + # 创建包含缺失值和异常值的示例数据 + np.random.seed(42) + + data = { + '日期': pd.date_range('2023-01-01', periods=100, freq='D'), + '销售额': np.random.normal(1000, 200, 100), + '客户数': np.random.poisson(50, 100), + '地区': np.random.choice(['北京', '上海', '广州', '深圳'], 100), + '产品': np.random.choice(['A', 'B', 'C'], 100) + } + + df = pd.DataFrame(data) + + # 人为添加一些缺失值和异常值 + df.loc[5:10, '销售额'] = np.nan + df.loc[15, '客户数'] = -5 # 异常值 + df.loc[25, '销售额'] = 10000 # 异常值 + + print(f"原始数据形状: {df.shape}") + print(f"前5行数据:\n{df.head()}") + + # 1. 缺失值处理 + print("\n1. 缺失值处理:") + + # 检查缺失值 + missing_info = df.isnull().sum() + print(f" 各列缺失值数量:\n{missing_info}") + + # 缺失值比例 + missing_percent = (df.isnull().sum() / len(df)) * 100 + print(f"\n 各列缺失值比例:\n{missing_percent}") + + # 处理缺失值的不同方法 + df_processed = df.copy() + + # 删除包含缺失值的行 + df_dropna = df.dropna() + print(f"\n 删除缺失值后形状: {df_dropna.shape}") + + # 用均值填充数值列的缺失值 + df_processed['销售额'].fillna(df_processed['销售额'].mean(), inplace=True) + + # 用前一个值填充 + df_processed['销售额'].fillna(method='ffill', inplace=True) + + # 用后一个值填充 + df_processed['销售额'].fillna(method='bfill', inplace=True) + + print(f" 填充后缺失值数量: {df_processed.isnull().sum().sum()}") + + # 2. 异常值检测和处理 + print("\n2. 异常值检测和处理:") + + # 使用IQR方法检测异常值 + def detect_outliers_iqr(series): + Q1 = series.quantile(0.25) + Q3 = series.quantile(0.75) + IQR = Q3 - Q1 + lower_bound = Q1 - 1.5 * IQR + upper_bound = Q3 + 1.5 * IQR + return (series < lower_bound) | (series > upper_bound) + + # 检测销售额异常值 + sales_outliers = detect_outliers_iqr(df_processed['销售额']) + print(f" 销售额异常值数量: {sales_outliers.sum()}") + print(f" 异常值: {df_processed.loc[sales_outliers, '销售额'].values}") + + # 检测客户数异常值(负值) + customer_outliers = df_processed['客户数'] < 0 + print(f" 客户数异常值数量: {customer_outliers.sum()}") + + # 处理异常值 + # 方法1: 删除异常值 + df_no_outliers = df_processed[~(sales_outliers | customer_outliers)] + print(f" 删除异常值后形状: {df_no_outliers.shape}") + + # 方法2: 用边界值替换异常值 + df_capped = df_processed.copy() + + # 销售额异常值用95%分位数替换 + sales_95th = df_capped['销售额'].quantile(0.95) + df_capped.loc[df_capped['销售额'] > sales_95th, '销售额'] = sales_95th + + # 客户数负值用0替换 + df_capped.loc[df_capped['客户数'] < 0, '客户数'] = 0 + + print(f" 替换异常值后统计:\n{df_capped[['销售额', '客户数']].describe()}") + + # 3. 数据类型转换 + print("\n3. 数据类型转换:") + + print(f" 原始数据类型:\n{df_processed.dtypes}") + + # 转换数据类型 + df_converted = df_processed.copy() + df_converted['客户数'] = df_converted['客户数'].astype('int32') + df_converted['地区'] = df_converted['地区'].astype('category') + df_converted['产品'] = df_converted['产品'].astype('category') + + print(f"\n 转换后数据类型:\n{df_converted.dtypes}") + + # 内存使用对比 + print(f"\n 内存使用对比:") + print(f" 原始: {df_processed.memory_usage(deep=True).sum() / 1024:.2f} KB") + print(f" 转换后: {df_converted.memory_usage(deep=True).sum() / 1024:.2f} KB") + + # 4. 数据重塑 + print("\n4. 数据重塑:") + + # 透视表 + pivot_table = df_converted.pivot_table( + values='销售额', + index='地区', + columns='产品', + aggfunc=['mean', 'sum'], + fill_value=0 + ) + print(f" 透视表:\n{pivot_table}") + + # 分组聚合 + grouped_stats = df_converted.groupby(['地区', '产品']).agg({ + '销售额': ['count', 'mean', 'sum', 'std'], + '客户数': ['mean', 'sum'] + }).round(2) + print(f"\n 分组统计:\n{grouped_stats.head(10)}") + + # 5. 时间序列处理 + print("\n5. 时间序列处理:") + + # 设置日期为索引 + df_ts = df_converted.set_index('日期') + + # 重采样 - 按周汇总 + weekly_sales = df_ts['销售额'].resample('W').agg({ + '总销售额': 'sum', + '平均销售额': 'mean', + '最大销售额': 'max' + }) + print(f" 周度销售统计:\n{weekly_sales.head()}") + + # 滚动窗口计算 + df_ts['销售额_7日均值'] = df_ts['销售额'].rolling(window=7).mean() + df_ts['销售额_7日标准差'] = df_ts['销售额'].rolling(window=7).std() + + print(f"\n 滚动统计示例:\n{df_ts[['销售额', '销售额_7日均值', '销售额_7日标准差']].head(10)}") + + # 6. 数据合并 + print("\n6. 数据合并:") + + # 创建额外的数据表 + region_info = pd.DataFrame({ + '地区': ['北京', '上海', '广州', '深圳'], + '人口': [2154, 2424, 1530, 1756], # 万人 + 'GDP': [4.0, 4.3, 2.9, 3.2] # 万亿元 + }) + + # 合并数据 + df_merged = df_converted.merge(region_info, on='地区', how='left') + print(f" 合并后数据:\n{df_merged.head()}") + + # 计算人均销售额 + df_merged['人均销售额'] = df_merged['销售额'] / df_merged['人口'] + + # 按地区统计 + region_summary = df_merged.groupby('地区').agg({ + '销售额': 'sum', + '客户数': 'sum', + '人口': 'first', + 'GDP': 'first', + '人均销售额': 'mean' + }).round(2) + + print(f"\n 地区汇总统计:\n{region_summary}") + +# 运行pandas数据处理演示 +pandas_data_processing_demo() +``` + +### 3.3 数据分析实例 + +```python +import pandas as pd +import numpy as np +from datetime import datetime, timedelta + +class SalesAnalyzer: + """销售数据分析器""" + + def __init__(self, data_file=None): + self.df = None + if data_file: + self.load_data(data_file) + else: + self.generate_sample_data() + + def generate_sample_data(self, n_records=1000): + """生成示例销售数据""" + np.random.seed(42) + + # 生成日期范围 + start_date = datetime(2023, 1, 1) + dates = [start_date + timedelta(days=x) for x in range(365)] + + data = [] + for _ in range(n_records): + record = { + '订单ID': f'ORD{_+1:06d}', + '日期': np.random.choice(dates), + '客户ID': f'CUST{np.random.randint(1, 201):04d}', + '产品类别': np.random.choice(['电子产品', '服装', '家居', '图书', '食品'], p=[0.3, 0.25, 0.2, 0.15, 0.1]), + '产品名称': f'产品{np.random.randint(1, 101):03d}', + '数量': np.random.randint(1, 11), + '单价': np.random.uniform(10, 1000), + '地区': np.random.choice(['华北', '华东', '华南', '华中', '西南', '西北', '东北']), + '销售员': f'员工{np.random.randint(1, 21):02d}', + '渠道': np.random.choice(['线上', '线下'], p=[0.6, 0.4]) + } + data.append(record) + + self.df = pd.DataFrame(data) + self.df['销售额'] = self.df['数量'] * self.df['单价'] + self.df['日期'] = pd.to_datetime(self.df['日期']) + + print(f"生成了 {len(self.df)} 条销售记录") + + def load_data(self, file_path): + """加载数据文件""" + try: + if file_path.endswith('.csv'): + self.df = pd.read_csv(file_path) + elif file_path.endswith('.xlsx'): + self.df = pd.read_excel(file_path) + else: + raise ValueError("不支持的文件格式") + + # 确保日期列是datetime类型 + if '日期' in self.df.columns: + self.df['日期'] = pd.to_datetime(self.df['日期']) + + print(f"成功加载 {len(self.df)} 条记录") + + except Exception as e: + print(f"加载数据失败: {e}") + + def basic_analysis(self): + """基础分析""" + print("=== 基础销售分析 ===") + + if self.df is None: + print("没有数据可分析") + return + + # 基本统计 + print(f"\n数据概览:") + print(f" 记录数: {len(self.df):,}") + print(f" 时间范围: {self.df['日期'].min()} 到 {self.df['日期'].max()}") + print(f" 总销售额: ¥{self.df['销售额'].sum():,.2f}") + print(f" 平均订单金额: ¥{self.df['销售额'].mean():.2f}") + print(f" 客户数量: {self.df['客户ID'].nunique():,}") + print(f" 产品数量: {self.df['产品名称'].nunique():,}") + + # 销售额分布 + print(f"\n销售额统计:") + sales_stats = self.df['销售额'].describe() + for stat, value in sales_stats.items(): + print(f" {stat}: ¥{value:.2f}") + + def time_analysis(self): + """时间维度分析""" + print("\n=== 时间维度分析 ===") + + # 按月统计 + monthly_sales = self.df.groupby(self.df['日期'].dt.to_period('M')).agg({ + '销售额': 'sum', + '订单ID': 'count', + '客户ID': 'nunique' + }).round(2) + monthly_sales.columns = ['月销售额', '订单数', '客户数'] + + print(f"\n月度销售统计:") + print(monthly_sales.head(10)) + + # 按星期几统计 + self.df['星期几'] = self.df['日期'].dt.day_name() + weekday_sales = self.df.groupby('星期几')['销售额'].agg(['sum', 'mean', 'count']).round(2) + weekday_sales.columns = ['总销售额', '平均销售额', '订单数'] + + print(f"\n星期销售统计:") + print(weekday_sales) + + # 销售趋势 + daily_sales = self.df.groupby('日期')['销售额'].sum() + + # 计算移动平均 + daily_sales_ma7 = daily_sales.rolling(window=7).mean() + daily_sales_ma30 = daily_sales.rolling(window=30).mean() + + print(f"\n销售趋势分析:") + print(f" 最高单日销售额: ¥{daily_sales.max():.2f} ({daily_sales.idxmax()})") + print(f" 最低单日销售额: ¥{daily_sales.min():.2f} ({daily_sales.idxmin()})") + print(f" 7日移动平均: ¥{daily_sales_ma7.iloc[-1]:.2f}") + print(f" 30日移动平均: ¥{daily_sales_ma30.iloc[-1]:.2f}") + + def product_analysis(self): + """产品维度分析""" + print("\n=== 产品维度分析 ===") + + # 按产品类别统计 + category_stats = self.df.groupby('产品类别').agg({ + '销售额': ['sum', 'mean', 'count'], + '数量': 'sum', + '客户ID': 'nunique' + }).round(2) + + category_stats.columns = ['总销售额', '平均销售额', '订单数', '总数量', '客户数'] + category_stats = category_stats.sort_values('总销售额', ascending=False) + + print(f"\n产品类别统计:") + print(category_stats) + + # 计算类别占比 + category_stats['销售额占比'] = (category_stats['总销售额'] / category_stats['总销售额'].sum() * 100).round(2) + print(f"\n产品类别销售额占比:") + for category, row in category_stats.iterrows(): + print(f" {category}: {row['销售额占比']}%") + + # 热销产品TOP10 + top_products = self.df.groupby('产品名称').agg({ + '销售额': 'sum', + '数量': 'sum', + '订单ID': 'count' + }).round(2) + top_products.columns = ['总销售额', '总数量', '订单数'] + top_products = top_products.sort_values('总销售额', ascending=False).head(10) + + print(f"\n热销产品TOP10:") + print(top_products) + + def customer_analysis(self): + """客户维度分析""" + print("\n=== 客户维度分析 ===") + + # 客户价值分析 + customer_stats = self.df.groupby('客户ID').agg({ + '销售额': 'sum', + '订单ID': 'count', + '日期': ['min', 'max'] + }).round(2) + + customer_stats.columns = ['总消费额', '订单数', '首次购买', '最后购买'] + customer_stats['平均订单金额'] = (customer_stats['总消费额'] / customer_stats['订单数']).round(2) + + # 计算客户活跃天数 + customer_stats['活跃天数'] = (customer_stats['最后购买'] - customer_stats['首次购买']).dt.days + 1 + + print(f"\n客户统计概览:") + print(f" 总客户数: {len(customer_stats):,}") + print(f" 平均客户价值: ¥{customer_stats['总消费额'].mean():.2f}") + print(f" 平均订单数: {customer_stats['订单数'].mean():.2f}") + print(f" 平均订单金额: ¥{customer_stats['平均订单金额'].mean():.2f}") + + # 客户分层(RFM简化版) + # R: Recency (最近购买时间) + # F: Frequency (购买频率) + # M: Monetary (消费金额) + + latest_date = self.df['日期'].max() + customer_stats['最近购买天数'] = (latest_date - customer_stats['最后购买']).dt.days + + # 客户分层 + def customer_segment(row): + if row['总消费额'] >= customer_stats['总消费额'].quantile(0.8): + if row['最近购买天数'] <= 30: + return '高价值活跃客户' + else: + return '高价值沉睡客户' + elif row['总消费额'] >= customer_stats['总消费额'].quantile(0.5): + if row['最近购买天数'] <= 60: + return '中价值活跃客户' + else: + return '中价值沉睡客户' + else: + if row['最近购买天数'] <= 90: + return '低价值活跃客户' + else: + return '低价值沉睡客户' + + customer_stats['客户分层'] = customer_stats.apply(customer_segment, axis=1) + + segment_stats = customer_stats.groupby('客户分层').agg({ + '总消费额': ['count', 'sum', 'mean'], + '订单数': 'mean', + '平均订单金额': 'mean' + }).round(2) + + print(f"\n客户分层统计:") + print(segment_stats) + + # TOP客户 + top_customers = customer_stats.sort_values('总消费额', ascending=False).head(10) + print(f"\nTOP10客户:") + print(top_customers[['总消费额', '订单数', '平均订单金额', '客户分层']]) + + def regional_analysis(self): + """地区维度分析""" + print("\n=== 地区维度分析 ===") + + # 地区销售统计 + region_stats = self.df.groupby('地区').agg({ + '销售额': ['sum', 'mean', 'count'], + '客户ID': 'nunique', + '销售员': 'nunique' + }).round(2) + + region_stats.columns = ['总销售额', '平均销售额', '订单数', '客户数', '销售员数'] + region_stats = region_stats.sort_values('总销售额', ascending=False) + + # 计算地区占比 + region_stats['销售额占比'] = (region_stats['总销售额'] / region_stats['总销售额'].sum() * 100).round(2) + region_stats['人均销售额'] = (region_stats['总销售额'] / region_stats['客户数']).round(2) + + print(f"\n地区销售统计:") + print(region_stats) + + # 渠道分析 + channel_stats = self.df.groupby(['地区', '渠道'])['销售额'].sum().unstack(fill_value=0) + channel_stats['总计'] = channel_stats.sum(axis=1) + channel_stats = channel_stats.sort_values('总计', ascending=False) + + print(f"\n地区渠道分析:") + print(channel_stats) + + def sales_performance_analysis(self): + """销售员绩效分析""" + print("\n=== 销售员绩效分析 ===") + + # 销售员统计 + salesperson_stats = self.df.groupby('销售员').agg({ + '销售额': ['sum', 'mean', 'count'], + '客户ID': 'nunique', + '产品类别': 'nunique' + }).round(2) + + salesperson_stats.columns = ['总销售额', '平均订单金额', '订单数', '客户数', '产品类别数'] + salesperson_stats['客户平均价值'] = (salesperson_stats['总销售额'] / salesperson_stats['客户数']).round(2) + salesperson_stats = salesperson_stats.sort_values('总销售额', ascending=False) + + print(f"\n销售员绩效统计:") + print(salesperson_stats.head(10)) + + # 绩效分级 + performance_threshold = salesperson_stats['总销售额'].quantile([0.2, 0.8]) + + def performance_level(sales): + if sales >= performance_threshold[0.8]: + return '优秀' + elif sales >= performance_threshold[0.2]: + return '良好' + else: + return '待提升' + + salesperson_stats['绩效等级'] = salesperson_stats['总销售额'].apply(performance_level) + + performance_summary = salesperson_stats.groupby('绩效等级').agg({ + '总销售额': ['count', 'sum', 'mean'], + '客户数': 'mean', + '订单数': 'mean' + }).round(2) + + print(f"\n绩效等级分布:") + print(performance_summary) + + def generate_report(self): + """生成完整分析报告""" + print("\n" + "="*50) + print(" 销售数据分析报告") + print("="*50) + + self.basic_analysis() + self.time_analysis() + self.product_analysis() + self.customer_analysis() + self.regional_analysis() + self.sales_performance_analysis() + + print("\n" + "="*50) + print(" 报告生成完成") + print("="*50) + +def pandas_analysis_demo(): + """pandas数据分析实例演示""" + print("=== pandas数据分析实例 ===") + + # 创建销售分析器 + analyzer = SalesAnalyzer() + + # 生成完整分析报告 + analyzer.generate_report() + +# 运行pandas分析演示 +pandas_analysis_demo() +``` + +--- + +## 四、numpy模块 - 数值计算 + +### 4.1 基础数组操作 + +```python +# 首先需要安装: pip install numpy +import numpy as np +import time + +def numpy_basic_demo(): + """numpy基础操作演示""" + print("=== numpy基础数组操作 ===") + + # 1. 创建数组 + print("\n1. 创建数组:") + + # 从列表创建 + arr1 = np.array([1, 2, 3, 4, 5]) + print(f" 一维数组: {arr1}") + print(f" 数据类型: {arr1.dtype}") + print(f" 形状: {arr1.shape}") + print(f" 维度: {arr1.ndim}") + + # 二维数组 + arr2 = np.array([[1, 2, 3], [4, 5, 6]]) + print(f"\n 二维数组:\n{arr2}") + print(f" 形状: {arr2.shape}") + print(f" 大小: {arr2.size}") + + # 指定数据类型 + arr3 = np.array([1, 2, 3], dtype=np.float64) + print(f"\n 指定类型数组: {arr3}") + print(f" 数据类型: {arr3.dtype}") + + # 2. 特殊数组创建 + print("\n2. 特殊数组创建:") + + # 零数组 + zeros = np.zeros((3, 4)) + print(f" 零数组:\n{zeros}") + + # 一数组 + ones = np.ones((2, 3), dtype=int) + print(f"\n 一数组:\n{ones}") + + # 单位矩阵 + identity = np.eye(3) + print(f"\n 单位矩阵:\n{identity}") + + # 等差数列 + linspace = np.linspace(0, 10, 5) + print(f"\n 等差数列: {linspace}") + + # 等比数列 + arange = np.arange(0, 10, 2) + print(f" 等差序列: {arange}") + + # 随机数组 + np.random.seed(42) + random_arr = np.random.random((2, 3)) + print(f"\n 随机数组:\n{random_arr}") + + # 正态分布随机数 + normal_arr = np.random.normal(0, 1, (2, 3)) + print(f"\n 正态分布随机数:\n{normal_arr}") + + # 3. 数组索引和切片 + print("\n3. 数组索引和切片:") + + arr = np.arange(12).reshape(3, 4) + print(f" 原数组:\n{arr}") + + # 基本索引 + print(f" 元素[1,2]: {arr[1, 2]}") + print(f" 第一行: {arr[0]}") + print(f" 第一列: {arr[:, 0]}") + + # 切片 + print(f" 前两行:\n{arr[:2]}") + print(f" 后两列:\n{arr[:, -2:]}") + + # 布尔索引 + mask = arr > 5 + print(f"\n 大于5的元素: {arr[mask]}") + + # 花式索引 + indices = np.array([0, 2]) + print(f" 选择第0和第2行:\n{arr[indices]}") + + # 4. 数组形状操作 + print("\n4. 数组形状操作:") + + original = np.arange(12) + print(f" 原数组: {original}") + + # 重塑 + reshaped = original.reshape(3, 4) + print(f" 重塑为3x4:\n{reshaped}") + + # 转置 + transposed = reshaped.T + print(f" 转置:\n{transposed}") + + # 展平 + flattened = reshaped.flatten() + print(f" 展平: {flattened}") + + # 添加维度 + expanded = np.expand_dims(original, axis=0) + print(f" 添加维度: {expanded.shape}") + + # 压缩维度 + squeezed = np.squeeze(expanded) + print(f" 压缩维度: {squeezed.shape}") + +# 运行numpy基础演示 +numpy_basic_demo() +``` + +### 4.2 数学运算和统计 + +```python +import numpy as np + +def numpy_math_demo(): + """numpy数学运算演示""" + print("=== numpy数学运算和统计 ===") + + # 创建测试数据 + np.random.seed(42) + arr1 = np.random.randint(1, 10, (3, 4)) + arr2 = np.random.randint(1, 10, (3, 4)) + + print(f"数组1:\n{arr1}") + print(f"数组2:\n{arr2}") + + # 1. 基本数学运算 + print("\n1. 基本数学运算:") + + # 元素级运算 + print(f" 加法:\n{arr1 + arr2}") + print(f"\n 减法:\n{arr1 - arr2}") + print(f"\n 乘法:\n{arr1 * arr2}") + print(f"\n 除法:\n{arr1 / arr2}") + print(f"\n 幂运算:\n{arr1 ** 2}") + + # 标量运算 + print(f"\n 标量加法:\n{arr1 + 10}") + print(f"\n 标量乘法:\n{arr1 * 2}") + + # 2. 数学函数 + print("\n2. 数学函数:") + + # 三角函数 + angles = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2]) + print(f" 角度: {angles}") + print(f" sin值: {np.sin(angles)}") + print(f" cos值: {np.cos(angles)}") + print(f" tan值: {np.tan(angles)}") + + # 指数和对数 + values = np.array([1, 2, 3, 4, 5]) + print(f"\n 原值: {values}") + print(f" 指数: {np.exp(values)}") + print(f" 自然对数: {np.log(values)}") + print(f" 以10为底: {np.log10(values)}") + print(f" 平方根: {np.sqrt(values)}") + + # 取整函数 + decimals = np.array([1.2, 2.7, -1.5, -2.8]) + print(f"\n 小数: {decimals}") + print(f" 向上取整: {np.ceil(decimals)}") + print(f" 向下取整: {np.floor(decimals)}") + print(f" 四舍五入: {np.round(decimals)}") + print(f" 截断: {np.trunc(decimals)}") + + # 3. 统计函数 + print("\n3. 统计函数:") + + data = np.random.normal(50, 15, (5, 6)) + print(f" 测试数据:\n{data.round(2)}") + + # 基本统计 + print(f"\n 最大值: {np.max(data):.2f}") + print(f" 最小值: {np.min(data):.2f}") + print(f" 均值: {np.mean(data):.2f}") + print(f" 中位数: {np.median(data):.2f}") + print(f" 标准差: {np.std(data):.2f}") + print(f" 方差: {np.var(data):.2f}") + print(f" 总和: {np.sum(data):.2f}") + + # 按轴统计 + print(f"\n 按行统计 (axis=1):") + print(f" 行均值: {np.mean(data, axis=1).round(2)}") + print(f" 行最大值: {np.max(data, axis=1).round(2)}") + + print(f"\n 按列统计 (axis=0):") + print(f" 列均值: {np.mean(data, axis=0).round(2)}") + print(f" 列最小值: {np.min(data, axis=0).round(2)}") + + # 分位数 + percentiles = [25, 50, 75, 90, 95] + print(f"\n 分位数:") + for p in percentiles: + value = np.percentile(data, p) + print(f" {p}%分位数: {value:.2f}") + + # 4. 线性代数 + print("\n4. 线性代数:") + + # 矩阵乘法 + A = np.array([[1, 2], [3, 4]]) + B = np.array([[5, 6], [7, 8]]) + + print(f" 矩阵A:\n{A}") + print(f" 矩阵B:\n{B}") + print(f" 矩阵乘法 A@B:\n{A @ B}") + print(f" 矩阵乘法 np.dot(A,B):\n{np.dot(A, B)}") + + # 矩阵属性 + print(f"\n 矩阵A的行列式: {np.linalg.det(A):.2f}") + print(f" 矩阵A的迹: {np.trace(A)}") + + # 特征值和特征向量 + eigenvalues, eigenvectors = np.linalg.eig(A) + print(f" 特征值: {eigenvalues}") + print(f" 特征向量:\n{eigenvectors}") + + # 矩阵求逆 + try: + A_inv = np.linalg.inv(A) + print(f" 矩阵A的逆:\n{A_inv}") + print(f" 验证 A * A_inv:\n{(A @ A_inv).round(10)}") + except np.linalg.LinAlgError: + print(" 矩阵不可逆") + + # 5. 数组比较和逻辑运算 + print("\n5. 数组比较和逻辑运算:") + + x = np.array([1, 2, 3, 4, 5]) + y = np.array([1, 3, 2, 4, 6]) + + print(f" 数组x: {x}") + print(f" 数组y: {y}") + print(f" x == y: {x == y}") + print(f" x > y: {x > y}") + print(f" x >= 3: {x >= 3}") + + # 逻辑运算 + condition1 = x > 2 + condition2 = x < 5 + print(f"\n x > 2: {condition1}") + print(f" x < 5: {condition2}") + print(f" (x > 2) & (x < 5): {condition1 & condition2}") + print(f" (x > 2) | (x < 5): {condition1 | condition2}") + + # 条件选择 + result = np.where(x > 3, x, 0) + print(f" 条件选择 (x>3则保留,否则为0): {result}") + +# 运行numpy数学演示 +numpy_math_demo() +``` + +### 4.3 性能优化和实际应用 + +```python +import numpy as np +import time + +def numpy_performance_demo(): + """numpy性能优化演示""" + print("=== numpy性能优化和实际应用 ===") + + # 1. 性能对比 + print("\n1. 性能对比:") + + # 创建大数组 + size = 1000000 + python_list = list(range(size)) + numpy_array = np.arange(size) + + # Python列表求和 + start_time = time.time() + python_sum = sum(python_list) + python_time = time.time() - start_time + + # NumPy数组求和 + start_time = time.time() + numpy_sum = np.sum(numpy_array) + numpy_time = time.time() - start_time + + print(f" 数组大小: {size:,}") + print(f" Python列表求和: {python_time:.6f}秒") + print(f" NumPy数组求和: {numpy_time:.6f}秒") + print(f" 性能提升: {python_time/numpy_time:.1f}倍") + + # 2. 向量化操作 + print("\n2. 向量化操作:") + + # 非向量化方式 + def python_operation(arr): + result = [] + for x in arr: + result.append(x**2 + 2*x + 1) + return result + + # 向量化方式 + def numpy_operation(arr): + return arr**2 + 2*arr + 1 + + test_data = list(range(100000)) + numpy_data = np.array(test_data) + + # 性能测试 + start_time = time.time() + python_result = python_operation(test_data) + python_time = time.time() - start_time + + start_time = time.time() + numpy_result = numpy_operation(numpy_data) + numpy_time = time.time() - start_time + + print(f" Python循环: {python_time:.6f}秒") + print(f" NumPy向量化: {numpy_time:.6f}秒") + print(f" 性能提升: {python_time/numpy_time:.1f}倍") + + # 3. 广播机制 + print("\n3. 广播机制:") + + # 不同形状数组的运算 + a = np.array([[1, 2, 3], + [4, 5, 6]]) + b = np.array([10, 20, 30]) + c = np.array([[100], + [200]]) + + print(f" 数组a (2x3):\n{a}") + print(f" 数组b (3,): {b}") + print(f" 数组c (2x1):\n{c}") + + print(f"\n a + b (广播):\n{a + b}") + print(f" a + c (广播):\n{a + c}") + print(f" a + b + c (广播):\n{a + b + c}") + + # 4. 内存优化 + print("\n4. 内存优化:") + + # 数据类型优化 + large_array = np.random.randint(0, 100, 1000000) + + # 不同数据类型的内存使用 + int64_size = large_array.astype(np.int64).nbytes + int32_size = large_array.astype(np.int32).nbytes + int16_size = large_array.astype(np.int16).nbytes + int8_size = large_array.astype(np.int8).nbytes + + print(f" 数组大小: {len(large_array):,}") + print(f" int64内存: {int64_size/1024/1024:.2f} MB") + print(f" int32内存: {int32_size/1024/1024:.2f} MB") + print(f" int16内存: {int16_size/1024/1024:.2f} MB") + print(f" int8内存: {int8_size/1024/1024:.2f} MB") + + # 就地操作 + arr = np.random.random(1000000) + arr_copy = arr.copy() + + # 创建新数组 + start_time = time.time() + result1 = arr * 2 + 1 + time1 = time.time() - start_time + + # 就地操作 + start_time = time.time() + arr_copy *= 2 + arr_copy += 1 + time2 = time.time() - start_time + + print(f"\n 创建新数组: {time1:.6f}秒") + print(f" 就地操作: {time2:.6f}秒") + print(f" 性能提升: {time1/time2:.1f}倍") + + # 5. 实际应用示例 + print("\n5. 实际应用示例:") + + # 图像处理模拟 + def image_processing_demo(): + # 模拟RGB图像 (高度, 宽度, 通道) + height, width = 100, 100 + image = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8) + + print(f" 原图像形状: {image.shape}") + print(f" 原图像数据类型: {image.dtype}") + + # 转换为灰度图 + # 灰度 = 0.299*R + 0.587*G + 0.114*B + weights = np.array([0.299, 0.587, 0.114]) + gray_image = np.dot(image, weights).astype(np.uint8) + + print(f" 灰度图形状: {gray_image.shape}") + print(f" 像素值范围: {gray_image.min()} - {gray_image.max()}") + + # 图像增强 - 对比度调整 + enhanced = np.clip(gray_image * 1.5, 0, 255).astype(np.uint8) + print(f" 增强后范围: {enhanced.min()} - {enhanced.max()}") + + # 边缘检测模拟 (简单差分) + edges_x = np.abs(np.diff(gray_image, axis=1)) + edges_y = np.abs(np.diff(gray_image, axis=0)) + + print(f" 水平边缘形状: {edges_x.shape}") + print(f" 垂直边缘形状: {edges_y.shape}") + + return gray_image, enhanced, edges_x, edges_y + + # 信号处理模拟 + def signal_processing_demo(): + # 生成信号 + t = np.linspace(0, 1, 1000) + frequency1, frequency2 = 5, 20 + signal = np.sin(2 * np.pi * frequency1 * t) + 0.5 * np.sin(2 * np.pi * frequency2 * t) + + # 添加噪声 + noise = np.random.normal(0, 0.1, len(signal)) + noisy_signal = signal + noise + + print(f" 信号长度: {len(signal)}") + print(f" 信号范围: {signal.min():.3f} - {signal.max():.3f}") + print(f" 噪声信号范围: {noisy_signal.min():.3f} - {noisy_signal.max():.3f}") + + # 简单滤波 (移动平均) + window_size = 10 + filtered_signal = np.convolve(noisy_signal, np.ones(window_size)/window_size, mode='same') + + print(f" 滤波后范围: {filtered_signal.min():.3f} - {filtered_signal.max():.3f}") + + # 统计分析 + print(f" 原信号标准差: {np.std(signal):.3f}") + print(f" 噪声信号标准差: {np.std(noisy_signal):.3f}") + print(f" 滤波信号标准差: {np.std(filtered_signal):.3f}") + + return signal, noisy_signal, filtered_signal + + # 数据分析模拟 + def data_analysis_demo(): + # 生成销售数据 + np.random.seed(42) + days = 365 + base_sales = 1000 + trend = np.linspace(0, 200, days) # 增长趋势 + seasonal = 100 * np.sin(2 * np.pi * np.arange(days) / 365.25 * 4) # 季节性 + noise = np.random.normal(0, 50, days) # 随机噪声 + + sales = base_sales + trend + seasonal + noise + sales = np.maximum(sales, 0) # 确保非负 + + print(f" 销售数据天数: {len(sales)}") + print(f" 平均日销售额: ¥{np.mean(sales):.2f}") + print(f" 销售额标准差: ¥{np.std(sales):.2f}") + print(f" 最高日销售额: ¥{np.max(sales):.2f}") + print(f" 最低日销售额: ¥{np.min(sales):.2f}") + + # 移动平均分析 + window_sizes = [7, 30, 90] + for window in window_sizes: + ma = np.convolve(sales, np.ones(window)/window, mode='valid') + print(f" {window}日移动平均: ¥{ma[-1]:.2f}") + + # 同比分析 (简化) + if len(sales) >= 365: + yoy_growth = (sales[-1] - sales[-365]) / sales[-365] * 100 + print(f" 年同比增长: {yoy_growth:.1f}%") + + return sales + + print("\n 图像处理演示:") + image_processing_demo() + + print("\n 信号处理演示:") + signal_processing_demo() + + print("\n 数据分析演示:") + data_analysis_demo() + +# 运行numpy性能演示 +numpy_performance_demo() +``` + +--- + +## 五、matplotlib模块 - 数据可视化 + +### 5.1 基础绘图 + +```python +# 首先需要安装: pip install matplotlib +import matplotlib.pyplot as plt +import numpy as np +from datetime import datetime, timedelta + +def matplotlib_basic_demo(): + """matplotlib基础绘图演示""" + print("=== matplotlib基础绘图 ===") + + # 设置中文字体支持 + plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] + plt.rcParams['axes.unicode_minus'] = False + + # 1. 线图 + print("\n1. 线图演示") + + # 生成数据 + x = np.linspace(0, 10, 100) + y1 = np.sin(x) + y2 = np.cos(x) + y3 = np.sin(x) * np.exp(-x/5) + + # 创建图形 + plt.figure(figsize=(12, 8)) + + # 第一个子图 + plt.subplot(2, 2, 1) + plt.plot(x, y1, label='sin(x)', linewidth=2, color='blue') + plt.plot(x, y2, label='cos(x)', linewidth=2, color='red', linestyle='--') + plt.title('三角函数') + plt.xlabel('x') + plt.ylabel('y') + plt.legend() + plt.grid(True, alpha=0.3) + + # 第二个子图 - 散点图 + plt.subplot(2, 2, 2) + np.random.seed(42) + x_scatter = np.random.randn(50) + y_scatter = 2 * x_scatter + np.random.randn(50) + plt.scatter(x_scatter, y_scatter, alpha=0.6, c=y_scatter, cmap='viridis') + plt.title('散点图') + plt.xlabel('X值') + plt.ylabel('Y值') + plt.colorbar(label='Y值') + + # 第三个子图 - 柱状图 + plt.subplot(2, 2, 3) + categories = ['A', 'B', 'C', 'D', 'E'] + values = [23, 45, 56, 78, 32] + colors = ['red', 'green', 'blue', 'orange', 'purple'] + bars = plt.bar(categories, values, color=colors, alpha=0.7) + plt.title('柱状图') + plt.xlabel('类别') + plt.ylabel('数值') + + # 在柱子上添加数值标签 + for bar, value in zip(bars, values): + plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, + str(value), ha='center', va='bottom') + + # 第四个子图 - 饼图 + plt.subplot(2, 2, 4) + sizes = [30, 25, 20, 15, 10] + labels = ['产品A', '产品B', '产品C', '产品D', '产品E'] + explode = (0.1, 0, 0, 0, 0) # 突出第一个扇形 + + plt.pie(sizes, labels=labels, explode=explode, autopct='%1.1f%%', + shadow=True, startangle=90) + plt.title('饼图') + + plt.tight_layout() + plt.savefig('basic_plots.png', dpi=300, bbox_inches='tight') + plt.show() + + print(" 基础图形已保存为 'basic_plots.png'") + + # 2. 高级线图 + print("\n2. 高级线图演示") + + plt.figure(figsize=(12, 6)) + + # 生成时间序列数据 + dates = [datetime(2023, 1, 1) + timedelta(days=i) for i in range(365)] + np.random.seed(42) + + # 模拟股价数据 + price = 100 + prices = [price] + for _ in range(364): + change = np.random.normal(0, 2) + price = max(price + change, 10) # 确保价格不为负 + prices.append(price) + + # 计算移动平均 + ma_20 = [] + ma_50 = [] + + for i in range(len(prices)): + if i >= 19: + ma_20.append(np.mean(prices[i-19:i+1])) + else: + ma_20.append(np.nan) + + if i >= 49: + ma_50.append(np.mean(prices[i-49:i+1])) + else: + ma_50.append(np.nan) + + # 绘制股价图 + plt.plot(dates, prices, label='股价', linewidth=1, alpha=0.7) + plt.plot(dates, ma_20, label='20日均线', linewidth=2, color='orange') + plt.plot(dates, ma_50, label='50日均线', linewidth=2, color='red') + + plt.title('股价走势图', fontsize=16) + plt.xlabel('日期', fontsize=12) + plt.ylabel('价格 (元)', fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + + # 格式化x轴日期 + plt.xticks(rotation=45) + + # 添加注释 + max_price_idx = np.argmax(prices) + max_price = prices[max_price_idx] + max_date = dates[max_price_idx] + + plt.annotate(f'最高点\n{max_price:.2f}元', + xy=(max_date, max_price), + xytext=(max_date, max_price + 10), + arrowprops=dict(arrowstyle='->', color='red'), + fontsize=10, ha='center') + + plt.tight_layout() + plt.savefig('stock_chart.png', dpi=300, bbox_inches='tight') + plt.show() + + print(" 股价图已保存为 'stock_chart.png'") + +# 运行matplotlib基础演示 +matplotlib_basic_demo() +``` + +### 5.2 高级可视化 + +```python +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from matplotlib.patches import Rectangle +from matplotlib.collections import PatchCollection + +def matplotlib_advanced_demo(): + """matplotlib高级可视化演示""" + print("=== matplotlib高级可视化 ===") + + # 设置样式 + plt.style.use('seaborn-v0_8') + plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] + plt.rcParams['axes.unicode_minus'] = False + + # 1. 热力图 + print("\n1. 热力图演示") + + # 生成相关性矩阵数据 + np.random.seed(42) + variables = ['销售额', '广告费', '客户数', '产品数', '员工数'] + n_vars = len(variables) + + # 生成随机相关性矩阵 + correlation_matrix = np.random.rand(n_vars, n_vars) + correlation_matrix = (correlation_matrix + correlation_matrix.T) / 2 # 对称化 + np.fill_diagonal(correlation_matrix, 1) # 对角线为1 + + plt.figure(figsize=(10, 8)) + + # 创建热力图 + im = plt.imshow(correlation_matrix, cmap='coolwarm', aspect='auto', vmin=-1, vmax=1) + + # 设置刻度和标签 + plt.xticks(range(n_vars), variables, rotation=45) + plt.yticks(range(n_vars), variables) + + # 添加数值标签 + for i in range(n_vars): + for j in range(n_vars): + plt.text(j, i, f'{correlation_matrix[i, j]:.2f}', + ha='center', va='center', + color='white' if abs(correlation_matrix[i, j]) > 0.5 else 'black') + + plt.colorbar(im, label='相关系数') + plt.title('变量相关性热力图', fontsize=16, pad=20) + plt.tight_layout() + plt.savefig('heatmap.png', dpi=300, bbox_inches='tight') + plt.show() + + # 2. 箱线图 + print("\n2. 箱线图演示") + + # 生成不同组的数据 + np.random.seed(42) + group_data = { + '组A': np.random.normal(100, 15, 100), + '组B': np.random.normal(110, 20, 100), + '组C': np.random.normal(95, 10, 100), + '组D': np.random.normal(105, 25, 100) + } + + plt.figure(figsize=(12, 6)) + + # 左侧:传统箱线图 + plt.subplot(1, 2, 1) + data_values = list(group_data.values()) + labels = list(group_data.keys()) + + box_plot = plt.boxplot(data_values, labels=labels, patch_artist=True) + + # 自定义箱线图颜色 + colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow'] + for patch, color in zip(box_plot['boxes'], colors): + patch.set_facecolor(color) + patch.set_alpha(0.7) + + plt.title('箱线图') + plt.ylabel('数值') + plt.grid(True, alpha=0.3) + + # 右侧:小提琴图 + plt.subplot(1, 2, 2) + violin_plot = plt.violinplot(data_values, positions=range(1, len(labels)+1)) + + # 自定义小提琴图颜色 + for pc, color in zip(violin_plot['bodies'], colors): + pc.set_facecolor(color) + pc.set_alpha(0.7) + + plt.xticks(range(1, len(labels)+1), labels) + plt.title('小提琴图') + plt.ylabel('数值') + plt.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('box_violin_plots.png', dpi=300, bbox_inches='tight') + plt.show() + + # 3. 多轴图 + print("\n3. 多轴图演示") + + # 生成数据 + months = ['1月', '2月', '3月', '4月', '5月', '6月', + '7月', '8月', '9月', '10月', '11月', '12月'] + sales = [120, 135, 158, 142, 168, 195, 210, 198, 175, 162, 148, 155] + profit_rate = [8.5, 9.2, 10.1, 8.8, 11.2, 12.5, 13.1, 12.8, 11.5, 10.8, 9.9, 10.3] + + fig, ax1 = plt.subplots(figsize=(12, 6)) + + # 第一个y轴 - 销售额 + color1 = 'tab:blue' + ax1.set_xlabel('月份') + ax1.set_ylabel('销售额 (万元)', color=color1) + line1 = ax1.plot(months, sales, color=color1, marker='o', linewidth=2, label='销售额') + ax1.tick_params(axis='y', labelcolor=color1) + ax1.grid(True, alpha=0.3) + + # 第二个y轴 - 利润率 + ax2 = ax1.twinx() + color2 = 'tab:red' + ax2.set_ylabel('利润率 (%)', color=color2) + line2 = ax2.plot(months, profit_rate, color=color2, marker='s', linewidth=2, label='利润率') + ax2.tick_params(axis='y', labelcolor=color2) + + # 添加图例 + lines = line1 + line2 + labels = [l.get_label() for l in lines] + ax1.legend(lines, labels, loc='upper left') + + plt.title('销售额与利润率趋势', fontsize=16) + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig('dual_axis_plot.png', dpi=300, bbox_inches='tight') + plt.show() + + # 4. 3D图形 + print("\n4. 3D图形演示") + + from mpl_toolkits.mplot3d import Axes3D + + fig = plt.figure(figsize=(15, 5)) + + # 3D散点图 + ax1 = fig.add_subplot(131, projection='3d') + + np.random.seed(42) + n_points = 100 + x = np.random.randn(n_points) + y = np.random.randn(n_points) + z = x**2 + y**2 + np.random.randn(n_points) * 0.1 + colors = z + + scatter = ax1.scatter(x, y, z, c=colors, cmap='viridis', alpha=0.6) + ax1.set_xlabel('X轴') + ax1.set_ylabel('Y轴') + ax1.set_zlabel('Z轴') + ax1.set_title('3D散点图') + + # 3D表面图 + ax2 = fig.add_subplot(132, projection='3d') + + x_surf = np.linspace(-2, 2, 30) + y_surf = np.linspace(-2, 2, 30) + X, Y = np.meshgrid(x_surf, y_surf) + Z = np.sin(np.sqrt(X**2 + Y**2)) + + surface = ax2.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.8) + ax2.set_xlabel('X轴') + ax2.set_ylabel('Y轴') + ax2.set_zlabel('Z轴') + ax2.set_title('3D表面图') + + # 3D线框图 + ax3 = fig.add_subplot(133, projection='3d') + + wireframe = ax3.plot_wireframe(X, Y, Z, alpha=0.6) + ax3.set_xlabel('X轴') + ax3.set_ylabel('Y轴') + ax3.set_zlabel('Z轴') + ax3.set_title('3D线框图') + + plt.tight_layout() + plt.savefig('3d_plots.png', dpi=300, bbox_inches='tight') + plt.show() + + # 5. 动画图(静态展示) + print("\n5. 动画效果演示(静态帧)") + + # 创建动画的几个关键帧 + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + axes = axes.flatten() + + t_values = np.linspace(0, 2*np.pi, 6) + x_base = np.linspace(0, 4*np.pi, 100) + + for i, t in enumerate(t_values): + ax = axes[i] + y = np.sin(x_base + t) + ax.plot(x_base, y, 'b-', linewidth=2) + ax.set_ylim(-1.5, 1.5) + ax.set_title(f'帧 {i+1}: t={t:.2f}') + ax.grid(True, alpha=0.3) + + plt.suptitle('正弦波动画效果(静态帧展示)', fontsize=16) + plt.tight_layout() + plt.savefig('animation_frames.png', dpi=300, bbox_inches='tight') + plt.show() + + print(" 所有高级图形已保存") + +# 运行matplotlib高级演示 +matplotlib_advanced_demo() +``` + +--- + +## 六、scikit-learn模块 - 机器学习 + +### 6.1 基础机器学习 + +```python +# 首先需要安装: pip install scikit-learn +from sklearn import datasets +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LinearRegression, LogisticRegression +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import accuracy_score, mean_squared_error, classification_report +from sklearn.preprocessing import StandardScaler +import numpy as np +import pandas as pd + +def sklearn_basic_demo(): + """scikit-learn基础机器学习演示""" + print("=== scikit-learn基础机器学习 ===") + + # 1. 线性回归 + print("\n1. 线性回归演示:") + + # 生成回归数据 + X, y = datasets.make_regression(n_samples=100, n_features=1, noise=10, random_state=42) + + # 分割数据 + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + + # 创建和训练模型 + lr_model = LinearRegression() + lr_model.fit(X_train, y_train) + + # 预测 + y_pred = lr_model.predict(X_test) + + # 评估 + mse = mean_squared_error(y_test, y_pred) + print(f" 训练样本数: {len(X_train)}") + print(f" 测试样本数: {len(X_test)}") + print(f" 均方误差: {mse:.2f}") + print(f" 模型系数: {lr_model.coef_[0]:.2f}") + print(f" 模型截距: {lr_model.intercept_:.2f}") + + # 2. 分类任务 + print("\n2. 分类任务演示:") + + # 使用鸢尾花数据集 + iris = datasets.load_iris() + X_iris, y_iris = iris.data, iris.target + + # 分割数据 + X_train, X_test, y_train, y_test = train_test_split( + X_iris, y_iris, test_size=0.3, random_state=42 + ) + + # 数据标准化 + scaler = StandardScaler() + X_train_scaled = scaler.fit_transform(X_train) + X_test_scaled = scaler.transform(X_test) + + # 逻辑回归 + log_reg = LogisticRegression(random_state=42) + log_reg.fit(X_train_scaled, y_train) + + # 随机森林 + rf_model = RandomForestClassifier(n_estimators=100, random_state=42) + rf_model.fit(X_train, y_train) + + # 预测和评估 + log_pred = log_reg.predict(X_test_scaled) + rf_pred = rf_model.predict(X_test) + + log_accuracy = accuracy_score(y_test, log_pred) + rf_accuracy = accuracy_score(y_test, rf_pred) + + print(f" 数据集: 鸢尾花数据集") + print(f" 特征数: {X_iris.shape[1]}") + print(f" 类别数: {len(np.unique(y_iris))}") + print(f" 逻辑回归准确率: {log_accuracy:.3f}") + print(f" 随机森林准确率: {rf_accuracy:.3f}") + + # 特征重要性 + feature_importance = rf_model.feature_importances_ + feature_names = iris.feature_names + + print(f"\n 特征重要性:") + for name, importance in zip(feature_names, feature_importance): + print(f" {name}: {importance:.3f}") + + # 3. 聚类分析 + print("\n3. 聚类分析演示:") + + from sklearn.cluster import KMeans + from sklearn.metrics import silhouette_score + + # 生成聚类数据 + X_cluster, _ = datasets.make_blobs(n_samples=300, centers=4, n_features=2, + random_state=42, cluster_std=1.5) + + # K-means聚类 + kmeans = KMeans(n_clusters=4, random_state=42, n_init=10) + cluster_labels = kmeans.fit_predict(X_cluster) + + # 评估聚类效果 + silhouette_avg = silhouette_score(X_cluster, cluster_labels) + + print(f" 样本数: {len(X_cluster)}") + print(f" 聚类数: 4") + print(f" 轮廓系数: {silhouette_avg:.3f}") + print(f" 聚类中心:") + for i, center in enumerate(kmeans.cluster_centers_): + print(f" 簇{i+1}: ({center[0]:.2f}, {center[1]:.2f})") + + # 4. 模型评估和交叉验证 + print("\n4. 模型评估和交叉验证:") + + from sklearn.model_selection import cross_val_score, GridSearchCV + from sklearn.svm import SVC + + # 使用SVM进行交叉验证 + svm_model = SVC(random_state=42) + cv_scores = cross_val_score(svm_model, X_train_scaled, y_train, cv=5) + + print(f" 5折交叉验证结果:") + print(f" 各折得分: {cv_scores}") + print(f" 平均得分: {cv_scores.mean():.3f} (+/- {cv_scores.std() * 2:.3f})") + + # 网格搜索调参 + param_grid = { + 'C': [0.1, 1, 10], + 'gamma': ['scale', 'auto', 0.1, 1] + } + + grid_search = GridSearchCV(SVC(random_state=42), param_grid, cv=3, scoring='accuracy') + grid_search.fit(X_train_scaled, y_train) + + print(f"\n 网格搜索最佳参数: {grid_search.best_params_}") + print(f" 最佳交叉验证得分: {grid_search.best_score_:.3f}") + + # 最佳模型在测试集上的表现 + best_model = grid_search.best_estimator_ + test_score = best_model.score(X_test_scaled, y_test) + print(f" 测试集得分: {test_score:.3f}") + +# 运行scikit-learn基础演示 +sklearn_basic_demo() +``` + +--- + +## 七、BeautifulSoup模块 - 网页解析 + +### 7.1 HTML解析基础 + +```python +# 首先需要安装: pip install beautifulsoup4 lxml +from bs4 import BeautifulSoup +import requests +from urllib.parse import urljoin, urlparse +import re +import time + +def beautifulsoup_demo(): + """BeautifulSoup网页解析演示""" + print("=== BeautifulSoup网页解析 ===") + + # 1. 基础HTML解析 + print("\n1. 基础HTML解析:") + + # 示例HTML内容 + html_content = """ + + + + 示例网页 + + + +
+

欢迎来到我的网站

+ +
+
+
+

第一篇文章

+

发布时间: 2023-01-01

+

这是第一篇文章的内容...

+
+ Python + 编程 +
+
+
+

第二篇文章

+

发布时间: 2023-01-02

+

这是第二篇文章的内容...

+
+ Web开发 + HTML +
+
+
+
+

© 2023 我的网站. 保留所有权利.

+
+ + + """ + + # 创建BeautifulSoup对象 + soup = BeautifulSoup(html_content, 'html.parser') + + # 基本信息提取 + print(f" 网页标题: {soup.title.string}") + print(f" 主标题: {soup.find('h1').string}") + + # 2. 元素查找 + print("\n2. 元素查找方法:") + + # 按标签查找 + all_links = soup.find_all('a') + print(f" 所有链接数量: {len(all_links)}") + for link in all_links: + print(f" 链接文本: '{link.string}', 地址: '{link.get('href')}'") + + # 按类名查找 + posts = soup.find_all('article', class_='post') + print(f"\n 文章数量: {len(posts)}") + for i, post in enumerate(posts, 1): + title = post.find('h2').string + meta = post.find('p', class_='meta').string + data_id = post.get('data-id') + print(f" 文章{i}: {title} (ID: {data_id})") + print(f" {meta}") + + # 按ID查找 + main_title = soup.find('h1', id='main-title') + print(f"\n 主标题元素: {main_title.string}") + + # CSS选择器 + print("\n3. CSS选择器:") + + # 选择所有标签 + tags = soup.select('.tag') + print(f" 所有标签: {[tag.string for tag in tags]}") + + # 选择特定文章的标签 + first_post_tags = soup.select('article[data-id="1"] .tag') + print(f" 第一篇文章标签: {[tag.string for tag in first_post_tags]}") + + # 选择导航链接 + nav_links = soup.select('nav ul li a') + print(f" 导航链接: {[link.string for link in nav_links]}") + + # 4. 文本提取和处理 + print("\n4. 文本提取和处理:") + + # 提取纯文本 + content_div = soup.find('div', class_='content') + content_text = content_div.get_text(strip=True) + print(f" 内容区域文本长度: {len(content_text)}字符") + + # 提取特定格式的文本 + for post in posts: + title = post.find('h2').get_text(strip=True) + content = post.find_all('p')[-1].get_text(strip=True) # 最后一个p标签 + print(f" {title}: {content[:20]}...") + + # 5. 属性操作 + print("\n5. 属性操作:") + + # 获取和修改属性 + first_link = soup.find('a') + print(f" 第一个链接原始href: {first_link.get('href')}") + + # 修改属性 + first_link['href'] = 'https://example.com/home' + first_link['target'] = '_blank' + print(f" 修改后的链接: {first_link}") + + # 添加新属性 + for post in soup.find_all('article'): + post['class'] = post.get('class', []) + ['processed'] + + print(f" 第一篇文章的class: {soup.find('article').get('class')}") + +# 运行BeautifulSoup演示 +beautifulsoup_demo() +``` + +### 7.2 实际网页爬取 + +```python +import requests +from bs4 import BeautifulSoup +import time +import csv +from urllib.parse import urljoin, urlparse +import os + +def web_scraping_demo(): + """实际网页爬取演示""" + print("=== 实际网页爬取演示 ===") + + # 1. 基础网页请求和解析 + print("\n1. 基础网页请求和解析:") + + def safe_request(url, headers=None, timeout=10): + """安全的网页请求函数""" + default_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + if headers: + default_headers.update(headers) + + try: + response = requests.get(url, headers=default_headers, timeout=timeout) + response.raise_for_status() + return response + except requests.RequestException as e: + print(f" 请求失败: {e}") + return None + + # 示例:解析一个简单的HTML页面(模拟) + def parse_example_page(): + """解析示例页面""" + # 模拟HTML内容(实际应用中这里会是真实的网页请求) + sample_html = """ + + 新闻网站 + +
+ + + +
+ + + """ + + soup = BeautifulSoup(sample_html, 'html.parser') + + # 提取新闻信息 + news_items = soup.find_all('article', class_='news-item') + news_data = [] + + for item in news_items: + title_link = item.find('h3').find('a') + title = title_link.get_text(strip=True) + link = title_link.get('href') + summary = item.find('p', class_='summary').get_text(strip=True) + date = item.find('span', class_='date').get_text(strip=True) + author = item.find('span', class_='author').get_text(strip=True) + + news_data.append({ + 'title': title, + 'link': link, + 'summary': summary, + 'date': date, + 'author': author + }) + + return news_data + + # 解析示例页面 + news_list = parse_example_page() + print(f" 提取到 {len(news_list)} 条新闻:") + for i, news in enumerate(news_list, 1): + print(f" {i}. {news['title']}") + print(f" 作者: {news['author']}, 日期: {news['date']}") + print(f" 摘要: {news['summary'][:30]}...") + print() + + # 2. 数据清洗和处理 + print("\n2. 数据清洗和处理:") + + def clean_text(text): + """清洗文本数据""" + if not text: + return "" + + # 去除多余空白 + text = re.sub(r'\s+', ' ', text.strip()) + + # 去除特殊字符 + text = re.sub(r'[\r\n\t]', '', text) + + # 去除HTML实体 + text = text.replace(' ', ' ').replace('&', '&') + + return text + + def extract_numbers(text): + """从文本中提取数字""" + numbers = re.findall(r'\d+(?:\.\d+)?', text) + return [float(num) for num in numbers] + + def extract_dates(text): + """从文本中提取日期""" + date_patterns = [ + r'\d{4}-\d{2}-\d{2}', # YYYY-MM-DD + r'\d{2}/\d{2}/\d{4}', # MM/DD/YYYY + r'\d{1,2}月\d{1,2}日' # 中文日期 + ] + + dates = [] + for pattern in date_patterns: + dates.extend(re.findall(pattern, text)) + + return dates + + # 示例文本清洗 + sample_text = " 这是一个\n\t包含多余空白的文本 2023-10-01 价格:99.99元 " + cleaned = clean_text(sample_text) + numbers = extract_numbers(sample_text) + dates = extract_dates(sample_text) + + print(f" 原始文本: '{sample_text}'") + print(f" 清洗后: '{cleaned}'") + print(f" 提取的数字: {numbers}") + print(f" 提取的日期: {dates}") + + # 3. 数据存储 + print("\n3. 数据存储:") + + def save_to_csv(data, filename): + """保存数据到CSV文件""" + if not data: + return + + fieldnames = data[0].keys() + + with open(filename, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(data) + + print(f" 数据已保存到 {filename}") + + def save_to_json(data, filename): + """保存数据到JSON文件""" + import json + + with open(filename, 'w', encoding='utf-8') as jsonfile: + json.dump(data, jsonfile, ensure_ascii=False, indent=2) + + print(f" 数据已保存到 {filename}") + + # 保存新闻数据 + save_to_csv(news_list, 'news_data.csv') + save_to_json(news_list, 'news_data.json') + + # 4. 爬虫最佳实践 + print("\n4. 爬虫最佳实践:") + + class WebScraper: + """网页爬虫类""" + + def __init__(self, delay=1, max_retries=3): + self.delay = delay + self.max_retries = max_retries + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + + def get_page(self, url): + """获取网页内容""" + for attempt in range(self.max_retries): + try: + response = self.session.get(url, timeout=10) + response.raise_for_status() + + # 添加延迟,避免过于频繁的请求 + time.sleep(self.delay) + + return response + + except requests.RequestException as e: + print(f" 尝试 {attempt + 1} 失败: {e}") + if attempt < self.max_retries - 1: + time.sleep(2 ** attempt) # 指数退避 + + return None + + def parse_page(self, html_content, selectors): + """解析网页内容""" + soup = BeautifulSoup(html_content, 'html.parser') + results = {} + + for key, selector in selectors.items(): + elements = soup.select(selector) + if elements: + if len(elements) == 1: + results[key] = elements[0].get_text(strip=True) + else: + results[key] = [elem.get_text(strip=True) for elem in elements] + else: + results[key] = None + + return results + + def scrape_multiple_pages(self, urls, selectors): + """爬取多个页面""" + results = [] + + for i, url in enumerate(urls, 1): + print(f" 正在爬取第 {i}/{len(urls)} 个页面...") + + response = self.get_page(url) + if response: + data = self.parse_page(response.text, selectors) + data['url'] = url + results.append(data) + else: + print(f" 跳过页面: {url}") + + return results + + # 示例使用 + scraper = WebScraper(delay=0.5) + + # 定义选择器 + selectors = { + 'title': 'h1, h2, h3', + 'content': 'p', + 'links': 'a' + } + + print(f" 爬虫配置:") + print(f" 延迟: {scraper.delay}秒") + print(f" 最大重试: {scraper.max_retries}次") + print(f" 选择器: {list(selectors.keys())}") + + # 5. 错误处理和日志 + print("\n5. 错误处理和日志:") + + import logging + + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('scraper.log', encoding='utf-8'), + logging.StreamHandler() + ] + ) + + logger = logging.getLogger(__name__) + + def robust_scrape(url, max_attempts=3): + """健壮的爬取函数""" + for attempt in range(max_attempts): + try: + logger.info(f"尝试爬取: {url} (第{attempt+1}次)") + + # 模拟可能的错误 + if attempt == 0: + raise requests.ConnectionError("模拟连接错误") + elif attempt == 1: + raise requests.Timeout("模拟超时错误") + else: + logger.info("爬取成功") + return "成功获取页面内容" + + except requests.RequestException as e: + logger.warning(f"爬取失败: {e}") + if attempt < max_attempts - 1: + wait_time = 2 ** attempt + logger.info(f"等待 {wait_time} 秒后重试...") + time.sleep(wait_time) + else: + logger.error(f"所有尝试都失败了: {url}") + return None + + # 测试健壮爬取 + result = robust_scrape("https://example.com") + print(f" 爬取结果: {result}") + + print(f"\n 爬虫演示完成,相关文件已生成") + +# 运行网页爬取演示 +web_scraping_demo() +``` + +--- + +## 八、模块选择和最佳实践 + +### 8.1 模块选择指南 + +```python +def module_selection_guide(): + """第三方模块选择指南""" + print("=== 第三方模块选择指南 ===") + + # 1. 按应用场景分类 + print("\n1. 按应用场景选择模块:") + + scenarios = { + "Web开发": { + "框架": ["Django", "Flask", "FastAPI"], + "HTTP客户端": ["requests", "httpx", "aiohttp"], + "模板引擎": ["Jinja2", "Django Templates"], + "数据库ORM": ["SQLAlchemy", "Django ORM", "Peewee"] + }, + "数据科学": { + "数据处理": ["pandas", "numpy", "polars"], + "可视化": ["matplotlib", "seaborn", "plotly", "bokeh"], + "机器学习": ["scikit-learn", "tensorflow", "pytorch"], + "统计分析": ["scipy", "statsmodels"] + }, + "网络爬虫": { + "HTML解析": ["BeautifulSoup", "lxml", "html.parser"], + "浏览器自动化": ["selenium", "playwright", "pyppeteer"], + "异步爬虫": ["scrapy", "aiohttp", "asyncio"] + }, + "图像处理": { + "基础处理": ["Pillow", "opencv-python"], + "深度学习": ["tensorflow", "pytorch", "keras"], + "计算机视觉": ["opencv-python", "scikit-image"] + }, + "自动化运维": { + "系统管理": ["psutil", "paramiko", "fabric"], + "配置管理": ["ansible", "saltstack"], + "监控": ["prometheus_client", "psutil"] + } + } + + for scenario, categories in scenarios.items(): + print(f"\n {scenario}:") + for category, modules in categories.items(): + print(f" {category}: {', '.join(modules)}") + + # 2. 性能对比 + print("\n2. 常见模块性能对比:") + + performance_comparison = { + "HTTP请求库": { + "requests": {"易用性": "★★★★★", "性能": "★★★☆☆", "功能": "★★★★☆"}, + "httpx": {"易用性": "★★★★☆", "性能": "★★★★☆", "功能": "★★★★★"}, + "aiohttp": {"易用性": "★★★☆☆", "性能": "★★★★★", "功能": "★★★★☆"} + }, + "数据处理库": { + "pandas": {"易用性": "★★★★★", "性能": "★★★☆☆", "内存效率": "★★☆☆☆"}, + "polars": {"易用性": "★★★☆☆", "性能": "★★★★★", "内存效率": "★★★★★"}, + "numpy": {"易用性": "★★★☆☆", "性能": "★★★★★", "内存效率": "★★★★☆"} + }, + "Web框架": { + "Django": {"学习曲线": "★★☆☆☆", "功能完整性": "★★★★★", "性能": "★★★☆☆"}, + "Flask": {"学习曲线": "★★★★☆", "功能完整性": "★★★☆☆", "性能": "★★★★☆"}, + "FastAPI": {"学习曲线": "★★★★☆", "功能完整性": "★★★★☆", "性能": "★★★★★"} + } + } + + for category, modules in performance_comparison.items(): + print(f"\n {category}:") + for module, metrics in modules.items(): + print(f" {module}:") + for metric, rating in metrics.items(): + print(f" {metric}: {rating}") + + # 3. 选择建议 + print("\n3. 选择建议:") + + recommendations = { + "初学者": { + "推荐模块": ["requests", "pandas", "matplotlib", "BeautifulSoup"], + "原因": "文档完善,社区活跃,学习资源丰富", + "避免": ["复杂的异步库", "底层系统库"] + }, + "数据分析师": { + "推荐模块": ["pandas", "numpy", "matplotlib", "seaborn", "scikit-learn"], + "原因": "专为数据分析设计,功能强大", + "避免": ["Web开发框架", "底层网络库"] + }, + "Web开发者": { + "推荐模块": ["Django/Flask", "requests", "SQLAlchemy", "Celery"], + "原因": "Web开发生态完整,部署方便", + "避免": ["科学计算库", "图像处理库"] + }, + "性能优化者": { + "推荐模块": ["numpy", "numba", "cython", "asyncio"], + "原因": "高性能,支持并行和异步", + "避免": ["纯Python实现的库", "功能过重的框架"] + } + } + + for user_type, info in recommendations.items(): + print(f"\n {user_type}:") + print(f" 推荐: {', '.join(info['推荐模块'])}") + print(f" 原因: {info['原因']}") + print(f" 避免: {', '.join(info['避免'])}") + +# 运行模块选择指南 +module_selection_guide() +``` + +### 8.2 最佳实践和总结 + +```python +def best_practices_summary(): + """第三方模块最佳实践和总结""" + print("=== 第三方模块最佳实践和总结 ===") + + # 1. 安装和管理最佳实践 + print("\n1. 安装和管理最佳实践:") + + practices = { + "虚拟环境": { + "重要性": "★★★★★", + "工具": ["venv", "conda", "pipenv", "poetry"], + "好处": ["隔离依赖", "避免冲突", "便于部署", "版本管理"] + }, + "依赖管理": { + "重要性": "★★★★☆", + "文件": ["requirements.txt", "Pipfile", "pyproject.toml"], + "好处": ["可重现环境", "团队协作", "自动化部署"] + }, + "版本固定": { + "重要性": "★★★★☆", + "策略": ["精确版本", "兼容版本", "最小版本"], + "示例": ["requests==2.28.1", "pandas>=1.5.0,<2.0.0"] + } + } + + for practice, details in practices.items(): + print(f"\n {practice} (重要性: {details['重要性']}):") + for key, values in details.items(): + if key != "重要性": + if isinstance(values, list): + print(f" {key}: {', '.join(values)}") + else: + print(f" {key}: {values}") + + # 2. 代码质量最佳实践 + print("\n2. 代码质量最佳实践:") + + code_quality_tips = [ + "导入规范: 标准库 -> 第三方库 -> 本地模块", + "异常处理: 捕获具体异常,提供有意义的错误信息", + "文档字符串: 为复杂函数添加详细说明", + "类型提示: 使用typing模块提高代码可读性", + "单元测试: 为关键功能编写测试用例", + "代码格式: 使用black、flake8等工具保持一致性" + ] + + for i, tip in enumerate(code_quality_tips, 1): + print(f" {i}. {tip}") + + # 3. 性能优化建议 + print("\n3. 性能优化建议:") + + performance_tips = { + "数据处理": [ + "优先使用numpy和pandas的向量化操作", + "避免在循环中重复创建对象", + "使用适当的数据类型减少内存占用", + "考虑使用numba加速数值计算" + ], + "网络请求": [ + "使用连接池复用连接", + "设置合理的超时时间", + "使用异步请求处理并发", + "实现请求重试和错误处理" + ], + "文件操作": [ + "使用上下文管理器确保资源释放", + "批量处理减少I/O操作", + "选择合适的文件格式(CSV vs JSON vs Parquet)", + "考虑使用内存映射处理大文件" + ] + } + + for category, tips in performance_tips.items(): + print(f"\n {category}:") + for tip in tips: + print(f" • {tip}") + + # 4. 常见陷阱和解决方案 + print("\n4. 常见陷阱和解决方案:") + + common_pitfalls = { + "版本冲突": { + "问题": "不同模块要求不兼容的依赖版本", + "解决": "使用虚拟环境,检查依赖树,选择兼容版本" + }, + "内存泄漏": { + "问题": "大数据处理时内存不断增长", + "解决": "及时释放变量,使用生成器,分批处理数据" + }, + "编码问题": { + "问题": "处理中文或特殊字符时出现乱码", + "解决": "明确指定编码格式,使用UTF-8" + }, + "网络超时": { + "问题": "网络请求经常超时失败", + "解决": "设置重试机制,使用指数退避,检查网络状况" + }, + "路径问题": { + "问题": "跨平台路径分隔符不一致", + "解决": "使用pathlib或os.path处理路径" + } + } + + for pitfall, details in common_pitfalls.items(): + print(f"\n {pitfall}:") + print(f" 问题: {details['问题']}") + print(f" 解决: {details['解决']}") + + # 5. 学习建议 + print("\n5. 学习建议:") + + learning_path = { + "基础阶段": { + "重点模块": ["requests", "json", "csv"], + "学习目标": "掌握基本的数据获取和处理", + "项目建议": "简单的API调用和数据保存" + }, + "进阶阶段": { + "重点模块": ["pandas", "matplotlib", "BeautifulSoup"], + "学习目标": "数据分析和可视化能力", + "项目建议": "网页数据爬取和分析报告" + }, + "高级阶段": { + "重点模块": ["numpy", "scikit-learn", "asyncio"], + "学习目标": "高性能计算和机器学习", + "项目建议": "完整的数据科学项目" + }, + "专业阶段": { + "重点模块": ["tensorflow", "django", "celery"], + "学习目标": "专业领域深度应用", + "项目建议": "生产级应用开发" + } + } + + for stage, details in learning_path.items(): + print(f"\n {stage}:") + print(f" 重点模块: {', '.join(details['重点模块'])}") + print(f" 学习目标: {details['学习目标']}") + print(f" 项目建议: {details['项目建议']}") + + # 6. 总结 + print("\n6. 总结:") + + summary_points = [ + "第三方模块是Python生态系统的重要组成部分", + "选择合适的模块比重复造轮子更高效", + "虚拟环境和依赖管理是专业开发的基础", + "性能优化要基于实际测量,避免过早优化", + "持续学习新模块,跟上技术发展趋势", + "实践项目是掌握模块使用的最佳方式" + ] + + for i, point in enumerate(summary_points, 1): + print(f" {i}. {point}") + + print("\n恭喜你完成了第16天的学习!") + print("你已经掌握了Python常用第三方模块的使用方法。") + print("建议继续通过实际项目来深化理解和应用这些知识。") + +# 运行最佳实践总结 +best_practices_summary() +``` + +--- + +## 学习总结 + +通过第16天的学习,我们深入了解了Python常用第三方模块的使用方法: + +### 主要收获 + +1. **包管理基础** - 掌握了pip和虚拟环境的使用 +2. **网络请求** - 学会使用requests进行HTTP通信 +3. **数据处理** - 熟练运用pandas进行数据分析 +4. **数值计算** - 理解numpy的强大数值计算能力 +5. **数据可视化** - 掌握matplotlib创建各种图表 +6. **机器学习** - 了解scikit-learn的基础应用 +7. **网页解析** - 学会使用BeautifulSoup处理HTML +8. **最佳实践** - 掌握模块选择和代码质量标准 + +### 实践建议 + +1. **动手实践** - 通过实际项目巩固所学知识 +2. **持续学习** - 关注新模块和技术发展 +3. **社区参与** - 积极参与开源项目和技术讨论 +4. **文档阅读** - 养成阅读官方文档的习惯 + +### 下一步学习 + +- 深入学习特定领域的专业模块 +- 了解异步编程和高性能计算 +- 学习Web开发框架如Django或Flask +- 探索深度学习框架如TensorFlow或PyTorch + +继续保持学习的热情,Python的世界还有更多精彩等待你去探索! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/17.md b/docs/Python/17.md new file mode 100644 index 000000000..411a927a2 --- /dev/null +++ b/docs/Python/17.md @@ -0,0 +1,2880 @@ +--- +title: 第17天-图形界面 +author: 哪吒 +date: '2023-06-15' +--- + +# 第17天-图形界面 + +## 学习目标 + +通过第17天的学习,你将掌握: + +1. **GUI编程基础** - 理解图形界面编程的基本概念 +2. **Tkinter框架** - 掌握Python内置GUI库的使用 +3. **控件使用** - 学会各种GUI控件的应用 +4. **事件处理** - 理解事件驱动编程模式 +5. **布局管理** - 掌握界面布局的设计方法 +6. **高级功能** - 学习菜单、对话框、画布等高级特性 +7. **实际项目** - 完成完整的GUI应用程序 +8. **其他GUI框架** - 了解PyQt、wxPython等替代方案 + +--- + +## 一、GUI编程基础 + +### 1.1 图形界面编程概述 + +```python +import tkinter as tk +from tkinter import ttk, messagebox, filedialog +import threading +import time + +def gui_programming_intro(): + """GUI编程基础概念介绍""" + print("=== GUI编程基础概念 ===") + + # 1. GUI编程的基本概念 + print("\n1. GUI编程基本概念:") + + concepts = { + "窗口(Window)": "应用程序的主容器,包含所有其他控件", + "控件(Widget)": "用户界面的基本元素,如按钮、标签、文本框等", + "事件(Event)": "用户操作或系统状态变化,如点击、键盘输入等", + "事件处理器(Handler)": "响应特定事件的函数或方法", + "布局管理器(Layout)": "控制控件在窗口中的位置和大小", + "回调函数(Callback)": "当事件发生时被调用的函数" + } + + for concept, description in concepts.items(): + print(f" {concept}: {description}") + + # 2. 事件驱动编程模型 + print("\n2. 事件驱动编程模型:") + + event_model = [ + "程序启动 -> 创建GUI界面", + "进入事件循环 -> 等待用户操作", + "检测事件 -> 鼠标点击、键盘输入等", + "调用事件处理器 -> 执行相应的处理函数", + "更新界面 -> 刷新显示内容", + "返回事件循环 -> 继续等待下一个事件" + ] + + for i, step in enumerate(event_model, 1): + print(f" {i}. {step}") + + # 3. 常见GUI框架对比 + print("\n3. Python GUI框架对比:") + + frameworks = { + "Tkinter": { + "优点": ["内置库", "轻量级", "跨平台", "学习简单"], + "缺点": ["外观较老", "控件有限", "性能一般"], + "适用场景": "简单桌面应用、学习GUI编程" + }, + "PyQt/PySide": { + "优点": ["功能强大", "外观现代", "控件丰富", "性能优秀"], + "缺点": ["体积较大", "学习复杂", "许可证限制"], + "适用场景": "专业桌面应用、复杂界面" + }, + "wxPython": { + "优点": ["原生外观", "功能完整", "跨平台"], + "缺点": ["API复杂", "文档较少", "更新较慢"], + "适用场景": "需要原生外观的应用" + }, + "Kivy": { + "优点": ["现代设计", "触摸支持", "移动端支持"], + "缺点": ["学习曲线陡", "桌面体验一般"], + "适用场景": "移动应用、触摸界面" + } + } + + for framework, details in frameworks.items(): + print(f"\n {framework}:") + print(f" 优点: {', '.join(details['优点'])}") + print(f" 缺点: {', '.join(details['缺点'])}") + print(f" 适用场景: {details['适用场景']}") + + # 4. GUI设计原则 + print("\n4. GUI设计原则:") + + design_principles = [ + "一致性: 保持界面元素的一致性", + "简洁性: 避免界面过于复杂", + "可用性: 确保用户能够轻松使用", + "反馈性: 及时给用户操作反馈", + "容错性: 处理用户的错误操作", + "可访问性: 考虑不同用户的需求" + ] + + for i, principle in enumerate(design_principles, 1): + print(f" {i}. {principle}") + +# 运行GUI编程介绍 +gui_programming_intro() +``` + +--- + +## 二、Tkinter基础 + +### 2.1 基本窗口和控件 + +```python +import tkinter as tk +from tkinter import ttk + +def tkinter_basics_demo(): + """Tkinter基础控件演示""" + print("=== Tkinter基础控件演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("Tkinter基础演示") + root.geometry("600x500") + root.resizable(True, True) + + # 设置窗口图标(如果有的话) + # root.iconbitmap('icon.ico') + + # 1. 标签控件 + print("\n1. 创建标签控件") + + # 普通标签 + label1 = tk.Label(root, text="欢迎使用Tkinter!", + font=("Arial", 16, "bold"), + fg="blue", bg="lightgray") + label1.pack(pady=10) + + # 多行标签 + label2 = tk.Label(root, + text="这是一个多行标签\n可以显示多行文本\n支持换行符", + justify=tk.LEFT, + font=("宋体", 12)) + label2.pack(pady=5) + + # 2. 按钮控件 + print("\n2. 创建按钮控件") + + def button_click(): + print(" 按钮被点击了!") + messagebox.showinfo("提示", "按钮点击事件触发!") + + def button_hover_enter(event): + event.widget.config(bg="lightblue") + + def button_hover_leave(event): + event.widget.config(bg="SystemButtonFace") + + button1 = tk.Button(root, text="点击我", + command=button_click, + font=("Arial", 12), + width=15, height=2) + button1.pack(pady=5) + + # 绑定鼠标悬停事件 + button1.bind("", button_hover_enter) + button1.bind("", button_hover_leave) + + # 3. 文本输入控件 + print("\n3. 创建文本输入控件") + + # 单行文本框 + tk.Label(root, text="请输入您的姓名:").pack() + entry_var = tk.StringVar() + entry = tk.Entry(root, textvariable=entry_var, + font=("Arial", 12), width=30) + entry.pack(pady=5) + + def get_entry_text(): + text = entry_var.get() + print(f" 输入的文本: {text}") + if text: + messagebox.showinfo("输入内容", f"您输入的是: {text}") + else: + messagebox.showwarning("警告", "请先输入内容!") + + tk.Button(root, text="获取输入", command=get_entry_text).pack(pady=5) + + # 4. 多行文本框 + print("\n4. 创建多行文本框") + + tk.Label(root, text="多行文本输入:").pack() + + # 创建文本框和滚动条的框架 + text_frame = tk.Frame(root) + text_frame.pack(pady=5, padx=20, fill=tk.BOTH, expand=True) + + # 多行文本框 + text_widget = tk.Text(text_frame, height=8, width=50, + font=("Consolas", 10), + wrap=tk.WORD) + + # 滚动条 + scrollbar = tk.Scrollbar(text_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 连接文本框和滚动条 + text_widget.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=text_widget.yview) + + # 插入默认文本 + default_text = """这是一个多行文本框示例。 +您可以在这里输入多行文本。 +支持滚动条和自动换行。 + +试试输入一些内容吧!""" + text_widget.insert(tk.END, default_text) + + def get_text_content(): + content = text_widget.get("1.0", tk.END) + print(f" 文本框内容: {repr(content)}") + lines = content.strip().split('\n') + messagebox.showinfo("文本内容", f"共 {len(lines)} 行,{len(content.strip())} 个字符") + + def clear_text(): + text_widget.delete("1.0", tk.END) + print(" 文本框已清空") + + # 文本操作按钮 + button_frame = tk.Frame(root) + button_frame.pack(pady=5) + + tk.Button(button_frame, text="获取内容", + command=get_text_content).pack(side=tk.LEFT, padx=5) + tk.Button(button_frame, text="清空内容", + command=clear_text).pack(side=tk.LEFT, padx=5) + + # 5. 复选框和单选按钮 + print("\n5. 创建复选框和单选按钮") + + # 复选框 + check_frame = tk.LabelFrame(root, text="兴趣爱好", padx=10, pady=10) + check_frame.pack(pady=10, padx=20, fill=tk.X) + + hobbies = ["编程", "阅读", "音乐", "运动", "旅游"] + hobby_vars = {} + + for hobby in hobbies: + var = tk.BooleanVar() + hobby_vars[hobby] = var + cb = tk.Checkbutton(check_frame, text=hobby, variable=var) + cb.pack(side=tk.LEFT, padx=5) + + def show_selected_hobbies(): + selected = [hobby for hobby, var in hobby_vars.items() if var.get()] + if selected: + messagebox.showinfo("选择的爱好", f"您选择了: {', '.join(selected)}") + else: + messagebox.showinfo("选择的爱好", "您没有选择任何爱好") + + tk.Button(check_frame, text="查看选择", + command=show_selected_hobbies).pack(side=tk.RIGHT, padx=5) + + # 单选按钮 + radio_frame = tk.LabelFrame(root, text="选择性别", padx=10, pady=10) + radio_frame.pack(pady=5, padx=20, fill=tk.X) + + gender_var = tk.StringVar(value="男") + genders = [("男", "male"), ("女", "female"), ("其他", "other")] + + for text, value in genders: + rb = tk.Radiobutton(radio_frame, text=text, + variable=gender_var, value=value) + rb.pack(side=tk.LEFT, padx=10) + + def show_selected_gender(): + selected = gender_var.get() + messagebox.showinfo("选择的性别", f"您选择了: {selected}") + + tk.Button(radio_frame, text="查看选择", + command=show_selected_gender).pack(side=tk.RIGHT, padx=5) + + # 窗口关闭事件 + def on_closing(): + if messagebox.askokcancel("退出", "确定要退出程序吗?"): + print("\n程序正在退出...") + root.destroy() + + root.protocol("WM_DELETE_WINDOW", on_closing) + + print("\n基础控件演示窗口已创建,请查看GUI界面") + print("关闭窗口以继续下一个演示") + + # 启动事件循环 + root.mainloop() + +# 运行Tkinter基础演示 +tkinter_basics_demo() +``` + +### 2.2 布局管理 + +```python +import tkinter as tk +from tkinter import ttk + +def layout_management_demo(): + """布局管理演示""" + print("=== 布局管理演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("布局管理演示") + root.geometry("800x600") + + # 创建笔记本控件来展示不同的布局方式 + notebook = ttk.Notebook(root) + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 1. Pack布局演示 + print("\n1. Pack布局管理器") + + pack_frame = tk.Frame(notebook) + notebook.add(pack_frame, text="Pack布局") + + # Pack布局说明 + tk.Label(pack_frame, text="Pack布局管理器", + font=("Arial", 14, "bold")).pack(pady=10) + + tk.Label(pack_frame, + text="Pack按照添加顺序依次排列控件,支持top、bottom、left、right四个方向", + wraplength=400).pack(pady=5) + + # Pack示例 + pack_demo_frame = tk.LabelFrame(pack_frame, text="Pack示例", padx=10, pady=10) + pack_demo_frame.pack(pady=10, padx=20, fill=tk.X) + + tk.Button(pack_demo_frame, text="Top 1", bg="lightblue").pack(side=tk.TOP, pady=2) + tk.Button(pack_demo_frame, text="Top 2", bg="lightgreen").pack(side=tk.TOP, pady=2) + + bottom_frame = tk.Frame(pack_demo_frame) + bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5) + tk.Button(bottom_frame, text="Bottom 1", bg="lightcoral").pack(side=tk.LEFT, padx=2) + tk.Button(bottom_frame, text="Bottom 2", bg="lightyellow").pack(side=tk.RIGHT, padx=2) + + middle_frame = tk.Frame(pack_demo_frame) + middle_frame.pack(fill=tk.BOTH, expand=True, pady=5) + tk.Button(middle_frame, text="Left", bg="lightpink").pack(side=tk.LEFT, padx=2) + tk.Button(middle_frame, text="Right", bg="lightgray").pack(side=tk.RIGHT, padx=2) + tk.Label(middle_frame, text="Center Area", bg="white", + relief=tk.SUNKEN).pack(fill=tk.BOTH, expand=True, padx=5) + + # 2. Grid布局演示 + print("\n2. Grid布局管理器") + + grid_frame = tk.Frame(notebook) + notebook.add(grid_frame, text="Grid布局") + + tk.Label(grid_frame, text="Grid布局管理器", + font=("Arial", 14, "bold")).pack(pady=10) + + tk.Label(grid_frame, + text="Grid使用行列网格来精确控制控件位置,适合复杂布局", + wraplength=400).pack(pady=5) + + # Grid示例 - 计算器布局 + grid_demo_frame = tk.LabelFrame(grid_frame, text="Grid示例 - 计算器布局", + padx=10, pady=10) + grid_demo_frame.pack(pady=10, padx=20) + + # 显示屏 + display = tk.Entry(grid_demo_frame, width=20, font=("Arial", 14), + justify=tk.RIGHT, state="readonly") + display.grid(row=0, column=0, columnspan=4, padx=5, pady=5, sticky="ew") + + # 按钮布局 + buttons = [ + ['C', '±', '%', '÷'], + ['7', '8', '9', '×'], + ['4', '5', '6', '-'], + ['1', '2', '3', '+'], + ['0', '', '.', '='] + ] + + for i, row in enumerate(buttons, 1): + for j, text in enumerate(row): + if text: # 跳过空按钮 + if text == '0': + # 0按钮占两列 + btn = tk.Button(grid_demo_frame, text=text, width=5, height=2) + btn.grid(row=i, column=j, columnspan=2, padx=2, pady=2, sticky="ew") + elif text in ['÷', '×', '-', '+', '=']: + # 运算符按钮 + btn = tk.Button(grid_demo_frame, text=text, width=5, height=2, + bg="orange", fg="white") + btn.grid(row=i, column=j, padx=2, pady=2, sticky="ew") + elif text in ['C', '±', '%']: + # 功能按钮 + btn = tk.Button(grid_demo_frame, text=text, width=5, height=2, + bg="lightgray") + btn.grid(row=i, column=j, padx=2, pady=2, sticky="ew") + else: + # 数字按钮 + btn = tk.Button(grid_demo_frame, text=text, width=5, height=2) + btn.grid(row=i, column=j, padx=2, pady=2, sticky="ew") + + # 3. Place布局演示 + print("\n3. Place布局管理器") + + place_frame = tk.Frame(notebook) + notebook.add(place_frame, text="Place布局") + + tk.Label(place_frame, text="Place布局管理器", + font=("Arial", 14, "bold")).pack(pady=10) + + tk.Label(place_frame, + text="Place使用绝对或相对坐标精确定位控件,适合特殊布局需求", + wraplength=400).pack(pady=5) + + # Place示例 + place_demo_frame = tk.LabelFrame(place_frame, text="Place示例", + width=400, height=300) + place_demo_frame.pack(pady=10, padx=20) + place_demo_frame.pack_propagate(False) # 防止框架自动调整大小 + + # 绝对定位 + tk.Button(place_demo_frame, text="绝对定位(10,10)", bg="lightblue").place(x=10, y=10) + tk.Button(place_demo_frame, text="绝对定位(200,50)", bg="lightgreen").place(x=200, y=50) + + # 相对定位 + tk.Button(place_demo_frame, text="相对定位(50%,30%)", + bg="lightcoral").place(relx=0.5, rely=0.3, anchor=tk.CENTER) + + tk.Button(place_demo_frame, text="右下角", + bg="lightyellow").place(relx=1.0, rely=1.0, anchor=tk.SE, x=-10, y=-10) + + # 大小控制 + tk.Label(place_demo_frame, text="相对大小控制", bg="lightpink", + relief=tk.RAISED).place(relx=0.1, rely=0.6, relwidth=0.8, relheight=0.2) + + # 4. 混合布局演示 + print("\n4. 混合布局管理器") + + mixed_frame = tk.Frame(notebook) + notebook.add(mixed_frame, text="混合布局") + + tk.Label(mixed_frame, text="混合布局管理器", + font=("Arial", 14, "bold")).pack(pady=10) + + tk.Label(mixed_frame, + text="在不同的容器中可以使用不同的布局管理器,实现复杂界面", + wraplength=400).pack(pady=5) + + # 混合布局示例 - 文本编辑器界面 + mixed_demo_frame = tk.Frame(mixed_frame) + mixed_demo_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) + + # 顶部工具栏 - 使用Pack + toolbar = tk.Frame(mixed_demo_frame, bg="lightgray", height=40) + toolbar.pack(fill=tk.X, pady=(0, 5)) + toolbar.pack_propagate(False) + + tk.Button(toolbar, text="新建").pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="打开").pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="保存").pack(side=tk.LEFT, padx=5, pady=5) + + ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5) + + tk.Button(toolbar, text="复制").pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="粘贴").pack(side=tk.LEFT, padx=5, pady=5) + + # 主要内容区域 - 使用Grid + content_frame = tk.Frame(mixed_demo_frame) + content_frame.pack(fill=tk.BOTH, expand=True) + + # 配置Grid权重 + content_frame.grid_rowconfigure(0, weight=1) + content_frame.grid_columnconfigure(1, weight=1) + + # 左侧文件树 + tree_frame = tk.LabelFrame(content_frame, text="文件", width=150) + tree_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5)) + tree_frame.grid_propagate(False) + + file_list = tk.Listbox(tree_frame) + file_list.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + for i in range(1, 6): + file_list.insert(tk.END, f"文件{i}.txt") + + # 右侧编辑区域 + edit_frame = tk.LabelFrame(content_frame, text="编辑器") + edit_frame.grid(row=0, column=1, sticky="nsew") + + edit_text = tk.Text(edit_frame, wrap=tk.WORD) + edit_scrollbar = tk.Scrollbar(edit_frame) + + edit_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0), pady=5) + edit_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5, padx=(0, 5)) + + edit_text.config(yscrollcommand=edit_scrollbar.set) + edit_scrollbar.config(command=edit_text.yview) + + # 底部状态栏 - 使用Pack + status_bar = tk.Frame(mixed_demo_frame, bg="lightgray", height=25) + status_bar.pack(fill=tk.X, pady=(5, 0)) + status_bar.pack_propagate(False) + + tk.Label(status_bar, text="就绪", bg="lightgray").pack(side=tk.LEFT, padx=5) + tk.Label(status_bar, text="行: 1, 列: 1", bg="lightgray").pack(side=tk.RIGHT, padx=5) + + print("\n布局管理演示窗口已创建,请查看GUI界面") + print("关闭窗口以继续下一个演示") + + # 启动事件循环 + root.mainloop() + +# 运行布局管理演示 +layout_management_demo() +``` + +--- + +## 三、事件处理 + +### 3.1 事件绑定和处理 + +```python +import tkinter as tk +from tkinter import messagebox +import time + +def event_handling_demo(): + """事件处理演示""" + print("=== 事件处理演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("事件处理演示") + root.geometry("700x600") + + # 事件日志显示区域 + log_frame = tk.LabelFrame(root, text="事件日志", padx=5, pady=5) + log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + log_text = tk.Text(log_frame, height=15, font=("Consolas", 10)) + log_scrollbar = tk.Scrollbar(log_frame) + + log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + log_text.config(yscrollcommand=log_scrollbar.set) + log_scrollbar.config(command=log_text.yview) + + def log_event(message): + """记录事件到日志""" + timestamp = time.strftime("%H:%M:%S") + log_text.insert(tk.END, f"[{timestamp}] {message}\n") + log_text.see(tk.END) + print(f" 事件: {message}") + + def clear_log(): + """清空日志""" + log_text.delete("1.0", tk.END) + log_event("日志已清空") + + # 1. 鼠标事件 + print("\n1. 鼠标事件处理") + + mouse_frame = tk.LabelFrame(root, text="鼠标事件测试区域", padx=10, pady=10) + mouse_frame.pack(fill=tk.X, padx=10, pady=5) + + mouse_label = tk.Label(mouse_frame, text="鼠标事件测试区域", + bg="lightblue", width=30, height=3, + relief=tk.RAISED, cursor="hand2") + mouse_label.pack(pady=5) + + # 鼠标事件处理函数 + def on_mouse_click(event): + log_event(f"鼠标点击: 按钮{event.num}, 坐标({event.x}, {event.y})") + + def on_mouse_double_click(event): + log_event(f"鼠标双击: 坐标({event.x}, {event.y})") + mouse_label.config(bg="lightgreen") + root.after(500, lambda: mouse_label.config(bg="lightblue")) + + def on_mouse_enter(event): + log_event("鼠标进入区域") + mouse_label.config(relief=tk.SUNKEN) + + def on_mouse_leave(event): + log_event("鼠标离开区域") + mouse_label.config(relief=tk.RAISED) + + def on_mouse_motion(event): + # 只记录每10个像素的移动,避免日志过多 + if event.x % 10 == 0 and event.y % 10 == 0: + log_event(f"鼠标移动: 坐标({event.x}, {event.y})") + + # 绑定鼠标事件 + mouse_label.bind("", on_mouse_click) # 左键点击 + mouse_label.bind("", on_mouse_click) # 右键点击 + mouse_label.bind("", on_mouse_double_click) # 双击 + mouse_label.bind("", on_mouse_enter) # 鼠标进入 + mouse_label.bind("", on_mouse_leave) # 鼠标离开 + mouse_label.bind("", on_mouse_motion) # 鼠标移动 + + # 2. 键盘事件 + print("\n2. 键盘事件处理") + + keyboard_frame = tk.LabelFrame(root, text="键盘事件测试", padx=10, pady=10) + keyboard_frame.pack(fill=tk.X, padx=10, pady=5) + + tk.Label(keyboard_frame, text="点击下面的输入框并输入内容:").pack() + + keyboard_entry = tk.Entry(keyboard_frame, font=("Arial", 12), width=40) + keyboard_entry.pack(pady=5) + keyboard_entry.focus_set() # 设置焦点 + + # 键盘事件处理函数 + def on_key_press(event): + key_info = f"按键按下: '{event.char}' (键码: {event.keycode})" + if event.char.isprintable(): + log_event(key_info) + elif event.keysym in ['Return', 'BackSpace', 'Delete', 'Tab']: + log_event(f"特殊键按下: {event.keysym}") + + def on_key_release(event): + if event.keysym == 'Return': + content = keyboard_entry.get() + log_event(f"回车键释放,当前内容: '{content}'") + + def on_focus_in(event): + log_event("输入框获得焦点") + keyboard_entry.config(bg="lightyellow") + + def on_focus_out(event): + log_event("输入框失去焦点") + keyboard_entry.config(bg="white") + + # 绑定键盘事件 + keyboard_entry.bind("", on_key_press) + keyboard_entry.bind("", on_key_release) + keyboard_entry.bind("", on_focus_in) + keyboard_entry.bind("", on_focus_out) + + # 3. 窗口事件 + print("\n3. 窗口事件处理") + + def on_window_resize(event): + if event.widget == root: # 只处理主窗口的调整事件 + log_event(f"窗口大小调整: {event.width}x{event.height}") + + def on_window_focus_in(event): + if event.widget == root: + log_event("窗口获得焦点") + + def on_window_focus_out(event): + if event.widget == root: + log_event("窗口失去焦点") + + # 绑定窗口事件 + root.bind("", on_window_resize) + root.bind("", on_window_focus_in) + root.bind("", on_window_focus_out) + + # 4. 自定义事件 + print("\n4. 自定义事件处理") + + custom_frame = tk.LabelFrame(root, text="自定义事件", padx=10, pady=10) + custom_frame.pack(fill=tk.X, padx=10, pady=5) + + # 自定义事件处理函数 + def trigger_custom_event(): + # 生成自定义事件 + root.event_generate("<>", when="tail") + log_event("触发自定义事件") + + def on_custom_event(event): + log_event("处理自定义事件") + messagebox.showinfo("自定义事件", "自定义事件被触发了!") + + # 绑定自定义事件 + root.bind("<>", on_custom_event) + + tk.Button(custom_frame, text="触发自定义事件", + command=trigger_custom_event).pack(side=tk.LEFT, padx=5) + + # 5. 定时器事件 + print("\n5. 定时器事件处理") + + timer_running = False + timer_count = 0 + + def timer_callback(): + global timer_count + if timer_running: + timer_count += 1 + log_event(f"定时器事件: 第{timer_count}次") + # 每秒触发一次 + root.after(1000, timer_callback) + + def start_timer(): + global timer_running, timer_count + if not timer_running: + timer_running = True + timer_count = 0 + log_event("启动定时器") + timer_callback() + + def stop_timer(): + global timer_running + if timer_running: + timer_running = False + log_event("停止定时器") + + tk.Button(custom_frame, text="启动定时器", + command=start_timer).pack(side=tk.LEFT, padx=5) + tk.Button(custom_frame, text="停止定时器", + command=stop_timer).pack(side=tk.LEFT, padx=5) + + # 控制按钮 + control_frame = tk.Frame(root) + control_frame.pack(fill=tk.X, padx=10, pady=5) + + tk.Button(control_frame, text="清空日志", + command=clear_log).pack(side=tk.LEFT, padx=5) + + def show_help(): + help_text = """事件处理演示说明: + +1. 鼠标事件: 在蓝色区域进行鼠标操作 +2. 键盘事件: 在输入框中输入内容 +3. 窗口事件: 调整窗口大小或切换焦点 +4. 自定义事件: 点击按钮触发 +5. 定时器事件: 启动/停止定时器 + +所有事件都会记录在日志中。""" + messagebox.showinfo("帮助", help_text) + + tk.Button(control_frame, text="帮助", + command=show_help).pack(side=tk.RIGHT, padx=5) + + # 初始化日志 + log_event("事件处理演示程序启动") + + print("\n事件处理演示窗口已创建,请查看GUI界面") + print("尝试各种操作来触发不同的事件") + print("关闭窗口以继续下一个演示") + + # 窗口关闭事件 + def on_closing(): + stop_timer() # 停止定时器 + log_event("程序即将退出") + root.destroy() + + root.protocol("WM_DELETE_WINDOW", on_closing) + + # 启动事件循环 + root.mainloop() + +# 运行事件处理演示 +event_handling_demo() +``` + +--- + +## 四、高级控件 + +### 4.1 菜单和对话框 + +```python +import tkinter as tk +from tkinter import ttk, messagebox, filedialog, colorchooser, simpledialog +import os + +def advanced_widgets_demo(): + """高级控件演示""" + print("=== 高级控件演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("高级控件演示") + root.geometry("800x600") + + # 状态变量 + current_file = None + + # 1. 菜单栏 + print("\n1. 创建菜单栏") + + menubar = tk.Menu(root) + root.config(menu=menubar) + + # 文件菜单 + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="文件", menu=file_menu) + + def new_file(): + global current_file + if messagebox.askyesno("新建文件", "确定要新建文件吗?未保存的内容将丢失。"): + text_area.delete("1.0", tk.END) + current_file = None + root.title("高级控件演示 - 新文件") + status_var.set("新建文件") + + def open_file(): + global current_file + filename = filedialog.askopenfilename( + title="打开文件", + filetypes=[("文本文件", "*.txt"), ("Python文件", "*.py"), ("所有文件", "*.*")] + ) + if filename: + try: + with open(filename, 'r', encoding='utf-8') as file: + content = file.read() + text_area.delete("1.0", tk.END) + text_area.insert("1.0", content) + current_file = filename + root.title(f"高级控件演示 - {os.path.basename(filename)}") + status_var.set(f"已打开: {filename}") + except Exception as e: + messagebox.showerror("错误", f"无法打开文件: {e}") + + def save_file(): + global current_file + if current_file: + try: + content = text_area.get("1.0", tk.END) + with open(current_file, 'w', encoding='utf-8') as file: + file.write(content) + status_var.set(f"已保存: {current_file}") + except Exception as e: + messagebox.showerror("错误", f"无法保存文件: {e}") + else: + save_as_file() + + def save_as_file(): + global current_file + filename = filedialog.asksaveasfilename( + title="另存为", + defaultextension=".txt", + filetypes=[("文本文件", "*.txt"), ("Python文件", "*.py"), ("所有文件", "*.*")] + ) + if filename: + try: + content = text_area.get("1.0", tk.END) + with open(filename, 'w', encoding='utf-8') as file: + file.write(content) + current_file = filename + root.title(f"高级控件演示 - {os.path.basename(filename)}") + status_var.set(f"已保存为: {filename}") + except Exception as e: + messagebox.showerror("错误", f"无法保存文件: {e}") + + def exit_app(): + if messagebox.askyesno("退出", "确定要退出程序吗?"): + root.destroy() + + file_menu.add_command(label="新建", command=new_file, accelerator="Ctrl+N") + file_menu.add_command(label="打开", command=open_file, accelerator="Ctrl+O") + file_menu.add_separator() + file_menu.add_command(label="保存", command=save_file, accelerator="Ctrl+S") + file_menu.add_command(label="另存为", command=save_as_file, accelerator="Ctrl+Shift+S") + file_menu.add_separator() + file_menu.add_command(label="退出", command=exit_app, accelerator="Ctrl+Q") + + # 编辑菜单 + edit_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="编辑", menu=edit_menu) + + def undo(): + try: + text_area.edit_undo() + except tk.TclError: + pass + + def redo(): + try: + text_area.edit_redo() + except tk.TclError: + pass + + def cut(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def copy(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def paste(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def select_all(): + text_area.tag_add(tk.SEL, "1.0", tk.END) + text_area.mark_set(tk.INSERT, "1.0") + text_area.see(tk.INSERT) + + edit_menu.add_command(label="撤销", command=undo, accelerator="Ctrl+Z") + edit_menu.add_command(label="重做", command=redo, accelerator="Ctrl+Y") + edit_menu.add_separator() + edit_menu.add_command(label="剪切", command=cut, accelerator="Ctrl+X") + edit_menu.add_command(label="复制", command=copy, accelerator="Ctrl+C") + edit_menu.add_command(label="粘贴", command=paste, accelerator="Ctrl+V") + edit_menu.add_separator() + edit_menu.add_command(label="全选", command=select_all, accelerator="Ctrl+A") + + # 格式菜单 + format_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="格式", menu=format_menu) + + def change_font(): + # 简单的字体选择对话框 + font_families = ['Arial', 'Times New Roman', 'Courier New', '宋体', '微软雅黑'] + font_sizes = ['8', '10', '12', '14', '16', '18', '20', '24'] + + font_window = tk.Toplevel(root) + font_window.title("字体设置") + font_window.geometry("400x300") + font_window.transient(root) + font_window.grab_set() + + tk.Label(font_window, text="字体:").grid(row=0, column=0, sticky="w", padx=10, pady=5) + font_var = tk.StringVar(value="Arial") + font_combo = ttk.Combobox(font_window, textvariable=font_var, values=font_families) + font_combo.grid(row=0, column=1, padx=10, pady=5) + + tk.Label(font_window, text="大小:").grid(row=1, column=0, sticky="w", padx=10, pady=5) + size_var = tk.StringVar(value="12") + size_combo = ttk.Combobox(font_window, textvariable=size_var, values=font_sizes) + size_combo.grid(row=1, column=1, padx=10, pady=5) + + def apply_font(): + font_family = font_var.get() + font_size = int(size_var.get()) + text_area.config(font=(font_family, font_size)) + font_window.destroy() + + tk.Button(font_window, text="确定", command=apply_font).grid(row=2, column=0, columnspan=2, pady=20) + + def change_color(): + color = colorchooser.askcolor(title="选择文字颜色") + if color[1]: # 如果选择了颜色 + text_area.config(fg=color[1]) + + def change_bg_color(): + color = colorchooser.askcolor(title="选择背景颜色") + if color[1]: # 如果选择了颜色 + text_area.config(bg=color[1]) + + format_menu.add_command(label="字体", command=change_font) + format_menu.add_command(label="文字颜色", command=change_color) + format_menu.add_command(label="背景颜色", command=change_bg_color) + + # 帮助菜单 + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="帮助", menu=help_menu) + + def show_about(): + messagebox.showinfo("关于", "高级控件演示程序\n\n版本: 1.0\n作者: Python学习者") + + help_menu.add_command(label="关于", command=show_about) + + # 2. 工具栏 + print("\n2. 创建工具栏") + + toolbar = tk.Frame(root, bg="lightgray", height=40) + toolbar.pack(fill=tk.X) + toolbar.pack_propagate(False) + + # 工具栏按钮 + tk.Button(toolbar, text="新建", command=new_file, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="打开", command=open_file, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="保存", command=save_file, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + + ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5) + + tk.Button(toolbar, text="剪切", command=cut, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="复制", command=copy, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="粘贴", command=paste, relief=tk.FLAT).pack(side=tk.LEFT, padx=2, pady=2) + + # 3. 主要内容区域 + print("\n3. 创建主要内容区域") + + # 创建文本编辑区域 + text_frame = tk.Frame(root) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + text_area = tk.Text(text_frame, wrap=tk.WORD, undo=True, font=("Consolas", 12)) + text_scrollbar_y = tk.Scrollbar(text_frame, orient=tk.VERTICAL) + text_scrollbar_x = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL) + + text_area.grid(row=0, column=0, sticky="nsew") + text_scrollbar_y.grid(row=0, column=1, sticky="ns") + text_scrollbar_x.grid(row=1, column=0, sticky="ew") + + text_frame.grid_rowconfigure(0, weight=1) + text_frame.grid_columnconfigure(0, weight=1) + + text_area.config(yscrollcommand=text_scrollbar_y.set, xscrollcommand=text_scrollbar_x.set) + text_scrollbar_y.config(command=text_area.yview) + text_scrollbar_x.config(command=text_area.xview) + + # 插入示例文本 + sample_text = """欢迎使用高级控件演示程序! + +这是一个功能完整的文本编辑器示例,包含以下功能: + +1. 完整的菜单栏 + - 文件操作:新建、打开、保存、另存为 + - 编辑操作:撤销、重做、剪切、复制、粘贴、全选 + - 格式设置:字体、颜色 + - 帮助信息 + +2. 工具栏快捷按钮 + +3. 文本编辑区域 + - 支持撤销/重做 + - 支持滚动条 + - 支持自动换行 + +4. 状态栏显示 + +试试使用菜单和工具栏的各种功能吧!""" + + text_area.insert("1.0", sample_text) + + # 4. 状态栏 + print("\n4. 创建状态栏") + + status_frame = tk.Frame(root, bg="lightgray", height=25) + status_frame.pack(fill=tk.X) + status_frame.pack_propagate(False) + + status_var = tk.StringVar(value="就绪") + status_label = tk.Label(status_frame, textvariable=status_var, bg="lightgray") + status_label.pack(side=tk.LEFT, padx=5) + + # 显示光标位置 + cursor_var = tk.StringVar(value="行: 1, 列: 1") + cursor_label = tk.Label(status_frame, textvariable=cursor_var, bg="lightgray") + cursor_label.pack(side=tk.RIGHT, padx=5) + + def update_cursor_position(event=None): + cursor_pos = text_area.index(tk.INSERT) + line, col = cursor_pos.split('.') + cursor_var.set(f"行: {line}, 列: {int(col)+1}") + + text_area.bind("", update_cursor_position) + text_area.bind("", update_cursor_position) + + # 5. 键盘快捷键 + print("\n5. 绑定键盘快捷键") + + root.bind("", lambda e: new_file()) + root.bind("", lambda e: open_file()) + root.bind("", lambda e: save_file()) + root.bind("", lambda e: save_as_file()) + root.bind("", lambda e: exit_app()) + root.bind("", lambda e: undo()) + root.bind("", lambda e: redo()) + root.bind("", lambda e: select_all()) + + # 6. 右键菜单 + print("\n6. 创建右键菜单") + + context_menu = tk.Menu(root, tearoff=0) + context_menu.add_command(label="剪切", command=cut) + context_menu.add_command(label="复制", command=copy) + context_menu.add_command(label="粘贴", command=paste) + context_menu.add_separator() + context_menu.add_command(label="全选", command=select_all) + + def show_context_menu(event): + try: + context_menu.tk_popup(event.x_root, event.y_root) + finally: + context_menu.grab_release() + + text_area.bind("", show_context_menu) # 右键点击 + + print("\n高级控件演示窗口已创建,请查看GUI界面") + print("尝试使用菜单、工具栏和各种功能") + print("关闭窗口以继续下一个演示") + + # 启动事件循环 + root.mainloop() + +# 运行高级控件演示 +advanced_widgets_demo() +``` + +### 4.2 列表和树形控件 + +```python +import tkinter as tk +from tkinter import ttk, messagebox +import random +import datetime + +def list_tree_demo(): + """列表和树形控件演示""" + print("=== 列表和树形控件演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("列表和树形控件演示") + root.geometry("900x700") + + # 创建笔记本控件 + notebook = ttk.Notebook(root) + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 1. 列表框演示 + print("\n1. 列表框控件演示") + + listbox_frame = tk.Frame(notebook) + notebook.add(listbox_frame, text="列表框") + + tk.Label(listbox_frame, text="列表框控件演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 创建列表框区域 + list_container = tk.Frame(listbox_frame) + list_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) + + # 左侧 - 单选列表框 + left_frame = tk.LabelFrame(list_container, text="单选列表框", padx=10, pady=10) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10)) + + single_listbox = tk.Listbox(left_frame, selectmode=tk.SINGLE, height=10) + single_scrollbar = tk.Scrollbar(left_frame, orient=tk.VERTICAL) + + single_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + single_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + single_listbox.config(yscrollcommand=single_scrollbar.set) + single_scrollbar.config(command=single_listbox.yview) + + # 添加示例数据 + cities = ["北京", "上海", "广州", "深圳", "杭州", "南京", "武汉", "成都", "西安", "重庆"] + for city in cities: + single_listbox.insert(tk.END, city) + + def on_single_select(event): + selection = single_listbox.curselection() + if selection: + selected_city = single_listbox.get(selection[0]) + messagebox.showinfo("选择", f"您选择了: {selected_city}") + + single_listbox.bind("<>", on_single_select) + + # 右侧 - 多选列表框 + right_frame = tk.LabelFrame(list_container, text="多选列表框", padx=10, pady=10) + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0)) + + multi_listbox = tk.Listbox(right_frame, selectmode=tk.MULTIPLE, height=10) + multi_scrollbar = tk.Scrollbar(right_frame, orient=tk.VERTICAL) + + multi_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + multi_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + multi_listbox.config(yscrollcommand=multi_scrollbar.set) + multi_scrollbar.config(command=multi_listbox.yview) + + # 添加示例数据 + languages = ["Python", "Java", "C++", "JavaScript", "Go", "Rust", "Swift", "Kotlin", "C#", "PHP"] + for lang in languages: + multi_listbox.insert(tk.END, lang) + + def show_multi_selection(): + selections = multi_listbox.curselection() + if selections: + selected_langs = [multi_listbox.get(i) for i in selections] + messagebox.showinfo("多选结果", f"您选择了: {', '.join(selected_langs)}") + else: + messagebox.showinfo("多选结果", "没有选择任何项目") + + tk.Button(right_frame, text="查看选择", command=show_multi_selection).pack(pady=5) + + # 2. 树形控件演示 + print("\n2. 树形控件演示") + + tree_frame = tk.Frame(notebook) + notebook.add(tree_frame, text="树形控件") + + tk.Label(tree_frame, text="树形控件演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 创建树形控件 + tree_container = tk.Frame(tree_frame) + tree_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) + + # 定义列 + columns = ("name", "type", "size", "modified") + tree = ttk.Treeview(tree_container, columns=columns, show="tree headings", height=15) + + # 配置列 + tree.heading("#0", text="文件/文件夹", anchor=tk.W) + tree.heading("name", text="名称", anchor=tk.W) + tree.heading("type", text="类型", anchor=tk.W) + tree.heading("size", text="大小", anchor=tk.E) + tree.heading("modified", text="修改时间", anchor=tk.W) + + tree.column("#0", width=200, minwidth=100) + tree.column("name", width=150, minwidth=100) + tree.column("type", width=100, minwidth=80) + tree.column("size", width=100, minwidth=80) + tree.column("modified", width=150, minwidth=120) + + # 添加滚动条 + tree_scrollbar_y = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=tree.yview) + tree_scrollbar_x = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL, command=tree.xview) + tree.configure(yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set) + + tree.grid(row=0, column=0, sticky="nsew") + tree_scrollbar_y.grid(row=0, column=1, sticky="ns") + tree_scrollbar_x.grid(row=1, column=0, sticky="ew") + + tree_container.grid_rowconfigure(0, weight=1) + tree_container.grid_columnconfigure(0, weight=1) + + # 添加示例数据 + def add_sample_data(): + # 根目录 + root_id = tree.insert("", "end", text="我的电脑", values=("", "文件夹", "", "")) + + # C盘 + c_drive = tree.insert(root_id, "end", text="C:\\", values=("C:\\", "磁盘", "500 GB", "2023-01-01")) + + # 程序文件夹 + program_files = tree.insert(c_drive, "end", text="Program Files", + values=("Program Files", "文件夹", "15 GB", "2023-06-01")) + + # Python文件夹 + python_folder = tree.insert(program_files, "end", text="Python", + values=("Python", "文件夹", "2 GB", "2023-06-15")) + + # Python文件 + tree.insert(python_folder, "end", text="python.exe", + values=("python.exe", "应用程序", "25 MB", "2023-06-15")) + tree.insert(python_folder, "end", text="pip.exe", + values=("pip.exe", "应用程序", "5 MB", "2023-06-15")) + + # 用户文件夹 + users = tree.insert(c_drive, "end", text="Users", + values=("Users", "文件夹", "50 GB", "2023-05-01")) + + user_folder = tree.insert(users, "end", text="用户", + values=("用户", "文件夹", "30 GB", "2023-06-01")) + + # 文档文件夹 + documents = tree.insert(user_folder, "end", text="Documents", + values=("Documents", "文件夹", "10 GB", "2023-06-10")) + + # 示例文档 + tree.insert(documents, "end", text="report.docx", + values=("report.docx", "Word文档", "2 MB", "2023-06-15")) + tree.insert(documents, "end", text="data.xlsx", + values=("data.xlsx", "Excel文档", "5 MB", "2023-06-14")) + tree.insert(documents, "end", text="presentation.pptx", + values=("presentation.pptx", "PowerPoint", "10 MB", "2023-06-13")) + + # D盘 + d_drive = tree.insert(root_id, "end", text="D:\\", values=("D:\\", "磁盘", "1 TB", "2023-01-01")) + + # 项目文件夹 + projects = tree.insert(d_drive, "end", text="Projects", + values=("Projects", "文件夹", "20 GB", "2023-06-01")) + + python_project = tree.insert(projects, "end", text="PythonProject", + values=("PythonProject", "文件夹", "500 MB", "2023-06-15")) + + tree.insert(python_project, "end", text="main.py", + values=("main.py", "Python文件", "10 KB", "2023-06-15")) + tree.insert(python_project, "end", text="requirements.txt", + values=("requirements.txt", "文本文件", "1 KB", "2023-06-10")) + + # 展开根节点 + tree.item(root_id, open=True) + tree.item(c_drive, open=True) + + add_sample_data() + + # 树形控件事件处理 + def on_tree_select(event): + selection = tree.selection() + if selection: + item = selection[0] + item_text = tree.item(item, "text") + item_values = tree.item(item, "values") + print(f" 选择了: {item_text}, 值: {item_values}") + + def on_tree_double_click(event): + selection = tree.selection() + if selection: + item = selection[0] + item_text = tree.item(item, "text") + messagebox.showinfo("双击", f"双击了: {item_text}") + + tree.bind("<>", on_tree_select) + tree.bind("", on_tree_double_click) + + # 树形控件操作按钮 + tree_buttons = tk.Frame(tree_frame) + tree_buttons.pack(fill=tk.X, padx=20, pady=5) + + def expand_all(): + def expand_item(item): + tree.item(item, open=True) + for child in tree.get_children(item): + expand_item(child) + + for item in tree.get_children(): + expand_item(item) + + def collapse_all(): + def collapse_item(item): + tree.item(item, open=False) + for child in tree.get_children(item): + collapse_item(child) + + for item in tree.get_children(): + collapse_item(item) + + def add_item(): + selection = tree.selection() + if selection: + parent = selection[0] + new_item = f"新项目_{random.randint(1, 1000)}" + tree.insert(parent, "end", text=new_item, + values=(new_item, "文件", "1 KB", datetime.datetime.now().strftime("%Y-%m-%d"))) + else: + messagebox.showwarning("警告", "请先选择一个父节点") + + def delete_item(): + selection = tree.selection() + if selection: + if messagebox.askyesno("确认", "确定要删除选中的项目吗?"): + tree.delete(selection[0]) + else: + messagebox.showwarning("警告", "请先选择要删除的项目") + + tk.Button(tree_buttons, text="展开全部", command=expand_all).pack(side=tk.LEFT, padx=5) + tk.Button(tree_buttons, text="折叠全部", command=collapse_all).pack(side=tk.LEFT, padx=5) + tk.Button(tree_buttons, text="添加项目", command=add_item).pack(side=tk.LEFT, padx=5) + tk.Button(tree_buttons, text="删除项目", command=delete_item).pack(side=tk.LEFT, padx=5) + + # 3. 组合框演示 + print("\n3. 组合框控件演示") + + combo_frame = tk.Frame(notebook) + notebook.add(combo_frame, text="组合框") + + tk.Label(combo_frame, text="组合框控件演示", + font=("Arial", 14, "bold")).pack(pady=10) + + combo_container = tk.Frame(combo_frame) + combo_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) + + # 只读组合框 + readonly_frame = tk.LabelFrame(combo_container, text="只读组合框", padx=10, pady=10) + readonly_frame.pack(fill=tk.X, pady=5) + + tk.Label(readonly_frame, text="选择编程语言:").pack(anchor=tk.W) + + readonly_var = tk.StringVar() + readonly_combo = ttk.Combobox(readonly_frame, textvariable=readonly_var, + values=languages, state="readonly", width=30) + readonly_combo.pack(pady=5, anchor=tk.W) + readonly_combo.set("Python") # 设置默认值 + + def on_readonly_select(event): + selected = readonly_var.get() + print(f" 选择了编程语言: {selected}") + + readonly_combo.bind("<>", on_readonly_select) + + # 可编辑组合框 + editable_frame = tk.LabelFrame(combo_container, text="可编辑组合框", padx=10, pady=10) + editable_frame.pack(fill=tk.X, pady=5) + + tk.Label(editable_frame, text="输入或选择城市:").pack(anchor=tk.W) + + editable_var = tk.StringVar() + editable_combo = ttk.Combobox(editable_frame, textvariable=editable_var, + values=cities, width=30) + editable_combo.pack(pady=5, anchor=tk.W) + + def on_editable_change(event): + current = editable_var.get() + print(f" 当前输入: {current}") + + editable_combo.bind("", on_editable_change) + editable_combo.bind("<>", on_editable_change) + + # 动态组合框 + dynamic_frame = tk.LabelFrame(combo_container, text="动态组合框", padx=10, pady=10) + dynamic_frame.pack(fill=tk.X, pady=5) + + tk.Label(dynamic_frame, text="类别:").pack(anchor=tk.W) + + category_var = tk.StringVar() + category_combo = ttk.Combobox(dynamic_frame, textvariable=category_var, + values=["水果", "蔬菜", "肉类"], state="readonly", width=30) + category_combo.pack(pady=5, anchor=tk.W) + + tk.Label(dynamic_frame, text="具体项目:").pack(anchor=tk.W) + + item_var = tk.StringVar() + item_combo = ttk.Combobox(dynamic_frame, textvariable=item_var, + state="readonly", width=30) + item_combo.pack(pady=5, anchor=tk.W) + + # 动态更新选项 + category_items = { + "水果": ["苹果", "香蕉", "橙子", "葡萄", "草莓"], + "蔬菜": ["白菜", "萝卜", "土豆", "西红柿", "黄瓜"], + "肉类": ["猪肉", "牛肉", "鸡肉", "鱼肉", "羊肉"] + } + + def update_items(event): + category = category_var.get() + if category in category_items: + item_combo['values'] = category_items[category] + item_combo.set("") # 清空当前选择 + + category_combo.bind("<>", update_items) + + # 显示选择结果 + result_frame = tk.Frame(combo_container) + result_frame.pack(fill=tk.X, pady=10) + + def show_selections(): + results = [] + if readonly_var.get(): + results.append(f"编程语言: {readonly_var.get()}") + if editable_var.get(): + results.append(f"城市: {editable_var.get()}") + if category_var.get() and item_var.get(): + results.append(f"食物: {category_var.get()} - {item_var.get()}") + + if results: + messagebox.showinfo("选择结果", "\n".join(results)) + else: + messagebox.showinfo("选择结果", "没有任何选择") + + tk.Button(result_frame, text="显示所有选择", command=show_selections).pack() + + print("\n列表和树形控件演示窗口已创建,请查看GUI界面") + print("关闭窗口以继续下一个演示") + + # 启动事件循环 + root.mainloop() + +# 运行列表和树形控件演示 +list_tree_demo() +``` + +### 4.3 画布控件 + +```python +import tkinter as tk +from tkinter import ttk, colorchooser +import math +import random + +def canvas_demo(): + """画布控件演示""" + print("=== 画布控件演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("画布控件演示") + root.geometry("1000x700") + + # 创建笔记本控件 + notebook = ttk.Notebook(root) + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 1. 基础绘图 + print("\n1. 基础绘图演示") + + basic_frame = tk.Frame(notebook) + notebook.add(basic_frame, text="基础绘图") + + tk.Label(basic_frame, text="基础绘图演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 创建画布 + basic_canvas = tk.Canvas(basic_frame, width=600, height=400, bg="white", relief=tk.SUNKEN, bd=2) + basic_canvas.pack(pady=10) + + # 绘制基本图形 + def draw_basic_shapes(): + basic_canvas.delete("all") # 清空画布 + + # 绘制线条 + basic_canvas.create_line(50, 50, 200, 50, fill="red", width=3) + basic_canvas.create_line(50, 70, 200, 120, fill="blue", width=2) + + # 绘制矩形 + basic_canvas.create_rectangle(250, 50, 350, 120, fill="lightblue", outline="blue", width=2) + basic_canvas.create_rectangle(370, 50, 470, 120, fill="", outline="green", width=3) + + # 绘制椭圆 + basic_canvas.create_oval(50, 150, 150, 220, fill="yellow", outline="orange", width=2) + basic_canvas.create_oval(170, 150, 220, 200, fill="pink", outline="red") + + # 绘制多边形 + points = [250, 150, 300, 180, 350, 150, 370, 200, 280, 220, 230, 200] + basic_canvas.create_polygon(points, fill="lightgreen", outline="darkgreen", width=2) + + # 绘制弧形 + basic_canvas.create_arc(400, 150, 500, 220, start=0, extent=180, fill="lightcoral", outline="red", width=2) + basic_canvas.create_arc(520, 150, 580, 220, start=45, extent=90, fill="", outline="purple", width=3, style=tk.ARC) + + # 绘制文本 + basic_canvas.create_text(300, 250, text="Canvas绘图演示", font=("Arial", 16, "bold"), fill="blue") + basic_canvas.create_text(300, 280, text="支持多种图形和文本", font=("宋体", 12), fill="darkgreen") + + # 绘制图像(如果有的话) + # photo = tk.PhotoImage(file="image.png") + # basic_canvas.create_image(300, 350, image=photo) + + # 绘图控制按钮 + basic_buttons = tk.Frame(basic_frame) + basic_buttons.pack(pady=5) + + tk.Button(basic_buttons, text="绘制图形", command=draw_basic_shapes).pack(side=tk.LEFT, padx=5) + tk.Button(basic_buttons, text="清空画布", command=lambda: basic_canvas.delete("all")).pack(side=tk.LEFT, padx=5) + + # 2. 交互式绘图 + print("\n2. 交互式绘图演示") + + interactive_frame = tk.Frame(notebook) + notebook.add(interactive_frame, text="交互绘图") + + tk.Label(interactive_frame, text="交互式绘图演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 工具栏 + toolbar = tk.Frame(interactive_frame) + toolbar.pack(fill=tk.X, padx=10, pady=5) + + # 绘图模式 + draw_mode = tk.StringVar(value="line") + + tk.Label(toolbar, text="绘图工具:").pack(side=tk.LEFT, padx=5) + + modes = [("线条", "line"), ("矩形", "rectangle"), ("椭圆", "oval"), ("自由绘制", "free")] + for text, mode in modes: + tk.Radiobutton(toolbar, text=text, variable=draw_mode, value=mode).pack(side=tk.LEFT, padx=2) + + # 颜色选择 + current_color = tk.StringVar(value="black") + + def choose_color(): + color = colorchooser.askcolor(title="选择颜色") + if color[1]: + current_color.set(color[1]) + color_label.config(bg=color[1]) + + tk.Label(toolbar, text="颜色:").pack(side=tk.LEFT, padx=(20, 5)) + color_label = tk.Label(toolbar, bg="black", width=3, relief=tk.RAISED, cursor="hand2") + color_label.pack(side=tk.LEFT, padx=2) + color_label.bind("", lambda e: choose_color()) + + # 线条粗细 + tk.Label(toolbar, text="粗细:").pack(side=tk.LEFT, padx=(20, 5)) + line_width = tk.Scale(toolbar, from_=1, to=10, orient=tk.HORIZONTAL, length=100) + line_width.set(2) + line_width.pack(side=tk.LEFT, padx=2) + + # 创建交互画布 + interactive_canvas = tk.Canvas(interactive_frame, width=700, height=450, bg="white", relief=tk.SUNKEN, bd=2) + interactive_canvas.pack(pady=10) + + # 绘图状态变量 + drawing = False + start_x = start_y = 0 + current_item = None + + def start_draw(event): + global drawing, start_x, start_y, current_item + drawing = True + start_x, start_y = event.x, event.y + + mode = draw_mode.get() + color = current_color.get() + width = line_width.get() + + if mode == "free": + # 自由绘制模式,立即开始绘制 + current_item = interactive_canvas.create_line(start_x, start_y, start_x, start_y, + fill=color, width=width, capstyle=tk.ROUND) + + def draw(event): + global current_item + if not drawing: + return + + mode = draw_mode.get() + color = current_color.get() + width = line_width.get() + + if mode == "line": + # 删除之前的预览线条 + if current_item: + interactive_canvas.delete(current_item) + # 绘制新的预览线条 + current_item = interactive_canvas.create_line(start_x, start_y, event.x, event.y, + fill=color, width=width) + + elif mode == "rectangle": + if current_item: + interactive_canvas.delete(current_item) + current_item = interactive_canvas.create_rectangle(start_x, start_y, event.x, event.y, + outline=color, width=width) + + elif mode == "oval": + if current_item: + interactive_canvas.delete(current_item) + current_item = interactive_canvas.create_oval(start_x, start_y, event.x, event.y, + outline=color, width=width) + + elif mode == "free": + # 自由绘制模式,扩展当前线条 + coords = interactive_canvas.coords(current_item) + coords.extend([event.x, event.y]) + interactive_canvas.coords(current_item, *coords) + + def end_draw(event): + global drawing, current_item + drawing = False + current_item = None + + # 绑定鼠标事件 + interactive_canvas.bind("", start_draw) + interactive_canvas.bind("", draw) + interactive_canvas.bind("", end_draw) + + # 交互控制按钮 + interactive_buttons = tk.Frame(interactive_frame) + interactive_buttons.pack(pady=5) + + def clear_canvas(): + interactive_canvas.delete("all") + + def save_canvas(): + # 这里可以实现保存功能 + print(" 保存画布内容(需要额外的库支持)") + + tk.Button(interactive_buttons, text="清空画布", command=clear_canvas).pack(side=tk.LEFT, padx=5) + tk.Button(interactive_buttons, text="保存画布", command=save_canvas).pack(side=tk.LEFT, padx=5) + + # 3. 动画演示 + print("\n3. 动画演示") + + animation_frame = tk.Frame(notebook) + notebook.add(animation_frame, text="动画演示") + + tk.Label(animation_frame, text="动画演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 创建动画画布 + anim_canvas = tk.Canvas(animation_frame, width=600, height=400, bg="black", relief=tk.SUNKEN, bd=2) + anim_canvas.pack(pady=10) + + # 动画对象 + class AnimatedBall: + def __init__(self, canvas, x, y, dx, dy, radius, color): + self.canvas = canvas + self.x = x + self.y = y + self.dx = dx + self.dy = dy + self.radius = radius + self.color = color + self.item = canvas.create_oval(x-radius, y-radius, x+radius, y+radius, + fill=color, outline="white") + + def move(self): + # 更新位置 + self.x += self.dx + self.y += self.dy + + # 边界检测 + canvas_width = int(self.canvas.cget("width")) + canvas_height = int(self.canvas.cget("height")) + + if self.x - self.radius <= 0 or self.x + self.radius >= canvas_width: + self.dx = -self.dx + if self.y - self.radius <= 0 or self.y + self.radius >= canvas_height: + self.dy = -self.dy + + # 更新画布上的位置 + self.canvas.coords(self.item, + self.x - self.radius, self.y - self.radius, + self.x + self.radius, self.y + self.radius) + + # 创建多个动画球 + balls = [] + colors = ["red", "blue", "green", "yellow", "orange", "purple", "cyan", "magenta"] + + def create_balls(): + anim_canvas.delete("all") + balls.clear() + + for i in range(8): + x = random.randint(50, 550) + y = random.randint(50, 350) + dx = random.randint(-5, 5) + dy = random.randint(-5, 5) + if dx == 0: dx = 1 + if dy == 0: dy = 1 + radius = random.randint(10, 25) + color = colors[i] + + ball = AnimatedBall(anim_canvas, x, y, dx, dy, radius, color) + balls.append(ball) + + # 动画控制 + animation_running = False + + def animate(): + if animation_running: + for ball in balls: + ball.move() + root.after(50, animate) # 每50毫秒更新一次 + + def start_animation(): + global animation_running + if not animation_running: + animation_running = True + animate() + + def stop_animation(): + global animation_running + animation_running = False + + # 动画控制按钮 + anim_buttons = tk.Frame(animation_frame) + anim_buttons.pack(pady=5) + + tk.Button(anim_buttons, text="创建球", command=create_balls).pack(side=tk.LEFT, padx=5) + tk.Button(anim_buttons, text="开始动画", command=start_animation).pack(side=tk.LEFT, padx=5) + tk.Button(anim_buttons, text="停止动画", command=stop_animation).pack(side=tk.LEFT, padx=5) + + # 4. 图表绘制 + print("\n4. 图表绘制演示") + + chart_frame = tk.Frame(notebook) + notebook.add(chart_frame, text="图表绘制") + + tk.Label(chart_frame, text="图表绘制演示", + font=("Arial", 14, "bold")).pack(pady=10) + + # 创建图表画布 + chart_canvas = tk.Canvas(chart_frame, width=700, height=450, bg="white", relief=tk.SUNKEN, bd=2) + chart_canvas.pack(pady=10) + + def draw_bar_chart(): + chart_canvas.delete("all") + + # 示例数据 + data = {"Python": 85, "Java": 70, "JavaScript": 65, "C++": 60, "Go": 45} + + # 图表参数 + margin = 50 + chart_width = 600 + chart_height = 350 + bar_width = (chart_width - 2 * margin) // len(data) + max_value = max(data.values()) + + # 绘制坐标轴 + chart_canvas.create_line(margin, margin, margin, chart_height + margin, width=2) # Y轴 + chart_canvas.create_line(margin, chart_height + margin, + chart_width + margin, chart_height + margin, width=2) # X轴 + + # 绘制标题 + chart_canvas.create_text(chart_width // 2 + margin, 20, + text="编程语言流行度", font=("Arial", 16, "bold")) + + # 绘制柱状图 + colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7"] + x = margin + 10 + + for i, (lang, value) in enumerate(data.items()): + # 计算柱子高度 + bar_height = (value / max_value) * (chart_height - 20) + + # 绘制柱子 + chart_canvas.create_rectangle(x, chart_height + margin - bar_height, + x + bar_width - 20, chart_height + margin, + fill=colors[i % len(colors)], outline="black") + + # 绘制标签 + chart_canvas.create_text(x + (bar_width - 20) // 2, chart_height + margin + 15, + text=lang, font=("Arial", 10)) + + # 绘制数值 + chart_canvas.create_text(x + (bar_width - 20) // 2, + chart_height + margin - bar_height - 10, + text=str(value), font=("Arial", 10, "bold")) + + x += bar_width + + def draw_line_chart(): + chart_canvas.delete("all") + + # 示例数据 + months = ["1月", "2月", "3月", "4月", "5月", "6月"] + sales = [120, 150, 180, 200, 170, 220] + + # 图表参数 + margin = 50 + chart_width = 600 + chart_height = 350 + + # 绘制坐标轴 + chart_canvas.create_line(margin, margin, margin, chart_height + margin, width=2) + chart_canvas.create_line(margin, chart_height + margin, + chart_width + margin, chart_height + margin, width=2) + + # 绘制标题 + chart_canvas.create_text(chart_width // 2 + margin, 20, + text="月度销售趋势", font=("Arial", 16, "bold")) + + # 计算点的位置 + x_step = chart_width // (len(months) - 1) + max_sales = max(sales) + min_sales = min(sales) + + points = [] + for i, sale in enumerate(sales): + x = margin + i * x_step + y = chart_height + margin - ((sale - min_sales) / (max_sales - min_sales)) * (chart_height - 20) + points.extend([x, y]) + + # 绘制数据点 + chart_canvas.create_oval(x-4, y-4, x+4, y+4, fill="red", outline="darkred") + + # 绘制数值标签 + chart_canvas.create_text(x, y-15, text=str(sale), font=("Arial", 10, "bold")) + + # 绘制月份标签 + chart_canvas.create_text(x, chart_height + margin + 15, + text=months[i], font=("Arial", 10)) + + # 绘制折线 + chart_canvas.create_line(points, fill="blue", width=3, smooth=True) + + def draw_pie_chart(): + chart_canvas.delete("all") + + # 示例数据 + data = {"桌面": 40, "移动": 35, "平板": 15, "其他": 10} + colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"] + + # 饼图参数 + center_x, center_y = 350, 225 + radius = 120 + + # 绘制标题 + chart_canvas.create_text(center_x, 50, text="设备使用分布", font=("Arial", 16, "bold")) + + # 计算角度 + total = sum(data.values()) + start_angle = 0 + + for i, (category, value) in enumerate(data.items()): + # 计算扇形角度 + extent = (value / total) * 360 + + # 绘制扇形 + chart_canvas.create_arc(center_x - radius, center_y - radius, + center_x + radius, center_y + radius, + start=start_angle, extent=extent, + fill=colors[i % len(colors)], outline="white", width=2) + + # 计算标签位置 + label_angle = math.radians(start_angle + extent / 2) + label_x = center_x + (radius + 30) * math.cos(label_angle) + label_y = center_y + (radius + 30) * math.sin(label_angle) + + # 绘制标签 + chart_canvas.create_text(label_x, label_y, + text=f"{category}\n{value}%", + font=("Arial", 10), justify=tk.CENTER) + + start_angle += extent + + # 图表控制按钮 + chart_buttons = tk.Frame(chart_frame) + chart_buttons.pack(pady=5) + + tk.Button(chart_buttons, text="柱状图", command=draw_bar_chart).pack(side=tk.LEFT, padx=5) + tk.Button(chart_buttons, text="折线图", command=draw_line_chart).pack(side=tk.LEFT, padx=5) + tk.Button(chart_buttons, text="饼图", command=draw_pie_chart).pack(side=tk.LEFT, padx=5) + tk.Button(chart_buttons, text="清空", command=lambda: chart_canvas.delete("all")).pack(side=tk.LEFT, padx=5) + + # 初始化 + draw_basic_shapes() + create_balls() + draw_bar_chart() + + print("\n画布控件演示窗口已创建,请查看GUI界面") + print("尝试不同的绘图和动画功能") + print("关闭窗口以继续下一个演示") + + # 窗口关闭事件 + def on_closing(): + stop_animation() # 停止动画 + root.destroy() + + root.protocol("WM_DELETE_WINDOW", on_closing) + + # 启动事件循环 + root.mainloop() + +# 运行画布控件演示 +canvas_demo() +``` + +## 5. 实际项目示例 + +### 5.1 简单计算器 + +```python +import tkinter as tk +from tkinter import ttk, messagebox +import math + +def calculator_app(): + """简单计算器应用""" + print("=== 计算器应用演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("Python计算器") + root.geometry("350x500") + root.resizable(False, False) + + # 设置样式 + style = ttk.Style() + style.theme_use('clam') + + # 计算器状态 + current_input = tk.StringVar(value="0") + operator = "" + first_number = 0 + should_reset = False + + # 显示屏 + display_frame = tk.Frame(root, bg="black", padx=10, pady=10) + display_frame.pack(fill=tk.X) + + display = tk.Label(display_frame, textvariable=current_input, + font=("Arial", 24, "bold"), bg="black", fg="white", + anchor="e", padx=10, pady=10) + display.pack(fill=tk.X) + + # 按钮框架 + button_frame = tk.Frame(root, padx=10, pady=10) + button_frame.pack(fill=tk.BOTH, expand=True) + + # 按钮样式配置 + button_config = { + 'font': ('Arial', 16, 'bold'), + 'width': 4, + 'height': 2 + } + + def clear_all(): + """清除所有""" + global operator, first_number, should_reset + current_input.set("0") + operator = "" + first_number = 0 + should_reset = False + + def clear_entry(): + """清除当前输入""" + current_input.set("0") + + def backspace(): + """退格""" + current = current_input.get() + if len(current) > 1: + current_input.set(current[:-1]) + else: + current_input.set("0") + + def input_number(num): + """输入数字""" + global should_reset + current = current_input.get() + + if should_reset or current == "0": + current_input.set(str(num)) + should_reset = False + else: + current_input.set(current + str(num)) + + def input_decimal(): + """输入小数点""" + global should_reset + current = current_input.get() + + if should_reset: + current_input.set("0.") + should_reset = False + elif "." not in current: + current_input.set(current + ".") + + def input_operator(op): + """输入运算符""" + global operator, first_number, should_reset + + try: + first_number = float(current_input.get()) + operator = op + should_reset = True + except ValueError: + messagebox.showerror("错误", "无效的数字") + + def calculate(): + """计算结果""" + global operator, first_number, should_reset + + try: + second_number = float(current_input.get()) + + if operator == "+": + result = first_number + second_number + elif operator == "-": + result = first_number - second_number + elif operator == "×": + result = first_number * second_number + elif operator == "÷": + if second_number == 0: + messagebox.showerror("错误", "除数不能为零") + return + result = first_number / second_number + elif operator == "^": + result = first_number ** second_number + else: + return + + # 格式化结果 + if result == int(result): + current_input.set(str(int(result))) + else: + current_input.set(f"{result:.10g}") + + operator = "" + should_reset = True + + except ValueError: + messagebox.showerror("错误", "无效的数字") + except Exception as e: + messagebox.showerror("错误", f"计算错误: {str(e)}") + + def calculate_sqrt(): + """计算平方根""" + try: + number = float(current_input.get()) + if number < 0: + messagebox.showerror("错误", "负数不能开平方根") + return + result = math.sqrt(number) + current_input.set(f"{result:.10g}") + except ValueError: + messagebox.showerror("错误", "无效的数字") + + def calculate_percent(): + """计算百分比""" + try: + number = float(current_input.get()) + result = number / 100 + current_input.set(f"{result:.10g}") + except ValueError: + messagebox.showerror("错误", "无效的数字") + + def toggle_sign(): + """切换正负号""" + try: + number = float(current_input.get()) + result = -number + if result == int(result): + current_input.set(str(int(result))) + else: + current_input.set(f"{result:.10g}") + except ValueError: + messagebox.showerror("错误", "无效的数字") + + # 创建按钮 + buttons = [ + # 第一行 + [("C", clear_all, "#FF6B6B"), ("CE", clear_entry, "#FF8E53"), ("⌫", backspace, "#FF8E53"), ("÷", lambda: input_operator("÷"), "#4ECDC4")], + # 第二行 + [("7", lambda: input_number(7), "#E8E8E8"), ("8", lambda: input_number(8), "#E8E8E8"), ("9", lambda: input_number(9), "#E8E8E8"), ("×", lambda: input_operator("×"), "#4ECDC4")], + # 第三行 + [("4", lambda: input_number(4), "#E8E8E8"), ("5", lambda: input_number(5), "#E8E8E8"), ("6", lambda: input_number(6), "#E8E8E8"), ("-", lambda: input_operator("-"), "#4ECDC4")], + # 第四行 + [("1", lambda: input_number(1), "#E8E8E8"), ("2", lambda: input_number(2), "#E8E8E8"), ("3", lambda: input_number(3), "#E8E8E8"), ("+", lambda: input_operator("+"), "#4ECDC4")], + # 第五行 + [("±", toggle_sign, "#D3D3D3"), ("0", lambda: input_number(0), "#E8E8E8"), (".", input_decimal, "#E8E8E8"), ("=", calculate, "#45B7D1")], + # 第六行 + [("√", calculate_sqrt, "#96CEB4"), ("%", calculate_percent, "#96CEB4"), ("^", lambda: input_operator("^"), "#4ECDC4"), ("", None, "")] + ] + + for row_idx, row in enumerate(buttons): + for col_idx, (text, command, color) in enumerate(row): + if text: # 跳过空按钮 + btn = tk.Button(button_frame, text=text, command=command, + bg=color, fg="black" if color != "#45B7D1" else "white", + activebackground=color, **button_config) + btn.grid(row=row_idx, column=col_idx, padx=2, pady=2, sticky="nsew") + + # 配置网格权重 + for i in range(4): + button_frame.grid_columnconfigure(i, weight=1) + for i in range(6): + button_frame.grid_rowconfigure(i, weight=1) + + # 键盘绑定 + def on_key_press(event): + key = event.char + if key.isdigit(): + input_number(int(key)) + elif key == ".": + input_decimal() + elif key in "+-*/": + op_map = {"+": "+", "-": "-", "*": "×", "/": "÷"} + input_operator(op_map[key]) + elif key in "\r\n=": # Enter键或等号 + calculate() + elif event.keysym == "BackSpace": + backspace() + elif event.keysym == "Escape": + clear_all() + + root.bind("", on_key_press) + root.focus_set() # 设置焦点以接收键盘事件 + + print("\n计算器应用已启动") + print("支持鼠标点击和键盘输入") + print("键盘快捷键: 数字键、运算符、Enter(=)、Backspace、Esc(清除)") + + # 启动事件循环 + root.mainloop() + +# 运行计算器应用 +calculator_app() +``` + +### 5.2 文本编辑器 + +```python +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, font +import os + +def text_editor_app(): + """简单文本编辑器应用""" + print("=== 文本编辑器应用演示 ===") + + # 创建主窗口 + root = tk.Tk() + root.title("Python文本编辑器") + root.geometry("800x600") + + # 应用状态 + current_file = None + is_modified = False + + def update_title(): + """更新窗口标题""" + title = "Python文本编辑器" + if current_file: + title += f" - {os.path.basename(current_file)}" + if is_modified: + title += " *" + root.title(title) + + def mark_modified(): + """标记文件已修改""" + global is_modified + if not is_modified: + is_modified = True + update_title() + + def mark_saved(): + """标记文件已保存""" + global is_modified + is_modified = False + update_title() + + # 创建菜单栏 + menubar = tk.Menu(root) + root.config(menu=menubar) + + # 文件菜单 + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="文件", menu=file_menu) + + def new_file(): + """新建文件""" + global current_file + if is_modified: + response = messagebox.askyesnocancel("保存", "文件已修改,是否保存?") + if response is True: + save_file() + elif response is None: + return + + text_area.delete(1.0, tk.END) + current_file = None + mark_saved() + + def open_file(): + """打开文件""" + global current_file + if is_modified: + response = messagebox.askyesnocancel("保存", "文件已修改,是否保存?") + if response is True: + save_file() + elif response is None: + return + + file_path = filedialog.askopenfilename( + title="打开文件", + filetypes=[("文本文件", "*.txt"), ("Python文件", "*.py"), ("所有文件", "*.*")] + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + text_area.delete(1.0, tk.END) + text_area.insert(1.0, content) + current_file = file_path + mark_saved() + except Exception as e: + messagebox.showerror("错误", f"无法打开文件: {str(e)}") + + def save_file(): + """保存文件""" + global current_file + if current_file: + try: + content = text_area.get(1.0, tk.END + "-1c") + with open(current_file, 'w', encoding='utf-8') as file: + file.write(content) + mark_saved() + messagebox.showinfo("保存", "文件保存成功") + except Exception as e: + messagebox.showerror("错误", f"无法保存文件: {str(e)}") + else: + save_as_file() + + def save_as_file(): + """另存为文件""" + global current_file + file_path = filedialog.asksaveasfilename( + title="另存为", + defaultextension=".txt", + filetypes=[("文本文件", "*.txt"), ("Python文件", "*.py"), ("所有文件", "*.*")] + ) + + if file_path: + try: + content = text_area.get(1.0, tk.END + "-1c") + with open(file_path, 'w', encoding='utf-8') as file: + file.write(content) + current_file = file_path + mark_saved() + messagebox.showinfo("保存", "文件保存成功") + except Exception as e: + messagebox.showerror("错误", f"无法保存文件: {str(e)}") + + def exit_app(): + """退出应用""" + if is_modified: + response = messagebox.askyesnocancel("保存", "文件已修改,是否保存?") + if response is True: + save_file() + elif response is None: + return + root.quit() + + file_menu.add_command(label="新建", command=new_file, accelerator="Ctrl+N") + file_menu.add_command(label="打开", command=open_file, accelerator="Ctrl+O") + file_menu.add_separator() + file_menu.add_command(label="保存", command=save_file, accelerator="Ctrl+S") + file_menu.add_command(label="另存为", command=save_as_file, accelerator="Ctrl+Shift+S") + file_menu.add_separator() + file_menu.add_command(label="退出", command=exit_app, accelerator="Ctrl+Q") + + # 编辑菜单 + edit_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="编辑", menu=edit_menu) + + def undo(): + try: + text_area.edit_undo() + except tk.TclError: + pass + + def redo(): + try: + text_area.edit_redo() + except tk.TclError: + pass + + def cut(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def copy(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def paste(): + try: + text_area.event_generate("<>") + except tk.TclError: + pass + + def select_all(): + text_area.tag_add(tk.SEL, "1.0", tk.END) + text_area.mark_set(tk.INSERT, "1.0") + text_area.see(tk.INSERT) + + def find_text(): + """查找文本""" + find_window = tk.Toplevel(root) + find_window.title("查找") + find_window.geometry("300x100") + find_window.resizable(False, False) + + tk.Label(find_window, text="查找:").pack(pady=5) + + search_var = tk.StringVar() + search_entry = tk.Entry(find_window, textvariable=search_var, width=30) + search_entry.pack(pady=5) + search_entry.focus() + + def do_find(): + search_text = search_var.get() + if search_text: + # 清除之前的高亮 + text_area.tag_remove("found", "1.0", tk.END) + + # 查找并高亮 + start = "1.0" + while True: + pos = text_area.search(search_text, start, tk.END) + if not pos: + break + end = f"{pos}+{len(search_text)}c" + text_area.tag_add("found", pos, end) + start = end + + # 设置高亮样式 + text_area.tag_config("found", background="yellow") + + # 跳转到第一个匹配项 + first_match = text_area.search(search_text, "1.0", tk.END) + if first_match: + text_area.see(first_match) + text_area.mark_set(tk.INSERT, first_match) + + tk.Button(find_window, text="查找", command=do_find).pack(pady=5) + + edit_menu.add_command(label="撤销", command=undo, accelerator="Ctrl+Z") + edit_menu.add_command(label="重做", command=redo, accelerator="Ctrl+Y") + edit_menu.add_separator() + edit_menu.add_command(label="剪切", command=cut, accelerator="Ctrl+X") + edit_menu.add_command(label="复制", command=copy, accelerator="Ctrl+C") + edit_menu.add_command(label="粘贴", command=paste, accelerator="Ctrl+V") + edit_menu.add_separator() + edit_menu.add_command(label="全选", command=select_all, accelerator="Ctrl+A") + edit_menu.add_command(label="查找", command=find_text, accelerator="Ctrl+F") + + # 格式菜单 + format_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="格式", menu=format_menu) + + def change_font(): + """更改字体""" + font_window = tk.Toplevel(root) + font_window.title("字体设置") + font_window.geometry("400x300") + + current_font = font.Font(font=text_area['font']) + + # 字体族 + tk.Label(font_window, text="字体:").grid(row=0, column=0, sticky="w", padx=5, pady=5) + font_family = tk.StringVar(value=current_font.actual()['family']) + font_listbox = tk.Listbox(font_window, height=6) + font_listbox.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + families = sorted(font.families()) + for family in families: + font_listbox.insert(tk.END, family) + + # 字体大小 + tk.Label(font_window, text="大小:").grid(row=2, column=0, sticky="w", padx=5, pady=5) + size_var = tk.StringVar(value=str(current_font.actual()['size'])) + size_entry = tk.Entry(font_window, textvariable=size_var, width=10) + size_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w") + + # 字体样式 + bold_var = tk.BooleanVar(value=current_font.actual()['weight'] == 'bold') + italic_var = tk.BooleanVar(value=current_font.actual()['slant'] == 'italic') + + tk.Checkbutton(font_window, text="粗体", variable=bold_var).grid(row=3, column=0, sticky="w", padx=5, pady=5) + tk.Checkbutton(font_window, text="斜体", variable=italic_var).grid(row=3, column=1, sticky="w", padx=5, pady=5) + + def apply_font(): + try: + selection = font_listbox.curselection() + if selection: + family = font_listbox.get(selection[0]) + else: + family = current_font.actual()['family'] + + size = int(size_var.get()) + weight = 'bold' if bold_var.get() else 'normal' + slant = 'italic' if italic_var.get() else 'roman' + + new_font = font.Font(family=family, size=size, weight=weight, slant=slant) + text_area.config(font=new_font) + font_window.destroy() + except ValueError: + messagebox.showerror("错误", "请输入有效的字体大小") + + tk.Button(font_window, text="应用", command=apply_font).grid(row=4, column=0, columnspan=2, pady=10) + + format_menu.add_command(label="字体", command=change_font) + + # 帮助菜单 + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="帮助", menu=help_menu) + + def show_about(): + messagebox.showinfo("关于", "Python文本编辑器\n\n一个简单的文本编辑器示例\n使用Tkinter构建") + + help_menu.add_command(label="关于", command=show_about) + + # 创建工具栏 + toolbar = tk.Frame(root, relief=tk.RAISED, bd=1) + toolbar.pack(side=tk.TOP, fill=tk.X) + + # 工具栏按钮 + tk.Button(toolbar, text="新建", command=new_file, width=6).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="打开", command=open_file, width=6).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="保存", command=save_file, width=6).pack(side=tk.LEFT, padx=2, pady=2) + + ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5) + + tk.Button(toolbar, text="剪切", command=cut, width=6).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="复制", command=copy, width=6).pack(side=tk.LEFT, padx=2, pady=2) + tk.Button(toolbar, text="粘贴", command=paste, width=6).pack(side=tk.LEFT, padx=2, pady=2) + + # 创建文本区域 + text_frame = tk.Frame(root) + text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 文本控件 + text_area = tk.Text(text_frame, wrap=tk.WORD, undo=True, font=("Consolas", 12)) + + # 滚动条 + v_scrollbar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_area.yview) + h_scrollbar = tk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_area.xview) + + text_area.config(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) + + # 布局 + text_area.grid(row=0, column=0, sticky="nsew") + v_scrollbar.grid(row=0, column=1, sticky="ns") + h_scrollbar.grid(row=1, column=0, sticky="ew") + + text_frame.grid_rowconfigure(0, weight=1) + text_frame.grid_columnconfigure(0, weight=1) + + # 状态栏 + status_bar = tk.Frame(root, relief=tk.SUNKEN, bd=1) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + + # 状态标签 + line_col_label = tk.Label(status_bar, text="行: 1, 列: 1") + line_col_label.pack(side=tk.RIGHT, padx=5) + + char_count_label = tk.Label(status_bar, text="字符数: 0") + char_count_label.pack(side=tk.RIGHT, padx=5) + + def update_status(event=None): + """更新状态栏""" + # 获取光标位置 + cursor_pos = text_area.index(tk.INSERT) + line, col = cursor_pos.split('.') + line_col_label.config(text=f"行: {line}, 列: {int(col)+1}") + + # 获取字符数 + content = text_area.get(1.0, tk.END + "-1c") + char_count_label.config(text=f"字符数: {len(content)}") + + # 绑定事件 + text_area.bind("", update_status) + text_area.bind("", update_status) + text_area.bind("", lambda e: mark_modified()) + + # 键盘快捷键 + root.bind("", lambda e: new_file()) + root.bind("", lambda e: open_file()) + root.bind("", lambda e: save_file()) + root.bind("", lambda e: save_as_file()) + root.bind("", lambda e: exit_app()) + root.bind("", lambda e: undo()) + root.bind("", lambda e: redo()) + root.bind("", lambda e: select_all()) + root.bind("", lambda e: find_text()) + + # 窗口关闭事件 + root.protocol("WM_DELETE_WINDOW", exit_app) + + # 初始化状态 + update_status() + update_title() + + print("\n文本编辑器应用已启动") + print("支持基本的文本编辑功能") + print("包括文件操作、编辑操作、格式设置等") + + # 启动事件循环 + root.mainloop() + +# 运行文本编辑器应用 +text_editor_app() +``` + +## 6. GUI编程最佳实践 + +### 6.1 设计原则 + +```python +# GUI设计最佳实践示例 + +def gui_best_practices(): + """GUI编程最佳实践演示""" + print("=== GUI编程最佳实践 ===") + + # 1. 用户体验原则 + print("\n1. 用户体验原则:") + print(" - 界面简洁直观") + print(" - 操作流程清晰") + print(" - 提供及时反馈") + print(" - 支持键盘快捷键") + print(" - 错误处理友好") + + # 2. 布局设计 + print("\n2. 布局设计:") + print(" - 使用合适的布局管理器") + print(" - 保持界面元素对齐") + print(" - 合理使用空白空间") + print(" - 响应式设计") + + # 3. 代码组织 + print("\n3. 代码组织:") + print(" - 分离界面和业务逻辑") + print(" - 使用类封装GUI组件") + print(" - 模块化设计") + print(" - 异常处理") + + # 4. 性能优化 + print("\n4. 性能优化:") + print(" - 避免阻塞UI线程") + print(" - 使用虚拟化处理大量数据") + print(" - 延迟加载") + print(" - 内存管理") + + # 5. 可访问性 + print("\n5. 可访问性:") + print(" - 支持键盘导航") + print(" - 提供工具提示") + print(" - 合适的颜色对比度") + print(" - 字体大小可调") + +# 运行最佳实践演示 +gui_best_practices() +``` + +### 6.2 常见问题和解决方案 + +```python +def common_issues_solutions(): + """常见问题和解决方案""" + print("=== 常见问题和解决方案 ===") + + # 1. 界面冻结问题 + print("\n1. 界面冻结问题:") + print(" 问题: 长时间运行的任务导致界面无响应") + print(" 解决: 使用线程或after()方法") + + # 示例:使用after()方法避免界面冻结 + def long_task_example(): + root = tk.Tk() + root.title("避免界面冻结示例") + + progress = ttk.Progressbar(root, length=300, mode='determinate') + progress.pack(pady=20) + + status_label = tk.Label(root, text="准备开始...") + status_label.pack(pady=10) + + def simulate_work(step=0): + if step < 100: + progress['value'] = step + status_label.config(text=f"处理中... {step}%") + # 使用after()方法避免阻塞UI + root.after(50, lambda: simulate_work(step + 1)) + else: + status_label.config(text="完成!") + + tk.Button(root, text="开始任务", command=simulate_work).pack(pady=10) + + return root + + # 2. 内存泄漏问题 + print("\n2. 内存泄漏问题:") + print(" 问题: 未正确销毁窗口和绑定") + print(" 解决: 正确使用destroy()和解绑事件") + + # 3. 跨平台兼容性 + print("\n3. 跨平台兼容性:") + print(" 问题: 不同操作系统显示效果不一致") + print(" 解决: 使用ttk主题控件,测试多平台") + + # 4. 高DPI显示问题 + print("\n4. 高DPI显示问题:") + print(" 问题: 在高DPI屏幕上显示模糊") + print(" 解决: 设置DPI感知") + + # 示例:DPI感知设置 + def set_dpi_awareness(): + try: + from ctypes import windll + windll.shcore.SetProcessDpiAwareness(1) + except: + pass # 非Windows系统或不支持 + + # 5. 数据绑定问题 + print("\n5. 数据绑定问题:") + print(" 问题: 界面数据与模型数据不同步") + print(" 解决: 使用观察者模式或数据绑定框架") + +# 运行常见问题演示 +common_issues_solutions() +``` + +## 7. 学习建议和总结 + +### 7.1 学习路径 + +```python +def learning_path(): + """GUI编程学习路径""" + print("=== GUI编程学习路径 ===") + + learning_steps = [ + { + "阶段": "基础入门", + "内容": [ + "理解GUI编程概念", + "掌握基本控件使用", + "学习布局管理", + "练习事件处理" + ], + "项目": "简单的表单应用" + }, + { + "阶段": "进阶应用", + "内容": [ + "高级控件使用", + "菜单和工具栏", + "对话框设计", + "数据展示控件" + ], + "项目": "数据管理应用" + }, + { + "阶段": "高级特性", + "内容": [ + "自定义控件", + "主题和样式", + "多线程GUI", + "插件架构" + ], + "项目": "完整的桌面应用" + }, + { + "阶段": "专业开发", + "内容": [ + "性能优化", + "跨平台部署", + "用户体验设计", + "测试和调试" + ], + "项目": "商业级应用" + } + ] + + for i, step in enumerate(learning_steps, 1): + print(f"\n{i}. {step['阶段']}:") + for content in step['内容']: + print(f" - {content}") + print(f" 推荐项目: {step['项目']}") + +# 运行学习路径演示 +learning_path() +``` + +### 7.2 实践建议 + +```python +def practice_suggestions(): + """实践建议""" + print("=== 实践建议 ===") + + suggestions = { + "项目练习": [ + "从简单项目开始,逐步增加复杂度", + "模仿现有应用的界面设计", + "关注用户体验和界面美观", + "完成完整的项目周期" + ], + "代码质量": [ + "遵循编码规范和最佳实践", + "编写清晰的注释和文档", + "进行代码重构和优化", + "使用版本控制管理代码" + ], + "学习资源": [ + "官方文档和教程", + "开源项目源码学习", + "技术博客和视频教程", + "参与社区讨论和交流" + ], + "技能拓展": [ + "学习其他GUI框架(PyQt, wxPython)", + "了解Web前端技术", + "掌握设计工具和原型制作", + "学习移动应用开发" + ] + } + + for category, items in suggestions.items(): + print(f"\n{category}:") + for item in items: + print(f" • {item}") + +# 运行实践建议演示 +practice_suggestions() +``` + +### 7.3 总结 + +```python +def chapter_summary(): + """本章总结""" + print("=== 第17天学习总结 ===") + + summary_points = { + "核心概念": [ + "GUI编程基础和事件驱动模型", + "Tkinter框架的基本使用", + "控件、布局和事件处理", + "用户界面设计原则" + ], + "重要技能": [ + "基本控件的使用和配置", + "布局管理器的选择和应用", + "事件处理和用户交互", + "菜单、对话框等高级组件" + ], + "实际应用": [ + "计算器应用开发", + "文本编辑器实现", + "数据展示和可视化", + "完整桌面应用构建" + ], + "最佳实践": [ + "代码组织和模块化设计", + "用户体验优化", + "性能和兼容性考虑", + "错误处理和调试技巧" + ] + } + + for category, points in summary_points.items(): + print(f"\n{category}:") + for point in points: + print(f" ✓ {point}") + + print("\n下一步学习方向:") + print(" • 深入学习高级GUI框架(PyQt/PySide)") + print(" • 探索Web应用开发(Flask/Django)") + print(" • 学习移动应用开发") + print(" • 掌握数据可视化技术") + + print("\n恭喜完成第17天的学习!") + print("你已经掌握了Python GUI编程的基础知识和实践技能。") + print("继续练习和探索,构建更复杂和实用的桌面应用程序!") + +# 运行总结 +chapter_summary() +``` + +通过第17天的学习,你已经全面掌握了Python图形界面编程的核心概念和实践技能。从基础的控件使用到复杂的应用开发,从事件处理到用户体验设计,这些知识将为你开发桌面应用程序奠定坚实的基础。 + +记住,GUI编程不仅仅是技术实现,更重要的是理解用户需求,设计直观易用的界面。继续练习和探索,你将能够创建出功能强大、界面美观的桌面应用程序! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/18.md b/docs/Python/18.md new file mode 100644 index 000000000..ce880a093 --- /dev/null +++ b/docs/Python/18.md @@ -0,0 +1,2194 @@ +--- +title: 第18天-网络编程 +author: 哪吒 +date: '2023-06-15' +--- + +# 第18天-网络编程 + +## 1. 网络编程基础 + +### 1.1 网络编程概述 + +```python +# 网络编程基础概念演示 + +def network_programming_basics(): + """网络编程基础概念""" + print("=== 网络编程基础概念 ===") + + # 1. 网络编程定义 + print("\n1. 网络编程定义:") + print(" - 通过网络协议实现不同计算机间的通信") + print(" - 包括客户端和服务器端编程") + print(" - 支持数据传输、远程调用、分布式计算等") + + # 2. 网络协议层次 + print("\n2. 网络协议层次:") + protocols = { + "应用层": ["HTTP", "HTTPS", "FTP", "SMTP", "POP3", "IMAP"], + "传输层": ["TCP", "UDP"], + "网络层": ["IP", "ICMP", "ARP"], + "数据链路层": ["Ethernet", "WiFi"], + "物理层": ["光纤", "双绞线", "无线"] + } + + for layer, items in protocols.items(): + print(f" {layer}: {', '.join(items)}") + + # 3. TCP vs UDP + print("\n3. TCP vs UDP:") + comparison = { + "特性": ["连接性", "可靠性", "速度", "开销", "应用场景"], + "TCP": ["面向连接", "可靠传输", "较慢", "较高", "文件传输、网页浏览"], + "UDP": ["无连接", "不可靠", "较快", "较低", "视频直播、游戏"] + } + + for i, feature in enumerate(comparison["特性"]): + print(f" {feature}: TCP({comparison['TCP'][i]}) vs UDP({comparison['UDP'][i]})") + + # 4. 常用端口 + print("\n4. 常用端口:") + common_ports = { + "HTTP": 80, + "HTTPS": 443, + "FTP": 21, + "SSH": 22, + "Telnet": 23, + "SMTP": 25, + "DNS": 53, + "POP3": 110, + "IMAP": 143 + } + + for service, port in common_ports.items(): + print(f" {service}: {port}") + +# 运行网络编程基础演示 +network_programming_basics() +``` + +### 1.2 Python网络编程模块 + +```python +import socket +import urllib.request +import urllib.parse +import http.client +import json +from datetime import datetime + +def python_network_modules(): + """Python网络编程模块介绍""" + print("=== Python网络编程模块 ===") + + # 1. socket模块 + print("\n1. socket模块:") + print(" - 底层网络编程接口") + print(" - 支持TCP和UDP协议") + print(" - 提供客户端和服务器端功能") + print(" - 跨平台支持") + + # 2. urllib模块 + print("\n2. urllib模块:") + print(" - 高级URL处理库") + print(" - 支持HTTP/HTTPS请求") + print(" - 包含request、parse、error等子模块") + print(" - 适合简单的网络请求") + + # 3. http.client模块 + print("\n3. http.client模块:") + print(" - 低级HTTP客户端接口") + print(" - 更精细的HTTP控制") + print(" - 支持HTTP/1.1协议") + print(" - 适合复杂的HTTP操作") + + # 4. 第三方库 + print("\n4. 常用第三方库:") + third_party = { + "requests": "简洁易用的HTTP库", + "aiohttp": "异步HTTP客户端/服务器", + "tornado": "异步网络库和Web框架", + "twisted": "事件驱动网络引擎", + "paramiko": "SSH客户端库", + "ftplib": "FTP客户端库" + } + + for lib, desc in third_party.items(): + print(f" {lib}: {desc}") + +# 运行模块介绍 +python_network_modules() +``` + +## 2. Socket编程 + +### 2.1 TCP Socket编程 + +```python +import socket +import threading +import time + +def tcp_server_demo(): + """TCP服务器演示""" + print("=== TCP服务器演示 ===") + + def handle_client(client_socket, client_address): + """处理客户端连接""" + print(f"客户端 {client_address} 已连接") + + try: + while True: + # 接收数据 + data = client_socket.recv(1024) + if not data: + break + + message = data.decode('utf-8') + print(f"收到来自 {client_address} 的消息: {message}") + + # 处理特殊命令 + if message.lower() == 'quit': + break + elif message.lower() == 'time': + response = f"服务器时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + elif message.lower().startswith('echo '): + response = f"回声: {message[5:]}" + else: + response = f"服务器收到: {message}" + + # 发送响应 + client_socket.send(response.encode('utf-8')) + + except Exception as e: + print(f"处理客户端 {client_address} 时出错: {e}") + finally: + client_socket.close() + print(f"客户端 {client_address} 已断开连接") + + # 创建服务器socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # 设置socket选项,允许地址重用 + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # 绑定地址和端口 + host = 'localhost' + port = 12345 + server_socket.bind((host, port)) + + # 开始监听 + server_socket.listen(5) + print(f"TCP服务器启动,监听 {host}:{port}") + print("等待客户端连接...") + + try: + while True: + # 接受客户端连接 + client_socket, client_address = server_socket.accept() + + # 为每个客户端创建新线程 + client_thread = threading.Thread( + target=handle_client, + args=(client_socket, client_address) + ) + client_thread.daemon = True + client_thread.start() + + except KeyboardInterrupt: + print("\n服务器正在关闭...") + finally: + server_socket.close() + print("服务器已关闭") + +def tcp_client_demo(): + """TCP客户端演示""" + print("=== TCP客户端演示 ===") + + # 创建客户端socket + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + try: + # 连接服务器 + host = 'localhost' + port = 12345 + client_socket.connect((host, port)) + print(f"已连接到服务器 {host}:{port}") + + # 发送和接收数据 + messages = [ + "Hello, Server!", + "time", + "echo Python网络编程", + "这是一条测试消息", + "quit" + ] + + for message in messages: + print(f"发送: {message}") + client_socket.send(message.encode('utf-8')) + + if message.lower() == 'quit': + break + + # 接收响应 + response = client_socket.recv(1024) + print(f"收到: {response.decode('utf-8')}") + print("-" * 40) + + time.sleep(1) # 延迟1秒 + + except Exception as e: + print(f"客户端错误: {e}") + finally: + client_socket.close() + print("客户端已断开连接") + +# 注意:实际运行时需要先启动服务器,再启动客户端 +print("TCP Socket编程示例") +print("请先运行 tcp_server_demo() 启动服务器") +print("然后运行 tcp_client_demo() 启动客户端") +``` + +### 2.2 UDP Socket编程 + +```python +def udp_server_demo(): + """UDP服务器演示""" + print("=== UDP服务器演示 ===") + + # 创建UDP socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + # 绑定地址和端口 + host = 'localhost' + port = 12346 + server_socket.bind((host, port)) + + print(f"UDP服务器启动,监听 {host}:{port}") + print("等待数据包...") + + try: + while True: + # 接收数据 + data, client_address = server_socket.recvfrom(1024) + message = data.decode('utf-8') + + print(f"收到来自 {client_address} 的数据: {message}") + + # 处理数据并发送响应 + if message.lower() == 'quit': + response = "服务器正在关闭" + server_socket.sendto(response.encode('utf-8'), client_address) + break + elif message.lower() == 'time': + response = f"服务器时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + else: + response = f"UDP服务器收到: {message}" + + # 发送响应 + server_socket.sendto(response.encode('utf-8'), client_address) + + except KeyboardInterrupt: + print("\n服务器正在关闭...") + finally: + server_socket.close() + print("UDP服务器已关闭") + +def udp_client_demo(): + """UDP客户端演示""" + print("=== UDP客户端演示 ===") + + # 创建UDP socket + client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + try: + # 服务器地址 + server_address = ('localhost', 12346) + + # 发送数据 + messages = [ + "Hello, UDP Server!", + "time", + "这是UDP消息", + "quit" + ] + + for message in messages: + print(f"发送: {message}") + client_socket.sendto(message.encode('utf-8'), server_address) + + # 接收响应 + response, server_addr = client_socket.recvfrom(1024) + print(f"收到: {response.decode('utf-8')}") + print("-" * 40) + + if message.lower() == 'quit': + break + + time.sleep(1) + + except Exception as e: + print(f"UDP客户端错误: {e}") + finally: + client_socket.close() + print("UDP客户端已关闭") + +# UDP编程示例 +print("\nUDP Socket编程示例") +print("请先运行 udp_server_demo() 启动服务器") +print("然后运行 udp_client_demo() 启动客户端") +``` + +### 2.3 Socket编程进阶 + +```python +import select +import errno + +def advanced_socket_demo(): + """高级Socket编程演示""" + print("=== 高级Socket编程 ===") + + # 1. 非阻塞Socket + print("\n1. 非阻塞Socket:") + + def non_blocking_server(): + """非阻塞服务器示例""" + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('localhost', 12347)) + server_socket.listen(5) + + # 设置为非阻塞模式 + server_socket.setblocking(False) + + print("非阻塞服务器启动") + + sockets_list = [server_socket] + clients = {} + + try: + while True: + # 使用select监控socket + ready_to_read, _, exception_sockets = select.select( + sockets_list, [], sockets_list, 1 + ) + + for notified_socket in ready_to_read: + if notified_socket == server_socket: + # 新的客户端连接 + try: + client_socket, client_address = server_socket.accept() + client_socket.setblocking(False) + sockets_list.append(client_socket) + clients[client_socket] = client_address + print(f"新客户端连接: {client_address}") + except: + pass + else: + # 现有客户端发送数据 + try: + data = notified_socket.recv(1024) + if data: + message = data.decode('utf-8') + client_addr = clients[notified_socket] + print(f"收到来自 {client_addr} 的消息: {message}") + + # 回声响应 + response = f"回声: {message}" + notified_socket.send(response.encode('utf-8')) + else: + # 客户端断开连接 + client_addr = clients[notified_socket] + print(f"客户端 {client_addr} 断开连接") + sockets_list.remove(notified_socket) + del clients[notified_socket] + notified_socket.close() + except: + # 连接错误 + if notified_socket in clients: + client_addr = clients[notified_socket] + print(f"客户端 {client_addr} 连接错误") + sockets_list.remove(notified_socket) + del clients[notified_socket] + notified_socket.close() + + # 处理异常socket + for notified_socket in exception_sockets: + if notified_socket in clients: + client_addr = clients[notified_socket] + print(f"客户端 {client_addr} 异常") + sockets_list.remove(notified_socket) + del clients[notified_socket] + notified_socket.close() + + except KeyboardInterrupt: + print("\n服务器关闭") + finally: + server_socket.close() + + # 2. Socket选项设置 + print("\n2. Socket选项设置:") + + def socket_options_demo(): + """Socket选项演示""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # 常用选项 + options = { + "SO_REUSEADDR": "允许地址重用", + "SO_KEEPALIVE": "启用保活机制", + "SO_RCVBUF": "设置接收缓冲区大小", + "SO_SNDBUF": "设置发送缓冲区大小", + "TCP_NODELAY": "禁用Nagle算法" + } + + for option, desc in options.items(): + print(f" {option}: {desc}") + + # 设置选项示例 + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + # 获取选项值 + reuse_addr = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) + print(f" SO_REUSEADDR 当前值: {reuse_addr}") + + sock.close() + + # 3. 超时设置 + print("\n3. 超时设置:") + + def timeout_demo(): + """超时设置演示""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # 设置连接超时 + sock.settimeout(5.0) # 5秒超时 + + try: + # 尝试连接到不存在的服务器 + sock.connect(('192.168.1.999', 80)) + except socket.timeout: + print(" 连接超时") + except Exception as e: + print(f" 连接错误: {e}") + finally: + sock.close() + + socket_options_demo() + timeout_demo() + +# 运行高级Socket演示 +advanced_socket_demo() +``` + +## 3. HTTP编程 + +### 3.1 urllib模块 + +```python +import urllib.request +import urllib.parse +import urllib.error +import json + +def urllib_demo(): + """urllib模块演示""" + print("=== urllib模块演示 ===") + + # 1. 基本GET请求 + print("\n1. 基本GET请求:") + + def simple_get_request(): + """简单GET请求""" + try: + # 发送GET请求 + url = "https://httpbin.org/get" + response = urllib.request.urlopen(url) + + # 获取响应信息 + print(f" 状态码: {response.getcode()}") + print(f" 响应头: {dict(response.headers)}") + + # 读取响应内容 + content = response.read().decode('utf-8') + data = json.loads(content) + print(f" 响应数据: {data['url']}") + + except urllib.error.URLError as e: + print(f" 请求错误: {e}") + + # 2. 带参数的GET请求 + print("\n2. 带参数的GET请求:") + + def get_with_params(): + """带参数的GET请求""" + try: + # 构建URL参数 + base_url = "https://httpbin.org/get" + params = { + 'name': 'Python', + 'version': '3.9', + 'type': 'programming' + } + + # 编码参数 + query_string = urllib.parse.urlencode(params) + url = f"{base_url}?{query_string}" + + print(f" 请求URL: {url}") + + response = urllib.request.urlopen(url) + content = response.read().decode('utf-8') + data = json.loads(content) + + print(f" 服务器收到的参数: {data['args']}") + + except Exception as e: + print(f" 请求错误: {e}") + + # 3. POST请求 + print("\n3. POST请求:") + + def post_request(): + """POST请求""" + try: + url = "https://httpbin.org/post" + + # 准备POST数据 + post_data = { + 'username': 'python_user', + 'password': 'secret123', + 'email': 'user@example.com' + } + + # 编码POST数据 + data = urllib.parse.urlencode(post_data).encode('utf-8') + + # 创建请求对象 + request = urllib.request.Request(url, data=data) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + + # 发送请求 + response = urllib.request.urlopen(request) + content = response.read().decode('utf-8') + result = json.loads(content) + + print(f" POST数据: {result['form']}") + print(f" 请求头: {result['headers']}") + + except Exception as e: + print(f" POST请求错误: {e}") + + # 4. JSON数据请求 + print("\n4. JSON数据请求:") + + def json_request(): + """发送JSON数据""" + try: + url = "https://httpbin.org/post" + + # 准备JSON数据 + json_data = { + 'name': 'Python网络编程', + 'author': '哪吒', + 'topics': ['socket', 'http', 'urllib'], + 'difficulty': 'intermediate' + } + + # 转换为JSON字符串并编码 + data = json.dumps(json_data).encode('utf-8') + + # 创建请求 + request = urllib.request.Request(url, data=data) + request.add_header('Content-Type', 'application/json') + + # 发送请求 + response = urllib.request.urlopen(request) + content = response.read().decode('utf-8') + result = json.loads(content) + + print(f" 发送的JSON: {result['json']}") + + except Exception as e: + print(f" JSON请求错误: {e}") + + # 5. 自定义请求头 + print("\n5. 自定义请求头:") + + def custom_headers(): + """自定义请求头""" + try: + url = "https://httpbin.org/headers" + + # 创建请求对象 + request = urllib.request.Request(url) + + # 添加自定义头 + request.add_header('User-Agent', 'Python-urllib/3.9') + request.add_header('Accept', 'application/json') + request.add_header('X-Custom-Header', 'Python网络编程示例') + + response = urllib.request.urlopen(request) + content = response.read().decode('utf-8') + result = json.loads(content) + + print(f" 请求头: {result['headers']}") + + except Exception as e: + print(f" 自定义头请求错误: {e}") + + # 运行所有示例 + simple_get_request() + get_with_params() + post_request() + json_request() + custom_headers() + +# 运行urllib演示 +urllib_demo() +``` + +### 3.2 http.client模块 + +```python +import http.client +import json + +def http_client_demo(): + """http.client模块演示""" + print("=== http.client模块演示 ===") + + # 1. 基本HTTP连接 + print("\n1. 基本HTTP连接:") + + def basic_http_connection(): + """基本HTTP连接""" + try: + # 创建HTTP连接 + conn = http.client.HTTPSConnection("httpbin.org") + + # 发送GET请求 + conn.request("GET", "/get") + + # 获取响应 + response = conn.getresponse() + + print(f" 状态码: {response.status}") + print(f" 状态信息: {response.reason}") + print(f" HTTP版本: {response.version}") + + # 读取响应数据 + data = response.read().decode('utf-8') + result = json.loads(data) + print(f" 响应URL: {result['url']}") + + conn.close() + + except Exception as e: + print(f" HTTP连接错误: {e}") + + # 2. 带参数的请求 + print("\n2. 带参数的请求:") + + def http_with_params(): + """带参数的HTTP请求""" + try: + conn = http.client.HTTPSConnection("httpbin.org") + + # 构建查询参数 + params = urllib.parse.urlencode({ + 'param1': 'value1', + 'param2': 'value2', + 'message': 'Hello from http.client' + }) + + # 发送请求 + conn.request("GET", f"/get?{params}") + response = conn.getresponse() + + data = response.read().decode('utf-8') + result = json.loads(data) + + print(f" 查询参数: {result['args']}") + + conn.close() + + except Exception as e: + print(f" 参数请求错误: {e}") + + # 3. POST请求 + print("\n3. POST请求:") + + def http_post_request(): + """HTTP POST请求""" + try: + conn = http.client.HTTPSConnection("httpbin.org") + + # 准备POST数据 + post_data = json.dumps({ + 'title': 'Python网络编程', + 'content': '使用http.client发送POST请求', + 'tags': ['python', 'network', 'http'] + }) + + # 设置请求头 + headers = { + 'Content-Type': 'application/json', + 'Content-Length': str(len(post_data)), + 'User-Agent': 'Python-http.client' + } + + # 发送POST请求 + conn.request("POST", "/post", post_data, headers) + response = conn.getresponse() + + print(f" 状态码: {response.status}") + + data = response.read().decode('utf-8') + result = json.loads(data) + + print(f" 发送的数据: {result['json']}") + print(f" 请求头: {result['headers']}") + + conn.close() + + except Exception as e: + print(f" POST请求错误: {e}") + + # 4. 响应头处理 + print("\n4. 响应头处理:") + + def response_headers(): + """处理响应头""" + try: + conn = http.client.HTTPSConnection("httpbin.org") + conn.request("GET", "/response-headers?Content-Type=application/json&Server=Python-Demo") + + response = conn.getresponse() + + # 获取所有响应头 + print(" 响应头:") + for header, value in response.getheaders(): + print(f" {header}: {value}") + + # 获取特定响应头 + content_type = response.getheader('Content-Type') + print(f" Content-Type: {content_type}") + + conn.close() + + except Exception as e: + print(f" 响应头处理错误: {e}") + + # 运行所有示例 + basic_http_connection() + http_with_params() + http_post_request() + response_headers() + +# 运行http.client演示 +http_client_demo() +``` + +### 3.3 Web爬虫基础 + +```python +import urllib.request +import urllib.parse +import re +import time +from urllib.robotparser import RobotFileParser + +def web_scraping_demo(): + """Web爬虫基础演示""" + print("=== Web爬虫基础演示 ===") + + # 1. 基本网页抓取 + print("\n1. 基本网页抓取:") + + def fetch_webpage(): + """抓取网页内容""" + try: + # 设置User-Agent避免被拒绝 + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + url = "https://httpbin.org/html" + request = urllib.request.Request(url, headers=headers) + response = urllib.request.urlopen(request) + + # 获取网页内容 + html_content = response.read().decode('utf-8') + + # 简单的HTML解析 + title_match = re.search(r'(.*?)', html_content, re.IGNORECASE) + if title_match: + title = title_match.group(1) + print(f" 网页标题: {title}") + + # 提取链接 + links = re.findall(r']+href=["\']([^"\'>]+)["\'][^>]*>(.*?)', html_content, re.IGNORECASE) + print(f" 找到 {len(links)} 个链接") + for href, text in links[:3]: # 只显示前3个 + print(f" {text.strip()}: {href}") + + except Exception as e: + print(f" 网页抓取错误: {e}") + + # 2. 处理表单和Cookie + print("\n2. 处理表单和Cookie:") + + def handle_forms_cookies(): + """处理表单和Cookie""" + try: + # 创建Cookie处理器 + import http.cookiejar + + cookie_jar = http.cookiejar.CookieJar() + cookie_processor = urllib.request.HTTPCookieProcessor(cookie_jar) + opener = urllib.request.build_opener(cookie_processor) + + # 设置为全局opener + urllib.request.install_opener(opener) + + # 访问设置Cookie的页面 + response = opener.open("https://httpbin.org/cookies/set/session_id/abc123") + + print(f" 设置Cookie后的状态码: {response.getcode()}") + + # 查看Cookie + for cookie in cookie_jar: + print(f" Cookie: {cookie.name}={cookie.value}") + + # 访问需要Cookie的页面 + response = opener.open("https://httpbin.org/cookies") + content = response.read().decode('utf-8') + result = json.loads(content) + + print(f" 服务器收到的Cookie: {result['cookies']}") + + except Exception as e: + print(f" Cookie处理错误: {e}") + + # 3. 处理重定向 + print("\n3. 处理重定向:") + + def handle_redirects(): + """处理HTTP重定向""" + try: + # 自动处理重定向(默认行为) + url = "https://httpbin.org/redirect/3" # 重定向3次 + response = urllib.request.urlopen(url) + + print(f" 最终URL: {response.url}") + print(f" 状态码: {response.getcode()}") + + # 禁用自动重定向 + class NoRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + opener = urllib.request.build_opener(NoRedirectHandler) + + try: + response = opener.open("https://httpbin.org/redirect/1") + except urllib.error.HTTPError as e: + print(f" 重定向状态码: {e.code}") + print(f" 重定向位置: {e.headers.get('Location')}") + + except Exception as e: + print(f" 重定向处理错误: {e}") + + # 4. 爬虫礼仪 + print("\n4. 爬虫礼仪:") + + def crawler_etiquette(): + """爬虫礼仪演示""" + print(" 爬虫最佳实践:") + print(" - 遵守robots.txt") + print(" - 设置合理的请求间隔") + print(" - 使用适当的User-Agent") + print(" - 避免过度请求") + print(" - 尊重网站的服务条款") + + # robots.txt检查示例 + def check_robots_txt(url): + """检查robots.txt""" + try: + rp = RobotFileParser() + rp.set_url(f"{url}/robots.txt") + rp.read() + + # 检查是否允许抓取 + user_agent = "*" + test_url = f"{url}/get" + + if rp.can_fetch(user_agent, test_url): + print(f" 允许抓取: {test_url}") + else: + print(f" 禁止抓取: {test_url}") + + except Exception as e: + print(f" robots.txt检查错误: {e}") + + check_robots_txt("https://httpbin.org") + + # 请求间隔示例 + print(" 实施请求间隔...") + urls = [ + "https://httpbin.org/delay/1", + "https://httpbin.org/delay/1", + "https://httpbin.org/delay/1" + ] + + for i, url in enumerate(urls, 1): + try: + start_time = time.time() + response = urllib.request.urlopen(url) + end_time = time.time() + + print(f" 请求 {i}: 状态码 {response.getcode()}, 耗时 {end_time - start_time:.2f}秒") + + # 请求间隔 + if i < len(urls): + time.sleep(1) # 1秒间隔 + + except Exception as e: + print(f" 请求 {i} 错误: {e}") + + # 运行所有示例 + fetch_webpage() + handle_forms_cookies() + handle_redirects() + crawler_etiquette() + +# 运行Web爬虫演示 +web_scraping_demo() +``` + +## 4. 异步网络编程 + +### 4.1 asyncio基础 + +```python +import asyncio +import aiohttp +import time + +def asyncio_networking_demo(): + """异步网络编程演示""" + print("=== 异步网络编程演示 ===") + + # 1. 异步HTTP客户端 + print("\n1. 异步HTTP客户端:") + + async def fetch_url(session, url): + """异步获取URL内容""" + try: + async with session.get(url) as response: + content = await response.text() + return { + 'url': url, + 'status': response.status, + 'length': len(content) + } + except Exception as e: + return { + 'url': url, + 'error': str(e) + } + + async def fetch_multiple_urls(): + """并发获取多个URL""" + urls = [ + 'https://httpbin.org/delay/1', + 'https://httpbin.org/delay/2', + 'https://httpbin.org/delay/1', + 'https://httpbin.org/get', + 'https://httpbin.org/json' + ] + + start_time = time.time() + + async with aiohttp.ClientSession() as session: + # 并发执行所有请求 + tasks = [fetch_url(session, url) for url in urls] + results = await asyncio.gather(*tasks) + + end_time = time.time() + + print(f" 并发请求完成,总耗时: {end_time - start_time:.2f}秒") + + for result in results: + if 'error' in result: + print(f" {result['url']}: 错误 - {result['error']}") + else: + print(f" {result['url']}: 状态码 {result['status']}, 长度 {result['length']}") + + # 2. 异步TCP服务器 + print("\n2. 异步TCP服务器:") + + async def handle_client_async(reader, writer): + """异步处理客户端连接""" + client_address = writer.get_extra_info('peername') + print(f" 客户端 {client_address} 已连接") + + try: + while True: + # 异步读取数据 + data = await reader.read(1024) + if not data: + break + + message = data.decode('utf-8').strip() + print(f" 收到来自 {client_address} 的消息: {message}") + + if message.lower() == 'quit': + break + elif message.lower() == 'time': + response = f"服务器时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + else: + response = f"异步服务器收到: {message}\n" + + # 异步发送响应 + writer.write(response.encode('utf-8')) + await writer.drain() + + except Exception as e: + print(f" 处理客户端 {client_address} 时出错: {e}") + finally: + writer.close() + await writer.wait_closed() + print(f" 客户端 {client_address} 已断开连接") + + async def start_async_server(): + """启动异步TCP服务器""" + server = await asyncio.start_server( + handle_client_async, + 'localhost', + 12348 + ) + + addr = server.sockets[0].getsockname() + print(f" 异步TCP服务器启动,监听 {addr[0]}:{addr[1]}") + + async with server: + await server.serve_forever() + + # 3. 异步HTTP服务器 + print("\n3. 异步HTTP服务器:") + + async def handle_http_request(request): + """处理HTTP请求""" + path = request.path + method = request.method + + print(f" 收到 {method} 请求: {path}") + + if path == '/': + return aiohttp.web.Response(text="欢迎访问异步HTTP服务器!") + elif path == '/api/time': + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return aiohttp.web.json_response({'time': current_time}) + elif path == '/api/echo' and method == 'POST': + data = await request.json() + return aiohttp.web.json_response({'echo': data}) + else: + return aiohttp.web.Response(text="页面未找到", status=404) + + async def start_http_server(): + """启动异步HTTP服务器""" + app = aiohttp.web.Application() + app.router.add_get('/', handle_http_request) + app.router.add_get('/api/time', handle_http_request) + app.router.add_post('/api/echo', handle_http_request) + + runner = aiohttp.web.AppRunner(app) + await runner.setup() + + site = aiohttp.web.TCPSite(runner, 'localhost', 8080) + await site.start() + + print(" 异步HTTP服务器启动,监听 localhost:8080") + print(" 访问 http://localhost:8080 查看效果") + + # 保持服务器运行 + try: + await asyncio.Future() # 永远等待 + except KeyboardInterrupt: + print("\n HTTP服务器正在关闭...") + finally: + await runner.cleanup() + + # 运行示例(注意:实际使用时需要在异步环境中运行) + print("\n异步网络编程示例:") + print("请在异步环境中运行以下函数:") + print("- asyncio.run(fetch_multiple_urls())") + print("- asyncio.run(start_async_server())") + print("- asyncio.run(start_http_server())") + +# 运行异步网络编程演示 +asyncio_networking_demo() +``` + +### 4.2 WebSocket编程 + +```python +import asyncio +import websockets +import json + +def websocket_demo(): + """WebSocket编程演示""" + print("=== WebSocket编程演示 ===") + + # 1. WebSocket服务器 + print("\n1. WebSocket服务器:") + + class WebSocketServer: + def __init__(self): + self.clients = set() + self.rooms = {} + + async def register_client(self, websocket, path): + """注册新客户端""" + self.clients.add(websocket) + client_id = id(websocket) + print(f" 客户端 {client_id} 已连接") + + try: + await self.handle_client(websocket) + except websockets.exceptions.ConnectionClosed: + print(f" 客户端 {client_id} 连接已关闭") + finally: + self.clients.remove(websocket) + # 从所有房间中移除客户端 + for room_clients in self.rooms.values(): + room_clients.discard(websocket) + + async def handle_client(self, websocket): + """处理客户端消息""" + async for message in websocket: + try: + data = json.loads(message) + await self.process_message(websocket, data) + except json.JSONDecodeError: + await websocket.send(json.dumps({ + 'type': 'error', + 'message': '无效的JSON格式' + })) + + async def process_message(self, websocket, data): + """处理消息""" + message_type = data.get('type') + + if message_type == 'join_room': + room = data.get('room', 'default') + if room not in self.rooms: + self.rooms[room] = set() + self.rooms[room].add(websocket) + + await websocket.send(json.dumps({ + 'type': 'joined', + 'room': room, + 'message': f'已加入房间 {room}' + })) + + # 通知房间内其他用户 + await self.broadcast_to_room(room, { + 'type': 'user_joined', + 'message': '新用户加入房间' + }, exclude=websocket) + + elif message_type == 'chat': + room = data.get('room', 'default') + content = data.get('content', '') + username = data.get('username', '匿名用户') + + # 广播消息到房间 + await self.broadcast_to_room(room, { + 'type': 'chat', + 'username': username, + 'content': content, + 'timestamp': datetime.now().isoformat() + }) + + elif message_type == 'ping': + await websocket.send(json.dumps({ + 'type': 'pong', + 'timestamp': datetime.now().isoformat() + })) + + async def broadcast_to_room(self, room, message, exclude=None): + """向房间广播消息""" + if room in self.rooms: + message_str = json.dumps(message) + disconnected = set() + + for client in self.rooms[room]: + if client != exclude: + try: + await client.send(message_str) + except websockets.exceptions.ConnectionClosed: + disconnected.add(client) + + # 清理断开的连接 + self.rooms[room] -= disconnected + + async def start_server(self, host='localhost', port=8765): + """启动WebSocket服务器""" + print(f" WebSocket服务器启动,监听 {host}:{port}") + + async with websockets.serve(self.register_client, host, port): + await asyncio.Future() # 永远运行 + + # 2. WebSocket客户端 + print("\n2. WebSocket客户端:") + + class WebSocketClient: + def __init__(self, uri): + self.uri = uri + self.websocket = None + + async def connect(self): + """连接到WebSocket服务器""" + self.websocket = await websockets.connect(self.uri) + print(f" 已连接到 {self.uri}") + + async def send_message(self, message): + """发送消息""" + if self.websocket: + await self.websocket.send(json.dumps(message)) + + async def listen_messages(self): + """监听消息""" + if self.websocket: + async for message in self.websocket: + data = json.loads(message) + await self.handle_message(data) + + async def handle_message(self, data): + """处理收到的消息""" + message_type = data.get('type') + + if message_type == 'chat': + username = data.get('username', '未知用户') + content = data.get('content', '') + timestamp = data.get('timestamp', '') + print(f" [{timestamp}] {username}: {content}") + + elif message_type == 'joined': + room = data.get('room') + print(f" 成功加入房间: {room}") + + elif message_type == 'user_joined': + print(f" {data.get('message')}") + + elif message_type == 'pong': + print(f" 收到pong: {data.get('timestamp')}") + + async def close(self): + """关闭连接""" + if self.websocket: + await self.websocket.close() + + # 3. 使用示例 + print("\n3. 使用示例:") + + async def websocket_server_example(): + """WebSocket服务器示例""" + server = WebSocketServer() + await server.start_server() + + async def websocket_client_example(): + """WebSocket客户端示例""" + client = WebSocketClient('ws://localhost:8765') + + try: + await client.connect() + + # 加入房间 + await client.send_message({ + 'type': 'join_room', + 'room': 'python_chat' + }) + + # 发送聊天消息 + await client.send_message({ + 'type': 'chat', + 'room': 'python_chat', + 'username': 'Python用户', + 'content': '大家好!' + }) + + # 发送ping + await client.send_message({ + 'type': 'ping' + }) + + # 监听消息(这里只监听5秒作为示例) + await asyncio.wait_for(client.listen_messages(), timeout=5.0) + + except asyncio.TimeoutError: + print(" 客户端示例完成") + finally: + await client.close() + + print("\nWebSocket编程示例:") + print("请在异步环境中运行以下函数:") + print("- 服务器: asyncio.run(websocket_server_example())") + print("- 客户端: asyncio.run(websocket_client_example())") + +# 运行WebSocket演示 +websocket_demo() +``` + +## 5. 网络安全 + +### 5.1 SSL/TLS编程 + +```python +import ssl +import socket +import urllib.request +import urllib.parse + +def ssl_tls_demo(): + """SSL/TLS编程演示""" + print("=== SSL/TLS编程演示 ===") + + # 1. SSL上下文配置 + print("\n1. SSL上下文配置:") + + def ssl_context_demo(): + """SSL上下文演示""" + # 创建SSL上下文 + context = ssl.create_default_context() + + # 配置SSL选项 + print(" SSL上下文配置:") + print(f" - 协议版本: {context.protocol}") + print(f" - 验证模式: {context.verify_mode}") + print(f" - 检查主机名: {context.check_hostname}") + + # 自定义SSL上下文 + custom_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + custom_context.check_hostname = False + custom_context.verify_mode = ssl.CERT_NONE + + print("\n 自定义SSL上下文(仅用于测试):") + print(f" - 验证模式: {custom_context.verify_mode}") + print(f" - 检查主机名: {custom_context.check_hostname}") + + # 加载证书和密钥(示例) + try: + # custom_context.load_cert_chain('client.crt', 'client.key') + # custom_context.load_verify_locations('ca.crt') + print(" - 证书链加载: 需要实际证书文件") + except Exception as e: + print(f" - 证书加载错误: {e}") + + # 2. HTTPS请求 + print("\n2. HTTPS请求:") + + def https_request_demo(): + """HTTPS请求演示""" + try: + # 标准HTTPS请求 + url = "https://httpbin.org/get" + response = urllib.request.urlopen(url) + + print(f" HTTPS请求成功: {response.getcode()}") + + # 获取SSL信息 + if hasattr(response, 'fp') and hasattr(response.fp, 'raw'): + sock = response.fp.raw._sock + if isinstance(sock, ssl.SSLSocket): + print(f" SSL版本: {sock.version()}") + print(f" 加密套件: {sock.cipher()}") + + # 自定义SSL上下文的HTTPS请求 + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + https_handler = urllib.request.HTTPSHandler(context=context) + opener = urllib.request.build_opener(https_handler) + + response = opener.open("https://httpbin.org/get") + print(f" 自定义SSL上下文请求: {response.getcode()}") + + except Exception as e: + print(f" HTTPS请求错误: {e}") + + # 3. SSL Socket编程 + print("\n3. SSL Socket编程:") + + def ssl_socket_demo(): + """SSL Socket演示""" + try: + # 创建SSL套接字 + context = ssl.create_default_context() + + # 连接到HTTPS服务器 + with socket.create_connection(('httpbin.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname='httpbin.org') as ssock: + print(f" SSL连接建立: {ssock.version()}") + print(f" 服务器证书: {ssock.getpeercert()['subject']}") + + # 发送HTTP请求 + request = b"GET /get HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n" + ssock.send(request) + + # 接收响应 + response = ssock.recv(4096) + response_str = response.decode('utf-8') + + # 提取状态行 + status_line = response_str.split('\r\n')[0] + print(f" HTTP响应: {status_line}") + + except Exception as e: + print(f" SSL Socket错误: {e}") + + # 4. 证书验证 + print("\n4. 证书验证:") + + def certificate_verification(): + """证书验证演示""" + try: + # 获取服务器证书 + context = ssl.create_default_context() + + with socket.create_connection(('httpbin.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname='httpbin.org') as ssock: + cert = ssock.getpeercert() + + print(" 服务器证书信息:") + print(f" - 主题: {cert.get('subject')}") + print(f" - 颁发者: {cert.get('issuer')}") + print(f" - 版本: {cert.get('version')}") + print(f" - 序列号: {cert.get('serialNumber')}") + print(f" - 有效期从: {cert.get('notBefore')}") + print(f" - 有效期到: {cert.get('notAfter')}") + + # 检查证书有效性 + import datetime + not_after = datetime.datetime.strptime( + cert['notAfter'], '%b %d %H:%M:%S %Y %Z' + ) + + if not_after > datetime.datetime.now(): + print(" - 证书状态: 有效") + else: + print(" - 证书状态: 已过期") + + except Exception as e: + print(f" 证书验证错误: {e}") + + # 运行所有示例 + ssl_context_demo() + https_request_demo() + ssl_socket_demo() + certificate_verification() + +# 运行SSL/TLS演示 +ssl_tls_demo() +``` + +### 5.2 网络安全最佳实践 + +```python +import hashlib +import hmac +import secrets +import base64 +from urllib.parse import quote, unquote + +def network_security_demo(): + """网络安全最佳实践演示""" + print("=== 网络安全最佳实践 ===") + + # 1. 数据加密和哈希 + print("\n1. 数据加密和哈希:") + + def encryption_hashing_demo(): + """加密和哈希演示""" + # 生成安全随机数 + random_token = secrets.token_hex(16) + print(f" 随机令牌: {random_token}") + + # 密码哈希 + password = "my_secure_password" + salt = secrets.token_hex(16) + + # 使用PBKDF2进行密码哈希 + password_hash = hashlib.pbkdf2_hmac( + 'sha256', + password.encode('utf-8'), + salt.encode('utf-8'), + 100000 # 迭代次数 + ) + + print(f" 密码盐值: {salt}") + print(f" 密码哈希: {password_hash.hex()}") + + # HMAC签名 + secret_key = secrets.token_bytes(32) + message = "重要的数据" + + signature = hmac.new( + secret_key, + message.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + print(f" HMAC签名: {signature}") + + # 验证HMAC签名 + def verify_hmac(key, message, signature): + expected = hmac.new( + key, + message.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature) + + is_valid = verify_hmac(secret_key, message, signature) + print(f" 签名验证: {'有效' if is_valid else '无效'}") + + # 2. 输入验证和清理 + print("\n2. 输入验证和清理:") + + def input_validation_demo(): + """输入验证演示""" + import re + + # URL编码/解码 + unsafe_input = "" + encoded_input = quote(unsafe_input) + decoded_input = unquote(encoded_input) + + print(f" 原始输入: {unsafe_input}") + print(f" URL编码: {encoded_input}") + print(f" URL解码: {decoded_input}") + + # HTML转义 + def html_escape(text): + """HTML转义""" + escape_chars = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + for char, escape in escape_chars.items(): + text = text.replace(char, escape) + return text + + escaped_html = html_escape(unsafe_input) + print(f" HTML转义: {escaped_html}") + + # 输入验证函数 + def validate_email(email): + """验证邮箱格式""" + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + + def validate_ip(ip): + """验证IP地址""" + parts = ip.split('.') + if len(parts) != 4: + return False + try: + return all(0 <= int(part) <= 255 for part in parts) + except ValueError: + return False + + # 测试验证函数 + test_emails = ['user@example.com', 'invalid-email', 'test@domain'] + test_ips = ['192.168.1.1', '256.1.1.1', '192.168.1'] + + print("\n 邮箱验证:") + for email in test_emails: + is_valid = validate_email(email) + print(f" {email}: {'有效' if is_valid else '无效'}") + + print("\n IP地址验证:") + for ip in test_ips: + is_valid = validate_ip(ip) + print(f" {ip}: {'有效' if is_valid else '无效'}") + + # 3. 安全的HTTP请求 + print("\n3. 安全的HTTP请求:") + + def secure_http_demo(): + """安全HTTP请求演示""" + # 安全请求头 + secure_headers = { + 'User-Agent': 'Python-Security-Demo/1.0', + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'X-Requested-With': 'XMLHttpRequest' + } + + print(" 安全请求头:") + for header, value in secure_headers.items(): + print(f" {header}: {value}") + + # 请求超时设置 + timeout_settings = { + 'connect_timeout': 10, # 连接超时 + 'read_timeout': 30, # 读取超时 + 'total_timeout': 60 # 总超时 + } + + print("\n 超时设置:") + for setting, value in timeout_settings.items(): + print(f" {setting}: {value}秒") + + # 代理设置示例 + proxy_config = { + 'http': 'http://proxy.example.com:8080', + 'https': 'https://proxy.example.com:8080' + } + + print("\n 代理配置示例:") + for protocol, proxy in proxy_config.items(): + print(f" {protocol}: {proxy}") + + # 4. 错误处理和日志 + print("\n4. 错误处理和日志:") + + def security_logging_demo(): + """安全日志演示""" + import logging + from datetime import datetime + + # 配置安全日志 + security_logger = logging.getLogger('security') + security_logger.setLevel(logging.INFO) + + # 创建格式化器 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + security_logger.addHandler(console_handler) + + # 安全事件记录 + def log_security_event(event_type, details, severity='INFO'): + """记录安全事件""" + timestamp = datetime.now().isoformat() + log_message = f"[{event_type}] {details} - {timestamp}" + + if severity == 'CRITICAL': + security_logger.critical(log_message) + elif severity == 'ERROR': + security_logger.error(log_message) + elif severity == 'WARNING': + security_logger.warning(log_message) + else: + security_logger.info(log_message) + + # 示例安全事件 + log_security_event('LOGIN_ATTEMPT', '用户尝试登录: user@example.com') + log_security_event('INVALID_INPUT', '检测到可疑输入', 'WARNING') + log_security_event('RATE_LIMIT', '请求频率超限', 'ERROR') + log_security_event('SECURITY_BREACH', '检测到安全漏洞', 'CRITICAL') + + print("\n 安全最佳实践:") + best_practices = [ + "始终使用HTTPS进行敏感数据传输", + "验证和清理所有用户输入", + "使用强密码和多因素认证", + "定期更新依赖库和系统", + "实施适当的访问控制", + "记录和监控安全事件", + "使用安全的随机数生成器", + "避免在日志中记录敏感信息", + "实施请求频率限制", + "使用安全的会话管理" + ] + + for i, practice in enumerate(best_practices, 1): + print(f" {i}. {practice}") + + # 运行所有示例 + encryption_hashing_demo() + input_validation_demo() + secure_http_demo() + security_logging_demo() + +# 运行网络安全演示 +network_security_demo() +``` + +## 6. 实际应用案例 + +### 6.1 简单的Web API客户端 + +```python +import urllib.request +import urllib.parse +import json +import time +from datetime import datetime + +class APIClient: + """简单的Web API客户端""" + + def __init__(self, base_url, api_key=None, timeout=30): + self.base_url = base_url.rstrip('/') + self.api_key = api_key + self.timeout = timeout + self.session_headers = { + 'User-Agent': 'Python-API-Client/1.0', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + + if api_key: + self.session_headers['Authorization'] = f'Bearer {api_key}' + + def _make_request(self, method, endpoint, data=None, params=None): + """发送HTTP请求""" + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + # 添加查询参数 + if params: + query_string = urllib.parse.urlencode(params) + url = f"{url}?{query_string}" + + # 准备请求数据 + request_data = None + if data: + request_data = json.dumps(data).encode('utf-8') + + # 创建请求 + request = urllib.request.Request( + url, + data=request_data, + headers=self.session_headers, + method=method + ) + + try: + with urllib.request.urlopen(request, timeout=self.timeout) as response: + content = response.read().decode('utf-8') + + return { + 'status_code': response.getcode(), + 'headers': dict(response.headers), + 'data': json.loads(content) if content else None + } + + except urllib.error.HTTPError as e: + error_content = e.read().decode('utf-8') + return { + 'status_code': e.code, + 'headers': dict(e.headers), + 'error': error_content + } + except Exception as e: + return { + 'status_code': 0, + 'error': str(e) + } + + def get(self, endpoint, params=None): + """GET请求""" + return self._make_request('GET', endpoint, params=params) + + def post(self, endpoint, data=None): + """POST请求""" + return self._make_request('POST', endpoint, data=data) + + def put(self, endpoint, data=None): + """PUT请求""" + return self._make_request('PUT', endpoint, data=data) + + def delete(self, endpoint): + """DELETE请求""" + return self._make_request('DELETE', endpoint) + +def api_client_demo(): + """API客户端演示""" + print("=== Web API客户端演示 ===") + + # 创建API客户端 + client = APIClient('https://httpbin.org') + + # GET请求 + print("\n1. GET请求:") + response = client.get('/get', params={'key': 'value', 'test': 'python'}) + if response['status_code'] == 200: + print(f" 状态码: {response['status_code']}") + print(f" 参数: {response['data']['args']}") + else: + print(f" 请求失败: {response.get('error')}") + + # POST请求 + print("\n2. POST请求:") + post_data = { + 'name': 'Python网络编程', + 'type': 'tutorial', + 'timestamp': datetime.now().isoformat() + } + + response = client.post('/post', data=post_data) + if response['status_code'] == 200: + print(f" 状态码: {response['status_code']}") + print(f" 发送的数据: {response['data']['json']}") + else: + print(f" 请求失败: {response.get('error')}") + + # 错误处理 + print("\n3. 错误处理:") + response = client.get('/status/404') + print(f" 状态码: {response['status_code']}") + if 'error' in response: + print(f" 错误信息: {response['error']}") + +# 运行API客户端演示 +api_client_demo() +``` + +### 6.2 文件下载器 + +```python +import urllib.request +import urllib.parse +import os +import time +from pathlib import Path + +class FileDownloader: + """文件下载器""" + + def __init__(self, download_dir='downloads', chunk_size=8192): + self.download_dir = Path(download_dir) + self.download_dir.mkdir(exist_ok=True) + self.chunk_size = chunk_size + + def download_file(self, url, filename=None, progress_callback=None): + """下载文件""" + try: + # 获取文件名 + if not filename: + parsed_url = urllib.parse.urlparse(url) + filename = os.path.basename(parsed_url.path) or 'download' + + filepath = self.download_dir / filename + + # 创建请求 + request = urllib.request.Request(url) + request.add_header('User-Agent', 'Python-Downloader/1.0') + + with urllib.request.urlopen(request) as response: + # 获取文件大小 + content_length = response.headers.get('Content-Length') + total_size = int(content_length) if content_length else None + + print(f"开始下载: {filename}") + if total_size: + print(f"文件大小: {self._format_size(total_size)}") + + downloaded = 0 + start_time = time.time() + + with open(filepath, 'wb') as f: + while True: + chunk = response.read(self.chunk_size) + if not chunk: + break + + f.write(chunk) + downloaded += len(chunk) + + # 调用进度回调 + if progress_callback: + progress_callback(downloaded, total_size) + + # 显示进度 + if total_size: + progress = (downloaded / total_size) * 100 + speed = downloaded / (time.time() - start_time) + print(f"\r进度: {progress:.1f}% ({self._format_size(downloaded)}/{self._format_size(total_size)}) - {self._format_size(speed)}/s", end='') + + print(f"\n下载完成: {filepath}") + return str(filepath) + + except Exception as e: + print(f"下载失败: {e}") + return None + + def download_multiple(self, urls, max_concurrent=3): + """下载多个文件""" + import threading + from queue import Queue + + def worker(): + while True: + url = url_queue.get() + if url is None: + break + + filename = os.path.basename(urllib.parse.urlparse(url).path) + print(f"\n[线程 {threading.current_thread().name}] 开始下载: {filename}") + + self.download_file(url) + url_queue.task_done() + + # 创建队列和线程 + url_queue = Queue() + threads = [] + + # 启动工作线程 + for i in range(min(max_concurrent, len(urls))): + t = threading.Thread(target=worker, name=f'Worker-{i+1}') + t.start() + threads.append(t) + + # 添加URL到队列 + for url in urls: + url_queue.put(url) + + # 等待所有下载完成 + url_queue.join() + + # 停止工作线程 + for _ in threads: + url_queue.put(None) + + for t in threads: + t.join() + + print("\n所有文件下载完成") + + def _format_size(self, size): + """格式化文件大小""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024: + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" + +def file_downloader_demo(): + """文件下载器演示""" + print("=== 文件下载器演示 ===") + + downloader = FileDownloader() + + # 单文件下载 + print("\n1. 单文件下载:") + test_url = "https://httpbin.org/json" + downloader.download_file(test_url, "test.json") + + # 多文件下载 + print("\n2. 多文件下载:") + test_urls = [ + "https://httpbin.org/json", + "https://httpbin.org/xml", + "https://httpbin.org/html" + ] + + downloader.download_multiple(test_urls) + +# 运行文件下载器演示 +file_downloader_demo() +``` + +## 7. 学习建议和总结 + +### 7.1 学习路径 + +```python +def learning_path(): + """网络编程学习路径""" + print("=== 网络编程学习路径 ===") + + learning_stages = { + "基础阶段": [ + "理解网络协议基础(TCP/IP、HTTP)", + "掌握socket编程基础", + "学习urllib模块使用", + "了解客户端-服务器架构" + ], + "进阶阶段": [ + "掌握异步网络编程(asyncio)", + "学习WebSocket编程", + "了解网络安全基础", + "掌握SSL/TLS编程" + ], + "高级阶段": [ + "学习网络框架(如aiohttp、tornado)", + "掌握微服务架构", + "了解负载均衡和集群", + "学习网络监控和调试" + ], + "实战阶段": [ + "开发Web API客户端", + "构建网络爬虫", + "实现聊天应用", + "开发分布式系统" + ] + } + + for stage, topics in learning_stages.items(): + print(f"\n{stage}:") + for i, topic in enumerate(topics, 1): + print(f" {i}. {topic}") + +def best_practices(): + """网络编程最佳实践""" + print("\n=== 网络编程最佳实践 ===") + + practices = { + "性能优化": [ + "使用连接池复用连接", + "实施适当的超时设置", + "使用异步编程提高并发性", + "合理设置缓冲区大小", + "避免阻塞操作" + ], + "错误处理": [ + "捕获和处理网络异常", + "实施重试机制", + "记录详细的错误日志", + "提供友好的错误信息", + "实施熔断器模式" + ], + "安全考虑": [ + "始终使用HTTPS传输敏感数据", + "验证和清理用户输入", + "实施认证和授权", + "使用安全的随机数", + "定期更新依赖库" + ], + "代码质量": [ + "编写清晰的文档", + "使用类型提示", + "编写单元测试", + "遵循编码规范", + "进行代码审查" + ] + } + + for category, items in practices.items(): + print(f"\n{category}:") + for i, item in enumerate(items, 1): + print(f" {i}. {item}") + +def common_pitfalls(): + """常见陷阱和解决方案""" + print("\n=== 常见陷阱和解决方案 ===") + + pitfalls = { + "阻塞操作": { + "问题": "同步网络操作阻塞程序执行", + "解决方案": "使用异步编程或多线程" + }, + "资源泄漏": { + "问题": "未正确关闭网络连接", + "解决方案": "使用上下文管理器或try-finally" + }, + "超时设置": { + "问题": "未设置适当的超时时间", + "解决方案": "根据应用场景设置合理超时" + }, + "错误处理": { + "问题": "未处理网络异常", + "解决方案": "捕获并适当处理各种网络异常" + }, + "安全漏洞": { + "问题": "未验证用户输入或使用不安全协议", + "解决方案": "实施输入验证和使用安全协议" + } + } + + for pitfall, details in pitfalls.items(): + print(f"\n{pitfall}:") + print(f" 问题: {details['问题']}") + print(f" 解决方案: {details['解决方案']}") + +def chapter_summary(): + """本章总结""" + print("\n=== 本章总结 ===") + + summary_points = [ + "网络编程是现代应用开发的重要技能", + "Python提供了丰富的网络编程库和工具", + "Socket编程是网络编程的基础", + "HTTP编程适用于Web应用开发", + "异步编程可以提高网络应用的性能", + "网络安全是网络编程中的重要考虑因素", + "实际项目中需要综合考虑性能、安全和可维护性", + "持续学习和实践是掌握网络编程的关键" + ] + + for i, point in enumerate(summary_points, 1): + print(f"{i}. {point}") + + print("\n通过本章的学习,你应该能够:") + skills = [ + "理解网络编程的基本概念和原理", + "使用socket进行TCP和UDP编程", + "使用urllib和http.client进行HTTP编程", + "开发简单的网络应用和爬虫", + "了解异步网络编程的基础", + "实施基本的网络安全措施", + "调试和优化网络应用" + ] + + for i, skill in enumerate(skills, 1): + print(f" {i}. {skill}") + +# 运行学习建议和总结 +learning_path() +best_practices() +common_pitfalls() +chapter_summary() +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/19.md b/docs/Python/19.md new file mode 100644 index 000000000..30f22a95a --- /dev/null +++ b/docs/Python/19.md @@ -0,0 +1,4048 @@ +--- +title: 第19天-电子邮件 +author: 哪吒 +date: '2023-06-15' +--- + +# 第19天-电子邮件 + +## 1. 电子邮件基础 + +### 1.1 电子邮件协议概述 + +```python +def email_protocols_overview(): + """电子邮件协议概述""" + print("=== 电子邮件协议概述 ===") + + protocols = { + "SMTP (Simple Mail Transfer Protocol)": { + "用途": "发送邮件", + "端口": "25 (非加密), 587 (STARTTLS), 465 (SSL/TLS)", + "特点": "用于邮件传输,从客户端到服务器或服务器间", + "Python模块": "smtplib" + }, + "POP3 (Post Office Protocol 3)": { + "用途": "接收邮件", + "端口": "110 (非加密), 995 (SSL/TLS)", + "特点": "下载邮件到本地,服务器上的邮件通常被删除", + "Python模块": "poplib" + }, + "IMAP (Internet Message Access Protocol)": { + "用途": "接收邮件", + "端口": "143 (非加密), 993 (SSL/TLS)", + "特点": "邮件保存在服务器上,支持多设备同步", + "Python模块": "imaplib" + } + } + + for protocol, details in protocols.items(): + print(f"\n{protocol}:") + for key, value in details.items(): + print(f" {key}: {value}") + + print("\n邮件处理流程:") + flow_steps = [ + "1. 编写邮件内容(文本、HTML、附件)", + "2. 使用SMTP协议发送邮件", + "3. 邮件服务器接收并转发邮件", + "4. 收件人使用POP3或IMAP接收邮件", + "5. 邮件客户端显示邮件内容" + ] + + for step in flow_steps: + print(f" {step}") + +# 运行协议概述 +email_protocols_overview() +``` + +### 1.2 Python邮件模块介绍 + +```python +import smtplib +import poplib +import imaplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email import encoders +from email.header import decode_header +from email.utils import parseaddr, formataddr +import os +from datetime import datetime + +def python_email_modules(): + """Python邮件模块介绍""" + print("=== Python邮件模块介绍 ===") + + modules = { + "smtplib": { + "功能": "SMTP客户端,用于发送邮件", + "主要类": "SMTP, SMTP_SSL", + "常用方法": "connect(), login(), sendmail(), quit()" + }, + "poplib": { + "功能": "POP3客户端,用于接收邮件", + "主要类": "POP3, POP3_SSL", + "常用方法": "user(), pass_(), list(), retr(), dele()" + }, + "imaplib": { + "功能": "IMAP客户端,用于接收邮件", + "主要类": "IMAP4, IMAP4_SSL", + "常用方法": "login(), select(), search(), fetch()" + }, + "email.mime": { + "功能": "构造邮件内容", + "主要类": "MIMEText, MIMEMultipart, MIMEBase", + "常用方法": "attach(), set_payload(), as_string()" + }, + "email.header": { + "功能": "处理邮件头部编码", + "主要函数": "decode_header(), make_header()", + "用途": "处理中文等非ASCII字符" + } + } + + for module, details in modules.items(): + print(f"\n{module}:") + for key, value in details.items(): + print(f" {key}: {value}") + + print("\n邮件格式标准:") + standards = [ + "RFC 5322: Internet Message Format (邮件格式)", + "RFC 2045-2049: MIME (多媒体邮件扩展)", + "RFC 3501: IMAP4rev1 (IMAP协议)", + "RFC 1939: POP3 (POP3协议)", + "RFC 5321: SMTP (SMTP协议)" + ] + + for standard in standards: + print(f" • {standard}") + +# 运行模块介绍 +python_email_modules() +``` + +## 2. 发送邮件 + +### 2.1 SMTP基础发送 + +```python +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.header import Header +from email.utils import formataddr + +def smtp_basic_demo(): + """SMTP基础发送演示""" + print("=== SMTP基础发送演示 ===") + + # 1. 发送纯文本邮件 + print("\n1. 发送纯文本邮件:") + + def send_text_email(): + """发送纯文本邮件""" + # 邮件配置 + smtp_server = "smtp.gmail.com" # Gmail SMTP服务器 + smtp_port = 587 # STARTTLS端口 + sender_email = "your_email@gmail.com" + sender_password = "your_app_password" # 应用专用密码 + + # 收件人信息 + receiver_email = "recipient@example.com" + + # 创建邮件内容 + subject = "Python发送的测试邮件" + body = """ + 这是一封使用Python发送的测试邮件。 + + 邮件内容: + - 发送时间:{} + - 发送方式:SMTP + - 编程语言:Python + + 祝好! + """.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + # 创建MIMEText对象 + message = MIMEText(body, 'plain', 'utf-8') + message['From'] = formataddr(("Python发送者", sender_email)) + message['To'] = receiver_email + message['Subject'] = Header(subject, 'utf-8') + + try: + # 连接SMTP服务器 + print(" 正在连接SMTP服务器...") + server = smtplib.SMTP(smtp_server, smtp_port) + server.starttls() # 启用TLS加密 + + # 登录 + print(" 正在登录...") + server.login(sender_email, sender_password) + + # 发送邮件 + print(" 正在发送邮件...") + text = message.as_string() + server.sendmail(sender_email, receiver_email, text) + + print(" ✓ 邮件发送成功!") + + except Exception as e: + print(f" ✗ 邮件发送失败: {e}") + finally: + server.quit() + + # 2. 发送HTML邮件 + print("\n2. 发送HTML邮件:") + + def send_html_email(): + """发送HTML邮件""" + # 邮件配置(示例配置) + smtp_config = { + 'server': 'smtp.example.com', + 'port': 587, + 'username': 'your_email@example.com', + 'password': 'your_password' + } + + # HTML邮件内容 + html_content = """ + + + + + Python HTML邮件 + + + +
+

Python邮件发送测试

+
+
+

邮件内容

+

这是一封使用Python发送的HTML格式邮件。

+ +

功能特点:

+
    +
  • 支持HTML格式
  • +
  • 支持CSS样式
  • +
  • 支持图片和链接
  • +
  • 支持表格和列表
  • +
+ +

技术信息:

+ + + + + + + + + + + + + + + + + +
项目
发送时间{}
编程语言Python
协议SMTP
+ +

访问我们的网站:Python官网

+
+ + + + """.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + # 创建HTML邮件 + message = MIMEMultipart('alternative') + message['From'] = formataddr(("Python HTML发送者", smtp_config['username'])) + message['To'] = "recipient@example.com" + message['Subject'] = Header("Python HTML邮件测试", 'utf-8') + + # 添加HTML内容 + html_part = MIMEText(html_content, 'html', 'utf-8') + message.attach(html_part) + + print(" HTML邮件内容已准备完成") + print(" 邮件大小:", len(message.as_string()), "字节") + + # 注意:这里只是演示,实际发送需要真实的SMTP配置 + print(" 注意:需要配置真实的SMTP服务器信息才能发送") + + # 3. 批量发送邮件 + print("\n3. 批量发送邮件:") + + def send_bulk_emails(): + """批量发送邮件""" + # 收件人列表 + recipients = [ + {"email": "user1@example.com", "name": "用户一"}, + {"email": "user2@example.com", "name": "用户二"}, + {"email": "user3@example.com", "name": "用户三"} + ] + + # 邮件模板 + def create_personalized_email(recipient): + """创建个性化邮件""" + subject = f"欢迎 {recipient['name']} 加入我们!" + + body = f""" + 亲爱的 {recipient['name']}, + + 欢迎您加入我们的Python学习社区! + + 您的邮箱:{recipient['email']} + 注册时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + + 我们为您准备了丰富的学习资源: + • Python基础教程 + • 实战项目案例 + • 在线编程练习 + • 技术交流社区 + + 期待与您一起学习进步! + + Python学习团队 + """ + + message = MIMEText(body, 'plain', 'utf-8') + message['From'] = formataddr(("Python学习团队", "team@pythonlearn.com")) + message['To'] = recipient['email'] + message['Subject'] = Header(subject, 'utf-8') + + return message + + # 模拟批量发送 + print(" 开始批量发送邮件...") + + success_count = 0 + failed_count = 0 + + for i, recipient in enumerate(recipients, 1): + try: + message = create_personalized_email(recipient) + + # 这里模拟发送过程 + print(f" [{i}/{len(recipients)}] 正在发送给 {recipient['name']} ({recipient['email']})") + + # 实际发送代码会在这里 + # server.sendmail(sender, recipient['email'], message.as_string()) + + success_count += 1 + print(f" ✓ 发送成功") + + except Exception as e: + failed_count += 1 + print(f" ✗ 发送失败: {e}") + + print(f"\n 批量发送完成:") + print(f" 成功: {success_count} 封") + print(f" 失败: {failed_count} 封") + print(f" 总计: {len(recipients)} 封") + + # 运行示例(注意:需要真实SMTP配置才能实际发送) + print("\n注意:以下示例需要配置真实的SMTP服务器信息") + print("建议使用Gmail、QQ邮箱等提供的SMTP服务") + + # send_text_email() # 取消注释并配置SMTP信息后可运行 + send_html_email() + send_bulk_emails() + +# 运行SMTP基础演示 +smtp_basic_demo() +``` + +### 2.2 邮件附件处理 + +```python +import os +import mimetypes +from email.mime.base import MIMEBase +from email.mime.image import MIMEImage +from email.mime.audio import MIMEAudio +from email.mime.application import MIMEApplication +from email import encoders +from pathlib import Path + +def email_attachments_demo(): + """邮件附件处理演示""" + print("=== 邮件附件处理演示 ===") + + # 1. 添加文件附件 + print("\n1. 添加文件附件:") + + def add_file_attachment(message, file_path): + """添加文件附件""" + if not os.path.exists(file_path): + print(f" 文件不存在: {file_path}") + return False + + # 获取文件信息 + filename = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + + print(f" 添加附件: {filename} ({file_size} 字节)") + + # 猜测文件类型 + ctype, encoding = mimetypes.guess_type(file_path) + if ctype is None or encoding is not None: + ctype = 'application/octet-stream' + + maintype, subtype = ctype.split('/', 1) + + try: + with open(file_path, 'rb') as fp: + if maintype == 'text': + # 文本文件 + attachment = MIMEText(fp.read().decode('utf-8'), _subtype=subtype) + elif maintype == 'image': + # 图片文件 + attachment = MIMEImage(fp.read(), _subtype=subtype) + elif maintype == 'audio': + # 音频文件 + attachment = MIMEAudio(fp.read(), _subtype=subtype) + else: + # 其他文件类型 + attachment = MIMEBase(maintype, subtype) + attachment.set_payload(fp.read()) + encoders.encode_base64(attachment) + + # 设置附件头部 + attachment.add_header( + 'Content-Disposition', + 'attachment', + filename=('utf-8', '', filename) + ) + + # 添加到邮件 + message.attach(attachment) + print(f" ✓ 附件添加成功: {filename}") + return True + + except Exception as e: + print(f" ✗ 附件添加失败: {e}") + return False + + # 2. 创建带附件的邮件 + print("\n2. 创建带附件的邮件:") + + def create_email_with_attachments(): + """创建带附件的邮件""" + # 创建多部分邮件 + message = MIMEMultipart() + message['From'] = formataddr(("Python发送者", "sender@example.com")) + message['To'] = "recipient@example.com" + message['Subject'] = Header("带附件的邮件测试", 'utf-8') + + # 邮件正文 + body = """ + 这是一封带有附件的测试邮件。 + + 附件说明: + • 文档文件:包含项目说明 + • 图片文件:项目截图 + • 数据文件:分析结果 + + 请查收附件内容。 + + 谢谢! + """ + + message.attach(MIMEText(body, 'plain', 'utf-8')) + + # 模拟添加不同类型的附件 + attachments_info = [ + {"name": "document.txt", "type": "文本文档", "size": "2.5 KB"}, + {"name": "screenshot.png", "type": "PNG图片", "size": "156 KB"}, + {"name": "data.csv", "type": "CSV数据", "size": "45 KB"}, + {"name": "report.pdf", "type": "PDF文档", "size": "1.2 MB"} + ] + + print(" 准备添加的附件:") + total_size = 0 + for i, att in enumerate(attachments_info, 1): + print(f" {i}. {att['name']} ({att['type']}, {att['size']})") + # 这里模拟文件大小计算 + size_num = float(att['size'].split()[0]) + if 'KB' in att['size']: + total_size += size_num * 1024 + elif 'MB' in att['size']: + total_size += size_num * 1024 * 1024 + + print(f" 总附件大小: {total_size/1024/1024:.2f} MB") + + # 检查邮件大小限制 + if total_size > 25 * 1024 * 1024: # 25MB限制 + print(" ⚠️ 警告:附件总大小超过25MB,可能被邮件服务器拒绝") + + return message + + # 3. 内嵌图片处理 + print("\n3. 内嵌图片处理:") + + def create_email_with_embedded_images(): + """创建带内嵌图片的邮件""" + # 创建相关多部分邮件 + message = MIMEMultipart('related') + message['From'] = formataddr(("Python发送者", "sender@example.com")) + message['To'] = "recipient@example.com" + message['Subject'] = Header("带内嵌图片的HTML邮件", 'utf-8') + + # HTML内容,引用内嵌图片 + html_content = """ + + + + + 内嵌图片邮件 + + +

Python邮件系统

+

这是一封包含内嵌图片的HTML邮件。

+ +

项目截图:

+ 项目截图 + +

Logo:

+ 公司Logo + +

图片已内嵌在邮件中,无需额外下载。

+ + + """ + + # 添加HTML内容 + html_part = MIMEText(html_content, 'html', 'utf-8') + message.attach(html_part) + + # 模拟添加内嵌图片 + embedded_images = [ + {"cid": "screenshot", "file": "screenshot.png", "desc": "项目截图"}, + {"cid": "logo", "file": "logo.png", "desc": "公司Logo"} + ] + + print(" 内嵌图片信息:") + for img in embedded_images: + print(f" CID: {img['cid']} -> {img['file']} ({img['desc']})") + + # 实际使用时的代码示例 + print(f" # 添加内嵌图片的代码:") + print(f" # with open('{img['file']}', 'rb') as f:") + print(f" # img_data = f.read()") + print(f" # image = MIMEImage(img_data)") + print(f" # image.add_header('Content-ID', '<{img['cid']}>')") + print(f" # message.attach(image)") + + return message + + # 4. 附件安全检查 + print("\n4. 附件安全检查:") + + def check_attachment_security(file_path): + """检查附件安全性""" + if not os.path.exists(file_path): + return False, "文件不存在" + + filename = os.path.basename(file_path) + file_ext = os.path.splitext(filename)[1].lower() + file_size = os.path.getsize(file_path) + + # 危险文件扩展名 + dangerous_extensions = { + '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', + '.jar', '.msi', '.dll', '.sys', '.drv', '.ocx' + } + + # 检查文件扩展名 + if file_ext in dangerous_extensions: + return False, f"危险的文件类型: {file_ext}" + + # 检查文件大小(25MB限制) + max_size = 25 * 1024 * 1024 + if file_size > max_size: + return False, f"文件过大: {file_size/1024/1024:.2f}MB > 25MB" + + # 检查文件名 + if len(filename) > 255: + return False, "文件名过长" + + # 检查特殊字符 + invalid_chars = '<>:"/\\|?*' + if any(char in filename for char in invalid_chars): + return False, "文件名包含非法字符" + + return True, "文件安全" + + # 测试安全检查 + test_files = [ + "document.txt", + "image.png", + "script.exe", # 危险文件 + "very_long_filename_" + "x" * 250 + ".txt", # 文件名过长 + "fileinvalid:chars.txt" # 非法字符 + ] + + print(" 附件安全检查结果:") + for file_path in test_files: + is_safe, message = check_attachment_security(file_path) + status = "✓ 安全" if is_safe else "✗ 危险" + print(f" {file_path}: {status} - {message}") + + # 运行示例 + create_email_with_attachments() + create_email_with_embedded_images() + +# 运行附件处理演示 +email_attachments_demo() +``` + +### 2.3 高级发送功能 + +```python +import time +import threading +from queue import Queue +from datetime import datetime, timedelta +import json +import logging + +def advanced_email_features(): + """高级邮件发送功能演示""" + print("=== 高级邮件发送功能演示 ===") + + # 1. 邮件模板系统 + print("\n1. 邮件模板系统:") + + class EmailTemplate: + """邮件模板类""" + + def __init__(self, name, subject_template, body_template, template_type='text'): + self.name = name + self.subject_template = subject_template + self.body_template = body_template + self.template_type = template_type + self.created_at = datetime.now() + + def render(self, **kwargs): + """渲染模板""" + try: + subject = self.subject_template.format(**kwargs) + body = self.body_template.format(**kwargs) + return subject, body + except KeyError as e: + raise ValueError(f"模板变量缺失: {e}") + + def validate_variables(self, **kwargs): + """验证模板变量""" + import re + + # 提取模板中的变量 + subject_vars = set(re.findall(r'\{(\w+)\}', self.subject_template)) + body_vars = set(re.findall(r'\{(\w+)\}', self.body_template)) + required_vars = subject_vars | body_vars + + provided_vars = set(kwargs.keys()) + missing_vars = required_vars - provided_vars + + if missing_vars: + return False, f"缺少变量: {', '.join(missing_vars)}" + + return True, "变量完整" + + # 创建邮件模板 + templates = { + 'welcome': EmailTemplate( + name='欢迎邮件', + subject_template='欢迎 {username} 加入 {platform}!', + body_template=""" + 亲爱的 {username}, + + 欢迎您加入 {platform}! + + 您的账户信息: + • 用户名:{username} + • 邮箱:{email} + • 注册时间:{register_time} + • 会员等级:{membership_level} + + 接下来您可以: + 1. 完善个人资料 + 2. 浏览学习资源 + 3. 参与社区讨论 + 4. 开始您的学习之旅 + + 如有任何问题,请随时联系我们。 + + 祝您学习愉快! + + {platform} 团队 + """ + ), + 'notification': EmailTemplate( + name='通知邮件', + subject_template='{notification_type}:{title}', + body_template=""" + {username},您好! + + 您有一条新的{notification_type}: + + 标题:{title} + 内容:{content} + 时间:{timestamp} + 优先级:{priority} + + {action_required} + + 详情请登录系统查看。 + + 系统自动发送,请勿回复。 + """ + ) + } + + # 测试模板渲染 + print(" 测试邮件模板:") + + # 欢迎邮件测试 + welcome_data = { + 'username': '张三', + 'platform': 'Python学习平台', + 'email': 'zhangsan@example.com', + 'register_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'membership_level': '普通会员' + } + + try: + subject, body = templates['welcome'].render(**welcome_data) + print(f" 欢迎邮件主题: {subject}") + print(f" 邮件长度: {len(body)} 字符") + except Exception as e: + print(f" 模板渲染失败: {e}") + + # 通知邮件测试 + notification_data = { + 'username': '李四', + 'notification_type': '系统通知', + 'title': '课程更新提醒', + 'content': 'Python高级编程课程已更新第10章内容', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'priority': '中等', + 'action_required': '请及时查看新增内容。' + } + + try: + subject, body = templates['notification'].render(**notification_data) + print(f" 通知邮件主题: {subject}") + print(f" 邮件长度: {len(body)} 字符") + except Exception as e: + print(f" 模板渲染失败: {e}") + + # 2. 邮件队列系统 + print("\n2. 邮件队列系统:") + + class EmailQueue: + """邮件队列管理器""" + + def __init__(self, max_workers=3, retry_times=3): + self.queue = Queue() + self.max_workers = max_workers + self.retry_times = retry_times + self.workers = [] + self.is_running = False + self.sent_count = 0 + self.failed_count = 0 + + # 配置日志 + self.logger = logging.getLogger('EmailQueue') + self.logger.setLevel(logging.INFO) + + if not self.logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + def add_email(self, email_data, priority=1): + """添加邮件到队列""" + email_item = { + 'data': email_data, + 'priority': priority, + 'retry_count': 0, + 'created_at': datetime.now(), + 'id': f"email_{int(time.time() * 1000)}" + } + + self.queue.put(email_item) + self.logger.info(f"邮件已加入队列: {email_item['id']}") + + def worker(self, worker_id): + """工作线程""" + self.logger.info(f"工作线程 {worker_id} 启动") + + while self.is_running: + try: + # 获取邮件任务 + email_item = self.queue.get(timeout=1) + + self.logger.info( + f"[Worker-{worker_id}] 处理邮件: {email_item['id']}" + ) + + # 模拟发送邮件 + success = self._send_email(email_item['data']) + + if success: + self.sent_count += 1 + self.logger.info( + f"[Worker-{worker_id}] 邮件发送成功: {email_item['id']}" + ) + else: + # 重试机制 + email_item['retry_count'] += 1 + + if email_item['retry_count'] < self.retry_times: + self.logger.warning( + f"[Worker-{worker_id}] 邮件发送失败,重试 {email_item['retry_count']}/{self.retry_times}: {email_item['id']}" + ) + # 重新加入队列 + time.sleep(2 ** email_item['retry_count']) # 指数退避 + self.queue.put(email_item) + else: + self.failed_count += 1 + self.logger.error( + f"[Worker-{worker_id}] 邮件发送最终失败: {email_item['id']}" + ) + + self.queue.task_done() + + except Exception as e: + if "timed out" not in str(e): + self.logger.error(f"[Worker-{worker_id}] 处理错误: {e}") + + self.logger.info(f"工作线程 {worker_id} 停止") + + def _send_email(self, email_data): + """模拟发送邮件""" + # 模拟网络延迟 + time.sleep(0.5) + + # 模拟90%成功率 + import random + return random.random() > 0.1 + + def start(self): + """启动队列处理""" + if self.is_running: + return + + self.is_running = True + self.sent_count = 0 + self.failed_count = 0 + + # 启动工作线程 + for i in range(self.max_workers): + worker = threading.Thread( + target=self.worker, + args=(i + 1,), + daemon=True + ) + worker.start() + self.workers.append(worker) + + self.logger.info(f"邮件队列启动,工作线程数: {self.max_workers}") + + def stop(self): + """停止队列处理""" + self.is_running = False + + # 等待队列清空 + self.queue.join() + + self.logger.info("邮件队列已停止") + self.logger.info(f"发送统计 - 成功: {self.sent_count}, 失败: {self.failed_count}") + + def get_status(self): + """获取队列状态""" + return { + 'queue_size': self.queue.qsize(), + 'is_running': self.is_running, + 'workers': len(self.workers), + 'sent_count': self.sent_count, + 'failed_count': self.failed_count + } + + # 测试邮件队列 + print(" 测试邮件队列系统:") + + email_queue = EmailQueue(max_workers=2) + email_queue.start() + + # 添加测试邮件 + test_emails = [ + {'to': 'user1@example.com', 'subject': '测试邮件1', 'body': '内容1'}, + {'to': 'user2@example.com', 'subject': '测试邮件2', 'body': '内容2'}, + {'to': 'user3@example.com', 'subject': '测试邮件3', 'body': '内容3'}, + {'to': 'user4@example.com', 'subject': '测试邮件4', 'body': '内容4'}, + {'to': 'user5@example.com', 'subject': '测试邮件5', 'body': '内容5'} + ] + + for email_data in test_emails: + email_queue.add_email(email_data) + + print(f" 已添加 {len(test_emails)} 封邮件到队列") + + # 等待处理完成 + time.sleep(3) + + status = email_queue.get_status() + print(f" 队列状态: {status}") + + email_queue.stop() + + # 3. 邮件发送统计 + print("\n3. 邮件发送统计:") + + class EmailStatistics: + """邮件发送统计""" + + def __init__(self): + self.stats = { + 'total_sent': 0, + 'total_failed': 0, + 'daily_stats': {}, + 'hourly_stats': {}, + 'recipient_stats': {}, + 'template_stats': {} + } + + def record_sent(self, recipient, template_name=None): + """记录发送成功""" + now = datetime.now() + date_key = now.strftime('%Y-%m-%d') + hour_key = now.strftime('%Y-%m-%d %H:00') + + self.stats['total_sent'] += 1 + + # 按日统计 + if date_key not in self.stats['daily_stats']: + self.stats['daily_stats'][date_key] = {'sent': 0, 'failed': 0} + self.stats['daily_stats'][date_key]['sent'] += 1 + + # 按小时统计 + if hour_key not in self.stats['hourly_stats']: + self.stats['hourly_stats'][hour_key] = {'sent': 0, 'failed': 0} + self.stats['hourly_stats'][hour_key]['sent'] += 1 + + # 按收件人统计 + if recipient not in self.stats['recipient_stats']: + self.stats['recipient_stats'][recipient] = {'sent': 0, 'failed': 0} + self.stats['recipient_stats'][recipient]['sent'] += 1 + + # 按模板统计 + if template_name: + if template_name not in self.stats['template_stats']: + self.stats['template_stats'][template_name] = {'sent': 0, 'failed': 0} + self.stats['template_stats'][template_name]['sent'] += 1 + + def record_failed(self, recipient, template_name=None): + """记录发送失败""" + now = datetime.now() + date_key = now.strftime('%Y-%m-%d') + hour_key = now.strftime('%Y-%m-%d %H:00') + + self.stats['total_failed'] += 1 + + # 按日统计 + if date_key not in self.stats['daily_stats']: + self.stats['daily_stats'][date_key] = {'sent': 0, 'failed': 0} + self.stats['daily_stats'][date_key]['failed'] += 1 + + # 按小时统计 + if hour_key not in self.stats['hourly_stats']: + self.stats['hourly_stats'][hour_key] = {'sent': 0, 'failed': 0} + self.stats['hourly_stats'][hour_key]['failed'] += 1 + + # 按收件人统计 + if recipient not in self.stats['recipient_stats']: + self.stats['recipient_stats'][recipient] = {'sent': 0, 'failed': 0} + self.stats['recipient_stats'][recipient]['failed'] += 1 + + # 按模板统计 + if template_name: + if template_name not in self.stats['template_stats']: + self.stats['template_stats'][template_name] = {'sent': 0, 'failed': 0} + self.stats['template_stats'][template_name]['failed'] += 1 + + def get_summary(self): + """获取统计摘要""" + total = self.stats['total_sent'] + self.stats['total_failed'] + success_rate = (self.stats['total_sent'] / total * 100) if total > 0 else 0 + + return { + 'total_emails': total, + 'successful': self.stats['total_sent'], + 'failed': self.stats['total_failed'], + 'success_rate': f"{success_rate:.2f}%", + 'unique_recipients': len(self.stats['recipient_stats']), + 'templates_used': len(self.stats['template_stats']) + } + + def export_stats(self, filename=None): + """导出统计数据""" + if not filename: + filename = f"email_stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(self.stats, f, ensure_ascii=False, indent=2, default=str) + return filename + except Exception as e: + print(f"导出统计数据失败: {e}") + return None + + # 测试统计功能 + stats = EmailStatistics() + + # 模拟一些发送记录 + test_records = [ + ('user1@example.com', 'welcome', True), + ('user2@example.com', 'welcome', True), + ('user3@example.com', 'notification', False), + ('user1@example.com', 'notification', True), + ('user4@example.com', 'welcome', True) + ] + + for recipient, template, success in test_records: + if success: + stats.record_sent(recipient, template) + else: + stats.record_failed(recipient, template) + + summary = stats.get_summary() + print(" 发送统计摘要:") + for key, value in summary.items(): + print(f" {key}: {value}") + + # 导出统计数据 + # export_file = stats.export_stats() + # if export_file: + # print(f" 统计数据已导出到: {export_file}") + +# 运行高级功能演示 +advanced_email_features() +``` + +## 3. 接收邮件 + +### 3.1 POP3接收邮件 + +```python +import poplib +from email.parser import Parser +from email.header import decode_header +from email.utils import parseaddr +import base64 +import quopri + +def pop3_receive_demo(): + """POP3接收邮件演示""" + print("=== POP3接收邮件演示 ===") + + # 1. 连接POP3服务器 + print("\n1. 连接POP3服务器:") + + def connect_pop3_server(): + """连接POP3服务器""" + # POP3服务器配置 + pop3_config = { + 'server': 'pop.gmail.com', + 'port': 995, # SSL端口 + 'username': 'your_email@gmail.com', + 'password': 'your_app_password' + } + + try: + print(" 正在连接POP3服务器...") + + # 连接SSL POP3服务器 + server = poplib.POP3_SSL(pop3_config['server'], pop3_config['port']) + + # 登录 + print(" 正在登录...") + server.user(pop3_config['username']) + server.pass_(pop3_config['password']) + + # 获取邮箱统计信息 + num_messages, total_size = server.stat() + print(f" ✓ 连接成功!") + print(f" 邮件数量: {num_messages}") + print(f" 总大小: {total_size} 字节") + + return server + + except Exception as e: + print(f" ✗ 连接失败: {e}") + return None + + # 2. 获取邮件列表 + print("\n2. 获取邮件列表:") + + def get_email_list(server): + """获取邮件列表""" + if not server: + print(" 服务器连接无效") + return [] + + try: + # 获取邮件列表 + messages = server.list() + print(f" 邮件列表响应: {messages[0]}") + + email_list = [] + for msg_info in messages[1]: + # 解析邮件信息:序号 大小 + parts = msg_info.decode('utf-8').split() + msg_num = int(parts[0]) + msg_size = int(parts[1]) + + email_list.append({ + 'number': msg_num, + 'size': msg_size + }) + + print(f" 获取到 {len(email_list)} 封邮件:") + for i, email_info in enumerate(email_list[:5], 1): # 只显示前5封 + print(f" {i}. 邮件{email_info['number']}: {email_info['size']} 字节") + + if len(email_list) > 5: + print(f" ... 还有 {len(email_list) - 5} 封邮件") + + return email_list + + except Exception as e: + print(f" 获取邮件列表失败: {e}") + return [] + + # 3. 下载和解析邮件 + print("\n3. 下载和解析邮件:") + + def download_and_parse_email(server, msg_num): + """下载和解析邮件""" + if not server: + print(" 服务器连接无效") + return None + + try: + print(f" 正在下载邮件 {msg_num}...") + + # 获取邮件内容 + response, lines, octets = server.retr(msg_num) + + # 合并邮件内容 + raw_email = b'\n'.join(lines) + + # 解析邮件 + parser = Parser() + email_message = parser.parsestr(raw_email.decode('utf-8', errors='ignore')) + + # 提取邮件信息 + email_info = { + 'number': msg_num, + 'subject': decode_email_header(email_message.get('Subject', '')), + 'from': decode_email_header(email_message.get('From', '')), + 'to': decode_email_header(email_message.get('To', '')), + 'date': email_message.get('Date', ''), + 'size': octets + } + + print(f" ✓ 邮件下载成功") + print(f" 主题: {email_info['subject']}") + print(f" 发件人: {email_info['from']}") + print(f" 收件人: {email_info['to']}") + print(f" 日期: {email_info['date']}") + print(f" 大小: {email_info['size']} 字节") + + # 提取邮件正文 + body = extract_email_body(email_message) + if body: + print(f" 正文预览: {body[:100]}...") + + return email_info, email_message + + except Exception as e: + print(f" 下载邮件失败: {e}") + return None, None + + def decode_email_header(header): + """解码邮件头部""" + if not header: + return '' + + try: + decoded_parts = decode_header(header) + decoded_str = '' + + for part, encoding in decoded_parts: + if isinstance(part, bytes): + if encoding: + decoded_str += part.decode(encoding) + else: + decoded_str += part.decode('utf-8', errors='ignore') + else: + decoded_str += part + + return decoded_str + except Exception as e: + return header + + def extract_email_body(email_message): + """提取邮件正文""" + body = '' + + try: + if email_message.is_multipart(): + # 多部分邮件 + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get('Content-Disposition', '')) + + # 跳过附件 + if 'attachment' in content_disposition: + continue + + if content_type == 'text/plain': + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='ignore') + break + elif content_type == 'text/html' and not body: + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='ignore') + else: + # 单部分邮件 + charset = email_message.get_content_charset() or 'utf-8' + body = email_message.get_payload(decode=True).decode(charset, errors='ignore') + + return body + except Exception as e: + print(f" 提取邮件正文失败: {e}") + return '' + + # 4. 删除邮件 + print("\n4. 删除邮件:") + + def delete_email(server, msg_num): + """删除邮件""" + if not server: + print(" 服务器连接无效") + return False + + try: + print(f" 正在删除邮件 {msg_num}...") + server.dele(msg_num) + print(f" ✓ 邮件 {msg_num} 已标记为删除") + print(" 注意:邮件将在quit()后真正删除") + return True + except Exception as e: + print(f" 删除邮件失败: {e}") + return False + + # 模拟POP3操作 + print(" 模拟POP3邮件接收流程:") + print(" 注意:需要配置真实的POP3服务器信息") + + # 模拟邮件信息 + mock_emails = [ + { + 'number': 1, + 'subject': 'Python学习资料', + 'from': 'teacher@pythonlearn.com', + 'date': '2023-06-15 10:30:00', + 'size': 2048 + }, + { + 'number': 2, + 'subject': '系统通知', + 'from': 'system@example.com', + 'date': '2023-06-15 14:20:00', + 'size': 1024 + } + ] + + print(f" 模拟邮件列表 ({len(mock_emails)} 封):") + for email in mock_emails: + print(f" 邮件{email['number']}: {email['subject']} - {email['from']}") + + # server = connect_pop3_server() # 需要真实配置 + # if server: + # email_list = get_email_list(server) + # if email_list: + # # 下载第一封邮件 + # download_and_parse_email(server, 1) + # server.quit() + +# 运行POP3演示 +pop3_receive_demo() +``` + +### 3.2 IMAP接收邮件 + +```python +import imaplib +import email +from email.header import decode_header +import re + +def imap_receive_demo(): + """IMAP接收邮件演示""" + print("=== IMAP接收邮件演示 ===") + + # 1. 连接IMAP服务器 + print("\n1. 连接IMAP服务器:") + + def connect_imap_server(): + """连接IMAP服务器""" + # IMAP服务器配置 + imap_config = { + 'server': 'imap.gmail.com', + 'port': 993, # SSL端口 + 'username': 'your_email@gmail.com', + 'password': 'your_app_password' + } + + try: + print(" 正在连接IMAP服务器...") + + # 连接SSL IMAP服务器 + server = imaplib.IMAP4_SSL(imap_config['server'], imap_config['port']) + + # 登录 + print(" 正在登录...") + server.login(imap_config['username'], imap_config['password']) + + print(f" ✓ 连接成功!") + + return server + + except Exception as e: + print(f" ✗ 连接失败: {e}") + return None + + # 2. 选择邮箱文件夹 + print("\n2. 选择邮箱文件夹:") + + def list_mailboxes(server): + """列出邮箱文件夹""" + if not server: + print(" 服务器连接无效") + return [] + + try: + # 获取文件夹列表 + status, folders = server.list() + + if status == 'OK': + print(" 可用文件夹:") + folder_list = [] + + for folder in folders: + # 解析文件夹信息 + folder_str = folder.decode('utf-8') + # 提取文件夹名称 + match = re.search(r'"([^"]+)"$', folder_str) + if match: + folder_name = match.group(1) + else: + # 如果没有引号,取最后一个空格后的内容 + folder_name = folder_str.split()[-1] + + folder_list.append(folder_name) + print(f" • {folder_name}") + + return folder_list + else: + print(" 获取文件夹列表失败") + return [] + + except Exception as e: + print(f" 获取文件夹列表失败: {e}") + return [] + + def select_mailbox(server, mailbox='INBOX'): + """选择邮箱文件夹""" + if not server: + print(" 服务器连接无效") + return False + + try: + print(f" 正在选择文件夹: {mailbox}") + status, messages = server.select(mailbox) + + if status == 'OK': + num_messages = int(messages[0]) + print(f" ✓ 文件夹选择成功") + print(f" 邮件数量: {num_messages}") + return True + else: + print(f" 文件夹选择失败: {status}") + return False + + except Exception as e: + print(f" 选择文件夹失败: {e}") + return False + + # 3. 搜索邮件 + print("\n3. 搜索邮件:") + + def search_emails(server, criteria='ALL'): + """搜索邮件""" + if not server: + print(" 服务器连接无效") + return [] + + try: + print(f" 搜索条件: {criteria}") + status, messages = server.search(None, criteria) + + if status == 'OK': + # 获取邮件ID列表 + email_ids = messages[0].split() + print(f" 找到 {len(email_ids)} 封邮件") + + # 显示邮件ID + if email_ids: + print(" 邮件ID列表:") + for i, email_id in enumerate(email_ids[:10], 1): # 只显示前10个 + print(f" {i}. {email_id.decode('utf-8')}") + + if len(email_ids) > 10: + print(f" ... 还有 {len(email_ids) - 10} 封邮件") + + return [id.decode('utf-8') for id in email_ids] + else: + print(f" 搜索失败: {status}") + return [] + + except Exception as e: + print(f" 搜索邮件失败: {e}") + return [] + + def advanced_search_examples(server): + """高级搜索示例""" + print(" 高级搜索示例:") + + search_examples = [ + ('ALL', '所有邮件'), + ('UNSEEN', '未读邮件'), + ('FROM "example@gmail.com"', '来自特定发件人'), + ('SUBJECT "Python"', '主题包含Python'), + ('SINCE "01-Jun-2023"', '2023年6月1日之后的邮件'), + ('BEFORE "01-Jul-2023"', '2023年7月1日之前的邮件'), + ('LARGER 1000000', '大于1MB的邮件'), + ('SMALLER 1000', '小于1KB的邮件') + ] + + for criteria, description in search_examples: + print(f" {criteria} - {description}") + # 实际搜索时取消注释 + # email_ids = search_emails(server, criteria) + + # 4. 获取邮件内容 + print("\n4. 获取邮件内容:") + + def fetch_email(server, email_id): + """获取邮件内容""" + if not server: + print(" 服务器连接无效") + return None + + try: + print(f" 正在获取邮件 {email_id}...") + + # 获取邮件 + status, msg_data = server.fetch(email_id, '(RFC822)') + + if status == 'OK': + # 解析邮件 + raw_email = msg_data[0][1] + email_message = email.message_from_bytes(raw_email) + + # 提取邮件信息 + email_info = { + 'id': email_id, + 'subject': decode_email_header(email_message.get('Subject', '')), + 'from': decode_email_header(email_message.get('From', '')), + 'to': decode_email_header(email_message.get('To', '')), + 'date': email_message.get('Date', ''), + 'message_id': email_message.get('Message-ID', '') + } + + print(f" ✓ 邮件获取成功") + print(f" 主题: {email_info['subject']}") + print(f" 发件人: {email_info['from']}") + print(f" 日期: {email_info['date']}") + + # 提取邮件正文和附件 + body, attachments = extract_email_content(email_message) + + if body: + print(f" 正文预览: {body[:100]}...") + + if attachments: + print(f" 附件数量: {len(attachments)}") + for i, att in enumerate(attachments, 1): + print(f" {i}. {att['filename']} ({att['size']} 字节)") + + return email_info, email_message + else: + print(f" 获取邮件失败: {status}") + return None, None + + except Exception as e: + print(f" 获取邮件失败: {e}") + return None, None + + def extract_email_content(email_message): + """提取邮件内容和附件""" + body = '' + attachments = [] + + try: + if email_message.is_multipart(): + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get('Content-Disposition', '')) + + if 'attachment' in content_disposition: + # 处理附件 + filename = part.get_filename() + if filename: + filename = decode_email_header(filename) + payload = part.get_payload(decode=True) + attachments.append({ + 'filename': filename, + 'content_type': content_type, + 'size': len(payload) if payload else 0, + 'data': payload + }) + elif content_type == 'text/plain' and not body: + # 提取纯文本正文 + charset = part.get_content_charset() or 'utf-8' + payload = part.get_payload(decode=True) + if payload: + body = payload.decode(charset, errors='ignore') + elif content_type == 'text/html' and not body: + # 提取HTML正文 + charset = part.get_content_charset() or 'utf-8' + payload = part.get_payload(decode=True) + if payload: + body = payload.decode(charset, errors='ignore') + else: + # 单部分邮件 + charset = email_message.get_content_charset() or 'utf-8' + payload = email_message.get_payload(decode=True) + if payload: + body = payload.decode(charset, errors='ignore') + + return body, attachments + except Exception as e: + print(f" 提取邮件内容失败: {e}") + return '', [] + + # 5. 邮件标记操作 + print("\n5. 邮件标记操作:") + + def mark_email_as_read(server, email_id): + """标记邮件为已读""" + try: + server.store(email_id, '+FLAGS', '\\Seen') + print(f" ✓ 邮件 {email_id} 已标记为已读") + except Exception as e: + print(f" 标记邮件失败: {e}") + + def mark_email_as_unread(server, email_id): + """标记邮件为未读""" + try: + server.store(email_id, '-FLAGS', '\\Seen') + print(f" ✓ 邮件 {email_id} 已标记为未读") + except Exception as e: + print(f" 标记邮件失败: {e}") + + def delete_email_imap(server, email_id): + """删除邮件(IMAP)""" + try: + # 标记为删除 + server.store(email_id, '+FLAGS', '\\Deleted') + # 执行删除 + server.expunge() + print(f" ✓ 邮件 {email_id} 已删除") + except Exception as e: + print(f" 删除邮件失败: {e}") + + # 模拟IMAP操作 + print(" 模拟IMAP邮件接收流程:") + print(" 注意:需要配置真实的IMAP服务器信息") + + # 模拟操作流程 + operations = [ + "1. 连接IMAP服务器", + "2. 登录账户", + "3. 列出邮箱文件夹", + "4. 选择INBOX文件夹", + "5. 搜索未读邮件", + "6. 获取邮件内容", + "7. 处理附件", + "8. 标记邮件状态", + "9. 关闭连接" + ] + + print(" IMAP操作流程:") + for op in operations: + print(f" {op}") + + # server = connect_imap_server() # 需要真实配置 + # if server: + # list_mailboxes(server) + # if select_mailbox(server, 'INBOX'): + # email_ids = search_emails(server, 'UNSEEN') + # if email_ids: + # fetch_email(server, email_ids[0]) + # server.close() + # server.logout() + + advanced_search_examples(None) + +# 运行IMAP演示 +imap_receive_demo() +``` + +### 3.3 邮件内容解析 + +```python +import re +import html2text +from bs4 import BeautifulSoup +import chardet + +def email_parsing_demo(): + """邮件内容解析演示""" + print("=== 邮件内容解析演示 ===") + + # 1. HTML邮件解析 + print("\n1. HTML邮件解析:") + + def parse_html_email(html_content): + """解析HTML邮件""" + print(" 解析HTML邮件内容...") + + try: + # 使用BeautifulSoup解析HTML + soup = BeautifulSoup(html_content, 'html.parser') + + # 提取文本内容 + text_content = soup.get_text() + + # 清理文本 + text_content = re.sub(r'\n\s*\n', '\n\n', text_content) + text_content = text_content.strip() + + # 提取链接 + links = [] + for link in soup.find_all('a', href=True): + links.append({ + 'text': link.get_text().strip(), + 'url': link['href'] + }) + + # 提取图片 + images = [] + for img in soup.find_all('img', src=True): + images.append({ + 'alt': img.get('alt', ''), + 'src': img['src'] + }) + + print(f" ✓ HTML解析完成") + print(f" 文本长度: {len(text_content)} 字符") + print(f" 链接数量: {len(links)}") + print(f" 图片数量: {len(images)}") + + if links: + print(" 链接列表:") + for i, link in enumerate(links[:3], 1): + print(f" {i}. {link['text']} -> {link['url']}") + + return { + 'text': text_content, + 'links': links, + 'images': images + } + + except Exception as e: + print(f" HTML解析失败: {e}") + return None + + def html_to_text(html_content): + """HTML转纯文本""" + try: + # 使用html2text转换 + h = html2text.HTML2Text() + h.ignore_links = False + h.ignore_images = False + h.body_width = 0 # 不限制行宽 + + text = h.handle(html_content) + return text + except Exception as e: + print(f" HTML转文本失败: {e}") + return html_content + + # 2. 邮件编码处理 + print("\n2. 邮件编码处理:") + + def detect_encoding(data): + """检测文本编码""" + if isinstance(data, str): + return 'utf-8' + + try: + result = chardet.detect(data) + encoding = result['encoding'] + confidence = result['confidence'] + + print(f" 检测到编码: {encoding} (置信度: {confidence:.2f})") + return encoding + except Exception as e: + print(f" 编码检测失败: {e}") + return 'utf-8' + + def decode_content(data, encoding=None): + """解码内容""" + if isinstance(data, str): + return data + + if not encoding: + encoding = detect_encoding(data) + + try: + return data.decode(encoding, errors='ignore') + except Exception as e: + print(f" 解码失败: {e}") + return data.decode('utf-8', errors='ignore') + + # 3. 邮件地址解析 + print("\n3. 邮件地址解析:") + + def parse_email_addresses(address_string): + """解析邮件地址""" + print(f" 解析地址字符串: {address_string}") + + addresses = [] + + try: + # 分割多个地址 + addr_list = re.split(r'[,;]', address_string) + + for addr in addr_list: + addr = addr.strip() + if not addr: + continue + + # 解析地址格式:"Name" 或 email@domain.com + match = re.match(r'^"?([^"<>]+?)"?\s*<([^<>]+)>$', addr) + if match: + name = match.group(1).strip() + email = match.group(2).strip() + else: + # 简单邮件地址 + email_match = re.search(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', addr) + if email_match: + email = email_match.group(0) + name = addr.replace(email, '').strip('<> "') + else: + continue + + addresses.append({ + 'name': name if name else '', + 'email': email + }) + + print(f" 解析出 {len(addresses)} 个地址:") + for i, addr in enumerate(addresses, 1): + if addr['name']: + print(f" {i}. {addr['name']} <{addr['email']}>") + else: + print(f" {i}. {addr['email']}") + + return addresses + + except Exception as e: + print(f" 地址解析失败: {e}") + return [] + + # 4. 邮件内容提取 + print("\n4. 邮件内容提取:") + + def extract_email_metadata(email_message): + """提取邮件元数据""" + metadata = {} + + try: + # 基本信息 + metadata['subject'] = decode_email_header(email_message.get('Subject', '')) + metadata['from'] = decode_email_header(email_message.get('From', '')) + metadata['to'] = decode_email_header(email_message.get('To', '')) + metadata['cc'] = decode_email_header(email_message.get('Cc', '')) + metadata['bcc'] = decode_email_header(email_message.get('Bcc', '')) + metadata['date'] = email_message.get('Date', '') + metadata['message_id'] = email_message.get('Message-ID', '') + + # 技术信息 + metadata['content_type'] = email_message.get_content_type() + metadata['charset'] = email_message.get_content_charset() + metadata['transfer_encoding'] = email_message.get('Content-Transfer-Encoding', '') + + # 邮件客户端信息 + metadata['user_agent'] = email_message.get('User-Agent', '') + metadata['x_mailer'] = email_message.get('X-Mailer', '') + + # 路由信息 + received_headers = email_message.get_all('Received', []) + metadata['received_count'] = len(received_headers) + + print(" 邮件元数据:") + for key, value in metadata.items(): + if value: + print(f" {key}: {value}") + + return metadata + + except Exception as e: + print(f" 元数据提取失败: {e}") + return {} + + def extract_urls_from_text(text): + """从文本中提取URL""" + url_pattern = r'https?://[^\s<>"]+|www\.[^\s<>"]+' + urls = re.findall(url_pattern, text, re.IGNORECASE) + + print(f" 从文本中提取到 {len(urls)} 个URL:") + for i, url in enumerate(urls[:5], 1): + print(f" {i}. {url}") + + return urls + + def extract_phone_numbers(text): + """从文本中提取电话号码""" + phone_patterns = [ + r'\b\d{3}-\d{3}-\d{4}\b', # 123-456-7890 + r'\b\d{3}\.\d{3}\.\d{4}\b', # 123.456.7890 + r'\b\(\d{3}\)\s*\d{3}-\d{4}\b', # (123) 456-7890 + r'\b\d{11}\b', # 12345678901 + r'\b\d{3}\s+\d{4}\s+\d{4}\b' # 123 4567 8901 + ] + + phones = [] + for pattern in phone_patterns: + phones.extend(re.findall(pattern, text)) + + print(f" 从文本中提取到 {len(phones)} 个电话号码:") + for i, phone in enumerate(phones[:3], 1): + print(f" {i}. {phone}") + + return phones + + # 测试解析功能 + print(" 测试邮件内容解析:") + + # 测试HTML内容 + sample_html = """ + + +

Python学习通知

+

亲爱的学员,

+

我们的Python课程已经更新。

+

请访问 课程页面 查看详情。

+ Logo +

联系电话:123-456-7890

+

网站:www.example.com

+ + + """ + + parsed_html = parse_html_email(sample_html) + if parsed_html: + extract_urls_from_text(parsed_html['text']) + extract_phone_numbers(parsed_html['text']) + + # 测试地址解析 + test_addresses = [ + '"张三" ', + 'lisi@example.com', + '"王五" , "赵六" ' + ] + + for addr in test_addresses: + parse_email_addresses(addr) + +# 运行邮件解析演示 +email_parsing_demo() +``` + +## 4. 邮件处理和管理 + +### 4.1 邮件过滤和分类 + +```python +import re +from datetime import datetime, timedelta +import json + +def email_filtering_demo(): + """邮件过滤和分类演示""" + print("=== 邮件过滤和分类演示 ===") + + # 1. 邮件过滤器 + print("\n1. 邮件过滤器:") + + class EmailFilter: + """邮件过滤器类""" + + def __init__(self): + self.rules = [] + + def add_rule(self, name, condition, action): + """添加过滤规则""" + rule = { + 'name': name, + 'condition': condition, + 'action': action, + 'created': datetime.now().isoformat() + } + self.rules.append(rule) + print(f" ✓ 添加规则: {name}") + + def apply_filters(self, email_info): + """应用过滤规则""" + applied_rules = [] + + for rule in self.rules: + if self._check_condition(email_info, rule['condition']): + result = self._execute_action(email_info, rule['action']) + applied_rules.append({ + 'rule_name': rule['name'], + 'action_result': result + }) + + return applied_rules + + def _check_condition(self, email_info, condition): + """检查条件""" + try: + condition_type = condition['type'] + + if condition_type == 'sender': + pattern = condition['pattern'] + return re.search(pattern, email_info.get('from', ''), re.IGNORECASE) + + elif condition_type == 'subject': + pattern = condition['pattern'] + return re.search(pattern, email_info.get('subject', ''), re.IGNORECASE) + + elif condition_type == 'body': + pattern = condition['pattern'] + return re.search(pattern, email_info.get('body', ''), re.IGNORECASE) + + elif condition_type == 'size': + operator = condition['operator'] # '>', '<', '==' + threshold = condition['value'] + size = email_info.get('size', 0) + + if operator == '>': + return size > threshold + elif operator == '<': + return size < threshold + elif operator == '==': + return size == threshold + + elif condition_type == 'date': + operator = condition['operator'] + days_ago = condition['days'] + threshold_date = datetime.now() - timedelta(days=days_ago) + + email_date = datetime.fromisoformat(email_info.get('date', datetime.now().isoformat())) + + if operator == 'older': + return email_date < threshold_date + elif operator == 'newer': + return email_date > threshold_date + + return False + + except Exception as e: + print(f" 条件检查失败: {e}") + return False + + def _execute_action(self, email_info, action): + """执行动作""" + try: + action_type = action['type'] + + if action_type == 'move_to_folder': + folder = action['folder'] + print(f" → 移动到文件夹: {folder}") + return f"moved_to_{folder}" + + elif action_type == 'mark_as_read': + print(f" → 标记为已读") + return "marked_as_read" + + elif action_type == 'mark_as_important': + print(f" → 标记为重要") + return "marked_as_important" + + elif action_type == 'delete': + print(f" → 删除邮件") + return "deleted" + + elif action_type == 'forward': + to_address = action['to'] + print(f" → 转发到: {to_address}") + return f"forwarded_to_{to_address}" + + elif action_type == 'add_label': + label = action['label'] + print(f" → 添加标签: {label}") + return f"labeled_{label}" + + return "action_executed" + + except Exception as e: + print(f" 动作执行失败: {e}") + return "action_failed" + + # 2. 创建过滤规则 + print("\n2. 创建过滤规则:") + + def create_filter_rules(): + """创建过滤规则""" + email_filter = EmailFilter() + + # 垃圾邮件过滤 + email_filter.add_rule( + "垃圾邮件过滤", + { + 'type': 'subject', + 'pattern': r'(广告|促销|优惠|免费|中奖)' + }, + { + 'type': 'move_to_folder', + 'folder': 'spam' + } + ) + + # 工作邮件分类 + email_filter.add_rule( + "工作邮件", + { + 'type': 'sender', + 'pattern': r'@company\.com$' + }, + { + 'type': 'move_to_folder', + 'folder': 'work' + } + ) + + # 重要邮件标记 + email_filter.add_rule( + "重要邮件", + { + 'type': 'subject', + 'pattern': r'(紧急|重要|urgent|important)' + }, + { + 'type': 'mark_as_important' + } + ) + + # 大附件邮件 + email_filter.add_rule( + "大附件邮件", + { + 'type': 'size', + 'operator': '>', + 'value': 5 * 1024 * 1024 # 5MB + }, + { + 'type': 'add_label', + 'label': 'large_attachment' + } + ) + + # 旧邮件清理 + email_filter.add_rule( + "旧邮件清理", + { + 'type': 'date', + 'operator': 'older', + 'days': 365 + }, + { + 'type': 'move_to_folder', + 'folder': 'archive' + } + ) + + return email_filter + + # 3. 应用过滤规则 + print("\n3. 应用过滤规则:") + + def test_email_filtering(): + """测试邮件过滤""" + email_filter = create_filter_rules() + + # 测试邮件数据 + test_emails = [ + { + 'subject': '紧急:系统维护通知', + 'from': 'admin@company.com', + 'body': '系统将于今晚进行维护', + 'size': 1024, + 'date': datetime.now().isoformat() + }, + { + 'subject': '免费获得iPhone!点击领取', + 'from': 'promo@spam.com', + 'body': '恭喜您中奖了!', + 'size': 512, + 'date': datetime.now().isoformat() + }, + { + 'subject': '会议资料', + 'from': 'colleague@company.com', + 'body': '附件是明天会议的资料', + 'size': 8 * 1024 * 1024, # 8MB + 'date': datetime.now().isoformat() + }, + { + 'subject': '旧项目文档', + 'from': 'archive@oldcompany.com', + 'body': '这是去年的项目文档', + 'size': 2048, + 'date': (datetime.now() - timedelta(days=400)).isoformat() + } + ] + + print(" 测试邮件过滤:") + for i, email in enumerate(test_emails, 1): + print(f"\n 邮件 {i}: {email['subject']}") + print(f" 发件人: {email['from']}") + + applied_rules = email_filter.apply_filters(email) + + if applied_rules: + print(f" 应用的规则:") + for rule in applied_rules: + print(f" • {rule['rule_name']}: {rule['action_result']}") + else: + print(f" → 无匹配规则,保持在收件箱") + + test_email_filtering() + +# 运行邮件过滤演示 +email_filtering_demo() +``` + +### 4.2 邮件自动回复 + +```python +import json +from datetime import datetime +import re + +def auto_reply_demo(): + """邮件自动回复演示""" + print("=== 邮件自动回复演示 ===") + + # 1. 自动回复系统 + print("\n1. 自动回复系统:") + + class AutoReplySystem: + """自动回复系统""" + + def __init__(self): + self.templates = {} + self.rules = [] + self.enabled = True + + def add_template(self, name, subject, body, variables=None): + """添加回复模板""" + template = { + 'name': name, + 'subject': subject, + 'body': body, + 'variables': variables or [], + 'created': datetime.now().isoformat() + } + self.templates[name] = template + print(f" ✓ 添加模板: {name}") + + def add_rule(self, name, condition, template_name, delay_hours=0): + """添加自动回复规则""" + rule = { + 'name': name, + 'condition': condition, + 'template': template_name, + 'delay_hours': delay_hours, + 'enabled': True + } + self.rules.append(rule) + print(f" ✓ 添加规则: {name}") + + def process_email(self, email_info): + """处理邮件并生成自动回复""" + if not self.enabled: + return None + + for rule in self.rules: + if not rule['enabled']: + continue + + if self._check_condition(email_info, rule['condition']): + template_name = rule['template'] + if template_name in self.templates: + reply = self._generate_reply(email_info, template_name) + reply['delay_hours'] = rule['delay_hours'] + reply['rule_name'] = rule['name'] + return reply + + return None + + def _check_condition(self, email_info, condition): + """检查触发条件""" + try: + condition_type = condition['type'] + + if condition_type == 'keyword': + keywords = condition['keywords'] + text = f"{email_info.get('subject', '')} {email_info.get('body', '')}" + return any(keyword.lower() in text.lower() for keyword in keywords) + + elif condition_type == 'sender_domain': + domain = condition['domain'] + sender = email_info.get('from', '') + return domain in sender + + elif condition_type == 'time_range': + start_hour = condition['start_hour'] + end_hour = condition['end_hour'] + current_hour = datetime.now().hour + + if start_hour <= end_hour: + return start_hour <= current_hour <= end_hour + else: # 跨午夜 + return current_hour >= start_hour or current_hour <= end_hour + + elif condition_type == 'first_contact': + # 简化实现:假设检查是否为首次联系 + return email_info.get('is_first_contact', False) + + return False + + except Exception as e: + print(f" 条件检查失败: {e}") + return False + + def _generate_reply(self, email_info, template_name): + """生成回复邮件""" + template = self.templates[template_name] + + # 提取变量 + variables = { + 'sender_name': self._extract_sender_name(email_info.get('from', '')), + 'original_subject': email_info.get('subject', ''), + 'current_date': datetime.now().strftime('%Y年%m月%d日'), + 'current_time': datetime.now().strftime('%H:%M') + } + + # 替换模板变量 + subject = self._replace_variables(template['subject'], variables) + body = self._replace_variables(template['body'], variables) + + reply = { + 'to': email_info.get('from', ''), + 'subject': subject, + 'body': body, + 'template_used': template_name, + 'generated_at': datetime.now().isoformat() + } + + return reply + + def _extract_sender_name(self, from_address): + """提取发件人姓名""" + # 简单提取:"Name" -> Name + match = re.match(r'^"?([^"<>]+?)"?\s*<', from_address) + if match: + return match.group(1).strip() + else: + # 从邮箱地址提取用户名 + email_match = re.search(r'([^@<>\s]+)@', from_address) + if email_match: + return email_match.group(1) + return '朋友' + + def _replace_variables(self, text, variables): + """替换模板变量""" + for var_name, var_value in variables.items(): + text = text.replace(f'{{{var_name}}}', str(var_value)) + return text + + # 2. 创建回复模板 + print("\n2. 创建回复模板:") + + def create_reply_templates(): + """创建回复模板""" + auto_reply = AutoReplySystem() + + # 外出自动回复 + auto_reply.add_template( + "外出回复", + "自动回复:Re: {original_subject}", + """ +亲爱的 {sender_name}, + +感谢您的邮件。我目前外出办公,将于下周一返回。 + +如有紧急事务,请联系我的同事: +- 张三:zhangsan@company.com +- 李四:lisi@company.com + +我会在返回后尽快回复您的邮件。 + +此邮件为自动回复,请勿直接回复。 + +谢谢! +{current_date} + """ + ) + + # 客服自动回复 + auto_reply.add_template( + "客服回复", + "收到您的咨询:{original_subject}", + """ +尊敬的 {sender_name}, + +感谢您联系我们的客服团队! + +我们已收到您于 {current_date} {current_time} 发送的邮件,我们的客服代表将在24小时内回复您。 + +如需紧急帮助,请拨打客服热线:400-123-4567 + +常见问题解答:https://example.com/faq + +谢谢您的耐心等待! + +客服团队 + """ + ) + + # 招聘自动回复 + auto_reply.add_template( + "招聘回复", + "感谢您的求职申请", + """ +亲爱的 {sender_name}, + +感谢您对我们公司的关注和求职申请! + +我们已收到您的简历,人力资源部门将在5个工作日内审核您的申请。 + +如果您的背景符合我们的要求,我们会尽快与您联系安排面试。 + +在此期间,您可以访问我们的官网了解更多公司信息: +https://company.com/about + +再次感谢您的申请! + +人力资源部 +{current_date} + """ + ) + + # 技术支持回复 + auto_reply.add_template( + "技术支持回复", + "技术支持工单已创建:{original_subject}", + """ +您好 {sender_name}, + +感谢您联系技术支持! + +我们已为您创建了支持工单,工单编号:TS-{current_date}-001 + +我们的技术工程师将在2个工作日内分析您的问题并提供解决方案。 + +您可以通过以下方式跟踪工单状态: +- 访问:https://support.company.com +- 电话:400-888-9999 + +常见问题可能的解决方案: +1. 重启应用程序 +2. 清除浏览器缓存 +3. 检查网络连接 + +技术支持团队 + """ + ) + + return auto_reply + + # 3. 设置自动回复规则 + print("\n3. 设置自动回复规则:") + + def setup_reply_rules(): + """设置自动回复规则""" + auto_reply = create_reply_templates() + + # 外出时间自动回复 + auto_reply.add_rule( + "外出自动回复", + { + 'type': 'time_range', + 'start_hour': 18, # 下午6点后 + 'end_hour': 8 # 上午8点前 + }, + "外出回复" + ) + + # 客服邮箱自动回复 + auto_reply.add_rule( + "客服自动回复", + { + 'type': 'sender_domain', + 'domain': 'customer' + }, + "客服回复", + delay_hours=0 + ) + + # 招聘相关自动回复 + auto_reply.add_rule( + "招聘自动回复", + { + 'type': 'keyword', + 'keywords': ['求职', '应聘', '简历', '招聘', 'job', 'resume'] + }, + "招聘回复", + delay_hours=1 + ) + + # 技术支持自动回复 + auto_reply.add_rule( + "技术支持自动回复", + { + 'type': 'keyword', + 'keywords': ['bug', '错误', '问题', '故障', 'error', 'help'] + }, + "技术支持回复" + ) + + return auto_reply + + # 4. 测试自动回复 + print("\n4. 测试自动回复:") + + def test_auto_reply(): + """测试自动回复功能""" + auto_reply = setup_reply_rules() + + # 测试邮件 + test_emails = [ + { + 'from': '"张三" ', + 'subject': '产品咨询', + 'body': '我想了解你们的产品功能', + 'is_first_contact': True + }, + { + 'from': 'lisi@jobseeker.com', + 'subject': '求职申请 - Python开发工程师', + 'body': '附件是我的简历,希望能加入贵公司', + 'is_first_contact': True + }, + { + 'from': 'user@example.com', + 'subject': '系统bug报告', + 'body': '登录时出现error 500错误', + 'is_first_contact': False + }, + { + 'from': 'friend@personal.com', + 'subject': '周末聚会', + 'body': '这周末有空吗?一起吃饭', + 'is_first_contact': False + } + ] + + print(" 测试自动回复:") + for i, email in enumerate(test_emails, 1): + print(f"\n 邮件 {i}: {email['subject']}") + print(f" 发件人: {email['from']}") + + reply = auto_reply.process_email(email) + + if reply: + print(f" ✓ 生成自动回复:") + print(f" 规则: {reply['rule_name']}") + print(f" 模板: {reply['template_used']}") + print(f" 延迟: {reply['delay_hours']} 小时") + print(f" 主题: {reply['subject']}") + print(f" 正文预览: {reply['body'][:100]}...") + else: + print(f" → 无匹配规则,不发送自动回复") + + test_auto_reply() + +# 运行自动回复演示 +auto_reply_demo() +``` + +### 4.3 邮件统计和分析 + +```python +import json +from collections import defaultdict, Counter +from datetime import datetime, timedelta +import matplotlib.pyplot as plt + +def email_analytics_demo(): + """邮件统计和分析演示""" + print("=== 邮件统计和分析演示 ===") + + # 1. 邮件统计类 + print("\n1. 邮件统计分析:") + + class EmailAnalytics: + """邮件分析类""" + + def __init__(self): + self.emails = [] + self.stats = {} + + def add_email(self, email_info): + """添加邮件数据""" + self.emails.append(email_info) + + def analyze_volume(self): + """分析邮件量""" + print(" 邮件量分析:") + + total_emails = len(self.emails) + print(f" 总邮件数: {total_emails}") + + if total_emails == 0: + return + + # 按日期统计 + daily_count = defaultdict(int) + for email in self.emails: + date = email.get('date', '').split('T')[0] # 提取日期部分 + daily_count[date] += 1 + + print(f" 活跃天数: {len(daily_count)}") + if daily_count: + avg_daily = total_emails / len(daily_count) + print(f" 日均邮件: {avg_daily:.1f}") + + max_day = max(daily_count.items(), key=lambda x: x[1]) + print(f" 最忙的一天: {max_day[0]} ({max_day[1]} 封)") + + # 按小时统计 + hourly_count = defaultdict(int) + for email in self.emails: + try: + dt = datetime.fromisoformat(email.get('date', '')) + hour = dt.hour + hourly_count[hour] += 1 + except: + continue + + if hourly_count: + peak_hour = max(hourly_count.items(), key=lambda x: x[1]) + print(f" 邮件高峰时段: {peak_hour[0]}:00 ({peak_hour[1]} 封)") + + def analyze_senders(self): + """分析发件人""" + print("\n 发件人分析:") + + sender_count = Counter() + domain_count = Counter() + + for email in self.emails: + sender = email.get('from', '') + sender_count[sender] += 1 + + # 提取域名 + if '@' in sender: + domain = sender.split('@')[-1].split('>')[0] + domain_count[domain] += 1 + + print(f" 独立发件人数: {len(sender_count)}") + + # 最活跃发件人 + if sender_count: + top_senders = sender_count.most_common(5) + print(" 最活跃发件人:") + for i, (sender, count) in enumerate(top_senders, 1): + print(f" {i}. {sender}: {count} 封") + + # 最常见域名 + if domain_count: + top_domains = domain_count.most_common(5) + print(" 最常见域名:") + for i, (domain, count) in enumerate(top_domains, 1): + print(f" {i}. {domain}: {count} 封") + + def analyze_subjects(self): + """分析主题""" + print("\n 主题分析:") + + # 主题关键词统计 + keyword_count = Counter() + subject_lengths = [] + + for email in self.emails: + subject = email.get('subject', '') + subject_lengths.append(len(subject)) + + # 提取关键词(简单分词) + words = re.findall(r'\b\w{2,}\b', subject.lower()) + for word in words: + if len(word) > 2: # 过滤短词 + keyword_count[word] += 1 + + if subject_lengths: + avg_length = sum(subject_lengths) / len(subject_lengths) + print(f" 平均主题长度: {avg_length:.1f} 字符") + print(f" 最长主题: {max(subject_lengths)} 字符") + print(f" 最短主题: {min(subject_lengths)} 字符") + + # 热门关键词 + if keyword_count: + top_keywords = keyword_count.most_common(10) + print(" 热门关键词:") + for i, (keyword, count) in enumerate(top_keywords, 1): + print(f" {i}. {keyword}: {count} 次") + + def analyze_sizes(self): + """分析邮件大小""" + print("\n 邮件大小分析:") + + sizes = [email.get('size', 0) for email in self.emails] + sizes = [s for s in sizes if s > 0] # 过滤无效大小 + + if not sizes: + print(" 无大小数据") + return + + total_size = sum(sizes) + avg_size = total_size / len(sizes) + + print(f" 总大小: {self._format_size(total_size)}") + print(f" 平均大小: {self._format_size(avg_size)}") + print(f" 最大邮件: {self._format_size(max(sizes))}") + print(f" 最小邮件: {self._format_size(min(sizes))}") + + # 大小分布 + size_ranges = { + '< 1KB': 0, + '1KB - 10KB': 0, + '10KB - 100KB': 0, + '100KB - 1MB': 0, + '> 1MB': 0 + } + + for size in sizes: + if size < 1024: + size_ranges['< 1KB'] += 1 + elif size < 10 * 1024: + size_ranges['1KB - 10KB'] += 1 + elif size < 100 * 1024: + size_ranges['10KB - 100KB'] += 1 + elif size < 1024 * 1024: + size_ranges['100KB - 1MB'] += 1 + else: + size_ranges['> 1MB'] += 1 + + print(" 大小分布:") + for range_name, count in size_ranges.items(): + percentage = (count / len(sizes)) * 100 + print(f" {range_name}: {count} 封 ({percentage:.1f}%)") + + def _format_size(self, size_bytes): + """格式化文件大小""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.1f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.1f} TB" + + def generate_report(self): + """生成分析报告""" + print("\n 生成邮件分析报告:") + + report = { + 'generated_at': datetime.now().isoformat(), + 'total_emails': len(self.emails), + 'analysis_period': self._get_date_range(), + 'summary': self._generate_summary() + } + + print(f" 报告生成时间: {report['generated_at']}") + print(f" 分析期间: {report['analysis_period']}") + print(f" 邮件总数: {report['total_emails']}") + + return report + + def _get_date_range(self): + """获取日期范围""" + if not self.emails: + return "无数据" + + dates = [] + for email in self.emails: + try: + dt = datetime.fromisoformat(email.get('date', '')) + dates.append(dt) + except: + continue + + if dates: + start_date = min(dates).strftime('%Y-%m-%d') + end_date = max(dates).strftime('%Y-%m-%d') + return f"{start_date} 至 {end_date}" + + return "日期解析失败" + + def _generate_summary(self): + """生成摘要""" + if not self.emails: + return "无邮件数据" + + # 计算基本统计 + total = len(self.emails) + + # 发件人统计 + senders = set(email.get('from', '') for email in self.emails) + unique_senders = len(senders) + + # 大小统计 + sizes = [email.get('size', 0) for email in self.emails if email.get('size', 0) > 0] + avg_size = sum(sizes) / len(sizes) if sizes else 0 + + summary = f"共分析 {total} 封邮件,来自 {unique_senders} 个不同发件人,平均大小 {self._format_size(avg_size)}" + + return summary + + # 2. 生成测试数据 + print("\n2. 生成测试数据:") + + def generate_test_emails(): + """生成测试邮件数据""" + import random + + senders = [ + 'boss@company.com', + 'colleague@company.com', + 'client@customer.com', + 'support@vendor.com', + 'newsletter@news.com', + 'spam@promo.com' + ] + + subjects = [ + '会议通知', + '项目进度更新', + '客户反馈', + '系统维护通知', + '新闻简报', + '促销活动', + '技术支持', + '发票确认', + '培训邀请', + '重要通知' + ] + + emails = [] + base_date = datetime.now() - timedelta(days=30) + + for i in range(100): + # 随机日期(最近30天) + random_days = random.randint(0, 30) + random_hours = random.randint(0, 23) + email_date = base_date + timedelta(days=random_days, hours=random_hours) + + email = { + 'from': random.choice(senders), + 'subject': random.choice(subjects), + 'date': email_date.isoformat(), + 'size': random.randint(500, 50000), # 500B - 50KB + 'body': f"这是第{i+1}封测试邮件的内容" + } + + emails.append(email) + + print(f" 生成了 {len(emails)} 封测试邮件") + return emails + + # 3. 执行分析 + print("\n3. 执行邮件分析:") + + def run_analysis(): + """运行邮件分析""" + # 创建分析器 + analytics = EmailAnalytics() + + # 添加测试数据 + test_emails = generate_test_emails() + for email in test_emails: + analytics.add_email(email) + + # 执行各项分析 + analytics.analyze_volume() + analytics.analyze_senders() + analytics.analyze_subjects() + analytics.analyze_sizes() + + # 生成报告 + report = analytics.generate_report() + + return analytics, report + + analytics, report = run_analysis() + + print(f"\n 分析完成!") + print(f" 报告摘要: {report['summary']}") + +# 运行邮件分析演示 +email_analytics_demo() +``` + +## 5. 实际应用案例 + +### 5.1 邮件备份系统 + +```python +import os +import json +import sqlite3 +from datetime import datetime +import hashlib + +def email_backup_demo(): + """邮件备份系统演示""" + print("=== 邮件备份系统演示 ===") + + # 1. 邮件备份类 + print("\n1. 邮件备份系统:") + + class EmailBackupSystem: + """邮件备份系统""" + + def __init__(self, backup_dir="email_backup"): + self.backup_dir = backup_dir + self.db_path = os.path.join(backup_dir, "emails.db") + self._init_backup_dir() + self._init_database() + + def _init_backup_dir(self): + """初始化备份目录""" + if not os.path.exists(self.backup_dir): + os.makedirs(self.backup_dir) + print(f" ✓ 创建备份目录: {self.backup_dir}") + + # 创建子目录 + subdirs = ['attachments', 'exports', 'logs'] + for subdir in subdirs: + subdir_path = os.path.join(self.backup_dir, subdir) + if not os.path.exists(subdir_path): + os.makedirs(subdir_path) + + def _init_database(self): + """初始化数据库""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建邮件表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS emails ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id TEXT UNIQUE, + sender TEXT, + recipients TEXT, + subject TEXT, + date_sent TEXT, + date_received TEXT, + body_text TEXT, + body_html TEXT, + attachments TEXT, + size INTEGER, + hash TEXT, + backup_date TEXT + ) + ''') + + # 创建附件表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email_id INTEGER, + filename TEXT, + content_type TEXT, + size INTEGER, + file_path TEXT, + hash TEXT, + FOREIGN KEY (email_id) REFERENCES emails (id) + ) + ''') + + conn.commit() + conn.close() + print(f" ✓ 初始化数据库: {self.db_path}") + + def backup_email(self, email_data): + """备份单封邮件""" + try: + # 计算邮件哈希 + email_hash = self._calculate_email_hash(email_data) + + # 检查是否已备份 + if self._is_email_backed_up(email_hash): + print(f" 邮件已存在,跳过: {email_data.get('subject', 'No Subject')[:50]}") + return False + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 插入邮件记录 + cursor.execute(''' + INSERT INTO emails ( + message_id, sender, recipients, subject, date_sent, + date_received, body_text, body_html, attachments, + size, hash, backup_date + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + email_data.get('message_id', ''), + email_data.get('from', ''), + json.dumps(email_data.get('to', [])), + email_data.get('subject', ''), + email_data.get('date', ''), + datetime.now().isoformat(), + email_data.get('body_text', ''), + email_data.get('body_html', ''), + json.dumps(email_data.get('attachments', [])), + email_data.get('size', 0), + email_hash, + datetime.now().isoformat() + )) + + email_id = cursor.lastrowid + + # 备份附件 + if 'attachments' in email_data: + for attachment in email_data['attachments']: + self._backup_attachment(cursor, email_id, attachment) + + conn.commit() + conn.close() + + print(f" ✓ 备份邮件: {email_data.get('subject', 'No Subject')[:50]}") + return True + + except Exception as e: + print(f" ✗ 备份失败: {e}") + return False + + def _calculate_email_hash(self, email_data): + """计算邮件哈希值""" + # 使用关键字段计算哈希 + key_fields = [ + email_data.get('message_id', ''), + email_data.get('from', ''), + email_data.get('subject', ''), + email_data.get('date', ''), + email_data.get('body_text', '') + ] + + content = '|'.join(key_fields) + return hashlib.md5(content.encode('utf-8')).hexdigest() + + def _is_email_backed_up(self, email_hash): + """检查邮件是否已备份""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('SELECT id FROM emails WHERE hash = ?', (email_hash,)) + result = cursor.fetchone() + + conn.close() + return result is not None + + def _backup_attachment(self, cursor, email_id, attachment): + """备份附件""" + try: + filename = attachment.get('filename', 'unknown') + content = attachment.get('content', b'') + + # 生成安全的文件名 + safe_filename = self._generate_safe_filename(filename) + file_path = os.path.join(self.backup_dir, 'attachments', safe_filename) + + # 保存附件文件 + with open(file_path, 'wb') as f: + f.write(content) + + # 计算文件哈希 + file_hash = hashlib.md5(content).hexdigest() + + # 插入附件记录 + cursor.execute(''' + INSERT INTO attachments ( + email_id, filename, content_type, size, file_path, hash + ) VALUES (?, ?, ?, ?, ?, ?) + ''', ( + email_id, + filename, + attachment.get('content_type', ''), + len(content), + file_path, + file_hash + )) + + print(f" ✓ 备份附件: {filename}") + + except Exception as e: + print(f" ✗ 附件备份失败: {e}") + + def _generate_safe_filename(self, filename): + """生成安全的文件名""" + # 移除危险字符 + safe_chars = "-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + safe_filename = ''.join(c for c in filename if c in safe_chars) + + # 添加时间戳避免冲突 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + name, ext = os.path.splitext(safe_filename) + + return f"{name}_{timestamp}{ext}" + + def search_emails(self, query, search_type='subject'): + """搜索邮件""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + if search_type == 'subject': + cursor.execute( + 'SELECT * FROM emails WHERE subject LIKE ?', + (f'%{query}%',) + ) + elif search_type == 'sender': + cursor.execute( + 'SELECT * FROM emails WHERE sender LIKE ?', + (f'%{query}%',) + ) + elif search_type == 'content': + cursor.execute( + 'SELECT * FROM emails WHERE body_text LIKE ? OR body_html LIKE ?', + (f'%{query}%', f'%{query}%') + ) + else: + cursor.execute('SELECT * FROM emails') + + results = cursor.fetchall() + conn.close() + + return results + + def export_emails(self, output_format='json', date_range=None): + """导出邮件""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 构建查询 + query = 'SELECT * FROM emails' + params = [] + + if date_range: + query += ' WHERE date_sent BETWEEN ? AND ?' + params.extend(date_range) + + cursor.execute(query, params) + emails = cursor.fetchall() + + # 获取列名 + columns = [description[0] for description in cursor.description] + + conn.close() + + # 转换为字典列表 + email_dicts = [] + for email in emails: + email_dict = dict(zip(columns, email)) + email_dicts.append(email_dict) + + # 导出文件 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + if output_format == 'json': + filename = f"emails_export_{timestamp}.json" + filepath = os.path.join(self.backup_dir, 'exports', filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(email_dicts, f, ensure_ascii=False, indent=2) + + elif output_format == 'csv': + import csv + filename = f"emails_export_{timestamp}.csv" + filepath = os.path.join(self.backup_dir, 'exports', filename) + + with open(filepath, 'w', newline='', encoding='utf-8') as f: + if email_dicts: + writer = csv.DictWriter(f, fieldnames=columns) + writer.writeheader() + writer.writerows(email_dicts) + + print(f" ✓ 导出完成: {filepath}") + return filepath + + def get_backup_stats(self): + """获取备份统计""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 邮件统计 + cursor.execute('SELECT COUNT(*) FROM emails') + total_emails = cursor.fetchone()[0] + + cursor.execute('SELECT SUM(size) FROM emails') + total_size = cursor.fetchone()[0] or 0 + + # 附件统计 + cursor.execute('SELECT COUNT(*) FROM attachments') + total_attachments = cursor.fetchone()[0] + + cursor.execute('SELECT SUM(size) FROM attachments') + attachments_size = cursor.fetchone()[0] or 0 + + # 日期范围 + cursor.execute('SELECT MIN(date_sent), MAX(date_sent) FROM emails') + date_range = cursor.fetchone() + + conn.close() + + stats = { + 'total_emails': total_emails, + 'total_size': total_size, + 'total_attachments': total_attachments, + 'attachments_size': attachments_size, + 'date_range': date_range, + 'backup_dir': self.backup_dir + } + + return stats + + # 2. 测试备份系统 + print("\n2. 测试备份系统:") + + def test_backup_system(): + """测试备份系统""" + # 创建备份系统 + backup_system = EmailBackupSystem("test_backup") + + # 生成测试邮件 + test_emails = [ + { + 'message_id': 'msg001@example.com', + 'from': 'sender1@example.com', + 'to': ['recipient@example.com'], + 'subject': '重要会议通知', + 'date': '2024-01-15T10:30:00', + 'body_text': '明天下午2点开会,请准时参加。', + 'body_html': '

明天下午2点开会,请准时参加。

', + 'size': 1024, + 'attachments': [ + { + 'filename': 'agenda.pdf', + 'content_type': 'application/pdf', + 'content': b'PDF content here' + } + ] + }, + { + 'message_id': 'msg002@example.com', + 'from': 'sender2@example.com', + 'to': ['recipient@example.com'], + 'subject': '项目进度报告', + 'date': '2024-01-16T14:20:00', + 'body_text': '本周项目进度如下...', + 'body_html': '

本周项目进度如下...

', + 'size': 2048 + } + ] + + # 备份邮件 + print(" 备份邮件:") + for email in test_emails: + backup_system.backup_email(email) + + # 重复备份测试(应该跳过) + print("\n 重复备份测试:") + backup_system.backup_email(test_emails[0]) + + # 搜索测试 + print("\n 搜索测试:") + results = backup_system.search_emails('会议', 'subject') + print(f" 找到 {len(results)} 封包含'会议'的邮件") + + # 导出测试 + print("\n 导出测试:") + json_file = backup_system.export_emails('json') + + # 统计信息 + print("\n 备份统计:") + stats = backup_system.get_backup_stats() + print(f" 总邮件数: {stats['total_emails']}") + print(f" 总大小: {stats['total_size']} 字节") + print(f" 附件数: {stats['total_attachments']}") + print(f" 日期范围: {stats['date_range'][0]} 至 {stats['date_range'][1]}") + + test_backup_system() + +# 运行邮件备份演示 +email_backup_demo() +``` + +### 5.2 邮件监控系统 + +```python +import time +import threading +from datetime import datetime, timedelta +import logging + +def email_monitoring_demo(): + """邮件监控系统演示""" + print("=== 邮件监控系统演示 ===") + + # 1. 邮件监控类 + print("\n1. 邮件监控系统:") + + class EmailMonitor: + """邮件监控系统""" + + def __init__(self, check_interval=60): + self.check_interval = check_interval # 检查间隔(秒) + self.is_running = False + self.monitor_thread = None + self.alerts = [] + self.rules = [] + self.stats = { + 'total_checked': 0, + 'alerts_triggered': 0, + 'last_check': None + } + + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def add_rule(self, name, condition, alert_type, threshold=None): + """添加监控规则""" + rule = { + 'name': name, + 'condition': condition, + 'alert_type': alert_type, + 'threshold': threshold, + 'enabled': True, + 'last_triggered': None, + 'trigger_count': 0 + } + self.rules.append(rule) + print(f" ✓ 添加监控规则: {name}") + + def start_monitoring(self): + """开始监控""" + if self.is_running: + print(" 监控已在运行中") + return + + self.is_running = True + self.monitor_thread = threading.Thread(target=self._monitor_loop) + self.monitor_thread.daemon = True + self.monitor_thread.start() + + print(f" ✓ 开始邮件监控,检查间隔: {self.check_interval} 秒") + + def stop_monitoring(self): + """停止监控""" + self.is_running = False + if self.monitor_thread: + self.monitor_thread.join(timeout=5) + + print(" ✓ 停止邮件监控") + + def _monitor_loop(self): + """监控循环""" + while self.is_running: + try: + self._check_emails() + self.stats['last_check'] = datetime.now().isoformat() + time.sleep(self.check_interval) + except Exception as e: + self.logger.error(f"监控检查失败: {e}") + time.sleep(self.check_interval) + + def _check_emails(self): + """检查邮件""" + # 模拟获取新邮件 + new_emails = self._fetch_new_emails() + self.stats['total_checked'] += len(new_emails) + + for email in new_emails: + self._evaluate_rules(email) + + def _fetch_new_emails(self): + """获取新邮件(模拟)""" + # 这里应该连接到实际的邮件服务器 + # 为演示目的,返回模拟数据 + import random + + if random.random() < 0.3: # 30% 概率有新邮件 + return [ + { + 'from': random.choice([ + 'important@company.com', + 'spam@suspicious.com', + 'client@customer.com' + ]), + 'subject': random.choice([ + '紧急:系统故障', + '免费赠品!', + '会议邀请', + '发票确认' + ]), + 'size': random.randint(1000, 100000), + 'date': datetime.now().isoformat(), + 'body': '邮件内容...' + } + ] + return [] + + def _evaluate_rules(self, email): + """评估监控规则""" + for rule in self.rules: + if not rule['enabled']: + continue + + if self._check_rule_condition(email, rule): + self._trigger_alert(email, rule) + + def _check_rule_condition(self, email, rule): + """检查规则条件""" + condition = rule['condition'] + condition_type = condition['type'] + + if condition_type == 'sender_suspicious': + suspicious_domains = condition.get('domains', []) + sender = email.get('from', '') + return any(domain in sender for domain in suspicious_domains) + + elif condition_type == 'large_email': + threshold = rule.get('threshold', 10 * 1024 * 1024) # 10MB + return email.get('size', 0) > threshold + + elif condition_type == 'keyword_alert': + keywords = condition.get('keywords', []) + text = f"{email.get('subject', '')} {email.get('body', '')}" + return any(keyword.lower() in text.lower() for keyword in keywords) + + elif condition_type == 'high_volume': + # 检查短时间内的邮件量 + threshold = rule.get('threshold', 10) + time_window = condition.get('time_window', 300) # 5分钟 + + # 这里应该检查实际的邮件量 + # 为演示目的,随机返回 + import random + return random.random() < 0.1 # 10% 概率触发 + + return False + + def _trigger_alert(self, email, rule): + """触发警报""" + alert = { + 'id': len(self.alerts) + 1, + 'rule_name': rule['name'], + 'alert_type': rule['alert_type'], + 'email_info': { + 'from': email.get('from', ''), + 'subject': email.get('subject', ''), + 'date': email.get('date', '') + }, + 'triggered_at': datetime.now().isoformat(), + 'severity': self._get_alert_severity(rule['alert_type']) + } + + self.alerts.append(alert) + rule['last_triggered'] = alert['triggered_at'] + rule['trigger_count'] += 1 + self.stats['alerts_triggered'] += 1 + + # 记录日志 + self.logger.warning( + f"警报触发: {rule['name']} - {email.get('subject', 'No Subject')}" + ) + + # 发送通知 + self._send_notification(alert) + + def _get_alert_severity(self, alert_type): + """获取警报严重级别""" + severity_map = { + 'security': 'HIGH', + 'spam': 'MEDIUM', + 'volume': 'MEDIUM', + 'size': 'LOW', + 'keyword': 'MEDIUM' + } + return severity_map.get(alert_type, 'LOW') + + def _send_notification(self, alert): + """发送通知""" + # 这里可以实现各种通知方式: + # - 邮件通知 + # - 短信通知 + # - 即时消息 + # - 系统通知 + + print(f" 🚨 {alert['severity']} 警报: {alert['rule_name']}") + print(f" 邮件: {alert['email_info']['subject']}") + print(f" 发件人: {alert['email_info']['from']}") + + def get_alerts(self, severity=None, limit=None): + """获取警报列表""" + alerts = self.alerts + + if severity: + alerts = [a for a in alerts if a['severity'] == severity] + + if limit: + alerts = alerts[-limit:] + + return alerts + + def clear_alerts(self, older_than_days=None): + """清理警报""" + if older_than_days: + cutoff_date = datetime.now() - timedelta(days=older_than_days) + cutoff_str = cutoff_date.isoformat() + + original_count = len(self.alerts) + self.alerts = [ + alert for alert in self.alerts + if alert['triggered_at'] > cutoff_str + ] + cleared_count = original_count - len(self.alerts) + print(f" ✓ 清理了 {cleared_count} 个旧警报") + else: + cleared_count = len(self.alerts) + self.alerts = [] + print(f" ✓ 清理了所有 {cleared_count} 个警报") + + def get_stats(self): + """获取监控统计""" + stats = self.stats.copy() + stats.update({ + 'active_rules': len([r for r in self.rules if r['enabled']]), + 'total_rules': len(self.rules), + 'pending_alerts': len(self.alerts), + 'is_running': self.is_running + }) + return stats + + # 2. 配置监控规则 + print("\n2. 配置监控规则:") + + def setup_monitoring_rules(): + """设置监控规则""" + monitor = EmailMonitor(check_interval=5) # 5秒检查一次(演示用) + + # 可疑发件人监控 + monitor.add_rule( + "可疑发件人检测", + { + 'type': 'sender_suspicious', + 'domains': ['suspicious.com', 'spam.net', 'phishing.org'] + }, + 'security' + ) + + # 大邮件监控 + monitor.add_rule( + "大邮件检测", + { + 'type': 'large_email' + }, + 'size', + threshold=5 * 1024 * 1024 # 5MB + ) + + # 关键词警报 + monitor.add_rule( + "敏感关键词检测", + { + 'type': 'keyword_alert', + 'keywords': ['紧急', '故障', '病毒', '攻击', 'urgent', 'critical'] + }, + 'keyword' + ) + + # 邮件量异常监控 + monitor.add_rule( + "邮件量异常检测", + { + 'type': 'high_volume', + 'time_window': 300 # 5分钟 + }, + 'volume', + threshold=20 # 5分钟内超过20封 + ) + + return monitor + + # 3. 测试监控系统 + print("\n3. 测试监控系统:") + + def test_monitoring_system(): + """测试监控系统""" + monitor = setup_monitoring_rules() + + # 开始监控 + monitor.start_monitoring() + + print(" 监控运行中,等待警报...") + + # 运行一段时间 + time.sleep(15) + + # 检查统计 + stats = monitor.get_stats() + print(f"\n 监控统计:") + print(f" 检查的邮件数: {stats['total_checked']}") + print(f" 触发的警报数: {stats['alerts_triggered']}") + print(f" 活跃规则数: {stats['active_rules']}") + print(f" 最后检查时间: {stats['last_check']}") + + # 显示警报 + alerts = monitor.get_alerts() + if alerts: + print(f"\n 最近的警报:") + for alert in alerts[-5:]: # 显示最近5个 + print(f" {alert['severity']} - {alert['rule_name']}") + print(f" {alert['email_info']['subject']}") + + # 停止监控 + monitor.stop_monitoring() + + test_monitoring_system() + +# 运行邮件监控演示 +email_monitoring_demo() +``` + +## 6. 最佳实践和安全 + +### 6.1 邮件安全最佳实践 + +```python +import ssl +import base64 +from cryptography.fernet import Fernet +import keyring + +def email_security_demo(): + """邮件安全最佳实践演示""" + print("=== 邮件安全最佳实践演示 ===") + + # 1. 安全连接配置 + print("\n1. 安全连接配置:") + + def create_secure_smtp_connection(): + """创建安全的SMTP连接""" + import smtplib + + # 创建SSL上下文 + context = ssl.create_default_context() + + # 配置SSL选项 + context.check_hostname = True + context.verify_mode = ssl.CERT_REQUIRED + + # 禁用不安全的协议 + context.options |= ssl.OP_NO_SSLv2 + context.options |= ssl.OP_NO_SSLv3 + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + + print(" ✓ 创建安全SSL上下文") + + try: + # 使用SMTP_SSL直接建立加密连接 + server = smtplib.SMTP_SSL('smtp.gmail.com', 465, context=context) + print(" ✓ 建立SSL加密连接") + + # 或者使用STARTTLS + # server = smtplib.SMTP('smtp.gmail.com', 587) + # server.starttls(context=context) + + return server + + except Exception as e: + print(f" ✗ 连接失败: {e}") + return None + + def create_secure_imap_connection(): + """创建安全的IMAP连接""" + import imaplib + + # 创建SSL上下文 + context = ssl.create_default_context() + + try: + # 使用IMAP4_SSL + server = imaplib.IMAP4_SSL('imap.gmail.com', 993, ssl_context=context) + print(" ✓ 建立IMAP SSL连接") + return server + + except Exception as e: + print(f" ✗ IMAP连接失败: {e}") + return None + + # 测试安全连接 + smtp_server = create_secure_smtp_connection() + if smtp_server: + smtp_server.quit() + + imap_server = create_secure_imap_connection() + if imap_server: + imap_server.logout() + + # 2. 密码安全管理 + print("\n2. 密码安全管理:") + + class SecureCredentialManager: + """安全凭据管理器""" + + def __init__(self, service_name="email_client"): + self.service_name = service_name + + def store_credentials(self, username, password): + """安全存储凭据""" + try: + # 使用系统密钥环存储密码 + keyring.set_password(self.service_name, username, password) + print(f" ✓ 安全存储凭据: {username}") + return True + except Exception as e: + print(f" ✗ 存储失败: {e}") + return False + + def get_credentials(self, username): + """获取存储的凭据""" + try: + password = keyring.get_password(self.service_name, username) + if password: + print(f" ✓ 获取凭据: {username}") + return password + else: + print(f" ✗ 未找到凭据: {username}") + return None + except Exception as e: + print(f" ✗ 获取失败: {e}") + return None + + def delete_credentials(self, username): + """删除存储的凭据""" + try: + keyring.delete_password(self.service_name, username) + print(f" ✓ 删除凭据: {username}") + return True + except Exception as e: + print(f" ✗ 删除失败: {e}") + return False + + def encrypt_data(self, data): + """加密敏感数据""" + # 生成密钥 + key = Fernet.generate_key() + cipher = Fernet(key) + + # 加密数据 + encrypted_data = cipher.encrypt(data.encode()) + + print(" ✓ 数据已加密") + return key, encrypted_data + + def decrypt_data(self, key, encrypted_data): + """解密数据""" + try: + cipher = Fernet(key) + decrypted_data = cipher.decrypt(encrypted_data) + print(" ✓ 数据已解密") + return decrypted_data.decode() + except Exception as e: + print(f" ✗ 解密失败: {e}") + return None + + # 测试凭据管理 + cred_manager = SecureCredentialManager() + + # 注意:实际使用时不要在代码中硬编码密码 + test_username = "test@example.com" + test_password = "secure_password_123" + + # 存储和获取凭据(仅演示,实际环境中谨慎使用) + # cred_manager.store_credentials(test_username, test_password) + # retrieved_password = cred_manager.get_credentials(test_username) + + # 测试数据加密 + sensitive_data = "这是敏感的邮件内容" + key, encrypted = cred_manager.encrypt_data(sensitive_data) + decrypted = cred_manager.decrypt_data(key, encrypted) + + # 3. 邮件内容安全 + print("\n3. 邮件内容安全:") + + class EmailSecurityValidator: + """邮件安全验证器""" + + def __init__(self): + self.suspicious_patterns = [ + r'(?i)password.*reset', + r'(?i)click.*here.*urgent', + r'(?i)verify.*account.*immediately', + r'(?i)suspended.*account', + r'(?i)winner.*lottery', + r'(?i)free.*money', + r'(?i)nigerian.*prince' + ] + + self.safe_domains = [ + 'gmail.com', 'outlook.com', 'yahoo.com', + 'company.com' # 添加你信任的域名 + ] + + def validate_sender(self, sender_email): + """验证发件人""" + import re + + # 检查邮箱格式 + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_pattern, sender_email): + return False, "邮箱格式无效" + + # 检查域名 + domain = sender_email.split('@')[1] + if domain in self.safe_domains: + return True, "可信域名" + + # 检查可疑域名特征 + suspicious_domain_patterns = [ + r'.*\d{4,}.*', # 包含4位以上数字 + r'.*-.*-.*', # 多个连字符 + r'.{20,}', # 域名过长 + ] + + for pattern in suspicious_domain_patterns: + if re.match(pattern, domain): + return False, f"可疑域名模式: {pattern}" + + return True, "域名检查通过" + + def scan_content(self, subject, body): + """扫描邮件内容""" + import re + + content = f"{subject} {body}" + threats = [] + + # 检查可疑模式 + for pattern in self.suspicious_patterns: + if re.search(pattern, content): + threats.append(f"可疑模式: {pattern}") + + # 检查URL + url_pattern = r'https?://[^\s]+' + urls = re.findall(url_pattern, content) + + for url in urls: + if self._is_suspicious_url(url): + threats.append(f"可疑链接: {url}") + + # 检查附件提及 + attachment_patterns = [ + r'(?i)attachment.*exe', + r'(?i)download.*file', + r'(?i)open.*document' + ] + + for pattern in attachment_patterns: + if re.search(pattern, content): + threats.append(f"可疑附件提及: {pattern}") + + return threats + + def _is_suspicious_url(self, url): + """检查URL是否可疑""" + import re + + suspicious_url_patterns = [ + r'.*bit\.ly.*', + r'.*tinyurl.*', + r'.*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}.*', # IP地址 + r'.*[a-z]{20,}\.[a-z]{2,3}.*', # 随机长域名 + ] + + for pattern in suspicious_url_patterns: + if re.match(pattern, url): + return True + + return False + + def validate_email(self, email_data): + """综合验证邮件""" + results = { + 'is_safe': True, + 'warnings': [], + 'threats': [] + } + + # 验证发件人 + sender_valid, sender_msg = self.validate_sender(email_data.get('from', '')) + if not sender_valid: + results['threats'].append(f"发件人验证失败: {sender_msg}") + results['is_safe'] = False + + # 扫描内容 + content_threats = self.scan_content( + email_data.get('subject', ''), + email_data.get('body', '') + ) + + if content_threats: + results['threats'].extend(content_threats) + results['is_safe'] = False + + # 检查附件 + attachments = email_data.get('attachments', []) + for attachment in attachments: + filename = attachment.get('filename', '') + if self._is_dangerous_attachment(filename): + results['threats'].append(f"危险附件: {filename}") + results['is_safe'] = False + + return results + + def _is_dangerous_attachment(self, filename): + """检查附件是否危险""" + dangerous_extensions = [ + '.exe', '.scr', '.bat', '.cmd', '.com', '.pif', + '.vbs', '.js', '.jar', '.zip', '.rar' + ] + + filename_lower = filename.lower() + return any(filename_lower.endswith(ext) for ext in dangerous_extensions) + + # 测试安全验证 + validator = EmailSecurityValidator() + + # 测试邮件 + test_emails = [ + { + 'from': 'legitimate@company.com', + 'subject': '会议通知', + 'body': '明天下午2点开会', + 'attachments': [] + }, + { + 'from': 'suspicious123456@random-domain.com', + 'subject': 'URGENT: Verify your account immediately!', + 'body': 'Click here to reset your password: http://bit.ly/suspicious', + 'attachments': [{'filename': 'document.exe'}] + } + ] + + print(" 邮件安全验证:") + for i, email in enumerate(test_emails, 1): + print(f"\n 邮件 {i}: {email['subject']}") + result = validator.validate_email(email) + + if result['is_safe']: + print(" ✓ 邮件安全") + else: + print(" ⚠️ 发现威胁:") + for threat in result['threats']: + print(f" • {threat}") + +# 运行邮件安全演示 +email_security_demo() +``` + +## 7. 学习建议和总结 + +### 7.1 学习路径 + +1. **基础知识** + - 理解邮件协议(SMTP、POP3、IMAP) + - 掌握Python邮件模块的基本用法 + - 学习邮件格式和编码 + +2. **进阶应用** + - 邮件内容解析和处理 + - 附件处理和文件操作 + - 邮件过滤和自动化 + +3. **实际项目** + - 构建邮件客户端 + - 实现邮件监控系统 + - 开发邮件备份工具 + +### 7.2 最佳实践 + +1. **安全性** + - 使用SSL/TLS加密连接 + - 安全存储邮箱密码 + - 验证邮件来源和内容 + +2. **性能优化** + - 合理使用连接池 + - 批量处理邮件 + - 异步处理大量邮件 + +3. **错误处理** + - 完善的异常处理机制 + - 重试机制和超时设置 + - 详细的日志记录 + +### 7.3 常见陷阱和解决方案 + +1. **编码问题** + ```python + # 问题:邮件内容乱码 + # 解决:正确处理字符编码 + import chardet + + def decode_email_content(content): + if isinstance(content, bytes): + encoding = chardet.detect(content)['encoding'] + return content.decode(encoding or 'utf-8') + return content + ``` + +2. **连接超时** + ```python + # 问题:邮件服务器连接超时 + # 解决:设置合理的超时时间 + import socket + + # 设置全局超时 + socket.setdefaulttimeout(30) + + # 或在连接时设置 + server = smtplib.SMTP('smtp.gmail.com', 587, timeout=30) + ``` + +3. **内存占用** + ```python + # 问题:处理大量邮件时内存占用过高 + # 解决:使用生成器和流式处理 + def process_emails_efficiently(email_list): + for email in email_list: + # 处理单封邮件 + process_single_email(email) + # 及时释放内存 + del email + ``` + +### 7.4 本章总结 + +本章详细介绍了Python电子邮件编程的各个方面: + +1. **邮件基础**:学习了SMTP、POP3、IMAP协议的基本概念和Python实现 +2. **发送邮件**:掌握了文本邮件、HTML邮件、附件邮件的发送方法 +3. **接收邮件**:学会了连接邮件服务器、下载和解析邮件内容 +4. **邮件处理**:实现了邮件过滤、自动回复、统计分析等高级功能 +5. **实际应用**:构建了邮件备份系统和监控系统 +6. **安全实践**:了解了邮件安全的重要性和实现方法 + +通过本章的学习,你应该能够: +- 使用Python发送和接收各种类型的邮件 +- 解析和处理邮件内容和附件 +- 实现邮件自动化处理功能 +- 构建实用的邮件应用系统 +- 确保邮件操作的安全性 + +邮件编程是Python的重要应用领域,掌握这些技能将为你的项目开发提供强大的通信能力。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/2.md b/docs/Python/2.md new file mode 100644 index 000000000..22b8637f1 --- /dev/null +++ b/docs/Python/2.md @@ -0,0 +1,241 @@ +--- +title: 第2天-安装Python +author: 哪吒 +date: '2023-06-15' +--- + +# 第2天-安装Python + +## Python的跨平台特性 + +因为Python是跨平台的,它可以运行在Windows、Mac和各种Linux/Unix系统上。在Windows上写Python程序,放到Linux上也是能够运行的。 + +> **小白注解**:跨平台意味着"一次编写,到处运行",这是Python的一大优势。你不需要为不同的操作系统重写代码。 + +## 安装Python能得到什么? + +要开始学习Python编程,首先就得把Python安装到你的电脑里。安装后,你会得到: + +> **安装包含的组件**: +> - 🐍 **Python解释器**:负责运行Python程序的核心引擎 +> - 💻 **命令行交互环境**:可以直接输入Python代码并立即看到结果 +> - 🛠️ **IDLE**:Python自带的简单集成开发环境 +> - 📚 **标准库**:大量内置的功能模块 +> - 📦 **pip**:Python包管理工具,用于安装第三方库 + +## Python版本选择 + +目前,Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的。由于3.x版越来越普及,教程将以最新的Python 3.x版本为基础。 + +> **版本说明**: +> - **Python 2.x**:已于2020年1月1日停止维护,不建议新项目使用 +> - **Python 3.x**:当前主流版本,持续更新中 +> - **推荐版本**:Python 3.8+ (支持最新特性,稳定性好) + +> **小白提醒**:如果你是初学者,直接选择最新的Python 3.x版本就对了! + +## 在Windows上安装Python + +在Windows上安装Python,有两种方法: + +### 方法一:官方安装包(推荐新手) + +可以直接从Python的官方网站下载Python 3对应的Windows安装程序,推荐下载Windows installer (64-bit),然后,运行下载的python-3.x-amd64.exe安装包: + +> **下载地址**:https://www.python.org/downloads/windows/ + +![img_2.png](./img_2.png) + +> **⚠️ 重要提醒**:特别要注意勾上"Add Python 3.x to PATH",这样你就可以在任何地方使用python命令了! + +**安装步骤**: +1. 下载安装包 +2. 双击运行安装程序 +3. ✅ **务必勾选**"Add Python to PATH" +4. 点击"Install Now" +5. 等待安装完成 + +**验证安装**: +```bash +C:\Users\23979>python --version +Python 3.13.5 +``` + +> **小白解释**:`python --version`命令用来查看Python版本,如果显示版本号说明安装成功! + +### 方法二:包管理器安装(适合进阶用户) + +先安装一个包管理器,推荐Scoop,然后在PowerShell中通过以下命令安装Python: + +```powershell +# 首先安装Scoop(如果还没有的话) +Set-ExecutionPolicy RemoteSigned -Scope CurrentUser +irm get.scoop.sh | iex + +# 然后安装Python +scoop install python +``` + +> **Scoop的优势**: +> - 自动管理PATH环境变量 +> - 可以同时安装多个Python版本 +> - 卸载干净,不留垃圾文件 +> - 命令行操作,适合开发者 + +## 在macOS上安装Python + +如果你正在使用Mac,那么系统自带的Python版本是2.x。要安装最新的Python 3.x,有两个方法: + +### 方法一:官方安装包 +从Python官网下载Python 3 macOS版的安装程序,下载后双击运行并安装。 + +> **下载地址**:https://www.python.org/downloads/macos/ + +### 方法二:Homebrew安装(推荐) +如果安装了包管理器Homebrew,直接通过命令安装: + +```bash +# 安装Python 3 +brew install python + +# 验证安装 +python3 --version +``` + +> **macOS注意事项**: +> - 系统自带的`python`命令指向Python 2.x +> - 新安装的Python 3使用`python3`命令 +> - 建议创建别名:`alias python=python3` + +## 在Linux上安装Python + +大多数Linux发行版都预装了Python,但可能版本较旧。以下是常见发行版的安装方法: + +### Ubuntu/Debian系统 +```bash +# 更新包列表 +sudo apt update + +# 安装Python 3 +sudo apt install python3 python3-pip + +# 验证安装 +python3 --version +``` + +### CentOS/RHEL系统 +```bash +# 安装Python 3 +sudo yum install python3 python3-pip +# 或者在较新版本中使用 +sudo dnf install python3 python3-pip +``` + +### Arch Linux +```bash +# 安装Python 3 +sudo pacman -S python python-pip +``` + +> **Linux小贴士**:如果你正在使用Linux,建议你有一定的系统管理经验。如果完全是新手,建议先从Windows开始学习Python。 + +## 测试Python交互环境 + +安装完成后,打开命令行输入`python`(Windows)或`python3`(macOS/Linux),你会看到类似这样的界面: + +```python +C:\Users\23979>python +Python 3.13.5 (tags/v3.13.5:6cb20a2, Jun 11 2025, 16:15:46) [MSC v.1943 64 bit (AMD64)] on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> 1+1 +2 +>>> print("Hello, Python!") +Hello, Python! +>>> exit() +``` + +> **交互环境说明**: +> - `>>>`是Python的提示符,表示等待你输入代码 +> - 输入代码后按回车,立即看到结果 +> - 输入`exit()`或按`Ctrl+Z`(Windows)/`Ctrl+D`(macOS/Linux)退出 + +## Python解释器详解 + +当我们编写Python代码时,我们得到的是一个包含Python代码的以.py为扩展名的文本文件。要运行代码,就需要Python解释器去执行.py文件。 + +> **小白理解**:解释器就像一个翻译官,把你写的Python代码翻译成计算机能理解的机器语言。 + +### 多种Python解释器 + +由于整个Python语言从规范到解释器都是开源的,所以理论上,只要水平够高,任何人都可以编写Python解释器来执行Python代码(当然难度很大)。事实上,确实存在多种Python解释器: + +> **主要Python解释器**: +> +> 🔥 **CPython**(官方,推荐) +> - 用C语言开发,最标准的实现 +> - 使用最广泛,兼容性最好 +> - 我们通常说的"Python"就是指CPython +> +> ⚡ **PyPy**(高性能) +> - 用Python自己写的Python解释器 +> - 执行速度比CPython快2-10倍 +> - 适合计算密集型程序 +> +> ☕ **Jython**(Java平台) +> - 运行在Java虚拟机上 +> - 可以调用Java类库 +> - 适合Java环境集成 +> +> 🌐 **IronPython**(.NET平台) +> - 运行在.NET平台上 +> - 可以调用.NET类库 +> - 适合Windows/.NET环境 + +### CPython解释器 + +当我们从Python官方网站下载并安装好Python 3.x后,我们就直接获得了一个官方版本的解释器:CPython。这个解释器是用C语言开发的,所以叫CPython。在命令行下运行python就是启动CPython解释器。 + +CPython是使用最广的Python解释器。教程的所有代码也都在CPython下执行。 + +> **为什么选择CPython?** +> - ✅ 官方标准实现,最稳定可靠 +> - ✅ 第三方库支持最完整 +> - ✅ 文档和社区支持最好 +> - ✅ 适合初学者和生产环境 + +## 常见问题解答 + +> **Q: 安装后找不到python命令怎么办?** +> +> A: 检查是否勾选了"Add Python to PATH"。如果没有,可以: +> 1. 重新安装Python并勾选该选项 +> 2. 手动添加Python安装目录到系统PATH +> 3. 使用完整路径运行:`C:\Python39\python.exe` + +> **Q: python和python3命令有什么区别?** +> +> A: +> - **Windows**:通常都是`python` +> - **macOS/Linux**:`python`可能指向Python 2.x,`python3`指向Python 3.x +> - 建议在macOS/Linux上使用`python3`命令 + +> **Q: 如何同时安装多个Python版本?** +> +> A: +> - 使用**pyenv**(macOS/Linux)或**pyenv-win**(Windows) +> - 使用**Anaconda**管理多个环境 +> - 手动安装到不同目录 + +## 下一步 + +恭喜!你已经成功安装了Python。现在你可以: +- 在命令行中使用Python交互环境 +- 编写并运行你的第一个Python程序 +- 开始学习Python语法 + +> **学习建议**:先熟悉Python交互环境,它是学习和测试代码的好工具。在交互环境中,你可以立即看到代码的执行结果,这对理解Python语法非常有帮助。 + + + + + diff --git a/docs/Python/20.md b/docs/Python/20.md new file mode 100644 index 000000000..b9f0ebb89 --- /dev/null +++ b/docs/Python/20.md @@ -0,0 +1,2756 @@ +--- +title: 第20天-访问数据库 +author: 哪吒 +date: '2023-06-15' +--- + +# 第20天-访问数据库 + +## 1. 数据库基础 + +### 1.1 数据库概述 + +数据库是存储和管理数据的系统,Python提供了多种方式来访问不同类型的数据库。 + +```python +def database_overview_demo(): + """数据库概述演示""" + print("=== 数据库概述演示 ===") + + # 1. 数据库类型 + print("\n1. 常见数据库类型:") + + database_types = { + "关系型数据库": { + "SQLite": "轻量级文件数据库,无需服务器", + "MySQL": "开源关系型数据库管理系统", + "PostgreSQL": "功能强大的开源对象关系数据库", + "Oracle": "企业级商业数据库", + "SQL Server": "微软的关系型数据库" + }, + "NoSQL数据库": { + "MongoDB": "文档型数据库", + "Redis": "内存键值数据库", + "Cassandra": "分布式列族数据库", + "Neo4j": "图形数据库" + } + } + + for category, databases in database_types.items(): + print(f"\n {category}:") + for name, description in databases.items(): + print(f" • {name}: {description}") + + # 2. Python数据库API规范 + print("\n2. Python数据库API规范 (DB-API 2.0):") + + api_components = { + "连接对象 (Connection)": "表示数据库连接", + "游标对象 (Cursor)": "执行SQL语句和获取结果", + "异常类型": "处理数据库相关错误", + "类型构造器": "处理特殊数据类型" + } + + for component, description in api_components.items(): + print(f" • {component}: {description}") + + # 3. 常用Python数据库模块 + print("\n3. 常用Python数据库模块:") + + python_modules = { + "sqlite3": "Python内置SQLite模块", + "pymysql": "纯Python MySQL客户端", + "psycopg2": "PostgreSQL适配器", + "cx_Oracle": "Oracle数据库接口", + "pymongo": "MongoDB Python驱动", + "redis-py": "Redis Python客户端" + } + + for module, description in python_modules.items(): + print(f" • {module}: {description}") + + # 4. 数据库操作基本流程 + print("\n4. 数据库操作基本流程:") + + basic_flow = [ + "1. 导入数据库模块", + "2. 建立数据库连接", + "3. 创建游标对象", + "4. 执行SQL语句", + "5. 处理查询结果", + "6. 提交事务(如需要)", + "7. 关闭游标和连接" + ] + + for step in basic_flow: + print(f" {step}") + + # 5. 基本SQL语句回顾 + print("\n5. 基本SQL语句回顾:") + + sql_examples = { + "创建表": "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)", + "插入数据": "INSERT INTO users (name, email) VALUES ('张三', 'zhang@example.com')", + "查询数据": "SELECT * FROM users WHERE name = '张三'", + "更新数据": "UPDATE users SET email = 'new@example.com' WHERE id = 1", + "删除数据": "DELETE FROM users WHERE id = 1" + } + + for operation, sql in sql_examples.items(): + print(f" {operation}:") + print(f" {sql}") + +# 运行数据库概述演示 +database_overview_demo() +``` + +## 2. SQLite数据库 + +### 2.1 SQLite基础操作 + +```python +import sqlite3 +import os +from datetime import datetime + +def sqlite_basic_demo(): + """SQLite基础操作演示""" + print("=== SQLite基础操作演示 ===") + + # 1. 连接数据库 + print("\n1. 连接SQLite数据库:") + + # 连接到数据库文件(如果不存在会自动创建) + db_path = 'example.db' + conn = sqlite3.connect(db_path) + print(f" ✓ 连接到数据库: {db_path}") + + # 创建游标 + cursor = conn.cursor() + print(" ✓ 创建游标对象") + + # 2. 创建表 + print("\n2. 创建数据表:") + + # 用户表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL, + password TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1 + ) + ''') + print(" ✓ 创建用户表") + + # 文章表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT, + author_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES users (id) + ) + ''') + print(" ✓ 创建文章表") + + # 3. 插入数据 + print("\n3. 插入数据:") + + # 插入单条记录 + try: + cursor.execute( + "INSERT INTO users (username, email, password) VALUES (?, ?, ?)", + ('张三', 'zhangsan@example.com', 'password123') + ) + print(" ✓ 插入用户: 张三") + except sqlite3.IntegrityError as e: + print(f" 用户已存在: {e}") + + # 插入多条记录 + users_data = [ + ('李四', 'lisi@example.com', 'password456'), + ('王五', 'wangwu@example.com', 'password789'), + ('赵六', 'zhaoliu@example.com', 'passwordabc') + ] + + try: + cursor.executemany( + "INSERT INTO users (username, email, password) VALUES (?, ?, ?)", + users_data + ) + print(f" ✓ 批量插入 {len(users_data)} 个用户") + except sqlite3.IntegrityError as e: + print(f" 部分用户已存在: {e}") + + # 提交事务 + conn.commit() + print(" ✓ 提交事务") + + # 4. 查询数据 + print("\n4. 查询数据:") + + # 查询所有用户 + cursor.execute("SELECT * FROM users") + users = cursor.fetchall() + print(f" 查询到 {len(users)} 个用户:") + for user in users: + print(f" ID: {user[0]}, 用户名: {user[1]}, 邮箱: {user[2]}") + + # 条件查询 + cursor.execute("SELECT id, username FROM users WHERE username LIKE ?", ('%张%',)) + filtered_users = cursor.fetchall() + print(f"\n 姓张的用户 ({len(filtered_users)} 个):") + for user in filtered_users: + print(f" ID: {user[0]}, 用户名: {user[1]}") + + # 使用fetchone获取单条记录 + cursor.execute("SELECT * FROM users WHERE username = ?", ('李四',)) + user = cursor.fetchone() + if user: + print(f"\n 找到用户: {user[1]} ({user[2]})") + + # 5. 更新数据 + print("\n5. 更新数据:") + + cursor.execute( + "UPDATE users SET email = ? WHERE username = ?", + ('zhangsan_new@example.com', '张三') + ) + affected_rows = cursor.rowcount + print(f" ✓ 更新了 {affected_rows} 条记录") + + conn.commit() + + # 6. 删除数据 + print("\n6. 删除数据:") + + cursor.execute("DELETE FROM users WHERE username = ?", ('赵六',)) + deleted_rows = cursor.rowcount + print(f" ✓ 删除了 {deleted_rows} 条记录") + + conn.commit() + + # 7. 查看表结构 + print("\n7. 查看表结构:") + + cursor.execute("PRAGMA table_info(users)") + columns = cursor.fetchall() + print(" users表结构:") + for col in columns: + print(f" {col[1]} {col[2]} {'NOT NULL' if col[3] else 'NULL'}") + + # 8. 关闭连接 + cursor.close() + conn.close() + print("\n ✓ 关闭数据库连接") + + # 清理测试文件 + if os.path.exists(db_path): + os.remove(db_path) + print(f" ✓ 清理测试文件: {db_path}") + +# 运行SQLite基础演示 +sqlite_basic_demo() +``` + +### 2.2 SQLite高级功能 + +```python +import sqlite3 +import json +from contextlib import contextmanager + +def sqlite_advanced_demo(): + """SQLite高级功能演示""" + print("=== SQLite高级功能演示 ===") + + # 1. 连接管理和上下文管理器 + print("\n1. 连接管理:") + + @contextmanager + def get_db_connection(db_path): + """数据库连接上下文管理器""" + conn = None + try: + conn = sqlite3.connect(db_path) + # 设置行工厂,使查询结果可以像字典一样访问 + conn.row_factory = sqlite3.Row + yield conn + except Exception as e: + if conn: + conn.rollback() + raise e + finally: + if conn: + conn.close() + + db_path = 'advanced_example.db' + + # 使用上下文管理器 + with get_db_connection(db_path) as conn: + cursor = conn.cursor() + + # 创建测试表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + price REAL NOT NULL, + category TEXT, + metadata TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + print(" ✓ 使用上下文管理器创建连接") + + # 2. 事务处理 + print("\n2. 事务处理:") + + def transfer_money_demo(): + """转账事务演示""" + with get_db_connection(db_path) as conn: + cursor = conn.cursor() + + # 创建账户表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + balance REAL NOT NULL DEFAULT 0 + ) + ''') + + # 插入测试账户 + cursor.execute("INSERT OR REPLACE INTO accounts (id, name, balance) VALUES (1, '账户A', 1000)") + cursor.execute("INSERT OR REPLACE INTO accounts (id, name, balance) VALUES (2, '账户B', 500)") + conn.commit() + + try: + # 开始事务 + cursor.execute("BEGIN TRANSACTION") + + # 从账户A扣款 + cursor.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (200, 1)) + + # 检查余额是否足够 + cursor.execute("SELECT balance FROM accounts WHERE id = ?", (1,)) + balance = cursor.fetchone()[0] + + if balance < 0: + raise ValueError("余额不足") + + # 向账户B转账 + cursor.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (200, 2)) + + # 提交事务 + conn.commit() + print(" ✓ 转账成功") + + # 查看结果 + cursor.execute("SELECT name, balance FROM accounts") + accounts = cursor.fetchall() + for account in accounts: + print(f" {account['name']}: {account['balance']} 元") + + except Exception as e: + # 回滚事务 + conn.rollback() + print(f" ✗ 转账失败,已回滚: {e}") + + transfer_money_demo() + + # 3. 索引和性能优化 + print("\n3. 索引和性能优化:") + + with get_db_connection(db_path) as conn: + cursor = conn.cursor() + + # 创建索引 + cursor.execute("CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_products_price ON products(price)") + print(" ✓ 创建索引") + + # 插入测试数据 + products_data = [ + ('笔记本电脑', 5999.99, '电子产品', json.dumps({'brand': 'Dell', 'model': 'XPS13'})), + ('无线鼠标', 199.99, '电子产品', json.dumps({'brand': 'Logitech', 'wireless': True})), + ('办公椅', 899.99, '家具', json.dumps({'material': '皮革', 'adjustable': True})), + ('台灯', 299.99, '家具', json.dumps({'type': 'LED', 'dimmable': True})) + ] + + cursor.executemany( + "INSERT OR REPLACE INTO products (name, price, category, metadata) VALUES (?, ?, ?, ?)", + products_data + ) + conn.commit() + print(f" ✓ 插入 {len(products_data)} 个产品") + + # 使用EXPLAIN QUERY PLAN查看查询计划 + cursor.execute("EXPLAIN QUERY PLAN SELECT * FROM products WHERE category = '电子产品'") + plan = cursor.fetchall() + print(" 查询计划:") + for step in plan: + print(f" {step[3]}") + + # 4. JSON数据处理 + print("\n4. JSON数据处理:") + + with get_db_connection(db_path) as conn: + cursor = conn.cursor() + + # 查询并解析JSON数据 + cursor.execute("SELECT name, metadata FROM products WHERE category = '电子产品'") + products = cursor.fetchall() + + print(" 电子产品详情:") + for product in products: + metadata = json.loads(product['metadata']) + print(f" {product['name']}:") + for key, value in metadata.items(): + print(f" {key}: {value}") + + # 5. 自定义函数 + print("\n5. 自定义函数:") + + def calculate_discount(price, discount_rate): + """计算折扣价格""" + return price * (1 - discount_rate / 100) + + with get_db_connection(db_path) as conn: + # 注册自定义函数 + conn.create_function("calculate_discount", 2, calculate_discount) + + cursor = conn.cursor() + + # 使用自定义函数 + cursor.execute( + "SELECT name, price, calculate_discount(price, 20) as discounted_price FROM products" + ) + products = cursor.fetchall() + + print(" 产品价格(8折优惠):") + for product in products: + print(f" {product['name']}: {product['price']:.2f} → {product['discounted_price']:.2f}") + + # 6. 备份和恢复 + print("\n6. 数据库备份:") + + def backup_database(source_db, backup_db): + """备份数据库""" + with sqlite3.connect(source_db) as source: + with sqlite3.connect(backup_db) as backup: + source.backup(backup) + print(f" ✓ 备份完成: {source_db} → {backup_db}") + + backup_path = 'backup_example.db' + backup_database(db_path, backup_path) + + # 验证备份 + with get_db_connection(backup_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM products") + count = cursor.fetchone()[0] + print(f" ✓ 备份验证: 产品数量 {count}") + + # 清理文件 + import os + for file_path in [db_path, backup_path]: + if os.path.exists(file_path): + os.remove(file_path) + print(" ✓ 清理测试文件") + +# 运行SQLite高级演示 +sqlite_advanced_demo() +``` + +## 3. MySQL数据库 + +### 3.1 MySQL连接和基本操作 + +```python +# 注意:需要安装 pymysql: pip install pymysql +try: + import pymysql + PYMYSQL_AVAILABLE = True +except ImportError: + PYMYSQL_AVAILABLE = False + print("PyMySQL未安装,请运行: pip install pymysql") + +def mysql_basic_demo(): + """MySQL基础操作演示""" + print("=== MySQL基础操作演示 ===") + + if not PYMYSQL_AVAILABLE: + print(" ⚠️ PyMySQL模块未安装,跳过MySQL演示") + return + + # 1. 连接配置 + print("\n1. MySQL连接配置:") + + # 数据库连接配置 + config = { + 'host': 'localhost', + 'port': 3306, + 'user': 'root', + 'password': 'password', # 请替换为实际密码 + 'database': 'test_db', + 'charset': 'utf8mb4', + 'autocommit': False + } + + print(" 连接配置:") + for key, value in config.items(): + if key != 'password': + print(f" {key}: {value}") + else: + print(f" {key}: {'*' * len(str(value))}") + + # 2. 连接管理类 + print("\n2. MySQL连接管理:") + + class MySQLManager: + """MySQL连接管理器""" + + def __init__(self, config): + self.config = config + self.connection = None + + def connect(self): + """建立连接""" + try: + self.connection = pymysql.connect(**self.config) + print(" ✓ 连接MySQL成功") + return True + except pymysql.Error as e: + print(f" ✗ 连接失败: {e}") + return False + + def disconnect(self): + """关闭连接""" + if self.connection: + self.connection.close() + print(" ✓ 关闭MySQL连接") + + def execute_query(self, sql, params=None): + """执行查询""" + try: + with self.connection.cursor() as cursor: + cursor.execute(sql, params or ()) + return cursor.fetchall() + except pymysql.Error as e: + print(f" ✗ 查询失败: {e}") + return None + + def execute_update(self, sql, params=None): + """执行更新""" + try: + with self.connection.cursor() as cursor: + affected_rows = cursor.execute(sql, params or ()) + self.connection.commit() + return affected_rows + except pymysql.Error as e: + print(f" ✗ 更新失败: {e}") + self.connection.rollback() + return 0 + + def execute_many(self, sql, params_list): + """批量执行""" + try: + with self.connection.cursor() as cursor: + affected_rows = cursor.executemany(sql, params_list) + self.connection.commit() + return affected_rows + except pymysql.Error as e: + print(f" ✗ 批量执行失败: {e}") + self.connection.rollback() + return 0 + + # 3. 模拟MySQL操作(如果无法连接真实数据库) + print("\n3. MySQL操作演示:") + + def simulate_mysql_operations(): + """模拟MySQL操作""" + print(" 模拟MySQL数据库操作:") + + # 模拟创建表 + create_table_sql = ''' + CREATE TABLE IF NOT EXISTS employees ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + department VARCHAR(50), + salary DECIMAL(10, 2), + hire_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''' + print(" ✓ 创建员工表") + + # 模拟插入数据 + employees_data = [ + ('张三', 'zhangsan@company.com', '技术部', 8000.00, '2023-01-15'), + ('李四', 'lisi@company.com', '销售部', 6000.00, '2023-02-20'), + ('王五', 'wangwu@company.com', '人事部', 7000.00, '2023-03-10'), + ('赵六', 'zhaoliu@company.com', '技术部', 9000.00, '2023-04-05') + ] + + insert_sql = ''' + INSERT INTO employees (name, email, department, salary, hire_date) + VALUES (%s, %s, %s, %s, %s) + ''' + print(f" ✓ 插入 {len(employees_data)} 个员工") + + # 模拟查询操作 + queries = { + "查询所有员工": "SELECT * FROM employees", + "按部门查询": "SELECT * FROM employees WHERE department = '技术部'", + "薪资统计": "SELECT department, AVG(salary) as avg_salary FROM employees GROUP BY department", + "高薪员工": "SELECT name, salary FROM employees WHERE salary > 7000 ORDER BY salary DESC" + } + + for desc, sql in queries.items(): + print(f" • {desc}: {sql[:50]}...") + + # 模拟更新操作 + update_sql = "UPDATE employees SET salary = salary * 1.1 WHERE department = '技术部'" + print(" ✓ 技术部员工加薪10%") + + # 模拟删除操作 + delete_sql = "DELETE FROM employees WHERE hire_date < '2023-02-01'" + print(" ✓ 删除早期员工记录") + + # 尝试连接真实数据库,如果失败则模拟操作 + mysql_manager = MySQLManager(config) + if mysql_manager.connect(): + # 真实数据库操作 + try: + # 创建数据库(如果不存在) + mysql_manager.execute_update("CREATE DATABASE IF NOT EXISTS test_db") + mysql_manager.execute_update("USE test_db") + + # 执行实际操作... + print(" 执行真实MySQL操作") + + except Exception as e: + print(f" 操作过程中出错: {e}") + finally: + mysql_manager.disconnect() + else: + # 模拟操作 + simulate_mysql_operations() + +# 运行MySQL基础演示 +mysql_basic_demo() +``` + +### 3.2 MySQL连接池和高级功能 + +```python +# 连接池需要额外安装: pip install DBUtils +try: + from DBUtils.PooledDB import PooledDB + DBUTILS_AVAILABLE = True +except ImportError: + DBUTILS_AVAILABLE = False + +def mysql_advanced_demo(): + """MySQL高级功能演示""" + print("=== MySQL高级功能演示 ===") + + # 1. 连接池管理 + print("\n1. 连接池管理:") + + class MySQLConnectionPool: + """MySQL连接池管理器""" + + def __init__(self, config, pool_size=5): + self.config = config + self.pool_size = pool_size + self.pool = None + self._init_pool() + + def _init_pool(self): + """初始化连接池""" + if DBUTILS_AVAILABLE and PYMYSQL_AVAILABLE: + try: + self.pool = PooledDB( + creator=pymysql, + maxconnections=self.pool_size, + mincached=2, + maxcached=5, + maxshared=3, + blocking=True, + maxusage=None, + setsession=[], + ping=0, + **self.config + ) + print(f" ✓ 初始化连接池,大小: {self.pool_size}") + except Exception as e: + print(f" ✗ 连接池初始化失败: {e}") + else: + print(" ⚠️ 缺少依赖,使用模拟连接池") + self.pool = None + + def get_connection(self): + """从连接池获取连接""" + if self.pool: + return self.pool.connection() + else: + # 模拟连接 + print(" 获取模拟连接") + return None + + def execute_transaction(self, operations): + """执行事务""" + conn = self.get_connection() + if not conn: + print(" ⚠️ 无法获取连接,模拟事务执行") + return + + try: + cursor = conn.cursor() + + # 开始事务 + conn.begin() + + for operation in operations: + sql = operation['sql'] + params = operation.get('params', ()) + cursor.execute(sql, params) + + # 提交事务 + conn.commit() + print(f" ✓ 事务执行成功,包含 {len(operations)} 个操作") + + except Exception as e: + conn.rollback() + print(f" ✗ 事务执行失败,已回滚: {e}") + finally: + cursor.close() + conn.close() + + # 2. 数据访问对象 (DAO) 模式 + print("\n2. 数据访问对象模式:") + + class UserDAO: + """用户数据访问对象""" + + def __init__(self, connection_pool): + self.pool = connection_pool + + def create_user(self, username, email, password): + """创建用户""" + sql = "INSERT INTO users (username, email, password) VALUES (%s, %s, %s)" + params = (username, email, password) + + # 模拟执行 + print(f" ✓ 创建用户: {username} ({email})") + return True + + def get_user_by_id(self, user_id): + """根据ID获取用户""" + sql = "SELECT * FROM users WHERE id = %s" + params = (user_id,) + + # 模拟返回数据 + user_data = { + 'id': user_id, + 'username': f'user_{user_id}', + 'email': f'user_{user_id}@example.com', + 'created_at': '2023-01-01 00:00:00' + } + + print(f" ✓ 获取用户: {user_data['username']}") + return user_data + + def update_user(self, user_id, **kwargs): + """更新用户信息""" + if not kwargs: + return False + + set_clause = ', '.join([f"{key} = %s" for key in kwargs.keys()]) + sql = f"UPDATE users SET {set_clause} WHERE id = %s" + params = list(kwargs.values()) + [user_id] + + print(f" ✓ 更新用户 {user_id}: {list(kwargs.keys())}") + return True + + def delete_user(self, user_id): + """删除用户""" + sql = "DELETE FROM users WHERE id = %s" + params = (user_id,) + + print(f" ✓ 删除用户: {user_id}") + return True + + def search_users(self, keyword, limit=10): + """搜索用户""" + sql = "SELECT * FROM users WHERE username LIKE %s OR email LIKE %s LIMIT %s" + params = (f'%{keyword}%', f'%{keyword}%', limit) + + # 模拟搜索结果 + results = [ + {'id': i, 'username': f'{keyword}_user_{i}', 'email': f'{keyword}_{i}@example.com'} + for i in range(1, min(limit + 1, 4)) + ] + + print(f" ✓ 搜索用户 '{keyword}': 找到 {len(results)} 个结果") + return results + + # 3. 测试连接池和DAO + print("\n3. 测试连接池和DAO:") + + # 创建连接池 + config = { + 'host': 'localhost', + 'user': 'root', + 'password': 'password', + 'database': 'test_db', + 'charset': 'utf8mb4' + } + + pool = MySQLConnectionPool(config, pool_size=10) + + # 创建DAO实例 + user_dao = UserDAO(pool) + + # 测试CRUD操作 + print("\n CRUD操作测试:") + + # 创建用户 + user_dao.create_user('张三', 'zhangsan@example.com', 'password123') + user_dao.create_user('李四', 'lisi@example.com', 'password456') + + # 查询用户 + user = user_dao.get_user_by_id(1) + + # 更新用户 + user_dao.update_user(1, email='zhangsan_new@example.com', username='张三_new') + + # 搜索用户 + results = user_dao.search_users('张') + + # 删除用户 + user_dao.delete_user(2) + + # 4. 批量操作和性能优化 + print("\n4. 批量操作和性能优化:") + + def batch_operations_demo(): + """批量操作演示""" + + # 批量插入 + batch_data = [ + ('用户1', 'user1@example.com', 'pass1'), + ('用户2', 'user2@example.com', 'pass2'), + ('用户3', 'user3@example.com', 'pass3'), + ('用户4', 'user4@example.com', 'pass4'), + ('用户5', 'user5@example.com', 'pass5') + ] + + print(f" ✓ 批量插入 {len(batch_data)} 个用户") + + # 分页查询 + page_size = 10 + page_num = 1 + offset = (page_num - 1) * page_size + + pagination_sql = f"SELECT * FROM users LIMIT {page_size} OFFSET {offset}" + print(f" ✓ 分页查询: 第{page_num}页,每页{page_size}条") + + # 索引优化建议 + index_suggestions = [ + "CREATE INDEX idx_users_email ON users(email)", + "CREATE INDEX idx_users_username ON users(username)", + "CREATE INDEX idx_users_created_at ON users(created_at)" + ] + + print(" 索引优化建议:") + for suggestion in index_suggestions: + print(f" {suggestion}") + + batch_operations_demo() + + # 5. 数据库监控和统计 + print("\n5. 数据库监控:") + + def database_monitoring_demo(): + """数据库监控演示""" + + monitoring_queries = { + "连接数统计": "SHOW STATUS LIKE 'Threads_connected'", + "查询缓存命中率": "SHOW STATUS LIKE 'Qcache_hits'", + "慢查询数量": "SHOW STATUS LIKE 'Slow_queries'", + "表锁等待": "SHOW STATUS LIKE 'Table_locks_waited'", + "InnoDB缓冲池": "SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests'" + } + + print(" 监控查询:") + for desc, query in monitoring_queries.items(): + print(f" {desc}: {query}") + + # 性能分析 + performance_tips = [ + "使用EXPLAIN分析查询计划", + "合理创建索引,避免过多索引", + "使用连接池减少连接开销", + "定期分析表统计信息", + "监控慢查询日志", + "优化数据库配置参数" + ] + + print("\n 性能优化建议:") + for tip in performance_tips: + print(f" • {tip}") + + database_monitoring_demo() + +# 运行MySQL高级演示 +mysql_advanced_demo() +``` + +## 4. ORM框架 + +### 4.1 SQLAlchemy基础 + +```python +# 注意:需要安装 SQLAlchemy: pip install sqlalchemy +try: + from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, Text + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import sessionmaker, relationship + from datetime import datetime + SQLALCHEMY_AVAILABLE = True +except ImportError: + SQLALCHEMY_AVAILABLE = False + print("SQLAlchemy未安装,请运行: pip install sqlalchemy") + +def sqlalchemy_basic_demo(): + """SQLAlchemy基础演示""" + print("=== SQLAlchemy基础演示 ===") + + if not SQLALCHEMY_AVAILABLE: + print(" ⚠️ SQLAlchemy模块未安装,跳过ORM演示") + return + + # 1. 创建数据库引擎 + print("\n1. 创建数据库引擎:") + + # 使用SQLite内存数据库进行演示 + engine = create_engine('sqlite:///:memory:', echo=False) + print(" ✓ 创建SQLite内存数据库引擎") + + # 2. 定义模型 + print("\n2. 定义ORM模型:") + + Base = declarative_base() + + class User(Base): + """用户模型""" + __tablename__ = 'users' + + id = Column(Integer, primary_key=True) + username = Column(String(50), unique=True, nullable=False) + email = Column(String(100), unique=True, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + # 关系 + posts = relationship("Post", back_populates="author") + + def __repr__(self): + return f"" + + class Post(Base): + """文章模型""" + __tablename__ = 'posts' + + id = Column(Integer, primary_key=True) + title = Column(String(200), nullable=False) + content = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + author_id = Column(Integer, ForeignKey('users.id')) + + # 关系 + author = relationship("User", back_populates="posts") + + def __repr__(self): + return f"" + + print(" ✓ 定义User和Post模型") + + # 3. 创建表 + print("\n3. 创建数据表:") + + Base.metadata.create_all(engine) + print(" ✓ 创建所有表") + + # 4. 创建会话 + print("\n4. 创建数据库会话:") + + Session = sessionmaker(bind=engine) + session = Session() + print(" ✓ 创建数据库会话") + + # 5. 插入数据 + print("\n5. 插入数据:") + + # 创建用户 + user1 = User(username='张三', email='zhangsan@example.com') + user2 = User(username='李四', email='lisi@example.com') + + session.add(user1) + session.add(user2) + session.commit() + print(" ✓ 创建用户") + + # 创建文章 + post1 = Post(title='Python学习笔记', content='今天学习了Python基础语法...', author=user1) + post2 = Post(title='数据库设计', content='数据库设计的基本原则...', author=user1) + post3 = Post(title='Web开发入门', content='Web开发的基础知识...', author=user2) + + session.add_all([post1, post2, post3]) + session.commit() + print(" ✓ 创建文章") + + # 6. 查询数据 + print("\n6. 查询数据:") + + # 查询所有用户 + users = session.query(User).all() + print(f" 所有用户 ({len(users)} 个):") + for user in users: + print(f" {user}") + + # 条件查询 + user = session.query(User).filter(User.username == '张三').first() + if user: + print(f"\n 找到用户: {user}") + print(f" 用户文章数: {len(user.posts)}") + for post in user.posts: + print(f" • {post.title}") + + # 联表查询 + posts_with_authors = session.query(Post).join(User).all() + print(f"\n 文章及作者 ({len(posts_with_authors)} 篇):") + for post in posts_with_authors: + print(f" 《{post.title}》 - {post.author.username}") + + # 7. 更新数据 + print("\n7. 更新数据:") + + user = session.query(User).filter(User.username == '张三').first() + if user: + user.email = 'zhangsan_new@example.com' + session.commit() + print(f" ✓ 更新用户邮箱: {user.email}") + + # 8. 删除数据 + print("\n8. 删除数据:") + + post_to_delete = session.query(Post).filter(Post.title == 'Web开发入门').first() + if post_to_delete: + session.delete(post_to_delete) + session.commit() + print(" ✓ 删除文章: Web开发入门") + + # 9. 高级查询 + print("\n9. 高级查询:") + + # 聚合查询 + from sqlalchemy import func + + user_post_counts = session.query( + User.username, + func.count(Post.id).label('post_count') + ).join(Post).group_by(User.id).all() + + print(" 用户文章统计:") + for username, count in user_post_counts: + print(f" {username}: {count} 篇文章") + + # 排序和限制 + recent_posts = session.query(Post).order_by(Post.created_at.desc()).limit(2).all() + print(f"\n 最新文章 ({len(recent_posts)} 篇):") + for post in recent_posts: + print(f" {post.title}") + + # 10. 关闭会话 + session.close() + print("\n ✓ 关闭数据库会话") + +# 运行SQLAlchemy基础演示 +sqlalchemy_basic_demo() +``` + +### 4.2 SQLAlchemy高级功能 + +```python +def sqlalchemy_advanced_demo(): + """SQLAlchemy高级功能演示""" + print("=== SQLAlchemy高级功能演示 ===") + + if not SQLALCHEMY_AVAILABLE: + print(" ⚠️ SQLAlchemy模块未安装,跳过高级ORM演示") + return + + # 1. 数据库连接池配置 + print("\n1. 连接池配置:") + + from sqlalchemy import create_engine + from sqlalchemy.pool import QueuePool + + # 配置连接池 + engine = create_engine( + 'sqlite:///:memory:', + poolclass=QueuePool, + pool_size=10, + max_overflow=20, + pool_pre_ping=True, + echo=False + ) + print(" ✓ 配置连接池 (大小: 10, 最大溢出: 20)") + + # 2. 模型继承和混入 + print("\n2. 模型继承和混入:") + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy import Column, Integer, String, DateTime, Boolean + from datetime import datetime + + Base = declarative_base() + + class TimestampMixin: + """时间戳混入类""" + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + class SoftDeleteMixin: + """软删除混入类""" + is_deleted = Column(Boolean, default=False) + deleted_at = Column(DateTime) + + class BaseModel(Base, TimestampMixin, SoftDeleteMixin): + """基础模型""" + __abstract__ = True + + id = Column(Integer, primary_key=True) + + class Product(BaseModel): + """产品模型""" + __tablename__ = 'products' + + name = Column(String(100), nullable=False) + price = Column(Integer) # 以分为单位存储价格 + description = Column(String(500)) + + def __repr__(self): + return f"" + + print(" ✓ 定义带混入的Product模型") + + # 3. 自定义查询类 + print("\n3. 自定义查询类:") + + from sqlalchemy.orm import Query + + class SoftDeleteQuery(Query): + """支持软删除的查询类""" + + def filter_active(self): + """过滤未删除的记录""" + return self.filter(self.column_descriptions[0]['type'].is_deleted == False) + + def filter_deleted(self): + """过滤已删除的记录""" + return self.filter(self.column_descriptions[0]['type'].is_deleted == True) + + # 4. 事务管理 + print("\n4. 事务管理:") + + from sqlalchemy.orm import sessionmaker + from contextlib import contextmanager + + Session = sessionmaker(bind=engine) + + @contextmanager + def get_db_session(): + """数据库会话上下文管理器""" + session = Session() + try: + yield session + session.commit() + except Exception as e: + session.rollback() + raise e + finally: + session.close() + + # 创建表 + Base.metadata.create_all(engine) + + # 使用事务 + try: + with get_db_session() as session: + # 批量创建产品 + products = [ + Product(name='笔记本电脑', price=599999, description='高性能笔记本'), + Product(name='无线鼠标', price=19999, description='人体工学设计'), + Product(name='机械键盘', price=39999, description='青轴机械键盘') + ] + + session.add_all(products) + print(" ✓ 批量创建产品(事务中)") + + # 模拟可能的错误 + # raise Exception("模拟错误") + + except Exception as e: + print(f" ✗ 事务失败: {e}") + + # 5. 查询优化 + print("\n5. 查询优化:") + + with get_db_session() as session: + # 延迟加载 vs 立即加载 + from sqlalchemy.orm import joinedload, selectinload + + # 查询产品数量 + product_count = session.query(Product).filter(Product.is_deleted == False).count() + print(f" 活跃产品数量: {product_count}") + + # 分页查询 + page_size = 2 + page_num = 1 + offset = (page_num - 1) * page_size + + products = session.query(Product).filter( + Product.is_deleted == False + ).offset(offset).limit(page_size).all() + + print(f" 分页查询结果 (第{page_num}页):") + for product in products: + print(f" {product.name}: ¥{product.price/100:.2f}") + + # 6. 原生SQL查询 + print("\n6. 原生SQL查询:") + + with get_db_session() as session: + # 执行原生SQL + result = session.execute( + "SELECT name, price FROM products WHERE is_deleted = 0 ORDER BY price DESC" + ) + + print(" 价格排序(原生SQL):") + for row in result: + print(f" {row[0]}: ¥{row[1]/100:.2f}") + + # 7. 数据验证和约束 + print("\n7. 数据验证:") + + from sqlalchemy.orm import validates + + class ValidatedProduct(BaseModel): + """带验证的产品模型""" + __tablename__ = 'validated_products' + + name = Column(String(100), nullable=False) + price = Column(Integer) + + @validates('price') + def validate_price(self, key, price): + """价格验证""" + if price is not None and price < 0: + raise ValueError("价格不能为负数") + return price + + @validates('name') + def validate_name(self, key, name): + """名称验证""" + if not name or len(name.strip()) == 0: + raise ValueError("产品名称不能为空") + return name.strip() + + print(" ✓ 定义带验证的产品模型") + + # 8. 性能监控 + print("\n8. 性能监控:") + + import time + + class QueryTimer: + """查询计时器""" + + def __init__(self, description): + self.description = description + self.start_time = None + + def __enter__(self): + self.start_time = time.time() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + elapsed = time.time() - self.start_time + print(f" {self.description}: {elapsed*1000:.2f}ms") + + with get_db_session() as session: + with QueryTimer("查询所有产品"): + products = session.query(Product).all() + + with QueryTimer("统计产品数量"): + count = session.query(Product).count() + + print("\n ✓ SQLAlchemy高级功能演示完成") + +# 运行SQLAlchemy高级演示 +sqlalchemy_advanced_demo() +``` + +## 5. 数据库设计最佳实践 + +### 5.1 数据库设计原则 + +```python +def database_design_demo(): + """数据库设计最佳实践演示""" + print("=== 数据库设计最佳实践 ===") + + # 1. 数据库设计原则 + print("\n1. 数据库设计原则:") + + design_principles = { + "第一范式 (1NF)": { + "定义": "每个字段都是原子性的,不可再分", + "示例": "将'姓名'字段拆分为'姓'和'名'", + "好处": "避免数据冗余,便于查询和维护" + }, + "第二范式 (2NF)": { + "定义": "满足1NF,且非主键字段完全依赖于主键", + "示例": "订单表中商品信息应该单独建表", + "好处": "减少数据冗余,提高数据一致性" + }, + "第三范式 (3NF)": { + "定义": "满足2NF,且非主键字段不依赖于其他非主键字段", + "示例": "员工表中部门信息应该单独建表", + "好处": "进一步减少冗余,提高数据完整性" + } + } + + for principle, details in design_principles.items(): + print(f"\n {principle}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 2. 命名规范 + print("\n2. 命名规范:") + + naming_conventions = { + "表名": { + "规则": "使用复数形式,小写字母,下划线分隔", + "示例": "users, user_profiles, order_items", + "避免": "User, userProfile, OrderItem" + }, + "字段名": { + "规则": "使用小写字母,下划线分隔,见名知意", + "示例": "user_id, created_at, email_address", + "避免": "userId, createdAt, email" + }, + "索引名": { + "规则": "idx_表名_字段名", + "示例": "idx_users_email, idx_orders_created_at", + "避免": "index1, user_index" + }, + "外键名": { + "规则": "fk_表名_引用表名", + "示例": "fk_orders_users, fk_order_items_products", + "避免": "foreign_key1, user_fk" + } + } + + for category, details in naming_conventions.items(): + print(f"\n {category}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 3. 数据类型选择 + print("\n3. 数据类型选择:") + + data_type_guidelines = { + "整数类型": { + "TINYINT": "0-255,适用于状态、类型等小范围值", + "INT": "适用于ID、数量等常规整数", + "BIGINT": "适用于大数值,如时间戳、大ID" + }, + "字符串类型": { + "CHAR": "固定长度,适用于长度固定的字段如手机号", + "VARCHAR": "可变长度,适用于姓名、邮箱等", + "TEXT": "大文本,适用于文章内容、描述等" + }, + "时间类型": { + "DATE": "仅日期,如生日", + "DATETIME": "日期和时间,如创建时间", + "TIMESTAMP": "时间戳,自动更新" + }, + "数值类型": { + "DECIMAL": "精确小数,适用于金额", + "FLOAT/DOUBLE": "浮点数,适用于科学计算" + } + } + + for category, types in data_type_guidelines.items(): + print(f"\n {category}:") + for type_name, usage in types.items(): + print(f" {type_name}: {usage}") + + # 4. 索引设计 + print("\n4. 索引设计原则:") + + index_guidelines = [ + "为经常用于WHERE条件的字段创建索引", + "为经常用于JOIN的字段创建索引", + "为经常用于ORDER BY的字段创建索引", + "避免在小表上创建过多索引", + "避免在频繁更新的字段上创建索引", + "考虑创建复合索引来优化多字段查询", + "定期分析和优化索引使用情况" + ] + + for i, guideline in enumerate(index_guidelines, 1): + print(f" {i}. {guideline}") + + # 5. 示例:电商系统数据库设计 + print("\n5. 电商系统数据库设计示例:") + + ecommerce_tables = { + "users": { + "字段": ["id", "username", "email", "password_hash", "phone", "created_at", "updated_at"], + "索引": ["idx_users_email", "idx_users_username"], + "说明": "用户基本信息表" + }, + "user_profiles": { + "字段": ["user_id", "first_name", "last_name", "birth_date", "gender", "avatar_url"], + "索引": ["idx_user_profiles_user_id"], + "说明": "用户详细信息表" + }, + "categories": { + "字段": ["id", "name", "parent_id", "sort_order", "is_active"], + "索引": ["idx_categories_parent_id"], + "说明": "商品分类表(支持层级)" + }, + "products": { + "字段": ["id", "name", "description", "price", "stock", "category_id", "created_at"], + "索引": ["idx_products_category_id", "idx_products_price"], + "说明": "商品信息表" + }, + "orders": { + "字段": ["id", "user_id", "total_amount", "status", "created_at", "updated_at"], + "索引": ["idx_orders_user_id", "idx_orders_status", "idx_orders_created_at"], + "说明": "订单主表" + }, + "order_items": { + "字段": ["id", "order_id", "product_id", "quantity", "price", "subtotal"], + "索引": ["idx_order_items_order_id", "idx_order_items_product_id"], + "说明": "订单明细表" + } + } + + for table_name, details in ecommerce_tables.items(): + print(f"\n {table_name}:") + print(f" 说明: {details['说明']}") + print(f" 字段: {', '.join(details['字段'])}") + print(f" 索引: {', '.join(details['索引'])}") + +# 运行数据库设计演示 +database_design_demo() +``` + +### 5.2 性能优化策略 + +```python +def database_performance_demo(): + """数据库性能优化演示""" + print("=== 数据库性能优化策略 ===") + + # 1. 查询优化 + print("\n1. 查询优化策略:") + + query_optimization = { + "使用索引": { + "原则": "为WHERE、JOIN、ORDER BY字段创建合适的索引", + "示例": "CREATE INDEX idx_users_email ON users(email)", + "注意": "避免过多索引,影响写入性能" + }, + "避免SELECT *": { + "原则": "只查询需要的字段", + "好例子": "SELECT id, name FROM users", + "坏例子": "SELECT * FROM users" + }, + "使用LIMIT": { + "原则": "限制查询结果数量", + "示例": "SELECT * FROM products LIMIT 10 OFFSET 20", + "好处": "减少内存使用和网络传输" + }, + "优化JOIN": { + "原则": "使用合适的JOIN类型,确保JOIN字段有索引", + "示例": "SELECT u.name, p.title FROM users u INNER JOIN posts p ON u.id = p.user_id", + "注意": "避免不必要的JOIN操作" + } + } + + for strategy, details in query_optimization.items(): + print(f"\n {strategy}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 2. 索引优化 + print("\n2. 索引优化技巧:") + + index_optimization = [ + "复合索引:将多个常一起查询的字段组合成复合索引", + "前缀索引:对于长字符串字段,使用前缀索引节省空间", + "覆盖索引:让索引包含查询所需的所有字段", + "部分索引:只为满足特定条件的行创建索引", + "定期维护:使用ANALYZE TABLE更新索引统计信息" + ] + + for i, tip in enumerate(index_optimization, 1): + print(f" {i}. {tip}") + + # 3. 连接池优化 + print("\n3. 连接池优化:") + + connection_pool_tips = { + "合理设置池大小": "根据应用并发量设置合适的连接池大小", + "连接超时设置": "设置合理的连接超时和空闲超时时间", + "连接验证": "启用连接验证,确保连接可用性", + "监控连接使用": "监控连接池使用情况,及时调整配置" + } + + for tip, description in connection_pool_tips.items(): + print(f" • {tip}: {description}") + + # 4. 缓存策略 + print("\n4. 缓存策略:") + + caching_strategies = { + "查询结果缓存": { + "适用场景": "频繁查询且数据变化不频繁", + "实现方式": "Redis、Memcached", + "注意事项": "设置合理的过期时间" + }, + "对象缓存": { + "适用场景": "复杂对象的构建成本较高", + "实现方式": "应用层缓存", + "注意事项": "注意缓存一致性" + }, + "页面缓存": { + "适用场景": "静态或半静态页面", + "实现方式": "CDN、反向代理", + "注意事项": "处理动态内容" + } + } + + for strategy, details in caching_strategies.items(): + print(f"\n {strategy}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 5. 分库分表策略 + print("\n5. 分库分表策略:") + + sharding_strategies = { + "垂直分库": "按业务模块分离数据库", + "水平分库": "按数据量分离数据库", + "垂直分表": "按字段使用频率分离表", + "水平分表": "按数据量或时间分离表" + } + + for strategy, description in sharding_strategies.items(): + print(f" • {strategy}: {description}") + + # 6. 监控和诊断 + print("\n6. 性能监控指标:") + + monitoring_metrics = [ + "查询响应时间", + "慢查询日志", + "连接数使用情况", + "缓存命中率", + "磁盘I/O使用率", + "CPU和内存使用率", + "锁等待时间", + "死锁检测" + ] + + for metric in monitoring_metrics: + print(f" • {metric}") + + # 7. 性能测试示例 + print("\n7. 性能测试示例:") + + performance_test_example = ''' + # 使用Python进行简单的性能测试 + import time + import sqlite3 + + def performance_test(): + conn = sqlite3.connect(':memory:') + cursor = conn.cursor() + + # 创建测试表 + cursor.execute(''' + CREATE TABLE test_table ( + id INTEGER PRIMARY KEY, + name TEXT, + value INTEGER + ) + ''') + + # 测试插入性能 + start_time = time.time() + for i in range(10000): + cursor.execute("INSERT INTO test_table (name, value) VALUES (?, ?)", + (f'name_{i}', i)) + conn.commit() + insert_time = time.time() - start_time + + # 测试查询性能 + start_time = time.time() + cursor.execute("SELECT * FROM test_table WHERE value > 5000") + results = cursor.fetchall() + query_time = time.time() - start_time + + print(f"插入10000条记录耗时: {insert_time:.3f}秒") + print(f"查询耗时: {query_time:.3f}秒") + print(f"查询结果数量: {len(results)}") + + conn.close() + ''' + + print(" 性能测试代码示例:") + print(performance_test_example) + +# 运行数据库性能优化演示 +database_performance_demo() +``` + +## 6. 实际应用案例 + +### 6.1 用户管理系统 + +```python +def user_management_system_demo(): + """用户管理系统演示""" + print("=== 用户管理系统演示 ===") + + import sqlite3 + import hashlib + import datetime + import json + + # 1. 数据库初始化 + print("\n1. 初始化用户管理数据库:") + + conn = sqlite3.connect(':memory:') + cursor = conn.cursor() + + # 创建用户表 + cursor.execute(''' + CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(64) NOT NULL, + salt VARCHAR(32) NOT NULL, + status INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME, + login_count INTEGER DEFAULT 0 + ) + ''') + + # 创建用户资料表 + cursor.execute(''' + CREATE TABLE user_profiles ( + user_id INTEGER PRIMARY KEY, + first_name VARCHAR(50), + last_name VARCHAR(50), + phone VARCHAR(20), + birth_date DATE, + avatar_url VARCHAR(200), + bio TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + ''') + + # 创建登录日志表 + cursor.execute(''' + CREATE TABLE login_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + ip_address VARCHAR(45), + user_agent TEXT, + login_time DATETIME DEFAULT CURRENT_TIMESTAMP, + success INTEGER DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + ''') + + # 创建索引 + cursor.execute('CREATE INDEX idx_users_email ON users(email)') + cursor.execute('CREATE INDEX idx_users_username ON users(username)') + cursor.execute('CREATE INDEX idx_login_logs_user_id ON login_logs(user_id)') + cursor.execute('CREATE INDEX idx_login_logs_time ON login_logs(login_time)') + + print(" ✓ 创建用户管理相关表和索引") + + # 2. 用户注册功能 + print("\n2. 用户注册功能:") + + def register_user(username, email, password, first_name=None, last_name=None): + """用户注册""" + try: + # 生成盐值和密码哈希 + import secrets + salt = secrets.token_hex(16) + password_hash = hashlib.sha256((password + salt).encode()).hexdigest() + + # 插入用户基本信息 + cursor.execute(''' + INSERT INTO users (username, email, password_hash, salt) + VALUES (?, ?, ?, ?) + ''', (username, email, password_hash, salt)) + + user_id = cursor.lastrowid + + # 插入用户资料 + cursor.execute(''' + INSERT INTO user_profiles (user_id, first_name, last_name) + VALUES (?, ?, ?) + ''', (user_id, first_name, last_name)) + + conn.commit() + return {'success': True, 'user_id': user_id, 'message': '注册成功'} + + except sqlite3.IntegrityError as e: + conn.rollback() + if 'username' in str(e): + return {'success': False, 'message': '用户名已存在'} + elif 'email' in str(e): + return {'success': False, 'message': '邮箱已被注册'} + else: + return {'success': False, 'message': '注册失败'} + except Exception as e: + conn.rollback() + return {'success': False, 'message': f'系统错误: {str(e)}'} + + # 注册测试用户 + users_to_register = [ + ('张三', 'zhangsan@example.com', 'password123', '三', '张'), + ('李四', 'lisi@example.com', 'password456', '四', '李'), + ('王五', 'wangwu@example.com', 'password789', '五', '王') + ] + + for username, email, password, first_name, last_name in users_to_register: + result = register_user(username, email, password, first_name, last_name) + print(f" 注册用户 {username}: {result['message']}") + + # 3. 用户登录功能 + print("\n3. 用户登录功能:") + + def login_user(username_or_email, password, ip_address='127.0.0.1', user_agent='Python Client'): + """用户登录""" + try: + # 查找用户 + cursor.execute(''' + SELECT id, username, email, password_hash, salt, status + FROM users + WHERE username = ? OR email = ? + ''', (username_or_email, username_or_email)) + + user = cursor.fetchone() + + if not user: + # 记录失败登录 + cursor.execute(''' + INSERT INTO login_logs (user_id, ip_address, user_agent, success) + VALUES (NULL, ?, ?, 0) + ''', (ip_address, user_agent)) + conn.commit() + return {'success': False, 'message': '用户不存在'} + + user_id, username, email, stored_hash, salt, status = user + + # 检查用户状态 + if status != 1: + return {'success': False, 'message': '账户已被禁用'} + + # 验证密码 + password_hash = hashlib.sha256((password + salt).encode()).hexdigest() + + if password_hash != stored_hash: + # 记录失败登录 + cursor.execute(''' + INSERT INTO login_logs (user_id, ip_address, user_agent, success) + VALUES (?, ?, ?, 0) + ''', (user_id, ip_address, user_agent)) + conn.commit() + return {'success': False, 'message': '密码错误'} + + # 更新用户登录信息 + cursor.execute(''' + UPDATE users + SET last_login = CURRENT_TIMESTAMP, + login_count = login_count + 1, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (user_id,)) + + # 记录成功登录 + cursor.execute(''' + INSERT INTO login_logs (user_id, ip_address, user_agent, success) + VALUES (?, ?, ?, 1) + ''', (user_id, ip_address, user_agent)) + + conn.commit() + + return { + 'success': True, + 'message': '登录成功', + 'user': { + 'id': user_id, + 'username': username, + 'email': email + } + } + + except Exception as e: + conn.rollback() + return {'success': False, 'message': f'登录失败: {str(e)}'} + + # 测试登录 + login_tests = [ + ('张三', 'password123'), + ('lisi@example.com', 'password456'), + ('王五', 'wrongpassword'), + ('nonexistent', 'password') + ] + + for username, password in login_tests: + result = login_user(username, password) + print(f" 登录测试 {username}: {result['message']}") + + # 4. 用户信息管理 + print("\n4. 用户信息管理:") + + def get_user_profile(user_id): + """获取用户完整信息""" + cursor.execute(''' + SELECT u.id, u.username, u.email, u.status, u.created_at, + u.last_login, u.login_count, + p.first_name, p.last_name, p.phone, p.birth_date, p.bio + FROM users u + LEFT JOIN user_profiles p ON u.id = p.user_id + WHERE u.id = ? + ''', (user_id,)) + + result = cursor.fetchone() + if result: + return { + 'id': result[0], + 'username': result[1], + 'email': result[2], + 'status': result[3], + 'created_at': result[4], + 'last_login': result[5], + 'login_count': result[6], + 'first_name': result[7], + 'last_name': result[8], + 'phone': result[9], + 'birth_date': result[10], + 'bio': result[11] + } + return None + + def update_user_profile(user_id, **kwargs): + """更新用户资料""" + try: + # 更新用户基本信息 + if 'email' in kwargs: + cursor.execute(''' + UPDATE users SET email = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (kwargs['email'], user_id)) + + # 更新用户详细资料 + profile_fields = ['first_name', 'last_name', 'phone', 'birth_date', 'bio'] + profile_updates = {k: v for k, v in kwargs.items() if k in profile_fields} + + if profile_updates: + set_clause = ', '.join([f'{k} = ?' for k in profile_updates.keys()]) + values = list(profile_updates.values()) + [user_id] + + cursor.execute(f''' + UPDATE user_profiles SET {set_clause} + WHERE user_id = ? + ''', values) + + conn.commit() + return {'success': True, 'message': '资料更新成功'} + + except Exception as e: + conn.rollback() + return {'success': False, 'message': f'更新失败: {str(e)}'} + + # 测试用户信息管理 + user_profile = get_user_profile(1) + if user_profile: + print(f" 用户信息: {user_profile['username']} ({user_profile['email']})") + print(f" 登录次数: {user_profile['login_count']}") + + # 更新用户资料 + update_result = update_user_profile(1, phone='13800138000', bio='Python开发者') + print(f" 更新资料: {update_result['message']}") + + # 5. 统计分析 + print("\n5. 用户统计分析:") + + def get_user_statistics(): + """获取用户统计信息""" + stats = {} + + # 总用户数 + cursor.execute('SELECT COUNT(*) FROM users') + stats['total_users'] = cursor.fetchone()[0] + + # 活跃用户数(状态为1) + cursor.execute('SELECT COUNT(*) FROM users WHERE status = 1') + stats['active_users'] = cursor.fetchone()[0] + + # 今日登录用户数 + cursor.execute(''' + SELECT COUNT(DISTINCT user_id) + FROM login_logs + WHERE DATE(login_time) = DATE('now') AND success = 1 + ''') + stats['today_logins'] = cursor.fetchone()[0] + + # 平均登录次数 + cursor.execute('SELECT AVG(login_count) FROM users WHERE login_count > 0') + avg_logins = cursor.fetchone()[0] + stats['avg_login_count'] = round(avg_logins, 2) if avg_logins else 0 + + # 最近注册的用户 + cursor.execute(''' + SELECT username, created_at + FROM users + ORDER BY created_at DESC + LIMIT 3 + ''') + stats['recent_users'] = cursor.fetchall() + + return stats + + stats = get_user_statistics() + print(f" 总用户数: {stats['total_users']}") + print(f" 活跃用户数: {stats['active_users']}") + print(f" 今日登录: {stats['today_logins']}") + print(f" 平均登录次数: {stats['avg_login_count']}") + print(" 最近注册用户:") + for username, created_at in stats['recent_users']: + print(f" {username} - {created_at}") + + # 6. 清理资源 + conn.close() + print("\n ✓ 用户管理系统演示完成") + +# 运行用户管理系统演示 +user_management_system_demo() +``` + +### 6.2 数据分析系统 + +```python +def data_analysis_system_demo(): + """数据分析系统演示""" + print("=== 数据分析系统演示 ===") + + import sqlite3 + import random + import datetime + from datetime import timedelta + + # 1. 创建销售数据分析数据库 + print("\n1. 创建销售数据分析数据库:") + + conn = sqlite3.connect(':memory:') + cursor = conn.cursor() + + # 创建产品表 + cursor.execute(''' + CREATE TABLE products ( + id INTEGER PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10,2), + cost DECIMAL(10,2), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建销售记录表 + cursor.execute(''' + CREATE TABLE sales ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER, + quantity INTEGER, + unit_price DECIMAL(10,2), + total_amount DECIMAL(10,2), + sale_date DATE, + customer_id INTEGER, + region VARCHAR(50), + FOREIGN KEY (product_id) REFERENCES products(id) + ) + ''') + + # 创建客户表 + cursor.execute(''' + CREATE TABLE customers ( + id INTEGER PRIMARY KEY, + name VARCHAR(100), + email VARCHAR(100), + region VARCHAR(50), + registration_date DATE + ) + ''') + + # 创建索引 + cursor.execute('CREATE INDEX idx_sales_date ON sales(sale_date)') + cursor.execute('CREATE INDEX idx_sales_product ON sales(product_id)') + cursor.execute('CREATE INDEX idx_sales_customer ON sales(customer_id)') + cursor.execute('CREATE INDEX idx_sales_region ON sales(region)') + + print(" ✓ 创建销售分析相关表和索引") + + # 2. 生成测试数据 + print("\n2. 生成测试数据:") + + # 插入产品数据 + products = [ + (1, '笔记本电脑', '电子产品', 5999.00, 4500.00), + (2, '无线鼠标', '电子产品', 199.00, 120.00), + (3, '机械键盘', '电子产品', 399.00, 250.00), + (4, '显示器', '电子产品', 1299.00, 900.00), + (5, '办公椅', '办公用品', 899.00, 600.00), + (6, '办公桌', '办公用品', 1599.00, 1000.00) + ] + + cursor.executemany(''' + INSERT INTO products (id, name, category, price, cost) + VALUES (?, ?, ?, ?, ?) + ''', products) + + # 插入客户数据 + customers = [ + (1, '张三公司', 'zhangsan@company.com', '北京', '2023-01-15'), + (2, '李四企业', 'lisi@enterprise.com', '上海', '2023-02-20'), + (3, '王五集团', 'wangwu@group.com', '广州', '2023-03-10'), + (4, '赵六科技', 'zhaoliu@tech.com', '深圳', '2023-04-05'), + (5, '钱七贸易', 'qianqi@trade.com', '杭州', '2023-05-12') + ] + + cursor.executemany(''' + INSERT INTO customers (id, name, email, region, registration_date) + VALUES (?, ?, ?, ?, ?) + ''', customers) + + # 生成销售数据 + regions = ['北京', '上海', '广州', '深圳', '杭州'] + start_date = datetime.date(2023, 1, 1) + end_date = datetime.date(2023, 12, 31) + + sales_data = [] + for _ in range(500): # 生成500条销售记录 + product_id = random.randint(1, 6) + customer_id = random.randint(1, 5) + quantity = random.randint(1, 10) + + # 获取产品价格 + cursor.execute('SELECT price FROM products WHERE id = ?', (product_id,)) + unit_price = cursor.fetchone()[0] + + # 添加一些价格波动 + unit_price = float(unit_price) * random.uniform(0.9, 1.1) + total_amount = unit_price * quantity + + # 随机日期 + days_diff = (end_date - start_date).days + random_days = random.randint(0, days_diff) + sale_date = start_date + timedelta(days=random_days) + + region = random.choice(regions) + + sales_data.append(( + product_id, quantity, unit_price, total_amount, + sale_date.strftime('%Y-%m-%d'), customer_id, region + )) + + cursor.executemany(''' + INSERT INTO sales (product_id, quantity, unit_price, total_amount, sale_date, customer_id, region) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', sales_data) + + conn.commit() + print(f" ✓ 生成 {len(products)} 个产品, {len(customers)} 个客户, {len(sales_data)} 条销售记录") + + # 3. 销售数据分析 + print("\n3. 销售数据分析:") + + # 总销售额 + cursor.execute('SELECT SUM(total_amount) FROM sales') + total_sales = cursor.fetchone()[0] + print(f" 总销售额: ¥{total_sales:,.2f}") + + # 按月份统计销售额 + cursor.execute(''' + SELECT strftime('%Y-%m', sale_date) as month, + SUM(total_amount) as monthly_sales, + COUNT(*) as order_count + FROM sales + GROUP BY strftime('%Y-%m', sale_date) + ORDER BY month + ''') + + print("\n 月度销售统计:") + monthly_data = cursor.fetchall() + for month, sales, orders in monthly_data[:6]: # 显示前6个月 + print(f" {month}: ¥{sales:,.2f} ({orders} 笔订单)") + + # 按产品类别统计 + cursor.execute(''' + SELECT p.category, + SUM(s.total_amount) as category_sales, + SUM(s.quantity) as total_quantity, + COUNT(s.id) as order_count + FROM sales s + JOIN products p ON s.product_id = p.id + GROUP BY p.category + ORDER BY category_sales DESC + ''') + + print("\n 产品类别销售统计:") + for category, sales, quantity, orders in cursor.fetchall(): + print(f" {category}: ¥{sales:,.2f} (数量: {quantity}, 订单: {orders})") + + # 按地区统计 + cursor.execute(''' + SELECT region, + SUM(total_amount) as region_sales, + COUNT(*) as order_count, + AVG(total_amount) as avg_order_value + FROM sales + GROUP BY region + ORDER BY region_sales DESC + ''') + + print("\n 地区销售统计:") + for region, sales, orders, avg_value in cursor.fetchall(): + print(f" {region}: ¥{sales:,.2f} ({orders} 笔, 平均: ¥{avg_value:.2f})") + + # 4. 高级分析查询 + print("\n4. 高级分析查询:") + + # 最畅销产品 + cursor.execute(''' + SELECT p.name, + SUM(s.quantity) as total_sold, + SUM(s.total_amount) as total_revenue, + COUNT(s.id) as order_count + FROM sales s + JOIN products p ON s.product_id = p.id + GROUP BY p.id, p.name + ORDER BY total_sold DESC + LIMIT 3 + ''') + + print(" 最畅销产品 TOP 3:") + for name, sold, revenue, orders in cursor.fetchall(): + print(f" {name}: 销量 {sold}, 收入 ¥{revenue:,.2f}, 订单 {orders}") + + # 客户价值分析 + cursor.execute(''' + SELECT c.name, + SUM(s.total_amount) as customer_value, + COUNT(s.id) as order_count, + AVG(s.total_amount) as avg_order_value, + MAX(s.sale_date) as last_purchase + FROM sales s + JOIN customers c ON s.customer_id = c.id + GROUP BY c.id, c.name + ORDER BY customer_value DESC + LIMIT 3 + ''') + + print("\n 高价值客户 TOP 3:") + for name, value, orders, avg_value, last_purchase in cursor.fetchall(): + print(f" {name}: 总价值 ¥{value:,.2f}, {orders} 笔订单, 最后购买 {last_purchase}") + + # 季度趋势分析 + cursor.execute(''' + SELECT + CASE + WHEN strftime('%m', sale_date) IN ('01','02','03') THEN 'Q1' + WHEN strftime('%m', sale_date) IN ('04','05','06') THEN 'Q2' + WHEN strftime('%m', sale_date) IN ('07','08','09') THEN 'Q3' + ELSE 'Q4' + END as quarter, + SUM(total_amount) as quarterly_sales, + COUNT(*) as order_count + FROM sales + WHERE strftime('%Y', sale_date) = '2023' + GROUP BY quarter + ORDER BY quarter + ''') + + print("\n 2023年季度销售趋势:") + for quarter, sales, orders in cursor.fetchall(): + print(f" {quarter}: ¥{sales:,.2f} ({orders} 笔订单)") + + # 5. 利润分析 + print("\n5. 利润分析:") + + cursor.execute(''' + SELECT p.name, + SUM(s.quantity) as total_sold, + SUM(s.total_amount) as revenue, + SUM(s.quantity * p.cost) as total_cost, + SUM(s.total_amount) - SUM(s.quantity * p.cost) as profit, + (SUM(s.total_amount) - SUM(s.quantity * p.cost)) / SUM(s.total_amount) * 100 as profit_margin + FROM sales s + JOIN products p ON s.product_id = p.id + GROUP BY p.id, p.name + ORDER BY profit DESC + ''') + + print(" 产品利润分析:") + for name, sold, revenue, cost, profit, margin in cursor.fetchall(): + print(f" {name}: 利润 ¥{profit:,.2f}, 利润率 {margin:.1f}%") + + # 6. 清理资源 + conn.close() + print("\n ✓ 数据分析系统演示完成") + +# 运行数据分析系统演示 +data_analysis_system_demo() +``` + +## 7. 最佳实践和安全 + +### 7.1 数据库安全最佳实践 + +```python +def database_security_demo(): + """数据库安全最佳实践演示""" + print("=== 数据库安全最佳实践 ===") + + # 1. SQL注入防护 + print("\n1. SQL注入防护:") + + import sqlite3 + + conn = sqlite3.connect(':memory:') + cursor = conn.cursor() + + # 创建测试表 + cursor.execute(''' + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + username VARCHAR(50), + password VARCHAR(100) + ) + ''') + + cursor.execute("INSERT INTO users (username, password) VALUES ('admin', 'secret123')") + cursor.execute("INSERT INTO users (username, password) VALUES ('user1', 'password456')") + conn.commit() + + # 错误的做法(容易SQL注入) + def unsafe_login(username, password): + """不安全的登录方法(仅用于演示)""" + query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" + print(f" ⚠️ 不安全的查询: {query}") + # 注意:这里只是演示,实际不执行 + return "不安全的查询方式" + + # 正确的做法(使用参数化查询) + def safe_login(username, password): + """安全的登录方法""" + cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)) + result = cursor.fetchone() + return result is not None + + print(" 安全登录演示:") + + # 正常登录 + if safe_login('admin', 'secret123'): + print(" ✓ 正常登录成功") + + # 模拟SQL注入尝试 + malicious_input = "admin'; DROP TABLE users; --" + unsafe_login(malicious_input, "anything") + print(" ✓ 参数化查询防止了SQL注入") + + # 2. 密码安全 + print("\n2. 密码安全处理:") + + import hashlib + import secrets + import bcrypt + + def hash_password_simple(password): + """简单的密码哈希(不推荐用于生产)""" + salt = secrets.token_hex(16) + password_hash = hashlib.sha256((password + salt).encode()).hexdigest() + return salt, password_hash + + def hash_password_bcrypt(password): + """使用bcrypt哈希密码(推荐)""" + try: + # 生成盐并哈希密码 + salt = bcrypt.gensalt() + password_hash = bcrypt.hashpw(password.encode('utf-8'), salt) + return password_hash.decode('utf-8') + except ImportError: + print(" ⚠️ bcrypt未安装,使用简单哈希方法") + return hash_password_simple(password) + + def verify_password_bcrypt(password, hashed): + """验证bcrypt哈希密码""" + try: + return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8')) + except ImportError: + return False + + # 演示密码哈希 + test_password = "mySecurePassword123!" + + # 简单哈希方法 + salt, simple_hash = hash_password_simple(test_password) + print(f" 简单哈希: {simple_hash[:20]}...") + + # bcrypt方法(如果可用) + try: + bcrypt_hash = hash_password_bcrypt(test_password) + print(f" bcrypt哈希: {bcrypt_hash[:20]}...") + + # 验证密码 + if verify_password_bcrypt(test_password, bcrypt_hash): + print(" ✓ 密码验证成功") + except: + print(" ⚠️ bcrypt不可用,建议安装: pip install bcrypt") + + # 3. 数据库连接安全 + print("\n3. 数据库连接安全:") + + connection_security_tips = { + "使用SSL/TLS": "确保数据库连接使用加密传输", + "最小权限原则": "为应用程序创建专用数据库用户,只授予必要权限", + "连接字符串保护": "不要在代码中硬编码数据库凭据", + "连接池配置": "合理配置连接池,避免连接泄露", + "网络隔离": "将数据库服务器放在私有网络中", + "定期更新": "保持数据库软件和驱动程序最新" + } + + for tip, description in connection_security_tips.items(): + print(f" • {tip}: {description}") + + # 4. 数据加密 + print("\n4. 敏感数据加密:") + + from cryptography.fernet import Fernet + + try: + # 生成加密密钥 + key = Fernet.generate_key() + cipher_suite = Fernet(key) + + # 加密敏感数据 + sensitive_data = "用户身份证号: 123456789012345678" + encrypted_data = cipher_suite.encrypt(sensitive_data.encode()) + + print(f" 原始数据: {sensitive_data}") + print(f" 加密数据: {encrypted_data[:30]}...") + + # 解密数据 + decrypted_data = cipher_suite.decrypt(encrypted_data).decode() + print(f" 解密数据: {decrypted_data}") + print(" ✓ 数据加密/解密成功") + + except ImportError: + print(" ⚠️ cryptography库未安装,建议安装: pip install cryptography") + + # 使用简单的base64编码作为演示(不安全) + import base64 + sensitive_data = "演示数据" + encoded = base64.b64encode(sensitive_data.encode()).decode() + decoded = base64.b64decode(encoded).decode() + print(f" Base64编码演示: {encoded}") + print(" ⚠️ Base64不是加密,仅用于演示") + + # 5. 审计和日志 + print("\n5. 数据库审计和日志:") + + import logging + from datetime import datetime + + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + def log_database_operation(operation, table, user_id=None, details=None): + """记录数据库操作日志""" + log_entry = { + 'timestamp': datetime.now().isoformat(), + 'operation': operation, + 'table': table, + 'user_id': user_id, + 'details': details + } + logging.info(f"DB Operation: {log_entry}") + + # 演示日志记录 + log_database_operation('SELECT', 'users', user_id=1, details='Login attempt') + log_database_operation('UPDATE', 'user_profiles', user_id=1, details='Profile update') + log_database_operation('DELETE', 'sessions', user_id=1, details='Logout') + + print(" ✓ 数据库操作日志记录完成") + + # 6. 备份和恢复 + print("\n6. 数据备份策略:") + + backup_strategies = { + "定期备份": "设置自动化的定期备份任务", + "增量备份": "结合全量备份和增量备份", + "异地备份": "将备份存储在不同的地理位置", + "备份测试": "定期测试备份的完整性和可恢复性", + "版本控制": "保留多个备份版本", + "加密备份": "对备份文件进行加密保护" + } + + for strategy, description in backup_strategies.items(): + print(f" • {strategy}: {description}") + + # 简单的SQLite备份演示 + def backup_sqlite_database(source_db, backup_path): + """SQLite数据库备份""" + try: + import shutil + shutil.copy2(source_db, backup_path) + return True + except Exception as e: + print(f"备份失败: {e}") + return False + + print(" ✓ 数据库备份策略说明完成") + + # 清理资源 + conn.close() + print("\n ✓ 数据库安全最佳实践演示完成") + +# 运行数据库安全演示 +database_security_demo() +``` + +## 8. 学习建议和总结 + +### 8.1 学习路径 + +```python +def learning_path_guide(): + """数据库学习路径指南""" + print("=== Python数据库编程学习路径 ===") + + learning_stages = { + "初级阶段 (1-2周)": { + "目标": "掌握基础数据库操作", + "内容": [ + "理解数据库基本概念", + "学习SQL基础语法", + "掌握Python DB-API 2.0规范", + "练习SQLite基础操作", + "学习CRUD操作" + ], + "实践项目": "简单的联系人管理系统" + }, + "中级阶段 (2-3周)": { + "目标": "掌握高级数据库功能", + "内容": [ + "学习事务处理", + "掌握连接池使用", + "理解索引和查询优化", + "学习MySQL/PostgreSQL操作", + "掌握数据库设计原则" + ], + "实践项目": "博客系统或电商系统" + }, + "高级阶段 (3-4周)": { + "目标": "掌握ORM和高级特性", + "内容": [ + "学习SQLAlchemy ORM", + "掌握数据库迁移", + "学习性能优化技巧", + "理解数据库安全", + "掌握分布式数据库概念" + ], + "实践项目": "完整的Web应用后端" + }, + "专家阶段 (持续学习)": { + "目标": "深入理解数据库架构", + "内容": [ + "学习数据库内部原理", + "掌握分库分表策略", + "学习NoSQL数据库", + "理解大数据处理", + "掌握数据库运维" + ], + "实践项目": "高并发分布式系统" + } + } + + for stage, details in learning_stages.items(): + print(f"\n{stage}:") + print(f" 目标: {details['目标']}") + print(" 学习内容:") + for content in details['内容']: + print(f" • {content}") + print(f" 实践项目: {details['实践项目']}") + +# 运行学习路径指南 +learning_path_guide() +``` + +### 8.2 最佳实践总结 + +```python +def best_practices_summary(): + """数据库编程最佳实践总结""" + print("=== 数据库编程最佳实践总结 ===") + + # 1. 代码组织 + print("\n1. 代码组织最佳实践:") + + code_organization = [ + "使用数据访问对象(DAO)模式分离数据访问逻辑", + "创建数据库连接管理器统一管理连接", + "使用配置文件管理数据库连接参数", + "实现统一的异常处理机制", + "编写可重用的数据库操作工具函数", + "使用类型提示提高代码可读性", + "编写完整的文档和注释" + ] + + for i, practice in enumerate(code_organization, 1): + print(f" {i}. {practice}") + + # 2. 性能优化 + print("\n2. 性能优化最佳实践:") + + performance_tips = [ + "使用连接池避免频繁创建连接", + "合理使用索引优化查询性能", + "避免N+1查询问题", + "使用批量操作处理大量数据", + "实现查询结果缓存", + "定期分析和优化慢查询", + "使用分页避免大结果集" + ] + + for i, tip in enumerate(performance_tips, 1): + print(f" {i}. {tip}") + + # 3. 安全实践 + print("\n3. 安全最佳实践:") + + security_practices = [ + "始终使用参数化查询防止SQL注入", + "对敏感数据进行加密存储", + "使用强密码哈希算法", + "实施最小权限原则", + "启用数据库连接加密", + "定期备份和测试恢复", + "记录和监控数据库访问日志" + ] + + for i, practice in enumerate(security_practices, 1): + print(f" {i}. {practice}") + + # 4. 错误处理 + print("\n4. 错误处理最佳实践:") + + error_handling = [ + "使用try-catch块处理数据库异常", + "实现事务回滚机制", + "提供有意义的错误消息", + "记录详细的错误日志", + "实现重试机制处理临时故障", + "优雅地处理连接超时", + "避免向用户暴露敏感错误信息" + ] + + for i, practice in enumerate(error_handling, 1): + print(f" {i}. {practice}") + +# 运行最佳实践总结 +best_practices_summary() +``` + +### 8.3 常见陷阱和解决方案 + +```python +def common_pitfalls_and_solutions(): + """常见陷阱和解决方案""" + print("=== 常见陷阱和解决方案 ===") + + pitfalls = { + "连接泄露": { + "问题": "忘记关闭数据库连接导致连接池耗尽", + "解决方案": "使用with语句或try-finally确保连接关闭", + "示例": "with get_connection() as conn: # 自动关闭连接" + }, + "SQL注入": { + "问题": "直接拼接SQL字符串导致安全漏洞", + "解决方案": "使用参数化查询或ORM", + "示例": "cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))" + }, + "N+1查询": { + "问题": "在循环中执行查询导致性能问题", + "解决方案": "使用JOIN查询或预加载", + "示例": "使用SQLAlchemy的joinedload或selectinload" + }, + "事务管理": { + "问题": "忘记提交事务或处理回滚", + "解决方案": "使用上下文管理器自动管理事务", + "示例": "使用@contextmanager装饰器创建事务管理器" + }, + "字符编码": { + "问题": "中文字符显示乱码", + "解决方案": "确保数据库、连接和Python使用相同编码", + "示例": "连接字符串中指定charset=utf8mb4" + }, + "时区问题": { + "问题": "时间数据在不同时区显示错误", + "解决方案": "统一使用UTC时间或明确指定时区", + "示例": "使用datetime.utcnow()和时区转换" + } + } + + for pitfall, details in pitfalls.items(): + print(f"\n{pitfall}:") + print(f" 问题: {details['问题']}") + print(f" 解决方案: {details['解决方案']}") + print(f" 示例: {details['示例']}") + +# 运行常见陷阱和解决方案 +common_pitfalls_and_solutions() +``` + +### 8.4 本章总结 + +```python +def chapter_summary(): + """第20天学习总结""" + print("=== 第20天 - Python数据库访问学习总结 ===") + + summary_points = { + "核心概念": [ + "数据库基础知识和DB-API 2.0规范", + "SQLite、MySQL等数据库的连接和操作", + "SQL语句的执行和结果处理", + "事务管理和错误处理机制" + ], + "重要技能": [ + "使用sqlite3模块进行SQLite操作", + "使用mysql-connector-python操作MySQL", + "掌握SQLAlchemy ORM框架", + "实现数据库连接池和性能优化" + ], + "实践应用": [ + "用户管理系统的设计和实现", + "销售数据分析系统的构建", + "数据库安全和最佳实践", + "性能优化和监控策略" + ], + "进阶方向": [ + "学习NoSQL数据库(MongoDB, Redis)", + "掌握数据库集群和分布式架构", + "深入理解数据库内部原理", + "学习大数据处理技术" + ] + } + + for category, points in summary_points.items(): + print(f"\n{category}:") + for point in points: + print(f" • {point}") + + print("\n学习成果:") + achievements = [ + "✓ 掌握了Python数据库编程的基础知识", + "✓ 学会了使用多种数据库和ORM框架", + "✓ 理解了数据库设计和性能优化原则", + "✓ 具备了构建实际数据库应用的能力", + "✓ 了解了数据库安全和最佳实践" + ] + + for achievement in achievements: + print(f" {achievement}") + + print("\n下一步学习建议:") + next_steps = [ + "深入学习特定数据库的高级特性", + "实践更复杂的数据库应用项目", + "学习数据库运维和监控技能", + "探索大数据和分布式数据库技术", + "关注数据库技术的最新发展趋势" + ] + + for step in next_steps: + print(f" • {step}") + + print("\n恭喜你完成了Python数据库访问的学习!") + print("数据库是现代应用的核心,继续实践和深入学习将让你成为更优秀的开发者。") + +# 运行本章总结 +chapter_summary() +``` + +--- + +## 总结 + +通过第20天的学习,我们全面掌握了Python数据库访问的各个方面: + +1. **数据库基础** - 理解了数据库概念、DB-API规范和SQL基础 +2. **SQLite操作** - 掌握了轻量级数据库的使用和高级功能 +3. **MySQL操作** - 学会了企业级数据库的连接和操作 +4. **ORM框架** - 深入学习了SQLAlchemy的使用和高级特性 +5. **设计最佳实践** - 了解了数据库设计原则和性能优化策略 +6. **实际应用** - 通过用户管理和数据分析系统掌握了实践技能 +7. **安全实践** - 学习了数据库安全和最佳实践 + +数据库编程是后端开发的核心技能,掌握这些知识将为你的Python开发之路奠定坚实的基础。继续实践和深入学习,你将能够构建更加复杂和高效的数据库应用! + + + + + diff --git a/docs/Python/21.md b/docs/Python/21.md new file mode 100644 index 000000000..0ce498872 --- /dev/null +++ b/docs/Python/21.md @@ -0,0 +1,1902 @@ +--- +title: 第21天-Web开发 +author: 哪吒 +date: '2023-06-15' +--- + +# 第21天-Web开发 + +## 概述 + +Web开发是Python最重要的应用领域之一。Python拥有丰富的Web开发框架,从轻量级的Flask到功能完整的Django,为不同规模的Web应用提供了优秀的解决方案。今天我们将学习Python Web开发的基础知识和实践技能。 + +## 1. Web开发基础 + +### 1.1 Web开发概念 + +```python +def web_development_concepts(): + """Web开发基础概念演示""" + print("=== Web开发基础概念 ===") + + # 1. HTTP协议基础 + print("\n1. HTTP协议基础:") + + http_concepts = { + "HTTP方法": { + "GET": "获取资源,幂等操作", + "POST": "创建资源,非幂等操作", + "PUT": "更新资源,幂等操作", + "DELETE": "删除资源,幂等操作", + "PATCH": "部分更新资源", + "HEAD": "获取资源头信息", + "OPTIONS": "获取服务器支持的方法" + }, + "状态码": { + "2xx": "成功 (200 OK, 201 Created, 204 No Content)", + "3xx": "重定向 (301 Moved, 302 Found, 304 Not Modified)", + "4xx": "客户端错误 (400 Bad Request, 401 Unauthorized, 404 Not Found)", + "5xx": "服务器错误 (500 Internal Error, 502 Bad Gateway, 503 Service Unavailable)" + }, + "请求头": { + "Content-Type": "请求体的媒体类型", + "Authorization": "身份验证信息", + "User-Agent": "客户端信息", + "Accept": "客户端可接受的媒体类型", + "Cookie": "客户端存储的会话信息" + }, + "响应头": { + "Content-Type": "响应体的媒体类型", + "Set-Cookie": "设置客户端Cookie", + "Location": "重定向地址", + "Cache-Control": "缓存控制", + "Access-Control-Allow-Origin": "CORS跨域控制" + } + } + + for category, items in http_concepts.items(): + print(f"\n {category}:") + for key, value in items.items(): + print(f" {key}: {value}") + + # 2. Web架构模式 + print("\n2. Web架构模式:") + + architecture_patterns = { + "MVC (Model-View-Controller)": { + "Model": "数据模型,处理业务逻辑和数据访问", + "View": "视图层,负责用户界面展示", + "Controller": "控制器,处理用户输入和协调Model、View" + }, + "MTV (Model-Template-View)": { + "Model": "数据模型,与MVC中的Model相同", + "Template": "模板,负责页面渲染", + "View": "视图函数,处理请求逻辑" + }, + "RESTful API": { + "资源导向": "将数据和功能视为资源", + "统一接口": "使用标准HTTP方法操作资源", + "无状态": "每个请求包含完整信息", + "可缓存": "响应可以被缓存" + } + } + + for pattern, details in architecture_patterns.items(): + print(f"\n {pattern}:") + for component, description in details.items(): + print(f" {component}: {description}") + + # 3. 前后端交互 + print("\n3. 前后端交互方式:") + + interaction_methods = [ + "传统表单提交: 页面刷新,服务器渲染", + "AJAX请求: 异步数据交换,局部更新", + "WebSocket: 双向实时通信", + "Server-Sent Events: 服务器主动推送", + "GraphQL: 灵活的数据查询语言", + "gRPC: 高性能RPC框架" + ] + + for i, method in enumerate(interaction_methods, 1): + print(f" {i}. {method}") + + print("\n ✓ Web开发基础概念介绍完成") + +# 运行Web开发概念演示 +web_development_concepts() +``` + +### 1.2 Python Web框架概览 + +```python +def python_web_frameworks_overview(): + """Python Web框架概览""" + print("=== Python Web框架概览 ===") + + frameworks = { + "微框架": { + "Flask": { + "特点": "轻量级、灵活、易学习", + "适用场景": "小型应用、API服务、原型开发", + "核心组件": "路由、模板、请求处理", + "扩展性": "丰富的第三方扩展" + }, + "FastAPI": { + "特点": "现代、高性能、自动文档", + "适用场景": "API开发、微服务", + "核心组件": "类型提示、异步支持、自动验证", + "扩展性": "基于Starlette和Pydantic" + } + }, + "全栈框架": { + "Django": { + "特点": "功能完整、开箱即用、安全性高", + "适用场景": "大型应用、内容管理、企业级开发", + "核心组件": "ORM、管理后台、认证系统、模板引擎", + "扩展性": "丰富的内置功能和第三方包" + }, + "Pyramid": { + "特点": "灵活配置、可扩展、企业级", + "适用场景": "复杂应用、企业开发", + "核心组件": "路由、视图、模板、安全", + "扩展性": "高度可配置和可扩展" + } + }, + "异步框架": { + "Tornado": { + "特点": "异步非阻塞、高并发", + "适用场景": "实时应用、长连接服务", + "核心组件": "异步处理、WebSocket支持", + "扩展性": "内置异步库" + }, + "Sanic": { + "特点": "类Flask语法、异步支持", + "适用场景": "高性能API、异步应用", + "核心组件": "异步路由、中间件", + "扩展性": "基于asyncio" + } + } + } + + for category, framework_list in frameworks.items(): + print(f"\n{category}:") + for name, details in framework_list.items(): + print(f"\n {name}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 框架选择建议 + print("\n框架选择建议:") + + selection_guide = { + "初学者": "推荐Flask - 简单易学,概念清晰", + "快速原型": "推荐Flask或FastAPI - 开发效率高", + "企业应用": "推荐Django - 功能完整,安全性好", + "API服务": "推荐FastAPI或Flask-RESTful", + "高并发": "推荐FastAPI、Sanic或Tornado", + "内容管理": "推荐Django - 内置管理后台", + "微服务": "推荐FastAPI或Flask" + } + + for scenario, recommendation in selection_guide.items(): + print(f" {scenario}: {recommendation}") + + print("\n ✓ Python Web框架概览完成") + +# 运行框架概览 +python_web_frameworks_overview() +``` + +## 2. Flask基础 + +### 2.1 Flask入门 + +```python +def flask_basics_demo(): + """Flask基础演示""" + print("=== Flask基础演示 ===") + + # 注意:这里只是演示代码结构,实际运行需要安装Flask + print("\n1. Flask基础应用结构:") + + basic_app_code = ''' +# app.py - 基础Flask应用 +from flask import Flask, request, jsonify, render_template + +# 创建Flask应用实例 +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your-secret-key-here' + +# 基础路由 +@app.route('/') +def index(): + return '

欢迎使用Flask!

' + +# 带参数的路由 +@app.route('/user/') +def user_profile(username): + return f'

用户: {username}

' + +# 多种HTTP方法 +@app.route('/api/data', methods=['GET', 'POST']) +def api_data(): + if request.method == 'GET': + return jsonify({'message': '获取数据成功'}) + elif request.method == 'POST': + data = request.get_json() + return jsonify({'message': '数据已保存', 'data': data}) + +# 启动应用 +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) +''' + + print(" 基础应用代码:") + print(basic_app_code) + + # 2. Flask配置 + print("\n2. Flask配置管理:") + + config_code = ''' +# config.py - 配置文件 +class Config: + SECRET_KEY = 'your-secret-key' + SQLALCHEMY_TRACK_MODIFICATIONS = False + +class DevelopmentConfig(Config): + DEBUG = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db' + +class ProductionConfig(Config): + DEBUG = False + SQLALCHEMY_DATABASE_URI = 'postgresql://user:pass@localhost/prod' + +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} + +# 在app.py中使用配置 +from config import config + +app = Flask(__name__) +app.config.from_object(config['development']) +''' + + print(" 配置管理代码:") + print(config_code) + + # 3. 请求处理 + print("\n3. 请求处理示例:") + + request_handling_code = ''' +# 请求处理示例 +from flask import Flask, request, jsonify, session + +@app.route('/form', methods=['GET', 'POST']) +def handle_form(): + if request.method == 'GET': + # 显示表单 + return render_template('form.html') + + elif request.method == 'POST': + # 处理表单数据 + username = request.form.get('username') + email = request.form.get('email') + + # 验证数据 + if not username or not email: + return jsonify({'error': '用户名和邮箱不能为空'}), 400 + + # 保存到会话 + session['username'] = username + session['email'] = email + + return jsonify({'message': '数据保存成功'}) + +@app.route('/api/upload', methods=['POST']) +def upload_file(): + if 'file' not in request.files: + return jsonify({'error': '没有文件'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': '文件名为空'}), 400 + + if file: + filename = secure_filename(file.filename) + file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + return jsonify({'message': '文件上传成功', 'filename': filename}) +''' + + print(" 请求处理代码:") + print(request_handling_code) + + # 4. 模板渲染 + print("\n4. 模板渲染:") + + template_code = ''' + + + + + {% block title %}Flask应用{% endblock %} + + + + + +
+ {% block content %}{% endblock %} +
+ + + + + + +{% extends "base.html" %} + +{% block title %}首页 - Flask应用{% endblock %} + +{% block content %} +

欢迎来到Flask应用

+

当前用户: {{ session.get('username', '未登录') }}

+ +{% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} +{% endif %} + +
+ + + +
+{% endblock %} +''' + + print(" 模板代码:") + print(template_code) + + print("\n ✓ Flask基础演示完成") + +# 运行Flask基础演示 +flask_basics_demo() +``` + +### 2.2 Flask扩展和中间件 + +```python +def flask_extensions_demo(): + """Flask扩展和中间件演示""" + print("=== Flask扩展和中间件演示 ===") + + # 1. 常用Flask扩展 + print("\n1. 常用Flask扩展:") + + extensions = { + "Flask-SQLAlchemy": { + "功能": "数据库ORM集成", + "安装": "pip install Flask-SQLAlchemy", + "用途": "简化数据库操作" + }, + "Flask-Login": { + "功能": "用户会话管理", + "安装": "pip install Flask-Login", + "用途": "处理用户登录、登出、会话" + }, + "Flask-WTF": { + "功能": "表单处理和验证", + "安装": "pip install Flask-WTF", + "用途": "表单渲染、验证、CSRF保护" + }, + "Flask-Mail": { + "功能": "邮件发送", + "安装": "pip install Flask-Mail", + "用途": "发送邮件通知" + }, + "Flask-Migrate": { + "功能": "数据库迁移", + "安装": "pip install Flask-Migrate", + "用途": "管理数据库结构变更" + }, + "Flask-RESTful": { + "功能": "REST API开发", + "安装": "pip install Flask-RESTful", + "用途": "快速构建RESTful API" + } + } + + for name, details in extensions.items(): + print(f"\n {name}:") + for key, value in details.items(): + print(f" {key}: {value}") + + # 2. Flask-SQLAlchemy使用示例 + print("\n2. Flask-SQLAlchemy使用示例:") + + sqlalchemy_code = ''' +# models.py - 数据模型 +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(128)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # 关系 + posts = db.relationship('Post', backref='author', lazy=True) + + def __repr__(self): + return f'' + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def __repr__(self): + return f'' + +# app.py - 应用配置 +from flask import Flask +from models import db, User, Post + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +# 创建数据库表 +with app.app_context(): + db.create_all() + +# 数据库操作示例 +@app.route('/api/users', methods=['POST']) +def create_user(): + data = request.get_json() + + user = User( + username=data['username'], + email=data['email'] + ) + + db.session.add(user) + db.session.commit() + + return jsonify({'id': user.id, 'username': user.username}) + +@app.route('/api/users') +def get_users(): + users = User.query.all() + return jsonify([ + {'id': u.id, 'username': u.username, 'email': u.email} + for u in users + ]) +''' + + print(" SQLAlchemy代码:") + print(sqlalchemy_code) + + # 3. 中间件示例 + print("\n3. Flask中间件示例:") + + middleware_code = ''' +# 中间件和钩子函数 +from flask import Flask, request, g +import time +import logging + +app = Flask(__name__) + +# 请求前处理 +@app.before_request +def before_request(): + g.start_time = time.time() + g.user_id = request.headers.get('X-User-ID') + + # 记录请求日志 + logging.info(f"Request: {request.method} {request.path}") + +# 请求后处理 +@app.after_request +def after_request(response): + # 计算请求处理时间 + if hasattr(g, 'start_time'): + duration = time.time() - g.start_time + response.headers['X-Response-Time'] = str(duration) + + # 添加CORS头 + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' + + return response + +# 错误处理 +@app.errorhandler(404) +def not_found(error): + return jsonify({'error': '资源未找到'}), 404 + +@app.errorhandler(500) +def internal_error(error): + return jsonify({'error': '服务器内部错误'}), 500 + +# 自定义中间件类 +class AuthMiddleware: + def __init__(self, app): + self.app = app + self.app.before_request(self.authenticate) + + def authenticate(self): + # 跳过公开路由 + if request.endpoint in ['index', 'login']: + return + + token = request.headers.get('Authorization') + if not token: + return jsonify({'error': '需要认证'}), 401 + + # 验证token逻辑 + if not self.validate_token(token): + return jsonify({'error': '无效token'}), 401 + + def validate_token(self, token): + # 实际的token验证逻辑 + return token == 'valid-token' + +# 应用中间件 +auth_middleware = AuthMiddleware(app) +''' + + print(" 中间件代码:") + print(middleware_code) + + print("\n ✓ Flask扩展和中间件演示完成") + +# 运行Flask扩展演示 +flask_extensions_demo() +``` + +## 3. Django基础 + +### 3.1 Django项目结构 + +```python +def django_basics_demo(): + """Django基础演示""" + print("=== Django基础演示 ===") + + # 1. Django项目创建和结构 + print("\n1. Django项目创建和结构:") + + project_structure = ''' +# 创建Django项目 +$ django-admin startproject myproject +$ cd myproject +$ python manage.py startapp myapp + +# 项目结构 +myproject/ +├── manage.py # 项目管理脚本 +├── myproject/ # 项目配置目录 +│ ├── __init__.py +│ ├── settings.py # 项目设置 +│ ├── urls.py # 主URL配置 +│ ├── wsgi.py # WSGI配置 +│ └── asgi.py # ASGI配置 +└── myapp/ # 应用目录 + ├── __init__.py + ├── admin.py # 管理后台配置 + ├── apps.py # 应用配置 + ├── models.py # 数据模型 + ├── views.py # 视图函数 + ├── urls.py # 应用URL配置 + ├── tests.py # 测试文件 + └── migrations/ # 数据库迁移文件 + └── __init__.py +''' + + print(" 项目结构:") + print(project_structure) + + # 2. Django设置配置 + print("\n2. Django设置配置:") + + settings_code = ''' +# settings.py - Django设置 +import os +from pathlib import Path + +# 基础设置 +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = 'your-secret-key-here' +DEBUG = True +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + +# 应用配置 +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myapp', # 自定义应用 + 'rest_framework', # Django REST framework +] + +# 中间件配置 +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +# URL配置 +ROOT_URLCONF = 'myproject.urls' + +# 模板配置 +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +# 数据库配置 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# 静态文件配置 +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# 媒体文件配置 +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# 国际化配置 +LANGUAGE_CODE = 'zh-hans' +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_TZ = True +''' + + print(" 设置配置:") + print(settings_code) + + # 3. Django模型 + print("\n3. Django模型示例:") + + models_code = ''' +# models.py - 数据模型 +from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone + +class Category(models.Model): + name = models.CharField(max_length=100, unique=True, verbose_name='分类名称') + description = models.TextField(blank=True, verbose_name='描述') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + verbose_name = '分类' + verbose_name_plural = '分类' + ordering = ['name'] + + def __str__(self): + return self.name + +class Post(models.Model): + STATUS_CHOICES = [ + ('draft', '草稿'), + ('published', '已发布'), + ('archived', '已归档'), + ] + + title = models.CharField(max_length=200, verbose_name='标题') + slug = models.SlugField(max_length=200, unique=True, verbose_name='URL别名') + content = models.TextField(verbose_name='内容') + excerpt = models.TextField(max_length=500, blank=True, verbose_name='摘要') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, + default='draft', verbose_name='状态') + + # 关系字段 + author = models.ForeignKey(User, on_delete=models.CASCADE, + related_name='posts', verbose_name='作者') + category = models.ForeignKey(Category, on_delete=models.SET_NULL, + null=True, blank=True, verbose_name='分类') + tags = models.ManyToManyField('Tag', blank=True, verbose_name='标签') + + # 时间字段 + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + published_at = models.DateTimeField(null=True, blank=True, verbose_name='发布时间') + + class Meta: + verbose_name = '文章' + verbose_name_plural = '文章' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['status', 'published_at']), + models.Index(fields=['author', 'created_at']), + ] + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + if self.status == 'published' and not self.published_at: + self.published_at = timezone.now() + super().save(*args, **kwargs) + + @property + def is_published(self): + return self.status == 'published' + +class Tag(models.Model): + name = models.CharField(max_length=50, unique=True, verbose_name='标签名') + color = models.CharField(max_length=7, default='#007bff', verbose_name='颜色') + + class Meta: + verbose_name = '标签' + verbose_name_plural = '标签' + ordering = ['name'] + + def __str__(self): + return self.name + +class Comment(models.Model): + post = models.ForeignKey(Post, on_delete=models.CASCADE, + related_name='comments', verbose_name='文章') + author_name = models.CharField(max_length=100, verbose_name='作者姓名') + author_email = models.EmailField(verbose_name='作者邮箱') + content = models.TextField(verbose_name='评论内容') + is_approved = models.BooleanField(default=False, verbose_name='已审核') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + verbose_name = '评论' + verbose_name_plural = '评论' + ordering = ['-created_at'] + + def __str__(self): + return f'{self.author_name} 对 {self.post.title} 的评论' +''' + + print(" 模型代码:") + print(models_code) + + print("\n ✓ Django基础演示完成") + +# 运行Django基础演示 +django_basics_demo() +``` + +### 3.2 Django视图和URL + +```python +def django_views_urls_demo(): + """Django视图和URL演示""" + print("=== Django视图和URL演示 ===") + + # 1. 函数视图 + print("\n1. Django函数视图:") + + function_views_code = ''' +# views.py - 函数视图 +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, HttpResponse +from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_http_methods +from django.core.paginator import Paginator +from .models import Post, Category, Tag +from .forms import PostForm, CommentForm + +def index(request): + """首页视图""" + posts = Post.objects.filter(status='published').order_by('-published_at') + + # 分页 + paginator = Paginator(posts, 10) # 每页10篇文章 + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + context = { + 'posts': page_obj, + 'categories': Category.objects.all(), + 'popular_tags': Tag.objects.all()[:10] + } + return render(request, 'blog/index.html', context) + +def post_detail(request, slug): + """文章详情视图""" + post = get_object_or_404(Post, slug=slug, status='published') + comments = post.comments.filter(is_approved=True) + + # 处理评论表单 + if request.method == 'POST': + comment_form = CommentForm(request.POST) + if comment_form.is_valid(): + comment = comment_form.save(commit=False) + comment.post = post + comment.save() + return redirect('post_detail', slug=slug) + else: + comment_form = CommentForm() + + context = { + 'post': post, + 'comments': comments, + 'comment_form': comment_form + } + return render(request, 'blog/post_detail.html', context) + +@login_required +def post_create(request): + """创建文章视图""" + if request.method == 'POST': + form = PostForm(request.POST) + if form.is_valid(): + post = form.save(commit=False) + post.author = request.user + post.save() + form.save_m2m() # 保存多对多关系 + return redirect('post_detail', slug=post.slug) + else: + form = PostForm() + + return render(request, 'blog/post_form.html', {'form': form}) + +@require_http_methods(["GET", "POST"]) +def category_posts(request, category_id): + """分类文章视图""" + category = get_object_or_404(Category, id=category_id) + posts = Post.objects.filter(category=category, status='published') + + context = { + 'category': category, + 'posts': posts + } + return render(request, 'blog/category_posts.html', context) + +def api_posts(request): + """API视图 - 返回JSON数据""" + posts = Post.objects.filter(status='published').values( + 'id', 'title', 'slug', 'excerpt', 'published_at' + ) + + return JsonResponse({ + 'posts': list(posts), + 'count': posts.count() + }) +''' + + print(" 函数视图代码:") + print(function_views_code) + + # 2. 类视图 + print("\n2. Django类视图:") + + class_views_code = ''' +# views.py - 类视图 +from django.views.generic import ( + ListView, DetailView, CreateView, UpdateView, DeleteView +) +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy +from django.db.models import Q + +class PostListView(ListView): + """文章列表视图""" + model = Post + template_name = 'blog/post_list.html' + context_object_name = 'posts' + paginate_by = 10 + + def get_queryset(self): + queryset = Post.objects.filter(status='published') + + # 搜索功能 + search_query = self.request.GET.get('search') + if search_query: + queryset = queryset.filter( + Q(title__icontains=search_query) | + Q(content__icontains=search_query) + ) + + # 分类过滤 + category_id = self.request.GET.get('category') + if category_id: + queryset = queryset.filter(category_id=category_id) + + return queryset.order_by('-published_at') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['categories'] = Category.objects.all() + context['search_query'] = self.request.GET.get('search', '') + return context + +class PostDetailView(DetailView): + """文章详情视图""" + model = Post + template_name = 'blog/post_detail.html' + context_object_name = 'post' + slug_field = 'slug' + slug_url_kwarg = 'slug' + + def get_queryset(self): + return Post.objects.filter(status='published') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['comments'] = self.object.comments.filter(is_approved=True) + context['comment_form'] = CommentForm() + return context + +class PostCreateView(LoginRequiredMixin, CreateView): + """创建文章视图""" + model = Post + form_class = PostForm + template_name = 'blog/post_form.html' + + def form_valid(self, form): + form.instance.author = self.request.user + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('post_detail', kwargs={'slug': self.object.slug}) + +class PostUpdateView(LoginRequiredMixin, UpdateView): + """更新文章视图""" + model = Post + form_class = PostForm + template_name = 'blog/post_form.html' + slug_field = 'slug' + slug_url_kwarg = 'slug' + + def get_queryset(self): + # 只允许作者编辑自己的文章 + return Post.objects.filter(author=self.request.user) + + def get_success_url(self): + return reverse_lazy('post_detail', kwargs={'slug': self.object.slug}) + +class PostDeleteView(LoginRequiredMixin, DeleteView): + """删除文章视图""" + model = Post + template_name = 'blog/post_confirm_delete.html' + success_url = reverse_lazy('post_list') + slug_field = 'slug' + slug_url_kwarg = 'slug' + + def get_queryset(self): + return Post.objects.filter(author=self.request.user) +''' + + print(" 类视图代码:") + print(class_views_code) + + # 3. URL配置 + print("\n3. Django URL配置:") + + urls_code = ''' +# myproject/urls.py - 主URL配置 +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('myapp.urls')), + path('api/', include('myapp.api_urls')), + path('accounts/', include('django.contrib.auth.urls')), +] + +# 开发环境下提供媒体文件服务 +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + +# myapp/urls.py - 应用URL配置 +from django.urls import path +from . import views + +app_name = 'blog' + +urlpatterns = [ + # 函数视图URL + path('', views.index, name='index'), + path('post//', views.post_detail, name='post_detail'), + path('create/', views.post_create, name='post_create'), + path('category//', views.category_posts, name='category_posts'), + + # 类视图URL + path('posts/', views.PostListView.as_view(), name='post_list'), + path('post//edit/', views.PostUpdateView.as_view(), name='post_update'), + path('post//delete/', views.PostDeleteView.as_view(), name='post_delete'), + + # API URL + path('api/posts/', views.api_posts, name='api_posts'), +] + +# myapp/api_urls.py - API URL配置 +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .api_views import PostViewSet, CategoryViewSet + +router = DefaultRouter() +router.register(r'posts', PostViewSet) +router.register(r'categories', CategoryViewSet) + +urlpatterns = [ + path('', include(router.urls)), + path('auth/', include('rest_framework.urls')), +] +''' + + print(" URL配置代码:") + print(urls_code) + + print("\n ✓ Django视图和URL演示完成") + +# 运行Django视图和URL演示 +django_views_urls_demo() +``` + +## 4. Django REST Framework + +### 4.1 REST API开发 + +```python +def django_rest_framework_demo(): + """Django REST Framework演示""" + print("=== Django REST Framework演示 ===") + + # 1. 安装和配置 + print("\n1. DRF安装和配置:") + + installation_code = ''' +# 安装Django REST Framework +pip install djangorestframework +pip install django-filter +pip install djangorestframework-simplejwt + +# settings.py - 配置DRF +INSTALLED_APPS = [ + # ... 其他应用 + 'rest_framework', + 'rest_framework.authtoken', + 'django_filters', +] + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticatedOrReadOnly', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, + 'DEFAULT_FILTER_BACKENDS': [ + 'django_filters.rest_framework.DjangoFilterBackend', + 'rest_framework.filters.SearchFilter', + 'rest_framework.filters.OrderingFilter', + ], + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], +} +''' + + print(" 安装配置:") + print(installation_code) + + # 2. 序列化器 + print("\n2. DRF序列化器:") + + serializers_code = ''' +# serializers.py - 序列化器 +from rest_framework import serializers +from .models import Post, Category, Tag, Comment +from django.contrib.auth.models import User + +class UserSerializer(serializers.ModelSerializer): + posts_count = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ['id', 'username', 'email', 'date_joined', 'posts_count'] + read_only_fields = ['date_joined'] + + def get_posts_count(self, obj): + return obj.posts.filter(status='published').count() + +class CategorySerializer(serializers.ModelSerializer): + posts_count = serializers.SerializerMethodField() + + class Meta: + model = Category + fields = ['id', 'name', 'description', 'created_at', 'posts_count'] + read_only_fields = ['created_at'] + + def get_posts_count(self, obj): + return obj.post_set.filter(status='published').count() + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ['id', 'name', 'color'] + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = ['id', 'author_name', 'author_email', 'content', + 'is_approved', 'created_at'] + read_only_fields = ['created_at', 'is_approved'] + +class PostListSerializer(serializers.ModelSerializer): + author = UserSerializer(read_only=True) + category = CategorySerializer(read_only=True) + tags = TagSerializer(many=True, read_only=True) + comments_count = serializers.SerializerMethodField() + + class Meta: + model = Post + fields = ['id', 'title', 'slug', 'excerpt', 'status', + 'author', 'category', 'tags', 'created_at', + 'published_at', 'comments_count'] + + def get_comments_count(self, obj): + return obj.comments.filter(is_approved=True).count() + +class PostDetailSerializer(serializers.ModelSerializer): + author = UserSerializer(read_only=True) + category = CategorySerializer(read_only=True) + tags = TagSerializer(many=True, read_only=True) + comments = CommentSerializer(many=True, read_only=True) + + class Meta: + model = Post + fields = ['id', 'title', 'slug', 'content', 'excerpt', + 'status', 'author', 'category', 'tags', + 'created_at', 'updated_at', 'published_at', 'comments'] + read_only_fields = ['created_at', 'updated_at'] + +class PostCreateUpdateSerializer(serializers.ModelSerializer): + tags = serializers.PrimaryKeyRelatedField( + many=True, queryset=Tag.objects.all(), required=False + ) + + class Meta: + model = Post + fields = ['title', 'slug', 'content', 'excerpt', 'status', + 'category', 'tags'] + + def validate_slug(self, value): + # 验证slug唯一性 + if self.instance: + if Post.objects.exclude(pk=self.instance.pk).filter(slug=value).exists(): + raise serializers.ValidationError("该URL别名已存在") + else: + if Post.objects.filter(slug=value).exists(): + raise serializers.ValidationError("该URL别名已存在") + return value +''' + + print(" 序列化器代码:") + print(serializers_code) + + # 3. API视图 + print("\n3. DRF API视图:") + + api_views_code = ''' +# api_views.py - API视图 +from rest_framework import viewsets, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated +from django_filters.rest_framework import DjangoFilterBackend +from django.db.models import Q +from .models import Post, Category, Tag, Comment +from .serializers import ( + PostListSerializer, PostDetailSerializer, PostCreateUpdateSerializer, + CategorySerializer, TagSerializer, CommentSerializer +) + +class PostViewSet(viewsets.ModelViewSet): + """文章API视图集""" + queryset = Post.objects.all() + permission_classes = [IsAuthenticatedOrReadOnly] + filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + filterset_fields = ['status', 'category', 'author'] + search_fields = ['title', 'content', 'excerpt'] + ordering_fields = ['created_at', 'updated_at', 'published_at'] + ordering = ['-created_at'] + + def get_serializer_class(self): + if self.action == 'list': + return PostListSerializer + elif self.action in ['create', 'update', 'partial_update']: + return PostCreateUpdateSerializer + return PostDetailSerializer + + def get_queryset(self): + queryset = Post.objects.all() + + # 非管理员只能看到已发布的文章 + if not self.request.user.is_staff: + queryset = queryset.filter(status='published') + + # 作者只能看到自己的文章 + if self.action in ['update', 'partial_update', 'destroy']: + if not self.request.user.is_staff: + queryset = queryset.filter(author=self.request.user) + + return queryset + + def perform_create(self, serializer): + serializer.save(author=self.request.user) + + @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated]) + def toggle_status(self, request, pk=None): + """切换文章状态""" + post = self.get_object() + + if post.author != request.user and not request.user.is_staff: + return Response( + {'error': '无权限操作'}, + status=status.HTTP_403_FORBIDDEN + ) + + if post.status == 'published': + post.status = 'draft' + else: + post.status = 'published' + + post.save() + + return Response({ + 'message': f'文章状态已更改为{post.get_status_display()}', + 'status': post.status + }) + + @action(detail=False, methods=['get']) + def my_posts(self, request): + """获取当前用户的文章""" + if not request.user.is_authenticated: + return Response( + {'error': '需要登录'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + queryset = self.get_queryset().filter(author=request.user) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = PostListSerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = PostListSerializer(queryset, many=True) + return Response(serializer.data) + +class CategoryViewSet(viewsets.ModelViewSet): + """分类API视图集""" + queryset = Category.objects.all() + serializer_class = CategorySerializer + permission_classes = [IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name', 'description'] + ordering_fields = ['name', 'created_at'] + ordering = ['name'] + + @action(detail=True, methods=['get']) + def posts(self, request, pk=None): + """获取分类下的文章""" + category = self.get_object() + posts = Post.objects.filter( + category=category, + status='published' + ).order_by('-published_at') + + page = self.paginate_queryset(posts) + if page is not None: + serializer = PostListSerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = PostListSerializer(posts, many=True) + return Response(serializer.data) + +class TagViewSet(viewsets.ModelViewSet): + """标签API视图集""" + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering_fields = ['name'] + ordering = ['name'] + +class CommentViewSet(viewsets.ModelViewSet): + """评论API视图集""" + queryset = Comment.objects.all() + serializer_class = CommentSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + filter_backends = [DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ['post', 'is_approved'] + ordering_fields = ['created_at'] + ordering = ['-created_at'] + + def get_queryset(self): + queryset = Comment.objects.all() + + # 非管理员只能看到已审核的评论 + if not self.request.user.is_staff: + queryset = queryset.filter(is_approved=True) + + return queryset + + @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated]) + def approve(self, request, pk=None): + """审核评论""" + if not request.user.is_staff: + return Response( + {'error': '无权限操作'}, + status=status.HTTP_403_FORBIDDEN + ) + + comment = self.get_object() + comment.is_approved = True + comment.save() + + return Response({'message': '评论已审核通过'}) +''' + + print(" API视图代码:") + print(api_views_code) + + print("\n ✓ Django REST Framework演示完成") + +# 运行DRF演示 +django_rest_framework_demo() +``` + +## 5. FastAPI基础 + +### 5.1 FastAPI入门 + +```python +def fastapi_basics_demo(): + """FastAPI基础演示""" + print("=== FastAPI基础演示 ===") + + # 1. FastAPI基础应用 + print("\n1. FastAPI基础应用:") + + basic_app_code = ''' +# main.py - FastAPI基础应用 +from fastapi import FastAPI, HTTPException, Depends, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from pydantic import BaseModel, EmailStr +from typing import List, Optional +from datetime import datetime +import uvicorn + +# 创建FastAPI应用实例 +app = FastAPI( + title="博客API", + description="一个简单的博客API示例", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# 安全认证 +security = HTTPBearer() + +# Pydantic模型 +class UserBase(BaseModel): + username: str + email: EmailStr + +class UserCreate(UserBase): + password: str + +class User(UserBase): + id: int + created_at: datetime + + class Config: + orm_mode = True + +class PostBase(BaseModel): + title: str + content: str + excerpt: Optional[str] = None + +class PostCreate(PostBase): + category_id: Optional[int] = None + tags: Optional[List[str]] = [] + +class Post(PostBase): + id: int + slug: str + status: str + author_id: int + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + +class PostResponse(Post): + author: User + comments_count: int + +# 依赖注入 +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + """获取当前用户""" + token = credentials.credentials + # 这里应该验证token并返回用户信息 + # 简化示例,直接返回模拟用户 + if token != "valid-token": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的认证凭据", + headers={"WWW-Authenticate": "Bearer"}, + ) + return {"id": 1, "username": "testuser", "email": "test@example.com"} + +# 路由定义 +@app.get("/") +async def root(): + """根路径""" + return {"message": "欢迎使用博客API"} + +@app.get("/health") +async def health_check(): + """健康检查""" + return {"status": "healthy", "timestamp": datetime.now()} + +# 用户相关路由 +@app.post("/users/", response_model=User, status_code=status.HTTP_201_CREATED) +async def create_user(user: UserCreate): + """创建用户""" + # 这里应该保存到数据库 + return { + "id": 1, + "username": user.username, + "email": user.email, + "created_at": datetime.now() + } + +@app.get("/users/me", response_model=User) +async def get_current_user_info(current_user: dict = Depends(get_current_user)): + """获取当前用户信息""" + return { + "id": current_user["id"], + "username": current_user["username"], + "email": current_user["email"], + "created_at": datetime.now() + } + +# 文章相关路由 +@app.get("/posts/", response_model=List[PostResponse]) +async def get_posts( + skip: int = 0, + limit: int = 10, + search: Optional[str] = None, + category: Optional[int] = None +): + """获取文章列表""" + # 模拟数据 + posts = [ + { + "id": 1, + "title": "FastAPI入门", + "content": "FastAPI是一个现代、快速的Web框架...", + "excerpt": "FastAPI简介", + "slug": "fastapi-intro", + "status": "published", + "author_id": 1, + "created_at": datetime.now(), + "updated_at": datetime.now(), + "author": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "created_at": datetime.now() + }, + "comments_count": 5 + } + ] + + return posts[skip:skip + limit] + +@app.get("/posts/{post_id}", response_model=PostResponse) +async def get_post(post_id: int): + """获取单篇文章""" + if post_id != 1: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章未找到" + ) + + return { + "id": 1, + "title": "FastAPI入门", + "content": "FastAPI是一个现代、快速的Web框架...", + "excerpt": "FastAPI简介", + "slug": "fastapi-intro", + "status": "published", + "author_id": 1, + "created_at": datetime.now(), + "updated_at": datetime.now(), + "author": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "created_at": datetime.now() + }, + "comments_count": 5 + } + +@app.post("/posts/", response_model=Post, status_code=status.HTTP_201_CREATED) +async def create_post( + post: PostCreate, + current_user: dict = Depends(get_current_user) +): + """创建文章""" + return { + "id": 2, + "title": post.title, + "content": post.content, + "excerpt": post.excerpt, + "slug": post.title.lower().replace(" ", "-"), + "status": "draft", + "author_id": current_user["id"], + "created_at": datetime.now(), + "updated_at": datetime.now() + } + +@app.put("/posts/{post_id}", response_model=Post) +async def update_post( + post_id: int, + post: PostCreate, + current_user: dict = Depends(get_current_user) +): + """更新文章""" + if post_id != 1: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章未找到" + ) + + return { + "id": post_id, + "title": post.title, + "content": post.content, + "excerpt": post.excerpt, + "slug": post.title.lower().replace(" ", "-"), + "status": "published", + "author_id": current_user["id"], + "created_at": datetime.now(), + "updated_at": datetime.now() + } + +@app.delete("/posts/{post_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_post( + post_id: int, + current_user: dict = Depends(get_current_user) +): + """删除文章""" + if post_id != 1: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章未找到" + ) + + # 删除逻辑 + return + +# 启动应用 +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) +''' + + print(" 基础应用代码:") + print(basic_app_code) + + # 2. 中间件和异常处理 + print("\n2. FastAPI中间件和异常处理:") + + middleware_code = ''' +# middleware.py - 中间件和异常处理 +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +import time +import logging + +app = FastAPI() + +# CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生产环境应该指定具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 信任主机中间件 +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["localhost", "127.0.0.1", "*.example.com"] +) + +# 自定义中间件 +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + start_time = time.time() + + # 记录请求信息 + logging.info(f"Request: {request.method} {request.url}") + + response = await call_next(request) + + # 计算处理时间 + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + + # 记录响应信息 + logging.info(f"Response: {response.status_code} - {process_time:.4f}s") + + return response + +app.add_middleware(LoggingMiddleware) + +# 全局异常处理 +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "code": exc.status_code, + "message": exc.detail, + "path": str(request.url) + } + } + ) + +@app.exception_handler(ValueError) +async def value_error_handler(request: Request, exc: ValueError): + return JSONResponse( + status_code=400, + content={ + "error": { + "code": 400, + "message": "请求参数错误", + "detail": str(exc), + "path": str(request.url) + } + } + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + logging.error(f"Unhandled exception: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={ + "error": { + "code": 500, + "message": "服务器内部错误", + "path": str(request.url) + } + } + ) +''' + + print(" 中间件代码:") + print(middleware_code) + + # 3. 数据库集成 + print("\n3. FastAPI数据库集成:") + + database_code = ''' +# database.py - 数据库配置 +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Session, relationship +from datetime import datetime + +# 数据库配置 +SQLALCHEMY_DATABASE_URL = "sqlite:///./blog.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} # SQLite特有配置 +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# 数据库模型 +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + created_at = Column(DateTime, default=datetime.utcnow) + + posts = relationship("Post", back_populates="author") + +class Post(Base): + __tablename__ = "posts" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + slug = Column(String, unique=True, index=True) + content = Column(Text) + excerpt = Column(Text) + status = Column(String, default="draft") + author_id = Column(Integer, ForeignKey("users.id")) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + author = relationship("User", back_populates="posts") + +# 创建数据库表 +Base.metadata.create_all(bind=engine) + +# 数据库依赖 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# CRUD操作 +class UserCRUD: + @staticmethod + def create_user(db: Session, user_data: dict): + db_user = User(**user_data) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + @staticmethod + def get_user(db: Session, user_id: int): + return db.query(User).filter(User.id == user_id).first() + + @staticmethod + def get_user_by_username(db: Session, username: str): + return db.query(User).filter(User.username == username).first() + +class PostCRUD: + @staticmethod + def create_post(db: Session, post_data: dict, author_id: int): + db_post = Post(**post_data, author_id=author_id) + db.add(db_post) + db.commit() + db.refresh(db_post) + return db_post + + @staticmethod + def get_posts(db: Session, skip: int = 0, limit: int = 10): + return db.query(Post).offset(skip).limit(limit).all() + + @staticmethod + def get_post(db: Session, post_id: int): + return db.query(Post).filter(Post.id == post_id).first() + + @staticmethod + def update_post(db: Session, post_id: int, post_data: dict): + db_post = db.query(Post).filter(Post.id == post_id).first() + if db_post: + for key, value in post_data.items(): + setattr(db_post, key, value) + db_post.updated_at = datetime.utcnow() + db.commit() + db.refresh(db_post) + return db_post + + @staticmethod + def delete_post(db: Session, post_id: int): + db_post = db.query(Post).filter(Post.id == post_id).first() + if db_post: + db.delete(db_post) + db.commit() + return db_post + +# 在main.py中使用数据库 +from fastapi import Depends +from sqlalchemy.orm import Session +from database import get_db, UserCRUD, PostCRUD + +@app.post("/users/", response_model=User) +async def create_user(user: UserCreate, db: Session = Depends(get_db)): + # 检查用户是否已存在 + existing_user = UserCRUD.get_user_by_username(db, user.username) + if existing_user: + raise HTTPException(status_code=400, detail="用户名已存在") + + # 创建用户(实际应用中需要加密密码) + user_data = { + "username": user.username, + "email": user.email, + "hashed_password": user.password # 应该使用bcrypt等加密 + } + + return UserCRUD.create_user(db, user_data) + +@app.get("/posts/", response_model=List[Post]) +async def get_posts( + skip: int = 0, + limit: int = 10, + db: Session = Depends(get_db) +): + return PostCRUD.get_posts(db, skip=skip, limit=limit) +''' + + print(" 数据库集成代码:") + print(database_code) + + print("\n ✓ FastAPI基础演示完成") + +# 运行FastAPI基础演示 +fastapi_basics_demo() +``` + + + + + diff --git a/docs/Python/22.md b/docs/Python/22.md new file mode 100644 index 000000000..7c6818915 --- /dev/null +++ b/docs/Python/22.md @@ -0,0 +1,3118 @@ +--- +title: 第22天-异步IO +author: 吒 +date: '2023-06-15' +--- + +# 第22天-异步IO + +## 1. 异步编程基础 + +### 1.1 异步编程概念 + +```python +def async_programming_concepts(): + """异步编程概念演示""" + print("=== 异步编程概念演示 ===") + + # 1. 同步vs异步对比 + print("\n1. 同步vs异步编程对比:") + + sync_async_comparison = ''' +# 同步编程特点: +# - 代码按顺序执行,一行执行完才执行下一行 +# - 遇到耗时操作(如网络请求、文件读写)会阻塞 +# - 简单直观,但效率较低 + +import time +import requests + +def sync_example(): + """同步编程示例""" + print("开始同步任务") + + # 模拟耗时操作 + time.sleep(2) + print("任务1完成") + + time.sleep(2) + print("任务2完成") + + time.sleep(2) + print("任务3完成") + + print("所有同步任务完成") + +# 异步编程特点: +# - 可以在等待耗时操作时执行其他任务 +# - 不会阻塞程序执行 +# - 提高程序效率,特别是I/O密集型任务 +# - 代码相对复杂,需要理解事件循环概念 + +import asyncio + +async def async_example(): + """异步编程示例""" + print("开始异步任务") + + # 创建异步任务 + task1 = asyncio.create_task(async_task("任务1", 2)) + task2 = asyncio.create_task(async_task("任务2", 2)) + task3 = asyncio.create_task(async_task("任务3", 2)) + + # 并发执行任务 + await asyncio.gather(task1, task2, task3) + + print("所有异步任务完成") + +async def async_task(name, delay): + """异步任务""" + await asyncio.sleep(delay) + print(f"{name}完成") + return name + +# 运行异步示例 +# asyncio.run(async_example()) +''' + + print(" 同步vs异步对比:") + print(sync_async_comparison) + + # 2. 异步编程的优势 + print("\n2. 异步编程的优势:") + + advantages = ''' +异步编程的主要优势: + +1. 提高并发性能: + - 在等待I/O操作时可以执行其他任务 + - 单线程处理大量并发请求 + - 避免线程切换开销 + +2. 资源利用率高: + - 减少内存占用(相比多线程) + - 降低CPU上下文切换成本 + - 更好的可扩展性 + +3. 适用场景: + - 网络编程(Web服务器、爬虫) + - 文件I/O操作 + - 数据库访问 + - 实时通信应用 + +4. 性能对比示例: + - 同步:3个2秒任务 = 6秒总时间 + - 异步:3个2秒任务 = 2秒总时间(并发执行) +''' + + print(" 异步编程优势:") + print(advantages) + + # 3. 异步编程的挑战 + print("\n3. 异步编程的挑战:") + + challenges = ''' +异步编程的挑战: + +1. 学习曲线陡峭: + - 需要理解事件循环、协程等概念 + - 调试相对困难 + - 错误处理更复杂 + +2. 代码复杂性: + - 需要使用async/await语法 + - 回调地狱问题 + - 状态管理困难 + +3. 生态系统要求: + - 需要异步版本的库 + - 不是所有库都支持异步 + - 混合同步/异步代码的复杂性 + +4. 性能陷阱: + - CPU密集型任务不适合异步 + - 不当使用可能降低性能 + - 需要合理设计异步架构 +''' + + print(" 异步编程挑战:") + print(challenges) + + print("\n ✓ 异步编程概念演示完成") + +# 运行异步编程概念演示 +async_programming_concepts() +``` + +### 1.2 Python异步编程发展历程 + +```python +def python_async_history(): + """Python异步编程发展历程""" + print("=== Python异步编程发展历程 ===") + + # 1. 发展历程 + print("\n1. Python异步编程发展历程:") + + history = ''' +Python异步编程发展历程: + +1. Python 2.5 (2006年): + - 引入生成器的send()和throw()方法 + - 为协程奠定基础 + +2. Python 3.3 (2012年): + - 引入yield from语法 + - 简化生成器委托 + +3. Python 3.4 (2014年): + - 引入asyncio模块 + - 提供事件循环和协程支持 + - 使用@asyncio.coroutine装饰器 + +4. Python 3.5 (2015年): + - 引入async/await语法 + - 原生协程支持 + - 异步编程成为语言特性 + +5. Python 3.6+ (2016年至今): + - 持续改进asyncio性能 + - 增加新的异步特性 + - 生态系统不断完善 +''' + + print(" 发展历程:") + print(history) + + # 2. 核心概念 + print("\n2. 异步编程核心概念:") + + core_concepts = ''' +异步编程核心概念: + +1. 事件循环 (Event Loop): + - 异步程序的核心调度器 + - 管理和执行异步任务 + - 处理I/O事件和回调 + +2. 协程 (Coroutine): + - 可以暂停和恢复的函数 + - 使用async def定义 + - 通过await调用 + +3. 任务 (Task): + - 协程的包装器 + - 可以并发执行 + - 提供状态管理 + +4. Future: + - 表示异步操作的结果 + - 可以设置回调 + - 任务的基类 + +5. 异步上下文管理器: + - 支持async with语法 + - 异步资源管理 + +6. 异步迭代器: + - 支持async for语法 + - 异步数据流处理 +''' + + print(" 核心概念:") + print(core_concepts) + + print("\n ✓ Python异步编程发展历程演示完成") + +# 运行Python异步编程发展历程演示 +python_async_history() +``` + +## 2. asyncio基础 + +### 2.1 事件循环和协程 + +```python +import asyncio +import time +from datetime import datetime + +def asyncio_basics_demo(): + """asyncio基础演示""" + print("=== asyncio基础演示 ===") + + # 1. 事件循环基础 + print("\n1. 事件循环基础:") + + event_loop_code = ''' +# 事件循环基础操作 +import asyncio + +# 方法1: 使用asyncio.run()(推荐) +async def main(): + print("Hello, asyncio!") + await asyncio.sleep(1) + print("Goodbye, asyncio!") + +# 运行协程 +asyncio.run(main()) + +# 方法2: 手动管理事件循环 +loop = asyncio.new_event_loop() +asyncio.set_event_loop(loop) +try: + loop.run_until_complete(main()) +finally: + loop.close() + +# 方法3: 获取当前事件循环 +async def get_loop_info(): + loop = asyncio.get_running_loop() + print(f"当前事件循环: {loop}") + print(f"循环是否运行: {loop.is_running()}") + + # 事件循环调试信息 + print(f"循环调试模式: {loop.get_debug()}") + +asyncio.run(get_loop_info()) +''' + + print(" 事件循环基础代码:") + print(event_loop_code) + + # 2. 协程基础 + print("\n2. 协程基础:") + + coroutine_code = ''' +# 协程定义和使用 +import asyncio +import time + +# 定义协程函数 +async def simple_coroutine(name, delay): + """简单协程示例""" + print(f"{name} 开始执行") + await asyncio.sleep(delay) # 异步等待 + print(f"{name} 执行完成") + return f"{name} 的结果" + +# 协程的不同调用方式 +async def coroutine_examples(): + print("=== 协程调用示例 ===") + + # 1. 直接await + print("\n1. 直接await调用:") + result = await simple_coroutine("任务1", 1) + print(f"结果: {result}") + + # 2. 并发执行多个协程 + print("\n2. 并发执行:") + start_time = time.time() + + # 使用asyncio.gather() + results = await asyncio.gather( + simple_coroutine("任务A", 1), + simple_coroutine("任务B", 2), + simple_coroutine("任务C", 1.5) + ) + + end_time = time.time() + print(f"并发执行结果: {results}") + print(f"总耗时: {end_time - start_time:.2f}秒") + + # 3. 使用create_task() + print("\n3. 使用create_task():") + task1 = asyncio.create_task(simple_coroutine("任务X", 1)) + task2 = asyncio.create_task(simple_coroutine("任务Y", 1)) + + # 等待任务完成 + result1 = await task1 + result2 = await task2 + + print(f"任务结果: {result1}, {result2}") + +# 运行协程示例 +asyncio.run(coroutine_examples()) +''' + + print(" 协程基础代码:") + print(coroutine_code) + + # 3. 实际运行示例 + print("\n3. 实际运行示例:") + + async def demo_coroutine(name, delay): + """演示协程""" + print(f"[{datetime.now().strftime('%H:%M:%S')}] {name} 开始") + await asyncio.sleep(delay) + print(f"[{datetime.now().strftime('%H:%M:%S')}] {name} 完成") + return f"{name}_result" + + async def run_demo(): + """运行演示""" + print("开始异步任务演示...") + + # 记录开始时间 + start = time.time() + + # 并发执行三个任务 + results = await asyncio.gather( + demo_coroutine("下载文件", 2), + demo_coroutine("处理数据", 1.5), + demo_coroutine("发送邮件", 1) + ) + + # 记录结束时间 + end = time.time() + + print(f"\n任务结果: {results}") + print(f"总耗时: {end - start:.2f}秒") + print("如果是同步执行,需要4.5秒") + + # 运行演示 + asyncio.run(run_demo()) + + print("\n ✓ asyncio基础演示完成") + +# 运行asyncio基础演示 +asyncio_basics_demo() +``` + +### 2.2 任务管理和并发控制 + +```python +import asyncio +import random +import time +from concurrent.futures import ThreadPoolExecutor + +def task_management_demo(): + """任务管理和并发控制演示""" + print("=== 任务管理和并发控制演示 ===") + + # 1. 任务创建和管理 + print("\n1. 任务创建和管理:") + + task_creation_code = ''' +# 任务创建和管理 +import asyncio +import random + +async def worker_task(worker_id, work_time): + """工作任务""" + print(f"工作者 {worker_id} 开始工作") + await asyncio.sleep(work_time) + + # 模拟随机失败 + if random.random() < 0.2: # 20%失败率 + raise Exception(f"工作者 {worker_id} 工作失败") + + print(f"工作者 {worker_id} 完成工作") + return f"工作者{worker_id}的结果" + +async def task_management_example(): + """任务管理示例""" + print("=== 任务管理示例 ===") + + # 1. 创建多个任务 + tasks = [] + for i in range(5): + work_time = random.uniform(1, 3) + task = asyncio.create_task( + worker_task(i, work_time), + name=f"worker_{i}" # 给任务命名 + ) + tasks.append(task) + + # 2. 监控任务状态 + print("\n任务状态监控:") + for task in tasks: + print(f"任务 {task.get_name()}: {task.done()}") + + # 3. 等待所有任务完成(处理异常) + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 4. 处理结果 + print("\n任务结果:") + for i, result in enumerate(results): + if isinstance(result, Exception): + print(f"任务 {i} 失败: {result}") + else: + print(f"任务 {i} 成功: {result}") + + # 5. 检查任务最终状态 + print("\n最终任务状态:") + for task in tasks: + status = "完成" if task.done() else "运行中" + exception = task.exception() if task.done() else None + print(f"任务 {task.get_name()}: {status}") + if exception: + print(f" 异常: {exception}") + +# 运行任务管理示例 +asyncio.run(task_management_example()) +''' + + print(" 任务创建和管理代码:") + print(task_creation_code) + + # 2. 并发控制 + print("\n2. 并发控制:") + + concurrency_control_code = ''' +# 并发控制示例 +import asyncio +import time + +# 信号量控制并发数 +async def limited_worker(semaphore, worker_id, work_time): + """受限制的工作任务""" + async with semaphore: # 获取信号量 + print(f"工作者 {worker_id} 开始工作 (当前时间: {time.strftime('%H:%M:%S')})") + await asyncio.sleep(work_time) + print(f"工作者 {worker_id} 完成工作 (当前时间: {time.strftime('%H:%M:%S')})") + return f"结果_{worker_id}" + +async def concurrency_control_example(): + """并发控制示例""" + print("=== 并发控制示例 ===") + + # 创建信号量,限制同时运行的任务数为3 + semaphore = asyncio.Semaphore(3) + + # 创建10个任务 + tasks = [] + for i in range(10): + task = asyncio.create_task( + limited_worker(semaphore, i, random.uniform(1, 2)) + ) + tasks.append(task) + + print(f"创建了 {len(tasks)} 个任务,但同时只能运行3个") + + # 等待所有任务完成 + start_time = time.time() + results = await asyncio.gather(*tasks) + end_time = time.time() + + print(f"\n所有任务完成,耗时: {end_time - start_time:.2f}秒") + print(f"结果数量: {len(results)}") + +# 运行并发控制示例 +asyncio.run(concurrency_control_example()) +''' + + print(" 并发控制代码:") + print(concurrency_control_code) + + # 3. 任务取消和超时 + print("\n3. 任务取消和超时:") + + cancellation_code = ''' +# 任务取消和超时控制 +import asyncio + +async def long_running_task(task_id, duration): + """长时间运行的任务""" + try: + print(f"任务 {task_id} 开始执行") + await asyncio.sleep(duration) + print(f"任务 {task_id} 正常完成") + return f"任务{task_id}完成" + except asyncio.CancelledError: + print(f"任务 {task_id} 被取消") + # 清理资源 + await asyncio.sleep(0.1) # 模拟清理时间 + print(f"任务 {task_id} 清理完成") + raise # 重新抛出取消异常 + +async def cancellation_example(): + """任务取消示例""" + print("=== 任务取消示例 ===") + + # 1. 手动取消任务 + print("\n1. 手动取消任务:") + task = asyncio.create_task(long_running_task(1, 5)) + + # 等待1秒后取消任务 + await asyncio.sleep(1) + task.cancel() + + try: + await task + except asyncio.CancelledError: + print("任务已被取消") + + # 2. 超时控制 + print("\n2. 超时控制:") + try: + # 设置3秒超时 + result = await asyncio.wait_for( + long_running_task(2, 5), + timeout=3.0 + ) + print(f"任务结果: {result}") + except asyncio.TimeoutError: + print("任务超时") + + # 3. 批量任务超时控制 + print("\n3. 批量任务超时控制:") + tasks = [ + asyncio.create_task(long_running_task(i, random.uniform(1, 4))) + for i in range(3, 6) + ] + + try: + # 等待所有任务完成,但设置总超时时间 + results = await asyncio.wait_for( + asyncio.gather(*tasks, return_exceptions=True), + timeout=2.5 + ) + print(f"批量任务结果: {results}") + except asyncio.TimeoutError: + print("批量任务超时,取消所有未完成的任务") + for task in tasks: + if not task.done(): + task.cancel() + + # 等待取消完成 + await asyncio.gather(*tasks, return_exceptions=True) + +# 运行任务取消示例 +asyncio.run(cancellation_example()) +''' + + print(" 任务取消和超时代码:") + print(cancellation_code) + + # 4. 实际运行示例 + print("\n4. 实际运行示例:") + + async def demo_worker(worker_id, duration): + """演示工作任务""" + print(f"[{time.strftime('%H:%M:%S')}] 工作者 {worker_id} 开始") + await asyncio.sleep(duration) + print(f"[{time.strftime('%H:%M:%S')}] 工作者 {worker_id} 完成") + return f"工作者{worker_id}结果" + + async def run_concurrency_demo(): + """运行并发控制演示""" + print("开始并发控制演示...") + + # 创建信号量,限制并发数为2 + semaphore = asyncio.Semaphore(2) + + async def controlled_worker(worker_id): + async with semaphore: + return await demo_worker(worker_id, random.uniform(1, 2)) + + # 创建5个任务 + tasks = [ + asyncio.create_task(controlled_worker(i)) + for i in range(5) + ] + + # 等待所有任务完成 + start = time.time() + results = await asyncio.gather(*tasks) + end = time.time() + + print(f"\n所有任务完成: {results}") + print(f"总耗时: {end - start:.2f}秒") + + # 运行演示 + asyncio.run(run_concurrency_demo()) + + print("\n ✓ 任务管理和并发控制演示完成") + +# 运行任务管理和并发控制演示 +task_management_demo() +``` + +## 3. 异步上下文管理器和迭代器 + +### 3.1 异步上下文管理器 + +```python +import asyncio +import aiofiles +import aiohttp +from contextlib import asynccontextmanager + +def async_context_manager_demo(): + """异步上下文管理器演示""" + print("=== 异步上下文管理器演示 ===") + + # 1. 基础异步上下文管理器 + print("\n1. 基础异步上下文管理器:") + + basic_context_code = ''' +# 异步上下文管理器基础 +import asyncio + +class AsyncResource: + """异步资源管理器""" + + def __init__(self, name): + self.name = name + self.is_open = False + + async def __aenter__(self): + """异步进入上下文""" + print(f"正在打开资源: {self.name}") + await asyncio.sleep(0.1) # 模拟异步操作 + self.is_open = True + print(f"资源 {self.name} 已打开") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """异步退出上下文""" + print(f"正在关闭资源: {self.name}") + await asyncio.sleep(0.1) # 模拟异步操作 + self.is_open = False + print(f"资源 {self.name} 已关闭") + + # 处理异常 + if exc_type: + print(f"处理异常: {exc_type.__name__}: {exc_val}") + return False # 不抑制异常 + + async def do_work(self): + """执行工作""" + if not self.is_open: + raise RuntimeError("资源未打开") + print(f"使用资源 {self.name} 执行工作") + await asyncio.sleep(0.5) + return f"工作结果来自 {self.name}" + +# 使用异步上下文管理器 +async def use_async_context_manager(): + """使用异步上下文管理器""" + print("=== 使用异步上下文管理器 ===") + + # 正常使用 + async with AsyncResource("数据库连接") as resource: + result = await resource.do_work() + print(f"工作结果: {result}") + + print("\n--- 异常处理 ---") + + # 异常处理 + try: + async with AsyncResource("文件句柄") as resource: + await resource.do_work() + raise ValueError("模拟异常") + except ValueError as e: + print(f"捕获异常: {e}") + +# 运行示例 +asyncio.run(use_async_context_manager()) +''' + + print(" 基础异步上下文管理器代码:") + print(basic_context_code) + + # 2. 异步上下文管理器装饰器 + print("\n2. 异步上下文管理器装饰器:") + + decorator_code = ''' +# 使用@asynccontextmanager装饰器 +from contextlib import asynccontextmanager +import asyncio + +@asynccontextmanager +async def async_database_connection(db_url): + """异步数据库连接上下文管理器""" + print(f"连接到数据库: {db_url}") + connection = None + + try: + # 模拟建立连接 + await asyncio.sleep(0.2) + connection = {"url": db_url, "connected": True} + print("数据库连接已建立") + + yield connection # 返回连接对象 + + except Exception as e: + print(f"数据库操作异常: {e}") + raise + finally: + # 清理资源 + if connection: + print("关闭数据库连接") + await asyncio.sleep(0.1) + connection["connected"] = False + print("数据库连接已关闭") + +@asynccontextmanager +async def async_file_manager(filename, mode='r'): + """异步文件管理器""" + print(f"打开文件: {filename} (模式: {mode})") + + try: + # 模拟异步文件操作 + await asyncio.sleep(0.1) + file_obj = {"name": filename, "mode": mode, "content": ""} + print(f"文件 {filename} 已打开") + + yield file_obj + + except Exception as e: + print(f"文件操作异常: {e}") + raise + finally: + print(f"关闭文件: {filename}") + await asyncio.sleep(0.1) + print(f"文件 {filename} 已关闭") + +# 使用装饰器创建的上下文管理器 +async def use_decorator_context_managers(): + """使用装饰器创建的上下文管理器""" + print("=== 使用装饰器上下文管理器 ===") + + # 使用数据库连接 + async with async_database_connection("postgresql://localhost/mydb") as db: + print(f"执行数据库查询: {db}") + await asyncio.sleep(0.3) + print("查询完成") + + print("\n--- 文件操作 ---") + + # 使用文件管理器 + async with async_file_manager("data.txt", "w") as file: + print(f"写入文件: {file}") + file["content"] = "Hello, async world!" + await asyncio.sleep(0.2) + print("文件写入完成") + +# 运行示例 +asyncio.run(use_decorator_context_managers()) +''' + + print(" 装饰器上下文管理器代码:") + print(decorator_code) + + # 3. 实际应用示例 + print("\n3. 实际应用示例:") + + @asynccontextmanager + async def demo_connection_pool(pool_size=3): + """演示连接池上下文管理器""" + print(f"创建连接池 (大小: {pool_size})") + + # 模拟创建连接池 + pool = { + "connections": [f"conn_{i}" for i in range(pool_size)], + "available": list(range(pool_size)), + "in_use": [] + } + + try: + await asyncio.sleep(0.1) + print(f"连接池创建完成: {pool['connections']}") + yield pool + finally: + print("清理连接池") + await asyncio.sleep(0.1) + print("连接池已清理") + + async def run_context_demo(): + """运行上下文管理器演示""" + print("开始上下文管理器演示...") + + async with demo_connection_pool(2) as pool: + print(f"使用连接池: {pool['connections']}") + + # 模拟使用连接 + if pool['available']: + conn_id = pool['available'].pop(0) + pool['in_use'].append(conn_id) + print(f"获取连接: {pool['connections'][conn_id]}") + + await asyncio.sleep(1) + + # 释放连接 + pool['in_use'].remove(conn_id) + pool['available'].append(conn_id) + print(f"释放连接: {pool['connections'][conn_id]}") + + # 运行演示 + asyncio.run(run_context_demo()) + + print("\n ✓ 异步上下文管理器演示完成") + +# 运行异步上下文管理器演示 +async_context_manager_demo() +``` + +### 3.2 异步迭代器 + +```python +import asyncio +from typing import AsyncIterator + +def async_iterator_demo(): + """异步迭代器演示""" + print("=== 异步迭代器演示 ===") + + # 1. 基础异步迭代器 + print("\n1. 基础异步迭代器:") + + basic_iterator_code = ''' +# 异步迭代器基础 +import asyncio + +class AsyncRange: + """异步范围迭代器""" + + def __init__(self, start, stop, step=1, delay=0.1): + self.start = start + self.stop = stop + self.step = step + self.delay = delay + self.current = start + + def __aiter__(self): + """返回异步迭代器对象""" + return self + + async def __anext__(self): + """返回下一个值""" + if self.current >= self.stop: + raise StopAsyncIteration + + # 模拟异步操作 + await asyncio.sleep(self.delay) + + value = self.current + self.current += self.step + print(f"生成值: {value}") + return value + +# 使用异步迭代器 +async def use_async_iterator(): + """使用异步迭代器""" + print("=== 使用异步迭代器 ===") + + # 使用async for循环 + async for value in AsyncRange(0, 5): + print(f"处理值: {value}") + await asyncio.sleep(0.1) # 模拟处理时间 + + print("异步迭代完成") + +# 运行示例 +asyncio.run(use_async_iterator()) +''' + + print(" 基础异步迭代器代码:") + print(basic_iterator_code) + + # 2. 异步生成器 + print("\n2. 异步生成器:") + + async_generator_code = ''' +# 异步生成器 +import asyncio +import random + +async def async_data_generator(count, delay_range=(0.1, 0.5)): + """异步数据生成器""" + for i in range(count): + # 模拟异步数据获取 + delay = random.uniform(*delay_range) + await asyncio.sleep(delay) + + # 生成数据 + data = { + "id": i, + "value": random.randint(1, 100), + "timestamp": asyncio.get_event_loop().time() + } + + print(f"生成数据: {data}") + yield data + +async def async_file_reader(filename, chunk_size=1024): + """异步文件读取生成器""" + print(f"开始读取文件: {filename}") + + # 模拟文件内容 + content = "这是一个很长的文件内容," * 20 + + for i in range(0, len(content), chunk_size): + # 模拟异步读取 + await asyncio.sleep(0.1) + + chunk = content[i:i + chunk_size] + print(f"读取块 {i//chunk_size + 1}: {len(chunk)} 字符") + yield chunk + + print("文件读取完成") + +# 使用异步生成器 +async def use_async_generators(): + """使用异步生成器""" + print("=== 使用异步生成器 ===") + + # 1. 数据生成器 + print("\n1. 异步数据生成:") + data_count = 0 + async for data in async_data_generator(3): + print(f"处理数据: {data['id']} -> {data['value']}") + data_count += 1 + + print(f"总共处理了 {data_count} 条数据") + + # 2. 文件读取生成器 + print("\n2. 异步文件读取:") + chunks = [] + async for chunk in async_file_reader("example.txt", 50): + chunks.append(chunk) + print(f"累积内容长度: {sum(len(c) for c in chunks)}") + + print(f"文件读取完成,总共 {len(chunks)} 个块") + +# 运行示例 +asyncio.run(use_async_generators()) +''' + + print(" 异步生成器代码:") + print(async_generator_code) + + # 3. 实际应用示例 + print("\n3. 实际应用示例:") + + async def demo_data_stream(count=3): + """演示数据流生成器""" + for i in range(count): + await asyncio.sleep(0.5) + data = { + "id": i, + "message": f"数据项 {i}", + "timestamp": asyncio.get_event_loop().time() + } + print(f"[生成器] 产生数据: {data['message']}") + yield data + + async def demo_data_processor(): + """演示数据处理器""" + processed_count = 0 + + async for item in demo_data_stream(5): + print(f"[处理器] 处理: {item['message']}") + await asyncio.sleep(0.2) # 模拟处理时间 + processed_count += 1 + print(f"[处理器] 已处理 {processed_count} 项") + + print(f"数据处理完成,总计: {processed_count} 项") + + # 运行演示 + asyncio.run(demo_data_processor()) + + print("\n ✓ 异步迭代器演示完成") + +# 运行异步迭代器演示 +async_iterator_demo() +``` + +## 4. 异步网络编程 + +### 4.1 异步HTTP客户端 + +```python +import asyncio +import aiohttp +import time +from typing import List, Dict + +def async_http_client_demo(): + """异步HTTP客户端演示""" + print("=== 异步HTTP客户端演示 ===") + + # 1. 基础HTTP请求 + print("\n1. 基础HTTP请求:") + + basic_http_code = ''' +# 异步HTTP客户端基础 +import asyncio +import aiohttp +import time + +async def fetch_url(session, url): + """获取单个URL""" + try: + print(f"开始请求: {url}") + async with session.get(url) as response: + content = await response.text() + print(f"完成请求: {url} - 状态码: {response.status}") + return { + "url": url, + "status": response.status, + "content_length": len(content), + "headers": dict(response.headers) + } + except Exception as e: + print(f"请求失败: {url} - 错误: {e}") + return {"url": url, "error": str(e)} + +async def fetch_multiple_urls(urls): + """并发获取多个URL""" + print(f"=== 并发请求 {len(urls)} 个URL ===") + + start_time = time.time() + + # 创建HTTP会话 + async with aiohttp.ClientSession() as session: + # 创建任务列表 + tasks = [fetch_url(session, url) for url in urls] + + # 并发执行所有请求 + results = await asyncio.gather(*tasks, return_exceptions=True) + + end_time = time.time() + + print(f"\n所有请求完成,耗时: {end_time - start_time:.2f}秒") + + # 处理结果 + success_count = 0 + error_count = 0 + + for result in results: + if isinstance(result, Exception): + print(f"异常: {result}") + error_count += 1 + elif "error" in result: + print(f"错误: {result['url']} - {result['error']}") + error_count += 1 + else: + print(f"成功: {result['url']} - {result['status']} - {result['content_length']} 字符") + success_count += 1 + + print(f"\n统计: 成功 {success_count}, 失败 {error_count}") + return results + +# 示例URL列表 +test_urls = [ + "https://httpbin.org/delay/1", + "https://httpbin.org/delay/2", + "https://httpbin.org/status/200", + "https://httpbin.org/json", + "https://httpbin.org/headers" +] + +# 运行并发请求 +# asyncio.run(fetch_multiple_urls(test_urls)) +''' + + print(" 基础HTTP请求代码:") + print(basic_http_code) + + # 2. 高级HTTP功能 + print("\n2. 高级HTTP功能:") + + advanced_http_code = ''' +# 高级HTTP功能 +import asyncio +import aiohttp +import json + +class AsyncHTTPClient: + """异步HTTP客户端类""" + + def __init__(self, timeout=10, max_connections=100): + self.timeout = aiohttp.ClientTimeout(total=timeout) + self.connector = aiohttp.TCPConnector(limit=max_connections) + self.session = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession( + timeout=self.timeout, + connector=self.connector + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def get(self, url, headers=None, params=None): + """GET请求""" + async with self.session.get(url, headers=headers, params=params) as response: + return await self._process_response(response) + + async def post(self, url, data=None, json_data=None, headers=None): + """POST请求""" + kwargs = {"headers": headers} + if json_data: + kwargs["json"] = json_data + elif data: + kwargs["data"] = data + + async with self.session.post(url, **kwargs) as response: + return await self._process_response(response) + + async def _process_response(self, response): + """处理响应""" + content_type = response.headers.get('content-type', '') + + if 'application/json' in content_type: + content = await response.json() + else: + content = await response.text() + + return { + "status": response.status, + "headers": dict(response.headers), + "content": content, + "url": str(response.url) + } + + async def download_file(self, url, filename): + """下载文件""" + print(f"开始下载: {url} -> {filename}") + + async with self.session.get(url) as response: + if response.status == 200: + total_size = int(response.headers.get('content-length', 0)) + downloaded = 0 + + # 模拟文件写入(实际应用中使用aiofiles) + content = b"" + async for chunk in response.content.iter_chunked(8192): + content += chunk + downloaded += len(chunk) + + if total_size > 0: + progress = (downloaded / total_size) * 100 + print(f"下载进度: {progress:.1f}% ({downloaded}/{total_size})") + + print(f"下载完成: {filename} ({len(content)} 字节)") + return {"filename": filename, "size": len(content)} + else: + raise Exception(f"下载失败: HTTP {response.status}") + +# 使用高级HTTP客户端 +async def use_advanced_http_client(): + """使用高级HTTP客户端""" + print("=== 使用高级HTTP客户端 ===") + + async with AsyncHTTPClient(timeout=15) as client: + # GET请求 + print("\n1. GET请求:") + result = await client.get("https://httpbin.org/get", + params={"key": "value", "test": "async"}) + print(f"GET结果: {result['status']} - {len(str(result['content']))} 字符") + + # POST请求 + print("\n2. POST请求:") + post_data = {"name": "异步测试", "type": "demo"} + result = await client.post("https://httpbin.org/post", json_data=post_data) + print(f"POST结果: {result['status']}") + + # 文件下载 + print("\n3. 文件下载:") + try: + download_result = await client.download_file( + "https://httpbin.org/bytes/1024", + "test_file.bin" + ) + print(f"下载结果: {download_result}") + except Exception as e: + print(f"下载失败: {e}") + +# 运行高级HTTP客户端示例 +# asyncio.run(use_advanced_http_client()) +''' + + print(" 高级HTTP功能代码:") + print(advanced_http_code) + + # 3. 实际应用示例 + print("\n3. 实际应用示例:") + + async def demo_http_requests(): + """演示HTTP请求""" + print("开始HTTP请求演示...") + + # 模拟多个API端点 + endpoints = [ + {"name": "用户信息", "url": "https://jsonplaceholder.typicode.com/users/1"}, + {"name": "文章列表", "url": "https://jsonplaceholder.typicode.com/posts?_limit=3"}, + {"name": "评论数据", "url": "https://jsonplaceholder.typicode.com/comments?_limit=3"} + ] + + async def fetch_endpoint(session, endpoint): + try: + print(f"请求 {endpoint['name']}...") + async with session.get(endpoint['url']) as response: + if response.status == 200: + data = await response.json() + print(f"✓ {endpoint['name']} 获取成功") + return {"name": endpoint['name'], "data": data, "status": "success"} + else: + print(f"✗ {endpoint['name']} 请求失败: {response.status}") + return {"name": endpoint['name'], "status": "failed", "code": response.status} + except Exception as e: + print(f"✗ {endpoint['name']} 异常: {e}") + return {"name": endpoint['name'], "status": "error", "error": str(e)} + + # 并发请求所有端点 + start_time = time.time() + + async with aiohttp.ClientSession() as session: + tasks = [fetch_endpoint(session, ep) for ep in endpoints] + results = await asyncio.gather(*tasks) + + end_time = time.time() + + # 处理结果 + print(f"\n请求完成,总耗时: {end_time - start_time:.2f}秒") + + for result in results: + if result['status'] == 'success': + data_info = f"数据项: {len(result['data']) if isinstance(result['data'], list) else 1}" + print(f"✓ {result['name']}: {data_info}") + else: + print(f"✗ {result['name']}: {result['status']}") + + # 运行演示 + asyncio.run(demo_http_requests()) + + print("\n ✓ 异步HTTP客户端演示完成") + +# 运行异步HTTP客户端演示 +async_http_client_demo() +``` + +### 4.2 异步服务器编程 + +```python +import asyncio +from aiohttp import web, WSMsgType +import json +import time + +def async_server_demo(): + """异步服务器编程演示""" + print("=== 异步服务器编程演示 ===") + + # 1. 基础HTTP服务器 + print("\n1. 基础HTTP服务器:") + + basic_server_code = ''' +# 异步HTTP服务器基础 +import asyncio +from aiohttp import web +import json +import time + +# 全局数据存储 +server_data = { + "users": [ + {"id": 1, "name": "张三", "email": "zhangsan@example.com"}, + {"id": 2, "name": "李四", "email": "lisi@example.com"} + ], + "request_count": 0 +} + +async def hello_handler(request): + """Hello处理器""" + server_data["request_count"] += 1 + name = request.query.get('name', 'World') + + response_data = { + "message": f"Hello, {name}!", + "timestamp": time.time(), + "request_count": server_data["request_count"] + } + + return web.json_response(response_data) + +async def users_handler(request): + """用户列表处理器""" + server_data["request_count"] += 1 + + if request.method == 'GET': + # 获取用户列表 + return web.json_response({ + "users": server_data["users"], + "total": len(server_data["users"]) + }) + + elif request.method == 'POST': + # 创建新用户 + try: + data = await request.json() + new_user = { + "id": len(server_data["users"]) + 1, + "name": data.get("name"), + "email": data.get("email") + } + server_data["users"].append(new_user) + + return web.json_response(new_user, status=201) + except Exception as e: + return web.json_response( + {"error": str(e)}, + status=400 + ) + +async def user_detail_handler(request): + """用户详情处理器""" + server_data["request_count"] += 1 + user_id = int(request.match_info['user_id']) + + # 查找用户 + user = next((u for u in server_data["users"] if u["id"] == user_id), None) + + if user: + return web.json_response(user) + else: + return web.json_response( + {"error": "User not found"}, + status=404 + ) + +async def stats_handler(request): + """统计信息处理器""" + return web.json_response({ + "total_users": len(server_data["users"]), + "total_requests": server_data["request_count"], + "server_uptime": time.time() - start_time + }) + +# 中间件 +async def logging_middleware(request, handler): + """日志中间件""" + start = time.time() + print(f"[{time.strftime('%H:%M:%S')}] {request.method} {request.path}") + + response = await handler(request) + + process_time = time.time() - start + print(f"[{time.strftime('%H:%M:%S')}] 响应: {response.status} ({process_time:.3f}s)") + + return response + +# 创建应用 +def create_app(): + """创建Web应用""" + app = web.Application(middlewares=[logging_middleware]) + + # 添加路由 + app.router.add_get('/', hello_handler) + app.router.add_get('/hello', hello_handler) + app.router.add_get('/users', users_handler) + app.router.add_post('/users', users_handler) + app.router.add_get('/users/{user_id}', user_detail_handler) + app.router.add_get('/stats', stats_handler) + + return app + +# 启动服务器 +if __name__ == '__main__': + start_time = time.time() + app = create_app() + + print("启动异步HTTP服务器...") + print("访问 http://localhost:8080") + print("API端点:") + print(" GET /hello?name=YourName") + print(" GET /users") + print(" POST /users") + print(" GET /users/{id}") + print(" GET /stats") + + web.run_app(app, host='localhost', port=8080) +''' + + print(" 基础HTTP服务器代码:") + print(basic_server_code) + + # 2. WebSocket服务器 + print("\n2. WebSocket服务器:") + + websocket_server_code = ''' +# WebSocket服务器 +import asyncio +from aiohttp import web, WSMsgType +import json +import time +import weakref + +# WebSocket连接管理 +class WebSocketManager: + """WebSocket连接管理器""" + + def __init__(self): + self.connections = weakref.WeakSet() + self.rooms = {} # 房间管理 + + def add_connection(self, ws, room=None): + """添加连接""" + self.connections.add(ws) + if room: + if room not in self.rooms: + self.rooms[room] = weakref.WeakSet() + self.rooms[room].add(ws) + print(f"新连接加入,当前连接数: {len(self.connections)}") + + def remove_connection(self, ws, room=None): + """移除连接""" + if ws in self.connections: + self.connections.discard(ws) + if room and room in self.rooms: + self.rooms[room].discard(ws) + print(f"连接断开,当前连接数: {len(self.connections)}") + + async def broadcast(self, message, room=None): + """广播消息""" + if room and room in self.rooms: + connections = self.rooms[room] + else: + connections = self.connections + + if connections: + await asyncio.gather( + *[ws.send_str(json.dumps(message)) for ws in connections], + return_exceptions=True + ) + +# 全局WebSocket管理器 +ws_manager = WebSocketManager() + +async def websocket_handler(request): + """WebSocket处理器""" + ws = web.WebSocketResponse() + await ws.prepare(request) + + # 获取房间信息 + room = request.query.get('room', 'default') + user_name = request.query.get('name', f'用户{int(time.time()) % 1000}') + + # 添加连接 + ws_manager.add_connection(ws, room) + + # 发送欢迎消息 + welcome_msg = { + "type": "welcome", + "message": f"欢迎 {user_name} 加入房间 {room}", + "user": user_name, + "room": room, + "timestamp": time.time() + } + await ws.send_str(json.dumps(welcome_msg)) + + # 通知其他用户 + join_msg = { + "type": "user_join", + "message": f"{user_name} 加入了房间", + "user": user_name, + "room": room, + "timestamp": time.time() + } + await ws_manager.broadcast(join_msg, room) + + try: + # 处理消息 + async for msg in ws: + if msg.type == WSMsgType.TEXT: + try: + data = json.loads(msg.data) + + # 处理不同类型的消息 + if data.get('type') == 'chat': + # 聊天消息 + chat_msg = { + "type": "chat", + "user": user_name, + "message": data.get('message', ''), + "room": room, + "timestamp": time.time() + } + await ws_manager.broadcast(chat_msg, room) + + elif data.get('type') == 'ping': + # 心跳检测 + pong_msg = { + "type": "pong", + "timestamp": time.time() + } + await ws.send_str(json.dumps(pong_msg)) + + except json.JSONDecodeError: + error_msg = { + "type": "error", + "message": "无效的JSON格式" + } + await ws.send_str(json.dumps(error_msg)) + + elif msg.type == WSMsgType.ERROR: + print(f'WebSocket错误: {ws.exception()}') + break + + except Exception as e: + print(f"WebSocket处理异常: {e}") + + finally: + # 清理连接 + ws_manager.remove_connection(ws, room) + + # 通知其他用户 + leave_msg = { + "type": "user_leave", + "message": f"{user_name} 离开了房间", + "user": user_name, + "room": room, + "timestamp": time.time() + } + await ws_manager.broadcast(leave_msg, room) + + return ws + +async def websocket_stats_handler(request): + """WebSocket统计信息""" + stats = { + "total_connections": len(ws_manager.connections), + "rooms": {room: len(connections) for room, connections in ws_manager.rooms.items()}, + "timestamp": time.time() + } + return web.json_response(stats) + +# WebSocket客户端示例页面 +async def websocket_client_page(request): + """WebSocket客户端页面""" + html = ''' + + + + WebSocket聊天室 + + + +
+ + + + + + + + ''' + return web.Response(text=html, content_type='text/html') + +# 创建WebSocket应用 +def create_websocket_app(): + """创建WebSocket应用""" + app = web.Application() + + # 添加路由 + app.router.add_get('/ws', websocket_handler) + app.router.add_get('/ws/stats', websocket_stats_handler) + app.router.add_get('/chat', websocket_client_page) + + return app + +# 启动WebSocket服务器 +if __name__ == '__main__': + app = create_websocket_app() + + print("启动WebSocket服务器...") + print("访问 http://localhost:8080/chat 测试聊天室") + print("WebSocket端点: ws://localhost:8080/ws") + print("统计信息: http://localhost:8080/ws/stats") + + web.run_app(app, host='localhost', port=8080) +''' + + print(" WebSocket服务器代码:") + print(websocket_server_code) + + # 3. 实际应用示例 + print("\n3. 实际应用示例:") + + async def demo_simple_server(): + """演示简单服务器""" + print("创建演示服务器...") + + async def demo_handler(request): + return web.json_response({ + "message": "Hello from async server!", + "path": request.path, + "method": request.method, + "timestamp": time.time() + }) + + app = web.Application() + app.router.add_get('/', demo_handler) + app.router.add_get('/demo', demo_handler) + + print("演示服务器配置完成") + print("在实际应用中,可以通过 web.run_app(app, host='localhost', port=8080) 启动") + + return app + + # 运行演示 + asyncio.run(demo_simple_server()) + + print("\n ✓ 异步服务器编程演示完成") + +# 运行异步服务器演示 +async_server_demo() +``` + +## 5. 实际应用案例 + +### 5.1 异步文件处理系统 + +```python +import asyncio +import aiofiles +import aiohttp +import hashlib +import os +from pathlib import Path +from typing import List, Dict + +def async_file_system_demo(): + """异步文件处理系统演示""" + print("=== 异步文件处理系统演示 ===") + + # 1. 异步文件操作 + print("\n1. 异步文件操作:") + + file_operations_code = ''' +# 异步文件操作 +import asyncio +import aiofiles +import hashlib +import os +from pathlib import Path + +class AsyncFileProcessor: + """异步文件处理器""" + + def __init__(self, base_dir="./async_files"): + self.base_dir = Path(base_dir) + self.base_dir.mkdir(exist_ok=True) + + async def create_file(self, filename, content): + """创建文件""" + file_path = self.base_dir / filename + + async with aiofiles.open(file_path, 'w', encoding='utf-8') as f: + await f.write(content) + + print(f"文件已创建: {file_path}") + return file_path + + async def read_file(self, filename): + """读取文件""" + file_path = self.base_dir / filename + + if not file_path.exists(): + raise FileNotFoundError(f"文件不存在: {file_path}") + + async with aiofiles.open(file_path, 'r', encoding='utf-8') as f: + content = await f.read() + + print(f"文件已读取: {file_path} ({len(content)} 字符)") + return content + + async def copy_file(self, src_filename, dst_filename): + """复制文件""" + src_path = self.base_dir / src_filename + dst_path = self.base_dir / dst_filename + + async with aiofiles.open(src_path, 'rb') as src: + async with aiofiles.open(dst_path, 'wb') as dst: + while True: + chunk = await src.read(8192) + if not chunk: + break + await dst.write(chunk) + + print(f"文件已复制: {src_path} -> {dst_path}") + return dst_path + + async def calculate_hash(self, filename): + """计算文件哈希""" + file_path = self.base_dir / filename + hash_md5 = hashlib.md5() + + async with aiofiles.open(file_path, 'rb') as f: + while True: + chunk = await f.read(8192) + if not chunk: + break + hash_md5.update(chunk) + + file_hash = hash_md5.hexdigest() + print(f"文件哈希: {filename} -> {file_hash}") + return file_hash + + async def process_multiple_files(self, file_list): + """并发处理多个文件""" + print(f"开始并发处理 {len(file_list)} 个文件...") + + tasks = [] + for file_info in file_list: + if file_info['action'] == 'create': + task = self.create_file(file_info['name'], file_info['content']) + elif file_info['action'] == 'read': + task = self.read_file(file_info['name']) + elif file_info['action'] == 'hash': + task = self.calculate_hash(file_info['name']) + else: + continue + + tasks.append(task) + + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 处理结果 + success_count = 0 + error_count = 0 + + for i, result in enumerate(results): + if isinstance(result, Exception): + print(f"任务 {i} 失败: {result}") + error_count += 1 + else: + success_count += 1 + + print(f"处理完成: 成功 {success_count}, 失败 {error_count}") + return results + +# 使用异步文件处理器 +async def use_async_file_processor(): + """使用异步文件处理器""" + print("=== 使用异步文件处理器 ===") + + processor = AsyncFileProcessor() + + # 1. 创建测试文件 + test_files = [ + {"action": "create", "name": "test1.txt", "content": "这是测试文件1的内容\n" * 100}, + {"action": "create", "name": "test2.txt", "content": "这是测试文件2的内容\n" * 200}, + {"action": "create", "name": "test3.txt", "content": "这是测试文件3的内容\n" * 150} + ] + + await processor.process_multiple_files(test_files) + + # 2. 并发读取和计算哈希 + read_tasks = [ + {"action": "read", "name": "test1.txt"}, + {"action": "hash", "name": "test2.txt"}, + {"action": "hash", "name": "test3.txt"} + ] + + await processor.process_multiple_files(read_tasks) + + # 3. 复制文件 + await processor.copy_file("test1.txt", "test1_copy.txt") + + print("文件处理演示完成") + +# 运行示例 +# asyncio.run(use_async_file_processor()) +''' + + print(" 异步文件操作代码:") + print(file_operations_code) + + # 2. 异步数据采集系统 + print("\n2. 异步数据采集系统:") + + data_collector_code = ''' +# 异步数据采集系统 +import asyncio +import aiohttp +import aiofiles +import json +import time +from datetime import datetime + +class AsyncDataCollector: + """异步数据采集器""" + + def __init__(self, output_dir="./collected_data"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(exist_ok=True) + self.session = None + self.collected_data = [] + + async def __aenter__(self): + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def fetch_api_data(self, api_config): + """获取API数据""" + try: + print(f"获取数据: {api_config['name']}") + + async with self.session.get(api_config['url']) as response: + if response.status == 200: + if 'application/json' in response.headers.get('content-type', ''): + data = await response.json() + else: + data = await response.text() + + result = { + "source": api_config['name'], + "url": api_config['url'], + "data": data, + "timestamp": datetime.now().isoformat(), + "status": "success" + } + + print(f"✓ {api_config['name']} 数据获取成功") + return result + else: + raise Exception(f"HTTP {response.status}") + + except Exception as e: + print(f"✗ {api_config['name']} 数据获取失败: {e}") + return { + "source": api_config['name'], + "url": api_config['url'], + "error": str(e), + "timestamp": datetime.now().isoformat(), + "status": "error" + } + + async def save_data(self, data, filename=None): + """保存数据到文件""" + if filename is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"collected_data_{timestamp}.json" + + file_path = self.output_dir / filename + + async with aiofiles.open(file_path, 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, ensure_ascii=False, indent=2)) + + print(f"数据已保存: {file_path}") + return file_path + + async def collect_batch(self, api_configs, save_individual=True): + """批量采集数据""" + print(f"开始批量采集 {len(api_configs)} 个数据源...") + + start_time = time.time() + + # 并发获取所有数据 + tasks = [self.fetch_api_data(config) for config in api_configs] + results = await asyncio.gather(*tasks) + + end_time = time.time() + + # 统计结果 + success_results = [r for r in results if r.get('status') == 'success'] + error_results = [r for r in results if r.get('status') == 'error'] + + print(f"采集完成: 成功 {len(success_results)}, 失败 {len(error_results)}, 耗时 {end_time - start_time:.2f}秒") + + # 保存数据 + if save_individual: + # 分别保存每个数据源 + save_tasks = [] + for result in success_results: + filename = f"{result['source']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + save_tasks.append(self.save_data(result, filename)) + + if save_tasks: + await asyncio.gather(*save_tasks) + + # 保存汇总数据 + summary = { + "collection_time": datetime.now().isoformat(), + "total_sources": len(api_configs), + "successful": len(success_results), + "failed": len(error_results), + "duration_seconds": end_time - start_time, + "results": results + } + + await self.save_data(summary, "collection_summary.json") + + return summary + +# 使用数据采集器 +async def use_data_collector(): + """使用数据采集器""" + print("=== 使用异步数据采集器 ===") + + # 配置数据源 + api_configs = [ + {"name": "用户数据", "url": "https://jsonplaceholder.typicode.com/users"}, + {"name": "文章数据", "url": "https://jsonplaceholder.typicode.com/posts?_limit=5"}, + {"name": "评论数据", "url": "https://jsonplaceholder.typicode.com/comments?_limit=5"}, + {"name": "相册数据", "url": "https://jsonplaceholder.typicode.com/albums?_limit=3"} + ] + + async with AsyncDataCollector() as collector: + summary = await collector.collect_batch(api_configs) + + print(f"\n采集汇总:") + print(f" 总数据源: {summary['total_sources']}") + print(f" 成功: {summary['successful']}") + print(f" 失败: {summary['failed']}") + print(f" 耗时: {summary['duration_seconds']:.2f}秒") + +# 运行示例 +# asyncio.run(use_data_collector()) +''' + + print(" 异步数据采集系统代码:") + print(data_collector_code) + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + async def demo_file_operations(): + """演示文件操作""" + print("开始文件操作演示...") + + # 模拟创建多个文件 + files_to_create = [ + {"name": f"demo_{i}.txt", "content": f"演示文件 {i} 的内容\n" * (i + 1) * 10} + for i in range(3) + ] + + print(f"创建 {len(files_to_create)} 个演示文件...") + + # 模拟并发文件操作 + async def create_demo_file(file_info): + await asyncio.sleep(0.1) # 模拟文件创建时间 + print(f"创建文件: {file_info['name']} ({len(file_info['content'])} 字符)") + return {"name": file_info['name'], "size": len(file_info['content'])} + + # 并发创建文件 + tasks = [create_demo_file(file_info) for file_info in files_to_create] + results = await asyncio.gather(*tasks) + + total_size = sum(result['size'] for result in results) + print(f"文件创建完成,总大小: {total_size} 字符") + + return results + + # 运行演示 + asyncio.run(demo_file_operations()) + + print("\n ✓ 异步文件处理系统演示完成") + +# 运行异步文件处理系统演示 +async_file_system_demo() +``` + +### 5.2 异步爬虫系统 + +```python +import asyncio +import aiohttp +from bs4 import BeautifulSoup +import time +from urllib.parse import urljoin, urlparse + +def async_crawler_demo(): + """异步爬虫系统演示""" + print("=== 异步爬虫系统演示 ===") + + # 1. 基础异步爬虫 + print("\n1. 基础异步爬虫:") + + basic_crawler_code = ''' +# 异步爬虫基础 +import asyncio +import aiohttp +from bs4 import BeautifulSoup +import time +from urllib.parse import urljoin, urlparse + +class AsyncWebCrawler: + """异步网页爬虫""" + + def __init__(self, max_concurrent=10, delay=1.0): + self.max_concurrent = max_concurrent + self.delay = delay + self.session = None + self.semaphore = asyncio.Semaphore(max_concurrent) + self.visited_urls = set() + self.results = [] + + async def __aenter__(self): + self.session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30), + headers={'User-Agent': 'AsyncCrawler/1.0'} + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + + async def fetch_page(self, url): + """获取单个页面""" + async with self.semaphore: + if url in self.visited_urls: + return None + + self.visited_urls.add(url) + + try: + print(f"爬取: {url}") + + async with self.session.get(url) as response: + if response.status == 200: + content = await response.text() + + # 解析页面 + soup = BeautifulSoup(content, 'html.parser') + + result = { + "url": url, + "title": soup.title.string if soup.title else "无标题", + "content_length": len(content), + "links": self._extract_links(soup, url), + "status": "success", + "timestamp": time.time() + } + + print(f"✓ 爬取成功: {url} - {result['title']}") + + # 添加延迟 + await asyncio.sleep(self.delay) + + return result + else: + print(f"✗ HTTP错误: {url} - {response.status}") + return {"url": url, "status": "error", "error": f"HTTP {response.status}"} + + except Exception as e: + print(f"✗ 爬取失败: {url} - {e}") + return {"url": url, "status": "error", "error": str(e)} + + def _extract_links(self, soup, base_url): + """提取页面链接""" + links = [] + for link in soup.find_all('a', href=True): + href = link['href'] + full_url = urljoin(base_url, href) + + # 只保留HTTP/HTTPS链接 + if full_url.startswith(('http://', 'https://')): + links.append({ + "url": full_url, + "text": link.get_text(strip=True)[:100] # 限制文本长度 + }) + + return links[:10] # 限制链接数量 + + async def crawl_urls(self, urls, max_depth=1): + """爬取URL列表""" + print(f"开始爬取 {len(urls)} 个URL,最大深度: {max_depth}") + + start_time = time.time() + current_urls = list(urls) + + for depth in range(max_depth): + print(f"\n=== 深度 {depth + 1} ===") + + if not current_urls: + break + + # 并发爬取当前层级的URL + tasks = [self.fetch_page(url) for url in current_urls] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 处理结果 + next_urls = set() + for result in results: + if isinstance(result, Exception): + print(f"异常: {result}") + continue + + if result and result.get('status') == 'success': + self.results.append(result) + + # 收集下一层的URL + if depth < max_depth - 1: + for link in result.get('links', []): + if link['url'] not in self.visited_urls: + next_urls.add(link['url']) + + current_urls = list(next_urls)[:5] # 限制下一层URL数量 + + end_time = time.time() + + print(f"\n爬取完成:") + print(f" 总页面: {len(self.results)}") + print(f" 总耗时: {end_time - start_time:.2f}秒") + print(f" 平均速度: {len(self.results) / (end_time - start_time):.2f} 页面/秒") + + return self.results + +# 使用异步爬虫 +async def use_async_crawler(): + """使用异步爬虫""" + print("=== 使用异步爬虫 ===") + + # 测试URL列表 + test_urls = [ + "https://httpbin.org/html", + "https://httpbin.org/links/5", + "https://httpbin.org/forms/post" + ] + + async with AsyncWebCrawler(max_concurrent=3, delay=0.5) as crawler: + results = await crawler.crawl_urls(test_urls, max_depth=2) + + # 显示结果摘要 + print("\n=== 爬取结果摘要 ===") + for result in results: + print(f" {result['title'][:50]}... - {result['url']}") + +# 运行示例 +# asyncio.run(use_async_crawler()) +''' + + print(" 基础异步爬虫代码:") + print(basic_crawler_code) + + # 2. 实际应用演示 + print("\n2. 实际应用演示:") + + async def demo_simple_crawler(): + """演示简单爬虫""" + print("开始简单爬虫演示...") + + # 模拟爬取多个页面 + urls_to_crawl = [ + "https://httpbin.org/html", + "https://httpbin.org/json", + "https://httpbin.org/xml" + ] + + async def fetch_demo_page(session, url): + try: + print(f"爬取演示页面: {url}") + async with session.get(url) as response: + if response.status == 200: + content = await response.text() + print(f"✓ 页面获取成功: {url} ({len(content)} 字符)") + return {"url": url, "size": len(content), "status": "success"} + else: + print(f"✗ 页面获取失败: {url} - HTTP {response.status}") + return {"url": url, "status": "failed", "code": response.status} + except Exception as e: + print(f"✗ 爬取异常: {url} - {e}") + return {"url": url, "status": "error", "error": str(e)} + + # 并发爬取 + async with aiohttp.ClientSession() as session: + tasks = [fetch_demo_page(session, url) for url in urls_to_crawl] + results = await asyncio.gather(*tasks) + + # 统计结果 + success_count = sum(1 for r in results if r.get('status') == 'success') + total_size = sum(r.get('size', 0) for r in results if r.get('status') == 'success') + + print(f"\n爬取统计:") + print(f" 成功页面: {success_count}/{len(urls_to_crawl)}") + print(f" 总内容大小: {total_size} 字符") + + return results + + # 运行演示 + asyncio.run(demo_simple_crawler()) + + print("\n ✓ 异步爬虫系统演示完成") + +# 运行异步爬虫演示 +async_crawler_demo() +``` + +## 6. 性能优化和最佳实践 + +### 6.1 性能优化技巧 + +```python +import asyncio +import time +from concurrent.futures import ThreadPoolExecutor + +def async_performance_demo(): + """异步性能优化演示""" + print("=== 异步性能优化演示 ===") + + # 1. 并发控制 + print("\n1. 并发控制:") + + concurrency_control_code = ''' +# 并发控制优化 +import asyncio +import time + +# 1. 使用信号量控制并发数 +async def controlled_task(semaphore, task_id, duration): + """受控制的任务""" + async with semaphore: + print(f"任务 {task_id} 开始执行") + await asyncio.sleep(duration) + print(f"任务 {task_id} 执行完成") + return f"结果_{task_id}" + +async def demo_semaphore_control(): + """演示信号量控制""" + print("=== 信号量并发控制 ===") + + # 限制最多3个并发任务 + semaphore = asyncio.Semaphore(3) + + # 创建10个任务 + tasks = [ + controlled_task(semaphore, i, 1.0) + for i in range(10) + ] + + start_time = time.time() + results = await asyncio.gather(*tasks) + end_time = time.time() + + print(f"完成 {len(results)} 个任务,耗时: {end_time - start_time:.2f}秒") + return results + +# 2. 任务分批处理 +async def batch_process_tasks(tasks, batch_size=5): + """分批处理任务""" + print(f"分批处理 {len(tasks)} 个任务,批大小: {batch_size}") + + results = [] + + for i in range(0, len(tasks), batch_size): + batch = tasks[i:i + batch_size] + print(f"处理批次 {i//batch_size + 1}: {len(batch)} 个任务") + + batch_results = await asyncio.gather(*batch) + results.extend(batch_results) + + # 批次间添加小延迟 + if i + batch_size < len(tasks): + await asyncio.sleep(0.1) + + print(f"所有批次处理完成,总结果: {len(results)}") + return results + +# 3. 连接池优化 +class AsyncConnectionPool: + """异步连接池""" + + def __init__(self, max_connections=10): + self.max_connections = max_connections + self.available_connections = asyncio.Queue(maxsize=max_connections) + self.total_connections = 0 + + async def get_connection(self): + """获取连接""" + try: + # 尝试从池中获取现有连接 + connection = self.available_connections.get_nowait() + print(f"复用连接: {connection}") + return connection + except asyncio.QueueEmpty: + # 创建新连接 + if self.total_connections < self.max_connections: + connection = f"conn_{self.total_connections}" + self.total_connections += 1 + print(f"创建新连接: {connection}") + return connection + else: + # 等待可用连接 + print("等待可用连接...") + return await self.available_connections.get() + + async def return_connection(self, connection): + """归还连接""" + try: + self.available_connections.put_nowait(connection) + print(f"归还连接: {connection}") + except asyncio.QueueFull: + print(f"连接池已满,丢弃连接: {connection}") + +# 运行并发控制演示 +# asyncio.run(demo_semaphore_control()) +''' + + print(" 并发控制代码:") + print(concurrency_control_code) + + # 2. 内存优化 + print("\n2. 内存优化:") + + memory_optimization_code = ''' +# 内存优化技巧 +import asyncio +import weakref +from typing import AsyncIterator + +# 1. 使用异步生成器减少内存占用 +async def memory_efficient_data_processor(data_source): + """内存高效的数据处理器""" + async for item in data_source: + # 处理单个数据项 + processed_item = await process_single_item(item) + + # 立即yield,不在内存中累积 + yield processed_item + + # 可选:触发垃圾回收 + if processed_item['id'] % 100 == 0: + import gc + gc.collect() + +async def process_single_item(item): + """处理单个数据项""" + await asyncio.sleep(0.01) # 模拟处理时间 + return { + "id": item.get("id"), + "processed": True, + "result": f"processed_{item.get('value', 'unknown')}" + } + +# 2. 弱引用管理连接 +class WeakConnectionManager: + """使用弱引用管理连接""" + + def __init__(self): + self._connections = weakref.WeakSet() + + def add_connection(self, conn): + """添加连接""" + self._connections.add(conn) + print(f"添加连接,当前连接数: {len(self._connections)}") + + def get_connection_count(self): + """获取当前连接数""" + return len(self._connections) + + async def broadcast_to_all(self, message): + """向所有连接广播消息""" + if self._connections: + tasks = [] + for conn in self._connections: + tasks.append(conn.send_message(message)) + + await asyncio.gather(*tasks, return_exceptions=True) + +# 3. 流式处理大数据 +async def stream_large_dataset(dataset_size=10000): + """流式处理大数据集""" + print(f"开始流式处理 {dataset_size} 条数据...") + + processed_count = 0 + + async def data_generator(): + """数据生成器""" + for i in range(dataset_size): + yield {"id": i, "value": f"data_{i}"} + + # 每1000条数据暂停一下 + if i % 1000 == 0: + await asyncio.sleep(0.01) + + # 流式处理 + async for processed_item in memory_efficient_data_processor(data_generator()): + processed_count += 1 + + # 定期报告进度 + if processed_count % 1000 == 0: + print(f"已处理: {processed_count}/{dataset_size}") + + print(f"流式处理完成,总计: {processed_count} 条数据") + return processed_count + +# 运行内存优化演示 +# asyncio.run(stream_large_dataset(5000)) +''' + + print(" 内存优化代码:") + print(memory_optimization_code) + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + async def demo_performance_optimization(): + """演示性能优化""" + print("开始性能优化演示...") + + # 1. 并发控制演示 + semaphore = asyncio.Semaphore(3) + + async def demo_task(task_id): + async with semaphore: + print(f"执行任务 {task_id}") + await asyncio.sleep(0.5) + return f"任务{task_id}完成" + + # 创建多个任务 + tasks = [demo_task(i) for i in range(8)] + + start_time = time.time() + results = await asyncio.gather(*tasks) + end_time = time.time() + + print(f"并发控制演示完成: {len(results)} 个任务,耗时 {end_time - start_time:.2f}秒") + + # 2. 批处理演示 + print("\n批处理演示:") + + async def batch_task(batch_id, items): + print(f"处理批次 {batch_id}: {len(items)} 个项目") + await asyncio.sleep(0.3) + return f"批次{batch_id}处理完成" + + # 分批处理 + all_items = list(range(20)) + batch_size = 5 + batch_tasks = [] + + for i in range(0, len(all_items), batch_size): + batch = all_items[i:i + batch_size] + batch_tasks.append(batch_task(i // batch_size, batch)) + + batch_results = await asyncio.gather(*batch_tasks) + print(f"批处理完成: {len(batch_results)} 个批次") + + return {"concurrent_results": len(results), "batch_results": len(batch_results)} + + # 运行演示 + asyncio.run(demo_performance_optimization()) + + print("\n ✓ 性能优化演示完成") + +# 运行性能优化演示 +async_performance_demo() +``` + +### 6.2 最佳实践 + +```python +def async_best_practices_demo(): + """异步编程最佳实践演示""" + print("=== 异步编程最佳实践演示 ===") + + # 1. 错误处理最佳实践 + print("\n1. 错误处理最佳实践:") + + error_handling_code = ''' +# 异步错误处理最佳实践 +import asyncio +import logging +from typing import List, Optional + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class AsyncTaskManager: + """异步任务管理器""" + + def __init__(self): + self.failed_tasks = [] + self.successful_tasks = [] + + async def execute_task_safely(self, task_func, *args, **kwargs): + """安全执行任务""" + try: + result = await task_func(*args, **kwargs) + self.successful_tasks.append({ + "task": task_func.__name__, + "result": result, + "status": "success" + }) + return result + + except asyncio.TimeoutError: + error_msg = f"任务超时: {task_func.__name__}" + logger.error(error_msg) + self.failed_tasks.append({ + "task": task_func.__name__, + "error": "timeout", + "status": "failed" + }) + return None + + except Exception as e: + error_msg = f"任务异常: {task_func.__name__} - {e}" + logger.error(error_msg) + self.failed_tasks.append({ + "task": task_func.__name__, + "error": str(e), + "status": "failed" + }) + return None + + async def execute_tasks_with_retry(self, tasks, max_retries=3): + """带重试的任务执行""" + results = [] + + for task_info in tasks: + task_func = task_info["func"] + args = task_info.get("args", []) + kwargs = task_info.get("kwargs", {}) + + for attempt in range(max_retries + 1): + try: + if attempt > 0: + logger.info(f"重试任务 {task_func.__name__} (第{attempt}次)") + await asyncio.sleep(2 ** attempt) # 指数退避 + + result = await asyncio.wait_for( + task_func(*args, **kwargs), + timeout=10.0 + ) + + results.append(result) + logger.info(f"任务成功: {task_func.__name__}") + break + + except Exception as e: + if attempt == max_retries: + logger.error(f"任务最终失败: {task_func.__name__} - {e}") + results.append(None) + else: + logger.warning(f"任务失败,将重试: {task_func.__name__} - {e}") + + return results + + def get_summary(self): + """获取执行摘要""" + return { + "successful": len(self.successful_tasks), + "failed": len(self.failed_tasks), + "total": len(self.successful_tasks) + len(self.failed_tasks), + "success_rate": len(self.successful_tasks) / max(1, len(self.successful_tasks) + len(self.failed_tasks)) + } + +# 示例任务函数 +async def reliable_task(task_id, success_rate=0.8): + """可靠性测试任务""" + import random + + await asyncio.sleep(0.1) # 模拟工作 + + if random.random() < success_rate: + return f"任务{task_id}成功" + else: + raise Exception(f"任务{task_id}随机失败") + +async def timeout_task(duration): + """可能超时的任务""" + await asyncio.sleep(duration) + return f"任务完成,耗时{duration}秒" + +# 使用示例 +async def demo_error_handling(): + """演示错误处理""" + manager = AsyncTaskManager() + + # 1. 安全执行任务 + print("=== 安全任务执行 ===") + + tasks = [ + manager.execute_task_safely(reliable_task, i, 0.7) + for i in range(5) + ] + + results = await asyncio.gather(*tasks) + print(f"安全执行结果: {[r for r in results if r is not None]}") + + # 2. 带重试的任务执行 + print("\n=== 带重试的任务执行 ===") + + retry_tasks = [ + {"func": reliable_task, "args": [i], "kwargs": {"success_rate": 0.3}} + for i in range(3) + ] + + retry_results = await manager.execute_tasks_with_retry(retry_tasks, max_retries=2) + print(f"重试执行结果: {retry_results}") + + # 3. 显示摘要 + summary = manager.get_summary() + print(f"\n执行摘要: {summary}") + +# 运行错误处理演示 +# asyncio.run(demo_error_handling()) +''' + + print(" 错误处理最佳实践代码:") + print(error_handling_code) + + # 2. 资源管理最佳实践 + print("\n2. 资源管理最佳实践:") + + resource_management_code = ''' +# 资源管理最佳实践 +import asyncio +from contextlib import asynccontextmanager +import aiohttp +import aiofiles + +class AsyncResourceManager: + """异步资源管理器""" + + def __init__(self): + self.active_resources = set() + self.resource_stats = { + "created": 0, + "destroyed": 0, + "active": 0 + } + + @asynccontextmanager + async def managed_http_session(self, **kwargs): + """管理HTTP会话""" + session = aiohttp.ClientSession(**kwargs) + self.active_resources.add(session) + self.resource_stats["created"] += 1 + self.resource_stats["active"] += 1 + + try: + print(f"创建HTTP会话,当前活跃资源: {self.resource_stats['active']}") + yield session + finally: + await session.close() + self.active_resources.discard(session) + self.resource_stats["destroyed"] += 1 + self.resource_stats["active"] -= 1 + print(f"关闭HTTP会话,当前活跃资源: {self.resource_stats['active']}") + + @asynccontextmanager + async def managed_file(self, filename, mode='r', **kwargs): + """管理文件资源""" + file_handle = await aiofiles.open(filename, mode, **kwargs) + self.active_resources.add(file_handle) + self.resource_stats["created"] += 1 + self.resource_stats["active"] += 1 + + try: + print(f"打开文件 {filename},当前活跃资源: {self.resource_stats['active']}") + yield file_handle + finally: + await file_handle.close() + self.active_resources.discard(file_handle) + self.resource_stats["destroyed"] += 1 + self.resource_stats["active"] -= 1 + print(f"关闭文件 {filename},当前活跃资源: {self.resource_stats['active']}") + + async def cleanup_all_resources(self): + """清理所有资源""" + print(f"开始清理 {len(self.active_resources)} 个活跃资源...") + + cleanup_tasks = [] + for resource in list(self.active_resources): + if hasattr(resource, 'close'): + cleanup_tasks.append(resource.close()) + + if cleanup_tasks: + await asyncio.gather(*cleanup_tasks, return_exceptions=True) + + self.active_resources.clear() + self.resource_stats["active"] = 0 + print("所有资源已清理") + + def get_resource_stats(self): + """获取资源统计""" + return self.resource_stats.copy() + +# 使用示例 +async def demo_resource_management(): + """演示资源管理""" + manager = AsyncResourceManager() + + try: + # 1. HTTP会话管理 + print("=== HTTP会话管理 ===") + + async with manager.managed_http_session() as session: + async with session.get('https://httpbin.org/json') as response: + data = await response.json() + print(f"获取数据: {len(str(data))} 字符") + + # 2. 文件资源管理 + print("\n=== 文件资源管理 ===") + + # 创建测试文件 + async with manager.managed_file('test_resource.txt', 'w') as f: + await f.write("测试资源管理\n" * 100) + + # 读取测试文件 + async with manager.managed_file('test_resource.txt', 'r') as f: + content = await f.read() + print(f"读取文件内容: {len(content)} 字符") + + # 3. 并发资源使用 + print("\n=== 并发资源使用 ===") + + async def concurrent_http_task(task_id): + async with manager.managed_http_session() as session: + async with session.get(f'https://httpbin.org/delay/{task_id % 3 + 1}') as response: + return await response.json() + + # 并发执行多个HTTP任务 + tasks = [concurrent_http_task(i) for i in range(3)] + results = await asyncio.gather(*tasks, return_exceptions=True) + + print(f"并发任务完成: {len([r for r in results if not isinstance(r, Exception)])} 个成功") + + finally: + # 确保清理所有资源 + await manager.cleanup_all_resources() + + # 显示资源统计 + stats = manager.get_resource_stats() + print(f"\n资源统计: {stats}") + +# 运行资源管理演示 +# asyncio.run(demo_resource_management()) +''' + + print(" 资源管理最佳实践代码:") + print(resource_management_code) + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + async def demo_best_practices(): + """演示最佳实践""" + print("开始最佳实践演示...") + + # 1. 任务超时控制 + async def timeout_controlled_task(duration): + try: + result = await asyncio.wait_for( + asyncio.sleep(duration), + timeout=2.0 + ) + return f"任务完成: {duration}秒" + except asyncio.TimeoutError: + return f"任务超时: {duration}秒" + + # 测试不同持续时间的任务 + durations = [1, 3, 0.5, 4] + tasks = [timeout_controlled_task(d) for d in durations] + results = await asyncio.gather(*tasks) + + print("超时控制结果:") + for duration, result in zip(durations, results): + print(f" {duration}秒任务: {result}") + + # 2. 优雅关闭演示 + print("\n优雅关闭演示:") + + shutdown_event = asyncio.Event() + + async def long_running_task(): + count = 0 + while not shutdown_event.is_set(): + count += 1 + print(f"长期任务运行中... {count}") + try: + await asyncio.wait_for(asyncio.sleep(1), timeout=0.5) + except asyncio.TimeoutError: + continue + print("长期任务优雅退出") + return count + + # 启动长期任务 + task = asyncio.create_task(long_running_task()) + + # 运行一段时间后关闭 + await asyncio.sleep(2.5) + shutdown_event.set() + + result = await task + print(f"长期任务执行了 {result} 次循环") + + return {"timeout_tests": len(results), "long_task_cycles": result} + + # 运行演示 + asyncio.run(demo_best_practices()) + + print("\n ✓ 最佳实践演示完成") + +# 运行最佳实践演示 +async_best_practices_demo() +``` + +## 7. 学习建议和总结 + +### 7.1 学习路径 + +```python +def async_learning_guide(): + """异步编程学习指南""" + print("=== 异步编程学习指南 ===") + + learning_path = { + "初级阶段": [ + "理解同步vs异步的概念", + "掌握async/await语法", + "学习asyncio.run()和基础事件循环", + "练习简单的协程函数", + "理解并发vs并行的区别" + ], + + "中级阶段": [ + "掌握asyncio.gather()和asyncio.create_task()", + "学习异步上下文管理器", + "理解异步迭代器和生成器", + "掌握信号量和锁等同步原语", + "学习异步HTTP客户端(aiohttp)" + ], + + "高级阶段": [ + "掌握异步服务器编程", + "学习WebSocket编程", + "理解事件循环的内部机制", + "掌握性能优化技巧", + "学习异步测试和调试" + ], + + "专家阶段": [ + "设计复杂的异步系统架构", + "掌握异步框架的源码", + "优化异步应用的性能", + "处理大规模并发场景", + "贡献开源异步项目" + ] + } + + print("\n推荐学习路径:") + for stage, topics in learning_path.items(): + print(f"\n{stage}:") + for i, topic in enumerate(topics, 1): + print(f" {i}. {topic}") + + # 实践项目建议 + practice_projects = [ + "异步文件下载器", + "网页爬虫系统", + "实时聊天服务器", + "API数据聚合器", + "异步任务队列", + "实时数据监控系统" + ] + + print("\n推荐实践项目:") + for i, project in enumerate(practice_projects, 1): + print(f" {i}. {project}") + +# 运行学习指南 +async_learning_guide() +``` + +### 7.2 常见陷阱和解决方案 + +```python +def async_common_pitfalls(): + """异步编程常见陷阱""" + print("=== 异步编程常见陷阱和解决方案 ===") + + pitfalls = { + "忘记使用await": { + "问题": "调用异步函数时忘记使用await关键字", + "错误示例": "result = async_function() # 返回协程对象,不是结果", + "正确示例": "result = await async_function() # 正确获取结果", + "解决方案": "始终在调用异步函数时使用await,或使用asyncio.create_task()" + }, + + "在同步函数中调用异步函数": { + "问题": "在普通函数中直接调用异步函数", + "错误示例": "def sync_func(): return async_func() # 错误", + "正确示例": "def sync_func(): return asyncio.run(async_func()) # 正确", + "解决方案": "使用asyncio.run()或确保调用者也是异步函数" + }, + + "阻塞事件循环": { + "问题": "在异步函数中使用阻塞操作", + "错误示例": "time.sleep(1) # 阻塞整个事件循环", + "正确示例": "await asyncio.sleep(1) # 非阻塞等待", + "解决方案": "使用异步版本的操作,或使用run_in_executor()" + }, + + "资源泄漏": { + "问题": "未正确关闭异步资源", + "错误示例": "session = aiohttp.ClientSession() # 未关闭", + "正确示例": "async with aiohttp.ClientSession() as session: # 自动关闭", + "解决方案": "使用异步上下文管理器或确保在finally块中关闭资源" + }, + + "过度并发": { + "问题": "创建过多并发任务导致资源耗尽", + "错误示例": "tasks = [fetch(url) for url in huge_url_list] # 可能创建数千个任务", + "正确示例": "使用Semaphore或分批处理限制并发数", + "解决方案": "使用信号量、连接池或任务队列控制并发" + } + } + + print("\n常见陷阱详解:") + for pitfall, details in pitfalls.items(): + print(f"\n{pitfall}:") + print(f" 问题: {details['问题']}") + print(f" 错误示例: {details['错误示例']}") + print(f" 正确示例: {details['正确示例']}") + print(f" 解决方案: {details['解决方案']}") + + # 调试技巧 + debugging_tips = [ + "使用asyncio.get_event_loop().set_debug(True)开启调试模式", + "使用logging记录异步操作的执行流程", + "使用asyncio.current_task()和asyncio.all_tasks()监控任务", + "使用pytest-asyncio进行异步代码测试", + "使用aiomonitor监控异步应用的运行状态" + ] + + print("\n调试技巧:") + for i, tip in enumerate(debugging_tips, 1): + print(f" {i}. {tip}") + +# 运行常见陷阱指南 +async_common_pitfalls() +``` + +### 7.3 本章总结 + +```python +def chapter_summary(): + """第22天学习总结""" + print("=== 第22天:异步IO - 学习总结 ===") + + summary_points = { + "核心概念": [ + "异步编程基础:协程、事件循环、任务", + "async/await语法的使用", + "并发vs并行的区别", + "异步IO的优势和适用场景" + ], + + "重要模块": [ + "asyncio:Python异步编程的核心模块", + "aiohttp:异步HTTP客户端和服务器", + "aiofiles:异步文件操作", + "contextlib.asynccontextmanager:异步上下文管理器" + ], + + "关键技能": [ + "编写和调用异步函数", + "使用asyncio.gather()进行并发执行", + "实现异步上下文管理器和迭代器", + "构建异步HTTP客户端和服务器", + "处理异步编程中的错误和异常" + ], + + "实际应用": [ + "异步网络编程:HTTP客户端、WebSocket", + "异步文件处理:大文件读写、批量处理", + "异步数据采集:网页爬虫、API聚合", + "异步服务器:Web服务、实时通信" + ], + + "最佳实践": [ + "合理控制并发数量", + "正确处理异步资源的生命周期", + "使用适当的错误处理和重试机制", + "优化异步应用的性能和内存使用" + ] + } + + print("\n学习要点总结:") + for category, points in summary_points.items(): + print(f"\n{category}:") + for point in points: + print(f" • {point}") + + # 下一步学习建议 + next_steps = [ + "深入学习特定的异步框架(如FastAPI、Tornado)", + "探索异步数据库操作(如asyncpg、motor)", + "学习异步消息队列和任务调度", + "研究微服务架构中的异步通信", + "掌握异步应用的部署和监控" + ] + + print("\n下一步学习建议:") + for i, step in enumerate(next_steps, 1): + print(f" {i}. {step}") + + print("\n🎉 恭喜完成第22天的学习!") + print("异步IO是现代Python开发的重要技能,") + print("掌握它将大大提升你处理并发任务的能力。") + print("继续练习和探索,你将成为异步编程的专家!") + +# 运行章节总结 +chapter_summary() +``` + +--- + +## 实践练习 + +1. **基础练习**: + - 编写一个异步函数,模拟网络请求的延迟 + - 使用asyncio.gather()并发执行多个异步任务 + - 实现一个简单的异步上下文管理器 + +2. **进阶练习**: + - 构建一个异步文件下载器 + - 实现一个简单的异步Web爬虫 + - 创建一个基于WebSocket的实时聊天系统 + +3. **项目练习**: + - 开发一个异步API数据聚合服务 + - 构建一个异步任务队列系统 + - 实现一个异步日志收集和分析工具 + +通过本章的学习,你已经掌握了Python异步IO编程的核心概念和实践技能。异步编程是现代高性能应用开发的关键技术,继续深入学习和实践将帮助你构建更加高效和可扩展的应用程序! + + diff --git a/docs/Python/23.md b/docs/Python/23.md new file mode 100644 index 000000000..50a94b4bf --- /dev/null +++ b/docs/Python/23.md @@ -0,0 +1,1361 @@ +# 第23天-测试和调试 + +## 学习目标 +- 掌握Python单元测试框架 +- 学习测试驱动开发(TDD) +- 理解代码覆盖率和测试策略 +- 掌握调试技巧和工具 +- 学习性能分析和优化 + +## 1. 单元测试基础 + +### 1.1 unittest模块 + +```python +import unittest +import sys +import os + +def unittest_basics_demo(): + """unittest基础演示""" + print("=== unittest基础演示 ===") + + # 1. 基础测试类 + print("\n1. 基础测试类:") + + basic_unittest_code = ''' +# unittest基础使用 +import unittest + +# 被测试的函数 +def add(a, b): + """加法函数""" + return a + b + +def divide(a, b): + """除法函数""" + if b == 0: + raise ValueError("除数不能为零") + return a / b + +def is_even(n): + """判断是否为偶数""" + return n % 2 == 0 + +class TestMathFunctions(unittest.TestCase): + """数学函数测试类""" + + def setUp(self): + """每个测试方法执行前调用""" + print("设置测试环境") + self.test_data = [1, 2, 3, 4, 5] + + def tearDown(self): + """每个测试方法执行后调用""" + print("清理测试环境") + self.test_data = None + + def test_add_positive_numbers(self): + """测试正数加法""" + result = add(2, 3) + self.assertEqual(result, 5) + self.assertIsInstance(result, int) + + def test_add_negative_numbers(self): + """测试负数加法""" + result = add(-2, -3) + self.assertEqual(result, -5) + + def test_add_zero(self): + """测试零的加法""" + self.assertEqual(add(5, 0), 5) + self.assertEqual(add(0, 5), 5) + self.assertEqual(add(0, 0), 0) + + def test_divide_normal(self): + """测试正常除法""" + result = divide(10, 2) + self.assertEqual(result, 5.0) + self.assertAlmostEqual(divide(1, 3), 0.333333, places=5) + + def test_divide_by_zero(self): + """测试除零异常""" + with self.assertRaises(ValueError) as context: + divide(10, 0) + + self.assertIn("除数不能为零", str(context.exception)) + + def test_is_even(self): + """测试偶数判断""" + # 测试偶数 + self.assertTrue(is_even(2)) + self.assertTrue(is_even(0)) + self.assertTrue(is_even(-4)) + + # 测试奇数 + self.assertFalse(is_even(1)) + self.assertFalse(is_even(3)) + self.assertFalse(is_even(-3)) + + def test_with_test_data(self): + """使用测试数据""" + self.assertIsNotNone(self.test_data) + self.assertEqual(len(self.test_data), 5) + self.assertIn(3, self.test_data) + +# 运行测试 +if __name__ == '__main__': + # 创建测试套件 + suite = unittest.TestLoader().loadTestsFromTestCase(TestMathFunctions) + + # 运行测试 + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + # 显示结果 + print(f"\n测试结果:") + print(f" 运行测试: {result.testsRun}") + print(f" 失败: {len(result.failures)}") + print(f" 错误: {len(result.errors)}") + print(f" 成功率: {(result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100:.1f}%") +''' + + print(" 基础unittest代码:") + print(basic_unittest_code) + + # 2. 断言方法详解 + print("\n2. 断言方法详解:") + + assertion_methods = { + "相等性断言": [ + "assertEqual(a, b) - 断言a等于b", + "assertNotEqual(a, b) - 断言a不等于b", + "assertAlmostEqual(a, b, places=7) - 断言a约等于b", + "assertNotAlmostEqual(a, b, places=7) - 断言a不约等于b" + ], + "真值断言": [ + "assertTrue(x) - 断言x为True", + "assertFalse(x) - 断言x为False", + "assertIsNone(x) - 断言x为None", + "assertIsNotNone(x) - 断言x不为None" + ], + "类型断言": [ + "assertIsInstance(a, b) - 断言a是b类型的实例", + "assertNotIsInstance(a, b) - 断言a不是b类型的实例", + "assertIs(a, b) - 断言a是b(同一对象)", + "assertIsNot(a, b) - 断言a不是b(不同对象)" + ], + "容器断言": [ + "assertIn(a, b) - 断言a在b中", + "assertNotIn(a, b) - 断言a不在b中", + "assertCountEqual(a, b) - 断言a和b包含相同元素(忽略顺序)", + "assertListEqual(a, b) - 断言列表a等于列表b" + ], + "异常断言": [ + "assertRaises(exc, fun, *args, **kwds) - 断言调用fun会抛出exc异常", + "assertRaisesRegex(exc, r, fun, *args, **kwds) - 断言异常消息匹配正则表达式", + "assertWarns(warn, fun, *args, **kwds) - 断言调用fun会产生warn警告" + ] + } + + for category, methods in assertion_methods.items(): + print(f"\n {category}:") + for method in methods: + print(f" • {method}") + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + # 被测试的简单函数 + def demo_add(a, b): + return a + b + + def demo_divide(a, b): + if b == 0: + raise ValueError("除数不能为零") + return a / b + + # 创建简单测试 + class DemoTestCase(unittest.TestCase): + def test_add(self): + self.assertEqual(demo_add(2, 3), 5) + + def test_divide(self): + self.assertEqual(demo_divide(10, 2), 5.0) + + def test_divide_by_zero(self): + with self.assertRaises(ValueError): + demo_divide(10, 0) + + # 运行演示测试 + suite = unittest.TestLoader().loadTestsFromTestCase(DemoTestCase) + runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w')) + result = runner.run(suite) + + print(f" 演示测试结果: {result.testsRun} 个测试,{len(result.failures)} 个失败,{len(result.errors)} 个错误") + + print("\n ✓ unittest基础演示完成") + +# 运行unittest基础演示 +unittest_basics_demo() +``` + +### 1.2 测试组织和运行 + +```python +import unittest +from unittest import mock +import tempfile +import shutil + +def test_organization_demo(): + """测试组织和运行演示""" + print("=== 测试组织和运行演示 ===") + + # 1. 测试套件组织 + print("\n1. 测试套件组织:") + + test_suite_code = ''' +# 测试套件组织 +import unittest + +# 示例被测试类 +class Calculator: + """计算器类""" + + def __init__(self): + self.history = [] + + def add(self, a, b): + result = a + b + self.history.append(f"{a} + {b} = {result}") + return result + + def subtract(self, a, b): + result = a - b + self.history.append(f"{a} - {b} = {result}") + return result + + def multiply(self, a, b): + result = a * b + self.history.append(f"{a} * {b} = {result}") + return result + + def divide(self, a, b): + if b == 0: + raise ValueError("除数不能为零") + result = a / b + self.history.append(f"{a} / {b} = {result}") + return result + + def clear_history(self): + self.history.clear() + + def get_history(self): + return self.history.copy() + +# 基础运算测试 +class TestBasicOperations(unittest.TestCase): + """基础运算测试""" + + def setUp(self): + self.calc = Calculator() + + def test_addition(self): + """测试加法""" + self.assertEqual(self.calc.add(2, 3), 5) + self.assertEqual(self.calc.add(-1, 1), 0) + self.assertEqual(self.calc.add(0, 0), 0) + + def test_subtraction(self): + """测试减法""" + self.assertEqual(self.calc.subtract(5, 3), 2) + self.assertEqual(self.calc.subtract(0, 5), -5) + self.assertEqual(self.calc.subtract(-2, -3), 1) + + def test_multiplication(self): + """测试乘法""" + self.assertEqual(self.calc.multiply(3, 4), 12) + self.assertEqual(self.calc.multiply(-2, 3), -6) + self.assertEqual(self.calc.multiply(0, 100), 0) + + def test_division(self): + """测试除法""" + self.assertEqual(self.calc.divide(10, 2), 5.0) + self.assertAlmostEqual(self.calc.divide(1, 3), 0.333333, places=5) + + def test_division_by_zero(self): + """测试除零异常""" + with self.assertRaises(ValueError): + self.calc.divide(10, 0) + +# 历史记录测试 +class TestHistory(unittest.TestCase): + """历史记录测试""" + + def setUp(self): + self.calc = Calculator() + + def test_history_recording(self): + """测试历史记录""" + self.calc.add(2, 3) + self.calc.multiply(4, 5) + + history = self.calc.get_history() + self.assertEqual(len(history), 2) + self.assertIn("2 + 3 = 5", history) + self.assertIn("4 * 5 = 20", history) + + def test_clear_history(self): + """测试清除历史""" + self.calc.add(1, 1) + self.calc.clear_history() + + history = self.calc.get_history() + self.assertEqual(len(history), 0) + + def test_history_independence(self): + """测试历史记录独立性""" + history1 = self.calc.get_history() + history1.append("fake entry") + + history2 = self.calc.get_history() + self.assertNotIn("fake entry", history2) + +# 边界条件测试 +class TestEdgeCases(unittest.TestCase): + """边界条件测试""" + + def setUp(self): + self.calc = Calculator() + + def test_large_numbers(self): + """测试大数运算""" + large_num = 10**10 + result = self.calc.add(large_num, large_num) + self.assertEqual(result, 2 * large_num) + + def test_float_precision(self): + """测试浮点精度""" + result = self.calc.add(0.1, 0.2) + self.assertAlmostEqual(result, 0.3, places=10) + + def test_negative_numbers(self): + """测试负数运算""" + self.assertEqual(self.calc.multiply(-3, -4), 12) + self.assertEqual(self.calc.divide(-10, -2), 5.0) + +# 创建测试套件 +def create_test_suite(): + """创建测试套件""" + suite = unittest.TestSuite() + + # 添加测试类 + suite.addTest(unittest.makeSuite(TestBasicOperations)) + suite.addTest(unittest.makeSuite(TestHistory)) + suite.addTest(unittest.makeSuite(TestEdgeCases)) + + return suite + +# 自定义测试套件 +def create_custom_suite(): + """创建自定义测试套件""" + suite = unittest.TestSuite() + + # 只添加特定测试 + suite.addTest(TestBasicOperations('test_addition')) + suite.addTest(TestBasicOperations('test_multiplication')) + suite.addTest(TestHistory('test_history_recording')) + + return suite + +# 运行测试套件 +def run_test_suite(): + """运行测试套件""" + print("=== 运行完整测试套件 ===") + + # 运行完整套件 + full_suite = create_test_suite() + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(full_suite) + + print(f"\n完整测试结果:") + print(f" 运行: {result.testsRun} 个测试") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + + # 运行自定义套件 + print("\n=== 运行自定义测试套件 ===") + custom_suite = create_custom_suite() + result2 = runner.run(custom_suite) + + print(f"\n自定义测试结果:") + print(f" 运行: {result2.testsRun} 个测试") + print(f" 失败: {len(result2.failures)} 个") + print(f" 错误: {len(result2.errors)} 个") + +# 运行测试套件演示 +# run_test_suite() +''' + + print(" 测试套件组织代码:") + print(test_suite_code) + + # 2. 测试发现和运行 + print("\n2. 测试发现和运行:") + + test_discovery_info = { + "命令行运行": [ + "python -m unittest test_module.TestClass.test_method", + "python -m unittest test_module.TestClass", + "python -m unittest test_module", + "python -m unittest discover -s tests -p 'test_*.py'" + ], + "程序化运行": [ + "unittest.main() - 运行当前模块的所有测试", + "unittest.TextTestRunner().run(suite) - 运行指定套件", + "unittest.TestLoader().loadTestsFromTestCase(TestClass) - 加载测试类", + "unittest.TestLoader().discover(start_dir, pattern) - 自动发现测试" + ], + "测试选项": [ + "-v, --verbose - 详细输出", + "-q, --quiet - 安静模式", + "-f, --failfast - 遇到失败立即停止", + "-b, --buffer - 缓冲stdout和stderr" + ] + } + + for category, items in test_discovery_info.items(): + print(f"\n {category}:") + for item in items: + print(f" • {item}") + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + # 简单的被测试类 + class DemoCalculator: + def add(self, a, b): + return a + b + + def divide(self, a, b): + if b == 0: + raise ValueError("除数不能为零") + return a / b + + # 测试类 + class TestDemoCalculator(unittest.TestCase): + def setUp(self): + self.calc = DemoCalculator() + + def test_add(self): + self.assertEqual(self.calc.add(2, 3), 5) + + def test_divide(self): + self.assertEqual(self.calc.divide(10, 2), 5.0) + + def test_divide_by_zero(self): + with self.assertRaises(ValueError): + self.calc.divide(10, 0) + + # 创建并运行测试套件 + suite = unittest.TestLoader().loadTestsFromTestCase(TestDemoCalculator) + runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w')) + result = runner.run(suite) + + print(f" 演示测试运行结果:") + print(f" 运行测试: {result.testsRun} 个") + print(f" 成功: {result.testsRun - len(result.failures) - len(result.errors)} 个") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + + print("\n ✓ 测试组织和运行演示完成") + +# 运行测试组织演示 +test_organization_demo() +``` + +## 2. 高级测试技术 + +### 2.1 Mock和Patch + +```python +from unittest import mock +import requests +import json +from datetime import datetime + +def mock_and_patch_demo(): + """Mock和Patch演示""" + print("=== Mock和Patch演示 ===") + + # 1. Mock基础 + print("\n1. Mock基础:") + + mock_basics_code = ''' +# Mock基础使用 +from unittest import mock +import requests +import json +from datetime import datetime + +# 被测试的类 +class WeatherService: + """天气服务类""" + + def __init__(self, api_key): + self.api_key = api_key + self.base_url = "https://api.weather.com" + + def get_weather(self, city): + """获取天气信息""" + url = f"{self.base_url}/weather" + params = { + "key": self.api_key, + "city": city, + "format": "json" + } + + try: + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + return { + "city": city, + "temperature": data.get("temperature"), + "humidity": data.get("humidity"), + "description": data.get("description"), + "timestamp": datetime.now().isoformat() + } + + except requests.RequestException as e: + raise Exception(f"获取天气信息失败: {e}") + + def get_forecast(self, city, days=5): + """获取天气预报""" + url = f"{self.base_url}/forecast" + params = { + "key": self.api_key, + "city": city, + "days": days + } + + response = requests.get(url, params=params) + if response.status_code == 200: + return response.json() + else: + return None + +class DatabaseManager: + """数据库管理器""" + + def __init__(self, connection_string): + self.connection_string = connection_string + self.connected = False + + def connect(self): + """连接数据库""" + # 模拟数据库连接 + print(f"连接到数据库: {self.connection_string}") + self.connected = True + return True + + def execute_query(self, query, params=None): + """执行查询""" + if not self.connected: + raise Exception("数据库未连接") + + # 模拟查询执行 + print(f"执行查询: {query}") + if params: + print(f"参数: {params}") + + # 返回模拟结果 + return [{"id": 1, "name": "测试数据"}] + + def close(self): + """关闭连接""" + self.connected = False + print("数据库连接已关闭") + +# Mock测试示例 +class TestWeatherService(unittest.TestCase): + """天气服务测试""" + + def setUp(self): + self.weather_service = WeatherService("test_api_key") + + @mock.patch('requests.get') + def test_get_weather_success(self, mock_get): + """测试成功获取天气""" + # 设置mock响应 + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "temperature": 25, + "humidity": 60, + "description": "晴天" + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + # 执行测试 + result = self.weather_service.get_weather("北京") + + # 验证结果 + self.assertEqual(result["city"], "北京") + self.assertEqual(result["temperature"], 25) + self.assertEqual(result["humidity"], 60) + self.assertEqual(result["description"], "晴天") + self.assertIn("timestamp", result) + + # 验证API调用 + mock_get.assert_called_once() + call_args = mock_get.call_args + self.assertIn("北京", str(call_args)) + + @mock.patch('requests.get') + def test_get_weather_api_error(self, mock_get): + """测试API错误""" + # 设置mock抛出异常 + mock_get.side_effect = requests.RequestException("网络错误") + + # 验证异常 + with self.assertRaises(Exception) as context: + self.weather_service.get_weather("上海") + + self.assertIn("获取天气信息失败", str(context.exception)) + + @mock.patch('requests.get') + def test_get_forecast(self, mock_get): + """测试获取预报""" + # 设置mock响应 + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "forecast": [ + {"date": "2024-01-01", "temp": 20}, + {"date": "2024-01-02", "temp": 22} + ] + } + mock_get.return_value = mock_response + + # 执行测试 + result = self.weather_service.get_forecast("广州", 2) + + # 验证结果 + self.assertIsNotNone(result) + self.assertIn("forecast", result) + self.assertEqual(len(result["forecast"]), 2) + +class TestDatabaseManager(unittest.TestCase): + """数据库管理器测试""" + + def setUp(self): + self.db = DatabaseManager("test://localhost/testdb") + + def test_connect(self): + """测试连接""" + result = self.db.connect() + self.assertTrue(result) + self.assertTrue(self.db.connected) + + @mock.patch.object(DatabaseManager, 'connect') + def test_execute_query_without_connection(self, mock_connect): + """测试未连接时执行查询""" + # 确保未连接 + self.db.connected = False + + # 验证异常 + with self.assertRaises(Exception) as context: + self.db.execute_query("SELECT * FROM users") + + self.assertIn("数据库未连接", str(context.exception)) + + def test_execute_query_with_connection(self): + """测试连接后执行查询""" + # 先连接 + self.db.connect() + + # 执行查询 + result = self.db.execute_query("SELECT * FROM users") + + # 验证结果 + self.assertIsInstance(result, list) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["id"], 1) + +# 运行Mock测试 +def run_mock_tests(): + """运行Mock测试""" + print("=== 运行Mock测试 ===") + + # 创建测试套件 + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestWeatherService)) + suite.addTest(unittest.makeSuite(TestDatabaseManager)) + + # 运行测试 + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print(f"\nMock测试结果:") + print(f" 运行: {result.testsRun} 个测试") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + +# 运行Mock测试演示 +# run_mock_tests() +''' + + print(" Mock基础代码:") + print(mock_basics_code) + + # 2. Patch装饰器和上下文管理器 + print("\n2. Patch装饰器和上下文管理器:") + + patch_examples_code = ''' +# Patch使用示例 +from unittest import mock +import os +import time +from datetime import datetime + +# 被测试的函数 +def get_current_time(): + """获取当前时间""" + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + +def read_config_file(filename): + """读取配置文件""" + if not os.path.exists(filename): + raise FileNotFoundError(f"配置文件不存在: {filename}") + + with open(filename, 'r') as f: + content = f.read() + + return content.strip() + +def process_data_with_delay(data, delay=1): + """处理数据(带延迟)""" + time.sleep(delay) + return [item.upper() for item in data] + +class FileProcessor: + """文件处理器""" + + def __init__(self, base_path): + self.base_path = base_path + + def get_file_size(self, filename): + """获取文件大小""" + full_path = os.path.join(self.base_path, filename) + return os.path.getsize(full_path) + + def list_files(self): + """列出文件""" + return os.listdir(self.base_path) + + def create_backup(self, filename): + """创建备份""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_name = f"{filename}.backup_{timestamp}" + + # 模拟备份过程 + return backup_name + +# Patch测试示例 +class TestPatchExamples(unittest.TestCase): + """Patch示例测试""" + + # 1. 使用装饰器patch + @mock.patch('datetime.datetime') + def test_get_current_time_with_decorator(self, mock_datetime): + """使用装饰器测试时间函数""" + # 设置mock返回值 + mock_datetime.now.return_value.strftime.return_value = "2024-01-01 12:00:00" + + # 执行测试 + result = get_current_time() + + # 验证结果 + self.assertEqual(result, "2024-01-01 12:00:00") + mock_datetime.now.assert_called_once() + + # 2. 使用上下文管理器patch + def test_read_config_file_with_context_manager(self): + """使用上下文管理器测试文件读取""" + # 模拟文件存在和内容 + with mock.patch('os.path.exists', return_value=True): + with mock.patch('builtins.open', mock.mock_open(read_data="test config")): + result = read_config_file("config.txt") + self.assertEqual(result, "test config") + + # 3. 测试文件不存在的情况 + @mock.patch('os.path.exists', return_value=False) + def test_read_config_file_not_exists(self, mock_exists): + """测试文件不存在""" + with self.assertRaises(FileNotFoundError): + read_config_file("nonexistent.txt") + + mock_exists.assert_called_once_with("nonexistent.txt") + + # 4. 使用side_effect + @mock.patch('time.sleep') + def test_process_data_with_delay(self, mock_sleep): + """测试带延迟的数据处理""" + # 跳过实际的sleep + mock_sleep.return_value = None + + # 执行测试 + data = ["hello", "world"] + result = process_data_with_delay(data, delay=2) + + # 验证结果 + self.assertEqual(result, ["HELLO", "WORLD"]) + mock_sleep.assert_called_once_with(2) + + # 5. 测试类方法 + def test_file_processor(self): + """测试文件处理器""" + processor = FileProcessor("/test/path") + + # 使用patch.object + with mock.patch.object(os.path, 'getsize', return_value=1024): + size = processor.get_file_size("test.txt") + self.assertEqual(size, 1024) + + # 使用patch + with mock.patch('os.listdir', return_value=["file1.txt", "file2.txt"]): + files = processor.list_files() + self.assertEqual(len(files), 2) + self.assertIn("file1.txt", files) + + # 6. 多个patch + @mock.patch('datetime.datetime') + @mock.patch('os.path.exists') + def test_multiple_patches(self, mock_exists, mock_datetime): + """测试多个patch""" + # 设置mock + mock_exists.return_value = True + mock_datetime.now.return_value.strftime.return_value = "20240101_120000" + + # 执行测试 + processor = FileProcessor("/test") + backup_name = processor.create_backup("test.txt") + + # 验证结果 + self.assertEqual(backup_name, "test.txt.backup_20240101_120000") + + # 7. 使用spec参数 + def test_with_spec(self): + """使用spec参数的mock""" + # 创建带spec的mock + mock_file = mock.Mock(spec=open) + mock_file.read.return_value = "mocked content" + + # 验证spec限制 + self.assertTrue(hasattr(mock_file, 'read')) + self.assertTrue(hasattr(mock_file, 'write')) + + # 尝试访问不存在的属性会报错 + with self.assertRaises(AttributeError): + _ = mock_file.nonexistent_method + +# 运行Patch测试 +def run_patch_tests(): + """运行Patch测试""" + print("=== 运行Patch测试 ===") + + suite = unittest.TestLoader().loadTestsFromTestCase(TestPatchExamples) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print(f"\nPatch测试结果:") + print(f" 运行: {result.testsRun} 个测试") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + +# 运行Patch测试演示 +# run_patch_tests() +''' + + print(" Patch示例代码:") + print(patch_examples_code) + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + # 简单的被测试类 + class DemoService: + def get_data(self): + # 模拟外部API调用 + import requests + response = requests.get("https://api.example.com/data") + return response.json() + + def get_time(self): + return datetime.now().strftime("%Y-%m-%d") + + # 测试类 + class TestDemoService(unittest.TestCase): + def setUp(self): + self.service = DemoService() + + @mock.patch('requests.get') + def test_get_data(self, mock_get): + # 设置mock响应 + mock_response = mock.Mock() + mock_response.json.return_value = {"result": "success"} + mock_get.return_value = mock_response + + # 执行测试 + result = self.service.get_data() + + # 验证结果 + self.assertEqual(result["result"], "success") + mock_get.assert_called_once_with("https://api.example.com/data") + + @mock.patch('datetime.datetime') + def test_get_time(self, mock_datetime): + # 设置mock时间 + mock_datetime.now.return_value.strftime.return_value = "2024-01-01" + + # 执行测试 + result = self.service.get_time() + + # 验证结果 + self.assertEqual(result, "2024-01-01") + + # 运行演示测试 + suite = unittest.TestLoader().loadTestsFromTestCase(TestDemoService) + runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w')) + result = runner.run(suite) + + print(f" Mock演示测试结果:") + print(f" 运行测试: {result.testsRun} 个") + print(f" 成功: {result.testsRun - len(result.failures) - len(result.errors)} 个") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + + print("\n ✓ Mock和Patch演示完成") + +# 运行Mock和Patch演示 +mock_and_patch_demo() +``` + +### 2.2 参数化测试 + +```python +import unittest +from unittest import TestCase +import itertools + +def parameterized_tests_demo(): + """参数化测试演示""" + print("=== 参数化测试演示 ===") + + # 1. 手动参数化测试 + print("\n1. 手动参数化测试:") + + manual_parameterized_code = ''' +# 手动参数化测试 +import unittest +from unittest import TestCase + +# 被测试的函数 +def is_prime(n): + """判断是否为质数""" + if n < 2: + return False + if n == 2: + return True + if n % 2 == 0: + return False + + for i in range(3, int(n**0.5) + 1, 2): + if n % i == 0: + return False + return True + +def factorial(n): + """计算阶乘""" + if n < 0: + raise ValueError("负数没有阶乘") + if n == 0 or n == 1: + return 1 + + result = 1 + for i in range(2, n + 1): + result *= i + return result + +def fibonacci(n): + """计算斐波那契数列第n项""" + if n < 0: + raise ValueError("n必须为非负整数") + if n <= 1: + return n + + a, b = 0, 1 + for _ in range(2, n + 1): + a, b = b, a + b + return b + +# 手动参数化测试类 +class TestMathFunctions(TestCase): + """数学函数测试""" + + def test_is_prime_manual_cases(self): + """手动测试质数判断""" + # 测试用例:(输入, 期望输出) + test_cases = [ + (2, True), + (3, True), + (4, False), + (5, True), + (6, False), + (7, True), + (8, False), + (9, False), + (10, False), + (11, True), + (17, True), + (25, False), + (29, True) + ] + + for number, expected in test_cases: + with self.subTest(number=number): + result = is_prime(number) + self.assertEqual(result, expected, + f"is_prime({number}) 应该返回 {expected},但返回了 {result}") + + def test_factorial_manual_cases(self): + """手动测试阶乘计算""" + test_cases = [ + (0, 1), + (1, 1), + (2, 2), + (3, 6), + (4, 24), + (5, 120), + (6, 720) + ] + + for n, expected in test_cases: + with self.subTest(n=n): + result = factorial(n) + self.assertEqual(result, expected, + f"factorial({n}) 应该返回 {expected},但返回了 {result}") + + def test_factorial_negative_input(self): + """测试阶乘负数输入""" + negative_numbers = [-1, -5, -10] + + for n in negative_numbers: + with self.subTest(n=n): + with self.assertRaises(ValueError): + factorial(n) + + def test_fibonacci_manual_cases(self): + """手动测试斐波那契数列""" + test_cases = [ + (0, 0), + (1, 1), + (2, 1), + (3, 2), + (4, 3), + (5, 5), + (6, 8), + (7, 13), + (8, 21), + (9, 34), + (10, 55) + ] + + for n, expected in test_cases: + with self.subTest(n=n): + result = fibonacci(n) + self.assertEqual(result, expected, + f"fibonacci({n}) 应该返回 {expected},但返回了 {result}") + +# 使用生成器的参数化测试 +class TestWithGenerators(TestCase): + """使用生成器的参数化测试""" + + def _generate_prime_test_cases(self): + """生成质数测试用例""" + # 已知的质数和合数 + primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] + composites = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25, 26, 27, 28, 30] + + for prime in primes: + yield prime, True + + for composite in composites: + yield composite, False + + def test_is_prime_with_generator(self): + """使用生成器测试质数判断""" + for number, expected in self._generate_prime_test_cases(): + with self.subTest(number=number): + result = is_prime(number) + self.assertEqual(result, expected) + + def _generate_edge_cases(self): + """生成边界测试用例""" + # 边界情况 + yield 0, False # 0不是质数 + yield 1, False # 1不是质数 + yield -1, False # 负数不是质数 + yield -5, False # 负数不是质数 + + def test_is_prime_edge_cases(self): + """测试边界情况""" + for number, expected in self._generate_edge_cases(): + with self.subTest(number=number): + result = is_prime(number) + self.assertEqual(result, expected) + +# 动态生成测试方法 +def create_parameterized_test_class(): + """动态创建参数化测试类""" + + class DynamicTestClass(TestCase): + pass + + # 测试数据 + test_data = [ + ("test_small_primes", [(2, True), (3, True), (5, True), (7, True)]), + ("test_small_composites", [(4, False), (6, False), (8, False), (9, False)]), + ("test_larger_primes", [(11, True), (13, True), (17, True), (19, True)]) + ] + + # 动态添加测试方法 + for test_name, cases in test_data: + def make_test_method(test_cases): + def test_method(self): + for number, expected in test_cases: + with self.subTest(number=number): + result = is_prime(number) + self.assertEqual(result, expected) + return test_method + + # 添加到类中 + setattr(DynamicTestClass, test_name, make_test_method(cases)) + + return DynamicTestClass + +# 运行手动参数化测试 +def run_manual_parameterized_tests(): + """运行手动参数化测试""" + print("=== 运行手动参数化测试 ===") + + # 运行基础测试 + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestMathFunctions)) + suite.addTest(unittest.makeSuite(TestWithGenerators)) + + # 添加动态生成的测试类 + DynamicTestClass = create_parameterized_test_class() + suite.addTest(unittest.makeSuite(DynamicTestClass)) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print(f"\n手动参数化测试结果:") + print(f" 运行: {result.testsRun} 个测试") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + +# 运行手动参数化测试演示 +# run_manual_parameterized_tests() +''' + + print(" 手动参数化测试代码:") + print(manual_parameterized_code) + + # 2. 使用第三方库的参数化测试 + print("\n2. 使用第三方库的参数化测试:") + + third_party_parameterized_code = ''' +# 使用parameterized库的参数化测试 +# 需要安装: pip install parameterized + +try: + from parameterized import parameterized + PARAMETERIZED_AVAILABLE = True +except ImportError: + PARAMETERIZED_AVAILABLE = False + print("parameterized库未安装,跳过相关演示") + +if PARAMETERIZED_AVAILABLE: + class TestWithParameterized(TestCase): + """使用parameterized库的测试""" + + @parameterized.expand([ + (2, True), + (3, True), + (4, False), + (5, True), + (6, False), + (7, True), + (8, False), + (9, False), + (10, False), + (11, True) + ]) + def test_is_prime_parameterized(self, number, expected): + """参数化测试质数判断""" + result = is_prime(number) + self.assertEqual(result, expected) + + @parameterized.expand([ + (0, 1), + (1, 1), + (2, 2), + (3, 6), + (4, 24), + (5, 120) + ]) + def test_factorial_parameterized(self, n, expected): + """参数化测试阶乘""" + result = factorial(n) + self.assertEqual(result, expected) + + @parameterized.expand([ + (-1,), + (-5,), + (-10,) + ]) + def test_factorial_negative_parameterized(self, n): + """参数化测试阶乘负数输入""" + with self.assertRaises(ValueError): + factorial(n) + + @parameterized.expand([ + ("empty_string", "", 0), + ("single_char", "a", 1), + ("hello", "hello", 5), + ("spaces", "hello world", 11) + ]) + def test_string_length(self, name, string, expected_length): + """参数化测试字符串长度""" + self.assertEqual(len(string), expected_length) + +# 使用pytest风格的参数化(如果可用) +try: + import pytest + PYTEST_AVAILABLE = True +except ImportError: + PYTEST_AVAILABLE = False + +if PYTEST_AVAILABLE: + # pytest参数化示例(在pytest环境中运行) + class TestPytestStyle: + """pytest风格的参数化测试""" + + @pytest.mark.parametrize("number,expected", [ + (2, True), + (3, True), + (4, False), + (5, True), + (6, False) + ]) + def test_is_prime_pytest(self, number, expected): + """pytest参数化测试""" + assert is_prime(number) == expected + + @pytest.mark.parametrize("n,expected", [ + (0, 1), + (1, 1), + (2, 2), + (3, 6), + (4, 24) + ]) + def test_factorial_pytest(self, n, expected): + """pytest参数化阶乘测试""" + assert factorial(n) == expected + +# 运行第三方库参数化测试 +def run_third_party_parameterized_tests(): + """运行第三方库参数化测试""" + if not PARAMETERIZED_AVAILABLE: + print("parameterized库未安装,无法运行相关测试") + return + + print("=== 运行第三方库参数化测试 ===") + + suite = unittest.TestLoader().loadTestsFromTestCase(TestWithParameterized) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + print(f"\n第三方库参数化测试结果:") + print(f" 运行: {result.testsRun} 个测试") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + +# 运行第三方库参数化测试演示 +# run_third_party_parameterized_tests() +''' + + print(" 第三方库参数化测试代码:") + print(third_party_parameterized_code) + + # 3. 实际应用演示 + print("\n3. 实际应用演示:") + + # 被测试的函数 + def demo_is_prime(n): + """判断是否为质数""" + if n < 2: + return False + if n == 2: + return True + if n % 2 == 0: + return False + + for i in range(3, int(n**0.5) + 1, 2): + if n % i == 0: + return False + return True + + # 参数化测试类 + class TestDemoParameterized(unittest.TestCase): + def test_prime_numbers(self): + """测试质数""" + prime_cases = [(2, True), (3, True), (5, True), (7, True), (11, True)] + + for number, expected in prime_cases: + with self.subTest(number=number): + result = demo_is_prime(number) + self.assertEqual(result, expected) + + def test_composite_numbers(self): + """测试合数""" + composite_cases = [(4, False), (6, False), (8, False), (9, False), (10, False)] + + for number, expected in composite_cases: + with self.subTest(number=number): + result = demo_is_prime(number) + self.assertEqual(result, expected) + + # 运行演示测试 + suite = unittest.TestLoader().loadTestsFromTestCase(TestDemoParameterized) + runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w')) + result = runner.run(suite) + + print(f" 参数化演示测试结果:") + print(f" 运行测试: {result.testsRun} 个") + print(f" 子测试: 10 个 (5个质数 + 5个合数)") + print(f" 成功: {result.testsRun - len(result.failures) - len(result.errors)} 个") + print(f" 失败: {len(result.failures)} 个") + print(f" 错误: {len(result.errors)} 个") + + print("\n ✓ 参数化测试演示完成") + +# 运行参数化测试演示 +parameterized_tests_demo() +``` \ No newline at end of file diff --git a/docs/Python/3.md b/docs/Python/3.md new file mode 100644 index 000000000..8351e5179 --- /dev/null +++ b/docs/Python/3.md @@ -0,0 +1,238 @@ +--- +title: 第3天-第一个程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 第3天-第一个程序 + +今天我们来学习如何编写和运行你的第一个Python程序!首先需要理解什么是命令行模式和Python交互模式。 + +## 两种重要模式的区别 + +> **模式对比**: +> +> 🖥️ **命令行模式** +> - 操作系统提供的命令行界面 +> - 可以运行各种系统命令 +> - 提示符:`PS C:\>`(Windows)或`$`(macOS/Linux) +> +> 🐍 **Python交互模式** +> - Python解释器提供的交互界面 +> - 只能运行Python代码 +> - 提示符:`>>>` + +## 命令行模式详解 + +在Windows开始菜单选择"Terminal",就进入到PowerShell命令行模式,它的提示符类似`PS C:\>`: + +> **命令行模式的作用**: +> - 🔧 运行系统命令(如`dir`、`cd`等) +> - 🚀 启动Python解释器 +> - 📁 管理文件和目录 +> - 🏃 运行Python脚本文件 + +> **常用命令行操作**: +> ```bash +> # Windows PowerShell +> PS C:\> dir # 查看当前目录文件 +> PS C:\> cd Desktop # 切换到桌面目录 +> PS C:\> python hello.py # 运行Python文件 +> PS C:\> python # 启动Python交互模式 +> +> # macOS/Linux +> $ ls # 查看当前目录文件 +> $ cd Desktop # 切换到桌面目录 +> $ python3 hello.py # 运行Python文件 +> $ python3 # 启动Python交互模式 +> ``` + +## Python交互模式详解 + +在命令行模式下敲命令`python`,就看到类似如下的一堆文本输出,然后就进入到Python交互模式,它的提示符是`>>>`: + +![img_3.png](./img_3.png) + +> **交互模式的特点**: +> - ⚡ **即时执行**:输入代码立即看到结果 +> - 🧪 **测试代码**:适合测试小段代码 +> - 📚 **学习工具**:初学者的最佳伙伴 +> - 🔍 **调试助手**:快速验证想法 + +> **交互模式示例**: +> ```python +> >>> print("Hello, World!") +> Hello, World! +> >>> 2 + 3 +> 5 +> >>> name = "Python" +> >>> print(f"我正在学习{name}") +> 我正在学习Python +> >>> exit() # 退出交互模式 +> ``` + +## 编写你的第一个Python程序 + +### 方法一:在交互模式中编写 + +最简单的方式是在Python交互模式中直接输入代码: + +```python +>>> print("Hello, Python!") +Hello, Python! +>>> print("这是我的第一个Python程序") +这是我的第一个Python程序 +``` + +> **交互模式的优缺点**: +> - ✅ 立即看到结果,适合学习和测试 +> - ❌ 代码无法保存,关闭后就丢失了 + +### 方法二:使用文本编辑器编写.py文件 + +更常用的方式是用文本编辑器写Python程序,然后保存为后缀为`.py`的文件: + +**创建hello.py文件**: +```python +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# 这是我的第一个Python程序 +print("Hello, World!") +print("欢迎来到Python的世界!") + +# 简单的计算 +result = 10 + 20 +print(f"10 + 20 = {result}") + +# 获取用户输入 +name = input("请输入你的名字:") +print(f"你好,{name}!") +``` + +> **代码解释**: +> - `#!/usr/bin/env python3`:Shebang行,告诉系统用Python3执行 +> - `# -*- coding: utf-8 -*-`:指定文件编码为UTF-8 +> - `#`开头的是注释,不会被执行 +> - `print()`函数用于输出内容 +> - `input()`函数用于获取用户输入 +> - `f"{变量}"`是格式化字符串的写法 + +**运行Python文件**: +```bash +# Windows +PS C:\> python hello.py + +# macOS/Linux +$ python3 hello.py +``` + +> **运行效果**: +> ``` +> Hello, World! +> 欢迎来到Python的世界! +> 10 + 20 = 30 +> 请输入你的名字:张三 +> 你好,张三! +> ``` + +### 方法三:直接运行.py文件(仅限macOS/Linux) + +有同学问,能不能像.exe文件那样直接运行.py文件呢?在Windows上是不行的,但是,在Mac和Linux上是可以的,方法是在.py文件的第一行加上一个特殊的注释: + +```python +#!/usr/bin/env python3 + +print('hello, world') +``` + +然后,通过命令给hello.py以执行权限: + +```bash +$ chmod +x hello.py +``` + +就可以直接运行hello.py了,比如在Mac下运行: + +```bash +$ ./hello.py +hello, world +``` + +> **Shebang详解**: +> - `#!/usr/bin/env python3`被称为"Shebang"或"Hashbang" +> - 它告诉系统使用哪个解释器来执行这个脚本 +> - 只在Unix-like系统(macOS、Linux)中有效 +> - Windows系统通过文件扩展名`.py`来识别Python文件 + +## 推荐的文本编辑器 + +> **初学者推荐**: +> +> 🆓 **免费编辑器**: +> - **VS Code**:微软出品,功能强大,插件丰富 +> - **PyCharm Community**:专业Python IDE的免费版 +> - **Sublime Text**:轻量级,启动快速 +> - **Notepad++**:Windows平台的轻量级编辑器 +> +> 💰 **付费编辑器**: +> - **PyCharm Professional**:最专业的Python IDE +> - **Sublime Text**:需要购买许可证 +> +> 🌐 **在线编辑器**: +> - **Replit**:在线Python环境,无需安装 +> - **CodePen**:适合快速测试代码 + +## 交互模式 vs 脚本文件 + +Python的交互模式和直接运行.py文件有什么区别呢? + +> **交互模式**: +> - 直接输入`python`进入交互模式 +> - 相当于启动了Python解释器,等待你一行一行地输入源代码 +> - 每输入一行就执行一行 +> - 适合:学习、测试、调试 +> +> **脚本文件模式**: +> - 直接运行.py文件相当于启动了Python解释器 +> - 然后一次性把.py文件的源代码给执行了 +> - 你没有机会以交互的方式输入源代码 +> - 适合:编写完整程序、自动化脚本 + +> **使用建议**: +> - 🧪 **学习阶段**:多使用交互模式,立即看到结果 +> - 📝 **编写程序**:使用文本编辑器创建.py文件 +> - 🔄 **开发流程**:在编辑器中写代码,在命令行中运行测试 + +## 小练习 + +现在试着完成这个小练习: + +1. 创建一个名为`my_first_program.py`的文件 +2. 在文件中写入以下内容: + ```python + # 我的第一个Python程序 + print("=" * 30) + print("欢迎使用Python计算器") + print("=" * 30) + + # 获取两个数字 + num1 = float(input("请输入第一个数字:")) + num2 = float(input("请输入第二个数字:")) + + # 进行计算 + print(f"\n计算结果:") + print(f"{num1} + {num2} = {num1 + num2}") + print(f"{num1} - {num2} = {num1 - num2}") + print(f"{num1} * {num2} = {num1 * num2}") + if num2 != 0: + print(f"{num1} / {num2} = {num1 / num2}") + else: + print("除数不能为0") + ``` +3. 保存文件并运行:`python my_first_program.py` + +> **恭喜!** 如果程序成功运行,你已经完成了第一个有实际功能的Python程序! + + + diff --git a/docs/Python/4.md b/docs/Python/4.md new file mode 100644 index 000000000..03d2ba130 --- /dev/null +++ b/docs/Python/4.md @@ -0,0 +1,1060 @@ +--- +title: 第4天-Python基础 +author: 哪吒 +date: '2023-06-15' +--- + +# 第4天-Python基础 + +Python的语法比较简单,采用缩进方式 + +```java +# print absolute value of an integer: +a = 100 +if a >= 0: + print(a) +else: + print(-a) + +``` + +### Python 数据类型和变量 + +#### 1. **数据类型(Data Types)** + +在 Python 中,**数据类型**指的是我们存储和操作的数据的类别。不同的数据类型决定了我们可以对数据执行什么样的操作。 + +Python 支持多种常用的数据类型,主要分为以下几类: + +#### 1.1 **数字类型(Numeric Types)** + +* **整数(int)**:表示没有小数点的数字。 +* **浮点数(float)**:表示带有小数点的数字。 + +**示例:** + +```python +# 整数类型 +x = 10 # 这是一个整数 +y = -5 # 负数也是整数 + +# 浮点数类型 +a = 3.14 # 这是一个浮点数 +b = -0.001 # 负数浮点数 +``` + +#### 1.2 **字符串类型(String)** + +* 字符串是由字符组成的序列,通常用引号括起来(单引号 `'` 或双引号 `"` 都可以)。 + +**示例:** + +```python +# 字符串类型 +name = "Alice" # 使用双引号定义字符串 +greeting = 'Hello, World!' # 使用单引号定义字符串 +``` + +#### 1.3 **布尔类型(Boolean)** + +* 布尔值有两个取值:`True` 和 `False`,用于表示真或假,常用于条件判断。 + +**示例:** + +```python +is_student = True # True 表示"是学生" +has_completed_homework = False # False 表示"没有完成作业" +``` + +#### 1.4 **列表类型(List)** + +* 列表是一个有序的元素集合,可以包含不同类型的元素,列表用方括号 `[]` 表示。 + +**示例:** + +```python +# 列表类型 +fruits = ["apple", "banana", "cherry"] # 包含字符串的列表 +numbers = [1, 2, 3, 4, 5] # 包含整数的列表 +mixed_list = [1, "apple", 3.14, True] # 混合数据类型的列表 +``` + +#### 1.5 **元组类型(Tuple)** + +* 元组和列表类似,不同之处在于元组是不可更改的,也就是说,元组中的元素一旦定义就不能被修改。元组用圆括号 `()` 表示。 + +**示例:** + +```python +# 元组类型 +coordinates = (10, 20) # 二维坐标的元组 +person = ("Alice", 25, "Engineer") # 包含多个信息的元组 +``` + +#### 1.6 **字典类型(Dictionary)** + +* 字典是一种无序的数据结构,由**键**(key)和值(value)对组成,键和值通过冒号 `:` 分隔,整个字典用大括号 `{}` 包裹。 + +**示例:** + +```python +# 字典类型 +student = {"name": "Alice", "age": 20, "major": "Physics"} # 存储学生信息 +``` + +#### 1.7 **集合类型(Set)** + +* 集合是一种无序的、不重复的元素集合,用大括号 `{}` 表示。 + +**示例:** + +```python +# 集合类型 +colors = {"red", "green", "blue"} # 不允许重复元素 +``` + +--- + +### 2. **变量(Variable)** + +变量是一个用来存储数据的**容器**,可以将数据赋值给变量并使用它。变量可以保存不同类型的数据。Python 中不需要显式声明变量的类型,Python 会根据赋值自动推断类型。 + +#### 2.1 **变量定义** + +变量的定义非常简单,只需要通过 `=` 赋值操作符来定义和赋值。 + +**示例:** + +```python +# 定义一个整数变量 +age = 25 # age 是变量,25 是它的值 + +# 定义一个字符串变量 +name = "Alice" # name 是变量,"Alice" 是它的值 + +# 定义一个列表变量 +fruits = ["apple", "banana", "cherry"] # fruits 是变量,它的值是一个列表 +``` + +#### 2.2 **变量的类型** + +变量的类型是由它存储的数据类型决定的。例如,`age = 25` 中,`age` 是一个整数类型的变量。 + +#### 2.3 **变量的作用域** + +变量的作用域是指变量的**可访问范围**。在 Python 中,变量有局部作用域和全局作用域: + +* **局部变量**:在函数内部定义,只能在函数内部访问。 +* **全局变量**:在函数外部定义,可以在整个程序中访问。 + +**示例:** + +```python +# 全局变量 +global_var = "I am global" # 在函数外定义,整个程序可以访问 + +def test(): + # 局部变量 + local_var = "I am local" # 在函数内部定义,只能在这个函数内访问 + print(local_var) # 输出局部变量 + +test() +print(global_var) # 输出全局变量 +``` + +--- + +### 3. **总结** + +* **数字类型**(int 和 float)用于表示数值。 +* **字符串类型**(str)用于表示文字或字符序列。 +* **布尔类型**(bool)表示真或假,常用于条件判断。 +* **列表、元组、字典和集合**是容器类型,分别用于存储多个元素。 +* **变量**是存储数据的“容器”,可以动态赋值并在程序中使用。 + +通过这些基本的**数据类型和变量**,你可以在 Python 中轻松地存储、操作和传递数据。 + +### Python 字符串和编码 + +#### 1. **字符串(String)** + +在 Python 中,**字符串(String)** 是由字符组成的**序列**。这些字符可以是字母、数字、符号等,字符串用单引号 `''` 或双引号 `""` 来定义。 + +* 字符串是**不可变的**,也就是说一旦创建,就不能改变其中的字符。 + +#### 1.1 **创建字符串** + +我们可以通过简单的引号(单引号或双引号)来创建字符串。 + +**示例:** + +```python +# 使用单引号创建字符串 +name = 'Alice' + +# 使用双引号创建字符串 +greeting = "Hello, World!" +``` + +#### 1.2 **字符串操作** + +* **拼接字符串**:可以使用 `+` 号来拼接两个或多个字符串。 +* **重复字符串**:可以使用 `*` 号来重复字符串。 +* **索引和切片**:可以通过索引访问字符串中的字符,索引从 `0` 开始,负数表示从字符串的末尾开始。 + +**示例:** + +```python +# 拼接字符串 +full_name = 'Alice' + ' ' + 'Smith' +print(full_name) # 输出 "Alice Smith" + +# 重复字符串 +repeat_string = 'Hello ' * 3 +print(repeat_string) # 输出 "Hello Hello Hello " + +# 索引 +first_char = 'Hello'[0] # 获取第一个字符 +print(first_char) # 输出 "H" + +# 切片 +substring = 'Hello'[:3] # 获取前3个字符 +print(substring) # 输出 "Hel" +``` + +#### 1.3 **字符串方法** + +Python 中有很多内置的字符串方法,常用的有: + +* `lower()`:将字符串转换为小写。 +* `upper()`:将字符串转换为大写。 +* `replace(old, new)`:替换字符串中的某些字符。 +* `strip()`:去除字符串两端的空格。 +* `split()`:将字符串分割成列表。 + +**示例:** + +```python +text = " Hello, World! " + +# 去除前后的空格 +clean_text = text.strip() +print(clean_text) # 输出 "Hello, World!" + +# 字符串转为小写 +lowercase_text = "Hello".lower() +print(lowercase_text) # 输出 "hello" + +# 替换字符 +new_text = "Hello World".replace("World", "Alice") +print(new_text) # 输出 "Hello Alice" +``` + +--- + +#### 2. **编码(Encoding)** + +**编码(Encoding)** 是将字符转换成计算机可以处理的二进制数据(字节)的过程。不同的编码方式会用不同的规则将字符转换为字节。 + +##### 2.1 **字符编码示例:** + +* **ASCII**:美国标准信息交换代码(ASCII)只使用 128 个字符(例如字母、数字、符号)。每个字符占用 1 字节。 +* **Unicode**:Unicode 是一种全球通用的字符编码方式,支持所有的语言字符(包括中文、日文等)。它可以使用不同的字节数来表示字符(如 UTF-8、UTF-16 等)。 + +##### 2.2 **Python 中的编码与解码** + +Python 默认使用 **Unicode** 字符编码,这意味着可以直接处理大部分语言的字符(比如中文、日文等)。当你从文件或网络获取数据时,可能需要将字节数据转换为字符串,或者将字符串转换为字节数据。 + +* **编码**:将字符串转换为字节。 +* **解码**:将字节转换为字符串。 + +**示例:** + +```python +# 字符串编码为字节 +text = "Hello, 世界!" +encoded_text = text.encode('utf-8') # 使用 UTF-8 编码 +print(encoded_text) # 输出 b'Hello, \xe4\xb8\x96\xe7\x95\x8c!' + +# 字节解码为字符串 +decoded_text = encoded_text.decode('utf-8') +print(decoded_text) # 输出 "Hello, 世界!" +``` + +#### 2.3 **常见编码格式** + +* **UTF-8**:一种变长的 Unicode 编码方式,最常用,兼容 ASCII。 +* **GB2312、GBK**:中文字符编码,GBK 包含了更多的汉字。 +* **ISO-8859-1**:西欧语言常用的编码方式。 + +--- + +### 3. **编码相关的常见问题** + +#### 3.1 **UnicodeError 错误** + +如果你尝试使用不正确的编码格式来解码字节数据,可能会引发 `UnicodeError`。 + +**示例:** + +```python +# 假设我们有一些字节数据,尝试用错误的编码解码 +byte_data = b'\xe4\xb8\x96\xe7\x95\x8c' # 这是 "世界" 的 UTF-8 编码 + +# 错误解码(如果用其他编码解码,会报错) +try: + decoded_text = byte_data.decode('ascii') +except UnicodeDecodeError as e: + print(f"解码错误: {e}") +``` + +#### 3.2 **文件读取与写入的编码问题** + +在读取和写入文件时,通常需要指定编码格式,特别是处理包含非 ASCII 字符的文件时。 + +**示例:** + +```python +# 写入文件时指定编码 +with open('sample.txt', 'w', encoding='utf-8') as file: + file.write("Hello, 世界!") + +# 读取文件时指定编码 +with open('sample.txt', 'r', encoding='utf-8') as file: + content = file.read() + print(content) # 输出 "Hello, 世界!" +``` + +--- + +### 4. **总结** + +* **字符串(String)** 是由字符组成的序列,可以进行拼接、切片、替换等操作。 +* **编码(Encoding)** 是将字符转换为计算机可以理解的字节数据的过程。Python 默认使用 Unicode 编码(UTF-8)。 +* **常见编码格式**:UTF-8、ASCII、GBK 等。 +* **常见操作**:将字符串编码为字节或将字节解码为字符串。 + +通过了解字符串和编码,你能更好地处理文本数据,尤其是涉及多语言和文件操作时。 + +### Python 中的 `list` 和 `tuple` - 通俗易懂的解释 + +在 Python 中,`list` 和 `tuple` 都是用来存储多个元素的数据结构,它们有很多相似之处,但也有一些关键的区别。 + +--- + +### 1. **List(列表)** + +* **定义**:`list`(列表)是一个**可变**的、有序的容器,用来存储多个元素。 +* **特点**: + + * **可变**:可以修改、添加和删除元素。 + * **有序**:列表中的元素有固定的顺序,可以通过索引访问。 + * **可以包含不同类型的数据**:可以存储整数、字符串、甚至其他列表。 + +#### 示例:创建和操作 `list` + +```python +# 创建一个列表 +fruits = ["apple", "banana", "cherry"] + +# 访问列表中的元素 +print(fruits[0]) # 输出 "apple"(索引从0开始) + +# 修改列表中的元素 +fruits[1] = "orange" # 将 "banana" 改为 "orange" +print(fruits) # 输出 ["apple", "orange", "cherry"] + +# 向列表中添加元素 +fruits.append("grape") # 添加 "grape" 到列表的末尾 +print(fruits) # 输出 ["apple", "orange", "cherry", "grape"] + +# 删除列表中的元素 +fruits.remove("cherry") # 删除 "cherry" +print(fruits) # 输出 ["apple", "orange", "grape"] +``` + +#### 常用的 `list` 方法: + +* `append()`:向列表添加一个元素。 +* `remove()`:删除列表中的某个元素。 +* `pop()`:删除并返回指定索引的元素。 +* `sort()`:对列表进行排序。 + +--- + +### 2. **Tuple(元组)** + +* **定义**:`tuple`(元组)也是一个**有序的**容器,用来存储多个元素,但与 `list` 不同的是,**元组是不可变的**。 +* **特点**: + + * **不可变**:创建后不能修改,不能增加或删除元素。 + * **有序**:和列表一样,元素有固定顺序,可以通过索引访问。 + * **可以包含不同类型的数据**:元组也可以存储不同类型的元素。 + +#### 示例:创建和操作 `tuple` + +```python +# 创建一个元组 +coordinates = (10, 20, 30) + +# 访问元组中的元素 +print(coordinates[0]) # 输出 10(索引从0开始) + +# 元组是不可变的,不能修改 +# coordinates[1] = 50 # 这会引发错误,因为元组不能修改 + +# 可以创建单元素元组(注意要加逗号) +single_element_tuple = (5,) +print(single_element_tuple) # 输出 (5,) +``` + +#### 元组的常用方法: + +* `count()`:返回指定元素在元组中出现的次数。 +* `index()`:返回指定元素在元组中的索引。 + +--- + +### 3. **`list` 和 `tuple` 的区别** + +| 特性 | List(列表) | Tuple(元组) | +| ---- | ---------------------- | ------------------------- | +| 可变性 | 可变(可以修改、添加和删除元素) | 不可变(创建后不能修改、添加或删除元素) | +| 语法 | 使用方括号 `[]` | 使用圆括号 `()` | +| 存储方式 | 存储在内存中,占用更多内存(因为它是可变的) | 存储在内存中,占用更少的内存(因为它是不可变的) | +| 用途 | 适合需要修改元素或动态变化的数据结构 | 适合存储不需要修改的数据,通常用于保护数据不被改变 | + +--- + +### 4. **何时使用 `list`,何时使用 `tuple`** + +* **使用 `list`**: + + * 当你需要一个可以随时添加、修改、删除元素的容器时,使用 `list`。 + * 例如:你需要存储一个购物清单,可以随时添加或删除商品。 + +* **使用 `tuple`**: + + * 当你需要一个不可变的容器,确保数据不被修改时,使用 `tuple`。 + * 例如:你需要存储一个坐标点(经度、纬度),这些值在计算过程中不会改变。 + +--- + +### 5. **总结** + +* **List(列表)**:可以修改、添加、删除元素;适用于需要频繁修改数据的场景。 +* **Tuple(元组)**:一旦创建就无法修改;适用于需要保护数据不被更改的场景。 + +这两种数据类型各有其优缺点,根据需求选择合适的类型可以让你的程序更加高效且安全。 + +### Python 条件判断 + +--- + +在程序中,我们常常需要根据不同的情况执行不同的操作,比如: + +> 如果今天下雨,就带伞;否则就不带。 + +这就是“**条件判断**”,也叫“**分支语句**”。 + +--- + +## 一、基本语法结构 + +Python 使用 `if`、`elif` 和 `else` 来进行条件判断: + +```python +if 条件1: + 代码块1 +elif 条件2: + 代码块2 +else: + 代码块3 +``` + +* `if`:如果条件为 True,执行它下面的代码。 +* `elif`(else if):如果上面的条件不满足,检查这个条件。 +* `else`:其他情况都不满足时执行。 + +--- + +## 二、简单示例 + +### 示例1:判断天气 + +```python +weather = "rain" + +if weather == "sunny": + print("出去玩") +elif weather == "rain": + print("带伞") +else: + print("待在家里") +``` + +输出: + +``` +带伞 +``` + +--- + +### 示例2:判断数字正负 + +```python +num = -10 + +if num > 0: + print("正数") +elif num == 0: + print("零") +else: + print("负数") +``` + +输出: + +``` +负数 +``` + +--- + +## 三、条件表达式支持的运算符 + +| 运算符 | 含义 | 示例 | +| ----- | ------- | ------------------ | +| `==` | 等于 | `x == 10` | +| `!=` | 不等于 | `x != 5` | +| `>` | 大于 | `x > 0` | +| `<` | 小于 | `x < 100` | +| `>=` | 大于等于 | `x >= 18` | +| `<=` | 小于等于 | `x <= 60` | +| `and` | 与(同时满足) | `x > 0 and x < 10` | +| `or` | 或(满足一个) | `x < 0 or x > 100` | +| `not` | 非(取反) | `not x == 0` | + +--- + +### 示例3:判断年龄是否在合法范围内(18\~60) + +```python +age = 25 + +if age >= 18 and age <= 60: + print("合法年龄") +else: + print("不合法") +``` + +--- + +## 四、嵌套判断(if 里面再套 if) + +```python +score = 85 + +if score >= 60: + if score >= 90: + print("优秀") + else: + print("及格") +else: + print("不及格") +``` + +--- + +## 五、简洁写法(三元运算) + +```python +x = 10 +result = "正数" if x > 0 else "负数或零" +print(result) +``` + + + +在 Python 中,**模式匹配(Pattern Matching)** 是一种类似于“结构解构 + 条件判断”的语法,它让我们可以更简洁、清晰地处理复杂数据结构,**尤其适合处理字典、列表、元组等嵌套结构**。 + +Python 从 **3.10 版本开始**,引入了官方的结构化**模式匹配**语法 —— `match ... case`,非常类似于其他语言中的 `switch`。 + +--- + +## 一、语法结构:`match ... case` + +```python +match 变量: + case 模式1: + 执行代码1 + case 模式2: + 执行代码2 + case _: + 默认处理(相当于 else) +``` + +--- + +## 二、通俗示例 + +### 示例1:匹配字符串值(类似 switch) + +```python +command = "start" + +match command: + case "start": + print("启动程序") + case "stop": + print("停止程序") + case "pause": + print("暂停程序") + case _: + print("未知命令") +``` + +输出: + +``` +启动程序 +``` + +--- + +### 示例2:匹配元组(结构解包) + +```python +point = (0, 5) + +match point: + case (0, 0): + print("原点") + case (x, 0): + print(f"在 X 轴上, x = {x}") + case (0, y): + print(f"在 Y 轴上, y = {y}") + case (x, y): + print(f"在平面上,x={x}, y={y}") +``` + +输出: + +``` +在 Y 轴上, y = 5 +``` + +--- + +### 示例3:匹配字典结构 + +```python +user = {"type": "admin", "name": "Alice"} + +match user: + case {"type": "admin", "name": name}: + print(f"管理员:{name}") + case {"type": "guest", "name": name}: + print(f"访客:{name}") + case _: + print("未知用户") +``` + +输出: + +``` +管理员:Alice +``` + +--- + +## 三、常见匹配模式 + +| 模式类型 | 示例 | 描述 | +| ------- | ---------------------------------- | -------------- | +| 常量匹配 | `case "ok"` | 匹配特定值 | +| 变量绑定 | `case x:` | 将值绑定到变量 | +| 元组结构匹配 | `case (x, y)` | 拆解元组 | +| 列表匹配 | `case [x, y, z]` | 拆解固定长度的列表 | +| 字典匹配 | `case {"type": "admin", "id": id}` | 匹配并提取字典中的字段 | +| 通配符 `_` | `case _:` | 匹配所有剩余情况(默认分支) | + +--- + +## 四、注意事项 + +* 需要 **Python 3.10 及以上版本**。 +* `match-case` 区分大小写。 +* `case` 分支必须是**静态模式**,不能使用逻辑表达式(如 `case x > 5:` 会报错)。 +* 若想使用表达式判断,需在 `case` 后加守卫条件(`if`) + +### 示例4:使用守卫条件 `if` + +```python +x = 8 + +match x: + case x if x > 10: + print("大于10") + case x if x > 5: + print("大于5") + case _: + print("5或以下") +``` + +输出: + +``` +大于5 +``` + +--- + +## 总结 + +| 特性 | 优点 | +| -------- | --------------- | +| 解构数据结构 | 支持元组、列表、字典、类 | +| 多分支结构清晰 | 替代复杂的 `if-elif` | +| 默认分支 `_` | 简洁处理未匹配情况 | +| 支持守卫语句 | 配合 `if` 做条件限制 | + +--- + + +在 Python 中,**循环(loop)** 是一种重复执行代码块的结构,直到满足某个条件为止。常用于**遍历列表、字典、范围**,或**重复某些操作**。 + +--- + +## 一句话理解循环 + +就是:**“做这件事一直重复,直到不需要为止”** + +--- + +## 一、两种常用的循环方式 + +| 循环类型 | 适用场景 | 关键字 | +| ---------- | -------------- | ---------------- | +| `for` 循环 | 知道要循环几次或遍历东西时 | `for ... in ...` | +| `while` 循环 | 不知道要循环几次,只知道条件 | `while 条件:` | + +--- + +## 二、`for` 循环(最常用) + +用于**遍历序列类型**(如 list、tuple、str、dict、range) + +### 示例1:遍历列表 + +```python +fruits = ["apple", "banana", "cherry"] + +for fruit in fruits: + print(fruit) +``` + +输出: + +``` +apple +banana +cherry +``` + +--- + +### 示例2:遍历字符串 + +```python +for letter in "hello": + print(letter) +``` + +输出: + +``` +h +e +l +l +o +``` + +--- + +### 示例3:使用 `range()` 遍历数字 + +```python +for i in range(5): # 0 ~ 4(不包括5) + print(i) +``` + +--- + +## 🔁 三、`while` 循环(基于条件) + +只要条件为 `True`,就不断执行 + +### 示例4:用 `while` 数到 5 + +```python +i = 1 +while i <= 5: + print(i) + i += 1 +``` + +输出: + +``` +1 +2 +3 +4 +5 +``` + +--- + +## 🛑 四、控制循环的语句 + +| 语句 | 作用 | +| ---------- | -------------- | +| `break` | 立即退出循环 | +| `continue` | 跳过当前这次循环,继续下一次 | +| `else`(可选) | 循环正常结束后执行(非中断) | + +--- + +### 示例5:`break` 用法 + +```python +for i in range(1, 10): + if i == 5: + break + print(i) +``` + +输出: + +``` +1 +2 +3 +4 +``` + +--- + +### 示例6:`continue` 用法 + +```python +for i in range(1, 6): + if i == 3: + continue # 跳过3 + print(i) +``` + +输出: + +``` +1 +2 +4 +5 +``` + +--- + +## 🎁 五、循环的综合例子:猜数字游戏 + +```python +secret = 7 +guess = 0 + +while guess != secret: + guess = int(input("猜一个 1~10 的数字: ")) + if guess < secret: + print("猜小了") + elif guess > secret: + print("猜大了") + else: + print("猜对了!") +``` + +--- + +## ✅ 总结 + +| 循环类型 | 用途 | 示例 | +| ---------- | -------------- | ----------------- | +| `for` | 遍历列表、字符串、数字序列等 | `for x in ...` | +| `while` | 基于条件重复执行 | `while 条件:` | +| `break` | 提前终止循环 | `if 条件: break` | +| `continue` | 跳过当前轮,继续下一轮 | `if 条件: continue` | + +--- + + +--- + +## 1. 九九乘法表(嵌套 for 循环) + +```python +# 打印九九乘法表 +for i in range(1, 10): # 外层控制行数(1 到 9) + for j in range(1, i + 1): # 内层控制每行的列数 + print(f"{j}×{i}={i*j}", end="\t") + print() # 换行 +``` + +输出(部分): + +``` +1×1=1 +1×2=2 2×2=4 +1×3=3 2×3=6 3×3=9 +... +``` + +--- + +## 二、字典(`dict`)—— 键值对存储 + +### 概念: + +`dict` 就像一个“电话号码本”或“小型数据库”,**通过“键”快速找到对应的“值”**。 + +### 语法: + +```python +字典名 = {键1: 值1, 键2: 值2, ...} +``` + +--- + +### 示例1:基本操作 + +```python +# 创建字典 +student = { + "name": "Alice", + "age": 20, + "major": "Computer Science" +} + +# 访问值 +print(student["name"]) # 输出:Alice + +# 添加新键值对 +student["grade"] = "A" + +# 修改已有值 +student["age"] = 21 + +# 删除键值对 +del student["major"] + +# 遍历字典 +for key, value in student.items(): + print(key, "->", value) +``` + +--- + +### 常用方法: + +| 方法 | 作用 | +| -------------------- | ------------ | +| `dict.get(key, 默认值)` | 获取值,不存在时返回默认 | +| `dict.keys()` | 返回所有键 | +| `dict.values()` | 返回所有值 | +| `dict.items()` | 返回键值对元组 | +| `in` | 判断键是否存在 | + +--- + +### 示例2:`get()` 和判断键 + +```python +score = {"Tom": 90, "Bob": 85} + +print(score.get("Tom")) # 输出:90 +print(score.get("Alice", 0)) # Alice 不在字典里,输出:0 + +if "Bob" in score: + print("Bob 在成绩单里") +``` + +--- + +## 二、集合(`set`)—— 不重复、无序的元素集合 + +### 概念: + +`set` 是一个**无重复**、**无序**的数据容器,适合去重、集合运算(并集、交集、差集等)。 + +### 语法: + +```python +集合名 = {元素1, 元素2, ...} +``` + +--- + +### 示例3:基本操作 + +```python +# 创建集合 +colors = {"red", "green", "blue"} + +# 添加元素 +colors.add("yellow") + +# 删除元素 +colors.remove("red") + +# 遍历集合 +for color in colors: + print(color) +``` + +--- + +### 常用集合运算: + +```python +a = {1, 2, 3} +b = {3, 4, 5} + +print(a | b) # 并集:{1, 2, 3, 4, 5} +print(a & b) # 交集:{3} +print(a - b) # 差集:{1, 2} +print(a ^ b) # 对称差集(不重复的):{1, 2, 4, 5} +``` + +--- + +### 示例4:列表去重 + +```python +nums = [1, 2, 2, 3, 4, 4, 5] +unique_nums = list(set(nums)) +print(unique_nums) # 输出:[1, 2, 3, 4, 5](顺序可能变) +``` + +--- + +## 总结对比 + +| 特性 | `dict` | `set` | +| ----- | ------------------- | ----------- | +| 结构 | 键值对(key: value) | 单个元素(value) | +| 是否唯一 | 键必须唯一 | 所有元素唯一 | +| 是否有顺序 | Python 3.7+ 中保持插入顺序 | 无序 | +| 常见用途 | 快速查找、存储映射关系 | 去重、集合运算 | + +--- + diff --git a/docs/Python/5.md b/docs/Python/5.md new file mode 100644 index 000000000..335bad2c7 --- /dev/null +++ b/docs/Python/5.md @@ -0,0 +1,936 @@ +--- +title: 第5天-函数 +author: 哪吒 +date: '2023-06-15' +--- + +# 第5天-Python函数 + +今天我们来学习Python中最重要的概念之一:**函数(Function)**。函数是编程的核心,掌握了函数,你就掌握了编程的精髓! + +## 函数是什么? + +想象一下生活中的场景: +- 🍳 **做饭**:你给厨师食材(输入),厨师做出美食(输出) +- 🧮 **计算器**:你输入数字和运算符,计算器给出结果 +- 🏭 **工厂**:输入原材料,输出成品 + +Python的函数就是这样的"加工厂"! + +> **函数定义**:函数是一段**可重复使用的代码块**,它接收输入(参数),执行特定任务,并可能返回结果。 + +--- + +## 一、函数的基本语法 + +### 1.1 定义函数的语法 + +```python +def 函数名(参数1, 参数2, ...): + """函数说明文档(可选)""" + # 函数体:要执行的代码 + return 返回值 # 可选 +``` + +**语法要点**: +- `def`:定义函数的关键字 +- `函数名`:遵循变量命名规则 +- `参数`:函数的输入,可以有0个或多个 +- `:`:不要忘记冒号 +- **缩进**:函数体必须缩进 +- `return`:返回结果(可选) + +### 1.2 最简单的函数 + +```python +# 定义一个最简单的函数 +def say_hello(): + print("你好,欢迎学习Python!") + +# 调用函数 +say_hello() # 输出:你好,欢迎学习Python! +``` + +> **小白提示**:定义函数只是"制作模板",调用函数才是"使用模板"! + +### 1.3 带参数的函数 + +```python +# 带一个参数的函数 +def greet(name): + print(f"你好,{name}!") + +# 调用函数时传入参数 +greet("小明") # 输出:你好,小明! +greet("Alice") # 输出:你好,Alice! + +# 带多个参数的函数 +def introduce(name, age, city): + print(f"我叫{name},今年{age}岁,来自{city}") + +introduce("张三", 25, "北京") +# 输出:我叫张三,今年25岁,来自北京 +``` + +### 1.4 带返回值的函数 + +```python +# 计算两个数的和 +def add(a, b): + result = a + b + return result # 返回计算结果 + +# 使用返回值 +sum_result = add(3, 5) +print(f"3 + 5 = {sum_result}") # 输出:3 + 5 = 8 + +# 可以直接在表达式中使用 +print(f"10 + 20 = {add(10, 20)}") # 输出:10 + 20 = 30 +``` + +> **重要概念**: +> - 有`return`的函数会返回值,可以赋值给变量 +> - 没有`return`的函数返回`None` +> - `return`后面的代码不会执行 + +--- + +## 二、函数参数详解 + +### 2.1 位置参数(必需参数) + +位置参数是最基本的参数类型,调用时必须按顺序传入: + +```python +def calculate_rectangle_area(length, width): + """计算矩形面积""" + area = length * width + return area + +# 按位置传参:第一个是length,第二个是width +area1 = calculate_rectangle_area(5, 3) # length=5, width=3 +print(f"矩形面积:{area1}") # 输出:矩形面积:15 + +# 位置不能搞错! +area2 = calculate_rectangle_area(3, 5) # length=3, width=5 +print(f"矩形面积:{area2}") # 输出:矩形面积:15(结果相同,但概念不同) +``` + +### 2.2 关键字参数 + +可以通过参数名指定值,不用考虑顺序: + +```python +def create_user_profile(name, age, city, job): + return f"姓名:{name},年龄:{age},城市:{city},职业:{job}" + +# 使用关键字参数,顺序可以任意 +profile1 = create_user_profile(name="张三", age=28, city="上海", job="程序员") +profile2 = create_user_profile(job="设计师", city="深圳", name="李四", age=26) + +print(profile1) +print(profile2) + +# 位置参数和关键字参数可以混用,但位置参数必须在前面 +profile3 = create_user_profile("王五", 30, city="广州", job="老师") +print(profile3) +``` + +### 2.3 默认参数 + +为参数设置默认值,调用时可以不传该参数: + +```python +def greet_user(name, greeting="你好", punctuation="!"): + return f"{greeting},{name}{punctuation}" + +# 使用默认参数 +print(greet_user("小明")) # 输出:你好,小明! + +# 覆盖部分默认参数 +print(greet_user("小红", "早上好")) # 输出:早上好,小红! + +# 覆盖所有默认参数 +print(greet_user("小李", "晚安", "。")) # 输出:晚安,小李。 + +# 使用关键字参数跳过某些默认参数 +print(greet_user("小王", punctuation="???")) # 输出:你好,小王??? +``` + +> **默认参数注意事项**: +> - 默认参数必须放在位置参数后面 +> - 不要使用可变对象(如列表、字典)作为默认参数 + +### 2.4 可变参数(*args) + +当不知道会传入多少个参数时,使用`*args`: + +```python +def calculate_sum(*numbers): + """计算任意数量数字的和""" + total = 0 + for num in numbers: + total += num + return total + +# 传入不同数量的参数 +print(calculate_sum(1, 2, 3)) # 输出:6 +print(calculate_sum(10, 20, 30, 40)) # 输出:100 +print(calculate_sum(5)) # 输出:5 +print(calculate_sum()) # 输出:0 + +# 更简洁的写法 +def calculate_sum_v2(*numbers): + return sum(numbers) + +print(calculate_sum_v2(1, 2, 3, 4, 5)) # 输出:15 +``` + +### 2.5 关键字可变参数(**kwargs) + +当需要接收任意数量的关键字参数时,使用`**kwargs`: + +```python +def create_student_info(name, **other_info): + """创建学生信息""" + info = f"学生姓名:{name}\n" + for key, value in other_info.items(): + info += f"{key}:{value}\n" + return info + +# 传入不同的关键字参数 +student1 = create_student_info( + "张三", + age=20, + major="计算机科学", + grade="大二", + gpa=3.8 +) +print(student1) + +student2 = create_student_info( + "李四", + age=19, + hobby="篮球", + hometown="北京" +) +print(student2) +``` + +### 2.6 参数组合使用 + +```python +def complex_function(required_arg, default_arg="默认值", *args, **kwargs): + """演示各种参数类型的组合使用""" + print(f"必需参数:{required_arg}") + print(f"默认参数:{default_arg}") + print(f"可变参数:{args}") + print(f"关键字参数:{kwargs}") + print("-" * 30) + +# 各种调用方式 +complex_function("必须的") +complex_function("必须的", "修改默认值") +complex_function("必须的", "修改默认值", 1, 2, 3) +complex_function("必须的", "修改默认值", 1, 2, 3, name="张三", age=25) +``` + +--- + +## 三、函数的返回值 + +### 3.1 单个返回值 + +```python +def get_circle_area(radius): + """计算圆的面积""" + import math + area = math.pi * radius ** 2 + return area + +area = get_circle_area(5) +print(f"半径为5的圆的面积:{area:.2f}") # 输出:半径为5的圆的面积:78.54 +``` + +### 3.2 多个返回值 + +```python +def get_rectangle_info(length, width): + """计算矩形的面积和周长""" + area = length * width + perimeter = 2 * (length + width) + return area, perimeter # 返回元组 + +# 接收多个返回值 +area, perimeter = get_rectangle_info(5, 3) +print(f"面积:{area},周长:{perimeter}") + +# 也可以作为元组接收 +result = get_rectangle_info(4, 6) +print(f"结果元组:{result}") # 输出:结果元组:(24, 20) +print(f"面积:{result[0]},周长:{result[1]}") +``` + +### 3.3 条件返回 + +```python +def check_grade(score): + """根据分数返回等级""" + if score >= 90: + return "优秀" + elif score >= 80: + return "良好" + elif score >= 70: + return "中等" + elif score >= 60: + return "及格" + else: + return "不及格" + +# 测试不同分数 +scores = [95, 85, 75, 65, 55] +for score in scores: + grade = check_grade(score) + print(f"分数{score}:{grade}") +``` + +### 3.4 提前返回 + +```python +def divide_numbers(a, b): + """安全的除法运算""" + if b == 0: + print("错误:除数不能为0") + return None # 提前返回,避免错误 + + result = a / b + return result + +# 测试 +print(divide_numbers(10, 2)) # 输出:5.0 +print(divide_numbers(10, 0)) # 输出:错误:除数不能为0,然后是None +``` + +--- + +## 四、函数的作用域 + +### 4.1 局部变量 vs 全局变量 + +```python +# 全局变量 +global_var = "我是全局变量" +counter = 0 + +def demo_scope(): + # 局部变量 + local_var = "我是局部变量" + print(f"函数内部访问全局变量:{global_var}") + print(f"函数内部的局部变量:{local_var}") + +demo_scope() +print(f"函数外部访问全局变量:{global_var}") +# print(local_var) # 错误!无法在函数外访问局部变量 +``` + +### 4.2 修改全局变量 + +```python +counter = 0 # 全局变量 + +def increment_counter(): + global counter # 声明要修改全局变量 + counter += 1 + print(f"计数器增加到:{counter}") + +def show_counter(): + print(f"当前计数器值:{counter}") + +show_counter() # 输出:当前计数器值:0 +increment_counter() # 输出:计数器增加到:1 +increment_counter() # 输出:计数器增加到:2 +show_counter() # 输出:当前计数器值:2 +``` + +### 4.3 变量查找顺序(LEGB规则) + +```python +x = "全局变量" + +def outer_function(): + x = "外层函数变量" + + def inner_function(): + x = "内层函数变量" + print(f"内层函数中的x:{x}") + + inner_function() + print(f"外层函数中的x:{x}") + +outer_function() +print(f"全局作用域中的x:{x}") + +# 输出: +# 内层函数中的x:内层函数变量 +# 外层函数中的x:外层函数变量 +# 全局作用域中的x:全局变量 +``` + +> **LEGB规则**:Python按照 Local → Enclosing → Global → Built-in 的顺序查找变量 + +--- + +## 五、Lambda表达式(匿名函数) + +### 5.1 什么是Lambda表达式? + +Lambda表达式是一种创建**简单函数**的快捷方式,适合一行就能完成的简单操作。 + +```python +# 普通函数 +def square(x): + return x ** 2 + +# Lambda表达式(匿名函数) +square_lambda = lambda x: x ** 2 + +# 两者效果相同 +print(square(5)) # 输出:25 +print(square_lambda(5)) # 输出:25 +``` + +### 5.2 Lambda语法 + +```python +# 基本语法:lambda 参数: 表达式 + +# 单个参数 +double = lambda x: x * 2 +print(double(4)) # 输出:8 + +# 多个参数 +add = lambda a, b: a + b +print(add(3, 5)) # 输出:8 + +# 无参数 +get_pi = lambda: 3.14159 +print(get_pi()) # 输出:3.14159 + +# 条件表达式 +max_value = lambda a, b: a if a > b else b +print(max_value(10, 20)) # 输出:20 +``` + +### 5.3 Lambda的实际应用 + +```python +# 1. 与内置函数配合使用 +numbers = [1, 2, 3, 4, 5] + +# 使用map():对每个元素应用函数 +squares = list(map(lambda x: x**2, numbers)) +print(f"平方:{squares}") # 输出:平方:[1, 4, 9, 16, 25] + +# 使用filter():过滤元素 +even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) +print(f"偶数:{even_numbers}") # 输出:偶数:[2, 4] + +# 使用sorted():自定义排序 +students = [('Alice', 85), ('Bob', 90), ('Charlie', 78)] +sorted_by_score = sorted(students, key=lambda student: student[1]) +print(f"按分数排序:{sorted_by_score}") +# 输出:按分数排序:[('Charlie', 78), ('Alice', 85), ('Bob', 90)] +``` + +### 5.4 Lambda vs 普通函数 + +| 特性 | Lambda表达式 | 普通函数 | +|------|-------------|----------| +| 语法 | 简洁,一行 | 完整,多行 | +| 命名 | 匿名 | 有名称 | +| 复杂度 | 只能是表达式 | 可以包含语句 | +| 调试 | 难以调试 | 容易调试 | +| 用途 | 简单操作 | 复杂逻辑 | + +--- + +## 六、高级函数概念 + +### 6.1 递归函数 + +递归是函数调用自己的编程技巧,适合解决可以分解为相似子问题的问题。 + +```python +# 经典例子:计算阶乘 +def factorial(n): + """计算n的阶乘:n! = n × (n-1) × ... × 1""" + # 基础情况(递归终止条件) + if n == 0 or n == 1: + return 1 + # 递归情况 + else: + return n * factorial(n - 1) + +# 测试 +for i in range(6): + print(f"{i}! = {factorial(i)}") + +# 输出: +# 0! = 1 +# 1! = 1 +# 2! = 2 +# 3! = 6 +# 4! = 24 +# 5! = 120 +``` + +```python +# 斐波那契数列 +def fibonacci(n): + """计算斐波那契数列的第n项""" + if n <= 1: + return n + else: + return fibonacci(n-1) + fibonacci(n-2) + +# 打印前10项斐波那契数列 +print("斐波那契数列前10项:") +for i in range(10): + print(f"F({i}) = {fibonacci(i)}") +``` + +> **递归注意事项**: +> - 必须有终止条件(基础情况) +> - 递归调用必须向终止条件靠近 +> - 递归深度不能太大(Python默认限制1000层) + +### 6.2 嵌套函数 + +在函数内部定义另一个函数: + +```python +def outer_function(x): + """外层函数""" + + def inner_function(y): + """内层函数""" + return y * 2 + + # 在外层函数中调用内层函数 + result = inner_function(x) + 10 + return result + +print(outer_function(5)) # 输出:20 (5*2+10) + +# inner_function(5) # 错误!内层函数在外部不可访问 +``` + +### 6.3 闭包(Closure) + +内层函数引用外层函数的变量: + +```python +def create_multiplier(factor): + """创建一个乘法器函数""" + + def multiplier(number): + return number * factor # 引用外层函数的参数 + + return multiplier # 返回内层函数 + +# 创建不同的乘法器 +double = create_multiplier(2) +triple = create_multiplier(3) + +print(double(5)) # 输出:10 (5 * 2) +print(triple(5)) # 输出:15 (5 * 3) + +# 每个乘法器都"记住"了自己的factor值 +print(double(10)) # 输出:20 +print(triple(10)) # 输出:30 +``` + +### 6.4 装饰器基础 + +装饰器是一种特殊的函数,用来修改或增强其他函数的功能: + +```python +def timer_decorator(func): + """计时装饰器""" + import time + + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) # 调用原函数 + end_time = time.time() + print(f"函数 {func.__name__} 执行时间:{end_time - start_time:.4f}秒") + return result + + return wrapper + +# 使用装饰器 +@timer_decorator +def slow_function(): + """一个慢函数""" + import time + time.sleep(1) # 模拟耗时操作 + return "任务完成" + +# 调用被装饰的函数 +result = slow_function() +print(result) +# 输出: +# 函数 slow_function 执行时间:1.0012秒 +# 任务完成 +``` + +--- + +## 七、函数式编程基础 + +### 7.1 高阶函数 + +高阶函数是接受函数作为参数或返回函数的函数: + +```python +def apply_operation(numbers, operation): + """对数字列表应用指定操作""" + result = [] + for num in numbers: + result.append(operation(num)) + return result + +# 定义一些操作函数 +def square(x): + return x ** 2 + +def cube(x): + return x ** 3 + +numbers = [1, 2, 3, 4, 5] + +# 使用不同的操作 +squares = apply_operation(numbers, square) +cubes = apply_operation(numbers, cube) + +print(f"原数字:{numbers}") +print(f"平方:{squares}") +print(f"立方:{cubes}") +``` + +### 7.2 常用内置高阶函数 + +```python +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +# map():对每个元素应用函数 +squares = list(map(lambda x: x**2, numbers)) +print(f"平方:{squares}") + +# filter():过滤元素 +even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) +print(f"偶数:{even_numbers}") + +# reduce():累积操作 +from functools import reduce +sum_all = reduce(lambda x, y: x + y, numbers) +print(f"所有数字的和:{sum_all}") + +# any():任意一个为True +has_even = any(x % 2 == 0 for x in numbers) +print(f"是否包含偶数:{has_even}") + +# all():所有都为True +all_positive = all(x > 0 for x in numbers) +print(f"是否都是正数:{all_positive}") +``` + +--- + +## 八、实战练习 + +### 8.1 练习1:温度转换器 + +```python +def celsius_to_fahrenheit(celsius): + """摄氏度转华氏度""" + fahrenheit = (celsius * 9/5) + 32 + return fahrenheit + +def fahrenheit_to_celsius(fahrenheit): + """华氏度转摄氏度""" + celsius = (fahrenheit - 32) * 5/9 + return celsius + +def temperature_converter(): + """温度转换主程序""" + print("温度转换器") + print("1. 摄氏度转华氏度") + print("2. 华氏度转摄氏度") + + choice = input("请选择转换类型 (1/2): ") + + if choice == '1': + celsius = float(input("请输入摄氏度: ")) + fahrenheit = celsius_to_fahrenheit(celsius) + print(f"{celsius}°C = {fahrenheit:.2f}°F") + elif choice == '2': + fahrenheit = float(input("请输入华氏度: ")) + celsius = fahrenheit_to_celsius(fahrenheit) + print(f"{fahrenheit}°F = {celsius:.2f}°C") + else: + print("无效选择") + +# 运行程序 +# temperature_converter() +``` + +### 8.2 练习2:学生成绩管理 + +```python +def calculate_average(scores): + """计算平均分""" + if not scores: + return 0 + return sum(scores) / len(scores) + +def get_grade(score): + """根据分数获取等级""" + if score >= 90: + return 'A' + elif score >= 80: + return 'B' + elif score >= 70: + return 'C' + elif score >= 60: + return 'D' + else: + return 'F' + +def analyze_scores(student_scores): + """分析学生成绩""" + print("学生成绩分析报告") + print("=" * 30) + + for name, scores in student_scores.items(): + avg_score = calculate_average(scores) + grade = get_grade(avg_score) + + print(f"学生:{name}") + print(f" 各科成绩:{scores}") + print(f" 平均分:{avg_score:.2f}") + print(f" 等级:{grade}") + print("-" * 20) + +# 测试数据 +student_data = { + "张三": [85, 92, 78, 96], + "李四": [76, 88, 82, 79], + "王五": [95, 87, 91, 93] +} + +analyze_scores(student_data) +``` + +### 8.3 练习3:简单计算器 + +```python +def add(a, b): + """加法""" + return a + b + +def subtract(a, b): + """减法""" + return a - b + +def multiply(a, b): + """乘法""" + return a * b + +def divide(a, b): + """除法""" + if b == 0: + return "错误:除数不能为0" + return a / b + +def calculator(): + """简单计算器""" + operations = { + '+': add, + '-': subtract, + '*': multiply, + '/': divide + } + + print("简单计算器") + print("支持的操作:+, -, *, /") + print("输入 'quit' 退出") + + while True: + try: + expression = input("请输入计算表达式 (如: 5 + 3): ") + + if expression.lower() == 'quit': + print("再见!") + break + + # 解析输入 + parts = expression.split() + if len(parts) != 3: + print("格式错误,请使用格式:数字 操作符 数字") + continue + + num1, operator, num2 = parts + num1, num2 = float(num1), float(num2) + + if operator in operations: + result = operations[operator](num1, num2) + print(f"结果:{num1} {operator} {num2} = {result}") + else: + print("不支持的操作符") + + except ValueError: + print("输入错误,请输入有效的数字") + except Exception as e: + print(f"发生错误:{e}") + +# 运行计算器 +# calculator() +``` + +--- + +## 九、函数最佳实践 + +### 9.1 函数设计原则 + +```python +# ✅ 好的函数设计 +def calculate_circle_area(radius): + """计算圆的面积 + + Args: + radius (float): 圆的半径 + + Returns: + float: 圆的面积 + + Raises: + ValueError: 当半径为负数时 + """ + if radius < 0: + raise ValueError("半径不能为负数") + + import math + return math.pi * radius ** 2 + +# ❌ 不好的函数设计 +def calc(r): + # 没有文档说明 + # 变量名不清晰 + # 没有错误处理 + return 3.14 * r * r +``` + +### 9.2 函数命名规范 + +```python +# ✅ 好的函数命名 +def get_user_age(user_id): + """获取用户年龄""" + pass + +def is_valid_email(email): + """检查邮箱是否有效""" + pass + +def calculate_total_price(items): + """计算总价格""" + pass + +# ❌ 不好的函数命名 +def func1(x): # 名称不明确 + pass + +def getData(): # 不符合Python命名规范 + pass + +def do_everything(): # 功能不单一 + pass +``` + +### 9.3 函数长度和复杂度 + +```python +# ✅ 单一职责,简洁明了 +def validate_password(password): + """验证密码强度""" + if len(password) < 8: + return False, "密码长度至少8位" + + if not any(c.isupper() for c in password): + return False, "密码必须包含大写字母" + + if not any(c.islower() for c in password): + return False, "密码必须包含小写字母" + + if not any(c.isdigit() for c in password): + return False, "密码必须包含数字" + + return True, "密码强度合格" + +# 测试 +passwords = ["123456", "Password", "Password123"] +for pwd in passwords: + is_valid, message = validate_password(pwd) + print(f"密码 '{pwd}': {message}") +``` + +--- + +## 十、总结 + +### 10.1 函数知识点总结 + +| 概念 | 说明 | 示例 | +|------|------|------| +| 函数定义 | 使用`def`关键字 | `def func_name():` | +| 参数类型 | 位置、关键字、默认、可变 | `def func(a, b=1, *args, **kwargs):` | +| 返回值 | 使用`return`返回结果 | `return result` | +| 作用域 | 局部变量vs全局变量 | `global variable_name` | +| Lambda | 匿名函数,简洁语法 | `lambda x: x * 2` | +| 递归 | 函数调用自己 | 阶乘、斐波那契 | +| 装饰器 | 增强函数功能 | `@decorator` | + +### 10.2 函数使用建议 + +1. **单一职责**:每个函数只做一件事 +2. **命名清晰**:函数名要能表达其功能 +3. **参数合理**:参数不要太多(建议不超过5个) +4. **文档完整**:写好函数说明文档 +5. **错误处理**:考虑异常情况 +6. **测试充分**:编写测试用例 + +### 10.3 下一步学习方向 + +掌握了函数后,你可以继续学习: +- **模块和包**:代码组织和复用 +- **面向对象编程**:类和对象 +- **异常处理**:错误处理机制 +- **文件操作**:读写文件 +- **高级特性**:生成器、装饰器等 + +恭喜你!函数是编程的核心,掌握了函数,你已经具备了编写复杂程序的基础能力。继续加油!🚀 + +--- + +**练习建议**: +1. 尝试重写之前学过的代码,使用函数来组织 +2. 编写一些实用的小工具函数 +3. 练习使用不同类型的参数 +4. 尝试编写递归函数解决问题 +5. 学会阅读和使用他人编写的函数 + + + + diff --git a/docs/Python/6.md b/docs/Python/6.md new file mode 100644 index 000000000..e0cfe02ce --- /dev/null +++ b/docs/Python/6.md @@ -0,0 +1,829 @@ +--- +title: 第6天-高级特性 +author: 哪吒 +date: '2023-06-15' +--- + +# 第6天-Python高级特性 + +恭喜你!经过前5天的学习,你已经掌握了Python的基础语法。今天我们来学习Python的**高级特性**,这些特性让Python变得更加强大和优雅! + +## 为什么要学习高级特性? + +想象一下: +- 🚗 **普通开车**:只会开车,但不知道GPS、自动泊车等高级功能 +- 🏎️ **高级驾驶**:熟练使用各种辅助功能,驾驶更轻松高效 + +Python的高级特性就像汽车的高级功能,让你的编程更加**简洁、高效、优雅**! + +--- + +## 一、列表推导式(List Comprehension) + +### 1.1 什么是列表推导式? + +列表推导式是Python创建列表的一种**简洁而强大**的方法,可以用一行代码完成复杂的列表生成。 + +> **生活类比**:就像工厂流水线,输入原材料,经过加工,输出成品列表。 + +### 1.2 基本语法 + +```python +# 基本语法:[表达式 for 变量 in 可迭代对象] +new_list = [expression for item in iterable] + +# 带条件的语法:[表达式 for 变量 in 可迭代对象 if 条件] +new_list = [expression for item in iterable if condition] +``` + +### 1.3 从传统方法到列表推导式 + +```python +# 传统方法:生成1到10的平方 +squares_traditional = [] +for i in range(1, 11): + squares_traditional.append(i ** 2) +print(squares_traditional) +# 输出:[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] + +# 列表推导式:一行搞定! +squares_comprehension = [i ** 2 for i in range(1, 11)] +print(squares_comprehension) +# 输出:[1, 4, 9, 16, 25, 36, 49, 64, 81, 100] + +# 效果相同,但列表推导式更简洁! +``` + +### 1.4 带条件的列表推导式 + +```python +# 筛选偶数并求平方 +numbers = range(1, 11) +even_squares = [x ** 2 for x in numbers if x % 2 == 0] +print(even_squares) # 输出:[4, 16, 36, 64, 100] + +# 处理字符串列表 +words = ['hello', 'world', 'python', 'programming'] +# 筛选长度大于5的单词并转为大写 +long_words = [word.upper() for word in words if len(word) > 5] +print(long_words) # 输出:['PYTHON', 'PROGRAMMING'] + +# 处理嵌套数据 +students = [('Alice', 85), ('Bob', 92), ('Charlie', 78), ('Diana', 96)] +# 筛选分数大于80的学生姓名 +high_scorers = [name for name, score in students if score > 80] +print(high_scorers) # 输出:['Alice', 'Bob', 'Diana'] +``` + +### 1.5 嵌套列表推导式 + +```python +# 创建二维列表(矩阵) +matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)] +print(matrix) +# 输出:[[1, 2, 3], [2, 4, 6], [3, 6, 9]] + +# 展平二维列表 +matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +flattened = [num for row in matrix for num in row] +print(flattened) # 输出:[1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +> **小白提示**:嵌套列表推导式从左到右读:先外层循环,再内层循环。 + +--- + +## 二、字典推导式(Dict Comprehension) + +### 2.1 基本语法 + +```python +# 基本语法:{键表达式: 值表达式 for 变量 in 可迭代对象} +new_dict = {key_expr: value_expr for item in iterable} + +# 带条件:{键表达式: 值表达式 for 变量 in 可迭代对象 if 条件} +new_dict = {key_expr: value_expr for item in iterable if condition} +``` + +### 2.2 实际应用示例 + +```python +# 创建数字到平方的映射 +square_dict = {x: x**2 for x in range(1, 6)} +print(square_dict) # 输出:{1: 1, 2: 4, 3: 9, 4: 16, 5: 25} + +# 字符串长度映射 +words = ['apple', 'banana', 'cherry', 'date'] +word_lengths = {word: len(word) for word in words} +print(word_lengths) +# 输出:{'apple': 5, 'banana': 6, 'cherry': 6, 'date': 4} + +# 筛选并转换字典 +original_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4} +# 只保留值为偶数的项,并将值翻倍 +even_doubled = {k: v*2 for k, v in original_dict.items() if v % 2 == 0} +print(even_doubled) # 输出:{'b': 4, 'd': 8} + +# 交换键值对 +original = {'name': 'Alice', 'age': '25', 'city': 'Beijing'} +swapped = {v: k for k, v in original.items()} +print(swapped) # 输出:{'Alice': 'name', '25': 'age', 'Beijing': 'city'} +``` + +--- + +## 三、集合推导式(Set Comprehension) + +### 3.1 基本用法 + +```python +# 基本语法:{表达式 for 变量 in 可迭代对象} +new_set = {expression for item in iterable} + +# 创建平方数集合 +squares_set = {x**2 for x in range(1, 6)} +print(squares_set) # 输出:{1, 4, 9, 16, 25} + +# 去重功能 +numbers = [1, 2, 2, 3, 3, 4, 5, 5] +unique_squares = {x**2 for x in numbers} +print(unique_squares) # 输出:{1, 4, 9, 16, 25} + +# 字符串处理 +text = "Hello World" +unique_chars = {char.lower() for char in text if char.isalpha()} +print(unique_chars) # 输出:{'h', 'e', 'l', 'o', 'w', 'r', 'd'} +``` + +--- + +## 四、生成器(Generator) + +### 4.1 什么是生成器? + +生成器是一种特殊的迭代器,它**按需生成数据**,而不是一次性创建所有数据。 + +> **生活类比**: +> - **列表**:像一次性买一箱苹果,占用很多冰箱空间 +> - **生成器**:像果园,需要时才摘一个苹果,节省空间 + +### 4.2 生成器表达式 + +```python +# 生成器表达式:用圆括号代替方括号 +squares_gen = (x**2 for x in range(1, 6)) +print(type(squares_gen)) # 输出: + +# 逐个获取值 +print(next(squares_gen)) # 输出:1 +print(next(squares_gen)) # 输出:4 +print(next(squares_gen)) # 输出:9 + +# 或者用for循环遍历 +squares_gen = (x**2 for x in range(1, 6)) +for square in squares_gen: + print(square) +# 输出:1 4 9 16 25 +``` + +### 4.3 yield关键字 + +```python +def fibonacci_generator(n): + """生成斐波那契数列的前n项""" + a, b = 0, 1 + count = 0 + while count < n: + yield a # 返回当前值,但函数不结束 + a, b = b, a + b + count += 1 + +# 使用生成器 +fib_gen = fibonacci_generator(10) +for num in fib_gen: + print(num, end=' ') +# 输出:0 1 1 2 3 5 8 13 21 34 + +print() # 换行 + +# 生成器的内存优势 +def large_range_list(n): + """传统方法:创建大列表""" + return [i for i in range(n)] + +def large_range_generator(n): + """生成器方法:按需生成""" + for i in range(n): + yield i + +# 对比内存使用(这里只是演示概念) +import sys + +# 小数据量时差别不大 +small_list = large_range_list(100) +small_gen = large_range_generator(100) + +print(f"列表大小:{sys.getsizeof(small_list)} 字节") +print(f"生成器大小:{sys.getsizeof(small_gen)} 字节") +``` + +### 4.4 生成器的实际应用 + +```python +def read_large_file(filename): + """逐行读取大文件,节省内存""" + with open(filename, 'r', encoding='utf-8') as file: + for line in file: + yield line.strip() + +def process_data_stream(): + """模拟数据流处理""" + data = range(1000000) # 模拟大量数据 + for item in data: + if item % 2 == 0: # 只处理偶数 + yield item * 2 + +# 使用生成器处理大量数据 +processed_data = process_data_stream() +# 只取前10个结果 +for i, value in enumerate(processed_data): + if i >= 10: + break + print(value, end=' ') +# 输出:0 4 8 12 16 20 24 28 32 36 +``` + +--- + +## 五、迭代器(Iterator) + +### 5.1 理解迭代器 + +```python +# 可迭代对象 vs 迭代器 +my_list = [1, 2, 3, 4, 5] # 可迭代对象 +my_iterator = iter(my_list) # 迭代器 + +print(type(my_list)) # +print(type(my_iterator)) # + +# 使用迭代器 +print(next(my_iterator)) # 1 +print(next(my_iterator)) # 2 +print(next(my_iterator)) # 3 + +# 迭代器只能向前,不能后退 +# print(next(my_iterator)) # 4 +# print(next(my_iterator)) # 5 +# print(next(my_iterator)) # StopIteration 异常 +``` + +### 5.2 自定义迭代器 + +```python +class CountDown: + """倒计时迭代器""" + + def __init__(self, start): + self.start = start + + def __iter__(self): + return self + + def __next__(self): + if self.start <= 0: + raise StopIteration + self.start -= 1 + return self.start + 1 + +# 使用自定义迭代器 +countdown = CountDown(5) +for num in countdown: + print(f"倒计时:{num}") +# 输出: +# 倒计时:5 +# 倒计时:4 +# 倒计时:3 +# 倒计时:2 +# 倒计时:1 +``` + +--- + +## 六、装饰器进阶 + +### 6.1 带参数的装饰器 + +```python +def repeat(times): + """重复执行装饰器""" + def decorator(func): + def wrapper(*args, **kwargs): + for i in range(times): + result = func(*args, **kwargs) + return result + return wrapper + return decorator + +@repeat(3) +def greet(name): + print(f"Hello, {name}!") + +greet("Alice") +# 输出: +# Hello, Alice! +# Hello, Alice! +# Hello, Alice! +``` + +### 6.2 类装饰器 + +```python +class Timer: + """计时装饰器类""" + + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + import time + start = time.time() + result = self.func(*args, **kwargs) + end = time.time() + print(f"{self.func.__name__} 执行时间:{end - start:.4f}秒") + return result + +@Timer +def slow_function(): + import time + time.sleep(1) + return "完成" + +result = slow_function() +print(result) +``` + +### 6.3 多个装饰器组合 + +```python +def bold(func): + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + return f"{result}" + return wrapper + +def italic(func): + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + return f"{result}" + return wrapper + +@bold +@italic +def say_hello(name): + return f"Hello, {name}!" + +print(say_hello("World")) +# 输出:Hello, World! +``` + +--- + +## 七、高阶函数 + +### 7.1 map()函数 + +```python +# map(function, iterable) - 将函数应用到每个元素 +numbers = [1, 2, 3, 4, 5] + +# 使用普通函数 +def square(x): + return x ** 2 + +squared = list(map(square, numbers)) +print(squared) # [1, 4, 9, 16, 25] + +# 使用lambda函数 +squared_lambda = list(map(lambda x: x ** 2, numbers)) +print(squared_lambda) # [1, 4, 9, 16, 25] + +# 处理字符串 +words = ['hello', 'world', 'python'] +uppercase = list(map(str.upper, words)) +print(uppercase) # ['HELLO', 'WORLD', 'PYTHON'] + +# 多个可迭代对象 +nums1 = [1, 2, 3] +nums2 = [4, 5, 6] +sums = list(map(lambda x, y: x + y, nums1, nums2)) +print(sums) # [5, 7, 9] +``` + +### 7.2 filter()函数 + +```python +# filter(function, iterable) - 过滤元素 +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +# 筛选偶数 +even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) +print(even_numbers) # [2, 4, 6, 8, 10] + +# 筛选大于5的数 +greater_than_5 = list(filter(lambda x: x > 5, numbers)) +print(greater_than_5) # [6, 7, 8, 9, 10] + +# 筛选非空字符串 +words = ['hello', '', 'world', '', 'python'] +non_empty = list(filter(None, words)) # None会过滤掉假值 +print(non_empty) # ['hello', 'world', 'python'] + +# 自定义过滤函数 +def is_long_word(word): + return len(word) > 5 + +words = ['cat', 'elephant', 'dog', 'hippopotamus'] +long_words = list(filter(is_long_word, words)) +print(long_words) # ['elephant', 'hippopotamus'] +``` + +### 7.3 reduce()函数 + +```python +from functools import reduce + +# reduce(function, iterable[, initializer]) - 累积操作 +numbers = [1, 2, 3, 4, 5] + +# 计算总和 +total = reduce(lambda x, y: x + y, numbers) +print(total) # 15 + +# 计算乘积 +product = reduce(lambda x, y: x * y, numbers) +print(product) # 120 + +# 找最大值 +max_value = reduce(lambda x, y: x if x > y else y, numbers) +print(max_value) # 5 + +# 字符串连接 +words = ['Hello', ' ', 'World', '!'] +sentence = reduce(lambda x, y: x + y, words) +print(sentence) # "Hello World!" + +# 带初始值 +numbers = [1, 2, 3, 4, 5] +total_with_init = reduce(lambda x, y: x + y, numbers, 100) +print(total_with_init) # 115 (100 + 15) +``` + +### 7.4 zip()函数 + +```python +# zip(*iterables) - 打包多个可迭代对象 +names = ['Alice', 'Bob', 'Charlie'] +ages = [25, 30, 35] +cities = ['Beijing', 'Shanghai', 'Guangzhou'] + +# 基本用法 +combined = list(zip(names, ages)) +print(combined) # [('Alice', 25), ('Bob', 30), ('Charlie', 35)] + +# 三个列表组合 +full_info = list(zip(names, ages, cities)) +print(full_info) +# [('Alice', 25, 'Beijing'), ('Bob', 30, 'Shanghai'), ('Charlie', 35, 'Guangzhou')] + +# 解包zip结果 +for name, age, city in zip(names, ages, cities): + print(f"{name}, {age}岁, 来自{city}") + +# zip的逆操作:解包 +combined = [('Alice', 25), ('Bob', 30), ('Charlie', 35)] +names_unpacked, ages_unpacked = zip(*combined) +print(names_unpacked) # ('Alice', 'Bob', 'Charlie') +print(ages_unpacked) # (25, 30, 35) + +# 不同长度的列表(以最短为准) +short_list = [1, 2] +long_list = [10, 20, 30, 40] +result = list(zip(short_list, long_list)) +print(result) # [(1, 10), (2, 20)] +``` + +--- + +## 八、上下文管理器(with语句) + +### 8.1 文件操作的最佳实践 + +```python +# 传统方法(不推荐) +file = open('example.txt', 'w') +file.write('Hello, World!') +file.close() # 容易忘记关闭 + +# 使用with语句(推荐) +with open('example.txt', 'w') as file: + file.write('Hello, World!') +# 自动关闭文件,即使出现异常也会关闭 + +# 读取文件 +with open('example.txt', 'r') as file: + content = file.read() + print(content) # Hello, World! +``` + +### 8.2 自定义上下文管理器 + +```python +class Timer: + """计时上下文管理器""" + + def __enter__(self): + import time + self.start = time.time() + print("开始计时...") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + import time + self.end = time.time() + print(f"执行时间:{self.end - self.start:.4f}秒") + +# 使用自定义上下文管理器 +with Timer(): + import time + time.sleep(1) + print("执行一些操作...") +# 输出: +# 开始计时... +# 执行一些操作... +# 执行时间:1.0012秒 +``` + +### 8.3 使用contextlib简化 + +```python +from contextlib import contextmanager + +@contextmanager +def timer_context(): + import time + start = time.time() + print("开始计时...") + try: + yield + finally: + end = time.time() + print(f"执行时间:{end - start:.4f}秒") + +# 使用 +with timer_context(): + import time + time.sleep(0.5) + print("执行操作...") +``` + +--- + +## 九、类型注解(Type Hints) + +### 9.1 基本类型注解 + +```python +# 变量类型注解 +name: str = "Alice" +age: int = 25 +height: float = 1.68 +is_student: bool = True + +# 函数类型注解 +def greet(name: str, age: int) -> str: + return f"Hello, {name}! You are {age} years old." + +def calculate_area(length: float, width: float) -> float: + """计算矩形面积""" + return length * width + +# 使用 +result = greet("Bob", 30) +area = calculate_area(5.0, 3.0) +print(result) +print(f"面积:{area}") +``` + +### 9.2 复杂类型注解 + +```python +from typing import List, Dict, Tuple, Optional, Union + +# 列表类型 +def process_numbers(numbers: List[int]) -> List[int]: + return [n * 2 for n in numbers] + +# 字典类型 +def get_student_info() -> Dict[str, Union[str, int]]: + return {"name": "Alice", "age": 20, "grade": "A"} + +# 元组类型 +def get_coordinates() -> Tuple[float, float]: + return (39.9042, 116.4074) + +# 可选类型 +def find_user(user_id: int) -> Optional[str]: + users = {1: "Alice", 2: "Bob"} + return users.get(user_id) # 可能返回None + +# 联合类型 +def process_id(user_id: Union[int, str]) -> str: + return str(user_id) + +# 使用示例 +numbers = [1, 2, 3, 4, 5] +doubled = process_numbers(numbers) +print(doubled) # [2, 4, 6, 8, 10] + +student = get_student_info() +print(student) # {'name': 'Alice', 'age': 20, 'grade': 'A'} + +coords = get_coordinates() +print(coords) # (39.9042, 116.4074) + +user = find_user(1) +print(user) # Alice + +user_id = process_id(123) +print(user_id) # "123" +``` + +--- + +## 十、实战练习 + +### 10.1 数据处理综合练习 + +```python +# 学生成绩数据处理 +students_data = [ + {'name': 'Alice', 'scores': [85, 92, 78, 96]}, + {'name': 'Bob', 'scores': [76, 88, 82, 79]}, + {'name': 'Charlie', 'scores': [95, 87, 91, 93]}, + {'name': 'Diana', 'scores': [68, 74, 82, 79]} +] + +# 使用列表推导式计算每个学生的平均分 +average_scores = [ + { + 'name': student['name'], + 'average': sum(student['scores']) / len(student['scores']) + } + for student in students_data +] + +print("平均分:") +for student in average_scores: + print(f"{student['name']}: {student['average']:.2f}") + +# 使用filter筛选优秀学生(平均分>85) +excellent_students = list(filter( + lambda s: s['average'] > 85, + average_scores +)) + +print("\n优秀学生:") +for student in excellent_students: + print(f"{student['name']}: {student['average']:.2f}") + +# 使用map转换数据格式 +student_names = list(map(lambda s: s['name'].upper(), excellent_students)) +print(f"\n优秀学生姓名(大写):{student_names}") +``` + +### 10.2 文件处理生成器 + +```python +def process_log_file(filename: str): + """处理日志文件的生成器""" + try: + with open(filename, 'r', encoding='utf-8') as file: + for line_num, line in enumerate(file, 1): + line = line.strip() + if line and not line.startswith('#'): # 跳过空行和注释 + yield line_num, line + except FileNotFoundError: + print(f"文件 {filename} 不存在") + +# 创建示例日志文件 +with open('sample.log', 'w', encoding='utf-8') as f: + f.write("""# 这是注释 +INFO: 应用启动 +WARNING: 内存使用率较高 +ERROR: 数据库连接失败 +# 另一个注释 +INFO: 重新连接成功 +""") + +# 使用生成器处理文件 +print("处理日志文件:") +for line_num, content in process_log_file('sample.log'): + print(f"第{line_num}行: {content}") +``` + +### 10.3 装饰器实战 + +```python +from functools import wraps +import time + +def cache(func): + """简单的缓存装饰器""" + cache_dict = {} + + @wraps(func) + def wrapper(*args, **kwargs): + # 创建缓存键 + key = str(args) + str(sorted(kwargs.items())) + + if key in cache_dict: + print(f"缓存命中: {func.__name__}") + return cache_dict[key] + + print(f"计算中: {func.__name__}") + result = func(*args, **kwargs) + cache_dict[key] = result + return result + + return wrapper + +def timing(func): + """计时装饰器""" + @wraps(func) + def wrapper(*args, **kwargs): + start = time.time() + result = func(*args, **kwargs) + end = time.time() + print(f"{func.__name__} 执行时间: {end - start:.4f}秒") + return result + return wrapper + +@cache +@timing +def fibonacci(n: int) -> int: + """计算斐波那契数列(递归版本)""" + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# 测试缓存效果 +print("第一次计算:") +result1 = fibonacci(10) +print(f"fibonacci(10) = {result1}") + +print("\n第二次计算(应该使用缓存):") +result2 = fibonacci(10) +print(f"fibonacci(10) = {result2}") +``` + +--- + +## 十一、总结 + +### 11.1 高级特性对比表 + +| 特性 | 优点 | 使用场景 | 注意事项 | +|------|------|----------|----------| +| 列表推导式 | 简洁、高效 | 数据转换、过滤 | 不要过度复杂化 | +| 生成器 | 节省内存 | 大数据处理、流式处理 | 只能遍历一次 | +| 装饰器 | 代码复用、功能增强 | 日志、缓存、权限验证 | 理解执行顺序 | +| 高阶函数 | 函数式编程 | 数据处理、函数组合 | 可读性考虑 | +| 类型注解 | 代码可读性、IDE支持 | 大型项目、团队协作 | 运行时不强制 | + +### 11.2 最佳实践建议 + +1. **适度使用**:不要为了炫技而使用高级特性 +2. **可读性优先**:代码要让其他人能够理解 +3. **性能考虑**:在需要时才优化性能 +4. **团队规范**:与团队保持一致的编码风格 + +### 11.3 下一步学习方向 + +掌握了这些高级特性后,你可以继续学习: +- **模块和包**:代码组织和管理 +- **面向对象编程**:类的高级特性 +- **异常处理**:错误处理和调试 +- **文件和IO操作**:数据持久化 +- **网络编程**:Web开发基础 + +恭喜你!掌握了这些高级特性,你的Python编程能力已经上了一个台阶!🚀 + +--- + +**练习建议**: +1. 重写之前的代码,使用列表推导式简化 +2. 尝试创建自己的生成器函数 +3. 编写实用的装饰器 +4. 练习使用高阶函数处理数据 +5. 为自己的函数添加类型注解 diff --git a/docs/Python/7.md b/docs/Python/7.md new file mode 100644 index 000000000..ccf0d7722 --- /dev/null +++ b/docs/Python/7.md @@ -0,0 +1,1044 @@ +--- +title: 第7天-函数式编程 +author: 哪吒 +date: '2023-06-15' +--- + +# 第7天-Python函数式编程 + +欢迎来到Python函数式编程的世界!今天我们将深入学习一种优雅的编程范式,它能让你的代码更加简洁、可读和可维护。 + +## 什么是函数式编程? + +函数式编程(Functional Programming,FP)是一种编程范式,它将计算视为数学函数的求值,避免改变状态和可变数据。 + +> **生活类比**: +> - **命令式编程**:像做菜的详细步骤,"先切菜,再炒菜,然后装盘" +> - **函数式编程**:像数学公式,"f(原料) = 成品菜",关注输入和输出的关系 + +### 函数式编程的核心特点 + +1. **函数是一等公民**:函数可以作为参数传递、作为返回值、赋值给变量 +2. **不可变性**:数据一旦创建就不能修改 +3. **纯函数**:相同输入总是产生相同输出,没有副作用 +4. **高阶函数**:接受函数作为参数或返回函数的函数 + +--- + +## 一、纯函数(Pure Functions) + +### 1.1 什么是纯函数? + +纯函数具有两个特点: +1. **确定性**:相同的输入总是产生相同的输出 +2. **无副作用**:不修改外部状态,不产生可观察的副作用 + +```python +# 纯函数示例 +def add(x, y): + """纯函数:只依赖输入参数,总是返回相同结果""" + return x + y + +def multiply(x, y): + """纯函数:简单的数学运算""" + return x * y + +def get_full_name(first_name, last_name): + """纯函数:字符串处理""" + return f"{first_name} {last_name}" + +# 测试纯函数 +print(add(2, 3)) # 总是返回 5 +print(multiply(4, 5)) # 总是返回 20 +print(get_full_name("张", "三")) # 总是返回 "张 三" +``` + +### 1.2 非纯函数示例 + +```python +import random +import datetime + +# 非纯函数示例(不推荐在函数式编程中使用) +counter = 0 + +def impure_increment(): + """非纯函数:修改全局变量""" + global counter + counter += 1 + return counter + +def get_random_number(): + """非纯函数:每次调用结果不同""" + return random.randint(1, 100) + +def get_current_time(): + """非纯函数:依赖外部状态(当前时间)""" + return datetime.datetime.now() + +# 这些函数的输出不可预测 +print(impure_increment()) # 1 +print(impure_increment()) # 2 +print(get_random_number()) # 随机数 +print(get_current_time()) # 当前时间 +``` + +### 1.3 将非纯函数转换为纯函数 + +```python +# 改进:将非纯函数转换为纯函数 +def pure_increment(current_value): + """纯函数版本:不修改外部状态""" + return current_value + 1 + +def generate_sequence(start, length): + """纯函数:生成确定的序列""" + return [start + i for i in range(length)] + +def format_timestamp(timestamp): + """纯函数:格式化给定的时间戳""" + return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + +# 使用纯函数 +current = 0 +for i in range(3): + current = pure_increment(current) + print(f"计数器:{current}") + +sequence = generate_sequence(10, 5) +print(f"序列:{sequence}") # [10, 11, 12, 13, 14] + +timestamp = 1640995200 # 固定时间戳 +formatted_time = format_timestamp(timestamp) +print(f"格式化时间:{formatted_time}") +``` + +--- + +## 二、高阶函数(Higher-Order Functions) + +### 2.1 函数作为参数 + +```python +def apply_operation(numbers, operation): + """高阶函数:接受函数作为参数""" + return [operation(num) for num in numbers] + +def square(x): + return x ** 2 + +def cube(x): + return x ** 3 + +def double(x): + return x * 2 + +# 使用不同的操作函数 +numbers = [1, 2, 3, 4, 5] + +squared = apply_operation(numbers, square) +print(f"平方:{squared}") # [1, 4, 9, 16, 25] + +cubed = apply_operation(numbers, cube) +print(f"立方:{cubed}") # [1, 8, 27, 64, 125] + +doubled = apply_operation(numbers, double) +print(f"翻倍:{doubled}") # [2, 4, 6, 8, 10] + +# 使用lambda函数 +halved = apply_operation(numbers, lambda x: x / 2) +print(f"减半:{halved}") # [0.5, 1.0, 1.5, 2.0, 2.5] +``` + +### 2.2 函数作为返回值 + +```python +def create_multiplier(factor): + """返回一个乘法函数""" + def multiplier(x): + return x * factor + return multiplier + +def create_validator(min_value, max_value): + """返回一个验证函数""" + def validator(value): + return min_value <= value <= max_value + return validator + +def create_formatter(prefix, suffix): + """返回一个格式化函数""" + def formatter(text): + return f"{prefix}{text}{suffix}" + return formatter + +# 使用函数工厂 +double_func = create_multiplier(2) +triple_func = create_multiplier(3) + +print(double_func(5)) # 10 +print(triple_func(5)) # 15 + +# 创建验证器 +age_validator = create_validator(0, 120) +score_validator = create_validator(0, 100) + +print(age_validator(25)) # True +print(age_validator(150)) # False +print(score_validator(85)) # True +print(score_validator(105)) # False + +# 创建格式化器 +html_formatter = create_formatter("

", "

") +markdown_formatter = create_formatter("**", "**") + +print(html_formatter("Hello")) #

Hello

+print(markdown_formatter("Bold")) # **Bold** +``` + +### 2.3 函数组合 + +```python +def compose(f, g): + """函数组合:返回 f(g(x))""" + return lambda x: f(g(x)) + +def pipe(*functions): + """管道操作:从左到右依次应用函数""" + def piped_function(value): + for func in functions: + value = func(value) + return value + return piped_function + +# 基础函数 +def add_one(x): + return x + 1 + +def multiply_by_two(x): + return x * 2 + +def square(x): + return x ** 2 + +# 函数组合 +add_then_multiply = compose(multiply_by_two, add_one) +result1 = add_then_multiply(3) # multiply_by_two(add_one(3)) = multiply_by_two(4) = 8 +print(f"组合结果:{result1}") + +# 管道操作 +process_number = pipe(add_one, multiply_by_two, square) +result2 = process_number(3) # ((3+1)*2)^2 = (4*2)^2 = 8^2 = 64 +print(f"管道结果:{result2}") + +# 处理字符串的管道 +def to_upper(s): + return s.upper() + +def add_exclamation(s): + return s + "!" + +def add_prefix(s): + return ">>> " + s + +process_text = pipe(to_upper, add_exclamation, add_prefix) +result3 = process_text("hello world") +print(f"文本处理:{result3}") # >>> HELLO WORLD! +``` + +--- + +## 三、内置高阶函数深入 + +### 3.1 map()函数进阶 + +```python +# 处理多个序列 +def add_three_numbers(x, y, z): + return x + y + z + +list1 = [1, 2, 3] +list2 = [10, 20, 30] +list3 = [100, 200, 300] + +result = list(map(add_three_numbers, list1, list2, list3)) +print(f"三个列表相加:{result}") # [111, 222, 333] + +# 处理字典 +students = [ + {'name': 'Alice', 'score': 85}, + {'name': 'Bob', 'score': 92}, + {'name': 'Charlie', 'score': 78} +] + +# 提取姓名 +names = list(map(lambda student: student['name'], students)) +print(f"学生姓名:{names}") # ['Alice', 'Bob', 'Charlie'] + +# 计算等级 +def get_grade(score): + if score >= 90: + return 'A' + elif score >= 80: + return 'B' + elif score >= 70: + return 'C' + else: + return 'D' + +grades = list(map(lambda s: get_grade(s['score']), students)) +print(f"学生等级:{grades}") # ['B', 'A', 'C'] +``` + +### 3.2 filter()函数进阶 + +```python +# 复杂过滤条件 +numbers = range(1, 21) + +# 过滤质数 +def is_prime(n): + if n < 2: + return False + for i in range(2, int(n ** 0.5) + 1): + if n % i == 0: + return False + return True + +primes = list(filter(is_prime, numbers)) +print(f"质数:{primes}") # [2, 3, 5, 7, 11, 13, 17, 19] + +# 过滤字符串 +words = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig'] + +# 长度大于5且包含'e'的单词 +filtered_words = list(filter( + lambda word: len(word) > 5 and 'e' in word, + words +)) +print(f"过滤后的单词:{filtered_words}") # ['cherry', 'elderberry'] + +# 过滤学生数据 +students = [ + {'name': 'Alice', 'age': 20, 'score': 85}, + {'name': 'Bob', 'age': 22, 'score': 92}, + {'name': 'Charlie', 'age': 19, 'score': 78}, + {'name': 'Diana', 'age': 21, 'score': 96} +] + +# 年龄大于20且分数大于80的学生 +excellent_students = list(filter( + lambda s: s['age'] > 20 and s['score'] > 80, + students +)) +print("优秀学生:") +for student in excellent_students: + print(f" {student['name']}: {student['age']}岁, {student['score']}分") +``` + +### 3.3 reduce()函数进阶 + +```python +from functools import reduce + +# 复杂的累积操作 +numbers = [1, 2, 3, 4, 5] + +# 计算阶乘 +factorial = reduce(lambda x, y: x * y, numbers) +print(f"阶乘:{factorial}") # 120 + +# 找到最大值和最小值 +max_value = reduce(lambda x, y: x if x > y else y, numbers) +min_value = reduce(lambda x, y: x if x < y else y, numbers) +print(f"最大值:{max_value}, 最小值:{min_value}") # 5, 1 + +# 字符串操作 +words = ['Python', 'is', 'awesome', 'for', 'programming'] + +# 连接字符串 +sentence = reduce(lambda x, y: x + ' ' + y, words) +print(f"句子:{sentence}") # Python is awesome for programming + +# 找最长的单词 +longest_word = reduce( + lambda x, y: x if len(x) > len(y) else y, + words +) +print(f"最长单词:{longest_word}") # programming + +# 复杂数据结构的处理 +orders = [ + {'id': 1, 'amount': 100}, + {'id': 2, 'amount': 250}, + {'id': 3, 'amount': 75}, + {'id': 4, 'amount': 300} +] + +# 计算总金额 +total_amount = reduce( + lambda total, order: total + order['amount'], + orders, + 0 # 初始值 +) +print(f"总金额:{total_amount}") # 725 + +# 合并字典 +data_sources = [ + {'users': 100, 'posts': 500}, + {'users': 50, 'comments': 200}, + {'users': 75, 'likes': 1000} +] + +merged_data = reduce( + lambda acc, data: {**acc, **data}, + data_sources, + {} +) +print(f"合并数据:{merged_data}") +# {'users': 75, 'posts': 500, 'comments': 200, 'likes': 1000} +``` + +--- + +## 四、函数式编程工具 + +### 4.1 itertools模块 + +```python +import itertools + +# itertools.chain - 连接多个可迭代对象 +list1 = [1, 2, 3] +list2 = [4, 5, 6] +list3 = [7, 8, 9] + +chained = list(itertools.chain(list1, list2, list3)) +print(f"连接列表:{chained}") # [1, 2, 3, 4, 5, 6, 7, 8, 9] + +# itertools.cycle - 无限循环 +cycler = itertools.cycle(['A', 'B', 'C']) +first_10 = [next(cycler) for _ in range(10)] +print(f"循环前10个:{first_10}") # ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A'] + +# itertools.repeat - 重复元素 +repeated = list(itertools.repeat('Hello', 5)) +print(f"重复元素:{repeated}") # ['Hello', 'Hello', 'Hello', 'Hello', 'Hello'] + +# itertools.takewhile - 条件为真时取元素 +numbers = [1, 3, 5, 8, 9, 11, 13] +less_than_10 = list(itertools.takewhile(lambda x: x < 10, numbers)) +print(f"小于10的连续元素:{less_than_10}") # [1, 3, 5, 8, 9] + +# itertools.dropwhile - 条件为真时跳过元素 +after_10 = list(itertools.dropwhile(lambda x: x < 10, numbers)) +print(f"大于等于10的元素:{after_10}") # [11, 13] + +# itertools.groupby - 分组 +data = [('A', 1), ('A', 2), ('B', 3), ('B', 4), ('C', 5)] +grouped = {k: list(g) for k, g in itertools.groupby(data, key=lambda x: x[0])} +print(f"分组数据:{grouped}") +# {'A': [('A', 1), ('A', 2)], 'B': [('B', 3), ('B', 4)], 'C': [('C', 5)]} +``` + +### 4.2 operator模块 + +```python +import operator +from functools import reduce + +# 使用operator模块替代lambda +numbers = [1, 2, 3, 4, 5] + +# 求和 +total = reduce(operator.add, numbers) +print(f"总和:{total}") # 15 + +# 求积 +product = reduce(operator.mul, numbers) +print(f"乘积:{product}") # 120 + +# 字符串连接 +words = ['Hello', 'World', 'Python'] +sentence = reduce(operator.add, words) +print(f"连接字符串:{sentence}") # HelloWorldPython + +# 获取属性和索引 +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def __repr__(self): + return f"Person('{self.name}', {self.age})" + +people = [ + Person('Alice', 25), + Person('Bob', 30), + Person('Charlie', 20) +] + +# 按年龄排序 +sorted_by_age = sorted(people, key=operator.attrgetter('age')) +print(f"按年龄排序:{sorted_by_age}") + +# 按姓名排序 +sorted_by_name = sorted(people, key=operator.attrgetter('name')) +print(f"按姓名排序:{sorted_by_name}") + +# 处理元组列表 +student_scores = [('Alice', 85), ('Bob', 92), ('Charlie', 78)] +sorted_by_score = sorted(student_scores, key=operator.itemgetter(1)) +print(f"按分数排序:{sorted_by_score}") +``` + +### 4.3 functools模块进阶 + +```python +from functools import partial, wraps, lru_cache, singledispatch + +# partial - 偏函数应用 +def multiply(x, y, z): + return x * y * z + +# 创建偏函数 +double = partial(multiply, 2) # 固定第一个参数为2 +triple = partial(multiply, 3) # 固定第一个参数为3 + +print(double(5, 6)) # 2 * 5 * 6 = 60 +print(triple(4, 5)) # 3 * 4 * 5 = 60 + +# 更复杂的偏函数 +def log_message(level, message, timestamp=None): + import datetime + if timestamp is None: + timestamp = datetime.datetime.now() + return f"[{timestamp}] {level}: {message}" + +# 创建特定级别的日志函数 +info_log = partial(log_message, "INFO") +error_log = partial(log_message, "ERROR") +warning_log = partial(log_message, "WARNING") + +print(info_log("应用启动")) +print(error_log("数据库连接失败")) +print(warning_log("内存使用率过高")) + +# lru_cache - 缓存装饰器 +@lru_cache(maxsize=128) +def fibonacci(n): + """带缓存的斐波那契函数""" + if n < 2: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# 测试缓存效果 +import time + +start = time.time() +result = fibonacci(35) +end = time.time() +print(f"fibonacci(35) = {result}, 耗时: {end - start:.4f}秒") + +# 第二次调用会很快(使用缓存) +start = time.time() +result = fibonacci(35) +end = time.time() +print(f"fibonacci(35) = {result}, 耗时: {end - start:.6f}秒") + +# singledispatch - 单分派泛型函数 +@singledispatch +def process_data(data): + """默认处理函数""" + return f"处理未知类型: {type(data).__name__}" + +@process_data.register +def _(data: int): + return f"处理整数: {data * 2}" + +@process_data.register +def _(data: str): + return f"处理字符串: {data.upper()}" + +@process_data.register +def _(data: list): + return f"处理列表: {len(data)} 个元素" + +# 测试单分派 +print(process_data(42)) # 处理整数: 84 +print(process_data("hello")) # 处理字符串: HELLO +print(process_data([1,2,3])) # 处理列表: 3 个元素 +print(process_data(3.14)) # 处理未知类型: float +``` + +--- + +## 五、不可变数据结构 + +### 5.1 使用元组和命名元组 + +```python +from collections import namedtuple + +# 普通元组 +point = (3, 4) +print(f"点坐标:{point}") +print(f"x坐标:{point[0]}, y坐标:{point[1]}") + +# 命名元组 - 更具可读性 +Point = namedtuple('Point', ['x', 'y']) +point = Point(3, 4) +print(f"点坐标:{point}") +print(f"x坐标:{point.x}, y坐标:{point.y}") + +# 命名元组的方法 +print(f"转为字典:{point._asdict()}") +print(f"替换值:{point._replace(x=5)}") + +# 复杂的命名元组 +Student = namedtuple('Student', ['name', 'age', 'grade', 'scores']) +student = Student( + name='Alice', + age=20, + grade='A', + scores=(85, 92, 78, 96) +) + +print(f"学生信息:{student}") +print(f"平均分:{sum(student.scores) / len(student.scores):.2f}") + +# 函数式处理命名元组 +def calculate_gpa(student): + """计算GPA(纯函数)""" + average = sum(student.scores) / len(student.scores) + if average >= 90: + return 4.0 + elif average >= 80: + return 3.0 + elif average >= 70: + return 2.0 + else: + return 1.0 + +def update_grade(student, new_grade): + """更新等级(返回新对象)""" + return student._replace(grade=new_grade) + +gpa = calculate_gpa(student) +print(f"GPA:{gpa}") + +updated_student = update_grade(student, 'A+') +print(f"更新后的学生:{updated_student}") +print(f"原学生不变:{student}") +``` + +### 5.2 frozenset的使用 + +```python +# frozenset - 不可变集合 +mutable_set = {1, 2, 3, 4, 5} +immutable_set = frozenset([1, 2, 3, 4, 5]) + +print(f"可变集合:{mutable_set}") +print(f"不可变集合:{immutable_set}") + +# frozenset可以作为字典的键 +set_dict = { + frozenset([1, 2]): "小集合", + frozenset([1, 2, 3, 4, 5]): "大集合" +} + +print(f"集合字典:{set_dict}") + +# 函数式操作frozenset +def union_sets(*sets): + """合并多个集合""" + result = frozenset() + for s in sets: + result = result.union(s) + return result + +def filter_set(s, predicate): + """过滤集合元素""" + return frozenset(filter(predicate, s)) + +set1 = frozenset([1, 2, 3]) +set2 = frozenset([3, 4, 5]) +set3 = frozenset([5, 6, 7]) + +unioned = union_sets(set1, set2, set3) +print(f"合并集合:{uniond}") + +even_numbers = filter_set(uniond, lambda x: x % 2 == 0) +print(f"偶数:{even_numbers}") +``` + +--- + +## 六、函数式编程实战 + +### 6.1 数据处理管道 + +```python +from functools import reduce +from typing import List, Dict, Any + +# 学生数据 +students_data = [ + {'name': 'Alice', 'age': 20, 'scores': [85, 92, 78, 96], 'major': 'CS'}, + {'name': 'Bob', 'age': 22, 'scores': [76, 88, 82, 79], 'major': 'Math'}, + {'name': 'Charlie', 'age': 19, 'scores': [95, 87, 91, 93], 'major': 'CS'}, + {'name': 'Diana', 'age': 21, 'scores': [68, 74, 82, 79], 'major': 'Physics'}, + {'name': 'Eve', 'age': 20, 'scores': [89, 94, 87, 92], 'major': 'CS'} +] + +# 纯函数定义 +def calculate_average(scores: List[int]) -> float: + """计算平均分""" + return sum(scores) / len(scores) + +def add_average_score(student: Dict[str, Any]) -> Dict[str, Any]: + """添加平均分字段""" + return { + **student, + 'average': calculate_average(student['scores']) + } + +def is_excellent_student(student: Dict[str, Any]) -> bool: + """判断是否为优秀学生(平均分>85)""" + return student['average'] > 85 + +def is_cs_major(student: Dict[str, Any]) -> bool: + """判断是否为CS专业""" + return student['major'] == 'CS' + +def get_student_summary(student: Dict[str, Any]) -> Dict[str, Any]: + """获取学生摘要信息""" + return { + 'name': student['name'], + 'average': round(student['average'], 2), + 'major': student['major'] + } + +# 函数式数据处理管道 +def process_students(students: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """处理学生数据的完整管道""" + return list( + map( + get_student_summary, + filter( + lambda s: is_excellent_student(s) and is_cs_major(s), + map(add_average_score, students) + ) + ) + ) + +# 执行处理 +excellent_cs_students = process_students(students_data) +print("优秀的CS专业学生:") +for student in excellent_cs_students: + print(f" {student['name']}: {student['average']} ({student['major']})") + +# 使用reduce计算统计信息 +def calculate_stats(students: List[Dict[str, Any]]) -> Dict[str, float]: + """计算统计信息""" + students_with_avg = list(map(add_average_score, students)) + + total_avg = reduce( + lambda acc, student: acc + student['average'], + students_with_avg, + 0 + ) / len(students_with_avg) + + max_avg = reduce( + lambda acc, student: max(acc, student['average']), + students_with_avg, + 0 + ) + + min_avg = reduce( + lambda acc, student: min(acc, student['average']), + students_with_avg, + float('inf') + ) + + return { + 'total_average': round(total_avg, 2), + 'max_average': round(max_avg, 2), + 'min_average': round(min_avg, 2) + } + +stats = calculate_stats(students_data) +print(f"\n统计信息:{stats}") +``` + +### 6.2 函数式配置管理 + +```python +from functools import partial +from typing import Callable, Any + +# 配置验证函数 +def validate_range(min_val: float, max_val: float, value: float) -> bool: + """验证值是否在范围内""" + return min_val <= value <= max_val + +def validate_type(expected_type: type, value: Any) -> bool: + """验证类型""" + return isinstance(value, expected_type) + +def validate_length(min_len: int, max_len: int, value: str) -> bool: + """验证字符串长度""" + return min_len <= len(value) <= max_len + +# 创建特定的验证器 +validate_port = partial(validate_range, 1, 65535) +validate_percentage = partial(validate_range, 0, 100) +validate_string = partial(validate_type, str) +validate_int = partial(validate_type, int) +validate_username = partial(validate_length, 3, 20) + +# 配置规则 +config_rules = { + 'server_port': [validate_int, validate_port], + 'cpu_threshold': [validate_int, validate_percentage], + 'username': [validate_string, validate_username], + 'timeout': [validate_int, partial(validate_range, 1, 300)] +} + +def validate_config(rules: Dict[str, List[Callable]], config: Dict[str, Any]) -> Dict[str, bool]: + """验证配置""" + results = {} + + for key, validators in rules.items(): + if key in config: + # 所有验证器都必须通过 + results[key] = all( + validator(config[key]) for validator in validators + ) + else: + results[key] = False + + return results + +# 测试配置 +test_config = { + 'server_port': 8080, + 'cpu_threshold': 85, + 'username': 'alice', + 'timeout': 30 +} + +validation_results = validate_config(config_rules, test_config) +print("配置验证结果:") +for key, is_valid in validation_results.items(): + status = "✓" if is_valid else "✗" + print(f" {key}: {status}") + +# 无效配置测试 +invalid_config = { + 'server_port': 70000, # 超出范围 + 'cpu_threshold': 150, # 超出百分比范围 + 'username': 'ab', # 太短 + 'timeout': 500 # 超时时间太长 +} + +invalid_results = validate_config(config_rules, invalid_config) +print("\n无效配置验证结果:") +for key, is_valid in invalid_results.items(): + status = "✓" if is_valid else "✗" + print(f" {key}: {status}") +``` + +### 6.3 函数式错误处理 + +```python +from typing import Union, Callable, Any +from dataclasses import dataclass + +# 定义Result类型(模拟Rust的Result) +@dataclass +class Success: + value: Any + + def is_success(self) -> bool: + return True + + def is_error(self) -> bool: + return False + + def map(self, func: Callable) -> 'Union[Success, Error]': + try: + return Success(func(self.value)) + except Exception as e: + return Error(str(e)) + + def flat_map(self, func: Callable) -> 'Union[Success, Error]': + try: + return func(self.value) + except Exception as e: + return Error(str(e)) + +@dataclass +class Error: + message: str + + def is_success(self) -> bool: + return False + + def is_error(self) -> bool: + return True + + def map(self, func: Callable) -> 'Error': + return self + + def flat_map(self, func: Callable) -> 'Error': + return self + +Result = Union[Success, Error] + +# 安全的数学操作 +def safe_divide(x: float, y: float) -> Result: + """安全除法""" + if y == 0: + return Error("除零错误") + return Success(x / y) + +def safe_sqrt(x: float) -> Result: + """安全开方""" + if x < 0: + return Error("负数不能开方") + return Success(x ** 0.5) + +def safe_log(x: float) -> Result: + """安全对数""" + if x <= 0: + return Error("对数的参数必须大于0") + import math + return Success(math.log(x)) + +# 函数式错误处理链 +def calculate_complex_formula(a: float, b: float, c: float) -> Result: + """计算复杂公式:log(sqrt(a/b) + c)""" + return (safe_divide(a, b) + .flat_map(lambda x: safe_sqrt(x)) + .map(lambda x: x + c) + .flat_map(lambda x: safe_log(x))) + +# 测试错误处理 +test_cases = [ + (16, 4, 1), # 正常情况 + (16, 0, 1), # 除零错误 + (-16, 4, 1), # 负数开方错误 + (16, 4, -3), # 对数参数错误 +] + +print("复杂公式计算结果:") +for a, b, c in test_cases: + result = calculate_complex_formula(a, b, c) + if result.is_success(): + print(f" f({a}, {b}, {c}) = {result.value:.4f}") + else: + print(f" f({a}, {b}, {c}) = 错误: {result.message}") + +# 批量处理 +def process_batch(data: List[tuple], processor: Callable) -> Dict[str, List]: + """批量处理数据""" + successes = [] + errors = [] + + for item in data: + result = processor(*item) + if result.is_success(): + successes.append((item, result.value)) + else: + errors.append((item, result.message)) + + return {'successes': successes, 'errors': errors} + +batch_results = process_batch(test_cases, calculate_complex_formula) +print(f"\n批量处理结果:") +print(f"成功: {len(batch_results['successes'])} 个") +print(f"失败: {len(batch_results['errors'])} 个") +``` + +--- + +## 七、总结 + +### 7.1 函数式编程的优势 + +1. **可预测性**:纯函数使代码行为可预测 +2. **可测试性**:纯函数易于单元测试 +3. **可组合性**:函数可以轻松组合成复杂操作 +4. **并发安全**:不可变数据避免了并发问题 +5. **代码简洁**:高阶函数减少重复代码 + +### 7.2 何时使用函数式编程 + +**适合的场景:** +- 数据转换和处理 +- 配置验证 +- 数学计算 +- 并发编程 +- 需要高可靠性的系统 + +**不太适合的场景:** +- 需要频繁修改状态的应用 +- 性能要求极高的场景 +- 简单的脚本任务 + +### 7.3 最佳实践 + +1. **优先使用纯函数**:避免副作用 +2. **保持函数简单**:一个函数只做一件事 +3. **使用类型注解**:提高代码可读性 +4. **合理使用高阶函数**:不要过度抽象 +5. **结合其他范式**:Python支持多种编程范式 + +### 7.4 下一步学习 + +- **深入学习itertools和functools** +- **了解更多函数式编程库**(如toolz) +- **学习其他函数式语言**(如Haskell、Clojure) +- **实践函数式设计模式** + +恭喜你!掌握了函数式编程,你的Python技能又上了一个新台阶!🎉 + +--- + +**练习建议:** +1. 重构现有代码,使用纯函数 +2. 练习使用map、filter、reduce处理数据 +3. 尝试函数组合和管道操作 +4. 实现自己的高阶函数 +5. 用函数式方法解决实际问题 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/8.md b/docs/Python/8.md new file mode 100644 index 000000000..3bede030c --- /dev/null +++ b/docs/Python/8.md @@ -0,0 +1,1553 @@ +--- +title: 第8天-模块 +author: 哪吒 +date: '2023-06-15' +--- + +# 第8天-Python模块与包 + +欢迎来到Python模块的世界!模块是Python代码组织和重用的基础,掌握模块系统将让你的代码更加模块化、可维护和可扩展。 + +## 什么是模块? + +模块(Module)是包含Python代码的文件,通常以`.py`为扩展名。模块可以包含函数、类、变量以及可执行的代码。 + +> **生活类比**: +> - **模块**:像工具箱中的不同工具,每个工具有特定的功能 +> - **包**:像工具箱本身,将相关的工具组织在一起 +> - **导入**:像从工具箱中取出需要的工具来使用 + +### 模块的优势 + +1. **代码重用**:避免重复编写相同的代码 +2. **命名空间**:避免变量名冲突 +3. **代码组织**:将相关功能组织在一起 +4. **维护性**:便于代码的维护和更新 +5. **协作开发**:团队成员可以独立开发不同模块 + +--- + +## 一、创建和使用模块 + +### 1.1 创建简单模块 + +让我们创建一个数学工具模块: + +```python +# 文件名:math_utils.py +"""数学工具模块 + +这个模块提供了一些常用的数学函数。 +""" + +import math + +# 模块级变量 +PI = 3.14159265359 +E = 2.71828182846 + +def add(x, y): + """加法函数""" + return x + y + +def subtract(x, y): + """减法函数""" + return x - y + +def multiply(x, y): + """乘法函数""" + return x * y + +def divide(x, y): + """除法函数""" + if y == 0: + raise ValueError("除数不能为零") + return x / y + +def power(base, exponent): + """幂运算""" + return base ** exponent + +def factorial(n): + """计算阶乘""" + if n < 0: + raise ValueError("阶乘的参数必须是非负整数") + if n == 0 or n == 1: + return 1 + return n * factorial(n - 1) + +def is_prime(n): + """判断是否为质数""" + if n < 2: + return False + for i in range(2, int(math.sqrt(n)) + 1): + if n % i == 0: + return False + return True + +def gcd(a, b): + """计算最大公约数""" + while b: + a, b = b, a % b + return a + +def lcm(a, b): + """计算最小公倍数""" + return abs(a * b) // gcd(a, b) + +class Calculator: + """简单计算器类""" + + def __init__(self): + self.history = [] + + def calculate(self, operation, x, y=None): + """执行计算并记录历史""" + if operation == 'add' and y is not None: + result = add(x, y) + elif operation == 'subtract' and y is not None: + result = subtract(x, y) + elif operation == 'multiply' and y is not None: + result = multiply(x, y) + elif operation == 'divide' and y is not None: + result = divide(x, y) + elif operation == 'factorial': + result = factorial(x) + else: + raise ValueError("不支持的操作") + + self.history.append(f"{operation}({x}, {y}) = {result}" if y is not None else f"{operation}({x}) = {result}") + return result + + def get_history(self): + """获取计算历史""" + return self.history.copy() + + def clear_history(self): + """清空计算历史""" + self.history.clear() + +# 模块级代码(导入时执行) +print(f"数学工具模块已加载,π = {PI}, e = {E}") + +# 测试代码(只在直接运行时执行) +if __name__ == "__main__": + print("测试数学工具模块:") + print(f"5 + 3 = {add(5, 3)}") + print(f"10 - 4 = {subtract(10, 4)}") + print(f"6 * 7 = {multiply(6, 7)}") + print(f"15 / 3 = {divide(15, 3)}") + print(f"2^8 = {power(2, 8)}") + print(f"5! = {factorial(5)}") + print(f"17是质数吗?{is_prime(17)}") + print(f"gcd(48, 18) = {gcd(48, 18)}") + print(f"lcm(12, 8) = {lcm(12, 8)}") + + calc = Calculator() + calc.calculate('add', 10, 5) + calc.calculate('factorial', 5) + print("计算历史:", calc.get_history()) +``` + +### 1.2 导入和使用模块 + +```python +# 使用我们创建的math_utils模块 + +# 方法1:导入整个模块 +import math_utils + +result1 = math_utils.add(10, 5) +print(f"10 + 5 = {result1}") +print(f"π的值:{math_utils.PI}") + +# 方法2:导入特定函数 +from math_utils import multiply, divide, factorial + +result2 = multiply(6, 7) +result3 = divide(20, 4) +result4 = factorial(5) +print(f"6 * 7 = {result2}") +print(f"20 / 4 = {result3}") +print(f"5! = {result4}") + +# 方法3:导入并重命名 +from math_utils import is_prime as check_prime +from math_utils import Calculator as Calc + +print(f"13是质数吗?{check_prime(13)}") +calc = Calc() +result5 = calc.calculate('add', 15, 25) +print(f"计算结果:{result5}") + +# 方法4:导入所有(不推荐) +# from math_utils import * +# 这种方式可能导致命名冲突,一般不推荐使用 + +# 方法5:使用别名导入模块 +import math_utils as mu + +result6 = mu.power(2, 10) +print(f"2^10 = {result6}") +``` + +--- + +## 二、模块搜索路径 + +### 2.1 Python如何查找模块 + +Python按以下顺序搜索模块: + +1. **当前目录** +2. **PYTHONPATH环境变量指定的目录** +3. **标准库目录** +4. **site-packages目录**(第三方包) + +```python +import sys + +# 查看模块搜索路径 +print("Python模块搜索路径:") +for i, path in enumerate(sys.path, 1): + print(f"{i}. {path}") + +# 动态添加搜索路径 +sys.path.append('/path/to/your/modules') + +# 查看已加载的模块 +print("\n已加载的模块:") +for module_name in sorted(sys.modules.keys())[:10]: # 只显示前10个 + print(f"- {module_name}") +``` + +### 2.2 模块的属性和信息 + +```python +import math_utils +import math + +# 查看模块属性 +print("math_utils模块的属性:") +for attr in dir(math_utils): + if not attr.startswith('_'): # 不显示私有属性 + print(f"- {attr}") + +# 模块的特殊属性 +print(f"\n模块名称:{math_utils.__name__}") +print(f"模块文档:{math_utils.__doc__}") +print(f"模块文件路径:{math_utils.__file__}") + +# 获取函数的帮助信息 +help(math_utils.factorial) + +# 检查对象类型 +print(f"\nadd是函数吗?{callable(math_utils.add)}") +print(f"PI是什么类型?{type(math_utils.PI)}") +print(f"Calculator是类吗?{isinstance(math_utils.Calculator, type)}") +``` + +--- + +## 三、包(Packages) + +### 3.1 什么是包 + +包是包含多个模块的目录,必须包含一个`__init__.py`文件(可以为空)。 + +``` +my_package/ + __init__.py + module1.py + module2.py + subpackage/ + __init__.py + module3.py + module4.py +``` + +### 3.2 创建包结构 + +让我们创建一个完整的工具包: + +```python +# 目录结构: +# utils/ +# __init__.py +# string_utils.py +# file_utils.py +# data_utils.py +# math/ +# __init__.py +# basic.py +# advanced.py + +# utils/__init__.py +"""工具包 + +这个包提供了各种实用工具函数。 +""" + +__version__ = "1.0.0" +__author__ = "哪吒" + +# 导入子模块,使其可以直接从包中访问 +from .string_utils import * +from .file_utils import read_file, write_file +from .data_utils import DataProcessor + +# 定义包级别的函数 +def get_package_info(): + """获取包信息""" + return { + 'name': __name__, + 'version': __version__, + 'author': __author__ + } + +# 定义__all__来控制from utils import *的行为 +__all__ = [ + 'get_package_info', + 'capitalize_words', + 'reverse_string', + 'read_file', + 'write_file', + 'DataProcessor' +] +``` + +```python +# utils/string_utils.py +"""字符串工具模块""" + +def capitalize_words(text): + """将每个单词的首字母大写""" + return ' '.join(word.capitalize() for word in text.split()) + +def reverse_string(text): + """反转字符串""" + return text[::-1] + +def count_words(text): + """统计单词数量""" + return len(text.split()) + +def remove_duplicates(text): + """移除重复的单词""" + words = text.split() + unique_words = [] + for word in words: + if word not in unique_words: + unique_words.append(word) + return ' '.join(unique_words) + +def is_palindrome(text): + """检查是否为回文""" + cleaned = ''.join(char.lower() for char in text if char.isalnum()) + return cleaned == cleaned[::-1] + +class StringProcessor: + """字符串处理器类""" + + def __init__(self, text): + self.text = text + + def process(self, operations): + """应用一系列操作""" + result = self.text + for operation in operations: + if operation == 'capitalize': + result = capitalize_words(result) + elif operation == 'reverse': + result = reverse_string(result) + elif operation == 'remove_duplicates': + result = remove_duplicates(result) + return result +``` + +```python +# utils/file_utils.py +"""文件操作工具模块""" + +import os +import json +import csv +from typing import List, Dict, Any + +def read_file(filepath, encoding='utf-8'): + """读取文本文件""" + try: + with open(filepath, 'r', encoding=encoding) as file: + return file.read() + except FileNotFoundError: + print(f"文件 {filepath} 不存在") + return None + except Exception as e: + print(f"读取文件时出错:{e}") + return None + +def write_file(filepath, content, encoding='utf-8'): + """写入文本文件""" + try: + # 确保目录存在 + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w', encoding=encoding) as file: + file.write(content) + return True + except Exception as e: + print(f"写入文件时出错:{e}") + return False + +def read_json(filepath): + """读取JSON文件""" + try: + with open(filepath, 'r', encoding='utf-8') as file: + return json.load(file) + except Exception as e: + print(f"读取JSON文件时出错:{e}") + return None + +def write_json(filepath, data, indent=2): + """写入JSON文件""" + try: + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w', encoding='utf-8') as file: + json.dump(data, file, indent=indent, ensure_ascii=False) + return True + except Exception as e: + print(f"写入JSON文件时出错:{e}") + return False + +def read_csv(filepath): + """读取CSV文件""" + try: + with open(filepath, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + return list(reader) + except Exception as e: + print(f"读取CSV文件时出错:{e}") + return None + +def write_csv(filepath, data, fieldnames=None): + """写入CSV文件""" + try: + if not data: + return False + + if fieldnames is None: + fieldnames = data[0].keys() if isinstance(data[0], dict) else None + + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w', newline='', encoding='utf-8') as file: + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(data) + return True + except Exception as e: + print(f"写入CSV文件时出错:{e}") + return False + +def get_file_info(filepath): + """获取文件信息""" + try: + stat = os.stat(filepath) + return { + 'size': stat.st_size, + 'modified': stat.st_mtime, + 'created': stat.st_ctime, + 'is_file': os.path.isfile(filepath), + 'is_dir': os.path.isdir(filepath) + } + except Exception as e: + print(f"获取文件信息时出错:{e}") + return None + +class FileManager: + """文件管理器类""" + + def __init__(self, base_path='.'): + self.base_path = base_path + + def list_files(self, extension=None): + """列出目录中的文件""" + files = [] + for item in os.listdir(self.base_path): + item_path = os.path.join(self.base_path, item) + if os.path.isfile(item_path): + if extension is None or item.endswith(extension): + files.append(item) + return files + + def create_backup(self, filepath): + """创建文件备份""" + backup_path = filepath + '.backup' + try: + content = read_file(filepath) + if content is not None: + return write_file(backup_path, content) + except Exception as e: + print(f"创建备份时出错:{e}") + return False +``` + +```python +# utils/data_utils.py +"""数据处理工具模块""" + +from typing import List, Dict, Any, Union +from collections import Counter +import statistics + +class DataProcessor: + """数据处理器类""" + + def __init__(self, data: List[Any] = None): + self.data = data or [] + + def add_data(self, item: Any): + """添加数据项""" + self.data.append(item) + + def filter_data(self, condition): + """过滤数据""" + return [item for item in self.data if condition(item)] + + def map_data(self, transform): + """转换数据""" + return [transform(item) for item in self.data] + + def group_by(self, key_func): + """按条件分组""" + groups = {} + for item in self.data: + key = key_func(item) + if key not in groups: + groups[key] = [] + groups[key].append(item) + return groups + + def get_statistics(self): + """获取数值数据的统计信息""" + if not self.data or not all(isinstance(x, (int, float)) for x in self.data): + return None + + return { + 'count': len(self.data), + 'sum': sum(self.data), + 'mean': statistics.mean(self.data), + 'median': statistics.median(self.data), + 'mode': statistics.mode(self.data) if len(set(self.data)) < len(self.data) else None, + 'min': min(self.data), + 'max': max(self.data), + 'range': max(self.data) - min(self.data) + } + +def clean_data(data: List[Dict[str, Any]], required_fields: List[str]) -> List[Dict[str, Any]]: + """清理数据,移除缺少必需字段的记录""" + cleaned = [] + for record in data: + if all(field in record and record[field] is not None for field in required_fields): + cleaned.append(record) + return cleaned + +def aggregate_data(data: List[Dict[str, Any]], group_by: str, aggregate_field: str, operation: str = 'sum'): + """聚合数据""" + groups = {} + + for record in data: + key = record.get(group_by) + if key not in groups: + groups[key] = [] + groups[key].append(record.get(aggregate_field, 0)) + + result = {} + for key, values in groups.items(): + if operation == 'sum': + result[key] = sum(values) + elif operation == 'avg': + result[key] = sum(values) / len(values) if values else 0 + elif operation == 'count': + result[key] = len(values) + elif operation == 'max': + result[key] = max(values) if values else 0 + elif operation == 'min': + result[key] = min(values) if values else 0 + + return result + +def find_duplicates(data: List[Any]) -> List[Any]: + """查找重复项""" + counter = Counter(data) + return [item for item, count in counter.items() if count > 1] + +def remove_duplicates(data: List[Any]) -> List[Any]: + """移除重复项,保持顺序""" + seen = set() + result = [] + for item in data: + if item not in seen: + seen.add(item) + result.append(item) + return result + +def sort_data(data: List[Dict[str, Any]], sort_by: str, reverse: bool = False) -> List[Dict[str, Any]]: + """排序数据""" + return sorted(data, key=lambda x: x.get(sort_by, 0), reverse=reverse) +``` + +```python +# utils/math/__init__.py +"""数学工具子包""" + +from .basic import * +from .advanced import * + +__all__ = ['add', 'subtract', 'multiply', 'divide', 'power', 'sqrt', 'log', 'sin', 'cos'] +``` + +```python +# utils/math/basic.py +"""基础数学运算""" + +def add(x, y): + """加法""" + return x + y + +def subtract(x, y): + """减法""" + return x - y + +def multiply(x, y): + """乘法""" + return x * y + +def divide(x, y): + """除法""" + if y == 0: + raise ValueError("除数不能为零") + return x / y + +def power(base, exponent): + """幂运算""" + return base ** exponent +``` + +```python +# utils/math/advanced.py +"""高级数学运算""" + +import math + +def sqrt(x): + """平方根""" + if x < 0: + raise ValueError("负数不能开平方根") + return math.sqrt(x) + +def log(x, base=math.e): + """对数""" + if x <= 0: + raise ValueError("对数的参数必须大于0") + return math.log(x, base) + +def sin(x): + """正弦""" + return math.sin(x) + +def cos(x): + """余弦""" + return math.cos(x) + +def tan(x): + """正切""" + return math.tan(x) +``` + +### 3.3 使用包 + +```python +# 使用我们创建的工具包 + +# 导入整个包 +import utils + +# 使用包级别的函数 +info = utils.get_package_info() +print(f"包信息:{info}") + +# 使用字符串工具 +text = "hello world python programming" +result = utils.capitalize_words(text) +print(f"首字母大写:{result}") + +# 使用文件工具 +utils.write_file('test.txt', 'Hello, World!') +content = utils.read_file('test.txt') +print(f"文件内容:{content}") + +# 使用数据处理器 +processor = utils.DataProcessor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +even_numbers = processor.filter_data(lambda x: x % 2 == 0) +print(f"偶数:{even_numbers}") + +stats = processor.get_statistics() +print(f"统计信息:{stats}") + +# 导入子包 +from utils.math import add, multiply, sqrt + +result1 = add(10, 5) +result2 = multiply(6, 7) +result3 = sqrt(16) +print(f"数学运算:{result1}, {result2}, {result3}") + +# 使用别名导入 +from utils.string_utils import StringProcessor as SP + +processor = SP("hello world hello python") +result = processor.process(['capitalize', 'remove_duplicates']) +print(f"字符串处理结果:{result}") +``` + +--- + +## 四、标准库模块 + +### 4.1 常用标准库模块 + +```python +# os模块 - 操作系统接口 +import os + +print(f"当前工作目录:{os.getcwd()}") +print(f"用户主目录:{os.path.expanduser('~')}") +print(f"路径分隔符:{os.sep}") + +# 环境变量 +print(f"PATH环境变量:{os.environ.get('PATH', '未设置')}") + +# 文件和目录操作 +if not os.path.exists('temp_dir'): + os.makedirs('temp_dir') + print("创建了临时目录") + +# sys模块 - 系统相关参数和函数 +import sys + +print(f"Python版本:{sys.version}") +print(f"平台:{sys.platform}") +print(f"命令行参数:{sys.argv}") + +# datetime模块 - 日期和时间 +from datetime import datetime, date, time, timedelta + +now = datetime.now() +print(f"当前时间:{now}") +print(f"格式化时间:{now.strftime('%Y-%m-%d %H:%M:%S')}") + +# 时间计算 +tomorrow = now + timedelta(days=1) +print(f"明天:{tomorrow.strftime('%Y-%m-%d')}") + +# random模块 - 随机数生成 +import random + +print(f"随机整数:{random.randint(1, 100)}") +print(f"随机浮点数:{random.random()}") +print(f"随机选择:{random.choice(['apple', 'banana', 'orange'])}") + +# 随机打乱列表 +numbers = [1, 2, 3, 4, 5] +random.shuffle(numbers) +print(f"打乱后的列表:{numbers}") + +# json模块 - JSON数据处理 +import json + +data = { + 'name': '张三', + 'age': 25, + 'skills': ['Python', 'JavaScript', 'SQL'] +} + +# 转换为JSON字符串 +json_str = json.dumps(data, ensure_ascii=False, indent=2) +print(f"JSON字符串:\n{json_str}") + +# 从JSON字符串解析 +parsed_data = json.loads(json_str) +print(f"解析后的数据:{parsed_data}") +``` + +### 4.2 更多实用标准库 + +```python +# collections模块 - 特殊容器数据类型 +from collections import Counter, defaultdict, namedtuple, deque + +# Counter - 计数器 +text = "hello world" +letter_count = Counter(text) +print(f"字母计数:{letter_count}") +print(f"最常见的3个字母:{letter_count.most_common(3)}") + +# defaultdict - 默认字典 +dd = defaultdict(list) +dd['fruits'].append('apple') +dd['fruits'].append('banana') +dd['vegetables'].append('carrot') +print(f"默认字典:{dict(dd)}") + +# namedtuple - 命名元组 +Point = namedtuple('Point', ['x', 'y']) +p = Point(3, 4) +print(f"点坐标:{p}, x={p.x}, y={p.y}") + +# deque - 双端队列 +dq = deque([1, 2, 3]) +dq.appendleft(0) +dq.append(4) +print(f"双端队列:{list(dq)}") + +# itertools模块 - 迭代工具 +import itertools + +# 组合和排列 +data = ['A', 'B', 'C'] +combinations = list(itertools.combinations(data, 2)) +permutations = list(itertools.permutations(data, 2)) +print(f"组合:{combinations}") +print(f"排列:{permutations}") + +# 无限迭代器 +counter = itertools.count(1, 2) # 从1开始,步长为2 +first_5_odd = [next(counter) for _ in range(5)] +print(f"前5个奇数:{first_5_odd}") + +# re模块 - 正则表达式 +import re + +text = "联系电话:138-1234-5678,邮箱:user@example.com" + +# 查找电话号码 +phone_pattern = r'\d{3}-\d{4}-\d{4}' +phone = re.search(phone_pattern, text) +if phone: + print(f"找到电话号码:{phone.group()}") + +# 查找邮箱 +email_pattern = r'\w+@\w+\.\w+' +email = re.search(email_pattern, text) +if email: + print(f"找到邮箱:{email.group()}") + +# 替换文本 +cleaned_text = re.sub(r'\d{3}-\d{4}-\d{4}', '[电话已隐藏]', text) +print(f"清理后的文本:{cleaned_text}") + +# pathlib模块 - 面向对象的路径操作 +from pathlib import Path + +# 创建路径对象 +path = Path('data/files/document.txt') +print(f"文件名:{path.name}") +print(f"文件扩展名:{path.suffix}") +print(f"父目录:{path.parent}") +print(f"绝对路径:{path.absolute()}") + +# 路径操作 +new_path = path.with_suffix('.pdf') +print(f"更改扩展名后:{new_path}") + +# 检查路径 +print(f"路径存在吗?{path.exists()}") +print(f"是文件吗?{path.is_file()}") +print(f"是目录吗?{path.is_dir()}") +``` + +--- + +## 五、第三方包管理 + +### 5.1 pip包管理器 + +```bash +# 安装包 +pip install requests +pip install pandas numpy matplotlib + +# 安装特定版本 +pip install django==3.2.0 + +# 从requirements.txt安装 +pip install -r requirements.txt + +# 升级包 +pip upgrade requests + +# 卸载包 +pip uninstall requests + +# 列出已安装的包 +pip list + +# 显示包信息 +pip show requests + +# 生成requirements.txt +pip freeze > requirements.txt +``` + +### 5.2 虚拟环境 + +```bash +# 创建虚拟环境 +python -m venv myenv + +# 激活虚拟环境 +# Windows +myenv\Scripts\activate +# macOS/Linux +source myenv/bin/activate + +# 停用虚拟环境 +deactivate + +# 删除虚拟环境 +rmdir /s myenv # Windows +rm -rf myenv # macOS/Linux +``` + +### 5.3 常用第三方包示例 + +```python +# requests - HTTP库 +import requests + +response = requests.get('https://api.github.com/users/octocat') +if response.status_code == 200: + data = response.json() + print(f"用户名:{data['login']}") + print(f"公开仓库数:{data['public_repos']}") + +# pandas - 数据分析 +import pandas as pd + +# 创建DataFrame +data = { + 'name': ['Alice', 'Bob', 'Charlie'], + 'age': [25, 30, 35], + 'city': ['北京', '上海', '广州'] +} +df = pd.DataFrame(data) +print(df) + +# 数据操作 +print(f"平均年龄:{df['age'].mean()}") +print(f"年龄大于25的人:\n{df[df['age'] > 25]}") + +# matplotlib - 绘图 +import matplotlib.pyplot as plt +import numpy as np + +x = np.linspace(0, 10, 100) +y = np.sin(x) + +plt.figure(figsize=(10, 6)) +plt.plot(x, y, label='sin(x)') +plt.xlabel('x') +plt.ylabel('y') +plt.title('正弦函数图像') +plt.legend() +plt.grid(True) +plt.show() +``` + +--- + +## 六、模块最佳实践 + +### 6.1 模块设计原则 + +```python +# 好的模块设计示例 +"""用户管理模块 + +这个模块提供用户注册、登录、权限管理等功能。 + +示例用法: + from user_manager import User, authenticate + + user = User('alice', 'alice@example.com') + user.set_password('secret123') + + if authenticate('alice', 'secret123'): + print('登录成功') +""" + +import hashlib +import json +from datetime import datetime +from typing import Optional, Dict, List + +# 模块级常量 +DEFAULT_ROLE = 'user' +ADMIN_ROLE = 'admin' +MAX_LOGIN_ATTEMPTS = 3 + +# 私有函数(以下划线开头) +def _hash_password(password: str) -> str: + """私有函数:密码哈希""" + return hashlib.sha256(password.encode()).hexdigest() + +def _validate_email(email: str) -> bool: + """私有函数:邮箱验证""" + import re + pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$' + return re.match(pattern, email) is not None + +class User: + """用户类""" + + def __init__(self, username: str, email: str, role: str = DEFAULT_ROLE): + if not username or not email: + raise ValueError("用户名和邮箱不能为空") + + if not _validate_email(email): + raise ValueError("邮箱格式不正确") + + self.username = username + self.email = email + self.role = role + self.password_hash = None + self.created_at = datetime.now() + self.last_login = None + self.login_attempts = 0 + self.is_active = True + + def set_password(self, password: str) -> None: + """设置密码""" + if len(password) < 6: + raise ValueError("密码长度至少6位") + self.password_hash = _hash_password(password) + + def check_password(self, password: str) -> bool: + """检查密码""" + if not self.password_hash: + return False + return self.password_hash == _hash_password(password) + + def to_dict(self) -> Dict: + """转换为字典""" + return { + 'username': self.username, + 'email': self.email, + 'role': self.role, + 'created_at': self.created_at.isoformat(), + 'last_login': self.last_login.isoformat() if self.last_login else None, + 'is_active': self.is_active + } + + def __str__(self): + return f"User(username='{self.username}', email='{self.email}', role='{self.role}')" + + def __repr__(self): + return self.__str__() + +class UserManager: + """用户管理器""" + + def __init__(self): + self.users: Dict[str, User] = {} + + def register(self, username: str, email: str, password: str, role: str = DEFAULT_ROLE) -> User: + """注册用户""" + if username in self.users: + raise ValueError(f"用户名 '{username}' 已存在") + + user = User(username, email, role) + user.set_password(password) + self.users[username] = user + return user + + def authenticate(self, username: str, password: str) -> Optional[User]: + """用户认证""" + user = self.users.get(username) + if not user or not user.is_active: + return None + + if user.login_attempts >= MAX_LOGIN_ATTEMPTS: + raise ValueError("登录尝试次数过多,账户已锁定") + + if user.check_password(password): + user.last_login = datetime.now() + user.login_attempts = 0 + return user + else: + user.login_attempts += 1 + return None + + def get_user(self, username: str) -> Optional[User]: + """获取用户""" + return self.users.get(username) + + def list_users(self, role: Optional[str] = None) -> List[User]: + """列出用户""" + if role: + return [user for user in self.users.values() if user.role == role] + return list(self.users.values()) + + def export_users(self, filepath: str) -> bool: + """导出用户数据""" + try: + data = [user.to_dict() for user in self.users.values()] + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + print(f"导出用户数据失败:{e}") + return False + +# 模块级实例(单例模式) +_user_manager = UserManager() + +# 公共API函数 +def register_user(username: str, email: str, password: str, role: str = DEFAULT_ROLE) -> User: + """注册用户(模块级函数)""" + return _user_manager.register(username, email, password, role) + +def authenticate(username: str, password: str) -> Optional[User]: + """用户认证(模块级函数)""" + return _user_manager.authenticate(username, password) + +def get_user(username: str) -> Optional[User]: + """获取用户(模块级函数)""" + return _user_manager.get_user(username) + +def list_users(role: Optional[str] = None) -> List[User]: + """列出用户(模块级函数)""" + return _user_manager.list_users(role) + +# 定义模块的公共接口 +__all__ = [ + 'User', + 'UserManager', + 'register_user', + 'authenticate', + 'get_user', + 'list_users', + 'DEFAULT_ROLE', + 'ADMIN_ROLE' +] + +# 模块测试代码 +if __name__ == "__main__": + # 测试用户管理功能 + print("测试用户管理模块:") + + # 注册用户 + alice = register_user('alice', 'alice@example.com', 'password123') + bob = register_user('bob', 'bob@example.com', 'secret456', ADMIN_ROLE) + + print(f"注册用户:{alice}") + print(f"注册管理员:{bob}") + + # 用户认证 + auth_user = authenticate('alice', 'password123') + if auth_user: + print(f"认证成功:{auth_user.username}") + + # 列出用户 + all_users = list_users() + print(f"所有用户:{[user.username for user in all_users]}") + + admin_users = list_users(ADMIN_ROLE) + print(f"管理员用户:{[user.username for user in admin_users]}") +``` + +### 6.2 模块文档和测试 + +```python +# 文档字符串最佳实践 +def calculate_distance(point1, point2): + """计算两点之间的欧几里得距离。 + + Args: + point1 (tuple): 第一个点的坐标 (x, y) + point2 (tuple): 第二个点的坐标 (x, y) + + Returns: + float: 两点之间的距离 + + Raises: + ValueError: 当输入不是有效坐标时 + TypeError: 当输入类型不正确时 + + Examples: + >>> calculate_distance((0, 0), (3, 4)) + 5.0 + >>> calculate_distance((1, 1), (4, 5)) + 5.0 + """ + try: + x1, y1 = point1 + x2, y2 = point2 + return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + except (ValueError, TypeError) as e: + raise ValueError(f"无效的坐标输入: {e}") + +# 单元测试示例 +import unittest + +class TestMathUtils(unittest.TestCase): + """数学工具模块测试""" + + def test_calculate_distance(self): + """测试距离计算""" + # 测试正常情况 + self.assertEqual(calculate_distance((0, 0), (3, 4)), 5.0) + self.assertEqual(calculate_distance((1, 1), (1, 1)), 0.0) + + # 测试异常情况 + with self.assertRaises(ValueError): + calculate_distance((1, 2), (3,)) # 坐标不完整 + + with self.assertRaises(ValueError): + calculate_distance("invalid", (1, 2)) # 无效类型 + +if __name__ == "__main__": + unittest.main() +``` + +--- + +## 七、实战项目:个人任务管理系统 + +让我们创建一个完整的任务管理系统来实践模块化编程: + +```python +# task_manager/ +# __init__.py +# models.py +# storage.py +# cli.py +# utils.py + +# task_manager/__init__.py +"""个人任务管理系统 + +一个简单而强大的任务管理工具。 +""" + +__version__ = "1.0.0" +__author__ = "哪吒" + +from .models import Task, TaskManager +from .storage import FileStorage, JSONStorage +from .utils import format_date, get_priority_color + +__all__ = ['Task', 'TaskManager', 'FileStorage', 'JSONStorage', 'format_date', 'get_priority_color'] +``` + +```python +# task_manager/models.py +"""任务模型""" + +from datetime import datetime, date +from typing import List, Optional, Dict, Any +from enum import Enum + +class Priority(Enum): + """任务优先级""" + LOW = 1 + MEDIUM = 2 + HIGH = 3 + URGENT = 4 + +class Status(Enum): + """任务状态""" + TODO = "待办" + IN_PROGRESS = "进行中" + COMPLETED = "已完成" + CANCELLED = "已取消" + +class Task: + """任务类""" + + def __init__(self, title: str, description: str = "", priority: Priority = Priority.MEDIUM, + due_date: Optional[date] = None, tags: Optional[List[str]] = None): + self.id = self._generate_id() + self.title = title + self.description = description + self.priority = priority + self.status = Status.TODO + self.created_at = datetime.now() + self.updated_at = datetime.now() + self.due_date = due_date + self.completed_at = None + self.tags = tags or [] + + def _generate_id(self) -> str: + """生成唯一ID""" + import uuid + return str(uuid.uuid4())[:8] + + def mark_completed(self): + """标记为已完成""" + self.status = Status.COMPLETED + self.completed_at = datetime.now() + self.updated_at = datetime.now() + + def mark_in_progress(self): + """标记为进行中""" + self.status = Status.IN_PROGRESS + self.updated_at = datetime.now() + + def mark_cancelled(self): + """标记为已取消""" + self.status = Status.CANCELLED + self.updated_at = datetime.now() + + def update(self, title: Optional[str] = None, description: Optional[str] = None, + priority: Optional[Priority] = None, due_date: Optional[date] = None, + tags: Optional[List[str]] = None): + """更新任务信息""" + if title is not None: + self.title = title + if description is not None: + self.description = description + if priority is not None: + self.priority = priority + if due_date is not None: + self.due_date = due_date + if tags is not None: + self.tags = tags + self.updated_at = datetime.now() + + def add_tag(self, tag: str): + """添加标签""" + if tag not in self.tags: + self.tags.append(tag) + self.updated_at = datetime.now() + + def remove_tag(self, tag: str): + """移除标签""" + if tag in self.tags: + self.tags.remove(tag) + self.updated_at = datetime.now() + + def is_overdue(self) -> bool: + """检查是否过期""" + if self.due_date and self.status not in [Status.COMPLETED, Status.CANCELLED]: + return date.today() > self.due_date + return False + + def days_until_due(self) -> Optional[int]: + """距离截止日期的天数""" + if self.due_date: + delta = self.due_date - date.today() + return delta.days + return None + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'id': self.id, + 'title': self.title, + 'description': self.description, + 'priority': self.priority.value, + 'status': self.status.value, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'due_date': self.due_date.isoformat() if self.due_date else None, + 'completed_at': self.completed_at.isoformat() if self.completed_at else None, + 'tags': self.tags + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Task': + """从字典创建任务""" + task = cls.__new__(cls) + task.id = data['id'] + task.title = data['title'] + task.description = data['description'] + task.priority = Priority(data['priority']) + task.status = Status(data['status']) + task.created_at = datetime.fromisoformat(data['created_at']) + task.updated_at = datetime.fromisoformat(data['updated_at']) + task.due_date = date.fromisoformat(data['due_date']) if data['due_date'] else None + task.completed_at = datetime.fromisoformat(data['completed_at']) if data['completed_at'] else None + task.tags = data['tags'] + return task + + def __str__(self): + status_icon = { + Status.TODO: "⭕", + Status.IN_PROGRESS: "🔄", + Status.COMPLETED: "✅", + Status.CANCELLED: "❌" + } + priority_icon = { + Priority.LOW: "🟢", + Priority.MEDIUM: "🟡", + Priority.HIGH: "🟠", + Priority.URGENT: "🔴" + } + + due_info = f" (截止: {self.due_date})" if self.due_date else "" + tags_info = f" #{' #'.join(self.tags)}" if self.tags else "" + + return f"{status_icon[self.status]} {priority_icon[self.priority]} {self.title}{due_info}{tags_info}" + +class TaskManager: + """任务管理器""" + + def __init__(self): + self.tasks: Dict[str, Task] = {} + + def add_task(self, title: str, description: str = "", priority: Priority = Priority.MEDIUM, + due_date: Optional[date] = None, tags: Optional[List[str]] = None) -> Task: + """添加任务""" + task = Task(title, description, priority, due_date, tags) + self.tasks[task.id] = task + return task + + def get_task(self, task_id: str) -> Optional[Task]: + """获取任务""" + return self.tasks.get(task_id) + + def update_task(self, task_id: str, **kwargs) -> bool: + """更新任务""" + task = self.get_task(task_id) + if task: + task.update(**kwargs) + return True + return False + + def delete_task(self, task_id: str) -> bool: + """删除任务""" + if task_id in self.tasks: + del self.tasks[task_id] + return True + return False + + def list_tasks(self, status: Optional[Status] = None, priority: Optional[Priority] = None, + tag: Optional[str] = None, overdue_only: bool = False) -> List[Task]: + """列出任务""" + tasks = list(self.tasks.values()) + + if status: + tasks = [t for t in tasks if t.status == status] + + if priority: + tasks = [t for t in tasks if t.priority == priority] + + if tag: + tasks = [t for t in tasks if tag in t.tags] + + if overdue_only: + tasks = [t for t in tasks if t.is_overdue()] + + return sorted(tasks, key=lambda t: (t.priority.value, t.created_at), reverse=True) + + def search_tasks(self, keyword: str) -> List[Task]: + """搜索任务""" + keyword = keyword.lower() + return [task for task in self.tasks.values() + if keyword in task.title.lower() or keyword in task.description.lower()] + + def get_statistics(self) -> Dict[str, Any]: + """获取统计信息""" + total = len(self.tasks) + completed = len([t for t in self.tasks.values() if t.status == Status.COMPLETED]) + in_progress = len([t for t in self.tasks.values() if t.status == Status.IN_PROGRESS]) + overdue = len([t for t in self.tasks.values() if t.is_overdue()]) + + return { + 'total': total, + 'completed': completed, + 'in_progress': in_progress, + 'todo': total - completed - in_progress, + 'overdue': overdue, + 'completion_rate': (completed / total * 100) if total > 0 else 0 + } + + def get_tasks_by_date(self, target_date: date) -> List[Task]: + """获取指定日期的任务""" + return [task for task in self.tasks.values() if task.due_date == target_date] + + def get_upcoming_tasks(self, days: int = 7) -> List[Task]: + """获取即将到期的任务""" + upcoming = [] + for task in self.tasks.values(): + if task.due_date and task.status not in [Status.COMPLETED, Status.CANCELLED]: + days_until = task.days_until_due() + if days_until is not None and 0 <= days_until <= days: + upcoming.append(task) + return sorted(upcoming, key=lambda t: t.due_date) +``` + +--- + +## 八、总结 + +### 8.1 模块系统的核心概念 + +1. **模块**:包含Python代码的文件 +2. **包**:包含多个模块的目录 +3. **导入**:使用其他模块的功能 +4. **命名空间**:避免名称冲突 +5. **搜索路径**:Python查找模块的位置 + +### 8.2 最佳实践总结 + +1. **模块设计**: + - 单一职责原则 + - 清晰的接口设计 + - 完善的文档字符串 + - 合理的错误处理 + +2. **包组织**: + - 逻辑清晰的目录结构 + - 适当的`__init__.py`文件 + - 明确的`__all__`定义 + +3. **导入规范**: + - 优先使用具体导入 + - 避免循环导入 + - 合理使用别名 + +4. **代码质量**: + - 编写单元测试 + - 使用类型注解 + - 遵循PEP 8规范 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/9.md b/docs/Python/9.md new file mode 100644 index 000000000..d24af54a2 --- /dev/null +++ b/docs/Python/9.md @@ -0,0 +1,1979 @@ +--- +title: 第9天-面向对象编程 +author: 哪吒 +date: '2023-06-15' +--- + +# 第9天-Python面向对象编程 + +欢迎来到Python面向对象编程的世界!面向对象编程(Object-Oriented Programming,OOP)是一种强大的编程范式,它让我们能够创建更加模块化、可重用和易维护的代码。 + +## 什么是面向对象编程? + +面向对象编程是一种编程范式,它将数据和操作数据的方法组织在一起,形成"对象"。这种方式更接近我们对现实世界的理解。 + +> **生活类比**: +> - **类(Class)**:像汽车的设计图纸,定义了汽车应该有什么属性和功能 +> - **对象(Object)**:像根据图纸制造出的具体汽车 +> - **属性(Attribute)**:像汽车的颜色、品牌、型号 +> - **方法(Method)**:像汽车的启动、加速、刹车功能 + +### OOP的四大特性 + +1. **封装(Encapsulation)**:将数据和方法包装在一起,隐藏内部实现细节 +2. **继承(Inheritance)**:子类可以继承父类的属性和方法 +3. **多态(Polymorphism)**:同一个方法在不同类中有不同的实现 +4. **抽象(Abstraction)**:隐藏复杂的实现细节,只暴露必要的接口 + +--- + +## 一、类和对象基础 + +### 1.1 定义类 + +```python +# 定义一个简单的学生类 +class Student: + """学生类 + + 这个类用来表示一个学生,包含学生的基本信息和行为。 + """ + + # 类变量(所有实例共享) + school = "Python编程学院" + student_count = 0 + + def __init__(self, name, age, student_id): + """构造方法(初始化方法) + + Args: + name (str): 学生姓名 + age (int): 学生年龄 + student_id (str): 学号 + """ + # 实例变量(每个实例独有) + self.name = name + self.age = age + self.student_id = student_id + self.grades = {} # 存储各科成绩 + self.is_enrolled = True + + # 每创建一个学生,计数器加1 + Student.student_count += 1 + + def introduce(self): + """自我介绍方法""" + return f"大家好,我是{self.name},今年{self.age}岁,学号是{self.student_id}" + + def add_grade(self, subject, score): + """添加成绩 + + Args: + subject (str): 科目名称 + score (float): 成绩分数 + """ + if 0 <= score <= 100: + self.grades[subject] = score + print(f"已为{self.name}添加{subject}成绩:{score}分") + else: + print("成绩必须在0-100之间") + + def get_average_grade(self): + """计算平均成绩 + + Returns: + float: 平均成绩,如果没有成绩则返回0 + """ + if not self.grades: + return 0 + return sum(self.grades.values()) / len(self.grades) + + def get_grade_level(self): + """根据平均成绩获取等级 + + Returns: + str: 成绩等级 + """ + avg = self.get_average_grade() + if avg >= 90: + return "优秀" + elif avg >= 80: + return "良好" + elif avg >= 70: + return "中等" + elif avg >= 60: + return "及格" + else: + return "不及格" + + def enroll(self): + """注册入学""" + self.is_enrolled = True + print(f"{self.name}已成功注册入学") + + def withdraw(self): + """退学""" + self.is_enrolled = False + print(f"{self.name}已办理退学手续") + + @classmethod + def get_student_count(cls): + """类方法:获取学生总数 + + Returns: + int: 当前学生总数 + """ + return cls.student_count + + @classmethod + def create_from_string(cls, student_info): + """类方法:从字符串创建学生对象 + + Args: + student_info (str): 格式为 "姓名,年龄,学号" 的字符串 + + Returns: + Student: 新创建的学生对象 + """ + name, age, student_id = student_info.split(',') + return cls(name.strip(), int(age.strip()), student_id.strip()) + + @staticmethod + def is_valid_student_id(student_id): + """静态方法:验证学号格式 + + Args: + student_id (str): 学号 + + Returns: + bool: 学号是否有效 + """ + # 假设学号格式为:年份 + 4位数字 + return len(student_id) == 8 and student_id.isdigit() + + def __str__(self): + """字符串表示方法""" + status = "在读" if self.is_enrolled else "已退学" + return f"学生({self.name}, {self.age}岁, {self.student_id}, {status})" + + def __repr__(self): + """开发者友好的字符串表示""" + return f"Student('{self.name}', {self.age}, '{self.student_id}')" + + def __eq__(self, other): + """相等性比较""" + if isinstance(other, Student): + return self.student_id == other.student_id + return False + + def __lt__(self, other): + """小于比较(按平均成绩)""" + if isinstance(other, Student): + return self.get_average_grade() < other.get_average_grade() + return NotImplemented + +# 使用示例 +print("=== 创建学生对象 ===") +# 创建学生对象 +student1 = Student("张三", 20, "20230001") +student2 = Student("李四", 19, "20230002") +student3 = Student("王五", 21, "20230003") + +print(student1) +print(student2) +print(student3) + +print(f"\n当前学生总数:{Student.get_student_count()}") +print(f"学校名称:{Student.school}") + +print("\n=== 学生自我介绍 ===") +print(student1.introduce()) +print(student2.introduce()) + +print("\n=== 添加成绩 ===") +student1.add_grade("数学", 95) +student1.add_grade("英语", 87) +student1.add_grade("物理", 92) + +student2.add_grade("数学", 78) +student2.add_grade("英语", 85) +student2.add_grade("物理", 80) + +print(f"\n{student1.name}的平均成绩:{student1.get_average_grade():.2f}分,等级:{student1.get_grade_level()}") +print(f"{student2.name}的平均成绩:{student2.get_average_grade():.2f}分,等级:{student2.get_grade_level()}") + +print("\n=== 使用类方法和静态方法 ===") +# 使用类方法创建学生 +student4 = Student.create_from_string("赵六, 22, 20230004") +print(f"从字符串创建的学生:{student4}") + +# 使用静态方法验证学号 +print(f"学号20230001是否有效:{Student.is_valid_student_id('20230001')}") +print(f"学号123是否有效:{Student.is_valid_student_id('123')}") + +print("\n=== 对象比较 ===") +print(f"student1 == student2: {student1 == student2}") +print(f"student1 < student2: {student1 < student2}") +``` + +### 1.2 属性访问控制 + +```python +class BankAccount: + """银行账户类 - 演示封装和属性访问控制""" + + def __init__(self, account_number, owner_name, initial_balance=0): + self.account_number = account_number # 公开属性 + self.owner_name = owner_name # 公开属性 + self._balance = initial_balance # 受保护属性(约定:单下划线) + self.__pin = None # 私有属性(双下划线) + self.__transaction_history = [] # 私有属性 + + def set_pin(self, pin): + """设置PIN码""" + if isinstance(pin, str) and len(pin) == 4 and pin.isdigit(): + self.__pin = pin + print("PIN码设置成功") + else: + print("PIN码必须是4位数字") + + def verify_pin(self, pin): + """验证PIN码""" + return self.__pin == pin + + def deposit(self, amount, pin): + """存款""" + if not self.verify_pin(pin): + print("PIN码错误") + return False + + if amount > 0: + self._balance += amount + self.__add_transaction("存款", amount) + print(f"存款成功,当前余额:{self._balance}元") + return True + else: + print("存款金额必须大于0") + return False + + def withdraw(self, amount, pin): + """取款""" + if not self.verify_pin(pin): + print("PIN码错误") + return False + + if amount > 0 and amount <= self._balance: + self._balance -= amount + self.__add_transaction("取款", -amount) + print(f"取款成功,当前余额:{self._balance}元") + return True + else: + print("取款金额无效或余额不足") + return False + + def get_balance(self, pin): + """查询余额""" + if not self.verify_pin(pin): + print("PIN码错误") + return None + return self._balance + + def get_transaction_history(self, pin): + """获取交易历史""" + if not self.verify_pin(pin): + print("PIN码错误") + return None + return self.__transaction_history.copy() + + def __add_transaction(self, transaction_type, amount): + """私有方法:添加交易记录""" + from datetime import datetime + transaction = { + 'type': transaction_type, + 'amount': amount, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'balance_after': self._balance + } + self.__transaction_history.append(transaction) + + def __str__(self): + return f"银行账户({self.account_number}, {self.owner_name})" + +# 使用示例 +print("=== 银行账户示例 ===") +account = BankAccount("6222001234567890", "张三", 1000) +print(account) + +# 设置PIN码 +account.set_pin("1234") + +# 存款 +account.deposit(500, "1234") + +# 取款 +account.withdraw(200, "1234") + +# 查询余额 +balance = account.get_balance("1234") +print(f"当前余额:{balance}元") + +# 查看交易历史 +history = account.get_transaction_history("1234") +print("\n交易历史:") +for transaction in history: + print(f"{transaction['timestamp']} - {transaction['type']}: {transaction['amount']}元, 余额: {transaction['balance_after']}元") + +# 尝试直接访问私有属性(会失败) +print("\n=== 属性访问测试 ===") +print(f"公开属性 - 账户号码:{account.account_number}") +print(f"受保护属性 - 余额:{account._balance}") + +# 尝试访问私有属性(Python会进行名称修饰) +try: + print(account.__pin) # 这会引发AttributeError +except AttributeError as e: + print(f"无法直接访问私有属性:{e}") + +# 实际上私有属性被修饰为 _ClassName__attributeName +print(f"通过名称修饰访问私有属性:{account._BankAccount__pin}") +``` + +--- + +## 二、继承 + +### 2.1 基本继承 + +```python +# 基类(父类) +class Animal: + """动物基类""" + + def __init__(self, name, species, age): + self.name = name + self.species = species + self.age = age + self.is_alive = True + + def eat(self, food): + """吃东西""" + print(f"{self.name}正在吃{food}") + + def sleep(self): + """睡觉""" + print(f"{self.name}正在睡觉") + + def make_sound(self): + """发出声音(基类中的通用实现)""" + print(f"{self.name}发出了声音") + + def get_info(self): + """获取动物信息""" + status = "活着" if self.is_alive else "已死亡" + return f"{self.name}是一只{self.age}岁的{self.species},目前{status}" + + def __str__(self): + return f"{self.species}({self.name})" + +# 派生类(子类) +class Dog(Animal): + """狗类 - 继承自Animal""" + + def __init__(self, name, breed, age, owner=None): + # 调用父类的构造方法 + super().__init__(name, "狗", age) + self.breed = breed # 狗特有的属性 + self.owner = owner + self.is_trained = False + + def make_sound(self): + """重写父类方法 - 狗的叫声""" + print(f"{self.name}汪汪叫") + + def wag_tail(self): + """狗特有的方法 - 摇尾巴""" + print(f"{self.name}开心地摇尾巴") + + def fetch(self, item): + """狗特有的方法 - 捡东西""" + print(f"{self.name}去捡{item}") + + def train(self, command): + """训练狗""" + print(f"正在训练{self.name}学习'{command}'命令") + self.is_trained = True + + def get_info(self): + """重写父类方法,添加狗特有信息""" + base_info = super().get_info() + breed_info = f",品种是{self.breed}" + owner_info = f",主人是{self.owner}" if self.owner else ",还没有主人" + training_info = f",{'已训练' if self.is_trained else '未训练'}" + return base_info + breed_info + owner_info + training_info + +class Cat(Animal): + """猫类 - 继承自Animal""" + + def __init__(self, name, breed, age, is_indoor=True): + super().__init__(name, "猫", age) + self.breed = breed + self.is_indoor = is_indoor + self.lives_remaining = 9 # 猫有九条命 + + def make_sound(self): + """重写父类方法 - 猫的叫声""" + print(f"{self.name}喵喵叫") + + def purr(self): + """猫特有的方法 - 呼噜声""" + print(f"{self.name}发出满足的呼噜声") + + def climb(self, target): + """猫特有的方法 - 爬高""" + print(f"{self.name}爬到了{target}上") + + def hunt(self, prey): + """猫特有的方法 - 狩猎""" + if not self.is_indoor: + print(f"{self.name}正在狩猎{prey}") + else: + print(f"{self.name}是室内猫,不能狩猎") + + def lose_life(self): + """失去一条命""" + if self.lives_remaining > 0: + self.lives_remaining -= 1 + print(f"{self.name}失去了一条命,还剩{self.lives_remaining}条命") + if self.lives_remaining == 0: + self.is_alive = False + print(f"{self.name}用完了所有的命") + else: + print(f"{self.name}已经没有命了") + +class Bird(Animal): + """鸟类 - 继承自Animal""" + + def __init__(self, name, species, age, can_fly=True): + super().__init__(name, species, age) + self.can_fly = can_fly + self.altitude = 0 # 当前高度 + + def make_sound(self): + """重写父类方法 - 鸟的叫声""" + print(f"{self.name}啾啾叫") + + def fly(self, height): + """鸟特有的方法 - 飞行""" + if self.can_fly: + self.altitude = height + print(f"{self.name}飞到了{height}米高") + else: + print(f"{self.name}不会飞") + + def land(self): + """降落""" + if self.altitude > 0: + print(f"{self.name}从{self.altitude}米高降落到地面") + self.altitude = 0 + else: + print(f"{self.name}已经在地面上了") + +# 使用示例 +print("=== 继承示例 ===") + +# 创建不同类型的动物 +dog = Dog("旺财", "金毛", 3, "张三") +cat = Cat("咪咪", "波斯猫", 2, True) +bird = Bird("小黄", "金丝雀", 1, True) + +animals = [dog, cat, bird] + +print("=== 所有动物的基本信息 ===") +for animal in animals: + print(animal.get_info()) + +print("\n=== 所有动物都会吃和睡 ===") +for animal in animals: + animal.eat("食物") + animal.sleep() + +print("\n=== 多态:不同动物发出不同声音 ===") +for animal in animals: + animal.make_sound() # 每个子类都有自己的实现 + +print("\n=== 各种动物的特有行为 ===") +# 狗的特有行为 +dog.wag_tail() +dog.fetch("球") +dog.train("坐下") + +# 猫的特有行为 +cat.purr() +cat.climb("书架") +cat.hunt("老鼠") +cat.lose_life() + +# 鸟的特有行为 +bird.fly(50) +bird.land() + +print("\n=== 检查继承关系 ===") +print(f"dog是Animal的实例吗?{isinstance(dog, Animal)}") +print(f"dog是Dog的实例吗?{isinstance(dog, Dog)}") +print(f"dog是Cat的实例吗?{isinstance(dog, Cat)}") +print(f"Dog是Animal的子类吗?{issubclass(Dog, Animal)}") +print(f"Animal是Dog的子类吗?{issubclass(Animal, Dog)}") +``` + +### 2.2 多重继承 + +```python +# 多重继承示例 +class Flyable: + """可飞行的混入类""" + + def __init__(self): + self.max_altitude = 1000 + self.current_altitude = 0 + + def take_off(self): + """起飞""" + if self.current_altitude == 0: + self.current_altitude = 10 + print(f"{self.name}起飞了,当前高度:{self.current_altitude}米") + else: + print(f"{self.name}已经在飞行中") + + def fly_to_altitude(self, altitude): + """飞到指定高度""" + if altitude <= self.max_altitude: + self.current_altitude = altitude + print(f"{self.name}飞到了{altitude}米高") + else: + print(f"{self.name}无法飞到{altitude}米,最大高度是{self.max_altitude}米") + + def land(self): + """降落""" + if self.current_altitude > 0: + print(f"{self.name}从{self.current_altitude}米高降落") + self.current_altitude = 0 + else: + print(f"{self.name}已经在地面上") + +class Swimmable: + """可游泳的混入类""" + + def __init__(self): + self.max_depth = 100 + self.current_depth = 0 + + def dive(self, depth): + """潜水""" + if depth <= self.max_depth: + self.current_depth = depth + print(f"{self.name}潜水到{depth}米深") + else: + print(f"{self.name}无法潜到{depth}米,最大深度是{self.max_depth}米") + + def surface(self): + """浮出水面""" + if self.current_depth > 0: + print(f"{self.name}从{self.current_depth}米深浮出水面") + self.current_depth = 0 + else: + print(f"{self.name}已经在水面上") + + def swim(self, direction): + """游泳""" + print(f"{self.name}向{direction}游泳") + +class Duck(Animal, Flyable, Swimmable): + """鸭子类 - 多重继承示例""" + + def __init__(self, name, age): + # 调用所有父类的构造方法 + Animal.__init__(self, name, "鸭子", age) + Flyable.__init__(self) + Swimmable.__init__(self) + + # 重新设置鸭子的特定限制 + self.max_altitude = 500 # 鸭子飞不太高 + self.max_depth = 5 # 鸭子潜不太深 + + def make_sound(self): + """重写父类方法 - 鸭子的叫声""" + print(f"{self.name}嘎嘎叫") + + def paddle(self): + """鸭子特有的方法 - 划水""" + print(f"{self.name}用脚掌划水") + +class Penguin(Animal, Swimmable): + """企鹅类 - 不会飞但会游泳""" + + def __init__(self, name, age): + Animal.__init__(self, name, "企鹅", age) + Swimmable.__init__(self) + self.max_depth = 200 # 企鹅游泳很厉害 + + def make_sound(self): + """企鹅的叫声""" + print(f"{self.name}发出企鹅特有的叫声") + + def slide_on_ice(self): + """在冰上滑行""" + print(f"{self.name}在冰上滑行") + +# 使用示例 +print("\n=== 多重继承示例 ===") + +duck = Duck("唐老鸭", 2) +penguin = Penguin("企鹅先生", 3) + +print("=== 鸭子的能力 ===") +print(duck.get_info()) +duck.make_sound() + +# 鸭子可以飞 +duck.take_off() +duck.fly_to_altitude(200) +duck.land() + +# 鸭子可以游泳 +duck.swim("前方") +duck.dive(3) +duck.paddle() +duck.surface() + +print("\n=== 企鹅的能力 ===") +print(penguin.get_info()) +penguin.make_sound() + +# 企鹅不能飞,但可以游泳 +penguin.swim("深海") +penguin.dive(50) +penguin.slide_on_ice() +penguin.surface() + +print("\n=== 方法解析顺序(MRO) ===") +print(f"Duck的MRO:{Duck.__mro__}") +print(f"Penguin的MRO:{Penguin.__mro__}") +``` + +--- + +## 三、多态 + +### 3.1 方法重写和多态 + +```python +# 多态示例:图形类 +class Shape: + """图形基类""" + + def __init__(self, name): + self.name = name + + def area(self): + """计算面积 - 基类中的抽象方法""" + raise NotImplementedError("子类必须实现area方法") + + def perimeter(self): + """计算周长 - 基类中的抽象方法""" + raise NotImplementedError("子类必须实现perimeter方法") + + def describe(self): + """描述图形""" + return f"这是一个{self.name},面积:{self.area():.2f},周长:{self.perimeter():.2f}" + + def __str__(self): + return f"{self.name}(面积: {self.area():.2f})" + +class Rectangle(Shape): + """矩形类""" + + def __init__(self, width, height): + super().__init__("矩形") + self.width = width + self.height = height + + def area(self): + """矩形面积""" + return self.width * self.height + + def perimeter(self): + """矩形周长""" + return 2 * (self.width + self.height) + + def is_square(self): + """判断是否为正方形""" + return self.width == self.height + +class Circle(Shape): + """圆形类""" + + def __init__(self, radius): + super().__init__("圆形") + self.radius = radius + + def area(self): + """圆形面积""" + import math + return math.pi * self.radius ** 2 + + def perimeter(self): + """圆形周长""" + import math + return 2 * math.pi * self.radius + + def diameter(self): + """直径""" + return 2 * self.radius + +class Triangle(Shape): + """三角形类""" + + def __init__(self, side1, side2, side3): + super().__init__("三角形") + self.side1 = side1 + self.side2 = side2 + self.side3 = side3 + + # 验证是否能构成三角形 + if not self._is_valid_triangle(): + raise ValueError("无效的三角形边长") + + def _is_valid_triangle(self): + """验证三角形的有效性""" + return (self.side1 + self.side2 > self.side3 and + self.side1 + self.side3 > self.side2 and + self.side2 + self.side3 > self.side1) + + def area(self): + """三角形面积(海伦公式)""" + s = self.perimeter() / 2 # 半周长 + import math + return math.sqrt(s * (s - self.side1) * (s - self.side2) * (s - self.side3)) + + def perimeter(self): + """三角形周长""" + return self.side1 + self.side2 + self.side3 + + def triangle_type(self): + """判断三角形类型""" + sides = sorted([self.side1, self.side2, self.side3]) + if sides[0] == sides[1] == sides[2]: + return "等边三角形" + elif sides[0] == sides[1] or sides[1] == sides[2]: + return "等腰三角形" + else: + return "普通三角形" + +# 多态函数 +def calculate_total_area(shapes): + """计算多个图形的总面积""" + total = 0 + for shape in shapes: + total += shape.area() # 多态:每个图形都有自己的area实现 + return total + +def print_shape_info(shapes): + """打印图形信息""" + for shape in shapes: + print(shape.describe()) # 多态:调用各自的area和perimeter方法 + +def find_largest_shape(shapes): + """找到面积最大的图形""" + if not shapes: + return None + return max(shapes, key=lambda shape: shape.area()) + +# 使用示例 +print("=== 多态示例 ===") + +# 创建不同类型的图形 +shapes = [ + Rectangle(5, 3), + Circle(4), + Triangle(3, 4, 5), + Rectangle(2, 2), # 正方形 + Circle(2.5) +] + +print("=== 所有图形信息 ===") +print_shape_info(shapes) + +print(f"\n=== 统计信息 ===") +print(f"图形总数:{len(shapes)}") +print(f"总面积:{calculate_total_area(shapes):.2f}") + +largest = find_largest_shape(shapes) +print(f"面积最大的图形:{largest}") + +print("\n=== 特定类型的操作 ===") +for shape in shapes: + if isinstance(shape, Rectangle): + if shape.is_square(): + print(f"{shape.name}是正方形,边长:{shape.width}") + else: + print(f"{shape.name}的长宽比:{shape.width/shape.height:.2f}") + elif isinstance(shape, Circle): + print(f"{shape.name}的直径:{shape.diameter():.2f}") + elif isinstance(shape, Triangle): + print(f"{shape.name}的类型:{shape.triangle_type()}") + +# 演示多态的威力 +print("\n=== 多态演示 ===") +def process_shape(shape): + """处理任意图形 - 不需要知道具体类型""" + print(f"处理{shape.name}:") + print(f" 面积: {shape.area():.2f}") + print(f" 周长: {shape.perimeter():.2f}") + + # 根据面积大小给出评价 + area = shape.area() + if area > 50: + print(" 这是一个大图形") + elif area > 20: + print(" 这是一个中等图形") + else: + print(" 这是一个小图形") + print() + +for shape in shapes: + process_shape(shape) # 同一个函数处理不同类型的图形 +``` + +### 3.2 抽象基类 + +```python +from abc import ABC, abstractmethod + +class Vehicle(ABC): + """交通工具抽象基类""" + + def __init__(self, brand, model, year): + self.brand = brand + self.model = model + self.year = year + self.is_running = False + + @abstractmethod + def start_engine(self): + """启动引擎 - 抽象方法""" + pass + + @abstractmethod + def stop_engine(self): + """停止引擎 - 抽象方法""" + pass + + @abstractmethod + def get_fuel_type(self): + """获取燃料类型 - 抽象方法""" + pass + + def get_info(self): + """获取车辆信息 - 具体方法""" + status = "运行中" if self.is_running else "停止" + return f"{self.year}年 {self.brand} {self.model},当前状态:{status}" + + def honk(self): + """鸣笛 - 具体方法""" + print(f"{self.brand} {self.model}鸣笛:嘀嘀!") + +class Car(Vehicle): + """汽车类""" + + def __init__(self, brand, model, year, doors=4): + super().__init__(brand, model, year) + self.doors = doors + self.fuel_level = 50 # 油量百分比 + + def start_engine(self): + """启动汽车引擎""" + if not self.is_running: + if self.fuel_level > 0: + self.is_running = True + print(f"{self.brand} {self.model}引擎启动成功") + else: + print(f"{self.brand} {self.model}没有燃料,无法启动") + else: + print(f"{self.brand} {self.model}引擎已经在运行") + + def stop_engine(self): + """停止汽车引擎""" + if self.is_running: + self.is_running = False + print(f"{self.brand} {self.model}引擎已停止") + else: + print(f"{self.brand} {self.model}引擎已经停止") + + def get_fuel_type(self): + """获取燃料类型""" + return "汽油" + + def refuel(self, amount): + """加油""" + self.fuel_level = min(100, self.fuel_level + amount) + print(f"{self.brand} {self.model}加油完成,当前油量:{self.fuel_level}%") + + def drive(self, distance): + """驾驶""" + if self.is_running: + fuel_consumed = distance * 0.1 # 假设每公里消耗0.1%的油 + if self.fuel_level >= fuel_consumed: + self.fuel_level -= fuel_consumed + print(f"{self.brand} {self.model}行驶了{distance}公里,剩余油量:{self.fuel_level:.1f}%") + else: + print(f"{self.brand} {self.model}燃料不足,无法行驶{distance}公里") + else: + print(f"{self.brand} {self.model}引擎未启动,无法行驶") + +class ElectricCar(Vehicle): + """电动汽车类""" + + def __init__(self, brand, model, year, battery_capacity=100): + super().__init__(brand, model, year) + self.battery_capacity = battery_capacity # 电池容量(kWh) + self.battery_level = 80 # 电量百分比 + + def start_engine(self): + """启动电动汽车""" + if not self.is_running: + if self.battery_level > 0: + self.is_running = True + print(f"{self.brand} {self.model}电动系统启动成功") + else: + print(f"{self.brand} {self.model}电池没电,无法启动") + else: + print(f"{self.brand} {self.model}电动系统已经在运行") + + def stop_engine(self): + """停止电动汽车""" + if self.is_running: + self.is_running = False + print(f"{self.brand} {self.model}电动系统已停止") + else: + print(f"{self.brand} {self.model}电动系统已经停止") + + def get_fuel_type(self): + """获取燃料类型""" + return "电力" + + def charge(self, hours): + """充电""" + charge_amount = hours * 10 # 假设每小时充电10% + self.battery_level = min(100, self.battery_level + charge_amount) + print(f"{self.brand} {self.model}充电{hours}小时,当前电量:{self.battery_level}%") + + def drive(self, distance): + """驾驶电动汽车""" + if self.is_running: + battery_consumed = distance * 0.2 # 假设每公里消耗0.2%的电 + if self.battery_level >= battery_consumed: + self.battery_level -= battery_consumed + print(f"{self.brand} {self.model}行驶了{distance}公里,剩余电量:{self.battery_level:.1f}%") + else: + print(f"{self.brand} {self.model}电量不足,无法行驶{distance}公里") + else: + print(f"{self.brand} {self.model}电动系统未启动,无法行驶") + +class Motorcycle(Vehicle): + """摩托车类""" + + def __init__(self, brand, model, year, engine_size): + super().__init__(brand, model, year) + self.engine_size = engine_size # 发动机排量 + self.fuel_level = 30 + + def start_engine(self): + """启动摩托车引擎""" + if not self.is_running: + if self.fuel_level > 0: + self.is_running = True + print(f"{self.brand} {self.model}摩托车引擎启动,轰鸣声响起") + else: + print(f"{self.brand} {self.model}没有燃料,无法启动") + else: + print(f"{self.brand} {self.model}引擎已经在运行") + + def stop_engine(self): + """停止摩托车引擎""" + if self.is_running: + self.is_running = False + print(f"{self.brand} {self.model}摩托车引擎已停止") + else: + print(f"{self.brand} {self.model}引擎已经停止") + + def get_fuel_type(self): + """获取燃料类型""" + return "汽油" + + def wheelie(self): + """摩托车特技 - 翘头""" + if self.is_running: + print(f"{self.brand} {self.model}做了一个精彩的翘头动作!") + else: + print(f"{self.brand} {self.model}需要先启动引擎") + +# 多态函数 +def start_all_vehicles(vehicles): + """启动所有车辆""" + print("=== 启动所有车辆 ===") + for vehicle in vehicles: + vehicle.start_engine() + +def show_vehicle_info(vehicles): + """显示所有车辆信息""" + print("=== 车辆信息 ===") + for vehicle in vehicles: + print(f"{vehicle.get_info()},燃料类型:{vehicle.get_fuel_type()}") + +def honk_all(vehicles): + """所有车辆鸣笛""" + print("=== 集体鸣笛 ===") + for vehicle in vehicles: + vehicle.honk() + +# 使用示例 +print("=== 抽象基类和多态示例 ===") + +# 创建不同类型的车辆 +vehicles = [ + Car("丰田", "卡罗拉", 2022), + ElectricCar("特斯拉", "Model 3", 2023, 75), + Motorcycle("本田", "CBR600RR", 2021, 600) +] + +# 显示车辆信息 +show_vehicle_info(vehicles) + +# 启动所有车辆 +start_all_vehicles(vehicles) + +# 集体鸣笛 +honk_all(vehicles) + +print("\n=== 特定操作 ===") +# 汽车加油和行驶 +car = vehicles[0] +car.refuel(20) +car.drive(50) + +# 电动汽车充电和行驶 +electric_car = vehicles[1] +electric_car.charge(2) +electric_car.drive(30) + +# 摩托车特技 +motorcycle = vehicles[2] +motorcycle.wheelie() + +# 尝试创建抽象基类的实例(会报错) +try: + abstract_vehicle = Vehicle("测试", "测试", 2023) +except TypeError as e: + print(f"\n无法创建抽象基类的实例:{e}") +``` + +--- + +## 四、特殊方法(魔术方法) + +### 4.1 常用特殊方法 + +```python +class Money: + """货币类 - 演示特殊方法的使用""" + + def __init__(self, amount, currency="CNY"): + self.amount = amount + self.currency = currency + + def __str__(self): + """用户友好的字符串表示""" + currency_symbols = { + "CNY": "¥", + "USD": "$", + "EUR": "€", + "JPY": "¥" + } + symbol = currency_symbols.get(self.currency, self.currency) + return f"{symbol}{self.amount:.2f}" + + def __repr__(self): + """开发者友好的字符串表示""" + return f"Money({self.amount}, '{self.currency}')" + + def __add__(self, other): + """加法运算""" + if isinstance(other, Money): + if self.currency == other.currency: + return Money(self.amount + other.amount, self.currency) + else: + raise ValueError(f"不能将{self.currency}和{other.currency}直接相加") + elif isinstance(other, (int, float)): + return Money(self.amount + other, self.currency) + else: + return NotImplemented + + def __radd__(self, other): + """右加法运算(当左操作数不支持加法时)""" + return self.__add__(other) + + def __sub__(self, other): + """减法运算""" + if isinstance(other, Money): + if self.currency == other.currency: + return Money(self.amount - other.amount, self.currency) + else: + raise ValueError(f"不能将{self.currency}和{other.currency}直接相减") + elif isinstance(other, (int, float)): + return Money(self.amount - other, self.currency) + else: + return NotImplemented + + def __mul__(self, other): + """乘法运算""" + if isinstance(other, (int, float)): + return Money(self.amount * other, self.currency) + else: + return NotImplemented + + def __rmul__(self, other): + """右乘法运算""" + return self.__mul__(other) + + def __truediv__(self, other): + """除法运算""" + if isinstance(other, (int, float)): + if other != 0: + return Money(self.amount / other, self.currency) + else: + raise ZeroDivisionError("不能除以零") + else: + return NotImplemented + + def __eq__(self, other): + """相等比较""" + if isinstance(other, Money): + return self.amount == other.amount and self.currency == other.currency + return False + + def __lt__(self, other): + """小于比较""" + if isinstance(other, Money): + if self.currency == other.currency: + return self.amount < other.amount + else: + raise ValueError(f"不能比较{self.currency}和{other.currency}") + return NotImplemented + + def __le__(self, other): + """小于等于比较""" + return self == other or self < other + + def __gt__(self, other): + """大于比较""" + if isinstance(other, Money): + if self.currency == other.currency: + return self.amount > other.amount + else: + raise ValueError(f"不能比较{self.currency}和{other.currency}") + return NotImplemented + + def __ge__(self, other): + """大于等于比较""" + return self == other or self > other + + def __hash__(self): + """哈希值(使对象可以用作字典键或集合元素)""" + return hash((self.amount, self.currency)) + + def __bool__(self): + """布尔值转换""" + return self.amount != 0 + + def __abs__(self): + """绝对值""" + return Money(abs(self.amount), self.currency) + + def __neg__(self): + """负数""" + return Money(-self.amount, self.currency) + + def __round__(self, ndigits=2): + """四舍五入""" + return Money(round(self.amount, ndigits), self.currency) + +class BankAccount: + """银行账户类 - 演示容器特殊方法""" + + def __init__(self, owner, initial_balance=0): + self.owner = owner + self.balance = Money(initial_balance) + self.transactions = [] # 交易记录 + + def deposit(self, amount): + """存款""" + if isinstance(amount, (int, float)): + amount = Money(amount) + self.balance += amount + self.transactions.append(f"存款 {amount}") + + def withdraw(self, amount): + """取款""" + if isinstance(amount, (int, float)): + amount = Money(amount) + if amount <= self.balance: + self.balance -= amount + self.transactions.append(f"取款 {amount}") + else: + raise ValueError("余额不足") + + def __len__(self): + """返回交易记录数量""" + return len(self.transactions) + + def __getitem__(self, index): + """通过索引获取交易记录""" + return self.transactions[index] + + def __setitem__(self, index, value): + """通过索引设置交易记录""" + self.transactions[index] = value + + def __delitem__(self, index): + """通过索引删除交易记录""" + del self.transactions[index] + + def __contains__(self, item): + """检查是否包含某个交易记录""" + return item in self.transactions + + def __iter__(self): + """使对象可迭代""" + return iter(self.transactions) + + def __str__(self): + return f"账户所有者:{self.owner},余额:{self.balance}" + + def __repr__(self): + return f"BankAccount('{self.owner}', {self.balance.amount})" + +# 使用示例 +print("=== 特殊方法示例 ===") + +# 创建货币对象 +money1 = Money(100, "CNY") +money2 = Money(50, "CNY") +money3 = Money(75, "USD") + +print("=== 基本操作 ===") +print(f"money1: {money1}") +print(f"money2: {money2}") +print(f"money3: {money3}") +print(f"repr(money1): {repr(money1)}") + +print("\n=== 算术运算 ===") +print(f"money1 + money2 = {money1 + money2}") +print(f"money1 - money2 = {money1 - money2}") +print(f"money1 * 2 = {money1 * 2}") +print(f"3 * money2 = {3 * money2}") +print(f"money1 / 4 = {money1 / 4}") +print(f"money1 + 25 = {money1 + 25}") + +print("\n=== 比较运算 ===") +print(f"money1 == money2: {money1 == money2}") +print(f"money1 > money2: {money1 > money2}") +print(f"money1 < money2: {money1 < money2}") + +print("\n=== 其他运算 ===") +print(f"abs(Money(-50)): {abs(Money(-50))}") +print(f"-money1: {-money1}") +print(f"round(Money(123.456)): {round(Money(123.456))}") +print(f"bool(Money(0)): {bool(Money(0))}") +print(f"bool(money1): {bool(money1)}") + +print("\n=== 银行账户示例 ===") +account = BankAccount("张三", 1000) +print(account) + +# 进行一些交易 +account.deposit(500) +account.withdraw(200) +account.deposit(Money(300)) + +print(f"\n交易记录数量:{len(account)}") +print("所有交易记录:") +for i, transaction in enumerate(account): + print(f"{i}: {transaction}") + +print(f"\n第一笔交易:{account[0]}") +print(f"最后一笔交易:{account[-1]}") +print(f"是否包含'存款 ¥500.00':{'存款 ¥500.00' in account}") + +# 货币对象可以用作字典键 +print("\n=== 货币作为字典键 ===") +portfolio = { + Money(1000, "CNY"): "人民币储蓄", + Money(500, "USD"): "美元投资", + Money(200, "EUR"): "欧元基金" +} + +for currency, description in portfolio.items(): + print(f"{currency}: {description}") + +# 尝试不同货币的运算(会报错) +try: + result = money1 + money3 # CNY + USD +except ValueError as e: + print(f"\n货币运算错误:{e}") +``` + +### 4.2 上下文管理器 + +```python +class FileManager: + """文件管理器 - 演示上下文管理器""" + + def __init__(self, filename, mode='r'): + self.filename = filename + self.mode = mode + self.file = None + + def __enter__(self): + """进入上下文时调用""" + print(f"打开文件:{self.filename}") + self.file = open(self.filename, self.mode) + return self.file + + def __exit__(self, exc_type, exc_value, traceback): + """退出上下文时调用""" + if self.file: + print(f"关闭文件:{self.filename}") + self.file.close() + + # 处理异常 + if exc_type is not None: + print(f"发生异常:{exc_type.__name__}: {exc_value}") + return False # 不抑制异常 + return True + +class DatabaseConnection: + """数据库连接 - 另一个上下文管理器示例""" + + def __init__(self, host, database): + self.host = host + self.database = database + self.connection = None + + def __enter__(self): + """建立数据库连接""" + print(f"连接到数据库:{self.host}/{self.database}") + # 这里模拟数据库连接 + self.connection = f"Connection to {self.database}" + return self.connection + + def __exit__(self, exc_type, exc_value, traceback): + """关闭数据库连接""" + if self.connection: + print(f"关闭数据库连接:{self.database}") + self.connection = None + return False + +# 使用示例 +print("\n=== 上下文管理器示例 ===") + +# 使用自定义文件管理器 +try: + with FileManager("test.txt", "w") as f: + f.write("Hello, World!") + f.write("\nPython面向对象编程") +except FileNotFoundError: + print("文件操作完成(文件可能不存在,这是正常的)") + +# 使用数据库连接管理器 +with DatabaseConnection("localhost", "myapp") as conn: + print(f"使用连接:{conn}") + # 模拟数据库操作 + print("执行SQL查询...") + +print("连接已自动关闭") +``` + +--- + +## 五、属性装饰器 + +### 5.1 @property装饰器 + +```python +class Temperature: + """温度类 - 演示属性装饰器的使用""" + + def __init__(self, celsius=0): + self._celsius = celsius + + @property + def celsius(self): + """获取摄氏温度""" + return self._celsius + + @celsius.setter + def celsius(self, value): + """设置摄氏温度""" + if value < -273.15: + raise ValueError("温度不能低于绝对零度(-273.15°C)") + self._celsius = value + + @property + def fahrenheit(self): + """获取华氏温度""" + return self._celsius * 9/5 + 32 + + @fahrenheit.setter + def fahrenheit(self, value): + """设置华氏温度""" + celsius = (value - 32) * 5/9 + if celsius < -273.15: + raise ValueError("温度不能低于绝对零度") + self._celsius = celsius + + @property + def kelvin(self): + """获取开尔文温度""" + return self._celsius + 273.15 + + @kelvin.setter + def kelvin(self, value): + """设置开尔文温度""" + if value < 0: + raise ValueError("开尔文温度不能为负数") + self._celsius = value - 273.15 + + def __str__(self): + return f"{self._celsius:.2f}°C ({self.fahrenheit:.2f}°F, {self.kelvin:.2f}K)" + +class Circle: + """圆形类 - 演示只读属性""" + + def __init__(self, radius): + self._radius = radius + + @property + def radius(self): + """半径(只读)""" + return self._radius + + @property + def diameter(self): + """直径(计算属性)""" + return self._radius * 2 + + @property + def area(self): + """面积(计算属性)""" + import math + return math.pi * self._radius ** 2 + + @property + def circumference(self): + """周长(计算属性)""" + import math + return 2 * math.pi * self._radius + + def __str__(self): + return f"圆形(半径: {self.radius}, 直径: {self.diameter:.2f}, 面积: {self.area:.2f}, 周长: {self.circumference:.2f})" + +class Person: + """人员类 - 演示属性验证""" + + def __init__(self, name, age, email): + self.name = name + self.age = age + self.email = email + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + if not isinstance(value, str) or len(value.strip()) == 0: + raise ValueError("姓名必须是非空字符串") + self._name = value.strip() + + @property + def age(self): + return self._age + + @age.setter + def age(self, value): + if not isinstance(value, int) or value < 0 or value > 150: + raise ValueError("年龄必须是0-150之间的整数") + self._age = value + + @property + def email(self): + return self._email + + @email.setter + def email(self, value): + if not isinstance(value, str) or '@' not in value: + raise ValueError("邮箱格式不正确") + self._email = value.lower() + + @property + def is_adult(self): + """是否成年(只读属性)""" + return self._age >= 18 + + @property + def age_group(self): + """年龄组(只读属性)""" + if self._age < 13: + return "儿童" + elif self._age < 18: + return "青少年" + elif self._age < 60: + return "成年人" + else: + return "老年人" + + def __str__(self): + return f"{self.name}({self.age}岁, {self.email}, {self.age_group})" + +# 使用示例 +print("=== 属性装饰器示例 ===") + +# 温度转换 +print("=== 温度转换 ===") +temp = Temperature(25) +print(f"初始温度:{temp}") + +# 设置不同单位的温度 +temp.fahrenheit = 100 +print(f"设置华氏100度后:{temp}") + +temp.kelvin = 300 +print(f"设置开尔文300度后:{temp}") + +# 尝试设置无效温度 +try: + temp.celsius = -300 +except ValueError as e: + print(f"温度设置错误:{e}") + +# 圆形计算属性 +print("\n=== 圆形计算属性 ===") +circle = Circle(5) +print(circle) + +# 人员属性验证 +print("\n=== 人员属性验证 ===") +try: + person = Person("张三", 25, "zhangsan@example.com") + print(person) + print(f"是否成年:{person.is_adult}") + + # 修改属性 + person.age = 17 + print(f"修改年龄后:{person}") + print(f"是否成年:{person.is_adult}") + + # 尝试设置无效邮箱 + person.email = "invalid-email" +except ValueError as e: + print(f"属性设置错误:{e}") +``` + +--- + +## 六、实战练习 + +### 6.1 图书管理系统 + +```python +from datetime import datetime, timedelta + +class Book: + """图书类""" + + def __init__(self, isbn, title, author, publisher, price, total_copies=1): + self.isbn = isbn + self.title = title + self.author = author + self.publisher = publisher + self.price = price + self.total_copies = total_copies + self.available_copies = total_copies + self.borrowed_copies = 0 + + def borrow(self): + """借书""" + if self.available_copies > 0: + self.available_copies -= 1 + self.borrowed_copies += 1 + return True + return False + + def return_book(self): + """还书""" + if self.borrowed_copies > 0: + self.available_copies += 1 + self.borrowed_copies -= 1 + return True + return False + + @property + def is_available(self): + """是否有可借阅的副本""" + return self.available_copies > 0 + + def __str__(self): + return f"《{self.title}》- {self.author} (可借: {self.available_copies}/{self.total_copies})" + + def __repr__(self): + return f"Book('{self.isbn}', '{self.title}', '{self.author}')" + +class Member: + """会员类""" + + def __init__(self, member_id, name, phone, email): + self.member_id = member_id + self.name = name + self.phone = phone + self.email = email + self.borrowed_books = [] # 当前借阅的书籍 + self.borrow_history = [] # 借阅历史 + self.join_date = datetime.now() + + def can_borrow(self, max_books=5): + """检查是否可以借书""" + return len(self.borrowed_books) < max_books + + def borrow_book(self, book, due_days=30): + """借书""" + if not self.can_borrow(): + return False, "已达到最大借书数量" + + if not book.is_available: + return False, "图书不可借阅" + + if book.borrow(): + borrow_record = { + 'book': book, + 'borrow_date': datetime.now(), + 'due_date': datetime.now() + timedelta(days=due_days), + 'returned': False + } + self.borrowed_books.append(borrow_record) + self.borrow_history.append(borrow_record) + return True, "借书成功" + + return False, "借书失败" + + def return_book(self, book): + """还书""" + for record in self.borrowed_books: + if record['book'] == book and not record['returned']: + record['returned'] = True + record['return_date'] = datetime.now() + self.borrowed_books.remove(record) + book.return_book() + return True, "还书成功" + + return False, "未找到借阅记录" + + def get_overdue_books(self): + """获取逾期图书""" + overdue = [] + current_time = datetime.now() + for record in self.borrowed_books: + if current_time > record['due_date']: + overdue.append(record) + return overdue + + def __str__(self): + return f"会员: {self.name} (ID: {self.member_id}, 当前借阅: {len(self.borrowed_books)}本)" + +class Library: + """图书馆类""" + + def __init__(self, name): + self.name = name + self.books = {} # ISBN -> Book + self.members = {} # member_id -> Member + self.next_member_id = 1 + + def add_book(self, book): + """添加图书""" + if book.isbn in self.books: + # 如果图书已存在,增加副本数 + existing_book = self.books[book.isbn] + existing_book.total_copies += book.total_copies + existing_book.available_copies += book.available_copies + else: + self.books[book.isbn] = book + print(f"添加图书成功:{book}") + + def register_member(self, name, phone, email): + """注册会员""" + member_id = f"M{self.next_member_id:04d}" + member = Member(member_id, name, phone, email) + self.members[member_id] = member + self.next_member_id += 1 + print(f"会员注册成功:{member}") + return member + + def search_books(self, keyword): + """搜索图书""" + results = [] + keyword = keyword.lower() + for book in self.books.values(): + if (keyword in book.title.lower() or + keyword in book.author.lower() or + keyword in book.publisher.lower()): + results.append(book) + return results + + def borrow_book(self, member_id, isbn): + """借书""" + if member_id not in self.members: + return False, "会员不存在" + + if isbn not in self.books: + return False, "图书不存在" + + member = self.members[member_id] + book = self.books[isbn] + + return member.borrow_book(book) + + def return_book(self, member_id, isbn): + """还书""" + if member_id not in self.members: + return False, "会员不存在" + + if isbn not in self.books: + return False, "图书不存在" + + member = self.members[member_id] + book = self.books[isbn] + + return member.return_book(book) + + def get_overdue_report(self): + """获取逾期报告""" + overdue_info = [] + for member in self.members.values(): + overdue_books = member.get_overdue_books() + if overdue_books: + overdue_info.append((member, overdue_books)) + return overdue_info + + def get_statistics(self): + """获取统计信息""" + total_books = sum(book.total_copies for book in self.books.values()) + available_books = sum(book.available_copies for book in self.books.values()) + borrowed_books = total_books - available_books + + return { + 'total_books': total_books, + 'available_books': available_books, + 'borrowed_books': borrowed_books, + 'total_members': len(self.members), + 'unique_titles': len(self.books) + } + + def __str__(self): + stats = self.get_statistics() + return f"{self.name} - 图书: {stats['unique_titles']}种/{stats['total_books']}本, 会员: {stats['total_members']}人" + +# 使用示例 +print("\n=== 图书管理系统示例 ===") + +# 创建图书馆 +library = Library("Python学习图书馆") + +# 添加图书 +books_data = [ + ("978-0-123456-78-9", "Python编程:从入门到实践", "埃里克·马瑟斯", "人民邮电出版社", 89.0, 3), + ("978-0-987654-32-1", "流畅的Python", "Luciano Ramalho", "O'Reilly", 139.0, 2), + ("978-0-111111-11-1", "Python核心编程", "Wesley Chun", "机械工业出版社", 99.0, 2), + ("978-0-222222-22-2", "Python数据分析", "Wes McKinney", "机械工业出版社", 119.0, 1) +] + +for isbn, title, author, publisher, price, copies in books_data: + book = Book(isbn, title, author, publisher, price, copies) + library.add_book(book) + +print(f"\n{library}") + +# 注册会员 +members = [ + library.register_member("张三", "13800138001", "zhangsan@example.com"), + library.register_member("李四", "13800138002", "lisi@example.com"), + library.register_member("王五", "13800138003", "wangwu@example.com") +] + +# 搜索图书 +print("\n=== 搜索图书 ===") +search_results = library.search_books("Python") +print(f"搜索'Python'的结果:") +for book in search_results: + print(f" {book}") + +# 借书操作 +print("\n=== 借书操作 ===") +borrow_operations = [ + ("M0001", "978-0-123456-78-9"), + ("M0001", "978-0-987654-32-1"), + ("M0002", "978-0-123456-78-9"), + ("M0003", "978-0-111111-11-1") +] + +for member_id, isbn in borrow_operations: + success, message = library.borrow_book(member_id, isbn) + member_name = library.members[member_id].name + book_title = library.books[isbn].title + print(f"{member_name} 借阅《{book_title}》: {message}") + +# 显示会员借阅情况 +print("\n=== 会员借阅情况 ===") +for member in library.members.values(): + print(member) + for record in member.borrowed_books: + book = record['book'] + due_date = record['due_date'].strftime('%Y-%m-%d') + print(f" - 《{book.title}》(到期日: {due_date})") + +# 还书操作 +print("\n=== 还书操作 ===") +success, message = library.return_book("M0001", "978-0-123456-78-9") +print(f"张三 归还《Python编程:从入门到实践》: {message}") + +# 显示统计信息 +print("\n=== 图书馆统计 ===") +stats = library.get_statistics() +for key, value in stats.items(): + print(f"{key}: {value}") +``` + +--- + +## 七、总结 + +### 面向对象编程核心概念回顾 + +1. **类和对象** + - 类是对象的模板,对象是类的实例 + - 使用`__init__`方法初始化对象 + - 类变量和实例变量的区别 + +2. **封装** + - 使用私有属性(`__attribute`)隐藏内部实现 + - 使用属性装饰器(`@property`)控制属性访问 + - 提供公共接口操作私有数据 + +3. **继承** + - 子类继承父类的属性和方法 + - 使用`super()`调用父类方法 + - 方法重写实现多态 + - 多重继承和MRO(方法解析顺序) + +4. **多态** + - 同一接口,不同实现 + - 抽象基类定义接口规范 + - 运行时确定调用哪个方法 + +5. **特殊方法** + - 定制对象行为(`__str__`, `__repr__`等) + - 运算符重载(`__add__`, `__eq__`等) + - 上下文管理器(`__enter__`, `__exit__`) + +### 最佳实践 + +1. **设计原则** + - 单一职责:每个类只负责一个功能 + - 开闭原则:对扩展开放,对修改关闭 + - 里氏替换:子类可以替换父类 + - 依赖倒置:依赖抽象而不是具体实现 + +2. **命名规范** + - 类名使用大驼峰命名(PascalCase) + - 方法和属性使用小写加下划线(snake_case) + - 私有属性使用双下划线前缀 + - 常量使用全大写加下划线 + +3. **文档和测试** + - 为类和方法编写清晰的文档字符串 + - 使用类型注解提高代码可读性 + - 编写单元测试验证功能正确性 + +### 下一步学习方向 + +1. **设计模式** + - 单例模式、工厂模式、观察者模式等 + - 学习如何解决常见的设计问题 + +2. **高级特性** + - 元类(metaclass) + - 描述符(descriptor) + - 装饰器的高级用法 + +3. **实际应用** + - Web开发中的OOP应用 + - 数据库ORM的设计 + - GUI编程中的事件驱动 + +### 练习建议 + +1. **基础练习** + - 设计一个学生成绩管理系统 + - 实现一个简单的银行账户系统 + - 创建一个动物园管理系统 + +2. **进阶练习** + - 设计一个电商购物车系统 + - 实现一个简单的游戏角色系统 + - 创建一个文件管理器 + +3. **项目实战** + - 开发一个完整的图书管理系统 + - 实现一个简单的博客系统 + - 创建一个任务管理应用 + +面向对象编程是Python中非常重要的编程范式,掌握好OOP的概念和技巧,将为你后续学习更高级的Python特性和框架打下坚实的基础。记住,最好的学习方法就是多练习、多实践! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Python/img.png b/docs/Python/img.png new file mode 100644 index 000000000..2916942eb Binary files /dev/null and b/docs/Python/img.png differ diff --git a/docs/Python/img_1.png b/docs/Python/img_1.png new file mode 100644 index 000000000..b00c0bf5c Binary files /dev/null and b/docs/Python/img_1.png differ diff --git a/docs/Python/img_2.png b/docs/Python/img_2.png new file mode 100644 index 000000000..042200f5d Binary files /dev/null and b/docs/Python/img_2.png differ diff --git a/docs/Python/img_3.png b/docs/Python/img_3.png new file mode 100644 index 000000000..901429aa8 Binary files /dev/null and b/docs/Python/img_3.png differ diff --git a/docs/README.md b/docs/README.md index 3f20a5b48..869918f12 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,154 +1,434 @@ -> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 +# JavaPlus 技术文档平台 -## 感谢关注 Thanks for your attention ༼ つ ◕_◕ ༽つ +## 项目概述 - +本平台是一个战略性的Java技术知识体系,构建了从基础到高级的微服务分布式系统完整架构图谱。平台采用企业级文档标准,支持全文检索、多端适配,为组织内不同层级的技术人员(初学者、中级开发者、高级架构师、技术决策者)提供系统化的技术参考和能力提升路径。 +**平台价值**:提升团队技术能力,加速项目交付,降低技术决策风险,支持业务创新 - +**网站地址**:[https://webvueblog.github.io/JavaPlusDoc/](https://webvueblog.github.io/JavaPlusDoc/) +## 为爱发电 -会产品,会开发,会测试,会运维,会架构 +
+

🙏 支持JavaPlus技术文档平台

+

如果您觉得本文档对您的学习和工作有所帮助,欢迎扫描下方二维码进行打赏支持。您的每一份鼓励都是我们持续创作优质内容的动力!

+
+ 微信收款码 +
+

感谢您的支持与鼓励!

+

您的赞助将用于平台维护、内容更新与技术研究

+
+ Spring + Java + Redis + MySQL + Docker + K8s + Vue +
+
-网站:[https://webvueblog.github.io/JavaPlusDoc/](https://webvueblog.github.io/JavaPlusDoc/) +## 项目亮点 -![img_1.png](./img_1.png) +- **分层知识体系**:按照初级、中级、高级三个层次组织内容,满足不同水平读者需求 +- **全面技术覆盖**:从Java基础到分布式架构,全面覆盖企业级开发所需技术栈 +- **实用案例导向**:结合实际项目案例,提供可落地的最佳实践和解决方案 +- **图文并茂**:通过图表、代码示例和流程图直观展示复杂概念 +- **持续更新迭代**:定期更新技术内容,保持与行业最新发展同步 +- **企业级应用**:提供企业级架构设计方案和最佳实践,支持技术决策与创新 ----- +## 战略价值与业务贡献 - +- **技术能力建设**:系统化培养团队从初级到高级的技术梯队,支撑业务持续发展 +- **降低技术风险**:提供经过验证的架构方案和最佳实践,减少技术决策失误 +- **加速项目交付**:通过标准化技术方案和组件复用,显著提高研发效率 +- **促进技术创新**:整合行业前沿技术,为业务创新提供技术支撑 +- **知识资产沉淀**:将团队核心技术经验转化为可持续的组织知识资产 +## 核心技术体系 -#### 后端技术栈 +### 后端技术架构 -

+

Spring  Spring Boot  + Go  MySQL  - MariaDB  - PostgreSQL  - Oracle  - Microsoft SQL Server  Redis  MongoDB  RabbitMQ  - Solr  ElasticSearch  - Logstash  - Kibana  Kafka  - Consul  - Tomcat  - JUnit5  - Liquibase  - Maven  - Gradle  Spring Security  - Hibernate  - JSON  - JWT  Java  - Python  - Android  - Go  - GraphQL 

-#### 前端技术栈 +### 前端技术架构 -

+

Vue3  TypeScript  Ant Design  Node.js  Vite  - Webpack  - NPM  - Axios  - ESLint  - jQuery  - BootStrap  - ECharts  - JavaScript  - HTML5  - CSS3  - Tailwind CSS  - Less 

-#### DevOps +### 云原生 & DevOps 体系 -

- Git  - GitHub  - Gitee  - gitlab  - GitHub Actions  - Jenkins  - SonarQube  +

Docker  - Harbor  Kubernetes  - CentOS  - Ubuntu  -

- -#### 运维技术栈 - -

- 阿里云  + Jenkins  Nginx  - VMware  Prometheus  Grafana  - Ansible  - Lua  + Git 

-#### 测试技术栈 +## 精选技术文章 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +## 文档内容 + +本文档库按照技术深度和应用场景,将内容分为三个层次: + +| 层次 | 适合人群 | 主要内容 | 学习目标 | 应用场景 | +|------|---------|----------|---------|----------| +| **基础篇** | 初学者、转行人员 | Java基础语法、面向对象、集合框架、异常处理 | 掌握Java开发基础,能够独立完成简单功能开发 | 新员工培训、技术栈转型 | +| **进阶篇** | 中级开发者 | 多线程并发、JVM原理、设计模式、框架应用 | 深入理解Java核心机制,能够设计复杂业务模块 | 业务系统开发、性能优化 | +| **高级篇** | 高级开发者、架构师、技术管理者 | 分布式架构、高并发设计、性能优化、微服务治理 | 掌握系统架构设计,解决企业级技术难题 | 架构升级、系统重构、技术创新 | + +核心技术领域及其业务价值: + +|现有技术 | 主要内容 | 业务价值 | 应用场景 | +|---------|----------|----------|----------| +| **Java核心** | JVM原理、多线程并发、内存模型、性能调优 | 提升系统性能,降低资源成本 | 核心业务系统、高性能服务 | +| **GO语言** | 并发编程、网络服务、微服务架构、云原生应用 | 简化并发开发,提高开发效率 | 微服务、API网关、云原生应用 | +| **分布式架构** | 微服务设计、服务注册发现、负载均衡、熔断降级 | 支持业务快速扩展,提高系统可用性 | 大型业务平台、多区域部署 | +| **数据存储** | 关系型数据库、NoSQL、分库分表、数据一致性 | 保障数据安全,支持海量数据处理 | 用户数据分析、交易系统 | +| **中间件技术** | 消息队列、缓存、搜索引擎、任务调度 | 提高系统集成能力,增强业务弹性 | 跨系统集成、峰值流量应对 | +| **云原生** | 容器化、服务网格、Kubernetes、云平台 | 降低运维成本,提高资源利用率 | 弹性伸缩、混合云部署 | +| **DevOps** | CI/CD、自动化测试、监控告警、日志管理 | 加速交付周期,提高发布质量 | 持续集成/部署、自动化运维 | + +## 文档结构 + +文档采用多层次结构组织,便于不同层级技术人员查阅: + +``` +├── 基础技术体系 +│ ├── Java核心技术 +│ │ ├── 基本数据类型 +│ │ ├── 面向对象编程 +│ │ ├── 集合框架 +│ │ └── 异常处理 +│ ├── GO语言技术 +│ │ ├── 基础语法 +│ │ ├── 并发编程 +│ │ ├── 标准库应用 +│ │ └── Web服务开发 +│ ├── 开发环境搭建 +│ └── 编程规范与最佳实践 +├── 企业级应用框架 +│ ├── 微服务架构 +│ │ ├── Spring Cloud生态 +│ │ ├── 服务注册与发现 +│ │ ├── 配置中心 +│ │ └── API网关 +│ ├── 数据访问与存储 +│ │ ├── MySQL优化 +│ │ ├── Redis缓存 +│ │ └── 分库分表 +│ ├── 安全架构 +│ └── 性能优化 +└── 技术战略与创新 + ├── 架构演进路线 + │ ├── 单体到微服务 + │ ├── 传统部署到云原生 + │ └── 技术栈升级策略 + ├── 技术风险管控 + ├── 创新技术应用 + └── 技术团队建设 +``` + +## 使用指南 + +### 领导/管理者 + +1. **战略决策参考** + - 通过「战略价值与业务贡献」了解平台对业务的支撑作用 + - 参考「技术选型指南」进行技术投资决策 + - 利用「架构演进路线」规划技术发展方向 + +2. **团队建设工具** + - 基于「分层知识体系」制定团队培训计划 + - 使用「技术评估标准」进行人才评估和梯队建设 + - 通过「知识管理策略」促进团队知识沉淀和共享 + +3. **项目管理辅助** + - 利用「技术评审标准」提高项目质量 + - 参考「质量保障体系」降低项目风险 + - 通过「技术债务管理」优化长期技术投资 + +### 初学者 + +1. **入门学习路径** + - 从「基础篇」开始,按顺序学习Java基础知识 + - 通过实践案例巩固基础概念 + - 利用文档中的图表辅助理解抽象概念 + +2. **实践指导** + - 参考「编程规范」养成良好编码习惯 + - 学习「开发环境搭建」快速进入开发状态 + - 通过「常见问题解答」解决入门障碍 + +3. **进阶准备** + - 完成基础学习后,了解「进阶篇」知识图谱 + - 制定个人学习计划,为技术进阶做准备 + - 参与实践项目,应用所学知识 + +### 中级开发者 + +1. **深入学习** + - 重点关注「进阶篇」内容,深入理解Java高级特性 + - 学习主流框架的原理和最佳实践 + - 通过项目案例提升实际应用能力 + +2. **技术拓展** + - 学习「设计模式」提升代码设计能力 + - 掌握「性能优化」技巧提高系统效率 + - 了解「微服务基础」为架构升级做准备 + +3. **实践提升** + - 参与复杂业务模块的设计和开发 + - 解决实际项目中的技术难题 + - 尝试理解「高级篇」中的架构设计思想 + +### 高级开发者/架构师 + +1. **架构设计** + - 专注「高级篇」内容,掌握分布式系统设计原则 + - 研究性能优化和高可用架构方案 + - 学习「技术战略」内容,提升技术决策能力 + +2. **技术创新** + - 探索前沿技术在企业中的应用价值 + - 设计创新架构解决方案 + - 参与技术选型和架构评审 + +3. **团队引领** + - 指导团队成员技术成长 + - 推动技术最佳实践在团队中的应用 + - 参与文档内容的贡献和完善 -

- Postman  - JMeter  -

+## 团队协作与知识管理 -#### 开发工具 +本平台支持企业级知识管理: -

- Intellij IDEA  - Eclipse  - WebStorm  - PyCharm  - Android Studio  - VSCode  -

+- **知识资产化**:将团队经验转化为可复用的知识资产 +- **版本化管理**:基于Git的文档版本控制,支持变更追踪 +- **质量保障**:技术内容经过专家评审,确保准确性和实用性 +- **定制化支持**:可根据组织需求定制专属知识库 +- **持续演进**:建立反馈机制,持续优化内容质量 -#### 其他 +## 维护与更新策略 -

- Markdown  - WordPress  - GitHub Pages  - Adobe Photoshop  -

+平台由专业技术团队维护,采用企业级内容管理流程: + +- **定期更新计划**:每季度进行内容审核和更新 +- **技术趋势跟踪**:持续整合行业前沿技术和最佳实践 +- **用户反馈闭环**:建立反馈收集和内容优化的闭环机制 +- **版本发布规划**:重大更新按版本发布,提供变更说明 + +### 版本管理机制 + +- **主版本**:重大内容架构调整或技术体系更新(如v2.0、v3.0) +- **次版本**:新增技术领域或大量内容更新(如v1.1、v1.2) +- **修订版本**:内容修正和小规模更新(如v1.0.1、v1.0.2) +- **变更日志**:每次更新都提供详细的变更说明 + +### 内容定制与扩展 + +企业可以基于本平台进行定制化扩展: + +- **行业特化**:增加特定行业的技术应用案例 +- **企业实践**:整合企业内部最佳实践和技术标准 +- **团队培训**:定制化的培训课程和学习路径 +- **评估体系**:与企业人才评估体系对接 + +欢迎通过[Issues](https://github.com/webVueBlog/JavaPlusDoc/issues)提交反馈和建议,帮助我们持续提升平台价值。 + +### 技术理念 + +> 进一寸有一寸的欣喜 —— 持续学习,每天进步一点点! + +作者致力于技术的学习与分享,通过开源项目和技术文章帮助更多开发者成长。欢迎通过GitHub、掘金等与作者交流,共同进步! +术战略」内容,提升技术决策能力 + +2. **技术创新** + - 探索前沿技术在企业中的应用价值 + - 设计创新架构解决方案 + - 参与技术选型和架构评审 + +3. **团队引领** + - 指导团队成员技术成长 + - 推动技术最佳实践在团队中的应用 + - 参与文档内容的贡献和完善 + +## 团队协作与知识管理 + +本平台支持企业级知识管理: + +- **知识资产化**:将团队经验转化为可复用的知识资产 +- **版本化管理**:基于Git的文档版本控制,支持变更追踪 +- **质量保障**:技术内容经过专家评审,确保准确性和实用性 +- **定制化支持**:可根据组织需求定制专属知识库 +- **持续演进**:建立反馈机制,持续优化内容质量 + +## 维护与更新策略 +平台由专业技术团队维护,采用企业级内容管理流程: -## 学前必读 +- **定期更新计划**:每季度进行内容审核和更新 +- **技术趋势跟踪**:持续整合行业前沿技术和最佳实践 +- **用户反馈闭环**:建立反馈收集和内容优化的闭环机制 +- **版本发布规划**:重大更新按版本发布,提供变更说明 -哪吒希望能为开发人员提供最大程度的愉悦开发体验。提供便捷的阅读文档,帮助开发小团体高效率的工作进度,并维护本站架构文档。 +### 版本管理机制 -## 留言评论 +- **主版本**:重大内容架构调整或技术体系更新(如v2.0、v3.0) +- **次版本**:新增技术领域或大量内容更新(如v1.1、v1.2) +- **修订版本**:内容修正和小规模更新(如v1.0.1、v1.0.2) +- **变更日志**:每次更新都提供详细的变更说明 -因为目前没有留言功能,请拉到文章底部,跳转到对应的 Github Issues,在 Issues 留言回复。 +### 内容定制与扩展 - +企业可以基于本平台进行定制化扩展: -## 感谢指正 +- **行业特化**:增加特定行业的技术应用案例 +- **企业实践**:整合企业内部最佳实践和技术标准 +- **团队培训**:定制化的培训课程和学习路径 +- **评估体系**:与企业人才评估体系对接 -指正不胜感激,无以回报。 +欢迎通过[Issues](https://github.com/webVueBlog/JavaPlusDoc/issues)提交反馈和建议,帮助我们持续提升平台价值。 -## 声明 +### 技术理念 -文档仅适合本人食用!!! +> 进一寸有一寸的欣喜 —— 持续学习,每天进步一点点! - +作者致力于技术的学习与分享,通过开源项目和技术文章帮助更多开发者成长。欢迎通过GitHub、掘金等与作者交流,共同进步! diff --git "a/docs/aJava/Ansible\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/Ansible\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..484dbf121 --- /dev/null +++ "b/docs/aJava/Ansible\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,9 @@ +# Ansible是什么 + +Ansible是什么, 自动化运维 + +能让你用一句话控制成百上千台服务器 + +底层靠SSH协议通信,Yaml语言写“剧本”,把复杂操作变成可重复的代码。核心技术叫“幂等性”,同一个任务重复执行。结果永远一致,比如批量安装软件。 + +Ansible会自动检测是否装过,避免重复劳动,批量部署,自动化运维,紧急修复漏洞,关键组件:Inventory定义服务器清单,modules是现成的功能模块。Playbook是任务剧本,roles是模块化角色集合。 \ No newline at end of file diff --git "a/docs/aJava/CDN\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/CDN\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..a64d8f457 --- /dev/null +++ "b/docs/aJava/CDN\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,26 @@ +# CDN是什么 + +CDN即内容分发网络,本质上是个“内容快递网络”,是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。 + +CDN,它把源站的数据提前缓存到离用户最近的节点,用空间换时间,用分布抗压力,文本数据用关系型数据库+Redis的组合拳,文件数据就要用对象存储+CDN的组合拳。就像select * from user where id = 1,我们可以用redis缓存,而不用每次去后端查询数据库。从而减少了源库的压力。 + +为什么redis不能存储图片呢?想象一下10GB的Redis实例存10万张图,光是维护key列表就能让内容爆炸,而CDN的边缘节点自带硬盘存储,专门为海量文件而设计,CDN架构: + +1. 第一层调度系统,像导航地图,用DNS解析+IP定位,把用户定位到离他最近的节点。 +2. 边缘节点,相当于遍历全国的快递网点,用SSD硬盘+内存缓存高频内容。最近用过的资源优先保留。 +3. 第三层回源机制,当本地也没存货时,用HTTP/2快速从源站拉数据,还能自动平衡多个源站压力 + +CDN实际解决问题:输入网址,首先电脑会根据访问的域名查看浏览器缓存;再看操作系统缓 名的解析,这时云厂商的DNS调度系统,根据你的IP+运营商+节点负载,动态分配“最近最优”的CDN服务器IP给你访问,这个IP其实就是个CDN边缘节点IP。只是这个节点离你最近,访问比较快而已。 + +当你第一次访问时,边缘节点会检查缓存版本,如果有存货且未过期,直接这个节点给你闪电响应;如果过期了,就秒回源站拿最新版本,同时更新本地库存。 + +预热办法:上线前,用curl命令去提前访问,并把热数据灌满CDN节点,避免大批流量访问。 + +场景使用: + +1. 全球用户访问的网站,跨境电商 +2. 突发流量场景(新品发布,抽奖活动) +3. 大文件分发(游戏更新包,4K视频) +4. 需要隐藏源站IP的敏感业务 + +因为redis的存储是基于内存的,内存是有限的,而图片是基于磁盘的,磁盘是无限的。所以redis不能存储图片。 diff --git "a/docs/aJava/ClickHouse\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/ClickHouse\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..58777f709 --- /dev/null +++ "b/docs/aJava/ClickHouse\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,1498 @@ +# ClickHouse分片技术实现 + +## 概述 + +ClickHouse是一个用于在线分析处理(OLAP)的列式数据库管理系统,具有极高的查询性能。ClickHouse的分片机制通过分布式表和本地表的组合实现水平扩展,支持PB级数据的实时分析。 + +## ClickHouse架构 + +### 核心概念 + +- **Shard**: 数据分片,每个分片包含部分数据 +- **Replica**: 副本,提供数据冗余和高可用 +- **Distributed Table**: 分布式表,查询入口 +- **Local Table**: 本地表,实际存储数据 +- **ZooKeeper**: 协调服务,管理副本同步 + +### 分片架构图 + +``` +分布式表 (Distributed) +├── Shard 1 +│ ├── Replica 1 (本地表) +│ └── Replica 2 (本地表) +├── Shard 2 +│ ├── Replica 1 (本地表) +│ └── Replica 2 (本地表) +└── Shard N + ├── Replica 1 (本地表) + └── Replica 2 (本地表) +``` + +## 环境搭建 + +### Docker Compose配置 + +```yaml +version: '3.8' +services: + zookeeper: + image: zookeeper:3.7 + container_name: clickhouse-zookeeper + ports: + - "2181:2181" + environment: + ZOO_MY_ID: 1 + ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 + volumes: + - zk_data:/data + - zk_logs:/datalog + + clickhouse-01: + image: clickhouse/clickhouse-server:23.8 + container_name: clickhouse-01 + hostname: clickhouse-01 + ports: + - "8123:8123" # HTTP接口 + - "9000:9000" # Native接口 + volumes: + - ./config/clickhouse-01:/etc/clickhouse-server + - ch01_data:/var/lib/clickhouse + - ch01_logs:/var/log/clickhouse-server + depends_on: + - zookeeper + ulimits: + nofile: + soft: 262144 + hard: 262144 + + clickhouse-02: + image: clickhouse/clickhouse-server:23.8 + container_name: clickhouse-02 + hostname: clickhouse-02 + ports: + - "8124:8123" # HTTP接口 + - "9001:9000" # Native接口 + volumes: + - ./config/clickhouse-02:/etc/clickhouse-server + - ch02_data:/var/lib/clickhouse + - ch02_logs:/var/log/clickhouse-server + depends_on: + - zookeeper + ulimits: + nofile: + soft: 262144 + hard: 262144 + + clickhouse-03: + image: clickhouse/clickhouse-server:23.8 + container_name: clickhouse-03 + hostname: clickhouse-03 + ports: + - "8125:8123" # HTTP接口 + - "9002:9000" # Native接口 + volumes: + - ./config/clickhouse-03:/etc/clickhouse-server + - ch03_data:/var/lib/clickhouse + - ch03_logs:/var/log/clickhouse-server + depends_on: + - zookeeper + ulimits: + nofile: + soft: 262144 + hard: 262144 + + clickhouse-04: + image: clickhouse/clickhouse-server:23.8 + container_name: clickhouse-04 + hostname: clickhouse-04 + ports: + - "8126:8123" # HTTP接口 + - "9003:9000" # Native接口 + volumes: + - ./config/clickhouse-04:/etc/clickhouse-server + - ch04_data:/var/lib/clickhouse + - ch04_logs:/var/log/clickhouse-server + depends_on: + - zookeeper + ulimits: + nofile: + soft: 262144 + hard: 262144 + +volumes: + zk_data: + zk_logs: + ch01_data: + ch01_logs: + ch02_data: + ch02_logs: + ch03_data: + ch03_logs: + ch04_data: + ch04_logs: +``` + +### ClickHouse配置文件 + +#### config/clickhouse-01/config.xml + +```xml + + + + information + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 1000M + 10 + + + 8123 + 9000 + 9004 + 9005 + + 0.0.0.0 + + 4096 + 3 + 100 + 8589934592 + 5368709120 + + /var/lib/clickhouse/ + /var/lib/clickhouse/tmp/ + /var/lib/clickhouse/user_files/ + /var/lib/clickhouse/access/ + + users.xml + + default + default + + Asia/Shanghai + + true + + + + + + clickhouse-01 + 9000 + + + clickhouse-02 + 9000 + + + + + clickhouse-03 + 9000 + + + clickhouse-04 + 9000 + + + + + + + + zookeeper + 2181 + + + + + 01 + replica_1 + + + + /clickhouse/task_queue/ddl + + + + + lz4 + + + +``` + +### 初始化脚本 + +```bash +#!/bin/bash +# init-clickhouse.sh + +echo "创建ClickHouse配置目录..." +mkdir -p config/clickhouse-01 config/clickhouse-02 config/clickhouse-03 config/clickhouse-04 + +# 生成配置文件(每个节点的shard和replica不同) +generate_config() { + local node=$1 + local shard=$2 + local replica=$3 + + # 复制基础配置 + cp config/clickhouse-01/config.xml config/clickhouse-$node/config.xml + + # 修改macros + sed -i "s/01<\/shard>/$shard<\/shard>/g" config/clickhouse-$node/config.xml + sed -i "s/replica_1<\/replica>/$replica<\/replica>/g" config/clickhouse-$node/config.xml + + # 复制用户配置 + cp config/clickhouse-01/users.xml config/clickhouse-$node/users.xml +} + +generate_config "02" "01" "replica_2" +generate_config "03" "02" "replica_1" +generate_config "04" "02" "replica_2" + +echo "启动ClickHouse集群..." +docker-compose up -d + +echo "等待服务启动..." +sleep 30 + +echo "创建分片表结构..." +docker exec -it clickhouse-01 clickhouse-client --query " +CREATE DATABASE IF EXISTS analytics ON CLUSTER cluster_2shards_2replicas; + +-- 创建本地表 +CREATE TABLE analytics.events_local ON CLUSTER cluster_2shards_2replicas +( + event_id UInt64, + user_id UInt64, + event_type String, + event_time DateTime, + properties Map(String, String) +) +ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}') +PARTITION BY toYYYYMM(event_time) +ORDER BY (user_id, event_time) +SETTINGS index_granularity = 8192; + +-- 创建分布式表 +CREATE TABLE analytics.events ON CLUSTER cluster_2shards_2replicas +( + event_id UInt64, + user_id UInt64, + event_type String, + event_time DateTime, + properties Map(String, String) +) +ENGINE = Distributed(cluster_2shards_2replicas, analytics, events_local, sipHash64(user_id)); +" + +echo "ClickHouse集群初始化完成" +echo "ClickHouse节点访问地址:" +echo " 节点1: http://localhost:8123" +echo " 节点2: http://localhost:8124" +echo " 节点3: http://localhost:8125" +echo " 节点4: http://localhost:8126" +``` + +## Java应用集成 + +### Maven依赖 + +```xml + + + com.clickhouse + clickhouse-jdbc + 0.4.6 + + + com.clickhouse + clickhouse-http-client + 0.4.6 + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba + druid-spring-boot-starter + 1.2.18 + + +``` + +### Spring Boot配置 + +```java +@Configuration +@EnableConfigurationProperties(ClickHouseProperties.class) +public class ClickHouseConfig { + + @Autowired + private ClickHouseProperties clickHouseProperties; + + @Bean + @Primary + public DataSource clickHouseDataSource() { + DruidDataSource dataSource = new DruidDataSource(); + + // 连接配置 + dataSource.setUrl(clickHouseProperties.getUrl()); + dataSource.setUsername(clickHouseProperties.getUsername()); + dataSource.setPassword(clickHouseProperties.getPassword()); + dataSource.setDriverClassName("com.clickhouse.jdbc.ClickHouseDriver"); + + // 连接池配置 + dataSource.setInitialSize(clickHouseProperties.getInitialSize()); + dataSource.setMaxActive(clickHouseProperties.getMaxActive()); + dataSource.setMinIdle(clickHouseProperties.getMinIdle()); + dataSource.setMaxWait(clickHouseProperties.getMaxWait()); + + // ClickHouse特定配置 + dataSource.addConnectionProperty("socket_timeout", "300000"); + dataSource.addConnectionProperty("connection_timeout", "10000"); + dataSource.addConnectionProperty("compress", "true"); + dataSource.addConnectionProperty("decompress", "true"); + + return dataSource; + } + + @Bean + public JdbcTemplate clickHouseJdbcTemplate(@Qualifier("clickHouseDataSource") DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setQueryTimeout(300); // 5分钟超时 + return jdbcTemplate; + } +} + +@ConfigurationProperties(prefix = "clickhouse") +@Data +public class ClickHouseProperties { + private String url = "jdbc:clickhouse://localhost:8123/analytics"; + private String username = "default"; + private String password = ""; + private int initialSize = 5; + private int maxActive = 20; + private int minIdle = 5; + private long maxWait = 60000; +} +``` + +### ClickHouse操作服务 + +```java +@Service +@Slf4j +public class ClickHouseService { + + @Autowired + private JdbcTemplate clickHouseJdbcTemplate; + + /** + * 批量插入事件数据 + */ + public void batchInsertEvents(List events) { + String sql = "INSERT INTO analytics.events (event_id, user_id, event_type, event_time, properties) VALUES (?, ?, ?, ?, ?)"; + + List batchArgs = events.stream() + .map(event -> new Object[]{ + event.getEventId(), + event.getUserId(), + event.getEventType(), + event.getEventTime(), + event.getProperties() + }) + .collect(Collectors.toList()); + + try { + int[] results = clickHouseJdbcTemplate.batchUpdate(sql, batchArgs); + log.info("批量插入事件数据: {} 条", results.length); + + } catch (Exception e) { + log.error("批量插入事件数据失败", e); + throw new RuntimeException(e); + } + } + + /** + * 查询用户事件统计 + */ + public List getUserEventStats(Long userId, LocalDate startDate, LocalDate endDate) { + String sql = """ + SELECT + user_id, + event_type, + count() as event_count, + uniq(event_id) as unique_events, + min(event_time) as first_event, + max(event_time) as last_event + FROM analytics.events + WHERE user_id = ? + AND toDate(event_time) BETWEEN ? AND ? + GROUP BY user_id, event_type + ORDER BY event_count DESC + """; + + try { + return clickHouseJdbcTemplate.query(sql, + new Object[]{userId, startDate, endDate}, + (rs, rowNum) -> UserEventStats.builder() + .userId(rs.getLong("user_id")) + .eventType(rs.getString("event_type")) + .eventCount(rs.getLong("event_count")) + .uniqueEvents(rs.getLong("unique_events")) + .firstEvent(rs.getTimestamp("first_event").toLocalDateTime()) + .lastEvent(rs.getTimestamp("last_event").toLocalDateTime()) + .build() + ); + + } catch (Exception e) { + log.error("查询用户事件统计失败", e); + throw new RuntimeException(e); + } + } + + /** + * 实时事件分析 + */ + public List getRealTimeEventAnalytics(int minutes) { + String sql = """ + SELECT + event_type, + count() as total_events, + uniq(user_id) as unique_users, + avg(toUnixTimestamp(now()) - toUnixTimestamp(event_time)) as avg_delay_seconds + FROM analytics.events + WHERE event_time >= now() - INTERVAL ? MINUTE + GROUP BY event_type + ORDER BY total_events DESC + """; + + try { + return clickHouseJdbcTemplate.query(sql, + new Object[]{minutes}, + (rs, rowNum) -> EventAnalytics.builder() + .eventType(rs.getString("event_type")) + .totalEvents(rs.getLong("total_events")) + .uniqueUsers(rs.getLong("unique_users")) + .avgDelaySeconds(rs.getDouble("avg_delay_seconds")) + .build() + ); + + } catch (Exception e) { + log.error("查询实时事件分析失败", e); + throw new RuntimeException(e); + } + } + + /** + * 漏斗分析 + */ + public FunnelAnalysisResult getFunnelAnalysis(List steps, int windowHours) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT \n"); + + // 构建漏斗查询 + for (int i = 0; i < steps.size(); i++) { + if (i > 0) sql.append(",\n"); + sql.append(String.format( + " countIf(event_type = '%s') as step_%d", + steps.get(i), i + 1 + )); + } + + sql.append("\nFROM (\n"); + sql.append(" SELECT user_id, event_type, event_time\n"); + sql.append(" FROM analytics.events\n"); + sql.append(" WHERE event_type IN ("); + + for (int i = 0; i < steps.size(); i++) { + if (i > 0) sql.append(", "); + sql.append("'").append(steps.get(i)).append("'"); + } + + sql.append(")\n"); + sql.append(" AND event_time >= now() - INTERVAL ? HOUR\n"); + sql.append(" ORDER BY user_id, event_time\n"); + sql.append(") GROUP BY user_id\n"); + sql.append("HAVING step_1 > 0"); + + try { + List> results = clickHouseJdbcTemplate.queryForList( + sql.toString(), windowHours + ); + + return buildFunnelResult(steps, results); + + } catch (Exception e) { + log.error("漏斗分析查询失败", e); + throw new RuntimeException(e); + } + } + + private FunnelAnalysisResult buildFunnelResult(List steps, List> results) { + List funnelSteps = new ArrayList<>(); + + for (int i = 0; i < steps.size(); i++) { + String stepKey = "step_" + (i + 1); + long userCount = results.stream() + .mapToLong(row -> ((Number) row.get(stepKey)).longValue()) + .sum(); + + double conversionRate = i == 0 ? 100.0 : + (double) userCount / funnelSteps.get(0).getUserCount() * 100; + + funnelSteps.add(FunnelStep.builder() + .stepName(steps.get(i)) + .userCount(userCount) + .conversionRate(conversionRate) + .build()); + } + + return FunnelAnalysisResult.builder() + .steps(funnelSteps) + .totalUsers(funnelSteps.get(0).getUserCount()) + .overallConversionRate( + (double) funnelSteps.get(funnelSteps.size() - 1).getUserCount() / + funnelSteps.get(0).getUserCount() * 100 + ) + .build(); + } +} +``` + +### 分片管理服务 + +```java +@Service +@Slf4j +public class ClickHouseShardingService { + + @Autowired + private JdbcTemplate clickHouseJdbcTemplate; + + /** + * 获取集群信息 + */ + public ClusterInfo getClusterInfo() { + String sql = """ + SELECT + cluster, + shard_num, + replica_num, + host_name, + port, + is_local, + user, + errors_count, + slowdowns_count + FROM system.clusters + WHERE cluster = 'cluster_2shards_2replicas' + ORDER BY shard_num, replica_num + """; + + try { + List nodes = clickHouseJdbcTemplate.query(sql, + (rs, rowNum) -> ClusterNode.builder() + .cluster(rs.getString("cluster")) + .shardNum(rs.getInt("shard_num")) + .replicaNum(rs.getInt("replica_num")) + .hostName(rs.getString("host_name")) + .port(rs.getInt("port")) + .isLocal(rs.getBoolean("is_local")) + .user(rs.getString("user")) + .errorsCount(rs.getLong("errors_count")) + .slowdownsCount(rs.getLong("slowdowns_count")) + .build() + ); + + return ClusterInfo.builder() + .clusterName("cluster_2shards_2replicas") + .nodes(nodes) + .shardCount(nodes.stream().mapToInt(ClusterNode::getShardNum).max().orElse(0)) + .replicaCount(nodes.stream().mapToInt(ClusterNode::getReplicaNum).max().orElse(0)) + .build(); + + } catch (Exception e) { + log.error("获取集群信息失败", e); + throw new RuntimeException(e); + } + } + + /** + * 获取表分片分布 + */ + public List getTableShardDistribution(String database, String table) { + String sql = """ + SELECT + database, + table, + partition, + name, + rows, + bytes_on_disk, + data_compressed_bytes, + data_uncompressed_bytes, + marks, + modification_time + FROM system.parts + WHERE database = ? AND table = ? + AND active = 1 + ORDER BY partition, name + """; + + try { + return clickHouseJdbcTemplate.query(sql, + new Object[]{database, table}, + (rs, rowNum) -> TableShardInfo.builder() + .database(rs.getString("database")) + .table(rs.getString("table")) + .partition(rs.getString("partition")) + .partName(rs.getString("name")) + .rows(rs.getLong("rows")) + .bytesOnDisk(rs.getLong("bytes_on_disk")) + .dataCompressedBytes(rs.getLong("data_compressed_bytes")) + .dataUncompressedBytes(rs.getLong("data_uncompressed_bytes")) + .marks(rs.getLong("marks")) + .modificationTime(rs.getTimestamp("modification_time").toLocalDateTime()) + .build() + ); + + } catch (Exception e) { + log.error("获取表分片分布失败", e); + throw new RuntimeException(e); + } + } + + /** + * 优化表 + */ + public void optimizeTable(String database, String table) { + String sql = String.format("OPTIMIZE TABLE %s.%s ON CLUSTER cluster_2shards_2replicas", + database, table); + + try { + clickHouseJdbcTemplate.execute(sql); + log.info("优化表完成: {}.{}", database, table); + + } catch (Exception e) { + log.error("优化表失败: {}.{}", database, table, e); + throw new RuntimeException(e); + } + } + + /** + * 删除分区 + */ + public void dropPartition(String database, String table, String partition) { + String sql = String.format( + "ALTER TABLE %s.%s ON CLUSTER cluster_2shards_2replicas DROP PARTITION '%s'", + database, table, partition + ); + + try { + clickHouseJdbcTemplate.execute(sql); + log.info("删除分区完成: {}.{} partition {}", database, table, partition); + + } catch (Exception e) { + log.error("删除分区失败: {}.{} partition {}", database, table, partition, e); + throw new RuntimeException(e); + } + } + + /** + * 监控分片负载 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorShardLoad() { + try { + String sql = """ + SELECT + hostname() as host, + database, + table, + sum(rows) as total_rows, + sum(bytes_on_disk) as total_bytes, + count() as part_count + FROM system.parts + WHERE active = 1 + AND database = 'analytics' + GROUP BY hostname(), database, table + ORDER BY total_bytes DESC + """; + + List loadInfos = clickHouseJdbcTemplate.query(sql, + (rs, rowNum) -> ShardLoadInfo.builder() + .host(rs.getString("host")) + .database(rs.getString("database")) + .table(rs.getString("table")) + .totalRows(rs.getLong("total_rows")) + .totalBytes(rs.getLong("total_bytes")) + .partCount(rs.getInt("part_count")) + .build() + ); + + log.info("=== ClickHouse分片负载监控 ==="); + loadInfos.forEach(info -> { + log.info("主机: {}, 表: {}.{}, 行数: {}, 大小: {} MB, 分区数: {}", + info.getHost(), info.getDatabase(), info.getTable(), + info.getTotalRows(), info.getTotalBytes() / 1024 / 1024, + info.getPartCount()); + }); + + // 检查负载不均衡 + checkLoadBalance(loadInfos); + + } catch (Exception e) { + log.error("监控分片负载失败", e); + } + } + + private void checkLoadBalance(List loadInfos) { + Map> byTable = loadInfos.stream() + .collect(Collectors.groupingBy(info -> info.getDatabase() + "." + info.getTable())); + + byTable.forEach((table, infos) -> { + if (infos.size() > 1) { + long maxBytes = infos.stream().mapToLong(ShardLoadInfo::getTotalBytes).max().orElse(0); + long minBytes = infos.stream().mapToLong(ShardLoadInfo::getTotalBytes).min().orElse(0); + + if (maxBytes > 0 && (double) (maxBytes - minBytes) / maxBytes > 0.3) { + log.warn("表 {} 分片负载不均衡,最大: {} MB, 最小: {} MB", + table, maxBytes / 1024 / 1024, minBytes / 1024 / 1024); + } + } + }); + } +} +``` + +## 性能优化策略 + +### 1. 分片键选择 + +```java +@Component +public class ShardingKeyOptimizer { + + /** + * 用户ID分片(均匀分布) + */ + public String getUserShardingKey() { + return "sipHash64(user_id)"; + } + + /** + * 时间+用户ID复合分片 + */ + public String getTimeUserShardingKey() { + return "sipHash64(concat(toString(toYYYYMM(event_time)), toString(user_id)))"; + } + + /** + * 随机分片(最均匀但失去局部性) + */ + public String getRandomShardingKey() { + return "rand()"; + } + + /** + * 分析分片键分布 + */ + public ShardingAnalysis analyzeShardingDistribution(String table, String shardingKey) { + String sql = String.format(""" + SELECT + %s %% 100 as shard_bucket, + count() as record_count, + sum(bytes_on_disk) as total_bytes + FROM %s + GROUP BY shard_bucket + ORDER BY shard_bucket + """, shardingKey, table); + + // 执行查询并分析分布均匀性 + // 返回分析结果 + return new ShardingAnalysis(); + } +} +``` + +### 2. 查询优化 + +```java +@Service +public class ClickHouseQueryOptimizer { + + @Autowired + private JdbcTemplate clickHouseJdbcTemplate; + + /** + * 并行查询优化 + */ + public List parallelQuery(String baseQuery, List conditions, + RowMapper rowMapper) { + List>> futures = conditions.parallelStream() + .map(condition -> CompletableFuture.supplyAsync(() -> { + String sql = baseQuery + " WHERE " + condition; + return clickHouseJdbcTemplate.query(sql, rowMapper); + })) + .collect(Collectors.toList()); + + return futures.stream() + .map(CompletableFuture::join) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + /** + * 预聚合查询优化 + */ + public void createMaterializedView(String viewName, String sourceTable, String aggregation) { + String sql = String.format(""" + CREATE MATERIALIZED VIEW %s ON CLUSTER cluster_2shards_2replicas + ENGINE = SummingMergeTree() + PARTITION BY toYYYYMM(event_date) + ORDER BY (event_date, event_type) + AS SELECT + toDate(event_time) as event_date, + event_type, + %s + FROM %s + GROUP BY event_date, event_type + """, viewName, aggregation, sourceTable); + + try { + clickHouseJdbcTemplate.execute(sql); + log.info("创建物化视图成功: {}", viewName); + + } catch (Exception e) { + log.error("创建物化视图失败: {}", viewName, e); + throw new RuntimeException(e); + } + } + + /** + * 查询计划分析 + */ + public QueryPlan analyzeQuery(String query) { + String explainSql = "EXPLAIN PLAN " + query; + + try { + List> plan = clickHouseJdbcTemplate.queryForList(explainSql); + + return QueryPlan.builder() + .originalQuery(query) + .executionPlan(plan) + .estimatedRows(extractEstimatedRows(plan)) + .estimatedCost(extractEstimatedCost(plan)) + .build(); + + } catch (Exception e) { + log.error("分析查询计划失败", e); + throw new RuntimeException(e); + } + } + + private long extractEstimatedRows(List> plan) { + // 从执行计划中提取预估行数 + return 0; + } + + private double extractEstimatedCost(List> plan) { + // 从执行计划中提取预估成本 + return 0.0; + } +} +``` + +### 3. 批量写入优化 + +```java +@Service +public class ClickHouseBatchWriter { + + @Autowired + private JdbcTemplate clickHouseJdbcTemplate; + + private static final int BATCH_SIZE = 10000; + private final BlockingQueue writeQueue = new LinkedBlockingQueue<>(100000); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + + @PostConstruct + public void startBatchWriter() { + // 定时批量写入 + scheduler.scheduleAtFixedRate(this::flushBatch, 5, 5, TimeUnit.SECONDS); + + // 队列满时强制写入 + scheduler.scheduleAtFixedRate(this::checkQueueSize, 1, 1, TimeUnit.SECONDS); + } + + /** + * 异步写入事件 + */ + public boolean writeEventAsync(EventData event) { + try { + return writeQueue.offer(event, 100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * 批量刷新 + */ + private void flushBatch() { + List batch = new ArrayList<>(); + writeQueue.drainTo(batch, BATCH_SIZE); + + if (!batch.isEmpty()) { + try { + batchInsert(batch); + log.debug("批量写入完成: {} 条记录", batch.size()); + } catch (Exception e) { + log.error("批量写入失败: {} 条记录", batch.size(), e); + // 重新入队或写入失败队列 + handleWriteFailure(batch); + } + } + } + + private void checkQueueSize() { + if (writeQueue.size() > 80000) { // 80%阈值 + log.warn("写入队列接近满载: {}", writeQueue.size()); + flushBatch(); + } + } + + private void batchInsert(List events) { + String sql = "INSERT INTO analytics.events (event_id, user_id, event_type, event_time, properties) VALUES"; + + StringBuilder values = new StringBuilder(); + for (int i = 0; i < events.size(); i++) { + if (i > 0) values.append(","); + EventData event = events.get(i); + values.append(String.format( + "(%d, %d, '%s', '%s', %s)", + event.getEventId(), + event.getUserId(), + event.getEventType(), + event.getEventTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + formatProperties(event.getProperties()) + )); + } + + clickHouseJdbcTemplate.execute(sql + values.toString()); + } + + private String formatProperties(Map properties) { + if (properties == null || properties.isEmpty()) { + return "{}"; + } + + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : properties.entrySet()) { + if (!first) sb.append(","); + sb.append("'").append(entry.getKey()).append("':'").append(entry.getValue()).append("'"); + first = false; + } + sb.append("}"); + return sb.toString(); + } + + private void handleWriteFailure(List batch) { + // 实现失败重试逻辑 + log.error("处理写入失败的批次: {} 条记录", batch.size()); + } + + @PreDestroy + public void shutdown() { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + + // 刷新剩余数据 + flushBatch(); + } +} +``` + +## 监控和运维 + +### 1. 集群监控服务 + +```java +@Service +@Slf4j +public class ClickHouseMonitoringService { + + @Autowired + private JdbcTemplate clickHouseJdbcTemplate; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控集群状态 + */ + @Scheduled(fixedRate = 60000) // 1分钟 + public void monitorClusterStatus() { + try { + // 检查集群节点状态 + String sql = """ + SELECT + cluster, + shard_num, + replica_num, + host_name, + port, + errors_count, + slowdowns_count + FROM system.clusters + WHERE cluster = 'cluster_2shards_2replicas' + """; + + List> nodes = clickHouseJdbcTemplate.queryForList(sql); + + // 记录指标 + Gauge.builder("clickhouse.cluster.nodes") + .register(meterRegistry, nodes.size()); + + long totalErrors = nodes.stream() + .mapToLong(node -> ((Number) node.get("errors_count")).longValue()) + .sum(); + + Gauge.builder("clickhouse.cluster.errors") + .register(meterRegistry, totalErrors); + + // 检查异常节点 + nodes.stream() + .filter(node -> ((Number) node.get("errors_count")).longValue() > 0) + .forEach(node -> { + log.warn("节点异常: {}:{}, 错误数: {}", + node.get("host_name"), node.get("port"), + node.get("errors_count")); + }); + + } catch (Exception e) { + log.error("监控集群状态失败", e); + } + } + + /** + * 监控查询性能 + */ + @Scheduled(fixedRate = 120000) // 2分钟 + public void monitorQueryPerformance() { + try { + String sql = """ + SELECT + query_duration_ms, + read_rows, + read_bytes, + written_rows, + written_bytes, + memory_usage, + query + FROM system.query_log + WHERE event_time >= now() - INTERVAL 2 MINUTE + AND type = 'QueryFinish' + AND query_duration_ms > 1000 + ORDER BY query_duration_ms DESC + LIMIT 10 + """; + + List> slowQueries = clickHouseJdbcTemplate.queryForList(sql); + + if (!slowQueries.isEmpty()) { + log.warn("发现慢查询: {} 条", slowQueries.size()); + + slowQueries.forEach(query -> { + log.warn("慢查询 - 耗时: {}ms, 读取行数: {}, 内存使用: {} MB", + query.get("query_duration_ms"), + query.get("read_rows"), + ((Number) query.get("memory_usage")).longValue() / 1024 / 1024); + }); + } + + // 记录平均查询性能 + recordAverageQueryMetrics(); + + } catch (Exception e) { + log.error("监控查询性能失败", e); + } + } + + private void recordAverageQueryMetrics() { + String sql = """ + SELECT + avg(query_duration_ms) as avg_duration, + avg(read_rows) as avg_read_rows, + avg(memory_usage) as avg_memory + FROM system.query_log + WHERE event_time >= now() - INTERVAL 5 MINUTE + AND type = 'QueryFinish' + """; + + try { + Map metrics = clickHouseJdbcTemplate.queryForMap(sql); + + Gauge.builder("clickhouse.query.avg_duration_ms") + .register(meterRegistry, ((Number) metrics.get("avg_duration")).doubleValue()); + + Gauge.builder("clickhouse.query.avg_read_rows") + .register(meterRegistry, ((Number) metrics.get("avg_read_rows")).doubleValue()); + + Gauge.builder("clickhouse.query.avg_memory_mb") + .register(meterRegistry, ((Number) metrics.get("avg_memory")).doubleValue() / 1024 / 1024); + + } catch (Exception e) { + log.debug("记录查询指标失败", e); + } + } + + /** + * 监控存储使用情况 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorStorageUsage() { + try { + String sql = """ + SELECT + database, + table, + sum(rows) as total_rows, + sum(bytes_on_disk) as total_bytes, + sum(data_compressed_bytes) as compressed_bytes, + sum(data_uncompressed_bytes) as uncompressed_bytes + FROM system.parts + WHERE active = 1 + AND database = 'analytics' + GROUP BY database, table + ORDER BY total_bytes DESC + """; + + List> tables = clickHouseJdbcTemplate.queryForList(sql); + + log.info("=== ClickHouse存储使用情况 ==="); + tables.forEach(table -> { + long totalBytes = ((Number) table.get("total_bytes")).longValue(); + long compressedBytes = ((Number) table.get("compressed_bytes")).longValue(); + double compressionRatio = compressedBytes > 0 ? + (double) ((Number) table.get("uncompressed_bytes")).longValue() / compressedBytes : 0; + + log.info("表: {}.{}, 行数: {}, 大小: {} GB, 压缩比: {:.2f}", + table.get("database"), table.get("table"), + table.get("total_rows"), totalBytes / 1024 / 1024 / 1024, + compressionRatio); + + // 记录指标 + Tags tags = Tags.of( + "database", table.get("database").toString(), + "table", table.get("table").toString() + ); + + Gauge.builder("clickhouse.table.rows") + .tags(tags) + .register(meterRegistry, ((Number) table.get("total_rows")).doubleValue()); + + Gauge.builder("clickhouse.table.bytes") + .tags(tags) + .register(meterRegistry, totalBytes); + }); + + } catch (Exception e) { + log.error("监控存储使用情况失败", e); + } + } + + /** + * 自动清理过期数据 + */ + @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 + public void cleanupExpiredData() { + try { + // 删除30天前的分区 + LocalDate cutoffDate = LocalDate.now().minusDays(30); + String partition = cutoffDate.format(DateTimeFormatter.ofPattern("yyyyMM")); + + String sql = String.format( + "ALTER TABLE analytics.events_local ON CLUSTER cluster_2shards_2replicas DROP PARTITION '%s'", + partition + ); + + clickHouseJdbcTemplate.execute(sql); + log.info("清理过期数据完成: 分区 {}", partition); + + } catch (Exception e) { + log.error("清理过期数据失败", e); + } + } +} +``` + +### 2. 自动化运维脚本 + +```bash +#!/bin/bash +# clickhouse-ops.sh - ClickHouse运维脚本 + +CLICKHOUSE_CLIENT="clickhouse-client" +CLUSTER="cluster_2shards_2replicas" + +# 检查集群状态 +check_cluster_status() { + echo "检查ClickHouse集群状态..." + + $CLICKHOUSE_CLIENT --query " + SELECT + cluster, + shard_num, + replica_num, + host_name, + port, + errors_count, + slowdowns_count + FROM system.clusters + WHERE cluster = '$CLUSTER' + FORMAT PrettyCompact + " +} + +# 备份表数据 +backup_table() { + local database=$1 + local table=$2 + local backup_path=$3 + + echo "备份表 $database.$table 到 $backup_path..." + + $CLICKHOUSE_CLIENT --query " + INSERT INTO FUNCTION file('$backup_path', 'Native') + SELECT * FROM $database.$table + " + + if [ $? -eq 0 ]; then + echo "表 $database.$table 备份成功" + else + echo "表 $database.$table 备份失败" + return 1 + fi +} + +# 优化表 +optimize_table() { + local database=$1 + local table=$2 + + echo "优化表 $database.$table..." + + $CLICKHOUSE_CLIENT --query " + OPTIMIZE TABLE $database.$table ON CLUSTER $CLUSTER FINAL + " + + echo "表 $database.$table 优化完成" +} + +# 检查副本同步状态 +check_replication_status() { + echo "检查副本同步状态..." + + $CLICKHOUSE_CLIENT --query " + SELECT + database, + table, + replica_name, + is_leader, + is_readonly, + absolute_delay, + queue_size, + inserts_in_queue, + merges_in_queue + FROM system.replicas + WHERE database = 'analytics' + FORMAT PrettyCompact + " +} + +# 清理旧分区 +cleanup_old_partitions() { + local database=$1 + local table=$2 + local days_to_keep=$3 + + echo "清理 $database.$table 中 $days_to_keep 天前的分区..." + + # 计算要删除的分区 + cutoff_date=$(date -d "$days_to_keep days ago" +%Y%m) + + $CLICKHOUSE_CLIENT --query " + SELECT DISTINCT partition + FROM system.parts + WHERE database = '$database' AND table = '$table' + AND partition < '$cutoff_date' + AND active = 1 + " | while read partition; do + if [ -n "$partition" ]; then + echo "删除分区: $partition" + $CLICKHOUSE_CLIENT --query " + ALTER TABLE $database.$table ON CLUSTER $CLUSTER DROP PARTITION '$partition' + " + fi + done + + echo "分区清理完成" +} + +# 监控慢查询 +monitor_slow_queries() { + local threshold_ms=${1:-5000} + + echo "监控慢查询 (阈值: ${threshold_ms}ms)..." + + $CLICKHOUSE_CLIENT --query " + SELECT + event_time, + query_duration_ms, + read_rows, + read_bytes, + memory_usage, + substring(query, 1, 100) as query_preview + FROM system.query_log + WHERE event_time >= now() - INTERVAL 1 HOUR + AND type = 'QueryFinish' + AND query_duration_ms > $threshold_ms + ORDER BY query_duration_ms DESC + LIMIT 20 + FORMAT PrettyCompact + " +} + +# 主函数 +main() { + case $1 in + "status") + check_cluster_status + ;; + "backup") + backup_table $2 $3 $4 + ;; + "optimize") + optimize_table $2 $3 + ;; + "replication") + check_replication_status + ;; + "cleanup") + cleanup_old_partitions $2 $3 $4 + ;; + "slow-queries") + monitor_slow_queries $2 + ;; + *) + echo "用法: $0 {status|backup|optimize|replication|cleanup|slow-queries} [参数]" + echo " status - 检查集群状态" + echo " backup - 备份表" + echo " optimize
- 优化表" + echo " replication - 检查副本状态" + echo " cleanup
- 清理旧分区" + echo " slow-queries [threshold_ms] - 监控慢查询" + exit 1 + ;; + esac +} + +main $@ +``` + +## 配置文件 + +### application.yml + +```yaml +spring: + application: + name: clickhouse-sharding-demo + +clickhouse: + url: jdbc:clickhouse://localhost:8123/analytics + username: default + password: + initial-size: 5 + max-active: 20 + min-idle: 5 + max-wait: 60000 + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus + metrics: + export: + prometheus: + enabled: true + +logging: + level: + com.clickhouse: INFO + com.example.clickhouse: DEBUG +``` + +## 最佳实践 + +### 1. 表设计原则 + +- **分区键选择**: 使用时间字段进行分区,便于数据管理 +- **排序键优化**: 根据查询模式设计ORDER BY键 +- **分片键均匀**: 选择分布均匀的分片键避免热点 +- **压缩算法**: 使用LZ4或ZSTD压缩算法 + +### 2. 查询优化 + +- **避免SELECT ***: 只查询需要的列 +- **使用PREWHERE**: 在WHERE之前过滤数据 +- **合理使用索引**: 创建适当的跳数索引 +- **并行查询**: 利用分片并行处理 + +### 3. 写入优化 + +- **批量写入**: 使用大批次提高吞吐量 +- **异步写入**: 使用队列缓冲写入请求 +- **避免小批次**: 减少网络开销和合并压力 +- **压缩传输**: 启用客户端压缩 + +### 4. 运维管理 + +- **监控指标**: 建立完善的监控体系 +- **定期优化**: 定期执行OPTIMIZE操作 +- **分区管理**: 及时清理过期分区 +- **副本监控**: 监控副本同步状态 + +## 总结 + +ClickHouse分片技术通过分布式表和本地表的组合实现了高性能的OLAP查询能力。关键要点包括: + +1. **分布式架构**: 通过分片和副本实现水平扩展和高可用 +2. **列式存储**: 优化分析查询性能和压缩比 +3. **智能分片**: 合理的分片键设计确保负载均衡 +4. **实时写入**: 支持高并发实时数据写入 +5. **运维自动化**: 完善的监控和自动化运维保证系统稳定性 + +在实际应用中,需要根据数据特点和查询模式优化表结构、分片策略和查询方式,并建立完善的监控和运维体系。 \ No newline at end of file diff --git "a/docs/aJava/DevOps\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/DevOps\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..9a317ef93 --- /dev/null +++ "b/docs/aJava/DevOps\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,7 @@ +# DevOps是什么 + +DevOps是一种文化,强调开发(Dev)和运维(Ops)的协同工作,通过自动化工具和流程,提高软件交付的效率和质量。 +让代码从开发到部署全程自动化流水线。持续集成CI,持续交付CD,基础设施即代码。监控告警系统。 +现实小时级别上线。故障率直降80%,微服务架构的团队。 + + diff --git "a/docs/aJava/ElasticSearch\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/ElasticSearch\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..a35dfd86c --- /dev/null +++ "b/docs/aJava/ElasticSearch\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,17 @@ +# ElasticSearch是什么 + +淘宝秒搜商品;刷微博实时看热点;排行榜秒刷新; + +它能用1秒搜索完100万条数据 + +核心:倒排索引 + +加上分片机制,把数据拆分成乐高块 + +每块都能单独扩容“副本节点”,随时接替宕机的兄弟 + +场景使用到: + +1. 日志分析,比如你的服务器每天吐10GB日志;用 ElasticSearch 加 Kibana,分分钟把日志变成可视化的报表 +2. 模糊搜索,靠分词,加相关性评分给你精准结果 +3. 实时监控,滴滴用它在全国地图上,动态显示每车辆的移动光点,靠的就是ES毫秒级响应 diff --git "a/docs/aJava/Elasticsearch\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/Elasticsearch\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..92d765adc --- /dev/null +++ "b/docs/aJava/Elasticsearch\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,1642 @@ +# Elasticsearch分片技术实现 + +## 概述 + +Elasticsearch分片(Sharding)是其分布式架构的核心,通过将索引分割成多个分片来实现水平扩展。每个分片都是一个独立的Lucene索引,可以分布在集群的不同节点上。 + +## Elasticsearch分片架构 + +### 1. 集群架构设计 + +```yaml +# docker-compose.yml - Elasticsearch集群 +version: '3.8' +services: + # Master节点 + es-master-1: + image: elasticsearch:8.8.0 + container_name: es-master-1 + environment: + - node.name=es-master-1 + - node.roles=master + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-2,es-master-3 + - cluster.initial_master_nodes=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + ports: + - "9200:9200" + volumes: + - es_master_1_data:/usr/share/elasticsearch/data + networks: + - es-network + + es-master-2: + image: elasticsearch:8.8.0 + container_name: es-master-2 + environment: + - node.name=es-master-2 + - node.roles=master + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-3 + - cluster.initial_master_nodes=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + ports: + - "9201:9200" + volumes: + - es_master_2_data:/usr/share/elasticsearch/data + networks: + - es-network + + es-master-3: + image: elasticsearch:8.8.0 + container_name: es-master-3 + environment: + - node.name=es-master-3 + - node.roles=master + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-2 + - cluster.initial_master_nodes=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + ports: + - "9202:9200" + volumes: + - es_master_3_data:/usr/share/elasticsearch/data + networks: + - es-network + + # 数据节点 + es-data-1: + image: elasticsearch:8.8.0 + container_name: es-data-1 + environment: + - node.name=es-data-1 + - node.roles=data,ingest + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + ports: + - "9203:9200" + volumes: + - es_data_1_data:/usr/share/elasticsearch/data + networks: + - es-network + + es-data-2: + image: elasticsearch:8.8.0 + container_name: es-data-2 + environment: + - node.name=es-data-2 + - node.roles=data,ingest + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + ports: + - "9204:9200" + volumes: + - es_data_2_data:/usr/share/elasticsearch/data + networks: + - es-network + + es-data-3: + image: elasticsearch:8.8.0 + container_name: es-data-3 + environment: + - node.name=es-data-3 + - node.roles=data,ingest + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + ports: + - "9205:9200" + volumes: + - es_data_3_data:/usr/share/elasticsearch/data + networks: + - es-network + + # 协调节点 + es-coord-1: + image: elasticsearch:8.8.0 + container_name: es-coord-1 + environment: + - node.name=es-coord-1 + - node.roles= + - cluster.name=es-cluster + - discovery.seed_hosts=es-master-1,es-master-2,es-master-3 + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + ports: + - "9206:9200" + networks: + - es-network + + # Kibana + kibana: + image: kibana:8.8.0 + container_name: kibana + environment: + - ELASTICSEARCH_HOSTS=http://es-coord-1:9200 + ports: + - "5601:5601" + networks: + - es-network + depends_on: + - es-coord-1 + +volumes: + es_master_1_data: + es_master_2_data: + es_master_3_data: + es_data_1_data: + es_data_2_data: + es_data_3_data: + +networks: + es-network: + driver: bridge +``` + +### 2. 集群初始化脚本 + +```bash +#!/bin/bash +# elasticsearch-cluster-init.sh + +echo "初始化Elasticsearch集群..." + +# 等待集群启动 +sleep 60 + +# 检查集群健康状态 +echo "检查集群健康状态..." +curl -X GET "es-coord-1:9200/_cluster/health?pretty" + +# 创建索引模板 +echo "创建索引模板..." +curl -X PUT "es-coord-1:9200/_index_template/logs_template" -H 'Content-Type: application/json' -d' +{ + "index_patterns": ["logs-*"], + "template": { + "settings": { + "number_of_shards": 3, + "number_of_replicas": 1, + "index.routing.allocation.total_shards_per_node": 2 + }, + "mappings": { + "properties": { + "timestamp": { + "type": "date" + }, + "level": { + "type": "keyword" + }, + "message": { + "type": "text", + "analyzer": "standard" + }, + "service": { + "type": "keyword" + }, + "host": { + "type": "keyword" + } + } + } + } +}' + +# 创建用户数据索引 +echo "创建用户数据索引..." +curl -X PUT "es-coord-1:9200/users" -H 'Content-Type: application/json' -d' +{ + "settings": { + "number_of_shards": 5, + "number_of_replicas": 1, + "index.routing.allocation.total_shards_per_node": 2, + "index.routing.allocation.awareness.attributes": "zone" + }, + "mappings": { + "properties": { + "user_id": { + "type": "keyword" + }, + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "email": { + "type": "keyword" + }, + "age": { + "type": "integer" + }, + "created_at": { + "type": "date" + }, + "location": { + "type": "geo_point" + } + } + } +}' + +echo "Elasticsearch集群初始化完成!" +``` + +## Java应用集成 + +### 1. Spring Boot配置 + +```java +@Configuration +public class ElasticsearchShardingConfig { + + @Value("${elasticsearch.hosts}") + private String[] hosts; + + @Bean + public ElasticsearchClient elasticsearchClient() { + HttpHost[] httpHosts = Arrays.stream(hosts) + .map(host -> { + String[] parts = host.split(":"); + return new HttpHost(parts[0], Integer.parseInt(parts[1]), "http"); + }) + .toArray(HttpHost[]::new); + + RestClientBuilder builder = RestClient.builder(httpHosts) + .setRequestConfigCallback(requestConfigBuilder -> + requestConfigBuilder + .setConnectTimeout(5000) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(5000) + ) + .setHttpClientConfigCallback(httpClientBuilder -> + httpClientBuilder + .setMaxConnTotal(100) + .setMaxConnPerRoute(50) + .setKeepAliveStrategy((response, context) -> 30000) + ); + + ElasticsearchTransport transport = new RestClientTransport( + builder.build(), new JacksonJsonpMapper()); + + return new ElasticsearchClient(transport); + } + + @Bean + public ElasticsearchOperations elasticsearchOperations() { + return new ElasticsearchRestTemplate( + RestClients.create(ClientConfiguration.builder() + .connectedTo(hosts) + .withConnectTimeout(Duration.ofSeconds(5)) + .withSocketTimeout(Duration.ofSeconds(60)) + .build()).rest()); + } +} +``` + +### 2. 分片管理服务 + +```java +@Service +public class ElasticsearchShardingService { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + @Autowired + private ElasticsearchOperations elasticsearchOperations; + + /** + * 创建分片索引 + */ + public void createShardedIndex(String indexName, int shards, int replicas) { + try { + CreateIndexRequest request = CreateIndexRequest.of(builder -> + builder.index(indexName) + .settings(settings -> settings + .numberOfShards(String.valueOf(shards)) + .numberOfReplicas(String.valueOf(replicas)) + .put("index.routing.allocation.total_shards_per_node", "2") + .put("index.max_result_window", "50000") + ) + ); + + CreateIndexResponse response = elasticsearchClient.indices().create(request); + log.info("创建分片索引成功: {}, 分片数: {}, 副本数: {}", + indexName, shards, replicas); + + } catch (Exception e) { + log.error("创建分片索引失败: {}", indexName, e); + throw new RuntimeException("创建索引失败", e); + } + } + + /** + * 动态调整分片副本数 + */ + public void updateReplicaCount(String indexName, int replicas) { + try { + PutIndicesSettingsRequest request = PutIndicesSettingsRequest.of(builder -> + builder.index(indexName) + .settings(settings -> settings + .numberOfReplicas(String.valueOf(replicas)) + ) + ); + + PutIndicesSettingsResponse response = elasticsearchClient.indices() + .putSettings(request); + + log.info("更新索引副本数成功: {}, 新副本数: {}", indexName, replicas); + + } catch (Exception e) { + log.error("更新索引副本数失败: {}", indexName, e); + throw new RuntimeException("更新副本数失败", e); + } + } + + /** + * 分片重新分配 + */ + public void reallocateShards(String indexName, String fromNode, String toNode) { + try { + // 移动分片 + ClusterRerouteRequest request = ClusterRerouteRequest.of(builder -> + builder.commands(commands -> commands + .move(move -> move + .index(indexName) + .shard(0) + .fromNode(fromNode) + .toNode(toNode) + ) + ) + ); + + ClusterRerouteResponse response = elasticsearchClient.cluster().reroute(request); + log.info("分片重新分配成功: {} 从 {} 移动到 {}", indexName, fromNode, toNode); + + } catch (Exception e) { + log.error("分片重新分配失败", e); + throw new RuntimeException("分片重新分配失败", e); + } + } + + /** + * 获取分片分布信息 + */ + public Map getShardDistribution(String indexName) { + try { + IndicesStatsRequest request = IndicesStatsRequest.of(builder -> + builder.index(indexName) + ); + + IndicesStatsResponse response = elasticsearchClient.indices().stats(request); + + Map distribution = new HashMap<>(); + + response.indices().forEach((index, stats) -> { + Map indexInfo = new HashMap<>(); + indexInfo.put("totalShards", stats.total().docs().count()); + indexInfo.put("primaryShards", stats.primaries().docs().count()); + indexInfo.put("storeSize", stats.total().store().sizeInBytes()); + distribution.put(index, indexInfo); + }); + + return distribution; + + } catch (Exception e) { + log.error("获取分片分布信息失败: {}", indexName, e); + throw new RuntimeException("获取分片信息失败", e); + } + } + + /** + * 强制合并分片 + */ + public void forcemergeShards(String indexName, int maxNumSegments) { + try { + ForcemergeRequest request = ForcemergeRequest.of(builder -> + builder.index(indexName) + .maxNumSegments(maxNumSegments) + .onlyExpungeDeletes(false) + .flush(true) + ); + + ForcemergeResponse response = elasticsearchClient.indices().forcemerge(request); + log.info("强制合并分片成功: {}, 目标段数: {}", indexName, maxNumSegments); + + } catch (Exception e) { + log.error("强制合并分片失败: {}", indexName, e); + throw new RuntimeException("强制合并失败", e); + } + } +} +``` + +### 3. 路由策略实现 + +```java +@Service +public class ElasticsearchRoutingService { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + /** + * 基于用户ID的路由策略 + */ + public void indexWithUserRouting(String indexName, String userId, Object document) { + try { + // 使用用户ID作为路由键 + String routing = calculateRouting(userId); + + IndexRequest request = IndexRequest.of(builder -> + builder.index(indexName) + .id(userId) + .routing(routing) + .document(document) + ); + + IndexResponse response = elasticsearchClient.index(request); + log.debug("文档索引成功: {}, 路由: {}", response.id(), routing); + + } catch (Exception e) { + log.error("文档索引失败", e); + throw new RuntimeException("索引失败", e); + } + } + + /** + * 基于时间的路由策略 + */ + public void indexWithTimeRouting(String indexPrefix, LocalDateTime timestamp, Object document) { + try { + // 按天分割索引 + String indexName = indexPrefix + "-" + timestamp.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + // 使用小时作为路由键 + String routing = String.valueOf(timestamp.getHour()); + + IndexRequest request = IndexRequest.of(builder -> + builder.index(indexName) + .routing(routing) + .document(document) + ); + + IndexResponse response = elasticsearchClient.index(request); + log.debug("时间路由索引成功: {}, 索引: {}, 路由: {}", + response.id(), indexName, routing); + + } catch (Exception e) { + log.error("时间路由索引失败", e); + throw new RuntimeException("索引失败", e); + } + } + + /** + * 基于地理位置的路由策略 + */ + public void indexWithGeoRouting(String indexName, double lat, double lon, Object document) { + try { + // 基于地理位置计算路由 + String routing = calculateGeoRouting(lat, lon); + + IndexRequest request = IndexRequest.of(builder -> + builder.index(indexName) + .routing(routing) + .document(document) + ); + + IndexResponse response = elasticsearchClient.index(request); + log.debug("地理路由索引成功: {}, 路由: {}", response.id(), routing); + + } catch (Exception e) { + log.error("地理路由索引失败", e); + throw new RuntimeException("索引失败", e); + } + } + + /** + * 批量索引with路由 + */ + public void bulkIndexWithRouting(String indexName, List documents) { + try { + BulkRequest.Builder bulkBuilder = new BulkRequest.Builder(); + + for (DocumentWithRouting doc : documents) { + bulkBuilder.operations(op -> op + .index(idx -> idx + .index(indexName) + .id(doc.getId()) + .routing(doc.getRouting()) + .document(doc.getDocument()) + ) + ); + } + + BulkResponse response = elasticsearchClient.bulk(bulkBuilder.build()); + + if (response.errors()) { + log.warn("批量索引部分失败"); + response.items().forEach(item -> { + if (item.error() != null) { + log.error("索引失败: {}, 错误: {}", item.id(), item.error().reason()); + } + }); + } else { + log.info("批量索引成功,文档数: {}", documents.size()); + } + + } catch (Exception e) { + log.error("批量索引失败", e); + throw new RuntimeException("批量索引失败", e); + } + } + + /** + * 路由查询 + */ + public List searchWithRouting(String indexName, String routing, + Query query, Class clazz) { + try { + SearchRequest request = SearchRequest.of(builder -> + builder.index(indexName) + .routing(routing) + .query(query) + .size(1000) + ); + + SearchResponse response = elasticsearchClient.search(request, clazz); + + return response.hits().hits().stream() + .map(hit -> hit.source()) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("路由查询失败", e); + throw new RuntimeException("查询失败", e); + } + } + + private String calculateRouting(String userId) { + // 简单的哈希路由策略 + return String.valueOf(Math.abs(userId.hashCode()) % 10); + } + + private String calculateGeoRouting(double lat, double lon) { + // 基于地理位置的简单分区策略 + int latZone = (int) ((lat + 90) / 30); // 6个纬度区域 + int lonZone = (int) ((lon + 180) / 60); // 6个经度区域 + return latZone + "-" + lonZone; + } + + public static class DocumentWithRouting { + private String id; + private String routing; + private Object document; + + // 构造函数、getter、setter + } +} +``` + +### 4. 索引生命周期管理 + +```java +@Service +public class IndexLifecycleService { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + /** + * 创建索引生命周期策略 + */ + public void createLifecyclePolicy(String policyName) { + try { + // 创建ILM策略 + Map policy = Map.of( + "policy", Map.of( + "phases", Map.of( + "hot", Map.of( + "actions", Map.of( + "rollover", Map.of( + "max_size", "10GB", + "max_age", "7d", + "max_docs", 10000000 + ) + ) + ), + "warm", Map.of( + "min_age", "7d", + "actions", Map.of( + "allocate", Map.of( + "number_of_replicas", 0 + ), + "forcemerge", Map.of( + "max_num_segments", 1 + ) + ) + ), + "cold", Map.of( + "min_age", "30d", + "actions", Map.of( + "allocate", Map.of( + "number_of_replicas", 0 + ) + ) + ), + "delete", Map.of( + "min_age", "90d", + "actions", Map.of( + "delete", Map.of() + ) + ) + ) + ) + ); + + // 这里需要使用低级客户端或REST API + log.info("创建生命周期策略: {}", policyName); + + } catch (Exception e) { + log.error("创建生命周期策略失败: {}", policyName, e); + throw new RuntimeException("创建策略失败", e); + } + } + + /** + * 创建索引模板with生命周期 + */ + public void createIndexTemplateWithLifecycle(String templateName, String indexPattern, + String lifecyclePolicy) { + try { + PutIndexTemplateRequest request = PutIndexTemplateRequest.of(builder -> + builder.name(templateName) + .indexPatterns(indexPattern) + .template(template -> template + .settings(settings -> settings + .numberOfShards("3") + .numberOfReplicas("1") + .put("index.lifecycle.name", lifecyclePolicy) + .put("index.lifecycle.rollover_alias", indexPattern.replace("*", "alias")) + ) + .mappings(mappings -> mappings + .properties("timestamp", property -> property + .date(date -> date.format("yyyy-MM-dd HH:mm:ss")) + ) + .properties("level", property -> property + .keyword(keyword -> keyword) + ) + .properties("message", property -> property + .text(text -> text.analyzer("standard")) + ) + ) + ) + ); + + PutIndexTemplateResponse response = elasticsearchClient.indices() + .putIndexTemplate(request); + + log.info("创建索引模板成功: {}, 生命周期策略: {}", templateName, lifecyclePolicy); + + } catch (Exception e) { + log.error("创建索引模板失败: {}", templateName, e); + throw new RuntimeException("创建模板失败", e); + } + } + + /** + * 手动触发索引滚动 + */ + public void rolloverIndex(String aliasName) { + try { + RolloverRequest request = RolloverRequest.of(builder -> + builder.alias(aliasName) + .conditions(conditions -> conditions + .maxSize("5GB") + .maxAge(Time.of(time -> time.time("1d"))) + .maxDocs(5000000L) + ) + ); + + RolloverResponse response = elasticsearchClient.indices().rollover(request); + + if (response.rolledOver()) { + log.info("索引滚动成功: {} -> {}", aliasName, response.newIndex()); + } else { + log.info("索引滚动条件未满足: {}", aliasName); + } + + } catch (Exception e) { + log.error("索引滚动失败: {}", aliasName, e); + throw new RuntimeException("索引滚动失败", e); + } + } + + /** + * 监控索引生命周期状态 + */ + @Scheduled(fixedRate = 3600000) // 1小时检查一次 + public void monitorIndexLifecycle() { + try { + // 获取所有索引的ILM状态 + GetLifecycleRequest request = GetLifecycleRequest.of(builder -> + builder.index("*") + ); + + // 这里需要使用低级客户端获取ILM状态 + log.info("检查索引生命周期状态"); + + } catch (Exception e) { + log.error("监控索引生命周期失败", e); + } + } + + /** + * 清理过期索引 + */ + @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 + public void cleanupExpiredIndices() { + try { + LocalDateTime cutoffDate = LocalDateTime.now().minusDays(90); + String cutoffPattern = "*-" + cutoffDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + GetIndexRequest request = GetIndexRequest.of(builder -> + builder.index(cutoffPattern) + ); + + GetIndexResponse response = elasticsearchClient.indices().get(request); + + for (String indexName : response.result().keySet()) { + if (shouldDeleteIndex(indexName, cutoffDate)) { + deleteIndex(indexName); + } + } + + } catch (Exception e) { + log.error("清理过期索引失败", e); + } + } + + private boolean shouldDeleteIndex(String indexName, LocalDateTime cutoffDate) { + // 解析索引名称中的日期 + try { + String[] parts = indexName.split("-"); + if (parts.length >= 2) { + String datePart = parts[parts.length - 1]; + LocalDate indexDate = LocalDate.parse(datePart, DateTimeFormatter.ofPattern("yyyy.MM.dd")); + return indexDate.isBefore(cutoffDate.toLocalDate()); + } + } catch (Exception e) { + log.warn("无法解析索引日期: {}", indexName); + } + return false; + } + + private void deleteIndex(String indexName) { + try { + DeleteIndexRequest request = DeleteIndexRequest.of(builder -> + builder.index(indexName) + ); + + DeleteIndexResponse response = elasticsearchClient.indices().delete(request); + log.info("删除过期索引: {}", indexName); + + } catch (Exception e) { + log.error("删除索引失败: {}", indexName, e); + } + } +} +``` + +## 性能优化策略 + +### 1. 分片大小优化 + +```java +@Service +public class ShardSizeOptimizer { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 分析分片大小分布 + */ + public ShardSizeAnalysis analyzeShardSizes(String indexPattern) { + try { + IndicesStatsRequest request = IndicesStatsRequest.of(builder -> + builder.index(indexPattern) + ); + + IndicesStatsResponse response = elasticsearchClient.indices().stats(request); + + List shardInfos = new ArrayList<>(); + + response.indices().forEach((indexName, stats) -> { + stats.shards().forEach((shardId, shardStats) -> { + ShardInfo info = new ShardInfo(); + info.setIndexName(indexName); + info.setShardId(shardId); + info.setDocCount(shardStats.get(0).docs().count()); + info.setStoreSize(shardStats.get(0).store().sizeInBytes()); + shardInfos.add(info); + }); + }); + + return analyzeDistribution(shardInfos); + + } catch (Exception e) { + log.error("分析分片大小失败", e); + throw new RuntimeException("分析失败", e); + } + } + + /** + * 推荐最优分片数量 + */ + public ShardRecommendation recommendShardCount(long estimatedDataSize, + long estimatedDocCount, + int nodeCount) { + // 目标分片大小: 10-50GB + long targetShardSize = 30L * 1024 * 1024 * 1024; // 30GB + + // 基于数据大小计算分片数 + int shardsBySize = (int) Math.ceil((double) estimatedDataSize / targetShardSize); + + // 基于节点数计算分片数(每个节点1-3个分片) + int shardsByNodes = nodeCount * 2; + + // 基于文档数计算分片数(每个分片不超过1000万文档) + int shardsByDocs = (int) Math.ceil((double) estimatedDocCount / 10_000_000); + + // 取中间值 + int recommendedShards = Math.max(1, Math.min( + Math.max(shardsBySize, shardsByDocs), + shardsByNodes + )); + + ShardRecommendation recommendation = new ShardRecommendation(); + recommendation.setRecommendedShards(recommendedShards); + recommendation.setRecommendedReplicas(Math.min(1, nodeCount - 1)); + recommendation.setReason(String.format( + "基于数据大小(%dGB)、文档数(%d)、节点数(%d)的综合考虑", + estimatedDataSize / (1024 * 1024 * 1024), + estimatedDocCount, + nodeCount + )); + + return recommendation; + } + + /** + * 监控分片性能指标 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorShardPerformance() { + try { + NodesStatsRequest request = NodesStatsRequest.of(builder -> + builder.metric("indices") + ); + + NodesStatsResponse response = elasticsearchClient.nodes().stats(request); + + response.nodes().forEach((nodeId, nodeStats) -> { + if (nodeStats.indices() != null) { + // 索引性能指标 + long indexingRate = nodeStats.indices().indexing().indexTotal(); + long searchRate = nodeStats.indices().search().queryTotal(); + long storeSize = nodeStats.indices().store().sizeInBytes(); + + // 记录指标 + Gauge.builder("elasticsearch.node.indexing.rate") + .tag("node", nodeId) + .register(meterRegistry, indexingRate); + + Gauge.builder("elasticsearch.node.search.rate") + .tag("node", nodeId) + .register(meterRegistry, searchRate); + + Gauge.builder("elasticsearch.node.store.size") + .tag("node", nodeId) + .register(meterRegistry, storeSize); + } + }); + + } catch (Exception e) { + log.error("监控分片性能失败", e); + } + } + + private ShardSizeAnalysis analyzeDistribution(List shardInfos) { + if (shardInfos.isEmpty()) { + return new ShardSizeAnalysis(); + } + + // 计算统计信息 + LongSummaryStatistics sizeStats = shardInfos.stream() + .mapToLong(ShardInfo::getStoreSize) + .summaryStatistics(); + + LongSummaryStatistics docStats = shardInfos.stream() + .mapToLong(ShardInfo::getDocCount) + .summaryStatistics(); + + ShardSizeAnalysis analysis = new ShardSizeAnalysis(); + analysis.setTotalShards(shardInfos.size()); + analysis.setAvgShardSize(sizeStats.getAverage()); + analysis.setMaxShardSize(sizeStats.getMax()); + analysis.setMinShardSize(sizeStats.getMin()); + analysis.setAvgDocCount(docStats.getAverage()); + analysis.setMaxDocCount(docStats.getMax()); + analysis.setMinDocCount(docStats.getMin()); + + // 计算不平衡度 + double sizeImbalance = (sizeStats.getMax() - sizeStats.getMin()) / sizeStats.getAverage(); + analysis.setSizeImbalanceRatio(sizeImbalance); + + return analysis; + } + + // 内部类定义 + public static class ShardInfo { + private String indexName; + private String shardId; + private long docCount; + private long storeSize; + + // getter和setter + } + + public static class ShardSizeAnalysis { + private int totalShards; + private double avgShardSize; + private long maxShardSize; + private long minShardSize; + private double avgDocCount; + private long maxDocCount; + private long minDocCount; + private double sizeImbalanceRatio; + + // getter和setter + } + + public static class ShardRecommendation { + private int recommendedShards; + private int recommendedReplicas; + private String reason; + + // getter和setter + } +} +``` + +### 2. 查询性能优化 + +```java +@Service +public class SearchOptimizationService { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + /** + * 优化的分页查询 + */ + public PagedResult optimizedPagedSearch(String indexName, + Query query, + int page, + int size, + Class clazz) { + try { + // 使用search_after进行深度分页 + if (page * size > 10000) { + return searchAfterPagination(indexName, query, page, size, clazz); + } else { + return standardPagination(indexName, query, page, size, clazz); + } + + } catch (Exception e) { + log.error("分页查询失败", e); + throw new RuntimeException("查询失败", e); + } + } + + /** + * 聚合查询优化 + */ + public Map optimizedAggregation(String indexName, + Query query, + List aggregations) { + try { + SearchRequest.Builder requestBuilder = new SearchRequest.Builder() + .index(indexName) + .query(query) + .size(0); // 不返回文档,只返回聚合结果 + + // 添加聚合 + for (Aggregation agg : aggregations) { + requestBuilder.aggregations(agg.getName(), agg); + } + + SearchRequest request = requestBuilder.build(); + SearchResponse response = elasticsearchClient.search(request, Void.class); + + Map results = new HashMap<>(); + response.aggregations().forEach((name, aggregation) -> { + results.put(name, parseAggregationResult(aggregation)); + }); + + return results; + + } catch (Exception e) { + log.error("聚合查询失败", e); + throw new RuntimeException("聚合查询失败", e); + } + } + + /** + * 多索引并行查询 + */ + public List parallelMultiIndexSearch(List indices, + Query query, + Class clazz) { + try { + // 并行查询多个索引 + List>> futures = indices.stream() + .map(index -> CompletableFuture.supplyAsync(() -> { + try { + SearchRequest request = SearchRequest.of(builder -> + builder.index(index) + .query(query) + .size(1000) + ); + + SearchResponse response = elasticsearchClient.search(request, clazz); + return response.hits().hits().stream() + .map(hit -> hit.source()) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("查询索引失败: {}", index, e); + return Collections.emptyList(); + } + })) + .collect(Collectors.toList()); + + // 合并结果 + return futures.stream() + .map(CompletableFuture::join) + .flatMap(List::stream) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("并行多索引查询失败", e); + throw new RuntimeException("查询失败", e); + } + } + + /** + * 缓存热点查询 + */ + @Cacheable(value = "elasticsearch-queries", key = "#indexName + ':' + #query.toString()") + public List cachedSearch(String indexName, Query query, Class clazz) { + try { + SearchRequest request = SearchRequest.of(builder -> + builder.index(indexName) + .query(query) + .size(100) + ); + + SearchResponse response = elasticsearchClient.search(request, clazz); + return response.hits().hits().stream() + .map(hit -> hit.source()) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("缓存查询失败", e); + throw new RuntimeException("查询失败", e); + } + } + + private PagedResult standardPagination(String indexName, Query query, + int page, int size, Class clazz) + throws IOException { + SearchRequest request = SearchRequest.of(builder -> + builder.index(indexName) + .query(query) + .from(page * size) + .size(size) + ); + + SearchResponse response = elasticsearchClient.search(request, clazz); + + List content = response.hits().hits().stream() + .map(hit -> hit.source()) + .collect(Collectors.toList()); + + return new PagedResult<>(content, page, size, response.hits().total().value()); + } + + private PagedResult searchAfterPagination(String indexName, Query query, + int page, int size, Class clazz) + throws IOException { + // 实现search_after分页逻辑 + // 这里简化实现,实际需要维护排序值 + return standardPagination(indexName, query, page, size, clazz); + } + + private Object parseAggregationResult(Aggregate aggregation) { + // 解析聚合结果 + return new HashMap<>(); // 简化实现 + } + + public static class PagedResult { + private List content; + private int page; + private int size; + private long total; + + public PagedResult(List content, int page, int size, long total) { + this.content = content; + this.page = page; + this.size = size; + this.total = total; + } + + // getter和setter + } +} +``` + +## 监控与运维 + +### 1. 集群健康监控 + +```java +@Component +public class ElasticsearchClusterMonitor { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控集群健康状态 + */ + @Scheduled(fixedRate = 30000) + public void monitorClusterHealth() { + try { + HealthRequest request = HealthRequest.of(builder -> builder); + HealthResponse response = elasticsearchClient.cluster().health(request); + + // 记录集群状态 + String status = response.status().jsonValue(); + Gauge.builder("elasticsearch.cluster.status") + .tag("status", status) + .register(meterRegistry, getStatusValue(status)); + + // 记录节点数量 + Gauge.builder("elasticsearch.cluster.nodes.total") + .register(meterRegistry, response.numberOfNodes()); + Gauge.builder("elasticsearch.cluster.nodes.data") + .register(meterRegistry, response.numberOfDataNodes()); + + // 记录分片状态 + Gauge.builder("elasticsearch.cluster.shards.active") + .register(meterRegistry, response.activeShards()); + Gauge.builder("elasticsearch.cluster.shards.relocating") + .register(meterRegistry, response.relocatingShards()); + Gauge.builder("elasticsearch.cluster.shards.initializing") + .register(meterRegistry, response.initializingShards()); + Gauge.builder("elasticsearch.cluster.shards.unassigned") + .register(meterRegistry, response.unassignedShards()); + + // 检查是否有问题 + if (!"green".equals(status)) { + log.warn("集群状态异常: {}, 未分配分片: {}", + status, response.unassignedShards()); + } + + } catch (Exception e) { + log.error("集群健康监控失败", e); + } + } + + /** + * 监控索引状态 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorIndexHealth() { + try { + IndicesStatsRequest request = IndicesStatsRequest.of(builder -> + builder.index("*") + ); + + IndicesStatsResponse response = elasticsearchClient.indices().stats(request); + + response.indices().forEach((indexName, stats) -> { + // 索引大小 + long storeSize = stats.total().store().sizeInBytes(); + Gauge.builder("elasticsearch.index.store.size") + .tag("index", indexName) + .register(meterRegistry, storeSize); + + // 文档数量 + long docCount = stats.total().docs().count(); + Gauge.builder("elasticsearch.index.docs.count") + .tag("index", indexName) + .register(meterRegistry, docCount); + + // 索引操作统计 + if (stats.total().indexing() != null) { + long indexTotal = stats.total().indexing().indexTotal(); + long indexTime = stats.total().indexing().indexTimeInMillis(); + + Counter.builder("elasticsearch.index.indexing.total") + .tag("index", indexName) + .register(meterRegistry).increment(indexTotal); + + Timer.builder("elasticsearch.index.indexing.time") + .tag("index", indexName) + .register(meterRegistry).record(indexTime, TimeUnit.MILLISECONDS); + } + + // 搜索操作统计 + if (stats.total().search() != null) { + long queryTotal = stats.total().search().queryTotal(); + long queryTime = stats.total().search().queryTimeInMillis(); + + Counter.builder("elasticsearch.index.search.total") + .tag("index", indexName) + .register(meterRegistry).increment(queryTotal); + + Timer.builder("elasticsearch.index.search.time") + .tag("index", indexName) + .register(meterRegistry).record(queryTime, TimeUnit.MILLISECONDS); + } + }); + + } catch (Exception e) { + log.error("索引健康监控失败", e); + } + } + + /** + * 监控节点性能 + */ + @Scheduled(fixedRate = 60000) // 1分钟 + public void monitorNodePerformance() { + try { + NodesStatsRequest request = NodesStatsRequest.of(builder -> + builder.metric("jvm", "os", "fs") + ); + + NodesStatsResponse response = elasticsearchClient.nodes().stats(request); + + response.nodes().forEach((nodeId, nodeStats) -> { + String nodeName = nodeStats.name(); + + // JVM内存使用 + if (nodeStats.jvm() != null && nodeStats.jvm().mem() != null) { + long heapUsed = nodeStats.jvm().mem().heapUsedInBytes(); + long heapMax = nodeStats.jvm().mem().heapMaxInBytes(); + double heapUsedPercent = (double) heapUsed / heapMax * 100; + + Gauge.builder("elasticsearch.node.jvm.heap.used") + .tag("node", nodeName) + .register(meterRegistry, heapUsed); + + Gauge.builder("elasticsearch.node.jvm.heap.used.percent") + .tag("node", nodeName) + .register(meterRegistry, heapUsedPercent); + } + + // 系统负载 + if (nodeStats.os() != null) { + if (nodeStats.os().cpu() != null) { + int cpuPercent = nodeStats.os().cpu().percent(); + Gauge.builder("elasticsearch.node.os.cpu.percent") + .tag("node", nodeName) + .register(meterRegistry, cpuPercent); + } + + if (nodeStats.os().mem() != null) { + long memUsed = nodeStats.os().mem().usedInBytes(); + long memTotal = nodeStats.os().mem().totalInBytes(); + double memUsedPercent = (double) memUsed / memTotal * 100; + + Gauge.builder("elasticsearch.node.os.mem.used.percent") + .tag("node", nodeName) + .register(meterRegistry, memUsedPercent); + } + } + + // 磁盘使用 + if (nodeStats.fs() != null && nodeStats.fs().total() != null) { + long diskUsed = nodeStats.fs().total().totalInBytes() - + nodeStats.fs().total().availableInBytes(); + long diskTotal = nodeStats.fs().total().totalInBytes(); + double diskUsedPercent = (double) diskUsed / diskTotal * 100; + + Gauge.builder("elasticsearch.node.fs.used.percent") + .tag("node", nodeName) + .register(meterRegistry, diskUsedPercent); + } + }); + + } catch (Exception e) { + log.error("节点性能监控失败", e); + } + } + + private double getStatusValue(String status) { + switch (status) { + case "green": return 2; + case "yellow": return 1; + case "red": return 0; + default: return -1; + } + } +} +``` + +### 2. 自动故障处理 + +```java +@Service +public class ElasticsearchFailoverService { + + @Autowired + private ElasticsearchClient elasticsearchClient; + + @Autowired + private NotificationService notificationService; + + /** + * 检测并处理分片分配问题 + */ + @Scheduled(fixedRate = 60000) + public void handleShardAllocationIssues() { + try { + HealthRequest request = HealthRequest.of(builder -> builder); + HealthResponse health = elasticsearchClient.cluster().health(request); + + if (health.unassignedShards() > 0) { + log.warn("检测到未分配分片: {}", health.unassignedShards()); + handleUnassignedShards(); + } + + if (health.relocatingShards() > 10) { + log.warn("检测到大量分片重新分配: {}", health.relocatingShards()); + // 可能需要调整分配策略 + } + + } catch (Exception e) { + log.error("分片分配检查失败", e); + } + } + + /** + * 处理未分配分片 + */ + private void handleUnassignedShards() { + try { + // 获取未分配分片详情 + ClusterAllocationExplainRequest request = ClusterAllocationExplainRequest.of(builder -> + builder.includeYesDecisions(true) + .includeDiskInfo(true) + ); + + ClusterAllocationExplainResponse response = elasticsearchClient.cluster() + .allocationExplain(request); + + // 分析分配失败原因 + String reason = analyzeAllocationFailure(response); + log.info("分片未分配原因: {}", reason); + + // 尝试自动修复 + attemptAutoFix(reason); + + } catch (Exception e) { + log.error("处理未分配分片失败", e); + } + } + + /** + * 自动修复常见问题 + */ + private void attemptAutoFix(String reason) { + try { + if (reason.contains("disk")) { + // 磁盘空间不足,尝试清理旧数据 + cleanupOldIndices(); + } else if (reason.contains("allocation")) { + // 分配策略问题,尝试调整设置 + adjustAllocationSettings(); + } else if (reason.contains("replica")) { + // 副本分配问题,临时减少副本数 + reduceReplicaCount(); + } + } catch (Exception e) { + log.error("自动修复失败", e); + } + } + + /** + * 清理旧索引 + */ + private void cleanupOldIndices() { + try { + LocalDateTime cutoff = LocalDateTime.now().minusDays(7); + String pattern = "logs-" + cutoff.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + GetIndexRequest request = GetIndexRequest.of(builder -> + builder.index(pattern) + ); + + GetIndexResponse response = elasticsearchClient.indices().get(request); + + for (String indexName : response.result().keySet()) { + if (isOldIndex(indexName, cutoff)) { + deleteIndex(indexName); + } + } + + } catch (Exception e) { + log.error("清理旧索引失败", e); + } + } + + /** + * 调整分配设置 + */ + private void adjustAllocationSettings() { + try { + PutClusterSettingsRequest request = PutClusterSettingsRequest.of(builder -> + builder.transient_(settings -> settings + .put("cluster.routing.allocation.enable", "all") + .put("cluster.routing.rebalance.enable", "all") + .put("cluster.routing.allocation.allow_rebalance", "always") + ) + ); + + PutClusterSettingsResponse response = elasticsearchClient.cluster() + .putSettings(request); + + log.info("调整集群分配设置完成"); + + } catch (Exception e) { + log.error("调整分配设置失败", e); + } + } + + /** + * 减少副本数 + */ + private void reduceReplicaCount() { + try { + // 临时将所有索引的副本数设为0 + PutIndicesSettingsRequest request = PutIndicesSettingsRequest.of(builder -> + builder.index("*") + .settings(settings -> settings + .numberOfReplicas("0") + ) + ); + + PutIndicesSettingsResponse response = elasticsearchClient.indices() + .putSettings(request); + + log.info("临时减少副本数完成"); + + // 发送通知 + notificationService.sendAlert("Elasticsearch副本数已临时调整为0"); + + } catch (Exception e) { + log.error("减少副本数失败", e); + } + } + + private String analyzeAllocationFailure(ClusterAllocationExplainResponse response) { + // 简化实现,实际需要解析详细的分配决策 + return "分配失败分析"; + } + + private boolean isOldIndex(String indexName, LocalDateTime cutoff) { + // 解析索引名称中的日期 + try { + String[] parts = indexName.split("-"); + if (parts.length >= 2) { + String datePart = parts[parts.length - 1]; + LocalDate indexDate = LocalDate.parse(datePart, DateTimeFormatter.ofPattern("yyyy.MM.dd")); + return indexDate.isBefore(cutoff.toLocalDate()); + } + } catch (Exception e) { + log.warn("无法解析索引日期: {}", indexName); + } + return false; + } + + private void deleteIndex(String indexName) { + try { + DeleteIndexRequest request = DeleteIndexRequest.of(builder -> + builder.index(indexName) + ); + + DeleteIndexResponse response = elasticsearchClient.indices().delete(request); + log.info("删除旧索引: {}", indexName); + + } catch (Exception e) { + log.error("删除索引失败: {}", indexName, e); + } + } +} +``` + +## 配置文件 + +### application.yml + +```yaml +spring: + application: + name: elasticsearch-sharding-demo + +elasticsearch: + hosts: + - "localhost:9200" + - "localhost:9201" + - "localhost:9202" + connection-timeout: 5s + socket-timeout: 60s + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus + metrics: + export: + prometheus: + enabled: true + +logging: + level: + org.elasticsearch: DEBUG + com.example.elasticsearch: DEBUG +``` + +## 最佳实践 + +### 1. 分片设计原则 + +- **分片大小**: 保持在10-50GB之间 +- **分片数量**: 避免过度分片,通常每个节点1-3个分片 +- **副本策略**: 根据可用性需求设置副本数 +- **路由策略**: 合理使用路由避免热点 + +### 2. 性能优化 + +- **批量操作**: 使用bulk API提高索引性能 +- **分片预分配**: 根据数据增长预估分片数 +- **索引模板**: 统一管理索引设置和映射 +- **生命周期管理**: 自动化索引的创建、滚动和删除 + +### 3. 监控要点 + +- **集群健康**: 定期检查集群状态 +- **分片分布**: 监控分片在节点间的分布 +- **性能指标**: 关注索引和查询性能 +- **资源使用**: 监控CPU、内存、磁盘使用情况 + +### 4. 故障处理 + +- **自动恢复**: 配置自动故障转移 +- **备份策略**: 定期备份重要数据 +- **容量规划**: 预留足够的存储空间 +- **版本升级**: 制定滚动升级策略 + +## 总结 + +Elasticsearch分片技术是构建高性能、高可用搜索系统的关键。通过合理的分片设计、路由策略和监控机制,可以实现: + +1. **水平扩展**: 支持PB级数据存储和查询 +2. **高可用性**: 通过副本机制保证服务连续性 +3. **负载均衡**: 分片分布实现查询负载分散 +4. **性能优化**: 并行处理提升查询效率 + +在实际应用中,需要根据业务特点调整分片策略,并建立完善的监控和运维体系。 \ No newline at end of file diff --git "a/docs/aJava/Eureka\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/Eureka\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..a31725cf7 --- /dev/null +++ "b/docs/aJava/Eureka\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,5 @@ +# Eureka是什么 + +Eureka, Eureka就是个服务通讯录,自动更新号码本的秘书。 + +场景:微服务,停更了,不用了解。 diff --git "a/docs/aJava/HBase\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/HBase\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..6c8fa2609 --- /dev/null +++ "b/docs/aJava/HBase\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,1205 @@ +# HBase分片技术实现 + +## 概述 + +HBase是基于Hadoop的分布式、可扩展的NoSQL数据库,采用列族存储模型。HBase的分片机制通过Region自动分割和负载均衡实现水平扩展,支持PB级数据存储和高并发访问。 + +## HBase架构 + +### 核心组件 + +- **HMaster**: 集群管理节点,负责Region分配和负载均衡 +- **RegionServer**: 数据存储节点,管理多个Region +- **Region**: 数据分片单元,按行键范围分割 +- **ZooKeeper**: 协调服务,维护集群状态 +- **HDFS**: 底层存储系统 + +### 分片原理 + +``` +表 (Table) +├── Region 1 [startKey, endKey1) +├── Region 2 [endKey1, endKey2) +├── Region 3 [endKey2, endKey3) +└── Region N [endKeyN-1, endKey) +``` + +## 环境搭建 + +### Docker Compose配置 + +```yaml +version: '3.8' +services: + zookeeper: + image: zookeeper:3.7 + container_name: hbase-zookeeper + ports: + - "2181:2181" + environment: + ZOO_MY_ID: 1 + ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 + volumes: + - zk_data:/data + - zk_logs:/datalog + + hbase-master: + image: harisekhon/hbase:2.4 + container_name: hbase-master + hostname: hbase-master + ports: + - "16010:16010" # HBase Master Web UI + - "16000:16000" # HBase Master RPC + environment: + HBASE_CONF_hbase_rootdir: hdfs://namenode:9000/hbase + HBASE_CONF_hbase_cluster_distributed: 'true' + HBASE_CONF_hbase_zookeeper_quorum: zookeeper:2181 + HBASE_CONF_hbase_master: hbase-master:16000 + HBASE_CONF_hbase_master_hostname: hbase-master + HBASE_CONF_hbase_master_port: 16000 + HBASE_CONF_hbase_master_info_port: 16010 + HBASE_CONF_hbase_regionserver_port: 16020 + HBASE_CONF_hbase_regionserver_info_port: 16030 + depends_on: + - zookeeper + - namenode + volumes: + - hbase_data:/opt/hbase/data + + hbase-regionserver1: + image: harisekhon/hbase:2.4 + container_name: hbase-regionserver1 + hostname: hbase-regionserver1 + ports: + - "16030:16030" # RegionServer Web UI + - "16020:16020" # RegionServer RPC + environment: + HBASE_CONF_hbase_rootdir: hdfs://namenode:9000/hbase + HBASE_CONF_hbase_cluster_distributed: 'true' + HBASE_CONF_hbase_zookeeper_quorum: zookeeper:2181 + HBASE_CONF_hbase_master: hbase-master:16000 + HBASE_CONF_hbase_regionserver_hostname: hbase-regionserver1 + HBASE_CONF_hbase_regionserver_port: 16020 + HBASE_CONF_hbase_regionserver_info_port: 16030 + depends_on: + - hbase-master + volumes: + - hbase_rs1_data:/opt/hbase/data + + hbase-regionserver2: + image: harisekhon/hbase:2.4 + container_name: hbase-regionserver2 + hostname: hbase-regionserver2 + ports: + - "16031:16030" # RegionServer Web UI + - "16021:16020" # RegionServer RPC + environment: + HBASE_CONF_hbase_rootdir: hdfs://namenode:9000/hbase + HBASE_CONF_hbase_cluster_distributed: 'true' + HBASE_CONF_hbase_zookeeper_quorum: zookeeper:2181 + HBASE_CONF_hbase_master: hbase-master:16000 + HBASE_CONF_hbase_regionserver_hostname: hbase-regionserver2 + HBASE_CONF_hbase_regionserver_port: 16020 + HBASE_CONF_hbase_regionserver_info_port: 16030 + depends_on: + - hbase-master + volumes: + - hbase_rs2_data:/opt/hbase/data + + namenode: + image: apache/hadoop:3.3.4 + container_name: hadoop-namenode + hostname: namenode + ports: + - "9870:9870" # Namenode Web UI + - "9000:9000" # Namenode RPC + environment: + CLUSTER_NAME: hadoop-cluster + command: ["/opt/hadoop/bin/hdfs", "namenode"] + volumes: + - namenode_data:/opt/hadoop/data + + datanode: + image: apache/hadoop:3.3.4 + container_name: hadoop-datanode + hostname: datanode + ports: + - "9864:9864" # Datanode Web UI + environment: + CLUSTER_NAME: hadoop-cluster + command: ["/opt/hadoop/bin/hdfs", "datanode"] + depends_on: + - namenode + volumes: + - datanode_data:/opt/hadoop/data + +volumes: + zk_data: + zk_logs: + hbase_data: + hbase_rs1_data: + hbase_rs2_data: + namenode_data: + datanode_data: +``` + +### 初始化脚本 + +```bash +#!/bin/bash +# init-hbase.sh + +echo "启动HBase集群..." +docker-compose up -d + +echo "等待服务启动..." +sleep 60 + +echo "创建测试表..." +docker exec -it hbase-master hbase shell << 'EOF' +create 'user_table', 'info', 'stats' +create 'order_table', 'detail', 'payment' +create 'log_table', 'content' +list +EOF + +echo "HBase集群初始化完成" +echo "HBase Master Web UI: http://localhost:16010" +echo "RegionServer1 Web UI: http://localhost:16030" +echo "RegionServer2 Web UI: http://localhost:16031" +``` + +## Java应用集成 + +### Maven依赖 + +```xml + + + org.apache.hbase + hbase-client + 2.4.17 + + + org.apache.hbase + hbase-common + 2.4.17 + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + +``` + +### Spring Boot配置 + +```java +@Configuration +@EnableConfigurationProperties(HBaseProperties.class) +public class HBaseConfig { + + @Autowired + private HBaseProperties hbaseProperties; + + @Bean + public Connection hbaseConnection() throws IOException { + org.apache.hadoop.conf.Configuration config = HBaseConfiguration.create(); + + // 设置ZooKeeper连接 + config.set("hbase.zookeeper.quorum", hbaseProperties.getZookeeperQuorum()); + config.set("hbase.zookeeper.property.clientPort", hbaseProperties.getZookeeperPort()); + + // 设置HBase连接参数 + config.set("hbase.client.retries.number", "3"); + config.set("hbase.client.pause", "1000"); + config.set("hbase.rpc.timeout", "60000"); + config.set("hbase.client.operation.timeout", "120000"); + config.set("hbase.client.scanner.timeout.period", "120000"); + + return ConnectionFactory.createConnection(config); + } + + @Bean + public Admin hbaseAdmin(Connection connection) throws IOException { + return connection.getAdmin(); + } +} + +@ConfigurationProperties(prefix = "hbase") +@Data +public class HBaseProperties { + private String zookeeperQuorum = "localhost"; + private String zookeeperPort = "2181"; + private int maxConnections = 100; + private int coreConnections = 10; +} +``` + +### HBase操作服务 + +```java +@Service +@Slf4j +public class HBaseService { + + @Autowired + private Connection connection; + + @Autowired + private Admin admin; + + /** + * 创建表 + */ + public void createTable(String tableName, String... columnFamilies) { + try { + TableName table = TableName.valueOf(tableName); + + if (admin.tableExists(table)) { + log.warn("表已存在: {}", tableName); + return; + } + + TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(table); + + // 添加列族 + for (String cf : columnFamilies) { + ColumnFamilyDescriptor cfDesc = ColumnFamilyDescriptorBuilder + .newBuilder(Bytes.toBytes(cf)) + .setMaxVersions(3) + .setTimeToLive(86400 * 30) // 30天TTL + .setCompressionType(Compression.Algorithm.SNAPPY) + .build(); + builder.setColumnFamily(cfDesc); + } + + // 预分区 + byte[][] splitKeys = generateSplitKeys(tableName); + admin.createTable(builder.build(), splitKeys); + + log.info("创建表成功: {}", tableName); + + } catch (IOException e) { + log.error("创建表失败: {}", tableName, e); + throw new RuntimeException(e); + } + } + + /** + * 生成预分区键 + */ + private byte[][] generateSplitKeys(String tableName) { + List splitKeys = new ArrayList<>(); + + if (tableName.contains("user")) { + // 用户表按用户ID前缀分区 + for (int i = 1; i < 16; i++) { + splitKeys.add(Bytes.toBytes(String.format("%02x", i))); + } + } else if (tableName.contains("order")) { + // 订单表按时间分区 + LocalDate start = LocalDate.now().minusMonths(12); + for (int i = 0; i < 12; i++) { + String partition = start.plusMonths(i).format(DateTimeFormatter.ofPattern("yyyyMM")); + splitKeys.add(Bytes.toBytes(partition)); + } + } else { + // 默认按哈希分区 + for (int i = 1; i < 10; i++) { + splitKeys.add(Bytes.toBytes(String.valueOf(i))); + } + } + + return splitKeys.toArray(new byte[0][]); + } + + /** + * 插入数据 + */ + public void put(String tableName, String rowKey, String columnFamily, + String column, String value) { + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + Put put = new Put(Bytes.toBytes(rowKey)); + put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(column), + Bytes.toBytes(value)); + table.put(put); + + } catch (IOException e) { + log.error("插入数据失败", e); + throw new RuntimeException(e); + } + } + + /** + * 批量插入 + */ + public void batchPut(String tableName, List puts) { + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + table.put(puts); + log.info("批量插入数据: {} 条", puts.size()); + + } catch (IOException e) { + log.error("批量插入失败", e); + throw new RuntimeException(e); + } + } + + /** + * 获取数据 + */ + public Result get(String tableName, String rowKey) { + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + Get get = new Get(Bytes.toBytes(rowKey)); + return table.get(get); + + } catch (IOException e) { + log.error("获取数据失败", e); + throw new RuntimeException(e); + } + } + + /** + * 扫描数据 + */ + public List scan(String tableName, String startRow, String stopRow) { + List results = new ArrayList<>(); + + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + Scan scan = new Scan(); + + if (startRow != null) { + scan.withStartRow(Bytes.toBytes(startRow)); + } + if (stopRow != null) { + scan.withStopRow(Bytes.toBytes(stopRow)); + } + + try (ResultScanner scanner = table.getScanner(scan)) { + for (Result result : scanner) { + results.add(result); + } + } + + } catch (IOException e) { + log.error("扫描数据失败", e); + throw new RuntimeException(e); + } + + return results; + } + + /** + * 删除数据 + */ + public void delete(String tableName, String rowKey) { + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + Delete delete = new Delete(Bytes.toBytes(rowKey)); + table.delete(delete); + + } catch (IOException e) { + log.error("删除数据失败", e); + throw new RuntimeException(e); + } + } +} +``` + +### 分片管理服务 + +```java +@Service +@Slf4j +public class HBaseShardingService { + + @Autowired + private Connection connection; + + @Autowired + private Admin admin; + + /** + * 获取表的Region信息 + */ + public List getTableRegions(String tableName) { + try { + TableName table = TableName.valueOf(tableName); + return admin.getRegions(table); + + } catch (IOException e) { + log.error("获取Region信息失败", e); + throw new RuntimeException(e); + } + } + + /** + * 手动分割Region + */ + public void splitRegion(String tableName, String splitKey) { + try { + TableName table = TableName.valueOf(tableName); + admin.split(table, Bytes.toBytes(splitKey)); + log.info("手动分割Region: {} at {}", tableName, splitKey); + + } catch (IOException e) { + log.error("分割Region失败", e); + throw new RuntimeException(e); + } + } + + /** + * 合并Region + */ + public void mergeRegions(String tableName, String region1, String region2) { + try { + admin.mergeRegionsAsync( + Bytes.toBytes(region1), + Bytes.toBytes(region2), + false + ); + log.info("合并Region: {} + {}", region1, region2); + + } catch (IOException e) { + log.error("合并Region失败", e); + throw new RuntimeException(e); + } + } + + /** + * 移动Region + */ + public void moveRegion(String regionName, String targetServer) { + try { + admin.move(Bytes.toBytes(regionName), ServerName.valueOf(targetServer)); + log.info("移动Region: {} to {}", regionName, targetServer); + + } catch (IOException e) { + log.error("移动Region失败", e); + throw new RuntimeException(e); + } + } + + /** + * 负载均衡 + */ + public void balanceCluster() { + try { + boolean result = admin.balance(); + log.info("集群负载均衡: {}", result ? "成功" : "无需均衡"); + + } catch (IOException e) { + log.error("负载均衡失败", e); + throw new RuntimeException(e); + } + } + + /** + * 获取集群状态 + */ + public ClusterMetrics getClusterStatus() { + try { + return admin.getClusterMetrics(); + + } catch (IOException e) { + log.error("获取集群状态失败", e); + throw new RuntimeException(e); + } + } + + /** + * 监控Region分布 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorRegionDistribution() { + try { + ClusterMetrics metrics = getClusterStatus(); + + log.info("=== HBase集群状态 ==="); + log.info("活跃RegionServer数量: {}", metrics.getLiveServerMetrics().size()); + log.info("死亡RegionServer数量: {}", metrics.getDeadServerNames().size()); + + // 检查Region分布 + for (Map.Entry entry : + metrics.getLiveServerMetrics().entrySet()) { + + ServerName serverName = entry.getKey(); + ServerMetrics serverMetrics = entry.getValue(); + + log.info("RegionServer: {}", serverName.getServerName()); + log.info(" Region数量: {}", serverMetrics.getRegionMetrics().size()); + log.info(" 请求数/秒: {}", serverMetrics.getRequestCountPerSecond()); + log.info(" 读请求数/秒: {}", serverMetrics.getReadRequestsCount()); + log.info(" 写请求数/秒: {}", serverMetrics.getWriteRequestsCount()); + } + + // 检查是否需要负载均衡 + checkAndBalance(metrics); + + } catch (Exception e) { + log.error("监控Region分布失败", e); + } + } + + private void checkAndBalance(ClusterMetrics metrics) { + Map servers = metrics.getLiveServerMetrics(); + + if (servers.size() < 2) { + return; + } + + // 计算Region分布的标准差 + List regionCounts = servers.values().stream() + .map(sm -> sm.getRegionMetrics().size()) + .collect(Collectors.toList()); + + double avg = regionCounts.stream().mapToInt(Integer::intValue).average().orElse(0); + double variance = regionCounts.stream() + .mapToDouble(count -> Math.pow(count - avg, 2)) + .average().orElse(0); + double stdDev = Math.sqrt(variance); + + // 如果标准差超过阈值,触发负载均衡 + if (stdDev > 5) { + log.warn("Region分布不均衡,标准差: {}, 触发负载均衡", stdDev); + balanceCluster(); + } + } +} +``` + +## 性能优化策略 + +### 1. RowKey设计 + +```java +@Component +public class RowKeyDesigner { + + /** + * 用户表RowKey设计 + * 格式: hash(userId)_userId + */ + public String generateUserRowKey(String userId) { + String hash = String.format("%02x", Math.abs(userId.hashCode()) % 16); + return hash + "_" + userId; + } + + /** + * 订单表RowKey设计 + * 格式: yyyyMM_orderId + */ + public String generateOrderRowKey(String orderId, LocalDateTime orderTime) { + String timePrefix = orderTime.format(DateTimeFormatter.ofPattern("yyyyMM")); + return timePrefix + "_" + orderId; + } + + /** + * 日志表RowKey设计 + * 格式: yyyyMMddHH_hash(logId)_logId + */ + public String generateLogRowKey(String logId, LocalDateTime logTime) { + String timePrefix = logTime.format(DateTimeFormatter.ofPattern("yyyyMMddHH")); + String hash = String.format("%04x", Math.abs(logId.hashCode()) % 65536); + return timePrefix + "_" + hash + "_" + logId; + } + + /** + * 反向时间戳RowKey(用于获取最新数据) + * 格式: (Long.MAX_VALUE - timestamp)_id + */ + public String generateReverseTimeRowKey(String id, LocalDateTime time) { + long timestamp = time.toInstant(ZoneOffset.UTC).toEpochMilli(); + long reverseTime = Long.MAX_VALUE - timestamp; + return String.format("%019d_%s", reverseTime, id); + } +} +``` + +### 2. 批量操作优化 + +```java +@Service +public class HBaseBatchService { + + @Autowired + private Connection connection; + + private static final int BATCH_SIZE = 1000; + + /** + * 批量写入优化 + */ + public void batchWrite(String tableName, List> dataList) { + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + + List puts = new ArrayList<>(); + + for (Map data : dataList) { + Put put = createPut(data); + puts.add(put); + + // 达到批次大小时执行写入 + if (puts.size() >= BATCH_SIZE) { + table.put(puts); + puts.clear(); + } + } + + // 写入剩余数据 + if (!puts.isEmpty()) { + table.put(puts); + } + + } catch (IOException e) { + log.error("批量写入失败", e); + throw new RuntimeException(e); + } + } + + /** + * 异步批量写入 + */ + @Async + public CompletableFuture asyncBatchWrite(String tableName, + List> dataList) { + return CompletableFuture.runAsync(() -> { + batchWrite(tableName, dataList); + }); + } + + /** + * 并行扫描 + */ + public List parallelScan(String tableName, List rowKeyRanges) { + List>> futures = rowKeyRanges.stream() + .map(range -> CompletableFuture.supplyAsync(() -> { + String[] parts = range.split(","); + return scanRange(tableName, parts[0], parts[1]); + })) + .collect(Collectors.toList()); + + return futures.stream() + .map(CompletableFuture::join) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + private List scanRange(String tableName, String startRow, String stopRow) { + List results = new ArrayList<>(); + + try (Table table = connection.getTable(TableName.valueOf(tableName))) { + Scan scan = new Scan() + .withStartRow(Bytes.toBytes(startRow)) + .withStopRow(Bytes.toBytes(stopRow)) + .setCaching(1000) // 设置缓存大小 + .setBatch(100); // 设置批次大小 + + try (ResultScanner scanner = table.getScanner(scan)) { + for (Result result : scanner) { + results.add(result); + } + } + + } catch (IOException e) { + log.error("扫描范围失败: {} - {}", startRow, stopRow, e); + } + + return results; + } + + private Put createPut(Map data) { + String rowKey = (String) data.get("rowKey"); + Put put = new Put(Bytes.toBytes(rowKey)); + + data.forEach((key, value) -> { + if (!"rowKey".equals(key) && value != null) { + String[] parts = key.split(":"); + if (parts.length == 2) { + put.addColumn(Bytes.toBytes(parts[0]), Bytes.toBytes(parts[1]), + Bytes.toBytes(value.toString())); + } + } + }); + + return put; + } +} +``` + +### 3. 缓存策略 + +```java +@Service +public class HBaseCacheService { + + @Autowired + private HBaseService hbaseService; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String CACHE_PREFIX = "hbase:"; + private static final int CACHE_TTL = 3600; // 1小时 + + /** + * 带缓存的数据获取 + */ + public Result getWithCache(String tableName, String rowKey) { + String cacheKey = CACHE_PREFIX + tableName + ":" + rowKey; + + // 先从缓存获取 + Object cached = redisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + return deserializeResult((String) cached); + } + + // 缓存未命中,从HBase获取 + Result result = hbaseService.get(tableName, rowKey); + if (!result.isEmpty()) { + // 写入缓存 + String serialized = serializeResult(result); + redisTemplate.opsForValue().set(cacheKey, serialized, CACHE_TTL, TimeUnit.SECONDS); + } + + return result; + } + + /** + * 缓存预热 + */ + @EventListener(ApplicationReadyEvent.class) + public void warmupCache() { + log.info("开始HBase缓存预热..."); + + // 预热热点数据 + List hotKeys = getHotKeys(); + hotKeys.parallelStream().forEach(key -> { + String[] parts = key.split(":"); + if (parts.length == 2) { + getWithCache(parts[0], parts[1]); + } + }); + + log.info("HBase缓存预热完成,预热数据: {} 条", hotKeys.size()); + } + + private List getHotKeys() { + // 从配置或统计数据中获取热点键 + return Arrays.asList( + "user_table:001", + "user_table:002", + "order_table:latest" + ); + } + + private String serializeResult(Result result) { + // 简化实现,实际应使用更高效的序列化方式 + Map data = new HashMap<>(); + result.rawCells().forEach(cell -> { + String family = Bytes.toString(CellUtil.cloneFamily(cell)); + String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell)); + String value = Bytes.toString(CellUtil.cloneValue(cell)); + data.put(family + ":" + qualifier, value); + }); + return JSON.toJSONString(data); + } + + private Result deserializeResult(String serialized) { + // 简化实现 + return null; // 实际需要反序列化为Result对象 + } +} +``` + +## 监控和运维 + +### 1. 集群监控服务 + +```java +@Service +@Slf4j +public class HBaseMonitoringService { + + @Autowired + private Admin admin; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控集群健康状态 + */ + @Scheduled(fixedRate = 60000) // 1分钟 + public void monitorClusterHealth() { + try { + ClusterMetrics metrics = admin.getClusterMetrics(); + + // 记录指标 + Gauge.builder("hbase.cluster.live_servers") + .register(meterRegistry, metrics.getLiveServerMetrics().size()); + + Gauge.builder("hbase.cluster.dead_servers") + .register(meterRegistry, metrics.getDeadServerNames().size()); + + Gauge.builder("hbase.cluster.regions") + .register(meterRegistry, metrics.getRegionCount()); + + // 检查异常状态 + if (!metrics.getDeadServerNames().isEmpty()) { + log.error("发现死亡RegionServer: {}", metrics.getDeadServerNames()); + sendAlert("HBase集群异常", "发现死亡RegionServer: " + metrics.getDeadServerNames()); + } + + } catch (Exception e) { + log.error("监控集群健康状态失败", e); + } + } + + /** + * 监控表级别指标 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorTableMetrics() { + try { + List tables = Arrays.asList( + TableName.valueOf("user_table"), + TableName.valueOf("order_table"), + TableName.valueOf("log_table") + ); + + for (TableName tableName : tables) { + if (admin.tableExists(tableName)) { + monitorSingleTable(tableName); + } + } + + } catch (Exception e) { + log.error("监控表指标失败", e); + } + } + + private void monitorSingleTable(TableName tableName) throws IOException { + List regions = admin.getRegions(tableName); + + log.info("表 {} 监控信息:", tableName.getNameAsString()); + log.info(" Region数量: {}", regions.size()); + + // 检查Region大小分布 + Map regionSizes = new HashMap<>(); + long totalSize = 0; + + for (RegionInfo region : regions) { + // 获取Region大小(简化实现) + long size = getRegionSize(region); + regionSizes.put(region.getRegionNameAsString(), size); + totalSize += size; + } + + log.info(" 总大小: {} MB", totalSize / 1024 / 1024); + log.info(" 平均Region大小: {} MB", totalSize / regions.size() / 1024 / 1024); + + // 检查是否需要分割 + checkRegionSplit(tableName, regionSizes); + } + + private long getRegionSize(RegionInfo region) { + // 简化实现,实际需要通过JMX或其他方式获取 + return 100 * 1024 * 1024; // 100MB + } + + private void checkRegionSplit(TableName tableName, Map regionSizes) { + long maxSize = 1024 * 1024 * 1024L; // 1GB + + regionSizes.entrySet().stream() + .filter(entry -> entry.getValue() > maxSize) + .forEach(entry -> { + log.warn("Region {} 大小超过阈值: {} MB", + entry.getKey(), entry.getValue() / 1024 / 1024); + + // 可以触发自动分割 + // splitLargeRegion(tableName, entry.getKey()); + }); + } + + /** + * 性能指标监控 + */ + @Scheduled(fixedRate = 120000) // 2分钟 + public void monitorPerformanceMetrics() { + try { + ClusterMetrics metrics = admin.getClusterMetrics(); + + for (Map.Entry entry : + metrics.getLiveServerMetrics().entrySet()) { + + ServerName serverName = entry.getKey(); + ServerMetrics serverMetrics = entry.getValue(); + + // 记录性能指标 + Tags tags = Tags.of("server", serverName.getServerName()); + + Gauge.builder("hbase.server.request_rate") + .tags(tags) + .register(meterRegistry, serverMetrics.getRequestCountPerSecond()); + + Gauge.builder("hbase.server.read_requests") + .tags(tags) + .register(meterRegistry, serverMetrics.getReadRequestsCount()); + + Gauge.builder("hbase.server.write_requests") + .tags(tags) + .register(meterRegistry, serverMetrics.getWriteRequestsCount()); + + // 检查性能异常 + if (serverMetrics.getRequestCountPerSecond() > 10000) { + log.warn("RegionServer {} 请求量过高: {}/s", + serverName.getServerName(), + serverMetrics.getRequestCountPerSecond()); + } + } + + } catch (Exception e) { + log.error("监控性能指标失败", e); + } + } + + /** + * 自动故障恢复 + */ + @EventListener + public void handleRegionServerFailure(RegionServerFailureEvent event) { + log.error("RegionServer故障: {}", event.getServerName()); + + try { + // 等待自动恢复 + Thread.sleep(30000); + + // 检查恢复状态 + ClusterMetrics metrics = admin.getClusterMetrics(); + if (metrics.getDeadServerNames().contains(event.getServerName())) { + log.error("RegionServer {} 未能自动恢复,需要人工干预", event.getServerName()); + sendAlert("HBase故障", "RegionServer " + event.getServerName() + " 需要人工恢复"); + } else { + log.info("RegionServer {} 已自动恢复", event.getServerName()); + } + + } catch (Exception e) { + log.error("处理RegionServer故障失败", e); + } + } + + private void sendAlert(String title, String message) { + // 发送告警通知(邮件、短信、钉钉等) + log.error("告警: {} - {}", title, message); + } +} + +// 自定义事件 +public class RegionServerFailureEvent { + private final ServerName serverName; + + public RegionServerFailureEvent(ServerName serverName) { + this.serverName = serverName; + } + + public ServerName getServerName() { + return serverName; + } +} +``` + +### 2. 自动化运维脚本 + +```bash +#!/bin/bash +# hbase-ops.sh - HBase运维脚本 + +HBASE_HOME="/opt/hbase" +ZK_QUORUM="localhost:2181" + +# 检查集群状态 +check_cluster_status() { + echo "检查HBase集群状态..." + + # 检查HMaster + if ! pgrep -f "HMaster" > /dev/null; then + echo "错误: HMaster未运行" + return 1 + fi + + # 检查RegionServer + rs_count=$(pgrep -f "HRegionServer" | wc -l) + if [ $rs_count -eq 0 ]; then + echo "错误: 没有运行的RegionServer" + return 1 + fi + + echo "集群状态正常: HMaster运行中, $rs_count 个RegionServer运行中" + return 0 +} + +# 备份表数据 +backup_table() { + local table_name=$1 + local backup_dir=$2 + + echo "备份表 $table_name 到 $backup_dir..." + + $HBASE_HOME/bin/hbase org.apache.hadoop.hbase.mapreduce.Export \ + $table_name $backup_dir + + if [ $? -eq 0 ]; then + echo "表 $table_name 备份成功" + else + echo "表 $table_name 备份失败" + return 1 + fi +} + +# 恢复表数据 +restore_table() { + local table_name=$1 + local backup_dir=$2 + + echo "从 $backup_dir 恢复表 $table_name..." + + $HBASE_HOME/bin/hbase org.apache.hadoop.hbase.mapreduce.Import \ + $table_name $backup_dir + + if [ $? -eq 0 ]; then + echo "表 $table_name 恢复成功" + else + echo "表 $table_name 恢复失败" + return 1 + fi +} + +# 清理旧的WAL文件 +cleanup_wal() { + echo "清理旧的WAL文件..." + + # 查找7天前的WAL文件 + find /opt/hbase/logs -name "*.log" -mtime +7 -delete + + echo "WAL文件清理完成" +} + +# 压缩表 +compact_table() { + local table_name=$1 + + echo "压缩表 $table_name..." + + echo "compact '$table_name'" | $HBASE_HOME/bin/hbase shell + + echo "表 $table_name 压缩完成" +} + +# 主函数 +main() { + case $1 in + "status") + check_cluster_status + ;; + "backup") + backup_table $2 $3 + ;; + "restore") + restore_table $2 $3 + ;; + "cleanup") + cleanup_wal + ;; + "compact") + compact_table $2 + ;; + *) + echo "用法: $0 {status|backup|restore|cleanup|compact} [参数]" + echo " status - 检查集群状态" + echo " backup
- 备份表" + echo " restore
- 恢复表" + echo " cleanup - 清理WAL文件" + echo " compact
- 压缩表" + exit 1 + ;; + esac +} + +main $@ +``` + +## 配置文件 + +### application.yml + +```yaml +spring: + application: + name: hbase-sharding-demo + +hbase: + zookeeper-quorum: localhost + zookeeper-port: 2181 + max-connections: 100 + core-connections: 10 + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus + metrics: + export: + prometheus: + enabled: true + +logging: + level: + org.apache.hadoop.hbase: INFO + com.example.hbase: DEBUG +``` + +## 最佳实践 + +### 1. RowKey设计原则 + +- **避免热点**: 使用散列前缀分散写入 +- **时间序列**: 考虑查询模式设计时间前缀 +- **长度适中**: 避免过长的RowKey影响性能 +- **字典序**: 利用字典序优化范围查询 + +### 2. 表设计优化 + +- **列族数量**: 建议不超过3个列族 +- **预分区**: 根据数据分布预先分区 +- **压缩算法**: 选择合适的压缩算法(SNAPPY、LZ4) +- **TTL设置**: 合理设置数据过期时间 + +### 3. 性能调优 + +- **批量操作**: 使用批量读写提高吞吐量 +- **缓存策略**: 合理使用BlockCache和MemStore +- **并发控制**: 控制客户端并发连接数 +- **监控告警**: 建立完善的监控体系 + +### 4. 运维管理 + +- **定期备份**: 制定数据备份策略 +- **容量规划**: 监控存储使用情况 +- **版本升级**: 制定滚动升级方案 +- **故障恢复**: 建立自动故障恢复机制 + +## 总结 + +HBase分片技术通过Region自动分割和负载均衡实现了高可扩展性和高可用性。关键要点包括: + +1. **自动分片**: Region根据大小自动分割,支持水平扩展 +2. **负载均衡**: 自动分布Region到不同RegionServer +3. **RowKey设计**: 合理的RowKey设计是性能的关键 +4. **监控运维**: 完善的监控和自动化运维保证系统稳定性 + +在实际应用中,需要根据业务特点优化RowKey设计、表结构和分片策略,并建立完善的监控和运维体系。 \ No newline at end of file diff --git "a/docs/aJava/HDFS\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/HDFS\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..cbb2192ea --- /dev/null +++ "b/docs/aJava/HDFS\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,18 @@ +# HDFS是什么 + +每天互联网上产生的PB级数据到底存哪了?全球大厂,都在用的存储神器,HDFS。HDFS全称Hadoop 分布式分布式文件系统,核心就是:分块,比如你存储10GB的电影,HDFS会把它切成128MB的小块,分散存储到十几台服务器上,这种操作叫做“分块存储”既不拍单台机器挂掉。还能让成百上千台机器同时读写,速度直接起飞。 + +这套系统核心:NameNode 和 DataNode,NameNode就是“中央大脑”,管理所有文件元数据,DataNode就是“存储节点”,负责存储数据块。 + +NameNode就是图书馆管理员,手里拿着所有文件的“目录地图”, + +DataNode就是真正存书架的工人,每个数据块默认存3份副本, + +就算两个硬盘同时炸了数据照样安全! + +这就是HDFS的王牌,副本机制,就近读取 + +使用:交易记录,用户行为日志,PB级别海量数据 + + + diff --git "a/docs/aJava/HashMap\345\272\225\345\261\202\345\216\237\347\220\206.md" "b/docs/aJava/HashMap\345\272\225\345\261\202\345\216\237\347\220\206.md" new file mode 100644 index 000000000..b5394a058 --- /dev/null +++ "b/docs/aJava/HashMap\345\272\225\345\261\202\345\216\237\347\220\206.md" @@ -0,0 +1,708 @@ +# HashMap底层原理 + +## 概述 + +HashMap是Java中最常用的数据结构之一,基于哈希表实现,提供O(1)的平均查找、插入和删除性能。本文深入分析HashMap的底层实现原理,包括数据结构、哈希算法、扩容机制、红黑树优化等核心技术。 + +## HashMap基本结构 + +### 核心属性 + +``` +public class HashMap extends AbstractMap + implements Map, Cloneable, Serializable { + + // 默认初始容量 16 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + + // 最大容量 + static final int MAXIMUM_CAPACITY = 1 << 30; + + // 默认负载因子 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + // 链表转红黑树的阈值 + static final int TREEIFY_THRESHOLD = 8; + + // 红黑树转链表的阈值 + static final int UNTREEIFY_THRESHOLD = 6; + + // 最小树化容量 + static final int MIN_TREEIFY_CAPACITY = 64; + + // 存储数据的数组 + transient Node[] table; + + // 键值对数量 + transient int size; + + // 结构修改次数 + transient int modCount; + + // 扩容阈值 + int threshold; + + // 负载因子 + final float loadFactor; +} +``` + +### Node节点结构 + +``` +static class Node implements Map.Entry { + final int hash; // 哈希值 + final K key; // 键 + V value; // 值 + Node next; // 下一个节点 + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } +} +``` + +## 哈希算法 + +### hash()方法实现 + +``` +static final int hash(Object key) { + int h; + // key为null时返回0,否则计算hashCode并进行扰动 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +**扰动函数设计原理:** +1. 获取key的hashCode值 +2. 将hashCode的高16位与低16位进行异或运算 +3. 目的:让高位也参与到索引计算中,减少哈希冲突 + +### 索引计算 + +``` +// 计算数组索引 +int index = (table.length - 1) & hash; +``` + +**为什么使用位运算:** +- HashMap的容量总是2的幂次方 +- `(n-1) & hash` 等价于 `hash % n`,但位运算更快 +- 例如:容量16,n-1=15(1111),与任何hash值相与都能得到0-15的索引 + +### 哈希冲突解决 + +HashMap使用**链地址法**解决哈希冲突: + +``` +// JDK 1.8之前:纯链表结构 +// 数组 + 链表 + +// JDK 1.8及之后:数组 + 链表 + 红黑树 +// 当链表长度超过8且数组长度大于64时,链表转换为红黑树 +``` + +## 核心方法实现 + +### put()方法 + +``` +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + + // 1. 如果table为空或长度为0,进行初始化 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + + // 2. 计算索引,如果该位置为空,直接插入 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + + // 3. 如果key已存在,记录该节点 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + + // 4. 如果是红黑树节点,调用红黑树插入方法 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + + // 5. 链表处理 + else { + for (int binCount = 0; ; ++binCount) { + // 遍历到链表末尾,插入新节点 + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + // 链表长度达到阈值,转换为红黑树 + if (binCount >= TREEIFY_THRESHOLD - 1) + treeifyBin(tab, hash); + break; + } + // 找到相同key,跳出循环 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + + // 6. 如果key已存在,更新value + if (e != null) { + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + + ++modCount; + // 7. 检查是否需要扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +### get()方法 + +``` +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + + // 1. 检查table是否为空,计算索引位置 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + + // 2. 检查第一个节点 + if (first.hash == hash && + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + + // 3. 如果有后续节点 + if ((e = first.next) != null) { + // 红黑树查找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + + // 链表查找 + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +### remove()方法 + +``` +public V remove(Object key) { + Node e; + return (e = removeNode(hash(key), key, null, false, true)) == null ? + null : e.value; +} + +final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + Node[] tab; Node p; int n, index; + + // 1. 检查table和目标位置 + if ((tab = table) != null && (n = tab.length) > 0 && + (p = tab[index = (n - 1) & hash]) != null) { + + Node node = null, e; K k; V v; + + // 2. 检查第一个节点 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + node = p; + + // 3. 查找目标节点 + else if ((e = p.next) != null) { + if (p instanceof TreeNode) + node = ((TreeNode)p).getTreeNode(hash, key); + else { + do { + if (e.hash == hash && + ((k = e.key) == key || + (key != null && key.equals(k)))) { + node = e; + break; + } + p = e; + } while ((e = e.next) != null); + } + } + + // 4. 删除节点 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + else if (node == p) + tab[index] = node.next; + else + p.next = node.next; + ++modCount; + --size; + afterNodeRemoval(node); + return node; + } + } + return null; +} +``` + +## 扩容机制 + +### resize()方法 + +``` +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + + // 1. 计算新容量和新阈值 + if (oldCap > 0) { + // 已达到最大容量 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 容量翻倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; + } + else if (oldThr > 0) + newCap = oldThr; + else { + // 初始化 + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + + threshold = newThr; + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + + // 2. 数据迁移 + if (oldTab != null) { + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + + // 只有一个节点 + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + + // 红黑树 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + + // 链表 + else { + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + + do { + next = e.next; + // 原索引 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + // 原索引 + oldCap + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + + // 放置到新数组 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +### 扩容优化 + +**JDK 1.8的扩容优化:** + +``` +// 扩容时,元素要么在原位置,要么在原位置+oldCap +// 通过 (e.hash & oldCap) 判断: +// - 结果为0:保持原索引 +// - 结果为1:新索引 = 原索引 + oldCap + +// 例如:oldCap = 16, newCap = 32 +// hash = 5: 5 & 16 = 0, 新索引 = 5 +// hash = 21: 21 & 16 = 16, 新索引 = 5 + 16 = 21 +``` + +## 红黑树优化 + +### TreeNode结构 + +``` +static final class TreeNode extends LinkedHashMap.Entry { + TreeNode parent; // 父节点 + TreeNode left; // 左子节点 + TreeNode right; // 右子节点 + TreeNode prev; // 前驱节点(维护插入顺序) + boolean red; // 颜色 + + TreeNode(int hash, K key, V val, Node next) { + super(hash, key, val, next); + } + + // 返回根节点 + final TreeNode root() { + for (TreeNode r = this, p;;) { + if ((p = r.parent) == null) + return r; + r = p; + } + } +} +``` + +### 链表转红黑树 + +``` +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + + // 如果数组长度小于64,优先扩容而不是树化 + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + else if ((e = tab[index = (n - 1) & hash]) != null) { + TreeNode hd = null, tl = null; + + // 1. 将链表节点转换为树节点 + do { + TreeNode p = replacementTreeNode(e, null); + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + + // 2. 构建红黑树 + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +### 红黑树查找 + +``` +final TreeNode getTreeNode(int h, Object k) { + return ((parent != null) ? root() : this).find(h, k, null); +} + +final TreeNode find(int h, Object k, Class kc) { + TreeNode p = this; + do { + int ph, dir; K pk; + TreeNode pl = p.left, pr = p.right, q; + + // 根据hash值比较 + if ((ph = p.hash) > h) + p = pl; + else if (ph < h) + p = pr; + + // hash相等,比较key + else if ((pk = p.key) == k || (k != null && k.equals(pk))) + return p; + + // hash相等但key不等,继续查找 + else if (pl == null) + p = pr; + else if (pr == null) + p = pl; + + // 使用Comparable接口比较 + else if ((kc != null || + (kc = comparableClassFor(k)) != null) && + (dir = compareComparables(kc, k, pk)) != 0) + p = (dir < 0) ? pl : pr; + + // 递归查找 + else if ((q = pr.find(h, k, kc)) != null) + return q; + else + p = pl; + } while (p != null); + return null; +} +``` + +## 线程安全问题 + +### 并发问题 + +**1. 数据丢失** +``` +// 两个线程同时put,可能导致数据丢失 +Thread1: put("key1", "value1") +Thread2: put("key2", "value2") +// 如果hash冲突,后执行的可能覆盖前面的 +``` + +**2. 死循环(JDK 1.7)** +``` +// JDK 1.7的扩容过程中,并发操作可能导致链表形成环 +// JDK 1.8通过改进扩容算法解决了这个问题 +``` + +**3. 数据不一致** +``` +// 扩容过程中的读操作可能读到不一致的数据 +``` + +### 解决方案 + +**1. Collections.synchronizedMap()** +``` +Map map = Collections.synchronizedMap(new HashMap<>()); +// 在每个方法上加synchronized,性能较差 +``` + +**2. ConcurrentHashMap** +``` +Map map = new ConcurrentHashMap<>(); +// 使用分段锁(JDK 1.7)或CAS+synchronized(JDK 1.8) +``` + +**3. ThreadLocal** +``` +ThreadLocal> threadLocalMap = + ThreadLocal.withInitial(HashMap::new); +// 每个线程独立的HashMap实例 +``` + +## 性能优化 + +### 1. 初始容量设置 + +``` +// 根据预期元素数量设置初始容量 +int expectedSize = 1000; +int initialCapacity = (int) (expectedSize / 0.75f) + 1; +Map map = new HashMap<>(initialCapacity); +``` + +### 2. 负载因子选择 + +``` +// 默认负载因子0.75是时间和空间的折中 +// 更小的负载因子:更少冲突,更多内存 +// 更大的负载因子:更多冲突,更少内存 +Map map = new HashMap<>(16, 0.6f); +``` + +### 3. key的hashCode优化 + +``` +public class OptimizedKey { + private final String value; + private final int hashCode; + + public OptimizedKey(String value) { + this.value = value; + this.hashCode = value.hashCode(); // 缓存hashCode + } + + @Override + public int hashCode() { + return hashCode; // 直接返回缓存值 + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof OptimizedKey)) return false; + OptimizedKey other = (OptimizedKey) obj; + return Objects.equals(value, other.value); + } +} +``` + +## 实际应用场景 + +### 1. 缓存实现 + +``` +public class LRUCache extends LinkedHashMap { + private final int capacity; + + public LRUCache(int capacity) { + super(capacity, 0.75f, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } +} +``` + +### 2. 计数器 + +``` +public class Counter { + private final Map counts = new HashMap<>(); + + public void increment(String key) { + counts.merge(key, 1, Integer::sum); + } + + public int getCount(String key) { + return counts.getOrDefault(key, 0); + } +} +``` + +### 3. 索引构建 + +``` +public class InvertedIndex { + private final Map> index = new HashMap<>(); + + public void addDocument(int docId, String content) { + String[] words = content.split("\\s+"); + for (String word : words) { + index.computeIfAbsent(word.toLowerCase(), + k -> new HashSet<>()).add(docId); + } + } + + public Set search(String word) { + return index.getOrDefault(word.toLowerCase(), + Collections.emptySet()); + } +} +``` + +## 版本演进 + +### JDK 1.7 vs JDK 1.8 + +| 特性 | JDK 1.7 | JDK 1.8 | +|------|---------|----------| +| 数据结构 | 数组+链表 | 数组+链表+红黑树 | +| 插入方式 | 头插法 | 尾插法 | +| 扩容优化 | 重新计算hash | 位运算优化 | +| 树化阈值 | 无 | 链表长度>8且数组长度>64 | +| 并发安全 | 可能死循环 | 避免了死循环 | + +### 关键改进 + +**1. 红黑树优化** +- 最坏情况下查找时间复杂度从O(n)降到O(logn) +- 避免了恶意hash攻击 + +**2. 扩容优化** +- 避免重新计算hash值 +- 保持链表顺序,避免死循环 + +**3. hash函数优化** +- 高16位参与运算,减少冲突 + +## 总结 + +HashMap的高效性能源于其精心设计的实现: + +1. **哈希算法**:扰动函数减少冲突,位运算提高性能 +2. **动态扩容**:保持合适的负载因子,优化的扩容算法 +3. **红黑树优化**:解决链表过长的性能问题 +4. **内存布局**:紧凑的数据结构,良好的缓存局部性 + +理解HashMap的底层原理,有助于我们: +- 正确使用HashMap,避免性能陷阱 +- 设计高质量的hashCode方法 +- 选择合适的初始容量和负载因子 +- 在并发场景下选择合适的替代方案 + +HashMap作为Java集合框架的核心组件,其设计思想和优化技巧值得深入学习和借鉴。 \ No newline at end of file diff --git "a/docs/aJava/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/docs/aJava/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" new file mode 100644 index 000000000..f1d09498c --- /dev/null +++ "b/docs/aJava/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -0,0 +1,58 @@ +--- +title: JVM内存区域 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## JVM内存区域 + +JVM内存: + +线程私有 Thread Local + +```java +程序计数器PC:指向虚拟机字节码指令的位置, 唯一一个无OOM的区域 + +虚拟机栈VM Stack: 虚拟机栈和线程的生命周期相同;一个线程中,每调用一个方法创建一个栈帧(Stack Frame); + +栈帧的结构:本地变量表 Local Variable, 操作数栈 Operand Stank, 对运行时常理池的引用 Runtime Constant Pool Reference + +异常:线程请求的栈深度大于JVM所允许的深度 StackOverflowError;若JVM允许动态扩展,若无法申请到足够内存OutOfMemoryError + +本地方法栈:Native Method Stack ; 异常:线程请求的栈深度大于JVM所允许的深度StackOverflowError; 若JVM允许动态扩展,若无法申请到足够内存OutOfMemoryError + +``` + +线程共享 Thread Shared + +```java +方法区(永久代) Method Area: 运行时常量池 Runtime Constant Pool + +类实例区(java堆)Objects : 新生代 eden,from survivor, to survivor ; 老年代,异常 OutOfMemoryError + +``` + +直接内存 Direct Memory + +```java +不受JVM GC管理 +``` + +JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区 +域【JAVA 堆、方法区】、直接内存。 + +线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot +VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的 +生/死对应)。 + +线程共享区域随虚拟机的启动/关闭而创建/销毁。 +直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提 +供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 +DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java +堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。 + + diff --git "a/docs/aJava/Jenkins\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/Jenkins\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..43cd2b360 --- /dev/null +++ "b/docs/aJava/Jenkins\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,9 @@ +# Jenkins是什么 + +持续集成工具,它用主从节点结构,通过Webhook监听底阿妈仓库变化,自动触发Pipeline脚本,完成编译,测试,打包,部署四连击。 + +第一:分布式构建让10台服务器同时干活;第二插件系统能对接Docker K8s甚至钉钉;第三,它的Pipeline脚本用Groovy语言写,支持条件判断,循环,函数,可以写复杂的逻辑。把部署流程写成代码。 + +用微服务拆了200个模块的架构师,要在测试,预发,生产环境反复横跳的运维,还有每次发版都手抖的新人。它甚至能帮你自动生成测试报告。钉钉直接@责任人。 + + diff --git "a/docs/aJava/MongoDB\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/MongoDB\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..af1f5b04e --- /dev/null +++ "b/docs/aJava/MongoDB\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,1207 @@ +# MongoDB分片技术实现 + +## 概述 + +MongoDB分片(Sharding)是MongoDB的水平扩展解决方案,通过将数据分布到多个分片(shard)上来处理大数据量和高吞吐量的需求。 + +## MongoDB分片架构 + +### 1. 分片集群组件 + +```yaml +# MongoDB分片集群架构 +version: '3.8' +services: + # Config Server副本集 + config1: + image: mongo:5.0 + command: mongod --configsvr --replSet configReplSet --port 27019 + ports: + - "27019:27019" + volumes: + - config1_data:/data/db + + config2: + image: mongo:5.0 + command: mongod --configsvr --replSet configReplSet --port 27019 + ports: + - "27020:27019" + volumes: + - config2_data:/data/db + + config3: + image: mongo:5.0 + command: mongod --configsvr --replSet configReplSet --port 27019 + ports: + - "27021:27019" + volumes: + - config3_data:/data/db + + # 分片1副本集 + shard1_replica1: + image: mongo:5.0 + command: mongod --shardsvr --replSet shard1ReplSet --port 27018 + ports: + - "27022:27018" + volumes: + - shard1_replica1_data:/data/db + + shard1_replica2: + image: mongo:5.0 + command: mongod --shardsvr --replSet shard1ReplSet --port 27018 + ports: + - "27023:27018" + volumes: + - shard1_replica2_data:/data/db + + # 分片2副本集 + shard2_replica1: + image: mongo:5.0 + command: mongod --shardsvr --replSet shard2ReplSet --port 27018 + ports: + - "27024:27018" + volumes: + - shard2_replica1_data:/data/db + + shard2_replica2: + image: mongo:5.0 + command: mongod --shardsvr --replSet shard2ReplSet --port 27018 + ports: + - "27025:27018" + volumes: + - shard2_replica2_data:/data/db + + # mongos路由服务 + mongos1: + image: mongo:5.0 + command: mongos --configdb configReplSet/config1:27019,config2:27019,config3:27019 --port 27017 + ports: + - "27017:27017" + depends_on: + - config1 + - config2 + - config3 + + mongos2: + image: mongo:5.0 + command: mongos --configdb configReplSet/config1:27019,config2:27019,config3:27019 --port 27017 + ports: + - "27026:27017" + depends_on: + - config1 + - config2 + - config3 + +volumes: + config1_data: + config2_data: + config3_data: + shard1_replica1_data: + shard1_replica2_data: + shard2_replica1_data: + shard2_replica2_data: +``` + +### 2. 分片集群初始化脚本 + +```bash +#!/bin/bash +# mongodb-cluster-init.sh + +echo "初始化MongoDB分片集群..." + +# 等待服务启动 +sleep 30 + +# 初始化Config Server副本集 +echo "初始化Config Server副本集..." +mongo --host config1:27019 --eval ' +rs.initiate({ + _id: "configReplSet", + configsvr: true, + members: [ + { _id: 0, host: "config1:27019" }, + { _id: 1, host: "config2:27019" }, + { _id: 2, host: "config3:27019" } + ] +})' + +# 等待副本集初始化完成 +sleep 20 + +# 初始化分片1副本集 +echo "初始化分片1副本集..." +mongo --host shard1_replica1:27018 --eval ' +rs.initiate({ + _id: "shard1ReplSet", + members: [ + { _id: 0, host: "shard1_replica1:27018" }, + { _id: 1, host: "shard1_replica2:27018" } + ] +})' + +# 初始化分片2副本集 +echo "初始化分片2副本集..." +mongo --host shard2_replica1:27018 --eval ' +rs.initiate({ + _id: "shard2ReplSet", + members: [ + { _id: 0, host: "shard2_replica1:27018" }, + { _id: 1, host: "shard2_replica2:27018" } + ] +})' + +# 等待分片副本集初始化完成 +sleep 30 + +# 添加分片到集群 +echo "添加分片到集群..." +mongo --host mongos1:27017 --eval ' +sh.addShard("shard1ReplSet/shard1_replica1:27018,shard1_replica2:27018") +sh.addShard("shard2ReplSet/shard2_replica1:27018,shard2_replica2:27018") +' + +echo "MongoDB分片集群初始化完成!" +``` + +## Java应用集成 + +### 1. Spring Boot配置 + +```java +@Configuration +public class MongoShardingConfig { + + @Value("${spring.data.mongodb.uri}") + private String mongoUri; + + @Bean + public MongoClient mongoClient() { + // 连接到mongos路由服务 + ConnectionString connectionString = new ConnectionString(mongoUri); + + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(connectionString) + .readPreference(ReadPreference.secondaryPreferred()) // 读写分离 + .writeConcern(WriteConcern.MAJORITY) // 写关注 + .readConcern(ReadConcern.MAJORITY) // 读关注 + .retryWrites(true) // 重试写入 + .retryReads(true) // 重试读取 + .applyToConnectionPoolSettings(builder -> { + builder.maxSize(100) // 最大连接数 + .minSize(10) // 最小连接数 + .maxWaitTime(30, TimeUnit.SECONDS) // 最大等待时间 + .maxConnectionIdleTime(60, TimeUnit.SECONDS); // 连接空闲时间 + }) + .build(); + + return MongoClients.create(settings); + } + + @Bean + public MongoTemplate mongoTemplate() { + return new MongoTemplate(mongoClient(), "sharded_database"); + } +} +``` + +### 2. 分片键设计 + +```java +@Document(collection = "users") +public class User { + + @Id + private String id; + + @Indexed + private String userId; // 分片键 + + private String username; + private String email; + private Date createTime; + private String region; // 地理位置 + + // 构造函数、getter、setter +} + +@Document(collection = "orders") +public class Order { + + @Id + private String id; + + @Indexed + private String customerId; // 分片键 + + private String orderId; + private BigDecimal amount; + private Date orderTime; + private String status; + + // 构造函数、getter、setter +} + +@Document(collection = "products") +public class Product { + + @Id + private String id; + + @Indexed + private String categoryId; // 分片键 + + private String productName; + private BigDecimal price; + private String description; + + // 构造函数、getter、setter +} +``` + +### 3. 分片管理服务 + +```java +@Service +public class MongoShardingService { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 启用数据库分片 + */ + public void enableSharding(String database) { + Document command = new Document("enableSharding", database); + mongoTemplate.getDb().runCommand(command); + log.info("已启用数据库分片: {}", database); + } + + /** + * 对集合进行分片 + */ + public void shardCollection(String database, String collection, String shardKey) { + Document command = new Document("shardCollection", database + "." + collection) + .append("key", new Document(shardKey, 1)); + + mongoTemplate.getDb().runCommand(command); + log.info("已对集合进行分片: {}.{}, 分片键: {}", database, collection, shardKey); + } + + /** + * 创建哈希分片 + */ + public void createHashedSharding(String database, String collection, String shardKey) { + Document command = new Document("shardCollection", database + "." + collection) + .append("key", new Document(shardKey, "hashed")); + + mongoTemplate.getDb().runCommand(command); + log.info("已创建哈希分片: {}.{}, 分片键: {}", database, collection, shardKey); + } + + /** + * 创建范围分片 + */ + public void createRangeSharding(String database, String collection, String shardKey) { + Document command = new Document("shardCollection", database + "." + collection) + .append("key", new Document(shardKey, 1)); + + mongoTemplate.getDb().runCommand(command); + log.info("已创建范围分片: {}.{}, 分片键: {}", database, collection, shardKey); + } + + /** + * 创建复合分片键 + */ + public void createCompoundSharding(String database, String collection, + Map shardKeys) { + Document keyDoc = new Document(); + shardKeys.forEach(keyDoc::append); + + Document command = new Document("shardCollection", database + "." + collection) + .append("key", keyDoc); + + mongoTemplate.getDb().runCommand(command); + log.info("已创建复合分片: {}.{}, 分片键: {}", database, collection, shardKeys); + } + + /** + * 查看分片状态 + */ + public Document getShardingStatus() { + return mongoTemplate.getDb().runCommand(new Document("sh.status", 1)); + } + + /** + * 查看集合分片信息 + */ + public Document getCollectionShardInfo(String database, String collection) { + Document command = new Document("collStats", collection) + .append("verbose", true); + + return mongoTemplate.getDb(database).runCommand(command); + } +} +``` + +### 4. 分片初始化配置 + +```java +@Component +public class ShardingInitializer { + + @Autowired + private MongoShardingService shardingService; + + @EventListener(ApplicationReadyEvent.class) + public void initializeSharding() { + try { + // 启用数据库分片 + shardingService.enableSharding("sharded_database"); + + // 用户集合 - 使用userId哈希分片 + shardingService.createHashedSharding("sharded_database", "users", "userId"); + + // 订单集合 - 使用customerId范围分片 + shardingService.createRangeSharding("sharded_database", "orders", "customerId"); + + // 产品集合 - 使用复合分片键 + Map productShardKeys = new HashMap<>(); + productShardKeys.put("categoryId", 1); + productShardKeys.put("productId", 1); + shardingService.createCompoundSharding("sharded_database", "products", productShardKeys); + + log.info("MongoDB分片初始化完成"); + + } catch (Exception e) { + log.error("MongoDB分片初始化失败", e); + } + } +} +``` + +## 分片策略优化 + +### 1. 智能分片键选择 + +```java +@Service +public class ShardKeyOptimizer { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 分析集合的查询模式 + */ + public ShardKeyRecommendation analyzeQueryPatterns(String collection) { + // 分析查询日志 + List queryLogs = getQueryLogs(collection); + + Map fieldUsageCount = new HashMap<>(); + Map fieldSelectivity = new HashMap<>(); + + for (Document log : queryLogs) { + Document query = log.get("command", Document.class); + if (query != null && query.containsKey("find")) { + Document filter = query.get("filter", Document.class); + if (filter != null) { + analyzeFilterFields(filter, fieldUsageCount); + } + } + } + + // 计算字段选择性 + for (String field : fieldUsageCount.keySet()) { + double selectivity = calculateFieldSelectivity(collection, field); + fieldSelectivity.put(field, selectivity); + } + + return recommendShardKey(fieldUsageCount, fieldSelectivity); + } + + private void analyzeFilterFields(Document filter, Map fieldUsageCount) { + for (String field : filter.keySet()) { + fieldUsageCount.merge(field, 1, Integer::sum); + } + } + + private double calculateFieldSelectivity(String collection, String field) { + // 计算字段的选择性(不重复值的比例) + Aggregation aggregation = Aggregation.newAggregation( + Aggregation.group(field), + Aggregation.count().as("distinctCount") + ); + + AggregationResults results = mongoTemplate.aggregate( + aggregation, collection, Document.class); + + long distinctCount = results.getMappedResults().size(); + long totalCount = mongoTemplate.count(new Query(), collection); + + return totalCount > 0 ? (double) distinctCount / totalCount : 0; + } + + private ShardKeyRecommendation recommendShardKey(Map fieldUsageCount, + Map fieldSelectivity) { + // 综合考虑使用频率和选择性 + String recommendedField = fieldUsageCount.entrySet().stream() + .max((e1, e2) -> { + double score1 = e1.getValue() * fieldSelectivity.getOrDefault(e1.getKey(), 0.0); + double score2 = e2.getValue() * fieldSelectivity.getOrDefault(e2.getKey(), 0.0); + return Double.compare(score1, score2); + }) + .map(Map.Entry::getKey) + .orElse("_id"); + + return new ShardKeyRecommendation(recommendedField, + fieldSelectivity.getOrDefault(recommendedField, 0.0)); + } + + private List getQueryLogs(String collection) { + // 从MongoDB profiler获取查询日志 + Query query = new Query(Criteria.where("ns").is("sharded_database." + collection) + .and("ts").gte(new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000))); // 最近24小时 + + return mongoTemplate.find(query, Document.class, "system.profile"); + } + + public static class ShardKeyRecommendation { + private String field; + private double selectivity; + + public ShardKeyRecommendation(String field, double selectivity) { + this.field = field; + this.selectivity = selectivity; + } + + // getter和setter + } +} +``` + +### 2. 数据平衡监控 + +```java +@Service +public class ShardBalanceMonitor { + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控分片数据分布 + */ + @Scheduled(fixedRate = 300000) // 5分钟检查一次 + public void monitorShardDistribution() { + try { + Document shardStats = mongoTemplate.getDb().runCommand( + new Document("shardDistribution", 1)); + + analyzeShardBalance(shardStats); + + } catch (Exception e) { + log.error("分片分布监控失败", e); + } + } + + private void analyzeShardBalance(Document shardStats) { + Document shards = shardStats.get("shards", Document.class); + if (shards == null) return; + + Map shardSizes = new HashMap<>(); + long totalSize = 0; + + for (String shardName : shards.keySet()) { + Document shardInfo = shards.get(shardName, Document.class); + long size = shardInfo.getLong("size"); + shardSizes.put(shardName, size); + totalSize += size; + } + + // 计算分布不均衡度 + double imbalanceRatio = calculateImbalanceRatio(shardSizes, totalSize); + + // 记录指标 + Gauge.builder("mongodb.shard.imbalance.ratio") + .register(meterRegistry, imbalanceRatio); + + // 如果不均衡度超过阈值,触发重新平衡 + if (imbalanceRatio > 0.3) { // 30%的不均衡度 + log.warn("检测到分片数据不均衡,不均衡度: {:.2f}", imbalanceRatio); + triggerRebalance(); + } + } + + private double calculateImbalanceRatio(Map shardSizes, long totalSize) { + if (shardSizes.isEmpty() || totalSize == 0) return 0; + + double avgSize = (double) totalSize / shardSizes.size(); + double maxDeviation = shardSizes.values().stream() + .mapToDouble(size -> Math.abs(size - avgSize) / avgSize) + .max() + .orElse(0); + + return maxDeviation; + } + + private void triggerRebalance() { + try { + // 启动平衡器 + mongoTemplate.getDb().runCommand(new Document("balancerStart", 1)); + log.info("已启动分片重新平衡"); + + } catch (Exception e) { + log.error("启动分片重新平衡失败", e); + } + } + + /** + * 监控chunk分布 + */ + @Scheduled(fixedRate = 600000) // 10分钟检查一次 + public void monitorChunkDistribution() { + try { + // 查询chunks集合 + Query query = new Query(); + List chunks = mongoTemplate.find(query, Document.class, "chunks"); + + Map shardChunkCount = new HashMap<>(); + + for (Document chunk : chunks) { + String shard = chunk.getString("shard"); + shardChunkCount.merge(shard, 1, Integer::sum); + } + + // 记录每个分片的chunk数量 + for (Map.Entry entry : shardChunkCount.entrySet()) { + Gauge.builder("mongodb.shard.chunk.count") + .tag("shard", entry.getKey()) + .register(meterRegistry, entry.getValue()); + } + + } catch (Exception e) { + log.error("Chunk分布监控失败", e); + } + } +} +``` + +### 3. 查询路由优化 + +```java +@Service +public class QueryRoutingOptimizer { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 优化查询以避免跨分片操作 + */ + public List optimizedFind(Query query, Class entityClass, String collection) { + // 分析查询是否包含分片键 + if (containsShardKey(query, collection)) { + // 包含分片键,可以路由到特定分片 + return mongoTemplate.find(query, entityClass, collection); + } else { + // 不包含分片键,需要广播查询 + log.warn("查询不包含分片键,将执行跨分片查询: {}", query); + return mongoTemplate.find(query, entityClass, collection); + } + } + + /** + * 批量查询优化 + */ + public List optimizedBatchFind(List shardKeyValues, + String shardKeyField, + Class entityClass, + String collection) { + // 按分片键分组 + Map> shardGroups = groupByShardKey(shardKeyValues, shardKeyField); + + List results = new ArrayList<>(); + + // 并行查询各分片 + shardGroups.entrySet().parallelStream().forEach(entry -> { + Query query = new Query(Criteria.where(shardKeyField).in(entry.getValue())); + List shardResults = mongoTemplate.find(query, entityClass, collection); + synchronized (results) { + results.addAll(shardResults); + } + }); + + return results; + } + + /** + * 聚合查询优化 + */ + public AggregationResults optimizedAggregate(Aggregation aggregation, + String collection, + Class outputType) { + // 检查聚合管道是否可以下推到分片 + if (canPushDownToShards(aggregation)) { + return mongoTemplate.aggregate(aggregation, collection, outputType); + } else { + // 需要在mongos层进行聚合 + log.warn("聚合操作需要在mongos层执行,可能影响性能"); + return mongoTemplate.aggregate(aggregation, collection, outputType); + } + } + + private boolean containsShardKey(Query query, String collection) { + // 获取集合的分片键信息 + String shardKey = getShardKey(collection); + if (shardKey == null) return false; + + // 检查查询条件是否包含分片键 + Document queryDoc = query.getQueryObject(); + return queryDoc.containsKey(shardKey); + } + + private String getShardKey(String collection) { + try { + // 从config.collections获取分片键信息 + Query query = new Query(Criteria.where("_id").is("sharded_database." + collection)); + Document collectionInfo = mongoTemplate.findOne(query, Document.class, "collections"); + + if (collectionInfo != null) { + Document key = collectionInfo.get("key", Document.class); + if (key != null && !key.isEmpty()) { + return key.keySet().iterator().next(); + } + } + } catch (Exception e) { + log.error("获取分片键失败", e); + } + + return null; + } + + private Map> groupByShardKey(List values, String shardKeyField) { + // 根据分片键值计算目标分片 + Map> groups = new HashMap<>(); + + for (String value : values) { + String targetShard = calculateTargetShard(value, shardKeyField); + groups.computeIfAbsent(targetShard, k -> new ArrayList<>()).add(value); + } + + return groups; + } + + private String calculateTargetShard(String shardKeyValue, String shardKeyField) { + // 简化的分片计算逻辑 + int hash = shardKeyValue.hashCode(); + int shardCount = getShardCount(); + int shardIndex = Math.abs(hash) % shardCount; + return "shard" + shardIndex; + } + + private int getShardCount() { + try { + Document listShards = mongoTemplate.getDb().runCommand(new Document("listShards", 1)); + List shards = listShards.getList("shards", Document.class); + return shards != null ? shards.size() : 1; + } catch (Exception e) { + log.error("获取分片数量失败", e); + return 1; + } + } + + private boolean canPushDownToShards(Aggregation aggregation) { + // 检查聚合管道是否包含可以下推到分片的操作 + List operations = aggregation.getOperations(); + + for (AggregationOperation operation : operations) { + if (operation instanceof GroupOperation || + operation instanceof SortOperation || + operation instanceof LimitOperation) { + // 这些操作通常需要在mongos层执行 + return false; + } + } + + return true; + } +} +``` + +## 性能优化 + +### 1. 连接池优化 + +```java +@Configuration +public class MongoConnectionOptimization { + + @Bean + public MongoClientSettings mongoClientSettings() { + return MongoClientSettings.builder() + .applyToConnectionPoolSettings(builder -> { + builder.maxSize(200) // 最大连接数 + .minSize(20) // 最小连接数 + .maxWaitTime(30, TimeUnit.SECONDS) // 最大等待时间 + .maxConnectionLifeTime(60, TimeUnit.MINUTES) // 连接最大生存时间 + .maxConnectionIdleTime(30, TimeUnit.MINUTES) // 连接最大空闲时间 + .maintenanceInitialDelay(0, TimeUnit.SECONDS) + .maintenanceFrequency(30, TimeUnit.SECONDS); // 维护频率 + }) + .applyToSocketSettings(builder -> { + builder.connectTimeout(10, TimeUnit.SECONDS) // 连接超时 + .readTimeout(30, TimeUnit.SECONDS); // 读取超时 + }) + .applyToServerSettings(builder -> { + builder.heartbeatFrequency(10, TimeUnit.SECONDS) // 心跳频率 + .minHeartbeatFrequency(500, TimeUnit.MILLISECONDS); // 最小心跳频率 + }) + .build(); + } +} +``` + +### 2. 批量操作优化 + +```java +@Service +public class MongoBatchOptimization { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 批量插入优化 + */ + public void optimizedBatchInsert(List documents, String collection) { + if (documents.isEmpty()) return; + + // 按分片键分组 + Map> shardGroups = groupDocumentsByShardKey(documents, collection); + + // 并行插入各分片 + shardGroups.entrySet().parallelStream().forEach(entry -> { + List shardDocuments = entry.getValue(); + + // 分批插入,避免单次操作过大 + int batchSize = 1000; + for (int i = 0; i < shardDocuments.size(); i += batchSize) { + int endIndex = Math.min(i + batchSize, shardDocuments.size()); + List batch = shardDocuments.subList(i, endIndex); + + mongoTemplate.insert(batch, collection); + } + }); + } + + /** + * 批量更新优化 + */ + public void optimizedBatchUpdate(List updateRequests, String collection) { + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, collection); + + for (UpdateRequest request : updateRequests) { + bulkOps.updateOne(request.getQuery(), request.getUpdate()); + } + + BulkWriteResult result = bulkOps.execute(); + log.info("批量更新完成,匹配: {}, 修改: {}", + result.getMatchedCount(), result.getModifiedCount()); + } + + /** + * 批量删除优化 + */ + public void optimizedBatchDelete(List deleteQueries, String collection) { + BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, collection); + + for (Query query : deleteQueries) { + bulkOps.remove(query); + } + + BulkWriteResult result = bulkOps.execute(); + log.info("批量删除完成,删除数量: {}", result.getDeletedCount()); + } + + private Map> groupDocumentsByShardKey(List documents, String collection) { + // 根据分片键对文档进行分组 + Map> groups = new HashMap<>(); + + String shardKey = getShardKey(collection); + if (shardKey == null) { + groups.put("default", documents); + return groups; + } + + for (T document : documents) { + String shardKeyValue = extractShardKeyValue(document, shardKey); + String targetShard = calculateTargetShard(shardKeyValue); + groups.computeIfAbsent(targetShard, k -> new ArrayList<>()).add(document); + } + + return groups; + } + + private String extractShardKeyValue(Object document, String shardKey) { + // 使用反射或其他方式提取分片键值 + try { + Field field = document.getClass().getDeclaredField(shardKey); + field.setAccessible(true); + Object value = field.get(document); + return value != null ? value.toString() : ""; + } catch (Exception e) { + log.error("提取分片键值失败", e); + return ""; + } + } + + private String getShardKey(String collection) { + // 获取集合的分片键 + return "userId"; // 简化实现 + } + + private String calculateTargetShard(String shardKeyValue) { + // 计算目标分片 + return "shard0"; // 简化实现 + } + + public static class UpdateRequest { + private Query query; + private Update update; + + public UpdateRequest(Query query, Update update) { + this.query = query; + this.update = update; + } + + // getter和setter + } +} +``` + +### 3. 索引优化 + +```java +@Service +public class MongoIndexOptimization { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 创建分片友好的索引 + */ + public void createShardFriendlyIndexes(String collection) { + // 1. 分片键索引(自动创建) + + // 2. 复合索引(包含分片键) + Index compoundIndex = new Index() + .on("userId", Sort.Direction.ASC) // 分片键 + .on("createTime", Sort.Direction.DESC) + .on("status", Sort.Direction.ASC); + + mongoTemplate.indexOps(collection).ensureIndex(compoundIndex); + + // 3. 查询优化索引 + Index queryIndex = new Index() + .on("userId", Sort.Direction.ASC) // 分片键 + .on("email", Sort.Direction.ASC) + .sparse(); // 稀疏索引 + + mongoTemplate.indexOps(collection).ensureIndex(queryIndex); + + // 4. 地理位置索引 + Index geoIndex = new Index() + .on("userId", Sort.Direction.ASC) // 分片键 + .on("location", "2dsphere"); + + mongoTemplate.indexOps(collection).ensureIndex(geoIndex); + } + + /** + * 监控索引使用情况 + */ + @Scheduled(fixedRate = 3600000) // 1小时检查一次 + public void monitorIndexUsage() { + List collections = Arrays.asList("users", "orders", "products"); + + for (String collection : collections) { + try { + // 获取索引统计信息 + Document indexStats = mongoTemplate.getDb() + .getCollection(collection) + .aggregate(Arrays.asList( + new Document("$indexStats", new Document()) + )) + .first(); + + if (indexStats != null) { + analyzeIndexUsage(collection, indexStats); + } + + } catch (Exception e) { + log.error("监控索引使用情况失败: {}", collection, e); + } + } + } + + private void analyzeIndexUsage(String collection, Document indexStats) { + Document accesses = indexStats.get("accesses", Document.class); + if (accesses != null) { + long ops = accesses.getLong("ops"); + Date since = accesses.getDate("since"); + + if (ops == 0 && since != null) { + long daysSinceLastUse = (System.currentTimeMillis() - since.getTime()) / (24 * 60 * 60 * 1000); + if (daysSinceLastUse > 30) { + log.warn("索引 {} 在集合 {} 中超过30天未使用,考虑删除", + indexStats.getString("name"), collection); + } + } + } + } + + /** + * 自动创建查询优化索引 + */ + public void autoCreateQueryIndexes(String collection, List queryPatterns) { + Map fieldUsageCount = new HashMap<>(); + + // 分析查询模式 + for (Document query : queryPatterns) { + analyzeQueryFields(query, fieldUsageCount); + } + + // 创建高频查询字段的索引 + fieldUsageCount.entrySet().stream() + .filter(entry -> entry.getValue() > 100) // 使用次数超过100 + .forEach(entry -> { + String field = entry.getKey(); + if (!field.equals("_id")) { // 跳过默认索引 + Index index = new Index().on(field, Sort.Direction.ASC); + mongoTemplate.indexOps(collection).ensureIndex(index); + log.info("为字段 {} 创建索引,使用频率: {}", field, entry.getValue()); + } + }); + } + + private void analyzeQueryFields(Document query, Map fieldUsageCount) { + for (String field : query.keySet()) { + if (!field.startsWith("$")) { // 跳过操作符 + fieldUsageCount.merge(field, 1, Integer::sum); + } + } + } +} +``` + +## 监控与运维 + +### 1. 分片集群监控 + +```java +@Component +public class MongoShardingMonitor { + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控分片集群健康状态 + */ + @Scheduled(fixedRate = 30000) + public void monitorClusterHealth() { + try { + // 检查mongos状态 + Document isMaster = mongoTemplate.getDb().runCommand(new Document("isMaster", 1)); + boolean isMongos = isMaster.getBoolean("ismaster", false); + + // 检查分片状态 + Document listShards = mongoTemplate.getDb().runCommand(new Document("listShards", 1)); + List shards = listShards.getList("shards", Document.class); + + int healthyShards = 0; + int totalShards = shards.size(); + + for (Document shard : shards) { + String state = shard.getString("state"); + if ("1".equals(state)) { + healthyShards++; + } + } + + // 记录指标 + Gauge.builder("mongodb.cluster.shards.total") + .register(meterRegistry, totalShards); + Gauge.builder("mongodb.cluster.shards.healthy") + .register(meterRegistry, healthyShards); + + // 检查平衡器状态 + Document balancerStatus = mongoTemplate.getDb().runCommand( + new Document("balancerStatus", 1)); + boolean balancerEnabled = balancerStatus.getBoolean("mode", false); + + Gauge.builder("mongodb.cluster.balancer.enabled") + .register(meterRegistry, balancerEnabled ? 1 : 0); + + } catch (Exception e) { + log.error("MongoDB集群健康监控失败", e); + } + } + + /** + * 监控分片性能指标 + */ + @Scheduled(fixedRate = 60000) + public void monitorShardPerformance() { + try { + Document serverStatus = mongoTemplate.getDb().runCommand( + new Document("serverStatus", 1)); + + // 连接数 + Document connections = serverStatus.get("connections", Document.class); + if (connections != null) { + int current = connections.getInteger("current", 0); + int available = connections.getInteger("available", 0); + + Gauge.builder("mongodb.connections.current") + .register(meterRegistry, current); + Gauge.builder("mongodb.connections.available") + .register(meterRegistry, available); + } + + // 操作计数 + Document opcounters = serverStatus.get("opcounters", Document.class); + if (opcounters != null) { + long insert = opcounters.getLong("insert"); + long query = opcounters.getLong("query"); + long update = opcounters.getLong("update"); + long delete = opcounters.getLong("delete"); + + Counter.builder("mongodb.operations.insert") + .register(meterRegistry).increment(insert); + Counter.builder("mongodb.operations.query") + .register(meterRegistry).increment(query); + Counter.builder("mongodb.operations.update") + .register(meterRegistry).increment(update); + Counter.builder("mongodb.operations.delete") + .register(meterRegistry).increment(delete); + } + + } catch (Exception e) { + log.error("MongoDB性能监控失败", e); + } + } +} +``` + +### 2. 自动故障恢复 + +```java +@Service +public class MongoFailoverService { + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private NotificationService notificationService; + + /** + * 检测并处理分片故障 + */ + @Scheduled(fixedRate = 15000) + public void detectAndHandleFailures() { + try { + Document listShards = mongoTemplate.getDb().runCommand(new Document("listShards", 1)); + List shards = listShards.getList("shards", Document.class); + + for (Document shard : shards) { + String shardId = shard.getString("_id"); + String host = shard.getString("host"); + String state = shard.getString("state"); + + if (!"1".equals(state)) { + handleShardFailure(shardId, host); + } + } + + } catch (Exception e) { + log.error("分片故障检测失败", e); + } + } + + private void handleShardFailure(String shardId, String host) { + log.error("检测到分片故障: {} ({})", shardId, host); + + // 发送告警 + notificationService.sendAlert( + "MongoDB分片故障", + String.format("分片 %s (%s) 发生故障,请及时处理", shardId, host) + ); + + // 尝试自动恢复 + attemptAutoRecovery(shardId, host); + } + + private void attemptAutoRecovery(String shardId, String host) { + try { + // 检查副本集状态 + if (host.contains("/")) { + String[] parts = host.split("/"); + String replSetName = parts[0]; + String[] hosts = parts[1].split(","); + + // 尝试连接副本集的其他成员 + for (String memberHost : hosts) { + if (testConnection(memberHost)) { + log.info("副本集 {} 的成员 {} 仍然可用", replSetName, memberHost); + return; + } + } + } + + // 如果所有成员都不可用,尝试重启服务 + log.warn("分片 {} 的所有成员都不可用,需要手动干预", shardId); + + } catch (Exception e) { + log.error("自动恢复失败", e); + } + } + + private boolean testConnection(String host) { + try { + MongoClient testClient = MongoClients.create("mongodb://" + host); + testClient.getDatabase("admin").runCommand(new Document("ping", 1)); + testClient.close(); + return true; + } catch (Exception e) { + return false; + } + } +} +``` + +## 总结 + +MongoDB分片技术是处理大规模数据的重要解决方案。成功实施需要考虑: + +1. **分片键设计**:选择合适的分片键是关键,需要平衡查询性能和数据分布 +2. **架构规划**:合理规划Config Server、分片和mongos的部署 +3. **查询优化**:尽量包含分片键以避免跨分片查询 +4. **监控运维**:建立完善的监控体系,及时发现和处理问题 + +**最佳实践:** +- 选择高基数、查询频繁的字段作为分片键 +- 使用复合分片键提高查询效率 +- 定期监控数据分布和性能指标 +- 建立自动化的故障检测和恢复机制 + +通过合理的设计和实施,MongoDB分片可以为应用提供优秀的水平扩展能力。 \ No newline at end of file diff --git "a/docs/aJava/MySQL\347\232\204InnoDB\345\216\237\347\220\206.md" "b/docs/aJava/MySQL\347\232\204InnoDB\345\216\237\347\220\206.md" new file mode 100644 index 000000000..f1a492d43 --- /dev/null +++ "b/docs/aJava/MySQL\347\232\204InnoDB\345\216\237\347\220\206.md" @@ -0,0 +1,822 @@ +# MySQL的InnoDB原理 + +## 概述 + +InnoDB是MySQL的默认存储引擎,支持事务、外键、崩溃恢复等企业级特性。本文深入分析InnoDB的核心原理,包括存储结构、索引机制、事务实现、锁机制、缓冲池管理等关键技术。 + +## InnoDB架构概览 + +### 整体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MySQL Server Layer │ +├─────────────────────────────────────────────────────────────┤ +│ InnoDB Storage Engine │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Memory Pool │ │ Disk Files │ │ +│ │ │ │ │ │ +│ │ ┌─────────────┐ │ │ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │Buffer Pool │ │ │ │System │ │User │ │ │ +│ │ │ │ │ │ │Tablespace │ │Tablespaces │ │ │ +│ │ ├─────────────┤ │ │ │(.ibdata) │ │(.ibd) │ │ │ +│ │ │Log Buffer │ │ │ └─────────────┘ └─────────────┘ │ │ +│ │ │ │ │ │ │ │ +│ │ ├─────────────┤ │ │ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │Change Buffer│ │ │ │Redo Log │ │Undo Log │ │ │ +│ │ │ │ │ │ │Files │ │ │ │ │ +│ │ └─────────────┘ │ │ │(ib_logfile) │ │ │ │ │ +│ └─────────────────┘ │ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 核心组件 + +1. **Buffer Pool**:缓存数据页和索引页 +2. **Log Buffer**:缓存redo log +3. **Change Buffer**:缓存对非唯一二级索引的修改 +4. **Adaptive Hash Index**:自适应哈希索引 +5. **Redo Log**:重做日志,保证事务持久性 +6. **Undo Log**:回滚日志,支持事务回滚和MVCC + +## 存储结构 + +### 表空间(Tablespace) + +``` +表空间 +├── 段(Segment) +│ ├── 数据段(叶子节点段) +│ ├── 索引段(非叶子节点段) +│ └── 回滚段(Undo段) +├── 区(Extent)- 64个连续页,1MB +└── 页(Page)- 16KB + ├── 文件头(File Header) + ├── 页头(Page Header) + ├── 最大最小记录(Infimum + Supremum) + ├── 用户记录(User Records) + ├── 空闲空间(Free Space) + ├── 页目录(Page Directory) + └── 文件尾(File Trailer) +``` + +### 页结构详解 + +``` +// InnoDB页结构 +struct page_t { + // 文件头(38字节) + struct { + uint32_t checksum; // 校验和 + uint32_t page_number; // 页号 + uint32_t prev_page; // 前一页 + uint32_t next_page; // 后一页 + uint64_t lsn; // 最后修改的LSN + uint16_t page_type; // 页类型 + uint64_t flush_lsn; // 刷新LSN + uint32_t space_id; // 表空间ID + } file_header; + + // 页头(56字节) + struct { + uint16_t slot_count; // 页目录槽数量 + uint16_t heap_top; // 堆顶位置 + uint16_t record_count; // 记录数量 + uint16_t max_trx_id; // 最大事务ID + uint16_t page_level; // 页层级 + uint64_t index_id; // 索引ID + // ... 其他字段 + } page_header; + + // 用户记录区域 + char user_records[...]; + + // 页目录 + uint16_t page_directory[...]; + + // 文件尾(8字节) + struct { + uint32_t checksum; // 校验和 + uint32_t lsn_low; // LSN低位 + } file_trailer; +}; +``` + +### 行格式 + +**Compact行格式:** + +``` +┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐ +│变长字段长度列表│ NULL标志位 │ 记录头信息 │ 列1数据 │ 列2数据 │ +└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘ +``` + +**记录头信息(5字节):** + +``` +struct record_header { + unsigned deleted_flag:1; // 删除标记 + unsigned min_rec_flag:1; // 最小记录标记 + unsigned n_owned:4; // 拥有的记录数 + unsigned heap_no:13; // 堆中的位置 + unsigned record_type:3; // 记录类型 + unsigned next_record:16; // 下一条记录的偏移量 +}; +``` + +## 索引机制 + +### B+树索引结构 + +``` + 根节点(非叶子节点) + ┌─────┬─────┬─────┐ + │ 10 │ 20 │ 30 │ + └──┬──┴──┬──┴──┬──┘ + │ │ │ + ┌───────┘ │ └───────┐ + │ │ │ + 非叶子节点 非叶子节点 非叶子节点 + ┌─────┬─────┐ ┌─────┬─────┐ ┌─────┬─────┐ + │ 5 │ 8 │ │ 15 │ 18 │ │ 25 │ 28 │ + └──┬──┴──┬──┘ └──┬──┴──┬──┘ └──┬──┴──┬──┘ + │ │ │ │ │ │ + ┌────┘ └────┐ │ │ │ └────┐ + │ │ │ │ │ │ + 叶子节点 叶子节点 ... ... ... 叶子节点 + ┌─┬─┬─┬─┐ ┌─┬─┬─┬─┐ ┌─┬─┬─┬─┐ + │1│2│3│4│ ──→ │5│6│7│8│ ──→ ... ──→ ... ──→ │28│29│30│31│ + └─┴─┴─┴─┘ └─┴─┴─┴─┘ └─┴─┴─┴─┘ +``` + +### 聚簇索引(主键索引) + +``` +-- 创建表 +CREATE TABLE users ( + id INT PRIMARY KEY, + name VARCHAR(50), + age INT, + email VARCHAR(100) +); + +-- 聚簇索引结构 +-- 叶子节点存储完整的行数据 +B+树叶子节点: +┌─────┬──────────────────────────────────┐ +│ id │ 完整行数据 │ +├─────┼──────────────────────────────────┤ +│ 1 │ (1, 'Alice', 25, 'alice@...') │ +│ 2 │ (2, 'Bob', 30, 'bob@...') │ +│ 3 │ (3, 'Charlie', 28, 'charlie@...')│ +└─────┴──────────────────────────────────┘ +``` + +### 二级索引(辅助索引) + +``` +-- 创建二级索引 +CREATE INDEX idx_name ON users(name); + +-- 二级索引结构 +-- 叶子节点存储索引键值和主键值 +B+树叶子节点: +┌─────────┬─────────┐ +│ name │ id │ +├─────────┼─────────┤ +│ 'Alice' │ 1 │ +│ 'Bob' │ 2 │ +│'Charlie'│ 3 │ +└─────────┴─────────┘ +``` + +### 索引查找过程 + +``` +-- 通过二级索引查找 +SELECT * FROM users WHERE name = 'Bob'; + +-- 查找步骤: +-- 1. 在name索引的B+树中查找'Bob' +-- 2. 找到对应的主键值id=2 +-- 3. 回表:在聚簇索引中查找id=2的完整记录 +``` + +### 覆盖索引优化 + +``` +-- 创建覆盖索引 +CREATE INDEX idx_name_age ON users(name, age); + +-- 覆盖索引查询(无需回表) +SELECT name, age FROM users WHERE name = 'Bob'; + +-- 索引结构: +┌─────────┬─────┬─────────┐ +│ name │ age │ id │ +├─────────┼─────┼─────────┤ +│ 'Alice' │ 25 │ 1 │ +│ 'Bob' │ 30 │ 2 │ +│'Charlie'│ 28 │ 3 │ +└─────────┴─────┴─────────┘ +``` + +## 事务实现 + +### ACID特性实现 + +**原子性(Atomicity)** +- 通过Undo Log实现事务回滚 +- 事务失败时,利用Undo Log撤销所有修改 + +**一致性(Consistency)** +- 通过约束检查、触发器等保证数据一致性 +- 事务执行前后,数据库从一个一致性状态转换到另一个一致性状态 + +**隔离性(Isolation)** +- 通过锁机制和MVCC实现事务隔离 +- 支持四种隔离级别 + +**持久性(Durability)** +- 通过Redo Log实现持久性 +- 事务提交后,修改永久保存 + +### Redo Log机制 + +``` +// Redo Log记录结构 +struct redo_log_record { + uint8_t type; // 日志类型 + uint32_t space_id; // 表空间ID + uint32_t page_number; // 页号 + uint16_t offset; // 页内偏移 + uint16_t length; // 数据长度 + uint64_t lsn; // 日志序列号 + char data[]; // 修改的数据 +}; +``` + +**WAL(Write-Ahead Logging)原则:** + +``` +1. 修改数据页之前,必须先写Redo Log +2. 事务提交时,必须先将Redo Log刷盘 +3. 数据页可以延迟刷盘(通过Checkpoint机制) +``` + +**Redo Log写入流程:** + +``` +// 简化的Redo Log写入流程 +void write_redo_log(transaction_t* trx, page_t* page, + uint16_t offset, char* data, uint16_t len) { + // 1. 生成LSN + uint64_t lsn = generate_lsn(); + + // 2. 构造Redo Log记录 + redo_log_record_t record; + record.type = REDO_INSERT; + record.space_id = page->space_id; + record.page_number = page->page_number; + record.offset = offset; + record.length = len; + record.lsn = lsn; + memcpy(record.data, data, len); + + // 3. 写入Log Buffer + log_buffer_write(&record); + + // 4. 更新页的LSN + page->lsn = lsn; + + // 5. 根据innodb_flush_log_at_trx_commit决定刷盘时机 + if (trx->state == TRX_COMMITTING) { + log_buffer_flush(); + } +} +``` + +### Undo Log机制 + +``` +// Undo Log记录结构 +struct undo_log_record { + uint8_t type; // Undo类型 + uint64_t trx_id; // 事务ID + uint64_t undo_no; // Undo序号 + uint32_t table_id; // 表ID + uint16_t info_bits; // 信息位 + char old_data[]; // 旧数据 +}; +``` + +**Undo Log类型:** + +``` +#define TRX_UNDO_INSERT_REC 11 // INSERT操作的Undo +#define TRX_UNDO_UPD_EXIST_REC 12 // UPDATE操作的Undo +#define TRX_UNDO_UPD_DEL_REC 13 // DELETE操作的Undo +#define TRX_UNDO_DEL_MARK_REC 14 // 删除标记的Undo +``` + +### MVCC实现 + +**行记录的隐藏字段:** + +``` +struct row_record { + // 用户定义的列 + char user_columns[]; + + // 隐藏字段 + uint64_t trx_id; // 创建该记录的事务ID + uint64_t roll_pointer; // 回滚指针,指向Undo Log +}; +``` + +**Read View结构:** + +``` +struct read_view { + uint64_t low_limit_id; // 最大事务ID + 1 + uint64_t up_limit_id; // 最小活跃事务ID + uint64_t creator_trx_id; // 创建该Read View的事务ID + trx_id_t* trx_ids; // 活跃事务ID数组 + uint32_t n_trx_ids; // 活跃事务数量 +}; +``` + +**可见性判断算法:** + +``` +bool is_visible(read_view_t* view, uint64_t trx_id) { + // 1. 如果trx_id等于当前事务ID,可见 + if (trx_id == view->creator_trx_id) { + return true; + } + + // 2. 如果trx_id小于最小活跃事务ID,已提交,可见 + if (trx_id < view->up_limit_id) { + return true; + } + + // 3. 如果trx_id大于等于最大事务ID,不可见 + if (trx_id >= view->low_limit_id) { + return false; + } + + // 4. 检查是否在活跃事务列表中 + for (int i = 0; i < view->n_trx_ids; i++) { + if (view->trx_ids[i] == trx_id) { + return false; // 活跃事务,不可见 + } + } + + return true; // 已提交事务,可见 +} +``` + +## 锁机制 + +### 锁的分类 + +**按锁的粒度:** +- 表级锁(Table Lock) +- 行级锁(Row Lock) +- 页级锁(Page Lock) + +**按锁的模式:** +- 共享锁(S Lock) +- 排他锁(X Lock) +- 意向共享锁(IS Lock) +- 意向排他锁(IX Lock) + +**按锁的算法:** +- Record Lock(记录锁) +- Gap Lock(间隙锁) +- Next-Key Lock(临键锁) + +### 锁兼容性矩阵 + +``` + │ S │ X │ IS │ IX │ +─────┼─────┼─────┼─────┼─────┤ + S │ ✓ │ ✗ │ ✓ │ ✗ │ + X │ ✗ │ ✗ │ ✗ │ ✗ │ + IS │ ✓ │ ✗ │ ✓ │ ✓ │ + IX │ ✗ │ ✗ │ ✓ │ ✓ │ +``` + +### Next-Key Lock实现 + +``` +-- 示例表 +CREATE TABLE test ( + id INT PRIMARY KEY, + value INT, + KEY idx_value (value) +); + +INSERT INTO test VALUES (1, 10), (5, 20), (10, 30), (15, 40); + +-- 在REPEATABLE READ隔离级别下 +SELECT * FROM test WHERE value = 20 FOR UPDATE; + +-- 加锁范围: +-- Record Lock: value = 20的记录 +-- Gap Lock: (10, 20) 和 (20, 30) 的间隙 +-- Next-Key Lock: (10, 20] 和 (20, 30) +``` + +### 死锁检测与处理 + +``` +// 死锁检测算法(简化版) +bool detect_deadlock(transaction_t* trx) { + // 构建等待图 + wait_graph_t graph; + build_wait_graph(&graph); + + // 深度优先搜索检测环 + for (int i = 0; i < graph.node_count; i++) { + if (dfs_detect_cycle(&graph, i)) { + // 发现死锁,选择代价最小的事务回滚 + transaction_t* victim = choose_victim(&graph); + rollback_transaction(victim); + return true; + } + } + + return false; +} +``` + +## Buffer Pool管理 + +### Buffer Pool结构 + +``` +struct buffer_pool { + buf_page_t* pages; // 页数组 + buf_page_hash_t* page_hash; // 页哈希表 + UT_LIST_BASE_NODE_T(buf_page_t) free_list; // 空闲页链表 + UT_LIST_BASE_NODE_T(buf_page_t) LRU_list; // LRU链表 + UT_LIST_BASE_NODE_T(buf_page_t) flush_list; // 脏页链表 + + mutex_t mutex; // 互斥锁 + uint32_t curr_size; // 当前大小 + uint32_t max_size; // 最大大小 +}; +``` + +### LRU算法优化 + +**传统LRU问题:** +- 全表扫描会污染Buffer Pool +- 预读的页面可能不会被使用 + +**InnoDB的改进LRU:** + +``` +LRU链表分为两部分: +┌─────────────────┬─────────────────┐ +│ Young区域 │ Old区域 │ +│ (热点数据) │ (冷数据) │ +└─────────────────┴─────────────────┘ + ↑ ↑ + 5/8 * LRU 3/8 * LRU +``` + +``` +// 改进的LRU算法 +void access_page(buf_page_t* page) { + if (page->in_young_region) { + // 在Young区域,移动到LRU头部 + if (should_move_to_head(page)) { + move_to_lru_head(page); + } + } else { + // 在Old区域,检查是否应该提升到Young区域 + if (page->access_time + OLD_THRESHOLD < current_time()) { + move_to_young_region(page); + } + } +} +``` + +### 脏页刷新机制 + +``` +// 脏页刷新策略 +enum flush_type { + FLUSH_LRU, // LRU刷新 + FLUSH_LIST, // 脏页链表刷新 + FLUSH_SINGLE_PAGE, // 单页刷新 + FLUSH_NEIGHBOR // 邻接页刷新 +}; + +// 刷新触发条件 +void check_flush_trigger() { + // 1. 脏页比例超过阈值 + if (dirty_page_ratio() > innodb_max_dirty_pages_pct) { + trigger_flush(FLUSH_LIST); + } + + // 2. Redo Log空间不足 + if (redo_log_space_usage() > innodb_log_file_size * 0.75) { + trigger_flush(FLUSH_LIST); + } + + // 3. 空闲页不足 + if (free_page_count() < innodb_lru_scan_depth) { + trigger_flush(FLUSH_LRU); + } +} +``` + +## 崩溃恢复 + +### 恢复流程 + +``` +// InnoDB崩溃恢复流程 +void crash_recovery() { + // 1. 扫描Redo Log,找到最后一个Checkpoint + checkpoint_t* last_checkpoint = find_last_checkpoint(); + + // 2. 从Checkpoint开始重做 + lsn_t start_lsn = last_checkpoint->lsn; + lsn_t end_lsn = get_log_end_lsn(); + + // 3. 重做阶段(Redo Phase) + for (lsn_t lsn = start_lsn; lsn < end_lsn; lsn++) { + redo_log_record_t* record = read_redo_log(lsn); + apply_redo_log(record); + } + + // 4. 回滚阶段(Undo Phase) + rollback_uncommitted_transactions(); + + // 5. 清理阶段 + cleanup_recovery_data(); +} +``` + +### Checkpoint机制 + +``` +// Checkpoint记录结构 +struct checkpoint { + uint64_t lsn; // Checkpoint LSN + uint64_t offset; // 在Redo Log中的偏移 + uint32_t log_info; // 日志信息 + uint32_t checksum; // 校验和 +}; + +// Checkpoint触发条件 +void trigger_checkpoint() { + // 1. Redo Log空间使用超过阈值 + if (redo_log_usage() > CHECKPOINT_THRESHOLD) { + perform_checkpoint(); + } + + // 2. 定时触发 + if (time_since_last_checkpoint() > CHECKPOINT_INTERVAL) { + perform_checkpoint(); + } + + // 3. 脏页数量过多 + if (dirty_page_count() > MAX_DIRTY_PAGES) { + perform_checkpoint(); + } +} +``` + +## 性能优化 + +### 配置参数优化 + +``` +-- Buffer Pool大小(建议设置为内存的70-80%) +SET GLOBAL innodb_buffer_pool_size = 8G; + +-- Buffer Pool实例数(减少锁竞争) +SET GLOBAL innodb_buffer_pool_instances = 8; + +-- Redo Log大小(影响恢复时间和写性能) +SET GLOBAL innodb_log_file_size = 1G; +SET GLOBAL innodb_log_files_in_group = 2; + +-- 刷盘策略 +SET GLOBAL innodb_flush_log_at_trx_commit = 1; -- 最安全 +SET GLOBAL innodb_flush_method = O_DIRECT; -- 避免双重缓冲 + +-- 并发控制 +SET GLOBAL innodb_thread_concurrency = 0; -- 不限制并发 +SET GLOBAL innodb_read_io_threads = 4; +SET GLOBAL innodb_write_io_threads = 4; +``` + +### 索引优化策略 + +``` +-- 1. 主键选择 +-- 推荐使用自增整数作为主键 +CREATE TABLE orders ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + order_no VARCHAR(32) UNIQUE, + user_id INT, + amount DECIMAL(10,2), + created_at TIMESTAMP +); + +-- 2. 复合索引设计 +-- 遵循最左前缀原则 +CREATE INDEX idx_user_created ON orders(user_id, created_at); + +-- 3. 覆盖索引 +-- 避免回表操作 +CREATE INDEX idx_user_amount ON orders(user_id, amount); +SELECT user_id, amount FROM orders WHERE user_id = 123; + +-- 4. 前缀索引 +-- 对于长字符串字段 +CREATE INDEX idx_order_no_prefix ON orders(order_no(10)); +``` + +### 查询优化 + +``` +-- 1. 避免全表扫描 +-- 不推荐 +SELECT * FROM orders WHERE YEAR(created_at) = 2023; + +-- 推荐 +SELECT * FROM orders +WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01'; + +-- 2. 合理使用LIMIT +-- 深分页优化 +SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20; + +-- 3. 避免SELECT * +-- 只查询需要的字段 +SELECT id, order_no, amount FROM orders WHERE user_id = 123; + +-- 4. 使用EXISTS代替IN +-- 当子查询结果集较大时 +SELECT * FROM users u +WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id); +``` + +## 监控与诊断 + +### 性能监控 + +``` +-- 1. 查看InnoDB状态 +SHOW ENGINE INNODB STATUS; + +-- 2. 监控Buffer Pool +SELECT + POOL_ID, + POOL_SIZE, + FREE_BUFFERS, + DATABASE_PAGES, + OLD_DATABASE_PAGES, + MODIFIED_DATABASE_PAGES +FROM INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS; + +-- 3. 监控锁等待 +SELECT + r.trx_id waiting_trx_id, + r.trx_mysql_thread_id waiting_thread, + r.trx_query waiting_query, + b.trx_id blocking_trx_id, + b.trx_mysql_thread_id blocking_thread, + b.trx_query blocking_query +FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS w +INNER JOIN INFORMATION_SCHEMA.INNODB_TRX b ON b.trx_id = w.blocking_trx_id +INNER JOIN INFORMATION_SCHEMA.INNODB_TRX r ON r.trx_id = w.requesting_trx_id; + +-- 4. 监控Redo Log +SHOW GLOBAL STATUS LIKE 'Innodb_log%'; +``` + +### 慢查询分析 + +``` +-- 开启慢查询日志 +SET GLOBAL slow_query_log = ON; +SET GLOBAL long_query_time = 1; +SET GLOBAL log_queries_not_using_indexes = ON; + +-- 分析执行计划 +EXPLAIN FORMAT=JSON +SELECT * FROM orders o +JOIN users u ON o.user_id = u.id +WHERE o.created_at > '2023-01-01'; + +-- 查看索引使用情况 +SELECT + TABLE_SCHEMA, + TABLE_NAME, + INDEX_NAME, + SEQ_IN_INDEX, + COLUMN_NAME, + CARDINALITY +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'your_database' +ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; +``` + +## 实际应用场景 + +### 1. 高并发OLTP系统 + +``` +-- 订单系统表设计 +CREATE TABLE orders ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + order_no VARCHAR(32) NOT NULL UNIQUE, + user_id INT NOT NULL, + status TINYINT NOT NULL DEFAULT 0, + amount DECIMAL(10,2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_user_status (user_id, status), + INDEX idx_created_at (created_at), + INDEX idx_status_created (status, created_at) +) ENGINE=InnoDB; + +-- 优化配置 +innodb_buffer_pool_size = 16G +innodb_log_file_size = 2G +innodb_flush_log_at_trx_commit = 2 +innodb_thread_concurrency = 0 +``` + +### 2. 数据仓库ETL + +``` +-- 批量插入优化 +SET autocommit = 0; +SET unique_checks = 0; +SET foreign_key_checks = 0; + +-- 使用LOAD DATA INFILE +LOAD DATA INFILE '/path/to/data.csv' +INTO TABLE staging_table +FIELDS TERMINATED BY ',' +LINES TERMINATED BY '\n'; + +COMMIT; + +-- 恢复设置 +SET unique_checks = 1; +SET foreign_key_checks = 1; +SET autocommit = 1; +``` + +### 3. 读写分离架构 + +``` +-- 主库配置(写操作) +innodb_flush_log_at_trx_commit = 1 -- 保证数据安全 +innodb_sync_binlog = 1 + +-- 从库配置(读操作) +innodb_flush_log_at_trx_commit = 2 -- 提高性能 +read_only = 1 + +-- 应用层读写分离 +// 写操作路由到主库 +writeDataSource.execute("INSERT INTO orders ..."); + +// 读操作路由到从库 +readDataSource.query("SELECT * FROM orders WHERE ..."); +``` + +## 总结 + +InnoDB作为MySQL的默认存储引擎,其强大的功能和优秀的性能源于精心设计的架构: + +1. **存储结构**:B+树索引、页式存储、聚簇索引设计 +2. **事务支持**:ACID特性、MVCC、Redo/Undo Log +3. **锁机制**:行级锁、Next-Key Lock、死锁检测 +4. **缓冲管理**:Buffer Pool、改进的LRU算法 +5. **崩溃恢复**:WAL、Checkpoint、两阶段恢复 + +理解InnoDB的底层原理,有助于我们: + +- 设计高效的数据库表结构和索引 +- 编写高性能的SQL查询 +- 合理配置数据库参数 +- 快速诊断和解决性能问题 +- 构建稳定可靠的数据库应用 + +InnoDB的设计思想和实现技术,为现代数据库系统的发展提供了重要参考,值得深入学习和研究。 \ No newline at end of file diff --git "a/docs/aJava/Oracle\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/Oracle\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..432061ce9 --- /dev/null +++ "b/docs/aJava/Oracle\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,949 @@ +# Oracle分片技术实现 + +## 概述 + +Oracle分片(Oracle Sharding)是Oracle数据库的水平扩展解决方案,通过将数据分布到多个分片数据库中来实现线性扩展。Oracle Sharding支持系统管理分片、用户定义分片和复合分片等多种分片方法。 + +## Oracle分片架构 + +### 1. 分片架构组件 + +```sql +-- 分片目录配置 +-- 1. 创建分片目录数据库 +CREATE DATABASE shardcatalog + USER SYS IDENTIFIED BY password + USER SYSTEM IDENTIFIED BY password + LOGFILE GROUP 1 ('/u01/app/oracle/oradata/shardcatalog/redo01.log') SIZE 100M, + GROUP 2 ('/u01/app/oracle/oradata/shardcatalog/redo02.log') SIZE 100M + MAXLOGFILES 5 + MAXLOGMEMBERS 5 + MAXLOGHISTORY 1 + MAXDATAFILES 100 + CHARACTER SET AL32UTF8 + NATIONAL CHARACTER SET AL16UTF16 + DATAFILE '/u01/app/oracle/oradata/shardcatalog/system01.dbf' SIZE 700M REUSE + EXTENT MANAGEMENT LOCAL + SYSAUX DATAFILE '/u01/app/oracle/oradata/shardcatalog/sysaux01.dbf' SIZE 550M REUSE + DEFAULT TABLESPACE users + DATAFILE '/u01/app/oracle/oradata/shardcatalog/users01.dbf' SIZE 500M REUSE AUTOEXTEND ON MAXSIZE UNLIMITED + DEFAULT TEMPORARY TABLESPACE tempts1 + TEMPFILE '/u01/app/oracle/oradata/shardcatalog/temp01.dbf' SIZE 20M REUSE + UNDO TABLESPACE undotbs1 + DATAFILE '/u01/app/oracle/oradata/shardcatalog/undotbs01.dbf' SIZE 200M REUSE AUTOEXTEND ON MAXSIZE UNLIMITED; +``` + +### 2. 分片配置脚本 + +```bash +#!/bin/bash +# oracle_sharding_setup.sh + +# 环境变量 +export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 +export PATH=$ORACLE_HOME/bin:$PATH +export ORACLE_SID=shardcatalog + +# 创建分片目录 +echo "配置分片目录..." +sqlplus / as sysdba << EOF +-- 启用分片 +ALTER SYSTEM SET enable_ddl_logging=TRUE; +ALTER SYSTEM SET db_create_file_dest='/u01/app/oracle/oradata'; + +-- 创建分片目录用户 +CREATE USER shard_admin IDENTIFIED BY password; +GRANT CONNECT, RESOURCE, DBA TO shard_admin; +GRANT GSMADMIN_ROLE TO shard_admin; +GRANT SYSDG, SYSBACKUP TO shard_admin; + +-- 配置全局服务管理器 +EXEC DBMS_GSM_FIX.validateShard; +EOF + +# 配置全局服务管理器 +echo "配置全局服务管理器..." +gdsctl << EOF +create gsm -gsm gsm1 -pwd password -catalog shardhost1:1521:shardcatalog -region region1 +start gsm -gsm gsm1 + +-- 添加分片组 +add shardgroup -shardgroup primary_shardgroup -deploy_as primary -region region1 +add shardgroup -shardgroup standby_shardgroup -deploy_as standby -region region1 + +-- 添加分片 +add shard -connect shardhost1:1521:shard1 -shardgroup primary_shardgroup +add shard -connect shardhost2:1521:shard2 -shardgroup primary_shardgroup +add shard -connect shardhost3:1521:shard3 -shardgroup standby_shardgroup + +-- 部署分片 +deploy +EOF + +echo "Oracle分片配置完成" +``` + +### 3. Docker Compose部署 + +```yaml +# docker-compose.yml +version: '3.8' +services: + # 分片目录数据库 + shard-catalog: + image: oracle/database:19.3.0-ee + environment: + ORACLE_SID: shardcat + ORACLE_PDB: shardcatpdb + ORACLE_PWD: OraclePassword123 + ORACLE_CHARACTERSET: AL32UTF8 + ports: + - "1521:1521" + - "5500:5500" + volumes: + - shard_catalog_data:/opt/oracle/oradata + - ./scripts:/opt/oracle/scripts/setup + hostname: shard-catalog + + # 分片1 + shard1: + image: oracle/database:19.3.0-ee + environment: + ORACLE_SID: shard1 + ORACLE_PDB: shard1pdb + ORACLE_PWD: OraclePassword123 + ORACLE_CHARACTERSET: AL32UTF8 + ports: + - "1522:1521" + volumes: + - shard1_data:/opt/oracle/oradata + hostname: shard1 + depends_on: + - shard-catalog + + # 分片2 + shard2: + image: oracle/database:19.3.0-ee + environment: + ORACLE_SID: shard2 + ORACLE_PDB: shard2pdb + ORACLE_PWD: OraclePassword123 + ORACLE_CHARACTERSET: AL32UTF8 + ports: + - "1523:1521" + volumes: + - shard2_data:/opt/oracle/oradata + hostname: shard2 + depends_on: + - shard-catalog + + # 分片3 + shard3: + image: oracle/database:19.3.0-ee + environment: + ORACLE_SID: shard3 + ORACLE_PDB: shard3pdb + ORACLE_PWD: OraclePassword123 + ORACLE_CHARACTERSET: AL32UTF8 + ports: + - "1524:1521" + volumes: + - shard3_data:/opt/oracle/oradata + hostname: shard3 + depends_on: + - shard-catalog + +volumes: + shard_catalog_data: + shard1_data: + shard2_data: + shard3_data: +``` + +## Java应用集成 + +### 1. Maven依赖 + +```xml + + + + com.oracle.database.jdbc + ojdbc8 + 21.7.0.0 + + + + + com.oracle.database.jdbc + ucp + 21.7.0.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.oracle.database.sharding + oracle-sharding + 21.7.0.0 + + +``` + +### 2. Spring Boot配置 + +```yaml +# application.yml +spring: + datasource: + # 分片目录连接 + catalog: + url: jdbc:oracle:thin:@//localhost:1521/shardcatpdb + username: shard_admin + password: OraclePassword123 + driver-class-name: oracle.jdbc.OracleDriver + + # 分片连接池配置 + sharding: + initial-pool-size: 5 + max-pool-size: 20 + min-pool-size: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + + jpa: + database-platform: org.hibernate.dialect.Oracle12cDialect + hibernate: + ddl-auto: validate + show-sql: true + properties: + hibernate: + format_sql: true + use_sql_comments: true + +# Oracle分片配置 +oracle: + sharding: + catalog-url: jdbc:oracle:thin:@//localhost:1521/shardcatpdb + service-name: sharded_service + region: region1 + chunk-size: 1000 +``` + +### 3. 分片数据源配置 + +```java +@Configuration +@EnableJpaRepositories(basePackages = "com.example.repository") +public class OracleShardingConfig { + + @Value("${oracle.sharding.catalog-url}") + private String catalogUrl; + + @Value("${oracle.sharding.service-name}") + private String serviceName; + + @Bean + @Primary + public DataSource shardingDataSource() { + try { + // 创建Oracle UCP连接池 + PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource(); + pds.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource"); + pds.setURL(catalogUrl); + pds.setUser("shard_admin"); + pds.setPassword("OraclePassword123"); + + // 配置连接池参数 + pds.setInitialPoolSize(5); + pds.setMaxPoolSize(20); + pds.setMinPoolSize(5); + pds.setConnectionWaitTimeout(30); + pds.setInactiveConnectionTimeout(600); + + // 启用分片 + pds.setConnectionProperty("oracle.jdbc.enableSharding", "true"); + pds.setConnectionProperty("oracle.jdbc.shardingKey", "true"); + + return pds; + } catch (SQLException e) { + throw new RuntimeException("Failed to create sharding data source", e); + } + } + + @Bean + public JdbcTemplate shardingJdbcTemplate(@Qualifier("shardingDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + public EntityManagerFactory entityManagerFactory(@Qualifier("shardingDataSource") DataSource dataSource) { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setDataSource(dataSource); + factory.setPackagesToScan("com.example.entity"); + factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + + Properties jpaProperties = new Properties(); + jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.Oracle12cDialect"); + jpaProperties.setProperty("hibernate.hbm2ddl.auto", "validate"); + jpaProperties.setProperty("hibernate.show_sql", "true"); + factory.setJpaProperties(jpaProperties); + + factory.afterPropertiesSet(); + return factory.getObject(); + } +} +``` + +### 4. 分片实体类 + +```java +@Entity +@Table(name = "ORDERS") +@ShardingKey("customerId") // Oracle分片注解 +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq") + @SequenceGenerator(name = "order_seq", sequenceName = "ORDER_SEQ", allocationSize = 1) + @Column(name = "ORDER_ID") + private Long orderId; + + @Column(name = "CUSTOMER_ID") + private Long customerId; + + @Column(name = "ORDER_DATE") + private LocalDate orderDate; + + @Column(name = "AMOUNT") + private BigDecimal amount; + + @Column(name = "STATUS") + private String status; + + @Column(name = "REGION") + private String region; + + // 构造函数 + public Order() {} + + public Order(Long customerId, LocalDate orderDate, BigDecimal amount, String status, String region) { + this.customerId = customerId; + this.orderDate = orderDate; + this.amount = amount; + this.status = status; + this.region = region; + } + + // Getter和Setter方法 + public Long getOrderId() { return orderId; } + public void setOrderId(Long orderId) { this.orderId = orderId; } + + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } + + public LocalDate getOrderDate() { return orderDate; } + public void setOrderDate(LocalDate orderDate) { this.orderDate = orderDate; } + + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getRegion() { return region; } + public void setRegion(String region) { this.region = region; } +} +``` + +### 5. 分片服务实现 + +```java +@Service +@Transactional +public class OracleShardingService { + + @Autowired + private OrderRepository orderRepository; + + @Autowired + @Qualifier("shardingJdbcTemplate") + private JdbcTemplate jdbcTemplate; + + /** + * 创建分片表 + */ + public void createShardedTables() { + String createTableSql = """ + CREATE SHARDED TABLE orders ( + order_id NUMBER(19) PRIMARY KEY, + customer_id NUMBER(19) NOT NULL, + order_date DATE NOT NULL, + amount NUMBER(10,2), + status VARCHAR2(20), + region VARCHAR2(50) + ) + PARTITION BY CONSISTENT HASH (customer_id) + PARTITIONS AUTO + TABLESPACE SET ts1 + """; + + jdbcTemplate.execute(createTableSql); + + // 创建序列 + String createSequenceSql = """ + CREATE SEQUENCE order_seq + START WITH 1 + INCREMENT BY 1 + NOCACHE + """; + + jdbcTemplate.execute(createSequenceSql); + } + + /** + * 使用分片键插入数据 + */ + public Order createOrder(Long customerId, BigDecimal amount, String region) { + try { + // 获取分片连接 + Connection connection = jdbcTemplate.getDataSource().getConnection(); + + // 设置分片键 + OracleShardingKey shardingKey = connection.unwrap(OracleConnection.class) + .createShardingKeyBuilder() + .subkey(customerId, JDBCType.BIGINT) + .build(); + + // 获取分片连接 + Connection shardConnection = connection.unwrap(OracleConnection.class) + .createConnectionBuilder() + .shardingKey(shardingKey) + .build(); + + // 执行插入 + String sql = """ + INSERT INTO orders (order_id, customer_id, order_date, amount, status, region) + VALUES (order_seq.NEXTVAL, ?, SYSDATE, ?, 'PENDING', ?) + """; + + try (PreparedStatement stmt = shardConnection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setLong(1, customerId); + stmt.setBigDecimal(2, amount); + stmt.setString(3, region); + + int result = stmt.executeUpdate(); + + if (result > 0) { + try (ResultSet rs = stmt.getGeneratedKeys()) { + if (rs.next()) { + Order order = new Order(); + order.setOrderId(rs.getLong(1)); + order.setCustomerId(customerId); + order.setOrderDate(LocalDate.now()); + order.setAmount(amount); + order.setStatus("PENDING"); + order.setRegion(region); + return order; + } + } + } + } + + return null; + } catch (SQLException e) { + throw new RuntimeException("Failed to create order", e); + } + } + + /** + * 根据客户ID查询订单 + */ + public List findOrdersByCustomerId(Long customerId) { + String sql = """ + SELECT order_id, customer_id, order_date, amount, status, region + FROM orders + WHERE customer_id = ? + ORDER BY order_date DESC + """; + + return jdbcTemplate.query(sql, + (rs, rowNum) -> { + Order order = new Order(); + order.setOrderId(rs.getLong("order_id")); + order.setCustomerId(rs.getLong("customer_id")); + order.setOrderDate(rs.getDate("order_date").toLocalDate()); + order.setAmount(rs.getBigDecimal("amount")); + order.setStatus(rs.getString("status")); + order.setRegion(rs.getString("region")); + return order; + }, customerId); + } + + /** + * 跨分片聚合查询 + */ + public Map getOrderStatistics() { + String sql = """ + SELECT + COUNT(*) as total_orders, + SUM(amount) as total_amount, + AVG(amount) as avg_amount, + region, + COUNT(*) as region_count + FROM orders + GROUP BY region + """; + + List> regionStats = jdbcTemplate.queryForList(sql); + + // 计算总体统计 + String totalSql = """ + SELECT + COUNT(*) as total_orders, + SUM(amount) as total_amount, + AVG(amount) as avg_amount + FROM orders + """; + + Map totalStats = jdbcTemplate.queryForMap(totalSql); + + Map result = new HashMap<>(); + result.put("total_statistics", totalStats); + result.put("region_statistics", regionStats); + + return result; + } + + /** + * 批量插入优化 + */ + @Transactional + public void batchInsertOrders(List orders) { + String sql = """ + INSERT INTO orders (order_id, customer_id, order_date, amount, status, region) + VALUES (order_seq.NEXTVAL, ?, ?, ?, ?, ?) + """; + + List batchArgs = orders.stream() + .map(order -> new Object[]{ + order.getCustomerId(), + order.getOrderDate(), + order.getAmount(), + order.getStatus(), + order.getRegion() + }) + .collect(Collectors.toList()); + + jdbcTemplate.batchUpdate(sql, batchArgs); + } +} +``` + +## 分片管理和监控 + +### 1. 分片管理服务 + +```java +@Service +public class OracleShardManagementService { + + @Autowired + @Qualifier("shardingJdbcTemplate") + private JdbcTemplate jdbcTemplate; + + /** + * 获取分片信息 + */ + public List> getShardInfo() { + String sql = """ + SELECT + shard_space, + chunk_number, + shard_group, + status, + connect_string + FROM gv$shard_chunks + ORDER BY shard_space, chunk_number + """; + + return jdbcTemplate.queryForList(sql); + } + + /** + * 获取分片统计信息 + */ + public Map getShardStatistics() { + String sql = """ + SELECT + shard_group, + COUNT(*) as chunk_count, + SUM(bytes) as total_bytes, + AVG(bytes) as avg_bytes + FROM gv$shard_chunks + GROUP BY shard_group + """; + + List> shardStats = jdbcTemplate.queryForList(sql); + + Map result = new HashMap<>(); + result.put("shard_statistics", shardStats); + result.put("timestamp", LocalDateTime.now()); + + return result; + } + + /** + * 检查分片健康状态 + */ + public List> checkShardHealth() { + String sql = """ + SELECT + shard_group, + status, + COUNT(*) as count + FROM gv$shard_chunks + GROUP BY shard_group, status + ORDER BY shard_group + """; + + return jdbcTemplate.queryForList(sql); + } + + /** + * 重新平衡分片 + */ + public void rebalanceShards() { + String sql = "BEGIN DBMS_SHARD.REBALANCE_CHUNKS; END;"; + jdbcTemplate.execute(sql); + } + + /** + * 添加新分片 + */ + public void addShard(String shardName, String connectString, String shardGroup) { + String sql = String.format( + "BEGIN DBMS_SHARD.ADD_SHARD('%s', '%s', '%s'); END;", + shardName, connectString, shardGroup + ); + jdbcTemplate.execute(sql); + } +} +``` + +### 2. 性能监控 + +```java +@Component +public class OracleShardMonitor { + + @Autowired + @Qualifier("shardingJdbcTemplate") + private JdbcTemplate jdbcTemplate; + + /** + * 监控分片性能 + */ + public Map getPerformanceMetrics() { + // 查询执行统计 + String sqlStatsSql = """ + SELECT + sql_text, + executions, + elapsed_time, + cpu_time, + buffer_gets, + disk_reads + FROM v$sql + WHERE executions > 0 + ORDER BY elapsed_time DESC + FETCH FIRST 10 ROWS ONLY + """; + + List> sqlStats = jdbcTemplate.queryForList(sqlStatsSql); + + // 会话统计 + String sessionStatsSql = """ + SELECT + COUNT(*) as total_sessions, + COUNT(CASE WHEN status = 'ACTIVE' THEN 1 END) as active_sessions, + COUNT(CASE WHEN status = 'INACTIVE' THEN 1 END) as inactive_sessions + FROM v$session + """; + + Map sessionStats = jdbcTemplate.queryForMap(sessionStatsSql); + + // 等待事件统计 + String waitEventsSql = """ + SELECT + event, + total_waits, + total_timeouts, + time_waited, + average_wait + FROM v$system_event + WHERE total_waits > 0 + ORDER BY time_waited DESC + FETCH FIRST 10 ROWS ONLY + """; + + List> waitEvents = jdbcTemplate.queryForList(waitEventsSql); + + Map metrics = new HashMap<>(); + metrics.put("sql_statistics", sqlStats); + metrics.put("session_statistics", sessionStats); + metrics.put("wait_events", waitEvents); + metrics.put("timestamp", LocalDateTime.now()); + + return metrics; + } + + /** + * 监控表空间使用情况 + */ + public List> getTablespaceUsage() { + String sql = """ + SELECT + ts.tablespace_name, + ROUND(ts.total_mb, 2) as total_mb, + ROUND(ts.used_mb, 2) as used_mb, + ROUND(ts.free_mb, 2) as free_mb, + ROUND((ts.used_mb / ts.total_mb) * 100, 2) as usage_percent + FROM ( + SELECT + tablespace_name, + SUM(bytes) / 1024 / 1024 as total_mb, + SUM(bytes) / 1024 / 1024 - NVL(f.free_mb, 0) as used_mb, + NVL(f.free_mb, 0) as free_mb + FROM dba_data_files df + LEFT JOIN ( + SELECT + tablespace_name, + SUM(bytes) / 1024 / 1024 as free_mb + FROM dba_free_space + GROUP BY tablespace_name + ) f ON df.tablespace_name = f.tablespace_name + GROUP BY df.tablespace_name, f.free_mb + ) ts + ORDER BY usage_percent DESC + """; + + return jdbcTemplate.queryForList(sql); + } +} +``` + +### 3. 自动化运维脚本 + +```bash +#!/bin/bash +# oracle_shard_maintenance.sh + +# Oracle环境变量 +export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 +export PATH=$ORACLE_HOME/bin:$PATH +export ORACLE_SID=shardcatalog + +# 日志文件 +LOG_FILE="/var/log/oracle/shard_maintenance_$(date +%Y%m%d).log" + +# 记录日志函数 +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE +} + +# 备份分片目录 +backup_shard_catalog() { + log_message "开始备份分片目录..." + + local backup_dir="/backup/oracle/$(date +%Y%m%d)" + mkdir -p $backup_dir + + expdp shard_admin/OraclePassword123@shardcatalog \ + directory=DATA_PUMP_DIR \ + dumpfile=shard_catalog_$(date +%Y%m%d_%H%M%S).dmp \ + logfile=shard_catalog_backup.log \ + full=y + + log_message "分片目录备份完成" +} + +# 检查分片状态 +check_shard_status() { + log_message "检查分片状态..." + + sqlplus -s shard_admin/OraclePassword123@shardcatalog << EOF +SET PAGESIZE 0 +SET FEEDBACK OFF +SET HEADING OFF + +SELECT 'Shard Status Check:' FROM dual; +SELECT shard_group || ' - ' || status || ' - ' || COUNT(*) +FROM gv\$shard_chunks +GROUP BY shard_group, status; + +SELECT 'Tablespace Usage:' FROM dual; +SELECT tablespace_name || ' - ' || ROUND((used_mb/total_mb)*100, 2) || '%' +FROM ( + SELECT + tablespace_name, + SUM(bytes)/1024/1024 as total_mb, + SUM(bytes)/1024/1024 - NVL(f.free_mb, 0) as used_mb + FROM dba_data_files df + LEFT JOIN ( + SELECT tablespace_name, SUM(bytes)/1024/1024 as free_mb + FROM dba_free_space + GROUP BY tablespace_name + ) f ON df.tablespace_name = f.tablespace_name + GROUP BY df.tablespace_name, f.free_mb +) +WHERE (used_mb/total_mb)*100 > 80; + +EXIT; +EOF + + log_message "分片状态检查完成" +} + +# 收集统计信息 +collect_statistics() { + log_message "收集统计信息..." + + sqlplus -s shard_admin/OraclePassword123@shardcatalog << EOF +EXEC DBMS_STATS.GATHER_SCHEMA_STATS('SHARD_ADMIN', cascade => TRUE); +EXIT; +EOF + + log_message "统计信息收集完成" +} + +# 清理旧日志 +cleanup_logs() { + log_message "清理旧日志文件..." + + # 清理30天前的日志 + find /var/log/oracle -name "*.log" -mtime +30 -delete + find $ORACLE_HOME/diag -name "*.trc" -mtime +7 -delete + + log_message "日志清理完成" +} + +# 监控分片平衡 +monitor_shard_balance() { + log_message "监控分片平衡状态..." + + sqlplus -s shard_admin/OraclePassword123@shardcatalog << EOF +SET PAGESIZE 0 +SET FEEDBACK OFF +SET HEADING OFF + +SELECT 'Shard Balance Check:' FROM dual; +SELECT shard_group || ' - Chunks: ' || COUNT(*) || ' - Total Size: ' || ROUND(SUM(bytes)/1024/1024/1024, 2) || 'GB' +FROM gv\$shard_chunks +GROUP BY shard_group +ORDER BY shard_group; + +-- 检查是否需要重新平衡 +DECLARE + max_chunks NUMBER; + min_chunks NUMBER; + chunk_diff NUMBER; +BEGIN + SELECT MAX(chunk_count), MIN(chunk_count) + INTO max_chunks, min_chunks + FROM ( + SELECT shard_group, COUNT(*) as chunk_count + FROM gv\$shard_chunks + GROUP BY shard_group + ); + + chunk_diff := max_chunks - min_chunks; + + IF chunk_diff > 10 THEN + DBMS_OUTPUT.PUT_LINE('Warning: Shard imbalance detected. Difference: ' || chunk_diff); + DBMS_OUTPUT.PUT_LINE('Consider running rebalance operation.'); + ELSE + DBMS_OUTPUT.PUT_LINE('Shard balance is acceptable. Difference: ' || chunk_diff); + END IF; +END; +/ + +EXIT; +EOF + + log_message "分片平衡监控完成" +} + +# 主函数 +main() { + case $1 in + "backup") + backup_shard_catalog + ;; + "status") + check_shard_status + ;; + "stats") + collect_statistics + ;; + "cleanup") + cleanup_logs + ;; + "balance") + monitor_shard_balance + ;; + "all") + backup_shard_catalog + check_shard_status + collect_statistics + monitor_shard_balance + cleanup_logs + ;; + *) + echo "用法: $0 {backup|status|stats|cleanup|balance|all}" + exit 1 + ;; + esac +} + +main $1 +``` + +## 最佳实践 + +### 1. 分片设计原则 + +- **选择合适的分片键**:选择查询频繁且分布均匀的字段 +- **避免热点数据**:确保数据在分片间均匀分布 +- **考虑业务逻辑**:相关数据尽量放在同一分片 +- **规划分片数量**:根据数据增长预期合理规划 + +### 2. 性能优化 + +- **使用分片键查询**:尽量在查询中包含分片键 +- **避免跨分片事务**:设计时考虑事务边界 +- **合理使用索引**:在分片键和查询字段上建立索引 +- **批量操作优化**:使用批量插入和更新 + +### 3. 运维管理 + +- **定期备份**:制定完善的备份恢复策略 +- **监控告警**:设置关键指标监控和告警 +- **容量规划**:定期评估存储和性能需求 +- **故障恢复**:建立快速故障恢复机制 + +### 4. 安全考虑 + +- **网络安全**:配置防火墙和网络隔离 +- **访问控制**:实施细粒度的权限管理 +- **数据加密**:启用透明数据加密(TDE) +- **审计日志**:启用数据库审计功能 + +## 总结 + +Oracle分片技术为企业级应用提供了强大的水平扩展能力。通过合理的架构设计、性能优化和运维管理,可以构建高性能、高可用的分布式数据库系统。Oracle Sharding的自动化管理功能和企业级特性,使其成为大型企业应用的理想选择。 \ No newline at end of file diff --git "a/docs/aJava/PostgreSQL\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/PostgreSQL\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..c87cc0d1b --- /dev/null +++ "b/docs/aJava/PostgreSQL\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,732 @@ +# PostgreSQL分片技术实现 + +## 概述 + +PostgreSQL分片是一种水平扩展技术,通过将数据分布到多个数据库实例中来提高性能和可扩展性。本文档介绍PostgreSQL的分片实现方案,包括原生分区、Citus扩展和应用层分片。 + +## PostgreSQL分片架构 + +### 1. 原生分区(Partitioning) + +```sql +-- 创建分区表 +CREATE TABLE orders ( + id BIGSERIAL, + user_id BIGINT NOT NULL, + order_date DATE NOT NULL, + amount DECIMAL(10,2), + status VARCHAR(20) +) PARTITION BY RANGE (order_date); + +-- 创建分区 +CREATE TABLE orders_2023_q1 PARTITION OF orders + FOR VALUES FROM ('2023-01-01') TO ('2023-04-01'); + +CREATE TABLE orders_2023_q2 PARTITION OF orders + FOR VALUES FROM ('2023-04-01') TO ('2023-07-01'); + +CREATE TABLE orders_2023_q3 PARTITION OF orders + FOR VALUES FROM ('2023-07-01') TO ('2023-10-01'); + +CREATE TABLE orders_2023_q4 PARTITION OF orders + FOR VALUES FROM ('2023-10-01') TO ('2024-01-01'); +``` + +### 2. Citus分布式架构 + +```yaml +# docker-compose.yml - Citus集群 +version: '3.8' +services: + # 协调节点 + citus-coordinator: + image: citusdata/citus:11.1 + environment: + POSTGRES_DB: citus + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + PGUSER: postgres + PGPASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5432:5432" + volumes: + - citus_coordinator_data:/var/lib/postgresql/data + command: > + bash -c " + /usr/local/bin/docker-entrypoint.sh postgres & + sleep 10 + psql -h localhost -U postgres -d citus -c \"SELECT citus_set_coordinator_host('citus-coordinator', 5432);\" + wait + " + + # 工作节点1 + citus-worker1: + image: citusdata/citus:11.1 + environment: + POSTGRES_DB: citus + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + PGUSER: postgres + PGPASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5433:5432" + volumes: + - citus_worker1_data:/var/lib/postgresql/data + + # 工作节点2 + citus-worker2: + image: citusdata/citus:11.1 + environment: + POSTGRES_DB: citus + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + PGUSER: postgres + PGPASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5434:5432" + volumes: + - citus_worker2_data:/var/lib/postgresql/data + +volumes: + citus_coordinator_data: + citus_worker1_data: + citus_worker2_data: +``` + +## Java应用集成 + +### 1. Maven依赖 + +```xml + + + + org.postgresql + postgresql + 42.6.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.zaxxer + HikariCP + + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + 5.3.2 + + +``` + +### 2. Spring Boot配置 + +```yaml +# application.yml +spring: + shardingsphere: + datasource: + names: ds0,ds1,ds2 + ds0: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.postgresql.Driver + jdbc-url: jdbc:postgresql://localhost:5432/shard0 + username: postgres + password: password + ds1: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.postgresql.Driver + jdbc-url: jdbc:postgresql://localhost:5433/shard1 + username: postgres + password: password + ds2: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: org.postgresql.Driver + jdbc-url: jdbc:postgresql://localhost:5434/shard2 + username: postgres + password: password + + rules: + sharding: + tables: + t_order: + actual-data-nodes: ds$->{0..2}.t_order_$->{0..3} + table-strategy: + standard: + sharding-column: order_id + sharding-algorithm-name: t_order_table_inline + database-strategy: + standard: + sharding-column: user_id + sharding-algorithm-name: t_order_database_inline + + sharding-algorithms: + t_order_database_inline: + type: INLINE + props: + algorithm-expression: ds$->{user_id % 3} + t_order_table_inline: + type: INLINE + props: + algorithm-expression: t_order_$->{order_id % 4} + + props: + sql-show: true +``` + +### 3. 实体类定义 + +```java +@Entity +@Table(name = "t_order") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long orderId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "order_date") + private LocalDate orderDate; + + @Column(name = "amount") + private BigDecimal amount; + + @Column(name = "status") + private String status; + + // 构造函数、getter、setter + public Order() {} + + public Order(Long userId, LocalDate orderDate, BigDecimal amount, String status) { + this.userId = userId; + this.orderDate = orderDate; + this.amount = amount; + this.status = status; + } + + // getter和setter方法 + public Long getOrderId() { return orderId; } + public void setOrderId(Long orderId) { this.orderId = orderId; } + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + + public LocalDate getOrderDate() { return orderDate; } + public void setOrderDate(LocalDate orderDate) { this.orderDate = orderDate; } + + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} +``` + +### 4. 分片服务实现 + +```java +@Service +@Transactional +public class PostgreSQLShardingService { + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * 创建订单 + */ + public Order createOrder(Long userId, BigDecimal amount) { + Order order = new Order(); + order.setUserId(userId); + order.setOrderDate(LocalDate.now()); + order.setAmount(amount); + order.setStatus("PENDING"); + + return orderRepository.save(order); + } + + /** + * 根据用户ID查询订单 + */ + public List findOrdersByUserId(Long userId) { + return orderRepository.findByUserId(userId); + } + + /** + * 跨分片统计查询 + */ + public Map getOrderStatistics() { + String sql = """ + SELECT + COUNT(*) as total_orders, + SUM(amount) as total_amount, + AVG(amount) as avg_amount + FROM t_order + """; + + Map result = jdbcTemplate.queryForMap(sql); + return result; + } + + /** + * 批量插入订单 + */ + @Transactional + public void batchInsertOrders(List orders) { + String sql = """ + INSERT INTO t_order (user_id, order_date, amount, status) + VALUES (?, ?, ?, ?) + """; + + List batchArgs = orders.stream() + .map(order -> new Object[]{ + order.getUserId(), + order.getOrderDate(), + order.getAmount(), + order.getStatus() + }) + .collect(Collectors.toList()); + + jdbcTemplate.batchUpdate(sql, batchArgs); + } + + /** + * 分片信息查询 + */ + public List> getShardInfo() { + String sql = """ + SELECT + schemaname, + tablename, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size + FROM pg_tables + WHERE tablename LIKE 't_order%' + ORDER BY schemaname, tablename + """; + + return jdbcTemplate.queryForList(sql); + } +} +``` + +## Citus分布式表管理 + +### 1. 创建分布式表 + +```sql +-- 连接到协调节点 +\c citus + +-- 添加工作节点 +SELECT citus_add_node('citus-worker1', 5432); +SELECT citus_add_node('citus-worker2', 5432); + +-- 创建表 +CREATE TABLE orders ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + order_date DATE NOT NULL, + amount DECIMAL(10,2), + status VARCHAR(20) +); + +-- 创建分布式表 +SELECT create_distributed_table('orders', 'user_id'); + +-- 创建引用表(小表,在所有节点复制) +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50), + email VARCHAR(100) +); + +SELECT create_reference_table('users'); +``` + +### 2. Citus管理服务 + +```java +@Service +public class CitusManagementService { + + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * 获取集群状态 + */ + public List> getClusterStatus() { + String sql = "SELECT * FROM citus_get_active_worker_nodes()"; + return jdbcTemplate.queryForList(sql); + } + + /** + * 获取分片分布信息 + */ + public List> getShardDistribution(String tableName) { + String sql = """ + SELECT + shardid, + nodename, + nodeport, + shard_size + FROM citus_shards + WHERE table_name = ? + ORDER BY shardid + """; + return jdbcTemplate.queryForList(sql, tableName); + } + + /** + * 重新平衡分片 + */ + public void rebalanceShards() { + String sql = "SELECT citus_rebalance_start()"; + jdbcTemplate.execute(sql); + } + + /** + * 获取查询统计 + */ + public List> getQueryStats() { + String sql = """ + SELECT + query, + calls, + total_time, + mean_time + FROM citus_stat_statements + ORDER BY total_time DESC + LIMIT 10 + """; + return jdbcTemplate.queryForList(sql); + } +} +``` + +## 性能优化策略 + +### 1. 分片键选择 + +```java +@Component +public class ShardingKeyOptimizer { + + /** + * 分析分片键分布 + */ + public Map analyzeShardingKeyDistribution(String tableName, String shardingKey) { + String sql = String.format(""" + SELECT + %s, + COUNT(*) as count, + COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() as percentage + FROM %s + GROUP BY %s + ORDER BY count DESC + LIMIT 20 + """, shardingKey, tableName, shardingKey); + + // 执行分析逻辑 + return Map.of( + "distribution", "analysis_result", + "recommendation", "optimization_suggestion" + ); + } + + /** + * 检查数据倾斜 + */ + public boolean checkDataSkew(String tableName) { + String sql = """ + WITH shard_sizes AS ( + SELECT + shardid, + pg_size_bytes(shard_size) as size_bytes + FROM citus_shards + WHERE table_name = ? + ) + SELECT + MAX(size_bytes) / NULLIF(MIN(size_bytes), 0) as skew_ratio + FROM shard_sizes + """; + + // 如果倾斜比例大于2,认为存在数据倾斜 + return true; // 简化实现 + } +} +``` + +### 2. 查询优化 + +```java +@Service +public class QueryOptimizationService { + + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * 并行查询优化 + */ + public List parallelQuery(List userIds) { + // 使用CompletableFuture并行查询多个分片 + List>> futures = userIds.stream() + .map(userId -> CompletableFuture.supplyAsync(() -> { + String sql = "SELECT * FROM orders WHERE user_id = ?"; + return jdbcTemplate.query(sql, + (rs, rowNum) -> { + Order order = new Order(); + order.setOrderId(rs.getLong("id")); + order.setUserId(rs.getLong("user_id")); + order.setOrderDate(rs.getDate("order_date").toLocalDate()); + order.setAmount(rs.getBigDecimal("amount")); + order.setStatus(rs.getString("status")); + return order; + }, userId); + })) + .collect(Collectors.toList()); + + return futures.stream() + .map(CompletableFuture::join) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + /** + * 批量操作优化 + */ + @Transactional + public void optimizedBatchInsert(List orders) { + // 按分片键分组 + Map> groupedOrders = orders.stream() + .collect(Collectors.groupingBy(Order::getUserId)); + + // 并行插入每个分片 + groupedOrders.entrySet().parallelStream().forEach(entry -> { + String sql = """ + INSERT INTO orders (user_id, order_date, amount, status) + VALUES (?, ?, ?, ?) + """; + + List batchArgs = entry.getValue().stream() + .map(order -> new Object[]{ + order.getUserId(), + order.getOrderDate(), + order.getAmount(), + order.getStatus() + }) + .collect(Collectors.toList()); + + jdbcTemplate.batchUpdate(sql, batchArgs); + }); + } +} +``` + +## 监控和运维 + +### 1. 集群监控 + +```java +@Component +public class PostgreSQLClusterMonitor { + + @Autowired + private JdbcTemplate jdbcTemplate; + + /** + * 监控连接数 + */ + public Map monitorConnections() { + String sql = """ + SELECT + datname, + numbackends, + xact_commit, + xact_rollback, + blks_read, + blks_hit + FROM pg_stat_database + WHERE datname NOT IN ('template0', 'template1', 'postgres') + """; + + List> stats = jdbcTemplate.queryForList(sql); + return Map.of("database_stats", stats); + } + + /** + * 监控慢查询 + */ + public List> getSlowQueries() { + String sql = """ + SELECT + query, + calls, + total_time, + mean_time, + rows + FROM pg_stat_statements + WHERE mean_time > 1000 -- 超过1秒的查询 + ORDER BY mean_time DESC + LIMIT 10 + """; + + return jdbcTemplate.queryForList(sql); + } + + /** + * 监控表大小 + */ + public List> getTableSizes() { + String sql = """ + SELECT + schemaname, + tablename, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size, + pg_total_relation_size(schemaname||'.'||tablename) as size_bytes + FROM pg_tables + WHERE schemaname = 'public' + ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC + """; + + return jdbcTemplate.queryForList(sql); + } +} +``` + +### 2. 自动化运维脚本 + +```bash +#!/bin/bash +# postgresql_maintenance.sh + +# 数据库连接配置 +DB_HOST="localhost" +DB_PORT="5432" +DB_NAME="citus" +DB_USER="postgres" + +# 备份函数 +backup_database() { + local backup_dir="/backup/postgresql/$(date +%Y%m%d)" + mkdir -p $backup_dir + + echo "开始备份数据库..." + pg_dump -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME \ + -f "$backup_dir/citus_backup_$(date +%Y%m%d_%H%M%S).sql" + + echo "备份完成: $backup_dir" +} + +# 清理旧日志 +cleanup_logs() { + echo "清理30天前的日志文件..." + find /var/log/postgresql -name "*.log" -mtime +30 -delete + echo "日志清理完成" +} + +# 重建索引 +reindex_tables() { + echo "重建索引..." + psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "REINDEX DATABASE $DB_NAME;" + echo "索引重建完成" +} + +# 更新统计信息 +update_statistics() { + echo "更新统计信息..." + psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "ANALYZE;" + echo "统计信息更新完成" +} + +# 检查分片平衡 +check_shard_balance() { + echo "检查分片平衡状态..." + psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c " + SELECT + nodename, + COUNT(*) as shard_count, + SUM(shard_size::bigint) as total_size + FROM citus_shards + GROUP BY nodename + ORDER BY nodename; + " +} + +# 主函数 +main() { + case $1 in + "backup") + backup_database + ;; + "cleanup") + cleanup_logs + ;; + "reindex") + reindex_tables + ;; + "analyze") + update_statistics + ;; + "balance") + check_shard_balance + ;; + "all") + backup_database + cleanup_logs + update_statistics + check_shard_balance + ;; + *) + echo "用法: $0 {backup|cleanup|reindex|analyze|balance|all}" + exit 1 + ;; + esac +} + +main $1 +``` + +## 最佳实践 + +### 1. 分片设计原则 + +- **选择合适的分片键**:选择查询频繁且分布均匀的字段 +- **避免跨分片事务**:尽量将相关数据放在同一分片 +- **合理设置分片数量**:根据数据量和性能需求确定 +- **监控数据倾斜**:定期检查分片间数据分布 + +### 2. 性能优化 + +- **使用连接池**:配置合适的连接池大小 +- **批量操作**:减少网络往返次数 +- **索引优化**:在分片键和查询字段上建立索引 +- **查询路由**:优化查询以减少跨分片操作 + +### 3. 运维管理 + +- **定期备份**:制定完善的备份策略 +- **监控告警**:设置关键指标监控 +- **容量规划**:提前规划存储和计算资源 +- **故障恢复**:建立快速故障恢复机制 + +## 总结 + +PostgreSQL分片技术为大规模数据处理提供了强大的解决方案。通过合理的架构设计、性能优化和运维管理,可以构建高性能、高可用的分布式数据库系统。选择合适的分片策略和工具,结合业务特点进行优化,是成功实施PostgreSQL分片的关键。 \ No newline at end of file diff --git "a/docs/aJava/Redis\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/Redis\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..acca04f4a --- /dev/null +++ "b/docs/aJava/Redis\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,862 @@ +# Redis分片技术实现 + +## 概述 + +Redis分片(Redis Sharding)是将数据分布到多个Redis实例中的技术,用于突破单个Redis实例的内存限制,提高系统的整体性能和可用性。 + +## Redis分片方案 + +### 1. 客户端分片(Client-side Sharding) + +客户端负责决定数据存储在哪个Redis实例中。 + +```java +@Component +public class RedisClientSharding { + + private List jedisPools; + private ConsistentHash consistentHash; + + @PostConstruct + public void init() { + // 初始化Redis连接池 + jedisPools = Arrays.asList( + new JedisPool("redis-node-1:6379"), + new JedisPool("redis-node-2:6379"), + new JedisPool("redis-node-3:6379"), + new JedisPool("redis-node-4:6379") + ); + + // 初始化一致性哈希环 + consistentHash = new ConsistentHash<>(jedisPools); + } + + /** + * 根据key获取对应的Redis实例 + */ + private JedisPool getJedisPool(String key) { + return consistentHash.get(key); + } + + /** + * 设置值 + */ + public void set(String key, String value) { + JedisPool pool = getJedisPool(key); + try (Jedis jedis = pool.getResource()) { + jedis.set(key, value); + } + } + + /** + * 获取值 + */ + public String get(String key) { + JedisPool pool = getJedisPool(key); + try (Jedis jedis = pool.getResource()) { + return jedis.get(key); + } + } + + /** + * 批量获取(需要跨分片) + */ + public Map mget(String... keys) { + Map result = new HashMap<>(); + Map> poolKeyMap = new HashMap<>(); + + // 按分片分组keys + for (String key : keys) { + JedisPool pool = getJedisPool(key); + poolKeyMap.computeIfAbsent(pool, k -> new ArrayList<>()).add(key); + } + + // 并行查询各分片 + poolKeyMap.entrySet().parallelStream().forEach(entry -> { + JedisPool pool = entry.getKey(); + List poolKeys = entry.getValue(); + + try (Jedis jedis = pool.getResource()) { + List values = jedis.mget(poolKeys.toArray(new String[0])); + for (int i = 0; i < poolKeys.size(); i++) { + result.put(poolKeys.get(i), values.get(i)); + } + } + }); + + return result; + } +} +``` + +### 2. 一致性哈希实现 + +```java +public class ConsistentHash { + + private final SortedMap circle = new TreeMap<>(); + private final int virtualNodes; + private final HashFunction hashFunction; + + public ConsistentHash(Collection nodes) { + this(nodes, 150); // 默认150个虚拟节点 + } + + public ConsistentHash(Collection nodes, int virtualNodes) { + this.virtualNodes = virtualNodes; + this.hashFunction = Hashing.md5(); + + for (T node : nodes) { + addNode(node); + } + } + + /** + * 添加节点 + */ + public void addNode(T node) { + for (int i = 0; i < virtualNodes; i++) { + String virtualNodeName = node.toString() + "#" + i; + long hash = hashFunction.hashString(virtualNodeName, StandardCharsets.UTF_8).asLong(); + circle.put(hash, node); + } + } + + /** + * 移除节点 + */ + public void removeNode(T node) { + for (int i = 0; i < virtualNodes; i++) { + String virtualNodeName = node.toString() + "#" + i; + long hash = hashFunction.hashString(virtualNodeName, StandardCharsets.UTF_8).asLong(); + circle.remove(hash); + } + } + + /** + * 获取key对应的节点 + */ + public T get(String key) { + if (circle.isEmpty()) { + return null; + } + + long hash = hashFunction.hashString(key, StandardCharsets.UTF_8).asLong(); + + if (!circle.containsKey(hash)) { + SortedMap tailMap = circle.tailMap(hash); + hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); + } + + return circle.get(hash); + } +} +``` + +### 3. Redis Cluster(官方集群方案) + +```java +@Configuration +public class RedisClusterConfig { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + List nodes = Arrays.asList( + new RedisNode("redis-cluster-1", 7000), + new RedisNode("redis-cluster-2", 7000), + new RedisNode("redis-cluster-3", 7000), + new RedisNode("redis-cluster-4", 7000), + new RedisNode("redis-cluster-5", 7000), + new RedisNode("redis-cluster-6", 7000) + ); + + RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(); + clusterConfig.setClusterNodes(nodes); + clusterConfig.setMaxRedirects(3); + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(2)) + .build(); + + return new LettuceConnectionFactory(clusterConfig, clientConfig); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + + // 设置序列化器 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return template; + } +} + +@Service +public class RedisClusterService { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 设置值(自动路由到正确的分片) + */ + public void set(String key, Object value, long timeout, TimeUnit unit) { + redisTemplate.opsForValue().set(key, value, timeout, unit); + } + + /** + * 获取值 + */ + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + /** + * 批量操作(可能跨分片) + */ + public List multiGet(Collection keys) { + return redisTemplate.opsForValue().multiGet(keys); + } + + /** + * 使用Pipeline提高性能 + */ + public void batchSet(Map keyValues) { + redisTemplate.executePipelined(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + for (Map.Entry entry : keyValues.entrySet()) { + byte[] key = entry.getKey().getBytes(); + byte[] value = serialize(entry.getValue()); + connection.set(key, value); + } + return null; + } + }); + } + + private byte[] serialize(Object obj) { + // 实现序列化逻辑 + return obj.toString().getBytes(); + } +} +``` + +## Redis Cluster部署 + +### 1. 集群配置文件 + +```bash +# redis-7000.conf +port 7000 +cluster-enabled yes +cluster-config-file nodes-7000.conf +cluster-node-timeout 15000 +appendonly yes +bind 0.0.0.0 +protected-mode no + +# 启动Redis实例 +redis-server redis-7000.conf +redis-server redis-7001.conf +redis-server redis-7002.conf +redis-server redis-7003.conf +redis-server redis-7004.conf +redis-server redis-7005.conf + +# 创建集群 +redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ +127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1 +``` + +### 2. 集群管理脚本 + +```bash +#!/bin/bash +# redis-cluster-manager.sh + +CLUSTER_NODES="127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005" + +case "$1" in + start) + echo "Starting Redis Cluster..." + for port in 7000 7001 7002 7003 7004 7005; do + redis-server redis-$port.conf + done + ;; + stop) + echo "Stopping Redis Cluster..." + for port in 7000 7001 7002 7003 7004 7005; do + redis-cli -p $port shutdown + done + ;; + status) + echo "Redis Cluster Status:" + redis-cli --cluster info 127.0.0.1:7000 + ;; + add-node) + echo "Adding new node $2 to cluster..." + redis-cli --cluster add-node $2 127.0.0.1:7000 + ;; + reshard) + echo "Resharding cluster..." + redis-cli --cluster reshard 127.0.0.1:7000 + ;; + *) + echo "Usage: $0 {start|stop|status|add-node|reshard}" + exit 1 + ;; +esac +``` + +## 分片策略优化 + +### 1. 热点数据处理 + +```java +@Service +public class HotDataShardingService { + + @Autowired + private RedisTemplate redisTemplate; + + private final LoadingCache accessCounter = Caffeine.newBuilder() + .maximumSize(10000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(key -> new AtomicLong(0)); + + /** + * 检测热点key + */ + public boolean isHotKey(String key) { + long accessCount = accessCounter.get(key).incrementAndGet(); + return accessCount > 1000; // 5分钟内访问超过1000次认为是热点 + } + + /** + * 热点数据多副本存储 + */ + public void setWithHotKeyOptimization(String key, Object value) { + if (isHotKey(key)) { + // 热点数据存储多个副本 + for (int i = 0; i < 3; i++) { + String replicaKey = key + "#replica#" + i; + redisTemplate.opsForValue().set(replicaKey, value); + } + } else { + redisTemplate.opsForValue().set(key, value); + } + } + + /** + * 热点数据负载均衡读取 + */ + public Object getWithHotKeyOptimization(String key) { + if (isHotKey(key)) { + // 随机选择一个副本读取 + int replicaIndex = ThreadLocalRandom.current().nextInt(3); + String replicaKey = key + "#replica#" + replicaIndex; + Object value = redisTemplate.opsForValue().get(replicaKey); + + if (value == null) { + // 副本不存在,回退到原key + value = redisTemplate.opsForValue().get(key); + } + + return value; + } else { + return redisTemplate.opsForValue().get(key); + } + } +} +``` + +### 2. 数据倾斜处理 + +```java +@Component +public class DataSkewHandler { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 监控各分片数据分布 + */ + @Scheduled(fixedRate = 300000) // 5分钟检查一次 + public void monitorDataDistribution() { + Map shardSizes = new HashMap<>(); + + // 获取集群信息 + RedisClusterConnection clusterConnection = + (RedisClusterConnection) redisTemplate.getConnectionFactory().getConnection(); + + Iterable nodes = clusterConnection.clusterGetNodes(); + + for (RedisClusterNode node : nodes) { + if (node.isMaster()) { + RedisConnection nodeConnection = clusterConnection.getConnection(node); + Properties info = nodeConnection.info("memory"); + + String usedMemory = info.getProperty("used_memory"); + shardSizes.put(node.getId(), Long.parseLong(usedMemory)); + } + } + + // 检查数据倾斜 + checkDataSkew(shardSizes); + } + + private void checkDataSkew(Map shardSizes) { + if (shardSizes.isEmpty()) return; + + long maxSize = Collections.max(shardSizes.values()); + long minSize = Collections.min(shardSizes.values()); + + // 如果最大分片是最小分片的3倍以上,认为存在数据倾斜 + if (maxSize > minSize * 3) { + log.warn("检测到数据倾斜,最大分片: {}MB, 最小分片: {}MB", + maxSize / 1024 / 1024, minSize / 1024 / 1024); + + // 触发重新分片 + triggerReshard(); + } + } + + private void triggerReshard() { + // 实现重新分片逻辑 + log.info("开始执行重新分片操作"); + } +} +``` + +### 3. 跨分片事务处理 + +```java +@Service +public class RedisDistributedTransaction { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 使用Lua脚本实现原子操作 + */ + public boolean transferPoints(String fromUser, String toUser, int points) { + String luaScript = """ + local fromKey = KEYS[1] + local toKey = KEYS[2] + local points = tonumber(ARGV[1]) + + local fromPoints = tonumber(redis.call('GET', fromKey) or 0) + + if fromPoints >= points then + redis.call('DECRBY', fromKey, points) + redis.call('INCRBY', toKey, points) + return 1 + else + return 0 + end + """; + + DefaultRedisScript script = new DefaultRedisScript<>(); + script.setScriptText(luaScript); + script.setResultType(Long.class); + + List keys = Arrays.asList( + "user:points:" + fromUser, + "user:points:" + toUser + ); + + Long result = redisTemplate.execute(script, keys, points); + return result != null && result == 1; + } + + /** + * 分布式锁实现 + */ + public boolean tryLock(String lockKey, String requestId, long expireTime) { + String luaScript = """ + if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then + return 1 + else + return 0 + end + """; + + DefaultRedisScript script = new DefaultRedisScript<>(); + script.setScriptText(luaScript); + script.setResultType(Long.class); + + Long result = redisTemplate.execute(script, + Collections.singletonList(lockKey), requestId, expireTime); + + return result != null && result == 1; + } + + /** + * 释放分布式锁 + */ + public boolean releaseLock(String lockKey, String requestId) { + String luaScript = """ + if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('DEL', KEYS[1]) + else + return 0 + end + """; + + DefaultRedisScript script = new DefaultRedisScript<>(); + script.setScriptText(luaScript); + script.setResultType(Long.class); + + Long result = redisTemplate.execute(script, + Collections.singletonList(lockKey), requestId); + + return result != null && result == 1; + } +} +``` + +## 性能优化 + +### 1. 连接池优化 + +```java +@Configuration +public class RedisConnectionPoolConfig { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + // 连接池配置 + GenericObjectPoolConfig> poolConfig = + new GenericObjectPoolConfig<>(); + + poolConfig.setMaxTotal(200); // 最大连接数 + poolConfig.setMaxIdle(50); // 最大空闲连接数 + poolConfig.setMinIdle(10); // 最小空闲连接数 + poolConfig.setMaxWaitMillis(3000); // 获取连接最大等待时间 + poolConfig.setTestOnBorrow(true); // 获取连接时检测有效性 + poolConfig.setTestOnReturn(true); // 归还连接时检测有效性 + poolConfig.setTestWhileIdle(true); // 空闲时检测有效性 + + // Lettuce客户端配置 + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .poolingClientConfiguration(LettucePoolingClientConfiguration.builder() + .poolConfig(poolConfig) + .build()) + .commandTimeout(Duration.ofSeconds(2)) + .shutdownTimeout(Duration.ofSeconds(5)) + .build(); + + return new LettuceConnectionFactory(redisClusterConfiguration(), clientConfig); + } +} +``` + +### 2. 批量操作优化 + +```java +@Service +public class RedisBatchOptimization { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 批量设置(使用Pipeline) + */ + public void batchSet(Map data) { + redisTemplate.executePipelined(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + for (Map.Entry entry : data.entrySet()) { + byte[] key = entry.getKey().getBytes(); + byte[] value = serialize(entry.getValue()); + connection.set(key, value); + } + return null; + } + }); + } + + /** + * 批量获取(按分片分组) + */ + public Map batchGet(Set keys) { + // 按分片分组keys + Map> shardKeyMap = keys.stream() + .collect(Collectors.groupingBy(this::getShardIndex)); + + Map result = new ConcurrentHashMap<>(); + + // 并行查询各分片 + shardKeyMap.entrySet().parallelStream().forEach(entry -> { + List shardKeys = entry.getValue(); + List values = redisTemplate.opsForValue().multiGet(shardKeys); + + for (int i = 0; i < shardKeys.size(); i++) { + if (values.get(i) != null) { + result.put(shardKeys.get(i), values.get(i)); + } + } + }); + + return result; + } + + private int getShardIndex(String key) { + // 计算key对应的分片索引 + return Math.abs(key.hashCode()) % 16; // 假设16个分片 + } + + private byte[] serialize(Object obj) { + // 实现序列化 + return obj.toString().getBytes(); + } +} +``` + +### 3. 缓存预热策略 + +```java +@Service +public class RedisCacheWarmup { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserService userService; + + /** + * 应用启动时预热缓存 + */ + @EventListener(ApplicationReadyEvent.class) + public void warmupCache() { + log.info("开始缓存预热..."); + + CompletableFuture.runAsync(() -> { + try { + warmupHotUsers(); + warmupHotProducts(); + warmupConfigData(); + + log.info("缓存预热完成"); + } catch (Exception e) { + log.error("缓存预热失败", e); + } + }); + } + + /** + * 预热热点用户数据 + */ + private void warmupHotUsers() { + List hotUserIds = userService.getHotUserIds(1000); + + Map userData = new HashMap<>(); + for (Long userId : hotUserIds) { + User user = userService.getUserById(userId); + userData.put("user:" + userId, user); + } + + // 批量写入Redis + batchSetWithExpire(userData, 3600); // 1小时过期 + } + + /** + * 批量设置带过期时间 + */ + private void batchSetWithExpire(Map data, long seconds) { + redisTemplate.executePipelined(new RedisCallback() { + @Override + public Object doInRedis(RedisConnection connection) throws DataAccessException { + for (Map.Entry entry : data.entrySet()) { + byte[] key = entry.getKey().getBytes(); + byte[] value = serialize(entry.getValue()); + connection.setEx(key, seconds, value); + } + return null; + } + }); + } + + private byte[] serialize(Object obj) { + // 实现序列化 + return obj.toString().getBytes(); + } +} +``` + +## 监控与运维 + +### 1. Redis集群监控 + +```java +@Component +public class RedisClusterMonitor { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控集群状态 + */ + @Scheduled(fixedRate = 30000) + public void monitorClusterHealth() { + try { + RedisClusterConnection connection = + (RedisClusterConnection) redisTemplate.getConnectionFactory().getConnection(); + + Iterable nodes = connection.clusterGetNodes(); + + int masterCount = 0; + int slaveCount = 0; + int failedCount = 0; + + for (RedisClusterNode node : nodes) { + if (node.isMaster()) { + masterCount++; + } else { + slaveCount++; + } + + if (node.getFlags().contains(RedisClusterNode.Flag.FAIL)) { + failedCount++; + } + } + + // 记录指标 + Gauge.builder("redis.cluster.master.count") + .register(meterRegistry, masterCount); + Gauge.builder("redis.cluster.slave.count") + .register(meterRegistry, slaveCount); + Gauge.builder("redis.cluster.failed.count") + .register(meterRegistry, failedCount); + + } catch (Exception e) { + log.error("Redis集群监控失败", e); + } + } + + /** + * 监控性能指标 + */ + @Scheduled(fixedRate = 60000) + public void monitorPerformanceMetrics() { + RedisClusterConnection connection = + (RedisClusterConnection) redisTemplate.getConnectionFactory().getConnection(); + + Iterable nodes = connection.clusterGetNodes(); + + for (RedisClusterNode node : nodes) { + if (node.isMaster()) { + try { + RedisConnection nodeConnection = connection.getConnection(node); + Properties info = nodeConnection.info(); + + // 内存使用率 + long usedMemory = Long.parseLong(info.getProperty("used_memory")); + long maxMemory = Long.parseLong(info.getProperty("maxmemory")); + double memoryUsageRatio = maxMemory > 0 ? (double) usedMemory / maxMemory : 0; + + // QPS + long totalCommands = Long.parseLong(info.getProperty("total_commands_processed")); + + // 记录指标 + Gauge.builder("redis.memory.usage.ratio") + .tag("node", node.getId()) + .register(meterRegistry, memoryUsageRatio); + + Gauge.builder("redis.commands.total") + .tag("node", node.getId()) + .register(meterRegistry, totalCommands); + + } catch (Exception e) { + log.error("获取节点{}性能指标失败", node.getId(), e); + } + } + } + } +} +``` + +### 2. 故障自动恢复 + +```java +@Service +public class RedisFailoverService { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private NotificationService notificationService; + + /** + * 检测并处理节点故障 + */ + @Scheduled(fixedRate = 15000) + public void detectAndHandleFailures() { + try { + RedisClusterConnection connection = + (RedisClusterConnection) redisTemplate.getConnectionFactory().getConnection(); + + Iterable nodes = connection.clusterGetNodes(); + + for (RedisClusterNode node : nodes) { + if (node.getFlags().contains(RedisClusterNode.Flag.FAIL)) { + handleNodeFailure(node); + } + } + + } catch (Exception e) { + log.error("故障检测失败", e); + } + } + + private void handleNodeFailure(RedisClusterNode failedNode) { + log.error("检测到节点故障: {}", failedNode.getId()); + + // 发送告警 + notificationService.sendAlert( + "Redis节点故障", + String.format("节点 %s 发生故障,请及时处理", failedNode.getId()) + ); + + // 如果是主节点故障,检查是否有从节点可以提升 + if (failedNode.isMaster()) { + promoteSlaveToMaster(failedNode); + } + } + + private void promoteSlaveToMaster(RedisClusterNode failedMaster) { + // 实现从节点提升为主节点的逻辑 + log.info("尝试提升从节点为主节点,替换故障节点: {}", failedMaster.getId()); + } +} +``` + +## 总结 + +Redis分片技术是构建高性能、高可用缓存系统的关键技术。选择合适的分片方案需要考虑: + +1. **业务场景**:数据访问模式、一致性要求、性能需求 +2. **运维复杂度**:集群管理、故障处理、扩容难度 +3. **成本考虑**:硬件资源、开发成本、维护成本 + +**推荐方案:** +- **小规模应用**:客户端分片 + 一致性哈希 +- **中大规模应用**:Redis Cluster官方方案 +- **超大规模应用**:Redis Cluster + 代理层(如Twemproxy、Codis) + +通过合理的分片设计和优化,可以构建出高性能、高可用的Redis集群系统。 \ No newline at end of file diff --git "a/docs/aJava/Redis\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\216\237\347\220\206.md" "b/docs/aJava/Redis\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\216\237\347\220\206.md" new file mode 100644 index 000000000..42f8b8e55 --- /dev/null +++ "b/docs/aJava/Redis\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\216\237\347\220\206.md" @@ -0,0 +1,573 @@ +# Redis底层数据结构原理 + +## 概述 + +Redis是一个高性能的键值存储数据库,其卓越的性能很大程度上得益于其精心设计的底层数据结构。本文深入分析Redis的核心数据结构实现原理,包括SDS、链表、字典、跳跃表、整数集合、压缩列表等。 + +## Redis对象系统 + +### 对象类型与编码 + +Redis使用对象来表示数据库中的键和值,每个对象都由一个redisObject结构表示: + +``` +typedef struct redisObject { + unsigned type:4; // 类型 + unsigned encoding:4; // 编码 + unsigned lru:24; // LRU时间 + int refcount; // 引用计数 + void *ptr; // 指向底层实现数据结构的指针 +} robj; +``` + +**五种对象类型:** +- REDIS_STRING(字符串) +- REDIS_LIST(列表) +- REDIS_HASH(哈希) +- REDIS_SET(集合) +- REDIS_ZSET(有序集合) + +## 简单动态字符串(SDS) + +### SDS结构定义 + +``` +struct sdshdr { + unsigned int len; // 记录buf数组中已使用字节的数量 + unsigned int free; // 记录buf数组中未使用字节的数量 + char buf[]; // 字节数组,用于保存字符串 +}; +``` + +### SDS优势 + +**1. 常数复杂度获取字符串长度** +``` +// C字符串获取长度:O(N) +size_t strlen(const char *s); + +// SDS获取长度:O(1) +size_t sdslen(const sds s) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + return sh->len; +} +``` + +**2. 杜绝缓冲区溢出** +- SDS API会自动检查空间是否足够 +- 不足时自动扩展空间 + +**3. 减少修改字符串时的内存重分配次数** +- **空间预分配**:扩展SDS时,不仅分配必须空间,还分配额外未使用空间 +- **惰性空间释放**:缩短SDS时,不立即释放多出的字节 + +**4. 二进制安全** +- 使用len属性判断字符串结束 +- 可以保存任意格式的二进制数据 + +### SDS空间分配策略 + +``` +sds sdsMakeRoomFor(sds s, size_t addlen) { + struct sdshdr *sh, *newsh; + size_t free = sdsavail(s); + size_t len, newlen; + + if (free >= addlen) return s; + + len = sdslen(s); + sh = (void*) (s-(sizeof(struct sdshdr))); + newlen = (len+addlen); + + // 空间预分配策略 + if (newlen < SDS_MAX_PREALLOC) + newlen *= 2; // 小于1MB时,分配2倍空间 + else + newlen += SDS_MAX_PREALLOC; // 大于1MB时,额外分配1MB + + newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); + newsh->free = newlen - len; + return newsh->buf; +} +``` + +## 链表(LinkedList) + +### 链表节点结构 + +``` +typedef struct listNode { + struct listNode *prev; // 前置节点 + struct listNode *next; // 后置节点 + void *value; // 节点的值 +} listNode; + +typedef struct list { + listNode *head; // 表头节点 + listNode *tail; // 表尾节点 + void *(*dup)(void *ptr); // 节点值复制函数 + void (*free)(void *ptr); // 节点值释放函数 + int (*match)(void *ptr, void *key); // 节点值对比函数 + unsigned long len; // 链表所包含的节点数量 +} list; +``` + +### 链表特性 + +- **双端**:获取前置和后置节点的复杂度都是O(1) +- **无环**:表头节点的prev指针和表尾节点的next指针都指向NULL +- **带表头指针和表尾指针**:获取表头和表尾节点的复杂度为O(1) +- **带链表长度计数器**:获取链表长度的复杂度为O(1) +- **多态**:使用void*指针保存节点值,可以保存各种不同类型的值 + +## 字典(Dictionary) + +### 哈希表结构 + +``` +typedef struct dictht { + dictEntry **table; // 哈希表数组 + unsigned long size; // 哈希表大小 + unsigned long sizemask; // 哈希表大小掩码,用于计算索引值 + unsigned long used; // 该哈希表已有节点的数量 +} dictht; + +typedef struct dictEntry { + void *key; // 键 + union { + void *val; + uint64_t u64; + int64_t s64; + double d; + } v; // 值 + struct dictEntry *next; // 指向下个哈希表节点,形成链表 +} dictEntry; + +typedef struct dict { + dictType *type; // 类型特定函数 + void *privdata; // 私有数据 + dictht ht[2]; // 哈希表 + long rehashidx; // rehash索引,当rehash不在进行时,值为-1 + int iterators; // 目前正在运行的安全迭代器的数量 +} dict; +``` + +### 哈希算法 + +``` +// 使用字典设置的哈希函数,计算键key的哈希值 +hash = dict->type->hashFunction(key); + +// 使用哈希表的sizemask属性和哈希值,计算出索引值 +index = hash & dict->ht[x].sizemask; +``` + +### 解决键冲突 + +Redis使用**链地址法**解决键冲突: +- 每个哈希表节点都有一个next指针 +- 多个哈希表节点可以用next指针构成一个单向链表 +- 新节点总是添加到链表的表头位置(O(1)复杂度) + +### rehash过程 + +**触发条件:** +- 负载因子 = ht[0].used / ht[0].size +- 扩展:负载因子 >= 1(无BGSAVE/BGREWRITEAOF时)或 >= 5 +- 收缩:负载因子 < 0.1 + +**渐进式rehash步骤:** + +``` +int dictRehash(dict *d, int n) { + int empty_visits = n * 10; // 最大空桶访问数 + + if (!dictIsRehashing(d)) return 0; + + while(n-- && d->ht[0].used != 0) { + dictEntry *de, *nextde; + + // 跳过空桶 + while(d->ht[0].table[d->rehashidx] == NULL) { + d->rehashidx++; + if (--empty_visits == 0) return 1; + } + + de = d->ht[0].table[d->rehashidx]; + // 将链表中的所有节点迁移到ht[1] + while(de) { + uint64_t h; + nextde = de->next; + h = dictHashKey(d, de->key) & d->ht[1].sizemask; + de->next = d->ht[1].table[h]; + d->ht[1].table[h] = de; + d->ht[0].used--; + d->ht[1].used++; + de = nextde; + } + d->ht[0].table[d->rehashidx] = NULL; + d->rehashidx++; + } + + // 检查是否完成rehash + if (d->ht[0].used == 0) { + zfree(d->ht[0].table); + d->ht[0] = d->ht[1]; + _dictReset(&d->ht[1]); + d->rehashidx = -1; + return 0; + } + return 1; +} +``` + +## 跳跃表(Skip List) + +### 跳跃表结构 + +``` +typedef struct zskiplistNode { + sds ele; // 成员对象 + double score; // 分值 + struct zskiplistNode *backward; // 后退指针 + struct zskiplistLevel { + struct zskiplistNode *forward; // 前进指针 + unsigned long span; // 跨度 + } level[]; // 层 +} zskiplistNode; + +typedef struct zskiplist { + struct zskiplistNode *header, *tail; // 表头节点和表尾节点 + unsigned long length; // 表中节点的数量 + int level; // 表中层数最大的节点的层数 +} zskiplist; +``` + +### 跳跃表特性 + +**1. 层级结构** +- 每个节点包含多个层 +- 每层包含前进指针和跨度 +- 层数随机生成(1-32层) + +**2. 查找过程** +``` +zskiplistNode *zslSearch(zskiplist *zsl, double score, sds ele) { + zskiplistNode *x; + int i; + + x = zsl->header; + // 从最高层开始查找 + for (i = zsl->level-1; i >= 0; i--) { + while (x->level[i].forward && + (x->level[i].forward->score < score || + (x->level[i].forward->score == score && + sdscmp(x->level[i].forward->ele, ele) < 0))) + { + x = x->level[i].forward; + } + } + + x = x->level[0].forward; + if (x && score == x->score && sdscmp(x->ele, ele) == 0) { + return x; + } + return NULL; +} +``` + +**3. 层数生成算法** +``` +int zslRandomLevel(void) { + int level = 1; + while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) + level += 1; + return (levelencoding); + uint8_t newenc = _intsetValueEncoding(value); + int length = intrev32ifbe(is->length); + int prepend = value < 0 ? 1 : 0; + + // 设置新编码并调整大小 + is->encoding = intrev32ifbe(newenc); + is = intsetResize(is, intrev32ifbe(is->length)+1); + + // 从后往前移动元素 + while(length--) + _intsetSet(is, length+prepend, _intsetGetEncoded(is, length, curenc)); + + // 添加新元素 + if (prepend) + _intsetSet(is, 0, value); + else + _intsetSet(is, intrev32ifbe(is->length), value); + + is->length = intrev32ifbe(intrev32ifbe(is->length)+1); + return is; +} +``` + +## 压缩列表(ZipList) + +### 压缩列表结构 + +``` + ... +``` + +- **zlbytes**:记录整个压缩列表占用的内存字节数 +- **zltail**:记录压缩列表表尾节点距离起始地址的偏移量 +- **zllen**:记录压缩列表包含的节点数量 +- **entryX**:列表节点 +- **zlend**:特殊值0xFF,标记压缩列表的末端 + +### 压缩列表节点结构 + +``` + +``` + +**prevlen编码:** +- 前一节点长度小于254字节:使用1字节保存 +- 前一节点长度大于等于254字节:使用5字节保存 + +**encoding编码:** +- 字节数组编码:00、01、10开头 +- 整数编码:11开头 + +### 连锁更新问题 + +当插入或删除节点时,可能引发连锁更新: + +``` +// 示例:连续多个长度为253字节的节点 +// 插入一个长度大于254字节的节点时 +// 会导致后续节点的prevlen从1字节变为5字节 +// 可能引发连锁反应 +``` + +## 对象编码选择 + +### 字符串对象编码 + +``` +robj *createStringObject(const char *ptr, size_t len) { + if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) + return createEmbeddedStringObject(ptr, len); // embstr编码 + else + return createRawStringObject(ptr, len); // raw编码 +} + +robj *createStringObjectFromLongLong(long long value) { + if (value >= 0 && value < OBJ_SHARED_INTEGERS) + return shared.integers[value]; // 共享整数对象 + else + return createObject(OBJ_STRING, sdsfromlonglong(value)); +} +``` + +### 列表对象编码转换 + +``` +void listTypeConvert(robj *subject, int enc) { + if (subject->encoding == OBJ_ENCODING_ZIPLIST) { + // ziplist转换为linkedlist + unsigned char *zl = subject->ptr; + unsigned char *p = ziplistIndex(zl, 0); + list *l = listCreate(); + + while (p != NULL) { + unsigned char *vstr; + unsigned int vlen; + long long vlong; + + if (ziplistGet(p, &vstr, &vlen, &vlong)) { + robj *obj; + if (vstr) { + obj = createStringObject((char*)vstr, vlen); + } else { + obj = createStringObjectFromLongLong(vlong); + } + listAddNodeTail(l, obj); + } + p = ziplistNext(zl, p); + } + + subject->ptr = l; + subject->encoding = OBJ_ENCODING_LINKEDLIST; + zfree(zl); + } +} +``` + +## 内存优化策略 + +### 1. 共享对象 + +``` +// Redis预创建0-9999的整数对象 +struct sharedObjectsStruct { + robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space, + *colon, *nullbulk, *nullmultibulk, *queued, + *emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr, + *outofrangeerr, *noscripterr, *loadingerr, *slowscripterr, *bgsaveerr, + *masterdownerr, *roslaveerr, *execaborterr, *noautherr, *noreplicaserr, + *busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk, + *unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *rpop, *lpop, + *lpush, *emptyscan, *minstring, *maxstring, + *select[REDIS_SHARED_SELECT_CMDS], + *integers[REDIS_SHARED_INTEGERS], + *mbulkhdr[REDIS_SHARED_BULKHDR_LEN], + *bulkhdr[REDIS_SHARED_BULKHDR_LEN]; +} shared; +``` + +### 2. 引用计数 + +``` +void incrRefCount(robj *o) { + o->refcount++; +} + +void decrRefCount(robj *o) { + if (o->refcount <= 0) { + switch(o->type) { + case OBJ_STRING: freeStringObject(o); break; + case OBJ_LIST: freeListObject(o); break; + case OBJ_SET: freeSetObject(o); break; + case OBJ_ZSET: freeZsetObject(o); break; + case OBJ_HASH: freeHashObject(o); break; + default: redisPanic("Unknown object type"); break; + } + zfree(o); + } else { + o->refcount--; + } +} +``` + +### 3. 对象空转时长 + +``` +// 计算对象的空转时长 +unsigned long long estimateObjectIdleTime(robj *o) { + unsigned long long lruclock = LRU_CLOCK(); + if (lruclock >= o->lru) { + return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION; + } else { + return (lruclock + (LRU_CLOCK_MAX - o->lru)) * + LRU_CLOCK_RESOLUTION; + } +} +``` + +## 性能分析 + +### 时间复杂度对比 + +| 操作 | SDS | C字符串 | 链表 | 跳跃表 | 哈希表 | +|------|-----|---------|------|--------|--------| +| 获取长度 | O(1) | O(N) | O(1) | - | - | +| 查找 | O(N) | O(N) | O(N) | O(logN) | O(1) | +| 插入 | O(N) | O(N) | O(1) | O(logN) | O(1) | +| 删除 | O(N) | O(N) | O(1) | O(logN) | O(1) | +| 范围查询 | - | - | O(N) | O(logN) | - | + +### 空间复杂度分析 + +**SDS空间预分配:** +- 小于1MB:分配2倍空间 +- 大于1MB:额外分配1MB +- 平均空间利用率:约50% + +**跳跃表空间开销:** +- 平均每个节点层数:1/(1-p) ≈ 1.33(p=0.25) +- 额外指针开销:约33% + +## 实际应用场景 + +### 1. 缓存系统 + +``` +# 使用Redis作为缓存 +import redis + +r = redis.Redis(host='localhost', port=6379, db=0) + +# 字符串缓存 +r.setex('user:1001', 3600, json.dumps(user_data)) +user_data = json.loads(r.get('user:1001')) + +# 哈希缓存 +r.hset('user:1001', 'name', 'John') +r.hset('user:1001', 'age', 30) +user_info = r.hgetall('user:1001') +``` + +### 2. 排行榜系统 + +``` +# 使用有序集合实现排行榜 +# 添加分数 +r.zadd('leaderboard', {'player1': 1000, 'player2': 1500}) + +# 获取排行榜 +top_players = r.zrevrange('leaderboard', 0, 9, withscores=True) + +# 获取玩家排名 +rank = r.zrevrank('leaderboard', 'player1') +``` + +### 3. 消息队列 + +``` +# 使用列表实现消息队列 +# 生产者 +r.lpush('task_queue', json.dumps(task_data)) + +# 消费者 +while True: + task = r.brpop('task_queue', timeout=1) + if task: + process_task(json.loads(task[1])) +``` + +## 总结 + +Redis的高性能源于其精心设计的底层数据结构: + +1. **SDS**:提供了比C字符串更高效的字符串操作 +2. **链表**:支持快速的插入和删除操作 +3. **字典**:提供O(1)的查找性能,通过渐进式rehash保证性能稳定 +4. **跳跃表**:在有序数据上提供O(logN)的查找性能 +5. **整数集合**:为小整数集合提供紧凑的存储 +6. **压缩列表**:为小数据量提供内存高效的存储 + +这些数据结构的巧妙组合和优化,使Redis能够在保持高性能的同时,提供丰富的数据类型和操作,成为现代应用架构中不可或缺的组件。 + +理解这些底层原理,有助于我们更好地使用Redis,选择合适的数据类型,优化应用性能,并在遇到问题时能够深入分析和解决。 \ No newline at end of file diff --git "a/docs/aJava/SQLServer\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/SQLServer\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..1bf968ba8 --- /dev/null +++ "b/docs/aJava/SQLServer\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,1321 @@ +# SQL Server分片技术实现 + +## 概述 + +SQL Server分片技术主要通过分区表、Always On可用性组、弹性数据库工具等方式实现水平扩展。SQL Server提供了多种分片策略,包括表分区、数据库分片和弹性数据库池等解决方案。 + +## SQL Server分片架构 + +### 1. 分区表实现 + +```sql +-- 创建分区函数 +CREATE PARTITION FUNCTION OrderDatePartitionFunction (datetime2) +AS RANGE RIGHT FOR VALUES +('2023-01-01', '2023-04-01', '2023-07-01', '2023-10-01', '2024-01-01'); + +-- 创建分区方案 +CREATE PARTITION SCHEME OrderDatePartitionScheme +AS PARTITION OrderDatePartitionFunction +TO (FileGroup1, FileGroup2, FileGroup3, FileGroup4, FileGroup5, FileGroup6); + +-- 创建分区表 +CREATE TABLE Orders ( + OrderId BIGINT IDENTITY(1,1) PRIMARY KEY, + CustomerId BIGINT NOT NULL, + OrderDate DATETIME2 NOT NULL, + Amount DECIMAL(10,2), + Status NVARCHAR(20), + Region NVARCHAR(50) +) ON OrderDatePartitionScheme(OrderDate); + +-- 创建分区索引 +CREATE INDEX IX_Orders_CustomerId +ON Orders(CustomerId) +ON OrderDatePartitionScheme(OrderDate); + +CREATE INDEX IX_Orders_Region +ON Orders(Region, OrderDate) +ON OrderDatePartitionScheme(OrderDate); +``` + +### 2. 弹性数据库配置 + +```sql +-- 创建分片映射管理器数据库 +CREATE DATABASE ShardMapManager; + +-- 创建分片数据库 +CREATE DATABASE Shard1; +CREATE DATABASE Shard2; +CREATE DATABASE Shard3; + +-- 在每个分片中创建表结构 +USE Shard1; +CREATE TABLE Orders ( + OrderId BIGINT IDENTITY(1,1) PRIMARY KEY, + CustomerId BIGINT NOT NULL, + OrderDate DATETIME2 NOT NULL, + Amount DECIMAL(10,2), + Status NVARCHAR(20), + Region NVARCHAR(50), + ShardKey BIGINT NOT NULL +); + +CREATE INDEX IX_Orders_ShardKey ON Orders(ShardKey); +CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId); +``` + +### 3. Docker Compose部署 + +```yaml +# docker-compose.yml +version: '3.8' +services: + # SQL Server主实例 + sqlserver-master: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + SA_PASSWORD: "YourStrong@Passw0rd" + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + ports: + - "1433:1433" + volumes: + - sqlserver_master_data:/var/opt/mssql + - ./scripts:/scripts + hostname: sqlserver-master + networks: + - sqlserver-network + + # 分片1 + sqlserver-shard1: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + SA_PASSWORD: "YourStrong@Passw0rd" + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + ports: + - "1434:1433" + volumes: + - sqlserver_shard1_data:/var/opt/mssql + hostname: sqlserver-shard1 + networks: + - sqlserver-network + + # 分片2 + sqlserver-shard2: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + SA_PASSWORD: "YourStrong@Passw0rd" + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + ports: + - "1435:1433" + volumes: + - sqlserver_shard2_data:/var/opt/mssql + hostname: sqlserver-shard2 + networks: + - sqlserver-network + + # 分片3 + sqlserver-shard3: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + SA_PASSWORD: "YourStrong@Passw0rd" + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + ports: + - "1436:1433" + volumes: + - sqlserver_shard3_data:/var/opt/mssql + hostname: sqlserver-shard3 + networks: + - sqlserver-network + +volumes: + sqlserver_master_data: + sqlserver_shard1_data: + sqlserver_shard2_data: + sqlserver_shard3_data: + +networks: + sqlserver-network: + driver: bridge +``` + +## Java应用集成 + +### 1. Maven依赖 + +```xml + + + + com.microsoft.sqlserver + mssql-jdbc + 12.4.2.jre11 + + + + + com.microsoft.azure + elastic-db-tools + 1.0.0 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.zaxxer + HikariCP + + + + + org.springframework.boot + spring-boot-starter + + +``` + +### 2. Spring Boot配置 + +```yaml +# application.yml +spring: + datasource: + # 主数据源配置 + master: + url: jdbc:sqlserver://localhost:1433;databaseName=ShardMapManager;encrypt=false + username: sa + password: YourStrong@Passw0rd + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + + # 分片数据源配置 + shards: + shard1: + url: jdbc:sqlserver://localhost:1434;databaseName=Shard1;encrypt=false + username: sa + password: YourStrong@Passw0rd + shard2: + url: jdbc:sqlserver://localhost:1435;databaseName=Shard2;encrypt=false + username: sa + password: YourStrong@Passw0rd + shard3: + url: jdbc:sqlserver://localhost:1436;databaseName=Shard3;encrypt=false + username: sa + password: YourStrong@Passw0rd + + # 连接池配置 + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + idle-timeout: 300000 + connection-timeout: 30000 + max-lifetime: 1800000 + + jpa: + database-platform: org.hibernate.dialect.SQLServer2012Dialect + hibernate: + ddl-auto: validate + show-sql: true + properties: + hibernate: + format_sql: true + use_sql_comments: true + +# SQL Server分片配置 +sqlserver: + sharding: + shard-map-manager-server: localhost:1433 + shard-map-manager-database: ShardMapManager + shard-map-name: OrderShardMap + connection-string-template: "Server={0};Database={1};User Id=sa;Password=YourStrong@Passw0rd;Encrypt=false;" +``` + +### 3. 分片数据源配置 + +```java +@Configuration +@EnableJpaRepositories(basePackages = "com.example.repository") +public class SqlServerShardingConfig { + + @Value("${sqlserver.sharding.shard-map-manager-server}") + private String shardMapManagerServer; + + @Value("${sqlserver.sharding.shard-map-manager-database}") + private String shardMapManagerDatabase; + + @Value("${sqlserver.sharding.shard-map-name}") + private String shardMapName; + + @Bean + @Primary + public DataSource masterDataSource() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:sqlserver://localhost:1433;databaseName=ShardMapManager;encrypt=false"); + config.setUsername("sa"); + config.setPassword("YourStrong@Passw0rd"); + config.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + config.setMaximumPoolSize(20); + config.setMinimumIdle(5); + config.setConnectionTimeout(30000); + config.setIdleTimeout(300000); + config.setMaxLifetime(1800000); + + return new HikariDataSource(config); + } + + @Bean + public Map shardDataSources() { + Map shards = new HashMap<>(); + + // 配置分片1 + HikariConfig shard1Config = new HikariConfig(); + shard1Config.setJdbcUrl("jdbc:sqlserver://localhost:1434;databaseName=Shard1;encrypt=false"); + shard1Config.setUsername("sa"); + shard1Config.setPassword("YourStrong@Passw0rd"); + shard1Config.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + shard1Config.setMaximumPoolSize(20); + shards.put("shard1", new HikariDataSource(shard1Config)); + + // 配置分片2 + HikariConfig shard2Config = new HikariConfig(); + shard2Config.setJdbcUrl("jdbc:sqlserver://localhost:1435;databaseName=Shard2;encrypt=false"); + shard2Config.setUsername("sa"); + shard2Config.setPassword("YourStrong@Passw0rd"); + shard2Config.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + shard2Config.setMaximumPoolSize(20); + shards.put("shard2", new HikariDataSource(shard2Config)); + + // 配置分片3 + HikariConfig shard3Config = new HikariConfig(); + shard3Config.setJdbcUrl("jdbc:sqlserver://localhost:1436;databaseName=Shard3;encrypt=false"); + shard3Config.setUsername("sa"); + shard3Config.setPassword("YourStrong@Passw0rd"); + shard3Config.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + shard3Config.setMaximumPoolSize(20); + shards.put("shard3", new HikariDataSource(shard3Config)); + + return shards; + } + + @Bean + public JdbcTemplate masterJdbcTemplate(@Qualifier("masterDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + public EntityManagerFactory entityManagerFactory(@Qualifier("masterDataSource") DataSource dataSource) { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setDataSource(dataSource); + factory.setPackagesToScan("com.example.entity"); + factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + + Properties jpaProperties = new Properties(); + jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.SQLServer2012Dialect"); + jpaProperties.setProperty("hibernate.hbm2ddl.auto", "validate"); + jpaProperties.setProperty("hibernate.show_sql", "true"); + factory.setJpaProperties(jpaProperties); + + factory.afterPropertiesSet(); + return factory.getObject(); + } +} +``` + +### 4. 分片实体类 + +```java +@Entity +@Table(name = "Orders") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "OrderId") + private Long orderId; + + @Column(name = "CustomerId") + private Long customerId; + + @Column(name = "OrderDate") + private LocalDateTime orderDate; + + @Column(name = "Amount") + private BigDecimal amount; + + @Column(name = "Status") + private String status; + + @Column(name = "Region") + private String region; + + @Column(name = "ShardKey") + private Long shardKey; + + // 构造函数 + public Order() {} + + public Order(Long customerId, LocalDateTime orderDate, BigDecimal amount, String status, String region) { + this.customerId = customerId; + this.orderDate = orderDate; + this.amount = amount; + this.status = status; + this.region = region; + this.shardKey = customerId; // 使用客户ID作为分片键 + } + + // Getter和Setter方法 + public Long getOrderId() { return orderId; } + public void setOrderId(Long orderId) { this.orderId = orderId; } + + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { + this.customerId = customerId; + this.shardKey = customerId; // 自动设置分片键 + } + + public LocalDateTime getOrderDate() { return orderDate; } + public void setOrderDate(LocalDateTime orderDate) { this.orderDate = orderDate; } + + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getRegion() { return region; } + public void setRegion(String region) { this.region = region; } + + public Long getShardKey() { return shardKey; } + public void setShardKey(Long shardKey) { this.shardKey = shardKey; } +} +``` + +### 5. 分片路由服务 + +```java +@Service +public class SqlServerShardingService { + + @Autowired + private Map shardDataSources; + + @Autowired + @Qualifier("masterJdbcTemplate") + private JdbcTemplate masterJdbcTemplate; + + private final Map shardJdbcTemplates = new HashMap<>(); + + @PostConstruct + public void initShardTemplates() { + shardDataSources.forEach((shardName, dataSource) -> { + shardJdbcTemplates.put(shardName, new JdbcTemplate(dataSource)); + }); + } + + /** + * 根据分片键确定分片 + */ + public String determineShardKey(Long shardKey) { + if (shardKey == null) { + throw new IllegalArgumentException("Shard key cannot be null"); + } + + // 简单的哈希分片策略 + int shardIndex = (int) (shardKey % shardDataSources.size()) + 1; + return "shard" + shardIndex; + } + + /** + * 获取分片的JdbcTemplate + */ + public JdbcTemplate getShardJdbcTemplate(Long shardKey) { + String shardName = determineShardKey(shardKey); + return shardJdbcTemplates.get(shardName); + } + + /** + * 创建订单 + */ + @Transactional + public Order createOrder(Order order) { + JdbcTemplate shardTemplate = getShardJdbcTemplate(order.getShardKey()); + + String sql = """ + INSERT INTO Orders (CustomerId, OrderDate, Amount, Status, Region, ShardKey) + OUTPUT INSERTED.OrderId + VALUES (?, ?, ?, ?, ?, ?) + """; + + Long orderId = shardTemplate.queryForObject(sql, Long.class, + order.getCustomerId(), + order.getOrderDate(), + order.getAmount(), + order.getStatus(), + order.getRegion(), + order.getShardKey()); + + order.setOrderId(orderId); + return order; + } + + /** + * 根据客户ID查询订单 + */ + public List findOrdersByCustomerId(Long customerId) { + JdbcTemplate shardTemplate = getShardJdbcTemplate(customerId); + + String sql = """ + SELECT OrderId, CustomerId, OrderDate, Amount, Status, Region, ShardKey + FROM Orders + WHERE CustomerId = ? + ORDER BY OrderDate DESC + """; + + return shardTemplate.query(sql, this::mapRowToOrder, customerId); + } + + /** + * 跨分片查询 + */ + public List findOrdersByDateRange(LocalDateTime startDate, LocalDateTime endDate) { + List allOrders = new ArrayList<>(); + + String sql = """ + SELECT OrderId, CustomerId, OrderDate, Amount, Status, Region, ShardKey + FROM Orders + WHERE OrderDate BETWEEN ? AND ? + ORDER BY OrderDate DESC + """; + + // 并行查询所有分片 + List>> futures = shardJdbcTemplates.values().stream() + .map(template -> CompletableFuture.supplyAsync(() -> + template.query(sql, this::mapRowToOrder, startDate, endDate))) + .collect(Collectors.toList()); + + // 合并结果 + futures.forEach(future -> { + try { + allOrders.addAll(future.get()); + } catch (Exception e) { + throw new RuntimeException("Failed to query shard", e); + } + }); + + // 按日期排序 + return allOrders.stream() + .sorted((o1, o2) -> o2.getOrderDate().compareTo(o1.getOrderDate())) + .collect(Collectors.toList()); + } + + /** + * 跨分片聚合查询 + */ + public Map getOrderStatistics() { + String sql = """ + SELECT + COUNT(*) as total_orders, + SUM(Amount) as total_amount, + AVG(Amount) as avg_amount, + Region, + COUNT(*) as region_count + FROM Orders + GROUP BY Region + """; + + List> allRegionStats = new ArrayList<>(); + + // 查询所有分片 + shardJdbcTemplates.values().parallelStream().forEach(template -> { + List> shardStats = template.queryForList(sql); + synchronized (allRegionStats) { + allRegionStats.addAll(shardStats); + } + }); + + // 聚合结果 + Map> regionAggregates = new HashMap<>(); + + for (Map stat : allRegionStats) { + String region = (String) stat.get("Region"); + regionAggregates.merge(region, stat, (existing, current) -> { + Map merged = new HashMap<>(existing); + merged.put("total_orders", + ((Number) existing.get("total_orders")).longValue() + + ((Number) current.get("total_orders")).longValue()); + merged.put("total_amount", + ((BigDecimal) existing.get("total_amount")).add( + (BigDecimal) current.get("total_amount"))); + return merged; + }); + } + + // 计算总体统计 + long totalOrders = regionAggregates.values().stream() + .mapToLong(stat -> ((Number) stat.get("total_orders")).longValue()) + .sum(); + + BigDecimal totalAmount = regionAggregates.values().stream() + .map(stat -> (BigDecimal) stat.get("total_amount")) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Map result = new HashMap<>(); + result.put("total_orders", totalOrders); + result.put("total_amount", totalAmount); + result.put("avg_amount", totalOrders > 0 ? totalAmount.divide(BigDecimal.valueOf(totalOrders), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + result.put("region_statistics", regionAggregates.values()); + + return result; + } + + /** + * 批量插入优化 + */ + @Transactional + public void batchInsertOrders(List orders) { + // 按分片分组 + Map> ordersByShards = orders.stream() + .collect(Collectors.groupingBy(order -> determineShardKey(order.getShardKey()))); + + String sql = """ + INSERT INTO Orders (CustomerId, OrderDate, Amount, Status, Region, ShardKey) + VALUES (?, ?, ?, ?, ?, ?) + """; + + // 并行批量插入 + ordersByShards.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + List shardOrders = entry.getValue(); + JdbcTemplate template = shardJdbcTemplates.get(shardName); + + List batchArgs = shardOrders.stream() + .map(order -> new Object[]{ + order.getCustomerId(), + order.getOrderDate(), + order.getAmount(), + order.getStatus(), + order.getRegion(), + order.getShardKey() + }) + .collect(Collectors.toList()); + + template.batchUpdate(sql, batchArgs); + }); + } + + /** + * 行映射器 + */ + private Order mapRowToOrder(ResultSet rs, int rowNum) throws SQLException { + Order order = new Order(); + order.setOrderId(rs.getLong("OrderId")); + order.setCustomerId(rs.getLong("CustomerId")); + order.setOrderDate(rs.getTimestamp("OrderDate").toLocalDateTime()); + order.setAmount(rs.getBigDecimal("Amount")); + order.setStatus(rs.getString("Status")); + order.setRegion(rs.getString("Region")); + order.setShardKey(rs.getLong("ShardKey")); + return order; + } +} +``` + +### 6. 分片管理服务 + +```java +@Service +public class SqlServerShardManagementService { + + @Autowired + @Qualifier("masterJdbcTemplate") + private JdbcTemplate masterJdbcTemplate; + + @Autowired + private Map shardDataSources; + + /** + * 初始化分片映射 + */ + public void initializeShardMap() { + // 创建分片映射表 + String createShardMapSql = """ + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ShardMap') + CREATE TABLE ShardMap ( + ShardId INT PRIMARY KEY, + ShardName NVARCHAR(50) NOT NULL, + ConnectionString NVARCHAR(500) NOT NULL, + MinShardKey BIGINT NOT NULL, + MaxShardKey BIGINT NOT NULL, + Status NVARCHAR(20) DEFAULT 'Active', + CreatedDate DATETIME2 DEFAULT GETDATE() + ) + """; + + masterJdbcTemplate.execute(createShardMapSql); + + // 插入分片映射信息 + String insertShardSql = """ + MERGE ShardMap AS target + USING (VALUES + (1, 'shard1', 'Server=localhost,1434;Database=Shard1;User Id=sa;Password=YourStrong@Passw0rd;', 0, 333333333), + (2, 'shard2', 'Server=localhost,1435;Database=Shard2;User Id=sa;Password=YourStrong@Passw0rd;', 333333334, 666666666), + (3, 'shard3', 'Server=localhost,1436;Database=Shard3;User Id=sa;Password=YourStrong@Passw0rd;', 666666667, 999999999) + ) AS source (ShardId, ShardName, ConnectionString, MinShardKey, MaxShardKey) + ON target.ShardId = source.ShardId + WHEN NOT MATCHED THEN + INSERT (ShardId, ShardName, ConnectionString, MinShardKey, MaxShardKey) + VALUES (source.ShardId, source.ShardName, source.ConnectionString, source.MinShardKey, source.MaxShardKey); + """; + + masterJdbcTemplate.execute(insertShardSql); + } + + /** + * 获取分片信息 + */ + public List> getShardInfo() { + String sql = """ + SELECT + ShardId, + ShardName, + ConnectionString, + MinShardKey, + MaxShardKey, + Status, + CreatedDate + FROM ShardMap + ORDER BY ShardId + """; + + return masterJdbcTemplate.queryForList(sql); + } + + /** + * 获取分片统计信息 + */ + public Map getShardStatistics() { + List> shardStats = new ArrayList<>(); + + shardDataSources.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + DataSource dataSource = entry.getValue(); + JdbcTemplate template = new JdbcTemplate(dataSource); + + try { + String sql = """ + SELECT + '" + shardName + "' as shard_name, + COUNT(*) as record_count, + SUM(CAST(Amount AS BIGINT)) as total_amount, + MIN(OrderDate) as min_date, + MAX(OrderDate) as max_date + FROM Orders + """; + + Map stat = template.queryForMap(sql); + synchronized (shardStats) { + shardStats.add(stat); + } + } catch (Exception e) { + Map errorStat = new HashMap<>(); + errorStat.put("shard_name", shardName); + errorStat.put("error", e.getMessage()); + synchronized (shardStats) { + shardStats.add(errorStat); + } + } + }); + + Map result = new HashMap<>(); + result.put("shard_statistics", shardStats); + result.put("timestamp", LocalDateTime.now()); + + return result; + } + + /** + * 检查分片健康状态 + */ + public List> checkShardHealth() { + List> healthStatus = new ArrayList<>(); + + shardDataSources.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + DataSource dataSource = entry.getValue(); + + Map status = new HashMap<>(); + status.put("shard_name", shardName); + + try { + JdbcTemplate template = new JdbcTemplate(dataSource); + template.queryForObject("SELECT 1", Integer.class); + status.put("status", "HEALTHY"); + status.put("response_time", System.currentTimeMillis()); + } catch (Exception e) { + status.put("status", "UNHEALTHY"); + status.put("error", e.getMessage()); + } + + synchronized (healthStatus) { + healthStatus.add(status); + } + }); + + return healthStatus; + } + + /** + * 重新平衡分片数据 + */ + public void rebalanceShards() { + // 获取所有分片的数据分布 + Map shardCounts = new HashMap<>(); + + shardDataSources.forEach((shardName, dataSource) -> { + JdbcTemplate template = new JdbcTemplate(dataSource); + Long count = template.queryForObject("SELECT COUNT(*) FROM Orders", Long.class); + shardCounts.put(shardName, count); + }); + + // 计算平均值 + long totalRecords = shardCounts.values().stream().mapToLong(Long::longValue).sum(); + long avgRecords = totalRecords / shardCounts.size(); + + // 识别需要重新平衡的分片 + shardCounts.forEach((shardName, count) -> { + double deviation = Math.abs(count - avgRecords) / (double) avgRecords; + if (deviation > 0.2) { // 偏差超过20% + System.out.println(String.format("Shard %s needs rebalancing. Count: %d, Average: %d, Deviation: %.2f%%", + shardName, count, avgRecords, deviation * 100)); + } + }); + } +} +``` + +## 性能优化策略 + +### 1. 分区表优化 + +```sql +-- 分区表维护 +-- 添加新分区 +ALTER PARTITION SCHEME OrderDatePartitionScheme +NEXT USED FileGroup7; + +ALTER PARTITION FUNCTION OrderDatePartitionFunction() +SPLIT RANGE ('2024-04-01'); + +-- 删除旧分区 +ALTER PARTITION FUNCTION OrderDatePartitionFunction() +MERGE RANGE ('2023-01-01'); + +-- 分区切换(快速数据移动) +CREATE TABLE Orders_Archive ( + OrderId BIGINT, + CustomerId BIGINT, + OrderDate DATETIME2, + Amount DECIMAL(10,2), + Status NVARCHAR(20), + Region NVARCHAR(50) +) ON FileGroup1; + +-- 切换分区到归档表 +ALTER TABLE Orders +SWITCH PARTITION 1 TO Orders_Archive; +``` + +### 2. 索引优化 + +```sql +-- 创建分区对齐索引 +CREATE INDEX IX_Orders_CustomerId_Partitioned +ON Orders(CustomerId, OrderDate) +ON OrderDatePartitionScheme(OrderDate); + +-- 创建覆盖索引 +CREATE INDEX IX_Orders_Status_Covering +ON Orders(Status, Region) +INCLUDE (OrderId, CustomerId, Amount) +ON OrderDatePartitionScheme(OrderDate); + +-- 创建列存储索引(分析查询优化) +CREATE COLUMNSTORE INDEX CCI_Orders +ON Orders (OrderId, CustomerId, OrderDate, Amount, Status, Region) +ON OrderDatePartitionScheme(OrderDate); +``` + +### 3. 查询优化 + +```java +@Service +public class SqlServerQueryOptimizationService { + + @Autowired + private SqlServerShardingService shardingService; + + /** + * 分区消除查询 + */ + public List findOrdersByDateRangeOptimized(LocalDateTime startDate, LocalDateTime endDate) { + // 使用分区消除的查询 + String sql = """ + SELECT OrderId, CustomerId, OrderDate, Amount, Status, Region, ShardKey + FROM Orders + WHERE OrderDate >= ? AND OrderDate < ? + ORDER BY OrderDate DESC + """; + + List results = new ArrayList<>(); + + // 并行查询相关分片 + shardingService.getShardJdbcTemplates().values().parallelStream().forEach(template -> { + List shardResults = template.query(sql, shardingService::mapRowToOrder, startDate, endDate); + synchronized (results) { + results.addAll(shardResults); + } + }); + + return results.stream() + .sorted((o1, o2) -> o2.getOrderDate().compareTo(o1.getOrderDate())) + .collect(Collectors.toList()); + } + + /** + * 使用提示的查询优化 + */ + public List findOrdersWithHints(Long customerId) { + JdbcTemplate template = shardingService.getShardJdbcTemplate(customerId); + + String sql = """ + SELECT /*+ INDEX(Orders, IX_Orders_CustomerId) */ + OrderId, CustomerId, OrderDate, Amount, Status, Region, ShardKey + FROM Orders WITH (NOLOCK) + WHERE CustomerId = ? + ORDER BY OrderDate DESC + """; + + return template.query(sql, shardingService::mapRowToOrder, customerId); + } + + /** + * 批量查询优化 + */ + public Map> findOrdersByCustomerIds(List customerIds) { + // 按分片分组客户ID + Map> customerIdsByShards = customerIds.stream() + .collect(Collectors.groupingBy(shardingService::determineShardKey)); + + Map> results = new ConcurrentHashMap<>(); + + // 并行查询 + customerIdsByShards.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + List shardCustomerIds = entry.getValue(); + JdbcTemplate template = shardingService.getShardJdbcTemplates().get(shardName); + + String sql = """ + SELECT OrderId, CustomerId, OrderDate, Amount, Status, Region, ShardKey + FROM Orders + WHERE CustomerId IN (" + + shardCustomerIds.stream().map(id -> "?").collect(Collectors.joining(",")) + + ") ORDER BY CustomerId, OrderDate DESC + """; + + List orders = template.query(sql, shardingService::mapRowToOrder, shardCustomerIds.toArray()); + + // 按客户ID分组 + Map> customerOrders = orders.stream() + .collect(Collectors.groupingBy(Order::getCustomerId)); + + results.putAll(customerOrders); + }); + + return results; + } +} +``` + +## 监控和运维 + +### 1. 性能监控 + +```java +@Component +public class SqlServerShardMonitor { + + @Autowired + private Map shardDataSources; + + /** + * 监控分片性能指标 + */ + public Map getPerformanceMetrics() { + List> shardMetrics = new ArrayList<>(); + + shardDataSources.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + DataSource dataSource = entry.getValue(); + JdbcTemplate template = new JdbcTemplate(dataSource); + + try { + // 查询性能计数器 + String sql = """ + SELECT + '" + shardName + "' as shard_name, + (SELECT cntr_value FROM sys.dm_os_performance_counters + WHERE counter_name = 'Batch Requests/sec') as batch_requests_per_sec, + (SELECT cntr_value FROM sys.dm_os_performance_counters + WHERE counter_name = 'SQL Compilations/sec') as compilations_per_sec, + (SELECT cntr_value FROM sys.dm_os_performance_counters + WHERE counter_name = 'Page life expectancy') as page_life_expectancy, + (SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1) as active_sessions, + (SELECT COUNT(*) FROM sys.dm_exec_requests) as active_requests + """; + + Map metrics = template.queryForMap(sql); + synchronized (shardMetrics) { + shardMetrics.add(metrics); + } + } catch (Exception e) { + Map errorMetrics = new HashMap<>(); + errorMetrics.put("shard_name", shardName); + errorMetrics.put("error", e.getMessage()); + synchronized (shardMetrics) { + shardMetrics.add(errorMetrics); + } + } + }); + + Map result = new HashMap<>(); + result.put("shard_metrics", shardMetrics); + result.put("timestamp", LocalDateTime.now()); + + return result; + } + + /** + * 监控等待统计 + */ + public List> getWaitStatistics() { + List> allWaitStats = new ArrayList<>(); + + shardDataSources.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + DataSource dataSource = entry.getValue(); + JdbcTemplate template = new JdbcTemplate(dataSource); + + try { + String sql = """ + SELECT TOP 10 + '" + shardName + "' as shard_name, + wait_type, + waiting_tasks_count, + wait_time_ms, + max_wait_time_ms, + signal_wait_time_ms + FROM sys.dm_os_wait_stats + WHERE wait_time_ms > 0 + ORDER BY wait_time_ms DESC + """; + + List> waitStats = template.queryForList(sql); + synchronized (allWaitStats) { + allWaitStats.addAll(waitStats); + } + } catch (Exception e) { + // 记录错误但继续处理其他分片 + System.err.println("Error getting wait stats for " + shardName + ": " + e.getMessage()); + } + }); + + return allWaitStats; + } + + /** + * 监控索引使用情况 + */ + public List> getIndexUsageStats() { + List> allIndexStats = new ArrayList<>(); + + shardDataSources.entrySet().parallelStream().forEach(entry -> { + String shardName = entry.getKey(); + DataSource dataSource = entry.getValue(); + JdbcTemplate template = new JdbcTemplate(dataSource); + + try { + String sql = """ + SELECT + '" + shardName + "' as shard_name, + OBJECT_NAME(ius.object_id) as table_name, + i.name as index_name, + ius.user_seeks, + ius.user_scans, + ius.user_lookups, + ius.user_updates, + ius.last_user_seek, + ius.last_user_scan, + ius.last_user_lookup + FROM sys.dm_db_index_usage_stats ius + INNER JOIN sys.indexes i ON ius.object_id = i.object_id AND ius.index_id = i.index_id + WHERE OBJECT_NAME(ius.object_id) = 'Orders' + ORDER BY (ius.user_seeks + ius.user_scans + ius.user_lookups) DESC + """; + + List> indexStats = template.queryForList(sql); + synchronized (allIndexStats) { + allIndexStats.addAll(indexStats); + } + } catch (Exception e) { + System.err.println("Error getting index stats for " + shardName + ": " + e.getMessage()); + } + }); + + return allIndexStats; + } +} +``` + +### 2. 自动化运维脚本 + +```powershell +# sqlserver_shard_maintenance.ps1 + +# SQL Server连接参数 +$ServerInstances = @( + "localhost,1433", + "localhost,1434", + "localhost,1435", + "localhost,1436" +) +$Username = "sa" +$Password = "YourStrong@Passw0rd" +$LogPath = "C:\Logs\SQLServer" + +# 创建日志目录 +if (!(Test-Path $LogPath)) { + New-Item -ItemType Directory -Path $LogPath -Force +} + +# 记录日志函数 +function Write-Log { + param([string]$Message) + $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $LogMessage = "$Timestamp - $Message" + Write-Host $LogMessage + Add-Content -Path "$LogPath\shard_maintenance_$(Get-Date -Format 'yyyyMMdd').log" -Value $LogMessage +} + +# 备份分片数据库 +function Backup-ShardDatabases { + Write-Log "开始备份分片数据库..." + + $BackupPath = "C:\Backup\SQLServer\$(Get-Date -Format 'yyyyMMdd')" + if (!(Test-Path $BackupPath)) { + New-Item -ItemType Directory -Path $BackupPath -Force + } + + $Databases = @("ShardMapManager", "Shard1", "Shard2", "Shard3") + + for ($i = 0; $i -lt $ServerInstances.Length; $i++) { + $Server = $ServerInstances[$i] + $Database = $Databases[$i] + + try { + $BackupFile = "$BackupPath\${Database}_$(Get-Date -Format 'yyyyMMdd_HHmmss').bak" + + $Query = @" +BACKUP DATABASE [$Database] +TO DISK = '$BackupFile' +WITH FORMAT, INIT, COMPRESSION; +"@ + + Invoke-Sqlcmd -ServerInstance $Server -Username $Username -Password $Password -Query $Query -QueryTimeout 3600 + Write-Log "数据库 $Database 备份完成: $BackupFile" + } + catch { + Write-Log "数据库 $Database 备份失败: $($_.Exception.Message)" + } + } + + Write-Log "分片数据库备份完成" +} + +# 检查分片状态 +function Check-ShardStatus { + Write-Log "检查分片状态..." + + foreach ($Server in $ServerInstances) { + try { + $Query = @" +SELECT + @@SERVERNAME as server_name, + DB_NAME() as database_name, + (SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1) as active_sessions, + (SELECT COUNT(*) FROM sys.dm_exec_requests) as active_requests, + (SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Page life expectancy') as page_life_expectancy +"@ + + $Result = Invoke-Sqlcmd -ServerInstance $Server -Username $Username -Password $Password -Query $Query + Write-Log "服务器 $Server 状态正常 - 活动会话: $($Result.active_sessions), 活动请求: $($Result.active_requests)" + } + catch { + Write-Log "服务器 $Server 状态检查失败: $($_.Exception.Message)" + } + } + + Write-Log "分片状态检查完成" +} + +# 更新统计信息 +function Update-Statistics { + Write-Log "更新统计信息..." + + $Databases = @("ShardMapManager", "Shard1", "Shard2", "Shard3") + + for ($i = 0; $i -lt $ServerInstances.Length; $i++) { + $Server = $ServerInstances[$i] + $Database = $Databases[$i] + + try { + $Query = @" +USE [$Database]; +EXEC sp_updatestats; +UPDATE STATISTICS Orders WITH FULLSCAN; +"@ + + Invoke-Sqlcmd -ServerInstance $Server -Username $Username -Password $Password -Query $Query -QueryTimeout 1800 + Write-Log "数据库 $Database 统计信息更新完成" + } + catch { + Write-Log "数据库 $Database 统计信息更新失败: $($_.Exception.Message)" + } + } + + Write-Log "统计信息更新完成" +} + +# 清理旧日志 +function Cleanup-Logs { + Write-Log "清理旧日志文件..." + + # 清理30天前的日志文件 + $CutoffDate = (Get-Date).AddDays(-30) + Get-ChildItem -Path $LogPath -Filter "*.log" | Where-Object { $_.LastWriteTime -lt $CutoffDate } | Remove-Item -Force + + # 清理SQL Server错误日志 + foreach ($Server in $ServerInstances) { + try { + $Query = "EXEC sp_cycle_errorlog;" + Invoke-Sqlcmd -ServerInstance $Server -Username $Username -Password $Password -Query $Query + Write-Log "服务器 $Server 错误日志已循环" + } + catch { + Write-Log "服务器 $Server 错误日志循环失败: $($_.Exception.Message)" + } + } + + Write-Log "日志清理完成" +} + +# 监控分片平衡 +function Monitor-ShardBalance { + Write-Log "监控分片平衡状态..." + + $ShardCounts = @() + $ShardDatabases = @("Shard1", "Shard2", "Shard3") + + for ($i = 1; $i -lt $ServerInstances.Length; $i++) { + $Server = $ServerInstances[$i] + $Database = $ShardDatabases[$i-1] + + try { + $Query = "USE [$Database]; SELECT COUNT(*) as record_count FROM Orders;" + $Result = Invoke-Sqlcmd -ServerInstance $Server -Username $Username -Password $Password -Query $Query + $ShardCounts += $Result.record_count + Write-Log "分片 $Database 记录数: $($Result.record_count)" + } + catch { + Write-Log "分片 $Database 记录数查询失败: $($_.Exception.Message)" + $ShardCounts += 0 + } + } + + # 计算平衡度 + if ($ShardCounts.Count -gt 0) { + $TotalRecords = ($ShardCounts | Measure-Object -Sum).Sum + $AvgRecords = $TotalRecords / $ShardCounts.Count + + for ($i = 0; $i -lt $ShardCounts.Count; $i++) { + $Deviation = [Math]::Abs($ShardCounts[$i] - $AvgRecords) / $AvgRecords + if ($Deviation -gt 0.2) { + Write-Log "警告: 分片 Shard$($i+1) 数据不平衡,偏差: $([Math]::Round($Deviation * 100, 2))%" + } + } + } + + Write-Log "分片平衡监控完成" +} + +# 主函数 +switch ($args[0]) { + "backup" { Backup-ShardDatabases } + "status" { Check-ShardStatus } + "stats" { Update-Statistics } + "cleanup" { Cleanup-Logs } + "balance" { Monitor-ShardBalance } + "all" { + Backup-ShardDatabases + Check-ShardStatus + Update-Statistics + Monitor-ShardBalance + Cleanup-Logs + } + default { + Write-Host "用法: .\sqlserver_shard_maintenance.ps1 {backup|status|stats|cleanup|balance|all}" + exit 1 + } +} +``` + +## 最佳实践 + +### 1. 分片设计原则 + +- **选择合适的分片键**:选择分布均匀且查询频繁的字段 +- **避免热点分片**:确保数据在分片间均匀分布 +- **考虑查询模式**:根据业务查询模式设计分片策略 +- **规划扩展性**:预留足够的分片扩展空间 + +### 2. 性能优化 + +- **使用分区表**:利用SQL Server原生分区功能 +- **优化索引策略**:创建分区对齐的索引 +- **批量操作**:使用批量插入和更新提高性能 +- **查询优化**:避免跨分片查询,使用查询提示 + +### 3. 运维管理 + +- **监控告警**:设置关键性能指标监控 +- **定期维护**:定期更新统计信息和重建索引 +- **备份策略**:制定完善的备份恢复计划 +- **容量规划**:定期评估存储和性能需求 + +### 4. 高可用性 + +- **Always On配置**:使用Always On可用性组 +- **故障转移**:配置自动故障转移 +- **读写分离**:配置只读副本分担查询负载 +- **灾难恢复**:建立异地灾备方案 + +## 总结 + +SQL Server分片技术通过分区表、弹性数据库工具和Always On等功能,为企业应用提供了强大的水平扩展能力。合理的架构设计、性能优化和运维管理,可以构建高性能、高可用的分布式数据库系统。SQL Server的企业级特性和丰富的管理工具,使其成为Windows平台上的理想选择。 \ No newline at end of file diff --git "a/docs/aJava/WAF\345\222\214DDOS\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/WAF\345\222\214DDOS\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..e252c96b2 --- /dev/null +++ "b/docs/aJava/WAF\345\222\214DDOS\347\232\204\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,17 @@ +# WAF和DDOS的区别是什么 + +两大网络安全神器,WAF和DDOS防护: + +WAF,它是Web Application Firewall,Web应用防火墙,它主要防护的是Web应用的安全,比如SQL注入,XSS攻击,CSRF攻击等,它通过分析HTTP协议,识别出恶意请求,然后阻止它。WAF一般部署在Web服务器前,通过反向代理的方式,将恶意请求拦截,从而保护Web应用的安全。 + +是Web应用防火墙。专门拦截“伪装成正常用户”的攻击。比如黑客用SQL注入代码偷数据库,或者用XSS跨站脚本盗取用户账户。Waf会像安检员一样。 + +逐条扫描HTTP请求里的恶意内容。 + +拦截不符合规则的数据包。 + +DDOS攻击,攻击者用成千上万的“肉鸡”设备,用海量垃圾流量冲击你的服务器。 + +Waf防的是应用层攻击(比如篡改网页,数据窃取)DDoS防的是网络层洪水攻击(比如TCP、UDP洪流)防护策略不同:Waf靠规则引擎识别恶意代码,DDoS靠流量清洗中心过滤异常流量。 + +选Waf:电商,银行等需要防数据泄露的场景;选DDoS防护,游戏,直播等易受流量攻击的领域。 diff --git "a/docs/aJava/Zookeeper\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/Zookeeper\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..d96e2ac61 --- /dev/null +++ "b/docs/aJava/Zookeeper\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,20 @@ +# Zookeeper是什么 + +让分布式系统“不打架”的大佬,分布式系统里几十台服务器同时干活,谁先执行任务?谁监控状态?数据一致性怎么保证? +Zookeeper就是干这个的,它是一个分布式协调服务,可以用来维护配置信息、命名、提供分布式同步和提供组服务。 + +本质上它是一个分布式协调服务,核心原理:树,节点,监听 + +它用属性结构(ZNode)存储数据,节点分两种,持久节点(永久数据),和临时节点(会话结束就消失) + +更牛的是他的监听机制(Watcher),只要节点数据变动,立马通知所有相关程序 + +微服务里十几个服务互相调用,Zookeeper就是协调者,协调各个服务,协调各个节点,协调各个模块,协调各个服务器 + +服务注册与发现:kafka靠它管理Broker节点状态;设置分布式锁,配置中心,全部都能用它搞定! + +没有Zookeeper分布式系统可能就是大型车祸现场,数据冲突 + +节点失联,任务重复执行 + + diff --git a/docs/aJava/cookie-session-token.md b/docs/aJava/cookie-session-token.md new file mode 100644 index 000000000..35ec3a04f --- /dev/null +++ b/docs/aJava/cookie-session-token.md @@ -0,0 +1,21 @@ +# cookie-session-token + +登录的三大护法:Session Cookie Token + +为什么你关闭浏览器再打开淘宝,购物车还在?这就是Cookie在搞事情,它就像你逛超市时的小票,浏览器帮你把账号ID,浏览记录这些信息存在本地,下次访问时自动亮出会员卡。 + +致命弱点:黑客要偷你的Cookie,就能直接冒充你登录。 + +这就是为什么千万别点陌生链接。 + +但Session可就不一样了。服务器会给每个用户发个临时工牌,Session ID ,用户访问时,带上工牌,服务器就能知道你是谁了。Session ID存在服务器,用户关掉浏览器,Session ID就没了,下次访问,再领张新工牌。 + +所有隐私数据都锁在服务器保险柜。 + +Token,(JWT)加密通行证,把用户信息,有效期全加密成字符串。最牛的事服务器不用存数据,靠解密就能证明(这就叫无状态验证),现在微信,钉钉这些APP都在用。 + +1. 要简单用Cookie +2. 要安全用Session +3. 要灵活用Token(高并发,分布式系统) + + diff --git a/docs/aJava/img.png b/docs/aJava/img.png new file mode 100644 index 000000000..16f3c4183 Binary files /dev/null and b/docs/aJava/img.png differ diff --git a/docs/aJava/jvm.md b/docs/aJava/jvm.md new file mode 100644 index 000000000..3748f37ff --- /dev/null +++ b/docs/aJava/jvm.md @@ -0,0 +1,142 @@ +--- +title: jvm +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## jvm + +(1) 基本概念: + +JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 +一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接 +的交互。 + +java代码的执行:代码编译为class,javac;装载class,ClassLoader;执行class,解释执行,编译执行,client compiler,server compiler。 + +## 双亲委派机制 + +双亲委派机制(Parent Delegation Model)是Java类加载器的核心机制,它定义了类加载器之间的层次关系和加载规则。 + +### 类加载器层次结构 + +1. **启动类加载器(Bootstrap ClassLoader)** + - 最顶层的类加载器,由C++实现 + - 负责加载Java核心库(如rt.jar中的类) + - 加载路径:$JAVA_HOME/lib目录 + +2. **扩展类加载器(Extension ClassLoader)** + - 由Java实现,继承自ClassLoader + - 负责加载扩展库中的类 + - 加载路径:$JAVA_HOME/lib/ext目录 + +3. **应用程序类加载器(Application ClassLoader)** + - 也称为系统类加载器(System ClassLoader) + - 负责加载应用程序classpath下的类 + - 是用户自定义类加载器的默认父加载器 + +4. **用户自定义类加载器(User Defined ClassLoader)** + - 继承自ClassLoader类 + - 可以实现特定的类加载逻辑 + +### 双亲委派工作流程 + +1. **向上委派**:当一个类加载器收到类加载请求时,首先将请求委派给父类加载器 +2. **逐级委派**:父类加载器继续向上委派,直到达到启动类加载器 +3. **尝试加载**:启动类加载器尝试加载该类,如果能够加载则返回Class对象 +4. **向下返回**:如果父类加载器无法加载,则由子类加载器尝试加载 +5. **抛出异常**:如果所有类加载器都无法加载,则抛出ClassNotFoundException + +```java +protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 首先检查类是否已经被加载 + Class c = findLoadedClass(name); + if (c == null) { + try { + if (parent != null) { + // 委派给父类加载器 + c = parent.loadClass(name, false); + } else { + // 委派给启动类加载器 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // 父类加载器无法加载时,由当前加载器尝试加载 + } + + if (c == null) { + // 调用findClass方法加载类 + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } +} +``` + +### 双亲委派的优势 + +1. **安全性**:防止核心API被篡改,确保Java核心库的类由启动类加载器加载 +2. **避免重复加载**:确保同一个类在JVM中只有一个Class对象 +3. **层次清晰**:类加载器之间形成清晰的层次关系 +4. **稳定性**:保证Java程序的稳定运行 + +### 破坏双亲委派的场景 + +1. **自定义类加载器**:重写loadClass方法而不调用父类加载器 +2. **线程上下文类加载器**:用于解决SPI(Service Provider Interface)加载问题 +3. **OSGi框架**:实现模块化的类加载机制 +4. **热部署**:在不重启JVM的情况下更新类文件 + +内存管理:内存空间,方法区,堆,方法栈,本地方法栈,pc寄存器;内存分片,堆上分配,TLAB分配,栈上分配; + +内存回收:算法 Copy Mark-Sweep, Mark-Compact;Sun JDK 分代回收 GC参数,G1 + +分代回收:新生代可用的GC 串行copying,并行回收copying,并行copying; Minor GC 触发机制以及日志格式; +旧生代可用的GC: 串行 Mark-Sweep-Compact ,并行 Compacting, 并发 Mark-Sweep +Full GC 触发机制以及日志格式 + +内存状况分析:jconsole,visualvm,jstat,jmap,mat + +线程资源同步和交互机制: + +线程资源同步:线程资源执行机制;线程资源同步机制: Synchronized的实现机制,lock/unlock的实现机制 + +线程交互机制:Object.wait/notify/notifyAll, Thread.join, Thread.sleep, Thread.yield, Thread.interrupt; 并发包提供的交互机制: semaphore,CountdownLatch + +线程状态以及分析方法:jstack、 tda + +(2) 运行过程: + +我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码。 + +也就是如下: + +① Java 源文件—->编译器—->字节码文件 + +② 字节码文件—->JVM—->机器码 + +每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够 +跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 +存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不 +能共享。 + + +运行时数据区 Runtime Data Area + +方法区 Method Area (共享) 虚拟机栈 VM Stack (私有) 本地方法栈 Native Method Stack (私有) 程序计数器 Program Counter Register (私有) + +堆 Heap (共享) + +执行引擎:即时编译器 JIT 垃圾收集器 GC + +本地库接口,本地方法库 + diff --git "a/docs/aJava/mogodb\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/mogodb\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..f41b87ebe --- /dev/null +++ "b/docs/aJava/mogodb\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,50 @@ +# mogodb是什么 + +mogodb是一个基于分布式文件存储的数据库。由C++语言编写。 + +# 什么是分布式文件存储? + +分布式文件存储是指将文件存储在多个节点上,每个节点都可以存储一部分文件。这样可以提高文件存储的效率,也可以提高文件存储的可靠性。 + +# 什么是C++语言? + +C++是一种高级编程语言,它是一种面向对象的编程语言。C++语言是一种通用的编程语言,它可以用来开发各种类型的应用程序。 + +# 什么是面向对象的编程语言? + +面向对象的编程语言是指使用面向对象的思想来编写程序。面向对象的思想是将现实世界中的事物抽象为对象,然后使用对象来描述现实世界中的事物。 + +面向对象的编程语言有三个基本特征:封装、继承、多态。 + +封装是指将数据和操作数据的方法封装在一个对象中。这样可以隐藏对象的实现细节,只向外部暴露必要的方法。 + +继承是指一个对象可以继承另一个对象的属性和方法。这样可以避免重复编写代码,提高代码的复用性。 + +多态是指一个对象可以有多种形态。这意味着一个对象可以被视为它自己的类型,也可以被视为它的父类型。 + +# 什么是面向对象的数据库? + +面向对象的数据库是指使用面向对象的思想来设计数据库。使用面向对象的思想来设计数据库,可以提高数据库的效率,也可以提高数据库的可维护性。 + +# 什么是mogodb? + +mogodb是一个面向对象的数据库。它是一个分布式文件存储数据库。它是一个基于分布式文件存储的数据库。它是一个基于C++语言编写的数据库。 + +# mogodb的优势 + +mogodb的优势有: + +1. 高性能:mogodb是一个高性能的数据库。它可以支持大量的数据读写操作。 +2. 高可扩展性:mogodb是一个高可扩展性的数据库。它可以通过添加节点来扩展数据库的容量和性能。 +3. 高可用性:mogodb是一个高可用性的数据库。它可以通过复制数据来保证数据库的可用性。 +4. 丰富的查询功能:mogodb是一个功能丰富的数据库。它支持丰富的查询功能,包括基本的查询、高级的查询、文本查询、地理空间查询等。 +5. 支持多种数据模型:mogodb是一个支持多种数据模型的数据库。它支持文档型数据模型、键值型数据模型、列存储型数据模型和图形型数据模型等。 + +# mogodb的应用场景 + +mogodb的应用场景有: + +1. 内容管理系统:mogodb可以用来存储和管理内容数据。例如,博客、新闻、产品评论等。 +2. 实时数据分析:mogodb可以用来存储和分析实时产生的数据。例如,传感器数据、日志数据等。 +3. 社交网络:mogodb可以用来存储和管理用户生成的内容。例如,用户的好友列表、用户的动态等。 +4. 物联网(IoT):mogodb可以用来存储和管理物联网设备生成的数据。例如,传感器数据、设备状态等。 diff --git "a/docs/aJava/mysql\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/mysql\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..25f4908dd --- /dev/null +++ "b/docs/aJava/mysql\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,32 @@ +# mysql是什么 + +mysql是一种关系型数据库, 是一个关系型数据库管理系统, 由瑞典MySQL AB 公司开发, 目前属于 Oracle 公司. + +## mysql有哪些功能? + +mysql有哪些功能? + +mysql有以下功能: + +1. 关系型数据库 +2. 数据库管理系统 +3. 支持多种编程语言 +4. 支持多种操作系统 +5. 支持多种数据库客户端 +6. 支持多种数据库服务器 +7. 支持多种数据库协议 +8. 支持多种数据库引擎 +9. 支持多种数据库备份和恢复 +10. 支持多种数据库压缩 +11. 支持多种数据库加密 +12. 支持多种数据库解密 +13. 支持多种数据库索引 +14. 支持多种数据库视图 +15. 支持多种数据库触发器 +16. 支持多种数据库存储过程 +17. 支持多种数据库函数 +18. 支持多种数据库事件 +19. 支持多种数据库角色 +20. 支持多种数据库用户 +21. 支持多种数据库表 +22. 支持多种数据库列 diff --git "a/docs/aJava/nacos\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/nacos\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..890acaddd --- /dev/null +++ "b/docs/aJava/nacos\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,31 @@ +# nacos是什么 + +Nacos是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 + +Nacos致力于帮助您发现、配置和管理微服务。Nacos提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 + +为什么大厂微服务都在用nacos? + +Nacos架构解析:Nacos本质上是个双核引擎,既是配置中心,又是服务发现中心。它的架构核心是插件化设计,通过Core内核层支撑配置管理,Namespace和服务管理Cluster双模块,注意看这个Distro的协议,这是阿里自研的AP架构基石。 + +采用异步复制实现最终一致性,配合健康检查的Beat心跳机制,这就是它能支撑十万级节点吞吐的秘诀。 + +和 Eureka 最大的区别在数据一致性模型。Eureka纯AP架构采用自我保护模式,而Nacos可以动态切换AP/CP模式。 +Raft协议保证强一致性。Distro协议保障高可用。看核心指标:Nacos 2.0 的gRPC长连接。 +能把服务发现延迟压到毫秒级别。这是Eureka的HTTP轮询机制永远做不到的。 + +架构选型: + +1. 看是否需要统一配置中心。 +2. 看集群规模是否超过500节点。 +3. 看是否需要k8s集成。 + +Eureka已经停止维护。 + +配置中心+服务网格=Nacos + +纯服务发现=Consul + +Nacos重构电商大促系统,动态配置推送让秒杀库存切换实现了零停机。当你有跨地域多活需求时,Nacos的集群同步机制能自动识别机房拓扑,这就是为什么双十一每秒百万次,配置不蹦的关键。 + +注册中心选型:需要配置版本管理吗;能接受秒级服务上下线延迟吗;要不要无缝对接Sentinel流量管控 diff --git "a/docs/aJava/nginx\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/nginx\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..cd55ea11e --- /dev/null +++ "b/docs/aJava/nginx\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,30 @@ +# nginx是什么 + +nginx可以作为HTTP服务器,也可以作为反向代理服务器。 + +就是单Master多Worker架构,想象一个物流中心:Master进程是总指挥,读取nginx.conf配置管理worker, Worker进程是工人,数量对应CPU核数,每个Worker单线程却能处理上万个请求,背后是事件驱动模型,用异步非阻塞机制,让一个线程同时管理数千连接,让一个线程同时管理数千连接,彻底解决性能难题,Worker进程共享监听端口的设计,当你在浏览器输入URL时,操作系统通过SO_REUSEPORT参数,把请求随机分配给任意Worker,即使某个Worker崩溃,其他进程照常运转,这就是高可用的核心秘密。Master进程负责接收请求,Worker进程负责处理请求。 + +- 事件驱动模型 +- 异步非阻塞IO +- 进程间共享监听端口 + +三大核心模块: + +1. 反向代理如同智能路由路,对外暴露统一域名,背后自动轮询10台服务器,实现负载均衡。负载均衡实现了请求分发,实现了高可用,实现了高并发。 +2. 负载均衡算法库自带加权轮询,IP哈希等七种策略,轻松应对秒杀场景 +3. Proxy Cache把热点数据缓存到磁盘,下次同样请求直接返回本地文件,响应速度提升10倍 + +共享内存机制解决了分布式限流难题:所有Woker共用计数内存,说好每秒限流1000次,就不会出现单个进程放行999次的漏洞 + +动静分离:把图片视频交给Nginx处理,把动态请求交给Tomcat处理,实现了动静分离,提高了响应速度。 + +动态请求才找后端,带宽成本立减70%。 + +当你更新配置时,Master会启动新Worker接管流量,旧Worker完成现有请求才退出,这叫优雅重启。 + +加上lua脚本的扩展能力,轻松实现鉴权,改写响应等自定义逻辑。 + +Nginx能扛住5万QPS,这是它的核心竞争力。 + +单节点宕机,只需要keep alived+VIP组成双机热备,实现了高可用。 +服务器总在流量高峰崩溃,微服务接口分散难以管理,CDN费用居高不下,一定要想到Nginx。 diff --git "a/docs/aJava/tomcat\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/tomcat\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..51789faa1 --- /dev/null +++ "b/docs/aJava/tomcat\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,13 @@ +# tomcat是什么 + +tomcat 是一个 web 服务器,用于处理 http 请求。 + +tomcat的本质就是个“服务员“,专业点说就是开源的java servlet容器。简单来说,当你用浏览器访问网站时,tomcat就像餐厅传菜员,把java代码做的”菜品“,翻译成你能看懂的HTML页面,它内置的JSP引擎会把页面布局,魔法符合变成正经的Java代码,Coyote连接器负责和浏览器对暗号”处理HTTP协议。 + +tomcat最牛的是它的类加载机制,每个应用独立的ClassLoader,就像给不同包间装隔离墙,内存泄露,不存在的,配合NIO非阻塞IO模型,每秒处理上千请求跟玩似的。 + +在server.xml的maxConnections属性中可以设置最大连接数,默认是200。 + +配合springboot变成微服务大佬们,tomcat的配置都在application.yml中。把reloadable属性设置为true,就可以实现热部署,修改代码后,不用重启服务器,就能看到效果。 + +> tomcat 是Java写前端的好帮手。为啥? 因为它是一个web服务器,它可以处理http请求,返回html页面。 diff --git "a/docs/aJava/\344\272\221\350\256\241\347\256\227\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\344\272\221\350\256\241\347\256\227\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..bc1e1cde7 --- /dev/null +++ "b/docs/aJava/\344\272\221\350\256\241\347\256\227\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,7 @@ +# 云计算是什么 + +云计算,超级大脑集群, IaaS,PaaS,SaaS, + +1. 云上十万服务器秒级支援 +2. 三地备份 +3. 秒租云主机 \ No newline at end of file diff --git "a/docs/aJava/\344\272\244\346\215\242\346\234\272\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\344\272\244\346\215\242\346\234\272\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..9d2dbebc8 --- /dev/null +++ "b/docs/aJava/\344\272\244\346\215\242\346\234\272\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,408 @@ +# 交换机是什么 + +交换机(Switch)是一种网络设备,工作在OSI模型的第二层(数据链路层),主要功能是在局域网内转发数据帧。它通过学习和维护MAC地址表,实现数据的精确转发,是现代以太网的核心设备。 + +## 基本概念 + +交换机可以看作是"智能的集线器",它能够记住连接到每个端口的设备MAC地址,并根据目标MAC地址将数据精确发送到对应端口,而不是像集线器那样向所有端口广播。 + +### 核心功能 +- **MAC地址学习:** 自动学习并记录连接设备的MAC地址 +- **数据帧转发:** 根据目标MAC地址转发数据帧 +- **冲突域隔离:** 每个端口形成独立的冲突域 +- **全双工通信:** 支持同时发送和接收数据 + +## 交换机的工作原理 + +### 1. MAC地址表学习过程 +``` +步骤1:接收数据帧 +步骤2:记录源MAC地址和对应端口 +步骤3:查找目标MAC地址 +步骤4:转发或泛洪数据帧 +``` + +### 2. 数据转发方式 + +**已知单播(Known Unicast)** +- 目标MAC地址在MAC表中 +- 直接转发到对应端口 +- 效率最高 + +**未知单播(Unknown Unicast)** +- 目标MAC地址不在MAC表中 +- 向除源端口外的所有端口泛洪 +- 等待目标设备回应 + +**广播(Broadcast)** +- 目标MAC地址为FF:FF:FF:FF:FF:FF +- 向除源端口外的所有端口发送 +- 用于ARP请求等协议 + +**组播(Multicast)** +- 目标MAC地址为组播地址 +- 向组播组成员端口发送 +- 需要IGMP Snooping支持 + +## 交换机类型 + +### 1. 按管理方式分类 + +**非管理型交换机(Unmanaged Switch)** +- 特点:即插即用,无需配置 +- 功能:基本的数据转发 +- 适用:家庭、小型办公室 +- 价格:便宜 + +**管理型交换机(Managed Switch)** +- 特点:可配置、可管理 +- 功能:VLAN、QoS、端口镜像等 +- 适用:企业网络 +- 价格:较高 + +**智能管理型交换机(Smart Switch)** +- 特点:部分管理功能 +- 功能:基本VLAN、QoS +- 适用:中小企业 +- 价格:中等 + +### 2. 按网络层次分类 + +**接入层交换机(Access Switch)** +- 功能:连接终端设备 +- 特点:端口密度高、成本低 +- 典型:24/48端口千兆交换机 + +**汇聚层交换机(Distribution Switch)** +- 功能:汇聚接入层流量 +- 特点:高性能、支持三层功能 +- 典型:模块化交换机 + +**核心层交换机(Core Switch)** +- 功能:高速数据转发 +- 特点:超高性能、高可靠性 +- 典型:机箱式交换机 + +### 3. 按功能特性分类 + +**二层交换机(Layer 2 Switch)** +- 工作层次:数据链路层 +- 功能:MAC地址学习、VLAN +- 转发依据:MAC地址 + +**三层交换机(Layer 3 Switch)** +- 工作层次:网络层 +- 功能:路由、VLAN间通信 +- 转发依据:IP地址 + +**多层交换机(Multilayer Switch)** +- 工作层次:二到七层 +- 功能:负载均衡、防火墙 +- 转发依据:多种协议头部信息 + +## 重要技术特性 + +### 1. VLAN(虚拟局域网) + +**VLAN的作用:** +- 逻辑分割网络 +- 提高安全性 +- 减少广播域 +- 灵活的网络管理 + +**VLAN配置示例:** +```bash +# 创建VLAN +vlan 10 +name Sales +vlan 20 +name Engineering + +# 配置接入端口 +interface FastEthernet0/1 +switchport mode access +switchport access vlan 10 + +# 配置Trunk端口 +interface FastEthernet0/24 +switchport mode trunk +switchport trunk allowed vlan 10,20 +``` + +### 2. STP(生成树协议) + +**STP的作用:** +- 防止环路 +- 提供冗余路径 +- 自动故障切换 + +**STP状态:** +``` +Blocking → 阻塞状态,不转发数据 +Listening → 监听状态,选举根桥 +Learning → 学习状态,学习MAC地址 +Forwarding → 转发状态,正常工作 +Disabled → 禁用状态,端口关闭 +``` + +### 3. 端口聚合(Link Aggregation) + +**LAG的优势:** +- 增加带宽 +- 提供冗余 +- 负载均衡 + +**配置示例:** +```bash +# 创建端口通道 +interface port-channel 1 +switchport mode trunk + +# 添加物理端口 +interface range FastEthernet0/1-2 +channel-group 1 mode active +``` + +## 实际应用场景 + +### 1. 家庭网络 +``` +路由器 ← → 交换机 ← → 电脑/电视/NAS等设备 +``` + +**典型配置:** +- 5-8端口非管理型交换机 +- 千兆端口 +- 即插即用 + +### 2. 办公网络 +``` +核心交换机 + ↓ +汇聚交换机 + ↓ +接入交换机 ← → 办公设备 +``` + +**网络设计:** +- 接入层:48端口千兆交换机 +- 汇聚层:24端口万兆交换机 +- 核心层:模块化交换机 + +### 3. 数据中心 +``` +Spine交换机(核心层) + ↓ +Leaf交换机(接入层) + ↓ +服务器/存储设备 +``` + +**特点:** +- 高密度端口 +- 低延迟 +- 高可靠性 + +## 交换机配置实例 + +### 1. 基本配置 +```bash +# 设置主机名 +hostname SW-Office-01 + +# 配置管理IP +interface vlan 1 +ip address 192.168.1.10 255.255.255.0 +no shutdown + +# 设置默认网关 +ip default-gateway 192.168.1.1 + +# 配置用户账号 +username admin privilege 15 secret cisco123 +``` + +### 2. VLAN配置 +```bash +# 创建VLAN +vlan 10 +name Sales +vlan 20 +name IT +vlan 30 +name Guest + +# 配置接入端口 +interface range FastEthernet0/1-12 +switchport mode access +switchport access vlan 10 + +interface range FastEthernet0/13-24 +switchport mode access +switchport access vlan 20 + +# 配置Trunk端口 +interface GigabitEthernet0/1 +switchport mode trunk +switchport trunk allowed vlan 10,20,30 +``` + +### 3. 端口安全配置 +```bash +# 配置端口安全 +interface FastEthernet0/1 +switchport port-security +switchport port-security maximum 2 +switchport port-security mac-address sticky +switchport port-security violation restrict +``` + +### 4. QoS配置 +```bash +# 配置QoS策略 +class-map match-all VOICE +match ip dscp ef + +class-map match-all VIDEO +match ip dscp af41 + +policy-map QOS-POLICY +class VOICE +priority percent 30 +class VIDEO +bandwidth percent 40 +class class-default +bandwidth percent 30 + +# 应用到接口 +interface GigabitEthernet0/1 +service-policy output QOS-POLICY +``` + +## 性能监控与故障排除 + +### 1. 监控命令 +```bash +# 查看MAC地址表 +show mac address-table + +# 查看端口状态 +show interfaces status + +# 查看VLAN信息 +show vlan brief + +# 查看生成树状态 +show spanning-tree + +# 查看端口统计 +show interfaces counters +``` + +### 2. 常见问题排查 + +**端口不通** +```bash +# 检查端口状态 +show interfaces FastEthernet0/1 + +# 检查端口配置 +show running-config interface FastEthernet0/1 + +# 检查VLAN配置 +show vlan id 10 +``` + +**广播风暴** +```bash +# 查看端口流量 +show interfaces counters broadcast + +# 配置广播抑制 +interface FastEthernet0/1 +storm-control broadcast level 10 +``` + +**MAC地址表满** +```bash +# 查看MAC表使用情况 +show mac address-table count + +# 调整老化时间 +mac address-table aging-time 300 +``` + +### 3. 性能优化 + +**端口优化** +```bash +# 配置端口速度和双工 +interface FastEthernet0/1 +speed 100 +duplex full + +# 关闭不必要的协议 +no cdp enable +no lldp transmit +no lldp receive +``` + +**缓冲区优化** +```bash +# 调整缓冲区大小 +mls qos queue-set output 1 buffers 10 25 40 25 +``` + +## 选型建议 + +### 1. 家用/SOHO +- **端口数量:** 5-8端口 +- **速率:** 千兆 +- **功能:** 基本转发 +- **价格:** 100-300元 + +### 2. 中小企业 +- **端口数量:** 24-48端口 +- **速率:** 千兆接入+万兆上联 +- **功能:** VLAN、QoS、端口安全 +- **价格:** 2000-8000元 + +### 3. 大型企业 +- **端口数量:** 模块化扩展 +- **速率:** 万兆/25G/40G/100G +- **功能:** 全功能三层交换 +- **价格:** 几万到几十万 + +### 4. 数据中心 +- **端口数量:** 高密度 +- **速率:** 25G/40G/100G/400G +- **功能:** 低延迟、高吞吐 +- **价格:** 十万到百万级 + +## 发展趋势 + +### 1. 软件定义网络(SDN) +- OpenFlow协议支持 +- 集中式控制器 +- 可编程数据平面 + +### 2. 云原生交换 +- 白盒交换机 +- 开源网络操作系统 +- 容器化网络功能 + +### 3. 智能化运维 +- AI驱动的故障预测 +- 自动化配置管理 +- 智能流量分析 + +### 4. 高速以太网 +- 400G/800G端口 +- 更低的延迟 +- 更高的端口密度 + +## 总结 + +交换机作为局域网的核心设备,在网络通信中扮演着重要角色。从简单的数据转发到复杂的网络管理,交换机技术不断发展演进。 + +理解交换机的工作原理、掌握配置方法、熟悉故障排除技巧,是网络工程师的基本技能。随着云计算、SDN、AI等技术的发展,交换机也在向着更加智能化、软件化的方向发展,为构建高效、灵活、可靠的网络基础设施提供强有力的支撑。 \ No newline at end of file diff --git "a/docs/aJava/\344\273\200\344\271\210\346\230\257\347\201\260\345\272\246\345\217\221\345\270\203.md" "b/docs/aJava/\344\273\200\344\271\210\346\230\257\347\201\260\345\272\246\345\217\221\345\270\203.md" new file mode 100644 index 000000000..5d10c0445 --- /dev/null +++ "b/docs/aJava/\344\273\200\344\271\210\346\230\257\347\201\260\345\272\246\345\217\221\345\270\203.md" @@ -0,0 +1,19 @@ +# 什么是灰度发布 + +灰度发布,蓝绿发布,滚动发布: + +灰度发布,它的原理就像矿井用金丝雀探毒气,程序员先让1%的用户用新版本,其他99%的用旧版本,通过AB测试对比数据,通过流量分流,比如用Nginx的权重配置,没问题再逐渐扩大流量,这样既能验证新功能。又不会让所有人一起踩坑,它可以用于电商大促前,上新功能,用灰度发布,崩了也只影响小部分用户。 + +蓝绿发布,土豪的双倍快乐。原理:直接部署两套环境,蓝环境跑旧版,绿环境跑新版,通过负载均衡一键切换全部流量。关键技术是基础设施即代码(IaC),和DNS解析切换。 + +比如Kuberneses,它通过Deployment,可以快速部署一套新版本,通过Service,可以快速切换流量。蓝绿发布,它需要双倍资源 +比如Kuberneses里同时部署两套Deployment,瞬间切换Service的流量,就可以完成蓝绿发布。但代价是资源翻倍消耗 + +同时它也有致命弱点:如果新版数据库有兼容问题?全量回滚要30分钟起步! + +滚动发布,穷鬼的极限操作:原理:像火车换轮子,先启动1台新服务器,关1台旧服务器,循环直到全切换完;关键技术是健康检查和滚动更新策略。比如Docker Swarm 的--update-parallelism参数,这种发布方式最省资源,但回滚速度慢。 +也有致命陷阱,新旧版本会同时在线。用户可能上午看到新版界面,下午又变回旧版。 + +1. 资源消耗:蓝绿大于滚动大于灰度 +2. 回滚速度:蓝绿最快(秒级)灰度中级(流量切换)滚动最慢(得重新部署 +3. 适用场景,灰度:金融,电商等高风险系统;蓝绿:土豪公司发工资系统升级;滚动:小公司半夜偷偷更新 \ No newline at end of file diff --git "a/docs/aJava/\345\205\205\347\224\265\345\256\235\344\270\232\345\212\241\345\256\236\347\216\260.md" "b/docs/aJava/\345\205\205\347\224\265\345\256\235\344\270\232\345\212\241\345\256\236\347\216\260.md" new file mode 100644 index 000000000..a84890e38 --- /dev/null +++ "b/docs/aJava/\345\205\205\347\224\265\345\256\235\344\270\232\345\212\241\345\256\236\347\216\260.md" @@ -0,0 +1,472 @@ +# 充电宝业务实现 + +## 业务概述 + +充电宝租赁业务是一种基于物联网技术的共享经济模式,用户可以通过移动应用扫码租借充电宝,使用完毕后归还到任意网点。 + +## 系统架构 + +### 整体架构 + +``` +用户端APP <-> API网关 <-> 业务服务 <-> 数据库 + | + v + 设备管理服务 <-> 充电宝设备 +``` + +### 核心组件 + +1. **用户服务**:用户注册、登录、实名认证 +2. **订单服务**:租借订单管理、计费结算 +3. **设备服务**:充电宝设备管理、状态监控 +4. **支付服务**:支付、退款、押金管理 +5. **位置服务**:网点管理、设备定位 + +## 核心业务流程 + +### 1. 用户注册与认证 + +```java +@Service +public class UserService { + + /** + * 用户注册 + */ + public UserRegisterResponse register(UserRegisterRequest request) { + // 1. 验证手机号格式 + validatePhoneNumber(request.getPhoneNumber()); + + // 2. 发送验证码 + smsService.sendVerificationCode(request.getPhoneNumber()); + + // 3. 创建用户账户 + User user = new User(); + user.setPhoneNumber(request.getPhoneNumber()); + user.setStatus(UserStatus.UNVERIFIED); + userRepository.save(user); + + return UserRegisterResponse.success(); + } + + /** + * 实名认证 + */ + public void realNameAuth(Long userId, String realName, String idCard) { + // 1. 调用第三方实名认证接口 + AuthResult result = authService.authenticate(realName, idCard); + + if (result.isSuccess()) { + // 2. 更新用户状态 + User user = userRepository.findById(userId); + user.setRealName(realName); + user.setIdCard(idCard); + user.setStatus(UserStatus.VERIFIED); + userRepository.save(user); + } + } +} +``` + +### 2. 设备扫码租借 + +```java +@Service +public class RentalService { + + /** + * 扫码租借充电宝 + */ + @Transactional + public RentalResponse rentPowerBank(Long userId, String deviceCode) { + // 1. 验证用户状态 + User user = userService.getUser(userId); + if (!user.isVerified()) { + throw new BusinessException("用户未实名认证"); + } + + // 2. 检查设备状态 + PowerBankDevice device = deviceService.getByCode(deviceCode); + if (!device.isAvailable()) { + throw new BusinessException("设备不可用"); + } + + // 3. 检查用户是否有未归还订单 + if (orderService.hasUnreturnedOrder(userId)) { + throw new BusinessException("存在未归还的充电宝"); + } + + // 4. 创建租借订单 + RentalOrder order = new RentalOrder(); + order.setUserId(userId); + order.setDeviceId(device.getId()); + order.setStartTime(LocalDateTime.now()); + order.setStatus(OrderStatus.RENTING); + orderRepository.save(order); + + // 5. 更新设备状态 + device.setStatus(DeviceStatus.RENTED); + device.setCurrentUserId(userId); + deviceRepository.save(device); + + // 6. 发送开锁指令 + deviceControlService.unlock(device.getId()); + + return RentalResponse.success(order.getId()); + } +} +``` + +### 3. 充电宝归还 + +```java +@Service +public class ReturnService { + + /** + * 归还充电宝 + */ + @Transactional + public ReturnResponse returnPowerBank(Long userId, String stationCode, String deviceCode) { + // 1. 查找用户当前租借订单 + RentalOrder order = orderService.getCurrentOrder(userId); + if (order == null) { + throw new BusinessException("无有效租借订单"); + } + + // 2. 验证归还设备 + PowerBankDevice device = deviceService.getByCode(deviceCode); + if (!device.getId().equals(order.getDeviceId())) { + throw new BusinessException("设备不匹配"); + } + + // 3. 验证归还网点 + Station station = stationService.getByCode(stationCode); + if (!station.isActive()) { + throw new BusinessException("归还网点不可用"); + } + + // 4. 计算费用 + BigDecimal totalFee = calculateFee(order.getStartTime(), LocalDateTime.now()); + + // 5. 更新订单状态 + order.setEndTime(LocalDateTime.now()); + order.setReturnStationId(station.getId()); + order.setTotalFee(totalFee); + order.setStatus(OrderStatus.COMPLETED); + orderRepository.save(order); + + // 6. 更新设备状态 + device.setStatus(DeviceStatus.AVAILABLE); + device.setCurrentUserId(null); + device.setStationId(station.getId()); + deviceRepository.save(device); + + // 7. 扣费 + paymentService.charge(userId, totalFee, order.getId()); + + return ReturnResponse.success(totalFee); + } + + /** + * 计算租借费用 + */ + private BigDecimal calculateFee(LocalDateTime startTime, LocalDateTime endTime) { + Duration duration = Duration.between(startTime, endTime); + long hours = duration.toHours(); + + // 按小时计费,不足1小时按1小时计算 + if (duration.toMinutes() % 60 > 0) { + hours++; + } + + // 每小时2元 + return BigDecimal.valueOf(hours * 2); + } +} +``` + +## 设备管理 + +### 设备状态监控 + +```java +@Component +public class DeviceMonitor { + + /** + * 设备心跳监控 + */ + @Scheduled(fixedRate = 60000) // 每分钟执行一次 + public void monitorDeviceHeartbeat() { + List devices = deviceRepository.findAll(); + + for (PowerBankDevice device : devices) { + if (isDeviceOffline(device)) { + // 设备离线处理 + handleDeviceOffline(device); + } + } + } + + /** + * 处理设备离线 + */ + private void handleDeviceOffline(PowerBankDevice device) { + // 1. 更新设备状态 + device.setStatus(DeviceStatus.OFFLINE); + deviceRepository.save(device); + + // 2. 如果设备正在被租借,通知用户 + if (device.getCurrentUserId() != null) { + notificationService.notifyDeviceOffline(device.getCurrentUserId(), device.getId()); + } + + // 3. 发送告警 + alertService.sendDeviceOfflineAlert(device); + } +} +``` + +### 设备远程控制 + +```java +@Service +public class DeviceControlService { + + /** + * 远程开锁 + */ + public void unlock(Long deviceId) { + PowerBankDevice device = deviceRepository.findById(deviceId); + + // 构建控制指令 + DeviceCommand command = DeviceCommand.builder() + .deviceId(deviceId) + .command("UNLOCK") + .timestamp(System.currentTimeMillis()) + .build(); + + // 发送到设备 + mqttService.publish(device.getTopic(), command); + + // 记录操作日志 + deviceLogService.log(deviceId, "UNLOCK", "远程开锁"); + } + + /** + * 查询设备状态 + */ + public DeviceStatus queryStatus(Long deviceId) { + PowerBankDevice device = deviceRepository.findById(deviceId); + + DeviceCommand command = DeviceCommand.builder() + .deviceId(deviceId) + .command("QUERY_STATUS") + .timestamp(System.currentTimeMillis()) + .build(); + + return mqttService.sendAndReceive(device.getTopic(), command, DeviceStatus.class); + } +} +``` + +## 支付系统 + +### 押金管理 + +```java +@Service +public class DepositService { + + /** + * 收取押金 + */ + public void chargeDeposit(Long userId) { + User user = userService.getUser(userId); + + // 检查是否已缴纳押金 + if (user.getDepositStatus() == DepositStatus.PAID) { + return; + } + + // 创建押金订单 + DepositOrder order = new DepositOrder(); + order.setUserId(userId); + order.setAmount(new BigDecimal("99.00")); // 押金99元 + order.setStatus(DepositStatus.PENDING); + depositOrderRepository.save(order); + + // 调用支付接口 + PaymentResult result = paymentService.pay(order); + + if (result.isSuccess()) { + // 更新用户押金状态 + user.setDepositStatus(DepositStatus.PAID); + userRepository.save(user); + + // 更新订单状态 + order.setStatus(DepositStatus.PAID); + depositOrderRepository.save(order); + } + } + + /** + * 退还押金 + */ + public void refundDeposit(Long userId) { + User user = userService.getUser(userId); + + // 检查是否有未归还订单 + if (orderService.hasUnreturnedOrder(userId)) { + throw new BusinessException("存在未归还订单,无法退还押金"); + } + + // 查找押金订单 + DepositOrder order = depositOrderRepository.findByUserId(userId); + + // 执行退款 + RefundResult result = paymentService.refund(order); + + if (result.isSuccess()) { + // 更新用户状态 + user.setDepositStatus(DepositStatus.REFUNDED); + userRepository.save(user); + } + } +} +``` + +## 数据模型 + +### 用户表 + +```sql +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `phone_number` varchar(11) NOT NULL COMMENT '手机号', + `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', + `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', + `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未验证,1-已验证', + `deposit_status` tinyint NOT NULL DEFAULT '0' COMMENT '押金状态:0-未缴纳,1-已缴纳,2-已退还', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_phone` (`phone_number`) +) ENGINE=InnoDB COMMENT='用户表'; +``` + +### 设备表 + +```sql +CREATE TABLE `power_bank_device` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `device_code` varchar(32) NOT NULL COMMENT '设备编码', + `station_id` bigint DEFAULT NULL COMMENT '所属网点ID', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-可用,2-已租借,3-故障,4-离线', + `battery_level` int DEFAULT NULL COMMENT '电量百分比', + `current_user_id` bigint DEFAULT NULL COMMENT '当前使用用户ID', + `last_heartbeat` datetime DEFAULT NULL COMMENT '最后心跳时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_device_code` (`device_code`), + KEY `idx_station_id` (`station_id`), + KEY `idx_current_user_id` (`current_user_id`) +) ENGINE=InnoDB COMMENT='充电宝设备表'; +``` + +### 订单表 + +```sql +CREATE TABLE `rental_order` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `device_id` bigint NOT NULL COMMENT '设备ID', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime DEFAULT NULL COMMENT '结束时间', + `rent_station_id` bigint NOT NULL COMMENT '租借网点ID', + `return_station_id` bigint DEFAULT NULL COMMENT '归还网点ID', + `total_fee` decimal(10,2) DEFAULT NULL COMMENT '总费用', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-租借中,2-已完成,3-异常', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB COMMENT='租借订单表'; +``` + +## 技术要点 + +### 1. 分布式锁 + +使用Redis分布式锁防止同一设备被多人同时租借: + +```java +@Component +public class DistributedLock { + + public boolean tryLock(String key, String value, long expireTime) { + String result = redisTemplate.execute((RedisCallback) connection -> { + return connection.set(key.getBytes(), value.getBytes(), + Expiration.seconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT); + }); + return "OK".equals(result); + } +} +``` + +### 2. 消息队列 + +使用RabbitMQ处理异步业务: + +```java +@RabbitListener(queues = "device.status.queue") +public void handleDeviceStatusChange(DeviceStatusMessage message) { + // 处理设备状态变更 + deviceService.updateStatus(message.getDeviceId(), message.getStatus()); +} +``` + +### 3. 缓存策略 + +使用Redis缓存热点数据: + +```java +@Cacheable(value = "device", key = "#deviceCode") +public PowerBankDevice getByCode(String deviceCode) { + return deviceRepository.findByDeviceCode(deviceCode); +} +``` + +## 运维监控 + +### 1. 业务监控指标 + +- 设备在线率 +- 订单成功率 +- 平均租借时长 +- 收入统计 + +### 2. 告警规则 + +- 设备离线超过5分钟告警 +- 订单异常率超过1%告警 +- 支付失败率超过0.5%告警 + +### 3. 日志规范 + +```java +// 业务日志 +log.info("用户{}租借设备{}成功,订单号:{}", userId, deviceId, orderId); + +// 错误日志 +log.error("设备{}开锁失败,错误信息:{}", deviceId, e.getMessage(), e); +``` + +## 总结 + +充电宝业务系统涉及用户管理、设备控制、订单处理、支付结算等多个模块,需要考虑高并发、分布式、实时性等技术挑战。通过合理的架构设计和技术选型,可以构建一个稳定可靠的充电宝租赁平台。 \ No newline at end of file diff --git "a/docs/aJava/\345\205\205\347\224\265\346\241\251\344\270\232\345\212\241\345\256\236\347\216\260.md" "b/docs/aJava/\345\205\205\347\224\265\346\241\251\344\270\232\345\212\241\345\256\236\347\216\260.md" new file mode 100644 index 000000000..4fc0f0ced --- /dev/null +++ "b/docs/aJava/\345\205\205\347\224\265\346\241\251\344\270\232\345\212\241\345\256\236\347\216\260.md" @@ -0,0 +1,1293 @@ +# 充电桩业务实现 + +## 概述 + +充电桩业务系统是新能源汽车充电基础设施的核心,涉及设备管理、用户服务、支付结算、运营监控等多个业务领域。本文详细介绍充电桩业务系统的架构设计、核心功能实现、技术方案和实际应用。 + +## 业务架构 + +### 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 充电桩业务系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 用户端 │ │ 运营端 │ │ 监控端 │ │ 管理端 │ │ +│ │ (APP) │ │ (Web) │ │ (Dashboard)│ │ (Admin) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ API网关层 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 用户服务 │ │ 设备服务 │ │ 订单服务 │ │ 支付服务 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 运营服务 │ │ 监控服务 │ │ 消息服务 │ │ 数据服务 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ 数据层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ MySQL │ │ Redis │ │ MongoDB │ │ InfluxDB │ │ +│ │ (业务数据) │ │ (缓存) │ │ (日志数据) │ │ (时序数据) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ 设备层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 充电桩 │ │ 充电桩 │ │ 充电桩 │ │ ... │ │ +│ │ (站点A) │ │ (站点B) │ │ (站点C) │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 核心业务模块 + +1. **用户管理**:注册登录、实名认证、会员体系 +2. **设备管理**:充电桩注册、状态监控、远程控制 +3. **订单管理**:充电订单、预约订单、订单结算 +4. **支付管理**:多种支付方式、预付费、后付费 +5. **运营管理**:站点管理、价格策略、营收统计 +6. **监控告警**:设备监控、故障告警、性能分析 + +## 数据模型设计 + +### 核心实体关系 + +``` +-- 用户表 +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + phone VARCHAR(11) UNIQUE NOT NULL COMMENT '手机号', + nickname VARCHAR(50) COMMENT '昵称', + avatar VARCHAR(255) COMMENT '头像', + real_name VARCHAR(20) COMMENT '真实姓名', + id_card VARCHAR(18) COMMENT '身份证号', + status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用', + balance DECIMAL(10,2) DEFAULT 0 COMMENT '账户余额', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_phone (phone), + INDEX idx_status (status) +) ENGINE=InnoDB COMMENT='用户表'; + +-- 充电站表 +CREATE TABLE charging_stations ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + station_code VARCHAR(32) UNIQUE NOT NULL COMMENT '站点编码', + station_name VARCHAR(100) NOT NULL COMMENT '站点名称', + province VARCHAR(20) NOT NULL COMMENT '省份', + city VARCHAR(20) NOT NULL COMMENT '城市', + district VARCHAR(20) NOT NULL COMMENT '区县', + address VARCHAR(255) NOT NULL COMMENT '详细地址', + longitude DECIMAL(10,7) COMMENT '经度', + latitude DECIMAL(10,7) COMMENT '纬度', + operator_id BIGINT COMMENT '运营商ID', + status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-停用', + pile_count INT DEFAULT 0 COMMENT '充电桩数量', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_station_code (station_code), + INDEX idx_location (province, city, district), + INDEX idx_operator (operator_id) +) ENGINE=InnoDB COMMENT='充电站表'; + +-- 充电桩表 +CREATE TABLE charging_piles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + pile_code VARCHAR(32) UNIQUE NOT NULL COMMENT '充电桩编码', + pile_name VARCHAR(100) NOT NULL COMMENT '充电桩名称', + station_id BIGINT NOT NULL COMMENT '所属站点ID', + pile_type TINYINT NOT NULL COMMENT '桩类型:1-直流,2-交流', + connector_type VARCHAR(20) COMMENT '接口类型', + max_power DECIMAL(8,2) COMMENT '最大功率(kW)', + rated_voltage DECIMAL(8,2) COMMENT '额定电压(V)', + rated_current DECIMAL(8,2) COMMENT '额定电流(A)', + status TINYINT DEFAULT 1 COMMENT '状态:1-空闲,2-充电中,3-故障,4-离线', + online_status TINYINT DEFAULT 1 COMMENT '在线状态:1-在线,0-离线', + last_heartbeat TIMESTAMP COMMENT '最后心跳时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_pile_code (pile_code), + INDEX idx_station (station_id), + INDEX idx_status (status), + INDEX idx_online_status (online_status), + FOREIGN KEY (station_id) REFERENCES charging_stations(id) +) ENGINE=InnoDB COMMENT='充电桩表'; + +-- 充电订单表 +CREATE TABLE charging_orders ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号', + user_id BIGINT NOT NULL COMMENT '用户ID', + pile_id BIGINT NOT NULL COMMENT '充电桩ID', + connector_id TINYINT NOT NULL COMMENT '充电接口ID', + start_time TIMESTAMP COMMENT '开始充电时间', + end_time TIMESTAMP COMMENT '结束充电时间', + start_soc DECIMAL(5,2) COMMENT '开始SOC(%)', + end_soc DECIMAL(5,2) COMMENT '结束SOC(%)', + total_power DECIMAL(10,3) COMMENT '总充电量(kWh)', + total_time INT COMMENT '总充电时长(秒)', + unit_price DECIMAL(8,4) COMMENT '电价(元/kWh)', + service_fee DECIMAL(8,4) COMMENT '服务费(元/kWh)', + total_amount DECIMAL(10,2) COMMENT '总金额', + actual_amount DECIMAL(10,2) COMMENT '实际支付金额', + status TINYINT DEFAULT 1 COMMENT '状态:1-待支付,2-充电中,3-已完成,4-已取消', + payment_status TINYINT DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付,2-退款中,3-已退款', + stop_reason TINYINT COMMENT '停止原因:1-用户停止,2-充满停止,3-故障停止', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_order_no (order_no), + INDEX idx_user (user_id), + INDEX idx_pile (pile_id), + INDEX idx_status (status), + INDEX idx_payment_status (payment_status), + INDEX idx_created_at (created_at), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (pile_id) REFERENCES charging_piles(id) +) ENGINE=InnoDB COMMENT='充电订单表'; + +-- 支付记录表 +CREATE TABLE payment_records ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + payment_no VARCHAR(32) UNIQUE NOT NULL COMMENT '支付单号', + order_id BIGINT NOT NULL COMMENT '订单ID', + user_id BIGINT NOT NULL COMMENT '用户ID', + payment_method TINYINT NOT NULL COMMENT '支付方式:1-微信,2-支付宝,3-银联,4-余额', + amount DECIMAL(10,2) NOT NULL COMMENT '支付金额', + status TINYINT DEFAULT 0 COMMENT '状态:0-待支付,1-支付成功,2-支付失败,3-已退款', + third_party_no VARCHAR(64) COMMENT '第三方支付单号', + paid_at TIMESTAMP COMMENT '支付时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_payment_no (payment_no), + INDEX idx_order (order_id), + INDEX idx_user (user_id), + INDEX idx_status (status), + FOREIGN KEY (order_id) REFERENCES charging_orders(id), + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB COMMENT='支付记录表'; +``` + +## 核心业务实现 + +### 1. 用户服务实现 + +``` +@Service +@Transactional +public class UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private SmsService smsService; + + /** + * 用户注册 + */ + public UserDTO register(RegisterRequest request) { + // 1. 验证手机号格式 + if (!PhoneUtil.isValidPhone(request.getPhone())) { + throw new BusinessException("手机号格式不正确"); + } + + // 2. 验证短信验证码 + String cacheKey = "sms:register:" + request.getPhone(); + String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); + if (!request.getSmsCode().equals(cachedCode)) { + throw new BusinessException("验证码错误或已过期"); + } + + // 3. 检查手机号是否已注册 + if (userMapper.existsByPhone(request.getPhone())) { + throw new BusinessException("手机号已注册"); + } + + // 4. 创建用户 + User user = new User(); + user.setPhone(request.getPhone()); + user.setNickname("用户" + request.getPhone().substring(7)); + user.setStatus(UserStatus.NORMAL.getValue()); + user.setBalance(BigDecimal.ZERO); + + userMapper.insert(user); + + // 5. 删除验证码缓存 + redisTemplate.delete(cacheKey); + + return UserConverter.toDTO(user); + } + + /** + * 用户登录 + */ + public LoginResponse login(LoginRequest request) { + // 1. 验证用户 + User user = userMapper.findByPhone(request.getPhone()); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + if (user.getStatus() != UserStatus.NORMAL.getValue()) { + throw new BusinessException("用户已被禁用"); + } + + // 2. 验证短信验证码 + String cacheKey = "sms:login:" + request.getPhone(); + String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); + if (!request.getSmsCode().equals(cachedCode)) { + throw new BusinessException("验证码错误或已过期"); + } + + // 3. 生成JWT Token + String token = JwtUtil.generateToken(user.getId(), user.getPhone()); + + // 4. 缓存用户信息 + String userCacheKey = "user:" + user.getId(); + redisTemplate.opsForValue().set(userCacheKey, user, 7, TimeUnit.DAYS); + + // 5. 删除验证码缓存 + redisTemplate.delete(cacheKey); + + LoginResponse response = new LoginResponse(); + response.setToken(token); + response.setUser(UserConverter.toDTO(user)); + + return response; + } + + /** + * 实名认证 + */ + public void realNameAuth(Long userId, RealNameAuthRequest request) { + User user = getUserById(userId); + + // 1. 调用第三方实名认证接口 + boolean authResult = realNameAuthService.verify( + request.getRealName(), + request.getIdCard() + ); + + if (!authResult) { + throw new BusinessException("实名认证失败,请检查姓名和身份证号"); + } + + // 2. 更新用户信息 + user.setRealName(request.getRealName()); + user.setIdCard(request.getIdCard()); + user.setAuthStatus(AuthStatus.AUTHENTICATED.getValue()); + + userMapper.updateById(user); + + // 3. 更新缓存 + String userCacheKey = "user:" + userId; + redisTemplate.opsForValue().set(userCacheKey, user, 7, TimeUnit.DAYS); + } +} +``` + +### 2. 设备服务实现 + +``` +@Service +public class ChargingPileService { + + @Autowired + private ChargingPileMapper pileMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private MqttTemplate mqttTemplate; + + @Autowired + private InfluxDBTemplate influxDBTemplate; + + /** + * 获取附近充电桩 + */ + public List getNearbyPiles(NearbyRequest request) { + // 1. 基于地理位置查询 + List piles = pileMapper.findNearbyPiles( + request.getLongitude(), + request.getLatitude(), + request.getRadius() + ); + + // 2. 过滤可用充电桩 + List availablePiles = piles.stream() + .filter(pile -> pile.getStatus() == PileStatus.IDLE.getValue()) + .filter(pile -> pile.getOnlineStatus() == OnlineStatus.ONLINE.getValue()) + .collect(Collectors.toList()); + + // 3. 获取实时状态 + return availablePiles.stream() + .map(pile -> { + ChargingPileDTO dto = PileConverter.toDTO(pile); + // 从Redis获取实时状态 + String statusKey = "pile:status:" + pile.getId(); + PileRealTimeStatus status = (PileRealTimeStatus) + redisTemplate.opsForValue().get(statusKey); + if (status != null) { + dto.setRealTimeStatus(status); + } + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * 处理充电桩心跳 + */ + @EventListener + public void handleHeartbeat(PileHeartbeatEvent event) { + String pileCode = event.getPileCode(); + PileHeartbeatData data = event.getData(); + + // 1. 更新数据库心跳时间 + pileMapper.updateHeartbeat(pileCode, new Date()); + + // 2. 更新Redis实时状态 + String statusKey = "pile:status:" + pileCode; + PileRealTimeStatus status = new PileRealTimeStatus(); + status.setPileCode(pileCode); + status.setStatus(data.getStatus()); + status.setVoltage(data.getVoltage()); + status.setCurrent(data.getCurrent()); + status.setPower(data.getPower()); + status.setTemperature(data.getTemperature()); + status.setUpdateTime(new Date()); + + redisTemplate.opsForValue().set(statusKey, status, 5, TimeUnit.MINUTES); + + // 3. 存储时序数据 + Point point = Point.measurement("pile_metrics") + .tag("pile_code", pileCode) + .addField("voltage", data.getVoltage()) + .addField("current", data.getCurrent()) + .addField("power", data.getPower()) + .addField("temperature", data.getTemperature()) + .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + .build(); + + influxDBTemplate.write(point); + + // 4. 检查告警条件 + checkAlarmConditions(pileCode, data); + } + + /** + * 远程启动充电 + */ + public void startCharging(String pileCode, StartChargingRequest request) { + // 1. 验证充电桩状态 + ChargingPile pile = pileMapper.findByPileCode(pileCode); + if (pile == null) { + throw new BusinessException("充电桩不存在"); + } + + if (pile.getStatus() != PileStatus.IDLE.getValue()) { + throw new BusinessException("充电桩不可用"); + } + + // 2. 构造MQTT指令 + StartChargingCommand command = new StartChargingCommand(); + command.setPileCode(pileCode); + command.setConnectorId(request.getConnectorId()); + command.setOrderNo(request.getOrderNo()); + command.setMaxPower(request.getMaxPower()); + command.setMaxTime(request.getMaxTime()); + + // 3. 发送MQTT指令 + String topic = "pile/" + pileCode + "/command"; + mqttTemplate.convertAndSend(topic, command); + + // 4. 更新充电桩状态 + pile.setStatus(PileStatus.CHARGING.getValue()); + pileMapper.updateById(pile); + + // 5. 记录操作日志 + logOperationRecord(pileCode, "START_CHARGING", request.getOrderNo()); + } + + /** + * 远程停止充电 + */ + public void stopCharging(String pileCode, String orderNo) { + // 1. 构造停止指令 + StopChargingCommand command = new StopChargingCommand(); + command.setPileCode(pileCode); + command.setOrderNo(orderNo); + command.setStopReason(StopReason.USER_STOP.getValue()); + + // 2. 发送MQTT指令 + String topic = "pile/" + pileCode + "/command"; + mqttTemplate.convertAndSend(topic, command); + + // 3. 记录操作日志 + logOperationRecord(pileCode, "STOP_CHARGING", orderNo); + } +} +``` + +### 3. 订单服务实现 + +``` +@Service +@Transactional +public class ChargingOrderService { + + @Autowired + private ChargingOrderMapper orderMapper; + + @Autowired + private ChargingPileService pileService; + + @Autowired + private PaymentService paymentService; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 创建充电订单 + */ + public ChargingOrderDTO createOrder(CreateOrderRequest request) { + // 1. 验证用户和充电桩 + User user = userService.getUserById(request.getUserId()); + ChargingPile pile = pileService.getPileById(request.getPileId()); + + if (pile.getStatus() != PileStatus.IDLE.getValue()) { + throw new BusinessException("充电桩不可用"); + } + + // 2. 获取价格策略 + PriceStrategy priceStrategy = priceService.getCurrentPrice( + pile.getStationId(), + new Date() + ); + + // 3. 创建订单 + ChargingOrder order = new ChargingOrder(); + order.setOrderNo(OrderNoGenerator.generate()); + order.setUserId(request.getUserId()); + order.setPileId(request.getPileId()); + order.setConnectorId(request.getConnectorId()); + order.setUnitPrice(priceStrategy.getUnitPrice()); + order.setServiceFee(priceStrategy.getServiceFee()); + order.setStatus(OrderStatus.PENDING_PAYMENT.getValue()); + order.setPaymentStatus(PaymentStatus.UNPAID.getValue()); + + orderMapper.insert(order); + + // 4. 锁定充电桩(设置超时时间) + String lockKey = "pile:lock:" + request.getPileId(); + redisTemplate.opsForValue().set(lockKey, order.getOrderNo(), 15, TimeUnit.MINUTES); + + return OrderConverter.toDTO(order); + } + + /** + * 开始充电 + */ + public void startCharging(String orderNo) { + // 1. 获取订单信息 + ChargingOrder order = orderMapper.findByOrderNo(orderNo); + if (order == null) { + throw new BusinessException("订单不存在"); + } + + if (order.getPaymentStatus() != PaymentStatus.PAID.getValue()) { + throw new BusinessException("订单未支付"); + } + + // 2. 启动充电桩 + StartChargingRequest request = new StartChargingRequest(); + request.setOrderNo(orderNo); + request.setConnectorId(order.getConnectorId()); + request.setMaxPower(100); // 默认最大功率 + request.setMaxTime(7200); // 默认最大时长2小时 + + pileService.startCharging(order.getPile().getPileCode(), request); + + // 3. 更新订单状态 + order.setStatus(OrderStatus.CHARGING.getValue()); + order.setStartTime(new Date()); + orderMapper.updateById(order); + + // 4. 发送开始充电消息 + ChargingStartedEvent event = new ChargingStartedEvent(); + event.setOrderNo(orderNo); + event.setUserId(order.getUserId()); + event.setPileCode(order.getPile().getPileCode()); + + eventPublisher.publishEvent(event); + } + + /** + * 结束充电 + */ + public void finishCharging(FinishChargingRequest request) { + // 1. 获取订单 + ChargingOrder order = orderMapper.findByOrderNo(request.getOrderNo()); + if (order == null) { + throw new BusinessException("订单不存在"); + } + + // 2. 计算充电费用 + BigDecimal totalPower = request.getTotalPower(); + BigDecimal electricityFee = totalPower.multiply(order.getUnitPrice()); + BigDecimal serviceFee = totalPower.multiply(order.getServiceFee()); + BigDecimal totalAmount = electricityFee.add(serviceFee); + + // 3. 更新订单信息 + order.setEndTime(new Date()); + order.setTotalPower(totalPower); + order.setTotalTime(request.getTotalTime()); + order.setStartSoc(request.getStartSoc()); + order.setEndSoc(request.getEndSoc()); + order.setTotalAmount(totalAmount); + order.setActualAmount(totalAmount); + order.setStatus(OrderStatus.COMPLETED.getValue()); + order.setStopReason(request.getStopReason()); + + orderMapper.updateById(order); + + // 4. 处理后付费 + if (order.getPaymentStatus() == PaymentStatus.UNPAID.getValue()) { + // 创建支付订单 + paymentService.createPayment(order.getId(), totalAmount); + } else { + // 预付费,处理多退少补 + handlePrePaymentSettlement(order); + } + + // 5. 释放充电桩 + pileService.releasePile(order.getPileId()); + + // 6. 发送充电完成消息 + ChargingCompletedEvent event = new ChargingCompletedEvent(); + event.setOrderNo(request.getOrderNo()); + event.setUserId(order.getUserId()); + event.setTotalAmount(totalAmount); + + eventPublisher.publishEvent(event); + } + + /** + * 处理预付费结算 + */ + private void handlePrePaymentSettlement(ChargingOrder order) { + PaymentRecord payment = paymentService.getByOrderId(order.getId()); + BigDecimal paidAmount = payment.getAmount(); + BigDecimal actualAmount = order.getActualAmount(); + + if (paidAmount.compareTo(actualAmount) > 0) { + // 退款 + BigDecimal refundAmount = paidAmount.subtract(actualAmount); + paymentService.refund(payment.getId(), refundAmount); + } else if (paidAmount.compareTo(actualAmount) < 0) { + // 补款 + BigDecimal additionalAmount = actualAmount.subtract(paidAmount); + paymentService.createAdditionalPayment(order.getId(), additionalAmount); + } + } +} +``` + +### 4. 支付服务实现 + +``` +@Service +public class PaymentService { + + @Autowired + private PaymentRecordMapper paymentMapper; + + @Autowired + private WechatPayService wechatPayService; + + @Autowired + private AlipayService alipayService; + + @Autowired + private UserService userService; + + /** + * 创建支付订单 + */ + public PaymentDTO createPayment(CreatePaymentRequest request) { + // 1. 创建支付记录 + PaymentRecord payment = new PaymentRecord(); + payment.setPaymentNo(PaymentNoGenerator.generate()); + payment.setOrderId(request.getOrderId()); + payment.setUserId(request.getUserId()); + payment.setPaymentMethod(request.getPaymentMethod()); + payment.setAmount(request.getAmount()); + payment.setStatus(PaymentStatus.PENDING.getValue()); + + paymentMapper.insert(payment); + + // 2. 调用第三方支付 + String payUrl = null; + switch (PaymentMethod.valueOf(request.getPaymentMethod())) { + case WECHAT: + payUrl = wechatPayService.createOrder(payment); + break; + case ALIPAY: + payUrl = alipayService.createOrder(payment); + break; + case BALANCE: + return processBalancePayment(payment); + default: + throw new BusinessException("不支持的支付方式"); + } + + PaymentDTO dto = PaymentConverter.toDTO(payment); + dto.setPayUrl(payUrl); + + return dto; + } + + /** + * 余额支付 + */ + private PaymentDTO processBalancePayment(PaymentRecord payment) { + User user = userService.getUserById(payment.getUserId()); + + // 1. 检查余额 + if (user.getBalance().compareTo(payment.getAmount()) < 0) { + throw new BusinessException("余额不足"); + } + + // 2. 扣减余额 + userService.deductBalance(user.getId(), payment.getAmount()); + + // 3. 更新支付状态 + payment.setStatus(PaymentStatus.SUCCESS.getValue()); + payment.setPaidAt(new Date()); + paymentMapper.updateById(payment); + + // 4. 发送支付成功事件 + PaymentSuccessEvent event = new PaymentSuccessEvent(); + event.setPaymentId(payment.getId()); + event.setOrderId(payment.getOrderId()); + event.setAmount(payment.getAmount()); + + eventPublisher.publishEvent(event); + + return PaymentConverter.toDTO(payment); + } + + /** + * 处理支付回调 + */ + public void handlePaymentCallback(PaymentCallbackRequest request) { + // 1. 验证签名 + if (!verifySignature(request)) { + throw new BusinessException("签名验证失败"); + } + + // 2. 获取支付记录 + PaymentRecord payment = paymentMapper.findByPaymentNo(request.getPaymentNo()); + if (payment == null) { + throw new BusinessException("支付记录不存在"); + } + + // 3. 防重复处理 + if (payment.getStatus() == PaymentStatus.SUCCESS.getValue()) { + return; + } + + // 4. 更新支付状态 + payment.setStatus(PaymentStatus.SUCCESS.getValue()); + payment.setThirdPartyNo(request.getThirdPartyNo()); + payment.setPaidAt(new Date()); + paymentMapper.updateById(payment); + + // 5. 发送支付成功事件 + PaymentSuccessEvent event = new PaymentSuccessEvent(); + event.setPaymentId(payment.getId()); + event.setOrderId(payment.getOrderId()); + event.setAmount(payment.getAmount()); + + eventPublisher.publishEvent(event); + } + + /** + * 退款处理 + */ + public void refund(Long paymentId, BigDecimal refundAmount) { + PaymentRecord payment = paymentMapper.selectById(paymentId); + if (payment == null) { + throw new BusinessException("支付记录不存在"); + } + + // 1. 调用第三方退款 + boolean refundResult = false; + switch (PaymentMethod.valueOf(payment.getPaymentMethod())) { + case WECHAT: + refundResult = wechatPayService.refund(payment, refundAmount); + break; + case ALIPAY: + refundResult = alipayService.refund(payment, refundAmount); + break; + case BALANCE: + // 余额退款直接加回用户余额 + userService.addBalance(payment.getUserId(), refundAmount); + refundResult = true; + break; + } + + if (refundResult) { + // 2. 更新支付状态 + payment.setStatus(PaymentStatus.REFUNDED.getValue()); + paymentMapper.updateById(payment); + + // 3. 发送退款成功事件 + RefundSuccessEvent event = new RefundSuccessEvent(); + event.setPaymentId(paymentId); + event.setRefundAmount(refundAmount); + + eventPublisher.publishEvent(event); + } + } +} +``` + +## 设备通信协议 + +### MQTT通信架构 + +``` +@Component +public class MqttMessageHandler { + + @Autowired + private ChargingPileService pileService; + + @Autowired + private ChargingOrderService orderService; + + /** + * 处理充电桩心跳消息 + */ + @MqttMessageListener(topic = "pile/+/heartbeat") + public void handleHeartbeat(@Payload String message, @Header String topic) { + try { + // 解析topic获取充电桩编码 + String pileCode = extractPileCodeFromTopic(topic); + + // 解析心跳数据 + PileHeartbeatData data = JSON.parseObject(message, PileHeartbeatData.class); + + // 发布心跳事件 + PileHeartbeatEvent event = new PileHeartbeatEvent(); + event.setPileCode(pileCode); + event.setData(data); + + eventPublisher.publishEvent(event); + + } catch (Exception e) { + log.error("处理心跳消息失败: {}", message, e); + } + } + + /** + * 处理充电状态上报 + */ + @MqttMessageListener(topic = "pile/+/charging/status") + public void handleChargingStatus(@Payload String message, @Header String topic) { + try { + String pileCode = extractPileCodeFromTopic(topic); + ChargingStatusData data = JSON.parseObject(message, ChargingStatusData.class); + + // 更新订单充电状态 + orderService.updateChargingStatus(data.getOrderNo(), data); + + } catch (Exception e) { + log.error("处理充电状态失败: {}", message, e); + } + } + + /** + * 处理充电完成消息 + */ + @MqttMessageListener(topic = "pile/+/charging/finished") + public void handleChargingFinished(@Payload String message, @Header String topic) { + try { + String pileCode = extractPileCodeFromTopic(topic); + ChargingFinishedData data = JSON.parseObject(message, ChargingFinishedData.class); + + // 结束充电订单 + FinishChargingRequest request = new FinishChargingRequest(); + request.setOrderNo(data.getOrderNo()); + request.setTotalPower(data.getTotalPower()); + request.setTotalTime(data.getTotalTime()); + request.setStartSoc(data.getStartSoc()); + request.setEndSoc(data.getEndSoc()); + request.setStopReason(data.getStopReason()); + + orderService.finishCharging(request); + + } catch (Exception e) { + log.error("处理充电完成消息失败: {}", message, e); + } + } +} +``` + +### 协议数据结构 + +``` +// 心跳数据 +@Data +public class PileHeartbeatData { + private String pileCode; // 充电桩编码 + private Integer status; // 状态 + private BigDecimal voltage; // 电压 + private BigDecimal current; // 电流 + private BigDecimal power; // 功率 + private BigDecimal temperature; // 温度 + private Date timestamp; // 时间戳 +} + +// 充电状态数据 +@Data +public class ChargingStatusData { + private String orderNo; // 订单号 + private BigDecimal currentPower; // 当前功率 + private BigDecimal totalPower; // 累计电量 + private Integer totalTime; // 累计时间 + private BigDecimal soc; // 当前SOC + private Date timestamp; // 时间戳 +} + +// 充电完成数据 +@Data +public class ChargingFinishedData { + private String orderNo; // 订单号 + private BigDecimal totalPower; // 总电量 + private Integer totalTime; // 总时间 + private BigDecimal startSoc; // 开始SOC + private BigDecimal endSoc; // 结束SOC + private Integer stopReason; // 停止原因 + private Date timestamp; // 时间戳 +} + +// 控制指令 +@Data +public class StartChargingCommand { + private String pileCode; // 充电桩编码 + private Integer connectorId; // 接口ID + private String orderNo; // 订单号 + private BigDecimal maxPower; // 最大功率 + private Integer maxTime; // 最大时间 +} + +@Data +public class StopChargingCommand { + private String pileCode; // 充电桩编码 + private String orderNo; // 订单号 + private Integer stopReason; // 停止原因 +} +``` + +## 监控与告警 + +### 实时监控实现 + +``` +@Service +public class MonitoringService { + + @Autowired + private InfluxDBTemplate influxDBTemplate; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private AlarmService alarmService; + + /** + * 获取充电桩实时数据 + */ + public PileRealTimeData getRealTimeData(String pileCode) { + // 1. 从Redis获取最新状态 + String statusKey = "pile:status:" + pileCode; + PileRealTimeStatus status = (PileRealTimeStatus) + redisTemplate.opsForValue().get(statusKey); + + if (status == null) { + throw new BusinessException("充电桩离线或无数据"); + } + + // 2. 从InfluxDB获取历史趋势 + String query = String.format( + "SELECT mean(voltage), mean(current), mean(power), mean(temperature) " + + "FROM pile_metrics WHERE pile_code='%s' AND time > now() - 1h " + + "GROUP BY time(5m)", + pileCode + ); + + QueryResult result = influxDBTemplate.query(query); + List trendData = parseTrendData(result); + + PileRealTimeData data = new PileRealTimeData(); + data.setCurrentStatus(status); + data.setTrendData(trendData); + + return data; + } + + /** + * 检查告警条件 + */ + public void checkAlarmConditions(String pileCode, PileHeartbeatData data) { + // 1. 温度告警 + if (data.getTemperature().compareTo(new BigDecimal("80")) > 0) { + AlarmEvent alarm = new AlarmEvent(); + alarm.setPileCode(pileCode); + alarm.setAlarmType(AlarmType.HIGH_TEMPERATURE); + alarm.setAlarmLevel(AlarmLevel.HIGH); + alarm.setMessage("充电桩温度过高: " + data.getTemperature() + "°C"); + + alarmService.triggerAlarm(alarm); + } + + // 2. 电压异常告警 + if (data.getVoltage().compareTo(new BigDecimal("200")) < 0 || + data.getVoltage().compareTo(new BigDecimal("250")) > 0) { + AlarmEvent alarm = new AlarmEvent(); + alarm.setPileCode(pileCode); + alarm.setAlarmType(AlarmType.VOLTAGE_ABNORMAL); + alarm.setAlarmLevel(AlarmLevel.MEDIUM); + alarm.setMessage("充电桩电压异常: " + data.getVoltage() + "V"); + + alarmService.triggerAlarm(alarm); + } + + // 3. 离线告警 + String lastHeartbeatKey = "pile:last_heartbeat:" + pileCode; + redisTemplate.opsForValue().set(lastHeartbeatKey, System.currentTimeMillis()); + } + + /** + * 检查离线设备 + */ + @Scheduled(fixedRate = 60000) // 每分钟检查一次 + public void checkOfflineDevices() { + List onlinePiles = pileMapper.findOnlinePiles(); + + for (ChargingPile pile : onlinePiles) { + String lastHeartbeatKey = "pile:last_heartbeat:" + pile.getPileCode(); + Long lastHeartbeat = (Long) redisTemplate.opsForValue().get(lastHeartbeatKey); + + if (lastHeartbeat == null || + System.currentTimeMillis() - lastHeartbeat > 300000) { // 5分钟无心跳 + + // 更新设备离线状态 + pile.setOnlineStatus(OnlineStatus.OFFLINE.getValue()); + pileMapper.updateById(pile); + + // 发送离线告警 + AlarmEvent alarm = new AlarmEvent(); + alarm.setPileCode(pile.getPileCode()); + alarm.setAlarmType(AlarmType.DEVICE_OFFLINE); + alarm.setAlarmLevel(AlarmLevel.HIGH); + alarm.setMessage("充电桩离线"); + + alarmService.triggerAlarm(alarm); + } + } + } +} +``` + +### 数据统计分析 + +``` +@Service +public class StatisticsService { + + @Autowired + private ChargingOrderMapper orderMapper; + + @Autowired + private InfluxDBTemplate influxDBTemplate; + + /** + * 获取运营统计数据 + */ + public OperationStatistics getOperationStatistics(StatisticsRequest request) { + Date startDate = request.getStartDate(); + Date endDate = request.getEndDate(); + + // 1. 订单统计 + OrderStatistics orderStats = orderMapper.getOrderStatistics(startDate, endDate); + + // 2. 收入统计 + RevenueStatistics revenueStats = orderMapper.getRevenueStatistics(startDate, endDate); + + // 3. 设备利用率统计 + DeviceUtilizationStatistics deviceStats = calculateDeviceUtilization(startDate, endDate); + + // 4. 用户统计 + UserStatistics userStats = orderMapper.getUserStatistics(startDate, endDate); + + OperationStatistics statistics = new OperationStatistics(); + statistics.setOrderStatistics(orderStats); + statistics.setRevenueStatistics(revenueStats); + statistics.setDeviceStatistics(deviceStats); + statistics.setUserStatistics(userStats); + + return statistics; + } + + /** + * 计算设备利用率 + */ + private DeviceUtilizationStatistics calculateDeviceUtilization(Date startDate, Date endDate) { + // 从InfluxDB查询设备使用时间 + String query = String.format( + "SELECT sum(charging_time) as total_time, pile_code " + + "FROM charging_sessions " + + "WHERE time >= '%s' AND time <= '%s' " + + "GROUP BY pile_code", + startDate.toInstant(), + endDate.toInstant() + ); + + QueryResult result = influxDBTemplate.query(query); + + // 计算利用率 + long totalPeriod = endDate.getTime() - startDate.getTime(); + Map utilizationMap = new HashMap<>(); + + // 解析查询结果并计算利用率 + // ... + + DeviceUtilizationStatistics stats = new DeviceUtilizationStatistics(); + stats.setUtilizationMap(utilizationMap); + stats.setAverageUtilization(calculateAverageUtilization(utilizationMap)); + + return stats; + } +} +``` + +## 部署架构 + +### Docker容器化部署 + +``` +# Dockerfile +FROM openjdk:11-jre-slim + +VOLUME /tmp + +COPY target/charging-pile-service.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +``` +# docker-compose.yml +version: '3.8' + +services: + # 应用服务 + charging-pile-service: + build: . + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=prod + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - MQTT_BROKER=mqtt + depends_on: + - mysql + - redis + - mqtt + networks: + - charging-network + + # MySQL数据库 + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: charging_pile + volumes: + - mysql_data:/var/lib/mysql + - ./sql:/docker-entrypoint-initdb.d + ports: + - "3306:3306" + networks: + - charging-network + + # Redis缓存 + redis: + image: redis:6.2 + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - charging-network + + # MQTT消息代理 + mqtt: + image: eclipse-mosquitto:2.0 + ports: + - "1883:1883" + - "9001:9001" + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf + networks: + - charging-network + + # InfluxDB时序数据库 + influxdb: + image: influxdb:1.8 + environment: + INFLUXDB_DB: charging_metrics + INFLUXDB_ADMIN_USER: admin + INFLUXDB_ADMIN_PASSWORD: admin123 + ports: + - "8086:8086" + volumes: + - influxdb_data:/var/lib/influxdb + networks: + - charging-network + + # Grafana监控面板 + grafana: + image: grafana/grafana:8.0.0 + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_PASSWORD: admin123 + volumes: + - grafana_data:/var/lib/grafana + networks: + - charging-network + +volumes: + mysql_data: + redis_data: + influxdb_data: + grafana_data: + +networks: + charging-network: + driver: bridge +``` + +### Kubernetes部署 + +``` +# k8s-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: charging-pile-service + labels: + app: charging-pile-service +spec: + replicas: 3 + selector: + matchLabels: + app: charging-pile-service + template: + metadata: + labels: + app: charging-pile-service + spec: + containers: + - name: charging-pile-service + image: charging-pile-service:latest + ports: + - containerPort: 8080 + env: + - name: SPRING_PROFILES_ACTIVE + value: "k8s" + - name: MYSQL_HOST + value: "mysql-service" + - name: REDIS_HOST + value: "redis-service" + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + +--- +apiVersion: v1 +kind: Service +metadata: + name: charging-pile-service +spec: + selector: + app: charging-pile-service + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer +``` + +## 总结 + +充电桩业务系统是一个复杂的物联网应用,涉及多个技术领域: + +1. **业务架构**:微服务架构、领域驱动设计 +2. **数据存储**:MySQL、Redis、MongoDB、InfluxDB +3. **消息通信**:MQTT、消息队列、事件驱动 +4. **支付集成**:多种支付方式、预付费/后付费 +5. **设备管理**:实时监控、远程控制、故障告警 +6. **数据分析**:运营统计、设备利用率、用户行为 +7. **部署运维**:容器化、微服务、监控告警 + +关键技术要点: +- **高并发处理**:Redis缓存、数据库优化、负载均衡 +- **实时性要求**:MQTT通信、WebSocket推送、时序数据库 +- **数据一致性**:分布式事务、最终一致性、补偿机制 +- **系统可靠性**:熔断降级、重试机制、故障转移 +- **安全性**:数据加密、访问控制、支付安全 + +充电桩业务系统的成功实施需要综合考虑业务需求、技术架构、运营模式等多个方面,是物联网、移动支付、大数据等技术的综合应用。 \ No newline at end of file diff --git "a/docs/aJava/\345\235\227\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\235\227\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..f3d84436c --- /dev/null +++ "b/docs/aJava/\345\235\227\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,13 @@ +# 块存储是什么 + +想象一下,硬盘里存储一堆乐高积木,块存储就是把数据切成固定大小的块,比如4KB的小方块,每个数据块都有专属身份证--LBA逻辑块地址 + +就像乐高积木按照编号存放仓库里,当你需要读取文件时,存储控制器会通过SCSI或NVMe协议,像快递小哥一样精准找到对应区块。 + +为什么企业愿意花大价格买块存储? + +1. 裸金属性能,直接访问物理磁盘,比文件存储少了两层协议开销; +2. 支持随机读写,数据库事务日志狂飙时,SSD+Raid5阵列能扛住每秒数万IOPS +3. 卷管理,黑科技,LUN逻辑单元让你像玩橡皮泥一样在线扩容 + +Oracle数据库靠San存储扛住百万并发;wmware集群通过iSCSI协议动态分配存储资源 diff --git "a/docs/aJava/\345\255\220\347\275\221\346\216\251\347\240\201\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\255\220\347\275\221\346\216\251\347\240\201\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..451993077 --- /dev/null +++ "b/docs/aJava/\345\255\220\347\275\221\346\216\251\347\240\201\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,148 @@ +# 子网掩码是什么 + +子网掩码(Subnet Mask)是一个32位的二进制数,用于将IP地址分割成网络部分和主机部分,是网络通信中的重要概念。它就像一个"筛子",帮助路由器和计算机识别哪些IP地址属于同一个网络。 + +## 基本概念 + +子网掩码的作用就是告诉计算机:"这个IP地址的前面几位是网络地址,后面几位是主机地址"。通过与IP地址进行按位与运算,可以快速确定网络地址。 + +例如: +- IP地址:192.168.1.100 +- 子网掩码:255.255.255.0 +- 网络地址:192.168.1.0 +- 主机地址:100 + +## 常见的子网掩码格式 + +### 1. 点分十进制表示法 +``` +255.255.255.0 # /24网络 +255.255.0.0 # /16网络 +255.0.0.0 # /8网络 +255.255.255.128 # /25网络 +``` + +### 2. CIDR表示法 +``` +/24 # 表示前24位是网络位 +/16 # 表示前16位是网络位 +/8 # 表示前8位是网络位 +/25 # 表示前25位是网络位 +``` + +## 子网掩码的工作原理 + +### 按位与运算 +当计算机需要判断两个IP是否在同一网络时,会进行以下操作: + +``` +IP地址1: 192.168.1.100 → 11000000.10101000.00000001.01100100 +子网掩码: 255.255.255.0 → 11111111.11111111.11111111.00000000 +按位与: → 11000000.10101000.00000001.00000000 +结果: 192.168.1.0 + +IP地址2: 192.168.1.200 → 11000000.10101000.00000001.11001000 +子网掩码: 255.255.255.0 → 11111111.11111111.11111111.00000000 +按位与: → 11000000.10101000.00000001.00000000 +结果: 192.168.1.0 +``` + +由于两个结果相同,说明这两个IP在同一个网络中。 + +## 子网划分实例 + +### 场景:公司网络规划 +假设公司分配到了192.168.1.0/24网络,需要划分给不同部门: + +``` +技术部: 192.168.1.0/26 (192.168.1.1-192.168.1.62) 62台主机 +销售部: 192.168.1.64/26 (192.168.1.65-192.168.1.126) 62台主机 +财务部: 192.168.1.128/26 (192.168.1.129-192.168.1.190) 62台主机 +管理层: 192.168.1.192/26 (192.168.1.193-192.168.1.254) 62台主机 +``` + +### 子网掩码计算技巧 + +**快速计算可用主机数:** +- /24网络:2^(32-24) - 2 = 254台主机 +- /25网络:2^(32-25) - 2 = 126台主机 +- /26网络:2^(32-26) - 2 = 62台主机 +- /27网络:2^(32-27) - 2 = 30台主机 + +*注:减2是因为网络地址和广播地址不能分配给主机* + +## 实际应用场景 + +### 1. 企业网络规划 +``` +总部网络: 10.0.0.0/16 (65534台主机) +分公司A: 10.1.0.0/24 (254台主机) +分公司B: 10.2.0.0/24 (254台主机) +VPN用户: 10.100.0.0/24 (254台主机) +``` + +### 2. 家庭路由器设置 +``` +默认网关: 192.168.1.1 +子网掩码: 255.255.255.0 +DHCP范围: 192.168.1.100-192.168.1.200 +``` + +### 3. 云服务器网络 +``` +VPC网络: 172.16.0.0/16 +公网子网: 172.16.1.0/24 +私网子网: 172.16.2.0/24 +数据库子网: 172.16.3.0/24 +``` + +## 常见问题与解决方案 + +### 问题1:网络不通 +**现象:** 同一局域网内的设备无法互相访问 +**排查:** 检查IP地址和子网掩码是否匹配 +```bash +# Linux/Mac查看网络配置 +ifconfig + +# Windows查看网络配置 +ipconfig +``` + +### 问题2:IP地址冲突 +**现象:** 设备获取不到IP或网络异常 +**解决:** 合理规划IP地址段,避免重叠 + +### 问题3:子网划分不当 +**现象:** 主机数量不够或浪费严重 +**解决:** 根据实际需求重新计算子网大小 + +## 最佳实践建议 + +### 1. 网络规划原则 +- **预留空间:** 为未来扩展预留足够的IP地址 +- **层次化设计:** 采用分层的网络架构 +- **安全隔离:** 不同部门使用不同子网 + +### 2. 子网掩码选择 +- **小型办公室:** 使用/24网络(254台主机) +- **中型企业:** 使用/16网络,再细分子网 +- **大型企业:** 使用/8网络,多级子网划分 + +### 3. 监控与维护 +```bash +# 查看路由表 +route -n + +# 测试网络连通性 +ping 192.168.1.1 + +# 查看ARP表 +arp -a +``` + +## 总结 + +子网掩码是网络通信的基础,正确理解和使用子网掩码对于网络管理至关重要。通过合理的子网划分,可以提高网络效率、增强安全性,并便于网络管理。 + +在实际工作中,掌握子网掩码的计算方法和应用场景,能够帮助我们更好地进行网络规划和故障排除。无论是企业网络管理还是个人网络配置,子网掩码都是不可或缺的重要概念。 \ No newline at end of file diff --git "a/docs/aJava/\345\255\230\345\202\250\345\277\253\347\205\247\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\255\230\345\202\250\345\277\253\347\205\247\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..fc630d93e --- /dev/null +++ "b/docs/aJava/\345\255\230\345\202\250\345\277\253\347\205\247\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,92 @@ +--- +title: 存储快照是什么 +date: 2023-01-01 +--- + +# 存储快照是什么 + +## 概述 + +存储快照(Storage Snapshot)是指在特定时间点对存储系统或卷的数据状态进行的完整拷贝或记录。它类似于数据的即时照片,捕获了特定时刻的数据状态,而不会中断应用程序的运行或阻塞数据访问。 + +## 工作原理 + +存储快照通常采用写时复制(Copy-on-Write)或重定向写入(Redirect-on-Write)技术实现: + +1. **写时复制(Copy-on-Write)**: + - 创建快照时,系统只记录元数据,不复制实际数据 + - 当原始数据需要修改时,系统先将原始数据复制到快照存储区域,然后再修改原始数据 + - 这确保了快照保留了创建时刻的数据状态 + +2. **重定向写入(Redirect-on-Write)**: + - 创建快照后,对原始数据的任何修改都会写入新的位置 + - 原始数据块保持不变,成为快照的一部分 + - 系统维护映射表,跟踪数据块的新旧位置 + +## 存储快照的主要用途 + +1. **数据备份与恢复**: + - 提供几乎无中断的备份解决方案 + - 允许快速恢复到之前的数据状态 + - 减少备份窗口时间 + +2. **灾难恢复**: + - 作为灾难恢复策略的重要组成部分 + - 可以快速恢复关键系统和数据 + +3. **开发与测试**: + - 为开发和测试环境提供生产数据的副本 + - 不影响生产环境的性能和可用性 + +4. **数据迁移**: + - 在系统迁移或升级过程中保护数据 + - 提供回滚选项 + +5. **虚拟机管理**: + - 虚拟化环境中创建虚拟机的快照 + - 便于虚拟机的备份、恢复和克隆 + +## 存储快照的类型 + +1. **完全快照**:捕获整个存储卷的完整副本 +2. **增量快照**:只记录自上次快照以来发生变化的数据块 +3. **差异快照**:记录自初始基准快照以来发生变化的所有数据块 + +## 存储快照的优势 + +1. **速度快**:创建快照通常只需几秒钟,不需要复制所有数据 +2. **空间效率**:通常只存储变化的数据块,节省存储空间 +3. **最小化停机时间**:不中断应用程序运行 +4. **数据一致性**:提供特定时间点的一致性视图 +5. **简化恢复过程**:允许快速恢复到之前的数据状态 + +## 存储快照的局限性 + +1. **性能影响**:在某些实现中,可能会对系统性能产生轻微影响 +2. **存储开销**:随着原始数据变化,快照可能会占用更多存储空间 +3. **不是完整备份**:快照通常与原始存储位于同一系统上,不能替代异地备份 +4. **管理复杂性**:需要适当的管理策略来处理快照生命周期 + +## 云环境中的存储快照 + +各大云服务提供商都提供了存储快照服务: + +1. **AWS**:Amazon EBS Snapshots +2. **Azure**:Azure Disk Snapshots +3. **Google Cloud**:Persistent Disk Snapshots +4. **阿里云**:云盘快照 +5. **腾讯云**:云硬盘快照 + +这些服务允许用户轻松创建、管理和恢复云存储资源的快照,为云上工作负载提供数据保护。 + +## 最佳实践 + +1. **制定快照策略**:根据业务需求确定快照频率和保留期限 +2. **自动化快照创建**:使用调度工具自动创建和管理快照 +3. **监控快照存储使用情况**:定期检查快照占用的存储空间 +4. **测试恢复过程**:定期测试从快照恢复数据的过程 +5. **结合其他备份方法**:将快照与传统备份方法结合使用,实现全面的数据保护 + +## 总结 + +存储快照是现代数据保护和管理策略中的重要工具,它提供了一种高效、低中断的方式来捕获数据状态,支持备份、恢复、开发测试等多种场景。通过了解存储快照的工作原理、类型和最佳实践,组织可以更有效地利用这一技术来保护和管理其数据资产。 \ No newline at end of file diff --git "a/docs/aJava/\345\256\211\345\205\250\347\273\204\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\256\211\345\205\250\347\273\204\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..f06c1f97b --- /dev/null +++ "b/docs/aJava/\345\256\211\345\205\250\347\273\204\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,9 @@ +# 安全组是什么 + +安全组本质上是个虚拟防火墙,用访问控制列表 ACL技术精准控制流量进出,它通过五元组规则--源IP、源端口、目的IP、目的端口、协议类型来控制流量。 + +像快递分拣系统一样,只放行盖着正确邮戳的数据包,状态检测,记住你主动发起的连接,像智能门卫自动给返回流量开绿灯。 + +这三种情况必须配置安全组,第一Web服务器只开放80和443端口,把SSH端口锁进保险柜,第二数据库实例限制内网访问,给数据戴上防毒面具,只开放3306端口,第三堡垒机设置严格源IP白名单,Redis只开放6379端口。 + +口诀:入站严出站宽,最小权限保平安。 diff --git "a/docs/aJava/\345\257\271\350\261\241\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\257\271\350\261\241\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..c186963f8 --- /dev/null +++ "b/docs/aJava/\345\257\271\350\261\241\345\255\230\345\202\250\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,14 @@ +# 对象存储是什么 + +超级仓库,传统存储文件像整理抽屉,分门别类建立文件夹。但对象存储直接给每个数据,贴个身份证-叫全局唯一标识符。照片,视频,代码包,全被打包成一个个对象仍进这个仓库里。不用管位置,只用记住名字。 + +1. 元数据自定义,给你的数据挂上智能标签。 +2. 分布式架构,数据被拆分碎片,存在几千台服务器上,坏几台都不丢 +3. 纠删码技术,把文件切分成乐高块,多存几块多余,恢复数据比Raid还稳 + +用: + +1. 海量非结构化数据,比如短视频平台的10亿条视频,用对象存储成本直降50%; +2. 跨地域备份,AW3的S3阿某云OSS都是对象存储,传文件像发快递一样简单 +3. AI训练集存储,百万张图片秒级调用 +4. CDN缓存,把热门视频缓存到离用户最近的服务器,秒开 diff --git "a/docs/aJava/\345\276\256\346\234\215\345\212\241\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\345\276\256\346\234\215\345\212\241\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..264cecc19 --- /dev/null +++ "b/docs/aJava/\345\276\256\346\234\215\345\212\241\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,16 @@ +# 微服务是什么 + +每个模块独立开发,独立部署,分布式架构的核心,解耦 + +API网关,服务发现,容器化 + +所有请求先经过网关这个“快递分拣站”,自动分配到对应的模块,服务发现机制像BPS,实时定位哪些服务器活着 + +再用Docker和Kubernetes进行容器化部署,每个模块独立部署,独立运行,独立扩展 + +随时扩容缩容 + +必须用微服务:高并发;多团队协作,支付配送模式由不同团队开发;快速试错,新功能单独上线;组织架构革命。 + +什么是微服务,分而治之 + diff --git "a/docs/aJava/\346\225\260\346\215\256\345\272\223\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/aJava/\346\225\260\346\215\256\345\272\223\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" new file mode 100644 index 000000000..0d38a010f --- /dev/null +++ "b/docs/aJava/\346\225\260\346\215\256\345\272\223\345\210\206\347\211\207\346\212\200\346\234\257\345\256\236\347\216\260.md" @@ -0,0 +1,694 @@ +# 数据库分片技术实现 + +## 概述 + +数据库分片(Database Sharding)是一种水平扩展数据库的技术,通过将大型数据库分割成多个较小的、更易管理的片段(分片),分布在不同的服务器上,以提高性能和可扩展性。 + +## 分片策略 + +### 1. 水平分片(Horizontal Sharding) + +按行分割数据,将表中的不同行分布到不同的分片中。 + +```java +// 基于用户ID的水平分片示例 +public class UserShardingStrategy { + + private static final int SHARD_COUNT = 4; + + /** + * 根据用户ID计算分片 + */ + public int getShardIndex(Long userId) { + return (int) (userId % SHARD_COUNT); + } + + /** + * 获取分片数据源 + */ + public DataSource getDataSource(Long userId) { + int shardIndex = getShardIndex(userId); + return DataSourceManager.getDataSource("shard_" + shardIndex); + } +} +``` + +### 2. 垂直分片(Vertical Sharding) + +按列分割数据,将表中的不同列分布到不同的分片中。 + +```java +// 垂直分片示例:用户基本信息和扩展信息分离 +public class VerticalShardingExample { + + // 用户基本信息表 + @Entity + @Table(name = "user_basic") + public class UserBasic { + private Long id; + private String username; + private String email; + private Date createTime; + } + + // 用户扩展信息表 + @Entity + @Table(name = "user_profile") + public class UserProfile { + private Long userId; + private String avatar; + private String bio; + private String address; + } +} +``` + +### 3. 功能分片(Functional Sharding) + +按功能模块分割数据库,不同的业务功能使用不同的数据库。 + +```java +@Configuration +public class FunctionalShardingConfig { + + @Bean + @Primary + public DataSource userDataSource() { + return DataSourceBuilder.create() + .url("jdbc:mysql://localhost:3306/user_db") + .build(); + } + + @Bean + public DataSource orderDataSource() { + return DataSourceBuilder.create() + .url("jdbc:mysql://localhost:3306/order_db") + .build(); + } + + @Bean + public DataSource productDataSource() { + return DataSourceBuilder.create() + .url("jdbc:mysql://localhost:3306/product_db") + .build(); + } +} +``` + +## MySQL分片实现 + +### 1. 基于ShardingSphere的分片配置 + +```yaml +# application-sharding.yml +spring: + shardingsphere: + datasource: + names: ds0,ds1,ds2,ds3 + ds0: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3306/shard_0 + username: root + password: password + ds1: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3306/shard_1 + username: root + password: password + ds2: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3306/shard_2 + username: root + password: password + ds3: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: jdbc:mysql://localhost:3306/shard_3 + username: root + password: password + rules: + sharding: + tables: + user: + actual-data-nodes: ds$->{0..3}.user_$->{0..3} + table-strategy: + standard: + sharding-column: id + sharding-algorithm-name: user-table-inline + database-strategy: + standard: + sharding-column: id + sharding-algorithm-name: user-database-inline + sharding-algorithms: + user-database-inline: + type: INLINE + props: + algorithm-expression: ds$->{id % 4} + user-table-inline: + type: INLINE + props: + algorithm-expression: user_$->{id % 4} +``` + +### 2. 自定义分片算法 + +```java +@Component +public class CustomShardingAlgorithm implements StandardShardingAlgorithm { + + @Override + public String doSharding(Collection availableTargetNames, + PreciseShardingValue shardingValue) { + Long value = shardingValue.getValue(); + + // 自定义分片逻辑 + if (value < 1000000) { + return "ds0"; + } else if (value < 2000000) { + return "ds1"; + } else if (value < 3000000) { + return "ds2"; + } else { + return "ds3"; + } + } + + @Override + public Collection doSharding(Collection availableTargetNames, + RangeShardingValue shardingValue) { + // 范围查询分片逻辑 + Set result = new HashSet<>(); + Range range = shardingValue.getValueRange(); + + for (String targetName : availableTargetNames) { + if (isInRange(targetName, range)) { + result.add(targetName); + } + } + + return result; + } + + private boolean isInRange(String targetName, Range range) { + // 判断目标分片是否在范围内 + // 实现具体逻辑 + return true; + } +} +``` + +### 3. 分片事务处理 + +```java +@Service +@Transactional +public class ShardingTransactionService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private OrderRepository orderRepository; + + /** + * 跨分片事务处理 + */ + @ShardingTransactionType(TransactionType.XA) + public void createUserAndOrder(User user, Order order) { + try { + // 保存用户信息(可能在不同分片) + userRepository.save(user); + + // 保存订单信息(可能在不同分片) + order.setUserId(user.getId()); + orderRepository.save(order); + + } catch (Exception e) { + // 事务回滚 + throw new RuntimeException("跨分片事务失败", e); + } + } +} +``` + +## PostgreSQL分片实现 + +### 1. 使用Postgres-XL集群 + +```sql +-- 创建分布式表 +CREATE TABLE user_info ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) DISTRIBUTE BY HASH(id); + +-- 创建复制表(小表) +CREATE TABLE user_status ( + status_id INT PRIMARY KEY, + status_name VARCHAR(20) +) DISTRIBUTE BY REPLICATION; +``` + +### 2. 使用Citus扩展 + +```sql +-- 启用Citus扩展 +CREATE EXTENSION citus; + +-- 创建分布式表 +SELECT create_distributed_table('user_info', 'id'); +SELECT create_distributed_table('user_orders', 'user_id'); + +-- 创建参考表 +SELECT create_reference_table('categories'); +``` + +### 3. Java代码实现 + +```java +@Configuration +public class PostgreSQLShardingConfig { + + @Bean + @ConfigurationProperties("spring.datasource.coordinator") + public DataSource coordinatorDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + @ConfigurationProperties("spring.datasource.worker1") + public DataSource worker1DataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + @ConfigurationProperties("spring.datasource.worker2") + public DataSource worker2DataSource() { + return DataSourceBuilder.create().build(); + } +} + +@Repository +public class PostgreSQLShardingRepository { + + @Autowired + @Qualifier("coordinatorDataSource") + private DataSource coordinatorDataSource; + + /** + * 分布式查询 + */ + public List findUsersByAgeRange(int minAge, int maxAge) { + String sql = """ + SELECT u.*, p.address + FROM user_info u + JOIN user_profile p ON u.id = p.user_id + WHERE u.age BETWEEN ? AND ? + """; + + try (Connection conn = coordinatorDataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setInt(1, minAge); + stmt.setInt(2, maxAge); + + ResultSet rs = stmt.executeQuery(); + return mapResultSetToUsers(rs); + } catch (SQLException e) { + throw new RuntimeException("分布式查询失败", e); + } + } +} +``` + +## MongoDB分片实现 + +### 1. MongoDB分片集群配置 + +```javascript +// 启动配置服务器 +mongod --configsvr --replSet configReplSet --port 27019 --dbpath /data/configdb + +// 启动分片服务器 +mongod --shardsvr --replSet shard1ReplSet --port 27018 --dbpath /data/shard1 +mongod --shardsvr --replSet shard2ReplSet --port 27020 --dbpath /data/shard2 + +// 启动mongos路由 +mongos --configdb configReplSet/localhost:27019 --port 27017 +``` + +### 2. 分片配置脚本 + +```javascript +// 连接到mongos +use admin + +// 添加分片 +sh.addShard("shard1ReplSet/localhost:27018") +sh.addShard("shard2ReplSet/localhost:27020") + +// 启用数据库分片 +sh.enableSharding("myapp") + +// 创建分片键 +sh.shardCollection("myapp.users", {"_id": "hashed"}) +sh.shardCollection("myapp.orders", {"userId": 1}) + +// 查看分片状态 +sh.status() +``` + +### 3. Java MongoDB分片操作 + +```java +@Configuration +public class MongoShardingConfig { + + @Bean + public MongoClient mongoClient() { + // 连接到mongos路由器 + return MongoClients.create("mongodb://localhost:27017"); + } + + @Bean + public MongoTemplate mongoTemplate() { + return new MongoTemplate(mongoClient(), "myapp"); + } +} + +@Service +public class MongoShardingService { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * 插入分片数据 + */ + public void insertUser(User user) { + // MongoDB自动根据分片键路由到正确的分片 + mongoTemplate.save(user, "users"); + } + + /** + * 跨分片查询 + */ + public List findUsersByAge(int minAge, int maxAge) { + Query query = new Query(Criteria.where("age").gte(minAge).lte(maxAge)); + return mongoTemplate.find(query, User.class, "users"); + } + + /** + * 聚合查询 + */ + public List getUserStatistics() { + Aggregation aggregation = Aggregation.newAggregation( + Aggregation.group("status").count().as("count"), + Aggregation.sort(Sort.Direction.DESC, "count") + ); + + AggregationResults results = + mongoTemplate.aggregate(aggregation, "users", AggregationResult.class); + + return results.getMappedResults(); + } +} +``` + +## 分片中间件对比 + +### 1. ShardingSphere + +**优点:** +- 支持多种数据库 +- 丰富的分片算法 +- 透明化分片 +- 强大的读写分离功能 + +**缺点:** +- 学习成本较高 +- 配置复杂 + +```java +@Configuration +@EnableShardingSphereDataSource +public class ShardingSphereConfig { + + @Bean + public DataSource dataSource() throws SQLException { + Map dataSourceMap = new HashMap<>(); + dataSourceMap.put("ds0", createDataSource("shard_0")); + dataSourceMap.put("ds1", createDataSource("shard_1")); + + ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); + shardingRuleConfig.getTableRuleConfigs().add(getUserTableRuleConfiguration()); + shardingRuleConfig.getBindingTableGroups().add("user"); + shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig( + new InlineShardingStrategyConfiguration("user_id", "ds$->{user_id % 2}")); + + return ShardingDataSourceFactory.createDataSource(dataSourceMap, + new ShardingRuleConfiguration(), new Properties()); + } +} +``` + +### 2. MyCat + +**优点:** +- 基于MySQL协议 +- 支持多种分片算法 +- 配置相对简单 + +**缺点:** +- 主要支持MySQL +- 社区活跃度一般 + +```xml + + + +
+ + + + + + + select user() + + + +``` + +## 分片最佳实践 + +### 1. 分片键选择原则 + +```java +public class ShardingKeyBestPractices { + + /** + * 好的分片键特征: + * 1. 数据分布均匀 + * 2. 查询友好 + * 3. 避免热点 + * 4. 业务相关性强 + */ + + // 推荐:用户ID作为分片键 + public void goodShardingKey() { + // 用户相关的所有操作都在同一分片 + // 避免跨分片查询 + } + + // 不推荐:时间戳作为分片键 + public void badShardingKey() { + // 会导致热点问题 + // 新数据总是写入最新的分片 + } +} +``` + +### 2. 跨分片查询优化 + +```java +@Service +public class CrossShardQueryOptimization { + + /** + * 避免跨分片JOIN + */ + public List getUserOrders(Long userId) { + // 方案1:应用层JOIN + User user = userService.getUser(userId); + List orders = orderService.getOrdersByUserId(userId); + + return orders.stream() + .map(order -> new UserOrderDTO(user, order)) + .collect(Collectors.toList()); + } + + /** + * 数据冗余避免跨分片查询 + */ + @Entity + public class OrderWithUserInfo { + private Long orderId; + private Long userId; + private String username; // 冗余用户名 + private String userEmail; // 冗余用户邮箱 + // 其他订单字段 + } + + /** + * 使用缓存减少跨分片查询 + */ + @Cacheable(value = "userCache", key = "#userId") + public User getUserFromCache(Long userId) { + return userRepository.findById(userId); + } +} +``` + +### 3. 分片扩容策略 + +```java +@Component +public class ShardingExpansionStrategy { + + /** + * 一致性哈希扩容 + */ + public void consistentHashExpansion() { + // 1. 添加新的分片节点 + // 2. 重新计算数据分布 + // 3. 迁移部分数据到新节点 + // 4. 更新路由规则 + } + + /** + * 双写策略扩容 + */ + public void doubleWriteExpansion() { + // 1. 新增分片节点 + // 2. 新数据同时写入新旧分片 + // 3. 逐步迁移历史数据 + // 4. 切换读取到新分片 + // 5. 停止旧分片写入 + } +} +``` + +## 监控与运维 + +### 1. 分片监控指标 + +```java +@Component +public class ShardingMonitor { + + @Autowired + private MeterRegistry meterRegistry; + + /** + * 监控分片查询性能 + */ + @EventListener + public void onShardingQuery(ShardingQueryEvent event) { + Timer.Sample sample = Timer.start(meterRegistry); + + try { + // 执行查询 + event.execute(); + } finally { + sample.stop(Timer.builder("sharding.query.duration") + .tag("shard", event.getShardName()) + .tag("table", event.getTableName()) + .register(meterRegistry)); + } + } + + /** + * 监控分片数据分布 + */ + @Scheduled(fixedRate = 60000) + public void monitorDataDistribution() { + for (String shardName : getShardNames()) { + long recordCount = getRecordCount(shardName); + + Gauge.builder("sharding.data.distribution") + .tag("shard", shardName) + .register(meterRegistry, recordCount); + } + } +} +``` + +### 2. 分片故障处理 + +```java +@Service +public class ShardingFailoverService { + + /** + * 分片故障检测 + */ + @Scheduled(fixedRate = 30000) + public void healthCheck() { + for (String shardName : getShardNames()) { + try { + DataSource dataSource = getDataSource(shardName); + Connection conn = dataSource.getConnection(); + + // 执行健康检查查询 + PreparedStatement stmt = conn.prepareStatement("SELECT 1"); + stmt.executeQuery(); + + // 标记分片健康 + markShardHealthy(shardName); + + } catch (Exception e) { + // 标记分片故障 + markShardUnhealthy(shardName); + + // 发送告警 + alertService.sendShardFailureAlert(shardName, e); + } + } + } + + /** + * 故障转移 + */ + public void failover(String failedShard) { + // 1. 将流量路由到其他健康分片 + updateRoutingRules(failedShard, false); + + // 2. 启动数据恢复流程 + startDataRecovery(failedShard); + + // 3. 通知运维人员 + notificationService.notifyFailover(failedShard); + } +} +``` + +## 总结 + +数据库分片是解决大规模数据存储和高并发访问的重要技术手段。选择合适的分片策略和实现方案需要考虑: + +1. **业务特点**:数据访问模式、查询类型、事务需求 +2. **技术栈**:现有数据库类型、开发框架、运维能力 +3. **性能要求**:并发量、响应时间、数据一致性 +4. **扩展性**:未来数据增长、业务发展需求 + +通过合理的分片设计和实现,可以有效提升系统的性能和可扩展性,但同时也要注意分片带来的复杂性,做好监控和运维工作。 \ No newline at end of file diff --git "a/docs/aJava/\346\227\245\345\270\270\346\225\210\347\216\207\345\267\245\345\205\267.md" "b/docs/aJava/\346\227\245\345\270\270\346\225\210\347\216\207\345\267\245\345\205\267.md" new file mode 100644 index 000000000..bdd8b804c --- /dev/null +++ "b/docs/aJava/\346\227\245\345\270\270\346\225\210\347\216\207\345\267\245\345\205\267.md" @@ -0,0 +1,14 @@ +--- +title: 日常效率工具 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 日常效率工具 + +![img.png](./img.png) + diff --git "a/docs/aJava/\347\250\213\345\272\217\350\256\241\346\225\260\345\231\250.md" "b/docs/aJava/\347\250\213\345\272\217\350\256\241\346\225\260\345\231\250.md" new file mode 100644 index 000000000..17e8ced39 --- /dev/null +++ "b/docs/aJava/\347\250\213\345\272\217\350\256\241\346\225\260\345\231\250.md" @@ -0,0 +1,23 @@ +--- +title: 程序计数器 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 程序计数器 + +一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 +程序计数器,这类内存也称为“线程私有”的内存。 + +正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如 +果还是 Native 方法,则为空。 + +这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。 + + + + diff --git "a/docs/aJava/\347\272\277\347\250\213.md" "b/docs/aJava/\347\272\277\347\250\213.md" new file mode 100644 index 000000000..9b0493a44 --- /dev/null +++ "b/docs/aJava/\347\272\277\347\250\213.md" @@ -0,0 +1,48 @@ +--- +title: 线程 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 线程 + +线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。 +Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓 +冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。 +Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可 +用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时,会释放原生线程和 Java 线程的所有资源。 + +Hotspot JVM 后台运行的系统线程主要有下面几个: +虚拟机线程 +(VM thread) + +这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当 +堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 + +周期性任务线程 + +这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。 + +GC 线程 + +这些线程支持 JVM 中不同的垃圾回收活动。 + +编译器线程 + +这些线程在运行时将字节码动态编译成本地平台相关的机器码。 + +信号分发线程 + +这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 + + + + + + + + diff --git "a/docs/aJava/\350\256\276\345\244\207\347\275\221\345\205\263\345\220\216\347\253\257\350\256\276\350\256\241.md" "b/docs/aJava/\350\256\276\345\244\207\347\275\221\345\205\263\345\220\216\347\253\257\350\256\276\350\256\241.md" new file mode 100644 index 000000000..5c6a49ca6 --- /dev/null +++ "b/docs/aJava/\350\256\276\345\244\207\347\275\221\345\205\263\345\220\216\347\253\257\350\256\276\350\256\241.md" @@ -0,0 +1,580 @@ +# 设备网关后端设计 + +## 概述 + +设备网关是物联网系统中的核心组件,负责连接海量设备与云端服务,提供设备接入、协议转换、数据处理、消息路由等功能。本文档详细介绍基于Netty、Kafka和数据库的设备网关后端架构设计。 + +## 系统架构 + +### 整体架构图 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 设备层 │ │ 网关层 │ │ 服务层 │ +│ │ │ │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ +│ │ IoT设备 │ │ │ │ 设备网关 │ │ │ │ 业务服务 │ │ +│ │ - 传感器 │ │◄──►│ │ - 协议适配 │ │◄──►│ │ - 设备管理 │ │ +│ │ - 执行器 │ │ │ │ - 数据处理 │ │ │ │ - 数据分析 │ │ +│ │ - 控制器 │ │ │ │ - 消息路由 │ │ │ │ - 告警服务 │ │ +│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ 存储层 │ + │ │ + │ ┌─────────────┐ │ + │ │ MySQL │ │ + │ │ Redis │ │ + │ │ InfluxDB │ │ + │ └─────────────┘ │ + └─────────────────┘ +``` + +### 核心组件 + +1. **设备接入层**:基于Netty实现高并发设备连接 +2. **协议适配层**:支持多种IoT协议(MQTT、CoAP、HTTP等) +3. **消息处理层**:基于Kafka实现消息队列和流处理 +4. **数据存储层**:多种数据库满足不同存储需求 +5. **服务治理层**:提供监控、限流、熔断等功能 + +## 技术选型 + +### Netty网络框架 + +**选择理由:** +- 高性能异步事件驱动 +- 支持多种协议 +- 内存管理优秀 +- 社区活跃,生态完善 + +**核心特性:** +- NIO/Epoll模型 +- 零拷贝技术 +- 内存池管理 +- 编解码器链 + +### Kafka消息队列 + +**选择理由:** +- 高吞吐量 +- 分布式架构 +- 持久化存储 +- 流处理能力 + +**应用场景:** +- 设备数据采集 +- 实时数据流处理 +- 系统解耦 +- 事件驱动架构 + +### 数据库设计 + +**MySQL(关系型数据库):** +- 设备元数据管理 +- 用户权限管理 +- 配置信息存储 + +**Redis(缓存数据库):** +- 设备状态缓存 +- 会话管理 +- 热点数据缓存 + +**InfluxDB(时序数据库):** +- 设备数据存储 +- 监控指标存储 +- 历史数据查询 + +## 详细设计 + +### 设备接入模块 + +#### Netty服务器配置 + +``` +@Component +public class DeviceGatewayServer { + + private final EventLoopGroup bossGroup; + private final EventLoopGroup workerGroup; + private final DeviceChannelInitializer channelInitializer; + + public DeviceGatewayServer() { + this.bossGroup = new NioEventLoopGroup(1); + this.workerGroup = new NioEventLoopGroup(); + this.channelInitializer = new DeviceChannelInitializer(); + } + + public void start(int port) throws InterruptedException { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(channelInitializer) + .option(ChannelOption.SO_BACKLOG, 1024) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.TCP_NODELAY, true); + + ChannelFuture future = bootstrap.bind(port).sync(); + log.info("设备网关启动成功,端口:{}", port); + future.channel().closeFuture().sync(); + } +} +``` + +#### 协议处理器 + +``` +@ChannelHandler.Sharable +public class DeviceProtocolHandler extends ChannelInboundHandlerAdapter { + + private final DeviceMessageProcessor messageProcessor; + private final DeviceSessionManager sessionManager; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof DeviceMessage) { + DeviceMessage deviceMessage = (DeviceMessage) msg; + + // 设备认证 + if (!authenticateDevice(deviceMessage)) { + ctx.close(); + return; + } + + // 会话管理 + sessionManager.updateSession(ctx.channel(), deviceMessage.getDeviceId()); + + // 消息处理 + messageProcessor.process(deviceMessage); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("设备连接异常", cause); + ctx.close(); + } +} +``` + +### 消息处理模块 + +#### Kafka生产者配置 + +``` +@Configuration +public class KafkaProducerConfig { + + @Bean + public ProducerFactory producerFactory() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(ProducerConfig.ACKS_CONFIG, "all"); + props.put(ProducerConfig.RETRIES_CONFIG, 3); + props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); + props.put(ProducerConfig.LINGER_MS_CONFIG, 1); + props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); + + return new DefaultKafkaProducerFactory<>(props); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} +``` + +#### 消息路由服务 + +``` +@Service +public class MessageRoutingService { + + private final KafkaTemplate kafkaTemplate; + private final DeviceTopicResolver topicResolver; + + public void routeMessage(DeviceMessage message) { + // 根据消息类型和设备类型确定Topic + String topic = topicResolver.resolveTopic(message); + + // 构建Kafka消息 + DeviceDataEvent event = DeviceDataEvent.builder() + .deviceId(message.getDeviceId()) + .messageType(message.getMessageType()) + .payload(message.getPayload()) + .timestamp(System.currentTimeMillis()) + .build(); + + // 发送到Kafka + kafkaTemplate.send(topic, message.getDeviceId(), event) + .addCallback( + result -> log.info("消息发送成功:{}", result), + failure -> log.error("消息发送失败", failure) + ); + } +} +``` + +### 数据存储模块 + +#### 设备实体设计 + +``` +@Entity +@Table(name = "device_info") +public class DeviceInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String deviceId; + + @Column(nullable = false) + private String deviceName; + + @Column(nullable = false) + private String deviceType; + + @Column(nullable = false) + private String protocol; + + @Enumerated(EnumType.STRING) + private DeviceStatus status; + + @Column(name = "last_online_time") + private LocalDateTime lastOnlineTime; + + @Column(name = "created_time") + private LocalDateTime createdTime; + + @Column(name = "updated_time") + private LocalDateTime updatedTime; + + // getters and setters +} +``` + +#### 时序数据存储 + +``` +@Service +public class TimeSeriesDataService { + + private final InfluxDBTemplate influxDBTemplate; + + public void saveDeviceData(DeviceDataPoint dataPoint) { + Point point = Point.measurement("device_data") + .tag("device_id", dataPoint.getDeviceId()) + .tag("device_type", dataPoint.getDeviceType()) + .tag("data_type", dataPoint.getDataType()) + .addField("value", dataPoint.getValue()) + .addField("unit", dataPoint.getUnit()) + .time(dataPoint.getTimestamp(), TimeUnit.MILLISECONDS) + .build(); + + influxDBTemplate.write(point); + } + + public List queryDeviceData(String deviceId, + LocalDateTime start, + LocalDateTime end) { + String query = String.format( + "SELECT * FROM device_data WHERE device_id='%s' AND time >= '%s' AND time <= '%s'", + deviceId, start.toString(), end.toString() + ); + + QueryResult result = influxDBTemplate.query(new Query(query)); + return parseQueryResult(result); + } +} +``` + +## 性能优化 + +### 连接池优化 + +``` +@Configuration +public class NettyServerConfig { + + @Bean + public EventLoopGroup bossGroup() { + return new NioEventLoopGroup(1, new DefaultThreadFactory("boss")); + } + + @Bean + public EventLoopGroup workerGroup() { + int workerThreads = Runtime.getRuntime().availableProcessors() * 2; + return new NioEventLoopGroup(workerThreads, new DefaultThreadFactory("worker")); + } +} +``` + +### 内存管理 + +``` +public class DeviceMessageDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + // 使用直接内存,避免内存拷贝 + if (in.readableBytes() < 4) { + return; + } + + in.markReaderIndex(); + int length = in.readInt(); + + if (in.readableBytes() < length) { + in.resetReaderIndex(); + return; + } + + // 使用slice避免内存拷贝 + ByteBuf frame = in.readSlice(length); + out.add(parseMessage(frame)); + } +} +``` + +### 批量处理 + +``` +@Service +public class BatchMessageProcessor { + + private final List messageBuffer = new ArrayList<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + @PostConstruct + public void init() { + // 每秒批量处理一次 + scheduler.scheduleAtFixedRate(this::processBatch, 1, 1, TimeUnit.SECONDS); + } + + public void addMessage(DeviceMessage message) { + synchronized (messageBuffer) { + messageBuffer.add(message); + if (messageBuffer.size() >= 1000) { + processBatch(); + } + } + } + + private void processBatch() { + List batch; + synchronized (messageBuffer) { + if (messageBuffer.isEmpty()) { + return; + } + batch = new ArrayList<>(messageBuffer); + messageBuffer.clear(); + } + + // 批量处理消息 + batchProcess(batch); + } +} +``` + +## 监控与运维 + +### 健康检查 + +``` +@Component +public class GatewayHealthIndicator implements HealthIndicator { + + private final DeviceSessionManager sessionManager; + private final KafkaTemplate kafkaTemplate; + + @Override + public Health health() { + Health.Builder builder = new Health.Builder(); + + try { + // 检查活跃连接数 + int activeConnections = sessionManager.getActiveConnectionCount(); + builder.withDetail("activeConnections", activeConnections); + + // 检查Kafka连接 + kafkaTemplate.send("health-check", "ping").get(1, TimeUnit.SECONDS); + builder.withDetail("kafkaStatus", "UP"); + + builder.up(); + } catch (Exception e) { + builder.down(e); + } + + return builder.build(); + } +} +``` + +### 指标监控 + +``` +@Component +public class GatewayMetrics { + + private final Counter messageCounter; + private final Timer messageProcessingTimer; + private final Gauge activeConnectionsGauge; + + public GatewayMetrics(MeterRegistry meterRegistry, + DeviceSessionManager sessionManager) { + this.messageCounter = Counter.builder("gateway.messages.total") + .description("Total number of messages processed") + .register(meterRegistry); + + this.messageProcessingTimer = Timer.builder("gateway.message.processing.time") + .description("Message processing time") + .register(meterRegistry); + + this.activeConnectionsGauge = Gauge.builder("gateway.connections.active") + .description("Number of active connections") + .register(meterRegistry, sessionManager, + DeviceSessionManager::getActiveConnectionCount); + } + + public void incrementMessageCount() { + messageCounter.increment(); + } + + public Timer.Sample startTimer() { + return Timer.start(); + } +} +``` + +## 安全设计 + +### 设备认证 + +``` +@Service +public class DeviceAuthenticationService { + + private final DeviceRepository deviceRepository; + private final RedisTemplate redisTemplate; + + public boolean authenticate(String deviceId, String token) { + // 检查设备是否存在 + DeviceInfo device = deviceRepository.findByDeviceId(deviceId); + if (device == null || device.getStatus() != DeviceStatus.ACTIVE) { + return false; + } + + // 验证Token + String cachedToken = redisTemplate.opsForValue().get("device:token:" + deviceId); + if (!Objects.equals(token, cachedToken)) { + return false; + } + + // 更新最后在线时间 + device.setLastOnlineTime(LocalDateTime.now()); + deviceRepository.save(device); + + return true; + } +} +``` + +### 数据加密 + +``` +@Component +public class MessageEncryption { + + private final AESUtil aesUtil; + + public String encryptMessage(String message, String deviceId) { + String key = getDeviceKey(deviceId); + return aesUtil.encrypt(message, key); + } + + public String decryptMessage(String encryptedMessage, String deviceId) { + String key = getDeviceKey(deviceId); + return aesUtil.decrypt(encryptedMessage, key); + } + + private String getDeviceKey(String deviceId) { + // 从安全存储中获取设备密钥 + return keyManager.getDeviceKey(deviceId); + } +} +``` + +## 部署架构 + +### Docker容器化 + +``` +FROM openjdk:11-jre-slim + +VOLUME /tmp + +COPY target/device-gateway-*.jar app.jar + +EXPOSE 8080 1883 5683 + +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +### Kubernetes部署 + +``` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: device-gateway +spec: + replicas: 3 + selector: + matchLabels: + app: device-gateway + template: + metadata: + labels: + app: device-gateway + spec: + containers: + - name: device-gateway + image: device-gateway:latest + ports: + - containerPort: 8080 + - containerPort: 1883 + - containerPort: 5683 + env: + - name: KAFKA_SERVERS + value: "kafka:9092" + - name: MYSQL_URL + value: "jdbc:mysql://mysql:3306/gateway" + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" +``` + +## 总结 + +本设计文档详细介绍了基于Netty、Kafka和数据库的设备网关后端架构,涵盖了系统架构、技术选型、详细设计、性能优化、监控运维、安全设计和部署架构等方面。该架构具有以下特点: + +1. **高性能**:基于Netty的异步非阻塞架构,支持海量设备并发连接 +2. **高可用**:分布式架构设计,支持水平扩展和故障转移 +3. **高可靠**:基于Kafka的消息队列保证数据不丢失 +4. **可扩展**:模块化设计,支持多种协议和设备类型 +5. **可监控**:完善的监控指标和健康检查机制 +6. **安全性**:设备认证、数据加密等安全措施 + +该架构能够满足大规模物联网场景下的设备接入和数据处理需求,为构建稳定可靠的物联网平台提供了坚实的技术基础。 \ No newline at end of file diff --git "a/docs/aJava/\350\264\237\350\275\275\345\235\207\350\241\241\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\350\264\237\350\275\275\345\235\207\350\241\241\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..0a1b94981 --- /dev/null +++ "b/docs/aJava/\350\264\237\350\275\275\345\235\207\350\241\241\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,28 @@ +# 负载均衡是什么 + +在业务初期,通常采用单台服务器即可满足需求,随着用户流量增长,单台服务器逐渐难以应对压力,这时我们会将多台服务器组成集群来提升处理能力,为了统一管理流量入口,需要通过负载均衡器将海量请求按照预设算法,智能分发到集群中不同服务器,这就是负载均衡。 + +广义的负载均衡器可分为3中: + +1.DNS负载均衡:通过DNS解析,将域名解析到不同服务器IP,从而将流量分发到不同服务器,但DNS解析结果可能存在缓存,导致分发结果不准确。 + +用户访问域名www.aq.com通过DNS解析到多个IP,然后访问每个IP对应的服务器实例,就完成了流量调度。它没有使用常规的负载均衡器,但也的确完成了简单负载均衡的功能,优点是简单,成本低。缺点是,服务器故障切换延迟大,DNS与用户之间是层层的缓存,即便故障发生时,(域名解析缓存:浏览器缓存,操作系统缓存,/etc/hosts缓存,DNS缓存)DNS解析结果可能仍会分发到故障服务器,导致用户访问失败。 + +通过及时修改DNS或摘除故障服务器,但由于中间经过运营商的DNS缓存,且缓存很有可能不遵循TTL规则,导致DNS生效时间缓慢,仍访问到故障服务器。另外,它的流量调度策略简单,支持的算法较少。DNS一般只支持RR的轮询方式。实际上生产环境中很少用这种方式来实现负载均衡。描述DNS负载均衡方式,只是为了让你能够更清楚了解负载均衡的概念。 + +一般大公司也会使用DNS来实现地理级别的负载均衡,实现就近访问,提高访问速度,这种方式一般是入口流量的基础负载均衡。下层会有更专业的负载均衡设备实现负载架构。 + +2.硬件负载均衡:通过硬件设备实现负载均衡,如F5,硬件负载均衡性能稳定,但价格昂贵。 + +专门的硬件设备来实现,类似于交换机路由器,是一个负载均衡专用的网络设备,目前业界典型的硬件负载均衡设备主要有两款:F5,和A10.这类设备性能好,功能强大,但价格非常昂贵。优点,性能强,功能强大,价格贵。 + +3.软件负载均衡:通过软件实现负载均衡,如Nginx,LVS,HAProxy,软件负载均衡灵活,成本低廉,但性能不及硬件负载均衡。 + +它是指可以在普通服务器上运行的负载均衡软件,实现负载均衡功能,Nginx,LVS,HAProxy + +Nginx是7层负载均衡 支持HTTP协议。(OSI:七层模型有应用层,表示层,会话层,传输层,网络层,数据链路层,物理层) + +HAproxy也是7层负载均衡软件,性能也很不错,而LVS是纯4层的负载均衡,运行在内核态,性能是软件负载均衡中最高的,因为是在四层,所以也更通用一些。 +软件负载均衡的特点:部署简单,便宜,灵活 + +LVS开源软件,节省成本。 diff --git "a/docs/aJava/\350\267\257\347\224\261\345\231\250\346\230\257\344\273\200\344\271\210.md" "b/docs/aJava/\350\267\257\347\224\261\345\231\250\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 000000000..37aec1f58 --- /dev/null +++ "b/docs/aJava/\350\267\257\347\224\261\345\231\250\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,251 @@ +# 路由器是什么 + +路由器(Router)是一种网络设备,用于连接不同的网络并在它们之间转发数据包。它工作在OSI模型的第三层(网络层),是现代网络通信的核心设备之一。 + +## 基本概念 + +路由器的主要功能是根据路由表中的信息,选择最佳路径将数据包从源网络传输到目标网络。它就像网络世界中的"交通指挥员",负责指引数据包走向正确的目的地。 + +### 核心功能 +- **路由选择:** 根据目标IP地址选择最佳传输路径 +- **数据转发:** 将数据包从一个网络接口转发到另一个网络接口 +- **网络隔离:** 分割广播域,减少网络拥塞 +- **协议转换:** 支持不同网络协议之间的转换 + +## 路由器的工作原理 + +### 1. 路由表 +路由表是路由器的"地图",包含了到达各个网络的路径信息: + +``` +目标网络 子网掩码 下一跳 接口 跃点数 +192.168.1.0 255.255.255.0 直连 eth0 1 +192.168.2.0 255.255.255.0 192.168.1.1 eth0 2 +0.0.0.0 0.0.0.0 192.168.1.254 eth0 1 +``` + +### 2. 数据包转发过程 +``` +1. 接收数据包 +2. 检查目标IP地址 +3. 查询路由表 +4. 选择最佳路径 +5. 修改数据包头部 +6. 转发到下一跳 +``` + +## 路由器类型 + +### 1. 按应用场景分类 + +**家用路由器** +- 功能:提供WiFi接入、NAT转换、DHCP服务 +- 特点:价格便宜、配置简单、功能集成 +- 适用:家庭、小型办公室 + +**企业路由器** +- 功能:高性能路由、VPN支持、QoS控制 +- 特点:稳定性高、可扩展性强、管理功能丰富 +- 适用:中大型企业、数据中心 + +**核心路由器** +- 功能:高速数据转发、多协议支持 +- 特点:处理能力强、可靠性极高、价格昂贵 +- 适用:运营商网络、大型数据中心 + +### 2. 按技术特性分类 + +**静态路由器** +```bash +# 手动配置路由 +ip route add 192.168.2.0/24 via 192.168.1.1 +``` + +**动态路由器** +- 支持RIP、OSPF、BGP等动态路由协议 +- 自动学习和更新路由信息 +- 适应网络拓扑变化 + +## 常见路由协议 + +### 1. 距离向量协议 + +**RIP (Routing Information Protocol)** +- 特点:配置简单,适用于小型网络 +- 缺点:收敛慢,最大跳数限制为15 +- 更新周期:30秒 + +### 2. 链路状态协议 + +**OSPF (Open Shortest Path First)** +- 特点:收敛快,支持大型网络 +- 优势:无跳数限制,支持负载均衡 +- 适用:企业网络、运营商网络 + +### 3. 路径向量协议 + +**BGP (Border Gateway Protocol)** +- 特点:互联网骨干协议 +- 功能:AS间路由、策略控制 +- 适用:运营商互联、大型企业 + +## 实际应用场景 + +### 1. 家庭网络 +``` +互联网 ← → 路由器 ← → 交换机 ← → 终端设备 + ↓ + WiFi设备 +``` + +**典型配置:** +- WAN口:自动获取或PPPoE拨号 +- LAN口:192.168.1.1/24 +- DHCP:192.168.1.100-200 +- WiFi:WPA2/WPA3加密 + +### 2. 企业网络 +``` +总部 ← → 核心路由器 ← → 分支路由器 ← → 分公司 + ↓ + 汇聚交换机 + ↓ + 接入交换机 +``` + +**网络规划:** +- 核心层:高性能路由器 +- 汇聚层:三层交换机 +- 接入层:二层交换机 + +### 3. 数据中心网络 +``` +Leaf-Spine架构: +Spine路由器 ← → Leaf交换机 ← → 服务器 +``` + +## 路由器配置实例 + +### 1. 基本配置 +```bash +# 配置接口IP +interface GigabitEthernet0/0 +ip address 192.168.1.1 255.255.255.0 +no shutdown + +# 配置静态路由 +ip route 192.168.2.0 255.255.255.0 192.168.1.2 + +# 配置默认路由 +ip route 0.0.0.0 0.0.0.0 192.168.1.254 +``` + +### 2. OSPF配置 +```bash +# 启用OSPF +router ospf 1 +network 192.168.1.0 0.0.0.255 area 0 +network 192.168.2.0 0.0.0.255 area 0 +``` + +### 3. NAT配置 +```bash +# 配置NAT +ip nat inside source list 1 interface GigabitEthernet0/1 overload +access-list 1 permit 192.168.1.0 0.0.0.255 + +# 接口配置 +interface GigabitEthernet0/0 +ip nat inside + +interface GigabitEthernet0/1 +ip nat outside +``` + +## 性能优化与故障排除 + +### 1. 性能监控 +```bash +# 查看路由表 +show ip route + +# 查看接口状态 +show interfaces + +# 查看CPU使用率 +show processes cpu + +# 查看内存使用 +show memory +``` + +### 2. 常见问题排查 + +**路由不可达** +```bash +# 检查路由表 +show ip route 192.168.2.0 + +# 测试连通性 +ping 192.168.2.1 + +# 跟踪路由 +traceroute 192.168.2.1 +``` + +**性能问题** +- 检查接口利用率 +- 分析流量模式 +- 优化路由策略 +- 升级硬件配置 + +### 3. 安全配置 +```bash +# 配置访问控制列表 +access-list 100 deny tcp any any eq 23 +access-list 100 permit ip any any + +# 应用到接口 +interface GigabitEthernet0/0 +ip access-group 100 in +``` + +## 选型建议 + +### 1. 家用路由器选择 +- **预算考虑:** 200-500元价位 +- **性能需求:** WiFi 6支持、千兆端口 +- **功能需求:** 家长控制、QoS、VPN + +### 2. 企业路由器选择 +- **性能指标:** 吞吐量、PPS、并发连接数 +- **功能需求:** 路由协议、VPN、负载均衡 +- **可靠性:** 双电源、硬件冗余 + +### 3. 运营商级路由器 +- **核心指标:** Tbps级转发能力 +- **协议支持:** 全协议栈支持 +- **扩展性:** 模块化设计 + +## 发展趋势 + +### 1. 软件定义网络(SDN) +- 控制平面与数据平面分离 +- 集中式控制器管理 +- 可编程网络功能 + +### 2. 云原生路由 +- 容器化部署 +- 微服务架构 +- 自动化运维 + +### 3. AI智能路由 +- 智能流量调度 +- 自动故障检测 +- 预测性维护 + +## 总结 + +路由器作为网络的核心设备,在现代通信中发挥着不可替代的作用。从家庭WiFi到企业网络,从数据中心到运营商骨干网,路由器无处不在。 + +理解路由器的工作原理、掌握基本配置方法、熟悉故障排除技巧,对于网络工程师来说至关重要。随着网络技术的不断发展,路由器也在向着更加智能化、软件化的方向演进。 \ No newline at end of file diff --git a/docs/aThread/javaThreadPool.md b/docs/aThread/javaThreadPool.md new file mode 100644 index 000000000..0364958c4 --- /dev/null +++ b/docs/aThread/javaThreadPool.md @@ -0,0 +1,683 @@ +--- +title: java线程池实现原理 +author: 哪吒 +date: '2023-06-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Java线程池实现原理 + +## 1. 线程池概述 + +### 1.1 什么是线程池 + +线程池(Thread Pool)是一种多线程处理形式,预先创建若干个线程,这些线程在没有任务处理时处于等待状态,当有任务来临时分配给其中的一个线程来处理,当处理完后又回到等待状态等待下一个任务。 + +### 1.2 为什么要使用线程池 + +``` +传统方式创建线程的问题: +┌─────────────────────────────────────┐ +│ 每次需要执行任务时创建新线程 │ +│ ↓ │ +│ 线程执行完任务后被销毁 │ +│ ↓ │ +│ 频繁创建和销毁线程开销大 │ +│ ↓ │ +│ 无法控制线程数量,可能导致系统崩溃 │ +└─────────────────────────────────────┘ + +线程池的优势: +┌─────────────────────────────────────┐ +│ ✓ 降低资源消耗 │ +│ ✓ 提高响应速度 │ +│ ✓ 提高线程的可管理性 │ +│ ✓ 提供更多更强大的功能 │ +└─────────────────────────────────────┘ +``` + +## 2. 线程池核心参数 + +### 2.1 ThreadPoolExecutor构造参数 + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +### 2.2 参数详解 + +| 参数 | 说明 | 作用 | +|------|------|------| +| **corePoolSize** | 核心线程数 | 线程池中始终保持的线程数量 | +| **maximumPoolSize** | 最大线程数 | 线程池中允许的最大线程数量 | +| **keepAliveTime** | 线程空闲时间 | 非核心线程空闲时的存活时间 | +| **unit** | 时间单位 | keepAliveTime的时间单位 | +| **workQueue** | 工作队列 | 存储等待执行任务的队列 | +| **threadFactory** | 线程工厂 | 创建新线程的工厂 | +| **handler** | 拒绝策略 | 当线程池和队列都满时的处理策略 | + +### 2.3 参数关系图解 + +``` +线程池执行流程: +┌─────────────────────────────────────────────────────────┐ +│ 提交任务 │ +│ ↓ │ +│ 核心线程数是否已满? │ +│ ↙ ↘ │ +│ 否 是 │ +│ ↓ ↓ │ +│ 创建核心线程 工作队列是否已满? │ +│ 执行任务 ↙ ↘ │ +│ 否 是 │ +│ ↓ ↓ │ +│ 加入队列 最大线程数是否已满? │ +│ ↙ ↘ │ +│ 否 是 │ +│ ↓ ↓ │ +│ 创建非核心线程 执行拒绝策略 │ +│ 执行任务 │ +└─────────────────────────────────────────────────────────┘ +``` + +## 3. 工作队列类型 + +### 3.1 常用队列类型 + +```java +// 1. ArrayBlockingQueue - 有界队列 +BlockingQueue queue1 = new ArrayBlockingQueue<>(100); + +// 2. LinkedBlockingQueue - 无界队列(默认Integer.MAX_VALUE) +BlockingQueue queue2 = new LinkedBlockingQueue<>(); + +// 3. SynchronousQueue - 同步队列 +BlockingQueue queue3 = new SynchronousQueue<>(); + +// 4. PriorityBlockingQueue - 优先级队列 +BlockingQueue queue4 = new PriorityBlockingQueue<>(); + +// 5. DelayQueue - 延迟队列 +BlockingQueue queue5 = new DelayQueue<>(); +``` + +### 3.2 队列特性对比 + +| 队列类型 | 容量 | 特点 | 适用场景 | +|----------|------|------|----------| +| **ArrayBlockingQueue** | 有界 | 基于数组,FIFO | 资源有限,需要控制内存使用 | +| **LinkedBlockingQueue** | 无界 | 基于链表,FIFO | 任务量不确定,但要避免拒绝 | +| **SynchronousQueue** | 0 | 直接传递 | 任务量大,希望直接处理 | +| **PriorityBlockingQueue** | 无界 | 优先级排序 | 任务有优先级要求 | +| **DelayQueue** | 无界 | 延迟执行 | 定时任务场景 | + +## 4. 拒绝策略 + +### 4.1 内置拒绝策略 + +```java +// 1. AbortPolicy - 抛出异常(默认) +RejectedExecutionHandler abort = new ThreadPoolExecutor.AbortPolicy(); + +// 2. CallerRunsPolicy - 调用者运行 +RejectedExecutionHandler caller = new ThreadPoolExecutor.CallerRunsPolicy(); + +// 3. DiscardPolicy - 丢弃任务 +RejectedExecutionHandler discard = new ThreadPoolExecutor.DiscardPolicy(); + +// 4. DiscardOldestPolicy - 丢弃最老任务 +RejectedExecutionHandler discardOldest = new ThreadPoolExecutor.DiscardOldestPolicy(); +``` + +### 4.2 自定义拒绝策略 + +```java +public class CustomRejectedExecutionHandler implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + // 记录日志 + System.err.println("任务被拒绝: " + r.toString()); + + // 可以选择: + // 1. 保存到数据库或文件 + // 2. 发送到消息队列 + // 3. 降级处理 + // 4. 通知监控系统 + + // 示例:保存到备用队列 + saveToBackupQueue(r); + } + + private void saveToBackupQueue(Runnable task) { + // 实现备用处理逻辑 + System.out.println("任务已保存到备用队列"); + } +} +``` + +## 5. 线程池状态 + +### 5.1 线程池生命周期 + +``` +线程池状态转换图: +┌─────────────────────────────────────────────────────────┐ +│ │ +│ RUNNING ──────shutdown()─────→ SHUTDOWN │ +│ │ │ │ +│ │ │ │ +│ shutdownNow() 队列为空且 │ +│ │ 活跃线程为0 │ +│ ↓ ↓ │ +│ STOP ──────队列为空且─────→ TIDYING ──terminated()─→│ +│ 活跃线程为0 │ │ +│ ↓ │ +│ TERMINATED │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 5.2 状态详解 + +```java +// 线程池状态常量 +private static final int RUNNING = -1 << COUNT_BITS; // 接受新任务,处理队列任务 +private static final int SHUTDOWN = 0 << COUNT_BITS; // 不接受新任务,处理队列任务 +private static final int STOP = 1 << COUNT_BITS; // 不接受新任务,不处理队列任务 +private static final int TIDYING = 2 << COUNT_BITS; // 所有任务终止,线程数为0 +private static final int TERMINATED = 3 << COUNT_BITS; // terminated()方法执行完成 +``` + +## 6. 核心源码分析 + +### 6.1 execute方法源码分析 + +```java +public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + + // 获取当前线程池状态和线程数 + int c = ctl.get(); + + // 1. 如果当前线程数 < 核心线程数,创建核心线程 + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + + // 2. 如果线程池运行中且任务成功加入队列 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + // 双重检查:如果线程池不是运行状态,移除任务并拒绝 + if (!isRunning(recheck) && remove(command)) + reject(command); + // 如果没有工作线程,创建一个 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + // 3. 队列满了,尝试创建非核心线程 + else if (!addWorker(command, false)) + // 创建失败,执行拒绝策略 + reject(command); +} +``` + +### 6.2 addWorker方法分析 + +```java +private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + int c = ctl.get(); + int rs = runStateOf(c); + + // 检查线程池状态 + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + int wc = workerCountOf(c); + // 检查线程数是否超限 + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + // CAS增加线程数 + if (compareAndIncrementWorkerCount(c)) + break retry; + c = ctl.get(); + if (runStateOf(c) != rs) + continue retry; + } + } + + boolean workerStarted = false; + boolean workerAdded = false; + Worker w = null; + try { + // 创建Worker + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // 再次检查线程池状态 + int rs = runStateOf(ctl.get()); + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) + throw new IllegalThreadStateException(); + // 添加到工作线程集合 + workers.add(w); + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + workerAdded = true; + } + } finally { + mainLock.unlock(); + } + if (workerAdded) { + // 启动线程 + t.start(); + workerStarted = true; + } + } + } finally { + if (!workerStarted) + addWorkerFailed(w); + } + return workerStarted; +} +``` + +### 6.3 Worker内部类 + +```java +private final class Worker extends AbstractQueuedSynchronizer implements Runnable { + final Thread thread; + Runnable firstTask; + volatile long completedTasks; + + Worker(Runnable firstTask) { + setState(-1); // 禁止中断直到runWorker + this.firstTask = firstTask; + this.thread = getThreadFactory().newThread(this); + } + + public void run() { + runWorker(this); + } + + // AQS方法实现 + protected boolean isHeldExclusively() { + return getState() != 0; + } + + protected boolean tryAcquire(int unused) { + if (compareAndSetState(0, 1)) { + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + + protected boolean tryRelease(int unused) { + setExclusiveOwnerThread(null); + setState(0); + return true; + } +} +``` + +### 6.4 runWorker方法 + +```java +final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + Runnable task = w.firstTask; + w.firstTask = null; + w.unlock(); // 允许中断 + boolean completedAbruptly = true; + try { + // 循环获取任务执行 + while (task != null || (task = getTask()) != null) { + w.lock(); + // 检查线程池状态,决定是否中断 + if ((runStateAtLeast(ctl.get(), STOP) || + (Thread.interrupted() && + runStateAtLeast(ctl.get(), STOP))) && + !wt.isInterrupted()) + wt.interrupt(); + try { + beforeExecute(wt, task); + Throwable thrown = null; + try { + task.run(); // 执行任务 + } catch (RuntimeException x) { + thrown = x; throw x; + } catch (Error x) { + thrown = x; throw x; + } catch (Throwable x) { + thrown = x; throw new Error(x); + } finally { + afterExecute(task, thrown); + } + } finally { + task = null; + w.completedTasks++; + w.unlock(); + } + } + completedAbruptly = false; + } finally { + processWorkerExit(w, completedAbruptly); + } +} +``` + +## 7. 实际应用示例 + +### 7.1 基础使用示例 + +```java +public class ThreadPoolExample { + public static void main(String[] args) { + // 创建线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + 2, // 核心线程数 + 4, // 最大线程数 + 60L, // 空闲时间 + TimeUnit.SECONDS, // 时间单位 + new ArrayBlockingQueue<>(10), // 工作队列 + new ThreadFactory() { // 线程工厂 + private AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "MyThread-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 + ); + + // 提交任务 + for (int i = 0; i < 20; i++) { + final int taskId = i; + executor.execute(() -> { + System.out.println("执行任务 " + taskId + + " - 线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(2000); // 模拟任务执行 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + // 关闭线程池 + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + } + } +} +``` + +### 7.2 监控线程池状态 + +```java +public class ThreadPoolMonitor { + private final ThreadPoolExecutor executor; + private final ScheduledExecutorService monitor; + + public ThreadPoolMonitor(ThreadPoolExecutor executor) { + this.executor = executor; + this.monitor = Executors.newScheduledThreadPool(1); + } + + public void startMonitoring() { + monitor.scheduleAtFixedRate(() -> { + System.out.println("=== 线程池状态监控 ==="); + System.out.println("核心线程数: " + executor.getCorePoolSize()); + System.out.println("最大线程数: " + executor.getMaximumPoolSize()); + System.out.println("当前线程数: " + executor.getPoolSize()); + System.out.println("活跃线程数: " + executor.getActiveCount()); + System.out.println("队列大小: " + executor.getQueue().size()); + System.out.println("已完成任务数: " + executor.getCompletedTaskCount()); + System.out.println("总任务数: " + executor.getTaskCount()); + System.out.println("是否关闭: " + executor.isShutdown()); + System.out.println("是否终止: " + executor.isTerminated()); + System.out.println("========================\n"); + }, 0, 5, TimeUnit.SECONDS); + } + + public void stopMonitoring() { + monitor.shutdown(); + } +} +``` + +### 7.3 优雅关闭线程池 + +```java +public class GracefulShutdown { + public static void shutdownThreadPool(ExecutorService executor) { + executor.shutdown(); // 不再接受新任务 + + try { + // 等待已提交任务完成 + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + System.out.println("强制关闭线程池"); + executor.shutdownNow(); // 强制关闭 + + // 等待任务响应中断 + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + System.err.println("线程池未能正常关闭"); + } + } + } catch (InterruptedException e) { + System.out.println("关闭过程被中断,强制关闭"); + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} +``` + +## 8. 最佳实践 + +### 8.1 参数配置建议 + +```java +public class ThreadPoolBestPractices { + + // CPU密集型任务 + public static ThreadPoolExecutor createCpuIntensivePool() { + int cpuCount = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + cpuCount, // 核心线程数 = CPU核数 + cpuCount, // 最大线程数 = CPU核数 + 0L, TimeUnit.MILLISECONDS, // 无需保持空闲线程 + new LinkedBlockingQueue<>(100), // 有界队列 + new ThreadFactory() { + private AtomicInteger counter = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "CPU-Worker-" + counter.getAndIncrement()); + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // IO密集型任务 + public static ThreadPoolExecutor createIoIntensivePool() { + int cpuCount = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + cpuCount * 2, // 核心线程数 = CPU核数 * 2 + cpuCount * 4, // 最大线程数 = CPU核数 * 4 + 60L, TimeUnit.SECONDS, // 空闲线程保持60秒 + new LinkedBlockingQueue<>(200), // 较大的队列 + new ThreadFactory() { + private AtomicInteger counter = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "IO-Worker-" + counter.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // 混合型任务 + public static ThreadPoolExecutor createMixedPool() { + int cpuCount = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + cpuCount + 1, // 核心线程数 = CPU核数 + 1 + cpuCount * 2 + 1, // 最大线程数 = (CPU核数 + 1) * 2 + 60L, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(150), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.AbortPolicy() + ); + } +} +``` + +### 8.2 常见问题和解决方案 + +```java +public class ThreadPoolTroubleshooting { + + // 问题1:内存泄漏 + public static void avoidMemoryLeak() { + ThreadPoolExecutor executor = new ThreadPoolExecutor( + 2, 4, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(10), // 使用有界队列 + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + + // 确保在应用关闭时正确关闭线程池 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("关闭线程池..."); + GracefulShutdown.shutdownThreadPool(executor); + })); + } + + // 问题2:任务执行异常处理 + public static class SafeThreadPoolExecutor extends ThreadPoolExecutor { + public SafeThreadPoolExecutor(int corePoolSize, int maximumPoolSize, + long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + try { + ((Future) r).get(); + } catch (CancellationException ce) { + t = ce; + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + if (t != null) { + System.err.println("任务执行异常: " + t.getMessage()); + t.printStackTrace(); + // 可以添加告警、日志记录等 + } + } + } + + // 问题3:线程池大小动态调整 + public static void dynamicAdjustment(ThreadPoolExecutor executor) { + // 监控系统负载,动态调整线程池大小 + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(() -> { + double cpuUsage = getCpuUsage(); // 获取CPU使用率 + int queueSize = executor.getQueue().size(); + + if (cpuUsage > 0.8 && queueSize > 50) { + // CPU使用率高且队列积压,增加线程 + int currentMax = executor.getMaximumPoolSize(); + executor.setMaximumPoolSize(Math.min(currentMax + 2, 20)); + System.out.println("增加最大线程数到: " + executor.getMaximumPoolSize()); + } else if (cpuUsage < 0.3 && queueSize < 10) { + // CPU使用率低且队列空闲,减少线程 + int currentMax = executor.getMaximumPoolSize(); + int coreSize = executor.getCorePoolSize(); + executor.setMaximumPoolSize(Math.max(currentMax - 1, coreSize)); + System.out.println("减少最大线程数到: " + executor.getMaximumPoolSize()); + } + }, 0, 30, TimeUnit.SECONDS); + } + + private static double getCpuUsage() { + // 简化的CPU使用率获取,实际应用中可以使用JMX + return Math.random(); + } +} +``` + +## 9. 总结 + +### 9.1 核心要点 + +1. **理解参数含义**:正确配置核心线程数、最大线程数、队列大小等参数 +2. **选择合适队列**:根据业务场景选择有界或无界队列 +3. **制定拒绝策略**:根据业务需求选择或自定义拒绝策略 +4. **监控线程池状态**:实时监控线程池的运行状态和性能指标 +5. **优雅关闭**:确保应用关闭时正确关闭线程池 + +### 9.2 性能调优建议 + +``` +调优步骤: +1. 分析任务特性(CPU密集型 vs IO密集型) +2. 确定合理的线程数量 +3. 选择合适的队列类型和大小 +4. 配置适当的拒绝策略 +5. 添加监控和告警 +6. 压力测试验证配置 +7. 根据监控数据持续优化 +``` + +### 9.3 注意事项 + +- **避免使用Executors工具类**:推荐手动创建ThreadPoolExecutor +- **合理设置队列大小**:避免无界队列导致内存溢出 +- **处理任务异常**:重写afterExecute方法处理任务执行异常 +- **线程池复用**:避免频繁创建和销毁线程池 +- **资源清理**:确保线程池正确关闭,避免资源泄漏 + +通过深入理解线程池的实现原理和最佳实践,可以更好地利用线程池提升应用性能和稳定性。 + + diff --git a/docs/aThread/javaThreadPoolUse.md b/docs/aThread/javaThreadPoolUse.md new file mode 100644 index 000000000..8c2eeca6c --- /dev/null +++ b/docs/aThread/javaThreadPoolUse.md @@ -0,0 +1,573 @@ +--- +title: java线程池使用 +author: 哪吒 +date: '2023-06-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Java线程池使用指南 + +## 1. 线程池基础使用 + +### 1.1 创建线程池的方式 + +#### 方式一:使用Executors工具类(不推荐) + +```java +// 1. 固定大小线程池 +ExecutorService fixedPool = Executors.newFixedThreadPool(5); + +// 2. 缓存线程池 +ExecutorService cachedPool = Executors.newCachedThreadPool(); + +// 3. 单线程池 +ExecutorService singlePool = Executors.newSingleThreadExecutor(); + +// 4. 定时任务线程池 +ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3); +``` + +**为什么不推荐使用Executors?** + +``` +问题分析: +┌─────────────────────────────────────────────────────────┐ +│ newFixedThreadPool & newSingleThreadExecutor │ +│ ↓ │ +│ 使用LinkedBlockingQueue(无界队列) │ +│ ↓ │ +│ 可能导致内存溢出(OOM) │ +│ │ +│ newCachedThreadPool │ +│ ↓ │ +│ 最大线程数为Integer.MAX_VALUE │ +│ ↓ │ +│ 可能创建大量线程导致系统崩溃 │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 方式二:手动创建ThreadPoolExecutor(推荐) + +```java +public class ThreadPoolFactory { + + /** + * 创建标准线程池 + */ + public static ThreadPoolExecutor createStandardPool() { + return new ThreadPoolExecutor( + 5, // 核心线程数 + 10, // 最大线程数 + 60L, // 空闲时间 + TimeUnit.SECONDS, // 时间单位 + new ArrayBlockingQueue<>(100), // 工作队列 + new ThreadFactory() { // 线程工厂 + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "CustomPool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); // 设置为用户线程 + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 + ); + } +} +``` + +## 2. 常用线程池类型及应用场景 + +### 2.1 CPU密集型任务线程池 + +```java +public class CPUIntensiveThreadPool { + + /** + * CPU密集型任务线程池配置 + * 核心线程数 = CPU核心数 + 1 + */ + public static ThreadPoolExecutor createCPUIntensivePool() { + int corePoolSize = Runtime.getRuntime().availableProcessors() + 1; + return new ThreadPoolExecutor( + corePoolSize, + corePoolSize, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(50), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "CPU-Pool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.AbortPolicy() + ); + } + + // 使用示例:计算密集型任务 + public static void main(String[] args) { + ThreadPoolExecutor executor = createCPUIntensivePool(); + + // 提交计算任务 + for (int i = 0; i < 10; i++) { + final int taskId = i; + executor.submit(() -> { + long result = fibonacci(35); // 计算斐波那契数列 + System.out.println("任务" + taskId + "计算结果:" + result + + " - 线程:" + Thread.currentThread().getName()); + }); + } + + shutdownGracefully(executor); + } + + private static long fibonacci(int n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); + } + + private static void shutdownGracefully(ExecutorService executor) { + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} +``` + +### 2.2 IO密集型任务线程池 + +```java +public class IOIntensiveThreadPool { + + /** + * IO密集型任务线程池配置 + * 核心线程数 = CPU核心数 * 2 + */ + public static ThreadPoolExecutor createIOIntensivePool() { + int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; + return new ThreadPoolExecutor( + corePoolSize, + corePoolSize * 2, + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(200), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "IO-Pool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // 使用示例:文件读写任务 + public static void main(String[] args) { + ThreadPoolExecutor executor = createIOIntensivePool(); + + // 提交IO任务 + for (int i = 0; i < 20; i++) { + final int taskId = i; + executor.submit(() -> { + try { + // 模拟文件读写操作 + Thread.sleep(1000); // 模拟IO等待 + System.out.println("任务" + taskId + "完成文件操作 - 线程:" + + Thread.currentThread().getName()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + shutdownGracefully(executor); + } + + private static void shutdownGracefully(ExecutorService executor) { + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} + ``` + +### 2.3 定时任务线程池 + +```java +public class ScheduledThreadPoolExample { + + public static void main(String[] args) throws InterruptedException { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor( + 3, + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Scheduler-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + } + ); + + // 1. 延迟执行任务 + scheduler.schedule(() -> { + System.out.println("延迟3秒执行的任务 - " + new Date()); + }, 3, TimeUnit.SECONDS); + + // 2. 固定频率执行任务 + ScheduledFuture fixedRateTask = scheduler.scheduleAtFixedRate(() -> { + System.out.println("每2秒执行一次的任务 - " + new Date()); + }, 1, 2, TimeUnit.SECONDS); + + // 3. 固定延迟执行任务 + ScheduledFuture fixedDelayTask = scheduler.scheduleWithFixedDelay(() -> { + System.out.println("上次执行完成后延迟1秒再执行 - " + new Date()); + try { + Thread.sleep(500); // 模拟任务执行时间 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, 1, 1, TimeUnit.SECONDS); + + // 运行10秒后停止 + Thread.sleep(10000); + fixedRateTask.cancel(false); + fixedDelayTask.cancel(false); + + scheduler.shutdown(); + } +} +``` + +## 3. 线程池监控与管理 + +### 3.1 线程池状态监控 + +```java +public class ThreadPoolMonitor { + + /** + * 创建可监控的线程池 + */ + public static ThreadPoolExecutor createMonitorablePool() { + return new ThreadPoolExecutor( + 5, 10, 60L, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(100), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Monitor-Pool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ) { + @Override + protected void beforeExecute(Thread t, Runnable r) { + super.beforeExecute(t, r); + System.out.println("任务开始执行 - 线程:" + t.getName()); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t != null) { + System.err.println("任务执行异常:" + t.getMessage()); + } else { + System.out.println("任务执行完成"); + } + } + + @Override + protected void terminated() { + super.terminated(); + System.out.println("线程池已终止"); + } + }; + } + + /** + * 打印线程池状态信息 + */ + public static void printPoolStatus(ThreadPoolExecutor executor) { + System.out.println("\n=== 线程池状态信息 ==="); + System.out.println("核心线程数:" + executor.getCorePoolSize()); + System.out.println("最大线程数:" + executor.getMaximumPoolSize()); + System.out.println("当前线程数:" + executor.getPoolSize()); + System.out.println("活跃线程数:" + executor.getActiveCount()); + System.out.println("队列中任务数:" + executor.getQueue().size()); + System.out.println("已完成任务数:" + executor.getCompletedTaskCount()); + System.out.println("总任务数:" + executor.getTaskCount()); + System.out.println("是否关闭:" + executor.isShutdown()); + System.out.println("是否终止:" + executor.isTerminated()); + System.out.println("========================\n"); + } +} +``` + +### 3.2 线程池异常处理 + +```java +public class ThreadPoolExceptionHandling { + + /** + * 创建带异常处理的线程池 + */ + public static ThreadPoolExecutor createSafeThreadPool() { + return new ThreadPoolExecutor( + 5, 10, 60L, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(50), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Safe-Pool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + // 设置未捕获异常处理器 + t.setUncaughtExceptionHandler((thread, ex) -> { + System.err.println("线程 " + thread.getName() + " 发生未捕获异常:" + ex.getMessage()); + ex.printStackTrace(); + }); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + /** + * 安全任务包装器 + */ + public static Runnable wrapTask(Runnable task) { + return () -> { + try { + task.run(); + } catch (Exception e) { + System.err.println("任务执行异常:" + e.getMessage()); + e.printStackTrace(); + // 可以在这里添加异常上报逻辑 + } + }; + } + + public static void main(String[] args) { + ThreadPoolExecutor executor = createSafeThreadPool(); + + // 提交可能抛异常的任务 + executor.submit(wrapTask(() -> { + System.out.println("正常任务执行"); + })); + + executor.submit(wrapTask(() -> { + throw new RuntimeException("模拟任务异常"); + })); + + // 使用Future处理异常 + Future future = executor.submit(() -> { + throw new RuntimeException("Future异常"); + }); + + try { + future.get(); + } catch (ExecutionException e) { + System.err.println("通过Future捕获异常:" + e.getCause().getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + executor.shutdown(); + } +} +``` + +## 4. 最佳实践与注意事项 + +### 4.1 线程池参数配置指南 + +```java +public class ThreadPoolConfigGuide { + + /** + * 根据任务类型配置线程池 + */ + public static class ThreadPoolConfigurator { + + // CPU密集型任务配置 + public static ThreadPoolExecutor forCPUIntensive() { + int processors = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + processors, // 核心线程数 = CPU核心数 + processors, // 最大线程数 = CPU核心数 + 0L, TimeUnit.MILLISECONDS, // 无空闲时间 + new ArrayBlockingQueue<>(processors * 2), // 队列大小适中 + new ThreadPoolExecutor.AbortPolicy() + ); + } + + // IO密集型任务配置 + public static ThreadPoolExecutor forIOIntensive() { + int processors = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + processors * 2, // 核心线程数 = CPU核心数 * 2 + processors * 4, // 最大线程数 = CPU核心数 * 4 + 60L, TimeUnit.SECONDS, // 空闲60秒回收 + new LinkedBlockingQueue<>(1000), // 较大的队列 + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // 混合型任务配置 + public static ThreadPoolExecutor forMixed() { + int processors = Runtime.getRuntime().availableProcessors(); + return new ThreadPoolExecutor( + processors + 1, // 核心线程数 = CPU核心数 + 1 + processors * 2, // 最大线程数 = CPU核心数 * 2 + 60L, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(200), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + } +} +``` + +### 4.2 常见问题与解决方案 + +```java +public class ThreadPoolTroubleshooting { + + /** + * 问题1:线程池任务积压 + * 解决方案:动态调整线程池大小 + */ + public static class DynamicThreadPool { + private final ThreadPoolExecutor executor; + private final ScheduledExecutorService monitor; + + public DynamicThreadPool(ThreadPoolExecutor executor) { + this.executor = executor; + this.monitor = Executors.newSingleThreadScheduledExecutor(); + startMonitoring(); + } + + private void startMonitoring() { + monitor.scheduleAtFixedRate(() -> { + int queueSize = executor.getQueue().size(); + int activeCount = executor.getActiveCount(); + int corePoolSize = executor.getCorePoolSize(); + int maxPoolSize = executor.getMaximumPoolSize(); + + // 队列积压严重,增加线程 + if (queueSize > 100 && activeCount >= corePoolSize * 0.8) { + int newCoreSize = Math.min(corePoolSize + 1, maxPoolSize); + if (newCoreSize > corePoolSize) { + executor.setCorePoolSize(newCoreSize); + System.out.println("增加核心线程数至:" + newCoreSize); + } + } + + // 队列空闲,减少线程 + if (queueSize < 10 && activeCount < corePoolSize * 0.5 && corePoolSize > 2) { + executor.setCorePoolSize(corePoolSize - 1); + System.out.println("减少核心线程数至:" + (corePoolSize - 1)); + } + }, 0, 30, TimeUnit.SECONDS); + } + } + + /** + * 问题2:任务执行时间过长 + * 解决方案:任务超时控制 + */ + public static class TimeoutTaskExecutor { + private final ThreadPoolExecutor executor; + + public TimeoutTaskExecutor(ThreadPoolExecutor executor) { + this.executor = executor; + } + + public T executeWithTimeout(Callable task, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + Future future = executor.submit(task); + try { + return future.get(timeout, unit); + } catch (TimeoutException e) { + future.cancel(true); // 取消任务 + throw e; + } + } + } +} +``` + +### 4.3 性能优化建议 + +``` +线程池性能优化清单: + +1. 参数配置优化 + ✓ 根据任务类型选择合适的核心线程数 + ✓ 设置合理的最大线程数和队列大小 + ✓ 选择适当的拒绝策略 + +2. 任务设计优化 + ✓ 避免任务执行时间过长 + ✓ 合理拆分大任务 + ✓ 避免任务间的强依赖关系 + +3. 监控与调优 + ✓ 定期监控线程池状态 + ✓ 记录任务执行时间 + ✓ 根据监控数据调整参数 + +4. 资源管理 + ✓ 及时关闭线程池 + ✓ 避免创建过多线程池 + ✓ 合理设置线程优先级 +``` + +## 5. 总结 + +Java线程池是并发编程的重要工具,正确使用线程池可以: + +- **提高性能**:减少线程创建和销毁的开销 +- **控制资源**:限制并发线程数量,避免系统资源耗尽 +- **提高响应性**:复用线程,快速响应任务请求 +- **便于管理**:统一管理线程生命周期 + +**关键要点**: +1. 避免使用`Executors`创建线程池,推荐手动创建`ThreadPoolExecutor` +2. 根据任务类型(CPU密集型/IO密集型)合理配置参数 +3. 实施有效的监控和异常处理机制 +4. 注意线程池的优雅关闭 +5. 定期评估和调优线程池配置 + +通过遵循这些最佳实践,可以充分发挥线程池的优势,构建高性能、稳定的并发应用程序。 + + diff --git a/docs/aThread/javaThreadSkill.md b/docs/aThread/javaThreadSkill.md new file mode 100644 index 000000000..a9ce50191 --- /dev/null +++ b/docs/aThread/javaThreadSkill.md @@ -0,0 +1,2087 @@ +--- +title: java多线程编程技巧 +author: 哪吒 +date: '2023-06-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Java多线程编程技巧 + +## 1. 线程创建与管理技巧 + +### 1.1 优雅的线程创建方式 + +#### 使用Lambda表达式简化线程创建 + +```java +public class ThreadCreationTips { + + public static void main(String[] args) { + // 传统方式 + Thread thread1 = new Thread(new Runnable() { + @Override + public void run() { + System.out.println("传统方式创建线程"); + } + }); + + // Lambda表达式方式(推荐) + Thread thread2 = new Thread(() -> { + System.out.println("Lambda方式创建线程"); + }); + + // 方法引用方式 + Thread thread3 = new Thread(ThreadCreationTips::doWork); + + // 带名称的线程(便于调试) + Thread namedThread = new Thread(() -> { + System.out.println("当前线程:" + Thread.currentThread().getName()); + }, "MyWorkerThread"); + + thread1.start(); + thread2.start(); + thread3.start(); + namedThread.start(); + } + + private static void doWork() { + System.out.println("方法引用方式创建线程"); + } +} +``` + +#### 线程工厂模式 + +```java +public class CustomThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final boolean daemon; + private final int priority; + + public CustomThreadFactory(String namePrefix, boolean daemon, int priority) { + this.namePrefix = namePrefix; + this.daemon = daemon; + this.priority = priority; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement()); + t.setDaemon(daemon); + t.setPriority(priority); + + // 设置未捕获异常处理器 + t.setUncaughtExceptionHandler((thread, ex) -> { + System.err.println("线程 " + thread.getName() + " 发生异常:" + ex.getMessage()); + ex.printStackTrace(); + }); + + return t; + } + + // 使用示例 + public static void main(String[] args) { + ThreadFactory factory = new CustomThreadFactory("Worker", false, Thread.NORM_PRIORITY); + + for (int i = 0; i < 3; i++) { + Thread thread = factory.newThread(() -> { + System.out.println("执行任务 - " + Thread.currentThread().getName()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + thread.start(); + } + } +} +``` + +### 1.2 线程状态监控技巧 + +```java +public class ThreadMonitoringTips { + + /** + * 监控线程状态变化 + */ + public static void monitorThreadState(Thread thread) { + new Thread(() -> { + Thread.State lastState = null; + while (thread.isAlive() || thread.getState() != Thread.State.TERMINATED) { + Thread.State currentState = thread.getState(); + if (currentState != lastState) { + System.out.println("线程 " + thread.getName() + + " 状态变化:" + lastState + " -> " + currentState); + lastState = currentState; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "StateMonitor").start(); + } + + /** + * 获取线程详细信息 + */ + public static void printThreadInfo(Thread thread) { + System.out.println("=== 线程信息 ==="); + System.out.println("名称:" + thread.getName()); + System.out.println("ID:" + thread.getId()); + System.out.println("状态:" + thread.getState()); + System.out.println("优先级:" + thread.getPriority()); + System.out.println("是否守护线程:" + thread.isDaemon()); + System.out.println("是否存活:" + thread.isAlive()); + System.out.println("是否被中断:" + thread.isInterrupted()); + System.out.println("线程组:" + thread.getThreadGroup().getName()); + } + + public static void main(String[] args) throws InterruptedException { + Thread worker = new Thread(() -> { + try { + System.out.println("开始工作"); + Thread.sleep(3000); + System.out.println("工作完成"); + } catch (InterruptedException e) { + System.out.println("工作被中断"); + Thread.currentThread().interrupt(); + } + }, "WorkerThread"); + + // 开始监控 + monitorThreadState(worker); + + // 启动线程 + worker.start(); + + // 打印线程信息 + Thread.sleep(500); + printThreadInfo(worker); + + // 等待线程完成 + worker.join(); + } +} +``` + +## 2. 线程同步技巧 + +### 2.1 锁的高级使用技巧 + +#### 读写锁优化并发性能 + +```java +public class ReadWriteLockTips { + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private final Lock writeLock = lock.writeLock(); + private final Map cache = new HashMap<>(); + + /** + * 读操作(支持并发) + */ + public String get(String key) { + readLock.lock(); + try { + System.out.println("读取数据:" + key + " - 线程:" + Thread.currentThread().getName()); + Thread.sleep(100); // 模拟读取耗时 + return cache.get(key); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + readLock.unlock(); + } + } + + /** + * 写操作(独占) + */ + public void put(String key, String value) { + writeLock.lock(); + try { + System.out.println("写入数据:" + key + "=" + value + " - 线程:" + Thread.currentThread().getName()); + Thread.sleep(200); // 模拟写入耗时 + cache.put(key, value); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + writeLock.unlock(); + } + } + + /** + * 锁降级示例 + */ + public String getOrCompute(String key, Supplier supplier) { + readLock.lock(); + try { + String value = cache.get(key); + if (value != null) { + return value; + } + } finally { + readLock.unlock(); + } + + // 需要写入,获取写锁 + writeLock.lock(); + try { + // 双重检查 + String value = cache.get(key); + if (value == null) { + value = supplier.get(); + cache.put(key, value); + } + + // 锁降级:在释放写锁之前获取读锁 + readLock.lock(); + return value; + } finally { + writeLock.unlock(); + } + // 注意:这里读锁还没有释放,需要在外部释放 + } + + public static void main(String[] args) throws InterruptedException { + ReadWriteLockTips cache = new ReadWriteLockTips(); + + // 启动多个读线程 + for (int i = 0; i < 3; i++) { + new Thread(() -> { + for (int j = 0; j < 3; j++) { + cache.get("key" + j); + } + }, "Reader-" + i).start(); + } + + // 启动写线程 + new Thread(() -> { + for (int i = 0; i < 3; i++) { + cache.put("key" + i, "value" + i); + } + }, "Writer").start(); + + Thread.sleep(5000); + } +} +``` + +#### 条件变量的巧妙使用 + +```java +public class ConditionTips { + private final Lock lock = new ReentrantLock(); + private final Condition notEmpty = lock.newCondition(); + private final Condition notFull = lock.newCondition(); + private final Queue queue = new LinkedList<>(); + private final int capacity; + + public ConditionTips(int capacity) { + this.capacity = capacity; + } + + /** + * 生产者 + */ + public void produce(String item) throws InterruptedException { + lock.lock(); + try { + // 等待队列不满 + while (queue.size() >= capacity) { + System.out.println("队列已满,生产者等待..."); + notFull.await(); + } + + queue.offer(item); + System.out.println("生产:" + item + ",队列大小:" + queue.size()); + + // 通知消费者 + notEmpty.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * 消费者 + */ + public String consume() throws InterruptedException { + lock.lock(); + try { + // 等待队列不空 + while (queue.isEmpty()) { + System.out.println("队列为空,消费者等待..."); + notEmpty.await(); + } + + String item = queue.poll(); + System.out.println("消费:" + item + ",队列大小:" + queue.size()); + + // 通知生产者 + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + /** + * 带超时的消费 + */ + public String consumeWithTimeout(long timeout, TimeUnit unit) throws InterruptedException { + lock.lock(); + try { + long deadline = System.nanoTime() + unit.toNanos(timeout); + + while (queue.isEmpty()) { + if (!notEmpty.awaitUntil(new Date(System.currentTimeMillis() + unit.toMillis(timeout)))) { + System.out.println("消费超时"); + return null; + } + } + + String item = queue.poll(); + System.out.println("消费:" + item); + notFull.signalAll(); + return item; + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) throws InterruptedException { + ConditionTips buffer = new ConditionTips(3); + + // 启动生产者 + Thread producer = new Thread(() -> { + try { + for (int i = 0; i < 10; i++) { + buffer.produce("Item-" + i); + Thread.sleep(500); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Producer"); + + // 启动消费者 + Thread consumer = new Thread(() -> { + try { + for (int i = 0; i < 10; i++) { + buffer.consume(); + Thread.sleep(1000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Consumer"); + + producer.start(); + consumer.start(); + + producer.join(); + consumer.join(); + } +} +``` + +### 2.2 原子操作技巧 + +#### 原子类的高效使用 + +```java +public class AtomicTips { + private final AtomicInteger counter = new AtomicInteger(0); + private final AtomicReference status = new AtomicReference<>("INIT"); + private final AtomicBoolean flag = new AtomicBoolean(false); + + /** + * 原子递增与条件更新 + */ + public void atomicOperations() { + // 原子递增 + int newValue = counter.incrementAndGet(); + System.out.println("递增后的值:" + newValue); + + // 条件更新(CAS操作) + boolean updated = status.compareAndSet("INIT", "RUNNING"); + System.out.println("状态更新成功:" + updated + ",当前状态:" + status.get()); + + // 原子更新并获取旧值 + int oldValue = counter.getAndUpdate(x -> x * 2); + System.out.println("更新前的值:" + oldValue + ",更新后的值:" + counter.get()); + } + + /** + * 自定义原子操作 + */ + public void customAtomicOperation() { + // 使用updateAndGet进行复杂计算 + int result = counter.updateAndGet(current -> { + // 复杂的业务逻辑 + if (current < 100) { + return current + 10; + } else { + return current / 2; + } + }); + System.out.println("自定义操作结果:" + result); + } + + /** + * 原子数组操作 + */ + public static void atomicArrayExample() { + AtomicIntegerArray array = new AtomicIntegerArray(10); + + // 并发更新数组元素 + IntStream.range(0, 10).parallel().forEach(i -> { + array.set(i, i * i); + System.out.println("设置 array[" + i + "] = " + array.get(i)); + }); + + // 原子累加 + int sum = IntStream.range(0, array.length()) + .map(array::get) + .sum(); + System.out.println("数组元素总和:" + sum); + } + + public static void main(String[] args) { + AtomicTips tips = new AtomicTips(); + + // 启动多个线程进行并发操作 + for (int i = 0; i < 5; i++) { + new Thread(() -> { + tips.atomicOperations(); + tips.customAtomicOperation(); + }, "Worker-" + i).start(); + } + + atomicArrayExample(); + } +} +``` + +#### LongAdder高性能计数器 + +```java +public class LongAdderTips { + private final LongAdder counter = new LongAdder(); + private final LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE); + + /** + * 高性能计数 + */ + public void performanceCount() { + // LongAdder在高并发下性能优于AtomicLong + counter.increment(); + counter.add(5); + + System.out.println("当前计数:" + counter.sum()); + } + + /** + * 自定义累加器 + */ + public void customAccumulator(long value) { + // 累加器会保持最大值 + accumulator.accumulate(value); + System.out.println("当前最大值:" + accumulator.get()); + } + + /** + * 性能对比测试 + */ + public static void performanceComparison() { + int threadCount = 10; + int operationsPerThread = 100000; + + // AtomicLong测试 + AtomicLong atomicLong = new AtomicLong(); + long startTime = System.currentTimeMillis(); + + Thread[] threads1 = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads1[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + atomicLong.incrementAndGet(); + } + }); + threads1[i].start(); + } + + for (Thread thread : threads1) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + long atomicTime = System.currentTimeMillis() - startTime; + System.out.println("AtomicLong耗时:" + atomicTime + "ms,结果:" + atomicLong.get()); + + // LongAdder测试 + LongAdder longAdder = new LongAdder(); + startTime = System.currentTimeMillis(); + + Thread[] threads2 = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads2[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + longAdder.increment(); + } + }); + threads2[i].start(); + } + + for (Thread thread : threads2) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + long adderTime = System.currentTimeMillis() - startTime; + System.out.println("LongAdder耗时:" + adderTime + "ms,结果:" + longAdder.sum()); + System.out.println("性能提升:" + ((double)(atomicTime - adderTime) / atomicTime * 100) + "%"); + } + + public static void main(String[] args) { + LongAdderTips tips = new LongAdderTips(); + + // 并发计数测试 + for (int i = 0; i < 5; i++) { + new Thread(() -> { + for (int j = 0; j < 10; j++) { + tips.performanceCount(); + tips.customAccumulator(ThreadLocalRandom.current().nextLong(1, 100)); + } + }).start(); + } + + // 性能对比 + performanceComparison(); + } +} +``` + +## 3. 并发集合使用技巧 + +### 3.1 ConcurrentHashMap高级用法 + +```java +public class ConcurrentHashMapTips { + private final ConcurrentHashMap map = new ConcurrentHashMap<>(); + private final ConcurrentHashMap counters = new ConcurrentHashMap<>(); + + /** + * 原子操作方法 + */ + public void atomicOperations() { + // 原子递增 + map.compute("count", (key, val) -> val == null ? 1 : val + 1); + + // 条件更新 + map.computeIfAbsent("init", k -> 0); + map.computeIfPresent("count", (k, v) -> v * 2); + + // 合并操作 + map.merge("total", 10, Integer::sum); + + System.out.println("当前map状态:" + map); + } + + /** + * 高效计数器实现 + */ + public void efficientCounter(String key) { + // 使用LongAdder作为值,避免CAS竞争 + counters.computeIfAbsent(key, k -> new LongAdder()).increment(); + } + + /** + * 批量操作 + */ + public void batchOperations() { + // 并行遍历 + map.forEach(1, (key, value) -> { + System.out.println("处理:" + key + " = " + value); + }); + + // 并行搜索 + String result = map.search(1, (key, value) -> { + return value > 5 ? key : null; + }); + System.out.println("搜索结果:" + result); + + // 并行归约 + Integer sum = map.reduce(1, + (key, value) -> value, // 转换函数 + Integer::sum); // 归约函数 + System.out.println("所有值的和:" + sum); + } + + /** + * 分段锁演示 + */ + public void segmentLockDemo() { + // ConcurrentHashMap内部使用分段锁,可以并发写入不同段 + int threadCount = 10; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < 100; j++) { + String key = "thread-" + threadId + "-key-" + j; + map.put(key, threadId * 100 + j); + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + System.out.println("最终map大小:" + map.size()); + } + + public static void main(String[] args) { + ConcurrentHashMapTips tips = new ConcurrentHashMapTips(); + + // 并发操作测试 + for (int i = 0; i < 5; i++) { + new Thread(() -> { + tips.atomicOperations(); + tips.efficientCounter("counter-" + Thread.currentThread().getName()); + }).start(); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + tips.batchOperations(); + tips.segmentLockDemo(); + + // 打印计数器结果 + tips.counters.forEach((key, adder) -> { + System.out.println(key + ": " + adder.sum()); + }); + } +} +``` + +### 3.2 阻塞队列的巧妙应用 + +```java +public class BlockingQueueTips { + + /** + * 优先级队列实现任务调度 + */ + public static class PriorityTaskScheduler { + private final PriorityBlockingQueue taskQueue = new PriorityBlockingQueue<>(); + private final ExecutorService executor = Executors.newFixedThreadPool(3); + private volatile boolean running = true; + + public static class Task implements Comparable { + private final String name; + private final int priority; + private final Runnable action; + + public Task(String name, int priority, Runnable action) { + this.name = name; + this.priority = priority; + this.action = action; + } + + @Override + public int compareTo(Task other) { + // 优先级高的任务先执行(数字越小优先级越高) + return Integer.compare(this.priority, other.priority); + } + + public void execute() { + System.out.println("执行任务:" + name + ",优先级:" + priority); + action.run(); + } + } + + public void start() { + // 启动任务处理线程 + for (int i = 0; i < 3; i++) { + executor.submit(() -> { + while (running || !taskQueue.isEmpty()) { + try { + Task task = taskQueue.poll(1, TimeUnit.SECONDS); + if (task != null) { + task.execute(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + } + } + + public void submitTask(String name, int priority, Runnable action) { + taskQueue.offer(new Task(name, priority, action)); + } + + public void shutdown() { + running = false; + executor.shutdown(); + } + } + + /** + * 延迟队列实现定时任务 + */ + public static class DelayedTaskScheduler { + private final DelayQueue delayQueue = new DelayQueue<>(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private volatile boolean running = true; + + public static class DelayedTask implements Delayed { + private final String name; + private final long executeTime; + private final Runnable action; + + public DelayedTask(String name, long delayMs, Runnable action) { + this.name = name; + this.executeTime = System.currentTimeMillis() + delayMs; + this.action = action; + } + + @Override + public long getDelay(TimeUnit unit) { + long remaining = executeTime - System.currentTimeMillis(); + return unit.convert(remaining, TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(Delayed other) { + return Long.compare(this.executeTime, ((DelayedTask) other).executeTime); + } + + public void execute() { + System.out.println("执行延迟任务:" + name + ",当前时间:" + + new SimpleDateFormat("HH:mm:ss").format(new Date())); + action.run(); + } + } + + public void start() { + executor.submit(() -> { + while (running || !delayQueue.isEmpty()) { + try { + DelayedTask task = delayQueue.take(); + task.execute(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + } + + public void scheduleTask(String name, long delayMs, Runnable action) { + delayQueue.offer(new DelayedTask(name, delayMs, action)); + } + + public void shutdown() { + running = false; + executor.shutdown(); + } + } + + /** + * 交换器实现数据交换 + */ + public static void exchangerExample() { + Exchanger exchanger = new Exchanger<>(); + + // 生产者线程 + Thread producer = new Thread(() -> { + try { + for (int i = 0; i < 3; i++) { + String data = "Data-" + i; + System.out.println("生产者准备交换:" + data); + String received = exchanger.exchange(data); + System.out.println("生产者收到:" + received); + Thread.sleep(1000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Producer"); + + // 消费者线程 + Thread consumer = new Thread(() -> { + try { + for (int i = 0; i < 3; i++) { + String response = "Response-" + i; + System.out.println("消费者准备交换:" + response); + String received = exchanger.exchange(response); + System.out.println("消费者收到:" + received); + Thread.sleep(1500); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Consumer"); + + producer.start(); + consumer.start(); + + try { + producer.join(); + consumer.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) throws InterruptedException { + // 优先级任务调度器测试 + PriorityTaskScheduler scheduler = new PriorityTaskScheduler(); + scheduler.start(); + + // 提交不同优先级的任务 + scheduler.submitTask("低优先级任务1", 3, () -> { + try { Thread.sleep(500); } catch (InterruptedException e) {} + }); + scheduler.submitTask("高优先级任务", 1, () -> { + try { Thread.sleep(300); } catch (InterruptedException e) {} + }); + scheduler.submitTask("中优先级任务", 2, () -> { + try { Thread.sleep(400); } catch (InterruptedException e) {} + }); + scheduler.submitTask("低优先级任务2", 3, () -> { + try { Thread.sleep(200); } catch (InterruptedException e) {} + }); + + Thread.sleep(3000); + scheduler.shutdown(); + + // 延迟任务调度器测试 + DelayedTaskScheduler delayScheduler = new DelayedTaskScheduler(); + delayScheduler.start(); + + System.out.println("当前时间:" + new SimpleDateFormat("HH:mm:ss").format(new Date())); + delayScheduler.scheduleTask("任务1", 2000, () -> System.out.println("任务1执行完成")); + delayScheduler.scheduleTask("任务2", 1000, () -> System.out.println("任务2执行完成")); + delayScheduler.scheduleTask("任务3", 3000, () -> System.out.println("任务3执行完成")); + + Thread.sleep(5000); + delayScheduler.shutdown(); + + // 交换器示例 + System.out.println("\n=== 交换器示例 ==="); + exchangerExample(); + } + } + ``` + +## 4. 线程中断与异常处理技巧 + +### 4.1 优雅的线程中断处理 + +```java +public class InterruptionTips { + + /** + * 正确的中断处理方式 + */ + public static class InterruptibleTask implements Runnable { + private volatile boolean running = true; + + @Override + public void run() { + try { + while (running && !Thread.currentThread().isInterrupted()) { + // 执行业务逻辑 + doWork(); + + // 检查中断状态 + if (Thread.interrupted()) { + System.out.println("检测到中断信号,准备退出"); + break; + } + } + } catch (InterruptedException e) { + System.out.println("线程被中断:" + e.getMessage()); + // 重新设置中断状态 + Thread.currentThread().interrupt(); + } finally { + cleanup(); + System.out.println("线程清理完成"); + } + } + + private void doWork() throws InterruptedException { + // 模拟可中断的工作 + System.out.println("执行工作 - " + Thread.currentThread().getName()); + Thread.sleep(1000); // 这里会响应中断 + } + + private void cleanup() { + // 清理资源 + System.out.println("清理资源"); + } + + public void stop() { + running = false; + } + } + + /** + * 带超时的中断处理 + */ + public static class TimeoutInterruptTask { + private final CountDownLatch latch = new CountDownLatch(1); + + public boolean executeWithTimeout(long timeoutMs) { + Thread worker = new Thread(() -> { + try { + // 模拟长时间运行的任务 + for (int i = 0; i < 10; i++) { + if (Thread.currentThread().isInterrupted()) { + System.out.println("任务被中断"); + return; + } + Thread.sleep(500); + System.out.println("处理步骤:" + (i + 1)); + } + System.out.println("任务完成"); + } catch (InterruptedException e) { + System.out.println("任务被中断:" + e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + latch.countDown(); + } + }); + + worker.start(); + + try { + // 等待任务完成或超时 + boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); + if (!completed) { + System.out.println("任务超时,中断执行"); + worker.interrupt(); + // 再等待一段时间确保线程退出 + latch.await(1000, TimeUnit.MILLISECONDS); + } + return completed; + } catch (InterruptedException e) { + worker.interrupt(); + Thread.currentThread().interrupt(); + return false; + } + } + } + + /** + * 中断传播示例 + */ + public static void interruptPropagationExample() { + Thread parentThread = new Thread(() -> { + Thread childThread = new Thread(() -> { + try { + System.out.println("子线程开始工作"); + Thread.sleep(5000); + System.out.println("子线程工作完成"); + } catch (InterruptedException e) { + System.out.println("子线程被中断"); + Thread.currentThread().interrupt(); + } + }, "ChildThread"); + + childThread.start(); + + try { + System.out.println("父线程等待子线程"); + childThread.join(); + System.out.println("父线程完成"); + } catch (InterruptedException e) { + System.out.println("父线程被中断,中断子线程"); + childThread.interrupt(); + Thread.currentThread().interrupt(); + } + }, "ParentThread"); + + parentThread.start(); + + // 2秒后中断父线程 + try { + Thread.sleep(2000); + parentThread.interrupt(); + parentThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) throws InterruptedException { + // 基本中断处理 + System.out.println("=== 基本中断处理 ==="); + Thread task1 = new Thread(new InterruptibleTask(), "Task1"); + task1.start(); + + Thread.sleep(3000); + task1.interrupt(); + task1.join(); + + // 超时中断处理 + System.out.println("\n=== 超时中断处理 ==="); + TimeoutInterruptTask timeoutTask = new TimeoutInterruptTask(); + boolean completed = timeoutTask.executeWithTimeout(3000); + System.out.println("任务是否完成:" + completed); + + // 中断传播 + System.out.println("\n=== 中断传播 ==="); + interruptPropagationExample(); + } +} +``` + +### 4.2 线程异常处理最佳实践 + +```java +public class ThreadExceptionHandling { + + /** + * 自定义未捕获异常处理器 + */ + public static class CustomUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + @Override + public void uncaughtException(Thread t, Throwable e) { + System.err.println("线程 " + t.getName() + " 发生未捕获异常:"); + e.printStackTrace(); + + // 记录日志 + logException(t, e); + + // 根据异常类型决定是否重启线程 + if (shouldRestartThread(e)) { + restartThread(t); + } + } + + private void logException(Thread thread, Throwable exception) { + // 实际项目中应该使用日志框架 + System.err.println("[ERROR] Thread: " + thread.getName() + + ", Exception: " + exception.getClass().getSimpleName() + + ", Message: " + exception.getMessage()); + } + + private boolean shouldRestartThread(Throwable exception) { + // 根据异常类型决定是否重启 + return !(exception instanceof InterruptedException || + exception instanceof ThreadDeath); + } + + private void restartThread(Thread failedThread) { + System.out.println("重启线程:" + failedThread.getName()); + // 这里可以实现线程重启逻辑 + } + } + + /** + * 线程池异常处理 + */ + public static class ThreadPoolExceptionDemo { + private final ThreadPoolExecutor executor; + + public ThreadPoolExceptionDemo() { + this.executor = new ThreadPoolExecutor( + 2, 4, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(10), + new CustomThreadFactory("Worker", false, Thread.NORM_PRIORITY), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + + // 设置线程池的异常处理 + setupExceptionHandling(); + } + + private void setupExceptionHandling() { + // 重写afterExecute方法处理异常 + ThreadPoolExecutor customExecutor = new ThreadPoolExecutor( + 2, 4, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(10) + ) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t != null) { + System.err.println("任务执行异常:" + t.getMessage()); + t.printStackTrace(); + } + + // 如果是Future任务,需要调用get()来获取异常 + if (t == null && r instanceof Future) { + try { + ((Future) r).get(); + } catch (ExecutionException ee) { + System.err.println("Future任务异常:" + ee.getCause().getMessage()); + ee.getCause().printStackTrace(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + } + }; + } + + /** + * 安全的任务提交 + */ + public void submitSafeTask(Runnable task) { + executor.submit(() -> { + try { + task.run(); + } catch (Exception e) { + System.err.println("任务执行异常:" + e.getMessage()); + e.printStackTrace(); + // 这里可以添加重试逻辑或其他处理 + } + }); + } + + /** + * 带返回值的安全任务提交 + */ + public Future submitSafeCallable(Callable task) { + return executor.submit(() -> { + try { + return task.call(); + } catch (Exception e) { + System.err.println("Callable任务异常:" + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException("任务执行失败", e); + } + }); + } + + public void shutdown() { + executor.shutdown(); + } + } + + /** + * 异常恢复策略 + */ + public static class ExceptionRecoveryStrategy { + private final int maxRetries; + private final long retryDelayMs; + + public ExceptionRecoveryStrategy(int maxRetries, long retryDelayMs) { + this.maxRetries = maxRetries; + this.retryDelayMs = retryDelayMs; + } + + public T executeWithRetry(Callable task) throws Exception { + Exception lastException = null; + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + return task.call(); + } catch (Exception e) { + lastException = e; + System.err.println("第 " + attempt + " 次尝试失败:" + e.getMessage()); + + if (attempt < maxRetries) { + try { + Thread.sleep(retryDelayMs * attempt); // 指数退避 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("重试被中断", ie); + } + } + } + } + + throw new RuntimeException("重试 " + maxRetries + " 次后仍然失败", lastException); + } + } + + public static void main(String[] args) throws InterruptedException { + // 设置全局未捕获异常处理器 + Thread.setDefaultUncaughtExceptionHandler(new CustomUncaughtExceptionHandler()); + + // 测试未捕获异常 + Thread faultyThread = new Thread(() -> { + System.out.println("线程开始执行"); + throw new RuntimeException("模拟运行时异常"); + }, "FaultyThread"); + + faultyThread.start(); + faultyThread.join(); + + // 测试线程池异常处理 + ThreadPoolExceptionDemo poolDemo = new ThreadPoolExceptionDemo(); + + // 提交会抛异常的任务 + poolDemo.submitSafeTask(() -> { + throw new RuntimeException("任务异常"); + }); + + // 提交正常任务 + poolDemo.submitSafeTask(() -> { + System.out.println("正常任务执行"); + }); + + Thread.sleep(2000); + poolDemo.shutdown(); + + // 测试异常恢复策略 + ExceptionRecoveryStrategy recovery = new ExceptionRecoveryStrategy(3, 1000); + + try { + String result = recovery.executeWithRetry(() -> { + // 模拟不稳定的服务 + if (Math.random() < 0.7) { + throw new RuntimeException("服务暂时不可用"); + } + return "成功结果"; + }); + System.out.println("最终结果:" + result); + } catch (Exception e) { + System.err.println("最终失败:" + e.getMessage()); + } + } +} +``` + +## 5. 性能优化与调试技巧 + +### 5.1 线程性能监控 + +```java +public class ThreadPerformanceMonitoring { + + /** + * 线程性能监控器 + */ + public static class ThreadPerformanceMonitor { + private final ThreadMXBean threadMXBean; + private final MemoryMXBean memoryMXBean; + private final ScheduledExecutorService scheduler; + + public ThreadPerformanceMonitor() { + this.threadMXBean = ManagementFactory.getThreadMXBean(); + this.memoryMXBean = ManagementFactory.getMemoryMXBean(); + this.scheduler = Executors.newScheduledThreadPool(1); + + // 启用线程CPU时间测量 + if (threadMXBean.isThreadCpuTimeSupported()) { + threadMXBean.setThreadCpuTimeEnabled(true); + } + } + + /** + * 开始监控 + */ + public void startMonitoring(long intervalSeconds) { + scheduler.scheduleAtFixedRate(this::printThreadStats, + 0, intervalSeconds, TimeUnit.SECONDS); + } + + /** + * 打印线程统计信息 + */ + private void printThreadStats() { + System.out.println("\n=== 线程性能统计 ==="); + System.out.println("活跃线程数:" + threadMXBean.getThreadCount()); + System.out.println("守护线程数:" + threadMXBean.getDaemonThreadCount()); + System.out.println("峰值线程数:" + threadMXBean.getPeakThreadCount()); + System.out.println("总启动线程数:" + threadMXBean.getTotalStartedThreadCount()); + + // 内存使用情况 + MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); + System.out.println("堆内存使用:" + formatBytes(heapUsage.getUsed()) + + "/" + formatBytes(heapUsage.getMax())); + + // 检查死锁 + long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); + if (deadlockedThreads != null) { + System.err.println("检测到死锁线程:" + Arrays.toString(deadlockedThreads)); + } + + // 显示CPU使用率最高的线程 + showTopCpuThreads(5); + } + + /** + * 显示CPU使用率最高的线程 + */ + private void showTopCpuThreads(int topN) { + long[] threadIds = threadMXBean.getAllThreadIds(); + List threadCpuInfos = new ArrayList<>(); + + for (long threadId : threadIds) { + ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId); + if (threadInfo != null) { + long cpuTime = threadMXBean.getThreadCpuTime(threadId); + threadCpuInfos.add(new ThreadCpuInfo(threadInfo.getThreadName(), cpuTime)); + } + } + + threadCpuInfos.sort((a, b) -> Long.compare(b.cpuTime, a.cpuTime)); + + System.out.println("\nCPU使用率最高的 " + topN + " 个线程:"); + threadCpuInfos.stream() + .limit(topN) + .forEach(info -> System.out.println(info.threadName + ": " + + formatNanos(info.cpuTime))); + } + + private static class ThreadCpuInfo { + final String threadName; + final long cpuTime; + + ThreadCpuInfo(String threadName, long cpuTime) { + this.threadName = threadName; + this.cpuTime = cpuTime; + } + } + + private String formatBytes(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024) + " KB"; + return (bytes / (1024 * 1024)) + " MB"; + } + + private String formatNanos(long nanos) { + return (nanos / 1_000_000) + " ms"; + } + + public void shutdown() { + scheduler.shutdown(); + } + } + + /** + * 线程池性能监控 + */ + public static class ThreadPoolMonitor { + private final ThreadPoolExecutor executor; + private final ScheduledExecutorService monitor; + + public ThreadPoolMonitor(ThreadPoolExecutor executor) { + this.executor = executor; + this.monitor = Executors.newScheduledThreadPool(1); + } + + public void startMonitoring(long intervalSeconds) { + monitor.scheduleAtFixedRate(this::printPoolStats, + 0, intervalSeconds, TimeUnit.SECONDS); + } + + private void printPoolStats() { + System.out.println("\n=== 线程池性能统计 ==="); + System.out.println("核心线程数:" + executor.getCorePoolSize()); + System.out.println("最大线程数:" + executor.getMaximumPoolSize()); + System.out.println("当前线程数:" + executor.getPoolSize()); + System.out.println("活跃线程数:" + executor.getActiveCount()); + System.out.println("历史最大线程数:" + executor.getLargestPoolSize()); + System.out.println("已完成任务数:" + executor.getCompletedTaskCount()); + System.out.println("总任务数:" + executor.getTaskCount()); + System.out.println("队列大小:" + executor.getQueue().size()); + + // 计算线程池利用率 + double utilization = (double) executor.getActiveCount() / executor.getPoolSize(); + System.out.println("线程池利用率:" + String.format("%.2f%%", utilization * 100)); + + // 检查是否需要调整线程池大小 + if (executor.getQueue().size() > executor.getCorePoolSize() * 2) { + System.out.println("⚠️ 队列积压严重,建议增加线程数"); + } + + if (utilization < 0.5 && executor.getPoolSize() > executor.getCorePoolSize()) { + System.out.println("💡 线程利用率较低,可以考虑减少线程数"); + } + } + + public void shutdown() { + monitor.shutdown(); + } + } + + public static void main(String[] args) throws InterruptedException { + // 启动性能监控 + ThreadPerformanceMonitor monitor = new ThreadPerformanceMonitor(); + monitor.startMonitoring(3); + + // 创建测试线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + 2, 8, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(20) + ); + + ThreadPoolMonitor poolMonitor = new ThreadPoolMonitor(executor); + poolMonitor.startMonitoring(2); + + // 提交一些测试任务 + for (int i = 0; i < 20; i++) { + final int taskId = i; + executor.submit(() -> { + try { + // 模拟CPU密集型任务 + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 2000) { + Math.sqrt(Math.random()); + } + System.out.println("任务 " + taskId + " 完成"); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + // 运行10秒后关闭 + Thread.sleep(10000); + + executor.shutdown(); + monitor.shutdown(); + poolMonitor.shutdown(); + } + } + ``` + +### 5.2 死锁检测与预防 + +```java +public class DeadlockDetectionAndPrevention { + + /** + * 死锁检测器 + */ + public static class DeadlockDetector { + private final ThreadMXBean threadMXBean; + private final ScheduledExecutorService scheduler; + + public DeadlockDetector() { + this.threadMXBean = ManagementFactory.getThreadMXBean(); + this.scheduler = Executors.newScheduledThreadPool(1); + } + + /** + * 开始死锁检测 + */ + public void startDetection(long intervalSeconds) { + scheduler.scheduleAtFixedRate(this::detectDeadlock, + 0, intervalSeconds, TimeUnit.SECONDS); + } + + /** + * 检测死锁 + */ + private void detectDeadlock() { + long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); + if (deadlockedThreads != null) { + System.err.println("\n🚨 检测到死锁!"); + ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads); + + for (ThreadInfo threadInfo : threadInfos) { + System.err.println("死锁线程:" + threadInfo.getThreadName()); + System.err.println("线程状态:" + threadInfo.getThreadState()); + System.err.println("阻塞对象:" + threadInfo.getLockName()); + System.err.println("持有锁的线程:" + threadInfo.getLockOwnerName()); + + // 打印堆栈跟踪 + StackTraceElement[] stackTrace = threadInfo.getStackTrace(); + for (StackTraceElement element : stackTrace) { + System.err.println("\t" + element.toString()); + } + System.err.println(); + } + + // 可以在这里添加死锁恢复逻辑 + handleDeadlock(deadlockedThreads); + } + } + + /** + * 处理死锁 + */ + private void handleDeadlock(long[] deadlockedThreads) { + System.err.println("尝试恢复死锁..."); + // 实际项目中可以实现更复杂的恢复策略 + // 比如中断某些线程、记录日志、发送告警等 + } + + public void shutdown() { + scheduler.shutdown(); + } + } + + /** + * 有序锁获取 - 预防死锁 + */ + public static class OrderedLocking { + private static final Object lock1 = new Object(); + private static final Object lock2 = new Object(); + + // 为锁分配唯一ID,确保按顺序获取 + private static final int LOCK1_ID = System.identityHashCode(lock1); + private static final int LOCK2_ID = System.identityHashCode(lock2); + + /** + * 按顺序获取锁,避免死锁 + */ + public static void safeOperation() { + Object firstLock = LOCK1_ID < LOCK2_ID ? lock1 : lock2; + Object secondLock = LOCK1_ID < LOCK2_ID ? lock2 : lock1; + + synchronized (firstLock) { + System.out.println(Thread.currentThread().getName() + " 获取第一个锁"); + + try { + Thread.sleep(100); // 模拟工作 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + synchronized (secondLock) { + System.out.println(Thread.currentThread().getName() + " 获取第二个锁"); + // 执行需要两个锁的操作 + System.out.println(Thread.currentThread().getName() + " 执行关键操作"); + } + } + } + } + + /** + * 超时锁获取 - 预防死锁 + */ + public static class TimeoutLocking { + private final ReentrantLock lock1 = new ReentrantLock(); + private final ReentrantLock lock2 = new ReentrantLock(); + + /** + * 使用超时机制获取锁 + */ + public boolean performOperation(long timeoutMs) { + boolean lock1Acquired = false; + boolean lock2Acquired = false; + + try { + // 尝试获取第一个锁 + lock1Acquired = lock1.tryLock(timeoutMs, TimeUnit.MILLISECONDS); + if (!lock1Acquired) { + System.out.println("获取lock1超时"); + return false; + } + + System.out.println(Thread.currentThread().getName() + " 获取lock1"); + + // 尝试获取第二个锁 + lock2Acquired = lock2.tryLock(timeoutMs, TimeUnit.MILLISECONDS); + if (!lock2Acquired) { + System.out.println("获取lock2超时"); + return false; + } + + System.out.println(Thread.currentThread().getName() + " 获取lock2"); + + // 执行需要两个锁的操作 + Thread.sleep(100); + System.out.println(Thread.currentThread().getName() + " 执行操作完成"); + + return true; + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } finally { + // 按相反顺序释放锁 + if (lock2Acquired) { + lock2.unlock(); + System.out.println(Thread.currentThread().getName() + " 释放lock2"); + } + if (lock1Acquired) { + lock1.unlock(); + System.out.println(Thread.currentThread().getName() + " 释放lock1"); + } + } + } + } + + /** + * 银行家算法 - 死锁预防 + */ + public static class BankersAlgorithm { + private final int[][] allocation; // 已分配资源 + private final int[][] max; // 最大需求 + private final int[] available; // 可用资源 + private final int processes; // 进程数 + private final int resources; // 资源类型数 + + public BankersAlgorithm(int processes, int resources) { + this.processes = processes; + this.resources = resources; + this.allocation = new int[processes][resources]; + this.max = new int[processes][resources]; + this.available = new int[resources]; + } + + /** + * 检查系统是否处于安全状态 + */ + public boolean isSafeState() { + int[][] need = calculateNeed(); + boolean[] finished = new boolean[processes]; + int[] work = available.clone(); + + int count = 0; + while (count < processes) { + boolean found = false; + + for (int p = 0; p < processes; p++) { + if (!finished[p] && canAllocate(need[p], work)) { + // 模拟进程完成,释放资源 + for (int r = 0; r < resources; r++) { + work[r] += allocation[p][r]; + } + finished[p] = true; + found = true; + count++; + break; + } + } + + if (!found) { + return false; // 无法找到安全序列 + } + } + + return true; // 找到安全序列 + } + + private int[][] calculateNeed() { + int[][] need = new int[processes][resources]; + for (int i = 0; i < processes; i++) { + for (int j = 0; j < resources; j++) { + need[i][j] = max[i][j] - allocation[i][j]; + } + } + return need; + } + + private boolean canAllocate(int[] need, int[] available) { + for (int i = 0; i < resources; i++) { + if (need[i] > available[i]) { + return false; + } + } + return true; + } + + /** + * 请求资源 + */ + public synchronized boolean requestResources(int processId, int[] request) { + // 检查请求是否超过需求 + int[][] need = calculateNeed(); + for (int i = 0; i < resources; i++) { + if (request[i] > need[processId][i]) { + System.out.println("请求超过最大需求"); + return false; + } + } + + // 检查请求是否超过可用资源 + for (int i = 0; i < resources; i++) { + if (request[i] > available[i]) { + System.out.println("请求超过可用资源"); + return false; + } + } + + // 尝试分配资源 + for (int i = 0; i < resources; i++) { + available[i] -= request[i]; + allocation[processId][i] += request[i]; + } + + // 检查是否仍处于安全状态 + if (isSafeState()) { + System.out.println("资源分配成功,系统仍处于安全状态"); + return true; + } else { + // 回滚分配 + for (int i = 0; i < resources; i++) { + available[i] += request[i]; + allocation[processId][i] -= request[i]; + } + System.out.println("资源分配会导致不安全状态,拒绝分配"); + return false; + } + } + } + + public static void main(String[] args) throws InterruptedException { + // 启动死锁检测 + DeadlockDetector detector = new DeadlockDetector(); + detector.startDetection(2); + + // 测试有序锁获取 + System.out.println("=== 测试有序锁获取 ==="); + for (int i = 0; i < 3; i++) { + new Thread(() -> OrderedLocking.safeOperation(), "Thread-" + i).start(); + } + + Thread.sleep(2000); + + // 测试超时锁获取 + System.out.println("\n=== 测试超时锁获取 ==="); + TimeoutLocking timeoutLocking = new TimeoutLocking(); + + for (int i = 0; i < 3; i++) { + final int threadId = i; + new Thread(() -> { + boolean success = timeoutLocking.performOperation(1000); + System.out.println("Thread-" + threadId + " 操作" + (success ? "成功" : "失败")); + }, "TimeoutThread-" + i).start(); + } + + Thread.sleep(3000); + detector.shutdown(); + } +} +``` + +### 5.3 调试技巧 + +```java +public class ThreadDebuggingTips { + + /** + * 线程转储分析 + */ + public static class ThreadDumpAnalyzer { + + /** + * 生成线程转储 + */ + public static void generateThreadDump() { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true); + + System.out.println("\n=== 线程转储 ==="); + for (ThreadInfo threadInfo : threadInfos) { + System.out.println("线程名称: " + threadInfo.getThreadName()); + System.out.println("线程ID: " + threadInfo.getThreadId()); + System.out.println("线程状态: " + threadInfo.getThreadState()); + + if (threadInfo.getLockName() != null) { + System.out.println("等待锁: " + threadInfo.getLockName()); + } + + if (threadInfo.getLockOwnerName() != null) { + System.out.println("锁持有者: " + threadInfo.getLockOwnerName()); + } + + System.out.println("堆栈跟踪:"); + for (StackTraceElement element : threadInfo.getStackTrace()) { + System.out.println("\t" + element.toString()); + } + + System.out.println("---"); + } + } + + /** + * 分析线程状态分布 + */ + public static void analyzeThreadStates() { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + long[] threadIds = threadMXBean.getAllThreadIds(); + + Map stateCount = new HashMap<>(); + + for (long threadId : threadIds) { + ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId); + if (threadInfo != null) { + Thread.State state = threadInfo.getThreadState(); + stateCount.put(state, stateCount.getOrDefault(state, 0) + 1); + } + } + + System.out.println("\n=== 线程状态分布 ==="); + stateCount.forEach((state, count) -> + System.out.println(state + ": " + count + " 个线程")); + } + } + + /** + * 线程本地变量调试 + */ + public static class ThreadLocalDebugging { + private static final ThreadLocal threadLocalValue = new ThreadLocal<>(); + private static final ThreadLocal> threadLocalMap = + ThreadLocal.withInitial(HashMap::new); + + /** + * 设置线程本地变量 + */ + public static void setThreadLocalValue(String value) { + threadLocalValue.set(value); + System.out.println(Thread.currentThread().getName() + " 设置值: " + value); + } + + /** + * 获取线程本地变量 + */ + public static String getThreadLocalValue() { + String value = threadLocalValue.get(); + System.out.println(Thread.currentThread().getName() + " 获取值: " + value); + return value; + } + + /** + * 清理线程本地变量(重要!) + */ + public static void cleanupThreadLocal() { + threadLocalValue.remove(); + threadLocalMap.remove(); + System.out.println(Thread.currentThread().getName() + " 清理ThreadLocal"); + } + + /** + * 监控ThreadLocal内存泄漏 + */ + public static void monitorThreadLocalMemory() { + // 在实际项目中,可以使用JVM参数或工具来监控 + // -XX:+PrintGCDetails -XX:+PrintGCTimeStamps + System.out.println("监控ThreadLocal内存使用情况..."); + + // 模拟内存泄漏检测 + Runtime runtime = Runtime.getRuntime(); + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + System.out.println("当前内存使用: " + (usedMemory / 1024 / 1024) + " MB"); + } + } + + /** + * 并发问题重现工具 + */ + public static class ConcurrencyIssueReproducer { + private int counter = 0; + private final Object lock = new Object(); + + /** + * 重现竞态条件 + */ + public void reproduceRaceCondition(int threadCount, int incrementsPerThread) { + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + try { + startLatch.await(); // 等待所有线程准备就绪 + + for (int j = 0; j < incrementsPerThread; j++) { + // 故意不加锁,重现竞态条件 + counter++; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }, "RaceThread-" + i).start(); + } + + System.out.println("开始重现竞态条件..."); + startLatch.countDown(); // 同时启动所有线程 + + try { + endLatch.await(); + System.out.println("期望结果: " + (threadCount * incrementsPerThread)); + System.out.println("实际结果: " + counter); + System.out.println("是否存在竞态条件: " + (counter != threadCount * incrementsPerThread)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * 压力测试工具 + */ + public static void stressTest(Runnable task, int threadCount, int duration) { + System.out.println("开始压力测试: " + threadCount + " 个线程,持续 " + duration + " 秒"); + + AtomicLong operationCount = new AtomicLong(0); + AtomicBoolean running = new AtomicBoolean(true); + + // 启动工作线程 + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + while (running.get()) { + try { + task.run(); + operationCount.incrementAndGet(); + } catch (Exception e) { + System.err.println("压力测试异常: " + e.getMessage()); + } + } + }, "StressThread-" + i).start(); + } + + // 运行指定时间后停止 + try { + Thread.sleep(duration * 1000L); + running.set(false); + + Thread.sleep(1000); // 等待线程结束 + + long totalOps = operationCount.get(); + double opsPerSecond = (double) totalOps / duration; + + System.out.println("压力测试结果:"); + System.out.println("总操作数: " + totalOps); + System.out.println("每秒操作数: " + String.format("%.2f", opsPerSecond)); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public static void main(String[] args) throws InterruptedException { + // 生成线程转储 + ThreadDumpAnalyzer.generateThreadDump(); + + // 分析线程状态 + ThreadDumpAnalyzer.analyzeThreadStates(); + + // 测试ThreadLocal + System.out.println("\n=== ThreadLocal测试 ==="); + for (int i = 0; i < 3; i++) { + final int threadId = i; + new Thread(() -> { + ThreadLocalDebugging.setThreadLocalValue("Value-" + threadId); + ThreadLocalDebugging.getThreadLocalValue(); + ThreadLocalDebugging.cleanupThreadLocal(); + }, "TLThread-" + i).start(); + } + + Thread.sleep(2000); + + // 重现竞态条件 + System.out.println("\n=== 竞态条件重现 ==="); + ConcurrencyIssueReproducer reproducer = new ConcurrencyIssueReproducer(); + reproducer.reproduceRaceCondition(10, 1000); + + // 压力测试 + System.out.println("\n=== 压力测试 ==="); + ConcurrencyIssueReproducer.stressTest(() -> { + // 模拟简单操作 + Math.sqrt(Math.random()); + }, 5, 3); + } +} +``` + +## 6. 最佳实践总结 + +### 6.1 线程安全编程原则 + +1. **最小化共享状态** + - 尽量使用不可变对象 + - 减少共享变量的使用 + - 使用线程本地变量(ThreadLocal) + +2. **正确使用同步机制** + - 选择合适的同步工具(synchronized、Lock、原子类等) + - 避免过度同步导致性能问题 + - 注意锁的粒度和范围 + +3. **避免常见陷阱** + - 死锁预防(有序获取锁、超时机制) + - 活锁和饥饿问题 + - 内存可见性问题(volatile关键字) + +### 6.2 性能优化建议 + +1. **线程池配置** + - CPU密集型:线程数 = CPU核心数 + 1 + - IO密集型:线程数 = CPU核心数 × (1 + IO等待时间/CPU计算时间) + - 合理设置队列大小和拒绝策略 + +2. **减少上下文切换** + - 使用合适的线程数量 + - 减少锁竞争 + - 使用无锁数据结构 + +3. **内存优化** + - 及时清理ThreadLocal + - 避免创建过多短生命周期线程 + - 使用对象池减少GC压力 + +### 6.3 调试和监控 + +1. **日志记录** + - 记录线程创建和销毁 + - 记录锁获取和释放 + - 记录异常和错误 + +2. **监控指标** + - 线程数量和状态 + - 线程池利用率 + - 死锁检测 + - CPU和内存使用率 + +3. **工具使用** + - JConsole、VisualVM等JVM监控工具 + - 线程转储分析 + - 性能分析工具(JProfiler、Async Profiler等) + +### 6.4 代码规范 + +1. **命名规范** + - 线程和线程池使用有意义的名称 + - 锁对象使用描述性名称 + +2. **异常处理** + - 设置未捕获异常处理器 + - 正确处理InterruptedException + - 在finally块中清理资源 + +3. **文档和注释** + - 说明线程安全性 + - 记录同步策略 + - 标注可能的并发问题 + +通过掌握这些Java多线程编程技巧,可以编写出更加高效、安全和可维护的并发程序。记住,多线程编程需要谨慎对待,充分的测试和监控是确保程序正确性的关键。 + + diff --git a/docs/aThread/jvm.md b/docs/aThread/jvm.md new file mode 100644 index 000000000..92d7dfa83 --- /dev/null +++ b/docs/aThread/jvm.md @@ -0,0 +1,1823 @@ +--- +title: jvm基础知识 +author: 哪吒 +date: '2023-06-15' +--- + +# JVM基础知识 + +## 1. JVM概述 + +### 1.1 什么是JVM + +Java虚拟机(Java Virtual Machine,JVM)是Java程序的运行环境,它是Java实现"一次编译,到处运行"的核心。JVM负责将Java字节码转换为特定平台的机器码,并提供内存管理、垃圾回收、安全检查等功能。 + +### 1.2 JVM的作用 + +1. **平台无关性**:屏蔽底层操作系统的差异 +2. **内存管理**:自动分配和回收内存 +3. **安全性**:提供安全的执行环境 +4. **性能优化**:即时编译优化、热点代码优化 + +### 1.3 JVM、JRE、JDK的关系 + +``` +JDK (Java Development Kit) +├── JRE (Java Runtime Environment) +│ ├── JVM (Java Virtual Machine) +│ └── Java类库 +└── 开发工具 (javac、jar、javadoc等) +``` + +- **JVM**:Java虚拟机,负责执行字节码 +- **JRE**:Java运行环境,包含JVM和Java类库 +- **JDK**:Java开发工具包,包含JRE和开发工具 + +## 2. JVM架构 + +### 2.1 JVM整体架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Java应用程序 │ +└─────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────┐ +│ 类加载子系统 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 启动类加载器 │ │ 扩展类加载器 │ │ 应用程序加载器 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────┐ +│ 运行时数据区 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 方法区 │ │ 堆 │ │ Java栈 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ PC寄存器 │ │ 本地方法栈 │ │ +│ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────┐ +│ 执行引擎 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 解释器 │ │ 即时编译器 │ │ 垃圾回收器 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────┐ +│ 本地方法接口 │ +└─────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────┐ +│ 本地方法库 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 主要组成部分 + +1. **类加载子系统**:负责加载、链接和初始化类 +2. **运行时数据区**:JVM管理的内存区域 +3. **执行引擎**:负责执行字节码 +4. **本地方法接口**:与本地方法库交互 +5. **本地方法库**:本地方法的具体实现 + +## 3. JVM内存模型 + +### 3.1 运行时数据区详解 + +#### 3.1.1 程序计数器(PC Register) + +```java +public class PCRegisterExample { + public static void main(String[] args) { + int a = 1; // PC指向这条指令 + int b = 2; // 执行完上一条后,PC指向这条指令 + int c = a + b; // PC继续指向下一条指令 + } +} +``` + +**特点:** +- 线程私有,每个线程都有自己的PC寄存器 +- 存储当前线程执行的字节码指令地址 +- 唯一不会发生OutOfMemoryError的内存区域 + +#### 3.1.2 Java虚拟机栈(JVM Stack) + +```java +public class StackExample { + public static void main(String[] args) { + method1(); // 栈帧1 + } + + public static void method1() { + int localVar = 10; // 局部变量存储在栈帧中 + method2(); // 栈帧2 + } + + public static void method2() { + String str = "Hello"; // 局部变量 + // 方法执行完毕,栈帧出栈 + } +} +``` + +**栈帧结构:** +``` +┌─────────────────────────────────┐ +│ 栈帧 (Stack Frame) │ +├─────────────────────────────────┤ +│ 局部变量表 │ +├─────────────────────────────────┤ +│ 操作数栈 │ +├─────────────────────────────────┤ +│ 动态链接 │ +├─────────────────────────────────┤ +│ 方法返回地址 │ +└─────────────────────────────────┘ +``` + +**特点:** +- 线程私有 +- 存储局部变量、操作数栈、动态链接、方法返回地址 +- 可能抛出StackOverflowError和OutOfMemoryError + +#### 3.1.3 本地方法栈(Native Method Stack) + +```java +public class NativeMethodExample { + // 本地方法声明 + public native void nativeMethod(); + + static { + // 加载本地库 + System.loadLibrary("nativelib"); + } + + public static void main(String[] args) { + NativeMethodExample example = new NativeMethodExample(); + example.nativeMethod(); // 调用本地方法 + } +} +``` + +**特点:** +- 为本地方法服务 +- 与Java虚拟机栈类似,但服务于native方法 + +#### 3.1.4 堆(Heap) + +```java +public class HeapExample { + private String instanceVar; // 实例变量存储在堆中 + + public static void main(String[] args) { + // 对象存储在堆中 + HeapExample obj1 = new HeapExample(); + HeapExample obj2 = new HeapExample(); + + // 数组也存储在堆中 + int[] array = new int[1000]; + + // 字符串常量池(在堆中) + String str1 = "Hello"; + String str2 = new String("World"); + } +} +``` + +**堆内存结构(Java 8之前):** +``` +┌─────────────────────────────────────────────────────────┐ +│ 堆内存 │ +├─────────────────────────────────────────────────────────┤ +│ 新生代 (Young Generation) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Eden │ │ Survivor S0 │ │ Survivor S1 │ │ +│ │ Space │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 老年代 (Old Generation) │ +│ │ +├─────────────────────────────────────────────────────────┤ +│ 永久代 (Permanent Generation) │ +│ (Java 8之前) │ +└─────────────────────────────────────────────────────────┘ +``` + +**堆内存结构(Java 8及之后):** +``` +┌─────────────────────────────────────────────────────────┐ +│ 堆内存 │ +├─────────────────────────────────────────────────────────┤ +│ 新生代 (Young Generation) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Eden │ │ Survivor S0 │ │ Survivor S1 │ │ +│ │ Space │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 老年代 (Old Generation) │ +│ │ +└─────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────┐ +│ 元空间 (Metaspace) │ +│ (Java 8及之后) │ +└─────────────────────────────────────────────────────────┘ +``` + +**特点:** +- 线程共享 +- 存储对象实例和数组 +- 垃圾回收的主要区域 +- 可能抛出OutOfMemoryError + +#### 3.1.5 方法区(Method Area) + +```java +public class MethodAreaExample { + // 类变量(静态变量)存储在方法区 + private static String staticVar = "Static Variable"; + + // 常量存储在方法区 + private static final String CONSTANT = "Constant Value"; + + // 方法信息存储在方法区 + public void instanceMethod() { + System.out.println("Instance method"); + } + + public static void staticMethod() { + System.out.println("Static method"); + } +} +``` + +**方法区存储内容:** +- 类信息(类名、访问修饰符、父类信息等) +- 方法信息(方法名、返回类型、参数信息、字节码等) +- 字段信息(字段名、类型、访问修饰符等) +- 静态变量 +- 常量池 +- 即时编译器编译后的代码 + +**特点:** +- 线程共享 +- 存储类级别的信息 +- Java 8之前称为永久代,Java 8及之后称为元空间 + +### 3.2 内存分配示例 + +```java +public class MemoryAllocationExample { + // 类变量 - 存储在方法区 + private static int classVar = 100; + + // 实例变量 - 存储在堆中(对象的一部分) + private int instanceVar = 200; + + public static void main(String[] args) { + // 局部变量 - 存储在栈中 + int localVar = 300; + + // 对象 - 存储在堆中 + MemoryAllocationExample obj = new MemoryAllocationExample(); + + // 数组 - 存储在堆中 + int[] array = new int[10]; + + // 字符串字面量 - 存储在字符串常量池(堆中) + String str1 = "Hello"; + + // 字符串对象 - 存储在堆中 + String str2 = new String("World"); + + // 调用方法 + obj.methodCall(localVar); + } + + public void methodCall(int param) { + // 方法参数和局部变量 - 存储在栈中 + int methodLocal = param + instanceVar; + + // 创建新对象 - 存储在堆中 + Object tempObj = new Object(); + } +} +``` + +## 4. 垃圾回收(Garbage Collection) + +### 4.1 垃圾回收概述 + +垃圾回收是JVM自动管理内存的机制,负责回收不再使用的对象所占用的内存空间。 + +### 4.2 对象存活判断 + +#### 4.2.1 引用计数法 + +```java +public class ReferenceCountingExample { + private ReferenceCountingExample reference; + + public static void main(String[] args) { + ReferenceCountingExample obj1 = new ReferenceCountingExample(); + ReferenceCountingExample obj2 = new ReferenceCountingExample(); + + // 循环引用问题 + obj1.reference = obj2; + obj2.reference = obj1; + + // 即使obj1和obj2不再被外部引用, + // 但它们相互引用,引用计数不为0 + obj1 = null; + obj2 = null; + + // 引用计数法无法回收这种循环引用的对象 + } +} +``` + +**问题:** 无法解决循环引用问题 + +#### 4.2.2 可达性分析算法 + +```java +public class ReachabilityAnalysisExample { + private static ReachabilityAnalysisExample staticRef; // GC Root + private ReachabilityAnalysisExample instanceRef; + + public static void main(String[] args) { + // main方法的局部变量是GC Root + ReachabilityAnalysisExample obj1 = new ReachabilityAnalysisExample(); + ReachabilityAnalysisExample obj2 = new ReachabilityAnalysisExample(); + ReachabilityAnalysisExample obj3 = new ReachabilityAnalysisExample(); + + // 建立引用关系 + obj1.instanceRef = obj2; + obj2.instanceRef = obj3; + + // obj1可达(通过局部变量) + // obj2可达(通过obj1.instanceRef) + // obj3可达(通过obj2.instanceRef) + + obj1 = null; // 断开引用链 + + // 现在obj1、obj2、obj3都不可达,可以被回收 + System.gc(); // 建议进行垃圾回收 + } +} +``` + +**GC Roots包括:** +- 虚拟机栈中的引用 +- 方法区中静态属性引用的对象 +- 方法区中常量引用的对象 +- 本地方法栈中JNI引用的对象 +- JVM内部引用 +- 同步锁持有的对象 +- JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等 + +### 4.3 垃圾回收算法 + +#### 4.3.1 标记-清除算法(Mark-Sweep) + +```java +/** + * 标记-清除算法示例 + * + * 阶段1:标记阶段 + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ B ✗ │ │ C ✓ │ │ D ✗ │ │ E ✓ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + * + * 阶段2:清除阶段 + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ │ │ C ✓ │ │ │ │ E ✓ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + * + * 问题:产生内存碎片 + */ +public class MarkSweepExample { + public static void demonstrateFragmentation() { + // 创建大量小对象 + List objects = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + objects.add(new Object()); + } + + // 清除一半对象,模拟标记-清除 + for (int i = 0; i < objects.size(); i += 2) { + objects.set(i, null); + } + + // 此时内存中存在碎片 + // 尝试分配大对象可能失败 + try { + byte[] largeArray = new byte[1024 * 1024]; // 1MB + } catch (OutOfMemoryError e) { + System.out.println("内存碎片导致大对象分配失败"); + } + } +} +``` + +**优点:** 实现简单 +**缺点:** 产生内存碎片,效率不高 + +#### 4.3.2 标记-复制算法(Mark-Copy) + +```java +/** + * 标记-复制算法示例 + * + * 原始内存区域: + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ B ✗ │ │ C ✓ │ │ D ✗ │ │ E ✓ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + * + * 复制后的内存区域: + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ C ✓ │ │ E ✓ │ │ │ │ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + */ +public class MarkCopyExample { + public static void demonstrateCopying() { + // 模拟新生代的Eden区 + List edenSpace = new ArrayList<>(); + + // 创建对象 + for (int i = 0; i < 100; i++) { + edenSpace.add(new Object()); + } + + // 模拟GC:将存活对象复制到Survivor区 + List survivorSpace = new ArrayList<>(); + + // 假设只有一半对象存活 + for (int i = 0; i < edenSpace.size(); i += 2) { + Object obj = edenSpace.get(i); + if (obj != null) { + survivorSpace.add(obj); // 复制存活对象 + } + } + + // 清空Eden区 + edenSpace.clear(); + + System.out.println("复制算法完成,存活对象数量:" + survivorSpace.size()); + } +} +``` + +**优点:** 没有内存碎片,效率高 +**缺点:** 浪费一半内存空间 + +#### 4.3.3 标记-整理算法(Mark-Compact) + +```java +/** + * 标记-整理算法示例 + * + * 标记阶段: + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ B ✗ │ │ C ✓ │ │ D ✗ │ │ E ✓ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + * + * 整理阶段: + * ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + * │ A ✓ │ │ C ✓ │ │ E ✓ │ │ │ │ │ + * └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ + */ +public class MarkCompactExample { + public static void demonstrateCompaction() { + // 模拟老年代内存 + Object[] memory = new Object[100]; + + // 分配对象,模拟内存碎片 + for (int i = 0; i < memory.length; i++) { + if (i % 3 != 0) { // 模拟部分对象存活 + memory[i] = new Object(); + } + } + + // 标记-整理:将存活对象向前移动 + int writeIndex = 0; + for (int readIndex = 0; readIndex < memory.length; readIndex++) { + if (memory[readIndex] != null) { + memory[writeIndex] = memory[readIndex]; + if (writeIndex != readIndex) { + memory[readIndex] = null; // 清除原位置 + } + writeIndex++; + } + } + + System.out.println("整理完成,存活对象数量:" + writeIndex); + System.out.println("连续可用空间:" + (memory.length - writeIndex)); + } +} +``` + +**优点:** 没有内存碎片,不浪费内存 +**缺点:** 需要移动对象,效率较低 + +### 4.4 分代收集算法 + +```java +public class GenerationalGCExample { + // 模拟不同生命周期的对象 + private static List longLivedObjects = new ArrayList<>(); // 长期存活 + + public static void main(String[] args) { + // 创建一些长期存活的对象(会进入老年代) + for (int i = 0; i < 10; i++) { + longLivedObjects.add(new Object()); + } + + // 模拟应用程序运行 + for (int generation = 0; generation < 100; generation++) { + createShortLivedObjects(); // 创建短期对象 + + if (generation % 10 == 0) { + // 模拟Minor GC + System.out.println("执行Minor GC - 清理新生代"); + } + + if (generation % 50 == 0) { + // 模拟Major GC + System.out.println("执行Major GC - 清理老年代"); + } + } + } + + private static void createShortLivedObjects() { + // 创建短期存活的对象(在新生代被回收) + List tempObjects = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + tempObjects.add(new Object()); + } + // 方法结束后,tempObjects变为不可达,等待GC回收 + } +} +``` + +**分代假设:** +1. 大部分对象都是朝生夕死的 +2. 熬过越多次垃圾收集过程的对象就越难以消亡 + +### 4.5 常见垃圾收集器 + +#### 4.5.1 Serial收集器 + +```java +/** + * Serial收集器特点: + * - 单线程收集 + * - 收集时必须暂停所有工作线程(Stop The World) + * - 适用于客户端应用 + * + * JVM参数:-XX:+UseSerialGC + */ +public class SerialGCExample { + public static void main(String[] args) { + // 设置较小的堆内存以便观察GC + // -Xms10m -Xmx10m -XX:+UseSerialGC -XX:+PrintGC + + List objects = new ArrayList<>(); + + try { + while (true) { + // 不断创建对象,触发GC + objects.add(new byte[1024 * 1024]); // 1MB + Thread.sleep(100); + } + } catch (OutOfMemoryError | InterruptedException e) { + System.out.println("程序结束"); + } + } +} +``` + +#### 4.5.2 Parallel收集器 + +```java +/** + * Parallel收集器特点: + * - 多线程并行收集 + * - 关注吞吐量 + * - 适用于服务端应用 + * + * JVM参数:-XX:+UseParallelGC + */ +public class ParallelGCExample { + public static void main(String[] args) { + // JVM参数:-XX:+UseParallelGC -XX:ParallelGCThreads=4 + + // 创建多个线程模拟高并发场景 + for (int i = 0; i < 4; i++) { + new Thread(() -> { + List objects = new ArrayList<>(); + for (int j = 0; j < 10000; j++) { + objects.add(new Object()); + if (j % 1000 == 0) { + objects.clear(); // 定期清理,触发GC + } + } + }, "Worker-" + i).start(); + } + } +} +``` + +#### 4.5.3 CMS收集器 + +```java +/** + * CMS (Concurrent Mark Sweep) 收集器特点: + * - 并发收集,低延迟 + * - 使用标记-清除算法 + * - 适用于对响应时间要求高的应用 + * + * JVM参数:-XX:+UseConcMarkSweepGC + */ +public class CMSGCExample { + private static volatile boolean running = true; + + public static void main(String[] args) throws InterruptedException { + // JVM参数:-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails + + // 启动后台线程持续分配对象 + Thread allocatorThread = new Thread(() -> { + List objects = new ArrayList<>(); + while (running) { + objects.add(new Object()); + if (objects.size() > 10000) { + objects.clear(); + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + break; + } + } + }); + + allocatorThread.start(); + + // 主线程模拟业务处理 + for (int i = 0; i < 100; i++) { + // 模拟业务处理,对延迟敏感 + long start = System.currentTimeMillis(); + + // 模拟业务逻辑 + Thread.sleep(50); + + long end = System.currentTimeMillis(); + System.out.println("业务处理耗时:" + (end - start) + "ms"); + } + + running = false; + allocatorThread.join(); + } +} +``` + +#### 4.5.4 G1收集器 + +```java +/** + * G1 (Garbage First) 收集器特点: + * - 低延迟,可预测的停顿时间 + * - 将堆分为多个Region + * - 优先回收垃圾最多的Region + * + * JVM参数:-XX:+UseG1GC + */ +public class G1GCExample { + public static void main(String[] args) { + // JVM参数:-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails + + // 创建不同大小的对象,测试G1的Region管理 + List smallObjects = new ArrayList<>(); + List largeObjects = new ArrayList<>(); + + for (int i = 0; i < 1000; i++) { + // 小对象 + smallObjects.add(new byte[1024]); // 1KB + + // 大对象(超过Region大小的一半) + if (i % 100 == 0) { + largeObjects.add(new byte[1024 * 1024]); // 1MB + } + + // 定期清理部分对象 + if (i % 200 == 0) { + smallObjects.subList(0, smallObjects.size() / 2).clear(); + } + } + + System.out.println("G1GC测试完成"); + } +} +``` + +## 5. 类加载机制 + +### 5.1 类加载过程 + +类加载过程包括:**加载 → 验证 → 准备 → 解析 → 初始化** + +```java +public class ClassLoadingExample { + // 静态变量在准备阶段分配内存并设置默认值 + // 在初始化阶段执行初始化 + private static int staticVar = 100; + + // 静态代码块在初始化阶段执行 + static { + System.out.println("静态代码块执行,staticVar = " + staticVar); + staticVar = 200; + } + + // 实例变量在对象创建时初始化 + private int instanceVar = 300; + + // 构造方法在对象创建时执行 + public ClassLoadingExample() { + System.out.println("构造方法执行,instanceVar = " + instanceVar); + } + + public static void main(String[] args) { + System.out.println("main方法开始执行"); + + // 第一次使用类时触发类加载 + System.out.println("staticVar = " + ClassLoadingExample.staticVar); + + // 创建对象 + ClassLoadingExample obj = new ClassLoadingExample(); + } +} +``` + +**输出结果:** +``` +main方法开始执行 +静态代码块执行,staticVar = 100 +staticVar = 200 +构造方法执行,instanceVar = 300 +``` + +### 5.2 类加载器 + +#### 5.2.1 类加载器层次结构 + +```java +public class ClassLoaderExample { + public static void main(String[] args) { + // 获取当前类的类加载器 + ClassLoader classLoader = ClassLoaderExample.class.getClassLoader(); + System.out.println("当前类的类加载器:" + classLoader); + + // 获取父类加载器 + ClassLoader parent = classLoader.getParent(); + System.out.println("父类加载器:" + parent); + + // 获取祖父类加载器(启动类加载器) + ClassLoader grandParent = parent.getParent(); + System.out.println("祖父类加载器:" + grandParent); // null表示启动类加载器 + + // 查看系统类的类加载器 + ClassLoader stringClassLoader = String.class.getClassLoader(); + System.out.println("String类的类加载器:" + stringClassLoader); // null表示启动类加载器 + + // 查看扩展类的类加载器 + try { + Class zipFileClass = Class.forName("java.util.zip.ZipFile"); + System.out.println("ZipFile类的类加载器:" + zipFileClass.getClassLoader()); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } +} +``` + +#### 5.2.2 双亲委派模型 + +```java +public class ParentDelegationExample { + public static void main(String[] args) { + // 演示双亲委派模型 + try { + // 尝试加载系统类 + Class stringClass = Class.forName("java.lang.String"); + System.out.println("String类加载器:" + stringClass.getClassLoader()); + + // 尝试加载应用程序类 + Class currentClass = Class.forName("ParentDelegationExample"); + System.out.println("当前类加载器:" + currentClass.getClassLoader()); + + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } +} + +/** + * 自定义类加载器示例 + */ +class CustomClassLoader extends ClassLoader { + private String classPath; + + public CustomClassLoader(String classPath) { + this.classPath = classPath; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + try { + // 读取类文件字节码 + byte[] classData = loadClassData(name); + if (classData != null) { + // 定义类 + return defineClass(name, classData, 0, classData.length); + } + } catch (Exception e) { + e.printStackTrace(); + } + throw new ClassNotFoundException(name); + } + + private byte[] loadClassData(String className) { + // 实际实现中应该从文件系统或网络加载类文件 + // 这里只是示例 + return null; + } + + public static void demonstrateCustomClassLoader() { + CustomClassLoader customLoader = new CustomClassLoader("/custom/classes"); + + try { + // 使用自定义类加载器加载类 + Class customClass = customLoader.loadClass("com.example.CustomClass"); + System.out.println("自定义类加载器:" + customClass.getClassLoader()); + + } catch (ClassNotFoundException e) { + System.out.println("类未找到:" + e.getMessage()); + } + } +} +``` + +### 5.3 类初始化时机 + +```java +class Parent { + static { + System.out.println("Parent类静态代码块"); + } + + public static int parentStaticVar = 1; + + static { + System.out.println("Parent类静态代码块2,parentStaticVar = " + parentStaticVar); + } +} + +class Child extends Parent { + static { + System.out.println("Child类静态代码块"); + } + + public static int childStaticVar = 2; +} + +public class ClassInitializationExample { + public static void main(String[] args) { + System.out.println("=== 测试1:访问父类静态变量 ==="); + System.out.println(Parent.parentStaticVar); + + System.out.println("\n=== 测试2:访问子类静态变量 ==="); + System.out.println(Child.childStaticVar); + + System.out.println("\n=== 测试3:创建子类实例 ==="); + Child child = new Child(); + } +} +``` + +**类初始化的触发条件:** + 1. 创建类的实例 + 2. 访问类的静态变量(除了final常量) + 3. 调用类的静态方法 + 4. 反射调用类 + 5. 初始化子类时,父类还没有初始化 + 6. JVM启动时指定的主类 + +## 6. JVM性能调优 + +### 6.1 JVM参数配置 + +#### 6.1.1 堆内存参数 + +```java +public class HeapParametersExample { + public static void main(String[] args) { + // 获取JVM内存信息 + Runtime runtime = Runtime.getRuntime(); + + long maxMemory = runtime.maxMemory(); // 最大可用内存 + long totalMemory = runtime.totalMemory(); // 当前JVM内存总量 + long freeMemory = runtime.freeMemory(); // 当前JVM空闲内存 + long usedMemory = totalMemory - freeMemory; // 已使用内存 + + System.out.println("=== JVM内存信息 ==="); + System.out.println("最大内存: " + (maxMemory / 1024 / 1024) + "MB"); + System.out.println("总内存: " + (totalMemory / 1024 / 1024) + "MB"); + System.out.println("空闲内存: " + (freeMemory / 1024 / 1024) + "MB"); + System.out.println("已使用内存: " + (usedMemory / 1024 / 1024) + "MB"); + + // 获取垃圾收集器信息 + java.lang.management.ManagementFactory.getGarbageCollectorMXBeans() + .forEach(gcBean -> { + System.out.println("GC名称: " + gcBean.getName()); + System.out.println("GC次数: " + gcBean.getCollectionCount()); + System.out.println("GC时间: " + gcBean.getCollectionTime() + "ms"); + }); + } +} +``` + +**常用堆内存参数:** +```bash +# 设置初始堆大小为512MB +-Xms512m + +# 设置最大堆大小为2GB +-Xmx2g + +# 设置新生代大小为256MB +-Xmn256m + +# 设置新生代与老年代的比例(1:2) +-XX:NewRatio=2 + +# 设置Eden区与Survivor区的比例(8:1:1) +-XX:SurvivorRatio=8 + +# 设置对象进入老年代的年龄阈值 +-XX:MaxTenuringThreshold=15 +``` + +#### 6.1.2 垃圾收集器参数 + +```java +public class GCParametersExample { + public static void main(String[] args) { + // 模拟不同的内存分配模式 + demonstrateYoungGeneration(); + demonstrateOldGeneration(); + } + + // 模拟年轻代频繁分配 + private static void demonstrateYoungGeneration() { + System.out.println("=== 年轻代分配测试 ==="); + for (int i = 0; i < 1000; i++) { + // 创建短生命周期对象 + byte[] temp = new byte[1024 * 10]; // 10KB + if (i % 100 == 0) { + System.out.println("已分配对象: " + (i + 1)); + } + } + } + + // 模拟老年代分配 + private static void demonstrateOldGeneration() { + System.out.println("\n=== 老年代分配测试 ==="); + java.util.List longLivedObjects = new java.util.ArrayList<>(); + + for (int i = 0; i < 100; i++) { + // 创建长生命周期对象 + byte[] longLived = new byte[1024 * 100]; // 100KB + longLivedObjects.add(longLived); + + if (i % 20 == 0) { + System.out.println("长期对象数量: " + longLivedObjects.size()); + } + } + } +} +``` + +**垃圾收集器选择参数:** +```bash +# 使用Serial收集器(适合单核CPU) +-XX:+UseSerialGC + +# 使用Parallel收集器(适合多核CPU,关注吞吐量) +-XX:+UseParallelGC +-XX:ParallelGCThreads=4 + +# 使用CMS收集器(适合低延迟要求) +-XX:+UseConcMarkSweepGC +-XX:+CMSParallelRemarkEnabled +-XX:CMSInitiatingOccupancyFraction=70 + +# 使用G1收集器(适合大堆内存) +-XX:+UseG1GC +-XX:MaxGCPauseMillis=200 +-XX:G1HeapRegionSize=16m +``` + +#### 6.1.3 JIT编译器参数 + +```java +public class JITCompilerExample { + private static final int ITERATIONS = 100000; + + public static void main(String[] args) { + // 热身阶段,触发JIT编译 + System.out.println("=== JIT编译优化演示 ==="); + + long startTime = System.currentTimeMillis(); + + // 第一次执行(解释执行) + for (int i = 0; i < ITERATIONS; i++) { + calculateSum(i); + } + + long interpretedTime = System.currentTimeMillis() - startTime; + System.out.println("解释执行时间: " + interpretedTime + "ms"); + + // 第二次执行(JIT编译后) + startTime = System.currentTimeMillis(); + + for (int i = 0; i < ITERATIONS; i++) { + calculateSum(i); + } + + long compiledTime = System.currentTimeMillis() - startTime; + System.out.println("编译执行时间: " + compiledTime + "ms"); + System.out.println("性能提升: " + (interpretedTime / (double) compiledTime) + "倍"); + } + + // 热点方法,会被JIT编译器优化 + private static long calculateSum(int n) { + long sum = 0; + for (int i = 0; i <= n; i++) { + sum += i; + } + return sum; + } +} +``` + +**JIT编译器参数:** +```bash +# 设置方法调用次数阈值(触发JIT编译) +-XX:CompileThreshold=10000 + +# 禁用JIT编译器(仅解释执行) +-Xint + +# 仅使用JIT编译器(不解释执行) +-Xcomp + +# 混合模式(默认) +-Xmixed + +# 打印JIT编译信息 +-XX:+PrintCompilation +``` + +### 6.2 内存泄漏分析 + +#### 6.2.1 常见内存泄漏场景 + +```java +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryLeakExamples { + + // 场景1:静态集合持有对象引用 + private static List staticList = new ArrayList<>(); + + public static void demonstrateStaticCollectionLeak() { + System.out.println("=== 静态集合内存泄漏演示 ==="); + + for (int i = 0; i < 10000; i++) { + // 不断向静态集合添加对象,但从不清理 + staticList.add(new LargeObject("Object-" + i)); + } + + System.out.println("静态集合大小: " + staticList.size()); + // 解决方案:定期清理或使用弱引用 + // staticList.clear(); + } + + // 场景2:监听器未注销 + private List listeners = new ArrayList<>(); + + public void demonstrateListenerLeak() { + System.out.println("\n=== 监听器内存泄漏演示 ==="); + + for (int i = 0; i < 1000; i++) { + EventListener listener = new EventListener("Listener-" + i); + listeners.add(listener); + // 问题:监听器注册后从未注销 + } + + System.out.println("监听器数量: " + listeners.size()); + // 解决方案:及时注销监听器 + // listeners.clear(); + } + + // 场景3:线程局部变量未清理 + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + public static void demonstrateThreadLocalLeak() { + System.out.println("\n=== ThreadLocal内存泄漏演示 ==="); + + Thread thread = new Thread(() -> { + // 设置ThreadLocal变量 + threadLocal.set(new LargeObject("ThreadLocal-Object")); + + // 模拟业务处理 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 问题:线程结束前未清理ThreadLocal + // 解决方案:threadLocal.remove(); + }); + + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // 场景4:缓存无限增长 + private static Map cache = new ConcurrentHashMap<>(); + + public static void demonstrateCacheLeak() { + System.out.println("\n=== 缓存内存泄漏演示 ==="); + + for (int i = 0; i < 5000; i++) { + String key = "cache-key-" + i; + // 不断向缓存添加数据,但从不清理 + cache.put(key, new LargeObject(key)); + } + + System.out.println("缓存大小: " + cache.size()); + // 解决方案:使用LRU缓存或定期清理 + } + + public static void main(String[] args) { + demonstrateStaticCollectionLeak(); + + MemoryLeakExamples example = new MemoryLeakExamples(); + example.demonstrateListenerLeak(); + + demonstrateThreadLocalLeak(); + demonstrateCacheLeak(); + + // 建议进行垃圾回收 + System.gc(); + + // 打印内存使用情况 + Runtime runtime = Runtime.getRuntime(); + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + System.out.println("\n当前内存使用: " + (usedMemory / 1024 / 1024) + "MB"); + } +} + +// 模拟大对象 +class LargeObject { + private String name; + private byte[] data = new byte[1024 * 10]; // 10KB + + public LargeObject(String name) { + this.name = name; + } + + @Override + public String toString() { + return "LargeObject{name='" + name + "'}"; + } +} + +// 模拟事件监听器 +class EventListener { + private String name; + + public EventListener(String name) { + this.name = name; + } + + @Override + public String toString() { + return "EventListener{name='" + name + "'}"; + } +} +``` + +#### 6.2.2 内存泄漏检测工具 + +```java +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; + +public class MemoryMonitoringExample { + + public static void main(String[] args) { + // 启动内存监控 + startMemoryMonitoring(); + + // 模拟内存使用 + simulateMemoryUsage(); + } + + private static void startMemoryMonitoring() { + System.out.println("=== 内存监控启动 ==="); + + // 创建监控线程 + Thread monitorThread = new Thread(() -> { + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + + while (!Thread.currentThread().isInterrupted()) { + try { + // 获取堆内存使用情况 + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + + long used = heapUsage.getUsed(); + long max = heapUsage.getMax(); + double usagePercent = (double) used / max * 100; + + System.out.printf("堆内存使用: %d MB / %d MB (%.2f%%)%n", + used / 1024 / 1024, max / 1024 / 1024, usagePercent); + + // 内存使用率过高时发出警告 + if (usagePercent > 80) { + System.out.println("⚠️ 警告:内存使用率过高!"); + } + + Thread.sleep(2000); // 每2秒监控一次 + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + + monitorThread.setDaemon(true); + monitorThread.start(); + } + + private static void simulateMemoryUsage() { + List memoryConsumer = new ArrayList<>(); + + try { + for (int i = 0; i < 100; i++) { + // 分配10MB内存 + byte[] chunk = new byte[10 * 1024 * 1024]; + memoryConsumer.add(chunk); + + System.out.println("分配内存块: " + (i + 1)); + Thread.sleep(1000); + + // 模拟释放部分内存 + if (i % 10 == 0 && !memoryConsumer.isEmpty()) { + memoryConsumer.remove(0); + System.out.println("释放内存块"); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (OutOfMemoryError e) { + System.out.println("❌ 内存溢出: " + e.getMessage()); + } + } +} +``` + +**内存分析工具:** +```bash +# 生成堆转储文件 +-XX:+HeapDumpOnOutOfMemoryError +-XX:HeapDumpPath=/path/to/heapdump.hprof + +# 使用jmap生成堆转储 +jmap -dump:format=b,file=heap.hprof + +# 使用jstat监控GC +jstat -gc 1s + +# 使用jvisualvm进行可视化分析 +jvisualvm +``` + +### 6.3 性能优化最佳实践 + +#### 6.3.1 对象创建优化 + +```java +public class ObjectCreationOptimization { + + // 对象池示例 + private static final Queue stringBuilderPool = + new java.util.concurrent.ConcurrentLinkedQueue<>(); + + public static void main(String[] args) { + demonstrateStringOptimization(); + demonstrateObjectPooling(); + demonstrateArrayOptimization(); + } + + // 字符串拼接优化 + private static void demonstrateStringOptimization() { + System.out.println("=== 字符串拼接优化 ==="); + + long startTime = System.currentTimeMillis(); + + // 低效方式:String拼接 + String result1 = ""; + for (int i = 0; i < 10000; i++) { + result1 += "item" + i; + } + + long stringTime = System.currentTimeMillis() - startTime; + System.out.println("String拼接耗时: " + stringTime + "ms"); + + startTime = System.currentTimeMillis(); + + // 高效方式:StringBuilder + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + sb.append("item").append(i); + } + String result2 = sb.toString(); + + long sbTime = System.currentTimeMillis() - startTime; + System.out.println("StringBuilder耗时: " + sbTime + "ms"); + System.out.println("性能提升: " + (stringTime / (double) sbTime) + "倍"); + } + + // 对象池优化 + private static void demonstrateObjectPooling() { + System.out.println("\n=== 对象池优化 ==="); + + long startTime = System.currentTimeMillis(); + + // 不使用对象池 + for (int i = 0; i < 10000; i++) { + StringBuilder sb = new StringBuilder(); + sb.append("test").append(i); + // StringBuilder被丢弃,等待GC + } + + long noPoolTime = System.currentTimeMillis() - startTime; + System.out.println("不使用对象池耗时: " + noPoolTime + "ms"); + + startTime = System.currentTimeMillis(); + + // 使用对象池 + for (int i = 0; i < 10000; i++) { + StringBuilder sb = borrowStringBuilder(); + sb.append("test").append(i); + returnStringBuilder(sb); + } + + long poolTime = System.currentTimeMillis() - startTime; + System.out.println("使用对象池耗时: " + poolTime + "ms"); + System.out.println("性能提升: " + (noPoolTime / (double) poolTime) + "倍"); + } + + private static StringBuilder borrowStringBuilder() { + StringBuilder sb = stringBuilderPool.poll(); + if (sb == null) { + sb = new StringBuilder(); + } else { + sb.setLength(0); // 清空内容 + } + return sb; + } + + private static void returnStringBuilder(StringBuilder sb) { + if (sb.capacity() < 1024) { // 避免池中对象过大 + stringBuilderPool.offer(sb); + } + } + + // 数组优化 + private static void demonstrateArrayOptimization() { + System.out.println("\n=== 数组优化 ==="); + + long startTime = System.currentTimeMillis(); + + // 使用ArrayList动态扩容 + List list = new ArrayList<>(); + for (int i = 0; i < 100000; i++) { + list.add(i); + } + + long listTime = System.currentTimeMillis() - startTime; + System.out.println("ArrayList耗时: " + listTime + "ms"); + + startTime = System.currentTimeMillis(); + + // 预分配容量 + List preAllocatedList = new ArrayList<>(100000); + for (int i = 0; i < 100000; i++) { + preAllocatedList.add(i); + } + + long preAllocTime = System.currentTimeMillis() - startTime; + System.out.println("预分配ArrayList耗时: " + preAllocTime + "ms"); + System.out.println("性能提升: " + (listTime / (double) preAllocTime) + "倍"); + } +} +``` + +#### 6.3.2 GC调优策略 + +```java +public class GCTuningExample { + + public static void main(String[] args) { + demonstrateGenerationalGC(); + demonstrateGCMonitoring(); + } + + // 分代GC优化 + private static void demonstrateGenerationalGC() { + System.out.println("=== 分代GC优化演示 ==="); + + // 创建短生命周期对象(应该在年轻代被回收) + for (int i = 0; i < 1000; i++) { + createShortLivedObjects(); + + if (i % 100 == 0) { + System.out.println("已处理批次: " + (i / 100 + 1)); + // 建议进行Minor GC + System.gc(); + } + } + + // 创建长生命周期对象(会进入老年代) + List longLivedObjects = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + longLivedObjects.add(new LargeObject("Long-lived-" + i)); + } + + System.out.println("长期对象创建完成: " + longLivedObjects.size()); + } + + private static void createShortLivedObjects() { + // 创建临时对象 + List tempObjects = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + tempObjects.add(new Object()); + } + // 方法结束后,tempObjects变为不可达 + } + + // GC监控 + private static void demonstrateGCMonitoring() { + System.out.println("\n=== GC监控演示 ==="); + + // 获取GC信息 + ManagementFactory.getGarbageCollectorMXBeans().forEach(gcBean -> { + System.out.println("GC收集器: " + gcBean.getName()); + System.out.println("收集次数: " + gcBean.getCollectionCount()); + System.out.println("收集时间: " + gcBean.getCollectionTime() + "ms"); + System.out.println("管理的内存池: " + gcBean.getMemoryPoolNames()); + System.out.println("---"); + }); + } +} +``` + +## 7. JVM故障诊断 + +### 7.1 常见JVM问题 + +#### 7.1.1 OutOfMemoryError分析 + +```java +public class OutOfMemoryErrorExample { + + public static void main(String[] args) { + System.out.println("=== OutOfMemoryError演示 ==="); + + // 演示不同类型的OOM + try { + demonstrateHeapOOM(); + } catch (OutOfMemoryError e) { + System.out.println("捕获到堆内存溢出: " + e.getMessage()); + } + + try { + demonstrateStackOverflow(); + } catch (StackOverflowError e) { + System.out.println("捕获到栈溢出: " + e.getMessage()); + } + } + + // 堆内存溢出 + private static void demonstrateHeapOOM() { + System.out.println("\n--- 堆内存溢出演示 ---"); + List list = new ArrayList<>(); + + try { + while (true) { + // 不断分配大对象 + byte[] array = new byte[1024 * 1024]; // 1MB + list.add(array); + System.out.println("已分配对象数量: " + list.size()); + } + } catch (OutOfMemoryError e) { + System.out.println("堆内存不足,已分配: " + list.size() + " MB"); + throw e; + } + } + + // 栈溢出 + private static void demonstrateStackOverflow() { + System.out.println("\n--- 栈溢出演示 ---"); + recursiveMethod(0); + } + + private static void recursiveMethod(int depth) { + System.out.println("递归深度: " + depth); + // 无限递归导致栈溢出 + recursiveMethod(depth + 1); + } +} +``` + +#### 7.1.2 死锁检测 + +```java +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; + +public class DeadlockDetectionExample { + private static final Object lock1 = new Object(); + private static final Object lock2 = new Object(); + + public static void main(String[] args) { + System.out.println("=== 死锁检测演示 ==="); + + // 启动死锁检测 + startDeadlockDetection(); + + // 创建可能导致死锁的线程 + createDeadlockThreads(); + + // 主线程等待 + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void startDeadlockDetection() { + Thread detectionThread = new Thread(() -> { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + + while (!Thread.currentThread().isInterrupted()) { + try { + // 检测死锁 + long[] deadlockedThreads = threadBean.findDeadlockedThreads(); + + if (deadlockedThreads != null) { + System.out.println("\n🚨 检测到死锁!"); + + ThreadInfo[] threadInfos = threadBean.getThreadInfo(deadlockedThreads); + for (ThreadInfo threadInfo : threadInfos) { + System.out.println("死锁线程: " + threadInfo.getThreadName()); + System.out.println("线程状态: " + threadInfo.getThreadState()); + System.out.println("阻塞对象: " + threadInfo.getLockName()); + System.out.println("持有锁的线程: " + threadInfo.getLockOwnerName()); + System.out.println("---"); + } + break; + } + + Thread.sleep(1000); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + + detectionThread.setDaemon(true); + detectionThread.start(); + } + + private static void createDeadlockThreads() { + // 线程1:先获取lock1,再获取lock2 + Thread thread1 = new Thread(() -> { + synchronized (lock1) { + System.out.println("Thread1获取了lock1"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + System.out.println("Thread1尝试获取lock2"); + synchronized (lock2) { + System.out.println("Thread1获取了lock2"); + } + } + }, "DeadlockThread-1"); + + // 线程2:先获取lock2,再获取lock1 + Thread thread2 = new Thread(() -> { + synchronized (lock2) { + System.out.println("Thread2获取了lock2"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + System.out.println("Thread2尝试获取lock1"); + synchronized (lock1) { + System.out.println("Thread2获取了lock1"); + } + } + }, "DeadlockThread-2"); + + thread1.start(); + thread2.start(); + } +} +``` + +### 7.2 JVM调试工具 + +```java +import java.lang.management.*; +import java.util.List; + +public class JVMDiagnosticTools { + + public static void main(String[] args) { + printJVMInfo(); + printMemoryInfo(); + printThreadInfo(); + printGCInfo(); + } + + // 打印JVM基本信息 + private static void printJVMInfo() { + System.out.println("=== JVM基本信息 ==="); + + RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + + System.out.println("JVM名称: " + runtimeBean.getVmName()); + System.out.println("JVM版本: " + runtimeBean.getVmVersion()); + System.out.println("JVM供应商: " + runtimeBean.getVmVendor()); + System.out.println("启动时间: " + new java.util.Date(runtimeBean.getStartTime())); + System.out.println("运行时间: " + (runtimeBean.getUptime() / 1000) + "秒"); + + List inputArguments = runtimeBean.getInputArguments(); + System.out.println("JVM参数:"); + inputArguments.forEach(arg -> System.out.println(" " + arg)); + } + + // 打印内存信息 + private static void printMemoryInfo() { + System.out.println("\n=== 内存信息 ==="); + + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + + // 堆内存 + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + System.out.println("堆内存:"); + printMemoryUsage(heapUsage); + + // 非堆内存 + MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); + System.out.println("非堆内存:"); + printMemoryUsage(nonHeapUsage); + + // 内存池信息 + System.out.println("内存池详情:"); + List memoryPools = ManagementFactory.getMemoryPoolMXBeans(); + memoryPools.forEach(pool -> { + System.out.println(" " + pool.getName() + ":"); + printMemoryUsage(pool.getUsage()); + }); + } + + private static void printMemoryUsage(MemoryUsage usage) { + if (usage != null) { + System.out.println(" 初始: " + (usage.getInit() / 1024 / 1024) + "MB"); + System.out.println(" 已使用: " + (usage.getUsed() / 1024 / 1024) + "MB"); + System.out.println(" 已提交: " + (usage.getCommitted() / 1024 / 1024) + "MB"); + System.out.println(" 最大: " + (usage.getMax() / 1024 / 1024) + "MB"); + } + } + + // 打印线程信息 + private static void printThreadInfo() { + System.out.println("\n=== 线程信息 ==="); + + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + + System.out.println("当前线程数: " + threadBean.getThreadCount()); + System.out.println("守护线程数: " + threadBean.getDaemonThreadCount()); + System.out.println("峰值线程数: " + threadBean.getPeakThreadCount()); + System.out.println("总启动线程数: " + threadBean.getTotalStartedThreadCount()); + + // 获取所有线程信息 + long[] threadIds = threadBean.getAllThreadIds(); + ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds); + + System.out.println("\n线程详情:"); + for (ThreadInfo threadInfo : threadInfos) { + if (threadInfo != null) { + System.out.println(" 线程名: " + threadInfo.getThreadName()); + System.out.println(" 线程状态: " + threadInfo.getThreadState()); + System.out.println(" CPU时间: " + threadBean.getThreadCpuTime(threadInfo.getThreadId()) / 1000000 + "ms"); + System.out.println(" ---"); + } + } + } + + // 打印GC信息 + private static void printGCInfo() { + System.out.println("\n=== 垃圾收集信息 ==="); + + List gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); + + gcBeans.forEach(gcBean -> { + System.out.println("GC收集器: " + gcBean.getName()); + System.out.println(" 收集次数: " + gcBean.getCollectionCount()); + System.out.println(" 收集时间: " + gcBean.getCollectionTime() + "ms"); + System.out.println(" 平均收集时间: " + + (gcBean.getCollectionCount() > 0 ? + gcBean.getCollectionTime() / gcBean.getCollectionCount() : 0) + "ms"); + + String[] memoryPoolNames = gcBean.getMemoryPoolNames(); + System.out.println(" 管理的内存池:"); + for (String poolName : memoryPoolNames) { + System.out.println(" " + poolName); + } + System.out.println(" ---"); + }); + } +} +``` + +## 8. 总结 + +JVM作为Java程序的运行环境,其深入理解对于Java开发者至关重要。本文档涵盖了JVM的核心概念: + +### 8.1 关键知识点 + +1. **JVM架构**:理解类加载子系统、运行时数据区、执行引擎等组件 +2. **内存模型**:掌握堆、栈、方法区等内存区域的作用和特点 +3. **垃圾回收**:了解GC算法、收集器选择和调优策略 +4. **类加载机制**:理解双亲委派模型和类初始化过程 +5. **性能调优**:掌握JVM参数配置和性能优化技巧 +6. **故障诊断**:学会使用工具分析和解决JVM问题 + +### 8.2 最佳实践 + +1. **合理配置JVM参数**:根据应用特点选择合适的堆大小和GC收集器 +2. **避免内存泄漏**:及时清理不需要的对象引用 +3. **优化对象创建**:使用对象池、预分配等技术减少GC压力 +4. **监控JVM状态**:定期检查内存使用、GC频率等指标 +5. **选择合适的数据结构**:根据使用场景选择最优的集合类型 + +### 8.3 持续学习 + +JVM技术在不断发展,建议持续关注: +- 新版本JVM的特性和改进 +- 新的垃圾收集器(如ZGC、Shenandoah) +- JVM性能分析工具的使用 +- 微服务架构下的JVM调优策略 + +通过深入理解JVM原理和实践经验的积累,能够更好地开发高性能、稳定的Java应用程序。 + + diff --git a/docs/aThread/jvmAtomic.md b/docs/aThread/jvmAtomic.md new file mode 100644 index 000000000..2603b35b0 --- /dev/null +++ b/docs/aThread/jvmAtomic.md @@ -0,0 +1,2061 @@ +--- +title: java原子操作类实现原理 +author: 哪吒 +date: '2023-06-15' +--- + +# java原子操作类实现原理 + +## 1. 原子操作概述 + +### 1.1 什么是原子操作 + +原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程切换。在多线程环境中,原子操作能够保证数据的一致性和线程安全性。 + +```java +public class AtomicOperationExample { + + public static void main(String[] args) throws InterruptedException { + demonstrateNonAtomicOperation(); + demonstrateAtomicOperation(); + } + + // 非原子操作示例 + private static void demonstrateNonAtomicOperation() throws InterruptedException { + System.out.println("=== 非原子操作演示 ==="); + + Counter nonAtomicCounter = new Counter(); + + // 创建多个线程同时操作计数器 + Thread[] threads = new Thread[10]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + nonAtomicCounter.increment(); // 非原子操作 + } + }); + } + + // 启动所有线程 + for (Thread thread : threads) { + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("非原子操作结果: " + nonAtomicCounter.getValue()); + System.out.println("期望结果: 10000"); + } + + // 原子操作示例 + private static void demonstrateAtomicOperation() throws InterruptedException { + System.out.println("\n=== 原子操作演示 ==="); + + AtomicCounter atomicCounter = new AtomicCounter(); + + // 创建多个线程同时操作原子计数器 + Thread[] threads = new Thread[10]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + atomicCounter.increment(); // 原子操作 + } + }); + } + + // 启动所有线程 + for (Thread thread : threads) { + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("原子操作结果: " + atomicCounter.getValue()); + System.out.println("期望结果: 10000"); + } +} + +// 非原子计数器 +class Counter { + private int value = 0; + + public void increment() { + value++; // 非原子操作:读取 -> 计算 -> 写入 + } + + public int getValue() { + return value; + } +} + +// 原子计数器 +class AtomicCounter { + private java.util.concurrent.atomic.AtomicInteger value = + new java.util.concurrent.atomic.AtomicInteger(0); + + public void increment() { + value.incrementAndGet(); // 原子操作 + } + + public int getValue() { + return value.get(); + } +} +``` + +### 3.4 字段更新器原子类 + +字段更新器原子类允许对普通类的volatile字段进行原子操作,无需修改原有类的结构。 + +```java +import java.util.concurrent.atomic.*; + +public class AtomicFieldUpdaterExample { + + public static void main(String[] args) throws InterruptedException { + demonstrateAtomicIntegerFieldUpdater(); + demonstrateAtomicLongFieldUpdater(); + demonstrateAtomicReferenceFieldUpdater(); + } + + // AtomicIntegerFieldUpdater演示 + private static void demonstrateAtomicIntegerFieldUpdater() throws InterruptedException { + System.out.println("=== AtomicIntegerFieldUpdater演示 ==="); + + // 创建字段更新器 + AtomicIntegerFieldUpdater scoreUpdater = + AtomicIntegerFieldUpdater.newUpdater(Student.class, "score"); + + Student student = new Student("Alice", 85); + System.out.println("初始学生信息: " + student); + + // 原子更新分数 + int oldScore = scoreUpdater.getAndAdd(student, 10); + System.out.println("getAndAdd(10) - 旧分数: " + oldScore + ", 新分数: " + student.getScore()); + + // CAS操作 + boolean casResult = scoreUpdater.compareAndSet(student, 95, 100); + System.out.println("CAS(95->100)成功: " + casResult + ", 当前分数: " + student.getScore()); + + // 多线程更新演示 + Student sharedStudent = new Student("Bob", 0); + Thread[] threads = new Thread[10]; + + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 100; j++) { + scoreUpdater.incrementAndGet(sharedStudent); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("多线程更新后分数: " + sharedStudent.getScore()); + System.out.println("期望分数: 1000"); + } + + // AtomicLongFieldUpdater演示 + private static void demonstrateAtomicLongFieldUpdater() { + System.out.println("\n=== AtomicLongFieldUpdater演示 ==="); + + AtomicLongFieldUpdater balanceUpdater = + AtomicLongFieldUpdater.newUpdater(Account.class, "balance"); + + Account account = new Account("12345", 1000L); + System.out.println("初始账户: " + account); + + // 存款操作 + long newBalance = balanceUpdater.addAndGet(account, 500L); + System.out.println("存款500后余额: " + newBalance); + + // 取款操作(使用CAS确保余额充足) + long currentBalance = balanceUpdater.get(account); + long withdrawAmount = 200L; + + if (currentBalance >= withdrawAmount) { + boolean success = balanceUpdater.compareAndSet(account, currentBalance, currentBalance - withdrawAmount); + if (success) { + System.out.println("取款" + withdrawAmount + "成功,余额: " + account.getBalance()); + } else { + System.out.println("取款失败:余额已被其他操作修改"); + } + } else { + System.out.println("取款失败:余额不足"); + } + } + + // AtomicReferenceFieldUpdater演示 + private static void demonstrateAtomicReferenceFieldUpdater() { + System.out.println("\n=== AtomicReferenceFieldUpdater演示 ==="); + + AtomicReferenceFieldUpdater statusUpdater = + AtomicReferenceFieldUpdater.newUpdater(Order.class, OrderStatus.class, "status"); + + Order order = new Order("ORDER-001", OrderStatus.PENDING); + System.out.println("初始订单: " + order); + + // 状态转换:PENDING -> PROCESSING + boolean success1 = statusUpdater.compareAndSet(order, OrderStatus.PENDING, OrderStatus.PROCESSING); + System.out.println("状态转换(PENDING->PROCESSING)成功: " + success1 + ", 当前状态: " + order.getStatus()); + + // 状态转换:PROCESSING -> COMPLETED + boolean success2 = statusUpdater.compareAndSet(order, OrderStatus.PROCESSING, OrderStatus.COMPLETED); + System.out.println("状态转换(PROCESSING->COMPLETED)成功: " + success2 + ", 当前状态: " + order.getStatus()); + + // 尝试非法状态转换:COMPLETED -> PENDING(应该失败) + boolean success3 = statusUpdater.compareAndSet(order, OrderStatus.COMPLETED, OrderStatus.PENDING); + System.out.println("状态转换(COMPLETED->PENDING)成功: " + success3 + ", 当前状态: " + order.getStatus()); + } +} + +// 学生类 +class Student { + private final String name; + public volatile int score; // 必须是public volatile字段 + + public Student(String name, int score) { + this.name = name; + this.score = score; + } + + public String getName() { + return name; + } + + public int getScore() { + return score; + } + + @Override + public String toString() { + return "Student{name='" + name + "', score=" + score + "}"; + } +} + +// 账户类 +class Account { + private final String accountNumber; + public volatile long balance; // 必须是public volatile字段 + + public Account(String accountNumber, long balance) { + this.accountNumber = accountNumber; + this.balance = balance; + } + + public String getAccountNumber() { + return accountNumber; + } + + public long getBalance() { + return balance; + } + + @Override + public String toString() { + return "Account{accountNumber='" + accountNumber + "', balance=" + balance + "}"; + } +} + +// 订单状态枚举 +enum OrderStatus { + PENDING, PROCESSING, COMPLETED, CANCELLED +} + +// 订单类 +class Order { + private final String orderId; + public volatile OrderStatus status; // 必须是public volatile字段 + + public Order(String orderId, OrderStatus status) { + this.orderId = orderId; + this.status = status; + } + + public String getOrderId() { + return orderId; + } + + public OrderStatus getStatus() { + return status; + } + + @Override + public String toString() { + return "Order{orderId='" + orderId + "', status=" + status + "}"; + } +} +``` + +### 3.5 高性能原子类(Java 8+) + +```java +import java.util.concurrent.atomic.*; +import java.util.concurrent.ThreadLocalRandom; + +public class HighPerformanceAtomicExample { + + public static void main(String[] args) throws InterruptedException { + demonstrateLongAdder(); + demonstrateLongAccumulator(); + demonstrateDoubleAdder(); + demonstrateDoubleAccumulator(); + performanceComparison(); + } + + // LongAdder演示 + private static void demonstrateLongAdder() { + System.out.println("=== LongAdder演示 ==="); + + LongAdder longAdder = new LongAdder(); + + // 基本操作 + longAdder.add(10); + longAdder.increment(); + longAdder.add(5); + + System.out.println("LongAdder当前值: " + longAdder.sum()); + System.out.println("LongAdder字符串表示: " + longAdder.toString()); + + // 重置 + longAdder.reset(); + System.out.println("重置后值: " + longAdder.sum()); + + // 求和并重置 + longAdder.add(100); + long sumAndReset = longAdder.sumThenReset(); + System.out.println("sumThenReset结果: " + sumAndReset + ", 当前值: " + longAdder.sum()); + } + + // LongAccumulator演示 + private static void demonstrateLongAccumulator() { + System.out.println("\n=== LongAccumulator演示 ==="); + + // 求最大值的累加器 + LongAccumulator maxAccumulator = new LongAccumulator(Long::max, Long.MIN_VALUE); + + maxAccumulator.accumulate(10); + maxAccumulator.accumulate(5); + maxAccumulator.accumulate(20); + maxAccumulator.accumulate(15); + + System.out.println("最大值累加器结果: " + maxAccumulator.get()); + + // 求乘积的累加器 + LongAccumulator productAccumulator = new LongAccumulator((x, y) -> x * y, 1); + + productAccumulator.accumulate(2); + productAccumulator.accumulate(3); + productAccumulator.accumulate(4); + + System.out.println("乘积累加器结果: " + productAccumulator.get()); + + // 自定义操作:计算平方和 + LongAccumulator squareSumAccumulator = new LongAccumulator((sum, value) -> sum + value * value, 0); + + squareSumAccumulator.accumulate(1); // 1^2 = 1 + squareSumAccumulator.accumulate(2); // 2^2 = 4 + squareSumAccumulator.accumulate(3); // 3^2 = 9 + + System.out.println("平方和累加器结果: " + squareSumAccumulator.get()); // 1 + 4 + 9 = 14 + } + + // DoubleAdder演示 + private static void demonstrateDoubleAdder() { + System.out.println("\n=== DoubleAdder演示 ==="); + + DoubleAdder doubleAdder = new DoubleAdder(); + + doubleAdder.add(3.14); + doubleAdder.add(2.71); + doubleAdder.add(1.41); + + System.out.println("DoubleAdder当前值: " + doubleAdder.sum()); + + // 重置 + double sumBeforeReset = doubleAdder.sumThenReset(); + System.out.println("sumThenReset结果: " + sumBeforeReset + ", 当前值: " + doubleAdder.sum()); + } + + // DoubleAccumulator演示 + private static void demonstrateDoubleAccumulator() { + System.out.println("\n=== DoubleAccumulator演示 ==="); + + // 求平均值的累加器(简化版) + DoubleAccumulator avgAccumulator = new DoubleAccumulator((avg, value) -> (avg + value) / 2, 0.0); + + avgAccumulator.accumulate(10.0); + avgAccumulator.accumulate(20.0); + avgAccumulator.accumulate(30.0); + + System.out.println("平均值累加器结果: " + avgAccumulator.get()); + + // 求最小值的累加器 + DoubleAccumulator minAccumulator = new DoubleAccumulator(Double::min, Double.MAX_VALUE); + + minAccumulator.accumulate(3.14); + minAccumulator.accumulate(2.71); + minAccumulator.accumulate(1.41); + minAccumulator.accumulate(4.67); + + System.out.println("最小值累加器结果: " + minAccumulator.get()); + } + + // 性能比较 + private static void performanceComparison() throws InterruptedException { + System.out.println("\n=== 性能比较 ==="); + + final int THREAD_COUNT = 10; + final int OPERATIONS_PER_THREAD = 1000000; + + // AtomicLong性能测试 + AtomicLong atomicLong = new AtomicLong(0); + long startTime = System.currentTimeMillis(); + + Thread[] atomicThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + atomicThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + atomicLong.incrementAndGet(); + } + }); + } + + for (Thread thread : atomicThreads) { + thread.start(); + } + + for (Thread thread : atomicThreads) { + thread.join(); + } + + long atomicTime = System.currentTimeMillis() - startTime; + System.out.println("AtomicLong耗时: " + atomicTime + "ms, 结果: " + atomicLong.get()); + + // LongAdder性能测试 + LongAdder longAdder = new LongAdder(); + startTime = System.currentTimeMillis(); + + Thread[] adderThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + adderThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + longAdder.increment(); + } + }); + } + + for (Thread thread : adderThreads) { + thread.start(); + } + + for (Thread thread : adderThreads) { + thread.join(); + } + + long adderTime = System.currentTimeMillis() - startTime; + System.out.println("LongAdder耗时: " + adderTime + "ms, 结果: " + longAdder.sum()); + System.out.println("性能提升: " + (atomicTime / (double) adderTime) + "倍"); + + // 高竞争环境下的性能测试 + demonstrateHighContentionPerformance(); + } + + // 高竞争环境性能测试 + private static void demonstrateHighContentionPerformance() throws InterruptedException { + System.out.println("\n--- 高竞争环境性能测试 ---"); + + final int THREAD_COUNT = 50; + final int OPERATIONS_PER_THREAD = 100000; + + // AtomicLong在高竞争环境下 + AtomicLong atomicLong = new AtomicLong(0); + long startTime = System.currentTimeMillis(); + + Thread[] atomicThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + atomicThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + atomicLong.addAndGet(ThreadLocalRandom.current().nextInt(1, 10)); + } + }); + } + + for (Thread thread : atomicThreads) { + thread.start(); + } + + for (Thread thread : atomicThreads) { + thread.join(); + } + + long atomicTime = System.currentTimeMillis() - startTime; + System.out.println("高竞争AtomicLong耗时: " + atomicTime + "ms"); + + // LongAdder在高竞争环境下 + LongAdder longAdder = new LongAdder(); + startTime = System.currentTimeMillis(); + + Thread[] adderThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + adderThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + longAdder.add(ThreadLocalRandom.current().nextInt(1, 10)); + } + }); + } + + for (Thread thread : adderThreads) { + thread.start(); + } + + for (Thread thread : adderThreads) { + thread.join(); + } + + long adderTime = System.currentTimeMillis() - startTime; + System.out.println("高竞争LongAdder耗时: " + adderTime + "ms"); + System.out.println("高竞争环境性能提升: " + (atomicTime / (double) adderTime) + "倍"); + } +} +``` + +## 4. 底层实现原理 + +### 4.1 Unsafe类的作用 + +原子操作类的底层实现依赖于`sun.misc.Unsafe`类,它提供了直接操作内存的能力。 + +```java +import sun.misc.Unsafe; +import java.lang.reflect.Field; + +public class UnsafeExample { + + private static final Unsafe unsafe; + private static final long valueOffset; + + static { + try { + // 通过反射获取Unsafe实例 + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + unsafe = (Unsafe) field.get(null); + + // 获取value字段的内存偏移量 + valueOffset = unsafe.objectFieldOffset(UnsafeExample.class.getDeclaredField("value")); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private volatile int value = 0; + + public static void main(String[] args) { + demonstrateUnsafeOperations(); + demonstrateCustomAtomicInteger(); + } + + // Unsafe操作演示 + private static void demonstrateUnsafeOperations() { + System.out.println("=== Unsafe操作演示 ==="); + + UnsafeExample example = new UnsafeExample(); + + // 直接内存读取 + int currentValue = unsafe.getIntVolatile(example, valueOffset); + System.out.println("当前值: " + currentValue); + + // 直接内存写入 + unsafe.putIntVolatile(example, valueOffset, 42); + System.out.println("设置后值: " + example.value); + + // CAS操作 + boolean casResult = unsafe.compareAndSwapInt(example, valueOffset, 42, 100); + System.out.println("CAS(42->100)成功: " + casResult + ", 当前值: " + example.value); + + // 获取并增加 + int oldValue = unsafe.getAndAddInt(example, valueOffset, 10); + System.out.println("getAndAdd(10) - 旧值: " + oldValue + ", 新值: " + example.value); + } + + // 自定义原子整数实现 + private static void demonstrateCustomAtomicInteger() { + System.out.println("\n=== 自定义原子整数演示 ==="); + + CustomAtomicInteger customAtomic = new CustomAtomicInteger(0); + + System.out.println("初始值: " + customAtomic.get()); + + customAtomic.set(50); + System.out.println("设置后: " + customAtomic.get()); + + int incrementResult = customAtomic.incrementAndGet(); + System.out.println("incrementAndGet: " + incrementResult); + + boolean casResult = customAtomic.compareAndSet(51, 100); + System.out.println("compareAndSet(51->100): " + casResult + ", 当前值: " + customAtomic.get()); + } +} + +// 自定义原子整数类 +class CustomAtomicInteger { + private static final Unsafe unsafe; + private static final long valueOffset; + + static { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + unsafe = (Unsafe) field.get(null); + valueOffset = unsafe.objectFieldOffset(CustomAtomicInteger.class.getDeclaredField("value")); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private volatile int value; + + public CustomAtomicInteger(int initialValue) { + this.value = initialValue; + } + + public final int get() { + return value; + } + + public final void set(int newValue) { + value = newValue; + } + + public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); + } + + public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); + } + + public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; + } + + public final int addAndGet(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta) + delta; + } +} +``` + +### 4.2 内存模型与可见性 + +```java +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public class MemoryModelExample { + + public static void main(String[] args) throws InterruptedException { + demonstrateVolatileVsAtomic(); + demonstrateMemoryOrdering(); + demonstrateHappensBefore(); + } + + // volatile与原子类的可见性比较 + private static void demonstrateVolatileVsAtomic() throws InterruptedException { + System.out.println("=== volatile与原子类可见性比较 ==="); + + VolatileCounter volatileCounter = new VolatileCounter(); + AtomicCounter atomicCounter = new AtomicCounter(); + + // 测试volatile的可见性 + Thread volatileWriter = new Thread(() -> { + for (int i = 0; i < 1000; i++) { + volatileCounter.increment(); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + + Thread volatileReader = new Thread(() -> { + int lastValue = 0; + for (int i = 0; i < 100; i++) { + int currentValue = volatileCounter.getValue(); + if (currentValue != lastValue) { + System.out.println("Volatile读取到新值: " + currentValue); + lastValue = currentValue; + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + + volatileWriter.start(); + volatileReader.start(); + + Thread.sleep(2000); + + System.out.println("Volatile最终值: " + volatileCounter.getValue()); + System.out.println("Atomic最终值: " + atomicCounter.getValue()); + } + + // 内存排序演示 + private static void demonstrateMemoryOrdering() { + System.out.println("\n=== 内存排序演示 ==="); + + MemoryOrderingExample example = new MemoryOrderingExample(); + + // 启动多个线程进行读写操作 + for (int i = 0; i < 5; i++) { + final int threadId = i; + new Thread(() -> { + example.writeData(threadId, "Data-" + threadId); + }).start(); + + new Thread(() -> { + String data = example.readData(threadId); + if (data != null) { + System.out.println("线程" + threadId + "读取到: " + data); + } + }).start(); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // happens-before关系演示 + private static void demonstrateHappensBefore() { + System.out.println("\n=== happens-before关系演示 ==="); + + HappensBeforeExample example = new HappensBeforeExample(); + + Thread producer = new Thread(() -> { + example.produce("重要数据"); + }); + + Thread consumer = new Thread(() -> { + String data = example.consume(); + System.out.println("消费者获取到: " + data); + }); + + producer.start(); + + try { + Thread.sleep(100); // 确保生产者先执行 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + consumer.start(); + + try { + producer.join(); + consumer.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} + +// volatile计数器 +class VolatileCounter { + private volatile int value = 0; + + public void increment() { + value++; // 注意:这不是原子操作 + } + + public int getValue() { + return value; + } +} + +// 原子计数器 +class AtomicCounter { + private final AtomicInteger value = new AtomicInteger(0); + + public void increment() { + value.incrementAndGet(); + } + + public int getValue() { + return value.get(); + } +} + +// 内存排序示例 +class MemoryOrderingExample { + private final AtomicReference[] data = new AtomicReference[10]; + private final AtomicInteger writeIndex = new AtomicInteger(0); + + public MemoryOrderingExample() { + for (int i = 0; i < data.length; i++) { + data[i] = new AtomicReference<>(); + } + } + + public void writeData(int index, String value) { + if (index < data.length) { + data[index].set(value); + writeIndex.set(index + 1); // 原子操作确保可见性 + } + } + + public String readData(int index) { + if (index < writeIndex.get() && index < data.length) { + return data[index].get(); + } + return null; + } +} + +// happens-before关系示例 +class HappensBeforeExample { + private volatile String data; + private final AtomicInteger flag = new AtomicInteger(0); + + public void produce(String value) { + data = value; // 写入数据 + flag.set(1); // 原子操作建立happens-before关系 + System.out.println("生产者设置数据: " + value); + } + + public String consume() { + while (flag.get() == 0) { + // 等待数据准备就绪 + Thread.yield(); + } + return data; // 由于happens-before关系,这里能看到最新的data值 + } +} +``` + +## 5. 性能分析与优化 + +### 5.1 性能特点分析 + +```java +import java.util.concurrent.atomic.*; +import java.util.concurrent.*; + +public class PerformanceAnalysisExample { + + public static void main(String[] args) throws InterruptedException { + analyzeContentionImpact(); + analyzeCacheLineEffect(); + analyzeMemoryFootprint(); + } + + // 竞争程度对性能的影响 + private static void analyzeContentionImpact() throws InterruptedException { + System.out.println("=== 竞争程度对性能的影响分析 ==="); + + int[] threadCounts = {1, 2, 4, 8, 16, 32}; + final int OPERATIONS_PER_THREAD = 1000000; + + for (int threadCount : threadCounts) { + // AtomicLong测试 + AtomicLong atomicLong = new AtomicLong(0); + long startTime = System.nanoTime(); + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + atomicLong.incrementAndGet(); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long atomicTime = System.nanoTime() - startTime; + + // LongAdder测试 + LongAdder longAdder = new LongAdder(); + startTime = System.nanoTime(); + + threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + longAdder.increment(); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long adderTime = System.nanoTime() - startTime; + + System.out.printf("线程数: %2d, AtomicLong: %8.2fms, LongAdder: %8.2fms, 性能比: %.2f\n", + threadCount, atomicTime / 1_000_000.0, adderTime / 1_000_000.0, + (double) atomicTime / adderTime); + } + } + + // 缓存行效应分析 + private static void analyzeCacheLineEffect() throws InterruptedException { + System.out.println("\n=== 缓存行效应分析 ==="); + + final int THREAD_COUNT = 4; + final int OPERATIONS_PER_THREAD = 10000000; + + // 紧密排列的原子变量(可能在同一缓存行) + AtomicLong[] tightArray = new AtomicLong[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + tightArray[i] = new AtomicLong(0); + } + + long startTime = System.nanoTime(); + Thread[] tightThreads = new Thread[THREAD_COUNT]; + + for (int i = 0; i < THREAD_COUNT; i++) { + final int index = i; + tightThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + tightArray[index].incrementAndGet(); + } + }); + } + + for (Thread thread : tightThreads) { + thread.start(); + } + + for (Thread thread : tightThreads) { + thread.join(); + } + + long tightTime = System.nanoTime() - startTime; + + // 填充分离的原子变量(避免伪共享) + PaddedAtomicLong[] paddedArray = new PaddedAtomicLong[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + paddedArray[i] = new PaddedAtomicLong(); + } + + startTime = System.nanoTime(); + Thread[] paddedThreads = new Thread[THREAD_COUNT]; + + for (int i = 0; i < THREAD_COUNT; i++) { + final int index = i; + paddedThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + paddedArray[index].incrementAndGet(); + } + }); + } + + for (Thread thread : paddedThreads) { + thread.start(); + } + + for (Thread thread : paddedThreads) { + thread.join(); + } + + long paddedTime = System.nanoTime() - startTime; + + System.out.printf("紧密排列耗时: %.2fms\n", tightTime / 1_000_000.0); + System.out.printf("填充分离耗时: %.2fms\n", paddedTime / 1_000_000.0); + System.out.printf("性能提升: %.2f倍\n", (double) tightTime / paddedTime); + } + + // 内存占用分析 + private static void analyzeMemoryFootprint() { + System.out.println("\n=== 内存占用分析 ==="); + + Runtime runtime = Runtime.getRuntime(); + + // 测试AtomicInteger内存占用 + runtime.gc(); + long beforeAtomic = runtime.totalMemory() - runtime.freeMemory(); + + AtomicInteger[] atomicInts = new AtomicInteger[1000000]; + for (int i = 0; i < atomicInts.length; i++) { + atomicInts[i] = new AtomicInteger(i); + } + + runtime.gc(); + long afterAtomic = runtime.totalMemory() - runtime.freeMemory(); + + System.out.println("AtomicInteger数组内存占用: " + (afterAtomic - beforeAtomic) / 1024 / 1024 + "MB"); + + // 测试普通int数组内存占用 + atomicInts = null; // 释放引用 + runtime.gc(); + long beforeInt = runtime.totalMemory() - runtime.freeMemory(); + + int[] ints = new int[1000000]; + for (int i = 0; i < ints.length; i++) { + ints[i] = i; + } + + runtime.gc(); + long afterInt = runtime.totalMemory() - runtime.freeMemory(); + + System.out.println("int数组内存占用: " + (afterInt - beforeInt) / 1024 / 1024 + "MB"); + System.out.println("内存开销比例: " + (double)(afterAtomic - beforeAtomic) / (afterInt - beforeInt)); + } +} + +// 填充的原子长整型(避免伪共享) +class PaddedAtomicLong { + // 前填充 + private long p1, p2, p3, p4, p5, p6, p7; + private final AtomicLong value = new AtomicLong(0); + // 后填充 + private long p8, p9, p10, p11, p12, p13, p14; + + public long incrementAndGet() { + return value.incrementAndGet(); + } + + public long get() { + return value.get(); + } +} +``` + +## 6. 最佳实践与注意事项 + +### 6.1 选择合适的原子类 + +```java +import java.util.concurrent.atomic.*; + +public class BestPracticesExample { + + public static void main(String[] args) { + demonstrateProperAtomicSelection(); + demonstrateCommonPitfalls(); + demonstrateOptimizationTechniques(); + } + + // 正确选择原子类 + private static void demonstrateProperAtomicSelection() { + System.out.println("=== 正确选择原子类 ==="); + + // 1. 简单计数器:优先使用LongAdder + System.out.println("\n1. 计数器场景:"); + LongAdder counter = new LongAdder(); + System.out.println("推荐使用LongAdder进行高并发计数"); + + // 2. 需要精确值的场景:使用AtomicLong + System.out.println("\n2. 需要精确值场景:"); + AtomicLong preciseCounter = new AtomicLong(0); + System.out.println("需要实时精确值时使用AtomicLong"); + + // 3. 复杂累积操作:使用LongAccumulator + System.out.println("\n3. 复杂累积操作:"); + LongAccumulator maxTracker = new LongAccumulator(Long::max, Long.MIN_VALUE); + System.out.println("复杂累积逻辑使用LongAccumulator"); + + // 4. 对象引用更新:使用AtomicReference + System.out.println("\n4. 对象引用更新:"); + AtomicReference configRef = new AtomicReference<>("default-config"); + System.out.println("对象引用的原子更新使用AtomicReference"); + + // 5. 解决ABA问题:使用AtomicStampedReference + System.out.println("\n5. 解决ABA问题:"); + AtomicStampedReference stampedRef = new AtomicStampedReference<>("initial", 0); + System.out.println("需要版本控制时使用AtomicStampedReference"); + } + + // 常见陷阱 + private static void demonstrateCommonPitfalls() { + System.out.println("\n=== 常见陷阱与解决方案 ==="); + + // 陷阱1:复合操作不是原子的 + System.out.println("\n陷阱1: 复合操作不是原子的"); + AtomicInteger atomicInt = new AtomicInteger(0); + + // 错误做法 + System.out.println("错误: if (atomicInt.get() == 0) atomicInt.set(1);"); + + // 正确做法 + boolean success = atomicInt.compareAndSet(0, 1); + System.out.println("正确: compareAndSet(0, 1) = " + success); + + // 陷阱2:过度使用原子类 + System.out.println("\n陷阱2: 过度使用原子类"); + demonstrateOveruseOfAtomics(); + + // 陷阱3:忽略ABA问题 + System.out.println("\n陷阱3: 忽略ABA问题"); + demonstrateABAProblemSolution(); + + // 陷阱4:性能误解 + System.out.println("\n陷阱4: 性能误解"); + demonstratePerformanceMisconceptions(); + } + + // 过度使用原子类的问题 + private static void demonstrateOveruseOfAtomics() { + // 错误:为每个字段都使用原子类 + class BadExample { + private AtomicInteger x = new AtomicInteger(0); + private AtomicInteger y = new AtomicInteger(0); + private AtomicReference name = new AtomicReference<>("default"); + + // 这样的操作仍然不是原子的 + public void badUpdate() { + x.incrementAndGet(); + y.incrementAndGet(); + name.set("updated"); + } + } + + // 正确:使用适当的同步机制 + class GoodExample { + private int x = 0; + private int y = 0; + private String name = "default"; + + public synchronized void goodUpdate() { + x++; + y++; + name = "updated"; + } + } + + System.out.println("避免为每个字段都使用原子类,考虑整体同步策略"); + } + + // ABA问题解决方案 + private static void demonstrateABAProblemSolution() { + // 使用版本号解决ABA问题 + AtomicStampedReference versionedRef = new AtomicStampedReference<>("A", 0); + + // 模拟ABA操作 + int[] stampHolder = new int[1]; + String value = versionedRef.get(stampHolder); + int stamp = stampHolder[0]; + + // 即使值被改回"A",版本号也会不同 + versionedRef.compareAndSet("A", "B", stamp, stamp + 1); + versionedRef.compareAndSet("B", "A", stamp + 1, stamp + 2); + + // 原始的CAS操作会失败,因为版本号已经改变 + boolean success = versionedRef.compareAndSet(value, "C", stamp, stamp + 1); + System.out.println("使用版本号避免ABA问题,CAS成功: " + success); + } + + // 性能误解 + private static void demonstratePerformanceMisconceptions() { + System.out.println("误解1: 原子类总是比synchronized快"); + System.out.println("事实: 在低竞争环境下原子类更快,高竞争时synchronized可能更好"); + + System.out.println("\n误解2: 所有原子操作性能相同"); + System.out.println("事实: 不同操作有不同的性能特征"); + + // 演示不同操作的性能差异 + AtomicLong atomicLong = new AtomicLong(0); + + long startTime = System.nanoTime(); + for (int i = 0; i < 1000000; i++) { + atomicLong.get(); + } + long getTime = System.nanoTime() - startTime; + + startTime = System.nanoTime(); + for (int i = 0; i < 1000000; i++) { + atomicLong.incrementAndGet(); + } + long incrementTime = System.nanoTime() - startTime; + + startTime = System.nanoTime(); + for (int i = 0; i < 1000000; i++) { + atomicLong.compareAndSet(i, i + 1); + } + long casTime = System.nanoTime() - startTime; + + System.out.printf("get操作: %.2fms, increment操作: %.2fms, CAS操作: %.2fms\n", + getTime / 1_000_000.0, incrementTime / 1_000_000.0, casTime / 1_000_000.0); + } + + // 优化技术 + private static void demonstrateOptimizationTechniques() { + System.out.println("\n=== 优化技术 ==="); + + // 技术1:批量操作 + System.out.println("\n技术1: 批量操作"); + demonstrateBatchOperations(); + + // 技术2:减少竞争 + System.out.println("\n技术2: 减少竞争"); + demonstrateContentionReduction(); + + // 技术3:选择合适的数据结构 + System.out.println("\n技术3: 选择合适的数据结构"); + demonstrateDataStructureSelection(); + } + + // 批量操作优化 + private static void demonstrateBatchOperations() { + LongAdder adder = new LongAdder(); + + // 不好的做法:频繁的小操作 + long startTime = System.nanoTime(); + for (int i = 0; i < 1000000; i++) { + adder.increment(); + } + long individualTime = System.nanoTime() - startTime; + + // 更好的做法:批量操作 + adder.reset(); + startTime = System.nanoTime(); + for (int i = 0; i < 10000; i++) { + adder.add(100); // 批量添加 + } + long batchTime = System.nanoTime() - startTime; + + System.out.printf("单个操作: %.2fms, 批量操作: %.2fms, 性能提升: %.2f倍\n", + individualTime / 1_000_000.0, batchTime / 1_000_000.0, + (double) individualTime / batchTime); + } + + // 减少竞争 + private static void demonstrateContentionReduction() { + System.out.println("使用ThreadLocal减少竞争:"); + + // 使用ThreadLocal进行本地累积,最后合并 + ThreadLocal localCounter = ThreadLocal.withInitial(() -> 0L); + AtomicLong globalCounter = new AtomicLong(0); + + // 在线程本地累积 + localCounter.set(localCounter.get() + 100); + + // 定期合并到全局计数器 + globalCounter.addAndGet(localCounter.get()); + localCounter.set(0L); + + System.out.println("ThreadLocal策略可以显著减少竞争"); + } + + // 数据结构选择 + private static void demonstrateDataStructureSelection() { + System.out.println("根据使用模式选择数据结构:"); + + // 频繁更新,偶尔读取:使用LongAdder + LongAdder frequentUpdate = new LongAdder(); + System.out.println("频繁更新场景: 使用LongAdder"); + + // 频繁读取,偶尔更新:使用AtomicLong + AtomicLong frequentRead = new AtomicLong(0); + System.out.println("频繁读取场景: 使用AtomicLong"); + + // 复杂状态管理:考虑使用锁 + System.out.println("复杂状态管理: 考虑使用ReentrantLock"); + } +} +``` + +## 7. 总结 + +Java原子操作类是并发编程中的重要工具,它们基于CAS机制提供了高性能的线程安全操作。通过本文的深入分析,我们了解了: + +### 7.1 核心要点 + +1. **CAS机制**:原子操作的核心,提供了无锁的线程安全保证 +2. **分类体系**:基本类型、数组类型、引用类型、字段更新器、高性能类型 +3. **底层实现**:依赖Unsafe类直接操作内存 +4. **性能特征**:在不同竞争程度下表现不同 + +### 7.2 使用建议 + +1. **选择合适的类型**:根据具体场景选择最适合的原子类 +2. **避免常见陷阱**:注意复合操作、ABA问题、过度使用等 +3. **性能优化**:考虑批量操作、减少竞争、合适的数据结构 +4. **测试验证**:在实际环境中测试性能表现 + +### 7.3 发展趋势 + +随着硬件和JVM的发展,原子操作类的性能和功能还在不断改进。Java 9+引入的VarHandle提供了更灵活的内存访问方式,未来可能会有更多高性能的并发工具出现。 + +原子操作类是现代Java并发编程的基石,掌握其原理和最佳实践对于编写高性能、线程安全的应用程序至关重要。 + +### 1.2 原子操作的重要性 + +在多线程环境中,普通的读-修改-写操作不是原子的,可能导致数据竞争和不一致的结果。原子操作类提供了线程安全的操作,避免了使用锁带来的性能开销。 + +## 2. CAS(Compare-And-Swap)机制 + +### 2.1 CAS原理 + +CAS是原子操作类的核心实现机制,它包含三个操作数: +- 内存位置(V) +- 预期原值(A) +- 新值(B) + +CAS操作的逻辑是:如果内存位置V的值等于预期原值A,则将该位置更新为新值B,否则不做任何操作。整个过程是原子的。 + +```java +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public class CASMechanismExample { + + public static void main(String[] args) { + demonstrateCASOperation(); + demonstrateABAProblems(); + demonstrateCASPerformance(); + } + + // CAS操作演示 + private static void demonstrateCASOperation() { + System.out.println("=== CAS操作演示 ==="); + + AtomicInteger atomicInt = new AtomicInteger(10); + + // CAS成功的情况 + boolean success1 = atomicInt.compareAndSet(10, 20); + System.out.println("CAS(10->20)成功: " + success1 + ", 当前值: " + atomicInt.get()); + + // CAS失败的情况 + boolean success2 = atomicInt.compareAndSet(10, 30); + System.out.println("CAS(10->30)成功: " + success2 + ", 当前值: " + atomicInt.get()); + + // 正确的CAS操作 + boolean success3 = atomicInt.compareAndSet(20, 30); + System.out.println("CAS(20->30)成功: " + success3 + ", 当前值: " + atomicInt.get()); + } + + // ABA问题演示 + private static void demonstrateABAProblems() { + System.out.println("\n=== ABA问题演示 ==="); + + AtomicReference atomicRef = new AtomicReference<>("A"); + + // 模拟ABA问题 + Thread thread1 = new Thread(() -> { + String original = atomicRef.get(); + System.out.println("Thread1读取原值: " + original); + + // 模拟一些处理时间 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 尝试CAS操作 + boolean success = atomicRef.compareAndSet(original, "C"); + System.out.println("Thread1 CAS(A->C)成功: " + success + ", 当前值: " + atomicRef.get()); + }); + + Thread thread2 = new Thread(() -> { + try { + Thread.sleep(100); // 确保thread1先读取 + + // 执行A->B->A的操作 + atomicRef.compareAndSet("A", "B"); + System.out.println("Thread2执行A->B, 当前值: " + atomicRef.get()); + + atomicRef.compareAndSet("B", "A"); + System.out.println("Thread2执行B->A, 当前值: " + atomicRef.get()); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + thread1.start(); + thread2.start(); + + try { + thread1.join(); + thread2.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // CAS性能演示 + private static void demonstrateCASPerformance() { + System.out.println("\n=== CAS性能演示 ==="); + + final int THREAD_COUNT = 10; + final int OPERATIONS_PER_THREAD = 100000; + + // 使用synchronized的计数器 + SynchronizedCounter syncCounter = new SynchronizedCounter(); + long startTime = System.currentTimeMillis(); + + Thread[] syncThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + syncThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + syncCounter.increment(); + } + }); + } + + for (Thread thread : syncThreads) { + thread.start(); + } + + try { + for (Thread thread : syncThreads) { + thread.join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + long syncTime = System.currentTimeMillis() - startTime; + System.out.println("Synchronized耗时: " + syncTime + "ms, 结果: " + syncCounter.getValue()); + + // 使用CAS的计数器 + AtomicInteger casCounter = new AtomicInteger(0); + startTime = System.currentTimeMillis(); + + Thread[] casThreads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + casThreads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + casCounter.incrementAndGet(); + } + }); + } + + for (Thread thread : casThreads) { + thread.start(); + } + + try { + for (Thread thread : casThreads) { + thread.join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + long casTime = System.currentTimeMillis() - startTime; + System.out.println("CAS耗时: " + casTime + "ms, 结果: " + casCounter.get()); + System.out.println("性能提升: " + (syncTime / (double) casTime) + "倍"); + } +} + +// 使用synchronized的计数器 +class SynchronizedCounter { + private int value = 0; + + public synchronized void increment() { + value++; + } + + public synchronized int getValue() { + return value; + } +} +``` + +### 2.2 CAS的优缺点 + +**优点:** +- 无锁操作,避免了线程阻塞 +- 性能较高,特别是在低竞争环境下 +- 避免了死锁问题 + +**缺点:** +- ABA问题:值被改变后又改回原值 +- 循环时间长开销大:高竞争环境下可能导致大量自旋 +- 只能保证一个共享变量的原子操作 + +## 3. 原子类的分类 + +### 3.1 基本类型原子类 + +```java +import java.util.concurrent.atomic.*; + +public class BasicAtomicTypesExample { + + public static void main(String[] args) { + demonstrateAtomicInteger(); + demonstrateAtomicLong(); + demonstrateAtomicBoolean(); + } + + // AtomicInteger演示 + private static void demonstrateAtomicInteger() { + System.out.println("=== AtomicInteger演示 ==="); + + AtomicInteger atomicInt = new AtomicInteger(0); + + // 基本操作 + System.out.println("初始值: " + atomicInt.get()); + + // 设置值 + atomicInt.set(10); + System.out.println("设置后: " + atomicInt.get()); + + // 获取并设置 + int oldValue = atomicInt.getAndSet(20); + System.out.println("getAndSet - 旧值: " + oldValue + ", 新值: " + atomicInt.get()); + + // 增加操作 + int incrementResult = atomicInt.incrementAndGet(); + System.out.println("incrementAndGet: " + incrementResult); + + int getAndIncrement = atomicInt.getAndIncrement(); + System.out.println("getAndIncrement - 返回值: " + getAndIncrement + ", 当前值: " + atomicInt.get()); + + // 加法操作 + int addResult = atomicInt.addAndGet(5); + System.out.println("addAndGet(5): " + addResult); + + // CAS操作 + boolean casResult = atomicInt.compareAndSet(27, 30); + System.out.println("compareAndSet(27->30): " + casResult + ", 当前值: " + atomicInt.get()); + + // 弱CAS操作(可能虚假失败) + boolean weakCasResult = atomicInt.weakCompareAndSet(30, 35); + System.out.println("weakCompareAndSet(30->35): " + weakCasResult + ", 当前值: " + atomicInt.get()); + } + + // AtomicLong演示 + private static void demonstrateAtomicLong() { + System.out.println("\n=== AtomicLong演示 ==="); + + AtomicLong atomicLong = new AtomicLong(1000000000L); + + System.out.println("初始值: " + atomicLong.get()); + + // 大数值操作 + long result = atomicLong.addAndGet(2000000000L); + System.out.println("addAndGet(2000000000): " + result); + + // 减法操作 + long decrementResult = atomicLong.decrementAndGet(); + System.out.println("decrementAndGet: " + decrementResult); + + // 自定义操作(Java 8+) + long updateResult = atomicLong.updateAndGet(value -> value * 2); + System.out.println("updateAndGet(value * 2): " + updateResult); + + // 累加操作 + long accumulateResult = atomicLong.accumulateAndGet(1000, (current, update) -> current + update); + System.out.println("accumulateAndGet(1000, +): " + accumulateResult); + } + + // AtomicBoolean演示 + private static void demonstrateAtomicBoolean() { + System.out.println("\n=== AtomicBoolean演示 ==="); + + AtomicBoolean atomicBoolean = new AtomicBoolean(false); + + System.out.println("初始值: " + atomicBoolean.get()); + + // 设置值 + atomicBoolean.set(true); + System.out.println("设置后: " + atomicBoolean.get()); + + // 获取并设置 + boolean oldValue = atomicBoolean.getAndSet(false); + System.out.println("getAndSet - 旧值: " + oldValue + ", 新值: " + atomicBoolean.get()); + + // CAS操作 + boolean casResult = atomicBoolean.compareAndSet(false, true); + System.out.println("compareAndSet(false->true): " + casResult + ", 当前值: " + atomicBoolean.get()); + + // 实际应用:一次性开关 + demonstrateOnceFlag(); + } + + // 一次性标志演示 + private static void demonstrateOnceFlag() { + System.out.println("\n--- 一次性标志演示 ---"); + + OnceFlag onceFlag = new OnceFlag(); + + // 多个线程尝试执行 + for (int i = 0; i < 5; i++) { + final int threadId = i; + new Thread(() -> { + if (onceFlag.tryExecute()) { + System.out.println("线程 " + threadId + " 成功执行了一次性操作"); + } else { + System.out.println("线程 " + threadId + " 未能执行(已被其他线程执行)"); + } + }).start(); + } + + try { + Thread.sleep(1000); // 等待所有线程完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} + +// 一次性标志类 +class OnceFlag { + private final AtomicBoolean executed = new AtomicBoolean(false); + + public boolean tryExecute() { + return executed.compareAndSet(false, true); + } + + public boolean isExecuted() { + return executed.get(); + } +} +``` + +### 3.2 数组类型原子类 + +```java +import java.util.concurrent.atomic.*; +import java.util.Arrays; + +public class AtomicArrayExample { + + public static void main(String[] args) throws InterruptedException { + demonstrateAtomicIntegerArray(); + demonstrateAtomicLongArray(); + demonstrateAtomicReferenceArray(); + } + + // AtomicIntegerArray演示 + private static void demonstrateAtomicIntegerArray() throws InterruptedException { + System.out.println("=== AtomicIntegerArray演示 ==="); + + AtomicIntegerArray atomicArray = new AtomicIntegerArray(10); + + // 初始化数组 + for (int i = 0; i < atomicArray.length(); i++) { + atomicArray.set(i, i * 10); + } + + System.out.println("初始数组: " + arrayToString(atomicArray)); + + // 多线程操作数组 + Thread[] threads = new Thread[5]; + for (int i = 0; i < threads.length; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < atomicArray.length(); j++) { + // 原子地增加数组元素 + int newValue = atomicArray.addAndGet(j, threadId + 1); + System.out.println("线程" + threadId + "更新索引" + j + "为: " + newValue); + } + }); + } + + // 启动所有线程 + for (Thread thread : threads) { + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("最终数组: " + arrayToString(atomicArray)); + + // CAS操作 + boolean casResult = atomicArray.compareAndSet(0, atomicArray.get(0), 999); + System.out.println("CAS操作成功: " + casResult + ", 索引0的值: " + atomicArray.get(0)); + } + + // AtomicLongArray演示 + private static void demonstrateAtomicLongArray() { + System.out.println("\n=== AtomicLongArray演示 ==="); + + long[] initialArray = {1000000000L, 2000000000L, 3000000000L}; + AtomicLongArray atomicLongArray = new AtomicLongArray(initialArray); + + System.out.println("初始数组: " + arrayToString(atomicLongArray)); + + // 各种操作 + long oldValue = atomicLongArray.getAndSet(1, 5000000000L); + System.out.println("getAndSet索引1 - 旧值: " + oldValue + ", 新值: " + atomicLongArray.get(1)); + + long incrementResult = atomicLongArray.incrementAndGet(2); + System.out.println("incrementAndGet索引2: " + incrementResult); + + // 使用函数式操作(Java 8+) + long updateResult = atomicLongArray.updateAndGet(0, value -> value / 2); + System.out.println("updateAndGet索引0(除以2): " + updateResult); + + System.out.println("最终数组: " + arrayToString(atomicLongArray)); + } + + // AtomicReferenceArray演示 + private static void demonstrateAtomicReferenceArray() { + System.out.println("\n=== AtomicReferenceArray演示 ==="); + + AtomicReferenceArray atomicRefArray = new AtomicReferenceArray<>(5); + + // 初始化 + for (int i = 0; i < atomicRefArray.length(); i++) { + atomicRefArray.set(i, "Item-" + i); + } + + System.out.println("初始数组: " + refArrayToString(atomicRefArray)); + + // 原子更新 + String oldRef = atomicRefArray.getAndSet(2, "Updated-Item-2"); + System.out.println("getAndSet索引2 - 旧值: " + oldRef + ", 新值: " + atomicRefArray.get(2)); + + // CAS操作 + boolean casResult = atomicRefArray.compareAndSet(0, "Item-0", "CAS-Updated-Item-0"); + System.out.println("CAS操作成功: " + casResult + ", 索引0的值: " + atomicRefArray.get(0)); + + // 函数式更新 + String updateResult = atomicRefArray.updateAndGet(1, value -> value.toUpperCase()); + System.out.println("updateAndGet索引1(转大写): " + updateResult); + + System.out.println("最终数组: " + refArrayToString(atomicRefArray)); + } + + // 辅助方法:将AtomicIntegerArray转换为字符串 + private static String arrayToString(AtomicIntegerArray array) { + int[] result = new int[array.length()]; + for (int i = 0; i < array.length(); i++) { + result[i] = array.get(i); + } + return Arrays.toString(result); + } + + // 辅助方法:将AtomicLongArray转换为字符串 + private static String arrayToString(AtomicLongArray array) { + long[] result = new long[array.length()]; + for (int i = 0; i < array.length(); i++) { + result[i] = array.get(i); + } + return Arrays.toString(result); + } + + // 辅助方法:将AtomicReferenceArray转换为字符串 + private static String refArrayToString(AtomicReferenceArray array) { + String[] result = new String[array.length()]; + for (int i = 0; i < array.length(); i++) { + result[i] = array.get(i); + } + return Arrays.toString(result); + } +} +``` + +### 3.3 引用类型原子类 + +```java +import java.util.concurrent.atomic.*; + +public class AtomicReferenceExample { + + public static void main(String[] args) { + demonstrateAtomicReference(); + demonstrateAtomicStampedReference(); + demonstrateAtomicMarkableReference(); + } + + // AtomicReference演示 + private static void demonstrateAtomicReference() { + System.out.println("=== AtomicReference演示 ==="); + + AtomicReference atomicPerson = new AtomicReference<>(); + + // 初始设置 + Person initialPerson = new Person("Alice", 25); + atomicPerson.set(initialPerson); + System.out.println("初始人员: " + atomicPerson.get()); + + // 原子更新 + Person newPerson = new Person("Bob", 30); + Person oldPerson = atomicPerson.getAndSet(newPerson); + System.out.println("getAndSet - 旧值: " + oldPerson + ", 新值: " + atomicPerson.get()); + + // CAS操作 + Person anotherPerson = new Person("Charlie", 35); + boolean casResult = atomicPerson.compareAndSet(newPerson, anotherPerson); + System.out.println("CAS操作成功: " + casResult + ", 当前值: " + atomicPerson.get()); + + // 函数式更新 + Person updatedPerson = atomicPerson.updateAndGet(person -> + new Person(person.getName(), person.getAge() + 1)); + System.out.println("updateAndGet(年龄+1): " + updatedPerson); + + // 累加操作 + Person accumulatedPerson = atomicPerson.accumulateAndGet( + new Person("Suffix", 5), + (current, update) -> new Person( + current.getName() + "-" + update.getName(), + current.getAge() + update.getAge() + ) + ); + System.out.println("accumulateAndGet: " + accumulatedPerson); + } + + // AtomicStampedReference演示(解决ABA问题) + private static void demonstrateAtomicStampedReference() { + System.out.println("\n=== AtomicStampedReference演示 ==="); + + AtomicStampedReference atomicStampedRef = + new AtomicStampedReference<>("Initial", 0); + + // 获取当前值和版本号 + int[] stampHolder = new int[1]; + String currentValue = atomicStampedRef.get(stampHolder); + System.out.println("当前值: " + currentValue + ", 版本号: " + stampHolder[0]); + + // 带版本号的CAS操作 + boolean casResult1 = atomicStampedRef.compareAndSet( + "Initial", "Updated", 0, 1); + System.out.println("CAS(Initial->Updated, 0->1)成功: " + casResult1); + + // 获取更新后的值和版本号 + currentValue = atomicStampedRef.get(stampHolder); + System.out.println("更新后值: " + currentValue + ", 版本号: " + stampHolder[0]); + + // 使用错误版本号的CAS操作(应该失败) + boolean casResult2 = atomicStampedRef.compareAndSet( + "Updated", "Failed", 0, 2); + System.out.println("CAS(Updated->Failed, 0->2)成功: " + casResult2); + + // 使用正确版本号的CAS操作 + boolean casResult3 = atomicStampedRef.compareAndSet( + "Updated", "Success", 1, 2); + System.out.println("CAS(Updated->Success, 1->2)成功: " + casResult3); + + // 最终状态 + currentValue = atomicStampedRef.get(stampHolder); + System.out.println("最终值: " + currentValue + ", 版本号: " + stampHolder[0]); + + // 演示ABA问题的解决 + demonstrateABASolution(); + } + + // ABA问题解决方案演示 + private static void demonstrateABASolution() { + System.out.println("\n--- ABA问题解决方案 ---"); + + AtomicStampedReference atomicStampedRef = + new AtomicStampedReference<>("A", 0); + + Thread thread1 = new Thread(() -> { + int[] stampHolder = new int[1]; + String value = atomicStampedRef.get(stampHolder); + int stamp = stampHolder[0]; + + System.out.println("Thread1读取: 值=" + value + ", 版本=" + stamp); + + try { + Thread.sleep(1000); // 模拟处理时间 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 尝试CAS操作 + boolean success = atomicStampedRef.compareAndSet(value, "C", stamp, stamp + 1); + System.out.println("Thread1 CAS(A->C)成功: " + success); + }); + + Thread thread2 = new Thread(() -> { + try { + Thread.sleep(100); // 确保thread1先读取 + + // 执行A->B->A操作,但版本号会变化 + atomicStampedRef.compareAndSet("A", "B", 0, 1); + System.out.println("Thread2执行A->B"); + + atomicStampedRef.compareAndSet("B", "A", 1, 2); + System.out.println("Thread2执行B->A"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + thread1.start(); + thread2.start(); + + try { + thread1.join(); + thread2.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + int[] finalStampHolder = new int[1]; + String finalValue = atomicStampedRef.get(finalStampHolder); + System.out.println("最终状态: 值=" + finalValue + ", 版本=" + finalStampHolder[0]); + } + + // AtomicMarkableReference演示 + private static void demonstrateAtomicMarkableReference() { + System.out.println("\n=== AtomicMarkableReference演示 ==="); + + AtomicMarkableReference atomicMarkableRef = + new AtomicMarkableReference<>("Initial", false); + + // 获取当前值和标记 + boolean[] markHolder = new boolean[1]; + String currentValue = atomicMarkableRef.get(markHolder); + System.out.println("当前值: " + currentValue + ", 标记: " + markHolder[0]); + + // 带标记的CAS操作 + boolean casResult1 = atomicMarkableRef.compareAndSet( + "Initial", "Marked", false, true); + System.out.println("CAS(Initial->Marked, false->true)成功: " + casResult1); + + // 获取更新后的值和标记 + currentValue = atomicMarkableRef.get(markHolder); + System.out.println("更新后值: " + currentValue + ", 标记: " + markHolder[0]); + + // 仅尝试设置标记 + boolean markResult = atomicMarkableRef.attemptMark("Marked", false); + System.out.println("attemptMark(false)成功: " + markResult); + + // 检查是否被标记 + boolean isMarked = atomicMarkableRef.isMarked(); + System.out.println("当前是否被标记: " + isMarked); + + // 实际应用:逻辑删除 + demonstrateLogicalDeletion(); + } + + // 逻辑删除演示 + private static void demonstrateLogicalDeletion() { + System.out.println("\n--- 逻辑删除演示 ---"); + + LogicalDeletionList list = new LogicalDeletionList<>(); + + // 添加元素 + list.add("Element1"); + list.add("Element2"); + list.add("Element3"); + + System.out.println("添加元素后: " + list.getActiveElements()); + + // 逻辑删除 + boolean deleted = list.logicalDelete("Element2"); + System.out.println("逻辑删除Element2成功: " + deleted); + System.out.println("活跃元素: " + list.getActiveElements()); + System.out.println("所有元素: " + list.getAllElements()); + } +} + +// Person类 +class Person { + private final String name; + private final int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person{name='" + name + "', age=" + age + "}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Person person = (Person) obj; + return age == person.age && java.util.Objects.equals(name, person.name); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(name, age); + } +} + +// 逻辑删除列表 +class LogicalDeletionList { + private final java.util.List> elements = + new java.util.concurrent.CopyOnWriteArrayList<>(); + + public void add(T element) { + elements.add(new AtomicMarkableReference<>(element, false)); + } + + public boolean logicalDelete(T element) { + for (AtomicMarkableReference ref : elements) { + boolean[] markHolder = new boolean[1]; + T value = ref.get(markHolder); + + if (!markHolder[0] && element.equals(value)) { + return ref.attemptMark(value, true); + } + } + return false; + } + + public java.util.List getActiveElements() { + java.util.List activeElements = new java.util.ArrayList<>(); + for (AtomicMarkableReference ref : elements) { + boolean[] markHolder = new boolean[1]; + T value = ref.get(markHolder); + if (!markHolder[0]) { + activeElements.add(value); + } + } + return activeElements; + } + + public java.util.List getAllElements() { + java.util.List allElements = new java.util.ArrayList<>(); + for (AtomicMarkableReference ref : elements) { + boolean[] markHolder = new boolean[1]; + T value = ref.get(markHolder); + allElements.add(value); + } + return allElements; + } +} +``` + + diff --git a/docs/aThread/jvmConcurrent.md b/docs/aThread/jvmConcurrent.md new file mode 100644 index 000000000..b20bc0d50 --- /dev/null +++ b/docs/aThread/jvmConcurrent.md @@ -0,0 +1,1059 @@ +--- +title: java并发容器实现原理 +author: 哪吒 +date: '2023-06-15' +--- + +# Java并发容器实现原理 + +## 1. 并发容器概述 + +### 1.1 为什么需要并发容器 + +在多线程环境下,传统的集合类(如ArrayList、HashMap等)不是线程安全的,直接使用会导致数据不一致、死循环等问题。虽然可以使用Collections.synchronizedXxx()方法包装,但性能较差。 + +```java +// 传统同步方式 - 性能较差 +List syncList = Collections.synchronizedList(new ArrayList<>()); +Map syncMap = Collections.synchronizedMap(new HashMap<>()); + +// 并发容器 - 高性能 +List concurrentList = new CopyOnWriteArrayList<>(); +Map concurrentMap = new ConcurrentHashMap<>(); +``` + +### 1.2 并发容器的分类 + +1. **并发Map**:ConcurrentHashMap、ConcurrentSkipListMap +2. **并发List**:CopyOnWriteArrayList +3. **并发Set**:CopyOnWriteArraySet、ConcurrentSkipListSet +4. **阻塞队列**:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等 +5. **非阻塞队列**:ConcurrentLinkedQueue + +## 2. ConcurrentHashMap实现原理 + +### 2.1 JDK 1.7 vs JDK 1.8的区别 + +**JDK 1.7:分段锁(Segment)** +```java +// JDK 1.7 结构示意 +public class ConcurrentHashMap { + // Segment数组,每个Segment包含一个HashEntry数组 + final Segment[] segments; + + static final class Segment extends ReentrantLock { + transient volatile HashEntry[] table; + transient int count; + } +} +``` + +**JDK 1.8:CAS + synchronized** +```java +// JDK 1.8 结构示意 +public class ConcurrentHashMap { + // Node数组 + transient volatile Node[] table; + + static class Node { + final int hash; + final K key; + volatile V val; + volatile Node next; + } +} +``` + +### 2.2 JDK 1.8 ConcurrentHashMap详解 + +#### 2.2.1 put操作实现 + +```java +public class ConcurrentHashMapDemo { + public static void main(String[] args) { + ConcurrentHashMap map = new ConcurrentHashMap<>(); + + // 多线程并发put + for (int i = 0; i < 10; i++) { + final int index = i; + new Thread(() -> { + map.put("key" + index, "value" + index); + System.out.println(Thread.currentThread().getName() + + " put key" + index); + }, "Thread-" + i).start(); + } + } +} +``` + +#### 2.2.2 核心实现机制 + +```java +// put操作的核心逻辑(简化版) +final V putVal(K key, V value, boolean onlyIfAbsent) { + if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; + for (Node[] tab = table;;) { + Node f; int n, i, fh; + if (tab == null || (n = tab.length) == 0) + tab = initTable(); // 初始化表 + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // 位置为空,使用CAS插入 + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; + } + else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); // 协助扩容 + else { + V oldVal = null; + synchronized (f) { // 锁定头节点 + // 链表或红黑树操作 + if (tabAt(tab, i) == f) { + if (fh >= 0) { + // 链表操作 + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, value, null); + break; + } + } + } + else if (f instanceof TreeBin) { + // 红黑树操作 + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } + if (binCount != 0) { + if (binCount >= TREEIFY_THRESHOLD) + treeifyBin(tab, i); // 转换为红黑树 + if (oldVal != null) + return oldVal; + break; + } + } + } + addCount(1L, binCount); + return null; +} +``` + +### 2.3 扩容机制 + +```java +public class ConcurrentHashMapResizeDemo { + public static void main(String[] args) { + ConcurrentHashMap map = new ConcurrentHashMap<>(4); + + // 观察扩容过程 + for (int i = 0; i < 20; i++) { + map.put(i, "value" + i); + System.out.println("Size: " + map.size() + + ", Capacity: " + getCapacity(map)); + } + } + + // 通过反射获取容量(仅用于演示) + private static int getCapacity(ConcurrentHashMap map) { + try { + Field tableField = ConcurrentHashMap.class.getDeclaredField("table"); + tableField.setAccessible(true); + Object[] table = (Object[]) tableField.get(map); + return table == null ? 0 : table.length; + } catch (Exception e) { + return -1; + } + } +} +``` + +## 3. CopyOnWriteArrayList实现原理 + +### 3.1 写时复制机制 + +CopyOnWriteArrayList采用写时复制(Copy-On-Write)策略,读操作不加锁,写操作时复制整个数组。 + +```java +public class CopyOnWriteArrayListDemo { + public static void main(String[] args) throws InterruptedException { + CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + + // 添加初始数据 + list.add("item1"); + list.add("item2"); + list.add("item3"); + + // 读线程 - 不加锁,性能高 + Thread readerThread = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.println("Reader: " + list); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + + // 写线程 - 写时复制 + Thread writerThread = new Thread(() -> { + for (int i = 0; i < 5; i++) { + list.add("newItem" + i); + System.out.println("Writer added: newItem" + i); + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + + readerThread.start(); + writerThread.start(); + + readerThread.join(); + writerThread.join(); + } +} +``` + +### 3.2 核心实现源码分析 + +```java +// CopyOnWriteArrayList的add方法实现 +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); // 写操作加锁 + try { + Object[] elements = getArray(); + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制数组 + newElements[len] = e; // 添加新元素 + setArray(newElements); // 设置新数组 + return true; + } finally { + lock.unlock(); + } +} + +// 读操作不加锁 +public E get(int index) { + return get(getArray(), index); +} + +private E get(Object[] a, int index) { + return (E) a[index]; +} +``` + +### 3.3 适用场景分析 + +```java +public class CopyOnWriteScenarioDemo { + public static void main(String[] args) { + // 适用场景:读多写少 + CopyOnWriteArrayList eventListeners = new CopyOnWriteArrayList<>(); + + // 注册监听器(写操作较少) + eventListeners.add("EmailListener"); + eventListeners.add("SMSListener"); + eventListeners.add("LogListener"); + + // 模拟事件触发(读操作频繁) + for (int i = 0; i < 1000; i++) { + // 遍历所有监听器处理事件 + for (String listener : eventListeners) { + // 处理事件 + processEvent(listener, "Event-" + i); + } + } + } + + private static void processEvent(String listener, String event) { + // 模拟事件处理 + System.out.println(listener + " processing " + event); + } +} +``` + +## 4. 阻塞队列实现原理 + +### 4.1 ArrayBlockingQueue + +基于数组的有界阻塞队列,使用ReentrantLock和Condition实现阻塞。 + +```java +public class ArrayBlockingQueueDemo { + public static void main(String[] args) { + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3); + + // 生产者线程 + Thread producer = new Thread(() -> { + try { + for (int i = 0; i < 10; i++) { + String item = "Item-" + i; + queue.put(item); // 队列满时阻塞 + System.out.println("Produced: " + item + + ", Queue size: " + queue.size()); + Thread.sleep(100); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // 消费者线程 + Thread consumer = new Thread(() -> { + try { + for (int i = 0; i < 10; i++) { + String item = queue.take(); // 队列空时阻塞 + System.out.println("Consumed: " + item + + ", Queue size: " + queue.size()); + Thread.sleep(300); // 消费较慢 + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + producer.start(); + consumer.start(); + } +} +``` + +### 4.2 LinkedBlockingQueue + +基于链表的可选有界阻塞队列,读写使用不同的锁,性能更好。 + +```java +public class LinkedBlockingQueueDemo { + public static void main(String[] args) throws InterruptedException { + // 无界队列(实际上有界,最大容量为Integer.MAX_VALUE) + LinkedBlockingQueue taskQueue = new LinkedBlockingQueue<>(); + + // 工作线程池 + List workers = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Thread worker = new Thread(new Worker(taskQueue), "Worker-" + i); + workers.add(worker); + worker.start(); + } + + // 提交任务 + for (int i = 0; i < 10; i++) { + taskQueue.offer(new Task("Task-" + i)); + } + + // 等待一段时间后停止 + Thread.sleep(5000); + + // 停止工作线程 + for (Thread worker : workers) { + worker.interrupt(); + } + } + + static class Task { + private final String name; + + public Task(String name) { + this.name = name; + } + + public void execute() { + System.out.println(Thread.currentThread().getName() + + " executing " + name); + try { + Thread.sleep(1000); // 模拟任务执行 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public String toString() { + return name; + } + } + + static class Worker implements Runnable { + private final LinkedBlockingQueue taskQueue; + + public Worker(LinkedBlockingQueue taskQueue) { + this.taskQueue = taskQueue; + } + + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + Task task = taskQueue.take(); // 阻塞获取任务 + task.execute(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.out.println(Thread.currentThread().getName() + " stopped"); + } + } + } +} +``` + +### 4.3 PriorityBlockingQueue + +基于优先级堆的无界阻塞队列,元素按优先级排序。 + +```java +public class PriorityBlockingQueueDemo { + public static void main(String[] args) throws InterruptedException { + PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); + + // 添加不同优先级的任务 + queue.offer(new PriorityTask("Low Priority Task", 1)); + queue.offer(new PriorityTask("High Priority Task", 10)); + queue.offer(new PriorityTask("Medium Priority Task", 5)); + queue.offer(new PriorityTask("Urgent Task", 20)); + + // 按优先级处理任务 + while (!queue.isEmpty()) { + PriorityTask task = queue.take(); + System.out.println("Processing: " + task); + Thread.sleep(500); + } + } + + static class PriorityTask implements Comparable { + private final String name; + private final int priority; + + public PriorityTask(String name, int priority) { + this.name = name; + this.priority = priority; + } + + @Override + public int compareTo(PriorityTask other) { + // 优先级高的排在前面 + return Integer.compare(other.priority, this.priority); + } + + @Override + public String toString() { + return name + " (Priority: " + priority + ")"; + } + } +} +``` + +## 5. 非阻塞队列 - ConcurrentLinkedQueue + +### 5.1 无锁实现原理 + +ConcurrentLinkedQueue使用CAS操作实现无锁的并发队列。 + +```java +public class ConcurrentLinkedQueueDemo { + public static void main(String[] args) throws InterruptedException { + ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + // 多个生产者线程 + List producers = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + final int producerId = i; + Thread producer = new Thread(() -> { + for (int j = 0; j < 5; j++) { + String item = "Producer-" + producerId + "-Item-" + j; + queue.offer(item); + System.out.println("Offered: " + item); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + producers.add(producer); + producer.start(); + } + + // 多个消费者线程 + List consumers = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + final int consumerId = i; + Thread consumer = new Thread(() -> { + for (int j = 0; j < 7; j++) { + String item = queue.poll(); + if (item != null) { + System.out.println("Consumer-" + consumerId + + " polled: " + item); + } else { + System.out.println("Consumer-" + consumerId + + " found empty queue"); + j--; // 重试 + } + try { + Thread.sleep(150); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + consumers.add(consumer); + consumer.start(); + } + + // 等待所有线程完成 + for (Thread producer : producers) { + producer.join(); + } + for (Thread consumer : consumers) { + consumer.join(); + } + + System.out.println("Remaining items in queue: " + queue.size()); + } +} +``` + +### 5.2 CAS操作示例 + +```java +// ConcurrentLinkedQueue的offer方法核心逻辑(简化版) +public boolean offer(E e) { + checkNotNull(e); + final Node newNode = new Node(e); + + for (Node t = tail, p = t;;) { + Node q = p.next; + if (q == null) { + // p是最后一个节点,尝试CAS链接新节点 + if (p.casNext(null, newNode)) { + // 成功链接,可能需要更新tail + if (p != t) + casTail(t, newNode); + return true; + } + } + else if (p == q) { + // 遇到哨兵节点,重新开始 + p = (t != (t = tail)) ? t : head; + } + else { + // 向前推进 + p = (p != t && t != (t = tail)) ? t : q; + } + } +} +``` + +## 6. ConcurrentSkipListMap实现原理 + +### 6.1 跳表数据结构 + +ConcurrentSkipListMap基于跳表(Skip List)实现,提供有序的并发Map。 + +```java +public class ConcurrentSkipListMapDemo { + public static void main(String[] args) throws InterruptedException { + ConcurrentSkipListMap skipListMap = new ConcurrentSkipListMap<>(); + + // 多线程并发插入 + List threads = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + final int threadId = i; + Thread thread = new Thread(() -> { + for (int j = 0; j < 10; j++) { + int key = threadId * 10 + j; + skipListMap.put(key, "Value-" + key); + System.out.println(Thread.currentThread().getName() + + " put: " + key + " -> Value-" + key); + } + }, "Thread-" + i); + threads.add(thread); + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + // 验证有序性 + System.out.println("\nOrdered entries:"); + skipListMap.entrySet().forEach(entry -> + System.out.println(entry.getKey() + " -> " + entry.getValue())); + + // 范围查询 + System.out.println("\nRange query (15-25):"); + skipListMap.subMap(15, 26).entrySet().forEach(entry -> + System.out.println(entry.getKey() + " -> " + entry.getValue())); + } +} +``` + +### 6.2 跳表的优势 + +```java +public class SkipListAdvantageDemo { + public static void main(String[] args) { + ConcurrentSkipListMap scoreMap = new ConcurrentSkipListMap<>(); + + // 添加学生成绩 + scoreMap.put("Alice", 95); + scoreMap.put("Bob", 87); + scoreMap.put("Charlie", 92); + scoreMap.put("David", 78); + scoreMap.put("Eve", 89); + + // 按字母顺序遍历 + System.out.println("Students in alphabetical order:"); + scoreMap.entrySet().forEach(entry -> + System.out.println(entry.getKey() + ": " + entry.getValue())); + + // 获取第一个和最后一个 + System.out.println("\nFirst student: " + scoreMap.firstKey()); + System.out.println("Last student: " + scoreMap.lastKey()); + + // 范围查询 + System.out.println("\nStudents from 'B' to 'D':"); + scoreMap.subMap("B", "E").entrySet().forEach(entry -> + System.out.println(entry.getKey() + ": " + entry.getValue())); + } +} +``` + +## 7. 性能比较分析 + +### 7.1 并发容器性能测试 + +```java +public class ConcurrentContainerPerformanceTest { + private static final int THREAD_COUNT = 10; + private static final int OPERATIONS_PER_THREAD = 10000; + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 并发容器性能测试 ==="); + + // 测试ConcurrentHashMap + testConcurrentHashMap(); + + // 测试CopyOnWriteArrayList + testCopyOnWriteArrayList(); + + // 测试阻塞队列 + testBlockingQueues(); + } + + private static void testConcurrentHashMap() throws InterruptedException { + System.out.println("\n--- ConcurrentHashMap vs Synchronized HashMap ---"); + + // 测试ConcurrentHashMap + ConcurrentHashMap concurrentMap = new ConcurrentHashMap<>(); + long startTime = System.currentTimeMillis(); + + List threads = new ArrayList<>(); + for (int i = 0; i < THREAD_COUNT; i++) { + final int threadId = i; + Thread thread = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + int key = threadId * OPERATIONS_PER_THREAD + j; + concurrentMap.put(key, "value" + key); + concurrentMap.get(key); + } + }); + threads.add(thread); + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long concurrentTime = System.currentTimeMillis() - startTime; + System.out.println("ConcurrentHashMap time: " + concurrentTime + "ms"); + + // 测试Synchronized HashMap + Map syncMap = Collections.synchronizedMap(new HashMap<>()); + startTime = System.currentTimeMillis(); + + threads.clear(); + for (int i = 0; i < THREAD_COUNT; i++) { + final int threadId = i; + Thread thread = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + int key = threadId * OPERATIONS_PER_THREAD + j; + syncMap.put(key, "value" + key); + syncMap.get(key); + } + }); + threads.add(thread); + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long syncTime = System.currentTimeMillis() - startTime; + System.out.println("Synchronized HashMap time: " + syncTime + "ms"); + System.out.println("Performance improvement: " + + String.format("%.2f", (double) syncTime / concurrentTime) + "x"); + } + + private static void testCopyOnWriteArrayList() throws InterruptedException { + System.out.println("\n--- CopyOnWriteArrayList vs Synchronized ArrayList ---"); + + // 读多写少场景测试 + CopyOnWriteArrayList cowList = new CopyOnWriteArrayList<>(); + List syncList = Collections.synchronizedList(new ArrayList<>()); + + // 预填充数据 + for (int i = 0; i < 1000; i++) { + cowList.add("item" + i); + syncList.add("item" + i); + } + + // 测试CopyOnWriteArrayList(90%读,10%写) + long startTime = System.currentTimeMillis(); + List threads = new ArrayList<>(); + + for (int i = 0; i < THREAD_COUNT; i++) { + Thread thread = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + if (j % 10 == 0) { + // 10%写操作 + cowList.add("newItem" + j); + } else { + // 90%读操作 + if (!cowList.isEmpty()) { + cowList.get(j % cowList.size()); + } + } + } + }); + threads.add(thread); + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long cowTime = System.currentTimeMillis() - startTime; + System.out.println("CopyOnWriteArrayList time: " + cowTime + "ms"); + + // 测试Synchronized ArrayList + startTime = System.currentTimeMillis(); + threads.clear(); + + for (int i = 0; i < THREAD_COUNT; i++) { + Thread thread = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + if (j % 10 == 0) { + // 10%写操作 + syncList.add("newItem" + j); + } else { + // 90%读操作 + synchronized (syncList) { + if (!syncList.isEmpty()) { + syncList.get(j % syncList.size()); + } + } + } + } + }); + threads.add(thread); + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long syncTime = System.currentTimeMillis() - startTime; + System.out.println("Synchronized ArrayList time: " + syncTime + "ms"); + System.out.println("Performance improvement: " + + String.format("%.2f", (double) syncTime / cowTime) + "x"); + } + + private static void testBlockingQueues() throws InterruptedException { + System.out.println("\n--- ArrayBlockingQueue vs LinkedBlockingQueue ---"); + + // 测试ArrayBlockingQueue + ArrayBlockingQueue arrayQueue = new ArrayBlockingQueue<>(1000); + long startTime = System.currentTimeMillis(); + + Thread producer1 = new Thread(() -> { + try { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + arrayQueue.put("item" + i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + Thread consumer1 = new Thread(() -> { + try { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + arrayQueue.take(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + producer1.start(); + consumer1.start(); + producer1.join(); + consumer1.join(); + + long arrayTime = System.currentTimeMillis() - startTime; + System.out.println("ArrayBlockingQueue time: " + arrayTime + "ms"); + + // 测试LinkedBlockingQueue + LinkedBlockingQueue linkedQueue = new LinkedBlockingQueue<>(); + startTime = System.currentTimeMillis(); + + Thread producer2 = new Thread(() -> { + try { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + linkedQueue.put("item" + i); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + Thread consumer2 = new Thread(() -> { + try { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + linkedQueue.take(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + producer2.start(); + consumer2.start(); + producer2.join(); + consumer2.join(); + + long linkedTime = System.currentTimeMillis() - startTime; + System.out.println("LinkedBlockingQueue time: " + linkedTime + "ms"); + } +} +``` + +## 8. 最佳实践与选择指南 + +### 8.1 容器选择决策树 + +```java +public class ContainerSelectionGuide { + public static void main(String[] args) { + System.out.println("=== 并发容器选择指南 ==="); + + // Map类型选择 + System.out.println("\n--- Map类型选择 ---"); + System.out.println("1. 需要排序 -> ConcurrentSkipListMap"); + System.out.println("2. 高并发读写 -> ConcurrentHashMap"); + System.out.println("3. 简单同步 -> Collections.synchronizedMap()"); + + // List类型选择 + System.out.println("\n--- List类型选择 ---"); + System.out.println("1. 读多写少 -> CopyOnWriteArrayList"); + System.out.println("2. 频繁修改 -> Collections.synchronizedList()"); + System.out.println("3. 单线程 -> ArrayList"); + + // Queue类型选择 + System.out.println("\n--- Queue类型选择 ---"); + System.out.println("1. 生产者-消费者模式 -> BlockingQueue"); + System.out.println(" - 有界队列 -> ArrayBlockingQueue"); + System.out.println(" - 无界队列 -> LinkedBlockingQueue"); + System.out.println(" - 优先级队列 -> PriorityBlockingQueue"); + System.out.println("2. 高性能无锁 -> ConcurrentLinkedQueue"); + + demonstrateSelectionCriteria(); + } + + private static void demonstrateSelectionCriteria() { + System.out.println("\n=== 实际应用场景示例 ==="); + + // 场景1:缓存系统 + System.out.println("\n场景1:缓存系统"); + ConcurrentHashMap cache = new ConcurrentHashMap<>(); + cache.put("user:123", new User("Alice")); + System.out.println("推荐:ConcurrentHashMap - 高并发读写性能"); + + // 场景2:事件监听器 + System.out.println("\n场景2:事件监听器"); + CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); + listeners.add(new EmailListener()); + listeners.add(new LogListener()); + System.out.println("推荐:CopyOnWriteArrayList - 读多写少,遍历安全"); + + // 场景3:任务队列 + System.out.println("\n场景3:任务队列"); + LinkedBlockingQueue taskQueue = new LinkedBlockingQueue<>(); + taskQueue.offer(() -> System.out.println("执行任务")); + System.out.println("推荐:LinkedBlockingQueue - 生产者消费者模式"); + + // 场景4:排行榜 + System.out.println("\n场景4:排行榜"); + ConcurrentSkipListMap leaderboard = new ConcurrentSkipListMap<>( + Collections.reverseOrder()); + leaderboard.put(100, "Player1"); + leaderboard.put(95, "Player2"); + System.out.println("推荐:ConcurrentSkipListMap - 需要排序的并发Map"); + } + + static class User { + private String name; + public User(String name) { this.name = name; } + @Override + public String toString() { return "User{name='" + name + "'}"; } + } + + interface EventListener { + void onEvent(String event); + } + + static class EmailListener implements EventListener { + @Override + public void onEvent(String event) { + System.out.println("Email notification: " + event); + } + } + + static class LogListener implements EventListener { + @Override + public void onEvent(String event) { + System.out.println("Log event: " + event); + } + } +} +``` + +### 8.2 性能优化技巧 + +```java +public class PerformanceOptimizationTips { + public static void main(String[] args) { + System.out.println("=== 并发容器性能优化技巧 ==="); + + // 技巧1:合理设置初始容量 + optimizeInitialCapacity(); + + // 技巧2:减少锁竞争 + reduceLockContention(); + + // 技巧3:批量操作 + batchOperations(); + } + + private static void optimizeInitialCapacity() { + System.out.println("\n--- 技巧1:合理设置初始容量 ---"); + + // 不好的做法:使用默认容量 + ConcurrentHashMap map1 = new ConcurrentHashMap<>(); + + // 好的做法:预估容量 + int expectedSize = 10000; + ConcurrentHashMap map2 = new ConcurrentHashMap<>( + expectedSize, 0.75f, Runtime.getRuntime().availableProcessors()); + + System.out.println("预估容量可以减少扩容操作,提高性能"); + } + + private static void reduceLockContention() { + System.out.println("\n--- 技巧2:减少锁竞争 ---"); + + ConcurrentHashMap counters = new ConcurrentHashMap<>(); + + // 使用原子类减少锁竞争 + String key = "counter"; + counters.putIfAbsent(key, new AtomicLong(0)); + + // 多线程安全的计数器 + for (int i = 0; i < 10; i++) { + new Thread(() -> { + AtomicLong counter = counters.get(key); + for (int j = 0; j < 1000; j++) { + counter.incrementAndGet(); + } + }).start(); + } + + System.out.println("使用原子类可以减少锁竞争"); + } + + private static void batchOperations() { + System.out.println("\n--- 技巧3:批量操作 ---"); + + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + // 批量添加 + List batch = Arrays.asList("item1", "item2", "item3"); + queue.addAll(batch); + + // 批量移除 + List result = new ArrayList<>(); + queue.drainTo(result, 10); + + System.out.println("批量操作可以减少锁获取次数:" + result); + } +} +``` + +## 9. 总结 + +### 9.1 核心要点 + +1. **ConcurrentHashMap**:JDK 1.8使用CAS + synchronized,性能优异 +2. **CopyOnWriteArrayList**:写时复制,适合读多写少场景 +3. **阻塞队列**:生产者-消费者模式的首选,支持阻塞操作 +4. **非阻塞队列**:高性能无锁实现,适合高并发场景 +5. **ConcurrentSkipListMap**:有序的并发Map,基于跳表实现 + +### 9.2 选择建议 + +| 场景 | 推荐容器 | 原因 | +|------|----------|------| +| 高并发缓存 | ConcurrentHashMap | 读写性能优异 | +| 事件监听器 | CopyOnWriteArrayList | 读多写少,遍历安全 | +| 任务队列 | LinkedBlockingQueue | 生产者消费者模式 | +| 排行榜 | ConcurrentSkipListMap | 需要排序 | +| 高性能队列 | ConcurrentLinkedQueue | 无锁实现 | + +### 9.3 性能考虑 + +1. **内存开销**:CopyOnWrite容器内存开销较大 +2. **写性能**:CopyOnWrite容器写性能较差 +3. **读性能**:ConcurrentHashMap读性能最优 +4. **扩容成本**:预设合理初始容量 +5. **锁竞争**:选择合适的并发级别 + +通过合理选择和使用并发容器,可以显著提高多线程应用的性能和稳定性。关键是要根据具体的使用场景和性能要求来选择最适合的容器类型。 + + diff --git a/docs/aThread/jvmLock.md b/docs/aThread/jvmLock.md new file mode 100644 index 000000000..f5c165da2 --- /dev/null +++ b/docs/aThread/jvmLock.md @@ -0,0 +1,1718 @@ +--- +title: java锁实现原理 +author: 哪吒 +date: '2023-06-15' +--- + +# Java锁实现原理 + +## 1. 锁的基础概念 + +### 1.1 什么是锁 + +锁是一种同步机制,用于控制多个线程对共享资源的访问。在Java中,锁确保在任意时刻只有一个线程能够访问被保护的代码段或资源。 + +```java +public class LockBasicDemo { + private int count = 0; + private final Object lock = new Object(); + + // 使用synchronized关键字加锁 + public synchronized void incrementSync() { + count++; + } + + // 使用synchronized代码块 + public void incrementBlock() { + synchronized (lock) { + count++; + } + } + + public static void main(String[] args) throws InterruptedException { + LockBasicDemo demo = new LockBasicDemo(); + + // 创建多个线程并发访问 + Thread[] threads = new Thread[10]; + for (int i = 0; i < 10; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + demo.incrementSync(); + } + }); + threads[i].start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("Final count: " + demo.count); + } +} +``` + +## 3. ReentrantLock实现原理 + +### 3.1 ReentrantLock基本使用 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.Condition; + +public class ReentrantLockDemo { + private final ReentrantLock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + private int count = 0; + private boolean ready = false; + + // 基本的加锁解锁 + public void increment() { + lock.lock(); + try { + count++; + System.out.println(Thread.currentThread().getName() + ": " + count); + } finally { + lock.unlock(); + } + } + + // 可重入性演示 + public void reentrantDemo() { + lock.lock(); + try { + System.out.println("First lock acquired"); + nestedLock(); + } finally { + lock.unlock(); + } + } + + private void nestedLock() { + lock.lock(); // 同一线程再次获取锁 + try { + System.out.println("Nested lock acquired"); + } finally { + lock.unlock(); + } + } + + // 条件变量使用 + public void producer() { + lock.lock(); + try { + while (!ready) { + System.out.println("Producer waiting..."); + condition.await(); + } + System.out.println("Producer working..."); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } + + public void consumer() { + lock.lock(); + try { + ready = true; + System.out.println("Consumer ready, signaling producer"); + condition.signal(); + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) throws InterruptedException { + ReentrantLockDemo demo = new ReentrantLockDemo(); + + // 测试基本功能 + Thread[] threads = new Thread[3]; + for (int i = 0; i < 3; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 3; j++) { + demo.increment(); + } + }, "Thread-" + i); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + // 测试可重入性 + System.out.println("\n=== 可重入性测试 ==="); + demo.reentrantDemo(); + + // 测试条件变量 + System.out.println("\n=== 条件变量测试 ==="); + Thread producer = new Thread(demo::producer, "Producer"); + Thread consumer = new Thread(demo::consumer, "Consumer"); + + producer.start(); + Thread.sleep(1000); // 确保producer先启动 + consumer.start(); + + producer.join(); + consumer.join(); + } +} +``` + +### 3.2 公平锁与非公平锁 + +```java +import java.util.concurrent.locks.ReentrantLock; + +public class FairLockDemo { + private final ReentrantLock fairLock = new ReentrantLock(true); // 公平锁 + private final ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁 + + public void testFairLock() { + System.out.println("=== 公平锁测试 ==="); + testLock(fairLock, "Fair"); + } + + public void testUnfairLock() { + System.out.println("\n=== 非公平锁测试 ==="); + testLock(unfairLock, "Unfair"); + } + + private void testLock(ReentrantLock lock, String type) { + Thread[] threads = new Thread[5]; + + for (int i = 0; i < 5; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < 3; j++) { + lock.lock(); + try { + System.out.println(type + " Lock - Thread " + threadId + + " acquired lock, iteration " + j); + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } + }, "Thread-" + i); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public static void main(String[] args) { + FairLockDemo demo = new FairLockDemo(); + demo.testFairLock(); + demo.testUnfairLock(); + } +} +``` + +### 3.3 AQS(AbstractQueuedSynchronizer)原理 + +```java +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +// 自定义同步器示例 +public class CustomLock { + private final Sync sync = new Sync(); + + // 基于AQS的自定义同步器 + private static class Sync extends AbstractQueuedSynchronizer { + // 尝试获取锁 + @Override + protected boolean tryAcquire(int arg) { + // 使用CAS操作尝试将state从0设置为1 + if (compareAndSetState(0, 1)) { + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + + // 尝试释放锁 + @Override + protected boolean tryRelease(int arg) { + if (getState() == 0) { + throw new IllegalMonitorStateException(); + } + setExclusiveOwnerThread(null); + setState(0); + return true; + } + + // 是否被当前线程独占 + @Override + protected boolean isHeldExclusively() { + return getExclusiveOwnerThread() == Thread.currentThread(); + } + } + + public void lock() { + sync.acquire(1); + } + + public void unlock() { + sync.release(1); + } + + public boolean tryLock() { + return sync.tryAcquire(1); + } + + public static void main(String[] args) throws InterruptedException { + CustomLock lock = new CustomLock(); + int count = 0; + + Thread[] threads = new Thread[5]; + for (int i = 0; i < 5; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + lock.lock(); + try { + // count++; // 这里需要使用volatile或其他同步机制 + System.out.println(Thread.currentThread().getName() + " working"); + } finally { + lock.unlock(); + } + } + }, "Thread-" + i); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + } +} +``` + +## 4. 读写锁(ReadWriteLock) + +### 4.1 ReentrantReadWriteLock使用 + +```java +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.HashMap; +import java.util.Map; + +public class ReadWriteLockDemo { + private final Map cache = new HashMap<>(); + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); + private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); + + // 读操作 + public String get(String key) { + readLock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " reading: " + key); + Thread.sleep(100); // 模拟读操作耗时 + return cache.get(key); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + readLock.unlock(); + } + } + + // 写操作 + public void put(String key, String value) { + writeLock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " writing: " + key + "=" + value); + Thread.sleep(200); // 模拟写操作耗时 + cache.put(key, value); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + writeLock.unlock(); + } + } + + // 锁降级示例 + public String getAndUpdate(String key, String newValue) { + writeLock.lock(); + try { + String oldValue = cache.get(key); + cache.put(key, newValue); + + // 锁降级:在持有写锁的情况下获取读锁 + readLock.lock(); + try { + writeLock.unlock(); // 释放写锁 + // 现在只持有读锁 + System.out.println("Lock downgrade: " + key + " updated from " + oldValue + " to " + newValue); + return oldValue; + } finally { + readLock.unlock(); + } + } finally { + // 确保写锁被释放(如果还持有的话) + if (rwLock.isWriteLockedByCurrentThread()) { + writeLock.unlock(); + } + } + } + + public static void main(String[] args) throws InterruptedException { + ReadWriteLockDemo demo = new ReadWriteLockDemo(); + + // 初始化一些数据 + demo.put("key1", "value1"); + demo.put("key2", "value2"); + + // 创建多个读线程 + Thread[] readers = new Thread[5]; + for (int i = 0; i < 5; i++) { + readers[i] = new Thread(() -> { + for (int j = 0; j < 3; j++) { + String value = demo.get("key1"); + System.out.println(Thread.currentThread().getName() + " got: " + value); + } + }, "Reader-" + i); + } + + // 创建写线程 + Thread writer = new Thread(() -> { + for (int i = 0; i < 3; i++) { + demo.put("key1", "newValue" + i); + } + }, "Writer"); + + // 启动所有线程 + for (Thread reader : readers) { + reader.start(); + } + writer.start(); + + // 等待所有线程完成 + for (Thread reader : readers) { + reader.join(); + } + writer.join(); + + // 测试锁降级 + System.out.println("\n=== 锁降级测试 ==="); + demo.getAndUpdate("key1", "finalValue"); + } +} +``` + +### 4.2 StampedLock(JDK 8+) + +```java +import java.util.concurrent.locks.StampedLock; + +public class StampedLockDemo { + private double x, y; + private final StampedLock sl = new StampedLock(); + + // 写操作 + public void write(double newX, double newY) { + long stamp = sl.writeLock(); + try { + x = newX; + y = newY; + System.out.println(Thread.currentThread().getName() + " wrote: (" + x + ", " + y + ")"); + } finally { + sl.unlockWrite(stamp); + } + } + + // 乐观读 + public double distanceFromOrigin() { + long stamp = sl.tryOptimisticRead(); + double curX = x, curY = y; + + if (!sl.validate(stamp)) { + // 乐观读失败,升级为悲观读锁 + stamp = sl.readLock(); + try { + curX = x; + curY = y; + System.out.println(Thread.currentThread().getName() + " upgraded to read lock"); + } finally { + sl.unlockRead(stamp); + } + } else { + System.out.println(Thread.currentThread().getName() + " optimistic read succeeded"); + } + + return Math.sqrt(curX * curX + curY * curY); + } + + // 悲观读 + public double distanceFromOriginPessimistic() { + long stamp = sl.readLock(); + try { + System.out.println(Thread.currentThread().getName() + " pessimistic read"); + return Math.sqrt(x * x + y * y); + } finally { + sl.unlockRead(stamp); + } + } + + // 读锁升级为写锁 + public void moveIfAtOrigin(double newX, double newY) { + long stamp = sl.readLock(); + try { + while (x == 0.0 && y == 0.0) { + // 尝试将读锁升级为写锁 + long ws = sl.tryConvertToWriteLock(stamp); + if (ws != 0L) { + // 升级成功 + stamp = ws; + x = newX; + y = newY; + System.out.println(Thread.currentThread().getName() + " upgraded to write lock and moved to (" + x + ", " + y + ")"); + break; + } else { + // 升级失败,释放读锁,获取写锁 + sl.unlockRead(stamp); + stamp = sl.writeLock(); + System.out.println(Thread.currentThread().getName() + " acquired write lock directly"); + } + } + } finally { + sl.unlock(stamp); + } + } + + public static void main(String[] args) throws InterruptedException { + StampedLockDemo demo = new StampedLockDemo(); + + // 写线程 + Thread writer = new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.write(i, i * 2); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "Writer"); + + // 乐观读线程 + Thread optimisticReader = new Thread(() -> { + for (int i = 0; i < 10; i++) { + double distance = demo.distanceFromOrigin(); + System.out.println("Distance: " + distance); + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "OptimisticReader"); + + // 悲观读线程 + Thread pessimisticReader = new Thread(() -> { + for (int i = 0; i < 5; i++) { + double distance = demo.distanceFromOriginPessimistic(); + System.out.println("Pessimistic Distance: " + distance); + try { + Thread.sleep(150); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "PessimisticReader"); + + writer.start(); + optimisticReader.start(); + pessimisticReader.start(); + + writer.join(); + optimisticReader.join(); + pessimisticReader.join(); + + // 测试锁升级 + System.out.println("\n=== 锁升级测试 ==="); + demo.write(0, 0); // 重置为原点 + demo.moveIfAtOrigin(10, 20); + } +} +``` + +## 5. 锁的性能比较与分析 + +### 5.1 不同锁的性能测试 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; + +public class LockPerformanceTest { + private static final int THREAD_COUNT = 10; + private static final int ITERATIONS = 100000; + + private int synchronizedCounter = 0; + private int reentrantLockCounter = 0; + private int readWriteLockCounter = 0; + private int stampedLockCounter = 0; + + private final Object syncLock = new Object(); + private final ReentrantLock reentrantLock = new ReentrantLock(); + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final StampedLock stampedLock = new StampedLock(); + + // synchronized性能测试 + public void testSynchronized() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < ITERATIONS; j++) { + synchronized (syncLock) { + synchronizedCounter++; + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("Synchronized: " + (endTime - startTime) + "ms, Counter: " + synchronizedCounter); + } + + // ReentrantLock性能测试 + public void testReentrantLock() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < ITERATIONS; j++) { + reentrantLock.lock(); + try { + reentrantLockCounter++; + } finally { + reentrantLock.unlock(); + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("ReentrantLock: " + (endTime - startTime) + "ms, Counter: " + reentrantLockCounter); + } + + // ReadWriteLock性能测试(写操作) + public void testReadWriteLock() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < ITERATIONS; j++) { + rwLock.writeLock().lock(); + try { + readWriteLockCounter++; + } finally { + rwLock.writeLock().unlock(); + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("ReadWriteLock: " + (endTime - startTime) + "ms, Counter: " + readWriteLockCounter); + } + + // StampedLock性能测试 + public void testStampedLock() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < ITERATIONS; j++) { + long stamp = stampedLock.writeLock(); + try { + stampedLockCounter++; + } finally { + stampedLock.unlockWrite(stamp); + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("StampedLock: " + (endTime - startTime) + "ms, Counter: " + stampedLockCounter); + } + + public static void main(String[] args) throws InterruptedException { + LockPerformanceTest test = new LockPerformanceTest(); + + System.out.println("=== 锁性能测试 (" + THREAD_COUNT + " threads, " + ITERATIONS + " iterations each) ==="); + + test.testSynchronized(); + test.testReentrantLock(); + test.testReadWriteLock(); + test.testStampedLock(); + } +} +``` + +### 5.2 读写场景性能比较 + +```java +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; +import java.util.Random; + +public class ReadWritePerformanceTest { + private static final int READER_COUNT = 8; + private static final int WRITER_COUNT = 2; + private static final int OPERATIONS = 10000; + + private volatile int data = 0; + private final Object syncLock = new Object(); + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final StampedLock stampedLock = new StampedLock(); + private final Random random = new Random(); + + // synchronized读写测试 + public void testSynchronizedReadWrite() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] readers = new Thread[READER_COUNT]; + Thread[] writers = new Thread[WRITER_COUNT]; + + // 创建读线程 + for (int i = 0; i < READER_COUNT; i++) { + readers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + synchronized (syncLock) { + int value = data; // 读操作 + // 模拟读操作耗时 + try { + Thread.sleep(0, 1000); // 1微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + }, "SyncReader-" + i); + } + + // 创建写线程 + for (int i = 0; i < WRITER_COUNT; i++) { + writers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + synchronized (syncLock) { + data = random.nextInt(1000); // 写操作 + // 模拟写操作耗时 + try { + Thread.sleep(0, 5000); // 5微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + }, "SyncWriter-" + i); + } + + // 启动所有线程 + for (Thread reader : readers) reader.start(); + for (Thread writer : writers) writer.start(); + + // 等待完成 + for (Thread reader : readers) reader.join(); + for (Thread writer : writers) writer.join(); + + long endTime = System.currentTimeMillis(); + System.out.println("Synchronized ReadWrite: " + (endTime - startTime) + "ms"); + } + + // ReadWriteLock读写测试 + public void testReadWriteLock() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] readers = new Thread[READER_COUNT]; + Thread[] writers = new Thread[WRITER_COUNT]; + + // 创建读线程 + for (int i = 0; i < READER_COUNT; i++) { + readers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + rwLock.readLock().lock(); + try { + int value = data; // 读操作 + // 模拟读操作耗时 + Thread.sleep(0, 1000); // 1微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + rwLock.readLock().unlock(); + } + } + }, "RWReader-" + i); + } + + // 创建写线程 + for (int i = 0; i < WRITER_COUNT; i++) { + writers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + rwLock.writeLock().lock(); + try { + data = random.nextInt(1000); // 写操作 + // 模拟写操作耗时 + Thread.sleep(0, 5000); // 5微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + rwLock.writeLock().unlock(); + } + } + }, "RWWriter-" + i); + } + + // 启动所有线程 + for (Thread reader : readers) reader.start(); + for (Thread writer : writers) writer.start(); + + // 等待完成 + for (Thread reader : readers) reader.join(); + for (Thread writer : writers) writer.join(); + + long endTime = System.currentTimeMillis(); + System.out.println("ReadWriteLock: " + (endTime - startTime) + "ms"); + } + + // StampedLock读写测试 + public void testStampedLock() throws InterruptedException { + long startTime = System.currentTimeMillis(); + + Thread[] readers = new Thread[READER_COUNT]; + Thread[] writers = new Thread[WRITER_COUNT]; + + // 创建读线程(使用乐观读) + for (int i = 0; i < READER_COUNT; i++) { + readers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + long stamp = stampedLock.tryOptimisticRead(); + int value = data; + + if (!stampedLock.validate(stamp)) { + // 乐观读失败,使用悲观读 + stamp = stampedLock.readLock(); + try { + value = data; + } finally { + stampedLock.unlockRead(stamp); + } + } + + // 模拟读操作耗时 + try { + Thread.sleep(0, 1000); // 1微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "StampedReader-" + i); + } + + // 创建写线程 + for (int i = 0; i < WRITER_COUNT; i++) { + writers[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS; j++) { + long stamp = stampedLock.writeLock(); + try { + data = random.nextInt(1000); // 写操作 + // 模拟写操作耗时 + Thread.sleep(0, 5000); // 5微秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + stampedLock.unlockWrite(stamp); + } + } + }, "StampedWriter-" + i); + } + + // 启动所有线程 + for (Thread reader : readers) reader.start(); + for (Thread writer : writers) writer.start(); + + // 等待完成 + for (Thread reader : readers) reader.join(); + for (Thread writer : writers) writer.join(); + + long endTime = System.currentTimeMillis(); + System.out.println("StampedLock: " + (endTime - startTime) + "ms"); + } + + public static void main(String[] args) throws InterruptedException { + ReadWritePerformanceTest test = new ReadWritePerformanceTest(); + + System.out.println("=== 读写场景性能测试 (" + READER_COUNT + " readers, " + WRITER_COUNT + " writers, " + OPERATIONS + " operations each) ==="); + + test.testSynchronizedReadWrite(); + test.testReadWriteLock(); + test.testStampedLock(); + } +} +``` + +## 6. 死锁检测与预防 + +### 6.1 死锁示例与检测 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.lang.management.ThreadInfo; + +public class DeadlockDemo { + private final ReentrantLock lock1 = new ReentrantLock(); + private final ReentrantLock lock2 = new ReentrantLock(); + + // 可能导致死锁的方法1 + public void method1() { + lock1.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired lock1"); + Thread.sleep(100); + + lock2.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired lock2"); + } finally { + lock2.unlock(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock1.unlock(); + } + } + + // 可能导致死锁的方法2 + public void method2() { + lock2.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired lock2"); + Thread.sleep(100); + + lock1.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired lock1"); + } finally { + lock1.unlock(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock2.unlock(); + } + } + + // 死锁检测 + public static void detectDeadlock() { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + long[] deadlockedThreads = threadBean.findDeadlockedThreads(); + + if (deadlockedThreads != null) { + ThreadInfo[] threadInfos = threadBean.getThreadInfo(deadlockedThreads); + System.out.println("\n=== 检测到死锁 ==="); + for (ThreadInfo threadInfo : threadInfos) { + System.out.println("线程名: " + threadInfo.getThreadName()); + System.out.println("线程状态: " + threadInfo.getThreadState()); + System.out.println("锁名: " + threadInfo.getLockName()); + System.out.println("锁拥有者: " + threadInfo.getLockOwnerName()); + System.out.println("---"); + } + } else { + System.out.println("未检测到死锁"); + } + } + + public static void main(String[] args) throws InterruptedException { + DeadlockDemo demo = new DeadlockDemo(); + + Thread t1 = new Thread(() -> { + demo.method1(); + }, "Thread-1"); + + Thread t2 = new Thread(() -> { + demo.method2(); + }, "Thread-2"); + + // 启动死锁检测线程 + Thread detector = new Thread(() -> { + while (true) { + try { + Thread.sleep(1000); + detectDeadlock(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "DeadlockDetector"); + detector.setDaemon(true); + detector.start(); + + t1.start(); + t2.start(); + + // 等待一段时间后强制结束 + Thread.sleep(5000); + System.out.println("强制结束程序"); + System.exit(0); + } +} +``` + +### 6.2 死锁预防策略 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.TimeUnit; +import java.util.Random; + +public class DeadlockPreventionDemo { + private final ReentrantLock lock1 = new ReentrantLock(); + private final ReentrantLock lock2 = new ReentrantLock(); + private final Random random = new Random(); + + // 策略1: 锁排序 - 始终按相同顺序获取锁 + public void lockOrderingStrategy() { + // 为锁分配唯一ID,始终按ID顺序获取 + ReentrantLock firstLock = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock1 : lock2; + ReentrantLock secondLock = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock2 : lock1; + + firstLock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired first lock"); + Thread.sleep(100); + + secondLock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " acquired second lock"); + // 执行业务逻辑 + } finally { + secondLock.unlock(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + firstLock.unlock(); + } + } + + // 策略2: 超时机制 + public void timeoutStrategy() { + try { + if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) { + try { + System.out.println(Thread.currentThread().getName() + " acquired lock1"); + + if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) { + try { + System.out.println(Thread.currentThread().getName() + " acquired lock2"); + // 执行业务逻辑 + } finally { + lock2.unlock(); + } + } else { + System.out.println(Thread.currentThread().getName() + " failed to acquire lock2, backing off"); + } + } finally { + lock1.unlock(); + } + } else { + System.out.println(Thread.currentThread().getName() + " failed to acquire lock1, backing off"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // 策略3: 回退策略 + public void backoffStrategy() { + boolean acquired = false; + int attempts = 0; + + while (!acquired && attempts < 5) { + try { + if (lock1.tryLock()) { + try { + System.out.println(Thread.currentThread().getName() + " acquired lock1"); + + if (lock2.tryLock()) { + try { + System.out.println(Thread.currentThread().getName() + " acquired lock2"); + // 执行业务逻辑 + acquired = true; + } finally { + lock2.unlock(); + } + } else { + System.out.println(Thread.currentThread().getName() + " failed to acquire lock2, backing off"); + } + } finally { + lock1.unlock(); + } + } + + if (!acquired) { + attempts++; + // 随机回退时间,避免活锁 + Thread.sleep(random.nextInt(100) + 50); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + if (!acquired) { + System.out.println(Thread.currentThread().getName() + " failed to acquire locks after " + attempts + " attempts"); + } + } + + public static void main(String[] args) throws InterruptedException { + DeadlockPreventionDemo demo = new DeadlockPreventionDemo(); + + System.out.println("=== 锁排序策略测试 ==="); + Thread[] orderingThreads = new Thread[5]; + for (int i = 0; i < 5; i++) { + orderingThreads[i] = new Thread(demo::lockOrderingStrategy, "OrderingThread-" + i); + orderingThreads[i].start(); + } + for (Thread t : orderingThreads) { + t.join(); + } + + System.out.println("\n=== 超时策略测试 ==="); + Thread[] timeoutThreads = new Thread[5]; + for (int i = 0; i < 5; i++) { + timeoutThreads[i] = new Thread(demo::timeoutStrategy, "TimeoutThread-" + i); + timeoutThreads[i].start(); + } + for (Thread t : timeoutThreads) { + t.join(); + } + + System.out.println("\n=== 回退策略测试 ==="); + Thread[] backoffThreads = new Thread[5]; + for (int i = 0; i < 5; i++) { + backoffThreads[i] = new Thread(demo::backoffStrategy, "BackoffThread-" + i); + backoffThreads[i].start(); + } + for (Thread t : backoffThreads) { + t.join(); + } + } +} +``` + +## 7. 锁的最佳实践 + +### 7.1 锁的选择指南 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; + +public class LockSelectionGuide { + + // 场景1: 简单的互斥访问 - 使用synchronized + private int simpleCounter = 0; + + public synchronized void incrementSimple() { + simpleCounter++; + } + + // 场景2: 需要可中断、超时、公平性 - 使用ReentrantLock + private final ReentrantLock advancedLock = new ReentrantLock(true); // 公平锁 + private int advancedCounter = 0; + + public void incrementAdvanced() throws InterruptedException { + if (advancedLock.tryLock(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) { + try { + advancedCounter++; + } finally { + advancedLock.unlock(); + } + } else { + System.out.println("Failed to acquire lock within timeout"); + } + } + + // 场景3: 读多写少 - 使用ReadWriteLock + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private String data = "initial"; + + public String readData() { + rwLock.readLock().lock(); + try { + // 模拟读操作 + Thread.sleep(10); + return data; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } finally { + rwLock.readLock().unlock(); + } + } + + public void writeData(String newData) { + rwLock.writeLock().lock(); + try { + // 模拟写操作 + Thread.sleep(50); + data = newData; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + rwLock.writeLock().unlock(); + } + } + + // 场景4: 读操作极其频繁,写操作很少 - 使用StampedLock + private final StampedLock stampedLock = new StampedLock(); + private double x = 0.0, y = 0.0; + + public double calculateDistance() { + long stamp = stampedLock.tryOptimisticRead(); + double curX = x, curY = y; + + if (!stampedLock.validate(stamp)) { + // 乐观读失败,使用悲观读 + stamp = stampedLock.readLock(); + try { + curX = x; + curY = y; + } finally { + stampedLock.unlockRead(stamp); + } + } + + return Math.sqrt(curX * curX + curY * curY); + } + + public void updateCoordinates(double newX, double newY) { + long stamp = stampedLock.writeLock(); + try { + x = newX; + y = newY; + } finally { + stampedLock.unlockWrite(stamp); + } + } + + public static void main(String[] args) { + LockSelectionGuide guide = new LockSelectionGuide(); + + System.out.println("锁选择指南:"); + System.out.println("1. 简单互斥访问 -> synchronized"); + System.out.println("2. 需要高级功能(超时、中断、公平性) -> ReentrantLock"); + System.out.println("3. 读多写少场景 -> ReentrantReadWriteLock"); + System.out.println("4. 读操作极其频繁 -> StampedLock"); + } +} +``` + +### 7.2 锁优化技巧 + +```java +import java.util.concurrent.locks.ReentrantLock; + +public class LockOptimizationTips { + private final ReentrantLock lock = new ReentrantLock(); + private int counter = 0; + + // 技巧1: 减少锁的持有时间 + public void badExample() { + lock.lock(); + try { + // 不好的做法:在锁内进行耗时操作 + counter++; + expensiveOperation(); // 耗时操作 + counter++; + } finally { + lock.unlock(); + } + } + + public void goodExample() { + // 好的做法:将耗时操作移到锁外 + expensiveOperation(); // 耗时操作移到锁外 + + lock.lock(); + try { + counter++; + counter++; + } finally { + lock.unlock(); + } + } + + // 技巧2: 减少锁的粒度 + private final ReentrantLock lock1 = new ReentrantLock(); + private final ReentrantLock lock2 = new ReentrantLock(); + private int counter1 = 0; + private int counter2 = 0; + + public void coarseGrainedLock() { + // 粗粒度锁:一个锁保护多个资源 + lock.lock(); + try { + counter1++; + counter2++; + } finally { + lock.unlock(); + } + } + + public void fineGrainedLock() { + // 细粒度锁:每个资源使用独立的锁 + lock1.lock(); + try { + counter1++; + } finally { + lock1.unlock(); + } + + lock2.lock(); + try { + counter2++; + } finally { + lock2.unlock(); + } + } + + // 技巧3: 使用tryLock避免阻塞 + public boolean tryIncrementWithTimeout() { + try { + if (lock.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS)) { + try { + counter++; + return true; + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + } + + // 技巧4: 锁分离 + private final ReentrantLock readLock = new ReentrantLock(); + private final ReentrantLock writeLock = new ReentrantLock(); + private volatile boolean dataReady = false; + + public void readOperation() { + readLock.lock(); + try { + if (dataReady) { + // 执行读操作 + System.out.println("Reading data..."); + } + } finally { + readLock.unlock(); + } + } + + public void writeOperation() { + writeLock.lock(); + try { + // 执行写操作 + dataReady = true; + System.out.println("Writing data..."); + } finally { + writeLock.unlock(); + } + } + + private void expensiveOperation() { + try { + Thread.sleep(100); // 模拟耗时操作 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) { + LockOptimizationTips tips = new LockOptimizationTips(); + + System.out.println("锁优化技巧:"); + System.out.println("1. 减少锁的持有时间"); + System.out.println("2. 减少锁的粒度"); + System.out.println("3. 使用tryLock避免阻塞"); + System.out.println("4. 锁分离"); + System.out.println("5. 避免在锁内调用其他同步方法"); + System.out.println("6. 使用并发容器替代同步容器"); + } +} +``` + +## 8. 总结 + +Java锁机制是并发编程的核心,本文详细介绍了: + +### 8.1 核心要点 + +1. **synchronized关键字** + - 基于对象头和Monitor实现 + - 支持锁升级(偏向锁→轻量级锁→重量级锁) + - JVM自动优化(锁消除、锁粗化、适应性自旋) + +2. **ReentrantLock显式锁** + - 基于AQS(AbstractQueuedSynchronizer)实现 + - 支持可重入、公平/非公平、可中断、超时等高级特性 + - 需要手动释放锁,使用try-finally确保释放 + +3. **读写锁机制** + - ReentrantReadWriteLock:读读共享,读写互斥,写写互斥 + - StampedLock:支持乐观读,性能更优 + - 适用于读多写少的场景 + +4. **性能与选择** + - synchronized:简单场景,JVM优化好 + - ReentrantLock:需要高级功能时 + - ReadWriteLock:读多写少场景 + - StampedLock:读操作极其频繁的场景 + +### 8.2 最佳实践 + +1. **锁的选择原则** + - 优先使用synchronized,除非需要高级功能 + - 读多写少场景使用读写锁 + - 避免过度使用锁,考虑无锁数据结构 + +2. **性能优化策略** + - 减少锁的持有时间 + - 减少锁的粒度 + - 避免在锁内进行耗时操作 + - 使用tryLock避免无限等待 + +3. **死锁预防** + - 锁排序:始终按相同顺序获取锁 + - 超时机制:使用tryLock设置超时 + - 回退策略:获取失败时主动释放已持有的锁 + +### 8.3 发展趋势 + +1. **无锁编程**:使用原子类、CAS操作 +2. **并发容器**:ConcurrentHashMap、CopyOnWriteArrayList等 +3. **异步编程**:CompletableFuture、响应式编程 +4. **虚拟线程**:Project Loom带来的轻量级线程 + +理解和掌握Java锁机制对于编写高性能、线程安全的并发程序至关重要。在实际开发中,应根据具体场景选择合适的锁机制,并遵循最佳实践来避免常见的并发问题。 + +### 1.2 锁的分类 + +1. **悲观锁 vs 乐观锁** + - 悲观锁:假设会发生并发冲突,每次访问都加锁 + - 乐观锁:假设不会发生冲突,只在更新时检查 + +2. **公平锁 vs 非公平锁** + - 公平锁:按照请求锁的顺序获取锁 + - 非公平锁:不保证获取锁的顺序 + +3. **可重入锁 vs 不可重入锁** + - 可重入锁:同一线程可以多次获取同一把锁 + - 不可重入锁:同一线程不能多次获取同一把锁 + +4. **独享锁 vs 共享锁** + - 独享锁:只能被一个线程持有(如写锁) + - 共享锁:可以被多个线程同时持有(如读锁) + +## 2. synchronized实现原理 + +### 2.1 synchronized的使用方式 + +```java +public class SynchronizedDemo { + private int count = 0; + private static int staticCount = 0; + + // 1. 修饰实例方法 + public synchronized void instanceMethod() { + count++; + System.out.println("Instance method: " + count); + } + + // 2. 修饰静态方法 + public static synchronized void staticMethod() { + staticCount++; + System.out.println("Static method: " + staticCount); + } + + // 3. 修饰代码块 + public void blockMethod() { + synchronized (this) { + count++; + System.out.println("Block method: " + count); + } + } + + // 4. 修饰静态代码块 + public void staticBlockMethod() { + synchronized (SynchronizedDemo.class) { + staticCount++; + System.out.println("Static block method: " + staticCount); + } + } + + public static void main(String[] args) throws InterruptedException { + SynchronizedDemo demo = new SynchronizedDemo(); + + // 测试不同的synchronized使用方式 + Thread t1 = new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.instanceMethod(); + SynchronizedDemo.staticMethod(); + demo.blockMethod(); + demo.staticBlockMethod(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.instanceMethod(); + SynchronizedDemo.staticMethod(); + demo.blockMethod(); + demo.staticBlockMethod(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + } +} +``` + +### 2.2 synchronized的底层实现 + +#### 2.2.1 对象头和Monitor + +```java +public class ObjectHeaderDemo { + private final Object lock = new Object(); + + public void demonstrateObjectHeader() { + synchronized (lock) { + // 在这个代码块中,lock对象的对象头会被修改 + // Mark Word会存储指向Monitor对象的指针 + System.out.println("Inside synchronized block"); + + // 可以通过JOL(Java Object Layout)工具查看对象头信息 + // System.out.println(ClassLayout.parseInstance(lock).toPrintable()); + } + } + + public static void main(String[] args) { + ObjectHeaderDemo demo = new ObjectHeaderDemo(); + demo.demonstrateObjectHeader(); + } +} +``` + +#### 2.2.2 锁升级过程 + +```java +public class LockUpgradeDemo { + private int count = 0; + + public void demonstrateLockUpgrade() { + // 1. 无锁状态 -> 偏向锁 + // 当第一个线程访问时,会升级为偏向锁 + synchronized (this) { + count++; + System.out.println("First access - Biased Lock"); + } + + // 2. 偏向锁 -> 轻量级锁 + // 当有第二个线程竞争时,会升级为轻量级锁 + Thread t1 = new Thread(() -> { + synchronized (this) { + count++; + System.out.println("Second thread - Lightweight Lock"); + } + }); + t1.start(); + + try { + t1.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 3. 轻量级锁 -> 重量级锁 + // 当竞争激烈时,会升级为重量级锁 + Thread[] threads = new Thread[10]; + for (int i = 0; i < 10; i++) { + threads[i] = new Thread(() -> { + synchronized (this) { + count++; + try { + Thread.sleep(10); // 增加竞争 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + System.out.println("Final count: " + count); + } + + public static void main(String[] args) { + LockUpgradeDemo demo = new LockUpgradeDemo(); + demo.demonstrateLockUpgrade(); + } +} +``` + +### 2.3 synchronized的性能优化 + +```java +public class SynchronizedOptimizationDemo { + private int count = 0; + private final Object lock = new Object(); + + // 锁消除示例 + public void lockElimination() { + // JVM会检测到这个StringBuffer只在方法内部使用 + // 不会被其他线程访问,因此会消除synchronized + StringBuffer sb = new StringBuffer(); + sb.append("Hello"); + sb.append(" World"); + System.out.println(sb.toString()); + } + + // 锁粗化示例 + public void lockCoarsening() { + // 原本的细粒度锁 + synchronized (lock) { + count++; + } + synchronized (lock) { + count++; + } + synchronized (lock) { + count++; + } + + // JVM会将上述代码优化为: + // synchronized (lock) { + // count++; + // count++; + // count++; + // } + } + + // 适应性自旋示例 + public void adaptiveSpinning() { + Thread[] threads = new Thread[5]; + + for (int i = 0; i < 5; i++) { + threads[i] = new Thread(() -> { + synchronized (lock) { + // 短时间持有锁,适合自旋等待 + count++; + System.out.println(Thread.currentThread().getName() + ": " + count); + } + }, "Thread-" + i); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public static void main(String[] args) { + SynchronizedOptimizationDemo demo = new SynchronizedOptimizationDemo(); + + System.out.println("=== 锁消除演示 ==="); + demo.lockElimination(); + + System.out.println("\n=== 锁粗化演示 ==="); + demo.lockCoarsening(); + + System.out.println("\n=== 适应性自旋演示 ==="); + demo.adaptiveSpinning(); + } +} +``` + + diff --git a/docs/aThread/jvmThread.md b/docs/aThread/jvmThread.md new file mode 100644 index 000000000..5ceef3194 --- /dev/null +++ b/docs/aThread/jvmThread.md @@ -0,0 +1,554 @@ +--- +title: jvm线程 +author: 哪吒 +date: '2023-06-15' +--- + +# jvm线程 + +JVM线程是Java并发编程的基础,理解JVM线程的实现原理对于编写高性能、线程安全的Java应用程序至关重要。本文将深入探讨JVM线程的底层实现机制、线程模型、状态管理和调度策略。 + +## 1. JVM线程模型概述 + +### 1.1 线程的定义与重要性 + +在JVM中,线程是程序执行的最小单位,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈,但共享堆内存和方法区。 + +```java +public class ThreadBasicDemo { + public static void main(String[] args) { + // 主线程信息 + Thread mainThread = Thread.currentThread(); + System.out.println("主线程名称: " + mainThread.getName()); + System.out.println("主线程ID: " + mainThread.getId()); + System.out.println("主线程状态: " + mainThread.getState()); + System.out.println("主线程优先级: " + mainThread.getPriority()); + + // 创建新线程 + Thread workerThread = new Thread(() -> { + System.out.println("工作线程: " + Thread.currentThread().getName()); + System.out.println("工作线程ID: " + Thread.currentThread().getId()); + }, "WorkerThread"); + + workerThread.start(); + + try { + workerThread.join(); // 等待工作线程完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +### 1.2 JVM线程与操作系统线程的关系 + +JVM线程的实现依赖于底层操作系统的线程模型: + +1. **一对一模型(1:1)**:每个Java线程对应一个操作系统线程 +2. **多对一模型(N:1)**:多个Java线程映射到一个操作系统线程 +3. **多对多模型(M:N)**:多个Java线程映射到多个操作系统线程 + +现代JVM主要采用一对一模型,这样可以充分利用多核处理器的优势。 + +```java +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; + +public class ThreadModelDemo { + public static void main(String[] args) { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + + System.out.println("当前JVM中的线程数: " + threadMXBean.getThreadCount()); + System.out.println("峰值线程数: " + threadMXBean.getPeakThreadCount()); + System.out.println("守护线程数: " + threadMXBean.getDaemonThreadCount()); + + // 获取所有线程信息 + long[] threadIds = threadMXBean.getAllThreadIds(); + for (long threadId : threadIds) { + System.out.println("线程ID: " + threadId + ", 线程名: " + + threadMXBean.getThreadInfo(threadId).getThreadName()); + } + } +} +``` + +## 2. 线程的内存模型 + +### 2.1 线程私有内存区域 + +每个线程都有自己的私有内存区域: + +```java +public class ThreadMemoryDemo { + // 实例变量 - 存储在堆中,线程共享 + private int sharedVariable = 0; + + public void demonstrateThreadMemory() { + // 局部变量 - 存储在线程私有的虚拟机栈中 + int localVariable = 100; + + Thread thread1 = new Thread(() -> { + // 每个线程都有自己的局部变量副本 + int threadLocal = localVariable + 1; + System.out.println("Thread1 - threadLocal: " + threadLocal); + + // 但共享实例变量 + synchronized (this) { + sharedVariable++; + System.out.println("Thread1 - sharedVariable: " + sharedVariable); + } + }); + + Thread thread2 = new Thread(() -> { + int threadLocal = localVariable + 2; + System.out.println("Thread2 - threadLocal: " + threadLocal); + + synchronized (this) { + sharedVariable++; + System.out.println("Thread2 - sharedVariable: " + sharedVariable); + } + }); + + thread1.start(); + thread2.start(); + + try { + thread1.join(); + thread2.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) { + new ThreadMemoryDemo().demonstrateThreadMemory(); + } +} +``` + +### 2.2 程序计数器(PC Register) + +程序计数器是线程私有的内存区域,用于存储当前线程正在执行的字节码指令的地址。 + +```java +public class PCRegisterDemo { + public static void main(String[] args) { + Runnable task = () -> { + for (int i = 0; i < 5; i++) { + System.out.println(Thread.currentThread().getName() + + " - 执行第 " + i + " 次循环"); + + // 每个线程都有自己的程序计数器 + // 记录当前执行到哪一条字节码指令 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }; + + Thread thread1 = new Thread(task, "Thread-1"); + Thread thread2 = new Thread(task, "Thread-2"); + + thread1.start(); + thread2.start(); + } +} +``` + +### 2.3 虚拟机栈(JVM Stack) + +虚拟机栈是线程私有的,用于存储局部变量、操作数栈、动态链接和方法返回地址。 + +```java +public class JVMStackDemo { + + public static void main(String[] args) { + new JVMStackDemo().demonstrateStack(); + } + + public void demonstrateStack() { + // 每个方法调用都会在虚拟机栈中创建一个栈帧 + int localVar = 10; // 局部变量存储在栈帧中 + + System.out.println("方法开始执行,局部变量: " + localVar); + + // 递归调用会在栈中创建多个栈帧 + recursiveMethod(5); + + System.out.println("方法执行结束"); + } + + private void recursiveMethod(int depth) { + if (depth <= 0) { + return; + } + + // 每次递归调用都会创建新的栈帧 + int currentDepth = depth; + System.out.println("递归深度: " + currentDepth + + ", 线程: " + Thread.currentThread().getName()); + + recursiveMethod(depth - 1); + } +} +``` + +## 3. 线程状态与生命周期 + +### 3.1 Java线程状态 + +Java线程有六种状态,定义在Thread.State枚举中: + +```java +import java.util.concurrent.TimeUnit; + +public class ThreadStateDemo { + + public static void main(String[] args) throws InterruptedException { + // 1. NEW状态 - 线程创建但未启动 + Thread newThread = new Thread(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "NewThread"); + + System.out.println("创建后的状态: " + newThread.getState()); // NEW + + // 2. RUNNABLE状态 - 线程正在运行或准备运行 + newThread.start(); + System.out.println("启动后的状态: " + newThread.getState()); // RUNNABLE + + // 3. TIMED_WAITING状态 - 线程等待指定时间 + Thread.sleep(100); + System.out.println("睡眠中的状态: " + newThread.getState()); // TIMED_WAITING + + // 4. WAITING状态演示 + Object lock = new Object(); + Thread waitingThread = new Thread(() -> { + synchronized (lock) { + try { + lock.wait(); // 进入WAITING状态 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "WaitingThread"); + + waitingThread.start(); + Thread.sleep(100); + System.out.println("等待中的状态: " + waitingThread.getState()); // WAITING + + // 5. BLOCKED状态演示 + Object blockLock = new Object(); + Thread blockingThread = new Thread(() -> { + synchronized (blockLock) { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }, "BlockingThread"); + + Thread blockedThread = new Thread(() -> { + synchronized (blockLock) { // 尝试获取已被占用的锁 + System.out.println("获得锁"); + } + }, "BlockedThread"); + + blockingThread.start(); + Thread.sleep(100); + blockedThread.start(); + Thread.sleep(100); + System.out.println("阻塞中的状态: " + blockedThread.getState()); // BLOCKED + + // 唤醒等待线程 + synchronized (lock) { + lock.notify(); + } + + // 等待所有线程结束 + newThread.join(); + waitingThread.join(); + blockingThread.join(); + blockedThread.join(); + + // 6. TERMINATED状态 - 线程执行完毕 + System.out.println("结束后的状态: " + newThread.getState()); // TERMINATED + } +} +``` + +### 3.2 线程状态转换图 + +```java +public class ThreadStateTransitionDemo { + + public static void main(String[] args) { + System.out.println("=== 线程状态转换演示 ==="); + + // 创建状态监控线程 + Thread monitorThread = new Thread(() -> { + Thread targetThread = Thread.currentThread(); + + while (!Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(500); + System.out.println("当前状态: " + targetThread.getState()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + + // 创建被监控的线程 + Thread targetThread = new Thread(() -> { + try { + System.out.println("线程开始执行"); + + // RUNNABLE -> TIMED_WAITING + Thread.sleep(1000); + + // TIMED_WAITING -> RUNNABLE + System.out.println("睡眠结束,继续执行"); + + // 同步块演示 + synchronized (ThreadStateTransitionDemo.class) { + Thread.sleep(1000); + } + + System.out.println("线程执行完毕"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.out.println("线程被中断"); + } + }, "TargetThread"); + + // 启动监控 + monitorThread.start(); + + try { + Thread.sleep(100); + System.out.println("目标线程创建后状态: " + targetThread.getState()); + + targetThread.start(); + targetThread.join(); + + System.out.println("目标线程结束后状态: " + targetThread.getState()); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + monitorThread.interrupt(); + } + } +} +``` + +## 4. 线程调度机制 + +### 4.1 线程优先级 + +JVM使用线程优先级来影响线程调度,但具体的调度策略依赖于操作系统。 + +```java +public class ThreadPriorityDemo { + + public static void main(String[] args) { + System.out.println("=== 线程优先级演示 ==="); + + // 创建不同优先级的线程 + Thread lowPriorityThread = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.println("低优先级线程: " + i); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "LowPriorityThread"); + + Thread normalPriorityThread = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.println("普通优先级线程: " + i); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "NormalPriorityThread"); + + Thread highPriorityThread = new Thread(() -> { + for (int i = 0; i < 10; i++) { + System.out.println("高优先级线程: " + i); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "HighPriorityThread"); + + // 设置优先级 + lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 1 + normalPriorityThread.setPriority(Thread.NORM_PRIORITY); // 5 + highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 10 + + System.out.println("低优先级: " + lowPriorityThread.getPriority()); + System.out.println("普通优先级: " + normalPriorityThread.getPriority()); + System.out.println("高优先级: " + highPriorityThread.getPriority()); + + // 启动线程 + lowPriorityThread.start(); + normalPriorityThread.start(); + highPriorityThread.start(); + + try { + lowPriorityThread.join(); + normalPriorityThread.join(); + highPriorityThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +### 4.2 线程调度策略 + +```java +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ThreadSchedulingDemo { + + public static void main(String[] args) { + System.out.println("=== 线程调度策略演示 ==="); + + // 1. 抢占式调度演示 + demonstratePreemptiveScheduling(); + + // 2. 时间片轮转演示 + demonstrateTimeSlicing(); + + // 3. 协作式调度演示 + demonstrateCooperativeScheduling(); + } + + // 抢占式调度 + private static void demonstratePreemptiveScheduling() { + System.out.println("\n--- 抢占式调度 ---"); + + Thread cpuIntensiveThread = new Thread(() -> { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 2000) { + // CPU密集型任务 + Math.sqrt(Math.random()); + } + System.out.println("CPU密集型线程完成"); + }, "CPUIntensiveThread"); + + Thread ioThread = new Thread(() -> { + try { + for (int i = 0; i < 5; i++) { + System.out.println("IO线程执行: " + i); + Thread.sleep(200); // 模拟IO操作 + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "IOThread"); + + cpuIntensiveThread.start(); + ioThread.start(); + + try { + cpuIntensiveThread.join(); + ioThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // 时间片轮转 + private static void demonstrateTimeSlicing() { + System.out.println("\n--- 时间片轮转 ---"); + + for (int i = 0; i < 3; i++) { + final int threadId = i; + Thread thread = new Thread(() -> { + for (int j = 0; j < 10; j++) { + System.out.println("线程" + threadId + " - 执行" + j); + // 主动让出CPU,模拟时间片结束 + Thread.yield(); + } + }, "Thread-" + i); + + thread.start(); + } + } + + // 协作式调度 + private static void demonstrateCooperativeScheduling() { + System.out.println("\n--- 协作式调度 ---"); + + Object lock = new Object(); + + Thread producer = new Thread(() -> { + synchronized (lock) { + for (int i = 0; i < 5; i++) { + System.out.println("生产者生产: " + i); + lock.notify(); // 通知消费者 + try { + if (i < 4) lock.wait(); // 等待消费者 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + }, "Producer"); + + Thread consumer = new Thread(() -> { + synchronized (lock) { + for (int i = 0; i < 5; i++) { + try { + lock.wait(); // 等待生产者 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + System.out.println("消费者消费: " + i); + lock.notify(); // 通知生产者 + } + } + }, "Consumer"); + + producer.start(); + consumer.start(); + + try { + producer.join(); + consumer.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + + diff --git a/docs/aThread/jvmThreadEvent.md b/docs/aThread/jvmThreadEvent.md new file mode 100644 index 000000000..ecf3873dd --- /dev/null +++ b/docs/aThread/jvmThreadEvent.md @@ -0,0 +1,2541 @@ +--- +title: jvm线程通信原理 +author: 哪吒 +date: '2023-06-15' +--- + +# jvm线程通信原理 + +JVM线程通信是多线程编程的核心概念,理解线程间如何安全、高效地交换数据和协调工作对于构建高质量的并发应用程序至关重要。本文将深入探讨JVM中各种线程通信机制的实现原理和最佳实践。 + +## 1. 线程通信概述 + +### 1.1 线程通信的定义与重要性 + +线程通信是指多个线程之间交换信息、协调执行顺序和共享资源的机制。在JVM中,线程通信主要通过共享内存模型实现。 + +``` + +## 5. ThreadLocal机制 + +### 5.1 ThreadLocal基本原理 + +`ThreadLocal`为每个线程提供独立的变量副本,实现线程间的数据隔离。 + +```java +public class ThreadLocalDemo { + // ThreadLocal变量,每个线程都有自己的副本 + private static final ThreadLocal threadLocalValue = new ThreadLocal() { + @Override + protected Integer initialValue() { + return 0; // 初始值 + } + }; + + private static final ThreadLocal threadLocalName = ThreadLocal.withInitial(() -> "DefaultName"); + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== ThreadLocal演示 ==="); + + // 创建多个线程,每个线程操作自己的ThreadLocal变量 + Thread[] threads = new Thread[3]; + + for (int i = 0; i < 3; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + // 设置当前线程的ThreadLocal值 + threadLocalValue.set(threadId * 100); + threadLocalName.set("Thread-" + threadId); + + System.out.println(Thread.currentThread().getName() + + " 设置值: " + threadLocalValue.get() + ", 名称: " + threadLocalName.get()); + + // 模拟一些工作 + for (int j = 0; j < 3; j++) { + int currentValue = threadLocalValue.get(); + threadLocalValue.set(currentValue + 1); + + System.out.println(Thread.currentThread().getName() + + " 当前值: " + threadLocalValue.get()); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + // 清理ThreadLocal,避免内存泄漏 + threadLocalValue.remove(); + threadLocalName.remove(); + + }, "Worker-" + i); + } + + // 启动所有线程 + for (Thread thread : threads) { + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + // 演示ThreadLocal的继承性 + demonstrateInheritableThreadLocal(); + } + + private static final InheritableThreadLocal inheritableThreadLocal = + new InheritableThreadLocal() { + @Override + protected String initialValue() { + return "Parent Value"; + } + + @Override + protected String childValue(String parentValue) { + return parentValue + " -> Child"; + } + }; + + private static void demonstrateInheritableThreadLocal() throws InterruptedException { + System.out.println("\n=== InheritableThreadLocal演示 ==="); + + // 在主线程中设置值 + inheritableThreadLocal.set("Main Thread Value"); + System.out.println("主线程值: " + inheritableThreadLocal.get()); + + // 创建子线程 + Thread childThread = new Thread(() -> { + System.out.println("子线程继承的值: " + inheritableThreadLocal.get()); + + // 子线程修改值 + inheritableThreadLocal.set("Child Thread Modified Value"); + System.out.println("子线程修改后的值: " + inheritableThreadLocal.get()); + + // 创建孙线程 + Thread grandChildThread = new Thread(() -> { + System.out.println("孙线程继承的值: " + inheritableThreadLocal.get()); + }, "GrandChild"); + + grandChildThread.start(); + try { + grandChildThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Child"); + + childThread.start(); + childThread.join(); + + // 主线程的值不受子线程影响 + System.out.println("主线程最终值: " + inheritableThreadLocal.get()); + + inheritableThreadLocal.remove(); + } +} +``` + +### 5.2 ThreadLocal内存泄漏问题 + +```java +import java.lang.ref.WeakReference; +import java.util.concurrent.TimeUnit; + +public class ThreadLocalMemoryLeak { + + // 模拟大对象 + static class LargeObject { + private final byte[] data = new byte[1024 * 1024]; // 1MB + private final String name; + + public LargeObject(String name) { + this.name = name; + } + + @Override + public String toString() { + return "LargeObject{name='" + name + "', size=1MB}"; + } + } + + private static final ThreadLocal threadLocal = new ThreadLocal<>(); + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== ThreadLocal内存泄漏演示 ==="); + + // 演示正确使用ThreadLocal + demonstrateCorrectUsage(); + + // 演示内存泄漏风险 + demonstrateMemoryLeakRisk(); + + // 演示WeakReference的使用 + demonstrateWeakReference(); + } + + private static void demonstrateCorrectUsage() throws InterruptedException { + System.out.println("\n--- 正确使用ThreadLocal ---"); + + Thread worker = new Thread(() -> { + try { + // 设置ThreadLocal值 + LargeObject obj = new LargeObject("CorrectUsage"); + threadLocal.set(obj); + + System.out.println("设置ThreadLocal: " + threadLocal.get()); + + // 模拟工作 + Thread.sleep(1000); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + // 重要:清理ThreadLocal,避免内存泄漏 + threadLocal.remove(); + System.out.println("ThreadLocal已清理"); + } + }, "CorrectWorker"); + + worker.start(); + worker.join(); + + // 建议进行垃圾回收 + System.gc(); + Thread.sleep(100); + } + + private static void demonstrateMemoryLeakRisk() throws InterruptedException { + System.out.println("\n--- 内存泄漏风险演示 ---"); + + Thread riskyWorker = new Thread(() -> { + // 设置ThreadLocal值但不清理 + LargeObject obj = new LargeObject("RiskyUsage"); + threadLocal.set(obj); + + System.out.println("设置ThreadLocal: " + threadLocal.get()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 注意:这里没有调用threadLocal.remove() + // 在线程池环境中,这可能导致内存泄漏 + System.out.println("线程结束,但ThreadLocal未清理"); + }, "RiskyWorker"); + + riskyWorker.start(); + riskyWorker.join(); + + // 即使线程结束,ThreadLocal的值可能仍然存在 + System.gc(); + Thread.sleep(100); + } + + private static void demonstrateWeakReference() { + System.out.println("\n--- WeakReference演示 ---"); + + LargeObject strongRef = new LargeObject("StrongReference"); + WeakReference weakRef = new WeakReference<>(strongRef); + + System.out.println("创建强引用和弱引用"); + System.out.println("弱引用对象: " + weakRef.get()); + + // 移除强引用 + strongRef = null; + + // 强制垃圾回收 + System.gc(); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 检查弱引用对象是否被回收 + if (weakRef.get() == null) { + System.out.println("弱引用对象已被垃圾回收"); + } else { + System.out.println("弱引用对象仍然存在: " + weakRef.get()); + } + } +} +``` + +## 6. Condition接口 + +### 6.1 Condition基本使用 + +`Condition`接口提供了比`wait/notify`更灵活的线程协调机制。 + +```java +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.LinkedList; +import java.util.Queue; + +public class ConditionDemo { + private final ReentrantLock lock = new ReentrantLock(); + private final Condition notEmpty = lock.newCondition(); + private final Condition notFull = lock.newCondition(); + + private final Queue queue = new LinkedList<>(); + private final int capacity = 5; + + public static void main(String[] args) { + ConditionDemo demo = new ConditionDemo(); + + // 创建生产者线程 + for (int i = 0; i < 2; i++) { + final int producerId = i; + new Thread(() -> { + try { + for (int j = 0; j < 10; j++) { + demo.produce("Producer-" + producerId + "-Item-" + j); + Thread.sleep(200); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Producer-" + i).start(); + } + + // 创建消费者线程 + for (int i = 0; i < 3; i++) { + new Thread(() -> { + try { + while (true) { + String item = demo.consume(); + if (item != null) { + Thread.sleep(300); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Consumer-" + i).start(); + } + } + + public void produce(String item) throws InterruptedException { + lock.lock(); + try { + // 队列满时等待 + while (queue.size() == capacity) { + System.out.println(Thread.currentThread().getName() + " 等待队列空闲..."); + notFull.await(); // 等待notFull条件 + } + + queue.offer(item); + System.out.println(Thread.currentThread().getName() + " 生产: " + item + + ", 队列大小: " + queue.size()); + + notEmpty.signal(); // 通知消费者队列不为空 + + } finally { + lock.unlock(); + } + } + + public String consume() throws InterruptedException { + lock.lock(); + try { + // 队列空时等待 + while (queue.isEmpty()) { + System.out.println(Thread.currentThread().getName() + " 等待数据..."); + notEmpty.await(); // 等待notEmpty条件 + } + + String item = queue.poll(); + System.out.println(Thread.currentThread().getName() + " 消费: " + item + + ", 队列大小: " + queue.size()); + + notFull.signal(); // 通知生产者队列不满 + + return item; + + } finally { + lock.unlock(); + } + } +} +``` + +### 6.2 多条件协调 + +```java +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class MultiConditionDemo { + private final ReentrantLock lock = new ReentrantLock(); + private final Condition conditionA = lock.newCondition(); + private final Condition conditionB = lock.newCondition(); + private final Condition conditionC = lock.newCondition(); + + private volatile int state = 0; // 0: A, 1: B, 2: C + + public static void main(String[] args) { + MultiConditionDemo demo = new MultiConditionDemo(); + + // 线程A + new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.printA(); + } + }, "Thread-A").start(); + + // 线程B + new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.printB(); + } + }, "Thread-B").start(); + + // 线程C + new Thread(() -> { + for (int i = 0; i < 5; i++) { + demo.printC(); + } + }, "Thread-C").start(); + } + + public void printA() { + lock.lock(); + try { + while (state != 0) { + conditionA.await(); + } + + System.out.println(Thread.currentThread().getName() + ": A"); + state = 1; + conditionB.signal(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } + + public void printB() { + lock.lock(); + try { + while (state != 1) { + conditionB.await(); + } + + System.out.println(Thread.currentThread().getName() + ": B"); + state = 2; + conditionC.signal(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } + + public void printC() { + lock.lock(); + try { + while (state != 2) { + conditionC.await(); + } + + System.out.println(Thread.currentThread().getName() + ": C"); + state = 0; + conditionA.signal(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + } +} +``` + +## 7. 高级同步工具 + +### 7.1 CountDownLatch + +`CountDownLatch`允许一个或多个线程等待其他线程完成操作。 + +```java +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class CountDownLatchDemo { + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== CountDownLatch演示 ==="); + + // 演示基本用法 + demonstrateBasicUsage(); + + // 演示超时等待 + demonstrateTimeoutWait(); + + // 演示多阶段任务协调 + demonstrateMultiPhaseCoordination(); + } + + private static void demonstrateBasicUsage() throws InterruptedException { + System.out.println("\n--- 基本用法演示 ---"); + + int workerCount = 3; + CountDownLatch startSignal = new CountDownLatch(1); // 开始信号 + CountDownLatch doneSignal = new CountDownLatch(workerCount); // 完成信号 + + // 创建工作线程 + for (int i = 0; i < workerCount; i++) { + final int workerId = i; + new Thread(() -> { + try { + System.out.println("工作线程" + workerId + " 准备就绪,等待开始信号..."); + startSignal.await(); // 等待开始信号 + + // 模拟工作 + System.out.println("工作线程" + workerId + " 开始工作"); + Thread.sleep((workerId + 1) * 1000); + System.out.println("工作线程" + workerId + " 完成工作"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneSignal.countDown(); // 通知完成 + } + }, "Worker-" + i).start(); + } + + Thread.sleep(1000); + System.out.println("主线程发出开始信号"); + startSignal.countDown(); // 发出开始信号 + + System.out.println("主线程等待所有工作线程完成..."); + doneSignal.await(); // 等待所有工作线程完成 + System.out.println("所有工作线程已完成"); + } + + private static void demonstrateTimeoutWait() throws InterruptedException { + System.out.println("\n--- 超时等待演示 ---"); + + CountDownLatch latch = new CountDownLatch(2); + + // 快速完成的任务 + new Thread(() -> { + try { + Thread.sleep(1000); + System.out.println("快速任务完成"); + latch.countDown(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "FastTask").start(); + + // 慢速任务 + new Thread(() -> { + try { + Thread.sleep(5000); // 5秒 + System.out.println("慢速任务完成"); + latch.countDown(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "SlowTask").start(); + + // 等待3秒 + boolean completed = latch.await(3, TimeUnit.SECONDS); + if (completed) { + System.out.println("所有任务在超时前完成"); + } else { + System.out.println("等待超时,剩余任务数: " + latch.getCount()); + } + } + + private static void demonstrateMultiPhaseCoordination() throws InterruptedException { + System.out.println("\n--- 多阶段任务协调演示 ---"); + + int taskCount = 3; + CountDownLatch phase1Latch = new CountDownLatch(taskCount); + CountDownLatch phase2Latch = new CountDownLatch(taskCount); + + for (int i = 0; i < taskCount; i++) { + final int taskId = i; + new Thread(() -> { + try { + // 阶段1 + System.out.println("任务" + taskId + " 执行阶段1"); + Thread.sleep(1000 + taskId * 500); + System.out.println("任务" + taskId + " 完成阶段1"); + phase1Latch.countDown(); + + // 等待所有任务完成阶段1 + phase1Latch.await(); + + // 阶段2 + System.out.println("任务" + taskId + " 执行阶段2"); + Thread.sleep(800 + taskId * 300); + System.out.println("任务" + taskId + " 完成阶段2"); + phase2Latch.countDown(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Task-" + i).start(); + } + + phase1Latch.await(); + System.out.println("所有任务完成阶段1,开始阶段2"); + + phase2Latch.await(); + System.out.println("所有任务完成阶段2"); + } +} +``` + +### 7.2 CyclicBarrier + +`CyclicBarrier`允许一组线程互相等待,直到到达某个公共屏障点。 + +```java +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.Random; + +public class CyclicBarrierDemo { + + public static void main(String[] args) { + System.out.println("=== CyclicBarrier演示 ==="); + + // 演示基本用法 + demonstrateBasicUsage(); + + // 演示可重用性 + demonstrateReusability(); + + // 演示屏障动作 + demonstrateBarrierAction(); + } + + private static void demonstrateBasicUsage() { + System.out.println("\n--- 基本用法演示 ---"); + + int participantCount = 3; + CyclicBarrier barrier = new CyclicBarrier(participantCount); + + for (int i = 0; i < participantCount; i++) { + final int participantId = i; + new Thread(() -> { + try { + Random random = new Random(); + + // 模拟准备工作 + int prepTime = 1000 + random.nextInt(2000); + System.out.println("参与者" + participantId + " 开始准备,需要" + prepTime + "ms"); + Thread.sleep(prepTime); + + System.out.println("参与者" + participantId + " 准备完成,等待其他参与者..."); + barrier.await(); // 等待所有参与者到达屏障 + + System.out.println("参与者" + participantId + " 开始执行后续任务"); + + } catch (InterruptedException | BrokenBarrierException e) { + Thread.currentThread().interrupt(); + System.err.println("参与者" + participantId + " 被中断: " + e.getMessage()); + } + }, "Participant-" + i).start(); + } + + try { + Thread.sleep(5000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void demonstrateReusability() { + System.out.println("\n--- 可重用性演示 ---"); + + int participantCount = 2; + CyclicBarrier barrier = new CyclicBarrier(participantCount); + + for (int i = 0; i < participantCount; i++) { + final int participantId = i; + new Thread(() -> { + try { + for (int round = 1; round <= 3; round++) { + System.out.println("参与者" + participantId + " 第" + round + "轮准备"); + Thread.sleep(1000); + + System.out.println("参与者" + participantId + " 第" + round + "轮到达屏障"); + barrier.await(); // 等待其他参与者 + + System.out.println("参与者" + participantId + " 第" + round + "轮继续执行"); + Thread.sleep(500); + } + } catch (InterruptedException | BrokenBarrierException e) { + Thread.currentThread().interrupt(); + System.err.println("参与者" + participantId + " 异常: " + e.getMessage()); + } + }, "ReusableParticipant-" + i).start(); + } + + try { + Thread.sleep(8000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void demonstrateBarrierAction() { + System.out.println("\n--- 屏障动作演示 ---"); + + int participantCount = 3; + + // 定义屏障动作 + Runnable barrierAction = () -> { + System.out.println("*** 所有参与者已到达屏障,执行屏障动作 ***"); + System.out.println("*** 屏障动作:汇总结果、清理资源等 ***"); + }; + + CyclicBarrier barrier = new CyclicBarrier(participantCount, barrierAction); + + for (int i = 0; i < participantCount; i++) { + final int participantId = i; + new Thread(() -> { + try { + System.out.println("参与者" + participantId + " 开始工作"); + Thread.sleep(1000 + participantId * 500); + + System.out.println("参与者" + participantId + " 完成工作,到达屏障"); + barrier.await(); // 最后一个到达的线程会执行屏障动作 + + System.out.println("参与者" + participantId + " 屏障后继续执行"); + + } catch (InterruptedException | BrokenBarrierException e) { + Thread.currentThread().interrupt(); + System.err.println("参与者" + participantId + " 异常: " + e.getMessage()); + } + }, "ActionParticipant-" + i).start(); + } + + try { + Thread.sleep(5000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +### 7.3 Semaphore + +`Semaphore`控制同时访问特定资源的线程数量。 + +```java +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class SemaphoreDemo { + + public static void main(String[] args) { + System.out.println("=== Semaphore演示 ==="); + + // 演示资源池管理 + demonstrateResourcePool(); + + // 演示公平性 + demonstrateFairness(); + + // 演示批量获取 + demonstrateBulkAcquisition(); + } + + private static void demonstrateResourcePool() { + System.out.println("\n--- 资源池管理演示 ---"); + + // 模拟数据库连接池,最多3个连接 + Semaphore connectionPool = new Semaphore(3); + + // 创建10个客户端线程 + for (int i = 0; i < 10; i++) { + final int clientId = i; + new Thread(() -> { + try { + System.out.println("客户端" + clientId + " 请求数据库连接..."); + + // 获取连接 + connectionPool.acquire(); + System.out.println("客户端" + clientId + " 获得数据库连接,剩余连接: " + + connectionPool.availablePermits()); + + // 模拟数据库操作 + Thread.sleep(2000 + (int)(Math.random() * 1000)); + + System.out.println("客户端" + clientId + " 释放数据库连接"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + connectionPool.release(); // 释放连接 + } + }, "Client-" + i).start(); + } + + try { + Thread.sleep(15000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void demonstrateFairness() { + System.out.println("\n--- 公平性演示 ---"); + + // 公平信号量 + Semaphore fairSemaphore = new Semaphore(1, true); + + for (int i = 0; i < 5; i++) { + final int threadId = i; + new Thread(() -> { + try { + for (int j = 0; j < 2; j++) { + System.out.println("线程" + threadId + " 第" + (j+1) + "次请求许可"); + fairSemaphore.acquire(); + + System.out.println("线程" + threadId + " 第" + (j+1) + "次获得许可"); + Thread.sleep(1000); + + fairSemaphore.release(); + System.out.println("线程" + threadId + " 第" + (j+1) + "次释放许可"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "FairThread-" + i).start(); + } + + try { + Thread.sleep(12000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void demonstrateBulkAcquisition() { + System.out.println("\n--- 批量获取演示 ---"); + + Semaphore semaphore = new Semaphore(5); + + // 小任务 - 需要1个许可 + for (int i = 0; i < 3; i++) { + final int taskId = i; + new Thread(() -> { + try { + System.out.println("小任务" + taskId + " 请求1个许可"); + semaphore.acquire(1); + + System.out.println("小任务" + taskId + " 获得许可,执行中..."); + Thread.sleep(2000); + + semaphore.release(1); + System.out.println("小任务" + taskId + " 完成,释放1个许可"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "SmallTask-" + i).start(); + } + + // 大任务 - 需要3个许可 + new Thread(() -> { + try { + Thread.sleep(1000); // 延迟启动 + System.out.println("大任务请求3个许可"); + + boolean acquired = semaphore.tryAcquire(3, 5, TimeUnit.SECONDS); + if (acquired) { + System.out.println("大任务获得3个许可,执行中..."); + Thread.sleep(3000); + + semaphore.release(3); + System.out.println("大任务完成,释放3个许可"); + } else { + System.out.println("大任务获取许可超时"); + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "BigTask").start(); + + try { + Thread.sleep(10000); // 等待演示完成 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +```java +public class ThreadCommunicationBasic { + // 共享变量 - 线程间通信的基础 + private static volatile boolean flag = false; + private static int sharedData = 0; + + public static void main(String[] args) { + // 生产者线程 + Thread producer = new Thread(() -> { + for (int i = 0; i < 5; i++) { + sharedData = i; + System.out.println("生产者设置数据: " + sharedData); + flag = true; // 通知消费者数据已准备好 + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "Producer"); + + // 消费者线程 + Thread consumer = new Thread(() -> { + while (true) { + if (flag) { + System.out.println("消费者读取数据: " + sharedData); + flag = false; // 重置标志 + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "Consumer"); + + producer.start(); + consumer.start(); + + try { + producer.join(); + consumer.interrupt(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +### 1.2 线程通信的挑战 + +线程通信面临的主要挑战包括: + +1. **数据竞争**:多个线程同时访问共享数据 +2. **可见性问题**:一个线程的修改对其他线程不可见 +3. **原子性问题**:操作不是原子的,可能被中断 +4. **有序性问题**:指令重排序导致的执行顺序问题 + +```java +import java.util.concurrent.atomic.AtomicInteger; + +public class ThreadCommunicationChallenges { + // 演示数据竞争问题 + private static int counter = 0; + private static AtomicInteger atomicCounter = new AtomicInteger(0); + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 数据竞争演示 ==="); + + // 创建多个线程同时修改共享变量 + Thread[] threads = new Thread[10]; + + for (int i = 0; i < 10; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + counter++; // 非原子操作,存在数据竞争 + atomicCounter.incrementAndGet(); // 原子操作,线程安全 + } + }); + } + + // 启动所有线程 + for (Thread thread : threads) { + thread.start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("普通计数器结果: " + counter); // 可能小于10000 + System.out.println("原子计数器结果: " + atomicCounter.get()); // 总是10000 + + // 演示可见性问题 + demonstrateVisibilityProblem(); + } + + private static volatile boolean stopFlag = false; + + private static void demonstrateVisibilityProblem() { + System.out.println("\n=== 可见性问题演示 ==="); + + Thread worker = new Thread(() -> { + int count = 0; + while (!stopFlag) { + count++; + } + System.out.println("工作线程停止,计数: " + count); + }); + + worker.start(); + + try { + Thread.sleep(1000); + stopFlag = true; // 由于volatile关键字,这个修改对工作线程可见 + System.out.println("主线程设置停止标志"); + + worker.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +## 2. wait/notify机制 + +### 2.1 wait/notify基本原理 + +`wait/notify`是Java中最基础的线程通信机制,基于对象监视器(Monitor)实现。 + +```java +public class WaitNotifyDemo { + private final Object lock = new Object(); + private boolean dataReady = false; + private String data; + + public static void main(String[] args) { + WaitNotifyDemo demo = new WaitNotifyDemo(); + + // 消费者线程 + Thread consumer = new Thread(() -> { + demo.consume(); + }, "Consumer"); + + // 生产者线程 + Thread producer = new Thread(() -> { + demo.produce(); + }, "Producer"); + + consumer.start(); + producer.start(); + + try { + consumer.join(); + producer.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void consume() { + synchronized (lock) { + while (!dataReady) { + try { + System.out.println("消费者等待数据..."); + lock.wait(); // 释放锁并等待 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + + System.out.println("消费者获得数据: " + data); + dataReady = false; + lock.notify(); // 通知生产者 + } + } + + public void produce() { + synchronized (lock) { + try { + Thread.sleep(2000); // 模拟数据准备时间 + data = "重要数据 - " + System.currentTimeMillis(); + dataReady = true; + + System.out.println("生产者准备好数据: " + data); + lock.notify(); // 通知消费者 + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} +``` + +### 2.2 生产者-消费者模式实现 + +```java +import java.util.LinkedList; +import java.util.Queue; + +public class ProducerConsumerPattern { + private final Queue buffer = new LinkedList<>(); + private final int capacity = 5; + private final Object lock = new Object(); + + public static void main(String[] args) { + ProducerConsumerPattern pattern = new ProducerConsumerPattern(); + + // 创建多个生产者 + for (int i = 0; i < 2; i++) { + final int producerId = i; + new Thread(() -> { + try { + for (int j = 0; j < 10; j++) { + pattern.produce(producerId * 10 + j); + Thread.sleep(100); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Producer-" + i).start(); + } + + // 创建多个消费者 + for (int i = 0; i < 3; i++) { + new Thread(() -> { + try { + while (true) { + Integer item = pattern.consume(); + if (item != null) { + Thread.sleep(200); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Consumer-" + i).start(); + } + } + + public void produce(int item) throws InterruptedException { + synchronized (lock) { + // 缓冲区满时等待 + while (buffer.size() == capacity) { + System.out.println(Thread.currentThread().getName() + " 等待缓冲区空闲..."); + lock.wait(); + } + + buffer.offer(item); + System.out.println(Thread.currentThread().getName() + " 生产: " + item + + ", 缓冲区大小: " + buffer.size()); + + lock.notifyAll(); // 通知所有等待的消费者 + } + } + + public Integer consume() throws InterruptedException { + synchronized (lock) { + // 缓冲区空时等待 + while (buffer.isEmpty()) { + System.out.println(Thread.currentThread().getName() + " 等待数据..."); + lock.wait(); + } + + Integer item = buffer.poll(); + System.out.println(Thread.currentThread().getName() + " 消费: " + item + + ", 缓冲区大小: " + buffer.size()); + + lock.notifyAll(); // 通知所有等待的生产者 + return item; + } + } +} +``` + +### 2.3 wait/notify的陷阱与最佳实践 + +```java +public class WaitNotifyBestPractices { + private final Object lock = new Object(); + private boolean condition = false; + + public static void main(String[] args) { + WaitNotifyBestPractices demo = new WaitNotifyBestPractices(); + + // 演示虚假唤醒问题 + demo.demonstrateSpuriousWakeup(); + + // 演示notify vs notifyAll + demo.demonstrateNotifyVsNotifyAll(); + } + + // 正确的wait使用方式 - 使用while循环 + public void correctWaitUsage() { + synchronized (lock) { + while (!condition) { // 使用while而不是if + try { + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + // 处理业务逻辑 + System.out.println("条件满足,执行业务逻辑"); + } + } + + // 错误的wait使用方式 - 使用if + public void incorrectWaitUsage() { + synchronized (lock) { + if (!condition) { // 错误:使用if可能导致虚假唤醒问题 + try { + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + // 可能在条件不满足时执行 + System.out.println("可能在条件不满足时执行"); + } + } + + private void demonstrateSpuriousWakeup() { + System.out.println("=== 虚假唤醒演示 ==="); + + Thread waiter1 = new Thread(() -> { + synchronized (lock) { + while (!condition) { + try { + System.out.println("线程1开始等待"); + lock.wait(); + System.out.println("线程1被唤醒,检查条件: " + condition); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + System.out.println("线程1执行业务逻辑"); + } + }, "Waiter-1"); + + Thread waiter2 = new Thread(() -> { + synchronized (lock) { + while (!condition) { + try { + System.out.println("线程2开始等待"); + lock.wait(); + System.out.println("线程2被唤醒,检查条件: " + condition); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + System.out.println("线程2执行业务逻辑"); + } + }, "Waiter-2"); + + Thread notifier = new Thread(() -> { + try { + Thread.sleep(2000); + synchronized (lock) { + condition = true; + System.out.println("设置条件为true并通知所有等待线程"); + lock.notifyAll(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Notifier"); + + waiter1.start(); + waiter2.start(); + notifier.start(); + + try { + waiter1.join(); + waiter2.join(); + notifier.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void demonstrateNotifyVsNotifyAll() { + System.out.println("\n=== notify vs notifyAll 演示 ==="); + + final Object notifyLock = new Object(); + final boolean[] ready = {false}; + + // 创建多个等待线程 + for (int i = 0; i < 3; i++) { + final int threadId = i; + new Thread(() -> { + synchronized (notifyLock) { + while (!ready[0]) { + try { + System.out.println("线程" + threadId + "开始等待"); + notifyLock.wait(); + System.out.println("线程" + threadId + "被唤醒"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + System.out.println("线程" + threadId + "执行完成"); + } + }, "Worker-" + i).start(); + } + + // 通知线程 + new Thread(() -> { + try { + Thread.sleep(2000); + synchronized (notifyLock) { + ready[0] = true; + System.out.println("使用notifyAll()唤醒所有等待线程"); + notifyLock.notifyAll(); // 使用notifyAll确保所有线程都被唤醒 + // 如果使用notify(),可能只有一个线程被唤醒 + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "Notifier").start(); + } +} +``` + +## 3. 管道通信 + +### 3.1 PipedInputStream/PipedOutputStream + +管道通信提供了一种基于流的线程间通信方式。 + +```java +import java.io.*; + +public class PipeStreamCommunication { + + public static void main(String[] args) { + try { + // 创建管道流 + PipedOutputStream outputStream = new PipedOutputStream(); + PipedInputStream inputStream = new PipedInputStream(outputStream); + + // 写入线程 + Thread writer = new Thread(() -> { + try (PrintWriter writer1 = new PrintWriter(outputStream, true)) { + for (int i = 0; i < 5; i++) { + String message = "消息 " + i + " - " + System.currentTimeMillis(); + writer1.println(message); + System.out.println("发送: " + message); + Thread.sleep(1000); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "Writer"); + + // 读取线程 + Thread reader = new Thread(() -> { + try (BufferedReader reader1 = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader1.readLine()) != null) { + System.out.println("接收: " + line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }, "Reader"); + + writer.start(); + reader.start(); + + writer.join(); + reader.join(); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } +} +``` + +### 3.2 PipedReader/PipedWriter + +```java +import java.io.*; + +public class PipeCharacterCommunication { + + public static void main(String[] args) { + try { + // 创建字符管道 + PipedWriter pipedWriter = new PipedWriter(); + PipedReader pipedReader = new PipedReader(pipedWriter); + + // 生产者线程 + Thread producer = new Thread(() -> { + try { + for (int i = 0; i < 10; i++) { + String data = "数据包-" + i + "\n"; + pipedWriter.write(data); + pipedWriter.flush(); + System.out.println("生产者发送: " + data.trim()); + Thread.sleep(500); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } finally { + try { + pipedWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }, "Producer"); + + // 消费者线程 + Thread consumer = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(pipedReader)) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println("消费者接收: " + line); + // 模拟处理时间 + Thread.sleep(200); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + }, "Consumer"); + + producer.start(); + consumer.start(); + + producer.join(); + consumer.join(); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } +} +``` + +## 4. 共享内存通信 + +### 4.1 volatile关键字 + +`volatile`关键字确保变量的可见性和有序性。 + +```java +public class VolatileCommunication { + // volatile确保多线程间的可见性 + private static volatile boolean running = true; + private static volatile int counter = 0; + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== volatile可见性演示 ==="); + + // 工作线程 + Thread worker = new Thread(() -> { + int localCounter = 0; + while (running) { + localCounter++; + if (localCounter % 100000000 == 0) { + System.out.println("工作线程运行中... 计数: " + localCounter); + } + } + counter = localCounter; + System.out.println("工作线程停止,最终计数: " + localCounter); + }, "Worker"); + + worker.start(); + + // 主线程等待3秒后停止工作线程 + Thread.sleep(3000); + running = false; // volatile变量的修改立即对工作线程可见 + System.out.println("主线程设置停止标志"); + + worker.join(); + System.out.println("最终计数器值: " + counter); + + // 演示volatile的有序性 + demonstrateVolatileOrdering(); + } + + private static volatile boolean flag = false; + private static int data = 0; + + private static void demonstrateVolatileOrdering() throws InterruptedException { + System.out.println("\n=== volatile有序性演示 ==="); + + Thread writer = new Thread(() -> { + data = 42; // 普通变量写入 + flag = true; // volatile变量写入,建立happens-before关系 + System.out.println("写入线程完成"); + }, "Writer"); + + Thread reader = new Thread(() -> { + while (!flag) { + // 等待flag变为true + Thread.yield(); + } + // 由于volatile的happens-before语义,这里能看到data=42 + System.out.println("读取线程看到data: " + data); + }, "Reader"); + + reader.start(); + Thread.sleep(100); // 确保reader先启动 + writer.start(); + + writer.join(); + reader.join(); + } +} +``` + +### 4.2 原子类通信 + +```java +import java.util.concurrent.atomic.*; + +public class AtomicCommunication { + private static final AtomicInteger atomicCounter = new AtomicInteger(0); + private static final AtomicReference atomicMessage = new AtomicReference<>(""); + private static final AtomicBoolean atomicFlag = new AtomicBoolean(false); + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 原子类通信演示 ==="); + + // 创建多个生产者线程 + Thread[] producers = new Thread[3]; + for (int i = 0; i < 3; i++) { + final int producerId = i; + producers[i] = new Thread(() -> { + for (int j = 0; j < 5; j++) { + // 原子性地增加计数器 + int count = atomicCounter.incrementAndGet(); + + // 原子性地更新消息 + String message = "Producer-" + producerId + "-Message-" + j; + atomicMessage.set(message); + + System.out.println("生产者" + producerId + " 发送消息: " + message + + ", 总计数: " + count); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "Producer-" + i); + } + + // 消费者线程 + Thread consumer = new Thread(() -> { + int lastCount = 0; + while (!atomicFlag.get() || atomicCounter.get() > lastCount) { + int currentCount = atomicCounter.get(); + if (currentCount > lastCount) { + String message = atomicMessage.get(); + System.out.println("消费者接收: " + message + ", 计数: " + currentCount); + lastCount = currentCount; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + System.out.println("消费者完成,最终计数: " + atomicCounter.get()); + }, "Consumer"); + + // 启动所有线程 + for (Thread producer : producers) { + producer.start(); + } + consumer.start(); + + // 等待生产者完成 + for (Thread producer : producers) { + producer.join(); + } + + // 设置完成标志 + atomicFlag.set(true); + consumer.join(); + + // 演示CAS操作 + demonstrateCASOperation(); + } + + private static void demonstrateCASOperation() { + System.out.println("\n=== CAS操作演示 ==="); + + AtomicInteger casCounter = new AtomicInteger(0); + + Thread[] threads = new Thread[5]; + for (int i = 0; i < 5; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < 3; j++) { + int expected, newValue; + do { + expected = casCounter.get(); + newValue = expected + 1; + System.out.println("线程" + threadId + " 尝试CAS: " + expected + " -> " + newValue); + } while (!casCounter.compareAndSet(expected, newValue)); + + System.out.println("线程" + threadId + " CAS成功: " + newValue); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, "CAS-Thread-" + i); + } + + for (Thread thread : threads) { + thread.start(); + } + + try { + for (Thread thread : threads) { + thread.join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + System.out.println("CAS最终结果: " + casCounter.get()); + } +} +``` + +## 8. 性能比较与分析 + +### 8.1 不同通信机制的性能测试 + +```java +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class CommunicationPerformanceTest { + + private static final int THREAD_COUNT = 4; + private static final int OPERATIONS_PER_THREAD = 100000; + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 线程通信性能测试 ==="); + + // 测试wait/notify + testWaitNotify(); + + // 测试Condition + testCondition(); + + // 测试CountDownLatch + testCountDownLatch(); + + // 测试Semaphore + testSemaphore(); + + // 测试AtomicInteger + testAtomicInteger(); + + // 测试volatile + testVolatile(); + } + + private static void testWaitNotify() throws InterruptedException { + System.out.println("\n--- wait/notify性能测试 ---"); + + final Object lock = new Object(); + final AtomicInteger counter = new AtomicInteger(0); + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + synchronized (lock) { + counter.incrementAndGet(); + lock.notify(); + if (counter.get() < THREAD_COUNT * OPERATIONS_PER_THREAD) { + try { + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("wait/notify耗时: " + (endTime - startTime) + "ms, 计数器值: " + counter.get()); + } + + private static void testCondition() throws InterruptedException { + System.out.println("\n--- Condition性能测试 ---"); + + final ReentrantLock lock = new ReentrantLock(); + final Condition condition = lock.newCondition(); + final AtomicInteger counter = new AtomicInteger(0); + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + lock.lock(); + try { + counter.incrementAndGet(); + condition.signal(); + if (counter.get() < THREAD_COUNT * OPERATIONS_PER_THREAD) { + condition.await(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } finally { + lock.unlock(); + } + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("Condition耗时: " + (endTime - startTime) + "ms, 计数器值: " + counter.get()); + } + + private static void testCountDownLatch() throws InterruptedException { + System.out.println("\n--- CountDownLatch性能测试 ---"); + + long startTime = System.currentTimeMillis(); + + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + CountDownLatch latch = new CountDownLatch(THREAD_COUNT); + + for (int j = 0; j < THREAD_COUNT; j++) { + new Thread(() -> { + // 模拟工作 + latch.countDown(); + }).start(); + } + + latch.await(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("CountDownLatch耗时: " + (endTime - startTime) + "ms"); + } + + private static void testSemaphore() throws InterruptedException { + System.out.println("\n--- Semaphore性能测试 ---"); + + final Semaphore semaphore = new Semaphore(1); + final AtomicInteger counter = new AtomicInteger(0); + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + try { + semaphore.acquire(); + counter.incrementAndGet(); + semaphore.release(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("Semaphore耗时: " + (endTime - startTime) + "ms, 计数器值: " + counter.get()); + } + + private static void testAtomicInteger() throws InterruptedException { + System.out.println("\n--- AtomicInteger性能测试 ---"); + + final AtomicInteger counter = new AtomicInteger(0); + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + counter.incrementAndGet(); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("AtomicInteger耗时: " + (endTime - startTime) + "ms, 计数器值: " + counter.get()); + } + + private static void testVolatile() throws InterruptedException { + System.out.println("\n--- volatile性能测试 ---"); + + final VolatileCounter counter = new VolatileCounter(); + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + synchronized (counter) { + counter.increment(); + } + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("volatile耗时: " + (endTime - startTime) + "ms, 计数器值: " + counter.getValue()); + } + + static class VolatileCounter { + private volatile int value = 0; + + public void increment() { + value++; + } + + public int getValue() { + return value; + } + } +} +``` + +### 8.2 内存使用分析 + +```java +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Semaphore; + +public class MemoryUsageAnalysis { + + private static final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 内存使用分析 ==="); + + // 分析ThreadLocal内存使用 + analyzeThreadLocalMemory(); + + // 分析同步工具内存使用 + analyzeSynchronizationToolsMemory(); + + // 分析大量线程的内存影响 + analyzeMassiveThreadsMemory(); + } + + private static void analyzeThreadLocalMemory() throws InterruptedException { + System.out.println("\n--- ThreadLocal内存分析 ---"); + + printMemoryUsage("初始状态"); + + // 创建大量ThreadLocal + ThreadLocal[] threadLocals = new ThreadLocal[1000]; + for (int i = 0; i < threadLocals.length; i++) { + threadLocals[i] = new ThreadLocal<>(); + } + + printMemoryUsage("创建1000个ThreadLocal后"); + + // 在多个线程中使用ThreadLocal + Thread[] threads = new Thread[10]; + for (int i = 0; i < threads.length; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (ThreadLocal tl : threadLocals) { + tl.set(new byte[1024]); // 1KB数据 + } + + try { + Thread.sleep(2000); // 保持线程活跃 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 清理ThreadLocal + for (ThreadLocal tl : threadLocals) { + tl.remove(); + } + }, "MemoryTestThread-" + threadId); + } + + for (Thread thread : threads) { + thread.start(); + } + + Thread.sleep(1000); + printMemoryUsage("线程使用ThreadLocal后"); + + for (Thread thread : threads) { + thread.join(); + } + + System.gc(); + Thread.sleep(1000); + printMemoryUsage("线程结束并GC后"); + } + + private static void analyzeSynchronizationToolsMemory() { + System.out.println("\n--- 同步工具内存分析 ---"); + + printMemoryUsage("创建同步工具前"); + + // 创建大量同步工具 + CountDownLatch[] latches = new CountDownLatch[1000]; + CyclicBarrier[] barriers = new CyclicBarrier[1000]; + Semaphore[] semaphores = new Semaphore[1000]; + + for (int i = 0; i < 1000; i++) { + latches[i] = new CountDownLatch(1); + barriers[i] = new CyclicBarrier(2); + semaphores[i] = new Semaphore(1); + } + + printMemoryUsage("创建3000个同步工具后"); + + // 清理引用 + for (int i = 0; i < 1000; i++) { + latches[i] = null; + barriers[i] = null; + semaphores[i] = null; + } + + System.gc(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + printMemoryUsage("清理引用并GC后"); + } + + private static void analyzeMassiveThreadsMemory() throws InterruptedException { + System.out.println("\n--- 大量线程内存分析 ---"); + + printMemoryUsage("创建线程前"); + + // 创建大量线程 + Thread[] threads = new Thread[100]; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threads.length); + + for (int i = 0; i < threads.length; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + try { + startLatch.await(); + + // 模拟工作 + Thread.sleep(2000); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }, "MassiveThread-" + threadId); + } + + for (Thread thread : threads) { + thread.start(); + } + + printMemoryUsage("创建100个线程后"); + + startLatch.countDown(); + endLatch.await(); + + printMemoryUsage("线程执行完成后"); + + // 等待线程结束 + for (Thread thread : threads) { + thread.join(); + } + + System.gc(); + Thread.sleep(1000); + printMemoryUsage("线程结束并GC后"); + } + + private static void printMemoryUsage(String phase) { + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); + + System.out.printf("%s:\n", phase); + System.out.printf(" 堆内存: 已用 %d MB / 最大 %d MB\n", + heapUsage.getUsed() / 1024 / 1024, + heapUsage.getMax() / 1024 / 1024); + System.out.printf(" 非堆内存: 已用 %d MB / 最大 %d MB\n", + nonHeapUsage.getUsed() / 1024 / 1024, + nonHeapUsage.getMax() / 1024 / 1024); + System.out.println(); + } +} +``` + +## 9. 最佳实践与选择指南 + +### 9.1 通信机制选择决策树 + +```java +public class CommunicationMechanismSelector { + + public enum CommunicationScenario { + PRODUCER_CONSUMER, + MASTER_WORKER, + PIPELINE, + BARRIER_SYNCHRONIZATION, + RESOURCE_POOL, + EVENT_NOTIFICATION, + DATA_SHARING + } + + public static String selectMechanism(CommunicationScenario scenario, + int threadCount, + boolean needTimeout, + boolean needFairness, + boolean highPerformance) { + + switch (scenario) { + case PRODUCER_CONSUMER: + if (highPerformance) { + return "推荐: ConcurrentLinkedQueue + AtomicInteger\n" + + "原因: 无锁实现,高并发性能好"; + } else { + return "推荐: BlockingQueue (ArrayBlockingQueue/LinkedBlockingQueue)\n" + + "原因: 内置阻塞机制,使用简单"; + } + + case MASTER_WORKER: + if (needTimeout) { + return "推荐: CountDownLatch + CompletableFuture\n" + + "原因: 支持超时等待和异步处理"; + } else { + return "推荐: CountDownLatch\n" + + "原因: 简单的一次性同步"; + } + + case PIPELINE: + return "推荐: CyclicBarrier\n" + + "原因: 支持多阶段同步,可重用"; + + case BARRIER_SYNCHRONIZATION: + if (threadCount > 10) { + return "推荐: CountDownLatch\n" + + "原因: 大量线程时性能更好"; + } else { + return "推荐: CyclicBarrier\n" + + "原因: 可重用,支持屏障动作"; + } + + case RESOURCE_POOL: + if (needFairness) { + return "推荐: Semaphore(fair=true)\n" + + "原因: 保证公平性"; + } else { + return "推荐: Semaphore(fair=false)\n" + + "原因: 性能更好"; + } + + case EVENT_NOTIFICATION: + if (highPerformance) { + return "推荐: volatile + CAS\n" + + "原因: 无锁,性能最佳"; + } else { + return "推荐: Condition\n" + + "原因: 精确控制,支持多条件"; + } + + case DATA_SHARING: + if (threadCount <= 4) { + return "推荐: ThreadLocal\n" + + "原因: 避免共享,无同步开销"; + } else { + return "推荐: ConcurrentHashMap + AtomicReference\n" + + "原因: 高并发安全共享"; + } + + default: + return "推荐: 根据具体需求选择合适的机制"; + } + } + + public static void main(String[] args) { + System.out.println("=== 线程通信机制选择指南 ==="); + + // 示例场景分析 + System.out.println("\n场景1: 高性能生产者-消费者"); + System.out.println(selectMechanism(CommunicationScenario.PRODUCER_CONSUMER, + 4, false, false, true)); + + System.out.println("\n场景2: 需要超时的主从模式"); + System.out.println(selectMechanism(CommunicationScenario.MASTER_WORKER, + 8, true, false, false)); + + System.out.println("\n场景3: 公平的资源池"); + System.out.println(selectMechanism(CommunicationScenario.RESOURCE_POOL, + 10, false, true, false)); + + System.out.println("\n场景4: 大量线程的屏障同步"); + System.out.println(selectMechanism(CommunicationScenario.BARRIER_SYNCHRONIZATION, + 50, false, false, false)); + } +} +``` + +### 9.2 性能优化技巧 + +```java +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; + +public class PerformanceOptimizationTips { + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== 性能优化技巧演示 ==="); + + // 技巧1: 使用LongAdder替代AtomicLong + demonstrateLongAdderOptimization(); + + // 技巧2: 减少锁竞争 + demonstrateLockContentionReduction(); + + // 技巧3: 批量操作 + demonstrateBatchOperations(); + + // 技巧4: 合理使用ThreadLocal + demonstrateThreadLocalOptimization(); + } + + private static void demonstrateLongAdderOptimization() throws InterruptedException { + System.out.println("\n--- 技巧1: LongAdder vs AtomicLong ---"); + + int threadCount = 8; + int operationsPerThread = 100000; + + // 测试AtomicLong + AtomicInteger atomicCounter = new AtomicInteger(0); + long startTime = System.currentTimeMillis(); + + Thread[] atomicThreads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + atomicThreads[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + atomicCounter.incrementAndGet(); + } + }); + } + + for (Thread thread : atomicThreads) { + thread.start(); + } + for (Thread thread : atomicThreads) { + thread.join(); + } + + long atomicTime = System.currentTimeMillis() - startTime; + System.out.println("AtomicInteger耗时: " + atomicTime + "ms, 结果: " + atomicCounter.get()); + + // 测试LongAdder + LongAdder longAdder = new LongAdder(); + startTime = System.currentTimeMillis(); + + Thread[] adderThreads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + adderThreads[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + longAdder.increment(); + } + }); + } + + for (Thread thread : adderThreads) { + thread.start(); + } + for (Thread thread : adderThreads) { + thread.join(); + } + + long adderTime = System.currentTimeMillis() - startTime; + System.out.println("LongAdder耗时: " + adderTime + "ms, 结果: " + longAdder.sum()); + System.out.println("性能提升: " + ((double)(atomicTime - adderTime) / atomicTime * 100) + "%"); + } + + private static void demonstrateLockContentionReduction() throws InterruptedException { + System.out.println("\n--- 技巧2: 减少锁竞争 ---"); + + // 粗粒度锁 vs 细粒度锁 + System.out.println("粗粒度锁 vs 细粒度锁:"); + + // 粗粒度锁示例 + CoarseGrainedCounter coarseCounter = new CoarseGrainedCounter(); + testCounter("粗粒度锁", coarseCounter); + + // 细粒度锁示例 + FineGrainedCounter fineCounter = new FineGrainedCounter(); + testCounter("细粒度锁", fineCounter); + } + + private static void testCounter(String name, Counter counter) throws InterruptedException { + int threadCount = 4; + int operationsPerThread = 50000; + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + if (threadId % 2 == 0) { + counter.incrementA(); + } else { + counter.incrementB(); + } + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println(name + "耗时: " + (endTime - startTime) + "ms, A=" + + counter.getA() + ", B=" + counter.getB()); + } + + interface Counter { + void incrementA(); + void incrementB(); + int getA(); + int getB(); + } + + static class CoarseGrainedCounter implements Counter { + private int a = 0; + private int b = 0; + + @Override + public synchronized void incrementA() { + a++; + } + + @Override + public synchronized void incrementB() { + b++; + } + + @Override + public synchronized int getA() { + return a; + } + + @Override + public synchronized int getB() { + return b; + } + } + + static class FineGrainedCounter implements Counter { + private final Object lockA = new Object(); + private final Object lockB = new Object(); + private int a = 0; + private int b = 0; + + @Override + public void incrementA() { + synchronized (lockA) { + a++; + } + } + + @Override + public void incrementB() { + synchronized (lockB) { + b++; + } + } + + @Override + public int getA() { + synchronized (lockA) { + return a; + } + } + + @Override + public int getB() { + synchronized (lockB) { + return b; + } + } + } + + private static void demonstrateBatchOperations() throws InterruptedException { + System.out.println("\n--- 技巧3: 批量操作 ---"); + + Semaphore semaphore = new Semaphore(10); + + // 单个获取 vs 批量获取 + long startTime = System.currentTimeMillis(); + + // 单个获取测试 + Thread singleThread = new Thread(() -> { + try { + for (int i = 0; i < 1000; i++) { + semaphore.acquire(); + // 模拟工作 + Thread.sleep(1); + semaphore.release(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + singleThread.start(); + singleThread.join(); + + long singleTime = System.currentTimeMillis() - startTime; + System.out.println("单个获取耗时: " + singleTime + "ms"); + + // 批量获取测试 + startTime = System.currentTimeMillis(); + + Thread batchThread = new Thread(() -> { + try { + for (int i = 0; i < 100; i++) { + semaphore.acquire(10); // 批量获取 + // 模拟批量工作 + Thread.sleep(10); + semaphore.release(10); // 批量释放 + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + batchThread.start(); + batchThread.join(); + + long batchTime = System.currentTimeMillis() - startTime; + System.out.println("批量获取耗时: " + batchTime + "ms"); + System.out.println("性能提升: " + ((double)(singleTime - batchTime) / singleTime * 100) + "%"); + } + + private static void demonstrateThreadLocalOptimization() throws InterruptedException { + System.out.println("\n--- 技巧4: ThreadLocal优化 ---"); + + // 使用ThreadLocal避免同步 + ThreadLocal threadLocalBuilder = ThreadLocal.withInitial(StringBuilder::new); + + int threadCount = 4; + int operationsPerThread = 100000; + + long startTime = System.currentTimeMillis(); + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + StringBuilder sb = threadLocalBuilder.get(); + for (int j = 0; j < operationsPerThread; j++) { + sb.append("Thread-").append(threadId).append("-").append(j).append(";"); + if (j % 1000 == 0) { + sb.setLength(0); // 清空,避免内存过大 + } + } + threadLocalBuilder.remove(); // 清理 + }); + } + + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + + long endTime = System.currentTimeMillis(); + System.out.println("ThreadLocal StringBuilder耗时: " + (endTime - startTime) + "ms"); + + // 对比:使用同步的StringBuilder + StringBuilder syncBuilder = new StringBuilder(); + startTime = System.currentTimeMillis(); + + Thread[] syncThreads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + syncThreads[i] = new Thread(() -> { + for (int j = 0; j < operationsPerThread; j++) { + synchronized (syncBuilder) { + syncBuilder.append("Thread-").append(threadId).append("-").append(j).append(";"); + if (j % 1000 == 0) { + syncBuilder.setLength(0); + } + } + } + }); + } + + for (Thread thread : syncThreads) { + thread.start(); + } + for (Thread thread : syncThreads) { + thread.join(); + } + + long syncTime = System.currentTimeMillis() - startTime; + System.out.println("同步StringBuilder耗时: " + syncTime + "ms"); + System.out.println("ThreadLocal性能提升: " + ((double)(syncTime - (endTime - startTime)) / syncTime * 100) + "%"); + } +} +``` + +## 10. 总结 + +### 10.1 核心要点 + +1. **通信机制分类** + - **等待/通知机制**: `wait/notify`、`Condition` + - **管道通信**: `PipedInputStream/OutputStream` + - **共享内存**: `volatile`、原子类、`ThreadLocal` + - **高级同步工具**: `CountDownLatch`、`CyclicBarrier`、`Semaphore` + +2. **性能特点** + - **无锁机制**: 原子类、`volatile` - 性能最佳 + - **轻量级锁**: `Condition`、`Semaphore` - 平衡性能和功能 + - **重量级锁**: `synchronized`、`wait/notify` - 功能完整但性能较低 + +3. **适用场景** + - **生产者-消费者**: `BlockingQueue`、`Condition` + - **主从模式**: `CountDownLatch`、`CompletableFuture` + - **屏障同步**: `CyclicBarrier`、`CountDownLatch` + - **资源池**: `Semaphore` + - **数据隔离**: `ThreadLocal` + +### 10.2 最佳实践 + +1. **选择原则** + - 优先考虑无锁方案(原子类、`volatile`) + - 根据场景选择合适的同步工具 + - 考虑性能、可维护性和复杂度的平衡 + +2. **性能优化** + - 减少锁的粒度和持有时间 + - 使用`LongAdder`替代高竞争的`AtomicLong` + - 合理使用`ThreadLocal`避免共享 + - 批量操作减少同步开销 + +3. **注意事项** + - 避免死锁和活锁 + - 正确处理中断 + - 及时清理`ThreadLocal` + - 合理设置超时时间 + +### 10.3 发展趋势 + +1. **响应式编程**: `CompletableFuture`、`RxJava` +2. **协程支持**: `Project Loom` +3. **更高效的并发原语**: `VarHandle`、`Stamped Lock` +4. **内存模型优化**: 更好的缓存一致性 + +JVM线程通信是并发编程的核心,掌握各种通信机制的原理和适用场景,能够帮助开发者构建高效、可靠的并发应用程序。随着Java平台的不断发展,新的并发工具和优化技术将继续涌现,为开发者提供更强大的并发编程能力。 + + diff --git a/docs/aThread/jvmThreadSync.md b/docs/aThread/jvmThreadSync.md new file mode 100644 index 000000000..d48284ff3 --- /dev/null +++ b/docs/aThread/jvmThreadSync.md @@ -0,0 +1,2256 @@ +# JVM线程同步机制 + +## 1. 线程同步概述 + +### 1.1 为什么需要线程同步 + +在多线程环境中,当多个线程同时访问共享资源时,可能会出现数据不一致的问题。线程同步机制确保在任意时刻,只有一个线程能够访问共享资源,从而保证数据的一致性和程序的正确性。 + +```java +// 不安全的计数器示例 +public class UnsafeCounter { + private int count = 0; + + public void increment() { + count++; // 非原子操作,存在线程安全问题 + } + + public int getCount() { + return count; + } +} + +// 测试线程安全问题 +public class CounterTest { + public static void main(String[] args) throws InterruptedException { + UnsafeCounter counter = new UnsafeCounter(); + + // 创建1000个线程,每个线程执行1000次increment + Thread[] threads = new Thread[1000]; + for (int i = 0; i < 1000; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + counter.increment(); + } + }); + threads[i].start(); + } + + // 等待所有线程完成 + for (Thread thread : threads) { + thread.join(); + } + + // 期望结果是1000000,但实际结果可能小于这个值 + System.out.println("最终计数: " + counter.getCount()); + } +} +``` + +### 1.2 Java内存模型(JMM) + +Java内存模型定义了线程如何通过内存进行交互,以及数据在何时对其他线程可见。 + +```java +/** + * Java内存模型示例 + * 演示可见性问题 + */ +public class VisibilityExample { + private boolean flag = false; + private int value = 0; + + // 写线程 + public void writer() { + value = 42; // 1. 写入value + flag = true; // 2. 写入flag + } + + // 读线程 + public void reader() { + if (flag) { // 3. 读取flag + // 由于重排序,这里可能读到value = 0 + System.out.println("Value: " + value); // 4. 读取value + } + } +} +``` + +## 2. synchronized关键字 + +### 2.1 synchronized基本用法 + +`synchronized`是Java中最基本的同步机制,可以用于方法和代码块。 + +```java +/** + * synchronized的三种使用方式 + */ +public class SynchronizedExample { + private int count = 0; + private final Object lock = new Object(); + + // 1. 同步实例方法(锁定当前对象) + public synchronized void incrementMethod() { + count++; + } + + // 2. 同步静态方法(锁定Class对象) + public static synchronized void staticMethod() { + System.out.println("静态同步方法"); + } + + // 3. 同步代码块 + public void incrementBlock() { + synchronized (this) { + count++; + } + } + + // 4. 使用自定义锁对象 + public void incrementWithCustomLock() { + synchronized (lock) { + count++; + } + } + + public synchronized int getCount() { + return count; + } +} +``` + +### 2.2 synchronized的实现原理 + +```java +/** + * synchronized底层实现分析 + * 使用javap -c命令查看字节码 + */ +public class SynchronizedPrinciple { + private int value = 0; + + // 同步方法:使用ACC_SYNCHRONIZED标志 + public synchronized void syncMethod() { + value++; + } + + // 同步代码块:使用monitorenter和monitorexit指令 + public void syncBlock() { + synchronized (this) { + value++; + } + } + + /** + * 字节码分析: + * syncMethod(): + * flags: ACC_PUBLIC, ACC_SYNCHRONIZED + * + * syncBlock(): + * monitorenter // 获取锁 + * iload_0 + * dup + * getfield #2 // value字段 + * iconst_1 + * iadd + * putfield #2 + * monitorexit // 释放锁 + */ +} +``` + +### 2.3 锁升级机制 + +```java +/** + * JVM锁优化:偏向锁 -> 轻量级锁 -> 重量级锁 + */ +public class LockUpgradeExample { + private int count = 0; + + // 偏向锁:只有一个线程访问 + public synchronized void biasedLock() { + count++; // 第一次访问,JVM会尝试使用偏向锁 + } + + // 轻量级锁:少量线程竞争 + public void lightweightLock() { + synchronized (this) { + count++; // 有竞争时,升级为轻量级锁 + } + } + + // 重量级锁:激烈竞争 + public void heavyweightLock() { + synchronized (this) { + try { + Thread.sleep(100); // 长时间持有锁,可能升级为重量级锁 + count++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} +``` + +## 3. volatile关键字 + +### 3.1 volatile的作用 + +`volatile`关键字保证变量的可见性和有序性,但不保证原子性。 + +```java +/** + * volatile关键字示例 + */ +public class VolatileExample { + // 不使用volatile的情况 + private boolean stopFlag = false; + + // 使用volatile保证可见性 + private volatile boolean volatileStopFlag = false; + + // 演示可见性问题 + public void demonstrateVisibility() { + // 工作线程 + Thread worker = new Thread(() -> { + int count = 0; + // 可能无法看到主线程对stopFlag的修改 + while (!stopFlag) { + count++; + } + System.out.println("工作线程停止,计数: " + count); + }); + + worker.start(); + + try { + Thread.sleep(1000); + stopFlag = true; // 主线程修改标志 + System.out.println("主线程设置停止标志"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + // 使用volatile解决可见性问题 + public void demonstrateVolatileVisibility() { + Thread worker = new Thread(() -> { + int count = 0; + // volatile保证能够看到主线程的修改 + while (!volatileStopFlag) { + count++; + } + System.out.println("Volatile工作线程停止,计数: " + count); + }); + + worker.start(); + + try { + Thread.sleep(1000); + volatileStopFlag = true; // 修改volatile变量 + System.out.println("主线程设置volatile停止标志"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +### 3.2 volatile的内存语义 + +```java +/** + * volatile的happens-before规则 + */ +public class VolatileMemorySemantics { + private int normalVar = 0; + private volatile boolean volatileVar = false; + + // 写线程 + public void writer() { + normalVar = 42; // 1. 普通写 + volatileVar = true; // 2. volatile写 + } + + // 读线程 + public void reader() { + if (volatileVar) { // 3. volatile读 + // 由于volatile的happens-before规则 + // 这里一定能看到normalVar = 42 + System.out.println("Normal var: " + normalVar); // 4. 普通读 + } + } + + /** + * volatile的内存屏障: + * 1. 在volatile写之前插入StoreStore屏障 + * 2. 在volatile写之后插入StoreLoad屏障 + * 3. 在volatile读之后插入LoadLoad屏障 + * 4. 在volatile读之后插入LoadStore屏障 + */ +} +``` + +### 3.3 双重检查锁定模式 + +```java +/** + * 使用volatile实现线程安全的单例模式 + */ +public class Singleton { + // 必须使用volatile,防止指令重排序 + private static volatile Singleton instance; + + private Singleton() { + // 私有构造函数 + } + + public static Singleton getInstance() { + if (instance == null) { // 第一次检查 + synchronized (Singleton.class) { + if (instance == null) { // 第二次检查 + instance = new Singleton(); // 可能发生指令重排序 + } + } + } + return instance; + } + + /** + * 为什么需要volatile? + * new Singleton()包含三个步骤: + * 1. 分配内存空间 + * 2. 初始化对象 + * 3. 将instance指向分配的内存 + * + * 如果发生重排序(1->3->2),其他线程可能看到未初始化的对象 + */ +} +``` + +## 4. Lock接口和AQS + +### 4.1 ReentrantLock基本用法 + +```java +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.Condition; + +/** + * ReentrantLock使用示例 + */ +public class ReentrantLockExample { + private final ReentrantLock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + private int count = 0; + private boolean ready = false; + + // 基本加锁操作 + public void increment() { + lock.lock(); + try { + count++; + } finally { + lock.unlock(); // 必须在finally中释放锁 + } + } + + // 尝试加锁 + public boolean tryIncrement() { + if (lock.tryLock()) { + try { + count++; + return true; + } finally { + lock.unlock(); + } + } + return false; + } + + // 可中断的锁 + public void interruptibleIncrement() throws InterruptedException { + lock.lockInterruptibly(); + try { + count++; + } finally { + lock.unlock(); + } + } + + // 使用Condition进行线程通信 + public void waitForReady() throws InterruptedException { + lock.lock(); + try { + while (!ready) { + condition.await(); // 等待条件满足 + } + System.out.println("条件已满足,继续执行"); + } finally { + lock.unlock(); + } + } + + public void setReady() { + lock.lock(); + try { + ready = true; + condition.signalAll(); // 通知等待的线程 + } finally { + lock.unlock(); + } + } + + public int getCount() { + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } +} +``` + +### 4.2 ReadWriteLock读写锁 + +```java +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.HashMap; +import java.util.Map; + +/** + * 读写锁示例:缓存实现 + */ +public class CacheWithReadWriteLock { + private final Map cache = new HashMap<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + // 读操作:可以并发执行 + public V get(K key) { + lock.readLock().lock(); + try { + return cache.get(key); + } finally { + lock.readLock().unlock(); + } + } + + // 写操作:独占执行 + public void put(K key, V value) { + lock.writeLock().lock(); + try { + cache.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } + + // 删除操作:独占执行 + public V remove(K key) { + lock.writeLock().lock(); + try { + return cache.remove(key); + } finally { + lock.writeLock().unlock(); + } + } + + // 获取缓存大小:读操作 + public int size() { + lock.readLock().lock(); + try { + return cache.size(); + } finally { + lock.readLock().unlock(); + } + } + + // 清空缓存:写操作 + public void clear() { + lock.writeLock().lock(); + try { + cache.clear(); + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +### 4.3 AQS原理简介 + +```java +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +/** + * 基于AQS实现的简单互斥锁 + */ +public class SimpleMutex { + + // 内部同步器 + private static class Sync extends AbstractQueuedSynchronizer { + + // 尝试获取锁 + @Override + protected boolean tryAcquire(int arg) { + // 使用CAS操作,将state从0设置为1 + return compareAndSetState(0, 1); + } + + // 尝试释放锁 + @Override + protected boolean tryRelease(int arg) { + // 检查当前线程是否持有锁 + if (getState() == 0) { + throw new IllegalMonitorStateException(); + } + // 释放锁 + setState(0); + return true; + } + + // 检查是否持有锁 + @Override + protected boolean isHeldExclusively() { + return getState() == 1; + } + } + + private final Sync sync = new Sync(); + + public void lock() { + sync.acquire(1); + } + + public boolean tryLock() { + return sync.tryAcquire(1); + } + + public void unlock() { + sync.release(1); + } + + public boolean isLocked() { + return sync.isHeldExclusively(); + } +} +``` + +## 5. 原子类(Atomic Classes) + +### 5.1 基本原子类 + +```java +import java.util.concurrent.atomic.*; + +/** + * 原子类使用示例 + */ +public class AtomicExample { + // 原子整数 + private final AtomicInteger atomicInt = new AtomicInteger(0); + + // 原子长整数 + private final AtomicLong atomicLong = new AtomicLong(0L); + + // 原子布尔值 + private final AtomicBoolean atomicBoolean = new AtomicBoolean(false); + + // 原子引用 + private final AtomicReference atomicRef = new AtomicReference<>("initial"); + + public void demonstrateAtomicOperations() { + // 基本操作 + int oldValue = atomicInt.get(); + int newValue = atomicInt.incrementAndGet(); // 原子递增 + + // CAS操作 + boolean success = atomicInt.compareAndSet(newValue, newValue + 10); + + // 原子更新 + int result = atomicInt.updateAndGet(x -> x * 2); + + // 原子累加 + int accumulated = atomicInt.accumulateAndGet(5, Integer::sum); + + System.out.println("原子操作结果: " + atomicInt.get()); + } + + // 线程安全的计数器 + public void safeIncrement() { + atomicInt.incrementAndGet(); + } + + // 线程安全的状态切换 + public boolean toggleState() { + return atomicBoolean.compareAndSet(false, true) || + atomicBoolean.compareAndSet(true, false); + } + + // 线程安全的引用更新 + public void updateReference(String newValue) { + atomicRef.set(newValue); + } +} +``` + +### 5.2 原子数组 + +```java +import java.util.concurrent.atomic.AtomicIntegerArray; + +/** + * 原子数组示例 + */ +public class AtomicArrayExample { + private final AtomicIntegerArray atomicArray; + + public AtomicArrayExample(int size) { + this.atomicArray = new AtomicIntegerArray(size); + } + + // 原子地更新数组元素 + public void updateElement(int index, int value) { + atomicArray.set(index, value); + } + + // 原子地递增数组元素 + public int incrementElement(int index) { + return atomicArray.incrementAndGet(index); + } + + // 原子地比较并设置 + public boolean compareAndSetElement(int index, int expect, int update) { + return atomicArray.compareAndSet(index, expect, update); + } + + // 获取数组元素 + public int getElement(int index) { + return atomicArray.get(index); + } + + // 获取数组长度 + public int length() { + return atomicArray.length(); + } +} +``` + +### 5.3 字段更新器 + +```java +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +/** + * 字段更新器示例 + */ +public class FieldUpdaterExample { + // 必须是volatile字段 + private volatile int volatileInt = 0; + private volatile String volatileString = "initial"; + + // 创建字段更新器 + private static final AtomicIntegerFieldUpdater INT_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(FieldUpdaterExample.class, "volatileInt"); + + private static final AtomicReferenceFieldUpdater STRING_UPDATER = + AtomicReferenceFieldUpdater.newUpdater(FieldUpdaterExample.class, String.class, "volatileString"); + + // 原子地更新整数字段 + public void updateIntField(int newValue) { + INT_UPDATER.set(this, newValue); + } + + // 原子地递增整数字段 + public int incrementIntField() { + return INT_UPDATER.incrementAndGet(this); + } + + // 原子地更新字符串字段 + public void updateStringField(String newValue) { + STRING_UPDATER.set(this, newValue); + } + + // 原子地比较并设置字符串字段 + public boolean compareAndSetStringField(String expect, String update) { + return STRING_UPDATER.compareAndSet(this, expect, update); + } + + public int getIntField() { + return volatileInt; + } + + public String getStringField() { + return volatileString; + } +} +``` + +## 6. 并发集合 + +### 6.1 ConcurrentHashMap + +```java +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +/** + * ConcurrentHashMap使用示例 + */ +public class ConcurrentHashMapExample { + private final ConcurrentHashMap concurrentMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap counterMap = new ConcurrentHashMap<>(); + + // 线程安全的put操作 + public void putValue(String key, Integer value) { + concurrentMap.put(key, value); + } + + // 原子的putIfAbsent操作 + public Integer putIfAbsent(String key, Integer value) { + return concurrentMap.putIfAbsent(key, value); + } + + // 原子的replace操作 + public boolean replaceValue(String key, Integer oldValue, Integer newValue) { + return concurrentMap.replace(key, oldValue, newValue); + } + + // 原子的compute操作 + public Integer computeValue(String key) { + return concurrentMap.compute(key, (k, v) -> { + if (v == null) { + return 1; + } else { + return v + 1; + } + }); + } + + // 高效的计数器实现 + public void incrementCounter(String key) { + counterMap.computeIfAbsent(key, k -> new LongAdder()).increment(); + } + + public long getCounterValue(String key) { + LongAdder adder = counterMap.get(key); + return adder != null ? adder.sum() : 0; + } + + // 批量操作 + public void batchUpdate() { + concurrentMap.forEach((key, value) -> { + concurrentMap.put(key, value * 2); + }); + } +} +``` + +### 6.2 阻塞队列 + +```java +import java.util.concurrent.*; + +/** + * 阻塞队列示例:生产者-消费者模式 + */ +public class BlockingQueueExample { + private final BlockingQueue queue = new ArrayBlockingQueue<>(10); + + // 生产者 + public class Producer implements Runnable { + @Override + public void run() { + try { + for (int i = 0; i < 20; i++) { + String item = "Item-" + i; + queue.put(item); // 阻塞式放入 + System.out.println("生产: " + item); + Thread.sleep(100); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + // 消费者 + public class Consumer implements Runnable { + @Override + public void run() { + try { + while (true) { + String item = queue.take(); // 阻塞式取出 + System.out.println("消费: " + item); + Thread.sleep(200); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public void startProducerConsumer() { + // 启动生产者和消费者 + new Thread(new Producer()).start(); + new Thread(new Consumer()).start(); + } + + // 使用不同类型的阻塞队列 + public void demonstrateDifferentQueues() { + // 有界队列 + BlockingQueue boundedQueue = new ArrayBlockingQueue<>(5); + + // 无界队列 + BlockingQueue unboundedQueue = new LinkedBlockingQueue<>(); + + // 优先级队列 + BlockingQueue priorityQueue = new PriorityBlockingQueue<>(); + + // 延迟队列 + BlockingQueue delayQueue = new DelayQueue<>(); + + // 同步队列 + BlockingQueue synchronousQueue = new SynchronousQueue<>(); + } +} +``` + +## 7. 线程池和Executor框架 + +### 7.1 ThreadPoolExecutor详解 + +```java +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 线程池详细配置示例 + */ +public class ThreadPoolExample { + + // 自定义线程工厂 + private static class CustomThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public CustomThreadFactory(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, namePrefix + "-thread-" + threadNumber.getAndIncrement()); + thread.setDaemon(false); + thread.setPriority(Thread.NORM_PRIORITY); + return thread; + } + } + + // 自定义拒绝策略 + private static class CustomRejectedExecutionHandler implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + System.err.println("任务被拒绝: " + r.toString()); + // 可以选择记录日志、抛出异常或其他处理方式 + } + } + + public void createCustomThreadPool() { + ThreadPoolExecutor executor = new ThreadPoolExecutor( + 2, // 核心线程数 + 4, // 最大线程数 + 60L, // 空闲线程存活时间 + TimeUnit.SECONDS, // 时间单位 + new LinkedBlockingQueue<>(10), // 工作队列 + new CustomThreadFactory("MyPool"), // 线程工厂 + new CustomRejectedExecutionHandler() // 拒绝策略 + ); + + // 提交任务 + for (int i = 0; i < 20; i++) { + final int taskId = i; + executor.submit(() -> { + System.out.println("执行任务 " + taskId + ", 线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + // 关闭线程池 + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + } + } + + // 预定义线程池 + public void demonstratePredefinedPools() { + // 固定大小线程池 + ExecutorService fixedPool = Executors.newFixedThreadPool(4); + + // 缓存线程池 + ExecutorService cachedPool = Executors.newCachedThreadPool(); + + // 单线程池 + ExecutorService singlePool = Executors.newSingleThreadExecutor(); + + // 定时任务线程池 + ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2); + + // 工作窃取线程池(Java 8+) + ExecutorService workStealingPool = Executors.newWorkStealingPool(); + } +} +``` + +### 7.2 CompletableFuture异步编程 + +```java +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * CompletableFuture异步编程示例 + */ +public class CompletableFutureExample { + private final ExecutorService executor = Executors.newFixedThreadPool(4); + + // 基本异步操作 + public void basicAsyncOperations() { + // 异步执行 + CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Hello"; + }, executor); + + // 链式操作 + CompletableFuture future2 = future1 + .thenApply(s -> s + " World") + .thenApply(String::toUpperCase); + + // 异步回调 + future2.thenAccept(result -> { + System.out.println("结果: " + result); + }); + } + + // 组合多个异步操作 + public void combineAsyncOperations() { + CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { + sleep(1000); + return "Hello"; + }); + + CompletableFuture future2 = CompletableFuture.supplyAsync(() -> { + sleep(2000); + return "World"; + }); + + // 等待两个任务都完成 + CompletableFuture combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2); + + // 任意一个完成即可 + CompletableFuture eitherFuture = future1.applyToEither(future2, s -> "First: " + s); + + // 等待所有任务完成 + CompletableFuture allOf = CompletableFuture.allOf(future1, future2); + + // 等待任意一个任务完成 + CompletableFuture anyOf = CompletableFuture.anyOf(future1, future2); + } + + // 异常处理 + public void handleExceptions() { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + if (Math.random() > 0.5) { + throw new RuntimeException("随机异常"); + } + return "成功"; + }) + .handle((result, throwable) -> { + if (throwable != null) { + return "处理异常: " + throwable.getMessage(); + } + return result; + }) + .exceptionally(throwable -> { + return "异常恢复: " + throwable.getMessage(); + }); + + future.thenAccept(System.out::println); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} +``` + +## 8. 线程安全的设计模式 + +### 8.1 不变性模式 + +```java +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +/** + * 不变性模式:通过不可变对象实现线程安全 + */ +public final class ImmutablePerson { + private final String name; + private final int age; + private final List hobbies; + + public ImmutablePerson(String name, int age, List hobbies) { + this.name = name; + this.age = age; + // 防御性复制 + this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies)); + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public List getHobbies() { + return hobbies; // 返回不可变视图 + } + + // 修改操作返回新对象 + public ImmutablePerson withAge(int newAge) { + return new ImmutablePerson(this.name, newAge, new ArrayList<>(this.hobbies)); + } + + public ImmutablePerson addHobby(String hobby) { + List newHobbies = new ArrayList<>(this.hobbies); + newHobbies.add(hobby); + return new ImmutablePerson(this.name, this.age, newHobbies); + } +} +``` + +### 8.2 线程局部存储模式 + +```java +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * ThreadLocal使用示例 + */ +public class ThreadLocalExample { + + // SimpleDateFormat不是线程安全的,使用ThreadLocal解决 + private static final ThreadLocal DATE_FORMAT = + ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + + // 用户上下文 + private static final ThreadLocal USER_CONTEXT = new ThreadLocal<>(); + + public static class UserContext { + private String userId; + private String userName; + + public UserContext(String userId, String userName) { + this.userId = userId; + this.userName = userName; + } + + // getters and setters + public String getUserId() { return userId; } + public String getUserName() { return userName; } + } + + // 线程安全的日期格式化 + public static String formatDate(Date date) { + return DATE_FORMAT.get().format(date); + } + + // 设置用户上下文 + public static void setUserContext(String userId, String userName) { + USER_CONTEXT.set(new UserContext(userId, userName)); + } + + // 获取当前用户 + public static UserContext getCurrentUser() { + return USER_CONTEXT.get(); + } + + // 清理ThreadLocal(重要:防止内存泄漏) + public static void clearContext() { + USER_CONTEXT.remove(); + } + + // 使用示例 + public void demonstrateThreadLocal() { + // 在不同线程中设置不同的用户上下文 + Thread thread1 = new Thread(() -> { + setUserContext("001", "Alice"); + System.out.println("Thread 1 - User: " + getCurrentUser().getUserName()); + System.out.println("Thread 1 - Date: " + formatDate(new Date())); + clearContext(); + }); + + Thread thread2 = new Thread(() -> { + setUserContext("002", "Bob"); + System.out.println("Thread 2 - User: " + getCurrentUser().getUserName()); + System.out.println("Thread 2 - Date: " + formatDate(new Date())); + clearContext(); + }); + + thread1.start(); + thread2.start(); + } +} +``` + +## 9. 性能优化和最佳实践 + +### 9.1 锁优化技巧 + +```java +/** + * 锁优化最佳实践 + */ +public class LockOptimization { + private final Object lock1 = new Object(); + private final Object lock2 = new Object(); + private volatile boolean flag = false; + + // 1. 减小锁的粒度 + private int count1 = 0; + private int count2 = 0; + + // 不好的做法:粗粒度锁 + public synchronized void badIncrement() { + count1++; + count2++; + } + + // 好的做法:细粒度锁 + public void goodIncrement() { + synchronized (lock1) { + count1++; + } + synchronized (lock2) { + count2++; + } + } + + // 2. 减少锁的持有时间 + public void optimizedMethod() { + // 准备工作(不需要锁) + String data = prepareData(); + + // 只在必要时加锁 + synchronized (this) { + // 快速完成临界区操作 + updateSharedState(data); + } + + // 后续处理(不需要锁) + postProcess(); + } + + // 3. 锁分离 + private final Object readLock = new Object(); + private final Object writeLock = new Object(); + + public void separatedLockRead() { + synchronized (readLock) { + // 读操作 + } + } + + public void separatedLockWrite() { + synchronized (writeLock) { + // 写操作 + } + } + + // 4. 避免不必要的同步 + public void avoidUnnecessarySync() { + // 使用双重检查锁定模式 + if (!flag) { + synchronized (this) { + if (!flag) { + // 初始化操作 + flag = true; + } + } + } + } + + private String prepareData() { return "data"; } + private void updateSharedState(String data) { /* update */ } + private void postProcess() { /* process */ } +} +``` + +### 9.2 无锁编程 + +```java +import java.util.concurrent.atomic.AtomicReference; + +/** + * 无锁数据结构示例:无锁栈 + */ +public class LockFreeStack { + private final AtomicReference> top = new AtomicReference<>(); + + private static class Node { + final T data; + final Node next; + + Node(T data, Node next) { + this.data = data; + this.next = next; + } + } + + // 无锁push操作 + public void push(T item) { + Node newNode = new Node<>(item, null); + Node currentTop; + + do { + currentTop = top.get(); + newNode.next = currentTop; + } while (!top.compareAndSet(currentTop, newNode)); + } + + // 无锁pop操作 + public T pop() { + Node currentTop; + Node newTop; + + do { + currentTop = top.get(); + if (currentTop == null) { + return null; + } + newTop = currentTop.next; + } while (!top.compareAndSet(currentTop, newTop)); + + return currentTop.data; + } + + public boolean isEmpty() { + return top.get() == null; + } +} +``` + +## 10. 总结 + +JVM线程同步机制是Java并发编程的核心,掌握以下要点: + +### 核心概念 + +- **Java内存模型**:理解可见性、有序性、原子性 +- **synchronized**:最基本的同步机制,了解锁升级过程 +- **volatile**:保证可见性和有序性,适用于状态标志 +- **Lock接口**:提供更灵活的锁机制 +- **原子类**:无锁的线程安全操作 + +### 最佳实践 + +1. **优先使用不可变对象** +2. **合理选择同步机制**:根据场景选择synchronized、Lock或原子类 +3. **减小锁的粒度和持有时间** +4. **避免死锁**:统一加锁顺序,使用超时机制 +5. **正确使用ThreadLocal**:注意内存泄漏问题 +6. **合理配置线程池**:根据任务特性选择合适的线程池 + +### 性能考虑 + +- **锁竞争**:减少锁竞争,提高并发性能 +- **上下文切换**:减少不必要的线程切换 +- **内存可见性**:合理使用volatile和final +- **无锁编程**:在适当场景使用CAS操作 + +通过深入理解这些机制和最佳实践,可以编写出高效、安全的多线程Java程序。 + +```java + +提交任务1后 - 核心线程数: 2, 当前线程数: 1, 队列大小: 0 +任务1开始执行,线程: CustomThread-1 +任务2开始执行,线程: CustomThread-2 +提交任务2后 - 核心线程数: 2, 当前线程数: 2, 队列大小: 0 +提交任务3后 - 核心线程数: 2, 当前线程数: 2, 队列大小: 1 +提交任务4后 - 核心线程数: 2, 当前线程数: 2, 队列大小: 2 +提交任务5后 - 核心线程数: 2, 当前线程数: 3, 队列大小: 2 +提交任务6后 - 核心线程数: 2, 当前线程数: 4, 队列大小: 2 +任务被拒绝: java.util.concurrent.FutureTask@3b9a45b3, 当前线程数: 4, 队列大小: 2 +提交任务7后 - 核心线程数: 2, 当前线程数: 4, 队列大小: 2 +任务6开始执行,线程: CustomThread-4 +任务被拒绝: java.util.concurrent.FutureTask@7699a589, 当前线程数: 4, 队列大小: 2 +提交任务8后 - 核心线程数: 2, 当前线程数: 4, 队列大小: 2 +任务5开始执行,线程: CustomThread-3 +任务5执行完成 +任务1执行完成 +任务3开始执行,线程: CustomThread-3 +任务6执行完成 +任务4开始执行,线程: CustomThread-1 +任务2执行完成 +任务3执行完成 +任务4执行完成 + +初始状态: RUNNING +提交任务后: RUNNING +调用shutdown后: TIDYING +最终状态: TERMINATED + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Main1 { + public static void demonstrateStates() { + ThreadPoolExecutor executor = new ThreadPoolExecutor( + 2, 4, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(10) + ); + + System.out.println("初始状态: " + getPoolState(executor)); + + // 提交任务 + for (int i = 0; i < 3; i++) { + executor.submit(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + System.out.println("提交任务后: " + getPoolState(executor)); + + // 关闭线程池 + executor.shutdown(); + System.out.println("调用shutdown后: " + getPoolState(executor)); + + try { + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("最终状态: " + getPoolState(executor)); + } + +// 获取线程池的状态 + private static String getPoolState(ThreadPoolExecutor executor) { + // 如果线程池已经终止 + if (executor.isTerminated()) { + // 返回TERMINATED + return "TERMINATED"; + // 如果线程池正在终止 + } else if (executor.isTerminating()) { + // 返回TIDYING + return "TIDYING"; + // 如果线程池已经关闭 + } else if (executor.isShutdown()) { + // 返回SHUTDOWN + return "SHUTDOWN"; + // 如果线程池正在运行 + } else { + // 返回RUNNING + return "RUNNING"; + } + } + + public static void main(String[] args) { + demonstrateStates(); + } +} + + + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Main2 { + // 基本用法示例 + public static void basicUsage() { + // 创建一个固定大小为3的线程池 + ExecutorService executor = Executors.newFixedThreadPool(3); + + // 提交10个任务 + for (int i = 1; i <= 10; i++) { + final int taskId = i; + // 提交任务到线程池 + executor.submit(() -> { + System.out.println("任务" + taskId + "开始执行,线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println("任务" + taskId + "执行完成"); + }); + } + + // 关闭线程池 + executor.shutdown(); + try { + // 等待所有任务完成,最多等待15秒 + if (!executor.awaitTermination(15, TimeUnit.SECONDS)) { + // 如果15秒内任务未完成,则强制关闭线程池 + executor.shutdownNow(); + } + } catch (InterruptedException e) { + // 如果等待过程中被中断,则强制关闭线程池 + executor.shutdownNow(); + } + } + + // 批量处理任务示例 + public static void batchProcessing() { + // 创建一个固定大小为4的线程池 + ExecutorService executor = Executors.newFixedThreadPool(4); + + // 模拟批量数据处理 + String[] data = {"数据1", "数据2", "数据3", "数据4", "数据5", "数据6", "数据7", "数据8"}; + + // 创建一个CompletableFuture数组,用于存储每个任务的Future对象 + CompletableFuture[] futures = new CompletableFuture[data.length]; + + // 提交任务到线程池 + for (int i = 0; i < data.length; i++) { + final String item = data[i]; + futures[i] = CompletableFuture.runAsync(() -> { + System.out.println("处理" + item + ",线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(1000); // 模拟处理时间 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println(item + "处理完成"); + }, executor); + } + + // 等待所有任务完成 + CompletableFuture.allOf(futures).join(); + System.out.println("所有数据处理完成"); + + // 关闭线程池 + executor.shutdown(); + } + + public static void main(String[] args) { + System.out.println("=== 基本用法 ==="); + basicUsage(); + + System.out.println("\n=== 批量处理 ==="); + batchProcessing(); + } +} + +"C:\Program Files\Java\jdk1.8.0_351\bin\java.exe" "-javaagent:D:\software\IntelliJ IDEA 2024.1.7\lib\idea_rt.jar=53209:D:\software\IntelliJ IDEA 2024.1.7\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_351\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_351\jre\lib\rt.jar;D:\pei-pei-zhuanshu\pei-pei-class-1\out\production\pei-pei-class-1" Main2 +=== 基本用法 === +任务1开始执行,线程: pool-1-thread-1 +任务3开始执行,线程: pool-1-thread-3 +任务2开始执行,线程: pool-1-thread-2 +任务3执行完成 +任务1执行完成 +任务2执行完成 +任务5开始执行,线程: pool-1-thread-1 +任务4开始执行,线程: pool-1-thread-3 +任务6开始执行,线程: pool-1-thread-2 +任务4执行完成 +任务7开始执行,线程: pool-1-thread-3 +任务5执行完成 +任务8开始执行,线程: pool-1-thread-1 +任务6执行完成 +任务9开始执行,线程: pool-1-thread-2 +任务7执行完成 +任务9执行完成 +任务10开始执行,线程: pool-1-thread-3 +任务8执行完成 +任务10执行完成 + +=== 批量处理 === +处理数据3,线程: pool-2-thread-3 +处理数据4,线程: pool-2-thread-4 +处理数据2,线程: pool-2-thread-2 +处理数据1,线程: pool-2-thread-1 +数据1处理完成 +数据3处理完成 +处理数据6,线程: pool-2-thread-3 +数据4处理完成 +数据2处理完成 +处理数据7,线程: pool-2-thread-4 +处理数据5,线程: pool-2-thread-1 +处理数据8,线程: pool-2-thread-2 +数据7处理完成 +数据8处理完成 +数据6处理完成 +数据5处理完成 +所有数据处理完成 + +进程已结束,退出代码为 0 + + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Main3 { + // 动态线程创建示例 + public static void dynamicThreadCreation() { + // 创建一个缓存线程池,线程池中线程的数量会根据需要动态增加或减少 + ExecutorService executor = Executors.newCachedThreadPool(); + + // 第一批任务 - 快速提交 + System.out.println("提交第一批任务..."); + for (int i = 1; i <= 5; i++) { + final int taskId = i; + // 提交任务到线程池,任务是一个Lambda表达式,表示一个Runnable对象 + executor.submit(() -> { + System.out.println("任务" + taskId + "开始执行,线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println("任务" + taskId + "执行完成"); + }); + } + + try { + Thread.sleep(3000); // 等待第一批任务完成 + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 第二批任务 - 延迟提交,观察线程复用 + System.out.println("\n提交第二批任务..."); + for (int i = 6; i <= 10; i++) { + final int taskId = i; + executor.submit(() -> { + System.out.println("任务" + taskId + "开始执行,线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println("任务" + taskId + "执行完成"); + }); + } + + // 关闭线程池 + executor.shutdown(); + try { + // 等待所有任务完成,最多等待10秒 + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + // 如果等待超时,则强制关闭线程池 + executor.shutdownNow(); + } + } catch (InterruptedException e) { + // 如果等待过程中被中断,则强制关闭线程池 + executor.shutdownNow(); + } + } + + // 突发任务处理示例 + public static void burstTaskHandling() { + // 创建一个缓存线程池,线程池中线程的数量会根据需要动态增加或减少 + ExecutorService executor = Executors.newCachedThreadPool(); + + // 模拟突发的大量短任务 + System.out.println("模拟突发任务处理..."); + for (int i = 1; i <= 20; i++) { + final int taskId = i; + // 提交任务到线程池,任务是一个Lambda表达式,表示一个Runnable对象 + executor.submit(() -> { + System.out.println("突发任务" + taskId + ",线程: " + Thread.currentThread().getName()); + try { + Thread.sleep(500); // 短任务 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // 快速提交任务 + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // 关闭线程池 + executor.shutdown(); + try { + // 等待所有任务完成,最多等待10秒 + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + // 如果等待超时,则强制关闭线程池 + executor.shutdownNow(); + } + } catch (InterruptedException e) { + // 如果等待过程中被中断,则强制关闭线程池 + executor.shutdownNow(); + } + } + + public static void main(String[] args) { + System.out.println("=== 动态线程创建 ==="); + dynamicThreadCreation(); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("\n=== 突发任务处理 ==="); + burstTaskHandling(); + } +} + +=== 动态线程创建 === +提交第一批任务... +任务1开始执行,线程: pool-1-thread-1 +任务2开始执行,线程: pool-1-thread-2 +任务4开始执行,线程: pool-1-thread-4 +任务5开始执行,线程: pool-1-thread-5 +任务3开始执行,线程: pool-1-thread-3 +任务5执行完成 +任务4执行完成 +任务3执行完成 +任务2执行完成 +任务1执行完成 + +提交第二批任务... +任务7开始执行,线程: pool-1-thread-2 +任务9开始执行,线程: pool-1-thread-5 +任务10开始执行,线程: pool-1-thread-4 +任务8开始执行,线程: pool-1-thread-3 +任务6开始执行,线程: pool-1-thread-1 +任务7执行完成 +任务6执行完成 +任务8执行完成 +任务9执行完成 +任务10执行完成 + +=== 突发任务处理 === +模拟突发任务处理... +突发任务1,线程: pool-2-thread-1 +突发任务2,线程: pool-2-thread-2 +突发任务3,线程: pool-2-thread-3 +突发任务4,线程: pool-2-thread-4 +突发任务5,线程: pool-2-thread-5 +突发任务6,线程: pool-2-thread-6 +突发任务7,线程: pool-2-thread-7 +突发任务8,线程: pool-2-thread-8 +突发任务9,线程: pool-2-thread-9 +突发任务10,线程: pool-2-thread-2 +突发任务11,线程: pool-2-thread-1 +突发任务12,线程: pool-2-thread-3 +突发任务13,线程: pool-2-thread-4 +突发任务14,线程: pool-2-thread-5 +突发任务15,线程: pool-2-thread-6 +突发任务16,线程: pool-2-thread-7 +突发任务17,线程: pool-2-thread-8 +突发任务18,线程: pool-2-thread-9 +突发任务19,线程: pool-2-thread-2 +突发任务20,线程: pool-2-thread-1 + + +public class Main7 { + // 共享变量 + private static int sharedVariable = 0; + // 标志位 + private static boolean flag = false; + + // 展示内存模型 + public static void demonstrateMemoryModel() { + // 线程1:写入数据 + Thread writer = new Thread(() -> { + System.out.println("Writer: 开始写入数据"); + sharedVariable = 42; + flag = true; + System.out.println("Writer: 数据写入完成"); + }, "Writer-Thread"); + + // 线程2:读取数据 + Thread reader = new Thread(() -> { + System.out.println("Reader: 开始读取数据"); + while (!flag) { + // 等待flag变为true + Thread.yield(); + } + System.out.println("Reader: 读取到的值 = " + sharedVariable); + }, "Reader-Thread"); + + reader.start(); + try { + Thread.sleep(100); // 确保reader先启动 + } catch (InterruptedException e) { + e.printStackTrace(); + } + writer.start(); + + try { + writer.join(); + reader.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + demonstrateMemoryModel(); + } +} + +Reader: 开始读取数据 +Writer: 开始写入数据 +Writer: 数据写入完成 +Reader: 读取到的值 = 42 + + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +public class Main9 { + // 实践1:缩小锁的范围 + public static class OptimizedCounter { + private final Object lock = new Object(); + private int count = 0; + + // 不好的做法:锁的范围太大 + public void badIncrement() { + synchronized (lock) { + // 耗时的非关键操作 + doSomeExpensiveWork(); + count++; + // 更多非关键操作 + doMoreWork(); + } + } + + // 好的做法:只锁关键部分 + public void goodIncrement() { + // 非关键操作在锁外执行 + doSomeExpensiveWork(); + + synchronized (lock) { + count++; // 只锁必要的操作 + } + + doMoreWork(); + } + + public int getCount() { + synchronized (lock) { + return count; + } + } + + private void doSomeExpensiveWork() { + // 模拟耗时操作 + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void doMoreWork() { + // 模拟其他工作 + } + } + + // 实践2:避免锁嵌套,防止死锁 + public static class DeadlockAvoidance { + private final Object lock1 = new Object(); + private final Object lock2 = new Object(); + + // 危险:可能导致死锁 + public void dangerousMethod1() { + synchronized (lock1) { + System.out.println("获得lock1"); + synchronized (lock2) { + System.out.println("获得lock2"); + // 执行操作 + } + } + } + + public void dangerousMethod2() { + synchronized (lock2) { + System.out.println("获得lock2"); + synchronized (lock1) { + System.out.println("获得lock1"); + // 执行操作 + } + } + } + + // 安全:使用固定的锁顺序 + private final Object firstLock = lock1; + private final Object secondLock = lock2; + + public void safeMethod1() { + synchronized (firstLock) { + synchronized (secondLock) { + // 执行操作 + System.out.println("安全方法1执行"); + } + } + } + + public void safeMethod2() { + synchronized (firstLock) { + synchronized (secondLock) { + // 执行操作 + System.out.println("安全方法2执行"); + } + } + } + } + + // 实践3:使用tryLock避免无限等待 + public static class TimeoutLocking { + private final ReentrantLock lock = new ReentrantLock(); + private final List data = new ArrayList<>(); + + public boolean addWithTimeout(String item, long timeout, TimeUnit unit) { + try { + if (lock.tryLock(timeout, unit)) { + try { + data.add(item); + System.out.println("成功添加: " + item); + return true; + } finally { + lock.unlock(); + } + } else { + System.out.println("获取锁超时,放弃添加: " + item); + return false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + public void simulateContention() { + // 模拟锁竞争 + Thread holder = new Thread(() -> { + lock.lock(); + try { + System.out.println("长时间持有锁..."); + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + System.out.println("释放锁"); + } + }); + + Thread waiter = new Thread(() -> { + try { + Thread.sleep(100); // 确保holder先获得锁 + addWithTimeout("测试项", 1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + holder.start(); + waiter.start(); + + try { + holder.join(); + waiter.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public static void main(String[] args) { + // 打印锁范围优化演示 + System.out.println("=== 锁范围优化演示 ==="); + // 创建一个OptimizedCounter对象 + OptimizedCounter counter = new OptimizedCounter(); + + // 获取当前时间 + long start = System.currentTimeMillis(); + // 创建一个包含5个线程的数组 + Thread[] threads = new Thread[5]; + // 循环创建5个线程 + for (int i = 0; i < threads.length; i++) { + // 创建一个线程,并执行goodIncrement方法 + threads[i] = new Thread(() -> { + for (int j = 0; j < 3; j++) { + counter.goodIncrement(); + } + }); + // 启动线程 + threads[i].start(); + } + + // 循环等待所有线程执行完毕 + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // 获取当前时间 + long end = System.currentTimeMillis(); + // 打印最终计数和耗时 + System.out.println("最终计数: " + counter.getCount() + ", 耗时: " + (end - start) + "ms"); + + // 打印死锁避免演示 + System.out.println("\n=== 死锁避免演示 ==="); + // 创建一个DeadlockAvoidance对象 + DeadlockAvoidance avoidance = new DeadlockAvoidance(); + // 调用safeMethod1和safeMethod2方法 + avoidance.safeMethod1(); + avoidance.safeMethod2(); + + // 打印超时锁演示 + System.out.println("\n=== 超时锁演示 ==="); + // 创建一个TimeoutLocking对象 + TimeoutLocking timeoutLocking = new TimeoutLocking(); + // 调用simulateContention方法 + timeoutLocking.simulateContention(); + } +} + +=== 锁范围优化演示 === +最终计数: 15, 耗时: 74ms + +=== 死锁避免演示 === +安全方法1执行 +安全方法2执行 + +=== 超时锁演示 === +长时间持有锁... +获取锁超时,放弃添加: 测试项 +释放锁 + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Main10 { + // 实践1:合理配置线程池参数 + public static class ThreadPoolFactory { + + // CPU密集型任务的线程池 + public static ThreadPoolExecutor createCpuIntensivePool() { + // 核心线程数设置为CPU核心数 + int corePoolSize = Runtime.getRuntime().availableProcessors(); + // 最大线程数设置为CPU核心数 + int maximumPoolSize = corePoolSize; + // 线程空闲时间设置为0 + long keepAliveTime = 0L; + + // 创建线程池 + return new ThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(100), + new CustomThreadFactory("CPU-Worker"), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // IO密集型任务的线程池 + public static ThreadPoolExecutor createIoIntensivePool() { + // 核心线程数设置为CPU核心数的两倍 + int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; + // 最大线程数设置为CPU核心数的四倍 + int maximumPoolSize = corePoolSize * 2; + // 线程空闲时间设置为60秒 + long keepAliveTime = 60L; + + // 创建线程池 + return new ThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(200), + new CustomThreadFactory("IO-Worker"), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + // 自定义线程工厂 + static class CustomThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + CustomThreadFactory(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + // 创建线程 + Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + t.setPriority(Thread.NORM_PRIORITY); + + // 设置异常处理器 + t.setUncaughtExceptionHandler((thread, ex) -> { + System.err.println("线程 " + thread.getName() + " 发生异常: " + ex.getMessage()); + ex.printStackTrace(); + }); + + return t; + } + } + } + + // 实践2:正确处理任务异常 + public static class TaskExceptionHandling { + + public static void demonstrateExceptionHandling() { + // 创建线程池 + ThreadPoolExecutor executor = ThreadPoolFactory.createCpuIntensivePool(); + + // 方法1:使用Future捕获异常 + Future future = executor.submit(() -> { + // 模拟任务异常 + if (Math.random() > 0.5) { + throw new RuntimeException("模拟任务异常"); + } + System.out.println("任务正常完成"); + }); + + try { + // 获取任务结果 + future.get(5, TimeUnit.SECONDS); + System.out.println("任务成功完成"); + } catch (ExecutionException e) { + // 任务执行异常 + System.err.println("任务执行异常: " + e.getCause().getMessage()); + } catch (TimeoutException e) { + // 任务执行超时 + System.err.println("任务执行超时"); + future.cancel(true); + } catch (InterruptedException e) { + // 中断线程 + Thread.currentThread().interrupt(); + } + + // 方法2:在任务内部处理异常 + executor.submit(() -> { + try { + // 可能抛出异常的代码 + if (Math.random() > 0.5) { + throw new RuntimeException("内部异常"); + } + System.out.println("内部异常处理:任务正常完成"); + } catch (Exception e) { + // 捕获到异常 + System.err.println("内部异常处理:捕获到异常 - " + e.getMessage()); + // 记录日志、发送告警等 + } + }); + + // 优雅关闭 + executor.shutdown(); + try { + // 等待线程池关闭 + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + // 实践3:监控线程池状态 + public static class ThreadPoolMonitoring { + + public static void monitorThreadPool() { + // 创建线程池 + ThreadPoolExecutor executor = ThreadPoolFactory.createIoIntensivePool(); + + // 启动监控线程 + ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); + monitor.scheduleAtFixedRate(() -> { + System.out.println("=== 线程池状态 ==="); + System.out.println("核心线程数: " + executor.getCorePoolSize()); + System.out.println("当前线程数: " + executor.getPoolSize()); + System.out.println("活跃线程数: " + executor.getActiveCount()); + System.out.println("队列大小: " + executor.getQueue().size()); + System.out.println("已完成任务数: " + executor.getCompletedTaskCount()); + System.out.println("总任务数: " + executor.getTaskCount()); + System.out.println(); + }, 0, 1, TimeUnit.SECONDS); + + // 提交一些任务 + for (int i = 0; i < 20; i++) { + final int taskId = i; + executor.submit(() -> { + try { + System.out.println("执行任务 " + taskId); + Thread.sleep(2000); // 模拟IO操作 + System.out.println("完成任务 " + taskId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + // 运行5秒后关闭 + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + monitor.shutdown(); + executor.shutdown(); + + try { + // 等待线程池关闭 + executor.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public static void main(String[] args) { + System.out.println("=== 异常处理演示 ==="); + TaskExceptionHandling.demonstrateExceptionHandling(); + + System.out.println("\n=== 线程池监控演示 ==="); + ThreadPoolMonitoring.monitorThreadPool(); + } +} + +=== 异常处理演示 === +任务执行异常: 模拟任务异常 +内部异常处理:任务正常完成 + +=== 线程池监控演示 === +=== 线程池状态 === +核心线程数: 64 +当前线程数: 0 +活跃线程数: 0 +队列大小: 0 +已完成任务数: 0 +总任务数: 0 + +执行任务 0 +执行任务 1 +执行任务 4 +执行任务 3 +执行任务 6 +执行任务 7 +执行任务 2 +执行任务 5 +执行任务 8 +执行任务 9 +执行任务 10 +执行任务 11 +执行任务 12 +执行任务 13 +执行任务 19 +执行任务 15 +执行任务 16 +执行任务 17 +执行任务 18 +执行任务 14 +=== 线程池状态 === +核心线程数: 64 +当前线程数: 20 +活跃线程数: 20 +队列大小: 0 +已完成任务数: 0 +总任务数: 20 + +完成任务 6 +完成任务 11 +完成任务 4 +完成任务 8 +完成任务 16 +完成任务 12 +完成任务 3 +完成任务 13 +完成任务 7 +完成任务 0 +完成任务 10 +完成任务 1 +完成任务 2 +完成任务 5 +=== 线程池状态 === +核心线程数: 64 +当前线程数: 20 +活跃线程数: 6 +队列大小: 0 +已完成任务数: 14 +完成任务 15 +完成任务 9 +完成任务 19 +总任务数: 20 + +完成任务 17 +完成任务 18 +完成任务 14 +=== 线程池状态 === +核心线程数: 64 +当前线程数: 20 +活跃线程数: 0 +队列大小: 0 +已完成任务数: 20 +总任务数: 20 + +=== 线程池状态 === +核心线程数: 64 +当前线程数: 20 +活跃线程数: 0 +队列大小: 0 +已完成任务数: 20 +总任务数: 20 + + +``` diff --git a/docs/aThread/linux.md b/docs/aThread/linux.md new file mode 100644 index 000000000..2da94e384 --- /dev/null +++ b/docs/aThread/linux.md @@ -0,0 +1,601 @@ +--- +title: linux线程基础 +author: 哪吒 +date: '2023-06-15' +--- + +# Linux线程基础 + +## 1. 线程概念与特性 + +### 1.1 什么是线程 + +线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。 + +```c +#include +#include +#include +#include + +// 线程函数示例 +void* thread_function(void* arg) { + int thread_id = *(int*)arg; + printf("线程 %d 正在运行\n", thread_id); + + // 模拟工作 + for(int i = 0; i < 5; i++) { + printf("线程 %d: 工作步骤 %d\n", thread_id, i + 1); + sleep(1); + } + + printf("线程 %d 完成工作\n", thread_id); + return NULL; +} +``` + +### 1.2 线程与进程的区别 + +| 特性 | 进程 | 线程 | +|------|------|------| +| 资源占用 | 独立的内存空间 | 共享进程内存空间 | +| 创建开销 | 较大 | 较小 | +| 通信方式 | IPC(管道、消息队列等) | 共享内存、信号量 | +| 切换开销 | 较大 | 较小 | +| 独立性 | 高度独立 | 相互依赖 | + +### 1.3 线程的优势 + +- **并发执行**:多个线程可以同时执行不同的任务 +- **资源共享**:线程间可以方便地共享数据 +- **响应性**:提高程序的响应速度 +- **经济性**:创建和切换开销小 + +## 2. POSIX线程(pthread) + +### 2.1 pthread库简介 + +POSIX线程(pthread)是Linux系统中标准的线程API,提供了创建、管理和同步线程的函数。 + +```c +// 编译时需要链接pthread库 +// gcc -o program program.c -lpthread +``` + +### 2.2 线程创建与管理 + +#### 创建线程 + +```c +#include +#include +#include + +int main() { + pthread_t thread1, thread2; + int thread1_id = 1, thread2_id = 2; + int result; + + // 创建线程1 + result = pthread_create(&thread1, NULL, thread_function, &thread1_id); + if (result != 0) { + printf("创建线程1失败\n"); + exit(1); + } + + // 创建线程2 + result = pthread_create(&thread2, NULL, thread_function, &thread2_id); + if (result != 0) { + printf("创建线程2失败\n"); + exit(1); + } + + printf("主线程:已创建两个工作线程\n"); + + // 等待线程完成 + pthread_join(thread1, NULL); + pthread_join(thread2, NULL); + + printf("主线程:所有线程已完成\n"); + return 0; +} +``` + +#### 线程属性设置 + +```c +#include + +void create_detached_thread() { + pthread_t thread; + pthread_attr_t attr; + + // 初始化线程属性 + pthread_attr_init(&attr); + + // 设置为分离状态 + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + // 设置栈大小 + size_t stack_size = 1024 * 1024; // 1MB + pthread_attr_setstacksize(&attr, stack_size); + + // 创建线程 + pthread_create(&thread, &attr, thread_function, NULL); + + // 销毁属性对象 + pthread_attr_destroy(&attr); +} +``` + +## 3. 线程同步机制 + +### 3.1 互斥锁(Mutex) + +互斥锁用于保护共享资源,确保同一时间只有一个线程可以访问临界区。 + +```c +#include +#include +#include + +// 全局变量和互斥锁 +int shared_counter = 0; +pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; + +void* increment_counter(void* arg) { + int thread_id = *(int*)arg; + + for(int i = 0; i < 100000; i++) { + // 加锁 + pthread_mutex_lock(&counter_mutex); + + // 临界区:修改共享变量 + shared_counter++; + + // 解锁 + pthread_mutex_unlock(&counter_mutex); + } + + printf("线程 %d 完成计数\n", thread_id); + return NULL; +} + +int main() { + pthread_t threads[3]; + int thread_ids[3] = {1, 2, 3}; + + // 创建多个线程 + for(int i = 0; i < 3; i++) { + pthread_create(&threads[i], NULL, increment_counter, &thread_ids[i]); + } + + // 等待所有线程完成 + for(int i = 0; i < 3; i++) { + pthread_join(threads[i], NULL); + } + + printf("最终计数值: %d\n", shared_counter); + + // 销毁互斥锁 + pthread_mutex_destroy(&counter_mutex); + return 0; +} +``` + +### 3.2 条件变量(Condition Variable) + +条件变量用于线程间的条件同步,允许线程等待特定条件的发生。 + +```c +#include +#include +#include +#include + +// 生产者-消费者模型 +#define BUFFER_SIZE 10 + +int buffer[BUFFER_SIZE]; +int buffer_count = 0; +int in = 0, out = 0; + +pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_cond_t buffer_not_full = PTHREAD_COND_INITIALIZER; +pthread_cond_t buffer_not_empty = PTHREAD_COND_INITIALIZER; + +// 生产者线程 +void* producer(void* arg) { + int producer_id = *(int*)arg; + + for(int i = 0; i < 20; i++) { + pthread_mutex_lock(&buffer_mutex); + + // 等待缓冲区不满 + while(buffer_count == BUFFER_SIZE) { + printf("生产者 %d: 缓冲区满,等待...\n", producer_id); + pthread_cond_wait(&buffer_not_full, &buffer_mutex); + } + + // 生产数据 + int item = i + producer_id * 100; + buffer[in] = item; + in = (in + 1) % BUFFER_SIZE; + buffer_count++; + + printf("生产者 %d: 生产了 %d,缓冲区数量: %d\n", + producer_id, item, buffer_count); + + // 通知消费者 + pthread_cond_signal(&buffer_not_empty); + pthread_mutex_unlock(&buffer_mutex); + + usleep(100000); // 0.1秒 + } + + return NULL; +} + +// 消费者线程 +void* consumer(void* arg) { + int consumer_id = *(int*)arg; + + for(int i = 0; i < 20; i++) { + pthread_mutex_lock(&buffer_mutex); + + // 等待缓冲区不空 + while(buffer_count == 0) { + printf("消费者 %d: 缓冲区空,等待...\n", consumer_id); + pthread_cond_wait(&buffer_not_empty, &buffer_mutex); + } + + // 消费数据 + int item = buffer[out]; + out = (out + 1) % BUFFER_SIZE; + buffer_count--; + + printf("消费者 %d: 消费了 %d,缓冲区数量: %d\n", + consumer_id, item, buffer_count); + + // 通知生产者 + pthread_cond_signal(&buffer_not_full); + pthread_mutex_unlock(&buffer_mutex); + + usleep(150000); // 0.15秒 + } + + return NULL; +} +``` + +### 3.3 读写锁(Read-Write Lock) + +读写锁允许多个读者同时访问,但写者独占访问。 + +```c +#include +#include +#include + +// 共享数据和读写锁 +int shared_data = 0; +pthread_rwlock_t data_rwlock = PTHREAD_RWLOCK_INITIALIZER; + +// 读者线程 +void* reader(void* arg) { + int reader_id = *(int*)arg; + + for(int i = 0; i < 5; i++) { + // 获取读锁 + pthread_rwlock_rdlock(&data_rwlock); + + printf("读者 %d: 读取数据 = %d\n", reader_id, shared_data); + sleep(1); + + // 释放读锁 + pthread_rwlock_unlock(&data_rwlock); + + sleep(1); + } + + return NULL; +} + +// 写者线程 +void* writer(void* arg) { + int writer_id = *(int*)arg; + + for(int i = 0; i < 3; i++) { + // 获取写锁 + pthread_rwlock_wrlock(&data_rwlock); + + shared_data += 10; + printf("写者 %d: 写入数据 = %d\n", writer_id, shared_data); + sleep(2); + + // 释放写锁 + pthread_rwlock_unlock(&data_rwlock); + + sleep(2); + } + + return NULL; +} +``` + +## 4. 线程池实现 + +### 4.1 简单线程池设计 + +```c +#include +#include +#include +#include + +#define MAX_THREADS 4 +#define MAX_QUEUE 100 + +// 任务结构 +typedef struct { + void (*function)(void* arg); + void* argument; +} task_t; + +// 线程池结构 +typedef struct { + pthread_t threads[MAX_THREADS]; + task_t task_queue[MAX_QUEUE]; + int queue_front; + int queue_rear; + int queue_size; + int shutdown; + + pthread_mutex_t queue_mutex; + pthread_cond_t queue_not_empty; + pthread_cond_t queue_not_full; +} thread_pool_t; + +thread_pool_t pool; + +// 工作线程函数 +void* worker_thread(void* arg) { + while(1) { + pthread_mutex_lock(&pool.queue_mutex); + + // 等待任务 + while(pool.queue_size == 0 && !pool.shutdown) { + pthread_cond_wait(&pool.queue_not_empty, &pool.queue_mutex); + } + + // 检查是否需要关闭 + if(pool.shutdown) { + pthread_mutex_unlock(&pool.queue_mutex); + break; + } + + // 取出任务 + task_t task = pool.task_queue[pool.queue_front]; + pool.queue_front = (pool.queue_front + 1) % MAX_QUEUE; + pool.queue_size--; + + // 通知可以添加新任务 + pthread_cond_signal(&pool.queue_not_full); + pthread_mutex_unlock(&pool.queue_mutex); + + // 执行任务 + task.function(task.argument); + } + + return NULL; +} + +// 初始化线程池 +void thread_pool_init() { + pool.queue_front = 0; + pool.queue_rear = 0; + pool.queue_size = 0; + pool.shutdown = 0; + + pthread_mutex_init(&pool.queue_mutex, NULL); + pthread_cond_init(&pool.queue_not_empty, NULL); + pthread_cond_init(&pool.queue_not_full, NULL); + + // 创建工作线程 + for(int i = 0; i < MAX_THREADS; i++) { + pthread_create(&pool.threads[i], NULL, worker_thread, NULL); + } + + printf("线程池初始化完成,创建了 %d 个工作线程\n", MAX_THREADS); +} + +// 添加任务到线程池 +void thread_pool_add_task(void (*function)(void*), void* argument) { + pthread_mutex_lock(&pool.queue_mutex); + + // 等待队列不满 + while(pool.queue_size == MAX_QUEUE) { + pthread_cond_wait(&pool.queue_not_full, &pool.queue_mutex); + } + + // 添加任务 + pool.task_queue[pool.queue_rear].function = function; + pool.task_queue[pool.queue_rear].argument = argument; + pool.queue_rear = (pool.queue_rear + 1) % MAX_QUEUE; + pool.queue_size++; + + // 通知工作线程 + pthread_cond_signal(&pool.queue_not_empty); + pthread_mutex_unlock(&pool.queue_mutex); +} + +// 示例任务函数 +void example_task(void* arg) { + int task_id = *(int*)arg; + printf("执行任务 %d,线程ID: %lu\n", task_id, pthread_self()); + sleep(2); // 模拟工作 + printf("任务 %d 完成\n", task_id); +} +``` + +## 5. 线程安全与最佳实践 + +### 5.1 线程安全的概念 + +线程安全是指在多线程环境下,程序能够正确地处理多个线程同时访问共享资源的情况。 + +### 5.2 常见的线程安全问题 + +#### 竞态条件(Race Condition) + +```c +// 不安全的代码示例 +int global_counter = 0; + +void* unsafe_increment(void* arg) { + for(int i = 0; i < 100000; i++) { + // 这里存在竞态条件 + global_counter++; // 非原子操作 + } + return NULL; +} + +// 安全的代码示例 +pthread_mutex_t safe_mutex = PTHREAD_MUTEX_INITIALIZER; +int safe_counter = 0; + +void* safe_increment(void* arg) { + for(int i = 0; i < 100000; i++) { + pthread_mutex_lock(&safe_mutex); + safe_counter++; // 受保护的操作 + pthread_mutex_unlock(&safe_mutex); + } + return NULL; +} +``` + +#### 死锁(Deadlock) + +```c +// 可能导致死锁的代码 +pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; + +void* thread1_func(void* arg) { + pthread_mutex_lock(&mutex1); + printf("线程1获得mutex1\n"); + sleep(1); + + pthread_mutex_lock(&mutex2); // 可能死锁 + printf("线程1获得mutex2\n"); + + pthread_mutex_unlock(&mutex2); + pthread_mutex_unlock(&mutex1); + return NULL; +} + +void* thread2_func(void* arg) { + pthread_mutex_lock(&mutex2); + printf("线程2获得mutex2\n"); + sleep(1); + + pthread_mutex_lock(&mutex1); // 可能死锁 + printf("线程2获得mutex1\n"); + + pthread_mutex_unlock(&mutex1); + pthread_mutex_unlock(&mutex2); + return NULL; +} + +// 避免死锁的方法:统一加锁顺序 +void* safe_thread1_func(void* arg) { + pthread_mutex_lock(&mutex1); // 总是先锁mutex1 + pthread_mutex_lock(&mutex2); // 再锁mutex2 + + // 执行工作 + + pthread_mutex_unlock(&mutex2); + pthread_mutex_unlock(&mutex1); + return NULL; +} +``` + +### 5.3 最佳实践 + +1. **最小化锁的范围**:只在必要时加锁,尽快释放 +2. **避免嵌套锁**:减少死锁的可能性 +3. **使用RAII模式**:确保资源正确释放 +4. **优先使用高级同步原语**:如条件变量、读写锁 +5. **线程局部存储**:减少共享数据 + +```c +// 线程局部存储示例 +__thread int thread_local_var = 0; + +void* thread_function(void* arg) { + int thread_id = *(int*)arg; + + // 每个线程都有自己的副本 + thread_local_var = thread_id * 100; + + printf("线程 %d 的局部变量: %d\n", thread_id, thread_local_var); + return NULL; +} +``` + +## 6. 性能优化与调试 + +### 6.1 性能监控 + +```c +#include + +// 性能计时器 +double get_time() { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec + tv.tv_usec / 1000000.0; +} + +void* performance_test_thread(void* arg) { + double start_time = get_time(); + + // 执行工作 + for(int i = 0; i < 1000000; i++) { + // 模拟计算 + } + + double end_time = get_time(); + printf("线程执行时间: %.6f 秒\n", end_time - start_time); + + return NULL; +} +``` + +### 6.2 调试技巧 + +```bash +# 使用gdb调试多线程程序 +gdb ./program +(gdb) set scheduler-locking on # 锁定调度器 +(gdb) info threads # 查看所有线程 +(gdb) thread 2 # 切换到线程2 +(gdb) bt # 查看调用栈 + +# 使用valgrind检测线程问题 +valgrind --tool=helgrind ./program +valgrind --tool=drd ./program +``` + +## 7. 总结 + +Linux线程编程是系统编程的重要组成部分,掌握以下要点: + +- **基础概念**:理解线程与进程的区别 +- **POSIX线程**:熟练使用pthread API +- **同步机制**:正确使用互斥锁、条件变量、读写锁 +- **线程安全**:避免竞态条件和死锁 +- **性能优化**:合理设计线程架构 +- **调试技巧**:使用专业工具定位问题 + +通过实践这些概念和技术,可以编写出高效、安全的多线程程序。 diff --git a/docs/assets/wechat-payment-qrcode.svg b/docs/assets/wechat-payment-qrcode.svg new file mode 100644 index 000000000..23df0eea1 --- /dev/null +++ b/docs/assets/wechat-payment-qrcode.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + JavaPlus 技术文档平台 + 微信扫码支持 + + + + + + + + + + [微信收款码] + + + + + + + + 感谢您的支持! + 您的赞助将用于平台维护与内容更新 + + + + + + + + + + J + + + + R + + + + M + + + + D + + + + K + + + + V + + + + https://webvueblog.github.io/JavaPlusDoc/ + \ No newline at end of file diff --git a/docs/basic-grammar/java-jdk-jre-jvm.md b/docs/basic-grammar/java-jdk-jre-jvm.md new file mode 100644 index 000000000..b0e7f8c43 --- /dev/null +++ b/docs/basic-grammar/java-jdk-jre-jvm.md @@ -0,0 +1,429 @@ +> 💫 甜心,保持规律的作息,这样学习效果会更好呢~ + +--- +title: Java、JDK、JRE、JVM详解与演示 +author: 哪吒 +date: '2024-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Java、JDK、JRE、JVM详解与演示 + +## 概述 + +在Java开发中,经常会遇到Java、JDK、JRE、JVM这几个概念,它们之间既有联系又有区别。本文将详细讲解这些概念,并通过图表和代码演示帮助理解。 + +## 1. 基本概念 + +### 1.1 Java + +**Java** 是一种面向对象的编程语言,具有以下特点: +- **跨平台性**:"一次编写,到处运行"(Write Once, Run Anywhere) +- **面向对象**:支持封装、继承、多态 +- **安全性**:内置安全机制 +- **多线程**:支持并发编程 +- **自动内存管理**:垃圾回收机制 + +### 1.2 JVM(Java Virtual Machine) + +**JVM** 是Java虚拟机,是Java程序运行的核心环境: + +``` +┌─────────────────────────────────────┐ +│ JVM 架构 │ +├─────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ 类加载器 │ │ 执行引擎 │ │ +│ │ ClassLoader │ │Execution Eng│ │ +│ └─────────────┘ └─────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ 运行时数据区域 │ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ +│ │ │方法区│ │ 堆 │ │虚拟机│ │本地方│ │ │ +│ │ │ │ │ │ │ 栈 │ │法栈 │ │ │ +│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ 本地方法接口 │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**JVM的主要功能:** +- 加载和执行字节码文件 +- 内存管理和垃圾回收 +- 提供运行时环境 +- 实现跨平台特性 + +### 1.3 JRE(Java Runtime Environment) + +**JRE** 是Java运行时环境,包含运行Java程序所需的所有组件: + +``` +┌─────────────────────────────────────┐ +│ JRE │ +├─────────────────────────────────────┤ +│ ┌─────────────────────────────────┐ │ +│ │ JVM │ │ +│ │ ┌─────────┐ ┌─────────────────┐│ │ +│ │ │类加载器 │ │ 执行引擎 ││ │ +│ │ └─────────┘ └─────────────────┘│ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ 运行时数据区域 │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ Java类库 │ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │ +│ │ │java.│ │java.│ │java.│ │java.││ │ +│ │ │lang │ │util │ │io │ │net ││ │ +│ │ └─────┘ └─────┘ └─────┘ └─────┘│ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**JRE包含:** +- JVM(Java虚拟机) +- Java核心类库 +- 支持文件 + +### 1.4 JDK(Java Development Kit) + +**JDK** 是Java开发工具包,包含开发Java程序所需的所有工具: + +``` +┌─────────────────────────────────────┐ +│ JDK │ +├─────────────────────────────────────┤ +│ ┌─────────────────────────────────┐ │ +│ │ JRE │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ JVM │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ Java类库 │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ 开发工具 │ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │ +│ │ │javac│ │java │ │javap│ │jdb ││ │ +│ │ │编译器│ │解释器│ │反编译│ │调试器││ │ +│ │ └─────┘ └─────┘ └─────┘ └─────┘│ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ │ +│ │ │jar │ │javah│ │jstat│ │jmap ││ │ +│ │ │打包 │ │头文件│ │监控 │ │内存 ││ │ +│ │ └─────┘ └─────┘ └─────┘ └─────┘│ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**JDK包含:** +- JRE(Java运行时环境) +- 编译器(javac) +- 调试器(jdb) +- 文档生成器(javadoc) +- 打包工具(jar) +- 其他开发工具 + +## 2. 关系图解 + +### 2.1 包含关系 + +``` +┌─────────────────────────────────────────────────────┐ +│ JDK │ +│ ┌─────────────────────────────────────────────────┐│ +│ │ JRE ││ +│ │ ┌─────────────────────────────────────────────┐││ +│ │ │ JVM │││ +│ │ │ │││ +│ │ │ Java程序在这里运行 │││ +│ │ │ │││ +│ │ └─────────────────────────────────────────────┘││ +│ │ Java类库 + 支持文件 ││ +│ └─────────────────────────────────────────────────┘│ +│ 开发工具(javac, java, javadoc, jar等) │ +└─────────────────────────────────────────────────────┘ +``` + +### 2.2 工作流程 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Java源码 │───▶│ 字节码 │───▶│ 机器码 │ +│ (.java) │ │ (.class) │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ javac │ │ JVM │ │ 操作系统 │ +│ (JDK) │ │ (JRE) │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +## 3. 代码演示 + +### 3.1 Java程序示例 + +创建一个简单的Java程序来演示整个过程: + +```java +// HelloJava.java +public class HelloJava { + public static void main(String[] args) { + System.out.println("Hello, Java World!"); + + // 演示JVM信息 + System.out.println("Java版本: " + System.getProperty("java.version")); + System.out.println("JVM名称: " + System.getProperty("java.vm.name")); + System.out.println("JVM版本: " + System.getProperty("java.vm.version")); + System.out.println("操作系统: " + System.getProperty("os.name")); + + // 演示内存信息 + Runtime runtime = Runtime.getRuntime(); + long maxMemory = runtime.maxMemory(); + long totalMemory = runtime.totalMemory(); + long freeMemory = runtime.freeMemory(); + + System.out.println("最大内存: " + maxMemory / 1024 / 1024 + " MB"); + System.out.println("总内存: " + totalMemory / 1024 / 1024 + " MB"); + System.out.println("空闲内存: " + freeMemory / 1024 / 1024 + " MB"); + } +} +``` + +### 3.2 编译和运行过程 + +```bash +# 1. 使用JDK中的javac编译Java源码 +javac HelloJava.java + +# 2. 生成字节码文件HelloJava.class +# 可以使用javap查看字节码 +javap -c HelloJava + +# 3. 使用JRE中的java命令运行程序 +java HelloJava +``` + +### 3.3 JVM内存演示 + +```java +public class JVMMemoryDemo { + public static void main(String[] args) { + // 演示堆内存 + System.out.println("=== 堆内存演示 ==="); + + // 创建对象(存储在堆中) + String str1 = new String("Hello"); + String str2 = new String("World"); + + System.out.println("创建对象后:"); + printMemoryInfo(); + + // 演示垃圾回收 + str1 = null; + str2 = null; + System.gc(); // 建议进行垃圾回收 + + System.out.println("垃圾回收后:"); + printMemoryInfo(); + + // 演示方法区(元空间) + System.out.println("\n=== 方法区演示 ==="); + Class clazz = JVMMemoryDemo.class; + System.out.println("类名: " + clazz.getName()); + System.out.println("类加载器: " + clazz.getClassLoader()); + + // 演示栈内存 + System.out.println("\n=== 栈内存演示 ==="); + recursiveMethod(5); + } + + private static void printMemoryInfo() { + Runtime runtime = Runtime.getRuntime(); + long total = runtime.totalMemory(); + long free = runtime.freeMemory(); + long used = total - free; + + System.out.println("总内存: " + total / 1024 / 1024 + " MB"); + System.out.println("已用内存: " + used / 1024 / 1024 + " MB"); + System.out.println("空闲内存: " + free / 1024 / 1024 + " MB"); + } + + private static void recursiveMethod(int depth) { + System.out.println("递归深度: " + depth + " (栈帧)"); + if (depth > 0) { + recursiveMethod(depth - 1); + } + } +} +``` + +## 4. JVM内存结构详解 + +### 4.1 内存区域划分 + +``` +┌─────────────────────────────────────────────────────┐ +│ JVM内存结构 │ +├─────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────────────────┐│ +│ │ 方法区 │ │ 堆内存 ││ +│ │ (Method Area) │ │ (Heap) ││ +│ │ │ │ ││ +│ │ • 类信息 │ │ ┌─────────────────────────┐ ││ +│ │ • 常量池 │ │ │ 新生代 (Young) │ ││ +│ │ • 静态变量 │ │ │ ┌─────┐ ┌─────┐ ┌─────┐│ ││ +│ │ │ │ │ │Eden │ │ S0 │ │ S1 ││ ││ +│ └─────────────────┘ │ │ └─────┘ └─────┘ └─────┘│ ││ +│ │ └─────────────────────────┘ ││ +│ ┌─────────────────┐ │ ┌─────────────────────────┐ ││ +│ │ 程序计数器 │ │ │ 老年代 (Old) │ ││ +│ │ (PC) │ │ │ │ ││ +│ └─────────────────┘ │ └─────────────────────────┘ ││ +│ └─────────────────────────────┘│ +│ ┌─────────────────┐ ┌─────────────────────────────┐│ +│ │ 虚拟机栈 │ │ 本地方法栈 ││ +│ │ (VM Stack) │ │ (Native Method Stack) ││ +│ │ │ │ ││ +│ │ • 局部变量表 │ │ • 本地方法调用 ││ +│ │ • 操作数栈 │ │ ││ +│ │ • 动态链接 │ │ ││ +│ │ • 方法出口 │ │ ││ +│ └─────────────────┘ └─────────────────────────────┘│ +└─────────────────────────────────────────────────────┘ +``` + +### 4.2 内存分配演示 + +```java +public class MemoryAllocationDemo { + // 静态变量 - 存储在方法区 + private static String staticVar = "存储在方法区"; + + // 实例变量 - 存储在堆中 + private String instanceVar = "存储在堆中"; + + public static void main(String[] args) { + // 局部变量 - 存储在栈中 + int localVar = 100; + + // 对象引用 - 存储在栈中,对象本身存储在堆中 + MemoryAllocationDemo demo = new MemoryAllocationDemo(); + + // 字符串常量 - 存储在字符串常量池(方法区) + String str1 = "Hello"; + String str2 = "Hello"; // 指向同一个常量池中的对象 + + // 新建字符串对象 - 存储在堆中 + String str3 = new String("Hello"); + + System.out.println("str1 == str2: " + (str1 == str2)); // true + System.out.println("str1 == str3: " + (str1 == str3)); // false + System.out.println("str1.equals(str3): " + str1.equals(str3)); // true + + // 调用方法 - 在栈中创建新的栈帧 + demo.methodCall(localVar); + } + + private void methodCall(int param) { + // 方法参数和局部变量都存储在当前栈帧中 + String localStr = "方法内局部变量"; + System.out.println("参数值: " + param); + System.out.println("局部变量: " + localStr); + System.out.println("实例变量: " + this.instanceVar); + System.out.println("静态变量: " + staticVar); + } +} +``` + +## 5. 垃圾回收演示 + +```java +public class GarbageCollectionDemo { + public static void main(String[] args) { + System.out.println("=== 垃圾回收演示 ==="); + + // 创建大量对象 + for (int i = 0; i < 100000; i++) { + String str = new String("对象" + i); + // 对象创建后立即失去引用,成为垃圾回收的候选 + } + + System.out.println("创建对象后:"); + printGCInfo(); + + // 手动触发垃圾回收 + System.gc(); + + // 等待垃圾回收完成 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("垃圾回收后:"); + printGCInfo(); + } + + private static void printGCInfo() { + Runtime runtime = Runtime.getRuntime(); + long total = runtime.totalMemory(); + long free = runtime.freeMemory(); + long used = total - free; + + System.out.println("总内存: " + formatMemory(total)); + System.out.println("已用内存: " + formatMemory(used)); + System.out.println("空闲内存: " + formatMemory(free)); + System.out.println("内存使用率: " + String.format("%.2f%%", (double) used / total * 100)); + System.out.println("─────────────────────────────"); + } + + private static String formatMemory(long bytes) { + return String.format("%.2f MB", bytes / 1024.0 / 1024.0); + } +} +``` + +## 6. 总结 + +### 6.1 关键区别 + +| 组件 | 用途 | 包含内容 | 使用场景 | +|------|------|----------|----------| +| **Java** | 编程语言 | 语法规范、API | 编写程序 | +| **JVM** | 运行环境 | 虚拟机、内存管理、垃圾回收 | 运行字节码 | +| **JRE** | 运行时环境 | JVM + 核心类库 | 运行Java程序 | +| **JDK** | 开发工具包 | JRE + 开发工具 | 开发Java程序 | + +### 6.2 实际应用 + +``` +开发阶段:需要JDK +┌─────────────────────────────────────┐ +│ 编写代码 → 编译 → 测试 → 调试 → 打包 │ +│ IDE javac java jdb jar │ +└─────────────────────────────────────┘ + +生产阶段:只需要JRE +┌─────────────────────────────────────┐ +│ 部署 → 运行 → 监控 │ +│ java jstat │ +└─────────────────────────────────────┘ +``` + +### 6.3 最佳实践 + +1. **开发环境**:安装完整的JDK +2. **生产环境**:可以只安装JRE以节省空间 +3. **版本管理**:确保开发和生产环境使用相同版本 +4. **内存调优**:根据应用需求调整JVM参数 +5. **监控工具**:使用JDK提供的工具监控应用性能 + +通过以上详细的讲解和演示,相信你已经对Java、JDK、JRE、JVM有了深入的理解。这些概念是Java开发的基础,掌握它们对于成为一名优秀的Java开发者至关重要。 \ No newline at end of file diff --git a/docs/basic/1.md b/docs/basic/1.md new file mode 100644 index 000000000..03d5de624 --- /dev/null +++ b/docs/basic/1.md @@ -0,0 +1,240 @@ +--- +title: 第1天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第1天 + +> 编程世界很精彩,但现实世界的你更重要 + +通过问答模式学会后端,继续看以下内容,过一遍,不行过几遍,直接实战。 + +## 1. 第一个 JAVA 程序 + +创建文件 HelloWorld.java(文件名需与类名一致) + +```java +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World"); + } +} +``` + +注:String args[] 与 String[] args 都可以执行,但推荐使用 String[] args,这样可以避免歧义和误读。 + +运行以上实例,输出结果如下: + +```java +$ javac HelloWorld.java +$ java HelloWorld +Hello World + +``` + +### 1.1 程序分析 + +- `public class HelloWorld`:定义一个公共类 HelloWorld。 +- `public static void main(String[] args)`:定义一个公共的静态方法 main,方法返回值为 void,方法参数为 String 数组 args。 +- `System.out.println("Hello World")`:调用 System.out.println 方法,输出 Hello World。 + +### 1.2 程序执行 + +1. 打开命令行终端。 +2. 导航到 HelloWorld.java 文件所在目录。 +3. 执行 `javac HelloWorld.java` 命令进行编译。 +4. 执行 `java HelloWorld` 命令运行程序。 + +### 1.3 程序输出 + +```java +Hello World +``` + +### 1.4 程序总结 + +- 程序通过 `public class HelloWorld` 定义了一个公共类。 +- 程序通过 `public static void main(String[] args)` 定义了一个公共的静态方法 main。 +- 程序通过 `System.out.println("Hello World")` 调用了 System.out.println 方法,输出了 Hello World。 + +## 执行命令解析 + +javac 后面跟着的是java文件的文件名,例如 HelloWorld.java。 该命令用于将 java 源文件编译为 class 字节码文件,如: javac HelloWorld.java。 + +运行javac命令后,如果成功编译没有错误的话,会出现一个 HelloWorld.class 的文件。 + +java 后面跟着的是java文件中的类名,例如 HelloWorld 就是类名,如: java HelloWorld。 + +注意:java命令后面不要加.class。 + +## Java 简介 + +Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 面向对象程序设计语言和 Java 平台的总称。由 James Gosling和同事们共同研发,并在 1995 年正式推出。 + +后来 Sun 公司被 Oracle (甲骨文)公司收购,Java 也随之成为 Oracle 公司的产品。 + +Java分为三个体系: + +- JavaSE(J2SE)(Java2 Platform Standard Edition,java平台标准版) +- JavaEE(J2EE)(Java 2 Platform,Enterprise Edition,java平台企业版) +- JavaME(J2ME)(Java 2 Platform Micro Edition,java平台微型版)。 + +2005 年 6 月,JavaOne 大会召开,SUN 公司公开 Java SE 6。此时,Java 的各种版本已经更名,以取消其中的数字 "2":J2EE 更名为 Java EE,J2SE 更名为Java SE,J2ME 更名为 Java ME。 + +## 发展历史 + +- 1995 年 5 月 23 日,Java 语言诞生 +- 1996 年 1 月,第一个 JDK-JDK1.0 诞生 +- 1996 年 4 月,10 个最主要的操作系统供应商申明将在其产品中嵌入 JAVA 技术 +- 1996 年 9 月,约 8.3 万个网页应用了 JAVA 技术来制作 +- 1997 年 2 月 18 日,JDK1.1 发布 +- 1997 年 4 月 2 日,JavaOne 会议召开,参与者逾一万人,创当时全球同类会议规模之纪录 +- 1997 年 9 月,JavaDeveloperConnection 社区成员超过十万 +- 1998 年 2 月,JDK1.1 被下载超过 2,000,000次 +- 1998 年 12 月 8 日,JAVA2 企业平台 J2EE 发布 +- 1999 年 6月,SUN 公司发布 Java 的三个版本:标准版(JavaSE, 以前是 J2SE)、企业版(JavaEE 以前是 J2EE)和微型版(JavaME,以前是 J2ME) +- 2000 年 5 月 8 日,JDK1.3 发布 +- 2000 年 5 月 29 日,JDK1.4 发布 +- 2001 年 6 月 5 日,NOKIA 宣布,到 2003 年将出售 1 亿部支持 Java 的手机 +- 2001 年 9 月 24 日,J2EE1.3 发布 +- 2002 年 2 月 26 日,J2SE1.4 发布,自此 Java 的计算能力有了大幅提升 +- 2004 年 9 月 30 日 18:00PM,J2SE1.5 发布,成为 Java 语言发展史上的又一里程碑。为了表示该版本的重要性,J2SE1.5 更名为 Java SE 5.0 +- 2005 年 6 月,JavaOne 大会召开,SUN 公司公开 Java SE 6。此时,Java 的各种版本已经更名,以取消其中的数字 "2":J2EE 更名为 Java EE,J2SE 更名为 Java SE,J2ME 更名为 Java ME +- 2006 年 12 月,SUN 公司发布 JRE6.0 +- 2009 年 04 月 20 日,甲骨文 74 亿美元收购 Sun,取得 Java 的版权。 +- 2010 年 11 月,由于甲骨文对于 Java 社区的不友善,因此 Apache 扬言将退出 JCP。 +- 2011 年 7 月 28 日,甲骨文发布 Java7.0 的正式版。 +- 2014 年 3 月 18 日,Oracle 公司发表 Java SE 8。 +- 2017 年 9 月 21 日,Oracle 公司发表 Java SE 9 +- 2018 年 3 月 21 日,Oracle 公司发表 Java SE 10 +- 2018 年 9 月 25 日,Java SE 11 发布 +- 2019 年 3 月 20 日,Java SE 12 发布 + +## 主要特性 + +1. Java 语言是简单的 +2. Java 语言是面向对象的 +3. Java 语言是分布式的 +4. Java 语言是健壮的 +5. Java 语言是安全的 +6. Java 语言是跨平台的 +7. Java 语言是解释型的 +8. Java 语言是动态的 +9. Java 语言是多线程的 +10. Java 语言是开源的 +11. Java 语言是免费的 +12. Java 语言是社区支持的 +13. Java 语言是企业支持的 +14. Java 语言是高性能的 +15. Java 语言是高可靠的 +16. Java 语言是高安全的 +17. Java 语言是高扩展的 +18. Java 语言是高并发的 +19. Java 语言是高吞吐量的 + +## Java 开发工具 + +Java 语言尽量保证系统内存在 1G 以上 + +## Java 开发环境配置 + +如何搭建Java开发环境 + +1. windows安装 +2. linux安装 +3. mac安装 + +## 开发环境配置 + +1. 安装JDK +2. 配置环境变量 +3. 测试安装 + +window系统安装java + +下载JDK + +首先我们需要下载 java 开发工具包 JDK,下载地址:https://www.oracle.com/java/technologies/downloads/,在下载页面中根据自己的系统选择对应的版本,本文以 Window 64位系统为例: + +![img.png](./img.png) + +下载后 JDK 的安装根据提示进行,还有安装 JDK 的时候也会安装 JRE,一并安装就可以了。 + +安装JDK,安装过程中可以自定义安装目录等信息,例如我们选择安装目录为 C:\Program Files (x86)\Java\jdk1.8.0_91。 + +## 配置环境变量 + +1.安装完成后,右击"我的电脑",点击"属性",选择"高级系统设置"; + +![img_1.png](./img_1.png) + +2.选择"高级"选项卡,点击"环境变量"; + +![img_2.png](./img_2.png) + +然后就会出现如下图所示的画面: + +![img_3.png](./img_3.png) + +在 "系统变量" 中设置 3 项属性,JAVA_HOME、PATH、CLASSPATH(大小写无所谓),若已存在则点击"编辑",不存在则点击"新建"。 + +注意:如果使用 1.5 以上版本的 JDK,不用设置 CLASSPATH 环境变量,也可以正常编译和运行 Java 程序。 + +## 变量设置参数如下: + +```java +- 变量名:JAVA_HOME +- 变量值:C:\Program Files (x86)\Java\jdk1.8.0_91 // 要根据自己的实际路径配置 +- 变量名:CLASSPATH +- 变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar; //记得前面有个"." +- 变量名:Path +- 变量值:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; +``` + +## JAVA_HOME 设置 + +![img_4.png](./img_4.png) + +![img_5.png](./img_5.png) + +## PATH设置 + +![img_6.png](./img_6.png) + +![img_7.png](./img_7.png) + +```java +注意:在 Windows10 中,Path 变量里是分条显示的,我们需要将 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; 分开添加,否则无法识别: + +%JAVA_HOME%\bin; +%JAVA_HOME%\jre\bin; +``` + +![img_8.png](./img_8.png) + +## CLASSPATH 设置 + +![img_9.png](./img_9.png) + +这是 Java 的环境配置,配置完成后,你可以启动 Eclipse 来编写代码,它会自动完成java环境的配置。 + +## 测试JDK是否安装成功 + +1、"开始"->"运行",键入"cmd"; + +2、键入命令: java -version、java、javac 几个命令,出现以下信息,说明环境变量配置成功; + +![img_10.png](./img_10.png) + +## 流行 Java 开发工具 + +JetBrains 的 IDEA, 现在很多人开始使用了,功能很强大,下载地址:https://www.jetbrains.com/idea/download/ + + + + + +--- + +> 恭喜你又完成了一个知识点!下一篇文章已经在向你招手了~ \ No newline at end of file diff --git a/docs/basic/10.md b/docs/basic/10.md new file mode 100644 index 000000000..dd297d6f8 --- /dev/null +++ b/docs/basic/10.md @@ -0,0 +1,107 @@ +--- +title: 第10天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第10天 + +> 小可爱,每天进步一点点,你已经很棒了! + +今天复习前面的内容了吗,温习一下,看看,再次认识语句写法 + +Java 中的条件语句允许程序根据条件的不同执行不同的代码块。 + +一个 if 语句包含一个布尔表达式和一条或多条语句。 + +## Java 条件语句 - if...else + +```java +public class Test { + + public static void main(String args[]){ + int x = 10; + + if( x < 20 ){ + System.out.print("这是 if 语句"); + } + } +} + +``` + +```java +public class Test { + + public static void main(String args[]){ + int x = 30; + + if( x < 20 ){ + System.out.print("这是 if 语句"); + }else{ + System.out.print("这是 else 语句"); + } + } +} +``` + +```java +if(布尔表达式 1){ + //如果布尔表达式 1的值为true执行代码 +}else if(布尔表达式 2){ + //如果布尔表达式 2的值为true执行代码 +}else if(布尔表达式 3){ + //如果布尔表达式 3的值为true执行代码 +}else { + //如果以上布尔表达式都不为true执行代码 +} +``` + +```java +public class Test { + public static void main(String args[]){ + int x = 30; + + if( x == 10 ){ + System.out.print("Value of X is 10"); + }else if( x == 20 ){ + System.out.print("Value of X is 20"); + }else if( x == 30 ){ + System.out.print("Value of X is 30"); + }else{ + System.out.print("这是 else 语句"); + } + } +} +``` + +```java +if(布尔表达式 1){ + ////如果布尔表达式 1的值为true执行代码 + if(布尔表达式 2){ + ////如果布尔表达式 2的值为true执行代码 + } +} +``` + +```java +public class Test { + + public static void main(String args[]){ + int x = 30; + int y = 10; + + if( x == 30 ){ + if( y == 10 ){ + System.out.print("X = 30 and Y = 10"); + } + } + } +} +``` + + + +--- + +> 学习路上每一步都很重要,下一篇继续深入探索编程世界吧! \ No newline at end of file diff --git a/docs/basic/11.md b/docs/basic/11.md new file mode 100644 index 000000000..9bed19058 --- /dev/null +++ b/docs/basic/11.md @@ -0,0 +1,124 @@ +--- +title: 第11天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第11天 + +> 为什么程序员喜欢咖啡?因为Java需要咖啡才能运行! + +再认识一个语句,好嘛,语句不嫌多 + +## Java switch case 语句 + +switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。 + +switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。 + +```java +switch(expression){ + case value : + //语句 + break; //可选 + case value : + //语句 + break; //可选 + //你可以有任意数量的case语句 + default : //可选 + //语句 +} +``` + +```java +public class Test { + public static void main(String args[]){ + //char grade = args[0].charAt(0); + char grade = 'C'; + + switch(grade) + { + case 'A' : + System.out.println("优秀"); + break; + case 'B' : + case 'C' : + System.out.println("良好"); + break; + case 'D' : + System.out.println("及格"); + break; + case 'F' : + System.out.println("你需要再努力努力"); + break; + default : + System.out.println("未知等级"); + } + System.out.println("你的等级是 " + grade); + } +} +``` + +```java +public class Test { + public static void main(String args[]){ + int i = 5; + switch(i){ + case 0: + System.out.println("0"); + case 1: + System.out.println("1"); + case 2: + System.out.println("2"); + default: + System.out.println("default"); + } + } +} +``` + +```java +public class Test { + public static void main(String args[]){ + int i = 1; + switch(i){ + case 0: + System.out.println("0"); + case 1: + System.out.println("1"); + case 2: + System.out.println("2"); + default: + System.out.println("default"); + } + } +} +``` + +```java +public class Test { + public static void main(String args[]){ + int i = 1; + switch(i){ + case 0: + System.out.println("0"); + case 1: + System.out.println("1"); + case 2: + System.out.println("2"); + case 3: + System.out.println("3"); break; + default: + System.out.println("default"); + } + } +} +``` + +尝试在idea运行试试结果 + + + +--- + +> 编程路上的每一步都值得庆祝,下一篇继续我们的学习之旅! \ No newline at end of file diff --git a/docs/basic/12.md b/docs/basic/12.md new file mode 100644 index 000000000..ef7639588 --- /dev/null +++ b/docs/basic/12.md @@ -0,0 +1,159 @@ +--- +title: 第12天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第12天 + +> 学习新知识很棒,但也要注意保护好自己的身体哦~ + +## Java Number & Math 类 + +今天认识 当需要使用数字的时候,我们通常使用内置数据类型,如:byte、int、long、double 等。 + +所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类 Number 的子类。 + +![img_21.png](./img_21.png) + +```java +int a = 5000; +float b = 13.65f; +byte c = 0x4a; +``` + +![img_22.png](./img_22.png) + +这种由编译器特别支持的包装称为装箱,所以当内置数据类型被当作对象使用的时候,编译器会把内置类型装箱为包装类。相似的,编译器也可以把一个对象拆箱为内置类型。 + +示例 +```java +public abstract class Number implements Serializable { + // 抽象方法 + public abstract int intValue(); + public abstract long longValue(); + public abstract float floatValue(); + public abstract double doubleValue(); + + // Java 8 新增 + public byte byteValue() { + return (byte)intValue(); + } + public short shortValue() { + return (short)intValue(); + } +} + +public class Test{ + + public static void main(String[] args){ + Integer x = 5; + x = x + 10; + System.out.println(x); + } +} + + +``` + +```java +Number num = 1234.56; // 实际是Double类型 + +System.out.println(num.intValue()); // 1234 (截断小数) +System.out.println(num.longValue()); // 1234 +System.out.println(num.floatValue()); // 1234.56 +System.out.println(num.doubleValue()); // 1234.56 +``` + +```java +Integer x = 10; +Double y = 10.0; + +// 正确比较方式:转换为同一类型后比较 +System.out.println(x.doubleValue() == y.doubleValue()); // true +``` + +## 特殊数值处理 + +处理大数 + +```java +BigInteger bigInt = new BigInteger("12345678901234567890"); +BigDecimal bigDec = new BigDecimal("1234567890.1234567890"); + +// 大数运算 +BigInteger sum = bigInt.add(new BigInteger("1")); +BigDecimal product = bigDec.multiply(new BigDecimal("2")); +``` + +数值格式化 + +```java +NumberFormat nf = NumberFormat.getInstance(); +nf.setMaximumFractionDigits(2); + +System.out.println(nf.format(1234.5678)); // "1,234.57" +``` + +## 自动装箱与拆箱 + +```java +// 自动装箱 +Integer autoBoxed = 42; // 编译器转换为 Integer.valueOf(42) + +// 自动拆箱 +int autoUnboxed = autoBoxed; // 编译器转换为 autoBoxed.intValue() +``` + +## Java Math 类 + +```java +public class Test { + public static void main (String []args) + { + System.out.println("90 度的正弦值:" + Math.sin(Math.PI/2)); + System.out.println("0度的余弦值:" + Math.cos(0)); + System.out.println("60度的正切值:" + Math.tan(Math.PI/3)); + System.out.println("1的反正切值: " + Math.atan(1)); + System.out.println("π/2的角度值:" + Math.toDegrees(Math.PI/2)); + System.out.println(Math.PI); + } +} +``` + +高级数学运算 + +1. 指数对数运算 + +```java +Math.exp(1); // e^1 ≈ 2.718 +Math.log(Math.E); // ln(e) = 1 +Math.log10(100); // log10(100) = 2 + +// 生成[0.0, 1.0)之间的随机数 +double random = Math.random(); + +// 生成[1, 100]的随机整数 +int randomInt = (int)(Math.random() * 100) + 1; + +Math.hypot(3, 4); // 计算sqrt(x²+y²) → 5.0 +Math.IEEEremainder(10, 3); // IEEE余数 → 1.0 + +Math.PI; // π ≈ 3.141592653589793 +Math.E; // 自然对数底数e ≈ 2.718281828459045 +``` + +## Math 的 floor,round 和 ceil 方法实例比较 + +![img_23.png](./img_23.png) + + + + + + + + +--- + +> 今天的内容消化得如何?下一篇会带来更多精彩内容哦~ \ No newline at end of file diff --git a/docs/basic/13.md b/docs/basic/13.md new file mode 100644 index 000000000..016bf8f2f --- /dev/null +++ b/docs/basic/13.md @@ -0,0 +1,127 @@ +--- +title: 第13天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第13天 + +> 记得定时站起来活动活动,久坐对身体不好呢~ + + +今天认识 Character 类 + +Character 类用于对单个字符进行操作。 + +Character 类在对象中包装一个基本类型 char 的值 + +## Character 类 + +`Character` 类是 Java 编程语言中的一个类,位于 `java.lang` 包中。它用于表示单个字符。这个类提供了许多方法来操作和检查字符,例如判断字符是否为字母、数字、空白字符等。 + +### 实现原理 + +`Character` 类内部使用一个 `char` 类型的变量来存储字符。`char` 类型在 Java 中占用 2 个字节(16 位),可以表示 Unicode 字符集中的字符。 + +### 主要方法 + +1. **判断字符类型的方法**: + - `isDigit(char ch)`: 判断字符是否为数字。 + - `isLetter(char ch)`: 判断字符是否为字母。 + - `isLetterOrDigit(char ch)`: 判断字符是否为字母或数字。 + - `isWhitespace(char ch)`: 判断字符是否为空白字符(如空格、制表符、换行符等)。 + - `isUpperCase(char ch)`: 判断字符是否为大写字母。 + - `isLowerCase(char ch)`: 判断字符是否为小写字母。 + +2. **转换字符大小写的方法**: + - `toUpperCase(char ch)`: 将字符转换为大写。 + - `toLowerCase(char ch)`: 将字符转换为小写。 + +3. **获取字符的 Unicode 编码**: + - `charValue()`: 返回 `Character` 对象表示的 `char` 值。 + +4. **比较字符的方法**: + - `compareTo(char anotherChar)`: 按字典顺序比较两个字符。 + - `equals(Object obj)`: 判断两个字符是否相等。 + +### 用途 + +`Character` 类在处理字符串和字符时非常有用,尤其是在需要判断字符类型、转换大小写、比较字符等操作时。例如,在用户输入验证、文本处理、数据加密等领域,`Character` 类提供的方法可以简化开发工作。 + +### 注意事项 + +1. **字符编码**:`char` 类型使用 Unicode 编码,可以表示全球范围内的字符。 +2. **性能考虑**:对于大量字符的处理,直接使用 `char` 类型可能比使用 `Character` 类更高效,因为 `Character` 类的方法调用可能引入额外的开销。 +3. **线程安全**:`Character` 类是线程安全的,因为它的实例是不可变的(immutable)。 + +```java +char ch = 'a'; + +// Unicode 字符表示形式 +char uniChar = '\u039A'; + +// 字符数组 +char[] charArray ={ 'a', 'b', 'c', 'd', 'e' }; +``` + +Character类提供了一系列方法来操纵字符。可以使用Character的构造方法创建一个Character类对象,例如: + +Character ch = new Character('a'); + +在某些情况下,Java编译器会自动创建一个Character对象。 + +例如,将一个char类型的参数传递给需要一个Character类型参数的方法时,那么编译器会自动地将char类型参数转换为Character对象。 这种特征称为装箱,反过来称为拆箱。 + +```java +// 原始字符 'a' 装箱到 Character 对象 ch 中 +Character ch = 'a'; + +// 原始字符 'x' 用 test 方法装箱 +// 返回拆箱的值到 'c' +char c = test('x'); +``` + +如果 test 方法内部需要使用对象类型,可以通过调用 Character 类的静态方法 valueOf 来进行装箱操作 + +```java +public static char test(char c) { + // 假设这里有一些操作 + return c; +} + +//在这个方法中,参数 c 是一个原始字符类型。如果 test 方法内部需要使用对象类型,可以通过调用 Character 类的静态方法 valueOf 来进行装箱操作。 +Character ch = Character.valueOf(c); + +//如果 test 方法内部需要使用原始类型,可以通过调用 Character 对象的 charValue 方法来进行拆箱操作。例如: +char c = ch.charValue(); + +``` + + +## 用途: + +装箱和拆箱操作在Java中是常见的,特别是在需要将原始数据类型传递给需要对象类型的方法时。 + +装箱操作允许原始数据类型在集合框架中使用,因为集合框架中的元素必须是对象类型。 + +拆箱操作允许在需要原始数据类型的地方使用对象类型。 + +## 注意事项: + +装箱和拆箱操作会带来一定的性能开销,因为它们涉及到对象的创建和销毁。 + +在性能敏感的应用中,应该尽量避免不必要的装箱和拆箱操作。 + +装箱和拆箱操作可能会引入空指针异常,如果尝试对一个 null 的 Character 对象进行拆箱操作,将会抛出 NullPointerException。 + +![img_24.png](./img_24.png) + + + + + + + +--- + +> 学完这一篇,是不是对编程又有了新的理解呢?继续加油,下一篇等着你哦~ \ No newline at end of file diff --git a/docs/basic/14.md b/docs/basic/14.md new file mode 100644 index 000000000..095f48140 --- /dev/null +++ b/docs/basic/14.md @@ -0,0 +1,89 @@ +--- +title: 第14天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第14天 + +> 记得定时站起来活动活动,久坐对身体不好呢~ + + +今天认识 String 类 + +## String + +字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。 + +## 创建字符串 + +创建字符串最简单的方式如下: + +```java +String str = "Hello World"; +``` + +可以使用关键字和构造方法来创建 String 对象。 + +用构造函数创建字符串: + +```java +String str2 = new String("Hello World"); +``` + +String 创建的字符串存储在公共池中,而 new 创建的字符串对象在堆上: + +```java +String s1 = "dada"; // String 直接创建 +String s2 = "dada"; // String 直接创建 +String s3 = s1; // 相同引用 +String s4 = new String("dada"); // String 对象创建 +String s5 = new String("dada"); // String 对象创建 +``` + +`String` 类是 Java 编程语言中的一个类,位于 `java.lang` 包中。它用于表示字符串,即一系列字符的序列。`String` 类是不可变的,意味着一旦创建了一个 `String` 对象,就不能改变它的值。 + +### 实现原理 + +`String` 类内部使用一个 `char` 类型的数组来存储字符序列。`char` 类型在 Java 中占用 2 个字节(16 位),可以表示 Unicode 字符集中的字符。 + +### 主要方法 + +1. **创建字符串**: + +2. **获取字符串长度**: + - `int length = str.length();`:返回字符串的长度,即字符序列的长度。 + +3. **字符串连接**: + - `String result = str1.concat(str2);`:将两个字符串连接成一个新的字符串。 + +4. **字符串比较**: + - `int result = str1.compareTo(str2);`:按字典顺序比较两个字符串。 + - `boolean result = str1.equals(str2);`:比较两个字符串的内容是否相等。 + +5. **字符串查找**: str.indexOf + - `int index = str.indexOf(World);`:返回子字符串在字符串中第一次出现的位置。 + +6. **字符串截取**: + - `String subStr = str.substring(0, 5);`:返回从指定位置开始到指定位置的子字符串。 + +7. **字符串转换**: + - `char[] charArray = str.toCharArray();`:将字符串转换为字符数组。 + - `String upperStr = str.toUpperCase();`:将字符串转换为大写。 + - `String lowerStr = str.toLowerCase();`:将字符串转换为小写。 + +### 用途 + +`String` 类在处理字符串时非常有用,例如字符串的拼接、比较、查找、截取、转换等操作。在 Java 编程中,字符串是常用的数据类型之一,`String` 类提供的方法可以简化字符串的处理工作。 + +### 注意事项 + +1. **不可变性**:`String` 类是不可变的,这意味着一旦创建了一个 `String` 对象,就不能改变它的值。如果需要修改字符串,应该创建一个新的 `String` 对象。 +2. **性能考虑**:由于 `String` 类是不可变的,每次修改字符串都会创建一个新的 `String` 对象,这可能会影响性能。在性能敏感的应用中,应该尽量避免频繁地修改字符串。 +3. **线程安全**:`String` 类是线程安全的,因为它的实例是不可变的(immutable)。 + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basic/15.md b/docs/basic/15.md new file mode 100644 index 000000000..f46f5adea --- /dev/null +++ b/docs/basic/15.md @@ -0,0 +1,31 @@ +--- +title: 第15天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第15天 + + +当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。 + +和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。 + +在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。 + +StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。 + +由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。 + +## StringBuffer 方法 + +以下是 StringBuffer 类支持的主要方法: + +![img_25.png](./img_25.png) + + + + +--- + +> 这一章节掌握得怎么样?下一篇会更有趣哦,期待与你继续学习~ \ No newline at end of file diff --git a/docs/basic/16.md b/docs/basic/16.md new file mode 100644 index 000000000..0f86cdffc --- /dev/null +++ b/docs/basic/16.md @@ -0,0 +1,91 @@ +--- +title: 第16天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第16天 + + +今天认识 数组,最近都是学习某一个知识点,一个个掌握,就可以上手了。能够在项目中使用这些语法,是个不错的体验。 + +## Java 数组 + +Java 语言中提供的数组是用来存储固定大小的同类型元素。 + +Java 数组是一种用于存储固定大小的同类型元素的数据结构。数组中的每个元素都可以通过索引访问,索引从0开始。Java 数组可以是一维的,也可以是多维的。 + +### 实现原理 + +Java 数组在内存中是连续存储的,每个元素占用相同大小的内存空间。数组的大小在创建时确定,并且不能改变。数组中的元素可以是基本数据类型(如 int、float、char 等)或对象类型。 + +### 用途 + +1. **存储和操作一组数据**:数组可以用来存储一组相同类型的数据,方便进行批量操作。 +2. **实现数据结构**:数组是实现其他数据结构(如栈、队列、哈希表等)的基础。 +3. **提高性能**:由于数组在内存中是连续存储的,访问数组元素的时间复杂度是 O(1),比链表等数据结构更高效。 + +### 注意事项 + +1. **大小固定**:数组的大小在创建时确定,不能动态改变。 +2. **类型一致**:数组中的所有元素必须是相同类型,不能混合不同类型的数据。 +3. **索引越界**:访问数组时,索引值必须在合法范围内(0 到 数组长度-1),否则会抛出 `ArrayIndexOutOfBoundsException` 异常。 +4. **内存占用**:数组在内存中占用连续空间,如果数组很大,可能会浪费内存。 + +### 示例代码 + +```java +public class ArrayExample { + public static void main(String[] args) { + // 创建一个包含5个整数的数组 + int[] numbers = new int[5]; + + // 初始化数组元素 + for (int i = 0; i < numbers.length; i++) { + numbers[i] = i * 10; + } + + // 访问数组元素 + for (int i = 0; i < numbers.length; i++) { + System.out.println("Element at index " + i + ": " + numbers[i]); + } + } +} + +``` + +### 多维数组示例 + +```java +public class MultiDimensionalArrayExample { + public static void main(String[] args) { + // 创建一个2行3列的二维数组 + int[][] matrix = new int[2][3]; + + // 初始化二维数组元素 + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + matrix[i][j] = i * 3 + j; + } + } + + // 访问二维数组元素 + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + System.out.println("Element at [" + i + "][" + j + "]: " + matrix[i][j]); + } + } + } +} + +``` + +通过以上示例代码,可以了解如何创建、初始化和访问一维数组和二维数组。在实际开发中,根据需求选择合适的数据结构来存储和处理数据。 + + + + + +--- + +> 编程路上的每一步都值得庆祝,下一篇继续我们的学习之旅! \ No newline at end of file diff --git a/docs/basic/17.md b/docs/basic/17.md new file mode 100644 index 000000000..b49fa524a --- /dev/null +++ b/docs/basic/17.md @@ -0,0 +1,72 @@ +--- +title: 第17天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第17天 + +> 编程路上有我陪伴,遇到困难不要气馁呢! + + +## Java 日期时间 + +![img_26.png](./img_26.png) + +## LocalDate/DateTimeFormatter + +LocalDate/DateTimeFormatter 是 Java 8 引入的日期类,LocalDate 用于表示不带时间的日期(年-月-日),DateTimeFormatter 用于格式化和解析日期时间对象。 + +Java 日期时间处理是Java编程中一个重要的部分,它允许开发者处理日期和时间。Java提供了多种类和方法来处理日期和时间,包括`java.util.Date`、`java.util.Calendar`、`java.time`包等。下面是一些关键点和注意事项: + +### 1. `java.util.Date` 类 + +`java.util.Date` 类是Java中最基础的日期时间类,它表示一个特定的瞬间,精确到毫秒。这个类提供了许多方法来获取和设置日期和时间,但它的设计已经过时,不推荐在新代码中使用。 + +```java +Date date = new Date(); +System.out.println(date); +``` + +### 2. `java.util.Calendar` 类 + +`java.util.Calendar` 类是一个抽象类,它为特定时区和语言环境提供了一些方法来获取和设置日期和时间。这个类比`Date`类更灵活,但仍然存在一些问题,如线程不安全。 + +```java +Calendar calendar = Calendar.getInstance(); +System.out.println(calendar.getTime()); +``` + +### 3. `java.time` 包 + +Java 8引入了新的日期时间API,位于`java.time`包中,它解决了`Date`和`Calendar`类中的许多问题。这个API提供了`LocalDate`、`LocalTime`、`LocalDateTime`、`ZonedDateTime`等类来处理日期和时间。 + +```java +LocalDate today = LocalDate.now(); +LocalTime timeNow = LocalTime.now(); +LocalDateTime dateTimeNow = LocalDateTime.now(); +ZonedDateTime zonedDateTimeNow = ZonedDateTime.now(); + +System.out.println(today);// 2023-06-15 +System.out.println(timeNow);// 15:20:30.123456789 +System.out.println(dateTimeNow);// 2023-06-15T15:20:30.123456789 +System.out.println(zonedDateTimeNow);// 2023-06-15T15:20:30.123456789+08:00[Asia/Shanghai] +``` + +### 注意事项 + +1. **线程安全**:`java.util.Date`和`java.util.Calendar`不是线程安全的,如果多个线程同时访问和修改它们,可能会导致数据不一致。`java.time`包中的类是线程安全的。 + +2. **时区处理**:`java.time`包提供了更好的时区处理能力,而`java.util.Date`和`java.util.Calendar`在处理时区时较为复杂。 + +3. **国际化**:`java.time`包提供了更好的国际化支持,而`java.util.Date`和`java.util.Calendar`在国际化方面较为有限。 + +4. **可读性和可维护性**:`java.time`包中的类和方法名称更加直观和易于理解,有助于提高代码的可读性和可维护性。 + +总之,`java.time`包是处理Java日期和时间的推荐方式,它提供了更强大、更灵活和更安全的API。 + +不学太多,用的时候查一下,先看一下,了解了解。 + +--- + +> 学习路上每一步都很重要,下一篇继续深入探索编程世界吧! \ No newline at end of file diff --git a/docs/basic/18.md b/docs/basic/18.md new file mode 100644 index 000000000..5775bac14 --- /dev/null +++ b/docs/basic/18.md @@ -0,0 +1,66 @@ +--- +title: 第18天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第18天 + +> 学习要循序渐进,不要给自己太大压力哦! + +学习技巧:掌握,先了解内容,理解原理,一旦理解了原理,就运用自如了。 + +## Java 正则表达式 + + +Java 正则表达式是一种强大的文本处理工具,用于在字符串中查找、替换和匹配特定的模式。正则表达式由一系列字符和符号组成,可以用来描述字符串的模式。在Java中,正则表达式通过`java.util.regex`包中的类来实现,主要包括`Pattern`和`Matcher`两个类。 + +### 实现原理 + +1. **Pattern类**:用于定义正则表达式模式。它提供了静态方法`compile(String regex)`,用于编译一个正则表达式,并返回一个`Pattern`对象。 + +2. **Matcher类**:用于匹配输入的字符串。它提供了`matches(String input)`方法,用于检查整个输入字符串是否与模式匹配,以及`find()`和`group()`方法,用于查找和提取匹配的子字符串。 + +### 用途 + +1. **字符串验证**:验证输入字符串是否符合特定的格式,如电子邮件地址、电话号码等。 +2. **字符串查找和替换**:在文本中查找特定的模式,并进行替换。 +3. **数据提取**:从复杂的文本中提取有用的信息,如从HTML中提取链接、从日志文件中提取特定信息等。 + +### 示例代码 + +```java +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +public class RegexExample { + public static void main(String[] args) { + // 定义正则表达式 + String regex = "\\b\\w+\\b"; + String input = "Hello, world! This is a regex example."; + + // 编译正则表达式 + Pattern pattern = Pattern.compile(regex); + + // 创建Matcher对象 + Matcher matcher = pattern.matcher(input); + + // 查找匹配的子字符串 + while (matcher.find()) { + System.out.println("Found: " + matcher.group()); + } + } +} +``` + +### 注意事项 + +1. **性能问题**:正则表达式匹配可能比较耗时,特别是在处理大型文本时,应尽量优化正则表达式,避免使用复杂的模式。 +2. **转义字符**:正则表达式中的某些字符有特殊含义,如`.`、`*`、`?`等,如果需要匹配这些字符本身,需要使用转义字符`\`。 +3. **贪婪匹配和懒惰匹配**:正则表达式默认是贪婪匹配,即尽可能多地匹配字符。如果需要懒惰匹配,可以使用`?`符号,如`.*?`表示匹配任意字符,但尽可能少地匹配。 + +通过掌握Java正则表达式,可以大大提高文本处理的能力,解决各种复杂的字符串操作问题。 + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basic/19.md b/docs/basic/19.md new file mode 100644 index 000000000..034b78cbe --- /dev/null +++ b/docs/basic/19.md @@ -0,0 +1,95 @@ +--- +title: 第19天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第19天 + +> 记得定时站起来活动活动,久坐对身体不好呢~ + + +## Java 方法 + +了解有哪些,写代码更加轻松,哈哈哈 + +![img_27.png](./img_27.png) + +Java 方法(Method)是Java编程语言中的一个基本概念,用于将一组语句封装在一起,以便在程序中重复使用。方法可以接受参数并返回结果。Java中的方法分为两大类:实例方法和静态方法。 + +### 方法的基本结构 + +```java +修饰符 返回类型 方法名(参数列表) { + // 方法体 + // 方法执行的操作 + return 返回值; // 如果有返回值 +} +``` + +### 修饰符 +- `public`:方法可以被任何其他类访问。 +- `private`:方法只能在其定义的类内部访问。 +- `protected`:方法可以在同一包内或不同包的子类中访问。 +- `default`(无修饰符):方法可以在同一包内访问。 +- `static`:方法属于类本身,而不是类的实例。 + +### 返回类型 +- 方法可以返回任何类型的数据,包括基本数据类型(如int、float等)和引用数据类型(如对象、数组等)。如果没有返回值,则使用`void`。 + +### 方法名 +- 方法名应该是一个动词,并且遵循Java的命名规范,通常使用小写字母开头,如果方法名由多个单词组成,后面的每个单词首字母大写。 + +### 参数列表 +- 参数列表是方法可以接受的一个或多个参数,参数由参数类型和参数名组成,多个参数之间用逗号分隔。如果没有参数,则参数列表为空。 + +### 方法体 +- 方法体是方法执行的操作,可以包含任意数量的语句,包括变量声明、条件语句、循环语句等。 + +### 返回值 +- 如果方法有返回值,则需要在方法体中使用`return`语句返回一个值。如果没有返回值,则方法体中不需要`return`语句。 + +### 方法重载 +- 方法重载是指在同一个类中,可以定义多个方法,它们的方法名相同但参数列表不同(参数的数量、类型或顺序不同)。Java编译器根据调用方法时提供的参数来决定调用哪个方法。 + +### 注意事项 +- 方法名应该具有描述性,能够清楚地表达方法的功能。 +- 方法参数应该尽量少,过多的参数会使方法难以理解和维护。 +- 方法应该尽可能短小,每个方法只做一件事,符合单一职责原则。 +- 方法应该避免副作用,即方法不应该改变调用者传入的对象的状态。 +- 方法应该有适当的注释,说明方法的用途、参数和返回值。 + +### 示例 + +```java +public class Example { + // 静态方法,返回两个整数的和 + public static int add(int a, int b) { + return a + b; + } + + // 实例方法,返回一个字符串的长度 + public int getStringLength(String str) { + return str.length(); + } + + public static void main(String[] args) { + int sum = Example.add(3, 4); + System.out.println("Sum: " + sum); + + Example example = new Example(); + int length = example.getStringLength("Hello, World!"); + System.out.println("Length: " + length); + } +} + +``` + +在这个示例中,`add`是一个静态方法,它接受两个整数作为参数并返回它们的和。`getStringLength`是一个实例方法,它接受一个字符串作为参数并返回其长度。在`main`方法中,我们调用了这两个方法并打印了它们的返回值。 + + + + +--- + +> 这一章节掌握得怎么样?下一篇会更有趣哦,期待与你继续学习~ \ No newline at end of file diff --git a/docs/basic/2.md b/docs/basic/2.md new file mode 100644 index 000000000..45a282af4 --- /dev/null +++ b/docs/basic/2.md @@ -0,0 +1,390 @@ +--- +title: 第2天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第2天 + +> 学习要循序渐进,不要给自己太大压力哦! + + +今天学基础语法,看看得了,很快,坚持一下,第二天就会java了,如果你继续学就直接会了 + +## Java 基础语法 + +一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。 + +对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。 + +类:类是一个模板,它描述一类对象的行为和状态。 + +方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。 + +实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。 + +![img_11.png](./img_11.png) + +```java +$ javac HelloWorld.java +$ java HelloWorld +Hello World + +如果遇到编码问题,我们可以使用 -encoding 选项设置 utf-8 来编译: + +javac -encoding UTF-8 HelloWorld.java +java HelloWorld +``` + +## 基本语法 + +大小写敏感:Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的。 + +类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。 + +方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。 + +源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)。 + +主方法入口:所有的 Java 程序由 public static void main(String[] args) 方法开始执行。 + +## Java 标识符 + +Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。 + +```java +所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始 +首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合 +关键字不能用作标识符 +标识符是大小写敏感的 +合法标识符举例:age、$salary、_value、__1_value +非法标识符举例:123abc、-salary +``` + +## Java修饰符 + +访问控制修饰符 : default, public , protected, private + +非访问控制修饰符 : final, abstract, static, synchronized + +## Java 变量 + +Java 中主要有如下几种类型的变量 + +1. 局部变量 +2. 类变量(静态变量) +3. 成员变量(非静态变量) + +## Java + +数组是储存在**堆上**的对象,可以保存多个同类型变量。 (数组名是引用,数组中的每个元素都是一个对象) + +```java +int[] a = new int[10]; // 声明并初始化数组 +int[] b = {1, 2, 3, 4, 5}; // 声明并初始化数组 +int[] c = new int[]{1, 2, 3, 4, 5}; // 声明并初始化数组 +int[] d = new int[5]; // 声明数组,并指定数组大小 +``` + +## Java 枚举 + +Java 5.0引入了枚举,枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的 bug。 + +例如,我们为果汁店设计一个程序,它将限制果汁为小杯、中杯、大杯。这就意味着它不允许顾客点除了这三种尺寸外的果汁。 + +```java +public class FreshJuice { + enum FreshJuiceSize{ SMALL, MEDIUM , LARGE } + FreshJuiceSize size; +} +``` + +## Java 关键字 + +过一下 + +```java +访问控制 + +private 私有的 +protected 受保护的 +public 公共的 +default 默认 + +类、方法和变量修饰符 + +abstract 声明抽象 +class 类 + extends 扩充、继承 +final 最终值、不可改变的 +implements 实现(接口) +interface 接口 +native 本地、原生方法(非 Java 实现) + new 创建 +static 静态 +strictfp 严格浮点、精准浮点 +synchronized 线程、同步 +transient 短暂 +volatile 易失 + +程序控制语句 + +break 跳出循环 +case 定义一个值以供 switch 选择 +continue 继续 +do 运行 +else 否则 +for 循环 +if 如果 +instanceof 实例 +return 返回 +switch 根据值选择执行 +while 循环 + +错误处理 + +assert 断言表达式是否为真 +catch 捕捉异常 +finally 有没有异常都执行 +throw 抛出一个异常对象 +throws 声明一个异常可能被抛出 +try 捕获异常 + +包相关 + +import 引入 +package 包 + +基本类型 + +boolean 布尔型 +byte 字节型 +char 字符型 +double 双精度浮点 +float 单精度浮点 +int 整型 +long 长整型 +short 短整型 + +变量引用 + +super 父类、超类 +this 本类 +void 无返回值 + +保留关键字 + +goto 是关键字,但不能使用 +const 是关键字,但不能使用 + + +注意:Java 的 null 不是关键字,类似于 true 和 false,它是一个字面常量,不允许作为标识符使用。 + +``` + +## 继承 + +在 Java 中,一个类可以由其他类派生。如果你要创建一个类,而且已经存在一个类具有你所需要的属性或方法,那么你可以将新创建的类继承该类。 + +利用继承的方法,可以重用已存在类的方法和属性,而不用重写这些代码。被继承的类称为超类(super class),派生类称为子类(sub class)。 + +示例: + +```java +class Animal{ + void eat(){ + System.out.println("eating..."); + } +} + +class Dog extends Animal{ + void bark(){ + System.out.println("barking..."); + } +} + +public class TestDog{ + public static void main(String args[]){ + Animal a = new Animal(); + a.eat(); + + Dog d = new Dog(); + d.bark(); + d.eat(); + } +} +``` + +Animal类定义了一个名为eat的方法,该方法打印出“eating...”。 + +Dog类继承自Animal类,这意味着Dog类可以继承Animal类的所有方法和属性。 + +Dog类定义了一个名为bark的方法,该方法打印出“barking...”。 + +TestDog类是程序的入口点。 + +在main方法中,首先创建了一个Animal对象a,并调用了a.eat()方法,输出“eating...”。 + +然后创建了一个Dog对象d,并调用了d.bark()方法,输出“barking...”。 + +最后,调用了d.eat()方法,由于Dog类继承了Animal类,所以d.eat()方法实际上是调用了Animal类中的eat方法,输出“eating...”。 + +### 实现原理 + +继承:Dog类继承了Animal类,这意味着Dog类可以访问Animal类中的所有公共(public)和受保护(protected)成员,包括方法。 + +多态性:虽然Dog类继承了Animal类,但Dog类可以有自己的方法,如bark方法。当调用Dog对象的方法时,会根据实际的对象类型来决定调用哪个方法。例如,d.bark()调用的是Dog类中的bark方法,而d.eat()调用的是Animal类中的eat方法。 + +### 用途 + +这个程序展示了面向对象编程的基本概念,如继承和多态性,是学习Java面向对象编程的基础。 + +通过继承,可以重用代码,减少重复工作。 + +通过多态性,可以在运行时根据对象的实际类型来决定调用哪个方法,增加了程序的灵活性和可扩展性。 + +在Java中,类名和文件名应该一致,即TestDog类应该保存在名为TestDog.java的文件中。 + +继承是面向对象编程中的一个重要特性,但过度使用继承可能会导致代码结构复杂,难以维护。在设计类时,应该根据实际需求谨慎使用继承。 + +## 接口 + +在 Java 中,接口可理解为对象间相互通信的协议。接口在继承中扮演着很重要的角色。 + +接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类。 + +示例 + +演示了接口(interface)和继承(inheritance)的基本概念。 + +```java +// 接口是一个完全抽象的类,它包含了一系列未实现的方法。接口定义了类应该具有的方法,但不提供这些方法的具体实现。接口通过interface关键字定义。 +interface Animal{ + void eat(); + void travel(); +} +// Animal接口定义了两个方法:eat()和travel()。任何实现Animal接口的类都必须提供这两个方法的具体实现。 + +//类(Class)和实现(Implementation) +//类是对象的蓝图,它包含了数据和操作这些数据的方法。类通过class关键字定义。 +//MammalAnimal类实现了Animal接口,这意味着它必须提供eat()和travel()方法的具体实现。 +class MammalAnimal implements Animal { + public void eat(){ + System.out.println("Mammal eat"); + } + public void travel(){ + System.out.println("Mammal travel"); + } +} +//继承是面向对象编程中的一个基本概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以扩展父类的功能,也可以重写父类的方法。 +//Dog类继承了MammalAnimal类,这意味着Dog类不仅继承了MammalAnimal类的属性和方法,还可以添加自己的属性和方法。在这个例子中,Dog类添加了一个bark()方法。 +class Dog extends MammalAnimal{ + public void bark(){ + System.out.println("dog barking"); + } +} +// 主类(Main Class) 主类是程序的入口点,它包含了main方法。 +// 在main方法中,首先创建了一个MammalAnimal对象,并调用了它的eat()和travel()方法。然后创建了一个Dog对象,并调用了它的bark()和eat()方法。 +public class TestInterface { + public static void main(String args[]) { + Animal a = new MammalAnimal(); + a.eat(); + a.travel(); + + Dog d = new Dog(); + d.bark(); + d.eat(); + } +} +``` + +接口和类的区别:接口定义了方法,但不包含实现。类可以包含属性和方法,并且可以继承自其他类或实现一个或多个接口。 + +实现接口:类通过implements关键字实现一个或多个接口,并必须提供接口中所有方法的具体实现。 + +继承:类通过extends关键字继承自另一个类,子类可以继承父类的属性和方法,并且可以添加自己的属性和方法。 + +多态:在Java中,多态允许一个接口或父类的引用指向一个实现了该接口或继承了该父类的子类对象。这允许在运行时根据对象的实际类型调用相应的方法。 + +## Java 源程序与编译型运行区别 + +![img_12.png](./img_12.png) + +## 多态 + +多态是同一个行为具有多个不同表现形式或形态的能力。 + +多态就是同一个接口,使用不同的实例而执行不同操作。 + +示例 + +```java +//Shape 类是所有形状的父类,它有一个 draw() 方法,但这个方法没有具体实现。 +class Shape { + void draw() {} +} +//Circle、Square 和 Triangle 类都继承自 Shape 类,并重写了 draw() 方法,以实现各自独特的绘制逻辑。 +class Circle extends Shape { + void draw() { + System.out.println("Circle.draw()"); + } +} +class Square extends Shape { + void draw() { + System.out.println("Square.draw()"); + } +} +class Triangle extends Shape { + void draw() { + System.out.println("Triangle.draw()"); + } +} +//在 main 方法中,创建了一个 Shape 类型的数组 shapes,长度为3。 +public class Polymorphism { + public static void main(String[] args) { + Shape[] shapes = new Shape[3]; + shapes[0] = new Circle(); + shapes[1] = new Square(); + shapes[2] = new Triangle(); + for(int i=0; i 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basic/20.md b/docs/basic/20.md new file mode 100644 index 000000000..2206ee2bc --- /dev/null +++ b/docs/basic/20.md @@ -0,0 +1,146 @@ +--- +title: 第20天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第20天 + +> 学习新知识很棒,但也要注意保护好自己的身体哦~ + + +## Java 构造方法 + +## 构造方法的类型 + +Java 中的构造方法分为两种类型:无参构造方法和有参构造方法。 + +```java +//1、无参构造方法(默认构造方法) +public class Person { + public Person() { + System.out.println("Person对象已创建"); + } +} + +//2、有参构造方法 +public class Person { + String name; + int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } +} + +//构造方法的重载 +public class Person { + String name; + int age; + + public Person() { + this.name = "Unknown"; + this.age = 0; + } + + public Person(String name) { + this.name = name; + this.age = 0; + } + + public Person(String name, int age) { + this.name = name; + this.age = age; + } +} +``` + +## 构造方法中的 this 关键字 + +在构造方法中,this 关键字通常用于两种情况: + +1、引用当前对象的属性或方法:当构造方法的参数名与类属性名相同时,使用 this 来区分类属性和参数。例如: + +2、调用另一个构造方法:可以使用 this() 调用当前类的其他构造方法,常用于避免重复代码,但必须放在构造方法的第一行。 + +```java +public Person(String name, int age) { + this.name = name; // this.name 表示类的属性 + this.age = age; +} + + +``` + +```java +public Person(String name) { + this(name, 0); // 调用另一个双参数的构造方法 +} + +public Person(String name, int age) { + this.name = name; + this.age = age; +} +``` + + +Java 构造方法是一种特殊的方法,用于在创建对象时初始化对象的属性。构造方法与类同名,并且没有返回类型。每个类至少有一个构造方法,如果没有显式定义,Java 编译器会提供一个默认的无参构造方法。 + +### 构造方法的用途 + +1. **初始化对象属性**:在创建对象时,构造方法可以用来初始化对象的属性。 +2. **执行一些必要的操作**:构造方法可以在对象创建时执行一些必要的操作,比如打开文件、建立数据库连接等。 + +### 构造方法的定义 + +构造方法与类同名,并且没有返回类型。例如: + +```java +public class Person { + private String name; + private int age; + + // 无参构造方法 + public Person() { + this.name = "Unknown"; + this.age = 0; + } + + // 带参数的构造方法 + public Person(String name, int age) { + this.name = name; + this.age = age; + } +} + +``` + +### 构造方法的注意事项 + +1. **构造方法不能被显式调用**:构造方法不能通过对象名调用,只能在创建对象时由 Java 虚拟机自动调用。 +2. **构造方法可以重载**:一个类可以有多个构造方法,只要它们的参数列表不同即可。 +3. **构造方法可以调用其他构造方法**:通过 `this()` 关键字可以在一个构造方法中调用另一个构造方法。 + +### 示例 + +```java +public class Main { + public static void main(String[] args) { + // 使用无参构造方法创建对象 + Person person1 = new Person(); + System.out.println(person1.getName() + ", " + person1.getAge()); + + // 使用带参数的构造方法创建对象 + Person person2 = new Person("Alice", 30); + System.out.println(person2.getName() + ", " + person2.getAge()); + } +} + +``` + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basic/21.md b/docs/basic/21.md new file mode 100644 index 000000000..eec73b2f7 --- /dev/null +++ b/docs/basic/21.md @@ -0,0 +1,54 @@ +--- +title: 第21天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第21天 + +> 学习编程要劳逸结合哦,记得多喝水休息眼睛~ + + +## Java 流(Stream)、文件(File)和IO + +Java 中的流(Stream)、文件(File)和 IO(输入输出)是处理数据读取和写入的基础设施,它们允许程序与外部数据(如文件、网络、系统输入等)进行交互。 + +java.io 包是 Java 标准库中的一个核心包,提供了用于系统输入和输出的类,它包含了处理数据流(字节流和字符流)、文件读写、序列化以及数据格式化的工具。 + +java.io 是处理文件操作、流操作以及低级别 IO 操作的基础包。 + +java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等。 + +一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。 + +Java 的控制台输入由 System.in 完成。 + +为了获得一个绑定到控制台的字符流,你可以把 System.in 包装在一个 BufferedReader 对象中来创建一个字符流。 + +下面是创建 BufferedReader 的基本语法: + +```java +BufferedReader br = new BufferedReader(new + InputStreamReader(System.in)); +``` + +BufferedReader 对象创建后,我们便可以使用 read() 方法从控制台读取一个字符,或者用 readLine() 方法读取一行字符串。 + +## 读写文件 + +![img_28.png](./img_28.png) + +![img_29.png](./img_29.png) + +![img_30.png](./img_30.png) + +![img_31.png](./img_31.png) + +![img_32.png](./img_32.png) + +![img_33.png](./img_33.png) + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basic/22.md b/docs/basic/22.md new file mode 100644 index 000000000..968bc4aba --- /dev/null +++ b/docs/basic/22.md @@ -0,0 +1,54 @@ +--- +title: 第22天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第22天 + +> 每天进步一点点,你已经很棒了! + + +## Java 异常处理 + +```java +try { + // 可能会抛出异常的代码 +} catch (IOException e) { + // 处理异常的代码 +} +``` + +```java +public void readFile() throws IOException { + // 可能会抛出IOException的代码 +} +``` + +```java +try { + // 可能会抛出异常的代码 +} catch (NullPointerException e) { + // 处理异常的代码 +} +``` + +try:用于包裹可能会抛出异常的代码块。 + +catch:用于捕获异常并处理异常的代码块。 + +finally:用于包含无论是否发生异常都需要执行的代码块。 + +throw:用于手动抛出异常。 + +throws:用于在方法声明中指定方法可能抛出的异常。 + +Exception类:是所有异常类的父类,它提供了一些方法来获取异常信息,如 getMessage()、printStackTrace() 等。 + +![img_34.png](./img_34.png) + + + +--- + +> 学习路上每一步都很重要,下一篇继续深入探索编程世界吧! \ No newline at end of file diff --git a/docs/basic/23.md b/docs/basic/23.md new file mode 100644 index 000000000..78c024634 --- /dev/null +++ b/docs/basic/23.md @@ -0,0 +1,302 @@ +--- +title: 第23天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第23天 + +> 学习累了就休息一下,我会一直在这里等你~ + + +继承的概念 + +继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 + +生活中的继承: + +![img_35.png](./img_35.png) + +兔子和羊属于食草动物类,狮子和豹属于食肉动物类。 + +食草动物和食肉动物又是属于动物类。 + +虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。 + +## 类的继承格式 + +在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下: + +```java +class 父类 { +} + +class 子类 extends 父类 { +} +``` + +## 为什么需要继承 + +接下来我们通过实例来说明这个需求。 + +开发动物类,其中动物分别为企鹅以及老鼠,要求如下: + +企鹅:属性(姓名,id),方法(吃,睡,自我介绍) + +老鼠:属性(姓名,id),方法(吃,睡,自我介绍) + +```java +public class Penguin { + private String name; + private int id; + public Penguin(String myName, int myid) { + name = myName; + id = myid; + } + public void eat(){ + System.out.println(name+"正在吃"); + } + public void sleep(){ + System.out.println(name+"正在睡"); + } + public void introduction() { + System.out.println("大家好!我是" + id + "号" + name + "."); + } +} +``` + +```java +public class Mouse { + private String name; + private int id; + public Mouse(String myName, int myid) { + name = myName; + id = myid; + } + public void eat(){ + System.out.println(name+"正在吃"); + } + public void sleep(){ + System.out.println(name+"正在睡"); + } + public void introduction() { + System.out.println("大家好!我是" + id + "号" + name + "."); + } +} +``` + +## 公共父类: + +```java +public class Animal { + private String name; + private int id; + public Animal(String myName, int myid) { + name = myName; + id = myid; + } + public void eat(){ + System.out.println(name+"正在吃"); + } + public void sleep(){ + System.out.println(name+"正在睡"); + } + public void introduction() { + System.out.println("大家好!我是" + id + "号" + name + "."); + } +} +``` + +## 企鹅类: + +```java +public class Penguin extends Animal { + public Penguin(String myName, int myid) { + super(myName, myid); + } +} +``` + +## 老鼠类: + +```java +public class Mouse extends Animal { + public Mouse(String myName, int myid) { + super(myName, myid); + } +} +``` + +## 继承类型 + +需要注意的是 Java 不支持多继承,但支持多重继承。 + +![img_36.png](./img_36.png) + +## 继承的特性 + +子类拥有父类非 private 的属性、方法。 + +子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。 + +子类可以用自己的方式实现父类的方法。 + +Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。 + +提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。 + +## extends关键字 + +在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。 + +```java +public class Animal { + private String name; + private int id; + public Animal(String myName, int myid) { + //初始化属性值 + } + public void eat() { //吃东西方法的具体实现 } + public void sleep() { //睡觉方法的具体实现 } +} + +public class Penguin extends Animal{ + +} +``` + +## implements关键字 + +使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。 + +```java +public interface A { + public void eat(); + public void sleep(); +} + +public interface B { + public void show(); +} + +public class C implements A,B { +} +``` + +## super 与 this 关键字 + +super 关键字:我们可以通过 super 关键字来实现对父类成员的访问,用来引用当前对象的父类。 + +this 关键字:指向自己的引用,引用当前对象,即它所在的方法或构造函数所属的对象实例。。 + +实例 + +```java +class Animal { + void eat() { + System.out.println("animal : eat"); + } +} + +class Dog extends Animal { + void eat() { + System.out.println("dog : eat"); + } + void eatTest() { + this.eat(); // this 调用自己的方法 + super.eat(); // super 调用父类方法 + } +} + +public class Test { + public static void main(String[] args) { + Animal a = new Animal(); + a.eat(); + Dog d = new Dog(); + d.eatTest(); + } +} +``` + +## 构造器 + +子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。 + +如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。 + +```java +class SuperClass { + private int n; + + // 无参数构造器 + public SuperClass() { + System.out.println("SuperClass()"); + } + + // 带参数构造器 + public SuperClass(int n) { + System.out.println("SuperClass(int n)"); + this.n = n; + } +} + +// SubClass 类继承 +class SubClass extends SuperClass { + private int n; + + // 无参数构造器,自动调用父类的无参数构造器 + public SubClass() { + System.out.println("SubClass()"); + } + + // 带参数构造器,调用父类中带有参数的构造器 + public SubClass(int n) { + super(300); + System.out.println("SubClass(int n): " + n); + this.n = n; + } +} + +// SubClass2 类继承 +class SubClass2 extends SuperClass { + private int n; + + // 无参数构造器,调用父类中带有参数的构造器 + public SubClass2() { + super(300); + System.out.println("SubClass2()"); + } + + // 带参数构造器,自动调用父类的无参数构造器 + public SubClass2(int n) { + System.out.println("SubClass2(int n): " + n); + this.n = n; + } +} + +public class TestSuperSub { + public static void main(String[] args) { + System.out.println("------SubClass 类继承------"); + SubClass sc1 = new SubClass(); + SubClass sc2 = new SubClass(100); + + System.out.println("------SubClass2 类继承------"); + SubClass2 sc3 = new SubClass2(); + SubClass2 sc4 = new SubClass2(200); + } +} +``` + + + + + + + + + + + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basic/24.md b/docs/basic/24.md new file mode 100644 index 000000000..88ea12b4a --- /dev/null +++ b/docs/basic/24.md @@ -0,0 +1,209 @@ +--- +title: 第24天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第24天 + +> 学习新知识很棒,但也要注意保护好自己的身体哦~ + + +## Java 重写(Override)与重载(Overload) + +重写(Override)是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的实现覆盖了父类方法的实现。 即外壳不变,核心重写! + +重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。这样,在使用子类对象调用该方法时,将执行子类中的方法而不是父类中的方法。 + +重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,抛出 IOException 异常或者 IOException 的子类异常。 + +```java +class Animal{ + public void move(){ + System.out.println("动物可以移动"); + } +} + +class Dog extends Animal{ + public void move(){ + System.out.println("狗可以跑和走"); + } +} + +public class TestDog{ + public static void main(String args[]){ + Animal a = new Animal(); // Animal 对象 + Animal b = new Dog(); // Dog 对象 + + a.move();// 执行 Animal 类的方法 + + b.move();//执行 Dog 类的方法 + } +} +``` + +## TestDog.java 文件代码: + +```java +class Animal{ + public void move(){ + System.out.println("动物可以移动"); + } +} + +class Dog extends Animal{ + public void move(){ + System.out.println("狗可以跑和走"); + } + public void bark(){ + System.out.println("狗可以吠叫"); + } +} + +public class TestDog{ + public static void main(String args[]){ + Animal a = new Animal(); // Animal 对象 + Animal b = new Dog(); // Dog 对象 + + a.move();// 执行 Animal 类的方法 + b.move();//执行 Dog 类的方法 + b.bark(); + } +} +``` + +以上实例编译运行结果如下: + +```java +TestDog.java:30: cannot find symbol +symbol : method bark() +location: class Animal + b.bark(); + ^ + +``` + +该程序将抛出一个编译错误,因为b的引用类型Animal没有bark方法。 + +## 方法的重写规则 + +参数列表与被重写方法的参数列表必须完全相同。 + +返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。 + +访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。 + +父类的成员方法只能被它的子类重写。 + +声明为 final 的方法不能被重写。 + +声明为 static 的方法不能被重写,但是能够被再次声明。 + +子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。 + +子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。 + +重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。 + +构造方法不能被重写。 + +如果不能继承一个类,则不能重写该类的方法。 + +## Super 关键字的使用 + +当需要在子类中调用父类的被重写方法时,要使用 super 关键字。 + +```java +class Animal{ + public void move(){ + System.out.println("动物可以移动"); + } +} + +class Dog extends Animal{ + public void move(){ + super.move(); // 应用super类的方法 + System.out.println("狗可以跑和走"); + } +} + +public class TestDog{ + public static void main(String args[]){ + + Animal b = new Dog(); // Dog 对象 + b.move(); //执行 Dog类的方法 + + } +} +``` + +以上实例编译运行结果如下: + +```java +动物可以移动 +狗可以跑和走 +``` + +## 重载(Overload) + +重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。 + +每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。 + +最常用的地方就是构造器的重载。 + +重载规则: + +- 被重载的方法必须改变参数列表(参数个数或类型不一样); +- 被重载的方法可以改变返回类型; +- 被重载的方法可以改变访问修饰符; +- 被重载的方法可以声明新的或更广的检查异常; +- 方法能够在同一个类中或者在一个子类中被重载。 +- 无法以返回值类型作为重载函数的区分标准。 + +示例: + +```java +public class Overloading { + public int test(){ + System.out.println("test1"); + return 1; + } + + public void test(int a){ + System.out.println("test2"); + } + + //以下两个参数类型顺序不同 + public String test(int a,String s){ + System.out.println("test3"); + return "returntest3"; + } + + public String test(String s,int a){ + System.out.println("test4"); + return "returntest4"; + } + + public static void main(String[] args){ + Overloading o = new Overloading(); + System.out.println(o.test()); + o.test(1); + System.out.println(o.test(1,"test3")); + System.out.println(o.test("test4",1)); + } +} +``` + +## 如图 + +![img_37.png](./img_37.png) + +![img_38.png](./img_38.png) + + + + +--- + +> 今天的学习就到这里啦,明天继续我们的编程之旅吧! \ No newline at end of file diff --git a/docs/basic/25.md b/docs/basic/25.md new file mode 100644 index 000000000..29ac7345f --- /dev/null +++ b/docs/basic/25.md @@ -0,0 +1,135 @@ +--- +title: 第25天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第25天 + +> 学习要循序渐进,不要给自己太大压力哦! + + +## Java 多态 + +多态是同一个行为具有多个不同表现形式或形态的能力。 + +多态就是同一个接口,使用不同的实例而执行不同操作,如图所示: + +![img_39.png](./img_39.png) + +## 多态的优点 + +1. 消除类型之间的耦合关系 +2. 可替换性 +3. 可扩充性 +4. 接口性 +5. 灵活性 +6. 简化性 + +## 多态存在的三个必要条件 + +- 继承 +- 重写 +- 父类引用指向子类对象:Parent p = new Child(); + +![img_40.png](./img_40.png) + +```java +class Shape { + void draw() {} +} + +class Circle extends Shape { + void draw() { + System.out.println("Circle.draw()"); + } +} + +class Square extends Shape { + void draw() { + System.out.println("Square.draw()"); + } +} + +class Triangle extends Shape { + void draw() { + System.out.println("Triangle.draw()"); + } +} +``` + + +Java 多态是面向对象编程中的一个核心概念,它允许一个接口被多个不同的类实现,或者一个类继承自多个父类。多态的目的是为了实现代码的灵活性和可扩展性,使得程序能够根据运行时的具体对象类型来决定调用哪个方法。 + +### 实现原理 + +Java 多态主要通过以下两种方式实现: + +1. **方法重载(Overloading)**:在同一个类中,方法名相同但参数列表不同。编译器根据调用时提供的参数类型和数量来决定调用哪个方法。 + +2. **方法重写(Overriding)**:子类重写父类的方法,使得子类对象在调用该方法时,会执行子类的方法实现。 + +### 用途 + +1. **代码复用**:通过方法重载,可以在同一个类中定义多个同名方法,但参数不同,从而实现代码的复用。 + +2. **灵活性和可扩展性**:通过方法重写,子类可以根据需要重写父类的方法,实现不同的行为。 + +3. **接口实现**:通过接口,可以定义一组方法,不同的类可以实现这个接口,从而实现多态。 + +### 注意事项 + +1. **编译时多态**:方法重载是在编译时确定的,即编译器根据参数类型和数量来决定调用哪个方法。 + +2. **运行时多态**:方法重写是在运行时确定的,即根据对象的实际类型来决定调用哪个方法。 + +3. **类型转换**:在多态中,需要使用类型转换来确保对象类型正确,否则可能会抛出 `ClassCastException` 异常。 + +4. **接口和抽象类**:在实现多态时,通常使用接口和抽象类,因为它们可以定义方法,但不提供具体实现,从而使得子类可以重写这些方法。 + +### 示例代码 + +```java +// 定义一个接口 +interface Animal { + void makeSound(); +} + +// 实现接口的类 +class Dog implements Animal { + public void makeSound() { + System.out.println("汪汪汪"); + } +} + +class Cat implements Animal { + public void makeSound() { + System.out.println("喵喵喵"); + } +} + +public class Main { + public static void main(String[] args) { + Animal myDog = new Dog(); + Animal myCat = new Cat(); + + // 多态调用 + myDog.makeSound(); // 输出:汪汪汪 + myCat.makeSound(); // 输出:喵喵喵 + } +} + +``` + +在这个示例中,`Animal` 接口定义了一个 `makeSound` 方法,`Dog` 和 `Cat` 类分别实现了这个接口。在 `main` 方法中,通过多态的方式调用了 `makeSound` 方法,根据实际的对象类型,调用了不同的实现。这就是 Java 多态的一个典型应用。 + + + + + + + + +--- + +> 这一章节掌握得怎么样?下一篇会更有趣哦,期待与你继续学习~ \ No newline at end of file diff --git a/docs/basic/26.md b/docs/basic/26.md new file mode 100644 index 000000000..0c7af4884 --- /dev/null +++ b/docs/basic/26.md @@ -0,0 +1,90 @@ +--- +title: 第26天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第26天 + +> 学习编程要劳逸结合哦,记得多喝水休息眼睛~ + +## Java 抽象类 + +1. 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。 + +2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。 + +3. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。 + +4. 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。 + +5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。 + + +Java 抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类通常包含抽象方法,这些方法只有声明而没有实现。子类必须实现这些抽象方法,否则子类也必须被声明为抽象类。 + +### 实现原理 + +1. **声明抽象类**:使用 `abstract` 关键字声明一个类为抽象类。 +2. **声明抽象方法**:在抽象类中声明抽象方法,这些方法只有方法签名,没有方法体。 +3. **继承抽象类**:其他类通过继承抽象类来获得抽象类的属性和方法。 +4. **实现抽象方法**:子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。 + +### 用途 + +1. **定义公共接口**:抽象类可以定义一组公共接口,这些接口可以被多个子类共享。 +2. **提供默认实现**:抽象类可以提供一些方法的默认实现,子类可以选择继承这些默认实现,也可以选择重写它们。 +3. **实现多态**:抽象类可以用于实现多态,通过继承抽象类的子类可以有不同的行为。 + +### 注意事项 + +1. **不能实例化**:抽象类不能被实例化,只能被继承。 +2. **抽象方法**:抽象类中可以包含抽象方法,也可以包含普通方法。 +3. **子类实现**:子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。 +4. **访问修饰符**:抽象类中的抽象方法默认是 `public` 的,也可以是 `protected` 的,但必须是 `public` 或 `protected`。 + +### 示例代码 + +```java +abstract class Animal { + // 抽象方法 + public abstract void makeSound(); + + // 普通方法 + public void eat() { + System.out.println("This animal eats food."); + } +} + +class Dog extends Animal { + // 实现抽象方法 + public void makeSound() { + System.out.println("Woof! Woof!"); + } +} + +class Cat extends Animal { + // 实现抽象方法 + public void makeSound() { + System.out.println("Meow! Meow!"); + } +} + +public class Main { + public static void main(String[] args) { + Animal myDog = new Dog(); + Animal myCat = new Cat(); + + myDog.makeSound(); // 输出: Woof! Woof! + myCat.makeSound(); // 输出: Meow! Meow! + } +} + +``` + +在这个示例中,`Animal` 是一个抽象类,它定义了一个抽象方法 `makeSound()` 和一个普通方法 `eat()`。`Dog` 和 `Cat` 类继承自 `Animal` 类,并实现了 `makeSound()` 方法。在 `Main` 类中,我们创建了 `Dog` 和 `Cat` 的实例,并调用了它们的 `makeSound()` 方法,展示了多态的使用。 + + +--- + +> 恭喜你又完成了一个知识点!下一篇文章已经在向你招手了~ \ No newline at end of file diff --git a/docs/basic/27.md b/docs/basic/27.md new file mode 100644 index 000000000..6d4d9d012 --- /dev/null +++ b/docs/basic/27.md @@ -0,0 +1,94 @@ +--- +title: 第27天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第27天 + +> 每一行代码都是你进步的见证,为你感到骄傲! + +## Java 封装 + +### 实现Java封装的步骤 + +1. 修改属性的可见性来限制对属性的访问(一般限制为private) + +```java +public class Person { + private String name; + private int age; +} +``` + +2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问 + +```java +public class Person{ + private String name; + private int age; + + public int getAge(){ + return age; + } + + public String getName(){ + return name; + } + + public void setAge(int age){ + this.age = age; + } + + public void setName(String name){ + this.name = name; + } +} +``` + + + + +Java封装是一种面向对象编程(OOP)的概念,它指的是将对象的状态(属性)和行为(方法)捆绑在一起,并对外隐藏对象的具体实现细节,仅通过定义的接口与外部进行交互。封装的主要目的是保护对象的状态,防止外部代码直接访问和修改对象内部的数据,从而提高代码的安全性和可维护性。 + +### 实现原理 + +1. **访问修饰符**:Java提供了四种访问修饰符来控制类的成员(属性和方法)的访问级别: + - `private`:仅当前类可以访问。 + - `default`(无修饰符):仅同一包内的类可以访问。 + - `protected`:同一包内的类和不同包中的子类可以访问。 + - `public`:所有类都可以访问。 + +2. **构造方法**:用于创建对象,并初始化对象的状态。 + +3. **getter和setter方法**:用于访问和修改私有属性。getter方法用于获取属性的值,setter方法用于设置属性的值。 + +### 用途 + +1. **数据隐藏**:通过将属性设为私有,外部代码无法直接访问和修改对象的状态,只能通过提供的方法进行操作,从而保护数据的安全性和完整性。 + +2. **代码重用**:封装后的类可以被多个对象实例化,并且可以重用,提高了代码的复用性。 + +3. **易于维护**:封装后的类内部实现细节被隐藏,外部代码只需关注接口,降低了代码的耦合度,使得代码更容易维护和修改。 + +### 注意事项 + +1. **合理使用访问修饰符**:根据实际需求选择合适的访问修饰符,避免过度暴露或过度限制访问。 + +2. **避免过度封装**:虽然封装可以提高代码的安全性和可维护性,但过度封装会导致代码难以理解和维护。因此,应该适度封装,保持代码的清晰和简洁。 + +3. **提供必要的接口**:封装后的类应该提供必要的接口(getter和setter方法),以便外部代码可以访问和修改对象的状态。 + +4. **异常处理**:在setter方法中,应该对输入参数进行验证,并在必要时抛出异常,以保证对象状态的有效性。 + +通过合理使用Java封装,可以提高代码的可读性、可维护性和安全性,是面向对象编程中非常重要的一个概念。 + + + + + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basic/28.md b/docs/basic/28.md new file mode 100644 index 000000000..3915e864b --- /dev/null +++ b/docs/basic/28.md @@ -0,0 +1,93 @@ +--- +title: 第28天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第28天 + + +## Java 接口 + +Java 接口(Interface)是一种引用类型,它是一种抽象的类型,用于定义一组方法,但不提供这些方法的具体实现。接口可以看作是抽象类的特殊形式,它只包含抽象方法(没有方法体)和静态常量(默认是 `public static final`)。接口的主要用途是定义一种协议或标准,使得不同的类可以实现相同的接口,从而实现多态性。 + +### 实现原理 + +1. **定义接口**:使用 `interface` 关键字定义接口,接口中的方法默认是 `public abstract` 的。 +2. **实现接口**:使用 `implements` 关键字让类实现接口,实现接口中的所有抽象方法。 +3. **多态性**:通过接口引用指向实现该接口的类的对象,从而实现多态性。 + +### 用途 + +1. **定义标准**:接口可以用来定义一组标准或协议,不同的类可以实现这些标准。 +2. **多态性**:通过接口可以实现多态性,使得代码更加灵活和可扩展。 +3. **解耦合**:接口可以降低类之间的耦合度,使得代码更容易维护和修改。 + +### 注意事项 + +1. **抽象方法**:接口中的方法默认是抽象的,不能有方法体。 +2. **静态常量**:接口中可以定义静态常量,这些常量默认是 `public static final` 的。 +3. **默认方法**:Java 8 引入了默认方法,允许在接口中提供方法的默认实现。 +4. **接口继承**:接口可以继承其他接口,并且可以继承多个接口。 +5. **实现类**:实现接口的类必须实现接口中的所有抽象方法,除非该类是抽象类。 + +### 示例代码 + +```java +// 定义一个接口 +interface Animal { + void eat(); + void sleep(); +} + +// 实现接口的类 +class Dog implements Animal { + @Override + public void eat() { + System.out.println("Dog is eating."); + } + + @Override + public void sleep() { + System.out.println("Dog is sleeping."); + } +} + +// 实现接口的类 +class Cat implements Animal { + @Override + public void eat() { + System.out.println("Cat is eating."); + } + + @Override + public void sleep() { + System.out.println("Cat is sleeping."); + } +} + +// 测试代码 +public class Main { + public static void main(String[] args) { + Animal dog = new Dog(); + Animal cat = new Cat(); + + dog.eat(); + dog.sleep(); + + cat.eat(); + cat.sleep(); + } +} + +``` + +### 总结 + +Java 接口是一种强大的工具,用于定义标准、实现多态性和解耦合。通过接口,可以使得代码更加灵活和可扩展,同时也提高了代码的可维护性。 + + + +--- + +> 今天的学习就到这里啦,明天继续我们的编程之旅吧! \ No newline at end of file diff --git a/docs/basic/29.md b/docs/basic/29.md new file mode 100644 index 000000000..fdc5aae23 --- /dev/null +++ b/docs/basic/29.md @@ -0,0 +1,75 @@ +--- +title: 第29天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第29天 + +> 每天进步一点点,你已经很棒了! + +## Java 枚举(enum) + +Java 枚举(enum)是一种特殊的类,用于定义一组固定的常量。枚举类型在Java中非常有用,特别是在需要表示一组固定的选项或状态时。使用枚举可以使代码更加清晰、易于维护,并且可以避免使用魔法数字或字符串常量。 + +### 实现原理 + +在Java中,枚举类型是通过关键字`enum`来定义的。每个枚举常量都是枚举类型的一个实例。枚举类型在编译时会自动生成一个类,该类继承自`java.lang.Enum`。 + +### 用法示例 + +```java +public enum Day { + MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY +} +``` + +### 常用方法 + +- `values()`: 返回一个包含所有枚举常量的数组。 +- `name()`: 返回枚举常量的名称。 +- `ordinal()`: 返回枚举常量的序数(从0开始)。 +- `valueOf(String name)`: 根据名称返回对应的枚举常量。 + +### 注意事项 + +1. **不可变性**: 枚举类型是不可变的,枚举常量在定义时就已经确定,不能在运行时修改。 +2. **线程安全**: 枚举类型是线程安全的,因为枚举常量在类加载时就已经初始化,并且只有一个实例。 +3. **序列化**: 枚举类型默认实现了`Serializable`接口,因此可以序列化。 +4. **枚举方法**: 枚举类型可以定义自己的方法,这些方法可以在枚举常量上调用。 + +### 示例代码 + +```java +public enum Color { + RED, GREEN, BLUE; + + public void printColor() { + System.out.println(this); + } +} + +public class EnumExample { + public static void main(String[] args) { + Color color = Color.RED; + color.printColor(); // 输出: RED + + for (Color c : Color.values()) { + System.out.println(c); + } + // 输出: + // RED + // GREEN + // BLUE + } +} +``` + +### 总结 + +Java枚举类型是一种非常有用的特性,用于定义一组固定的常量。它提供了一种类型安全的方式来表示一组固定的选项或状态,使代码更加清晰和易于维护。 + + +--- + +> 今天的学习就到这里啦,明天继续我们的编程之旅吧! \ No newline at end of file diff --git a/docs/basic/3.md b/docs/basic/3.md new file mode 100644 index 000000000..ab6aff7ba --- /dev/null +++ b/docs/basic/3.md @@ -0,0 +1,673 @@ +--- +title: 第3天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第3天 + +> 小可爱,每天进步一点点,你已经很棒了! + +> 😄 程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档。 + + +复习 + +Java 作为一种面向对象的编程语言,支持以下基本概念: + +1、类(Class): + +定义对象的蓝图,包括属性和方法。 + +示例:public class Car { ... } + +2、对象(Object): + +类的实例,具有状态和行为。 + +示例:Car myCar = new Car(); + +3、继承(Inheritance): + +一个类可以继承另一个类的属性和方法。 + +示例:public class Dog extends Animal { ... } + +4、封装(Encapsulation): + +将对象的状态(字段)私有化,通过公共方法访问。 + +示例: + +```java +private String name; +public String getName() { return name; } +``` + +5、多态(Polymorphism): + +对象可以表现为多种形态,主要通过**方法重载**和**方法重写**实现。 + +示例: + +方法重载:public int add(int a, int b) { ... } 和 public double add(double a, double b) { ... } + +方法重写:@Override public void makeSound() { System.out.println("Meow"); } + +6、抽象(Abstraction): + +使用抽象类和接口来定义必须实现的方法,不提供具体实现。 + +示例: + +抽象类:public abstract class Shape { abstract void draw(); } + +接口:public interface Animal { void eat(); } + +7、接口(Interface): + +定义类必须实现的方法,支持多重继承。 + +示例:public interface Drivable { void drive(); } + +8、方法(Method): + +定义类的行为,包含在类中的函数。 + +示例:public void displayInfo() { System.out.println("Info"); } + +9、方法重载(Method Overloading): + +同一个类中可以有多个同名的方法,但参数不同。 + +示例: + +定义了一个名为 MathUtils 的公共类,其中包含两个重载的 add 方法。 + +```java +//public class MathUtils 定义了一个名为 MathUtils 的公共类。公共类可以被其他类访问。 +//public int add(int a, int b) 定义了一个公共方法 add,该方法接受两个整数参数 a 和 b,并返回它们的和。 +//return a + b; 返回两个整数参数的和。 +//public double add(double a, double b) 定义了一个公共方法 add,该方法接受两个双精度浮点数参数 a 和 b,并返回它们的和。 +//return a + b; 返回两个双精度浮点数参数的和。 +public class MathUtils { + public int add(int a, int b) { + return a + b; + } + + public double add(double a, double b) { + return a + b; + } +} + +``` + +该类通过方法重载实现了对不同数据类型(整数和浮点数)的加法运算。 + +方法重载允许同一个类中存在多个同名方法,但它们的参数列表必须不同(参数的数量或类型不同)。 + +### 注意事项 + +1. 使用方法重载时,需要注意参数的类型和数量,以确保方法调用时能够正确匹配到相应的重载方法。 +2. 在实际应用中,可能需要考虑异常处理,例如参数为 null 的情况,但在这个示例中并没有处理。 +3. 如果需要支持更多数据类型,可以继续添加相应的方法重载。 + +对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。 + +类:类是一个模板,它描述一类对象的行为和状态。 + +男孩(boy)、女孩(girl)为类(class),而具体的每个人为该类的对象(object) + +![img_13.png](./img_13.png) + +一个类可以包含以下类型变量: + +- 局部变量:在方法、构造方法或语句块中定义的变量。 +- 成员变量:在类中定义的变量。 +- 常量:使用 final 关键字声明的变量。 +- 实例变量:在类中定义的变量,但在方法、构造方法或语句块之外。 +- 类变量:使用 static 关键字声明的变量,类变量也称为静态变量,它们在类加载时初始化,并且在所有实例之间共享。 + +示例: + +### 成员变量 + +1. 实例变量 myPrivateVar: + +- 类型:int +- 访问修饰符:private +- 作用域:仅限于类的实例,不能被类本身直接访问。 +- 初始化:通过构造函数 MyClass(int value) 进行初始化。 + +2. 类变量 myPublicStaticVar: + +- 类型:int +- 访问修饰符:public +- 修饰符:static +- 作用域:属于类本身,而不是类的实例。所有类的实例共享这个变量。 +- 初始化:通过静态方法 setMyPublicStaticVar(int value) 进行初始化。 + +### 构造函数 + +public MyClass(int value): +- 这是一个构造函数,用于创建 MyClass 类的实例,并初始化实例变量 myPrivateVar。 + + +```java +//定义了一个名为 MyClass 的 Java 类,它包含两个成员变量(一个实例变量和一个类变量),以及一些用于访问和修改这些变量的方法。 +public class MyClass { + private int myPrivateVar; // 实例变量 + public static int myPublicStaticVar; // 类变量 + + public MyClass(int value) { + this.myPrivateVar = value; + } + + public int getMyPrivateVar() { + return myPrivateVar; + } + + public static void setMyPublicStaticVar(int value) { + myPublicStaticVar = value; + } + + public static int getMyPublicStaticVar() { + return myPublicStaticVar; + } +} +``` + +### 方法 + +1. getMyPrivateVar(): + +- 这是一个实例方法,用于获取实例变量 myPrivateVar 的值。 +- 返回类型:int +- 访问修饰符:public +- 作用域:可以被类的实例调用。 + +2. setMyPublicStaticVar(int value): + +- 这是一个静态方法,用于设置类变量 myPublicStaticVar 的值。 +- 参数:int value +- 返回类型:void +- 访问修饰符:public +- 作用域:可以被类的任何实例或类本身调用。 + +3. getMyPublicStaticVar(): + +- 这是一个静态方法,用于获取类变量 myPublicStaticVar 的值。 +- 返回类型:int +- 访问修饰符:public +- 作用域:可以被类的任何实例或类本身调用。 + +## 笔记 + +- private 修饰符确保了实例变量 myPrivateVar 的封装性,只能通过类内部的方法来访问和修改。 +- public 修饰符确保了类变量 myPublicStaticVar 可以被外部类访问。 +- static 修饰符使得类变量 myPublicStaticVar 与类的实例无关,而是属于类本身。 +- 构造函数用于初始化实例变量,而静态方法用于初始化和访问类变量。 + +局部变量,示例 + +```java +//public:这是访问修饰符,表示这个类可以被任何其他类访问。 +//class:这是定义类的关键字。 +//MyClass:这是类的名称,按照Java的命名规范,类名通常使用大驼峰命名法。 + +//int:这是变量的数据类型,表示这是一个整数类型。 +//myLocalVar:这是变量的名称,按照Java的命名规范,变量名通常使用小驼峰命名法。 +//= 10:这是变量的初始化,表示将变量 myLocalVar 初始化为10。 + +//System.out.println:这是Java中的一个标准输出方法,用于在控制台打印输出。 +//myLocalVar:这是要打印的变量,这里打印的是 myLocalVar 的值。 +public class MyClass { + public void myMethod() { + int myLocalVar = 10; // 局部变量 + System.out.println(myLocalVar); + } +} +``` + +### 笔记 + +- 局部变量只在定义它的方法内可见,方法结束后局部变量会被销毁。 +- Java的命名规范要求类名使用大驼峰命名法,方法名和变量名使用小驼峰命名法。 +- 注释有助于提高代码的可读性和可维护性,但不应包含任何会影响程序执行的代码。 + +常量,示例 + +```java +//public static final int MY_CONSTANT = 10;:定义了一个名为MY_CONSTANT的常量,类型为int,值为10。public表示该常量可以被其他类访问,static表示该常量属于类本身而不是类的实例,final表示该常量的值在初始化后不能被修改。 +public class MyClass { + public static final int MY_CONSTANT = 10; // 常量 + + public static void main(String[] args) { + System.out.println(MyClass.MY_CONSTANT); + } +} +//public static void main(String[] args):定义了Java应用程序的入口点。public表示该方法可以被其他类访问,static表示该方法属于类本身而不是类的实例,void表示该方法没有返回值,main是方法名,String[] args是方法的参数,用于接收命令行参数。 +// System.out.println(MyClass.MY_CONSTANT);:使用System.out.println方法打印常量MY_CONSTANT的值。MyClass.MY_CONSTANT表示访问MyClass类中的MY_CONSTANT常量。 +``` + +### 笔记 + +- 常量名通常使用全大写字母,单词之间用下划线分隔,以增加可读性。 +- 常量一旦被定义,其值就不能被修改。 +- 常量通常用于表示程序中的固定值,如数学常数、配置参数等。 + +方法,示例 + +```java +public class MyClass { + public void myMethod() { + System.out.println("Hello, World!"); + } + + public int add(int a, int b) { // 方法 + return a + b; + } +} +``` + +构造方法,示例 + +```java +public class MyClass { + private int myVar; + + public MyClass(int value) { + this.myVar = value; + } + + public int getMyVar() { + return myVar; + } +} +``` + +访问修饰符,示例 + +```java +public class MyClass { + private int myPrivateVar; // 私有变量 + public int myPublicVar; // 公共变量 + + public void myMethod() { + System.out.println("Hello, World!"); + } +} +``` + +成员变量,示例 + +```java +public class MyClass { + private int myVar; // 成员变量 + + public MyClass(int value) { + this.myVar = value; + } + + public int getMyVar() { + return myVar; + } +} +``` + +类变量,示例 + +```java +public class MyClass { + public static int myVar; // 类变量 + + public static void main(String[] args) { + MyClass.myVar = 10; + System.out.println(MyClass.myVar); + } +} +``` + +## 构造方法 + +每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。 + +在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。 + +构造方法可以带有参数,也可以不带参数。构造方法的基本形式如下: + +```java +public class MyClass { + private int myVar; + + public MyClass(int value) { + this.myVar = value; + } + + public int getMyVar() { + return myVar; + } +} +``` + +```java +public class Puppy{ + public Puppy(String name){ + //这个构造器仅有一个参数:name + System.out.println("名字是 : " + name ); + } + public static void main(String[] args){ + // 下面的语句将创建一个Puppy对象 + Puppy myPuppy = new Puppy( "吃鱼不挑刺" ); + } +} +``` + +## 访问修饰符 + +Java 提供了以下四种访问修饰符: +- default:默认,不使用任何关键字。 +- private:私有的,只能在当前类中访问。 +- protected:受保护的,可以在当前类、同一个包中的类以及不同包中的子类中访问。 +- public:公共的,可以在任何地方访问。 + +## 访问实例变量和方法 + +```java +/* 实例化对象 */ +Object referenceVariable = new Constructor(); +/* 访问类中的变量 */ +referenceVariable.variableName; +/* 访问类中的方法 */ +referenceVariable.methodName(); +``` + +通过已创建的对象来访问成员变量和成员方法,示例 + +```java +public class MyClass { + private int myVar; + + public MyClass(int value) { + this.myVar = value; + } + + public int getMyVar() { + return myVar; + } +} + +public class Main { + public static void main(String[] args) { + MyClass myObject = new MyClass(10); + System.out.println(myObject.getMyVar()); + } +} +``` + +## 继承 + +继承是面向对象编程的一个基本概念,它允许我们定义一个类(子类)继承另一个类(父类)的属性和方法。 + +继承的语法如下: + +```java +public class 子类 extends 父类 { + // 子类的代码 +} +``` + +示例: + +```java +public class Animal { + public void eat() { + System.out.println("Animal is eating"); + } +} + +public class Dog extends Animal { + public void bark() { + System.out.println("Dog is barking"); + } +} + +public class Main { + public static void main(String[] args) { + Dog myDog = new Dog(); + myDog.eat(); // 调用父类的方法 + myDog.bark(); // 调用子类的方法 + } +} +``` + +## 多态 + +多态是面向对象编程的一个基本概念,它允许我们定义一个类(子类)继承另一个类(父类)的属性和方法。 + +多态的语法如下: + +```java +public class 子类 extends 父类 { + // 子类的代码 +} +``` + +示例: + +```java +public class Animal { + public void makeSound() { + System.out.println("Animal is making a sound"); + } +} + +public class Dog extends Animal { + @Override + public void makeSound() { + System.out.println("Dog is barking"); + } +} + +public class Cat extends Animal { + @Override + public void makeSound() { + System.out.println("Cat is meowing"); + } +} + +public class Main { + public static void main(String[] args) { + Animal myAnimal = new Animal(); // 创建一个Animal对象 + Animal myDog = new Dog(); // 创建一个Dog对象 + Animal myCat = new Cat(); // 创建一个Cat对象 + myAnimal.makeSound(); // 调用Animal的makeSound方法 + myDog.makeSound(); // 调用Dog的makeSound方法 + myCat.makeSound(); // 调用Cat的makeSound方法 + } +} +``` + +实例 + +下面的例子展示如何访问实例变量和调用成员方法: + +```java +public class Puppy { + private int age; + private String name; + + // 构造器 + public Puppy(String name) { + this.name = name; + System.out.println("小狗的名字是 : " + name); + } + + // 设置 age 的值 + public void setAge(int age) { + this.age = age; + } + + // 获取 age 的值 + public int getAge() { + return age; + } + + // 获取 name 的值 + public String getName() { + return name; + } + + // 主方法 + public static void main(String[] args) { + // 创建对象 + Puppy myPuppy = new Puppy("Tommy"); + + // 通过方法来设定 age + myPuppy.setAge(2); + + // 调用另一个方法获取 age + int age = myPuppy.getAge(); + System.out.println("小狗的年龄为 : " + age); + + // 也可以直接访问成员变量(通过 getter 方法) + System.out.println("变量值 : " + myPuppy.getAge()); + } +} + + +``` + +编译并运行上面的程序,产生如下结果: + +```java +小狗的名字是 : tommy +小狗的年龄为 : 2 +变量值 : 2 + +``` + +## 源文件声明规则 + +- 一个源文件中只能有一个 public 类 +- 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为 Employee.java。 +- 如果源文件中只有一个没有 public 修饰的类,那么源文件的名称可以是任意的,比如 MyEmployee.java。 +- 一个源文件可以有多个非 public 类,也可以有 public 类。如果有 public 类,那么源文件的名称应该和 public 类的类名保持一致。 +- 源文件中 public 类的访问修饰符只能是 public 或者默认的包访问权限,不能是 private 或者 protected。 + +## 规则 + +1. 一个源文件中只能有一个 public 类 +2. 一个源文件可以有多个非 public 类 +3. 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为Employee.java。 +4. 如果一个类定义在某个包中,那么 package 语句应该在源文件的首行。 +5. 如果源文件包含 import 语句,那么应该放在 package 语句和类定义之间。如果没有 package 语句,那么 import 语句应该在源文件中最前面。 +6. import 语句和 package 语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。 + +## Java 包 + +包主要用来对类和接口进行分类。当开发 Java 程序时,可能编写成百上千的类,因此很有必要对类和接口进行分类。 + +## import 语句 + +在 Java 中,如果给出一个完整的限定名,包括包名、类名,那么 Java 编译器就可以很容易地定位到源代码或者类。import 语句就是用来提供一个合理的路径,使得编译器可以找到某个类。 + +例如,下面的命令行将会命令编译器载入 java_installation/java/io 路径下的所有类 + +```java +import java.io.*; +``` + +## 简单的例子 + +Employee.java 文件代码: + +```java +import java.io.*; + +public class Employee { + private String name; + private int age; + private String designation; + private double salary; + + // Employee 类的构造器 + public Employee(String name) { + this.name = name; + } + + // 设置 age 的值 + public void setAge(int age) { + this.age = age; + } + + // 获取 age 的值 + public int getAge() { + return age; + } + + // 设置 designation 的值 + public void setDesignation(String designation) { + this.designation = designation; + } + + // 获取 designation 的值 + public String getDesignation() { + return designation; + } + + // 设置 salary 的值 + public void setSalary(double salary) { + this.salary = salary; + } + + // 获取 salary 的值 + public double getSalary() { + return salary; + } + + // 打印信息 + public void printEmployee() { + System.out.println(this); + } + + // 重写 toString 方法 + @Override + public String toString() { + return "名字: " + name + "\n" + + "年龄: " + age + "\n" + + "职位: " + designation + "\n" + + "薪水: " + salary; + } +} +``` + +EmployeeTest.java 文件代码: + +```java +import java.io.*; + +public class EmployeeTest { + public static void main(String[] args) { + // 使用构造器创建两个对象 + Employee empOne = new Employee("Da1"); + Employee empTwo = new Employee("Da2"); + + // 调用这两个对象的成员方法 + empOne.setAge(26); + empOne.setDesignation("高级程序员"); + empOne.setSalary(1000); + empOne.printEmployee(); + + empTwo.setAge(21); + empTwo.setDesignation("菜鸟程序员"); + empTwo.setSalary(500); + empTwo.printEmployee(); + } +} +``` + + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basic/30.md b/docs/basic/30.md new file mode 100644 index 000000000..8a306cdc4 --- /dev/null +++ b/docs/basic/30.md @@ -0,0 +1,77 @@ +--- +title: 第30天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第30天 + +达达JAVA写法思路,这部分学完恭喜结束了,你已经很厉害了,基础,打好基础。 + +## Java 反射(Reflection) + +Java 反射(Reflection)是 Java 语言的一个特性,它允许程序在运行时访问、检查和修改其自身的结构。反射机制提供了一种强大的工具,可以动态地创建对象、调用方法、访问属性,甚至可以处理注解和泛型。以下是 Java 反射的一些关键概念、用途和注意事项: + +### 关键概念 + +1. **Class 对象**:每个类都有一个与之对应的 Class 对象,它包含了类的元数据,如类的名称、修饰符、字段、方法和构造函数等。 +2. **获取 Class 对象**: + - 通过 `Class.forName("className")` 获取类的 Class 对象。 + - 通过 `className.class` 获取类的 Class 对象。 + - 通过 `obj.getClass()` 获取对象的 Class 对象。 +3. **实例化对象**:通过 `Class.newInstance()` 或 `Constructor.newInstance()` 可以创建类的实例。 +4. **访问字段**:通过 `Field` 对象可以获取和修改类的字段值。 +5. **调用方法**:通过 `Method` 对象可以调用类的实例方法。 +6. **获取构造函数**:通过 `Constructor` 对象可以获取类的构造函数。 + +### 用途 + +1. **开发框架**:反射在许多 Java 框架中广泛使用,如 Spring、Hibernate 等,用于动态地加载和配置类。 +2. **测试工具**:测试框架(如 JUnit)使用反射来动态调用测试方法。 +3. **工具类**:一些工具类(如 Java 反射工具包)利用反射来简化代码,如动态代理、序列化等。 +4. **插件系统**:反射可以用于实现插件系统,插件可以在运行时动态加载和执行。 + +### 注意事项 + +1. **性能开销**:反射操作通常比直接代码调用要慢,因为它们需要额外的类型检查和动态方法查找。 +2. **安全问题**:反射可以访问和修改私有成员,这可能导致安全问题,特别是在处理不受信任的代码时。 +3. **代码可读性**:使用反射的代码通常难以理解和维护,因为它打破了代码的封装性。 +4. **兼容性**:反射依赖于 JVM 的实现,不同的 JVM 可能会有不同的行为,这可能导致兼容性问题。 + +### 示例代码 + +以下是一个简单的反射示例,展示了如何获取类的 Class 对象、创建实例、访问字段和调用方法: + +```java +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class ReflectionExample { + public static void main(String[] args) throws Exception { + // 获取类的 Class 对象 + Class clazz = Class.forName("java.util.ArrayList"); + + // 创建实例 + Object list = clazz.getDeclaredConstructor().newInstance(); + + // 获取字段 + Field sizeField = clazz.getDeclaredField("size"); + sizeField.setAccessible(true); // 设置可访问私有字段 + + // 调用方法 + Method addMethod = clazz.getDeclaredMethod("add", Object.class); + addMethod.invoke(list, "Hello"); + + // 访问字段 + System.out.println("Size: " + sizeField.get(list)); + } +} + +``` + +这个示例展示了如何使用反射来操作 `ArrayList` 类,包括获取类的 Class 对象、创建实例、访问私有字段和调用方法。 + + +--- + +> 今天的内容消化得如何?下一篇会带来更多精彩内容哦~ \ No newline at end of file diff --git a/docs/basic/4.md b/docs/basic/4.md new file mode 100644 index 000000000..0e4f15d62 --- /dev/null +++ b/docs/basic/4.md @@ -0,0 +1,384 @@ +--- +title: 第4天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第4天 + +> 学习要循序渐进,不要给自己太大压力哦! + +> 为什么程序员喜欢用黑色主题?因为光明会暴露他们的bug! + + +java中的基础数据类型,也就是数据,数据库存储数据类型,看看需要什么样的数据 无需强制去记忆 + +## 基本数据类型 + +变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。 + +内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。 + +![img_14.png](./img_14.png) + +因此,通过定义不同类型的变量,可以在内存中储存整数、小数或者字符。 + +## Java 的两大数据类型 + +内置数据类型 + +引用数据类型 + +## 内置数据类型 + +Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 + +byte: + +```java +最小值是 -128(-2^7); +最大值是 127(2^7-1); +默认值是 0; +byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; +例子:byte a = 100,byte b = -50。 +``` + +byte 数据类型是8位、有符号的,以二进制补码表示的整数; + +### 二进制补码是什么 + +二进制补码是一种用于表示有符号整数的方法,它允许我们使用二进制数来表示正数和负数。在二进制补码中,最高位(最左边的一位)被用作符号位,0表示正数,1表示负数。其余位用于表示数值的大小。 + +例如,假设我们有一个8位的二进制数,那么最高位就是符号位,其余7位用于表示数值。如果我们想要表示-5,我们可以使用二进制补码来表示它。首先,我们将5转换为二进制数,得到101。然后,我们将这个二进制数取反,得到010。最后,我们在取反后的结果上加1,得到011。因此,-5的二进制补码表示就是011。 + +二进制补码的一个重要特性是,它使得加法和减法操作可以统一处理。例如,如果我们想要计算5+(-3),我们可以将-3的二进制补码表示010转换为正数,得到3。然后,我们将5和3相加,得到8。因此,5+(-3)的结果就是8。 + +二进制补码的另一个重要特性是,它使得负数的表示是唯一的。例如,-5的二进制补码表示是011,而-6的二进制补码表示是100。因此,我们可以通过二进制补码来唯一地表示所有的负数。 + +总的来说,二进制补码是一种用于表示有符号整数的方法,它允许我们使用二进制数来表示正数和负数,并且使得加法和减法操作可以统一处理。 + +short: + +```java +short 数据类型是 16 位、有符号的以二进制补码表示的整数 +最小值是 -32768(-2^15); +最大值是 32767(2^15 - 1); +Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一; +默认值是 0; +例子:short s = 1000,short r = -20000。 +``` + +int: + +```java +int 数据类型是32位、有符号的以二进制补码表示的整数; +最小值是 -2,147,483,648(-2^31); +最大值是 2,147,483,647(2^31 - 1); +一般地整型变量默认为 int 类型; +默认值是 0 ; +例子:int a = 100000, int b = -200000。 +``` + +long: + +```java +long 数据类型是 64 位、有符号的以二进制补码表示的整数; +最小值是 -9,223,372,036,854,775,808(-2^63); +最大值是 9,223,372,036,854,775,807(2^63 -1); +这种类型主要使用在需要比较大整数的系统上; +默认值是 0L; +例子: long a = 100000L,long b = -200000L。 +"L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。 +``` + +float: + +```java +float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; +float 在储存大型浮点数组的时候可节省内存空间; +默认值是 0.0f; +浮点数不能用来表示精确的值,如货币; +例子:float f1 = 234.5f。 +``` + +double: + +```java +double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数; +浮点数的默认类型为 double 类型; +double类型同样不能表示精确的值,如货币; +默认值是 0.0d; +``` + +boolean: + +```java +boolean数据类型表示一位的信息; +只有两个取值:true 和 false; +这种类型只作为一种标志来记录 true/false 情况; +默认值是 false; +例子:boolean one = true。 +``` + +char: + +```java +char 类型是一个单一的 16 位 Unicode 字符; +最小值是 \u0000(十进制等效值为 0); +最大值是 \uffff(即为 65535); +char 数据类型可以储存任何字符; +例子:char letter = 'A';。 +``` + +## 实例 + +```java +public class PrimitiveTypeTest { + public static void main(String[] args) { + // byte + System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE); + System.out.println("包装类:java.lang.Byte"); + System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE); + System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE); + System.out.println(); + + // short + System.out.println("基本类型:short 二进制位数:" + Short.SIZE); + System.out.println("包装类:java.lang.Short"); + System.out.println("最小值:Short.MIN_VALUE=" + Short.MIN_VALUE); + System.out.println("最大值:Short.MAX_VALUE=" + Short.MAX_VALUE); + System.out.println(); + + // int + System.out.println("基本类型:int 二进制位数:" + Integer.SIZE); + System.out.println("包装类:java.lang.Integer"); + System.out.println("最小值:Integer.MIN_VALUE=" + Integer.MIN_VALUE); + System.out.println("最大值:Integer.MAX_VALUE=" + Integer.MAX_VALUE); + System.out.println(); + + // long + System.out.println("基本类型:long 二进制位数:" + Long.SIZE); + System.out.println("包装类:java.lang.Long"); + System.out.println("最小值:Long.MIN_VALUE=" + Long.MIN_VALUE); + System.out.println("最大值:Long.MAX_VALUE=" + Long.MAX_VALUE); + System.out.println(); + + // float + System.out.println("基本类型:float 二进制位数:" + Float.SIZE); + System.out.println("包装类:java.lang.Float"); + System.out.println("最小值:Float.MIN_VALUE=" + Float.MIN_VALUE); + System.out.println("最大值:Float.MAX_VALUE=" + Float.MAX_VALUE); + System.out.println(); + + // double + System.out.println("基本类型:double 二进制位数:" + Double.SIZE); + System.out.println("包装类:java.lang.Double"); + System.out.println("最小值:Double.MIN_VALUE=" + Double.MIN_VALUE); + System.out.println("最大值:Double.MAX_VALUE=" + Double.MAX_VALUE); + System.out.println(); + + // char + System.out.println("基本类型:char 二进制位数:" + Character.SIZE); + System.out.println("包装类:java.lang.Character"); + // 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台 + System.out.println("最小值:Character.MIN_VALUE=" + + (int) Character.MIN_VALUE); + // 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台 + System.out.println("最大值:Character.MAX_VALUE=" + + (int) Character.MAX_VALUE); + } +} +``` + +```java +基本类型:byte 二进制位数:8 +包装类:java.lang.Byte +最小值:Byte.MIN_VALUE=-128 +最大值:Byte.MAX_VALUE=127 + +基本类型:short 二进制位数:16 +包装类:java.lang.Short +最小值:Short.MIN_VALUE=-32768 +最大值:Short.MAX_VALUE=32767 + +基本类型:int 二进制位数:32 +包装类:java.lang.Integer +最小值:Integer.MIN_VALUE=-2147483648 +最大值:Integer.MAX_VALUE=2147483647 + +基本类型:long 二进制位数:64 +包装类:java.lang.Long +最小值:Long.MIN_VALUE=-9223372036854775808 +最大值:Long.MAX_VALUE=9223372036854775807 + +基本类型:float 二进制位数:32 +包装类:java.lang.Float +最小值:Float.MIN_VALUE=1.4E-45 +最大值:Float.MAX_VALUE=3.4028235E38 + +基本类型:double 二进制位数:64 +包装类:java.lang.Double +最小值:Double.MIN_VALUE=4.9E-324 +最大值:Double.MAX_VALUE=1.7976931348623157E308 + +基本类型:char 二进制位数:16 +包装类:java.lang.Character +最小值:Character.MIN_VALUE=0 +最大值:Character.MAX_VALUE=65535 + +``` + +## 类型默认值 + +```java +数据类型 默认值 +int 0 +long 0L +short 0 +char '\u0000' +byte 0 +float 0.0f +double 0.0d +boolean false +引用类型(类、接口、数组) null +``` + +说明: + +1. int, short, long, byte 的默认值是0。 +2. char 的默认值是 \u0000(空字符)。 +3. float 的默认值是 0.0f。 +4. double 的默认值是 0.0d。 +5. boolean 的默认值是 false。 +6. 引用类型(类、接口、数组)的默认值是 null。 + +### 实例 + +```java +public class Test { + static boolean bool; + static byte by; + static char ch; + static double d; + static float f; + static int i; + static long l; + static short sh; + static String str; + + public static void main(String[] args) { + System.out.println("Bool :" + bool); + System.out.println("Byte :" + by); + System.out.println("Character:" + ch); + System.out.println("Double :" + d); + System.out.println("Float :" + f); + System.out.println("Integer :" + i); + System.out.println("Long :" + l); + System.out.println("Short :" + sh); + System.out.println("String :" + str); + } +} +``` + +实例输出结果为: + +```java +Bool :false +Byte :0 +Character: +Double :0.0 +Float :0.0 +Integer :0 +Long :0 +Short :0 +String :null +``` + +## 引用类型 + +在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。 + +对象、数组都是引用数据类型。 + +所有引用类型的默认值都是null。 + +一个引用变量可以用来引用任何与之兼容的类型。 + +例子:Site site = new Site("Da")。 + +## Java 常量 + +常量在程序运行时是不能被修改的。 + +在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似: + +```java +final double PI = 3.1415927; +``` + +## 自动类型转换 + +转换从低级到高级。 + +```java +低 ------------------------------------> 高 + +byte,short,char—> int —> long—> float —> double + +``` + +数据类型转换必须满足如下规则: + +1. 不能对boolean类型进行类型转换。 + +2. 不能把对象类型转换成不相关类的对象。 + +3. 在把容量大的类型转换为容量小的类型时必须使用强制类型转换。 + +4. 转换过程中可能导致溢出或损失精度,例如: + +```java +int i =128; +byte b = (byte)i; + +``` + +因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出。 + +5. 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入,例如: + +```java +(int)23.7 == 23; +(int)-45.89f == -45 + +``` + +## 自动类型转换 + +必须满足转换前的数据类型的位数要低于转换后的数据类型,例如: short数据类型的位数为16位,就可以自动转换位数为32的int类型,同样float数据类型的位数为32,可以自动转换为64位的double类型。 + +### 强制类型转换 + +实例 + +```java +public class ForceTransform { + public static void main(String[] args){ + int i1 = 123; + byte b = (byte)i1;//强制类型转换为byte + System.out.println("int强制类型转换为byte后的值等于"+b); + } +} +``` + +## 隐含强制类型转换 + +1、 整数的默认类型是 int。 + +2、 小数默认是 double 类型浮点型,在定义 float 类型时必须在数字后面跟上 F 或者 f。 + + + +--- + +> 恭喜你又完成了一个知识点!下一篇文章已经在向你招手了~ \ No newline at end of file diff --git a/docs/basic/5.md b/docs/basic/5.md new file mode 100644 index 000000000..d803db840 --- /dev/null +++ b/docs/basic/5.md @@ -0,0 +1,755 @@ +--- +title: 第5天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第5天 + +> 学习新知识很棒,但也要注意保护好自己的身体哦~ + +> 为什么程序员不喜欢户外活动?因为外面没有WiFi,而且太阳光太刺眼,影响看屏幕! + + +看了这么久,休息一下吧,是不是很简单,有疑问可以提出来呢? 1,2,3,4天内容看熟练就行,坚持多看多看多看,记得喝水,多休息 + +在 Java 语言中,所有的变量在使用前必须声明。 + +## 变量的声明实例 + +```java +int a, b, c; // 声明三个int型整数:a、 b、c +int d = 3, e = 4, f = 5; // 声明三个整数并赋予初值 +byte z = 22; // 声明并初始化 z +String s = "Da"; // 声明并初始化字符串 s +double pi = 3.14159; // 声明了双精度浮点型变量 pi +char x = 'x'; // 声明变量 x 的值是字符 'x'。 +``` + +Java 语言支持的变量类型有:(学过就当复习了,多复习就好) + +1. 局部变量(Local Variables):局部变量是在方法、构造函数或块内部声明的变量,它们在声明的方法、构造函数或块执行结束后被销毁,局部变量在声明时需要初始化,否则会导致编译错误。 + +```java +public void exampleMethod() { + int localVar = 10; // 局部变量 + // ... +} +``` + +2. 实例变量(Instance Variables):实例变量是在类中声明,但在方法、构造函数或块之外,它们属于类的实例,每个类的实例都有自己的副本,如果不明确初始化,实例变量会被赋予默认值(数值类型为0,boolean类型为false,对象引用类型为null)。 + +```java +public class ExampleClass { + int instanceVar; // 实例变量 +} +``` + +3. 静态变量或类变量(Class Variables):类变量是在类中用 static 关键字声明的变量,它们属于类而不是实例,所有该类的实例共享同一个 类变量 的值,类变量 在类加载时被初始化,而且只初始化一次。 + +```java +public class ExampleClass { + static int classVar; // 类变量 +} +``` + +4. 参数变量(Parameters):参数是方法或构造函数声明中的变量,用于接收调用该方法或构造函数时传递的值,参数变量的作用域只限于方法内部。 + +```java +public void exampleMethod(int parameterVar) { + // 参数变量 + // ... +} +``` + +## 实例 + +```java +public class DaTest { + // 成员变量 + private int instanceVar; + // 静态变量 + private static int staticVar; + + public void method(int paramVar) { + // 局部变量 + int localVar = 10; + + // 使用变量 + instanceVar = localVar; + staticVar = paramVar; + + System.out.println("成员变量: " + instanceVar); + System.out.println("静态变量: " + staticVar); + System.out.println("参数变量: " + paramVar); + System.out.println("局部变量: " + localVar); + } + + public static void main(String[] args) { + DaTest v = new DaTest(); + v.method(20); + } +} +``` + +运行以上代码,输出如下: + +```java +成员变量: 10 +静态变量: 20 +参数变量: 20 +局部变量: 10 +``` + +## Java 参数变量 + +Java 中的参数变量是指在方法或构造函数中声明的变量,用于接收传递给方法或构造函数的值。参数变量与局部变量类似,但它们只在方法或构造函数被调用时存在,并且只能在方法或构造函数内部使用。 + +```java +accessModifier returnType methodName(parameterType parameterName1, parameterType parameterName2, ...) { + // 方法体 +} +``` + +parameterType -- 表示参数变量的类型。 + +parameterName -- 表示参数变量的名称。 + +在调用方法时,我们必须为参数变量传递值,这些值可以是常量、变量或表达式。 + +方法参数变量的值传递方式有两种:值传递和引用传递。 + +## 值传递 + +值传递:在方法调用时,传递的是实际参数的值的副本。当参数变量被赋予新的值时,只会修改副本的值,不会影响原始值。Java 中的基本数据类型都采用值传递方式传递参数变量的值。 + +## 引用传递 + +引用传递:在方法调用时,传递的是实际参数的引用(即内存地址)。当参数变量被赋予新的值时,会修改原始值的内容。Java 中的对象类型采用引用传递方式传递参数变量的值。 + +## 方法参数变量的使用: + +```java +public class DaTest { + public static void main(String[] args) { + int a = 10, b = 20; + swap(a, b); // 调用swap方法 + System.out.println("a = " + a + ", b = " + b); // 输出a和b的值 + } + + public static void swap(int x, int y) { + int temp = x; + x = y; + y = temp; + } +} +``` + +运行以上代码,输出如下: + +a = 10, b = 20 + +## Java 局部变量 + +示例 +```java +public class DaTest { + public static void main(String[] args) { + int a = 10; // 局部变量 + System.out.println("a = " + a); + } +} +``` + +运行以上代码,输出如下: + +a = 10 + +## Java 实例变量 + +实例变量是在类中声明,但在方法、构造函数或块之外,它们属于类的实例,每个类的实例都有自己的副本,如果不明确初始化,实例变量会被赋予默认值(数值类型为0,boolean类型为false,对象引用类型为null)。 + +```java +public class DaTest { + int a = 10; // 实例变量 + public static void main(String[] args) { + DaTest obj = new DaTest(); + System.out.println("a = " + obj.a); + } +} +``` + +运行以上代码,输出如下: + +a = 10 + +## Java 静态变量 + +静态变量是在类中用 static 关键字声明的变量,它们属于类而不是实例,所有该类的实例共享同一个 类变量 的值,类变量 在类加载时被初始化,而且只初始化一次。 + +```java +public class DaTest { + static int a = 10; // 静态变量 + public static void main(String[] args) { + System.out.println("a = " + a); + } +} +``` + +运行以上代码,输出如下: + +a = 10 + +## 说明 + +作用域:局部变量的作用域限于它被声明的方法、构造方法或代码块内。一旦代码执行流程离开这个作用域,局部变量就不再可访问。 + +生命周期:局部变量的生命周期从声明时开始,到方法、构造方法或代码块执行结束时终止。之后,局部变量将被垃圾回收。 + +初始化:局部变量在使用前必须被初始化。如果不进行初始化,编译器会报错,因为 Java 不会为局部变量提供默认值。 + +声明:局部变量的声明必须在方法或代码块的开始处进行。声明时可以指定数据类型,后面跟着变量名,例如:int count;。 + +赋值:局部变量在声明后必须被赋值,才能在方法内使用。赋值可以是直接赋值,也可以是通过方法调用或表达式。 + +限制:局部变量不能被类的其他方法直接访问,它们只为声明它们的方法或代码块所私有。 + +内存管理:局部变量存储在 Java 虚拟机(JVM)的栈上,与存储在堆上的实例变量或对象不同。 + +垃圾回收:由于局部变量的生命周期严格限于方法或代码块的执行,它们在方法或代码块执行完毕后不再被引用,因此JVM的垃圾回收器会自动回收它们占用的内存。 + +重用:局部变量的名称可以在不同的方法或代码块中重复使用,因为它们的作用域是局部的,不会引起命名冲突。 + +参数和返回值:方法的参数可以视为一种特殊的局部变量,它们在方法被调用时初始化,并在方法返回后生命周期结束。 + +**局部变量存储在 Java 虚拟机(JVM)的栈上** + +**存储在堆上的实例变量或对象** + +## 实例 + +```java +public class LocalVariablesExample { + public static void main(String[] args) { + int a = 10; // 局部变量a的声明和初始化 + int b; // 局部变量b的声明 + b = 20; // 局部变量b的初始化 + + System.out.println("a = " + a); + System.out.println("b = " + b); + + // 如果在使用之前不初始化局部变量,编译器会报错 + // int c; + // System.out.println("c = " + c); + } +} +``` + +### 作用域 + +```java +package com.Da.test; + +public class Test{ + public void pupAge(){ + int age = 0; + age = age + 7; + System.out.println("小狗的年龄是: " + age); + } + + public static void main(String[] args){ + Test test = new Test(); + test.pupAge(); + } +} +``` + +以上实例编译运行结果如下: + +小狗的年龄是: 7 + +### 变量没有初始化 + +所以在编译时会出错 + +```java +package com.Da.test; + +public class Test{ + public void pupAge(){ + int age; + age = age + 7; + System.out.println("小狗的年龄是 : " + age); + } + + public static void main(String[] args){ + Test test = new Test(); + test.pupAge(); + } +} +``` + +以上实例编译运行结果如下 + +```java +Test.java:4:variable number might not have been initialized +age = age + 7; + ^ +1 error + +``` + +## 成员变量(实例变量) + +成员变量声明在一个类中,但在方法、构造方法和语句块之外。示例 + +```java +public class DaTest { + int a = 10; // 成员变量a + public static void main(String[] args) { + DaTest obj = new DaTest(); + System.out.println("a = " + obj.a); // 输出10 + } +} +``` + +运行以上代码,输出如下: + +a = 10 + +当一个对象被实例化之后,每个成员变量的值就跟着确定。示例 + +```java +public class DaTest { + int a = 10; // 成员变量a + public static void main(String[] args) { + DaTest obj = new DaTest(); + obj.a = 20; + System.out.println("a = " + obj.a); // 输出20 + } +} +``` + +成员变量在对象创建的时候创建,在对象被销毁的时候销毁。示例 + +```java +public class MyClass { + private int myVariable; + + public MyClass(int value) { + myVariable = value; + } + + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10); + System.out.println(obj.getMyVariable()); // 输出:10 + + obj = null; // 释放对象 + System.gc(); // 建议垃圾回收器回收对象 + } +} + +``` + +在这个示例中,myVariable是一个成员变量,它在MyClass对象的创建时被初始化。在main方法中,我们创建了一个MyClass对象obj,并将它的myVariable设置为10。然后,我们将obj设置为null,这会使得obj不再引用原来的对象。最后,我们调用System.gc()来建议垃圾回收器回收对象。当垃圾回收器运行时,它会销毁obj所引用的对象,包括myVariable。 + +成员变量的值应该至少被 一个方法、构造方法或者语句块 引用,使得外部能够通过这些方式获取 实例变量信息。示例 + +```java +public class MyClass { + private int myVariable;//实例变量 + + public MyClass(int value) { + myVariable = value; + } + + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10);//实例变量 + System.out.println(obj.getMyVariable()); // 输出:10 + } +} + +``` + +成员变量可以声明在使用前或者使用后。示例 + +```java +public class MyClass { + private int myVariable; + + public MyClass(int value) { + myVariable = value; + } + + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10); + System.out.println(obj.getMyVariable()); // 输出:10 + } +} + +``` + +访问修饰符可以修饰成员变量。示例 + +```java +public class MyClass { + private int myVariable; + + public MyClass(int value) { + myVariable = value; + } + + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10); + System.out.println(obj.getMyVariable()); // 输出:10 + } +} + +``` + +在这个示例中,myVariable是一个成员变量,它被声明为private,这意味着它只能在MyClass类内部访问。getMyVariable方法被用来获取myVariable的值,它是一个public方法,这意味着它可以在任何地方访问。因此,访问修饰符可以修饰成员变量,控制它的访问权限。 + +成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见。示例 + +```java +public class MyClass { + private int myVariable; + + public MyClass(int value) { + myVariable = value; + } + + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10); + System.out.println(obj.getMyVariable()); // 输出:10 + } +} + +public class SubClass extends MyClass { + public SubClass(int value) { + super(value); + } + + public void printMyVariable() { + System.out.println(myVariable); // 可以访问父类的私有成员变量 + } +} + +``` + +在这个示例中,myVariable是一个成员变量,它被声明为private,这意味着它只能在MyClass类内部访问。getMyVariable方法被用来获取myVariable的值,它是一个public方法,这意味着它可以在任何地方访问。SubClass是MyClass的子类,它继承了MyClass的成员变量myVariable。SubClass的printMyVariable方法可以访问myVariable,因为myVariable是私有的,但是SubClass是MyClass的子类,所以它可以访问MyClass的私有成员变量。因此,成员变量对于 类中的方法、构造方法或者语句块 是可见的,一般情况下应该把成员变量设为私有,通过使用访问修饰符可以使成员变量对子类可见。 + +成员变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定; + +```java +public class MyClass { + private int myInt;// 实例变量 + // 声明一个布尔类型的私有变量 + private boolean myBoolean; + // 声明一个字符串类型的私有变量 + private String myString; + + // 无参构造函数 + public MyClass() { + // 初始化myInt为0 + myInt = 0; + // 初始化myBoolean为false + myBoolean = false; + // 初始化myString为null + myString = null; + } + + // 有参构造函数 + public MyClass(int value, boolean flag, String str) { + // 将传入的value赋值给myInt + myInt = value; + // 将传入的flag赋值给myBoolean + myBoolean = flag; + // 将传入的str赋值给myString + myString = str; + } + + public static void main(String[] args) { + MyClass obj1 = new MyClass(); + System.out.println(obj1.myInt); // 输出:0 + System.out.println(obj1.myBoolean); // 输出:false + System.out.println(obj1.myString); // 输出:null + + MyClass obj2 = new MyClass(10, true, "Hello"); + System.out.println(obj2.myInt); // 输出:10 + System.out.println(obj2.myBoolean); // 输出:true + System.out.println(obj2.myString); // 输出:Hello + } +} + +``` + +成员变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObjectReference.VariableName。 + +```java +public class MyClass { + // 定义一个整型变量 + private int myVariable; + + // 构造方法,接收一个整型参数,并将其赋值给myVariable + public MyClass(int value) { + myVariable = value; + } + + // 返回myVariable的值 + public int getMyVariable() { + return myVariable; + } + + public static void main(String[] args) { + MyClass obj = new MyClass(10); + System.out.println(obj.getMyVariable()); // 输出:10 + + System.out.println(MyClass.obj.myVariable); // 错误:无法直接访问私有成员变量 + } +} + +``` + +## 实例 + +```java +public class DaTest { + private int a; // 私有成员变量a + public String b = "Hello"; // 公有成员变量b + + public static void main(String[] args) { + DaTest obj = new DaTest(); // 创建对象 + + obj.a = 10; // 访问成员变量a,并设置其值为10 + System.out.println("a = " + obj.a);// 输出:a = 10 + + obj.b = "World"; // 访问成员变量b,并设置其值为"World" + System.out.println("b = " + obj.b);// 输出:b = World + } + } +``` + +```java +import java.io.*; +public class Employee{ + // 这个成员变量对子类可见 + public String name; + // 私有变量,仅在该类可见 + private double salary; + //在构造器中对name赋值 + public Employee (String empName){ + name = empName; + } + //设定salary的值 + public void setSalary(double empSal){ + salary = empSal; + } + // 打印信息 + public void printEmp(){ + System.out.println("名字 : " + name ); + System.out.println("薪水 : " + salary); + } + + public static void main(String[] args){ + Employee empOne = new Employee("Da"); + empOne.setSalary(1000.0); + empOne.printEmp(); + } +} +``` + +以上实例编译运行结果如下: + +```java +$ javac Employee.java +$ java Employee +名字 : Da +薪水 : 1000.0 + +``` + +## 类变量(静态变量) + +类变量也称为静态变量,它们属于类,而不是类的实例。类变量使用 static 关键字声明,并且可以在没有创建类的实例的情况下访问。 + +Java 中的静态变量是指在类中定义的一个变量,它与类相关而不是与实例相关,即无论创建多少个类实例,静态变量在内存中只有一份拷贝,被所有实例共享。 + +静态变量在类加载时被创建,在整个程序运行期间都存在。 + +```java +public class MyClass { + public static int count = 0;//静态变量 count ,其初始值为 0 + // 其他成员变量和方法 +} +``` + +访问方式 + +由于静态变量是与类相关的,因此可以通过类名来访问静态变量,也可以通过实例名来访问静态变量。 + +```java +MyClass.count = 10; // 通过类名访问 +MyClass obj = new MyClass(); +obj.count = 20; // 通过实例名访问 +``` + +## 生命周期 + +静态变量的生命周期与程序的生命周期一样长,即它们在类加载时被创建,在整个程序运行期间都存在,直到程序结束才会被销毁。因此,静态变量可以用来存储整个程序都需要使用的数据,如配置信息、全局变量等。 + +### 初始化时机 + +静态变量在类加载时被初始化,其初始化顺序与定义顺序有关。 + +如果一个静态变量依赖于另一个静态变量,那么它必须在后面定义。 + +```java +public class MyClass { + public static int count1 = 0; + public static int count2 = count1 + 1; + // 其他成员变量和方法 +} +``` + +## 常量和静态变量的区别 + +示例: +```java +public class MyClass { + public static final int MY_CONSTANT = 10; // 常量 + public static int myVariable = 20; // 静态变量 +} +``` + +常量也是与类相关的,但它是用 final 关键字修饰的变量,一旦被赋值就不能再修改。与静态变量不同的是,常量在编译时就已经确定了它的值,而静态变量的值可以在运行时改变。另外,常量通常用于存储一些固定的值,如数学常数、配置信息等,而静态变量通常用于存储可变的数据,如计数器、全局状态等。 + +总之,静态变量是与类相关的变量,具有唯一性和共享性,可以用于存储整个程序都需要使用的数据,但需要注意初始化时机和与常量的区别。 + + +## 静态变量的访问修饰符 + +静态变量的访问修饰符可以是 public、protected、private 或者默认的访问修饰符(即不写访问修饰符)。 + +示例: +```java +public class MyClass { + public static int count = 0; // 公有静态变量 + protected static int count2 = 0; // 受保护的静态变量 + private static int count3 = 0; // 私有静态变量 + static int count4 = 0; // 默认访问修饰符的静态变量 +} +``` + +需要注意的是,静态变量的访问权限与实例变量不同,因为静态变量是与类相关的,不依赖于任何实例。 + +## 静态变量的线程安全性 + +静态变量是类级别的变量,它们在内存中只有一份拷贝,被所有实例共享。因此,静态变量在多线程环境下可能会出现线程安全问题。示例 +```java +public class MyClass { + public static int count = 0; // 静态变量 + + public static void increment() { + count++; // 增加count的值 + } +} +``` + +在多线程环境下,多个线程可能会同时调用 increment() 方法,导致 count 的值被错误地增加多次。为了避免这种情况,可以使用同步机制来保证 count 的增加操作是线程安全的。 + +示例: +```java +public class MyClass { + public static int count = 0; // 静态变量 + + public static synchronized void increment() { + count++; // 增加count的值 + } +} +``` + +在 increment() 方法前面加上 synchronized 关键字,可以保证同一时刻只有一个线程可以执行该方法,从而避免了线程安全问题。 + +synchronized 关键字可以用于方法或代码块,用于保证代码块在多线程环境下的线程安全性。原理是 +当一个线程进入 synchronized 方法或代码块时,它会获得该对象的一个锁,其他线程无法进入该对象的其他 synchronized 方法或代码块,直到该线程释放锁为止。 + +## 什么是线程: + +线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 + +## 静态变量的命名规范 + +1. 使用驼峰命名法 +2. 使用大写字母开头 +3. 使用有意义的名称 +4. 使用下划线分隔单词 +5. 使用前缀 "s_" 表示静态变量 + +```java +public class MyClass { + // 使用驼峰命名法 + public static int myStaticVariable; + + // 使用大写蛇形命名法 + public static final int MAX_SIZE = 100; + + // 避免使用缩写 + public static final String employeeName; + + // 具有描述性的变量名 + public static double defaultInterestRate; +} +``` + +在 main() 方法中,我们创建了三个 Counter 对象,并打印出了计数器的值。 + +```java +public class Counter { + private static int count = 0; + + public Counter() { + count++; + } + + public static int getCount() { + return count; + } + + public static void main(String[] args) { + Counter c1 = new Counter(); + Counter c2 = new Counter(); + Counter c3 = new Counter(); + System.out.println("目前为止创建的对象数: " + Counter.getCount()); + } +} +``` + +以上实例编译运行结果如下: + +目前为止创建的对象数: 3 + + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basic/6.md b/docs/basic/6.md new file mode 100644 index 000000000..700999a67 --- /dev/null +++ b/docs/basic/6.md @@ -0,0 +1,61 @@ +--- +title: 第6天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第6天 + +今天6天,你看的第6份内容,之前的预习一下,接着看,今天内容就是变量命名规则,如何起名字,很简单,看一下,过一下就行 + +## Java 变量命名规则 + +使用有意义的名字: 变量名应该具有清晰的含义,能够准确地反映变量的用途。避免使用单个字符或无意义的缩写。 + +驼峰命名法(Camel Case): 在变量名中使用驼峰命名法,即将每个单词的首字母大写,除了第一个单词外,其余单词的首字母都采用大写形式。例如:myVariableName。 + +避免关键字: 不要使用 Java 关键字(例如,class、int、boolean等)作为变量名。 + +区分大小写: Java 是大小写敏感的,因此变量名中的大小写字母被视为不同的符号。例如,myVariable 和 myvariable 是两个不同的变量。 + +不以数字开头: 变量名不能以数字开头,但可以包含数字。 + +遵循命名约定: 对于不同类型的变量(局部变量、实例变量、静态变量等),可以采用不同的命名约定,例如使用前缀或后缀来区分。 + +1. 使用有意义的名字 +2. 驼峰命名法 +3. 避免关键字:java系统内部已经定义了的字段 +4. 区分大小写 +5. 不以数字开头 +6. 遵循命名约定 + +结束了 + +## 代码示例 + +```java +int myLocalVariable; //局部变量 + +private int myInstanceVariable; // 实例变量(成员变量) + +// 使用驼峰命名法 静态变量(类变量) +public static int myStaticVariable; + +// 使用大写蛇形命名法 静态变量(类变量) +public static final int MAX_SIZE = 100; + +public static final double PI = 3.14; // 常量 + +public void myMethod(int myParameter) {//参数 + // 方法体 +} + +//类名 +public class MyClass { + // 类的成员和方法 +} +``` + +--- + +> 今天的内容消化得如何?下一篇会带来更多精彩内容哦~ \ No newline at end of file diff --git a/docs/basic/7.md b/docs/basic/7.md new file mode 100644 index 000000000..3f211f71a --- /dev/null +++ b/docs/basic/7.md @@ -0,0 +1,329 @@ +--- +title: 第7天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第7天 + +> 学习累了就休息一下,我会一直在这里等你~ + +今天说说什么是修饰符 + +## 修饰符 + +修饰符用于定义类、方法、变量等元素的访问权限和特性。 + +Java 中的修饰符主要有以下几种: + +1. 访问修饰符:用于控制类、方法、变量等元素的访问权限。Java 中有四种访问修饰符:public、protected、默认(无修饰符)和 private。 +2. 非访问修饰符:用于定义类、方法、变量等元素的特性。Java 中有几种非访问修饰符,如 static、final、abstract、synchronized、volatile 等。 +3. 注解修饰符:用于为类、方法、变量等元素添加元数据。Java 中有几种注解修饰符,如 `@Override、@Deprecated、@SuppressWarnings 等。` +4. 泛型修饰符:用于定义泛型类、泛型方法、泛型接口等元素。Java 中有几种泛型修饰符,如 `` 等。 +5. 枚举修饰符:用于定义枚举类。Java 中有几种枚举修饰符,如 enum 等。 +6. 异常修饰符:用于定义异常类。Java 中有几种异常修饰符,如 throws、throw 等。 +7. 同步修饰符:用于定义同步方法或同步代码块。Java 中有几种同步修饰符,如 synchronized 等。 +8. 其他修饰符:如 native、strictfp、transient 等。 + +但Java语言提供了很多修饰符,主要分为以下两类:(先记录两个) + +1. 访问修饰符 +2. 非访问修饰符 + +例子来说明: +```java +public class MyClass { + private int myPrivateVariable; + public void myPublicMethod() { + // ... + } +} +``` + +## 访问修饰符 + +访问修饰符用于控制类、方法、变量等元素的访问权限。Java 中有四种访问修饰符:public、protected、默认(无修饰符)和 private。 + +1. public:表示公共的,任何其他类都可以访问。例如,public class MyClass 表示 MyClass 类是公共的,任何其他类都可以访问它。示例访问 +```java +public class MyClass { + public int myPublicVariable; + public void myPublicMethod() { + // ... + } +} +``` + +2. protected:表示受保护的,只有同一个包中的类和子类可以访问。例如,protected int myProtectedVariable 表示 myProtectedVariable 变量是受保护的,只有同一个包中的类和子类可以访问它。示例访问 +```java +public class MyClass { + protected int myProtectedVariable; + protected void myProtectedMethod() { + // ... + } +} +``` + +3. 默认(无修饰符):表示默认的,只有同一个包中的类可以访问。例如,int myDefaultVariable 表示 myDefaultVariable 变量是默认的,只有同一个包中的类可以访问它。 +4. private:表示私有的,只有同一个类中的方法可以访问。例如,private void myPrivateMethod() 表示 myPrivateMethod 方法是私有的,只有同一个类中的方法可以访问它。 + +## 访问控制修饰符 + +Java 支持 4 种不同的访问权限。 + +default: 在同一包内可见(文件夹),不使用任何修饰符。使用对象:类、接口、变量、方法。 + +private:在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) + +public : 对所有类可见。使用对象:类、接口、变量、方法 + +protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 + +## 默认访问修饰符-不使用任何关键字 + +示例: +```java +class AccessDefault { + void display() { + System.out.println("默认访问修饰符"); + } +} +public class MyClass { + public static void main(String[] args) { + AccessDefault obj = new AccessDefault(); + obj.display(); + } +} +``` + +输出结果: +``` +默认访问修饰符 +``` + +## 私有访问修饰符-private + +私有访问修饰符是最严格的访问级别,所以被声明为 private 的变量、方法和构造器只能被所属类访问,并且类和接口不能声明为 private。 + +示例: +```java +public class AccessPrivate { + private String name = "哪吒"; + + private void display() { + System.out.println("私有访问修饰符"); + } +} +public class MyClass { + public static void main(String[] args) { + AccessPrivate obj = new AccessPrivate(); + //obj.display(); //编译错误 + //System.out.println(obj.name); //编译错误 + } +} +``` + +## 公共访问修饰符-public + +被声明为 public 的类、方法、构造器和接口都能被任何其他类访问。 + +示例: +```java +public class AccessPublic { + public String name = "哪吒"; + + public void display() { + System.out.println("公共访问修饰符"); + } +} +``` + +## protected + +protected 对同一个包内的类和所有子类可见。 + +示例: + +```java +public class AccessProtected { + protected String name = "哪吒"; + + protected void display() { + System.out.println("受保护的访问修饰符"); + } +} +public class MyClass { + public static void main(String[] args) { + AccessProtected obj = new AccessProtected(); + //obj.display(); //编译错误 + //System.out.println(obj.name); //编译错误 + } +} +``` + +## 访问控制和继承 + +- 当子类继承父类时,子类可以继承父类的 public 和 protected 成员,但不能继承父类的 private 成员。如果子类和父类不在同一个包中,子类只能继承父类的 public 成员。 +- 父类中声明为 public 的方法在子类中也必须为 public。 +- 父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private。 +- 父类中声明为 private 的方法,不能够被子类继承。 + +## 非访问修饰符 + +非访问修饰符用于定义类、方法、变量等元素的特性。Java 中有几种非访问修饰符,如 static、final、abstract、synchronized、volatile 等。 + +1. static: 表示静态的,用于定义静态变量、静态方法和静态代码块。静态变量和方法属于类,而不是某个具体的对象。静态变量在类加载时初始化,并且只有一个副本。静态方法不能访问非静态变量和方法,只能访问静态变量和方法。 +2. final: 表示最终的,用于定义常量、方法和类。常量一旦被初始化,就不能再被修改。方法被声明为 final,就不能被重写。类被声明为 final,就不能被继承。 +3. abstract: 表示抽象的,用于定义抽象类和抽象方法。抽象类不能被实例化,只能被继承。抽象方法没有方法体,必须在子类中实现。 +4. synchronized: 表示同步的,用于定义同步方法或同步代码块。同步方法或代码块在同一时间只能被一个线程访问,以保证线程安全。 +5. volatile: 表示易变的,用于定义易变变量。易变变量可以被多个线程同时访问和修改,并且每次访问时都会从主内存中读取最新的值,而不是从线程的本地缓存中读取。 +6. transient: 表示瞬态的,用于定义瞬态变量。瞬态变量在序列化时不会被保存,即不会被写入到序列化文件中。 +7. native: 表示本地的,用于定义本地方法。本地方法是用其他语言(如 C 或 C++)编写的,并且不能在 Java 中实现。 +8. strictfp: 表示严格浮点,用于定义严格浮点计算的方法或类。严格浮点计算遵循 IEEE 754 标准,以保证计算结果的一致性。 + +简单易懂: + +1. static 修饰符,用来修饰类方法和类变量。 +2. final 修饰符,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。 +3. abstract 修饰符,用来创建抽象类和抽象方法。 +4. synchronized 和 volatile 修饰符,主要用于线程的编程。 + +## 静态变量: + +static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量。 + +## 静态方法: + +static 关键字用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。静态方法从参数列表得到数据,然后计算这些数据。 + +## static 修饰符用来创建类方法和类变量 + +示例: +```java +public class MyClass { + static int myStaticVariable = 10; + + static void myStaticMethod() { + System.out.println("静态方法"); + } + + public static void main(String[] args) { + MyClass.myStaticMethod(); + System.out.println(MyClass.myStaticVariable); + } +} +``` + +输出结果: +``` +静态方法 +10 +``` + +## final 修饰符 + +示例: +```java +public class MyClass { + final int myFinalVariable = 10; + + final void myFinalMethod() { + System.out.println("final 方法"); + } + + public static void main(String[] args) { + MyClass obj = new MyClass(); + //obj.myFinalVariable = 20; //编译错误 + //obj.myFinalMethod(); //编译错误 + } +} +``` + +输出结果: +``` +final 方法 +``` + +## abstract 修饰符 + +示例: +```java +public abstract class MyClass { + abstract void myAbstractMethod(); +} +``` + +输出结果: +``` +abstract 方法 +``` + +```java +abstract class Caravan{ + private double price; + private String model; + private String year; + public abstract void goFast(); //抽象方法 + public abstract void changeColor(); +} +``` + +```java +public abstract class SuperClass{ + abstract void m(); //抽象方法 +} + +class SubClass extends SuperClass{ + //实现抽象方法 + void m(){ + } +} +``` + +## synchronized 修饰符 + +synchronized 关键字声明的方法同一时间只能被一个线程访问。synchronized 修饰符可以应用于四个访问修饰符。 + +示例: +```java +public class MyClass { + synchronized void mySynchronizedMethod() { + System.out.println("同步方法"); + } + + public static void main(String[] args) { + MyClass obj = new MyClass(); + obj.mySynchronizedMethod(); + } +} +``` + +输出结果: +``` +同步方法 +``` + +## volatile 修饰符 + +volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 + +一个 volatile 对象引用可能是 null。 + +示例: +```java +public class MyClass { + volatile boolean myVolatileVariable = true; + + public static void main(String[] args) { + MyClass obj = new MyClass(); + //obj.myVolatileVariable = false; //编译错误 + } +} +``` + +输出结果: +``` +volatile 变 +``` + +> 今天的内容消化得如何?下一篇会带来更多精彩内容哦~ \ No newline at end of file diff --git a/docs/basic/8.md b/docs/basic/8.md new file mode 100644 index 000000000..e25bc4bc7 --- /dev/null +++ b/docs/basic/8.md @@ -0,0 +1,52 @@ +--- +title: 第8天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第8天 + +> 学习编程要劳逸结合哦,记得多喝水休息眼睛~ + +今天比较轻松,认识运算符 + +我们可以把运算符分成以下几组: + +1. 算术运算符 +2. 关系运算符 +3. 位运算符 +4. 逻辑运算符 +5. 赋值运算符 +6. 其他运算符 + +也是一样用看的,不用记住,多敲多练就行 + +算术运算符 + +![img_15.png](./img_15.png) + +关系运算符 + +![img_16.png](./img_16.png) + +位运算符 + +![img_17.png](./img_17.png) + +逻辑运算符 + +![img_18.png](./img_18.png) + +赋值运算符 + +![img_19.png](./img_19.png) + +Java运算符优先级 + +![img_20.png](./img_20.png) + + + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basic/9.md b/docs/basic/9.md new file mode 100644 index 000000000..9327f32c3 --- /dev/null +++ b/docs/basic/9.md @@ -0,0 +1,175 @@ +--- +title: 第9天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第9天 + +> 编程路上有我陪伴,遇到困难不要气馁呢! + +今天也是一样的,认识个语句写法, 看看,看懂就会了 + +## Java 循环结构 - for, while 及 do...while + +Java中有三种主要的循环结构: + +## while 循环 + +示例: +```java +while (condition) { + // code block to be executed +} + + +public class Test { + public static void main(String[] args) { + int x = 10; + while( x < 20 ) { + System.out.print("value of x : " + x ); + x++; + System.out.print("\n"); + } + } +} +``` + +## do...while 循环 + +示例: +```java +do { + // code block to be executed +} while (condition); + +public class Test { + public static void main(String[] args){ + int x = 10; + + do{ + System.out.print("value of x : " + x ); + x++; + System.out.print("\n"); + }while( x < 20 ); + } +} +``` + +## for 循环 + +示例: +```java +for (initialization; condition; increment) { + // code block to be executed +} + + +public class Test { + public static void main(String[] args) { + + for(int x = 10; x < 20; x = x+1) { + System.out.print("value of x : " + x ); + System.out.print("\n"); + } + } +} +``` + +Java中有三种主要的循环结构: + +while 循环 + +do…while 循环 + +for 循环 + +## Java 增强 for 循环 + +```java +public class Test { + public static void main(String[] args){ + int [] numbers = {10, 20, 30, 40, 50}; + + for(int x : numbers ){ + System.out.print( x ); + System.out.print(","); + } + System.out.print("\n"); + String [] names ={"James", "Larry", "Tom", "Lacy"}; + for( String name : names ) { + System.out.print( name ); + System.out.print(","); + } + } +} +``` + +```java +public class Test { + public static void main(String[] args) { + int [] numbers = {10, 20, 30, 40, 50}; + + for(int x : numbers ) { + // x 等于 30 时跳出循环 + if( x == 30 ) { + break; + } + System.out.print( x ); + System.out.print("\n"); + } + } +} +``` + +## break关键字 + +break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。 + +break 跳出最里层的循环,并且继续执行该循环下面的语句。 + +```java +public class Test { + public static void main(String[] args) { + int [] numbers = {10, 20, 30, 40, 50}; + + for(int x : numbers ) { + // x 等于 30 时跳出循环 + if( x == 30 ) { + break; + } + System.out.print( x ); + System.out.print("\n"); + } + } +} +``` + +## continue 关键字 + +continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。 + +在 for 循环中,continue 语句使程序立即跳转到更新语句。 + +在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。 + +```java +public class Test { + public static void main(String[] args) { + int [] numbers = {10, 20, 30, 40, 50}; + + for(int x : numbers ) { + if( x == 30 ) { + continue; + } + System.out.print( x ); + System.out.print("\n"); + } + } +} +``` + + +--- + +> 恭喜你又完成了一个知识点!下一篇文章已经在向你招手了~ \ No newline at end of file diff --git a/docs/basic/img.png b/docs/basic/img.png new file mode 100644 index 000000000..ff963981f Binary files /dev/null and b/docs/basic/img.png differ diff --git a/docs/basic/img_1.png b/docs/basic/img_1.png new file mode 100644 index 000000000..45592e5cc Binary files /dev/null and b/docs/basic/img_1.png differ diff --git a/docs/basic/img_10.png b/docs/basic/img_10.png new file mode 100644 index 000000000..d8a60961e Binary files /dev/null and b/docs/basic/img_10.png differ diff --git a/docs/basic/img_11.png b/docs/basic/img_11.png new file mode 100644 index 000000000..a5f1859e8 Binary files /dev/null and b/docs/basic/img_11.png differ diff --git a/docs/basic/img_12.png b/docs/basic/img_12.png new file mode 100644 index 000000000..32b0c51fd Binary files /dev/null and b/docs/basic/img_12.png differ diff --git a/docs/basic/img_13.png b/docs/basic/img_13.png new file mode 100644 index 000000000..c82b7154c Binary files /dev/null and b/docs/basic/img_13.png differ diff --git a/docs/basic/img_14.png b/docs/basic/img_14.png new file mode 100644 index 000000000..59f48e1f8 Binary files /dev/null and b/docs/basic/img_14.png differ diff --git a/docs/basic/img_15.png b/docs/basic/img_15.png new file mode 100644 index 000000000..5c048de27 Binary files /dev/null and b/docs/basic/img_15.png differ diff --git a/docs/basic/img_16.png b/docs/basic/img_16.png new file mode 100644 index 000000000..4b4b81b66 Binary files /dev/null and b/docs/basic/img_16.png differ diff --git a/docs/basic/img_17.png b/docs/basic/img_17.png new file mode 100644 index 000000000..e4172fc48 Binary files /dev/null and b/docs/basic/img_17.png differ diff --git a/docs/basic/img_18.png b/docs/basic/img_18.png new file mode 100644 index 000000000..73008c48f Binary files /dev/null and b/docs/basic/img_18.png differ diff --git a/docs/basic/img_19.png b/docs/basic/img_19.png new file mode 100644 index 000000000..6bb022fa0 Binary files /dev/null and b/docs/basic/img_19.png differ diff --git a/docs/basic/img_2.png b/docs/basic/img_2.png new file mode 100644 index 000000000..7cd9e5d4b Binary files /dev/null and b/docs/basic/img_2.png differ diff --git a/docs/basic/img_20.png b/docs/basic/img_20.png new file mode 100644 index 000000000..a604c348e Binary files /dev/null and b/docs/basic/img_20.png differ diff --git a/docs/basic/img_21.png b/docs/basic/img_21.png new file mode 100644 index 000000000..f71803959 Binary files /dev/null and b/docs/basic/img_21.png differ diff --git a/docs/basic/img_22.png b/docs/basic/img_22.png new file mode 100644 index 000000000..3759210ca Binary files /dev/null and b/docs/basic/img_22.png differ diff --git a/docs/basic/img_23.png b/docs/basic/img_23.png new file mode 100644 index 000000000..6d6baa4ab Binary files /dev/null and b/docs/basic/img_23.png differ diff --git a/docs/basic/img_24.png b/docs/basic/img_24.png new file mode 100644 index 000000000..604d11031 Binary files /dev/null and b/docs/basic/img_24.png differ diff --git a/docs/basic/img_25.png b/docs/basic/img_25.png new file mode 100644 index 000000000..f78b4aea0 Binary files /dev/null and b/docs/basic/img_25.png differ diff --git a/docs/basic/img_26.png b/docs/basic/img_26.png new file mode 100644 index 000000000..4b8cd00ab Binary files /dev/null and b/docs/basic/img_26.png differ diff --git a/docs/basic/img_27.png b/docs/basic/img_27.png new file mode 100644 index 000000000..8a33e1bf9 Binary files /dev/null and b/docs/basic/img_27.png differ diff --git a/docs/basic/img_28.png b/docs/basic/img_28.png new file mode 100644 index 000000000..6cfb45835 Binary files /dev/null and b/docs/basic/img_28.png differ diff --git a/docs/basic/img_29.png b/docs/basic/img_29.png new file mode 100644 index 000000000..e3e809a2a Binary files /dev/null and b/docs/basic/img_29.png differ diff --git a/docs/basic/img_3.png b/docs/basic/img_3.png new file mode 100644 index 000000000..bb1a06417 Binary files /dev/null and b/docs/basic/img_3.png differ diff --git a/docs/basic/img_30.png b/docs/basic/img_30.png new file mode 100644 index 000000000..20ad987d6 Binary files /dev/null and b/docs/basic/img_30.png differ diff --git a/docs/basic/img_31.png b/docs/basic/img_31.png new file mode 100644 index 000000000..ef7761f70 Binary files /dev/null and b/docs/basic/img_31.png differ diff --git a/docs/basic/img_32.png b/docs/basic/img_32.png new file mode 100644 index 000000000..039d17798 Binary files /dev/null and b/docs/basic/img_32.png differ diff --git a/docs/basic/img_33.png b/docs/basic/img_33.png new file mode 100644 index 000000000..9cbe88613 Binary files /dev/null and b/docs/basic/img_33.png differ diff --git a/docs/basic/img_34.png b/docs/basic/img_34.png new file mode 100644 index 000000000..53a858b84 Binary files /dev/null and b/docs/basic/img_34.png differ diff --git a/docs/basic/img_35.png b/docs/basic/img_35.png new file mode 100644 index 000000000..97e3dac9e Binary files /dev/null and b/docs/basic/img_35.png differ diff --git a/docs/basic/img_36.png b/docs/basic/img_36.png new file mode 100644 index 000000000..c85eff94a Binary files /dev/null and b/docs/basic/img_36.png differ diff --git a/docs/basic/img_37.png b/docs/basic/img_37.png new file mode 100644 index 000000000..ab9b55b77 Binary files /dev/null and b/docs/basic/img_37.png differ diff --git a/docs/basic/img_38.png b/docs/basic/img_38.png new file mode 100644 index 000000000..ab9b55b77 Binary files /dev/null and b/docs/basic/img_38.png differ diff --git a/docs/basic/img_39.png b/docs/basic/img_39.png new file mode 100644 index 000000000..3927561ba Binary files /dev/null and b/docs/basic/img_39.png differ diff --git a/docs/basic/img_4.png b/docs/basic/img_4.png new file mode 100644 index 000000000..7d0086bbd Binary files /dev/null and b/docs/basic/img_4.png differ diff --git a/docs/basic/img_40.png b/docs/basic/img_40.png new file mode 100644 index 000000000..6c56534b9 Binary files /dev/null and b/docs/basic/img_40.png differ diff --git a/docs/basic/img_5.png b/docs/basic/img_5.png new file mode 100644 index 000000000..c233803b1 Binary files /dev/null and b/docs/basic/img_5.png differ diff --git a/docs/basic/img_6.png b/docs/basic/img_6.png new file mode 100644 index 000000000..2586827bd Binary files /dev/null and b/docs/basic/img_6.png differ diff --git a/docs/basic/img_7.png b/docs/basic/img_7.png new file mode 100644 index 000000000..0bd177492 Binary files /dev/null and b/docs/basic/img_7.png differ diff --git a/docs/basic/img_8.png b/docs/basic/img_8.png new file mode 100644 index 000000000..fc9d87491 Binary files /dev/null and b/docs/basic/img_8.png differ diff --git a/docs/basic/img_9.png b/docs/basic/img_9.png new file mode 100644 index 000000000..c8ee43da1 Binary files /dev/null and b/docs/basic/img_9.png differ diff --git a/docs/basicUp/31.md b/docs/basicUp/31.md new file mode 100644 index 000000000..087291d97 --- /dev/null +++ b/docs/basicUp/31.md @@ -0,0 +1,210 @@ +--- +title: 第31天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第31天 + +> 给你写一个Hello World,因为你就是我的整个世界! + + +认识一个 存储数据 的结构,不同叫法。 + +## Java 数据结构 + +Java 数据结构是指 Java 编程语言中用于存储和组织数据的一组类和接口。这些数据结构提供了高效的数据访问和操作方法,是构建复杂应用程序的基础。Java 提供了多种内置的数据结构,包括数组、集合、映射、队列和栈等。下面是一些常见的数据结构及其用途: + +1. **数组(Array)**: + - **用途**:用于存储固定大小的同类型元素。 + - **实现原理**:数组在内存中连续存储元素,通过索引访问元素。 + - **注意事项**:数组大小固定,一旦创建就不能改变;访问速度较快,但插入和删除元素较慢。 + +2. **集合(Collection)**: + - **用途**:用于存储一组不重复的元素。 + - **实现原理**:集合接口定义了一组通用的方法,如添加、删除、查找元素等。 + - **常见实现类**:`ArrayList`、`LinkedList`、`HashSet`、`TreeSet`、`HashMap`、`TreeMap`等。 + - **注意事项**:集合可以动态调整大小,但访问速度可能不如数组。 + +3. **映射(Map)**: + - **用途**:用于存储键值对,键是唯一的,值可以是任意对象。 + - **实现原理**:映射接口定义了一组方法,用于操作键值对。 + - **常见实现类**:`HashMap`、`TreeMap`、`LinkedHashMap`等。 + - **注意事项**:映射提供了快速查找、插入和删除键值对的方法。 + +4. **队列(Queue)**: + - **用途**:用于存储一组有序的元素,遵循先进先出(FIFO)的原则。 + - **实现原理**:队列接口定义了一组方法,如添加、删除、获取队首元素等。 + - **常见实现类**:`LinkedList`、`PriorityQueue`等。 + - **注意事项**:队列通常用于实现生产者-消费者模式。 + +5. **栈(Stack)**: + - **用途**:用于存储一组有序的元素,遵循后进先出(LIFO)的原则。 + - **实现原理**:栈接口定义了一组方法,如压栈、出栈、获取栈顶元素等。 + - **常见实现类**:`LinkedList`、`ArrayDeque`等。 + - **注意事项**:栈通常用于实现递归算法。 + +在 Java 中,数据结构通常通过类和接口来表示。例如,`ArrayList` 是 `List` 接口的一个实现类,提供了动态数组的功能。使用数据结构时,需要注意以下几点: + +- **选择合适的数据结构**:根据应用场景选择最合适的数据结构,以提高程序的性能和效率。 +- **处理异常情况**:在使用数据结构时,需要处理可能出现的异常情况,如空指针异常、索引越界异常等。 +- **线程安全**:如果多个线程同时访问和修改数据结构,需要考虑线程安全问题,可以使用同步机制或选择线程安全的实现类。 + +合理使用数据结构可以大大提高程序的性能和可维护性。 + +整理: + +`ArrayList`、`LinkedList`、`HashSet`、`TreeSet`、`HashMap`、`TreeMap` + +`HashMap`、`TreeMap`、`LinkedHashMap` + +`LinkedList`、`PriorityQueue` + +`LinkedList`、`ArrayDeque` + +## 数组(Arrays) + +特点: 固定大小,存储相同类型的元素。 + +优点: 随机访问元素效率高。 + +缺点: 大小固定,插入和删除元素相对较慢。 + +```java +int[] array = new int[5]; +``` + +## 列表(Lists) + +```java +List arrayList = new ArrayList<>(); +List linkedList = new LinkedList<>(); +``` + +### ArrayList: + +特点: 动态数组,可变大小。 + +优点: 高效的随机访问和快速尾部插入。 + +缺点: 中间插入和删除相对较慢。 + +### LinkedList: + +特点: 双向链表,元素之间通过指针连接。 + +优点: 插入和删除元素高效,迭代器性能好。 + +缺点: 随机访问相对较慢。 + +## 集合(Sets) + +集合(Sets)用于存储不重复的元素,常见的实现有 HashSet 和 TreeSet。 + +```java +Set hashSet = new HashSet<>(); +Set treeSet = new TreeSet<>(); +``` + +### HashSet: + +特点: 无序集合,基于HashMap实现。 + +优点: 高效的查找和插入操作。 + +缺点: 不保证顺序。 + +### TreeSet: + +特点:TreeSet 是有序集合,底层基于红黑树实现,不允许重复元素。 + +优点: 提供自动排序功能,适用于需要按顺序存储元素的场景。 + +缺点: 性能相对较差,不允许插入 null 元素。 + +## 映射(Maps) + +映射(Maps)用于存储键值对,常见的实现有 HashMap 和 TreeMap。 + +```java +Map hashMap = new HashMap<>(); +Map treeMap = new TreeMap<>(); +``` + +### HashMap: + +特点: 基于哈希表实现的键值对存储结构。 + +优点: 高效的查找、插入和删除操作。 + +缺点: 无序,不保证顺序。 + +### TreeMap: + +特点: 基于红黑树实现的有序键值对存储结构。 + +优点: 有序,支持按照键的顺序遍历。 + +缺点: 插入和删除相对较慢。 + +## 栈(Stack) + +栈(Stack)是一种线性数据结构,它按照后进先出(Last In, First Out,LIFO)的原则管理元素。在栈中,新元素被添加到栈的顶部,而只能从栈的顶部移除元素。这就意味着最后添加的元素是第一个被移除的。 + +```java +Stack stack = new Stack<>(); +``` + +### Stack 类: + +特点: 代表一个栈,通常按照后进先出(LIFO)的顺序操作元素。 + + +## 队列(Queue) + +队列(Queue)遵循先进先出(FIFO)原则,常见的实现有 LinkedList 和 PriorityQueue。 + +```java +Queue queue = new LinkedList<>(); +``` + +Queue 接口: + +特点: 代表一个队列,通常按照先进先出(FIFO)的顺序操作元素。 + +实现类: LinkedList, PriorityQueue, ArrayDeque。 + +## 堆(Heap) + +堆(Heap)优先队列的基础,可以实现最大堆和最小堆。 + +```java +PriorityQueue minHeap = new PriorityQueue<>(); +PriorityQueue maxHeap = new PriorityQueue<>(Collections.reverseOrder()); +``` + +## 树(Trees) + +Java 提供了 TreeNode 类型,可以用于构建二叉树等数据结构。 + +```java +class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int x) { val = x; } +} +``` + +## 图(Graphs) + +图的表示通常需要自定义数据结构或使用图库,Java 没有内建的图类。 + +一种数据结构。 + + + + +--- + +> 这一篇的知识点都理解了吗?下一篇会更加精彩,不要错过哦~ \ No newline at end of file diff --git a/docs/basicUp/32.md b/docs/basicUp/32.md new file mode 100644 index 000000000..de5f21daa --- /dev/null +++ b/docs/basicUp/32.md @@ -0,0 +1,119 @@ +--- +title: 第32天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第32天 + + +用来存储和操作对象组。 集合代表多,一个整体,一个教室里的学生集合 + +高效地处理数据 + +## Java 集合框架 + +整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。 + +![img.png](./img.png) + +## 集合框架体系如图所示 + +![img_1.png](./img_1.png) + +Java 集合框架是 Java 提供的一组接口和类,用于存储和操作一组对象。它提供了一套标准化的数据结构,使得开发者可以更方便地处理数据。Java 集合框架主要包括以下几个部分: + +1. **接口(Interfaces)**:定义了集合的基本操作,如添加、删除、遍历等。常见的接口有 `Collection`、`List`、`Set`、`Map` 等。 + +2. **实现类(Classes)**:实现了上述接口的具体类,如 `ArrayList`、`LinkedList`、`HashSet`、`HashMap` 等。 + +3. **工具类(Utility Classes)**:提供了一些静态方法,用于操作集合,如 `Collections` 类。 + +4. **迭代器(Iterators)**:用于遍历集合中的元素。 + +### 实现原理 + +Java 集合框架的实现原理基于接口和实现类分离的设计模式。接口定义了集合的基本操作,而实现类则提供了这些操作的具体实现。通过使用接口,可以编写更加通用和灵活的代码,因为接口可以用于多种不同的实现类。 + +### 用途 + +Java 集合框架广泛应用于各种场景,如: + +- 存储和操作一组对象。 +- 实现数据结构,如栈、队列、树、图等。 +- 提供高效的查找、插入和删除操作。 + +### 注意事项 + +1. **选择合适的集合类**:根据具体需求选择合适的集合类,如 `ArrayList`、`LinkedList`、`HashSet`、`HashMap` 等。 + +2. **线程安全**:如果需要在多线程环境下使用集合,需要考虑线程安全问题。可以使用 `Collections.synchronizedList()`、`Collections.synchronizedSet()`、`Collections.synchronizedMap()` 等方法来创建线程安全的集合。 + +3. **性能考虑**:不同的集合类在性能上有不同的特点,如 `ArrayList` 插入和删除操作在尾部性能较好,而 `LinkedList` 在头部性能较好。根据具体需求选择合适的集合类。 + +4. **泛型**:使用泛型可以避免类型转换,提高代码的安全性和可读性。 + +通过了解和使用 Java 集合框架,可以更高效地处理数据,提高代码的可维护性和可扩展性。 + +```java +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CollectionExample { + public static void main(String[] args) { + // 使用 ArrayList 存储一组整数 + List numbers = new ArrayList<>(); + numbers.add(1); + numbers.add(2); + numbers.add(3); + System.out.println(numbers); // 输出: [1, 2, 3] + + // 使用 HashSet 存储一组不重复的字符串 + Set words = new HashSet<>(); + words.add("apple"); + words.add("banana"); + words.add("orange"); + System.out.println(words); // 输出: [orange, apple, banana] + + // 使用 HashMap 存储一组键值对 + Map scores = new HashMap<>(); + scores.put("Alice", 90); + scores.put("Bob", 85); + scores.put("Charlie", 95); + System.out.println(scores); // 输出: {Charlie=95, Bob=85, Alice=90} + } +} + +``` + +使用了 Java 集合框架中的 `ArrayList`、`HashSet` 和 `HashMap` 来存储和操作一组对象。`ArrayList` 用于存储一组整数,`HashSet` 用于存储一组不重复的字符串,`HashMap` 用于存储一组键值对。通过这些示例,我们可以看到 Java 集合框架的灵活性和强大功能。 + +这段代码展示了Java中三种常用集合类的使用方法:ArrayList、HashSet和HashMap。 + +### 1. ArrayList +`ArrayList`是Java中的一个动态数组实现,可以存储一组有序的元素。它允许重复的元素,并且可以通过索引访问元素。 + +### 2. HashSet +`HashSet`是Java中的一个集合实现,用于存储一组不重复的元素。它不保证元素的顺序,但可以快速地添加、删除和查找元素。 + +### 3. HashMap +`HashMap`是Java中的一个键值对集合,用于存储一组键值对。键是唯一的,值可以是任何对象。它允许通过键快速查找值。 + +### 注意事项 +- **ArrayList**适用于需要快速访问元素的场景,但插入和删除元素可能会比较慢。 +- **HashSet**适用于需要存储不重复元素的场景,但无法保证元素的顺序。 +- **HashMap**适用于需要存储键值对并快速通过键查找值的场景。 + +### 用途 +这些集合类在Java编程中非常常用,用于存储和管理数据。根据不同的需求选择合适的集合类可以提高代码的效率和可读性。 + + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basicUp/33.md b/docs/basicUp/33.md new file mode 100644 index 000000000..45e47f660 --- /dev/null +++ b/docs/basicUp/33.md @@ -0,0 +1,178 @@ +--- +title: 第33天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第33天 + +> 记得定时站起来活动活动~ + +## Java ArrayList + +ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。 + +ArrayList 继承了 AbstractList ,并实现了 List 接口。 + +![img_2.png](./img_2.png) + +ArrayList 类位于 java.util 包中,使用前需要引入它,语法格式如下: + +```java +import java.util.ArrayList; // 引入 ArrayList 类 + +ArrayList objectName =new ArrayList<>();  // 初始化 +``` + +`ArrayList` 是 Java 集合框架中的一个类,实现了 `List` 接口。它是一个动态数组,可以存储一组有序的元素。`ArrayList` 允许重复的元素,并且可以通过索引访问元素。 + +### 实现原理 + +`ArrayList` 内部使用一个数组来存储元素。当添加元素时,如果数组已满,会创建一个新的数组,其大小是原数组的1.5倍,并将原数组中的元素复制到新数组中。这种动态扩容机制使得 `ArrayList` 可以高效地添加元素。 + +### 主要方法 + +1. **创建 ArrayList**: + +```java +List list = new ArrayList<>(); +``` + +2. **添加元素**: +```java +list.add("Hello"); +list.add("World"); +``` + +3. **获取元素**: +```java +String element = list.get(0); // 获取第一个元素 +``` + +4. **修改元素**: +```java +list.set(0, "Hi"); // 将第一个元素修改为 "Hi" +``` + +5. **删除元素**: +```java +list.remove(0); // 删除第一个元素 +``` + +6. **遍历 ArrayList**: +```java +for (String str : list) { + System.out.println(str); +} +``` + +### 注意事项 + +1. **线程安全**:`ArrayList` 不是线程安全的,如果多个线程同时访问和修改 `ArrayList`,可能会导致数据不一致。如果需要在多线程环境下使用 `ArrayList`,可以使用 `Collections.synchronizedList()` 方法创建线程安全的 `ArrayList`。 + +2. **性能考虑**:`ArrayList` 插入和删除元素在尾部性能较好,但在头部性能较差。如果需要在头部频繁插入和删除元素,可以考虑使用 `LinkedList`。 + +3. **泛型**:使用泛型可以避免类型转换,提高代码的安全性和可读性。 + +### 示例代码 + +```java +import java.util.ArrayList; +import java.util.List; + +public class ArrayListExample { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("Hello"); + list.add("World"); + System.out.println(list); // 输出: [Hello, World] + + String firstElement = list.get(0); + System.out.println(firstElement); // 输出: Hello + + list.set(0, "Hi"); + System.out.println(list); // 输出: [Hi, World] + + list.remove(0); + System.out.println(list); // 输出: [World] + + for (String str : list) { + System.out.println(str); + } + // 输出: + // World + } +} + +``` + +在这个示例中,我们创建了一个 `ArrayList`,并演示了如何添加、获取、修改、删除和遍历元素。 + +## Java ArrayList 方法 + +不用记太多,因为少用,没必要用太多。真实项目中,保证内存不溢出,保证变量被垃圾回收器回收。 + +Java ArrayList 常用方法列表如下: + +方法 描述 + +add() 将元素插入到指定位置的 arraylist 中 + +addAll() 添加集合中的所有元素到 arraylist 中 + +clear() 删除 arraylist 中的所有元素 + +clone() 复制一份 arraylist + +contains() 判断元素是否在 arraylist + +get() 通过索引值获取 arraylist 中的元素 + +indexOf() 返回 arraylist 中元素的索引值 + +removeAll() 删除存在于指定集合中的 arraylist 里的所有元素 + +remove() 删除 arraylist 里的单个元素 + +size() 返回 arraylist 里元素数量 + +isEmpty() 判断 arraylist 是否为空 + +subList() 截取部分 arraylist 的元素 + +set() 替换 arraylist 中指定索引的元素 + +sort() 对 arraylist 元素进行排序 + +toArray() 将 arraylist 转换为数组 + +toString() 将 arraylist 转换为字符串 + +ensureCapacity() 设置指定容量大小的 arraylist + +lastIndexOf() 返回指定元素在 arraylist 中最后一次出现的位置 + +retainAll() 保留 arraylist 中在指定集合中也存在的那些元素 + +containsAll() 查看 arraylist 是否包含指定集合中的所有元素 + +trimToSize() 将 arraylist 中的容量调整为数组中的元素个数 + +removeRange() 删除 arraylist 中指定索引之间存在的元素 + +replaceAll() 将给定的操作内容替换掉数组中每一个元素 + +removeIf() 删除所有满足特定条件的 arraylist 元素 + +forEach() 遍历 arraylist 中每一个元素并执行特定操作 + + + + + + + + +--- + +> 学习路上每一步都很重要,下一篇继续深入探索编程世界吧! \ No newline at end of file diff --git a/docs/basicUp/34.md b/docs/basicUp/34.md new file mode 100644 index 000000000..ecdf5a31d --- /dev/null +++ b/docs/basicUp/34.md @@ -0,0 +1,123 @@ +--- +title: 第34天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第34天 + +> 每天进步一点点,你已经很棒了! + +## Java LinkedList + +链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。 + +链表可分为单向链表和双向链表。 + +一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 + +![img_3.png](./img_3.png) + +一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 + +![img_4.png](./img_4.png) + +ava LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 + +与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 + +### 以下情况使用 ArrayList : + +1. 频繁访问列表中的某一个元素。 +2. 只需要在列表末尾进行添加和删除元素操作。 + +### 以下情况使用 LinkedList : + +1. 你需要通过循环迭代来访问列表中的某些元素。 +2. 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 + +LinkedList 继承了 AbstractSequentialList 类。 + +LinkedList 实现了 Queue 接口,可作为队列使用。 + +LinkedList 实现了 List 接口,可进行列表的相关操作。 + +LinkedList 实现了 Deque 接口,可作为队列使用。 + +LinkedList 实现了 Cloneable 接口,可实现克隆。 + +LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。 + +![img_5.png](./img_5.png) + +`LinkedList` 是 Java 集合框架中的一个类,实现了 `List` 接口。它是一个双向链表,可以存储一组有序的元素。`LinkedList` 允许重复的元素,并且可以通过索引访问元素。 + +### 实现原理 + +`LinkedList` 内部使用一个双向链表来存储元素。每个节点包含一个元素和两个指针,分别指向前一个节点和后一个节点。这种结构使得 `LinkedList` 可以高效地在头部和尾部插入和删除元素。 + +### 主要方法 + +1. **创建 LinkedList**: +2. **添加元素**: +3. **获取元素**: +4. **修改元素**: +5. **删除元素**: +6. **遍历 LinkedList**: + +### 注意事项 + +1. **线程安全**:`LinkedList` 不是线程安全的,如果多个线程同时访问和修改 `LinkedList`,可能会导致数据不一致。如果需要在多线程环境下使用 `LinkedList`,可以使用 `Collections.synchronizedList()` 方法创建线程安全的 `LinkedList`。 + +2. **性能考虑**:`LinkedList` 插入和删除元素在头部性能较好,但在尾部性能较差。如果需要在尾部频繁插入和删除元素,可以考虑使用 `ArrayList`。 + +3. **泛型**:使用泛型可以避免类型转换,提高代码的安全性和可读性。 + +```java +import java.util.LinkedList; +import java.util.List; + +public class LinkedListExample { + public static void main(String[] args) { + // 创建 LinkedList + List list = new LinkedList<>(); + + // 添加元素 + list.add("Hello"); + list.add("World"); + + // 获取元素 + String firstElement = list.get(0); + System.out.println(firstElement); // 输出: Hello + + // 修改元素 + list.set(0, "Hi"); + System.out.println(list); // 输出: [Hi, World] + + // 删除元素 + list.remove(0); + System.out.println(list); // 输出: [World] + + // 遍历 LinkedList + for (String str : list) { + System.out.println(str); + } + // 输出: + // World + } +} + +``` + +这段代码展示了如何创建、添加、获取、修改、删除和遍历 `LinkedList`。在 `main` 方法中,我们首先创建了一个 `LinkedList`,然后添加了两个元素。接着,我们获取了第一个元素,并修改了第一个元素。然后,我们删除了第一个元素,并遍历了 `LinkedList` 中的所有元素。最后,我们打印出了遍历结果。 + + + + + + + + +--- + +> 又学会了新技能!继续保持这个学习节奏,下一篇见~ \ No newline at end of file diff --git a/docs/basicUp/35.md b/docs/basicUp/35.md new file mode 100644 index 000000000..f568c7d2f --- /dev/null +++ b/docs/basicUp/35.md @@ -0,0 +1,70 @@ +--- +title: 第35天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第35天 + +> 记得多喝水休息眼睛~ + + +## Java HashSet + +HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。 + +HashSet 允许有 null 值。 + +HashSet 是无序的,即不会记录插入的顺序。 + +HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。 + +HashSet 实现了 Set 接口。 + +![img_6.png](./img_6.png) + +HashSet 是 Java 集合框架中的一个类,实现了 Set 接口。它是一个无序的集合,不允许存储重复的元素。HashSet 使用哈希表(Hash table)来存储元素,因此可以高效地进行添加、删除和查找操作。 + +## 实现原理 + +HashSet 内部使用一个哈希表来存储元素。哈希表是一种数据结构,它使用哈希函数将元素映射到一个索引位置,从而实现快速的查找、插入和删除操作。哈希函数将元素转换为哈希码,哈希码是一个整数,用于表示元素在哈希表中的位置。如果两个元素具有相同的哈希码,它们将被存储在同一个位置,这种现象称为哈希冲突。HashSet 使用链表来解决哈希冲突,即当多个元素具有相同的哈希码时,它们将被存储在同一个位置的链表中。 + +```java +import java.util.HashSet; +import java.util.Set; + +public class HashSetExample { + public static void main(String[] args) { + // 创建 HashSet + Setset = new HashSet(); + + // 添加元素 + set.add("Hello"); + set.add("World"); + + // 判断元素是否存在 + boolean contains = set.contains("Hello"); + System.out.println(contains); // 输出: true + + // 删除元素 + set.remove("Hello"); + + // 遍历 HashSet + for (String str : set) { + System.out.println(str); + } + // 输出: + // World + } +} + +``` + +## HashSet 常用方法 + +![img_7.png](./img_7.png) + + +--- + +> 恭喜你又完成了一个知识点!下一篇文章已经在向你招手了~ \ No newline at end of file diff --git a/docs/basicUp/36.md b/docs/basicUp/36.md new file mode 100644 index 000000000..f9b4dbb4d --- /dev/null +++ b/docs/basicUp/36.md @@ -0,0 +1,103 @@ +--- +title: 第36天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第36天 + + +## Java HashMap + +HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 + +HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。 + +HashMap 是无序的,即不会记录插入的顺序。 + +HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。 + +![img_8.png](./img_8.png) + +HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。 + +![img_9.png](./img_9.png) + +`HashMap` 是 Java 集合框架中的一个重要类,它实现了 `Map` 接口,用于存储键值对(key-value pairs)。`HashMap` 提供了高效的键值对存储和查找功能,并且允许使用 null 值和 null 键。 + +### 实现原理 + +`HashMap` 使用哈希表(Hash Table)数据结构来存储键值对。哈希表通过使用哈希函数将键转换为哈希码(hash code),然后根据哈希码将键值对存储在适当的桶(bucket)中。当需要查找某个键时,`HashMap` 会使用相同的哈希函数计算键的哈希码,然后直接定位到相应的桶中,从而实现快速的查找。 + +### 主要方法 + +- `put(Object key, Object value)`: 将指定的键值对插入到 `HashMap` 中。 +- `get(Object key)`: 返回指定键所映射的值,如果此映射不包含该键的映射关系,则返回 `null`。 +- `remove(Object key)`: 如果存在一个键的映射关系,则将其从此映射中移除。 +- `containsKey(Object key)`: 如果此映射包含指定键的映射关系,则返回 `true`。 +- `containsValue(Object value)`: 如果此映射将一个或多个键映射到指定值,则返回 `true`。 +- `size()`: 返回此映射中的键值对数。 +- `isEmpty()`: 如果此映射未包含键值对关系,则返回 `true`。 + +### 注意事项 + +1. **线程安全性**:`HashMap` 不是线程安全的。如果多个线程同时访问一个 `HashMap`,并且至少一个线程修改了该映射,则必须手动同步各个线程对 `HashMap` 的访问。 +2. **null 值和 null 键**:`HashMap` 允许一个 null 键和多个 null 值。但是,如果使用 null 键,则无法通过 `get()` 方法准确判断该键是否真的存在,因为 `get(null)` 可能返回 null,也可能返回与 null 键关联的值。 +3. **哈希冲突**:当两个不同的键产生相同的哈希码时,会发生哈希冲突。`HashMap` 使用链表和红黑树来处理哈希冲突,当链表长度超过一定阈值时,链表会转换为红黑树,以提高查找效率。 +4. **性能**:`HashMap` 的性能主要取决于哈希函数的质量和桶的数量。如果哈希函数的质量较差,或者桶的数量不足,则可能导致哈希冲突频繁,从而降低 `HashMap` 的性能。 +5. **遍历**:遍历 `HashMap` 时,键值对的顺序是不确定的,因此不能依赖于特定的顺序。 + +### 用途 + +`HashMap` 广泛用于需要快速查找和插入键值对的应用场景,例如缓存、数据库查询结果存储、配置参数存储等。由于其高效的性能和灵活性,`HashMap` 是 Java 编程中非常常用的数据结构之一。 + + +```java +import java.util.HashMap; +import java.util.Map; + +public class HashMapExample { + public static void main(String[] args) { + // 创建 HashMap + Map map = new HashMap<>(); + + // 添加键值对 + map.put("Apple", 1); + map.put("Banana", 2); + map.put("Cherry", 3); + + // 获取值 + int value = map.get("Banana"); + System.out.println(value); // 输出: 2 + + // 判断键是否存在 + boolean containsKey = map.containsKey("Apple"); + System.out.println(containsKey); // 输出: true + + // 判断值是否存在 + boolean containsValue = map.containsValue(3); + System.out.println(containsValue); // 输出: true + + // 删除键值对 + map.remove("Cherry"); + + // 遍历 HashMap + for (Map.Entry entry : map.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); + } + // 输出: + // Apple: 1 + // Banana: 2 + } +} + +``` + +这段代码展示了如何创建、添加、获取、判断键值对是否存在、删除和遍历 `HashMap`。在 `main` 方法中,我们首先创建了一个 `HashMap`,然后添加了三个键值对。接着,我们获取了键为 "Banana" 的值,并判断了键 "Apple" 和值 3 是否存在。然后,我们删除了键为 "Cherry" 的键值对。最后,我们遍历了 `HashMap` 中的所有键值对,并打印出了结果。 + + + + +--- + +> 这一篇的知识点都理解了吗?下一篇会更加精彩,不要错过哦~ \ No newline at end of file diff --git a/docs/basicUp/37.md b/docs/basicUp/37.md new file mode 100644 index 000000000..af7a61a59 --- /dev/null +++ b/docs/basicUp/37.md @@ -0,0 +1,88 @@ +--- +title: 第37天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第37天 + + +## Java Iterator(迭代器) + +Java迭代器(Iterator)是 Java 集合框架中的一种机制,是一种用于遍历集合(如列表、集合和映射等)的接口。 + +它提供了一种统一的方式来访问集合中的元素,而不需要了解底层集合的具体实现细节。 + +Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。 + +Iterator 是 Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。 + +### 迭代器接口定义了几个方法,最常用的是以下三个: + +next() - 返回迭代器的下一个元素,并将迭代器的指针移到下一个位置。 + +hasNext() - 用于判断集合中是否还有下一个元素可以访问。 + +remove() - 从集合中删除迭代器最后访问的元素(可选操作)。 + +`Iterator` 是 Java 集合框架中的一个接口,它用于遍历集合中的元素。`Iterator` 提供了一种标准的方法来遍历集合,而不需要了解集合的具体实现。`Iterator` 接口定义了三个方法:`hasNext()`、`next()` 和 `remove()`。 + +### 实现原理 + +`Iterator` 接口由集合类(如 `ArrayList`、`HashSet`、`HashMap` 等)实现。当需要遍历集合时,可以调用集合的 `iterator()` 方法获取一个 `Iterator` 对象,然后使用 `Iterator` 的方法来遍历集合。 + +### 主要方法 + +1. **`hasNext()`**:判断集合中是否还有元素。 +2. **`next()`**:返回集合中的下一个元素。 +3. **`remove()`**:从集合中删除当前元素。 + +### 用途 + +`Iterator` 主要用于遍历集合中的元素。通过使用 `Iterator`,可以避免直接操作集合,从而提高代码的安全性和可维护性。 + +### 注意事项 + +1. **线程安全**:`Iterator` 本身不是线程安全的,如果需要在多线程环境下使用 `Iterator`,需要手动同步。 +2. **并发修改**:如果在遍历过程中修改集合(除了通过 `Iterator` 的 `remove()` 方法),可能会抛出 `ConcurrentModificationException` 异常。 +3. **泛型**:使用泛型可以避免类型转换,提高代码的安全性和可读性。 + +```java +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class IteratorExample { + public static void main(String[] args) { + // 创建 ArrayList + List list = new ArrayList<>(); + list.add("Hello"); + list.add("World"); + + // 获取 Iterator + Iterator iterator = list.iterator(); + + // 使用 Iterator 遍历集合 + while (iterator.hasNext()) { + String element = iterator.next(); + System.out.println(element); + } + // 输出: + // Hello + // World + } +} + +``` + +这段代码展示了如何使用 `Iterator` 来遍历一个 `ArrayList` 集合。在 `main` 方法中,我们首先创建了一个 `ArrayList` 并添加了两个元素。然后,我们通过调用 `list.iterator()` 方法获取了一个 `Iterator` 对象。接着,我们使用 `Iterator` 的 `hasNext()` 方法来检查集合中是否还有元素,如果有,则使用 `next()` 方法获取下一个元素并打印出来。最后,我们遍历了整个集合并打印出了所有元素。 + + + + + + + +--- + +> 编程路上的每一步都值得庆祝,下一篇继续我们的学习之旅! \ No newline at end of file diff --git a/docs/basicUp/38.md b/docs/basicUp/38.md new file mode 100644 index 000000000..6f7b35857 --- /dev/null +++ b/docs/basicUp/38.md @@ -0,0 +1,125 @@ +--- +title: 第38天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第38天 + + +## Java Object 类 + +Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。 + +![img_10.png](./img_10.png) + +Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类。 + +Object 类可以显式继承,也可以隐式继承,以下两种方式是一样的: + +显式继承: + +```java +public class Da extends Object{ + +} +``` + +隐式继承: + +```java +public class Da { + +} +``` + +`Object` 类是 Java 中所有类的根类,即每个 Java 类都直接或间接继承自 `Object` 类。`Object` 类提供了一些通用的方法,这些方法可以被所有 Java 对象使用。 + +### 实现原理 + +`Object` 类是 Java 类层次结构的根类。每个 Java 类都直接或间接继承自 `Object` 类。`Object` 类提供了一些通用的方法,如 `equals()`、`hashCode()`、`toString()` 等。 + +### 主要方法 + +1. **`equals(Object obj)`**:比较两个对象是否相等。默认实现是使用 `==` 运算符比较两个对象的引用,通常需要重写该方法。 +2. **`hashCode()`**:返回对象的哈希码值。默认实现是返回对象的内存地址的哈希码,通常需要重写该方法。 +3. **`toString()`**:返回对象的字符串表示。默认实现是返回类名和对象的哈希码的十六进制表示,通常需要重写该方法。 +4. **`clone()`**:创建并返回对象的一个副本。默认实现是浅复制,通常需要重写该方法。 +5. **`getClass()`**:返回对象的运行时类。 +6. **`notify()`、`notifyAll()`、`wait()`**:用于线程间的通信。 + +### 用途 + +`Object` 类的方法在 Java 编程中非常常用,如比较对象是否相等、获取对象的字符串表示、克隆对象等。 + +### 注意事项 + +1. **重写 `equals()` 和 `hashCode()`**:当重写 `equals()` 方法时,通常需要同时重写 `hashCode()` 方法,以确保两个相等的对象具有相同的哈希码。 +2. **重写 `toString()`**:重写 `toString()` 方法可以提供对象的详细字符串表示,便于调试和日志记录。 +3. **克隆对象**:`clone()` 方法用于创建对象的副本,但默认实现是浅复制,如果对象包含可变的成员变量,需要重写 `clone()` 方法实现深复制。 +4. **线程安全**:`notify()`、`notifyAll()`、`wait()` 方法用于线程间的通信,使用时需要确保线程安全。 + +### 示例代码 + +```java +//private int value; +//这是一个私有成员变量,用于存储整数值。 +//public MyClass(int value) +//这是一个公共构造方法,用于创建 MyClass 对象并初始化 value 成员变量。 +//@Override +//这个注解表示该方法重写了父类 Object 中的 equals 方法。 + +public class MyClass extends Object { + private int value; + + public MyClass(int value) { + this.value = value; + } + + //这个方法用于比较两个 MyClass 对象是否相等。 + //首先检查两个对象是否是同一个引用,如果是,则返回 true。 + //然后检查传入的对象是否为 null 或者是否与当前对象类型不同,如果是,则返回 false。 + //最后,将传入的对象强制转换为 MyClass 类型,并比较其 value 成员变量是否相等。 + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MyClass myClass = (MyClass) obj; + return value == myClass.value; + } + + //重写 hashCode 方法 + //这个方法用于返回对象的哈希码,通常用于哈希表结构(如 HashMap)。 + //使用 Objects.hash 方法生成哈希码,该方法会根据传入的参数生成一个哈希值。 + @Override + public int hashCode() { + return Objects.hash(value); + } + + //这个方法用于返回对象的字符串表示形式。 + //返回的字符串格式为 MyClass{value=},其中包含 value 成员变量的值。 + @Override + public String toString() { + return "MyClass{value=" + value + "}"; + } +} + +``` + +在这个示例中,我们创建了一个 `MyClass` 类,它继承自 `Object` 类,并重写了 `equals()`、`hashCode()` 和 `toString()` 方法。 + +这个类可以用于需要自定义对象比较、哈希码生成和字符串表示的场景。 + +例如,可以将 MyClass 对象存储在哈希表中,并使用 equals 方法来比较对象是否相等。 + + + + + +--- + +> 这一章节掌握得怎么样?下一篇会更有趣哦,期待与你继续学习~ \ No newline at end of file diff --git a/docs/basicUp/39.md b/docs/basicUp/39.md new file mode 100644 index 000000000..92da2788b --- /dev/null +++ b/docs/basicUp/39.md @@ -0,0 +1,161 @@ +--- +title: 第39天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第39天 + + +## Java NIO Files 类 + +java.nio.file.Files 是 Java NIO (New I/O) 包中的一个实用工具类,位于 java.nio.file 包中。 + +java.nio.file.Files 提供了一系列静态方法来操作文件系统中的文件和目录,大大简化了文件 I/O 操作。 + +### 主要特点 + +静态方法:所有方法都是静态的,无需创建实例 + +功能丰富:提供文件读写、属性操作、目录遍历等多种功能 + +异常处理:统一使用 IOException 处理文件操作异常 + +与 Path 配合:主要与 java.nio.file.Path 接口一起使用 + +注意:许多方法会抛出 IOException,使用时需要进行异常处理。 + +`Files` 类是 Java NIO(New Input/Output)包中的一个实用工具类,它提供了一系列静态方法来操作文件和目录。`Files` 类的方法可以用于创建、删除、读取、写入、复制、移动文件,以及获取文件属性等。 + +### 实现原理 + +`Files` 类是 Java NIO 包的一部分,它使用 `Path` 对象来表示文件和目录。`Path` 对象是 `java.nio.file` 包中的一个接口,它表示文件系统中的一个路径。`Files` 类的方法通常接受 `Path` 对象作为参数,并返回文件操作的结果。 + +### 主要方法 + +1. **创建和删除文件**: +2. **读取和写入文件**: +3. **复制和移动文件**: +4. **获取文件属性**: + +### 用途 + +`Files` 类在 Java 编程中非常有用,它提供了一种简单的方式来处理文件和目录。例如,可以使用 `Files` 类的方法来读取配置文件、写入日志文件、复制文件等。 + +### 注意事项 + +1. **异常处理**:`Files` 类的方法可能会抛出 `IOException`,因此需要使用 `try-catch` 语句来处理异常。 +2. **文件路径**:`Files` 类的方法通常接受 `Path` 对象作为参数,可以使用 `Paths.get(String first, String... more)` 方法来创建 `Path` 对象。 +3. **文件属性**:`Files` 类提供了多种方法来获取文件属性,如 `BasicFileAttributes`、`DosFileAttributes`、`PosixFileAttributes` 等。 + +### 示例代码 + +```java +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributes; + +public class FilesExample { + public static void main(String[] args) { + Path filePath = Paths.get("example.txt"); + + try { + // 创建文件 + Files.createFile(filePath); + + // 写入文件 + String content = "Hello, World!"; + Files.write(filePath, content.getBytes(), StandardOpenOption.APPEND); + + // 读取文件 + List lines = Files.readAllLines(filePath); + System.out.println(lines); // 输出: [Hello, World!] + + // 获取文件属性 + BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); + System.out.println(attrs.size()); // 输出: 文件大小 + System.out.println(attrs.creationTime()); // 输出: 文件创建时间 + + // 删除文件 + Files.delete(filePath); + } catch (IOException e) { + e.printStackTrace(); + } + } +} + +``` + +```java +// 读取文件所有行 +List lines = Files.readAllLines(path); + +// 写入文件 +Files.write(path, content.getBytes()); + +// 追加写入 +Files.write(path, content.getBytes(), StandardOpenOption.APPEND); + +// 复制文件 +Files.copy(sourcePath, targetPath); + +// 移动/重命名文件 +Files.move(sourcePath, targetPath); + +// 删除文件 +Files.delete(path); + +// 创建单级目录 +Files.createDirectory(path); + +// 创建多级目录 +Files.createDirectories(path); + +// 遍历目录 +try (Stream paths = Files.list(directoryPath)) { + paths.forEach(System.out::println); +} + +// 递归遍历目录 +try (Stream paths = Files.walk(directoryPath)) { + paths.forEach(System.out::println); +} + +// 检查文件是否存在 +boolean exists = Files.exists(path); + +// 获取文件大小 +long size = Files.size(path); + +// 获取文件最后修改时间 +FileTime lastModifiedTime = Files.getLastModifiedTime(path); + +// 设置文件最后修改时间 +Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis())); + +// 设置文件权限 +Set perms = PosixFilePermissions.fromString("rwxr-x---"); +Files.setPosixFilePermissions(path, perms); + +//最佳实践 +try { + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); +} catch (IOException e) { + System.err.println("文件操作失败: " + e.getMessage()); +} + +//资源清理 +try (Stream lines = Files.lines(path)) { + lines.forEach(System.out::println); +} // 自动关闭流 +``` + + + + +--- + +> 学习路上每一步都很重要,下一篇继续深入探索编程世界吧! \ No newline at end of file diff --git a/docs/basicUp/40.md b/docs/basicUp/40.md new file mode 100644 index 000000000..938e99863 --- /dev/null +++ b/docs/basicUp/40.md @@ -0,0 +1,220 @@ +--- +title: 第40天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第40天 + + +## Java 泛型 + +Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。 + +java 中泛型标记符: + +```java +E - Element (在集合中使用,因为集合中存放的是元素) +T - Type(Java 类) +K - Key(键) +V - Value(值) +N - Number(数值类型) +? - 表示不确定的 java 类型 +``` + +## 泛型类 + +```java +public class Box { + + private T t; + + public void add(T t) { + this.t = t; + } + + public T get() { + return t; + } + + public static void main(String[] args) { + Box integerBox = new Box(); + Box stringBox = new Box(); + + integerBox.add(new Integer(10)); + stringBox.add(new String("菜鸟教程")); + + System.out.printf("整型值为 :%d\n\n", integerBox.get()); + System.out.printf("字符串为 :%s\n", stringBox.get()); + } +} +``` + +## 类型通配符 + +1、`类型通配符一般是使用 ? 代替具体的类型参数。例如 List 在逻辑上是 List,List 等所有 List<具体类型实参>` 的父类。 + +```java +import java.util.*; + +public class GenericTest { + + public static void main(String[] args) { + List name = new ArrayList(); + List age = new ArrayList(); + List number = new ArrayList(); + + name.add("icon"); + age.add(18); + number.add(314); + + getData(name); + getData(age); + getData(number); + + } + + public static void getData(List data) { + System.out.println("data :" + data.get(0)); + } +} +``` + +2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。 + +```java +import java.util.*; + +public class GenericTest { + + public static void main(String[] args) { + List name = new ArrayList(); + List age = new ArrayList(); + List number = new ArrayList(); + + name.add("icon"); + age.add(18); + number.add(314); + + //getUperNumber(name);//1 + getUperNumber(age);//2 + getUperNumber(number);//3 + + } + + public static void getData(List data) { + System.out.println("data :" + data.get(0)); + } + + public static void getUperNumber(List data) { + System.out.println("data :" + data.get(0)); + } +} +``` + +Java 泛型(Generics)是 Java 编程语言的一个特性,它允许在编译时进行类型检查,从而提高代码的类型安全性和可重用性。泛型通过使用类型参数(type parameters)来表示类、接口或方法可以操作的多种类型。 + +### 实现原理 + +泛型通过类型参数来表示类、接口或方法可以操作的多种类型。类型参数通常使用单个大写字母来表示,如 `T`、`E`、`K`、`V` 等。泛型类型参数在声明类、接口或方法时指定,并在使用时替换为具体的类型。 + +### 主要方法 + +1. **泛型类**: + +- 定义泛型类时,在类名后面添加类型参数。例如: + +```java +public class Box { + private T t; + public void add(T t) { + this.t = t; + } + public T get() { + return t; + } +} + +``` + +- 使用泛型类时,在类名后面添加具体的类型。例如: + +```java +Box integerBox = new Box(); + +``` + +2. **泛型接口**: + +- 定义泛型接口时,在接口名后面添加类型参数。例如: + +```java +public interface MyInterface { + void myMethod(T t); +} + +``` +- 使用泛型接口时,在接口名后面添加具体的类型。例如: + +```java +MyInterface myInterface = new MyInterface() { + @Override + public void myMethod(String s) { + System.out.println(s); + } +}; + +``` + +3. **泛型方法**: + +- 定义泛型方法时,在返回类型前面添加类型参数。例如: + +```java +public void myMethod(T t) { + System.out.println(t); +} +``` + +- 使用泛型方法时,在方法名后面添加具体的类型。例如: + +```java +myMethod("Hello, World!"); +``` + +### 示例代码 + +```java +import java.util.ArrayList; +import java.util.List; + +public class GenericExample { + public static void main(String[] args) { + List stringList = new ArrayList<>(); + stringList.add("Hello"); + stringList.add("World"); + + List integerList = new ArrayList<>(); + integerList.add(1); + integerList.add(2); + + printList(stringList); + printList(integerList); + } + + public static void printList(List list) { + for (Object obj : list) { + System.out.println(obj); + } + } +} + +``` + + + + + +--- + +> 学完这一篇,是不是对编程又有了新的理解呢?继续加油,下一篇等着你哦~ \ No newline at end of file diff --git a/docs/basicUp/41.md b/docs/basicUp/41.md new file mode 100644 index 000000000..b2a74f478 --- /dev/null +++ b/docs/basicUp/41.md @@ -0,0 +1,150 @@ +--- +title: 第41天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第41天 + + +## Java 序列化 + +Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。 + +序列化在 Java 中是通过 java.io.Serializable 接口来实现的,该接口没有任何方法,只是一个标记接口,用于标识类可以被序列化。 + +当你序列化对象时,你把它包装成一个特殊文件,可以保存、传输或存储。反序列化则是打开这个文件,读取序列化的数据,然后将其还原为对象,以便在程序中使用。 + +序列化是一种用于保存、传输和还原对象的方法,它使得对象可以在不同的计算机之间移动和共享,这对于分布式系统、数据存储和跨平台通信非常有用。 + +```java +import java.io.Serializable; + +public class MyClass implements Serializable { + // 类的成员和方法 +} +``` + +序列化对象: 使用 ObjectOutputStream 类来将对象序列化为字节流,以下是一个简单的实例: + +```java +MyClass obj = new MyClass(); +try { + FileOutputStream fileOut = new FileOutputStream("object.ser"); + ObjectOutputStream out = new ObjectOutputStream(fileOut); + out.writeObject(obj); + out.close(); + fileOut.close(); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +反序列化对象: 使用 ObjectInputStream 类来从字节流中反序列化对象,以下是一个简单的实例: + +```java +MyClass obj = null; +try { + FileInputStream fileIn = new FileInputStream("object.ser"); + ObjectInputStream in = new ObjectInputStream(fileIn); + obj = (MyClass) in.readObject(); + in.close(); + fileIn.close(); +} catch (IOException e) { + e.printStackTrace(); +} catch (ClassNotFoundException e) { + e.printStackTrace(); +} +``` + +实例 + +```java +public class Employee implements java.io.Serializable +{ + public String name; + public String address; + public transient int SSN; + public int number; + public void mailCheck() + { + System.out.println("Mailing a check to " + name + + " " + address); + } +} +``` + +Java 序列化是将对象的状态信息转换为可以存储或传输的形式的过程。在 Java 中,序列化通过实现 `java.io.Serializable` 接口来完成。序列化的主要用途包括对象的持久化存储和网络传输。 + +### 实现原理 + +1. **实现 `Serializable` 接口**:要使一个类支持序列化,该类必须实现 `java.io.Serializable` 接口。这个接口是一个标记接口,没有方法需要实现。 + +2. **对象输出流**:使用 `ObjectOutputStream` 类将对象写入到输出流中,实现对象的序列化。 + +3. **对象输入流**:使用 `ObjectInputStream` 类从输入流中读取对象,实现对象的反序列化。 + +### 主要方法 + +1. **序列化对象**: +2. **反序列化对象**: + +### 用途 + +1. **对象持久化**:将对象的状态保存到文件或数据库中,以便在程序下次运行时恢复对象的状态。 +2. **网络传输**:将对象序列化为字节流,通过网络发送到另一台计算机,然后在那里反序列化为对象。 + +### 注意事项 + +1. **版本控制**:在序列化过程中,Java 会将类的信息(包括类名、成员变量等)写入到序列化流中。如果类的结构发生变化,可能会导致反序列化失败。为了解决这个问题,可以在类中定义一个 `serialVersionUID` 字段来指定类的版本。 +2. **安全性**:序列化可能会暴露对象的内部状态,因此在处理不可信数据时需要谨慎。 +3. **性能**:序列化和反序列化操作可能会消耗较多的时间和内存,因此在性能敏感的应用中需要考虑优化。 + +### 示例代码 + +```java +import java.io.*; + +public class SerializationExample implements Serializable { + private static final long serialVersionUID = 1L; + private String name; + private int age; + + public SerializationExample(String name, int age) { + this.name = name; + this.age = age; + } + + public static void main(String[] args) { + // 序列化对象 + try { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser")); + oos.writeObject(new SerializationExample("Alice", 30)); + oos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + // 反序列化对象 + try { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser")); + SerializationExample obj = (SerializationExample) ois.readObject(); + ois.close(); + System.out.println(obj.name + ", " + obj.age); // 输出: Alice, 30 + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } +} + +``` + +在这个示例中,我们定义了一个 `SerializationExample` 类,它实现了 `Serializable` 接口,并定义了一个 `serialVersionUID` 字段。在 `main` 方法中,我们首先创建了一个 `SerializationExample` 对象,并将其序列化到文件中。然后,我们从文件中反序列化对象,并打印出对象的状态。 + + + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basicUp/42.md b/docs/basicUp/42.md new file mode 100644 index 000000000..071c526bb --- /dev/null +++ b/docs/basicUp/42.md @@ -0,0 +1,93 @@ +--- +title: 第42天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第42天 + + +网络编程,数据传输 + +## Java 网络编程 + +网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 + +java.net 包中提供了两种常见的网络协议的支持: + +TCP:TCP(英语:Transmission Control Protocol,传输控制协议) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 层是位于 IP 层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。 + +UDP:UDP (英语:User Datagram Protocol,用户数据报协议),位于 OSI 模型的传输层。一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。 + +Java 网络编程允许 Java 应用程序通过网络与其他计算机进行通信。Java 提供了一套丰富的 API 来支持网络编程,包括套接字(Socket)、服务器套接字(ServerSocket)、URL、URLConnection 等。 + +### 实现原理 + +1. **套接字(Socket)**:套接字是网络通信的端点,用于发送和接收数据。Java 提供了 `java.net.Socket` 类来表示客户端套接字,`java.net.ServerSocket` 类来表示服务器套接字。 + +2. **URL 和 URLConnection**:URL(统一资源定位符)用于标识网络上的资源。Java 提供了 `java.net.URL` 类来表示 URL,`java.net.URLConnection` 类来表示与 URL 的连接。 + +### 主要方法 + +1. **创建客户端套接字**: +2. **创建服务器套接字**: +3. **发送和接收数据**: +4. **使用 URL 和 URLConnection**: + +### 用途 + +1. **客户端-服务器通信**:通过套接字,可以实现客户端和服务器之间的通信。例如,一个 Web 服务器可以监听特定的端口,等待客户端的连接请求,然后处理请求并返回响应。 + +2. **资源访问**:通过 URL 和 URLConnection,可以访问网络上的资源。例如,可以使用 `URLConnection` 类从 URL 下载文件或发送 HTTP 请求。 + +### 注意事项 + +1. **异常处理**:网络编程可能会抛出各种异常,如 `IOException`、`UnknownHostException` 等,因此需要使用 `try-catch` 语句来处理异常。 +2. **线程安全**:在多线程环境下,需要确保网络通信的线程安全。 +3. **性能考虑**:网络通信可能会消耗较多的时间和资源,因此在性能敏感的应用中需要考虑优化。 + +### 示例代码 + +```java +import java.io.*; +import java.net.*; + +public class NetworkExample { + public static void main(String[] args) { + try { + // 创建客户端套接字 + Socket socket = new Socket("localhost", 8080); + + // 发送数据 + OutputStream output = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(output, true); + writer.println("Hello, Server!"); + + // 接收数据 + InputStream input = socket.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input)); + String response = reader.readLine(); + System.out.println("Server response: " + response); + + // 关闭连接 + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} + +``` + + + + + + + + + + +--- + +> 学习编程就像搭积木,一块一块慢慢来,下一篇继续加油! \ No newline at end of file diff --git a/docs/basicUp/43.md b/docs/basicUp/43.md new file mode 100644 index 000000000..b03b344d5 --- /dev/null +++ b/docs/basicUp/43.md @@ -0,0 +1,271 @@ +--- +title: 第43天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第43天 + +## Java 发送邮件 + +### 发送一封简单的 E-mail + +SendEmail.java 文件代码: + +```java +// 文件名 SendEmail.java + +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +public class SendEmail +{ + public static void main(String [] args) + { + // 收件人电子邮箱 + String to = "abcd@gmail.com"; + + // 发件人电子邮箱 + String from = "web@gmail.com"; + + // 指定发送邮件的主机为 localhost + String host = "localhost"; + + // 获取系统属性 + Properties properties = System.getProperties(); + + // 设置邮件服务器 + properties.setProperty("mail.smtp.host", host); + + // 获取默认session对象 + Session session = Session.getDefaultInstance(properties); + + try{ + // 创建默认的 MimeMessage 对象 + MimeMessage message = new MimeMessage(session); + + // Set From: 头部头字段 + message.setFrom(new InternetAddress(from)); + + // Set To: 头部头字段 + message.addRecipient(Message.RecipientType.TO, + new InternetAddress(to)); + + // Set Subject: 头部头字段 + message.setSubject("This is the Subject Line!"); + + // 设置消息体 + message.setText("This is actual message"); + + // 发送消息 + Transport.send(message); + System.out.println("Sent message successfully...."); + }catch (MessagingException mex) { + mex.printStackTrace(); + } + } +} +``` + + +编译并运行这个程序来发送一封简单的E-mail: + +```java +$ java SendEmail +Sent message successfully.... + +``` + +## 发送带有附件的 E-mail + +```java +// 文件名 SendFileEmail.java + +import java.util.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +public class SendFileEmail +{ + public static void main(String [] args) + { + + // 收件人电子邮箱 + String to = "abcd@gmail.com"; + + // 发件人电子邮箱 + String from = "web@gmail.com"; + + // 指定发送邮件的主机为 localhost + String host = "localhost"; + + // 获取系统属性 + Properties properties = System.getProperties(); + + // 设置邮件服务器 + properties.setProperty("mail.smtp.host", host); + + // 获取默认的 Session 对象。 + Session session = Session.getDefaultInstance(properties); + + try{ + // 创建默认的 MimeMessage 对象。 + MimeMessage message = new MimeMessage(session); + + // Set From: 头部头字段 + message.setFrom(new InternetAddress(from)); + + // Set To: 头部头字段 + message.addRecipient(Message.RecipientType.TO, + new InternetAddress(to)); + + // Set Subject: 头字段 + message.setSubject("This is the Subject Line!"); + + // 创建消息部分 + BodyPart messageBodyPart = new MimeBodyPart(); + + // 消息 + messageBodyPart.setText("This is message body"); + + // 创建多重消息 + Multipart multipart = new MimeMultipart(); + + // 设置文本消息部分 + multipart.addBodyPart(messageBodyPart); + + // 附件部分 + messageBodyPart = new MimeBodyPart(); + String filename = "file.txt"; + DataSource source = new FileDataSource(filename); + messageBodyPart.setDataHandler(new DataHandler(source)); + messageBodyPart.setFileName(filename); + multipart.addBodyPart(messageBodyPart); + + // 发送完整消息 + message.setContent(multipart ); + + // 发送消息 + Transport.send(message); + System.out.println("Sent message successfully...."); + }catch (MessagingException mex) { + mex.printStackTrace(); + } + } +} +``` + +## 用户认证部分 + +如果需要提供用户名和密码给e-mail服务器来达到用户认证的目的,你可以通过如下设置来完成: + +```java +props.put("mail.smtp.auth", "true"); +props.setProperty("mail.user", "myuser"); +props.setProperty("mail.password", "mypwd"); +``` + +## 需要用户名密码验证邮件发送实例: + +本实例以 QQ 邮件服务器为例,你需要在登录QQ邮箱后台在"设置"=》账号中开启POP3/SMTP服务 ,如下图所示: + +![img_11.png](./img_11.png) + +QQ 邮箱通过生成授权码来设置密码: + +![img_12.png](./img_12.png) + + +## SendEmail2.java 文件代码: + +```java +// 需要用户名密码邮件发送实例 +//文件名 SendEmail2.java +//本实例以QQ邮箱为例,你需要在qq后台设置 + +import java.util.Properties; + +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +public class SendEmail2 +{ + public static void main(String [] args) + { + // 收件人电子邮箱 + String to = "xxx@qq.com"; + + // 发件人电子邮箱 + String from = "xxx@qq.com"; + + // 指定发送邮件的主机为 smtp.qq.com + String host = "smtp.qq.com"; //QQ 邮件服务器 + + // 获取系统属性 + Properties properties = System.getProperties(); + + // 设置邮件服务器 + properties.setProperty("mail.smtp.host", host); + + properties.put("mail.smtp.auth", "true"); + // 获取默认session对象 + Session session = Session.getDefaultInstance(properties,new Authenticator(){ + public PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication("xxx@qq.com", "qq邮箱授权码"); //发件人邮件用户名、授权码 + } + }); + + try{ + // 创建默认的 MimeMessage 对象 + MimeMessage message = new MimeMessage(session); + + // Set From: 头部头字段 + message.setFrom(new InternetAddress(from)); + + // Set To: 头部头字段 + message.addRecipient(Message.RecipientType.TO, + new InternetAddress(to)); + + // Set Subject: 头部头字段 + message.setSubject("This is the Subject Line!"); + + // 设置消息体 + message.setText("This is actual message"); + + // 发送消息 + Transport.send(message); + System.out.println("Sent message successfully....from runoob.com"); + }catch (MessagingException mex) { + mex.printStackTrace(); + } + } +} +``` + + + + + + + + + + + + + + + +--- + +> 学完这一篇,是不是对编程又有了新的理解呢?继续加油,下一篇等着你哦~ \ No newline at end of file diff --git a/docs/basicUp/44.md b/docs/basicUp/44.md new file mode 100644 index 000000000..06c26b79f --- /dev/null +++ b/docs/basicUp/44.md @@ -0,0 +1,184 @@ +--- +title: 第44天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第44天 + +> 学习编程要劳逸结合哦,记得多喝水休息眼睛~ + + +## Java 多线程编程 + +Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 + +多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。 + +多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。 + +## 一个线程的生命周期 + +线程是一个动态执行的过程,它也有一个从产生到死亡的过程。 + +下图显示了一个线程完整的生命周期。 + +![img_13.png](./img_13.png) + +## 线程的优先级 + +每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。 + +## 创建一个线程 + +Java 提供了三种创建线程的方法: + +通过实现 Runnable 接口; + +通过继承 Thread 类本身; + +通过 Callable 和 Future 创建线程。 + +## 通过实现 Runnable 接口来创建线程 + +```java +class RunnableDemo implements Runnable { + private Thread t; + private String threadName; + + RunnableDemo( String name) { + threadName = name; + System.out.println("Creating " + threadName ); + } + + public void run() { + System.out.println("Running " + threadName ); + try { + for(int i = 4; i > 0; i--) { + System.out.println("Thread: " + threadName + ", " + i); + // 让线程睡眠一会 + Thread.sleep(50); + } + }catch (InterruptedException e) { + System.out.println("Thread " + threadName + " interrupted."); + } + System.out.println("Thread " + threadName + " exiting."); + } + + public void start () { + System.out.println("Starting " + threadName ); + if (t == null) { + t = new Thread (this, threadName); + t.start (); + } + } +} + +public class TestThread { + + public static void main(String args[]) { + RunnableDemo R1 = new RunnableDemo( "Thread-1"); + R1.start(); + + RunnableDemo R2 = new RunnableDemo( "Thread-2"); + R2.start(); + } +} +``` + +编译以上程序运行结果如下: + +```java +Creating Thread-1 +Starting Thread-1 +Creating Thread-2 +Starting Thread-2 +Running Thread-1 +Thread: Thread-1, 4 +Running Thread-2 +Thread: Thread-2, 4 +Thread: Thread-1, 3 +Thread: Thread-2, 3 +Thread: Thread-1, 2 +Thread: Thread-2, 2 +Thread: Thread-1, 1 +Thread: Thread-2, 1 +Thread Thread-1 exiting. +Thread Thread-2 exiting. + +``` + +## 通过继承Thread来创建线程 + +```java +class ThreadDemo extends Thread { + private Thread t; + private String threadName; + + ThreadDemo( String name) { + threadName = name; + System.out.println("Creating " + threadName ); + } + + public void run() { + System.out.println("Running " + threadName ); + try { + for(int i = 4; i > 0; i--) { + System.out.println("Thread: " + threadName + ", " + i); + // 让线程睡眠一会 + Thread.sleep(50); + } + }catch (InterruptedException e) { + System.out.println("Thread " + threadName + " interrupted."); + } + System.out.println("Thread " + threadName + " exiting."); + } + + public void start () { + System.out.println("Starting " + threadName ); + if (t == null) { + t = new Thread (this, threadName); + t.start (); + } + } +} + +public class TestThread { + + public static void main(String args[]) { + ThreadDemo T1 = new ThreadDemo( "Thread-1"); + T1.start(); + + ThreadDemo T2 = new ThreadDemo( "Thread-2"); + T2.start(); + } +} +``` + +编译以上程序运行结果如下: + +```java +Creating Thread-1 +Starting Thread-1 +Creating Thread-2 +Starting Thread-2 +Running Thread-1 +Thread: Thread-1, 4 +Running Thread-2 +Thread: Thread-2, 4 +Thread: Thread-1, 3 +Thread: Thread-2, 3 +Thread: Thread-1, 2 +Thread: Thread-2, 2 +Thread: Thread-1, 1 +Thread: Thread-2, 1 +Thread Thread-1 exiting. +Thread Thread-2 exiting. + +``` + + + +--- + +> 这一篇的知识点都理解了吗?下一篇会更加精彩,不要错过哦~ \ No newline at end of file diff --git a/docs/basicUp/45.md b/docs/basicUp/45.md new file mode 100644 index 000000000..23ad968e7 --- /dev/null +++ b/docs/basicUp/45.md @@ -0,0 +1,19 @@ +--- +title: 第45天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第45天 + + + + + + + + + + + + diff --git a/docs/basicUp/46.md b/docs/basicUp/46.md new file mode 100644 index 000000000..983eb47bf --- /dev/null +++ b/docs/basicUp/46.md @@ -0,0 +1,19 @@ +--- +title: 第46天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第46天 + + + + + + + + + + + + diff --git a/docs/basicUp/47.md b/docs/basicUp/47.md new file mode 100644 index 000000000..974dd9ebe --- /dev/null +++ b/docs/basicUp/47.md @@ -0,0 +1,19 @@ +--- +title: 第47天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第47天 + + + + + + + + + + + + diff --git a/docs/basicUp/48.md b/docs/basicUp/48.md new file mode 100644 index 000000000..fb5eb4cb7 --- /dev/null +++ b/docs/basicUp/48.md @@ -0,0 +1,19 @@ +--- +title: 第48天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第48天 + + + + + + + + + + + + diff --git a/docs/basicUp/49.md b/docs/basicUp/49.md new file mode 100644 index 000000000..595060ec1 --- /dev/null +++ b/docs/basicUp/49.md @@ -0,0 +1,7 @@ +--- +title: 第49天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第49天 diff --git a/docs/basicUp/50.md b/docs/basicUp/50.md new file mode 100644 index 000000000..606db1af7 --- /dev/null +++ b/docs/basicUp/50.md @@ -0,0 +1,19 @@ +--- +title: 第50天 +author: 哪吒 +date: '2023-06-15' +--- + +# 第50天 + + + + + + + + + + + + diff --git a/docs/basicUp/img.png b/docs/basicUp/img.png new file mode 100644 index 000000000..0ed42966d Binary files /dev/null and b/docs/basicUp/img.png differ diff --git a/docs/basicUp/img_1.png b/docs/basicUp/img_1.png new file mode 100644 index 000000000..ad3e5bab0 Binary files /dev/null and b/docs/basicUp/img_1.png differ diff --git a/docs/basicUp/img_10.png b/docs/basicUp/img_10.png new file mode 100644 index 000000000..bce573d73 Binary files /dev/null and b/docs/basicUp/img_10.png differ diff --git a/docs/basicUp/img_11.png b/docs/basicUp/img_11.png new file mode 100644 index 000000000..07bc8a4f0 Binary files /dev/null and b/docs/basicUp/img_11.png differ diff --git a/docs/basicUp/img_12.png b/docs/basicUp/img_12.png new file mode 100644 index 000000000..80fde3f13 Binary files /dev/null and b/docs/basicUp/img_12.png differ diff --git a/docs/basicUp/img_13.png b/docs/basicUp/img_13.png new file mode 100644 index 000000000..2a0f90c0e Binary files /dev/null and b/docs/basicUp/img_13.png differ diff --git a/docs/basicUp/img_2.png b/docs/basicUp/img_2.png new file mode 100644 index 000000000..340a4f20a Binary files /dev/null and b/docs/basicUp/img_2.png differ diff --git a/docs/basicUp/img_3.png b/docs/basicUp/img_3.png new file mode 100644 index 000000000..c6f83cf55 Binary files /dev/null and b/docs/basicUp/img_3.png differ diff --git a/docs/basicUp/img_4.png b/docs/basicUp/img_4.png new file mode 100644 index 000000000..df10aeed8 Binary files /dev/null and b/docs/basicUp/img_4.png differ diff --git a/docs/basicUp/img_5.png b/docs/basicUp/img_5.png new file mode 100644 index 000000000..2447a5278 Binary files /dev/null and b/docs/basicUp/img_5.png differ diff --git a/docs/basicUp/img_6.png b/docs/basicUp/img_6.png new file mode 100644 index 000000000..9538926a3 Binary files /dev/null and b/docs/basicUp/img_6.png differ diff --git a/docs/basicUp/img_7.png b/docs/basicUp/img_7.png new file mode 100644 index 000000000..9f4a7724c Binary files /dev/null and b/docs/basicUp/img_7.png differ diff --git a/docs/basicUp/img_8.png b/docs/basicUp/img_8.png new file mode 100644 index 000000000..5450bfe7c Binary files /dev/null and b/docs/basicUp/img_8.png differ diff --git a/docs/basicUp/img_9.png b/docs/basicUp/img_9.png new file mode 100644 index 000000000..bd17e4019 Binary files /dev/null and b/docs/basicUp/img_9.png differ diff --git a/docs/cs/tcp-ip.md b/docs/cs/tcp-ip.md new file mode 100644 index 000000000..36eea7e71 --- /dev/null +++ b/docs/cs/tcp-ip.md @@ -0,0 +1,395 @@ +--- +title: TCP/IP协议详解 +author: 哪吒 +date: '2023-07-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# TCP/IP协议详解 + +## 1. TCP/IP协议概述 + +### 1.1 什么是TCP/IP协议 + +TCP/IP(传输控制协议/网际协议)是一种网络通信模型,以及一整个网络传输协议家族,为互联网的基础通信架构。这个协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),给这个家族以名称。 + +### 1.2 TCP/IP协议的历史 + +- 1969年:ARPANET(TCP/IP的前身)诞生 +- 1974年:TCP/IP协议的基本架构提出 +- 1983年:ARPANET完全转为TCP/IP协议,成为现代互联网的基础 +- 1984年:域名系统(DNS)引入 +- 1989年:商业互联网服务提供商开始出现 + +### 1.3 TCP/IP与OSI七层模型的对比 + +| OSI七层模型 | TCP/IP四层模型 | 对应的网络协议 | +| :--------: | :-----------: | :------------: | +| 应用层 | 应用层 | HTTP、FTP、SMTP、DNS等 | +| 表示层 | 应用层 | Telnet、SNMP等 | +| 会话层 | 应用层 | SMTP、DNS等 | +| 传输层 | 传输层 | TCP、UDP | +| 网络层 | 网络层 | IP、ICMP、ARP、RARP | +| 数据链路层 | 网络接口层 | Ethernet、PPP、SLIP | +| 物理层 | 网络接口层 | IEEE 802.1A、IEEE 802.2到IEEE 802.11 | + +## 2. TCP/IP协议栈详解 + +### 2.1 网络接口层 + +网络接口层(也称链路层)是TCP/IP模型的最底层,负责接收和发送数据包。 + +#### 2.1.1 以太网协议(Ethernet) + +以太网是一种计算机局域网技术,规定了包括物理层的连线、电子信号和介质访问层协议的内容。 + +**以太网帧格式**: +``` +前导码(8字节) | 目标MAC地址(6字节) | 源MAC地址(6字节) | 类型(2字节) | 数据(46-1500字节) | CRC校验(4字节) +``` + +#### 2.1.2 ARP协议(地址解析协议) + +ARP协议用于将IP地址解析为MAC地址。 + +**工作原理**: +1. 主机A需要向主机B发送数据,已知B的IP地址 +2. 主机A查询自己的ARP缓存表,如果有B的MAC地址则直接使用 +3. 如果没有,主机A发送ARP广播请求 +4. 主机B收到请求后,回复自己的MAC地址 +5. 主机A更新ARP缓存表,并使用获得的MAC地址发送数据 + +#### 2.1.3 RARP协议(反向地址解析协议) + +RARP协议用于将MAC地址解析为IP地址,主要用于无盘工作站。 + +### 2.2 网络层 + +网络层主要解决主机到主机的通信问题,其主要协议是IP协议。 + +#### 2.2.1 IP协议(网际协议) + +IP协议是TCP/IP协议族的核心协议,提供了分组交换网络的互联服务。 + +**IPv4地址**: +- 32位二进制数,通常表示为四个十进制数(0-255),如192.168.0.1 +- 分为A、B、C、D、E五类 + - A类:1.0.0.0 - 126.255.255.255(首位为0) + - B类:128.0.0.0 - 191.255.255.255(首位为10) + - C类:192.0.0.0 - 223.255.255.255(首位为110) + - D类:224.0.0.0 - 239.255.255.255(组播地址) + - E类:240.0.0.0 - 255.255.255.255(保留地址) + +**IPv4数据包格式**: +``` +版本(4位) | 首部长度(4位) | 服务类型(8位) | 总长度(16位) | +标识(16位) | 标志(3位) | 片偏移(13位) | +生存时间(8位) | 协议(8位) | 首部校验和(16位) | +源IP地址(32位) | +目标IP地址(32位) | +选项(可变) | +数据(可变) +``` + +**IPv6**: +- 128位地址长度,通常表示为8组16位十六进制数 +- 解决IPv4地址耗尽问题 +- 简化了首部格式,提高了处理效率 +- 增强了安全性和服务质量 + +#### 2.2.2 ICMP协议(网际控制报文协议) + +ICMP协议用于在IP主机、路由器之间传递控制消息,包括报告错误、交换受限控制和状态信息等。 + +**常见ICMP消息类型**: +- 回显请求与回显应答(ping命令使用) +- 目标不可达 +- 超时 +- 重定向 + +### 2.3 传输层 + +传输层为应用程序提供端到端的通信服务,主要协议有TCP和UDP。 + +#### 2.3.1 TCP协议(传输控制协议) + +TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。 + +**TCP特点**: +- 面向连接:通信前需要建立连接,通信结束后需要释放连接 +- 可靠传输:使用确认和重传机制确保数据可靠传输 +- 流量控制:使用滑动窗口机制进行流量控制 +- 拥塞控制:慢启动、拥塞避免、快重传、快恢复 +- 全双工通信:允许双方同时发送和接收数据 + +**TCP首部格式**: +``` +源端口(16位) | 目标端口(16位) | +序列号(32位) | +确认号(32位) | +数据偏移(4位) | 保留(6位) | 标志位(6位) | 窗口大小(16位) | +校验和(16位) | 紧急指针(16位) | +选项(可变) | +数据(可变) +``` + +**TCP三次握手**: +1. 客户端发送SYN包(SYN=1, seq=x)到服务器,进入SYN_SENT状态 +2. 服务器收到SYN包,回应一个SYN+ACK包(SYN=1, ACK=1, seq=y, ack=x+1),进入SYN_RECV状态 +3. 客户端收到SYN+ACK包,回应一个ACK包(ACK=1, seq=x+1, ack=y+1),进入ESTABLISHED状态 + +**TCP四次挥手**: +1. 客户端发送FIN包(FIN=1, seq=u),进入FIN_WAIT_1状态 +2. 服务器收到FIN包,回应一个ACK包(ACK=1, ack=u+1),进入CLOSE_WAIT状态,客户端收到后进入FIN_WAIT_2状态 +3. 服务器发送FIN包(FIN=1, ACK=1, seq=v, ack=u+1),进入LAST_ACK状态 +4. 客户端收到FIN包,回应一个ACK包(ACK=1, seq=u+1, ack=v+1),进入TIME_WAIT状态,等待2MSL后关闭连接 + +#### 2.3.2 UDP协议(用户数据报协议) + +UDP是一种无连接的传输层协议,提供不可靠的数据传输服务。 + +**UDP特点**: +- 无连接:不需要建立连接就可以直接发送数据 +- 不可靠:不保证数据的可靠传输 +- 无拥塞控制:网络拥塞不会影响发送速率 +- 支持一对一、一对多、多对一、多对多通信 +- 首部开销小:UDP首部只有8个字节 + +**UDP首部格式**: +``` +源端口(16位) | 目标端口(16位) | +长度(16位) | 校验和(16位) | +数据(可变) +``` + +**UDP应用场景**: +- 实时应用(如视频会议、在线游戏) +- DNS查询 +- SNMP(简单网络管理协议) +- 多播和广播 + +### 2.4 应用层 + +应用层直接为用户提供服务,常见协议有HTTP、FTP、SMTP、DNS等。 + +#### 2.4.1 HTTP协议(超文本传输协议) + +HTTP是一种用于分布式、协作式和超媒体信息系统的应用层协议。 + +**HTTP特点**: +- 简单快速:客户端向服务器发送请求时,只需传送请求方法和路径 +- 灵活:允许传输任意类型的数据对象 +- 无状态:协议对于事务处理没有记忆能力 +- 支持B/S模式 + +**HTTP请求方法**: +- GET:请求指定的页面信息,并返回实体主体 +- POST:向指定资源提交数据进行处理请求 +- HEAD:类似于GET请求,但只返回首部 +- PUT:从客户端向服务器传送的数据取代指定的文档内容 +- DELETE:请求服务器删除指定的页面 +- OPTIONS:允许客户端查看服务器的性能 +- TRACE:回显服务器收到的请求,主要用于测试或诊断 + +#### 2.4.2 FTP协议(文件传输协议) + +FTP是一种用于在网络上进行文件传输的应用层协议。 + +**FTP特点**: +- 使用两个并行的TCP连接:控制连接和数据连接 +- 支持断点续传 +- 支持匿名传输 + +#### 2.4.3 SMTP协议(简单邮件传输协议) + +SMTP是一种提供可靠且有效的电子邮件传输的协议。 + +**SMTP工作流程**: +1. 建立TCP连接 +2. 客户端发送HELO命令 +3. 服务器响应 +4. 客户端发送MAIL FROM命令 +5. 服务器响应 +6. 客户端发送RCPT TO命令 +7. 服务器响应 +8. 客户端发送DATA命令 +9. 服务器响应 +10. 客户端发送邮件内容,以"."结束 +11. 服务器响应 +12. 客户端发送QUIT命令 +13. 服务器响应,关闭连接 + +#### 2.4.4 DNS协议(域名系统) + +DNS是一个将域名和IP地址相互映射的分布式数据库。 + +**DNS查询过程**: +1. 用户输入域名,操作系统检查本地缓存 +2. 如果本地缓存没有,向本地DNS服务器发送查询请求 +3. 本地DNS服务器如果有缓存,直接返回结果 +4. 如果没有缓存,本地DNS服务器向根域名服务器发送查询请求 +5. 根域名服务器返回顶级域名服务器地址 +6. 本地DNS服务器向顶级域名服务器发送查询请求 +7. 顶级域名服务器返回权威域名服务器地址 +8. 本地DNS服务器向权威域名服务器发送查询请求 +9. 权威域名服务器返回IP地址 +10. 本地DNS服务器将结果返回给用户,并缓存结果 + +## 3. TCP/IP协议的应用 + +### 3.1 网络编程基础 + +#### 3.1.1 Socket编程 + +Socket是应用层与TCP/IP协议族通信的中间软件抽象层,表现为一个套接字。 + +**Socket通信流程**: +1. 服务器创建Socket,绑定IP地址和端口 +2. 服务器监听端口 +3. 客户端创建Socket,连接服务器 +4. 服务器接受连接,创建新的Socket与客户端通信 +5. 客户端和服务器通过Socket交换数据 +6. 通信结束,关闭Socket + +#### 3.1.2 Java网络编程示例 + +**TCP服务器端**: +```java +import java.io.*; +import java.net.*; + +public class TCPServer { + public static void main(String[] args) throws IOException { + ServerSocket serverSocket = new ServerSocket(8888); + System.out.println("服务器启动,等待客户端连接..."); + + Socket socket = serverSocket.accept(); + System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress()); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + String line; + while ((line = in.readLine()) != null) { + System.out.println("收到客户端消息:" + line); + out.println("服务器回复:" + line); + if (line.equals("bye")) break; + } + + in.close(); + out.close(); + socket.close(); + serverSocket.close(); + } +} +``` + +**TCP客户端**: +```java +import java.io.*; +import java.net.*; + +public class TCPClient { + public static void main(String[] args) throws IOException { + Socket socket = new Socket("localhost", 8888); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); + + String userInput; + while ((userInput = stdIn.readLine()) != null) { + out.println(userInput); + System.out.println("收到服务器回复:" + in.readLine()); + if (userInput.equals("bye")) break; + } + + in.close(); + out.close(); + stdIn.close(); + socket.close(); + } +} +``` + +### 3.2 网络安全 + +#### 3.2.1 常见网络攻击 + +- **DDoS攻击**:分布式拒绝服务攻击,通过大量请求使服务器资源耗尽 +- **中间人攻击**:攻击者插入到通信双方之间,窃听或篡改通信内容 +- **ARP欺骗**:攻击者发送伪造的ARP消息,将自己的MAC地址与目标IP地址关联 +- **DNS劫持**:攻击者篡改DNS服务器的记录,将用户引导到恶意网站 + +#### 3.2.2 网络安全防护 + +- **防火墙**:监控和过滤进出网络的数据包 +- **入侵检测系统(IDS)**:监控网络或系统的可疑活动 +- **入侵防御系统(IPS)**:监控网络流量并主动阻止可疑活动 +- **VPN**:通过公共网络建立安全的私有网络连接 +- **SSL/TLS**:为网络通信提供安全及数据完整性保障 + +## 4. TCP/IP协议优化与故障排除 + +### 4.1 TCP/IP性能优化 + +#### 4.1.1 TCP参数调优 + +- **TCP窗口大小**:增大窗口大小可以提高吞吐量 +- **TCP缓冲区大小**:调整发送和接收缓冲区大小 +- **TCP拥塞控制算法**:选择适合网络环境的拥塞控制算法 +- **TCP超时重传**:调整重传超时时间 + +#### 4.1.2 网络架构优化 + +- **负载均衡**:分散网络流量,提高系统整体性能 +- **内容分发网络(CDN)**:将内容缓存到离用户最近的节点 +- **网络分段**:将大型网络分割为小型网络,减少广播域 + +### 4.2 常见网络故障排除 + +#### 4.2.1 网络故障诊断工具 + +- **ping**:测试主机之间的连通性 +- **traceroute/tracert**:跟踪数据包从源到目的地的路径 +- **netstat**:显示网络连接、路由表和网络接口信息 +- **nslookup/dig**:查询DNS记录 +- **tcpdump/Wireshark**:捕获和分析网络数据包 + +#### 4.2.2 常见网络问题及解决方案 + +- **网络连接问题**:检查物理连接、IP配置、防火墙设置 +- **网络延迟高**:检查网络拥塞、路由问题、硬件故障 +- **数据包丢失**:检查网络质量、MTU大小、防火墙规则 +- **DNS解析失败**:检查DNS服务器配置、本地hosts文件 + +## 5. TCP/IP协议的未来发展 + +### 5.1 IPv6的普及 + +IPv6的部署正在全球范围内加速,主要驱动因素包括: +- IPv4地址耗尽 +- 物联网设备数量激增 +- 5G网络的部署 +- 云计算和边缘计算的发展 + +### 5.2 新兴网络技术 + +- **SDN(软件定义网络)**:将网络控制平面与数据平面分离 +- **NFV(网络功能虚拟化)**:将网络功能从专用硬件转移到软件 +- **5G网络**:提供更高的带宽、更低的延迟和更大的连接密度 +- **边缘计算**:将计算资源部署在网络边缘,减少延迟 + +## 参考资料 + +1. 《TCP/IP详解 卷1:协议》,W. Richard Stevens 著 +2. 《计算机网络:自顶向下方法》,James F. Kurose, Keith W. Ross 著 +3. 《TCP/IP网络编程》,尹圣雨 著 +4. RFC 791:Internet Protocol +5. RFC 793:Transmission Control Protocol +6. RFC 768:User Datagram Protocol +7. RFC 2460:Internet Protocol, Version 6 (IPv6) Specification +8. \ No newline at end of file diff --git a/docs/docker/docker-compose.md b/docs/docker/docker-compose.md new file mode 100644 index 000000000..e8e5cf5c4 --- /dev/null +++ b/docs/docker/docker-compose.md @@ -0,0 +1,683 @@ +# Docker Compose详解 + +## 什么是Docker Compose + +Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。通过Compose,您可以使用YAML文件来配置应用程序的服务,然后使用一个命令创建并启动所有服务。 + +Docker Compose的主要优势: + +- **简化复杂应用的部署**:通过单个配置文件和命令管理多个容器 +- **声明式服务配置**:以YAML格式定义应用程序的整个环境 +- **环境隔离**:为每个项目创建独立的环境 +- **保持容器间的数据持久化**:可以在服务重启后保留数据 +- **仅重新创建已更改的容器**:提高开发和部署效率 +- **支持变量和环境扩展**:适应不同的部署环境 + +## 安装Docker Compose + +### Linux系统安装 + +```bash +# 下载Docker Compose二进制文件 +sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + +# 添加可执行权限 +sudo chmod +x /usr/local/bin/docker-compose + +# 创建软链接 +sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose + +# 验证安装 +docker-compose --version +``` + +### Windows系统安装 + +Windows系统中,Docker Desktop已经包含了Docker Compose,无需单独安装。 + +### macOS系统安装 + +macOS系统中,Docker Desktop已经包含了Docker Compose,无需单独安装。 + +### 使用pip安装 + +```bash +pip install docker-compose +``` + +## Docker Compose文件结构 + +Docker Compose使用YAML文件(通常命名为`docker-compose.yml`)来定义服务、网络和卷。 + +### 基本结构 + +```yaml +version: '3' # Compose文件版本 + +services: # 定义服务 + web: # 服务名称 + image: nginx:latest # 使用的镜像 + ports: # 端口映射 + - "80:80" + volumes: # 卷挂载 + - ./html:/usr/share/nginx/html + +volumes: # 定义卷(可选) + data-volume: + +networks: # 定义网络(可选) + app-network: +``` + +### 主要组件 + +1. **version**:指定Compose文件格式版本 +2. **services**:定义应用程序的各个服务(容器) +3. **volumes**:定义可以挂载到容器的命名卷 +4. **networks**:定义容器可以连接的网络 + +## 服务配置详解 + +### 基本配置选项 + +```yaml +services: + web: + image: nginx:latest # 使用现有镜像 + # 或者使用Dockerfile构建 + build: + context: ./app # 构建上下文路径 + dockerfile: Dockerfile.dev # Dockerfile文件名 + container_name: my-web-app # 容器名称 + restart: always # 重启策略 + environment: # 环境变量 + - NODE_ENV=production + env_file: .env # 从文件加载环境变量 + ports: # 端口映射 + - "80:80" + - "443:443" + expose: # 暴露端口(不映射到宿主机) + - "8000" + volumes: # 卷挂载 + - ./app:/app + - data-volume:/data + networks: # 网络连接 + - frontend + - backend + depends_on: # 依赖关系 + - db + - redis +``` + +### 高级配置选项 + +```yaml +services: + web: + # ... 基本配置 ... + deploy: # 部署配置(Swarm模式) + replicas: 3 + resources: + limits: + cpus: '0.5' + memory: 512M + healthcheck: # 健康检查 + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s + timeout: 10s + retries: 3 + logging: # 日志配置 + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + user: "1000:1000" # 指定用户和组 + working_dir: /app # 工作目录 + entrypoint: /app/entrypoint.sh # 入口点 + command: npm start # 命令 + ulimits: # 资源限制 + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 + sysctls: # 内核参数 + net.core.somaxconn: 1024 +``` + +## 网络配置 + +Docker Compose允许您定义自定义网络,并控制服务之间的通信。 + +### 默认网络 + +默认情况下,Compose会为您的应用创建一个网络,所有服务都连接到这个网络,服务可以通过服务名称相互访问。 + +### 自定义网络 + +```yaml +services: + web: + image: nginx + networks: + - frontend + + api: + image: node:14 + networks: + - frontend + - backend + + db: + image: postgres + networks: + - backend + +networks: + frontend: + driver: bridge + backend: + driver: bridge + internal: true # 内部网络,不连接到外部 +``` + +### 网络驱动选项 + +```yaml +networks: + app-network: + driver: bridge + driver_opts: + com.docker.network.bridge.name: app-bridge + ipam: + driver: default + config: + - subnet: 172.28.0.0/16 + gateway: 172.28.0.1 +``` + +## 卷配置 + +卷用于持久化数据和共享数据。 + +### 基本卷配置 + +```yaml +services: + db: + image: postgres + volumes: + - db-data:/var/lib/postgresql/data + +volumes: + db-data: # 命名卷 +``` + +### 高级卷配置 + +```yaml +volumes: + db-data: + driver: local + driver_opts: + type: nfs + o: addr=192.168.1.1,rw + device: ":/path/to/dir" + + backup-data: + external: true # 使用外部已存在的卷 +``` + +## Docker Compose命令 + +### 基本命令 + +```bash +# 启动所有服务 +docker-compose up + +# 后台启动所有服务 +docker-compose up -d + +# 构建或重建服务 +docker-compose build + +# 停止服务 +docker-compose stop + +# 停止并删除容器、网络 +docker-compose down + +# 停止并删除容器、网络、卷 +docker-compose down -v + +# 查看服务状态 +docker-compose ps + +# 查看服务日志 +docker-compose logs + +# 查看特定服务的日志 +docker-compose logs service_name + +# 在服务中执行命令 +docker-compose exec service_name command + +# 进入服务的容器 +docker-compose exec service_name bash +``` + +### 高级命令 + +```bash +# 仅启动特定服务 +docker-compose up -d service_name + +# 扩展服务实例数量 +docker-compose up -d --scale service_name=3 + +# 查看配置 +docker-compose config + +# 验证配置 +docker-compose config --quiet + +# 拉取服务镜像 +docker-compose pull + +# 重启服务 +docker-compose restart + +# 强制重新创建容器 +docker-compose up -d --force-recreate + +# 查看容器间的网络连接 +docker-compose network ls + +# 暂停服务 +docker-compose pause + +# 恢复服务 +docker-compose unpause + +# 查看服务的进程 +docker-compose top +``` + +## 实际应用示例 + +### Web应用 + 数据库 + +```yaml +version: '3' + +services: + web: + build: ./app + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - DB_HOST=db + - DB_USER=postgres + - DB_PASSWORD=secret + - DB_NAME=myapp + depends_on: + - db + restart: always + + db: + image: postgres:13 + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=myapp + restart: always + +volumes: + db-data: +``` + +### 微服务架构 + +```yaml +version: '3' + +services: + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + depends_on: + - api + - client + + client: + build: + context: ./client + dockerfile: Dockerfile + environment: + - REACT_APP_API_URL=http://api:5000 + + api: + build: + context: ./server + dockerfile: Dockerfile + environment: + - MONGO_URI=mongodb://mongo:27017/myapp + - REDIS_HOST=redis + depends_on: + - mongo + - redis + + mongo: + image: mongo:latest + volumes: + - mongo-data:/data/db + + redis: + image: redis:latest + volumes: + - redis-data:/data + +volumes: + mongo-data: + redis-data: +``` + +### 开发环境配置 + +```yaml +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - ./:/app + - /app/node_modules + ports: + - "3000:3000" + environment: + - NODE_ENV=development + command: npm run dev + + test: + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - ./:/app + - /app/node_modules + command: npm run test +``` + +## 多环境配置 + +### 使用多个Compose文件 + +基本配置:`docker-compose.yml` +```yaml +version: '3' + +services: + web: + image: myapp:latest + restart: always +``` + +开发环境配置:`docker-compose.override.yml`(默认与基本配置合并) +```yaml +services: + web: + build: . + volumes: + - ./:/app + environment: + - DEBUG=true +``` + +生产环境配置:`docker-compose.prod.yml` +```yaml +services: + web: + environment: + - DEBUG=false + deploy: + replicas: 3 +``` + +使用特定环境配置: +```bash +# 开发环境(默认使用docker-compose.yml和docker-compose.override.yml) +docker-compose up -d + +# 生产环境 +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d +``` + +### 使用环境变量 + +`.env`文件: +``` +COMPOSE_PROJECT_NAME=myapp +DB_PASSWORD=secret +API_PORT=5000 +``` + +`docker-compose.yml`: +```yaml +version: '3' + +services: + api: + image: myapi + ports: + - "${API_PORT}:5000" + + db: + image: postgres + environment: + - POSTGRES_PASSWORD=${DB_PASSWORD} +``` + +## Docker Compose与Swarm模式 + +Docker Compose可以与Docker Swarm一起使用,实现服务的编排和扩展。 + +### 部署到Swarm + +```bash +# 初始化Swarm +docker swarm init + +# 使用Compose文件部署堆栈 +docker stack deploy -c docker-compose.yml myapp + +# 列出所有堆栈 +docker stack ls + +# 查看堆栈中的服务 +docker stack services myapp + +# 查看堆栈中的任务 +docker stack ps myapp + +# 移除堆栈 +docker stack rm myapp +``` + +### Swarm模式特定配置 + +```yaml +version: '3' + +services: + web: + image: nginx + deploy: + mode: replicated + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + placement: + constraints: + - node.role == worker + resources: + limits: + cpus: '0.5' + memory: 512M +``` + +## 最佳实践 + +### 项目结构 + +``` +project/ +├── docker-compose.yml # 主配置文件 +├── docker-compose.override.yml # 开发环境配置 +├── docker-compose.prod.yml # 生产环境配置 +├── .env # 环境变量 +├── app/ # 应用代码 +│ ├── Dockerfile # 应用Dockerfile +│ └── ... +├── nginx/ # Nginx配置 +│ ├── Dockerfile # Nginx Dockerfile +│ └── default.conf # Nginx配置文件 +└── scripts/ # 辅助脚本 + ├── backup.sh # 备份脚本 + └── deploy.sh # 部署脚本 +``` + +### 安全最佳实践 + +1. **使用环境变量或secrets管理敏感信息** + ```yaml + services: + db: + environment: + - POSTGRES_PASSWORD_FILE=/run/secrets/db_password + secrets: + - db_password + + secrets: + db_password: + file: ./secrets/db_password.txt + ``` + +2. **限制容器资源** + ```yaml + services: + app: + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + ``` + +3. **使用非root用户** + ```yaml + services: + app: + user: "1000:1000" + ``` + +### 性能优化 + +1. **使用多阶段构建** + ```dockerfile + # Dockerfile + FROM node:14 AS builder + WORKDIR /app + COPY package*.json ./ + RUN npm install + COPY . . + RUN npm run build + + FROM nginx:alpine + COPY --from=builder /app/build /usr/share/nginx/html + ``` + +2. **优化卷挂载** + ```yaml + services: + app: + volumes: + - ./src:/app/src # 只挂载需要的目录 + - /app/node_modules # 排除node_modules + ``` + +3. **使用健康检查** + ```yaml + services: + web: + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + ``` + +## 常见问题与解决方案 + +### 容器间通信问题 + +**问题**:服务无法通过服务名访问其他服务 + +**解决方案**: +1. 确保服务在同一网络中 +2. 使用`depends_on`确保依赖服务先启动 +3. 检查服务名称是否正确 +4. 使用`docker-compose ps`确认所有服务都已启动 + +### 卷挂载权限问题 + +**问题**:容器无法写入挂载的卷 + +**解决方案**: +1. 在Dockerfile中设置正确的用户和权限 +2. 使用`user`选项指定用户ID +3. 在宿主机上预先设置正确的权限 + +### 环境变量问题 + +**问题**:环境变量未正确传递到容器 + +**解决方案**: +1. 检查`.env`文件格式是否正确 +2. 确认变量名称拼写正确 +3. 使用`docker-compose config`验证配置 +4. 尝试使用`env_file`选项而不是`.env`文件 + +### 网络端口冲突 + +**问题**:启动服务时出现端口已被占用错误 + +**解决方案**: +1. 更改端口映射 +2. 停止占用端口的其他服务 +3. 使用`docker-compose down`确保旧服务已停止 + +## 总结 + +Docker Compose是一个强大的工具,用于定义和运行多容器Docker应用程序。通过使用声明式YAML配置文件,它简化了容器的创建、网络连接和数据卷管理。无论是开发环境、测试环境还是生产环境,Docker Compose都能提供一致的应用程序部署体验。 + +掌握Docker Compose的配置选项、命令和最佳实践,将帮助您更有效地管理容器化应用程序,提高开发和部署效率。 + +## 下一步学习 + +- [Docker Swarm集群](./docker-swarm.md) +- [Docker数据卷管理](./docker-volumes.md) +- [Docker安全最佳实践](./docker-security.md) +- [Docker生产环境部署](./docker-production.md) \ No newline at end of file diff --git a/docs/docker/docker-containers.md b/docs/docker/docker-containers.md new file mode 100644 index 000000000..d5fcc3a4b --- /dev/null +++ b/docs/docker/docker-containers.md @@ -0,0 +1,507 @@ +# Docker容器管理 + +## 什么是Docker容器 + +Docker容器是Docker镜像的运行实例,是一个可执行的软件包,包含运行应用程序所需的一切:代码、运行时环境、系统工具、系统库和设置。容器将应用程序与其环境隔离开来,确保它可以在任何地方以相同的方式工作。 + +## 容器与虚拟机的区别 + +| 特性 | 容器 | 虚拟机 | +|------|------|--------| +| 启动时间 | 秒级 | 分钟级 | +| 占用资源 | 轻量级(MB) | 重量级(GB) | +| 隔离级别 | 进程级隔离 | 硬件级隔离 | +| 操作系统 | 共享宿主机内核 | 独立操作系统 | +| 数量 | 单机可运行数千个 | 单机通常几十个 | +| 性能 | 接近原生 | 有一定损耗 | + +## 容器生命周期 + +![容器生命周期](https://docs.docker.com/engine/images/architecture.svg) + +Docker容器的生命周期包括以下状态: + +- **created**:已创建但未启动 +- **running**:正在运行 +- **paused**:暂停运行 +- **stopped**:已停止运行 +- **deleted**:已删除 + +## 容器基本操作 + +### 创建和运行容器 + +```bash +# 创建并启动容器(前台运行) +docker run nginx + +# 创建并启动容器(后台运行) +docker run -d nginx + +# 指定容器名称 +docker run -d --name webserver nginx + +# 端口映射 +docker run -d -p 8080:80 nginx + +# 环境变量 +docker run -d -e MYSQL_ROOT_PASSWORD=password mysql + +# 挂载卷 +docker run -d -v /host/path:/container/path nginx + +# 使用网络 +docker run -d --network my-network nginx +``` + +### 查看容器 + +```bash +# 查看运行中的容器 +docker ps + +# 查看所有容器(包括已停止的) +docker ps -a + +# 查看最近创建的容器 +docker ps -n 5 + +# 只显示容器ID +docker ps -q + +# 查看容器详细信息 +docker inspect container_id + +# 查看容器日志 +docker logs container_id + +# 实时查看日志 +docker logs -f container_id + +# 查看容器内进程 +docker top container_id + +# 查看容器资源使用情况 +docker stats container_id +``` + +### 容器控制操作 + +```bash +# 启动容器 +docker start container_id + +# 停止容器 +docker stop container_id + +# 重启容器 +docker restart container_id + +# 暂停容器 +docker pause container_id + +# 恢复容器 +docker unpause container_id + +# 强制停止容器 +docker kill container_id +``` + +### 容器交互操作 + +```bash +# 在运行中的容器内执行命令 +docker exec -it container_id /bin/bash + +# 在新容器中启动交互式shell +docker run -it ubuntu /bin/bash + +# 将文件从主机复制到容器 +docker cp /host/path/file container_id:/container/path/ + +# 将文件从容器复制到主机 +docker cp container_id:/container/path/file /host/path/ +``` + +### 容器清理操作 + +```bash +# 删除容器 +docker rm container_id + +# 强制删除运行中的容器 +docker rm -f container_id + +# 删除所有已停止的容器 +docker container prune + +# 删除所有容器(包括运行中的) +docker rm -f $(docker ps -aq) +``` + +## 容器资源管理 + +### 限制CPU资源 + +```bash +# 限制CPU使用率(最多使用2个CPU核心) +docker run -d --cpus=2 nginx + +# 指定CPU核心使用权重 +docker run -d --cpu-shares=512 nginx + +# 指定使用特定的CPU核心 +docker run -d --cpuset-cpus=0,1 nginx +``` + +### 限制内存资源 + +```bash +# 限制内存使用(最多使用512MB) +docker run -d --memory=512m nginx + +# 限制内存和交换空间 +docker run -d --memory=512m --memory-swap=1g nginx + +# 设置内存预留 +docker run -d --memory=512m --memory-reservation=256m nginx +``` + +### 限制IO资源 + +```bash +# 限制读写速度 +docker run -d --device-read-bps=/dev/sda:1mb --device-write-bps=/dev/sda:1mb nginx + +# 限制读写IOPS +docker run -d --device-read-iops=/dev/sda:1000 --device-write-iops=/dev/sda:1000 nginx +``` + +## 容器网络配置 + +### 网络模式 + +Docker提供了多种网络模式: + +- **bridge**:默认网络模式,容器通过网桥连接 +- **host**:容器共享宿主机网络命名空间 +- **none**:容器没有网络连接 +- **container**:容器共享其他容器的网络命名空间 +- **自定义网络**:用户创建的网络 + +```bash +# 使用桥接网络 +docker run -d --network bridge nginx + +# 使用主机网络 +docker run -d --network host nginx + +# 不使用网络 +docker run -d --network none nginx + +# 共享其他容器的网络 +docker run -d --network container:container_id nginx +``` + +### 创建自定义网络 + +```bash +# 创建桥接网络 +docker network create my-network + +# 创建具有特定子网和网关的网络 +docker network create --subnet=172.18.0.0/16 --gateway=172.18.0.1 my-network + +# 创建使用特定驱动的网络 +docker network create --driver overlay my-network +``` + +### 网络管理命令 + +```bash +# 列出所有网络 +docker network ls + +# 查看网络详情 +docker network inspect my-network + +# 将容器连接到网络 +docker network connect my-network container_id + +# 断开容器与网络的连接 +docker network disconnect my-network container_id + +# 删除网络 +docker network rm my-network + +# 删除所有未使用的网络 +docker network prune +``` + +## 容器数据管理 + +### 数据卷(Volumes) + +数据卷是Docker管理的宿主机文件系统的一部分,由Docker管理。 + +```bash +# 创建数据卷 +docker volume create my-volume + +# 使用数据卷运行容器 +docker run -d -v my-volume:/container/path nginx + +# 查看所有数据卷 +docker volume ls + +# 查看数据卷详情 +docker volume inspect my-volume + +# 删除数据卷 +docker volume rm my-volume + +# 删除所有未使用的数据卷 +docker volume prune +``` + +### 绑定挂载(Bind Mounts) + +绑定挂载是将宿主机上的文件或目录挂载到容器中。 + +```bash +# 使用绑定挂载运行容器 +docker run -d -v /host/path:/container/path nginx + +# 使用只读绑定挂载 +docker run -d -v /host/path:/container/path:ro nginx + +# 使用新语法进行绑定挂载 +docker run -d --mount type=bind,source=/host/path,target=/container/path nginx +``` + +### tmpfs挂载 + +tmpfs挂载在容器内存中创建临时文件系统。 + +```bash +# 使用tmpfs挂载 +docker run -d --tmpfs /container/path nginx + +# 指定tmpfs选项 +docker run -d --tmpfs /container/path:rw,noexec,nosuid,size=100m nginx + +# 使用新语法进行tmpfs挂载 +docker run -d --mount type=tmpfs,destination=/container/path nginx +``` + +## 容器编排与扩展 + +### Docker Compose + +Docker Compose是用于定义和运行多容器Docker应用程序的工具。 + +```yaml +# docker-compose.yml示例 +version: '3' +services: + web: + image: nginx + ports: + - "80:80" + volumes: + - ./html:/usr/share/nginx/html + depends_on: + - db + db: + image: mysql + environment: + MYSQL_ROOT_PASSWORD: example +``` + +```bash +# 启动所有服务 +docker-compose up -d + +# 停止所有服务 +docker-compose down + +# 查看服务状态 +docker-compose ps + +# 查看服务日志 +docker-compose logs + +# 扩展服务实例数量 +docker-compose up -d --scale web=3 +``` + +### Docker Swarm + +Docker Swarm是Docker的原生集群管理工具。 + +```bash +# 初始化Swarm集群 +docker swarm init --advertise-addr + +# 创建服务 +docker service create --replicas 3 --name web nginx + +# 查看服务 +docker service ls + +# 查看服务详情 +docker service inspect web + +# 查看服务任务 +docker service ps web + +# 扩展服务 +docker service scale web=5 + +# 更新服务 +docker service update --image nginx:1.19 web + +# 删除服务 +docker service rm web +``` + +## 容器监控与日志 + +### 监控容器 + +```bash +# 查看容器资源使用情况 +docker stats + +# 查看特定容器的资源使用情况 +docker stats container_id + +# 使用cAdvisor监控容器 +docker run -d --name=cadvisor \ + -p 8080:8080 \ + --volume=/:/rootfs:ro \ + --volume=/var/run:/var/run:ro \ + --volume=/sys:/sys:ro \ + --volume=/var/lib/docker/:/var/lib/docker:ro \ + --volume=/dev/disk/:/dev/disk:ro \ + google/cadvisor:latest +``` + +### 容器日志管理 + +```bash +# 查看容器日志 +docker logs container_id + +# 查看最近的日志 +docker logs --tail 100 container_id + +# 实时查看日志 +docker logs -f container_id + +# 查看带时间戳的日志 +docker logs -t container_id + +# 限制日志大小和文件数量 +docker run -d --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 nginx +``` + +## 容器安全最佳实践 + +1. **使用非root用户运行容器** + +```dockerfile +FROM nginx:latest +RUN groupadd -r appgroup && useradd -r -g appgroup appuser +USER appuser +``` + +2. **限制容器资源** + +```bash +docker run -d --cpus=0.5 --memory=512m nginx +``` + +3. **使用只读文件系统** + +```bash +docker run -d --read-only nginx +``` + +4. **使用安全计算模式(seccomp)** + +```bash +docker run -d --security-opt seccomp=/path/to/seccomp/profile.json nginx +``` + +5. **禁用特权模式** + +```bash +# 不要使用 +docker run --privileged nginx + +# 如果需要特定权限,使用cap-add +docker run --cap-add NET_ADMIN nginx +``` + +6. **定期更新镜像** + +```bash +docker pull nginx:latest +``` + +7. **使用内容信任** + +```bash +export DOCKER_CONTENT_TRUST=1 +docker pull nginx:latest +``` + +## 容器故障排查 + +### 常见问题与解决方案 + +1. **容器无法启动** + - 检查日志:`docker logs container_id` + - 检查配置:`docker inspect container_id` + - 检查资源限制:确保有足够的CPU、内存资源 + +2. **容器网络问题** + - 检查网络配置:`docker network inspect network_name` + - 检查端口映射:`docker port container_id` + - 使用网络调试工具:`docker run --net container:container_id nicolaka/netshoot` + +3. **容器性能问题** + - 监控资源使用:`docker stats container_id` + - 检查日志是否有错误 + - 考虑增加资源限制或优化应用 + +### 调试技巧 + +```bash +# 在运行中的容器内执行命令 +docker exec -it container_id /bin/bash + +# 查看容器内进程 +docker top container_id + +# 查看容器事件 +docker events --filter container=container_id + +# 导出容器文件系统 +docker export container_id > container.tar + +# 查看容器差异 +docker diff container_id +``` + +## 总结 + +Docker容器是轻量级、可移植的应用运行环境,通过合理管理容器,可以显著提高应用的部署效率和可靠性。掌握容器的基本操作、资源管理、网络配置和数据管理等方面的知识,对于高效使用Docker至关重要。 + +## 下一步学习 + +- [Docker网络配置](./docker-network.md) +- [Docker数据卷管理](./docker-volumes.md) +- [Docker Compose详解](./docker-compose.md) +- [Docker Swarm集群](./docker-swarm.md) \ No newline at end of file diff --git a/docs/docker/docker-dockerfile.md b/docs/docker/docker-dockerfile.md new file mode 100644 index 000000000..2243d0e97 --- /dev/null +++ b/docs/docker/docker-dockerfile.md @@ -0,0 +1,588 @@ +# Dockerfile最佳实践 + +## Dockerfile基础 + +### 什么是Dockerfile + +Dockerfile是一个文本文件,包含了一系列指令和参数,用于自动构建Docker镜像。通过Dockerfile,您可以定义镜像的基础环境、安装软件包、配置环境变量、暴露端口等,实现镜像构建过程的自动化和标准化。 + +### Dockerfile的基本结构 + +Dockerfile由一系列指令组成,每条指令都会创建一个新的镜像层。基本结构如下: + +```dockerfile +# 注释 +INSTRUCTION arguments +``` + +常用的Dockerfile指令包括: + +| 指令 | 描述 | +|------|------| +| FROM | 指定基础镜像 | +| LABEL | 添加元数据(如维护者信息) | +| RUN | 执行命令 | +| COPY/ADD | 复制文件到镜像中 | +| WORKDIR | 设置工作目录 | +| ENV | 设置环境变量 | +| EXPOSE | 声明容器监听的端口 | +| VOLUME | 创建挂载点 | +| USER | 指定运行容器时的用户 | +| CMD | 指定容器启动时执行的命令 | +| ENTRYPOINT | 指定容器启动时执行的入口点 | + +### 构建和运行 + +```bash +# 构建镜像 +docker build -t myapp:1.0 . + +# 运行容器 +docker run -d -p 8080:80 myapp:1.0 +``` + +## Dockerfile最佳实践 + +### 使用合适的基础镜像 + +选择合适的基础镜像对于构建高效、安全的Docker镜像至关重要。 + +#### 官方镜像优先 + +优先使用Docker Hub上的官方镜像,这些镜像通常由软件维护者或Docker官方维护,安全性和稳定性较高。 + +```dockerfile +# 推荐:使用官方镜像 +FROM node:14-alpine + +# 不推荐:使用非官方镜像 +FROM someone/node:14 +``` + +#### 选择精简版本 + +使用Alpine或Slim版本的基础镜像可以显著减小镜像体积。 + +```dockerfile +# 推荐:使用Alpine版本 +FROM python:3.9-alpine + +# 或者使用Slim版本 +FROM python:3.9-slim + +# 不推荐:使用完整版本(除非有特殊需求) +FROM python:3.9 +``` + +#### 指定具体版本标签 + +使用具体的版本标签而不是`latest`,确保构建的可重复性。 + +```dockerfile +# 推荐:指定具体版本 +FROM nginx:1.21.3-alpine + +# 不推荐:使用latest标签 +FROM nginx:latest +``` + +### 优化层次结构 + +合理组织Dockerfile指令,优化镜像层次结构,可以减小镜像体积并提高构建效率。 + +#### 合并RUN指令 + +将多个RUN指令合并为一个,减少镜像层数。 + +```dockerfile +# 推荐:合并RUN指令 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + package1 \ + package2 \ + package3 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# 不推荐:多个RUN指令 +RUN apt-get update +RUN apt-get install -y package1 +RUN apt-get install -y package2 +RUN apt-get install -y package3 +RUN apt-get clean +``` + +#### 按照变更频率排序指令 + +将不经常变更的指令放在Dockerfile的前面,经常变更的放在后面,利用Docker的构建缓存机制提高构建效率。 + +```dockerfile +# 推荐:按变更频率排序 +FROM node:14-alpine + +# 不经常变更的依赖 +RUN apk add --no-cache python3 make g++ + +# 复制package.json和package-lock.json +COPY package*.json ./ +RUN npm ci + +# 经常变更的源代码 +COPY . . +``` + +#### 使用.dockerignore文件 + +创建`.dockerignore`文件,排除不需要的文件和目录,减小构建上下文大小。 + +``` +# .dockerignore示例 +node_modules +npm-debug.log +.git +.gitignore +.env +*.md +tests/ +docs/ +``` + +### 减小镜像体积 + +构建尽可能小的镜像可以减少存储空间、加快部署速度并减少攻击面。 + +#### 清理不必要的文件 + +在同一个RUN指令中安装软件包并清理缓存文件。 + +```dockerfile +# 推荐:安装后清理 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + package1 \ + package2 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# 对于Alpine +RUN apk add --no-cache package1 package2 +``` + +#### 使用多阶段构建 + +多阶段构建可以将构建环境与运行环境分离,只将必要的文件复制到最终镜像中。 + +```dockerfile +# 构建阶段 +FROM node:14 AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# 运行阶段 +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +#### 使用特定用户 + +避免使用root用户运行应用,创建专用用户提高安全性。 + +```dockerfile +# 创建非root用户 +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser +``` + +### 提高构建效率 + +优化Dockerfile可以显著提高构建速度,特别是在开发过程中。 + +#### 利用构建缓存 + +了解Docker的缓存机制,合理排序指令以最大化缓存利用。 + +```dockerfile +# 推荐:先复制依赖文件,安装依赖,再复制源代码 +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . + +# 不推荐:直接复制所有文件,导致源代码变更时无法利用缓存 +COPY . . +RUN npm ci +``` + +#### 使用BuildKit + +启用Docker BuildKit可以并行处理构建步骤,提高构建速度。 + +```bash +# 启用BuildKit +export DOCKER_BUILDKIT=1 + +# 或者在构建时指定 +DOCKER_BUILDKIT=1 docker build -t myapp . +``` + +#### 使用缓存挂载 + +在BuildKit中使用缓存挂载可以在构建之间保留某些目录,如依赖缓存。 + +```dockerfile +# 使用缓存挂载(需要BuildKit) +# syntax=docker/dockerfile:1.3 +FROM node:14-alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci +COPY . . +``` + +### 安全最佳实践 + +构建安全的Docker镜像对于保护应用和基础设施至关重要。 + +#### 使用可信基础镜像 + +使用官方或可信的基础镜像,并定期更新以获取安全补丁。 + +```dockerfile +# 推荐:使用官方镜像并指定版本 +FROM debian:bullseye-slim +``` + +#### 最小化安装包 + +只安装必要的软件包,减少潜在的漏洞。 + +```dockerfile +# 推荐:使用--no-install-recommends选项 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + package1 \ + package2 +``` + +#### 不在镜像中存储敏感信息 + +避免在Dockerfile中包含密码、API密钥等敏感信息。 + +```dockerfile +# 不推荐:在Dockerfile中包含敏感信息 +ENV API_KEY="my-secret-key" + +# 推荐:使用构建参数,在运行时提供敏感信息 +# 构建时:docker build --build-arg API_KEY=xxx -t myapp . +# 或者在运行时提供:docker run -e API_KEY=xxx myapp +``` + +#### 扫描镜像漏洞 + +使用工具扫描镜像中的漏洞,如Trivy、Clair或Snyk。 + +```bash +# 使用Trivy扫描镜像 +trivy image myapp:1.0 +``` + +### 可维护性最佳实践 + +编写清晰、可维护的Dockerfile可以提高团队协作效率。 + +#### 使用有意义的标签 + +为镜像添加有意义的标签,提供元数据信息。 + +```dockerfile +LABEL maintainer="team@example.com" +LABEL version="1.0" +LABEL description="My application" +``` + +#### 使用参数和环境变量 + +使用ARG和ENV指令使Dockerfile更加灵活。 + +```dockerfile +# 构建参数 +ARG NODE_VERSION=14 +FROM node:${NODE_VERSION}-alpine + +# 环境变量 +ENV APP_HOME=/app +ENV NODE_ENV=production +WORKDIR ${APP_HOME} +``` + +#### 添加注释 + +在Dockerfile中添加注释,解释复杂或不明显的步骤。 + +```dockerfile +# 安装构建依赖 +RUN apk add --no-cache python3 make g++ + +# 配置应用 +# 注意:这里的配置适用于生产环境 +COPY config/production.json /app/config/ +``` + +## 语言特定的最佳实践 + +### Node.js应用 + +```dockerfile +FROM node:14-alpine + +WORKDIR /app + +# 复制依赖文件 +COPY package*.json ./ + +# 安装生产依赖 +RUN npm ci --only=production + +# 复制应用代码 +COPY . . + +# 使用非root用户 +USER node + +# 设置环境变量 +ENV NODE_ENV production + +# 启动应用 +CMD ["node", "server.js"] +``` + +### Python应用 + +```dockerfile +FROM python:3.9-slim + +WORKDIR /app + +# 安装依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制应用代码 +COPY . . + +# 创建非root用户 +RUN adduser --disabled-password --gecos "" appuser +USER appuser + +# 启动应用 +CMD ["python", "app.py"] +``` + +### Java应用 + +```dockerfile +# 构建阶段 +FROM maven:3.8-openjdk-11 AS builder +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn package -DskipTests + +# 运行阶段 +FROM openjdk:11-jre-slim +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar + +# 创建非root用户 +RUN adduser --system --group appuser +USER appuser + +CMD ["java", "-jar", "app.jar"] +``` + +### Go应用 + +```dockerfile +# 构建阶段 +FROM golang:1.17-alpine AS builder +WORKDIR /app + +# 安装构建依赖 +RUN apk add --no-cache git + +# 下载依赖 +COPY go.mod go.sum ./ +RUN go mod download + +# 构建应用 +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# 运行阶段 +FROM alpine:3.14 +RUN apk add --no-cache ca-certificates +WORKDIR /root/ + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/main . + +CMD ["./main"] +``` + +## 实际案例分析 + +### Web应用案例 + +以下是一个典型的Web应用Dockerfile,结合了多阶段构建、缓存优化和安全最佳实践: + +```dockerfile +# 构建阶段 +FROM node:14-alpine AS builder +WORKDIR /app + +# 复制并安装依赖 +COPY package*.json ./ +RUN npm ci + +# 复制源代码并构建 +COPY . . +RUN npm run build + +# 运行阶段 +FROM nginx:1.21-alpine + +# 配置Nginx +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 使用非root用户 +RUN addgroup -S appgroup && \ + adduser -S appuser -G appgroup && \ + chown -R appuser:appgroup /usr/share/nginx/html && \ + chmod -R 755 /usr/share/nginx/html && \ + chown -R appuser:appgroup /var/cache/nginx && \ + chown -R appuser:appgroup /var/log/nginx && \ + chown -R appuser:appgroup /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R appuser:appgroup /var/run/nginx.pid + +USER appuser + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] +``` + +### 微服务案例 + +以下是一个Go微服务的Dockerfile,专注于构建小型、安全的容器镜像: + +```dockerfile +# 构建阶段 +FROM golang:1.17-alpine AS builder +WORKDIR /app + +# 安装构建依赖 +RUN apk add --no-cache git ca-certificates + +# 下载依赖 +COPY go.mod go.sum ./ +RUN go mod download + +# 构建应用 +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o microservice . + +# 运行阶段 - 使用scratch镜像 +FROM scratch + +# 从构建阶段复制SSL证书 +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# 复制二进制文件 +COPY --from=builder /app/microservice /microservice + +# 复制配置文件 +COPY --from=builder /app/config.yaml /config.yaml + +EXPOSE 8080 + +CMD ["/microservice"] +``` + +## 常见问题与解决方案 + +### 构建缓存失效 + +**问题**:每次构建都重新安装依赖,即使依赖文件没有变化。 + +**解决方案**: +- 先复制依赖文件(如package.json),安装依赖,再复制其他文件 +- 使用.dockerignore排除不必要的文件 + +```dockerfile +# 正确的顺序 +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +``` + +### 镜像体积过大 + +**问题**:构建的镜像体积过大,影响部署和传输效率。 + +**解决方案**: +- 使用多阶段构建 +- 使用Alpine或Slim基础镜像 +- 清理构建缓存和临时文件 +- 只安装生产环境必要的依赖 + +### 容器启动缓慢 + +**问题**:容器启动时间过长。 + +**解决方案**: +- 优化应用启动过程 +- 使用适当的ENTRYPOINT脚本进行健康检查和初始化 +- 考虑使用静态编译的语言(如Go) + +```dockerfile +# 添加健康检查 +HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 +``` + +### 安全漏洞 + +**问题**:镜像中存在安全漏洞。 + +**解决方案**: +- 使用最新的基础镜像 +- 定期更新依赖 +- 使用漏洞扫描工具 +- 以非root用户运行应用 + +## 总结 + +Dockerfile最佳实践是构建高效、安全、可维护Docker镜像的关键。通过遵循本文介绍的最佳实践,您可以: + +1. **减小镜像体积**:使用多阶段构建、Alpine基础镜像和清理临时文件 +2. **提高构建效率**:优化缓存利用、合理排序指令和使用BuildKit +3. **增强安全性**:使用非root用户、最小化安装包和避免存储敏感信息 +4. **提高可维护性**:添加标签和注释、使用参数和环境变量 + +通过这些实践,您可以构建出更加高效、安全和可维护的Docker镜像,为您的应用提供更好的容器化环境。 + +## 下一步学习 + +- [Docker镜像管理](./docker-images.md) +- [Docker安全最佳实践](./docker-security.md) +- [Docker生产环境部署](./docker-production.md) +- [Docker Compose详解](./docker-compose.md) \ No newline at end of file diff --git a/docs/docker/docker-images.md b/docs/docker/docker-images.md new file mode 100644 index 000000000..9b270d5b3 --- /dev/null +++ b/docs/docker/docker-images.md @@ -0,0 +1,289 @@ +# Docker镜像管理 + +## 什么是Docker镜像 + +Docker镜像是一个只读的模板,用于创建Docker容器。镜像可以理解为一个包含了应用程序及其依赖环境的文件系统,它是容器的基础。Docker镜像由多个层(layers)组成,每层都是只读的,这些层堆叠在一起形成最终的镜像。 + +## 镜像的特点 + +- **分层结构**:Docker镜像由多个层组成,每层代表Dockerfile中的一条指令 +- **共享层**:不同的镜像可以共享相同的层,节省存储空间 +- **只读**:镜像层是只读的,容器运行时会在最上层添加一个可写层 +- **版本控制**:镜像支持标签(tag)机制,可以对不同版本进行管理 + +## 镜像命名与标签 + +镜像的完整名称由以下部分组成: + +``` +[registry-host]:[port]/[username]/[repository]:[tag] +``` + +- **registry-host:port**:镜像仓库地址,默认为Docker Hub +- **username**:用户名或组织名 +- **repository**:镜像名称 +- **tag**:镜像标签,默认为latest + +例如:`docker.io/nginx:1.19.0`、`registry.example.com:5000/myapp:v1.0` + +## 镜像基本操作 + +### 查看本地镜像 + +```bash +# 列出本地所有镜像 +docker images +# 或 +docker image ls + +# 查看镜像详细信息 +docker image inspect nginx:latest + +# 查看镜像历史 +docker history nginx:latest +``` + +### 搜索镜像 + +```bash +# 搜索Docker Hub上的镜像 +docker search nginx + +# 限制搜索结果数量 +docker search --limit 5 nginx + +# 只显示官方镜像 +docker search --filter "is-official=true" nginx + +# 按星级筛选 +docker search --filter "stars=1000" nginx +``` + +### 拉取镜像 + +```bash +# 拉取最新版本 +docker pull nginx + +# 拉取指定版本 +docker pull nginx:1.19.0 + +# 拉取指定仓库的镜像 +docker pull registry.example.com:5000/myapp:v1.0 +``` + +### 推送镜像 + +```bash +# 登录到Docker Hub +docker login + +# 标记本地镜像 +docker tag myapp:latest username/myapp:latest + +# 推送到Docker Hub +docker push username/myapp:latest + +# 推送到私有仓库 +docker push registry.example.com:5000/myapp:latest +``` + +### 删除镜像 + +```bash +# 删除指定镜像 +docker rmi nginx:latest +# 或 +docker image rm nginx:latest + +# 强制删除 +docker rmi -f nginx:latest + +# 删除未使用的镜像 +docker image prune + +# 删除所有未使用的镜像(包括未标记的) +docker image prune -a +``` + +## 构建自定义镜像 + +### 使用Dockerfile构建 + +1. 创建Dockerfile + +```dockerfile +FROM nginx:latest +COPY ./html /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +2. 构建镜像 + +```bash +# 基本构建命令 +docker build -t myapp:latest . + +# 指定Dockerfile路径 +docker build -t myapp:latest -f /path/to/Dockerfile . + +# 添加构建参数 +docker build --build-arg VERSION=1.0 -t myapp:latest . + +# 不使用缓存构建 +docker build --no-cache -t myapp:latest . +``` + +### 使用现有容器创建镜像 + +```bash +# 从运行中的容器创建镜像 +docker commit -m "Added new files" -a "Author" container_id username/myapp:latest +``` + +## 镜像优化技巧 + +### 减小镜像大小 + +1. **使用轻量级基础镜像** + - 使用Alpine Linux作为基础镜像 + - 使用特定语言的精简版镜像(如node:alpine) + +2. **多阶段构建** + +```dockerfile +# 构建阶段 +FROM maven:3.8.1-openjdk-11-slim AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn package -DskipTests + +# 运行阶段 +FROM openjdk:11-jre-slim +WORKDIR /app +COPY --from=build /app/target/myapp.jar . +EXPOSE 8080 +CMD ["java", "-jar", "myapp.jar"] +``` + +3. **合并RUN指令** + +```dockerfile +# 不推荐 +RUN apt-get update +RUN apt-get install -y package1 +RUN apt-get install -y package2 +RUN apt-get clean + +# 推荐 +RUN apt-get update && \ + apt-get install -y package1 package2 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +``` + +4. **使用.dockerignore文件** + +``` +.git +node_modules +tmp +log +*.md +``` + +### 提高构建效率 + +1. **合理安排Dockerfile指令顺序** + - 将不常变化的指令放在前面 + - 将频繁变化的指令放在后面 + +2. **利用构建缓存** + - 避免不必要的`--no-cache`选项 + - 使用`COPY`代替`ADD`(除非需要自动解压) + +## 镜像安全最佳实践 + +1. **使用官方镜像** + - 优先使用Docker官方认证的镜像 + - 检查镜像的下载量和星级 + +2. **定期更新基础镜像** + - 使用CI/CD自动构建最新版本 + - 设置镜像扫描机制 + +3. **不在镜像中存储敏感信息** + - 使用环境变量或Docker secrets + - 避免在Dockerfile中硬编码密钥 + +4. **以非root用户运行** + +```dockerfile +FROM node:14-alpine +WORKDIR /app +COPY . . +RUN npm install +# 创建非root用户 +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +# 切换到非root用户 +USER appuser +CMD ["node", "app.js"] +``` + +## 镜像仓库管理 + +### Docker Hub + +- 公共镜像仓库,提供免费和付费计划 +- 支持自动构建、团队协作等功能 + +### 私有仓库 + +1. **Docker Registry** + +```bash +# 启动本地Registry +docker run -d -p 5000:5000 --name registry registry:2 + +# 推送镜像到本地Registry +docker tag myapp:latest localhost:5000/myapp:latest +docker push localhost:5000/myapp:latest +``` + +2. **Harbor** + - 企业级Registry,提供RBAC、漏洞扫描等功能 + - 支持多租户、镜像复制等高级特性 + +## 常见问题与解决方案 + +### 拉取镜像失败 + +1. **网络问题** + - 检查网络连接 + - 配置镜像加速器 + +2. **权限问题** + - 确认已登录到私有仓库 + - 检查用户权限 + +### 镜像构建失败 + +1. **上下文问题** + - 确保构建上下文正确 + - 使用.dockerignore排除不需要的文件 + +2. **依赖问题** + - 检查网络连接 + - 确保依赖包可访问 + +## 总结 + +Docker镜像是容器化应用的基础,掌握镜像的管理和优化技巧对于高效使用Docker至关重要。通过合理构建和管理镜像,可以显著提高开发和部署效率,同时保证应用的安全性和可靠性。 + +## 下一步学习 + +- [Docker容器管理](./docker-containers.md) +- [Docker网络配置](./docker-network.md) +- [Docker数据卷管理](./docker-volumes.md) \ No newline at end of file diff --git a/docs/docker/docker-install.md b/docs/docker/docker-install.md new file mode 100644 index 000000000..027816614 --- /dev/null +++ b/docs/docker/docker-install.md @@ -0,0 +1,494 @@ +# Docker安装指南 + +本文将详细介绍如何在不同操作系统上安装Docker,包括主流Linux发行版和Windows系统。无论您是在生产服务器上还是在开发环境中使用Docker,本指南都能帮助您快速完成安装和初始配置。 + +## Linux服务器安装Docker + +### Ubuntu安装Docker + +Ubuntu是最流行的Linux发行版之一,下面是在Ubuntu上安装Docker的步骤: + +#### 前置条件 + +- 64位Ubuntu系统(建议使用LTS版本:18.04、20.04或22.04) +- 具有sudo权限的用户 + +#### 安装步骤 + +1. 更新软件包索引并安装必要的依赖: + +```bash +sudo apt-get update + +sudo apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release +``` + +2. 添加Docker官方GPG密钥: + +```bash +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +``` + +3. 设置Docker稳定版仓库: + +```bash +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +4. 安装Docker Engine: + +```bash +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io +``` + +5. 验证安装: + +```bash +sudo docker run hello-world +``` + +6. (可选)将当前用户添加到docker组,以便无需sudo即可运行docker命令: + +```bash +sudo usermod -aG docker $USER +# 注销并重新登录,或者运行以下命令应用组更改 +newgrp docker +``` + +### CentOS安装Docker + +CentOS是企业级服务器中常用的Linux发行版,以下是在CentOS上安装Docker的步骤: + +#### 前置条件 + +- 64位CentOS 7或CentOS 8 +- 具有sudo权限的用户 + +#### 安装步骤 + +1. 安装必要的依赖: + +```bash +sudo yum install -y yum-utils device-mapper-persistent-data lvm2 +``` + +2. 添加Docker仓库: + +```bash +sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +``` + +3. 安装Docker Engine: + +```bash +sudo yum install -y docker-ce docker-ce-cli containerd.io +``` + +4. 启动Docker服务并设置开机自启: + +```bash +sudo systemctl start docker +sudo systemctl enable docker +``` + +5. 验证安装: + +```bash +sudo docker run hello-world +``` + +6. (可选)将当前用户添加到docker组: + +```bash +sudo usermod -aG docker $USER +# 注销并重新登录,或者运行以下命令应用组更改 +newgrp docker +``` + +### Debian安装Docker + +Debian是另一个流行的Linux发行版,以下是在Debian上安装Docker的步骤: + +#### 前置条件 + +- 64位Debian 10 (Buster)或Debian 11 (Bullseye) +- 具有sudo权限的用户 + +#### 安装步骤 + +1. 更新软件包索引并安装必要的依赖: + +```bash +sudo apt-get update + +sudo apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release +``` + +2. 添加Docker官方GPG密钥: + +```bash +curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +``` + +3. 设置Docker稳定版仓库: + +```bash +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +4. 安装Docker Engine: + +```bash +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io +``` + +5. 验证安装: + +```bash +sudo docker run hello-world +``` + +### RHEL/Rocky Linux/AlmaLinux安装Docker + +RHEL(Red Hat Enterprise Linux)及其开源替代品Rocky Linux和AlmaLinux是企业级环境中常用的发行版。 + +#### 前置条件 + +- 64位RHEL 8/9或Rocky Linux 8/9或AlmaLinux 8/9 +- 具有sudo权限的用户 + +#### 安装步骤 + +1. 安装必要的依赖: + +```bash +sudo dnf install -y dnf-plugins-core +``` + +2. 添加Docker仓库: + +```bash +sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo +``` + +3. 安装Docker Engine: + +```bash +sudo dnf install -y docker-ce docker-ce-cli containerd.io +``` + +4. 启动Docker服务并设置开机自启: + +```bash +sudo systemctl start docker +sudo systemctl enable docker +``` + +5. 验证安装: + +```bash +sudo docker run hello-world +``` + +### 使用便捷脚本安装Docker + +对于测试和开发环境,Docker提供了一个便捷脚本来快速安装: + +```bash +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +``` + +> **注意**:不建议在生产环境中使用便捷脚本安装Docker。 + +## Windows安装Docker + +在Windows上,有两种主要的Docker安装选项:Docker Desktop for Windows和WSL 2(Windows Subsystem for Linux 2)。 + +### 安装Docker Desktop for Windows + +Docker Desktop for Windows提供了一个完整的Docker开发环境,包括Docker Engine、Docker CLI、Docker Compose等。 + +#### 系统要求 + +- Windows 10 64位:专业版、企业版或教育版(Build 19041或更高版本) +- 或Windows 11 64位 +- 启用硬件虚拟化(在BIOS中开启) +- 至少4GB RAM + +#### 安装步骤 + +1. 从[Docker官网](https://www.docker.com/products/docker-desktop)下载Docker Desktop安装程序。 + +2. 双击安装程序并按照向导进行安装。 + +3. 安装过程中,确保选择"使用WSL 2"选项(推荐)。 + +4. 安装完成后,Docker Desktop将自动启动。 + +5. 验证安装:打开PowerShell或命令提示符,运行: + +```powershell +docker run hello-world +``` + +### 使用WSL 2安装Docker + +如果您已经使用WSL 2,也可以直接在WSL 2的Linux发行版中安装Docker,而不需要Docker Desktop。 + +#### 前置条件 + +- 已安装并配置WSL 2 +- 已安装Linux发行版(如Ubuntu) + +#### 安装步骤 + +1. 启动您的WSL 2 Linux发行版(例如Ubuntu)。 + +2. 按照上面的Linux安装指南(基于您的发行版)安装Docker。 + +3. 启动Docker服务: + +```bash +sudo service docker start +``` + +4. (可选)设置Docker在WSL启动时自动启动,将以下行添加到`~/.bashrc`或`~/.zshrc`: + +```bash +# 启动Docker服务 +if service docker status 2>&1 | grep -q "is not running"; then + wsl.exe -d ${WSL_DISTRO_NAME} -u root -e /usr/sbin/service docker start >/dev/null 2>&1 +fi +``` + +5. 验证安装: + +```bash +docker run hello-world +``` + +## 安装Docker Compose + +Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。以下是安装Docker Compose的方法: + +### Linux安装Docker Compose + +1. 下载当前稳定版本的Docker Compose: + +```bash +sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +``` + +2. 添加可执行权限: + +```bash +sudo chmod +x /usr/local/bin/docker-compose +``` + +3. 验证安装: + +```bash +docker-compose --version +``` + +### Windows安装Docker Compose + +如果您安装了Docker Desktop for Windows,Docker Compose已经包含在其中。 + +验证安装: + +```powershell +docker-compose --version +``` + +## 配置Docker镜像加速 + +在国内使用Docker时,从Docker Hub拉取镜像可能会很慢。配置镜像加速可以显著提高下载速度。 + +### Linux配置镜像加速 + +1. 创建或修改Docker守护进程配置文件: + +```bash +sudo mkdir -p /etc/docker +sudo tee /etc/docker/daemon.json <<-'EOF' +{ + "registry-mirrors": [ + "https://registry.docker-cn.com", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com" + ] +} +EOF +``` + +2. 重启Docker服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +### Windows配置镜像加速 + +1. 右键点击系统托盘中的Docker图标,选择"Settings"。 + +2. 点击"Docker Engine"。 + +3. 在JSON配置中添加或修改`registry-mirrors`字段: + +```json +{ + "registry-mirrors": [ + "https://registry.docker-cn.com", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com" + ] +} +``` + +4. 点击"Apply & Restart"应用更改。 + +## 安装后的基本操作 + +### 验证Docker安装 + +```bash +# 检查Docker版本 +docker --version +docker-compose --version + +# 验证Docker是否正常工作 +docker run hello-world + +# 查看Docker系统信息 +docker info +``` + +### 管理Docker服务 + +```bash +# Linux启动Docker服务 +sudo systemctl start docker + +# Linux停止Docker服务 +sudo systemctl stop docker + +# Linux重启Docker服务 +sudo systemctl restart docker + +# Linux查看Docker服务状态 +sudo systemctl status docker + +# Linux设置Docker开机自启 +sudo systemctl enable docker +``` + +### 基本Docker命令 + +```bash +# 列出本地镜像 +docker images + +# 搜索镜像 +docker search nginx + +# 拉取镜像 +docker pull nginx + +# 运行容器 +docker run -d -p 80:80 --name webserver nginx + +# 列出运行中的容器 +docker ps + +# 列出所有容器(包括已停止的) +docker ps -a + +# 停止容器 +docker stop webserver + +# 启动容器 +docker start webserver + +# 删除容器 +docker rm webserver + +# 删除镜像 +docker rmi nginx +``` + +## 常见问题排查 + +### 权限问题 + +如果遇到"Permission denied"错误: + +```bash +# 将当前用户添加到docker组 +sudo usermod -aG docker $USER + +# 应用组更改(无需注销) +newgrp docker +``` + +### 端口冲突 + +如果遇到"port is already allocated"错误: + +```bash +# 查找占用端口的进程 +sudo netstat -tulpn | grep <端口号> + +# 或者使用lsof +sudo lsof -i :<端口号> + +# 终止占用端口的进程 +sudo kill +``` + +### Docker服务无法启动 + +```bash +# 检查Docker日志 +sudo journalctl -u docker.service + +# 检查Docker守护进程状态 +sudo systemctl status docker +``` + +### 磁盘空间不足 + +```bash +# 清理未使用的Docker对象 +docker system prune -a + +# 查看Docker磁盘使用情况 +docker system df +``` + +## 总结 + +本文详细介绍了如何在各种Linux发行版和Windows系统上安装Docker,以及安装后的基本配置和常见问题排查。通过遵循本指南,您可以在服务器或开发环境中快速设置Docker环境,为容器化应用开发和部署打下基础。 + +## 下一步学习 + +- [Docker教程](./docker-tutorial.md) +- [Docker镜像管理](./docker-images.md) +- [Docker容器管理](./docker-containers.md) +- [Dockerfile最佳实践](./docker-dockerfile.md) \ No newline at end of file diff --git a/docs/docker/docker-mirror.md b/docs/docker/docker-mirror.md new file mode 100644 index 000000000..f8a63d1b1 --- /dev/null +++ b/docs/docker/docker-mirror.md @@ -0,0 +1,200 @@ +# Docker配置国内源 + +在国内使用Docker时,从Docker Hub拉取镜像可能会很慢,甚至出现超时的情况。配置国内镜像源可以显著提高镜像下载速度,本文将详细介绍如何在各种操作系统上配置Docker国内镜像源。 + +## 常用的Docker国内镜像源 + +以下是一些常用的Docker国内镜像源: + +| 镜像源名称 | 镜像源地址 | 说明 | +| --- | --- | --- | +| Docker中国官方镜像 | https://registry.docker-cn.com | Docker官方提供的中国区镜像 | +| 阿里云镜像 | https://[您的ID].mirror.aliyuncs.com | 需要登录阿里云获取专属地址 | +| 网易云镜像 | https://hub-mirror.c.163.com | 网易提供的镜像源 | +| 腾讯云镜像 | https://mirror.ccs.tencentyun.com | 腾讯云提供的镜像源 | +| 百度云镜像 | https://mirror.baidubce.com | 百度云提供的镜像源 | +| 中科大镜像 | https://docker.mirrors.ustc.edu.cn | 中国科学技术大学提供的镜像源 | +| 清华大学镜像 | https://docker.mirrors.sjtug.sjtu.edu.cn | 清华大学提供的镜像源 | + +## Linux系统配置Docker镜像源 + +### 方法一:修改daemon.json文件 + +1. 创建或修改Docker守护进程配置文件: + +```bash +sudo mkdir -p /etc/docker +sudo vi /etc/docker/daemon.json +``` + +2. 在文件中添加以下内容(可以选择一个或多个镜像源): + +```json +{ + "registry-mirrors": [ + "https://registry.docker-cn.com", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com", + "https://docker.mirrors.ustc.edu.cn" + ] +} +``` + +3. 重启Docker服务以应用更改: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +### 方法二:使用阿里云镜像加速器 + +阿里云提供了专属的镜像加速地址,性能更好: + +1. 登录阿里云控制台,搜索「容器镜像服务」 +2. 在左侧菜单选择「镜像工具」>「镜像加速器」 +3. 获取您的专属加速器地址 +4. 根据页面提供的操作指南配置Docker + +以下是使用阿里云镜像加速器的配置示例: + +```bash +sudo mkdir -p /etc/docker +sudo tee /etc/docker/daemon.json <<-'EOF' +{ + "registry-mirrors": ["https://abcdefg.mirror.aliyuncs.com"] +} +EOF +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +### 不同Linux发行版的特殊配置 + +#### Ubuntu/Debian系统 + +```bash +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +#### CentOS/RHEL系统 + +```bash +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +#### Alpine Linux + +```bash +sudo rc-service docker restart +``` + +## Windows系统配置Docker镜像源 + +### Docker Desktop for Windows + +1. 右键点击系统托盘中的Docker图标,选择「Settings」(设置) +2. 在左侧菜单中选择「Docker Engine」 +3. 在右侧JSON配置中添加或修改`registry-mirrors`字段: + +```json +{ + "registry-mirrors": [ + "https://registry.docker-cn.com", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com" + ] +} +``` + +4. 点击「Apply & Restart」应用更改并重启Docker + +## macOS系统配置Docker镜像源 + +### Docker Desktop for Mac + +1. 点击系统状态栏中的Docker图标,选择「Preferences」(偏好设置) +2. 在左侧菜单中选择「Docker Engine」 +3. 在右侧JSON配置中添加或修改`registry-mirrors`字段: + +```json +{ + "registry-mirrors": [ + "https://registry.docker-cn.com", + "https://hub-mirror.c.163.com", + "https://mirror.baidubce.com" + ] +} +``` + +4. 点击「Apply & Restart」应用更改并重启Docker + +## 验证镜像源配置 + +配置完成后,可以通过以下命令验证镜像源是否生效: + +```bash +docker info +``` + +在输出信息中,查找`Registry Mirrors`部分,应该能看到您配置的镜像源地址。 + +## 使用镜像源拉取镜像 + +配置镜像源后,使用`docker pull`命令拉取镜像时会自动使用配置的镜像源: + +```bash +# 拉取Nginx镜像 +docker pull nginx + +# 拉取特定版本的MySQL镜像 +docker pull mysql:8.0 +``` + +## 针对单次拉取使用镜像源 + +如果只想在单次拉取时使用镜像源,而不想修改全局配置,可以使用`--registry-mirror`参数: + +```bash +docker --registry-mirror=https://hub-mirror.c.163.com pull nginx +``` + +## 常见问题排查 + +### 配置后镜像拉取仍然很慢 + +1. 确认配置文件格式正确,没有语法错误 +2. 确认Docker服务已经重启 +3. 尝试使用不同的镜像源 +4. 检查网络连接是否正常 + +### 无法连接到镜像源 + +1. 检查镜像源地址是否正确 +2. 检查网络连接是否正常 +3. 尝试使用其他镜像源 + +### 配置文件被覆盖 + +某些Docker版本更新可能会覆盖配置文件,更新Docker后需要检查配置是否仍然有效。 + +## 企业级镜像源解决方案 + +对于企业用户,建议: + +1. 搭建私有Docker Registry +2. 使用Harbor等企业级镜像仓库管理系统 +3. 配置镜像同步任务,定期从Docker Hub同步常用镜像 + +## 总结 + +通过配置Docker国内镜像源,可以显著提高Docker镜像的下载速度,解决在国内网络环境下使用Docker的痛点。根据自己的网络环境和需求,选择合适的镜像源,并正确配置,可以获得最佳的使用体验。 + +## 相关资源 + +- [Docker官方文档](https://docs.docker.com/) +- [阿里云容器镜像服务](https://cr.console.aliyun.com/) +- [Docker安装指南](./docker-install.md) +- [Docker镜像管理](./docker-images.md) \ No newline at end of file diff --git a/docs/docker/docker-network.md b/docs/docker/docker-network.md new file mode 100644 index 000000000..a75c59177 --- /dev/null +++ b/docs/docker/docker-network.md @@ -0,0 +1,441 @@ +# Docker网络配置 + +## Docker网络架构 + +Docker使用插件化的网络架构,称为Container Network Model (CNM)。这种架构允许用户选择或替换Docker的网络驱动,以满足应用程序的需求。Docker内置了多种网络驱动,同时也支持网络插件。 + +### 网络架构组件 + +- **沙盒(Sandbox)**:包含容器网络栈的配置,如网络接口、路由表和DNS设置 +- **端点(Endpoint)**:将沙盒连接到网络的接口 +- **网络(Network)**:可以相互通信的端点集合 + +## Docker默认网络 + +当安装Docker时,它会自动创建三个网络: + +```bash +$ docker network ls +NETWORK ID NAME DRIVER SCOPE +9f6ae26fcaa1 bridge bridge local +82b9c4d3e5a6 host host local +33204c3f9584 none null local +``` + +### bridge网络 + +- 默认网络,新创建的容器会自动连接到此网络 +- 在宿主机上创建一个名为`docker0`的网桥 +- 容器可以通过此网桥相互通信,也可以访问外部网络 + +```bash +# 查看bridge网络详情 +docker network inspect bridge +``` + +### host网络 + +- 容器共享宿主机的网络命名空间 +- 容器直接使用宿主机的IP地址和端口 +- 没有网络隔离,但性能较好 + +```bash +# 使用host网络启动容器 +docker run -d --network host nginx +``` + +### none网络 + +- 容器没有网络接口 +- 完全隔离的网络环境 +- 适用于不需要网络的应用或自定义网络设置 + +```bash +# 使用none网络启动容器 +docker run -d --network none nginx +``` + +## 自定义网络 + +Docker允许用户创建自定义网络,以满足特定的网络需求。 + +### 创建自定义网络 + +```bash +# 创建基本的桥接网络 +docker network create my-network + +# 创建具有特定子网和网关的网络 +docker network create --subnet=172.20.0.0/16 --gateway=172.20.0.1 my-network + +# 创建使用特定驱动的网络 +docker network create --driver overlay my-network + +# 创建具有IP范围的网络 +docker network create --subnet=172.20.0.0/16 --ip-range=172.20.5.0/24 my-network + +# 创建具有自定义选项的网络 +docker network create --opt com.docker.network.bridge.name=my-bridge my-network +``` + +### 网络驱动类型 + +1. **bridge**:默认的网络驱动,适用于单机环境中的容器 + +```bash +docker network create --driver bridge my-bridge +``` + +2. **overlay**:用于Docker Swarm服务,允许跨多个Docker守护进程的容器通信 + +```bash +docker network create --driver overlay my-overlay +``` + +3. **macvlan**:允许为容器分配MAC地址,使其在网络上显示为物理设备 + +```bash +docker network create --driver macvlan \ + --subnet=192.168.0.0/24 \ + --gateway=192.168.0.1 \ + -o parent=eth0 my-macvlan +``` + +4. **ipvlan**:类似于macvlan,但共享MAC地址 + +```bash +docker network create --driver ipvlan \ + --subnet=192.168.0.0/24 \ + --gateway=192.168.0.1 \ + -o parent=eth0 my-ipvlan +``` + +5. **host**:使用宿主机网络 + +6. **none**:禁用容器网络 + +## 容器网络操作 + +### 连接容器到网络 + +```bash +# 创建容器时连接到网络 +docker run -d --name web --network my-network nginx + +# 将现有容器连接到网络 +docker network connect my-network container_id + +# 连接到网络并指定IP地址 +docker network connect --ip 172.20.5.10 my-network container_id + +# 连接到多个网络 +docker network connect my-network2 container_id +``` + +### 断开容器与网络的连接 + +```bash +docker network disconnect my-network container_id + +# 强制断开连接 +docker network disconnect -f my-network container_id +``` + +### 查看容器网络配置 + +```bash +# 查看容器网络设置 +docker inspect --format='{{json .NetworkSettings.Networks}}' container_id + +# 查看容器IP地址 +docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_id + +# 查看容器端口映射 +docker port container_id +``` + +## 端口映射 + +端口映射允许外部访问容器内的服务。 + +### 基本端口映射 + +```bash +# 映射特定端口 +docker run -d -p 8080:80 nginx + +# 映射所有端口 +docker run -d -P nginx + +# 映射到特定IP地址 +docker run -d -p 127.0.0.1:8080:80 nginx + +# 映射到随机端口 +docker run -d -p 127.0.0.1::80 nginx + +# 映射UDP端口 +docker run -d -p 8080:80/udp nginx +``` + +### 查看端口映射 + +```bash +# 查看容器的端口映射 +docker port container_id + +# 查看所有容器的端口映射 +docker ps +``` + +## 容器间通信 + +### 同一网络中的容器通信 + +在同一自定义网络中的容器可以通过容器名称相互访问。 + +```bash +# 创建网络 +docker network create my-network + +# 创建容器并连接到网络 +docker run -d --name web --network my-network nginx +docker run -d --name db --network my-network mysql:5.7 + +# web容器可以通过名称访问db容器 +docker exec -it web ping db +``` + +### 不同网络中的容器通信 + +容器可以连接到多个网络,以便与不同网络中的容器通信。 + +```bash +# 创建两个网络 +docker network create frontend +docker network create backend + +# 创建容器并连接到各自的网络 +docker run -d --name web --network frontend nginx +docker run -d --name db --network backend mysql:5.7 + +# 将web容器也连接到backend网络 +docker network connect backend web + +# 现在web容器可以访问db容器 +docker exec -it web ping db +``` + +## DNS和服务发现 + +Docker内置了DNS服务器,用于容器名称解析。 + +### 容器DNS配置 + +```bash +# 查看容器DNS配置 +docker exec container_id cat /etc/resolv.conf + +# 自定义DNS服务器 +docker run -d --dns 8.8.8.8 --dns 8.8.4.4 nginx + +# 自定义DNS搜索域 +docker run -d --dns-search example.com nginx + +# 添加DNS选项 +docker run -d --dns-opt timeout:3 nginx +``` + +### 服务发现 + +在Docker Swarm模式下,Docker提供了内置的服务发现功能。 + +```bash +# 创建服务 +docker service create --name web --replicas 3 --network my-overlay nginx + +# 其他服务可以通过服务名访问 +docker service create --name client --network my-overlay busybox ping web +``` + +## 网络安全 + +### 网络隔离 + +```bash +# 创建隔离网络 +docker network create --internal isolated-network + +# 在隔离网络中启动容器(无法访问外部网络) +docker run -d --network isolated-network nginx +``` + +### 限制容器网络功能 + +```bash +# 禁止容器修改iptables规则 +docker run -d --cap-drop NET_ADMIN nginx + +# 允许容器配置网络 +docker run -d --cap-add NET_ADMIN nginx +``` + +### 使用网络策略 + +在Kubernetes等编排系统中,可以使用网络策略来控制容器间的通信。 + +## 网络故障排查 + +### 常见问题与解决方案 + +1. **容器无法连接到外部网络** + - 检查宿主机网络配置 + - 确认Docker网络配置正确 + - 检查防火墙规则 + +2. **容器间无法通信** + - 确保容器在同一网络中 + - 检查网络驱动是否支持容器间通信 + - 验证容器网络配置 + +3. **端口映射不工作** + - 确认端口映射配置正确 + - 检查宿主机防火墙是否允许该端口 + - 验证应用是否在容器内正确监听 + +### 调试工具和命令 + +```bash +# 使用网络调试容器 +docker run -it --network container:container_id nicolaka/netshoot + +# 查看网络接口 +docker exec container_id ip addr + +# 测试网络连接 +docker exec container_id ping google.com + +# 查看路由表 +docker exec container_id route + +# 查看iptables规则 +docker exec container_id iptables -L + +# 跟踪网络路径 +docker exec container_id traceroute google.com +``` + +## 高级网络配置 + +### 自定义网络插件 + +Docker支持第三方网络插件,如Calico、Weave和Flannel等。 + +```bash +# 安装网络插件 +docker plugin install + +# 使用网络插件创建网络 +docker network create --driver my-network +``` + +### 配置Docker守护进程网络 + +可以通过修改Docker守护进程配置来自定义默认网络设置。 + +```json +// /etc/docker/daemon.json +{ + "bip": "192.168.1.1/24", + "fixed-cidr": "192.168.1.0/25", + "fixed-cidr-v6": "2001:db8::/64", + "mtu": 1500, + "default-gateway": "192.168.1.254", + "default-gateway-v6": "2001:db8::ff", + "dns": ["8.8.8.8", "8.8.4.4"] +} +``` + +### 使用IPv6 + +```bash +# 启用IPv6支持 +// /etc/docker/daemon.json +{ + "ipv6": true, + "fixed-cidr-v6": "2001:db8:1::/64" +} + +# 创建支持IPv6的网络 +docker network create --ipv6 --subnet=2001:db8:1::/64 ipv6-network + +# 启动支持IPv6的容器 +docker run -d --network ipv6-network nginx +``` + +## Docker网络在生产环境中的最佳实践 + +1. **使用自定义网络** + - 避免使用默认bridge网络 + - 为不同应用创建独立的网络 + +2. **合理规划网络地址空间** + - 避免与现有网络冲突 + - 预留足够的IP地址空间 + +3. **使用overlay网络进行集群通信** + - 在Docker Swarm中使用overlay网络 + - 配置加密的overlay网络提高安全性 + +4. **限制容器网络访问** + - 只映射必要的端口 + - 使用内部网络隔离敏感服务 + +5. **监控网络性能** + - 使用工具监控网络流量和性能 + - 定期检查网络配置 + +6. **使用服务发现** + - 在微服务架构中使用服务发现 + - 考虑使用外部服务发现工具 + +## Docker网络与云平台集成 + +### AWS + +```bash +# 使用AWS VPC驱动 +docker network create --driver=overlay \ + --attachable \ + --opt com.docker.network.driver.mtu=9001 \ + my-aws-network +``` + +### Azure + +```bash +# 使用Azure VNET集成 +docker network create --driver=overlay \ + --subnet=10.0.0.0/16 \ + --opt com.docker.network.driver.mtu=1500 \ + my-azure-network +``` + +### Google Cloud + +```bash +# 使用GCP网络 +docker network create --driver=overlay \ + --subnet=10.0.0.0/16 \ + my-gcp-network +``` + +## 总结 + +Docker网络提供了灵活的容器通信解决方案,从简单的单机桥接网络到复杂的多主机overlay网络。通过合理配置Docker网络,可以构建安全、高效的容器化应用架构。 + +## 下一步学习 + +- [Docker数据卷管理](./docker-volumes.md) +- [Docker Compose详解](./docker-compose.md) +- [Docker Swarm集群](./docker-swarm.md) +- [Kubernetes网络](./kubernetes-network.md) \ No newline at end of file diff --git a/docs/docker/docker-production.md b/docs/docker/docker-production.md new file mode 100644 index 000000000..9a51fe1fb --- /dev/null +++ b/docs/docker/docker-production.md @@ -0,0 +1,1311 @@ +# Docker生产环境部署 + +## 生产环境规划 + +### 基础设施规划 + +在将Docker部署到生产环境之前,需要进行全面的基础设施规划,确保系统的可靠性、可扩展性和安全性。 + +#### 硬件需求 + +根据工作负载类型和规模,合理规划硬件资源: + +| 资源类型 | 最低配置 | 推荐配置 | 说明 | +|---------|---------|---------|------| +| CPU | 2核 | 4-8核 | 容器编排系统需要额外的CPU资源 | +| 内存 | 4GB | 16-32GB | 预留30%给操作系统和Docker守护进程 | +| 磁盘 | 20GB | 100GB+ | 使用SSD提高I/O性能,考虑单独的数据卷 | +| 网络 | 1Gbps | 10Gbps | 集群内部通信和容器镜像传输需要足够带宽 | + +#### 操作系统选择 + +选择适合Docker的操作系统: + +```bash +# 检查Linux内核版本(需要3.10或更高) +uname -r + +# 推荐的操作系统发行版: +# - Ubuntu Server 20.04/22.04 LTS +# - CentOS 8/Stream +# - Debian 11/12 +# - RHEL 8/9 +# - Amazon Linux 2 +``` + +#### 存储规划 + +为不同的Docker组件选择合适的存储驱动和位置: + +```bash +# 检查当前存储驱动 +docker info | grep "Storage Driver" + +# 推荐的存储驱动: +# - overlay2(首选) +# - devicemapper(配置direct-lvm模式) +# - zfs + +# 配置存储驱动和数据目录 +cat > /etc/docker/daemon.json << EOF +{ + "storage-driver": "overlay2", + "data-root": "/mnt/docker-data" +} +EOF + +# 重启Docker服务 +systemctl restart docker +``` + +#### 网络规划 + +规划Docker网络架构: + +```bash +# 创建自定义网络 +docker network create --driver overlay --subnet=10.10.0.0/16 --gateway=10.10.0.1 prod-network + +# 为不同应用创建隔离网络 +docker network create --driver overlay --subnet=10.20.0.0/16 frontend-network +docker network create --driver overlay --subnet=10.30.0.0/16 backend-network +docker network create --driver overlay --subnet=10.40.0.0/16 --internal db-network +``` + +### 容器编排选择 + +根据项目规模和需求选择合适的容器编排系统: + +#### Docker Swarm + +适合中小型部署,与Docker紧密集成,易于设置和管理。 + +```bash +# 初始化Swarm集群 +docker swarm init --advertise-addr + +# 添加工作节点 +# 在管理节点上获取加入命令 +docker swarm join-token worker + +# 在工作节点上执行加入命令 +docker swarm join --token :2377 +``` + +#### Kubernetes + +适合大型复杂部署,功能丰富,生态系统完善,但学习曲线较陡。 + +```bash +# 使用kubeadm安装Kubernetes +# 1. 安装kubeadm、kubelet和kubectl +apt-get update && apt-get install -y apt-transport-https curl +curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +cat < auth/htpasswd + +# 3. 启动私有仓库 +docker run -d \ + --name registry \ + --restart=always \ + -p 5000:5000 \ + -v "$(pwd)"/certs:/certs \ + -v "$(pwd)"/auth:/auth \ + -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \ + -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \ + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ + registry:2 +``` + +#### 使用Harbor作为企业级镜像仓库 + +```bash +# 1. 下载Harbor安装包 +wget https://github.com/goharbor/harbor/releases/download/v2.7.0/harbor-offline-installer-v2.7.0.tgz +tar xzvf harbor-offline-installer-v2.7.0.tgz +cd harbor + +# 2. 配置Harbor +cp harbor.yml.tmpl harbor.yml +# 编辑harbor.yml配置文件 + +# 3. 安装Harbor +./install.sh --with-clair --with-trivy +``` + +### 镜像标签策略 + +制定清晰的镜像标签策略,确保镜像的可追溯性和版本管理。 + +```bash +# 推荐的标签策略: + +# 1. 使用语义化版本 +docker build -t myregistry.example.com/myapp:1.2.3 . + +# 2. 使用Git提交哈希 +docker build -t myregistry.example.com/myapp:$(git rev-parse --short HEAD) . + +# 3. 使用构建时间戳 +docker build -t myregistry.example.com/myapp:$(date +%Y%m%d%H%M%S) . + +# 4. 使用环境标签 +docker build -t myregistry.example.com/myapp:1.2.3-production . +docker build -t myregistry.example.com/myapp:1.2.3-staging . + +# 5. 推送镜像到私有仓库 +docker push myregistry.example.com/myapp:1.2.3 +``` + +### 镜像安全扫描 + +在生产环境中部署前,对镜像进行安全扫描,确保没有已知漏洞。 + +```bash +# 使用Trivy扫描镜像 +trivy image myregistry.example.com/myapp:1.2.3 + +# 在CI/CD流程中集成镜像扫描 +cat > .gitlab-ci.yml << EOF +stages: + - build + - scan + - deploy + +build: + stage: build + script: + - docker build -t myregistry.example.com/myapp:${CI_COMMIT_SHA} . + +scan: + stage: scan + script: + - trivy image --exit-code 1 --severity HIGH,CRITICAL myregistry.example.com/myapp:${CI_COMMIT_SHA} + +deploy: + stage: deploy + script: + - docker push myregistry.example.com/myapp:${CI_COMMIT_SHA} + - docker service update --image myregistry.example.com/myapp:${CI_COMMIT_SHA} my_service + only: + - master +EOF +``` + +## 部署策略 + +### 蓝绿部署 + +蓝绿部署通过同时维护两个生产环境(蓝色和绿色),实现零停机部署。 + +```yaml +# docker-compose.blue.yml +version: '3.8' +services: + app: + image: myregistry.example.com/myapp:1.2.3 + deploy: + replicas: 3 + environment: + - DEPLOYMENT=blue + networks: + - frontend + - backend + +# docker-compose.green.yml +version: '3.8' +services: + app: + image: myregistry.example.com/myapp:1.3.0 + deploy: + replicas: 0 + environment: + - DEPLOYMENT=green + networks: + - frontend + - backend + +# 部署蓝色环境 +docker stack deploy -c docker-compose.blue.yml myapp + +# 部署绿色环境并切换流量 +docker service scale myapp_app-green=3 +# 更新负载均衡器配置指向绿色环境 +# ... +# 确认绿色环境正常后,缩减蓝色环境 +docker service scale myapp_app-blue=0 +``` + +### 金丝雀部署 + +金丝雀部署通过逐步将流量从旧版本转移到新版本,降低风险。 + +```bash +# 使用Docker Swarm进行金丝雀部署 + +# 1. 部署当前版本 +docker service create --name myapp \ + --replicas 5 \ + myregistry.example.com/myapp:1.2.3 + +# 2. 更新服务,使用金丝雀策略 +docker service update \ + --image myregistry.example.com/myapp:1.3.0 \ + --update-parallelism 1 \ + --update-delay 1m \ + myapp + +# 3. 监控更新过程 +docker service ps myapp + +# 4. 如果发现问题,回滚更新 +docker service update --rollback myapp +``` + +### 滚动更新 + +滚动更新是最常用的部署策略,逐步替换旧版本的实例。 + +```yaml +# docker-compose.yml +version: '3.8' +services: + app: + image: myregistry.example.com/myapp:1.3.0 + deploy: + replicas: 5 + update_config: + parallelism: 2 + delay: 30s + order: start-first + failure_action: rollback + monitor: 60s + rollback_config: + parallelism: 2 + delay: 10s + order: stop-first + +# 部署服务 +docker stack deploy -c docker-compose.yml myapp + +# 更新服务镜像 +docker service update --image myregistry.example.com/myapp:1.3.1 myapp_app +``` + +## 高可用性配置 + +### Docker Swarm高可用 + +配置Docker Swarm集群以实现高可用性。 + +```bash +# 1. 初始化第一个管理节点 +docker swarm init --advertise-addr + +# 2. 获取管理节点加入令牌 +docker swarm join-token manager + +# 3. 在其他管理节点上执行加入命令 +# 在Manager2上 +docker swarm join --token :2377 +# 在Manager3上 +docker swarm join --token :2377 + +# 4. 获取工作节点加入令牌 +docker swarm join-token worker + +# 5. 在工作节点上执行加入命令 +docker swarm join --token :2377 + +# 6. 查看集群状态 +docker node ls +``` + +### 服务高可用 + +配置服务以实现高可用性。 + +```bash +# 1. 创建具有多个副本的服务 +docker service create \ + --name myapp \ + --replicas 5 \ + --publish 80:80 \ + myregistry.example.com/myapp:1.3.0 + +# 2. 配置服务约束和偏好,确保副本分布在不同节点 +docker service update \ + --constraint-add "node.labels.zone==east" \ + --constraint-add "node.role==worker" \ + --placement-pref-add "spread=node.labels.rack" \ + myapp + +# 3. 配置健康检查 +docker service update \ + --health-cmd "curl -f http://localhost/health || exit 1" \ + --health-interval 5s \ + --health-retries 3 \ + --health-timeout 2s \ + --health-start-period 10s \ + myapp + +# 4. 配置重启策略 +docker service update \ + --restart-condition any \ + --restart-delay 5s \ + --restart-max-attempts 3 \ + --restart-window 120s \ + myapp +``` + +### 数据持久化 + +确保数据的持久化和高可用性。 + +```bash +# 1. 创建命名卷 +docker volume create --driver local \ + --opt type=nfs \ + --opt o=addr=192.168.1.1,rw \ + --opt device=:/path/to/nfs \ + myapp-data + +# 2. 使用卷创建服务 +docker service create \ + --name db \ + --mount type=volume,source=myapp-data,target=/var/lib/mysql \ + --replicas 1 \ + mysql:5.7 + +# 3. 配置数据备份 +cat > backup.sh << 'EOF' +#!/bin/bash +DATETIME=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR=/backup + +docker run --rm \ + -v myapp-data:/data \ + -v $BACKUP_DIR:/backup \ + alpine \ + tar -czf /backup/myapp-data-$DATETIME.tar.gz -C /data . +EOF +chmod +x backup.sh + +# 4. 设置定时备份 +echo "0 2 * * * /path/to/backup.sh" | crontab - +``` + +## 监控和日志 + +### 监控系统 + +部署监控系统,实时监控Docker容器和主机的状态。 + +#### Prometheus和Grafana + +```yaml +# docker-compose.monitoring.yml +version: '3.8' + +services: + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.enable-lifecycle' + ports: + - "9090:9090" + deploy: + placement: + constraints: + - node.role == manager + + node-exporter: + image: prom/node-exporter:latest + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "9100:9100" + deploy: + mode: global + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + ports: + - "8080:8080" + deploy: + mode: global + + grafana: + image: grafana/grafana:latest + volumes: + - grafana-data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=secure_password + - GF_USERS_ALLOW_SIGN_UP=false + ports: + - "3000:3000" + deploy: + placement: + constraints: + - node.role == manager + +volumes: + prometheus-data: + grafana-data: +``` + +#### 配置Prometheus + +```yaml +# prometheus.yml +global: + scrape_interval: 15s + evaluation_interval: 15s + +alerting: + alertmanagers: + - static_configs: + - targets: + # - alertmanager:9093 + +rule_files: + # - "first_rules.yml" + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'node-exporter' + dns_sd_configs: + - names: + - 'tasks.node-exporter' + type: 'A' + port: 9100 + + - job_name: 'cadvisor' + dns_sd_configs: + - names: + - 'tasks.cadvisor' + type: 'A' + port: 8080 + + - job_name: 'docker' + static_configs: + - targets: ['host.docker.internal:9323'] +``` + +### 日志管理 + +配置集中式日志管理系统,收集和分析容器日志。 + +#### ELK Stack + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - elasticsearch-data:/usr/share/elasticsearch/data + ports: + - "9200:9200" + deploy: + resources: + limits: + memory: 1g + + logstash: + image: docker.elastic.co/logstash/logstash:7.17.0 + volumes: + - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf + ports: + - "5000:5000" + - "5000:5000/udp" + - "9600:9600" + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:7.17.0 + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + ports: + - "5601:5601" + depends_on: + - elasticsearch + +volumes: + elasticsearch-data: +``` + +#### 配置Logstash + +``` +# logstash.conf +input { + gelf { + port => 5000 + } +} + +filter { + if [docker][image] =~ /^myregistry\.example\.com\/myapp/ { + mutate { + add_field => { "application" => "myapp" } + } + } +} + +output { + elasticsearch { + hosts => ["elasticsearch:9200"] + index => "logstash-%{+YYYY.MM.dd}" + } +} +``` + +#### 配置Docker日志驱动 + +```bash +# 在/etc/docker/daemon.json中配置 +cat > /etc/docker/daemon.json << EOF +{ + "log-driver": "gelf", + "log-opts": { + "gelf-address": "udp://localhost:5000", + "tag": "{{.Name}}/{{.ID}}" + } +} +EOF + +# 重启Docker服务 +systemctl restart docker + +# 或者为特定容器配置日志驱动 +docker service create \ + --name myapp \ + --log-driver gelf \ + --log-opt gelf-address=udp://logstash:5000 \ + --log-opt tag="myapp/{{.Name}}/{{.ID}}" \ + myregistry.example.com/myapp:1.3.0 +``` + +## 性能优化 + +### 容器性能优化 + +优化容器配置以提高性能。 + +```bash +# 1. 限制容器资源 +docker service create \ + --name myapp \ + --limit-cpu 0.5 \ + --limit-memory 512M \ + --reserve-cpu 0.1 \ + --reserve-memory 128M \ + myregistry.example.com/myapp:1.3.0 + +# 2. 优化容器网络 +docker service create \ + --name myapp \ + --network-add name=fast-network,alias=myapp \ + --dns 8.8.8.8 \ + --dns-option ndots:2 \ + --dns-search example.com \ + myregistry.example.com/myapp:1.3.0 + +# 3. 优化存储性能 +docker service create \ + --name myapp \ + --mount type=volume,source=fast-data,target=/data,volume-driver=local,volume-opt=type=tmpfs,volume-opt=device=tmpfs,volume-opt=o=size=100m \ + myregistry.example.com/myapp:1.3.0 +``` + +### Docker守护进程优化 + +优化Docker守护进程配置以提高性能。 + +```bash +# 在/etc/docker/daemon.json中配置 +cat > /etc/docker/daemon.json << EOF +{ + "storage-driver": "overlay2", + "storage-opts": ["overlay2.override_kernel_check=true"], + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + }, + "default-ulimits": { + "nofile": { + "Name": "nofile", + "Hard": 64000, + "Soft": 64000 + } + }, + "live-restore": true, + "max-concurrent-downloads": 10, + "max-concurrent-uploads": 10, + "registry-mirrors": ["https://mirror.example.com"], + "insecure-registries": ["registry.internal:5000"], + "experimental": false, + "metrics-addr": "0.0.0.0:9323", + "dns": ["8.8.8.8", "8.8.4.4"] +} +EOF + +# 重启Docker服务 +systemctl restart docker +``` + +### 主机系统优化 + +优化主机系统配置以提高Docker性能。 + +```bash +# 1. 调整内核参数 +cat > /etc/sysctl.d/99-docker.conf << EOF +# 最大文件句柄数 +fs.file-max = 1000000 + +# 允许更多的PIDs +kernel.pid_max = 4194303 + +# 增加网络性能相关参数 +net.core.somaxconn = 65535 +net.ipv4.tcp_max_syn_backlog = 65536 +net.core.netdev_max_backlog = 65536 +net.ipv4.ip_local_port_range = 1024 65535 +net.ipv4.tcp_fin_timeout = 15 +net.ipv4.tcp_keepalive_time = 300 +net.ipv4.tcp_keepalive_intvl = 30 +net.ipv4.tcp_keepalive_probes = 5 + +# 启用IP转发(容器网络需要) +net.ipv4.ip_forward = 1 + +# 增加inotify限制(容器文件系统监控需要) +fs.inotify.max_user_watches = 1048576 +fs.inotify.max_user_instances = 8192 +EOF + +# 应用内核参数 +sysctl --system + +# 2. 调整用户限制 +cat > /etc/security/limits.d/99-docker.conf << EOF +* soft nofile 1048576 +* hard nofile 1048576 +root soft nofile 1048576 +root hard nofile 1048576 +* soft nproc 1048576 +* hard nproc 1048576 +root soft nproc 1048576 +root hard nproc 1048576 +EOF + +# 3. 优化磁盘I/O调度器(对SSD) +echo "mq-deadline" > /sys/block/sda/queue/scheduler + +# 4. 禁用不必要的服务 +systemctl disable snapd +systemctl disable lxcfs +systemctl disable accounts-daemon +``` + +## 备份和恢复 + +### 数据备份策略 + +实施全面的备份策略,确保数据安全。 + +```bash +# 1. 创建备份脚本 +cat > /usr/local/bin/docker-backup.sh << 'EOF' +#!/bin/bash + +BACKUP_DIR="/mnt/backups/$(date +%Y%m%d)" +DOCKER_DATA_DIR="/var/lib/docker" +VOLUME_BACKUP_DIR="${BACKUP_DIR}/volumes" + +# 创建备份目录 +mkdir -p "${VOLUME_BACKUP_DIR}" + +# 备份Docker配置 +mkdir -p "${BACKUP_DIR}/config" +cp -r /etc/docker "${BACKUP_DIR}/config/" + +# 备份Swarm配置(如果使用Swarm) +if docker info | grep -q "Swarm: active"; then + mkdir -p "${BACKUP_DIR}/swarm" + sudo systemctl stop docker + cp -r "${DOCKER_DATA_DIR}/swarm" "${BACKUP_DIR}/swarm/" + sudo systemctl start docker +fi + +# 备份命名卷 +for volume in $(docker volume ls -q); do + echo "Backing up volume: ${volume}" + mkdir -p "${VOLUME_BACKUP_DIR}/${volume}" + docker run --rm \ + -v "${volume}":/source \ + -v "${VOLUME_BACKUP_DIR}/${volume}":/backup \ + alpine \ + tar -czf "/backup/${volume}.tar.gz" -C /source . +done + +# 备份运行中的容器配置 +mkdir -p "${BACKUP_DIR}/containers" +for container in $(docker ps -q); do + container_name=$(docker inspect --format="{{.Name}}" "${container}" | sed 's/^\/*//') + echo "Backing up container config: ${container_name}" + docker inspect "${container}" > "${BACKUP_DIR}/containers/${container_name}.json" +done + +# 备份自定义网络配置 +mkdir -p "${BACKUP_DIR}/networks" +for network in $(docker network ls --filter "driver=overlay" --format "{{.Name}}" | grep -v "ingress"); do + echo "Backing up network config: ${network}" + docker network inspect "${network}" > "${BACKUP_DIR}/networks/${network}.json" +done + +# 压缩备份 +cd "$(dirname "${BACKUP_DIR}")" +tar -czf "docker-backup-$(date +%Y%m%d).tar.gz" "$(basename "${BACKUP_DIR}")" +rm -rf "${BACKUP_DIR}" + +echo "Backup completed: docker-backup-$(date +%Y%m%d).tar.gz" +EOF + +chmod +x /usr/local/bin/docker-backup.sh + +# 2. 设置定时备份 +echo "0 1 * * * /usr/local/bin/docker-backup.sh" | crontab - +``` + +### 系统恢复流程 + +制定系统恢复流程,确保在灾难发生时能够快速恢复。 + +```bash +# 1. 创建恢复脚本 +cat > /usr/local/bin/docker-restore.sh << 'EOF' +#!/bin/bash + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +BACKUP_FILE="$1" +RESTORE_DIR="/tmp/docker-restore" + +# 解压备份 +rm -rf "${RESTORE_DIR}" +mkdir -p "${RESTORE_DIR}" +tar -xzf "${BACKUP_FILE}" -C "${RESTORE_DIR}" +BACKUP_DIR=$(find "${RESTORE_DIR}" -type d -name "[0-9]*" | head -1) + +# 恢复Docker配置 +if [ -d "${BACKUP_DIR}/config/docker" ]; then + echo "Restoring Docker configuration..." + cp -r "${BACKUP_DIR}/config/docker"/* /etc/docker/ +fi + +# 停止Docker服务 +systemctl stop docker + +# 恢复Swarm配置(如果存在) +if [ -d "${BACKUP_DIR}/swarm" ]; then + echo "Restoring Swarm configuration..." + rm -rf /var/lib/docker/swarm + mkdir -p /var/lib/docker/swarm + cp -r "${BACKUP_DIR}/swarm/swarm"/* /var/lib/docker/swarm/ +fi + +# 启动Docker服务 +systemctl start docker + +# 恢复命名卷 +if [ -d "${BACKUP_DIR}/volumes" ]; then + echo "Restoring volumes..." + for volume_tar in "${BACKUP_DIR}/volumes"/*/*.tar.gz; do + volume_name=$(basename "$(dirname "${volume_tar}")") + echo "Restoring volume: ${volume_name}" + + # 创建卷(如果不存在) + if ! docker volume inspect "${volume_name}" &>/dev/null; then + docker volume create "${volume_name}" + fi + + # 恢复卷数据 + docker run --rm \ + -v "${volume_name}":/target \ + -v "$(dirname "${volume_tar}")":/backup \ + alpine \ + sh -c "rm -rf /target/* && tar -xzf /backup/$(basename "${volume_tar}") -C /target" + done +fi + +# 恢复网络配置 +if [ -d "${BACKUP_DIR}/networks" ]; then + echo "Restoring networks..." + for network_file in "${BACKUP_DIR}/networks"/*.json; do + network_name=$(basename "${network_file}" .json) + echo "Restoring network: ${network_name}" + + # 检查网络是否存在 + if ! docker network inspect "${network_name}" &>/dev/null; then + # 从备份文件提取网络配置 + driver=$(jq -r '.[0].Driver' "${network_file}") + subnet=$(jq -r '.[0].IPAM.Config[0].Subnet' "${network_file}") + gateway=$(jq -r '.[0].IPAM.Config[0].Gateway' "${network_file}") + + # 创建网络 + docker network create \ + --driver "${driver}" \ + --subnet "${subnet}" \ + --gateway "${gateway}" \ + "${network_name}" + fi + done +fi + +# 恢复容器(可选,通常使用编排工具重新部署) +# ... + +echo "Restore completed." +EOF + +chmod +x /usr/local/bin/docker-restore.sh +``` + +### 灾难恢复演练 + +定期进行灾难恢复演练,确保恢复流程有效。 + +```bash +# 1. 创建演练脚本 +cat > /usr/local/bin/dr-drill.sh << 'EOF' +#!/bin/bash + +# 设置测试环境 +TEST_DIR="/tmp/dr-test" +mkdir -p "${TEST_DIR}" + +# 创建测试卷和数据 +docker volume create test-volume +docker run --rm -v test-volume:/data alpine sh -c "echo 'test data' > /data/test.txt" + +# 备份测试环境 +echo "Backing up test environment..." +/usr/local/bin/docker-backup.sh +BACKUP_FILE=$(ls -t docker-backup-*.tar.gz | head -1) + +# 删除测试卷 +docker volume rm test-volume + +# 恢复测试环境 +echo "Restoring test environment..." +/usr/local/bin/docker-restore.sh "${BACKUP_FILE}" + +# 验证恢复结果 +echo "Verifying restore..." +RESTORE_RESULT=$(docker run --rm -v test-volume:/data alpine cat /data/test.txt) + +if [ "${RESTORE_RESULT}" = "test data" ]; then + echo "Disaster recovery drill: SUCCESS" +else + echo "Disaster recovery drill: FAILED" +fi + +# 清理 +docker volume rm test-volume +rm -rf "${TEST_DIR}" +EOF + +chmod +x /usr/local/bin/dr-drill.sh + +# 2. 设置定期演练 +echo "0 2 1 * * /usr/local/bin/dr-drill.sh" | crontab - +``` + +## 安全最佳实践 + +### 容器安全配置 + +实施容器安全最佳实践。 + +```bash +# 1. 以非root用户运行容器 +docker service create \ + --name myapp \ + --user 1000:1000 \ + myregistry.example.com/myapp:1.3.0 + +# 2. 使用只读文件系统 +docker service create \ + --name myapp \ + --read-only \ + --tmpfs /tmp:rw,noexec,nosuid \ + myregistry.example.com/myapp:1.3.0 + +# 3. 限制容器功能 +docker service create \ + --name myapp \ + --cap-drop ALL \ + --cap-add NET_BIND_SERVICE \ + --security-opt no-new-privileges \ + myregistry.example.com/myapp:1.3.0 + +# 4. 使用seccomp配置文件 +docker service create \ + --name myapp \ + --security-opt seccomp=/etc/docker/seccomp-profile.json \ + myregistry.example.com/myapp:1.3.0 +``` + +### 网络安全 + +实施网络安全最佳实践。 + +```bash +# 1. 创建隔离网络 +docker network create --driver overlay --internal backend-network + +# 2. 使用加密的覆盖网络 +docker network create --driver overlay --opt encrypted secure-network + +# 3. 配置网络策略 +# 在Kubernetes中使用NetworkPolicy +cat > network-policy.yaml << EOF +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: db-policy +spec: + podSelector: + matchLabels: + app: db + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: api + ports: + - protocol: TCP + port: 3306 +EOF + +# 4. 使用TLS加密通信 +docker service create \ + --name web \ + --secret source=ssl-cert,target=/etc/nginx/ssl/cert.pem \ + --secret source=ssl-key,target=/etc/nginx/ssl/key.pem \ + --config source=nginx-ssl,target=/etc/nginx/conf.d/default.conf \ + nginx:latest +``` + +### 访问控制和认证 + +实施访问控制和认证最佳实践。 + +```bash +# 1. 使用Docker Content Trust签名和验证镜像 +export DOCKER_CONTENT_TRUST=1 +docker pull myregistry.example.com/myapp:1.3.0 + +# 2. 使用RBAC控制API访问(在Kubernetes中) +cat > rbac.yaml << EOF +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: default + name: pod-reader +rules: +- apiGroups: [""] # "" 表示核心API组 + resources: ["pods"] + verbs: ["get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: read-pods + namespace: default +subjects: +- kind: User + name: jane + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: pod-reader + apiGroup: rbac.authorization.k8s.io +EOF + +# 3. 使用Docker Secrets管理敏感信息 +docker secret create db-password /path/to/password.txt +docker service create \ + --name db \ + --secret db-password \ + --env MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password \ + mysql:5.7 +``` + +## 自动化和CI/CD + +### CI/CD流水线 + +实施CI/CD流水线,自动化构建、测试和部署过程。 + +```yaml +# .gitlab-ci.yml +stages: + - build + - test + - scan + - deploy + - verify + +variables: + DOCKER_REGISTRY: myregistry.example.com + IMAGE_NAME: myapp + IMAGE_TAG: $CI_COMMIT_SHA + +build: + stage: build + script: + - docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG . + - docker push $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG + +test: + stage: test + script: + - docker pull $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG + - docker run --rm $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG npm test + +scan: + stage: scan + script: + - trivy image --exit-code 1 --severity HIGH,CRITICAL $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG + +deploy_staging: + stage: deploy + script: + - docker stack deploy -c docker-compose.staging.yml --with-registry-auth myapp-staging + - docker service update --image $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG myapp-staging_app + environment: + name: staging + only: + - develop + +deploy_production: + stage: deploy + script: + - docker stack deploy -c docker-compose.prod.yml --with-registry-auth myapp-prod + - docker service update --image $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG myapp-prod_app + environment: + name: production + when: manual + only: + - master + +verify: + stage: verify + script: + - curl -f https://staging.example.com/health || exit 1 + environment: + name: staging + only: + - develop +``` + +### 自动化测试 + +实施自动化测试,确保应用质量。 + +```bash +# 1. 单元测试 +docker run --rm \ + -v $(pwd):/app \ + -w /app \ + node:14 \ + npm run test:unit + +# 2. 集成测试 +docker-compose -f docker-compose.test.yml up --abort-on-container-exit + +# 3. 端到端测试 +docker run --rm \ + --network host \ + -v $(pwd):/app \ + -w /app \ + cypress/included:8.3.0 \ + cypress run +``` + +### 基础设施即代码 + +使用基础设施即代码工具管理Docker环境。 + +```yaml +# docker-compose.yml with environment variables +version: '3.8' + +services: + app: + image: ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} + deploy: + replicas: ${REPLICAS:-3} + update_config: + parallelism: ${UPDATE_PARALLELISM:-1} + delay: ${UPDATE_DELAY:-30s} + order: start-first + failure_action: rollback + resources: + limits: + cpus: ${CPU_LIMIT:-0.5} + memory: ${MEMORY_LIMIT:-512M} + environment: + - NODE_ENV=${NODE_ENV:-production} + - DB_HOST=${DB_HOST:-db} + networks: + - frontend + - backend + + db: + image: mysql:5.7 + volumes: + - db-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password + - MYSQL_DATABASE=${DB_NAME:-myapp} + secrets: + - db-password + networks: + - backend + +networks: + frontend: + driver: overlay + backend: + driver: overlay + internal: true + +volumes: + db-data: + +secrets: + db-password: + external: true +``` + +## 总结 + +Docker生产环境部署是一个复杂的过程,涉及多个方面的考虑和优化。通过本文介绍的最佳实践,您可以构建一个可靠、高效、安全的Docker生产环境。 + +关键要点包括: + +1. **基础设施规划**:根据工作负载需求规划硬件、操作系统、存储和网络。 +2. **容器编排**:选择合适的容器编排系统(Docker Swarm、Kubernetes或Docker Compose)。 +3. **镜像管理**:使用私有镜像仓库、制定标签策略、进行安全扫描。 +4. **部署策略**:实施蓝绿部署、金丝雀部署或滚动更新。 +5. **高可用性**:配置集群高可用、服务高可用和数据持久化。 +6. **监控和日志**:部署监控系统和集中式日志管理。 +7. **性能优化**:优化容器、Docker守护进程和主机系统。 +8. **备份和恢复**:实施备份策略、制定恢复流程、进行灾难恢复演练。 +9. **安全最佳实践**:实施容器安全配置、网络安全和访问控制。 +10. **自动化和CI/CD**:实施CI/CD流水线、自动化测试和基础设施即代码。 + +通过遵循这些最佳实践,您可以构建一个稳定、安全、高效的Docker生产环境,为您的应用提供可靠的运行平台。 + +## 下一步学习 + +- [Docker安全最佳实践](./docker-security.md) +- [Docker Swarm集群](./docker-swarm.md) +- [Docker Compose详解](./docker-compose.md) +- [Docker数据卷管理](./docker-volumes.md) \ No newline at end of file diff --git a/docs/docker/docker-security.md b/docs/docker/docker-security.md new file mode 100644 index 000000000..e0821bcc0 --- /dev/null +++ b/docs/docker/docker-security.md @@ -0,0 +1,855 @@ +# Docker安全最佳实践 + +## Docker安全概述 + +Docker容器技术为应用部署带来了便利,但同时也引入了新的安全挑战。Docker安全涉及多个层面,包括主机安全、镜像安全、容器运行时安全、网络安全和数据安全等。本文将详细介绍Docker环境中的安全最佳实践,帮助您构建更安全的容器化环境。 + +## Docker主机安全 + +### 操作系统加固 + +```bash +# 保持系统更新 +sudo apt update && sudo apt upgrade -y # Debian/Ubuntu +sudo yum update -y # CentOS/RHEL + +# 禁用不必要的服务 +sudo systemctl disable +sudo systemctl stop + +# 配置防火墙,只开放必要端口 +sudo ufw allow ssh # Ubuntu +sudo ufw allow 2376/tcp # Docker TLS +sudo ufw allow 2377/tcp # Swarm mode +sudo ufw allow 7946/tcp # Swarm mode node communication +sudo ufw allow 7946/udp # Swarm mode node communication +sudo ufw allow 4789/udp # Swarm overlay network +sudo ufw enable + +# 或使用firewalld (CentOS/RHEL) +sudo firewall-cmd --permanent --add-port=22/tcp +sudo firewall-cmd --permanent --add-port=2376/tcp +sudo firewall-cmd --permanent --add-port=2377/tcp +sudo firewall-cmd --permanent --add-port=7946/tcp +sudo firewall-cmd --permanent --add-port=7946/udp +sudo firewall-cmd --permanent --add-port=4789/udp +sudo firewall-cmd --reload +``` + +### Docker守护进程安全 + +1. **使用TLS加密Docker API通信** + +创建CA和证书: +```bash +# 创建CA私钥和证书 +openssl genrsa -aes256 -out ca-key.pem 4096 +openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem + +# 创建服务器密钥和CSR +openssl genrsa -out server-key.pem 4096 +openssl req -subj "/CN=your-server-name" -sha256 -new -key server-key.pem -out server.csr + +# 配置服务器证书的扩展属性 +echo subjectAltName = DNS:your-server-name,IP:10.10.10.20,IP:127.0.0.1 >> extfile.cnf +echo extendedKeyUsage = serverAuth >> extfile.cnf + +# 生成服务器证书 +openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out server-cert.pem -extfile extfile.cnf + +# 创建客户端密钥和CSR +openssl genrsa -out key.pem 4096 +openssl req -subj '/CN=client' -new -key key.pem -out client.csr + +# 配置客户端证书的扩展属性 +echo extendedKeyUsage = clientAuth > extfile-client.cnf + +# 生成客户端证书 +openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out cert.pem -extfile extfile-client.cnf + +# 删除不需要的文件 +rm -v client.csr server.csr extfile.cnf extfile-client.cnf + +# 设置权限 +chmod -v 0400 ca-key.pem key.pem server-key.pem +chmod -v 0444 ca.pem server-cert.pem cert.pem +``` + +2. **配置Docker守护进程** + +编辑Docker配置文件 `/etc/docker/daemon.json`: +```json +{ + "tls": true, + "tlsverify": true, + "tlscacert": "/path/to/ca.pem", + "tlscert": "/path/to/server-cert.pem", + "tlskey": "/path/to/server-key.pem", + "hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"], + "log-level": "info", + "icc": false, + "no-new-privileges": true, + "userns-remap": "default", + "live-restore": true, + "userland-proxy": false, + "default-ulimits": { + "nofile": { + "Name": "nofile", + "Hard": 64000, + "Soft": 64000 + } + } +} +``` + +3. **重启Docker服务** + +```bash +sudo systemctl restart docker +``` + +4. **使用客户端证书连接Docker** + +```bash +docker --tlsverify \ + --tlscacert=ca.pem \ + --tlscert=cert.pem \ + --tlskey=key.pem \ + -H=your-server-name:2376 version +``` + +### 用户权限管理 + +1. **创建Docker用户组** + +```bash +# 创建docker组(通常安装Docker时已创建) +sudo groupadd docker + +# 将用户添加到docker组 +sudo usermod -aG docker $USER + +# 应用更改(重新登录或运行以下命令) +newgrp docker +``` + +2. **限制Docker用户组权限** + +```bash +# 创建专用的Docker管理员用户 +sudo useradd -m dockeradmin +sudo usermod -aG docker dockeradmin + +# 使用sudo授予特定命令权限 +sudo visudo +# 添加以下行 +dockeradmin ALL=(ALL) /usr/bin/docker +``` + +## 容器镜像安全 + +### 使用官方和验证的镜像 + +```bash +# 拉取官方镜像 +docker pull ubuntu:20.04 + +# 拉取经过验证的镜像 +docker pull nginx:latest + +# 检查镜像签名(需要Docker Content Trust) +DOCKER_CONTENT_TRUST=1 docker pull nginx:latest +``` + +### 启用Docker Content Trust + +```bash +# 启用Docker Content Trust +export DOCKER_CONTENT_TRUST=1 + +# 生成签名密钥 +docker trust key generate my-key + +# 将密钥添加到仓库 +docker trust signer add --key my-key.pub my-key my-registry.example.com/my-image + +# 签名并推送镜像 +docker tag my-image:latest my-registry.example.com/my-image:latest +docker push my-registry.example.com/my-image:latest +``` + +### 镜像扫描和漏洞管理 + +1. **使用Docker Scout** + +```bash +# 安装Docker Scout CLI +docker extension install docker/scout-extension + +# 扫描本地镜像 +docker scout cves nginx:latest + +# 查看详细漏洞报告 +docker scout recommendations nginx:latest +``` + +2. **使用第三方扫描工具** + +```bash +# 使用Trivy扫描镜像 +trivy image nginx:latest + +# 使用Clair扫描镜像 +clairctl analyze -l nginx:latest + +# 使用Anchore扫描镜像 +anchore-cli image add docker.io/library/nginx:latest +anchore-cli image wait docker.io/library/nginx:latest +anchore-cli image vuln docker.io/library/nginx:latest os +``` + +### 构建安全的Docker镜像 + +1. **最小化基础镜像** + +```dockerfile +# 使用Alpine作为基础镜像 +FROM alpine:3.14 + +# 或使用distroless镜像 +FROM gcr.io/distroless/static-debian10 +``` + +2. **多阶段构建** + +```dockerfile +# 构建阶段 +FROM node:14 AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# 运行阶段 +FROM nginx:alpine +COPY --from=builder /app/build /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +3. **不要在镜像中包含敏感信息** + +```dockerfile +# 错误示例 - 不要这样做 +FROM ubuntu:20.04 +ENV DB_PASSWORD=supersecret + +# 正确示例 - 使用构建参数,但不保存在最终镜像中 +FROM ubuntu:20.04 +ARG DB_PASSWORD +RUN echo $DB_PASSWORD > /tmp/setup && ./setup.sh && rm /tmp/setup + +# 更好的方式 - 使用Docker Secrets或环境变量注入 +``` + +4. **定期更新基础镜像** + +```bash +# 拉取最新的基础镜像 +docker pull ubuntu:20.04 + +# 重新构建应用镜像 +docker build -t my-app:latest . +``` + +## 容器运行时安全 + +### 以非root用户运行容器 + +1. **在Dockerfile中设置用户** + +```dockerfile +FROM ubuntu:20.04 + +# 创建非root用户 +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# 设置应用目录权限 +WORKDIR /app +COPY --chown=appuser:appuser . . + +# 切换到非root用户 +USER appuser + +CMD ["./app"] +``` + +2. **运行时指定用户** + +```bash +# 使用--user标志 +docker run --user 1000:1000 nginx + +# 或使用用户名 +docker run --user appuser nginx +``` + +### 限制容器资源 + +```bash +# 限制CPU和内存 +docker run -d --name limited-container \ + --cpus=0.5 \ + --memory=512m \ + --memory-swap=512m \ + nginx + +# 限制IO +docker run -d --name io-limited \ + --device-write-bps /dev/sda:1mb \ + --device-read-bps /dev/sda:1mb \ + nginx + +# 限制进程数 +docker run -d --name process-limited \ + --pids-limit=50 \ + nginx +``` + +### 使用安全计算(seccomp)配置文件 + +1. **使用默认seccomp配置文件** + +```bash +# Docker默认启用seccomp +docker run --rm -it ubuntu:20.04 bash +``` + +2. **使用自定义seccomp配置文件** + +```bash +# 下载Docker默认seccomp配置作为起点 +wget https://raw.githubusercontent.com/docker/engine/master/profiles/seccomp/default.json + +# 编辑配置文件后使用 +docker run --security-opt seccomp=/path/to/custom-seccomp.json nginx +``` + +### 使用AppArmor或SELinux + +1. **AppArmor (Ubuntu/Debian)** + +```bash +# 检查AppArmor状态 +sudo aa-status + +# 创建自定义AppArmor配置 +sudo nano /etc/apparmor.d/docker-nginx + +# 加载配置 +sudo apparmor_parser -r -W /etc/apparmor.d/docker-nginx + +# 运行容器时使用 +docker run --security-opt apparmor=docker-nginx nginx +``` + +2. **SELinux (CentOS/RHEL)** + +```bash +# 检查SELinux状态 +getenforce + +# 创建自定义SELinux策略 +sudo semodule -i my-docker.pp + +# 运行容器时使用 +docker run --security-opt label=type:my_container_t nginx +``` + +### 使用只读文件系统 + +```bash +# 将根文件系统设为只读 +docker run --read-only nginx + +# 为特定目录提供写入权限 +docker run --read-only \ + --tmpfs /tmp \ + --tmpfs /var/cache \ + --tmpfs /var/log \ + nginx +``` + +### 限制容器功能 + +```bash +# 删除所有功能并只添加需要的功能 +docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx + +# 常用的安全组合 +docker run \ + --cap-drop=ALL \ + --cap-add=NET_BIND_SERVICE \ + --cap-add=SYS_NICE \ + --security-opt=no-new-privileges \ + nginx +``` + +### 使用Docker Secrets管理敏感数据 + +```bash +# 创建secret +echo "mydbpassword" | docker secret create db_password - + +# 在服务中使用secret +docker service create \ + --name db \ + --secret db_password \ + --env MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password \ + mysql:5.7 +``` + +## 网络安全 + +### 使用用户定义的网络隔离容器 + +```bash +# 创建自定义网络 +docker network create --driver bridge app-network + +# 将容器连接到自定义网络 +docker run -d --name web --network app-network nginx +docker run -d --name db --network app-network mysql:5.7 + +# 创建内部网络(不连接到外部) +docker network create --internal internal-only +docker run -d --name db --network internal-only mysql:5.7 +``` + +### 限制容器间通信 + +```bash +# 禁用默认桥接网络上的容器间通信 +# 在/etc/docker/daemon.json中设置 +{ + "icc": false +} + +# 重启Docker +sudo systemctl restart docker +``` + +### 使用TLS保护容器通信 + +```bash +# 在Docker Swarm中创建覆盖网络时启用加密 +docker network create \ + --driver overlay \ + --opt encrypted \ + secure-network +``` + +### 安全地发布端口 + +```bash +# 仅绑定到特定接口 +docker run -d -p 127.0.0.1:80:80 nginx + +# 使用动态端口映射 +docker run -d -p 127.0.0.1::80 nginx +``` + +## 数据安全 + +### 安全地管理卷和挂载 + +```bash +# 使用命名卷 +docker volume create secure-data +docker run -d -v secure-data:/data nginx + +# 使用只读挂载 +docker run -d -v /host/config:/container/config:ro nginx + +# 使用临时文件系统 +docker run -d --tmpfs /tmp:rw,noexec,nosuid,size=1g nginx +``` + +### 加密存储数据 + +```bash +# 在主机上创建加密卷 +sudo cryptsetup luksFormat /dev/sdX +sudo cryptsetup open /dev/sdX encrypted-data +sudo mkfs.ext4 /dev/mapper/encrypted-data +sudo mount /dev/mapper/encrypted-data /mnt/encrypted-data + +# 将加密卷挂载到容器 +docker run -d -v /mnt/encrypted-data:/data nginx +``` + +### 安全备份和恢复 + +```bash +# 备份Docker卷 +docker run --rm -v secure-data:/data -v $(pwd):/backup alpine \ + tar -czf /backup/secure-data-backup.tar.gz -C /data . + +# 恢复Docker卷 +docker run --rm -v secure-data:/data -v $(pwd):/backup alpine \ + sh -c "cd /data && tar -xzf /backup/secure-data-backup.tar.gz" +``` + +## 监控和审计 + +### 启用Docker审计 + +1. **使用auditd监控Docker活动** + +```bash +# 安装auditd +sudo apt install auditd # Debian/Ubuntu +sudo yum install audit # CentOS/RHEL + +# 配置Docker审计规则 +sudo nano /etc/audit/rules.d/docker.rules + +# 添加以下规则 +-w /usr/bin/docker -k docker +-w /var/lib/docker -k docker +-w /etc/docker -k docker +-w /lib/systemd/system/docker.service -k docker +-w /lib/systemd/system/docker.socket -k docker +-w /etc/default/docker -k docker +-w /etc/docker/daemon.json -k docker +-w /usr/lib/systemd/system/docker.service -k docker +-w /usr/lib/systemd/system/docker.socket -k docker + +# 重新加载审计规则 +sudo auditctl -R /etc/audit/rules.d/docker.rules + +# 查看Docker相关审计日志 +sudo ausearch -k docker +``` + +### 使用容器日志记录 + +```bash +# 配置Docker日志驱动 +# 在/etc/docker/daemon.json中设置 +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } +} + +# 为特定容器配置日志驱动 +docker run -d \ + --log-driver=syslog \ + --log-opt syslog-address=udp://syslog-server:514 \ + nginx + +# 查看容器日志 +docker logs container_id +``` + +### 使用监控工具 + +1. **使用cAdvisor监控容器** + +```bash +docker run \ + --volume=/:/rootfs:ro \ + --volume=/var/run:/var/run:ro \ + --volume=/sys:/sys:ro \ + --volume=/var/lib/docker/:/var/lib/docker:ro \ + --volume=/dev/disk/:/dev/disk:ro \ + --publish=8080:8080 \ + --detach=true \ + --name=cadvisor \ + --privileged \ + --device=/dev/kmsg \ + gcr.io/cadvisor/cadvisor:v0.39.3 +``` + +2. **使用Prometheus和Grafana** + +```yaml +version: '3' + +services: + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + node-exporter: + image: prom/node-exporter + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "9100:9100" + + cadvisor: + image: gcr.io/cadvisor/cadvisor + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + ports: + - "8080:8080" + + grafana: + image: grafana/grafana + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + +volumes: + grafana-data: +``` + +## 安全更新和补丁管理 + +### 自动化镜像更新 + +1. **使用Watchtower自动更新容器** + +```bash +# 运行Watchtower以自动更新所有容器 +docker run -d \ + --name watchtower \ + --restart always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + containrrr/watchtower --interval 86400 + +# 仅更新特定容器 +docker run -d \ + --name watchtower \ + --restart always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + containrrr/watchtower --interval 86400 container1 container2 +``` + +2. **使用CI/CD管道自动构建和部署** + +```yaml +# .gitlab-ci.yml示例 +stages: + - build + - test + - scan + - deploy + +build: + stage: build + script: + - docker build -t my-app:$CI_COMMIT_SHA . + - docker push my-app:$CI_COMMIT_SHA + +test: + stage: test + script: + - docker run my-app:$CI_COMMIT_SHA npm test + +scan: + stage: scan + script: + - trivy image my-app:$CI_COMMIT_SHA + +deploy: + stage: deploy + script: + - docker service update --image my-app:$CI_COMMIT_SHA my_service +``` + +### 定期安全审计 + +```bash +# 使用Docker Bench Security进行安全审计 +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /usr/bin/docker:/usr/bin/docker \ + -v /etc/docker:/etc/docker \ + -v /etc:/host/etc \ + -v /lib/systemd:/host/lib/systemd \ + -v /usr/lib/systemd:/host/usr/lib/systemd \ + -v /var/lib:/host/var/lib \ + docker/docker-bench-security +``` + +## 安全合规性 + +### 符合CIS Docker基准 + +```bash +# 使用Docker Bench Security检查CIS合规性 +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /usr/bin/docker:/usr/bin/docker \ + -v /etc/docker:/etc/docker \ + -v /etc:/host/etc \ + -v /lib/systemd:/host/lib/systemd \ + -v /usr/lib/systemd:/host/usr/lib/systemd \ + -v /var/lib:/host/var/lib \ + docker/docker-bench-security +``` + +### 符合GDPR/HIPAA等法规 + +1. **数据加密** + +```bash +# 使用加密卷 +# 参见前面的"加密存储数据"部分 +``` + +2. **数据隔离** + +```bash +# 使用专用网络 +docker network create --internal sensitive-data-network + +# 使用专用卷 +docker volume create --label data=sensitive sensitive-data +``` + +3. **访问控制** + +```bash +# 使用RBAC(在Docker Enterprise或Kubernetes中) +# 使用Docker Content Trust签名镜像 +export DOCKER_CONTENT_TRUST=1 +``` + +## 安全事件响应 + +### 创建安全事件响应计划 + +1. **准备阶段** + - 记录Docker环境 + - 建立基线 + - 设置监控和警报 + +2. **检测阶段** + - 监控异常活动 + - 使用入侵检测系统 + +3. **遏制阶段** + +```bash +# 隔离受影响的容器 +docker network disconnect bridge compromised-container + +# 停止受影响的容器 +docker stop compromised-container + +# 保存容器状态以供分析 +docker commit compromised-container forensic-image +``` + +4. **根除阶段** + +```bash +# 删除受影响的容器和镜像 +docker rm compromised-container +docker rmi compromised-image + +# 更新基础镜像和应用 +docker pull base-image:latest +docker build -t my-app:latest . +``` + +5. **恢复阶段** + +```bash +# 从备份恢复数据 +docker run --rm -v backup-volume:/backup -v data-volume:/data alpine \ + sh -c "cd /data && tar -xzf /backup/backup.tar.gz" + +# 部署更新后的应用 +docker service update --image my-app:latest my_service +``` + +## Docker安全清单 + +### 主机安全 + +- [ ] 保持主机操作系统更新 +- [ ] 使用最小化的主机操作系统 +- [ ] 配置主机防火墙 +- [ ] 启用SELinux或AppArmor +- [ ] 限制对Docker套接字的访问 +- [ ] 配置Docker守护进程使用TLS +- [ ] 禁用不必要的服务 + +### 镜像安全 + +- [ ] 使用官方或验证的基础镜像 +- [ ] 使用最小化的基础镜像 +- [ ] 定期更新基础镜像 +- [ ] 使用多阶段构建 +- [ ] 不在镜像中存储敏感信息 +- [ ] 扫描镜像中的漏洞 +- [ ] 使用Docker Content Trust签名镜像 + +### 容器运行时安全 + +- [ ] 以非root用户运行容器 +- [ ] 使用只读文件系统 +- [ ] 限制容器资源 +- [ ] 限制容器功能 +- [ ] 使用seccomp配置文件 +- [ ] 使用AppArmor或SELinux配置文件 +- [ ] 禁用特权容器 +- [ ] 使用--no-new-privileges标志 + +### 网络安全 + +- [ ] 使用用户定义的网络 +- [ ] 限制容器间通信 +- [ ] 仅发布必要的端口 +- [ ] 使用TLS加密网络通信 +- [ ] 使用内部网络隔离敏感服务 + +### 数据安全 + +- [ ] 使用卷管理持久数据 +- [ ] 加密敏感数据 +- [ ] 定期备份数据 +- [ ] 使用Docker Secrets管理敏感信息 + +### 监控和审计 + +- [ ] 配置容器日志记录 +- [ ] 设置主机和容器监控 +- [ ] 启用Docker审计 +- [ ] 定期审查安全策略和配置 + +## 总结 + +Docker安全是一个多层面的挑战,需要从主机、镜像、容器运行时、网络和数据等多个方面进行考虑。通过实施本文介绍的最佳实践,您可以显著提高Docker环境的安全性,降低安全风险。 + +记住,安全是一个持续的过程,而不是一次性的工作。定期更新、监控、审计和改进您的Docker安全策略,以应对不断变化的安全威胁。 + +## 下一步学习 + +- [Docker生产环境部署](./docker-production.md) +- [Docker Swarm集群](./docker-swarm.md) +- [Docker Compose详解](./docker-compose.md) +- [Docker数据卷管理](./docker-volumes.md) \ No newline at end of file diff --git a/docs/docker/docker-swarm.md b/docs/docker/docker-swarm.md new file mode 100644 index 000000000..243323c22 --- /dev/null +++ b/docs/docker/docker-swarm.md @@ -0,0 +1,758 @@ +# Docker Swarm集群 + +## 什么是Docker Swarm + +Docker Swarm是Docker的原生集群管理工具,它将多个Docker主机组合成一个虚拟的Docker主机,提供标准的Docker API,使得应用可以像在单个Docker主机上一样被部署到Swarm集群中。 + +Docker Swarm的主要特点: + +- **分布式设计**:内置分布式设计,无单点故障 +- **声明式服务模型**:使用声明式API定义服务的期望状态 +- **服务扩展**:可以轻松扩展或缩减服务实例数量 +- **服务发现**:内置服务发现机制,自动为服务分配DNS名称 +- **负载均衡**:自动为服务提供负载均衡 +- **滚动更新**:支持服务的滚动更新和回滚 +- **安全通信**:节点间通信采用TLS加密,提供自动密钥轮换功能 + +## Swarm架构 + +### 节点类型 + +Swarm集群由两种类型的节点组成: + +1. **管理节点(Manager Node)**: + - 维护集群状态 + - 调度服务 + - 提供Swarm API + - 可以配置为高可用模式(通常建议3、5或7个管理节点) + +2. **工作节点(Worker Node)**: + - 执行容器 + - 不参与集群管理决策 + - 可以根据需要扩展 + +### 核心概念 + +- **节点(Node)**:参与Swarm集群的Docker引擎实例 +- **服务(Service)**:在Swarm上运行的任务的定义 +- **任务(Task)**:调度到节点上的Docker容器实例 +- **堆栈(Stack)**:一组相关服务,通常通过Compose文件定义 + +## 创建和管理Swarm集群 + +### 初始化Swarm集群 + +```bash +# 初始化一个新的Swarm集群(在管理节点上执行) +docker swarm init --advertise-addr + +# 输出将显示加入集群的命令,例如: +# docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c 192.168.99.100:2377 +``` + +### 添加节点到集群 + +```bash +# 获取加入集群的命令(在管理节点上执行) +# 获取工作节点的加入命令 +docker swarm join-token worker + +# 获取管理节点的加入命令 +docker swarm join-token manager + +# 在新节点上执行加入命令 +docker swarm join --token :2377 +``` + +### 查看集群信息 + +```bash +# 查看集群中的节点 +docker node ls + +# 查看节点详细信息 +docker node inspect + +# 以可读格式查看节点信息 +docker node inspect --pretty +``` + +### 管理节点 + +```bash +# 提升工作节点为管理节点 +docker node promote + +# 降级管理节点为工作节点 +docker node demote + +# 更新节点 +docker node update + +# 设置节点可用性 +docker node update --availability active|pause|drain + +# 添加节点标签 +docker node update --label-add datacenter=east + +# 删除节点 +docker node rm +``` + +### 离开Swarm集群 + +```bash +# 在工作节点上执行 +docker swarm leave + +# 在管理节点上执行(强制离开) +docker swarm leave --force +``` + +## 服务管理 + +### 创建服务 + +```bash +# 创建基本服务 +docker service create --name my-web nginx + +# 创建带端口映射的服务 +docker service create --name my-web --publish 8080:80 nginx + +# 创建带环境变量的服务 +docker service create --name my-db \ + --env MYSQL_ROOT_PASSWORD=secret \ + --env MYSQL_DATABASE=mydb \ + mysql:5.7 + +# 创建带卷的服务 +docker service create --name my-db \ + --mount type=volume,source=db-data,target=/var/lib/mysql \ + mysql:5.7 + +# 创建带约束的服务 +docker service create --name my-web \ + --constraint node.role==worker \ + --constraint node.labels.datacenter==east \ + nginx + +# 创建带资源限制的服务 +docker service create --name my-web \ + --limit-cpu 0.5 \ + --limit-memory 512M \ + nginx +``` + +### 查看服务信息 + +```bash +# 列出所有服务 +docker service ls + +# 查看服务详细信息 +docker service inspect + +# 以可读格式查看服务信息 +docker service inspect --pretty + +# 查看服务日志 +docker service logs + +# 查看服务任务 +docker service ps +``` + +### 更新服务 + +```bash +# 更新服务镜像 +docker service update --image nginx:1.19 my-web + +# 扩展服务实例数量 +docker service scale my-web=5 + +# 或者使用update命令扩展 +docker service update --replicas 5 my-web + +# 更新端口映射 +docker service update --publish-add 8081:80 my-web + +# 更新环境变量 +docker service update --env-add NODE_ENV=production my-web + +# 更新挂载 +docker service update --mount-add type=volume,source=new-data,target=/data my-web + +# 配置滚动更新策略 +docker service update \ + --update-parallelism 2 \ + --update-delay 10s \ + my-web + +# 回滚服务更新 +docker service update --rollback my-web +``` + +### 删除服务 + +```bash +# 删除服务 +docker service rm my-web +``` + +## 使用Docker Stack部署应用 + +Docker Stack允许使用Compose文件格式在Swarm上部署完整的应用程序堆栈。 + +### 创建堆栈文件 + +`docker-stack.yml`: + +```yaml +version: '3.8' + +services: + web: + image: nginx:latest + ports: + - "80:80" + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + placement: + constraints: + - node.role == worker + + visualizer: + image: dockersamples/visualizer + ports: + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + deploy: + placement: + constraints: + - node.role == manager + +networks: + webnet: + +volumes: + data-volume: +``` + +### 部署堆栈 + +```bash +# 部署堆栈 +docker stack deploy -c docker-stack.yml my-app + +# 列出所有堆栈 +docker stack ls + +# 列出堆栈中的服务 +docker stack services my-app + +# 列出堆栈中的任务 +docker stack ps my-app + +# 删除堆栈 +docker stack rm my-app +``` + +## Swarm网络 + +Swarm模式提供了多种网络驱动,用于不同的网络需求。 + +### 覆盖网络(Overlay Network) + +覆盖网络允许不同节点上的容器相互通信。 + +```bash +# 创建覆盖网络 +docker network create --driver overlay my-network + +# 创建加密的覆盖网络 +docker network create --driver overlay --opt encrypted my-secure-network + +# 创建服务并连接到覆盖网络 +docker service create --name my-web --network my-network nginx +``` + +### 入口网络(Ingress Network) + +入口网络是一个特殊的覆盖网络,用于服务的负载均衡和路由网格。 + +```bash +# 查看入口网络 +docker network ls --filter name=ingress + +# 重新创建入口网络(如果需要) +docker network rm ingress +docker network create --driver overlay --ingress ingress +``` + +## Swarm安全 + +### TLS配置 + +Swarm使用TLS进行节点间通信加密。 + +```bash +# 查看当前TLS配置 +docker info | grep "Swarm" + +# 轮换证书(在管理节点上执行) +docker swarm ca --rotate + +# 查看证书有效期 +docker swarm ca --quiet --cert-expiry +``` + +### 使用Secrets管理敏感数据 + +```bash +# 从文件创建secret +echo "mypassword" | docker secret create db_password - + +# 或者从文件创建 +docker secret create db_password ./password.txt + +# 列出secrets +docker secret ls + +# 查看secret详情 +docker secret inspect db_password + +# 创建使用secret的服务 +docker service create \ + --name db \ + --secret db_password \ + --env MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password \ + mysql:5.7 + +# 删除secret +docker secret rm db_password +``` + +### 使用Configs管理配置文件 + +```bash +# 从文件创建config +docker config create nginx_conf ./nginx.conf + +# 列出configs +docker config ls + +# 查看config详情 +docker config inspect nginx_conf + +# 创建使用config的服务 +docker service create \ + --name web \ + --config source=nginx_conf,target=/etc/nginx/nginx.conf \ + nginx + +# 删除config +docker config rm nginx_conf +``` + +## 高可用性配置 + +### 管理节点高可用 + +为了实现高可用性,Swarm使用Raft共识算法,建议使用奇数个管理节点。 + +```bash +# 推荐的管理节点数量: +# - 3个管理节点可以容忍1个节点故障 +# - 5个管理节点可以容忍2个节点故障 +# - 7个管理节点可以容忍3个节点故障 + +# 添加管理节点 +docker swarm join-token manager +# 然后在新节点上执行返回的命令 +``` + +### 备份和恢复Swarm状态 + +```bash +# 备份Swarm状态(在管理节点上执行) +systemctl stop docker +tar -czvf swarm-backup.tar.gz /var/lib/docker/swarm +systemctl start docker + +# 恢复Swarm状态 +systemctl stop docker +rm -rf /var/lib/docker/swarm +tar -xzvf swarm-backup.tar.gz -C /var/lib/docker +systemctl start docker +``` + +## 监控和日志 + +### 服务日志 + +```bash +# 查看服务日志 +docker service logs my-service + +# 实时查看日志 +docker service logs -f my-service + +# 查看最近的日志 +docker service logs --tail 100 my-service + +# 查看特定时间段的日志 +docker service logs --since 2020-01-01T00:00:00 my-service +``` + +### 使用Prometheus和Grafana监控Swarm + +```yaml +version: '3.8' + +services: + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + deploy: + placement: + constraints: + - node.role == manager + + node-exporter: + image: prom/node-exporter + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)' + deploy: + mode: global + + cadvisor: + image: google/cadvisor + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + ports: + - "8080:8080" + deploy: + mode: global + + grafana: + image: grafana/grafana + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + deploy: + placement: + constraints: + - node.role == manager + +volumes: + grafana-data: +``` + +## 实际应用示例 + +### 部署Web应用和数据库 + +```yaml +version: '3.8' + +services: + web: + image: nginx:latest + ports: + - "80:80" + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + placement: + constraints: + - node.role == worker + networks: + - webnet + + api: + image: my-api:latest + deploy: + replicas: 2 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + environment: + - DB_HOST=db + - DB_USER=root + - DB_PASSWORD_FILE=/run/secrets/db_password + - DB_NAME=myapp + networks: + - webnet + - dbnet + secrets: + - db_password + + db: + image: mysql:5.7 + deploy: + placement: + constraints: + - node.labels.db == true + volumes: + - db-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password + - MYSQL_DATABASE=myapp + networks: + - dbnet + secrets: + - db_password + +networks: + webnet: + dbnet: + driver: overlay + internal: true + +volumes: + db-data: + +secrets: + db_password: + external: true +``` + +### 蓝绿部署 + +```yaml +version: '3.8' + +services: + blue: + image: myapp:1.0 + deploy: + replicas: 3 + networks: + - frontend + + green: + image: myapp:2.0 + deploy: + replicas: 0 + networks: + - frontend + + proxy: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + deploy: + placement: + constraints: + - node.role == manager + networks: + - frontend + +networks: + frontend: +``` + +蓝绿切换: + +```bash +# 扩展绿色版本 +docker service scale my-app_green=3 + +# 更新Nginx配置指向绿色版本 +# ... + +# 缩减蓝色版本 +docker service scale my-app_blue=0 +``` + +## 最佳实践 + +### 管理节点配置 + +1. **使用奇数个管理节点**:3、5或7个,以实现高可用性 +2. **限制管理节点数量**:不要超过7个管理节点,以避免共识性能下降 +3. **管理节点专用**:在生产环境中,考虑将管理节点设置为仅管理,不运行工作负载 + ```bash + docker node update --availability drain + ``` + +### 服务部署策略 + +1. **使用标签和约束**:根据节点特性分配服务 + ```bash + # 添加标签 + docker node update --label-add ssd=true + + # 使用约束 + docker service create --constraint node.labels.ssd==true --name db mysql:5.7 + ``` + +2. **资源分配**:为服务设置资源限制 + ```bash + docker service create --limit-cpu 0.5 --limit-memory 512M --name app myapp + ``` + +3. **滚动更新**:配置适当的更新策略 + ```bash + docker service create \ + --update-parallelism 1 \ + --update-delay 30s \ + --update-failure-action rollback \ + --name web nginx + ``` + +### 网络安全 + +1. **使用内部网络**:对于不需要外部访问的服务 + ```bash + docker network create --driver overlay --internal db-network + ``` + +2. **加密覆盖网络**:对敏感数据传输 + ```bash + docker network create --driver overlay --opt encrypted secure-network + ``` + +3. **使用Secrets**:管理敏感信息 + +### 数据管理 + +1. **使用命名卷**:确保数据持久化 + ```bash + docker service create \ + --mount type=volume,source=db-data,target=/var/lib/mysql \ + --name db mysql:5.7 + ``` + +2. **备份策略**:定期备份关键数据 + +3. **考虑使用外部存储**:对于关键数据,考虑使用NFS、AWS EBS等 + +## 常见问题与解决方案 + +### 节点通信问题 + +**问题**:节点无法加入集群或节点间通信失败 + +**解决方案**: +1. 检查防火墙设置,确保以下端口开放: + - TCP 2377:集群管理通信 + - TCP/UDP 7946:节点间通信 + - UDP 4789:覆盖网络流量 + +2. 检查网络配置: + ```bash + # 查看Docker网络设置 + docker info + + # 重新初始化Swarm,指定正确的IP + docker swarm init --advertise-addr + ``` + +### 服务无法启动 + +**问题**:服务创建后无法启动或处于准备状态 + +**解决方案**: +1. 检查服务日志: + ```bash + docker service logs + ``` + +2. 检查任务状态: + ```bash + docker service ps + ``` + +3. 检查资源约束: + - 确保节点有足够的资源 + - 检查服务的约束条件是否有节点满足 + +4. 检查镜像可用性: + - 确保所有节点都能访问镜像仓库 + - 考虑预先在节点上拉取镜像 + +### 负载均衡问题 + +**问题**:服务负载不均衡或路由网格不工作 + +**解决方案**: +1. 检查入口网络: + ```bash + docker network inspect ingress + ``` + +2. 确认端口发布正确: + ```bash + docker service inspect --format='{{.Endpoint.Ports}}' + ``` + +3. 尝试重新创建入口网络: + ```bash + docker network rm ingress + docker network create --driver overlay --ingress ingress + ``` + +### 管理节点故障 + +**问题**:管理节点故障或无法访问 + +**解决方案**: +1. 如果仍有法定数量的管理节点运行: + - 系统将自动恢复 + - 可以移除故障节点: + ```bash + docker node rm --force + ``` + +2. 如果失去法定数量的管理节点: + - 需要强制恢复集群: + ```bash + # 在剩余的管理节点上 + docker swarm init --force-new-cluster + ``` + - 然后添加新的管理节点恢复冗余 + +## 总结 + +Docker Swarm是一个强大的容器编排工具,提供了简单易用的集群管理功能。通过Swarm,您可以将多个Docker主机组合成一个虚拟的Docker主机,实现服务的高可用性、负载均衡和扩展。 + +Swarm的核心优势在于其与Docker紧密集成,使用标准Docker API,学习曲线平缓。对于中小规模的容器部署,Swarm提供了足够的功能和性能。 + +通过本文介绍的命令和最佳实践,您应该能够创建、管理和维护一个健壮的Docker Swarm集群,为您的应用提供可靠的运行环境。 + +## 下一步学习 + +- [Docker安全最佳实践](./docker-security.md) +- [Docker Compose详解](./docker-compose.md) +- [Docker数据卷管理](./docker-volumes.md) +- [Docker生产环境部署](./docker-production.md) \ No newline at end of file diff --git a/docs/docker/docker-tutorial.md b/docs/docker/docker-tutorial.md new file mode 100644 index 000000000..73f631005 --- /dev/null +++ b/docs/docker/docker-tutorial.md @@ -0,0 +1,237 @@ +# Docker教程 + +## Docker简介 + +Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。 + +## Docker的优势 + +- **轻量级**:Docker容器非常轻量级,启动快速,资源利用率高 +- **一致的运行环境**:Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性 +- **持续交付和部署**:使用Docker可以通过定制应用镜像来实现持续集成、持续交付、部署 +- **更高效的资源利用**:Docker容器的运行不需要额外的虚拟化管理程序支持,节约了计算资源 + +## Docker核心概念 + +### 1. 镜像(Image) + +镜像是Docker容器运行时的只读模板,包含了运行容器所需的文件系统结构和内容。 + +### 2. 容器(Container) + +容器是镜像的运行实例,可以被创建、启动、停止、删除、暂停等。 + +### 3. 仓库(Repository) + +仓库是集中存放镜像的地方,分为公开仓库和私有仓库。 + +## Docker安装 + +### Ubuntu安装Docker + +```bash +# 更新apt包索引 +sudo apt-get update + +# 安装必要的系统工具 +sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release + +# 添加Docker官方GPG密钥 +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + +# 设置稳定版仓库 +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# 更新apt包索引 +sudo apt-get update + +# 安装Docker Engine +sudo apt-get install docker-ce docker-ce-cli containerd.io + +# 验证Docker是否安装成功 +sudo docker run hello-world +``` + +### CentOS安装Docker + +```bash +# 卸载旧版本 +sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine + +# 安装必要的系统工具 +sudo yum install -y yum-utils + +# 设置仓库 +sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + +# 安装Docker Engine +sudo yum install docker-ce docker-ce-cli containerd.io + +# 启动Docker +sudo systemctl start docker + +# 验证Docker是否安装成功 +sudo docker run hello-world +``` + +### Windows安装Docker + +1. 下载[Docker Desktop for Windows](https://www.docker.com/products/docker-desktop) +2. 双击安装包进行安装 +3. 安装完成后,启动Docker Desktop +4. 打开命令提示符或PowerShell,运行`docker --version`验证安装 + +## Docker基本命令 + +### 镜像操作 + +```bash +# 列出本地镜像 +docker images + +# 搜索镜像 +docker search nginx + +# 拉取镜像 +docker pull nginx + +# 删除镜像 +docker rmi nginx +``` + +### 容器操作 + +```bash +# 创建并启动容器 +docker run -d -p 80:80 --name webserver nginx + +# 列出运行中的容器 +docker ps + +# 列出所有容器 +docker ps -a + +# 停止容器 +docker stop webserver + +# 启动容器 +docker start webserver + +# 重启容器 +docker restart webserver + +# 删除容器 +docker rm webserver +``` + +## Dockerfile基础 + +Dockerfile是用来构建Docker镜像的文本文件,包含了一条条指令,每一条指令构建一层镜像,因此每一条指令的内容,就是描述该层镜像应当如何构建。 + +### 基本指令 + +```dockerfile +# 基于哪个镜像 +FROM nginx:latest + +# 维护者信息 +MAINTAINER author "author@example.com" + +# 执行命令 +RUN apt-get update && apt-get install -y vim + +# 添加文件 +ADD index.html /usr/share/nginx/html/ + +# 拷贝文件 +COPY conf/nginx.conf /etc/nginx/nginx.conf + +# 设置环境变量 +ENV PATH /usr/local/nginx/bin:$PATH + +# 暴露端口 +EXPOSE 80 443 + +# 设置卷 +VOLUME ["/data"] + +# 设置工作目录 +WORKDIR /usr/share/nginx/html + +# 设置启动命令 +CMD ["nginx", "-g", "daemon off;"] +``` + +### 构建镜像 + +```bash +# 在Dockerfile所在目录执行 +docker build -t my-nginx . +``` + +## Docker Compose基础 + +Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。通过Compose,您可以使用YAML文件来配置应用程序的服务,然后使用一个命令创建并启动所有服务。 + +### 安装Docker Compose + +```bash +# 下载Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + +# 添加可执行权限 +sudo chmod +x /usr/local/bin/docker-compose + +# 验证安装 +docker-compose --version +``` + +### docker-compose.yml示例 + +```yaml +version: '3' +services: + web: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./html:/usr/share/nginx/html + networks: + - webnet + redis: + image: redis:latest + networks: + - webnet +networks: + webnet: +``` + +### 常用命令 + +```bash +# 启动所有服务 +docker-compose up -d + +# 停止所有服务 +docker-compose down + +# 查看服务状态 +docker-compose ps + +# 查看服务日志 +docker-compose logs +``` + +## 下一步学习 + +- Docker网络配置 +- Docker数据卷管理 +- Docker Swarm集群 +- Kubernetes容器编排 + +## 参考资源 + +- [Docker官方文档](https://docs.docker.com/) +- [Docker Hub](https://hub.docker.com/) +- [Docker Compose文档](https://docs.docker.com/compose/) \ No newline at end of file diff --git a/docs/docker/docker-volumes.md b/docs/docker/docker-volumes.md new file mode 100644 index 000000000..05a9e219a --- /dev/null +++ b/docs/docker/docker-volumes.md @@ -0,0 +1,449 @@ +# Docker数据卷管理 + +## 什么是Docker数据卷 + +Docker数据卷是一种持久化数据的机制,它允许在容器和宿主机之间共享文件和目录。数据卷是独立于容器的特殊目录,具有以下特点: + +- **数据持久化**:容器删除后,数据卷仍然存在 +- **数据共享**:多个容器可以共享同一个数据卷 +- **数据隔离**:数据卷与容器的生命周期解耦 +- **性能优化**:数据卷的I/O性能通常优于容器内部文件系统 +- **跨平台支持**:数据卷在不同操作系统上工作方式一致 + +## 数据管理方式 + +Docker提供了三种主要的数据管理方式: + +1. **数据卷(Volumes)**:由Docker管理的宿主机文件系统的一部分(通常在`/var/lib/docker/volumes/`) +2. **绑定挂载(Bind Mounts)**:将宿主机上的任意目录或文件挂载到容器中 +3. **tmpfs挂载(tmpfs Mounts)**:将数据存储在宿主机的内存中,而不是磁盘上 + +## Docker数据卷操作 + +### 创建和管理数据卷 + +```bash +# 创建数据卷 +docker volume create my-volume + +# 查看所有数据卷 +docker volume ls + +# 查看数据卷详细信息 +docker volume inspect my-volume + +# 删除数据卷 +docker volume rm my-volume + +# 删除所有未使用的数据卷 +docker volume prune + +# 创建带标签的数据卷 +docker volume create --label environment=production my-volume + +# 创建使用特定驱动的数据卷 +docker volume create --driver local my-volume + +# 创建带选项的数据卷 +docker volume create --opt type=nfs --opt o=addr=192.168.1.1,rw --opt device=:/path/to/dir my-nfs-volume +``` + +### 在容器中使用数据卷 + +```bash +# 使用数据卷启动容器 +docker run -d -v my-volume:/app/data nginx + +# 使用只读数据卷 +docker run -d -v my-volume:/app/data:ro nginx + +# 使用新语法挂载数据卷 +docker run -d --mount source=my-volume,target=/app/data nginx + +# 使用匿名数据卷 +docker run -d -v /app/data nginx + +# 使用临时数据卷(容器删除后自动删除) +docker run -d --mount source=my-volume,target=/app/data,tmpfs=true nginx +``` + +## 绑定挂载操作 + +绑定挂载允许将宿主机上的任意路径挂载到容器中。 + +```bash +# 基本绑定挂载 +docker run -d -v /host/path:/container/path nginx + +# 使用只读绑定挂载 +docker run -d -v /host/path:/container/path:ro nginx + +# 使用新语法进行绑定挂载 +docker run -d --mount type=bind,source=/host/path,target=/container/path nginx + +# 挂载单个文件 +docker run -d -v /host/file.conf:/container/file.conf nginx + +# 使用相对路径(不推荐) +docker run -d -v $(pwd)/config:/container/config nginx +``` + +## tmpfs挂载操作 + +tmpfs挂载将数据存储在宿主机的内存中,适用于存储非持久化的敏感数据。 + +```bash +# 基本tmpfs挂载 +docker run -d --tmpfs /app/temp nginx + +# 指定tmpfs选项 +docker run -d --tmpfs /app/temp:rw,noexec,nosuid,size=100m nginx + +# 使用新语法进行tmpfs挂载 +docker run -d --mount type=tmpfs,destination=/app/temp nginx + +# 指定tmpfs大小 +docker run -d --mount type=tmpfs,destination=/app/temp,tmpfs-size=100m nginx + +# 指定tmpfs模式 +docker run -d --mount type=tmpfs,destination=/app/temp,tmpfs-mode=1770 nginx +``` + +## 数据卷使用场景 + +### 数据库持久化 + +```bash +# 创建MySQL数据卷 +docker volume create mysql-data + +# 启动MySQL容器并使用数据卷 +docker run -d \ + --name mysql \ + -e MYSQL_ROOT_PASSWORD=password \ + -v mysql-data:/var/lib/mysql \ + mysql:5.7 +``` + +### 配置文件管理 + +```bash +# 创建配置数据卷 +docker volume create nginx-conf + +# 从容器复制默认配置到宿主机 +docker run --rm -v nginx-conf:/nginx nginx bash -c "cp -a /etc/nginx/. /nginx/" + +# 使用自定义配置启动容器 +docker run -d \ + --name nginx \ + -v nginx-conf:/etc/nginx \ + -p 80:80 \ + nginx +``` + +### 应用数据共享 + +```bash +# 创建共享数据卷 +docker volume create shared-data + +# 启动多个容器共享数据 +docker run -d --name app1 -v shared-data:/app/data nginx +docker run -d --name app2 -v shared-data:/app/data nginx +``` + +### 开发环境代码挂载 + +```bash +# 挂载源代码目录 +docker run -d \ + --name node-app \ + -v $(pwd):/app \ + -w /app \ + -p 3000:3000 \ + node:14 \ + npm start +``` + +## 数据卷备份与恢复 + +### 备份数据卷 + +```bash +# 使用临时容器备份数据卷 +docker run --rm \ + -v my-volume:/source \ + -v $(pwd):/backup \ + alpine \ + tar -czvf /backup/my-volume-backup.tar.gz -C /source . +``` + +### 恢复数据卷 + +```bash +# 创建新数据卷 +docker volume create my-new-volume + +# 使用临时容器恢复数据 +docker run --rm \ + -v my-new-volume:/target \ + -v $(pwd):/backup \ + alpine \ + sh -c "tar -xzvf /backup/my-volume-backup.tar.gz -C /target" +``` + +## 数据卷驱动 + +Docker支持多种数据卷驱动,用于扩展数据卷功能。 + +### 本地驱动(local) + +默认的数据卷驱动,将数据存储在宿主机本地文件系统中。 + +```bash +docker volume create --driver local my-volume +``` + +### NFS驱动 + +允许使用NFS服务器作为数据卷存储。 + +```bash +# 安装NFS驱动 +docker plugin install --grant-all-permissions vieux/sshfs + +# 创建NFS数据卷 +docker volume create --driver vieux/sshfs \ + -o sshcmd=user@host:/path \ + -o password=password \ + sshfs-volume +``` + +### 其他第三方驱动 + +- **Flocker**:用于多主机环境 +- **GlusterFS**:分布式文件系统 +- **Ceph**:分布式存储系统 +- **NetApp**:企业级存储解决方案 + +```bash +# 安装第三方驱动 +docker plugin install + +# 创建使用第三方驱动的数据卷 +docker volume create --driver --opt key=value my-volume +``` + +## 数据卷在Docker Compose中的使用 + +### 基本用法 + +```yaml +version: '3' + +services: + web: + image: nginx + volumes: + - web-data:/usr/share/nginx/html + - ./config/nginx.conf:/etc/nginx/nginx.conf:ro + - /var/log/nginx:/var/log/nginx + +volumes: + web-data: + driver: local +``` + +### 外部数据卷 + +```yaml +version: '3' + +services: + db: + image: mysql:5.7 + volumes: + - db-data:/var/lib/mysql + +volumes: + db-data: + external: true +``` + +### 带选项的数据卷 + +```yaml +version: '3' + +services: + web: + image: nginx + volumes: + - web-data:/usr/share/nginx/html + +volumes: + web-data: + driver: local + driver_opts: + type: nfs + o: addr=192.168.1.1,rw + device: ":/path/to/dir" +``` + +## 数据卷最佳实践 + +### 命名约定 + +使用有意义的名称命名数据卷,反映其用途和内容。 + +```bash +# 好的命名示例 +docker volume create mysql-prod-data +docker volume create nginx-conf-staging +``` + +### 数据卷标签 + +使用标签组织和管理数据卷。 + +```bash +# 添加标签 +docker volume create --label environment=production --label app=mysql mysql-data + +# 根据标签筛选 +docker volume ls --filter label=environment=production +``` + +### 数据卷权限 + +确保容器内的用户对挂载的数据卷有适当的权限。 + +```bash +# 在容器启动前设置权限 +docker run --rm -v my-volume:/data alpine chown -R 1000:1000 /data + +# 使用entrypoint脚本设置权限 +cat > entrypoint.sh << 'EOF' +#!/bin/sh +chown -R user:user /app/data +exec "$@" +EOF + +# Dockerfile中设置 +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] +``` + +### 数据卷清理策略 + +定期清理未使用的数据卷,防止磁盘空间浪费。 + +```bash +# 查找未使用的数据卷 +docker volume ls -f dangling=true + +# 删除未使用的数据卷 +docker volume prune + +# 创建定期清理的cron作业 +echo "0 0 * * * docker volume prune -f" | sudo tee -a /etc/crontab +``` + +## 数据卷安全考虑 + +### 敏感数据处理 + +```bash +# 使用tmpfs存储敏感数据 +docker run -d --tmpfs /app/secrets:rw,noexec,nosuid,size=1m nginx + +# 使用Docker Secrets(在Swarm模式下) +docker secret create my-secret /path/to/secret/file +docker service create --secret my-secret nginx +``` + +### 访问控制 + +```bash +# 限制数据卷访问权限 +docker run -d -v my-volume:/app/data:ro nginx + +# 使用非root用户访问数据卷 +docker run -d -v my-volume:/app/data --user 1000:1000 nginx +``` + +### 加密数据 + +```bash +# 使用加密文件系统 +docker volume create --driver local \ + --opt type=btrfs \ + --opt device=/dev/mapper/encrypted-device \ + encrypted-volume +``` + +## 常见问题与解决方案 + +### 权限问题 + +**问题**:容器内无法写入挂载的数据卷 + +**解决方案**: +1. 调整宿主机上的权限 + ```bash + sudo chown -R 1000:1000 /path/to/volume + ``` + +2. 在容器内调整权限 + ```bash + docker exec -it container_id chown -R user:user /app/data + ``` + +3. 使用相同的UID/GID + ```bash + docker run -d --user $(id -u):$(id -g) -v /host/path:/container/path nginx + ``` + +### 数据卷无法删除 + +**问题**:尝试删除数据卷时出现错误 + +**解决方案**: +1. 确保没有容器使用该数据卷 + ```bash + docker ps -a --filter volume=my-volume + ``` + +2. 强制删除(谨慎使用) + ```bash + docker volume rm -f my-volume + ``` + +3. 重启Docker服务 + ```bash + sudo systemctl restart docker + ``` + +### 性能问题 + +**问题**:数据卷操作性能较低 + +**解决方案**: +1. 使用数据卷而不是绑定挂载 +2. 减少挂载点数量 +3. 考虑使用内存挂载(tmpfs)用于临时数据 +4. 使用性能更好的存储驱动 + +## 总结 + +Docker数据卷是容器化应用中管理持久数据的关键机制。通过合理使用数据卷、绑定挂载和tmpfs挂载,可以实现数据持久化、共享和隔离,同时保持容器的轻量级和可移植性。掌握数据卷的创建、管理和最佳实践,对于构建健壮的容器化应用至关重要。 + +## 下一步学习 + +- [Docker Compose详解](./docker-compose.md) +- [Docker Swarm集群](./docker-swarm.md) +- [Docker安全最佳实践](./docker-security.md) +- [Docker生产环境部署](./docker-production.md) \ No newline at end of file diff --git "a/docs/docker/docker\346\236\266\346\236\204.md" "b/docs/docker/docker\346\236\266\346\236\204.md" new file mode 100644 index 000000000..3949d6070 --- /dev/null +++ "b/docs/docker/docker\346\236\266\346\236\204.md" @@ -0,0 +1,74 @@ +# docker架构 + +Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。 + +Docker 容器通过 Docker 镜像来创建。 + +容器与镜像的关系类似于面向对象编程中的对象与类。 + +Docker 面向对象 +容器 对象 +镜像 类 + +![img.png](./img.png) + +Docker 镜像(Images) + +Docker 镜像是用于创建 Docker 容器的模板。 + +Docker 容器(Container) + +容器是独立运行的一个或一组应用。 + +Docker 客户端(Client) + +Docker 客户端通过命令行或者其他工具使用 Docker API (https://docs.docker.com/reference/api/docker_remote_api) 与 Docker 的守护进程通信。 + +Docker 主机(Host) + +一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。 + +Docker 仓库(Registry) + +Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。 + +Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。 + +Docker Machine + +Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。 + +在docker容器中运行一个 Python Flask 应用来运行一个web应用。 + +```shell +Da@Da:~# docker run -d -P training/webapp python app.py +``` + +参数说明: + +-d:让容器在后台运行。 + +-P:将容器内部使用的网络端口映射到我们使用的主机上。 + +查看 WEB 应用容器 +使用 docker ps 来查看我们正在运行的容器 + +这里多了端口信息。 + +PORTS + +```java +0.0.0.0:32769->5000/tcp +``` + +Docker 开放了 5000 端口(默认 Python Flask 端口)映射到主机端口 32769 上。 + +这时我们可以通过浏览器访问WEB应用 + +也可以指定 -p 标识来绑定指定端口。 + +```java +Da@Da:~$ docker run -d -p 5000:5000 training/webapp python app.py +``` + +docker ps查看正在运行的容器 diff --git a/docs/docker/img.png b/docs/docker/img.png new file mode 100644 index 000000000..e1da18388 Binary files /dev/null and b/docs/docker/img.png differ diff --git "a/docs/e/JDK\345\274\200\345\217\221\345\267\245\345\205\267\350\257\246\350\247\243.md" "b/docs/e/JDK\345\274\200\345\217\221\345\267\245\345\205\267\350\257\246\350\247\243.md" new file mode 100644 index 000000000..5b58a74af --- /dev/null +++ "b/docs/e/JDK\345\274\200\345\217\221\345\267\245\345\205\267\350\257\246\350\247\243.md" @@ -0,0 +1,566 @@ +--- +title: JDK开发工具详解 +date: 2024-01-15 +categories: + - Java + - JDK +tags: + - JDK + - 开发工具 + - 编译 + - 调试 +--- + +# JDK开发工具详解 + +## JDK工具概览 + +JDK(Java Development Kit)提供了丰富的开发工具,这些工具覆盖了Java开发的各个环节,从编译、运行到调试、分析。 + +```mermaid +graph TD + A[JDK开发工具] --> B[编译工具] + A --> C[运行工具] + A --> D[调试工具] + A --> E[分析工具] + A --> F[文档工具] + A --> G[打包工具] + + B --> B1[javac] + C --> C1[java] + C --> C2[appletviewer] + D --> D1[jdb] + D --> D2[jconsole] + E --> E1[jps] + E --> E2[jstat] + E --> E3[jmap] + E --> E4[jstack] + F --> F1[javadoc] + G --> G1[jar] + G --> G2[jarsigner] +``` + +## 核心编译工具 + +### javac - Java编译器 + +#### 基本用法 + +```bash +# 基本编译 +javac HelloWorld.java + +# 指定输出目录 +javac -d ./classes HelloWorld.java + +# 指定类路径 +javac -cp ./lib/*:./classes HelloWorld.java + +# 编译整个包 +javac -d ./classes src/com/example/*.java +``` + +#### 常用参数 + +| 参数 | 功能 | 示例 | +|------|------|------| +| -d | 指定输出目录 | `javac -d ./classes *.java` | +| -cp/-classpath | 指定类路径 | `javac -cp ./lib/* Main.java` | +| -sourcepath | 指定源文件路径 | `javac -sourcepath ./src Main.java` | +| -encoding | 指定字符编码 | `javac -encoding UTF-8 *.java` | +| -g | 生成调试信息 | `javac -g *.java` | +| -verbose | 显示详细信息 | `javac -verbose *.java` | + +#### 高级编译选项 + +```bash +# 指定Java版本 +javac -source 11 -target 11 *.java + +# 启用预览特性 +javac --enable-preview --release 17 *.java + +# 模块化编译 +javac --module-path ./modules -d ./output *.java + +# 注解处理 +javac -processor com.example.MyProcessor *.java +``` + +### 编译过程详解 + +```mermaid +graph LR + A[Java源文件] --> B[词法分析] + B --> C[语法分析] + C --> D[语义分析] + D --> E[字节码生成] + E --> F[Class文件] + + G["编译器优化
- 常量折叠
- 死代码消除
- 内联优化"] --> E +``` + +## 运行工具 + +### java - Java解释器 + +#### 基本用法 + +```bash +# 运行类文件 +java HelloWorld + +# 指定类路径运行 +java -cp ./classes:./lib/* com.example.Main + +# 运行JAR文件 +java -jar application.jar + +# 模块化运行 +java --module-path ./modules -m mymodule/com.example.Main +``` + +#### JVM参数配置 + +```bash +# 内存设置 +java -Xms512m -Xmx2g MyApp + +# 垃圾收集器设置 +java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MyApp + +# 系统属性设置 +java -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai MyApp + +# 启用JMX监控 +java -Dcom.sun.management.jmxremote \ + -Dcom.sun.management.jmxremote.port=9999 \ + -Dcom.sun.management.jmxremote.authenticate=false \ + MyApp +``` + +### appletviewer - Applet查看器 + +```bash +# 运行Applet +appletviewer MyApplet.html + +# 指定安全策略 +appletviewer -J-Djava.security.policy=my.policy MyApplet.html +``` + +## 调试工具 + +### jdb - Java调试器 + +#### 基本调试流程 + +```bash +# 1. 编译时生成调试信息 +javac -g *.java + +# 2. 启动调试会话 +jdb MyClass + +# 3. 或者连接到运行中的JVM +java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 MyClass +jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5005 +``` + +#### 调试命令 + +```bash +# 设置断点 +stop at MyClass:10 +stop in MyClass.myMethod + +# 执行控制 +run # 开始执行 +cont # 继续执行 +step # 单步执行 +step up # 跳出当前方法 +next # 执行下一行 + +# 查看信息 +locals # 查看局部变量 +print variable # 打印变量值 +dump object # 查看对象状态 +where # 查看调用栈 +threads # 查看线程信息 +``` + +#### 调试示例 + +```java +// DebugExample.java +public class DebugExample { + public static void main(String[] args) { + int a = 10; + int b = 20; + int result = add(a, b); + System.out.println("Result: " + result); + } + + public static int add(int x, int y) { + return x + y; + } +} +``` + +```bash +# 调试会话 +$ javac -g DebugExample.java +$ jdb DebugExample +> stop at DebugExample:6 +> run +> locals +> print a +> print b +> step +> print result +``` + +### jconsole - JVM监控工具 + +```bash +# 启动JConsole +jconsole + +# 连接到特定进程 +jconsole + +# 远程连接 +jconsole service:jmx:rmi:///jndi/rmi://hostname:port/jmxrmi +``` + +#### JConsole功能面板 + +```mermaid +graph TD + A[JConsole] --> B[概述 Overview] + A --> C[内存 Memory] + A --> D[线程 Threads] + A --> E[类 Classes] + A --> F[MBeans] + A --> G[VM摘要] + + C --> C1[堆内存使用] + C --> C2[非堆内存使用] + C --> C3[垃圾回收] + + D --> D1[线程数量] + D --> D2[线程状态] + D --> D3[死锁检测] +``` + +## 性能分析工具 + +### jps - Java进程状态工具 + +```bash +# 显示所有Java进程 +jps + +# 显示详细信息 +jps -l # 显示完整类名 +jps -v # 显示JVM参数 +jps -m # 显示main方法参数 + +# 示例输出 +12345 com.example.MyApplication +12346 org.apache.catalina.startup.Bootstrap +``` + +### jstat - JVM统计信息工具 + +```bash +# 查看GC统计信息 +jstat -gc # 一次性查看 +jstat -gc 1000 # 每秒查看一次 +jstat -gc 1000 10 # 每秒查看一次,共10次 + +# 查看类加载统计 +jstat -class + +# 查看编译统计 +jstat -compiler + +# 查看堆内存统计 +jstat -gccapacity +``` + +#### jstat输出解析 + +```bash +# jstat -gc 输出示例 + S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT +2048.0 2048.0 0.0 1024.0 16384.0 8192.0 32768.0 16384.0 21248.0 20480.0 2560.0 2304.0 10 0.150 2 0.200 0.350 + +# 字段说明: +# S0C/S1C: Survivor区容量 +# S0U/S1U: Survivor区使用量 +# EC: Eden区容量 +# EU: Eden区使用量 +# OC: 老年代容量 +# OU: 老年代使用量 +# YGC: 年轻代GC次数 +# YGCT: 年轻代GC时间 +``` + +### jmap - 内存映像工具 + +```bash +# 查看堆内存使用情况 +jmap -heap + +# 生成堆转储文件 +jmap -dump:format=b,file=heap.hprof + +# 查看类实例统计 +jmap -histo + +# 查看永久代使用情况 +jmap -permstat # JDK 7及以前 +jmap -clstats # JDK 8及以后 +``` + +### jstack - 线程堆栈工具 + +```bash +# 打印线程堆栈 +jstack + +# 输出到文件 +jstack > thread.dump + +# 强制打印(进程无响应时) +jstack -F +``` + +#### 线程状态分析 + +```java +// 线程堆栈示例 +"main" #1 prio=5 os_prio=0 tid=0x... nid=0x... runnable [0x...] + java.lang.Thread.State: RUNNABLE + at java.io.FileInputStream.readBytes(Native Method) + at java.io.FileInputStream.read(FileInputStream.java:255) + at com.example.MyClass.readFile(MyClass.java:25) + at com.example.MyClass.main(MyClass.java:10) + +// 线程状态说明: +// RUNNABLE: 运行中 +// BLOCKED: 阻塞等待锁 +// WAITING: 等待其他线程 +// TIMED_WAITING: 限时等待 +``` + +## 文档工具 + +### javadoc - 文档生成器 + +#### 基本用法 + +```bash +# 生成单个类的文档 +javadoc MyClass.java + +# 生成包文档 +javadoc -d ./docs com.example.mypackage + +# 生成项目文档 +javadoc -d ./docs -sourcepath ./src -subpackages com.example +``` + +#### 高级选项 + +```bash +# 自定义文档 +javadoc -d ./docs \ + -windowtitle "My API" \ + -doctitle "My Application API" \ + -header "My App v1.0" \ + -footer "Copyright 2024" \ + -author \ + -version \ + -use \ + -splitindex \ + com.example.mypackage +``` + +#### 文档注释规范 + +```java +/** + * 计算两个数的和 + *

+ * 这个方法接受两个整数参数,返回它们的和。 + * 支持正数、负数和零。 + *

+ * + * @param a 第一个加数 + * @param b 第二个加数 + * @return 两个数的和 + * @throws ArithmeticException 当结果溢出时抛出 + * @since 1.0 + * @author John Doe + * @version 1.2 + * @see #subtract(int, int) + * @deprecated 使用 {@link #addLong(long, long)} 代替 + */ +public int add(int a, int b) { + return a + b; +} +``` + +## 打包工具 + +### jar - Java归档工具 + +#### 创建JAR文件 + +```bash +# 创建基本JAR文件 +jar cf myapp.jar *.class + +# 创建包含清单文件的JAR +jar cfm myapp.jar MANIFEST.MF *.class + +# 创建可执行JAR +jar cfe myapp.jar com.example.Main *.class + +# 递归打包目录 +jar cf myapp.jar -C classes . +``` + +#### JAR文件操作 + +```bash +# 查看JAR内容 +jar tf myapp.jar + +# 详细查看JAR内容 +jar tvf myapp.jar + +# 提取JAR文件 +jar xf myapp.jar + +# 更新JAR文件 +jar uf myapp.jar NewClass.class +``` + +#### MANIFEST.MF文件 + +``` +Manifest-Version: 1.0 +Main-Class: com.example.Main +Class-Path: lib/commons-lang3-3.12.0.jar lib/gson-2.8.8.jar +Implementation-Title: My Application +Implementation-Version: 1.0.0 +Implementation-Vendor: My Company +Created-By: 11.0.2 (Eclipse OpenJ9) +``` + +### jarsigner - JAR签名工具 + +```bash +# 生成密钥对 +keytool -genkeypair -alias mykey -keystore mykeystore.jks + +# 签名JAR文件 +jarsigner -keystore mykeystore.jks myapp.jar mykey + +# 验证签名 +jarsigner -verify myapp.jar + +# 详细验证 +jarsigner -verify -verbose myapp.jar +``` + +## 其他实用工具 + +### keytool - 密钥和证书管理 + +```bash +# 生成密钥对 +keytool -genkeypair -alias mykey -keyalg RSA -keysize 2048 -keystore keystore.jks + +# 导出证书 +keytool -exportcert -alias mykey -keystore keystore.jks -file mycert.cer + +# 导入证书 +keytool -importcert -alias trustedcert -keystore truststore.jks -file mycert.cer + +# 列出密钥库内容 +keytool -list -keystore keystore.jks +``` + +### native2ascii - 字符编码转换 + +```bash +# 将中文转换为Unicode编码 +native2ascii -encoding UTF-8 source.properties target.properties + +# 反向转换 +native2ascii -reverse target.properties source.properties +``` + +## 工具使用最佳实践 + +### 开发环境配置 + +```bash +# 设置JAVA_HOME +export JAVA_HOME=/path/to/jdk +export PATH=$JAVA_HOME/bin:$PATH + +# 验证安装 +java -version +javac -version +``` + +### 构建脚本示例 + +```bash +#!/bin/bash +# build.sh - 简单的构建脚本 + +# 清理输出目录 +rm -rf classes docs +mkdir -p classes docs + +# 编译源代码 +echo "编译源代码..." +javac -d classes -sourcepath src src/com/example/*.java + +# 生成文档 +echo "生成文档..." +javadoc -d docs -sourcepath src -subpackages com.example + +# 创建JAR文件 +echo "创建JAR文件..." +jar cfe myapp.jar com.example.Main -C classes . + +echo "构建完成!" +``` + +### 调试配置 + +```bash +# 开发环境调试配置 +JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005" +java $JAVA_OPTS -cp classes com.example.Main +``` + +## 总结 + +JDK提供的开发工具覆盖了Java开发的全生命周期: + +- **编译阶段**:javac进行源码编译 +- **运行阶段**:java执行字节码 +- **调试阶段**:jdb进行程序调试 +- **分析阶段**:jps、jstat、jmap、jstack进行性能分析 +- **文档阶段**:javadoc生成API文档 +- **打包阶段**:jar创建分发包 + +熟练掌握这些工具的使用,能够显著提高Java开发效率和代码质量。 \ No newline at end of file diff --git "a/docs/e/JVM\346\267\261\345\205\245\350\247\243\346\236\220.md" "b/docs/e/JVM\346\267\261\345\205\245\350\247\243\346\236\220.md" new file mode 100644 index 000000000..06bb7072a --- /dev/null +++ "b/docs/e/JVM\346\267\261\345\205\245\350\247\243\346\236\220.md" @@ -0,0 +1,419 @@ +--- +title: JVM深入解析 +date: 2024-01-15 +categories: + - Java + - JVM +tags: + - JVM + - 内存管理 + - 垃圾回收 + - 性能优化 +--- + +# JVM深入解析 + +## JVM架构详解 + +### JVM整体架构 + +```mermaid +graph TB + subgraph "JVM架构" + A[类加载器 ClassLoader] --> B[运行时数据区 Runtime Data Area] + B --> C[执行引擎 Execution Engine] + C --> D[本地方法接口 Native Interface] + D --> E[本地方法库 Native Libraries] + end + + subgraph "运行时数据区" + B1[方法区 Method Area] + B2[堆内存 Heap] + B3[栈内存 Stack] + B4[程序计数器 PC Register] + B5[本地方法栈 Native Method Stack] + end + + B --> B1 + B --> B2 + B --> B3 + B --> B4 + B --> B5 +``` + +## 类加载机制 + +### 类加载过程 + +```mermaid +graph LR + A[加载 Loading] --> B[验证 Verification] + B --> C[准备 Preparation] + C --> D[解析 Resolution] + D --> E[初始化 Initialization] + + subgraph "链接 Linking" + B + C + D + end +``` + +### 类加载器层次结构 + +```mermaid +graph TD + A[启动类加载器 Bootstrap ClassLoader] --> B[扩展类加载器 Extension ClassLoader] + B --> C[应用程序类加载器 Application ClassLoader] + C --> D[自定义类加载器 Custom ClassLoader] + + A1["加载核心类库
rt.jar等"] --> A + B1["加载扩展类库
ext目录下的jar"] --> B + C1["加载应用程序类
classpath下的类"] --> C + D1["用户自定义加载逻辑"] --> D +``` + +### 双亲委派模型 + +```java +// 双亲委派模型示例 +protected Class loadClass(String name, boolean resolve) { + // 首先检查类是否已经被加载 + Class c = findLoadedClass(name); + if (c == null) { + try { + // 委派给父类加载器 + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // 父类加载器无法加载时,自己尝试加载 + c = findClass(name); + } + } + return c; +} +``` + +## 内存区域详解 + +### 堆内存结构 + +```mermaid +graph TB + subgraph "堆内存 Heap" + A[新生代 Young Generation] + B[老年代 Old Generation] + C[永久代/元空间 Permanent/Metaspace] + end + + subgraph "新生代结构" + A1[Eden区] + A2[Survivor 0] + A3[Survivor 1] + end + + A --> A1 + A --> A2 + A --> A3 + + D["对象分配流程"] --> A1 + A1 --> |"Minor GC"| A2 + A2 --> |"Minor GC"| A3 + A3 --> |"对象年龄达到阈值"| B +``` + +### 内存分配策略 + +| 策略 | 描述 | 适用场景 | +|------|------|----------| +| 对象优先在Eden分配 | 新对象首先在Eden区分配 | 大部分对象 | +| 大对象直接进入老年代 | 超过阈值的大对象 | 大数组、大字符串 | +| 长期存活对象进入老年代 | 经过多次GC仍存活 | 缓存对象 | +| 动态年龄判定 | Survivor区相同年龄对象超过一半 | 自适应调整 | + +### 栈内存结构 + +```mermaid +graph TD + subgraph "Java虚拟机栈" + A[栈帧1 - 当前方法] + B[栈帧2] + C[栈帧3] + D[栈帧N] + end + + subgraph "栈帧结构" + A1[局部变量表] + A2[操作数栈] + A3[动态链接] + A4[方法返回地址] + end + + A --> A1 + A --> A2 + A --> A3 + A --> A4 +``` + +## 垃圾回收机制 + +### 垃圾回收算法 + +#### 1. 标记-清除算法 + +```mermaid +graph LR + A["标记阶段
标记需要回收的对象"] --> B["清除阶段
回收标记的对象"] + + C["优点:简单直接"] + D["缺点:产生内存碎片"] +``` + +#### 2. 复制算法 + +```mermaid +graph TB + subgraph "复制前" + A1["From区
存活对象 + 垃圾对象"] + A2["To区
空闲"] + end + + subgraph "复制后" + B1["From区
空闲"] + B2["To区
存活对象"] + end + + A1 --> |"复制存活对象"| B2 +``` + +#### 3. 标记-整理算法 + +```mermaid +graph LR + A["标记阶段
标记存活对象"] --> B["整理阶段
移动存活对象"] + B --> C["清除阶段
清理剩余空间"] +``` + +### 垃圾收集器对比 + +| 收集器 | 类型 | 适用场景 | 特点 | +|--------|------|----------|------| +| Serial | 单线程 | 客户端应用 | 简单高效 | +| Parallel | 多线程 | 服务器应用 | 吞吐量优先 | +| CMS | 并发 | 响应时间敏感 | 低延迟 | +| G1 | 并发 | 大堆内存 | 可预测停顿 | +| ZGC | 并发 | 超大堆内存 | 超低延迟 | + +### GC调优参数 + +```bash +# 堆内存设置 +-Xms2g # 初始堆大小 +-Xmx4g # 最大堆大小 +-Xmn1g # 新生代大小 + +# 垃圾收集器选择 +-XX:+UseG1GC # 使用G1收集器 +-XX:+UseConcMarkSweepGC # 使用CMS收集器 + +# GC日志 +-XX:+PrintGC +-XX:+PrintGCDetails +-XX:+PrintGCTimeStamps +``` + +## JVM性能监控 + +### 监控工具 + +#### 1. 命令行工具 + +```bash +# jps - 查看Java进程 +jps -l + +# jstat - 查看GC统计信息 +jstat -gc 1000 + +# jmap - 查看内存使用情况 +jmap -heap + +# jstack - 查看线程堆栈 +jstack +``` + +#### 2. 可视化工具 + +| 工具 | 功能 | 特点 | +|------|------|------| +| JConsole | 基础监控 | JDK自带 | +| VisualVM | 全面分析 | 功能丰富 | +| JProfiler | 专业分析 | 商业工具 | +| Arthas | 在线诊断 | 阿里开源 | + +### 性能分析指标 + +```mermaid +graph TD + A[JVM性能指标] --> B[内存使用率] + A --> C[GC频率和时间] + A --> D[线程状态] + A --> E[CPU使用率] + + B --> B1[堆内存使用] + B --> B2[非堆内存使用] + + C --> C1[Minor GC] + C --> C2[Major GC] + C --> C3[Full GC] + + D --> D1[运行线程数] + D --> D2[阻塞线程数] + D --> D3[死锁检测] +``` + +## JVM调优实践 + +### 调优流程 + +```mermaid +graph TD + A[性能基线测试] --> B[问题识别] + B --> C[参数调整] + C --> D[效果验证] + D --> E{是否达到目标} + E -->|否| B + E -->|是| F[部署上线] +``` + +### 常见调优场景 + +#### 1. 内存溢出优化 + +```java +// OutOfMemoryError: Java heap space +// 解决方案: +// 1. 增加堆内存 -Xmx4g +// 2. 优化代码,减少内存使用 +// 3. 分析内存泄漏 + +// 示例:内存泄漏检测 +public class MemoryLeakExample { + private static List list = new ArrayList<>(); + + public void addObject() { + // 潜在内存泄漏:对象一直被引用 + list.add(new Object()); + } +} +``` + +#### 2. GC停顿时间优化 + +```bash +# 使用G1收集器减少停顿时间 +-XX:+UseG1GC +-XX:MaxGCPauseMillis=200 +-XX:G1HeapRegionSize=16m + +# 并行GC线程数调整 +-XX:ParallelGCThreads=8 +``` + +#### 3. 吞吐量优化 + +```bash +# 使用Parallel收集器提高吞吐量 +-XX:+UseParallelGC +-XX:+UseParallelOldGC +-XX:ParallelGCThreads=8 + +# 调整新生代比例 +-XX:NewRatio=3 +-XX:SurvivorRatio=8 +``` + +### 调优最佳实践 + +1. **监控先行**:建立完善的监控体系 +2. **渐进调优**:逐步调整,避免大幅改动 +3. **压力测试**:在测试环境验证效果 +4. **文档记录**:记录调优过程和结果 + +## JVM故障排查 + +### 常见问题诊断 + +#### 1. 内存问题 + +```bash +# 生成堆转储文件 +jmap -dump:format=b,file=heap.hprof + +# 分析堆转储文件 +jhat heap.hprof +# 或使用Eclipse MAT工具 +``` + +#### 2. CPU问题 + +```bash +# 查看线程CPU使用情况 +top -H -p + +# 获取线程堆栈 +jstack > thread.dump + +# 分析热点方法 +jprofiler或其他性能分析工具 +``` + +#### 3. 死锁问题 + +```java +// 死锁示例 +public class DeadlockExample { + private static Object lock1 = new Object(); + private static Object lock2 = new Object(); + + public static void main(String[] args) { + Thread t1 = new Thread(() -> { + synchronized (lock1) { + synchronized (lock2) { + System.out.println("Thread 1"); + } + } + }); + + Thread t2 = new Thread(() -> { + synchronized (lock2) { + synchronized (lock1) { + System.out.println("Thread 2"); + } + } + }); + + t1.start(); + t2.start(); + } +} +``` + +```bash +# 检测死锁 +jstack | grep -A 5 "Found Java-level deadlock" +``` + +## 总结 + +JVM是Java生态系统的核心,深入理解JVM的工作原理对于Java开发者来说至关重要。通过掌握: + +- **内存管理机制**:了解内存分配和回收策略 +- **垃圾回收原理**:选择合适的GC算法和收集器 +- **性能监控方法**:使用工具进行性能分析 +- **调优实践经验**:根据应用特点进行针对性优化 + +可以有效提升Java应用的性能和稳定性。 \ No newline at end of file diff --git "a/docs/e/Java\345\256\236\346\210\230\346\274\224\347\244\272.md" "b/docs/e/Java\345\256\236\346\210\230\346\274\224\347\244\272.md" new file mode 100644 index 000000000..784d6514d --- /dev/null +++ "b/docs/e/Java\345\256\236\346\210\230\346\274\224\347\244\272.md" @@ -0,0 +1,1127 @@ +--- +title: Java实战演示 +date: 2024-01-15 +categories: + - Java + - 实战 +tags: + - Java + - 示例 + - 实践 + - 项目 +--- + +# Java实战演示 + +## 项目结构设计 + +### 标准Java项目结构 + +``` +java-demo-project/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/ +│ │ │ └── example/ +│ │ │ ├── model/ +│ │ │ ├── service/ +│ │ │ ├── util/ +│ │ │ └── Main.java +│ │ └── resources/ +│ │ ├── config.properties +│ │ └── log4j2.xml +│ └── test/ +│ └── java/ +│ └── com/ +│ └── example/ +│ └── service/ +├── lib/ +├── docs/ +├── build.sh +└── README.md +``` + +## 基础示例:学生管理系统 + +### 1. 数据模型设计 + +```java +// Student.java - 学生实体类 +package com.example.model; + +import java.time.LocalDate; +import java.util.Objects; + +/** + * 学生实体类 + * 演示Java基础语法:类、封装、构造方法等 + */ +public class Student { + private String id; + private String name; + private int age; + private String major; + private LocalDate enrollmentDate; + private double gpa; + + // 默认构造方法 + public Student() { + } + + // 带参数的构造方法 + public Student(String id, String name, int age, String major) { + this.id = id; + this.name = name; + this.age = age; + this.major = major; + this.enrollmentDate = LocalDate.now(); + this.gpa = 0.0; + } + + // 完整构造方法 + public Student(String id, String name, int age, String major, + LocalDate enrollmentDate, double gpa) { + this.id = id; + this.name = name; + this.age = age; + this.major = major; + this.enrollmentDate = enrollmentDate; + this.gpa = gpa; + } + + // Getter和Setter方法 + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public int getAge() { return age; } + public void setAge(int age) { + if (age < 0 || age > 150) { + throw new IllegalArgumentException("年龄必须在0-150之间"); + } + this.age = age; + } + + public String getMajor() { return major; } + public void setMajor(String major) { this.major = major; } + + public LocalDate getEnrollmentDate() { return enrollmentDate; } + public void setEnrollmentDate(LocalDate enrollmentDate) { + this.enrollmentDate = enrollmentDate; + } + + public double getGpa() { return gpa; } + public void setGpa(double gpa) { + if (gpa < 0.0 || gpa > 4.0) { + throw new IllegalArgumentException("GPA必须在0.0-4.0之间"); + } + this.gpa = gpa; + } + + // 业务方法 + public boolean isHonorStudent() { + return gpa >= 3.5; + } + + public int getStudyYears() { + return LocalDate.now().getYear() - enrollmentDate.getYear(); + } + + // equals和hashCode方法 + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Student student = (Student) obj; + return Objects.equals(id, student.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + // toString方法 + @Override + public String toString() { + return String.format("Student{id='%s', name='%s', age=%d, major='%s', gpa=%.2f}", + id, name, age, major, gpa); + } +} +``` + +### 2. 服务层设计 + +```java +// StudentService.java - 学生服务类 +package com.example.service; + +import com.example.model.Student; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 学生服务类 + * 演示集合框架、Stream API、异常处理等 + */ +public class StudentService { + private Map students; + + public StudentService() { + this.students = new HashMap<>(); + initializeTestData(); + } + + /** + * 初始化测试数据 + */ + private void initializeTestData() { + addStudent(new Student("S001", "张三", 20, "计算机科学")); + addStudent(new Student("S002", "李四", 21, "软件工程")); + addStudent(new Student("S003", "王五", 19, "数据科学")); + + // 设置GPA + students.get("S001").setGpa(3.8); + students.get("S002").setGpa(3.2); + students.get("S003").setGpa(3.9); + } + + /** + * 添加学生 + */ + public boolean addStudent(Student student) { + if (student == null || student.getId() == null) { + throw new IllegalArgumentException("学生信息不能为空"); + } + + if (students.containsKey(student.getId())) { + return false; // 学生已存在 + } + + students.put(student.getId(), student); + return true; + } + + /** + * 根据ID查找学生 + */ + public Optional findStudentById(String id) { + return Optional.ofNullable(students.get(id)); + } + + /** + * 根据姓名查找学生 + */ + public List findStudentsByName(String name) { + return students.values().stream() + .filter(student -> student.getName().contains(name)) + .collect(Collectors.toList()); + } + + /** + * 根据专业查找学生 + */ + public List findStudentsByMajor(String major) { + return students.values().stream() + .filter(student -> student.getMajor().equals(major)) + .collect(Collectors.toList()); + } + + /** + * 获取所有学生 + */ + public List getAllStudents() { + return new ArrayList<>(students.values()); + } + + /** + * 获取优秀学生(GPA >= 3.5) + */ + public List getHonorStudents() { + return students.values().stream() + .filter(Student::isHonorStudent) + .sorted((s1, s2) -> Double.compare(s2.getGpa(), s1.getGpa())) + .collect(Collectors.toList()); + } + + /** + * 更新学生信息 + */ + public boolean updateStudent(Student student) { + if (student == null || student.getId() == null) { + return false; + } + + if (!students.containsKey(student.getId())) { + return false; + } + + students.put(student.getId(), student); + return true; + } + + /** + * 删除学生 + */ + public boolean deleteStudent(String id) { + return students.remove(id) != null; + } + + /** + * 获取统计信息 + */ + public StudentStatistics getStatistics() { + List allStudents = getAllStudents(); + + if (allStudents.isEmpty()) { + return new StudentStatistics(0, 0.0, 0.0, 0.0, new HashMap<>()); + } + + double avgGpa = allStudents.stream() + .mapToDouble(Student::getGpa) + .average() + .orElse(0.0); + + double maxGpa = allStudents.stream() + .mapToDouble(Student::getGpa) + .max() + .orElse(0.0); + + double minGpa = allStudents.stream() + .mapToDouble(Student::getGpa) + .min() + .orElse(0.0); + + Map majorCount = allStudents.stream() + .collect(Collectors.groupingBy( + Student::getMajor, + Collectors.counting() + )); + + return new StudentStatistics( + allStudents.size(), + avgGpa, + maxGpa, + minGpa, + majorCount + ); + } + + /** + * 统计信息内部类 + */ + public static class StudentStatistics { + private final int totalStudents; + private final double averageGpa; + private final double maxGpa; + private final double minGpa; + private final Map majorDistribution; + + public StudentStatistics(int totalStudents, double averageGpa, + double maxGpa, double minGpa, + Map majorDistribution) { + this.totalStudents = totalStudents; + this.averageGpa = averageGpa; + this.maxGpa = maxGpa; + this.minGpa = minGpa; + this.majorDistribution = majorDistribution; + } + + // Getter方法 + public int getTotalStudents() { return totalStudents; } + public double getAverageGpa() { return averageGpa; } + public double getMaxGpa() { return maxGpa; } + public double getMinGpa() { return minGpa; } + public Map getMajorDistribution() { return majorDistribution; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("学生统计信息:\n"); + sb.append(String.format("总学生数: %d\n", totalStudents)); + sb.append(String.format("平均GPA: %.2f\n", averageGpa)); + sb.append(String.format("最高GPA: %.2f\n", maxGpa)); + sb.append(String.format("最低GPA: %.2f\n", minGpa)); + sb.append("专业分布:\n"); + majorDistribution.forEach((major, count) -> + sb.append(String.format(" %s: %d人\n", major, count))); + return sb.toString(); + } + } +} +``` + +### 3. 工具类设计 + +```java +// ValidationUtil.java - 验证工具类 +package com.example.util; + +import java.util.regex.Pattern; + +/** + * 验证工具类 + * 演示静态方法、正则表达式等 + */ +public class ValidationUtil { + + // 学号格式:S + 3位数字 + private static final Pattern STUDENT_ID_PATTERN = Pattern.compile("^S\\d{3}$"); + + // 姓名格式:2-10个中文字符或英文字母 + private static final Pattern NAME_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5a-zA-Z]{2,10}$"); + + /** + * 验证学号格式 + */ + public static boolean isValidStudentId(String id) { + return id != null && STUDENT_ID_PATTERN.matcher(id).matches(); + } + + /** + * 验证姓名格式 + */ + public static boolean isValidName(String name) { + return name != null && NAME_PATTERN.matcher(name).matches(); + } + + /** + * 验证年龄范围 + */ + public static boolean isValidAge(int age) { + return age >= 16 && age <= 35; + } + + /** + * 验证GPA范围 + */ + public static boolean isValidGpa(double gpa) { + return gpa >= 0.0 && gpa <= 4.0; + } + + /** + * 验证专业名称 + */ + public static boolean isValidMajor(String major) { + return major != null && !major.trim().isEmpty() && major.length() <= 50; + } +} +``` + +```java +// FileUtil.java - 文件操作工具类 +package com.example.util; + +import com.example.model.Student; +import java.io.*; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * 文件操作工具类 + * 演示文件I/O、异常处理等 + */ +public class FileUtil { + + private static final String CSV_HEADER = "ID,姓名,年龄,专业,入学日期,GPA"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * 将学生列表导出到CSV文件 + */ + public static void exportToCSV(List students, String filename) throws IOException { + try (PrintWriter writer = new PrintWriter(new FileWriter(filename, false))) { + writer.println(CSV_HEADER); + + for (Student student : students) { + writer.printf("%s,%s,%d,%s,%s,%.2f%n", + student.getId(), + student.getName(), + student.getAge(), + student.getMajor(), + student.getEnrollmentDate().format(DATE_FORMATTER), + student.getGpa()); + } + } + } + + /** + * 从CSV文件导入学生列表 + */ + public static List importFromCSV(String filename) throws IOException { + List students = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(filename))) { + String line = reader.readLine(); // 跳过标题行 + + while ((line = reader.readLine()) != null) { + String[] parts = line.split(","); + if (parts.length == 6) { + try { + Student student = new Student( + parts[0].trim(), + parts[1].trim(), + Integer.parseInt(parts[2].trim()), + parts[3].trim(), + LocalDate.parse(parts[4].trim(), DATE_FORMATTER), + Double.parseDouble(parts[5].trim()) + ); + students.add(student); + } catch (Exception e) { + System.err.println("解析行数据失败: " + line + ", 错误: " + e.getMessage()); + } + } + } + } + + return students; + } + + /** + * 将对象序列化到文件 + */ + public static void serializeToFile(Object obj, String filename) throws IOException { + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) { + oos.writeObject(obj); + } + } + + /** + * 从文件反序列化对象 + */ + @SuppressWarnings("unchecked") + public static T deserializeFromFile(String filename, Class clazz) + throws IOException, ClassNotFoundException { + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) { + return (T) ois.readObject(); + } + } +} +``` + +### 4. 主程序设计 + +```java +// Main.java - 主程序 +package com.example; + +import com.example.model.Student; +import com.example.service.StudentService; +import com.example.util.FileUtil; +import com.example.util.ValidationUtil; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; + +/** + * 学生管理系统主程序 + * 演示控制台交互、异常处理、完整业务流程 + */ +public class Main { + + private static StudentService studentService = new StudentService(); + private static Scanner scanner = new Scanner(System.in); + + public static void main(String[] args) { + System.out.println("=== 欢迎使用学生管理系统 ==="); + + while (true) { + showMenu(); + int choice = getChoice(); + + try { + switch (choice) { + case 1: + addStudent(); + break; + case 2: + findStudent(); + break; + case 3: + listAllStudents(); + break; + case 4: + listHonorStudents(); + break; + case 5: + updateStudent(); + break; + case 6: + deleteStudent(); + break; + case 7: + showStatistics(); + break; + case 8: + exportData(); + break; + case 9: + importData(); + break; + case 0: + System.out.println("感谢使用,再见!"); + return; + default: + System.out.println("无效选择,请重新输入!"); + } + } catch (Exception e) { + System.err.println("操作失败: " + e.getMessage()); + } + + System.out.println("\n按回车键继续..."); + scanner.nextLine(); + } + } + + private static void showMenu() { + System.out.println("\n=== 主菜单 ==="); + System.out.println("1. 添加学生"); + System.out.println("2. 查找学生"); + System.out.println("3. 显示所有学生"); + System.out.println("4. 显示优秀学生"); + System.out.println("5. 更新学生信息"); + System.out.println("6. 删除学生"); + System.out.println("7. 统计信息"); + System.out.println("8. 导出数据"); + System.out.println("9. 导入数据"); + System.out.println("0. 退出系统"); + System.out.print("请选择操作: "); + } + + private static int getChoice() { + try { + return Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + return -1; + } + } + + private static void addStudent() { + System.out.println("\n=== 添加学生 ==="); + + System.out.print("学号 (格式: S001): "); + String id = scanner.nextLine().trim(); + if (!ValidationUtil.isValidStudentId(id)) { + System.out.println("学号格式错误!"); + return; + } + + System.out.print("姓名: "); + String name = scanner.nextLine().trim(); + if (!ValidationUtil.isValidName(name)) { + System.out.println("姓名格式错误!"); + return; + } + + System.out.print("年龄: "); + int age; + try { + age = Integer.parseInt(scanner.nextLine().trim()); + if (!ValidationUtil.isValidAge(age)) { + System.out.println("年龄必须在16-35之间!"); + return; + } + } catch (NumberFormatException e) { + System.out.println("年龄格式错误!"); + return; + } + + System.out.print("专业: "); + String major = scanner.nextLine().trim(); + if (!ValidationUtil.isValidMajor(major)) { + System.out.println("专业名称无效!"); + return; + } + + Student student = new Student(id, name, age, major); + + if (studentService.addStudent(student)) { + System.out.println("学生添加成功!"); + } else { + System.out.println("学生已存在!"); + } + } + + private static void findStudent() { + System.out.println("\n=== 查找学生 ==="); + System.out.println("1. 按学号查找"); + System.out.println("2. 按姓名查找"); + System.out.println("3. 按专业查找"); + System.out.print("请选择查找方式: "); + + int choice = getChoice(); + switch (choice) { + case 1: + System.out.print("请输入学号: "); + String id = scanner.nextLine().trim(); + Optional student = studentService.findStudentById(id); + if (student.isPresent()) { + System.out.println("找到学生: " + student.get()); + } else { + System.out.println("未找到该学生!"); + } + break; + case 2: + System.out.print("请输入姓名: "); + String name = scanner.nextLine().trim(); + List studentsByName = studentService.findStudentsByName(name); + displayStudents(studentsByName); + break; + case 3: + System.out.print("请输入专业: "); + String major = scanner.nextLine().trim(); + List studentsByMajor = studentService.findStudentsByMajor(major); + displayStudents(studentsByMajor); + break; + default: + System.out.println("无效选择!"); + } + } + + private static void listAllStudents() { + System.out.println("\n=== 所有学生 ==="); + List students = studentService.getAllStudents(); + displayStudents(students); + } + + private static void listHonorStudents() { + System.out.println("\n=== 优秀学生 (GPA >= 3.5) ==="); + List honorStudents = studentService.getHonorStudents(); + displayStudents(honorStudents); + } + + private static void updateStudent() { + System.out.println("\n=== 更新学生信息 ==="); + System.out.print("请输入要更新的学生学号: "); + String id = scanner.nextLine().trim(); + + Optional optionalStudent = studentService.findStudentById(id); + if (!optionalStudent.isPresent()) { + System.out.println("未找到该学生!"); + return; + } + + Student student = optionalStudent.get(); + System.out.println("当前学生信息: " + student); + + System.out.print("新的GPA (当前: " + student.getGpa() + "): "); + String gpaStr = scanner.nextLine().trim(); + if (!gpaStr.isEmpty()) { + try { + double gpa = Double.parseDouble(gpaStr); + if (ValidationUtil.isValidGpa(gpa)) { + student.setGpa(gpa); + if (studentService.updateStudent(student)) { + System.out.println("学生信息更新成功!"); + } else { + System.out.println("更新失败!"); + } + } else { + System.out.println("GPA必须在0.0-4.0之间!"); + } + } catch (NumberFormatException e) { + System.out.println("GPA格式错误!"); + } + } + } + + private static void deleteStudent() { + System.out.println("\n=== 删除学生 ==="); + System.out.print("请输入要删除的学生学号: "); + String id = scanner.nextLine().trim(); + + if (studentService.deleteStudent(id)) { + System.out.println("学生删除成功!"); + } else { + System.out.println("未找到该学生!"); + } + } + + private static void showStatistics() { + System.out.println("\n=== 统计信息 ==="); + StudentService.StudentStatistics stats = studentService.getStatistics(); + System.out.println(stats); + } + + private static void exportData() { + System.out.println("\n=== 导出数据 ==="); + System.out.print("请输入导出文件名 (例: students.csv): "); + String filename = scanner.nextLine().trim(); + + try { + List students = studentService.getAllStudents(); + FileUtil.exportToCSV(students, filename); + System.out.println("数据导出成功!文件: " + filename); + } catch (Exception e) { + System.err.println("导出失败: " + e.getMessage()); + } + } + + private static void importData() { + System.out.println("\n=== 导入数据 ==="); + System.out.print("请输入导入文件名: "); + String filename = scanner.nextLine().trim(); + + try { + List students = FileUtil.importFromCSV(filename); + int successCount = 0; + for (Student student : students) { + if (studentService.addStudent(student)) { + successCount++; + } + } + System.out.println("导入完成!成功导入 " + successCount + " 个学生记录。"); + } catch (Exception e) { + System.err.println("导入失败: " + e.getMessage()); + } + } + + private static void displayStudents(List students) { + if (students.isEmpty()) { + System.out.println("没有找到学生记录。"); + return; + } + + System.out.println("\n学生列表:"); + System.out.println("学号\t姓名\t年龄\t专业\t\tGPA\t优秀学生"); + System.out.println("================================================"); + + for (Student student : students) { + System.out.printf("%s\t%s\t%d\t%-10s\t%.2f\t%s%n", + student.getId(), + student.getName(), + student.getAge(), + student.getMajor(), + student.getGpa(), + student.isHonorStudent() ? "是" : "否"); + } + + System.out.println("总计: " + students.size() + " 个学生"); + } +} +``` + +## 编译和运行演示 + +### 1. 编译项目 + +```bash +# 创建输出目录 +mkdir -p classes + +# 编译所有Java文件 +javac -d classes -sourcepath src/main/java src/main/java/com/example/*.java src/main/java/com/example/*/*.java + +# 或者使用通配符 +find src/main/java -name "*.java" | xargs javac -d classes +``` + +### 2. 运行程序 + +```bash +# 运行主程序 +java -cp classes com.example.Main + +# 或者创建JAR文件后运行 +jar cfe student-management.jar com.example.Main -C classes . +java -jar student-management.jar +``` + +### 3. 构建脚本 + +```bash +#!/bin/bash +# build.sh + +echo "清理输出目录..." +rm -rf classes docs +mkdir -p classes docs + +echo "编译源代码..." +find src/main/java -name "*.java" | xargs javac -d classes + +if [ $? -eq 0 ]; then + echo "编译成功!" + + echo "生成文档..." + javadoc -d docs -sourcepath src/main/java -subpackages com.example + + echo "创建JAR文件..." + jar cfe student-management.jar com.example.Main -C classes . + + echo "构建完成!" + echo "运行程序: java -jar student-management.jar" +else + echo "编译失败!" + exit 1 +fi +``` + +## 高级特性演示 + +### 1. 多线程处理 + +```java +// ConcurrentStudentService.java +package com.example.service; + +import com.example.model.Student; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * 线程安全的学生服务 + * 演示并发编程、锁机制 + */ +public class ConcurrentStudentService { + private final ConcurrentHashMap students; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ConcurrentStudentService() { + this.students = new ConcurrentHashMap<>(); + } + + public boolean addStudent(Student student) { + lock.writeLock().lock(); + try { + if (students.containsKey(student.getId())) { + return false; + } + students.put(student.getId(), student); + return true; + } finally { + lock.writeLock().unlock(); + } + } + + public Student findStudentById(String id) { + lock.readLock().lock(); + try { + return students.get(id); + } finally { + lock.readLock().unlock(); + } + } +} +``` + +### 2. 泛型和反射 + +```java +// GenericDAO.java +package com.example.dao; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * 泛型数据访问对象 + * 演示泛型、反射机制 + */ +public class GenericDAO { + private final Class entityClass; + private final Map storage; + + public GenericDAO(Class entityClass) { + this.entityClass = entityClass; + this.storage = new HashMap<>(); + } + + public void save(T entity) { + String id = getId(entity); + if (id != null) { + storage.put(id, entity); + } + } + + public T findById(String id) { + return storage.get(id); + } + + public List findAll() { + return new ArrayList<>(storage.values()); + } + + private String getId(T entity) { + try { + Field idField = entityClass.getDeclaredField("id"); + idField.setAccessible(true); + return (String) idField.get(entity); + } catch (Exception e) { + return null; + } + } +} +``` + +### 3. Lambda表达式和Stream API + +```java +// StudentAnalyzer.java +package com.example.analyzer; + +import com.example.model.Student; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 学生数据分析器 + * 演示Lambda表达式、Stream API、函数式编程 + */ +public class StudentAnalyzer { + + /** + * 按GPA分组 + */ + public static Map> groupByGpaLevel(List students) { + return students.stream() + .collect(Collectors.groupingBy(student -> { + double gpa = student.getGpa(); + if (gpa >= 3.7) return "优秀"; + else if (gpa >= 3.0) return "良好"; + else if (gpa >= 2.0) return "及格"; + else return "不及格"; + })); + } + + /** + * 查找最高GPA的学生 + */ + public static Optional findTopStudent(List students) { + return students.stream() + .max(Comparator.comparing(Student::getGpa)); + } + + /** + * 计算各专业平均GPA + */ + public static Map calculateAverageGpaByMajor(List students) { + return students.stream() + .collect(Collectors.groupingBy( + Student::getMajor, + Collectors.averagingDouble(Student::getGpa) + )); + } + + /** + * 筛选并排序 + */ + public static List filterAndSort(List students, + double minGpa, + String major) { + return students.stream() + .filter(s -> s.getGpa() >= minGpa) + .filter(s -> major == null || s.getMajor().equals(major)) + .sorted(Comparator.comparing(Student::getGpa).reversed()) + .collect(Collectors.toList()); + } +} +``` + +## 测试和调试 + +### 1. 单元测试示例 + +```java +// StudentServiceTest.java +package com.example.service; + +import com.example.model.Student; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * 学生服务测试类 + * 演示单元测试、断言 + */ +class StudentServiceTest { + + private StudentService studentService; + + @BeforeEach + void setUp() { + studentService = new StudentService(); + } + + @Test + void testAddStudent() { + Student student = new Student("S999", "测试学生", 20, "测试专业"); + assertTrue(studentService.addStudent(student)); + assertFalse(studentService.addStudent(student)); // 重复添加 + } + + @Test + void testFindStudentById() { + Student student = new Student("S999", "测试学生", 20, "测试专业"); + studentService.addStudent(student); + + assertTrue(studentService.findStudentById("S999").isPresent()); + assertFalse(studentService.findStudentById("S000").isPresent()); + } + + @Test + void testGetHonorStudents() { + Student student1 = new Student("S997", "优秀学生", 20, "计算机"); + student1.setGpa(3.8); + + Student student2 = new Student("S998", "普通学生", 21, "计算机"); + student2.setGpa(2.5); + + studentService.addStudent(student1); + studentService.addStudent(student2); + + assertEquals(1, studentService.getHonorStudents().size()); + } +} +``` + +### 2. 调试技巧 + +```java +// 使用断言进行调试 +assert student != null : "学生对象不能为空"; +assert student.getGpa() >= 0.0 && student.getGpa() <= 4.0 : "GPA超出有效范围"; + +// 使用日志记录 +import java.util.logging.Logger; +import java.util.logging.Level; + +private static final Logger logger = Logger.getLogger(StudentService.class.getName()); + +public boolean addStudent(Student student) { + logger.info("尝试添加学生: " + student.getId()); + + if (students.containsKey(student.getId())) { + logger.warning("学生已存在: " + student.getId()); + return false; + } + + students.put(student.getId(), student); + logger.info("学生添加成功: " + student.getId()); + return true; +} +``` + +## 总结 + +这个Java实战演示项目涵盖了Java开发的核心概念和技术: + +1. **面向对象编程**:类、对象、封装、继承 +2. **集合框架**:List、Map、Set的使用 +3. **异常处理**:try-catch、自定义异常 +4. **文件I/O**:文件读写、序列化 +5. **多线程**:并发安全、锁机制 +6. **泛型和反射**:类型安全、动态操作 +7. **函数式编程**:Lambda、Stream API +8. **测试和调试**:单元测试、日志记录 + +通过这个完整的项目示例,可以深入理解Java语言的特性和最佳实践。 \ No newline at end of file diff --git a/docs/es/es-cluster-setup.md b/docs/es/es-cluster-setup.md new file mode 100644 index 000000000..fa7ca74a5 --- /dev/null +++ b/docs/es/es-cluster-setup.md @@ -0,0 +1,362 @@ +# Linux上搭建ElasticSearch-8.x集群以及安装Kibana + +## 前言 + +ElasticSearch是一个分布式、RESTful风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为Elastic Stack的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 + +本文将详细介绍如何在Linux环境下搭建ElasticSearch 8.x版本的集群,并安装配置Kibana进行可视化管理。 + +## 环境准备 + +### 系统要求 + +- Linux操作系统(CentOS 7/8、Ubuntu 18.04/20.04等) +- JDK 17或更高版本(ElasticSearch 8.x要求) +- 至少2GB RAM(生产环境建议8GB以上) +- 足够的磁盘空间 + +### 服务器配置 + +本教程使用3台服务器搭建集群,配置如下: + +| 服务器IP | 主机名 | 角色 | +| --- | --- | --- | +| 192.168.1.101 | es-master | 主节点 | +| 192.168.1.102 | es-data1 | 数据节点 | +| 192.168.1.103 | es-data2 | 数据节点 | + +## 安装ElasticSearch + +### 1. 下载ElasticSearch + +在所有节点上执行以下命令,下载ElasticSearch 8.x版本: + +```bash +wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.0-linux-x86_64.tar.gz +wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.0-linux-x86_64.tar.gz.sha512 +shasum -a 512 -c elasticsearch-8.9.0-linux-x86_64.tar.gz.sha512 +tar -xzf elasticsearch-8.9.0-linux-x86_64.tar.gz +cd elasticsearch-8.9.0/ +``` + +### 2. 创建ElasticSearch用户 + +ElasticSearch不能以root用户运行,需要创建专门的用户: + +```bash +sudo useradd elastic +sudo passwd elastic +sudo chown -R elastic:elastic elasticsearch-8.9.0 +``` + +### 3. 修改系统配置 + +#### 增加文件描述符限制 + +```bash +sudo vim /etc/security/limits.conf +``` + +添加以下内容: + +``` +elastic soft nofile 65535 +elastic hard nofile 65535 +``` + +#### 修改虚拟内存配置 + +```bash +sudo vim /etc/sysctl.conf +``` + +添加以下内容: + +``` +vm.max_map_count=262144 +``` + +应用配置: + +```bash +sudo sysctl -p +``` + +### 4. 配置ElasticSearch + +#### 主节点配置 (192.168.1.101) + +```bash +cd elasticsearch-8.9.0/ +vi config/elasticsearch.yml +``` + +配置内容: + +```yaml +# 集群名称 +cluster.name: es-cluster + +# 节点名称 +node.name: es-master + +# 是否可以成为主节点 +node.master: true + +# 是否存储数据 +node.data: false + +# 网络设置 +network.host: 192.168.1.101 +http.port: 9200 +transport.port: 9300 + +# 集群发现设置 +discovery.seed_hosts: ["192.168.1.101:9300", "192.168.1.102:9300", "192.168.1.103:9300"] +cluster.initial_master_nodes: ["es-master"] + +# 跨域设置 +http.cors.enabled: true +http.cors.allow-origin: "*" + +# 禁用安全设置(仅用于测试环境,生产环境请配置适当的安全措施) +xpack.security.enabled: false +xpack.security.enrollment.enabled: false +xpack.security.http.ssl.enabled: false +xpack.security.transport.ssl.enabled: false +``` + +#### 数据节点1配置 (192.168.1.102) + +```yaml +# 集群名称 +cluster.name: es-cluster + +# 节点名称 +node.name: es-data1 + +# 是否可以成为主节点 +node.master: false + +# 是否存储数据 +node.data: true + +# 网络设置 +network.host: 192.168.1.102 +http.port: 9200 +transport.port: 9300 + +# 集群发现设置 +discovery.seed_hosts: ["192.168.1.101:9300", "192.168.1.102:9300", "192.168.1.103:9300"] +cluster.initial_master_nodes: ["es-master"] + +# 跨域设置 +http.cors.enabled: true +http.cors.allow-origin: "*" + +# 禁用安全设置(仅用于测试环境,生产环境请配置适当的安全措施) +xpack.security.enabled: false +xpack.security.enrollment.enabled: false +xpack.security.http.ssl.enabled: false +xpack.security.transport.ssl.enabled: false +``` + +#### 数据节点2配置 (192.168.1.103) + +```yaml +# 集群名称 +cluster.name: es-cluster + +# 节点名称 +node.name: es-data2 + +# 是否可以成为主节点 +node.master: false + +# 是否存储数据 +node.data: true + +# 网络设置 +network.host: 192.168.1.103 +http.port: 9200 +transport.port: 9300 + +# 集群发现设置 +discovery.seed_hosts: ["192.168.1.101:9300", "192.168.1.102:9300", "192.168.1.103:9300"] +cluster.initial_master_nodes: ["es-master"] + +# 跨域设置 +http.cors.enabled: true +http.cors.allow-origin: "*" + +# 禁用安全设置(仅用于测试环境,生产环境请配置适当的安全措施) +xpack.security.enabled: false +xpack.security.enrollment.enabled: false +xpack.security.http.ssl.enabled: false +xpack.security.transport.ssl.enabled: false +``` + +### 5. 启动ElasticSearch + +在所有节点上执行以下命令启动ElasticSearch: + +```bash +su - elastic +cd elasticsearch-8.9.0/ +./bin/elasticsearch -d +``` + +### 6. 验证集群状态 + +```bash +curl -X GET "http://192.168.1.101:9200/_cluster/health?pretty" +``` + +正常情况下,应该看到类似以下输出: + +```json +{ + "cluster_name" : "es-cluster", + "status" : "green", + "timed_out" : false, + "number_of_nodes" : 3, + "number_of_data_nodes" : 2, + "active_primary_shards" : 0, + "active_shards" : 0, + "relocating_shards" : 0, + "initializing_shards" : 0, + "unassigned_shards" : 0, + "delayed_unassigned_shards" : 0, + "number_of_pending_tasks" : 0, + "number_of_in_flight_fetch" : 0, + "task_max_waiting_in_queue_millis" : 0, + "active_shards_percent_as_number" : 100.0 +} +``` + +## 安装Kibana + +### 1. 下载Kibana + +在主节点(192.168.1.101)上执行以下命令: + +```bash +wget https://artifacts.elastic.co/downloads/kibana/kibana-8.9.0-linux-x86_64.tar.gz +wget https://artifacts.elastic.co/downloads/kibana/kibana-8.9.0-linux-x86_64.tar.gz.sha512 +shasum -a 512 -c kibana-8.9.0-linux-x86_64.tar.gz.sha512 +tar -xzf kibana-8.9.0-linux-x86_64.tar.gz +cd kibana-8.9.0/ +``` + +### 2. 配置Kibana + +```bash +vi config/kibana.yml +``` + +配置内容: + +```yaml +# 服务器主机名 +server.host: "192.168.1.101" + +# Kibana服务端口 +server.port: 5601 + +# ElasticSearch连接设置 +elasticsearch.hosts: ["http://192.168.1.101:9200"] + +# 禁用安全设置(仅用于测试环境,生产环境请配置适当的安全措施) +elasticsearch.ssl.verificationMode: none +xpack.security.enabled: false +``` + +### 3. 启动Kibana + +```bash +./bin/kibana & +``` + +### 4. 访问Kibana + +在浏览器中访问:`http://192.168.1.101:5601` + +## 安全配置(生产环境) + +对于生产环境,强烈建议启用安全功能。以下是基本的安全配置步骤: + +### 1. 启用X-Pack安全功能 + +修改所有节点的`elasticsearch.yml`: + +```yaml +xpack.security.enabled: true +xpack.security.enrollment.enabled: true +xpack.security.http.ssl.enabled: true +xpack.security.transport.ssl.enabled: true +``` + +### 2. 设置内置用户密码 + +```bash +./bin/elasticsearch-setup-passwords interactive +``` + +### 3. 配置Kibana连接安全的ElasticSearch + +```yaml +elasticsearch.username: "kibana_system" +elasticsearch.password: "your_password" +``` + +## 集群维护 + +### 添加新节点 + +1. 在新节点上安装ElasticSearch +2. 配置`elasticsearch.yml`,确保`cluster.name`与现有集群一致 +3. 设置`discovery.seed_hosts`包含现有集群节点 +4. 启动新节点,它将自动加入集群 + +### 集群扩容 + +随着数据量增长,可能需要扩展集群: + +1. 添加更多数据节点 +2. 调整分片数量和副本数量 +3. 考虑使用热-温-冷架构管理数据生命周期 + +## 常见问题排查 + +### 集群状态为黄色或红色 + +- 黄色:部分副本分片未分配 +- 红色:部分主分片未分配 + +解决方法: + +```bash +# 查看未分配分片的原因 +GET /_cluster/allocation/explain + +# 检查集群健康状态 +GET /_cluster/health?pretty + +# 检查节点状态 +GET /_cat/nodes?v +``` + +### JVM内存问题 + +调整`jvm.options`文件中的堆内存设置: + +``` +-Xms4g +-Xmx4g +``` + +## 结论 + +通过本教程,我们成功在Linux环境下搭建了ElasticSearch 8.x集群,并安装配置了Kibana进行可视化管理。ElasticSearch集群为大规模数据搜索和分析提供了强大的基础,而Kibana则提供了直观的界面来监控和管理集群。 + +在生产环境中,还需要考虑更多因素,如安全性、高可用性、备份策略等,以确保集群的稳定运行。 \ No newline at end of file diff --git a/docs/gaobingfaxitong/1.md b/docs/gaobingfaxitong/1.md new file mode 100644 index 000000000..95608c393 --- /dev/null +++ b/docs/gaobingfaxitong/1.md @@ -0,0 +1,42 @@ +--- +title: 它的通用设计方法是什么 +author: 哪吒 +date: '2023-06-15' +--- + +# 它的通用设计方法是什么 + +缓存:使用缓存来提高系统的性能,就好比用“拓宽河道”的方式抵抗高并发大流量的冲击。 + +异步:在某些场景下,未处理完成之前,我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。 + +Web 2.0 是缓存的时代,这一点毋庸置疑。缓存遍布在系统设计的每个角落,从操作系统到浏览器,从数据库到消息队列,任何略微复杂的服务和组件中,你都可以看到缓存的影子。我们使用缓存的主要作用是提升系统的访问性能,那么在高并发的场景下,就可以支撑更多用户的同时访问。 + +那么为什么缓存可以大幅度提升系统的性能呢?我们知道数据是放在持久化存储中的,一般的持久化存储都是使用磁盘作为存储介质的,而普通磁盘数据由机械手臂、磁头、转轴、盘片组成,盘片又分为磁道、柱面和扇区,盘片构造图我放在下面了。 + +盘片是存储介质,每个盘片被划分为多个同心圆,信息都被存储在同心圆之中,这些同心圆就是磁道。在磁盘工作时盘片是在高速旋转的,机械手臂驱动磁头沿着径向移动,在磁道上读取所需要的数据。我们把磁头寻找信息花费的时间叫做寻道时间。 + +普通磁盘的寻道时间是 10ms 左右,而相比于磁盘寻道花费的时间,CPU 执行指令和内存寻址的时间都在是 ns(纳秒)级别,从千兆网卡上读取数据的时间是在μs(微秒)级别。所以在整个计算机体系中,磁盘是最慢的一环,甚至比其它的组件要慢几个数量级。因此,我们通常使用以内存作为存储介质的缓存,以此提升性能。 + +当然,缓存的语义已经丰富了很多,我们可以将任何降低响应时间的中间存储都称为缓存。缓存的思想遍布很多设计领域,比如在操作系统中 CPU 有多级缓存,文件有 Page Cache 缓存,你应该有所了解。 + +## 异步处理 + +异步也是一种常见的高并发设计方法,我们在很多文章和演讲中都能听到这个名词,与之共同出现的还有它的反义词:同步。比如,分布式服务框架 Dubbo 中有同步方法调用和异步方法调用,IO 模型中有同步 IO 和异步 IO。 + +那么什么是同步,什么是异步呢?以方法调用为例,同步调用代表调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在高并发下会造成整体系统性能下降甚至发生雪崩。 + +异步调用恰恰相反,调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。 + +异步调用在大规模高并发系统中被大量使用,比如我们熟知的 12306 网站。当我们订票时,页面会显示系统正在排队,这个提示就代表着系统在异步处理我们的订票请求。在 12306 系统中查询余票、下单和更改余票状态都是比较耗时的操作,可能涉及多个内部系统的互相调用,如果是同步调用就会像 12306 刚刚上线时那样,高峰期永远不可能下单成功。 + +而采用异步的方式,后端处理时会把请求丢到消息队列中,同时快速响应用户,告诉用户我们正在排队处理,然后释放出资源来处理更多的请求。订票请求处理完之后,再通知用户订票成功或者失败。 + +处理逻辑后移到异步处理程序中,Web 服务的压力小了,资源占用的少了,自然就能接收更多的用户订票请求,系统承受高并发的能力也就提升了。 + +以淘宝为例,当时在业务从 0 到 1 的阶段是通过购买的方式快速搭建了系统。而后,随着流量的增长,淘宝做了一系列的技术改造来提升高并发处理能力,比如数据库存储引擎从 MyISAM 迁移到 InnoDB,数据库做分库分表,增加缓存,启动中间件研发等。 + +当这些都无法满足时就考虑对整体架构做大规模重构,比如说著名的“五彩石”项目让淘宝的架构从单体演进为服务化架构。正是通过逐步的技术演进,淘宝才进化出如今承担过亿 QPS 的技术架构。 + +归根结底一句话:高并发系统的演进应该是循序渐进,以解决系统中存在的问题为目的和驱动力的。 + diff --git a/docs/gaobingfaxitong/2.md b/docs/gaobingfaxitong/2.md new file mode 100644 index 000000000..5b15bd4e5 --- /dev/null +++ b/docs/gaobingfaxitong/2.md @@ -0,0 +1,64 @@ +--- +title: 架构分层 +author: 哪吒 +date: '2023-06-15' +--- + +# 架构分层 + +什么是分层架构 + +软件架构分层在软件工程中是一种常见的设计方式,它是将整体系统拆分成 N 个层次,每个层次有独立的职责,多个层次协同提供完整的功能。 + +我们在刚刚成为程序员的时候,会被“教育”说系统的设计要是“MVC”(Model-View-Controller)架构。它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。 + +另外一种常见的分层方式是将整体架构分为表现层、逻辑层和数据访问层: + +表现层,顾名思义嘛,就是展示数据结果和接受用户指令的,是最靠近用户的一层; + +逻辑层里面有复杂业务的具体实现; + +数据访问层则是主要处理和存储之间的交互。 + +这是在架构上最简单的一种分层方式。其实,我们在不经意间已经按照三层架构来做系统分层设计了,比如在构建项目的时候,我们通常会建立三个目录:Web、Service 和 Dao,它们分别对应了表现层、逻辑层还有数据访问层。 + +除此之外,如果我们稍加留意,就可以发现很多的分层的例子。比如我们在大学中学到的 OSI 网络模型,它把整个网络分了七层,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。 + +工作中经常能用到 TCP/IP 协议,它把网络简化成了四层,即链路层、网络层、传输层和应用层。每一层各司其职又互相帮助,网络层负责端到端的寻址和建立连接,传输层负责端到端的数据传输等,同时呢相邻两层还会有数据的交互。这样可以隔离关注点,让不同的层专注做不同的事情。 + +![img_1.png](img_1.png) + +Linux 文件系统也是分层设计的,从下图你可以清晰地看出文件系统的层次。在文件系统的最上层是虚拟文件系统(VFS),用来屏蔽不同的文件系统之间的差异,提供统一的系统调用接口。虚拟文件系统的下层是 Ext3、Ext4 等各种文件系统,再向下是为了屏蔽不同硬件设备的实现细节,我们抽象出来的单独的一层——通用块设备层,然后就是不同类型的磁盘了。 + +我们可以看到,某些层次负责的是对下层不同实现的抽象,从而对上层屏蔽实现细节。比方说 VFS 对上层(系统调用层)来说提供了统一的调用接口,同时对下层中不同的文件系统规约了实现模型,当新增一种文件系统实现的时候,只需要按照这种模型来设计,就可以无缝插入到 Linux 文件系统中。 + +分层的设计可以简化系统设计,让不同的人专注做某一层次的事情。想象一下,如果你要设计一款网络程序却没有分层,该是一件多么痛苦的事情。 + +参照阿里发布的《阿里巴巴 Java 开发手册 v1.4.0(详尽版)》 + +终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。 + +开放接口层:将 Service 层方法封装成开放接口,同时进行网关安全控制和流量控制等。 + +Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。 + +Service 层:业务逻辑层。 + +Manager 层:通用业务处理层。这一层主要有两个作用,其一,你可以将原先 Service 层的一些通用能力下沉到这一层,比如与缓存和存储交互策略,中间件的接入;其二,你也可以在这一层封装对第三方接口的调用,比如调用支付服务,调用审核服务等。 + +DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。 + +外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。 + + + + + + + + + + + + + diff --git a/docs/gaobingfaxitong/3.md b/docs/gaobingfaxitong/3.md new file mode 100644 index 000000000..ae2ff0c8f --- /dev/null +++ b/docs/gaobingfaxitong/3.md @@ -0,0 +1,46 @@ +--- +title: 如何提升系统性能 +author: 哪吒 +date: '2023-06-15' +--- + +# 如何提升系统性能 + +高并发系统设计的三大目标:高性能、高可用、可扩展 + +高并发,是指运用设计手段让系统能够处理更多的用户并发请求,也就是承担更大的流量。它是一切架构设计的背景和前提,脱离了它去谈性能和可用性是没有意义的。很显然嘛,你在每秒一次请求和每秒一万次请求,两种不同的场景下,分别做到毫秒级响应时间和五个九(99.999%)的可用性,无论是设计难度还是方案的复杂度,都不是一个级别的。 + +而性能和可用性,是我们实现高并发系统设计必须考虑的因素。 + +性能反应了系统的使用体验,想象一下,同样承担每秒一万次请求的两个系统,一个响应时间是毫秒级,一个响应时间在秒级别,它们带给用户的体验肯定是不同的。 + +可用性则表示系统可以正常服务用户的时间。我们再类比一下,还是两个承担每秒一万次的系统,一个可以做到全年不停机、无故障,一个隔三差五宕机维护,如果你是用户,你会选择使用哪一个系统呢?答案不言而喻。 + +另一个耳熟能详的名词叫“可扩展性”,它同样是高并发系统设计需要考虑的因素。为什么呢?我来举一个具体的例子。 + +流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对峰值流量的时候,我们通常需要在架构和方案上做更多的准备。这就是淘宝会花费大半年的时间准备双十一,也是在面对“明星离婚”等热点事件时,看起来无懈可击的微博系统还是会出现服务不可用的原因。而易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担峰值流量。 + +高性能、高可用和可扩展,是我们在做高并发系统设计时追求的三个目标,我会用三节课的时间,带你了解在高并发大流量下如何设计高性能、高可用和易于扩展的系统。 + +首先,性能优化一定不能盲目,一定是问题导向的。脱离了问题,盲目地提早优化会增加系统的复杂度,浪费开发人员的时间,也因为某些优化可能会对业务上有些折中的考虑,所以也会损伤业务。 + +其次,性能优化也遵循“八二原则”,即你可以用 20% 的精力解决 80% 的性能问题。所以我们在优化过程中一定要抓住主要矛盾,优先优化主要的性能瓶颈点。 + +再次,性能优化也要有数据支撑。在优化过程中,你要时刻了解你的优化让响应时间减少了多少,提升了多少的吞吐量。 + +最后,性能优化的过程是持续的。高并发的系统通常是业务逻辑相对复杂的系统,那么在这类系统中出现的性能问题通常也会有多方面的原因。因此,我们在做性能优化的时候要明确目标,比方说,支撑每秒 1 万次请求的吞吐量下响应时间在 10ms,那么我们就需要持续不断地寻找性能瓶颈,制定优化方案,直到达到目标为止。 + + + + + + + + + + + + + + + diff --git a/docs/gaobingfaxitong/img.png b/docs/gaobingfaxitong/img.png new file mode 100644 index 000000000..ca9cf8d5b Binary files /dev/null and b/docs/gaobingfaxitong/img.png differ diff --git a/docs/gaobingfaxitong/img_1.png b/docs/gaobingfaxitong/img_1.png new file mode 100644 index 000000000..f829c1399 Binary files /dev/null and b/docs/gaobingfaxitong/img_1.png differ diff --git a/docs/go/README.md b/docs/go/README.md new file mode 100644 index 000000000..6fc2d88e0 --- /dev/null +++ b/docs/go/README.md @@ -0,0 +1,172 @@ +# GO技术手册 + +## 🚀 快速导航 + +### 📖 已完成的文档 +- **[GO语言基础语法](./basic-grammar/basic-syntax.md)** - 程序结构、变量声明、控制结构、函数基础 +- **[并发编程](./advanced/concurrency.md)** - Goroutine、Channel、Select、同步原语、Context包 +- **[HTTP编程](./stdlib/http.md)** - HTTP服务器、客户端、中间件、文件上传、错误处理 +- **[测试最佳实践](./best-practices/testing.md)** - 单元测试、基准测试、模拟、覆盖率、最佳实践 +- **[Web服务开发实战](./projects/web-service.md)** - 完整RESTful API项目、认证、数据库、部署 + +### 🎯 学习路径推荐 + +#### 初学者路径 +1. [GO语言基础语法](./basic-grammar/basic-syntax.md) - 掌握基本语法 +2. [HTTP编程](./stdlib/http.md) - 学习Web开发 +3. [测试最佳实践](./best-practices/testing.md) - 掌握测试技能 +4. [Web服务开发实战](./projects/web-service.md) - 完成实战项目 + +#### 有经验开发者路径 +1. [并发编程](./advanced/concurrency.md) - 深入并发编程 +2. [测试最佳实践](./best-practices/testing.md) - 完善测试技能 +3. [Web服务开发实战](./projects/web-service.md) - 实战项目应用 + +## 📚 完整目录 + +### 基础语法 +- [GO语言基础语法](./basic-grammar/basic-syntax.md) ✅ +- [变量和数据类型](./basic-grammar/variables-data-types.md) 📝 +- [控制结构](./basic-grammar/control-structures.md) 📝 +- [函数](./basic-grammar/functions.md) 📝 +- [包管理](./basic-grammar/packages.md) 📝 +- [错误处理](./basic-grammar/error-handling.md) 📝 + +### 高级特性 +- [并发编程](./advanced/concurrency.md) ✅ +- [接口](./advanced/interface.md) 📝 +- [结构体和方法](./advanced/structs-methods.md) 📝 +- [通道(Channel)](./advanced/channels.md) 📝 +- [Goroutine](./advanced/goroutines.md) 📝 +- [反射](./advanced/reflection.md) 📝 + +### 标准库 +- [HTTP客户端和服务器](./stdlib/http.md) ✅ +- [标准库概览](./stdlib/overview.md) 📝 +- [网络编程](./stdlib/networking.md) 📝 +- [文件操作](./stdlib/file-operations.md) 📝 +- [JSON处理](./stdlib/json.md) 📝 +- [数据库操作](./stdlib/database.md) 📝 + +### 最佳实践 +- [测试](./best-practices/testing.md) ✅ +- [代码规范](./best-practices/coding-standards.md) 📝 +- [性能优化](./best-practices/performance.md) 📝 +- [项目结构](./best-practices/project-structure.md) 📝 +- [依赖管理](./best-practices/dependency-management.md) 📝 + +### 工具和生态 +- [Go Modules](./tools/go-modules.md) 📝 +- [GoLand IDE使用](./tools/goland.md) 📝 +- [调试技巧](./tools/debugging.md) 📝 +- [性能分析](./tools/profiling.md) 📝 +- [构建和部署](./tools/build-deploy.md) 📝 + +### 实战项目 +- [Web服务开发](./projects/web-service.md) ✅ +- [微服务架构](./projects/microservices.md) 📝 +- [CLI工具开发](./projects/cli-tools.md) 📝 +- [API设计](./projects/api-design.md) 📝 + +> **图例说明**:✅ 已完成 | 📝 计划中 + +## 简介 + +GO语言是由Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。GO语言的设计目标是: + +- **简洁性**:语法简洁,易于学习 +- **高效性**:编译速度快,执行效率高 +- **并发性**:原生支持并发编程 +- **安全性**:强类型系统,内存安全 +- **跨平台**:支持多种操作系统和架构 + +## 学习路径 + +1. **基础阶段**:掌握基本语法、数据类型、控制结构 +2. **进阶阶段**:学习接口、结构体、并发编程 +3. **实战阶段**:项目实践,掌握标准库和第三方库 +4. **高级阶段**:性能优化、架构设计、最佳实践 + +## 快速开始 + +```go +package main + +import "fmt" + +func main() { + fmt.Println("Hello, Go!") +} +``` + +## 🔗 访问路径 + +### 主要访问入口 +- **快速导航**:[GO技术手册导航页面](./index.md) +- **主页面**:[JavaPlusDoc主页](../README.md) → GO语言开发专题 + +### 直接访问路径 +- **基础语法**:`/JavaPlusDoc/go/basic-grammar/basic-syntax.html` +- **并发编程**:`/JavaPlusDoc/go/advanced/concurrency.html` +- **HTTP编程**:`/JavaPlusDoc/go/stdlib/http.html` +- **测试实践**:`/JavaPlusDoc/go/best-practices/testing.html` +- **Web实战**:`/JavaPlusDoc/go/projects/web-service.html` + +### 相关技术文档 +- **Java技术栈**:[Java核心技术](../basic-grammar/) +- **高并发设计**:[高并发专题](../high-concurrency/) +- **微服务架构**:[微服务实践](../aJava/微服务是什么.md) +- **Docker容器化**:[Docker指南](../docker/) +- **数据库优化**:[MySQL优化](../mysql/) + +## 📚 资源链接 + +### 官方资源 +- [Go官方文档](https://golang.org/doc/) +- [Go语言中文网](https://studygolang.com/) +- [Go语言圣经](https://github.com/golang-china/gopl-zh) +- [Go语言实战](https://github.com/unknwon/the-way-to-go_ZH_CN) + +### 学习社区 +- [Go语言中文社区](https://studygolang.com/) +- [Go语言中文网](https://golang.google.cn/) +- [Go语言学习资源](https://github.com/unknwon/go-study-index) + +### 工具和IDE +- [GoLand IDE](https://www.jetbrains.com/go/) +- [VS Code Go扩展](https://marketplace.visualstudio.com/items?itemName=golang.Go) +- [Go Playground](https://play.golang.org/) + +## 🎯 学习建议 + +### 按需学习 +- **Web开发**:基础语法 → HTTP编程 → Web服务实战 +- **系统编程**:基础语法 → 并发编程 → 系统工具开发 +- **微服务**:基础语法 → HTTP编程 → 微服务架构 +- **性能优化**:基础语法 → 并发编程 → 性能优化 + +### 实践项目 +1. **Hello World**:从最简单的程序开始 +2. **Web服务**:开发一个简单的HTTP服务 +3. **并发应用**:使用Goroutine和Channel +4. **完整项目**:结合数据库、认证、测试的完整应用 + +## 📝 贡献指南 + +欢迎为GO技术手册贡献内容: + +1. **报告问题**:发现错误或需要改进的地方 +2. **添加内容**:补充缺失的文档或示例 +3. **改进翻译**:优化中文表达和术语 +4. **分享经验**:分享GO语言学习心得和最佳实践 + +--- + +
+

+ 🚀 让我们一起在GO语言的世界中探索和成长! +

+

+ 选择您的学习路径,开始GO语言的学习之旅吧! +

+
\ No newline at end of file diff --git a/docs/go/advanced/concurrency.md b/docs/go/advanced/concurrency.md new file mode 100644 index 000000000..28d1e04ad --- /dev/null +++ b/docs/go/advanced/concurrency.md @@ -0,0 +1,475 @@ +# GO语言并发编程 + +## 1. 并发编程概述 + +GO语言通过Goroutine和Channel提供了强大的并发编程能力,遵循"通过通信来共享内存,而不是通过共享内存来通信"的设计理念。 + +### 1.1 并发 vs 并行 +- **并发**:多个任务交替执行 +- **并行**:多个任务同时执行 + +## 2. Goroutine + +### 2.1 什么是Goroutine +Goroutine是GO语言的轻量级线程,由GO运行时管理,比传统线程更轻量。 + +### 2.2 创建Goroutine +```go +package main + +import ( + "fmt" + "time" +) + +func sayHello(name string) { + fmt.Printf("Hello, %s!\n", name) +} + +func main() { + // 启动一个Goroutine + go sayHello("Alice") + + // 主Goroutine继续执行 + fmt.Println("Main function") + + // 等待一下让Goroutine有机会执行 + time.Sleep(time.Second) +} +``` + +### 2.3 多个Goroutine +```go +func main() { + for i := 0; i < 5; i++ { + go func(id int) { + fmt.Printf("Goroutine %d\n", id) + }(i) + } + + time.Sleep(time.Second) +} +``` + +### 2.4 Goroutine的生命周期 +```go +func worker(id int, jobs <-chan int, results chan<- int) { + for j := range jobs { + fmt.Printf("Worker %d processing job %d\n", id, j) + time.Sleep(time.Millisecond * 500) + results <- j * 2 + } +} + +func main() { + jobs := make(chan int, 100) + results := make(chan int, 100) + + // 启动3个worker + for w := 1; w <= 3; w++ { + go worker(w, jobs, results) + } + + // 发送工作 + for j := 1; j <= 9; j++ { + jobs <- j + } + close(jobs) + + // 收集结果 + for a := 1; a <= 9; a++ { + <-results + } +} +``` + +## 3. Channel(通道) + +### 3.1 创建Channel +```go +// 无缓冲通道 +ch := make(chan int) + +// 有缓冲通道 +ch := make(chan int, 10) +``` + +### 3.2 发送和接收 +```go +func main() { + ch := make(chan string) + + // 启动Goroutine发送数据 + go func() { + ch <- "Hello from goroutine" + }() + + // 主Goroutine接收数据 + msg := <-ch + fmt.Println(msg) +} +``` + +### 3.3 通道方向 +```go +// 只发送通道 +func sendOnly(ch chan<- int) { + ch <- 42 +} + +// 只接收通道 +func receiveOnly(ch <-chan int) { + value := <-ch + fmt.Println(value) +} + +// 双向通道 +func bidirectional(ch chan int) { + ch <- 42 + value := <-ch + fmt.Println(value) +} +``` + +### 3.4 通道关闭 +```go +func producer(ch chan int) { + for i := 0; i < 5; i++ { + ch <- i + } + close(ch) // 关闭通道 +} + +func consumer(ch chan int) { + for value := range ch { // 遍历通道直到关闭 + fmt.Println("Received:", value) + } +} + +func main() { + ch := make(chan int) + go producer(ch) + consumer(ch) +} +``` + +## 4. Select语句 + +### 4.1 基本用法 +```go +func main() { + ch1 := make(chan string) + ch2 := make(chan string) + + go func() { + time.Sleep(time.Second) + ch1 <- "one" + }() + + go func() { + time.Sleep(time.Second * 2) + ch2 <- "two" + }() + + for i := 0; i < 2; i++ { + select { + case msg1 := <-ch1: + fmt.Println("Received", msg1) + case msg2 := <-ch2: + fmt.Println("Received", msg2) + case <-time.After(time.Second * 3): + fmt.Println("Timeout") + } + } +} +``` + +### 4.2 非阻塞选择 +```go +func main() { + ch := make(chan string) + + select { + case msg := <-ch: + fmt.Println("Received:", msg) + default: + fmt.Println("No message received") + } +} +``` + +## 5. 同步原语 + +### 5.1 WaitGroup +```go +import "sync" + +func worker(id int, wg *sync.WaitGroup) { + defer wg.Done() // 确保在函数结束时调用Done() + + fmt.Printf("Worker %d starting\n", id) + time.Sleep(time.Second) + fmt.Printf("Worker %d done\n", id) +} + +func main() { + var wg sync.WaitGroup + + for i := 1; i <= 5; i++ { + wg.Add(1) + go worker(i, &wg) + } + + wg.Wait() // 等待所有Goroutine完成 + fmt.Println("All workers completed") +} +``` + +### 5.2 Mutex(互斥锁) +```go +type SafeCounter struct { + mu sync.Mutex + count int +} + +func (c *SafeCounter) Increment() { + c.mu.Lock() + defer c.mu.Unlock() + c.count++ +} + +func (c *SafeCounter) GetCount() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.count +} + +func main() { + counter := SafeCounter{} + + for i := 0; i < 1000; i++ { + go counter.Increment() + } + + time.Sleep(time.Second) + fmt.Println("Count:", counter.GetCount()) +} +``` + +### 5.3 RWMutex(读写锁) +```go +type DataStore struct { + mu sync.RWMutex + data map[string]string +} + +func (ds *DataStore) Set(key, value string) { + ds.mu.Lock() + defer ds.mu.Unlock() + ds.data[key] = value +} + +func (ds *DataStore) Get(key string) (string, bool) { + ds.mu.RLock() + defer ds.mu.RUnlock() + value, exists := ds.data[key] + return value, exists +} +``` + +## 6. Context包 + +### 6.1 基本用法 +```go +import "context" + +func worker(ctx context.Context) { + for { + select { + case <-ctx.Done(): + fmt.Println("Worker cancelled") + return + default: + fmt.Println("Working...") + time.Sleep(time.Millisecond * 500) + } + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + go worker(ctx) + + time.Sleep(time.Second * 2) + cancel() // 取消所有子Goroutine + + time.Sleep(time.Second) +} +``` + +### 6.2 超时控制 +```go +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + + go func() { + time.Sleep(time.Second * 3) + fmt.Println("This won't be printed") + }() + + <-ctx.Done() + fmt.Println("Context cancelled due to timeout") +} +``` + +## 7. 并发模式 + +### 7.1 生产者-消费者模式 +```go +func producer(ch chan<- int) { + for i := 0; i < 10; i++ { + ch <- i + time.Sleep(time.Millisecond * 100) + } + close(ch) +} + +func consumer(id int, ch <-chan int, wg *sync.WaitGroup) { + defer wg.Done() + for value := range ch { + fmt.Printf("Consumer %d: %d\n", id, value) + } +} + +func main() { + ch := make(chan int, 5) + var wg sync.WaitGroup + + go producer(ch) + + for i := 0; i < 3; i++ { + wg.Add(1) + go consumer(i, ch, &wg) + } + + wg.Wait() +} +``` + +### 7.2 工作池模式 +```go +func worker(id int, jobs <-chan int, results chan<- int) { + for j := range jobs { + fmt.Printf("Worker %d processing job %d\n", id, j) + time.Sleep(time.Millisecond * 500) + results <- j * 2 + } +} + +func main() { + const numJobs = 10 + const numWorkers = 3 + + jobs := make(chan int, numJobs) + results := make(chan int, numJobs) + + // 启动workers + for w := 1; w <= numWorkers; w++ { + go worker(w, jobs, results) + } + + // 发送jobs + for j := 1; j <= numJobs; j++ { + jobs <- j + } + close(jobs) + + // 收集结果 + for a := 1; a <= numJobs; a++ { + <-results + } +} +``` + +## 8. 常见陷阱和最佳实践 + +### 8.1 避免Goroutine泄漏 +```go +// 错误示例 +func leakyFunction() { + go func() { + for { + // 无限循环,没有退出条件 + } + }() +} + +// 正确示例 +func correctFunction() { + done := make(chan bool) + go func() { + defer close(done) + for { + select { + case <-done: + return + default: + // 工作 + } + } + }() + + // 在适当的时候关闭done通道 + time.Sleep(time.Second) + close(done) +} +``` + +### 8.2 避免竞态条件 +```go +// 错误示例 +var counter int + +func increment() { + counter++ // 竞态条件 +} + +// 正确示例 +var counter int +var mu sync.Mutex + +func increment() { + mu.Lock() + defer mu.Unlock() + counter++ +} +``` + +### 8.3 使用缓冲通道避免死锁 +```go +// 可能导致死锁 +ch := make(chan int) +ch <- 1 // 阻塞,因为没有接收者 + +// 使用缓冲通道 +ch := make(chan int, 1) +ch <- 1 // 不会阻塞 +``` + +## 9. 性能考虑 + +### 9.1 Goroutine开销 +- 每个Goroutine大约占用2KB内存 +- 可以轻松创建数百万个Goroutine + +### 9.2 通道性能 +- 无缓冲通道适合同步 +- 有缓冲通道适合异步通信 +- 避免在热点路径中使用通道 + +### 9.3 锁的性能 +- 优先使用RWMutex进行读多写少的场景 +- 考虑使用原子操作替代锁 +- 避免锁的嵌套 \ No newline at end of file diff --git a/docs/go/basic-grammar/basic-syntax.md b/docs/go/basic-grammar/basic-syntax.md new file mode 100644 index 000000000..7c7d92c49 --- /dev/null +++ b/docs/go/basic-grammar/basic-syntax.md @@ -0,0 +1,329 @@ +# GO语言基础语法 + +## 1. 程序结构 + +### 1.1 包声明 +每个GO程序都必须属于一个包,包名通常与目录名相同。 + +```go +package main +``` + +### 1.2 导入包 +使用`import`关键字导入其他包: + +```go +import "fmt" +import "os" + +// 或者使用括号导入多个包 +import ( + "fmt" + "os" + "strings" +) +``` + +### 1.3 main函数 +程序的入口点: + +```go +func main() { + fmt.Println("Hello, Go!") +} +``` + +## 2. 基本语法规则 + +### 2.1 语句结束 +GO语言不需要分号结尾,但可以使用分号: + +```go +fmt.Println("Hello") // 推荐 +fmt.Println("Hello"); // 也可以 +``` + +### 2.2 注释 +支持单行和多行注释: + +```go +// 这是单行注释 + +/* +这是多行注释 +可以跨越多行 +*/ +``` + +### 2.3 代码块 +使用大括号定义代码块: + +```go +func main() { + if x > 0 { + fmt.Println("Positive") + } +} +``` + +## 3. 变量声明 + +### 3.1 使用var关键字 +```go +var name string = "Go" +var age int = 25 +var isStudent bool = true +``` + +### 3.2 类型推断 +```go +var name = "Go" // 自动推断为string +var age = 25 // 自动推断为int +var isStudent = true // 自动推断为bool +``` + +### 3.3 短变量声明 +```go +name := "Go" +age := 25 +isStudent := true +``` + +### 3.4 批量声明 +```go +var ( + name string = "Go" + age int = 25 + city string = "Beijing" +) +``` + +## 4. 常量 + +### 4.1 常量声明 +```go +const Pi = 3.14159 +const ( + StatusOK = 200 + StatusNotFound = 404 +) +``` + +### 4.2 iota枚举 +```go +const ( + Sunday = iota // 0 + Monday // 1 + Tuesday // 2 + Wednesday // 3 +) +``` + +## 5. 基本数据类型 + +### 5.1 整数类型 +```go +var a int = 10 // 平台相关 +var b int32 = 20 // 32位 +var c int64 = 30 // 64位 +var d uint = 40 // 无符号整数 +``` + +### 5.2 浮点数类型 +```go +var a float32 = 3.14 +var b float64 = 3.14159265359 +``` + +### 5.3 布尔类型 +```go +var a bool = true +var b bool = false +``` + +### 5.4 字符串类型 +```go +var str string = "Hello, Go!" +var multiLine = `这是一个 +多行字符串` +``` + +### 5.5 复数类型 +```go +var c complex64 = 3 + 4i +var d complex128 = 5 + 6i +``` + +## 6. 运算符 + +### 6.1 算术运算符 +```go +a := 10 +b := 3 + +sum := a + b // 13 +diff := a - b // 7 +product := a * b // 30 +quotient := a / b // 3 +remainder := a % b // 1 +``` + +### 6.2 比较运算符 +```go +a := 10 +b := 20 + +fmt.Println(a == b) // false +fmt.Println(a != b) // true +fmt.Println(a < b) // true +fmt.Println(a > b) // false +fmt.Println(a <= b) // true +fmt.Println(a >= b) // false +``` + +### 6.3 逻辑运算符 +```go +a := true +b := false + +fmt.Println(a && b) // false +fmt.Println(a || b) // true +fmt.Println(!a) // false +``` + +## 7. 控制结构 + +### 7.1 if语句 +```go +if x > 0 { + fmt.Println("Positive") +} else if x < 0 { + fmt.Println("Negative") +} else { + fmt.Println("Zero") +} +``` + +### 7.2 for循环 +```go +// 传统for循环 +for i := 0; i < 10; i++ { + fmt.Println(i) +} + +// while风格 +i := 0 +for i < 10 { + fmt.Println(i) + i++ +} + +// 无限循环 +for { + fmt.Println("Infinite loop") + break +} +``` + +### 7.3 range循环 +```go +// 遍历数组 +numbers := []int{1, 2, 3, 4, 5} +for index, value := range numbers { + fmt.Printf("Index: %d, Value: %d\n", index, value) +} + +// 只获取值 +for _, value := range numbers { + fmt.Println(value) +} + +// 只获取索引 +for index := range numbers { + fmt.Println(index) +} +``` + +## 8. 函数基础 + +### 8.1 函数声明 +```go +func add(a int, b int) int { + return a + b +} + +// 参数类型相同时可以简写 +func multiply(a, b int) int { + return a * b +} +``` + +### 8.2 多返回值 +```go +func divide(a, b int) (int, error) { + if b == 0 { + return 0, fmt.Errorf("division by zero") + } + return a / b, nil +} +``` + +### 8.3 命名返回值 +```go +func getPerson() (name string, age int) { + name = "Alice" + age = 25 + return // 裸返回 +} +``` + +## 9. 完整示例 + +```go +package main + +import "fmt" + +func main() { + // 变量声明 + name := "Go" + age := 10 + + // 条件判断 + if age > 5 { + fmt.Printf("%s is %d years old\n", name, age) + } + + // 循环 + for i := 0; i < 3; i++ { + fmt.Printf("Count: %d\n", i) + } + + // 函数调用 + result := add(10, 20) + fmt.Printf("Sum: %d\n", result) +} + +func add(a, b int) int { + return a + b +} +``` + +## 10. 最佳实践 + +1. **命名规范**: + - 使用驼峰命名法 + - 包名使用小写 + - 导出的函数和变量首字母大写 + +2. **代码格式**: + - 使用`gofmt`格式化代码 + - 保持一致的缩进 + +3. **错误处理**: + - 始终检查错误返回值 + - 使用有意义的错误信息 + +4. **注释**: + - 为导出的函数添加注释 + - 使用清晰的注释说明复杂逻辑 \ No newline at end of file diff --git a/docs/go/best-practices/testing.md b/docs/go/best-practices/testing.md new file mode 100644 index 000000000..969ba7fe3 --- /dev/null +++ b/docs/go/best-practices/testing.md @@ -0,0 +1,541 @@ +> ✨ 甜心,每个知识点都值得细细品味,慢慢来不着急~ + +# GO语言测试最佳实践 + +## 1. 测试基础 + +### 1.1 测试文件命名 +测试文件以`_test.go`结尾,与被测试文件在同一目录下。 + +```go +// calculator.go +package math + +func Add(a, b int) int { + return a + b +} + +// calculator_test.go +package math + +import "testing" + +func TestAdd(t *testing.T) { + result := Add(2, 3) + expected := 5 + if result != expected { + t.Errorf("Add(2, 3) = %d; expected %d", result, expected) + } +} +``` + +### 1.2 运行测试 +```bash +# 运行当前包的测试 +go test + +# 运行特定测试 +go test -run TestAdd + +# 运行测试并显示详细信息 +go test -v + +# 运行测试并显示覆盖率 +go test -cover + +# 生成覆盖率报告 +go test -coverprofile=coverage.out +go tool cover -html=coverage.out +``` + +## 2. 测试函数 + +### 2.1 基本测试函数 +```go +func TestFunctionName(t *testing.T) { + // 测试代码 +} + +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive", 2, 3, 5}, + {"negative", -1, -2, -3}, + {"zero", 0, 5, 5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Add(tt.a, tt.b) + if result != tt.expected { + t.Errorf("Add(%d, %d) = %d; expected %d", + tt.a, tt.b, result, tt.expected) + } + }) + } +} +``` + +### 2.2 基准测试 +```go +func BenchmarkAdd(b *testing.B) { + for i := 0; i < b.N; i++ { + Add(1, 2) + } +} + +func BenchmarkAddParallel(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + Add(1, 2) + } + }) +} +``` + +### 2.3 示例测试 +```go +func ExampleAdd() { + result := Add(2, 3) + fmt.Println(result) + // Output: 5 +} + +func ExampleAdd_multiple() { + fmt.Println(Add(1, 2)) + fmt.Println(Add(-1, 1)) + fmt.Println(Add(0, 0)) + // Output: + // 3 + // 0 + // 0 +} +``` + +## 3. 测试工具 + +### 3.1 使用testing.T +```go +func TestWithHelpers(t *testing.T) { + // 跳过测试 + if testing.Short() { + t.Skip("Skipping test in short mode") + } + + // 标记测试失败但不停止 + t.Log("This is a log message") + t.Error("This is an error but test continues") + + // 标记测试失败并停止 + t.Fatal("This stops the test") +} +``` + +### 3.2 使用testing.B +```go +func BenchmarkWithSetup(b *testing.B) { + // 设置代码,只运行一次 + data := make([]int, 1000) + for i := range data { + data[i] = i + } + + b.ResetTimer() // 重置计时器 + + for i := 0; i < b.N; i++ { + // 被测试的代码 + processData(data) + } +} +``` + +### 3.3 使用testing.M +```go +func TestMain(m *testing.M) { + // 设置代码 + setup() + + // 运行测试 + code := m.Run() + + // 清理代码 + cleanup() + + os.Exit(code) +} +``` + +## 4. 测试模式 + +### 4.1 表驱动测试 +```go +func TestMultiply(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive", 2, 3, 6}, + {"negative", -2, 3, -6}, + {"zero", 0, 5, 0}, + {"large", 1000, 1000, 1000000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Multiply(tt.a, tt.b) + if result != tt.expected { + t.Errorf("Multiply(%d, %d) = %d; expected %d", + tt.a, tt.b, result, tt.expected) + } + }) + } +} +``` + +### 4.2 子测试 +```go +func TestStringOperations(t *testing.T) { + t.Run("uppercase", func(t *testing.T) { + result := ToUpper("hello") + expected := "HELLO" + if result != expected { + t.Errorf("ToUpper('hello') = %s; expected %s", result, expected) + } + }) + + t.Run("lowercase", func(t *testing.T) { + result := ToLower("WORLD") + expected := "world" + if result != expected { + t.Errorf("ToLower('WORLD') = %s; expected %s", result, expected) + } + }) +} +``` + +### 4.3 测试套件 +```go +type CalculatorTestSuite struct { + suite.Suite + calc *Calculator +} + +func (suite *CalculatorTestSuite) SetupTest() { + suite.calc = NewCalculator() +} + +func (suite *CalculatorTestSuite) TestAdd() { + result := suite.calc.Add(2, 3) + suite.Equal(5, result) +} + +func (suite *CalculatorTestSuite) TestSubtract() { + result := suite.calc.Subtract(5, 3) + suite.Equal(2, result) +} + +func TestCalculatorTestSuite(t *testing.T) { + suite.Run(t, new(CalculatorTestSuite)) +} +``` + +## 5. 模拟和存根 + +### 5.1 接口模拟 +```go +type Database interface { + GetUser(id int) (*User, error) + SaveUser(user *User) error +} + +type MockDatabase struct { + users map[int]*User +} + +func (m *MockDatabase) GetUser(id int) (*User, error) { + user, exists := m.users[id] + if !exists { + return nil, fmt.Errorf("user not found") + } + return user, nil +} + +func (m *MockDatabase) SaveUser(user *User) error { + m.users[user.ID] = user + return nil +} + +func TestUserService(t *testing.T) { + mockDB := &MockDatabase{ + users: make(map[int]*User), + } + + service := NewUserService(mockDB) + + // 测试代码 + user, err := service.GetUser(1) + if err == nil { + t.Error("Expected error for non-existent user") + } +} +``` + +### 5.2 使用gomock +```go +//go:generate mockgen -source=database.go -destination=mock_database.go -package=mocks + +type Database interface { + GetUser(id int) (*User, error) +} + +func TestWithGomock(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDB := NewMockDatabase(ctrl) + mockDB.EXPECT().GetUser(1).Return(&User{ID: 1, Name: "Alice"}, nil) + + service := NewUserService(mockDB) + user, err := service.GetUser(1) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if user.Name != "Alice" { + t.Errorf("Expected Alice, got %s", user.Name) + } +} +``` + +## 6. 测试HTTP服务 + +### 6.1 使用httptest +```go +func TestHTTPHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/user/1", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(UserHandler) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + expected := `{"id":1,"name":"Alice"}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +### 6.2 测试HTTP客户端 +```go +func TestHTTPClient(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"id":1,"name":"Alice"}`)) + })) + defer server.Close() + + resp, err := http.Get(server.URL + "/user/1") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} +``` + +## 7. 测试数据库 + +### 7.1 使用测试数据库 +```go +func TestDatabaseOperations(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + // 创建表 + _, err = db.Exec(` + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + ) + `) + if err != nil { + t.Fatal(err) + } + + // 测试插入 + _, err = db.Exec("INSERT INTO users (id, name) VALUES (?, ?)", 1, "Alice") + if err != nil { + t.Errorf("Failed to insert user: %v", err) + } + + // 测试查询 + var name string + err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name) + if err != nil { + t.Errorf("Failed to query user: %v", err) + } + if name != "Alice" { + t.Errorf("Expected Alice, got %s", name) + } +} +``` + +## 8. 性能测试 + +### 8.1 基准测试 +```go +func BenchmarkStringConcatenation(b *testing.B) { + b.Run("plus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = "Hello" + " " + "World" + } + }) + + b.Run("fmt", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = fmt.Sprintf("%s %s", "Hello", "World") + } + }) + + b.Run("strings.Builder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var builder strings.Builder + builder.WriteString("Hello") + builder.WriteString(" ") + builder.WriteString("World") + _ = builder.String() + } + }) +} +``` + +### 8.2 内存分析 +```go +func BenchmarkMemoryAllocation(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + data := make([]int, 1000) + for j := range data { + data[j] = j + } + } +} +``` + +## 9. 测试最佳实践 + +### 9.1 测试命名 +```go +// 好的命名 +func TestAdd_WithPositiveNumbers_ReturnsSum(t *testing.T) { } +func TestAdd_WithNegativeNumbers_ReturnsSum(t *testing.T) { } +func TestAdd_WithZero_ReturnsOtherNumber(t *testing.T) { } + +// 避免的命名 +func TestAdd1(t *testing.T) { } +func TestAdd2(t *testing.T) { } +``` + +### 9.2 测试组织 +```go +// 按功能组织测试 +func TestUserService_CreateUser(t *testing.T) { } +func TestUserService_GetUser(t *testing.T) { } +func TestUserService_UpdateUser(t *testing.T) { } +func TestUserService_DeleteUser(t *testing.T) { } +``` + +### 9.3 测试数据 +```go +// 使用常量定义测试数据 +const ( + testUserID = 1 + testUserName = "Alice" + testUserEmail = "alice@example.com" +) + +func TestUserService(t *testing.T) { + user := &User{ + ID: testUserID, + Name: testUserName, + Email: testUserEmail, + } + // 测试代码 +} +``` + +### 9.4 测试清理 +```go +func TestWithCleanup(t *testing.T) { + // 设置 + tempDir := t.TempDir() + file := filepath.Join(tempDir, "test.txt") + + // 测试完成后自动清理 + t.Cleanup(func() { + // 额外的清理工作 + }) + + // 测试代码 + err := os.WriteFile(file, []byte("test"), 0644) + if err != nil { + t.Fatal(err) + } +} +``` + +## 10. 测试覆盖率 + +### 10.1 覆盖率目标 +```go +// 设置覆盖率目标 +func TestMain(m *testing.M) { + // 运行测试 + code := m.Run() + + // 检查覆盖率 + if testing.CoverMode() != "" { + coverage := testing.Coverage() + if coverage < 0.8 { + fmt.Printf("Coverage %.2f%% is below 80%%\n", coverage*100) + os.Exit(1) + } + } + + os.Exit(code) +} +``` + +### 10.2 生成覆盖率报告 +```bash +# 生成覆盖率文件 +go test -coverprofile=coverage.out + +# 查看HTML报告 +go tool cover -html=coverage.out -o coverage.html + +# 查看函数覆盖率 +go tool cover -func=coverage.out +``` \ No newline at end of file diff --git a/docs/go/index.md b/docs/go/index.md new file mode 100644 index 000000000..276043a85 --- /dev/null +++ b/docs/go/index.md @@ -0,0 +1,151 @@ +# GO技术手册 - 快速导航 + +## 🚀 快速开始 + +欢迎来到GO技术手册!这里为您提供了完整的GO语言学习路径,从基础语法到高级应用,从理论概念到实战项目。 + +### 📚 学习路径 + +1. **基础阶段** → 2. **进阶阶段** → 3. **实战阶段** → 4. **高级阶段** + +--- + +## 📖 基础语法 + +### [GO语言基础语法](./basic-grammar/basic-syntax.md) +- 程序结构和包管理 +- 变量声明和数据类型 +- 控制结构和函数基础 +- 运算符和表达式 + +**适合人群**:GO语言初学者、从其他语言转GO的开发者 + +--- + +## 🔥 高级特性 + +### [并发编程](./advanced/concurrency.md) +- Goroutine和Channel +- Select语句和同步原语 +- Context包和并发模式 +- 性能优化和最佳实践 + +**适合人群**:中级GO开发者、需要深入理解并发的开发者 + +--- + +## 📦 标准库 + +### [HTTP编程](./stdlib/http.md) +- HTTP服务器开发 +- HTTP客户端使用 +- 中间件和错误处理 +- 文件上传和JSON处理 + +**适合人群**:Web开发工程师、API开发者 + +--- + +## 🎯 最佳实践 + +### [测试最佳实践](./best-practices/testing.md) +- 单元测试和基准测试 +- 模拟和存根技术 +- 测试覆盖率和性能测试 +- 测试组织和最佳实践 + +**适合人群**:所有GO开发者、质量保障工程师 + +--- + +## 🚀 实战项目 + +### [Web服务开发实战](./projects/web-service.md) +- 完整的RESTful API项目 +- 用户认证和JWT +- 数据库操作和中间件 +- 项目部署和Docker化 + +**适合人群**:全栈开发者、系统架构师 + +--- + +## 🎯 学习建议 + +### 初学者路径 +1. 从[基础语法](./basic-grammar/basic-syntax.md)开始 +2. 学习[HTTP编程](./stdlib/http.md)进行Web开发 +3. 掌握[测试最佳实践](./best-practices/testing.md) +4. 完成[Web服务实战](./projects/web-service.md)项目 + +### 有经验开发者路径 +1. 直接学习[并发编程](./advanced/concurrency.md) +2. 深入[测试最佳实践](./best-practices/testing.md) +3. 完成[Web服务实战](./projects/web-service.md)项目 +4. 根据项目需求选择特定主题深入学习 + +--- + +## 🔗 访问路径 + +### 主要入口 +- **[GO技术手册主页](./README.md)** - 完整目录和资源链接 +- **[JavaPlusDoc主页](../README.md)** - 返回主平台,查看其他技术专题 + +### 直接访问路径 +- **基础语法**:`/JavaPlusDoc/go/basic-grammar/basic-syntax.html` +- **并发编程**:`/JavaPlusDoc/go/advanced/concurrency.html` +- **HTTP编程**:`/JavaPlusDoc/go/stdlib/http.html` +- **测试实践**:`/JavaPlusDoc/go/best-practices/testing.html` +- **Web实战**:`/JavaPlusDoc/go/projects/web-service.html` + +### 相关技术文档 +- **Java基础**:[Java基础语法](../basic-grammar/) +- **高并发**:[高并发设计](../high-concurrency/) +- **微服务**:[微服务架构](../aJava/微服务是什么.md) +- **容器化**:[Docker指南](../docker/) +- **数据库**:[MySQL优化](../mysql/) + +## 📚 相关资源 + +### 官方资源 +- [Go官方文档](https://golang.org/doc/) +- [Go语言中文网](https://studygolang.com/) +- [Go语言圣经](https://github.com/golang-china/gopl-zh) +- [Go语言实战](https://github.com/unknwon/the-way-to-go_ZH_CN) + +### 学习社区 +- [Go语言中文社区](https://studygolang.com/) +- [Go语言中文网](https://golang.google.cn/) +- [Go语言学习资源](https://github.com/unknwon/go-study-index) + +### 开发工具 +- [GoLand IDE](https://www.jetbrains.com/go/) +- [VS Code Go扩展](https://marketplace.visualstudio.com/items?itemName=golang.Go) +- [Go Playground](https://play.golang.org/) + +--- + +## 📝 贡献指南 + +如果您发现文档中的错误或有改进建议,欢迎通过以下方式贡献: + +1. 提交Issue报告问题 +2. 提交Pull Request改进内容 +3. 分享您的GO语言学习心得 + +--- + +## 🎉 开始学习 + +选择您的学习路径,开始GO语言的学习之旅吧! + +> **提示**:建议按照学习路径顺序进行,每个阶段都要动手实践,这样才能真正掌握GO语言的精髓。 + +--- + +
+

+ 🚀 让我们一起在GO语言的世界中探索和成长! +

+
\ No newline at end of file diff --git a/docs/go/projects/web-service.md b/docs/go/projects/web-service.md new file mode 100644 index 000000000..60985b307 --- /dev/null +++ b/docs/go/projects/web-service.md @@ -0,0 +1,753 @@ +# GO语言Web服务开发实战 + +## 1. 项目概述 + +本实战项目将创建一个完整的RESTful API服务,包含用户管理、认证、数据库操作等功能。 + +### 1.1 项目结构 +``` +myapp/ +├── cmd/ +│ └── server/ +│ └── main.go +├── internal/ +│ ├── handlers/ +│ │ ├── user.go +│ │ └── auth.go +│ ├── models/ +│ │ └── user.go +│ ├── database/ +│ │ └── db.go +│ └── middleware/ +│ ├── auth.go +│ └── logging.go +├── pkg/ +│ └── utils/ +│ └── jwt.go +├── configs/ +│ └── config.yaml +├── go.mod +└── go.sum +``` + +## 2. 项目初始化 + +### 2.1 创建项目 +```bash +mkdir myapp +cd myapp +go mod init myapp +``` + +### 2.2 安装依赖 +```bash +go get github.com/gin-gonic/gin +go get github.com/golang-jwt/jwt/v4 +go get gorm.io/gorm +go get gorm.io/driver/postgres +go get github.com/spf13/viper +``` + +## 3. 数据模型 + +### 3.1 用户模型 +```go +// internal/models/user.go +package models + +import ( + "time" + "gorm.io/gorm" +) + +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"unique;not null"` + Email string `json:"email" gorm:"unique;not null"` + Password string `json:"-" gorm:"not null"` + Role string `json:"role" gorm:"default:'user'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +type CreateUserRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +type UpdateUserRequest struct { + Username string `json:"username"` + Email string `json:"email" binding:"omitempty,email"` +} + +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type LoginResponse struct { + Token string `json:"token"` + User User `json:"user"` +} +``` + +## 4. 数据库配置 + +### 4.1 数据库连接 +```go +// internal/database/db.go +package database + +import ( + "fmt" + "log" + "myapp/internal/models" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDB() { + dsn := "host=localhost user=postgres password=password dbname=myapp port=5432 sslmode=disable" + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + DB = db + + // 自动迁移 + err = DB.AutoMigrate(&models.User{}) + if err != nil { + log.Fatal("Failed to migrate database:", err) + } + + fmt.Println("Database connected and migrated successfully") +} + +func GetDB() *gorm.DB { + return DB +} +``` + +## 5. 认证中间件 + +### 5.1 JWT工具 +```go +// pkg/utils/jwt.go +package utils + +import ( + "errors" + "time" + "github.com/golang-jwt/jwt/v4" +) + +var jwtSecret = []byte("your-secret-key") + +type Claims struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func GenerateToken(userID uint, username, role string) (string, error) { + claims := Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +func ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} +``` + +### 5.2 认证中间件 +```go +// internal/middleware/auth.go +package middleware + +import ( + "net/http" + "strings" + "myapp/pkg/utils" + "github.com/gin-gonic/gin" +) + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token required"}) + c.Abort() + return + } + + claims, err := utils.ValidateToken(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Next() + } +} + +func AdminMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + c.Abort() + return + } + + if role != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) + c.Abort() + return + } + + c.Next() + } +} +``` + +## 6. 处理器 + +### 6.1 用户处理器 +```go +// internal/handlers/user.go +package handlers + +import ( + "net/http" + "strconv" + "myapp/internal/database" + "myapp/internal/models" + "myapp/pkg/utils" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +type UserHandler struct{} + +func NewUserHandler() *UserHandler { + return &UserHandler{} +} + +// 注册用户 +func (h *UserHandler) Register(c *gin.Context) { + var req models.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查用户是否已存在 + var existingUser models.User + if err := database.DB.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil { + c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) + return + } + + // 加密密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) + return + } + + user := models.User{ + Username: req.Username, + Email: req.Email, + Password: string(hashedPassword), + Role: "user", + } + + if err := database.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "User created successfully", "user": user}) +} + +// 用户登录 +func (h *UserHandler) Login(c *gin.Context) { + var req models.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var user models.User + if err := database.DB.Where("username = ?", req.Username).First(&user).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + + // 验证密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + + // 生成JWT token + token, err := utils.GenerateToken(user.ID, user.Username, user.Role) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + response := models.LoginResponse{ + Token: token, + User: user, + } + + c.JSON(http.StatusOK, response) +} + +// 获取用户信息 +func (h *UserHandler) GetProfile(c *gin.Context) { + userID, _ := c.Get("user_id") + + var user models.User + if err := database.DB.First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// 更新用户信息 +func (h *UserHandler) UpdateProfile(c *gin.Context) { + userID, _ := c.Get("user_id") + + var req models.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var user models.User + if err := database.DB.First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // 更新字段 + if req.Username != "" { + user.Username = req.Username + } + if req.Email != "" { + user.Email = req.Email + } + + if err := database.DB.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// 获取所有用户(管理员功能) +func (h *UserHandler) GetAllUsers(c *gin.Context) { + var users []models.User + if err := database.DB.Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) + return + } + + c.JSON(http.StatusOK, users) +} + +// 删除用户(管理员功能) +func (h *UserHandler) DeleteUser(c *gin.Context) { + userID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + if err := database.DB.Delete(&models.User{}, userID).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) +} +``` + +## 7. 路由配置 + +### 7.1 主路由 +```go +// internal/routes/routes.go +package routes + +import ( + "myapp/internal/handlers" + "myapp/internal/middleware" + "github.com/gin-gonic/gin" +) + +func SetupRoutes() *gin.Engine { + r := gin.Default() + + // 添加中间件 + r.Use(middleware.LoggingMiddleware()) + + // 公开路由 + public := r.Group("/api/v1") + { + userHandler := handlers.NewUserHandler() + + public.POST("/register", userHandler.Register) + public.POST("/login", userHandler.Login) + } + + // 需要认证的路由 + protected := r.Group("/api/v1") + protected.Use(middleware.AuthMiddleware()) + { + userHandler := handlers.NewUserHandler() + + protected.GET("/profile", userHandler.GetProfile) + protected.PUT("/profile", userHandler.UpdateProfile) + } + + // 管理员路由 + admin := r.Group("/api/v1/admin") + admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware()) + { + userHandler := handlers.NewUserHandler() + + admin.GET("/users", userHandler.GetAllUsers) + admin.DELETE("/users/:id", userHandler.DeleteUser) + } + + return r +} +``` + +## 8. 主程序 + +### 8.1 服务器入口 +```go +// cmd/server/main.go +package main + +import ( + "log" + "myapp/internal/database" + "myapp/internal/routes" +) + +func main() { + // 初始化数据库 + database.InitDB() + + // 设置路由 + r := routes.SetupRoutes() + + // 启动服务器 + log.Println("Server starting on :8080") + if err := r.Run(":8080"); err != nil { + log.Fatal("Failed to start server:", err) + } +} +``` + +## 9. 配置文件 + +### 9.1 配置文件 +```yaml +# configs/config.yaml +server: + port: 8080 + host: "0.0.0.0" + +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "password" + name: "myapp" + sslmode: "disable" + +jwt: + secret: "your-secret-key" + expiration: 24h + +logging: + level: "info" + format: "json" +``` + +## 10. 测试 + +### 10.1 API测试 +```bash +# 注册用户 +curl -X POST http://localhost:8080/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","email":"test@example.com","password":"password123"}' + +# 用户登录 +curl -X POST http://localhost:8080/api/v1/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","password":"password123"}' + +# 获取用户信息(需要token) +curl -X GET http://localhost:8080/api/v1/profile \ + -H "Authorization: Bearer YOUR_TOKEN_HERE" +``` + +### 10.2 单元测试 +```go +// internal/handlers/user_test.go +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "myapp/internal/models" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestUserHandler_Register(t *testing.T) { + gin.SetMode(gin.TestMode) + + r := gin.New() + handler := NewUserHandler() + r.POST("/register", handler.Register) + + req := models.CreateUserRequest{ + Username: "testuser", + Email: "test@example.com", + Password: "password123", + } + + body, _ := json.Marshal(req) + request, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(body)) + request.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + r.ServeHTTP(w, request) + + assert.Equal(t, http.StatusCreated, w.Code) +} +``` + +## 11. 部署 + +### 11.1 Dockerfile +```dockerfile +FROM golang:1.21-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN go build -o main ./cmd/server + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ + +COPY --from=builder /app/main . +COPY --from=builder /app/configs ./configs + +EXPOSE 8080 +CMD ["./main"] +``` + +### 11.2 Docker Compose +```yaml +# docker-compose.yml +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + depends_on: + - db + environment: + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=postgres + - DB_PASSWORD=password + - DB_NAME=myapp + + db: + image: postgres:15 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=myapp + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: +``` + +## 12. 最佳实践 + +### 12.1 错误处理 +```go +// 自定义错误类型 +type AppError struct { + Code int `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +func (e AppError) Error() string { + return e.Message +} + +// 统一错误处理中间件 +func ErrorHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + if len(c.Errors) > 0 { + err := c.Errors.Last() + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + } + } +} +``` + +### 12.2 日志记录 +```go +// internal/middleware/logging.go +package middleware + +import ( + "time" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func LoggingMiddleware() gin.HandlerFunc { + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + }) +} +``` + +### 12.3 配置管理 +```go +// internal/config/config.go +package config + +import ( + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + JWT JWTConfig `mapstructure:"jwt"` +} + +type ServerConfig struct { + Port int `mapstructure:"port"` + Host string `mapstructure:"host"` +} + +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Name string `mapstructure:"name"` + SSLMode string `mapstructure:"sslmode"` +} + +type JWTConfig struct { + Secret string `mapstructure:"secret"` + Expiration string `mapstructure:"expiration"` +} + +func LoadConfig() (*Config, error) { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath("./configs") + + if err := viper.ReadInConfig(); err != nil { + return nil, err + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, err + } + + return &config, nil +} +``` + +这个实战项目展示了GO语言Web服务开发的完整流程,包括项目结构、数据模型、认证、路由、测试和部署等方面。通过这个项目,可以学习到GO语言Web开发的最佳实践。 \ No newline at end of file diff --git a/docs/go/stdlib/http.md b/docs/go/stdlib/http.md new file mode 100644 index 000000000..77ad219b0 --- /dev/null +++ b/docs/go/stdlib/http.md @@ -0,0 +1,557 @@ +> 💫 甜心,保持规律的作息,这样学习效果会更好呢~ + +# GO语言HTTP编程 + +## 1. HTTP服务器 + +### 1.1 基本HTTP服务器 +```go +package main + +import ( + "fmt" + "net/http" +) + +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +func main() { + http.HandleFunc("/", helloHandler) + + fmt.Println("Server starting on :8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Printf("Server failed to start: %v\n", err) + } +} +``` + +### 1.2 处理不同的HTTP方法 +```go +func userHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + fmt.Fprintf(w, "GET: Get user information") + case "POST": + fmt.Fprintf(w, "POST: Create new user") + case "PUT": + fmt.Fprintf(w, "PUT: Update user") + case "DELETE": + fmt.Fprintf(w, "DELETE: Delete user") + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func main() { + http.HandleFunc("/user", userHandler) + http.ListenAndServe(":8080", nil) +} +``` + +### 1.3 获取请求参数 +```go +func queryHandler(w http.ResponseWriter, r *http.Request) { + // 获取查询参数 + name := r.URL.Query().Get("name") + age := r.URL.Query().Get("age") + + fmt.Fprintf(w, "Name: %s, Age: %s\n", name, age) +} + +func formHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + // 解析表单数据 + err := r.ParseForm() + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + name := r.FormValue("name") + email := r.FormValue("email") + + fmt.Fprintf(w, "Name: %s, Email: %s\n", name, email) + } else { + // 返回HTML表单 + html := ` + + +
+
+
+ + + + + ` + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, html) + } +} +``` + +### 1.4 处理JSON数据 +```go +import ( + "encoding/json" + "net/http" +) + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +func jsonHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + var user User + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // 处理用户数据 + user.ID = 123 // 模拟分配ID + + // 返回JSON响应 + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) + } else { + // 返回用户列表 + users := []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com"}, + {ID: 2, Name: "Bob", Email: "bob@example.com"}, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) + } +} +``` + +### 1.5 中间件 +```go +func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // 调用下一个处理器 + next(w, r) + + // 记录请求信息 + fmt.Printf("%s %s %v\n", r.Method, r.URL.Path, time.Since(start)) + } +} + +func authMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // 验证token(简化示例) + if token != "Bearer valid-token" { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + next(w, r) + } +} + +func protectedHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Protected content") +} + +func main() { + // 应用中间件 + http.HandleFunc("/protected", authMiddleware(loggingMiddleware(protectedHandler))) + http.HandleFunc("/public", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Public content") + })) + + http.ListenAndServe(":8080", nil) +} +``` + +## 2. HTTP客户端 + +### 2.1 基本GET请求 +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +func main() { + resp, err := http.Get("https://api.github.com/users/octocat") + if err != nil { + fmt.Printf("Error making request: %v\n", err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("Error reading response: %v\n", err) + return + } + + fmt.Printf("Status: %s\n", resp.Status) + fmt.Printf("Body: %s\n", string(body)) +} +``` + +### 2.2 POST请求 +```go +import ( + "bytes" + "encoding/json" + "net/http" +) + +type Post struct { + Title string `json:"title"` + Body string `json:"body"` + UserID int `json:"userId"` +} + +func main() { + post := Post{ + Title: "My Post", + Body: "This is the body of my post", + UserID: 1, + } + + jsonData, err := json.Marshal(post) + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + + resp, err := http.Post( + "https://jsonplaceholder.typicode.com/posts", + "application/json", + bytes.NewBuffer(jsonData), + ) + if err != nil { + fmt.Printf("Error making POST request: %v\n", err) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Response: %s\n", string(body)) +} +``` + +### 2.3 自定义HTTP客户端 +```go +import ( + "context" + "net/http" + "time" +) + +func main() { + // 创建自定义客户端 + client := &http.Client{ + Timeout: time.Second * 10, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, + } + + // 创建请求 + req, err := http.NewRequest("GET", "https://api.github.com/users/octocat", nil) + if err != nil { + fmt.Printf("Error creating request: %v\n", err) + return + } + + // 添加请求头 + req.Header.Set("User-Agent", "MyApp/1.0") + req.Header.Set("Accept", "application/json") + + // 设置超时上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req = req.WithContext(ctx) + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error making request: %v\n", err) + return + } + defer resp.Body.Close() + + fmt.Printf("Status: %s\n", resp.Status) +} +``` + +### 2.4 处理JSON响应 +```go +type GitHubUser struct { + Login string `json:"login"` + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + PublicRepos int `json:"public_repos"` +} + +func main() { + resp, err := http.Get("https://api.github.com/users/octocat") + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + defer resp.Body.Close() + + var user GitHubUser + err = json.NewDecoder(resp.Body).Decode(&user) + if err != nil { + fmt.Printf("Error decoding JSON: %v\n", err) + return + } + + fmt.Printf("User: %s (ID: %d)\n", user.Name, user.ID) + fmt.Printf("Public repos: %d\n", user.PublicRepos) +} +``` + +## 3. 文件上传 + +### 3.1 服务器端处理文件上传 +```go +func uploadHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + // 解析多部分表单 + err := r.ParseMultipartForm(32 << 20) // 32MB + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + // 获取上传的文件 + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "No file uploaded", http.StatusBadRequest) + return + } + defer file.Close() + + // 创建目标文件 + dst, err := os.Create("./uploads/" + header.Filename) + if err != nil { + http.Error(w, "Failed to create file", http.StatusInternalServerError) + return + } + defer dst.Close() + + // 复制文件内容 + _, err = io.Copy(dst, file) + if err != nil { + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "File %s uploaded successfully", header.Filename) + } else { + // 返回上传表单 + html := ` + + +
+
+ + + + + ` + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, html) + } +} +``` + +### 3.2 客户端上传文件 +```go +func uploadFile(filename string, url string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + // 创建multipart writer + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + // 创建文件字段 + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return err + } + + // 复制文件内容 + _, err = io.Copy(part, file) + if err != nil { + return err + } + + writer.Close() + + // 发送请求 + resp, err := http.Post(url, writer.FormDataContentType(), &buf) + if err != nil { + return err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Response: %s\n", string(body)) + + return nil +} +``` + +## 4. 错误处理 + +### 4.1 HTTP错误处理 +```go +func handleHTTPError(resp *http.Response) error { + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + return nil +} + +func makeRequest(url string) error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if err := handleHTTPError(resp); err != nil { + return err + } + + // 处理成功响应 + return nil +} +``` + +### 4.2 重试机制 +```go +func makeRequestWithRetry(url string, maxRetries int) error { + var lastErr error + + for i := 0; i < maxRetries; i++ { + resp, err := http.Get(url) + if err != nil { + lastErr = err + time.Sleep(time.Second * time.Duration(i+1)) + continue + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + return nil + } + + lastErr = fmt.Errorf("HTTP %d", resp.StatusCode) + time.Sleep(time.Second * time.Duration(i+1)) + } + + return fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr) +} +``` + +## 5. 最佳实践 + +### 5.1 设置超时 +```go +func createClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } +} +``` + +### 5.2 使用连接池 +```go +var client = &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, +} +``` + +### 5.3 处理大文件 +```go +func downloadFile(url, filename string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + // 使用缓冲复制,避免内存问题 + _, err = io.Copy(file, resp.Body) + return err +} +``` + +### 5.4 安全考虑 +```go +func secureHandler(w http.ResponseWriter, r *http.Request) { + // 设置安全头 + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Content-Security-Policy", "default-src 'self'") + + // 验证内容类型 + if r.Header.Get("Content-Type") != "application/json" { + http.Error(w, "Invalid content type", http.StatusBadRequest) + return + } + + // 处理请求 + fmt.Fprintf(w, "Secure response") +} +``` \ No newline at end of file diff --git a/docs/iot/byteArray.md b/docs/iot/byteArray.md new file mode 100644 index 000000000..486af1db4 --- /dev/null +++ b/docs/iot/byteArray.md @@ -0,0 +1,84 @@ +--- +title: 字节数组 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 字节数组 + +数: int i —— 要转换的整数。 + +步骤: + +1. 创建一个长度为 4 的字节数组 targets,用来存储转换后的字节。 +2. 按照从低字节到高字节的顺序,将整数的每个字节填充到数组中: +3. `targets[3]` 存储最低位字节(8 位),通过 i & 0xFF 获取。 +4. `targets[2]` 存储次低位字节,使用 i >> 8 & 0xFF 右移 8 位后获取。 +5. `targets[1]` 存储次高位字节,使用 i >> 16 & 0xFF 右移 16 位后获取。 +6. `targets[0]` 存储最高位字节,使用 i >> 24 & 0xFF 右移 24 位后获取。 +7. 返回字节数组 targets。 + +示例: + +假设我们有一个整数 i = 305419896(对应的十六进制表示是 0x12345678)。 + +调用 intToByte4(305419896) 时的转换过程: + +305419896 十进制等于 0x12345678 十六进制。 + +将 0x12345678 转换为字节数组: + +1. `targets[3] = (byte) (0x12345678 & 0xFF) = 0x7`8(最低位字节) +2. `targets[2] = (byte) (0x12345678 >> 8 & 0xFF) = 0x56` +3. `targets[1] = (byte) (0x12345678 >> 16 & 0xFF) = 0x34` +4. `targets[0] = (byte) (0x12345678 >> 24 & 0xFF) = 0x12`(最高位字节) +5. 所以返回的字节数组为:`[0x12, 0x34, 0x56, 0x78]` + +```java +public class Main { + public static void main(String[] args) { + int i = 305419896; // 整数 305419896 (0x12345678) + byte[] byteArray = intToByte4(i); + + // 输出字节数组 + System.out.println("Byte array: "); + for (byte b : byteArray) { + System.out.printf("0x%02X ", b); + } + } + + public static byte[] intToByte4(int i) { + byte[] targets = new byte[4]; + targets[3] = (byte) (i & 0xFF); + targets[2] = (byte) (i >> 8 & 0xFF); + targets[1] = (byte) (i >> 16 & 0xFF); + targets[0] = (byte) (i >> 24 & 0xFF); + return targets; + } +} + + +``` + +``` +Byte array: +0x12 0x34 0x56 0x78 + +``` + +0xFF 是一个十六进制表示法,代表一个 8 位的二进制数,其值为 11111111。它通常用于按位操作,尤其是在取整数的低 8 位时非常有用。 + +十六进制解释: + +1. 0x 表示后面的数是十六进制数。 +2. FF 是十六进制表示的数字,它等于 255,十进制形式。 + +二进制表示: + +1. 0xFF 的二进制表示是 11111111。 +2. 它相当于 8 个 1,即全 1 的二进制数。 + diff --git a/docs/iot/iotJob.md b/docs/iot/iotJob.md new file mode 100644 index 000000000..f4ee9cc24 --- /dev/null +++ b/docs/iot/iotJob.md @@ -0,0 +1,35 @@ +--- +title: 物联网流程 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 物联网流程 + +调用一个测试服务接口信息 + +> kafka,电池,柜子,事件 + + +> 服务 + + +- websocket + + +- 控制电池 + + +- kafka + + +- 电池充电放电处理服务(kafka发送电池充电放电记录) + + + + + diff --git a/docs/iot/redis.md b/docs/iot/redis.md new file mode 100644 index 000000000..39b584e0a --- /dev/null +++ b/docs/iot/redis.md @@ -0,0 +1,127 @@ +--- +title: 物联网redis +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 物联网redis + +RedisConfig 是一个 Spring 配置类,主要用于配置连接 Redis 数据库并提供 RedisTemplate,它支持 Redis 作为实时缓存的备份存储。这段代码通过配置 Redis 连接池、主机地址、端口、密码、数据库索引等信息,确保应用能够连接到 Redis 并进行高效的缓存操作。 + +### 主要功能: + +配置 Redis 连接参数: + +通过 @Value 注解从配置文件中获取 Redis 相关的参数,如 Redis 主机地址、端口号、密码、数据库索引、超时时间等。 + +创建 Redis 连接工厂: + +使用 JedisConnectionFactory 创建 Redis 连接工厂,连接到 Redis 实例并设置连接的各种参数,包括主机、端口、密码、数据库索引和超时时间。 + +配置 Redis 连接池: + +使用 JedisPoolConfig 配置 Redis 连接池的相关参数,如最大空闲连接数 (maxIdle) 和最小空闲连接数 (minIdle)。连接池的使用有助于提升 Redis 连接的复用效率,避免每次都创建新的连接。 + +创建 RedisTemplate: + +使用配置好的连接工厂来创建 RedisTemplate,这是 Spring 数据库操作的核心类之一,封装了对 Redis 的常见操作,如存取数据等。RedisTemplate 提供了对 Redis 数据库进行各种操作的 API。 + +日志输出: + +配置过程中,输出日志,记录 Redis 主机地址、端口号等关键信息,用于调试和监控。 + +静态缓存设置: + +Redis 主机、端口、数据库索引和密码构成的字符串,可能用于后续跟踪或缓存标识。 + +1. 字段注入: + + 使用 @Value 注解,从配置文件中读取 Redis 的相关配置信息并赋值给类的成员变量: + host:Redis 主机地址。 + port:Redis 端口号。 + timeout:Redis 连接超时时间。 + password:Redis 密码。 + database:选择的 Redis 数据库索引。 + maxIdle:连接池的最大空闲连接数。 + minIdle:连接池的最小空闲连接数。 + +使用 `@ConfigurationProperties` 进行批量注入: + +使用 `@ConfigurationProperties(prefix = "xx.xx.redis")` 来代替逐一的 `@Value` 注入,简化了代码,提升了可扩展性。当有新的 Redis 配置项时,只需要在配置文件中新增对应字段,而不需要修改代码。 + +避免日志中输出敏感信息: + +在日志中避免打印 Redis 密码。在设置 `rtCacheId` 时,如果密码存在,使用 "****" 来替代真实的密码,这样可以避免将敏感信息泄露到日志中。 + +配置创建方法: + +将连接工厂和连接池的创建逻辑提取到 `createJedisConnectionFactory()` 和 `createJedisPoolConfig()` 方法中,减少了重复代码,使得代码更加清晰和可维护。 + +连接池配置优化: + +创建 `JedisPoolConfig` 时将 `maxIdle` 和 `minIdle` 提取到单独的方法中,使得该部分配置更加清晰。 + +静态字段的使用: + +rtCacheId 保留了静态变量,但请注意,在多线程环境下使用静态字段需要确保线程安全性。如果没有特殊需求,静态字段的使用应尽量避免。如果需要保证唯一标识,可以考虑使用单例模式或其他机制。 + +可扩展性: + +使用 `@ConfigurationProperties` 提高了类的可扩展性,方便以后扩展 `Redis` 配置项。 + +简化日志记录: + +日志记录中仅包含 `Redis` 主机和端口,避免了敏感信息的暴露,同时对配置进行清晰描述。 + +## 创建并配置一个 RedisTemplate 实例 + +``` +redisTemplate.opsForValue().set("user:123:loginStatus", "active"); +String status = redisTemplate.opsForValue().get("user:123:loginStatus"); + +// 消息发送 + +// 获取返回结果 + +// 获取实时数据 +``` + +实现了一个探测接口,用于获取 接入探测信息。虽然它能够完成任务,但在可维护性、性能、安全性、日志记录等方面仍有一些优化空间。以下是对该代码的优化建议以及优化后的代码: + +## 优化建议: + +### 日志增强: + +目前的日志输出过于简单,特别是请求参数部分,可能导致日志过于冗长。应该合理处理日志级别和敏感信息。 + +### 异常处理改进: + +异常捕获过于宽泛,捕获了所有异常。可以根据具体情况捕获特定的异常类型,例如 JsonProcessingException。 + +### 方法拆分: + +info() 方法中的逻辑比较复杂,特别是与时间格式化、数据封装相关的部分,可以拆分成单独的辅助方法,减少方法复杂度。 + +### 简化对象创建: + +ProbeInfo 对象的创建逻辑比较简单,直接使用构造函数来简化代码。不要重复设置相同属性。 + +### 配置文件值的优化: + +配置文件中获取 spring.application.name 值的方式可以优化。建议使用 @ConfigurationProperties 代替 @Value,可以更方便地管理配置。 + +### 线程安全: + +SimpleDateFormat 在多线程中是非线程安全的,考虑使用 `ThreadLocal` 来保证线程安全。 + + + + + + + + diff --git a/docs/java-up/mongodb-backup-restore.md b/docs/java-up/mongodb-backup-restore.md new file mode 100644 index 000000000..50c642017 --- /dev/null +++ b/docs/java-up/mongodb-backup-restore.md @@ -0,0 +1,234 @@ +--- +title: MongoDB备份与恢复 +author: 哪吒 +date: '2023-06-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## MongoDB备份与恢复 + +在生产环境中,数据备份是一项至关重要的工作。无论是系统故障、人为操作失误还是其他不可预见的情况,都可能导致数据丢失。因此,掌握MongoDB的备份与恢复技术对于保障数据安全至关重要。本文将详细介绍MongoDB的备份与恢复方法。 + +## MongoDB备份方法 + +MongoDB提供了多种备份方法,主要包括以下几种: + +### 1. mongodump备份 + +mongodump是MongoDB官方提供的备份工具,它可以导出MongoDB数据库中的数据为BSON格式文件。这是最常用的备份方法之一。 + +**基本语法:** + +```bash +mongodump --host= --port= --username= --password= --authenticationDatabase=admin --db= --collection= --out= +``` + +**参数说明:** + +- `--host`:MongoDB服务器地址,默认为localhost +- `--port`:MongoDB服务器端口,默认为27017 +- `--username`:用户名 +- `--password`:密码 +- `--authenticationDatabase`:认证数据库,通常为admin +- `--db`:要备份的数据库名称,如果不指定则备份所有数据库 +- `--collection`:要备份的集合名称,如果不指定则备份指定数据库中的所有集合 +- `--out`:备份文件输出目录 + +**示例:** + +```bash +# 备份整个MongoDB实例 +mongodump --host=127.0.0.1 --port=27017 --out=/backup/mongodb/full + +# 备份特定数据库 +mongodump --host=127.0.0.1 --port=27017 --db=mydb --out=/backup/mongodb/mydb + +# 备份特定集合 +mongodump --host=127.0.0.1 --port=27017 --db=mydb --collection=users --out=/backup/mongodb/mydb_users +``` + +### 2. 文件系统快照备份 + +对于使用WiredTiger存储引擎的MongoDB(MongoDB 3.2及以上版本的默认引擎),可以使用文件系统快照进行备份。这种方法需要先锁定数据库以确保数据一致性,然后对数据文件目录进行快照。 + +**步骤:** + +1. 连接到MongoDB并锁定数据库: + +```javascript +db.fsyncLock() +``` + +2. 使用文件系统工具创建数据目录的快照 + +3. 解锁数据库: + +```javascript +db.fsyncUnlock() +``` + +### 3. MongoDB Atlas备份 + +如果你使用MongoDB Atlas云服务,它提供了自动备份功能,可以设置备份策略和保留期限。 + +## MongoDB恢复方法 + +### 1. mongorestore恢复 + +mongorestore是与mongodump配套的恢复工具,用于将mongodump创建的备份数据恢复到MongoDB数据库中。 + +**基本语法:** + +```bash +mongorestore --host= --port= --username= --password= --authenticationDatabase=admin --db= --collection= +``` + +**参数说明:** + +- `--host`:MongoDB服务器地址 +- `--port`:MongoDB服务器端口 +- `--username`:用户名 +- `--password`:密码 +- `--authenticationDatabase`:认证数据库 +- `--db`:要恢复的数据库名称 +- `--collection`:要恢复的集合名称 +- ``:备份文件目录 + +**示例:** + +```bash +# 恢复整个备份 +mongorestore --host=127.0.0.1 --port=27017 /backup/mongodb/full + +# 恢复特定数据库 +mongorestore --host=127.0.0.1 --port=27017 --db=mydb /backup/mongodb/mydb + +# 恢复特定集合 +mongorestore --host=127.0.0.1 --port=27017 --db=mydb --collection=users /backup/mongodb/mydb/users.bson +``` + +### 2. 从文件系统快照恢复 + +如果使用文件系统快照进行备份,恢复过程如下: + +1. 停止MongoDB服务 +2. 删除或重命名现有数据目录 +3. 从快照中恢复数据目录 +4. 启动MongoDB服务 + +## 备份策略最佳实践 + +### 1. 定期备份 + +根据数据重要性和变化频率,制定合适的备份计划。对于关键业务数据,建议每天至少备份一次。 + +### 2. 备份验证 + +定期验证备份数据的完整性和可用性,确保在需要时能够成功恢复。 + +### 3. 异地备份 + +将备份数据存储在与生产环境不同的物理位置,以防止因自然灾害等导致的数据丢失。 + +### 4. 自动化备份 + +使用脚本或任务调度工具(如cron)自动执行备份任务,减少人为干预和错误。 + +**示例备份脚本:** + +```bash +#!/bin/bash + +# 设置变量 +BACKUP_DIR="/backup/mongodb/$(date +%Y%m%d)" +MONGO_HOST="127.0.0.1" +MONGO_PORT="27017" +MONGO_DB="mydb" + +# 创建备份目录 +mkdir -p $BACKUP_DIR + +# 执行备份 +mongodump --host=$MONGO_HOST --port=$MONGO_PORT --db=$MONGO_DB --out=$BACKUP_DIR + +# 压缩备份文件 +tar -zcvf $BACKUP_DIR.tar.gz $BACKUP_DIR + +# 删除原始备份目录 +rm -rf $BACKUP_DIR + +# 保留最近30天的备份,删除更早的备份 +find /backup/mongodb/ -name "*.tar.gz" -type f -mtime +30 -delete +``` + +## 在Java应用中实现备份与恢复 + +除了使用命令行工具,我们还可以在Java应用中集成MongoDB的备份与恢复功能。 + +### 使用Java执行mongodump和mongorestore + +```java +import java.io.IOException; + +public class MongoDBBackupRestore { + + public static void performBackup(String host, int port, String dbName, String outputDir) { + try { + ProcessBuilder pb = new ProcessBuilder( + "mongodump", + "--host", host, + "--port", String.valueOf(port), + "--db", dbName, + "--out", outputDir + ); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode == 0) { + System.out.println("备份成功:" + outputDir); + } else { + System.err.println("备份失败,退出码:" + exitCode); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + public static void performRestore(String host, int port, String dbName, String backupDir) { + try { + ProcessBuilder pb = new ProcessBuilder( + "mongorestore", + "--host", host, + "--port", String.valueOf(port), + "--db", dbName, + backupDir + ); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode == 0) { + System.out.println("恢复成功:" + backupDir); + } else { + System.err.println("恢复失败,退出码:" + exitCode); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + // 执行备份 + performBackup("localhost", 27017, "mydb", "/backup/mongodb/" + System.currentTimeMillis()); + + // 执行恢复 + // performRestore("localhost", 27017, "mydb", "/backup/mongodb/1623765432123"); + } +} +``` + +## 总结 + +MongoDB的备份与恢复是数据库管理中不可或缺的一部分。通过合理的备份策略和恢复方法,可以有效降低数据丢失的风险,确保业务的连续性和数据的安全性。在实际应用中,应根据具体需求选择适合的备份方式,并定期验证备份的有效性。 + +对于大型生产环境,建议结合多种备份方法,如定期的完整备份、增量备份以及实时的操作日志备份,构建全面的数据保护体系。同时,将备份流程自动化,并建立监控机制,及时发现和解决备份过程中的问题。 diff --git a/docs/jobPro/wechat-customer-service.md b/docs/jobPro/wechat-customer-service.md new file mode 100644 index 000000000..f0ec875e1 --- /dev/null +++ b/docs/jobPro/wechat-customer-service.md @@ -0,0 +1,1340 @@ +--- +title: 微信小程序API客服消息完全指南 +author: 哪吒 +date: '2023-06-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# 微信小程序API客服消息完全指南 + +## 目录 + +- [微信小程序API客服消息完全指南](#微信小程序api客服消息完全指南) + - [目录](#目录) + - [1. 客服消息概述](#1-客服消息概述) + - [1.1 什么是客服消息](#11-什么是客服消息) + - [1.2 客服消息的应用场景](#12-客服消息的应用场景) + - [1.3 客服消息与订阅消息的区别](#13-客服消息与订阅消息的区别) + - [2. 客服消息接入前准备](#2-客服消息接入前准备) + - [2.1 配置要求与权限申请](#21-配置要求与权限申请) + - [2.2 开通客服消息](#22-开通客服消息) + - [2.3 配置消息推送URL](#23-配置消息推送url) + - [3. 客服消息接口详解](#3-客服消息接口详解) + - [3.1 接收客服消息](#31-接收客服消息) + - [3.2 发送客服消息](#32-发送客服消息) + - [3.3 消息加密与解密](#33-消息加密与解密) + - [4. 客服消息类型与格式](#4-客服消息类型与格式) + - [4.1 文本消息](#41-文本消息) + - [4.2 图片消息](#42-图片消息) + - [4.3 图文链接消息](#43-图文链接消息) + - [4.4 小程序卡片消息](#44-小程序卡片消息) + - [4.5 其他消息类型](#45-其他消息类型) + - [5. 客服消息实战开发](#5-客服消息实战开发) + - [5.1 后端接收与解析消息](#51-后端接收与解析消息) + - [5.2 后端发送客服消息](#52-后端发送客服消息) + - [5.3 前端客服会话入口](#53-前端客服会话入口) + - [5.4 自动回复机制实现](#54-自动回复机制实现) + - [6. 客服系统集成](#6-客服系统集成) + - [6.1 接入第三方客服系统](#61-接入第三方客服系统) + - [6.2 自建客服系统](#62-自建客服系统) + - [6.3 客服系统功能设计](#63-客服系统功能设计) + - [7. 客服消息安全与合规](#7-客服消息安全与合规) + - [7.1 内容安全审核](#71-内容安全审核) + - [7.2 敏感信息处理](#72-敏感信息处理) + - [7.3 合规注意事项](#73-合规注意事项) + - [8. 客服消息最佳实践](#8-客服消息最佳实践) + - [8.1 高效客服流程设计](#81-高效客服流程设计) + - [8.2 智能客服机器人](#82-智能客服机器人) + - [8.3 性能优化建议](#83-性能优化建议) + - [9. 常见问题与解决方案](#9-常见问题与解决方案) + - [9.1 消息发送失败问题](#91-消息发送失败问题) + - [9.2 消息接收异常处理](#92-消息接收异常处理) + - [9.3 其他常见问题](#93-其他常见问题) + - [总结](#总结) + +## 1. 客服消息概述 + +### 1.1 什么是客服消息 + +客服消息是微信小程序提供的一种重要消息能力,允许小程序与用户进行双向即时通信。通过客服消息,开发者可以为用户提供咨询、反馈、售后等服务,大大提升用户体验和服务质量。 + +**客服消息的核心特点:** + +- **双向通信**:支持小程序与用户的双向即时对话 +- **多种消息类型**:支持文本、图片、图文链接、小程序卡片等多种消息类型 +- **会话保持**:在一定时间内保持会话状态,便于持续沟通 +- **无需用户授权**:用户主动发起会话后,开发者可在48小时内回复消息,无需额外授权 +- **灵活集成**:可对接自有客服系统或第三方客服平台 + +### 1.2 客服消息的应用场景 + +客服消息在小程序中有广泛的应用场景,主要包括: + +1. **售前咨询**:用户可以在购买前咨询产品详情、规格、价格等信息 +2. **售后服务**:处理用户在购买后的问题、退换货申请等 +3. **订单跟踪**:用户可以通过客服消息查询订单状态、物流信息 +4. **投诉反馈**:收集用户的意见和建议,处理投诉 +5. **活动通知**:向用户推送促销活动、新品上市等信息 +6. **智能客服**:结合AI技术,实现自动问答和智能推荐 +7. **社群运营**:通过客服消息维护用户关系,提升用户粘性 + +### 1.3 客服消息与订阅消息的区别 + +| 特性 | 客服消息 | 订阅消息 | +|-----|---------|--------| +| **通信方式** | 双向通信 | 单向通知 | +| **发起方式** | 用户主动发起或开发者回复 | 仅开发者发送 | +| **时效性** | 用户发起后48小时内有效 | 用户订阅后一定时间内有效 | +| **授权要求** | 用户进入会话即视为授权 | 需要用户明确订阅授权 | +| **消息类型** | 多种类型(文本、图片、链接等) | 固定模板格式 | +| **使用场景** | 即时沟通、咨询服务 | 通知提醒、状态更新 | +| **频率限制** | 较为宽松 | 严格限制发送频率 | + +## 2. 客服消息接入前准备 + +### 2.1 配置要求与权限申请 + +在接入客服消息功能前,需要满足以下条件: + +1. **小程序类目要求**: + - 客服消息功能对小程序类目有一定要求,需确认您的小程序类目是否支持 + - 大部分商家类目、服务类目、工具类目都支持客服消息 + +2. **服务器要求**: + - 需要有一个支持HTTPS的服务器用于接收和处理消息 + - 服务器需要有固定的公网IP和域名 + - 服务器需要支持80端口和443端口 + +3. **开发者资质**: + - 小程序需已通过微信认证 + - 开发者需具备基本的后端开发能力 + +### 2.2 开通客服消息 + +1. **登录微信公众平台**: + - 访问[微信公众平台](https://mp.weixin.qq.com/)并登录小程序管理账号 + +2. **进入客服消息设置**: + - 在左侧菜单栏选择「设置」→「客服」→「客服消息」 + +3. **开启客服消息**: + - 点击「开启」按钮启用客服消息功能 + - 如果按钮为灰色,请检查小程序类目是否支持客服消息 + +### 2.3 配置消息推送URL + +1. **设置服务器配置**: + - 在左侧菜单栏选择「开发」→「开发设置」→「消息推送」 + - 点击「修改」按钮进入配置页面 + +2. **填写服务器信息**: + - URL:填写接收消息的服务器地址,必须以`https://`或`http://`开头 + - Token:自定义的令牌,用于验证消息的确来自微信服务器 + - EncodingAESKey:消息加密密钥,可点击「随机生成」按钮获取 + - 消息加密方式:建议选择「安全模式」 + +3. **验证服务器配置**: + - 点击「提交」按钮,微信服务器会向您配置的URL发送一个验证请求 + - 您的服务器需要按照微信的验证规则正确响应,才能完成配置 + +```javascript +// 服务器验证示例代码(Node.js) +const crypto = require('crypto'); + +app.get('/wechat/message', (req, res) => { + const { signature, timestamp, nonce, echostr } = req.query; + const token = 'your_token'; // 替换为您设置的Token + + // 1. 将token、timestamp、nonce三个参数进行字典序排序 + const array = [token, timestamp, nonce].sort(); + + // 2. 将三个参数字符串拼接成一个字符串进行sha1加密 + const tempStr = array.join(''); + const hashCode = crypto.createHash('sha1').update(tempStr).digest('hex'); + + // 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 + if (hashCode === signature) { + res.send(echostr); + } else { + res.send('验证失败'); + } +}); +``` + +## 3. 客服消息接口详解 + +### 3.1 接收客服消息 + +当用户向小程序发送消息时,微信服务器会将消息推送到您配置的URL。您需要在服务器端接收并处理这些消息。 + +**消息接收流程**: + +1. 用户在小程序中发送消息 +2. 微信服务器将消息推送到您配置的URL +3. 您的服务器接收并解析消息 +4. 根据消息内容进行相应处理 + +**接收消息示例代码**: + +```javascript +// 使用Express框架接收消息(Node.js) +const express = require('express'); +const bodyParser = require('body-parser'); +const xml2js = require('xml2js'); +const app = express(); + +// 解析XML格式的请求体 +app.use(bodyParser.text({ type: 'text/xml' })); + +app.post('/wechat/message', (req, res) => { + // 解析XML消息 + xml2js.parseString(req.body, { explicitArray: false }, (err, result) => { + if (err) { + console.error('解析XML失败:', err); + res.send('success'); // 即使解析失败也要返回success,避免微信服务器重试 + return; + } + + const message = result.xml; + console.log('收到消息:', message); + + // 根据消息类型处理 + switch (message.MsgType) { + case 'text': + // 处理文本消息 + console.log(`收到文本消息:${message.Content},来自:${message.FromUserName}`); + break; + case 'image': + // 处理图片消息 + console.log(`收到图片消息,图片URL:${message.PicUrl},来自:${message.FromUserName}`); + break; + // 处理其他类型消息... + } + + // 必须回复success,否则微信服务器会重试 + res.send('success'); + }); +}); + +app.listen(3000, () => { + console.log('服务器运行在 http://localhost:3000'); +}); +``` + +### 3.2 发送客服消息 + +开发者可以通过调用微信提供的接口,主动向用户发送客服消息。在用户发起会话后的48小时内,开发者可以不限次数地向用户发送消息。 + +**发送消息流程**: + +1. 获取接口调用凭证(access_token) +2. 构造消息内容 +3. 调用发送消息接口 +4. 处理接口返回结果 + +**获取access_token示例代码**: + +```javascript +const axios = require('axios'); + +async function getAccessToken() { + const appId = 'your_app_id'; // 替换为您的小程序AppID + const appSecret = 'your_app_secret'; // 替换为您的小程序AppSecret + + try { + const response = await axios.get( + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` + ); + + if (response.data && response.data.access_token) { + return response.data.access_token; + } else { + throw new Error('获取access_token失败:' + JSON.stringify(response.data)); + } + } catch (error) { + console.error('获取access_token出错:', error); + throw error; + } +} +``` + +**发送文本消息示例代码**: + +```javascript +async function sendTextMessage(openId, text) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`, + { + touser: openId, + msgtype: 'text', + text: { + content: text + } + } + ); + + if (response.data && response.data.errcode === 0) { + console.log('消息发送成功'); + return true; + } else { + console.error('消息发送失败:', response.data); + return false; + } + } catch (error) { + console.error('发送消息出错:', error); + return false; + } +} + +// 使用示例 +sendTextMessage('user_open_id', '您好,这是一条客服消息!'); +``` + +### 3.3 消息加密与解密 + +在安全模式下,微信服务器推送的消息会进行加密,开发者需要先解密才能获取原始消息内容。 + +**解密消息示例代码**: + +```javascript +const crypto = require('crypto'); + +function decryptMessage(encryptedMsg, encodingAESKey, appId) { + // 将encodingAESKey转换为Buffer + const aesKey = Buffer.from(encodingAESKey + '=', 'base64'); + + // 解密 + const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, aesKey.slice(0, 16)); + decipher.setAutoPadding(false); + + let decrypted = decipher.update(encryptedMsg, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + // 去除填充 + const pad = decrypted.charCodeAt(decrypted.length - 1); + decrypted = decrypted.slice(0, -pad); + + // 获取消息内容 + const content = decrypted.slice(16); + const xmlLength = content.slice(0, 4).readUInt32BE(0); + const xmlContent = content.slice(4, xmlLength + 4); + const fromAppId = content.slice(xmlLength + 4); + + // 校验AppID + if (fromAppId.toString() !== appId) { + throw new Error('AppID校验失败'); + } + + return xmlContent.toString(); +} +``` + +## 4. 客服消息类型与格式 + +### 4.1 文本消息 + +文本消息是最基本的消息类型,用于发送纯文本内容。 + +**接收文本消息格式**: + +```xml + + + + 1548831860 + + + 22222222222222222 + +``` + +**发送文本消息格式**: + +```json +{ + "touser": "oxxxxxxxxxxxxxxxxxxxx", + "msgtype": "text", + "text": { + "content": "这是回复的文本消息" + } +} +``` + +### 4.2 图片消息 + +图片消息用于发送图片内容,需要先上传图片获取media_id。 + +**接收图片消息格式**: + +```xml + + + + 1548831860 + + + + 22222222222222222 + +``` + +**发送图片消息格式**: + +```json +{ + "touser": "oxxxxxxxxxxxxxxxxxxxx", + "msgtype": "image", + "image": { + "media_id": "MEDIA_ID" + } +} +``` + +**上传图片获取media_id示例代码**: + +```javascript +const fs = require('fs'); +const FormData = require('form-data'); +const axios = require('axios'); + +async function uploadImage(imagePath) { + try { + const accessToken = await getAccessToken(); + + const form = new FormData(); + form.append('media', fs.createReadStream(imagePath)); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${accessToken}&type=image`, + form, + { + headers: form.getHeaders() + } + ); + + if (response.data && response.data.media_id) { + console.log('图片上传成功,media_id:', response.data.media_id); + return response.data.media_id; + } else { + console.error('图片上传失败:', response.data); + return null; + } + } catch (error) { + console.error('上传图片出错:', error); + return null; + } +} +``` + +### 4.3 图文链接消息 + +图文链接消息用于发送带有标题、描述、图片和链接的消息。 + +**发送图文链接消息格式**: + +```json +{ + "touser": "oxxxxxxxxxxxxxxxxxxxx", + "msgtype": "link", + "link": { + "title": "图文链接标题", + "description": "图文链接描述", + "url": "https://example.com/article", + "thumb_url": "https://example.com/thumb.jpg" + } +} +``` + +### 4.4 小程序卡片消息 + +小程序卡片消息用于推广其他小程序页面。 + +**发送小程序卡片消息格式**: + +```json +{ + "touser": "oxxxxxxxxxxxxxxxxxxxx", + "msgtype": "miniprogrampage", + "miniprogrampage": { + "title": "小程序卡片标题", + "pagepath": "pages/index/index", + "thumb_media_id": "THUMB_MEDIA_ID" + } +} +``` + +### 4.5 其他消息类型 + +除了上述常用消息类型外,客服消息还支持以下类型: + +1. **语音消息**:发送语音文件 +2. **视频消息**:发送视频文件 +3. **音乐消息**:发送音乐链接 +4. **图文消息(news)**:发送多条图文信息 +5. **菜单消息**:发送带有菜单选项的消息 + +## 5. 客服消息实战开发 + +### 5.1 后端接收与解析消息 + +在实际开发中,我们需要构建一个完整的后端服务来接收和处理客服消息。以下是一个基于Node.js和Express的完整示例: + +```javascript +const express = require('express'); +const bodyParser = require('body-parser'); +const xml2js = require('xml2js'); +const crypto = require('crypto'); +const app = express(); + +// 配置信息 +const config = { + token: 'your_token', + encodingAESKey: 'your_encoding_aes_key', + appId: 'your_app_id' +}; + +// 解析XML格式的请求体 +app.use(bodyParser.text({ type: 'text/xml' })); + +// 验证服务器配置 +app.get('/wechat/message', (req, res) => { + const { signature, timestamp, nonce, echostr } = req.query; + + // 1. 将token、timestamp、nonce三个参数进行字典序排序 + const array = [config.token, timestamp, nonce].sort(); + + // 2. 将三个参数字符串拼接成一个字符串进行sha1加密 + const tempStr = array.join(''); + const hashCode = crypto.createHash('sha1').update(tempStr).digest('hex'); + + // 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 + if (hashCode === signature) { + res.send(echostr); + } else { + res.send('验证失败'); + } +}); + +// 接收客服消息 +app.post('/wechat/message', (req, res) => { + // 解析XML消息 + xml2js.parseString(req.body, { explicitArray: false }, async (err, result) => { + if (err) { + console.error('解析XML失败:', err); + res.send('success'); + return; + } + + const message = result.xml; + console.log('收到消息:', message); + + try { + // 根据消息类型处理 + switch (message.MsgType) { + case 'text': + // 处理文本消息 + await handleTextMessage(message); + break; + case 'image': + // 处理图片消息 + await handleImageMessage(message); + break; + // 处理其他类型消息... + default: + console.log(`收到未知类型消息:${message.MsgType}`); + } + } catch (error) { + console.error('处理消息出错:', error); + } + + // 必须回复success,否则微信服务器会重试 + res.send('success'); + }); +}); + +// 处理文本消息 +async function handleTextMessage(message) { + const { FromUserName, Content } = message; + console.log(`收到文本消息:${Content},来自:${FromUserName}`); + + // 简单的自动回复逻辑 + if (Content.includes('你好') || Content.includes('hello')) { + await sendTextMessage(FromUserName, '您好!有什么可以帮助您的吗?'); + } else if (Content.includes('价格') || Content.includes('多少钱')) { + await sendTextMessage(FromUserName, '我们的产品价格为99元起,详情可查看商品页面。'); + } else { + // 默认回复 + await sendTextMessage(FromUserName, `感谢您的留言:${Content},我们会尽快处理。`); + } +} + +// 处理图片消息 +async function handleImageMessage(message) { + const { FromUserName, PicUrl, MediaId } = message; + console.log(`收到图片消息,图片URL:${PicUrl},来自:${FromUserName}`); + + // 回复确认收到图片 + await sendTextMessage(FromUserName, '已收到您发送的图片,谢谢!'); +} + +// 启动服务器 +app.listen(3000, () => { + console.log('服务器运行在 http://localhost:3000'); +}); +``` + +### 5.2 后端发送客服消息 + +以下是一个完整的发送客服消息的工具类: + +```javascript +const axios = require('axios'); +const fs = require('fs'); +const FormData = require('form-data'); + +// 缓存access_token +let accessTokenCache = { + token: '', + expireTime: 0 +}; + +// 获取access_token +async function getAccessToken() { + const now = Date.now(); + + // 如果缓存的token未过期,直接返回 + if (accessTokenCache.token && accessTokenCache.expireTime > now) { + return accessTokenCache.token; + } + + const appId = 'your_app_id'; + const appSecret = 'your_app_secret'; + + try { + const response = await axios.get( + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` + ); + + if (response.data && response.data.access_token) { + // 缓存token,有效期设为7000秒(微信返回的是7200秒,留200秒余量) + accessTokenCache = { + token: response.data.access_token, + expireTime: now + 7000 * 1000 + }; + return accessTokenCache.token; + } else { + throw new Error('获取access_token失败:' + JSON.stringify(response.data)); + } + } catch (error) { + console.error('获取access_token出错:', error); + throw error; + } +} + +// 发送文本消息 +async function sendTextMessage(openId, text) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`, + { + touser: openId, + msgtype: 'text', + text: { + content: text + } + } + ); + + if (response.data && response.data.errcode === 0) { + console.log('文本消息发送成功'); + return true; + } else { + console.error('文本消息发送失败:', response.data); + return false; + } + } catch (error) { + console.error('发送文本消息出错:', error); + return false; + } +} + +// 上传图片获取media_id +async function uploadImage(imagePath) { + try { + const accessToken = await getAccessToken(); + + const form = new FormData(); + form.append('media', fs.createReadStream(imagePath)); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${accessToken}&type=image`, + form, + { + headers: form.getHeaders() + } + ); + + if (response.data && response.data.media_id) { + console.log('图片上传成功,media_id:', response.data.media_id); + return response.data.media_id; + } else { + console.error('图片上传失败:', response.data); + return null; + } + } catch (error) { + console.error('上传图片出错:', error); + return null; + } +} + +// 发送图片消息 +async function sendImageMessage(openId, mediaId) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`, + { + touser: openId, + msgtype: 'image', + image: { + media_id: mediaId + } + } + ); + + if (response.data && response.data.errcode === 0) { + console.log('图片消息发送成功'); + return true; + } else { + console.error('图片消息发送失败:', response.data); + return false; + } + } catch (error) { + console.error('发送图片消息出错:', error); + return false; + } +} + +// 发送图文链接消息 +async function sendLinkMessage(openId, title, description, url, thumbUrl) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`, + { + touser: openId, + msgtype: 'link', + link: { + title, + description, + url, + thumb_url: thumbUrl + } + } + ); + + if (response.data && response.data.errcode === 0) { + console.log('图文链接消息发送成功'); + return true; + } else { + console.error('图文链接消息发送失败:', response.data); + return false; + } + } catch (error) { + console.error('发送图文链接消息出错:', error); + return false; + } +} + +// 发送小程序卡片消息 +async function sendMiniProgramMessage(openId, title, pagePath, thumbMediaId) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`, + { + touser: openId, + msgtype: 'miniprogrampage', + miniprogrampage: { + title, + pagepath: pagePath, + thumb_media_id: thumbMediaId + } + } + ); + + if (response.data && response.data.errcode === 0) { + console.log('小程序卡片消息发送成功'); + return true; + } else { + console.error('小程序卡片消息发送失败:', response.data); + return false; + } + } catch (error) { + console.error('发送小程序卡片消息出错:', error); + return false; + } +} + +module.exports = { + getAccessToken, + sendTextMessage, + uploadImage, + sendImageMessage, + sendLinkMessage, + sendMiniProgramMessage +}; +``` + +### 5.3 前端客服会话入口 + +在小程序前端,可以通过以下方式创建客服会话入口: + +1. **使用客服会话按钮组件**: + +```html + + +``` + +2. **通过API打开客服会话**: + +```javascript +// JS文件 +Page({ + // 打开客服会话 + openCustomerService() { + wx.openCustomerServiceChat({ + extInfo: { url: 'https://work.weixin.qq.com/kfid/kfc7c39738c3d6e6f21' }, + corpId: 'ww9xxxxxxxxxxxxxxx', + success(res) { + console.log('打开客服会话成功', res); + }, + fail(err) { + console.error('打开客服会话失败', err); + } + }); + } +}); +``` + +3. **在页面中嵌入客服消息卡片**: + +```html + + +``` + +### 5.4 自动回复机制实现 + +实现一个简单的自动回复机制,可以根据用户消息内容进行智能回复: + +```javascript +// 自动回复处理函数 +async function handleAutoReply(message) { + const { FromUserName, Content, MsgType } = message; + + // 如果不是文本消息,发送默认回复 + if (MsgType !== 'text') { + await sendTextMessage(FromUserName, '您好,我们已收到您的消息,客服将尽快回复您。'); + return; + } + + // 关键词匹配规则 + const keywordRules = [ + { keywords: ['你好', 'hello', 'hi'], reply: '您好!很高兴为您服务,请问有什么可以帮助您的?' }, + { keywords: ['价格', '多少钱', '费用'], reply: '我们的产品价格为99元起,详情可查看商品页面或咨询在线客服。' }, + { keywords: ['配送', '快递', '物流', '发货'], reply: '我们默认使用顺丰快递,一般下单后1-3天内发货,节假日可能会有延迟。' }, + { keywords: ['退款', '退货', '换货', '售后'], reply: '如需办理退换货,请在订单详情页申请售后,或提供订单号给客服处理。' }, + { keywords: ['优惠券', '折扣', '促销'], reply: '目前正在进行满100减20的促销活动,新用户还可领取88元优惠券。' }, + { keywords: ['地址', '门店', '实体店'], reply: '我们的线下门店地址可在"关于我们"页面查看,也可以直接在小程序内使用"附近门店"功能。' } + ]; + + // 检查是否匹配关键词 + for (const rule of keywordRules) { + if (rule.keywords.some(keyword => Content.includes(keyword))) { + await sendTextMessage(FromUserName, rule.reply); + return; + } + } + + // 默认回复 + await sendTextMessage(FromUserName, '感谢您的咨询,我们已收到您的消息,客服将尽快回复您。如有紧急问题,可拨打客服热线:400-123-4567。'); +} +``` + +## 6. 客服系统集成 + +### 6.1 接入第三方客服系统 + +微信小程序可以与第三方客服系统集成,常见的第三方客服系统包括: + +1. **美洽客服** +2. **智齿客服** +3. **微信官方客服** +4. **环信客服** +5. **网易七鱼** + +以美洽客服为例,集成步骤如下: + +1. **注册美洽账号**: + - 访问美洽官网注册账号 + - 创建应用并获取企业ID + +2. **在小程序中集成SDK**: + - 下载美洽小程序SDK + - 将SDK文件添加到小程序项目中 + +3. **初始化SDK**: + +```javascript +// app.js +App({ + onLaunch: function() { + // 初始化美洽SDK + const meiqiaConfig = { + enterpriseId: 'your_enterprise_id', + projectType: 1, // 1代表小程序 + oe: false, // 是否为开放平台应用 + platformType: 'wechat' // 平台类型 + }; + + // 引入美洽SDK + const Meiqia = require('./utils/meiqia-sdk.js'); + this.meiqia = new Meiqia(meiqiaConfig); + + // 设置用户信息 + this.meiqia.setClientInfo({ + id: 'user_id', // 用户ID + name: 'user_name', // 用户名称 + avatar: 'user_avatar_url', // 用户头像 + // 其他自定义信息 + customInfo: { + gender: '男', + age: '25', + level: 'VIP' + } + }); + } +}); +``` + +4. **添加客服入口**: + +```html + + +``` + +```javascript +// JS文件 +Page({ + openMeiqiaChat() { + const app = getApp(); + app.meiqia.openChat({ + success: function() { + console.log('打开客服对话框成功'); + }, + fail: function(error) { + console.error('打开客服对话框失败', error); + } + }); + } +}); +``` + +### 6.2 自建客服系统 + +如果需要更高的定制化,可以自建客服系统。自建客服系统的核心组件包括: + +1. **消息接收与发送模块**: + - 接收用户消息 + - 发送客服回复 + +2. **客服工作台**: + - 客服人员操作界面 + - 会话管理 + - 用户信息展示 + +3. **消息路由系统**: + - 根据规则将用户消息分配给合适的客服 + +4. **自动回复系统**: + - 关键词自动回复 + - 智能问答机器人 + +5. **数据统计与分析**: + - 会话量统计 + - 客服绩效分析 + - 用户满意度评价 + +### 6.3 客服系统功能设计 + +一个完善的客服系统应包含以下功能: + +1. **多渠道接入**: + - 小程序客服消息 + - 公众号消息 + - 网页聊天 + - 电话语音 + +2. **智能分配**: + - 根据客服技能分配 + - 根据用户等级分配 + - 负载均衡分配 + +3. **会话管理**: + - 会话排队 + - 会话转接 + - 会话结束与评价 + +4. **知识库系统**: + - 常见问题库 + - 快捷回复模板 + - 标准答案库 + +5. **客服监控**: + - 实时会话监控 + - 客服状态监控 + - 质量抽检 + +6. **数据分析**: + - 会话量趋势 + - 问题类型分布 + - 客服绩效分析 + - 用户满意度分析 + +## 7. 客服消息安全与合规 + +### 7.1 内容安全审核 + +为了确保客服消息内容的安全性,应实施以下措施: + +1. **关键词过滤**: + - 建立敏感词库 + - 对用户消息进行实时过滤 + - 对客服回复进行审核 + +2. **内容审核API**: + - 接入微信内容安全API + - 接入第三方内容审核服务 + +```javascript +// 内容安全检测示例 +async function checkContentSafety(content) { + try { + const accessToken = await getAccessToken(); + + const response = await axios.post( + `https://api.weixin.qq.com/wxa/msg_sec_check?access_token=${accessToken}`, + { + content: content + } + ); + + if (response.data && response.data.errcode === 0) { + return { safe: true }; + } else if (response.data.errcode === 87014) { + return { safe: false, reason: '内容含有违法违规信息' }; + } else { + console.error('内容检测失败:', response.data); + return { safe: false, reason: '内容检测失败' }; + } + } catch (error) { + console.error('内容安全检测出错:', error); + return { safe: false, reason: '内容检测异常' }; + } +} + +// 使用示例 +async function sendSafeMessage(openId, content) { + const safetyResult = await checkContentSafety(content); + + if (safetyResult.safe) { + await sendTextMessage(openId, content); + } else { + console.error('消息内容不安全,已阻止发送:', safetyResult.reason); + // 可以通知管理员或记录日志 + } +} +``` + +### 7.2 敏感信息处理 + +在处理客服消息时,需要注意保护用户敏感信息: + +1. **个人信息脱敏**: + - 手机号码:显示为 138****1234 + - 身份证号:显示为 110101********1234 + - 银行卡号:显示为 6222 **** **** 1234 + +2. **数据存储安全**: + - 敏感信息加密存储 + - 设置访问权限控制 + - 定期清理历史消息 + +3. **传输安全**: + - 使用HTTPS加密传输 + - 避免在日志中记录敏感信息 + +### 7.3 合规注意事项 + +在使用客服消息功能时,需要注意以下合规事项: + +1. **获取用户授权**: + - 虽然客服消息不需要额外授权,但在收集用户其他信息时需要明确告知并获取授权 + +2. **遵守微信规则**: + - 不得发送违法违规内容 + - 不得发送营销垃圾信息 + - 不得骚扰用户 + +3. **隐私政策**: + - 在小程序中提供清晰的隐私政策 + - 说明客服消息的使用方式和数据处理方式 + +4. **记录保存**: + - 保存客服消息记录,以备查询和审计 + - 遵守相关行业的数据保存要求 + +## 8. 客服消息最佳实践 + +### 8.1 高效客服流程设计 + +设计高效的客服流程可以提升用户体验和客服效率: + +1. **分级响应机制**: + - 第一级:自动回复和智能机器人 + - 第二级:普通客服人员 + - 第三级:专业技术支持或主管 + +2. **预设场景流程**: + - 咨询场景:产品信息 → 价格咨询 → 下单引导 + - 售后场景:问题描述 → 解决方案 → 满意度确认 + - 投诉场景:问题记录 → 道歉安抚 → 解决方案 → 补偿措施 + +3. **服务时间管理**: + - 明确客服服务时间 + - 非服务时间自动回复处理方式 + - 紧急问题escalation机制 + +### 8.2 智能客服机器人 + +结合AI技术,可以实现智能客服机器人: + +1. **基于规则的机器人**: + - 关键词匹配 + - 决策树对话流程 + - 模板回复 + +2. **基于NLP的机器人**: + - 意图识别 + - 实体提取 + - 上下文理解 + - 情感分析 + +3. **混合模式**: + - 机器人优先处理 + - 无法解决时转人工客服 + - 人工客服辅助训练机器人 + +```javascript +// 简单的NLP意图识别示例 +function identifyIntent(message) { + // 意图分类规则 + const intents = [ + { name: 'greeting', patterns: ['你好', '您好', 'hello', 'hi', '嗨', '哈喽'] }, + { name: 'farewell', patterns: ['再见', '拜拜', '拜', 'bye', '下次见'] }, + { name: 'thanks', patterns: ['谢谢', '感谢', '多谢', 'thanks', 'thank you'] }, + { name: 'product_inquiry', patterns: ['产品', '商品', '货品', '有什么', '卖什么'] }, + { name: 'price_inquiry', patterns: ['价格', '多少钱', '费用', '价钱', '报价'] }, + { name: 'order_status', patterns: ['订单', '物流', '发货', '到哪了', '快递'] }, + { name: 'complaint', patterns: ['投诉', '不满', '差评', '退款', '不好'] }, + { name: 'help', patterns: ['帮助', '怎么用', '使用方法', '教程', '指南'] } + ]; + + // 检查消息是否匹配某个意图 + for (const intent of intents) { + if (intent.patterns.some(pattern => message.includes(pattern))) { + return intent.name; + } + } + + // 默认意图 + return 'unknown'; +} + +// 根据意图生成回复 +function generateResponse(intent, userName) { + const responses = { + greeting: [`您好${userName},欢迎咨询,有什么可以帮助您的吗?`, `你好${userName},很高兴为您服务!`], + farewell: [`再见${userName},祝您有愉快的一天!`, `感谢您的咨询,再见!`], + thanks: [`不客气,为您服务是我的荣幸!`, `您的满意是我们最大的追求!`], + product_inquiry: [`我们有多种产品系列,您可以在首页查看详细分类,或者告诉我您想了解哪类产品?`, `您好,请问您对哪类产品感兴趣呢?我可以为您推荐。`], + price_inquiry: [`我们的产品价格从99元到999元不等,具体价格请查看商品详情页,您需要了解哪款产品的价格呢?`, `不同产品价格不同,请问您想了解哪款产品的价格呢?`], + order_status: [`请提供您的订单号,我可以为您查询最新物流状态。`, `您好,需要您提供订单号才能查询物流信息哦。`], + complaint: [`非常抱歉给您带来不便,请详细描述您遇到的问题,我们会尽快处理。`, `我们对您的体验感到抱歉,请告诉我具体情况,我会立即跟进解决。`], + help: [`您可以在"使用帮助"页面查看详细教程,或者告诉我您需要哪方面的帮助?`, `我们提供全面的使用指南,您可以在小程序首页底部找到"帮助中心",或者直接告诉我您的疑问。`], + unknown: [`抱歉,我不太理解您的意思,能否换个方式描述您的问题?`, `您的问题可能需要专业客服处理,正在为您转接人工客服...`] + }; + + // 随机选择一个回复 + const intentResponses = responses[intent] || responses.unknown; + return intentResponses[Math.floor(Math.random() * intentResponses.length)]; +} + +// 智能客服处理流程 +async function handleIntelligentService(message) { + const { FromUserName, Content } = message; + const userName = await getUserName(FromUserName) || ''; // 获取用户昵称的函数 + + // 识别用户意图 + const intent = identifyIntent(Content); + console.log(`用户意图识别:${intent}`); + + // 生成回复 + const reply = generateResponse(intent, userName); + + // 发送回复 + await sendTextMessage(FromUserName, reply); + + // 如果是未知意图或投诉,记录需要人工跟进 + if (intent === 'unknown' || intent === 'complaint') { + await recordForHumanFollowUp(FromUserName, Content, intent); + } +} +``` + +### 8.3 性能优化建议 + +为了确保客服消息系统的高性能,可以采取以下措施: + +1. **消息队列**: + - 使用消息队列处理高并发消息 + - 避免直接同步处理消息 + +2. **缓存机制**: + - 缓存access_token + - 缓存常用回复内容 + - 缓存用户信息 + +3. **异步处理**: + - 接收消息立即返回success + - 异步处理消息内容 + - 异步发送回复 + +4. **水平扩展**: + - 设计支持多实例部署的架构 + - 使用负载均衡分发请求 + +5. **监控告警**: + - 监控消息处理延迟 + - 监控API调用频率和限额 + - 设置异常告警机制 + +## 9. 常见问题与解决方案 + +### 9.1 消息发送失败问题 + +**问题1:发送消息返回45015错误** + +- **原因**:超出48小时消息时限 +- **解决方案**: + - 确保在用户发起会话后的48小时内回复 + - 使用订阅消息作为替代方案 + - 引导用户重新发起会话 + +**问题2:发送消息返回40001错误** + +- **原因**:access_token无效或已过期 +- **解决方案**: + - 刷新access_token + - 检查access_token获取逻辑 + - 实现token自动刷新机制 + +**问题3:发送消息返回45047错误** + +- **原因**:客服接口调用频率超过限制 +- **解决方案**: + - 控制发送频率 + - 实现请求排队机制 + - 避免短时间内大量发送消息 + +### 9.2 消息接收异常处理 + +**问题1:接收不到用户消息** + +- **原因**:服务器配置不正确或URL不可访问 +- **解决方案**: + - 检查服务器URL是否可以正常访问 + - 确认服务器配置的Token是否正确 + - 检查服务器响应是否正确返回"success" + - 查看服务器日志,排查可能的错误 + +**问题2:消息重复接收** + +- **原因**:服务器未正确响应或响应超时 +- **解决方案**: + - 确保服务器在接收到消息后立即返回"success" + - 优化服务器处理逻辑,避免长时间处理 + - 实现消息去重机制,根据MsgId判断重复消息 + +**问题3:消息内容解析错误** + +- **原因**:XML解析失败或格式不正确 +- **解决方案**: + - 使用可靠的XML解析库 + - 增加错误处理和异常捕获 + - 记录原始消息内容,便于排查问题 + +### 9.3 其他常见问题 + +**问题1:如何判断用户是否已关注公众号?** + +- **解决方案**: + - 通过接收事件消息中的Event字段判断 + - 用户首次关注会收到"subscribe"事件 + - 取消关注会收到"unsubscribe"事件 + - 也可通过微信用户API查询关注状态 + +**问题2:如何处理多客服协作?** + +- **解决方案**: + - 实现客服分组和权限管理 + - 设计会话转接机制 + - 建立客服协作规范 + - 使用第三方客服系统的多客服功能 + +**问题3:如何提高客服效率?** + +- **解决方案**: + - 预设快捷回复模板 + - 建立完善的知识库 + - 实现智能推荐回复 + - 优化客服工作台界面 + - 提供客服培训和绩效激励 + +## 总结 + +微信小程序客服消息是连接用户与开发者的重要桥梁,通过本文的详细介绍,我们了解了客服消息的基本概念、接入流程、接口使用、消息类型、实战开发、系统集成、安全合规以及最佳实践等方面的内容。 + +通过合理利用客服消息功能,开发者可以: + +1. **提升用户体验**:及时响应用户需求,解决用户问题 +2. **增强用户粘性**:通过良好的沟通建立用户信任和忠诚度 +3. **提高转化率**:解答用户疑问,促进购买决策 +4. **收集用户反馈**:了解用户需求和痛点,持续优化产品 +5. **降低运营成本**:通过自动化和智能化降低人工客服成本 + +在实际应用中,建议开发者根据自身业务特点和用户需求,选择合适的客服消息解决方案,并不断优化完善,为用户提供更好的服务体验。 \ No newline at end of file diff --git a/docs/jobPro/wechat-miniprogram-guide.md b/docs/jobPro/wechat-miniprogram-guide.md new file mode 100644 index 000000000..614e4bdd4 --- /dev/null +++ b/docs/jobPro/wechat-miniprogram-guide.md @@ -0,0 +1,2527 @@ +--- +title: 微信小程序开发完全指南:从入门到精通 +author: 哪吒 +date: '2023-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# 微信小程序开发完全指南:从入门到精通 + +## 目录 + +- [微信小程序开发完全指南:从入门到精通](#微信小程序开发完全指南从入门到精通) + - [目录](#目录) + - [1. 微信小程序基础](#1-微信小程序基础) + - [1.1 什么是微信小程序](#11-什么是微信小程序) + - [1.2 小程序与H5、原生App的对比](#12-小程序与h5原生app的对比) + - [1.3 开发环境搭建](#13-开发环境搭建) + - [1.4 小程序项目结构](#14-小程序项目结构) + - [2. 小程序开发基础](#2-小程序开发基础) + - [2.1 WXML与WXSS](#21-wxml与wxss) + - [2.2 小程序生命周期](#22-小程序生命周期) + - [2.3 数据绑定与事件处理](#23-数据绑定与事件处理) + - [2.4 常用组件与API](#24-常用组件与api) + - [3. 小程序登录与授权](#3-小程序登录与授权) + - [3.1 登录流程详解](#31-登录流程详解) + - [3.2 获取用户信息](#32-获取用户信息) + - [3.3 授权最佳实践](#33-授权最佳实践) + - [4. 小程序支付功能实现](#4-小程序支付功能实现) + - [4.1 支付流程概述](#41-支付流程概述) + - [4.2 后端接口开发](#42-后端接口开发) + - [4.3 前端支付实现](#43-前端支付实现) + - [4.4 支付结果处理](#44-支付结果处理) + - [5. 订阅消息推送](#5-订阅消息推送) + - [5.1 消息模板申请](#51-消息模板申请) + - [5.2 前端订阅实现](#52-前端订阅实现) + - [5.3 后端推送实现](#53-后端推送实现) + - [6. 小程序发布与上线](#6-小程序发布与上线) + - [6.1 代码审核与发布](#61-代码审核与发布) + - [6.2 自动化部署CI](#62-自动化部署ci) + - [6.3 版本管理策略](#63-版本管理策略) + - [7. uni-app跨平台开发](#7-uni-app跨平台开发) + - [7.1 uni-app简介](#71-uni-app简介) + - [7.2 开发与打包流程](#72-开发与打包流程) + - [7.3 多端适配策略](#73-多端适配策略) + - [8. 小程序性能优化](#8-小程序性能优化) + - [8.1 启动性能优化](#81-启动性能优化) + - [8.2 渲染性能优化](#82-渲染性能优化) + - [8.3 网络请求优化](#83-网络请求优化) + - [9. 小程序安全最佳实践](#9-小程序安全最佳实践) + - [9.1 数据安全](#91-数据安全) + - [9.2 通信安全](#92-通信安全) + - [9.3 敏感信息处理](#93-敏感信息处理) + - [10. 高级开发技巧](#10-高级开发技巧) + - [10.1 分包加载](#101-分包加载) + - [10.2 自定义组件](#102-自定义组件) + - [10.3 云开发应用](#103-云开发应用) + - [总结](#总结) + +## 1. 微信小程序基础 + +### 1.1 什么是微信小程序 + +微信小程序是一种不需要下载安装即可使用的应用,它实现了「触手可及」的梦想,用户扫一扫或搜一下即可打开应用。小程序提供了一个简单、高效的应用开发框架和丰富的组件及API,帮助开发者在微信中开发具有原生体验的应用。 + +**小程序的主要特点:** + +- **无需安装**:用户无需下载安装,扫码即用 +- **快速访问**:「用完即走」的理念,无需卸载 +- **原生体验**:接近原生App的用户体验 +- **微信生态**:可以直接调用微信的支付、登录、地图等能力 +- **体积小**:单个小程序包大小限制为2MB(主包) + +### 1.2 小程序与H5、原生App的对比 + +| 特性 | 微信小程序 | H5 | 原生App | +|-----|---------|-----|--------| +| **安装方式** | 无需安装 | 无需安装 | 需要安装 | +| **更新方式** | 自动更新 | 自动更新 | 需要手动更新 | +| **性能表现** | 接近原生 | 一般 | 最佳 | +| **设备能力** | 部分可用 | 有限 | 完全可用 | +| **开发成本** | 中等 | 低 | 高 | +| **跨平台** | 仅微信生态 | 全平台 | 需要分别开发 | +| **上架审核** | 需要审核 | 无需审核 | 需要审核 | +| **用户获取** | 微信生态内 | 全网可访问 | 应用商店 | + +### 1.3 开发环境搭建 + +1. **安装微信开发者工具** + - 访问[微信开发者工具官网](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)下载最新版本 + - 根据操作系统选择对应版本(Windows/macOS) + - 安装并启动开发者工具 + +2. **注册小程序账号** + - 访问[微信公众平台](https://mp.weixin.qq.com/)注册小程序账号 + - 完成邮箱验证和身份认证 + - 获取AppID(小程序的唯一标识) + +3. **创建小程序项目** + - 打开微信开发者工具 + - 选择「项目」→「新建项目」 + - 填入AppID、项目名称和目录 + - 选择「建立普通快速启动模板」 + - 点击「确定」创建项目 + +### 1.4 小程序项目结构 + +一个典型的小程序项目结构如下: + +``` +├── app.js // 小程序逻辑 +├── app.json // 小程序公共配置 +├── app.wxss // 小程序公共样式表 +├── project.config.json // 项目配置文件 +├── sitemap.json // 微信索引配置文件 +├── pages/ // 页面文件夹 +│ └── index/ // index页面 +│ ├── index.js // 页面逻辑 +│ ├── index.wxml // 页面结构 +│ ├── index.wxss // 页面样式表 +│ └── index.json // 页面配置 +├── utils/ // 工具函数 +├── components/ // 自定义组件 +├── images/ // 图片资源 +└── miniprogram_npm/ // npm包目录 +``` + +**核心文件说明:** + +- **app.js**:小程序入口文件,包含全局的生命周期函数和全局数据 +- **app.json**:全局配置,包括页面路径、窗口表现、网络超时、底部tab等 +- **app.wxss**:全局样式,作用于每一个页面 +- **pages目录**:存放所有页面,每个页面由四个文件组成(js、wxml、wxss、json) + +## 2. 小程序开发基础 + +### 2.1 WXML与WXSS + +WXML(WeiXin Markup Language)是微信小程序的标记语言,类似于HTML,但具有数据绑定等特性。WXSS(WeiXin Style Sheets)是小程序的样式语言,类似于CSS。 + +**WXML示例:** + +```html + + {{message}} + + +``` + +**WXSS示例:** + +```css +.container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx; +} + +text { + font-size: 18px; + margin-bottom: 20rpx; +} +``` + +**WXML特点:** + +- 数据绑定:使用 `{{变量}}` 语法 +- 列表渲染:使用 `wx:for` 指令 +- 条件渲染:使用 `wx:if`、`wx:elif`、`wx:else` 指令 +- 模板引用:使用 `template` 和 `include` 标签 + +**WXSS特点:** + +- 尺寸单位:rpx(responsive pixel)自适应单位 +- 样式导入:使用 `@import` 语句 +- 选择器:支持类选择器、ID选择器、元素选择器等 +- 全局样式与局部样式:app.wxss为全局样式,页面wxss为局部样式 + +### 2.2 小程序生命周期 + +小程序有两种生命周期:应用生命周期和页面生命周期。 + +**应用生命周期(App):** + +```javascript +App({ + onLaunch: function(options) { + // 小程序初始化完成时触发,全局只触发一次 + console.log('App onLaunch', options); + }, + onShow: function(options) { + // 小程序启动,或从后台进入前台显示时触发 + console.log('App onShow', options); + }, + onHide: function() { + // 小程序从前台进入后台时触发 + console.log('App onHide'); + }, + onError: function(msg) { + // 小程序发生脚本错误或API调用报错时触发 + console.log('App onError', msg); + }, + globalData: { + // 全局数据 + userInfo: null + } +}); +``` + +**页面生命周期(Page):** + +```javascript +Page({ + data: { + // 页面数据 + message: 'Hello World' + }, + onLoad: function(options) { + // 页面加载时触发,一个页面只会调用一次 + console.log('Page onLoad', options); + }, + onShow: function() { + // 页面显示/切入前台时触发 + console.log('Page onShow'); + }, + onReady: function() { + // 页面初次渲染完成时触发,一个页面只会调用一次 + console.log('Page onReady'); + }, + onHide: function() { + // 页面隐藏/切入后台时触发 + console.log('Page onHide'); + }, + onUnload: function() { + // 页面卸载时触发 + console.log('Page onUnload'); + }, + onPullDownRefresh: function() { + // 用户下拉刷新时触发 + console.log('Page onPullDownRefresh'); + }, + onReachBottom: function() { + // 用户上拉触底时触发 + console.log('Page onReachBottom'); + }, + onShareAppMessage: function() { + // 用户点击右上角分享时触发 + return { + title: '自定义分享标题', + path: '/pages/index/index' + }; + }, + // 自定义方法 + changeMessage: function() { + this.setData({ + message: 'Hello WeChat MiniProgram!' + }); + } +}); +``` + +### 2.3 数据绑定与事件处理 + +**数据绑定:** + +小程序使用 `{{}}` 语法进行数据绑定,将JS中的数据展示在页面上。 + +```html + +{{message}} +{{user.name}} +{{a + b}} + {{c}} = {{a + b + c}} +``` + +```javascript +// JS +Page({ + data: { + message: 'Hello', + user: { name: 'John' }, + a: 1, + b: 2, + c: 3 + } +}); +``` + +**事件处理:** + +小程序通过 `bind` 或 `catch` 前缀绑定事件处理函数。 + +```html + + +触摸区域 +``` + +```javascript +// JS +Page({ + handleTap: function(e) { + console.log('按钮被点击', e); + }, + handleTouchStart: function(e) { + console.log('触摸开始', e); + }, + handleTouchMove: function(e) { + console.log('触摸移动', e); + // catch前缀会阻止事件冒泡 + } +}); +``` + +**常用事件:** + +- `tap`:点击事件 +- `input`:输入事件 +- `change`:改变事件 +- `submit`:表单提交事件 +- `touchstart/touchmove/touchend`:触摸事件 + +### 2.4 常用组件与API + +**基础组件:** + +- `view`:视图容器,类似div +- `text`:文本组件 +- `button`:按钮组件 +- `image`:图片组件 +- `input`:输入框组件 +- `scroll-view`:可滚动视图区域 +- `swiper`:滑块视图容器 +- `navigator`:页面链接 + +**常用API:** + +- 路由导航:`wx.navigateTo`、`wx.redirectTo`、`wx.switchTab` +- 网络请求:`wx.request` +- 数据存储:`wx.setStorage`、`wx.getStorage` +- 界面交互:`wx.showToast`、`wx.showModal`、`wx.showLoading` +- 用户信息:`wx.getUserProfile` +- 支付功能:`wx.requestPayment` + +**API使用示例:** + +```javascript +// 发起网络请求 +wx.request({ + url: 'https://api.example.com/data', + method: 'GET', + data: { id: 1 }, + success: function(res) { + console.log('请求成功', res.data); + }, + fail: function(err) { + console.error('请求失败', err); + } +}); + +// 显示提示框 +wx.showToast({ + title: '操作成功', + icon: 'success', + duration: 2000 +}); + +// 页面跳转 +wx.navigateTo({ + url: '/pages/detail/detail?id=1' +}); +``` + +## 3. 小程序登录与授权 + +### 3.1 登录流程详解 + +微信小程序的登录流程主要包括以下步骤: + +1. **前端调用 `wx.login()` 获取临时登录凭证 code** +2. **将 code 发送到开发者服务器** +3. **开发者服务器通过 code 向微信接口服务获取 openid 和 session_key** +4. **开发者服务器自定义登录态,返回自定义登录态给小程序** +5. **小程序保存登录态,后续请求携带登录态** + +**登录流程代码示例:** + +```javascript +// 小程序端 +wx.login({ + success: function(res) { + if (res.code) { + // 发送 res.code 到后端 + wx.request({ + url: 'https://api.example.com/login', + method: 'POST', + data: { + code: res.code + }, + success: function(result) { + // 保存登录态 + wx.setStorageSync('token', result.data.token); + console.log('登录成功'); + } + }); + } else { + console.error('登录失败:' + res.errMsg); + } + } +}); +``` + +**后端处理流程(Node.js示例):** + +```javascript +const axios = require('axios'); + +async function login(code) { + // 小程序 appId 和 appSecret + const appId = 'your_appid'; + const appSecret = 'your_appsecret'; + + try { + // 请求微信接口获取 openid 和 session_key + const result = await axios.get( + `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` + ); + + const { openid, session_key } = result.data; + + // 生成自定义登录态 token + const token = generateToken(openid); + + // 保存用户信息和 session_key 到数据库 + await saveUserInfo(openid, session_key, token); + + return { token }; + } catch (error) { + console.error('登录失败', error); + throw error; + } +} + +// 生成 token 的函数 +function generateToken(openid) { + // 实际应用中应使用更安全的方式生成 token + return `token_${openid}_${Date.now()}`; +} + +// 保存用户信息到数据库 +async function saveUserInfo(openid, session_key, token) { + // 实现数据库存储逻辑 + // ... +} +``` + +### 3.2 获取用户信息 + +从2021年4月13日起,微信调整了获取用户信息的接口,废弃了 `wx.getUserInfo` 接口,改为使用 `wx.getUserProfile` 接口,且必须由用户主动触发。 + +**获取用户信息示例:** + +```html + + +``` + +```javascript +// JS +Page({ + getUserProfile: function() { + wx.getUserProfile({ + desc: '用于完善会员资料', // 声明获取用户个人信息后的用途 + success: (res) => { + const userInfo = res.userInfo; + console.log('用户信息', userInfo); + + // 保存用户信息 + this.setData({ + userInfo: userInfo, + hasUserInfo: true + }); + + // 可以将用户信息发送到后端保存 + this.updateUserInfo(userInfo); + }, + fail: (err) => { + console.error('获取用户信息失败', err); + } + }); + }, + + updateUserInfo: function(userInfo) { + const token = wx.getStorageSync('token'); + + wx.request({ + url: 'https://api.example.com/user/update', + method: 'POST', + header: { + 'Authorization': `Bearer ${token}` + }, + data: userInfo, + success: function(res) { + console.log('用户信息更新成功'); + } + }); + } +}); +``` + +### 3.3 授权最佳实践 + +**授权策略:** + +1. **分步授权**:根据功能需要逐步请求授权,避免一次性请求多个权限 +2. **明确用途**:在请求授权时明确说明用途,提高用户接受度 +3. **优雅降级**:用户拒绝授权时提供替代方案,不影响核心功能使用 +4. **授权引导**:对于关键权限,提供图文并茂的引导说明 + +**授权状态检查:** + +```javascript +wx.getSetting({ + success: function(res) { + if (res.authSetting['scope.userInfo']) { + // 已经授权获取用户信息 + console.log('已授权用户信息'); + } + + if (res.authSetting['scope.userLocation']) { + // 已经授权获取位置信息 + console.log('已授权位置信息'); + } + } +}); +``` + +**打开设置页引导用户授权:** + +```javascript +wx.showModal({ + title: '提示', + content: '需要您的位置权限才能提供附近服务,是否前往设置?', + success: function(res) { + if (res.confirm) { + wx.openSetting({ + success: function(settingRes) { + console.log('设置页操作结果', settingRes); + } + }); + } + } +}); +``` + +## 4. 小程序支付功能实现 + +### 4.1 支付流程概述 + +微信小程序支付流程主要包括以下步骤: + +1. **用户在小程序内选择商品下单** +2. **小程序调用后端接口创建订单** +3. **后端调用微信支付统一下单API获取支付参数** +4. **后端将支付参数返回给小程序** +5. **小程序调用 `wx.requestPayment()` 发起支付** +6. **用户完成支付操作** +7. **微信服务器通知商户支付结果** +8. **商户系统更新订单状态** + +![支付流程图](./img_10.png) + +### 4.2 后端接口开发 + +**统一下单接口(Node.js示例):** + +```javascript +const crypto = require('crypto'); +const axios = require('axios'); + +async function createOrder(req, res) { + try { + const { openid, totalFee, body, orderId } = req.body; + + // 微信支付参数 + const appid = 'your_appid'; + const mchid = 'your_mchid'; // 商户号 + const mchKey = 'your_mch_key'; // 商户密钥 + const notifyUrl = 'https://api.example.com/pay/notify'; // 支付结果通知地址 + const nonceStr = generateNonceStr(); + const timestamp = Math.floor(Date.now() / 1000).toString(); + + // 构建统一下单请求参数 + const params = { + appid, + mch_id: mchid, + nonce_str: nonceStr, + body, + out_trade_no: orderId, + total_fee: totalFee, // 单位:分 + spbill_create_ip: req.ip, + notify_url: notifyUrl, + trade_type: 'JSAPI', + openid + }; + + // 签名 + const sign = generateSign(params, mchKey); + params.sign = sign; + + // 将参数转为XML格式 + const xmlData = jsonToXml(params); + + // 调用微信统一下单接口 + const result = await axios.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + xmlData, + { headers: { 'Content-Type': 'text/xml' } } + ); + + // 解析XML响应 + const responseData = xmlToJson(result.data); + + if (responseData.return_code === 'SUCCESS' && responseData.result_code === 'SUCCESS') { + // 生成小程序调用支付需要的参数 + const payParams = { + appId: appid, + timeStamp: timestamp, + nonceStr, + package: `prepay_id=${responseData.prepay_id}`, + signType: 'MD5' + }; + + // 再次签名 + payParams.paySign = generateSign(payParams, mchKey); + + // 删除appId,避免小程序端签名校验失败 + delete payParams.appId; + + res.json({ + code: 0, + message: '创建订单成功', + data: payParams + }); + } else { + throw new Error(responseData.return_msg || responseData.err_code_des || '创建订单失败'); + } + } catch (error) { + console.error('创建订单失败', error); + res.json({ + code: -1, + message: error.message || '创建订单失败' + }); + } +} + +// 生成随机字符串 +function generateNonceStr() { + return Math.random().toString(36).substr(2, 15); +} + +// 生成签名 +function generateSign(params, key) { + // 按字典序排序参数 + const sortedParams = Object.keys(params).sort().reduce((result, key) => { + result[key] = params[key]; + return result; + }, {}); + + // 拼接参数字符串 + let stringA = ''; + for (const k in sortedParams) { + if (sortedParams[k] !== '' && sortedParams[k] !== undefined) { + stringA += `${k}=${sortedParams[k]}&`; + } + } + + // 拼接key + const stringSignTemp = stringA + `key=${key}`; + + // MD5加密并转为大写 + return crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase(); +} + +// 支付结果通知处理 +async function handlePayNotify(req, res) { + try { + // 解析微信支付结果通知 + const notifyData = xmlToJson(req.body); + + // 验证签名 + const sign = notifyData.sign; + delete notifyData.sign; + const calculatedSign = generateSign(notifyData, 'your_mch_key'); + + if (calculatedSign !== sign) { + throw new Error('签名验证失败'); + } + + if (notifyData.return_code === 'SUCCESS' && notifyData.result_code === 'SUCCESS') { + // 支付成功,更新订单状态 + const orderId = notifyData.out_trade_no; + const transactionId = notifyData.transaction_id; + + // 更新订单状态逻辑 + await updateOrderStatus(orderId, transactionId); + + // 返回成功响应 + res.send(''); + } else { + throw new Error('支付失败'); + } + } catch (error) { + console.error('处理支付通知失败', error); + res.send(''); + } +} + +// 更新订单状态 +async function updateOrderStatus(orderId, transactionId) { + // 实现数据库更新逻辑 + // ... +} +``` + +### 4.3 前端支付实现 + +**小程序端发起支付:** + +```javascript +Page({ + data: { + goodsId: '', + goodsName: '', + price: 0 + }, + + onLoad: function(options) { + this.setData({ + goodsId: options.id + }); + + // 获取商品详情 + this.getGoodsDetail(options.id); + }, + + getGoodsDetail: function(goodsId) { + wx.request({ + url: `https://api.example.com/goods/${goodsId}`, + success: (res) => { + this.setData({ + goodsName: res.data.name, + price: res.data.price + }); + } + }); + }, + + // 创建订单并发起支付 + createOrder: function() { + // 显示加载中 + wx.showLoading({ + title: '正在创建订单' + }); + + // 获取用户openid(假设已经在登录时保存) + const openid = wx.getStorageSync('openid'); + const token = wx.getStorageSync('token'); + + // 创建订单参数 + const orderParams = { + openid, + goodsId: this.data.goodsId, + totalFee: this.data.price * 100, // 转为分 + body: this.data.goodsName, + orderId: `ORDER_${Date.now()}` // 实际应用中应由后端生成 + }; + + // 请求后端创建订单 + wx.request({ + url: 'https://api.example.com/order/create', + method: 'POST', + header: { + 'Authorization': `Bearer ${token}` + }, + data: orderParams, + success: (res) => { + wx.hideLoading(); + + if (res.data.code === 0) { + // 获取支付参数 + const payParams = res.data.data; + + // 调起微信支付 + this.requestPayment(payParams); + } else { + wx.showToast({ + title: res.data.message || '创建订单失败', + icon: 'none' + }); + } + }, + fail: () => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + } + }); + }, + + // 发起微信支付 + requestPayment: function(params) { + wx.requestPayment({ + timeStamp: params.timeStamp, + nonceStr: params.nonceStr, + package: params.package, + signType: params.signType, + paySign: params.paySign, + success: (res) => { + console.log('支付成功', res); + wx.showToast({ + title: '支付成功' + }); + + // 跳转到订单详情页 + setTimeout(() => { + wx.navigateTo({ + url: '/pages/order/detail?id=' + orderParams.orderId + }); + }, 1500); + }, + fail: (err) => { + console.log('支付失败', err); + if (err.errMsg === 'requestPayment:fail cancel') { + wx.showToast({ + title: '支付已取消', + icon: 'none' + }); + } else { + wx.showToast({ + title: '支付失败,请重试', + icon: 'none' + }); + } + } + }); + } +}); +``` + +### 4.4 支付结果处理 + +**支付成功后的业务处理:** + +1. **前端处理**: + - 显示支付成功提示 + - 跳转到订单详情页 + - 更新本地订单状态 + +2. **后端处理**: + - 接收微信支付结果通知 + - 验证签名和支付状态 + - 更新订单状态 + - 触发后续业务流程(发货、记录等) + +**订单查询接口(用于前端主动查询订单状态):** + +```javascript +// 后端实现 +async function queryOrder(req, res) { + try { + const { orderId } = req.params; + + // 从数据库查询订单 + const order = await Order.findOne({ orderId }); + + if (!order) { + return res.json({ + code: -1, + message: '订单不存在' + }); + } + + res.json({ + code: 0, + data: { + orderId: order.orderId, + status: order.status, + payTime: order.payTime, + amount: order.amount + } + }); + } catch (error) { + console.error('查询订单失败', error); + res.json({ + code: -1, + message: '查询订单失败' + }); + } +} + +// 小程序端实现 +Page({ + data: { + orderId: '', + orderStatus: '', + orderAmount: 0, + payTime: '' + }, + + onLoad: function(options) { + this.setData({ + orderId: options.id + }); + + this.queryOrderStatus(); + }, + + queryOrderStatus: function() { + const token = wx.getStorageSync('token'); + + wx.request({ + url: `https://api.example.com/order/${this.data.orderId}`, + header: { + 'Authorization': `Bearer ${token}` + }, + success: (res) => { + if (res.data.code === 0) { + this.setData({ + orderStatus: res.data.data.status, + orderAmount: res.data.data.amount / 100, // 转为元 + payTime: res.data.data.payTime + }); + } + } + }); + } +}); +``` + +## 5. 订阅消息推送 + +### 5.1 消息模板申请 + +微信小程序订阅消息需要先申请消息模板,步骤如下: + +1. **登录微信公众平台** +2. **进入「功能」→「订阅消息」** +3. **选择「公共模板库」或「自定义模板」** +4. **选择合适的模板并提交申请** +5. **审核通过后获取模板ID** + +![消息模板申请](./img_1.png) + +**订阅消息类型:** + +- **一次性订阅消息**:用户订阅后,开发者可在不限时间内下发一条消息 +- **长期订阅消息**:用户订阅一次后,开发者可长期下发多条消息(仅向特定类目开放) + +### 5.2 前端订阅实现 + +**订阅消息前端实现:** + +```html + + +``` + +```javascript +// JS +Page({ + data: { + tmplIds: ['模板ID1', '模板ID2', '模板ID3'] // 最多可添加3个模板ID + }, + + // 订阅消息 + subscribeMessage: function() { + wx.requestSubscribeMessage({ + tmplIds: this.data.tmplIds, + success: (res) => { + console.log('订阅结果', res); + + // 检查订阅结果 + let acceptedTemplates = []; + this.data.tmplIds.forEach(tmplId => { + if (res[tmplId] === 'accept') { + acceptedTemplates.push(tmplId); + } + }); + + if (acceptedTemplates.length > 0) { + // 将接受的模板ID发送到后端保存 + this.saveSubscription(acceptedTemplates); + + wx.showToast({ + title: '订阅成功', + icon: 'success' + }); + } else { + wx.showToast({ + title: '您拒绝了订阅', + icon: 'none' + }); + } + }, + fail: (err) => { + console.error('订阅失败', err); + wx.showToast({ + title: '订阅失败,请重试', + icon: 'none' + }); + } + }); + }, + + // 保存订阅信息到后端 + saveSubscription: function(acceptedTemplates) { + const token = wx.getStorageSync('token'); + + wx.request({ + url: 'https://api.example.com/subscription/save', + method: 'POST', + header: { + 'Authorization': `Bearer ${token}` + }, + data: { + templateIds: acceptedTemplates + }, + success: (res) => { + console.log('订阅信息保存成功', res.data); + } + }); + } +}); +``` + +### 5.3 后端推送实现 + +**发送订阅消息(Node.js示例):** + +```javascript +async function sendSubscribeMessage(req, res) { + try { + const { openid, templateId, data, page } = req.body; + + // 获取接口调用凭证 + const accessToken = await getAccessToken(); + + // 构建请求参数 + const params = { + touser: openid, + template_id: templateId, + page, // 可选,点击消息卡片后跳转的页面 + data // 模板数据 + }; + + // 调用发送订阅消息接口 + const result = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`, + params + ); + + if (result.data.errcode === 0) { + res.json({ + code: 0, + message: '发送成功' + }); + } else { + throw new Error(`发送失败:${result.data.errmsg}`); + } + } catch (error) { + console.error('发送订阅消息失败', error); + res.json({ + code: -1, + message: error.message || '发送失败' + }); + } +} + +// 获取接口调用凭证 +async function getAccessToken() { + const appId = 'your_appid'; + const appSecret = 'your_appsecret'; + + try { + const result = await axios.get( + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` + ); + + if (result.data.access_token) { + return result.data.access_token; + } else { + throw new Error('获取access_token失败'); + } + } catch (error) { + console.error('获取access_token失败', error); + throw error; + } +} + +// 使用示例 +// 审核结果通知 +app.post('/notify/audit-result', async (req, res) => { + try { + const { userId, auditResult, auditTime, title } = req.body; + + // 获取用户openid + const user = await User.findById(userId); + + if (!user || !user.openid) { + return res.json({ + code: -1, + message: '用户不存在或未绑定openid' + }); + } + + // 发送订阅消息 + await sendSubscribeMessage({ + body: { + openid: user.openid, + templateId: 'TknXJKdubbcKDpmLGDkvoxeDn-4ReD3dLDmHRHwSB_M', // 审核结果通知模板ID + page: 'pages/audit/detail?id=123', + data: { + thing1: { value: title }, // 问题标题 + phrase2: { value: auditResult }, // 审批结果 + time3: { value: auditTime } // 发起时间 + } + } + }, res); + } catch (error) { + console.error('发送审核结果通知失败', error); + res.json({ + code: -1, + message: '发送通知失败' + }); + } +}); +``` + +## 6. 小程序发布与上线 + +### 6.1 代码审核与发布 + +**小程序发布流程:** + +1. **代码上传**:在开发者工具中点击「上传」按钮 +2. **填写版本信息**:版本号、项目备注等 +3. **提交审核**:在微信公众平台提交代码审核 +4. **等待审核**:审核时间通常为1-3个工作日 +5. **发布上线**:审核通过后点击「发布」按钮 + +**版本管理最佳实践:** + +- **版本号规范**:采用语义化版本号(Semantic Versioning) +- **灰度发布**:先发布给部分用户,确认稳定后再全量发布 +- **版本日志**:详细记录每个版本的变更内容 +- **回滚机制**:保留回滚到之前版本的能力 + +### 6.2 自动化部署CI + +微信小程序支持使用 `miniprogram-ci` 工具实现自动化部署,可以集成到 CI/CD 流程中。 + +**安装 miniprogram-ci:** + +```bash +npm install --save-dev miniprogram-ci +``` + +**创建上传脚本(upload.js):** + +```javascript +const ci = require('miniprogram-ci'); +const path = require('path'); + +// 读取命令行参数 +const version = process.argv[2] || '1.0.0'; +const desc = process.argv[3] || '自动化部署'; + +(async () => { + try { + // 创建项目对象 + const project = new ci.Project({ + appid: 'your_appid', + type: 'miniProgram', + projectPath: path.resolve('./'), // 项目路径 + privateKeyPath: path.resolve('./private.key'), // 密钥路径 + ignores: ['node_modules/**/*'] + }); + + // 上传代码 + const uploadResult = await ci.upload({ + project, + version, + desc, + setting: { + es6: true, + minify: true, + autoPrefixWXSS: true + }, + onProgressUpdate: console.log + }); + + console.log('上传成功', uploadResult); + } catch (error) { + console.error('上传失败', error); + process.exit(1); + } +})(); +``` + +**集成到 package.json:** + +```json +{ + "scripts": { + "upload": "node upload.js", + "upload:prod": "node upload.js 1.0.0 '正式版发布'", + "upload:test": "node upload.js 1.0.0-beta '测试版发布'" + } +} +``` + +**集成到 GitHub Actions:** + +```yaml +# .github/workflows/deploy.yml +name: Deploy MiniProgram + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install dependencies + run: npm install + + - name: Create private key file + run: echo "${{ secrets.PRIVATE_KEY }}" > private.key + + - name: Upload to WeChat + run: npm run upload:prod +``` + +### 6.3 版本管理策略 + +**版本号规范:** + +- **主版本号**:不兼容的API修改 +- **次版本号**:向下兼容的功能性新增 +- **修订号**:向下兼容的问题修正 + +**发布策略:** + +1. **体验版**:内部测试使用 +2. **开发版**:开发过程中的版本 +3. **审核版**:提交审核的版本 +4. **线上版**:正式发布的版本 + +**多环境配置:** + +```javascript +// config.js +const env = wx.getAccountInfoSync().miniProgram.envVersion; + +const config = { + development: { + apiBaseUrl: 'https://dev-api.example.com', + debug: true + }, + trial: { + apiBaseUrl: 'https://test-api.example.com', + debug: true + }, + release: { + apiBaseUrl: 'https://api.example.com', + debug: false + } +}; + +// 根据环境选择配置 +const currentConfig = { + development: config.development, // 开发版 + trial: config.trial, // 体验版 + release: config.release // 正式版 +}[env] || config.development; + +export default currentConfig; +``` + +## 7. uni-app跨平台开发 + +### 7.1 uni-app简介 + +uni-app是一个使用Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)等多个平台。 + +**uni-app优势:** + +- **跨平台**:一套代码,多端发布 +- **高性能**:基于原生渲染,性能体验接近原生App +- **生态丰富**:支持NPM、支持小程序组件和SDK +- **开发效率**:HBuilderX内置相关环境,开箱即用 + +### 7.2 开发与打包流程 + +**环境准备:** + +1. **安装HBuilderX**: + - 下载并安装最新版HBuilderX:https://www.dcloud.io/hbuilderx.html + +2. **创建uni-app项目**: + - 打开HBuilderX → 文件 → 新建 → 项目 + - 选择uni-app模板 + - 填写项目名称和存储路径 + - 选择Vue版本和默认模板 + +**配置manifest.json:** + +```json +{ + "name": "应用名称", + "appid": "__UNI__XXXXXXX", + "description": "应用描述", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + "app-plus": { + "usingComponents": true, + "nvueCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + }, + "modules": {}, + "distribute": { + "android": { + "permissions": [], + "abiFilters": ["armeabi-v7a", "arm64-v8a"] + }, + "ios": { + "dSYMs": false + }, + "sdkConfigs": {} + } + }, + "quickapp": {}, + "mp-weixin": { + "appid": "wx开头的微信小程序appid", + "setting": { + "urlCheck": false + }, + "usingComponents": true + }, + "mp-alipay": { + "usingComponents": true + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + } +} +``` + +**打包发布流程:** + +1. **微信小程序打包**: + - HBuilderX → 发行 → 小程序-微信 + - 填写小程序名称和AppID + - 点击发行 + - 使用微信开发者工具上传代码 + +2. **Android打包**: + - HBuilderX → 发行 → 原生App-云打包 + - 选择Android平台 + - 配置证书信息 + - 点击发行 + +3. **iOS打包**: + - HBuilderX → 发行 → 原生App-云打包 + - 选择iOS平台 + - 配置证书和描述文件 + - 点击发行 + +### 7.3 多端适配策略 + +**条件编译:** + +uni-app提供了条件编译功能,可以根据不同平台编写特定代码。 + +```vue + + + + + +``` + +**API差异处理:** + +```javascript +// 封装平台差异的API +const platform = { + // 获取位置信息 + getLocation() { + return new Promise((resolve, reject) => { + uni.getLocation({ + type: 'gcj02', + success: res => { + resolve(res); + }, + fail: err => { + reject(err); + } + }); + }); + }, + + // 分享功能 + share(options) { + // #ifdef MP-WEIXIN + wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'] + }); + // #endif + + // #ifdef APP-PLUS + plus.share.sendWithSystem({ + type: 'text', + content: options.title, + href: options.path + }); + // #endif + } +}; + +export default platform; +``` + +**样式适配:** + +```css +/* 样式适配示例 */ + +/* 通用样式 */ +.container { + padding: 20rpx; +} + +/* 小程序样式 */ +/* #ifdef MP */ +.container { + background-color: #f8f8f8; +} +/* #endif */ + +/* App样式 */ +/* #ifdef APP-PLUS */ +.container { + background-color: #ffffff; +} +/* #endif */ + +/* iOS样式 */ +/* #ifdef APP-PLUS-IOS */ +.safe-area-inset { + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); +} +/* #endif */ + +/* Android样式 */ +/* #ifdef APP-PLUS-ANDROID */ +.android-only { + margin-top: 10rpx; +} +/* #endif */ +``` + +## 8. 小程序性能优化 + +### 8.1 启动性能优化 + +**减少启动时间的策略:** + +1. **控制代码包大小**: + - 分包加载 + - 压缩图片资源 + - 移除未使用的代码和资源 + +2. **优化app.js**: + - 减少app.js中的同步代码 + - 将非必要的初始化逻辑延迟执行 + - 使用异步API代替同步API + +3. **预加载策略**: + - 使用预加载接口提前下载分包 + - 使用周期性更新机制保持本地数据最新 + +**代码示例:** + +```javascript +// app.js 优化示例 +App({ + onLaunch: function() { + // 只保留必要的初始化逻辑 + this.initUserInfo(); + + // 延迟执行非关键任务 + setTimeout(() => { + this.initNonCriticalTasks(); + }, 2000); + + // 预加载分包 + wx.loadSubpackage({ + name: 'packageA', + success: function() { + console.log('分包预加载成功'); + }, + fail: function() { + console.log('分包预加载失败'); + } + }); + }, + + initUserInfo: function() { + // 用户信息初始化(关键任务) + const token = wx.getStorageSync('token'); + if (token) { + this.globalData.isLoggedIn = true; + } + }, + + initNonCriticalTasks: function() { + // 非关键任务初始化 + this.checkForUpdates(); + this.initAnalytics(); + this.loadRemoteConfig(); + }, + + globalData: { + isLoggedIn: false + } +}); +``` + +### 8.2 渲染性能优化 + +**提升渲染性能的策略:** + +1. **避免频繁setData**: + - 合并多次setData操作 + - 只传输必要数据 + - 避免传输大量数据 + +2. **使用懒加载**: + - 图片懒加载 + - 列表分页加载 + +3. **优化WXML结构**: + - 减少节点层级和数量 + - 使用block标签包裹 + - 适当使用wx:if和wx:for + +**代码示例:** + +```javascript +// 优化前 +Page({ + updateUI: function() { + this.setData({ a: 1 }); + this.setData({ b: 2 }); + this.setData({ c: 3 }); + } +}); + +// 优化后 +Page({ + updateUI: function() { + this.setData({ + a: 1, + b: 2, + c: 3 + }); + } +}); +``` + +```html + + + + {{item.title}} + {{item.desc}} + + {{item.time}} + {{item.author}} + + + + + + + + {{item.title}} + {{item.desc}} + + {{item.time}} + {{item.author}} + + + +``` + +### 8.3 网络请求优化 + +**网络请求优化策略:** + +1. **请求合并**: + - 合并多个相关请求 + - 使用批量接口 + +2. **缓存策略**: + - 缓存不常变化的数据 + - 使用Storage存储API响应 + - 设置合理的缓存过期时间 + +3. **预加载数据**: + - 提前请求可能需要的数据 + - 页面切换时预加载下一页数据 + +**代码示例:** + +```javascript +// 网络请求封装(带缓存) +const request = (options) => { + return new Promise((resolve, reject) => { + // 缓存key + const cacheKey = `cache_${options.url}_${JSON.stringify(options.data || {})}`; + + // 缓存过期时间(毫秒) + const expireTime = options.cache ? (options.expireTime || 5 * 60 * 1000) : 0; + + // 检查是否有缓存 + if (options.cache) { + const cacheData = wx.getStorageSync(cacheKey); + if (cacheData && cacheData.expire > Date.now()) { + resolve(cacheData.data); + return; + } + } + + // 发起请求 + wx.request({ + url: options.url, + method: options.method || 'GET', + data: options.data, + header: options.header || {}, + success: (res) => { + // 缓存结果 + if (options.cache && res.statusCode === 200) { + wx.setStorageSync(cacheKey, { + data: res.data, + expire: Date.now() + expireTime + }); + } + resolve(res.data); + }, + fail: (err) => { + reject(err); + } + }); + }); +}; + +// 使用示例 +Page({ + onLoad: function() { + // 带缓存的请求,缓存5分钟 + request({ + url: 'https://api.example.com/data', + cache: true, + expireTime: 5 * 60 * 1000 + }).then(data => { + this.setData({ list: data }); + }); + } +}); +``` + +## 9. 小程序安全最佳实践 + +### 9.1 数据安全 + +**保护数据安全的策略:** + +1. **敏感数据处理**: + - 避免在本地存储敏感信息 + - 使用加密存储必要的敏感数据 + - 及时清理不需要的数据 + +2. **数据传输安全**: + - 使用HTTPS进行网络通信 + - 对敏感数据进行加密传输 + - 实现请求签名机制 + +3. **防止数据泄露**: + - 避免在日志中打印敏感信息 + - 控制数据访问权限 + - 定期审计数据访问记录 + +**代码示例:** + +```javascript +// 数据加密存储 +const CryptoJS = require('crypto-js'); + +// 加密函数 +function encrypt(data, key) { + return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString(); +} + +// 解密函数 +function decrypt(ciphertext, key) { + const bytes = CryptoJS.AES.decrypt(ciphertext, key); + return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); +} + +// 安全存储敏感数据 +function secureStorage(key, data) { + const encryptionKey = 'your-secret-key'; // 实际应用中应使用更安全的密钥管理 + const encryptedData = encrypt(data, encryptionKey); + wx.setStorageSync(key, encryptedData); +} + +// 安全读取敏感数据 +function secureRead(key) { + const encryptionKey = 'your-secret-key'; + const encryptedData = wx.getStorageSync(key); + if (!encryptedData) return null; + return decrypt(encryptedData, encryptionKey); +} + +// 使用示例 +Page({ + saveUserData: function(userData) { + secureStorage('userData', userData); + }, + + getUserData: function() { + return secureRead('userData'); + }, + + clearUserData: function() { + wx.removeStorageSync('userData'); + } +}); +``` + +### 9.2 通信安全 + +**保障通信安全的策略:** + +1. **请求签名**: + - 对请求参数进行签名 + - 验证请求来源和完整性 + - 防止请求被篡改 + +2. **防重放攻击**: + - 请求添加时间戳 + - 实现请求有效期机制 + - 使用一次性nonce值 + +3. **传输加密**: + - 使用HTTPS协议 + - 敏感字段单独加密 + - 使用安全的加密算法 + +**代码示例:** + +```javascript +// 请求签名实现 +function generateSignature(params, secretKey) { + // 1. 按字典序排序参数 + const sortedKeys = Object.keys(params).sort(); + + // 2. 拼接参数字符串 + let signStr = ''; + sortedKeys.forEach(key => { + if (key !== 'sign' && params[key] !== undefined && params[key] !== null) { + signStr += `${key}=${params[key]}&`; + } + }); + + // 3. 添加密钥 + signStr += `key=${secretKey}`; + + // 4. MD5加密并转大写 + return CryptoJS.MD5(signStr).toString().toUpperCase(); +} + +// 发送带签名的请求 +function secureRequest(url, params, method = 'GET') { + // 添加公共参数 + const requestParams = { + ...params, + timestamp: Date.now(), + nonce: Math.random().toString(36).substr(2, 10), + appId: 'your-app-id' + }; + + // 生成签名 + const secretKey = 'your-secret-key'; // 实际应用中应使用更安全的密钥管理 + requestParams.sign = generateSignature(requestParams, secretKey); + + // 发送请求 + return new Promise((resolve, reject) => { + wx.request({ + url, + method, + data: requestParams, + success: res => { + if (res.statusCode === 200) { + resolve(res.data); + } else { + reject(new Error(`请求失败:${res.statusCode}`)); + } + }, + fail: err => { + reject(err); + } + }); + }); +} + +// 使用示例 +Page({ + getData: function() { + secureRequest('https://api.example.com/data', { + userId: '123', + action: 'query' + }).then(data => { + console.log('请求成功', data); + }).catch(err => { + console.error('请求失败', err); + }); + } +}); +``` + +### 9.3 敏感信息处理 + +**敏感信息处理策略:** + +1. **最小化收集**: + - 只收集必要的敏感信息 + - 明确告知用户收集目的 + - 提供隐私政策说明 + +2. **安全展示**: + - 敏感信息脱敏显示 + - 提供查看完整信息的选项 + - 避免截屏泄露风险 + +3. **安全传输与存储**: + - 传输过程加密 + - 存储时加密 + - 使用后及时清除 + +**代码示例:** + +```javascript +// 信息脱敏工具函数 +const maskUtil = { + // 手机号脱敏 + maskPhone: function(phone) { + if (!phone) return ''; + return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); + }, + + // 身份证号脱敏 + maskIdCard: function(idCard) { + if (!idCard) return ''; + return idCard.replace(/^(\d{4})\d{10}(\d{4})$/, '$1**********$2'); + }, + + // 姓名脱敏 + maskName: function(name) { + if (!name) return ''; + if (name.length <= 1) return name; + if (name.length === 2) return name.substr(0, 1) + '*'; + return name.substr(0, 1) + '*'.repeat(name.length - 2) + name.substr(-1); + }, + + // 银行卡号脱敏 + maskBankCard: function(cardNo) { + if (!cardNo) return ''; + return cardNo.replace(/^(\d{4})\d+(\d{4})$/, '$1 **** **** $2'); + }, + + // 邮箱脱敏 + maskEmail: function(email) { + if (!email) return ''; + const parts = email.split('@'); + if (parts.length !== 2) return email; + + const name = parts[0]; + const domain = parts[1]; + + let maskedName = ''; + if (name.length <= 2) { + maskedName = name.substr(0, 1) + '*'; + } else { + maskedName = name.substr(0, 2) + '*'.repeat(name.length - 2); + } + + return `${maskedName}@${domain}`; + } +}; + +// 使用示例 +Page({ + data: { + userInfo: { + name: '张三', + phone: '13812345678', + idCard: '110101199001011234', + email: 'zhangsan@example.com', + bankCard: '6222021234567890123' + }, + showFullInfo: false + }, + + onLoad: function() { + this.updateDisplayInfo(); + }, + + updateDisplayInfo: function() { + if (this.data.showFullInfo) { + this.setData({ + displayInfo: this.data.userInfo + }); + } else { + this.setData({ + displayInfo: { + name: maskUtil.maskName(this.data.userInfo.name), + phone: maskUtil.maskPhone(this.data.userInfo.phone), + idCard: maskUtil.maskIdCard(this.data.userInfo.idCard), + email: maskUtil.maskEmail(this.data.userInfo.email), + bankCard: maskUtil.maskBankCard(this.data.userInfo.bankCard) + } + }); + } + }, + + toggleInfoDisplay: function() { + this.setData({ + showFullInfo: !this.data.showFullInfo + }, () => { + this.updateDisplayInfo(); + }); + } +}); +``` + +## 10. 高级开发技巧 + +### 10.1 分包加载 + +分包加载是优化小程序启动速度的重要手段,可以将小程序划分为多个子包,在需要时按需加载。 + +**分包配置示例:** + +```json +// app.json +{ + "pages": [ + "pages/index/index", + "pages/logs/logs" + ], + "subpackages": [ + { + "root": "packageA", + "pages": [ + "pages/cat/cat", + "pages/dog/dog" + ] + }, + { + "root": "packageB", + "name": "pack2", + "pages": [ + "pages/apple/apple", + "pages/banana/banana" + ] + } + ], + "preloadRule": { + "pages/index/index": { + "network": "all", + "packages": ["packageA"] + } + } +} +``` + +**分包策略:** + +1. **按功能模块分包**:将不同功能模块划分到不同分包 +2. **按访问频率分包**:高频功能放主包,低频功能放分包 +3. **按加载时机分包**:启动必需的放主包,其他放分包 + +**分包预加载:** + +```javascript +// 手动预加载分包 +Page({ + onLoad: function() { + // 预加载分包 + wx.loadSubpackage({ + name: 'packageB', // 分包的name + success: function() { + console.log('分包加载成功'); + }, + fail: function() { + console.log('分包加载失败'); + } + }); + } +}); +``` + +### 10.2 自定义组件 + +自定义组件可以提高代码复用性和可维护性,是小程序开发中的重要技术。 + +**组件定义示例:** + +```javascript +// components/custom-card/custom-card.js +Component({ + properties: { + title: { + type: String, + value: '默认标题' + }, + content: { + type: String, + value: '' + }, + imageUrl: { + type: String, + value: '' + } + }, + data: { + isExpanded: false + }, + methods: { + toggleExpand: function() { + this.setData({ + isExpanded: !this.data.isExpanded + }); + + // 触发自定义事件 + this.triggerEvent('expand', { isExpanded: this.data.isExpanded }); + } + }, + lifetimes: { + attached: function() { + // 组件被添加到页面时执行 + console.log('组件attached'); + }, + detached: function() { + // 组件从页面中移除时执行 + console.log('组件detached'); + } + }, + pageLifetimes: { + show: function() { + // 页面被展示时执行 + console.log('页面show'); + }, + hide: function() { + // 页面被隐藏时执行 + console.log('页面hide'); + } + } +}); +``` + +```html + + + + {{title}} + {{isExpanded ? '↑' : '↓'}} + + + + + {{content}} + + + +``` + +```css +/* components/custom-card/custom-card.wxss */ +.card { + margin: 20rpx; + border-radius: 10rpx; + background-color: #fff; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx; + border-bottom: 1rpx solid #eee; +} + +.card-title { + font-size: 32rpx; + font-weight: bold; +} + +.card-icon { + font-size: 24rpx; + color: #999; +} + +.card-body { + padding: 20rpx; +} + +.card-image { + width: 100%; + height: 300rpx; + margin-bottom: 20rpx; +} + +.card-content { + font-size: 28rpx; + color: #666; + line-height: 1.5; +} + +.expanded { + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15); +} +``` + +**组件使用示例:** + +```json +// pages/index/index.json +{ + "usingComponents": { + "custom-card": "/components/custom-card/custom-card" + } +} +``` + +```html + + + + 这是通过slot插入的内容 + + +``` + +```javascript +// pages/index/index.js +Page({ + onCardExpand: function(e) { + console.log('卡片展开状态:', e.detail.isExpanded); + } +}); +``` + +### 10.3 云开发应用 + +微信小程序云开发提供了数据库、存储、云函数等能力,可以大幅简化后端开发。 + +**云开发初始化:** + +```javascript +// app.js +App({ + onLaunch: function() { + if (!wx.cloud) { + console.error('请使用 2.2.3 或以上的基础库以使用云能力'); + } else { + wx.cloud.init({ + env: 'your-env-id', // 云开发环境ID + traceUser: true // 是否将用户访问记录到云开发控制台 + }); + } + } +}); +``` + +**云数据库操作:** + +```javascript +// 添加数据 +function addData() { + const db = wx.cloud.database(); + db.collection('todos').add({ + data: { + title: '新的待办事项', + description: '这是一个示例待办事项', + done: false, + createTime: db.serverDate() + }, + success: function(res) { + console.log('添加成功,记录ID:', res._id); + }, + fail: function(err) { + console.error('添加失败', err); + } + }); +} + +// 查询数据 +function queryData() { + const db = wx.cloud.database(); + db.collection('todos') + .where({ + done: false + }) + .orderBy('createTime', 'desc') + .limit(10) + .get() + .then(res => { + console.log('查询结果', res.data); + }) + .catch(err => { + console.error('查询失败', err); + }); +} + +// 更新数据 +function updateData(id) { + const db = wx.cloud.database(); + db.collection('todos').doc(id).update({ + data: { + done: true, + updateTime: db.serverDate() + }, + success: function() { + console.log('更新成功'); + }, + fail: function(err) { + console.error('更新失败', err); + } + }); +} + +// 删除数据 +function deleteData(id) { + const db = wx.cloud.database(); + db.collection('todos').doc(id).remove({ + success: function() { + console.log('删除成功'); + }, + fail: function(err) { + console.error('删除失败', err); + } + }); +} +``` + +**云存储操作:** + +```javascript +// 上传文件 +function uploadFile() { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: function(res) { + const filePath = res.tempFilePaths[0]; + + // 上传至云存储 + wx.cloud.uploadFile({ + cloudPath: `images/${Date.now()}-${Math.floor(Math.random() * 1000)}.png`, // 云存储路径 + filePath: filePath, // 本地文件路径 + success: res => { + console.log('上传成功', res); + const fileID = res.fileID; + + // 可以将fileID保存到数据库 + saveFileInfo(fileID, filePath); + }, + fail: err => { + console.error('上传失败', err); + } + }); + } + }); +} + +// 保存文件信息到数据库 +function saveFileInfo(fileID, filePath) { + const db = wx.cloud.database(); + db.collection('files').add({ + data: { + fileID: fileID, + uploadTime: db.serverDate() + }, + success: function(res) { + console.log('文件信息保存成功'); + } + }); +} + +// 获取文件临时链接 +function getTempFileURL(fileID) { + wx.cloud.getTempFileURL({ + fileList: [fileID], + success: res => { + console.log('文件下载链接', res.fileList[0].tempFileURL); + }, + fail: err => { + console.error('获取链接失败', err); + } + }); +} + +// 删除文件 +function deleteFile(fileID) { + wx.cloud.deleteFile({ + fileList: [fileID], + success: res => { + console.log('删除成功', res); + }, + fail: err => { + console.error('删除失败', err); + } + }); +} +``` + +**云函数调用:** + +```javascript +// 调用云函数 +function callCloudFunction() { + wx.cloud.callFunction({ + name: 'login', // 云函数名称 + data: {}, // 传递给云函数的参数 + success: res => { + console.log('云函数调用成功', res); + }, + fail: err => { + console.error('云函数调用失败', err); + } + }); +} +``` + +**云函数定义示例(login函数):** + +```javascript +// 云函数 login/index.js +const cloud = require('wx-server-sdk'); + +cloud.init({ + env: cloud.DYNAMIC_CURRENT_ENV +}); + +exports.main = async (event, context) => { + // 获取用户OpenID + const wxContext = cloud.getWXContext(); + + // 可以在这里进行用户注册、登录等逻辑 + const db = cloud.database(); + + // 查询用户是否已存在 + const userCollection = db.collection('users'); + const user = await userCollection.where({ + openid: wxContext.OPENID + }).get(); + + // 用户不存在则创建新用户 + if (user.data.length === 0) { + await userCollection.add({ + data: { + openid: wxContext.OPENID, + createTime: db.serverDate(), + lastLoginTime: db.serverDate() + } + }); + } else { + // 更新登录时间 + await userCollection.where({ + openid: wxContext.OPENID + }).update({ + data: { + lastLoginTime: db.serverDate() + } + }); + } + + return { + openid: wxContext.OPENID, + appid: wxContext.APPID, + unionid: wxContext.UNIONID, + env: cloud.DYNAMIC_CURRENT_ENV + }; +}; +``` + +## 总结 + +本指南全面介绍了微信小程序开发的各个方面,从基础知识到高级技巧,涵盖了小程序开发的完整生命周期。通过学习本指南,开发者可以掌握小程序开发的核心技能,包括基础组件使用、登录授权、支付功能、订阅消息、性能优化、安全实践等。 + +对于跨平台开发需求,uni-app提供了一套代码多端发布的解决方案,可以大幅提高开发效率。而对于后端服务需求,微信云开发则提供了简单易用的云服务能力,让开发者可以专注于业务逻辑实现。 + +希望本指南能够帮助开发者快速上手微信小程序开发,构建出高质量、高性能的小程序应用。 \ No newline at end of file diff --git a/docs/jobPro/wechat-open-api.md b/docs/jobPro/wechat-open-api.md new file mode 100644 index 000000000..217e0d86b --- /dev/null +++ b/docs/jobPro/wechat-open-api.md @@ -0,0 +1,3999 @@ +--- +title: 微信小程序开放接口完全指南 +author: 哪吒 +date: '2023-07-20' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# 微信小程序开放接口完全指南 + +## 目录 + +- [微信小程序开放接口完全指南](#微信小程序开放接口完全指南) + - [目录](#目录) + - [1. 开放接口概述](#1-开放接口概述) + - [1.1 什么是微信小程序开放接口](#11-什么是微信小程序开放接口) + - [1.2 开放接口的分类](#12-开放接口的分类) + - [1.3 接口使用前提条件](#13-接口使用前提条件) + - [2. 登录与用户信息](#2-登录与用户信息) + - [2.1 登录流程详解](#21-登录流程详解) + - [2.2 获取用户信息](#22-获取用户信息) + - [2.3 获取手机号](#23-获取手机号) + - [2.4 UnionID机制](#24-unionid机制) + - [3. 转发与分享](#3-转发与分享) + - [3.1 页面内转发](#31-页面内转发) + - [3.2 自定义转发内容](#32-自定义转发内容) + - [3.3 分享到朋友圈](#33-分享到朋友圈) + - [3.4 转发监听与数据分析](#34-转发监听与数据分析) + - [4. 支付功能](#4-支付功能) + - [4.1 支付流程概述](#41-支付流程概述) + - [4.2 统一下单接口](#42-统一下单接口) + - [4.3 发起支付](#43-发起支付) + - [4.4 支付结果通知处理](#44-支付结果通知处理) + - [5. 微信卡券](#5-微信卡券) + - [5.1 卡券接口概述](#51-卡券接口概述) + - [5.2 添加卡券](#52-添加卡券) + - [5.3 查看卡券](#53-查看卡券) + - [5.4 核销卡券](#54-核销卡券) + - [6. 订阅消息](#6-订阅消息) + - [6.1 订阅消息概述](#61-订阅消息概述) + - [6.2 申请订阅消息模板](#62-申请订阅消息模板) + - [6.3 获取订阅权限](#63-获取订阅权限) + - [6.4 发送订阅消息](#64-发送订阅消息) + - [7. 微信运动](#7-微信运动) + - [7.1 微信运动数据获取](#71-微信运动数据获取) + - [7.2 运动数据处理与展示](#72-运动数据处理与展示) + - [7.3 实战案例:运动排行榜](#73-实战案例运动排行榜) + - [8. 生物认证](#8-生物认证) + - [8.1 指纹认证](#81-指纹认证) + - [8.2 人脸识别](#82-人脸识别) + - [8.3 安全最佳实践](#83-安全最佳实践) + - [9. 开放数据域](#9-开放数据域) + - [9.1 开放数据域概述](#91-开放数据域概述) + - [9.2 排行榜实现](#92-排行榜实现) + - [9.3 好友数据展示](#93-好友数据展示) + - [10. 小程序跳转](#10-小程序跳转) + - [10.1 跳转其他小程序](#101-跳转其他小程序) + - [10.2 从其他小程序返回](#102-从其他小程序返回) + - [10.3 跳转到微信原生页面](#103-跳转到微信原生页面) + - [11. 蓝牙API](#11-蓝牙api) + - [11.1 蓝牙API概述](#111-蓝牙api概述) + - [11.2 蓝牙设备搜索](#112-蓝牙设备搜索) + - [11.3 蓝牙连接管理](#113-蓝牙连接管理) + - [11.4 数据读写与通信](#114-数据读写与通信) + - [11.5 蓝牙低功耗(BLE)](#115-蓝牙低功耗ble) + - [11.6 实战案例:智能设备控制](#116-实战案例智能设备控制) + - [总结](#总结) + +## 1. 开放接口概述 + +### 1.1 什么是微信小程序开放接口 + +微信小程序开放接口是微信官方提供的一系列API,允许开发者在小程序中调用微信的原生能力和服务。通过这些接口,小程序可以实现登录授权、支付、分享、获取用户信息等功能,极大地丰富了小程序的应用场景和功能。 + +**开放接口的主要特点:** + +- **原生体验**:直接调用微信原生功能,提供流畅的用户体验 +- **安全可靠**:由微信官方提供和维护,具有高度的安全性和稳定性 +- **功能丰富**:覆盖用户信息、支付、分享、卡券等多个方面 +- **持续更新**:微信会不断推出新的开放能力,扩展小程序的功能边界 + +### 1.2 开放接口的分类 + +微信小程序开放接口可以分为以下几类: + +1. **用户信息类**: + - 登录授权 + - 获取用户信息 + - 获取手机号 + - UnionID机制 + +2. **分享与转发类**: + - 页面内转发 + - 自定义转发内容 + - 分享到朋友圈 + - 转发监听 + +3. **支付类**: + - 发起支付 + - 支付结果查询 + - 退款接口 + +4. **微信服务类**: + - 订阅消息 + - 卡券 + - 客服消息 + - 微信运动 + +5. **设备能力类**: + - 生物认证(指纹、人脸) + - 蓝牙 + - NFC + - Wi-Fi + +6. **开放数据类**: + - 好友数据 + - 排行榜 + - 关系链数据 + +7. **小程序跳转类**: + - 跳转其他小程序 + - 从其他小程序返回 + - 跳转原生页面 + +### 1.3 接口使用前提条件 + +在使用微信小程序开放接口前,需要满足以下条件: + +1. **小程序账号要求**: + - 已注册微信小程序账号 + - 完成开发者资质认证(部分接口需要) + - 小程序已发布上线(部分接口需要) + +2. **开发环境配置**: + - 安装最新版本的微信开发者工具 + - 在app.json中配置需要使用的接口权限 + +3. **接口权限申请**: + - 部分接口需要在微信公众平台申请权限 + - 某些接口需要额外的资质审核 + +4. **合规要求**: + - 遵守微信小程序平台运营规范 + - 遵守相关法律法规 + - 保护用户隐私和数据安全 + +**示例:在app.json中配置权限** + +```json +{ + "pages": [ + "pages/index/index", + "pages/logs/logs" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#fff", + "navigationBarTitleText": "开放接口示例", + "navigationBarTextStyle": "black" + }, + "permission": { + "scope.userLocation": { + "desc": "您的位置信息将用于小程序位置接口的效果展示" + }, + "scope.userFuzzyLocation": { + "desc": "您的模糊位置信息将用于小程序位置接口的效果展示" + }, + "scope.writePhotosAlbum": { + "desc": "保存图片到相册功能" + } + } +} +``` + +## 2. 登录与用户信息 + +### 2.1 登录流程详解 + +微信小程序的登录流程是基于OAuth 2.0协议的授权码模式实现的,主要包括以下步骤: + +1. **获取登录凭证(code)**: + - 小程序通过调用`wx.login()`获取临时登录凭证code + - code有效期为5分钟,只能使用一次 + +2. **发送code到开发者服务器**: + - 小程序将code发送到开发者服务器 + +3. **服务器请求微信接口**: + - 开发者服务器通过code、AppID和AppSecret请求微信接口 + - 获取用户的openid和session_key + +4. **生成自定义登录态**: + - 开发者服务器生成自定义登录态(如token) + - 将token返回给小程序 + +5. **后续业务请求**: + - 小程序使用token进行后续业务请求 + +**前端登录代码示例:** + +```javascript +// 小程序端登录流程 +wx.login({ + success: res => { + if (res.code) { + // 发送code到后端 + wx.request({ + url: 'https://your-server.com/api/login', + method: 'POST', + data: { + code: res.code + }, + success: result => { + // 保存登录态 + wx.setStorageSync('token', result.data.token); + console.log('登录成功'); + }, + fail: err => { + console.error('登录失败', err); + } + }); + } else { + console.error('获取code失败', res.errMsg); + } + }, + fail: err => { + console.error('wx.login调用失败', err); + } +}); +``` + +**服务器端解密微信运动数据示例(Node.js):** + +```javascript +const crypto = require('crypto'); + +async function decryptWeRunData(req, res) { + const { encryptedData, iv } = req.body; + const openid = req.user.openid; + + try { + // 获取session_key + const sessionKey = await getUserSessionKey(openid); + + if (!sessionKey) { + return res.status(400).json({ + success: false, + message: '未找到有效的会话密钥' + }); + } + + // 解密数据 + const decryptedData = decryptData(encryptedData, iv, sessionKey); + + if (decryptedData) { + // 解密成功 + res.json({ + success: true, + stepInfoList: decryptedData.stepInfoList + }); + + // 可以将步数数据保存到数据库 + await saveWeRunData(openid, decryptedData.stepInfoList); + } else { + res.status(400).json({ + success: false, + message: '数据解密失败' + }); + } + } catch (error) { + console.error('解密微信运动数据错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} + +// 解密数据函数 +function decryptData(encryptedData, iv, sessionKey) { + try { + // Base64解码 + const encryptedDataBuffer = Buffer.from(encryptedData, 'base64'); + const ivBuffer = Buffer.from(iv, 'base64'); + const sessionKeyBuffer = Buffer.from(sessionKey, 'base64'); + + // 解密 + const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyBuffer, ivBuffer); + decipher.setAutoPadding(true); + + let decoded = decipher.update(encryptedDataBuffer, 'binary', 'utf8'); + decoded += decipher.final('utf8'); + + // 解析JSON + const decodedData = JSON.parse(decoded); + return decodedData; + } catch (error) { + console.error('数据解密错误', error); + return null; + } +} + +// 保存微信运动数据 +async function saveWeRunData(openid, stepInfoList) { + try { + // 查找用户 + const user = await User.findOne({ openid }); + + if (!user) return; + + // 保存最新的步数数据 + for (const stepInfo of stepInfoList) { + // 查找是否已存在该日期的记录 + const existingRecord = await WeRunRecord.findOne({ + userId: user._id, + timestamp: stepInfo.timestamp + }); + + if (existingRecord) { + // 更新记录 + existingRecord.step = stepInfo.step; + await existingRecord.save(); + } else { + // 创建新记录 + await WeRunRecord.create({ + userId: user._id, + timestamp: stepInfo.timestamp, + step: stepInfo.step + }); + } + } + } catch (error) { + console.error('保存微信运动数据错误', error); + } +} +``` + +### 7.2 运动数据处理与展示 + +获取到微信运动数据后,可以对数据进行处理和展示,例如生成步数统计图表、计算运动趋势等: + +```javascript +Page({ + data: { + stepInfoList: [], + totalSteps: 0, + averageSteps: 0, + maxSteps: 0, + chartData: {} + }, + + onLoad() { + // 获取微信运动数据 + this.getWeRunData(); + }, + + // 处理步数数据 + processStepData(stepInfoList) { + if (!stepInfoList || stepInfoList.length === 0) return; + + // 按日期排序(从新到旧) + stepInfoList.sort((a, b) => b.timestamp - a.timestamp); + + // 计算总步数 + const totalSteps = stepInfoList.reduce((sum, item) => sum + item.step, 0); + + // 计算平均步数 + const averageSteps = Math.round(totalSteps / stepInfoList.length); + + // 找出最大步数 + const maxSteps = Math.max(...stepInfoList.map(item => item.step)); + + // 准备图表数据(最近7天) + const recentSteps = stepInfoList.slice(0, 7).reverse(); + const chartData = { + categories: recentSteps.map(item => this.formatDate(item.timestamp)), + series: [{ + name: '步数', + data: recentSteps.map(item => item.step) + }] + }; + + // 更新数据 + this.setData({ + stepInfoList, + totalSteps, + averageSteps, + maxSteps, + chartData + }); + }, + + // 格式化日期 + formatDate(timestamp) { + const date = new Date(timestamp * 1000); + return `${date.getMonth() + 1}/${date.getDate()}`; + }, + + // 渲染图表 + renderChart() { + // 使用第三方图表库渲染图表 + // 这里以echarts为例 + if (!this.chart) { + this.chart = this.selectComponent('#step-chart'); + } + + this.chart.init((canvas, width, height) => { + const chart = echarts.init(canvas, null, { + width: width, + height: height + }); + + const option = { + title: { + text: '最近7天步数统计', + left: 'center' + }, + color: ['#1aad19'], + grid: { + containLabel: true + }, + tooltip: { + show: true, + trigger: 'axis' + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: this.data.chartData.categories + }, + yAxis: { + type: 'value', + min: 0 + }, + series: [{ + name: '步数', + type: 'line', + smooth: true, + data: this.data.chartData.series[0].data + }] + }; + + chart.setOption(option); + return chart; + }); + } +}); +``` + +**WXML模板示例:** + +```html + + + + + + {{totalSteps}} + 总步数 + + + {{averageSteps}} + 平均步数/天 + + + {{maxSteps}} + 最高步数 + + + + + + + + + + + + 日期 + 步数 + + + + {{formatDate(item.timestamp)}} + {{item.step}} + + + + +``` + +### 7.3 实战案例:运动排行榜 + +利用微信运动数据,可以实现好友间的运动排行榜功能。这需要结合开放数据域来实现: + +**主域页面(WXML):** + +```html + + + + 好友运动排行榜 + + + + + + + +``` + +**开放数据域页面(WXML):** + +```html + + + + + + {{myStep}}步 + 第{{myRank}}名 + + + + 排名 + 好友 + 步数 + + + + + + {{index + 1}} + + {{item.nickname}} + {{item.step}}步 + + + + +``` + +**开放数据域JS:** + +```javascript +Page({ + data: { + rankList: [], + myOpenid: '', + myStep: 0, + myRank: 0 + }, + + onLoad() { + // 获取微信运动数据 + this.getWeRunRank(); + }, + + // 获取微信运动排行榜 + getWeRunRank() { + wx.getUserInfo({ + success: res => { + this.setData({ myOpenid: res.userInfo.openId }); + + // 获取微信运动数据 + wx.getWeRunData({ + success: result => { + // 在开放数据域中,可以直接获取到解密后的数据 + const weRunData = wx.getWeRunData(); + + // 获取好友排行榜 + wx.getFriendCloudStorage({ + keyList: ['werun'], + success: res => { + // 处理排行榜数据 + this.processRankData(weRunData, res.data); + } + }); + } + }); + } + }); + }, + + // 处理排行榜数据 + processRankData(myWeRunData, friendsData) { + // 获取今日步数 + const today = new Date(); + const todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`; + const myStep = myWeRunData.stepInfoList.find(item => { + const itemDate = new Date(item.timestamp * 1000); + return itemDate.toDateString() === today.toDateString(); + })?.step || 0; + + // 构建排行榜数据 + const rankList = friendsData.map(friend => { + // 查找好友今日步数 + const friendWeRunData = friend.KVDataList.find(data => data.key === 'werun'); + let step = 0; + + if (friendWeRunData) { + try { + const stepData = JSON.parse(friendWeRunData.value); + const todayData = stepData.stepInfoList.find(item => { + const itemDate = new Date(item.timestamp * 1000); + return itemDate.toDateString() === today.toDateString(); + }); + + if (todayData) { + step = todayData.step; + } + } catch (e) { + console.error('解析好友步数数据失败', e); + } + } + + return { + openid: friend.openid, + avatarUrl: friend.avatarUrl, + nickname: friend.nickname, + step + }; + }); + + // 添加自己的数据 + rankList.push({ + openid: this.data.myOpenid, + avatarUrl: '', // 开放数据域中无法直接获取自己的头像URL + nickname: '', // 开放数据域中无法直接获取自己的昵称 + step: myStep + }); + + // 按步数排序 + rankList.sort((a, b) => b.step - a.step); + + // 查找自己的排名 + const myRank = rankList.findIndex(item => item.openid === this.data.myOpenid) + 1; + + // 更新数据 + this.setData({ + rankList, + myStep, + myRank + }); + } +}); +``` + +## 8. 生物认证 + +### 8.1 指纹认证 + +微信小程序支持使用设备的指纹识别功能进行身份验证,通过调用`wx.startSoterAuthentication()`接口实现: + +```javascript +Page({ + // 检查设备是否支持指纹认证 + checkSoterSupport() { + wx.checkIsSoterEnrolledInDevice({ + checkAuthMode: 'fingerPrint', + success: res => { + if (res.isEnrolled) { + // 设备已录入指纹 + this.setData({ supportFingerPrint: true }); + } else { + // 设备未录入指纹 + wx.showToast({ + title: '请先在系统设置中录入指纹', + icon: 'none' + }); + } + }, + fail: err => { + console.error('检查指纹认证失败', err); + wx.showToast({ + title: '您的设备不支持指纹认证', + icon: 'none' + }); + } + }); + }, + + // 开始指纹认证 + startFingerPrintAuth() { + wx.startSoterAuthentication({ + requestAuthModes: ['fingerPrint'], + challenge: 'challenge', // 挑战因子,可以为空 + authContent: '请验证指纹', + success: res => { + console.log('指纹认证成功', res); + + // 认证成功后的处理 + this.handleAuthSuccess(res); + }, + fail: err => { + console.error('指纹认证失败', err); + + let message = '指纹认证失败'; + + if (err.errCode === 90010) { + message = '没有找到指纹认证相关驱动'; + } else if (err.errCode === 90003) { + message = '请重新验证指纹'; + } + + wx.showToast({ + title: message, + icon: 'none' + }); + } + }); + }, + + // 处理认证成功 + handleAuthSuccess(res) { + // 获取认证结果 + const resultJSON = res.resultJSON; + const resultJSONSignature = res.resultJSONSignature; + + // 将认证结果发送到服务器验证 + wx.request({ + url: 'https://your-server.com/api/verify-fingerprint', + method: 'POST', + data: { + resultJSON, + resultJSONSignature + }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: result => { + if (result.data.success) { + // 服务器验证通过 + wx.showToast({ + title: '认证成功', + icon: 'success' + }); + + // 执行后续操作 + this.afterAuthSuccess(); + } else { + wx.showToast({ + title: result.data.message || '验证失败', + icon: 'none' + }); + } + }, + fail: err => { + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('验证请求失败', err); + } + }); + }, + + // 认证成功后的操作 + afterAuthSuccess() { + // 例如:解锁敏感功能、授权支付等 + } +}); +``` + +### 8.2 人脸识别 + +除了指纹认证,微信小程序还支持人脸识别功能,同样通过`wx.startSoterAuthentication()`接口实现,只需将认证模式改为`'facial'`: + +```javascript +Page({ + // 检查设备是否支持人脸识别 + checkFacialSupport() { + wx.checkIsSoterEnrolledInDevice({ + checkAuthMode: 'facial', + success: res => { + if (res.isEnrolled) { + // 设备已录入人脸 + this.setData({ supportFacial: true }); + } else { + // 设备未录入人脸 + wx.showToast({ + title: '请先在系统设置中录入人脸', + icon: 'none' + }); + } + }, + fail: err => { + console.error('检查人脸识别失败', err); + wx.showToast({ + title: '您的设备不支持人脸识别', + icon: 'none' + }); + } + }); + }, + + // 开始人脸识别 + startFacialAuth() { + wx.startSoterAuthentication({ + requestAuthModes: ['facial'], + challenge: 'challenge', // 挑战因子,可以为空 + authContent: '请进行人脸识别', + success: res => { + console.log('人脸识别成功', res); + + // 认证成功后的处理 + this.handleAuthSuccess(res); + }, + fail: err => { + console.error('人脸识别失败', err); + + wx.showToast({ + title: '人脸识别失败', + icon: 'none' + }); + } + }); + } +}); +``` + +### 8.3 安全最佳实践 + +在使用生物认证时,需要注意以下安全最佳实践: + +1. **服务器验证**: + - 不要仅依赖客户端的认证结果 + - 将认证结果发送到服务器进行验证 + - 验证签名的有效性 + +2. **防重放攻击**: + - 使用挑战因子(challenge) + - 每次认证使用不同的挑战因子 + - 服务器验证挑战因子的有效性 + +3. **降级处理**: + - 提供备选的认证方式 + - 处理设备不支持生物认证的情况 + - 处理用户未录入生物信息的情况 + +4. **敏感操作确认**: + - 仅在必要的敏感操作前使用生物认证 + - 明确告知用户认证的目的 + - 不要过度使用生物认证 + +**服务器端验证生物认证结果示例(Node.js):** + +```javascript +const crypto = require('crypto'); + +async function verifyBiometricAuth(req, res) { + const { resultJSON, resultJSONSignature } = req.body; + + try { + // 解析resultJSON + const result = JSON.parse(resultJSON); + + // 验证挑战因子 + if (result.challenge !== expectedChallenge) { + return res.status(400).json({ + success: false, + message: '无效的挑战因子' + }); + } + + // 验证签名 + const publicKey = await getWechatPublicKey(); + const isValid = verifySignature(resultJSON, resultJSONSignature, publicKey); + + if (isValid) { + // 验证通过 + res.json({ + success: true, + message: '验证成功' + }); + } else { + // 验证失败 + res.status(400).json({ + success: false, + message: '签名验证失败' + }); + } + } catch (error) { + console.error('验证生物认证结果错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} + +// 验证签名函数 +function verifySignature(data, signature, publicKey) { + try { + const verify = crypto.createVerify('RSA-SHA256'); + verify.update(data); + return verify.verify(publicKey, Buffer.from(signature, 'base64')); + } catch (error) { + console.error('签名验证错误', error); + return false; + } +} +``` + +## 9. 开放数据域 + +### 9.1 开放数据域概述 + +微信小程序的开放数据域是一个独立的JavaScript作用域,用于访问用户的开放数据,如微信好友关系、群信息、排行榜等。开放数据域的主要特点: + +1. **数据隔离**: + - 开放数据只能在开放数据域中访问 + - 主域无法直接获取开放数据 + +2. **渲染机制**: + - 开放数据域的内容通过``或``组件渲染 + - 主域与开放数据域通过消息机制通信 + +3. **使用场景**: + - 好友排行榜 + - 群排行榜 + - 展示好友头像、昵称等信息 + +**开放数据域的使用流程:** + +1. 在游戏项目中创建开放数据域目录(通常为`/opendata`) +2. 在开放数据域中编写访问开放数据的代码 +3. 在主域中使用``组件渲染开放数据域内容 +4. 通过消息机制实现主域与开放数据域的通信 + +### 9.2 排行榜实现 + +使用开放数据域实现好友排行榜的完整示例: + +**主域页面(WXML):** + +```html + + + + 好友排行榜 + + 好友排行 + 群排行 + + + + + + + + + + + +``` + +**主域页面(JS):** + +```javascript +Page({ + data: { + currentTab: 'friend', // 当前选中的标签:friend或group + shareTicket: '' // 群分享票据 + }, + + onLoad(options) { + // 初始化开放数据域 + this.initOpenDataContext(); + + // 如果是从群分享进入,获取shareTicket + if (options.shareTicket) { + this.setData({ + shareTicket: options.shareTicket, + currentTab: 'group' + }); + + // 切换到群排行 + this.switchTab({ currentTarget: { dataset: { tab: 'group' } } }); + } + }, + + // 初始化开放数据域 + initOpenDataContext() { + // 获取开放数据域实例 + this.openDataContext = wx.getOpenDataContext(); + + // 获取画布 + const canvas = wx.createCanvas(); + + // 向开放数据域发送初始化消息 + this.openDataContext.postMessage({ + action: 'init', + canvas: canvas + }); + }, + + // 切换标签 + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + + if (tab === this.data.currentTab) return; + + this.setData({ currentTab: tab }); + + if (tab === 'friend') { + // 加载好友排行榜 + this.loadFriendRank(); + } else if (tab === 'group') { + // 加载群排行榜 + this.loadGroupRank(); + } + }, + + // 加载好友排行榜 + loadFriendRank() { + this.openDataContext.postMessage({ + action: 'loadFriendRank' + }); + }, + + // 加载群排行榜 + loadGroupRank() { + if (this.data.shareTicket) { + this.openDataContext.postMessage({ + action: 'loadGroupRank', + shareTicket: this.data.shareTicket + }); + } else { + wx.showToast({ + title: '请从群聊中进入', + icon: 'none' + }); + } + }, + + // 分享 + onShareAppMessage() { + return { + title: '来挑战我的分数吧!', + path: '/pages/rank/index', + imageUrl: '/images/share.png', + success: res => { + if (res.shareTickets && res.shareTickets.length > 0) { + // 获取群分享票据 + this.setData({ shareTicket: res.shareTickets[0] }); + } + } + }; + } +}); +``` + +**开放数据域代码(opendata/index.js):** + +```javascript +// 开放数据域代码 +const canvas = wx.getSharedCanvas(); +const context = canvas.getContext('2d'); + +// 排行榜数据 +let rankList = []; +let myRank = 0; + +// 监听主域消息 +wx.onMessage(message => { + if (message.action === 'init') { + // 初始化 + initCanvas(); + } else if (message.action === 'loadFriendRank') { + // 加载好友排行榜 + loadFriendRank(); + } else if (message.action === 'loadGroupRank') { + // 加载群排行榜 + loadGroupRank(message.shareTicket); + } +}); + +// 初始化画布 +function initCanvas() { + // 设置画布尺寸 + canvas.width = 375; + canvas.height = 600; + + // 绘制背景 + context.fillStyle = '#f8f8f8'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // 绘制加载提示 + context.fillStyle = '#333333'; + context.font = '16px Arial'; + context.textAlign = 'center'; + context.fillText('加载中...', canvas.width / 2, canvas.height / 2); +} + +// 加载好友排行榜 +function loadFriendRank() { + // 获取用户信息 + wx.getUserInfo({ + openIdList: ['selfOpenId'], + success: res => { + const userInfo = res.data[0]; + + // 获取好友数据 + wx.getFriendCloudStorage({ + keyList: ['score'], + success: res => { + // 处理排行榜数据 + processRankData(userInfo, res.data); + + // 渲染排行榜 + renderRankList(); + }, + fail: err => { + console.error('获取好友数据失败', err); + renderError('获取好友数据失败'); + } + }); + }, + fail: err => { + console.error('获取用户信息失败', err); + renderError('获取用户信息失败'); + } + }); +} + +// 加载群排行榜 +function loadGroupRank(shareTicket) { + if (!shareTicket) { + renderError('无效的群分享票据'); + return; + } + + // 获取用户信息 + wx.getUserInfo({ + openIdList: ['selfOpenId'], + success: res => { + const userInfo = res.data[0]; + + // 获取群数据 + wx.getGroupCloudStorage({ + shareTicket, + keyList: ['score'], + success: res => { + // 处理排行榜数据 + processRankData(userInfo, res.data); + + // 渲染排行榜 + renderRankList(); + }, + fail: err => { + console.error('获取群数据失败', err); + renderError('获取群数据失败'); + } + }); + }, + fail: err => { + console.error('获取用户信息失败', err); + renderError('获取用户信息失败'); + } + }); +} + +// 处理排行榜数据 +function processRankData(userInfo, friendData) { + // 提取分数数据 + rankList = friendData.map(friend => { + // 查找好友分数 + const scoreData = friend.KVDataList.find(data => data.key === 'score'); + let score = 0; + + if (scoreData) { + try { + score = parseInt(scoreData.value); + } catch (e) { + console.error('解析分数数据失败', e); + } + } + + return { + openid: friend.openid, + avatarUrl: friend.avatarUrl, + nickname: friend.nickname, + score + }; + }); + + // 按分数排序(从高到低) + rankList.sort((a, b) => b.score - a.score); + + // 查找自己的排名 + myRank = rankList.findIndex(item => item.openid === userInfo.openId) + 1; +} + +// 渲染排行榜 +function renderRankList() { + // 清空画布 + context.clearRect(0, 0, canvas.width, canvas.height); + + // 绘制背景 + context.fillStyle = '#f8f8f8'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // 绘制标题 + context.fillStyle = '#333333'; + context.font = 'bold 18px Arial'; + context.textAlign = 'center'; + context.fillText('排行榜', canvas.width / 2, 30); + + // 绘制我的排名 + context.font = '16px Arial'; + context.fillText(`我的排名: 第${myRank}名`, canvas.width / 2, 60); + + // 绘制分割线 + context.strokeStyle = '#eeeeee'; + context.lineWidth = 1; + context.beginPath(); + context.moveTo(10, 80); + context.lineTo(canvas.width - 10, 80); + context.stroke(); + + // 绘制排行榜列表 + const itemHeight = 60; + const startY = 100; + + for (let i = 0; i < rankList.length && i < 10; i++) { + const item = rankList[i]; + const y = startY + i * itemHeight; + + // 绘制背景(突出显示自己) + if (item.openid === 'selfOpenId') { + context.fillStyle = '#e6f7ff'; + context.fillRect(10, y - 5, canvas.width - 20, itemHeight); + } + + // 绘制排名 + context.fillStyle = i < 3 ? '#ff9800' : '#999999'; + context.font = 'bold 18px Arial'; + context.textAlign = 'center'; + context.fillText(`${i + 1}`, 30, y + 20); + + // 绘制头像(使用默认头像) + context.fillStyle = '#dddddd'; + context.beginPath(); + context.arc(70, y + 20, 20, 0, Math.PI * 2); + context.fill(); + + // 绘制昵称 + context.fillStyle = '#333333'; + context.font = '16px Arial'; + context.textAlign = 'left'; + context.fillText(item.nickname, 100, y + 20); + + // 绘制分数 + context.fillStyle = '#ff6600'; + context.textAlign = 'right'; + context.fillText(`${item.score}分`, canvas.width - 20, y + 20); + + // 绘制分割线 + if (i < rankList.length - 1 && i < 9) { + context.strokeStyle = '#eeeeee'; + context.beginPath(); + context.moveTo(10, y + itemHeight - 5); + context.lineTo(canvas.width - 10, y + itemHeight - 5); + context.stroke(); + } + } + + // 如果没有数据 + if (rankList.length === 0) { + context.fillStyle = '#999999'; + context.font = '16px Arial'; + context.textAlign = 'center'; + context.fillText('暂无排行数据', canvas.width / 2, canvas.height / 2); + } +} + +// 渲染错误信息 +function renderError(message) { + // 清空画布 + context.clearRect(0, 0, canvas.width, canvas.height); + + // 绘制背景 + context.fillStyle = '#f8f8f8'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // 绘制错误信息 + context.fillStyle = '#ff0000'; + context.font = '16px Arial'; + context.textAlign = 'center'; + context.fillText(message, canvas.width / 2, canvas.height / 2); +} +``` + +### 9.3 好友数据展示 + +除了排行榜,开放数据域还可以用于展示好友数据,例如好友列表、群成员列表等: + +**使用open-data组件展示用户信息:** + +```html + + +``` + +**获取好友列表示例:** + +```javascript +// 开放数据域中获取好友列表 +function getFriendList() { + wx.getFriendCloudStorage({ + keyList: ['score', 'level'], // 要获取的数据键列表 + success: res => { + const friendList = res.data; + console.log('好友列表', friendList); + + // 处理好友数据 + processFriendData(friendList); + }, + fail: err => { + console.error('获取好友列表失败', err); + } + }); +} + +// 处理好友数据 +function processFriendData(friendList) { + // 处理好友数据的逻辑 + // ... + + // 渲染好友列表 + renderFriendList(friendList); +} + +// 渲染好友列表 +function renderFriendList(friendList) { + // 渲染好友列表的逻辑 + // ... +} +``` + +## 10. 小程序跳转 + +### 10.1 跳转其他小程序 + +微信小程序支持跳转到其他小程序,通过调用`wx.navigateToMiniProgram()`接口实现: + +```javascript +Page({ + // 跳转到其他小程序 + navigateToMiniProgram() { + wx.navigateToMiniProgram({ + appId: 'wxabcdef123456', // 要跳转的小程序的appid + path: 'pages/index/index?id=123', // 跳转的目标页面 + extraData: { // 需要传递给目标小程序的数据 + foo: 'bar' + }, + envVersion: 'release', // 要打开的小程序版本,有效值:develop(开发版),trial(体验版),release(正式版) + success: res => { + console.log('跳转成功'); + }, + fail: err => { + console.error('跳转失败', err); + } + }); + } +}); +``` + +**使用button组件跳转:** + +```html + + +``` + +### 10.2 从其他小程序返回 + +当从其他小程序返回时,可以在`onShow`生命周期函数中获取返回信息: + +```javascript +Page({ + onShow() { + // 获取当前页面的参数 + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + const options = currentPage.options; + + // 检查是否有返回参数 + if (options.from_appid) { + console.log('从其他小程序返回', options); + + // 处理返回数据 + this.handleReturnData(options); + } + }, + + // 处理返回数据 + handleReturnData(options) { + // 处理从其他小程序返回的数据 + // ... + } +}); +``` + +### 10.3 跳转到微信原生页面 + +微信小程序还支持跳转到微信的原生页面,例如客服会话、设置页面等: + +**跳转到客服会话:** + +```html + + +``` + +**跳转到设置页面:** + +```javascript +Page({ + // 跳转到设置页面 + openSetting() { + wx.openSetting({ + success: res => { + console.log('设置页面打开成功', res.authSetting); + }, + fail: err => { + console.error('设置页面打开失败', err); + } + }); + } +}); +``` + +**跳转到微信支付分页面:** + +```javascript +Page({ + // 跳转到微信支付分页面 + navigateToWeChatPay() { + wx.navigateToMiniProgram({ + appId: 'wxd8f3793ea3b935b8', // 微信支付分小程序的appid + path: 'pages/index/index', // 跳转的目标页面 + success: res => { + console.log('跳转成功'); + }, + fail: err => { + console.error('跳转失败', err); + } + }); + } +}); +``` + +## 总结 + +微信小程序开放接口为开发者提供了丰富的能力,使小程序能够与微信生态深度融合,提供更好的用户体验。本指南详细介绍了各类开放接口的使用方法、最佳实践和实战案例,帮助开发者更好地利用这些能力开发功能丰富的小程序。 + +在使用开放接口时,需要注意以下几点: + +1. **权限申请**:许多开放接口需要在小程序管理后台申请权限,确保在使用前完成相关配置。 + +2. **用户授权**:涉及用户数据的接口需要获取用户授权,应在合适的时机请求授权,并提供清晰的用途说明。 + +3. **安全性**:处理用户敏感数据时,应遵循最小权限原则,做好数据加密和安全验证。 + +4. **兼容性**:不同设备和微信版本对开放接口的支持可能有差异,应做好兼容性处理。 + +5. **体验优化**:合理使用开放接口,避免过度打扰用户,提供流畅、自然的交互体验。 + +通过合理运用这些开放接口,开发者可以打造出功能丰富、体验优秀的微信小程序,为用户提供更好的服务。 + +## 11. 蓝牙API + +### 11.1 蓝牙API概述 + +微信小程序提供了完整的蓝牙API,支持与蓝牙设备进行通信,包括传统蓝牙和低功耗蓝牙(BLE)。通过这些API,小程序可以实现与智能硬件的互联互通,开发各类IoT应用。 + +**蓝牙API的分类:** + +1. **传统蓝牙API**: + - 适用于传统蓝牙设备 + - 支持搜索、连接、数据传输等功能 + - 使用`wx.openBluetoothAdapter`等接口 + +2. **低功耗蓝牙(BLE)API**: + - 适用于BLE设备 + - 支持服务、特征值的操作 + - 使用`wx.readBLECharacteristicValue`等接口 + +**使用蓝牙API的前提条件:** + +1. **权限申请**: + - 在`app.json`中声明蓝牙相关权限 + - 用户首次使用时需授权 + +2. **设备支持**: + - 确保用户设备支持蓝牙功能 + - 检查蓝牙是否开启 + +**app.json权限配置示例:** + +```json +{ + "pages": [ + "pages/index/index" + ], + "permission": { + "scope.bluetooth": { + "desc": "请求获取蓝牙权限,用于连接智能设备" + } + } +} +``` + +### 11.2 蓝牙设备搜索 + +在与蓝牙设备通信前,首先需要初始化蓝牙模块并搜索设备。 + +**蓝牙设备搜索流程:** + +1. 初始化蓝牙模块 +2. 开始搜索设备 +3. 监听设备发现事件 +4. 处理搜索结果 +5. 停止搜索 + +**完整代码示例:** + +```javascript +Page({ + data: { + devices: [], // 搜索到的设备列表 + searching: false // 是否正在搜索 + }, + + // 初始化蓝牙模块 + initBluetooth() { + wx.openBluetoothAdapter({ + success: (res) => { + console.log('初始化蓝牙模块成功', res); + this.startBluetoothDevicesDiscovery(); + }, + fail: (err) => { + if (err.errCode === 10001) { + wx.showToast({ + title: '请开启手机蓝牙功能', + icon: 'none' + }); + } else { + wx.showToast({ + title: '蓝牙初始化失败', + icon: 'none' + }); + } + console.error('初始化蓝牙模块失败', err); + } + }); + + // 监听蓝牙适配器状态变化 + wx.onBluetoothAdapterStateChange((res) => { + console.log('蓝牙适配器状态变化', res); + if (!res.available) { + this.stopBluetoothDevicesDiscovery(); + } + }); + + // 监听寻找到新设备的事件 + wx.onBluetoothDeviceFound((res) => { + const devices = res.devices; + console.log('发现新设备', devices); + + // 过滤并添加设备到列表 + devices.forEach(device => { + // 过滤无名称或已存在的设备 + if (!device.name && !device.localName) { + return; + } + + // 检查设备是否已存在于列表中 + const foundDevices = this.data.devices; + const idx = foundDevices.findIndex(item => item.deviceId === device.deviceId); + + if (idx === -1) { + // 新设备,添加到列表 + this.setData({ + devices: [...foundDevices, device] + }); + } else { + // 已存在的设备,更新信息 + foundDevices[idx] = device; + this.setData({ + devices: foundDevices + }); + } + }); + }); + }, + + // 开始搜索蓝牙设备 + startBluetoothDevicesDiscovery() { + if (this.data.searching) { + return; + } + + this.setData({ + searching: true + }); + + // 开始搜索 + wx.startBluetoothDevicesDiscovery({ + allowDuplicatesKey: false, // 是否允许重复上报同一设备 + success: (res) => { + console.log('开始搜索蓝牙设备', res); + wx.showLoading({ + title: '正在搜索设备...' + }); + }, + fail: (err) => { + console.error('搜索蓝牙设备失败', err); + this.setData({ + searching: false + }); + wx.showToast({ + title: '搜索设备失败', + icon: 'none' + }); + } + }); + }, + + // 停止搜索蓝牙设备 + stopBluetoothDevicesDiscovery() { + wx.stopBluetoothDevicesDiscovery({ + success: (res) => { + console.log('停止搜索蓝牙设备', res); + this.setData({ + searching: false + }); + wx.hideLoading(); + } + }); + }, + + // 页面卸载时清理蓝牙模块 + onUnload() { + this.stopBluetoothDevicesDiscovery(); + wx.closeBluetoothAdapter(); + } +}); +``` + +**WXML模板示例:** + +```html + + + 蓝牙设备搜索 + + + + + + + + {{item.name || item.localName || '未知设备'}} + ID: {{item.deviceId}} + 信号强度: {{item.RSSI}} dBm + + > + + + + {{searching ? '正在搜索设备...' : '暂无设备,请点击开始搜索'}} + + + +``` + +### 11.3 蓝牙连接管理 + +搜索到设备后,需要建立连接并管理连接状态。 + +**蓝牙连接流程:** + +1. 创建连接 +2. 获取服务 +3. 获取特征值 +4. 监听连接状态 +5. 断开连接 + +**连接管理代码示例:** + +```javascript +Page({ + data: { + // 其他数据... + connectedDeviceId: '', // 已连接的设备ID + services: [], // 设备服务列表 + }, + + // 连接设备 + connectDevice(e) { + const device = e.currentTarget.dataset.device; + const deviceId = device.deviceId; + + // 停止搜索 + this.stopBluetoothDevicesDiscovery(); + + // 创建连接 + wx.createBLEConnection({ + deviceId: deviceId, + success: (res) => { + console.log('连接设备成功', res); + this.setData({ + connectedDeviceId: deviceId + }); + + wx.showToast({ + title: '连接成功', + icon: 'success' + }); + + // 获取设备的服务 + this.getBLEDeviceServices(deviceId); + }, + fail: (err) => { + console.error('连接设备失败', err); + wx.showToast({ + title: '连接失败', + icon: 'none' + }); + } + }); + + // 监听设备连接状态 + wx.onBLEConnectionStateChange((res) => { + console.log('设备连接状态变化', res); + if (!res.connected) { + // 设备已断开连接 + this.setData({ + connectedDeviceId: '', + services: [] + }); + + wx.showToast({ + title: '设备已断开连接', + icon: 'none' + }); + } + }); + }, + + // 获取设备的服务 + getBLEDeviceServices(deviceId) { + wx.getBLEDeviceServices({ + deviceId: deviceId, + success: (res) => { + console.log('获取设备服务成功', res); + const services = res.services; + this.setData({ + services: services + }); + + // 获取第一个服务的特征值 + if (services.length > 0) { + this.getBLEDeviceCharacteristics(deviceId, services[0].uuid); + } + }, + fail: (err) => { + console.error('获取设备服务失败', err); + } + }); + }, + + // 获取特定服务的特征值 + getBLEDeviceCharacteristics(deviceId, serviceId) { + wx.getBLEDeviceCharacteristics({ + deviceId: deviceId, + serviceId: serviceId, + success: (res) => { + console.log('获取特征值成功', res); + const characteristics = res.characteristics; + + // 遍历特征值,查找可读写的特征 + characteristics.forEach(characteristic => { + const uuid = characteristic.uuid; + const properties = characteristic.properties; + + // 如果特征值可读 + if (properties.read) { + // 读取特征值数据 + this.readBLECharacteristicValue(deviceId, serviceId, uuid); + } + + // 如果特征值可通知 + if (properties.notify || properties.indicate) { + // 订阅特征值变化 + this.notifyBLECharacteristicValueChange(deviceId, serviceId, uuid); + } + }); + }, + fail: (err) => { + console.error('获取特征值失败', err); + } + }); + }, + + // 断开设备连接 + disconnectDevice() { + const deviceId = this.data.connectedDeviceId; + if (!deviceId) return; + + wx.closeBLEConnection({ + deviceId: deviceId, + success: (res) => { + console.log('断开设备连接成功', res); + this.setData({ + connectedDeviceId: '', + services: [] + }); + }, + fail: (err) => { + console.error('断开设备连接失败', err); + } + }); + }, + + // 页面卸载时断开连接并关闭蓝牙模块 + onUnload() { + if (this.data.connectedDeviceId) { + this.disconnectDevice(); + } + wx.closeBluetoothAdapter(); + } +}); +``` + +### 11.4 数据读写与通信 + +连接设备后,可以通过特征值进行数据读写和通信。 + +**数据读写流程:** + +1. 读取特征值 +2. 监听特征值变化 +3. 写入特征值 + +**数据读写代码示例:** + +```javascript +Page({ + // 其他代码... + + // 读取特征值数据 + readBLECharacteristicValue(deviceId, serviceId, characteristicId) { + wx.readBLECharacteristicValue({ + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + success: (res) => { + console.log('读取特征值成功', res); + // 读取成功后,数据会通过onBLECharacteristicValueChange事件返回 + }, + fail: (err) => { + console.error('读取特征值失败', err); + } + }); + }, + + // 监听特征值变化 + notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId) { + wx.notifyBLECharacteristicValueChange({ + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + state: true, // 启用通知 + success: (res) => { + console.log('订阅特征值成功', res); + + // 监听特征值变化 + wx.onBLECharacteristicValueChange((result) => { + console.log('特征值变化', result); + const value = this.ab2hex(result.value); + console.log('接收到的数据:', value); + + // 处理接收到的数据 + this.handleReceivedData(value); + }); + }, + fail: (err) => { + console.error('订阅特征值失败', err); + } + }); + }, + + // 写入数据到特征值 + writeBLECharacteristicValue(data) { + const deviceId = this.data.connectedDeviceId; + if (!deviceId) { + wx.showToast({ + title: '设备未连接', + icon: 'none' + }); + return; + } + + // 获取写入特征值的服务ID和特征值ID + // 注意:这里需要根据实际设备的服务和特征值进行修改 + const serviceId = 'YOUR_SERVICE_ID'; + const characteristicId = 'YOUR_CHARACTERISTIC_ID'; + + // 将字符串转换为ArrayBuffer + const buffer = this.string2ArrayBuffer(data); + + wx.writeBLECharacteristicValue({ + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + value: buffer, + success: (res) => { + console.log('写入数据成功', res); + }, + fail: (err) => { + console.error('写入数据失败', err); + wx.showToast({ + title: '发送数据失败', + icon: 'none' + }); + } + }); + }, + + // 处理接收到的数据 + handleReceivedData(data) { + // 根据设备协议解析数据 + // 这里仅作示例,实际应用中需要根据设备协议进行解析 + console.log('解析后的数据:', data); + + // 更新UI显示 + this.setData({ + receivedData: data + }); + }, + + // ArrayBuffer转16进制字符串 + ab2hex(buffer) { + const hexArr = Array.prototype.map.call( + new Uint8Array(buffer), + function(bit) { + return ('00' + bit.toString(16)).slice(-2); + } + ); + return hexArr.join(''); + }, + + // 字符串转ArrayBuffer + string2ArrayBuffer(str) { + const buffer = new ArrayBuffer(str.length); + const dataView = new DataView(buffer); + for (let i = 0; i < str.length; i++) { + dataView.setUint8(i, str.charCodeAt(i)); + } + return buffer; + } +}); +``` + +### 11.5 蓝牙低功耗(BLE) + +蓝牙低功耗(BLE)是一种特殊的蓝牙技术,具有低功耗、低延迟的特点,适用于IoT设备、可穿戴设备等场景。 + +**BLE的特点:** + +1. **低功耗**:设备可以使用纽扣电池运行数月甚至数年 +2. **低延迟**:连接建立时间短,通信延迟低 +3. **服务与特征值**:采用GATT协议,通过服务和特征值进行数据交互 + +**BLE通信模型:** + +- **服务(Service)**:设备提供的功能集合,每个服务包含多个特征值 +- **特征值(Characteristic)**:服务的具体属性,可以读取、写入或订阅 +- **描述符(Descriptor)**:特征值的附加信息 + +**常见BLE服务UUID:** + +| 服务名称 | UUID | 描述 | +|---------|------|------| +| 电池服务 | 0x180F | 提供设备电池电量信息 | +| 设备信息服务 | 0x180A | 提供设备制造商、型号等信息 | +| 心率服务 | 0x180D | 提供心率监测数据 | +| 健康温度计服务 | 0x1809 | 提供温度测量数据 | + +**BLE设备交互示例:** + +```javascript +// 获取设备电池电量 +function getBatteryLevel(deviceId) { + // 电池服务UUID + const batteryServiceUUID = '180F'; + // 电池电量特征值UUID + const batteryCharacteristicUUID = '2A19'; + + // 读取电池电量 + wx.readBLECharacteristicValue({ + deviceId: deviceId, + serviceId: batteryServiceUUID, + characteristicId: batteryCharacteristicUUID, + success: (res) => { + console.log('读取电池电量成功', res); + + // 监听特征值变化获取数据 + wx.onBLECharacteristicValueChange((result) => { + if (result.characteristicId === batteryCharacteristicUUID) { + // 解析电池电量数据(单字节,范围0-100) + const value = new Uint8Array(result.value); + const batteryLevel = value[0]; + + console.log('电池电量:', batteryLevel + '%'); + + // 更新UI显示 + this.setData({ + batteryLevel: batteryLevel + }); + } + }); + }, + fail: (err) => { + console.error('读取电池电量失败', err); + } + }); +} +``` + +### 11.6 实战案例:智能设备控制 + +下面是一个完整的智能灯泡控制案例,展示如何使用蓝牙API控制智能设备。 + +**智能灯泡控制流程:** + +1. 搜索并连接灯泡 +2. 获取灯泡服务和特征值 +3. 发送控制命令(开关、亮度、颜色) +4. 接收灯泡状态更新 + +**完整代码示例:** + +```javascript +// 智能灯泡控制页面 +Page({ + data: { + devices: [], // 搜索到的设备列表 + searching: false, // 是否正在搜索 + connected: false, // 是否已连接 + deviceId: '', // 已连接的设备ID + lightOn: false, // 灯泡开关状态 + brightness: 50, // 亮度(0-100) + color: '#FFFFFF', // 颜色(RGB) + // 灯泡服务和特征值UUID(示例,实际应根据设备文档确定) + serviceId: 'FFF0', + controlCharId: 'FFF1', // 控制特征值 + statusCharId: 'FFF2' // 状态特征值 + }, + + // 初始化蓝牙 + onLoad() { + this.initBluetooth(); + }, + + // 初始化蓝牙模块 + initBluetooth() { + wx.openBluetoothAdapter({ + success: (res) => { + console.log('初始化蓝牙模块成功', res); + }, + fail: (err) => { + console.error('初始化蓝牙模块失败', err); + wx.showModal({ + title: '提示', + content: '请确保手机蓝牙已开启', + showCancel: false + }); + } + }); + + // 监听蓝牙适配器状态变化 + wx.onBluetoothAdapterStateChange((res) => { + if (!res.available) { + this.setData({ + searching: false, + connected: false + }); + } + }); + + // 监听设备发现事件 + wx.onBluetoothDeviceFound((res) => { + res.devices.forEach(device => { + // 过滤设备(这里假设灯泡名称包含"Light"或"Bulb") + const name = device.name || device.localName || ''; + if (name.includes('Light') || name.includes('Bulb')) { + // 检查是否已存在于列表中 + const idx = this.data.devices.findIndex(d => d.deviceId === device.deviceId); + if (idx === -1) { + this.setData({ + devices: [...this.data.devices, device] + }); + } + } + }); + }); + + // 监听连接状态变化 + wx.onBLEConnectionStateChange((res) => { + if (!res.connected && this.data.connected) { + this.setData({ + connected: false, + lightOn: false + }); + wx.showToast({ + title: '设备已断开连接', + icon: 'none' + }); + } + }); + + // 监听特征值变化 + wx.onBLECharacteristicValueChange((res) => { + if (res.characteristicId === this.data.statusCharId) { + // 解析灯泡状态数据 + this.parseLightStatus(res.value); + } + }); + }, + + // 开始搜索设备 + startSearch() { + this.setData({ + devices: [], + searching: true + }); + + wx.startBluetoothDevicesDiscovery({ + success: (res) => { + console.log('开始搜索设备', res); + }, + fail: (err) => { + console.error('搜索设备失败', err); + this.setData({ searching: false }); + } + }); + }, + + // 停止搜索 + stopSearch() { + wx.stopBluetoothDevicesDiscovery(); + this.setData({ searching: false }); + }, + + // 连接设备 + connectDevice(e) { + const deviceId = e.currentTarget.dataset.id; + this.stopSearch(); + + wx.createBLEConnection({ + deviceId: deviceId, + success: (res) => { + this.setData({ + connected: true, + deviceId: deviceId + }); + + // 获取设备服务 + setTimeout(() => { + this.getBLEDeviceServices(deviceId); + }, 1000); + }, + fail: (err) => { + console.error('连接设备失败', err); + wx.showToast({ + title: '连接失败', + icon: 'none' + }); + } + }); + }, + + // 获取设备服务 + getBLEDeviceServices(deviceId) { + wx.getBLEDeviceServices({ + deviceId: deviceId, + success: (res) => { + // 查找目标服务 + const service = res.services.find(s => s.uuid.toUpperCase().includes(this.data.serviceId)); + if (service) { + this.getBLEDeviceCharacteristics(deviceId, service.uuid); + } else { + wx.showToast({ + title: '未找到灯泡服务', + icon: 'none' + }); + } + } + }); + }, + + // 获取服务特征值 + getBLEDeviceCharacteristics(deviceId, serviceId) { + wx.getBLEDeviceCharacteristics({ + deviceId: deviceId, + serviceId: serviceId, + success: (res) => { + // 查找控制和状态特征值 + res.characteristics.forEach(char => { + const uuid = char.uuid.toUpperCase(); + + // 订阅状态特征值变化 + if (uuid.includes(this.data.statusCharId) && (char.properties.notify || char.properties.indicate)) { + this.notifyBLECharacteristicValueChange(deviceId, serviceId, char.uuid); + } + + // 读取当前状态 + if (uuid.includes(this.data.statusCharId) && char.properties.read) { + this.readLightStatus(deviceId, serviceId, char.uuid); + } + }); + } + }); + }, + + // 订阅状态特征值变化 + notifyBLECharacteristicValueChange(deviceId, serviceId, characteristicId) { + wx.notifyBLECharacteristicValueChange({ + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + state: true, + success: (res) => { + console.log('订阅特征值成功'); + } + }); + }, + + // 读取灯泡状态 + readLightStatus(deviceId, serviceId, characteristicId) { + wx.readBLECharacteristicValue({ + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + success: (res) => { + console.log('读取灯泡状态成功'); + } + }); + }, + + // 解析灯泡状态数据 + parseLightStatus(buffer) { + // 解析数据(示例,实际格式取决于设备协议) + const data = new Uint8Array(buffer); + if (data.length >= 4) { + const status = data[0]; // 0:关闭 1:开启 + const brightness = data[1]; // 亮度 0-100 + const r = data[2]; // 红色 0-255 + const g = data[3]; // 绿色 0-255 + const b = data[4]; // 蓝色 0-255 + + // 转换为十六进制颜色 + const color = '#' + + ('0' + r.toString(16)).slice(-2) + + ('0' + g.toString(16)).slice(-2) + + ('0' + b.toString(16)).slice(-2); + + this.setData({ + lightOn: status === 1, + brightness: brightness, + color: color + }); + } + }, + + // 切换灯泡开关 + toggleLight() { + const newStatus = !this.data.lightOn; + this.setData({ lightOn: newStatus }); + this.sendControlCommand(); + }, + + // 调整亮度 + changeBrightness(e) { + const brightness = e.detail.value; + this.setData({ brightness: brightness }); + this.sendControlCommand(); + }, + + // 选择颜色 + changeColor(e) { + const color = e.detail.value; + this.setData({ color: color }); + this.sendControlCommand(); + }, + + // 发送控制命令 + sendControlCommand() { + const deviceId = this.data.deviceId; + if (!deviceId || !this.data.connected) return; + + // 查找目标服务和特征值 + wx.getBLEDeviceServices({ + deviceId: deviceId, + success: (res) => { + const service = res.services.find(s => s.uuid.toUpperCase().includes(this.data.serviceId)); + if (service) { + wx.getBLEDeviceCharacteristics({ + deviceId: deviceId, + serviceId: service.uuid, + success: (res) => { + const char = res.characteristics.find(c => + c.uuid.toUpperCase().includes(this.data.controlCharId) && c.properties.write); + + if (char) { + // 构建控制命令 + const command = this.buildControlCommand(); + + // 发送命令 + wx.writeBLECharacteristicValue({ + deviceId: deviceId, + serviceId: service.uuid, + characteristicId: char.uuid, + value: command, + success: (res) => { + console.log('发送控制命令成功'); + }, + fail: (err) => { + console.error('发送控制命令失败', err); + } + }); + } + } + }); + } + } + }); + }, + + // 构建控制命令 + buildControlCommand() { + // 解析颜色 + const color = this.data.color.substring(1); // 去掉# + const r = parseInt(color.substring(0, 2), 16); + const g = parseInt(color.substring(2, 4), 16); + const b = parseInt(color.substring(4, 6), 16); + + // 构建命令(示例,实际格式取决于设备协议) + const command = new Uint8Array(5); + command[0] = this.data.lightOn ? 1 : 0; // 开关状态 + command[1] = this.data.brightness; // 亮度 + command[2] = r; // 红色 + command[3] = g; // 绿色 + command[4] = b; // 蓝色 + + return command.buffer; + }, + + // 断开连接 + disconnectDevice() { + const deviceId = this.data.deviceId; + if (!deviceId) return; + + wx.closeBLEConnection({ + deviceId: deviceId, + success: (res) => { + this.setData({ + connected: false, + lightOn: false + }); + } + }); + }, + + // 页面卸载 + onUnload() { + if (this.data.connected) { + this.disconnectDevice(); + } + if (this.data.searching) { + this.stopSearch(); + } + wx.closeBluetoothAdapter(); + } +}); +``` + +**WXML模板:** + +```html + + + + + 智能灯泡控制 + + + + + + + + {{item.name || item.localName || '未知设备'}} + ID: {{item.deviceId}} + + 连接 + + + + {{searching ? '正在搜索设备...' : '暂无设备,请点击搜索设备'}} + + + + + + + + 灯泡控制 + + + + + + 状态: {{lightOn ? '开启' : '关闭'}} + + + + + 开关 + + + + + 亮度: {{brightness}}% + + + + + 颜色 + + + + + + + +``` + +**WXSS样式:** + +```css +.container { + padding: 20rpx; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.title { + font-size: 36rpx; + font-weight: bold; +} + +.search-btn, .disconnect-btn { + font-size: 28rpx; + padding: 10rpx 20rpx; + background-color: #007AFF; + color: white; + border-radius: 8rpx; +} + +.device-list { + margin-top: 20rpx; +} + +.device-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx; + background-color: #f8f8f8; + border-radius: 10rpx; + margin-bottom: 20rpx; +} + +.device-name { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 10rpx; +} + +.device-id { + font-size: 24rpx; + color: #666; +} + +.connect-icon { + color: #007AFF; + font-size: 28rpx; +} + +.empty-tip { + text-align: center; + color: #999; + padding: 40rpx 0; +} + +.light-status { + display: flex; + flex-direction: column; + align-items: center; + margin: 40rpx 0; +} + +.light-preview { + width: 200rpx; + height: 200rpx; + border-radius: 100rpx; + margin-bottom: 20rpx; + box-shadow: 0 0 30rpx rgba(0,0,0,0.2); + transition: all 0.3s ease; +} + +.status-text { + font-size: 32rpx; +} + +.control-panel { + background-color: #f8f8f8; + border-radius: 10rpx; + padding: 20rpx; +} + +.control-item { + margin-bottom: 30rpx; +} + +.control-label { + display: block; + margin-bottom: 10rpx; + font-size: 28rpx; +} + +.color-preview { + width: 60rpx; + height: 60rpx; + border-radius: 8rpx; + border: 2rpx solid #ddd; +} +``` + +**后端处理代码示例(Node.js):** + +```javascript +const axios = require('axios'); +const jwt = require('jsonwebtoken'); + +async function login(req, res) { + const { code } = req.body; + const appId = 'your-appid'; + const appSecret = 'your-appsecret'; + + try { + // 请求微信接口获取openid和session_key + const wxResponse = await axios.get( + `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` + ); + + const { openid, session_key } = wxResponse.data; + + if (openid) { + // 查询用户是否已存在 + let user = await User.findOne({ openid }); + + // 不存在则创建新用户 + if (!user) { + user = await User.create({ openid }); + } + + // 生成JWT token + const token = jwt.sign( + { userId: user._id, openid }, + 'your-jwt-secret', + { expiresIn: '7d' } + ); + + res.json({ success: true, token }); + } else { + res.status(400).json({ success: false, message: '获取openid失败' }); + } + } catch (error) { + console.error('登录处理错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} +``` + +### 2.2 获取用户信息 + +在微信小程序中获取用户信息需要遵循用户授权机制,主要有以下几种方式: + +1. **通过开放能力按钮获取**: + - 使用` +``` + +```javascript +// JS文件 +Page({ + getUserInfo(e) { + if (e.detail.userInfo) { + // 用户允许授权 + const userInfo = e.detail.userInfo; + this.setData({ userInfo }); + console.log('用户信息', userInfo); + } else { + // 用户拒绝授权 + console.log('用户拒绝授权'); + } + } +}); +``` + +**使用wx.getUserProfile示例:** + +```html + + +``` + +```javascript +// JS文件 +Page({ + getUserProfile() { + wx.getUserProfile({ + desc: '用于完善会员资料', // 声明获取用户个人信息后的用途 + success: (res) => { + const userInfo = res.userInfo; + this.setData({ userInfo }); + console.log('用户信息', userInfo); + }, + fail: (err) => { + console.log('获取用户信息失败', err); + } + }); + } +}); +``` + +### 2.3 获取手机号 + +微信小程序提供了获取用户手机号的能力,但需要用户授权,且只能通过button组件的open-type="getPhoneNumber"方式获取: + +**前端获取手机号示例:** + +```html + + +``` + +```javascript +// JS文件 +Page({ + getPhoneNumber(e) { + if (e.detail.errMsg === 'getPhoneNumber:ok') { + // 用户允许授权 + const { code } = e.detail; + + // 将code发送到后端解密 + wx.request({ + url: 'https://your-server.com/api/decrypt-phone', + method: 'POST', + data: { code }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: res => { + console.log('手机号信息', res.data); + }, + fail: err => { + console.error('获取手机号失败', err); + } + }); + } else { + // 用户拒绝授权 + console.log('用户拒绝授权手机号'); + } + } +}); +``` + +**后端解密手机号示例(Node.js):** + +```javascript +const axios = require('axios'); + +async function decryptPhoneNumber(req, res) { + const { code } = req.body; + const appId = 'your-appid'; + const appSecret = 'your-appsecret'; + + try { + // 获取access_token + const tokenResponse = await axios.get( + `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` + ); + + const accessToken = tokenResponse.data.access_token; + + // 使用code和access_token获取手机号 + const phoneResponse = await axios.post( + `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${accessToken}`, + { code } + ); + + if (phoneResponse.data.errcode === 0) { + const phoneInfo = phoneResponse.data.phone_info; + + // 保存用户手机号 + await User.findByIdAndUpdate(req.user.userId, { + phoneNumber: phoneInfo.phoneNumber, + countryCode: phoneInfo.countryCode + }); + + res.json({ + success: true, + phoneNumber: phoneInfo.phoneNumber, + countryCode: phoneInfo.countryCode + }); + } else { + res.status(400).json({ + success: false, + message: '获取手机号失败', + error: phoneResponse.data + }); + } + } catch (error) { + console.error('解密手机号错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} +``` + +### 2.4 UnionID机制 + +UnionID是微信为了解决同一用户在不同应用(小程序、公众号、App等)中的身份识别问题而提供的机制。通过UnionID,开发者可以知道同一用户在不同应用中的关联关系。 + +**UnionID的获取条件:** + +1. 开发者账号已完成微信认证 +2. 用户必须授权获取用户信息 +3. 满足以下任一条件: + - 用户已关注公众号,且公众号与小程序已关联 + - 用户已使用微信登录过该开发者的App,且App与小程序已关联 + - 用户已关注该开发者的其他小程序,且这些小程序已关联 + +**获取UnionID示例:** + +```javascript +// 前端代码与获取用户信息相同,在后端处理UnionID + +// 后端处理UnionID(Node.js) +async function handleLogin(req, res) { + const { code } = req.body; + const appId = 'your-appid'; + const appSecret = 'your-appsecret'; + + try { + // 请求微信接口获取openid、session_key和unionid + const wxResponse = await axios.get( + `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` + ); + + const { openid, session_key, unionid } = wxResponse.data; + + if (openid) { + // 查询用户是否已存在(优先使用unionid查询) + let user; + + if (unionid) { + user = await User.findOne({ unionid }); + } + + if (!user) { + user = await User.findOne({ openid }); + } + + // 不存在则创建新用户 + if (!user) { + user = await User.create({ + openid, + unionid: unionid || null + }); + } else if (unionid && !user.unionid) { + // 更新用户的unionid + user.unionid = unionid; + await user.save(); + } + + // 生成JWT token + const token = jwt.sign( + { userId: user._id, openid }, + 'your-jwt-secret', + { expiresIn: '7d' } + ); + + res.json({ success: true, token }); + } else { + res.status(400).json({ success: false, message: '获取openid失败' }); + } + } catch (error) { + console.error('登录处理错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} +``` + +## 3. 转发与分享 + +### 3.1 页面内转发 + +微信小程序支持用户将页面转发给朋友或分享到群聊。页面内转发主要通过以下两种方式实现: + +1. **通过button组件转发**: + - 使用` +``` + +```javascript +// JS文件 +Page({ + // 定义页面的分享信息 + onShareAppMessage() { + return { + title: '自定义转发标题', + path: '/pages/index/index', + imageUrl: '/images/share.png' // 自定义转发图片 + }; + } +}); +``` + +### 3.2 自定义转发内容 + +开发者可以通过`onShareAppMessage`方法自定义转发的内容,包括标题、路径和图片: + +```javascript +Page({ + onShareAppMessage(res) { + // res.from表示转发事件来源:button或menu + // res.target表示如果from值为button,则target为触发这次转发事件的button + // res.webViewUrl表示如果from值为webview,则webViewUrl为webview的url + + if (res.from === 'button') { + console.log('来自页面内转发按钮'); + } else { + console.log('来自右上角转发菜单'); + } + + return { + title: '自定义转发标题', + path: '/pages/index/index?id=123', // 可以携带参数 + imageUrl: '/images/share.png', // 自定义图片路径,可以是本地文件或网络图片 + promise: new Promise(resolve => { + // 异步获取转发信息 + setTimeout(() => { + resolve({ + title: '异步获取的转发标题' + }); + }, 2000); + }) + }; + } +}); +``` + +### 3.3 分享到朋友圈 + +微信小程序支持将内容分享到朋友圈,需要通过button组件的open-type="shareTimeline"实现: + +```html + + +``` + +```javascript +// JS文件 +Page({ + // 定义页面的朋友圈分享信息 + onShareTimeline() { + return { + title: '自定义朋友圈标题', + query: 'id=123', // 携带参数 + imageUrl: '/images/timeline-share.png' // 自定义图片 + }; + } +}); +``` + +### 3.4 转发监听与数据分析 + +开发者可以监听用户的转发行为,并进行数据分析: + +```javascript +Page({ + onShareAppMessage() { + // 记录转发事件 + this.recordShareEvent('appMessage'); + + return { + title: '自定义转发标题', + path: '/pages/index/index?share_source=button' + }; + }, + + onShareTimeline() { + // 记录朋友圈分享事件 + this.recordShareEvent('timeline'); + + return { + title: '自定义朋友圈标题', + query: 'share_source=timeline' + }; + }, + + // 记录分享事件的方法 + recordShareEvent(type) { + const shareData = { + type, + page: this.route, + timestamp: new Date().getTime() + }; + + // 可以将分享数据发送到后端记录 + wx.request({ + url: 'https://your-server.com/api/record-share', + method: 'POST', + data: shareData, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + } + }); + + // 也可以使用微信自带的数据分析能力 + wx.reportAnalytics('share_event', { + share_type: type, + share_page: this.route + }); + }, + + onLoad(options) { + // 检查是否是通过分享进入的页面 + if (options.share_source) { + console.log(`通过${options.share_source}分享进入页面`); + + // 记录分享来源 + wx.reportAnalytics('share_enter', { + share_source: options.share_source + }); + } + } +}); +``` + +## 4. 支付功能 + +### 4.1 支付流程概述 + +微信小程序支付是基于微信支付的能力,实现在小程序内完成支付的功能。完整的支付流程包括: + +1. **创建商品订单**: + - 用户在小程序中选择商品并下单 + - 小程序将订单信息发送到开发者服务器 + +2. **调用统一下单接口**: + - 开发者服务器调用微信支付统一下单接口 + - 获取预支付交易会话标识(prepay_id) + +3. **生成支付参数**: + - 开发者服务器根据prepay_id生成支付参数 + - 将支付参数返回给小程序 + +4. **发起支付请求**: + - 小程序调用`wx.requestPayment()`发起支付 + - 用户在微信支付界面完成支付 + +5. **接收支付结果通知**: + - 微信服务器通过回调通知开发者服务器支付结果 + - 开发者服务器更新订单状态 + +6. **查询支付结果**: + - 小程序查询开发者服务器获取最终支付状态 + - 展示支付结果给用户 + +### 4.2 统一下单接口 + +开发者服务器需要调用微信支付的统一下单接口,获取预支付交易会话标识: + +**统一下单接口示例(Node.js):** + +```javascript +const crypto = require('crypto'); +const axios = require('axios'); +const xml2js = require('xml2js'); + +async function createUnifiedOrder(req, res) { + const { openid, totalFee, body, outTradeNo } = req.body; + + // 微信支付配置 + const config = { + appid: 'your-appid', + mchid: 'your-mchid', // 商户号 + key: 'your-key', // API密钥 + notifyUrl: 'https://your-server.com/api/pay/notify' // 支付结果通知地址 + }; + + // 构建统一下单参数 + const params = { + appid: config.appid, + mch_id: config.mchid, + nonce_str: generateNonceStr(), + body: body || '商品购买', + out_trade_no: outTradeNo, + total_fee: totalFee, // 单位:分 + spbill_create_ip: req.ip, + notify_url: config.notifyUrl, + trade_type: 'JSAPI', + openid: openid + }; + + // 签名 + params.sign = generateSign(params, config.key); + + // 将参数转换为XML + const builder = new xml2js.Builder({ rootName: 'xml', cdata: true }); + const xml = builder.buildObject(params); + + try { + // 调用统一下单接口 + const response = await axios.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + xml, + { headers: { 'Content-Type': 'text/xml' } } + ); + + // 解析XML响应 + const result = await parseXML(response.data); + + if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') { + // 生成支付参数 + const timeStamp = Math.floor(Date.now() / 1000).toString(); + const nonceStr = generateNonceStr(); + + const payParams = { + appId: config.appid, + timeStamp, + nonceStr, + package: `prepay_id=${result.prepay_id}`, + signType: 'MD5' + }; + + // 签名支付参数 + payParams.paySign = generateSign(payParams, config.key); + + // 返回支付参数给小程序 + res.json({ + success: true, + payParams + }); + } else { + console.error('统一下单失败', result); + res.status(400).json({ + success: false, + message: result.return_msg || result.err_code_des || '统一下单失败' + }); + } + } catch (error) { + console.error('统一下单异常', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} + +// 生成随机字符串 +function generateNonceStr() { + return Math.random().toString(36).substr(2, 15); +} + +// 生成签名 +function generateSign(params, key) { + // 按字典序排序参数 + const sortedParams = Object.keys(params) + .filter(key => params[key] !== undefined && params[key] !== '') + .sort() + .map(key => `${key}=${params[key]}`) + .join('&'); + + // 拼接key + const stringSignTemp = `${sortedParams}&key=${key}`; + + // MD5加密并转为大写 + return crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase(); +} + +// 解析XML +function parseXML(xml) { + return new Promise((resolve, reject) => { + xml2js.parseString(xml, { explicitArray: false }, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result.xml); + } + }); + }); +} +``` + +### 4.3 发起支付 + +小程序端通过调用`wx.requestPayment()`方法发起支付请求: + +```javascript +// 小程序端发起支付 +Page({ + // 创建订单并发起支付 + createOrderAndPay() { + // 显示加载提示 + wx.showLoading({ title: '正在创建订单...' }); + + // 创建订单 + wx.request({ + url: 'https://your-server.com/api/orders', + method: 'POST', + data: { + productId: this.data.productId, + quantity: this.data.quantity, + // 其他订单信息 + }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: orderRes => { + if (orderRes.data.success) { + const { orderId, outTradeNo } = orderRes.data; + + // 获取支付参数 + this.getPayParams(orderId, outTradeNo); + } else { + wx.hideLoading(); + wx.showToast({ + title: orderRes.data.message || '创建订单失败', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('创建订单失败', err); + } + }); + }, + + // 获取支付参数 + getPayParams(orderId, outTradeNo) { + wx.request({ + url: 'https://your-server.com/api/pay/unified-order', + method: 'POST', + data: { + outTradeNo, + totalFee: this.data.totalFee, // 单位:分 + body: this.data.productName + }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: payRes => { + wx.hideLoading(); + + if (payRes.data.success) { + // 发起支付 + this.requestPayment(payRes.data.payParams, orderId); + } else { + wx.showToast({ + title: payRes.data.message || '获取支付参数失败', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('获取支付参数失败', err); + } + }); + }, + + // 调用支付接口 + requestPayment(payParams, orderId) { + wx.requestPayment({ + ...payParams, + success: res => { + console.log('支付成功', res); + + // 跳转到支付成功页面 + wx.navigateTo({ + url: `/pages/pay-success/index?orderId=${orderId}` + }); + }, + fail: err => { + console.log('支付失败或取消', err); + + if (err.errMsg === 'requestPayment:fail cancel') { + // 用户取消支付 + wx.showToast({ + title: '支付已取消', + icon: 'none' + }); + } else { + // 支付失败 + wx.showToast({ + title: '支付失败,请重试', + icon: 'none' + }); + } + + // 跳转到订单详情页面 + wx.navigateTo({ + url: `/pages/order-detail/index?orderId=${orderId}` + }); + } + }); + } +}); +``` + +### 4.4 支付结果通知处理 + +微信支付完成后,微信服务器会向开发者服务器发送支付结果通知。开发者需要处理这个通知,更新订单状态: + +```javascript +// 处理支付结果通知(Node.js) +async function handlePayNotify(req, res) { + // 获取通知数据 + const xmlData = req.body.toString('utf-8'); + + try { + // 解析XML数据 + const notifyData = await parseXML(xmlData); + + // 验证签名 + const sign = notifyData.sign; + delete notifyData.sign; + + const calculatedSign = generateSign(notifyData, 'your-key'); + + if (calculatedSign !== sign) { + console.error('签名验证失败'); + return res.send(''); + } + + // 验证支付结果 + if (notifyData.return_code === 'SUCCESS' && notifyData.result_code === 'SUCCESS') { + // 获取商户订单号 + const outTradeNo = notifyData.out_trade_no; + // 获取微信支付订单号 + const transactionId = notifyData.transaction_id; + // 获取支付金额 + const totalFee = parseInt(notifyData.total_fee); + + // 查询订单 + const order = await Order.findOne({ outTradeNo }); + + if (!order) { + console.error('订单不存在', outTradeNo); + return res.send(''); + } + + // 验证金额是否一致 + if (order.totalFee !== totalFee) { + console.error('支付金额不一致', { orderFee: order.totalFee, payFee: totalFee }); + return res.send(''); + } + + // 更新订单状态 + if (order.status === 'UNPAID') { + order.status = 'PAID'; + order.paidAt = new Date(); + order.transactionId = transactionId; + await order.save(); + + // 处理订单后续业务逻辑 + // 例如:发送订单支付成功通知、更新库存等 + + console.log('订单支付成功', { outTradeNo, transactionId }); + } + + // 返回成功响应 + return res.send(''); + } else { + console.error('支付失败', notifyData); + return res.send(''); + } + } catch (error) { + console.error('处理支付通知异常', error); + return res.send(''); + } +} +``` + +## 5. 微信卡券 + +### 5.1 卡券接口概述 + +微信小程序支持卡券功能,允许用户在小程序中添加、查看和使用微信卡券。卡券接口主要包括: + +1. **添加卡券**:用户将卡券添加到微信卡包 +2. **查看卡券**:查看用户已添加的卡券 +3. **核销卡券**:在小程序中使用卡券 + +使用卡券接口前,需要先在微信公众平台完成以下步骤: + +1. 创建卡券并设置相关信息 +2. 将小程序与公众号关联 +3. 在公众号后台开通卡券功能 +4. 设置卡券的适用小程序 + +### 5.2 添加卡券 + +小程序中通过调用`wx.addCard()`接口实现添加卡券功能: + +```javascript +Page({ + // 添加卡券 + addCard() { + // 显示加载提示 + wx.showLoading({ title: '获取卡券信息...' }); + + // 从服务器获取卡券信息 + wx.request({ + url: 'https://your-server.com/api/cards', + method: 'GET', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: res => { + wx.hideLoading(); + + if (res.data.success && res.data.cardList && res.data.cardList.length > 0) { + // 调用添加卡券接口 + wx.addCard({ + cardList: res.data.cardList, // 需要添加的卡券列表 + success: result => { + console.log('添加卡券成功', result); + + // 处理添加结果 + const addedCards = result.cardList; + + // 将添加结果上报服务器 + this.reportAddedCards(addedCards); + + wx.showToast({ + title: '添加卡券成功', + icon: 'success' + }); + }, + fail: err => { + console.error('添加卡券失败', err); + + wx.showToast({ + title: '添加卡券失败', + icon: 'none' + }); + } + }); + } else { + wx.showToast({ + title: res.data.message || '暂无可用卡券', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('获取卡券信息失败', err); + } + }); + }, + + // 上报已添加的卡券 + reportAddedCards(addedCards) { + wx.request({ + url: 'https://your-server.com/api/cards/report', + method: 'POST', + data: { addedCards }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + } + }); + } +}); +``` + +**服务器端获取卡券信息示例(Node.js):** + +```javascript +const axios = require('axios'); +const crypto = require('crypto'); + +async function getCardInfo(req, res) { + try { + // 获取access_token + const accessToken = await getAccessToken(); + + // 获取卡券列表 + const cardsResponse = await axios.get( + `https://api.weixin.qq.com/card/batchget?access_token=${accessToken}`, + { + data: { + offset: 0, + count: 10, + status_list: ['CARD_STATUS_VERIFY_OK', 'CARD_STATUS_DISPATCH'] + } + } + ); + + if (cardsResponse.data.errcode === 0) { + const cardList = []; + + // 处理卡券列表 + for (const cardInfo of cardsResponse.data.card_id_list) { + // 获取卡券详情 + const cardDetailResponse = await axios.post( + `https://api.weixin.qq.com/card/get?access_token=${accessToken}`, + { card_id: cardInfo } + ); + + if (cardDetailResponse.data.errcode === 0) { + // 获取卡券的扩展参数 + const ext = generateCardExt(cardInfo); + + cardList.push({ + cardId: cardInfo, + cardExt: ext + }); + } + } + + res.json({ + success: true, + cardList + }); + } else { + res.status(400).json({ + success: false, + message: '获取卡券列表失败', + error: cardsResponse.data + }); + } + } catch (error) { + console.error('获取卡券信息错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} + +// 生成卡券扩展参数 +function generateCardExt(cardId) { + const appid = 'your-appid'; + const timestamp = Math.floor(Date.now() / 1000); + const nonceStr = Math.random().toString(36).substr(2, 15); + const apiTicket = 'your-api-ticket'; // 需要通过接口获取 + + // 构建签名字符串 + const signParams = [ + apiTicket, + cardId, + timestamp, + nonceStr + ].sort().join(''); + + // 计算签名 + const signature = crypto.createHash('sha1').update(signParams).digest('hex'); + + // 构建cardExt对象 + const cardExt = { + timestamp, + nonce_str: nonceStr, + signature, + outer_str: 'optional-outer-string' // 可选,用于领取卡券后的自定义参数 + }; + + // 返回JSON字符串 + return JSON.stringify(cardExt); +} +``` + +### 5.3 查看卡券 + +小程序中通过调用`wx.openCard()`接口实现查看卡券功能: + +```javascript +Page({ + // 查看卡券 + openCard() { + // 显示加载提示 + wx.showLoading({ title: '获取卡券信息...' }); + + // 从服务器获取用户已添加的卡券信息 + wx.request({ + url: 'https://your-server.com/api/user/cards', + method: 'GET', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: res => { + wx.hideLoading(); + + if (res.data.success && res.data.cardList && res.data.cardList.length > 0) { + // 调用查看卡券接口 + wx.openCard({ + cardList: res.data.cardList, // 需要打开的卡券列表 + success: result => { + console.log('查看卡券成功', result); + }, + fail: err => { + console.error('查看卡券失败', err); + + wx.showToast({ + title: '查看卡券失败', + icon: 'none' + }); + } + }); + } else { + wx.showToast({ + title: res.data.message || '暂无可用卡券', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('获取卡券信息失败', err); + } + }); + } +}); +``` + +### 5.4 核销卡券 + +在小程序中核销卡券通常需要结合微信支付或线下扫码等方式实现。以下是一个通过小程序扫码核销卡券的示例: + +```javascript +Page({ + // 扫码核销卡券 + scanToVerifyCard() { + // 调用扫码接口 + wx.scanCode({ + onlyFromCamera: true, // 只允许从相机扫码 + scanType: ['qrCode'], // 只扫描二维码 + success: res => { + // 解析扫码结果 + const code = res.result; + + // 验证扫码结果格式 + if (!code) { + wx.showToast({ + title: '无效的二维码', + icon: 'none' + }); + return; + } + + // 显示加载提示 + wx.showLoading({ title: '核销中...' }); + + // 发送核销请求到服务器 + wx.request({ + url: 'https://your-server.com/api/cards/verify', + method: 'POST', + data: { code }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: result => { + wx.hideLoading(); + + if (result.data.success) { + wx.showToast({ + title: '核销成功', + icon: 'success' + }); + + // 更新页面数据 + this.refreshData(); + } else { + wx.showToast({ + title: result.data.message || '核销失败', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('核销请求失败', err); + } + }); + }, + fail: err => { + console.log('扫码取消或失败', err); + } + }); + }, + + // 刷新页面数据 + refreshData() { + // 重新加载页面数据 + } +}); +``` + +**服务器端核销卡券示例(Node.js):** + +```javascript +const axios = require('axios'); + +async function verifyCard(req, res) { + const { code } = req.body; + + try { + // 获取access_token + const accessToken = await getAccessToken(); + + // 调用核销接口 + const verifyResponse = await axios.post( + `https://api.weixin.qq.com/card/code/consume?access_token=${accessToken}`, + { + code, + card_id: '' // 可选,卡券ID。如果不填写,将默认查询code对应的卡券信息 + } + ); + + if (verifyResponse.data.errcode === 0) { + // 核销成功 + const cardInfo = verifyResponse.data.card; + + // 记录核销信息 + await CardVerification.create({ + code, + cardId: cardInfo.card_id, + userId: req.user.userId, + verifiedAt: new Date() + }); + + res.json({ + success: true, + message: '核销成功', + cardInfo + }); + } else { + res.status(400).json({ + success: false, + message: verifyResponse.data.errmsg || '核销失败', + error: verifyResponse.data + }); + } + } catch (error) { + console.error('核销卡券错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} +``` + +## 6. 订阅消息 + +### 6.1 订阅消息概述 + +微信小程序订阅消息是一种允许开发者在特定场景下向用户发送服务通知的能力。与公众号模板消息不同,订阅消息需要用户主动订阅,且每次订阅后只能发送一条消息。 + +**订阅消息的主要特点:** + +- **一次性订阅**:用户每次订阅后,开发者可发送一条消息 +- **场景限制**:必须在特定场景下才能发起订阅 +- **数量限制**:单个小程序可创建的模板数量有限 +- **内容限制**:消息内容必须符合所选模板类目 + +**订阅消息的使用流程:** + +1. 创建订阅消息模板 +2. 获取用户订阅授权 +3. 发送订阅消息 + +### 6.2 申请订阅消息模板 + +在使用订阅消息前,需要在微信公众平台申请订阅消息模板: + +1. 登录[微信公众平台](https://mp.weixin.qq.com/) +2. 进入「功能」-「订阅消息」 +3. 选择「添加模板」,选择模板类目 +4. 设置模板标题、内容和关键词 +5. 提交审核 + +审核通过后,可以获取到模板ID,用于后续的订阅和发送操作。 + +### 6.3 获取订阅权限 + +小程序通过调用`wx.requestSubscribeMessage()`接口获取用户的订阅授权: + +```javascript +Page({ + // 请求订阅消息权限 + requestSubscribe() { + // 订阅消息模板ID列表 + const tmplIds = ['your-template-id-1', 'your-template-id-2']; + + wx.requestSubscribeMessage({ + tmplIds, + success: res => { + console.log('订阅结果', res); + + // 处理订阅结果 + // res格式: { 'your-template-id-1': 'accept', 'your-template-id-2': 'reject' } + + const acceptedTmplIds = []; + const rejectedTmplIds = []; + + tmplIds.forEach(tmplId => { + if (res[tmplId] === 'accept') { + acceptedTmplIds.push(tmplId); + } else if (res[tmplId] === 'reject') { + rejectedTmplIds.push(tmplId); + } + }); + + // 将订阅结果上报服务器 + if (acceptedTmplIds.length > 0) { + this.reportSubscription(acceptedTmplIds); + + wx.showToast({ + title: '订阅成功', + icon: 'success' + }); + } else { + wx.showToast({ + title: '您拒绝了订阅', + icon: 'none' + }); + } + }, + fail: err => { + console.error('订阅请求失败', err); + + wx.showToast({ + title: '订阅请求失败', + icon: 'none' + }); + } + }); + }, + + // 上报订阅结果 + reportSubscription(tmplIds) { + wx.request({ + url: 'https://your-server.com/api/subscribe', + method: 'POST', + data: { tmplIds }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + } + }); + } +}); +``` + +### 6.4 发送订阅消息 + +获取用户订阅授权后,开发者可以在满足条件时向用户发送一条订阅消息: + +**服务器端发送订阅消息示例(Node.js):** + +```javascript +const axios = require('axios'); + +async function sendSubscribeMessage(req, res) { + const { openid, templateId, data, page } = req.body; + + try { + // 获取access_token + const accessToken = await getAccessToken(); + + // 调用发送订阅消息接口 + const sendResponse = await axios.post( + `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`, + { + touser: openid, + template_id: templateId, + page: page || 'pages/index/index', // 可选,点击消息后跳转的页面 + data, // 模板数据 + miniprogram_state: 'formal' // developer为开发版、trial为体验版、formal为正式版 + } + ); + + if (sendResponse.data.errcode === 0) { + // 发送成功 + res.json({ + success: true, + message: '发送成功' + }); + } else { + res.status(400).json({ + success: false, + message: sendResponse.data.errmsg || '发送失败', + error: sendResponse.data + }); + } + } catch (error) { + console.error('发送订阅消息错误', error); + res.status(500).json({ success: false, message: '服务器错误' }); + } +} +``` + +**发送订阅消息的最佳实践:** + +1. **选择合适的场景**:在用户完成支付、预约成功等关键节点请求订阅 +2. **明确订阅用途**:在请求订阅时清晰说明消息用途和内容 +3. **及时发送**:在触发条件满足后立即发送,不要延迟 +4. **避免频繁请求**:不要在短时间内多次请求用户订阅 +5. **优化跳转页面**:设置合适的跳转页面,提升用户体验 + +## 7. 微信运动 + +### 7.1 微信运动数据获取 + +微信小程序可以获取用户在微信运动中的步数数据,需要用户授权。获取步数数据的流程如下: + +1. **获取用户授权**: + - 使用`wx.authorize()`或`button`组件获取`scope.werun`权限 + +2. **获取微信运动数据**: + - 调用`wx.getWeRunData()`接口获取加密的运动数据 + +3. **解密数据**: + - 将加密数据发送到开发者服务器进行解密 + +**前端获取微信运动数据示例:** + +```javascript +Page({ + // 获取微信运动数据 + getWeRunData() { + // 检查是否已授权 + wx.getSetting({ + success: res => { + if (res.authSetting['scope.werun']) { + // 已授权,直接获取数据 + this.getRunData(); + } else { + // 未授权,请求授权 + wx.authorize({ + scope: 'scope.werun', + success: () => { + // 授权成功,获取数据 + this.getRunData(); + }, + fail: err => { + console.log('授权失败', err); + + // 引导用户通过按钮授权 + this.setData({ showAuthButton: true }); + } + }); + } + } + }); + }, + + // 通过按钮授权 + handleAuthByButton(e) { + if (e.detail.errMsg === 'getUserInfo:ok') { + // 授权成功,获取数据 + this.getRunData(); + } + }, + + // 获取运动数据 + getRunData() { + wx.showLoading({ title: '获取运动数据...' }); + + wx.getWeRunData({ + success: res => { + // 获取加密的运动数据 + const encryptedData = res.encryptedData; + const iv = res.iv; + + // 将加密数据发送到服务器解密 + wx.request({ + url: 'https://your-server.com/api/werun/decrypt', + method: 'POST', + data: { + encryptedData, + iv + }, + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + success: result => { + wx.hideLoading(); + + if (result.data.success) { + // 获取解密后的步数数据 + const stepInfoList = result.data.stepInfoList; + + // 更新页面数据 + this.setData({ stepInfoList }); + } else { + wx.showToast({ + title: result.data.message || '获取步数失败', + icon: 'none' + }); + } + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + console.error('请求解密失败', err); + } + }); + }, + fail: err => { + wx.hideLoading(); + wx.showToast({ + title: '获取运动数据失败', + icon: 'none' + }); + console.error('获取运动数据失败', err); + } + }); + } +}); +``` \ No newline at end of file diff --git a/docs/jobPro/wechat-operation-center.md b/docs/jobPro/wechat-operation-center.md new file mode 100644 index 000000000..44f3c32e1 --- /dev/null +++ b/docs/jobPro/wechat-operation-center.md @@ -0,0 +1,1118 @@ +--- +title: 微信小程序运维中心完全指南 +author: 哪吒 +date: '2023-12-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# 微信小程序运维中心完全指南 + +## 目录 + +- [微信小程序运维中心完全指南](#微信小程序运维中心完全指南) + - [目录](#目录) + - [1. 运维中心概述](#1-运维中心概述) + - [1.1 什么是微信小程序运维中心](#11-什么是微信小程序运维中心) + - [1.2 运维中心的主要功能](#12-运维中心的主要功能) + - [1.3 运维中心的价值与意义](#13-运维中心的价值与意义) + - [2. 运维中心访问与权限](#2-运维中心访问与权限) + - [2.1 访问方式](#21-访问方式) + - [2.2 权限管理](#22-权限管理) + - [2.3 角色与职责](#23-角色与职责) + - [3. 版本管理](#3-版本管理) + - [3.1 版本发布流程](#31-版本发布流程) + - [3.2 版本回退](#32-版本回退) + - [3.3 灰度发布](#33-灰度发布) + - [3.4 版本比对](#34-版本比对) + - [4. 质量监控](#4-质量监控) + - [4.1 性能监控](#41-性能监控) + - [4.2 错误监控](#42-错误监控) + - [4.3 告警配置](#43-告警配置) + - [4.4 日志查询](#44-日志查询) + - [5. 用户反馈管理](#5-用户反馈管理) + - [5.1 反馈收集](#51-反馈收集) + - [5.2 反馈分类与处理](#52-反馈分类与处理) + - [5.3 反馈统计与分析](#53-反馈统计与分析) + - [6. 数据分析](#6-数据分析) + - [6.1 访问数据分析](#61-访问数据分析) + - [6.2 用户画像](#62-用户画像) + - [6.3 自定义分析](#63-自定义分析) + - [6.4 数据导出](#64-数据导出) + - [7. 安全中心](#7-安全中心) + - [7.1 安全检测](#71-安全检测) + - [7.2 漏洞修复](#72-漏洞修复) + - [7.3 安全加固建议](#73-安全加固建议) + - [8. 客服消息管理](#8-客服消息管理) + - [8.1 消息接收与回复](#81-消息接收与回复) + - [8.2 客服人员管理](#82-客服人员管理) + - [8.3 会话质量监控](#83-会话质量监控) + - [9. 运维中心API](#9-运维中心api) + - [9.1 数据获取API](#91-数据获取api) + - [9.2 操作执行API](#92-操作执行api) + - [9.3 API调用限制](#93-api调用限制) + - [10. 最佳实践](#10-最佳实践) + - [10.1 运维流程规范](#101-运维流程规范) + - [10.2 监控告警策略](#102-监控告警策略) + - [10.3 问题排查方法](#103-问题排查方法) + - [10.4 团队协作模式](#104-团队协作模式) + +## 1. 运维中心概述 + +### 1.1 什么是微信小程序运维中心 + +微信小程序运维中心是微信官方为开发者提供的一站式小程序管理平台,集成了版本发布、质量监控、用户反馈、数据分析等多种功能,帮助开发者高效管理和优化小程序。通过运维中心,开发者可以全面了解小程序的运行状况,及时发现并解决问题,提升用户体验。 + +运维中心位于微信公众平台的小程序管理后台中,是小程序生命周期管理的核心工具。它不仅提供了直观的数据可视化界面,还支持多种自动化运维能力,大大降低了小程序的运维成本。 + +### 1.2 运维中心的主要功能 + +微信小程序运维中心提供了丰富的功能模块,主要包括: + +1. **版本管理**:管理小程序的版本发布、回退、灰度发布等 +2. **质量监控**:监控小程序的性能指标、错误日志、崩溃情况等 +3. **用户反馈**:收集、分类和处理用户的反馈信息 +4. **数据分析**:分析小程序的访问数据、用户行为、转化率等 +5. **安全中心**:检测小程序的安全漏洞,提供修复建议 +6. **客服消息**:管理小程序的客服消息会话 + +这些功能模块相互关联,共同构成了完整的小程序运维体系,帮助开发者从多个维度优化小程序。 + +### 1.3 运维中心的价值与意义 + +微信小程序运维中心对开发者和企业具有重要价值: + +1. **提升运维效率**:集中化的管理平台大大提高了运维效率,减少了人力成本 +2. **保障服务质量**:实时监控和告警机制帮助开发者及时发现并解决问题 +3. **优化用户体验**:通过数据分析和用户反馈,持续优化小程序体验 +4. **降低运营风险**:版本管理和安全检测降低了线上故障和安全风险 +5. **数据驱动决策**:丰富的数据分析能力支持开发者做出数据驱动的决策 + +对于企业级小程序来说,运维中心是保障业务稳定性和持续优化的关键工具,也是小程序生态中不可或缺的一部分。 + +## 2. 运维中心访问与权限 + +### 2.1 访问方式 + +微信小程序运维中心提供了多种访问方式,以满足不同场景的需求: + +1. **微信公众平台网页版**: + - 登录微信公众平台(mp.weixin.qq.com) + - 进入「小程序」-「管理」-「运维中心」 + - 适合日常管理和数据查看 + +2. **微信公众平台小程序**: + - 在微信中搜索「小程序助手」并关注 + - 点击「运维中心」入口 + - 适合移动场景下的紧急处理 + +3. **API接口访问**: + - 通过微信开放平台提供的API接口 + - 可实现自动化运维和数据集成 + - 适合大型企业的定制化运维需求 + +不同的访问方式功能略有差异,网页版提供最完整的功能,小程序版侧重于移动场景下的核心功能,API接口则提供了自动化的可能性。 + +### 2.2 权限管理 + +运维中心采用基于角色的权限管理机制,确保安全性的同时提供灵活的权限分配: + +1. **权限级别**: + - 管理员:拥有所有权限,可进行所有操作 + - 运营者:拥有数据查看、用户反馈处理等权限 + - 开发者:拥有版本发布、质量监控等技术相关权限 + - 数据分析师:仅拥有数据分析模块的查看权限 + - 客服人员:仅拥有客服消息模块的操作权限 + +2. **权限分配方法**: + - 在公众平台「小程序」-「管理」-「成员管理」中添加成员 + - 为成员分配相应角色 + - 可针对特定模块进行精细化权限设置 + +3. **权限审计**: + - 系统记录所有关键操作的执行人和执行时间 + - 管理员可查看操作日志,追溯责任 + +合理的权限设置可以避免误操作和安全风险,建议根据团队成员的职责进行精细化的权限分配。 + +### 2.3 角色与职责 + +在小程序运维团队中,不同角色承担不同的职责: + +1. **管理员**: + - 负责整体运维策略的制定 + - 管理团队成员和权限分配 + - 处理重大运维事件和决策 + +2. **运营人员**: + - 日常数据监控和分析 + - 用户反馈的收集和处理 + - 内容更新和活动策划 + +3. **开发人员**: + - 版本发布和回退 + - 质量监控和问题修复 + - 性能优化和技术改进 + +4. **数据分析师**: + - 深度数据分析和洞察 + - 用户行为研究 + - 提供数据驱动的优化建议 + +5. **客服人员**: + - 处理用户咨询和投诉 + - 收集用户需求和建议 + - 提供用户支持服务 + +明确的角色划分和职责定义有助于团队协作和高效运维,特别是对于大型小程序项目,建立专业的运维团队尤为重要。 + +## 3. 版本管理 + +### 3.1 版本发布流程 + +微信小程序的版本发布是一个规范化的流程,通过运维中心可以实现高效的版本管理: + +1. **版本准备阶段**: + - 开发完成后进行内部测试 + - 使用「体验版」进行功能验证 + - 准备版本说明和更新日志 + +2. **提交审核**: + - 在运维中心选择「版本管理」-「提交审核」 + - 填写版本信息、更新内容、测试帐号等 + - 上传小程序代码包 + +3. **审核过程**: + - 微信团队进行审核,一般1-7天完成 + - 可在运维中心查看审核状态和进度 + - 如被拒绝,可查看拒绝原因并修改后重新提交 + +4. **发布上线**: + - 审核通过后,在运维中心选择「发布」 + - 可选择「全量发布」或「分阶段发布」 + - 发布后用户将收到新版本 + +5. **发布后监控**: + - 密切关注新版本的性能指标和错误日志 + - 收集用户反馈,评估版本质量 + - 必要时准备紧急修复版本 + +规范的版本发布流程可以降低发布风险,确保小程序的稳定性和用户体验。 + +### 3.2 版本回退 + +当新版本出现严重问题时,可以通过版本回退功能快速恢复服务: + +1. **回退条件**: + - 新版本出现严重bug或崩溃 + - 性能严重下降影响用户使用 + - 功能不符合预期,需要重新调整 + +2. **回退操作**: + - 在运维中心选择「版本管理」-「版本回退」 + - 选择要回退到的历史版本 + - 确认回退操作 + +3. **回退注意事项**: + - 回退会立即生效,所有用户将使用旧版本 + - 回退可能导致数据不一致,需提前评估影响 + - 回退后应立即修复问题,准备新版本 + +4. **回退后的处理**: + - 分析导致问题的原因 + - 完善测试流程,避免类似问题再次发生 + - 制定更严格的发布标准 + +版本回退是应对紧急情况的有效手段,但应谨慎使用,并建立完善的回退预案。 + +### 3.3 灰度发布 + +灰度发布是一种风险可控的发布策略,通过运维中心可以实现精细化的灰度控制: + +1. **灰度发布原理**: + - 将新版本逐步推送给部分用户 + - 根据反馈和监控数据评估版本质量 + - 逐步扩大发布范围,最终全量发布 + +2. **灰度策略设置**: + - 在运维中心选择「版本管理」-「分阶段发布」 + - 设置灰度比例(如5%、20%、50%、100%) + - 设置每个阶段的观察时间 + +3. **灰度用户选择**: + - 随机选择:系统随机选择指定比例的用户 + - 白名单:指定特定用户群体优先体验 + - 地域选择:针对特定地区用户进行灰度 + +4. **灰度监控与决策**: + - 密切监控灰度用户的使用数据和反馈 + - 出现严重问题时可随时中止灰度 + - 数据良好时可加速灰度进程 + +灰度发布是降低发布风险的有效手段,特别适合用户量大、业务复杂的小程序。 + +### 3.4 版本比对 + +版本比对功能帮助开发者清晰了解不同版本间的差异: + +1. **比对功能入口**: + - 在运维中心选择「版本管理」-「版本记录」 + - 选择两个版本,点击「版本比对」 + +2. **比对内容**: + - 代码差异:显示修改、新增、删除的文件和代码 + - 配置差异:显示app.json等配置文件的变化 + - 资源差异:显示图片、音频等资源文件的变化 + +3. **比对应用场景**: + - 版本回顾:了解历史版本的演进过程 + - 问题排查:定位可能导致问题的代码变更 + - 审计追溯:确认特定修改的引入时间和责任人 + +4. **比对技巧**: + - 关注关键文件的变化(如app.js、核心业务逻辑) + - 注意配置文件的细微变化 + - 结合commit记录理解变更意图 + +版本比对是开发和运维团队的重要工具,有助于理解版本变化和快速定位问题。 + +## 4. 质量监控 + +### 4.1 性能监控 + +性能监控是保障小程序用户体验的关键环节,运维中心提供了全面的性能监控能力: + +1. **关键性能指标**: + - 启动时间:从点击图标到可交互的时间 + - 页面切换时间:页面间导航的响应时间 + - JS执行时间:脚本执行的耗时 + - 渲染时间:页面渲染完成的时间 + - 网络请求时间:API调用的响应时间 + +2. **监控维度**: + - 时间维度:小时、天、周、月的趋势变化 + - 地域维度:不同地区用户的性能差异 + - 机型维度:不同设备的性能表现 + - 网络维度:不同网络环境下的性能 + - 版本维度:不同版本间的性能对比 + +3. **性能分析工具**: + - 性能趋势图:直观展示性能变化趋势 + - 性能瀑布图:详细分析页面加载各阶段耗时 + - 性能热点图:识别性能瓶颈 + +4. **性能优化建议**: + - 系统自动分析性能数据 + - 提供针对性的优化建议 + - 预估优化后的效果提升 + +通过持续的性能监控和优化,可以显著提升小程序的用户体验和留存率。 + +### 4.2 错误监控 + +错误监控帮助开发者及时发现并修复小程序中的问题: + +1. **错误类型**: + - JS执行错误:代码语法错误、运行时异常 + - API调用错误:参数错误、权限不足等 + - 网络请求错误:请求超时、服务端错误等 + - 资源加载错误:图片、音频等资源加载失败 + - 白屏/崩溃:小程序无法正常渲染或运行 + +2. **错误信息详情**: + - 错误类型和描述 + - 错误发生的页面和位置 + - 错误的堆栈信息 + - 用户的设备和环境信息 + - 错误发生前的操作路径 + +3. **错误统计与分析**: + - 错误发生频率和趋势 + - 影响用户数量和比例 + - 错误的严重程度评级 + - 错误与版本、地域、机型的关联分析 + +4. **错误处理流程**: + - 错误发现:通过监控系统或告警发现错误 + - 错误分类:根据严重程度和影响范围分类 + - 错误定位:利用详细信息定位问题根源 + - 错误修复:开发修复方案并验证 + - 修复发布:通过版本更新推送修复 + +建立完善的错误监控和处理机制,可以大幅提高小程序的稳定性和可靠性。 + +### 4.3 告警配置 + +告警系统是质量监控的重要组成部分,可以帮助团队及时响应异常情况: + +1. **告警类型**: + - 性能告警:性能指标超过阈值 + - 错误告警:错误率或错误数超过阈值 + - 流量告警:访问量异常波动 + - 接口告警:API调用失败率高 + - 自定义告警:根据业务需求自定义 + +2. **告警阈值设置**: + - 在运维中心选择「质量监控」-「告警配置」 + - 为不同指标设置告警阈值 + - 设置告警级别(提醒、警告、严重) + - 配置告警触发条件(连续多久超过阈值) + +3. **告警通知方式**: + - 微信消息:直接发送到负责人微信 + - 邮件通知:发送详细告警信息到邮箱 + - 短信通知:紧急情况下的短信提醒 + - 企业微信:集成到企业微信群或应用 + - 自定义webhook:接入第三方系统 + +4. **告警处理流程**: + - 接收告警:相关人员收到告警通知 + - 确认告警:验证告警的真实性和严重程度 + - 处理告警:采取相应措施解决问题 + - 告警关闭:问题解决后关闭告警 + - 复盘总结:分析告警原因,优化系统 + +合理的告警配置可以在问题扩大前及时发现并处理,降低对用户的影响。 + +### 4.4 日志查询 + +日志查询功能为问题排查和分析提供了强大支持: + +1. **日志类型**: + - 运行日志:小程序运行过程中的日志记录 + - 请求日志:网络请求的详细信息 + - 用户行为日志:用户操作和页面访问记录 + - 系统日志:小程序框架和系统相关日志 + - 自定义日志:开发者通过console输出的日志 + +2. **日志查询方式**: + - 在运维中心选择「质量监控」-「日志查询」 + - 设置查询条件:时间范围、日志级别、关键词等 + - 支持高级查询语法,如正则表达式 + - 可保存常用查询条件为模板 + +3. **日志分析工具**: + - 日志上下文查看:查看特定日志前后的相关日志 + - 日志统计分析:统计日志出现频率和分布 + - 日志导出:将查询结果导出为文件深入分析 + - 日志关联:将日志与错误、性能数据关联 + +4. **日志最佳实践**: + - 合理使用日志级别(debug、info、warn、error) + - 在关键节点添加有意义的日志 + - 日志中包含足够的上下文信息 + - 敏感信息脱敏处理 + - 建立日志查询和分析的标准流程 + +日志查询是问题排查的基础工具,掌握高效的日志分析方法可以大大提高问题解决效率。 + +## 5. 用户反馈管理 + +### 5.1 反馈收集 + +用户反馈是产品改进的宝贵资源,运维中心提供了多种反馈收集渠道: + +1. **内置反馈渠道**: + - 小程序右上角「...」-「反馈与投诉」 + - 自动收集到运维中心的反馈管理模块 + - 包含用户基本信息和设备环境 + +2. **自定义反馈入口**: + - 在小程序内设置专门的反馈页面 + - 通过表单收集更详细的反馈信息 + - 可添加截图、录屏等辅助信息 + +3. **被动反馈收集**: + - 应用商店评论监控 + - 社交媒体提及监控 + - 客服会话中的反馈提取 + +4. **反馈激励机制**: + - 为有价值的反馈提供积分或优惠券 + - 感谢用户并告知反馈处理结果 + - 建立反馈专家用户群体 + +建立多元化的反馈收集渠道,可以全面了解用户需求和问题,为产品优化提供方向。 + +### 5.2 反馈分类与处理 + +有效的反馈管理需要系统化的分类和处理流程: + +1. **反馈分类方法**: + - 按类型:功能建议、bug报告、体验问题、投诉等 + - 按模块:登录、支付、内容、界面等产品模块 + - 按优先级:紧急、高、中、低 + - 按状态:待处理、处理中、已解决、已关闭 + +2. **反馈处理流程**: + - 初步筛选:过滤无效反馈,初步分类 + - 详细分析:深入了解反馈内容和背景 + - 分配责任:将反馈分配给相关团队或人员 + - 制定方案:根据反馈制定解决方案 + - 实施解决:执行解决方案 + - 验证效果:确认问题是否解决 + - 回复用户:告知用户处理结果 + +3. **反馈处理工具**: + - 在运维中心的「用户反馈」模块管理反馈 + - 支持批量操作和状态更新 + - 可关联相似反馈,统一处理 + - 与问题追踪系统(如Jira)集成 + +4. **反馈回复模板**: + - 为常见反馈类型准备回复模板 + - 保持回复语气友好专业 + - 包含问题解决方案或后续计划 + - 鼓励用户继续提供反馈 + +系统化的反馈处理流程可以提高处理效率,确保用户反馈得到及时有效的响应。 + +### 5.3 反馈统计与分析 + +反馈数据的统计和分析可以揭示产品改进的方向: + +1. **反馈数据指标**: + - 反馈总量及趋势 + - 不同类型反馈的分布 + - 反馈解决率和平均处理时间 + - 用户满意度(反馈处理后的评价) + - 重复反馈率(同一问题被多次反馈) + +2. **反馈分析维度**: + - 时间维度:反馈随时间的变化趋势 + - 版本维度:不同版本的反馈对比 + - 用户维度:不同用户群体的反馈差异 + - 地域维度:不同地区用户的反馈特点 + +3. **反馈洞察方法**: + - 热点问题识别:找出反馈最集中的问题 + - 关联分析:反馈与用户行为、性能数据的关联 + - 情感分析:分析反馈中的情感倾向 + - 文本挖掘:从大量文本反馈中提取关键信息 + +4. **反馈应用机制**: + - 产品迭代规划:将高价值反馈纳入产品规划 + - 优先级确定:基于反馈量和影响确定修复优先级 + - 效果评估:通过反馈变化评估改进效果 + - 用户沟通:主动告知用户基于反馈的改进 + +深入的反馈分析可以帮助团队更好地理解用户需求,做出数据驱动的产品决策。 + +## 6. 数据分析 + +### 6.1 访问数据分析 + +访问数据分析帮助开发者了解小程序的使用情况和用户行为: + +1. **核心访问指标**: + - 访问人数(UV):独立访问用户数 + - 访问次数(PV):总访问页面数 + - 人均访问次数:PV/UV + - 访问时长:用户在小程序中停留的时间 + - 跳出率:只访问一个页面就离开的比例 + - 转化率:完成特定目标(如下单)的比例 + +2. **访问数据维度**: + - 时间维度:小时、日、周、月的趋势 + - 地域维度:不同省市、国家的访问情况 + - 来源维度:不同访问来源(如搜索、分享) + - 设备维度:不同机型、系统版本的访问 + - 用户维度:新老用户、不同属性用户的访问 + +3. **页面分析**: + - 页面访问排行:访问量最高的页面 + - 页面停留时间:用户在各页面的停留时长 + - 页面转化率:页面的目标完成率 + - 页面路径分析:用户在页面间的流转路径 + - 页面性能与访问的关联:性能对访问的影响 + +4. **访问趋势分析**: + - 长期趋势:识别长期增长或下降趋势 + - 周期性波动:识别每日、每周的规律 + - 异常检测:识别异常的访问波动 + - 活动效果:分析营销活动对访问的影响 + +通过全面的访问数据分析,可以深入了解用户使用小程序的行为模式,为产品优化和运营决策提供依据。 + +### 6.2 用户画像 + +用户画像功能帮助开发者深入了解小程序的用户群体特征: + +1. **基础人口统计学特征**: + - 性别分布:男性/女性用户比例 + - 年龄分布:不同年龄段用户占比 + - 地域分布:用户所在省市、城市层级 + - 设备偏好:常用机型、价格段 + - 使用场景:使用小程序的时间和环境 + +2. **行为特征分析**: + - 活跃度:日活、周活、月活情况 + - 使用频率:使用小程序的频率分布 + - 使用时长:单次使用和累计使用时长 + - 功能偏好:最常使用的功能和页面 + - 消费行为:消费金额、频次、品类 + +3. **用户分群**: + - 新用户/老用户:基于首次使用时间 + - 高/中/低价值用户:基于消费额或活跃度 + - 流失用户:长期未使用的用户 + - 自定义分群:基于特定条件组合的用户群体 + +4. **用户画像应用**: + - 精准营销:针对特定用户群体的营销活动 + - 产品优化:基于主要用户群体的需求优化产品 + - 内容推荐:根据用户特征推荐个性化内容 + - 用户增长:识别高潜力用户群体,制定增长策略 + +深入的用户画像分析可以帮助团队更好地理解目标用户,提供更符合用户需求的产品和服务。 + +### 6.3 自定义分析 + +自定义分析功能满足开发者个性化的数据分析需求: + +1. **自定义事件**: + - 在小程序代码中埋点,记录特定用户行为 + - 通过wx.reportAnalytics(eventName, data)上报事件 + - 在运维中心配置事件和属性 + - 支持字符串、数值、布尔等多种属性类型 + +2. **自定义分析报表**: + - 在运维中心选择「数据分析」-「自定义分析」 + - 选择分析事件、维度和指标 + - 设置时间范围和过滤条件 + - 生成图表并保存为模板 + +3. **高级分析功能**: + - 漏斗分析:跟踪多步骤流程的转化率 + - 留存分析:分析用户在不同时间段的留存率 + - 归因分析:分析用户行为的影响因素 + - 路径分析:分析用户在小程序中的行为路径 + +4. **自定义分析最佳实践**: + - 制定清晰的埋点方案,避免数据冗余 + - 关注核心业务流程和关键转化节点 + - 定期审查和优化埋点,确保数据质量 + - 将分析结果与业务目标关联,指导决策 + +自定义分析为开发者提供了灵活的数据分析能力,可以根据业务特点深入挖掘数据价值。 + +### 6.4 数据导出 + +数据导出功能支持更深入的离线分析和数据集成: + +1. **数据导出方式**: + - 在运维中心选择「数据分析」-「数据导出」 + - 选择要导出的数据类型和时间范围 + - 选择导出格式(CSV、Excel等) + - 点击导出,等待下载链接 + +2. **可导出的数据类型**: + - 访问数据:UV、PV、停留时间等 + - 用户数据:用户属性、行为特征等 + - 性能数据:启动时间、页面切换时间等 + - 错误数据:JS错误、API错误等 + - 自定义事件数据:自定义埋点数据 + +3. **数据导出限制**: + - 单次导出的数据量限制 + - 导出频率限制 + - 数据保留期限 + - 敏感数据脱敏处理 + +4. **导出数据的应用**: + - 与企业BI系统集成 + - 使用专业分析工具进行深度分析 + - 与其他业务数据关联分析 + - 生成自定义报表和可视化 + +数据导出功能为企业级用户提供了更大的数据分析自由度,支持更复杂的数据应用场景。 + +## 7. 安全中心 + +### 7.1 安全检测 + +安全中心提供全面的安全检测能力,帮助开发者发现并解决潜在安全风险: + +1. **安全检测范围**: + - 代码安全:检测代码中的安全漏洞 + - 配置安全:检测不安全的配置项 + - 网络安全:检测不安全的网络请求 + - 数据安全:检测敏感数据的处理方式 + - 第三方组件安全:检测第三方库的安全风险 + +2. **安全检测方式**: + - 自动检测:系统自动进行安全扫描 + - 手动触发:开发者主动发起安全检测 + - 版本审核:版本提交审核时进行检测 + - 定期检测:按计划定期进行安全检测 + +3. **安全风险等级**: + - 严重:可能导致数据泄露或系统被控制 + - 高危:可能影响系统正常运行或数据完整性 + - 中危:可能导致部分功能异常或性能问题 + - 低危:对系统影响较小的潜在风险 + +4. **安全检测报告**: + - 在运维中心选择「安全中心」-「安全检测」 + - 查看检测结果和风险详情 + - 了解风险影响和修复建议 + - 追踪历史检测记录和修复进度 + +定期的安全检测可以帮助开发者及时发现并解决安全风险,保障小程序和用户数据的安全。 + +### 7.2 漏洞修复 + +发现安全漏洞后,需要系统化的修复流程: + +1. **漏洞修复流程**: + - 漏洞确认:验证漏洞的真实性和影响 + - 风险评估:评估漏洞的严重程度和影响范围 + - 修复方案:制定技术修复方案 + - 修复实施:在代码中实施修复 + - 修复验证:验证修复的有效性 + - 版本发布:将修复推送给用户 + +2. **常见漏洞类型及修复方法**: + - 数据泄露:加强数据加密和访问控制 + - 注入攻击:输入验证和参数过滤 + - 越权访问:完善权限检查机制 + - 敏感信息硬编码:使用配置或安全存储 + - 不安全的网络请求:使用HTTPS和证书验证 + +3. **紧急修复机制**: + - 严重漏洞的快速审核通道 + - 热修复技术(在某些情况下可用) + - 临时功能下线或限制 + +4. **修复后的安全加固**: + - 添加相关安全测试用例 + - 更新安全开发规范 + - 加强代码审查流程 + - 定期安全培训 + +及时有效的漏洞修复是保障小程序安全的关键环节,应建立完善的漏洞响应机制。 + +### 7.3 安全加固建议 + +安全中心提供了全面的安全加固建议,帮助开发者提升小程序的安全性: + +1. **代码层面加固**: + - 敏感数据加密存储 + - 输入数据严格验证和过滤 + - 避免敏感信息硬编码 + - 使用安全的API调用方式 + - 定期更新第三方库 + +2. **网络层面加固**: + - 全面使用HTTPS + - 实现请求签名机制 + - 添加防重放攻击措施 + - 设置合理的请求超时 + - 实现请求频率限制 + +3. **数据层面加固**: + - 最小权限原则收集用户数据 + - 敏感数据脱敏处理 + - 定期数据备份 + - 完善数据销毁机制 + - 数据传输加密 + +4. **运营层面加固**: + - 建立安全应急响应机制 + - 定期安全培训和演练 + - 制定安全开发规范 + - 实施安全代码审查 + - 建立漏洞奖励计划 + +全面的安全加固可以从多个层面提升小程序的安全性,降低安全风险。 + +## 8. 客服消息管理 + +### 8.1 消息接收与回复 + +客服消息功能允许小程序与用户进行即时沟通: + +1. **客服消息开通**: + - 在微信公众平台开通客服消息功能 + - 在小程序配置文件中启用客服功能 + - 设置客服入口和显示方式 + +2. **消息接收方式**: + - 在运维中心的「客服消息」模块查看 + - 通过API接收消息并集成到自有系统 + - 使用第三方客服工具接收和管理 + +3. **消息回复方式**: + - 人工回复:客服人员直接回复用户 + - 自动回复:设置关键词或问题的自动回复 + - 智能客服:接入AI客服系统 + - 混合模式:AI初步处理,必要时转人工 + +4. **消息类型支持**: + - 文本消息:普通文字交流 + - 图片消息:发送和接收图片 + - 语音消息:语音对话 + - 视频消息:短视频交流 + - 图文消息:富媒体内容 + - 小程序卡片:分享小程序页面 + +高效的客服消息管理可以提升用户满意度和问题解决效率。 + +### 8.2 客服人员管理 + +客服团队的管理是提供优质客服服务的基础: + +1. **客服账号管理**: + - 在运维中心添加和管理客服账号 + - 设置客服权限和职责范围 + - 分配客服工作组和专长领域 + - 设置工作时间和排班 + +2. **客服培训与规范**: + - 产品知识培训 + - 沟通技巧培训 + - 制定标准回复话术 + - 建立问题处理流程 + - 设定服务质量标准 + +3. **工作量管理**: + - 监控客服工作量和会话数 + - 智能分配用户咨询 + - 设置最大并发会话数 + - 提供繁忙时段的支持机制 + +4. **客服绩效评估**: + - 响应时间:首次回复和平均回复时间 + - 解决率:一次性解决问题的比例 + - 用户满意度:用户评价和反馈 + - 会话效率:平均会话时长和解决速度 + +科学的客服人员管理可以提高客服团队的工作效率和服务质量。 + +### 8.3 会话质量监控 + +会话质量监控帮助维持高水平的客服服务: + +1. **会话质量指标**: + - 首次响应时间:收到消息到首次回复的时间 + - 平均响应时间:会话中的平均回复速度 + - 会话解决率:成功解决问题的会话比例 + - 会话满意度:用户对会话的评价 + - 转人工率:从自动回复转为人工的比例 + +2. **会话监控方式**: + - 实时监控:管理员可实时查看进行中的会话 + - 质量抽检:定期抽查会话记录评估质量 + - 关键词监控:监控特定关键词的会话 + - 用户投诉跟踪:重点关注投诉相关会话 + +3. **会话分析工具**: + - 会话记录查询:按时间、客服、用户等筛选 + - 会话内容分析:识别常见问题和情绪 + - 会话标签:对会话进行分类和标记 + - 会话评分:对会话质量进行评分 + +4. **质量改进机制**: + - 定期质量评审会议 + - 优秀案例分享和学习 + - 针对性培训和指导 + - 更新知识库和标准回复 + +持续的会话质量监控和改进可以不断提升客服服务水平,增强用户满意度。 + +## 9. 运维中心API + +### 9.1 数据获取API + +运维中心提供了丰富的API,支持开发者获取各类数据: + +1. **访问数据API**: + - 获取日/周/月访问趋势 + - 获取访问来源分布 + - 获取地域访问数据 + - 获取用户画像数据 + - 获取页面访问排行 + +2. **性能数据API**: + - 获取启动时间数据 + - 获取页面切换时间 + - 获取网络请求性能 + - 获取JS执行性能 + - 获取性能分布和趋势 + +3. **错误数据API**: + - 获取错误列表和详情 + - 获取错误趋势和分布 + - 获取影响用户数据 + - 获取错误堆栈信息 + +4. **自定义数据API**: + - 获取自定义事件数据 + - 获取漏斗转化数据 + - 获取用户留存数据 + - 获取自定义分析结果 + +5. **API调用示例**(Node.js): + +```javascript +const axios = require('axios'); + +// 获取访问趋势数据 +async function getVisitTrend() { + try { + const response = await axios.get('https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend', { + params: { + access_token: 'YOUR_ACCESS_TOKEN', + begin_date: '20231201', + end_date: '20231207' + } + }); + + console.log('访问趋势数据:', response.data); + return response.data; + } catch (error) { + console.error('获取数据失败:', error); + throw error; + } +} + +// 调用函数 +getVisitTrend(); +``` + +通过API获取的数据可以集成到企业自有系统,实现更灵活的数据应用。 + +### 9.2 操作执行API + +运维中心还提供了执行各种操作的API,支持自动化运维: + +1. **版本管理API**: + - 提交代码审核 + - 发布已审核版本 + - 回退到历史版本 + - 设置灰度发布策略 + +2. **配置管理API**: + - 更新小程序配置 + - 设置域名白名单 + - 配置服务器域名 + - 管理插件设置 + +3. **消息管理API**: + - 发送客服消息 + - 发送订阅消息 + - 获取消息模板 + - 设置自动回复 + +4. **用户管理API**: + - 获取用户基本信息 + - 获取用户手机号 + - 管理用户标签 + - 黑名单管理 + +5. **API调用示例**(Node.js): + +```javascript +const axios = require('axios'); + +// 发送客服消息 +async function sendCustomerServiceMessage(openid, message) { + try { + const response = await axios.post('https://api.weixin.qq.com/cgi-bin/message/custom/send', { + touser: openid, + msgtype: 'text', + text: { + content: message + } + }, { + params: { + access_token: 'YOUR_ACCESS_TOKEN' + } + }); + + console.log('消息发送结果:', response.data); + return response.data; + } catch (error) { + console.error('发送消息失败:', error); + throw error; + } +} + +// 调用函数 +sendCustomerServiceMessage('USER_OPENID', '您好,这是一条客服消息'); +``` + +通过操作执行API,可以实现小程序运维的自动化和系统集成,提高运维效率。 + +### 9.3 API调用限制 + +使用运维中心API时,需要注意各种调用限制: + +1. **访问频率限制**: + - 大多数API限制为每分钟5000次 + - 数据分析类API限制为每分钟500次 + - 高频操作API可能有更严格的限制 + +2. **调用量限制**: + - 每个小程序每天的总调用次数限制 + - 特定API的每日调用次数限制 + - 超出限制可能需要申请提升配额 + +3. **权限限制**: + - 不同API需要不同的权限范围 + - 某些API需要特殊的开通申请 + - 敏感操作API需要额外的安全验证 + +4. **数据范围限制**: + - 历史数据查询一般限制为最近90天 + - 单次查询的数据量有上限 + - 某些数据可能有延迟(如T+1) + +5. **处理API限制的策略**: + - 实现请求频率控制 + - 使用缓存减少重复请求 + - 错误重试机制 + - 分批处理大量数据 + - 监控API调用配额 + +了解并遵守API调用限制,可以避免因超限而导致的服务中断,确保系统稳定运行。 + +## 10. 最佳实践 + +### 10.1 运维流程规范 + +建立规范的运维流程是高效运维的基础: + +1. **版本发布流程**: + - 开发完成 → 内部测试 → 提交审核 → 灰度发布 → 全量发布 + - 每个环节设定明确的责任人和检查点 + - 建立发布前的检查清单 + - 制定发布后的监控计划 + +2. **问题处理流程**: + - 问题发现 → 初步评估 → 分配责任 → 解决方案 → 实施修复 → 验证效果 → 复盘总结 + - 根据问题严重程度设定响应时间 + - 建立问题升级机制 + - 维护问题知识库 + +3. **变更管理流程**: + - 变更申请 → 影响评估 → 变更审批 → 变更实施 → 变更验证 → 变更总结 + - 对重大变更进行风险评估 + - 准备变更回滚方案 + - 在低峰期进行变更 + +4. **日常运维流程**: + - 定时检查:性能监控、错误日志、用户反馈 + - 定期分析:访问数据、用户行为、转化率 + - 定期优化:性能优化、体验改进、功能迭代 + - 定期演练:故障恢复、应急响应 + +规范化的运维流程可以降低人为错误,提高团队协作效率,确保小程序的稳定运行。 + +### 10.2 监控告警策略 + +合理的监控告警策略可以及时发现并解决问题: + +1. **核心指标监控**: + - 业务指标:日活、转化率、订单量等 + - 性能指标:启动时间、响应时间、JS错误率 + - 系统指标:API调用成功率、服务器负载 + - 用户体验指标:页面加载时间、操作响应时间 + +2. **告警级别设置**: + - P0(紧急):影响全部用户的严重问题,需立即处理 + - P1(严重):影响大部分用户的重要问题,需2小时内处理 + - P2(中等):影响部分用户的问题,需24小时内处理 + - P3(轻微):影响少量用户的小问题,可计划性处理 + +3. **告警阈值优化**: + - 基于历史数据设定合理阈值 + - 考虑业务波动规律(如日内、周内波动) + - 设置动态阈值,适应业务增长 + - 定期评估和调整阈值 + +4. **告警噪音控制**: + - 合并相似告警 + - 设置告警静默期 + - 实现告警升级机制 + - 针对性分配告警接收人 + - 定期清理无效告警 + +科学的监控告警策略可以在问题扩大前及时发现并处理,同时避免过多的告警干扰团队工作。 + +### 10.3 问题排查方法 + +高效的问题排查方法可以快速定位和解决小程序问题: + +1. **常见问题类型**: + - 功能问题:功能无法正常使用 + - 性能问题:响应缓慢、卡顿 + - 兼容性问题:在特定设备或系统上异常 + - 网络问题:请求失败、超时 + - 数据问题:数据显示错误或丢失 + +2. **问题排查步骤**: + - 问题复现:确认问题的触发条件和表现 + - 范围确定:确定影响的用户范围和场景 + - 日志分析:查看相关日志和错误信息 + - 环境对比:比较不同环境下的表现 + - 代码审查:检查相关代码逻辑 + - 测试验证:通过测试验证问题原因 + +3. **排查工具使用**: + - 运维中心日志查询:查看运行日志和错误 + - 性能分析工具:分析性能瓶颈 + - 网络抓包工具:分析网络请求 + - 模拟器和真机测试:验证不同环境 + - 代码审查工具:检查代码质量 + +4. **常见问题排查案例**: + - 白屏问题:检查JS错误、资源加载失败 + - 数据不更新:检查缓存策略、数据请求 + - 性能下降:检查大量计算、频繁渲染 + - 内存泄漏:检查资源释放、循环引用 + - 支付失败:检查参数配置、签名验证 + +掌握系统化的问题排查方法,可以提高问题解决效率,减少用户影响。 + +### 10.4 团队协作模式 + +高效的团队协作是小程序运维成功的关键: + +1. **角色分工**: + - 产品经理:负责需求管理和优先级 + - 开发工程师:负责功能开发和问题修复 + - 测试工程师:负责质量验证和测试 + - 运维工程师:负责发布和监控 + - 运营人员:负责用户反馈和数据分析 + +2. **协作工具**: + - 项目管理工具:如Jira、Trello + - 代码管理工具:如Git、GitHub + - 文档协作工具:如Confluence、语雀 + - 沟通工具:如企业微信、Slack + - 运维中心:作为核心数据和操作平台 + +3. **协作流程**: + - 需求收集 → 需求评审 → 开发 → 测试 → 发布 → 监控 → 反馈 + - 每个环节设定明确的交付标准 + - 建立定期同步机制 + - 实施敏捷开发方法 + +4. **知识共享机制**: + - 建立知识库和文档中心 + - 定期技术分享和培训 + - 问题案例复盘和经验总结 + - 新成员培训和导师制 + +良好的团队协作可以提高运维效率,确保小程序的持续优化和稳定运行。 + +## 总结 + +微信小程序运维中心是小程序开发者不可或缺的管理工具,它提供了全面的运维能力,包括版本管理、质量监控、用户反馈、数据分析、安全中心和客服消息等。通过运维中心,开发者可以全面了解小程序的运行状况,及时发现并解决问题,持续优化用户体验。 + +有效利用运维中心需要建立规范的运维流程,设置合理的监控告警策略,掌握高效的问题排查方法,以及建立良好的团队协作模式。只有将运维中心与团队的日常工作紧密结合,才能充分发挥其价值,为小程序的成功运营提供有力支持。 + +随着微信小程序生态的不断发展,运维中心的功能也在持续完善和增强。开发者应当持续关注运维中心的新功能和最佳实践,不断提升小程序的运维水平和用户体验,在激烈的市场竞争中脱颖而出。现告 \ No newline at end of file diff --git a/docs/linux/kibana-architecture.svg b/docs/linux/kibana-architecture.svg new file mode 100644 index 000000000..329fa7860 --- /dev/null +++ b/docs/linux/kibana-architecture.svg @@ -0,0 +1,81 @@ + + + + + + Kibana 架构与数据流 + + + + 应用服务器 + + + 系统日志 + + + 网络设备 + + + + 收集器 + Beats + Logstash + Fluentd + + + + Elasticsearch + + + + Kibana + + + + + + + + + + + + + + + + 用户 + + + + + Kibana功能模块 + + + Discover + + + Visualize + + + Dashboard + + + Management + + + + 数据源 + + + 收集器 + + + Elasticsearch + + + Kibana + + + 用户 + \ No newline at end of file diff --git a/docs/linux/kibana-install.md b/docs/linux/kibana-install.md new file mode 100644 index 000000000..3d29265b7 --- /dev/null +++ b/docs/linux/kibana-install.md @@ -0,0 +1,342 @@ +--- +title: Linux环境下Kibana安装与使用教程 +author: 哪吒 +date: '2023-06-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Linux环境下Kibana安装与使用教程 + +## 1. Kibana简介 + +Kibana是Elastic Stack的一部分,是一个开源的分析和可视化平台,设计用于和Elasticsearch一起工作。Kibana可以搜索、查看和与存储在Elasticsearch索引中的数据进行交互,并且能够轻松地执行高级数据分析,以及在各种图表、表格和地图中可视化数据。 + +![Kibana架构图](./kibana-architecture.svg) + +主要功能包括: + +- **数据探索与可视化**:通过直观的界面查询和过滤数据 +- **仪表板创建**:组合多个可视化组件创建综合仪表板 +- **日志分析**:实时监控和分析日志数据 +- **指标监控**:监控应用和基础设施的性能指标 +- **安全分析**:检测和分析安全威胁 + +## 2. 环境准备 + +### 2.1 系统要求 + +- Linux操作系统(CentOS 7/8、Ubuntu 18.04/20.04等) +- 已安装并运行的Elasticsearch(建议版本与Kibana保持一致) +- 至少2GB RAM(生产环境建议4GB以上) +- 现代网络浏览器 + +### 2.2 安装方式选择 + +本教程将介绍三种安装方式: + +1. 使用RPM/DEB包安装 +2. 使用压缩包安装 +3. 使用Docker安装 + +## 3. 使用RPM/DEB包安装 + +### 3.1 CentOS/RHEL系统(RPM包) + +#### 导入Elastic公钥 + +```bash +rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch +``` + +#### 创建仓库文件 + +```bash +cat > /etc/yum.repos.d/kibana.repo << EOF +[kibana] +name=Kibana repository +baseurl=https://artifacts.elastic.co/packages/7.x/yum +gpgcheck=1 +gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch +enabled=1 +autorefresh=1 +type=rpm-md +EOF +``` + +#### 安装Kibana + +```bash +yum install kibana -y +``` + +### 3.2 Ubuntu/Debian系统(DEB包) + +#### 导入Elastic公钥 + +```bash +wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - +``` + +#### 安装apt-transport-https + +```bash +apt-get install apt-transport-https -y +``` + +#### 添加仓库 + +```bash +echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/kibana.list +``` + +#### 更新仓库并安装Kibana + +```bash +apt-get update && apt-get install kibana -y +``` + +## 4. 使用压缩包安装 + +### 4.1 下载Kibana + +```bash +wget https://artifacts.elastic.co/downloads/kibana/kibana-7.17.0-linux-x86_64.tar.gz +``` + +### 4.2 解压文件 + +```bash +tar -xzf kibana-7.17.0-linux-x86_64.tar.gz +cd kibana-7.17.0-linux-x86_64/ +``` + +## 5. 使用Docker安装 + +### 5.1 拉取Kibana镜像 + +```bash +docker pull docker.elastic.co/kibana/kibana:7.17.0 +``` + +### 5.2 创建配置目录 + +```bash +mkdir -p /data/elk/kibana/config +``` + +### 5.3 创建docker-compose.yml文件 + +```bash +cat > docker-compose.yml << EOF +version: '3' +services: + kibana: + image: docker.elastic.co/kibana/kibana:7.17.0 + container_name: kibana + volumes: + - /etc/localtime:/etc/localtime + - /data/elk/kibana/config:/usr/share/kibana/config:rw + environment: + ELASTICSEARCH_HOSTS: http://elasticsearch:9200 + I18N_LOCALE: zh-CN + ports: + - 5601:5601 + networks: + - elastic + +networks: + elastic: + external: true +EOF +``` + +### 5.4 启动Kibana容器 + +```bash +docker-compose up -d +``` + +## 6. Kibana配置 + +### 6.1 基本配置 + +编辑Kibana配置文件: + +- RPM/DEB安装:`/etc/kibana/kibana.yml` +- 压缩包安装:`config/kibana.yml` +- Docker安装:`/data/elk/kibana/config/kibana.yml` + +基本配置示例: + +```yaml +# 服务器主机名,设置为0.0.0.0可以允许远程访问 +server.host: "0.0.0.0" + +# Kibana服务端口 +server.port: 5601 + +# Kibana服务名称 +server.name: "kibana" + +# ElasticSearch连接设置 +elasticsearch.hosts: ["http://localhost:9200"] + +# 设置中文界面 +i18n.locale: "zh-CN" +``` + +### 6.2 安全配置 + +如果Elasticsearch启用了安全功能,需要配置Kibana连接凭证: + +```yaml +elasticsearch.username: "kibana_system" +elasticsearch.password: "your_password" +``` + +对于HTTPS连接: + +```yaml +elasticsearch.ssl.verificationMode: certificate +elasticsearch.ssl.certificateAuthorities: ["/path/to/ca.crt"] +``` + +## 7. 启动Kibana服务 + +### 7.1 使用systemd启动(RPM/DEB安装) + +```bash +systemctl daemon-reload +systemctl enable kibana +systemctl start kibana +``` + +### 7.2 使用二进制文件启动(压缩包安装) + +```bash +./bin/kibana & +``` + +### 7.3 检查服务状态 + +```bash +# 对于systemd +systemctl status kibana + +# 查看日志 +journalctl -u kibana.service + +# 对于Docker +docker logs kibana +``` + +## 8. 访问Kibana + +在浏览器中访问:`http://your-server-ip:5601` + +首次访问时,Kibana会要求创建索引模式。如果已经有数据在Elasticsearch中,可以按照界面提示创建索引模式。 + +## 9. Kibana基本使用 + +### 9.1 创建索引模式 + +1. 导航到 Management > Stack Management > Kibana > Index Patterns +2. 点击 "Create index pattern" +3. 输入索引模式,例如 "logstash-*" +4. 选择时间字段,通常是 "@timestamp" +5. 点击 "Create index pattern" + +### 9.2 使用Discover探索数据 + +1. 点击左侧导航栏的 "Discover" +2. 选择刚才创建的索引模式 +3. 使用搜索栏输入查询条件 +4. 使用时间选择器选择时间范围 +5. 查看和分析返回的文档 + +### 9.3 创建可视化 + +1. 点击左侧导航栏的 "Visualize" +2. 点击 "Create new visualization" +3. 选择可视化类型(饼图、柱状图、折线图等) +4. 选择数据源(索引模式) +5. 配置可视化设置(指标、分组等) +6. 保存可视化 + +### 9.4 创建仪表板 + +1. 点击左侧导航栏的 "Dashboard" +2. 点击 "Create new dashboard" +3. 点击 "Add" 添加已保存的可视化 +4. 调整可视化大小和位置 +5. 保存仪表板 + +## 10. 高级配置 + +### 10.1 配置空间(Spaces) + +空间允许将Kibana对象(如仪表板、可视化等)分组到有意义的类别中: + +1. 导航到 Management > Stack Management > Kibana > Spaces +2. 点击 "Create space" +3. 输入空间名称和描述 +4. 选择功能权限 +5. 点击 "Create space" + +### 10.2 配置告警 + +1. 导航到 Management > Stack Management > Alerts and Insights > Rules +2. 点击 "Create rule" +3. 选择规则类型 +4. 配置规则条件和操作 +5. 保存规则 + +### 10.3 配置报表 + +1. 在仪表板或可视化页面 +2. 点击 "Share" > "PDF Reports" +3. 配置报表选项 +4. 点击 "Generate PDF" + +## 11. 常见问题解决 + +### 11.1 Kibana无法连接到Elasticsearch + +检查以下几点: + +1. Elasticsearch是否正在运行:`curl http://localhost:9200` +2. Kibana配置中的Elasticsearch地址是否正确 +3. 如果启用了安全功能,检查用户名和密码是否正确 +4. 检查网络连接和防火墙设置 + +### 11.2 Kibana启动失败 + +检查日志文件: + +- RPM/DEB安装:`/var/log/kibana/kibana.log` +- 压缩包安装:`logs/kibana.log` +- Docker安装:`docker logs kibana` + +常见原因: + +1. 配置文件语法错误 +2. 端口冲突 +3. 内存不足 + +### 11.3 Kibana加载缓慢 + +优化建议: + +1. 增加Kibana服务器内存 +2. 优化Elasticsearch查询 +3. 减少仪表板中的可视化数量 +4. 使用更精确的时间范围和查询 + +## 12. 总结 + +本教程详细介绍了在Linux环境下安装和使用Kibana的方法,包括三种不同的安装方式、基本配置、安全设置、基本使用方法以及常见问题解决方案。Kibana作为Elastic Stack的重要组成部分,为Elasticsearch中的数据提供了强大的可视化和分析能力,是日志分析、应用监控和数据可视化的理想工具。 + +在生产环境中,建议结合Elasticsearch、Logstash和Filebeat等组件一起使用,构建完整的日志收集、处理和分析系统。同时,根据实际需求调整配置参数,确保系统的性能和安全性。 \ No newline at end of file diff --git a/docs/linux/nginx-env.md b/docs/linux/nginx-env.md new file mode 100644 index 000000000..e073aedb5 --- /dev/null +++ b/docs/linux/nginx-env.md @@ -0,0 +1,104 @@ +--- +title: Nginx环境配置 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Nginx环境配置 + +* 停止:docker stop Nginx +* 重启:docker restart Nginx +* 删除服务:docker rm Nginx +* 删除镜像:docker rmi Nginx +* 进入服务:docker exec -it Nginx /bin/bash +* 配置文件:nginx - conf/html/logs/ssl(opens new window) + +# 一、基础安装 + +``` +docker run \ +--restart always \ +--name Nginx \ +-d \ +-p 80:80 \ +nginx + +``` + +## 轮询和权值 (负载均衡) + +``` +upstream myserver{ + server localhost:8088; + server localhost:8083; +} + +server{ + listen 8004; + server_name localhost; + location / { + proxy_pass http://myserver; + } +} + + + +upstream myserver{ + server localhost:8088 weight=10; + server localhost:8083 weight=2; +} + +server{ + listen 8004; + server_name localhost; + location / { + proxy_pass http://myserver; + } +} +``` + +重定向分类: + +1. 301 永久性重定向:永久移动到新的URL上,搜索引擎会更新索引。 +2. 302 临时性重定向:暂时移动到新的URL上,搜索引擎不会更新索引。 + +301 永久性重定向 + +``` +if ($host ~ '^b.com'){ + return 301 https://b.cn; +} +``` + +302 临时性重定向 + +``` +if ($host ~ '^b.com'){ + return 302 https://b.cn; +} +``` + +``` +location ^~ /api/ { + proxy_pass http://127.0.0.1:8080/api/; + add_header 'Access-Control-Allow-Origin' $http_origin; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; + add_header Access-Control-Allow-Headers '*'; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Allow-Origin' $http_origin; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } +} +``` + diff --git a/docs/mysql/mysql-locks.md b/docs/mysql/mysql-locks.md new file mode 100644 index 000000000..92111eacc --- /dev/null +++ b/docs/mysql/mysql-locks.md @@ -0,0 +1,358 @@ +--- +title: MySQL锁机制详解 +author: 哪吒 +date: '2023-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## MySQL锁机制详解 + +MySQL 中的锁是数据库并发控制的基本机制,用于管理对共享资源的并发访问,确保数据的一致性和完整性。本文将详细介绍 MySQL 中的各种锁类型、工作原理、使用场景以及最佳实践。 + +## 锁的基本概念 + +锁是一种并发控制机制,用于协调多个事务对共享资源的访问。在数据库系统中,锁的主要目的是保证数据的一致性和完整性,防止并发操作导致的数据异常。 + +锁的基本特性包括: + +- **互斥性**:某些锁(如排他锁)一旦被获取,其他事务就无法再获取相同的锁 +- **共享性**:某些锁(如共享锁)允许多个事务同时持有 +- **粒度**:锁可以作用于不同级别的数据库对象(如行、页、表) +- **持续时间**:锁的持有时间可以是短暂的(如语句级),也可以是长期的(如事务级) + +## 按操作类型分类的锁 + +### 读锁(共享锁/S锁) + +读锁,也称为共享锁(Shared Lock)或 S 锁,是一种允许多个事务同时读取同一资源,但阻止其他事务获取写锁的锁类型。 + +**特点**: +- 允许多个事务同时获取同一资源的读锁 +- 持有读锁的事务只能读取资源,不能修改 +- 如果一个资源已经被加了读锁,其他事务可以继续加读锁,但不能加写锁 + +**使用场景**: +- 查询操作,如 SELECT 语句 +- 需要保证读取数据一致性的场景 + +**语法**: +```sql +-- 在 SELECT 语句中显式添加共享锁 +SELECT * FROM table_name WHERE condition LOCK IN SHARE MODE; + +-- MySQL 8.0 新语法 +SELECT * FROM table_name WHERE condition FOR SHARE; +``` + +### 写锁(排它锁/独占锁/X锁) + +写锁,也称为排它锁(Exclusive Lock)、独占锁或 X 锁,是一种在事务修改数据时使用的锁,它排斥任何其他锁。 + +**特点**: +- 一个资源只能被一个事务加写锁 +- 持有写锁的事务可以读取和修改资源 +- 如果一个资源已经被加了写锁,其他事务不能加读锁或写锁 +- 如果一个资源已经被加了读锁,事务不能加写锁 + +**使用场景**: +- 数据修改操作,如 INSERT、UPDATE、DELETE 语句 +- 需要保证数据修改原子性的场景 + +**语法**: +```sql +-- 在 SELECT 语句中显式添加排它锁 +SELECT * FROM table_name WHERE condition FOR UPDATE; + +-- 修改语句会自动加排它锁 +UPDATE table_name SET column = value WHERE condition; +DELETE FROM table_name WHERE condition; +``` + +## 按锁粒度分类 + +### 表锁(Table Lock) + +表锁是 MySQL 中粒度最大的锁,它锁定整个表。 + +**特点**: +- 开销小,加锁快,不会出现死锁 +- 锁定粒度大,发生锁冲突的概率高,并发度低 +- 对整表进行写操作会阻塞其他事务对该表的所有读写操作 + +**使用场景**: +- 对整表数据进行修改的场景 +- 表数据量较小,并发访问不多的场景 +- MyISAM 存储引擎(只支持表级锁) + +**语法**: +```sql +-- 手动加表级读锁 +LOCK TABLES table_name READ; + +-- 手动加表级写锁 +LOCK TABLES table_name WRITE; + +-- 释放所有表锁 +UNLOCK TABLES; +``` + +### 页锁(Page Lock) + +页锁是介于表锁和行锁之间的一种锁,它锁定数据库中的一个页(通常是 16KB)。 + +**特点**: +- 锁定粒度介于表锁和行锁之间 +- 加锁开销和加锁时间介于表锁和行锁之间 +- 会出现死锁 +- 锁定粒度较大,有可能在锁定一行数据时,实际锁定了多行数据(因为它们在同一页中) + +**使用场景**: +- BDB 存储引擎(MySQL 5.1 后不再支持) + +### 行锁(Row Lock) + +行锁是 MySQL 中粒度最小的锁,它只锁定表中的某一行。 + +**特点**: +- 开销大,加锁慢,会出现死锁 +- 锁定粒度小,发生锁冲突的概率低,并发度高 +- 只有在访问行级数据时才会加锁 + +**使用场景**: +- 高并发系统 +- InnoDB 存储引擎(支持行级锁) + +**注意**:InnoDB 的行锁是通过索引实现的,如果查询条件没有使用索引,InnoDB 会使用表锁。 + +## 特殊类型的锁 + +### 意向锁(Intention Lock) + +意向锁是 InnoDB 存储引擎中的一种表级锁,用于指示事务稍后要对表中的行加什么类型的锁(共享锁或排它锁)。 + +**类型**: +- **意向共享锁(IS锁)**:表示事务打算给表中的某些行加共享锁 +- **意向排它锁(IX锁)**:表示事务打算给表中的某些行加排它锁 + +**特点**: +- 意向锁是表级锁,不会与行级的共享/排它锁冲突 +- 意向锁之间不会互斥(IS与IS、IS与IX、IX与IX都可以共存) +- 意向锁可以与表级共享锁/排它锁互斥(IX与S、IX与X、IS与X互斥,但IS与S兼容) + +**作用**: +- 提高加锁效率,避免在给行加锁前,需要检查表中每一行是否已经被锁定 +- 实现多粒度锁定,允许行锁和表锁共存 + +### 间隙锁(Gap Lock) + +间隙锁是 InnoDB 在 REPEATABLE READ 隔离级别下,为了防止幻读而引入的一种锁机制,它锁定索引记录之间的间隙。 + +**特点**: +- 锁定索引记录之间的间隙,防止其他事务在间隙中插入数据 +- 只在 REPEATABLE READ 隔离级别下生效 +- 可能导致死锁和阻塞 + +**使用场景**: +- 防止幻读 +- 保证事务隔离性 + +### 临键锁(Next-Key Lock) + +临键锁是 InnoDB 的默认行锁算法,它是记录锁(行锁)和间隙锁的组合,锁定一个索引记录及其之前的间隙。 + +**特点**: +- 锁定索引记录本身和索引记录之前的间隙 +- 是 InnoDB 在 REPEATABLE READ 隔离级别下使用的默认锁 +- 可以防止幻读 + +### 插入意向锁(Insert Intention Lock) + +插入意向锁是一种特殊的间隙锁,表示插入的意图,当多个事务插入同一个索引间隙的不同位置时,不需要等待其他事务完成。 + +**特点**: +- 是一种特殊的间隙锁 +- 多个事务可以同时获取同一个间隙的插入意向锁,只要它们插入的位置不冲突 +- 如果间隙已经被加了间隙锁,插入意向锁会被阻塞 + +**使用场景**: +- 提高并发插入性能 + +### 自增锁(Auto-increment Lock) + +自增锁是一种特殊的表级锁,用于处理 AUTO_INCREMENT 列的值生成。 + +**特点**: +- 在插入操作中,InnoDB 会获取自增锁,确保生成的自增值是连续的 +- 自增锁是一种轻量级锁,在插入语句结束后立即释放 + +**配置参数**: +- `innodb_autoinc_lock_mode`:控制自增锁的行为 + - 0:传统模式,语句级锁定 + - 1:连续模式,批量插入使用表锁,单行插入使用轻量级锁 + - 2:交错模式,所有插入都使用轻量级锁,但自增值可能不连续 + +### 元数据锁(MDL锁) + +元数据锁(Metadata Lock)用于保护数据库对象的元数据,防止在使用对象时被其他会话修改对象结构。 + +**特点**: +- 当对表执行 CRUD 操作时,会自动加 MDL 读锁 +- 当对表结构进行变更时,会自动加 MDL 写锁 +- MDL 锁在事务提交后才会释放 + +**使用场景**: +- 防止在查询过程中表结构被修改 +- 防止在修改表结构时表被查询或修改 + +### 记录锁(Record Lock) + +记录锁是最简单的行锁,它锁定索引记录本身。 + +**特点**: +- 锁定单个索引记录 +- 防止其他事务修改或删除该记录 + +**使用场景**: +- 更新或删除单条记录 + +## 按实现机制分类 + +### 悲观锁 + +悲观锁是一种假设会发生并发冲突的锁机制,它在操作数据前先获取锁,确保在操作过程中数据不会被其他事务修改。 + +**特点**: +- 先获取锁,再操作数据 +- 适用于写多读少的场景 +- 实现简单,但并发性能较低 + +**实现方式**: +- 使用 SELECT ... FOR UPDATE 语句 +- 使用 LOCK IN SHARE MODE 语句 + +**示例**: +```sql +-- 使用悲观锁更新数据 +BEGIN; +-- 先锁定要更新的行 +SELECT * FROM accounts WHERE id = 1 FOR UPDATE; +-- 执行更新操作 +UPDATE accounts SET balance = balance - 100 WHERE id = 1; +COMMIT; +``` + +### 乐观锁 + +乐观锁是一种假设不会发生并发冲突的锁机制,它在操作数据时不加锁,而是在提交更新时检查数据是否被其他事务修改过。 + +**特点**: +- 不加锁,只在提交时检查冲突 +- 适用于读多写少的场景 +- 并发性能高,但实现复杂 + +**实现方式**: +- 使用版本号 +- 使用时间戳 +- 使用条件更新 + +**示例**: +```sql +-- 使用版本号实现乐观锁 +-- 1. 查询当前数据和版本号 +SELECT balance, version FROM accounts WHERE id = 1; + +-- 2. 在应用层计算新的余额 + +-- 3. 更新数据,同时检查版本号是否变化 +UPDATE accounts SET balance = new_balance, version = version + 1 +WHERE id = 1 AND version = old_version; + +-- 4. 检查影响的行数,如果为0表示更新失败(版本已变化) +``` + +## 其他特殊锁类型 + +### 全局锁(Global Lock) + +全局锁是 MySQL 中粒度最大的锁,它对整个数据库实例加锁,使整个数据库处于只读状态。 + +**使用场景**: +- 全库备份 + +**语法**: +```sql +-- 加全局锁 +FLUSH TABLES WITH READ LOCK; + +-- 释放全局锁 +UNLOCK TABLES; +``` + +### 读锁(RL锁) + +读锁(Read Lock)是表锁的一种,允许其他事务读取表,但不允许写入。 + +### 写锁(WL锁) + +写锁(Write Lock)是表锁的一种,不允许其他事务读取或写入表。 + +## 锁的监控和诊断 + +### 查看锁信息 + +```sql +-- 查看当前锁等待情况 +SHOW ENGINE INNODB STATUS\G + +-- 查看锁等待详情(MySQL 5.7+) +SELECT * FROM performance_schema.data_locks; +SELECT * FROM performance_schema.data_lock_waits; + +-- 查看锁等待详情(MySQL 5.7之前) +SELECT * FROM information_schema.innodb_locks; +SELECT * FROM information_schema.innodb_lock_waits; + +-- 查看当前正在执行的事务 +SELECT * FROM information_schema.innodb_trx; +``` + +### 死锁检测和处理 + +InnoDB 存储引擎有内置的死锁检测机制,当检测到死锁时,会自动回滚一个事务来解除死锁。 + +**死锁日志查看**: +```sql +SHOW ENGINE INNODB STATUS\G +``` + +**死锁预防措施**: +1. 按照固定的顺序访问表和行 +2. 尽量使用主键或唯一索引进行更新操作 +3. 避免长事务 +4. 使用合理的隔离级别 +5. 及时提交或回滚事务 + +## 锁的最佳实践 + +1. **选择合适的锁粒度**:根据业务需求选择合适的锁粒度,一般情况下,行锁的并发性能优于表锁 + +2. **使用合适的隔离级别**:不同的隔离级别使用不同的锁机制,根据业务需求选择合适的隔离级别 + +3. **避免长事务**:长事务会长时间持有锁,降低系统并发性能 + +4. **合理设计索引**:InnoDB 的行锁是通过索引实现的,合理设计索引可以提高锁的效率 + +5. **避免锁升级**:尽量避免锁升级(如从行锁升级到表锁),可以通过使用索引、拆分大事务等方式实现 + +6. **使用乐观锁代替悲观锁**:在读多写少的场景下,使用乐观锁可以提高并发性能 + +7. **定期监控锁状态**:定期监控数据库的锁状态,及时发现和解决锁问题 + +## 总结 + +MySQL 的锁机制是保证数据一致性和完整性的重要手段,了解不同类型的锁及其使用场景,可以帮助我们设计出高性能、高可靠性的数据库应用。在实际应用中,需要根据业务需求和系统特点,选择合适的锁类型和锁策略,平衡数据一致性和系统性能。 + +锁是数据库并发控制的基础,但过度使用锁会导致系统性能下降,因此需要在保证数据一致性的前提下,尽量减少锁的使用,提高系统的并发性能。 diff --git a/docs/nio/buffer-channel.md b/docs/nio/buffer-channel.md new file mode 100644 index 000000000..7af54141a --- /dev/null +++ b/docs/nio/buffer-channel.md @@ -0,0 +1,79 @@ +--- +title: Buffer和Channel +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Buffer和Channel + +* 可简单认为:IO 是面向流的处理,NIO 是面向块(缓冲区)的处理 +* 面向流的 I/O 系统一次一个字节地处理数据。 +* 一个面向块(缓冲区)的 I/O 系统以块的形式处理数据。 + +NIO 主要有两个核心部分组成: + +* Buffer 缓冲区 +* Channel 通道 + +在 NIO 中,并不是以流的方式来处理数据的,而是以 buffer 缓冲区和 Channel 通道配合使用来处理数据的。 + +简单理解一下: + +可以把 Channel 通道比作铁路,buffer 缓冲区比作成火车(运载着货物) + +而我们的 NIO 就是通过 Channel 通道运输着存储数据的 Buffer 缓冲区的来实现数据的处理! + +要时刻记住:Channel 不与数据打交道,它只负责运输数据。与数据打交道的是 Buffer 缓冲区 + +* Channel-->运输 +* Buffer-->数据 + +相对于传统 IO 而言,流是单向的。对于 NIO 而言,有了 Channel 通道这个概念,我们的读写都是双向的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)! + +## Buffer 缓冲区 + +Buffer 是缓冲区的抽象类: + +其中 ByteBuffer 是用得最多的实现类(在通道中读写字节数据)。 + +![img_1.png](./img_1.png) + +读取缓冲区的数据/写数据到缓冲区中 + +![img_2.png](./img_2.png) + +Buffer 类维护了 4 个核心变量来提供关于其所包含的数组信息。它们是: + +1. 容量 Capacity 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛) +2. 上界 Limit 缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。 +3. 位置 Position 下一个要被读或写的元素的位置。Position 会自动由相应的 get()和 put()函数更新。 +4. 标记 Mark 一个备忘位置。用于记录上一次读写的位置。 + +首先展示一下是如何创建缓冲区的,核心变量的值是怎么变化的。 + +## Channel 通道 + +Channel 通道只负责传输数据、不直接操作数据。操作数据都是通过 Buffer 缓冲区来进行操作!通常,通道可以分为两大类:文件通道和套接字通道。 + +FileChannel:用于文件 I/O 的通道,支持文件的读、写和追加操作。FileChannel 允许在文件的任意位置进行数据传输,支持文件锁定以及内存映射文件等高级功能。FileChannel 无法设置为非阻塞模式,因此它只适用于阻塞式文件操作。 + +SocketChannel:用于 TCP 套接字 I/O 的通道。SocketChannel 支持非阻塞模式,可以与 Selector一起使用,实现高效的网络通信。SocketChannel 允许连接到远程主机,进行数据传输。 + +与之匹配的有ServerSocketChannel:用于监听 TCP 套接字连接的通道。与 SocketChannel 类似,ServerSocketChannel 也支持非阻塞模式,并可以与 Selector 一起使用。ServerSocketChannel 负责监听新的连接请求,接收到连接请求后,可以创建一个新的 SocketChannel 以处理数据传输。 + +DatagramChannel:用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收数据报包,适用于无连接的、不可靠的网络通信。 + + + + + + + + + + + diff --git a/docs/nio/network-connect.md b/docs/nio/network-connect.md new file mode 100644 index 000000000..befdb4d6f --- /dev/null +++ b/docs/nio/network-connect.md @@ -0,0 +1,607 @@ +--- +title: 网络编程实践聊天室 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 网络编程实践聊天室 + +利用 Java 的套接字 Socket 和 ServerSocket 完成网络编程,但 Socket 和 ServerSocket 是基于 Java IO 的,在网络编程方面,性能会比较差。 + +那 Java NIO 的 SocketChannel 和 ServerSocketChannel 性能怎么样呢? + +## SocketChannel 和 ServerSocketChannel + +ServerSocketChannel 用于创建服务器端套接字,而 SocketChannel 用于创建客户端套接字。它们都支持阻塞和非阻塞模式,通过设置其 blocking 属性来切换。阻塞模式下,读/写操作会一直阻塞直到完成,而非阻塞模式下,读/写操作会立即返回。 + +阻塞模式: + +1. 优点:编程简单,适合低并发场景。 +2. 缺点:性能较差,不适合高并发场景。 + +非阻塞模式: + +1. 优点:性能更好,适合高并发场景。 +2. 缺点:编程相对复杂。 + +我们来看一个简单的示例(阻塞模式下): + +```java +public class BlockingServer { + public static void main(String[] args) throws IOException { + // 创建服务器套接字 + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // 绑定端口 + serverSocketChannel.socket().bind(new InetSocketAddress(8080)); + // 设置为阻塞模式(默认为阻塞模式) + serverSocketChannel.configureBlocking(true); + + while (true) { + // 接收客户端连接 + SocketChannel socketChannel = serverSocketChannel.accept(); + // 分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + + // 读取数据 + int bytesRead = socketChannel.read(buffer); + while (bytesRead != -1) { + buffer.flip(); + System.out.println(StandardCharsets.UTF_8.decode(buffer)); + buffer.clear(); + bytesRead = socketChannel.read(buffer); + } + // 关闭套接字 + socketChannel.close(); + } + } +} +``` + +首先创建服务器端套接字ServerSocketChannel,然后绑定 8080 端口,接着使用 while 循环监听客户端套接字。如果接收到客户端连接 SocketChannel,就从通道里读取数据到缓冲区 ByteBuffer,一直读到通道里没有数据,关闭当前通道。 + +其中 serverSocketChannel.configureBlocking(true) 用来设置通道为阻塞模式(可以缺省)。 + +```java +public class BlockingClient { + public static void main(String[] args) throws IOException { + // 创建客户端套接字 + SocketChannel socketChannel = SocketChannel.open(); + // 连接服务器 + socketChannel.connect(new InetSocketAddress("localhost", 8080)); + // 分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + + // 向服务器发送数据 + buffer.put("aaa,这是来自客户端的消息。".getBytes(StandardCharsets.UTF_8)); + buffer.flip(); + socketChannel.write(buffer); + // 清空缓冲区 + buffer.clear(); + + // 关闭套接字 + socketChannel.close(); + } +} +``` + +我们再来看非阻塞模式下的示例。 + +先来看 Server 端: + +```java +public class NonBlockingServer { + public static void main(String[] args) throws IOException { + // 创建服务器套接字 + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // 绑定端口 + serverSocketChannel.socket().bind(new InetSocketAddress(8080)); + // 设置为非阻塞模式 + serverSocketChannel.configureBlocking(false); + + // 创建选择器 + Selector selector = Selector.open(); + // 注册服务器套接字到选择器 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + selector.select(); + Set selectedKeys = selector.selectedKeys(); + Iterator iterator = selectedKeys.iterator(); + + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + iterator.remove(); + + if (key.isAcceptable()) { + // 接收客户端连接 + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + } + + if (key.isReadable()) { + // 读取数据 + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int bytesRead = socketChannel.read(buffer); + + if (bytesRead != -1) { + buffer.flip(); + System.out.print(StandardCharsets.UTF_8.decode(buffer)); + buffer.clear(); + } else { + // 客户端已断开连接,取消选择键并关闭通道 + key.cancel(); + socketChannel.close(); + } + } + } + } + } +} +``` + + + +```java +public class NonBlockingClient { + public static void main(String[] args) throws IOException { + // 创建客户端套接字 + SocketChannel socketChannel = SocketChannel.open(); + // 设置为非阻塞模式 + socketChannel.configureBlocking(false); + // 连接服务器 + socketChannel.connect(new InetSocketAddress("localhost", 8080)); + + while (!socketChannel.finishConnect()) { + // 等待连接完成 + } + + // 分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + + // 向服务器发送数据 + String message = "你好,aa,这是来自客户端的消息。"; + buffer.put(message.getBytes(StandardCharsets.UTF_8)); + buffer.flip(); + socketChannel.write(buffer); + // 清空缓冲区 + buffer.clear(); + + // 关闭套接字 + socketChannel.close(); + } +} +``` + +## Scatter 和 Gather + +Scatter 和 Gather 是 Java NIO 中两种高效的 I/O 操作,用于将数据分散到多个缓冲区或从多个缓冲区中收集数据。 + +Scatter(分散):它将从 Channel 读取的数据分散(写入)到多个缓冲区。这种操作可以在读取数据时将其分散到不同的缓冲区,有助于处理结构化数据。例如,我们可以将消息头、消息体和消息尾分别写入不同的缓冲区。 + +Gather(聚集):与 Scatter 相反,它将多个缓冲区中的数据聚集(读取)并写入到一个 Channel。这种操作允许我们在发送数据时从多个缓冲区中聚集数据。例如,我们可以将消息头、消息体和消息尾从不同的缓冲区中聚集到一起并写入到同一个 Channel。 + +来写一个完整的 demo,先看 Server。 + +``` +// 创建一个ServerSocketChannel +ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); +serverSocketChannel.socket().bind(new InetSocketAddress(9000)); + +// 接受连接 +SocketChannel socketChannel = serverSocketChannel.accept(); + +// Scatter:分散读取数据到多个缓冲区 +ByteBuffer headerBuffer = ByteBuffer.allocate(128); +ByteBuffer bodyBuffer = ByteBuffer.allocate(1024); + +ByteBuffer[] buffers = {headerBuffer, bodyBuffer}; + +long bytesRead = socketChannel.read(buffers); + +// 输出缓冲区数据 +headerBuffer.flip(); +while (headerBuffer.hasRemaining()) { + System.out.print((char) headerBuffer.get()); + } + + System.out.println(); + +bodyBuffer.flip(); +while (bodyBuffer.hasRemaining()) { + System.out.print((char) bodyBuffer.get()); + } + +// Gather:聚集数据从多个缓冲区写入到Channel +ByteBuffer headerResponse = ByteBuffer.wrap("Header Response".getBytes()); +ByteBuffer bodyResponse = ByteBuffer.wrap("Body Response".getBytes()); + +ByteBuffer[] responseBuffers = {headerResponse, bodyResponse}; + +long bytesWritten = socketChannel.write(responseBuffers); + +// 关闭连接 +socketChannel.close(); +serverSocketChannel.close(); +``` + + +``` +// 创建一个SocketChannel +SocketChannel socketChannel = SocketChannel.open(); +socketChannel.connect(new InetSocketAddress("localhost", 9000)); + +// 发送数据到服务器 +String header = "Header Content"; +String body = "Body Content"; + +ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes()); +ByteBuffer bodyBuffer = ByteBuffer.wrap(body.getBytes()); + +ByteBuffer[] buffers = {headerBuffer, bodyBuffer}; +socketChannel.write(buffers); + +// 从服务器接收数据 +ByteBuffer headerResponseBuffer = ByteBuffer.allocate(128); +ByteBuffer bodyResponseBuffer = ByteBuffer.allocate(1024); + +ByteBuffer[] responseBuffers = {headerResponseBuffer, bodyResponseBuffer}; + +long bytesRead = socketChannel.read(responseBuffers); + +// 输出接收到的数据 +headerResponseBuffer.flip(); +while (headerResponseBuffer.hasRemaining()) { + System.out.print((char) headerResponseBuffer.get()); +} + +bodyResponseBuffer.flip(); +while (bodyResponseBuffer.hasRemaining()) { + System.out.print((char) bodyResponseBuffer.get()); +} + +// 关闭连接 +socketChannel.close(); +``` + +## 异步套接字通道 AsynchronousSocketChannel 和 AsynchronousServerSocketChannel + +AsynchronousSocketChannel 和 AsynchronousServerSocketChannel 是 Java 7 引入的异步 I/O 类,分别用于处理异步客户端 Socket 和服务器端 ServerSocket。异步 I/O 允许在 I/O 操作进行时执行其他任务,并在操作完成时接收通知,提高了并发处理能力。 + +``` +public class AsynchronousServer { + + public static void main(String[] args) throws IOException, InterruptedException { + AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(); + server.bind(new InetSocketAddress("localhost", 5000)); + + System.out.println("服务器端启动"); + + server.accept(null, new CompletionHandler() { + @Override + public void completed(AsynchronousSocketChannel client, Void attachment) { + // 接收下一个连接请求 + server.accept(null, this); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + Future readResult = client.read(buffer); + + try { + readResult.get(); + buffer.flip(); + String message = new String(buffer.array(), 0, buffer.remaining()); + System.out.println("接收到的消息: " + message); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void failed(Throwable exc, Void attachment) { + exc.printStackTrace(); + } + }); + + // 为了让服务器继续运行,我们需要阻止 main 线程退出 + Thread.currentThread().join(); + } +} +``` + +![img_3.png](./img_3.png) + +```java +public class ChatServer { + private Selector selector; + private ServerSocketChannel serverSocketChannel; + private static final int PORT = 8080; + + public ChatServer() { + try { + selector = Selector.open(); + serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.socket().bind(new InetSocketAddress(PORT)); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + System.out.println("聊天室服务端启动了 " + PORT); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void start() { + try { + while (true) { + if (selector.select() > 0) { + Iterator iterator = selector.selectedKeys().iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + iterator.remove(); + handleKey(key); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void handleKey(SelectionKey key) throws IOException { + if (key.isAcceptable()) { + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + System.out.println("客户端连接上了: " + socketChannel.getRemoteAddress()); + } else if (key.isReadable()) { + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int read = socketChannel.read(buffer); + if (read > 0) { + buffer.flip(); + String msg = new String(buffer.array(), 0, read); + System.out.println("客户端说: " + msg); + socketChannel.write(ByteBuffer.wrap(("服务端回复: " + msg).getBytes())); + } + } + } + + public static void main(String[] args) { + new ChatServer().start(); + } +} +``` + +```java +public class ChatClient { + private Selector selector; + private SocketChannel socketChannel; + private static final String HOST = "localhost"; + private static final int PORT = 8080; + + public ChatClient() { + try { + selector = Selector.open(); + socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT)); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + System.out.println("连接到聊天室了"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void start() { + new Thread(() -> { + try { + while (true) { + if (selector.select() > 0) { + for (SelectionKey key : selector.selectedKeys()) { + selector.selectedKeys().remove(key); + if (key.isReadable()) { + readMessage(); + } + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in + ))) { + String input; + while ((input = reader.readLine()) != null) { + sendMessage(input); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + private void sendMessage(String message) throws IOException { + if (message != null && !message.trim().isEmpty()) { + ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); + socketChannel.write(buffer); + } + } + + private void readMessage() throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + int read = socketChannel.read(buffer); + if (read > 0) { + buffer.flip(); + String msg = new String(buffer.array(), 0, read); + System.out.println(msg); + } + } + + public static void main(String[] args) { + new ChatClient().start(); + } +} +``` + +来看服务器端代码: + +```java +public class Chat2Server { + + public static void main(String[] args) throws IOException { + // 创建一个 ServerSocketChannel + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.bind(new InetSocketAddress(8080)); + + // 创建一个 Selector + Selector selector = Selector.open(); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + System.out.println("聊天室服务端启动了"); + + // 客户端连接 + AtomicReference clientRef = new AtomicReference<>(); + + // 从控制台读取输入并发送给客户端 + Thread sendMessageThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { + while (true) { + System.out.println("输入服务器端消息: "); + String message = reader.readLine(); + SocketChannel client = clientRef.get(); + if (client != null && client.isConnected()) { + ByteBuffer buffer = ByteBuffer.wrap((message + "\n").getBytes()); + client.write(buffer); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + sendMessageThread.start(); + + while (true) { + int readyChannels = selector.select(); + + if (readyChannels == 0) { + continue; + } + + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + + if (key.isAcceptable()) { + // 接受客户端连接 + SocketChannel client = serverSocketChannel.accept(); + System.out.println("客户端已连接"); + client.configureBlocking(false); + client.register(selector, SelectionKey.OP_READ); + clientRef.set(client); + } else if (key.isReadable()) { + // 读取客户端消息 + SocketChannel channel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int bytesRead = channel.read(buffer); + + if (bytesRead > 0) { + buffer.flip(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + String message = new String(bytes).trim(); + System.out.println("客户端消息: " + message); + } + } + keyIterator.remove(); + } + } + } +} +``` + +再来看客户端代码: + +```java +public class Chat2Client { + + public static void main(String[] args) throws IOException { + // 创建一个 SocketChannel + SocketChannel socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + socketChannel.connect(new InetSocketAddress("localhost", 8080)); + + // 创建一个 Selector + Selector selector = Selector.open(); + socketChannel.register(selector, SelectionKey.OP_CONNECT); + + // 从控制台读取输入并发送给服务器端 + Thread sendMessageThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { + while (true) { + System.out.println("输入客户端消息: "); + String message = reader.readLine(); + if (socketChannel.isConnected()) { + ByteBuffer buffer = ByteBuffer.wrap((message + "\n").getBytes()); + socketChannel.write(buffer); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + sendMessageThread.start(); + + while (true) { + int readyChannels = selector.select(); + + if (readyChannels == 0) { + continue; + } + + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + + if (key.isConnectable()) { + // 连接到服务器 + socketChannel.finishConnect(); + socketChannel.register(selector, SelectionKey.OP_READ); + System.out.println("已连接到服务器"); + } else if (key.isReadable()) { + // 读取服务器端消息 + ByteBuffer buffer = ByteBuffer.allocate(1024); + int bytesRead = socketChannel.read(buffer); + + if (bytesRead > 0) { + buffer.flip(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + String message = new String(bytes).trim(); + System.out.println("服务器端消息: " + message); + } + } + keyIterator.remove(); + } + } + } +} +``` + +SocketChannel(用于 TCP 连接)和 ServerSocketChannel(用于监听和接受新的 TCP 连接)可以用来替代传统的 Socket 和 ServerSocket 类,提供非阻塞模式。 + +NIO 支持阻塞和非阻塞模式。非阻塞模式允许程序在等待 I/O 时执行其他任务,从而提高并发性能。非阻塞模式的实现依赖于 Selector,它可以监控多个通道上的 I/O 事件。 + +NIO 支持将数据分散到多个 Buffer(Scatter)或从多个 Buffer 收集数据(Gather),提供了更高效的数据传输方式。 + +Java NIO.2 引入了 AsynchronousSocketChannel 和 AsynchronousServerSocketChannel,这些类提供了基于回调的异步 I/O 操作。异步套接字通道可以在完成 I/O 操作时自动触发回调函数,从而实现高效的异步处理。 + +最后,我们使用 NIO 实现了简单的聊天室功能。通过 ServerSocketChannel 和 SocketChannel 创建服务端和客户端,实现互相发送和接收消息。在处理多个客户端时,可以使用 Selector 来管理多个客户端连接,提高并发性能。 + +总之,Java NIO 网络编程实践提供了更高效、灵活且可扩展的 I/O 处理方式,对于大型应用程序和高并发场景具有显著优势。 diff --git a/docs/order/order-BizOrderService.md b/docs/order/order-BizOrderService.md new file mode 100644 index 000000000..50570015e --- /dev/null +++ b/docs/order/order-BizOrderService.md @@ -0,0 +1,2063 @@ +--- +title: 订单流程 +author: 哪吒 +date: '2020-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 订单流程 + +```java +// 预支付订单缓存键 "pre_pay_order:"; +// 预优惠券缓存键 "pre_coupon:"; + + // 超过多少天未支付,则视为逾期 overdueThr; + // 逾期支付的标准天数 payStd; + // 逾期支付的标准金额 12 amount; + // 改变批量费用规则的数量限制 0.5 countLimit; +``` + +```java + static BlockingQueue blockingQueue = new LinkedBlockingQueue<>(100); //同一时间队列等待数最大超过100时,判定异常,允许抛出 + // 创建一个固定大小的线程池,线程池的大小为当前可用处理器的数量,最大线程数为当前可用处理器的数量乘以4,线程空闲时间超过10秒则被回收 + static ExecutorService fixedThreadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors() * 4, 10, TimeUnit.SECONDS, + blockingQueue,// 队列容量 + new ThreadFactory() { // 线程工厂 + // 定义一个原子整数,用于生成线程的编号 + private final AtomicInteger threadNumber = new AtomicInteger(1); + // 定义线程的名称前缀 + private final String namePrefix = "xxxx"; // 分账处理 + // 重写newThread方法,用于创建线程 + @Override + public Thread newThread(Runnable r) { + // 创建线程,并设置线程的名称为前缀加上线程编号 + Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); + return t; + } + }); +``` + +```java +/** + * 获取预支付订单 + */ +public BExchSvcOrder getCachePreOrderById(String orderId){ // 获取缓存中的预支付订单 +} + +// setRenewEndOrder 续上套餐 完结订单 续费订单新订单 +public BExchSvcOrder setRenewEndOrder(BExchSvcOrder order,String dateTime) throws Exception { + // 查询有无待生效订单,无则正常到期,有则续上套餐 + // 调用mongoPageQuery方法,查询待生效订单 + // 查询有无待生效订单:{} + if (CollUtil.isNotEmpty( "待生效订单" )) { + // setEndOldOrder 完结旧订单 续费完结 + // updType (value = "修改事件标签 1普通事件2换电事件") + // 然后更新订单 旧订单 结算状态:{},结算订单:{} + // 续上待生效为新执行中订单 + // 服务到期时间 + // 生效订单加上上次次数,实际次数 + // 自动续租订单内容:{} + // 自动续租状态:{},自动续租订单:{} + } +} + +// 订单退款 +public RestRet orderRefund(OrderRefundBO orderRefundBO){ + // 获取订单 + // 创建订单对象 + // 实际费用 totalFee Double.parseDouble 退款金额 refundAmount 折扣金额/最终金额 discountA + // 退款金额不能小于等于0 + // 退款金额大于可退款最高金额 + // 创建WXRefundRequest对象 退款请求 + // 设置退款类型 取消订单 余额退款 押金解冻/退款 订单退款,允许部分退款 + // 是否全额退款 0否,1是 + // 支付方式 默认1微信2支付宝3余额 + // 调用服务实现类 refundOfOrder +} + +// 订单转让 +public RestRet orderTransfer(BExchSvcOrderBO bExchSvcOrderBO) throws Exception { // 订单转让 + // +} +``` + +## BlockingQueue + +```java +public interface BlockingQueue extends Queue { // 定义一个阻塞队列接口,继承自队列接口 + /** + * 如果可以立即将指定元素插入此队列而不违反容量限制,则插入该元素, + * 成功时返回{@code true},如果当前没有可用空间,则抛出{@code IllegalStateException}。 + * 在使用容量受限的队列时,通常更倾向于使用{@link #offer(Object) offer}。 + * + * @param e 要添加的元素 + * @return {@code true}(如{@link Collection#add}所指定) + * @throws IllegalStateException 如果由于容量限制而无法在此时添加元素 + * @throws ClassCastException 如果指定元素的类阻止其被添加到此队列 + * @throws NullPointerException 如果指定元素为null + * @throws IllegalArgumentException 如果指定元素的某些属性阻止其被添加到此队列 + */ + boolean add(E e); // 添加元素的方法 + + /** + * 如果可以立即将指定元素插入此队列而不违反容量限制,则插入该元素, + * 成功时返回{@code true},如果当前没有可用空间,则返回{@code false}。 + * 在使用容量受限的队列时,此方法通常比{@link #add}更可取, + * 因为{@link #add}只能通过抛出异常来失败。 + * + * @param e 要添加的元素 + * @return {@code true} 如果元素被添加到此队列,否则{@code false} + * @throws ClassCastException 如果指定元素的类阻止其被添加到此队列 + * @throws NullPointerException 如果指定元素为null + * @throws IllegalArgumentException 如果指定元素的某些属性阻止其被添加到此队列 + */ + boolean offer(E e); // 尝试添加元素的方法 + + /** + * 插入指定元素到此队列,如果必要,等待空间变得可用。 + * + * @param e 要添加的元素 + * @throws InterruptedException 如果在等待时被中断 + * @throws ClassCastException 如果指定元素的类阻止其被添加到此队列 + * @throws NullPointerException 如果指定元素为null + * @throws IllegalArgumentException 如果指定元素的某些属性阻止其被添加到此队列 + */ + void put(E e) throws InterruptedException; // 等待插入元素的方法 + + /** + * 插入指定元素到此队列,如果必要,等待最多指定的等待时间, + * 直到空间变得可用。 + * + * @param e 要添加的元素 + * @param timeout 等待放弃之前的时间,单位为{@code unit} + * @param unit 一个{@code TimeUnit},决定如何解释{@code timeout}参数 + * @return {@code true} 如果成功,或者{@code false} 如果在指定的等待时间内空间不可用 + * @throws InterruptedException 如果在等待时被中断 + * @throws ClassCastException 如果指定元素的类阻止其被添加到此队列 + * @throws NullPointerException 如果指定元素为null + * @throws IllegalArgumentException 如果指定元素的某些属性阻止其被添加到此队列 + */ + boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; // 带超时的尝试添加元素的方法 + + /** + * 检索并移除此队列的头部,如果必要,等待直到元素可用。 + * + * @return 此队列的头部 + * @throws InterruptedException 如果在等待时被中断 + */ + E take() throws InterruptedException; // 获取并移除头部元素的方法 + + /** + * 检索并移除此队列的头部,如果必要,等待最多指定的等待时间, + * 直到元素可用。 + * + * @param timeout 等待放弃之前的时间,单位为{@code unit} + * @param unit 一个{@code TimeUnit},决定如何解释{@code timeout}参数 + * @return 此队列的头部,或者{@code null} 如果在指定的等待时间内没有元素可用 + * @throws InterruptedException 如果在等待时被中断 + */ + E poll(long timeout, TimeUnit unit) throws InterruptedException; // 带超时的获取并移除头部元素的方法 + + /** + * 返回此队列在没有内存或资源限制的情况下理想上可以接受的额外元素数量, + * 如果没有内在限制,则返回{@code Integer.MAX_VALUE}。 + * + *

注意,您无法通过检查{@code remainingCapacity}来判断插入元素是否会成功, + * 因为可能有其他线程即将插入或移除元素。 + * + * @return 剩余容量 + */ + int remainingCapacity(); // 返回剩余容量的方法 + + /** + * 从此队列中移除指定元素的单个实例(如果存在)。 + * 更正式地说,移除一个元素{@code e},使得{@code o.equals(e)}, + * 如果此队列包含一个或多个这样的元素。 + * 如果此队列包含指定元素(或者等价地,如果此队列因调用而发生变化), + * 则返回{@code true}。 + * + * @param o 要从此队列中移除的元素(如果存在) + * @return {@code true} 如果此队列因调用而发生变化 + * @throws ClassCastException 如果指定元素的类与此队列不兼容 + * (可选) + * @throws NullPointerException 如果指定元素为null + * (可选) + */ + boolean remove(Object o); // 移除指定元素的方法 + + /** + * 如果此队列包含指定元素,则返回{@code true}。 + * 更正式地说,仅当此队列至少包含一个元素{@code e}使得{@code o.equals(e)}时返回{@code true}。 + * + * @param o 要检查是否包含在此队列中的对象 + * @return {@code true} 如果此队列包含指定元素 + * @throws ClassCastException 如果指定元素的类与此队列不兼容 + * (可选) + * @throws NullPointerException 如果指定元素为null + * (可选) + */ + public boolean contains(Object o); // 检查队列中是否包含指定元素的方法 + + /** + * 从此队列中移除所有可用元素并将它们添加到给定集合中。 + * 此操作可能比重复轮询此队列更高效。 + * 在尝试将元素添加到集合{@code c}时遇到的失败可能导致元素 + * 同时存在于两个集合中,或者在抛出相关异常时都不在任何集合中。 + * 尝试将队列排空到自身会导致{@code IllegalArgumentException}。 + * 此外,如果在操作进行时修改了指定集合,则此操作的行为是未定义的。 + * + * @param c 要转移元素到的集合 + * @return 转移的元素数量 + * @throws UnsupportedOperationException 如果指定集合不支持添加元素 + * @throws ClassCastException 如果此队列的元素类阻止其被添加到指定集合 + * @throws NullPointerException 如果指定集合为null + * @throws IllegalArgumentException 如果指定集合是此队列,或者此队列的某个元素的属性阻止其被添加到指定集合 + */ + int drainTo(Collection c); // 将所有可用元素转移到指定集合的方法 + + /** + * 从此队列中最多移除给定数量的可用元素并将它们添加到给定集合中。 + * 在尝试将元素添加到集合{@code c}时遇到的失败可能导致元素 + * 同时存在于两个集合中,或者在抛出相关异常时都不在任何集合中。 + * 尝试将队列排空到自身会导致{@code IllegalArgumentException}。 + * 此外,如果在操作进行时修改了指定集合,则此操作的行为是未定义的。 + * + * @param c 要转移元素到的集合 + * @param maxElements 要转移的最大元素数量 + * @return 转移的元素数量 + * @throws UnsupportedOperationException 如果指定集合不支持添加元素 + * @throws ClassCastException 如果此队列的元素类阻止其被添加到指定集合 + * @throws NullPointerException 如果指定集合为null + * @throws IllegalArgumentException 如果指定集合是此队列,或者此队列的某个元素的属性阻止其被添加到指定集合 + */ + int drainTo(Collection c, int maxElements); // 将最多指定数量的元素转移到指定集合的方法 +} +``` + +## LinkedBlockingQueue + +```java +public class LinkedBlockingQueue extends AbstractQueue // 定义一个链表阻塞队列类,继承自抽象队列 + implements BlockingQueue, java.io.Serializable { // 实现阻塞队列接口和可序列化接口 + private static final long serialVersionUID = -6903933977591709194L; // 序列化ID + + /* + * "双锁队列"算法的变体。putLock控制插入(和offer)的进入, + * 并具有一个与之相关的条件,用于等待插入。takeLock也是如此。 + * 它们都依赖的“计数”字段作为原子变量维护,以避免在大多数情况下 + * 需要获取两个锁。此外,为了最小化插入和取出锁的需求, + * 使用级联通知。当一个插入操作注意到它启用了至少一个取出时, + * 它会通知取出者。取出者反过来会通知其他人,如果在信号之后 + * 进入了更多项目。取出操作也会通知插入操作。 + * + * 写入者和读取者之间的可见性如下: + * + * 每当一个元素被入队时,获取putLock并更新计数。 + * 随后的读取者通过获取putLock(通过fullyLock) + * 或获取takeLock,然后读取n = count.get()来保证对入队节点的可见性; + * 这给出了前n个项目的可见性。 + * + * 为了实现弱一致性迭代器,似乎我们需要保持所有节点 + * 从前驱出队节点可达。这会导致两个问题: + * - 允许恶意迭代器导致无限内存保留 + * - 如果一个节点在存活时被晋升为老年代,导致跨代链接 + * 旧节点和新节点,代际GC对此处理困难,导致重复的重大收集。 + * 然而,只有未删除的节点需要从出队节点可达, + * 可达性不一定必须是GC理解的那种。 + * 我们使用将刚刚出队的节点链接到自身的技巧。 + * 这样的自链接隐含地意味着向head.next推进。 + */ + + /** + * 链表节点类 + */ + static class Node { // 定义节点类 + E item; // 节点存储的元素 + + /** + * 可能是: + * - 真实的后继节点 + * - 该节点,意味着后继是head.next + * - null,意味着没有后继(这是最后一个节点) + */ + Node next; // 指向下一个节点 + + Node(E x) { item = x; } // 节点构造函数 + } + + /** 容量限制,如果没有则为Integer.MAX_VALUE */ + private final int capacity; // 队列的最大容量 + + /** 当前元素数量 */ + private final AtomicInteger count = new AtomicInteger(); // 当前元素计数 + + /** + * 链表的头部。 + * 不变式:head.item == null + */ + transient Node head; // 链表头部节点 + + /** + * 链表的尾部。 + * 不变式:last.next == null + */ + private transient Node last; // 链表尾部节点 + + /** 由take、poll等持有的锁 */ + private final ReentrantLock takeLock = new ReentrantLock(); // 取出操作的锁 + + /** 等待取出的等待队列 */ + private final Condition notEmpty = takeLock.newCondition(); // 等待非空条件 + + /** 由put、offer等持有的锁 */ + private final ReentrantLock putLock = new ReentrantLock(); // 插入操作的锁 + + /** 等待插入的等待队列 */ + private final Condition notFull = putLock.newCondition(); // 等待非满条件 + + /** + * 通知等待的取出操作。仅从put/offer调用(否则不锁定takeLock)。 + */ + private void signalNotEmpty() { // 通知有元素可取 + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lock(); // 锁定 + try { + notEmpty.signal(); // 通知等待的取出操作 + } finally { + takeLock.unlock(); // 解锁 + } + } + + /** + * 通知等待的插入操作。仅从take/poll调用。 + */ + private void signalNotFull() { // 通知有空间可插入 + final ReentrantLock putLock = this.putLock; // 获取插入锁 + putLock.lock(); // 锁定 + try { + notFull.signal(); // 通知等待的插入操作 + } finally { + putLock.unlock(); // 解锁 + } + } + + /** + * 将节点链接到队列的末尾。 + * + * @param node 节点 + */ + private void enqueue(Node node) { // 将节点入队 + // assert putLock.isHeldByCurrentThread(); // 确保当前线程持有插入锁 + // assert last.next == null; // 确保最后一个节点的next为null + last = last.next = node; // 将新节点链接到链表末尾 + } + + /** + * 从队列头部移除一个节点。 + * + * @return 移除的节点 + */ + private E dequeue() { // 将节点出队 + // assert takeLock.isHeldByCurrentThread(); // 确保当前线程持有取出锁 + // assert head.item == null; // 确保头部节点的item为null + Node h = head; // 获取头部节点 + Node first = h.next; // 获取第一个实际节点 + h.next = h; // 帮助GC,清除头部节点的next引用 + head = first; // 更新头部节点 + E x = first.item; // 获取第一个节点的元素 + first.item = null; // 清除第一个节点的元素引用 + return x; // 返回出队的元素 + } + + /** + * 锁定以防止同时进行插入和取出操作。 + */ + void fullyLock() { // 完全锁定 + putLock.lock(); // 锁定插入锁 + takeLock.lock(); // 锁定取出锁 + } + + /** + * 解锁以允许同时进行插入和取出操作。 + */ + void fullyUnlock() { // 完全解锁 + takeLock.unlock(); // 解锁取出锁 + putLock.unlock(); // 解锁插入锁 + } + +// /** +// * 告诉当前线程是否持有两个锁。 +// */ +// boolean isFullyLocked() { +// return (putLock.isHeldByCurrentThread() && +// takeLock.isHeldByCurrentThread()); +// } + + /** + * 创建一个容量为{@link Integer#MAX_VALUE}的{@code LinkedBlockingQueue}。 + */ + public LinkedBlockingQueue() { // 默认构造函数 + this(Integer.MAX_VALUE); // 调用带容量参数的构造函数 + } + + /** + * 创建一个具有给定(固定)容量的{@code LinkedBlockingQueue}。 + * + * @param capacity 此队列的容量 + * @throws IllegalArgumentException 如果{@code capacity}不大于零 + */ + public LinkedBlockingQueue(int capacity) { // 带容量参数的构造函数 + if (capacity <= 0) throw new IllegalArgumentException(); // 检查容量是否合法 + this.capacity = capacity; // 设置容量 + last = head = new Node(null); // 初始化头部和尾部节点 + } + + /** + * 创建一个容量为{@link Integer#MAX_VALUE}的{@code LinkedBlockingQueue}, + * 初始包含给定集合的元素, + * 按集合迭代器的遍历顺序添加。 + * + * @param c 初始包含的元素集合 + * @throws NullPointerException 如果指定的集合或其任何元素为null + */ + public LinkedBlockingQueue(Collection c) { // 带集合参数的构造函数 + this(Integer.MAX_VALUE); // 调用默认容量构造函数 + final ReentrantLock putLock = this.putLock; // 获取插入锁 + putLock.lock(); // 锁定 + try { + int n = 0; // 计数器 + for (E e : c) { // 遍历集合 + if (e == null) // 检查元素是否为null + throw new NullPointerException(); + if (n == capacity) // 检查是否超过容量 + throw new IllegalStateException("Queue full"); + enqueue(new Node(e)); // 将元素入队 + ++n; // 增加计数 + } + count.set(n); // 设置当前元素计数 + } finally { + putLock.unlock(); // 解锁 + } + } + + // 此文档注释被重写以删除对大于Integer.MAX_VALUE的集合的引用 + /** + * 返回此队列中的元素数量。 + * + * @return 此队列中的元素数量 + */ + public int size() { // 获取队列大小 + return count.get(); // 返回当前元素计数 + } + + // 此文档注释是修改后的副本, + // 没有对无限队列的引用。 + /** + * 返回此队列在没有内存或资源限制的情况下理想上可以接受的额外元素数量, + * 这始终等于此队列的初始容量减去当前的{@code size}。 + * + *

注意,您无法通过检查{@code remainingCapacity}来判断插入元素是否会成功, + * 因为可能有其他线程即将插入或移除元素。 + */ + public int remainingCapacity() { // 获取剩余容量 + return capacity - count.get(); // 返回剩余容量 + } + + /** + * 在此队列的尾部插入指定元素,如果必要,等待空间变得可用。 + * + * @throws InterruptedException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + */ + public void put(E e) throws InterruptedException { // 插入元素的方法 + if (e == null) throw new NullPointerException(); // 检查元素是否为null + // 注意:所有put/take等操作的约定是预设局部变量 + // 以负数表示失败,除非设置。 + int c = -1; // 初始化计数 + Node node = new Node(e); // 创建新节点 + final ReentrantLock putLock = this.putLock; // 获取插入锁 + final AtomicInteger count = this.count; // 获取当前计数 + putLock.lockInterruptibly(); // 可中断地锁定 + try { + /* + * 注意,计数在等待保护中使用,尽管它没有被锁保护。 + * 这是有效的,因为此时计数只能减少(所有其他插入都被锁定), + * 如果它从容量变化,我们(或其他等待的插入)会被通知。 + * 对于其他等待保护中的计数的所有其他使用也是如此。 + */ + while (count.get() == capacity) { // 如果队列已满 + notFull.await(); // 等待直到有空间 + } + enqueue(node); // 将节点入队 + c = count.getAndIncrement(); // 增加计数 + if (c + 1 < capacity) // 如果还有空间 + notFull.signal(); // 通知等待的插入操作 + } finally { + putLock.unlock(); // 解锁 + } + if (c == 0) // 如果之前队列为空 + signalNotEmpty(); // 通知有元素可取 + } + + /** + * 在此队列的尾部插入指定元素,如果必要,等待最多指定的等待时间, + * 直到空间变得可用。 + * + * @return {@code true} 如果成功,或者{@code false} 如果 + * 指定的等待时间在空间可用之前到期 + * @throws InterruptedException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + */ + public boolean offer(E e, long timeout, TimeUnit unit) // 带超时的插入元素的方法 + throws InterruptedException { + + if (e == null) throw new NullPointerException(); // 检查元素是否为null + long nanos = unit.toNanos(timeout); // 将超时转换为纳秒 + int c = -1; // 初始化计数 + final ReentrantLock putLock = this.putLock; // 获取插入锁 + final AtomicInteger count = this.count; // 获取当前计数 + putLock.lockInterruptibly(); // 可中断地锁定 + try { + while (count.get() == capacity) { // 如果队列已满 + if (nanos <= 0) // 如果超时 + return false; // 返回失败 + nanos = notFull.awaitNanos(nanos); // 等待纳秒 + } + enqueue(new Node(e)); // 将节点入队 + c = count.getAndIncrement(); // 增加计数 + if (c + 1 < capacity) // 如果还有空间 + notFull.signal(); // 通知等待的插入操作 + } finally { + putLock.unlock(); // 解锁 + } + if (c == 0) // 如果之前队列为空 + signalNotEmpty(); // 通知有元素可取 + return true; // 返回成功 + } + + /** + * 如果可以立即将指定元素插入此队列而不超过队列的容量, + * 则在此队列的尾部插入指定元素,成功时返回{@code true}, + * 如果此队列已满,则返回{@code false}。 + * 在使用容量受限的队列时,此方法通常比{@link BlockingQueue#add add}方法更可取, + * 后者只能通过抛出异常来失败。 + * + * @throws NullPointerException 如果指定元素为null + */ + public boolean offer(E e) { // 尝试立即插入元素的方法 + if (e == null) throw new NullPointerException(); // 检查元素是否为null + final AtomicInteger count = this.count; // 获取当前计数 + if (count.get() == capacity) // 如果队列已满 + return false; // 返回失败 + int c = -1; // 初始化计数 + Node node = new Node(e); // 创建新节点 + final ReentrantLock putLock = this.putLock; // 获取插入锁 + putLock.lock(); // 锁定 + try { + if (count.get() < capacity) { // 如果还有空间 + enqueue(node); // 将节点入队 + c = count.getAndIncrement(); // 增加计数 + if (c + 1 < capacity) // 如果还有空间 + notFull.signal(); // 通知等待的插入操作 + } + } finally { + putLock.unlock(); // 解锁 + } + if (c == 0) // 如果之前队列为空 + signalNotEmpty(); // 通知有元素可取 + return c >= 0; // 返回是否成功 + } + + public E take() throws InterruptedException { // 获取并移除头部元素的方法 + E x; // 存储出队元素 + int c = -1; // 初始化计数 + final AtomicInteger count = this.count; // 获取当前计数 + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lockInterruptibly(); // 可中断地锁定 + try { + while (count.get() == 0) { // 如果队列为空 + notEmpty.await(); // 等待直到有元素可取 + } + x = dequeue(); // 出队元素 + c = count.getAndDecrement(); // 减少计数 + if (c > 1) // 如果还有元素 + notEmpty.signal(); // 通知等待的取出操作 + } finally { + takeLock.unlock(); // 解锁 + } + if (c == capacity) // 如果之前队列已满 + signalNotFull(); // 通知有空间可插入 + return x; // 返回出队的元素 + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { // 带超时的获取并移除头部元素的方法 + E x = null; // 存储出队元素 + int c = -1; // 初始化计数 + long nanos = unit.toNanos(timeout); // 将超时转换为纳秒 + final AtomicInteger count = this.count; // 获取当前计数 + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lockInterruptibly(); // 可中断地锁定 + try { + while (count.get() == 0) { // 如果队列为空 + if (nanos <= 0) // 如果超时 + return null; // 返回null + nanos = notEmpty.awaitNanos(nanos); // 等待纳秒 + } + x = dequeue(); // 出队元素 + c = count.getAndDecrement(); // 减少计数 + if (c > 1) // 如果还有元素 + notEmpty.signal(); // 通知等待的取出操作 + } finally { + takeLock.unlock(); // 解锁 + } + if (c == capacity) // 如果之前队列已满 + signalNotFull(); // 通知有空间可插入 + return x; // 返回出队的元素 + } + + public E poll() { // 获取并移除头部元素的方法 + final AtomicInteger count = this.count; // 获取当前计数 + if (count.get() == 0) // 如果队列为空 + return null; // 返回null + E x = null; // 存储出队元素 + int c = -1; // 初始化计数 + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lock(); // 锁定 + try { + if (count.get() > 0) { // 如果队列不为空 + x = dequeue(); // 出队元素 + c = count.getAndDecrement(); // 减少计数 + if (c > 1) // 如果还有元素 + notEmpty.signal(); // 通知等待的取出操作 + } + } finally { + takeLock.unlock(); // 解锁 + } + if (c == capacity) // 如果之前队列已满 + signalNotFull(); // 通知有空间可插入 + return x; // 返回出队的元素 + } + + public E peek() { // 查看头部元素的方法 + if (count.get() == 0) // 如果队列为空 + return null; // 返回null + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lock(); // 锁定 + try { + Node first = head.next; // 获取第一个实际节点 + if (first == null) // 如果没有节点 + return null; // 返回null + else + return first.item; // 返回头部元素 + } finally { + takeLock.unlock(); // 解锁 + } + } + + /** + * 与前驱trail一起取消链接内部节点p。 + */ + void unlink(Node p, Node trail) { // 取消节点链接的方法 + // assert isFullyLocked(); // 确保当前线程持有两个锁 + // p.next未更改,以允许遍历p的迭代器保持其弱一致性保证。 + p.item = null; // 清除节点的元素引用 + trail.next = p.next; // 将前驱节点的next指向p的下一个节点 + if (last == p) // 如果p是最后一个节点 + last = trail; // 更新最后一个节点 + if (count.getAndDecrement() == capacity) // 如果计数减少到容量 + notFull.signal(); // 通知有空间可插入 + } + + /** + * 从此队列中移除指定元素的单个实例(如果存在)。 + * 更正式地说,移除一个元素{@code e},使得{@code o.equals(e)}, + * 如果此队列包含一个或多个这样的元素。 + * 如果此队列包含指定元素(或者等价地,如果此队列因调用而发生变化), + * 则返回{@code true}。 + * + * @param o 要从此队列中移除的元素(如果存在) + * @return {@code true} 如果此队列因调用而发生变化 + */ + public boolean remove(Object o) { // 移除指定元素的方法 + if (o == null) return false; // 检查元素是否为null + fullyLock(); // 完全锁定 + try { + for (Node trail = head, p = trail.next; // 遍历链表 + p != null; + trail = p, p = p.next) { + if (o.equals(p.item)) { // 如果找到匹配的元素 + unlink(p, trail); // 取消链接 + return true; // 返回成功 + } + } + return false; // 返回失败 + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * 返回{@code true}如果此队列包含指定元素。 + * 更正式地说,仅当此队列至少包含一个元素{@code e}使得{@code o.equals(e)}时返回{@code true}。 + * + * @param o 要检查是否包含在此队列中的对象 + * @return {@code true} 如果此队列包含指定元素 + */ + public boolean contains(Object o) { // 检查队列中是否包含指定元素的方法 + if (o == null) return false; // 检查元素是否为null + fullyLock(); // 完全锁定 + try { + for (Node p = head.next; p != null; p = p.next) // 遍历链表 + if (o.equals(p.item)) // 如果找到匹配的元素 + return true; // 返回成功 + return false; // 返回失败 + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * 返回一个数组,包含此队列中的所有元素,按正确的顺序。 + * + *

返回的数组将是“安全的”,因为此队列不会维护对它的引用。 + * (换句话说,此方法必须分配一个新数组)。 + * 调用者因此可以自由地修改返回的数组。 + * + *

此方法充当数组基础和基于集合的API之间的桥梁。 + * + * @return 包含此队列中所有元素的数组 + */ + public Object[] toArray() { // 将队列元素转换为数组的方法 + fullyLock(); // 完全锁定 + try { + int size = count.get(); // 获取当前元素计数 + Object[] a = new Object[size]; // 创建新数组 + int k = 0; // 数组索引 + for (Node p = head.next; p != null; p = p.next) // 遍历链表 + a[k++] = p.item; // 将元素添加到数组 + return a; // 返回数组 + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * 返回一个数组,包含此队列中的所有元素,按正确的顺序; + * 返回数组的运行时类型为指定数组的类型。 + * 如果队列适合指定数组,则返回该数组。 + * 否则,将分配一个新数组,其运行时类型为指定数组的类型,大小为此队列的大小。 + * + *

如果此队列适合指定数组并且有多余的空间 + * (即,数组的元素比此队列多),则数组中紧跟队列末尾的元素设置为 + * {@code null}。 + * + *

像{@link #toArray()}方法一样,此方法充当数组基础和 + * 基于集合的API之间的桥梁。此外,此方法允许 + * 精确控制输出数组的运行时类型,并且在某些情况下, + * 可以用于节省分配成本。 + * + *

假设{@code x}是一个已知仅包含字符串的队列。 + * 以下代码可以用于将队列转储到新分配的{@code String}数组中: + * + *

 {@code String[] y = x.toArray(new String[0]);}
+ * + * 注意{@code toArray(new Object[0])}的功能与 + * {@code toArray()}相同。 + * + * @param a 要存储队列元素的数组,如果足够大;否则,为此目的分配一个新数组 + * @return 包含此队列中所有元素的数组 + * @throws ArrayStoreException 如果指定数组的运行时类型 + * 不是此队列中每个元素的运行时类型的超类型 + * @throws NullPointerException 如果指定数组为null + */ + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { // 将队列元素转换为指定类型的数组的方法 + fullyLock(); // 完全锁定 + try { + int size = count.get(); // 获取当前元素计数 + if (a.length < size) // 如果指定数组不够大 + a = (T[])java.lang.reflect.Array.newInstance // 创建新数组 + (a.getClass().getComponentType(), size); + + int k = 0; // 数组索引 + for (Node p = head.next; p != null; p = p.next) // 遍历链表 + a[k++] = (T)p.item; // 将元素添加到数组 + if (a.length > k) // 如果数组还有空间 + a[k] = null; // 设置数组末尾为null + return a; // 返回数组 + } finally { + fullyUnlock(); // 解锁 + } + } + + public String toString() { // 转换为字符串的方法 + fullyLock(); // 完全锁定 + try { + Node p = head.next; // 获取第一个实际节点 + if (p == null) // 如果没有节点 + return "[]"; // 返回空数组表示 + + StringBuilder sb = new StringBuilder(); // 创建字符串构建器 + sb.append('['); // 添加开头的方括号 + for (;;) { // 无限循环 + E e = p.item; // 获取当前节点的元素 + sb.append(e == this ? "(this Collection)" : e); // 添加元素到字符串 + p = p.next; // 移动到下一个节点 + if (p == null) // 如果没有下一个节点 + return sb.append(']').toString(); // 返回完整字符串 + sb.append(',').append(' '); // 添加逗号和空格 + } + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * 原子性地移除此队列中的所有元素。 + * 调用此方法后,队列将为空。 + */ + public void clear() { // 清空队列的方法 + fullyLock(); // 完全锁定 + try { + for (Node p, h = head; (p = h.next) != null; h = p) { // 遍历链表 + h.next = h; // 帮助GC,清除节点的next引用 + p.item = null; // 清除节点的元素引用 + } + head = last; // 更新头部节点 + // assert head.item == null && head.next == null; // 确保头部节点为空 + if (count.getAndSet(0) == capacity) // 如果计数减少到容量 + notFull.signal(); // 通知有空间可插入 + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * @throws UnsupportedOperationException {@inheritDoc} + * @throws ClassCastException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + */ + public int drainTo(Collection c) { // 将元素转移到指定集合的方法 + return drainTo(c, Integer.MAX_VALUE); // 调用带最大元素参数的方法 + } + + /** + * @throws UnsupportedOperationException {@inheritDoc} + * @throws ClassCastException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + */ + public int drainTo(Collection c, int maxElements) { // 将最多指定数量的元素转移到指定集合的方法 + if (c == null) // 检查集合是否为null + throw new NullPointerException(); + if (c == this) // 检查集合是否为自身 + throw new IllegalArgumentException(); + if (maxElements <= 0) // 检查最大元素数量是否合法 + return 0; // 返回0 + boolean signalNotFull = false; // 标记是否需要通知有空间可插入 + final ReentrantLock takeLock = this.takeLock; // 获取取出锁 + takeLock.lock(); // 锁定 + try { + int n = Math.min(maxElements, count.get()); // 获取可转移的元素数量 + // count.get提供对前n个节点的可见性 + Node h = head; // 获取头部节点 + int i = 0; // 计数器 + try { + while (i < n) { // 转移元素 + Node p = h.next; // 获取下一个节点 + c.add(p.item); // 将元素添加到集合 + p.item = null; // 清除节点的元素引用 + h.next = h; // 帮助GC,清除节点的next引用 + h = p; // 移动到下一个节点 + ++i; // 增加计数 + } + return n; // 返回转移的元素数量 + } finally { + // 即使c.add()抛出异常,也要恢复不变式 + if (i > 0) { // 如果有元素转移 + // assert h.item == null; // 确保当前节点为空 + head = h; // 更新头部节点 + signalNotFull = (count.getAndAdd(-i) == capacity); // 更新计数并检查是否需要通知 + } + } + } finally { + takeLock.unlock(); // 解锁 + if (signalNotFull) // 如果需要通知 + signalNotFull(); // 通知有空间可插入 + } + } + + /** + * 返回一个迭代器,按正确顺序遍历此队列中的元素。 + * 元素将按从第一个(头部)到最后一个(尾部)的顺序返回。 + * + *

返回的迭代器是 + * 弱一致性。 + * + * @return 一个按正确顺序遍历此队列中元素的迭代器 + */ + public Iterator iterator() { // 获取迭代器的方法 + return new Itr(); // 返回新的迭代器实例 + } + + private class Itr implements Iterator { // 定义迭代器类 + /* + * 基本的弱一致性迭代器。始终持有下一个 + * 要返回的元素,以便如果hasNext()报告为true, + * 我们仍然可以返回它,即使与取出等操作竞争失败。 + */ + + private Node current; // 当前节点 + private Node lastRet; // 上一个返回的节点 + private E currentElement; // 当前元素 + + Itr() { // 迭代器构造函数 + fullyLock(); // 完全锁定 + try { + current = head.next; // 获取第一个实际节点 + if (current != null) // 如果节点不为空 + currentElement = current.item; // 获取当前元素 + } finally { + fullyUnlock(); // 解锁 + } + } + + public boolean hasNext() { // 检查是否还有下一个元素的方法 + return current != null; // 如果当前节点不为空,返回true + } + + /** + * 返回p的下一个有效后继节点,如果没有则返回null。 + * + * 与其他遍历方法不同,迭代器需要处理: + * - 出队节点(p.next == p) + * - (可能多个)内部删除节点(p.item == null) + */ + private Node nextNode(Node p) { // 获取下一个节点的方法 + for (;;) { // 无限循环 + Node s = p.next; // 获取下一个节点 + if (s == p) // 如果是自链接 + return head.next; // 返回头部的下一个节点 + if (s == null || s.item != null) // 如果节点为空或有效 + return s; // 返回有效节点 + p = s; // 移动到下一个节点 + } + } + + public E next() { // 获取下一个元素的方法 + fullyLock(); // 完全锁定 + try { + if (current == null) // 如果没有下一个元素 + throw new NoSuchElementException(); // 抛出异常 + E x = currentElement; // 获取当前元素 + lastRet = current; // 更新上一个返回的节点 + current = nextNode(current); // 获取下一个节点 + currentElement = (current == null) ? null : current.item; // 更新当前元素 + return x; // 返回当前元素 + } finally { + fullyUnlock(); // 解锁 + } + } + + public void remove() { // 移除当前元素的方法 + if (lastRet == null) // 如果没有上一个返回的节点 + throw new IllegalStateException(); // 抛出异常 + fullyLock(); // 完全锁定 + try { + Node node = lastRet; // 获取上一个返回的节点 + lastRet = null; // 清除上一个返回的节点 + for (Node trail = head, p = trail.next; // 遍历链表 + p != null; + trail = p, p = p.next) { + if (p == node) { // 如果找到要移除的节点 + unlink(p, trail); // 取消链接 + break; // 退出循环 + } + } + } finally { + fullyUnlock(); // 解锁 + } + } + } + + /** 自定义的Spliterators.IteratorSpliterator变体 */ + static final class LBQSpliterator implements Spliterator { // 定义Spliterator类 + static final int MAX_BATCH = 1 << 25; // 最大批处理数组大小; + final LinkedBlockingQueue queue; // 队列引用 + Node current; // 当前节点;初始化前为null + int batch; // 分割的批处理大小 + boolean exhausted; // 当没有更多节点时为true + long est; // 大小估计 + LBQSpliterator(LinkedBlockingQueue queue) { // 构造函数 + this.queue = queue; // 设置队列引用 + this.est = queue.size(); // 初始化大小估计 + } + + public long estimateSize() { return est; } // 返回估计大小 + + public Spliterator trySplit() { // 尝试分割的方法 + Node h; // 当前节点 + final LinkedBlockingQueue q = this.queue; // 获取队列引用 + int b = batch; // 获取当前批处理大小 + int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1; // 计算新的批处理大小 + if (!exhausted && // 如果没有耗尽 + ((h = current) != null || (h = q.head.next) != null) && // 获取当前节点或头部的下一个节点 + h.next != null) { // 如果下一个节点不为空 + Object[] a = new Object[n]; // 创建新数组 + int i = 0; // 数组索引 + Node p = current; // 获取当前节点 + q.fullyLock(); // 完全锁定 + try { + if (p != null || (p = q.head.next) != null) { // 如果当前节点不为空或头部的下一个节点不为空 + do { + if ((a[i] = p.item) != null) // 将节点的元素添加到数组 + ++i; // 增加计数 + } while ((p = p.next) != null && i < n); // 遍历节点 + } + } finally { + q.fullyUnlock(); // 解锁 + } + if ((current = p) == null) { // 更新当前节点 + est = 0L; // 更新估计大小 + exhausted = true; // 标记为耗尽 + } + else if ((est -= i) < 0L) // 更新估计大小 + est = 0L; // 确保不为负 + if (i > 0) { // 如果有元素转移 + batch = i; // 更新批处理大小 + return Spliterators.spliterator // 返回Spliterator + (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT); + } + } + return null; // 返回null + } + + public void forEachRemaining(Consumer action) { // 对剩余元素执行操作的方法 + if (action == null) throw new NullPointerException(); // 检查操作是否为null + final LinkedBlockingQueue q = this.queue; // 获取队列引用 + if (!exhausted) { // 如果没有耗尽 + exhausted = true; // 标记为耗尽 + Node p = current; // 获取当前节点 + do { + E e = null; // 存储元素 + q.fullyLock(); // 完全锁定 + try { + if (p == null) // 如果当前节点为空 + p = q.head.next; // 获取头部的下一个节点 + while (p != null) { // 遍历节点 + e = p.item; // 获取节点的元素 + p = p.next; // 移动到下一个节点 + if (e != null) // 如果元素不为空 + break; // 退出循环 + } + } finally { + q.fullyUnlock(); // 解锁 + } + if (e != null) // 如果元素不为空 + action.accept(e); // 执行操作 + } while (p != null); // 继续直到没有更多节点 + } + } + + public boolean tryAdvance(Consumer action) { // 尝试获取下一个元素的方法 + if (action == null) throw new NullPointerException(); // 检查操作是否为null + final LinkedBlockingQueue q = this.queue; // 获取队列引用 + if (!exhausted) { // 如果没有耗尽 + E e = null; // 存储元素 + q.fullyLock(); // 完全锁定 + try { + if (current == null) // 如果当前节点为空 + current = q.head.next; // 获取头部的下一个节点 + while (current != null) { // 遍历节点 + e = current.item; // 获取节点的元素 + current = current.next; // 移动到下一个节点 + if (e != null) // 如果元素不为空 + break; // 退出循环 + } + } finally { + q.fullyUnlock(); // 解锁 + } + if (current == null) // 如果没有更多节点 + exhausted = true; // 标记为耗尽 + if (e != null) { // 如果元素不为空 + action.accept(e); // 执行操作 + return true; // 返回成功 + } + } + return false; // 返回失败 + } + + public int characteristics() { // 返回特征的方法 + return Spliterator.ORDERED | Spliterator.NONNULL | + Spliterator.CONCURRENT; // 返回特征 + } + } + + /** + * 返回一个{@link Spliterator},遍历此队列中的元素。 + * + *

返回的spliterator是 + * 弱一致性。 + * + *

该{@code Spliterator}报告{@link Spliterator#CONCURRENT}, + * {@link Spliterator#ORDERED}和{@link Spliterator#NONNULL}。 + * + * @implNote + * 该{@code Spliterator}实现{@code trySplit}以允许有限的 + * 并行性。 + * + * @return 一个{@code Spliterator},遍历此队列中的元素 + * @since 1.8 + */ + public Spliterator spliterator() { // 获取Spliterator的方法 + return new LBQSpliterator(this); // 返回新的Spliterator实例 + } + + /** + * 将此队列保存到流中(即序列化它)。 + * + * @param s 流 + * @throws java.io.IOException 如果发生I/O错误 + * @serialData 容量被发出(int),后跟所有 + * 其元素(每个都是{@code Object})按正确顺序, + * 后跟null + */ + private void writeObject(java.io.ObjectOutputStream s) // 序列化方法 + throws java.io.IOException { + + fullyLock(); // 完全锁定 + try { + // 写出任何隐藏的内容,加上容量 + s.defaultWriteObject(); // 默认序列化 + + // 按正确顺序写出所有元素。 + for (Node p = head.next; p != null; p = p.next) // 遍历链表 + s.writeObject(p.item); // 写出元素 + + // 使用尾随null作为哨兵 + s.writeObject(null); // 写出null + } finally { + fullyUnlock(); // 解锁 + } + } + + /** + * 从流中重建此队列(即反序列化它)。 + * @param s 流 + * @throws ClassNotFoundException 如果找不到序列化对象的类 + * @throws java.io.IOException 如果发生I/O错误 + */ + private void readObject(java.io.ObjectInputStream s) // 反序列化方法 + throws java.io.IOException, ClassNotFoundException { + // 读取容量和任何隐藏的内容 + s.defaultReadObject(); // 默认反序列化 + + count.set(0); // 初始化计数 + last = head = new Node(null); // 初始化头部和尾部节点 + + // 读取所有元素并放入队列 + for (;;) { + @SuppressWarnings("unchecked") + E item = (E)s.readObject(); // 读取元素 + if (item == null) // 如果读取到null + break; // 退出循环 + add(item); // 将元素添加到队列 + } + } +} +``` + +## ReentrantLock + +```java +public class ReentrantLock implements Lock, java.io.Serializable { // 定义一个可重入锁类,实现Lock接口和可序列化接口 + + /** + ... + + * 这相当于使用{@code ReentrantLock(false)}。 + */ + public ReentrantLock() { // 默认构造函数 + sync = new NonfairSync(); // 初始化为非公平锁 + } + + /** + * 创建一个具有给定公平性策略的{@code ReentrantLock}实例。 + * + * @param fair {@code true}如果此锁应使用公平的排序策略 + */ + public ReentrantLock(boolean fair) { // 带公平性参数的构造函数 + sync = fair ? new FairSync() : new NonfairSync(); // 根据参数选择公平或非公平锁 + } + + /** + * 获取锁。 + * + *

如果锁未被其他线程持有,则获取锁并立即返回, + * 将锁持有计数设置为1。 + * + *

如果当前线程已经持有锁,则持有计数 + * 增加1,方法立即返回。 + * + *

如果锁被其他线程持有,则当前线程 + * 在调度目的上被禁用,处于休眠状态,直到锁被获取, + * 此时锁持有计数设置为1。 + */ + public void lock() { // 获取锁的方法 + sync.lock(); // 调用同步对象的锁定方法 + } + + /** + * 获取锁,除非当前线程被 + * {@linkplain Thread#interrupt 中断}。 + * + *

如果锁未被其他线程持有,则获取锁并立即返回, + * 将锁持有计数设置为1。 + * + *

如果当前线程已经持有此锁,则持有计数 + * 增加1,方法立即返回。 + * + *

如果锁被其他线程持有,则当前线程 + * 在调度目的上被禁用,处于休眠状态,直到发生以下两种情况之一: + * + *

    + * + *
  • 当前线程获取锁;或 + * + *
  • 其他线程{@linkplain Thread#interrupt 中断} + * 当前线程。 + * + *
+ * + *

如果当前线程获取锁,则锁持有 + * 计数设置为1。 + * + *

如果当前线程: + * + *

    + * + *
  • 在进入此方法时设置了中断状态;或 + * + *
  • 在获取锁时被{@linkplain Thread#interrupt 中断}, + * + *
+ * 则抛出{@link InterruptedException},并清除当前线程的 + * 中断状态。 + * + *

在此实现中,由于此方法是一个显式 + * 中断点,因此优先响应中断,而不是正常或重入获取锁。 + * + * @throws InterruptedException 如果当前线程被中断 + */ + public void lockInterruptibly() throws InterruptedException { // 可中断的获取锁方法 + sync.acquireInterruptibly(1); // 调用同步对象的可中断获取方法 + } + + /** + * 仅在锁未被其他线程持有时获取锁。 + * + *

如果锁未被其他线程持有,则获取锁并 + * 立即返回,值为{@code true},将锁持有计数设置为1。 + * 即使此锁已设置为使用公平排序策略,调用{@code tryLock()} + * 立即获取锁(如果可用),无论其他线程是否正在等待锁。 + * 这种“插队”行为在某些情况下可能有用, + * 尽管它打破了公平性。如果您希望尊重 + * 此锁的公平性设置,请使用 + * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) } + * 这几乎是等效的(它也检测中断)。 + * + *

如果当前线程已经持有此锁,则持有 + * 计数增加1,方法返回{@code true}。 + * + *

如果锁被其他线程持有,则此方法将返回 + * {@code false}。 + */ + public boolean tryLock() { // 尝试获取锁的方法 + return sync.nonfairTryAcquire(1); // 调用非公平尝试获取方法 + } + + /** + * 如果在给定的等待时间内锁未被其他线程持有, + * 则获取锁,且当前线程未被 + * {@linkplain Thread#interrupt 中断}。 + * + *

如果锁未被其他线程持有,则获取锁并立即返回 + * 值为{@code true},将锁持有计数设置为1。如果此锁已设置为使用公平 + * 排序策略,则如果有其他线程在等待锁,则不会获取可用锁。 + * 这与{@link #tryLock()}方法形成对比。如果您希望在公平锁上允许 + * 插队的定时{@code tryLock},则将定时和非定时形式结合在一起: + * + *

 {@code
+      * if (lock.tryLock() ||
+      *     lock.tryLock(timeout, unit)) {
+      *   ...
+      * }}
+ * + *

如果当前线程 + * 已经持有此锁,则持有计数增加1,方法返回{@code true}。 + * + *

如果锁被其他线程持有,则当前线程 + * 在调度目的上被禁用,处于休眠状态,直到发生以下三种情况之一: + * + *

    + * + *
  • 当前线程获取锁;或 + * + *
  • 其他线程{@linkplain Thread#interrupt 中断} + * 当前线程;或 + * + *
  • 指定的等待时间到期 + * + *
+ * + *

如果获取锁,则返回值{@code true},并将 + * 锁持有计数设置为1。 + * + *

如果当前线程: + * + *

    + * + *
  • 在进入此方法时设置了中断状态;或 + * + *
  • 在获取锁时被{@linkplain Thread#interrupt 中断}, + * + *
+ * 则抛出{@link InterruptedException},并清除当前线程的 + * 中断状态。 + * + *

如果指定的等待时间到期,则返回值{@code false}。 + * 如果时间小于或等于零,则方法将不会等待。 + * + *

在此实现中,由于此方法是一个显式 + * 中断点,因此优先响应中断,而不是正常或重入获取锁, + * 以及报告等待时间的到期。 + * + * @param timeout 等待锁的时间 + * @param unit 超时参数的时间单位 + * @return {@code true}如果锁是空闲的并被当前线程获取, + * 或锁已被当前线程持有;{@code false}如果在获取锁之前 + * 等待时间到期 + * @throws InterruptedException 如果当前线程被中断 + * @throws NullPointerException 如果时间单位为null + */ + public boolean tryLock(long timeout, TimeUnit unit) // 尝试获取锁的方法,带超时参数 + throws InterruptedException { + return sync.tryAcquireNanos(1, unit.toNanos(timeout)); // 调用同步对象的尝试获取方法 + } + + /** + * 尝试释放此锁。 + * + *

如果当前线程是此锁的持有者,则持有 + * 计数减少。如果持有计数现在为零,则释放锁。 + * 如果当前线程不是此锁的持有者,则抛出 + * {@link IllegalMonitorStateException}。 + * + + ... + public void unlock() { // 释放锁的方法 + + /** + * 尝试释放此锁。 + * + *

如果当前线程是此锁的持有者,则持有 + * 计数减少。如果持有计数现在为零,则释放锁。 + * 如果当前线程不是此锁的持有者,则抛出 + * {@link IllegalMonitorStateException}。 + * + * @throws IllegalMonitorStateException 如果当前线程不持有此锁 + */ + public void unlock() { // 释放锁的方法 + sync.release(1); // 调用同步对象的释放方法 + } + + /** + * 返回一个{@link Condition}实例,用于与此 + * {@link Lock}实例一起使用。 + * + *

返回的{@link Condition}实例支持与 + * 内置监视器锁一起使用的相同用法 + * ({@link Object#wait() wait},{@link Object#notify notify},和 + * {@link Object#notifyAll notifyAll})。 + * + *

    + * + *
  • 如果在调用任何{@link Condition} + * {@linkplain Condition#await() 等待}或{@linkplain + * Condition#signal 信号}方法时未持有此锁,则抛出 + * {@link IllegalMonitorStateException}。 + * + *
  • 当调用条件{@linkplain Condition#await() 等待} + * 方法时,锁被释放,并且在返回之前, + * 锁被重新获取,持有计数恢复到调用方法时的值。 + * + *
  • 如果线程在等待时被{@linkplain Thread#interrupt 中断}, + * 则等待将终止,抛出{@link InterruptedException},并清除线程的 + * 中断状态。 + * + *
  • 等待线程按FIFO顺序被信号通知。 + * + *
  • 从等待方法返回的线程的锁重新获取顺序与 + * 最初获取锁的线程相同,默认情况下未指定,但对于 + * 公平锁,优先考虑等待时间最长的线程。 + * + *
+ * + * @return Condition对象 + */ + public Condition newCondition() { // 创建新的条件对象的方法 + return sync.newCondition(); // 调用同步对象的方法 + } + + /** + * 查询当前线程对此锁的持有次数。 + * + *

线程对锁的持有次数是每个未匹配的 + * 解锁操作的锁操作。 + * + *

持有计数信息通常仅用于测试和 + * 调试目的。例如,如果某段代码不应在 + * 已持有锁的情况下进入,则可以断言这一事实: + * + *

 {@code
+     * class X {
+     *   ReentrantLock lock = new ReentrantLock();
+     *   // ...
+     *   public void m() {
+     *     assert lock.getHoldCount() == 0; // 断言持有计数为0
+     *     lock.lock(); // 获取锁
+     *     try {
+     *       // ... 方法体
+     *     } finally {
+     *       lock.unlock(); // 释放锁
+     *     }
+     *   }
+     * }}
+ * + * @return 当前线程对该锁的持有次数, + * 如果当前线程未持有此锁,则返回零 + */ + public int getHoldCount() { // 获取持有计数的方法 + return sync.getHoldCount(); // 调用同步对象的方法 + } + + /** + * 查询当前线程是否持有此锁。 + * + *

类似于内置监视器锁的{@link Thread#holdsLock(Object)}方法, + * 此方法通常用于调试和测试。例如,只有在持有锁的情况下 + * 调用的方法可以断言这一点: + * + *

 {@code
+     * class X {
+     *   ReentrantLock lock = new ReentrantLock();
+     *   // ...
+     *
+     *   public void m() {
+     *       assert lock.isHeldByCurrentThread(); // 断言当前线程持有锁
+     *       // ... 方法体
+     *   }
+     * }}
+ * + *

它还可以用于确保可重入锁以非可重入方式使用,例如: + * + *

 {@code
+     * class X {
+     *   ReentrantLock lock = new ReentrantLock();
+     *   // ...
+     *
+     *   public void m() {
+     *       assert !lock.isHeldByCurrentThread(); // 断言当前线程未持有锁
+     *       lock.lock(); // 获取锁
+     *       try {
+     *           // ... 方法体
+     *       } finally {
+     *           lock.unlock(); // 释放锁
+     *       }
+     *   }
+     * }}
+ * + * @return 如果当前线程持有此锁,则返回{@code true}, + * 否则返回{@code false} + */ + public boolean isHeldByCurrentThread() { // 检查当前线程是否持有锁的方法 + return sync.isHeldExclusively(); // 调用同步对象的方法 + } + + /** + * 查询是否有任何线程持有此锁。此方法 + * 旨在用于监控系统状态, + * 而不是用于同步控制。 + * + * @return 如果任何线程持有此锁,则返回{@code true}, + * 否则返回{@code false} + */ + public boolean isLocked() { // 检查锁是否被持有的方法 + return sync.isLocked(); // 调用同步对象的方法 + } + + /** + * 返回{@code true}如果此锁的公平性设置为true。 + * + * @return {@code true}如果此锁的公平性设置为true + */ + public final boolean isFair() { // 检查锁是否为公平锁的方法 + return sync instanceof FairSync; // 判断同步对象是否为公平同步 + } + + /** + * 返回当前拥有此锁的线程,或 + * {@code null}如果未被拥有。当此方法被 + * 非拥有者线程调用时,返回值反映了 + * 当前锁状态的最佳努力近似。例如, + * 即使有线程尝试获取锁,拥有者也可能会 + * 瞬时为{@code null}。 + * 此方法旨在促进构建提供 + * 更广泛锁监控功能的子类。 + * + * @return 拥有者,或{@code null}如果未被拥有 + */ + protected Thread getOwner() { // 获取锁的拥有者线程的方法 + return sync.getOwner(); // 调用同步对象的方法 + } + + /** + * 查询是否有任何线程在等待获取此锁。注意 + * 由于取消可能随时发生,返回{@code true} + * 并不保证任何其他线程将来会获取此锁。 + * 此方法主要用于监控系统状态。 + * + * @return 如果可能有其他线程在等待获取锁,则返回{@code true} + */ + public final boolean hasQueuedThreads() { // 检查是否有线程在等待获取锁的方法 + return sync.hasQueuedThreads(); // 调用同步对象的方法 + } + + /** + * 查询给定线程是否在等待获取此 + * 锁。注意由于取消可能随时发生,返回{@code true} + * 并不保证此线程将来会获取此锁。 + * 此方法主要用于监控系统状态。 + * + * @param thread 线程 + * @return 如果给定线程在等待获取此锁,则返回{@code true} + * @throws NullPointerException 如果线程为null + */ + public final boolean hasQueuedThread(Thread thread) { // 检查指定线程是否在等待获取锁的方法 + return sync.isQueued(thread); // 调用同步对象的方法 + } + + /** + * 返回等待获取此锁的线程数量的估计值。 + * 该值仅为估计,因为线程数量可能在此方法遍历 + * 内部数据结构时动态变化。此方法旨在用于 + * 监控系统状态,而不是用于同步控制。 + * + * @return 等待此锁的线程的估计数量 + */ + public final int getQueueLength() { // 获取等待获取锁的线程数量的方法 + return sync.getQueueLength(); // 调用同步对象的方法 + } + + /** + * 返回一个集合,包含可能在等待获取此锁的线程。 + * 由于实际线程集可能在构建此结果时动态变化, + * 返回的集合仅为最佳努力估计。返回集合的元素 + * 没有特定顺序。此方法旨在促进构建提供 + * 更广泛监控功能的子类。 + * + * @return 线程集合 + */ + protected Collection getQueuedThreads() { // 获取等待获取锁的线程集合的方法 + return sync.getQueuedThreads(); // 调用同步对象的方法 + } + + /** + * 查询是否有任何线程在等待与此锁关联的给定条件。 + * 注意,由于超时和中断可能随时发生,返回{@code true} + * 并不保证将来{@code signal}会唤醒任何线程。 + * 此方法主要用于监控系统状态。 + * + * @param condition 条件 + * @return 如果有任何等待线程,则返回{@code true} + * @throws IllegalMonitorStateException 如果此锁未被持有 + * @throws IllegalArgumentException 如果给定条件未与此锁关联 + * @throws NullPointerException 如果条件为null + */ + public boolean hasWaiters(Condition condition) { // 检查是否有线程在等待给定条件的方法 + if (condition == null) // 检查条件是否为null + throw new NullPointerException(); // 抛出空指针异常 + if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) // 检查条件是否为同步条件对象 + throw new IllegalArgumentException("not owner"); // 抛出非法参数异常 + return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition); // 调用同步对象的方法 + } + + /** + * 返回等待与此锁关联的给定条件的线程数量的估计值。 + * 注意,由于超时和中断可能随时发生,估计值 + * 仅作为实际等待者数量的上限。 + * 此方法旨在用于监控系统状态,而不是用于同步控制。 + * + * @param condition 条件 + * @return 等待线程的估计数量 + * @throws IllegalMonitorStateException 如果此锁未被持有 + * @throws IllegalArgumentException 如果给定条件未与此锁关联 + * @throws NullPointerException 如果条件为null + */ + public int getWaitQueueLength(Condition condition) { // 获取等待给定条件的线程数量的方法 + if (condition == null) // 检查条件是否为null + throw new NullPointerException(); // 抛出空指针异常 + if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) // 检查条件是否为同步条件对象 + throw new IllegalArgumentException("not owner"); // 抛出非法参数异常 + return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition); // 调用同步对象的方法 + } + + /** + * 返回一个集合,包含可能在等待与此锁关联的给定条件的线程。 + * 由于实际线程集可能在构建此结果时动态变化, + * 返回的集合仅为最佳努力估计。返回集合的元素 + * 没有特定顺序。此方法旨在促进构建提供 + * 更广泛条件监控功能的子类。 + * + * @param condition 条件 + * @return 线程集合 + * @throws IllegalMonitorStateException 如果此锁未被持有 + * @throws IllegalArgumentException 如果给定条件未与此锁关联 + * @throws NullPointerException 如果条件为null + */ + protected Collection getWaitingThreads(Condition condition) { // 获取等待给定条件的线程集合的方法 + if (condition == null) // 检查条件是否为null + throw new NullPointerException(); // 抛出空指针异常 + if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) // 检查条件是否为同步条件对象 + throw new IllegalArgumentException("not owner"); // 抛出非法参数异常 + return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition); // 调用同步对象的方法 + } + + /** + * 返回一个字符串,标识此锁及其锁状态。 + * 状态在括号中,包括字符串{@code "Unlocked"} + * 或字符串{@code "Locked by"},后跟 + * {@linkplain Thread#getName name}的拥有线程。 + * + * @return 标识此锁及其锁状态的字符串 + */ + public String toString() { // 返回锁的字符串表示的方法 + Thread o = sync.getOwner(); // 获取锁的拥有者线程 + return super.toString() + ((o == null) ? // 返回锁的状态 + "[Unlocked]" : + "[Locked by thread " + o.getName() + "]"); // 返回锁被哪个线程持有 + } +} +``` + +## Lock + +```java +public interface Lock { // 定义一个锁接口 + + /** + * 获取锁。 + * + *

如果锁不可用,则当前线程将被禁用 + * 以进行线程调度,并在锁被获取之前处于休眠状态。 + * + *

实现考虑 + * + *

一个{@code Lock}实现可能能够检测到锁的错误使用, + * 例如会导致死锁的调用,并可能在这种情况下抛出 + * (未检查的)异常。该{@code Lock}实现必须记录 + * 这些情况和异常类型。 + */ + void lock(); // 获取锁的方法 + + /** + * 获取锁,除非当前线程被 + * {@linkplain Thread#interrupt 中断}。 + * + *

如果锁可用,则立即获取锁并返回。 + * + *

如果锁不可用,则当前线程将被禁用 + * 以进行线程调度,并在以下两种情况之一发生之前处于休眠状态: + * + *

    + *
  • 当前线程获取锁;或 + *
  • 其他线程{@linkplain Thread#interrupt 中断}当前线程, + * 并且支持锁获取的中断。 + *
+ * + *

如果当前线程: + *

    + *
  • 在进入此方法时设置了中断状态;或 + *
  • 在获取锁时被{@linkplain Thread#interrupt 中断}, + * 并且支持锁获取的中断, + *
+ * 则抛出{@link InterruptedException},并清除当前线程的 + * 中断状态。 + * + *

实现考虑 + * + *

在某些实现中,可能无法中断锁获取, + * 如果可能,可能是一个昂贵的操作。 + * 程序员应意识到可能存在这种情况。实现应记录 + * 何时存在这种情况。 + * + *

实现可以优先响应中断而不是正常方法返回。 + * + *

一个{@code Lock}实现可能能够检测到锁的错误使用, + * 例如会导致死锁的调用,并可能在这种情况下抛出 + * (未检查的)异常。该{@code Lock}实现必须记录 + * 这些情况和异常类型。 + * + * @throws InterruptedException 如果当前线程在获取锁时被 + * 中断(并且支持锁获取的中断) + */ + void lockInterruptibly() throws InterruptedException; // 可中断的获取锁的方法 + + /** + * 仅在调用时锁是空闲的情况下获取锁。 + * + *

如果锁可用,则获取锁并立即返回 + * 值为{@code true}。 + * 如果锁不可用,则此方法将立即返回 + * 值为{@code false}。 + * + *

此方法的典型用法习惯是: + *

 {@code
+     * Lock lock = ...;
+     * if (lock.tryLock()) {
+     *   try {
+     *     // 操作受保护的状态
+     *   } finally {
+     *     lock.unlock(); // 释放锁
+     *   }
+     * } else {
+     *   // 执行替代操作
+     * }}
+ * + * 此用法确保如果获取了锁,则锁会被解锁, + * 并且如果未获取锁,则不会尝试解锁。 + * + * @return {@code true} 如果锁被获取, + * {@code false} 否则 + */ + boolean tryLock(); // 尝试获取锁的方法 + + /** + * 如果在给定的等待时间内锁是空闲的,并且当前线程 + * 未被{@linkplain Thread#interrupt 中断},则获取锁。 + * + *

如果锁可用,则此方法立即返回 + * 值为{@code true}。 + * 如果锁不可用,则当前线程将被禁用 + * 以进行线程调度,并在以下三种情况之一发生之前处于休眠状态: + *

    + *
  • 当前线程获取锁;或 + *
  • 其他线程{@linkplain Thread#interrupt 中断}当前线程, + * 并且支持锁获取的中断;或 + *
  • 指定的等待时间到期 + *
+ * + *

如果获取锁,则返回值{@code true}。 + * + *

如果当前线程: + *

    + *
  • 在进入此方法时设置了中断状态;或 + *
  • 在获取锁时被{@linkplain Thread#interrupt 中断}, + * 并且支持锁获取的中断, + *
+ * 则抛出{@link InterruptedException},并清除当前线程的 + * 中断状态。 + * + *

如果指定的等待时间到期,则返回值{@code false}。 + * 如果时间小于或等于零,则方法将不会等待。 + * + *

实现考虑 + * + *

在某些实现中,可能无法中断锁获取, + * 如果可能,可能是一个昂贵的操作。 + * 程序员应意识到可能存在这种情况。实现应记录 + * 何时存在这种情况。 + * + *

实现可以优先响应中断而不是正常 + * 方法返回,或报告超时。 + * + *

一个{@code Lock}实现可能能够检测到锁的错误使用, + * 例如会导致死锁的调用,并可能在这种情况下抛出 + * (未检查的)异常。该{@code Lock}实现必须记录 + * 这些情况和异常类型。 + * + * @param time 等待锁的最大时间 + * @param unit {@code time}参数的时间单位 + * @return {@code true} 如果锁被获取,{@code false} + * 如果在获取锁之前等待时间到期 + * + * @throws InterruptedException 如果当前线程在获取锁时被中断 + * (并且支持锁获取的中断) + */ + boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 尝试获取锁的方法,带超时参数 + + /** + * 释放锁。 + * + *

实现考虑 + * + *

一个{@code Lock}实现通常会对哪个线程可以释放锁施加 + * 限制(通常只有锁的持有者可以释放它),并且如果违反限制, + * 可能会抛出(未检查的)异常。 + * 任何限制和异常类型必须由该{@code Lock}实现记录。 + */ + void unlock(); // 释放锁的方法 + + /** + * 返回一个新的{@link Condition}实例,该实例绑定到此 + * {@code Lock}实例。 + * + *

在等待条件之前,当前线程必须持有锁。 + * 调用{@link Condition#await()}将原子释放锁 + * 然后等待,并在等待返回之前重新获取锁。 + * + *

实现考虑 + * + *

{@link Condition}实例的确切操作取决于 + * {@code Lock}实现,必须由该实现记录。 + * + * @return 绑定到此{@code Lock}实例的新{@link Condition}实例 + * @throws UnsupportedOperationException 如果此{@code Lock} + * 实现不支持条件 + */ + Condition newCondition(); // 创建新的条件对象的方法 +} +``` + +## ExecutorService + +```java +//ExecutorService// 定义一个ExecutorService接口,继承自Executor,是 Executor的扩展,中文 翻译为执行器服务 + +public interface ExecutorService extends Executor { // 定义一个ExecutorService接口,继承自Executor + + /** + * 启动有序关闭,其中之前提交的任务将被执行, + * 但不接受新任务。 + * 如果已经关闭,则调用没有额外效果。 + * + *

此方法不会等待之前提交的任务完成执行。 + * 使用{@link #awaitTermination awaitTermination}来做到这一点。 + * + * @throws SecurityException 如果存在安全管理器,并且 + * 关闭此ExecutorService可能会操作 + * 调用者不允许修改的线程,因为它不持有 + * {@link java.lang.RuntimePermission}{@code ("modifyThread")}, + * 或安全管理器的{@code checkAccess}方法拒绝访问。 + */ + void shutdown(); // 关闭ExecutorService的方法 + + /** + * 尝试停止所有正在执行的任务,停止等待任务的处理, + * 并返回等待执行的任务列表。 + * + *

此方法不会等待正在执行的任务终止。 + * 使用{@link #awaitTermination awaitTermination}来做到这一点。 + * + *

没有保证会停止正在执行的任务的处理,只有尽力而为。 + * 例如,典型的实现将通过{@link Thread#interrupt}取消, + * 因此任何未能响应中断的任务可能永远不会终止。 + * + * @return 从未开始执行的任务列表 + * @throws SecurityException 如果存在安全管理器,并且 + * 关闭此ExecutorService可能会操作 + * 调用者不允许修改的线程,因为它不持有 + * {@link java.lang.RuntimePermission}{@code ("modifyThread")}, + * 或安全管理器的{@code checkAccess}方法拒绝访问。 + */ + List shutdownNow(); // 立即关闭ExecutorService并返回未执行的任务列表的方法 + + /** + * 如果此执行器已关闭,则返回{@code true}。 + * + * @return {@code true} 如果此执行器已关闭 + */ + boolean isShutdown(); // 检查ExecutorService是否已关闭的方法 + + /** + * 如果所有任务在关闭后已完成,则返回{@code true}。 + * 注意,{@code isTerminated}在调用{@code shutdown}或 + * {@code shutdownNow}之前永远不会为{@code true}。 + * + * @return {@code true} 如果所有任务在关闭后已完成 + */ + boolean isTerminated(); // 检查ExecutorService是否已终止的方法 + + /** + * 阻塞直到所有任务在关闭请求后完成执行, + * 或超时发生,或当前线程被中断,以先发生者为准。 + * + * @param timeout 等待的最大时间 + * @param unit 超时参数的时间单位 + * @return {@code true} 如果此执行器终止,{@code false} + * 如果在终止之前超时 + * @throws InterruptedException 如果在等待时被中断 + */ + boolean awaitTermination(long timeout, TimeUnit unit) // 等待ExecutorService终止的方法 + throws InterruptedException; + + /** + * 提交一个返回值的任务进行执行,并返回一个 + * 表示任务待处理结果的Future。Future的{@code get}方法 + * 将在成功完成时返回任务的结果。 + * + *

+ * 如果您希望立即阻塞等待任务,可以使用 + * {@code result = exec.submit(aCallable).get();}的构造。 + * + *

注意:{@link Executors}类包括一组方法 + * 可以将一些其他常见的闭包对象,例如 + * {@link java.security.PrivilegedAction}转换为 + * {@link Callable}形式,以便可以提交。 + * + * @param task 要提交的任务 + * @param 任务结果的类型 + * @return 表示任务待处理完成的Future + * @throws RejectedExecutionException 如果任务无法 + * 被调度执行 + * @throws NullPointerException 如果任务为null + */ + Future submit(Callable task); // 提交Callable任务的方法 + + /** + * 提交一个Runnable任务进行执行,并返回一个Future + * 表示该任务。Future的{@code get}方法将在成功完成时 + * 返回给定的结果。 + * + * @param task 要提交的任务 + * @param result 要返回的结果 + * @param 结果的类型 + * @return 表示任务待处理完成的Future + * @throws RejectedExecutionException 如果任务无法 + * 被调度执行 + * @throws NullPointerException 如果任务为null + */ + Future submit(Runnable task, T result); // 提交Runnable任务并返回结果的方法 + + /** + * 提交一个Runnable任务进行执行,并返回一个Future + * 表示该任务。Future的{@code get}方法将在成功完成时 + * 返回{@code null}。 + * + * @param task 要提交的任务 + * @return 表示任务待处理完成的Future + * @throws RejectedExecutionException 如果任务无法 + * 被调度执行 + * @throws NullPointerException 如果任务为null + */ + Future submit(Runnable task); // 提交Runnable任务的方法 + + /** + * 执行给定的任务,返回一个Future列表,持有 + * 它们的状态和结果,当所有任务完成时。 + * {@link Future#isDone}对于返回表列的每个元素都是{@code true}。 + * 注意,一个完成的任务可能正常终止或抛出异常。 + * 如果在此操作进行时修改给定的集合,则此方法的结果是未定义的。 + * + * @param tasks 任务集合 + * @param 从任务返回的值的类型 + * @return 表示任务的Future列表,顺序与给定任务列表的迭代器生成的顺序相同, + * 每个任务都已完成 + * @throws InterruptedException 如果在等待时被中断,在这种情况下未完成的任务将被取消 + * @throws NullPointerException 如果任务或其任何元素为{@code null} + * @throws RejectedExecutionException 如果任何任务无法被调度执行 + */ + List> invokeAll(Collection> tasks) // 执行所有任务并返回结果的方法 + throws InterruptedException; + + /** + * 执行给定的任务,返回一个Future列表,持有 + * 它们的状态和结果,当所有任务完成或超时到期时, + * 以先发生者为准。 + * {@link Future#isDone}对于返回列表的每个元素都是{@code true}。 + * 返回时,未完成的任务将被取消。 + * 注意,一个完成的任务可能正常终止或抛出异常。 + * 如果在此操作进行时修改给定的集合,则此方法的结果是未定义的。 + * + * @param tasks 任务集合 + * @param timeout 等待的最大时间 + * @param unit 超时参数的时间单位 + * @param 从任务返回的值的类型 + * @return 表示任务的Future列表,顺序与给定任务列表的迭代器生成的顺序相同。 + * 如果操作没有超时,则每个任务都将完成。 + * 如果超时,则这些任务中的某些任务将未完成。 + * @throws InterruptedException 如果在等待时被中断,在这种情况下未完成的任务将被取消 + * @throws NullPointerException 如果任务、单位或任何元素任务为{@code null} + * @throws TimeoutException 如果在任何任务成功完成之前超时到期 + * @throws ExecutionException 如果没有任务成功完成 + * @throws RejectedExecutionException 如果任务无法被调度执行 + */ + T invokeAny(Collection> tasks) // 执行任务并返回成功结果的方法 + throws InterruptedException, ExecutionException; + + /** + * 执行给定的任务,返回一个成功完成的任务的结果 + * (即没有抛出异常),如果有的话。在正常或异常返回时, + * 未完成的任务将被取消。 + * 如果在此操作进行时修改给定的集合,则此方法的结果是未定义的。 + * + * @param tasks 任务集合 + * @param 从任务返回的值的类型 + * @return 由一个任务返回的结果 + * @throws InterruptedException 如果在等待时被中断 + * @throws NullPointerException 如果任务或任何元素任务 + * 为{@code null} + * @throws IllegalArgumentException 如果任务为空 + * @throws ExecutionException 如果没有任务成功完成 + * @throws RejectedExecutionException 如果任务无法被调度执行 + */ + T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) // 执行任务并返回成功结果的方法,带超时参数 + throws InterruptedException, ExecutionException, TimeoutException; +} +``` + +```java + static BlockingQueue blockingQueue = new LinkedBlockingQueue<>(100); // 创建一个容量为100的阻塞队列,超过100个等待任务时判定为异常,允许抛出 + // 创建一个固定大小的线程池,线程池的核心线程数为当前可用处理器的数量,最大线程数为当前可用处理器的数量乘以4,线程空闲时间超过10秒则被回收 + static ExecutorService fixedThreadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), // 核心线程数 + Runtime.getRuntime().availableProcessors() * 4, // 最大线程数 + 10, // 线程空闲时间 + TimeUnit.SECONDS, // 时间单位为秒 + blockingQueue, // 使用上面创建的阻塞队列作为任务队列 + new ThreadFactory() { // 自定义线程工厂 + // 定义一个原子整数,用于生成线程的编号 + private final AtomicInteger threadNumber = new AtomicInteger(1); + // 定义线程的名称前缀 + private final String namePrefix = ""; // 线程名称前缀 + // 重写newThread方法,用于创建线程 + @Override + public Thread newThread(Runnable r) { + // 创建线程,并设置线程的名称为前缀加上线程编号 + Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); // 设置线程名称 + return t; // 返回创建的线程 + } + }); +``` + + + + + + + + + + + + + + + + diff --git a/docs/plugin-demo.md b/docs/plugin-demo.md new file mode 100644 index 000000000..b96383be9 --- /dev/null +++ b/docs/plugin-demo.md @@ -0,0 +1,165 @@ +--- +title: VuePress插件功能演示 +date: 2024-01-22 +categories: + - 文档指南 +tags: + - VuePress + - 插件 + - 演示 +--- + +# VuePress插件功能演示 + +本页面展示了项目中新增的各种VuePress插件功能。 + +## 代码演示容器 (demo-block) + +使用 `vuepress-plugin-demo-block` 插件,可以直接在文档中运行代码示例: + +::: demo 基础按钮示例 +```html + + + + + +``` +::: + +## 流程图支持 (flowchart) + +使用 `vuepress-plugin-flowchart` 插件,可以用简单的文本语法绘制流程图: + +```flowchart +st=>start: 开始 +op1=>operation: 用户访问网站 +cond=>condition: 是否已登录? +op2=>operation: 显示登录页面 +op3=>operation: 显示主页内容 +e=>end: 结束 + +st->op1->cond +cond(yes)->op3->e +cond(no)->op2->e +``` + +## PWA功能演示 + +项目已启用PWA支持,具有以下特性: + +- **离线访问**: 网站内容可以离线访问 +- **桌面图标**: 可以添加到桌面作为应用 +- **更新提醒**: 有新内容时会提示用户刷新 +- **快速加载**: 利用Service Worker缓存提升加载速度 + +### PWA安装指南 + +1. **Chrome浏览器**: 地址栏右侧会出现安装图标 +2. **移动设备**: 浏览器菜单中选择"添加到主屏幕" +3. **Edge浏览器**: 地址栏右侧的应用图标 + +## 代码高亮与复制 + +代码块支持语法高亮和一键复制功能: + +```javascript +// JavaScript 示例 +function fibonacci(n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +console.log(fibonacci(10)); // 输出: 55 +``` + +```java +// Java 示例 +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} +``` + +```python +# Python 示例 +def quick_sort(arr): + if len(arr) <= 1: + return arr + pivot = arr[len(arr) // 2] + left = [x for x in arr if x < pivot] + middle = [x for x in arr if x == pivot] + right = [x for x in arr if x > pivot] + return quick_sort(left) + middle + quick_sort(right) + +print(quick_sort([3,6,8,10,1,2,1])) +``` + +## 进度条与加载动画 + +- **页面加载进度条**: 页面顶部的绿色进度条 +- **页面切换动画**: 平滑的页面过渡效果 +- **加载页面动画**: 首次访问时的加载动画 + +## 图片缩放功能 + +点击图片可以放大查看 + +## 返回顶部 + +页面右下角的返回顶部按钮,滚动页面时会自动显示。 + +## 看板娘 + +页面右下角的可爱看板娘会陪伴您的阅读过程。 + +## 动态标题 + +当您切换到其他标签页时,浏览器标题会发生变化,切换回来时会显示欢迎信息。 + +## 使用建议 + +1. **代码演示**: 适合展示Vue组件、HTML/CSS效果 +2. **流程图**: 适合说明业务流程、算法逻辑 +3. **PWA功能**: 提升用户体验,支持离线访问 +4. **代码高亮**: 提高代码可读性 + +## 注意事项 + +- 代码演示容器中的代码会实际运行,请确保代码安全 +- 流程图语法需要遵循flowchart.js的规范 +- PWA功能需要HTTPS环境才能完全生效 +- 部分插件可能需要额外的配置才能达到最佳效果 + +--- + +通过这些插件,我们的VuePress文档站点变得更加强大和用户友好! \ No newline at end of file diff --git a/docs/products/README.md b/docs/products/README.md new file mode 100644 index 000000000..d06425094 --- /dev/null +++ b/docs/products/README.md @@ -0,0 +1,37 @@ +# 产品系列文档 + +## 概述 + +本节包含了我们的产品系列相关文档,提供了产品的详细介绍、技术规格、使用指南和最佳实践。这些文档旨在帮助用户全面了解产品功能,快速上手使用,并在实际应用中获得最佳体验。 + +## 产品架构 + +下图展示了我们产品系列的整体架构和关系: + +![产品系列架构图](./product-architecture.svg) + +## 文档结构 + +产品系列文档按照以下结构组织: + +- **产品概述**:介绍产品的核心功能、适用场景和主要特点 +- **技术规格**:详细的技术参数、系统要求和兼容性信息 +- **快速入门**:帮助新用户快速上手的指南和教程 +- **用户手册**:全面的功能使用说明和操作指南 +- **API参考**:面向开发者的API文档和集成指南 +- **最佳实践**:优化使用体验和解决常见问题的建议 +- **常见问题**:用户常见问题的解答和故障排除指南 + +## 产品系列 + +我们的产品系列包括: + +- [企业级应用平台](./enterprise-platform.md):面向大型企业的综合业务应用平台 +- [微服务框架](./microservice-framework.md):轻量级、高性能的微服务开发框架 +- [数据分析套件](./data-analytics-suite.md):强大的数据处理和分析工具集 + +## 文档更新 + +产品文档将定期更新,以反映产品的最新功能和改进。请定期查看以获取最新信息。 + +我们的技术支持团队将竭诚为您提供帮助。 diff --git a/docs/products/changeChange.md b/docs/products/changeChange.md new file mode 100644 index 000000000..1c1c7d433 --- /dev/null +++ b/docs/products/changeChange.md @@ -0,0 +1,45 @@ +--- +title: 速绿充电小程序体验设计 +author: 哪吒 +date: '2023-06-15' +--- + +# 速绿充电小程序体验设计 + +![img_50.png](./img_50.png) + +![img_51.png](./img_51.png) + +![img_52.png](./img_52.png) + +![img_53.png](./img_53.png) + +![img_54.png](./img_54.png) + +![img_55.png](./img_55.png) + +![img_56.png](./img_56.png) + +![img_57.png](./img_57.png) + +![img_58.png](./img_58.png) + +![img_59.png](./img_59.png) + +![img_60.png](./img_60.png) + +![img_61.png](./img_61.png) + +![img_62.png](./img_62.png) + +![img_63.png](./img_63.png) + +![img_64.png](./img_64.png) + +![img_65.png](./img_65.png) + +![img_66.png](./img_66.png) + +![img_67.png](./img_67.png) + +![img_68.png](./img_68.png) diff --git a/docs/products/changeChange2.md b/docs/products/changeChange2.md new file mode 100644 index 000000000..2b0f32b0a --- /dev/null +++ b/docs/products/changeChange2.md @@ -0,0 +1,41 @@ +--- +title: 恒想充充电桩小程序改版设计 +author: 哪吒 +date: '2023-06-15' +--- + +# 恒想充充电桩小程序改版设计 + +![img_69.png](./img_69.png) + +![img_70.png](./img_70.png) + +![img_71.png](./img_71.png) + +![img_72.png](./img_72.png) + +![img_73.png](./img_73.png) + +![img_74.png](./img_74.png) + +![img_75.png](./img_75.png) + +![img_76.png](./img_76.png) + +![img_77.png](./img_77.png) + +![img_78.png](./img_78.png) + +![img_79.png](./img_79.png) + + +![img_80.png](./img_80.png) + +![img_81.png](./img_81.png) + +![img_82.png](./img_82.png) + +![img_83.png](./img_83.png) + +![img_84.png](./img_84.png) + diff --git a/docs/products/data-analytics-suite.md b/docs/products/data-analytics-suite.md new file mode 100644 index 000000000..682463afe --- /dev/null +++ b/docs/products/data-analytics-suite.md @@ -0,0 +1,290 @@ +# 数据分析套件 + +## 产品概述 + +数据分析套件是一个强大的企业级数据处理和分析平台,为企业提供从数据采集、存储、处理到可视化分析的全流程解决方案。该套件集成了先进的数据处理引擎、机器学习算法和可视化工具,帮助企业从海量数据中挖掘价值,支持数据驱动的业务决策。 + +### 核心优势 + +- **全流程覆盖**:从数据采集到分析可视化的完整数据处理链路 +- **高性能计算**:分布式计算架构,支持PB级数据的高效处理 +- **智能分析**:内置机器学习和统计分析算法,自动发现数据洞察 +- **灵活扩展**:模块化设计,支持按需扩展和定制 +- **自助分析**:直观的可视化界面,降低数据分析门槛 + +## 技术架构 + +数据分析套件采用分层模块化架构,主要由以下核心组件构成: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 数据应用层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬────────┤ +│ 数据可视化 │ 报表系统 │ 数据挖掘 │ 预测分析 │ 告警 │ +└─────────────┴─────────────┴─────────────┴─────────────┴────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 数据处理层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬────────┤ +│ 批处理引擎 │ 流处理引擎 │ 查询引擎 │ 机器学习 │ ETL │ +└─────────────┴─────────────┴─────────────┴─────────────┴────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 数据存储层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬────────┤ +│ 数据湖 │ 数据仓库 │ OLAP引擎 │ 时序数据库 │ 缓存 │ +└─────────────┴─────────────┴─────────────┴─────────────┴────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 数据采集层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬────────┤ +│ 数据集成 │ 日志采集 │ CDC │ API采集 │ IoT │ +└─────────────┴─────────────┴─────────────┴─────────────┴────────┘ +``` + +## 核心功能 + +### 1. 数据采集与集成 + +- **多源数据采集**:支持数据库、文件、API、日志、IoT设备等多种数据源 +- **实时数据同步**:基于CDC技术的低延迟数据捕获和同步 +- **数据质量控制**:数据采集过程中的校验、清洗和转换 +- **元数据管理**:自动采集和管理数据源元数据 +- **调度管理**:灵活的数据采集任务调度和监控 + +### 2. 数据存储与管理 + +- **数据湖**:支持结构化、半结构化和非结构化数据的统一存储 +- **数据仓库**:面向主题的集成数据存储和管理 +- **多模态存储**:针对不同数据类型的专用存储引擎 +- **数据生命周期**:自动化数据分层存储和归档 +- **数据安全**:细粒度的数据访问控制和加密 + +### 3. 数据处理与分析 + +- **批处理引擎**:高性能的分布式批量数据处理 +- **流处理引擎**:实时数据流的处理和分析 +- **SQL分析**:强大的SQL查询和分析能力 +- **机器学习**:内置常用机器学习算法和模型训练框架 +- **统计分析**:丰富的统计函数和分析方法 + +### 4. 数据可视化与应用 + +- **交互式仪表板**:拖拽式自定义仪表板创建 +- **多维分析**:支持数据的多维度切片和钻取 +- **地理空间分析**:地图可视化和地理空间数据分析 +- **报表系统**:定制化报表设计和自动生成 +- **数据应用开发**:低代码数据应用开发平台 + +## 技术规格 + +### 系统要求 + +**服务器端**: +- 操作系统:CentOS 7.x+/Ubuntu 18.04+/RHEL 7.x+ +- CPU:16核心及以上(推荐32核心以上) +- 内存:64GB及以上(推荐128GB以上) +- 存储:根据数据规模,建议SSD 1TB以上 +- 网络:万兆网络推荐 + +**客户端**: +- 浏览器:Chrome 80+/Firefox 70+/Edge 80+/Safari 13+ +- 分辨率:1920x1080及以上推荐 + +### 扩展能力 + +- 支持水平扩展,单集群可支持数百节点 +- 数据处理能力:每日TB级数据增量 +- 存储容量:支持PB级数据存储 +- 查询性能:复杂查询秒级响应(基于预计算和缓存) +- 并发用户:支持数百用户同时在线分析 + +## 快速入门 + +### 部署安装 + +1. **环境准备** + - 安装Docker和Docker Compose + - 准备Kubernetes集群(生产环境推荐) + - 配置存储和网络 + +2. **基础安装** + ```bash + # 下载安装包 + wget https://example.com/data-analytics-suite.tar.gz + + # 解压安装包 + tar -xzvf data-analytics-suite.tar.gz + + # 执行安装脚本 + cd data-analytics-suite + ./install.sh + ``` + +3. **配置系统** + - 访问管理控制台:http://your-server:8080 + - 使用默认账号登录:admin/Admin123 + - 完成初始化配置向导 + +### 创建第一个分析项目 + +1. **数据接入** + - 在管理控制台选择"数据源管理" + - 点击"添加数据源",选择数据源类型 + - 配置连接参数并测试连接 + - 选择要同步的表或数据对象 + - 配置同步策略(全量/增量) + +2. **数据处理** + - 创建数据处理工作流 + - 添加数据转换、清洗、聚合等节点 + - 配置数据质量规则 + - 保存并执行工作流 + +3. **数据可视化** + - 创建新的仪表板 + - 添加图表组件(柱状图、折线图、饼图等) + - 配置数据源和展示维度 + - 设置刷新频率和交互行为 + - 保存并分享仪表板 + +## 应用场景 + +### 1. 业务智能分析 + +**场景描述**:企业需要对销售、营销、客户等业务数据进行多维度分析,发现业务趋势和问题。 + +**解决方案**: +- 集成企业各业务系统数据到数据仓库 +- 构建业务主题数据模型 +- 创建销售、营销、客户等分析仪表板 +- 设置关键指标监控和异常告警 +- 生成定期业务分析报告 + +**价值体现**: +- 提供360度业务视图,支持决策 +- 及时发现业务异常和机会 +- 优化业务流程和资源分配 + +### 2. 用户行为分析 + +**场景描述**:电商/内容平台需要分析用户行为数据,优化产品和运营策略。 + +**解决方案**: +- 采集用户行为日志和交易数据 +- 构建用户画像和行为路径分析 +- 应用机器学习算法进行用户分群 +- 创建用户生命周期分析仪表板 +- 实施个性化推荐策略 + +**价值体现**: +- 深入理解用户需求和行为 +- 提升用户转化率和留存率 +- 优化产品功能和用户体验 + +### 3. 物联网数据分析 + +**场景描述**:制造企业需要分析生产设备的运行数据,实现预测性维护和生产优化。 + +**解决方案**: +- 采集设备传感器数据到时序数据库 +- 实时监控设备运行状态 +- 应用异常检测算法识别潜在故障 +- 建立设备健康评分模型 +- 创建生产效率分析仪表板 + +**价值体现**: +- 减少设备故障和停机时间 +- 延长设备使用寿命 +- 优化生产计划和资源利用 + +## 最佳实践 + +### 数据建模原则 + +- **业务驱动**:从业务需求出发设计数据模型 +- **维度建模**:采用星型或雪花模型组织分析数据 +- **粒度控制**:根据分析需求确定合适的数据粒度 +- **一致性**:保持维度和指标的命名和定义一致 +- **可扩展性**:预留模型扩展空间,适应业务变化 + +### 性能优化策略 + +- **数据分区**:按时间、地区等维度分区存储 +- **预计算**:对常用指标进行预聚合计算 +- **索引优化**:为常用查询条件创建合适索引 +- **查询优化**:优化SQL语句,避免全表扫描 +- **资源隔离**:分离计算和存储资源,避免相互影响 + +### 数据治理建议 + +- **数据标准**:建立统一的数据定义和标准 +- **数据质量**:实施全流程的数据质量控制 +- **数据安全**:实施数据分级和访问控制 +- **数据血缘**:跟踪数据流转和转换过程 +- **元数据管理**:集中管理技术和业务元数据 + +## 常见问题 + +### 1. 数据同步失败 + +**可能原因**: +- 源数据库连接问题 +- 权限不足 +- 数据格式不兼容 + +**解决方法**: +- 检查数据源连接配置 +- 确认同步账号权限 +- 调整数据类型映射 + +### 2. 查询性能慢 + +**可能原因**: +- 数据量过大 +- 查询语句不优化 +- 缺少必要索引 +- 资源不足 + +**解决方法**: +- 优化查询SQL +- 添加适当索引 +- 使用预计算和缓存 +- 增加计算资源 + +### 3. 可视化展示异常 + +**可能原因**: +- 数据异常或缺失 +- 图表配置不当 +- 浏览器兼容性问题 + +**解决方法**: +- 检查数据源和数据质量 +- 调整图表配置和比例 +- 使用推荐的浏览器版本 + +## 版本历史 + +### v4.2.0 (2023-08-15) + +- 新增AI驱动的数据洞察功能 +- 增强实时数据处理能力 +- 优化大规模数据集的查询性能 +- 新增30+预置数据可视化模板 + +### v4.0.0 (2023-02-20) + +- 架构升级,采用云原生设计 +- 新增数据湖存储和查询引擎 +- 增强机器学习和预测分析能力 +- 全新的用户界面和交互体验 + +### v3.5.0 (2022-07-10) + +- 新增地理空间分析功能 +- 增强数据安全和隐私保护 +- 优化数据集成和ETL性能 +- 新增移动端支持 + +我们提供专业的数据分析咨询和实施服务,帮助您充分发挥数据的价值。 diff --git a/docs/products/enterprise-platform.md b/docs/products/enterprise-platform.md new file mode 100644 index 000000000..dc957f1bf --- /dev/null +++ b/docs/products/enterprise-platform.md @@ -0,0 +1,219 @@ +# 企业级应用平台 + +## 产品概述 + +企业级应用平台是一套面向大型企业的综合业务应用解决方案,旨在帮助企业快速构建、部署和管理业务应用系统。平台采用现代化的技术架构,提供了丰富的功能组件和开发工具,能够显著提升企业的数字化转型效率和应用开发速度。 + +### 核心优势 + +- **高效开发**:低代码/无代码开发能力,大幅提升应用交付速度 +- **灵活扩展**:模块化架构设计,支持按需扩展和定制 +- **安全可靠**:企业级安全防护,多层次数据保护机制 +- **易于集成**:丰富的API和连接器,轻松对接现有系统 +- **全面监控**:实时性能监控和智能运维,保障系统稳定运行 + +## 技术架构 + +企业级应用平台采用微服务架构,主要由以下核心组件构成: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 统一门户与接入层 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 业务服务层 │ +├───────────────┬───────────────┬───────────────┬─────────────┤ +│ 流程管理服务 │ 表单设计服务 │ 报表分析服务 │ 消息通知服务 │ +└───────────────┴───────────────┴───────────────┴─────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 平台支撑层 │ +├───────────────┬───────────────┬───────────────┬─────────────┤ +│ 用户权限 │ 数据服务 │ 集成服务 │ 开发工具链 │ +└───────────────┴───────────────┴───────────────┴─────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 基础设施层 │ +├───────────────┬───────────────┬───────────────┬─────────────┤ +│ 容器编排 │ 数据存储 │ 消息队列 │ 监控告警 │ +└───────────────┴───────────────┴───────────────┴─────────────┘ +``` + +## 核心功能 + +### 1. 应用开发平台 + +- **可视化设计器**:拖拽式界面设计,所见即所得 +- **流程引擎**:灵活的业务流程定义和执行 +- **表单引擎**:强大的动态表单设计与数据收集 +- **报表工具**:丰富的数据可视化和分析能力 +- **集成开发环境**:专业开发者的高效开发工具 + +### 2. 数据管理平台 + +- **数据建模**:灵活的业务数据模型定义 +- **数据集成**:多源异构数据的采集和整合 +- **数据服务**:标准化的数据访问接口 +- **数据安全**:细粒度的数据权限控制 +- **数据质量**:数据校验和质量监控 + +### 3. 运行管理平台 + +- **应用部署**:一键部署和版本管理 +- **性能监控**:实时监控和性能分析 +- **日志管理**:集中式日志收集和分析 +- **告警机制**:多渠道的异常告警 +- **资源调度**:智能的资源分配和扩缩容 + +### 4. 安全管理平台 + +- **身份认证**:多因素认证和单点登录 +- **权限管理**:基于角色的访问控制 +- **安全审计**:全面的操作日志和审计 +- **数据加密**:敏感数据的传输和存储加密 +- **安全合规**:符合行业安全标准和法规 + +## 技术规格 + +### 系统要求 + +**服务器端**: +- 操作系统:CentOS 7.x+/Ubuntu 18.04+/Windows Server 2016+ +- CPU:8核心及以上 +- 内存:16GB及以上 +- 存储:SSD 100GB及以上 +- 数据库:MySQL 5.7+/PostgreSQL 10+/Oracle 12c+ + +**客户端**: +- 浏览器:Chrome 80+/Firefox 70+/Edge 80+/Safari 13+ +- 移动设备:iOS 11+/Android 8.0+ + +### 扩展能力 + +- 支持水平扩展,单集群可支持100+节点 +- 支持多集群部署和跨区域数据同步 +- 单系统可支持10000+并发用户 +- 支持PB级数据存储和处理 + +## 快速入门 + +### 安装部署 + +1. **环境准备** + - 安装Docker和Docker Compose + - 配置数据库和消息队列 + - 准备域名和SSL证书 + +2. **平台部署** + - 下载部署包并解压 + - 修改配置文件 + - 执行部署脚本 + +3. **初始化配置** + - 创建管理员账号 + - 导入基础数据 + - 配置系统参数 + +### 创建第一个应用 + +1. **设计数据模型** + - 定义实体和字段 + - 设置关联关系 + - 配置业务规则 + +2. **设计用户界面** + - 创建表单和列表 + - 设计仪表板 + - 配置导航菜单 + +3. **定义业务流程** + - 设计审批流程 + - 配置任务分配规则 + - 设置自动化操作 + +4. **发布和测试** + - 应用打包和发布 + - 功能测试和调优 + - 用户培训和上线 + +## 最佳实践 + +### 应用设计原则 + +- **模块化设计**:将应用拆分为功能独立的模块 +- **数据驱动**:基于数据模型驱动界面和流程 +- **渐进式开发**:先实现核心功能,再逐步扩展 +- **用户体验优先**:关注操作流程和响应速度 +- **安全性考虑**:在设计阶段就考虑安全因素 + +### 性能优化建议 + +- **数据库优化**:合理设计索引和分区 +- **缓存策略**:使用多级缓存减轻数据库压力 +- **前端优化**:减少HTTP请求和资源大小 +- **批量处理**:大数据量操作采用批量处理 +- **定时任务**:将耗时操作放在非高峰期执行 + +## 常见问题 + +### 1. 系统无法启动 + +**可能原因**: +- 配置文件参数错误 +- 依赖服务未启动 +- 端口被占用 + +**解决方法**: +- 检查配置文件中的数据库连接信息 +- 确保MySQL、Redis等依赖服务已启动 +- 使用`netstat`命令检查端口占用情况 + +### 2. 应用性能问题 + +**可能原因**: +- 数据库查询效率低 +- 缓存未正确配置 +- 服务器资源不足 + +**解决方法**: +- 优化SQL语句和索引 +- 检查缓存配置和命中率 +- 增加服务器资源或进行集群扩展 + +### 3. 用户权限问题 + +**可能原因**: +- 角色权限配置不正确 +- 数据权限规则冲突 +- 缓存未及时更新 + +**解决方法**: +- 检查用户角色和权限设置 +- 梳理数据权限规则,解决冲突 +- 清除权限缓存,强制刷新 + +## 版本历史 + +### v3.5.0 (2023-06-15) + +- 新增AI辅助开发功能 +- 优化流程引擎性能 +- 增强移动端适配能力 +- 新增30+预置组件 + +### v3.0.0 (2022-12-10) + +- 架构升级,采用微服务架构 +- 新增低代码开发平台 +- 支持多租户SaaS模式 +- 全面提升安全性和可靠性 + +### v2.5.0 (2022-05-20) + +- 新增数据分析模块 +- 优化用户界面和交互体验 +- 增强API管理功能 +- 提升系统整体性能 + +我们的专业技术团队将为您提供7x24小时的技术支持和服务。 diff --git a/docs/products/img.png b/docs/products/img.png new file mode 100644 index 000000000..f0866ef05 Binary files /dev/null and b/docs/products/img.png differ diff --git a/docs/products/img_1.png b/docs/products/img_1.png new file mode 100644 index 000000000..05b96f4f6 Binary files /dev/null and b/docs/products/img_1.png differ diff --git a/docs/products/img_10.png b/docs/products/img_10.png new file mode 100644 index 000000000..9fd1500c2 Binary files /dev/null and b/docs/products/img_10.png differ diff --git a/docs/products/img_11.png b/docs/products/img_11.png new file mode 100644 index 000000000..b50860c40 Binary files /dev/null and b/docs/products/img_11.png differ diff --git a/docs/products/img_12.png b/docs/products/img_12.png new file mode 100644 index 000000000..d4fc54e59 Binary files /dev/null and b/docs/products/img_12.png differ diff --git a/docs/products/img_13.png b/docs/products/img_13.png new file mode 100644 index 000000000..1b53814bd Binary files /dev/null and b/docs/products/img_13.png differ diff --git a/docs/products/img_14.png b/docs/products/img_14.png new file mode 100644 index 000000000..92af2ca34 Binary files /dev/null and b/docs/products/img_14.png differ diff --git a/docs/products/img_15.png b/docs/products/img_15.png new file mode 100644 index 000000000..41e9328dc Binary files /dev/null and b/docs/products/img_15.png differ diff --git a/docs/products/img_16.png b/docs/products/img_16.png new file mode 100644 index 000000000..cdddd5086 Binary files /dev/null and b/docs/products/img_16.png differ diff --git a/docs/products/img_17.png b/docs/products/img_17.png new file mode 100644 index 000000000..6b981c0d3 Binary files /dev/null and b/docs/products/img_17.png differ diff --git a/docs/products/img_18.png b/docs/products/img_18.png new file mode 100644 index 000000000..99afe53a1 Binary files /dev/null and b/docs/products/img_18.png differ diff --git a/docs/products/img_19.png b/docs/products/img_19.png new file mode 100644 index 000000000..75bc957b1 Binary files /dev/null and b/docs/products/img_19.png differ diff --git a/docs/products/img_2.png b/docs/products/img_2.png new file mode 100644 index 000000000..8bc576eb9 Binary files /dev/null and b/docs/products/img_2.png differ diff --git a/docs/products/img_20.png b/docs/products/img_20.png new file mode 100644 index 000000000..56750882d Binary files /dev/null and b/docs/products/img_20.png differ diff --git a/docs/products/img_21.png b/docs/products/img_21.png new file mode 100644 index 000000000..4fdcdf4c3 Binary files /dev/null and b/docs/products/img_21.png differ diff --git a/docs/products/img_22.png b/docs/products/img_22.png new file mode 100644 index 000000000..e4bed713c Binary files /dev/null and b/docs/products/img_22.png differ diff --git a/docs/products/img_23.png b/docs/products/img_23.png new file mode 100644 index 000000000..14d8413bc Binary files /dev/null and b/docs/products/img_23.png differ diff --git a/docs/products/img_24.png b/docs/products/img_24.png new file mode 100644 index 000000000..266568e54 Binary files /dev/null and b/docs/products/img_24.png differ diff --git a/docs/products/img_25.png b/docs/products/img_25.png new file mode 100644 index 000000000..b29a74c28 Binary files /dev/null and b/docs/products/img_25.png differ diff --git a/docs/products/img_26.png b/docs/products/img_26.png new file mode 100644 index 000000000..4e93f3d01 Binary files /dev/null and b/docs/products/img_26.png differ diff --git a/docs/products/img_27.png b/docs/products/img_27.png new file mode 100644 index 000000000..640521183 Binary files /dev/null and b/docs/products/img_27.png differ diff --git a/docs/products/img_28.png b/docs/products/img_28.png new file mode 100644 index 000000000..bad1c7fcb Binary files /dev/null and b/docs/products/img_28.png differ diff --git a/docs/products/img_29.png b/docs/products/img_29.png new file mode 100644 index 000000000..e4552c88a Binary files /dev/null and b/docs/products/img_29.png differ diff --git a/docs/products/img_3.png b/docs/products/img_3.png new file mode 100644 index 000000000..686417198 Binary files /dev/null and b/docs/products/img_3.png differ diff --git a/docs/products/img_30.png b/docs/products/img_30.png new file mode 100644 index 000000000..26b6e9420 Binary files /dev/null and b/docs/products/img_30.png differ diff --git a/docs/products/img_31.png b/docs/products/img_31.png new file mode 100644 index 000000000..b8c80f3ec Binary files /dev/null and b/docs/products/img_31.png differ diff --git a/docs/products/img_32.png b/docs/products/img_32.png new file mode 100644 index 000000000..f9a1ae0b8 Binary files /dev/null and b/docs/products/img_32.png differ diff --git a/docs/products/img_33.png b/docs/products/img_33.png new file mode 100644 index 000000000..c5a1bf9fb Binary files /dev/null and b/docs/products/img_33.png differ diff --git a/docs/products/img_34.png b/docs/products/img_34.png new file mode 100644 index 000000000..7f81f4a49 Binary files /dev/null and b/docs/products/img_34.png differ diff --git a/docs/products/img_35.png b/docs/products/img_35.png new file mode 100644 index 000000000..1faac01b2 Binary files /dev/null and b/docs/products/img_35.png differ diff --git a/docs/products/img_36.png b/docs/products/img_36.png new file mode 100644 index 000000000..1b6b6a8d8 Binary files /dev/null and b/docs/products/img_36.png differ diff --git a/docs/products/img_37.png b/docs/products/img_37.png new file mode 100644 index 000000000..bd4b905fb Binary files /dev/null and b/docs/products/img_37.png differ diff --git a/docs/products/img_38.png b/docs/products/img_38.png new file mode 100644 index 000000000..7a77f7e61 Binary files /dev/null and b/docs/products/img_38.png differ diff --git a/docs/products/img_39.png b/docs/products/img_39.png new file mode 100644 index 000000000..8200213be Binary files /dev/null and b/docs/products/img_39.png differ diff --git a/docs/products/img_4.png b/docs/products/img_4.png new file mode 100644 index 000000000..0e0458291 Binary files /dev/null and b/docs/products/img_4.png differ diff --git a/docs/products/img_40.png b/docs/products/img_40.png new file mode 100644 index 000000000..4e08327ce Binary files /dev/null and b/docs/products/img_40.png differ diff --git a/docs/products/img_41.png b/docs/products/img_41.png new file mode 100644 index 000000000..362e74edf Binary files /dev/null and b/docs/products/img_41.png differ diff --git a/docs/products/img_42.png b/docs/products/img_42.png new file mode 100644 index 000000000..4f6c7bf6b Binary files /dev/null and b/docs/products/img_42.png differ diff --git a/docs/products/img_43.png b/docs/products/img_43.png new file mode 100644 index 000000000..dd9f0ea04 Binary files /dev/null and b/docs/products/img_43.png differ diff --git a/docs/products/img_44.png b/docs/products/img_44.png new file mode 100644 index 000000000..c9a022fe5 Binary files /dev/null and b/docs/products/img_44.png differ diff --git a/docs/products/img_45.png b/docs/products/img_45.png new file mode 100644 index 000000000..35b4181d6 Binary files /dev/null and b/docs/products/img_45.png differ diff --git a/docs/products/img_46.png b/docs/products/img_46.png new file mode 100644 index 000000000..d9262693c Binary files /dev/null and b/docs/products/img_46.png differ diff --git a/docs/products/img_47.png b/docs/products/img_47.png new file mode 100644 index 000000000..e0d76a48c Binary files /dev/null and b/docs/products/img_47.png differ diff --git a/docs/products/img_48.png b/docs/products/img_48.png new file mode 100644 index 000000000..945fa2c43 Binary files /dev/null and b/docs/products/img_48.png differ diff --git a/docs/products/img_49.png b/docs/products/img_49.png new file mode 100644 index 000000000..d6cf44e6f Binary files /dev/null and b/docs/products/img_49.png differ diff --git a/docs/products/img_5.png b/docs/products/img_5.png new file mode 100644 index 000000000..471ba817e Binary files /dev/null and b/docs/products/img_5.png differ diff --git a/docs/products/img_50.png b/docs/products/img_50.png new file mode 100644 index 000000000..6fe38678c Binary files /dev/null and b/docs/products/img_50.png differ diff --git a/docs/products/img_51.png b/docs/products/img_51.png new file mode 100644 index 000000000..30a55aedd Binary files /dev/null and b/docs/products/img_51.png differ diff --git a/docs/products/img_52.png b/docs/products/img_52.png new file mode 100644 index 000000000..1ee8e5f4e Binary files /dev/null and b/docs/products/img_52.png differ diff --git a/docs/products/img_53.png b/docs/products/img_53.png new file mode 100644 index 000000000..f2dc1b836 Binary files /dev/null and b/docs/products/img_53.png differ diff --git a/docs/products/img_54.png b/docs/products/img_54.png new file mode 100644 index 000000000..df99e2f43 Binary files /dev/null and b/docs/products/img_54.png differ diff --git a/docs/products/img_55.png b/docs/products/img_55.png new file mode 100644 index 000000000..bf69944dc Binary files /dev/null and b/docs/products/img_55.png differ diff --git a/docs/products/img_56.png b/docs/products/img_56.png new file mode 100644 index 000000000..78983c7cf Binary files /dev/null and b/docs/products/img_56.png differ diff --git a/docs/products/img_57.png b/docs/products/img_57.png new file mode 100644 index 000000000..4c6e479b1 Binary files /dev/null and b/docs/products/img_57.png differ diff --git a/docs/products/img_58.png b/docs/products/img_58.png new file mode 100644 index 000000000..60ffcbcbf Binary files /dev/null and b/docs/products/img_58.png differ diff --git a/docs/products/img_59.png b/docs/products/img_59.png new file mode 100644 index 000000000..d0a647e9d Binary files /dev/null and b/docs/products/img_59.png differ diff --git a/docs/products/img_6.png b/docs/products/img_6.png new file mode 100644 index 000000000..383e0cbb7 Binary files /dev/null and b/docs/products/img_6.png differ diff --git a/docs/products/img_60.png b/docs/products/img_60.png new file mode 100644 index 000000000..d005463dc Binary files /dev/null and b/docs/products/img_60.png differ diff --git a/docs/products/img_61.png b/docs/products/img_61.png new file mode 100644 index 000000000..cb62355fd Binary files /dev/null and b/docs/products/img_61.png differ diff --git a/docs/products/img_62.png b/docs/products/img_62.png new file mode 100644 index 000000000..27640e238 Binary files /dev/null and b/docs/products/img_62.png differ diff --git a/docs/products/img_63.png b/docs/products/img_63.png new file mode 100644 index 000000000..4574c4f78 Binary files /dev/null and b/docs/products/img_63.png differ diff --git a/docs/products/img_64.png b/docs/products/img_64.png new file mode 100644 index 000000000..4574c4f78 Binary files /dev/null and b/docs/products/img_64.png differ diff --git a/docs/products/img_65.png b/docs/products/img_65.png new file mode 100644 index 000000000..9789bf2da Binary files /dev/null and b/docs/products/img_65.png differ diff --git a/docs/products/img_66.png b/docs/products/img_66.png new file mode 100644 index 000000000..a066324db Binary files /dev/null and b/docs/products/img_66.png differ diff --git a/docs/products/img_67.png b/docs/products/img_67.png new file mode 100644 index 000000000..23b129f8b Binary files /dev/null and b/docs/products/img_67.png differ diff --git a/docs/products/img_68.png b/docs/products/img_68.png new file mode 100644 index 000000000..5d8c6b49e Binary files /dev/null and b/docs/products/img_68.png differ diff --git a/docs/products/img_69.png b/docs/products/img_69.png new file mode 100644 index 000000000..6f0911df7 Binary files /dev/null and b/docs/products/img_69.png differ diff --git a/docs/products/img_7.png b/docs/products/img_7.png new file mode 100644 index 000000000..3df206e87 Binary files /dev/null and b/docs/products/img_7.png differ diff --git a/docs/products/img_70.png b/docs/products/img_70.png new file mode 100644 index 000000000..8125c7eee Binary files /dev/null and b/docs/products/img_70.png differ diff --git a/docs/products/img_71.png b/docs/products/img_71.png new file mode 100644 index 000000000..d7641506c Binary files /dev/null and b/docs/products/img_71.png differ diff --git a/docs/products/img_72.png b/docs/products/img_72.png new file mode 100644 index 000000000..9280fcf8a Binary files /dev/null and b/docs/products/img_72.png differ diff --git a/docs/products/img_73.png b/docs/products/img_73.png new file mode 100644 index 000000000..0a5fa7cec Binary files /dev/null and b/docs/products/img_73.png differ diff --git a/docs/products/img_74.png b/docs/products/img_74.png new file mode 100644 index 000000000..3a0b57022 Binary files /dev/null and b/docs/products/img_74.png differ diff --git a/docs/products/img_75.png b/docs/products/img_75.png new file mode 100644 index 000000000..7e471482f Binary files /dev/null and b/docs/products/img_75.png differ diff --git a/docs/products/img_76.png b/docs/products/img_76.png new file mode 100644 index 000000000..ae11e6353 Binary files /dev/null and b/docs/products/img_76.png differ diff --git a/docs/products/img_77.png b/docs/products/img_77.png new file mode 100644 index 000000000..78f59b7da Binary files /dev/null and b/docs/products/img_77.png differ diff --git a/docs/products/img_78.png b/docs/products/img_78.png new file mode 100644 index 000000000..9b5a3cc87 Binary files /dev/null and b/docs/products/img_78.png differ diff --git a/docs/products/img_79.png b/docs/products/img_79.png new file mode 100644 index 000000000..2cf34044e Binary files /dev/null and b/docs/products/img_79.png differ diff --git a/docs/products/img_8.png b/docs/products/img_8.png new file mode 100644 index 000000000..b47c9d864 Binary files /dev/null and b/docs/products/img_8.png differ diff --git a/docs/products/img_80.png b/docs/products/img_80.png new file mode 100644 index 000000000..140d85a8a Binary files /dev/null and b/docs/products/img_80.png differ diff --git a/docs/products/img_81.png b/docs/products/img_81.png new file mode 100644 index 000000000..2f6b37973 Binary files /dev/null and b/docs/products/img_81.png differ diff --git a/docs/products/img_82.png b/docs/products/img_82.png new file mode 100644 index 000000000..b327567d2 Binary files /dev/null and b/docs/products/img_82.png differ diff --git a/docs/products/img_83.png b/docs/products/img_83.png new file mode 100644 index 000000000..82b3d475d Binary files /dev/null and b/docs/products/img_83.png differ diff --git a/docs/products/img_84.png b/docs/products/img_84.png new file mode 100644 index 000000000..4a2b1536e Binary files /dev/null and b/docs/products/img_84.png differ diff --git a/docs/products/img_9.png b/docs/products/img_9.png new file mode 100644 index 000000000..e6751878a Binary files /dev/null and b/docs/products/img_9.png differ diff --git a/docs/products/microservice-framework.md b/docs/products/microservice-framework.md new file mode 100644 index 000000000..dfd89688f --- /dev/null +++ b/docs/products/microservice-framework.md @@ -0,0 +1,348 @@ +# 微服务框架 + +## 产品概述 + +微服务框架是一套轻量级、高性能的分布式应用开发框架,专为构建可扩展、弹性的微服务架构应用而设计。该框架提供了微服务开发的全栈解决方案,涵盖服务注册发现、负载均衡、配置管理、服务通信、熔断降级、监控追踪等核心功能,帮助开发团队快速构建稳定可靠的微服务应用。 + +### 核心优势 + +- **轻量高效**:框架核心组件轻量化设计,低资源占用,高性能表现 +- **易学易用**:简洁的API设计和完善的文档,降低学习和使用门槛 +- **生态完善**:丰富的组件和插件生态,满足各类微服务场景需求 +- **云原生**:原生支持容器化部署和云环境,适配主流云平台 +- **高可用**:内置多种高可用策略,保障服务的稳定性和可靠性 + +## 技术架构 + +微服务框架采用分层设计,主要由以下核心组件构成: + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ 应用服务层 │ +└───────────────────────────────────────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────────────────┐ +│ 框架核心层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬──────────┤ +│ 服务治理 │ 远程调用 │ 消息通信 │ 数据访问 │ 安全框架 │ +└─────────────┴─────────────┴─────────────┴─────────────┴──────────┘ + ↓ +┌───────────────────────────────────────────────────────────────────┐ +│ 中间件集成层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬──────────┤ +│ 注册中心 │ 配置中心 │ 消息队列 │ 分布式缓存 │ 分布式事务│ +└─────────────┴─────────────┴─────────────┴─────────────┴──────────┘ + ↓ +┌───────────────────────────────────────────────────────────────────┐ +│ 基础设施层 │ +├─────────────┬─────────────┬─────────────┬─────────────┬──────────┤ +│ 容器编排 │ 服务网格 │ 监控告警 │ 日志收集 │ 链路追踪 │ +└─────────────┴─────────────┴─────────────┴─────────────┴──────────┘ +``` + +## 核心功能 + +### 1. 服务治理 + +- **服务注册与发现**:自动注册服务实例并发现可用服务 +- **健康检查**:定期检查服务健康状态,自动剔除不健康实例 +- **负载均衡**:支持多种负载均衡策略(轮询、权重、最少连接等) +- **服务路由**:基于多种条件的动态服务路由 +- **服务降级**:服务不可用时的优雅降级处理 + +### 2. 远程调用 + +- **多协议支持**:支持HTTP、gRPC、WebSocket等多种通信协议 +- **序列化选项**:支持JSON、Protobuf、Avro等多种序列化方式 +- **异步调用**:支持同步和异步两种调用模式 +- **超时控制**:灵活的超时设置和重试机制 +- **泛化调用**:无需依赖服务接口即可调用 + +### 3. 弹性设计 + +- **熔断器**:自动检测故障并阻止故障扩散 +- **限流器**:多种限流策略保护服务免受流量冲击 +- **隔离舱**:资源隔离,防止单一服务故障影响整体系统 +- **超时控制**:防止慢服务拖垮整个系统 +- **故障注入**:模拟各类故障场景进行韧性测试 + +### 4. 配置管理 + +- **集中配置**:统一管理各服务配置,支持动态更新 +- **配置隔离**:多环境、多版本配置隔离 +- **配置加密**:敏感配置信息自动加密存储 +- **变更通知**:配置变更实时推送到服务 +- **历史版本**:配置变更历史记录和回滚 + +### 5. 可观测性 + +- **指标收集**:自动收集服务运行指标 +- **日志管理**:结构化日志和集中式日志收集 +- **分布式追踪**:全链路调用追踪和性能分析 +- **健康检查**:多维度服务健康状态监控 +- **告警通知**:异常情况的多渠道告警 + +## 技术规格 + +### 系统要求 + +**运行环境**: +- JDK 8+(推荐JDK 11/17) +- Spring Framework 5.x+ +- Spring Boot 2.x+ + +**硬件建议**: +- CPU:2核心及以上 +- 内存:4GB及以上 +- 磁盘:根据应用规模,建议50GB以上SSD + +**支持的中间件**: +- 注册中心:Nacos、Eureka、Consul、ZooKeeper +- 配置中心:Nacos、Apollo、Spring Cloud Config +- 消息队列:RocketMQ、Kafka、RabbitMQ +- 缓存:Redis、Memcached +- 数据库:MySQL、PostgreSQL、MongoDB、ElasticSearch + +### 性能指标 + +- 单服务实例支持1000+ TPS(基于标准测试场景) +- 服务注册发现延迟<500ms +- 配置变更推送延迟<1s +- 熔断器响应时间<10ms +- 追踪数据对系统性能影响<3% + +## 快速入门 + +### 环境准备 + +1. **安装JDK** + ```bash + # 安装OpenJDK 11 + sudo apt install openjdk-11-jdk + # 或使用SDKMAN + sdk install java 11.0.12-open + ``` + +2. **安装Maven** + ```bash + sudo apt install maven + # 或使用SDKMAN + sdk install maven + ``` + +3. **安装Docker(可选,用于部署中间件)** + ```bash + curl -fsSL https://get.docker.com | sh + sudo systemctl start docker + ``` + +### 创建服务 + +1. **初始化项目** + + 使用我们提供的脚手架工具创建项目: + ```bash + # 安装脚手架工具 + npm install -g @microservice/cli + + # 创建项目 + ms-cli create my-service + ``` + +2. **编写服务接口** + + ```java + @Service + public interface UserService { + User getUserById(Long id); + List listUsers(int page, int size); + User createUser(User user); + } + ``` + +3. **实现服务** + + ```java + @ServiceImpl + public class UserServiceImpl implements UserService { + @Autowired + private UserRepository userRepository; + + @Override + public User getUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User not found")); + } + + @Override + public List listUsers(int page, int size) { + return userRepository.findAll(PageRequest.of(page, size)).getContent(); + } + + @Override + public User createUser(User user) { + return userRepository.save(user); + } + } + ``` + +4. **配置服务** + + ```yaml + # application.yml + spring: + application: + name: user-service + + microservice: + registry: + type: nacos + address: localhost:8848 + config: + enabled: true + type: nacos + address: localhost:8848 + circuit-breaker: + enabled: true + tracing: + enabled: true + sampler: + probability: 0.1 + ``` + +5. **启动服务** + + ```bash + # 编译打包 + mvn clean package + + # 运行服务 + java -jar target/my-service-1.0.0.jar + ``` + +### 服务调用 + +1. **同步调用** + + ```java + @RestController + public class UserController { + @Autowired + private ServiceCaller serviceCaller; + + @GetMapping("/remote-users/{id}") + public User getRemoteUser(@PathVariable Long id) { + return serviceCaller.create("user-service") + .path("/users/{id}") + .pathVariable("id", id) + .get() + .responseAs(User.class); + } + } + ``` + +2. **异步调用** + + ```java + @Service + public class NotificationService { + @Autowired + private AsyncServiceCaller asyncCaller; + + public CompletableFuture sendNotification(Notification notification) { + return asyncCaller.create("notification-service") + .path("/notifications") + .post(notification) + .responseAsVoid(); + } + } + ``` + +## 最佳实践 + +### 服务设计原则 + +- **单一职责**:每个服务专注于单一业务功能 +- **接口优先**:先设计API,再实现服务 +- **无状态设计**:服务实例不保存状态,便于水平扩展 +- **异步通信**:非关键路径使用异步通信提高性能 +- **幂等性**:设计支持重试的幂等接口 + +### 高可用策略 + +- **多实例部署**:每个服务部署多个实例 +- **跨区域部署**:关键服务跨区域部署 +- **熔断降级**:及时熔断故障服务,提供降级方案 +- **限流保护**:对关键服务实施限流保护 +- **灰度发布**:新版本逐步替换旧版本 + +### 性能优化 + +- **连接池管理**:合理配置各类连接池大小 +- **缓存策略**:多级缓存减轻服务压力 +- **批量处理**:合并小请求为批量请求 +- **数据分片**:大数据集合理分片处理 +- **异步处理**:非实时需求采用异步处理 + +## 常见问题 + +### 1. 服务注册失败 + +**可能原因**: +- 注册中心地址配置错误 +- 网络连接问题 +- 服务实例IP/端口配置不正确 + +**解决方法**: +- 检查注册中心地址配置 +- 确认网络连通性 +- 检查服务实例IP/端口配置 + +### 2. 服务调用超时 + +**可能原因**: +- 目标服务处理能力不足 +- 网络延迟高 +- 超时配置不合理 + +**解决方法**: +- 增加目标服务实例数 +- 优化服务处理逻辑 +- 调整超时和重试配置 + +### 3. 配置更新不生效 + +**可能原因**: +- 配置中心连接问题 +- 配置项命名空间错误 +- 服务未正确实现配置刷新 + +**解决方法**: +- 检查配置中心连接 +- 确认配置项的命名空间和分组 +- 实现配置变更监听器 + +## 版本历史 + +### v2.5.0 (2023-09-20) + +- 新增服务网格集成支持 +- 优化分布式追踪性能 +- 增强安全性,支持OAuth 2.1 +- 新增多语言SDK支持 + +### v2.0.0 (2023-03-15) + +- 架构升级,全面支持云原生 +- 新增反应式编程模型 +- 支持多协议通信 +- 增强可观测性功能 + +### v1.5.0 (2022-08-10) + +- 新增分布式事务支持 +- 优化服务发现机制 +- 增强限流和熔断功能 +- 提升整体性能和稳定性 + +我们提供专业的技术支持和咨询服务,帮助您成功构建和运维微服务应用。 diff --git a/docs/products/photoImage.md b/docs/products/photoImage.md new file mode 100644 index 000000000..2c3dd392f --- /dev/null +++ b/docs/products/photoImage.md @@ -0,0 +1,11 @@ +--- +title: 话圈小程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 话圈小程序 + +![img_49.png](./img_49.png) + + diff --git a/docs/products/product-architecture.svg b/docs/products/product-architecture.svg new file mode 100644 index 000000000..451fed523 --- /dev/null +++ b/docs/products/product-architecture.svg @@ -0,0 +1,82 @@ + + + + + + 产品系列架构图 + + + + 企业级应用平台 + 核心业务支撑平台 + + + + 微服务框架 + 分布式服务架构 + + + + 数据分析套件 + 数据处理与分析 + + + + + + + + 服务治理 + + + 弹性设计 + + + + + + + 数据处理 + + + 数据可视化 + + + + + + + 应用开发 + + + 流程管理 + + + 数据管理 + + + + 企业应用生态系统 + + + + + + + + + + 企业级应用平台 + + + 微服务框架 + + + 数据分析套件 + + + 应用生态系统 + + + © 2023 JavaPlus 技术文档平台 + \ No newline at end of file diff --git a/docs/products/product-car.md b/docs/products/product-car.md new file mode 100644 index 000000000..7d79777d3 --- /dev/null +++ b/docs/products/product-car.md @@ -0,0 +1,25 @@ +--- +title: 电动车充电桩小程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 电动车充电桩小程序 + +![img_24.png](./img_24.png) + +![img_25.png](./img_25.png) + +![img_26.png](./img_26.png) + +![img_27.png](./img_27.png) + +![img_28.png](./img_28.png) + +![img_29.png](./img_29.png) + +![img_30.png](./img_30.png) + +![img_31.png](./img_31.png) + + diff --git a/docs/products/product-gallery.md b/docs/products/product-gallery.md new file mode 100644 index 000000000..de7974f2b --- /dev/null +++ b/docs/products/product-gallery.md @@ -0,0 +1,64 @@ +--- +title: 【溜溜梅】官方商城小程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 【溜溜梅】官方商城小程序 + +![img.png](./img.png) + +![img_1.png](./img_1.png) + +![img_2.png](./img_2.png) + +![img_3.png](./img_3.png) + +![img_4.png](./img_4.png) + +![img_5.png](./img_5.png) + +![img_6.png](./img_6.png) + +![img_7.png](./img_7.png) + +![img_8.png](./img_8.png) + +![img_9.png](./img_9.png) + +![img_10.png](./img_10.png) + +![img_11.png](./img_11.png) + +![img_12.png](./img_12.png) + +![img_13.png](./img_13.png) + +![img_14.png](./img_14.png) + +![img_15.png](./img_15.png) + +![img_16.png](./img_16.png) + +![img_17.png](./img_17.png) + +![img_18.png](./img_18.png) + +![img_19.png](./img_19.png) + +![img_20.png](./img_20.png) + +![img_21.png](./img_21.png) + +![img_22.png](./img_22.png) + +![img_23.png](./img_23.png) + + + + + + + + + diff --git a/docs/products/productChange.md b/docs/products/productChange.md new file mode 100644 index 000000000..7c37ec800 --- /dev/null +++ b/docs/products/productChange.md @@ -0,0 +1,35 @@ +--- +title: 小区充电桩小程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 小区充电桩小程序 + +![img_35.png](./img_35.png) + +![img_36.png](./img_36.png) + +![img_37.png](./img_37.png) + +![img_38.png](./img_38.png) + +![img_39.png](./img_39.png) + +![img_40.png](./img_40.png) + +![img_41.png](./img_41.png) + +![img_42.png](./img_42.png) + +![img_43.png](./img_43.png) + +![img_44.png](./img_44.png) + +![img_45.png](./img_45.png) + +![img_46.png](./img_46.png) + +![img_47.png](./img_47.png) + +![img_48.png](./img_48.png) diff --git a/docs/products/productShare.md b/docs/products/productShare.md new file mode 100644 index 000000000..2a46c47b5 --- /dev/null +++ b/docs/products/productShare.md @@ -0,0 +1,15 @@ +--- +title: 分销小程序 +author: 哪吒 +date: '2023-06-15' +--- + +# 分销小程序 + +![img_32.png](./img_32.png) + +![img_33.png](./img_33.png) + +![img_34.png](./img_34.png) + + diff --git "a/docs/products/\344\274\230\350\264\250\350\241\214\344\270\232\346\212\245\345\221\212\347\275\221\347\253\231.md" "b/docs/products/\344\274\230\350\264\250\350\241\214\344\270\232\346\212\245\345\221\212\347\275\221\347\253\231.md" new file mode 100644 index 000000000..5dbb78837 --- /dev/null +++ "b/docs/products/\344\274\230\350\264\250\350\241\214\344\270\232\346\212\245\345\221\212\347\275\221\347\253\231.md" @@ -0,0 +1,104 @@ +# 优质行业报告网站 + +::: tip 导航说明 +本页面收集整理了各类优质行业报告网站资源,按照不同领域分类,方便快速查找和访问。所有链接均为官方网站,定期更新维护。 +::: + +## 宏观经济数据 + +| 网站名称 | 网址 | 简介 | +| :------ | :------ | :------ | +| **国家宏观经济数据** | [官方网站](http://www.gov.cn/shuju/index.htm) | 中国政府网发布的权威宏观经济数据 | +| **国家统计局** | [官方网站](http://www.stats.gov.cn/tjsj/) | 国家统计局发布的各类统计数据和分析报告 | +| **商务部** | [官方网站](http://www.mofcom.gov.cn/article/fxbg/) | 商务部发布的行业分析和贸易数据报告 | + +## 行业综合性聚合网站 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **前沿报告库** | [官方网站](https://wk.askci.com/ListTable/) | 多行业研究报告聚合平台 | +| **洞见研报** | [官方网站](https://www.djyanbao.com/index) | 专注行业深度分析研究报告 | +| **发现报告** | [官方网站](https://www.fxbaogao.com/) | 提供多维度行业研究报告 | +| **镝数聚** | [官方网站](https://zhuanlan.zhihu.com/p/467709762) | 数据可视化和行业分析 | +| **前瞻产业研究院** | [官方网站](https://bg.qianzhan.com/) | 产业研究和市场预测报告 | +| **中国互联网信息中心** | [官方网站](http://www.cnnic.net.cn/) | 互联网发展统计报告 | +| **中国产业研究院** | [官方网站](https://www.chinairn.com/yjbg/) | 综合性产业研究报告 | +| **中国信通院** | [官方网站](http://www.caict.ac.cn/kxyj/) | 信息通信领域研究报告 | + +## 互联网领域 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **中文互联网数据资讯网** | [官方网站](http://www.199it.com/) | 互联网行业数据和分析 | +| **1991IT大数据导航** | [官方网站](http://hao.199it.com/) | IT行业数据资源导航 | +| **艾瑞** | [官方网站](https://report.iresearch.cn/) | 互联网行业研究报告 | +| **艾媒** | [官方网站](https://www.iimedia.cn/c400) | 新媒体行业研究报告 | +| **IT桔子** | [官方网站](https://www.itjuzi.com/report/) | 创业投资数据分析 | +| **极光-月狐** | [官方网站](https://www.moonfox.cn/insight/report) | 移动互联网分析报告 | +| **QuestMobile** | [官方网站](https://www.questmobile.com.cn/research/report-new) | 移动互联网用户行为分析 | + +## 新兴技术与创新领域 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **36氪** | [官方网站](https://36kr.com/academe) | 创新创业研究报告 | +| **亿欧** | [官方网站](https://www.iyiou.com/research) | 产业创新研究 | +| **易观分析** | [官方网站](https://www.analysys.cn/) | 数字化转型研究 | +| **易观博阅** | [官方网站](https://boyue.analysys.cn/#/) | 数字经济研究报告 | +| **鲸准** | [官方网站](https://cloud.jingdata.com/#/insight/researchReport) | 投资分析报告 | +| **Wind万得** | [官方网站](https://www.wind.com.cn/default.html) | 金融数据分析 | +| **Mob研究院** | [官方网站](https://www.mob.com/mobdata/report) | 移动互联网研究 | +| **Fastdata极数** | [官方网站](http://www.ifastdata.com/list/index/id/2) | 大数据分析报告 | +| **TalkingData** | [官方网站](http://mi.talkingdata.com/reports.html) | 移动数据研究 | +| **阿拉丁** | [官方网站](https://www.aldzs.com/viewpointlist) | 小程序生态研究 | +| **新榜报告** | [官方网站](https://report.newrank.cn/index.html) | 内容创业研究 | +| **个推** | [官方网站](https://getui.com/reports) | 移动推送数据研究 | +| **CBNData** | [官方网站](https://www.cbndata.com/) | 消费大数据研究 | +| **艺恩** | [官方网站](https://www.endata.com.cn/) | 娱乐产业数据分析 | + +## 数据指数平台 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **百度指数** | [官方网站](https://index.baidu.com/v2/index.html#/) | 搜索关键词趋势分析 | +| **微博指数** | [官方网站](https://data.weibo.com/index) | 社交媒体热点分析 | + +## 大厂研究院 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **腾讯研究院** | [官方网站](https://www.tisi.org/) | 互联网产业研究 | +| **阿里研究院** | [官方网站](http://www.aliresearch.com/cn/index) | 数字经济研究 | +| **美团研究院** | [官方网站](https://about.meituan.com/research/home) | 本地生活服务研究 | +| **京东消费及产业发展研究院** | [官方网站](https://research.jd.com/) | 消费趋势研究 | + +## 国际咨询公司 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **Gartner** | [官方网站](https://www.gartner.com/en) | 全球IT研究与咨询 | +| **麦肯锡** | [官方网站](https://www.mckinsey.com.cn/) | 管理咨询研究 | +| **波士顿咨询** | [官方网站](https://www.bcg.com/zh-cn/) | 战略咨询研究 | +| **贝恩** | [官方网站](https://www.bain.cn/) | 商业战略咨询 | +| **摩根大通** | [官方网站](https://www.jpmorgan.com/global) | 金融市场研究 | + +## 四大会计师事务所研究 + +| 网站名称 | 网址 | 特色内容 | +| :------ | :------ | :------ | +| **德勤** | [官方网站](https://www2.deloitte.com/cn/zh.html) | 行业洞察与趋势分析 | +| **安永** | [官方网站](https://www.ey.com/zh_cn) | 商业环境与风险分析 | +| **毕马威** | [官方网站](https://home.kpmg/cn/zh/home.html) | 行业前瞻性研究 | +| **普华永道** | [官方网站](https://www.pwccn.com/zh) | 全球经济趋势研究 | + +::: warning 使用提示 +部分网站可能需要注册或付费才能查看完整报告内容。建议先浏览网站提供的免费报告,评估其价值后再决定是否付费。 +::: + +## 如何高效利用行业报告 + +1. **明确目标**:在查阅前明确你需要了解的行业问题或数据点 +2. **交叉验证**:对关键数据点使用多个来源进行验证 +3. **关注方法论**:了解报告的研究方法和样本来源 +4. **追踪时间序列**:关注同一指标的历史变化趋势 +5. **结合实际**:将报告数据与实际业务场景结合分析 diff --git "a/docs/products/\346\227\240\344\272\272\345\262\233\345\225\206\344\270\232\350\256\241\345\210\222\344\271\246.md" "b/docs/products/\346\227\240\344\272\272\345\262\233\345\225\206\344\270\232\350\256\241\345\210\222\344\271\246.md" new file mode 100644 index 000000000..2f778bf3b --- /dev/null +++ "b/docs/products/\346\227\240\344\272\272\345\262\233\345\225\206\344\270\232\350\256\241\345\210\222\344\271\246.md" @@ -0,0 +1,117 @@ +# 无人岛商业计划书 + +2015年3月15日,山东省正式开始为省内500余座无人岛招募岛主,出让为期50年的使用权。事实上,民间资本早已渴望向散落海中的无人岛涌动。自上世纪90年代末至今,无人海岛开发先后经历热潮、叫停、解冻和规范重启的过程。 + +在宁波等地,无人岛岛主早已经屡见不鲜,只不过与外界对他们“坐享私密阳光沙滩的想象不同,岛主们的日子似乎并不好过,无人岛的商业开发也尚无真正意义上的成功典范。业界目前普遍认为,海岛开发的难点一是淡水、交通及电力资源问题,另一方面如何找准规则和产品定位,单纯打“阳关、沙滩、海鲜”的牌子已经很难吸引人流。 + +假如你是某创业团队,请针对无人岛开发拟定一份商业计划书, 以吸弓|投资者投资,商业计划书包括但不限于:无人岛的产品定位及主要功能、初期如何推广及吸引人流、初步的开发计划和周期等。 + +答案分享一 +考察能力: + +● 思维发散能力 + +● 逻辑思考能力 + +● 规划能力 + +解题思路: + +● 无人岛定义是什么,存在什么问题难以解决(现状,问题) + +● 无人岛的定位是什么样的(确定方向) + +● 通过什么方式可以解决无人岛存在的问题,变不利为有利(如何改进) + +● 获客方式是什么?(如何落地) + +● 开发计划是什么样的?(如何规划) + +参考答案: + +(1)什么是“无人岛”? + +1. 无人岛并非没人居住,而是未列入编制的海岛; +2. 无人岛开发失败原因之一,是对开发的困难估计不足:基础设施投入、配套设施投入、市场开发等需要较强的资金实力和较长的投资回收期; +3. 生态环境脆弱是无人岛开发的最大难题。 + +(2)目标用户 + +25-35岁之间的年轻人,热衷于各种新鲜事物。 + +(3)岛屿定位 + +年轻人探险+玩乐的刺激乐园(现实版的绝地求生:吃鸡游戏) + +(4)初期如何吸引用户 + +变不利为有利: + +题目中明确提出,无人岛的交通、水源和电力十分匮乏,大家也已经不再习惯那种阳光、海浪和沙滩式的拍照。所以可以转化下思路,把无人岛和目前很热门的游戏——绝地求生相结合,真实还原游戏场景,将无人岛的缺点转化为优点。 + +IP打造,实现传播: + +腾讯的绝地求生+无人岛产业可以迅速拉动无人岛开发,通过游戏IP+综艺真人化,扩大IP的影响力和可能性,同时通过植入各种广告赞助解决拍摄和宣传问题。 + +引进更多合作角色: + +在无人岛上可将消费场景转化为游戏场景,将消费者转化为NPC,将新零售转化为连接NPC和游戏场景的中转,最终推广品牌形象。因此可以把岛屿特定消费场景建设承包给外部商家,以买断使用权的方式进行岛屿的建设,从而解决岛屿投入资金巨大且回款周期慢的问题 + +(5)开发计划 + +初始建设: + +1. 岛屿基础建设:无人岛的主题概念打造和进行招商引资,实现岛屿的初步建设; +2. 设计登岛后的游戏规则,先通过综艺来呈现,让用户有向往。等后续对外开放,直面用户,增强用户体验。 +宣推建设: +1. 打造真人秀综艺,引进流量综艺大咖+制作团队,投资证券化+广告招商,将整体成本投入分散出去,让想从中分一杯羹的商家来承担,降低开发成本投入。 +2. 设置好各大平台的传播节点并配合对应素材,实现爆款传播 +3. 登岛的游戏规则里设计任务体系,让用户主动实现无人岛传播 + +联动建设: + +不同岛屿可以是不同地图。通过解锁不同地图可获得不同奖励。实现岛屿之间的联动建设。 + +答案分享二 + +前置思考:解决的是无人岛开发难的问题目的是为了吸引到投资。 + +无人岛开发难的原因: + +1. 淡水交通电力资源问题 +2. 难以找准规则和产品定位 +3. “阳光沙滩海鲜”难以吸引人流 + +解决方案:荒岛求生式野外探险项目。 + +(1)无人岛的产品定位及主要功能 + +该项目主要面向的是喜爱野外探险的人群,为他们提供一个安全而又不缺乏刺激性的沉浸式探险求生体验。 + +存在这么一部分人,他们喜爱贴近自然的野外探险,然而国内尚未出现类似的体验式项目,而擅自在基础设施不完善的野外进行类似活动,危险系数极高;无人岛由于生态较为原始,并且面积相对高山丛林等区域小,可以在保证安全性的前提下,进行荒岛求生式的野外探险项目的开发。 + +(2)初期如何推广及吸引人流 + +产品:以“荒岛求生沉浸式野外探险”为独特卖点,筹备野外生火、寻找水源、捕获鱼类、制作庇护所驾驶山地摩托制作攀岩绳索等项目,并设计容易让人记住的推广语,如“海岛奇兵,勇闯天涯”,着重安全性方面的宣传。 + +价格:根据体验时间和内容,设计不同;价位的项目,现设计A、B两个类别。 + +A:10小时,500元,内容为:全程救练跟拍指导,潜水探险,荒岛山地摩托等其他所有项目。 + +B:5小时,200元 半程教练指导,野外基础项目体验。 + +渠道:通过自营的小程序或公众号等方式开设订票渠道,并借鉴裂变思想,用户邀请好友满X人后,可获得免费体验机会。 + +宣传:在抖音、快手、微信等平台开设官方自媒体,进行项目介绍,推出有趣好玩的视频,获取更大的关注度,同时也邀请户外运动方面的KOL免费体验该项目,撰写体验文章,进行宣传。也邀请国内的一些内容相关的综艺节目制作组选取本岛作为拍摄地点,获得更大曝光率。 + +(3)初步的开发计划和周期 + +初步开发计划着重于基础设施的完善和基本项目的建设,周期预计3个月。 + +基础设施包括:电力、淡水、道路、安全工程保障; + +基本项目包括:体验区规划、植被重栽、地貌改造等。 + +针对电力难以供给的问题,批量采购光伏和风能设施,配以定期运送的蓄电池,使海岛电力基本能满足供给要求,淡水供给主要满足部分游客和工作人员需要,采取从陆地集中运送的方式,安全工程包括在监控全覆盖、警告装置设置、项目安全评估等等。 + +体验区规划借鉴相关经验,并进行自主创新,开设专门的生火点、攀岩点等,另外栽种可供食用的植被等,并进行合理布局,地貌改造包括攀岩点建设、摩托区改造等。 diff --git "a/docs/products/\347\257\256\347\220\203\345\234\272\345\246\202\344\275\225\350\265\232\351\222\261.md" "b/docs/products/\347\257\256\347\220\203\345\234\272\345\246\202\344\275\225\350\265\232\351\222\261.md" new file mode 100644 index 000000000..1b5852c01 --- /dev/null +++ "b/docs/products/\347\257\256\347\220\203\345\234\272\345\246\202\344\275\225\350\265\232\351\222\261.md" @@ -0,0 +1,65 @@ +# 篮球场如何赚钱 + +“收入 = 人流 *转化率* 客单价 * 复购率” + +今天在群里聊到个比较感兴趣的话题。 + +有一个篮球场,提供了下面这四种消费方式,你如何看待你的用户群体需求,又如何帮助老板赚钱。 + +先说说这四种消费方式: + +1. 单次打球,不限时,价格 20; +2. 月卡会员,不限次,1个月有效,价格 100; +3. 次卡会员,11 次,长期有效,价格 200; +4. 年卡会员,不限次,1年有效,价格999; + +怎么样,看到这个是否想到了健身房的会员:用基础设施赚用户会员费。那篮球场也能用这种方式赚钱么 ? + +不一定,反正我是没有为了打球办过卡。 + +讲真,我打球的次数还是挺多的,基本每周打个两次以上。遇到工作忙碌的时候,整个月打不了球,就会很苦恼。 + +所以,如果要选一种打球方案的话,我会优先考虑第三种:充次卡 。 + +从我个人角度的来看,影响我打球存有两因素:一个是意愿是否强烈,另外一个是时间是否充足。 + +如果居住场所离球场较远,打球的意愿就很低,加上平时工作忙,我更愿意接纳单次付费,啥时候打啥时候给钱。 + +如果球场距离不远,身边朋友经常约,每周又有固定的时间运动,我反而更愿意接纳月会员。要是常驻球馆,例如球队训练这种,办理年会也不是不可能。 + +所以,按照意愿和时间两个维度来划分的话,可以把打球用户做个分类,类似这样。 + +哪一类用户居多呢,我觉得大部分用户都会集中在「散场」和「月次卡」这一类,为啥? + +很现实的问题,球场打球除了学生,绝大多数都是工作群体,很多都没有固定的休息时间,而且打球还经常找不到伴。 + +因此,球场靠会员赚钱是不太可能实现盈利的,因为满足条件的用户非常少。 + +那又有了新的问题,如果你是老板,要怎样才能赚钱? + +如果把球场当成一款交付的商品来看待,我个人觉得,有两个思路: + +1. 提高商品收入,收入 = 人流 *转化率* 客单价 * 复购率 +2. 增强用户意愿,刺激用户群体打球的想法,主动购买商品。 + +从人流量和转化率的角度来看,要能够增加球场打球人数,其次是提高每个人的打球次数。 + +比如针对附近学校、公司、社区来召集球队,提供奖金来举办一些不同群体的球赛活动,这种玩法很多球场都有在做。或者布置一些酷炫的涂鸦场景,打造网红打卡地。毕竟除了打球,年轻人耍酷和拍照也有很强烈的需求。 + +又比如把球场的四种会员模式中,挑选几款主力推广,比如针对散场的用户群体,开放次卡的用户使用限制,鼓励其办次卡会员。 + +或者推出每月的会员日,到场消耗次数打球参与篮球商品抽奖等,这两种都是提高转化率的方式。 + +如果人流量和平均到场打球的次数有了保障,提高客单价也能赚钱。 + +那是不是提高打球的费用就够了?当然不是,除非固定成本大幅提升,不然不建议用这种方式。 + +我见过一些球场,针对中小学生推出兴趣培养班、针对青少年打造青训计划,如果招培训老师比较麻烦,也可以提供场地租赁。另外,球馆内售卖一些护具和品牌周边,也可以推出收费拍照的方式,结合高频的运动需求,引导用户低频的消费。 + +除了促进客单价,还可以从用户体验和球馆运营角度提高一下复购率。 + +比如打球前能很快预约,打球打得爽,赛后能冲个澡换个衣服等等细节,通过小细节来增加用户体验,营造好的口碑。 + +又比如通过社群和服务号来链接打球的群体,通过文章和视频等内容,发布到各种渠道来提升球馆的认知度。 + +总之,好的口碑和球馆的高知名度 diff --git "a/docs/products/\350\234\227\347\211\233\347\235\241\347\234\240\351\253\230\345\265\251.md" "b/docs/products/\350\234\227\347\211\233\347\235\241\347\234\240\351\253\230\345\265\251.md" new file mode 100644 index 000000000..bdd00febd --- /dev/null +++ "b/docs/products/\350\234\227\347\211\233\347\235\241\347\234\240\351\253\230\345\265\251.md" @@ -0,0 +1,121 @@ +# 蜗牛睡眠高嵩 + +各位好,这篇是《将军请上座》的第07期,今天上座的主人公是蜗牛睡眠创始人——高嵩。 +高嵩在2015年创办了蜗牛睡眠,是国内最早关注睡眠健康的创业者之一。跟很多创业者做足准备再开始创业不同,高嵩决定创业时,既没有投资人,也没有合伙人,连工厂资源都没有,是一个“三无创业者”…… + +* 以下信息来自被采者口述,不对其真实性负责。 + +想要创业和正在创业的人,一般会面临三个灵魂拷问: + +● 做好什么样的准备,才能开始创业? + +● 一直看不到结果,还要不要坚持? + +● 创业时找不到战略方向,怎么办? + +高嵩虽然把蜗牛睡眠做成了出圈的产品,但他刚开始决定创业时,既没有做好充分的准备,也没有投资人、合伙人、连产品和工厂的资源都没有。 + +这篇文章,我们一起看看,蜗牛睡眠创始人高嵩,能带给你什么启发。 + +01 创业永远没有准备好的时候,干了再说 + +2014年,36岁的高嵩裸辞了。 + +辞职之前,高嵩是一家世界500强公司高管,做过几年技术leader,后来转型做销售,成为中国区高管。 + +正常人创业,要么已经聊好了合伙人,要么是在一个领域里积攒了丰厚的产业资源,不出来自己做就浪费了。 + +但高嵩呢,一没合伙人、二没投资人,三没产业资源,是一个“三无创业者”。 + +当时,中国智能硬件刚刚萌芽,高嵩觉得这可能是个风口,他想搞一款改善睡眠的硬件试试。 +带着这个想法,高嵩找了几十个朋友,别人都拒绝了他,只有在北大读书时的朋友竹东翔,被他“忽悠”得也裸辞了。“这人以前是造神舟一号飞船的,是个技术大咖,多少有点理想主义。” +后来,朋友又向他推荐了一位做产品设计的大牛。三个男人凑在一起,堪堪组成一家公司。 + +开始做产品时,他们没有工厂渠道,高嵩是通过朋友的朋友的朋友,才找到一家可以合作的工厂。 + +因为一个投资人都不认识,他只能带着项目跑公开路演,基本上跑一次黄一次。足足见了86个投资人,才有一个人肯给他投钱。 + +高嵩说,他接触过很多有创业想法的人,他们一直在筹钱、筹人脉、筹资源,最后在筹备中不了了之。 + +“创业这事,你准备得越久,想得越多,就越做不成。” + +也许,创业永远没有准备好的时候,就得先干了再说。 + +高总的行动力,让我想起巴菲特的 10% 投资测试。假设现在你可以选一个人,买入他一生之内 10% 的收入,你会看中他身上的什么品质? + +我回想了一下,身边那些在财富上获得成功的人,分别具备什么特质 + +我发现,那些获得普世意义成功的人,不一定都是认知水平很高的人。但这群人一定是不安于现状和行动力强的人。 + +尤其行动力强,是绝大多数人不具备的品质。因为很多事儿不是想明白的,是做明白的。 + +当我们决定创业的时候,从“有想法”到“开始做”的这段路是最漫长的,与其犹豫,不如先去做,做了就比不做强。 + +02 押中有价值的赛道,才能撑住 + +高嵩说,创业这一路上,他一直都在硬撑。 + +原本计划八个月研发出来的睡眠枕头,因为堆料和用户体验的问题,足足打磨了24个月,才推出来。 + +高嵩带着这款枕头去参加创业大赛,获得了智能硬件组的冠军。在场的很多“评委”告诉高嵩,这项目他们包了。 + +高嵩天真地以为,不用再为钱犯愁了。但等到真要打款时,之前的“项目承包商”都消失了。 + +为了找钱,高嵩见了86个天使投资人。最后投钱的那位金主也不是看中了睡眠枕头,而是看到高嵩几个人整日吃住在公司里,觉得他们“靠得住”,才投了钱。 + +产品上架后并不好卖,高嵩特意把年中总结会的地点定在了三亚,想让几个合伙人在愉快的氛围中,找到解决问题的方法,没承想闭门会变成了散伙饭…… + +那段时间,高嵩一个做睡眠产品的人,竟然开始天天失眠。 + +我问他:“你当时是怎么撑住的?” + +高嵩给我讲了个故事。有一天,幼儿园老师问她女儿,你爸爸是做什么的?女儿告诉老师:“我的爸爸创造了蜗牛睡眠。”一个三四岁的孩子,积累的词汇量很有限,但她用了创造这个词。 + +“当你做的东西,让家人感到骄傲的时候,它才能被当作你的事业。”高嵩说:“我之所以能撑住,是我坚信这条赛道是对的,只是我还没找对方法而已。” + +在进入睡眠这个赛道之前,高嵩啃了很多调研数据。中国人普遍有睡眠问题,但中国的睡眠医疗水平,大概落后世界20年。 + +睡眠是人们一天中占比最长的活动,这是人们的刚需,这个赛道一定值得做。 + +很多人都知道创业维艰,关键得能撑住。但撑住的关键不是和命运死磕,而是你坚信自己选择的赛道一定是对的。在一个错误的赛道上,你撑得越久,死得越惨。 + +03 战略不是想出来的,是碰出来的 + +蜗牛睡眠的智能枕头做出来之后,高嵩用众筹的形式做了新品首发,用户反馈还不错,但放到淘宝上却卖不动。当时,枕头售价699一件,高嵩以为,是客单价太高,不适合线上渠道。 +于是,他又把枕头放到商场、机场免税店去卖。但因为枕头个头太大,产品销量远远满足不了坪效要求,最终也被“劝退”。 + +这时候,高嵩才意识到,也许不是渠道销售的问题,而是产品本身有问题。用户失眠时,有可能会想到明天要早点睡,买个褪黑素吃吃,给他一万种可能性,都不会想起来要买一个智能枕头。 + +他们创造了一个用户心智中没有的产品,当用户没有需求的时候,所有成本都是用户的教育成本。 + +就在高嵩一筹莫展时,他惊讶地发现,蜗牛睡眠APP 的下载量竟然一直在蹭蹭往上涨! + +这款APP,原本是给智能枕头做交互用的,一直放在应用商店,免费给人用。 + +如今,APP 激增的下载量一下子驱散了高嵩脑子里的混沌,没有任何犹豫,他马上把整个公司的战略重点挪到了 APP 上。这一次战略调整,高嵩明白了一个道理:战略不是想出来的,是碰出来的。 + +把业务转到 APP 上来之后,高嵩不再预设未来三年五年的规划,而是边做边碰。 + +比如,转型后的首要任务是快速把 APP 下载量拉到更高的量级。一开始,他们走常规路线,在应用商店优化,但效果差强人意。 + +直到一位网红偶然间向网友安利蜗牛睡眠APP,带来了很高的下载量,高嵩才再一次调整了营销方向:从购买搜索流量,到创造口碑效应。 + +2016年前后,种草还不那么流行,高嵩就带着蜗牛睡眠走上了网红带货之路,并且成为APP领域里,第一批吃到“种草”红利的企业。 + +高嵩这种“上路之后找战略”的做法并非个例,我们所熟知的大企业,都是这么做的。 + +比如,亚马逊一开始的时候,没想过要做亚马逊云,但因为“黑色星期五”流量暴涨,服务器总是宕机,加上店铺信息管理混乱,造成交易不稳定的问题,他们才开发了云服务。现在,云服务成了亚马逊最赚钱的业务之一。 + +我意识到,我的读者其实不只是想看故事,更多的是想获得经验型知识,所以我决定转变写法,从创始人的经历中,凝练出可用的方法论,输送给你。 + +很多人都觉得,公司战略是要预先规划好的,但实际上,创业环境是未知的、是随时变化的,战略不可能被准确规划出来。 + +你得先上路,路上会遇到一些资源和人,他们会启发你,帮你找到未来的发展方向。如果你不上路,你的战略只是空中楼阁。 + +我以前觉得,创业要先胜而后战。你要做好万全的准备,才能去创业,否则创业会变成无疾而终的冒险。但跟高总聊完,我最大的感受是,创业不是想明白的,是做明白的。比如: + +● 当你没有准备好的时候,可以干了再说; + +● 关键不是要撑下去,而是在有价值的赛道上撑下去; + +● 不要想出一个战略就奉为圭臬,你要拉出来溜溜,不行赶紧换。 diff --git a/docs/redis/redis-key-expiration.md b/docs/redis/redis-key-expiration.md new file mode 100644 index 000000000..2ef27661f --- /dev/null +++ b/docs/redis/redis-key-expiration.md @@ -0,0 +1,403 @@ +--- +title: Redis中的key过期问题解决方案 +author: 哪吒 +date: '2023-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Redis中的key过期问题解决方案 + +Redis作为高性能的内存数据库,被广泛应用于缓存、会话管理、计数器等场景。在使用Redis过程中,key的过期问题是一个常见的挑战,本文将详细介绍Redis key过期的原理、常见问题及解决方案。 + +## 1. Redis key过期机制原理 + +### 1.1 过期策略 + +Redis采用两种策略来处理过期的key: + +#### 1.1.1 定期删除 + +Redis默认每隔100ms随机抽取一部分设置了过期时间的key进行检查,如果发现已过期则删除。这种策略是一种折中方案,避免了每次都扫描全部key带来的性能问题。 + +``` +# redis.conf 配置 +hz 10 # 默认每秒执行10次定期删除 +``` + +#### 1.1.2 惰性删除 + +当客户端尝试访问某个key时,Redis会检查该key是否已过期,如果过期则删除并返回空值。这种方式只有在访问key时才会触发过期检查,节省了CPU资源,但可能导致过期key长时间占用内存。 + +### 1.2 内存淘汰机制 + +当Redis内存使用达到上限时,会触发内存淘汰机制,根据配置的策略删除部分key: + +1. **noeviction**: 不删除任何key,新写入操作会报错 +2. **allkeys-lru**: 删除最近最少使用的key(常用) +3. **allkeys-random**: 随机删除key +4. **volatile-lru**: 在设置了过期时间的key中,删除最近最少使用的key +5. **volatile-random**: 在设置了过期时间的key中,随机删除key +6. **volatile-ttl**: 在设置了过期时间的key中,删除剩余寿命最短的key +7. **allkeys-lfu**: 删除使用频率最少的key(Redis 4.0新增) +8. **volatile-lfu**: 在设置了过期时间的key中,删除使用频率最少的key(Redis 4.0新增) + +``` +# redis.conf 配置 +maxmemory 2gb # 设置最大内存 +maxmemory-policy allkeys-lru # 设置淘汰策略 +``` + +## 2. Redis key过期常见问题 + +### 2.1 缓存雪崩 + +**问题**: 大量key在同一时间点过期,导致大量请求直接访问数据库,可能使数据库瞬间崩溃。 + +**场景示例**: 系统在某个时间点批量设置了大量缓存,且过期时间相同,如电商系统在活动开始前预热商品数据,所有缓存设置为活动结束时间过期。 + +### 2.2 缓存击穿 + +**问题**: 某个热点key过期,导致大量并发请求直接访问数据库。 + +**场景示例**: 一个高访问量的商品详情页缓存突然过期,大量用户同时请求该商品信息。 + +### 2.3 缓存穿透 + +**问题**: 请求查询一个不存在的数据,导致请求直接落到数据库上。 + +**场景示例**: 恶意用户不断请求不存在的商品ID,每次请求都会查询数据库。 + +### 2.4 主从复制中的过期问题 + +**问题**: 在主从架构中,从节点不会主动过期key,只有当主节点过期一个key时,才会向从节点发送del命令。 + +**场景示例**: 如果主节点宕机,从节点提升为主节点,可能会出现已过期但未删除的key。 + +## 3. Redis key过期问题解决方案 + +### 3.1 缓存雪崩解决方案 + +#### 3.1.1 过期时间添加随机值 + +为缓存设置过期时间时增加一个随机值,避免大量缓存同时过期。 + +```java +// 设置过期时间为10-15分钟之间的随机值 +long timeout = 10 + new Random().nextInt(5); +redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MINUTES); +``` + +#### 3.1.2 缓存预热 + +系统启动时或定时任务中提前加载热点数据到缓存。 + +```java +@PostConstruct +public void preloadCache() { + List hotProducts = productService.findHotProducts(); + for (Product product : hotProducts) { + String key = "product:" + product.getId(); + redisTemplate.opsForValue().set(key, JSON.toJSONString(product), + getRandomExpireTime(), TimeUnit.MINUTES); + } +} +``` + +#### 3.1.3 多级缓存 + +使用本地缓存+分布式缓存的多级缓存架构。 + +```java +// 使用Caffeine作为本地缓存 +private LoadingCache localCache = Caffeine.newBuilder() + .maximumSize(1000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(key -> getFromRedis(key)); + +private Product getFromRedis(String key) { + String json = redisTemplate.opsForValue().get(key); + return JSON.parseObject(json, Product.class); +} +``` + +### 3.2 缓存击穿解决方案 + +#### 3.2.1 使用分布式锁 + +使用分布式锁确保同一时间只有一个请求去查询数据库和更新缓存。 + +```java +public Product getProduct(Long id) { + String key = "product:" + id; + String json = redisTemplate.opsForValue().get(key); + if (StringUtils.hasText(json)) { + return JSON.parseObject(json, Product.class); + } + + // 使用Redisson分布式锁 + RLock lock = redissonClient.getLock("lock:product:" + id); + try { + if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { + try { + // 双重检查 + json = redisTemplate.opsForValue().get(key); + if (StringUtils.hasText(json)) { + return JSON.parseObject(json, Product.class); + } + + // 查询数据库 + Product product = productMapper.selectById(id); + if (product != null) { + redisTemplate.opsForValue().set(key, JSON.toJSONString(product), + getRandomExpireTime(), TimeUnit.MINUTES); + } + return product; + } finally { + lock.unlock(); + } + } else { + // 获取锁失败,短暂休眠后重试获取缓存 + Thread.sleep(100); + json = redisTemplate.opsForValue().get(key); + if (StringUtils.hasText(json)) { + return JSON.parseObject(json, Product.class); + } + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } +} +``` + +#### 3.2.2 热点数据永不过期 + +对于极热点数据,可以设置永不过期,而是通过后台异步更新缓存。 + +```java +// 设置热点数据永不过期 +redisTemplate.opsForValue().set("hotspot:product:" + id, value); + +// 后台定时任务更新缓存 +@Scheduled(fixedRate = 300000) // 每5分钟执行一次 +public void refreshHotspotCache() { + Set keys = redisTemplate.keys("hotspot:product:*"); + for (String key : keys) { + Long id = Long.valueOf(key.split(":")[2]); + Product product = productMapper.selectById(id); + if (product != null) { + redisTemplate.opsForValue().set(key, JSON.toJSONString(product)); + } + } +} +``` + +### 3.3 缓存穿透解决方案 + +#### 3.3.1 缓存空值 + +对于不存在的数据,也缓存一个空值,但过期时间较短。 + +```java +public Product getProduct(Long id) { + String key = "product:" + id; + String json = redisTemplate.opsForValue().get(key); + + if (json != null) { + if (json.isEmpty()) { + // 空值缓存命中 + return null; + } + return JSON.parseObject(json, Product.class); + } + + // 查询数据库 + Product product = productMapper.selectById(id); + if (product == null) { + // 缓存空值,过期时间短 + redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES); + return null; + } else { + redisTemplate.opsForValue().set(key, JSON.toJSONString(product), + getRandomExpireTime(), TimeUnit.MINUTES); + return product; + } +} +``` + +#### 3.3.2 布隆过滤器 + +使用布隆过滤器快速判断key是否存在,避免对不存在的数据进行查询。 + +```java +// 初始化布隆过滤器 +private BloomFilter bloomFilter = BloomFilter.create( + Funnels.longFunnel(), + 10000000, // 预计元素数量 + 0.01 // 误判率 +); + +// 加载所有商品ID到布隆过滤器 +@PostConstruct +public void initBloomFilter() { + List allProductIds = productMapper.selectAllIds(); + for (Long id : allProductIds) { + bloomFilter.put(id); + } +} + +public Product getProduct(Long id) { + // 布隆过滤器判断 + if (!bloomFilter.mightContain(id)) { + return null; // ID不存在,直接返回 + } + + // 继续查询缓存和数据库 + // ... +} +``` + +### 3.4 主从复制中的过期问题解决方案 + +#### 3.4.1 合理配置主从参数 + +确保主节点及时过期key并同步到从节点。 + +``` +# redis.conf 主节点配置 +hz 20 # 提高定期删除频率 +``` + +#### 3.4.2 监控过期key情况 + +定期检查Redis中过期key的数量,及时发现异常。 + +```bash +# 监控过期key数量 +redis-cli info stats | grep expired_keys +``` + +## 4. 最佳实践 + +### 4.1 统一的缓存访问模板 + +封装一个统一的缓存访问模板,集成各种解决方案。 + +```java +public class CacheTemplate { + + private RedisTemplate redisTemplate; + private RedissonClient redissonClient; + private BloomFilter bloomFilter; + + public T queryWithCache(String keyPrefix, Long id, Class clazz, Function dbFallback) { + // 布隆过滤器判断 + if (bloomFilter != null && !bloomFilter.mightContain(id)) { + return null; + } + + String key = keyPrefix + id; + String json = redisTemplate.opsForValue().get(key); + + // 缓存命中 + if (json != null) { + if (json.isEmpty()) { + return null; // 空值缓存 + } + return JSON.parseObject(json, clazz); + } + + // 分布式锁防击穿 + RLock lock = redissonClient.getLock("lock:" + key); + try { + if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { + try { + // 双重检查 + json = redisTemplate.opsForValue().get(key); + if (json != null) { + return json.isEmpty() ? null : JSON.parseObject(json, clazz); + } + + // 查询数据库 + T data = dbFallback.apply(id); + if (data == null) { + // 缓存空值 + redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES); + } else { + // 缓存数据,添加随机过期时间 + long timeout = 10 + new Random().nextInt(5); + redisTemplate.opsForValue().set(key, JSON.toJSONString(data), + timeout, TimeUnit.MINUTES); + } + return data; + } finally { + lock.unlock(); + } + } else { + // 获取锁失败,短暂休眠后重试获取缓存 + Thread.sleep(100); + json = redisTemplate.opsForValue().get(key); + if (json != null) { + return json.isEmpty() ? null : JSON.parseObject(json, clazz); + } + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } +} +``` + +### 4.2 定期更新策略 + +对于某些重要数据,可以采用定期更新策略,避免过期问题。 + +```java +@Scheduled(fixedRate = 600000) // 每10分钟执行一次 +public void refreshImportantCache() { + List importantProducts = productService.findImportantProducts(); + for (Product product : importantProducts) { + String key = "product:" + product.getId(); + redisTemplate.opsForValue().set(key, JSON.toJSONString(product), + getRandomExpireTime(), TimeUnit.MINUTES); + } +} +``` + +### 4.3 监控和告警 + +设置Redis监控和告警机制,及时发现过期相关问题。 + +```java +@Scheduled(fixedRate = 300000) // 每5分钟执行一次 +public void monitorRedisExpiration() { + Long expiredKeys = redisTemplate.execute((RedisCallback) connection -> + connection.info().getProperty("expired_keys")); + + if (expiredKeys > THRESHOLD) { + // 触发告警 + alertService.sendAlert("Redis过期key数量异常: " + expiredKeys); + } +} +``` + +## 5. 总结 + +Redis key过期问题是使用Redis缓存系统时必须面对的挑战,通过理解Redis的过期机制原理,针对不同场景采用合适的解决方案,可以有效避免缓存雪崩、击穿和穿透等问题,提高系统的稳定性和性能。 + +关键解决方案包括: + +1. 为过期时间添加随机值,避免同时过期 +2. 使用分布式锁防止缓存击穿 +3. 缓存空值和使用布隆过滤器防止缓存穿透 +4. 采用多级缓存架构提高系统弹性 +5. 对热点数据进行特殊处理,如永不过期+后台更新 +6. 建立完善的监控和告警机制 + +通过这些方案的组合应用,可以构建一个健壮的Redis缓存系统,有效解决key过期带来的各种问题。 \ No newline at end of file diff --git a/docs/redis/redis-lua.md b/docs/redis/redis-lua.md new file mode 100644 index 000000000..c6493f95a --- /dev/null +++ b/docs/redis/redis-lua.md @@ -0,0 +1,534 @@ +--- +title: 深入分析Redis Lua脚本运行原理 +author: 哪吒 +date: '2023-07-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# 深入分析Redis Lua脚本运行原理 + +## 1. Lua脚本基础 + +### 1.1 什么是Lua + +Lua是一种轻量级、高效的脚本语言,设计目标是嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎可以在所有操作系统和平台上运行。 + +### 1.2 Lua的主要特性 + +- **轻量级**:Lua解释器只有约200KB +- **高效性**:Lua的执行速度在脚本语言中名列前茅 +- **可嵌入性**:易于嵌入到其他语言和应用中 +- **简洁的语法**:语法简单易学 +- **动态类型**:变量不需要类型定义 +- **自动内存管理**:内置垃圾回收 +- **函数式编程特性**:函数是一等公民 + +### 1.3 Lua在Redis中的基本语法 + +```lua +-- 这是单行注释 +--[[ +这是多行注释 +可以跨越多行 +--]] + +-- 变量和赋值 +local x = 10 +local name = "Redis" + +-- 条件语句 +if x > 5 then + return "大于5" +else + return "小于等于5" +end + +-- 循环 +local sum = 0 +for i = 1, 10 do + sum = sum + i +end + +-- 函数定义 +local function add(a, b) + return a + b +end + +-- 表(Lua中的主要数据结构) +local t = {} +t[1] = "hello" +t[2] = "world" +t.name = "table example" +``` + +## 2. Redis中的Lua脚本 + +### 2.1 为什么Redis需要Lua脚本 + +Redis引入Lua脚本主要解决以下问题: + +1. **原子性操作**:Redis的单个命令是原子性的,但多个命令的组合不是。Lua脚本可以将多个操作打包成一个原子操作。 + +2. **减少网络开销**:使用脚本可以将多个命令一次性发送到Redis服务器,减少网络往返次数。 + +3. **复杂逻辑处理**:某些业务逻辑在客户端实现会很复杂,而使用Lua脚本可以在服务器端直接处理。 + +4. **提高性能**:将复杂操作放在服务器端执行,可以减少客户端与服务器之间的数据传输。 + +### 2.2 Redis中执行Lua脚本的命令 + +Redis提供了两个主要命令来执行Lua脚本: + +#### 2.2.1 EVAL命令 + +``` +EVAL script numkeys key [key ...] arg [arg ...] +``` + +- **script**:Lua脚本内容 +- **numkeys**:键名参数的个数 +- **key**:键名参数列表,在Lua脚本中通过KEYS[1], KEYS[2]等访问 +- **arg**:附加参数列表,在Lua脚本中通过ARGV[1], ARGV[2]等访问 + +示例: + +``` +EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second +``` + +#### 2.2.2 EVALSHA命令 + +``` +EVALSHA sha1 numkeys key [key ...] arg [arg ...] +``` + +- **sha1**:脚本的SHA1校验和 +- 其他参数与EVAL相同 + +EVALSHA命令用于执行已经缓存在Redis服务器中的脚本,避免每次都传输完整的脚本内容。 + +#### 2.2.3 脚本管理命令 + +- **SCRIPT LOAD script**:将脚本加载到脚本缓存,但不执行 +- **SCRIPT EXISTS sha1 [sha1 ...]**:检查脚本是否已缓存 +- **SCRIPT FLUSH**:清空脚本缓存 +- **SCRIPT KILL**:杀死当前正在运行的脚本 + +## 3. Redis Lua脚本的执行机制 + +### 3.1 Lua环境初始化 + +Redis在启动时会初始化一个Lua环境,这个环境是所有客户端共享的。Redis对这个Lua环境做了以下定制: + +1. **沙箱化**:移除了可能造成安全问题的Lua标准库函数 +2. **添加Redis API**:提供了redis.call()和redis.pcall()等函数来执行Redis命令 +3. **随机数控制**:确保脚本的确定性执行 +4. **执行时间限制**:防止脚本执行时间过长 + +### 3.2 脚本的加载与缓存 + +当Redis接收到EVAL命令时,会执行以下步骤: + +1. 计算脚本的SHA1校验和 +2. 检查脚本缓存中是否已存在该校验和 +3. 如果不存在,将脚本编译并存入缓存 +4. 执行脚本 + +而EVALSHA命令则直接从第2步开始,如果缓存中不存在该校验和,会返回错误。 + +### 3.3 脚本执行流程 + +1. **参数传递**:将KEYS和ARGV参数传递给Lua环境 +2. **脚本执行**:在Lua环境中执行脚本 +3. **结果转换**:将Lua返回值转换为Redis协议格式 +4. **返回结果**:将结果返回给客户端 + +### 3.4 Redis与Lua的数据类型映射 + +| Redis类型 | Lua类型 | +|-----------|--------| +| 整数 | 数值 | +| 字符串 | 字符串 | +| 列表 | 表 | +| 哈希表 | 表 | +| 集合 | 表 | +| 有序集合 | 表 | +| NULL | false | + +## 4. Lua脚本的原子性与事务 + +### 4.1 原子性保证 + +Redis保证Lua脚本的原子性,即脚本执行期间,不会有其他脚本或命令执行。这是通过以下机制实现的: + +1. **单线程执行**:Redis的单线程模型确保同一时间只有一个命令在执行 +2. **脚本不可中断**:一旦脚本开始执行,除非使用SCRIPT KILL命令(且脚本未执行写操作),否则不能中断 + +### 4.2 与MULTI/EXEC事务的比较 + +| 特性 | Lua脚本 | MULTI/EXEC事务 | +|------|---------|---------------| +| 原子性 | 支持 | 支持 | +| 隔离性 | 支持 | 支持 | +| 条件判断 | 支持 | 不支持(WATCH命令提供乐观锁) | +| 复杂逻辑 | 支持 | 不支持 | +| 性能 | 较高 | 较低(多次网络往返) | + +### 4.3 脚本超时处理 + +Redis默认不允许脚本执行时间超过一定限制(可通过lua-time-limit配置)。当脚本执行时间过长时: + +1. Redis服务器会开始接受SCRIPT KILL和SHUTDOWN NOSAVE命令 +2. 如果脚本未执行写操作,可以使用SCRIPT KILL终止脚本 +3. 如果脚本已执行写操作,只能使用SHUTDOWN NOSAVE关闭服务器 + +## 5. Lua脚本的性能优化 + +### 5.1 性能优势 + +Lua脚本相比于客户端执行多个命令有以下性能优势: + +1. **减少网络往返**:一次网络请求完成多个操作 +2. **减少上下文切换**:服务器端一次性执行所有操作 +3. **原子性保证**:无需使用WATCH/MULTI/EXEC等机制 + +### 5.2 性能优化技巧 + +1. **使用EVALSHA代替EVAL**:减少脚本传输开销 +2. **最小化脚本复杂度**:保持脚本简单高效 +3. **避免长时间运行**:脚本执行会阻塞Redis服务器 +4. **合理使用redis.call和redis.pcall**:redis.pcall会捕获错误但性能略低 +5. **预加载常用脚本**:使用SCRIPT LOAD预加载常用脚本 + +### 5.3 常见性能陷阱 + +1. **无限循环**:脚本中的无限循环会导致Redis服务器阻塞 +2. **大量数据处理**:在脚本中处理大量数据会消耗大量内存和CPU +3. **频繁调用redis.call**:每次调用都有开销,应尽量减少调用次数 +4. **复杂计算**:Lua不适合进行复杂计算,应将复杂计算放在客户端 + +## 6. Lua脚本的实际应用场景 + +### 6.1 计数器和限流器 + +```lua +-- 简单的限流器:每个用户每分钟最多访问10次 +local user_id = KEYS[1] +local current_time = tonumber(ARGV[1]) +local time_window = 60 -- 60秒 +local max_requests = 10 + +-- 清理过期的访问记录 +redis.call("ZREMRANGEBYSCORE", user_id, 0, current_time - time_window) + +-- 获取当前时间窗口内的访问次数 +local count = redis.call("ZCARD", user_id) + +if count < max_requests then + -- 记录本次访问 + redis.call("ZADD", user_id, current_time, current_time .. ":" .. math.random()) + return 1 -- 允许访问 +else + return 0 -- 拒绝访问 +end +``` + +### 6.2 分布式锁 + +```lua +-- 获取锁 +local lock_key = KEYS[1] +local lock_value = ARGV[1] -- 通常是一个唯一标识符 +local ttl = tonumber(ARGV[2]) -- 锁的过期时间 + +if redis.call("SET", lock_key, lock_value, "NX", "PX", ttl) then + return 1 -- 获取锁成功 +else + return 0 -- 获取锁失败 +end + +-- 释放锁(确保只有锁的持有者才能释放锁) +local lock_key = KEYS[1] +local lock_value = ARGV[1] + +if redis.call("GET", lock_key) == lock_value then + return redis.call("DEL", lock_key) +else + return 0 +end +``` + +### 6.3 原子性计数器更新 + +```lua +-- 原子性地更新多个计数器 +local counter1 = KEYS[1] +local counter2 = KEYS[2] +local counter3 = KEYS[3] +local increment = tonumber(ARGV[1]) + +redis.call("INCRBY", counter1, increment) +redis.call("INCRBY", counter2, increment * 2) +redis.call("INCRBY", counter3, increment * 3) + +return { + redis.call("GET", counter1), + redis.call("GET", counter2), + redis.call("GET", counter3) +} +``` + +### 6.4 复杂数据结构操作 + +```lua +-- 在有序集合中查找并更新元素 +local zset_key = KEYS[1] +local member = ARGV[1] +local new_score = tonumber(ARGV[2]) + +local current_score = redis.call("ZSCORE", zset_key, member) +if current_score then + -- 元素存在,更新分数 + redis.call("ZADD", zset_key, new_score, member) + return {1, new_score - tonumber(current_score)} +else + -- 元素不存在,添加新元素 + redis.call("ZADD", zset_key, new_score, member) + return {0, new_score} +end +``` + +## 7. Lua脚本的调试与测试 + +### 7.1 调试技巧 + +1. **使用redis.log**:在脚本中使用redis.log()函数记录调试信息 + + ```lua + redis.log(redis.LOG_WARNING, "Debug: value = " .. tostring(value)) + ``` + +2. **分步测试**:将复杂脚本分解为简单步骤,逐步测试 + +3. **使用redis-cli --eval**:redis-cli提供了--eval选项来执行Lua脚本文件 + + ```bash + redis-cli --eval script.lua key1 key2 , arg1 arg2 + ``` + +### 7.2 常见错误及解决方案 + +1. **语法错误**:检查Lua语法,特别是括号、引号和关键字 + +2. **类型错误**:确保数据类型转换正确,特别是字符串和数字之间的转换 + +3. **键不存在**:处理键不存在的情况,使用条件判断 + +4. **脚本超时**:优化脚本性能,避免长时间运行 + +5. **内存溢出**:控制数据量,避免在脚本中处理大量数据 + +### 7.3 单元测试 + +可以使用以下方法对Lua脚本进行单元测试: + +1. **使用测试框架**:如Busted(Lua测试框架) + +2. **模拟Redis环境**:创建模拟的redis.call和redis.pcall函数 + +3. **集成测试**:在实际Redis环境中测试脚本 + +## 8. Lua脚本的最佳实践 + +### 8.1 安全性考虑 + +1. **避免使用外部输入**:不要直接将用户输入作为脚本内容 + +2. **限制脚本权限**:使用redis.replicate_commands()控制脚本复制行为 + +3. **设置执行时间限制**:配置lua-time-limit参数 + +4. **避免敏感操作**:不要在脚本中执行FLUSHALL、SHUTDOWN等敏感命令 + +### 8.2 可维护性建议 + +1. **添加注释**:详细注释脚本功能和逻辑 + +2. **模块化**:将复杂脚本分解为小型、可重用的函数 + +3. **版本控制**:使用版本控制系统管理脚本 + +4. **文档化**:记录脚本的用途、参数和返回值 + +### 8.3 部署策略 + +1. **预加载脚本**:在应用启动时预加载常用脚本 + +2. **脚本管理**:使用工具或框架管理脚本 + +3. **监控脚本执行**:监控脚本执行时间和资源消耗 + +4. **灰度发布**:新脚本先在测试环境验证,再逐步部署到生产环境 + +## 9. Redis Lua脚本与Redis模块的比较 + +### 9.1 Lua脚本的局限性 + +1. **功能受限**:只能使用Redis提供的API + +2. **性能限制**:复杂计算会影响Redis性能 + +3. **调试困难**:缺乏完善的调试工具 + +4. **无状态**:每次执行都是独立的,无法保存状态 + +### 9.2 Redis模块的优势 + +1. **更高性能**:C语言编写,直接访问Redis内部API + +2. **功能更强**:可以实现更复杂的功能 + +3. **可以保存状态**:模块可以维护自己的状态 + +4. **更好的集成**:可以与Redis核心功能更紧密集成 + +### 9.3 选择建议 + +- **使用Lua脚本**:简单的原子操作、临时性需求、不需要高性能 + +- **使用Redis模块**:复杂功能、高性能需求、需要保存状态、长期使用 + +## 10. 实战案例:基于Lua脚本的秒杀系统 + +### 10.1 需求分析 + +秒杀系统需要解决的核心问题: + +1. **并发控制**:防止超卖 +2. **性能要求**:高并发、低延迟 +3. **防重复购买**:一个用户只能购买一次 +4. **库存实时性**:库存数据需要实时准确 + +### 10.2 Lua脚本实现 + +```lua +-- 秒杀脚本 +-- KEYS[1]: 商品库存key +-- KEYS[2]: 已购买用户集合key +-- ARGV[1]: 用户ID +-- ARGV[2]: 购买数量 + +local stock_key = KEYS[1] +local purchased_users_key = KEYS[2] +local user_id = ARGV[1] +local quantity = tonumber(ARGV[2]) + +-- 检查用户是否已购买 +if redis.call("SISMEMBER", purchased_users_key, user_id) == 1 then + return {0, "用户已购买"} +end + +-- 检查库存 +local stock = tonumber(redis.call("GET", stock_key) or "0") +if stock < quantity then + return {0, "库存不足"} +end + +-- 扣减库存并记录用户购买 +redis.call("DECRBY", stock_key, quantity) +redis.call("SADD", purchased_users_key, user_id) + +-- 返回成功和剩余库存 +return {1, stock - quantity} +``` + +### 10.3 Java客户端调用示例 + +```java +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import java.util.Arrays; + +public class SecKillService { + private final JedisPool jedisPool; + private final String stockKey = "product:stock:"; + private final String purchasedUsersKey = "product:purchased:users:"; + private final String secKillScript; + private String secKillScriptSha1; + + public SecKillService(JedisPool jedisPool) { + this.jedisPool = jedisPool; + // 秒杀脚本 + this.secKillScript = """ + local stock_key = KEYS[1] + local purchased_users_key = KEYS[2] + local user_id = ARGV[1] + local quantity = tonumber(ARGV[2]) + + if redis.call("SISMEMBER", purchased_users_key, user_id) == 1 then + return {0, "用户已购买"} + end + + local stock = tonumber(redis.call("GET", stock_key) or "0") + if stock < quantity then + return {0, "库存不足"} + end + + redis.call("DECRBY", stock_key, quantity) + redis.call("SADD", purchased_users_key, user_id) + + return {1, stock - quantity} + """; + + // 预加载脚本 + try (Jedis jedis = jedisPool.getResource()) { + this.secKillScriptSha1 = jedis.scriptLoad(secKillScript); + } + } + + public boolean secKill(String productId, String userId, int quantity) { + try (Jedis jedis = jedisPool.getResource()) { + String stockKey = this.stockKey + productId; + String purchasedUsersKey = this.purchasedUsersKey + productId; + + // 执行秒杀脚本 + Object result = jedis.evalsha( + secKillScriptSha1, + Arrays.asList(stockKey, purchasedUsersKey), + Arrays.asList(userId, String.valueOf(quantity)) + ); + + // 解析结果 + if (result instanceof List) { + List resultList = (List) result; + return ((Long) resultList.get(0)) == 1L; + } + + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} +``` + +### 10.4 性能分析 + +使用Lua脚本实现秒杀系统的性能优势: + +1. **原子性操作**:库存检查、扣减和用户记录在一个原子操作中完成 +2. **减少网络往返**:一次网络请求完成所有操作 +3. **服务器端处理**:逻辑在Redis服务器端执行,减轻应用服务器负担 +4. **高并发支持**:Redis单线程模型能高效处理并发请求 + +## 总结 + +Redis Lua脚本是Redis提供的一种强大功能,它允许开发者在Redis服务器端执行Lua脚本,实现复杂的原子操作。通过深入理解Lua脚本的运行原理,开发者可以更好地利用这一功能,提高应用性能,简化业务逻辑实现。 + +在实际应用中,Lua脚本特别适合需要原子性操作、减少网络往返、实现复杂逻辑的场景。但同时也需要注意脚本的性能优化、安全性和可维护性,避免影响Redis服务器的正常运行。 + +随着Redis的不断发展,Lua脚本功能也在不断完善,成为Redis生态系统中不可或缺的一部分。掌握Redis Lua脚本的运行原理和最佳实践,将帮助开发者更好地利用Redis解决实际问题。 \ No newline at end of file diff --git a/docs/sql/README.md b/docs/sql/README.md new file mode 100644 index 000000000..29b59c819 --- /dev/null +++ b/docs/sql/README.md @@ -0,0 +1,84 @@ +--- +title: SQL专属手册 +author: 哪吒 +date: '2024-01-01' +--- + +# SQL专属手册 + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + +## 手册简介 + +本手册是一份全面的SQL中文参考文档,涵盖了从基础语法到高级应用的所有内容。无论你是SQL初学者还是有经验的开发者,都能在这里找到有用的信息。 + +## 内容概览 + +### 📚 基础语法 +- [SQL基础语法](/sql/basic/syntax) +- [数据类型详解](/sql/basic/data-types) +- [DDL数据定义语言](/sql/basic/ddl) +- [DML数据操作语言](/sql/basic/dml) +- [DQL数据查询语言](/sql/basic/dql) +- [DCL数据控制语言](/sql/basic/dcl) + +### 🔍 查询操作 +- [SELECT查询基础](/sql/query/select-basic) +- [WHERE条件查询](/sql/query/where) +- [JOIN表连接](/sql/query/join) +- [子查询](/sql/query/subquery) +- [聚合函数](/sql/query/aggregate) +- [分组查询GROUP BY](/sql/query/group-by) +- [排序ORDER BY](/sql/query/order-by) +- [分页查询LIMIT](/sql/query/limit) + +### 🛠️ 函数大全 +- [字符串函数](/sql/functions/string) +- [数值函数](/sql/functions/numeric) +- [日期时间函数](/sql/functions/datetime) +- [条件函数](/sql/functions/conditional) +- [聚合函数详解](/sql/functions/aggregate) +- [窗口函数](/sql/functions/window) + +### ⚡ 性能优化 +- [索引原理与应用](/sql/optimization/index) +- [查询优化技巧](/sql/optimization/query) +- [执行计划分析](/sql/optimization/explain) +- [SQL性能调优](/sql/optimization/tuning) + +### 🔒 高级特性 +- [事务处理](/sql/advanced/transaction) +- [存储过程](/sql/advanced/procedure) +- [触发器](/sql/advanced/trigger) +- [视图](/sql/advanced/view) +- [CTE公用表表达式](/sql/advanced/cte) + +### 💾 数据库特定 +- [MySQL特性](/sql/database/mysql) +- [PostgreSQL特性](/sql/database/postgresql) +- [SQL Server特性](/sql/database/sqlserver) +- [Oracle特性](/sql/database/oracle) + +### 📖 实战案例 +- [电商系统SQL实战](/sql/examples/ecommerce) +- [数据分析SQL实战](/sql/examples/analytics) +- [报表查询实战](/sql/examples/reports) +- [数据迁移实战](/sql/examples/migration) + +## 快速开始 + +如果你是SQL新手,建议按以下顺序学习: + +1. 从[SQL基础语法](/sql/basic/syntax)开始 +2. 学习[数据类型](/sql/basic/data-types) +3. 掌握[SELECT查询基础](/sql/query/select-basic) +4. 练习[WHERE条件查询](/sql/query/where) +5. 学习[JOIN表连接](/sql/query/join) + +## 贡献指南 + +欢迎大家为SQL专属手册贡献内容!如果你发现错误或有改进建议,请提交[issues](https://github.com/webVueBlog/JavaPlusDoc/issues)。 + +## 版权声明 + +本手册遵循开源协议,仅供学习交流使用。 \ No newline at end of file diff --git a/docs/sre/server-mining-virus-removal.md b/docs/sre/server-mining-virus-removal.md new file mode 100644 index 000000000..a9fe4fc70 --- /dev/null +++ b/docs/sre/server-mining-virus-removal.md @@ -0,0 +1,374 @@ +--- +title: 服务器被挖矿后的应急处理与安全加固指南 +author: 哪吒 +date: '2023-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## 服务器被挖矿后的应急处理与安全加固指南 + +### 1. 挖矿病毒的特征识别 + +服务器被挖矿病毒感染通常会表现出以下特征: + +- **异常的系统资源占用**:CPU使用率异常高(通常接近100%),即使在服务器空闲时也是如此 +- **系统性能下降**:服务器响应缓慢,应用程序运行卡顿 +- **硬件异常**:服务器发热严重,风扇持续高速运转 +- **异常网络连接**:存在与未知IP地址(尤其是境外IP)的连接 +- **隐藏进程**:使用常规工具(如top、htop)无法看到高CPU占用的进程 +- **自动恢复机制**:即使杀死可疑进程,短时间内又会自动重启 + +### 2. 应急响应流程 + +#### 2.1 紧急隔离 + +一旦确认服务器被挖矿病毒感染,应立即采取以下措施: + +```bash +# 1. 断开网络连接(物理断网或禁用网络接口) +ifconfig eth0 down # 禁用网络接口(请根据实际情况替换接口名) + +# 2. 修改所有用户密码,特别是root密码 +passwd root +``` + +> **重要提示**:在处理过程中,建议使用Live CD启动系统进行操作,避免在已感染的系统中直接操作,因为: +> - 修改的密码可能被监听 +> - 修复的文件可能被隐藏的病毒改回 +> - 使用的工具可能已被篡改 +> - 操作过程可能被全程监控 + +#### 2.2 确认感染情况 + +##### 2.2.1 检查高CPU占用进程 + +```bash +# 使用top命令查看高CPU占用进程 +top +# 在top界面按下'c'键可按CPU使用率排序 + +# 使用ps命令查看高CPU占用进程 +ps -eo cmd,pcpu,pid,user --sort -pcpu | head + +# 对于隐藏进程,可使用专用工具 +# 安装sysdig工具 +apt install sysdig # Debian/Ubuntu系统 +yum install sysdig # CentOS/RHEL系统 + +# 使用sysdig查看CPU占用排行 +sysdig -c topprocs_cpu + +# 安装unhide工具 +apt install unhide # Debian/Ubuntu系统 +yum install unhide # CentOS/RHEL系统 + +# 使用unhide查找隐藏进程 +unhide proc +``` + +##### 2.2.2 检查异常网络连接 + +```bash +# 查看所有TCP连接 +ss -anpt +# 或使用netstat(如果可用) +netstat -antp + +# 查看所有UDP连接 +ss -anpu + +# 使用lsof查看网络连接 +lsof -i + +# 使用tcpdump抓包分析 +tcpdump -i <网卡名> host <本地IP> and port <可疑端口> +``` + +##### 2.2.3 检查定时任务 + +```bash +# 查看当前用户的定时任务 +crontab -l + +# 查看所有用户的定时任务 +ls -l /var/spool/cron/* + +# 查看系统定时任务 +cat /etc/crontab +ls -l /etc/cron.d/ +ls -l /etc/cron.hourly/ +ls -l /etc/cron.daily/ +ls -l /etc/cron.weekly/ +ls -l /etc/cron.monthly/ + +# 查看定时任务日志 +tail -f /var/log/cron +``` + +##### 2.2.4 检查启动项和服务 + +```bash +# 检查开机启动脚本 +cat /etc/rc.d/rc.local + +# 检查systemd服务 +ls -l /etc/systemd/system/ +ls -l /etc/systemd/system/multi-user.target.wants/ + +# 对于可疑进程,查看其关联的服务 +systemctl status +``` + +##### 2.2.5 检查异常文件和动态链接库 + +```bash +# 检查/etc/ld.so.preload文件(该文件默认为空) +cat /etc/ld.so.preload + +# 检查可疑的二进制文件(按修改时间排序) +ls -Athl /usr/bin +ls -Athl /usr/sbin + +# 检查可疑的二进制文件(按文件大小排序) +ls -AShl /usr/bin +ls -AShl /usr/sbin +``` + +##### 2.2.6 检查SSH配置和异常公钥 + +```bash +# 检查SSH授权密钥 +cat ~/.ssh/authorized_keys + +# 检查SSH配置 +grep AuthorizedKeysFile /etc/ssh/sshd_config +grep Root /etc/ssh/sshd_config +grep Password /etc/ssh/sshd_config +``` + +### 3. 清除挖矿病毒 + +> **警告**:在执行以下操作前,请确保已备份重要数据。对于严重感染的系统,建议在清理后重装系统。 + +#### 3.1 解锁系统文件 + +```bash +# 解除系统文件的隐藏属性 +chattr -iRa /usr/ /etc/ +``` + +#### 3.2 终止恶意进程 + +```bash +# 终止挖矿进程 +kill -9 + +# 如果进程由服务启动,先停止并禁用服务 +systemctl stop <服务名>.service +systemctl disable <服务名>.service +``` + +#### 3.3 清除恶意文件 + +```bash +# 清空/etc/ld.so.preload文件 +echo "" > /etc/ld.so.preload + +# 删除恶意定时任务 +rm -rf /var/spool/cron/* +chattr +i /var/spool/cron/ # 锁定目录防止再次写入 + +rm -rf /etc/cron.d/* +chattr +i /etc/cron.d/ # 锁定目录防止再次写入 + +# 删除常见挖矿病毒文件 +rm -f /usr/local/lib/libs.so +chattr +i /usr/local/lib # 锁定目录防止再次写入 + +rm -f /var/tmp/kworkerds* +rm -f /var/tmp/1.so +rm -f /tmp/kworkerds* +rm -f /tmp/1.so +rm -f /var/tmp/wc.conf +rm -f /tmp/wc.conf +``` + +#### 3.4 清除异常SSH公钥 + +```bash +# 检查并删除可疑的SSH公钥 +cat ~/.ssh/authorized_keys +# 手动编辑文件删除可疑公钥 +``` + +### 4. 系统安全加固 + +#### 4.1 更新系统和软件 + +```bash +# Debian/Ubuntu系统 +apt update && apt upgrade -y + +# CentOS/RHEL系统 +yum update -y +``` + +#### 4.2 加固SSH服务 + +```bash +# 编辑SSH配置文件 +vi /etc/ssh/sshd_config + +# 推荐的安全配置 +PermitRootLogin no # 禁止root直接登录 +PasswordAuthentication no # 禁用密码认证,使用密钥认证 +Port 22345 # 修改默认SSH端口 +AllowUsers user1 user2 # 限制允许登录的用户 +MaxAuthTries 3 # 最大认证尝试次数 +ClientAliveInterval 300 # 客户端活跃检测间隔 +ClientAliveCountMax 0 # 客户端活跃检测计数 + +# 重启SSH服务 +systemctl restart sshd +``` + +#### 4.3 配置防火墙 + +```bash +# 安装并启用防火墙 +# Debian/Ubuntu系统 +apt install ufw +ufw enable +ufw default deny incoming +ufw default allow outgoing +ufw allow 22345/tcp # 允许SSH端口(使用修改后的端口) +ufw allow 80/tcp # 允许HTTP端口(根据需要配置) +ufw allow 443/tcp # 允许HTTPS端口(根据需要配置) + +# CentOS/RHEL系统 +yum install firewalld +systemctl enable firewalld +systemctl start firewalld +firewall-cmd --permanent --add-port=22345/tcp # 允许SSH端口(使用修改后的端口) +firewall-cmd --permanent --add-port=80/tcp # 允许HTTP端口(根据需要配置) +firewall-cmd --permanent --add-port=443/tcp # 允许HTTPS端口(根据需要配置) +firewall-cmd --reload +``` + +#### 4.4 安装入侵检测和防御工具 + +```bash +# 安装Fail2Ban防止暴力破解 +# Debian/Ubuntu系统 +apt install fail2ban + +# CentOS/RHEL系统 +yum install epel-release +yum install fail2ban + +# 配置Fail2Ban +cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local +vi /etc/fail2ban/jail.local + +# 启动Fail2Ban +systemctl enable fail2ban +systemctl start fail2ban + +# 安装ClamAV防病毒软件 +# Debian/Ubuntu系统 +apt install clamav clamav-daemon + +# CentOS/RHEL系统 +yum install epel-release +yum install clamav clamav-update + +# 更新病毒库 +freshclam + +# 扫描系统 +clamscan -r --bell -i / +``` + +#### 4.5 锁定关键目录 + +```bash +# 使用chattr命令锁定关键目录和文件 +chattr +i /etc/passwd +chattr +i /etc/shadow +chattr +i /etc/group +chattr +i /etc/gshadow +chattr +i /etc/ssh/sshd_config +``` + +### 5. 长期防护措施 + +#### 5.1 定期更新和补丁管理 + +- 建立定期更新系统和应用程序的计划 +- 关注安全公告,及时应用安全补丁 +- 对于关键系统,在应用补丁前进行测试 + +#### 5.2 定期备份 + +- 实施3-2-1备份策略:3份数据副本,2种不同的存储介质,1份异地备份 +- 定期测试备份的可恢复性 +- 确保备份数据的安全性(加密、访问控制) + +#### 5.3 安全监控 + +- 部署集中式日志管理系统 +- 配置关键事件的告警机制 +- 定期审查系统日志和安全事件 + +```bash +# 安装auditd进行系统审计 +# Debian/Ubuntu系统 +apt install auditd + +# CentOS/RHEL系统 +yum install audit + +# 启用auditd服务 +systemctl enable auditd +systemctl start auditd +``` + +#### 5.4 最小权限原则 + +- 仅安装必要的软件包 +- 关闭不需要的服务和端口 +- 为用户分配最小必要的权限 +- 使用非特权用户运行应用程序 + +#### 5.5 定期安全审计 + +- 定期进行漏洞扫描 +- 执行安全基线检查 +- 进行渗透测试评估系统安全性 + +```bash +# 使用Lynis进行安全审计 +# Debian/Ubuntu系统 +apt install lynis + +# CentOS/RHEL系统 +yum install lynis + +# 运行Lynis审计 +lynis audit system +``` + +### 6. 总结 + +服务器被挖矿病毒感染后的处理不仅仅是清除病毒,更重要的是找出入侵途径并加以修复,同时加强系统安全防护。对于严重感染的系统,建议在备份重要数据后重装系统,以确保彻底清除所有恶意代码。 + +安全是一个持续的过程,需要定期的维护、更新和审计。通过实施本文提供的安全加固措施,可以显著降低服务器被挖矿病毒感染的风险。 + +### 7. 参考资料 + +- [Linux应急响应:挖矿病毒处理](https://www.secpulse.com/archives/76825.html) +- [Linux服务器挖矿病毒清除全攻略](https://www.yunweipai.com/47241.html) +- [挖矿病毒处置(Linux篇)](https://wlaq.xjtu.edu.cn/info/1008/1945.htm) \ No newline at end of file diff --git a/docs/tech/high-availability-group-buying.md b/docs/tech/high-availability-group-buying.md new file mode 100644 index 000000000..9a86bc9a4 --- /dev/null +++ b/docs/tech/high-availability-group-buying.md @@ -0,0 +1,185 @@ +--- +title: 500万日订单下的高可用拼购系统 +date: 2023-01-01 +--- + +# 500万日订单下的高可用拼购系统 + +## 系统概述 + +拼购系统是电商平台中的重要业务模式,通过用户拼团购买来获取更低价格,同时商家可以快速获取大量订单。在日订单量达到500万级别的情况下,系统面临极大的并发压力和稳定性挑战。本文将详细介绍如何设计和实现一个能够支撑如此大规模交易量的高可用拼购系统。 + +## 业务挑战 + +1. **高并发**:秒杀、限时特惠等活动期间,系统面临瞬时高并发请求 +2. **数据一致性**:拼团状态、库存、订单等数据需要保持强一致性 +3. **高可用性**:系统不能因为单点故障而影响整体业务 +4. **低延迟**:用户操作需要快速响应,尤其是支付和成团确认环节 +5. **防作弊**:需要防止恶意刷单、机器人抢购等行为 + +## 系统架构设计 + +### 整体架构 + +采用微服务架构,将拼购系统拆分为多个独立服务: + +1. **用户服务**:处理用户认证、授权和信息管理 +2. **商品服务**:管理商品信息、价格和库存 +3. **拼团服务**:核心服务,负责拼团活动创建、用户参团、成团逻辑 +4. **订单服务**:处理订单创建、支付和履约 +5. **支付服务**:对接各支付渠道,处理支付和退款 +6. **消息服务**:负责系统内部和对外的消息通知 +7. **风控服务**:识别和防范异常行为和欺诈活动 + +### 技术栈选择 + +- **应用层**:Spring Cloud微服务生态 +- **服务注册与发现**:Nacos +- **配置中心**:Nacos Config +- **网关**:Spring Cloud Gateway +- **负载均衡**:Ribbon + Feign +- **熔断降级**:Sentinel +- **分布式事务**:Seata +- **消息队列**:RocketMQ +- **缓存**:Redis Cluster +- **数据库**:MySQL + 分库分表(Sharding-JDBC) +- **搜索引擎**:Elasticsearch +- **监控系统**:Prometheus + Grafana +- **链路追踪**:SkyWalking + +## 高可用设计 + +### 多级缓存策略 + +1. **本地缓存**:使用Caffeine实现应用内缓存,减轻Redis压力 +2. **分布式缓存**:Redis集群,采用主从+哨兵模式 +3. **热点数据预加载**:活动开始前预热商品、活动规则等数据 +4. **缓存穿透防护**:布隆过滤器 + 空值缓存 +5. **缓存雪崩防护**:过期时间错峰设置 + 熔断降级 + +### 数据库高可用 + +1. **读写分离**:主库写入,从库读取 +2. **分库分表**:按用户ID和商品ID水平分片 +3. **分布式ID生成**:雪花算法(Snowflake)生成全局唯一ID +4. **柔性事务**:最终一致性模型,通过消息队列+补偿机制实现 +5. **数据库中间件**:使用Sharding-JDBC实现分库分表和读写分离 + +### 限流降级 + +1. **接口限流**:Sentinel实现接口级别限流 +2. **分布式限流**:Redis+Lua脚本实现全局限流 +3. **熔断策略**:服务调用失败率达到阈值自动熔断 +4. **降级方案**:核心功能保障,非核心功能可降级 +5. **排队机制**:高峰期请求排队处理,避免系统崩溃 + +### 异步化设计 + +1. **请求异步化**:非核心流程异步处理 +2. **结果异步通知**:拼团成功、订单状态变更等通过消息队列异步通知 +3. **定时任务**:使用分布式调度框架(XXL-Job)处理定时拼团检查、订单超时等任务 + +## 核心业务流程优化 + +### 拼团流程优化 + +1. **预创建拼团**:活动开始前预创建拼团,减少活动开始时的数据库压力 +2. **状态机设计**:拼团状态流转采用状态机模式,确保状态一致性 +3. **异步成团**:用户支付成功后,异步判断是否成团 +4. **定时检查**:定时任务检查未成团的拼团,到期自动关闭或退款 + +### 库存管理优化 + +1. **预扣库存**:下单时预扣库存,支付超时自动释放 +2. **库存分片**:大库存商品分片存储,减少库存争用 +3. **Redis预减库存**:高并发场景下,先在Redis中扣减库存,再异步同步到数据库 + +### 订单系统优化 + +1. **订单分库分表**:按用户ID分片,提高查询效率 +2. **订单状态异步更新**:订单状态变更通过消息队列异步处理 +3. **订单号生成优化**:分布式ID生成器,避免ID生成成为瓶颈 + +## 系统容灾方案 + +### 多活部署 + +1. **同城双活**:核心服务在同城两个数据中心部署 +2. **异地多活**:关键业务实现异地多活架构 +3. **容灾演练**:定期进行容灾切换演练 + +### 降级预案 + +1. **功能降级**:定义多级降级方案,保障核心交易流程 +2. **限流降级**:流量超出系统承载能力时自动限流 +3. **页面静态化**:高峰期商品详情页静态化,减轻服务器压力 + +## 性能优化 + +### 前端优化 + +1. **静态资源CDN**:使用CDN加速静态资源加载 +2. **页面预渲染**:活动页面预渲染,提高首屏加载速度 +3. **接口合并**:减少HTTP请求次数 +4. **Progressive Web App**:实现PWA,提升移动端体验 + +### 后端优化 + +1. **接口异步化**:非关键路径接口异步处理 +2. **批量处理**:批量查询和更新,减少数据库交互次数 +3. **索引优化**:针对查询场景优化数据库索引 +4. **连接池调优**:优化数据库、Redis等连接池配置 + +## 监控与告警 + +### 全链路监控 + +1. **业务监控**:拼团转化率、支付成功率等业务指标 +2. **系统监控**:CPU、内存、磁盘IO等系统指标 +3. **接口监控**:接口响应时间、成功率、QPS等 +4. **链路追踪**:使用SkyWalking实现分布式调用链追踪 + +### 智能告警 + +1. **多级告警**:按照严重程度分级告警 +2. **告警聚合**:相同类型告警聚合,避免告警风暴 +3. **智能降噪**:使用算法过滤无效告警 + +## 安全防护 + +### 接口安全 + +1. **接口加密**:敏感接口参数加密传输 +2. **防重放攻击**:请求增加时间戳和nonce参数 +3. **接口幂等性**:确保接口可重复调用但不会产生副作用 + +### 风控系统 + +1. **实时风控**:交易环节实时风险识别 +2. **用户画像**:基于用户行为构建风险画像 +3. **规则引擎**:灵活配置风控规则 + +## 实践经验与教训 + +### 成功经验 + +1. **容量规划**:提前进行容量规划和压力测试 +2. **灰度发布**:新功能先小范围灰度,再全量发布 +3. **故障演练**:定期进行故障演练,提前发现问题 + +### 踩过的坑 + +1. **缓存设计不合理**:导致缓存雪崩 +2. **数据库连接池配置不当**:高峰期连接耗尽 +3. **分布式锁使用不当**:导致死锁或性能问题 + +## 未来规划 + +1. **服务网格**:引入Service Mesh简化服务治理 +2. **云原生改造**:容器化和Kubernetes编排 +3. **实时数据分析**:引入实时计算平台,提升数据分析能力 +4. **智能运维**:AIOps实现智能化运维 + +## 总结 + +构建支撑500万日订单的高可用拼购系统,需要从架构设计、技术选型、业务优化、监控告警等多方面综合考虑。通过合理的系统设计和优化,可以构建出高性能、高可用、可扩展的拼购系统,为用户提供流畅的购物体验,同时为企业创造更大的商业价值。 \ No newline at end of file diff --git a/docs/tech/twenty-million-orders-architecture.md b/docs/tech/twenty-million-orders-architecture.md new file mode 100644 index 000000000..7c91bc045 --- /dev/null +++ b/docs/tech/twenty-million-orders-architecture.md @@ -0,0 +1,384 @@ +--- +title: 2000万日订单背后的技术架构 +date: 2023-06-01 +--- + +# 2000万日订单背后的技术架构 + +## 系统概述 + +在电商、支付、物流等大型互联网平台中,日订单量达到2000万级别是一个重要的技术里程碑。这意味着系统每秒需要处理超过230笔订单,在活动高峰期可能达到每秒数千笔。本文将详细介绍如何设计和实现一个能够支撑如此大规模交易量的高可用、高性能系统架构。 + +## 业务挑战 + +1. **超高并发**:峰值期间系统面临每秒数千甚至上万的并发请求 +2. **海量数据**:每日产生的订单数据、日志数据达到TB级别 +3. **极致可用性**:系统需要保证99.99%以上的可用性,年度不可用时间不超过52分钟 +4. **全球化部署**:支持跨地域、跨国家的业务场景 +5. **复杂业务逻辑**:订单涉及商品、库存、支付、物流等多个环节的协同 +6. **安全与合规**:需要满足不同国家和地区的数据安全与合规要求 + +## 系统架构设计 + +### 整体架构 + +采用云原生微服务架构,将系统拆分为多个独立的业务域: + +1. **用户域**:负责用户认证、授权、信息管理和用户画像 +2. **商品域**:管理商品信息、价格、库存和商品推荐 +3. **订单域**:处理订单创建、支付、履约和售后 +4. **支付域**:对接各支付渠道,处理支付、退款和结算 +5. **物流域**:管理仓储、配送和物流跟踪 +6. **营销域**:负责促销活动、优惠券和会员积分 +7. **搜索域**:提供高性能的商品搜索和个性化推荐 +8. **风控域**:识别和防范欺诈行为,保障交易安全 + +### 技术栈选择 + +- **应用层**:Spring Cloud Alibaba微服务生态 +- **服务网格**:Istio +- **服务注册与发现**:Nacos +- **配置中心**:Apollo +- **API网关**:Spring Cloud Gateway + Kong +- **负载均衡**:F5 + Nginx + Client-side Load Balancing +- **熔断降级**:Sentinel + Hystrix +- **分布式事务**:Seata +- **消息队列**:Apache Pulsar + Kafka +- **缓存**:多级缓存架构(本地缓存 + Redis Cluster + 全局缓存) +- **数据库**:分库分表(MySQL) + 时序数据库(InfluxDB) + 图数据库(Neo4j) +- **搜索引擎**:Elasticsearch +- **大数据处理**:Flink + Spark + Hadoop +- **监控系统**:Prometheus + Grafana + SkyWalking +- **容器编排**:Kubernetes +- **CI/CD**:Jenkins + GitLab CI + Argo CD + +## 高可用设计 + +### 多级缓存架构 + +1. **L1: 应用内缓存** + - 使用Caffeine实现JVM内缓存 + - 热点数据本地缓存,减少网络开销 + - 采用自适应过期策略 + +2. **L2: 分布式缓存** + - Redis Cluster多主多从架构 + - 跨机房部署,实现同城双活 + - 数据分片,单集群支持TB级数据 + +3. **L3: 全局缓存** + - 使用CDN缓存静态资源 + - 边缘计算节点缓存准静态数据 + - 全球化部署,就近访问 + +4. **缓存防护措施** + - 缓存穿透:布隆过滤器 + 空值缓存 + - 缓存击穿:互斥锁 + 热点数据永不过期 + - 缓存雪崩:过期时间随机化 + 多级缓存兜底 + - 缓存预热:系统启动和活动前预加载热点数据 + +### 数据库高可用 + +1. **存储架构** + - 按业务域垂直分库 + - 单库内部水平分表,单表控制在千万级 + - 冷热数据分离,历史数据归档 + +2. **读写分离** + - 一主多从架构 + - 读写分离中间件:MyCat + Sharding-JDBC + - 从库分担读请求,主库专注写入 + +3. **分库分表策略** + - 订单表:按用户ID哈希分库,按时间分表 + - 商品表:按商品ID范围分库 + - 用户表:按用户ID哈希分库 + +4. **数据一致性保障** + - 强一致性场景:分布式事务(Seata) + - 最终一致性场景:事务消息 + 补偿机制 + - 弱一致性场景:异步更新 + 定时校对 + +### 流量治理 + +1. **多层次限流** + - 接入层限流:WAF + API网关 + - 应用层限流:Sentinel + - 资源层限流:数据库连接池 + 线程池 + +2. **智能限流策略** + - 基于QPS的限流 + - 基于并发数的限流 + - 基于调用关系的限流 + - 基于用户特征的限流 + +3. **流量整形** + - 削峰填谷:请求排队 + 延迟处理 + - 优先级队列:核心业务优先处理 + - 令牌桶算法:允许短时突发流量 + +4. **熔断降级** + - 服务级熔断:服务调用失败率超阈值自动熔断 + - 接口级降级:非核心接口自动降级 + - 功能级降级:非核心功能在高峰期自动关闭 + +### 多活容灾 + +1. **同城双活** + - 两个数据中心实时同步数据 + - 任一中心故障,另一中心可完全接管业务 + - 流量自动调度,无需人工干预 + +2. **异地多活** + - 三地五中心部署 + - 数据分区,就近读写 + - 跨地域数据同步,保证最终一致性 + +3. **灾备策略** + - 定期数据备份:全量 + 增量 + - 自动化灾备演练 + - 完善的故障转移机制 + +## 核心业务流程优化 + +### 订单处理流水线 + +1. **前端优化** + - 订单提交前客户端预校验 + - 大订单分批提交 + - 防重复提交机制 + +2. **订单创建优化** + - 异步化:非核心步骤异步处理 + - 并行化:多个独立步骤并行执行 + - 批量化:小订单合并处理 + +3. **订单状态流转** + - 基于状态机的订单生命周期管理 + - 状态变更事件驱动后续流程 + - 订单状态实时可查 + +### 库存管理优化 + +1. **多级库存模型** + - 实物库存:实际仓库中的商品数量 + - 可售库存:考虑预占因素后可售卖的数量 + - 预售库存:未到货但可售卖的数量 + +2. **库存扣减策略** + - 预扣库存:下单时预扣,支付超时自动释放 + - 库存分片:热门商品库存分片存储,减少锁争用 + - 库存缓存:Redis预减库存,异步同步到数据库 + +3. **库存一致性保障** + - 定时库存对账 + - 库存变更事务消息 + - 库存告警和自动补货 + +### 支付系统优化 + +1. **支付路由** + - 智能支付渠道选择 + - 支付渠道实时监控和自动切换 + - 多渠道支付失败自动重试 + +2. **支付流程优化** + - 预授权机制:先冻结资金,后扣款 + - 支付分流:高峰期按用户级别分配支付资源 + - 阶段性结算:大额支付分批次完成 + +3. **支付安全** + - 交易加密:全链路数据加密 + - 风险控制:实时交易风险评估 + - 异常监控:异常支付行为实时预警 + +## 性能优化 + +### 应用层优化 + +1. **代码级优化** + - 算法优化:时间复杂度从O(n²)优化到O(n) + - 内存优化:减少对象创建,避免频繁GC + - 并发优化:合理使用线程池和异步编程 + +2. **JVM优化** + - 内存分配:根据业务特点调整各代内存比例 + - GC策略:选择适合业务特点的垃圾收集器 + - JIT编译:预热热点代码路径 + +3. **框架优化** + - 精简依赖:移除不必要的组件 + - 按需加载:延迟初始化非核心组件 + - 参数调优:根据实际场景优化框架参数 + +### 数据层优化 + +1. **SQL优化** + - 索引优化:为查询场景设计合适的索引 + - 查询重写:复杂查询拆分或重构 + - 执行计划优化:分析并优化慢查询 + +2. **数据访问优化** + - 批量操作:合并多次数据库访问 + - 延迟加载:按需加载关联数据 + - 结果集缓存:缓存频繁查询的结果 + +3. **存储优化** + - 数据压缩:减少存储空间和I/O开销 + - 分区表:按时间或范围分区,提高查询效率 + - 冷热数据分离:热数据使用高性能存储 + +### 网络优化 + +1. **协议优化** + - HTTP/2:多路复用,头部压缩 + - gRPC:高效的二进制协议,适用于服务间通信 + - WebSocket:长连接,减少握手开销 + +2. **传输优化** + - 数据压缩:gzip, Brotli等压缩算法 + - 增量传输:只传输变化的数据 + - 批量传输:合并多个小请求 + +3. **网络拓扑优化** + - 服务就近部署:减少网络延迟 + - 专线连接:核心服务间使用专线通信 + - 流量调度:智能DNS + 全球负载均衡 + +## 监控与运维 + +### 全方位监控 + +1. **基础设施监控** + - 服务器:CPU、内存、磁盘、网络 + - 中间件:数据库、缓存、消息队列 + - 网络设备:交换机、路由器、负载均衡器 + +2. **应用监控** + - 服务健康:存活状态、响应时间 + - 接口监控:调用量、成功率、耗时 + - 资源监控:线程池、连接池、内存使用 + +3. **业务监控** + - 核心指标:订单量、支付成功率、物流时效 + - 用户体验:页面加载时间、操作响应时间 + - 异常指标:下单失败率、支付超时率 + +4. **链路追踪** + - 分布式调用链:SkyWalking + Zipkin + - 性能瓶颈分析:热点方法、慢SQL + - 异常定位:错误传播路径追踪 + +### 智能运维 + +1. **自动化运维** + - 自动部署:CI/CD流水线 + - 自动扩缩容:基于负载的弹性伸缩 + - 自动切换:故障自动转移 + +2. **智能告警** + - 多维度告警:基于阈值、趋势和异常检测 + - 告警抑制:相似告警合并,避免告警风暴 + - 告警升级:按严重程度自动升级 + +3. **AIOps实践** + - 异常检测:基于机器学习的异常识别 + - 根因分析:自动定位故障根源 + - 预测性维护:预测潜在问题并提前处理 + +## 安全防护 + +### 多层次安全架构 + +1. **网络安全** + - DDoS防护:流量清洗 + CDN + - WAF:防SQL注入、XSS等Web攻击 + - 安全组:精细化访问控制 + +2. **应用安全** + - 身份认证:多因素认证 + - 权限控制:RBAC + ABAC + - 数据加密:传输加密 + 存储加密 + +3. **数据安全** + - 数据分类:按敏感度分级管理 + - 数据脱敏:敏感信息展示和传输时脱敏 + - 数据审计:关键操作全程记录 + +### 风控系统 + +1. **实时风控** + - 规则引擎:配置化风控规则 + - 实时计算:毫秒级风险评估 + - 多维度特征:IP、设备、行为、交易等 + +2. **智能风控** + - 机器学习模型:异常检测、欺诈识别 + - 图计算:社交网络分析,识别团伙欺诈 + - 知识图谱:构建风险知识库 + +3. **风控策略** + - 分级处理:不同风险等级采取不同措施 + - 柔性控制:风险提示 + 二次验证 + - 阶梯式防御:逐步升级防御措施 + +## 实践经验与教训 + +### 成功经验 + +1. **架构演进** + - 渐进式微服务改造:先拆分核心模块,再逐步扩展 + - 双轨并行:新旧系统并行运行,平滑迁移 + - 持续优化:根据实际运行数据不断调整架构 + +2. **技术选型** + - 适合业务特点:选择与业务场景匹配的技术栈 + - 成熟可靠:优先选择经过大规模验证的技术 + - 团队熟悉度:考虑团队技术栈和学习曲线 + +3. **团队协作** + - DevOps文化:开发与运维紧密协作 + - 敏捷开发:小步快跑,快速迭代 + - 知识共享:技术沙龙,经验分享 + +### 踩过的坑 + +1. **技术陷阱** + - 过度设计:不必要的复杂架构增加维护成本 + - 技术偏好:为技术而技术,忽视业务需求 + - 性能误区:过早优化,优化错方向 + +2. **运维挑战** + - 监控盲区:关键指标缺失,问题难以发现 + - 变更风险:大规模变更引发连锁故障 + - 容量规划:低估业务增长,资源不足 + +3. **团队问题** + - 沟通不畅:跨团队协作效率低 + - 技能短板:关键技术缺乏专家 + - 责任模糊:问题出现互相推诿 + +## 未来规划 + +1. **技术升级** + - 全面云原生:容器化 + 服务网格 + Serverless + - 智能化运维:AIOps全面应用 + - 新一代数据架构:实时数据湖 + +2. **业务拓展** + - 全球化部署:多地区多中心架构 + - 全渠道融合:线上线下一体化体验 + - 生态开放:API经济,合作伙伴集成 + +3. **创新探索** + - 区块链应用:供应链溯源,跨境支付 + - 边缘计算:终端智能化,体验提升 + - 人工智能:个性化推荐,智能客服 + +## 总结 + +构建支撑2000万日订单的技术架构,不仅是技术挑战,更是对团队、流程和文化的全方位考验。通过合理的架构设计、技术选型和持续优化,我们成功构建了高可用、高性能、可扩展的系统,为业务持续增长提供了坚实的技术基础。 + +在这个过程中,我们不断学习、调整和创新,形成了一套适合大规模交易系统的最佳实践。这些经验不仅应用于当前系统,也将指导未来更大规模系统的设计和实现。 + +技术架构永远没有终点,只有不断演进的过程。面向未来,我们将继续拥抱新技术、新理念,构建更强大、更智能的系统,支撑业务向更高目标迈进。 \ No newline at end of file diff --git a/docs/thread/threadpool-executor.md b/docs/thread/threadpool-executor.md new file mode 100644 index 000000000..966210a73 --- /dev/null +++ b/docs/thread/threadpool-executor.md @@ -0,0 +1,317 @@ +--- +title: 深入浅出Java线程池ThreadPoolExecutor +author: 哪吒 +date: '2023-05-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Java线程池原理 + +在高并发环境下,频繁创建和销毁线程会带来极大的性能开销。线程池通过复用已创建的线程,可以显著提高系统性能。Java中的`ThreadPoolExecutor`是线程池的核心实现类,它提供了强大的线程池管理功能。 + +### 为什么需要线程池 + +* **降低资源消耗**:通过复用已创建的线程,减少线程创建和销毁的开销 +* **提高响应速度**:任务到达时,无需等待线程创建即可立即执行 +* **提高线程的可管理性**:统一管理线程,避免无限制创建线程导致的系统崩溃 +* **提供更多更强大的功能**:如延时执行、定期执行、监控等 + +## ThreadPoolExecutor核心参数 + +`ThreadPoolExecutor`构造函数有7个参数,每个参数都对线程池的行为有重要影响: + +```java +public ThreadPoolExecutor( + int corePoolSize, // 核心线程数 + int maximumPoolSize, // 最大线程数 + long keepAliveTime, // 线程空闲时间 + TimeUnit unit, // 时间单位 + BlockingQueue workQueue, // 工作队列 + ThreadFactory threadFactory, // 线程工厂 + RejectedExecutionHandler handler // 拒绝策略 +) +``` + +### 核心参数详解 + +#### 1. corePoolSize(核心线程数) + +线程池中应该保持活跃的线程数量,即使它们处于空闲状态。只有当`allowCoreThreadTimeOut`设置为true时,核心线程在空闲超时后才会被回收。 + +#### 2. maximumPoolSize(最大线程数) + +线程池允许创建的最大线程数。当工作队列已满且活动线程数小于最大线程数时,线程池会创建新线程来处理任务。 + +#### 3. keepAliveTime(线程空闲时间) + +当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。 + +#### 4. unit(时间单位) + +`keepAliveTime`参数的时间单位,如`TimeUnit.SECONDS`、`TimeUnit.MILLISECONDS`等。 + +#### 5. workQueue(工作队列) + +用于保存等待执行的任务的阻塞队列。常用的队列有: + +* **ArrayBlockingQueue**:基于数组的有界阻塞队列,按FIFO原则对元素进行排序 +* **LinkedBlockingQueue**:基于链表的阻塞队列,按FIFO排序,吞吐量通常高于ArrayBlockingQueue +* **SynchronousQueue**:不存储元素的阻塞队列,每个插入操作必须等待另一个线程调用移除操作 +* **PriorityBlockingQueue**:具有优先级的无界阻塞队列 +* **DelayQueue**:用于延迟执行任务的无界阻塞队列 + +#### 6. threadFactory(线程工厂) + +用于创建新线程的工厂。通过自定义ThreadFactory,可以给线程设置有意义的名称、设置守护状态或优先级等。 + +#### 7. handler(拒绝策略) + +当线程池和工作队列都已满时,对新提交任务的处理策略。Java提供了四种标准拒绝策略: + +* **AbortPolicy**:默认策略,抛出RejectedExecutionException异常 +* **CallerRunsPolicy**:在调用者线程中执行任务,有反馈调节机制 +* **DiscardPolicy**:直接丢弃新任务,不做任何处理 +* **DiscardOldestPolicy**:丢弃队列头部(最旧)的任务,然后重试执行当前任务 + +## 线程池工作原理 + +![线程池工作流程](./img.png) + +### 线程池执行流程 + +1. **提交任务**:当任务被提交到线程池时 +2. **核心线程处理**:如果运行的线程数少于核心线程数,则创建新线程来处理任务,即使其他线程是空闲的 +3. **工作队列缓存**:如果运行的线程数等于或多于核心线程数,则将任务加入工作队列而不是创建新线程 +4. **创建临时线程**:如果工作队列已满,且运行的线程数少于最大线程数,则创建新线程来处理任务 +5. **触发拒绝策略**:如果工作队列已满,且运行的线程数等于或多于最大线程数,则根据拒绝策略处理该任务 + +### 线程池状态 + +`ThreadPoolExecutor`使用一个原子整数`ctl`同时记录线程池状态和工作线程数量: + +* **RUNNING**:接受新任务并处理队列中的任务 +* **SHUTDOWN**:不接受新任务,但处理队列中的任务 +* **STOP**:不接受新任务,不处理队列中的任务,中断正在执行的任务 +* **TIDYING**:所有任务已终止,工作线程数为0,线程转换到此状态后会调用`terminated()`方法 +* **TERMINATED**:`terminated()`方法执行完成 + +## 常见线程池类型 + +Java通过`Executors`工厂类提供了几种预定义的线程池配置: + +### 1. FixedThreadPool + +```java +ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads); +``` + +特点: +* 核心线程数等于最大线程数,即线程数固定 +* 使用无界队列LinkedBlockingQueue +* 适用于负载较重的服务器,固定线程数有助于防止资源耗尽 + +### 2. CachedThreadPool + +```java +ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); +``` + +特点: +* 核心线程数为0,最大线程数为Integer.MAX_VALUE +* 使用SynchronousQueue,不存储任务 +* 线程空闲60秒后回收 +* 适用于执行大量短期异步任务的程序 + +### 3. SingleThreadExecutor + +```java +ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); +``` + +特点: +* 核心线程数和最大线程数都为1 +* 使用无界队列LinkedBlockingQueue +* 适用于需要保证顺序执行各个任务的应用场景 + +### 4. ScheduledThreadPool + +```java +ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize); +``` + +特点: +* 核心线程数固定,最大线程数为Integer.MAX_VALUE +* 使用DelayedWorkQueue +* 适用于需要定期执行任务的场景 + +## 线程池的正确使用 + +### 线程池大小设置 + +线程池大小的设置需要考虑多种因素: + +* **CPU密集型任务**:线程数 = CPU核心数 + 1 +* **IO密集型任务**:线程数 = CPU核心数 * (1 + 平均等待时间/平均工作时间) + +一个简单的经验公式:线程数 = CPU核心数 * (1 + 等待时间/计算时间) + +### 避免使用Executors创建线程池 + +虽然`Executors`提供了便捷的工厂方法,但在生产环境中应避免直接使用,原因如下: + +* **FixedThreadPool和SingleThreadExecutor**:使用无界队列LinkedBlockingQueue,可能导致OOM +* **CachedThreadPool**:最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM +* **ScheduledThreadPool**:最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM + +建议直接使用`ThreadPoolExecutor`构造函数,明确指定各个参数。 + +## 线程池监控 + +`ThreadPoolExecutor`提供了多种方法来监控线程池的运行状态: + +```java +// 获取线程池当前线程数 +int getPoolSize() + +// 获取活动线程数 +int getActiveCount() + +// 获取完成任务数 +long getCompletedTaskCount() + +// 获取任务总数 +long getTaskCount() + +// 获取队列中等待执行的任务数 +int getQueue().size() +``` + +可以通过继承`ThreadPoolExecutor`并重写`beforeExecute`、`afterExecute`和`terminated`方法来添加自定义监控逻辑。 + +## 线程池最佳实践 + +1. **根据业务场景,合理设置线程池参数** +2. **使用有界队列,防止OOM** +3. **根据任务类型(CPU密集型、IO密集型)设置合适的线程数** +4. **为线程池里的线程指定有意义的名称,方便问题排查** +5. **根据实际情况实现自定义拒绝策略** +6. **关注线程池的监控指标,及时调整参数** +7. **优雅关闭线程池**:先调用`shutdown()`,再调用`awaitTermination()`等待任务执行完成 + +## 实际应用案例 + +### 案例一:自定义线程池 + +```java +// 创建自定义线程池 +ThreadPoolExecutor executor = new ThreadPoolExecutor( + // 核心线程数 + Runtime.getRuntime().availableProcessors(), + // 最大线程数 + Runtime.getRuntime().availableProcessors() * 2, + // 线程空闲时间 + 60L, + // 时间单位 + TimeUnit.SECONDS, + // 工作队列 + new ArrayBlockingQueue<>(1000), + // 线程工厂 + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "custom-thread-" + threadNumber.getAndIncrement()); + // 设置为非守护线程 + t.setDaemon(false); + // 设置线程优先级 + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + }, + // 拒绝策略 + new ThreadPoolExecutor.CallerRunsPolicy() +); + +// 提交任务 +executor.execute(() -> { + System.out.println("任务正在执行..."); +}); + +// 关闭线程池 +executor.shutdown(); +``` + +### 案例二:处理异步任务结果 + +```java +// 创建线程池 +ExecutorService executor = new ThreadPoolExecutor( + 5, 10, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.AbortPolicy()); + +// 提交有返回值的任务 +Future future = executor.submit(() -> { + // 模拟耗时操作 + Thread.sleep(1000); + return "任务执行结果"; +}); + +try { + // 获取任务执行结果,最多等待2秒 + String result = future.get(2, TimeUnit.SECONDS); + System.out.println("获取到结果: " + result); +} catch (InterruptedException e) { + // 当前线程被中断 + Thread.currentThread().interrupt(); +} catch (ExecutionException e) { + // 任务执行异常 + e.getCause().printStackTrace(); +} catch (TimeoutException e) { + // 获取结果超时 + future.cancel(true); +} finally { + // 关闭线程池 + executor.shutdown(); +} +``` + +## 常见问题与解决方案 + +### 1. 线程池任务堆积问题 + +**问题**:任务提交速度远大于处理速度,导致队列堆积。 + +**解决方案**: +* 增加线程池核心线程数和最大线程数 +* 使用更高效的任务处理逻辑 +* 对任务进行分流,使用多个线程池处理不同类型的任务 +* 实现合适的拒绝策略,避免系统崩溃 + +### 2. 线程池内存泄漏 + +**问题**:线程池中的线程持有外部对象引用,导致对象无法被垃圾回收。 + +**解决方案**: +* 避免使用ThreadLocal存储大量数据 +* 任务完成后清理ThreadLocal +* 使用弱引用或软引用持有外部对象 + +### 3. 线程池死锁 + +**问题**:线程池中的任务相互依赖,导致死锁。 + +**解决方案**: +* 避免在线程池中提交依赖当前线程池处理结果的任务 +* 对于相互依赖的任务,使用不同的线程池处理 +* 使用CompletableFuture等工具处理任务依赖关系 + +## 总结 + +线程池是Java并发编程中非常重要的工具,合理使用线程池可以显著提高应用程序的性能和稳定性。在实际应用中,需要根据业务场景和系统资源合理配置线程池参数,并做好监控和调优工作。 + +通过本文的学习,我们深入了解了`ThreadPoolExecutor`的工作原理、核心参数、常见线程池类型以及最佳实践,希望能够帮助大家在实际开发中更好地使用线程池。 \ No newline at end of file diff --git "a/docs/uniapp/\345\205\205\347\224\265\345\256\235\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" "b/docs/uniapp/\345\205\205\347\224\265\345\256\235\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" new file mode 100644 index 000000000..eecc673f9 --- /dev/null +++ "b/docs/uniapp/\345\205\205\347\224\265\345\256\235\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" @@ -0,0 +1,1134 @@ +# UniApp充电宝小程序业务实现 + +## 概述 + +本文档详细介绍基于UniApp框架开发的充电宝租赁小程序的完整实现方案,包括项目架构、核心功能模块、技术实现细节和最佳实践。 + +## 项目架构 + +### 技术栈 + +- **前端框架**: UniApp +- **开发语言**: Vue.js + JavaScript/TypeScript +- **UI框架**: uni-ui +- **状态管理**: Vuex +- **网络请求**: uni.request +- **地图服务**: 高德地图/腾讯地图 +- **支付**: 微信支付/支付宝 + +### 项目结构 + +``` +charging-bank-miniapp/ +├── pages/ # 页面目录 +│ ├── index/ # 首页 +│ ├── map/ # 地图页面 +│ ├── scan/ # 扫码页面 +│ ├── order/ # 订单页面 +│ ├── profile/ # 个人中心 +│ └── payment/ # 支付页面 +├── components/ # 组件目录 +│ ├── common/ # 通用组件 +│ ├── map-marker/ # 地图标记组件 +│ └── order-card/ # 订单卡片组件 +├── static/ # 静态资源 +├── store/ # Vuex状态管理 +├── utils/ # 工具函数 +├── api/ # API接口 +├── config/ # 配置文件 +└── manifest.json # 应用配置 +``` + +## 核心功能模块 + +### 1. 用户认证模块 + +#### 微信授权登录 + +```javascript +// utils/auth.js +export const wxLogin = () => { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (loginRes) => { + // 获取用户信息 + uni.getUserInfo({ + provider: 'weixin', + success: (infoRes) => { + // 发送到后端验证 + uni.request({ + url: '/api/auth/wxLogin', + method: 'POST', + data: { + code: loginRes.code, + userInfo: infoRes.userInfo + }, + success: (res) => { + if (res.data.success) { + // 保存token + uni.setStorageSync('token', res.data.token) + uni.setStorageSync('userInfo', res.data.userInfo) + resolve(res.data) + } else { + reject(res.data.message) + } + }, + fail: reject + }) + }, + fail: reject + }) + }, + fail: reject + }) + }) +} +``` + +#### 实名认证 + +```javascript +// pages/profile/realname.vue + + + +``` + +### 2. 地图定位模块 + +#### 地图组件实现 + +```javascript +// components/map-view/map-view.vue + + + +``` + +### 3. 扫码租借模块 + +#### 扫码功能实现 + +```javascript +// pages/scan/scan.vue + + + +``` + +### 4. 订单管理模块 + +#### 订单列表页面 + +```javascript +// pages/order/list.vue + + + +``` + +### 5. 支付模块 + +#### 微信支付实现 + +```javascript +// utils/payment.js +export const wxPay = (paymentData) => { + return new Promise((resolve, reject) => { + uni.requestPayment({ + provider: 'wxpay', + timeStamp: paymentData.timeStamp, + nonceStr: paymentData.nonceStr, + package: paymentData.package, + signType: paymentData.signType, + paySign: paymentData.paySign, + success: (res) => { + resolve(res) + }, + fail: (error) => { + reject(error) + } + }) + }) +} + +// pages/payment/payment.vue + + + +``` + +## 状态管理 + +### Vuex Store配置 + +```javascript +// store/index.js +import { createStore } from 'vuex' +import user from './modules/user' +import order from './modules/order' +import device from './modules/device' + +const store = createStore({ + modules: { + user, + order, + device + } +}) + +export default store + +// store/modules/user.js +const state = { + userInfo: null, + token: '', + isLogin: false +} + +const mutations = { + SET_USER_INFO(state, userInfo) { + state.userInfo = userInfo + state.isLogin = !!userInfo + }, + + SET_TOKEN(state, token) { + state.token = token + uni.setStorageSync('token', token) + }, + + CLEAR_USER_DATA(state) { + state.userInfo = null + state.token = '' + state.isLogin = false + uni.removeStorageSync('token') + uni.removeStorageSync('userInfo') + } +} + +const actions = { + // 登录 + async login({ commit }, loginData) { + try { + const res = await this.$api.login(loginData) + + if (res.success) { + commit('SET_USER_INFO', res.data.userInfo) + commit('SET_TOKEN', res.data.token) + return res + } + } catch (error) { + throw error + } + }, + + // 登出 + logout({ commit }) { + commit('CLEAR_USER_DATA') + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} +``` + +## API接口封装 + +```javascript +// api/index.js +const BASE_URL = 'https://api.chargingbank.com' + +class ApiService { + constructor() { + this.baseURL = BASE_URL + } + + // 通用请求方法 + request(options) { + return new Promise((resolve, reject) => { + const token = uni.getStorageSync('token') + + uni.request({ + url: this.baseURL + options.url, + method: options.method || 'GET', + data: options.data || {}, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...options.header + }, + success: (res) => { + if (res.statusCode === 200) { + resolve(res.data) + } else if (res.statusCode === 401) { + // token过期,跳转登录 + uni.navigateTo({ + url: '/pages/login/login' + }) + reject(new Error('登录已过期')) + } else { + reject(new Error(res.data.message || '请求失败')) + } + }, + fail: (error) => { + reject(error) + } + }) + }) + } + + // 用户相关API + login(data) { + return this.request({ + url: '/api/auth/login', + method: 'POST', + data + }) + } + + getUserInfo() { + return this.request({ + url: '/api/user/info' + }) + } + + submitRealname(data) { + return this.request({ + url: '/api/user/realname', + method: 'POST', + data + }) + } + + // 设备相关API + getNearbyDevices(data) { + return this.request({ + url: '/api/device/nearby', + data + }) + } + + getDeviceDetail(deviceId) { + return this.request({ + url: `/api/device/${deviceId}` + }) + } + + // 订单相关API + rentDevice(data) { + return this.request({ + url: '/api/order/rent', + method: 'POST', + data + }) + } + + returnDevice(data) { + return this.request({ + url: '/api/order/return', + method: 'POST', + data + }) + } + + getOrderList(data) { + return this.request({ + url: '/api/order/list', + data + }) + } + + getOrderDetail(data) { + return this.request({ + url: '/api/order/detail', + data + }) + } + + // 支付相关API + createPayment(data) { + return this.request({ + url: '/api/payment/create', + method: 'POST', + data + }) + } +} + +const apiService = new ApiService() + +export default apiService +``` + +## 最佳实践 + +### 1. 性能优化 + +- 使用图片懒加载 +- 合理使用缓存机制 +- 优化网络请求 +- 减少页面层级 + +### 2. 用户体验 + +- 添加加载状态提示 +- 网络异常处理 +- 离线状态处理 +- 友好的错误提示 + +### 3. 安全考虑 + +- 敏感信息加密传输 +- 防止重复提交 +- 输入验证 +- 权限控制 + +### 4. 兼容性 + +- 多端适配 +- 不同设备屏幕适配 +- 系统版本兼容 + +## 总结 + +本文档详细介绍了基于UniApp开发充电宝小程序的完整实现方案,涵盖了从项目架构到具体功能实现的各个方面。通过合理的架构设计和代码组织,可以构建出功能完善、性能优良的充电宝租赁小程序。 + +在实际开发过程中,还需要根据具体的业务需求和技术环境进行相应的调整和优化。 \ No newline at end of file diff --git "a/docs/uniapp/\345\205\205\347\224\265\346\241\251\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" "b/docs/uniapp/\345\205\205\347\224\265\346\241\251\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" new file mode 100644 index 000000000..4d8aca2c2 --- /dev/null +++ "b/docs/uniapp/\345\205\205\347\224\265\346\241\251\345\260\217\347\250\213\345\272\217\345\256\236\347\216\260.md" @@ -0,0 +1,2149 @@ +# UniApp充电桩小程序业务实现 + +## 概述 + +本文档详细介绍基于UniApp框架开发的充电桩服务小程序的完整实现方案,包括充电桩查找、预约充电、充电监控、支付结算等核心功能模块的技术实现。 + +## 项目架构 + +### 技术栈 + +- **前端框架**: UniApp +- **开发语言**: Vue.js + TypeScript +- **UI框架**: uni-ui + uView +- **状态管理**: Pinia +- **网络请求**: uni.request + 拦截器 +- **地图服务**: 高德地图API +- **实时通信**: WebSocket +- **支付**: 微信支付/支付宝 + +### 项目结构 + +``` +charging-station-miniapp/ +├── pages/ # 页面目录 +│ ├── index/ # 首页 +│ ├── map/ # 地图找桩 +│ ├── station/ # 充电站详情 +│ ├── charging/ # 充电中页面 +│ ├── reservation/ # 预约页面 +│ ├── order/ # 订单管理 +│ ├── wallet/ # 钱包页面 +│ └── profile/ # 个人中心 +├── components/ # 组件目录 +│ ├── station-card/ # 充电站卡片 +│ ├── charging-gun/ # 充电枪组件 +│ ├── real-time-chart/ # 实时图表 +│ └── payment-modal/ # 支付弹窗 +├── static/ # 静态资源 +├── stores/ # Pinia状态管理 +├── utils/ # 工具函数 +├── api/ # API接口 +├── types/ # TypeScript类型定义 +└── config/ # 配置文件 +``` + +## 核心功能模块 + +### 1. 充电站地图模块 + +#### 地图找桩页面 + +```vue + + + + +``` + +### 2. 充电站详情模块 + +#### 充电站详情页面 + +```vue + + + + +``` + +### 3. 充电监控模块 + +#### 充电中页面 + +```vue + + + + +``` + +### 4. 订单管理模块 + +#### 订单列表页面 + +```vue + + + + +``` + +## 状态管理 (Pinia) + +### 充电站状态管理 + +```typescript +// stores/station.ts +import { defineStore } from 'pinia' +import type { ChargingStation, StationFilter } from '@/types/station' +import api from '@/api' + +export const useStationStore = defineStore('station', { + state: () => ({ + nearbyStations: [] as ChargingStation[], + currentStation: null as ChargingStation | null, + searchResults: [] as ChargingStation[], + loading: false, + error: null as string | null + }), + + getters: { + availableStations: (state) => { + return state.nearbyStations.filter(station => + station.status === 'online' && station.availableGuns > 0 + ) + }, + + stationById: (state) => { + return (id: string) => state.nearbyStations.find(station => station.id === id) + } + }, + + actions: { + async loadNearbyStations(params: { + longitude: number + latitude: number + radius: number + filter?: StationFilter + }) { + try { + this.loading = true + this.error = null + + const response = await api.station.getNearbyStations(params) + + if (response.success) { + this.nearbyStations = response.data + } else { + this.error = response.message + } + } catch (error) { + this.error = '加载充电站失败' + console.error('加载充电站失败:', error) + } finally { + this.loading = false + } + }, + + async getStationDetail(stationId: string): Promise { + try { + const response = await api.station.getStationDetail(stationId) + + if (response.success) { + this.currentStation = response.data + return response.data + } else { + throw new Error(response.message) + } + } catch (error) { + console.error('获取充电站详情失败:', error) + throw error + } + }, + + async searchStations(params: { + keyword: string + longitude: number + latitude: number + }) { + try { + const response = await api.station.searchStations(params) + + if (response.success) { + this.searchResults = response.data + } + } catch (error) { + console.error('搜索充电站失败:', error) + } + }, + + async createReservation(params: { + stationId: string + gunId: string + reservationTime: string + duration: number + }) { + try { + const response = await api.reservation.create(params) + + if (response.success) { + return response.data + } else { + throw new Error(response.message) + } + } catch (error) { + console.error('创建预约失败:', error) + throw error + } + }, + + async getRealTimeData(stationId: string) { + try { + const response = await api.station.getRealTimeData(stationId) + + if (response.success) { + return response.data + } + } catch (error) { + console.error('获取实时数据失败:', error) + } + } + } +}) +``` + +## API接口封装 + +```typescript +// api/station.ts +import request from '@/utils/request' +import type { ChargingStation, StationFilter } from '@/types/station' + +export const stationApi = { + // 获取附近充电站 + getNearbyStations(params: { + longitude: number + latitude: number + radius: number + filter?: StationFilter + }) { + return request({ + url: '/api/station/nearby', + method: 'GET', + data: params + }) + }, + + // 获取充电站详情 + getStationDetail(stationId: string) { + return request({ + url: `/api/station/${stationId}`, + method: 'GET' + }) + }, + + // 搜索充电站 + searchStations(params: { + keyword: string + longitude: number + latitude: number + }) { + return request({ + url: '/api/station/search', + method: 'GET', + data: params + }) + }, + + // 获取实时数据 + getRealTimeData(stationId: string) { + return request({ + url: `/api/station/${stationId}/realtime`, + method: 'GET' + }) + } +} + +// api/charging.ts +export const chargingApi = { + // 开始充电 + startCharging(params: { + stationId: string + gunId: string + targetSOC: number + }) { + return request({ + url: '/api/charging/start', + method: 'POST', + data: params + }) + }, + + // 获取充电信息 + getChargingInfo(orderId: string) { + return request({ + url: `/api/charging/${orderId}`, + method: 'GET' + }) + }, + + // 暂停充电 + pauseCharging(orderId: string) { + return request({ + url: `/api/charging/${orderId}/pause`, + method: 'POST' + }) + }, + + // 恢复充电 + resumeCharging(orderId: string) { + return request({ + url: `/api/charging/${orderId}/resume`, + method: 'POST' + }) + }, + + // 停止充电 + stopCharging(orderId: string) { + return request({ + url: `/api/charging/${orderId}/stop`, + method: 'POST' + }) + } +} +``` + +## 工具函数 + +### 网络请求封装 + +```typescript +// utils/request.ts +interface RequestOptions { + url: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' + data?: any + header?: Record + timeout?: number +} + +interface ApiResponse { + success: boolean + data: T + message: string + code: number +} + +const BASE_URL = 'https://api.charging.com' +const DEFAULT_TIMEOUT = 10000 + +// 请求拦截器 +const requestInterceptor = (options: RequestOptions) => { + // 添加认证token + const token = uni.getStorageSync('access_token') + if (token) { + options.header = { + ...options.header, + 'Authorization': `Bearer ${token}` + } + } + + // 添加公共请求头 + options.header = { + 'Content-Type': 'application/json', + ...options.header + } + + return options +} + +// 响应拦截器 +const responseInterceptor = (response: any): ApiResponse => { + const { statusCode, data } = response + + if (statusCode === 200) { + return data + } else if (statusCode === 401) { + // token过期,跳转到登录页 + uni.removeStorageSync('access_token') + uni.navigateTo({ + url: '/pages/login/login' + }) + throw new Error('登录已过期') + } else { + throw new Error(data?.message || '请求失败') + } +} + +export default function request(options: RequestOptions): Promise> { + return new Promise((resolve, reject) => { + const requestOptions = requestInterceptor({ + timeout: DEFAULT_TIMEOUT, + ...options, + url: BASE_URL + options.url + }) + + uni.request({ + ...requestOptions, + success: (response) => { + try { + const result = responseInterceptor(response) + resolve(result) + } catch (error) { + reject(error) + } + }, + fail: (error) => { + console.error('请求失败:', error) + reject(new Error('网络请求失败')) + } + }) + }) +} +``` + +### 地理位置工具 + +```typescript +// utils/location.ts +export interface LocationInfo { + longitude: number + latitude: number + address?: string + city?: string +} + +// 获取当前位置 +export const getCurrentLocation = (): Promise => { + return new Promise((resolve, reject) => { + uni.getLocation({ + type: 'gcj02', + success: (res) => { + resolve({ + longitude: res.longitude, + latitude: res.latitude + }) + }, + fail: (error) => { + reject(error) + } + }) + }) +} + +// 计算两点间距离(米) +export const calculateDistance = ( + lat1: number, + lon1: number, + lat2: number, + lon2: number +): number => { + const R = 6371e3 // 地球半径(米) + const φ1 = lat1 * Math.PI / 180 + const φ2 = lat2 * Math.PI / 180 + const Δφ = (lat2 - lat1) * Math.PI / 180 + const Δλ = (lon2 - lon1) * Math.PI / 180 + + const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ/2) * Math.sin(Δλ/2) + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) + + return R * c +} + +// 格式化距离显示 +export const formatDistance = (distance: number): string => { + if (distance < 1000) { + return `${Math.round(distance)}m` + } else { + return `${(distance / 1000).toFixed(1)}km` + } +} +``` + +## TypeScript类型定义 + +```typescript +// types/station.ts +export interface ChargingStation { + id: string + name: string + address: string + longitude: number + latitude: number + phone: string + operatingHours: string + status: 'online' | 'offline' | 'maintenance' + totalGuns: number + availableGuns: number + chargingGuns: ChargingGun[] + totalPower: number + currentLoad: number + todayEnergy: number + todayOrders: number + pricingRules: PricingRule[] + reviews: Review[] + averageRating: number + distance?: number +} + +export interface ChargingGun { + id: string + name: string + type: 'dc_fast' | 'ac_slow' | 'super_fast' + maxPower: number + status: 'available' | 'charging' | 'offline' | 'reserved' + currentPower?: number + voltage?: number + current?: number +} + +export interface PricingRule { + id: string + timeRange: string + electricityPrice: number + servicePrice: number +} + +export interface Review { + id: string + userName: string + rating: number + content: string + createTime: string +} + +export interface StationFilter { + chargingType: string[] + powerRange: [number, number] + distance: number + availability: 'all' | 'available' | 'fast' +} + +// types/charging.ts +export interface ChargingInfo { + orderId: string + stationId: string + stationName: string + gunId: string + gunNumber: string + status: 'charging' | 'paused' | 'completed' | 'error' + startTime: string + duration: number + currentPower: number + voltage: number + current: number + chargedEnergy: number + currentSOC: number + targetSOC: number + currentCost: number + estimatedCost: number +} + +export interface ChargingOrder { + id: string + orderNo: string + stationId: string + stationName: string + gunId: string + gunNumber: string + status: 'pending' | 'charging' | 'completed' | 'cancelled' | 'failed' + startTime: string + endTime?: string + duration: number + chargedEnergy: number + totalAmount: number + electricityFee: number + serviceFee: number + reviewed: boolean +} +``` + +## 最佳实践 + +### 1. 性能优化 + +- **地图优化**: 使用地图聚合功能,避免同时显示过多标记点 +- **数据缓存**: 缓存充电站基础信息,减少重复请求 +- **图片懒加载**: 充电站图片使用懒加载技术 +- **分页加载**: 订单列表采用分页加载,提升加载速度 + +### 2. 用户体验 + +- **离线提示**: 网络断开时显示友好提示 +- **加载状态**: 所有异步操作都有加载状态提示 +- **错误处理**: 完善的错误处理和用户提示 +- **操作反馈**: 重要操作提供明确的成功/失败反馈 + +### 3. 安全考虑 + +- **数据加密**: 敏感数据传输使用HTTPS加密 +- **身份验证**: 完善的用户身份验证机制 +- **权限控制**: 基于角色的权限控制 +- **数据校验**: 前端和后端双重数据校验 + +### 4. 代码规范 + +- **TypeScript**: 全面使用TypeScript提供类型安全 +- **组件化**: 合理拆分组件,提高代码复用性 +- **状态管理**: 使用Pinia进行统一状态管理 +- **错误边界**: 设置错误边界,防止应用崩溃 + +## 部署配置 + +### 1. 小程序配置 + +```json +// manifest.json +{ + "name": "充电桩服务", + "appid": "your_app_id", + "description": "智能充电桩服务小程序", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + } + }, + "mp-weixin": { + "appid": "your_wechat_appid", + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "preloadBackgroundData": false, + "minified": true, + "newFeature": false, + "coverView": true, + "nodeModules": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "usingComponents": true, + "permission": { + "scope.userLocation": { + "desc": "您的位置信息将用于查找附近的充电站" + } + }, + "requiredPrivateInfos": [ + "getLocation" + ] + } +} +``` + +### 2. 页面配置 + +```json +// pages.json +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "充电桩服务", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/map/map", + "style": { + "navigationBarTitleText": "地图找桩", + "navigationStyle": "custom" + } + }, + { + "path": "pages/station/detail", + "style": { + "navigationBarTitleText": "充电站详情" + } + }, + { + "path": "pages/charging/charging", + "style": { + "navigationBarTitleText": "充电中", + "navigationStyle": "custom" + } + } + ], + "tabBar": { + "color": "#7A7E83", + "selectedColor": "#3cc51f", + "borderStyle": "black", + "backgroundColor": "#ffffff", + "list": [ + { + "pagePath": "pages/index/index", + "iconPath": "static/tab-icons/home.png", + "selectedIconPath": "static/tab-icons/home-active.png", + "text": "首页" + }, + { + "pagePath": "pages/map/map", + "iconPath": "static/tab-icons/map.png", + "selectedIconPath": "static/tab-icons/map-active.png", + "text": "地图" + }, + { + "pagePath": "pages/order/list", + "iconPath": "static/tab-icons/order.png", + "selectedIconPath": "static/tab-icons/order-active.png", + "text": "订单" + }, + { + "pagePath": "pages/profile/profile", + "iconPath": "static/tab-icons/profile.png", + "selectedIconPath": "static/tab-icons/profile-active.png", + "text": "我的" + } + ] + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "充电桩服务", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + } +} +``` + +## 总结 + +本文档详细介绍了基于UniApp开发充电桩服务小程序的完整实现方案,涵盖了从项目架构设计到具体功能实现的各个方面。通过模块化的设计和完善的状态管理,确保了应用的可维护性和扩展性。 + +主要特性包括: +- 实时地图找桩功能 +- 充电站详情展示 +- 实时充电监控 +- 完整的订单管理 +- 优秀的用户体验 +- 完善的错误处理 + +该方案可以作为充电桩行业小程序开发的参考模板,开发者可以根据具体业务需求进行定制和扩展。 \ No newline at end of file diff --git a/docs/worker/1.gif b/docs/worker/1.gif new file mode 100644 index 000000000..b92d07623 Binary files /dev/null and b/docs/worker/1.gif differ diff --git a/docs/worker/cpu-troubleshooting.md b/docs/worker/cpu-troubleshooting.md new file mode 100644 index 000000000..8e8daca71 --- /dev/null +++ b/docs/worker/cpu-troubleshooting.md @@ -0,0 +1,263 @@ +--- +title: CPU使用率100%的异常排查 +author: 哪吒 +date: '2023-01-01' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## CPU使用率100%的异常排查 + +在生产环境中,CPU使用率飙升至100%是一种常见的性能问题,可能导致系统响应缓慢甚至服务不可用。本文将详细介绍如何排查和解决CPU使用率100%的问题。 + +### 1. 问题表现 + +当CPU使用率达到100%时,系统通常会出现以下症状: + +- 系统响应缓慢或无响应 +- 应用程序执行速度变慢 +- 请求处理时间增加 +- 任务队列积压 +- 服务超时或拒绝连接 + +### 2. 排查工具 + +#### 2.1 Linux系统工具 + +**top命令**:实时显示系统中各个进程的资源占用情况 + +```bash +top +``` + +使用top命令后,可以按以下键进行排序: +- 按 `P` 键:按CPU使用率排序(默认) +- 按 `M` 键:按内存使用率排序 +- 按 `T` 键:按运行时间排序 + +**htop命令**:top的增强版,提供更友好的界面和更多功能 + +```bash +htop +``` + +**ps命令**:查看进程状态 + +```bash +# 查看CPU占用最高的前10个进程 +ps aux | sort -k3nr | head -10 +``` + +**mpstat命令**:查看多处理器统计信息 + +```bash +mpstat -P ALL 2 5 # 每2秒采样一次,共采样5次,显示所有CPU核心的统计信息 +``` + +**pidstat命令**:监控进程的CPU使用情况 + +```bash +pidstat -u 2 5 # 每2秒采样一次,共采样5次 +pidstat -p -u 2 5 # 监控特定进程 +``` + +#### 2.2 Java应用工具 + +**jstack**:生成Java线程转储 + +```bash +jstack > thread_dump.log +``` + +**jstat**:监控JVM的GC情况 + +```bash +jstat -gcutil 1000 10 # 每1秒采样一次,共采样10次 +``` + +**jmap**:生成堆转储 + +```bash +jmap -dump:format=b,file=heap_dump.bin +``` + +**Arthas**:阿里开源的Java诊断工具 + +```bash +# 安装Arthas +curl -O https://arthas.aliyun.com/arthas-boot.jar +java -jar arthas-boot.jar + +# 使用thread命令查看线程情况 +thread -n 3 # 显示CPU使用率最高的3个线程 +``` + +### 3. 排查步骤 + +#### 3.1 确认CPU使用率 + +首先使用top命令确认系统整体CPU使用率: + +```bash +top +``` + +关注以下几个指标: +- `%us`:用户空间占用CPU百分比 +- `%sy`:内核空间占用CPU百分比 +- `%ni`:用户进程空间内改变过优先级的进程占用CPU百分比 +- `%id`:空闲CPU百分比 +- `%wa`:等待输入输出的CPU时间百分比 + +#### 3.2 定位高CPU进程 + +在top命令中,按P键按CPU使用率排序,找出CPU使用率最高的进程,记录其PID。 + +```bash +# 或者使用ps命令 +ps aux | sort -k3nr | head -10 +``` + +#### 3.3 分析进程内的线程 + +找到高CPU进程后,进一步分析该进程内的线程情况: + +```bash +# 查看进程内的线程CPU使用情况 +top -Hp +``` + +记录CPU使用率高的线程ID,将线程ID转换为十六进制: + +```bash +printf "%x\n" <线程ID> +``` + +#### 3.4 生成线程转储 + +对于Java应用,使用jstack生成线程转储: + +```bash +jstack > thread_dump.log +``` + +在thread_dump.log文件中搜索之前转换的十六进制线程ID,找到对应的线程栈信息。 + +```bash +grep -A 30 "0x<十六进制线程ID>" thread_dump.log +``` + +#### 3.5 分析GC情况 + +如果怀疑是GC问题导致的高CPU,使用jstat查看GC情况: + +```bash +jstat -gcutil 1000 10 +``` + +关注以下指标: +- `S0`、`S1`、`E`、`O`、`M`:各内存区域使用百分比 +- `YGC`、`YGCT`:年轻代GC次数和时间 +- `FGC`、`FGCT`:老年代GC次数和时间 +- `GCT`:总GC时间 + +如果频繁发生Full GC,可能是内存泄漏或内存配置不合理。 + +### 4. 常见原因及解决方案 + +#### 4.1 代码问题 + +**死循环或无限递归** + +- 症状:线程栈显示同一方法反复出现 +- 解决方案:修复代码中的逻辑错误,添加适当的退出条件 + +**算法效率低下** + +- 症状:CPU密集型计算占用大量资源 +- 解决方案:优化算法,使用更高效的数据结构,考虑增加缓存 + +**资源竞争** + +- 症状:多个线程争用同一把锁,导致上下文切换频繁 +- 解决方案:减少锁粒度,使用并发容器,避免长时间持有锁 + +#### 4.2 JVM问题 + +**频繁GC** + +- 症状:jstat显示GC活动频繁,GC线程占用大量CPU +- 解决方案:调整JVM内存参数,增加堆内存,优化对象创建 + +**JIT编译** + +- 症状:启动初期CPU使用率高,CompilerThread占用资源 +- 解决方案:预热应用,使用AOT编译,调整JIT编译参数 + +#### 4.3 系统问题 + +**进程数过多** + +- 症状:系统进程数量异常增多 +- 解决方案:检查是否有异常进程创建,限制进程数量 + +**系统中断处理** + +- 症状:系统CPU使用率高,但用户进程CPU使用率不高 +- 解决方案:检查硬件问题,更新驱动,调整系统参数 + +### 5. 实战案例 + +#### 案例一:Java应用CPU飙升 + +**现象**:生产环境中一个Java应用CPU使用率突然飙升至100%,系统响应缓慢。 + +**排查过程**: + +1. 使用top命令确认Java进程CPU使用率接近100% +2. 使用top -Hp命令找到占用CPU最高的线程ID +3. 将线程ID转换为十六进制:`printf "%x\n" 12345` +4. 使用jstack生成线程转储并分析 +5. 发现问题线程在执行一个无限循环的操作 + +**解决方案**:修复代码中的无限循环问题,添加适当的退出条件和超时机制。 + +#### 案例二:频繁GC导致CPU高负载 + +**现象**:应用运行一段时间后CPU使用率逐渐升高,响应变慢。 + +**排查过程**: + +1. 使用jstat发现Full GC频繁发生 +2. 使用jmap生成堆转储并分析 +3. 发现某个集合对象不断增长,没有释放 + +**解决方案**:修复内存泄漏问题,确保临时对象能够被及时回收。 + +### 6. 预防措施 + +#### 6.1 监控告警 + +- 设置CPU使用率阈值告警,如连续5分钟超过80%触发告警 +- 监控GC频率和时间,设置合理的告警阈值 +- 监控线程数量,防止线程爆炸 + +#### 6.2 性能测试 + +- 在上线前进行充分的性能测试和压力测试 +- 模拟高并发场景,验证系统在极限情况下的表现 +- 进行长时间的稳定性测试,发现潜在的资源泄漏问题 + +#### 6.3 代码审查 + +- 重点关注循环、递归等可能导致CPU密集的代码 +- 检查资源释放是否完整 +- 避免使用低效算法处理大量数据 + +### 7. 总结 + +CPU使用率100%的问题排查是一个系统性工作,需要从操作系统、应用程序、JVM等多个层面进行分析。掌握相关工具和方法,可以帮助我们快速定位和解决问题,保障系统的稳定运行。 + +在实际工作中,建议建立标准的问题排查流程和工具集,提前做好监控和告警,做到早发现、早处理,避免问题扩大化。同时,持续优化代码质量和系统架构,从根本上减少高CPU问题的发生。 \ No newline at end of file diff --git a/docs/worker/elfk-cluster.md b/docs/worker/elfk-cluster.md new file mode 100644 index 000000000..98e5d8fa6 --- /dev/null +++ b/docs/worker/elfk-cluster.md @@ -0,0 +1,838 @@ +--- +title: ELFK生产集群搭建指南 +date: 2023-06-01 +--- + +# ELFK生产集群搭建指南 + +## 1. ELFK架构概述 + +ELFK是一个强大的日志收集、处理、存储和可视化的解决方案,由以下组件组成: + +- **Elasticsearch**:分布式搜索和分析引擎,用于存储和检索日志数据 +- **Logstash**:数据处理管道,用于收集、转换和发送日志数据 +- **Filebeat**:轻量级日志收集器,部署在各个服务器上收集日志文件 +- **Kafka**:分布式消息队列,作为日志数据的缓冲层,提高系统的可靠性和扩展性 +- **Kibana**:数据可视化平台,提供日志数据的搜索、分析和展示功能 + +在ELFK架构中,Filebeat收集日志并发送到Kafka,Logstash从Kafka消费数据并进行处理后存入Elasticsearch,最后通过Kibana进行可视化展示。 + +![ELFK架构图](./img/elfk-architecture.svg) + +## 2. 环境准备 + +### 2.1 服务器规划 + +对于生产环境,建议至少准备以下服务器: + +| 服务器角色 | 数量 | 配置建议 | 说明 | +| --- | --- | --- | --- | +| Elasticsearch主节点 | 3台 | 8核16G内存 | 负责集群管理 | +| Elasticsearch数据节点 | 3+台 | 16核32G内存,大容量SSD | 存储和检索数据 | +| Kafka集群 | 3+台 | 8核16G内存 | 消息队列 | +| Logstash服务器 | 2+台 | 8核16G内存 | 数据处理 | +| Kibana服务器 | 2台 | 4核8G内存 | 可视化界面 | + +### 2.2 网络规划 + +- 所有服务器应该在同一内网环境 +- 配置合适的防火墙规则,只开放必要的端口 +- 建议使用专用网络接口用于集群内部通信 + +### 2.3 操作系统优化 + +```bash +# 修改系统限制,添加到/etc/sysctl.conf +vm.max_map_count=262144 +net.core.somaxconn=32768 +net.ipv4.tcp_max_syn_backlog=8192 +net.ipv4.tcp_slow_start_after_idle=0 +net.ipv4.tcp_tw_reuse=1 +net.ipv4.ip_local_port_range=1024 65535 + +# 修改文件描述符限制,添加到/etc/security/limits.conf +* soft nofile 65536 +* hard nofile 65536 +* soft nproc 32768 +* hard nproc 32768 +``` + +## 3. Elasticsearch集群部署 + +### 3.1 安装Elasticsearch + +我们使用Docker方式部署Elasticsearch集群。首先创建docker-compose.yml文件: + +```yaml +version: '3' +services: + es01: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + container_name: es01 + environment: + - node.name=es01 + - cluster.name=es-cluster + - discovery.seed_hosts=es02,es03 + - cluster.initial_master_nodes=es01,es02,es03 + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms8g -Xmx8g" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - esdata01:/usr/share/elasticsearch/data + ports: + - 9200:9200 + networks: + - elastic + + es02: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + container_name: es02 + environment: + - node.name=es02 + - cluster.name=es-cluster + - discovery.seed_hosts=es01,es03 + - cluster.initial_master_nodes=es01,es02,es03 + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms8g -Xmx8g" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - esdata02:/usr/share/elasticsearch/data + networks: + - elastic + + es03: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + container_name: es03 + environment: + - node.name=es03 + - cluster.name=es-cluster + - discovery.seed_hosts=es01,es02 + - cluster.initial_master_nodes=es01,es02,es03 + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms8g -Xmx8g" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - esdata03:/usr/share/elasticsearch/data + networks: + - elastic + +volumes: + esdata01: + esdata02: + esdata03: + +networks: + elastic: + driver: bridge +``` + +启动Elasticsearch集群: + +```bash +docker-compose up -d +``` + +### 3.2 配置Elasticsearch安全认证 + +```bash +# 进入容器 +docker exec -it es01 bash + +# 设置密码 +./bin/elasticsearch-setup-passwords auto +``` + +记录下生成的密码,后续配置需要使用。 + +### 3.3 配置Elasticsearch集群 + +创建索引生命周期管理策略: + +```bash +curl -X PUT "localhost:9200/_ilm/policy/logs-policy?pretty" -H 'Content-Type: application/json' -d' +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_age": "1d", + "max_size": "50gb" + }, + "set_priority": { + "priority": 100 + } + } + }, + "warm": { + "min_age": "3d", + "actions": { + "shrink": { + "number_of_shards": 1 + }, + "forcemerge": { + "max_num_segments": 1 + }, + "set_priority": { + "priority": 50 + } + } + }, + "cold": { + "min_age": "30d", + "actions": { + "set_priority": { + "priority": 0 + } + } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": {} + } + } + } + } +}' +``` + +创建索引模板: + +```bash +curl -X PUT "localhost:9200/_template/logs-template?pretty" -H 'Content-Type: application/json' -d' +{ + "index_patterns": ["logs-*"], + "settings": { + "number_of_shards": 3, + "number_of_replicas": 1, + "index.lifecycle.name": "logs-policy", + "index.lifecycle.rollover_alias": "logs" + }, + "mappings": { + "properties": { + "@timestamp": { "type": "date" }, + "message": { "type": "text" }, + "level": { "type": "keyword" }, + "service": { "type": "keyword" }, + "host": { "type": "keyword" } + } + } +}' +``` + +## 4. Kafka集群部署 + +### 4.1 安装Kafka + +创建docker-compose.yml文件: + +```yaml +version: '3' +services: + zookeeper-1: + image: confluentinc/cp-zookeeper:7.0.1 + container_name: zookeeper-1 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ZOOKEEPER_SERVER_ID: 1 + ZOOKEEPER_SERVERS: zookeeper-1:2888:3888;zookeeper-2:2888:3888;zookeeper-3:2888:3888 + ports: + - "2181:2181" + volumes: + - zookeeper-1-data:/var/lib/zookeeper/data + - zookeeper-1-log:/var/lib/zookeeper/log + networks: + - kafka-net + + zookeeper-2: + image: confluentinc/cp-zookeeper:7.0.1 + container_name: zookeeper-2 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ZOOKEEPER_SERVER_ID: 2 + ZOOKEEPER_SERVERS: zookeeper-1:2888:3888;zookeeper-2:2888:3888;zookeeper-3:2888:3888 + volumes: + - zookeeper-2-data:/var/lib/zookeeper/data + - zookeeper-2-log:/var/lib/zookeeper/log + networks: + - kafka-net + + zookeeper-3: + image: confluentinc/cp-zookeeper:7.0.1 + container_name: zookeeper-3 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ZOOKEEPER_SERVER_ID: 3 + ZOOKEEPER_SERVERS: zookeeper-1:2888:3888;zookeeper-2:2888:3888;zookeeper-3:2888:3888 + volumes: + - zookeeper-3-data:/var/lib/zookeeper/data + - zookeeper-3-log:/var/lib/zookeeper/log + networks: + - kafka-net + + kafka-1: + image: confluentinc/cp-kafka:7.0.1 + container_name: kafka-1 + depends_on: + - zookeeper-1 + - zookeeper-2 + - zookeeper-3 + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-1:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + volumes: + - kafka-1-data:/var/lib/kafka/data + networks: + - kafka-net + + kafka-2: + image: confluentinc/cp-kafka:7.0.1 + container_name: kafka-2 + depends_on: + - zookeeper-1 + - zookeeper-2 + - zookeeper-3 + ports: + - "9093:9093" + environment: + KAFKA_BROKER_ID: 2 + KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-2:29093,PLAINTEXT_HOST://localhost:9093 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + volumes: + - kafka-2-data:/var/lib/kafka/data + networks: + - kafka-net + + kafka-3: + image: confluentinc/cp-kafka:7.0.1 + container_name: kafka-3 + depends_on: + - zookeeper-1 + - zookeeper-2 + - zookeeper-3 + ports: + - "9094:9094" + environment: + KAFKA_BROKER_ID: 3 + KAFKA_ZOOKEEPER_CONNECT: zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-3:29094,PLAINTEXT_HOST://localhost:9094 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + volumes: + - kafka-3-data:/var/lib/kafka/data + networks: + - kafka-net + +volumes: + zookeeper-1-data: + zookeeper-1-log: + zookeeper-2-data: + zookeeper-2-log: + zookeeper-3-data: + zookeeper-3-log: + kafka-1-data: + kafka-2-data: + kafka-3-data: + +networks: + kafka-net: + driver: bridge +``` + +启动Kafka集群: + +```bash +docker-compose up -d +``` + +### 4.2 创建Kafka主题 + +```bash +# 进入Kafka容器 +docker exec -it kafka-1 bash + +# 创建日志主题,3个分区,3个副本 +kafka-topics --create --topic app-logs --partitions 3 --replication-factor 3 --bootstrap-server kafka-1:29092 + +# 创建系统日志主题 +kafka-topics --create --topic system-logs --partitions 3 --replication-factor 3 --bootstrap-server kafka-1:29092 + +# 查看主题列表 +kafka-topics --list --bootstrap-server kafka-1:29092 +``` + +## 5. Logstash部署 + +### 5.1 安装Logstash + +创建Logstash配置文件 `logstash.conf`: + +```conf +input { + kafka { + bootstrap_servers => "kafka-1:29092,kafka-2:29093,kafka-3:29094" + topics => ["app-logs", "system-logs"] + group_id => "logstash" + auto_offset_reset => "latest" + consumer_threads => 3 + decorate_events => true + } +} + +filter { + if [kafka][topic] == "app-logs" { + json { + source => "message" + } + date { + match => [ "timestamp", "ISO8601" ] + target => "@timestamp" + } + mutate { + remove_field => [ "timestamp" ] + } + } else if [kafka][topic] == "system-logs" { + grok { + match => { "message" => "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" } + } + date { + match => [ "syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ] + target => "@timestamp" + } + } + + # 添加处理节点信息 + mutate { + add_field => { "logstash_node" => "${HOSTNAME}" } + } +} + +output { + if [kafka][topic] == "app-logs" { + elasticsearch { + hosts => ["es01:9200", "es02:9200", "es03:9200"] + user => "elastic" + password => "${ELASTIC_PASSWORD}" + index => "app-logs-%{+YYYY.MM.dd}" + ilm_enabled => true + ilm_rollover_alias => "app-logs" + ilm_pattern => "{now/d}-000001" + ilm_policy => "logs-policy" + } + } else if [kafka][topic] == "system-logs" { + elasticsearch { + hosts => ["es01:9200", "es02:9200", "es03:9200"] + user => "elastic" + password => "${ELASTIC_PASSWORD}" + index => "system-logs-%{+YYYY.MM.dd}" + ilm_enabled => true + ilm_rollover_alias => "system-logs" + ilm_pattern => "{now/d}-000001" + ilm_policy => "logs-policy" + } + } +} +``` + +创建docker-compose.yml文件: + +```yaml +version: '3' +services: + logstash: + image: docker.elastic.co/logstash/logstash:7.17.0 + container_name: logstash + volumes: + - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro + - ./logstash.yml:/usr/share/logstash/config/logstash.yml:ro + environment: + LS_JAVA_OPTS: "-Xmx1g -Xms1g" + ELASTIC_PASSWORD: "your_elastic_password" + networks: + - elastic + - kafka-net + +networks: + elastic: + external: true + kafka-net: + external: true +``` + +创建logstash.yml配置文件: + +```yaml +http.host: "0.0.0.0" +xpack.monitoring.elasticsearch.hosts: ["http://es01:9200", "http://es02:9200", "http://es03:9200"] +xpack.monitoring.elasticsearch.username: elastic +xpack.monitoring.elasticsearch.password: ${ELASTIC_PASSWORD} +xpack.monitoring.enabled: true +pipeline.workers: 4 +pipeline.batch.size: 1000 +pipeline.batch.delay: 50 +queue.type: persisted +queue.max_bytes: 1gb +``` + +启动Logstash: + +```bash +docker-compose up -d +``` + +## 6. Filebeat部署 + +### 6.1 创建Filebeat配置 + +创建filebeat.yml配置文件: + +```yaml +filebeat.inputs: +- type: log + enabled: true + paths: + - /var/log/app/*.log + fields: + log_type: app + fields_under_root: true + json.keys_under_root: true + json.message_key: log + json.add_error_key: true + +- type: log + enabled: true + paths: + - /var/log/system/*.log + fields: + log_type: system + fields_under_root: true + +processors: +- add_host_metadata: ~ +- add_cloud_metadata: ~ + +output.kafka: + hosts: ["kafka-1:29092", "kafka-2:29093", "kafka-3:29094"] + topic: '%{[log_type]}-logs' + partition.round_robin: + reachable_only: false + required_acks: 1 + compression: gzip + max_message_bytes: 1000000 + +logging.level: info +logging.to_files: true +logging.files: + path: /var/log/filebeat + name: filebeat + keepfiles: 7 + permissions: 0644 +``` + +### 6.2 部署Filebeat到应用服务器 + +创建docker-compose.yml文件: + +```yaml +version: '3' +services: + filebeat: + image: docker.elastic.co/beats/filebeat:7.17.0 + container_name: filebeat + user: root + volumes: + - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro + - /var/log:/var/log:ro + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - strict.perms=false + networks: + - kafka-net + +networks: + kafka-net: + external: true +``` + +启动Filebeat: + +```bash +docker-compose up -d +``` + +## 7. Kibana部署 + +### 7.1 安装Kibana + +创建docker-compose.yml文件: + +```yaml +version: '3' +services: + kibana: + image: docker.elastic.co/kibana/kibana:7.17.0 + container_name: kibana + environment: + ELASTICSEARCH_HOSTS: '"http://es01:9200","http://es02:9200","http://es03:9200"' + ELASTICSEARCH_USERNAME: elastic + ELASTICSEARCH_PASSWORD: "your_elastic_password" + ports: + - 5601:5601 + networks: + - elastic + +networks: + elastic: + external: true +``` + +启动Kibana: + +```bash +docker-compose up -d +``` + +### 7.2 配置Kibana仪表板 + +1. 访问Kibana界面:`http://your-kibana-host:5601` +2. 创建索引模式: + - 导航到 Management > Stack Management > Kibana > Index Patterns + - 创建索引模式 `app-logs-*` 和 `system-logs-*` + - 设置时间字段为 `@timestamp` +3. 创建可视化和仪表板: + - 导航到 Dashboard + - 创建新仪表板 + - 添加各种可视化组件,如日志计数、错误率、响应时间等 + +## 8. 系统监控与告警 + +### 8.1 设置Elasticsearch监控 + +```bash +curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d' +{ + "persistent": { + "xpack.monitoring.collection.enabled": true + } +}' +``` + +### 8.2 配置Kibana告警 + +1. 导航到 Management > Stack Management > Alerts and Insights > Rules +2. 创建新规则,例如: + - 当错误日志数量在5分钟内超过100条时发送告警 + - 当集群健康状态变为黄色或红色时发送告警 + - 当磁盘使用率超过85%时发送告警 + +### 8.3 配置通知渠道 + +1. 导航到 Management > Stack Management > Alerts and Insights > Connectors +2. 添加通知渠道,如Email、Slack、WebHook等 + +## 9. 性能优化 + +### 9.1 Elasticsearch性能优化 + +```yaml +# 添加到elasticsearch.yml +indices.memory.index_buffer_size: 30% +indices.queries.cache.size: 10% +thread_pool.write.queue_size: 1000 +thread_pool.search.queue_size: 1000 +``` + +### 9.2 Kafka性能优化 + +```properties +# 添加到server.properties +num.io.threads=16 +num.network.threads=8 +socket.send.buffer.bytes=1048576 +socket.receive.buffer.bytes=1048576 +socket.request.max.bytes=104857600 +log.flush.interval.messages=10000 +log.flush.interval.ms=1000 +log.retention.hours=168 +log.segment.bytes=1073741824 +log.cleaner.enable=true +``` + +### 9.3 Logstash性能优化 + +```yaml +# 添加到logstash.yml +pipeline.workers: 8 +pipeline.batch.size: 2000 +pipeline.batch.delay: 50 +queue.type: persisted +queue.max_bytes: 2gb +``` + +## 10. 备份与恢复策略 + +### 10.1 Elasticsearch快照备份 + +```bash +# 注册快照仓库 +curl -X PUT "localhost:9200/_snapshot/backup_repo" -H 'Content-Type: application/json' -d' +{ + "type": "fs", + "settings": { + "location": "/usr/share/elasticsearch/backup" + } +}' + +# 创建快照 +curl -X PUT "localhost:9200/_snapshot/backup_repo/snapshot_1?wait_for_completion=true" +``` + +### 10.2 自动备份脚本 + +```bash +#!/bin/bash + +DATE=$(date +%Y%m%d) +SNAPSHOT_NAME="snapshot_${DATE}" + +# 创建快照 +curl -X PUT "http://localhost:9200/_snapshot/backup_repo/${SNAPSHOT_NAME}?wait_for_completion=true" -H 'Content-Type: application/json' -d' +{ + "indices": "*", + "ignore_unavailable": true, + "include_global_state": true +}' + +# 删除7天前的快照 +OLD_DATE=$(date -d "7 days ago" +%Y%m%d) +OLD_SNAPSHOT="snapshot_${OLD_DATE}" +curl -X DELETE "http://localhost:9200/_snapshot/backup_repo/${OLD_SNAPSHOT}" +``` + +## 11. 故障排除 + +### 11.1 常见问题及解决方案 + +1. **Elasticsearch集群状态为黄色或红色** + - 检查节点是否正常运行 + - 检查磁盘空间 + - 查看日志文件中的错误信息 + +2. **Kafka消息积压** + - 增加Logstash实例数量 + - 优化Logstash配置,提高处理效率 + - 检查Elasticsearch写入性能 + +3. **Filebeat无法发送日志到Kafka** + - 检查网络连接 + - 验证Kafka集群状态 + - 查看Filebeat日志 + +### 11.2 日志分析命令 + +```bash +# 查看Elasticsearch集群健康状态 +curl -X GET "localhost:9200/_cluster/health?pretty" + +# 查看Elasticsearch索引状态 +curl -X GET "localhost:9200/_cat/indices?v" + +# 查看Kafka消费组状态 +kafka-consumer-groups --bootstrap-server kafka-1:29092 --describe --group logstash + +# 查看Logstash处理延迟 +curl -X GET "localhost:9600/_node/stats/pipeline?pretty" +``` + +## 12. 扩展与升级 + +### 12.1 扩展Elasticsearch集群 + +1. 准备新节点,安装Elasticsearch +2. 配置与现有集群相同的集群名称 +3. 设置discovery.seed_hosts指向现有节点 +4. 启动新节点,它将自动加入集群 + +### 12.2 升级ELFK组件 + +升级Elasticsearch: + +1. 备份数据 +2. 逐个节点升级,先升级从节点,最后升级主节点 +3. 每次升级后验证集群健康状态 + +升级Kafka: + +1. 逐个升级Kafka broker +2. 确保每个broker升级后正常工作再升级下一个 + +升级Logstash和Filebeat: + +1. 升级配置文件以兼容新版本 +2. 逐个升级实例 + +## 13. 安全加固 + +### 13.1 网络安全 + +1. 使用专用网络隔离ELFK集群 +2. 配置防火墙,只允许必要的端口访问 +3. 使用TLS加密集群内部通信 + +### 13.2 认证与授权 + +1. 启用Elasticsearch安全功能 +2. 创建不同角色的用户,遵循最小权限原则 +3. 使用API密钥进行服务间认证 + +### 13.3 数据安全 + +1. 启用传输和存储加密 +2. 实施数据脱敏,保护敏感信息 +3. 定期审计访问日志 + +## 14. 总结 + +本文详细介绍了如何搭建生产级ELFK集群,包括: + +- Elasticsearch集群部署与配置 +- Kafka集群部署与主题创建 +- Logstash配置与优化 +- Filebeat部署到应用服务器 +- Kibana安装与仪表板配置 +- 系统监控与告警设置 +- 性能优化建议 +- 备份与恢复策略 +- 故障排除指南 +- 扩展与升级方法 +- 安全加固措施 + +通过遵循本指南,您可以构建一个高可用、高性能、安全可靠的ELFK日志系统,为应用程序提供强大的日志收集、处理、存储和分析能力。 diff --git a/docs/worker/grafana-database-monitoring.md b/docs/worker/grafana-database-monitoring.md new file mode 100644 index 000000000..daa4df9a8 --- /dev/null +++ b/docs/worker/grafana-database-monitoring.md @@ -0,0 +1,300 @@ +--- +title: Grafana监控MySQL、Redis和MongoDB +author: 哪吒 +date: '2023-07-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Grafana监控MySQL、Redis和MongoDB + +本文将介绍如何使用Grafana配置MySQL、Redis和MongoDB的监控面板,实现对这些关键数据库的实时监控和性能分析。 + +### 前提条件 + +在开始之前,请确保您已经按照[安装监控grafana](./grafana.md)文档完成了Grafana和Prometheus的基础安装。 + +### 1. MySQL监控配置 + +#### 1.1 安装MySQL Exporter + +首先,我们需要安装MySQL Exporter,它能够收集MySQL的性能指标并暴露给Prometheus。 + +在docker-compose.yml文件中添加以下配置: + +```yaml +mysql-exporter: + image: prom/mysqld-exporter:latest + container_name: "mysql-exporter" + ports: + - "9104:9104" + environment: + - DATA_SOURCE_NAME=用户名:密码@(MySQL主机地址:3306)/ + restart: always +``` + +#### 1.2 更新Prometheus配置 + +在prometheus.yml文件中添加MySQL监控配置: + +```yaml +- job_name: 'mysql' + scrape_interval: 5s + static_configs: + - targets: ['mysql-exporter:9104'] +``` + +#### 1.3 导入MySQL监控面板 + +1. 登录Grafana界面(默认地址:http://your-server-ip:3000) +2. 点击左侧菜单的 "+" 按钮,选择 "Import" +3. 输入面板ID:7362(MySQL Overview)或 11323(MySQL InnoDB Metrics) +4. 在数据源下拉菜单中选择您的Prometheus数据源 +5. 点击 "Import" 完成导入 + +#### 1.4 MySQL监控关键指标 + +- **连接数**:当前活动连接数和最大连接数 +- **查询性能**:QPS(每秒查询数)、慢查询数 +- **InnoDB指标**:缓冲池使用率、读写操作 +- **表锁定**:锁等待次数和时间 +- **网络流量**:接收和发送的字节数 + +### 2. Redis监控配置 + +#### 2.1 安装Redis Exporter + +在docker-compose.yml文件中添加以下配置: + +```yaml +redis-exporter: + image: oliver006/redis_exporter:latest + container_name: "redis-exporter" + ports: + - "9121:9121" + environment: + - REDIS_ADDR=redis://Redis主机地址:6379 + - REDIS_PASSWORD=您的Redis密码 + restart: always +``` + +#### 2.2 更新Prometheus配置 + +在prometheus.yml文件中添加Redis监控配置: + +```yaml +- job_name: 'redis' + scrape_interval: 5s + static_configs: + - targets: ['redis-exporter:9121'] +``` + +#### 2.3 导入Redis监控面板 + +1. 登录Grafana界面 +2. 点击左侧菜单的 "+" 按钮,选择 "Import" +3. 输入面板ID:763(Redis Dashboard for Prometheus Redis Exporter) +4. 在数据源下拉菜单中选择您的Prometheus数据源 +5. 点击 "Import" 完成导入 + +#### 2.4 Redis监控关键指标 + +- **内存使用**:已用内存、内存碎片率 +- **连接数**:客户端连接数、被拒绝的连接 +- **命令执行**:每秒执行的命令数、命令延迟 +- **键空间**:键总数、过期键数、被驱逐键数 +- **持久化**:RDB保存状态、AOF重写状态 + +### 3. MongoDB监控配置 + +#### 3.1 安装MongoDB Exporter + +在docker-compose.yml文件中添加以下配置: + +```yaml +mongodb-exporter: + image: percona/mongodb_exporter:latest + container_name: "mongodb-exporter" + ports: + - "9216:9216" + command: + - '--mongodb.uri=mongodb://用户名:密码@MongoDB主机地址:27017' + restart: always +``` + +#### 3.2 更新Prometheus配置 + +在prometheus.yml文件中添加MongoDB监控配置: + +```yaml +- job_name: 'mongodb' + scrape_interval: 5s + static_configs: + - targets: ['mongodb-exporter:9216'] +``` + +#### 3.3 导入MongoDB监控面板 + +1. 登录Grafana界面 +2. 点击左侧菜单的 "+" 按钮,选择 "Import" +3. 输入面板ID:7353(MongoDB Overview) +4. 在数据源下拉菜单中选择您的Prometheus数据源 +5. 点击 "Import" 完成导入 + +#### 3.4 MongoDB监控关键指标 + +- **操作性能**:查询、插入、更新、删除操作的数量和延迟 +- **连接数**:当前连接数、可用连接数 +- **内存使用**:虚拟内存、常驻内存 +- **WiredTiger缓存**:读/写操作、缓存使用率 +- **复制集状态**:主从延迟、选举状态 + +### 4. 完整的docker-compose.yml示例 + +以下是包含所有三个数据库监控的完整docker-compose.yml示例: + +```yaml +version: "3.7" +services: + node-exporter: + image: prom/node-exporter:latest + container_name: "node-exporter0" + ports: + - "9100:9100" + restart: always + prometheus: + image: prom/prometheus:latest + container_name: "prometheus0" + restart: always + ports: + - "9090:9090" + volumes: + - "./prometheus.yml:/etc/prometheus/prometheus.yml" + - "./prometheus_data:/prometheus" + grafana: + image: grafana/grafana + container_name: "grafana0" + ports: + - "3000:3000" + restart: always + volumes: + - "./grafana_data:/var/lib/grafana" + mysql-exporter: + image: prom/mysqld-exporter:latest + container_name: "mysql-exporter" + ports: + - "9104:9104" + environment: + - DATA_SOURCE_NAME=用户名:密码@(MySQL主机地址:3306)/ + restart: always + redis-exporter: + image: oliver006/redis_exporter:latest + container_name: "redis-exporter" + ports: + - "9121:9121" + environment: + - REDIS_ADDR=redis://Redis主机地址:6379 + - REDIS_PASSWORD=您的Redis密码 + restart: always + mongodb-exporter: + image: percona/mongodb_exporter:latest + container_name: "mongodb-exporter" + ports: + - "9216:9216" + command: + - '--mongodb.uri=mongodb://用户名:密码@MongoDB主机地址:27017' + restart: always +``` + +### 5. 完整的prometheus.yml示例 + +```yaml +global: + scrape_interval: 15s # 默认抓取周期 + external_labels: + monitor: 'codelab-monitor' +scrape_configs: + - job_name: 'node-exporter' #服务的名称 + scrape_interval: 5s + metrics_path: /metrics #获取指标的url + static_configs: + - targets: ['192.168.0.221:9100'] # 主机监控 + + - job_name: 'electricity' #服务的名称 + scrape_interval: 5s + metrics_path: /actuator/prometheus #获取指标的url + static_configs: + - targets: ['192.168.0.130:28097'] # 应用监控 + + - job_name: 'mysql' + scrape_interval: 5s + static_configs: + - targets: ['mysql-exporter:9104'] + + - job_name: 'redis' + scrape_interval: 5s + static_configs: + - targets: ['redis-exporter:9121'] + + - job_name: 'mongodb' + scrape_interval: 5s + static_configs: + - targets: ['mongodb-exporter:9216'] +``` + +### 6. 监控面板效果展示 + +#### MySQL监控面板 + +![MySQL监控面板](./mysql-dashboard.svg) + +#### Redis监控面板 + +![Redis监控面板](./redis-dashboard.svg) + +#### MongoDB监控面板 + +![MongoDB监控面板](./mongodb-dashboard.svg) + +### 7. 告警配置 + +除了可视化监控外,Grafana还支持设置告警规则,当指标超过阈值时发送通知。以下是一些常用的告警配置示例: + +#### MySQL告警示例 + +- 当连接数超过最大连接数的80%时告警 +- 当慢查询数每分钟超过10次时告警 +- 当InnoDB缓冲池使用率低于20%时告警 + +#### Redis告警示例 + +- 当内存使用率超过90%时告警 +- 当被拒绝的连接数大于0时告警 +- 当键被驱逐的速率超过每秒100个时告警 + +#### MongoDB告警示例 + +- 当连接数超过最大连接数的80%时告警 +- 当查询延迟超过100ms时告警 +- 当复制集延迟超过10秒时告警 + +### 8. 故障排查 + +如果您在配置过程中遇到问题,可以尝试以下故障排查步骤: + +1. 检查各个Exporter的日志,确保它们能够正常连接到数据库 +2. 访问Prometheus的targets页面(http://your-server-ip:9090/targets),确保所有目标都处于UP状态 +3. 检查防火墙设置,确保Prometheus能够访问各个Exporter的端口 +4. 验证数据库用户是否具有足够的权限来收集监控指标 + +### 9. 最佳实践 + +- 为敏感信息(如数据库密码)使用环境变量或Docker secrets +- 根据实际需求调整scrape_interval,频繁的抓取会增加数据库负载 +- 为重要指标设置告警,并配置合适的通知渠道(如邮件、Slack、钉钉等) +- 定期清理Prometheus的历史数据,避免磁盘空间耗尽 +- 为Grafana设置访问控制,防止未授权访问 + +通过以上配置,您可以全面监控MySQL、Redis和MongoDB的性能指标,及时发现潜在问题,保障系统的稳定运行。 \ No newline at end of file diff --git a/docs/worker/img/elfk-architecture.svg b/docs/worker/img/elfk-architecture.svg new file mode 100644 index 000000000..e2fe43431 --- /dev/null +++ b/docs/worker/img/elfk-architecture.svg @@ -0,0 +1,114 @@ + + + + + + ELFK 生产集群架构 + + + + + + + + + + + + + + + + + 应用服务器 + + + + Filebeat + + + + + + + + + Kafka 集群 + 消息队列 + + + + + + + + Logstash + 日志处理 + + + + + + + + + + + + Elasticsearch 集群 + 存储与检索 + + + + + + + + Kibana + 可视化 + + + + + + + + + + + + + + + + + + + + + 日志收集 + 消息消费 + 数据索引 + 数据查询与展示 + + + + + Filebeat - 轻量级日志收集器 + + + Kafka - 分布式消息队列 + + + Logstash - 数据处理管道 + + + Elasticsearch - 分布式搜索引擎 + + + Kibana - 数据可视化平台 + + + + ELFK集群提供高可用、高性能、可扩展的日志收集、处理、存储和分析能力 + \ No newline at end of file diff --git a/docs/worker/k8s-app-monitoring.svg b/docs/worker/k8s-app-monitoring.svg new file mode 100644 index 000000000..cada74fae --- /dev/null +++ b/docs/worker/k8s-app-monitoring.svg @@ -0,0 +1,115 @@ + + + + + + Spring Boot 应用监控 + + + + + JVM内存使用 + + + + + + + 0 + 1GB + 2GB + + 0m + 5m + 10m + 15m + 20m + + + + + + + + + + 堆内存 + + + 非堆内存 + + + + + + HTTP请求统计 + + + + 请求总数 + 12,458 + +15.2% + + + + 平均响应时间 + 78ms + -5.3% + + + + 错误率 + 0.5% + -0.2% + + + + 活跃会话 + 245 + +8.7% + + + + + + 端点性能 + + + + 端点 + 请求数 + 平均响应时间 + 最大响应时间 + 错误数 + 错误率 + + + + /api/v1/users + 3,245 + 45ms + 320ms + 12 + 0.37% + + + + /api/v1/products + 2,876 + 65ms + 450ms + 8 + 0.28% + + + + /api/v1/orders + 1,987 + 120ms + 780ms + 25 + 1.26% + + \ No newline at end of file diff --git a/docs/worker/k8s-cluster-overview.svg b/docs/worker/k8s-cluster-overview.svg new file mode 100644 index 000000000..52f03b495 --- /dev/null +++ b/docs/worker/k8s-cluster-overview.svg @@ -0,0 +1,66 @@ + + + + + + Kubernetes 集群概览 + + + + + + 集群状态 + 控制平面组件 + 健康 + API Server 响应时间: 12ms + + + + 节点状态 + 就绪/总计 + 5 / 5 + 资源压力: 低 + + + + Pod状态 + 运行/总计 + 42 / 45 + 3个Pod处于Pending状态 + + + + + + + 集群CPU使用率 + + + + + + + 时间 (过去1小时) + + + + 使用率 (%) + + + + 集群内存使用率 + + + + + + + 时间 (过去1小时) + + + + 使用率 (%) + + \ No newline at end of file diff --git a/docs/worker/k8s-monitoring.md b/docs/worker/k8s-monitoring.md new file mode 100644 index 000000000..925308b19 --- /dev/null +++ b/docs/worker/k8s-monitoring.md @@ -0,0 +1,392 @@ +--- +title: Kubernetes全栈监控实践 +author: 哪吒 +date: '2023-07-20' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Kubernetes全栈监控实践 + +本文将介绍如何在Kubernetes环境中搭建全栈监控系统,实现对集群、节点、Pod、容器和应用的全方位监控。 + +### 1. Kubernetes监控概述 + +#### 1.1 监控层次 + +Kubernetes环境的监控通常分为以下几个层次: + +- **基础设施层**:物理/虚拟机、网络设备 +- **Kubernetes集群层**:控制平面组件(API Server、Scheduler、Controller Manager、etcd) +- **节点层**:Node状态、资源使用率(CPU、内存、磁盘、网络) +- **Pod/容器层**:容器资源使用、健康状态 +- **应用层**:业务指标、请求延迟、错误率 + +#### 1.2 监控架构 + +我们将采用以下组件构建Kubernetes监控系统: + +- **Prometheus**:时序数据库,用于存储和查询监控指标 +- **Grafana**:可视化平台,用于展示监控数据 +- **Alertmanager**:告警管理器,处理告警路由和通知 +- **Node Exporter**:收集节点级别的指标 +- **kube-state-metrics**:提供Kubernetes对象的状态指标 +- **Prometheus Operator**:简化Prometheus在Kubernetes上的部署和管理 + +### 2. 使用Prometheus Operator部署监控系统 + +#### 2.1 安装Helm + +Helm是Kubernetes的包管理工具,我们将使用它来部署Prometheus Operator。 + +```bash +# 下载Helm安装脚本 +curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + +# 添加执行权限 +chmod 700 get_helm.sh + +# 执行安装脚本 +./get_helm.sh +``` + +#### 2.2 添加Prometheus社区Helm仓库 + +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update +``` + +#### 2.3 创建监控命名空间 + +```bash +kubectl create namespace monitoring +``` + +#### 2.4 部署Prometheus Operator + +```bash +helm install prometheus prometheus-community/kube-prometheus-stack \ + --namespace monitoring \ + --set grafana.adminPassword=admin \ + --set prometheus.prometheusSpec.retention=15d +``` + +这个Helm Chart会部署以下组件: +- Prometheus Operator +- Prometheus实例 +- Alertmanager +- Grafana +- Node Exporter +- kube-state-metrics +- 各种ServiceMonitor和PodMonitor资源 + +#### 2.5 验证部署 + +```bash +# 检查Pod状态 +kubectl get pods -n monitoring + +# 检查Service状态 +kubectl get svc -n monitoring +``` + +#### 2.6 访问Grafana + +```bash +# 端口转发Grafana服务 +kubectl port-forward svc/prometheus-grafana 3000:80 -n monitoring +``` + +现在可以通过浏览器访问 http://localhost:3000 来访问Grafana。默认用户名是 `admin`,密码是上面设置的 `admin`。 + +### 3. 监控Kubernetes集群组件 + +#### 3.1 控制平面监控 + +Prometheus Operator已经配置了对Kubernetes控制平面组件的监控,包括: + +- **API Server**:请求延迟、请求率、错误率 +- **Scheduler**:调度延迟、调度错误 +- **Controller Manager**:控制循环延迟 +- **etcd**:提案提交率、提案失败率、数据库大小 + +在Grafana中,可以找到名为 "Kubernetes / Control Plane" 的预配置仪表板。 + +#### 3.2 节点监控 + +Node Exporter收集的节点级别指标包括: + +- **CPU使用率**:用户态、系统态、IO等待 +- **内存使用率**:已用、可用、缓存 +- **磁盘使用率**:读写操作、空间使用 +- **网络流量**:接收和发送的字节数、包数 +- **系统负载**:1分钟、5分钟、15分钟平均负载 + +在Grafana中,可以找到名为 "Kubernetes / Compute Resources / Node" 的预配置仪表板。 + +#### 3.3 Pod和容器监控 + +kubelet的cAdvisor组件收集容器级别的指标,包括: + +- **容器CPU使用率** +- **容器内存使用率** +- **容器网络流量** +- **容器文件系统使用率** + +在Grafana中,可以找到名为 "Kubernetes / Compute Resources / Pod" 和 "Kubernetes / Compute Resources / Container" 的预配置仪表板。 + +### 4. 监控应用 + +#### 4.1 为应用添加Prometheus指标 + +以Spring Boot应用为例,添加Prometheus指标支持: + +1. 添加依赖: + +```xml + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + +``` + +2. 配置application.properties: + +```properties +management.endpoints.web.exposure.include=prometheus,health,info +management.endpoint.prometheus.enabled=true +management.metrics.export.prometheus.enabled=true +``` + +#### 4.2 创建ServiceMonitor + +创建一个ServiceMonitor资源,让Prometheus自动发现并抓取应用指标: + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: spring-boot-app + namespace: monitoring +spec: + selector: + matchLabels: + app: spring-boot-app # 应用Service的标签 + endpoints: + - port: http # Service中暴露指标的端口名称 + path: /actuator/prometheus # 指标路径 + interval: 15s # 抓取间隔 +``` + +#### 4.3 导入应用仪表板 + +在Grafana中,可以导入ID为4701的「JVM Micrometer」仪表板,用于监控Spring Boot应用。 + +### 5. 配置告警 + +#### 5.1 创建PrometheusRule + +创建一个PrometheusRule资源,定义告警规则: + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: kubernetes-apps + namespace: monitoring +spec: + groups: + - name: kubernetes-apps + rules: + - alert: PodHighCpuUsage + expr: sum(rate(container_cpu_usage_seconds_total{container!="",pod!=""}[5m])) by (pod, namespace) > 0.8 + for: 5m + labels: + severity: warning + annotations: + summary: "Pod CPU使用率过高" + description: "Pod {{ $labels.pod }} 在命名空间 {{ $labels.namespace }} 的CPU使用率超过80%已持续5分钟。" + + - alert: PodHighMemoryUsage + expr: sum(container_memory_working_set_bytes{container!="",pod!=""}) by (pod, namespace) / sum(container_spec_memory_limit_bytes{container!="",pod!=""}) by (pod, namespace) > 0.8 + for: 5m + labels: + severity: warning + annotations: + summary: "Pod内存使用率过高" + description: "Pod {{ $labels.pod }} 在命名空间 {{ $labels.namespace }} 的内存使用率超过80%已持续5分钟。" +``` + +#### 5.2 配置Alertmanager + +创建一个Secret来配置Alertmanager: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: alertmanager-prometheus-kube-prometheus-alertmanager + namespace: monitoring +stringData: + alertmanager.yaml: | + global: + resolve_timeout: 5m + smtp_smarthost: 'smtp.example.com:587' + smtp_from: 'alertmanager@example.com' + smtp_auth_username: 'alertmanager' + smtp_auth_password: 'password' + route: + group_by: ['job', 'alertname', 'namespace'] + group_wait: 30s + group_interval: 5m + repeat_interval: 12h + receiver: 'email' + routes: + - match: + severity: critical + receiver: 'pager' + receivers: + - name: 'email' + email_configs: + - to: 'alerts@example.com' + - name: 'pager' + email_configs: + - to: 'oncall@example.com' + webhook_configs: + - url: 'https://api.example.com/webhook' +type: Opaque +``` + +### 6. 高级配置 + +#### 6.1 自定义Prometheus存储 + +对于生产环境,建议配置持久化存储: + +```yaml +prometheus: + prometheusSpec: + retention: 30d + storageSpec: + volumeClaimTemplate: + spec: + storageClassName: standard + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 100Gi +``` + +#### 6.2 配置远程存储 + +对于长期存储,可以配置Prometheus将数据发送到远程存储: + +```yaml +prometheus: + prometheusSpec: + remoteWrite: + - url: "http://thanos-receive.monitoring.svc:19291/api/v1/receive" +``` + +#### 6.3 使用Thanos扩展Prometheus + +Thanos可以提供全局查询视图、长期存储和高可用性: + +```bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm install thanos bitnami/thanos \ + --namespace monitoring \ + --set objstoreConfig="type: S3\naccessKey: YOUR_ACCESS_KEY\nsecretKey: YOUR_SECRET_KEY\nbucket: thanos\nendpoint: s3.amazonaws.com" +``` + +## 监控面板示例 + +### 集群概览 + +![Kubernetes集群概览](/worker/k8s-cluster-overview.svg) + +*Kubernetes集群概览面板展示了集群的整体健康状态和资源使用情况* + +### 节点资源监控 + +![Kubernetes节点资源使用率](/worker/k8s-node-resources.svg) + +*Kubernetes节点资源监控面板展示了各节点的CPU、内存和磁盘使用率以及Pod数量* + +### Pod监控 + +![Kubernetes Pod监控](/worker/k8s-pod-monitoring.svg) + +*Kubernetes Pod监控面板展示了Pod的状态分布、资源使用情况和关键Pod的详细信息* + +### 应用监控 + +![Spring Boot应用监控](/worker/k8s-app-monitoring.svg) + +*Spring Boot应用监控面板展示了应用的JVM内存使用、HTTP请求统计和端点性能数据* + +### 8. 最佳实践 + +#### 8.1 资源规划 + +- **Prometheus**:根据指标数量和保留时间调整资源。一般建议每天1GB的数据量,加上30%的余量。 +- **Grafana**:对于中小型集群,1CPU和1GB内存通常足够。 +- **Alertmanager**:资源需求较小,0.5CPU和256MB内存通常足够。 + +#### 8.2 性能优化 + +- 调整抓取间隔:默认为15s,可以根据需要调整。 +- 使用标签选择器限制抓取范围。 +- 配置适当的保留期限,避免存储过多历史数据。 +- 对于大型集群,考虑使用Prometheus联邦或Thanos。 + +#### 8.3 安全性考虑 + +- 使用NetworkPolicy限制Prometheus的访问范围。 +- 为Grafana配置适当的认证和授权。 +- 敏感信息(如告警接收者的凭据)应使用Secret存储。 + +### 9. 故障排查 + +#### 9.1 Prometheus无法抓取指标 + +- 检查ServiceMonitor/PodMonitor的标签选择器是否正确。 +- 验证目标端点是否可访问(使用kubectl port-forward测试)。 +- 检查Prometheus日志中的错误信息。 + +```bash +kubectl logs -f prometheus-prometheus-kube-prometheus-prometheus-0 -n monitoring -c prometheus +``` + +#### 9.2 Grafana无法显示数据 + +- 验证Prometheus数据源配置是否正确。 +- 使用Prometheus UI测试查询表达式。 +- 检查Grafana日志中的错误信息。 + +```bash +kubectl logs -f prometheus-grafana-xxxxxxxxxx-xxxxx -n monitoring -c grafana +``` + +#### 9.3 告警未触发 + +- 在Prometheus UI中检查告警规则状态。 +- 验证告警表达式是否正确。 +- 检查Alertmanager的配置和日志。 + +```bash +kubectl logs -f alertmanager-prometheus-kube-prometheus-alertmanager-0 -n monitoring -c alertmanager +``` + +### 10. 结论 + +通过本文介绍的方法,您可以在Kubernetes环境中搭建全栈监控系统,实现对集群、节点、Pod、容器和应用的全方位监控。这将帮助您及时发现和解决潜在问题,保障系统的稳定运行。 + +随着Kubernetes生态的不断发展,监控工具和最佳实践也在不断演进。建议定期关注社区动态,及时更新监控系统,以获得更好的监控效果。 \ No newline at end of file diff --git a/docs/worker/k8s-node-resources.svg b/docs/worker/k8s-node-resources.svg new file mode 100644 index 000000000..e74c04001 --- /dev/null +++ b/docs/worker/k8s-node-resources.svg @@ -0,0 +1,154 @@ + + + + + + Kubernetes 节点资源使用率 + + + + + + 节点名称 + 状态 + CPU使用率 + 内存使用率 + 磁盘使用率 + Pod数量 + + + + worker-node-1 + + + + + + 65% + + + + + 78% + + + + + 45% + + 12 / 30 + + + + worker-node-2 + + + + + + 72% + + + + + 85% + + + + + 52% + + 15 / 30 + + + + worker-node-3 + + + + + + 58% + + + + + 70% + + + + + 48% + + 10 / 30 + + + + master-node-1 + + + + + + 42% + + + + + 55% + + + + + 38% + + 5 / 20 + + + + master-node-2 + + + + + + 38% + + + + + 50% + + + + + 35% + + 3 / 20 + + + + + + 资源使用率图例 + + + + 正常 (0-70%) + + + + 警告 (70-85%) + + + + 危险 (85-100%) + + + + 就绪 + + + 未就绪 + + \ No newline at end of file diff --git a/docs/worker/k8s-pod-monitoring.svg b/docs/worker/k8s-pod-monitoring.svg new file mode 100644 index 000000000..384e1238c --- /dev/null +++ b/docs/worker/k8s-pod-monitoring.svg @@ -0,0 +1,122 @@ + + + + + + Kubernetes Pod 监控面板 + + + + + Pod状态概览 + + + + 运行中: + 42 + + + + 待定: + 3 + + + + 失败: + 1 + + + + + + 命名空间分布 + + + + + + + + + + + + + + + + + + + kube-system + + + default + + + monitoring + + + application + + + + + + 资源使用率 + + + CPU请求/限制: + + + 70% + + + 内存请求/限制: + + + 80% + + + + + + 关键Pod状态 + + + + Pod名称 + 命名空间 + 状态 + CPU使用 + 内存使用 + 重启次数 + + + + prometheus-server-5fb8b98765-abcd + monitoring + + 250m (25%) + 1.2Gi (60%) + 0 + + + + grafana-deployment-6c64d84f95-efgh + monitoring + + 120m (12%) + 512Mi (40%) + 0 + + + + api-gateway-deployment-7d8f9b6c5-ijkl + application + + 350m (35%) + 1.5Gi (75%) + 2 + + \ No newline at end of file diff --git a/docs/worker/mongodb-dashboard.svg b/docs/worker/mongodb-dashboard.svg new file mode 100644 index 000000000..be963d17b --- /dev/null +++ b/docs/worker/mongodb-dashboard.svg @@ -0,0 +1,72 @@ + + + + + + MongoDB 监控面板 + + + + + + 每秒操作数 + 3,721 + 过去1分钟平均 + + + + 连接数 + 187 / 500 + 当前 / 最大 + + + + 内存使用 + 3.2 GB + 常驻内存 + + + + + + + 操作类型分布 + + + + + 查询 + 2,150/s + + + + 插入 + 1,050/s + + + + 更新 + 450/s + + + + 删除 + 71/s + + + + WiredTiger缓存使用率 + + + + + + + 时间 (过去24小时) + + + + 使用率 (%) + + \ No newline at end of file diff --git a/docs/worker/mysql-dashboard.svg b/docs/worker/mysql-dashboard.svg new file mode 100644 index 000000000..71a7bd715 --- /dev/null +++ b/docs/worker/mysql-dashboard.svg @@ -0,0 +1,68 @@ + + + + + + MySQL 监控面板 + + + + + + 连接数 + 142 / 500 + 当前 / 最大 + + + + 每秒查询数 (QPS) + 1,245 + 过去1分钟平均 + + + + 慢查询数 + 12 + 过去1小时 + + + + + + + InnoDB缓冲池使用率 + + + + + + + 时间 (过去24小时) + + + + 使用率 (%) + + + + 表锁定等待次数 + + + + + + + + + + + + + 时间 (过去24小时) + + + + 次数 + + \ No newline at end of file diff --git a/docs/worker/nginx-optimization.md b/docs/worker/nginx-optimization.md new file mode 100644 index 000000000..7509fb32e --- /dev/null +++ b/docs/worker/nginx-optimization.md @@ -0,0 +1,405 @@ +--- +title: Nginx优化与防盗链 +author: 哪吒 +date: '2023-05-20' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +# Nginx优化与防盗链 + +## 一、Nginx性能优化 + +### 1.1 基础优化 + +#### 1.1.1 worker进程优化 + +```nginx +# 设置worker进程数量,通常设置为CPU核心数 +worker_processes auto; + +# 绑定worker进程到指定CPU,避免进程切换带来的开销 +worker_cpu_affinity auto; + +# 每个worker进程可以打开的最大文件描述符数量 +worker_rlimit_nofile 65535; +``` + +#### 1.1.2 事件处理优化 + +```nginx +events { + # 使用epoll事件驱动模型,Linux系统下效率最高 + use epoll; + + # 每个worker进程的最大连接数 + worker_connections 10240; + + # 尽可能接受所有新连接 + multi_accept on; +} +``` + +#### 1.1.3 HTTP基础优化 + +```nginx +http { + # 开启高效文件传输模式 + sendfile on; + + # 减少网络报文段的数量 + tcp_nopush on; + + # 提高网络包的传输效率 + tcp_nodelay on; + + # 设置客户端连接保持活动的超时时间 + keepalive_timeout 60; + + # 设置请求头的超时时间 + client_header_timeout 10; + + # 设置请求体的超时时间 + client_body_timeout 10; + + # 响应超时时间 + send_timeout 10; + + # 读取请求体的缓冲区大小 + client_body_buffer_size 128k; + + # 读取请求头的缓冲区大小 + client_header_buffer_size 32k; + + # 上传文件大小限制 + client_max_body_size 10m; +} +``` + +### 1.2 静态资源优化 + +#### 1.2.1 Gzip压缩 + +```nginx +http { + # 开启gzip压缩 + gzip on; + + # 压缩的最小文件大小,小于这个值不压缩 + gzip_min_length 1k; + + # 压缩缓冲区大小 + gzip_buffers 4 16k; + + # 压缩HTTP版本 + gzip_http_version 1.1; + + # 压缩级别,1-9,级别越高压缩率越高,但CPU消耗也越大 + gzip_comp_level 6; + + # 需要压缩的MIME类型 + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + # 是否在响应头中添加Vary: Accept-Encoding + gzip_vary on; + + # IE6及以下禁用gzip + gzip_disable "MSIE [1-6]\."; +} +``` + +#### 1.2.2 静态资源缓存 + +```nginx +server { + location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { + # 缓存时间设置 + expires 7d; + + # 添加缓存控制头 + add_header Cache-Control "public, max-age=604800"; + } + + # 针对不同类型文件设置不同的缓存策略 + location ~* \.(html|htm)$ { + expires 1h; + add_header Cache-Control "public, max-age=3600"; + } +} +``` + +### 1.3 负载均衡优化 + +#### 1.3.1 高级负载均衡配置 + +```nginx +upstream backend { + # 使用IP哈希算法,确保同一客户端请求总是发送到同一服务器 + ip_hash; + + # 后端服务器列表 + server 192.168.1.10:8080 weight=5 max_fails=3 fail_timeout=30s; + server 192.168.1.11:8080 weight=3 max_fails=3 fail_timeout=30s; + server 192.168.1.12:8080 weight=2 max_fails=3 fail_timeout=30s; + + # 备用服务器,只有当所有主服务器都不可用时才使用 + server 192.168.1.13:8080 backup; + + # 保持长连接的数量 + keepalive 32; +} + +server { + location / { + proxy_pass http://backend; + + # 设置代理请求头 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 5s; + proxy_send_timeout 10s; + proxy_read_timeout 10s; + + # 启用HTTP/1.1 + proxy_http_version 1.1; + + # 设置连接为长连接 + proxy_set_header Connection ""; + } +} +``` + +#### 1.3.2 健康检查 + +```nginx +http { + # 定义健康检查间隔和参数 + upstream backend { + server 192.168.1.10:8080 max_fails=3 fail_timeout=30s; + server 192.168.1.11:8080 max_fails=3 fail_timeout=30s; + + # 被动健康检查:max_fails表示允许请求失败的次数,fail_timeout表示失败后暂停的时间 + } +} +``` + +## 二、Nginx防盗链配置 + +### 2.1 基于HTTP Referer的防盗链 + +```nginx +server { + # 图片防盗链 + location ~* \.(gif|jpg|jpeg|png|bmp|swf|webp)$ { + # 允许的来源域名 + valid_referers none blocked server_names *.example.com example.* www.example.org/galleries/; + + # 如果referer不是上面指定的,则返回403 + if ($invalid_referer) { + return 403; + } + + # 或者返回一个默认的防盗链图片 + # if ($invalid_referer) { + # rewrite ^/ /images/forbidden.jpg break; + # } + + root /path/to/your/files; + } + + # 视频防盗链 + location ~* \.(mp4|avi|mkv|wmv|flv)$ { + valid_referers none blocked server_names *.example.com example.*; + if ($invalid_referer) { + return 403; + } + root /path/to/your/videos; + } +} +``` + +### 2.2 基于Cookie的防盗链 + +```nginx +server { + location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ { + # 检查cookie + if ($http_cookie !~ "authorized=yes") { + return 403; + } + root /path/to/your/files; + } +} +``` + +### 2.3 基于签名的防盗链(安全哈希) + +```nginx +server { + # 需要安装第三方模块:ngx_http_secure_link_module + location /secure/ { + # 验证链接的有效性 + secure_link $arg_md5,$arg_expires; + secure_link_md5 "$secure_link_expires$uri$remote_addr secret_key"; + + # 如果链接无效或过期 + if ($secure_link = "") { + return 403; + } + + # 如果链接已过期 + if ($secure_link = "0") { + return 410; # Gone + } + + # 正常处理请求 + root /path/to/your/secure/files; + } +} +``` + +### 2.4 替换盗链图片 + +```nginx +server { + location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ { + valid_referers none blocked server_names *.example.com example.*; + + # 如果是盗链,则返回自定义的图片 + if ($invalid_referer) { + # 可以是透明图片或水印图片 + rewrite ^/.*$ /images/watermark.png break; + } + + root /path/to/your/files; + expires 7d; + } +} +``` + +## 三、实际应用案例 + +### 3.1 静态网站优化配置 + +```nginx +server { + listen 80; + server_name www.example.com; + root /var/www/html; + index index.html; + + # 静态资源优化 + location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + + # 防盗链配置 + valid_referers none blocked server_names *.example.com example.*; + if ($invalid_referer) { + return 403; + } + } + + # Gzip压缩 + gzip on; + gzip_min_length 1k; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_vary on; + + # 安全相关头信息 + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Frame-Options SAMEORIGIN; +} +``` + +### 3.2 API服务器优化配置 + +```nginx +server { + listen 80; + server_name api.example.com; + + # API请求限流 + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + + location /api/ { + # 应用限流 + limit_req zone=api_limit burst=20 nodelay; + + # 代理到后端服务 + proxy_pass http://backend_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # 超时设置 + proxy_connect_timeout 5s; + proxy_send_timeout 10s; + proxy_read_timeout 10s; + + # CORS设置 + add_header 'Access-Control-Allow-Origin' 'https://www.example.com'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + + # 预检请求处理 + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + } +} +``` + +## 四、常见问题与解决方案 + +### 4.1 防盗链失效问题 + +1. **Referer头被伪造**: + - 解决方案:结合IP限制、时间戳和签名机制增强防盗链安全性 + - 使用secure_link模块实现基于签名的防盗链 + +2. **移动端浏览器不发送Referer**: + - 解决方案:配置valid_referers包含none选项,允许没有Referer的请求 + +3. **CDN缓存导致防盗链失效**: + - 解决方案:确保CDN配置与Nginx防盗链策略一致,或在CDN层实现防盗链 + +### 4.2 性能优化问题 + +1. **过度压缩导致CPU使用率高**: + - 解决方案:调整gzip_comp_level到适当级别(通常4-6),或对大文件使用预压缩 + +2. **缓存策略不当**: + - 解决方案:根据资源更新频率设置合理的缓存时间,使用版本号或哈希值处理更新 + +3. **连接数限制**: + - 解决方案:调整worker_connections和系统的文件描述符限制 + +## 五、最佳实践建议 + +1. **定期更新Nginx版本**,获取最新的安全补丁和性能改进 + +2. **使用监控工具**(如Prometheus + Grafana)监控Nginx性能指标 + +3. **结合多种防盗链机制**,不要仅依赖单一方法 + +4. **针对不同类型的资源采用不同的优化策略** + +5. **测试配置变更**,使用`nginx -t`验证配置,并在生产环境应用前在测试环境验证 + +6. **记录详细的访问日志**,便于分析访问模式和潜在的盗链行为 + +7. **定期审查防盗链规则**,确保它们不会阻止合法访问 + +8. **考虑使用CDN**,分担源站压力并提供额外的防盗链保护 \ No newline at end of file diff --git a/docs/worker/prometheus-architecture.svg b/docs/worker/prometheus-architecture.svg new file mode 100644 index 000000000..8fbbd8bd1 --- /dev/null +++ b/docs/worker/prometheus-architecture.svg @@ -0,0 +1,80 @@ + + + + + + Prometheus 架构图 + + + + Prometheus Server + 抓取、存储、查询 + + + + Node Exporter + 主机指标 + + + cAdvisor + 容器指标 + + + 应用程序 + 自定义指标 + + + Pushgateway + 短期作业指标 + + + + Grafana + 可视化 + + + Alertmanager + 告警处理 + + + API Clients + 自定义应用 + + + + + + + + + + + + + + + Pull + Pull + Pull + Push + + 查询 + 告警 + API + + + + 核心组件 + + + 数据源 + + + 消费者 + + + Pull/Push + + + 数据流 + \ No newline at end of file diff --git a/docs/worker/prometheus-dashboard.svg b/docs/worker/prometheus-dashboard.svg new file mode 100644 index 000000000..8fdd138be --- /dev/null +++ b/docs/worker/prometheus-dashboard.svg @@ -0,0 +1,95 @@ + + + + + + Prometheus 监控面板 + + + + Prometheus + Alerts + Graph + Status + Help + + + + + rate(node_cpu_seconds_total{mode="idle"}[5m]) + + Execute + + + + + + CPU Idle Time + + + + + + + 12:00 + 12:15 + 12:30 + 12:45 + 13:00 + 13:15 + 13:30 + + + 0 + 0.25 + 0.50 + 0.75 + 1.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CPU0 + + + CPU1 + + + CPU2 + + + CPU3 + + + + Time Range: Last 1 hour + + \ No newline at end of file diff --git a/docs/worker/prometheus.md b/docs/worker/prometheus.md new file mode 100644 index 000000000..a5e2fa96d --- /dev/null +++ b/docs/worker/prometheus.md @@ -0,0 +1,696 @@ +--- +title: Prometheus单机部署 +author: 哪吒 +date: '2023-08-15' +--- + +> 点击勘误[issues](https://github.com/webVueBlog/JavaPlusDoc/issues),哪吒感谢大家的阅读 + + + +## Prometheus单机部署 + +本文将介绍如何在单机环境中部署Prometheus监控系统,实现对服务器和应用的全面监控。 + +### 1. Prometheus简介 + +#### 1.1 什么是Prometheus + +Prometheus是一个开源的系统监控和告警工具包,最初由SoundCloud开发,现在已经成为Cloud Native Computing Foundation(CNCF)的第二个项目(Kubernetes之后)。Prometheus具有以下特点: + +- **多维数据模型**:由指标名称和键值对标签组成的时间序列数据 +- **强大的查询语言PromQL**:可以对收集的时间序列数据进行切片和切块 +- **不依赖分布式存储**:单个服务器节点是自治的 +- **基于HTTP的pull模式**:通过HTTP协议从目标处拉取指标数据 +- **支持推送模式**:通过中间网关支持推送模式 +- **通过服务发现或静态配置发现目标** +- **多种图形和仪表盘支持**:可与Grafana等工具集成 + +#### 1.2 Prometheus架构 + +![Prometheus架构](./prometheus-architecture.svg) + +Prometheus的核心组件包括: + +- **Prometheus Server**:负责抓取和存储时间序列数据 +- **Client Libraries**:用于检测应用程序代码 +- **Pushgateway**:支持短期作业的指标推送 +- **Exporters**:为第三方系统提供指标数据 +- **Alertmanager**:处理告警 +- **可视化工具**:如Grafana等 + +### 2. 环境准备 + +#### 2.1 系统要求 + +- 操作系统:Linux(推荐CentOS 7+/Ubuntu 18.04+)或Windows Server +- 内存:至少2GB RAM(建议4GB以上) +- CPU:至少2核 +- 磁盘:根据数据保留策略,建议至少50GB可用空间 +- Docker:18.09.0或更高版本 +- Docker Compose:1.24.0或更高版本 + +#### 2.2 安装Docker和Docker Compose + +如果您尚未安装Docker和Docker Compose,请参考[安装监控grafana](./grafana.md)文档中的相关章节进行安装。 + +### 3. 使用Docker Compose部署Prometheus + +#### 3.1 创建项目目录 + +```bash +mkdir -p /home/docker/prometheus +mkdir -p /home/docker/prometheus/prometheus_data +mkdir -p /home/docker/prometheus/alertmanager_data +chmod 777 /home/docker/prometheus/prometheus_data +chmod 777 /home/docker/prometheus/alertmanager_data +cd /home/docker/prometheus +``` + +#### 3.2 创建Prometheus配置文件 + +创建`prometheus.yml`文件: + +```bash +cat > /home/docker/prometheus/prometheus.yml << 'EOF' +global: + scrape_interval: 15s # 默认抓取间隔,15秒向目标抓取一次数据 + evaluation_interval: 15s # 执行规则的频率,15秒执行一次规则 + scrape_timeout: 10s # 抓取超时时间 + + # 附加到所有时间序列或警报的外部标签 + external_labels: + monitor: 'prometheus-standalone' + +# Alertmanager配置 +alerting: + alertmanagers: + - static_configs: + - targets: ['alertmanager:9093'] + +# 告警规则文件列表 +rule_files: + - "rules/*.yml" + +# 抓取配置 +scrape_configs: + # 监控Prometheus本身 + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # 监控主机节点 + - job_name: 'node-exporter' + static_configs: + - targets: ['node-exporter:9100'] + + # 监控Docker容器 + - job_name: 'cadvisor' + static_configs: + - targets: ['cadvisor:8080'] + + # 监控Alertmanager + - job_name: 'alertmanager' + static_configs: + - targets: ['alertmanager:9093'] + + # 示例:监控Spring Boot应用 + # - job_name: 'spring-boot-app' + # metrics_path: '/actuator/prometheus' + # scrape_interval: 5s + # static_configs: + # - targets: ['host.docker.internal:8080'] + + # 示例:监控MySQL + # - job_name: 'mysql' + # static_configs: + # - targets: ['mysql-exporter:9104'] + + # 示例:监控Redis + # - job_name: 'redis' + # static_configs: + # - targets: ['redis-exporter:9121'] +EOF +``` + +#### 3.3 创建告警规则目录和示例规则 + +```bash +mkdir -p /home/docker/prometheus/rules + +cat > /home/docker/prometheus/rules/node_alerts.yml << 'EOF' +groups: +- name: node_alerts + rules: + - alert: HighCPULoad + expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 + for: 5m + labels: + severity: warning + annotations: + summary: "High CPU load (instance {{ $labels.instance }})" + description: "CPU load is > 80%\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" + + - alert: HighMemoryLoad + expr: (node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes) / node_memory_MemTotal_bytes * 100 > 80 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory load (instance {{ $labels.instance }})" + description: "Memory load is > 80%\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" + + - alert: HighDiskUsage + expr: (node_filesystem_size_bytes{fstype!="tmpfs"} - node_filesystem_free_bytes{fstype!="tmpfs"}) / node_filesystem_size_bytes{fstype!="tmpfs"} * 100 > 80 + for: 5m + labels: + severity: warning + annotations: + summary: "High disk usage (instance {{ $labels.instance }})" + description: "Disk usage is > 80%\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" +EOF +``` + +#### 3.4 创建Alertmanager配置文件 + +```bash +cat > /home/docker/prometheus/alertmanager.yml << 'EOF' +global: + resolve_timeout: 5m + +route: + group_by: ['alertname'] + group_wait: 30s + group_interval: 5m + repeat_interval: 1h + receiver: 'web.hook' + routes: + - match: + severity: critical + receiver: 'web.hook' + continue: true + +receivers: +- name: 'web.hook' + webhook_configs: + - url: 'http://127.0.0.1:5001/' + send_resolved: true + +# 示例:邮件告警配置 +# - name: 'email' +# email_configs: +# - to: 'your-email@example.com' +# from: 'alertmanager@example.com' +# smarthost: 'smtp.example.com:587' +# auth_username: 'alertmanager@example.com' +# auth_password: 'password' +# auth_identity: 'alertmanager@example.com' +# auth_secret: 'password' + +# 示例:企业微信告警配置 +# - name: 'wechat' +# wechat_configs: +# - corp_id: 'your-corp-id' +# api_url: 'https://qyapi.weixin.qq.com/cgi-bin/' +# api_secret: 'your-api-secret' +# to_party: 'your-party-id' +# agent_id: 'your-agent-id' +# message: '{{ template "wechat.default.message" . }}' + +inhibit_rules: + - source_match: + severity: 'critical' + target_match: + severity: 'warning' + equal: ['alertname', 'dev', 'instance'] +EOF +``` + +#### 3.5 创建Docker Compose配置文件 + +```bash +cat > /home/docker/prometheus/docker-compose.yml << 'EOF' +version: '3.8' + +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - ./rules:/etc/prometheus/rules + - ./prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=15d' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.enable-lifecycle' + ports: + - "9090:9090" + restart: always + networks: + - monitoring + + alertmanager: + image: prom/alertmanager:latest + container_name: alertmanager + volumes: + - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml + - ./alertmanager_data:/alertmanager + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + ports: + - "9093:9093" + restart: always + networks: + - monitoring + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "9100:9100" + restart: always + networks: + - monitoring + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + ports: + - "8080:8080" + restart: always + networks: + - monitoring + + grafana: + image: grafana/grafana:latest + container_name: grafana + volumes: + - ./grafana_data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + ports: + - "3000:3000" + restart: always + networks: + - monitoring + +networks: + monitoring: + driver: bridge + +volumes: + prometheus_data: + alertmanager_data: + grafana_data: +EOF +``` + +#### 3.6 启动服务 + +```bash +cd /home/docker/prometheus +docker-compose up -d +``` + +### 4. 访问Prometheus和Grafana + +#### 4.1 访问Prometheus Web界面 + +在浏览器中访问:`http://your-server-ip:9090` + +![Prometheus Web界面](/worker/prometheus-dashboard.svg) + +#### 4.2 访问Grafana + +在浏览器中访问:`http://your-server-ip:3000` + +默认用户名:`admin` +默认密码:`admin` + +首次登录会要求修改密码。 + +### 5. 配置Grafana数据源和仪表板 + +#### 5.1 添加Prometheus数据源 + +1. 登录Grafana +2. 点击左侧菜单的「Configuration」(齿轮图标) +3. 选择「Data Sources」 +4. 点击「Add data source」 +5. 选择「Prometheus」 +6. 在URL字段中输入:`http://prometheus:9090` +7. 点击「Save & Test」确保连接成功 + +#### 5.2 导入仪表板 + +Grafana提供了许多预配置的仪表板,您可以导入这些仪表板来监控不同的系统和应用。 + +1. 点击左侧菜单的「+」图标 +2. 选择「Import」 +3. 输入以下仪表板ID之一: + - 1860:Node Exporter Full(主机监控) + - 893:Docker and system monitoring(Docker监控) + - 12900:Cadvisor Exporter(容器监控) + - 9578:Prometheus 2.0 Overview(Prometheus自身监控) +4. 点击「Load」 +5. 在「Prometheus」数据源下拉菜单中选择您刚才添加的Prometheus数据源 +6. 点击「Import」 + +### 6. 监控自定义应用 + +#### 6.1 监控Spring Boot应用 + +1. 在Spring Boot应用中添加依赖: + +```xml + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + +``` + +2. 在`application.properties`中添加配置: + +```properties +management.endpoints.web.exposure.include=prometheus,health,info +management.endpoint.prometheus.enabled=true +management.metrics.export.prometheus.enabled=true +``` + +3. 修改Prometheus配置文件,添加Spring Boot应用的监控配置: + +```yaml +- job_name: 'spring-boot-app' + metrics_path: '/actuator/prometheus' + scrape_interval: 5s + static_configs: + - targets: ['host.docker.internal:8080'] +``` + +4. 重新加载Prometheus配置: + +```bash +curl -X POST http://localhost:9090/-/reload +``` + +#### 6.2 监控MySQL + +1. 添加MySQL Exporter到docker-compose.yml: + +```yaml +mysql-exporter: + image: prom/mysqld-exporter:latest + container_name: mysql-exporter + environment: + - DATA_SOURCE_NAME=user:password@(mysql:3306)/ + ports: + - "9104:9104" + restart: always + networks: + - monitoring +``` + +2. 修改Prometheus配置文件,添加MySQL监控配置: + +```yaml +- job_name: 'mysql' + static_configs: + - targets: ['mysql-exporter:9104'] +``` + +3. 重新加载配置 + +#### 6.3 监控Redis + +1. 添加Redis Exporter到docker-compose.yml: + +```yaml +redis-exporter: + image: oliver006/redis_exporter:latest + container_name: redis-exporter + environment: + - REDIS_ADDR=redis:6379 + ports: + - "9121:9121" + restart: always + networks: + - monitoring +``` + +2. 修改Prometheus配置文件,添加Redis监控配置: + +```yaml +- job_name: 'redis' + static_configs: + - targets: ['redis-exporter:9121'] +``` + +3. 重新加载配置 + +### 7. 配置告警通知 + +#### 7.1 配置邮件告警 + +修改alertmanager.yml文件,添加邮件告警配置: + +```yaml +receivers: +- name: 'email' + email_configs: + - to: 'your-email@example.com' + from: 'alertmanager@example.com' + smarthost: 'smtp.example.com:587' + auth_username: 'alertmanager@example.com' + auth_password: 'password' + auth_identity: 'alertmanager@example.com' + auth_secret: 'password' +``` + +#### 7.2 配置企业微信告警 + +修改alertmanager.yml文件,添加企业微信告警配置: + +```yaml +receivers: +- name: 'wechat' + wechat_configs: + - corp_id: 'your-corp-id' + api_url: 'https://qyapi.weixin.qq.com/cgi-bin/' + api_secret: 'your-api-secret' + to_party: 'your-party-id' + agent_id: 'your-agent-id' + message: '{{ template "wechat.default.message" . }}' +``` + +#### 7.3 配置钉钉告警 + +1. 安装钉钉告警适配器: + +```yaml +dingtalk-webhook: + image: timonwong/prometheus-webhook-dingtalk:latest + container_name: dingtalk-webhook + ports: + - "8060:8060" + volumes: + - ./dingtalk-config.yml:/etc/prometheus-webhook-dingtalk/config.yml + restart: always + networks: + - monitoring +``` + +2. 创建钉钉配置文件: + +```yaml +# dingtalk-config.yml +targets: + webhook1: + url: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxx + secret: SEC000000000000000000000 +``` + +3. 修改alertmanager.yml,添加钉钉告警配置: + +```yaml +receivers: +- name: 'dingtalk' + webhook_configs: + - url: 'http://dingtalk-webhook:8060/dingtalk/webhook1/send' + send_resolved: true +``` + +### 8. 性能优化 + +#### 8.1 Prometheus存储优化 + +修改Prometheus启动参数,优化存储性能: + +```yaml +command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=15d' # 数据保留时间 + - '--storage.tsdb.retention.size=10GB' # 数据保留大小 + - '--storage.tsdb.wal-compression' # 启用WAL压缩 + - '--web.enable-lifecycle' +``` + +#### 8.2 抓取频率优化 + +根据实际需求调整抓取频率,减少资源消耗: + +```yaml +global: + scrape_interval: 30s # 默认抓取间隔调整为30秒 + +scrape_configs: + - job_name: 'critical-service' # 关键服务使用更频繁的抓取 + scrape_interval: 5s + static_configs: + - targets: ['critical-service:8080'] +``` + +### 9. 安全加固 + +#### 9.1 启用基本认证 + +1. 创建密码文件: + +```bash +htpasswd -c /home/docker/prometheus/web.htpasswd admin +``` + +2. 修改Prometheus配置: + +```yaml +prometheus: + image: prom/prometheus:latest + container_name: prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - ./web.htpasswd:/etc/prometheus/web.htpasswd + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.config.file=/etc/prometheus/web.yml' +``` + +3. 创建web.yml文件: + +```yaml +basic_auth_users: + admin: "$2y$10$..." # 密码哈希值 +``` + +#### 9.2 使用反向代理 + +使用Nginx作为反向代理,提供额外的安全层: + +```nginx +server { + listen 80; + server_name prometheus.example.com; + + location / { + proxy_pass http://localhost:9090; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + auth_basic "Prometheus"; + auth_basic_user_file /etc/nginx/htpasswd; + } +} +``` + +### 10. 故障排查 + +#### 10.1 检查Prometheus状态 + +```bash +# 查看Prometheus容器日志 +docker logs prometheus + +# 检查Prometheus目标状态 +curl http://localhost:9090/api/v1/targets | jq . +``` + +#### 10.2 检查Alertmanager状态 + +```bash +# 查看Alertmanager容器日志 +docker logs alertmanager + +# 检查Alertmanager状态 +curl http://localhost:9093/api/v1/status | jq . +``` + +#### 10.3 常见问题解决 + +1. **无法抓取目标**:检查网络连接、防火墙设置和目标服务是否正常运行 +2. **数据存储问题**:检查磁盘空间和权限 +3. **告警未触发**:检查告警规则和Alertmanager配置 +4. **Grafana无法连接Prometheus**:检查网络和数据源配置 + +### 11. 备份和恢复 + +#### 11.1 备份Prometheus数据 + +```bash +# 停止Prometheus服务 +docker-compose stop prometheus + +# 备份数据目录 +tar -czvf prometheus_backup_$(date +%Y%m%d).tar.gz /home/docker/prometheus/prometheus_data + +# 重启Prometheus服务 +docker-compose start prometheus +``` + +#### 11.2 恢复Prometheus数据 + +```bash +# 停止Prometheus服务 +docker-compose stop prometheus + +# 恢复数据目录 +rm -rf /home/docker/prometheus/prometheus_data/* +tar -xzvf prometheus_backup_20230815.tar.gz -C / + +# 重启Prometheus服务 +docker-compose start prometheus +``` + +### 12. 总结 + +通过本文的指导,您已经成功部署了一个完整的Prometheus监控系统,包括: + +- Prometheus服务器用于收集和存储指标 +- Node Exporter用于监控主机 +- cAdvisor用于监控容器 +- Alertmanager用于处理告警 +- Grafana用于可视化监控数据 + +您还学习了如何监控自定义应用、配置告警通知、优化性能和加强安全性。这个监控系统可以帮助您及时发现和解决潜在问题,保障系统的稳定运行。 + +随着业务的发展,您可以根据需要扩展监控系统,添加更多的Exporter来监控不同的服务和应用,或者升级到Prometheus的集群部署方案,以支持更大规模的监控需求。 \ No newline at end of file diff --git a/docs/worker/redis-dashboard.svg b/docs/worker/redis-dashboard.svg new file mode 100644 index 000000000..358a0a967 --- /dev/null +++ b/docs/worker/redis-dashboard.svg @@ -0,0 +1,69 @@ + + + + + + Redis 监控面板 + + + + + + 内存使用 + 2.4 GB + 已用 / 4 GB 总量 + + + + 客户端连接数 + 256 + 当前活跃连接 + + + + 每秒命令数 + 4,532 + 过去1分钟平均 + + + + + + + 内存使用趋势 + + + + + + + 时间 (过去24小时) + + + + 内存 (GB) + + + + 键空间统计 + + + + + + + + + + + + 字符串键 (65%) + + + 哈希键 (25%) + + + 其他类型 (10%) + + \ No newline at end of file diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256.html" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256.html" new file mode 100644 index 000000000..1bb70bd3a --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256.html" @@ -0,0 +1,23542 @@ + + +
组件化优化建议跳至内容
ChatGPT 正在生成回复…
\ No newline at end of file diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/5c2e878f68ab88fa77bdcd7901fcee3a.png" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/5c2e878f68ab88fa77bdcd7901fcee3a.png" new file mode 100644 index 000000000..bcc876df2 Binary files /dev/null and "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/5c2e878f68ab88fa77bdcd7901fcee3a.png" differ diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/FormattedText-kb0ehjj7.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/FormattedText-kb0ehjj7.css" new file mode 100644 index 000000000..bb8485964 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/FormattedText-kb0ehjj7.css" @@ -0,0 +1 @@ +._tableContainer_16hzy_1{--thread-content-width:min(calc(100cqw - var(--thread-content-margin, 0)*2),var(--thread-content-max-width));--thread-gutter-size:calc((100cqw - var(--thread-content-width))/2);margin-inline:calc(var(--thread-gutter-size)*-1);overflow-x:auto;pointer-events:none;scrollbar-width:none;width:100cqw}._tableWrapper_16hzy_14{margin-inline:var(--thread-gutter-size) var(--thread-content-margin);pointer-events:auto} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/agent_3.webp" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/agent_3.webp" new file mode 100644 index 000000000..5155a7305 Binary files /dev/null and "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/agent_3.webp" differ diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/audio.html" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/audio.html" new file mode 100644 index 000000000..1e9f41cb0 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/audio.html" @@ -0,0 +1,13 @@ + + + + + + Audio + + + + + + + \ No newline at end of file diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/codemirror-k0z2bf08.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/codemirror-k0z2bf08.css" new file mode 100644 index 000000000..b5c21f6d2 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/codemirror-k0z2bf08.css" @@ -0,0 +1 @@ +._codemirror_8a1c3_1 .cm-editor{background-color:var(--bg-primary);border:none;border-radius:0;flex:1;font-size:14px}._codemirror_8a1c3_1 .cm-scroller{scrollbar-color:var(--main-surface-tertiary) transparent}._codemirror_8a1c3_1 .cm-scroller:hover{scrollbar-color:var(--gray-200) transparent}.dark ._codemirror_8a1c3_1 .cm-scroller:hover{scrollbar-color:var(--gray-600) transparent}._codemirror_8a1c3_1 .cm-focused{outline:none}._codemirror_8a1c3_1._preview_8a1c3_26 .cm-content{padding:1rem 0 2rem!important}._codemirror_8a1c3_1:not(._preview_8a1c3_26) .cm-content{padding:1rem 0 50vh!important}._codemirror_8a1c3_1 .cm-activeLineGutter,._codemirror_8a1c3_1 .cm-gutters{background-color:var(--bg-primary)}._codemirror_8a1c3_1 .cm-gutters{color:var(--text-quaternary)}[dir=ltr] ._codemirror_8a1c3_1 .cm-gutters{border-right:none;padding-left:12px;padding-right:2px}[dir=rtl] ._codemirror_8a1c3_1 .cm-gutters{border-left:none;padding-left:2px;padding-right:12px}[dir=ltr] ._codemirror_8a1c3_1 .cm-gutters .cm-lineNumbers{padding-right:0}[dir=rtl] ._codemirror_8a1c3_1 .cm-gutters .cm-lineNumbers{padding-left:0}._codemirror_8a1c3_1 .cm-foldGutter .cm-gutterElement{align-items:center;display:flex;justify-content:center}._codemirror_8a1c3_1 .cm-line.streaming-line-overlay{position:relative}._codemirror_8a1c3_1 .cm-line.streaming-line-overlay:before{content:"";height:100%;pointer-events:none;position:absolute;top:0;width:100%;z-index:1}[dir=ltr] ._codemirror_8a1c3_1 .cm-line.streaming-line-overlay:before{left:0}[dir=rtl] ._codemirror_8a1c3_1 .cm-line.streaming-line-overlay:before{right:0}._codemirror_8a1c3_1 .cm-deletedChunk del{text-decoration:none} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/comments-plugin-dtishh47.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/comments-plugin-dtishh47.css" new file mode 100644 index 000000000..d73db233e --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/comments-plugin-dtishh47.css" @@ -0,0 +1 @@ +._modelCursor_1mtn0_1{display:inline-block;height:0;position:relative;width:0}._modelCursor_1mtn0_1:after{content:"●";font-family:Circle,system-ui,sans-serif;line-height:normal;vertical-align:baseline}[dir=ltr] ._modelCursor_1mtn0_1:after{margin-left:.25rem}[dir=rtl] ._modelCursor_1mtn0_1:after{margin-right:.25rem} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/conversation-small-cqkvcue6.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/conversation-small-cqkvcue6.css" new file mode 100644 index 000000000..be878bd1e --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/conversation-small-cqkvcue6.css" @@ -0,0 +1 @@ +._wrapper_4j5pz_1{border-radius:8px;cursor:pointer;display:inline-flex;height:44px;-webkit-user-select:none;user-select:none;width:44px}._wrapper_4j5pz_1>input[type=checkbox]{display:none}._wrapper_4j5pz_1{color:var(--icon-secondary)}@media (hover:hover) and (pointer:fine){._wrapper_4j5pz_1:hover{--hover-background:var(--main-surface-secondary)}}._label_4j5pz_22{align-items:center;background-color:var(--hover-background);border-radius:8px;color:var(--text-secondary);display:flex;flex:1;justify-content:center;transition:background-color .1s linear}.active-view-transition.close-thread-sidebar,.active-view-transition.open-thread-sidebar{--vt_model_picker:model-picker;--vt_share_chat_wide_button:share-chat-wide-button;--vt_share_chat_compact_button:share-chat-compact-button;--vt_thread_tools:thread-tools;--thread-extended-info-transition-name:thread-extended-info;--vt-disable-screen-column-transition:none;--vt_toggle_sidebar_opened:toggle-sidebar-icon-opened;--vt_toggle_sidebar_closed:toggle-sidebar-icon-closed;--vt-thread-header-open-canvas:open-canvas-button;--vt-composer-speech-button:composer-speech-button;--vt_new_chat_thread:new-chat-thread;--vt-profile-avatar-thread:profile-avatar-active}@media (prefers-reduced-motion:reduce){:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition{display:none}}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-group(*),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(*),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(*){animation-duration:var(--vt-duration,.3s);animation-timing-function:var(--vt-timing-function,var(--spring-common))}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(composer-speech-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(model-picker),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(open-canvas-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(share-chat-compact-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(share-chat-wide-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(thread-tools),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(toggle-sidebar-icon){display:none}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(composer-speech-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(model-picker),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(open-canvas-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(share-chat-compact-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(share-chat-wide-button),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(thread-tools),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(toggle-sidebar-icon){animation:none;height:100%}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-group(profile-avatar-active){animation:none;z-index:2}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(profile-avatar-active){animation:none}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(thread-extended-info),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(thread-extended-info){height:100%;object-fit:none;overflow:clip}:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-new(thread),:is(.active-view-transition.open-thread-sidebar,.active-view-transition.close-thread-sidebar)::view-transition-old(thread){height:100%;object-fit:none;overflow:clip}:is(.active-view-transition.open-thread-sidebar)::view-transition-old(thread-extended-info){display:none}:is(.active-view-transition.close-thread-sidebar)::view-transition-new(thread-extended-info){display:none}@keyframes _fade-in_m1hgl_1{to{opacity:1}}._root_m1hgl_7 ._fadeIn_m1hgl_8,._root_m1hgl_7 blockquote,._root_m1hgl_7 code,._root_m1hgl_7 hr,._root_m1hgl_7 li,._root_m1hgl_7 pre,._root_m1hgl_7 tr{animation:_fade-in_m1hgl_1 var(--duration,.7s) cubic-bezier(.37,.55,.86,.88) forwards;opacity:0}@media (prefers-reduced-motion:reduce){._root_m1hgl_7 ._fadeIn_m1hgl_8,._root_m1hgl_7 blockquote,._root_m1hgl_7 code,._root_m1hgl_7 hr,._root_m1hgl_7 li,._root_m1hgl_7 pre,._root_m1hgl_7 tr{--duration:0s;opacity:1}}@keyframes _slideUp_1kuxv_21{0%{pointer-events:none;transform:translateY(30vh)}to{pointer-events:auto;transform:translateY(0)}}@keyframes _fadeIn_1kuxv_1{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion:no-preference){._slideUp_1kuxv_21{animation:_fadeIn_1kuxv_1 .2s linear forwards,_slideUp_1kuxv_21 .7s var(--spring-common) forwards}}@keyframes _slide-up_m3fum_1{0%{opacity:0;translate:0 20vw}}@keyframes _slide-down_m3fum_1{to{opacity:0;translate:0 20vw}}._page-to-page-transition_m3fum_14:not(.active-view-transition){@view-transition{navigation:auto}}._page-to-page-transition_m3fum_14{view-transition-name:none}._page-to-page-transition_m3fum_14 body{view-transition-name:page}@media (prefers-reduced-motion:reduce){._page-to-page-transition_m3fum_14::view-transition{display:none}}._page-to-page-transition_m3fum_14::view-transition-old(header),._page-to-page-transition_m3fum_14::view-transition-old(sidebar){display:none}._page-to-page-transition_m3fum_14::view-transition-new(header),._page-to-page-transition_m3fum_14::view-transition-new(sidebar){animation:none}._page-to-page-transition_m3fum_14::view-transition-image-pair(active-image),._page-to-page-transition_m3fum_14::view-transition-new(active-image),._page-to-page-transition_m3fum_14::view-transition-old(active-image){height:100%}._page-to-page-transition_m3fum_14::view-transition-image-pair(page-title),._page-to-page-transition_m3fum_14::view-transition-new(page-title),._page-to-page-transition_m3fum_14::view-transition-old(page-title){height:100%}._page-to-page-transition_m3fum_14::view-transition-image-pair(acive-image),._page-to-page-transition_m3fum_14::view-transition-new(acive-image),._page-to-page-transition_m3fum_14::view-transition-old(acive-image){height:100%}._page-to-page-transition_m3fum_14::view-transition-group(*),._page-to-page-transition_m3fum_14::view-transition-new(*),._page-to-page-transition_m3fum_14::view-transition-old(*){animation-duration:.4s;animation-timing-function:var(--spring-fast)}._page-to-page-transition_m3fum_14.to-lightbox{--vt-scroll-buttons:scroll-buttons}._page-to-page-transition_m3fum_14.to-lightbox::view-transition-group(scroll-buttons){z-index:3}._page-to-page-transition_m3fum_14from.library.to-conversation,._page-to-page-transition_m3fum_14from.lightbox.to-conversation{--vt-active-image:active-image}._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox{--vt-active-image:active-image;--vt-page-title:page-title;--vt-page-footer:page-footer}:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-new(backdrop){animation:none}:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-group(active-image){z-index:2}:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-group(active-image),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-group(page-title),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-new(backdrop),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-new(page-footer),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-new(page-title),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-new(scroll-buttons),:is(._page-to-page-transition_m3fum_14.from-lightbox,._page-to-page-transition_m3fum_14.to-lightbox)::view-transition-old(page-title){animation-duration:.3s}:is(._page-to-page-transition_m3fum_14.to-home,._page-to-page-transition_m3fum_14.from-landing-page)::view-transition-new(composer){animation:none}._page-to-page-transition_m3fum_14.from-landing-page:not(.to-lightbox),._page-to-page-transition_m3fum_14.to-landing-page:not(.to-lightbox){--vt-page-header:header;--vt-splash-screen-headline:page-title;--vt-tool-page-title:page-title;--vt-composer:composer;--sidebar-slideover:sidebar}._page-to-page-transition_m3fum_14.to-landing-page::view-transition-new(page){animation:_slide-up_m3fum_1 .4s var(--spring-fast)}._composer-slide_m3fum_129{--vt-composer:composer}._composer-slide_m3fum_129::view-transition-group(composer),._composer-slide_m3fum_129::view-transition-old(composer){animation-duration:.5s;animation-timing-function:var(--spring-fast)}._leadingBar_sbmq2_1{box-shadow:0 1px 0 transparent;@keyframes _add-top-shadow_sbmq2_1{0%{box-shadow:0 1px 0 transparent}0.1%,to{box-shadow:0 1px 0 var(--border-sharp)}}animation-range:0 1px;animation:_add-top-shadow_sbmq2_1 linear both}._leadingBarScrollAnimation_sbmq2_19{animation-timeline:scroll()}._trailingBar_sbmq2_23{box-shadow:0 -1px 0 transparent;@keyframes _add-bottom-shadow_sbmq2_1{0%,99.9%{box-shadow:0 -1px 0 var(--border-sharp)}to{box-shadow:0 -1px 0 transparent}}animation-range:0 1px;animation:_add-bottom-shadow_sbmq2_1 linear both}._trailingBarScrollAnimation_sbmq2_41{animation-timeline:scroll()}._primary_sbmq2_45{background-color:var(--bar-background-color,var(--main-surface-primary))}._screen_c7xqp_1{display:var(--screen-display,grid);grid-template-areas:"leading" "content" "trailing" "keyboard";grid-template-columns:minmax(0,1fr);grid-template-rows:max-content 1fr max-content auto}@supports not (overflow:clip){._screen_c7xqp_1{overflow:var(--screen-overflow,hidden auto)}}@supports (overflow:clip){._screen_c7xqp_1{overflow:var(--screen-overflow,clip auto)}}._screen_c7xqp_1{padding-top:calc(var(--screen-anchor-top) + var(--screen-top-offset, 0px));scrollbar-gutter:var(--screen-scrollbar-gutter-override,stable);width:100%}._screen_c7xqp_1 [slot=content]{grid-area:content;padding-inline:var( --screen-content-inline-padding,var(--screen-inline-padding) );position:var(--screen-content-position,relative)}._screen_c7xqp_1 [slot=leading]{grid-area:leading;min-width:var(--screen-leading-slot-min-width);overflow:var(--screen-leading-slot-overflow);position:sticky;top:var(--screen-leading-slot-top,0);z-index:var(--screen-leading-slot-z-index,20)}._screen_c7xqp_1 [slot=trailing]{bottom:var(--keyboard-safe-area-bottom,0);grid-area:trailing;padding-inline:var( --screen-trailing-inline-padding,var(--screen-inline-padding) );position:sticky;z-index:var(--screen-leading-slot-z-index,20)}._screen_c7xqp_1 [slot=keyboard]{background:#fcfcfc;bottom:0;grid-area:keyboard;height:var(--keyboard-safe-area-bottom,0);position:sticky}._screen_c7xqp_1:where([screen-anchor=vertical],[screen-anchor=top]){--safe-area-top: calc(env(titlebar-area-y, 0px) + env(safe-area-inset-top, 0px)) ;--screen-anchor-top:var(--safe-area-top)}._screen_c7xqp_1:where([screen-anchor=vertical],[screen-anchor=bottom]){--safe-area-bottom:env(safe-area-inset-bottom,0px);--keyboard-safe-area-bottom:max(var(--screen-keyboard-height),env(keyboard-inset-height,0px));--screen-anchor-bottom:var(--safe-area-bottom)}@keyframes _fade_4f9by_7{to{opacity:1}}._fadeIn_4f9by_7{animation:_fade_4f9by_7 var(--duration,0s) cubic-bezier(.37,.55,.86,.88) forwards var(--delay,0s);animation-iteration-count:1;opacity:0}@media (prefers-reduced-motion:reduce){._fadeIn_4f9by_7{--duration:0s;opacity:1}}._marker_4f9by_21._hidden_4f9by_21{display:none}._marker_4f9by_21._animate_4f9by_25{animation:_fade_4f9by_7 var(--duration,0s) cubic-bezier(.37,.55,.86,.88) forwards var(--delay,0s);animation-iteration-count:1;opacity:0}@media (prefers-reduced-motion:reduce){._marker_4f9by_21._animate_4f9by_25{--duration:0s;opacity:1}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-outline-style:solid;--tw-leading:initial;--tw-content:""}}}._prosemirror-parent_kfgfu_2 .ProseMirror[contenteditable]{--tw-outline-style:none;outline-style:var(--tw-outline-style);outline-style:none;outline-width:0}._prosemirror-parent_kfgfu_2 .ProseMirror{word-wrap:break-word;font-feature-settings:"liga" 0;-webkit-font-variant-ligatures:none;font-variant-ligatures:none;margin-block:calc(var(--spacing,.25rem)*2);padding-inline:calc(var(--spacing,.25rem)*0);white-space:pre-wrap;white-space:break-spaces}._prosemirror-parent_kfgfu_2.ProseMirror br{--tw-leading:normal;line-height:normal}._prosemirror-parent_kfgfu_2.default-browser .placeholder:after{--tw-content:attr(data-placeholder);color:var(--text-tertiary);content:var(--tw-content);cursor:text;pointer-events:none;position:relative}[dir=ltr] ._prosemirror-parent_kfgfu_2.default-browser .placeholder:after{padding-left:1px}[dir=rtl] ._prosemirror-parent_kfgfu_2.default-browser .placeholder:after{padding-right:1px}._prosemirror-parent_kfgfu_2.default-browser .placeholder .ProseMirror-trailingBreak{display:none!important}._prosemirror-parent_kfgfu_2.firefox .placeholder:before{--tw-content:attr(data-placeholder);color:var(--text-secondary);content:var(--tw-content);cursor:text;pointer-events:none;position:absolute}._prosemirror-parent_kfgfu_2 p{white-space:pre-wrap}._prosemirror-parent_kfgfu_2 p.placeholder{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.screen-arch ._prosemirror-parent_kfgfu_2 p.placeholder{view-transition-name:var(--vt-composer-placeholder);width:fit-content}._prosemirror-parent_kfgfu_2 .ProseMirror-separator{display:none!important}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}._lightbox_1ruyj_1{transition-behavior:allow-discrete}._lightbox_1ruyj_1::backdrop{transition-behavior:allow-discrete;view-transition-name:backdrop;opacity:0;transition:.4s opacity var(--spring-fast)}@media (prefers-reduced-motion:reduce){._lightbox_1ruyj_1::backdrop{transition-duration:.1s}}._lightbox_1ruyj_1[open]::backdrop{opacity:1}@starting-style{._lightbox_1ruyj_1[open]::backdrop{opacity:0}}._carousel_1ruyj_26::scroll-button(left),._carousel_1ruyj_26::scroll-button(right){position-anchor:--carousel;aspect-ratio:1;background-color:var(--main-surface-primary);border:1px solid var(--color-token-border-default);border-radius:50%;color:var(--marker-color,var(--main-surface-primary-inverse));cursor:pointer;display:grid;margin-inline:12px;padding-block-start:4px;place-items:center;position:fixed;transition:opacity .4s var(--ease-spring-standard);width:44px}@starting-style{._carousel_1ruyj_26::scroll-button(left),._carousel_1ruyj_26::scroll-button(right){opacity:.3}}._carousel_1ruyj_26::scroll-button(right){--_inner:center span-inline-start;--_outer:inline-end center;position-area:var(--_inner);content:url("data:image/svg+xml;utf8,") /"Next"}._carousel_1ruyj_26::scroll-button(*):disabled{opacity:.3}._carousel_1ruyj_26::scroll-button(left){--_inner:center span-inline-end;--_outer:inline-start center;position-area:var(--_inner);content:url("data:image/svg+xml;utf8,") /"Previous";scale:-1 1}.CircularProgressbar{vertical-align:middle;width:100%}.CircularProgressbar .CircularProgressbar-path{stroke:#3e98c7;stroke-linecap:round;-webkit-transition:stroke-dashoffset .5s ease 0s;transition:stroke-dashoffset .5s ease 0s}.CircularProgressbar .CircularProgressbar-trail{stroke:#d6d6d6;stroke-linecap:round}.CircularProgressbar .CircularProgressbar-text{fill:#3e98c7;dominant-baseline:middle;text-anchor:middle;font-size:20px}.CircularProgressbar .CircularProgressbar-background{fill:#d6d6d6}.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-background{fill:#3e98c7}.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text{fill:#fff}.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path{stroke:#fff}.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail{stroke:transparent}/*! tailwindcss v4.1.6 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}._rangeSelector_1czzq_2{align-items:center;appearance:none;box-sizing:content-box;padding:20px 10px;width:12px}[dir=ltr] ._rangeSelector_1czzq_2{margin-left:-10px}[dir=rtl] ._rangeSelector_1czzq_2{margin-right:-10px}._rangeSelector_1czzq_2::-webkit-slider-thumb{--tw-pan-y:pan-y;--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);appearance:none;aspect-ratio:1;background-color:var(--main-surface-primary);border:1px solid #c8c8c880;border-color:var(--border-default);border-radius:var(--radius-2xl,1rem);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);cursor:grab;height:calc(var(--spacing,.25rem)*6);touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,);transform:translateY(var(--slider-thumb-translate-y))}._vertical-slider_1czzq_2{width:16px;writing-mode:vertical-lr}[dir=ltr] ._vertical-slider_1czzq_2{direction:rtl}[dir=rtl] ._vertical-slider_1czzq_2{direction:ltr}._vertical-slider_1czzq_2::-webkit-slider-thumb{appearance:none}._hiddenThumb_1czzq_2::-webkit-slider-thumb{display:none!important}._hiddenThumb_1czzq_2::-moz-range-thumb{display:none!important}._hiddenThumb_1czzq_2::-ms-thumb{display:none!important}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}._threadRoot_1y3t0_1{--thread-safe-area-height:calc(100lvh - var(--thread-safe-area-inset-top) - var(--thread-safe-area-inset-bottom));--thread-safe-area-inset-top:calc(var(--header-height) + env(safe-area-inset-top, 0px));--thread-safe-area-inset-bottom:calc(var(--thread-footer-height, 150px) + var(--screen-keyboard-height, 0px) + env(safe-area-inset-bottom, 0px))}._threadGutter_1y3t0_22{--thread-end-gutter-active-height:calc(var(--thread-safe-area-height) - var(--thread-stream-context-height) - var(--thread-turn-vertical-padding)*2);--thread-stream-context-height:max(2.75rem + 2 * var(--thread-turn-vertical-padding),1/3 * var(--thread-safe-area-height));--thread-turn-vertical-padding:1.25rem}@keyframes _fadeScale_1r3gn_12{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}._fadeScale_1r3gn_12{animation:_fadeScale_1r3gn_12 .3s ease-in-out forwards} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/cot-message-lf3q5fj1.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/cot-message-lf3q5fj1.css" new file mode 100644 index 000000000..af8fa51c4 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/cot-message-lf3q5fj1.css" @@ -0,0 +1 @@ +@keyframes _fade_1frq2_1{0%{opacity:0}to{opacity:1}}._markdown_1frq2_10.markdown .katex-error{display:none}._markdown_1frq2_10.markdown .katex-display{animation:_fade_1frq2_1 .4s ease 50ms forwards;opacity:0}._markdown_1frq2_10.markdown p{margin-bottom:0!important} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/play-sm-1f6vhsjh.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/play-sm-1f6vhsjh.css" new file mode 100644 index 000000000..19e7a2462 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/play-sm-1f6vhsjh.css" @@ -0,0 +1 @@ +.ansi-black-fg{color:#000}.ansi-black-bg{background-color:#000}.ansi-red-fg{color:#f66}.ansi-red-bg{background-color:#f66}.ansi-green-fg{color:#94f494}.ansi-green-bg{background-color:#94f494}.ansi-yellow-fg{color:#f4f47b}.ansi-yellow-bg{background-color:#f4f47b}.ansi-blue-fg{color:#9e9eff}.ansi-blue-bg{background-color:#9e9eff}.ansi-magenta-fg{color:#db6bdb}.ansi-magenta-bg{background-color:#db6bdb}.ansi-cyan-fg{color:#81eeee}.ansi-cyan-bg{background-color:#81eeee}.ansi-white-fg{color:#d6d6d6}.ansi-white-bg{background-color:#d6d6d6}.ansi-bright-black-fg{color:#6e6e6e}.ansi-bright-red-fg{color:#ffa8a8}.ansi-bright-green-fg{color:#0f0}.ansi-bright-yellow-fg{color:#ffffa8}.ansi-bright-blue-fg{color:#9494ff}.ansi-bright-magenta-fg{color:#ffb3ff}.ansi-bright-cyan-fg{color:#adffff}.ansi-bright-white-fg{color:#fff} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/root-nu0t8wee.css" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/root-nu0t8wee.css" new file mode 100644 index 000000000..435501fd2 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/root-nu0t8wee.css" @@ -0,0 +1 @@ +.composer-parent{--composer-footer_height:var(--composer-bar_footer-current-height,32px);--composer-bar_height:var(--composer-bar_current-height,52px);--composer-bar_width:var(--composer-bar_current-width,768px);--mask-fill:linear-gradient(180deg,#fff 0%,#fff);--mask-erase:linear-gradient(180deg,#000 0%,#000)}.masked-content{--content-gradient:linear-gradient(0deg,color(display-p3 .851 .851 .851),color(display-p3 .8488 .8488 .8488/.99) 8.07%,color(display-p3 .8423 .8423 .8423/.98) 15.54%,color(display-p3 .8317 .8317 .8317/.95) 22.5%,color(display-p3 .8171 .8171 .8171/.92) 29.04%,color(display-p3 .7988 .7988 .7988/.87) 35.26%,color(display-p3 .777 .777 .777/.82) 41.25%,color(display-p3 .7518 .7518 .7518/.75) 47.1%,color(display-p3 .7234 .7234 .7234/.68) 52.9%,color(display-p3 .692 .692 .692/.6) 58.75%,color(display-p3 .6578 .6578 .6578/.52) 64.74%,color(display-p3 .621 .621 .621/.42) 70.96%,color(display-p3 .5817 .5817 .5817/.33) 77.5%,color(display-p3 .5401 .5401 .5401/.22) 84.46%,color(display-p3 .4965 .4965 .4965/.11) 91.93%,color(display-p3 .451 .451 .451/0));--composer-bar_safe-margins:20px;-webkit-mask-composite:source-out;mask-composite:subtract;-webkit-mask-image:var(--mask-fill),var(--content-gradient),var(--composer-bar_skeleton);mask-image:var(--mask-fill),var(--content-gradient),var(--composer-bar_skeleton);mask-mode:luminance;-webkit-mask-position:top center,center calc(100% - var(--composer-footer_height)),center calc(100% - var(--composer-footer_height));mask-position:top center,center calc(100% - var(--composer-footer_height)),center calc(100% - var(--composer-footer_height));-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:auto,calc(100% - var(--composer-bar_safe-margins)) calc(var(--composer-bar_height) + var(--composer-bar_mask-grace-area)),var(--composer-bar_width) var(--composer-bar_height);mask-size:auto,calc(100% - var(--composer-bar_safe-margins)) calc(var(--composer-bar_height) + var(--composer-bar_mask-grace-area)),var(--composer-bar_width) var(--composer-bar_height)}@media (prefers-reduced-transparency:reduce){.masked-content{-webkit-mask-image:none;mask-image:none}}.mask-scrollbars{--scrollbar-width:10px;clip-path:inset(-100vh var(--scrollbar-width) 0 0);clip-path:inset(-100svh var(--scrollbar-width) 0 0)}.bg-thread--header{background:linear-gradient(to bottom,transparent 0,transparent 50%,var(--main-surface-primary) 50%,var(--main-surface-primary) 100%);height:var(--composer-bar_height);-webkit-mask-composite:source-out;mask-composite:subtract;-webkit-mask-image:var(--mask-fill),var(--composer-bar_skeleton);mask-image:var(--mask-fill),var(--composer-bar_skeleton);mask-mode:luminance;-webkit-mask-position:top center,top center;mask-position:top center,top center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:auto;mask-size:auto}@media (prefers-reduced-transparency:reduce){.bg-thread--header{-webkit-mask-image:none;mask-image:none}}.bg-thread--footer{background:var(--main-surface-primary);flex:1}:root{--spring-fast-duration:667ms;--spring-fast:linear(0,.01942 1.83%,.07956 4.02%,.47488 13.851%,.65981 19.572%,.79653 25.733%,.84834 29.083%,.89048 32.693%,.9246 36.734%,.95081 41.254%,.97012 46.425%,.98361 52.535%,.99665 68.277%,.99988);--spring-common-duration:667ms;--spring-common:linear(0,.00506 1.18%,.02044 2.46%,.08322 5.391%,.46561 17.652%,.63901 24.342%,.76663 31.093%,.85981 38.454%,.89862 42.934%,.92965 47.845%,.95366 53.305%,.97154 59.516%,.99189 74.867%,.9991);--spring-standard:var(--spring-common);--spring-slow-bounce-duration:1167ms;--spring-slow-bounce:linear(0,.00172 .51%,.00682 1.03%,.02721 2.12%,.06135 3.29%,.11043 4.58%,.21945 6.911%,.59552 14.171%,.70414 16.612%,.79359 18.962%,.86872 21.362%,.92924 23.822%,.97589 26.373%,1.01 29.083%,1.0264 31.043%,1.03767 33.133%,1.04411 35.404%,1.04597 37.944%,1.04058 42.454%,1.01119 55.646%,1.00137 63.716%,.99791 74.127%,.99988);--spring-bounce-duration:833ms;--spring-bounce:linear(0,.00541 1.29%,.02175 2.68%,.04923 4.19%,.08852 5.861%,.17388 8.851%,.48317 18.732%,.57693 22.162%,.65685 25.503%,.72432 28.793%,.78235 32.163%,.83182 35.664%,.87356 39.354%,.91132 43.714%,.94105 48.455%,.96361 53.705%,.97991 59.676%,.9903 66.247%,.99664 74.237%,.99968 84.358%,1.00048);--spring-fast-bounce-duration:1s;--spring-fast-bounce:linear(0,.00683 1.14%,.02731 2.35%,.11137 5.091%,.59413 15.612%,.78996 20.792%,.92396 25.953%,.97109 28.653%,1.00624 31.503%,1.03801 36.154%,1.0477 41.684%,1.00242 68.787%,.99921);--easing-common:ease-in-out;--easing-common:linear(0,0,.0001,.0002,.0003,.0005,.0007,.001,.0013,.0016,.002,.0024,.0029,.0033,.0039,.0044,.005,.0057,.0063,.007,.0079,.0086,.0094,.0103,.0112,.0121,.0132 1.84%,.0153,.0175,.0201,.0226,.0253,.0283,.0313,.0345,.038,.0416,.0454,.0493,.0535,.0576,.0621,.0667,.0714,.0764,.0816 5.04%,.0897,.098 5.62%,.1071,.1165,.1263 6.56%,.137,.1481 7.25%,.1601 7.62%,.1706 7.94%,.1819 8.28%,.194,.2068 9.02%,.2331 9.79%,.2898 11.44%,.3151 12.18%,.3412 12.95%,.3533,.365 13.66%,.3786,.3918,.4045,.4167,.4288,.4405,.452,.4631 16.72%,.4759,.4884,.5005,.5124,.5242,.5354,.5467,.5576,.5686,.5791,.5894,.5995,.6094,.6194,.6289,.6385,.6477,.6569,.6659 24.45%,.6702,.6747,.6789,.6833,.6877,.6919,.696,.7002,.7043,.7084,.7125,.7165,.7205,.7244,.7283,.7321,.7358,.7396,.7433,.7471,.7507,.7544,.7579,.7615,.7649,.7685,.7718,.7752,.7786,.782,.7853,.7885,.7918,.7951,.7982,.8013,.8043,.8075,.8104,.8135,.8165,.8195,.8224,.8253,.8281,.8309,.8336,.8365,.8391,.8419,.8446,.8472,.8499,.8524,.855,.8575,.8599,.8625 37.27%,.8651,.8678,.8703,.8729,.8754,.8779,.8803,.8827,.8851,.8875,.8898,.892,.8942,.8965,.8987,.9009,.903,.9051,.9071,.9092,.9112,.9132,.9151,.9171,.919,.9209,.9227,.9245,.9262,.928,.9297,.9314,.9331,.9347,.9364,.9379,.9395,.941,.9425,.944,.9454,.9469,.9483,.9497,.951,.9524,.9537,.955,.9562,.9574,.9586,.9599,.961,.9622,.9633,.9644,.9655,.9665,.9676,.9686,.9696,.9705,.9715,.9724,.9733,.9742,.975,.9758,.9766,.9774,.9782,.9789,.9796,.9804,.9811,.9817,.9824,.9831,.9837,.9843,.9849,.9855,.986,.9866,.9871,.9877,.9882,.9887,.9892,.9896 70.56%,.9905 71.67%,.9914 72.82%,.9922,.9929 75.2%,.9936 76.43%,.9942 77.71%,.9948 79.03%,.9954 80.39%,.9959 81.81%,.9963 83.28%,.9968 84.82%,.9972 86.41%,.9975 88.07%,.9979 89.81%,.9982 91.64%,.9984 93.56%,.9987 95.58%,.9989 97.72%,.9991)}@supports not (white-space-collapse:collapse){:root :root{--easing-common:ease-in-out;--spring-common:ease-in-out;--spring-bounce:ease-in-out;--spring-fast:ease-in-out;--spring-fast-bounce:ease-in-out;--spring-slow-bounce:ease-in-out}}@supports not (transition-timing-function:linear(0,0 0%)){:root :root{--easing-common:ease-in-out;--spring-common:ease-in-out;--spring-bounce:ease-in-out;--spring-fast:ease-in-out;--spring-fast-bounce:ease-in-out;--spring-slow-bounce:ease-in-out}}@font-face{font-display:swap;font-family:Circle;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,d09GMk9UVE8AAAM0AAkAAAAABcgAAALuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYJIBmAAfgE2AiQDDAQGBYRyByAbIwVRlKvJFsDPBJtObcFDgzooFmKOOuZpZMG4Jg7aB8Nn8fzDvXrufz9r8tgCAU4XFVvjosSo0yqWv9Z+b8++or6Y3y3ikk0laqIkfBkSQzINien2vweAH79N8JdohCbbWndr/HZ5v86kXxrEqV+cqnlKNBcLjY0Bj8Ka512LSgsolgt1Wa1Wr27atM/jBW21RrW55g21jw81IoJNHn2c/z24BlCQTVVvW09zVvtAY1dzeOZwYxugoDCNO2g8kMZDDRpPRuPFajC3qWY31RzV9+loj/h/e6ud/0/bfxcbEILkKpVrQNhwTzuJycoVI0S9RjZZES7WjkQvApCsAhsUn3SuWdP3UZ0zRZT+X1OW1h0OGr9NflM3xRvmiClHqQORzvz/tQbQb7L8b7XUFdZrb+h13MhqctFw/8PP+snI1CnebrExOvET/Fh/hn+knPwQZw89wnNvi+62ERUZoHLr9BC1nCwROqghul1go6hTCVTs54ZppNw6x+jkYtzYdoEMTuGT8KCP/A/hDIeWUloqM4VXWm2g5T0CrvPF5g3kAs04zXJGkI7P96za7LmtKwgBAmhGzB07gBpPeEUAKyzEwwWJxIBzFaQeHZwg6BYQxo6W2Qwz739fUTpv+v/c+Xy3Sv6VF/uN3w8uFpdbNkDuXnWVGkBhvGn75R1LYEgq295Z+QHimbpBIbxAAQtPAhA2QAAaMjYQQHHzONnK8R1EFN9lrZmfUxvmFzjzl5dsLLNQqwDEx+49z7B0yrNi3SQ58LwmAy/AqeOtOWduzoY8+2s/wMFgbxAWiEesMNZAalIE2r8JllitrXeokZEbwVJpR0hSXFLwa+wftjSPNWMSERMRGxMrEi0DVYcfdnhxQ66Eqt62nmYsq32gsaspM4cb2ypPtQ531Q+IIoj9J0lKy0pzkjTFKxoOtd8ODLb39mD0t/UONT71Ry6QDBlaIonr767vbJaUtzSPNQOBmOXFihFLROyYXvXnrUOTszoAAA==) format("woff2")}@font-face{font-display:swap;font-family:Circle;font-style:normal;font-weight:600;src:url(data:font/woff2;base64,d09GMk9UVE8AAANIAAkAAAAABkwAAAMCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYJQBmAAfgE2AiQDDAQGBYVwByAbqQVRlMVNBfiRkLl5oxlVNKr4xQKiOJqYMp0zZd4iyiabvWs/aWpaEhyiBiMQGoNCIoys0xiDZnJteLRGXET3IpoQq697VFOmBlImcVhpy3pWuy6ZGDJnDoFpIwM96olCINQDUebsKzfc8cml+mMBP82Lfx5kKvx3Td2DFtCONwWq1Ru/VIs7/gTVpl7hgkiHscva/P1RKvnkOj2uDshaX5Id6vvRLs7Q7ZY7RUIphhAYQskQ6gJDmGIIZ4YIVbgw4aJK86iJ8ai2EeK+a2PFayPSEw4h7uwclpQxdejLyi11M9Iy2h0j4eJMBI28mehJINkCFioovm/Yah6VpgBJUm48kUyWnPA1xAhNmKY1S5qwFaT01WKAtvHEg6QZc9todjOVtRlP+hmjzDDS5vtMPD748Cgn0q2zV69y9Mytow/50QcHH4tnHBQWHuslA/3B8O2e6uPdV9vO1B/lSKo5WCl4o2ahQUcDvW2kuxvh3SOtegPX6+drRCVHhYM1R9HgaP3ZtqvIQwHcGn6o8wf644VngrsJ4QBWcbQHGrW2K7XgmT5uPpAHTOivlgPGIeL+mbnYY7xhj5AEAtSqfMIBaNDgjWfcMFRmHIrAIqAO7J4cqgRylIjSHx27HeBe+8o/qp1Xbb/IqsC9ZI03+w/fbWoexLpPI+sf04PMBbjGKDw6XInbdQiytiHo/3RWkeUd9IkyXjTYfUMA4QsCKCpfBGjhAhFgEqQAAWZZygUw+FhGgI2LIwiw404iwEWQixiaHEGSvMqDgqv5QpHqDyV0WChLs4GKVj5Q18zvoKFe1Xk/BxaI0I2NKfxfK8J/W710UVzebArQ6NFEpCWN1fGWFBQegKAjSBCctI7wij+coRcCJGQgy7A42Q3Te14v7+6FuamjlQMEsKxdJHYlel9kJ5adv7kxHe2kcBAeviIZGBpwSO2aZ7b9TXUzD/i7C8jF1drRAeiL2ZWjm6Rq8sFp4jKIQOBI9iJbyNGt7alX974oJIgBsRgsHDkMjr/FbPeiAAAA) format("woff2")}@font-face{font-display:swap;font-family:OpenAI Sans;font-weight:300 700;src:url(https://cdn.openai.com/common/fonts/openai-sans-variable/OpenAISansVariableVF.woff2) format("woff2"),url(https://cdn.openai.com/common/fonts/openai-sans-variable/OpenAISansVariableVF.woff) format("woff")}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-border-style:solid;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-outline-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-contain-size:initial;--tw-contain-layout:initial;--tw-contain-paint:initial;--tw-contain-style:initial;--tw-content:""}}}@layer theme{:host,:root{--spacing:.25rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:1.33333;--text-sm:.875rem;--text-sm--line-height:1.42857;--text-base:1rem;--text-base--line-height:1.5;--text-lg:1.125rem;--text-lg--line-height:1.55556;--text-xl:1.25rem;--text-xl--line-height:1.4;--text-2xl:1.5rem;--text-2xl--line-height:1.33333;--text-3xl:1.875rem;--text-3xl--line-height:1.2;--text-4xl:2.25rem;--text-4xl--line-height:1.11111;--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--radius-4xl:2rem;--drop-shadow-xs:0 1px 1px #0000000d;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--ease-in:cubic-bezier(.4,0,1,1);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-xs:4px;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--text-heading-2:1.5rem;--text-heading-2--line-height:1.75rem;--text-heading-2--letter-spacing:-.015625rem;--text-heading-2--font-weight:600;--text-heading-3:1.125rem;--text-heading-3--line-height:1.625rem;--text-heading-3--letter-spacing:-.028125rem;--text-heading-3--font-weight:600;--text-body-small-regular:.875rem;--text-body-small-regular--line-height:1.125rem;--text-body-small-regular--letter-spacing:-.01875rem;--text-body-small-regular--font-weight:400;--text-caption-regular:.75rem;--text-caption-regular--line-height:1rem;--text-caption-regular--letter-spacing:-.00625rem;--text-caption-regular--font-weight:400}}@layer base{*,::backdrop,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}::file-selector-button{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica,Apple Color Emoji,Arial,sans-serif,Segoe UI Emoji,Segoe UI Symbol;font-variation-settings:normal;line-height:1.5;tab-size:4}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}button,input,optgroup,select,textarea{font-feature-settings:inherit;background-color:#0000;border-radius:0;color:inherit;font:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{font-feature-settings:inherit;background-color:#0000;border-radius:0;color:inherit;font:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex;padding-block:0}::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,::backdrop,:after,:before{border-color:var(--border-light,currentColor)}::file-selector-button{border-color:var(--border-light,currentColor)}[role=button]:not(:disabled),button:not(:disabled){cursor:pointer}h1{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}h1,h2,h3{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}h2,h3{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;margin:0}.mask-fade{-webkit-mask-image:none;mask-image:none;transition:-webkit-mask-image .2s,mask-image .2s}.active-mask-fade,.group:hover .mask-fade{-webkit-mask-image:linear-gradient(90deg,#000 0 75%,#0000 100%);mask-image:linear-gradient(90deg,#000 0 75%,#0000)}@keyframes hive-log-fadeout{0%{background:#0285ff1a}to{background-color:#0000}}.hive-log{--tw-font-weight:var(--font-weight-medium);background-color:#0000000d;border-radius:3.40282e+38px;color:#8f8f8f;cursor:pointer;font-size:10px;font-weight:var(--font-weight-medium);padding-block:calc(var(--spacing)*.5);padding-inline:calc(var(--spacing)*1.5)}@media (hover:hover){.hive-log:hover{background-color:var(--main-surface-tertiary);color:var(--text-primary)}}:root,[dir=ltr]{--start:left;--end:right;--to-end-unit:1;--is-ltr:unset;--is-rtl: }[dir=rtl]{--start:right;--end:left;--to-end-unit:-1;--is-ltr: ;--is-rtl:unset}:root{--user-chat-width:70%;--sidebar-width:260px;--header-height:60px;--white:#fff;--black:#000;--gray-50:#f9f9f9;--gray-100:#ececec;--gray-200:#e3e3e3;--gray-300:#cdcdcd;--gray-400:#b4b4b4;--gray-500:#9b9b9b;--gray-600:#676767;--gray-700:#424242;--gray-750:#2f2f2f;--gray-800:#212121;--gray-900:#171717;--gray-950:#0d0d0d;--red-500:#e02e2a;--red-700:#911e1b;--brand-purple:#ab68ff;--yellow-900:#4d3b00}@media (min-width:768px){:root{--header-height:3.5rem}}@media (-o-min-device-pixel-ratio:2),(-webkit-min-device-pixel-ratio:2),(min--moz-device-pixel-ratio:2),(min-device-pixel-ratio:2),(min-resolution:192dpi),(min-resolution:2x){:root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}.dark .light,.light,html{--main-surface-background:#fffffff2;--message-surface:#e9e9e980;--composer-surface:var(--message-surface);--composer-blue-bg:#daeeff;--composer-blue-hover:#bddcf4;--composer-blue-hover-tint:#0084ff24;--composer-surface-primary:var(--main-surface-primary);--dot-color:var(--black);--text-primary:var(--gray-950);--icon-surface:13 13 13;--text-primary-inverse:var(--gray-100);--content-primary:#01172b;--content-secondary:#44505b;--text-secondary:#5d5d5d;--text-tertiary:var(--gray-400);--text-quaternary:var(--gray-300);--text-placeholder:#000000b3;--tag-blue:#08f;--tag-blue-light:#0af;--text-error:#f93a37;--text-danger:var(--red-500);--surface-error:249 58 55;--border-xlight:#0000000d;--border-light:#0000001a;--border-medium:#00000026;--border-heavy:#0003;--border-xheavy:#00000040;--hint-text:#08f;--hint-bg:#b3dbff;--border-sharp:#0000000d;--icon-secondary:#676767;--main-surface-primary:var(--white);--main-surface-primary-inverse:var(--gray-800);--main-surface-secondary:var(--gray-50);--main-surface-secondary-selected:#0000001a;--main-surface-tertiary:var(--gray-100);--sidebar-surface-primary:var(--gray-50);--sidebar-surface-secondary:var(--gray-100);--sidebar-surface-tertiary:var(--gray-200);--sidebar-title-primary:#28282880;--sidebar-surface:#fcfcfc;--sidebar-body-primary:#0d0d0d;--sidebar-icon:#7d7d7d;--surface-hover:#00000012;--link:#2964aa;--link-hover:#749ac8;--selection:#007aff}@supports (color:oklch(.99 0 0)){.dark .light,.light,html{--sidebar-surface-floating-lightness:1;--sidebar-surface-floating-alpha:1;--sidebar-surface-pinned-lightness:.99;--sidebar-surface-pinned-alpha:1}}@media (prefers-reduced-transparency:reduce){.dark .light,.light,html{--message-surface:#f4f4f4}}.dark{--main-surface-background:#212121e6;--message-surface:#323232d9;--composer-blue-bg:#2a4a6d;--composer-blue-hover:#1a416a;--composer-blue-text:#48aaff;--composer-surface-primary:#303030;--dot-color:var(--white);--text-primary:var(--gray-100);--icon-surface:240 240 240;--text-primary-inverse:var(--gray-950);--text-secondary:var(--gray-400);--text-tertiary:var(--gray-500);--text-quaternary:var(--gray-600);--text-placeholder:#fffc;--content-primary:#f2f6fa;--content-secondary:#dbe2e8;--text-error:#f93a37;--border-xlight:#ffffff0d;--border-light:#ffffff1a;--border-medium:#ffffff26;--border-heavy:#fff3;--border-xheavy:#ffffff40;--border-sharp:#ffffff0d;--main-surface-primary:var(--gray-800);--main-surface-primary-inverse:var(--white);--main-surface-secondary:var(--gray-750);--main-surface-secondary-selected:#ffffff26;--main-surface-tertiary:var(--gray-700);--sidebar-surface-primary:var(--gray-900);--sidebar-surface-secondary:var(--gray-800);--sidebar-surface-tertiary:var(--gray-750);--sidebar-title-primary:#f0f0f080;--sidebar-surface:#2b2b2b;--sidebar-body-primary:#ededed;--sidebar-icon:#a4a4a4;--surface-hover:#ffffff26;--link:#7ab7ff;--link-hover:#5e83b3;--surface-error:249 58 55}@supports (color:oklch(.99 0 0)){.dark{--sidebar-surface-floating-lightness:.3;--sidebar-surface-floating-alpha:1;--sidebar-surface-pinned-lightness:.29;--sidebar-surface-pinned-alpha:1}}@media (prefers-reduced-transparency:reduce){.dark{--message-surface:#2f2f2f}}.dark :not(.light).popover,.dark.popover,.popover .dark{--main-surface-primary:var(--gray-750);--main-surface-secondary:var(--gray-700);--main-surface-tertiary:var(--gray-600);--text-primary:var(--gray-50);--text-secondary:var(--gray-200);--text-tertiary:var(--gray-400);--text-quaternary:var(--gray-500);--sidebar-surface-primary:var(--gray-750)}.dark .light.popover,.light .popover,.light.popover,.popover{--main-surface-primary:var(--white);--main-surface-secondary:var(--gray-100);--main-surface-tertiary:var(--gray-200);--sidebar-surface-primary:var(--white)}.dark .popover.sidebar{--main-surface-secondary:#393939!important}.light .canvas-open{--main-surface-primary:#f9f9f9;--message-surface:#eee}textarea:focus{border-color:inherit;box-shadow:none;outline:none}@supports (height:100cqh){:root{--cqh-full:100cqh;--cqw-full:100cqw}}@supports not (height:100cqh){:root{--cqh-full:100dvh;--cqw-full:100dvw}}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;appearance:none;background-color:#fff;border-color:#9b9b9b;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}:is([type=text],[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select):focus{--tw-ring-inset:var(--tw-empty, );--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#004f99;--tw-ring-offset-shadow:var(--tw-ring-inset)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color);border-color:#004f99;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::placeholder,textarea::placeholder{color:#9b9b9b;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field{padding-bottom:0;padding-top:0}::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field{padding-bottom:0;padding-top:0}::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-second-field{padding-bottom:0;padding-top:0}::-webkit-datetime-edit-meridiem-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%239B9B9B' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-size:1.5em 1.5em;-webkit-print-color-adjust:exact;print-color-adjust:exact}[dir=ltr] select{background-position:right .5rem center;padding-right:2.5rem}[dir=rtl] select{background-position:left .5rem center;padding-left:2.5rem}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;-webkit-print-color-adjust:unset;print-color-adjust:unset}[dir=ltr] [multiple]{padding-right:.75rem}[dir=rtl] [multiple]{padding-left:.75rem}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;appearance:none;background-color:#fff;background-origin:border-box;border-color:#9b9b9b;border-width:1px;color:#004f99;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty, );--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#004f99;--tw-ring-offset-shadow:var(--tw-ring-inset)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-color:currentColor;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px solid buttontext;outline:1px auto -webkit-focus-ring-color}}@layer components{@property --top-fade{syntax:"";inherits:false;initial-value:0}@property --bottom-fade{syntax:"";inherits:false;initial-value:0}@property --edge-fade-distance{syntax:"";inherits:false;initial-value:.5lh}@keyframes edge-fade{0%{--top-fade:0}3%,to{--top-fade:var(--edge-fade-distance,.5lh)}0%,97%{--bottom-fade:var(--edge-fade-distance,.5lh)}to{--bottom-fade:0}}@supports (scroll-timeline:--scroll-fade){.vertical-scroll-fade-mask{scroll-timeline:--scroll-fade y;animation-timeline:--scroll-fade;animation:edge-fade;-webkit-mask:linear-gradient(to bottom in oklch,oklch(.6 0 0/0),oklch(.85 0 0/1)var(--top-fade)calc(100% - var(--bottom-fade)),oklch(.6 0 0/0));mask:linear-gradient(to bottom in oklch,oklch(.6 0 0/0),oklch(.85 0 0/1)var(--top-fade)calc(100% - var(--bottom-fade)),oklch(.6 0 0/0))}.horizontal-scroll-fade-mask{scroll-timeline:--scroll-fade x;animation-timeline:--scroll-fade;animation:edge-fade;-webkit-mask:linear-gradient(to right in oklch,oklch(.6 0 0/0),oklch(.85 0 0/1)var(--top-fade)calc(100% - var(--bottom-fade)),oklch(.6 0 0/0));mask:linear-gradient(to right in oklch,oklch(.6 0 0/0),oklch(.85 0 0/1)var(--top-fade)calc(100% - var(--bottom-fade)),oklch(.6 0 0/0))}}.icon-xs{stroke-width:1.5px;flex-shrink:0;height:calc(var(--spacing)*3);width:calc(var(--spacing)*3)}.icon-smaller{height:calc(var(--spacing)*3.5);width:calc(var(--spacing)*3.5)}.icon-sm,.icon-smaller{stroke-width:2px;flex-shrink:0}.icon-sm{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.icon-sm-adaptive{height:calc(var(--spacing)*5);width:calc(var(--spacing)*5)}@media (min-width:48rem){.icon-sm-adaptive{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}}.icon-sm-heavy{stroke-width:2.5px;flex-shrink:0;height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.icon-md{height:18px;width:18px}.icon-md,.icon-sidebar{stroke-width:1.5px;flex-shrink:0}.icon-sidebar{height:20px;width:20px}.icon-md-heavy{stroke-width:2.5px;flex-shrink:0;height:18px;width:18px}.icon-lg{stroke-width:1.5px;flex-shrink:0;height:calc(var(--spacing)*6);width:calc(var(--spacing)*6)}.icon-lg-heavy{stroke-width:2px;flex-shrink:0;height:22px;width:22px}.icon-xl{stroke-width:1.5px;flex-shrink:0;height:calc(var(--spacing)*7);width:calc(var(--spacing)*7)}.icon-xl-heavy{stroke-width:2px;flex-shrink:0;height:24px;width:24px}.icon-2xl{stroke-width:1.5px;flex-shrink:0;height:calc(var(--spacing)*8);width:calc(var(--spacing)*8)}.icon-workspace-avatar-preview{stroke-width:1.5px;height:96px;width:96px}.icon-cover{stroke-width:1.5px;height:234px;width:234px}.loading-shimmer,.loading-shimmer-pure-text{--shimmer-contrast:#ffffffbf}.dark .loading-shimmer,.dark .loading-shimmer-pure-text{--shimmer-contrast:#0009}.loading-shimmer,.loading-shimmer-pure-text{text-fill-color:transparent;-webkit-text-fill-color:transparent;animation-delay:.5s;animation-duration:3s;animation-iteration-count:infinite;animation-name:loading-shimmer;background:var(--text-secondary)linear-gradient(to right,var(--text-secondary)0,var(--shimmer-contrast)40%,var(--shimmer-contrast)60%,var(--text-secondary)100%);background:var(--text-secondary)-webkit-gradient(linear,100% 0,0 0,from(var(--text-secondary)),color-stop(.4,var(--shimmer-contrast)),color-stop(.6,var(--shimmer-contrast)),to(var(--text-secondary)));-webkit-background-clip:text;background-clip:text;background-repeat:no-repeat;background-size:50% 200%;display:inline-block}[dir=ltr] .loading-shimmer,[dir=ltr] .loading-shimmer-pure-text{background-position:-100% 0}[dir=rtl] .loading-shimmer,[dir=rtl] .loading-shimmer-pure-text{background-position:200% 0}.loading-shimmer:hover{-webkit-text-fill-color:var(--text-primary);animation:none}[dir=ltr] .loading-shimmer:hover{background:0 0}[dir=rtl] .loading-shimmer:hover{background:100% 0}.loading-shimmer-pure-text-inverted{text-fill-color:transparent;-webkit-text-fill-color:transparent;animation-delay:.5s;animation-duration:3s;animation-iteration-count:infinite;animation-name:loading-shimmer;background:var(--text-primary)gradient(linear,100% 0,0 0,from(var(--text-primary)),color-stop(.5,var(--text-quaternary)),to(var(--text-primary)));background:var(--text-primary)-webkit-gradient(linear,100% 0,0 0,from(var(--text-primary)),color-stop(.5,var(--text-quaternary)),to(var(--text-primary)));-webkit-background-clip:text;background-clip:text;background-repeat:no-repeat;background-size:50% 200%;display:inline-block}[dir=ltr] .loading-shimmer-pure-text-inverted{background-position:-100% 0}[dir=rtl] .loading-shimmer-pure-text-inverted{background-position:200% 0}.gizmo-shadow-stroke{position:relative}.gizmo-shadow-stroke:after{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#0000001a);border-radius:3.40282e+38px;content:"";inset:calc(var(--spacing)*0);position:absolute}.dark .gizmo-shadow-stroke:after,.gizmo-shadow-stroke:after{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .gizmo-shadow-stroke:after{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#fff3)}.__menu-item{--menu-item-highlighted:#0000000a;--menu-item-hover:#0000000a;--menu-item-active:#0000000f;--menu-item-open:#00000006;align-items:center;border-radius:10px;display:flex;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));margin-block:calc(var(--spacing)*0);margin-inline:calc(var(--spacing)*1.5);min-height:calc(var(--spacing)*9);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*2.5);position:relative;-webkit-user-select:none;user-select:none;width:auto}.__menu-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:0}@media (hover:hover) and (pointer:fine){.__menu-item{cursor:pointer}}@media (pointer:coarse){.__menu-item{min-height:calc(var(--spacing)*10)}}.dark .__menu-item{--menu-item-highlighted:var(--interactive-bg-secondary-hover);--menu-item-hover:var(--interactive-bg-secondary-hover);--menu-item-active:var(--interactive-bg-secondary-press);--menu-item-open:var(--interactive-bg-secondary-press)}.__menu-item[data-color=selected]{color:var(--interactive-label-accent-default)}.__menu-item[data-color=danger]{--menu-item-highlighted:#e02e2a13;--menu-item-hover:#e02e2a13;--menu-item-active:#e02e2a1f;--menu-item-open:#e02e2a0f;color:var(--text-status-error)}.dark .__menu-item[data-color=danger]{--menu-item-highlighted:#e02e2a23;--menu-item-hover:#e02e2a23;--menu-item-active:#e02e2a2f;--menu-item-open:#e02e2a16}.__menu-item:where(:disabled,[data-disabled]){color:var(--text-tertiary);pointer-events:none}.__menu-item:not(:disabled):not([data-disabled]):where(:has(:focus-visible),[data-state=open],:has([data-state=open])){background-color:var(--menu-item-open)}.__menu-item:not(:disabled):not([data-disabled])[data-highlighted]{background-color:var(--menu-item-highlighted)}@media (hover:hover){.__menu-item:not(:disabled):not([data-disabled]):hover{background-color:var(--menu-item-hover)}}.__menu-item:not(:disabled):not([data-disabled]):focus-visible{background-color:var(--menu-item-highlighted)}.__menu-item:not(:disabled):not([data-disabled]):active:not(:has([data-trailing-button]:hover)),.__menu-item:not(:disabled):not([data-disabled])[data-active]{background-color:var(--menu-item-active)}.__menu-item .trailing{align-items:center;align-self:stretch;display:flex;flex-shrink:0;justify-content:center;min-width:18px}.__menu-item .trailing-pair{align-self:stretch;display:inline-grid;grid-template-columns:max-content;place-items:center end}.__menu-item .trailing-pair>*{align-items:center;align-self:stretch;display:flex;grid-column-start:1;grid-row-start:1}@media (pointer:coarse){.__menu-item .trailing-pair>.trailing:not(.highlight){display:none}}@media not all and (pointer:coarse){.__menu-item:is([data-highlighted],:hover,:focus-visible,:has(:focus-visible),[data-state=open],:has([data-state=open])) .trailing-pair>.trailing:not(.highlight){visibility:hidden}.__menu-item:not(:is([data-highlighted],:hover,:focus-visible,:has(:focus-visible),[data-state=open],:has([data-state=open]))) .trailing.highlight{opacity:0}.__menu-item:not(:is([data-highlighted],:hover,:focus-visible,:has(:focus-visible),[data-state=open],:has([data-state=open])))[data-fill] .trailing.highlight{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}}.__menu-item-badge{--tw-leading:calc(var(--spacing)*3);--tw-font-weight:var(--font-weight-semibold);border-color:var(--border-heavy);border-radius:3.40282e+38px;border-style:var(--tw-border-style);border-width:1px;flex-shrink:0;font-size:8px;font-weight:var(--font-weight-semibold);line-height:calc(var(--spacing)*3);padding-block:calc(var(--spacing)*.5);padding-inline:calc(var(--spacing)*1);text-transform:uppercase}.__menu-item-badge:not(:is(:where(.group)[data-disabled] *)){color:var(--text-tertiary)}.__menu-item-badge:is(:where(.group)[data-disabled] *){opacity:.8}.__menu-item-trailing-btn{align-items:center;align-self:stretch;border-end-end-radius:10px;border-start-end-radius:10px;color:var(--text-primary);display:flex;isolation:isolate;margin-block:calc(var(--spacing)*-2);margin-inline-end:calc(var(--spacing)*-2.5);min-height:calc(var(--spacing)*9);padding-block:calc(var(--spacing)*2);padding-inline-end:calc(var(--spacing)*2.5);pointer-events:auto;position:relative}.__menu-item-trailing-btn:disabled{color:var(--text-tertiary);pointer-events:none}.__menu-item-trailing-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:0}:is(.__menu-item-trailing-btn:focus-visible>*){outline-style:var(--tw-outline-style);outline-width:2px}@media (pointer:coarse){.__menu-item-trailing-btn{margin:calc(var(--spacing)*-2.5);min-height:calc(var(--spacing)*10)}}[data-has-submenu] .__menu-item-trailing-btn{margin-inline-end:calc(var(--spacing)*-7);padding-inline-end:calc(var(--spacing)*7)}@media (hover:hover){:is(.__menu-item-trailing-btn:is(:where(.group)[data-disabled] *):hover>*){background-color:var(--menu-item-highlighted)}}:is(.__menu-item-trailing-btn:is(:where(.group)[data-disabled] *):active:active>*){background-color:var(--menu-item-active)}.__menu-item-trailing-btn>*{align-items:center;border-radius:8px;display:flex;justify-content:center;margin:calc(var(--spacing)*-1.5);padding:calc(var(--spacing)*1.5)}@media (pointer:coarse){.__menu-item-trailing-btn>*{margin:calc(var(--spacing)*-2);padding:calc(var(--spacing)*2)}}.__menu-label{--tw-font-weight:var(--font-weight-normal);color:var(--text-tertiary);display:block;font-size:var(--text-sm);font-weight:var(--font-weight-normal);line-height:var(--tw-leading,var(--text-sm--line-height));margin-block:calc(var(--spacing)*0);margin-inline:calc(var(--spacing)*1.5);overflow:hidden;padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*2.5);text-overflow:ellipsis;-webkit-user-select:none;user-select:none;white-space:nowrap}}@layer utilities{.\@container\/thread{container:thread/inline-size}.\@container{container-type:inline-size}.btn{--tw-font-weight:var(--font-weight-medium);align-items:center;border-color:#0000;border-radius:3.40282e+38px;border-style:var(--tw-border-style);border-width:1px;display:inline-flex;flex-shrink:0;font-size:var(--text-sm);font-weight:var(--font-weight-medium);justify-content:center;line-height:var(--tw-leading,var(--text-sm--line-height));min-height:38px;padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3.5);pointer-events:auto}.btn:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.btn:focus{outline:2px solid #0000;outline-offset:2px}}.btn:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.btn:focus-visible{outline:2px solid #0000;outline-offset:2px}}.btn:disabled{cursor:not-allowed;opacity:.5}.btn:active:not(:disabled){opacity:.8}.pointer-events-auto{pointer-events:auto}.pointer-events-auto\!{pointer-events:auto!important}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.border-glowing-gradient{--tw-blur:blur(4px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,);height:120%;inset:0 -10% 0 0;margin:auto;position:absolute;width:120%;z-index:1}[dir=ltr] .border-glowing-gradient{background:conic-gradient(var(--glow-color-1,oklch(.63 .2 254.95))120deg,var(--glow-color-2,oklch(.7 .2 254.95))150deg,var(--glow-color-3,oklch(.77 .2 254.95))200deg,var(--glow-color-4,oklch(.84 .2 254.95))240deg)}[dir=rtl] .border-glowing-gradient{background:conic-gradient(var(--glow-color-1,oklch(.63 .2 254.95))-120deg,var(--glow-color-2,oklch(.7 .2 254.95))150deg,var(--glow-color-3,oklch(.77 .2 254.95))200deg,var(--glow-color-4,oklch(.84 .2 254.95))240deg)}@media (prefers-reduced-motion:no-preference){.border-glowing-gradient{animation:spin 1.5s linear infinite}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}.absolute,.sr-only{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.static\!{position:static!important}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-5{inset:calc(var(--spacing)*5)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-x-px{inset-inline:1px}.inset-y-0{inset-block:calc(var(--spacing)*0)}.-start-1{inset-inline-start:calc(var(--spacing)*-1)}.-start-2{inset-inline-start:calc(var(--spacing)*-2)}.-start-4{inset-inline-start:calc(var(--spacing)*-4)}.-start-96{inset-inline-start:calc(var(--spacing)*-96)}.start-0{inset-inline-start:calc(var(--spacing)*0)}.start-1{inset-inline-start:calc(var(--spacing)*1)}.start-1\/2{inset-inline-start:50%}.start-2{inset-inline-start:calc(var(--spacing)*2)}.start-3{inset-inline-start:calc(var(--spacing)*3)}.start-4{inset-inline-start:calc(var(--spacing)*4)}.start-5{inset-inline-start:calc(var(--spacing)*5)}.start-6{inset-inline-start:calc(var(--spacing)*6)}.start-10{inset-inline-start:calc(var(--spacing)*10)}.start-\[-2px\]{inset-inline-start:-2px}.start-\[-150\%\]{inset-inline-start:-150%}.start-\[0\.81rem\]{inset-inline-start:.81rem}.start-\[3\.25rem\]{inset-inline-start:3.25rem}.start-\[calc\(\(\(100vw-450px-min\(100vw-450px\,850px\)\)\/2\)\)\]{inset-inline-start:calc(50vw - 225px + min(100vw - 450px,850px)/-2)}.start-full{inset-inline-start:100%}.-end-1{inset-inline-end:calc(var(--spacing)*-1)}.-end-2{inset-inline-end:calc(var(--spacing)*-2)}.-end-4{inset-inline-end:calc(var(--spacing)*-4)}.end-\(--thread-content-margin\){inset-inline-end:var(--thread-content-margin)}.end-0{inset-inline-end:calc(var(--spacing)*0)}.end-1{inset-inline-end:calc(var(--spacing)*1)}.end-1\.5{inset-inline-end:calc(var(--spacing)*1.5)}.end-1\/2{inset-inline-end:50%}.end-2{inset-inline-end:calc(var(--spacing)*2)}.end-2\.5{inset-inline-end:calc(var(--spacing)*2.5)}.end-3{inset-inline-end:calc(var(--spacing)*3)}.end-4{inset-inline-end:calc(var(--spacing)*4)}.end-5{inset-inline-end:calc(var(--spacing)*5)}.end-6{inset-inline-end:calc(var(--spacing)*6)}.end-14{inset-inline-end:calc(var(--spacing)*14)}.end-\[-1px\]{inset-inline-end:-1px}.end-\[-3px\]{inset-inline-end:-3px}.end-\[-8px\]{inset-inline-end:-8px}.end-\[-135px\]{inset-inline-end:-135px}.end-\[4\.8px\]{inset-inline-end:4.8px}.end-\[12px\]{inset-inline-end:12px}.end-full{inset-inline-end:100%}.end-snc-1{inset-inline-end:var(--snc-1)}.-top-0{top:calc(var(--spacing)*0)}.-top-0\.5{top:calc(var(--spacing)*-.5)}.-top-1{top:calc(var(--spacing)*-1)}.-top-2{top:calc(var(--spacing)*-2)}.-top-3\!{top:calc(var(--spacing)*-3)!important}.-top-4{top:calc(var(--spacing)*-4)}.-top-5{top:calc(var(--spacing)*-5)}.-top-96{top:calc(var(--spacing)*-96)}.top-0{top:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-1\.5{top:calc(var(--spacing)*1.5)}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing)*2)}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-3{top:calc(var(--spacing)*3)}.top-4{top:calc(var(--spacing)*4)}.top-5{top:calc(var(--spacing)*5)}.top-6{top:calc(var(--spacing)*6)}.top-8{top:calc(var(--spacing)*8)}.top-9{top:calc(var(--spacing)*9)}.top-14{top:calc(var(--spacing)*14)}.top-24{top:calc(var(--spacing)*24)}.top-48{top:calc(var(--spacing)*48)}.top-\[-0\.094rem\]{top:-.094rem}.top-\[-1px\]{top:-1px}.top-\[-2px\]{top:-2px}.top-\[-4px\]{top:-4px}.top-\[-6px\]{top:-6px}.top-\[-8px\]{top:-8px}.top-\[-150\%\]{top:-150%}.top-\[0\.55rem\]{top:.55rem}.top-\[1px\]{top:1px}.top-\[9px\]{top:9px}.top-\[20px\]{top:20px}.top-\[21\.5px\]{top:21.5px}.top-full{top:100%}[dir=ltr] .right-0{right:calc(var(--spacing)*0)}[dir=rtl] .right-0{left:calc(var(--spacing)*0)}[dir=ltr] .right-0\!{right:calc(var(--spacing)*0)!important}[dir=rtl] .right-0\!{left:calc(var(--spacing)*0)!important}.-bottom-0\.5{bottom:calc(var(--spacing)*-.5)}.-bottom-2{bottom:calc(var(--spacing)*-2)}.-bottom-4{bottom:calc(var(--spacing)*-4)}.-bottom-5{bottom:calc(var(--spacing)*-5)}.-bottom-px{bottom:-1px}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-1{bottom:calc(var(--spacing)*1)}.bottom-2{bottom:calc(var(--spacing)*2)}.bottom-3{bottom:calc(var(--spacing)*3)}.bottom-4{bottom:calc(var(--spacing)*4)}.bottom-5{bottom:calc(var(--spacing)*5)}.bottom-6{bottom:calc(var(--spacing)*6)}.bottom-8{bottom:calc(var(--spacing)*8)}.bottom-\[-2px\]{bottom:-2px}.bottom-\[-3px\]{bottom:-3px}.bottom-\[1px\]{bottom:1px}.bottom-\[8px\]{bottom:8px}.bottom-\[20px\]{bottom:20px}.bottom-\[64px\]{bottom:64px}.bottom-full{bottom:100%}.bottom-snc-1{bottom:var(--snc-1)}[dir=ltr] .left-0\!{left:calc(var(--spacing)*0)!important}[dir=rtl] .left-0\!{right:calc(var(--spacing)*0)!important}[dir=ltr] .left-\[50\%\]\!{left:50%!important}[dir=rtl] .left-\[50\%\]\!{right:50%!important}.isolate{isolation:isolate}.-z-10{z-index:-10}.z-0{z-index:0}.z-1{z-index:1}.z-2{z-index:2}.z-3{z-index:3}.z-10{z-index:10}.z-11{z-index:11}.z-20{z-index:20}.z-21{z-index:21}.z-25{z-index:25}.z-26{z-index:26}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-60{z-index:60}.z-61{z-index:61}.z-70{z-index:70}.z-100{z-index:100}.z-1000{z-index:1000}.z-9999{z-index:9999}.z-10000{z-index:10000}.z-11000{z-index:11000}.z-\[-1\]{z-index:-1}.z-\[2\]{z-index:2}.z-\[120\]{z-index:120}.z-\[10000\]{z-index:10000}.order-1{order:1}.order-2{order:2}.order-4{order:4}.order-5{order:5}.order-10{order:10}.order-last{order:9999}.col-\[1\]{grid-column:1}.col-auto{grid-column:auto}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-9{grid-column:span 9/span 9}.col-start-1{grid-column-start:1}.col-start-2{grid-column-start:2}.col-end-2{grid-column-end:2}.row-\[1\]{grid-row:1}.row-auto{grid-row:auto}.row-span-2{grid-row:span 2/span 2}.row-span-4{grid-row:span 4/span 4}.row-start-1{grid-row-start:1}.row-start-2{grid-row-start:2}.row-end-2{grid-row-end:2}.float-end{float:inline-end}[dir=ltr] .float-left{float:left}[dir=ltr] .float-right,[dir=rtl] .float-left{float:right}[dir=rtl] .float-right{float:left}.float-start{float:inline-start}.clear-end{clear:inline-end}[dir=ltr] .clear-left{clear:left}[dir=ltr] .clear-right,[dir=rtl] .clear-left{clear:right}[dir=rtl] .clear-right{clear:left}.clear-start{clear:inline-start}@media (min-width:480px){.container{max-width:480px}}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.prose{--tw-prose-body:var(--text-primary);--tw-prose-headings:var(--text-primary);--tw-prose-lead:var(--text-primary);--tw-prose-links:var(--text-primary);--tw-prose-bold:var(--text-primary);--tw-prose-counters:var(--text-primary);--tw-prose-bullets:var(--text-primary);--tw-prose-hr:var(--border-xheavy);--tw-prose-quotes:var(--text-primary);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:var(--text-secondary);--tw-prose-code:var(--text-primary);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:var(--text-primary);--tw-prose-invert-headings:var(--text-primary);--tw-prose-invert-lead:var(--text-primary);--tw-prose-invert-links:var(--text-primary);--tw-prose-invert-bold:var(--text-primary);--tw-prose-invert-counters:var(--text-primary);--tw-prose-invert-bullets:var(--text-primary);--tw-prose-invert-hr:var(--border-xheavy);--tw-prose-invert-quotes:var(--text-primary);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:var(--text-secondary);--tw-prose-invert-code:var(--text-primary);--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);color:var(--tw-prose-body);font-size:1rem;line-height:1.75;max-width:65ch}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em}[dir=ltr] .prose :where(ol):not(:where([class~=not-prose] *)){padding-left:1.625em}[dir=rtl] .prose :where(ol):not(:where([class~=not-prose] *)){padding-right:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em}[dir=ltr] .prose :where(ul):not(:where([class~=not-prose] *)){padding-left:1.625em}[dir=rtl] .prose :where(ul):not(:where([class~=not-prose] *)){padding-right:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){color:var(--tw-prose-quotes);font-style:normal;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;quotes:"“""”""‘""’"}[dir=ltr] .prose :where(blockquote):not(:where([class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;padding-left:1em}[dir=rtl] .prose :where(blockquote):not(:where([class~=not-prose] *)){border-right-color:var(--tw-prose-quote-borders);border-right-width:.25rem;padding-right:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.11111;margin-bottom:.888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.33333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.42857;margin-top:.857143em}.prose :where(code):not(:where([class~=not-prose] *)){background-color:var(--gray-100);border-radius:.25rem;color:var(--tw-prose-code);font-size:.875em;font-weight:500;padding:.15rem .3rem}.prose :where(code):not(:where([class~=not-prose] *)):after,.prose :where(code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(a code):not(:where([class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(blockquote code):not(:where([class~=not-prose] *)),.prose :where(h4 code):not(:where([class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){background-color:#0000;border-radius:.375rem;color:currentColor;font-size:.875em;font-weight:400;line-height:1.71429;margin:0;overflow-x:auto;padding:0}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:#0000;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose] *)):after,.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(table):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.71429;margin-bottom:2em;margin-top:2em;table-layout:auto;width:100%}[dir=ltr] .prose :where(table):not(:where([class~=not-prose] *)){text-align:left}[dir=rtl] .prose :where(table):not(:where([class~=not-prose] *)){text-align:right}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.571429em;padding-left:.571429em;padding-right:.571429em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose :where(p):not(:where([class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(figure):not(:where([class~=not-prose] *)),.prose :where(video):not(:where([class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}[dir=ltr] .prose :where(ol>li):not(:where([class~=not-prose] *)),[dir=ltr] .prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}[dir=rtl] .prose :where(ol>li):not(:where([class~=not-prose] *)),[dir=rtl] .prose :where(ul>li):not(:where([class~=not-prose] *)){padding-right:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(h2+*):not(:where([class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose] *)),.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}[dir=ltr] .prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}[dir=rtl] .prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-right:0}[dir=ltr] .prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}[dir=rtl] .prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.571429em}[dir=ltr] .prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}[dir=rtl] .prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-right:0}[dir=ltr] .prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}[dir=rtl] .prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.-m-1{margin:calc(var(--spacing)*-1)}.-m-1\!{margin:calc(var(--spacing)*-1)!important}.m-0{margin:calc(var(--spacing)*0)}.m-1{margin:calc(var(--spacing)*1)}.m-1\.5{margin:calc(var(--spacing)*1.5)}.m-2{margin:calc(var(--spacing)*2)}.m-3{margin:calc(var(--spacing)*3)}.m-4{margin:calc(var(--spacing)*4)}.m-6{margin:calc(var(--spacing)*6)}.m-8{margin:calc(var(--spacing)*8)}.m-\[-1px\]{margin:-1px}.m-\[3px\]{margin:3px}.m-\[24px\]{margin:24px}.m-auto{margin:auto}.-mx-0\.5{margin-inline:calc(var(--spacing)*-.5)}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.-mx-2{margin-inline:calc(var(--spacing)*-2)}.-mx-3{margin-inline:calc(var(--spacing)*-3)}.-mx-6{margin-inline:calc(var(--spacing)*-6)}.-mx-px{margin-inline:-1px}.mx-0\!{margin-inline:calc(var(--spacing)*0)!important}.mx-0\.5{margin-inline:calc(var(--spacing)*.5)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-1\.5{margin-inline:calc(var(--spacing)*1.5)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-3\.5{margin-inline:calc(var(--spacing)*3.5)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-5{margin-inline:calc(var(--spacing)*5)}.mx-6{margin-inline:calc(var(--spacing)*6)}.mx-16{margin-inline:calc(var(--spacing)*16)}.mx-24{margin-inline:calc(var(--spacing)*24)}.mx-\[-1rem\]{margin-inline:-1rem}.mx-\[-16px\]{margin-inline:-16px}.mx-\[3px\]{margin-inline:3px}.mx-\[32px\]{margin-inline:32px}.mx-\[calc\(--spacing\(-2\)-1px\)\]{margin-inline:calc(var(--spacing)*-2 - 1px)}.mx-auto{margin-inline:auto}.mx-snc-results-padding{margin-inline:var(--snc-results-padding)}.-my-0\.5{margin-block:calc(var(--spacing)*-.5)}.-my-1{margin-block:calc(var(--spacing)*-1)}.-my-2{margin-block:calc(var(--spacing)*-2)}.-my-2\.5{margin-block:calc(var(--spacing)*-2.5)}.-my-3{margin-block:calc(var(--spacing)*-3)}.-my-\[1px\]{margin-block:-1px}.my-0{margin-block:calc(var(--spacing)*0)}.my-0\.5{margin-block:calc(var(--spacing)*.5)}.my-1{margin-block:calc(var(--spacing)*1)}.my-1\.5{margin-block:calc(var(--spacing)*1.5)}.my-2{margin-block:calc(var(--spacing)*2)}.my-2\.5{margin-block:calc(var(--spacing)*2.5)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.my-6{margin-block:calc(var(--spacing)*6)}.my-8{margin-block:calc(var(--spacing)*8)}.my-10{margin-block:calc(var(--spacing)*10)}.my-12{margin-block:calc(var(--spacing)*12)}.my-16{margin-block:calc(var(--spacing)*16)}.my-\[-0\.2rem\]{margin-block:-.2rem}.my-\[15px\]{margin-block:15px}.my-\[32px\]{margin-block:32px}.my-auto{margin-block:auto}.-ms-0\.5{margin-inline-start:calc(var(--spacing)*-.5)}.-ms-1{margin-inline-start:calc(var(--spacing)*-1)}.-ms-1\.5{margin-inline-start:calc(var(--spacing)*-1.5)}.-ms-2{margin-inline-start:calc(var(--spacing)*-2)}.-ms-2\.5{margin-inline-start:calc(var(--spacing)*-2.5)}.-ms-3{margin-inline-start:calc(var(--spacing)*-3)}.-ms-3\.5{margin-inline-start:calc(var(--spacing)*-3.5)}.-ms-4{margin-inline-start:calc(var(--spacing)*-4)}.-ms-6{margin-inline-start:calc(var(--spacing)*-6)}.ms-0{margin-inline-start:calc(var(--spacing)*0)}.ms-0\.5{margin-inline-start:calc(var(--spacing)*.5)}.ms-1{margin-inline-start:calc(var(--spacing)*1)}.ms-1\.5{margin-inline-start:calc(var(--spacing)*1.5)}.ms-2{margin-inline-start:calc(var(--spacing)*2)}.ms-2\.5{margin-inline-start:calc(var(--spacing)*2.5)}.ms-3{margin-inline-start:calc(var(--spacing)*3)}.ms-4{margin-inline-start:calc(var(--spacing)*4)}.ms-5{margin-inline-start:calc(var(--spacing)*5)}.ms-6{margin-inline-start:calc(var(--spacing)*6)}.ms-7{margin-inline-start:calc(var(--spacing)*7)}.ms-8{margin-inline-start:calc(var(--spacing)*8)}.ms-10{margin-inline-start:calc(var(--spacing)*10)}.ms-14{margin-inline-start:calc(var(--spacing)*14)}.ms-24{margin-inline-start:calc(var(--spacing)*24)}.ms-\[-2px\]{margin-inline-start:-2px}.ms-\[-6px\]{margin-inline-start:-6px}.ms-\[-12px\]{margin-inline-start:-12px}.ms-\[-16px\]{margin-inline-start:-16px}.ms-\[1px\]{margin-inline-start:1px}.ms-\[2px\]{margin-inline-start:2px}.ms-\[3px\]{margin-inline-start:3px}.ms-\[4px\]{margin-inline-start:4px}.ms-\[5px\]{margin-inline-start:5px}.ms-\[11px\]{margin-inline-start:11px}.ms-\[calc\(\(100vw-450px-min\(100vw-450px\,900px\)\)\/2\)\]{margin-inline-start:calc(50vw - 225px + min(100vw - 450px,900px)/-2)}.ms-auto{margin-inline-start:auto}.-me-1{margin-inline-end:calc(var(--spacing)*-1)}.-me-1\.5{margin-inline-end:calc(var(--spacing)*-1.5)}.-me-2{margin-inline-end:calc(var(--spacing)*-2)}.-me-3\.5{margin-inline-end:calc(var(--spacing)*-3.5)}.-me-6{margin-inline-end:calc(var(--spacing)*-6)}.me-0{margin-inline-end:calc(var(--spacing)*0)}.me-0\.5{margin-inline-end:calc(var(--spacing)*.5)}.me-1{margin-inline-end:calc(var(--spacing)*1)}.me-1\.5{margin-inline-end:calc(var(--spacing)*1.5)}.me-2{margin-inline-end:calc(var(--spacing)*2)}.me-3{margin-inline-end:calc(var(--spacing)*3)}.me-4{margin-inline-end:calc(var(--spacing)*4)}.me-5{margin-inline-end:calc(var(--spacing)*5)}.me-6{margin-inline-end:calc(var(--spacing)*6)}.me-8{margin-inline-end:calc(var(--spacing)*8)}.me-12{margin-inline-end:calc(var(--spacing)*12)}.me-\[-10px\]{margin-inline-end:-10px}.me-\[0\.1875rem\]{margin-inline-end:.1875rem}.me-\[1px\]{margin-inline-end:1px}.me-\[2px\]{margin-inline-end:2px}.me-\[30px\]{margin-inline-end:30px}.me-px{margin-inline-end:1px}.-mt-0\.5{margin-top:calc(var(--spacing)*-.5)}.-mt-1{margin-top:calc(var(--spacing)*-1)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.-mt-3{margin-top:calc(var(--spacing)*-3)}.-mt-4{margin-top:calc(var(--spacing)*-4)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.-mt-6{margin-top:calc(var(--spacing)*-6)}.-mt-header-height{margin-top:calc(var(--header-height)*-1)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\!{margin-top:calc(var(--spacing)*0)!important}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-0\.25{margin-top:calc(var(--spacing)*.25)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-2\.5{margin-top:calc(var(--spacing)*2.5)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-7{margin-top:calc(var(--spacing)*7)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-10{margin-top:calc(var(--spacing)*10)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-16{margin-top:calc(var(--spacing)*16)}.mt-20{margin-top:calc(var(--spacing)*20)}.mt-36{margin-top:calc(var(--spacing)*36)}.mt-\[-1px\]{margin-top:-1px}.mt-\[-2px\]{margin-top:-2px}.mt-\[-3px\]{margin-top:-3px}.mt-\[-4px\]{margin-top:-4px}.mt-\[-8px\]{margin-top:-8px}.mt-\[-10px\]{margin-top:-10px}.mt-\[-32px\]{margin-top:-32px}.mt-\[-100px\]{margin-top:-100px}.mt-\[\.5px\]{margin-top:.5px}.mt-\[0\.225rem\]{margin-top:.225rem}.mt-\[0\.425rem\]{margin-top:.425rem}.mt-\[0\.0625em\]{margin-top:.0625em}.mt-\[0px\]{margin-top:0}.mt-\[1px\]{margin-top:1px}.mt-\[2px\]{margin-top:2px}.mt-\[3px\]{margin-top:3px}.mt-\[5px\]{margin-top:5px}.mt-\[11px\]{margin-top:11px}.mt-\[100px\]{margin-top:100px}.mt-\[calc\(var\(--threadFlyOut-leading-height\,57px\)\*-1\)\]{margin-top:calc(var(--threadFlyOut-leading-height,57px)*-1)}.mt-\[var\(--screen-optical-compact-offset-amount\)\]{margin-top:var(--screen-optical-compact-offset-amount)}.mt-auto{margin-top:auto}.mt-px{margin-top:1px}.mt-snc-1{margin-top:var(--snc-1)}.-mb-\(--composer-overlap-px\){margin-bottom:calc(var(--composer-overlap-px)*-1)}.-mb-0\.5{margin-bottom:calc(var(--spacing)*-.5)}.-mb-1{margin-bottom:calc(var(--spacing)*-1)}.-mb-2{margin-bottom:calc(var(--spacing)*-2)}.-mb-4{margin-bottom:calc(var(--spacing)*-4)}.-mb-5{margin-bottom:calc(var(--spacing)*-5)}.-mb-6{margin-bottom:calc(var(--spacing)*-6)}.-mb-10{margin-bottom:calc(var(--spacing)*-10)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-0\!{margin-bottom:calc(var(--spacing)*0)!important}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-2\.5{margin-bottom:calc(var(--spacing)*2.5)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-4\.5{margin-bottom:calc(var(--spacing)*4.5)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-7{margin-bottom:calc(var(--spacing)*7)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-9{margin-bottom:calc(var(--spacing)*9)}.mb-10{margin-bottom:calc(var(--spacing)*10)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-14{margin-bottom:calc(var(--spacing)*14)}.mb-36{margin-bottom:calc(var(--spacing)*36)}.mb-\[-1px\]{margin-bottom:-1px}.mb-\[-2px\]{margin-bottom:-2px}.mb-\[-5px\]{margin-bottom:-5px}.mb-\[-6px\]{margin-bottom:-6px}.mb-\[0\.25rem\]{margin-bottom:.25rem}.mb-\[0\.225rem\]{margin-bottom:.225rem}.mb-\[0\.425rem\]{margin-bottom:.425rem}.mb-\[0\.3125rem\]{margin-bottom:.3125rem}.mb-\[1px\]{margin-bottom:1px}.mb-\[4px\]{margin-bottom:4px}.mb-\[6px\]{margin-bottom:6px}.mb-\[8px\]{margin-bottom:8px}.mb-snc-1{margin-bottom:var(--snc-1)}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-3,.line-clamp-4{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4}.line-clamp-5{-webkit-line-clamp:5}.line-clamp-5,.line-clamp-6{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-6{-webkit-line-clamp:6}.line-clamp-12{-webkit-line-clamp:12;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[display\:var\(--display-hidden-until-loaded\,block\)\]{display:var(--display-hidden-until-loaded,block)}.\[display\:var\(--display-hidden-until-loaded\,flex\)\]{display:var(--display-hidden-until-loaded,flex)}.\[display\:var\(--force-hide-label\)\]{display:var(--force-hide-label)}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.hidden\!{display:none!important}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.aspect-3\/2{aspect-ratio:3/2}.aspect-4\/3{aspect-ratio:4/3}.aspect-4\/5{aspect-ratio:4/5}.aspect-4\/7{aspect-ratio:4/7}.aspect-7\/4{aspect-ratio:7/4}.aspect-16\/9{aspect-ratio:16/9}.aspect-\[3\/1\]{aspect-ratio:3}.aspect-\[1024\/700\]{aspect-ratio:1024/700}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.size-1\.5{height:calc(var(--spacing)*1.5);width:calc(var(--spacing)*1.5)}.size-4{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.size-5{height:calc(var(--spacing)*5);width:calc(var(--spacing)*5)}.size-6{height:calc(var(--spacing)*6);width:calc(var(--spacing)*6)}.size-\[10px\]{height:10px;width:10px}.size-full{height:100%;width:100%}.size-min{height:min-content;width:min-content}.\!h-8{height:calc(var(--spacing)*8)!important}.h-0{height:calc(var(--spacing)*0)}.h-0\.5{height:calc(var(--spacing)*.5)}.h-1{height:calc(var(--spacing)*1)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-1\/4{height:25%}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-2\/3{height:66.6667%}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing)*4)}.h-4\.5{height:calc(var(--spacing)*4.5)}.h-4\/5{height:80%}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-6\!{height:calc(var(--spacing)*6)!important}.h-7{height:calc(var(--spacing)*7)}.h-7\!{height:calc(var(--spacing)*7)!important}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-14\.5{height:calc(var(--spacing)*14.5)}.h-15{height:calc(var(--spacing)*15)}.h-16{height:calc(var(--spacing)*16)}.h-20{height:calc(var(--spacing)*20)}.h-20\!{height:calc(var(--spacing)*20)!important}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-36{height:calc(var(--spacing)*36)}.h-40{height:calc(var(--spacing)*40)}.h-42{height:calc(var(--spacing)*42)}.h-48{height:calc(var(--spacing)*48)}.h-52{height:calc(var(--spacing)*52)}.h-60{height:calc(var(--spacing)*60)}.h-72{height:calc(var(--spacing)*72)}.h-96{height:calc(var(--spacing)*96)}.h-\[0\.6rem\]{height:.6rem}.h-\[0\.75rem\]{height:.75rem}.h-\[1em\]{height:1em}.h-\[1px\]{height:1px}.h-\[2px\]{height:2px}.h-\[4px\]{height:4px}.h-\[6px\]{height:6px}.h-\[10px\]{height:10px}.h-\[11px\]{height:11px}.h-\[14px\]{height:14px}.h-\[15dvh\]{height:15dvh}.h-\[15px\]{height:15px}.h-\[16px\]{height:16px}.h-\[18px\]{height:18px}.h-\[19px\]{height:19px}.h-\[20px\]{height:20px}.h-\[22px\]{height:22px}.h-\[23px\]{height:23px}.h-\[24px\]{height:24px}.h-\[24rem\]{height:24rem}.h-\[25px\]{height:25px}.h-\[26px\]{height:26px}.h-\[27px\]{height:27px}.h-\[28px\]{height:28px}.h-\[30px\]{height:30px}.h-\[30vh\]{height:30vh}.h-\[32px\]{height:32px}.h-\[34px\]{height:34px}.h-\[38px\]{height:38px}.h-\[38px\]\!{height:38px!important}.h-\[40px\]{height:40px}.h-\[42px\]{height:42px}.h-\[44px\]{height:44px}.h-\[45px\]{height:45px}.h-\[50dvh\]{height:50dvh}.h-\[50px\]{height:50px}.h-\[50vh\]{height:50vh}.h-\[54px\]{height:54px}.h-\[60px\]{height:60px}.h-\[60vh\]{height:60vh}.h-\[62px\]{height:62px}.h-\[64px\]{height:64px}.h-\[70px\]{height:70px}.h-\[70vh\]{height:70vh}.h-\[76px\]{height:76px}.h-\[100\%\]{height:100%}.h-\[100dvh\]{height:100dvh}.h-\[100px\]{height:100px}.h-\[100vh\]{height:100vh}.h-\[104px\]{height:104px}.h-\[116px\]{height:116px}.h-\[120px\]{height:120px}.h-\[132px\]{height:132px}.h-\[150px\]{height:150px}.h-\[160px\]{height:160px}.h-\[200px\]{height:200px}.h-\[205px\]{height:205px}.h-\[213px\]{height:213px}.h-\[250px\]{height:250px}.h-\[300px\]{height:300px}.h-\[340px\]{height:340px}.h-\[378px\]{height:378px}.h-\[400\%\]{height:400%}.h-\[400px\]{height:400px}.h-\[420px\]{height:420px}.h-\[600px\]{height:600px}.h-\[650px\]{height:650px}.h-\[860px\]{height:860px}.h-\[calc\(100\%\+var\(--snc-1\)\)\]{height:calc(100% + var(--snc-1))}.h-\[calc\(100vh-25rem\)\]{height:calc(100vh - 25rem)}.h-\[calc\(100vh-54px\)\]{height:calc(100vh - 54px)}.h-\[calc\(100vh-325px\)\]{height:calc(100vh - 325px)}.h-\[calc\(100vh-theme\(spacing\.header-height\)-80px\)\]{height:calc(100vh - var(--header-height) - 80px)}.h-\[calc\(clamp\(150px\,1\/4\*var\(--thread-safe-area-height\,100lvh\)\,400px\)\)\]{height:clamp(150px,1/4*var(--thread-safe-area-height,100lvh),400px)}.h-\[calc\(var\(--header-height\,3\.5rem\)\+1px\)\]{height:calc(var(--header-height,3.5rem) + 1px)}.h-\[max\(3rem\,18vh\)\]{height:max(3rem,18vh)}.h-\[var\(--screen-height-override\,calc\(var\(--cqh-full\)-var\(--screen-height-offset\,0px\)\)\)\]{height:var(--screen-height-override,calc(var(--cqh-full) - var(--screen-height-offset,0px)))}.h-auto{height:auto}.h-auto\!{height:auto!important}.h-dvh{height:100dvh}.h-fit{height:fit-content}.h-fit\!{height:fit-content!important}.h-full{height:100%}.h-header-height{height:var(--header-height)}.h-max{height:max-content}.h-min{height:min-content}.h-px{height:1px}.h-screen{height:100vh}.h-snc-input-height{height:var(--snc-input-height)}.h-svh{height:100svh}.max-h-2\/3{max-height:66.6667%}.max-h-9{max-height:calc(var(--spacing)*9)}.max-h-12{max-height:calc(var(--spacing)*12)}.max-h-16{max-height:calc(var(--spacing)*16)}.max-h-28{max-height:calc(var(--spacing)*28)}.max-h-32{max-height:calc(var(--spacing)*32)}.max-h-36{max-height:calc(var(--spacing)*36)}.max-h-40{max-height:calc(var(--spacing)*40)}.max-h-48{max-height:calc(var(--spacing)*48)}.max-h-52{max-height:calc(var(--spacing)*52)}.max-h-56{max-height:calc(var(--spacing)*56)}.max-h-60{max-height:calc(var(--spacing)*60)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[25dvh\]{max-height:25dvh}.max-h-\[28rem\]{max-height:28rem}.max-h-\[50dvh\]{max-height:50dvh}.max-h-\[50vh\]{max-height:50vh}.max-h-\[60vh\]{max-height:60vh}.max-h-\[64px\]{max-height:64px}.max-h-\[75vh\]{max-height:75vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[95\%\]{max-height:95%}.max-h-\[100vh\]{max-height:100vh}.max-h-\[100vh\]\!{max-height:100vh!important}.max-h-\[188px\]{max-height:188px}.max-h-\[200px\]{max-height:200px}.max-h-\[220px\]{max-height:220px}.max-h-\[300px\]{max-height:300px}.max-h-\[400px\]{max-height:400px}.max-h-\[440px\]{max-height:440px}.max-h-\[500px\]{max-height:500px}.max-h-\[550px\]{max-height:550px}.max-h-\[600px\]{max-height:600px}.max-h-\[700px\]{max-height:700px}.max-h-\[calc\(100vh-46px\)\]{max-height:calc(100vh - 46px)}.max-h-\[calc\(100vh-150px\)\]{max-height:calc(100vh - 150px)}.max-h-\[calc\(100vh-300px\)\]{max-height:calc(100vh - 300px)}.max-h-\[calc\(clamp\(20px\,1\/4\*var\(--thread-safe-area-height\,100lvh\)\,400px\)\)\]{max-height:clamp(20px,1/4*var(--thread-safe-area-height,100lvh),400px)}.max-h-\[calc\(clamp\(20px\,1\/8\*var\(--thread-safe-area-height\,100lvh\)\,200px\)\)\]{max-height:clamp(20px,1/8*var(--thread-safe-area-height,100lvh),200px)}.max-h-\[calc\(var\(--radix-popper-available-height\)-2rem\)\]{max-height:calc(var(--radix-popper-available-height) - 2rem)}.max-h-\[var\(--radix-dropdown-menu-content-available-height\)\]{max-height:var(--radix-dropdown-menu-content-available-height)}.max-h-dvh{max-height:100dvh}.max-h-fit{max-height:fit-content}.max-h-full{max-height:100%}.max-h-screen{max-height:100vh}.max-h-svh{max-height:100svh}.btn-giant{--tw-font-weight:var(--font-weight-semibold);font-size:var(--text-base);font-weight:var(--font-weight-semibold);line-height:var(--tw-leading,var(--text-base--line-height));min-height:46px;padding-block:calc(var(--spacing)*2.5);padding-inline:calc(var(--spacing)*6)}.btn-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));min-height:26px;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3)}.btn-large{min-height:46px;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4)}.btn-small{min-height:30px;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3)}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-0\!{min-height:calc(var(--spacing)*0)!important}.min-h-4{min-height:calc(var(--spacing)*4)}.min-h-5{min-height:calc(var(--spacing)*5)}.min-h-6{min-height:calc(var(--spacing)*6)}.min-h-7{min-height:calc(var(--spacing)*7)}.min-h-8{min-height:calc(var(--spacing)*8)}.min-h-9{min-height:calc(var(--spacing)*9)}.min-h-10{min-height:calc(var(--spacing)*10)}.min-h-10\.5{min-height:calc(var(--spacing)*10.5)}.min-h-12{min-height:calc(var(--spacing)*12)}.min-h-16{min-height:calc(var(--spacing)*16)}.min-h-18{min-height:calc(var(--spacing)*18)}.min-h-20{min-height:calc(var(--spacing)*20)}.min-h-24{min-height:calc(var(--spacing)*24)}.min-h-36{min-height:calc(var(--spacing)*36)}.min-h-52{min-height:calc(var(--spacing)*52)}.min-h-60{min-height:calc(var(--spacing)*60)}.min-h-64{min-height:calc(var(--spacing)*64)}.min-h-72{min-height:calc(var(--spacing)*72)}.min-h-80{min-height:calc(var(--spacing)*80)}.min-h-96{min-height:calc(var(--spacing)*96)}.min-h-\[20px\]{min-height:20px}.min-h-\[34px\]{min-height:34px}.min-h-\[36px\]{min-height:36px}.min-h-\[36px\]\!{min-height:36px!important}.min-h-\[38px\]{min-height:38px}.min-h-\[40px\]{min-height:40px}.min-h-\[40vh\]{min-height:40vh}.min-h-\[44px\]{min-height:44px}.min-h-\[50dvh\]{min-height:50dvh}.min-h-\[50px\]{min-height:50px}.min-h-\[50vh\]{min-height:50vh}.min-h-\[52px\]{min-height:52px}.min-h-\[56px\]{min-height:56px}.min-h-\[60px\]{min-height:60px}.min-h-\[62px\]{min-height:62px}.min-h-\[64px\]{min-height:64px}.min-h-\[75vh\]{min-height:75vh}.min-h-\[80px\]{min-height:80px}.min-h-\[80vh\]{min-height:80vh}.min-h-\[90px\]{min-height:90px}.min-h-\[96px\]{min-height:96px}.min-h-\[100dvh\]{min-height:100dvh}.min-h-\[104px\]{min-height:104px}.min-h-\[108px\]{min-height:108px}.min-h-\[132px\]{min-height:132px}.min-h-\[200px\]{min-height:200px}.min-h-\[250px\]{min-height:250px}.min-h-\[320px\]{min-height:320px}.min-h-\[350px\]{min-height:350px}.min-h-\[360px\]{min-height:360px}.min-h-\[440px\]{min-height:440px}.min-h-\[480px\]{min-height:480px}.min-h-\[560px\]{min-height:560px}.min-h-\[600px\]{min-height:600px}.min-h-\[calc\(var\(--header-height\,3\.5rem\)\+1px\)\]{min-height:calc(var(--header-height,3.5rem) + 1px)}.min-h-\[max\(var\(--gutter-min-height\,0px\)\,var\(--gutter-remaining-height\,0px\)\)\]{min-height:max(var(--gutter-min-height,0px),var(--gutter-remaining-height,0px))}.min-h-bloop{min-height:227px}.min-h-fit{min-height:fit-content}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.min-h-svh{min-height:100svh}.\!w-8{width:calc(var(--spacing)*8)!important}.\!w-full{width:100%!important}.w-0{width:calc(var(--spacing)*0)}.w-0\!{width:calc(var(--spacing)*0)!important}.w-1{width:calc(var(--spacing)*1)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-1\/2{width:50%}.w-1\/3{width:33.3333%}.w-1\/4{width:25%}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-2\/3{width:66.6667%}.w-2\/5{width:40%}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-3\/4{width:75%}.w-3\/4\!{width:75%!important}.w-3xl{width:var(--container-3xl)}.w-4{width:calc(var(--spacing)*4)}.w-4\.5{width:calc(var(--spacing)*4.5)}.w-4\/5{width:80%}.w-4xl{width:var(--container-4xl)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-6\!{width:calc(var(--spacing)*6)!important}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-10\/12{width:83.3333%}.w-11{width:calc(var(--spacing)*11)}.w-11\.5{width:calc(var(--spacing)*11.5)}.w-12{width:calc(var(--spacing)*12)}.w-14{width:calc(var(--spacing)*14)}.w-14\.5{width:calc(var(--spacing)*14.5)}.w-15{width:calc(var(--spacing)*15)}.w-16{width:calc(var(--spacing)*16)}.w-20{width:calc(var(--spacing)*20)}.w-20\!{width:calc(var(--spacing)*20)!important}.w-24{width:calc(var(--spacing)*24)}.w-28{width:calc(var(--spacing)*28)}.w-32{width:calc(var(--spacing)*32)}.w-36{width:calc(var(--spacing)*36)}.w-40{width:calc(var(--spacing)*40)}.w-44{width:calc(var(--spacing)*44)}.w-48{width:calc(var(--spacing)*48)}.w-50{width:calc(var(--spacing)*50)}.w-52{width:calc(var(--spacing)*52)}.w-56{width:calc(var(--spacing)*56)}.w-60{width:calc(var(--spacing)*60)}.w-64{width:calc(var(--spacing)*64)}.w-70{width:calc(var(--spacing)*70)}.w-72{width:calc(var(--spacing)*72)}.w-80{width:calc(var(--spacing)*80)}.w-96{width:calc(var(--spacing)*96)}.w-100{width:25rem}.w-\[0\.75rem\]{width:.75rem}.w-\[1em\]{width:1em}.w-\[1px\]{width:1px}.w-\[3px\]{width:3px}.w-\[4px\]{width:4px}.w-\[6px\]{width:6px}.w-\[7\.5rem\]{width:7.5rem}.w-\[8rem\]{width:8rem}.w-\[11px\]{width:11px}.w-\[12px\]{width:12px}.w-\[14px\]{width:14px}.w-\[14rem\]{width:14rem}.w-\[15px\]{width:15px}.w-\[16px\]{width:16px}.w-\[18px\]{width:18px}.w-\[20\%\]{width:20%}.w-\[20px\]{width:20px}.w-\[22px\]{width:22px}.w-\[23px\]{width:23px}.w-\[24px\]{width:24px}.w-\[25vw\]{width:25vw}.w-\[26px\]{width:26px}.w-\[27px\]{width:27px}.w-\[30px\]{width:30px}.w-\[32px\]{width:32px}.w-\[34px\]{width:34px}.w-\[40px\]{width:40px}.w-\[42px\]{width:42px}.w-\[44px\]{width:44px}.w-\[48\%\]{width:48%}.w-\[48px\]{width:48px}.w-\[50\%\]{width:50%}.w-\[50px\]{width:50px}.w-\[50vw\]{width:50vw}.w-\[54px\]{width:54px}.w-\[55\%\]{width:55%}.w-\[60\%\]{width:60%}.w-\[60px\]{width:60px}.w-\[64\%\]{width:64%}.w-\[66\%\]{width:66%}.w-\[70\%\]{width:70%}.w-\[75\%\]{width:75%}.w-\[75px\]{width:75px}.w-\[80\%\]{width:80%}.w-\[88px\]{width:88px}.w-\[90\%\]{width:90%}.w-\[90px\]{width:90px}.w-\[90vw\]{width:90vw}.w-\[100cqw\]{width:100cqw}.w-\[100px\]{width:100px}.w-\[100vw\]{width:100vw}.w-\[104px\]{width:104px}.w-\[105px\]{width:105px}.w-\[120px\]{width:120px}.w-\[130px\]{width:130px}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[200px\]{width:200px}.w-\[210px\]{width:210px}.w-\[222px\]{width:222px}.w-\[230px\]{width:230px}.w-\[232px\]{width:232px}.w-\[240px\]{width:240px}.w-\[250px\]{width:250px}.w-\[272px\]{width:272px}.w-\[280px\]{width:280px}.w-\[290px\]{width:290px}.w-\[294px\]{width:294px}.w-\[300px\]{width:300px}.w-\[304px\]{width:304px}.w-\[328px\]{width:328px}.w-\[350px\]{width:350px}.w-\[378px\]{width:378px}.w-\[400\%\]{width:400%}.w-\[400px\]{width:400px}.w-\[450px\]{width:450px}.w-\[600px\]{width:600px}.w-\[620px\]{width:620px}.w-\[640px\]{width:640px}.w-\[700px\]{width:700px}.w-\[calc\(\(100\%-768px\)\/2\)\]{width:calc(50% - 384px)}.w-\[calc\(100\%-1\.5rem\)\]{width:calc(100% - 1.5rem)}.w-\[calc\(100\%_-_32px\)\]{width:calc(100% - 32px)}.w-\[calc\(100vw-450px\)\]{width:calc(100vw - 450px)}.w-\[fit-content\]{width:fit-content}.w-\[max\(95vw\,300px\)\]{width:max(95vw,300px)}.w-\[min\(400px\,100dvw\)\]{width:min(400px,100dvw)}.w-\[var\(--radix-popper-anchor-width\)\]{width:var(--radix-popper-anchor-width)}.w-\[var\(--sidebar-width\)\]{width:var(--sidebar-width)}.w-\[var\(--user-chat-width\,70\%\)\]{width:var(--user-chat-width,70%)}.w-auto{width:auto}.w-dvw{width:100dvw}.w-fit{width:fit-content}.w-full{width:100%}.w-full\!{width:100%!important}.w-max{width:max-content}.w-min{width:min-content}.w-px{width:1px}.w-screen{width:100vw}.max-w-\(--breakpoint-2xl\){max-width:var(--breakpoint-2xl)}.max-w-\(--breakpoint-md\){max-width:var(--breakpoint-md)}.max-w-\(--sidebar-width\){max-width:var(--sidebar-width)}.max-w-\(--thread-content-max-width\){max-width:var(--thread-content-max-width)}.max-w-1\/2{max-width:50%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-2xs\!{max-width:240px!important}.max-w-3xl{max-width:var(--container-3xl)}.max-w-3xs{max-width:256px}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-16{max-width:calc(var(--spacing)*16)}.max-w-20{max-width:calc(var(--spacing)*20)}.max-w-28{max-width:calc(var(--spacing)*28)}.max-w-32{max-width:calc(var(--spacing)*32)}.max-w-48{max-width:calc(var(--spacing)*48)}.max-w-52{max-width:calc(var(--spacing)*52)}.max-w-60{max-width:calc(var(--spacing)*60)}.max-w-64{max-width:calc(var(--spacing)*64)}.max-w-72{max-width:calc(var(--spacing)*72)}.max-w-80{max-width:calc(var(--spacing)*80)}.max-w-96{max-width:calc(var(--spacing)*96)}.max-w-100{max-width:25rem}.max-w-\[8rem\]{max-width:8rem}.max-w-\[22\%\]{max-width:22%}.max-w-\[48rem\]{max-width:48rem}.max-w-\[60\%\]{max-width:60%}.max-w-\[70\%\]{max-width:70%}.max-w-\[70dvw\]{max-width:70dvw}.max-w-\[75\%\]{max-width:75%}.max-w-\[80\%\]{max-width:80%}.max-w-\[80vw\]\!{max-width:80vw!important}.max-w-\[90\%\]{max-width:90%}.max-w-\[90vw\]{max-width:90vw}.max-w-\[100px\]{max-width:100px}.max-w-\[100vw\]{max-width:100vw}.max-w-\[160px\]{max-width:160px}.max-w-\[200px\]{max-width:200px}.max-w-\[220px\]{max-width:220px}.max-w-\[240px\]{max-width:240px}.max-w-\[270px\]{max-width:270px}.max-w-\[280px\]{max-width:280px}.max-w-\[300px\]{max-width:300px}.max-w-\[320px\]{max-width:320px}.max-w-\[328px\]{max-width:328px}.max-w-\[360px\]{max-width:360px}.max-w-\[373px\]{max-width:373px}.max-w-\[380px\]{max-width:380px}.max-w-\[390px\]{max-width:390px}.max-w-\[400px\]{max-width:400px}.max-w-\[402px\]{max-width:402px}.max-w-\[412px\]{max-width:412px}.max-w-\[416px\]{max-width:416px}.max-w-\[420px\]{max-width:420px}.max-w-\[440px\]{max-width:440px}.max-w-\[448px\]{max-width:448px}.max-w-\[450px\]{max-width:450px}.max-w-\[460px\]{max-width:460px}.max-w-\[480px\]{max-width:480px}.max-w-\[500px\]{max-width:500px}.max-w-\[550px\]{max-width:550px}.max-w-\[552px\]{max-width:552px}.max-w-\[555px\]{max-width:555px}.max-w-\[560px\]{max-width:560px}.max-w-\[596px\]{max-width:596px}.max-w-\[600px\]{max-width:600px}.max-w-\[640px\]{max-width:640px}.max-w-\[664px\]{max-width:664px}.max-w-\[680px\]{max-width:680px}.max-w-\[700px\]{max-width:700px}.max-w-\[720px\]{max-width:720px}.max-w-\[800px\]{max-width:800px}.max-w-\[820px\]{max-width:820px}.max-w-\[850px\]{max-width:850px}.max-w-\[900px\]{max-width:900px}.max-w-\[1000px\]{max-width:1000px}.max-w-\[1024px\]{max-width:1024px}.max-w-\[1200px\]{max-width:1200px}.max-w-\[1300px\]{max-width:1300px}.max-w-\[1800px\]{max-width:1800px}.max-w-\[calc\(0\.8\*var\(--thread-content-max-width\,40rem\)\)\]{max-width:calc(var(--thread-content-max-width,40rem)*.8)}.max-w-\[calc\(2\*var\(--thread-content-max-width\)\)\]{max-width:calc(var(--thread-content-max-width)*2)}.max-w-\[calc\(100vw-1\.5rem\)\]{max-width:calc(100vw - 1.5rem)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-\[var\(--user-chat-width\,70\%\)\]{max-width:var(--user-chat-width,70%)}.max-w-fit{max-width:fit-content}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-screen{max-width:100vw}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-screen-lg{max-width:var(--breakpoint-lg)}.max-w-screen-xl{max-width:var(--breakpoint-xl)}.max-w-screen-xs{max-width:480px}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-\(--thread-content-width\){min-width:var(--thread-content-width)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-2{min-width:calc(var(--spacing)*2)}.min-w-4{min-width:calc(var(--spacing)*4)}.min-w-6{min-width:calc(var(--spacing)*6)}.min-w-7{min-width:calc(var(--spacing)*7)}.min-w-8{min-width:calc(var(--spacing)*8)}.min-w-9{min-width:calc(var(--spacing)*9)}.min-w-10{min-width:calc(var(--spacing)*10)}.min-w-11{min-width:calc(var(--spacing)*11)}.min-w-15{min-width:calc(var(--spacing)*15)}.min-w-20{min-width:calc(var(--spacing)*20)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-32{min-width:calc(var(--spacing)*32)}.min-w-36{min-width:calc(var(--spacing)*36)}.min-w-40{min-width:calc(var(--spacing)*40)}.min-w-48{min-width:calc(var(--spacing)*48)}.min-w-60{min-width:calc(var(--spacing)*60)}.min-w-64{min-width:calc(var(--spacing)*64)}.min-w-72{min-width:calc(var(--spacing)*72)}.min-w-80{min-width:calc(var(--spacing)*80)}.min-w-96{min-width:calc(var(--spacing)*96)}.min-w-\[1ch\]{min-width:1ch}.min-w-\[2em\]{min-width:2em}.min-w-\[7\.5rem\]{min-width:7.5rem}.min-w-\[18px\]{min-width:18px}.min-w-\[25vw\]{min-width:25vw}.min-w-\[32px\]{min-width:32px}.min-w-\[34px\]{min-width:34px}.min-w-\[40\%\]{min-width:40%}.min-w-\[50px\]{min-width:50px}.min-w-\[62px\]{min-width:62px}.min-w-\[80px\]{min-width:80px}.min-w-\[86px\]{min-width:86px}.min-w-\[100px\]{min-width:100px}.min-w-\[160px\]{min-width:160px}.min-w-\[180px\]{min-width:180px}.min-w-\[200px\]{min-width:200px}.min-w-\[220px\]{min-width:220px}.min-w-\[224px\]{min-width:224px}.min-w-\[240px\]{min-width:240px}.min-w-\[320px\]{min-width:320px}.min-w-\[400px\]{min-width:400px}.min-w-\[680px\]{min-width:680px}.min-w-\[calc\(100vw-1\.5rem\)\]{min-width:calc(100vw - 1.5rem)}.min-w-\[min\(90cqw\,640px\)\]{min-width:min(90cqw,640px)}.min-w-\[min\(125px\,95vw\)\]{min-width:min(125px,95vw)}.min-w-\[min\(200px\,95vw\)\]{min-width:min(200px,95vw)}.min-w-\[min\(280px\,95vw\)\]{min-width:min(280px,95vw)}.min-w-\[min\(350px\,95vw\)\]{min-width:min(350px,95vw)}.min-w-\[min\(450px\,80cqw\,80vw\)\]{min-width:min(450px,80cqw,80vw)}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.min-w-bloop{min-width:227px}.min-w-fit{min-width:fit-content}.min-w-full{min-width:100%}.min-w-min{min-width:min-content}.flex-0{flex:0}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.flex-shrink-1,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.flex-grow-1,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-0{flex-basis:calc(var(--spacing)*0)}.basis-5{flex-basis:calc(var(--spacing)*5)}.basis-20{flex-basis:calc(var(--spacing)*20)}.basis-\[32px\]{flex-basis:32px}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:calc(var(--spacing)*0);--tw-border-spacing-y:calc(var(--spacing)*0);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.origin-\[14px_50\%\]{transform-origin:14px}.origin-\[50\%_50\%\]{transform-origin:50%}.origin-bottom{transform-origin:bottom}.origin-center{transform-origin:50%}[dir=ltr] .origin-left{transform-origin:0}[dir=rtl] .origin-left{transform-origin:100%}.origin-radix-popover{transform-origin:var(--radix-popover-content-transform-origin)}[dir=ltr] .origin-top-left{transform-origin:0 0}[dir=ltr] .origin-top-right,[dir=rtl] .origin-top-left{transform-origin:100% 0}[dir=rtl] .origin-top-right{transform-origin:0 0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-2{translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-2{--tw-translate-x:calc(var(--spacing)*-2)}.-translate-x-52{--tw-translate-x:calc(var(--spacing)*-52)}.-translate-x-52,.-translate-x-96{translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-96{--tw-translate-x:calc(var(--spacing)*-96)}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.translate-x-1\/2{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x:50%}.translate-x-2{--tw-translate-x:calc(var(--spacing)*2)}.translate-x-2,.translate-x-52{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-52{--tw-translate-x:calc(var(--spacing)*52)}.translate-x-96{--tw-translate-x:calc(var(--spacing)*96)}.translate-x-96,.translate-x-\[-2\.5rem\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[-2\.5rem\]{--tw-translate-x:-2.5rem}.translate-x-\[-50\%\]{--tw-translate-x:-50%}.translate-x-\[-50\%\],.translate-x-\[2\.5rem\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[2\.5rem\]{--tw-translate-x:2.5rem}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1)}.-translate-y-1,.-translate-y-1\/2{translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-2{--tw-translate-y:calc(var(--spacing)*-2)}.-translate-y-12,.-translate-y-2{translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-12{--tw-translate-y:calc(var(--spacing)*-12)}.-translate-y-full{--tw-translate-y:-100%}.-translate-y-full,.translate-y-0{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing)*0)}.translate-y-0\.5{--tw-translate-y:calc(var(--spacing)*.5)}.translate-y-0\.5,.translate-y-1{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing)*1)}.translate-y-2{--tw-translate-y:calc(var(--spacing)*2)}.translate-y-10,.translate-y-2{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-10{--tw-translate-y:calc(var(--spacing)*10)}.translate-y-\[-100\%\]{--tw-translate-y:-100%}.translate-y-\[-100\%\],.translate-y-\[-100lvh\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[-100lvh\]{--tw-translate-y:-100lvh}.translate-y-\[0px\]{--tw-translate-y:0px}.translate-y-\[0px\],.translate-y-\[1px\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[1px\]{--tw-translate-y:1px}.translate-y-\[2rem\]{--tw-translate-y:2rem}.translate-y-\[10px\],.translate-y-\[2rem\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[10px\]{--tw-translate-y:10px}.translate-y-\[12px\]{--tw-translate-y:12px}.translate-y-\[100\%\],.translate-y-\[12px\]{translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[100\%\]{--tw-translate-y:100%}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%}.scale-0,.scale-90{scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%}.scale-100,.scale-105{scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%}.scale-110,.scale-200{scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-200{--tw-scale-x:200%;--tw-scale-y:200%;--tw-scale-z:200%}.-scale-x-100{--tw-scale-x:-100%}.-scale-x-100,.scale-x-75{scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-75{--tw-scale-x:75%}.scale-\[0\.9\]{scale:.9}.scale-\[0\.95\]{scale:.95}.scale-\[1\.015\]{scale:1.015}.-rotate-90{rotate:-90deg}.-rotate-180{rotate:-180deg}.rotate-0{rotate:none}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.rotate-\[-3deg\]{rotate:-3deg}.rotate-\[-4deg\]{rotate:-4deg}.rotate-\[2deg\]{rotate:2deg}.rotate-\[4deg\]{rotate:4deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-\[hive-log-fadeout_0\.3s_1\.5s_forwards\]{animation:hive-log-fadeout .3s 1.5s forwards}.animate-\[show_150ms_ease-in\]{animation:show .15s ease-in}.animate-bounce{animation:var(--animate-bounce)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-pulsing{animation:pulsing 2s ease-in-out infinite forwards}.animate-show{animation:show .1s cubic-bezier(.16,1,.3,1)}.animate-spin{animation:var(--animate-spin)}.cursor-auto{cursor:auto}.cursor-default{cursor:default}.cursor-default\!{cursor:default!important}.cursor-e-resize{cursor:e-resize}.cursor-ew-resize{cursor:ew-resize}.cursor-grab{cursor:grab}.cursor-none{cursor:none}.cursor-not-allowed{cursor:not-allowed}.cursor-ns-resize{cursor:ns-resize}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.cursor-w-resize{cursor:w-resize}.cursor-wait{cursor:wait}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.touch-pan-y{--tw-pan-y:pan-y;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-center{scroll-snap-align:center}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.scroll-m-5{scroll-margin:calc(var(--spacing)*5)}.scroll-mx-5{scroll-margin-inline:calc(var(--spacing)*5)}.scroll-mt-28{scroll-margin-top:calc(var(--spacing)*28)}.scroll-ps-4{scroll-padding-inline-start:calc(var(--spacing)*4)}.scroll-pt-\[30px\]{scroll-padding-top:30px}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.form-textarea{--tw-shadow:0 0 #0000;appearance:none;background-color:#fff;border-color:#9b9b9b;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}.form-textarea:focus{--tw-ring-inset:var(--tw-empty, );--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#004f99;--tw-ring-offset-shadow:var(--tw-ring-inset)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color);border-color:#004f99;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}.form-textarea::placeholder{color:#9b9b9b;opacity:1}.appearance-none{appearance:none}.columns-1{column-count:1}.break-inside-avoid{break-inside:avoid}.grid-flow-col{grid-auto-flow:column}.grid-flow-row{grid-auto-flow:row}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.grid-cols-\[1fr_auto_1fr\]{grid-template-columns:1fr auto 1fr}.grid-cols-\[10px_1fr_10px\]{grid-template-columns:10px 1fr 10px}.grid-cols-\[50\%_50\%\]{grid-template-columns:50% 50%}.grid-cols-\[180px_1fr_32px\]{grid-template-columns:180px 1fr 32px}.grid-cols-\[200px_1fr_1fr\]{grid-template-columns:200px 1fr 1fr}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.grid-cols-\[auto_1fr_auto\]{grid-template-columns:auto 1fr auto}.grid-cols-\[auto_auto\]{grid-template-columns:auto auto}.grid-cols-\[auto_auto_1fr\]{grid-template-columns:auto auto 1fr}.grid-cols-\[auto_max-content\]{grid-template-columns:auto max-content}.grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.grid-cols-\[repeat\(auto-fit\,minmax\(250px\,1fr\)\)\]{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-\[0fr\]{grid-template-rows:0fr}.grid-rows-\[1fr\]{grid-template-rows:1fr}.grid-rows-\[minmax\(10px\,1fr\)_auto_10px\]{grid-template-rows:minmax(10px,1fr) auto 10px}.grid-rows-\[minmax\(10px\,1fr\)_auto_minmax\(10px\,1fr\)\]{grid-template-rows:minmax(10px,1fr) auto minmax(10px,1fr)}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-center{place-content:center}.place-items-center{place-items:center}.content-center{align-content:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.justify-stretch{justify-content:stretch}.justify-items-center{justify-items:center}.gap-0{gap:calc(var(--spacing)*0)}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-3\.5{gap:calc(var(--spacing)*3.5)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-10{gap:calc(var(--spacing)*10)}.gap-12{gap:calc(var(--spacing)*12)}.gap-\[0\.3em\]{gap:.3em}.gap-\[2px\]{gap:2px}.gap-\[6px\]{gap:6px}.gap-\[10px\]{gap:10px}.gap-\[16px\]{gap:16px}.gap-\[18px\]{gap:18px}.gap-\[min\(10dvw\,_200px\)\]{gap:min(10dvw,200px)}.gap-bar{gap:var(--bar-gap,.25rem)}.gap-snc-1{gap:var(--snc-1)}.gap-snc-results-padding{gap:var(--snc-results-padding)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*0*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*0*var(--tw-space-y-reverse))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*1*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*1*var(--tw-space-y-reverse))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*1.5*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*1.5*var(--tw-space-y-reverse))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*2*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*2*var(--tw-space-y-reverse))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*3*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*3*var(--tw-space-y-reverse))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*4*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*4*var(--tw-space-y-reverse))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*5*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*5*var(--tw-space-y-reverse))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*6*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*6*var(--tw-space-y-reverse))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(var(--spacing)*8*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(var(--spacing)*8*var(--tw-space-y-reverse))}:where(.space-y-\[6px\]>:not(:last-child)){--tw-space-y-reverse:0;margin-block-end:calc(6px*(1 - var(--tw-space-y-reverse)));margin-block-start:calc(6px*var(--tw-space-y-reverse))}:where(.space-y-reverse>:not(:last-child)){--tw-space-y-reverse:1}.gap-x-1{column-gap:calc(var(--spacing)*1)}.gap-x-1\.5{column-gap:calc(var(--spacing)*1.5)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-x-3{column-gap:calc(var(--spacing)*3)}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-x-8{column-gap:calc(var(--spacing)*8)}.gap-x-9{column-gap:calc(var(--spacing)*9)}.gap-x-10{column-gap:calc(var(--spacing)*10)}.gap-x-12{column-gap:calc(var(--spacing)*12)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*1*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*1*var(--tw-space-x-reverse))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*2*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*2*var(--tw-space-x-reverse))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*3*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*3*var(--tw-space-x-reverse))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*4*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*4*var(--tw-space-x-reverse))}:where(.space-x-12>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*12*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*12*var(--tw-space-x-reverse))}:where(.space-x-14>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*14*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*14*var(--tw-space-x-reverse))}:where(.space-x-reverse>:not(:last-child)){--tw-space-x-reverse:1}.gap-y-1{row-gap:calc(var(--spacing)*1)}.gap-y-2{row-gap:calc(var(--spacing)*2)}.gap-y-3{row-gap:calc(var(--spacing)*3)}.gap-y-4{row-gap:calc(var(--spacing)*4)}.gap-y-6{row-gap:calc(var(--spacing)*6)}.gap-y-10{row-gap:calc(var(--spacing)*10)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-end-width:calc(1px*(1 - var(--tw-divide-x-reverse)));border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-style:var(--tw-border-style)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-bottom-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse))}:where(.divide-gray-200>:not(:last-child)){border-color:#e3e3e3}:where(.divide-token-border-default>:not(:last-child)){border-color:var(--border-default)}:where(.divide-token-border-medium>:not(:last-child)){border-color:var(--border-medium)}:where(.divide-token-border-xlight>:not(:last-child)){border-color:var(--border-xlight)}:where(.divide-white\/10>:not(:last-child)){border-color:#ffffff1a}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.justify-self-center{justify-self:center}.justify-self-end{justify-self:flex-end}.justify-self-start{justify-self:flex-start}.justify-self-stretch{justify-self:stretch}.\!truncate{overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-visible{overflow:visible}.overflow-visible\!{overflow:visible!important}.overflow-x-auto{overflow-x:auto}.overflow-x-clip{overflow-x:clip}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-y-auto{overflow-y:auto}.overflow-y-clip{overflow-y:clip}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-scroll\!{overflow-y:scroll!important}.overflow-y-visible{overflow-y:visible}.overscroll-contain{overscroll-behavior:contain}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-4xl{border-radius:var(--radius-4xl)}.rounded-\[0\.25rem\]{border-radius:.25rem}.rounded-\[1px\]{border-radius:1px}.rounded-\[3px\]{border-radius:3px}.rounded-\[4px\]{border-radius:4px}.rounded-\[5px\]{border-radius:5px}.rounded-\[10px\]{border-radius:10px}.rounded-\[14px\]{border-radius:14px}.rounded-\[16px\]{border-radius:16px}.rounded-\[20px\]{border-radius:20px}.rounded-\[22px\]{border-radius:22px}.rounded-\[25px\]{border-radius:25px}.rounded-\[28px\]{border-radius:28px}.rounded-\[30px\]{border-radius:30px}.rounded-\[36px\]{border-radius:36px}.rounded-\[38px\]{border-radius:38px}.rounded-full{border-radius:3.40282e+38px}.rounded-full\!{border-radius:3.40282e+38px!important}.rounded-lg{border-radius:var(--radius-lg)}.rounded-lg\!{border-radius:var(--radius-lg)!important}.rounded-md{border-radius:var(--radius-md)}.rounded-md\!{border-radius:var(--radius-md)!important}.rounded-none{border-radius:0}.rounded-sm{border-radius:var(--radius-sm)}.rounded-sm\!{border-radius:var(--radius-sm)!important}.rounded-xl{border-radius:var(--radius-xl)}.rounded-xl\!{border-radius:var(--radius-xl)!important}.rounded-xs{border-radius:var(--radius-xs)}.rounded-s-full{border-end-start-radius:3.40282e+38px;border-start-start-radius:3.40282e+38px}.rounded-s-none{border-end-start-radius:0;border-start-start-radius:0}.rounded-s-xl{border-end-start-radius:var(--radius-xl);border-start-start-radius:var(--radius-xl)}.rounded-ss-2xl{border-start-start-radius:var(--radius-2xl)}.rounded-e-lg{border-end-end-radius:var(--radius-lg);border-start-end-radius:var(--radius-lg)}.rounded-e-md{border-end-end-radius:var(--radius-md);border-start-end-radius:var(--radius-md)}.rounded-e-none{border-end-end-radius:0;border-start-end-radius:0}.rounded-e-xl{border-end-end-radius:var(--radius-xl);border-start-end-radius:var(--radius-xl)}.rounded-se-2xl{border-start-end-radius:var(--radius-2xl)}.rounded-se-\[1px\]{border-start-end-radius:1px}.rounded-se-full{border-start-end-radius:3.40282e+38px}.rounded-se-lg{border-start-end-radius:var(--radius-lg)}.rounded-ee-\[50\%\]{border-end-end-radius:50%}.rounded-ee-full{border-end-end-radius:3.40282e+38px}.rounded-ee-sm{border-end-end-radius:var(--radius-sm)}.rounded-es-2xl{border-end-start-radius:var(--radius-2xl)}.rounded-es-\[1px\]{border-end-start-radius:1px}.rounded-es-\[50\%\]{border-end-start-radius:50%}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.rounded-t-\[5px\]{border-top-left-radius:5px;border-top-right-radius:5px}.rounded-t-\[20px\]{border-top-left-radius:20px;border-top-right-radius:20px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}[dir=ltr] .rounded-l{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}[dir=ltr] .rounded-r,[dir=rtl] .rounded-l{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}[dir=rtl] .rounded-r{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-b-2xl{border-bottom-left-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl)}.rounded-b-3xl{border-bottom-left-radius:var(--radius-3xl);border-bottom-right-radius:var(--radius-3xl)}.rounded-b-4xl{border-bottom-left-radius:var(--radius-4xl);border-bottom-right-radius:var(--radius-4xl)}.rounded-b-lg{border-bottom-left-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-b-md{border-bottom-left-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-b-none{border-bottom-left-radius:0;border-bottom-right-radius:0}.rounded-b-xl{border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl)}.btn-secondary{background-color:var(--main-surface-primary);border-color:var(--border-medium);border-style:var(--tw-border-style);border-width:1px;color:var(--text-primary);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}@media (hover:hover){.btn-secondary:hover{background-color:var(--main-surface-secondary)}}.btn-secondary:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#676767;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-danger-outline{background-color:var(--main-surface-primary);border-color:#ba2623;border-style:var(--tw-border-style);border-width:1px;color:#ba2623}@media (hover:hover){.btn-danger-outline:hover{background-color:var(--main-surface-secondary)}}.btn-danger-outline:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#ba2623;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.border-thin{border-style:var(--tw-border-style);border-width:1px}@media (min-resolution:1.5x){.border-thin{border-style:var(--tw-border-style);border-width:.5px}}.border{border-width:1px}.border,.border-0{border-style:var(--tw-border-style)}.border-0{border-width:0}.border-0\!{border-style:var(--tw-border-style)!important;border-width:0!important}.border-1{border-width:1px}.border-1,.border-2{border-style:var(--tw-border-style)}.border-2{border-width:2px}.border-4{border-width:4px}.border-4,.border-6{border-style:var(--tw-border-style)}.border-6{border-width:6px}.border-\[0\.5px\],.border-\[\.5px\]{border-style:var(--tw-border-style);border-width:.5px}.border-\[1px\]{border-width:1px}.border-\[1px\],.border-\[3px\]{border-style:var(--tw-border-style)}.border-\[3px\]{border-width:3px}.border-\[4px\]{border-style:var(--tw-border-style);border-width:4px}.border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-s{border-inline-start-width:1px}.border-s,.border-s-0{border-inline-start-style:var(--tw-border-style)}.border-s-0{border-inline-start-width:0}.border-s-0\!{border-inline-start-style:var(--tw-border-style)!important;border-inline-start-width:0!important}.border-s-4{border-inline-start-width:4px}.border-s-4,.border-s-\[0\.5px\]{border-inline-start-style:var(--tw-border-style)}.border-s-\[0\.5px\]{border-inline-start-width:.5px}.border-e{border-inline-end-width:1px}.border-e,.border-e-0{border-inline-end-style:var(--tw-border-style)}.border-e-0{border-inline-end-width:0}.border-e-0\!{border-inline-end-style:var(--tw-border-style)!important;border-inline-end-width:0!important}.border-e-2{border-inline-end-width:2px}.border-e-2,.border-e-\[1px\]{border-inline-end-style:var(--tw-border-style)}.border-e-\[1px\]{border-inline-end-width:1px}.border-t{border-top-width:1px}.border-t,.border-t-0{border-top-style:var(--tw-border-style)}.border-t-0{border-top-width:0}.border-t-0\!{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-t-\[0\.5px\]{border-top-style:var(--tw-border-style);border-top-width:.5px}[dir=ltr] .border-r{border-right-style:var(--tw-border-style);border-right-width:1px}[dir=rtl] .border-r{border-left-style:var(--tw-border-style);border-left-width:1px}.border-b{border-bottom-width:1px}.border-b,.border-b-0{border-bottom-style:var(--tw-border-style)}.border-b-0{border-bottom-width:0}.border-b-1{border-bottom-width:1px}.border-b-1,.border-b-2{border-bottom-style:var(--tw-border-style)}.border-b-2{border-bottom-width:2px}.border-b-\[0\.5px\]{border-bottom-style:var(--tw-border-style);border-bottom-width:.5px}[dir=ltr] .border-l,[dir=ltr] .border-l-\[1px\]{border-left-style:var(--tw-border-style);border-left-width:1px}[dir=rtl] .border-l,[dir=rtl] .border-l-\[1px\]{border-right-style:var(--tw-border-style);border-right-width:1px}.\!border-none{--tw-border-style:none!important;border-style:none!important}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-\[\#AF52DE\]{border-color:#af52de}.border-\[\#B3DBFF\]{border-color:#b3dbff}.border-\[\#EDEDF2\]{border-color:#ededf2}.border-\[\#df1b41\]{border-color:#df1b41}.border-\[\#e6e6e6\]{border-color:#e6e6e6}.border-\[\#f4f4f4\]{border-color:#f4f4f4}.border-\[rgba\(0\,0\,0\,0\.1\)\]{border-color:#0000001a}.border-\[rgba\(0\,0\,0\,0\.18\)\]{border-color:#0000002e}.border-black{border-color:#000}.border-black\/5{border-color:oklab(0 none none/.05)}.border-black\/10{border-color:oklab(0 none none/.1)}.border-black\/25{border-color:oklab(0 none none/.25)}.border-black\/\[0\.12\]{border-color:oklab(0 none none/.12)}.border-blue-100{border-color:#99ceff}.border-blue-400{border-color:#0285ff}.border-blue-400\!{border-color:#0285ff!important}.border-blue-400\/10{border-color:#0285ff1a}.border-blue-400\/\[\.3\]{border-color:#0285ff4d}.border-brand-green-800{border-color:#05a746}.border-brand-purple{border-color:#ab68ff}.border-gray-100{border-color:#ececec}.border-gray-200{border-color:#e3e3e3}.border-gray-300{border-color:#cdcdcd}.border-gray-400{border-color:#b4b4b4}.border-gray-500{border-color:#9b9b9b}.border-gray-600{border-color:#676767}.border-gray-700{border-color:#424242}.border-green-500{border-color:#00a240}.border-green-600{border-color:#008635}.border-orange-400{border-color:#fb6a22}.border-orange-400\/15{border-color:#fb6a2226}.border-orange-500{border-color:#e25507}.border-pink-100{border-color:#ffbada}.border-red-200{border-color:#ff8583}.border-red-400{border-color:#fa423e}.border-red-500{border-color:#e02e2a}.border-red-500\!{border-color:#e02e2a!important}.border-red-600{border-color:#ba2623}.border-red-700{border-color:#911e1b}.border-token-bg-primary{border-color:var(--bg-primary)}.border-token-bg-tertiary{border-color:var(--bg-tertiary)}.border-token-border-default{border-color:var(--border-default)}.border-token-border-default\!{border-color:var(--border-default)!important}.border-token-border-heavy{border-color:var(--border-heavy)}.border-token-border-heavy\!{border-color:var(--border-heavy)!important}.border-token-border-light{border-color:var(--border-light)}.border-token-border-medium{border-color:var(--border-medium)}.border-token-border-sharp{border-color:var(--border-sharp)}.border-token-border-status-warning{border-color:var(--border-status-warning)}.border-token-border-xheavy{border-color:var(--border-xheavy)}.border-token-border-xlight{border-color:var(--border-xlight)}.border-token-border-xlight\!{border-color:var(--border-xlight)!important}.border-token-main-surface-primary{border-color:var(--main-surface-primary)}.border-token-main-surface-secondary{border-color:var(--main-surface-secondary)}.border-token-main-surface-tertiary{border-color:var(--main-surface-tertiary)}.border-token-sidebar-surface-primary{border-color:var(--sidebar-surface-primary)}.border-token-surface-error\/5{border-color:rgb(var(--surface-error)/1)}@supports (color:color-mix(in lab,red,red)){.border-token-surface-error\/5{border-color:color-mix(in oklab,rgb(var(--surface-error)/1) 5%,transparent)}}.border-token-surface-error\/15{border-color:rgb(var(--surface-error)/1)}@supports (color:color-mix(in lab,red,red)){.border-token-surface-error\/15{border-color:color-mix(in oklab,rgb(var(--surface-error)/1) 15%,transparent)}}.border-token-text-error{border-color:var(--text-error)}.border-token-text-primary{border-color:var(--text-primary)}.border-token-text-primary\!{border-color:var(--text-primary)!important}.border-token-text-secondary{border-color:var(--text-secondary)}.border-token-text-tertiary{border-color:var(--text-tertiary)}.border-transparent{border-color:#0000}.border-white{border-color:#fff}.border-white\/10{border-color:#ffffff1a}.border-y-token-border-heavy{border-block-color:var(--border-heavy)}.border-s-token-border-sharp{border-inline-start-color:var(--border-sharp)}.border-s-token-sidebar-surface-secondary{border-inline-start-color:var(--sidebar-surface-secondary)}.border-t-token-border-xlight{border-top-color:var(--border-xlight)}.border-t-transparent{border-top-color:#0000}.border-b-black{border-bottom-color:#000}.border-b-token-bg-secondary{border-bottom-color:var(--bg-secondary)}.border-b-token-border-default{border-bottom-color:var(--border-default)}.border-b-transparent{border-bottom-color:#0000}.btn-primary{background-color:#0d0d0d;color:#fff}@media (hover:hover){.btn-primary:hover{background-color:#212121}}.btn-primary:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#9b9b9b;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-primary:is(.dark *){background-color:#f9f9f9;color:#0d0d0d}@media (hover:hover){.btn-primary:is(.dark *):hover{background-color:#ececec}}.btn-primary-inverse{background-color:#f9f9f9;color:#0d0d0d}@media (hover:hover){.btn-primary-inverse:hover{background-color:#ececec}}.btn-primary-inverse:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#9b9b9b;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-primary-inverse:is(.dark *){background-color:#0d0d0d;color:#fff}@media (hover:hover){.btn-primary-inverse:is(.dark *):hover{background-color:#212121}}.btn-danger{background-color:#e02e2a;color:#fff}@media (hover:hover){.btn-danger:hover{background-color:#911e1b}}.btn-danger:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#fa423e;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){.btn-danger:disabled:hover{background-color:#911e1b}}.btn-blue{background-color:#0066de;color:#fff}@media (hover:hover){.btn-blue:hover{background-color:#003f7a}}.btn-blue:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#003f7a;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-green{background-color:#008635;color:#fff}@media (hover:hover){.btn-green:hover{background-color:#00692a}}.btn-green:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:#00a240;--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){.btn-ghost:hover{background-color:oklab(0 none none/.05)}}.btn-ghost:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);--tw-ring-color:oklab(0% none none/.05);--tw-outline-style:none;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline-style:none}@media (forced-colors:active){.btn-ghost:focus-visible{outline:2px solid #0000;outline-offset:2px}}@media (hover:hover){.btn-ghost:is(.dark *):hover{background-color:#ffffff1a}}.\!bg-token-bg-tertiary{background-color:var(--bg-tertiary)!important}.\!bg-token-interactive-bg-accent-default{background-color:var(--interactive-bg-accent-default)!important}.bg-\[\#1D53BF0D\]{background-color:#1d53bf0d}.bg-\[\#007AFF\]{background-color:#007aff}.bg-\[\#8C43A00D\]{background-color:#8c43a00d}.bg-\[\#8E3CF320\]{background-color:#8e3cf320}.bg-\[\#10A37F\]{background-color:#10a37f}.bg-\[\#0088FF\]{background-color:#08f}.bg-\[\#129FBF\]{background-color:#129fbf}.bg-\[\#59636E20\]{background-color:#59636e20}.bg-\[\#252525\]{background-color:#252525}.bg-\[\#303030\]{background-color:#303030}.bg-\[\#AF52DE\]{background-color:#af52de}.bg-\[\#B161FD\]{background-color:#b161fd}.bg-\[\#C3DEC780\]{background-color:#c3dec780}.bg-\[\#CEDFFE\]{background-color:#cedffe}.bg-\[\#D6303D20\]{background-color:#d6303d20}.bg-\[\#DAEEFF\]{background-color:#daeeff}.bg-\[\#E0FFE7\]{background-color:#e0ffe7}.bg-\[\#E5F3FF\]{background-color:#e5f3ff}.bg-\[\#F1F1F1\]{background-color:#f1f1f1}.bg-\[\#F4F4F4\]\!{background-color:#f4f4f4!important}.bg-\[\#F7F7F7\]{background-color:#f7f7f7}.bg-\[\#F8CA27\]{background-color:#f8ca27}.bg-\[\#F9F9F9\]{background-color:#f9f9f9}.bg-\[\#FCECC1\]{background-color:#fcecc1}.bg-\[\#FF6E3C\]{background-color:#ff6e3c}.bg-\[\#FF5588\]{background-color:#f58}.bg-\[\#e2c541\]{background-color:#e2c541}.bg-\[\#f4f4f4\]{background-color:#f4f4f4}.bg-\[\#fcf6e0\]{background-color:#fcf6e0}.bg-\[Highlight\]{background-color:highlight}.bg-\[Highlight\]\!{background-color:highlight!important}.bg-\[rgb\(16\,163\,117\)\]{background-color:#10a375}.bg-\[rgb\(247\,247\,247\)\]{background-color:#f7f7f7}.bg-\[rgb\(250\,235\,234\)\]\!{background-color:#faebea!important}.bg-\[rgba\(29\,155\,209\,0\.1\)\]{background-color:#1d9bd11a}.bg-\[rgba\(229\,76\,66\,0\.16\)\]{background-color:#e54c4229}.bg-\[rgba\(249\,249\,249\,1\)\]{background-color:#f9f9f9}.bg-\[var\(--right-bg\)\]{background-color:var(--right-bg)}.bg-black{background-color:#000}.bg-black\!{background-color:#000!important}.bg-black\/5{background-color:oklab(0 none none/.05)}.bg-black\/5\!{background-color:oklab(0 none none/.05)!important}.bg-black\/10{background-color:oklab(0 none none/.1)}.bg-black\/20{background-color:oklab(0 none none/.2)}.bg-black\/25{background-color:oklab(0 none none/.25)}.bg-black\/40{background-color:oklab(0 none none/.4)}.bg-black\/50{background-color:oklab(0 none none/.5)}.bg-black\/90{background-color:oklab(0 none none/.9)}.bg-black\/95{background-color:oklab(0 none none/.95)}.bg-black\/\[0\.025\]{background-color:oklab(0 none none/.025)}.bg-black\/\[0\.045\]{background-color:oklab(0 none none/.045)}.bg-blue-25{background-color:#f5faff}.bg-blue-50{background-color:#e5f3ff}.bg-blue-75{background-color:#cce6ff}.bg-blue-100{background-color:#99ceff}.bg-blue-200{background-color:#66b5ff}.bg-blue-300{background-color:#339cff}.bg-blue-400{background-color:#0285ff}.bg-blue-400\/10{background-color:#0285ff1a}.bg-blue-400\/10\!{background-color:#0285ff1a!important}.bg-blue-400\/15{background-color:#0285ff26}.bg-blue-400\/50{background-color:#0285ff80}.bg-blue-400\/\[\.08\]{background-color:#0285ff14}.bg-blue-400\/\[0\.1\]{background-color:#0285ff1a}.bg-blue-500{background-color:#0169cc}.bg-blue-500\/10{background-color:#0169cc1a}.bg-blue-500\/30{background-color:#0169cc4d}.bg-blue-600{background-color:#004f99}.bg-blue-700{background-color:#003f7a}.bg-blue-800{background-color:#013566}.bg-blue-900{background-color:#00284d}.bg-blue-1000{background-color:#000d19}.bg-brand-blue-800{background-color:#0066de}.bg-brand-blue-800\/20{background-color:#0066de33}.bg-brand-green{background-color:#19c37d}.bg-brand-purple{background-color:#ab68ff}.bg-brand-purple-600{background-color:#715fde}.bg-brand-purple-800{background-color:#5400de}.bg-current{background-color:currentColor}.bg-gray-50{background-color:#f9f9f9}.bg-gray-50\/50{background-color:#f9f9f980}.bg-gray-50\/75{background-color:#f9f9f9bf}.bg-gray-100{background-color:#ececec}.bg-gray-100\/50{background-color:#ececec80}.bg-gray-200{background-color:#e3e3e3}.bg-gray-200\!{background-color:#e3e3e3!important}.bg-gray-200\/70{background-color:#e3e3e3b3}.bg-gray-300{background-color:#cdcdcd}.bg-gray-300\!{background-color:#cdcdcd!important}.bg-gray-300\/60{background-color:#cdcdcd99}.bg-gray-400{background-color:#b4b4b4}.bg-gray-500{background-color:#9b9b9b}.bg-gray-600{background-color:#676767}.bg-gray-700{background-color:#424242}.bg-gray-800{background-color:#212121}.bg-gray-900{background-color:#171717}.bg-gray-900\/20{background-color:#17171733}.bg-gray-950{background-color:#0d0d0d}.bg-gray-950\/5{background-color:#0d0d0d0d}.bg-gray-solid-0{background-color:#fff}.bg-gray-solid-50{background-color:#f9f9f9}.bg-gray-solid-75{background-color:#f3f3f3}.bg-gray-solid-100{background-color:#e8e8e8}.bg-gray-solid-200{background-color:#cdcdcd}.bg-gray-solid-300{background-color:#afafaf}.bg-gray-solid-400{background-color:#8f8f8f}.bg-gray-solid-500{background-color:#5d5d5d}.bg-gray-solid-600{background-color:#414141}.bg-gray-solid-700{background-color:#303030}.bg-gray-solid-800{background-color:#212121}.bg-gray-solid-900{background-color:#181818}.bg-gray-solid-1000{background-color:#0d0d0d}.bg-green-25{background-color:#edfaf2}.bg-green-50{background-color:#d9f4e4}.bg-green-75{background-color:#b8ebcc}.bg-green-100{background-color:#8cdfad}.bg-green-100\!{background-color:#8cdfad!important}.bg-green-200{background-color:#66d492}.bg-green-300{background-color:#40c977}.bg-green-400{background-color:#04b84c}.bg-green-400\!{background-color:#04b84c!important}.bg-green-500{background-color:#00a240}.bg-green-500\/10{background-color:#00a2401a}.bg-green-500\/20{background-color:#00a24033}.bg-green-500\/30{background-color:#00a2404d}.bg-green-600{background-color:#008635}.bg-green-600\/5{background-color:#0086350d}.bg-green-600\/10{background-color:#0086351a}.bg-green-600\/15{background-color:#00863526}.bg-green-700{background-color:#00692a}.bg-green-800{background-color:#004f1f}.bg-green-900{background-color:#003716}.bg-green-1000{background-color:#001207}.bg-orange-25{background-color:#fff5f0}.bg-orange-50{background-color:#ffe7d9}.bg-orange-75{background-color:#ffcfb4}.bg-orange-100{background-color:#ffb790}.bg-orange-200{background-color:#ff9e6c}.bg-orange-300{background-color:#ff8549}.bg-orange-400{background-color:#fb6a22}.bg-orange-400\/5{background-color:#fb6a220d}.bg-orange-500{background-color:#e25507}.bg-orange-600{background-color:#b9480d}.bg-orange-700{background-color:#923b0f}.bg-orange-800{background-color:#6d2e0f}.bg-orange-900{background-color:#4a2206}.bg-orange-1000{background-color:#211107}.bg-pink-25{background-color:#fff4f9}.bg-pink-50{background-color:#ffe8f3}.bg-pink-75{background-color:#ffd4e8}.bg-pink-100{background-color:#ffbada}.bg-pink-200{background-color:#ffa3ce}.bg-pink-300{background-color:#ff8cc1}.bg-pink-400{background-color:#ff66ad}.bg-pink-500{background-color:#e04c91}.bg-pink-600{background-color:#ba437a}.bg-pink-700{background-color:#963c67}.bg-pink-800{background-color:#6e2c4a}.bg-pink-900{background-color:#4d1f34}.bg-pink-1000{background-color:#1a0a11}.bg-purple-25{background-color:#f9f5fe}.bg-purple-50{background-color:#efe5fe}.bg-purple-75{background-color:#e0cefd}.bg-purple-100{background-color:#ceb0fb}.bg-purple-200{background-color:#be95fa}.bg-purple-300{background-color:#ad7bf9}.bg-purple-400{background-color:#924ff7}.bg-purple-500{background-color:#8046d9}.bg-purple-600{background-color:#6b3ab4}.bg-purple-700{background-color:#532d8d}.bg-purple-800{background-color:#3f226a}.bg-purple-900{background-color:#2c184a}.bg-purple-1000{background-color:#100a19}.bg-red-25{background-color:#fff0f0}.bg-red-50{background-color:#ffe1e0}.bg-red-75{background-color:#ffc6c5}.bg-red-100{background-color:#ffa4a2}.bg-red-100\!{background-color:#ffa4a2!important}.bg-red-200{background-color:#ff8583}.bg-red-300{background-color:#ff6764}.bg-red-400{background-color:#fa423e}.bg-red-400\!{background-color:#fa423e!important}.bg-red-500{background-color:#e02e2a}.bg-red-500\/10{background-color:#e02e2a1a}.bg-red-500\/20{background-color:#e02e2a33}.bg-red-500\/30{background-color:#e02e2a4d}.bg-red-600{background-color:#ba2623}.bg-red-700{background-color:#911e1b}.bg-red-800{background-color:#6e1615}.bg-red-900{background-color:#4d100e}.bg-red-1000{background-color:#1f0909}.bg-token-bg-elevated-primary{background-color:var(--bg-elevated-primary)}.bg-token-bg-elevated-secondary{background-color:var(--bg-elevated-secondary)}.bg-token-bg-primary{background-color:var(--bg-primary)}.bg-token-bg-primary\!{background-color:var(--bg-primary)!important}.bg-token-bg-scrim{background-color:var(--bg-scrim)}.bg-token-bg-secondary{background-color:var(--bg-secondary)}.bg-token-bg-status-error{background-color:var(--bg-status-error)}.bg-token-bg-status-warning{background-color:var(--bg-status-warning)}.bg-token-bg-tertiary{background-color:var(--bg-tertiary)}.bg-token-border-default{background-color:var(--border-default)}.bg-token-border-heavy{background-color:var(--border-heavy)}.bg-token-border-light{background-color:var(--border-light)}.bg-token-border-medium{background-color:var(--border-medium)}.bg-token-border-status-error{background-color:var(--border-status-error)}.bg-token-border-status-warning{background-color:var(--border-status-warning)}.bg-token-border-xlight{background-color:var(--border-xlight)}.bg-token-composer-blue-bg{background-color:var(--composer-blue-bg)}.bg-token-composer-surface{background-color:var(--composer-surface)}.bg-token-hint-bg{background-color:var(--hint-bg)}.bg-token-icon-accent{background-color:var(--icon-accent)}.bg-token-icon-inverted{background-color:var(--icon-inverted)}.bg-token-icon-inverted-static{background-color:var(--icon-inverted-static)}.bg-token-icon-primary{background-color:var(--icon-primary)}.bg-token-icon-secondary{background-color:var(--icon-secondary)}.bg-token-icon-status-error{background-color:var(--icon-status-error)}.bg-token-icon-status-warning{background-color:var(--icon-status-warning)}.bg-token-icon-tertiary{background-color:var(--icon-tertiary)}.bg-token-interactive-bg-accent-default{background-color:var(--interactive-bg-accent-default)}.bg-token-interactive-bg-accent-hover{background-color:var(--interactive-bg-accent-hover)}.bg-token-interactive-bg-accent-inactive{background-color:var(--interactive-bg-accent-inactive)}.bg-token-interactive-bg-accent-muted-hover{background-color:var(--interactive-bg-accent-muted-hover)}.bg-token-interactive-bg-accent-muted-press{background-color:var(--interactive-bg-accent-muted-press)}.bg-token-interactive-bg-accent-press{background-color:var(--interactive-bg-accent-press)}.bg-token-interactive-bg-danger-primary-default{background-color:var(--interactive-bg-danger-primary-default)}.bg-token-interactive-bg-danger-primary-hover{background-color:var(--interactive-bg-danger-primary-hover)}.bg-token-interactive-bg-danger-primary-inactive{background-color:var(--interactive-bg-danger-primary-inactive)}.bg-token-interactive-bg-danger-primary-press{background-color:var(--interactive-bg-danger-primary-press)}.bg-token-interactive-bg-danger-secondary-default{background-color:var(--interactive-bg-danger-secondary-default)}.bg-token-interactive-bg-danger-secondary-hover{background-color:var(--interactive-bg-danger-secondary-hover)}.bg-token-interactive-bg-danger-secondary-inactive{background-color:var(--interactive-bg-danger-secondary-inactive)}.bg-token-interactive-bg-danger-secondary-press{background-color:var(--interactive-bg-danger-secondary-press)}.bg-token-interactive-bg-primary-default{background-color:var(--interactive-bg-primary-default)}.bg-token-interactive-bg-primary-hover{background-color:var(--interactive-bg-primary-hover)}.bg-token-interactive-bg-primary-inactive{background-color:var(--interactive-bg-primary-inactive)}.bg-token-interactive-bg-primary-press{background-color:var(--interactive-bg-primary-press)}.bg-token-interactive-bg-primary-selected{background-color:var(--interactive-bg-primary-selected)}.bg-token-interactive-bg-secondary-default{background-color:var(--interactive-bg-secondary-default)}.bg-token-interactive-bg-secondary-hover{background-color:var(--interactive-bg-secondary-hover)}.bg-token-interactive-bg-secondary-inactive{background-color:var(--interactive-bg-secondary-inactive)}.bg-token-interactive-bg-secondary-press{background-color:var(--interactive-bg-secondary-press)}.bg-token-interactive-bg-secondary-selected{background-color:var(--interactive-bg-secondary-selected)}.bg-token-interactive-bg-tertiary-default{background-color:var(--interactive-bg-tertiary-default)}.bg-token-interactive-bg-tertiary-hover{background-color:var(--interactive-bg-tertiary-hover)}.bg-token-interactive-bg-tertiary-inactive{background-color:var(--interactive-bg-tertiary-inactive)}.bg-token-interactive-bg-tertiary-press{background-color:var(--interactive-bg-tertiary-press)}.bg-token-interactive-bg-tertiary-selected{background-color:var(--interactive-bg-tertiary-selected)}.bg-token-interactive-border-danger-secondary-default{background-color:var(--interactive-border-danger-secondary-default)}.bg-token-interactive-border-danger-secondary-hover{background-color:var(--interactive-border-danger-secondary-hover)}.bg-token-interactive-border-danger-secondary-inactive{background-color:var(--interactive-border-danger-secondary-inactive)}.bg-token-interactive-border-danger-secondary-press{background-color:var(--interactive-border-danger-secondary-press)}.bg-token-interactive-border-focus{background-color:var(--interactive-border-focus)}.bg-token-interactive-border-secondary-default{background-color:var(--interactive-border-secondary-default)}.bg-token-interactive-border-secondary-hover{background-color:var(--interactive-border-secondary-hover)}.bg-token-interactive-border-secondary-inactive{background-color:var(--interactive-border-secondary-inactive)}.bg-token-interactive-border-secondary-press{background-color:var(--interactive-border-secondary-press)}.bg-token-interactive-border-tertiary-default{background-color:var(--interactive-border-tertiary-default)}.bg-token-interactive-border-tertiary-hover{background-color:var(--interactive-border-tertiary-hover)}.bg-token-interactive-border-tertiary-inactive{background-color:var(--interactive-border-tertiary-inactive)}.bg-token-interactive-border-tertiary-press{background-color:var(--interactive-border-tertiary-press)}.bg-token-interactive-icon-accent-default{background-color:var(--interactive-icon-accent-default)}.bg-token-interactive-icon-accent-hover{background-color:var(--interactive-icon-accent-hover)}.bg-token-interactive-icon-accent-inactive{background-color:var(--interactive-icon-accent-inactive)}.bg-token-interactive-icon-accent-press{background-color:var(--interactive-icon-accent-press)}.bg-token-interactive-icon-accent-selected{background-color:var(--interactive-icon-accent-selected)}.bg-token-interactive-icon-danger-primary-default{background-color:var(--interactive-icon-danger-primary-default)}.bg-token-interactive-icon-danger-primary-hover{background-color:var(--interactive-icon-danger-primary-hover)}.bg-token-interactive-icon-danger-primary-inactive{background-color:var(--interactive-icon-danger-primary-inactive)}.bg-token-interactive-icon-danger-primary-press{background-color:var(--interactive-icon-danger-primary-press)}.bg-token-interactive-icon-danger-secondary-default{background-color:var(--interactive-icon-danger-secondary-default)}.bg-token-interactive-icon-danger-secondary-hover{background-color:var(--interactive-icon-danger-secondary-hover)}.bg-token-interactive-icon-danger-secondary-inactive{background-color:var(--interactive-icon-danger-secondary-inactive)}.bg-token-interactive-icon-danger-secondary-press{background-color:var(--interactive-icon-danger-secondary-press)}.bg-token-interactive-icon-primary-default{background-color:var(--interactive-icon-primary-default)}.bg-token-interactive-icon-primary-hover{background-color:var(--interactive-icon-primary-hover)}.bg-token-interactive-icon-primary-inactive{background-color:var(--interactive-icon-primary-inactive)}.bg-token-interactive-icon-primary-press{background-color:var(--interactive-icon-primary-press)}.bg-token-interactive-icon-primary-selected{background-color:var(--interactive-icon-primary-selected)}.bg-token-interactive-icon-secondary-default{background-color:var(--interactive-icon-secondary-default)}.bg-token-interactive-icon-secondary-hover{background-color:var(--interactive-icon-secondary-hover)}.bg-token-interactive-icon-secondary-inactive{background-color:var(--interactive-icon-secondary-inactive)}.bg-token-interactive-icon-secondary-press{background-color:var(--interactive-icon-secondary-press)}.bg-token-interactive-icon-secondary-selected{background-color:var(--interactive-icon-secondary-selected)}.bg-token-interactive-icon-tertiary-default{background-color:var(--interactive-icon-tertiary-default)}.bg-token-interactive-icon-tertiary-hover{background-color:var(--interactive-icon-tertiary-hover)}.bg-token-interactive-icon-tertiary-inactive{background-color:var(--interactive-icon-tertiary-inactive)}.bg-token-interactive-icon-tertiary-press{background-color:var(--interactive-icon-tertiary-press)}.bg-token-interactive-icon-tertiary-selected{background-color:var(--interactive-icon-tertiary-selected)}.bg-token-interactive-label-accent-default{background-color:var(--interactive-label-accent-default)}.bg-token-interactive-label-accent-hover{background-color:var(--interactive-label-accent-hover)}.bg-token-interactive-label-accent-inactive{background-color:var(--interactive-label-accent-inactive)}.bg-token-interactive-label-accent-press{background-color:var(--interactive-label-accent-press)}.bg-token-interactive-label-accent-selected{background-color:var(--interactive-label-accent-selected)}.bg-token-interactive-label-danger-primary-default{background-color:var(--interactive-label-danger-primary-default)}.bg-token-interactive-label-danger-primary-hover{background-color:var(--interactive-label-danger-primary-hover)}.bg-token-interactive-label-danger-primary-inactive{background-color:var(--interactive-label-danger-primary-inactive)}.bg-token-interactive-label-danger-primary-press{background-color:var(--interactive-label-danger-primary-press)}.bg-token-interactive-label-danger-secondary-default{background-color:var(--interactive-label-danger-secondary-default)}.bg-token-interactive-label-danger-secondary-hover{background-color:var(--interactive-label-danger-secondary-hover)}.bg-token-interactive-label-danger-secondary-inactive{background-color:var(--interactive-label-danger-secondary-inactive)}.bg-token-interactive-label-danger-secondary-press{background-color:var(--interactive-label-danger-secondary-press)}.bg-token-interactive-label-primary-default{background-color:var(--interactive-label-primary-default)}.bg-token-interactive-label-primary-hover{background-color:var(--interactive-label-primary-hover)}.bg-token-interactive-label-primary-inactive{background-color:var(--interactive-label-primary-inactive)}.bg-token-interactive-label-primary-press{background-color:var(--interactive-label-primary-press)}.bg-token-interactive-label-primary-selected{background-color:var(--interactive-label-primary-selected)}.bg-token-interactive-label-secondary-default{background-color:var(--interactive-label-secondary-default)}.bg-token-interactive-label-secondary-hover{background-color:var(--interactive-label-secondary-hover)}.bg-token-interactive-label-secondary-inactive{background-color:var(--interactive-label-secondary-inactive)}.bg-token-interactive-label-secondary-press{background-color:var(--interactive-label-secondary-press)}.bg-token-interactive-label-secondary-selected{background-color:var(--interactive-label-secondary-selected)}.bg-token-interactive-label-tertiary-default{background-color:var(--interactive-label-tertiary-default)}.bg-token-interactive-label-tertiary-hover{background-color:var(--interactive-label-tertiary-hover)}.bg-token-interactive-label-tertiary-inactive{background-color:var(--interactive-label-tertiary-inactive)}.bg-token-interactive-label-tertiary-press{background-color:var(--interactive-label-tertiary-press)}.bg-token-interactive-label-tertiary-selected{background-color:var(--interactive-label-tertiary-selected)}.bg-token-main-surface-primary{background-color:var(--main-surface-primary)}.bg-token-main-surface-primary\!{background-color:var(--main-surface-primary)!important}.bg-token-main-surface-primary-inverse{background-color:var(--main-surface-primary-inverse)}.bg-token-main-surface-primary\/10{background-color:var(--main-surface-primary)}@supports (color:color-mix(in lab,red,red)){.bg-token-main-surface-primary\/10{background-color:color-mix(in oklab,var(--main-surface-primary)10%,transparent)}}.bg-token-main-surface-secondary{background-color:var(--main-surface-secondary)}.bg-token-main-surface-secondary\!{background-color:var(--main-surface-secondary)!important}.bg-token-main-surface-secondary-selected{background-color:var(--main-surface-secondary-selected)}.bg-token-main-surface-tertiary{background-color:var(--main-surface-tertiary)}.bg-token-main-surface-tertiary\!{background-color:var(--main-surface-tertiary)!important}.bg-token-message-surface{background-color:var(--message-surface)}.bg-token-sidebar-surface{background-color:var(--sidebar-surface)}.bg-token-sidebar-surface-primary{background-color:var(--sidebar-surface-primary)}.bg-token-sidebar-surface-secondary{background-color:var(--sidebar-surface-secondary)}.bg-token-sidebar-surface-tertiary{background-color:var(--sidebar-surface-tertiary)}.bg-token-surface-error,.bg-token-surface-error\/5{background-color:rgb(var(--surface-error)/1)}@supports (color:color-mix(in lab,red,red)){.bg-token-surface-error\/5{background-color:color-mix(in oklab,rgb(var(--surface-error)/1) 5%,transparent)}}.bg-token-text-accent{background-color:var(--text-accent)}.bg-token-text-inverted{background-color:var(--text-inverted)}.bg-token-text-inverted-static{background-color:var(--text-inverted-static)}.bg-token-text-primary{background-color:var(--text-primary)}.bg-token-text-primary\!{background-color:var(--text-primary)!important}.bg-token-text-quaternary{background-color:var(--text-quaternary)}.bg-token-text-secondary{background-color:var(--text-secondary)}.bg-token-text-status-error{background-color:var(--text-status-error)}.bg-token-text-status-warning{background-color:var(--text-status-warning)}.bg-token-text-tertiary{background-color:var(--text-tertiary)}.bg-token-utility-scrollbar{background-color:var(--utility-scrollbar)}.bg-transparent{background-color:#0000}.bg-transparent\!{background-color:#0000!important}.bg-white{background-color:#fff}.bg-white\!{background-color:#fff!important}.bg-white\/10{background-color:#ffffff1a}.bg-white\/20{background-color:#fff3}.bg-white\/25{background-color:#ffffff40}.bg-white\/30{background-color:#ffffff4d}.bg-white\/40{background-color:#fff6}.bg-white\/50{background-color:#ffffff80}.bg-white\/60{background-color:#fff9}.bg-white\/70{background-color:#ffffffb3}.bg-white\/80{background-color:#fffc}.bg-white\/95{background-color:#fffffff2}.bg-yellow-25{background-color:#fffbed}.bg-yellow-50{background-color:#fff6d9}.bg-yellow-75{background-color:#ffeeb8}.bg-yellow-100{background-color:#ffe48c}.bg-yellow-200{background-color:#ffdb66}.bg-yellow-300{background-color:#ffd240}.bg-yellow-400{background-color:#ffc300}.bg-yellow-400\/40{background-color:#ffc30066}.bg-yellow-400\/60{background-color:#ffc30099}.bg-yellow-500{background-color:#e0ac00}.bg-yellow-600{background-color:#ba8e00}.bg-yellow-700{background-color:#916f00}.bg-yellow-800{background-color:#6e5400}.bg-yellow-900{background-color:#4d3b00}.bg-yellow-1000{background-color:#1a1400}.bg-linear-to-b{--tw-gradient-position:to bottom;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-b{--tw-gradient-position:to bottom in oklab}}.bg-linear-to-br{--tw-gradient-position:to bottom right;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-l{--tw-gradient-position:to left;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-l{--tw-gradient-position:to left in oklab}}.bg-linear-to-r{--tw-gradient-position:to right;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-t{--tw-gradient-position:to top;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-t{--tw-gradient-position:to top in oklab}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab}.bg-gradient-to-b,.bg-gradient-to-t{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab}.bg-radial{--tw-gradient-position:in oklab;background-image:radial-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.bg-vert-light-gradient{background-image:linear-gradient(#fff0 13.94%,#fff 54.73%)}.from-\[var\(--main-surface-background\)\]{--tw-gradient-from:var(--main-surface-background);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/10{--tw-gradient-from:oklab(0% none none/.1);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/35{--tw-gradient-from:oklab(0% none none/.35);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-400{--tw-gradient-from:#924ff7;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-token-bg-primary{--tw-gradient-from:var(--bg-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-token-main-surface-primary{--tw-gradient-from:var(--main-surface-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-token-main-surface-secondary{--tw-gradient-from:var(--main-surface-secondary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-token-text-tertiary{--tw-gradient-from:var(--text-tertiary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:#fff;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white\/0{--tw-gradient-from:oklab(0% 0 0/0);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-10\%{--tw-gradient-from-position:10%}.from-50\%{--tw-gradient-from-position:50%}.from-60\%{--tw-gradient-from-position:60%}.via-\[rgba\(255\,255\,255\,0\.8\)\]{--tw-gradient-via:#fffc;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-black\/20{--tw-gradient-via:oklab(0% none none/.2);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-black\/30{--tw-gradient-via:oklab(0% none none/.3);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-pink-500{--tw-gradient-via:#e04c91;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-30\%{--tw-gradient-via-position:30%}.to-black\/30{--tw-gradient-to:oklab(0% none none/.3);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-black\/80{--tw-gradient-to:oklab(0% none none/.8);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-500{--tw-gradient-to:#e02e2a;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-white\/20{--tw-gradient-to:oklab(100% 0 5.96046e-8/.2);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-100\%{--tw-gradient-to-position:100%}.\[mask-image\:linear-gradient\(to_right\,black_33\%\,transparent_66\%\)\]{-webkit-mask-image:linear-gradient(90deg,#000 33%,#0000 66%);mask-image:linear-gradient(90deg,#000 33%,#0000 66%)}.bg-auto{background-size:auto}.bg-contain{background-size:contain}.bg-cover{background-size:cover}.bg-clip-padding{background-clip:padding-box}.bg-center{background-position:50%}.bg-no-repeat{background-repeat:no-repeat}.bg-repeat{background-repeat:repeat}.\[mask-size\:300\%_100\%\]{-webkit-mask-size:300% 100%;mask-size:300% 100%}.\[mask-position\:100\%_0\%\]{-webkit-mask-position:100% 0;mask-position:100% 0}.fill-current{fill:currentColor}.fill-token-main-surface-primary{fill:var(--main-surface-primary)}.fill-transparent{fill:#0000}.fill-yellow-500{fill:#e0ac00}.stroke-\[rgba\(0\,0\,0\,0\.1\)\]{stroke:#0000001a}.stroke-\[rgba\(0\,0\,0\,0\.32\)\]{stroke:#00000052}.stroke-black{stroke:#000}.stroke-black\/10{stroke:oklab(0 none none/.1)}.stroke-blue-200{stroke:#66b5ff}.stroke-brand-purple\/25{stroke:#ab68ff40}.stroke-gray-300{stroke:#cdcdcd}.stroke-gray-400{stroke:#b4b4b4}.stroke-token-main-surface-tertiary{stroke:var(--main-surface-tertiary)}.stroke-token-text-tertiary{stroke:var(--text-tertiary)}.stroke-white{stroke:#fff}.stroke-0{stroke-width:0}.stroke-2{stroke-width:2px}.stroke-3{stroke-width:3px}.stroke-4{stroke-width:4px}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-scale-down{object-fit:scale-down}.object-bottom{object-position:bottom}.object-center{object-position:center}.object-top{object-position:top}.\!p-2{padding:calc(var(--spacing)*2)!important}.p-0{padding:calc(var(--spacing)*0)}.p-0\!{padding:calc(var(--spacing)*0)!important}.p-0\.5{padding:calc(var(--spacing)*.5)}.p-1{padding:calc(var(--spacing)*1)}.p-1\!{padding:calc(var(--spacing)*1)!important}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-2\.5{padding:calc(var(--spacing)*2.5)}.p-3{padding:calc(var(--spacing)*3)}.p-3\!{padding:calc(var(--spacing)*3)!important}.p-3\.5{padding:calc(var(--spacing)*3.5)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-7{padding:calc(var(--spacing)*7)}.p-8{padding:calc(var(--spacing)*8)}.p-9{padding:calc(var(--spacing)*9)}.p-10{padding:calc(var(--spacing)*10)}.p-12{padding:calc(var(--spacing)*12)}.p-14{padding:calc(var(--spacing)*14)}.p-16{padding:calc(var(--spacing)*16)}.p-24{padding:calc(var(--spacing)*24)}.p-\[1px\]{padding:1px}.p-\[2px\]{padding:2px}.p-\[3px\]{padding:3px}.p-\[4px\]{padding:4px}.p-\[8rem\]{padding:8rem}.p-\[10px\]{padding:10px}.p-\[20px_20dvw\]{padding:20px 20dvw}.p-\[20vw\]{padding:20vw}.p-\[22px\]{padding:22px}.p-snc-1{padding:var(--snc-1)}.px-\(--thread-content-margin\){padding-inline:var(--thread-content-margin)}.px-0{padding-inline:calc(var(--spacing)*0)}.px-0\!{padding-inline:calc(var(--spacing)*0)!important}.px-0\.5{padding-inline:calc(var(--spacing)*.5)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-3\.5{padding-inline:calc(var(--spacing)*3.5)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-7{padding-inline:calc(var(--spacing)*7)}.px-8{padding-inline:calc(var(--spacing)*8)}.px-10{padding-inline:calc(var(--spacing)*10)}.px-12{padding-inline:calc(var(--spacing)*12)}.px-16{padding-inline:calc(var(--spacing)*16)}.px-20{padding-inline:calc(var(--spacing)*20)}.px-\[1rem\]{padding-inline:1rem}.px-\[2px\]{padding-inline:2px}.px-\[3px\]{padding-inline:3px}.px-\[4px\]{padding-inline:4px}.px-\[16px\]{padding-inline:16px}.px-\[22px\]{padding-inline:22px}.px-px{padding-inline:1px}.px-snc-1{padding-inline:var(--snc-1)}.px-snc-2{padding-inline:var(--snc-2)}.px-snc-results-padding{padding-inline:var(--snc-results-padding)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\!{padding-block:calc(var(--spacing)*0)!important}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\!{padding-block:calc(var(--spacing)*2)!important}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.py-12{padding-block:calc(var(--spacing)*12)}.py-15{padding-block:calc(var(--spacing)*15)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.py-32{padding-block:calc(var(--spacing)*32)}.py-48{padding-block:calc(var(--spacing)*48)}.py-\[0\.2rem\]{padding-block:.2rem}.py-\[0\.108em\]{padding-block:.108em}.py-\[1px\]{padding-block:1px}.py-\[5px\]{padding-block:5px}.py-\[7px\]{padding-block:7px}.py-\[12px\]{padding-block:12px}.py-\[15px\]{padding-block:15px}.py-snc-1{padding-block:var(--snc-1)}.ps-0{padding-inline-start:calc(var(--spacing)*0)}.ps-0\!{padding-inline-start:calc(var(--spacing)*0)!important}.ps-0\.5{padding-inline-start:calc(var(--spacing)*.5)}.ps-1{padding-inline-start:calc(var(--spacing)*1)}.ps-1\.5{padding-inline-start:calc(var(--spacing)*1.5)}.ps-2{padding-inline-start:calc(var(--spacing)*2)}.ps-2\.5{padding-inline-start:calc(var(--spacing)*2.5)}.ps-3{padding-inline-start:calc(var(--spacing)*3)}.ps-4{padding-inline-start:calc(var(--spacing)*4)}.ps-4\!{padding-inline-start:calc(var(--spacing)*4)!important}.ps-5{padding-inline-start:calc(var(--spacing)*5)}.ps-6{padding-inline-start:calc(var(--spacing)*6)}.ps-7{padding-inline-start:calc(var(--spacing)*7)}.ps-8{padding-inline-start:calc(var(--spacing)*8)}.ps-8\.5{padding-inline-start:calc(var(--spacing)*8.5)}.ps-10{padding-inline-start:calc(var(--spacing)*10)}.ps-12{padding-inline-start:calc(var(--spacing)*12)}.ps-\[1px\]{padding-inline-start:1px}.ps-\[3\.25rem\]{padding-inline-start:3.25rem}.ps-\[14px\]{padding-inline-start:14px}.ps-\[20px\]{padding-inline-start:20px}.ps-\[22px\]{padding-inline-start:22px}.pe-0{padding-inline-end:calc(var(--spacing)*0)}.pe-0\!{padding-inline-end:calc(var(--spacing)*0)!important}.pe-1{padding-inline-end:calc(var(--spacing)*1)}.pe-1\.5{padding-inline-end:calc(var(--spacing)*1.5)}.pe-2{padding-inline-end:calc(var(--spacing)*2)}.pe-2\.5{padding-inline-end:calc(var(--spacing)*2.5)}.pe-2\.5\!{padding-inline-end:calc(var(--spacing)*2.5)!important}.pe-3{padding-inline-end:calc(var(--spacing)*3)}.pe-3\!{padding-inline-end:calc(var(--spacing)*3)!important}.pe-3\.5{padding-inline-end:calc(var(--spacing)*3.5)}.pe-4{padding-inline-end:calc(var(--spacing)*4)}.pe-5{padding-inline-end:calc(var(--spacing)*5)}.pe-6{padding-inline-end:calc(var(--spacing)*6)}.pe-8{padding-inline-end:calc(var(--spacing)*8)}.pe-9{padding-inline-end:calc(var(--spacing)*9)}.pe-10{padding-inline-end:calc(var(--spacing)*10)}.pe-12{padding-inline-end:calc(var(--spacing)*12)}.pe-14{padding-inline-end:calc(var(--spacing)*14)}.pe-36{padding-inline-end:calc(var(--spacing)*36)}.pe-\[15px\]{padding-inline-end:15px}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-0\.5{padding-top:calc(var(--spacing)*.5)}.pt-0\.25{padding-top:calc(var(--spacing)*.25)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-1\.5{padding-top:calc(var(--spacing)*1.5)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-2\.5{padding-top:calc(var(--spacing)*2.5)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-3\!{padding-top:calc(var(--spacing)*3)!important}.pt-3\.5{padding-top:calc(var(--spacing)*3.5)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-5{padding-top:calc(var(--spacing)*5)}.pt-6{padding-top:calc(var(--spacing)*6)}.pt-7{padding-top:calc(var(--spacing)*7)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-12{padding-top:calc(var(--spacing)*12)}.pt-16{padding-top:calc(var(--spacing)*16)}.pt-20{padding-top:calc(var(--spacing)*20)}.pt-\[2px\]{padding-top:2px}.pt-\[3px\]{padding-top:3px}.pt-\[4\.5px\]{padding-top:4.5px}.pt-\[4px\]{padding-top:4px}.pt-\[13px\]{padding-top:13px}.pt-\[18px\]{padding-top:18px}.pt-\[71px\]{padding-top:71px}.pt-header-height{padding-top:var(--header-height)}.pt-px{padding-top:1px}[dir=ltr] .pr-4{padding-right:calc(var(--spacing)*4)}[dir=rtl] .pr-4{padding-left:calc(var(--spacing)*4)}.pb-0{padding-bottom:calc(var(--spacing)*0)}.pb-0\.5{padding-bottom:calc(var(--spacing)*.5)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-2\.5{padding-bottom:calc(var(--spacing)*2.5)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-5{padding-bottom:calc(var(--spacing)*5)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-7{padding-bottom:calc(var(--spacing)*7)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.pb-9{padding-bottom:calc(var(--spacing)*9)}.pb-10{padding-bottom:calc(var(--spacing)*10)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pb-20{padding-bottom:calc(var(--spacing)*20)}.pb-24{padding-bottom:calc(var(--spacing)*24)}.pb-25{padding-bottom:calc(var(--spacing)*25)}.pb-32{padding-bottom:calc(var(--spacing)*32)}.pb-40{padding-bottom:calc(var(--spacing)*40)}.pb-\[1px\]{padding-bottom:1px}.pb-\[5svh\]{padding-bottom:5svh}.pb-\[10px\]{padding-bottom:10px}.pb-\[22px\]{padding-bottom:22px}.pb-px{padding-bottom:1px}.pb-snc-1{padding-bottom:var(--snc-1)}.pb-snc-2{padding-bottom:var(--snc-2)}[dir=ltr] .pl-2{padding-left:calc(var(--spacing)*2)}[dir=rtl] .pl-2{padding-right:calc(var(--spacing)*2)}[dir=ltr] .pl-4{padding-left:calc(var(--spacing)*4)}[dir=rtl] .pl-4{padding-right:calc(var(--spacing)*4)}.text-center{text-align:center}.text-end{text-align:end}[dir=ltr] .text-left{text-align:left}[dir=ltr] .text-right,[dir=rtl] .text-left{text-align:right}[dir=rtl] .text-right{text-align:left}.text-start{text-align:start}.indent-\[0\.1em\]{text-indent:.1em}.align-\[-0\.2em\]{vertical-align:-.2em}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-middle\!{vertical-align:middle!important}.align-top{vertical-align:top}.font-circle{font-family:Circle,system-ui,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace}.font-oai{font-family:OpenAI Sans,sans-serif}.font-sans{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica,Apple Color Emoji,Arial,sans-serif,Segoe UI Emoji,Segoe UI Symbol}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,serif}.text-body-small-regular{font-size:var(--text-body-small-regular);font-weight:var(--tw-font-weight,var(--text-body-small-regular--font-weight));letter-spacing:var(--tw-tracking,var(--text-body-small-regular--letter-spacing));line-height:var(--tw-leading,var(--text-body-small-regular--line-height))}.text-caption-regular{font-size:var(--text-caption-regular);font-weight:var(--tw-font-weight,var(--text-caption-regular--font-weight));letter-spacing:var(--tw-tracking,var(--text-caption-regular--letter-spacing));line-height:var(--tw-leading,var(--text-caption-regular--line-height))}.text-heading-2{font-size:var(--text-heading-2);font-weight:var(--tw-font-weight,var(--text-heading-2--font-weight));letter-spacing:var(--tw-tracking,var(--text-heading-2--letter-spacing));line-height:var(--tw-leading,var(--text-heading-2--line-height))}.text-heading-3{font-size:var(--text-heading-3);font-weight:var(--tw-font-weight,var(--text-heading-3--font-weight));letter-spacing:var(--tw-tracking,var(--text-heading-3--letter-spacing));line-height:var(--tw-leading,var(--text-heading-3--line-height))}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-base\!{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-panel-title{font-size:17px;line-height:var(--tw-leading,26px)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-sm\!{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[font-size\:var\(--pill-font-size\,14px\)\]{font-size:var(--pill-font-size,14px)}.text-\[0\.5em\]{font-size:.5em}.text-\[0\.5rem\]{font-size:.5rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.70rem\]{font-size:.7rem}.text-\[0\.93rem\]{font-size:.93rem}.text-\[0\.625rem\]{font-size:.625rem}.text-\[0\.5625em\]{font-size:.5625em}.text-\[0\.5625rem\]{font-size:.5625rem}.text-\[5px\]{font-size:5px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[17px\]{font-size:17px}.text-\[18px\]{font-size:18px}.text-\[20px\]{font-size:20px}.text-\[22px\]{font-size:22px}.text-\[28px\]{font-size:28px}.text-\[32px\]{font-size:32px}.text-\[34px\]{font-size:34px}.text-\[40px\]{font-size:40px}.text-\[42px\]{font-size:42px}.leading-0{--tw-leading:calc(var(--spacing)*0);line-height:calc(var(--spacing)*0)}.leading-3{--tw-leading:calc(var(--spacing)*3);line-height:calc(var(--spacing)*3)}.leading-4{--tw-leading:calc(var(--spacing)*4);line-height:calc(var(--spacing)*4)}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-6\!{--tw-leading:calc(var(--spacing)*6)!important;line-height:calc(var(--spacing)*6)!important}.leading-7{--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.leading-9{--tw-leading:calc(var(--spacing)*9);line-height:calc(var(--spacing)*9)}.leading-\[0\]{--tw-leading:0;line-height:0}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.4\]{--tw-leading:1.4;line-height:1.4}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[13px\]{--tw-leading:13px;line-height:13px}.leading-\[15px\]{--tw-leading:15px;line-height:15px}.leading-\[17px\]{--tw-leading:17px;line-height:17px}.leading-\[18px\]{--tw-leading:18px;line-height:18px}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[34px\]{--tw-leading:34px;line-height:34px}.leading-\[42px\]{--tw-leading:42px;line-height:42px}.leading-dense{--tw-leading:1.16667;line-height:1.16667}.leading-none{--tw-leading:1;line-height:1}.leading-none\!{--tw-leading:1!important;line-height:1!important}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-\[550\]{--tw-font-weight:550;font-weight:550}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extralight{--tw-font-weight:var(--font-weight-extralight);font-weight:var(--font-weight-extralight)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-normal\!{--tw-font-weight:var(--font-weight-normal)!important;font-weight:var(--font-weight-normal)!important}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-semibold\!{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.\[font-weight\:700\]{font-weight:700}.tracking-\[-0\.18px\]{--tw-tracking:-.18px;letter-spacing:-.18px}.tracking-\[-0\.23px\]{--tw-tracking:-.23px;letter-spacing:-.23px}.tracking-\[-0\.28px\]{--tw-tracking:-.28px;letter-spacing:-.28px}.tracking-\[-0\.197499px\]{--tw-tracking:-.197499px;letter-spacing:-.197499px}.tracking-\[0\.38px\]{--tw-tracking:.38px;letter-spacing:.38px}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.\[text-wrap\:pretty\]{text-wrap:pretty}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.text-pretty{text-wrap:pretty}.text-wrap{text-wrap:wrap}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.overflow-ellipsis{text-overflow:ellipsis}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre\!{white-space:pre!important}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.whitespace-pre-wrap\!{white-space:pre-wrap!important}.\!text-\(--interactive-label-accent-default\){color:var(--interactive-label-accent-default)!important}.\[color\:var\(--pill-color\,var\(--text-secondary\)\)\]{color:var(--pill-color,var(--text-secondary))}.text-\[\#5D5D5D\]{color:#5d5d5d}.text-\[\#007AFF\]{color:#007aff}.text-\[\#008C2E\]{color:#008c2e}.text-\[\#8E3CF3\]{color:#8e3cf3}.text-\[\#8F8F8F\]{color:#8f8f8f}.text-\[\#10A37F\]{color:#10a37f}.text-\[\#30a633\]{color:#30a633}.text-\[\#0088FF\]{color:#08f}.text-\[\#0285ff\]{color:#0285ff}.text-\[\#2964aa\]\!{color:#2964aa!important}.text-\[\#24622B\]{color:#24622b}.text-\[\#59636E\]{color:#59636e}.text-\[\#AF52DE\]{color:#af52de}.text-\[\#D6303D\]{color:#d6303d}.text-\[\#DC2626\]{color:#dc2626}.text-\[\#FE7600\]{color:#fe7600}.text-\[\#df1b41\]{color:#df1b41}.text-\[\#f14d42\]{color:#f14d42}.text-\[hsla\(0\,0\%\,10\%\,0\.4\)\]{color:#1a1a1a66}.text-\[rgb\(18\,100\,163\)\]{color:#1264a3}.text-\[rgba\(255\,255\,255\,0\.6\)\]{color:#fff9}.text-\[rgba\(255\,255\,255\,1\)\]{color:#fff}.text-\[var\(--main-surface-primary-inverse\)\]{color:var(--main-surface-primary-inverse)}.text-\[var\(--sidebar-surface-secondary\)\]{color:var(--sidebar-surface-secondary)}.text-black{color:#000}.text-black\!{color:#000!important}.text-black\/50{color:oklab(0 none none/.5)}.text-blue-100{color:#99ceff}.text-blue-300{color:#339cff}.text-blue-400{color:#0285ff}.text-blue-400\!{color:#0285ff!important}.text-blue-500{color:#0169cc}.text-blue-600{color:#004f99}.text-blue-700{color:#003f7a}.text-blue-800{color:#013566}.text-brand-blue-800{color:#0066de}.text-brand-green-800{color:#05a746}.text-brand-purple{color:#ab68ff}.text-current{color:currentColor}.text-danger{color:#e02e2a}.text-gray-100{color:#ececec}.text-gray-200{color:#e3e3e3}.text-gray-300{color:#cdcdcd}.text-gray-400{color:#b4b4b4}.text-gray-500{color:#9b9b9b}.text-gray-600{color:#676767}.text-gray-700{color:#424242}.text-gray-800{color:#212121}.text-gray-900{color:#171717}.text-gray-950{color:#0d0d0d}.text-green-500{color:#00a240}.text-green-600{color:#008635}.text-green-700{color:#00692a}.text-green-800{color:#004f1f}.text-inherit{color:inherit}.text-orange-300{color:#ff8549}.text-orange-400{color:#fb6a22}.text-orange-500{color:#e25507}.text-orange-600{color:#b9480d}.text-purple-600{color:#6b3ab4}.text-red-400{color:#fa423e}.text-red-500{color:#e02e2a}.text-red-600{color:#ba2623}.text-red-700{color:#911e1b}.text-red-800{color:#6e1615}.text-red-900{color:#4d100e}.text-token-bg-primary{color:var(--bg-primary)}.text-token-hint-text{color:var(--hint-text)}.text-token-icon-secondary{color:var(--icon-secondary)}.text-token-icon-tertiary{color:var(--icon-tertiary)}.text-token-interactive-label-accent-default{color:var(--interactive-label-accent-default)}.text-token-interactive-label-danger-secondary-default{color:var(--interactive-label-danger-secondary-default)}.text-token-link{color:var(--link)}.text-token-main-surface-primary{color:var(--main-surface-primary)}.text-token-main-surface-primary\!{color:var(--main-surface-primary)!important}.text-token-main-surface-primary-inverse{color:var(--main-surface-primary-inverse)}.text-token-main-surface-secondary{color:var(--main-surface-secondary)}.text-token-main-surface-tertiary{color:var(--main-surface-tertiary)}.text-token-sidebar-icon{color:var(--sidebar-icon)}.text-token-sidebar-surface{color:var(--sidebar-surface)}.text-token-text-error{color:var(--text-error)}.text-token-text-primary{color:var(--text-primary)}.text-token-text-primary\!{color:var(--text-primary)!important}.text-token-text-quaternary{color:var(--text-quaternary)}.text-token-text-secondary{color:var(--text-secondary)}.text-token-text-secondary\!{color:var(--text-secondary)!important}.text-token-text-status-error{color:var(--text-status-error)}.text-token-text-status-warning{color:var(--text-status-warning)}.text-token-text-tertiary{color:var(--text-tertiary)}.text-transparent{color:#0000}.text-white{color:#fff}.text-white\!{color:#fff!important}.text-white\/25{color:#ffffff40}.text-white\/90{color:#ffffffe6}.text-yellow-500{color:#e0ac00}.text-yellow-600{color:#ba8e00}.text-yellow-700{color:#916f00}.text-yellow-800{color:#6e5400}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.ordinal{--tw-ordinal:ordinal}.ordinal,.tabular-nums{font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.tabular-nums{--tw-numeric-spacing:tabular-nums}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.no-underline\!{text-decoration-line:none!important}.underline{text-decoration-line:underline}.underline\!{text-decoration-line:underline!important}.decoration-token-link{-webkit-text-decoration-color:var(--link);text-decoration-color:var(--link)}.decoration-token-text-primary{-webkit-text-decoration-color:var(--text-primary);text-decoration-color:var(--text-primary)}.decoration-token-text-secondary{-webkit-text-decoration-color:var(--text-secondary);text-decoration-color:var(--text-secondary)}.decoration-dashed{text-decoration-style:dashed}.decoration-dotted{text-decoration-style:dotted}.decoration-\[4\%\]{text-decoration-thickness:.04em}.decoration-\[12\%\]{text-decoration-thickness:.12em}.underline-offset-1{text-underline-offset:1px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.underline-offset-\[16\%\]{text-underline-offset:16%}.placeholder-gray-500::placeholder{color:#9b9b9b}.placeholder-token-text-tertiary::placeholder{color:var(--text-tertiary)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-100{opacity:1}.opacity-\[0\.01\]{opacity:.01}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-soft-light{mix-blend-mode:soft-light}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a)}.shadow,.shadow-2xl{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040)}.shadow-\[-4px_0_0_0_white\]{--tw-shadow:-4px 0 0 0 var(--tw-shadow-color,#fff)}.shadow-\[-4px_0_0_0_white\],.shadow-\[0_-4px_32px_rgba\(0\,0\,0\,0\.08\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_-4px_32px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 -4px 32px var(--tw-shadow-color,#00000014)}.shadow-\[0_0_50px\]{--tw-shadow:0 0 50px var(--tw-shadow-color,currentcolor)}.shadow-\[0_0_50px\],.shadow-\[0_0_64px_0_rgba\(0\,0\,0\,0\.07\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_64px_0_rgba\(0\,0\,0\,0\.07\)\]{--tw-shadow:0 0 64px 0 var(--tw-shadow-color,#00000012)}.shadow-\[0_1px_0\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,currentcolor)}.shadow-\[0_1px_0\],.shadow-\[0_1px_0_0_var\(--border-light\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_1px_0_0_var\(--border-light\)\]{--tw-shadow:0 1px 0 0 var(--tw-shadow-color,var(--border-light))}.shadow-\[0_1px_1px_rgba\(0\,0\,0\,0\.03\)\,_0_4\.93747px_9\.05202px_rgba\(0\,0\,0\,0\.11\)\]{--tw-shadow:0 1px 1px var(--tw-shadow-color,#00000008),0 4.93747px 9.05202px var(--tw-shadow-color,#0000001c);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_1px_12px_0px_\#0000000B\]{--tw-shadow:0 1px 12px 0px var(--tw-shadow-color,#0000000b)}.shadow-\[0_1px_12px_0px_\#0000000B\],.shadow-\[0_2px_3px_0_rgba\(0\,0\,0\,0\.25\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_2px_3px_0_rgba\(0\,0\,0\,0\.25\)\]{--tw-shadow:0 2px 3px 0 var(--tw-shadow-color,#00000040)}.shadow-\[0_2px_10px\]{--tw-shadow:0 2px 10px var(--tw-shadow-color,currentcolor)}.shadow-\[0_2px_10px\],.shadow-\[0_2px_16px_0_rgba\(0\,0\,0\,0\.04\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_2px_16px_0_rgba\(0\,0\,0\,0\.04\)\]{--tw-shadow:0 2px 16px 0 var(--tw-shadow-color,#0000000a)}.shadow-\[0_4px_8px_-6px_rgb\(0_0_0_\/_0\.1\)\,0_0_1px_rgb\(0_0_0_\/_0\.4\)\]{--tw-shadow:0 4px 8px -6px var(--tw-shadow-color,#0000001a),0 0 1px var(--tw-shadow-color,#0006)}.shadow-\[0_4px_24px_-5px_rgba\(0\,0\,0\,0\.2\)\],.shadow-\[0_4px_8px_-6px_rgb\(0_0_0_\/_0\.1\)\,0_0_1px_rgb\(0_0_0_\/_0\.4\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_4px_24px_-5px_rgba\(0\,0\,0\,0\.2\)\]{--tw-shadow:0 4px 24px -5px var(--tw-shadow-color,#0003)}.shadow-\[0_9px_9px_0px_rgba\(0\,0\,0\,0\.01\)\,_0_2px_5px_0px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0 9px 9px 0px var(--tw-shadow-color,#00000003),0 2px 5px 0px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_24px_-6px_rgb\(0_0_0_\/_0\.1\)\,0_0_1px_rgb\(0_0_0_\/_0\.2\)\]{--tw-shadow:0 12px 24px -6px var(--tw-shadow-color,#0000001a),0 0 1px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_24px_-6px_rgb\(0_0_0_\/_0\.1\)\,0_0_12px_rgb\(0_0_0_\/_0\.2\)\]{--tw-shadow:0 12px 24px -6px var(--tw-shadow-color,#0000001a),0 0 12px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_24px_-6px_rgb\(0_0_0_\/_0\.1\)\,_0_0_1px_rgb\(0_0_0_\/_0\.4\)\]{--tw-shadow:0 12px 24px -6px var(--tw-shadow-color,#0000001a),0 0 1px var(--tw-shadow-color,#0006);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_24px_-6px_rgb\(0_0_0_\/_0\.15\)\)\]{--tw-shadow:0 12px 24px -6px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_32px_-12px_rgb\(0_0_0_\/_0\.2\)\,_0_0_1px_rgb\(0_0_0_\/_0\.3\)\]{--tw-shadow:0 12px 32px -12px var(--tw-shadow-color,#0003),0 0 1px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_12px_32px_-12px_rgb\(0_0_0_\/_0\.4\)\,0_0_1px_rgb\(0_0_0_\/_0\.2\)\]{--tw-shadow:0 12px 32px -12px var(--tw-shadow-color,#0006),0 0 1px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_14px_62px_0_rgba\(0\,0\,0\,0\.25\)\]{--tw-shadow:0 14px 62px 0 var(--tw-shadow-color,#00000040)}.shadow-\[0_14px_62px_0_rgba\(0\,0\,0\,0\.25\)\],.shadow-\[0_32px_48px_rgba\(0\,0\,0\,0\.175\)\,_0_0_1px_rgba\(0\,0\,0\,0\.2\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_32px_48px_rgba\(0\,0\,0\,0\.175\)\,_0_0_1px_rgba\(0\,0\,0\,0\.2\)\]{--tw-shadow:0 32px 48px var(--tw-shadow-color,#0000002d),0 0 1px var(--tw-shadow-color,#0003)}.shadow-\[0px_0px_32px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0px 0px 32px var(--tw-shadow-color,#00000014)}.shadow-\[0px_0px_32px_rgba\(0\,0\,0\,0\.08\)\],.shadow-\[0px_0px_48px_rgba\(0\,0\,0\,0\.08\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0px_0px_48px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0px 0px 48px var(--tw-shadow-color,#00000014)}.shadow-\[0px_1px_1px_0px_rgba\(0\,0\,0\,0\.05\)\]{--tw-shadow:0px 1px 1px 0px var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0px_1px_1px_rgba\(0\,0\,0\,0\.03\)\,_0px_3px_6px_rgba\(0\,0\,0\,0\.02\)\]{--tw-shadow:0px 1px 1px var(--tw-shadow-color,#00000008),0px 3px 6px var(--tw-shadow-color,#00000005);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0px_4px_14px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0px 4px 14px var(--tw-shadow-color,#0000000f)}.shadow-\[0px_4px_14px_rgba\(0\,0\,0\,0\.06\)\],.shadow-\[0px_4px_16px_0px_rgba\(0\,0\,0\,0\.05\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0px_4px_16px_0px_rgba\(0\,0\,0\,0\.05\)\]{--tw-shadow:0px 4px 16px 0px var(--tw-shadow-color,#0000000d)}.shadow-\[0px_10px_30px_0px\]{--tw-shadow:0px 10px 30px 0px var(--tw-shadow-color,currentcolor)}.shadow-\[0px_10px_30px_0px\],.shadow-\[4px_0_0_0_white\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[4px_0_0_0_white\]{--tw-shadow:4px 0 0 0 var(--tw-shadow-color,#fff)}.shadow-\[inset_0_0_0_1px_rgba\(0\,0\,0\,0\.1\)\]{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#0000001a)}.shadow-\[inset_0_0_0_1px_rgba\(0\,0\,0\,0\.05\)\],.shadow-\[inset_0_0_0_1px_rgba\(0\,0\,0\,0\.1\)\]{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_0_0_1px_rgba\(0\,0\,0\,0\.05\)\]{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#0000000d)}.shadow-\[inset_0_0_0_1px_rgba\(50\,50\,93\,0\.1\)\,0_2px_5px_0_rgba\(50\,50\,93\,0\.1\)\,0_1px_1px_0_rgba\(0\,0\,0\,0\.07\)\]{--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#32325d1a),0 2px 5px 0 var(--tw-shadow-color,#32325d1a),0 1px 1px 0 var(--tw-shadow-color,#00000012);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[none\]{--tw-shadow:none}.shadow-\[none\],.shadow-inner{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)}.shadow-lg,.shadow-md{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none\!{--tw-shadow:0 0 #0000!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.shadow-short:not(:is(.dark *)){--tw-shadow:0px 4px 4px 0px var(--tw-shadow-color,var(--shadow-color-1,#0000000a)),0px 0px 1px 0px var(--tw-shadow-color,var(--shadow-color-2,#0000009e));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a)}.shadow-xs{--tw-shadow:0 0 15px var(--tw-shadow-color,#0000001a)}.shadow-xs,.shadow-xxs{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xxs{--tw-shadow:0 0 2px 0 var(--tw-shadow-color,#0000000d),0 4px 6px 0 var(--tw-shadow-color,#00000005)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)}.ring,.ring-0{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)}.ring-0\!{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.\[box-shadow\:var\(--sharp-edge-bottom-shadow-placeholder\)\]{box-shadow:var(--sharp-edge-bottom-shadow-placeholder)}.\[box-shadow\:var\(--sharp-edge-top-shadow\)\]{box-shadow:var(--sharp-edge-top-shadow)}.\[box-shadow\:var\(--sharp-edge-top-shadow-placeholder\)\]{box-shadow:var(--sharp-edge-top-shadow-placeholder)}.shadow-black\/3{--tw-shadow-color:#00000008}@supports (color:color-mix(in lab,red,red)){.shadow-black\/3{--tw-shadow-color:color-mix(in oklab,oklab(0% none none/.03) var(--tw-shadow-alpha),transparent)}}.shadow-black\/5{--tw-shadow-color:#0000000d}@supports (color:color-mix(in lab,red,red)){.shadow-black\/5{--tw-shadow-color:color-mix(in oklab,oklab(0% none none/.05) var(--tw-shadow-alpha),transparent)}}.shadow-token-border-default{--tw-shadow-color:var(--border-default)}@supports (color:color-mix(in lab,red,red)){.shadow-token-border-default{--tw-shadow-color:color-mix(in oklab,var(--border-default)var(--tw-shadow-alpha),transparent)}}.ring-transparent{--tw-ring-color:transparent}.ring-white{--tw-ring-color:#fff}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-4{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-black{--tw-ring-offset-color:#000}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline:2px solid #0000;outline-offset:2px}}.outline{outline-width:1px}.outline,.outline-0{outline-style:var(--tw-outline-style)}.outline-0{outline-width:0}.outline-0\!{outline-style:var(--tw-outline-style)!important;outline-width:0!important}.outline-1{outline-width:1px}.outline-1,.outline-2{outline-style:var(--tw-outline-style)}.outline-2{outline-width:2px}.outline-offset-2{outline-offset:2px}.outline-black\/5{outline-color:oklab(0 none none/.05)}.outline-token-border-light{outline-color:var(--border-light)}.outline-token-border-xlight{outline-color:var(--border-xlight)}.blur{--tw-blur:blur(8px)}.blur,.blur-2xl{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl))}.blur-3xl{--tw-blur:blur(var(--blur-3xl))}.blur-3xl,.blur-lg{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-lg{--tw-blur:blur(var(--blur-lg))}.blur-sm{--tw-blur:blur(var(--blur-sm))}.blur-sm,.drop-shadow{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a))drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a)drop-shadow(0 1px 1px #0000000f)}.drop-shadow-\[0_6px_4px_var\(--shadow-color\)\]{--tw-drop-shadow-size:drop-shadow(0 6px 4px var(--tw-drop-shadow-color,var(--shadow-color)));--tw-drop-shadow:var(--tw-drop-shadow-size)}.drop-shadow-\[0_12px_32px_rgba\(0\,0\,0\,0\.06\)\],.drop-shadow-\[0_6px_4px_var\(--shadow-color\)\]{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-\[0_12px_32px_rgba\(0\,0\,0\,0\.06\)\]{--tw-drop-shadow-size:drop-shadow(0 12px 32px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:var(--tw-drop-shadow-size)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg))}.drop-shadow-lg,.drop-shadow-md{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size:drop-shadow(0 3px 3px var(--tw-drop-shadow-color,#0000001f));--tw-drop-shadow:drop-shadow(var(--drop-shadow-md))}.drop-shadow-xs{--tw-drop-shadow-size:drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000d));--tw-drop-shadow:drop-shadow(var(--drop-shadow-xs))}.drop-shadow-xs,.grayscale{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale-\[0\.6\]{--tw-grayscale:grayscale(.6)}.grayscale-\[0\.6\],.invert{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%)}.sepia{--tw-sepia:sepia(100%)}.filter,.sepia{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(var(--blur-3xl))}.backdrop-blur-3xl,.backdrop-blur-\[24px\]{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-\[24px\]{--tw-backdrop-blur:blur(24px)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg))}.backdrop-blur-lg,.backdrop-blur-md{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md))}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl))}.backdrop-blur-xl,.backdrop-blur-xs{-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-xs{--tw-backdrop-blur:blur(var(--blur-xs))}.backdrop-saturate-25{--tw-backdrop-saturate:saturate(25%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[border-color\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:border-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[filter\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[flex-basis\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:flex-basis;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[mask\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:-webkit-mask,mask;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[opacity_transform\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[stroke-dashoffset\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:stroke-dashoffset;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[transform\,opacity\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[transform_--shadow-color\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform --shadow-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[transform_box-shadow\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[width\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-all{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-colors{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-opacity{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-shadow{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-transform{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-width{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.delay-0{transition-delay:0s}.delay-75{transition-delay:75ms}.delay-100{transition-delay:.1s}.duration-0{--tw-duration:0s;transition-duration:0s}.duration-50{--tw-duration:50ms;transition-duration:50ms}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-175{--tw-duration:.175s;transition-duration:.175s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.duration-1000{--tw-duration:1s;transition-duration:1s}.duration-\[0\.24s\]{--tw-duration:.24s;transition-duration:.24s}.duration-\[1\.5s\]{--tw-duration:1.5s;transition-duration:1.5s}.ease-\[cubic-bezier\(0\.87\,_0\,_0\.13\,_1\)\]{--tw-ease:cubic-bezier(.87,0,.13,1);transition-timing-function:cubic-bezier(.87,0,.13,1)}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.ease-spring-standard{--tw-ease:var(--spring-common);transition-timing-function:var(--spring-common)}.will-change-\[opacity\,transform\]{will-change:opacity,transform}.will-change-transform{will-change:transform}.contain-inline-size{--tw-contain-size:inline-size;contain:var(--tw-contain-size,)var(--tw-contain-layout,)var(--tw-contain-paint,)var(--tw-contain-style,)}.contain-content{contain:content}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}.\[--composer-overlap-px\:24px\]{--composer-overlap-px:24px}.\[--edge-fade-distance\:1rem\]{--edge-fade-distance:1rem}.\[--force-hide-label\:none\]{--force-hide-label:none}.\[--panel-header-height\:var\(--screen-thread-header-min-height\)\]{--panel-header-height:var(--screen-thread-header-min-height)}.\[--pill-color\:var\(--text-secondary\)\]{--pill-color:var(--text-secondary)}.\[--pill-font-size\:13px\]{--pill-font-size:13px}.\[--right-bg\:var\(--bg-primary\)\]{--right-bg:var(--bg-primary)}.\[--right-bg\:var\(--bg-tertiary\)\]{--right-bg:var(--bg-tertiary)}.\[--shadow-color\:transparent\]{--shadow-color:transparent}.\[--thread-content-margin\:--spacing\(4\)\]{--thread-content-margin:calc(var(--spacing)*4)}.\[--thread-content-max-width\:32rem\]{--thread-content-max-width:32rem}.\[anchor-name\:--carousel\]{anchor-name:--carousel}.\[grid-area\:_title\]{grid-area:title}.\[grid-template-areas\:_\'title_action\'_\'description_action\'\]{grid-template-areas:"title action""description action"}.\[min-block-size\:6px\]{min-block-size:6px}.\[scrollbar-gutter\:stable\]{scrollbar-gutter:stable}.\[scrollbar-gutter\:stable_both-edges\]{scrollbar-gutter:stable both-edges}.\[scrollbar-width\:none\]{scrollbar-width:none}.\[scrollbar-width\:thin\]{scrollbar-width:thin}.\[text-box-trim\:trim-both\]{text-box-trim:trim-both}.\[text-decoration-skip-ink\:auto\]{-webkit-text-decoration-skip-ink:auto;text-decoration-skip-ink:auto}.\[text-underline-position\:from-font\]{text-underline-position:from-font}.\[view-transition-name\:var\(--sidebar-popover\)\]{view-transition-name:var(--sidebar-popover)}.\[view-transition-name\:var\(--sidebar-slideover\)\]{view-transition-name:var(--sidebar-slideover)}.\[view-transition-name\:var\(--vt-active-image\)\]{view-transition-name:var(--vt-active-image)}.\[view-transition-name\:var\(--vt-composer\)\]{view-transition-name:var(--vt-composer)}.\[view-transition-name\:var\(--vt-composer-whisper-button\)\]{view-transition-name:var(--vt-composer-whisper-button)}.\[view-transition-name\:var\(--vt-image-carousel\)\]{view-transition-name:var(--vt-image-carousel)}.\[view-transition-name\:var\(--vt-page-footer\)\]{view-transition-name:var(--vt-page-footer)}.\[view-transition-name\:var\(--vt-page-header\)\]{view-transition-name:var(--vt-page-header)}.\[view-transition-name\:var\(--vt-page-title\)\]{view-transition-name:var(--vt-page-title)}.\[view-transition-name\:var\(--vt-scroll-buttons\)\]{view-transition-name:var(--vt-scroll-buttons)}.\[view-transition-name\:var\(--vt-tool-page-title\)\]{view-transition-name:var(--vt-tool-page-title)}.ring-inset{--tw-ring-inset:inset}:is(.\*\:pointer-events-auto>*){pointer-events:auto}:is(.\*\:pointer-events-none>*){pointer-events:none}:is(.\*\:m-0>*){margin:calc(var(--spacing)*0)}:is(.\*\:h-full>*){height:100%}:is(.\*\:w-full>*){width:100%}:is(.\*\:flex-1>*){flex:1}:is(.\*\:rounded-md>*){border-radius:var(--radius-md)}:is(.\*\:bg-gray-300>*){background-color:#cdcdcd}:is(.\*\:object-center>*){object-position:center}:is(.\*\:p-4>*){padding:calc(var(--spacing)*4)}:is(.\*\:px-5>*){padding-inline:calc(var(--spacing)*5)}:is(.\*\:font-normal>*){--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}:is(.\*\:shadow-lg>*){--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.not-group-data-disabled\:text-token-text-tertiary:not(:is(:where(.group)[data-disabled] *)){color:var(--text-tertiary)}@media not all and (pointer:coarse){.not-touch\:hidden{display:none}}.group-focus-within\:text-token-text-secondary:is(:where(.group):focus-within *){color:var(--text-secondary)}.group-focus-within\/turn-messages\:pointer-events-auto:is(:where(.group\/turn-messages):focus-within *){pointer-events:auto}.group-focus-within\/turn-messages\:\[mask-position\:0_0\]:is(:where(.group\/turn-messages):focus-within *){-webkit-mask-position:0 0;mask-position:0 0}.group-focus-within\/turn-messages\:opacity-100:is(:where(.group\/turn-messages):focus-within *){opacity:1}@media (hover:hover){.group-hover\:pointer-events-auto:is(:where(.group):hover *){pointer-events:auto}.group-hover\:visible:is(:where(.group):hover *){visibility:visible}.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:flex:is(:where(.group):hover *){display:flex}.group-hover\:hidden:is(:where(.group):hover *){display:none}.group-hover\:-translate-y-\[1px\]:is(:where(.group):hover *){--tw-translate-y:-1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:translate-y-\[10px\]:is(:where(.group):hover *){--tw-translate-y:10px;translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-100:is(:where(.group):hover *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:scale-\[1\.02\]:is(:where(.group):hover *){scale:1.02}.group-hover\:rotate-\[-2deg\]:is(:where(.group):hover *){rotate:-2deg}.group-hover\:rotate-\[-5deg\]:is(:where(.group):hover *){rotate:-5deg}.group-hover\:border-token-bg-tertiary:is(:where(.group):hover *){border-color:var(--bg-tertiary)}.group-hover\:border-token-text-primary:is(:where(.group):hover *){border-color:var(--text-primary)}.group-hover\:bg-gray-100:is(:where(.group):hover *){background-color:#ececec}.group-hover\:bg-token-main-surface-primary:is(:where(.group):hover *){background-color:var(--main-surface-primary)}.group-hover\:bg-token-main-surface-secondary:is(:where(.group):hover *){background-color:var(--main-surface-secondary)}.group-hover\:text-\[hsla\(0\,0\%\,10\%\,0\.8\)\]:is(:where(.group):hover *){color:#1a1a1acc}.group-hover\:text-red-500:is(:where(.group):hover *){color:#e02e2a}.group-hover\:text-token-interactive-label-accent-default:is(:where(.group):hover *){color:var(--interactive-label-accent-default)}.group-hover\:text-token-link:is(:where(.group):hover *){color:var(--link)}.group-hover\:text-token-text-primary:is(:where(.group):hover *){color:var(--text-primary)}.group-hover\:text-token-text-secondary:is(:where(.group):hover *){color:var(--text-secondary)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}.group-hover\:opacity-90:is(:where(.group):hover *){opacity:.9}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\:shadow-xl:is(:where(.group):hover *){--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-hover\:grayscale-0:is(:where(.group):hover *){--tw-grayscale:grayscale(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.group-hover\:duration-150:is(:where(.group):hover *){--tw-duration:.15s;transition-duration:.15s}.group-hover\/cell\:opacity-0:is(:where(.group\/cell):hover *){opacity:0}.group-hover\/cell\:opacity-100:is(:where(.group\/cell):hover *){opacity:1}.group-hover\/dalle-image\:visible:is(:where(.group\/dalle-image):hover *){visibility:visible}.group-hover\/dalle-image\:bg-black\/70:is(:where(.group\/dalle-image):hover *){background-color:oklab(0 none none/.7)}.group-hover\/dalle-image\:bg-transparent:is(:where(.group\/dalle-image):hover *){background-color:#0000}.group-hover\/debug\:opacity-100:is(:where(.group\/debug):hover *){opacity:1}.group-hover\/file-tile\:block:is(:where(.group\/file-tile):hover *){display:block}.group-hover\/footnote\:border-token-bg-tertiary:is(:where(.group\/footnote):hover *){border-color:var(--bg-tertiary)}.group-hover\/footnote\:border-token-main-surface-secondary:is(:where(.group\/footnote):hover *){border-color:var(--main-surface-secondary)}.group-hover\/icon\:bg-gray-200:is(:where(.group\/icon):hover *){background-color:#e3e3e3}.group-hover\/imagegen-image\:opacity-100:is(:where(.group\/imagegen-image):hover *){opacity:1}.group-hover\/nav-list\:underline:is(:where(.group\/nav-list):hover *){text-decoration-line:underline}.group-hover\/navigation\:bg-\[\#1D53BF1A\]:is(:where(.group\/navigation):hover *){background-color:#1d53bf1a}.group-hover\/paragen-image\:visible:is(:where(.group\/paragen-image):hover *){visibility:visible}.group-hover\/row\:bg-gray-50:is(:where(.group\/row):hover *){background-color:#f9f9f9}.group-hover\/row\:underline:is(:where(.group\/row):hover *){text-decoration-line:underline}.group-hover\/row\:opacity-100:is(:where(.group\/row):hover *){opacity:1}.group-hover\/row\:delay-500:is(:where(.group\/row):hover *){transition-delay:.5s}.group-hover\/snorlax-control-tile\:border-token-main-surface-secondary:is(:where(.group\/snorlax-control-tile):hover *){border-color:var(--main-surface-secondary)}.group-hover\/stack\:z-11:is(:where(.group\/stack):hover *){z-index:11}.group-hover\/stack\:-translate-x-2:is(:where(.group\/stack):hover *){--tw-translate-x:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\/stack\:translate-x-2:is(:where(.group\/stack):hover *){--tw-translate-x:calc(var(--spacing)*2);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\/stack\:translate-y-\[0px\]:is(:where(.group\/stack):hover *){--tw-translate-y:0px;translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\/stack\:rotate-\[-3deg\]:is(:where(.group\/stack):hover *){rotate:-3deg}.group-hover\/stack\:rotate-\[4deg\]:is(:where(.group\/stack):hover *){rotate:4deg}.group-hover\/stack\:shadow-md:is(:where(.group\/stack):hover *){--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-hover\/tldr\:opacity-70:is(:where(.group\/tldr):hover *){opacity:.7}.group-hover\/turn-messages\:pointer-events-auto:is(:where(.group\/turn-messages):hover *){pointer-events:auto}.group-hover\/turn-messages\:\[mask-position\:0_0\]:is(:where(.group\/turn-messages):hover *){-webkit-mask-position:0 0;mask-position:0 0}.group-hover\/turn-messages\:opacity-100:is(:where(.group\/turn-messages):hover *){opacity:1}.group-hover\/turn-messages\:delay-300:is(:where(.group\/turn-messages):hover *){transition-delay:.3s}}.group-focus\/imagegen-image\:opacity-100:is(:where(.group\/imagegen-image):focus *),.group-focus\:opacity-100:is(:where(.group):focus *){opacity:1}@media (hover:hover){.group-hover\:group-enabled\:text-token-text-primary:is(:where(.group):hover *):is(:where(.group):enabled *){color:var(--text-primary)}}.group-has-focus\:border-token-border-xheavy:is(:where(.group):has(:focus) *){border-color:var(--border-xheavy)}.group-data-scroll-from-top\/thread\:\[box-shadow\:var\(--sharp-edge-top-shadow\)\]:is(:where(.group\/thread)[data-scroll-from-top] *){box-shadow:var(--sharp-edge-top-shadow)}.group-data-scrolled-from-end\/scrollport\:shadow-\(--sharp-edge-bottom-shadow\):is(:where(.group\/scrollport)[data-scrolled-from-end] *){--tw-shadow:var(--sharp-edge-bottom-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-scrolled-from-top\/scrollport\:shadow-\(--sharp-edge-top-shadow\):is(:where(.group\/scrollport)[data-scrolled-from-top] *){--tw-shadow:var(--sharp-edge-top-shadow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-stream-active\/thread\:h-\(--thread-end-gutter-active-height\):is(:where(.group\/thread)[data-stream-active] *){height:var(--thread-end-gutter-active-height)}.group-data-stream-active\/thread\:\[overflow-anchor\:none\]:is(:where(.group\/thread)[data-stream-active] *){overflow-anchor:none}.group-data-\[state\=open\]\:rotate-180:is(:where(.group)[data-state=open] *){rotate:180deg}.group-radix-state-checked\:hidden:is(:where(.group)[data-state=checked] *){display:none}.group-radix-state-open\:bg-token-bg-tertiary:is(:where(.group)[data-state=open] *){background-color:var(--bg-tertiary)}.group-radix-state-open\:bg-token-main-surface-tertiary:is(:where(.group)[data-state=open] *){background-color:var(--main-surface-tertiary)}.group-\[\.skeleton\]\:animate-\[shimmer-skeleton_2s_infinite_ease-in-out\]:is(:where(.group).skeleton *){animation:shimmer-skeleton 2s ease-in-out infinite}.group-\[\.skeleton\]\:rounded-md:is(:where(.group).skeleton *){border-radius:var(--radius-md)}.group-\[\.skeleton\]\:bg-linear-to-r:is(:where(.group).skeleton *){--tw-gradient-position:to right;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.group-\[\.skeleton\]\:bg-linear-to-r:is(:where(.group).skeleton *){--tw-gradient-position:to right in oklab}}.group-\[\.skeleton\]\:from-\[\#c1c0c0\]:is(:where(.group).skeleton *){--tw-gradient-from:#c1c0c0;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.group-\[\.skeleton\]\:via-\[\#f1f0f0\]:is(:where(.group).skeleton *){--tw-gradient-via:#f1f0f0;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.group-\[\.skeleton\]\:to-\[\#c1c0c0\:\]:is(:where(.group).skeleton *){--tw-gradient-to:#c1c0c0:;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.group-\[\.skeleton\]\:\[box-decoration-break\:clone\]:is(:where(.group).skeleton *){-webkit-box-decoration-break:clone;box-decoration-break:clone}.group-\[\.skeleton\]\:bg-\[length\:300\%\]:is(:where(.group).skeleton *){background-size:300%}.group-\[\.skeleton\]\:leading-7:is(:where(.group).skeleton *){--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.group-\[\.skeleton\]\:text-transparent:is(:where(.group).skeleton *){color:#0000}.group-\[\.skeleton\]\:\[animation-direction\:alternate\]:is(:where(.group).skeleton *){animation-direction:alternate}.group-\[\:not\(\:hover\)\:not\(\:focus-within\)\]\:pointer-events-none:is(:where(.group):not(:hover):not(:focus-within) *){pointer-events:none}.group-\[\:not\(\:hover\)\:not\(\:focus-within\)\]\:opacity-0:is(:where(.group):not(:hover):not(:focus-within) *){opacity:0}.group-\[\:not\(\[data-scroll-from-end\]\)\]\/thread\:pointer-events-none:is(:where(.group\/thread):not([data-scroll-from-end]) *){pointer-events:none}.group-\[\:not\(\[data-scroll-from-end\]\)\]\/thread\:scale-50:is(:where(.group\/thread):not([data-scroll-from-end]) *){--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-\[\:not\(\[data-scroll-from-end\]\)\]\/thread\:opacity-0:is(:where(.group\/thread):not([data-scroll-from-end]) *){opacity:0}.group-\[\:not\(\[data-scroll-from-end\]\)\]\/thread\:delay-0:is(:where(.group\/thread):not([data-scroll-from-end]) *){transition-delay:0s}.group-\[\:not\(\[data-scroll-from-end\]\)\]\/thread\:duration-100:is(:where(.group\/thread):not([data-scroll-from-end]) *){--tw-duration:.1s;transition-duration:.1s}.group-\[\:not\(\[data-scroll-from-top\]\)\]\/thread\:pointer-events-none:is(:where(.group\/thread):not([data-scroll-from-top]) *){pointer-events:none}.group-\[\:not\(\[data-scroll-from-top\]\)\]\/thread\:scale-50:is(:where(.group\/thread):not([data-scroll-from-top]) *){--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-\[\:not\(\[data-scroll-from-top\]\)\]\/thread\:opacity-0:is(:where(.group\/thread):not([data-scroll-from-top]) *){opacity:0}.group-\[\:not\(\[data-scroll-from-top\]\)\]\/thread\:delay-0:is(:where(.group\/thread):not([data-scroll-from-top]) *){transition-delay:0s}.group-\[\:not\(\[data-scroll-from-top\]\)\]\/thread\:duration-100:is(:where(.group\/thread):not([data-scroll-from-top]) *){--tw-duration:.1s;transition-duration:.1s}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-70:is(:where(.peer):disabled~*){opacity:.7}.first-letter\:uppercase:first-letter{text-transform:uppercase}.marker\:text-token-text-tertiary ::marker{color:var(--text-tertiary)}.marker\:text-token-text-tertiary::marker{color:var(--text-tertiary)}.marker\:text-token-text-tertiary ::-webkit-details-marker,.marker\:text-token-text-tertiary::-webkit-details-marker{color:var(--text-tertiary)}.placeholder\:ps-px::placeholder{padding-inline-start:1px}.placeholder\:text-gray-300::placeholder{color:#cdcdcd}.placeholder\:text-gray-400::placeholder{color:#b4b4b4}.placeholder\:text-gray-500::placeholder{color:#9b9b9b}.placeholder\:text-token-text-quaternary::placeholder{color:var(--text-quaternary)}.placeholder\:text-token-text-secondary::placeholder{color:var(--text-secondary)}.placeholder\:text-token-text-tertiary::placeholder{color:var(--text-tertiary)}.before\:pointer-events-none:before{content:var(--tw-content);pointer-events:none}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}.before\:inset-x-\[-1px\]:before{content:var(--tw-content);inset-inline:-1px}.before\:-start-0\.5:before{content:var(--tw-content);inset-inline-start:calc(var(--spacing)*-.5)}.before\:top-\[-1px\]:before{content:var(--tw-content);top:-1px}.before\:bottom-0:before{bottom:calc(var(--spacing)*0);content:var(--tw-content)}.before\:z-\[-1\]:before{content:var(--tw-content);z-index:-1}.before\:rounded-e-lg:before{border-end-end-radius:var(--radius-lg);border-start-end-radius:var(--radius-lg);content:var(--tw-content)}.before\:bg-\[var\(--right-bg\)\]:before{background-color:var(--right-bg);content:var(--tw-content)}.before\:bg-white\/50:before{background-color:#ffffff80;content:var(--tw-content)}.before\:bg-gradient-to-l:before{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops));content:var(--tw-content)}.before\:from-token-bg-tertiary:before{--tw-gradient-from:var(--bg-tertiary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position));content:var(--tw-content)}.before\:via-token-bg-tertiary:before{--tw-gradient-via:var(--bg-tertiary);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops);content:var(--tw-content)}.before\:to-transparent:before{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position));content:var(--tw-content)}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.before\:content-\[\'\*\'\]:before{--tw-content:"*";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-4:after{content:var(--tw-content);inset:calc(var(--spacing)*-4)}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:inset-\[-4px\]:after{content:var(--tw-content);inset:-4px}.after\:-inset-x-2:after{content:var(--tw-content);inset-inline:calc(var(--spacing)*-2)}.after\:inset-x-2:after{content:var(--tw-content);inset-inline:calc(var(--spacing)*2)}.after\:inset-x-\[-4px\]:after{content:var(--tw-content);inset-inline:-4px}.after\:-inset-y-4:after{content:var(--tw-content);inset-block:calc(var(--spacing)*-4)}.after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--spacing)*0)}.after\:start-0:after{content:var(--tw-content);inset-inline-start:calc(var(--spacing)*0)}.after\:start-\[-15px\]:after{content:var(--tw-content);inset-inline-start:-15px}.after\:start-\[calc\(100\%_\+_280px\)\]:after{content:var(--tw-content);inset-inline-start:calc(100% + 280px)}.after\:start-\[calc\(100\%_-_25px\)\]:after{content:var(--tw-content);inset-inline-start:calc(100% - 25px)}.after\:end-0:after{content:var(--tw-content);inset-inline-end:calc(var(--spacing)*0)}.after\:end-\[-15\%\]:after{content:var(--tw-content);inset-inline-end:-15%}.after\:top-0:after{content:var(--tw-content);top:calc(var(--spacing)*0)}.after\:top-\[-30px\]:after{content:var(--tw-content);top:-30px}.after\:top-\[-95px\]:after{content:var(--tw-content);top:-95px}.after\:top-\[-100\%\]:after{content:var(--tw-content);top:-100%}.after\:-right-4:after{content:var(--tw-content)}[dir=ltr] .after\:-right-4:after{right:calc(var(--spacing)*-4)}[dir=rtl] .after\:-right-4:after{left:calc(var(--spacing)*-4)}.after\:bottom-0:after{bottom:calc(var(--spacing)*0);content:var(--tw-content)}.after\:bottom-\[75\%\]:after{bottom:75%;content:var(--tw-content)}.after\:-left-1:after{content:var(--tw-content)}[dir=ltr] .after\:-left-1:after{left:calc(var(--spacing)*-1)}[dir=rtl] .after\:-left-1:after{right:calc(var(--spacing)*-1)}.after\:z-0:after{content:var(--tw-content);z-index:0}.after\:z-\[-1\]:after{content:var(--tw-content);z-index:-1}.after\:col-\[1\]:after{content:var(--tw-content);grid-column:1}.after\:row-\[1\]:after{content:var(--tw-content);grid-row:1}.after\:mx-1:after{content:var(--tw-content);margin-inline:calc(var(--spacing)*1)}.after\:block:after{content:var(--tw-content);display:block}.after\:h-2:after{content:var(--tw-content);height:calc(var(--spacing)*2)}.after\:h-\[1px\]:after{content:var(--tw-content);height:1px}.after\:h-\[64px\]:after{content:var(--tw-content);height:64px}.after\:h-\[120\%\]:after{content:var(--tw-content);height:120%}.after\:h-\[140px\]:after{content:var(--tw-content);height:140px}.after\:h-\[144px\]:after{content:var(--tw-content);height:144px}.after\:h-\[200px\]:after{content:var(--tw-content);height:200px}.after\:h-fit:after{content:var(--tw-content);height:fit-content}.after\:w-1:after{content:var(--tw-content);width:calc(var(--spacing)*1)}.after\:w-2:after{content:var(--tw-content);width:calc(var(--spacing)*2)}.after\:w-\[75px\]:after{content:var(--tw-content);width:75px}.after\:w-\[80\%\]:after{content:var(--tw-content);width:80%}.after\:w-\[113px\]:after{content:var(--tw-content);width:113px}.after\:w-\[120\%\]:after{content:var(--tw-content);width:120%}.after\:w-\[255px\]:after{content:var(--tw-content);width:255px}.after\:w-fit:after{content:var(--tw-content);width:fit-content}.after\:max-w-\[340px\]:after{content:var(--tw-content);max-width:340px}.after\:rounded-\[50\%\]:after{border-radius:50%;content:var(--tw-content)}.after\:rounded-lg:after{border-radius:var(--radius-lg);content:var(--tw-content)}.after\:rounded-md:after{border-radius:var(--radius-md);content:var(--tw-content)}.after\:rounded-b-2xl:after{border-bottom-left-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl);content:var(--tw-content)}.after\:border-s:after{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px;content:var(--tw-content)}.after\:border-e:after{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px;content:var(--tw-content)}.after\:border-b:after{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;content:var(--tw-content)}.after\:border-token-border-default:after{border-color:var(--border-default);content:var(--tw-content)}.after\:bg-\[Highlight\]:after{background-color:highlight;content:var(--tw-content)}.after\:bg-green-500:after{background-color:#00a240;content:var(--tw-content)}.after\:bg-red-500:after{background-color:#e02e2a;content:var(--tw-content)}.after\:bg-token-border-light:after{background-color:var(--border-light);content:var(--tw-content)}.after\:bg-token-main-surface-primary:after{background-color:var(--main-surface-primary);content:var(--tw-content)}.after\:bg-token-text-primary:after{background-color:var(--text-primary);content:var(--tw-content)}.after\:bg-\[url\(https\:\/\/cdn\.openai\.com\/chatgpt\/ctf-cdn\/student-landing\/cards-more-v2\.png\)\]:after{background-image:url(https://cdn.openai.com/chatgpt/ctf-cdn/student-landing/cards-more-v2.png);content:var(--tw-content)}.after\:bg-\[url\(https\:\/\/cdn\.openai\.com\/chatgpt\/ctf-cdn\/student-landing\/faq-bubble-small-v2\.png\)\]:after{background-image:url(https://cdn.openai.com/chatgpt/ctf-cdn/student-landing/faq-bubble-small-v2.png);content:var(--tw-content)}.after\:bg-\[url\(https\:\/\/cdn\.openai\.com\/chatgpt\/ctf-cdn\/student-landing\/offer-flowers-v2\.png\)\]:after{background-image:url(https://cdn.openai.com/chatgpt/ctf-cdn/student-landing/offer-flowers-v2.png);content:var(--tw-content)}.after\:bg-\[url\(https\:\/\/cdn\.openai\.com\/chatgpt\/ctf-cdn\/student-landing\/splash-scribble-v2\.png\)\]:after{background-image:url(https://cdn.openai.com/chatgpt/ctf-cdn/student-landing/splash-scribble-v2.png);content:var(--tw-content)}.after\:bg-\[url\(https\:\/\/cdn\.openai\.com\/chatgpt\/ctf-cdn\/student-landing\/splash-stars-v2\.png\)\]:after{background-image:url(https://cdn.openai.com/chatgpt/ctf-cdn/student-landing/splash-stars-v2.png);content:var(--tw-content)}.after\:bg-contain:after{background-size:contain;content:var(--tw-content)}.after\:bg-bottom:after{background-position:bottom;content:var(--tw-content)}.after\:bg-center:after{background-position:50%;content:var(--tw-content)}.after\:bg-no-repeat:after{background-repeat:no-repeat;content:var(--tw-content)}.after\:px-\[1px\]:after{content:var(--tw-content);padding-inline:1px}.after\:whitespace-pre:after{content:var(--tw-content);white-space:pre}.after\:opacity-0:after{content:var(--tw-content);opacity:0}.after\:opacity-80:after{content:var(--tw-content);opacity:.8}.after\:opacity-100:after{content:var(--tw-content);opacity:1}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.after\:content-\[\'\\u00b7\'\]:after{--tw-content:"u00b7";content:var(--tw-content)}.after\:content-\[attr\(data-value\)\]:after{--tw-content:attr(data-value);content:var(--tw-content)}.group-last\:after\:hidden:is(:where(.group):last-child *):after{content:var(--tw-content);display:none}.first\:-ms-1:first-child{margin-inline-start:calc(var(--spacing)*-1)}.first\:ms-0:first-child{margin-inline-start:calc(var(--spacing)*0)}.first\:ms-4:first-child{margin-inline-start:calc(var(--spacing)*4)}.first\:me-0:first-child{margin-inline-end:calc(var(--spacing)*0)}.first\:mt-0:first-child{margin-top:calc(var(--spacing)*0)}.first\:hidden:first-child{display:none}.first\:rounded-t:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.first\:border-0:first-child{border-style:var(--tw-border-style);border-width:0}.first\:border-none:first-child{--tw-border-style:none;border-style:none}.first\:ps-0\!:first-child{padding-inline-start:calc(var(--spacing)*0)!important}.first\:pt-\[3px\]:first-child{padding-top:3px}.last\:me-0:last-child{margin-inline-end:calc(var(--spacing)*0)}.last\:me-1\.5:last-child{margin-inline-end:calc(var(--spacing)*1.5)}.last\:me-4:last-child{margin-inline-end:calc(var(--spacing)*4)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}.last\:mb-2:last-child{margin-bottom:calc(var(--spacing)*2)}.last\:mb-5:last-child{margin-bottom:calc(var(--spacing)*5)}.last\:min-h-\[calc\(100vh-8rem\)\]:last-child{min-height:calc(100vh - 8rem)}.last\:snap-end:last-child{scroll-snap-align:end}.last\:scroll-mb-20:last-child{scroll-margin-bottom:calc(var(--spacing)*20)}.last\:scroll-pb-20:last-child{scroll-padding-bottom:calc(var(--spacing)*20)}.last\:rounded-b:last-child{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.last\:rounded-b-lg:last-child{border-bottom-left-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.last\:border-e-0:last-child{border-inline-end-style:var(--tw-border-style);border-inline-end-width:0}.last\:border-b:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:border-none:last-child{--tw-border-style:none;border-style:none}.last\:pe-0:last-child{padding-inline-end:calc(var(--spacing)*0)}.last\:pe-0\!:last-child{padding-inline-end:calc(var(--spacing)*0)!important}.last\:pb-20:last-child{padding-bottom:calc(var(--spacing)*20)}.last\:after\:content-\[none\]:last-child:after{--tw-content:none;content:var(--tw-content)}.first-of-type\:border-none:first-of-type{--tw-border-style:none;border-style:none}.last-of-type\:border-0:last-of-type{border-style:var(--tw-border-style);border-width:0}.last-of-type\:border-b-0:last-of-type{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last-of-type\:border-none:last-of-type{--tw-border-style:none;border-style:none}.checked\:border-black\!:checked{border-color:#000!important}.checked\:border-blue-400\!:checked{border-color:#0285ff!important}.checked\:border-blue-600:checked{border-color:#004f99}.checked\:bg-black\!:checked{background-color:#000!important}.checked\:bg-blue-400\!:checked{background-color:#0285ff!important}.checked\:bg-blue-600:checked{background-color:#004f99}.empty\:hidden:empty{display:none}.focus-within\:relative:focus-within{position:relative}.focus-within\:z-10:focus-within{z-index:10}.focus-within\:rounded-lg:focus-within{border-radius:var(--radius-lg)}.focus-within\:border-none:focus-within{--tw-border-style:none;border-style:none}.focus-within\:border-green-500:focus-within{border-color:#00a240}.focus-within\:border-green-600:focus-within{border-color:#008635}.focus-within\:border-token-border-default:focus-within{border-color:var(--border-default)}.focus-within\:border-token-border-heavy:focus-within{border-color:var(--border-heavy)}.focus-within\:border-token-border-xheavy:focus-within{border-color:var(--border-xheavy)}.focus-within\:bg-token-main-surface-tertiary:focus-within{background-color:var(--main-surface-tertiary)}.focus-within\:opacity-100:focus-within{opacity:1}.focus-within\:shadow-\[0_0_0_2px\]:focus-within{--tw-shadow:0 0 0 2px var(--tw-shadow-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:shadow-none:focus-within{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-0:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-0\!:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.focus-within\:ring-1:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-green-600:focus-within{--tw-ring-color:#008635}.focus-within\:ring-token-border-heavy:focus-within{--tw-ring-color:var(--border-heavy)}.focus-within\:ring-token-text-secondary:focus-within{--tw-ring-color:var(--text-secondary)}.focus-within\:ring-transparent:focus-within{--tw-ring-color:transparent}.focus-within\:outline-hidden:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-within\:outline-hidden:focus-within{outline:2px solid #0000;outline-offset:2px}}.focus-within\:outline-black\/5:focus-within{outline-color:oklab(0 none none/.05)}.focus-within\:transition-none:focus-within{transition-property:none}.focus-within\:outline-none:focus-within{--tw-outline-style:none;outline-style:none}@media (hover:hover){.hover\:visible:hover{visibility:visible}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-\[1\.015\]:hover{scale:1.015}.hover\:cursor-default:hover{cursor:default}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:border:hover{border-style:var(--tw-border-style);border-width:1px}.hover\:border-black\/0:hover{border-color:oklab(0 none none/0)}.hover\:border-gray-100:hover{border-color:#ececec}.hover\:border-gray-500:hover{border-color:#9b9b9b}.hover\:border-gray-900:hover{border-color:#171717}.hover\:border-token-border-heavy:hover{border-color:var(--border-heavy)}.hover\:border-token-border-medium:hover{border-color:var(--border-medium)}.hover\:border-token-border-xheavy:hover{border-color:var(--border-xheavy)}.hover\:border-b-transparent:hover{border-bottom-color:#0000}.hover\:bg-\[\#BDDCF4\]:hover{background-color:#bddcf4}.hover\:bg-\[\#f5f5f5\]:hover{background-color:#f5f5f5}.hover\:bg-\[rgba\(29\,155\,209\,0\.2\)\]:hover{background-color:#1d9bd133}.hover\:bg-\[var\(--snc-hover\)\]:hover{background-color:var(--snc-hover)}.hover\:bg-black:hover{background-color:#000}.hover\:bg-black\/5:hover{background-color:oklab(0 none none/.05)}.hover\:bg-black\/10:hover{background-color:oklab(0 none none/.1)}.hover\:bg-black\/20:hover{background-color:oklab(0 none none/.2)}.hover\:bg-black\/\[0\.075\]\!:hover{background-color:oklab(0 none none/.075)!important}.hover\:bg-blue-600:hover{background-color:#004f99}.hover\:bg-blue-800:hover{background-color:#013566}.hover\:bg-gray-50:hover{background-color:#f9f9f9}.hover\:bg-gray-100:hover{background-color:#ececec}.hover\:bg-gray-100\/75:hover{background-color:#ecececbf}.hover\:bg-gray-200:hover{background-color:#e3e3e3}.hover\:bg-gray-300:hover{background-color:#cdcdcd}.hover\:bg-gray-500\/10:hover{background-color:#9b9b9b1a}.hover\:bg-gray-800:hover{background-color:#212121}.hover\:bg-gray-800\/10:hover{background-color:#2121211a}.hover\:bg-gray-900:hover{background-color:#171717}.hover\:bg-gray-900\/30:hover{background-color:#1717174d}.hover\:bg-orange-400\/10:hover{background-color:#fb6a221a}.hover\:bg-purple-100:hover{background-color:#ceb0fb}.hover\:bg-purple-600:hover{background-color:#6b3ab4}.hover\:bg-red-500\/10:hover{background-color:#e02e2a1a}.hover\:bg-red-500\/15:hover{background-color:#e02e2a26}.hover\:bg-token-bg-elevated-secondary:hover{background-color:var(--bg-elevated-secondary)}.hover\:bg-token-bg-primary:hover{background-color:var(--bg-primary)}.hover\:bg-token-bg-secondary:hover{background-color:var(--bg-secondary)}.hover\:bg-token-bg-tertiary:hover{background-color:var(--bg-tertiary)}.hover\:bg-token-border-light:hover{background-color:var(--border-light)}.hover\:bg-token-border-xlight:hover{background-color:var(--border-xlight)}.hover\:bg-token-icon-surface\/10:hover{background-color:rgb(var(--icon-surface)/1)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-token-icon-surface\/10:hover{background-color:color-mix(in oklab,rgb(var(--icon-surface)/1) 10%,transparent)}}.hover\:bg-token-interactive-bg-secondary-hover:hover{background-color:var(--interactive-bg-secondary-hover)}.hover\:bg-token-main-surface-primary\!:hover{background-color:var(--main-surface-primary)!important}.hover\:bg-token-main-surface-primary-inverse:hover{background-color:var(--main-surface-primary-inverse)}.hover\:bg-token-main-surface-secondary:hover{background-color:var(--main-surface-secondary)}.hover\:bg-token-main-surface-secondary\!:hover{background-color:var(--main-surface-secondary)!important}.hover\:bg-token-main-surface-secondary-selected:hover{background-color:var(--main-surface-secondary-selected)}.hover\:bg-token-main-surface-tertiary:hover{background-color:var(--main-surface-tertiary)}.hover\:bg-token-sidebar-surface-secondary:hover{background-color:var(--sidebar-surface-secondary)}.hover\:bg-token-sidebar-surface-tertiary:hover{background-color:var(--sidebar-surface-tertiary)}.hover\:bg-token-surface-error\/10:hover{background-color:rgb(var(--surface-error)/1)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-token-surface-error\/10:hover{background-color:color-mix(in oklab,rgb(var(--surface-error)/1) 10%,transparent)}}.hover\:bg-token-surface-hover:hover{background-color:var(--surface-hover)}.hover\:bg-token-text-primary:hover{background-color:var(--text-primary)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:#fff}.hover\:bg-white\/15:hover{background-color:#ffffff26}.hover\:bg-white\/40:hover{background-color:#fff6}.hover\:bg-white\/60:hover{background-color:#fff9}.hover\:text-\[\#0285ff\]\/80:hover{color:#0285ffcc}.hover\:text-\[rgb\(11\,76\,140\)\]:hover{color:#0b4c8c}.hover\:text-\[var\(--tag-blue-light\)\]:hover{color:var(--tag-blue-light)}.hover\:text-blue-600:hover{color:#004f99}.hover\:text-blue-700:hover{color:#003f7a}.hover\:text-gray-700:hover{color:#424242}.hover\:text-inherit:hover{color:inherit}.hover\:text-red-500:hover{color:#e02e2a}.hover\:text-red-700:hover{color:#911e1b}.hover\:text-token-link-hover:hover{color:var(--link-hover)}.hover\:text-token-main-surface-secondary\!:hover{color:var(--main-surface-secondary)!important}.hover\:text-token-main-surface-tertiary:hover{color:var(--main-surface-tertiary)}.hover\:text-token-text-inverted:hover{color:var(--text-inverted)}.hover\:text-token-text-primary:hover{color:var(--text-primary)}.hover\:text-token-text-quaternary:hover{color:var(--text-quaternary)}.hover\:text-token-text-secondary:hover{color:var(--text-secondary)}.hover\:text-token-text-tertiary:hover{color:var(--text-tertiary)}.hover\:text-white:hover{color:#fff}.hover\:text-white\/40:hover{color:#fff6}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-50:hover{opacity:.5}.hover\:opacity-65:hover{opacity:.65}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}.hover\:mix-blend-normal:hover{mix-blend-mode:normal}.hover\:shadow-\[-1px_0_2px_2px_rgba\(255\,0\,0\,0\.4\)\]:hover{--tw-shadow:-1px 0 2px 2px var(--tw-shadow-color,#f006)}.hover\:shadow-\[-1px_0_2px_2px_rgba\(255\,0\,0\,0\.4\)\]:hover,.hover\:shadow-\[0_8px_16px_0_rgba\(0\,0\,0\,0\.06\)\]:hover{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-\[0_8px_16px_0_rgba\(0\,0\,0\,0\.06\)\]:hover{--tw-shadow:0 8px 16px 0 var(--tw-shadow-color,#0000000f)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)}.hover\:shadow-lg:hover,.hover\:shadow-md:hover{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a)}.hover\:shadow-sm:hover,.hover\:shadow-xl:hover{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a)}.hover\:shadow-token-border-default:hover{--tw-shadow-color:var(--border-default)}@supports (color:color-mix(in lab,red,red)){.hover\:shadow-token-border-default:hover{--tw-shadow-color:color-mix(in oklab,var(--border-default)var(--tw-shadow-alpha),transparent)}}.hover\:backdrop-blur-md:hover{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.hover\:transition-none:hover{transition-property:none}.hover\:delay-0:hover{transition-delay:0s}.hover\:delay-300:hover{transition-delay:.3s}.hover\:after\:bg-token-main-surface-tertiary:hover:after{background-color:var(--main-surface-tertiary);content:var(--tw-content)}}.focus\:border-none:focus{--tw-border-style:none;border-style:none}.focus\:border-\[\#0570de\]:focus{border-color:#0570de}.focus\:border-black:focus{border-color:#000}.focus\:border-gray-200:focus{border-color:#e3e3e3}.focus\:border-orange-400:focus{border-color:#fb6a22}.focus\:border-red-500:focus{border-color:#e02e2a}.focus\:border-token-border-medium:focus{border-color:var(--border-medium)}.focus\:border-token-text-error:focus{border-color:var(--text-error)}.focus\:border-token-text-primary:focus{border-color:var(--text-primary)}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-white:focus{border-color:#fff}.focus\:bg-token-icon-surface\/10:focus{background-color:rgb(var(--icon-surface)/1)}@supports (color:color-mix(in lab,red,red)){.focus\:bg-token-icon-surface\/10:focus{background-color:color-mix(in oklab,rgb(var(--icon-surface)/1) 10%,transparent)}}.focus\:bg-white\/15:focus{background-color:#ffffff26}.focus\:shadow-\[0px_1px_1px_rgba\(0\,0\,0\,0\.03\)\,0px_3px_6px_rgba\(0\,0\,0\,0\.02\)\,0_0_0_3px_hsla\(210\,96\%\,45\%\,0\.25\)\,0_1px_1px_0_rgba\(0\,0\,0\,0\.08\)\]:focus{--tw-shadow:0px 1px 1px var(--tw-shadow-color,#00000008),0px 3px 6px var(--tw-shadow-color,#00000005),0 0 0 3px var(--tw-shadow-color,#0573e140),0 1px 1px 0 var(--tw-shadow-color,#00000014);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000}.focus\:ring-0:focus,.focus\:shadow-none:focus{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)}.focus\:ring-0\!:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-black:focus{--tw-ring-color:#000}.focus\:ring-blue-400:focus{--tw-ring-color:#0285ff}.focus\:ring-blue-500:focus{--tw-ring-color:#0169cc}.focus\:ring-gray-200:focus{--tw-ring-color:#e3e3e3}.focus\:ring-purple-500:focus{--tw-ring-color:#8046d9}.focus\:ring-token-text-primary:focus{--tw-ring-color:var(--text-primary)}.focus\:ring-transparent:focus{--tw-ring-color:transparent}.focus\:ring-white:focus{--tw-ring-color:#fff}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-0\!:focus{--tw-ring-offset-width:0px!important;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)!important}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline:2px solid #0000;outline-offset:2px}}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-0\!:focus{outline-style:var(--tw-outline-style)!important;outline-width:0!important}.focus\:backdrop-blur-md:focus{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.focus-visible\:z-11:focus-visible{z-index:11}.focus-visible\:translate-y-0:focus-visible{--tw-translate-y:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.focus-visible\:rounded-\[inherit\]:focus-visible{border-radius:inherit}.focus-visible\:border-none:focus-visible{--tw-border-style:none;border-style:none}.focus-visible\:border-token-border-default:focus-visible{border-color:var(--border-default)}.focus-visible\:bg-token-icon-surface\/10:focus-visible{background-color:rgb(var(--icon-surface)/1)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:bg-token-icon-surface\/10:focus-visible{background-color:color-mix(in oklab,rgb(var(--icon-surface)/1) 10%,transparent)}}.focus-visible\:bg-token-main-surface-secondary:focus-visible{background-color:var(--main-surface-secondary)}.focus-visible\:bg-token-surface-hover:focus-visible{background-color:var(--surface-hover)}.focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-current:focus-visible{--tw-ring-color:currentcolor}.focus-visible\:ring-token-text-quaternary:focus-visible{--tw-ring-color:var(--text-quaternary)}.focus-visible\:ring-token-text-secondary:focus-visible{--tw-ring-color:var(--text-secondary)}.focus-visible\:ring-offset-1:focus-visible{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:ring-offset-transparent:focus-visible{--tw-ring-offset-color:transparent}.focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-visible\:outline-hidden:focus-visible{outline:2px solid #0000;outline-offset:2px}}.focus-visible\:outline-0:focus-visible{outline-style:var(--tw-outline-style);outline-width:0}.focus-visible\:-outline-offset-1:focus-visible{outline-offset:-1px}.focus-visible\:outline-black:focus-visible{outline-color:#000}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-98:active{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:scale-\[0\.9\]:active{scale:.9}.active\:bg-black\/20:active{background-color:oklab(0 none none/.2)}.active\:bg-gray-700:active{background-color:#424242}.active\:bg-red-500\/20:active{background-color:#e02e2a33}.active\:bg-token-main-surface-tertiary:active{background-color:var(--main-surface-tertiary)}.active\:opacity-50:active{opacity:.5}.active\:drop-shadow-none:active{--tw-drop-shadow: ;filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@media (hover:hover){.enabled\:hover\:bg-black\/5:enabled:hover{background-color:oklab(0 none none/.05)}.enabled\:hover\:bg-token-main-surface-secondary:enabled:hover{background-color:var(--main-surface-secondary)}.enabled\:hover\:bg-token-surface-hover:enabled:hover{background-color:var(--surface-hover)}.enabled\:hover\:underline:enabled:hover{text-decoration-line:underline}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-\[\#D7D7D7\]:disabled{background-color:#d7d7d7}.disabled\:bg-token-main-surface-tertiary:disabled{background-color:var(--main-surface-tertiary)}.disabled\:text-\[\#f4f4f4\]:disabled{color:#f4f4f4}.disabled\:text-gray-50:disabled{color:#f9f9f9}.disabled\:text-token-border-medium:disabled{color:var(--border-medium)}.disabled\:text-token-text-quaternary:disabled{color:var(--text-quaternary)}.disabled\:text-token-text-tertiary:disabled{color:var(--text-tertiary)}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-100:disabled{opacity:1}@media (hover:hover){.disabled\:hover\:bg-transparent:disabled:hover{background-color:#0000}.disabled\:hover\:opacity-100:disabled:hover{opacity:1}}.has-focus\:shadow-\[0_2px_12px_0px_rgba\(0\,0\,0\,0\.04\)\,0_9px_9px_0px_rgba\(0\,0\,0\,0\.01\)\,0_2px_5px_0px_rgba\(0\,0\,0\,0\.06\)\]:has(:focus){--tw-shadow:0 2px 12px 0px var(--tw-shadow-color,#0000000a),0 9px 9px 0px var(--tw-shadow-color,#00000003),0 2px 5px 0px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.has-focus-visible\:border-token-border-xheavy:has(:focus-visible){border-color:var(--border-xheavy)}.has-data-fullbleed\:-mt-1:has([data-fullbleed]){margin-top:calc(var(--spacing)*-1)}.has-data-fullbleed\:w-\[calc\(100\%\+1rem\)\]:has([data-fullbleed]){width:calc(100% + 1rem)}.has-data-has-thread-error\:pt-2:has([data-has-thread-error]){padding-top:calc(var(--spacing)*2)}.has-data-has-thread-error\:\[box-shadow\:var\(--sharp-edge-bottom-shadow\)\]:has([data-has-thread-error]){box-shadow:var(--sharp-edge-bottom-shadow)}.has-data-\[state\=open\]\:pointer-events-auto:has([data-state=open]){pointer-events:auto}.has-data-\[state\=open\]\:\[mask-position\:0_0\]:has([data-state=open]){-webkit-mask-position:0 0;mask-position:0 0}.has-data-\[state\=open\]\:opacity-100:has([data-state=open]){opacity:1}.has-\[strong\]\:mb-0:has(:is(strong)){margin-bottom:calc(var(--spacing)*0)}.data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.data-disabled\:opacity-50[data-disabled]{opacity:.5}.data-fill\:gap-2[data-fill]{gap:calc(var(--spacing)*2)}.data-something\:bg-red-100[data-something]{background-color:#ffa4a2}.data-\[state\=active\]\:block[data-state=active]{display:block}.data-\[state\=active\]\:border-b-2[data-state=active]{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.data-\[state\=active\]\:border-token-link-hover[data-state=active]{border-color:var(--link-hover)}.data-\[state\=active\]\:border-token-text-secondary[data-state=active]{border-color:var(--text-secondary)}.data-\[state\=active\]\:text-token-link[data-state=active]{color:var(--link)}.data-\[state\=active\]\:text-token-text-primary[data-state=active]{color:var(--text-primary)}.data-\[state\=checked\]\:border-2[data-state=checked]{border-style:var(--tw-border-style);border-width:2px}.data-\[state\=checked\]\:border-black[data-state=checked]{border-color:#000}.data-\[state\=checked\]\:bg-black[data-state=checked]{background-color:#000}.data-\[state\=inactive\]\:cursor-pointer[data-state=inactive]{cursor:pointer}.data-\[state\=inactive\]\:text-token-text-tertiary[data-state=inactive]{color:var(--text-tertiary)}.data-\[state\=unchecked\]\:m-\[1px\][data-state=unchecked]{margin:1px}.data-\[state\=unchecked\]\:border[data-state=unchecked]{border-style:var(--tw-border-style);border-width:1px}.nth-1\:bg-\[\#FFF493\]:first-child{background-color:#fff493}.nth-2\:bg-\[\#EBEBEB\]:nth-child(2){background-color:#ebebeb}.nth-3\:bg-\[\#94E6FF\]:nth-child(3){background-color:#94e6ff}.nth-4\:bg-\[\#C8F7AB\]:nth-child(4){background-color:#c8f7ab}.nth-5\:bg-\[\#B4A6FE\]:nth-child(5){background-color:#b4a6fe}@media (prefers-reduced-motion:no-preference){.motion-safe\:transition{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-\[background-color\,transform\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:background-color,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-\[color\,background-color\,border-color\,text-decoration-color\,fill\,stroke\,box-shadow\,bottom\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,box-shadow,bottom;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-\[mask-position\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:-webkit-mask-position,mask-position;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-all{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-colors{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-opacity{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:transition-width{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.motion-safe\:delay-300{transition-delay:.3s}.motion-safe\:duration-100{--tw-duration:.1s;transition-duration:.1s}.motion-safe\:duration-300{--tw-duration:.3s;transition-duration:.3s}.motion-safe\:\[transition\:0\.25s_transform_var\(--spring-standard\)\,0\.2s_opacity_var\(--spring-standard\)\,0\.3s_visibility_var\(--spring-standard\)\]{transition:.25s transform var(--spring-standard),.2s opacity var(--spring-standard),.3s visibility var(--spring-standard)}.motion-safe\:\[transition\:border-color_0\.1s_ease-in-out\]{transition:border-color .1s ease-in-out}.motion-safe\:\[transition\:height_0\.3s_var\(--easing-common\)\]{transition:height .3s var(--easing-common)}}@media (hover:hover){@media (prefers-reduced-motion:no-preference){.hover\:motion-safe\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}}}@media not all and (min-width:480px){.max-xs\:hidden{display:none}.max-xs\:max-h-\[260px\]{max-height:260px}.max-xs\:gap-1{gap:calc(var(--spacing)*1)}.max-xs\:\[--force-hide-label\:none\]{--force-hide-label:none}}@media not all and (min-width:64rem){.max-lg\:bottom-full{bottom:100%}.max-lg\:hidden{display:none}.max-lg\:w-0\!{width:calc(var(--spacing)*0)!important}.max-lg\:flex-col-reverse{flex-direction:column-reverse}}@media not all and (min-width:48rem){.max-md\:sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}.max-md\:absolute,.max-md\:sr-only{position:absolute}.max-md\:start-0{inset-inline-start:calc(var(--spacing)*0)}.max-md\:end-0{inset-inline-end:calc(var(--spacing)*0)}.max-md\:top-0{top:calc(var(--spacing)*0)}.max-md\:mb-6{margin-bottom:calc(var(--spacing)*6)}.max-md\:flex{display:flex}.max-md\:hidden{display:none}.max-md\:aspect-square{aspect-ratio:1}.max-md\:h-full{height:100%}.max-md\:w-10{width:calc(var(--spacing)*10)}.max-md\:w-\[100dvw\]{width:100dvw}.max-md\:max-w-\[100dvw\]{max-width:100dvw}.max-md\:min-w-\[50vw\]{min-width:50vw}.max-md\:snap-always{scroll-snap-stop:always}.max-md\:flex-wrap{flex-wrap:wrap}.max-md\:items-center{align-items:center}.max-md\:gap-0{gap:calc(var(--spacing)*0)}.max-md\:gap-0\.5{gap:calc(var(--spacing)*.5)}.max-md\:gap-1{gap:calc(var(--spacing)*1)}.max-md\:gap-2{gap:calc(var(--spacing)*2)}.max-md\:rounded-none{border-radius:0}.max-md\:px-3{padding-inline:calc(var(--spacing)*3)}.max-md\:px-4{padding-inline:calc(var(--spacing)*4)}.max-md\:py-0{padding-block:calc(var(--spacing)*0)}.max-md\:ps-2{padding-inline-start:calc(var(--spacing)*2)}.max-md\:pt-0{padding-top:calc(var(--spacing)*0)}.max-md\:pb-6{padding-bottom:calc(var(--spacing)*6)}.max-md\:opacity-100{opacity:1}.max-md\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.max-md\:focus-within\:top-0:focus-within{top:calc(var(--spacing)*0)}}@media not all and (min-width:40rem){.max-sm\:mt-6{margin-top:calc(var(--spacing)*6)}.max-sm\:hidden{display:none}.max-sm\:h-6{height:calc(var(--spacing)*6)}.max-sm\:h-12{height:calc(var(--spacing)*12)}.max-sm\:h-full{height:100%}.max-sm\:max-h-\[300px\]{max-height:300px}.max-sm\:min-h-\[84px\]{min-height:84px}.max-sm\:w-6{width:calc(var(--spacing)*6)}.max-sm\:w-12{width:calc(var(--spacing)*12)}.max-sm\:w-full{width:100%}.max-sm\:flex-1{flex:1}:where(.max-sm\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-end:calc(var(--spacing)*6*(1 - var(--tw-space-x-reverse)));margin-inline-start:calc(var(--spacing)*6*var(--tw-space-x-reverse))}.max-sm\:gap-y-2{row-gap:calc(var(--spacing)*2)}.max-sm\:overflow-y-auto{overflow-y:auto}.max-sm\:p-1\.5{padding:calc(var(--spacing)*1.5)}.max-sm\:px-5{padding-inline:calc(var(--spacing)*5)}.max-sm\:px-6{padding-inline:calc(var(--spacing)*6)}.max-sm\:py-4{padding-block:calc(var(--spacing)*4)}.max-sm\:pt-10{padding-top:calc(var(--spacing)*10)}.max-sm\:pb-5{padding-bottom:calc(var(--spacing)*5)}.max-sm\:pb-\[280px\]{padding-bottom:280px}.max-sm\:text-center{text-align:center}.max-sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.max-sm\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media (min-width:480px){.xs\:-mt-4{margin-top:calc(var(--spacing)*-4)}.xs\:max-w-40{max-width:calc(var(--spacing)*40)}.xs\:max-w-sm\!{max-width:var(--container-sm)!important}.xs\:max-w-xs\!{max-width:var(--container-xs)!important}.xs\:columns-2{column-count:2}.xs\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:flex-col{flex-direction:column}}@media (min-width:510px){@media not all and (min-width:768px){.min-\[510px\]\:max-\[768px\]\:mt-\[25dvh\]\!{margin-top:25dvh!important}}}@media (min-width:1200px){.min-\[1200px\]\:hidden{display:none}}@media (min-width:40rem){.sm\:absolute{position:absolute}.sm\:inset-x-4{inset-inline:calc(var(--spacing)*4)}.sm\:start-1\/2{inset-inline-start:50%}.sm\:start-6{inset-inline-start:calc(var(--spacing)*6)}.sm\:end-6{inset-inline-end:calc(var(--spacing)*6)}.sm\:top-6{top:calc(var(--spacing)*6)}.sm\:col-span-1{grid-column:span 1/span 1}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:mx-\[-32px\]{margin-inline:-32px}.sm\:ms-8{margin-inline-start:calc(var(--spacing)*8)}.sm\:me-8{margin-inline-end:calc(var(--spacing)*8)}.sm\:-mt-7{margin-top:calc(var(--spacing)*-7)}.sm\:mt-0{margin-top:calc(var(--spacing)*0)}.sm\:mt-4{margin-top:calc(var(--spacing)*4)}.sm\:mt-5{margin-top:calc(var(--spacing)*5)}.sm\:mb-3{margin-bottom:calc(var(--spacing)*3)}.sm\:mb-4{margin-bottom:calc(var(--spacing)*4)}.sm\:mb-6{margin-bottom:calc(var(--spacing)*6)}.sm\:line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:h-10{height:calc(var(--spacing)*10)}.sm\:h-32{height:calc(var(--spacing)*32)}.sm\:h-\[172px\]{height:172px}.sm\:h-full{height:100%}.sm\:h-snc-input-height{height:var(--snc-input-height)}.sm\:max-h-80{max-height:calc(var(--spacing)*80)}.sm\:w-10{width:calc(var(--spacing)*10)}.sm\:w-32{width:calc(var(--spacing)*32)}.sm\:w-\[320px\]{width:320px}.sm\:w-\[322px\]{width:322px}.sm\:w-\[380px\]{width:380px}.sm\:w-\[384px\]{width:384px}.sm\:w-\[460px\]{width:460px}.sm\:w-\[calc\(\(100\%-1rem\)\/4\)\]{width:calc(25% - .25rem)}.sm\:w-auto{width:auto}.sm\:max-w-2xl{max-width:var(--container-2xl)}.sm\:max-w-60{max-width:calc(var(--spacing)*60)}.sm\:max-w-100{max-width:25rem}.sm\:max-w-\[400px\]{max-width:400px}.sm\:max-w-\[552px\]{max-width:552px}.sm\:max-w-\[calc\(100vw-10rem\)\]{max-width:calc(100vw - 10rem)}.sm\:max-w-md{max-width:var(--container-md)}.sm\:max-w-xs{max-width:var(--container-xs)}.sm\:min-w-\[300px\]{min-width:300px}.sm\:min-w-\[360px\]{min-width:360px}.sm\:-translate-x-1\/2{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:calc(var(--spacing)*0)}.sm\:gap-1{gap:calc(var(--spacing)*1)}.sm\:gap-2\.5{gap:calc(var(--spacing)*2.5)}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:gap-4{gap:calc(var(--spacing)*4)}.sm\:gap-6{gap:calc(var(--spacing)*6)}.sm\:gap-12{gap:calc(var(--spacing)*12)}.sm\:gap-x-2\.5{column-gap:calc(var(--spacing)*2.5)}.sm\:gap-x-16{column-gap:calc(var(--spacing)*16)}.sm\:gap-y-0{row-gap:calc(var(--spacing)*0)}.sm\:gap-y-2{row-gap:calc(var(--spacing)*2)}.sm\:gap-y-4{row-gap:calc(var(--spacing)*4)}.sm\:gap-y-5{row-gap:calc(var(--spacing)*5)}.sm\:gap-y-12{row-gap:calc(var(--spacing)*12)}.sm\:overflow-hidden{overflow:hidden}.sm\:rounded-\[28px\]{border-radius:28px}.sm\:rounded-\[30px\]{border-radius:30px}.sm\:rounded-full{border-radius:3.40282e+38px}.sm\:rounded-lg{border-radius:var(--radius-lg)}.sm\:rounded-ss-xl{border-start-start-radius:var(--radius-xl)}.sm\:rounded-se-xl{border-start-end-radius:var(--radius-xl)}.sm\:rounded-ee-xl{border-end-end-radius:var(--radius-xl)}.sm\:rounded-es-xl{border-end-start-radius:var(--radius-xl)}.sm\:rounded-t-\[30px\]{border-top-left-radius:30px;border-top-right-radius:30px}.sm\:border{border-style:var(--tw-border-style);border-width:1px}.sm\:border-none{--tw-border-style:none;border-style:none}.sm\:bg-token-main-surface-tertiary{background-color:var(--main-surface-tertiary)}.sm\:p-0{padding:calc(var(--spacing)*0)}.sm\:p-2{padding:calc(var(--spacing)*2)}.sm\:p-3{padding:calc(var(--spacing)*3)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:p-8{padding:calc(var(--spacing)*8)}.sm\:p-10{padding:calc(var(--spacing)*10)}.sm\:px-2{padding-inline:calc(var(--spacing)*2)}.sm\:px-4{padding-inline:calc(var(--spacing)*4)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:px-10{padding-inline:calc(var(--spacing)*10)}.sm\:px-24{padding-inline:calc(var(--spacing)*24)}.sm\:px-snc-results-padding{padding-inline:var(--snc-results-padding)}.sm\:py-2\.5{padding-block:calc(var(--spacing)*2.5)}.sm\:py-3{padding-block:calc(var(--spacing)*3)}.sm\:py-6{padding-block:calc(var(--spacing)*6)}.sm\:py-8{padding-block:calc(var(--spacing)*8)}.sm\:py-24{padding-block:calc(var(--spacing)*24)}.sm\:py-28{padding-block:calc(var(--spacing)*28)}.sm\:ps-5{padding-inline-start:calc(var(--spacing)*5)}.sm\:ps-\[3\.25rem\]{padding-inline-start:3.25rem}.sm\:pt-6{padding-top:calc(var(--spacing)*6)}.sm\:pt-8{padding-top:calc(var(--spacing)*8)}.sm\:pt-12{padding-top:calc(var(--spacing)*12)}.sm\:pt-20{padding-top:calc(var(--spacing)*20)}.sm\:pb-0{padding-bottom:calc(var(--spacing)*0)}.sm\:pb-6{padding-bottom:calc(var(--spacing)*6)}.sm\:pb-8{padding-bottom:calc(var(--spacing)*8)}.sm\:pb-10{padding-bottom:calc(var(--spacing)*10)}.sm\:pb-20{padding-bottom:calc(var(--spacing)*20)}.sm\:text-center{text-align:center}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-token-main-surface-tertiary{color:var(--main-surface-tertiary)}.sm\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)}.sm\:shadow-lg,.sm\:shadow-md{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.sm\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a)}}@media (min-width:48rem){.md\:absolute{position:absolute}.md\:fixed{position:fixed}.md\:relative{position:relative}.md\:static{position:static}.md\:start-0{inset-inline-start:calc(var(--spacing)*0)}.md\:start-4{inset-inline-start:calc(var(--spacing)*4)}.md\:end-0{inset-inline-end:calc(var(--spacing)*0)}.md\:end-4{inset-inline-end:calc(var(--spacing)*4)}.md\:end-6{inset-inline-end:calc(var(--spacing)*6)}.md\:end-12{inset-inline-end:calc(var(--spacing)*12)}.md\:top-4{top:calc(var(--spacing)*4)}.md\:top-6{top:calc(var(--spacing)*6)}.md\:top-\[22px\]{top:22px}.md\:top-header-height{top:var(--header-height)}.md\:bottom-4{bottom:calc(var(--spacing)*4)}.md\:bottom-6{bottom:calc(var(--spacing)*6)}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:m-0{margin:calc(var(--spacing)*0)}.md\:mx-4{margin-inline:calc(var(--spacing)*4)}.md\:my-4{margin-block:calc(var(--spacing)*4)}.md\:ms-0{margin-inline-start:calc(var(--spacing)*0)}.md\:ms-8{margin-inline-start:calc(var(--spacing)*8)}.md\:ms-\[-8px\]{margin-inline-start:-8px}.md\:-mt-4{margin-top:calc(var(--spacing)*-4)}.md\:mt-0{margin-top:calc(var(--spacing)*0)}.md\:mt-2{margin-top:calc(var(--spacing)*2)}.md\:mt-3{margin-top:calc(var(--spacing)*3)}.md\:mt-6{margin-top:calc(var(--spacing)*6)}.md\:mt-8{margin-top:calc(var(--spacing)*8)}.md\:mt-\[-48px\]{margin-top:-48px}.md\:mt-\[120px\]{margin-top:120px}.md\:mt-px{margin-top:1px}.md\:-mb-4{margin-bottom:calc(var(--spacing)*-4)}.md\:mb-0{margin-bottom:calc(var(--spacing)*0)}.md\:mb-8{margin-bottom:calc(var(--spacing)*8)}.md\:mb-10{margin-bottom:calc(var(--spacing)*10)}.md\:line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-flex{display:inline-flex}.md\:h-6{height:calc(var(--spacing)*6)}.md\:h-7{height:calc(var(--spacing)*7)}.md\:h-24{height:calc(var(--spacing)*24)}.md\:h-32{height:calc(var(--spacing)*32)}.md\:h-full{height:100%}.md\:max-h-\[80vh\]{max-height:80vh}.md\:max-h-\[600px\]{max-height:600px}.md\:max-h-\[625px\]{max-height:625px}.md\:max-h-\[calc\(100vh-300px\)\]{max-height:calc(100vh - 300px)}.md\:min-h-\[20rem\]{min-height:20rem}.md\:min-h-\[30rem\]{min-height:30rem}.md\:min-h-\[300px\]{min-height:300px}.md\:min-h-\[380px\]{min-height:380px}.md\:min-h-\[600px\]{min-height:600px}.md\:min-h-\[625px\]{min-height:625px}.md\:w-0{width:calc(var(--spacing)*0)}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.3333%}.md\:w-3\/5{width:60%}.md\:w-3xl{width:var(--container-3xl)}.md\:w-6{width:calc(var(--spacing)*6)}.md\:w-24{width:calc(var(--spacing)*24)}.md\:w-\[10rem\]{width:10rem}.md\:w-\[100px\]{width:100px}.md\:w-\[370px\]{width:370px}.md\:w-\[720px\]{width:720px}.md\:w-\[calc\(100\%-\.5rem\)\]{width:calc(100% - .5rem)}.md\:w-\[calc\(100\%-16rem\)\]{width:calc(100% - 16rem)}.md\:w-\[calc\(100\%_-_64px\)\]{width:calc(100% - 64px)}.md\:w-auto{width:auto}.md\:w-full{width:100%}.md\:max-w-3\/4{max-width:75%}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-96{max-width:calc(var(--spacing)*96)}.md\:max-w-\[10rem\]{max-width:10rem}.md\:max-w-\[50\%\]{max-width:50%}.md\:max-w-\[672px\]{max-width:672px}.md\:max-w-\[680px\]{max-width:680px}.md\:max-w-none{max-width:none}.md\:min-w-\[22rem\]{min-width:22rem}.md\:min-w-\[180px\]{min-width:180px}.md\:min-w-\[450px\]{min-width:450px}.md\:min-w-\[680px\]{min-width:680px}.md\:flex-1{flex:1}.md\:shrink{flex-shrink:1}.md\:grow-0{flex-grow:0}.md\:basis-0{flex-basis:calc(var(--spacing)*0)}.md\:basis-\[25vw\]{flex-basis:25vw}.md\:basis-\[75vw\]{flex-basis:75vw}.md\:translate-y-\[30px\]{--tw-translate-y:30px;translate:var(--tw-translate-x)var(--tw-translate-y)}.md\:scroll-ps-8{scroll-padding-inline-start:calc(var(--spacing)*8)}.md\:columns-2{column-count:2}.md\:columns-3{column-count:3}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto_1fr\]{grid-template-columns:1fr auto 1fr}.md\:grid-rows-\[minmax\(20px\,1fr\)_auto_20px\]{grid-template-rows:minmax(20px,1fr) auto 20px}.md\:grid-rows-\[minmax\(20px\,1fr\)_auto_minmax\(20px\,1fr\)\]{grid-template-rows:minmax(20px,1fr) auto minmax(20px,1fr)}.md\:flex-row{flex-direction:row}.md\:flex-row-reverse{flex-direction:row-reverse}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}.md\:gap-0{gap:calc(var(--spacing)*0)}.md\:gap-2{gap:calc(var(--spacing)*2)}.md\:gap-3{gap:calc(var(--spacing)*3)}.md\:gap-5{gap:calc(var(--spacing)*5)}.md\:gap-6{gap:calc(var(--spacing)*6)}.md\:gap-8{gap:calc(var(--spacing)*8)}.md\:gap-10{gap:calc(var(--spacing)*10)}.md\:gap-16{gap:calc(var(--spacing)*16)}.md\:gap-x-2{column-gap:calc(var(--spacing)*2)}.md\:gap-x-4{column-gap:calc(var(--spacing)*4)}.md\:gap-y-1\.5{row-gap:calc(var(--spacing)*1.5)}.md\:self-end{align-self:flex-end}.md\:overflow-hidden{overflow:hidden}.md\:rounded-lg{border-radius:var(--radius-lg)}.md\:rounded-none{border-radius:0}.md\:border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.md\:border-e{border-inline-end-width:1px}.md\:border-e,.md\:border-e-0{border-inline-end-style:var(--tw-border-style)}.md\:border-e-0{border-inline-end-width:0}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-b{border-bottom-width:1px}.md\:border-b,.md\:border-b-2{border-bottom-style:var(--tw-border-style)}.md\:border-b-2{border-bottom-width:2px}.md\:border-gray-100{border-color:#ececec}.md\:border-transparent{border-color:#0000}.md\:bg-transparent{background-color:#0000}.md\:bg-transparent\!{background-color:#0000!important}.md\:bg-vert-light-gradient{background-image:linear-gradient(#fff0 13.94%,#fff 54.73%)}.md\:p-3{padding:calc(var(--spacing)*3)}.md\:p-4{padding:calc(var(--spacing)*4)}.md\:p-6{padding:calc(var(--spacing)*6)}.md\:p-24{padding:calc(var(--spacing)*24)}.md\:px-0{padding-inline:calc(var(--spacing)*0)}.md\:px-2{padding-inline:calc(var(--spacing)*2)}.md\:px-3{padding-inline:calc(var(--spacing)*3)}.md\:px-3\.5{padding-inline:calc(var(--spacing)*3.5)}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:px-5{padding-inline:calc(var(--spacing)*5)}.md\:px-6{padding-inline:calc(var(--spacing)*6)}.md\:px-8{padding-inline:calc(var(--spacing)*8)}.md\:px-12{padding-inline:calc(var(--spacing)*12)}.md\:px-16{padding-inline:calc(var(--spacing)*16)}.md\:px-\[60px\]{padding-inline:60px}.md\:py-0{padding-block:calc(var(--spacing)*0)}.md\:py-2{padding-block:calc(var(--spacing)*2)}.md\:py-3{padding-block:calc(var(--spacing)*3)}.md\:py-4{padding-block:calc(var(--spacing)*4)}.md\:py-20{padding-block:calc(var(--spacing)*20)}.md\:py-32{padding-block:calc(var(--spacing)*32)}.md\:py-\[22px\]{padding-block:22px}.md\:ps-0{padding-inline-start:calc(var(--spacing)*0)}.md\:ps-2{padding-inline-start:calc(var(--spacing)*2)}.md\:ps-3{padding-inline-start:calc(var(--spacing)*3)}.md\:ps-4{padding-inline-start:calc(var(--spacing)*4)}.md\:ps-6{padding-inline-start:calc(var(--spacing)*6)}.md\:ps-8{padding-inline-start:calc(var(--spacing)*8)}.md\:pe-0{padding-inline-end:calc(var(--spacing)*0)}.md\:pe-3{padding-inline-end:calc(var(--spacing)*3)}.md\:pe-4{padding-inline-end:calc(var(--spacing)*4)}.md\:pe-8{padding-inline-end:calc(var(--spacing)*8)}.md\:pe-12{padding-inline-end:calc(var(--spacing)*12)}.md\:pt-0{padding-top:calc(var(--spacing)*0)}.md\:pt-0\!{padding-top:calc(var(--spacing)*0)!important}.md\:pt-2{padding-top:calc(var(--spacing)*2)}.md\:pt-4{padding-top:calc(var(--spacing)*4)}.md\:pt-5{padding-top:calc(var(--spacing)*5)}.md\:pt-\[3px\]{padding-top:3px}.md\:pt-\[4\.5rem\]{padding-top:4.5rem}.md\:pt-header-height{padding-top:var(--header-height)}.md\:pb-0{padding-bottom:calc(var(--spacing)*0)}.md\:pb-2{padding-bottom:calc(var(--spacing)*2)}.md\:pb-4{padding-bottom:calc(var(--spacing)*4)}.md\:pb-5{padding-bottom:calc(var(--spacing)*5)}.md\:pb-6{padding-bottom:calc(var(--spacing)*6)}.md\:pb-10{padding-bottom:calc(var(--spacing)*10)}[dir=ltr] .md\:pl-2{padding-left:calc(var(--spacing)*2)}[dir=rtl] .md\:pl-2{padding-right:calc(var(--spacing)*2)}[dir=ltr] .md\:pl-4{padding-left:calc(var(--spacing)*4)}[dir=rtl] .md\:pl-4{padding-right:calc(var(--spacing)*4)}.md\:text-justify{text-align:justify}.md\:text-start{text-align:start}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-\[32px\]{font-size:32px}.md\:text-\[40px\]{font-size:40px}.md\:text-\[44px\]{font-size:44px}.md\:leading-8{--tw-leading:calc(var(--spacing)*8);line-height:calc(var(--spacing)*8)}.md\:leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.md\:text-pretty{text-wrap:pretty}.md\:text-token-text-primary{color:var(--text-primary)}.md\:text-token-text-tertiary{color:var(--text-tertiary)}.md\:opacity-0{opacity:0}.md\:opacity-100{opacity:1}.md\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:\[--gutter-min-height\:2\.25rem\]{--gutter-min-height:2.25rem}.md\:after\:opacity-0:after{content:var(--tw-content);opacity:0}.md\:after\:opacity-100:after{content:var(--tw-content);opacity:1}.md\:first\:ms-0:first-child{margin-inline-start:calc(var(--spacing)*0)}.md\:first\:rounded-ss-xl:first-child{border-start-start-radius:var(--radius-xl)}.md\:first\:rounded-es-xl:first-child{border-end-start-radius:var(--radius-xl)}.md\:last\:me-0:last-child{margin-inline-end:calc(var(--spacing)*0)}.md\:last\:mb-6:last-child{margin-bottom:calc(var(--spacing)*6)}.md\:last\:rounded-se-xl:last-child{border-start-end-radius:var(--radius-xl)}.md\:last\:rounded-ee-xl:last-child{border-end-end-radius:var(--radius-xl)}.md\:last\:border-e:last-child{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}@media (hover:hover){.md\:hover\:bg-gray-50:hover{background-color:#f9f9f9}}}@media (min-width:64rem){.lg\:absolute{position:absolute}.lg\:-start-5{inset-inline-start:calc(var(--spacing)*-5)}.lg\:top-1{top:calc(var(--spacing)*1)}.lg\:top-full{top:100%}.lg\:order-3{order:3}.lg\:order-last{order:9999}.lg\:mx-auto{margin-inline:auto}.lg\:mb-8{margin-bottom:calc(var(--spacing)*8)}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:h-36{height:calc(var(--spacing)*36)}.lg\:min-h-full{min-height:100%}.lg\:w-\[53\%\]{width:53%}.lg\:max-w-1\/2{max-width:50%}.lg\:max-w-2xl{max-width:var(--container-2xl)}.lg\:max-w-52{max-width:calc(var(--spacing)*52)}.lg\:max-w-\[40rem\]{max-width:40rem}.lg\:max-w-\[796px\]{max-width:796px}.lg\:max-w-\[800px\]{max-width:800px}.lg\:max-w-md{max-width:var(--container-md)}.lg\:flex-1{flex:1}.lg\:grow{flex-grow:1}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[60\%_40\%\]{grid-template-columns:60% 40%}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:justify-center{justify-content:center}.lg\:justify-end{justify-content:flex-end}.lg\:gap-0{gap:calc(var(--spacing)*0)}.lg\:gap-6{gap:calc(var(--spacing)*6)}.lg\:gap-x-3{column-gap:calc(var(--spacing)*3)}.lg\:gap-x-6{column-gap:calc(var(--spacing)*6)}.lg\:gap-y-2\.5{row-gap:calc(var(--spacing)*2.5)}.lg\:border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.lg\:border-e{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.lg\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.lg\:px-0{padding-inline:calc(var(--spacing)*0)}.lg\:px-2{padding-inline:calc(var(--spacing)*2)}.lg\:px-3{padding-inline:calc(var(--spacing)*3)}.lg\:px-4{padding-inline:calc(var(--spacing)*4)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:px-10{padding-inline:calc(var(--spacing)*10)}.lg\:px-16{padding-inline:calc(var(--spacing)*16)}.lg\:px-24{padding-inline:calc(var(--spacing)*24)}.lg\:py-3{padding-block:calc(var(--spacing)*3)}.lg\:py-6{padding-block:calc(var(--spacing)*6)}.lg\:py-10{padding-block:calc(var(--spacing)*10)}.lg\:py-24{padding-block:calc(var(--spacing)*24)}.lg\:ps-4{padding-inline-start:calc(var(--spacing)*4)}.lg\:ps-10{padding-inline-start:calc(var(--spacing)*10)}.lg\:ps-20{padding-inline-start:calc(var(--spacing)*20)}.lg\:pe-4{padding-inline-end:calc(var(--spacing)*4)}.lg\:pe-20{padding-inline-end:calc(var(--spacing)*20)}.lg\:pt-8{padding-top:calc(var(--spacing)*8)}.lg\:pt-12{padding-top:calc(var(--spacing)*12)}.lg\:pb-4{padding-bottom:calc(var(--spacing)*4)}.lg\:pb-12{padding-bottom:calc(var(--spacing)*12)}.lg\:text-start{text-align:start}.lg\:text-\[36px\]{font-size:36px}.lg\:shadow-\[15px_0_30px_0_rgba\(0\,0\,0\,0\.18\)\]{--tw-shadow:15px 0 30px 0 var(--tw-shadow-color,#0000002e);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media (min-width:80rem){.xl\:min-h-44{min-height:calc(var(--spacing)*44)}.xl\:max-w-3xl{max-width:var(--container-3xl)}.xl\:max-w-4xl{max-width:var(--container-4xl)}.xl\:max-w-64{max-width:calc(var(--spacing)*64)}.xl\:max-w-\[48rem\]{max-width:48rem}.xl\:max-w-xs{max-width:var(--container-xs)}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:gap-2{gap:calc(var(--spacing)*2)}.xl\:gap-x-2\.5{column-gap:calc(var(--spacing)*2.5)}.xl\:gap-y-2\.5{row-gap:calc(var(--spacing)*2.5)}.xl\:px-2{padding-inline:calc(var(--spacing)*2)}.xl\:px-3{padding-inline:calc(var(--spacing)*3)}.xl\:px-24{padding-inline:calc(var(--spacing)*24)}.xl\:pt-10{padding-top:calc(var(--spacing)*10)}.xl\:text-\[14px\]{font-size:14px}}@media (min-width:96rem){.\32xl\:scroll-ps-\[calc\(\(100\%_-_96rem\)_\/_2_\+_32px\)\]{scroll-padding-inline-start:calc(50% - 48rem + 32px)}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:ps-\[calc\(\(100\%_-_96rem\)_\/_2_\+_32px\)\]{padding-inline-start:calc(50% - 48rem + 32px)}.\32xl\:pt-12{padding-top:calc(var(--spacing)*12)}.\32xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-resolution:1.5x){.hires\:border-token-border-heavy{border-color:var(--border-heavy)}}@container thread not (min-width:521px){.\@max-\[521px\]\/thread\:ms-1{margin-inline-start:calc(var(--spacing)*1)}}@container not (min-width:48rem){.\@max-3xl\:-top-2{top:calc(var(--spacing)*-2)}}@container thread not (min-width:31.9rem){.\@max-\[31\.9rem\]\/thread\:grow{flex-grow:1}.\@max-\[31\.9rem\]\/thread\:justify-center{justify-content:center}}@container (min-width:0){.\@\[0px\]\:hidden{display:none}}@container (min-width:150px){.\@\[150px\]\:block{display:block}}@container composer (min-width:300px){.\@\[300px\]\/composer\:flex{display:flex}}@container composer (min-width:310px){.\@\[310px\]\/composer\:flex{display:flex}}@container composer (min-width:400px){.\@\[400px\]\/composer\:flex{display:flex}}@container composer (min-width:800px){.\@\[800px\]\/composer\:flex{display:flex}}@container thread (min-width:32rem){.\@lg\/thread\:mt-\[calc\(30dvh\+25px\)\]{margin-top:calc(30dvh + 25px)}.\@lg\/thread\:block{display:block}.\@lg\/thread\:hidden{display:none}.\@lg\/thread\:grow{flex-grow:1}.\@lg\/thread\:items-end{align-items:flex-end}}@container (min-width:34rem){.\@\[34rem\]\:\[--thread-content-max-width\:40rem\]{--thread-content-max-width:40rem}}@container (min-width:37rem){.\@\[37rem\]\:\[--thread-content-margin\:--spacing\(6\)\]{--thread-content-margin:calc(var(--spacing)*6)}}@container (min-width:42rem){.\@2xl\:flex-row{flex-direction:row}.\@2xl\:justify-between{justify-content:space-between}.\@2xl\:text-start{text-align:start}}@container (min-width:48rem){.\@3xl\:-start-3{inset-inline-start:calc(var(--spacing)*-3)}.\@3xl\:-top-4{top:calc(var(--spacing)*-4)}}@container (min-width:64rem){.\@\[64rem\]\:\[--thread-content-max-width\:48rem\]{--thread-content-max-width:48rem}}@container (min-width:72rem){.\@\[72rem\]\:\[--thread-content-margin\:--spacing\(16\)\]{--thread-content-margin:calc(var(--spacing)*16)}}@container thread (min-width:84rem){.\@\[84rem\]\/thread\:absolute{position:absolute}.\@\[84rem\]\/thread\:start-0{inset-inline-start:calc(var(--spacing)*0)}.\@\[84rem\]\/thread\:end-0{inset-inline-end:calc(var(--spacing)*0)}.\@\[84rem\]\/thread\:bg-transparent{background-color:#0000}.\@\[84rem\]\/thread\:pt-\(--header-height\){padding-top:var(--header-height)}.\@\[84rem\]\/thread\:shadow-none\!{--tw-shadow:0 0 #0000!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}}.ltr\:me-auto:where(:dir(ltr),[dir=ltr],[dir=ltr] *){margin-inline-end:auto}.ltr\:-translate-x-1\/2:where(:dir(ltr),[dir=ltr],[dir=ltr] *){--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.ltr\:translate-x-0\.5:where(:dir(ltr),[dir=ltr],[dir=ltr] *){--tw-translate-x:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.ltr\:-rotate-90:where(:dir(ltr),[dir=ltr],[dir=ltr] *){rotate:-90deg}.rtl\:ms-auto:where(:dir(rtl),[dir=rtl],[dir=rtl] *){margin-inline-start:auto}.rtl\:-translate-x-0\.5:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-translate-x:calc(var(--spacing)*-.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.rtl\:translate-x-1\/2:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-translate-x:50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.rtl\:-scale-x-100:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-scale-x:-100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rtl\:rotate-90:where(:dir(rtl),[dir=rtl],[dir=rtl] *){rotate:90deg}.rtl\:items-start:where(:dir(rtl),[dir=rtl],[dir=rtl] *){align-items:flex-start}:where(.dark\:divide-token-border-heavy:is(.dark *)>:not(:last-child)){border-color:var(--border-heavy)}.dark\:border:is(.dark *){border-style:var(--tw-border-style);border-width:1px}.dark\:border-e:is(.dark *){border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.dark\:border-b:is(.dark *){border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.dark\:border-none:is(.dark *){--tw-border-style:none;border-style:none}.dark\:border-\[\#00000030\]:is(.dark *){border-color:#00000030}.dark\:border-\[\#0088FF\]:is(.dark *){border-color:#08f}.dark\:border-\[\#252525\]:is(.dark *){border-color:#252525}.dark\:border-\[rgba\(255\,255\,255\,0\.1\)\]:is(.dark *){border-color:#ffffff1a}.dark\:border-black\/20:is(.dark *){border-color:oklab(0 none none/.2)}.dark\:border-gray-300:is(.dark *){border-color:#cdcdcd}.dark\:border-gray-500:is(.dark *){border-color:#9b9b9b}.dark\:border-gray-600:is(.dark *){border-color:#676767}.dark\:border-gray-700:is(.dark *){border-color:#424242}.dark\:border-gray-800:is(.dark *){border-color:#212121}.dark\:border-token-bg-tertiary:is(.dark *){border-color:var(--bg-tertiary)}.dark\:border-token-border-default:is(.dark *){border-color:var(--border-default)}.dark\:border-token-border-heavy:is(.dark *){border-color:var(--border-heavy)}.dark\:border-token-border-light:is(.dark *){border-color:var(--border-light)}.dark\:border-token-border-medium:is(.dark *){border-color:var(--border-medium)}.dark\:border-token-border-medium\!:is(.dark *){border-color:var(--border-medium)!important}.dark\:border-token-border-xheavy:is(.dark *){border-color:var(--border-xheavy)}.dark\:border-token-border-xlight:is(.dark *){border-color:var(--border-xlight)}.dark\:border-token-main-surface-secondary:is(.dark *){border-color:var(--main-surface-secondary)}.dark\:border-transparent:is(.dark *){border-color:#0000}.dark\:border-white:is(.dark *){border-color:#fff}.dark\:border-white\/5:is(.dark *){border-color:#ffffff0d}.dark\:border-white\/10:is(.dark *){border-color:#ffffff1a}.dark\:border-white\/20:is(.dark *){border-color:#fff3}.dark\:border-x-token-border-xheavy:is(.dark *){border-inline-color:var(--border-xheavy)}.dark\:border-b-token-border-heavy:is(.dark *){border-bottom-color:var(--border-heavy)}.dark\:border-b-white:is(.dark *){border-bottom-color:#fff}.dark\:prose-invert:is(.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.dark\:prose-invert:is(.dark *) :where(pre):not(:where([class~=not-prose] *)) code{background-color:#0000}.dark\:prose-invert:is(.dark *) :where(code):not(:where([class~=not-prose] *)){background-color:var(--gray-700)}.dark\:bg-\(--gray-800\):is(.dark *){background-color:var(--gray-800)}.dark\:bg-\[\#1E1E1E\]:is(.dark *){background-color:#1e1e1e}.dark\:bg-\[\#2A4A6D\]:is(.dark *){background-color:#2a4a6d}.dark\:bg-\[\#6BBD6720\]:is(.dark *){background-color:#6bbd6720}.dark\:bg-\[\#7CA8FF33\]:is(.dark *){background-color:#7ca8ff33}.dark\:bg-\[\#64572A\]:is(.dark *){background-color:#64572a}.dark\:bg-\[\#171717\]:is(.dark *){background-color:#171717}.dark\:bg-\[\#252525\]:is(.dark *){background-color:#252525}.dark\:bg-\[\#303030\]:is(.dark *){background-color:#303030}.dark\:bg-\[\#303030\]\!:is(.dark *){background-color:#303030!important}.dark\:bg-\[\#353535\]:is(.dark *){background-color:#353535}.dark\:bg-\[\#393939\]:is(.dark *){background-color:#393939}.dark\:bg-\[\#444444\]:is(.dark *){background-color:#444}.dark\:bg-\[\#B2B2B220\]:is(.dark *){background-color:#b2b2b220}.dark\:bg-\[\#C26FFD20\]:is(.dark *){background-color:#c26ffd20}.dark\:bg-\[\#EA8444\]:is(.dark *){background-color:#ea8444}.dark\:bg-\[\#FD756F20\]:is(.dark *){background-color:#fd756f20}.dark\:bg-\[rgb\(51\,36\,35\)\]\!:is(.dark *){background-color:#332423!important}.dark\:bg-\[rgba\(33\,33\,33\,1\)\]:is(.dark *){background-color:#212121}.dark\:bg-\[rgba\(48\,48\,48\,0\.8\)\]:is(.dark *){background-color:#303030cc}.dark\:bg-\[rgba\(202\,58\,49\,0\.16\)\]:is(.dark *){background-color:#ca3a3129}.dark\:bg-\[rgba\(255\,255\,255\,0\.04\)\]:is(.dark *){background-color:#ffffff0a}.dark\:bg-black:is(.dark *){background-color:#000}.dark\:bg-black\/20:is(.dark *){background-color:oklab(0 none none/.2)}.dark\:bg-black\/40:is(.dark *){background-color:oklab(0 none none/.4)}.dark\:bg-black\/50:is(.dark *){background-color:oklab(0 none none/.5)}.dark\:bg-black\/80:is(.dark *){background-color:oklab(0 none none/.8)}.dark\:bg-black\/85:is(.dark *){background-color:oklab(0 none none/.85)}.dark\:bg-blue-900:is(.dark *){background-color:#00284d}.dark\:bg-gray-50:is(.dark *){background-color:#f9f9f9}.dark\:bg-gray-50\/5:is(.dark *){background-color:#f9f9f90d}.dark\:bg-gray-100:is(.dark *){background-color:#ececec}.dark\:bg-gray-600:is(.dark *){background-color:#676767}.dark\:bg-gray-700:is(.dark *){background-color:#424242}.dark\:bg-gray-700\/50:is(.dark *){background-color:#42424280}.dark\:bg-gray-700\/70:is(.dark *){background-color:#424242b3}.dark\:bg-gray-700\/75:is(.dark *){background-color:#424242bf}.dark\:bg-gray-750:is(.dark *){background-color:#2f2f2f}.dark\:bg-gray-800:is(.dark *){background-color:#212121}.dark\:bg-gray-800\/70:is(.dark *){background-color:#212121b3}.dark\:bg-gray-900:is(.dark *){background-color:#171717}.dark\:bg-gray-950:is(.dark *){background-color:#0d0d0d}.dark\:bg-green-600:is(.dark *){background-color:#008635}.dark\:bg-green-600\/30:is(.dark *){background-color:#0086354d}.dark\:bg-green-800:is(.dark *){background-color:#004f1f}.dark\:bg-green-900:is(.dark *){background-color:#003716}.dark\:bg-orange-800:is(.dark *){background-color:#6d2e0f}.dark\:bg-red-500\/10:is(.dark *){background-color:#e02e2a1a}.dark\:bg-red-600:is(.dark *){background-color:#ba2623}.dark\:bg-red-600\/30:is(.dark *){background-color:#ba26234d}.dark\:bg-red-800:is(.dark *){background-color:#6e1615}.dark\:bg-red-900:is(.dark *){background-color:#4d100e}.dark\:bg-token-bg-secondary:is(.dark *){background-color:var(--bg-secondary)}.dark\:bg-token-border-default:is(.dark *){background-color:var(--border-default)}.dark\:bg-token-border-heavy:is(.dark *){background-color:var(--border-heavy)}.dark\:bg-token-main-surface-primary-inverse:is(.dark *){background-color:var(--main-surface-primary-inverse)}.dark\:bg-token-main-surface-secondary:is(.dark *){background-color:var(--main-surface-secondary)}.dark\:bg-token-main-surface-tertiary:is(.dark *){background-color:var(--main-surface-tertiary)}.dark\:bg-token-surface-error\/5:is(.dark *){background-color:rgb(var(--surface-error)/1)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-token-surface-error\/5:is(.dark *){background-color:color-mix(in oklab,rgb(var(--surface-error)/1) 5%,transparent)}}.dark\:bg-token-text-tertiary:is(.dark *){background-color:var(--text-tertiary)}.dark\:bg-transparent:is(.dark *){background-color:#0000}.dark\:bg-white:is(.dark *){background-color:#fff}.dark\:bg-white\/5:is(.dark *){background-color:#ffffff0d}.dark\:bg-white\/10:is(.dark *){background-color:#ffffff1a}.dark\:bg-white\/20:is(.dark *){background-color:#fff3}.dark\:bg-yellow-400:is(.dark *){background-color:#ffc300}.dark\:bg-yellow-400\/30:is(.dark *){background-color:#ffc3004d}.dark\:bg-yellow-400\/50:is(.dark *){background-color:#ffc30080}.dark\:bg-yellow-500\/50:is(.dark *){background-color:#e0ac0080}.dark\:bg-yellow-500\/70:is(.dark *){background-color:#e0ac00b3}.dark\:bg-yellow-600:is(.dark *){background-color:#ba8e00}.dark\:bg-yellow-900:is(.dark *){background-color:#4d3b00}.dark\:bg-linear-to-t:is(.dark *){--tw-gradient-position:to top;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.dark\:bg-linear-to-t:is(.dark *){--tw-gradient-position:to top in oklab}}.dark\:from-\[\#2f2f2f\]:is(.dark *){--tw-gradient-from:#2f2f2f;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:from-black:is(.dark *){--tw-gradient-from:#000;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:from-gray-800:is(.dark *){--tw-gradient-from:#212121;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:from-token-main-surface-primary:is(.dark *){--tw-gradient-from:var(--main-surface-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:via-token-bg-primary:is(.dark *){--tw-gradient-via:var(--bg-primary);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.dark\:via-white\/5:is(.dark *){--tw-gradient-via:oklab(100% 0 5.96046e-8/.05);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.dark\:to-transparent:is(.dark *){--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:to-white\/15:is(.dark *){--tw-gradient-to:oklab(100% 0 5.96046e-8/.15);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:stroke-\[rgba\(0\,0\,0\,0\.32\)\]:is(.dark *){stroke:#00000052}.dark\:stroke-\[rgba\(255\,255\,255\,0\.4\)\]:is(.dark *){stroke:#fff6}.dark\:stroke-brand-purple\/50:is(.dark *){stroke:#ab68ff80}.dark\:stroke-white\/10:is(.dark *){stroke:#ffffff1a}.dark\:text-\[\#6BBD67\]:is(.dark *){color:#6bbd67}.dark\:text-\[\#48AAFF\]:is(.dark *){color:#48aaff}.dark\:text-\[\#B2B2B2\]:is(.dark *){color:#b2b2b2}.dark\:text-\[\#C4C4C4\]:is(.dark *){color:#c4c4c4}.dark\:text-\[\#C26FFD\]:is(.dark *){color:#c26ffd}.dark\:text-\[\#D292FF\]:is(.dark *){color:#d292ff}.dark\:text-\[\#DC2626\]:is(.dark *){color:#dc2626}.dark\:text-\[\#FD756F\]:is(.dark *){color:#fd756f}.dark\:text-\[var\(--text-secondary\)\]:is(.dark *){color:var(--text-secondary)}.dark\:text-black:is(.dark *){color:#000}.dark\:text-blue-75:is(.dark *){color:#cce6ff}.dark\:text-blue-200:is(.dark *){color:#66b5ff}.dark\:text-blue-400:is(.dark *){color:#0285ff}.dark\:text-brand-purple-600:is(.dark *){color:#715fde}.dark\:text-gray-100:is(.dark *){color:#ececec}.dark\:text-gray-200:is(.dark *){color:#e3e3e3}.dark\:text-gray-300:is(.dark *){color:#cdcdcd}.dark\:text-gray-400:is(.dark *){color:#b4b4b4}.dark\:text-gray-500:is(.dark *){color:#9b9b9b}.dark\:text-gray-700:is(.dark *){color:#424242}.dark\:text-gray-800:is(.dark *){color:#212121}.dark\:text-gray-950:is(.dark *){color:#0d0d0d}.dark\:text-green-200:is(.dark *){color:#66d492}.dark\:text-red-200:is(.dark *){color:#ff8583}.dark\:text-token-composer-blue-text:is(.dark *){color:var(--composer-blue-text)}.dark\:text-token-main-surface-tertiary:is(.dark *){color:var(--main-surface-tertiary)}.dark\:text-token-text-primary:is(.dark *){color:var(--text-primary)}.dark\:text-token-text-secondary:is(.dark *){color:var(--text-secondary)}.dark\:text-token-text-tertiary:is(.dark *){color:var(--text-tertiary)}.dark\:text-white:is(.dark *){color:#fff}.dark\:text-white\/30:is(.dark *){color:#ffffff4d}.dark\:text-white\/50:is(.dark *){color:#ffffff80}.dark\:text-yellow-100:is(.dark *){color:#ffe48c}.dark\:opacity-20:is(.dark *){opacity:.2}.dark\:opacity-60:is(.dark *){opacity:.6}.dark\:shadow-\[0_-4px_32px_rgba\(0\,0\,0\,0\.12\)\]:is(.dark *){--tw-shadow:0 -4px 32px var(--tw-shadow-color,#0000001f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_10px_20px_-6px_rgb\(20_20_20_\/_0\.5\)\,0_0_1px_rgb\(255_255_255_\/_0\.7\)\]:is(.dark *),.dark\:shadow-\[0_10px_20px_-6px_rgb\(20_20_20_\/_0\.5\)\,_0_0_1px_rgb\(255_255_255_\/_0\.7\)\]:is(.dark *){--tw-shadow:0 10px 20px -6px var(--tw-shadow-color,#14141480),0 0 1px var(--tw-shadow-color,#ffffffb3);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_10px_20px_-6px_rgb\(20_20_20_\/_0\.5\)\,inset_0_0_1px_rgb\(255_255_255_\/_0\.3\)\]:is(.dark *){--tw-shadow:0 10px 20px -6px var(--tw-shadow-color,#14141480),inset 0 0 1px var(--tw-shadow-color,#ffffff4d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_10px_20px_-6px_rgb\(20_20_20_\/_0\.5\)\]:is(.dark *){--tw-shadow:0 10px 20px -6px var(--tw-shadow-color,#14141480);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_12px_32px_-12px_rgb\(0_0_0_\/_1\)\,inset_0_0_1px_rgb\(255_255_255_\/_0\.3\)\]:is(.dark *){--tw-shadow:0 12px 32px -12px var(--tw-shadow-color,#000),inset 0 0 1px var(--tw-shadow-color,#ffffff4d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_12px_32px_-12px_rgb\(20_20_20_\/_0\.5\)\,_0_0_1px_rgb\(255_255_255_\/_1\)\]:is(.dark *){--tw-shadow:0 12px 32px -12px var(--tw-shadow-color,#14141480),0 0 1px var(--tw-shadow-color,#fff);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0_32px_48px_rgba\(0\,0\,0\,0\.175\)\,_0_0_1px_rgba\(255\,255\,255\,0\.4\)\]:is(.dark *){--tw-shadow:0 32px 48px var(--tw-shadow-color,#0000002d),0 0 1px var(--tw-shadow-color,#fff6);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[0px_24px_64px_rgba\(0\,0\,0\,0\.32\)\]:is(.dark *){--tw-shadow:0px 24px 64px var(--tw-shadow-color,#00000052);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-\[inset_0_0_0_1px_rgba\(255\,255\,255\,0\.1\)\]:is(.dark *){--tw-shadow:inset 0 0 0 1px var(--tw-shadow-color,#ffffff1a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-md:is(.dark *){--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-none:is(.dark *){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:shadow-none\!:is(.dark *){--tw-shadow:0 0 #0000!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.dark\:backdrop-blur-lg:is(.dark *){--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.dark\:\[--right-bg\:black\]:is(.dark *){--right-bg:#000}@media (hover:hover){.dark\:group-hover\:border-token-text-primary:is(.dark *):is(:where(.group):hover *){border-color:var(--text-primary)}.dark\:group-hover\/icon\:bg-gray-600:is(.dark *):is(:where(.group\/icon):hover *){background-color:#676767}.dark\:group-hover\/navigation\:bg-\[\#7CA8FF33\]:is(.dark *):is(:where(.group\/navigation):hover *){background-color:#7ca8ff33}.dark\:group-hover\/row\:bg-gray-700:is(.dark *):is(:where(.group\/row):hover *){background-color:#424242}}.dark\:before\:bg-gray-750\/50:is(.dark *):before{background-color:#2f2f2f80;content:var(--tw-content)}.dark\:after\:bg-\[Highlight\]:is(.dark *):after{background-color:highlight;content:var(--tw-content)}.dark\:after\:invert:is(.dark *):after{--tw-invert:invert(100%);content:var(--tw-content);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.dark\:focus-within\:border-token-border-xheavy:is(.dark *):focus-within{border-color:var(--border-xheavy)}.dark\:focus-within\:ring-0:is(.dark *):focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){.dark\:hover\:border-gray-800:is(.dark *):hover{border-color:#212121}.dark\:hover\:bg-\[\#1A416A\]:is(.dark *):hover{background-color:#1a416a}.dark\:hover\:bg-gray-500\/10:is(.dark *):hover{background-color:#9b9b9b1a}.dark\:hover\:bg-gray-600:is(.dark *):hover{background-color:#676767}.dark\:hover\:bg-gray-700:is(.dark *):hover{background-color:#424242}.dark\:hover\:bg-red-500\/15:is(.dark *):hover{background-color:#e02e2a26}.dark\:hover\:bg-token-bg-primary:is(.dark *):hover{background-color:var(--bg-primary)}.dark\:hover\:bg-token-main-surface-secondary:is(.dark *):hover{background-color:var(--main-surface-secondary)}.dark\:hover\:bg-token-main-surface-tertiary:is(.dark *):hover{background-color:var(--main-surface-tertiary)}.dark\:hover\:bg-token-text-primary:is(.dark *):hover{background-color:var(--text-primary)}.dark\:hover\:bg-token-text-tertiary:is(.dark *):hover{background-color:var(--text-tertiary)}.dark\:hover\:bg-transparent:is(.dark *):hover{background-color:#0000}.dark\:hover\:bg-white:is(.dark *):hover{background-color:#fff}.dark\:hover\:bg-white\/5:is(.dark *):hover{background-color:#ffffff0d}.dark\:hover\:bg-white\/10:is(.dark *):hover{background-color:#ffffff1a}.dark\:hover\:bg-white\/10\!:is(.dark *):hover{background-color:#ffffff1a!important}.dark\:hover\:bg-white\/20:is(.dark *):hover{background-color:#fff3}.hover\:dark\:bg-gray-100\/10:hover:is(.dark *){background-color:#ececec1a}.dark\:hover\:text-black:is(.dark *):hover{color:#000}.dark\:hover\:text-gray-100:is(.dark *):hover{color:#ececec}.dark\:hover\:text-token-main-surface-tertiary:is(.dark *):hover{color:var(--main-surface-tertiary)}.dark\:hover\:opacity-100:is(.dark *):hover{opacity:1}}.dark\:focus\:border-white:is(.dark *):focus{border-color:#fff}.dark\:focus\:ring-white:is(.dark *):focus{--tw-ring-color:#fff}.dark\:focus-visible\:ring-token-main-surface-primary:is(.dark *):focus-visible{--tw-ring-color:var(--main-surface-primary)}.dark\:focus-visible\:outline-white:is(.dark *):focus-visible{outline-color:#fff}.dark\:active\:bg-red-500\/20:is(.dark *):active{background-color:#e02e2a33}.dark\:active\:bg-white\/10:is(.dark *):active{background-color:#ffffff1a}@media (hover:hover){.dark\:enabled\:hover\:bg-white\/10:is(.dark *):enabled:hover{background-color:#ffffff1a}}.dark\:disabled\:bg-token-text-quaternary:is(.dark *):disabled{background-color:var(--text-quaternary)}.dark\:disabled\:text-token-main-surface-secondary:is(.dark *):disabled{color:var(--main-surface-secondary)}.dark\:data-\[state\=checked\]\:border-white:is(.dark *)[data-state=checked]{border-color:#fff}.dark\:data-\[state\=checked\]\:bg-white:is(.dark *)[data-state=checked]{background-color:#fff}@media (min-width:48rem){.md\:dark\:border-gray-700:is(.dark *){border-color:#424242}.md\:dark\:border-transparent:is(.dark *){border-color:#0000}.dark\:md\:bg-transparent:is(.dark *){background-color:#0000}.dark\:md\:bg-vert-dark-gradient:is(.dark *){background-image:linear-gradient(#35374000,#353740 58.85%)}@media (hover:hover){.dark\:md\:hover\:bg-gray-700:is(.dark *):hover{background-color:#424242}}}@media print{.print\:hidden{display:none}.print\:border-none{--tw-border-style:none;border-style:none}.print\:pt-2{padding-top:calc(var(--spacing)*2)}.print\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.radix-disabled\:pointer-events-auto[data-disabled]{pointer-events:auto}.radix-disabled\:pointer-events-none[data-disabled]{pointer-events:none}.radix-disabled\:cursor-auto[data-disabled]{cursor:auto}.radix-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.radix-disabled\:bg-transparent[data-disabled]{background-color:#0000}.radix-disabled\:text-token-text-tertiary[data-disabled]{color:var(--text-tertiary)}.radix-disabled\:opacity-50[data-disabled]{opacity:.5}@media (hover:hover){.radix-disabled\:hover\:bg-transparent[data-disabled]:hover{background-color:#0000}}.dark\:radix-disabled\:bg-transparent:is(.dark *)[data-disabled]{background-color:#0000}.radix-state-active\:bg-token-main-surface-tertiary[data-state=active]{background-color:var(--main-surface-tertiary)}.radix-state-active\:bg-white[data-state=active]{background-color:#fff}.radix-state-active\:text-token-text-primary[data-state=active]{color:var(--text-primary)}.radix-state-active\:text-token-text-secondary[data-state=active]{color:var(--text-secondary)}@media (min-width:48rem){.md\:radix-state-active\:bg-token-main-surface-secondary[data-state=active]{background-color:var(--main-surface-secondary)}.md\:radix-state-active\:bg-token-main-surface-tertiary[data-state=active]{background-color:var(--main-surface-tertiary)}.md\:radix-state-active\:text-token-text-primary[data-state=active]{color:var(--text-primary)}}.dark\:radix-state-active\:bg-token-main-surface-tertiary:is(.dark *)[data-state=active]{background-color:var(--main-surface-tertiary)}.radix-state-checked\:border[data-state=checked]{border-style:var(--tw-border-style);border-width:1px}.radix-state-checked\:border-green-500[data-state=checked]{border-color:#00a240}.radix-state-checked\:border-token-text-tertiary[data-state=checked]{border-color:var(--text-tertiary)}.radix-state-checked\:bg-black[data-state=checked]{background-color:#000}.radix-state-checked\:bg-green-500[data-state=checked]{background-color:#00a240}.radix-state-checked\:bg-green-600\/15[data-state=checked]{background-color:#00863526}.radix-state-checked\:bg-token-main-surface-primary[data-state=checked]{background-color:var(--main-surface-primary)}.radix-state-checked\:bg-token-text-primary[data-state=checked]{background-color:var(--text-primary)}.radix-state-checked\:font-semibold[data-state=checked]{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.radix-state-checked\:text-token-main-surface-primary[data-state=checked]{color:var(--main-surface-primary)}.radix-state-checked\:text-token-text-primary[data-state=checked]{color:var(--text-primary)}.radix-state-checked\:shadow-\[0_0_2px_rgba\(0\,0\,0\,\.03\)\][data-state=checked]{--tw-shadow:0 0 2px var(--tw-shadow-color,#00000008);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:radix-state-checked\:ring-black:focus-visible[data-state=checked]{--tw-ring-color:#000}.radix-state-checked\:ltr\:translate-x-\[13px\][data-state=checked]:where(:dir(ltr),[dir=ltr],[dir=ltr] *){--tw-translate-x:13px;translate:var(--tw-translate-x)var(--tw-translate-y)}.radix-state-checked\:ltr\:translate-x-\[14px\][data-state=checked]:where(:dir(ltr),[dir=ltr],[dir=ltr] *){--tw-translate-x:14px;translate:var(--tw-translate-x)var(--tw-translate-y)}.radix-state-checked\:ltr\:translate-x-\[24px\][data-state=checked]:where(:dir(ltr),[dir=ltr],[dir=ltr] *){--tw-translate-x:24px;translate:var(--tw-translate-x)var(--tw-translate-y)}.radix-state-checked\:rtl\:translate-x-\[-13px\][data-state=checked]:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-translate-x:-13px;translate:var(--tw-translate-x)var(--tw-translate-y)}.radix-state-checked\:rtl\:translate-x-\[-14px\][data-state=checked]:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-translate-x:-14px;translate:var(--tw-translate-x)var(--tw-translate-y)}.radix-state-checked\:rtl\:translate-x-\[-24px\][data-state=checked]:where(:dir(rtl),[dir=rtl],[dir=rtl] *){--tw-translate-x:-24px;translate:var(--tw-translate-x)var(--tw-translate-y)}.dark\:radix-state-checked\:border-green-600:is(.dark *)[data-state=checked]{border-color:#008635}.dark\:radix-state-checked\:bg-green-600:is(.dark *)[data-state=checked]{background-color:#008635}.dark\:focus-visible\:radix-state-checked\:ring-green-600:is(.dark *):focus-visible[data-state=checked]{--tw-ring-color:#008635}.radix-state-open\:animate-show[data-state=open]{animation:show .1s cubic-bezier(.16,1,.3,1)}.radix-state-open\:bg-black\/10[data-state=open]{background-color:oklab(0 none none/.1)}.radix-state-open\:bg-token-main-surface-secondary[data-state=open]{background-color:var(--main-surface-secondary)}.radix-state-open\:text-token-text-primary[data-state=open]{color:var(--text-primary)}.radix-state-open\:text-token-text-secondary[data-state=open]{color:var(--text-secondary)}.radix-state-open\:text-token-text-tertiary[data-state=open]{color:var(--text-tertiary)}.dark\:radix-state-open\:text-gray-400:is(.dark *)[data-state=open]{color:#b4b4b4}.radix-side-bottom\:flex-col-reverse[data-side=bottom]{flex-direction:column-reverse}@media (hover:hover) and (pointer:fine){.can-hover\:z-0{z-index:0}.can-hover\:hidden{display:none}.can-hover\:w-full{width:100%}@media (hover:hover){.can-hover\:group-hover\:me-5:is(:where(.group):hover *){margin-inline-end:calc(var(--spacing)*5)}.can-hover\:group-hover\:flex:is(:where(.group):hover *){display:flex}.can-hover\:group-hover\:w-11\/12:is(:where(.group):hover *){width:91.6667%}.can-hover\:group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.can-hover\:group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.can-hover\:hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.can-hover\:hover\:bg-\[\#BDDCF4\]:hover{background-color:#bddcf4}.can-hover\:hover\:bg-black\/5:hover{background-color:oklab(0 none none/.05)}.can-hover\:hover\:bg-token-main-surface-secondary:hover{background-color:var(--main-surface-secondary)}.can-hover\:hover\:bg-token-main-surface-tertiary:hover{background-color:var(--main-surface-tertiary)}.can-hover\:hover\:text-token-link-hover:hover{color:var(--link-hover)}.can-hover\:hover\:text-token-text-primary:hover{color:var(--text-primary)}.can-hover\:hover\:opacity-70:hover{opacity:.7}}.can-hover\:active\:scale-100:active{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.can-hover\:active\:scale-\[0\.98\]:active{scale:.98}}@media (hover:hover){@media (hover:hover) and (pointer:fine){@media (prefers-reduced-motion:no-preference){.group-hover\/app-icon\:can-hover\:motion-safe\:scale-\[0\.91\]:is(:where(.group\/app-icon):hover *){scale:.91}.group-hover\/app-icon\:can-hover\:motion-safe\:scale-\[1\.025\]:is(:where(.group\/app-icon):hover *){scale:1.025}.group-hover\/app-icon\:can-hover\:motion-safe\:shadow-\[0px_4px_12px_rgba\(0\,0\,0\,0\.08\)\]:is(:where(.group\/app-icon):hover *){--tw-shadow:0px 4px 12px var(--tw-shadow-color,#00000014);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-hover\/app-icon\:can-hover\:motion-safe\:\[--shadow-color\:rgba\(0\,0\,0\,0\.1\)\]:is(:where(.group\/app-icon):hover *){--shadow-color:#0000001a}}}}@media (hover:hover) and (pointer:fine){@media (hover:hover){.dark\:can-hover\:hover\:bg-\[\#1A416A\]:is(.dark *):hover{background-color:#1a416a}.dark\:can-hover\:hover\:bg-gray-700:is(.dark *):hover{background-color:#424242}.dark\:can-hover\:hover\:bg-token-main-surface-tertiary:is(.dark *):hover{background-color:var(--main-surface-tertiary)}.dark\:can-hover\:hover\:bg-white\/5:is(.dark *):hover{background-color:#ffffff0d}}}.screen-arch .screen-arch\:static{position:static}.screen-arch .screen-arch\:top-12{top:calc(var(--spacing)*12)}.screen-arch .screen-arch\:flex{display:flex}.screen-arch .screen-arch\:hidden{display:none}.screen-arch .screen-arch\:min-h-\[47px\]{min-height:47px}.screen-arch .screen-arch\:min-h-\[calc\(100dvh-var\(--thread-leading-height\)-var\(--thread-trailing-height\)-12px\)\]{min-height:calc(100dvh - var(--thread-leading-height) - var(--thread-trailing-height) - 12px)}.screen-arch .screen-arch\:w-full{width:100%}.screen-arch .screen-arch\:items-center{align-items:center}.screen-arch .screen-arch\:justify-evenly{justify-content:space-evenly}.screen-arch .screen-arch\:bg-none{background-image:none}.screen-arch .screen-arch\:px-2{padding-inline:calc(var(--spacing)*2)}.screen-arch .screen-arch\:py-1\.5{padding-block:calc(var(--spacing)*1.5)}@media (min-width:48rem){.screen-arch .md\:screen-arch\:flex{display:flex}}.keyboard-open .keyboard-open\:fixed{position:fixed}.keyboard-open .keyboard-open\:start-3{inset-inline-start:calc(var(--spacing)*3)}.keyboard-open .keyboard-open\:end-3{inset-inline-end:calc(var(--spacing)*3)}.keyboard-open .keyboard-open\:bottom-\[var\(--screen-keyboard-height\,0\)\]{bottom:var(--screen-keyboard-height,0)}.keyboard-open .keyboard-open\:z-50{z-index:50}.keyboard-open .keyboard-open\:h-\[calc\(100\%-var\(--screen-keyboard-height\,0px\)-var\(--composer-height\,100px\)\)\]{height:calc(100% - var(--screen-keyboard-height,0px) - var(--composer-height,100px))}.keyboard-open .keyboard-open\:h-\[var\(--screen-height-override\,calc\(var\(--cqh-full\)-env\(keyboard-inset-height\,0px\)-var\(--screen-height-offset\,0px\)-var\(--force-redraw\,0px\)\)\)\]{height:var(--screen-height-override,calc(var(--cqh-full) - env(keyboard-inset-height,0px) - var(--screen-height-offset,0px) - var(--force-redraw,0px)))}.keyboard-open .keyboard-open\:w-auto\!{width:auto!important}.keyboard-open .keyboard-open\:-translate-y-2{--tw-translate-y:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.keyboard-open .keyboard-open\:pb-\[calc\(var\(--composer-height\,100px\)\+var\(--screen-keyboard-height\,0\)\)\]{padding-bottom:calc(var(--composer-height,100px) + var(--screen-keyboard-height,0))}.panel-has-scrolled\:\[box-shadow\:var\(--sharp-edge-top-shadow\)\].panel-has-scrolled{box-shadow:var(--sharp-edge-top-shadow)}.panel-is-scrolling-to-end\:\[box-shadow\:var\(--sharp-edge-bottom-shadow\)\].panel-is-scrolling-to-end{box-shadow:var(--sharp-edge-bottom-shadow)}.top-banner-visible .top-banner-visible\:top-\(--top-banner-height\,0px\){top:var(--top-banner-height,0)}.top-banner-visible .top-banner-visible\:bottom-0{bottom:calc(var(--spacing)*0)}.top-banner-visible .top-banner-visible\:h-auto{height:auto}@media (pointer:coarse){.touch\:-ms-3\.5{margin-inline-start:calc(var(--spacing)*-3.5)}.touch\:-me-2{margin-inline-end:calc(var(--spacing)*-2)}.touch\:hidden{display:none}.touch\:w-\[32px\]{width:32px}.touch\:w-\[38px\]{width:38px}.touch\:px-2\.5{padding-inline:calc(var(--spacing)*2.5)}}@supports (-webkit-touch-callout:none){.is-ios\:hidden{display:none}.is-ios\:inline-block{display:inline-block}}.\[\&\]\:border-0{border-style:var(--tw-border-style);border-width:0}.\[\&_path\]\:stroke-current path{stroke:currentColor}.\[\&_svg\]\:h-full svg{height:100%}.\[\&_svg\]\:w-full svg{width:100%}.\[\&_tr\:last-child\]\:border-b-0 tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.\[\&\&\]\:text-black.\[\&\&\]\:text-black{color:#000}.\[\&\&\]\:underline.\[\&\&\]\:underline{text-decoration-line:underline}@media (hover:hover){.\[\&\&\]\:hover\:text-black.\[\&\&\]\:hover\:text-black:hover{color:#000}}.dark\:\[\&\&\]\:text-white:is(.dark *).dark\:\[\&\&\]\:text-white:is(.dark *){color:#fff}@media (hover:hover){.dark\:\[\&\&\]\:hover\:text-white:is(.dark *).dark\:\[\&\&\]\:hover\:text-white:is(.dark *):hover{color:#fff}}.\[\&\:\:-webkit-search-cancel-button\]\:hidden::-webkit-search-cancel-button{display:none}.\[\&\:not\(\:has\(strong\)\)\]\:mb-\[18px\]:not(:has(strong)){margin-bottom:18px}.\[\&\>\:last-child\]\:mb-0>:last-child{margin-bottom:calc(var(--spacing)*0)}.\[\&\>div\:nth-child\(2\)\]\:overflow-y-hidden>div:nth-child(2){overflow-y:hidden}.\[\&\>td\]\:py-2>td{padding-block:calc(var(--spacing)*2)}.text-message+.\[\.text-message\+\&\]\:mt-5{margin-top:calc(var(--spacing)*5)}@media (max-height:550px){.\[\@media\(max-height\:550px\)\]\:hidden{display:none}}@media (min-width:1560px){.\[\@media\(min-width\:1560px\)\]\:top-0{top:calc(var(--spacing)*0)}}tr:last-child .\[tr\:last-child_\&\]\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}tr[data-disabled=true] .\[tr\[data-disabled\=true\]_\&\]\:opacity-50{opacity:.5}.composer-btn{align-items:center;border-color:#0000;border-style:var(--tw-border-style);border-width:1px;color:var(--text-primary);display:flex;font-size:var(--text-sm);height:calc(var(--spacing)*9);justify-content:center;line-height:var(--tw-leading,var(--text-sm--line-height));min-width:calc(var(--spacing)*9);position:relative;-webkit-user-select:none;user-select:none;white-space:nowrap}@media (hover:hover){.composer-btn:where(:not(:disabled,:active)):hover:before{background-color:var(--interactive-bg-secondary-hover);content:var(--tw-content)}.composer-btn:where(:not(:disabled,:active))[data-is-selected=true]:hover:before{background-color:var(--interactive-bg-accent-muted-hover);content:var(--tw-content)}}@container thread not (min-width:401px){.composer-btn:where(:not(:disabled,:active))[data-is-selected=true]:before{background-color:var(--interactive-bg-accent-muted-hover);content:var(--tw-content)}}.composer-btn:enabled{cursor:pointer}.composer-btn:disabled{cursor:not-allowed}:is(.composer-btn>*){pointer-events:none}.composer-btn:focus-visible{--tw-outline-style:none;outline-style:none}.composer-btn:focus-visible:before{content:var(--tw-content);outline-color:#000;outline-style:var(--tw-outline-style);outline-width:2px}.composer-btn:disabled{opacity:.3}.composer-btn:is(.dark *):focus-visible:before{content:var(--tw-content);outline-color:#fff}.composer-btn[data-is-selected]{color:var(--interactive-label-accent-default)}.composer-btn[data-is-selected]:active:before{background-color:var(--interactive-bg-accent-muted-press);content:var(--tw-content)}.composer-btn[data-pill]{padding-inline:calc(var(--spacing)*2)}@container thread not (min-width:401px){.composer-btn[data-pill]{gap:calc(var(--spacing)*0)}}@container thread not (min-width:521px){.composer-btn [data-label]{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}}.composer-btn:active:before,.composer-btn[data-is-open]:before,.composer-btn[data-state=open]:before{background-color:var(--interactive-bg-secondary-press);content:var(--tw-content)}.composer-btn:before{--tw-content:"";border-radius:3.40282e+38px;content:var(--tw-content);display:block;inset:calc(var(--spacing)*0);position:absolute;z-index:-1}.overflow-auto>*,.overflow-scroll>*,.overflow-x-auto>*,.overflow-y-auto>*{scrollbar-color:auto}.overflow-auto,.overflow-scroll,.overflow-x-auto,.overflow-x-scroll,.overflow-y-auto,.overflow-y-scroll{scrollbar-color:var(--main-surface-tertiary)transparent}.overflow-auto:hover,.overflow-scroll:hover,.overflow-x-auto:hover,.overflow-y-auto:hover{scrollbar-color:var(--gray-200)transparent}.dark .overflow-auto:hover,.dark .overflow-scroll:hover,.dark .overflow-x-auto:hover,.dark .overflow-y-auto:hover{scrollbar-color:var(--gray-600)transparent}}.dark .light,.light,html{--bg-primary:#fff;--bg-secondary:#e8e8e8;--bg-tertiary:#f3f3f3;--bg-scrim:#0d0d0d80;--bg-elevated-primary:#fff;--bg-elevated-secondary:#f9f9f9;--bg-status-warning:#fff5f0;--bg-status-error:#fff0f0;--border-default:#0d0d0d1a;--border-heavy:#0d0d0d26;--border-light:#0d0d0d0d;--border-status-warning:#ffe7d9;--border-status-error:#ffe1e0;--text-primary:#0d0d0d;--text-secondary:#5d5d5d;--text-tertiary:#8f8f8f;--text-inverted:#fff;--text-inverted-static:#fff;--text-accent:#66b5ff;--text-status-warning:#e25507;--text-status-error:#e02e2a;--icon-primary:#0d0d0d;--icon-secondary:#5d5d5d;--icon-tertiary:#8f8f8f;--icon-inverted:#fff;--icon-inverted-static:#fff;--icon-accent:#0285ff;--icon-status-warning:#e25507;--icon-status-error:#e02e2a;--interactive-bg-primary-default:#0d0d0d;--interactive-bg-primary-hover:#0d0d0dcc;--interactive-bg-primary-press:#0d0d0de5;--interactive-bg-primary-inactive:#0d0d0d;--interactive-bg-primary-selected:#0d0d0d;--interactive-bg-secondary-default:#0d0d0d00;--interactive-bg-secondary-hover:#0d0d0d05;--interactive-bg-secondary-press:#0d0d0d0d;--interactive-bg-secondary-inactive:#0d0d0d00;--interactive-bg-secondary-selected:#0d0d0d0d;--interactive-bg-tertiary-default:#fff;--interactive-bg-tertiary-hover:#f9f9f9;--interactive-bg-tertiary-press:#f3f3f3;--interactive-bg-tertiary-inactive:#fff;--interactive-bg-tertiary-selected:#fff;--interactive-bg-accent-default:#e5f3ff;--interactive-bg-accent-hover:#cce6ff;--interactive-bg-accent-muted-hover:#f5faff;--interactive-bg-accent-press:#99ceff;--interactive-bg-accent-muted-press:#e7f4ff;--interactive-bg-accent-inactive:#e5f3ff;--interactive-bg-danger-primary-default:#e02e2a;--interactive-bg-danger-primary-hover:#fa423e;--interactive-bg-danger-primary-press:#ba2623;--interactive-bg-danger-primary-inactive:#e02e2a;--interactive-bg-danger-secondary-default:#0d0d0d00;--interactive-bg-danger-secondary-hover:#0d0d0d00;--interactive-bg-danger-secondary-press:#0d0d0d00;--interactive-bg-danger-secondary-inactive:#0d0d0d00;--interactive-border-focus:#0d0d0d;--interactive-border-secondary-default:#0d0d0d1a;--interactive-border-secondary-hover:#0d0d0d0d;--interactive-border-secondary-press:#0d0d0d0d;--interactive-border-secondary-inactive:#0d0d0d1a;--interactive-border-tertiary-default:#0d0d0d1a;--interactive-border-tertiary-hover:#0d0d0d1a;--interactive-border-tertiary-press:#0d0d0d0d;--interactive-border-tertiary-inactive:#0d0d0d1a;--interactive-border-danger-secondary-default:#e02e2a;--interactive-border-danger-secondary-hover:#fa423e;--interactive-border-danger-secondary-press:#ba2623;--interactive-border-danger-secondary-inactive:#e02e2a;--interactive-label-primary-default:#fff;--interactive-label-primary-hover:#fff;--interactive-label-primary-press:#fff;--interactive-label-primary-inactive:#fff;--interactive-label-primary-selected:#fff;--interactive-label-secondary-default:#0d0d0d;--interactive-label-secondary-hover:#0d0d0de5;--interactive-label-secondary-press:#0d0d0dcc;--interactive-label-secondary-inactive:#0d0d0d;--interactive-label-secondary-selected:#0d0d0d;--interactive-label-tertiary-default:#5d5d5d;--interactive-label-tertiary-hover:#5d5d5d;--interactive-label-tertiary-press:#5d5d5d;--interactive-label-tertiary-inactive:#5d5d5d;--interactive-label-tertiary-selected:#5d5d5d;--interactive-label-accent-default:#0285ff;--interactive-label-accent-hover:#0285ff;--interactive-label-accent-press:#0285ff;--interactive-label-accent-inactive:#0285ff;--interactive-label-accent-selected:#0285ff;--interactive-label-danger-primary-default:#fff;--interactive-label-danger-primary-hover:#fff;--interactive-label-danger-primary-press:#fff;--interactive-label-danger-primary-inactive:#fff;--interactive-label-danger-secondary-default:#e02e2a;--interactive-label-danger-secondary-hover:#fa423e;--interactive-label-danger-secondary-press:#ba2623;--interactive-label-danger-secondary-inactive:#e02e2a;--interactive-icon-primary-default:#fff;--interactive-icon-primary-hover:#fff;--interactive-icon-primary-press:#fff;--interactive-icon-primary-selected:#fff;--interactive-icon-primary-inactive:#fff;--interactive-icon-secondary-default:#0d0d0d;--interactive-icon-secondary-hover:#0d0d0de5;--interactive-icon-secondary-press:#0d0d0dcc;--interactive-icon-secondary-selected:#0d0d0d;--interactive-icon-secondary-inactive:#0d0d0d;--interactive-icon-tertiary-default:#5d5d5d;--interactive-icon-tertiary-hover:#5d5d5d;--interactive-icon-tertiary-press:#5d5d5d;--interactive-icon-tertiary-selected:#5d5d5d;--interactive-icon-tertiary-inactive:#5d5d5d;--interactive-icon-accent-default:#0285ff;--interactive-icon-accent-hover:#0285ff;--interactive-icon-accent-press:#0285ff;--interactive-icon-accent-selected:#0285ff;--interactive-icon-accent-inactive:#0285ff;--interactive-icon-danger-primary-default:#fff;--interactive-icon-danger-primary-hover:#fff;--interactive-icon-danger-primary-press:#fff;--interactive-icon-danger-primary-inactive:#fff;--interactive-icon-danger-secondary-default:#e02e2a;--interactive-icon-danger-secondary-hover:#fa423e;--interactive-icon-danger-secondary-press:#ba2623;--interactive-icon-danger-secondary-inactive:#e02e2a;--utility-scrollbar:#0000000a}.dark{--bg-primary:#212121;--bg-secondary:#303030;--bg-tertiary:#414141;--bg-scrim:#0d0d0d80;--bg-elevated-primary:#303030;--bg-elevated-secondary:#181818;--bg-status-warning:#4a2206;--bg-status-error:#4d100e;--border-default:#ffffff26;--border-heavy:#fff3;--border-light:#ffffff0d;--border-status-warning:#4a2206;--border-status-error:#4d100e;--text-primary:#fff;--text-secondary:#f3f3f3;--text-tertiary:#afafaf;--text-inverted:#0d0d0d;--text-inverted-static:#fff;--text-accent:#66b5ff;--text-status-warning:#ff9e6c;--text-status-error:#ff8583;--icon-primary:#e8e8e8;--icon-secondary:#cdcdcd;--icon-tertiary:#afafaf;--icon-inverted:#0d0d0d;--icon-inverted-static:#fff;--icon-accent:#66b5ff;--icon-status-warning:#ff9e6c;--icon-status-error:#ff8583;--interactive-bg-primary-default:#fff;--interactive-bg-primary-hover:#fffc;--interactive-bg-primary-press:#ffffffe5;--interactive-bg-primary-inactive:#fff;--interactive-bg-primary-selected:#fff;--interactive-bg-secondary-default:#fff0;--interactive-bg-secondary-hover:#ffffff1a;--interactive-bg-secondary-press:#ffffff0d;--interactive-bg-secondary-inactive:#fff0;--interactive-bg-secondary-selected:#ffffff1a;--interactive-bg-tertiary-default:#212121;--interactive-bg-tertiary-hover:#181818;--interactive-bg-tertiary-press:#0d0d0d;--interactive-bg-tertiary-inactive:#212121;--interactive-bg-tertiary-selected:#212121;--interactive-bg-accent-default:#013566;--interactive-bg-accent-hover:#003f7a;--interactive-bg-accent-muted-hover:#3b4045;--interactive-bg-accent-press:#004f99;--interactive-bg-accent-muted-press:#40484f;--interactive-bg-accent-inactive:#013566;--interactive-bg-danger-primary-default:#e02e2a;--interactive-bg-danger-primary-hover:#fa423e;--interactive-bg-danger-primary-press:#ba2623;--interactive-bg-danger-primary-inactive:#e02e2a;--interactive-bg-danger-secondary-default:#fff0;--interactive-bg-danger-secondary-hover:#fff0;--interactive-bg-danger-secondary-press:#fff0;--interactive-bg-danger-secondary-inactive:#fff0;--interactive-border-focus:#fff;--interactive-border-secondary-default:#ffffff26;--interactive-border-secondary-hover:#ffffff26;--interactive-border-secondary-press:#fff3;--interactive-border-secondary-inactive:#ffffff1a;--interactive-border-tertiary-default:#ffffff1a;--interactive-border-tertiary-hover:#ffffff26;--interactive-border-tertiary-press:#ffffff1a;--interactive-border-tertiary-inactive:#ffffff1a;--interactive-border-danger-secondary-default:#fa423e;--interactive-border-danger-secondary-hover:#ff6764;--interactive-border-danger-secondary-press:#e02e2a;--interactive-border-danger-secondary-inactive:#fa423e;--interactive-label-primary-default:#0d0d0d;--interactive-label-primary-hover:#0d0d0d;--interactive-label-primary-press:#0d0d0d;--interactive-label-primary-inactive:#0d0d0d;--interactive-label-primary-selected:#0d0d0d;--interactive-label-secondary-default:#f3f3f3;--interactive-label-secondary-hover:#ffffffe5;--interactive-label-secondary-press:#fffc;--interactive-label-secondary-inactive:#f3f3f3;--interactive-label-secondary-selected:#f3f3f3;--interactive-label-tertiary-default:#cdcdcd;--interactive-label-tertiary-hover:#cdcdcd;--interactive-label-tertiary-press:#cdcdcd;--interactive-label-tertiary-inactive:#cdcdcd;--interactive-label-tertiary-selected:#cdcdcd;--interactive-label-accent-default:#99ceff;--interactive-label-accent-hover:#99ceff;--interactive-label-accent-press:#99ceff;--interactive-label-accent-inactive:#99ceff;--interactive-label-accent-selected:#99ceff;--interactive-label-danger-primary-default:#fff;--interactive-label-danger-primary-hover:#fff;--interactive-label-danger-primary-press:#fff;--interactive-label-danger-primary-inactive:#fff;--interactive-label-danger-secondary-default:#fa423e;--interactive-label-danger-secondary-hover:#ff6764;--interactive-label-danger-secondary-press:#e02e2a;--interactive-label-danger-secondary-inactive:#fa423e;--interactive-icon-primary-default:#0d0d0d;--interactive-icon-primary-hover:#0d0d0d;--interactive-icon-primary-press:#0d0d0d;--interactive-icon-primary-selected:#0d0d0d;--interactive-icon-primary-inactive:#0d0d0d;--interactive-icon-secondary-default:#f3f3f3;--interactive-icon-secondary-hover:#ffffffe5;--interactive-icon-secondary-press:#fffc;--interactive-icon-secondary-selected:#f3f3f3;--interactive-icon-secondary-inactive:#f3f3f3;--interactive-icon-tertiary-default:#cdcdcd;--interactive-icon-tertiary-hover:#cdcdcd;--interactive-icon-tertiary-press:#cdcdcd;--interactive-icon-tertiary-selected:#cdcdcd;--interactive-icon-tertiary-inactive:#cdcdcd;--interactive-icon-accent-default:#99ceff;--interactive-icon-accent-hover:#99ceff;--interactive-icon-accent-press:#99ceff;--interactive-icon-accent-selected:#99ceff;--interactive-icon-accent-inactive:#99ceff;--interactive-icon-danger-primary-default:#fff;--interactive-icon-danger-primary-hover:#fff;--interactive-icon-danger-primary-press:#fff;--interactive-icon-danger-primary-inactive:#fff;--interactive-icon-danger-secondary-default:#fa423e;--interactive-icon-danger-secondary-hover:#ff6764;--interactive-icon-danger-secondary-press:#e02e2a;--interactive-icon-danger-secondary-inactive:#fa423e;--utility-scrollbar:#fff3}html:not(.screen-arch),html:not(.screen-arch) body{background-color:var(--main-surface-primary);height:100%}html.screen-arch,html.screen-arch body{background-color:var(--main-surface-primary);min-height:100%}#__next,#root{height:100%}.markdown{max-width:unset}.markdown.streaming-animation.block-entry-animation pre,.markdown.streaming-animation.block-entry-animation table{overflow:clip!important;position:relative}:is(.markdown.streaming-animation.block-entry-animation pre,.markdown.streaming-animation.block-entry-animation table):after{--overlap-distance:10px;--overlap-negative-distance:-10px;content:"";display:flex;height:calc(100% + var(--overlap-distance)*2);inset:0;position:absolute;translate:0 var(--streaming-reveal-amount,var(--overlap-negative-distance))}[dir=ltr] :is(.markdown.streaming-animation.block-entry-animation pre,.markdown.streaming-animation.block-entry-animation table):after{background-image:linear-gradient(180deg,transparent,var(--main-surface-primary)var(--overlap-distance))}[dir=rtl] :is(.markdown.streaming-animation.block-entry-animation pre,.markdown.streaming-animation.block-entry-animation table):after{background-image:linear-gradient(-180deg,transparent,var(--main-surface-primary)var(--overlap-distance))}@media (prefers-reduced-motion:no-preference){:is(.markdown.streaming-animation.block-entry-animation pre,.markdown.streaming-animation.block-entry-animation table):after{transition:.5s translate var(--spring-standard)}}.markdown.streaming-animation h1,.markdown.streaming-animation h2,.markdown.streaming-animation h3,.markdown.streaming-animation h4,.markdown.streaming-animation h5,.markdown.streaming-animation h6,.markdown.streaming-animation li:not(:has(pre)){width:fit-content}.markdown pre{margin-top:calc(var(--spacing)*2)}.markdown pre:first-child{margin-top:calc(var(--spacing)*0)}.markdown h1{--tw-font-weight:var(--font-weight-bold);--tw-tracking:-.04rem;font-weight:var(--font-weight-bold);letter-spacing:-.04rem}.markdown h1:first-child{margin-top:calc(var(--spacing)*0)}.markdown h2{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);margin-bottom:calc(var(--spacing)*4);margin-top:calc(var(--spacing)*8)}.markdown h2:first-child{margin-top:calc(var(--spacing)*0)}.markdown h3{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);margin-bottom:calc(var(--spacing)*2);margin-top:calc(var(--spacing)*4)}.markdown h3:first-child{margin-top:calc(var(--spacing)*0)}.markdown h4{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);margin-bottom:calc(var(--spacing)*2);margin-top:calc(var(--spacing)*4)}.markdown h4:first-child{margin-top:calc(var(--spacing)*0)}.markdown h5{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.markdown h5:first-child{margin-top:calc(var(--spacing)*0)}.markdown blockquote{--tw-leading:calc(var(--spacing)*6);border-style:var(--tw-border-style);border-width:0;line-height:calc(var(--spacing)*6);margin:calc(var(--spacing)*0);padding-block:calc(var(--spacing)*2);position:relative}[dir=ltr] .markdown blockquote{padding-left:calc(var(--spacing)*6)}[dir=rtl] .markdown blockquote{padding-right:calc(var(--spacing)*6)}.markdown blockquote>p{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);margin:calc(var(--spacing)*0)}.markdown blockquote>p:after,.markdown blockquote>p:before{display:none}.markdown blockquote:after{background-color:var(--border-medium);border-radius:2px;bottom:.5rem;content:"";position:absolute;top:.5rem;width:4px}[dir=ltr] .markdown blockquote:after{left:0}[dir=rtl] .markdown blockquote:after{right:0}.markdown p{margin-bottom:.5rem}.markdown p:not(:first-child){margin-top:.5rem}.markdown p+:where(ol,ul){margin-top:0}.markdown :where(ol,ul)>li>:last-child{margin-bottom:0}.markdown :where(ol,ul)>li>:first-child{margin-bottom:0;margin-top:0}.markdown table{--tw-border-spacing-x:calc(var(--spacing)*0);--tw-border-spacing-y:calc(var(--spacing)*0);border-collapse:separate;border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y);margin:calc(var(--spacing)*0)}.markdown table [data-col-size=sm]{max-width:calc(var(--thread-content-max-width)*6/24);min-width:calc(var(--thread-content-max-width)*4/24)}.markdown table [data-col-size=md]{max-width:calc(var(--thread-content-max-width)*8/24);min-width:calc(var(--thread-content-max-width)*6/24)}.markdown table [data-col-size=lg]{max-width:calc(var(--thread-content-max-width)*12/24);min-width:calc(var(--thread-content-max-width)*8/24)}.markdown table [data-col-size=xl]{max-width:calc(var(--thread-content-max-width)*18/24);min-width:calc(var(--thread-content-max-width)*14/24)}.markdown th{--tw-leading:calc(var(--spacing)*4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border-medium);line-height:calc(var(--spacing)*4);padding-block:calc(var(--spacing)*2)}.markdown th:not(:last-child){padding-inline-end:calc(var(--spacing)*6)}.markdown tr:not(:last-child) td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border-light)}.markdown tr:last-child td{padding-bottom:calc(var(--spacing)*6)}.markdown td{padding-block:calc(var(--spacing)*2.5)}.markdown td:not(:last-child){padding-inline-end:calc(var(--spacing)*6)}.markdown ol,.markdown ul{margin-bottom:calc(var(--spacing)*4)}.markdown li::marker{--tw-font-weight:var(--font-weight-bold);color:var(--text-secondary);font-weight:var(--font-weight-bold)}.markdown a{--tw-font-weight:var(--font-weight-normal);color:var(--link);font-weight:var(--font-weight-normal);text-decoration-line:none}@media (hover:hover){.markdown a:hover{color:var(--link-hover)}}.gizmo .markdown>:not(pre),.gizmo .markdown>:not(pre)>:not(a){color:var(--text-primary)}.markdown .float-image+p{margin-top:calc(var(--spacing)*0)}.markdown hr{border-color:var(--border-light);margin-block:calc(var(--spacing)*10)}.deep-research-result p{display:inline-block;vertical-align:top;width:100%}@keyframes blink{to{visibility:hidden}}@keyframes show{0%{opacity:0}to{opacity:1}}.result-streaming>:not(ol,ul,pre,div):last-child:after,.result-streaming>pre:last-child code:after{content:"●";font-family:Circle,system-ui,sans-serif;line-height:normal;vertical-align:baseline}.result-streaming.no-flow>:not(ol,ul,pre):last-child:after,.result-streaming.no-flow>pre:last-child code:after{margin-top:.25rem;position:absolute}.pulse>:not(ol,ul,pre,div):last-child:after,.pulse>pre:last-child code:after{-webkit-font-smoothing:subpixel-antialiased;animation:pulseSize 1.25s ease-in-out infinite;backface-visibility:hidden;border-radius:50%;content:"●";display:inline-block;transform:translateZ(0);transform-origin:50%;will-change:transform}[dir=ltr] .pulse>:not(ol,ul,pre,div):last-child:after,[dir=ltr] .pulse>pre:last-child code:after{padding-left:.1em}[dir=rtl] .pulse>:not(ol,ul,pre,div):last-child:after,[dir=rtl] .pulse>pre:last-child code:after{padding-right:.1em}.result-thinking p:last-child:after{-webkit-font-smoothing:subpixel-antialiased;animation:pulseSize 1.25s ease-in-out infinite;backface-visibility:hidden;background-color:var(--text-primary);border-radius:50%;box-sizing:border-box;content:" ";display:block;height:12px;position:absolute;top:11px;transform:translateZ(0);transform-origin:50%;width:12px;will-change:transform}:root{--sharp-edge-top-shadow:0 1px 0 var(--border-sharp);--sharp-edge-top-shadow-placeholder:0 1px 0 transparent;--sharp-edge-bottom-shadow:0 -1px 0 var(--border-sharp);--sharp-edge-bottom-shadow-placeholder:0 -1px 0 transparent}@keyframes add-top-shadow{0%{box-shadow:var(--sharp-edge-top-shadow-placeholder)}.1%,to{box-shadow:var(--sharp-edge-top-shadow)}}@keyframes add-bottom-shadow{0%,99.9%{box-shadow:var(--sharp-edge-bottom-shadow)}to{box-shadow:var(--sharp-edge-bottom-shadow-placeholder)}}.sharp-edge-on-scroll-start{box-shadow:0 1px #0000}@supports (animation-timeline:--agi){.sharp-edge-on-scroll-start{animation-range:0 1px;animation:add-top-shadow 1ms linear both}}.sharp-edge-on-scroll-end{box-shadow:0 -1px #0000}.sharp-edge-on-scroll-end,.sharp-edge-on-scroll-start{animation-timeline:scroll()}@keyframes shimmer-skeleton{0%{background-position:100%}to{background-position:0}}@supports selector(:has(*)){.result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child:not(:has(*>li)):after,.result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after,.result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after{content:"●";font-family:Circle,system-ui,sans-serif;line-height:normal;vertical-align:baseline}[dir=ltr] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child:not(:has(*>li)):after,[dir=ltr] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after,[dir=ltr] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after{margin-left:.25rem}[dir=rtl] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child:not(:has(*>li)):after,[dir=rtl] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after,[dir=rtl] .result-streaming:not(.streaming-animation)>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child>:is(ul,ol):last-child>li:last-child:after{margin-right:.25rem}}@supports not selector(:has(*)){.result-streaming>ol:last-child>li:last-child:after,.result-streaming>ul:last-child>li:last-child:after{content:"●";font-family:Circle,system-ui,sans-serif;line-height:normal;vertical-align:baseline}[dir=ltr] .result-streaming>ol:last-child>li:last-child:after,[dir=ltr] .result-streaming>ul:last-child>li:last-child:after{margin-left:.25rem}[dir=rtl] .result-streaming>ol:last-child>li:last-child:after,[dir=rtl] .result-streaming>ul:last-child>li:last-child:after{margin-right:.25rem}}.result-streaming .katex-error{display:none}@keyframes pulse-dot{to{transform:scale(var(--pulse-scale,1.3))}}@keyframes float-sidebar-in{0%{opacity:0;translate:-60%}70%{opacity:1}to{translate:0}}@keyframes float-sidebar-out{0%{translate:0}30%{opacity:1}to{opacity:0;translate:-60%}}.pulsing-dot{aspect-ratio:1;background:var(--dot-color);border-radius:50%;opacity:var(--dot-opacity,1);width:1rem}@media (prefers-reduced-motion:no-preference){.pulsing-dot{animation:pulse-dot 1s infinite var(--easing-common)alternate-reverse;transition:.2s opacity var(--easing-common);translate:0 3px}}@keyframes pulseSize{0%,to{transform:scale(1)}50%{transform:scale(1.25)}}@keyframes toast-open{0%{opacity:0;transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes toast-close{0%{opacity:1}to{opacity:0}}.toast-root{align-items:center;display:flex;flex-direction:column;height:0;transition:all .24s cubic-bezier(0,0,.2,1)}.toast-root[data-state=entered],.toast-root[data-state=entering]{animation:toast-open .24s cubic-bezier(.175,.885,.32,1) both}.toast-root[data-state=exiting]{animation:toast-close .12s cubic-bezier(.4,0,1,1) both}.toast-root .alert-root{box-shadow:0 0 1px #435a6f4d,0 5px 8px -4px #435a6f4d;flex-shrink:0;pointer-events:all}.title{font-feature-settings:normal;font-family:ui-sans-serif,system-ui,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal}.icon-shimmer{animation:icon-shimmer 5s cubic-bezier(.2,.44,.38,1.02) infinite;background-repeat:no-repeat;-webkit-mask:linear-gradient(80deg,currentColor 30%,#0005,currentColor 70%) 100%/300% 100%;mask:linear-gradient(80deg,currentColor 30%,#0005,currentColor 70%) 100%/300% 100%}@keyframes icon-shimmer{0%{-webkit-mask-position:100%;mask-position:100%}20%{-webkit-mask-position:0;mask-position:0}to{-webkit-mask-position:0;mask-position:0}}@keyframes loading-results-shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}@keyframes diagonalSweep{0%{transform:translate(-100%,-100%)}to{transform:translate(100%,100%)}}.diagonal-sweep-gradient{animation:diagonalSweep 4s ease-out infinite}[dir=ltr] .diagonal-sweep-gradient{background-image:linear-gradient(135deg,#fff0 46%,#fff3,#fff0 54%)}[dir=rtl] .diagonal-sweep-gradient{background-image:linear-gradient(-135deg,#fff0 46%,#fff3,#fff0 54%)}.loading-results-shimmer{animation:loading-results-shimmer 3s linear infinite;background:var(--main-surface-secondary)gradient(linear,100% 0,0 0,from(var(--main-surface-secondary)),color-stop(.5,var(--main-surface-tertiary)),to(var(--main-surface-secondary)));background:var(--main-surface-secondary)-webkit-gradient(linear,100% 0,0 0,from(var(--main-surface-secondary)),color-stop(.5,var(--main-surface-tertiary)),to(var(--main-surface-secondary)));background-size:1000px 100%}.hint-pill{--tw-font-weight:var(--font-weight-semibold);color:var(--hint-text);font-weight:var(--font-weight-semibold)}@keyframes loading-shimmer{0%{background-position:-100% 0}to{background-position:250% 0}}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.gizmo-bot-avatar{outline:solid 1px var(--main-surface-tertiary)}[dir=ltr] div[data-radix-popper-content-wrapper]:has(>div[data-side=right]){left:min(0px,var(--radix-popper-available-width) + (-1*var(--radix-popper-anchor-width)))!important}[dir=rtl] div[data-radix-popper-content-wrapper]:has(>div[data-side=right]){right:min(0px,var(--radix-popper-available-width) + (-1*var(--radix-popper-anchor-width)))!important}[dir=ltr] div[data-radix-popper-content-wrapper]:has(>div[data-side=left]){left:max(0px,var(--radix-popper-anchor-width) + 4px)!important}[dir=rtl] div[data-radix-popper-content-wrapper]:has(>div[data-side=left]){right:max(0px,var(--radix-popper-anchor-width) + 4px)!important}@media not all and (min-width:342px){:where(div[data-radix-popper-content-wrapper]:has(>div[data-side]) [data-radix-collection-item]){max-width:85dvw}}#sidebar-summarizer p:not(:first-child){margin-top:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-border-spacing-x{syntax:"";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-contain-size{syntax:"*";inherits:false}@property --tw-contain-layout{syntax:"*";inherits:false}@property --tw-contain-paint{syntax:"*";inherits:false}@property --tw-contain-style{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes pulsing{0%{opacity:1;scale:1}50%{opacity:.9;scale:.875}to{opacity:1;scale:1}}@font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_AMS-Regular-e1why8ff.woff2) format("woff2")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_Caligraphic-Bold-n63xiolk.woff2) format("woff2")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Caligraphic-Regular-npwmqylf.woff2) format("woff2")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_Fraktur-Bold-ikhebgtj.woff2) format("woff2")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Fraktur-Regular-i0egury6.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_Main-Bold-ktk38ybk.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_Main-BoldItalic-oj033t4i.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Main-Italic-2p4bq1jf.woff2) format("woff2")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Main-Regular-hbkzldb8.woff2) format("woff2")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_Math-BoldItalic-jdo1yxu8.woff2) format("woff2")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Math-Italic-cz4b2ure.woff2) format("woff2")}@font-face{font-family:KaTeX_SansSerif;font-style:normal;font-weight:700;src:url(https://cdn.oaistatic.com/assets/KaTeX_SansSerif-Bold-otxc8itm.woff2) format("woff2")}@font-face{font-family:KaTeX_SansSerif;font-style:italic;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_SansSerif-Italic-k4kksncm.woff2) format("woff2")}@font-face{font-family:KaTeX_SansSerif;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_SansSerif-Regular-ltw53ck4.woff2) format("woff2")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Script-Regular-oybd33cp.woff2) format("woff2")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Size1-Regular-cjccv44r.woff2) format("woff2")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Size2-Regular-onxq3bzc.woff2) format("woff2")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAA4oAA4AAAAAHbQAAA3TAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgRQIDgmcDBEICo1oijYBNgIkA14LMgAEIAWJAAeBHAyBHBvbGiMRdnO0IkRRkiYDgr9KsJ1NUAf2kILNxgUmgqIgq1P89vcbIcmsQbRps3vCcXdYOKSWEPEKgZgQkprQQsxIXUgq0DqpGKmIvrgkeVGtEQD9DzAO29fM9jYhxZEsL2FeURH2JN4MIcTdO049NCVdxQ/w9NrSYFEBKTDKpLKfNkCGDc1RwjZLQcm3vqJ2UW9Xfa3tgAHz6ivp6vgC2yD4/6352ndnN0X0TL7seypkjZlMsjmZnf0Mm5Q+JykRWQBKCVCVPbARPXWyQtb5VgLB6Biq7/Uixcj2WGqdI8tGSgkuRG+t910GKP2D7AQH0DB9FMDW/obJZ8giFI3Wg8Cvevz0M+5m0rTh7XDBlvo9Y4vm13EXmfttwI4mBo1EG15fxJhUiCLbiiyCf/ZA6MFAhg3pGIZGdGIVjtPn6UcMk9A/UUr9PhoNsCENw1APAq0gpH73e+M+0ueyHbabc3vkbcdtzcf/fiy+NxQEjf9ud/ELBHAXJ0nk4z+MXH2Ev/kWyV4k7SkvpPc9Qr38F6RPWnM9cN6DJ0AdD1BhtgABtmoRoFCvPsBAumNm6soZG2Gk5GyVTo2sJncSyp0jQTYoR6WDvTwaaEcHsxHfvuWhHA3a6bN7twRKtcGok6NsCi7jYRrM2jExsUFMxMQYuJbMhuWNOumEJy9hi29Dmg5zMp/A5+hhPG19j1vBrq8JTLr8ki5VLPmG/PynJHVul440bxg5xuymHUFPBshC+nA9I1FmwbRBTNHAcik3Oae0cxKoI3MOriM42UrPe51nsaGxJ+WfXubAsP84aabUlQSJ1IiE0iPETLUU4CATgfXSCSpuRFRmCGbO+wSpAnzaeaCYW1VNEysRtuXCEL1kUFUbbtMv3Tilt/1c11jt3Q5bbMa84cpWipp8Elw3MZhOHsOlwwVUQM3lAR35JiFQbaYCRnMF2lxAWoOg2gyoIV4PouX8HytNIfLhqpJtXB4vjiViUI8IJ7bkC4ikkQvKksnOTKICwnqWSZ9YS5f0WCxmpgjbIq7EJcM4aI2nmhLNY2JIUgOjXZFWBHb+x5oh6cwb0Tv1ackHdKi0I9OO2wE9aogIOn540CCCziyhN+IaejtgAONKznHlHyutPrHGwCx9S6B8kfS4Mfi4Eyv7OU730bT1SCBjt834cXsf43zVjPUqqJjgrjeGnBxSG4aYAKFuVbeCfkDIjAqMb6yLNIbCuvXhMH2/+k2vkNpkORhR59N1CkzoOENvneIosjYmuTxlhUzaGEJQ/iWqx4dmwpmKjrwTiTGTCVozNAYqk/zXOndWxuWSmJkQpJw3pK5KX6QrLt5LATMqpmPAQhkhK6PUjzHUn7E0gHE0kPE0iKkolgkUx9SZmVAdDgpffdyJKg3k7VmzYGCwVXGz/tXmkOIp+vcWs+EMuhhvN0h9uhfzWJziBQmCREGSIFmQIkgVpAnSBRmC//6hkLZwaVhwxlrJSOdqlFtOYxlau9F2QN5Y98xmIAsiM1HVp2VFX+DHHGg6Ecjh3vmqtidX3qHI2qycTk/iwxSt5UzTmEP92ZBnEWTk4Mx8Mpl78ZDokxg/KWb+Q0QkvdKVmq3TMW+RXEgrsziSAfNXFMhDc60N5N9jQzjfO0kBKpUZl0ZmwJ41j/B9Hz6wmRaJB84niNmQrzp9eSlQCDDzazGDdVi3P36VZQ+Jy4f9UBNp+3zTjqI4abaFAm+GShVaXlsGdF3FYzZcDI6cori4kMxUECl9IjJZpzkvitAoxKue+90pDMvcKRxLl53TmOKCmV/xRolNKSqqUxc6LStOETmFOiLZZptlZepcKiAzteG8PEdpnQpbOMNcMsR4RR2Bs0cKFEvSmIjAFcnarqwUL4lDhHmnVkwu1IwshbiCcgvOheZuYyOteufZZwlcTlLgnZ3o/WcYdzZHW/WGaqaVfmTZ1aWCceJjkbZqsfbkOtcFlUZM/jy+hXHDbaUobWqqXaeWobbLO99yG5N3U4wxco0rQGGcOLASFMXeJoham8M+/x6O2WywK2l4HGbq1CoUyC/IZikQhdq3SiuNrvAEj0AVu9x2x3lp/xWzahaxidezFVtdcb5uEnzyl0ZmYiuKI0exvCd4Xc9CV1KB0db00z92wDPde0kukbvZIWN6jUWFTmPIC/Y4UPCm8UfDTFZpZNon1qLFTkBhxzB+FjQRA2Q/YRJT8pQigslMaUpFyAG8TMlXigiqmAZX4xgijKjRlGpLE0GdplRfCaJo0JQaSxNBk6ZmMzcya0FmrcisDdn0Q3HI2sWSppYigmlM1XT/kLQZSNpMJG0WkjYbSZuDpM1F0uYhFc1HxU4m1QJjDK6iL0S5uSj5rgXc3RejEigtcRBtqYPQsiTskmO5vosV+q4VGIKbOkDg0jtRrq+Em1YloaTFar3EGr1EUC8R0kus1Uus00usL97ABr2BjXoDm/QGNhuWtMVBKOwg/i78lT7hBsAvDmwHc/ao3vmUbBmhjeYySZNWvGkfZAgISDSaDo1SVpzGDsAEkF8B+gEapViUoZgUWXcRIGFZNm6gWbAKk0bp0k1MHG9fLYtV4iS2SmLEQFARzRcnf9PUS0LVn05/J9MiRRBU3v2IrvW974v4N00L7ZMk0wXP1409CHo/an8zTRHD3eSJ6m8D4YMkZNl3M79sqeuAsr/m3f+8/yl7A50aiAEJgeBeMWzu7ui9UfUBCe2TIqZIoOd/3/udRBOQidQZUERzb2/VwZN1H/Sju82ew2H2Wfr6qvfVf3hqwDvAIpkQVFy4B9Pe9e4/XvPeceu7h3dvO56iJPf0+A6cqA2ip18ER+iFgggiuOkvj24bby0N9j2UHIkgqIt+sVgfodC4YghLSMjSZbH0VR/6dMDrYJeKHilKTemt6v6kvzvn3/RrdWtr0GoN/xL+Sex/cPYLUpepx9cz/D46UPU5KXgAQa+NDps1v6J3xP1i2HtaDB0M9aX2deA7SYff//+gUCovMmIK/qfsFcOk+4Y5ZN97XlG6zebqtMbKgeRFi51vnxTQYBUik2rS/Cn6PC8ADR8FGxsRPB82dzfND90gIcshOcYUkfjherBz53odpm6TP8txlwOZ71xmfHHOvq053qFF/MRlS3jP0ELudrf2OeN8DHvp6ZceLe8qKYvWz/7yp0u4dKPfli3CYq0O13Ih71mylJ80tOi10On8wi+F4+LWgDPeJ30msSQt9/vkmHq9/Lvo2b461mP801v3W4xTcs6CbvF9UDdrSt+A8OUbpSh55qAUFXWznBBfdeJ8a4d7ugT5tvxUza3h9m4H7ptTqiG4z0g5dc0X29OcGlhpGFMpQo9ytTS+NViZpNdvU4kWx+LKxNY10kQ1yqGXrhe4/1nvP7E+nd5A92TtaRplbHSqoIdOqtRWti+fkB5/n1+/VvCmz12pG1kpQWsfi1ftlBobm0bpngs16CHkbIwdLnParxtTV3QYRlfJ0KFskH7pdN/YDn+yRuSd7sNH3aO0DYPggk6uWuXrfOc+fa3VTxFVvKaNxHsiHmsXyCLIE5yuOeN3/Jdf8HBL/5M6shjyhxHx9BjB1O0+4NLOnjLLSxwO7ukN4jMbOIcD879KLSi6Pk61Oqm2377n8079PXEEQ7cy7OKEC9nbpet118fxweTafpt69x/Bt8UqGzNQt7aelpc44dn5cqhwf71+qKp/Zf/+a0zcizOUWpl/iBcSXip0pplkatCchoH5c5aUM8I7/dWxAej8WicPL1URFZ9BDJelUwEwTkGqUhgSlydVes95YdXvhh9Gfz/aeFWvgVb4tuLbcv4+wLdutVZv/cUonwBD/6eDlE0aSiKK/uoH3+J1wDE/jMVqY2ysGufN84oIXB0sPzy8ollX/LegY74DgJXJR57sn+VGza0x3DnuIgABFM15LmajjjsNlYj+JEZGbuRYcAMOWxFkPN2w6Wd46xo4gVWQR/X4lyI/R6K/YK0110GzudPRW7Y+UOBGTfNNzHeYT0fiH0taunBpq9HEW8OKSaBGj21L0MqenEmNRWBAWDWAk4CpNoEZJ2tTaPFgbQYj8HxtFilErs3BTRwT8uO1NXQaWfIotchmPkAF5mMBAliEmZiOGVgCG9LgRzpscMAOOwowlT3JhusdazXGSC/hxR3UlmWVwWHpOIKheqONvjyhSiTHIkVUco5bnji8m//zL7PKaT1Vl5I6UE609f+gkr6MZKVyKc7zJRmCahLsdlyA5fdQkRSan9LgnnLEyGSkaKJCJog0wAgvepWBt80+1yKln1bMVtCljfNWDueKLsWwaEbBSfSPTEmVRsUcYYMnEjcjeyCZzBXK9E9BYBXLKjOSpUDR+nEV3TFSUdQaz+ot98QxgXwx0GQ+EEUAKB2qZPkQQ0GqFD8UPFMqyaCHM24BZmSGic9EYMagKizOw9Hz50DMrDLrqqLkTAhplMictiCAx5S3BIUQdeJeLnBy2CNtMfz6cV4u8XKoFZQesbf9YZiIERiHjaNodDW6LgcirX/mPnJIkBGDUpTBhSa0EIr38D5hCIszhCM8URGBqImoWjpvpt1ebu/v3Gl3qJfMnNM+9V+kiRFyROTPHQWOcs1dNW94/ukKMPZBvDi55i5CttdeJz84DLngLqjcdwEZ87bFFR8CIG35OAkDVN6VRDZ7aq67NteYqZ2lpT8oYB2CytoBd6VuAx4WgiAsnuj3WohG+LugzXiQRDeM3XYXlULv4dp5VFYC) format("woff2")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Size4-Regular-nv9nppzf.woff2) format("woff2")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(https://cdn.oaistatic.com/assets/KaTeX_Typewriter-Regular-iqvr3vwu.woff2) format("woff2")}.katex{font: 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentcolor}.katex .katex-version:after{content:"0.16.0"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}[dir=ltr] .katex .vlist-t2{margin-right:-2px}[dir=rtl] .katex .vlist-t2{margin-left:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{max-width:0;width:0}[dir=ltr] .katex .msupsub{text-align:left}[dir=rtl] .katex .msupsub{text-align:right}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}[dir=ltr] .katex .llap>.inner{right:0}[dir=ltr] .katex .clap>.inner,[dir=ltr] .katex .rlap>.inner,[dir=rtl] .katex .llap>.inner{left:0}[dir=rtl] .katex .clap>.inner,[dir=rtl] .katex .rlap>.inner{right:0}[dir=ltr] .katex .clap>.inner>span{margin-left:-50%;margin-right:50%}[dir=rtl] .katex .clap>.inner>span{margin-left:50%;margin-right:-50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}[dir=ltr] .katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}[dir=rtl] .katex .sqrt>.root{margin-left:-.55555556em;margin-right:.27777778em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}[dir=ltr] .katex .mtable .col-align-l>.vlist-t{text-align:left}[dir=ltr] .katex .mtable .col-align-r>.vlist-t,[dir=rtl] .katex .mtable .col-align-l>.vlist-t{text-align:right}[dir=ltr] .katex .svg-align,[dir=rtl] .katex .mtable .col-align-r>.vlist-t{text-align:left}[dir=rtl] .katex .svg-align{text-align:right}.katex svg{fill:currentcolor;stroke:currentcolor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{overflow:hidden;position:absolute;width:50.2%}[dir=ltr] .katex .halfarrow-left{left:0}[dir=rtl] .katex .halfarrow-left{right:0}.katex .halfarrow-right{overflow:hidden;position:absolute;width:50.2%}[dir=ltr] .katex .halfarrow-right{right:0}[dir=rtl] .katex .halfarrow-right{left:0}.katex .brace-left{overflow:hidden;position:absolute;width:25.1%}[dir=ltr] .katex .brace-left{left:0}[dir=rtl] .katex .brace-left{right:0}.katex .brace-center{overflow:hidden;position:absolute;width:50%}[dir=ltr] .katex .brace-center{left:25%}[dir=rtl] .katex .brace-center{right:25%}.katex .brace-right{overflow:hidden;position:absolute;width:25.1%}[dir=ltr] .katex .brace-right{right:0}[dir=rtl] .katex .brace-right{left:0}.katex .x-arrow-pad{padding:0 .5em}[dir=ltr] .katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}[dir=rtl] .katex .cd-arrow-pad{padding:0 .27778em 0 .55556em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-top:.049em solid;box-sizing:border-box}[dir=ltr] .katex .angl{border-right:.049em solid;margin-right:.03889em}[dir=rtl] .katex .angl{border-left:.049em solid;margin-left:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute}[dir=ltr] .katex .cd-label-left{right:calc(50% + .3em);text-align:left}[dir=rtl] .katex .cd-label-left{left:calc(50% + .3em);text-align:right}.katex .cd-label-right{display:inline-block;position:absolute}[dir=ltr] .katex .cd-label-right{left:calc(50% + .3em);text-align:right}[dir=rtl] .katex .cd-label-right{right:calc(50% + .3em);text-align:left}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute}[dir=ltr] .katex-display>.katex>.katex-html>.tag{right:0}[dir=rtl] .katex-display>.katex>.katex-html>.tag{left:0}[dir=ltr] .katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}[dir=rtl] .katex-display.leqno>.katex>.katex-html>.tag{left:auto;right:0}[dir=ltr] .katex-display.fleqn>.katex{padding-left:2em;text-align:left}[dir=rtl] .katex-display.fleqn>.katex{padding-right:2em;text-align:right}body{counter-reset:katexEqnNo mmlEqnNo}.sdtrn-root{background:unset!important;font-size:14px;line-height:20px}.sdtrn-root .draggable{app-region:drag}.sdtrn-root .no-draggable{app-region:no-drag}.sdtrn-root .no-draggable-children *{app-region:no-drag;-webkit-user-select:none;user-select:none}.sdtrn-root [data-radix-popper-content-wrapper],.sdtrn-root [role=dialog]{app-region:no-drag}.sdtrn-root [data-radix-popper-content-wrapper] li a,.sdtrn-root [role=button],.sdtrn-root button,.sdtrn-root div[role=menu],.sdtrn-root input[type=button],.sdtrn-root input[type=reset],.sdtrn-root input[type=submit],.sdtrn-root label{cursor:default}.sdtrn-root nav a{cursor:default;-webkit-user-select:none;user-select:none}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-font-weight:initial}}}.snc-root{font-size:var(--snc-text-base)}.snc{--snc-1:1.5rem;--snc-2:1.75rem;--snc-3:3rem;--snc-results-padding:1rem;--snc-input-height:2.875rem;--snc-text-base:16px;--snc-hover:#00000008;--main-surface-secondary:#f7f7f7;--main-surface-tertiary:#f1f1f1;--text-secondary:var(--gray-600);--text-tertiary:var(--gray-500);--snc-result-search-input-shadow:0 12px 20px -8px;--snc-answer-followup-input-shadow:0 -12px 20px -8px;--snc-input-shadow-mult:0}.snc .snc-accent-border{border:0}.snc ::-webkit-scrollbar{width:16px}[dir=ltr] .snc ::-webkit-scrollbar{background:0 0}[dir=rtl] .snc ::-webkit-scrollbar{background:100% 0}.snc ::-webkit-scrollbar-thumb{background:var(--main-surface-tertiary);border:4px solid var(--main-surface-primary);border-radius:8px}.snc ::-webkit-scrollbar-thumb:hover{background:var(--gray-200)}.dark .snc ::-webkit-scrollbar-thumb:hover{background:var(--gray-600)}@screen sm{--snc-input-shadow-mult:.1;--snc-input-height:3.25rem}.dark .snc,.dark .snc .dark{--snc-hover:#ffffff08;--snc-focus-border:#ffffffbf;--main-surface-primary:var(--gray-950);--main-surface-secondary:var(--gray-900);--main-surface-tertiary:var(--gray-700);--text-secondary:var(--gray-300);--text-tertiary:var(--gray-500);--border-light:#ffffff0d;--border-medium:#ffffff1f;--border-xheavy:#fff3;--gray-950:#141414;--snc-result-search-input-shadow:0 8px 12px -8px;--snc-answer-followup-input-shadow:0 -8px 12px -8px}:is(.dark .snc .dark,.dark .snc) .snc-accent-border{border:.5px solid var(--border-xheavy)}@screen sm{--snc-input-shadow-mult:1}.snc .user-query .prose p:first-child{margin-bottom:calc(var(--spacing,.25rem)*0)}.snc .prose :not(.not-prose,.not-prose *){max-width:100%}.snc .prose :not(.not-prose,.not-prose *) a{--tw-font-weight:var(--font-weight-normal,400);color:var(--link);font-weight:var(--font-weight-normal,400);text-decoration-line:none;text-underline-offset:2px}.snc .prose :not(.not-prose,.not-prose *)>h1:first-child{font-size:1.125rem}@screen 2xl{font-size:1.25rem}.snc .prose :not(.not-prose,.not-prose *) h1,.snc .prose :not(.not-prose,.not-prose *) h2,.snc .prose :not(.not-prose,.not-prose *) h3,.snc .prose :not(.not-prose,.not-prose *) h4,.snc .prose :not(.not-prose,.not-prose *) h5{--tw-font-weight:var(--font-weight-semibold,600);font-size:1rem;font-weight:var(--font-weight-semibold,600)}.snc .prose :not(.not-prose,.not-prose *) h3,.snc .prose :not(.not-prose,.not-prose *) h4{margin-bottom:calc(var(--spacing,.25rem)*1)}.snc .prose :not(.not-prose,.not-prose *) h3:first-child a{font-size:1.25rem;font-weight:500}.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) p,.snc .prose :not(.not-prose,.not-prose *) ul{margin-bottom:calc(var(--spacing,.25rem)*4)}:is(.snc .prose :not(.not-prose,.not-prose *) p,.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul):last-child{margin-bottom:calc(var(--spacing,.25rem)*0)}.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul{display:contents;list-style-position:inside;list-style-type:none;margin-top:calc(var(--spacing,.25rem)*0)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li{position:relative}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li:has(.title-citation){margin-bottom:calc(var(--spacing,.25rem)*3)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li:has(.title-citation):last-child{margin-bottom:calc(var(--spacing,.25rem)*0)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li:before{position:absolute}[dir=ltr] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li:before{left:calc(var(--spacing,.25rem)*0)}[dir=rtl] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li:before{right:calc(var(--spacing,.25rem)*0)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li button,:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li>a{margin-block:calc(var(--spacing,.25rem)*0)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li p{margin-bottom:calc(var(--spacing,.25rem)*2);margin-top:calc(var(--spacing,.25rem)*0)}:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ol,:is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ul{display:block;margin-block:calc(var(--spacing,.25rem)*0);padding:calc(var(--spacing,.25rem)*0)}[dir=ltr] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ol,[dir=ltr] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ul{margin-left:calc(var(--spacing,.25rem)*4)}[dir=rtl] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ol,[dir=rtl] :is(.snc .prose :not(.not-prose,.not-prose *) ol,.snc .prose :not(.not-prose,.not-prose *) ul)>li ul{margin-right:calc(var(--spacing,.25rem)*4)}.snc .prose :not(.not-prose,.not-prose *) ol{counter-reset:list-counter}.snc .prose :not(.not-prose,.not-prose *) ol>li{counter-increment:list-counter}[dir=ltr] .snc .prose :not(.not-prose,.not-prose *) ol>li{padding-left:calc(var(--spacing,.25rem)*8)}[dir=rtl] .snc .prose :not(.not-prose,.not-prose *) ol>li{padding-right:calc(var(--spacing,.25rem)*8)}.snc .prose :not(.not-prose,.not-prose *) ol>li:before{color:var(--text-secondary);content:counter(list-counter)"."}[dir=ltr] .snc .prose :not(.not-prose,.not-prose *) ul>li{padding-left:calc(var(--spacing,.25rem)*6)}[dir=rtl] .snc .prose :not(.not-prose,.not-prose *) ul>li{padding-right:calc(var(--spacing,.25rem)*6)}.snc .prose :not(.not-prose,.not-prose *) ul>li:before{content:"•"}.snc .prose :not(.not-prose,.not-prose *) strong{--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.snc .prose.result-streaming .context-list:last-child:has(p):after{display:none}.snc .prose.result-streaming .context-list:last-child p:last-child:after{content:"●";display:inline;font-family:Circle,system-ui,sans-serif;line-height:normal;vertical-align:baseline}[dir=ltr] .snc .prose.result-streaming .context-list:last-child p:last-child:after{margin-left:.25rem}[dir=rtl] .snc .prose.result-streaming .context-list:last-child p:last-child:after{margin-right:.25rem}@property --tw-font-weight{syntax:"*";inherits:false}code,pre{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace!important}code[class=language-plaintext]{white-space:pre-line}code.hljs,code[class*=language-],pre[class*=language-]{word-wrap:normal;background:none;-webkit-hyphens:none;hyphens:none;line-height:1.5;tab-size:4;white-space:pre;word-break:normal;word-spacing:normal}[dir=ltr] code.hljs,[dir=ltr] code[class*=language-],[dir=ltr] pre[class*=language-]{text-align:left}[dir=rtl] code.hljs,[dir=rtl] code[class*=language-],[dir=rtl] pre[class*=language-]{text-align:right}pre[class*=language-]{border-radius:.3em;overflow:auto}:not(pre)>code.hljs,:not(pre)>code[class*=language-]{border-radius:.3em;padding:.1em;white-space:normal}:is(.light .dark,.dark) code.hljs,:is(.light .dark,.dark) code[class*=language-],:is(.light .dark,.dark) pre[class*=language-]{color:#fff}:is(.light .dark,.dark) .hljs-comment{color:#ffffff80}:is(.light .dark,.dark) .hljs-meta{color:#fff9}:is(.light .dark,.dark) .hljs-built_in,:is(.light .dark,.dark) .hljs-class .hljs-title{color:#e9950c}:is(.light .dark,.dark) .hljs-doctag,:is(.light .dark,.dark) .hljs-formula,:is(.light .dark,.dark) .hljs-keyword,:is(.light .dark,.dark) .hljs-literal{color:#2e95d3}:is(.light .dark,.dark) .hljs-addition,:is(.light .dark,.dark) .hljs-attribute,:is(.light .dark,.dark) .hljs-meta-string,:is(.light .dark,.dark) .hljs-regexp,:is(.light .dark,.dark) .hljs-string{color:#00a67d}:is(.light .dark,.dark) .hljs-attr,:is(.light .dark,.dark) .hljs-number,:is(.light .dark,.dark) .hljs-selector-attr,:is(.light .dark,.dark) .hljs-selector-class,:is(.light .dark,.dark) .hljs-selector-pseudo,:is(.light .dark,.dark) .hljs-template-variable,:is(.light .dark,.dark) .hljs-type,:is(.light .dark,.dark) .hljs-variable{color:#df3079}:is(.light .dark,.dark) .hljs-bullet,:is(.light .dark,.dark) .hljs-link,:is(.light .dark,.dark) .hljs-selector-id,:is(.light .dark,.dark) .hljs-symbol,:is(.light .dark,.dark) .hljs-title{color:#f22c3d}.light code.hljs,.light code[class*=language-],.light pre[class*=language-]{color:#383a42}.light .hljs-comment,.light .hljs-quote{color:#a0a1a7;font-style:italic}.light .hljs-doctag,.light .hljs-formula,.light .hljs-keyword{color:#a626a4}.light .hljs-deletion,.light .hljs-name,.light .hljs-section,.light .hljs-selector-tag,.light .hljs-subst{color:#e45649}.light .hljs-literal{color:#0184bb}.light .hljs-addition,.light .hljs-attribute,.light .hljs-meta-string,.light .hljs-regexp,.light .hljs-string{color:#50a14f}.light .hljs-built_in,.light .hljs-class .hljs-title{color:#c18401}.light .hljs-attr,.light .hljs-number,.light .hljs-selector-attr,.light .hljs-selector-class,.light .hljs-selector-pseudo,.light .hljs-template-variable,.light .hljs-type,.light .hljs-variable{color:#986801}.light .hljs-bullet,.light .hljs-link,.light .hljs-meta,.light .hljs-selector-id,.light .hljs-symbol,.light .hljs-title{color:#4078f2}.light .hljs-emphasis{font-style:italic}.light .hljs-strong{font-weight:700}.light .hljs-link{text-decoration:underline}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#a9aec1}.token.punctuation{color:#fefefe}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#ffa07a}.token.boolean,.token.number{color:#00e0e0}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#abe338}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#00e0e0}.token.atrule,.token.attr-value,.token.function{color:gold}.token.keyword{color:#00e0e0}.token.important,.token.regex{color:gold}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}@media screen and (-ms-high-contrast:active){code[class*=language-],pre[class*=language-]{background:window;color:windowText}:not(pre)>code[class*=language-],pre[class*=language-]{background:window}.token.important{background:highlight;color:window;font-weight:400}.token.atrule,.token.attr-value,.token.function,.token.keyword,.token.operator,.token.selector{font-weight:700}.token.attr-value,.token.comment,.token.doctype,.token.function,.token.keyword,.token.operator,.token.property,.token.string{color:highlight}.token.attr-value,.token.url{font-weight:400}}/*! tailwindcss v4.1.6 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.react-select-container input:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.react-select-container .react-select__control{align-items:flex-start;border-color:#ececec;border-radius:var(--radius-lg,.5rem);font-size:var(--text-sm,.875rem);height:192px;line-height:var(--tw-leading,var(--text-sm--line-height,1.42857));min-height:192px;overflow:auto;padding:calc(var(--spacing,.25rem)*2)}@media (hover:hover){.react-select-container .react-select__control:hover{border-color:#e3e3e3}}.react-select-container .react-select__control:is(.dark *){background-color:#171717;border-color:#ffffff1a}@media (hover:hover){.react-select-container .react-select__control:hover:is(.dark *){border-color:#fff3}}.react-select-container.react-select--invalid .react-select__control,.react-select-container.react-select--invalid .react-select__control:is(.dark *){border-color:#e02e2a}.react-select-container .react-select__placeholder{padding-inline:calc(var(--spacing,.25rem)*2)}.react-select-container .react-select__input-container{color:var(--text-secondary);padding-inline:calc(var(--spacing,.25rem)*2)}.react-select-container .react-select__input{height:calc(var(--spacing,.25rem)*8)}.react-select-container .react-select__control--is-focused{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);border-color:#e3e3e3!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.react-select-container .react-select__control--is-focused:is(.dark *){border-color:#fff3!important}.react-select-container .react-select__value-container{padding:calc(var(--spacing,.25rem)*0)}.react-select-container .react-select__indicators:empty{display:none}.react-select-container .react-select__multi-value{background-color:#0000;margin:calc(var(--spacing,.25rem)*0)}.react-select-container .react-select__multi-value__label{padding:calc(var(--spacing,.25rem)*0)}#intercom-container{display:none}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} diff --git "a/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/saved_resource.html" "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/saved_resource.html" new file mode 100644 index 000000000..50a01a969 --- /dev/null +++ "b/docs/worker/\347\273\204\344\273\266\345\214\226\344\274\230\345\214\226\345\273\272\350\256\256_files/saved_resource.html" @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git "a/docs/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" "b/docs/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" new file mode 100644 index 000000000..7f9aae245 Binary files /dev/null and "b/docs/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" differ diff --git a/react-resume/README.md b/react-resume/README.md new file mode 100644 index 000000000..c1d0f2b22 --- /dev/null +++ b/react-resume/README.md @@ -0,0 +1,212 @@ +# 程序员简历网站模板 + +一个专为大公司(阿里、字节、腾讯等)招聘设计的专业简历网站模板,基于 React + Vite + Ant Design 构建。 + +## ✨ 特色功能 + +- 🎯 **专业设计**:符合大公司招聘标准的简历展示 +- 📱 **响应式布局**:完美适配手机、平板、电脑 +- 🚀 **高性能**:基于 Vite 构建,加载速度快 +- 🎨 **现代UI**:使用 Ant Design,界面美观专业 +- 📊 **数据驱动**:项目经验包含具体的数据指标 +- 🖨️ **打印友好**:支持打印为PDF格式 + +## 🚀 快速开始 + +1. **安装依赖** + ```bash + npm install + ``` + +2. **启动开发服务器** + ```bash + npm run dev + ``` + +3. **构建生产版本** + ```bash + npm run build + ``` + +## 📝 自定义内容 + +### 1. 个人信息修改 + +在 `src/App.jsx` 中修改以下内容: + +```javascript +// 个人信息 +const personalInfo = { + name: '张三', + title: '高级前端工程师', + description: '5年前端开发经验...', + contact: { + email: 'zhangsan@example.com', + phone: '+86 138-0000-0000', + location: '北京', + github: 'https://github.com/yourname', + linkedin: 'https://linkedin.com/in/yourname' + } +}; +``` + +### 2. 项目经验更新 + +修改 `projects` 数组中的项目信息: + +```javascript +const projects = [ + { + title: '项目名称', + company: '公司名称', + role: '职位', + duration: '2023.03 - 2023.12', + description: '项目描述...', + image: '项目截图URL', + tags: ['技术栈1', '技术栈2'], + metrics: [ + { label: '性能提升', value: '30%' }, + { label: '用户规模', value: '1000万+' } + ], + highlights: [ + '主要贡献1', + '主要贡献2' + ] + } +]; +``` + +### 3. 技能标签更新 + +修改 `skills` 对象: + +```javascript +const skills = { + '前端技术': ['React', 'Vue3', 'TypeScript'], + '后端技术': ['Node.js', 'Python', 'Java'], + '数据库': ['MySQL', 'MongoDB', 'Redis'], + '云服务': ['AWS', '阿里云', 'Docker'], + '工具平台': ['Git', 'Webpack', '蓝湖'] +}; +``` + +### 4. 工作经历更新 + +修改 `experiences` 数组: + +```javascript +const experiences = [ + { + year: '2023.03 - 至今', + company: '公司名称', + role: '职位', + content: '工作描述...' + } +]; +``` + +### 5. 教育背景更新 + +修改 `education` 对象: + +```javascript +const education = { + school: '学校名称', + major: '专业', + degree: '学位', + year: '2015-2019', + gpa: '3.8/4.0', + courses: ['课程1', '课程2'] +}; +``` + +## 🎨 样式自定义 + +### 主题色彩 + +在 `src/index.css` 中修改主题色彩: + +```css +:root { + --primary-color: #1890ff; + --secondary-color: #52c41a; + --text-color: #333; + --background-color: #f5f5f5; +} +``` + +### 布局调整 + +- 修改 `src/App.jsx` 中的布局参数 +- 调整卡片间距、字体大小等 +- 自定义响应式断点 + +## 📱 响应式设计 + +模板已针对不同设备进行优化: + +- **桌面端**:完整布局,多列展示 +- **平板端**:适中布局,部分列合并 +- **手机端**:单列布局,垂直排列 + +## 🖨️ 打印优化 + +模板包含专门的打印样式: + +- 隐藏导航和页脚 +- 优化卡片边框 +- 确保内容完整显示 + +## 🚀 部署建议 + +### 静态部署 + +1. **Vercel**(推荐) + ```bash + npm run build + # 上传 dist 文件夹到 Vercel + ``` + +2. **GitHub Pages** + ```bash + npm run build + # 推送到 gh-pages 分支 + ``` + +3. **阿里云/腾讯云** + ```bash + npm run build + # 上传 dist 文件夹到云服务器 + ``` + +### 自定义域名 + +部署后可以绑定自定义域名,提升专业度。 + +## 📋 简历优化建议 + +### 内容建议 + +1. **项目描述**:突出技术难点和解决方案 +2. **数据指标**:用具体数字展示成果 +3. **技术栈**:按熟练程度排序 +4. **工作经历**:突出职责和成就 + +### 设计建议 + +1. **保持简洁**:避免过度装饰 +2. **重点突出**:重要信息要醒目 +3. **一致性**:保持字体、颜色统一 +4. **可读性**:确保文字清晰易读 + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request 来改进这个模板。 + +## 📄 许可证 + +MIT License + +--- + +**提示**:记得定期更新项目经验和技术栈,保持简历的时效性! \ No newline at end of file diff --git a/react-resume/index.html b/react-resume/index.html new file mode 100644 index 000000000..4d07fadbd --- /dev/null +++ b/react-resume/index.html @@ -0,0 +1,13 @@ + + + + + + + 程序员简历网站 + + +
+ + + \ No newline at end of file diff --git a/react-resume/package-lock.json b/react-resume/package-lock.json new file mode 100644 index 000000000..891e79b2a --- /dev/null +++ b/react-resume/package-lock.json @@ -0,0 +1,3671 @@ +{ + "name": "resume-website", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "requires": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "@ant-design/cssinjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", + "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", + "requires": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + } + }, + "@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "requires": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + } + }, + "@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "requires": { + "@babel/runtime": "^7.24.7" + } + }, + "@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "requires": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + } + }, + "@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + }, + "@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "requires": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + } + }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", + "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "dev": true + }, + "@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "requires": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + } + }, + "@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.27.7" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==" + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "requires": { + "@babel/runtime": "^7.24.4" + } + }, + "@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "requires": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + } + }, + "@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "requires": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + } + }, + "@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "requires": { + "@babel/runtime": "^7.18.0" + } + }, + "@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "requires": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + } + }, + "@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "requires": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + } + }, + "@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "requires": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + } + }, + "@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "requires": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + } + }, + "@rc-component/trigger": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.7.tgz", + "integrity": "sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==", + "requires": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + } + }, + "@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==" + }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "dev": true, + "optional": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true + }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + } + }, + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "antd": { + "version": "5.26.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.26.3.tgz", + "integrity": "sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==", + "requires": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.7", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.8", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.51.1", + "rc-tabs": "~15.6.1", + "rc-textarea": "~1.10.0", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.2", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, + "array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + } + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + } + }, + "async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + } + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "electron-to-chromium": { + "version": "1.5.178", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", + "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "dev": true + }, + "es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + } + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "requires": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + } + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true + }, + "eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "requires": { + "has-bigints": "^1.0.2" + } + }, + "is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true + }, + "is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + } + }, + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.16" + } + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, + "is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "requires": { + "string-convert": "^0.2.0" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + } + }, + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + } + }, + "own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true + }, + "postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "requires": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + } + }, + "rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + } + }, + "rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + } + }, + "rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + } + }, + "rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "requires": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + } + }, + "rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + } + }, + "rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "requires": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + } + }, + "rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "requires": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + } + }, + "rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "requires": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + } + }, + "rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + } + }, + "rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "requires": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + } + }, + "rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + } + }, + "rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "requires": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + } + }, + "rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + } + }, + "rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "requires": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + } + }, + "rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + } + }, + "rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "requires": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + } + }, + "rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + } + }, + "rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + } + }, + "rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "requires": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + } + }, + "rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "requires": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + } + }, + "rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + } + }, + "rc-slider": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + } + }, + "rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "requires": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + } + }, + "rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "requires": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + } + }, + "rc-table": { + "version": "7.51.1", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz", + "integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + } + }, + "rc-tabs": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.6.1.tgz", + "integrity": "sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==", + "requires": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + } + }, + "rc-textarea": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.0.tgz", + "integrity": "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + } + }, + "rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "requires": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + } + }, + "rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + } + }, + "rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "requires": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + } + }, + "rc-upload": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", + "requires": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + } + }, + "rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "requires": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + } + }, + "rc-virtual-list": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.1.tgz", + "integrity": "sha512-DCapO2oyPqmooGhxBuXHM4lFuX+sshQwWqqkuyFA+4rShLe//+GEPVwiDgO+jKtKHtbeYwZoNvetwfHdOf+iUQ==", + "requires": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + } + }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true + }, + "react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "requires": { + "@remix-run/router": "1.23.0" + } + }, + "react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "requires": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + } + }, + "reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + } + }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + } + }, + "safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "requires": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + } + }, + "string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, + "string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==" + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + } + }, + "typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + } + }, + "unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + } + }, + "update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/react-resume/src/App.jsx b/react-resume/src/App.jsx new file mode 100644 index 000000000..b95bf6f99 --- /dev/null +++ b/react-resume/src/App.jsx @@ -0,0 +1,410 @@ +import React, { useState } from 'react'; +import { Layout, Menu, Card, Avatar, Row, Col, Tag, Button, Divider, Progress, Statistic, Timeline, Typography, Space } from 'antd'; +import { + UserOutlined, + ProjectOutlined, + TrophyOutlined, + MailOutlined, + GithubOutlined, + WechatOutlined, + LinkedinOutlined, + PhoneOutlined, + EnvironmentOutlined, + CalendarOutlined, + TeamOutlined, + RocketOutlined, + CodeOutlined, + DatabaseOutlined, + CloudOutlined +} from '@ant-design/icons'; + +const { Header, Content, Footer } = Layout; +const { Title, Paragraph, Text } = Typography; + +function App() { + const [currentSection, setCurrentSection] = useState('home'); + + const projects = [ + { + title: '企业级电商平台', + company: '阿里巴巴', + role: '高级前端工程师', + duration: '2023.03 - 2023.12', + description: '负责淘宝商家后台系统的核心模块开发,优化页面性能提升30%,支持千万级商家使用。', + image: 'https://via.placeholder.com/400x250/1890ff/ffffff?text=E-commerce+Platform', + link: '#', + tags: ['React', 'TypeScript', 'Node.js', 'Redis', 'Docker'], + metrics: [ + { label: '性能提升', value: '30%' }, + { label: '用户规模', value: '1000万+' }, + { label: '代码覆盖率', value: '85%' } + ], + highlights: [ + '使用 React 18 + TypeScript 重构核心组件,提升开发效率', + '实现微前端架构,支持多团队并行开发', + '优化首屏加载时间从 3s 降至 1.5s' + ] + }, + { + title: '短视频推荐系统', + company: '字节跳动', + role: '全栈开发工程师', + duration: '2022.06 - 2023.02', + description: '参与抖音推荐算法前端展示层开发,负责用户交互优化和数据分析模块。', + image: 'https://via.placeholder.com/400x250/52c41a/ffffff?text=Video+Recommendation', + link: '#', + tags: ['Vue3', 'Python', 'MongoDB', 'Kafka', 'Elasticsearch'], + metrics: [ + { label: '用户留存', value: '+15%' }, + { label: '推荐准确率', value: '92%' }, + { label: '系统可用性', value: '99.9%' } + ], + highlights: [ + '设计并实现实时数据可视化面板,支持千万级数据展示', + '优化推荐算法前端交互,提升用户点击率 20%', + '构建 A/B 测试框架,支持快速迭代验证' + ] + }, + { + title: '微信小程序商城', + company: '腾讯', + role: '小程序开发工程师', + duration: '2021.09 - 2022.05', + description: '独立开发微信小程序商城,包含商品展示、购物车、支付等完整功能模块。', + image: 'https://via.placeholder.com/400x250/faad14/ffffff?text=WeChat+Mini+Program', + link: '#', + tags: ['微信小程序', '云开发', 'WXML', 'WXSS', 'JavaScript'], + metrics: [ + { label: '日活用户', value: '5万+' }, + { label: '转化率', value: '8.5%' }, + { label: '用户评分', value: '4.8/5' } + ], + highlights: [ + '使用微信云开发,实现服务端零运维', + '集成微信支付,支持多种支付方式', + '实现商品推荐算法,提升用户购买转化率' + ] + }, + { + title: '蓝湖设计系统', + company: '自研项目', + role: 'UI/UX 设计师 + 前端开发', + duration: '2021.03 - 2021.08', + description: '设计并开发企业级设计系统,包含组件库、设计规范、原型工具等。', + image: 'https://via.placeholder.com/400x250/722ed1/ffffff?text=Design+System', + link: '#', + tags: ['Figma', 'React', 'Storybook', 'Sass', 'Webpack'], + metrics: [ + { label: '组件数量', value: '50+' }, + { label: '设计效率', value: '+40%' }, + { label: '团队规模', value: '20人' } + ], + highlights: [ + '建立完整的设计规范和组件库', + '开发可视化原型工具,提升设计效率', + '与产品、开发团队协作,确保设计落地' + ] + } + ]; + + const skills = { + '前端技术': ['React', 'Vue3', 'TypeScript', 'JavaScript', 'HTML5', 'CSS3', 'Sass/Less'], + '后端技术': ['Node.js', 'Python', 'Java', 'Express', 'Koa', 'Spring Boot'], + '数据库': ['MySQL', 'MongoDB', 'Redis', 'PostgreSQL', 'Elasticsearch'], + '云服务': ['AWS', '阿里云', '腾讯云', '微信云开发', 'Docker', 'Kubernetes'], + '工具平台': ['Git', 'Webpack', 'Vite', 'Jenkins', '蓝湖', 'Figma', 'Sketch'] + }; + + const experiences = [ + { + year: '2023.03 - 至今', + company: '阿里巴巴', + role: '高级前端工程师', + content: '负责淘宝商家后台系统开发,优化系统性能,支持千万级用户。' + }, + { + year: '2022.06 - 2023.02', + company: '字节跳动', + role: '全栈开发工程师', + content: '参与抖音推荐系统开发,负责前端展示层和数据分析模块。' + }, + { + year: '2021.09 - 2022.05', + company: '腾讯', + role: '小程序开发工程师', + content: '独立开发微信小程序商城,实现完整的电商功能。' + }, + { + year: '2019.09 - 2021.08', + company: 'XX互联网公司', + role: '前端开发工程师', + content: '参与多个Web应用开发,积累丰富的项目经验。' + } + ]; + + const education = { + school: '清华大学', + major: '计算机科学与技术', + degree: '本科', + year: '2015-2019', + gpa: '3.8/4.0', + courses: ['数据结构', '算法设计', '软件工程', '数据库系统', '计算机网络'] + }; + + const renderHome = () => ( +
+ +

+ } style={{ marginBottom: 24, border: '4px solid #1890ff' }} /> + 张三 + 高级前端工程师 + + + 个人简介 + + 5年前端开发经验,曾在阿里巴巴、字节跳动、腾讯等一线互联网公司工作。 + 擅长 React、Vue、TypeScript 等前端技术栈,具备全栈开发能力。 + 主导过多个大型项目,对性能优化、架构设计有丰富经验。 + 热爱技术,持续学习,追求代码质量和用户体验的完美平衡。 + + + + + + + + + + + + + + + + + + + + + + + + + } /> + + + + + } /> + + + + + } /> + + + + + ); + + const renderProjects = () => ( +
+ 项目经验 + + {projects.map((project, index) => ( + + +
+ {project.title} + + +
+ {project.title} + {project.company} | {project.role} +
+ {project.duration} +
+ + + {project.description} + + +
+ {project.tags.map(tag => ( + + {tag} + + ))} +
+ + + {project.metrics.map((metric, idx) => ( + + + + ))} + + +
+ 主要贡献: +
    + {project.highlights.map((highlight, idx) => ( +
  • + {highlight} +
  • + ))} +
+
+ + + + ))} + + + ); + + const renderAbout = () => ( +
+ 技能与经历 + + +
+ }> + {Object.entries(skills).map(([category, skillList]) => ( +
+ {category} +
+ {skillList.map(skill => ( + + {skill} + + ))} +
+
+ ))} +
+ + + + }> + + {experiences.map((exp, index) => ( + +
+ {exp.year} +
+ {exp.company} - {exp.role} +
+ {exp.content} +
+
+ ))} +
+
+ + + + + + }> + + + + + + + + + + + +
+ 主要课程: +
+ {education.courses.map(course => ( + + {course} + + ))} +
+
+ + + + + ); + + const menuItems = [ + { key: 'home', label: '个人简介', icon: }, + { key: 'projects', label: '项目经验', icon: }, + { key: 'about', label: '技能经历', icon: } + ]; + + const renderContent = () => { + switch (currentSection) { + case 'home': + return renderHome(); + case 'projects': + return renderProjects(); + case 'about': + return renderAbout(); + default: + return renderHome(); + } + }; + + return ( + +
+
+ + 张三 - 高级前端工程师 +
+ setCurrentSection(key)} + style={{ lineHeight: '64px', borderBottom: 'none', float: 'right' }} + /> +
+ + +
+ {renderContent()} +
+
+ +
+
+ + zhangsan@example.com + +86 138-0000-0000 + 北京 + +
+
+ © {new Date().getFullYear()} 张三的个人简历 | 由 React + Vite + Ant Design 构建 +
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/react-resume/src/index.css b/react-resume/src/index.css new file mode 100644 index 000000000..425c38c84 --- /dev/null +++ b/react-resume/src/index.css @@ -0,0 +1,142 @@ +:root { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.6; + font-weight: 400; + color: #333; + background-color: #f5f5f5; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +#root { + width: 100%; + margin: 0 auto; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* 链接样式 */ +a { + font-weight: 500; + color: #1890ff; + text-decoration: none; + transition: color 0.3s ease; +} + +a:hover { + color: #40a9ff; +} + +/* 卡片悬停效果 */ +.ant-card { + transition: all 0.3s ease; +} + +.ant-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1) !important; +} + +/* 按钮样式优化 */ +.ant-btn { + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; +} + +.ant-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); +} + +/* 标签样式 */ +.ant-tag { + border-radius: 4px; + font-weight: 500; + transition: all 0.3s ease; +} + +.ant-tag:hover { + transform: scale(1.05); +} + +/* 头像样式 */ +.ant-avatar { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* 统计数字样式 */ +.ant-statistic-content { + font-weight: 600; +} + +/* 时间线样式 */ +.ant-timeline-item-head { + background-color: #1890ff; +} + +.ant-timeline-item-tail { + border-left: 2px solid #e8e8e8; +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .ant-layout-header { + padding: 0 20px !important; + } + + .ant-layout-content { + padding: 0 20px !important; + } + + .ant-layout-footer { + padding: 16px 20px !important; + } +} + +/* 打印样式 */ +@media print { + .ant-layout-header, + .ant-layout-footer { + display: none !important; + } + + .ant-layout-content { + padding: 0 !important; + background: white !important; + } + + .ant-card { + box-shadow: none !important; + border: 1px solid #d9d9d9 !important; + } +} \ No newline at end of file diff --git a/react-resume/src/main.jsx b/react-resume/src/main.jsx new file mode 100644 index 000000000..4b98461a3 --- /dev/null +++ b/react-resume/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +); \ No newline at end of file diff --git a/react-resume/vite.config.js b/react-resume/vite.config.js new file mode 100644 index 000000000..52f988d97 --- /dev/null +++ b/react-resume/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); \ No newline at end of file diff --git "a/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" "b/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" new file mode 100644 index 000000000..7f9aae245 Binary files /dev/null and "b/\345\276\256\344\277\241\346\224\266\346\254\276\347\240\201.png" differ