news 2026/10/8 20:30:55

Java实习生必读:Redis核心知识实战,缓存三大问题与分布式锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java实习生必读:Redis核心知识实战,缓存三大问题与分布式锁

带实习生三年多,我交出去的第一步任务,永远是让他们把公司项目的 Redis key 全部导出来,统计前缀、过期时间、大 key 分布。有人觉得枯燥,有人却能从一份 key 清单里把缓存的业务模型反推出来。后来观察下来,能不能独立处理线上 Redis 问题,差距从这一步就开始拉开了。

这篇文章不讲源码,不聊底层数据结构,只聚焦 Java 实习生在真实工程环境里绕不开的 Redis 知识:数据类型对应哪种业务模型,缓存穿透击穿雪崩到底怎么解,分布式锁从简单到可靠经历了什么,持久化与内存淘汰如何选型,以及 Java 客户端接入时最容易被坑的序列化、超时、连接池问题。看完之后,不说让你成为 Redis 专家,至少项目里再碰到 Redis 超时、缓存不生效、锁失效这类事,你能往正确方向排查,而不是乱猜。

1. 实习生第一次接触 Redis 时,最需要建立的是“它到底在系统里扮演什么角色”

很多实习生的第一步是从网上找一份 Redis 命令手册,然后启动一个 redis-server,敲几个命令,陷入沉思:“这不就是个 map 吗?为什么要单独拿出来用?”

ConcurrentHashMap 只能在单个进程内部共享数据。一旦服务拆成多个节点,或者同一套代码部署了多个副本,每个进程的 Map 就各自独立了。而 Redis 是独立进程,通过 TCP 对外提供读写接口,天然成为多个应用节点之间的“共享状态容器”。这才是它在分布式系统里无法被本地缓存替代的根本原因。

工程上,Redis 在 Java 系统里主要出现在四个位置:

  • 缓存层:商品详情、用户信息这类读多写少的数据,先查 Redis,命中就不打到数据库。这是最常见的角色。
  • 分布式锁:多个应用实例同时抢同一份资源时,用 Redis 的原子命令做跨进程互斥。
  • 计数器与限流:库存预减、点赞数、访问量这类需要原子自增的场景,用 INCR/DECR 比数据库行锁成本低得多。
  • 临时状态存储:验证码、登录 token、短期会话,生命周期短且需要自动过期,正好匹配 Redis 的 TTL 机制。

实习生最容易犯的错误,是只把 Redis 当成缓存来背概念,却忽略了它的网络模型和单线程执行模型。Redis 的服务端是单线程处理命令,所以每个命令都是原子的。这也决定了使用它的方式:能一次命令解决的事,不要拆成多次;能用 Pipeline 批量发的,不要循环单发。这两条习惯,直接影响线上性能表现。

理解了 Redis 是干什么的,再往下看具体知识点,就不会觉得它们是一堆孤立命令了。

2. 数据类型不是八股文:每种数据结构都能对上具体的业务模型

网上关于 Redis 数据类型的文章,大多是“String 是字符串,Hash 是哈希,List 是列表”这种解释。对实习生来说,背五张命令表不如搞清楚一件事:这个类型在真实业务里到底解决什么问题。

2.1 String:不只是存字符串,还是计数器

String 能存的不仅是字符串,数字也是按字符串存的。使用 INCR、DECR、INCRBY 这类命令时,Redis 会把字符串解析成数值再原子增减。秒杀预减库存、视频播放量、点赞数,都是直接调用然后判断返回值。

// 扣减库存,返回扣减后的剩余值 Long remain = redisTemplate.opsForValue().decrement("stock:sku:1001", 1); if (remain < 0) { // 超卖,需要回滚并记录 redisTemplate.opsForValue().increment("stock:sku:1001", 1); }

这条命令每秒能扛几十万次操作,比数据库 update stock = stock - 1 再判断 affected rows 轻量太多。但要注意,Redis 计数器只能做前置判断,真正的订单扣减还是以数据库事务为准。

另外 String 还承担了分布式锁的载体,SETNX、SETEX 都在这个类型上实现。后面专门展开。

2.2 Hash:对象的最佳形态

Java 里一个对象有多个字段,在 Redis 里最直观的落法就是 Hash。key 存对象 ID,field 是属性名,value 是属性值。商品详情、用户资料、购物车,都能用 Hash 表达。

