Rainman's Blog - 痛点问题
type: channel
link: https://blog.x2boot.com/category/point_issue/
description: JAVA痛点问题
items: 高并发下如何防止重复下单？
items: 高并发下如何保证接口的幂等性？
items: 高并发下如何防止商品超卖？
items: 项目中使用多线程，带来的9大坑
items: 跨域问题如何解决？
items: 调用远程API的15大坑（知识星球专享）
items: 解决分布式事务的8个小技巧
items: 系统上线时SQL脚本的9大坑
items: 糟糕，接口被刷了，怎么办？
items: 糟了，线上服务出现OOM了

高并发下如何防止重复下单？
type: item
date: 2026-06-05T20:37:00+08:00
description: 高并发下如何防止重复下单？前言当你的用户疯狂点击提交按钮时，你的系统准备好迎接这场“连击风暴”了吗？在电商系统的实战中，我见过太多因重复提交导致的资损事故——用户一次点击，系统却创建了多个订单，导致库存错乱、用户重复支付、客服投诉爆棚。有些小伙伴在工作中可能遇到过这样的场景：大促期间，用户反馈“明明只点了一次，为什么扣了两次款？”开发同学查了半天日志，发现同一个用户请求在毫秒级内真的到达了服务器两次。今天这篇文章就跟大家聊聊高并发下防止重复提交订单，希望对你会有所帮助。01 为什么会重复提交？在深入解决方案前，我们必须搞清楚重复提交是如何发生的。常见的场景有：用户无意识重复点击：网络延迟时，用户心急多次点击提交按钮前端防抖失效：前端做了防抖处理，但被绕过或配置不当网络超时重试：请求超时后，客户端或网关自动重试恶意攻击：竞争对手或黑客故意重复提交后端处理超时：第一个请求处理慢，客户端以为失败又发一次来看一个典型的用户操作流程，以及其中可能发生重复的各个环节：从图中可以看到，从用户点击到订单落库，几乎每个环节都可能成为重复提交的“案发现场”。下面，我们就针对这些环节，层层布防。02 第一道防线：前端防抖与按钮控制这是最直观、成本最低的防护措施。原则是：在用户交互层面尽量减少无效请求。2.1 按钮状态控制// 前端防抖实现示例（Vue + Element UI） <el-button :loading="submitting" :disabled="submitting" @click="handleSubmitOrder" > {{ submitting ? '提交中...' : '提交订单' }} </el-button> 2.2 请求防抖与拦截// 使用axios拦截器实现请求防抖 import axios from 'axios' // 存储正在进行的请求 const pendingRequests = new Map() // 生成请求key const generateReqKey = (config) => { const { method, url, params, data } = config return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&') } // 请求拦截器 axios.interceptors.request.use(config => { const key = generateReqKey(config) if (pendingRequests.has(key)) { // 请求已存在，取消当前请求 config.cancelToken = new axios.CancelToken(cancel => { cancel(`重复请求已被拦截: ${key}`) }) } else { // 新请求，添加到pending中 pendingRequests.set(key, config) } return config }) // 响应拦截器 axios.interceptors.response.use( response => { const key = generateReqKey(response.config) pendingRequests.delete(key) return response }, error => { if (axios.isCancel(error)) { console.log('请求被取消:', error.message) return Promise.reject(error) } // 错误处理完成后，也要从pending中移除 if (error.config) { const key = generateReqKey(error.config) pendingRequests.delete(key) } return Promise.reject(error) } )前端防护小结：优点：实现简单，能拦截大部分用户无意识的重复点击缺点：可被绕过（如直接调用API、禁用JS、使用Postman等工具）结论：前端防护是必要但不充分的措施，绝不能作为唯一防线03 第二道防线：后端接口幂等性设计幂等性是解决重复提交的核心理念。所谓幂等，就是同一个操作执行多次的结果与执行一次的结果相同。3.1 什么是幂等性？对于订单提交接口：幂等：无论调用1次还是N次，都只创建一个订单非幂等：调用N次可能创建N个订单3.2 基于Token的幂等实现这是最常用的幂等实现方案，流程如下：客户端在提交前，先向后端申请一个唯一Token提交订单时携带此Token服务端检查Token是否已…

