Building a Scalable Follow Service with MySQL, Redis Sets, and Spring Boot
This article walks through the design and implementation of a friend‑follow microservice using MySQL for persistence, Redis Sets for fast set operations, and Spring Boot, covering requirement analysis, data modeling, dependency setup, configuration, service logic, and testing with concrete code examples and performance insights.
Requirement Analysis
The follow feature is a core social function that includes follow/unfollow, retrieving a user's followers, following list, mutual follows, and related queries. While a relational database can store simple follower lists, computing intersections (e.g., mutual follows) becomes inefficient at scale. Redis provides native set operations (intersection, union, difference) that make these queries fast and simple.
Design Idea
The solution combines MySQL and Redis. MySQL stores the durable follow records, while Redis Sets handle fast set operations. Each user maintains two sets: one for the IDs they follow and another for the IDs of their followers.
SADD – add a member to a set (follow)
SREM – remove a member from a set (unfollow)
SCARD – count members in a set (followers/following count)
SISMEMBER – check membership (is already followed?)
SMEMBERS – retrieve all members (list followers/following)
SINTER – compute intersection (mutual follows)
Database Table Design
The table t_follow records the relationship between a user and the user they follow, plus a validity flag.
CREATE TABLE `t_follow` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT '当前登录用户的id',
`follow_user_id` int(11) DEFAULT NULL COMMENT '当前登录用户关注的用户的id',
`is_valid` tinyint(1) DEFAULT NULL COMMENT '关注状态,0-没有关注,1-关注了',
`create_date` datetime DEFAULT NULL,
`update_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户和用户关注表';Creating the Follow Microservice
Dependencies and Configuration
The Maven pom.xml includes Spring Cloud Eureka client, Spring Web, MySQL connector, Spring Data Redis, MyBatis, and Swagger.
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<parent>
<artifactId>redis-seckill</artifactId>
<groupId>com.zjq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>ms-follow</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.battcn</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>Spring Boot configuration (application.yml) defines the service port, MySQL datasource, Redis connection, Swagger base package, and Eureka registration.
server:
port: 7004
spring:
application:
name: ms-follow
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
redis:
host: localhost
port: 6379
password: 123456
database: 2
swagger:
base-package: com.zjq.follow
title: 好用功能微服务API接口文档
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/Redis Template Configuration
A custom RedisTemplate serializes keys as strings and values as JSON using Jackson.
package com.zjq.seckill.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfiguration {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}Follow/Unfollow Implementation
Service Layer
The FollowService handles the business logic:
Validate input parameters.
Retrieve the logged‑in user via an OAuth call.
Check existing follow records in MySQL.
Depending on the current state and requested action, insert, update, or delete the record.
Synchronize Redis sets using addToRedisSet or removeFromRedisSet to keep the in‑memory view consistent.
public ResultInfo follow(Integer followUserId, int isFollowed, String accessToken, String path) {
AssertUtil.isTrue(followUserId == null || followUserId < 1, "请选择要关注的人");
SignInUserInfo dinerInfo = loadSignInDinerInfo(accessToken);
Follow follow = followMapper.selectFollow(dinerInfo.getId(), followUserId);
if (follow == null && isFollowed == 1) {
int count = followMapper.save(dinerInfo.getId(), followUserId);
if (count == 1) addToRedisSet(dinerInfo.getId(), followUserId);
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "关注成功", path, "关注成功");
}
if (follow != null && follow.getIsValid() == 1 && isFollowed == 0) {
int count = followMapper.update(follow.getId(), isFollowed);
if (count == 1) removeFromRedisSet(dinerInfo.getId(), followUserId);
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "成功取关", path, "成功取关");
}
if (follow != null && follow.getIsValid() == 0 && isFollowed == 1) {
int count = followMapper.update(follow.getId(), isFollowed);
if (count == 1) addToRedisSet(dinerInfo.getId(), followUserId);
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "关注成功", path, "关注成功");
}
return ResultInfoUtil.buildSuccess(path, "操作成功");
}
private void addToRedisSet(Integer dinerId, Integer followUserId) {
redisTemplate.opsForSet().add(RedisKeyConstant.following.getKey() + dinerId, followUserId);
redisTemplate.opsForSet().add(RedisKeyConstant.followers.getKey() + followUserId, dinerId);
}
private void removeFromRedisSet(Integer dinerId, Integer followUserId) {
redisTemplate.opsForSet().remove(RedisKeyConstant.following.getKey() + dinerId, followUserId);
redisTemplate.opsForSet().remove(RedisKeyConstant.followers.getKey() + followUserId, dinerId);
}Controller Layer
The REST endpoint maps POST /{followUserId} to the service method.
@RestController
public class FollowController {
@Resource private FollowService followService;
@Resource private HttpServletRequest request;
@PostMapping("/{followUserId}")
public ResultInfo follow(@PathVariable Integer followUserId,
@RequestParam int isFollowed,
String access_token) {
return followService.follow(followUserId, isFollowed, access_token, request.getServletPath());
}
}Mutual Follow List
To obtain users that both the logged‑in user and another user follow, the service reads the two Redis sets and computes their intersection.
@Transactional(rollbackFor = Exception.class)
public ResultInfo findCommonsFriends(Integer userId, String accessToken, String path) {
AssertUtil.isTrue(userId == null || userId < 1, "请选择要查看的人");
SignInUserInfo userInfo = loadSignInuserInfo(accessToken);
String loginKey = RedisKeyConstant.following.getKey() + userInfo.getId();
String targetKey = RedisKeyConstant.following.getKey() + userId;
Set<Integer> userIds = redisTemplate.opsForSet().intersect(loginKey, targetKey);
if (userIds == null || userIds.isEmpty()) {
return ResultInfoUtil.buildSuccess(path, new ArrayList<ShortUserInfo>());
}
ResultInfo resultInfo = restTemplate.getForObject(usersServerName + "findByIds?access_token={accessToken}&ids={ids}",
ResultInfo.class, accessToken, String.join(",", userIds));
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
List<LinkedHashMap> maps = (ArrayList) resultInfo.getData();
List<ShortUserInfo> infos = maps.stream()
.map(m -> BeanUtil.fillBeanWithMap(m, new ShortUserInfo(), true))
.collect(Collectors.toList());
return ResultInfoUtil.buildSuccess(path, infos);
}Testing the Service
After starting Eureka, the gateway, the authentication service, and the follow microservice, the following scenarios were verified:
User 5 follows user 1 – Redis shows a following set for user 5 and a followers set for user 1.
User 7 follows user 1 – both users now share a common follow (user 1).
Additional follows (user 5 → 3, users 5/6/7 → 2) were added, and Redis sets reflected the updates.
Mutual follow queries returned expected results: users 5 and 7 share follows {1,2}; users 6 and 7 share follow {2}.
The verification confirms that the combination of MySQL for persistence and Redis Sets for set operations provides a performant and straightforward solution for follow‑related features.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
