12-MySQL项目高频问题汇总:重复插入、锁超时、事务失效 MySQL项目高频问题汇总重复插入、锁超时、事务失效作者黒漂技术佬适用读者踩过坑但不知道根因、想一次性把高频问题搞清楚的同学关联场景无人售货柜订单重复、工控设备状态更新异常一、问题1数据重复插入1.1 复现场景用户点击下单按钮因为网络抖动前端连点了两次。后端收到两个相同请求请求1INSERT INTO orders(order_no, user_id, amount) VALUES(OD20240601001, 1001, 3.50) 请求2INSERT INTO orders(order_no, user_id, amount) VALUES(OD20240601001, 1001, 3.50) → 没有唯一约束 → 两条都插入成功 → 一笔订单变两笔1.2 解决方案方案1唯一索引INSERT IGNORE-- 先加唯一索引ALTERTABLEordersADDUNIQUEINDEXuk_order_no(order_no);-- 插入时用INSERT IGNORE重复则跳过INSERTIGNOREINTOorders(order_no,user_id,amount)VALUES(OD20240601001,1001,3.50);-- 重复时影响行数0不报错方案2唯一索引ON DUPLICATE KEY UPDATE推荐INSERTINTOorders(order_no,user_id,amount,update_time)VALUES(OD20240601001,1001,3.50,NOW())ONDUPLICATEKEYUPDATEamountVALUES(amount),update_timeNOW();-- 重复时走UPDATE分支不重复时INSERT方案3唯一索引REPLACE INTOREPLACEINTOorders(order_no,user_id,amount)VALUES(OD20240601001,1001,3.50);-- 重复时先DELETE旧行再INSERT新行-- 注意REPLACE会触发DELETEINSERT触发器主键ID会变方案行为适用INSERT IGNORE重复跳过不动旧数据幂等插入旧数据优先ON DUPLICATE KEY UPDATE重复则更新插入或更新upsertREPLACE INTO重复则删旧插新完全覆盖旧数据工程建议用ON DUPLICATE KEY UPDATE最多。它等价于有就改、没有就插符合大多数幂等场景。配合业务唯一键订单号、流水号用。二、问题2锁超时 Lock Wait Timeout Exceeded2.1 报错ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction2.2 复现事务A 事务B BEGIN; UPDATE product SET stockstock-1 WHERE id1001; -- 拿到id1001的X锁不提交 BEGIN; UPDATE product SET stockstock-1 WHERE id1001; -- 等锁 等50秒... ERROR 1205: Lock wait timeout exceeded事务A拿锁不释放忘了COMMIT或程序异常事务B等锁超时。2.3 排查步骤-- 1. 看当前锁等待SELECT*FROMinformation_schema.innodb_lock_waits;-- 2. 看持有锁的事务SELECT*FROMinformation_schema.innodb_trxWHEREtrx_stateLOCK WAIT;-- 3. 看完整状态SHOWENGINEINNODBSTATUS\G-- 关注 TRANSACTIONS 段看哪个事务一直活跃-- 4. 如果确定是僵尸事务手动杀-- 先查thread_idSELECTtrx_mysql_thread_idFROMinformation_schema.innodb_trxWHEREtrx_id僵尸事务ID;-- 杀掉KILLthread_id;2.4 根因和预防锁超时的根因 1. 事务没提交代码BUG、连接泄漏 2. 事务执行太慢大事务、慢SQL 3. 死锁检测漏了极少见 预防 1. 事务尽量短小快速提交 2. 设置合理的innodb_lock_wait_timeout默认50秒 3. 应用层加超时控制避免无限等待 4. 监控长事务告警售货柜场景典型用户开门取货后结算事务跑了几秒重量算法慢期间其他用户想改同一商品库存会等锁。优化算法、缩短事务时长是关键。三、问题3事务失效的7种场景这是SpringMySQL项目里最常见的坑。明明加了Transactional结果操作不回滚。3.1 场景1方法非publicServicepublicclassOrderService{TransactionalvoidplaceOrder(OrderRequestreq){// 包级私有非publicproductMapper.deductStock(...);orderMapper.insert(...);// 抛异常 → 不回滚}}原因Spring事务基于AOP代理默认只拦截public方法JDK动态代理限制。非public方法代理不生效。解决把方法改成public。3.2 场景2自调用this调用ServicepublicclassOrderService{publicvoidcheckout(OrderRequestreq){// this调用不走代理this.placeOrder(req);}TransactionalpublicvoidplaceOrder(OrderRequestreq){productMapper.deductStock(...);orderMapper.insert(...);// 抛异常 → 不回滚}}原因this.placeOrder()是直接调用当前对象的方法没经过Spring代理。事务是代理对象才能触发的绕过了代理就绕过了事务。解决把被调方法抽到另一个Service或注入自身代理ServicepublicclassOrderService{AutowiredprivateOrderServiceself;// 注入自身代理Spring 4.3publicvoidcheckout(OrderRequestreq){self.placeOrder(req);// 通过代理调用}TransactionalpublicvoidplaceOrder(OrderRequestreq){...}}3.3 场景3异常被catch吞掉TransactionalpublicvoidplaceOrder(OrderRequestreq){try{productMapper.deductStock(...);orderMapper.insert(...);}catch(Exceptione){log.error(下单失败,e);// 异常被吞了Spring不知道出错了 → 不回滚}}原因Spring事务通过捕获方法抛出的异常来决定是否回滚。异常被catch了方法正常返回Spring以为成功了。解决catch后重新抛出或手动标记回滚TransactionalpublicvoidplaceOrder(OrderRequestreq){try{productMapper.deductStock(...);orderMapper.insert(...);}catch(Exceptione){log.error(下单失败,e);// 手动标记回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 或重新抛出thrownewRuntimeException(e);}}3.4 场景4抛出非RuntimeExceptionTransactionalpublicvoidplaceOrder(OrderRequestreq)throwsBusinessException{productMapper.deductStock(...);if(req.getAmount().compareTo(BigDecimal.ZERO)0){thrownewBusinessException(金额不能为负);// 受检异常}orderMapper.insert(...);// 抛出BusinessException → 不回滚}原因Spring默认只对RuntimeException和Error回滚受检异常如IOException、BusinessException不回滚。解决指定rollbackForTransactional(rollbackForException.class)// 所有异常都回滚publicvoidplaceOrder(OrderRequestreq)throwsBusinessException{...}强烈建议所有Transactional都加rollbackFor Exception.class。默认规则坑太多统一规则省心。3.5 场景5传播行为配置错误Transactional// 默认REQUIREDpublicvoidouterMethod(){productMapper.update(...);innerMethod();// 内部方法}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidinnerMethod(){orderMapper.insert(...);thrownewRuntimeException(失败);}坑innerMethod配了REQUIRES_NEW意思是开新事务。如果异常被外层catch了内层事务已经独立提交外层回滚也拉不回内层。传播行为速查传播行为行为REQUIRED默认有就加入没有就新建REQUIRES_NEW总是新建挂起当前事务NESTED嵌套事务可单独回滚SUPPORTS有就加入没有就非事务NOT_SUPPORTED非事务执行NEVER有事务则报错MANDATORY必须有事务没有报错大多数业务用默认REQUIRED就够。需要内层失败不影响外层才用REQUIRES_NEW且必须注意异常处理。3.6 场景6多线程调用TransactionalpublicvoidbatchSettle(ListOrderRequestrequests){requests.parallelStream().forEach(req-{productMapper.deductStock(req.getProductId());// 多线程});// 抛异常 → 不回滚}原因Spring事务基于ThreadLocal管理连接。多线程下每个线程拿不同连接不在同一个事务里。解决不要在事务方法里用多线程改数据或每个线程内自己管理事务。3.7 场景7数据库不支持事务-- 建表用了MyISAM引擎CREATETABLEorders(...)ENGINEMyISAM;-- 不支持事务原因MyISAM引擎不支持事务不管怎么加Transactional都没用。只有InnoDB支持事务。解决表引擎改成InnoDBALTERTABLEordersENGINEInnoDB;MySQL 5.5默认引擎就是InnoDB但老库或手动建表可能用MyISAM。看到事务不回滚先SHOW CREATE TABLE看引擎。四、问题4中文乱码4.1 复现售货柜商品表插入中文 INSERT INTO product(name) VALUES(可口可乐500ml); 查出来 SELECT name FROM product; → å¯å£å¯ä¹500ml 乱码4.2 根因字符集不匹配。常见组合乱码场景 - 数据库字符集latin1存UTF-8数据 → 乱码 - 表字符集gbk应用连接用utf8mb4 → 乱码 - JDBC URL没指定字符集 → 用默认可能乱码4.3 解决方案统一所有层级用utf8mb4# MySQL配置文件my.cnf [mysqld] character_set_server utf8mb4 collation_server utf8mb4_unicode_ci [client] default-character-set utf8mb4 [mysql] default-character-set utf8mb4-- 建表时指定CREATETABLEproduct(...)DEFAULTCHARSETutf8mb4COLLATEutf8mb4_unicode_ci;-- 已有表修改ALTERTABLEproductCONVERTTOCHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ci;# JDBC连接URL指定字符集 jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8mb4为什么是utf8mb4而不是utf8MySQL的utf8最多存3字节存不了4字节的emoji表情。utf8mb4才是完整UTF-8。现在标准推荐都用utf8mb4。五、问题5时区问题5.1 复现应用服务器时区Asia/Shanghai (UTC8) MySQL服务器时区UTC 用户下单时间2024-06-01 10:00:00北京时间 存到MySQL2024-06-01 02:00:00UTC少了8小时 查出来给用户2024-06-01 02:00:00 → 用户懵了我10点下的单5.2 解决方案方案1JDBC URL指定时区jdbc:mysql://localhost:3306/shop?serverTimezoneAsia/Shanghai方案2MySQL全局时区设为东八区[mysqld] default_time_zone 8:00-- 或动态设置SETGLOBALtime_zone8:00;SETSESSIONtime_zone8:00;-- 查看当前时区SELECTglobal.time_zone,session.time_zone;SELECTNOW();方案3应用层统一用UTC存储展示时转// 存储统一UTCLocalDateTimenowLocalDateTime.now(ZoneOffset.UTC);// 展示时转东八区LocalDateTimedisplaynow.atZone(ZoneOffset.UTC).withZoneSameInstant(ZoneId.of(Asia/Shanghai)).toLocalDateTime();多时区业务跨境售货柜必须用方案3——存储UTC、展示按用户时区转换。单时区直接方案1最简单。生产配置千万不能漏serverTimezone否则报错java.sql.SQLException: The server time zone value XXX is unrecognized or represents more than one time zone.六、问题6连接数被打满6.1 现象应用报错 Caused by: java.sql.SQLException: Data source rejected establishment of connection, message from server: Too many connections6.2 排查-- 看最大连接数SHOWVARIABLESLIKEmax_connections;-- 看当前连接数SHOWSTATUSLIKEThreads_connected;-- 看各连接在干什么SHOWPROCESSLIST;-- 或SELECT*FROMinformation_schema.processlistWHEREcommand!SleepORDERBYtimeDESC;6.3 解决应急 1. 找出长时间执行的连接 KILL掉 2. 临时调大max_connections SET GLOBAL max_connections 1000; 根治 1. 应用侧限制连接池大小 2. 优化慢SQL让连接快速释放 3. 找有没有连接泄漏开了不关七、总结问题核心方案数据重复插入唯一索引 ON DUPLICATE KEY UPDATE锁超时查innodb_trx找僵尸事务KILL掉缩短事务时长事务失效-非public改成public事务失效-自调用注入自身代理self.xxx()调用事务失效-异常吞掉catch后setRollbackOnly()或重新抛出事务失效-非RuntimeException加rollbackFor Exception.class事务失效-传播行为默认REQUIRED够用REQUIRES_NEW慎用事务失效-多线程不在事务方法内多线程改数据事务失效-引擎改用InnoDB中文乱码全链路utf8mb4MySQL配置建表JDBC URL时区问题JDBC URL加serverTimezoneAsia/Shanghai连接打满KILL长连接调max_connections根治慢SQL和泄漏这些坑都是项目实战里反复出现的。唯一索引防重复、rollbackFor防吞异常、serverTimezone防时区——这三条记牢能避免90%的MySQL生产事故。