高并发统计数据+攻略统计数据持久化和初始化(8.12)
一、高并发统计数字
1、业务分析
(1)为什么使用Redis?
viewnum(阅读量)、replynum(评论量)、favornum(收藏量)、sharenum(分享量)、thumbsupnum(点赞量)这些数据具有典型的高频并发写入特征。
- 访问量大: 任何用户打开页面都会触发读取,点击交互会触发写入。
- 更新频繁: 尤其是阅读量和点赞量,属于典型的写多读多场景。
①MySQL 的局限性
- 效率低下:基于磁盘存储,I/O 开销大,高并发写入时响应显著变慢。
- 锁竞争严重:为保证数据一致性需加锁,高并发下易引发严重的锁等待与阻塞。
- 扩展性差:单表更新压力过大,难以支撑海量并发。
② Redis 的核心优势
- 原子性操作:单线程模型保证
INCR等自增命令天然原子,无脏数据且无需复杂锁机制。 - 高性能:纯内存存储,读写达微秒级,轻松应对每秒数十万并发写入。
- 减轻主库压力:承载高频计数,通过异步同步回 MySQL 持久化,保护核心业务库稳定。
(2)Redis数据怎么存储
方式一:String类型存储(独立Key模式)Map<String, String>
将对象的每个属性拆解为独立的 Redis Key 进行存储。
viewnum:117 -> "200"
replynum:117 -> "3"
favornum:117 -> "10"
sharenum:117 -> "1"
thumbsup:117 -> "34"
优点:直白容易理解
缺点:key过多,浪费空间,拓展性差
方式二:String 类型存储(JSON序列化模式)Map<String, Object>
将整个对象序列化为 JSON 字符串,作为一个整体 Value 存储在单个 Key 中。
strategy_num:117 "(viewnum:200, replynum:3, favornum:10, sharenum:1, thumbsup:34)"
优点:解决key过多的问题,查询很快
缺点:修改麻烦并且有并发问题
修改步骤:
①读取缓存获取数据
②将数据转换成java对象
③将java对象中的viewnum + 1
④将java对象转化为json串
⑤将json串存储到redis中
并发情况模拟:
假设缓存中初始数据为 0,此时两人(客户端 A 和客户端 B)同时发起操作:
- 客户端 A 读取缓存数据,获取到
0。 - 此时 客户端 B 也能正常读取缓存数据,同样获取到
0(因为 A 还在处理中,尚未写回)。 - 两人都在各自的内存中将数据
+1变为1。 - 客户端 A 将结果
1写回 Redis。 - 客户端 B 随后也将结果
1写回 Redis,直接覆盖了 A 的数据。
结果: 两次操作后,最终结果依然是 1 而不是预期的 2。这种模式在并发量小时只是修改麻烦,但在并发量大时,极易导致数据丢失。
方式三:Hash 类型存储(Hash Map 模式)Map<String, Map<String, String>>
KEY: strategy_static_hash:117
-----------------------
viewnum -> 200
replynum -> 3
favornum -> 10
sharenum -> 1
thumbsupnum-> 34
优点:单独操作某个小key的值,是redis的原子操作,解决并发问题,不会丢数据;解决key过多问题
缺点:需要自己封装查询方法,实现遍历查询
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 互联网平台 | 方式三 (Hash) | 高并发场景下,点赞、浏览数更新频繁,必须保证数据准确性(原子性),且需要频繁单独更新某个字段。 |
| 后台管理系统 | 方式二 (String JSON) | 数据基本不改变,或者只有管理员偶尔修改,不存在高并发写入冲突。此时方式二读取性能最好,开发最简单。 |
| 简单的计数器 / 临时状态 | 方式一 (String) | 仅当只需要关注某一个指标,或者指标之间完全无关联时使用。 |
2、代码实现阅读量++和评论++
将高频的统计自增操作与页面数据查询接口(Controller)分离,独立发送请求,主要基于以下两点考量:
- 读写解耦,保障性能:避免高频的统计写入操作占用过多资源,防止底层服务(Redis/MySQL)被拖垮,从而保障页面数据查询接口的稳定与高效。
- 支持页面静态化:为未来将页面转为静态资源(如接入 CDN)预留空间。若将统计操作耦合在查询接口中,页面静态化后接口不再被访问,统计功能将随之失效。
参数是攻略id,返回格式是Map
viewnum:117 -> 200
replynum:117 -> 3
favornum:117 -> 10
sharenum:117 -> 1
thumbsup:117 -> 34
sid -> 1 后期数据持久化,要将上面数据存到MySql中去
@PostMapping("/viewnumIncr")
public R<Map<String, Object>> viewnumIncr(Long sid){
Map<String, Object> map = strategyService.viewnumIncr(sid);
return R.ok(map);
}
/**
* 获取 key,viewnum + 1,再获取到最新的缓存,返回缓存
*/
@Override
public Map<String, Object> viewnumIncr(Long sid) {
String key = strategyHashInit(sid);
redisService.incrementCacheMapValue(key, "viewnum",1);
Map<String, Object> cacheMap = redisService.getCacheMap(key);
return cacheMap;
}
@Override
public Map<String, Object> replynumIncr(Long sid) {
String key = strategyHashInit(sid);
redisService.incrementCacheMapValue(key, "replynum",1);
Map<String, Object> cacheMap = redisService.getCacheMap(key);
return cacheMap;
}
-------------------------------------------------------------------------------------------------
@Getter
public enum RedisKeys {
STRATEGY_STATIS_HASH("strategy_statis_hash", -1),
// 验证码
VERIFY_CODE("verify_code", 60 * 3);
...
}
--------------------------------------------------------------------------------------------------
/**
* 初始化策略缓存
* 拼接key,如果key不存在,就创建缓存,否则返回缓存key
*/
private String strategyHashInit(Long sid) {
String key = RedisKeys.STRATEGY_STATIS_HASH.join(sid.toString());
if(!redisService.hasKey(key)){
Strategy strategy = baseMapper.selectById(sid);
Map<String,Object> map = new HashMap<>();
map.put("viewnum", strategy.getViewnum().intValue());
map.put("replynum", strategy.getReplynum().intValue());
map.put("favornum", strategy.getFavornum().intValue());
map.put("sharenum", strategy.getSharenum().intValue());
map.put("thumbsupnum", strategy.getThumbsupnum().intValue());
// 后期持久化
map.put("id", strategy.getId());
redisService.setCacheMap(key, map);
}
return key;
}
注意:数据同步,Redis缓存中的数据要与MySql表中的数据一致。
二、攻略统计数据持久化
上面我们已经把viewnum(阅读量)、replynum(评论量)存储到Redis缓存中了,现在我们要考虑维护MySql表,定期写回给MySql。