HashOperations<String, String, Object> ops = redisTemplate.opsForHash(); ops.put("cart:user:123", "sku_1001", "2"); ops.put("cart:user:123", "sku_1002", "1"); Map<String, Object> cart = ops.entries("cart:user:123");

Hash 跟 String + JSON 的最重要区别在于:Hash 支持单独修改某个字段而不需要反序列化整个对象。比如修改用户头像字段,Hash 只需要 hset 一个 field,String + JSON 则要先 get,再反序列化,改字段,再序列化,再 set,高并发下容易产生覆盖问题。

不过 Hash 也不是没有缺点。如果字段数量膨胀到几千个,hgetall 会一次性拉大量数据,容易产生大 key。工程上建议字段少、访问频繁的对象用 Hash,字段多的对象还是 String + JSON 更适合。

2.3 List:可以是队列,也可以是栈

List 的 LPUSH/RPOP 组合,就是典型的生产者消费者队列。在 Redis 还没引入消息队列模块之前,很多老项目就是用 List 做异步任务的。现在虽然更推荐 Stream,但 List 依然有它的价值:实现简单,天然阻塞版本 BLPOP 谁都会用。

// 生产者 redisTemplate.opsForList().leftPush("task:queue", taskId); // 消费者,阻塞获取 String taskId = redisTemplate.opsForList().rightPop("task:queue", 10, TimeUnit.SECONDS);

实习生要知道的坑在于:使用 List 做队列,消费端宕机时数据会丢。没有 ACK 机制,LPOP 之后数据就没了。如果业务要求不丢数据,至少得用 Stream,或者干脆上 MQ。

2.4 Set:去重与集合运算

Set 无序、元素唯一,适合做标签系统、好友关系、抽奖池。两个 Set 之间做交集、并集、差集,一条命令就能完成。比如“共同关注”功能,就是用 sinter 同时传入两个用户的关注集合。

Set<String> common = redisTemplate.opsForSet().intersect("follow:user:1001", "follow:user:1002");

另外 SETNX 的去重能力还常用于“用户是否已参与活动”这类判断,虽然严格来说用 String 也能做,但 Set 天然支持多个活动一个 key 内的去重,写起来更整洁。

2.5 ZSet:排行榜和延时队列的底层基础

ZSet 每个成员附带一个 score,按 score 排序。排行榜是最直接的应用:score 是分数,member 是用户 ID,ZREVRANGE 取前 N 名。

redisTemplate.opsForZSet().incrementScore("rank:week:2024W45", userId, 1); Set<ZSetOperations.TypedTuple<String>> top = redisTemplate.opsForZSet().reverseRangeWithScores("rank:week:2024W45", 0, 9);

ZSet 还有一个冷门但实用的玩法:延时队列。score 存执行时间戳,轮询 zrangebyscore 取出 score 小于当前时间的数据执行。很多简单场景的延迟任务,用 Redis ZSet 就够,不需要引入消息中间件。

2.6 扩展类型:Bitmap、HyperLogLog、Geo

这三个类型是实习生的加分项。Bitmap 适合签到、在线状态的位图存储;HyperLogLog 适合 UV 统计,牺牲极小精度换来极低内存;Geo 适合附近的人、门店距离排序。它们各自命令不多,但能体现你对 Redis 的了解不是停留在五种基础类型上。

类型选型没有绝对标准,但有原则:先想清楚业务操作是什么,再选数据形态。用错类型最常见的后果,就是代码里出现大量 get、set、delete 轮询,把一个本该用一条命令完成的操作拆成了几十次网络请求。

3. 缓存三大问题:穿透、击穿、雪崩,面试会问,上线也会踩

这是实习生面试必问、工作也容易碰上的问题组。我把三个问题放在一起讲,因为它们的现象和解决方案经常被混淆。

3.1 缓存穿透:查了个不存在的值

用户请求一个数据库中也不存在的数据,比如商品 ID 是 -1,缓存里自然没有,请求直接打到数据库。如果这种请求是恶意的、大量的,数据库会被无效查询压垮。

