n8n 新手上路：从一无所知到跑出第一个自动化流程
AI 自动化这几年真的越来越重要了不管是处理重复任务、整理数据，还是用 AI 做一些智能化的流程，现在这些都能被自动化接管这是我第一次接触 n8n ，也是一无所知的新手，但在简单的了解过后，我已经跑通了一个完整的自动化工作流：从 RSS 读取今日热点 → AI 自动总结 → AI 自动制图 → 生成 HTML 日报页什么是 n8n如果你没用过 n8n，可以简单把它想象成开源的 Coze不同的是它：能自托管拖几个节点就能把各种 API 接起来和 AI 配合的时候真的很爽以前做自动化要写 Python / NodeJS，配定时任务，自己处理数据结构、容错之类的。现在很多东西拖一拖就完了n8n 的强大之处不仅在于它能做传统自动化（如备份、同步、发通知）而是在 AI 时代，它可以成为「调度大脑」它自己不做什么复杂逻辑，但非常擅长“把事情串起来”例如：使用 AI 进行复杂抓取与筛选多源 RSS / API / Webhook 信息聚合使用 AI 自动判断价值度对新闻进行 分类、比对、摘要、聚合自动过滤重复内容按重要程度排序，生成清晰结构化数据我的第一个流程：RSS → AI 总结 → AI 制图 → HTML 日报这是我第一次在 n8n 里动手做出的完整流程：从 RSS 获取热点 → AI 自动总结 → AI 自动生成日报图 → 生成 HTML 页面这里推荐一个持续跟进官方的 汉化版本 ，方便学习部署docker volume create n8n_data docker run -it --name n8n -p 5678:5678 -e N8N_RELEASE_TYPE=stable -e N8N_DIAGNOSTICS_ENABLED=false -e N8N_VERSION_NOTIFICATIONS_ENABLED=false -e N8N_HIDE_USAGE_PAGE=true -e N8N_LICENSE_AUTO_RENEW_ENABLED=false -e N8N_RUNNERS_ENABLED=true -e NODE_ENV=development -e N8N_DEFAULT_LOCALE=zh-CN -e N8N_ENTERPRISE_MOCK=true -v n8n_data:/home/node/.n8n ghcr.io/deluxebear/n8n:chs随后直接访问 http://localhost:5678 即可流程展示Webhook → RSS Read → AI Summary → AI Image → Markdown → HTML → Webhook Response整个流程非常直观，基本看一眼图就能理解整个 workflow 是如何工作的通过 Webhook 触发 RSS Read, 交给 AI 进行总结内容，随后将输出结果使用 AI 制作出图片,我这里使用的是 Nano Banana Pro 返回的是 Markdown 格式的 Base64,需要转换成 HTML 才能响应 Webhook，展示在前端。预览效果如下各节点分析Webhook（流程入口）这是整个流程的触发器, 只要访问 http://localhost:5678/webhook/xxxxx 就能出发整个流程的运行RSS Read这个节点负责获取信息源作为简单的项目演示，采用了 知乎每日新闻的 RSS https://plink.anyfeeder.com/zhihu/daily 运行返回结果如下[ { "title":"为什么古代中国没有其它文明那么多大型石砌建筑？", "link":"https://daily.zhihu.com/story/9785710", "pubDate":"Invalid Date", "content":" \n \n\n \n\n\n\n \n\n \n\n\n\n\n \n \n\n \n\n \n \nBrian C，有问题请私信加微信\n查看知乎原文\n \n\n \n 为什么古代中国没有其它文明那么多大型石砌建筑？ \n 因为任何大型建筑本质上就是两门科学“物理和数学”的应用。 \n 物理学上讲， \n 中国古代人无法把向下的力转化为水平的力， ...这些内容随后被送入 AI 做内容提炼与聚合AI 总结这一节点是整个工作流的核心我这里用的不是官方 OpenAI 接口，而是一个第三方聚合平台，所以 n8n 里直接用 HTTP Request 节点 来调用 AI。看起来很高端，其实流程非常简单：就是发一个接口，把 prompt 丢进去，让 AI 返回你想要的文本或是一些其他的如图片等拿到结果后，再用 n8n 的 Code 或 Set 节点稍微处理一下，就能继续用在下一步流程（例如生成 Markdown、渲染 HTML、推送消息）流程其实不复杂：AI Summary：生成结构化日报AI Image：根据日报生成海报（Base64 返回）Code / Set 节点：轻度加工 AI 输出只要能发送 HTTP 请求，就能调用任何模型，n8n 在这里就像一个完美的调度层渲染处理我使用的生图模型是 gemini-3-pro-image-preview，它输出的是 Base64为了在浏览器中体现，我做了三步：把 Base64 拼成 markdown 图片语法用 markdownToHTML 转成 Webhook Response 输出 HTML 页面你可以在这里下载此次的 workflow: My first workflow.json一些开源的 N8N 项目n8n 官方示例库N8N‑Telegram‑News‑Agentn8n‑workflows我第一次用 n8n 的一些感受作为一个第一次接触 n8n 的新手，我的整体体验是非常惊喜的本以为会需要一些学习成本，实则不然上手速度非常快可视化编辑比写代码更直观每一步都能实时看到输出，非常适合调试和 AI 搭配后，真正实现自动化过去我们做自动化，往往要：写 Python / Node.js 脚本搭建定时任务平台, 设置定时任务自己处理 API 调用手动处理数据维护项目结构排查错误而 n8n 给我最大的感受是：你不再需要写多少代码了，你只是在 表达你的流程以前需要几百行脚本，现在可能只需要：一个 HTTP 节点: 信息获取一个 AI 处理: AI 信息加工各种中间件节点: 对内容进行定制化加工响应节点: 通知或是渲染这一切都是可视化的我觉得 n8n 的核心价值在于：让自动化从“写代码”变成“表达业务逻辑”让 AI 从“单次调用”变成“自动化协作”你拼装的不是流程，而是 一个能自主处理信息的 AI 工作代理人（AI Agent Workflow）这对我来说非常震撼期待接下来深入 n8n，继续折腾更多自动化项目！
为什么 includes 让内存暴涨，而 SQL 子查询却如此高效 —— Rails N+1 查询的真相本文基于 Akshay Khot 的文章 和本地实验结果，深入剖析 Rails 中 N+1 查询的本质、为什么 includes 会消耗更多内存，以及 SQL 子查询（Subquery）在时间与空间上的取舍。🧩 什么是 N+1 查询在 Rails 开发中，N+1 查询是常见的性能陷阱。以 User 和 Post 两个模型为例：class User < ApplicationRecord has_many :posts end class Post < ApplicationRecord belongs_to :user end控制器中简单地读取文章列表：def index @posts = Post.order(created_at: :desc) end在视图中渲染每篇文章及其作者：<% @posts.each do |post| %> <%= post.title %> User：<%= post.user.name %> <% end %>这段代码背后会执行：一条查询读取所有 posts，随后对每个 post 再单独查询对应的 user。形成了“一条主查询 + N 条子查询”的模式，被称作 N+1 查询。当数据量较大时，多次数据库往返会显著拉长响应时间。⚙️ 性能分析工具：Rack Mini Profiler为了更好地观察这些查询带来的开销，我们使用 Rack Mini Profiler。在开发环境中安装并配置后，在浏览器 URL 后加 ?pp=profile-memory 可以看到页面顶部的性能面板，显示 SQL 查询次数、执行时间和内存占用等信息。🧪 实验环境与数据生成使用 Rails 8 和 SQLite 创建简化的 users、posts 表。每个用户生成 5 000 篇文章，共两名用户，以便观察不同查询方式的差异。⚖️ 三种方案的性能对比性能对比一览模式查询数页面加载时间SQL 总耗时内存分配Ruby 对象数N+1 查询10 0015.8 s146.3 ms5.9 KB86includes2188 ms56.7 ms455.4 KB3675子查询10 0015.7 s128.8 ms7.2 KB91🚨 方案一：N+1 查询在控制器中简单地 @posts = Post.order(created_at: :desc)，然后在视图中访问 post.user.name。这种方式代码简洁，但会导致每次访问关联字段都单独执行一次查询。截图： 下图展示了 N+1 查询在 Rack Mini Profiler 中的总览。可以看到页面渲染时间超过 5.8 秒，SQL 概述中显示查询次数达到 10001 条。内存分配： 另一张图给出了 N+1 查询的内存分配情况小结：简单但最慢——因为往返次数爆炸⚙️ 方案二：includes 预加载通过 includes 让 Rails 预先加载所有关联用户，减少数据库往返：@posts = Post.includes(:user).order(created_at: :desc)Rails 会先取出所有 posts，再批量查询这些 post.user 对象：截图： Rack Mini Profiler 显示 includes 方式只发起了 2 次查询，页面响应时间从 8 秒级降至约 188 毫秒内存占用： includes 虽然快了很多，但在内存里实例化了用户对象，仍有一定开销。测试中分配的内存为 455.4 kb，生成了 3675 个对象小结：用内存换查询次数。当仅需要某个字段时，把整批关联对象都载入内存，往往不划算。🧮 方案三：SQL 子查询（Subquery）第三种方式是将关联逻辑交给数据库，通过子查询直接把 users.name 字段放在 posts 查询结果中：subquery = User .select(:name) .where("users.id = posts.user_id") .limit(1) .to_sql @posts = Post .select("posts.*, (#{subquery}) AS author_name") .order(created_at: :desc)生成的 SQL 如下：SELECT posts.*, (SELECT users.name FROM users WHERE users.id = posts.user_id LIMIT 1) AS author_name FROM posts ORDER BY created_at DESC;理论上，这能减少 Rails 与数据库的往返和对象生成。但在测试环境中，每一条 post 的行都执行了一次子查询：截图： Rack Mini Profiler 提示页面加载时间约 5.7 秒，SQL 概述中显示 10001 条查询内存占用： 使用子查询方式时，内存使用量为约 7.2 KB，对象数约 91小结：让数据库干擅长的事。对于「每行只需要一个派生字段」的场景，子查询通常**更快且更省内存。🔍 Rails N+1 查询的本质懒加载机制： ActiveRecord 天生按需查询，在视图里访问关联对象时才执行 SQL，这导致循环里触发多次查询。慢在往返，不在 SQL： N+1 查询慢在无谓的数据库往返，而不是 SQL 执行本身。即使单次查询只有几毫秒，累加起来仍然很慢。includes 的权衡： 通过提前加载关联对象来减少往返，但需要额外内存。Subquery 的局限： 如果每行数据都触发一次子查询，虽然减少了 Ruby 对象生成，但查询次数仍然是 N+1 级别，整体反而更慢。⚖️ 开发中的选择建议场景推荐方案理由数据量小（<20 条），需要多个关联字段.includes减少查询次数，代码直观数据量大，仅需单字段聚合子查询或聚合函数内存占用小，减少对象创建高频访问且字段变化不大缓存或反规范化字段减少实时查询，提升速度💡 总结N+1 查询本质问题在于数据库往返次数过多includes 通过预加载在内存中构建关联，适合小数据集子查询能减少 Ruby 对象生成，但如果每行都执行子查询，响应时间不一定快根据场景选择合适的策略，让数据库做它擅长的工作，Rails 应该更专注于渲染最后，想送给自己，也送给大家一句话：纸上得来终觉浅，绝知此事要躬行。仅仅是看一遍、懂了大概或是一知半解，并不能真正掌握。唯有亲自动手实践，才能体会其中的重点与要领。希望大家在遇到问题时，不妨多尝试自己解决，在行动中积累经验，才能真正看清自己的实力与成长。参考链接：Fix N+1 Queries Without Eager Loading Using SQL Subqueries – Akshay Khot
Vibe Coding 中，如何提高开发效率随着 AI 技术的发展，越来越多的开发者开始尝试 Vibe Coding ——也就是把 AI 当作“结对编程的伙伴”，通过对话的方式完成代码编写、调试和优化一、现阶段主流 AI 工具对比目前我常用到的 AI 模型有 GPT-5、Gemini 2.5（Pro/Flash）以及 Claude 4.x。不同工具各有优势，适合的场景也不一样。工具优势适用场景GPT-5语言理解和推理能力极强，支持复杂的多轮对话和跨领域问题；适合生成计划以及修复 BUG复杂项目、跨栈开发、需要较高逻辑推理的任务Gemini 2.5 Pro上下文记忆更持久，长对话能力强，适合规划大型项目需要详细讨论和规划的长期开发Claude 4.x编码能力极强，且 UI 方面也非常不错。Plan 模式有助于结构化开发，代码逻辑清晰需要详细规划、文档和代码整合的场景补充：作为全栈开发，GPT-5 往往能提供更强的逻辑和上下文理解，利用其强大的理解能力，在配合上 Claude 的 Plan 模式。做到 先规划在执行 的场景，这样的开发会很流畅二、Vibe Coding 中常见问题在实际项目中利用 AI 开发，新手往往会遇到包括但不限于以下问题：1. Prompt 不清晰案例：我在 xxx 项目中想加一个团队管理功能，直接对 GPT-5 说“帮我做一个团队系统”，它给我生成了一个全新的 Team 模型和用户关联，但忽略了代码或者模板本身提供的如 Jumpstart 内置的组织/用户模型结构的功能。导致结果冲突很多，花了不少时间重构。重复造轮子2. 功能过于宽泛案例：我让 AI “帮我实现订阅功能”，它直接写了 Stripe 集成的控制器和视图，AI 可能会直接去帮你实现，但是确实按照了它的逻辑，如果你没有主动告诉 AI 你已经有部分实现的代码，那么大概率最终代码重复且冲突。3. 缺乏计划阶段案例：让 Claude 帮我“加一个发票模块”，Claude 写的代码看似完整，但路由、前端样式和 TailwindCSS 组件完全没有和前端模板对齐，导致的后果就是，同一个项目，像是不同的人在施工，参杂了各种冗余且复杂的代码。如果在一开始有计划的先让 AI 输出“发票模块所需的字段、数据库迁移、UI 组件和权限控制”，再进入编码，效率会更高。4. 上下文管理不当案例：在一次对话中不断添加新需求，比如“加多租户支持”“再加 API Key 管理”，但是没有维护一份需求文档。导致 GPT-5 忘记了我项目已经使用 Apartment gem 做多租户，后来又建议我用另外一种方案，冲突严重。三、解决思路与效率提升方法针对上述问题，总结出以下几条经验：1. 提升 Prompt 质量在 Vibe Coding 中，Prompt 就是需求说明书。AI 能输出什么样的结果，往往取决于你给它的输入。模糊的 Prompt 会让 AI 胡乱发挥，而一个精准的 Prompt 则能让 AI 像资深开发一样给出贴合需求的代码。Prompt 的作用明确边界：告诉 AI 该做什么、不该做什么，避免生成多余或冲突的代码。统一风格：通过 Prompt 说明项目用的框架、依赖和代码风格（例如 Rails 8 + Hotwire + TailwindCSS + Flowbite），让 AI 始终保持一致。减少返工：当 Prompt 已经涵盖关键细节，AI 生成的结果往往能直接使用，而不是需要开发者大规模重写。为什么好的 Prompt 能事半功倍降低沟通成本：把复杂需求转化成清晰的文字，AI 能一次性理解，大幅减少多轮交互。提高输出质量：明确的上下文信息，让 AI 生成的代码更可维护，避免“屎山代码”。提升团队效率：在多人协作时，标准化的 Prompt 能确保每个开发者和 AI 的交互保持一致，不会因为表达差异而导致风格混乱。优秀 Prompt 的参考资源在 GIthub 里，已经有不少优秀的 Prompt 示例可以直接拿来学习和套用：awesome-chatgpt-prompts 经典的 Prompt 收集库，涵盖开发、调试、代码审查等场景。prompt-engineering-for-developers 面向开发者的 Prompt 工程指南，详细解释了如何写高质量 Prompt，以及不同场景下的策略。高质量 Prompt 不仅告诉 AI 做什么，还明确了 如何做、用什么技术做，结果往往能直接落地，极大提升效率。2. 分解任务，逐步引导在 Vibe Coding 中，最容易犯的错误就是“一股脑把需求抛给 AI”。这样做的结果往往是：AI 给出一大段貌似完整的代码，但和你的项目框架、依赖不兼容，甚至出现“屎山代码”。解决办法有两个核心原则：任务拆解：把大需求分成小模块，逐步实现；计划先行：让 AI 在动手写代码前，先输出一份开发计划。实践案例：文件下载并上传功能模型设计添加 FileRecord 模型，字段包括 user_id、file_name、status 等确认文件存储策略（本地/云存储）下载逻辑在拿到文件直链后直接在本地服务器上使用 xxx 类中的方法进行下载。上传逻辑使用 Active Storage 进行上传操作测试覆盖编写 RSpec 系统测试，验证下载链接、上传逻辑和文件状态更新的正确性。用 Plan 模式提升质量在 Claude 或 GPT-5 中，可以直接让它先生成“计划”而不是代码，这样做的好处是：减少返工：先验证方案对不对，再让 AI 编码；方便协作：团队可以先讨论 AI 给的计划，然后再落地；保持一致性：避免不同模块之间风格和实现不统一。总结一句话：把需求拆小，并让 AI 先写“计划书”，再写“代码”。这样 Vibe Coding 才能真正做到高效，而不是边改边救火。4. 维护项目记忆可以新建一个 project_context.md 文件，记录：项目架构，功能代码风格编码前后是否要运行某些检查脚本当前开发进度我在 project_context.md 中写清楚了这些，后续和 AI 对话时，只要贴上这份文件，它就不用再反复强调项目的事情了四、总结（感受）在实际项目的开发中，我最大的感受是：AI 是一个极好的生产力放大器，但前提是你必须先帮它把边界划清楚。（也就是把 AI 当作傻子看待，不要以为它能理解）比如在实现团队订阅功能时，如果我只是简单说“帮我加订阅”，AI 生成的代码大概率和 Jumpstart 内置的结构冲突；但当我明确说明“基于 Jumpstart 的 Pay gem，在团队维度做订阅，并使用 Flowbite 组件做 UI”，AI 给的代码几乎可以直接落地Vibe Coding 的关键不在于“AI 能写多少代码”，而在于“你能否把需求描述清楚、拆分合理，并维护上下文”。一旦这些做到位，AI 就能帮你完成大量的重复劳动，而你只需要把控架构和关键实现从我的经验来看，Vibe Coding 更像是带着一个随叫随到的天才傻子写代码
本篇文章只记录 索引优化 和 SQL优化索引优化有无索引对比准备 test 表CREATE TABLE `test` ( `id` bigint NOT NULL, `name` char(4) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;插入 100w 数据import pymysql import random import string import time # 数据库配置 (根据实际情况修改) DB_CONFIG = { 'host': 'localhost', 'user': 'your_username', 'password': 'your_password', 'database': 'your_database', 'charset': 'utf8mb4' } # 生成随机4字符名称 def random_name(): return ''.join(random.choices(string.ascii_letters + string.digits, k=4)) def insert_massive_data(): try: # 连接数据库 conn = pymysql.connect(**DB_CONFIG) cursor = conn.cursor() print("开始插入数据...") start_time = time.time() # 批量插入配置 batch_size = 5000 # 每批插入量 total_records = 1_000_000 batch_count = total_records // batch_size # 循环插入批次数据 for batch in range(batch_count): # 构建批量数据 data = [] for offset in range(batch_size): id_val = batch * batch_size + offset + 1 data.append((id_val, random_name())) # 执行批量插入 sql = "INSERT INTO test (id, name) VALUES (%s, %s)" cursor.executemany(sql, data) conn.commit() # 打印进度 if (batch + 1) % 10 == 0 or batch == 0: processed = (batch + 1) * batch_size print(f"已插入: {processed:,} 条, 进度: {processed/total_records:.1%}") # 插入剩余数据（如有） remaining = total_records % batch_size if remaining: data = [] start_id = total_records - remaining + 1 for i in range(remaining): data.append((start_id + i, random_name())) cursor.executemany(sql, data) conn.commit() print(f"最后一批插入完成: {remaining} 条") # 统计结果 duration = time.time() - start_time print(f"插入完成! 总共插入 {total_records:,} 条记录") print(f"耗时: {duration:.2f} 秒") print(f"平均速度: {total_records/duration:,.0f} 条/秒") except pymysql.Error as e: print(f"数据库错误: {e}") conn.rollback() finally: if conn: conn.close() if __name__ == "__main__": insert_massive_data()有索引情况无索引情况总结性能对比分析查询类型查询语句执行时间扫描行数​​有索引情况​​SELECT * FROM test WHERE id = 7894560.00s1​​无索引情况​​SELECT * FROM test WHERE name = 'abc'0.16s1,000,000关键发现​​索引的性能优势​​：​​数量级差异​​：索引查询仅扫描 1 行，而无索引查询扫描全部 100 万行​​时间差异​​：索引查询接近 0 秒(0.00s)，无索引查询耗时 0.16 秒​​全表扫描的代价​​：数据库必须遍历每一条记录进行匹配I/O 操作量呈线性增长对性能的影响随数据量增大呈指数级增加​​主键索引的特殊优势​​：B+ 树结构实现 O(log n)时间复杂度的查找不需要回表查询（索引即包含所有数据）索引选择建议索引类型适用场景优点缺点​​主键索引​​唯一标识的记录查询最佳性能，数据物理排序只能有一个​​普通索引​​WHERE/JOIN/ORDER BY 字段提高特定条件查询性能需要额外空间​​复合索引​​多条件组合查询覆盖索引避免回表顺序敏感​​前缀索引​​长文本字段（char/varchar）节省索引空间可能降低精度合理使用索引可以在百万级数据环境中实现查询响应时间的量级提升！最左前缀原则最左前缀原则（Leftmost Prefix Principle）​​ 是数据库索引优化中针对​​复合索引（Combined Index，也称组合索引或多列索引）​​ 的核心使用规则。该原则规定了复合索引只有在满足特定条件下才能被有效利用，确保索引发挥最大效能。简单来说 当 SQL 查询条件​​从左向右连续匹配​​复合索引的列顺序时，索引才能生效。如同"连续开锁"：必须从第一把锁开始按顺序打开，不能跳过或打乱顺序。准备 user 表CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, country_code CHAR(2) NOT NULL COMMENT '国家代码，如CN/US', -- 高频查询字段 province VARCHAR(20) NOT NULL COMMENT '省份/州', -- 常用查询字段 city VARCHAR(30) NOT NULL COMMENT '城市', -- 常用查询字段 district VARCHAR(30) COMMENT '区县', -- 较少查询 gender ENUM('M','F','O') DEFAULT 'O' COMMENT '性别', -- 低区分度字段 birth_date DATE NOT NULL COMMENT '出生日期', -- 范围查询字段 register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', last_login_time DATETIME COMMENT '最后登录时间', phone VARCHAR(20) UNIQUE COMMENT '手机号', email VARCHAR(50) UNIQUE COMMENT '邮箱', hashed_password CHAR(128) NOT NULL COMMENT '密码哈希值', INDEX idx_loc (country_code, province, city), -- 复合索引1 INDEX idx_activity (register_time, last_login_time), -- 复合索引2 INDEX idx_profile (birth_date, gender) -- 复合索引3 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;插入 100w 数据import pymysql import random import string import time from datetime import datetime, timedelta # 数据库配置（根据实际情况修改） DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': 'your_password', 'database': 'your_database', 'charset': 'utf8mb4' } # 国家省份城市数据（简化版） COUNTRIES = [ ('CN', '中国'), ('US', '美国'), ('JP', '日本'), ('KR', '韩国'), ('UK', '英国') ] # 中国省份和城市 CN_PROVINCES = [ ('Guangdong', ['Guangzhou', 'Shenzhen', 'Dongguan']), ('Jiangsu', ['Nanjing', 'Suzhou', 'Wuxi']), ('Zhejiang', ['Hangzhou', 'Ningbo', 'Wenzhou']), ('Beijing', ['Beijing']), ('Shanghai', ['Shanghai']) ] # 美国州和城市 US_STATES = [ ('California', ['Los Angeles', 'San Francisco', 'San Diego']), ('Texas', ['Houston', 'Dallas', 'Austin']), ('New York', ['New York City', 'Buffalo', 'Rochester']) ] # 其他国家主要城市 OTHER_CITIES = ['Tokyo', 'Seoul', 'London', 'Paris', 'Singapore'] # 生成唯一手机号（确保百万数据中唯一） def random_phone(user_id): # 前三位：固定为1 + 3X（30-39） prefix = "1" + str(random.randint(30, 39)) # 中间4位：基于用户ID和批次计算 mid = f"{(user_id // 1000) % 10000:04d}" # 后4位：基于用户ID确保唯一 suffix = f"{user_id % 10000:04d}" return prefix + mid + suffix # 生成唯一邮箱（确保百万数据中唯一） def random_email(user_id): domains = ["gmail.com", "yahoo.com", "hotmail.com", "example.com"] return f"user{user_id}@{random.choice(domains)}" # 生成随机地址 def random_location(): # 70%概率为中国 if random.random() < 0.7: country = 'CN' province_data = random.choice(CN_PROVINCES) province = province_data[0] city = random.choice(province_data[1]) else: country = random.choice(['US', 'JP', 'KR', 'UK']) if country == 'US': province_data = random.choice(US_STATES) province = province_data[0] city = random.choice(province_data[1]) else: province = country city = random.choice(OTHER_CITIES) district = f"District {random.randint(1, 20)}" return country, province, city, district # 生成随机生日（18-65岁） def random_birthdate(): end = datetime.now() - timedelta(days=365 * 18) start = end - timedelta(days=365 * 47) # 18-65岁 random_days = random.randint(0, (end - start).days) return (start + timedelta(days=random_days)).date() # 生成随机注册时间（2020-2023） def random_register_time(): start = datetime(2020, 1, 1) end = datetime(2023, 12, 31) random_seconds = random.randint(0, int((end - start).total_seconds())) return start + timedelta(seconds=random_seconds) # 生成用户密码哈希（简化版） def random_password_hash(): salt = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) return f"sha256${salt}${''.join(random.choices(string.hexdigits, k=64))}" def insert_massive_users(): total_records = 1_000_000 batch_size = 5000 batch_count = total_records // batch_size start_time = time.time() conn = pymysql.connect(**DB_CONFIG) cursor = conn.cursor() print(f"开始向user表插入{total_records:,}条数据...") for batch in range(batch_count): data = [] base_id = batch * batch_size + 1 for i in range(batch_size): user_id = base_id + i country, province, city, district = random_location() gender = random.choice(['M', 'F', 'O']) birth_date = random_birthdate() register_time = random_register_time() # 10%的用户没有最后登录时间 last_login = register_time + timedelta(days=random.randint(1, 365)) if random.random() < 0.1: last_login = None phone = random_phone(user_id) email = random_email(user_id) password = random_password_hash() data.append(( user_id, country, province, city, district, gender, birth_date, register_time, last_login, phone, email, password )) # 执行批量插入 sql = """ INSERT IGNORE INTO user ( id, country_code, province, city, district, gender, birth_date, register_time, last_login_time, phone, email, hashed_password ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ try: cursor.executemany(sql, data) conn.commit() except pymysql.err.IntegrityError as e: print(f"检测到重复条目错误: {e}") print("正在处理重复项...") # 逐条处理失败的数据 success_count = 0 for record in data: try: cursor.execute(sql, record) conn.commit() success_count += 1 except pymysql.err.IntegrityError: # 为手机号添加后缀修复冲突 record_list = list(record) record_list[9] += str(random.randint(0, 9)) # 在手机号后加随机数字 cursor.execute(sql, tuple(record_list)) conn.commit() success_count += 1 print(f"处理完成: {success_count}/{len(data)} 条数据成功插入") # 打印进度 processed = (batch + 1) * batch_size if (batch + 1) % 10 == 0 or batch == 0: progress = processed / total_records * 100 print(f"已插入: {processed:,}条 ({progress:.1f}%)") # 插入剩余记录 remaining = total_records % batch_size if remaining: data = [] base_id = batch_count * batch_size + 1 for i in range(remaining): user_id = base_id + i country, province, city, district = random_location() gender = random.choice(['M', 'F', 'O']) birth_date = random_birthdate() register_time = random_register_time() last_login = register_time + timedelta(days=random.randint(1, 365)) if random.random() < 0.1: last_login = None phone = random_phone(user_id) email = random_email(user_id) password = random_password_hash() data.append(( user_id, country, province, city, district, gender, birth_date, register_time, last_login, phone, email, password )) try: cursor.executemany(sql, data) conn.commit() print(f"最后一批插入完成: {remaining}条") except pymysql.err.IntegrityError: for record in data: try: cursor.execute(sql, record) conn.commit() except pymysql.err.IntegrityError: record_list = list(record) record_list[9] += str(random.randint(0, 9)) cursor.execute(sql, tuple(record_list)) conn.commit() # 统计结果 duration = time.time() - start_time print(f"插入完成! 总共插入 {total_records:,} 条用户记录") print(f"耗时: {duration:.2f} 秒") print(f"平均速度: {total_records/duration:,.0f} 条/秒") cursor.close() conn.close() if __name__ == "__main__": insert_massive_users()最左前缀原则演示查询复合索引 1：idx_loc (country_code, province, city)-- ✅ 有效查询1: 最左列精确匹配 SELECT * FROM user WHERE country_code = 'CN'; -- ✅ 有效查询2: 连续两列精确匹配 SELECT * FROM user WHERE country_code = 'US' AND province = 'California'; -- ✅ 有效查询3: 全三列精确匹配 SELECT * FROM user WHERE country_code = 'JP' AND province = 'Tokyo' AND city = 'Shibuya'; -- ❌ 无效查询1: 缺少最左列 SELECT * FROM user WHERE province = 'Guangdong' AND city = 'Shenzhen'; -- ❌ 无效查询2: 跳过中间列 SELECT * FROM user WHERE country_code = 'KR' AND city = 'Seoul'; -- ⚠️ 部分有效查询: 最左列范围查询 SELECT * FROM user WHERE country_code IN ('CN', 'TW') AND province = 'Fujian'; -- 仅country_code使用索引复合索引 2：idx_activity (register_time, last_login_time)-- ✅ 有效查询: 最左列范围查询 + 后续列精确 SELECT * FROM user WHERE register_time >= '2023-01-01' AND last_login_time IS NOT NULL; -- ❌ 无效查询: 非最左列单独查询 SELECT * FROM user WHERE last_login_time < '2023-06-01'; -- ⚠️ 特殊情况: 最左列精确 + 第二列范围 SELECT * FROM user WHERE register_time = '2023-05-15' AND last_login_time BETWEEN '2023-06-01' AND '2023-06-30'; -- 两列均可使用索引复合索引 3：idx_profile (birth_date, gender)-- ✅ 有效查询: 低区分度字段结合其他条件 SELECT COUNT(*) FROM user WHERE birth_date BETWEEN '1990-01-01' AND '1999-12-31' AND gender = 'F'; -- 出生日期筛选后性别区分度变高 -- ❌ 无效查询: 单独低区分度字段 SELECT * FROM user WHERE gender = 'M'; -- 全表扫描更高效性能对比测试数据查询场景使用索引10 万行耗时未用索引耗时国家 + 省份精确查询idx_loc2ms120ms城市单独查询(未使用)105ms105ms注册时间范围 + 最后登录精确idx_activity5ms95ms单独性别查询(全表扫描)90ms90ms索引设计实践建议1. 复合索引顺序黄金法则-- 推荐顺序：高频精确查询 → 范围查询 → 低区分度字段 CREATE INDEX idx_optimal ON user ( country_code, -- 精确过滤 >90% register_time, -- 范围过滤剩余10% gender -- 低区分度，但前两列过滤后更有效 );2. 覆盖索引设计-- 创建包含查询列的复合索引 CREATE INDEX idx_covering ON user ( country_code, province, city ) INCLUDE (register_time, last_login_time); -- MySQL 8.0+ 支持 -- 查询完全通过索引完成 SELECT country_code, province, register_time FROM user WHERE country_code = 'CN' AND province = 'Jiangsu';3. 防止索引失效的优化写法-- 原始低效查询： SELECT * FROM user WHERE YEAR(birth_date) = 1995; -- 函数导致索引失效 -- 优化写法： SELECT * FROM user WHERE birth_date BETWEEN '1995-01-01' AND '1995-12-31'; -- 模糊查询优化： -- 原始：WHERE city LIKE '%York' -- 前模糊索引失效 -- 优化：WHERE city LIKE 'New%' -- 前缀匹配可用索引索引失效情况1. 索引列参加了运算失效原因​​：当在 WHERE 子句中对索引列进行运算或函数处理时，优化器无法直接定位索引值-- 索引失效 SELECT * FROM users WHERE YEAR(birth_date) = 1990; -- 优化方案（使用范围查询） SELECT * FROM users WHERE birth_date BETWEEN '1990-01-01' AND '1990-12-31';优化建议​​：避免对索引列使用函数（UPPER(), LOWER(), DATE_FORMAT() 等）将运算移至等号右侧对于日期类型，使用范围查询代替日期函数2. 索引列使用了模糊查询，以 % 开始失效原因​​：前通配符破坏了索引的有序性-- 索引失效（全表扫描） SELECT * FROM products WHERE name LIKE '%apple%'; -- 优化方案（使用全文索引） ALTER TABLE products ADD FULLTEXT(name); SELECT * FROM products WHERE MATCH(name) AGAINST('apple');优化建议​​：限制后缀模糊查询（LIKE 'apple%'）使用全文检索（FULLTEXT）替代 LIKE %% 查询对于短文本考虑前缀索引（INDEX(name(10))）3. 索引列是字符串类型char varchar，但查询时省略了 '' 单引号失效原因​​：数据库会进行隐式类型转换，导致无法使用索引-- 索引失效（phone为varchar类型） SELECT * FROM users WHERE phone = 13800138000; -- 优化方案（正确使用引号） SELECT * FROM users WHERE phone = '13800138000';优化建议​​：始终为字符串值添加引号使用 ORM 框架的严格类型绑定开启 SQL_MODE 的 STRICT_TRANS_TABLES 避免隐式转换4. 查询条件中有 or ，有未添加的索引字段失效原因​​：MySQL 处理 OR 条件时，需要合并多个结果集-- 假设email有索引但mobile无索引 SELECT * FROM users WHERE email = 'test@example.com' OR mobile = '13800138000'; -- 优化方案（使用UNION或索引覆盖） (SELECT * FROM users WHERE email = 'test@example.com') UNION ALL (SELECT * FROM users WHERE mobile = '13800138000')优化建议​​：为 OR 条件中的所有字段添加单独索引使用 UNION ALL 替代 OR调整查询逻辑避免混合条件5. 查询的符合条件的记录在表中占比过大失效原因​​：当查询返回超过表行数的 15-20% 时，全表扫描比索引更快-- 假设status=1占全表80% SELECT * FROM orders WHERE status = 1; -- 优化方案（分批处理） SELECT * FROM orders WHERE status = 1 AND id > 10000 LIMIT 5000; -- 使用游标分页优化建议​​：添加限制条件（时间范围、ID 区间等）强制索引（FORCE INDEX(index_name)）分批查询 + 游标处理大数据集6. is null 和 is not null 的相关问题失效原理​​：当 NULL 值比例很低时（<5%），IS NULL 可能使用索引当 NULL 值比例很高时（>30%），IS NOT NULL 可能使用索引多数情况下 IS NOT NULL 会全表扫描类似 第5 查询的符合条件的记录在表中占比过大-- 最坏情况（全表扫描） SELECT * FROM users WHERE last_login_time IS NOT NULL; -- 优化方案（使用范围查询） SELECT * FROM users WHERE last_login_time > '1970-01-01';​​优化建议​​：避免可空字段，设置默认值对高选择性的 IS NULL 添加筛选索引：使用 COALESCE() 函数转换 NULL 值：指定索引(强制索引)当 MySQL 优化器没有选择最优索引时，我们可以强制指定索引来实现更高效的查询使用场景：优化器错误选择了效率较低的索引表中有多个相似索引但特定场景需要特定索引测试比较不同索引的性能差异-- 强制使用特定索引 SELECT * FROM orders FORCE INDEX (idx_order_date) WHERE customer_id = 100 AND order_date > '2023-01-01'; -- 建议使用索引（优化器可能拒绝） SELECT * FROM orders USE INDEX (idx_customer) WHERE status = 'shipped' AND amount > 1000; -- 忽略特定索引 SELECT * FROM orders IGNORE INDEX (idx_status) WHERE create_time < '2023-06-01';覆盖索引当索引包含所有查询字段时，称为覆盖索引，此时查询无需回表-- 包含所有查询字段的复合索引 CREATE INDEX idx_covering ON orders ( customer_id, order_date, amount ); -- MySQL 8.0+ 使用INCLUDE子句 CREATE INDEX idx_include ON orders (order_date) INCLUDE (customer_id, amount);前缀索引针对长字符串字段（如 CHAR/VARCHAR/TEXT），仅索引字段开头部分字符适用场景：VARCHAR(255) 字段但实际平均长度短TEXT 类型字段需要索引减少复合索引大小-- 对email字段前20字符创建索引 CREATE INDEX idx_email_prefix ON users (email(20)); -- 联合索引中的前缀索引 CREATE INDEX idx_name_prefix ON users ( last_name(10), first_name(5) );评估最优前缀长度SELECT COUNT(DISTINCT SUBSTRING(字段,1, 长度)) /COUNT(*) FROM table; 查询结果越接近 1 则此长度越适合索引创建原则表数据量庞大，通常超过百万宗数据。经常出现在where，order by，group by后面的字段建议添加索引。创建索引的字段尽是具有很强的唯一性。如果字段存储文本，内容较大，一定要创建前缀索引。尽星使用复合索引，使用单列索引容易回表查询。如果一个字段中的数据不会为NULL，建议建表时添加not null约束，这样优化器就知道使用哪个索引列更加有效。不要创建太多索引，当对数据进行增删改的时候，索引需要重新重新排序。如果很少的查询，经常的增刚改不建议加索引。SQL优化order by 优化优化原理与策略当使用 ORDER BY 时，避免文件排序（filesort）是关键目标。通过合理利用索引可将时间复杂度从 O(n log n) 降低到 O(n)-- 原始查询（可能导致 filesort） SELECT * FROM orders WHERE customer_id = 100 ORDER BY order_date DESC; -- 优化方案（添加组合索引） CREATE INDEX idx_customer_date ON orders(customer_id, order_date DESC);优化技术方案：​​索引排序覆盖​​：-- 索引 (customer_id, order_date) SELECT order_id, order_date FROM orders WHERE customer_id = 100 ORDER BY order_date DESC; -- 直接使用索引排序多字段排序优化​​：-- 错误示例： SELECT * FROM products ORDER BY category, price DESC; -- 混合排序方向 -- 优化方案（统一方向）： SELECT * FROM products ORDER BY category ASC, price ASC; -- 使用索引 (category, price)限制结果集大小​​：-- 文件排序优化 SELECT*FROM logs ORDERBY create_time DESC LIMIT 100; -- 优先返回最新 100 条性能对比：方法百万数据耗时资源消耗索引排序10-50ms低内存文件排序500-2000ms高磁盘文件排序5-30s非常高group by 优化优化核心技术：通过创建合适的索引将分组操作从临时表处理转为松散索引扫描（Loose Index Scan）-- 原始低效查询 SELECT category, COUNT(*) FROM products GROUP BY category; -- 优化方案 ALTER TABLE products ADD INDEX (category);limit 优化传统分页痛点-- 深度分页性能差 SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20; -- 读取100020行后丢弃前10万优化方案索引 + 子查询 ：SELECT o.* FROM orders or JOIN ( SELECT id FROM orders LIMIT 100000, 20 ) o ON o.id = or.id主键优化设计原则​​自增 INT/BIGINT 主键​​：CREATE TABLE users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, ... ) ENGINE=InnoDB;​​复合主键优化​​：-- 时间序列数据 CREATE TABLE metrics ( device_id INT, metric_time DATETIME(6), value FLOAT, PRIMARY KEY (device_id, metric_time) ) WITH CLUSTERING ORDER BY (metric_time DESC);​​主键性能影响对比​​：主键类型插入速度范围查询存储开销自增 INT100% (基准)优4 字节UUID35-60%差16 字节复合主键70-90%优不定​​分库分表全局 ID 方案​​：-- Snowflake ID 结构 -- 63位整型：1位符号+41位时间戳+10位节点ID+12位序列号 CREATE TABLE orders ( id BIGINT UNSIGNED PRIMARY KEY, ... );count 优化精确优化计数-- 原始（慢）： SELECT COUNT(*) FROM orders WHERE status = 'shipped'; -- 优化方案1（汇总表）： SELECT total_shipped FROM order_stats; -- 优化方案2（条件计数）： SELECT COUNT(id) FROM orders WHERE status = 'shipped'; -- 优于COUNT(*)insert 优化大文本推荐 LOAD DATA INFILE,需要先开启 set global local_infile=1;LOAD DATA INFILE '/tmp/orders.csv' INTO TABLE orders FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';update 优化避免全表锁 ： 即尽量不要让索引失效，否则将会由行级锁变为全表锁
此文章内容记录学习 JUC 时的各种问题问题虚假唤醒​​虚假唤醒​​是多线程编程中的一个典型问题，它是指线程在未被明确通知（如 signal() 或 signalAll()）的情况下从等待状态中唤醒。这可能导致程序在条件未满足时继续执行，从而引发逻辑错误为什么会发生虚假唤醒？​​底层操作系统机制​​操作系统底层的条件变量实现（如 POSIX 的 pthread_cond_wait()）允许在无显式信号时唤醒线程（通常出于性能考虑）。Java 作为跨平台语言无法完全避免这一问题。​​中断和超时机制​​当等待的线程被外部中断（interrupt()）或达到超时时间时，也可能意外唤醒。class SharedResource { private int item = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); // 生产者方法：非安全实现 public void produce() throws InterruptedException { lock.lock(); try { // 错误：使用if而非while检查条件 if (item != 0) { condition.await(); // 可能虚假唤醒后继续执行 } item++; System.out.println("生产: " + item); condition.signal(); } finally { lock.unlock(); } } // 消费者方法：非安全实现 public void consume() throws InterruptedException { lock.lock(); try { // 错误：使用if而非while检查条件 if (item == 0) { condition.await(); // 可能虚假唤醒后继续执行 } System.out.println("消费: " + item); item--; condition.signal(); } finally { lock.unlock(); } } public static void main(String[] args) { SharedResource share = new SharedResource(); for(int i=0; i<2; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { try { share.produce(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Producer" + i).start(); } for(int i=0; i<2; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { try { share.consume(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Consumer" + i).start(); } } }可以看到已经产生了虚假唤醒解决方案使用while代替ifclass SafeResource { private int value = 0; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); // 安全的生产者 public void safeIncrement() throws InterruptedException { lock.lock(); try { // 修复：使用while循环防御虚假唤醒 while (value != 0) { condition.await(); } value++; System.out.println(Thread.currentThread().getName() + " 安全生产后: " + value); condition.signalAll(); } finally { lock.unlock(); } } // 安全的消费者 public void safeDecrement() throws InterruptedException { lock.lock(); try { // 修复：使用while循环防御虚假唤醒 while (value != 1) { condition.await(); } value--; System.out.println(Thread.currentThread().getName() + " 安全消费后: " + value); condition.signalAll(); } finally { lock.unlock(); } } }死锁死锁是多线程编程中的一种严重问题，指​​两个或多个线程永久阻塞​​，每个线程都在等待其他线程释放资源的状态。死锁产生的四个必要条件​​互斥条件​​：资源只能被一个线程占用​​持有等待​​：线程持有资源同时等待其他资源​​不可剥夺​​：资源不能被强制抢占​​循环等待​​：多个线程形成资源等待环示例public class DeadLockExample { public static void main(String[] args) { Object a = new Object(); Object b = new Object(); new Thread(() -> { synchronized (a) { System.out.println("获取到了锁 a, 尝试获取锁 b"); try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("获取到了锁 b"); } } }, "A").start(); new Thread(() -> { synchronized (b) { System.out.println("获取到了锁 b, 尝试获取锁 a"); synchronized (a) { System.out.println("获取到了锁 a"); } } }, "B").start(); } }获取到了锁 a, 尝试获取锁 b 获取到了锁 b, 尝试获取锁 a解决方案从四个必要条件入手破坏请求条件破坏不可剥夺条件破坏互斥条件破坏循环等待条件练习创建四个线程，并按照 D -> B -> C -> A 的顺序执行class Control { private int flag = 0; private final int MAX_NUMBER = 4; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void wait(int target) throws InterruptedException { lock.lock(); try { while(flag % MAX_NUMBER != target - 1) { condition.await(); } flag++; System.out.println(Thread.currentThread().getName()); condition.signalAll(); } finally { lock.unlock(); } } } public class Test01 { public static void main(String[] args) { Control control = new Control(); new Thread( () -> { try { control.wait(4); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "A" ).start(); new Thread( () -> { try { control.wait(2); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "B" ).start(); new Thread( () -> { try { control.wait(3); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "C" ).start(); new Thread( () -> { try { control.wait(1); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "D" ).start(); } }输出D B C A
最近看到某论坛上有关闲鱼获取详细的帖子，便有了写个脚本的想法闲鱼h5的研究可以看到咸鱼的这个这个接口便是对应的数据url: https://h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail/1.0/param: jsv=2.7.2&appKey=34839810&t=1749660271468&sign=d99215598a87fc030a45a608269f6906&v=1.0&type=originaljson&accountSite=xianyu&dataType=json&timeout=20000&api=mtop.taobao.idle.pc.detail&sessionOption=AutoLoginOnly&spm_cnt=a21ybx.item.0.0data: {"itemId":"859199211604"}cookie: ..._m_h5_tk=xxx; _m_h5_tk_enc=xxx经过我多次尝试，只有t,sign,_m_h5_tk,_m_h5_tk_enc是变化的，现在目标就很明确了编码实现t为13位时间戳，而经过查阅资料可以分析出，token: _m_h5_tk sign：token&t&appKey&data,然后取MD5_m_h5_tk会有过期时间，通过开发者工具，这个地址https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/在首次访问时候，在header里面的set-cookie能找到_m_h5_tk,_m_h5_tk_enc，而首次访问所携带的token必为空，也就是说，计算sign只需要t和data即可代码实现def _get_m_h5_tk(self) -> list: """获取API认证所需的_m_h5_tk令牌""" # 构建一个简单的请求数据和参数 data = '{"piUrl":"https://h5.m.goofish.com/wow/moyu/moyu-project/xy-site/pages/announcement"}' # 生成时间戳 t = str(round(time.time() * 1000)) token = "" appKey = "34839810" # token + & + t + & + appKey + & + data_str sign_string = f"{token}&{t}&{appKey}&{data}" sign = md5(sign_string.encode()).hexdigest() params = { 'jsv': "2.7.2", 'appKey': "34839810", "t": t, "sign": sign, 'api': 'mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get', 'v': '1.0', "type": "originaljson", "accountSite": "xianyu", "dataType": "json", "timeout": "20000", "sessionOption": "AutoLoginOnly", "spm_cnt": "a21ybx.item.0.0", } # API请求地址 api = 'https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/' response = requests.get(api, params=params) # 从响应cookies中提取_m_h5_tk cookies = response.cookies _m_h5_tk = cookies.get('_m_h5_tk', '') _m_h5_tk_enc = cookies.get('_m_h5_tk_enc', '') return [_m_h5_tk, _m_h5_tk_enc]['08c31977d09c9c8c610114029b8e17a5_1749669468863', 'b8d02be3c6b0ee2a0f5a21d8c7023b93']对应的获取商品详细也能写出来了def get_info(self, url: str): """ 获取闲鱼商品信息 """ itemId = self._get_itemId(url) if itemId is None: logging.error("获取商品ID失败") return None cookies = self._update_tokens() # 准备参数 appKey = "34839810" api = "mtop.taobao.idle.pc.detail" v = "1.0" data_str = f'{{"itemId":"{itemId}"}}' # 生成时间戳 t = str(round(time.time() * 1000)) # 从 _m_h5_tk 中提取 token token = cookies['_m_h5_tk'].split('_')[0] # token + & + t + & + appKey + & + data_str sign_string = f"{token}&{t}&{appKey}&{data_str}" sign = md5(sign_string.encode()).hexdigest() # URL 参数 params = { "jsv": "2.7.2", "appKey": appKey, "t": t, "sign": sign, "v": v, "type": "originaljson", "accountSite": "xianyu", "dataType": "json", "timeout": "20000", "api": api, "sessionOption": "AutoLoginOnly", "spm_cnt": "a21ybx.item.0.0", # "spm_pre": "a21ybx.item.itemCnxh.2.165c3da6uwAGTv", } query = '&'.join([f"{k}={quote(str(v))}" for k, v in params.items()]) url = f"https://h5api.m.goofish.com/h5/{api}/{v}/?{query}" headers = { "Host": "h5api.m.goofish.com", "Connection": "keep-alive", "sec-ch-ua-platform": '"Windows"', "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0", "Accept": "application/json", "sec-ch-ua": 'Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99', "Content-type": "application/x-www-form-urlencoded", "sec-ch-ua-mobile": "?0", "Origin": "https://www.goofish.com", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "https://www.goofish.com/", "Cookie": self.xyCookie.strip() } data = f"data={quote(data_str)}" res = requests.post(url, headers=headers, data=data) res.encoding = "utf-8" if res.status_code != 200: logging.error("获取商品信息失败") return None data = self._format_item_info(res.json()) return data{'itemId': 859199211604, 'title': '出曲面屏笔记本电脑，新出厂的曲面屏[火][火]', 'desc': '出曲面屏笔记本电脑，新出厂的曲面屏[火][火]\n感兴趣的话点“我想要”和我私聊吧～', 'soldPrice': '1466', 'originalPrice': '0', 'transportFee': '0.00', 'image_urls': ['http://img.alicdn.com/bao/uploaded/i3/O1CN01uw42JI2Az5fSKioAd_!!4611686018427383553-0-fleamarket.jpg'], 'tags': [ '包邮'], 'browseCnt': 32499, 'wantCnt': 63, 'gmtCreate': 1733215983000}注意，此方法已经被我的其他方法封装过了，如self._get_itemId即获取商品id，self._format_item_info即格式化最后的json，因为数据过多，只保留我需要的即可
关于浏览器传参丢失精度的问题的解决办法
今天在写项目时候，需要前端传一个 number 类型的参数，可能由于类型过长，url接收时候直接丢失了精度，导致了一系列的错误前端源码const doEdit = () => { console.log(p.value.id) router.push({ path: '/add', query: { id: p.value.id }, }) }这里用控制台打印了 p.value.id 来确认是否是后端传参前端接收时丢失了精度，但是控制台打印是正常的结果 1927331787680264194, 但是看浏览器重重定向的地址为 http://localhost:8081/api/get/vo?id=1927331787680264200 明显是这里出现了问题问题出在 JavaScript 的数值精度限制上。JavaScript 使用 IEEE 754 双精度浮点数表示数字，最大可精确表示的整数是 2^53 - 1（约 16 位数字或 ±9007199254740991）所以出现了精度丢失的问题解决方案解决问题很简单 String(p.value.id) 只需把高精度的数字转换成 string 类型传入即可
vue使用 vue-waterfall-plugin-next 实现瀑布流Vue3瀑布流插件支持PC和移动端，包含Animate的各种动画效果以及图片懒加载功能。根据文档，可以先写出这样的一个例子 <Waterfall ref="waterfall" :list="list" :breakpoints="breakpoints"> <LazyImg class="card_img" :url="url" /> </Waterfall> 跨域问题文档提供的是本地图片，再用远程图片加载时候，不出意外地会出现跨域问题Access to image at 'https://emample.com/1.png' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 仔细研究文档，会看到 Props参数 里面有个跨域的参数参数名类型默认值描述crossOriginBooleantrue图片加载是否开启跨域把默认值修改成 false 即可，即 <Waterfall :crossOrigin="false">
最近在使用Mybatis-Plus的分页查询方法时候，发现并没有生效。查询的参数为{ "current": 1, "pageSize":"2" }但是实际把所有数据都返回了{ "code": 0, "data": { "records": [ { "id": 1923634783594762241, "userAccount": "admin", "createTime": "2025-05-17T07:00:46.000+00:00" }, ... ], "total": 0, "size": 2, "current": 1, "pages": 0 }, "message": "ok" }这里可以看到返回的 VO 中石油total size等字段的，但是没有生效，于是就去官网看文档,于 v3.5.9 起，PaginationInnerInterceptor 已分离出来。如需使用，则需单独引入 mybatis-plus-jsqlparser 依赖 ， 具体请查看 安装 一章。解决办法在 Maven bom标题下，可以看到使用 maven bom 管理依赖，减少版本号的冲突。因为 jsqlparser 5.0+ 版本不再支持 jdk8 针对这个问题解耦 jsqlparser 依赖。 正确打开姿势，引入 mybatis-plus-bom 模块，然后引入 ..starter 和 ..jsqlparser.. 依赖所以需要引入对应的包管理器和依赖<dependencyManagement> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-bom</artifactId> <version>3.5.9+ 版本</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>Spring Boot3 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> </dependency>Spring Boot2 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency>另外还需根据自己JDK版本引入不同的依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser-4.9</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> </dependency>此外，还需要在 Spring Boot 项目中，通过 Java 配置来添加分页插件@Configuration @MapperScan("scan.your.mapper.package") public class MybatisPlusConfig { /** * 添加分页插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加 // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType return interceptor; } }大功告成{ "code": 0, "data": { "records": [ { "id": 1923634783594762241, "userAccount": "admin", "userRole": "admin", "createTime": "2025-05-17T07:00:46.000+00:00" }, { "id": 1924441288589975553, "userAccount": "test001", "userRole": "user", "createTime": "2025-05-19T12:25:32.000+00:00" } ], "total": 10, "size": 2, "current": 1, "pages": 5 }, "message": "ok" }
这是以前刚学 JAVA 时候记录的，现在补充到博客上JAVA学习笔记变量局部变量和全局变量全局变量有默认值，而局部变量即方法中的变量无默认值，因此方法中的变量必须进行复制才能引用局部变量前面不能加修饰符（public，private，等）变量类型注意JAVA中默认的小数常量为double类型，若需要float类型，则在数字末尾添加f或者F当类型为double或是float时，0.*中的0可省略，例如 0.1 可写成 .1自动转换类型低精度到高精度可自动转换转换规则： char->int->long->float->double byte->short->int->long->float->doubleeg：int num = "a"; //a是由char类型转换为int类型，从低精度转换为高精度 char -> int double d = 80 // int -> double自由转换数据类型转为字符串只需要在数值后面加上 "" 即可eg:int a = 10; String b = a + "";String类型转其它需要引入Integer类eg：//转为Int String a = "aa"; int b = Integer.parseInt(a); //转为double double c = Double.parseDouble(a); //以此类推即可运算符+-*/不再赘述%取余a % b => a - a / b * b前++和后++作为独立语句时i++和++i作为独立语句时据等价于i = i + 1作为非独立语句时i++表示先赋值后运算++i表示先运算后赋值eg：int a = 1; int b = a++; //等价于 b = a,a = a + 1 int c = 1; int d = ++c; //等价于 d = c + 1,c = d或c = c + 1三元运算符基本语法条件表达式?表达式1:表达式2运算规则1.若条件表达式返回为True，则执行表达式22.若条件表达式返回为False，则执行表达式2int a = 1; int b = 2; int result = a > b?a++:b--; // 返回result=1 // 这里原来的a，b变量仍然不会改变Switch如下代码，表达式中返回一个结果，case中匹配常量和结果，若相同，则执行同级case下的语句，否则匹配下个case，若没有相同的常量，则默认执行default下的语句表达式的返回值必须是 byte short int srting char enumcase后的语句必须是常量或是 常量表达式，不能是变量当执行完一个case语句没有break语句时，会顺序执行到switch结尾，直到遇到break，注意，期间不管case是否匹配，都会执行下面的语句switch(表达式){ case 表达式结果等于这里的常量时: // 语句1 break; case 常量2: // 语句2 break; default: // 语句三 break; }do...while与while不同的是，while是先判断在执行循环体，do...while是先执行循环体在判断while不一定执行循环体，do...while至少执行一次循环体int count = 0; do{ count++; System.out.println(count); }while(count<=10);break细节通过lable可控制具体跳出的循环体若不指定跳出的循环体，则默认跳出离break最近的循环体lable1:{}break lable1;lable1: for(){ lable2: while(){ lable3 do{ }while(); break lable1; break lable2; break lable3; } }递归函数强烈推荐使用内存分析法下面是案例分析阶乘案例class A{ public int factorial(int num){ if n == 1{ return 1; }else{ return factorial(num - 1) * num; } } } A test = new A(); int ret = test.facaorial(5); System.out.println(ret); // 返回结果是120分析首先传入参数5，进入到第6行的factorial函数中，然后再在栈中新开辟一个内存空间假设为t1，t1的sum为4，然后再在执行factorial函数，在开辟t2，t2sum为3，....t4sum为1，所以t4返回值为1，t4空间销毁回收，返回给t3，即1sum，t3的sum为2，即1*2,返回给t2，返回值为2，然后2*3返回为6，t1是6*4返回给第一次计算的空间即24*5，所以最后结果为120可变参数当一个方法接收的参数类型一样且为多个时，可以用可变参数接收public void static main(String[] args){ class sum{ public int sum(int...nums){ # nums的数据类型相当于数组 System.out.println(nums.length); # 求和 int res = 0; for(int i = ;i < nums.length ; i++){ res += nums[i]; } } } }注意可变参数可以接受数组int...nums 中int为数据类型，... 不可动，nums为形参普通形参和可变参数放在一起时，可变参数要放在普通形参后面一个方法中只能有一个可变参数构造器注意事项格式： [修饰符] 类名(形参){}不能有return语句构造器可以重载构造器中如果有This语句，则This语句必须放在第一条class Person(){ String name; int age; public Person(String pname,int page){ name = pname; age = page } } Person p = new Person("名字",10); System.out.println(p.name + p.age); # 名字10This，Super关键字Super只能在构造器中使用，且This和Super不能同时使用This相当于Python整的self，在类中则代表当前对象Java示例如下class Dog{ String name; int age; public Dog(String name,int age){ this.name = name; this.age = age; } public void t1(){ System.out.println("t1()方法被调用"); } public void t2(){ // 调用t1的方法 this.t1(); } }Python示例如下class Dog: def __init__(self,name, age): self.name = name self.age = age def t1(): print("t1()方法被调用") def t2(): # 调用t1方法 self.t1()访问构造器注意访问构造器语法 this(); 必须放在构造器第一行class Dog{ String name; int age; public Dog(){ this("小明", 20); } public Dog(String name, int age){ this.name = name; this.age = age; System.out.println(this.name + this.age); } } Dog d = new Dog(); // 输出 小明20this(name)在继承中代表访问一个参数只有name的构造器class Person { public static void prt(String s) { System.out.println(s); } Person() { prt("父类·无参数构造方法： "+"A Person."); }//构造方法(1) Person(String name) { prt("父类·含一个参数的构造方法： "+"A person's name is " + name); }//构造方法(2) } public class test extends Person { test() { super(); // 调用父类构造方法（1） prt("子类·调用父类无参数构造方法： "+"A chinese coder."); } test(String name) { super(name);// 调用父类具有相同形参的构造方法（2） prt("子类·调用父类含一个参数的构造方法： "+"his name is " + name); } test(String name, int age) { this(name);// 调用具有相同形参的构造方法（3） prt("子类：调用子类具有相同形参的构造方法：his age is " + age); } public static void main(String[] args) { test cn = new test(); // cn = new test("codersai"); cn = new test("codersai", 18); } } // 输出如下 //父类·无参数构造方法： A Person. //子类·调用父类无参数构造方法： A chinese coder. //父类·含一个参数的构造方法： A person's name is codersai //子类·调用父类含一个参数的构造方法： his name is codersai //子类：调用子类具有相同形参的构造方法：his age is 18Supersuper.属性可以访问跟父类的属性，但不能访问父类的私有属性spuer.方法名(参数列表)可以访问父类的方法，但是不能访问父类私有的方法super()必须放在子类构造器的第一行，且不能与this同时存在访问修饰符类中的变量，函数都可用访问修饰符类也可以用访问修饰符，不过只能同 public 和 默认继承关键词 extends注意事项子类继承了所有的属性和方法，但是私有属性和方法不能在子类直接访问，要通过公用的方法去访问当创建子类对象时，不管子类使用哪个构造器，默认情况下会去调用父类的无参构造器，如果父类中没有无参构造器，则必须在子类的构造器中用 super()去指定父类哪个构造器来完成对父类的初始化工作，否则编译不通过。多态多态因为可以理解为多种形态方法的多态方法的重载，重写实际上就是多态的一种对象的多态1.一个对象的编译类型和运行类型可以不一致2.编译类型在定义对象时，就确定了，不能改变了3.运行类型是可以改变的4.编译类型看定义时 = 号 的左边，运行类型看 = 号 的右边class Animal{ //父类 private String Name; public void setName(String Name){ this.Name = Name; } public String getName(){ return Name; } } class Dog extends Animal{ //子类 private String Name = "Dog"; public String getName(){ return Name; } } class Cat extends Animal{ //子类 private String Name = " Cat"; public String getName(){ return Name; } } public static void main(String[] args){ Animal dog = new Dog(); //dog的编译类型是Animal，运行类型是Dog dog = new Cat(); //此时dog的编译类型还是Animal，运行类型变成了Cat }对象的多态，就是在调用类的时候传入的参数可以是一个对象，通过对象去调用其子类或本类的方法eg：public static void main(String[] args){ Animal dog = new Dog(); Animal cat = new Cat(); call(dog); //输出Dog is runing call(cat); //输出Cat is runing } public void call(Animal name){ System.out.println(name.getName + " is runing"); }向上转型向上转型就是 Animal dog = new Dog() dog去调用Dog类和Animal类的方法，弊端是假如说Dog中有A方法可以调用，Animal中的A方法为私有方法或者没有此方法，那么就无法编译通过。虽然是从子类到父类去寻找方法，但是由于Java特性，无法编译通过，于是有了向下转型向下转型书接上回，dog如何调用Dog类中独有的方法呢？就用到了向下转型，也可以理解为强转，转的 不是编译类型，因为编译类型确定后就无法改变，转的是运行类型。Animal dog = new Dog(); Dog dog1 = (Dog) dog;这样dog1对象就能调用Dog类的方法了。注意语法 子类类型 引用名 = （子类类型）父类引用;要求父类的引用必须指向的是当前目标类型的对象（也就是说上方代码第二行最后一个的dog的运行类型必须是dog1的编译类型）变量不能重写，以向上转型为例，Animal animal = new Dog(); System.out.print(amimal.name) 假设父类Animal和子类Dog都有公共的变量name，则以编译类型的变量为主，打印出来的就是Animal的name。属性看遍历类型，方法看运行类型。instanceof语法： 对象A instanceof 类B 返回布尔值判断对象A的运行类型的类是否为类B或类B的子类，若是则为True，反之为False动态绑定机制当调用对象方法时候，该方法会和该对象的内存地址/运行类型绑定当调用对象属性时，没有动态的绑定机制，哪里声明哪里使用。class A { //父类 public int i =10; public int sum(){ return getI + 10; } public int sum1(){ return i + 10; } public int getI(){ return i; } } class B extends A { //子类 public int i = 20; public int sum(){ return i + 20; } public int getI(){ return i; } public int sum1(){ return i + 10; } } // main方法 A a = new B(); //向上转型 System.out.println(a.sum()); //40 System.out.println(a.sum1()); //30！！！如果删除掉子类B的sum()方法，则调用 a.sum()时候会继承A的sum()方法，返回的时 getI() + 10，而由于动态绑定机制，getI()指向的是a的运行类型也就是B类中的getI()方法，所以此时返回的是30多态数组定义：数组的定义为父类类型，厘米那的实际元素类型为子类类型。代码块代码块和方法不同没有方法名，没有返回值，没有参数，只有方法体，而且不用通过对象或类显式调用，而是在加载类时，，或创建类时隐式调用基本语法[修饰符]{ Code }；注意修饰符为可选项，要写的话，也只能写static代码块分为两类，静态代码块即用static修饰的代码块和普通代码块即没有修饰符的末尾;可写可不写代码块什么时候被加载创建对象实例时创建子对象实例时候，父类的代码块也会被加载（父类的代码块先加载，接着加载子类的代码块）使用类的静态成员时（静态方法，静态变量）ps：使用类的静态成员时普通代码块即非静态代码块不会被执行静态代码块只会被执行1次，非静态的没创建一次就会被执行一次优先级静态代码块和静态属性的优先级一样倘若有多个静态代码块和多个静态属性，则按照书写顺序加载class A{ private static int num = getNum(); static { System.out.println("静态代码块初始化"); } public int getNum(){ System.out.println("getNum方法被调用"); return 100; } } new A();！！！当实例化A类时候，优先初始化第一个静态属性即 num，此时 num调用了 getNum方法，所以优先输出 getNum方法被调用，然后再初始化静态代码块静态代码块和静态属性的优先级高于普通代码块和普通属性普通代码块和普通属性的优先级一样构造器的优先级是最低的class AAA{ public AAA(){ //super(); //调用本类的普通代码块 System.out.println("AAA类的构造器被调用"); } } class BBB extends AAA{ { System.out.println("BBB的普通代码块被调用"); } public BBB(){ //super(); //调用本类的普通代码块 System.out.println("BBB类的构造器被调用"); } } new BBB();每个类的构造器下都有隐藏的代码，调用 super()调用本类的普通代码块所以上述代码的运行结果就是 1 AAA类构造器 2 BBB普通代码块 3 BBB构造器总结执行顺序如下父类的静态代码块和静态属性（优先级一样，按定义顺序执行）子类的静态代码块和静态属性（优先级一样，按定义顺序执行）父类的普通代码块和普通属性（优先级一样，按定义顺序执行）父类的构造方法子类的普通代码块和普通属性（优先级一样，按定义顺序执行）子类的构造方法final关键字结论不能被继承，重写，修改里面的值三种赋值方式定义时构造器中代码块中如果final修饰的属性是静态的，那么只能在代码块和定义时赋值。final修饰的类不能被继承，一般final修饰类后，方法就不用加final修饰了final和staticfinal和static同时使用不会创建类、class Test{ public final static int num = 555; static{ System.out.println("静态代码块被调用"); } }假如只有static没有final，那么调用 Test.num时候静态代码块也会被调用，若加上final，则不会调用静态代码块。抽象类 abstract抽象类就是没有方法体的方法，方便子类继承重写方法，抽象类和抽象方法的定义需要用到 abstract关键字当一个类中存在抽象方法时，该类必须声明为抽象类abstract class Animal{ abstract public void eat(); }细节抽象类不能被实例化抽象类不一定要有抽象方法abstract关键字只能修饰类和方法，不能修饰属性如果一个类继承了抽象类，则必须实现抽象类的所有抽象方法，否则子类也要声明为抽象类抽象方法不能用 private,static,final关键字修饰，因为这些关键字是与重写相违背的接口 interFace基本语法定义接口//接口类中可省略abstract和publicpublic interface Method{ //属性 //方法（1.抽象方法 2.默认default方法 3.静态static方法） void job(); //接口类中可省略abstract和public } //实现（重写接口中的方法） public class Mian implenets Method{ public void job(){ System.out.println("Hello World"); } } public class T1(){ public void run(interface Method){ Method.job(); } } //调用 public class void T2{ public static void main(String[] args){ T1 tt = new T1(); Main mm = new Main(); tt.run(mm); } }注意如果一个类实现了一个接口方法，即 一个类 implenets 接口 则这个类必须实现这个接口的所有的抽象方法在JDK8之后，可以有静态方法如果用到非抽象方法，需要加上default的关键字接口不能被实例化一个类可以同时实现多个接口接口中的属性，只能是final的，且为 public static final，假设我写了一个int x = 1,那么它就是public static final int x = 1;（必须初始化）接口不能继承类，但是接口可以继承extends多个接口内部类局部内部类（有类名）局部内部类可以直接访问外部类的所有成员对象，包括私有的内部类不能用除final的修饰符修饰如果外部类和局部内部类的成员名重名时，遵循就近原则，如果要调用外部类则需要 外部类名.this.成员名匿名内部类（没有类名）基于接口首先定义一个接口innerTest，使其拥有一个test方法以下是内部匿名类的用法，通过实例化一个接口并重写其方法来实现匿名内部类下图是上图的底层源码匿名内部类实际上是有名字的，我们看不到，系统自动去分配，而他的底层源码就是上图所示的形式上图的编译类型是接口的innerTest类型，而他的运行类型则是个底层代码的te的类型基于类跟基于接口的类似很牛逼的使用匿名内部类匿名内部类可以直接当作一个参数使用interface IA{ void test(); } class main{ public static void method(IA i){ i.test(); } public static void main(String[] args){ method(new IA(){ public void test(){ System.out.println("世界名画！"); } }); } } // 输出 世界名画细节匿名类有两种调用模式，第一种就是上面使用的的，利用可动态绑定机制，第二种可以直接去调用class outer{ new test(){ public void test(){ System.out.println("我重写了test函数"); } }.test(); } class test{ public void test(){ System.out.println("test"); } }外部其他类不能访问内部类成员内部类成员内部类是定义在外部类的成员位置成员内部类可以访问外部类的所有成员，包括私有的可以添加任意的访问修饰符，因为它本身就是一个成员如果外部类和成员内部类的方法或者属性变量重名，依然遵守就近原则，访问外部类：外部类.this.方法名/属性名外部其他类调用成员内部类方法一// 假设outer为外部类，inner为成员内部类 outer.inner test = new outer.inner();方法二在外部类中写一个方法返回内部类的对象静态内部类说明：静态内部类仍然定义在外部类的成员位置，只是多了个static修饰可以直接访问外部类的静态成员，包括私有的，但是不能直接访问非静态的因为他也是成员内部类，所以可以使用修饰符（不太一样，无this）如果外部类和成员内部类的方法或者属性变量重名，依然遵守就近原则，访问外部类：外部类.方法名/属性名调用方法同上枚举自定义枚举构造器私有化创建一个静态成员new一个所需的对象，用static final修饰class test{ public int i; public static final test CREATEST1 = new test(1); public static final test CREATEST2 = new test(2); public static final test CREATEST3 = new test(3); private test(int i){ this.i = i; } } // static 和 final 一起使用时候会使类不会被加载 // 调用就直接 test.CREATEST1.XXX // 枚举对象名一般大写关键字enum优化后的代码enum test{ CREATEST1(1),CREATEST2(2),CREATEST3(3),WHAT; public int i; private test(int i){ this.i = i; } private test(){ } }注意使用关键字enum代替classpublic static final test CREATEST1 = new test(1);直接使用CREATEST1(1)代替，若有多个则用逗号隔开CREATEST1(1) 括号里面是实参，对应构造器的形参CREATEST1(1)必须写在最前面如果使用无参构造器，则可以省略 ( ) 直接写常量对象名使用enum定义枚举时候，就不能在继承其他类了，因为它已经隐式的继承了enum类了，然仍然能实现接口注解/元数据@Override该注解表示方法重写了父类的某个方法该注解只能用在某个方法上如果写了@Override注解，java编译器在编译时候会寻找父类中是否含有该方法，如果真的被重写，则编译通过。否则就抛出异常@Deprecated表示程序元素（类，方法）已经过时了可以修饰 类 方法 参数 包 字段等等过时并不能代表不能用@SuppressWarnings抑制编译器警告用法@SuppressWarnings{""}{" "}里面写抑制的类型，all 代表一直所有警告@Target该注解是修饰注解的注解，称为元注解集合List基本方法（实现了Collection的接口方法）add：添加单个元素remove：删除元素（可指定索引或指定删除的对象）contains：查找元素是否存在size：获取元素个数isEmpty：判断是否为空clear：清空addAll：添加多个元素（可以放实现了Collection接口的对象，比如说集合）containsAll：查找多个元素是否都存在（可以放实现了Collection接口的对象，比如说集合）removeAll：删除多个元素（可以放实现了Collection接口的对象，比如说集合）迭代器遍历Iterator iterator = coll.iterator() //得到一个集合的迭代器.hasNext()是否还有下一个元素 编译类型是Object.next() 显示当前元素内容，并对指针进行下移第二次遍历需要重置迭代器，否则就会抛出异常iterator = coll.iterator() //重置迭代器IDEA快速生成whlie循环遍历的快捷键：ititIDEA快速生成增强for循环遍历的快捷键：I增强for循环遍历for(元素类型(Object) name:col){ System.out.println(clo); }SetSet接口介绍无序的（添加和取出的顺序不一样），没有索引不允许有重复的元素，所以最多包含一个null注意：取出的顺序虽然是无序的，但是是固定的，底层算法决定的方法说明因为set没有索引，故不能用get方法取出，遍历的话同上实现了List的接口的类一样，增强for循环和迭代器add方法说明，add方法在这里有一个布尔类型的返回值，用来判断是否添加成功remove 删除指定对象Mapkey:value方法说明put(key,value) 添加：key的值是唯一的不能重复，value的值可以重复，假如说key的值重复了，那么则会替换先前存在key的value的值(Hashtable的key和value都不能为空)remove(key)根据键删除对应的映射关系get(key)获取对应key的value 返回值类型为objectsize()返回k-v的个数isEmpty()判断是否为空clear()清空键值对containsKey(key)查找键是否存在keySet() 返回一个set集合，集合里面是所有的key遍历的六大方式（比List和Set稍微复杂点，但是基本原理相同）利用 keySet()方法获取所有的key，然后用增强for循环get到每一个valueMap map = new HashMap(); map.put("no1","Lin"); map.put("no2","Kenen"); map.put("no3","f"); map.put("no4","z"); Set set = map.keySet(); for(Object i:set){ System.out.println(map.get(i)); }迭代器Iterator iterator = map.keySet().iterator(); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println(map.get(key)); }增强for 同上迭代器 同上entrySet set = map.entrySet(); for (Object i:set){ Map.Entry m = (Map.Entry) i; System.out.println(m.getKey()+"-"+m.getValue()); }Collections工具类Collections类能对Set，List，Map等集合进行操作常用方法reverse(List) ：反转列表shuffle(List) ：对列表进行随机排序sort(List) ：排序sort(List,Comparatoe) ：根据Comparator产生的顺序对列表排序swap(List,int,int) ：将指定List的集合的两个元素进行交换位置copy(desp,list) ：将list的内容拷贝到desp中。注意：desp的长度必须>=list的长度，都则会抛出异常boolean replaceALL(List,old,new) ：将List集合中的old值全部替换为new的值frequency(Collection,Object) ：返回指定集合中指定元素出现的次数泛型泛型的通配和继承泛型不具备继承机制 List<Object> list = new list<String>(); //错误<?> 支持任意泛型<? extends A>支持A类及A类的子类，规定了泛型的上限<? super A> 支持A类及A类的父类，规定了泛型的下线举例public void test(List<?> list){ for(Object o:list){ System.out.println(o); } } public void test(List<? extends AA> list){ for(Object o:list){ System.out.println(o); } } public void test(List<? super AA> list){ for(Object o:list){ System.out.println(o); } }多线程创建线程的两种方式继承Thread类，重写run方法实现Runnable接口，重写run方法*为什么是开启新线程是start方法而不是run方法？进入start方法可以看到，最主要开启的新的线程的方法是start0()方法，这个是JVM的内部方法，底层是c/c++编写，实际就是由底层创建了一个新的线程去调用了run方法代码示例//继承实现 public class Cat extends Thread{ @Override public void run(){ int count = 0; while (true){ System.out.println("我是小猫，喵喵喵"+ (++count)); if (count > 8){ break; } try { Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } } } } //实现Runnable接口实现 public class Dog implements Runnable{ @Override public void run(){ int count = 0; while (true){ System.out.println("我是小狗，汪汪汪"+ (++count)); if (count > 8){ break; } try { Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } } } } //主方法 public class Test { public static void main(String[] args) { Cat cat = new Cat(); Dog dog = new Dog(); cat.start(); Thread thread = new Thread(dog); thread.start(); } }总结继承了Thread的类可以直接调用start方法，但由于Java是单继承机制，所以可以实现Runnable接口，此时开启新线程就需要在创建一个新的Thread的类，并传入实例化的对象，然后调用其start的方法区别从Java的设计来看，继承Thread和实现Runnable没有本质的区别实现Runnable的接口方式更适合多个线程共享一个资源的情况，并避免了单继承机制的限制class Te implements Runnable{ public void run(){} } class main{ public static void main(String[] args){ Te t0 = new Te(); Thread t1 = new Thread(t0); Thread t2 = new Thread(t0); t1.start; t2.start; } }线程常用方法setName：设置线程名称getName：获取线程名称start：开始执行线程run：调用线程run方法setPriority：更改线程优先级setPriority：获取线程优先级sleep：休眠interrput：中断线程，不是结束线程 一般用于zheng'zai线程插队yield：线程的礼让，让出CPU，让其他线程执行，但是礼让时间不确定，所以礼让不一定成功join：线程的插队。插队的线程一旦插队成功，则肯定先执行完插入的线程的所以任务（会阻塞线程（主线程也会被阻塞））守护线程引入当子线程是一个无限循环的线程时，主线程退出，子线程并不会退出，为了此时让子线程也退出，就用到了守护线程解决办法线程名.setDaemon(true);Synchronized线程同步机制在多线程编程，一些敏感的数据不允许被多个线程同时访问，此时就是使用同步访问技术，保证数据在任何同一时刻，最多只有一个线程访问，以保证数据的完整性。也可以理解成：线程同步：即当有一个线程对内存进行操作时，其他线程都不可以对这个内存进行操作，知道该线程完成操作，其他线程才能对该内存进行操作实现同步的具体方法-Synchronized同步代码块synchronized (对象){ // 这里放需要被同步的代码 } public void m1(){ // Code synchronized (this){ // Code } }Synchronized放在方法声明中，表示整个方法为同步方法public synchronized void m1(){ // 这里放需要被同步的代码 }注意选择同步代码块的对象this 是指本对象，如果多个new对象对其进行start，那么是锁不住的，所以推荐采用实现Runnable接口的方法实现多线程线程的死锁介绍多个线程占用了对面的资源，但不肯相让，导致了死锁。释放锁会释放锁的操作当前线程的同步方法，同步代码块执行完毕当前线程在同步方法，同步代码块中遇到了return或break当前线程在同步方法，同步代码块中遇到了Error或Exception，导致异常结束当前线程在同步方法，同步代码块中执行了wait()方法，当前线程暂停，并释放锁不会释放锁的操作当前线程在同步方法，同步代码块中执行了 Thread.sleep() 或者 Thread.yield()方法当前线程在同步方法，同步代码块时，其他线程调用了该线程的suspend()方法，该线程将会被挂起，不会被释放四大核心函数式接口函数式接口称谓参数类型用途Consumer<T>消费性接口T对类型为T的对象应用操作，包含方法 void accept(T t)Supplier<T>供给型接口无返回类型为T的对象，包含方法 T get()Function<T,R>函数型接口T对类型为T的对象应用操作，并返回结果，结果是R类型的对象，包含方法 R apply(T t)Predicate<T>判断型接口T确定类型为T的对象是否满足约束，并返回 boolean值，包含方法 boolean test(T t)public class MyTest { // Consumer Interface Demo // 和 JavaScript 的 CallBack 有异曲同工之妙 public void consumer(Integer num, Consumer<Integer> consumer) { consumer.accept(num); } @Test public void testConsumer() { // 可以这么理解 consumer(x,callback) consumer(500,num -> System.out.println("The number is " + num)); } // Supplier Interface Demo public <T> T supplier(Supplier<T> supplier) { return supplier.get(); } public String hello(){ return "hello"; } @Test public void testSupplier() { System.out.println(supplier(() -> hello())); } // Function Interface Demo public <T,R> R function(T t,Function<T,R> function){ return function.apply(t); } @Test public void testFunction() { String function = function(100, num -> String.valueOf(num)); System.out.println(function); } // Predicate Interface Demo public <T> void predicate(T t,Predicate<T> predicate) { System.out.println(predicate.test(t)); } @Test public void testPredicate() { predicate(100,num -> num == 1000); }