先写一个定时任务,再远程调用Feign。Feign接口中定义方法,写降级。
@Component("strategyTask")
public class StrategyTask {
@Autowired
private RemoteStrategyService remoteStrategyService;
// 统计数据持久化任务
public void statisHashPersistence(){
remoteStrategyService.statisHashPersistence(SecurityConstants.INNER);
}
}
-----------------------------------------------------------------------------------------------
@PostMapping("/strategies/feign/statisHashPersistence")
R<?> statisHashPersistence(@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
------------------------------------------------------------------------------------------------
/**
* 攻略 远程服务降级回调
*
* @author y
* @date 2026-08-06
*/
@Component
public class RemoteStrategyFallbackFactory implements FallbackFactory<RemoteStrategyService> {
private static final Logger log = LoggerFactory.getLogger(RemoteStrategyFallbackFactory.class);
@Override
public RemoteStrategyService create(Throwable throwable) {
log.error("攻略服务调用失败:{}", throwable.getMessage());
return new RemoteStrategyService() {
@Override
public R<?> statisHashPersistence(String source) {
return R.fail(Lists.newArrayList(), "统计数据持久化失败:" + throwable.getMessage());
}
};
}
}
@PostMapping("/feign/statisHashPersistence")
public R<?> statisHashPersistence(){
strategyService.statisHashPersistence();
return R.ok();
}
---------------------------------------------------------------------------------------------------
/**
* 先拼接前缀,再获取所有key,最后遍历key,获取缓存,更新redis数据库
*/
@Override
public void statisHashPersistence() {
String key = RedisKeys.STRATEGY_STATIS_HASH.join("*");
Collection<String> keys = redisService.keys(key);
if (keys != null && keys.size()>0){
for (String k : keys) {
Map<String, Object> map = redisService.getCacheMap(k);
Long id = (Long) map.get("id");
Integer viewnum = (Integer) map.get("viewnum");
Integer replynum = (Integer) map.get("replynum");
Integer sharenum = (Integer) map.get("sharenum");
Integer thumbsupnum = (Integer) map.get("thumbsupnum");
Integer favornum = (Integer) map.get("favornum");
lambdaUpdate().eq(Strategy::getId,id)
.set(Strategy::getViewnum,viewnum)
.set(Strategy::getReplynum,replynum)
.set(Strategy::getSharenum,sharenum)
.set(Strategy::getThumbsupnum,thumbsupnum)
.set(Strategy::getFavornum,favornum)
.update();
}
}
}
三、攻略统计数据初始化
初始化就是把MySql里的数据预热到Redis中,正常是拿MySql近期的数据,不可能拿所有数据到Redis中。方法是根据create_time把当前天数减N天,大于这个时间的数据放到Redis中。本次项目中是拿所有数据。
1、初始化时机:服务器第一次启动时
2、方法:使用监听器
3、Spring框架中有哪些不同类型的事件?
Spring 提供了以下5种标准的事件:
(1)上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
(2)上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
(3)上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
(4)上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
(5)请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。
如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
4、代码实现
@Component
public class StatisHashInitListener implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private IStrategyService strategyService;
@Autowired
private RedisService redisService;
// 当这个事件到来时
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.err.println("程序初始化完成====");
// 查询所有数据
List<Strategy> strategies = strategyService.list();
for(Strategy strategy:strategies){
// 获取id,拼接key
Long id = strategy.getId();
String key = RedisKeys.STRATEGY_STATIS_HASH.join(id.toString());
// Redis数据是新的,所以如果redis这个key有数据,就不存到Redis中了;否则将数据预热
if(redisService.hasKey(key)){
continue;
}
Map<String,Object> map = new HashMap<>();
map.put("viewnum", strategy.getViewnum().intValue());
map.put("replynum", strategy.getReplynum().intValue());
map.put("favornum", strategy.getFavornum().intValue());
map.put("sharenum", strategy.getSharenum().intValue());
map.put("thumbsupnum", strategy.getThumbsupnum().intValue());
map.put("id", strategy.getId());
redisService.setCacheMap(key, map);
}
}
}
拓展:
Q1:为什么要执行 .intValue()?
答:主要是为了解决 Java 中的类型转换与空指针风险。当 getSharenum() 返回的是 Long 类型或包装类时,需要显式转换为 int 或 Integer 类型以匹配 Map 的泛型要求;同时,若该字段为 null,调用 .intValue() 会触发明确的空指针异常,迫使开发者提前处理空值。
Q2:存入 Redis 默认是 String 吗?
答:是的。Redis 底层所有的 Value 本质上都是二进制安全的 String 类型。但 Redis 具备智能编码优化机制,当检测到存入的字符串是纯数字时,会在内存中将其自动优化为 64位有符号整数(long)存储,以节省内存并提升运算效率;而在对外读取时,又会将其转回字符串返回给客户端。
浙公网安备 33010602011771号