不能直接用@Async更新数据库,因为异步线程不参与主事务,主方法回滚时异步任务仍执行,导致缓存与DB不一致;且默认SimpleAsyncTaskExecutor无队列、无复用、易OOM。

为什么不能直接用 @Async 更新数据库?
很多人一上来就给更新浏览量的方法加 @Async,结果发现数据库没更新、Redis里数据却涨了——根本原因是:异步线程默认不参与主事务,如果主方法抛异常回滚,异步任务照常执行,造成缓存和DB不一致。更隐蔽的问题是,@Async 默认使用简单线程池(SimpleAsyncTaskExecutor),无队列、无复用、不设限,高并发下容易打爆内存或触发OOM。
RedisTemplate.increment 必须用 Long 类型的 key
文章浏览量本质是计数器,必须用 Redis 的原子递增命令,否则并发写会丢数据。但 redisTemplate.opsForValue().increment("article:1001:view") 要求 key 对应的 value 在 Redis 中必须是字符串形式的整数(如 "123"),不能是 JSON 或空值。如果初始化时没设初值,第一次调用会报 ERR value is not an integer or out of range。
- 启动时用
CommandLineRunner预热:遍历文章表,对每个article:{id}:view执行redisTemplate.opsForValue().setIfAbsent(key, "0") - 递增前先检查是否存在:
if (!redisTemplate.hasKey(key)) { redisTemplate.opsForValue().set(key, "0"); } - 别用
StringRedisTemplate直接get后转Integer.parseInt再set,这不是原子操作
定时同步任务要防重复执行和漏同步
用 @Scheduled(cron = "0 0/10 * * * ?") 每10分钟刷一次,看似简单,但在集群部署下,多个实例会同时触发,导致同一批数据被多次写入数据库,甚至引发主键冲突或重复累加。另外,如果某次定时任务因网络或DB故障失败,下次执行只会覆盖最新值,中间10分钟的增量就丢了。
- 加分布式锁:用
redisTemplate.opsForValue().setIfAbsent("sync:article:view:lock", "1", 15, TimeUnit.MINUTES),成功才执行同步逻辑 - 同步时用 pipeline 批量读取 + 批量 upsert:避免单条 select + update 的 N+1 查询
- 记录最后同步时间戳到 Redis(如
last_sync_time),失败时从该时间点重试,而不是只刷当前快照
别忽略 Redis 过期策略和 DB 主键冲突
浏览量 key 通常不设过期时间,但如果业务要求“只统计最近7天浏览量”,就得用带 TTL 的 key,比如 redisTemplate.opsForValue().set(key, "0", 7, TimeUnit.DAYS)。这时候要注意:key 过期后,increment 会重新创建 key 并从 0 开始计,但旧数据已丢失,无法补全。
更关键的是 DB 同步环节:如果数据库表用自增主键,而 Redis key 是 article:1001:view,同步时得确保目标表有 article_id 字段做唯一索引,否则批量 upsert 可能因主键冲突失败。推荐用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 INSERT ... ON CONFLICT DO UPDATE(PostgreSQL)语句,而不是先查再插。
真正难的不是写代码,而是想清楚:哪部分必须强一致(比如用户点赞)、哪部分允许短暂不一致(比如文章浏览量)、以及当 Redis 宕机时,是否降级为直写 DB。这些决策点,比选哪个序列化器重要得多。