核心解法有三种:

  • 缓存空值:查询数据库发现不存在,也在 Redis 里放一个空结果,过期时间设短,比如 30 到 60 秒。代码上要区分“缓存命中空值”和“缓存未命中”。
  • 布隆过滤器:启动时把存在的 ID 全量加入布隆过滤器,请求先过过滤器,不存在的直接拒绝。缺点是布隆过滤器有误判率,且需要保证数据更新时同步维护。
  • 参数校验:ID 负数、超长、非法字符,在 Controller 层直接拦截。这一步成本最低,很多穿透事故的根因就是没做基础校验。

我见过最离谱的案例,是实习生上线后日志里全是 key = null 的空值查询,因为拼接缓存 key 时用了 null 字符串。参数校验不是可有可无。

3.2 缓存击穿:热点 key 过期的一瞬间

某个热点 key 过期时,大量并发请求同时发现缓存 miss,全部涌向数据库。和穿透的区别是:穿透是查不存在,击穿是查一个存在但刚过期的热点数据。

常用解法:

  • 互斥锁:缓存 miss 时先加分布式锁,只有一个线程能去查库并回填缓存,其他线程等待后重新查缓存。实现简单,代价是首次 miss 有短暂阻塞。
  • 逻辑过期:缓存里不设置 TTL,而是存一个过期时间戳。读取时发现逻辑过期,异步去刷新缓存,旧数据继续返回。适合允许短暂脏读、但必须扛住高并发的场景。
// 互斥锁思路 String lockKey = "lock:hot:" + id; String lockValue = UUID.randomUUID().toString(); try { Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 查数据库、回填缓存 } else { Thread.sleep(50); return redisTemplate.opsForValue().get(key); } } finally { if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } }

互斥锁代码写起来不难,难在释放锁的判断,后面分布式锁那节会具体讲为什么不能直接 delete。

3.3 缓存雪崩:大量 key 同一时间过期

雪崩覆盖面更广,现象是大量 key 集中过期,导致缓存命中率骤降,数据库瞬时压力飙升。常见原因有:设置了相同的过期时间、Redis 实例宕机。

解法对应为:

  • 过期时间加随机值:比如 30 分钟加 0 到 300 秒随机数,避免集中失效。
  • 多级缓存:本地缓存(Caffeine)再兜一层,Redis miss 时先查本地缓存,减少直击数据库的请求量。
  • 高可用:Redis Sentinel 或 Cluster,避免单点故障导致全局雪崩。

三种问题经常一起出现:一个接口数据量大、存在脏数据、热点集中过期,三个条件同时满足才是最头疼的。排查时先看监控里缓存命中率断崖下跌发生在哪个时间点,再对 key 列表判断是哪一类问题。

4. 分布式锁:从“能用”到“可靠”,差的不仅是几行代码

很多实习生花了很多时间研究 synchronized,到写分布式锁时,会用 SETNX 就觉得自己会了。但线上锁失效导致的超卖、重复执行、死锁问题,恰恰都在“以为会了”的细节里。

4.1 最基础版本:SETNX + EXPIRE

Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, value); if (ok) { redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); try { // 业务逻辑 } finally { redisTemplate.delete(lockKey); } }

这个版本的问题一眼就能看出来:如果 set 成功之后、expire 之前,进程崩了,锁就没有过期时间,直接死锁。虽然概率低,但不能靠概率写生产代码。

4.2 改进:SET 命令带上过期时间

Redis 2.6.12 之后,SET 命令支持 NX 和 EX 参数同时设置,一条命令保证原子性。Spring Data Redis 里的 setIfAbsent(key, value, timeout, unit) 就是干这个的。

Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);

锁的问题是加锁和解锁不是一对天然原子操作。如果线程 A 执行业务超过 30 秒,锁自动过期了,线程 B 拿到锁开始执行。这时 A 执行完,finally 里 delete 锁,会把自己加的锁释放掉——实际上释放的是 B 的锁。

这就是为什么要给锁一个唯一标识,释放前先比较。我上面缓存击穿示例里已经写了雏形:

if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); }

比较和删除之间仍然有窗口期,最简单的做法是用 Lua 脚本让“比较+删除”原子执行。Spring Data Redis 支持直接执行 Lua 脚本,但大多数时候你不需要手动写——Redisson 已经把整套逻辑封装好了。

4.3 实用方案:Redisson 的看门狗机制

