当你的社交平台面临百万用户实时互动,如何确保关注操作毫秒级响应?如何保证粉丝列表的实时性和一致性? 这个看似基础的功能背后,隐藏着读写扩散、数据一致性、热点用户等架构难题。本文将带你从业务模型到代码落地,构建一个支撑千万级关系的社交系统。
典型关注业务流程:
高并发场景下的核心挑战:
@Service
public class FollowService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private static final String FOLLOWING_KEY = "following:%d"; // 用户关注集合
private static final String FOLLOWERS_KEY = "followers:%d"; // 粉丝集合
private static final String FOLLOW_COUNT = "follow_count:%d"; // 计数Key
/**
* 关注操作(原子性保证)
* @param userId 操作者ID
* @param targetId 被关注用户ID
* @return 是否成功
*/
public boolean followUser(long userId, long targetId) {
// 1. 检查是否已关注
String followingKey = String.format(FOLLOWING_KEY, userId);
if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(followingKey, String.valueOf(targetId)))) {
throw new BusinessException("已关注该用户");
}
// 2. 使用Lua脚本保证原子操作
String luaScript =
"local followingKey = KEYS[1] " +
"local followersKey = KEYS[2] " +
"local userId = ARGV[1] " +
"local targetId = ARGV[2] " +
// 添加关注关系
"redis.call('sadd', followingKey, targetId) " +
"redis.call('sadd', followersKey, userId) " +
// 更新计数
"redis.call('hincrby', KEYS[3], 'following', 1) " +
"redis.call('hincrby', KEYS[4], 'followers', 1) " +
"return 1";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
String followersCountKey = String.format(FOLLOW_COUNT, targetId);
String followingCountKey = String.format(FOLLOW_COUNT, userId);
redisTemplate.execute(
script,
Arrays.asList(
followingKey,
String.format(FOLLOWERS_KEY, targetId),
followingCountKey,
followersCountKey
),
String.valueOf(userId),
String.valueOf(targetId)
);
// 3. 异步持久化到数据库
kafkaTemplate.send("follow-events",
new FollowEvent(userId, targetId, System.currentTimeMillis()).toJson()
);
// 4. 实时推送新粉丝通知
pushNewFollowerNotification(targetId, userId);
return true;
}
// WebSocket实时推送
private void pushNewFollowerNotification(long targetId, long followerId) {
String channel = "user:" + targetId + ":followers";
redisTemplate.convertAndSend(channel, String.valueOf(followerId));
}
}
@Service
public class FollowerQueryService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ElasticsearchRestTemplate esTemplate;
/**
* 获取粉丝列表(带分页和缓存)
* @param userId 用户ID
* @param page 页码
* @param size 每页大小
* @return 粉丝ID列表
*/
public List<Long> getFollowers(long userId, int page, int size) {
String redisKey = String.format("followers:%d", userId);
// 1. 尝试从Redis获取
Set<String> followers = redisTemplate.opsForZSet().reverseRange(
redisKey, page * size, (page + 1) * size - 1
);
if (followers != null && !followers.isEmpty()) {
return followers.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
}
// 2. Redis未命中,查询ES(冷数据)
return queryFollowersFromES(userId, page, size);
}
// ES分页查询(用于历史数据)
private List<Long> queryFollowersFromES(long userId, int page, int size) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("targetId", userId))
.withPageable(PageRequest.of(page, size))
.withSort(SortBuilders.fieldSort("followTime").order(SortOrder.DESC))
.build();
SearchHits<FollowerDoc> hits = esTemplate.search(query, FollowerDoc.class);
return hits.getSearchHits().stream()
.map(hit -> hit.getContent().getFollowerId())
.collect(Collectors.toList());
}
}
@Service
public class CountService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取粉丝数(优先缓存)
* @param userId 用户ID
* @return 粉丝数量
*/
public long getFollowerCount(long userId) {
String countKey = String.format("follow_count:%d", userId);
String count = redisTemplate.opsForHash().get(countKey, "followers");
if (count != null) {
return Long.parseLong(count);
}
// 缓存未命中,从DB加载
long dbCount = jdbcTemplate.queryForObject(
"SELECT follower_count FROM user_stats WHERE user_id = ?",
Long.class, userId
);
// 回填缓存
redisTemplate.opsForHash().put(countKey, "followers", String.valueOf(dbCount));
return dbCount;
}
/**
* 异步更新数据库计数
*/
@KafkaListener(topics = "count-updates")
public void updateCount(ConsumerRecord<String, String> record) {
CountUpdateEvent event = CountUpdateEvent.fromJson(record.value());
jdbcTemplate.update(
"INSERT INTO user_stats (user_id, follower_count, following_count) " +
"VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE " +
"follower_count = follower_count + ?, " +
"following_count = following_count + ?",
event.getUserId(),
event.getFollowerDelta(), event.getFollowingDelta(),
event.getFollowerDelta(), event.getFollowingDelta()
);
}
}
@Service
public class RelationGraphService {
@Autowired
private Neo4jTemplate neo4jTemplate;
/**
* 查询共同关注(二度关系)
* @param userId1 用户A
* @param userId2 用户B
* @return 共同关注的用户列表
*/
public List<Long> findMutualFollows(long userId1, long userId2) {
String query = "MATCH (u1:User {id: $id1})-[:FOLLOWS]->(common:User)<-[:FOLLOWS]-(u2:User {id: $id2}) " +
"RETURN common.id";
Map<String, Object> params = Map.of("id1", userId1, "id2", userId2);
return neo4jTemplate.findAll(query, params, Long.class);
}
/**
* 推荐可能认识的人(三度关系)
* @param userId 当前用户
* @return 推荐用户列表
*/
public List<Long> recommendUsers(long userId) {
String query = "MATCH (me:User {id: $userId})-[:FOLLOWS*2..3]->(potential:User) " +
"WHERE NOT (me)-[:FOLLOWS]->(potential) " +
"RETURN potential.id, COUNT(*) AS commonConnections " +
"ORDER BY commonConnections DESC LIMIT 10";
return neo4jTemplate.findAll(query, Map.of("userId", userId), Long.class);
}
}
读写分离:
热点用户特殊处理:
// 对百万粉丝账号使用分片存储
String shardKey = "followers:" + userId + ":" + (followerId % 32);
缓存策略:
数据冷热分离:
粉丝列表分页陷阱:
// 错误:ZRANGE不支持跨分片
// 正确:使用ZSCAN+游标分页
Cursor<ZSetOperations.TypedTuple<String>> cursor = redisTemplate.opsForZSet()
.scan(key, ScanOptions.scanOptions().count(100).build());
缓存穿透解决方案:
// 布隆过滤器防止无效用户查询
if (!bloomFilter.mightContain(userId)) {
throw new UserNotFoundException();
}
数据一致性保障:
// 对账任务伪代码
public void reconcileCount(long userId) {
long redisCount = getFollowerCountFromRedis(userId);
long dbCount = getFollowerCountFromDB(userId);
if (redisCount != dbCount) {
logger.warn("计数不一致: userId={}, redis={}, db={}", userId, redisCount, dbCount);
// 自动修复逻辑...
}
}
突发流量应对:
// 降级返回前100粉丝+总数
public FollowerList getFollowersDegraded(long userId) {
return new FollowerList(
redisTemplate.opsForZSet().reverseRange("followers:"+userId, 0, 99),
getFollowerCount(userId)
);
}
设计关注/粉丝系统就像构建一座社交关系的大厦:既要保证地基(数据存储)的稳固可靠,又要让电梯(读写性能)高速运行,还要能随时扩建(水平扩展)。通过本文的读写分离、冷热分层、图数据库等方案,我们成功支撑了千万级用户的实时社交关系维护。
记住三条黄金法则:
架构没有银弹,最好的设计永远是适合业务发展的设计。当你的系统面临下一个量级挑战时,不妨回头看看:当前的瓶颈是否源于昨天的妥协?