高并发下如何保证接口的幂等性？
type: item
date: 2026-06-05T20:09:03+08:00
description: 接口幂等性问题，对于开发人员来说，是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法，绝大部分内容我在项目中实践过的，给有需要的小伙伴一个参考。不知道你有没有遇到过这些场景：有时我们在填写某些form表单时，保存按钮不小心快速点了两次，表中竟然产生了两条重复的数据，只是id不一样。我们在项目中为了解决接口超时问题，通常会引入了重试机制。第一次请求接口超时了，请求方没能及时获取返回结果（此时有可能已经成功了），为了避免返回错误的结果（这种情况不可能直接返回失败吧？），于是会对该请求重试几次，这样也会产生重复的数据。mq消费者在读取消息时，有时候会读取到重复消息（至于什么原因这里先不说，有兴趣的小伙伴，可以找我私聊），如果处理不好，也会产生重复的数据。没错，这些都是幂等性问题。接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的，不会因为多次点击而产生了副作用。这类问题多发于接口的：insert操作，这种情况下多次请求，可能会产生重复数据。update操作，如果只是单纯的更新数据，比如：update user set status=1 where id=1，是没有问题的。如果还有计算，比如：update user set status=status+1 where id=1，这种情况下多次请求，可能会导致数据错误。那么我们要如何保证接口幂等性？本文将会告诉你答案。1. insert前先select通常情况下，在保存数据的接口中，我们为了防止产生重复数据，一般会在insert前，先根据name或code字段select一下数据。如果该数据已存在，则执行update操作，如果不存在，才执行 insert操作。该方案可能是我们平时在防止产生重复数据时，使用最多的方案。但是该方案不适用于并发场景，在并发场景中，要配合其他方案一起使用，否则同样会产生重复数据。我在这里提一下，是为了避免大家踩坑。2. 加悲观锁在支付场景中，用户A的账号余额有150元，想转出100元，正常情况下用户A的余额只剩50元。一般情况下，sql是这样的：update user amount = amount-100 where id=123;如果出现多次相同的请求，可能会导致用户A的余额变成负数。这种情况，用户A来可能要哭了。于此同时，系统开发人员可能也要哭了，因为这是很严重的系统bug。为了解决这个问题，可以加悲观锁，将用户A的那行数据锁住，在同一时刻只允许一个请求获得锁，更新数据，其他的请求则等待。通常情况下通过如下sql锁住单行数据：select * from user id=123 for update;具体流程如下：具体步骤：多个请求同时根据id查询用户信息。判断余额是否不足100，如果余额不足，则直接返回余额不足。如果余额充足，则通过for update再次查询用户信息，并且尝试获取锁。只有第一个请求能获取到行锁，其余没有获取锁的请求，则等待下一次获取锁的机会。第一个请求获取到锁之后，判断余额是否不足100，如果余额足够，则进行update操作。如果余额不足，说明是重复请求，则直接返回成功。需要特别注意的是：如果使用的是mysql数据库，存储引擎必须用innodb，因为它才支持事务。此外，这里id字段一定要是主键或者唯一索引，不然会锁住整张表。悲观锁需要在同一个事务操作过程中锁住一行数据，如果事务耗时比较长，会造成大量的请求等待，影响接口性能。此外，每次请求接口很难保证都有相同的返回值，所以不适合幂等性设计场景，但是在防重场景中是可以的使用的。在这里顺便说一下，防重设计 和 幂等设计，其实是有区别的。防重设计主要为了避免产生重复数据，对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外，还要求每次请求都返回一样的结果。3. 加乐观锁既然悲观锁有性能问题，为了提升接口性能，我们可以使用乐观锁。需要在表中增加一个timestamp或者version字段，这里以version字段为例。在更新数据之前先查询一下数据：select id,amount,version from user id=123;如果数据存在，假设查到的version等于1，再使用id和version字段作为查询条件更新数据：update user set amount=amount+100,version=version+1where id=123 and version=1;更新数据的同时version+1，然后判断本次update操作的影响行数，如果大于0，则说明本次更新成功，如果等于0，则说明本次更新没有让数据变更。由于第一次请求version等于1是可以成功的，操作成功后version变成2了。这时如果并发的请求过来，再执行相同的sql： up…