Redisson 是 Java 生态里最主流的 Redis 客户端之一,它的分布式锁解决了两个核心问题:

  • 自动续期:默认锁超时 30 秒,拿到锁的线程如果还没执行完,后台会每 10 秒自动续期一次。业务崩了,续期逻辑也会停止,锁最终自动释放。
  • 释放安全:内部通过 Lua 脚本比较持有者标识再删除,不会误删别人的锁。

使用方式非常简单:

RLock lock = redissonClient.getLock("lock:order:" + orderId); try { if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }

实习生要学会的是:Redisson 也不是银弹。如果 Redis 主节点宕机,锁数据还没有同步到从节点,另一个节点可能仍然拿到锁。这种极端情况下需要 Redlock 算法,但工程里大多数业务对“锁丢失”的容忍度没那么低。理解 Redisson 解决的是什么、没解决什么,比背 Redlock 七步流程更重要。

另外要知道:同一把锁在不同线程里不是可重入的,除非用 Redisson 的 getLock。用 synchronized 的习惯带入分布式锁,最容易踩的坑就是两个不同方法里用了同一个 key,导致互相把自己的锁释放掉。

5. 持久化和内存淘汰:Redis 作为缓存和存储的底线

实习生最容易忽略的知识点,就是持久化。因为本地开发时 Redis 崩了重启,数据丢了也就丢了,但线上 redis 里存着用户 session、库存数据,宕机恢复后的数据一致性是大事。

5.1 RDB:定期快照

RDB 是把内存数据定期保存为二进制快照文件,文件小、加载快,适合做备份和灾难恢复。缺点是两次快照之间的数据会丢。默认配置里,900 秒内有 1 次写操作、300 秒内有 10 次写操作、60 秒内有 10000 次写操作就触发快照。

RDB 生成时用 fork 子进程,内存充足时问题不大,但大内存服务器 + 写频繁时,fork 开销不能忽略。线上遇到 Redis 突然卡顿,有时候就是 RDB 触发了。

5.2 AOF:追加写日志

AOF 记录每一条写命令,按执行顺序追加到文件。数据安全性高,可以通过 appendfsync 策略控制同步频率:always 每次写都同步磁盘,最安全但性能最差;everysec 每秒同步,最多丢一秒数据,工程里最常用。

AOF 文件会越来越大,需要定期 rewrite 压缩。Redis 4.0 之后默认开启混合持久化:RDB 作为基线快照,AOF 记录增量命令,兼顾加载速度和安全性。

实习生需要掌握的实际操作,是正确配置和排障时的判断思路:

# redis.conf 核心配置 appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

如果线上同时开启 RDB 和 AOF,重启时 Redis 会优先加载 AOF。记住这个顺序,你排障时会快很多。

5.3 内存淘汰策略:数据满了怎么办

Redis 默认配置 maxmemory 不设上限,但生产环境必须设置,否则内存被撑爆,操作系统会杀掉 Redis 进程。设置之后,数据达到上限时按照驱逐策略处理。

常用策略:

  • noeviction:达到上限后写请求直接报错,防止数据意外丢失,适合不能丢数据的业务。
  • allkeys-lru:从所有 key 里淘汰最近最少使用的,最常用。
  • volatile-lru:只从设置过过期时间的 key 里淘汰,适合冷热数据分离明显的场景。
  • allkeys-lfu / volatile-lfu:按访问频率淘汰,适合有稳定访问特征的数据。

选策略的思路是:先问这些数据丢不丢得起。缓存数据丢得起,选 allkeys-lru;有状态数据丢不起,选 noeviction,同时做好内存监控和预警。

maxmemory 4gb maxmemory-policy allkeys-lru

我经常让实习生做的事,是在压测环境把 maxmemory 调小,观察驱逐发生时的日志和命中率变化。只有亲眼看到 key 被淘汰、缓存命中率掉下去,才会真正明白驱逐策略为什么重要。

6. Java 客户端与 RedisTemplate:序列化、连接池、超时,三个隐形坑

Java 接入 Redis,大部分项目用的是 Spring Data Redis,底层默认客户端是 Lettuce。实习生写代码最容易出现的问题,不在 Redis 端,而在客户端封装和序列化环节。

6.1 序列化器决定你能不能用肉眼读懂缓存

