1. 事务嵌套和mybatis一级缓存
某个方法,事务进行了嵌套,发现怎么也查不出来数据,最后发现是mybatis一级缓存的问题。尽量不要嵌套事务。
@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
String mob = "123456789";
// 查询数据
Resume exist = repository.findByMobileNumber(mob);
// 插入数据
if (exist == null) {
((ResumeCommandApiImpl) AopContext.currentProxy()).insert(mob);
}
// 再次查询数据
exist = repository.findByMobileNumber(mob);
if (exist == null) {
System.out.println("giao 总是查不到");
} else {
System.out.println("giao 不可能查得到,因为这时候走的是mybatis一级缓存");
}
}
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
protected void insert(String mob) {
// 插入数据
}
2. mybatis一级缓存和二级缓存
一级缓存(Local Cache)
- 作用域:SqlSession 级别
- 默认开启
- 同一个 SqlSession 中,相同的查询语句(包括参数),第二次不会访问数据库,而是从缓存中拿。
失效条件
- 执行 update、insert、delete
- 手动调用 clearCache()
- SqlSession 关闭
- 查询参数不同(哪怕只是分页参数)
实际开发中的注意事项
-
多次查询相同数据时,应复用同一个 SqlSession(比如 Spring 管理下的事务环境)
-
MyBatis 在 Spring 中默认每个 Mapper 方法就是一个新的 SqlSession,如果你希望利用一级缓存,需要显式设置事务传播(比如用 @Transactional 包住多次查询)
@Service public class UserService {
@Autowired private UserMapper userMapper; @Transactional // 这个注解很关键!!没有 @Transactional 的话,u1 和 u2 是两个 SqlSession,各查一次数据库。 public void doSomething() { // 这两个调用共用一个 SqlSession,第二次查询会命中一级缓存 User u1 = userMapper.getUserById(123); User u2 = userMapper.getUserById(123); }}
二级缓存(Global Cache)
- 作用域:Mapper namespace 级别
- 需要显式开启,默认关闭
- 跨 SqlSession 生效,缓存数据存在于 Mapper 级别(namespace)
- 支持自定义缓存实现(如使用 Redis、Ehcache)
总之一句话,没啥卵用。真有那种固定的查询,你直接放redis里面多好。
3. 执行save,一级缓存会失效,那么,save报错了呢?还会失效吗?
只要执行了 insert、update、delete 语句,无论成功还是失败、是否回滚,MyBatis 都会立即清空当前 SqlSession 的一级缓存。
源码如下,先clearLocalCache,再doUpdate。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
4. MyBatis的Executor.update()
executor.update(ms, wrapCollection(parameter));
观察到MyBatis 中,insert/update/delete 都是“更新型”操作,底层统一走 Executor.update() 方法。
三类操作统一通过update() 方法执行,是一种统一写操作处理逻辑的架构设计,不影响语义,只是内部实现策略。简化架构。
5. MVP版本
“打 MVP”这个说法,源于互联网/软件开发行业中“MVP(Minimum Viable Product)”的概念,即“最小可行产品”。
但是:MVP ≠ 半成品,不是说能跑就完事,而是:质量要可用、数据要真实、用户要能完成核心目标。
6. 一次字段命名带来的感悟
场景:有个字段,业务方叫做交付群编号,我这边在表结构中要不要体现交付群这个概念?比如我可以直接用number表示,也可以用delivery_group_number。
ai答:实话实说:建议用 delivery_group_number(或 delivery_group_id)明确体现“交付群”这个业务概念,而不是模糊地用 number。
原因如下:
1. 表达清晰,增强语义可读性
2. 便于多人协作与维护
3. 领域语言与数据库设计一致
第三点我认为是最关键的一点,即不丢失上下文信息,这对于DDD可太重要了,如果只有一个number在DDD中代码就没什么语义了,但是用上delivery_group_number,对于写DDD代码,简直太爽了,特别清晰,可以明确表达业务含义。
7. 还有表名称
city_company_number 还是 city_company_delivery_group_number?
city_company_number
语义不清:number 是什么编号?城市公司编号还是交付群编号?容易误导未来维护者,尤其是和其它“number”字段混用时。
city_company_delivery_group_number
啰嗦 + 模糊:既长又含糊,number 没说明是 ID 还是编码,结构表达不清晰。
推荐命名:city_company_delivery_group
实体之间的关系!!!!!!!!!!!!!实体表和关系表。
8. 离线数仓 和 实时数仓。
数仓体系结构:离线 vs 实时
维度
离线数仓
实时数仓
核心处理方式
批处理(Batch)
流处理(Stream)
数据延迟
小时级、天级(T+1、T+0)
秒级、亚秒级
技术代表
Hive、Spark、Presto、Azkaban
Kafka、Flink、ClickHouse、Doris
存储介质
HDFS、Object Storage(如 S3)
ClickHouse、Doris、HBase、Redis
典型用途
报表、分析、模型训练、归档
看板、风控、推荐、告警
调度方式
定时触发(crontab、调度器)
实时触发(数据到达即处理)
9. Outbox Pattern(发件箱模式)
“消息落库 + 异步投递 + 定时补偿”方案:
- 数据库表中新增 outbox_message 表,记录待发送消息
- 本地事务中同时写订单表和消息表(强一致)
- 独立线程异步轮询发送消息表中的消息(失败重试 + 状态标记)
- 可加定时任务定期补偿未成功投递的消息
这种模式叫 Outbox Pattern,牺牲实时性但易维护、高可用。做开发票的时候,用的就是这种方法,有两个定时任务。一个扫描等待开票的数据,另一个扫描开票失败的数据。我说怎么这么好使!原来这条路前人都走过了。
10. rocketmq事务消息和Outbox比
Outbox Pattern 更稳、更可控
RocketMQ 原生事务消息的问题:
缺点
说明
高耦合性
Producer 要实现事务回查接口,逻辑和 MQ 耦死
难扩展
未来想换 MQ(如 Kafka)等于推倒重来
无法批量
一次只能一条事务消息,吞吐受限
Broker 回查机制不稳定
Producer 宕机会导致回查失败,消息“悬挂”
11. DeferredResult 失效
DeferredResult 是 Spring MVC 提供的异步请求处理机制之一,适用于服务端需要“延迟返回结果”的场景,让当前线程释放,避免 Tomcat 请求线程阻塞。做扫码登录这种需求的时候可以用。相当于提前告诉浏览器:“我收到请求了,但结果我晚点再告诉你。”。
但是,遇到了失效的情况。经过排查,发现和下面这个过滤器有关。
public class WebReqAndResLogFilterConfig extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 普通的 request 和 response 的输入输出流一旦读取/写入就不能再读/写,包装后通过缓存可以多次读取内容。
ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response);
// 执行过滤链(让请求继续向下走)
filterChain.doFilter(cachingRequest, cachingResponse);
// 打印请求日志
String method = request.getMethod();
String uri = request.getRequestURI();
byte[] requestContent = cachingRequest.getContentAsByteArray();
log.info("Request: {}, {}, {}", method, uri, new String(requestContent, StandardCharsets.UTF_8));
// 打印响应日志
int status = response.getStatus();
byte[] responseContent = cachingResponse.getContentAsByteArray();
log.info("Response: {}, {}", status, new String(responseContent, StandardCharsets.UTF_8));
// 响应内容重新写回(关键)
// 由于我们读取了 response 内容(getContentAsByteArray()),它已经“被消费”了,必须写回原始输出流。
// copyBodyToResponse() 就是把缓存的 response 数据再写一遍到真实响应流。
cachingResponse.copyBodyToResponse();
}
}正确的解决方向:在过滤器中增加判断,只对非异步请求做日志处理。// 加到那该死的doFilter后面!!
// request.isAsyncStarted() 返回 true 的前提是:
// 在后面的 Controller 或业务代码中调用了 startAsync(),比如通过 DeferredResult、Callable 启用了异步处理。
// 但你调用 filterChain.doFilter() 之前,请求还没有进入 Controller,isAsyncStarted()自然不会返回true。
if (request.isAsyncStarted()) {
filterChain.doFilter(request, response); // 直接走原始 request/response
return;
}
12. ApplicationRunner
简单说,ApplicationRunner 是 Spring Boot 提供的一个接口,用来在 Spring 容器启动完成、所有Bean创建和初始化完毕后,执行一些代码。比如:初始化缓存/字典、打印启动信息、校验关键配置、启动定时任务、预热数据等。
什么时候用它?当你做的事需要:所有 Bean 已准备好,可以注入使用的时候,就用它。
13. 比如:查看spring 有多少个filter,刚好就可以用ApplicationRunner
注意:beanFactory.getBeansOfType(),这句只能拿到,通过显式注册的 FilterRegistrationBean 类型的 Bean(也就是显式用 Java Config 注册过的过滤器),如果你的某些过滤器没有显式注册,也拿不到。从工程实践角度看建议都注册上,方便管理。
@Component
public class FilterAsyncChecker implements ApplicationRunner {
@Autowired
private ListableBeanFactory beanFactory;
@Override
public void run(ApplicationArguments args) throws Exception {
Map<String, FilterRegistrationBean> filters = beanFactory.getBeansOfType(FilterRegistrationBean.class);
for (Map.Entry<String, FilterRegistrationBean> entry : filters.entrySet()) {
entry.getValue().setAsyncSupported(true);
System.out.println("Filter bean: " + entry.getKey() + ", asyncSupported: " + entry.getValue().isAsyncSupported());
}
}
}
14. 查看有多少aop
和查看filter数量一样,查看项目里面aop的数量,能够更好的把握项目运行情况。
@Component
public class AopScanner implements ApplicationRunner {
@Autowired
private ListableBeanFactory beanFactory;
@Override
public void run(ApplicationArguments args) {
Map<String, Object> aspectBeans = beanFactory.getBeansWithAnnotation(Aspect.class);
System.out.println("🔍 自定义 AOP 切面数量:" + aspectBeans.size());
aspectBeans.forEach((name, bean) -> {
System.out.println("切面Bean: " + name + ", 类型: " + bean.getClass().getName());
});
}
}
没想到没过几天就有用武之地了。
15. 用archunit检测返回值是否含有Long 或 long
人都会有失误。有些事不应该告诉别人怎么做,然后靠自觉遵守。而应该是:你不能这么做,你一旦这么做了,你会无法继续前进。
好的架构不是出一堆规范,然后让人自觉遵守,而是应该在设计层面就不允许违反规范的情况出现。出现就噶。约束优于约定。
想了想还是不做了,项目启动已经很慢了,再加这么个扫描,还干不干活了。。。就这么个返回值,idea搜索 *VO.java 全出来了,闲了搜一搜得了。
16. 循环分页工具类
项目中有很多循环分页查询的代码,比如给职位过期。你不可能一下把项目里面的数据全查出来,或者直接执行个update语句。一般都是分页查询,然后循环这个分页,去执行过期操作。这就导致很多模板代码,看着不爽。于是优化了一版。真是泰裤辣。
public class PageLoopHelper {
public static <T> void scrollByPage(long startPage, long pageSize, PageFetcher<T> fetcher, PageProcessor<T> processor) {
long current = startPage;
IAScrollPage<T> page = fetcher.fetch(current, pageSize);
while (CollectionUtils.isNotEmpty(page.getRecords())) {
processor.process(page.getRecords());
if (!page.isHasMore()) {
break;
}
current++;
page = fetcher.fetch(current, pageSize);
}
}
// 重载(默认 1L 和 100L)
public static <T> void scrollByPage(PageFetcher<T> fetcher, PageProcessor<T> processor) {
scrollByPage(1L, 100L, fetcher, processor);
}
}
@FunctionalInterface
public interface PageFetcher<T> {
// 框架固定参数签名(current, size),如果有额外参数,调用方通过 lambda 捕获额外参数。
IAScrollPage<T> fetch(long current, long size);
}
@FunctionalInterface
public interface PageProcessor<T> {
void process(List<T> records);
}
// 使用例子。最骚的是,这玩意可以固定俩参数。然后后面你爱传什么就传什么,实用性拉满。优雅~ ~
PageLoopHelper.scrollByPage(
(current, size) -> xxxRepository.findPage(current, size, year, month),
this::saveOrUpdateBatch);
17. 统计报表
做统计,有些数据需要落表,有些需要实时查询和计算。
18. 做需求之前,讲明白边界
比如做导入,是把数据导入就行?还是也要校验模板。
失败怎么办?要返回为何失败原因吗?
开始做的时候就讲清楚,避免后续扯皮。边界要清晰,讲清楚再做!磨刀不误砍柴工。
19. AopContext.currentProxy())获取失败
Spring 并不会默认给所有类创建代理对象,只有当你使用了 AOP 相关功能时,才会为命中的 Bean 创建代理对象。
所以,使用的时候要注意观察,看看当前调用方是否有代理对象。
20. 关于文件下载的方案
方案1:直接返回给前端流
实现简单,不需要额外存储空间,适合小文件或临时文件。
缺点是大文件或者耗时久的文件,返回慢。本质上是个同步的操作,用户点击下载就要等待返回。
方案2:将文件上传到OSS,返回链接
实现稍微麻烦,需要额外存储空间。但是文件的下载会由oss负责。
这是异步的,部分耗时场景或者大文件,可以做到先让用户返回,等下载好了再让用户来拿链接就行了。
21. 关于前后端交互-百分比
"rate": 0.66,
"rateDisplay": "66%"
百分比字段,后端应该给前端两个字段。
好处1:保留信息,如果这个接口还有图标展示需求,可以直接用rate
好处2:后端如果有导出excel功能,也可以直接用rateDisplay,有计算就用rate
22. new BigDecimal 和 BigDecimal.valueOf
public static BigDecimal valueOf(double val) { return new BigDecimal(Double.toString(val)); }
其实就是里面包了一层,不同的类型会有不同的处理,切记,用valueOf,永远不要用new BigDecimal,不小心new 到double和float类型,你会哭的。
23. 好像就不应该分页去查数据库
前几天写的那个PageLoopHelper,好像是在一坨shit上面优化。
分页去查询数据库,然后执行一些操作。为什么我们不能一次把所有的要操作的数据的d都查出来呢?就算是千万数据,拿出来一千万个id能占用多少资源?
nonono,还是得分页。更好的做法是:按主键 ID 扫描。
SELECT id FROM your_table WHERE expire_time < NOW() AND status != 2 AND id > ? ORDER BY id LIMIT 1000;
24. VarHandle
VarHandle handle = MethodHandles.lookup()
.in(MyClass.class)
.findVarHandle(MyClass.class, "field", int.class);
handle.set(myObject, 42); // 相当于 myObject.field = 42
-
VarHandle(变量句柄)是 Java 9 引入的一个类,位于 java.lang.invoke 包。
-
它类似于一种安全的指针(handle),可以用来访问类字段、数组元素、静态变量等的变量。
-
它是 Unsafe 的官方替代,提供更安全和规范的方式来做底层内存访问。
-
允许执行各种内存访问操作,包括:
-
读写(get/set)
-
原子操作(CAS、getAndSet 等)
-
有序访问(volatile、lazySet)
方法名
作用
get(Object...)
读取变量值
set(Object..., V)
设置变量值
compareAndSet(...)
原子比较并设置
getAndSet(...)
原子获取旧值并设置新值
getVolatile(...)
以 volatile 语义读取
setVolatile(...)
以 volatile 语义写入
lazySet(...)
有序写入(性能更优但不是强同步)