高并发下如何防止商品超卖？
type: item
date: 2026-06-05T20:09:00+08:00
description: 前言"快看我们的秒杀系统！库存显示-500了！"3年前的这个电话让我记忆犹新。当时某电商大促，我们自认为完美的分布式架构，在0点整瞬间被击穿。数据库连接池耗尽，库存表出现负数，客服电话被打爆...今天这篇文章跟大家一起聊聊商品超卖的问题，希望对你会有所帮助。1 为什么会发生超卖？首先我们一起看看为什么会发送超卖？1.1 数据库的"最后防线"漏洞我们用下面的列子，给大家介绍一下商品超卖是如何发生的。public boolean buy(int goodsId) { // 1. 查询库存 int stock = getStockFromDatabase(goodsId); if (stock > 0) { // 2. 扣减库存 updateStock(goodsId, stock - 1); return true; } return false; }在并发场景下可能变成下图这样的：请求1和请求2都将库存更新成9。根本原因：数据库的查询和更新操作，不是原子性校验，多个事务可能同时通过stock>0的条件检查。1.2 超卖的本质商品超卖的本质是：多个请求同时穿透缓存，同一时刻读取到相同库存值，最终在数据库层发生覆盖。就像100个人同时看上一件衣服，都去试衣间前看了眼牌子，出来时都觉得自己应该拿到那件衣服。2 防止超卖的方案2.1 数据库乐观锁数据库乐观锁的核心原理是通过版本号控制并发。例如下面这样的：UPDATE product SET stock = stock -1, version=version+1 WHERE id=123 AND version=#{currentVersion};Java的实现代码如下：@Transactional public boolean deductStock(Long productId) { Product product = productDao.selectForUpdate(productId); if (product.getStock() <= 0) return false; int affected = productDao.updateWithVersion( productId, product.getVersion(), product.getStock()-1 ); return affected > 0; }基于数据库乐观锁方案的架构图如下：优缺点分析：无需额外中间件高并发时DB压力大实现简单可能出现大量更新失败适用场景：日订单量1万以下的中小系统。2.2 Redis原子操作Redis原子操作的核心原理是使用：Redis + Lua脚本。核心代码如下：// Lua脚本保证原子性 String lua = "if redis.call('get', KEYS >= ARGV[1] then " + "return redis.call('decrby', KEYS[1], ARGV " + "else return -1 end"; public boolean preDeduct(String itemId, int count) { RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(itemId), count); return result != null && result >= 0; }该方案的架构图如下：性能对比：单节点QPS：数据库方案500 vs Redis方案8万响应时间：<1ms vs 50ms+2.3 分布式锁目前最常用的分布式锁的方案是Redisson。下面是Redisson的实现：RLock lock = redisson.getLock("stock_lock:"+productId); try { if (lock.tryLock(1, 10, TimeUnit.SECONDS)) { // 执行库存操作 } } finally { lock.unlock(); }注意事项1.锁粒度要细化到商品级别2.必须设置等待时间和自动释放3.配合异步队列使用效果更佳该方案的架构图如下：2.4 消息队列削峰可以使用 RocketMQ的事务消息。核心代码如下：// RocketMQ事务消息示例 TransactionMQProducer producer = new TransactionMQProducer("stock_group"); producer.setExecutor(new TransactionListener…

项目中使用多线程，带来的9大坑
type: item
date: 2026-06-05T20:08:51+08:00
description: 前言很多时候，我们为了提升接口的性能，会把之前单线程同步执行的代码，改成多线程异步执行。比如：查询用户信息接口，需要返回用户基本信息、积分信息、成长值信息，而用户、积分和成长值，需要调用不同的接口获取数据。如果查询用户信息接口，同步调用三个接口获取数据，会非常耗时。这就非常有必要把三个接口调用，改成异步调用，最后汇总结果。再比如：注册用户接口，该接口主要包含：写用户表，分配权限，配置用户导航页，发通知消息等功能。该用户注册接口包含的业务逻辑比较多，如果在接口中同步执行这些代码，该接口响应时间会非常慢。这时就需要把业务逻辑梳理一下，划分：核心逻辑和非核心逻辑。这个例子中的核心逻辑是：写用户表和分配权限，非核心逻辑是：配置用户导航页和发通知消息。显然核心逻辑必须在接口中同步执行，而非核心逻辑可以多线程异步执行。等等。需要使用多线程的业务场景太多了，使用多线程异步执行的好处不言而喻。但我要说的是，如果多线程没有使用好，它也会给我们带来很多意想不到的问题，不信往后继续看。今天跟大家一起聊聊，代码改成多线程调用之后，带来的9大问题。1.获取不到返回值如果你通过直接继承Thread类，或者实现Runnable接口的方式去创建线程。那么，恭喜你，你将没法获取该线程方法的返回值。使用线程的场景有两种：不需要关注线程方法的返回值。需要关注线程方法的返回值。大部分业务场景是不需要关注线程方法返回值的，但如果我们有些业务需要关注线程方法的返回值该怎么处理呢？查询用户信息接口，需要返回用户基本信息、积分信息、成长值信息，而用户、积分和成长值，需要调用不同的接口获取数据。如下图所示：在Java8之前可以通过实现Callable接口，获取线程返回结果。Java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例：public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException { final UserInfo userInfo = new UserInfo(); CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { getRemoteUserAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> { getRemoteBonusAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> { getRemoteGrowthAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join(); userFuture.get(); bonusFuture.get(); growthFuture.get(); return userInfo; }温馨提醒一下，这两种方式别忘了使用线程池。示例中我用到了executor，表示自定义的线程池，为了防止高并发场景下，出现线程过多的问题。此外，Fork/join框架也提供了执行任务并返回结果的能力。2.数据丢失我们还是以注册用户接口为例，该接口主要包含：写用户表，分配权限，配置用户导航页，发通知消息等功能。其中：写用户表和分配权限功能，需要在一个事务中同步执行。而剩余的配置用户导航页和发通知消息功能，使用多线程异步执行。表面上看起来没问题。但如果前面的写用户表和分配权限功能成功了，用户注册接口就直接返回成功了。但如果后面异步执行的配置用户导航页，或发通知消息功能失败了，怎么办？如下图所示：该接口前面明明已经提示用户成功了，但结果后面又有一部分功能在多线程异步执行中失败了。这时该如何处理呢？没错，你可以做失败重试。但如果重试了一定的次数，还是没有成功，这条请求数据该如何处理呢？如果不做任何处理，该数据是不是就丢掉了？为了防止数据丢失，可以用如下方案：使用mq异步处理。在分配权限之后，发送一条mq消息，到mq服务器，然后在mq的消费者中使用多线程，去配置用户导航页和发通知消息。如果mq消费者中处理失败了，可以自己重试。使用job异步处…

跨域问题如何解决？
type: item
date: 2026-06-05T20:08:40+08:00
description: 跨域，对后端工程师来说，可谓既熟悉又陌生。这两个月我以架构师的角色参与一款教育产品的孵化，有了一段难忘的跨域之旅。写这篇文章，我想分享我在跨域这个知识点上的经历和思考，希望对大家有所启发。1 遇见跨域产品有多端：机构端，局方端 ，家长端等 。每端都有独立的域名，有的是在PC上访问，有的是通过微信公众号来访问，有的是扫码后H5展现。接入层调用的接口域名统一使用 api.training.com这个独立的域名，通过Nginx来配置请求转发。通常，我们提到的跨域指：CORS。CORS是一个W3C标准，全称是"跨域资源共享"（Cross-origin resource sharing）, 它需要浏览器和服务器同时支持他，允许浏览器向跨源服务器发送XMLHttpRequest请求，从而克服 AJAX 只能同源使用的限制。那么如何定义同源呢？我们先看下一个典型的网站的地址：同源是指：协议、域名、端口号完全相同。下表给出了与 URL http://www.training.com/dir/page.html 的源进行对比的示例:当用户通过浏览器访问应用（http://admin.training.com）时，调用接口的域名非同源域名(http://api.training.com)，这是显而易见的跨域场景。2 CORS详解跨域资源共享标准新增了一组 HTTP 首部字段，允许服务器声明哪些源站通过浏览器有权限访问哪些资源。规范要求，对那些可能对服务器数据产生副作用的 HTTP 请求方法（特别是 GET 以外的 HTTP 请求，或者搭配某些 MIME 类型的 POST 请求），浏览器必须首先使用 OPTIONS 方法发起一个预检请求（preflight request），从而获知服务端是否允许该跨域请求。服务器确认允许之后，才发起实际的 HTTP 请求。在预检请求的返回中，服务器端也可以通知客户端，是否需要携带身份凭证（包括 Cookies 和 HTTP 认证相关数据）。2.1 简单请求当请求同时满足如下条件时，CORS验证机制会使用简单请求， 否则CORS验证机制会使用预检请求。使用GET、POST、HEAD其中一种方法；只使用了如下的安全首部字段，不得人为设置其他首部字段；AcceptAccept-LanguageContent-LanguageContent-Type 仅限三种之一：text/plain，multipart/form-data，application/x-www-form-urlencoded：HTML头部 header field字段：DPR、Download、Save-Data、Viewport-Width、WIdth请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器；XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问；请求中没有使用 ReadableStream 对象。简单请求模式，浏览器直接发送跨域请求，并在请求头中携带Origin的头，表明这是一个跨域的请求。服务器端接到请求后，会根据自己的跨域规则，通过Access-Control-Allow-Origin和Access-Control-Allow-Methods响应头，来返回验证结果。应答中携带了跨域头 Access-Control-Allow-Origin。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中，服务端返回的 Access-Control-Allow-Origin: * 表明，该资源可以被任意外域访问。如果服务端仅允许来自 http://admin.training.com 的访问，该首部字段的内容如下：Access-Control-Allow-Origin: http://admin.training.com现在，除了 http://admin.training.com，其它外域均不能访问该资源。2.2 预检请求浏览器在发现页面发出的请求非简单请求，并不会立即执行对应的请求代码，而是会触发预先请求模式。预先请求模式会先发送preflight request（预先验证请求），preflight request是一个OPTION请求，用于询问要被跨域访问的服务器，是否允许当前域名下的页面发送跨域的请求。在得到服务器的跨域授权后才能发送真正的HTTP请求。OPTIONS请求头部中会包含以下头部：服务器收到OPTIONS请求后，设置头部与浏览器沟通来判断是否允许这个请求。如果preflight request验证通过，浏览器才会发送真正的跨域请求。3 后端配置后端配置我尝试过两种方式，经过两个月的测试…

调用远程API的15大坑（知识星球专享）
type: item
date: 2026-06-05T20:07:09+08:00
description: 前言在实际工作中，我们经常需要在项目中调用第三方API接口，获取数据，或者上报数据，进行数据交换和通信。那么，调用第三方API接口会遇到哪些问题？如何解决这些问题呢？这篇文章就跟大家一起聊聊第三方API接口的话题，希望对你会有所帮助。1 域名访问不到一般我们在第一次对接第三方平台的API接口时，可能会先通过浏览器或者postman调用一下，该接口是否可以访问。有些人可能觉得多次一举。其实不然。有可能你调用第三方平台的API接口时，他们的接口真的挂了，他们还不知道。还有一种最重要的情况，就是你的工作网络，是否可以访问这个外网的接口。有些公司为了安全考虑，对内网的开发环境，是设置了防火墙的，或者有一些其他的限制，有些ip白名单，只能访问一些指定的外网接口。如果你发现你访问的域名，在开发环境访问不通，就要到运维同学给你添加ip白名单了。2 签名错误很多第三方API接口为了防止别人篡改数据，通常会增加数字签名(sign)的验证。sign = md5(多个参数拼接 + 密钥)在刚开始对接第三方平台接口时，会遇到参数错误，签名错误等问题。其中参数错误比较好解决，重点是签名错误这个问题。签名是由一些算法生成的。比如：将参数名和参数值用冒号拼接，如果有多个参数，则按首字母排序，然后再将多个参数一起拼接。然后加盐（即：密钥），再通过md5，生成一个签名。如果有多个参数，你是按首字母倒序的，则最后生成的签名会出问题。如果你开发环境的密钥，用的生产环境的，也可能会导致生产的签名出现问题。如果第三方平台要求最后3次md5生成签名，而你只用了1次，也可能会导致生产的签名出现问题。因此，接口签名在接口联调时是比较麻烦的事情。如果第三方平台有提供sdk生成签名是最好的，如果没有，就只能根据他们文档手写签名算法了。3 签名过期通过上面一步，我们将签名调通了，可以正常访问第三方平台获取数据了。但你可能会发现，同一个请求，15分钟之后，再获取数据，却返回失败了。第三方平台在设计接口时，在签名中增加了时间戳校验，同一个请求在15分钟之内，允许返回数据。如果超过了15分钟，则直接返回失败。这种设计是为了安全考虑。防止有人利用工具进行暴力破解，不停伪造签名，不停调用接口校验，如果一直穷举下去的话，总有一天可以校验通过的。sign = md5(多个参数拼接 + 密钥 + 时间戳)因此，有必要增加时间戳的校验。如果出现这种情况，不要慌，重新发起一次新的请求即可。​4 接口突然没返回数据如果你调用第三方平台的某个API接口查询数据，刚开始一直都有数据返回。但突然某一天没返回数据了。但是该API接口能够正常响应。不要感到意外，有可能是第三方平台将数据删除了。我对接完第三方平台的API接口后，部署到了测试环境，发现他们接口竟然没有返回数据，原因是他们有一天将测试环境的数据删完了。因此，在部署测试环境之前，要先跟对方沟通，要用哪些数据测试，不能删除。5 try...catch吗?我们通过调用第三方平台分页查询接口，同步数据时，如果接口查询了异常需要try/catch异常不？这是一个好问题。我们可能在一个循环中调用分页查询接口查数据，如果查询中间某一页的数据时，接口返回出现了异常。我们是直接终止程序，回滚前面写入表中的数据，还是try/catch忽略本次异常，直接查询下一页的数据呢？很显然第三方平台将API接口设置成分页查询，说明他们考虑过，该接口返回的数据太多了，如果一次返回可能会超时。如果我们的程序在同步某一页数据时出现了异常，而直接回滚前面已同步过数据，也可能会出现回滚超时的问题，显然非常不现实。我们需要在同步程序中，做幂等设计，支持重复发起请求。如果我们的程序在同步某一页数据时出现了异常，try/catch忽略本次异常，直接查询下一页的数据。对于异常数据，我们可以在后面的某个时间点，再跑一次同步程序即可。如果要更智能化一点，可以增加失败自动重试机制。6 token失效有些平台的API接口在请求之前，先要调用另外一个API接口获取token，然后再header中携带该token信息才能访问其他的业务API接口。在获取token的API接口中，我们需要传入账号、密码和密钥等信息。每个接口对接方，这些信息都不一样。我们在请求其他的API接口之前，每次都实时调用一次获取token的接口获取token？还是请求一次token，将其缓存到redis中，后面直接从redis获取数据呢？很显然我们更倾向于后者，因为如果每次请求其他的API接口之前，都实时调用一次获取token的接口获取token，这样每次都会请求两次接口，性能上会有一些影响。如果将请求的token，保存到redis，又会出现另外一个问题：token失效的问题。我们调用第三方平台获取token的接口获取到的token，一般都…

解决分布式事务的8个小技巧
type: item
date: 2026-06-05T20:06:56+08:00
description: 种1. 引言在上篇文章中，我们已经谈到了分布式中至关重要的两个理论：CAP 理论和 BASE 理论。今天将带大家探讨分布式事务里的“八奇技”，帮助大家在实际的分布式系统中更好地运用事务。2. 分布式事务常见的解决分案分布式事务是在分布式系统中，跨越多个计算机节点或数据存储系统进行的事务，在这种环境下保证事务的ACID（原子性、一致性、隔离性、持久性）属性是一大挑战。对此，业界有以下 8 种常见的解决方案，俗称 “八奇技”。奇技1）2PC二阶段提交协议（Two-phase commit protocol），简称 2PC。两阶段提交是一种强一致性事务协议，它分为准备阶段和提交阶段。在准备阶段，协调者节点询问所有参与者是否准备好提交事务，如果所有参与者都答应准备好了，那么在提交阶段，协调者会通知所有参与者提交事务。如果有任何一个参与者在准备阶段没有准备好，那么协调者会通知所有参与者回滚事务。有熟悉 MySQL 的同学可能马上想到了，MySQL 的事务提交就是通过几种日志来实现二阶段提交的。不了解MySQL执行流程的可以看我之前写的这篇文章：一张图看懂SQL执行流程优点原子性保证：2PC 协议可以保证所有参与者要么全部提交成功，要么全部失败回滚，从而实现跨多个分布式节点的事务的原子性。简单直观：2PC 的设计思路简单，逻辑清晰，容易理解，这使得它在很多传统的数据库和分布式系统中得到了广泛的应用，比如 MySQL 从 5.5 版本开始支持。缺点同步阻塞：在 2PC 的第一阶段，所有参与者在响应协调者的准备请求后，必须等待最终的提交或回滚指令。这期间，所有参与者都处于阻塞状态，无法进行其他操作，导致资源锁定时间较长，在高并发场景下很明显不太适用。单点故障：如果协调者在第二阶段崩溃，参与者可能会无限期地等待指令，因为它们不知道应该提交还是回滚。这使得整个系统容易受到单点故障的影响。数据不一致：如果在第二阶段中协调者向某些参与者发送了提交指令，而其他参与者因为网络问题没有收到指令，那么这些没有收到指令的参与者可能会选择回滚，导致数据不一致。复杂的恢复机制：当系统崩溃后，恢复过程非常复杂，所有参与者必须保持足够的信息以便在系统恢复后能够继续完成 2PC 协议。奇技2）3PC三阶段提交协议（Three-phase commit protocol），简称 3PC。三阶段提交（3PC）是两阶段提交（2PC）的改进版本，它旨在减少在协调者和参与者之间的阻塞时间，同时增加系统在某些故障情况下的容错能力，以下是 3PC 的三个阶段：CanCommit 阶段协调者行动: 发送 CanCommit 请求到所有参与者，并等待回应。参与者行动: 如果参与者可以提交事务，它就返回 Yes，并进入预备状态；如果不能提交，则返回 No。PreCommit 阶段协调者行动: 如果所有参与者回答 Yes，协调者发送 PreCommit 请求给所有参与者，并进入 Prepared 阶段；如果有任何参与者回答 No，或者等待超时，协调者发送 abort 请求。参与者行动: 在收到 PreCommit 请求后，参与者会执行事务操作，写入日志，但不提交，然后响应 ACK，并等待最终指令。如果参与者在这个阶段超时没有收到协调者的消息，它将中止事务。DoCommit 阶段协调者行动: 一旦协调者收到所有参与者的 ACK，它会进入 DoCommit 阶段，发送 commit 请求给所有参与者。参与者行动: 参与者在收到 commit 请求后，提交事务，释放所有事务锁定的资源，并向协调者发送完成消息。与 2PC 相比，3PC 在 PreCommit 阶段引入了超时机制，允许参与者在没有接收到协调者的最终指令时自行决定中止事务，这减少了协调者成为单点故障的可能性。实际业务场景3PC通常用于需要较高可靠性的分布式系统中，尤其是在那些不能接受长时间锁定资源的场景。例如：分布式数据库系统：分布式数据库可能使用 3PC 来确保跨多个数据中心的事务一致性。例如，一个全球性的银行可能需要在不同国家的分支机构之间处理账户转账，这时3PC可以减少在网络延迟或某个分支机构失去响应时的影响。电信网络：在电信运营商的计费系统中，可能会使用 3PC 来同步跨多个服务点的账单信息，这些系统通常要求高可用性和快速响应，因此不能长时间阻塞。大型分布式系统：对于需要跨多个服务和组件协调工作的大型分布式系统，比如云计算平台，3PC可以在保持事务一致性的同时，减少参与者等待协调者指令的时间。使用 3PC 的考虑因素虽然 3PC 提供了比 2PC 更好的容错性和减少了阻塞的时间，但它仍然有一些缺点：复杂性：3PC 比 2PC 更复杂，需要更多的消息交换和更多的状态管理。性能开销：3PC 引入了额外的阶段和网络通信，可能会导致更大…

系统上线时SQL脚本的9大坑
type: item
date: 2026-06-05T20:06:39+08:00
description: 前言系统上线时，非常容易出问题。即使之前在测试环境，已经执行过SQL脚本了。但是有时候，在系统上线时，在生产环境执行相同的SQL脚本，还是有可能出现一些问题。有些小公司，SQL脚本是开发自己执行的，有很大的风险。有些大厂，有专业的DBA把关，但DBA也不是万能的，还是有可能会让一些错误的SQL脚本被生产环境执行了，比如：update语句的顺序不对。今天跟大家一起聊聊，系统上线时SQL脚本的9大坑，以便于大家吸取教训，能够防微杜渐，希望对你会有所帮助。1 漏脚本了我们上线时执行的SQL脚本，出现次数最多的问题，应该是漏脚本了。有时候少加了一个字段。有时候字段的注释没有及时修改。有时候有些新表没创建。有时候字段类型忘了修改。等等。我们的SQL脚本中漏脚本的情况有很多。那么，如何解决这个问题呢？答：将SQL脚本做成代码的一部分。在项目的代码中，创建一个专门的sql目录，在该目录下根据每个迭代创建一个子目录，比如：mv3.2.1，将SQL脚本存放到mv3.2.1下。我们在开发环境任何对表的相关操作，比如：增加字段、修改字段类型、修改注释、增加索引、创建表等等，都需要通过SQL语句操作，然后把该SQL语句，整理到SQL脚本中。最后提交到公司的GitLab上，我们在测试环境和生产环境发版时，去GitLab上找相关迭代版本的SQL脚本执行。通过该方式基本可以解决漏脚本的问题。2 脚本语法错误有些小伙伴看到这个标题可能有点懵，SQL脚本不是已经在测试环境执行过了吗？为什么还会出现语法错误？比如说有这样的场景：原本你的SQL脚本没问题的，但没有按照规范，给一张表的添加多个字段，你写了多条ALTER语句。例如：alter table t_user add column `work` varchar(30) DEFAULT NULL COMMENT '工作'; alter table t_user add column `provice` varchar(10) DEFAULT NULL COMMENT '籍贯';在上线时，你给DBA提SQL工单时，该工单被DBA审核拒绝打回来了。然后为了赶时间，你急急忙忙把多条ALTER语句改成一条ALTER语句。例如：alter table t_user add `work` varchar(30) DEFAULT NULL COMMENT '工作', add `provice` varchar(10) DEFAULT NULL COMMENT '籍贯';但在修改的过程中，有地方少了一个逗号，就可能会出现SQL语法错误。因此，不管是什么SQL语句，要养成好习惯，只要修改了一定要记得到开发环境的数据库中，先执行测试一下，切勿直接提到生产环境，即使你有很大的把握，也需要再更慎重一些。这样基本可以避免SQL语法错误的问题。3 脚本顺序不对有些时候，我们在上线系统时，DBA在执行SQL脚本的时候，没有报错，但最后的数据就是不对。有可能是脚本顺序不对导致的。比如有这样一种场景：你往某张表通过insert初始化了一条数据。例如：INSERT INTO `sue`.`t_user`(`id`, `code`, `age`, `name`, `height`, `address`, `work`, `provice`) VALUES (1, '101', 21, '周星驰', 173, '香港', NULL, NULL);另外一个人要基于你这条数据，通过update修改数据。例如：update t_user set age=25 where id=1;你们提了两条SQL脚本。另外一个人先提的，你后提的。DBA先把他的SQL工单审核通过了，先update数据，此时通过id是没法找到那条数据的，影响行数为0。然后DBA再审核你的SQL工单，审核通过了，插入了一条数据。由于SQL脚本的顺序不对，导致最终系统上线时的数据不对。那么这个问题要如何解决呢？双方要事先沟通好，把另外一个同事的SQL脚本加到你的初始化脚本中，你的脚本在初始化时，直接去修改数据即可。例如：INSERT INTO `sue`.`t_user`(`id`, `code`, `age`, `name`, `height`, `address`, `work`, `provice`) VALUES (1, '101', 25, '周星驰', 173, '香港', NULL, NULL);这样可以避免执行顺序问题。4 执行时机不对有些系统功能已经上线了，在后面的迭代中，为了尽量避免少影响线上功能，可以增加一个pre（即预生产环境）。该环境跟生产环境是差不多的，连接了相同的数据库，使用了相同的apollo配置。但唯一的区别是pre环境没有实际的用户流量，只能公司内部人员才能…

糟糕，接口被刷了，怎么办？
type: item
date: 2026-06-05T20:06:30+08:00
description: 前言在面试时，经常会被问一个问题：如何防止别人恶意刷接口？这是一个非常有意思的问题，防范措施挺多的。今天这篇文章专门跟大家一起聊聊，希望对你会有所帮助。1 防火墙防火墙是网络安全中最基本的安全设备之一，主要用于防止未经授权的网络访问和攻击。防火墙可以防止的攻击行为包括：无效数据包：防火墙可以识别和过滤掉无效的数据包，如错误的 IP 地址、伪造的数据包和无法识别的协议等。DOS 和 DDOS 攻击：防火墙可以使用不同的技术来检测和阻止 DOS 和 DDOS 攻击，如阻止大量 TCP/UDP 连接、IP 地址过滤和流量限制等。病毒和蠕虫攻击：防火墙可以使用特定的病毒和蠕虫检测技术，如签名检测、行为检测、模式识别等，来防止这些恶意软件的传播。网络钓鱼和欺骗攻击：防火墙可以检测和防止网络钓鱼和欺骗攻击，如防止虚假登录页面和欺骗的网站等。恶意流量攻击：防火墙可以检测和防止恶意流量攻击，如过滤掉带有恶意载荷的数据包和防止被黑客利用的端口。网络侦察攻击：防火墙可以使用一些技术来防止网络侦察攻击，如防止扫描、端口扫描和漏洞利用等。防火墙主要用于过滤和控制网络流量，以保护网络安全。2 验证码对于一些非常重要的接口，在做接口设计的时候，要考虑恶意用户刷接口的情况。最早的用户注册接口，是需要用图形验证码校验的，比如下面这样的：用户只需要输入：账号名称、密码和验证码即可，完成注册。其中账号名称作为用户的唯一标识。但有些图形验证码比较简单，很容易被一些暴力破解工具破解。由此，要给图形验证码增加难道，增加一些干扰项，增加暴力破解工具的难道。但有个问题是：如果图形验证码太复杂了，会对正常用户使用造成一点的困扰，增加了用户注册的成本，让用户注册功能的效果会大打折扣。因此，仅靠图形验证码，防止用户注册接口被刷，难道太大了。后来，又出现了一种移动滑块形式的图形验证方式，安全性更高。此外，使用验证码比较多的地方是发手机短信的功能。发手机短信的功能，一般是购买的云服务厂商的短信服务，按次收费，比如：发一条短信0.1元。如果发送短信的接口，不做限制，被用户恶意调用，可能会产生非常昂贵的费用。3 鉴权对于有些查看对外的API接口，需要用户登录之后，才能访问。这种情况就需要校验登录了。可以从当前用户上下文中获取用户信息，校验用户是否登录。如果用户登录了，当前用户上下文中该用户的信息不为空。否则，如果用户没登录，则当前用户上下文中该用户的信息为空。对于有些重要的接口，比如订单审核接口，只有拥有订单审核权限的运营账号，才有权限访问该接口。我们需要对该接口做功能权限控制。可以自定义一个权限注解，在注解上可以添加权限点。在网关层有个拦截器，会根据当前请求的用户的权限，去跟请求的接口的权限做匹配，只有匹配上次允许访问该接口。4 IP白名单对于有些非常重要的基础性的接口，比如：会员系统的开通会员接口，业务系统可能会调用该接口开通会员。会员系统为了安全性考虑，在设计开通会员接口的时候，可能会加一个ip白名单，对非法的服务器请求进行拦截。这个ip白名单前期可以做成一个Apollo配置，可以动态生效。如果后期ip数量多了的话，可以直接保存到数据库。只有ip在白名单中的那些服务器，才允许调用开通会员接口。这样即使开通会员接口地址和请求参数被泄露了，调用者的ip不在白名单上，请求开通会员接口会直接失败。除非调用者登录到了某一个白名单ip的对应的服务器，这种情况极少，因为一般运维会设置对访问器访问的防火墙。当然如果用了Fegin这种走内部域名的方式访问接口，可以不用设置ip白名单，内部域名只有在公司的内部服务器之间访问，外面的用户根本访问不了。但对于一些第三方平台的接口，他们更多的是通过设置ip白名单的方式保证接口的安全性。5 数据加密以前很多接口使用的是HTTP（HyperText Transport Protocol，即超文本传输协议）协议，它用于传输客户端和服务器端的数据。虽说HTTP使用很简单也很方便，但却存在以下3个致命问题：使用明文通讯，内容容易被窃听。不验证通讯方的真实身份，容易遭到伪装。无法证明报文的完整性，报文很容易被篡改。为了解决HTTP协议的这些问题，出现了HTTPS协议。HTTPS协议是在HTTP协议的基础上，添加了加密机制：SSL：它是Secure Socket Layer的缩写， 表示安全套接层。TLS：它是Transport Layer Security的缩写，表示传输层安全。HTTPS = HTTP + 加密 + 认证 + 完整性保护。为了安全性考虑，我们的接口如果能使用HTTPS协议，尽量少使用HTTP协议。如果你访问过一些大厂的网站，会发现他们提供的接口，都是使用的HTTPS协议。6 限流之前提到的发送短信接口，只校验验证码还不够，还需要对用户请求做限流。从页…

糟了，线上服务出现OOM了
type: item
date: 2026-06-05T20:06:20+08:00
description: 前言前一段时间，公司同事的一个线上服务OOM的问题，我觉得挺有意思的，在这里跟大家一起分享一下。我当时其实也参与了一部分问题的定位。1 案发现场他们有个mq消费者服务，在某一天下午，出现OOM了，导致服务直接挂掉。当时我们收到了很多内存的报警邮件。发现问题之后，运维第一时间，帮他们dump了当时的内存快照，以便于开发人员好定位问题。之后，运维重启了该服务，系统暂时恢复了正常。大家都知道，如果出现了线上OOM问题，为了不影响用户的正常使用，最快的解决办法就是重启服务。但重启服务治标不治本，只能临时解决一下问题，如果不找到真正的原因，难免下次在某个不经意的时间点，又会出现OOM问题。所以，有必要定位一下具体原因。2 初步定位问题当时运维dump下来的内存快照文件有3G多，太大了，由于公司内网限制，没办法及时给到开发这边。没办法，只能先从日志文件下手了。在查日志之前，我们先查看了prometheus上的服务监控。查到了当时那个mq消费者服务的内存使用情况，该服务的内存使用率一直都比较平稳，从2022-09-26 14:16:29开始，出现了一个明显的内存飙升情况。根据以往经验总结出来的，在追查日志时，时间点是一个非常重要的过滤条件。所以，我们当时重点排查了2022-09-26 14:16:29前后5秒钟的日志。由于这个服务，并发量不大，在那段时间的日志量并不多。所以，我们很快就锁定了excel文件导入导出功能。该功能的流程图如下：用户通过浏览器上传excel，调用文件上传接口。该接口会上传excel到文件服务器。然后将文件url，通过mq消息，发送到mq服务器。mq消费者消费mq消息，从文件服务器中获取excel数据，做业务处理，然后把结果写入新的excel中。mq消费者将新excel文件上传到文件服务器，然后发websocket消息通知用户。用户收到通知结果，然后可以下载新的excel。经过日志分析，时间点刚好吻合，从excel文件导入之后，mq消费者服务的内存使用率一下子飙升。3. 打不开dump文件从上面分析我们得出初步的结论，线上mq消费者服务的OOM问题，是由于excel导入导出导致的。于是，我们查看了相关excel文件导入导出代码，并没有发现明显的异常。为了找到根本原因，我们不得不把内存快照解析出来。此时，运维把内存快照已经想办法发给了相关的开发人员（我的同事）。那位同事用电脑上安装的内存分析工具：MAT（Memory Analyzer Tool），准备打开那个内存快照文件。但由于该文件太大，占了3G多的内存，直接打开失败了。MemoryAnalyzer.ini文件默认支持打开的内存文件是1G，后来它将参数-xmx修改为4096m。修改之后，文件可以打开了，但打开的内容却有问题。猛然发现，原来是JDK版本不匹配导致的。他用的MAT工具是基于SunJDK，而我们生成环境用的OpenJDK，二者有些差异。SunJDK采用JRL协议发布，而OpenJDK则采用GPL V2协议发布。两个协议虽然都是开放源代码的，但是在使用上的不同，GPL V2允许在商业上使用，而JRL只允许个人研究使用。所以需要下载一个基于OpenJDK版本的MAT内存分析工具。4. 进一步分析刚好，另一个同事的电脑上下载过OpenJDK版本的MAT内存分析工具。把文件发给他帮忙分析了一下。最后发现org.apache.poi.xssf.usermodel.XSSFSheet类的对象占用的内存是最多的。目前excel的导入导出功能，大部分是基于apache的POI技术，而POI给我们提供了WorkBook接口。常用的WorkBook接口实现有三种：HSSFWorkbook：它是早期使用最多的工具，支持Excel2003以前的版本，Excel的扩展名是.xls。只能导出65535条数据，如果超过最大记录条数会报错，但不会出现内存溢出。XSSFWorkbook：它可以操作Excel2003-Excel2007之间的版本，Excel的扩展名是.xlsx。最多可以导出104w条数据，会创建大量的对象存放到内存中，可能会导致内存溢出。SXSSFWorkbook：它可以操作Excel2007之后的所有版本，Excel的扩展名是.xlsx。SXSSFWorkbook是streaming版本的XSSFWorkbook，它只会保存最新的rows在内存里供查看，以前的rows都会被写入到硬盘里。用磁盘空间换内存空间，不会导致内存溢出。看到了这个类，可以验证之前我们通过日志分析问题，得出excel导入导出功能引起OOM的结论，是正确的。那个引起OOM问题的功能，刚好使用了XSSFWorkbook处理excel，一次性创建了大量的对象。关键代码如下：XSSFWorkbook wb =…