Spring Data Redis 的 RedisTemplate 默认使用 JdkSerializationRedisSerializer,直接把对象序列化成二进制。问题在于:

  • 存进去的是二进制乱码,redis-cli 里看不出来。
  • Redis 里存的 key 会带一串反序列化前缀,导致用 StringRedisTemplate 去 get 时查不到。
  • 对象结构变更后反序列化容易报错。

工程里最普遍的做法是统一使用 JSON 序列化。推荐配置用 GenericJackson2JsonRedisSerializer,或者更精细化的 Fastjson2RedisSerializer / Jackson 自定义序列化器。

@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setKeySerializer(RedisSerializer.string()); template.setValueSerializer(serializer); template.setHashKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(serializer); return template; }

换序列化器最麻烦的是老数据兼容问题。上线前要先评估存量 key 的可读性,必要时通过临时脚本把数据迁移一轮。除非是全新项目,不然不要轻易切换序列化方式。

6.2 RedisTemplate 和 StringRedisTemplate 别混用

StringRedisTemplate 的序列化器默认是 String,存进去的是纯字符串。RedisTemplate 如果没配置,存进去是二进制。同一个 key,用 StringRedisTemplate set,再用 RedisTemplate get,会直接 miss。

实习生经常在同一个项目里手滑混用两个模板。排查起来很费劲,因为代码里看起来都是 set 和 get。我的建议是:项目基准统一用 StringRedisTemplate 存字符串,特殊对象序列化为 JSON 字符串后也走 StringRedisTemplate;RedisTemplate 只在需要 HashOperations 等类型化操作时用。

6.3 Lettuce 连接池和超时问题

Lettuce 默认是基于 Netty 的,连接数可以很少,但它不默认开启连接池。如果项目里用 Lettuce 默认配置,高并发下可能出现连接创建频繁、命令超时。常见的报错:

Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException

这类报错,在连接池未生效时最容易出现。需要在配置文件里显式开启连接池:

spring: data: redis: timeout: 3s lettuce: pool: enabled: true max-active: 16 max-idle: 8 min-idle: 4 max-wait: 3s

配对要记清楚:max-active 给高并发系统预留多少连接,max-wait 是连接池耗尽时调用方的等待时间。max-wait 设太短会直接抛异常,设太长又拖慢请求。一般 3 到 5 秒是常见取值。

Lettuce 还有一个特性:连接是不可变共享的,底层命令通过不同 channel 并发发送。这带来吞吐上的好处,但也造成排障时不容易直观对应连接。所以出现超时问题时,不要只盯着连接数,先看慢查询日志和大 key。

7. 实习期必做的 Redis 自查清单:慢查询、大 Key、热 Key

最后这部分,是带实习生时我最常给的一句话:项目里 Redis 出问题,百分之八十不是命令用错了,而是数据形态有问题。慢查询、大 key、热 key,这三样排查清楚,大部分线上问题都能找到源头。

7.1 慢查询日志怎么看

Redis 会把超过慢查询阈值的命令记录到日志,默认阈值 10000 微秒,生产环境可以调到 1000 微秒,也就是 1 毫秒。

CONFIG SET slowlog-log-slower-than 1000 CONFIG SET slowlog-max-len 128 SLOWLOG GET 20

看到慢查询后,不用急着背命令参数,先判断命令本身是不是 O(N) 操作。像 KEYS、HGETALL、LRANGE 一个大范围的 List,都有可能是慢查询根源。KEYS 在线上要替换成 SCAN;LRANGE biglist 0 -1 就是典型的坑。

7.2 大 Key 的危害和定位

Redis 里单个 key 的 value 过大,比如一个 Hash 有几万字段,或者一个 Set 有几百万成员,会带来三个问题:

  • 删除或过期时,主线程需要释放大量内存,阻塞其他命令。
  • 读写操作整体耗时飙升。
  • 网络传输占用带宽。

定位大 key 用 redis-cli 的 bigkeys 扫描:

redis-cli --bigkeys

它会统计每种类型里最大的 key 和最大的元素。要注意,bigkeys 是逐个 key 扫描,线上注意低峰执行。

处理方式就几种:压缩对象、拆分成多个 key、Hash 改成多个小 Hash、大 Set 拆成多个子 Set。核心思路就是不要让一个 key 承担过多数据。

