[{"content":"补充支付相关项目经验 图灵商城项目支付流程 全局概览 支付通道: 当前实现了支付宝当面付（F2F）二维码支付。 关键流程: 下单锁库存 → 生成支付二维码 → 支付宝异步回调验签 → 本地事务更新订单状态 → 事务消息提交 → 商品服务消费消息扣减真实库存（含幂等）。 一致性策略: RocketMQ事务消息 + 本地事务日志 + 定时回查/超时关单，确保“支付-订单-库存”最终一致。 幂等与安全: 使用事务ID落库做幂等校验；支付宝回调做签名验签。 端到端流程（按时间线） 下单并锁定库存（订单状态=待付款） 在生成订单时锁定库存，订单状态初始化为待付款，后续支付成功才扣减真实库存。 如果采用“异步下单+事务消息”，还会发一条“创建订单”的事务消息，用于后续“超时取消”。 生成支付宝支付二维码 控制器 /order/tradeQrCode 调用交易服务生成当面付二维码，设置回调 notifyUrl，并将二维码路径写回订单。 这里所需参数，以及如何封装，在demo中皆已提供\n// ... 省略若干构建请求参数 ... .setNotifyUrl(tradePayProp.getPaySuccessCallBack()+\u0026#34;/1\u0026#34;)// 支付成功回调地址，/1 表示支付宝 .setGoodsDetailList(goodsDetailList); log.info(\u0026#34;alipay callback url:---\u0026gt;\u0026#34;+builder.getNotifyUrl()); AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder); // 成功后生成二维码图片并返回其http访问路径 支付宝异步回调（验签 + 触发支付成功逻辑） 回调入口 /order/paySuccess/{payType}；验签后取 out_trade_no 作为订单号，调用支付成功处理。 验证签名通过后，即开始正式扣减库存\n// 1. 收集参数并验签 boolean isPassed = AlipaySignature.rsaCheckV2(param, Configs.getAlipayPublicKey(),\u0026#34;utf-8\u0026#34;,Configs.getSignType()); // 2. 取出订单号并执行业务 if(isPassed){ Long orderId = Long.parseLong(param.get(\u0026#34;out_trade_no\u0026#34;)); int count = portalOrderService.paySuccess(orderId,payType); // 写回给支付宝 \u0026#34;success\u0026#34;/\u0026#34;unSuccess\u0026#34; } 订单服务：支付成功的本地事务 + 事务消息 旧版是“直接改订单 + 远程扣库存”，目前核心实现改为“发事务消息 → 本地事务更新订单 → 提交消息由商品服务扣库存”，更安全。 // 使用事务消息机制发送扣减库存消息（包含 orderId/payType/明细列表/transactionId） return reduceStockMsgSender.sendReduceStockMsg(orderId,payType,orderDetail)? 1 : -1; 事务消息发送封装（携带事务ID用于幂等） String destination = \u0026#34;reduce-stock\u0026#34;; String transactionId = UUID.randomUUID().toString(); Message\u0026lt;StockChangeEvent\u0026gt; message = MessageBuilder.withPayload(stockChangeEvent) //传递事务ID，用于回查事务是否执行 .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId) .setHeader(\u0026#34;orderId\u0026#34;,orderId) .setHeader(\u0026#34;payType\u0026#34;,payType) .build(); // destination：目的地(主题)，这里发送给reduce-stock这个topic // message：发送给消费者的消息体，需要使用MessageBuilder.withPayload() 来构建消息 // RocketMQ事务监听器：修改订单状态、回查支付状态 TransactionSendResult sendResult = extRocketMQTemplate.sendMessageInTransaction(destination,message,orderId); 订单侧的“事务监听器”本地事务：更新订单状态=待发货、设置支付方式/支付时间，并记录事务日志，返回 COMMIT Long orderId = Long.parseLong(String.valueOf(arg)); String transactionId = (String) message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID); Integer payType = Integer.valueOf((String)message.getHeaders().get(\u0026#34;payType\u0026#34;)); // 修改订单状态 + 记录事务日志 portalOrderService.updateOrderStatus(orderId,payType,transactionId); return RocketMQLocalTransactionState.COMMIT; 事务状态回查：靠“本地事务日志”判断是否已执行过 int existTx = omsOrderMapper.isExistTx(transactionId); return existTx \u0026gt; 0 ? COMMIT : UNKNOWN; 商品服务：消费“扣库存”消息并落幂等日志 监听 reduce-stock 主题，按订单明细扣减真实库存；使用 transactionId 做幂等。 @RocketMQMessageListener(consumerGroup = \u0026#34;${rocketmq.consumer.group}\u0026#34;,topic = \u0026#34;${rocketmq.consumer.topic}\u0026#34;) public class ReduceStockMsgConsumer implements RocketMQListener\u0026lt;StockChangeEvent\u0026gt; { public void onMessage(StockChangeEvent stockChangeEvent) { stockManageService.reduceStock(stockChangeEvent); } } // 幂等校验：存在事务记录直接返回 if(skuStockMapper.isExistTx(stockChangeEvent.getTransactionId())\u0026gt;0){ return; } // 扣减冻结库存（真实库存） skuStockMapper.updateSkuStock(stockChangesList); // 记录事务日志用于幂等 skuStockMapper.addTx(stockChangeEvent.getTransactionId()); 超时未支付的订单处理（订单创建事务消息 + 回查 + 取消） 下单时会发“创建订单”的事务消息，监听器会用 Redis 记录回查次数，循环查询支付宝交易状态；超过次数未支付则关单并发送取消消息，消费者释放锁定库存。 if(retryTimes \u0026gt;= maxTryMums){ // 订单置为无效，返回 COMMIT（触发取消订单消费者释放锁定库存） } else { // 查询支付宝支付状态，已支付则 ROLLBACK（丢弃消息），未支付则回查次数+1返回 UNKNOWN } @RocketMQMessageListener(... topic = \u0026#34;${rocketmq.tulingmall.asyncOrderTopic}\u0026#34;) public void onMessage(String message) { // 取消订单 \u0026amp; 释放锁定库存 omsPortalOrderService.cancelOrder(orderId,memberId); } 关键状态与类型 /*订单状态：0待付款；1待发货；2已发货；3已完成；4已关闭；5无效订单*/ public static final int ORDER_STATUS_UNPAY = 0; public static final int ORDER_STATUS_UNDELIVERY = 1; // ... /*支付方式：0未支付；1支付宝；2微信*/ public static final int ORDER_PAY_TYPE_ALIPAY = 1; 安全与配置要点 支付宝验签: 使用平台公钥与一致的 sign_type 验签；回调里剔除 sign_type 字段参与验签。 回调地址: 通过配置 trade.zhifu.qrcode.paySuccessCallBack 注入，生成二维码时带上 notifyUrl。 配置加载: 支付宝参数读取 zfbinfo.properties。 private static String alipayPublicKey; // 支付宝RSA公钥，用于验签支付宝应答 private static String signType; // 签名类型 public synchronized static void init(String filePath) { configs = new PropertiesConfiguration(filePath); ... } 面试亮点与可复述要点 支付对账与一致性: 采用“支付回调 + 事务消息 + 本地事务日志 + 幂等 + 超时回查”的组合拳，确保支付触发的后续库存扣减“最终一致”。 库存两阶段: 下单先“锁定库存”，支付成功后“扣减真实库存”，取消/超时释放锁定库存，避免超卖。 事务消息落地: 订单服务在本地事务内更新订单并记录 transactionId，提交消息后由商品服务消费；两端均用“事务日志”做幂等。 回查策略: 未支付场景通过消息事务回查 + Redis计数 + 最多重试 + 关单，使系统可在外部系统不可靠时自愈。 安全合规: 回调验签、过滤 sign_type、仅认 out_trade_no 作为业务订单号，回调响应“success”/“unSuccess”。 可优化建议（面试加分） 补齐微信支付实现与统一支付抽象（策略模式），兼容多通道。 回调参数签名验签后做参数完整性校验（金额、商户号等）与订单状态幂等检查，补齐退款回滚流程。 结合“事务外盒/本地消息表”进一步强化可靠投递与失败补偿。 使用延迟消息替代回查或优化回查间隔与次数策略。 要点小结\n支付宝当面付二维码 → 回调验签 → 订单本地事务更新 + 事务消息 → 商品服务幂等扣库存。 订单创建事务消息 + 回查机制 → 超时关单并释放锁定库存。 事务ID日志做幂等；核心在“事务消息+最终一致性”的工程化实现。 面试中，支付问题点 支付相关的项目经验核心关注的是与支付渠道（如支付宝、微信支付等）的对接逻辑、订单与支付的联动流程、以及支付环节中的技术细节（如接口调用、签名验证、回调处理、异常场景应对等）。而支付宝订单流程的对接通常会涉及这些关键环节：\n比如调用支付宝 SDK 生成支付参数、发起支付请求； 处理支付宝的同步 / 异步回调（验证签名、解析支付结果、更新订单状态）； 可能涉及的支付超时处理、重复支付校验； 甚至可能包括与业务系统（如订单系统、库存系统）的联动逻辑等。 面试时，你可以重点阐述这部分经验的细节，比如：\n具体负责了哪些环节（如支付参数组装、回调接口开发、订单状态同步等）； 对接过程中遇到的技术问题（如签名失败、回调重复触发、网络超时等）及解决方案； 如何保证支付流程的安全性（如签名验证、参数加密）和数据一致性（如分布式事务、幂等性处理）。 这些细节能体现你对支付流程的理解和技术实现能力，足以证明你具备支付相关的项目经验。如果还有其他相关延伸（如退款流程、对账逻辑等），也可以补充说明，进一步丰富你的经验展示。\n","date":"2025-11-10T13:10:26+08:00","permalink":"https://readnovel.org/posts/%E5%AF%B9%E6%8E%A5%E6%94%AF%E4%BB%98%E5%AE%9D%E8%AE%A2%E5%8D%95%E6%B5%81%E7%A8%8B/","title":"对接支付宝订单流程"},{"content":"频繁FullGC问题如何排查 频繁的Full GC（完全垃圾收集）通常表明应用程序在内存管理方面存在问题，可能导致性能下降。以下是排查步骤和一个详细的示例：\n排查步骤 收集GC日志 分析GC日志 监控JVM内存使用情况 分析堆内存 检查代码中的内存使用 调整JVM参数 优化代码 验证改进 详细示例 假设我们有一个大型电子商务网站，最近用户反馈系统响应变慢。运维团队发现服务器频繁出现Full GC，严重影响性能。\n1. 收集GC日志 首先，我们需要开启详细的GC日志。在JVM参数中添加：\n-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log 2. 分析GC日志 使用工具（如GCViewer）分析GC日志，我们发现：\nFull GC频率：每10分钟一次 每次Full GC耗时：平均3秒 Old Gen使用情况：每次GC后仍然保持在80%以上 GC日志片段示例：\n2023-05-15T14:30:45.123+0800: [Full GC (Ergonomics) [PSYoungGen: 20M-\u0026gt;0M(60M)] [ParOldGen: 180M-\u0026gt;175M(200M)] 200M-\u0026gt;175M(260M), [Metaspace: 30M-\u0026gt;30M(1024M)], 3.2345678 secs] [Times: user=10.23 sys=0.25, real=3.23 secs] 3. 监控JVM内存使用情况 使用工具如VisualVM或JConsole实时监控JVM内存使用。我们观察到：\nEden区：频繁被填满后触发Minor GC Survivor区：经常接近满载 Old Gen：持续增长，即使在Full GC后也难以下降 4. 分析堆内存 使用jmap生成堆转储文件：\njmap -dump:format=b,file=heap_dump.hprof \u0026lt;pid\u0026gt; 使用MAT（Memory Analyzer Tool）分析堆转储，发现：\n大量的com.example.Order对象占用了Old Gen的大部分空间 这些Order对象中包含了大量历史订单数据 5. 检查代码中的内存使用 审查相关代码，发现问题：\npublic class OrderService { private static final List\u0026lt;Order\u0026gt; allOrders = new ArrayList\u0026lt;\u0026gt;(); public void processOrder(Order order) { // 处理订单 allOrders.add(order); // 问题所在：持续添加订单到静态列表 } // 其他方法... } 6. 调整JVM参数 临时调整JVM参数以缓解问题：\n-Xms4g -Xmx4g -XX:NewRatio=2 -XX:SurvivorRatio=8 7. 优化代码 修改代码以解决根本问题：\npublic class OrderService { private static final int MAX_ORDERS = 10000; private static final Queue\u0026lt;Order\u0026gt; recentOrders = new LinkedList\u0026lt;\u0026gt;(); public void processOrder(Order order) { // 处理订单 recentOrders.offer(order); if (recentOrders.size() \u0026gt; MAX_ORDERS) { recentOrders.poll(); // 移除最旧的订单 } } // 其他方法... } 同时，实现一个定时任务将处理过的订单数据持久化到数据库，并从内存中清除。\n8. 验证改进 重新部署优化后的应用 监控GC活动和内存使用 进行负载测试 结果：\nFull GC频率降低到每小时1-2次 每次Full GC耗时减少到1秒以内 Old Gen使用率稳定在60%左右 总结 通过这个详细的排查过程，我们：\n使用GC日志和监控工具识别了问题 通过堆内存分析找到了内存泄漏的根源 优化了代码中的内存使用模式 调整了JVM参数以更好地适应应用特性 验证了优化效果 这个例子展示了如何系统地排查和解决频繁Full GC问题，涵盖了从问题发现、原因分析到解决方案实施的完整过程。在实际工作中，具体的问题可能更加复杂，但这个方法论可以作为一个基础框架来处理各种GC相关的性能问题。\n","date":"2025-11-09T18:51:55+08:00","permalink":"https://readnovel.org/posts/%E9%A2%91%E7%B9%81fullgc%E9%97%AE%E9%A2%98%E5%A6%82%E4%BD%95%E6%8E%92%E6%9F%A5/","title":"频繁FullGC问题如何排查"},{"content":"Explain使用与详解 在 select 语句之前增加 explain 关键字，MySQL 会在查询上设置一个标记，执行查询会返回执行计划的信息，而不是执行这条SQL。\n参考官方文档：https://dev.mysql.com/doc/refman/5.7/en/explain-output.html\n以下数据表作为演示使用\n-- 示例表 DROP TABLE IF EXISTS `actor`; CREATE TABLE `actor` ( `id` int(11) NOT NULL, `name` varchar(45) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1,\u0026#39;a\u0026#39;,\u0026#39;2017-12-22 15:17:18\u0026#39;), (2,\u0026#39;b\u0026#39;,\u0026#39;2017-12-22 15:27:18\u0026#39;), (3,\u0026#39;c\u0026#39;,\u0026#39;2017-12-22 15:27:18\u0026#39;); DROP TABLE IF EXISTS `film`; CREATE TABLE `film` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `film` (`id`, `name`) VALUES (3,\u0026#39;film0\u0026#39;),(1,\u0026#39;film1\u0026#39;),(2,\u0026#39;film2\u0026#39;); DROP TABLE IF EXISTS `film_actor`; CREATE TABLE `film_actor` ( `id` int(11) NOT NULL, `film_id` int(11) NOT NULL, `actor_id` int(11) NOT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_film_actor_id` (`film_id`,`actor_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`) VALUES (1,1,1),(2,1,2),(3,2,1); Explain的两种变体 explain extended 在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 show warnings 命令可以得到优化后的查询语句，从而看出优化器优化了什么。\n额外还有 filtered 列，是一个百分比的值，rows*filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数（前一个表指 explain 中的id值比当前表id值小的表）。\nmysql\u0026gt; explain extended select * from film where id = 1; #显示mysql优化后的伪sql(不一定可执行) mysql\u0026gt; show warnings; explain partitions 相比 explain 多了个 partitions 字段，如果查询是基于分区表的话，会显示查询将访问的分区。\n目前在mysql8中，explain extended与explain partitions两种变体都已被移除，原本的变种功能大多移植到了普通的explain中。\n下文sql示例基于mysql5.7，执行时若环境为mysql8，将变体替换为explain即可\nExplain中的一些重要的列 id列: select 的序列号\n有几个 select 就有几个id，并且id的顺序是按 select 出现的顺序增长的。 id列越大执行优先级越高，id相同则从上往下执行，id为NULL最后执行。 select_type列: 表示对应行是简单还是复杂的查询\nsimple：简单查询。查询不包含子查询和union primary：复杂查询中最外层的 select subquery：包含在 select 中的子查询（不在 from 子句中） derived：包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中，也称为派生表（derived的英文含义） #关闭mysql5.7新特性对衍生表的合并优化 mysql\u0026gt; set session optimizer_switch=\u0026#39;derived_merge=off\u0026#39;; mysql\u0026gt; explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der;\t通过set session optimizer_switch='derived_merge=on'可还原默认配置 union：在 union 语句中的第二个和随后的 select explain select id from actor union select actor_id from film_actor; table列: 表示 explain 的一行正在访问的表\n当 from 子句中有子查询时，table列是 \u0026lt;derivedN\u0026gt; 格式，表示当前查询依赖 id=N 的查询，于是先执行 id=N 的查询。 当有 union 时，UNION RESULT 的 table 列的值为\u0026lt;union1,2\u0026gt;，1和2表示参与 union 的 select 行id type列: 表示关联类型或访问类型，即如何查找行\n次从最优到最差依次分别为：system \u0026gt; const \u0026gt; eq_ref \u0026gt; ref \u0026gt; range \u0026gt; index \u0026gt; ALL 一般来说，得保证查询达到range级别，最好达到ref\nNULL：mysql能够在优化阶段分解查询语句，在执行阶段用不着再访问表或索引。例如：在索引列中选取最小值，可以单独查找索引来完成，不需要在执行时访问表 explain select min(id) from film; const, system：mysql能对查询的某部分进行优化并将其转化成一个常量（可以看show warnings 的结果）。用于primary key 或 unique key 的所有列与常数比较时，所以表最多有一个匹配行，读取1次，速度比较快。system是const的特例，表里只有一条数据且匹配时为system explain extended select * from (select * from film where id = 1) tmp; eq_ref：primary key 或 unique key 索引的所有部分被连接使用（唯一值关联） ，最多只会返回一条符合条件的记录。这可能是在const 之外最好的联接类型了，简单的 select 查询不会出现这种 type (TODO) explain select * from film_actor left join film on film_actor.film_id = film.id; ref：相比 eq_ref，不使用主键索引或唯一索引，而是使用普通索引或者唯一性索引的部分前缀，索引要和某个值相比较，可能会找到多个符合条件的行。 range：范围扫描, 通常出现在 in(), between ,\u0026gt; ,\u0026lt;, \u0026gt;= 等操作中。使用一个索引来检索给定范围的行.explain select * from actor where id \u0026gt; 1; index：扫描全索引就能拿到结果，一般是扫描某个二级索引，这种扫描不会从索引树根节点开始快速查找，而是直接对二级索引的叶子节点遍历和扫描，速度还是比较慢的，这种查询一般为使用覆盖索引，二级索引一般比较小，所以这种通常比ALL快一些。 explain select * from film; ALL：即全表扫描，扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。explain select * from actor; possible_keys列: 查询可能使用哪些索引来查找 explain 时可能出现 possible_keys 有列，而 key 显示 NULL 的情况，这种情况是因为表中数据不多，mysql认为索引对此查询帮助不大，选择了全表查询。 如果该列是NULL，则没有相关的索引。在这种情况下，可以通过检查 where 子句看是否可以创造一个适当的索引来提高查询性能，然后用 explain 查看效果。 6. key列: 表示mysql实际采用的索引。 如果没有使用索引，则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引，在查询中使用 force index、ignore index。 \u0026gt; 经过mysql优化器内部评估后认为走索引并不比全表扫描更快，此时他不会走索引。一般这种情况你force index也不会有什么性能提升 7. key_len列： 显示mysql在索引里使用的字节数 通过这个值可以算出具体使用了索引中的哪些列。 举例来说，film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成，并且每个int是4字节。通过结果中的key_len=4可推断出查询使用了第一个列：film_id列来执行索引查找（最左前缀原则）。\texplain select * from film_actor where film_id = 2; key_len计算规则：\n- 字符串，char(n)和varchar(n)，n代表字符数，而不是字节数 - char(n)：如果存汉字长度就是 3n 字节 - varchar(n)：如果存汉字则长度是 3n + 2 字节，加的2字节用来存储字符串长度，因为varchar是变长字符串 - 数值类型 - tinyint：1字节 - smallint：2字节 - int：4字节 - bigint：8字节 - 时间类型 - date：3字节timestamp：4字节 - datetime：8字节 - 如果字段允许为 NULL，需要1字节记录是否为 NULL 索引最大长度是768字节，当字符串过长时，mysql会做一个类似左前缀索引的处理，将前半部分的字符提取出来做索引。 ref列: 这一列显示了在key列记录的索引中，表查找值所用到的列或常量 常见的有：const（常量，如where id=1），字段名（关联表的关联字段，例：film.id）\nrows列 这一列是mysql估计要读取并检测的行数，注意这个不是结果集里的行数。 这个值不确定，不能完全将其作为参考依据！！\nExtra列: 这一列展示了一些额外信息\nUsing index： 使用覆盖索引 表示explain结果里的key列有使用索引，如果select后面查询的字段都可以从这个索引的树中获取，这种情况一般可以说是用到了覆盖索引，这时extra里一般都有using index；覆盖索引一般针对的是辅助索引，整个查询结果只通过辅助索引就能拿到结果，不需要通过辅助索引树找到主键，再通过主键去主键索引树里获取其它字段值 （无需回表）\nUsing where： 使用 where 语句来处理结果，并且查询的列未被索引覆盖 explain select * from film_actor where film_id \u0026gt; 1;\nUsing index condition: 查询的列不完全被索引覆盖，where条件中是一个前导列的范围；explain select * from film_actor where film_id \u0026gt; 1;\nUsing temporary: mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的，首先是想到用索引来优化。\nactor.name没有索引，此时创建了张临时表来distinct explain select distinct name from actor; film.name建立了idx_name索引，此时查询时extra是using index,没有用临时表 #在遍历二级索引树时就做了去重 explain select distinct name from film; Using filesort: 将用外部排序而不是索引排序，数据较小时从内存排序，否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。\nactor.name未创建索引，会浏览actor整个表，保存排序关键字name和对应的id，然后排序name并检索行记录explain select * from actor order by name; film.name 建立了idx_name索引，此时查询时extra是using index explain select * from film order by name; Select tables optimized away：使用某些聚合函数（比如 max、min）来访问存在索引的某个字段\texplain select min(id) from film;\n索引最佳实践 CREATE TABLE `employees` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;姓名\u0026#39;, `age` int(11) NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;年龄\u0026#39;, `position` varchar(20) NOT NULL DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;职位\u0026#39;, `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;入职时间\u0026#39;, PRIMARY KEY (`id`), KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=\u0026#39;员工记录表\u0026#39;; INSERT INTO employees(name,age,position,hire_time) VALUES(\u0026#39;LiLei\u0026#39;,22,\u0026#39;manager\u0026#39;,NOW()); INSERT INTO employees(name,age,position,hire_time) VALUES(\u0026#39;HanMeimei\u0026#39;,21,\u0026#39;dev\u0026#39;,NOW()); INSERT INTO employees(name,age,position,hire_time) VALUES(\u0026#39;Lucy\u0026#39;,23,\u0026#39;dev\u0026#39;,NOW()); 1.全值匹配 最左前缀法则: 如果索引了多列，要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。\nEXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39;;\tEXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age = 22; EXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age = 22 AND position =\u0026#39;manager\u0026#39;; 2.操作索引列导致失效 在索引列上做任何操作（计算、函数、（自动or手动）类型转换），会导致索引失效而转向全表扫描\nEXPLAIN SELECT * FROM employees WHERE name = \u0026#39;LiLei\u0026#39;; EXPLAIN SELECT * FROM employees WHERE left(name,3) = \u0026#39;LiLei\u0026#39;; 3.存储引擎不能使用索引中范围条件右边的列 #正常结果 EXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age = 22 AND position =\u0026#39;manager\u0026#39;; #position不走索引，前面的数据对其来说无序，故position索引失效,牢记索引树特征！！！ EXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age \u0026gt; 22 AND position =\u0026#39;manager\u0026#39;; 4.尽量使用覆盖索引 （只访问索引的查询（索引列包含查询列）），减少 select * 语句\n#走索引覆盖索引，无需回表 EXPLAIN SELECT name,age FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age = 23 AND position=\u0026#39;manager\u0026#39;; EXPLAIN SELECT * FROM employees WHERE name= \u0026#39;LiLei\u0026#39; AND age = 23 AND position =\u0026#39;manager\u0026#39;; 5. 会导致索引失效的一些条件语句 mysql在使用不等于（！=或者\u0026lt;\u0026gt;），not in ，not exists 的时候无法使用索引会导致全表扫描\u0026lt; 小于、 \u0026gt; 大于、 \u0026lt;=、\u0026gt;= 这些，mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引； is null,is not null 一般情况下也无法使用索引 like以通配符开头（\u0026rsquo;$abc\u0026hellip;\u0026rsquo;）mysql索引失效会变成全表扫描操作 EXPLAIN SELECT * FROM employees WHERE name like \u0026#39;%Lei\u0026#39; # 走idx_name_age_position的部分前缀索引 EXPLAIN SELECT * FROM employees WHERE name like \u0026#39;Lei%\u0026#39; 如何解决呢？使用覆盖所有，查询字段中必须都是建立索引字段。 否则可能需要尝试搜索引擎 EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%';\n少用or或in，用它查询时，mysql不一定使用索引，mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引，详见范围查询优化 EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei'; 6.字符串不加单引号索引失效 EXPLAIN SELECT * FROM employees WHERE name = \u0026#39;1000\u0026#39;; EXPLAIN SELECT * FROM employees WHERE name = 1000; 这个应该算是小问题，不会还有人字符串条件不加引号吧？？？\n7.范围查询优化 首先这里我们通过存储过程插入足量的测试数据：\n# 通过存储过程插入10万条数据 drop procedure if exists insert_emp; delimiter ;; create procedure insert_emp() begin declare i int; set i = 1; while(i \u0026lt;= 100000) do insert into employees(name, age, position) values (CONCAT(\u0026#39;zhuge\u0026#39;, i), i, \u0026#39;dev\u0026#39;); set i = i + 1; end while; end;; delimiter ; call insert_emp(); #给年龄添加单值索引 ALTER TABLE `employees` ADD INDEX `idx_age` (`age`) USING BTREE ; explain select * from employees where age \u0026gt;=1 and age \u0026lt;=20000; 没走索引原因：mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。比如这个例子，可能是由于单次数据量查询过大导致优化器最终选择不走索引。\n优化方法：可以将大的范围拆分成多个小范围\nexplain select * from employees where age \u0026gt;=1 and age \u0026lt;=10000; explain select * from employees where age \u0026gt;=10001 and age \u0026lt;=20000; 我猜测根据电脑本身性能不同，同样的数据，mysql优化器决定是否走索引的临界值也是不同的。比如我的这台笔电测试age\u0026lt;12027时走索引，而其他卡顿的电脑这个值会更小。 总之呢，需要根据具体情况做测试之后再做拆分。\n其他 like KK%相当于=常量，%KK和%KK% 相当于范围\nmysql5.7关闭ONLY_FULL_GROUP_BY报错 select version(), @@sql_mode;SET sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));\n","date":"2025-08-31T21:31:52+08:00","permalink":"https://readnovel.org/posts/explain%E4%BD%BF%E7%94%A8%E4%B8%8E%E8%AF%A6%E8%A7%A3/","title":"Explain使用与详解"},{"content":"mysql索引 底层数据结构与算法 Mysql索引的底层数据结构 首先想清楚，什么是索引？它是一种查询高效、排好序的数据结构！\n常见的索引数据结构有：二叉树、红黑树、Hash表、B-Tree，mysql 索引的默认数据结构式是B+Tree，这是B-Tree的一个变种。\n更深入地了解，我们需要区分几种数据结构的不同与优劣，这样才能明白为什么要选择B+Tree作为默认的索引结构。\n区分不同索引结构 二叉树 二叉树最坏的情况，所有的节点都在左侧，或者都在右侧。这样的单边增长，会让树的高度非常大，检索效率极低\n红黑树 红黑树能够一定程度上自平衡，无法控制其深度。大数据量下可能左右树倾斜严重，mysql查询的数据如果恰好是子节点，则查询速度会十分地缓慢\n红黑树推荐文章：# 最易懂的红黑树讲解\nB-Tree 观察上图可以发现B-Tree的一些特点：叶子节点具有相同的高度、节点中索引数据从左到右递增\nB+Tree 对比B-Tree可以发现：B+Tree非叶子节点不存储数据，在叶子节点中包含了所有的索引数据。这样做的目的是，在固定大小的节点空间内能够存储更多的索引（对应存储更多的叶子节点数据）\n另外，叶子节点之间前后以指针链接，这样访问后续叶子节点中的数据更加高效\nHash表 hash表的查询效率比B+Tree更高效，只需要一次hash计算就能定位到数据；\n但是hash只能支持等值查询，如“=”、“in”，无法进行范围查询，此外存在hash冲突问题\n区分Innodb与myisam存储引擎 MyISAM索引文件和数据文件是分离的(非聚集)，而Inonodb索引和数据在同一个文件中。\nInnodb存储引擎中的一些特点：\n表数据存储结构完全符合B+Tree的特点，数据只存储在聚集索引的叶子节点中。\n区分是否聚集索引，就看索引和数据是否分离，辅助索引查询后根据主句ID，回表查询聚集索引。\n为什么建表时必须建主键？\n若是建表时没有建立主键，1.mysql会选择一列没有重复数据的作为主键，2.若不存在这样的列，则生成一个类似行号的作为主键。\n这样无规则的字段作为主键是非常影响效率的。\n推荐以整型自增主键，好处是方便比较大小，查询效率更高。\n联合索引和最左前缀原则 创建联合索引的语法：key indexName (field1,field2,field3....) [using btree]\n该索引树将依次按照字段1、字段2、字段3来排序。在索引树的叶子节点中包含了联合索引的所有字段，以及主键ID\n最左前缀： 联合索引只能从左往右的顺序依次索引，跳过则后续索引失效。\n索引覆盖： 若查询所需字段都包含在联合索引中，则辅助索引无需回表\n","date":"2025-08-30T23:57:51+08:00","permalink":"https://readnovel.org/posts/mysql%E7%B4%A2%E5%BC%95-%E5%BA%95%E5%B1%82%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/","title":"Mysql索引 底层数据结构与算法"},{"content":"全面理解Mysql架构 mysql官方文档：https://dev.mysql.com/doc/refman/8.0/en/installing.html\n安装Mysql Centos7中安装Mysql\n通过Docker安装Mysql\n在Docker中运行的Mysql与实体机中相比，虽说性能上略有不如，不过却可以很方便地帮我们拉起一个集群。\nmkdir -p /home/mysql8/data /home/mysql8/config /home/mysql8/logs docker run -d \\ --name mysql8 \\ --privileged=true \\ --restart=always \\ -p 3310:3306 \\ -v /home/mysql8/data:/var/lib/mysql \\ -v /home/mysql8/config:/etc/mysql/conf.d \\ -v /home/mysql8/logs:/logs \\ -e MYSQL_ROOT_PASSWORD=123456 \\ -e TZ=Asia/Shanghai mysql:8.0.27 \\ --lower_case_table_names=1 全面理解Mysql架构 安装Mysql Centos7中安装Mysql\n通过Docker安装Mysql\n在Docker中运行的Mysql与实体机中相比，虽说性能上略有不如，不过却可以很方便地帮我们拉起一个集群。\nmkdir -p /home/mysql8/data /home/mysql8/config /home/mysql8/logs docker run -d \\ --name mysql8 \\ --privileged=true \\ --restart=always \\ -p 3310:3306 \\ -v /home/mysql8/data:/var/lib/mysql \\ -v /home/mysql8/config:/etc/mysql/conf.d \\ -v /home/mysql8/logs:/logs \\ -e MYSQL_ROOT_PASSWORD=123456 \\ -e TZ=Asia/Shanghai mysql:8.0.27 \\ --lower_case_table_names=1 了解mysql的架构 mysql的查询与更新流程 Mysql主要由连接器、分析器、优化器、执行器，以及存储引擎组成\n当客户端一个连接请求过来时，首先由连接器校验该用户是否有建立连接的权限（如mysql -uroot -p） 分析器：分析编写的sql是否符合规则（验证表执行权限） 词法分析：information_schema表中记录了表字段信息，检测出操作的字段是否存在（1054 - Unknown column 'NAME' in 'field list'） 语法分析：sql语法不对，会提示错误大概的位置near by（You have an error in your SQL syntax） 优化器：在一个查询语句的查询方案中选出最优解，如表连接中小表驱动大表 执行器：最终去执行sql语句。需要验证执行权限 为什么表的执行权限在分析器中已经验证过了，到了执行器中又重复验证一边呢？ 因为由触发器、存储过程等附带的表，只有到执行的时候才知道有没有权限\n5.查询缓存：之前查询过一次的sql会放在查询缓存中，命中率太低，且影响并发，所以在mysql8中已经废除了\n更新语句的流程：update T set c=c+1 where ID=2;\n与上面的查询流程大差不差，主要区别就在：通过分析器确定是更新语句、通过优化器确定通过ID来更新记录、最终由执行器更新数据\nRedolog与Binlog的二阶段提交（内部XA） redolog与binlog用来保证mysql崩溃后的数据恢复，其中redolog是Innodb特有的，binlog是数据mysql的server层。\n执行一条更新语句：\n首先拿到记录并更新， 写入内存 写入到redolog（此时redolog的状态是prepare，还没有正式写入redolog日志文件） 写入binlog 提交事务，此时redolog的状态为commit 若是binlog更新完，还没来得及commit，数据库突然崩溃，重启mysql之后能够恢复嘛？\n可以的，重启之后发现redolog是prepare状态，则回去检查binlog的数据是否完整。若binlog数据完整则直接提交，否则回滚数据\n","date":"2025-08-29T17:29:59+08:00","permalink":"https://readnovel.org/posts/%E5%85%A8%E9%9D%A2%E7%90%86%E8%A7%A3mysql%E6%9E%B6%E6%9E%84/","title":"全面理解Mysql架构"},{"content":"你是否曾经被各种复杂的网站构建工具搞得头晕目眩？是否厌倦了那些需要大量配置的博客系统？那么 Hugo 可能就是你一直在寻找的答案。作为一个快速、灵活且强大的静态网站生成器，Hugo 让创建网站变得简单而有趣。\n在这篇文章中，我们将一起探索 Hugo 的常用操作，让你能够轻松上手并快速构建自己的网站。\nHugo 是什么？ Hugo 是一个用 Go 语言编写的静态网站生成器。它以速度著称，能够在几秒钟内生成包含数千个页面的网站。与需要数据库和服务器端处理的动态网站不同，Hugo 生成的是纯静态文件，这使得网站加载速度极快，同时部署也变得异常简单。\n安装 Hugo 在开始之前，我们需要先安装 Hugo。根据你的操作系统，可以选择以下方式：\nmacOS: 使用 Homebrew 安装：brew install hugo Windows: 使用 Chocolatey 安装：choco install hugo Linux: 使用包管理器或从官网下载二进制文件 安装完成后，可以通过 hugo version 命令验证安装是否成功。\n创建新站点 安装好 Hugo 后，我们就可以创建自己的网站了。在终端中运行以下命令：\nhugo new site my-blog 这会创建一个名为 my-blog 的新目录，其中包含了 Hugo 站点的基本结构。\n选择和安装主题 Hugo 的魅力之一在于它丰富的主题生态系统。你可以从 Hugo 官方主题网站 找到各种风格的主题。\n安装主题通常有两种方式：\n作为 git submodule 安装：\ngit init git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke 直接下载主题文件到 themes 目录。\n安装完成后，在 config.toml 文件中添加主题配置：\ntheme = \u0026#34;ananke\u0026#34; 内容目录结构 Hugo 使用清晰的内容目录结构来组织网站内容。默认情况下，Hugo 会创建以下目录结构：\ncontent/ ├── posts/ # 存放博客文章 ├── about/ # 存放关于页面 └── _index.md # 网站首页内容 创建不同类型的页面 你可以在 content 目录下创建不同的子目录来组织不同类型的内容：\n文章目录：通常使用 posts 目录存放博客文章 页面目录：可以创建 about、contact 等目录存放独立页面 分类目录：根据需要创建不同的分类目录 要创建一个新页面，可以使用以下命令：\n# 创建博客文章 hugo new posts/my-first-post.md # 创建关于页面 hugo new about/index.md # 创建特定分类下的文章 hugo new tutorials/how-to-use-hugo.md 注意，Hugo 会根据你指定的路径自动选择合适的模板。如果你不指定目录，Hugo 会默认在 content 根目录下创建文件。\n创建内容 Hugo 使用 Markdown 格式来编写内容，这使得写作变得简单而直观。\n创建文章 要创建一篇新文章，可以使用以下命令：\nhugo new posts/my-first-post.md 这会在 content/posts/ 目录下创建一个新的 Markdown 文件。文件的开头是 Front Matter（前置元数据），包含了文章的标题、日期、草稿状态等信息。\nFront Matter 详解 Front Matter 是 Hugo 中非常重要的概念，它位于文件的顶部，用于定义页面的元数据。常见的配置包括：\n--- title: \u0026#34;文章标题\u0026#34; date: 2025-08-22T14:44:45+08:00 draft: false tags: [\u0026#34;标签1\u0026#34;, \u0026#34;标签2\u0026#34;] categories: [\u0026#34;分类1\u0026#34;] --- 使用草稿 Hugo 支持草稿功能，可以在 Front Matter 中设置 draft: true。在开发过程中，你可以使用 hugo server -D 命令来预览包括草稿在内的所有内容。\n配置菜单 一个网站怎么能没有导航菜单呢？Hugo 提供了灵活的菜单配置方式。\n在 config.toml 中配置菜单 你可以在 config.toml 文件中直接定义菜单项：\n[[menu.main]] name = \u0026#34;首页\u0026#34; url = \u0026#34;/\u0026#34; weight = 1 [[menu.main]] name = \u0026#34;关于\u0026#34; url = \u0026#34;/about/\u0026#34; weight = 2 [[menu.main]] name = \u0026#34;文章\u0026#34; url = \u0026#34;/posts/\u0026#34; weight = 3 其中 weight 参数用于控制菜单项的显示顺序，数值越小越靠前。\n在 Front Matter 中配置菜单 你也可以在单个页面的 Front Matter 中添加菜单配置：\n--- title: \u0026#34;关于我\u0026#34; menu: \u0026#34;main\u0026#34; --- 将文章归类到指定菜单 要将特定文章或页面归类到指定菜单下，有两种方法：\n在 Front Matter 中指定菜单：\n--- title: \u0026#34;我的文章\u0026#34; menu: main: weight: 10 --- 使用 pageRef 关联菜单项： 在 config.toml 中配置菜单时，使用 pageRef 参数指向具体的页面路径：\n[[menu.main]] name = \u0026#34;我的文章\u0026#34; pageRef = \u0026#34;/posts/my-article\u0026#34; weight = 10 创建多级菜单 Hugo 还支持创建多级嵌套菜单：\n[[menu.main]] name = \u0026#34;文档\u0026#34; weight = 10 [[menu.main]] name = \u0026#34;入门指南\u0026#34; parent = \u0026#34;文档\u0026#34; pageRef = \u0026#34;/docs/getting-started\u0026#34; weight = 1 [[menu.main]] name = \u0026#34;配置说明\u0026#34; parent = \u0026#34;文档\u0026#34; pageRef = \u0026#34;/docs/configuration\u0026#34; weight = 2 启动本地服务器 在开发过程中，Hugo 内置的服务器可以实时预览你的网站：\nhugo server 这个命令会启动一个本地服务器，默认地址是 http://localhost:1313。Hugo 会自动监视文件变化，并在你修改内容时实时刷新浏览器。\n如果你想要预览草稿文章，可以使用：\nhugo server -D 生成静态文件 当你完成网站的开发后，就可以生成静态文件进行部署了：\nhugo 这个命令会在 public 目录下生成所有静态文件，你可以将这些文件部署到任何支持静态文件托管的服务上，如 GitHub Pages、Vercel、Netlify 等。\n常用命令总结 为了方便查阅，这里总结一些常用的 Hugo 命令：\n命令 说明 hugo new site sitename 创建新站点 hugo new content/posts/postname.md 创建新文章 hugo new content/about/index.md 创建新页面 hugo server 启动本地服务器 hugo server -D 启动本地服务器并预览草稿 hugo 生成静态文件 hugo -D 生成静态文件并包含草稿 结语 Hugo 以其简单易用和强大功能，成为了许多博主和技术写作者的首选工具。通过本文的介绍，相信你已经掌握了 Hugo 的基本操作。当然，Hugo 的功能远不止这些，它还支持多语言、自定义模板、短代码等高级功能，等待你去探索。\n现在，就让我们一起用 Hugo 创造属于自己的网站吧！\n","date":"2025-08-22T14:44:45+08:00","permalink":"https://readnovel.org/posts/hugo-commands/","title":"Hugo 常用操作指南：从新手到熟练"},{"content":"Cut out summary from your post content here.\nThe remaining content of your post.\nnihao ","date":"2025-08-22T14:12:52+08:00","permalink":"https://readnovel.org/posts/my-first-post/","title":"My First Post"}]