别再被高大上的架构忽悠了!真实业务场景下的技术选型指南
不知道大家在实现功能的时候,有没有考虑过到底应该用Redis还是直接写MySQL?可能刚开始干开发的时候,大家不需要考虑,领导怎么说就怎么干。但是如果想要逐渐成长为独当一面的人才,这些是一定要考虑的,再不济在面试的时候也可以应付一下面试官。
今天我就用大白话,告诉你不同阶段该用什么方案。
一、创业公司:怎么快怎么来
场景: 你们团队5个人,产品刚上线,每天就几百个用户
核心诉求: 快速上线验证业务,别整复杂了
基础数据模型设计
-- 文章表
CREATE TABLE article (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
like_count INT DEFAULT 0, -- 点赞数
favorite_count INT DEFAULT 0, -- 收藏数
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 用户行为表(最简版本)
CREATE TABLE user_actions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
article_id BIGINT NOT NULL,
action_type ENUM('like', 'favorite'), -- 操作类型
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_article (user_id, article_id)
);
// 方案1:最简数据库实现(推荐)
@RestController
public class BasicLikeController {
@PostMapping("/articles/{id}/like")
public ResponseEntity<?> likeArticle(@PathVariable Long id,
@RequestParam Long userId) {
// 1. 检查是否已经点赞
boolean hasLiked = userActionRepository.existsByUserIdAndArticleIdAndActionType(
userId, id, "like");
if (hasLiked) {
return ResponseEntity.badRequest().body("已经点过赞了");
}
// 2. 开启事务
return transactionTemplate.execute(status -> {
// 3. 更新文章点赞数
articleRepository.incrementLikeCount(id);
// 4. 记录用户行为
UserAction action = new UserAction();
action.setUserId(userId);
action.setArticleId(id);
action.setActionType("like");
userActionRepository.save(action);
return ResponseEntity.ok("点赞成功");
});
}
}
为什么选这个方案?
- 开发快:2小时搞定
- 不用学新技术:Java开发都会写SQL
- 好排查:数据都在数据库,查问题方便
- 成本低:不用买Redis,省服务器钱
可能遇到的问题:
- 如果突然有个帖子火了,几千人同时点赞,数据库可能会卡
- 用户多了以后,查询会变慢
解决方案: 先上线,等真遇到问题再说!创业公司最重要的是活下去,不是提前优化。
二、成长型公司:用户破10万了
场景: 你们日活过万,经常有热门内容,数据库开始报警
核心诉求: 保证系统稳定,用户体验要好
// 方案2:Redis缓存 + 异步落库(性价比最高)
@Service
public class LikeServiceV2 {
public void likeArticle(Long articleId, Long userId) {
String userLikeKey = "user_like:" + userId + ":" + articleId;
// 先用Redis判断是否点过赞(比查数据库快100倍)
if (redisTemplate.hasKey(userLikeKey)) {
throw new RuntimeException("您已经点过赞了");
}
// Redis原子操作增加点赞数(不会出现并发问题)
String countKey = "article_like_count:" + articleId;
redisTemplate.opsForValue().increment(countKey);
// 记录用户点赞关系(30天过期,防止内存爆掉)
redisTemplate.opsForValue().set(userLikeKey, "1", 30, TimeUnit.DAYS);
// 异步写到数据库(就算数据库挂了,点赞数还在Redis里)
CompletableFuture.runAsync(() -> {
saveToDatabase(userId, articleId);
});
}
// 获取点赞数:先查Redis,没有再查数据库
public Integer getLikeCount(Long articleId) {
String countKey = "article_like_count:" + articleId;
String count = redisTemplate.opsForValue().get(countKey);
if (count != null) {
return Integer.parseInt(count);
}
// 缓存没有,从数据库查并回填缓存
Integer dbCount = articleRepository.getLikeCount(articleId);
redisTemplate.opsForValue().set(countKey, dbCount.toString(), 1, TimeUnit.HOURS);
return dbCount;
}
}
为什么选这个方案?
- 性能提升:Redis比MySQL快得多
- 用户体验:点赞响应速度从100ms降到10ms
- 成本可控:买个几百块的Redis够用了
- 风险可控:Redis挂了还能降级到数据库
部署要点:
- Redis一定要设置内存淘汰策略
- 记得设置Key过期时间
- 做好Redis监控,内存满了及时报警
三、成熟公司:准备迎接百万用户
场景: 你们已经是细分领域头部,每天百万UV,准备融资打市场
核心诉求: 系统要稳定,能抗住流量冲击,不能宕机
核心数据模型优化
-- 优化后的用户行为表
CREATE TABLE user_actions (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
article_id BIGINT NOT NULL,
action_type TINYINT NOT NULL, -- 1:点赞 2:收藏 3:取消点赞
client_ip INT UNSIGNED, -- 客户端IP(整数存储)
user_agent_hash BINARY(16), -- 用户代理哈希
created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3), -- 毫秒级时间戳
updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
-- 复合主键(用于分库分表)
PRIMARY KEY (user_id, article_id, action_type),
-- 索引设计
INDEX idx_article_action (article_id, action_type, created_at),
INDEX idx_user_action (user_id, action_type, created_at),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(user_id) PARTITIONS 32; -- 分表
// 方案3:多级缓存 + 消息队列 + 容灾降级
@Service
public class LikeServiceV3 {
// 本地缓存(Caffeine),比Redis还快
private Cache<Long, Integer> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
public LikeResult likeArticle(LikeRequest request) {
// 1. 频率限制:防止刷赞
if (!rateLimiter.tryAcquire(request.getUserId())) {
return LikeResult.fail("点得太快了,歇会儿吧");
}
// 2. 防重复提交
String duplicateKey = "req_" + request.getRequestId();
if (!redisLock.tryLock(duplicateKey, 5)) {
return LikeResult.fail("请不要重复提交");
}
try {
// 3. 更新本地缓存
updateLocalCache(request.getArticleId());
// 4. 更新Redis
updateRedisCache(request);
// 5. 发消息到MQ,让下游慢慢处理
kafkaTemplate.send("like_topic", request);
return LikeResult.success("点赞成功");
} finally {
redisLock.unlock(duplicateKey);
}
}
// 读取策略:Cache-Aside
// 获取点赞数:本地缓存 → Redis → 数据库
public Integer getLikeCount(Long articleId) {
// 先查本地缓存(纳秒级)
Integer count = localCache.getIfPresent(articleId);
if (count != null) {
return count;
}
// 再查Redis(毫秒级)
count = getFromRedis(articleId);
if (count != null) {
localCache.put(articleId, count);
return count;
}
// 最后查数据库
count = getFromDatabase(articleId);
if (count != null) {
// 回填缓存
updateCache(articleId, count);
}
return count;
}
}
配套措施:
# 部署架构
前端 → 负载均衡 → 应用集群 → Redis集群 → MySQL集群
↓
监控告警
↓
日志分析
为什么选这个方案?
- 高可用:任何一环挂了都有备用方案
- 高性能:本地缓存让读取速度飞起
- 可扩展:水平扩容很容易
- 有保障:监控告警齐全,出问题能及时发现
四、大厂级别:亿级用户架构
场景: 你是BAT级别的公司,双十一要抗住每秒几十万点赞
核心诉求: 绝对不能宕机,性能要极致,成本要优化
这种场景下,要考虑的问题就多了:
- 热点问题:明星发微博,瞬间百万点赞怎么办?
- 数据分片:数据太大,一个Redis存不下怎么办?
- 跨机房:北京用户点赞,上海用户要立即看到怎么办?
- 成本优化:每天几个亿的点赞,存储成本怎么降?
// 方案4:分布式架构 + 智能路由 + 成本控制
@Service
public class LikeServiceV4 {
public void likeArticle(LikeRequest request) {
// 1. 智能路由:热点数据特殊处理
String routeKey = routeManager.getRouteKey(request);
// 2. 分布式锁:保证原子性
DistributedLock lock = lockFactory.getLock(routeKey);
try {
if (lock.tryLock(1000)) {
// 3. 写入时序数据库(成本低,适合日志类数据)
timeseriesDB.writeLikeRecord(request);
// 4. 更新实时计数(用内存计算,速度快)
realtimeCounter.increment(request.getArticleId());
// 5. 数据同步到各地机房
syncManager.syncToAllDC(request);
}
} finally {
lock.unlock();
}
}
}
部署架构:
用户 → CDN → 网关层 → 业务层 → 缓存层 → 存储层
↓ ↓ ↓ ↓ ↓
边缘计算 限流降级 服务网格 集群分片 冷热分离
五、怎么选?看这张表就够了
| 公司阶段 | 推荐方案 | 开发周期 | 服务器成本 | 维护难度 | 适用场景 |
|---|---|---|---|---|---|
| 初创公司 | 直接数据库 | 1-2天 | 几百/月 | ⭐☆☆☆☆ | 验证想法,用户量小 |
| 成长公司 | Redis缓存 | 3-5天 | 1-2千/月 | ⭐⭐☆☆☆ | 用户过万,追求体验 |
| 成熟公司 | 多级缓存 | 1-2周 | 3-5千/月 | ⭐⭐⭐☆☆ | 准备融资,要稳定性 |
| 大厂 | 分布式架构 | 1-2月 | 几万/月 | ⭐⭐⭐⭐⭐ | 亿级用户,不能宕机 |
六、实际踩坑经验
坑1:缓存雪崩
// 错误做法:所有Key同时过期
redisTemplate.opsForValue().set("count_123", "100", 1, TimeUnit.HOURS);
// 正确做法:过期时间加随机值
int randomExpire = 3600 + new Random().nextInt(600); // 1小时+随机10分钟
redisTemplate.opsForValue().set("count_123", "100", randomExpire, TimeUnit.SECONDS);
坑2:缓存穿透
// 错误做法:查询不存在的数据,每次都击穿到数据库
public Integer getCount(Long articleId) {
String count = redisTemplate.get("count_" + articleId);
if (count == null) {
// 如果文章不存在,这里每次都会查数据库
return articleRepository.getCount(articleId);
}
return Integer.parseInt(count);
}
// 正确做法:缓存空值
public Integer getCount(Long articleId) {
String count = redisTemplate.get("count_" + articleId);
if (count != null) {
if ("NULL".equals(count)) {
return 0; // 缓存了空值
}
return Integer.parseInt(count);
}
Integer dbCount = articleRepository.getCount(articleId);
if (dbCount == null) {
// 数据库也没有,缓存空值,防止继续穿透
redisTemplate.opsForValue().set("count_" + articleId, "NULL", 5, TimeUnit.MINUTES);
return 0;
}
redisTemplate.opsForValue().set("count_" + articleId, dbCount.toString(), 1, TimeUnit.HOURS);
return dbCount;
}
七、总结
技术选型就像买车:
- 创业公司:买辆二手捷达,能跑就行
- 成长公司:买辆新丰田,省心耐用
- 成熟公司:买辆宝马,要有面子要舒适
- 大厂:买车队,还要配维修团队
最好的架构不是最复杂的,而是最适合当前业务阶段的。
记住三句话:
- 别用开飞机的技术来开拖拉机
- 合适的才是最好的
- 先上线,再优化