7.3 热 Key:单点高并发导致的“假死”

某个 key 的访问频率特别高,比如热点商品的缓存 key,单台 Redis 实例可能都无法支撑,出现延迟飘高或超时。常见的解决思路:

  • 本地缓存放一层:Caffeine 缓存热点数据,Redis 只作为兜底。
  • 多副本复制:同一个 key 散列到多个节点,读写时随机选择一个副本。
  • 热点 key 拆分:把同一个 key 变成 key_1 到 key_N,每个副本只承载一部分流量。

排查热 key 可以用 redis-cli 的 hotkeys 参数,前提是开启了 LFU 淘汰策略(maxmemory-policy 为 allkeys-lfu 或 volatile-lfu)。

7.4 上线前过一遍这份清单

我让每个实习生在做完 Redis 相关功能后,对照下面几项自查,通过之后才允许提测:

  • 缓存 key 是否带业务模块前缀,例如user:info:{userId},避免跟其他模块冲突。
  • 有没有设置过期时间,过期时间是否加了随机值。
  • 有没有在循环里逐条 set/get 同一类 key,能否改成 Pipeline。
  • 写接口有没有考虑缓存更新策略,是基于更新的主动写,还是过期后的被动回填。
  • 锁是否用到 Redisson,还是手写的 SETNX 方案;释放锁时是否校验持有者。
  • 线上数据是否经过序列化后仍能在 redis-cli 里读懂。

这份清单我用了三年,每次 review 都能筛出一堆低级问题。实习生如果能主动照着自查,成长速度会明显快过别人。毕竟工程能力这种东西,不是背会几个命令就有的,而是在一次次排查和反思里堆出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 20:28:11

隔离内网AI Agent实战:从模型部署、知识检索到多Agent协作

1. 隔离内网先解决“模型从哪来”&#xff0c;连带解决“知识从哪进”先说一个多数人容易误判的点&#xff1a;在隔离内网里做 AI Agent&#xff0c;最先卡住你的往往不是大模型有多聪明&#xff0c;而是模型根本进不来、数据也出不去。外网环境下我们习惯的方式——直接调云端…

作者头像 李华
网站建设 2026/10/8 20:27:20

多Agent并行编程的状态盲区与探活实战方法

同时开五个AI编程Agent&#xff0c;听起来很有效率&#xff0c;但大多数时候&#xff0c;你盯着满屏滚动的终端日志&#xff0c;心里的焦虑反而更重&#xff1a;到底哪个干完了在等我拍板&#xff1f;哪个还在闷头改代码&#xff1f;又有哪个其实早就卡死、只是在疯狂刷日志营造…

作者头像 李华
网站建设 2026/10/8 20:26:41

Spring Boot医院管理系统实战:从数据库设计到部署上线全解析

做了几年的医疗信息化项目&#xff0c;大大小小的医院管理系统也经手过好几套。从早期用JSPServlet手撸的笨重老系统&#xff0c;到后来基于Spring Boot的轻量级微服务架构&#xff0c;中间踩过的坑、重构掉的代码&#xff0c;说多了都是泪。今天这篇没什么高大上的概念&#x…

作者头像 李华
网站建设 2026/10/8 20:23:30

Mac mini 搭建家庭本地 AI 工作流:Ollama + n8n + SSH 实战

1. 项目概述&#xff1a;为什么一台 Mac mini 能撑起整个家庭 AI 工作流&#xff1f;Mac mini 不是玩具&#xff0c;更不是摆设。它是一台被严重低估的、静音、低功耗、全金属机身的“准服务器级”计算终端——尤其当它搭载 Apple M 系列芯片后&#xff0c;其能效比、内存带宽、…

作者头像 李华
网站建设 2026/10/8 20:21:30

Dbsyncer MySQL全量增量同步实战与踩坑记录

如果你手上管着好几套 MySQL&#xff0c;隔三差五要把一张表从生产库同步到分析库、从主库复制到从库、或者在不同环境之间做数据迁移&#xff0c;那你大概率经历过手写脚本的痛苦。Dbsyncer 这个开源数据同步中间件&#xff0c;我之前也只是在 Gitee 上刷到过&#xff0c;后来…

作者头像 李华