在数据库业务场景中,COUNT(*)是高频使用的统计操作,当数据量增长到千万级甚至亿级时,直接对主表执行COUNT(*)查询会触发全表扫描或大量索引遍历,导致查询耗时从几十毫秒飙升到数秒,严重影响接口响应效率。针对这个问题,结合辅助表统计与Redis缓存的方案可以从根源上降低统计查询的耗时。

方案核心设计思路
整个方案的核心是将实时统计转换为预计算+缓存读取的模式,主要分为两个环节:
- 辅助表统计:维护一张独立的统计辅助表,记录主表的核心统计维度数据,当主表发生增删改操作时,同步更新辅助表的统计数据,避免直接查询主表。
- Redis缓存:将辅助表中的热点统计数据同步到Redis中,查询时优先从Redis读取,缓存未命中再查询辅助表,同时回写缓存减少后续查询耗时。
辅助表的设计与维护
首先需要根据业务统计需求设计辅助表,假设我们有一个用户订单表order_info,需要统计不同状态的订单总数,辅助表可以设计为如下结构:
-- 订单统计辅助表 CREATE TABLE `order_count_helper` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_status` tinyint(4) NOT NULL COMMENT '订单状态 1-待支付 2-已支付 3-已取消', `total_count` int(11) NOT NULL DEFAULT 0 COMMENT '对应状态的订单总数', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_status` (`order_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单统计辅助表';
当order_info表发生数据变更时,需要同步更新辅助表的统计数据,以新增订单为例,更新逻辑如下:
-- 新增订单后更新辅助表统计
INSERT INTO order_count_helper (order_status, total_count)
VALUES (#{orderStatus}, 1)
ON DUPLICATE KEY UPDATE total_count = total_count + 1;
如果订单状态发生变更,比如从待支付变为已支付,需要同时减少原状态的计数,增加新状态的计数:
-- 订单状态变更时更新辅助表
UPDATE order_count_helper SET total_count = total_count - 1 WHERE order_status = #{oldStatus};
UPDATE order_count_helper SET total_count = total_count + 1 WHERE order_status = #{newStatus};
Redis缓存的接入实现
辅助表虽然避免了全表扫描,但每次查询仍然需要访问数据库,对于高频的统计查询,还需要引入Redis缓存进一步提升性能。缓存的Key可以设计为order:count:status:{orderStatus}的格式,Value存储对应的统计总数。
查询统计数据的完整流程如下:
- 根据统计维度生成对应的Redis缓存Key
- 尝试从Redis中获取缓存值,如果命中直接返回结果
- 缓存未命中时,查询辅助表获取统计数据
- 将查询结果写入Redis,设置合理的过期时间,比如300秒
- 返回统计结果
以下是Java语言的实现示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.concurrent.TimeUnit;
public class OrderCountService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
// 缓存过期时间 300秒
private static final long CACHE_EXPIRE_SECONDS = 300;
/**
* 获取指定状态的订单总数
* @param orderStatus 订单状态
* @return 订单总数
*/
public Integer getOrderCountByStatus(Integer orderStatus) {
String cacheKey = "order:count:status:" + orderStatus;
// 1. 先查Redis缓存
Integer count = redisTemplate.opsForValue().get(cacheKey);
if (count != null) {
return count;
}
// 2. 缓存未命中,查询辅助表
String sql = "SELECT total_count FROM order_count_helper WHERE order_status = ?";
count = jdbcTemplate.queryForObject(sql, new Object[]{orderStatus}, Integer.class);
if (count == null) {
count = 0;
}
// 3. 写入Redis缓存
redisTemplate.opsForValue().set(cacheKey, count, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
return count;
}
}
缓存与辅助表的一致性保障
为了保证缓存数据和辅助表数据的一致性,需要在辅助表数据更新时同步更新Redis缓存,避免脏数据问题。可以在数据变更的事务中,同时删除对应的Redis缓存,下次查询时自动加载最新数据:
/**
* 新增订单时更新统计数据和缓存
* @param orderStatus 订单状态
*/
public void addOrderAndUpdateCount(Integer orderStatus) {
// 1. 插入订单主表逻辑省略
// 2. 更新辅助表统计
String updateHelperSql = "INSERT INTO order_count_helper (order_status, total_count) VALUES (?, 1) ON DUPLICATE KEY UPDATE total_count = total_count + 1";
jdbcTemplate.update(updateHelperSql, orderStatus);
// 3. 删除对应缓存,下次查询自动加载最新数据
String cacheKey = "order:count:status:" + orderStatus;
redisTemplate.delete(cacheKey);
}
方案适用场景与注意事项
该方案适合以下场景:
- 统计维度相对固定,不需要频繁新增统计维度
- 统计查询的频率远高于数据变更的频率,缓存命中率较高
- 可以接受短时间内的统计数据延迟,比如缓存过期时间内的数据不是绝对实时
需要注意的问题:
- 辅助表的更新需要和主表操作在同一个事务中,避免数据不一致
- 缓存过期时间需要根据业务对数据实时性的要求设置,实时性要求高可以设置较短的过期时间,反之可以设置更长
- 如果统计维度较多,辅助表的设计需要提前规划,避免后续扩展成本过高
性能对比
我们可以通过简单的测试对比优化前后的查询耗时:
| 场景 | 数据量 | 优化前COUNT(*)耗时 | 优化后查询耗时 |
|---|---|---|---|
| 单状态订单统计 | 1000万 | 1200ms | 5ms(缓存命中) |
| 单状态订单统计 | 1000万 | 1200ms | 20ms(缓存未命中查辅助表) |
从测试结果可以看到,优化后的查询效率提升非常明显,尤其是缓存命中的场景下,耗时从秒级降到了毫秒级,完全可以满足高并发业务的需求。