How to Build a High‑Performance Follow Service with MySQL, Redis, and Spring Boot
This article explains how to design and implement a friend/follow microservice using MySQL for persistence and Redis sets for efficient set operations, covering database schema, Spring Boot configuration, Redis template setup, service and controller logic, and testing of follow, unfollow, and mutual‑follow features.
Requirement Analysis
Friend functionality is essential in social scenarios, including follow/unfollow, viewing followers/following, mutual follows, etc. Simple retrieval of a user's followers can be done with a database, but finding common followers or followings among multiple users is inefficient; Redis sets provide fast intersection, union, and difference operations.
Design Idea
The solution combines MySQL for persistent storage and Redis Sets for set operations. Each user maintains two sets: one for the users they follow and another for their followers.
SADD – add member (follow)
SREM – remove member (unfollow)
SCARD – count members (follow/follower count)
SISMEMBER – check membership (whether followed)
SMEMBERS – retrieve members (list)
SINTER – intersect sets (common follows)
Database Table Design
The table records user id, followed user id, and follow status.
CREATE TABLE `t_follow` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT 'Current logged‑in user id',
`follow_user_id` int(11) DEFAULT NULL COMMENT 'Id of the user being followed',
`is_valid` tinyint(1) DEFAULT NULL COMMENT 'Follow status, 0‑unfollowed, 1‑followed',
`create_date` datetime DEFAULT NULL,
`update_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='User follow table';Create Follow Microservice
Dependencies and Configuration
POM dependencies include Spring Cloud Eureka client, Spring Web, MySQL connector, Spring Data Redis, MyBatis, common utilities, and Swagger.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>redis-seckill</artifactId>
<groupId>com.zjq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ms-follow</artifactId>
<dependencies>
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- spring data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- commons -->
<dependency>
<groupId>com.zjq</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>com.battcn</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>Spring Boot configuration (application.yml) sets server port, datasource, Redis connection, Swagger, Eureka client, and service URLs.
server:
port: 7004 # port
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:
port: 6379
host: localhost
timeout: 3000
password: 123456
database: 2
swagger:
base-package: com.zjq.follow
title: 好用功能微服务API接口文档
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:7000/eureka/
service:
name:
ms-oauth-server: http://ms-oauth2-server/
ms-diners-server: http://ms-users/
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
pattern:
console: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'Redis Configuration Class
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;
/**
* RedisTemplate configuration class
*/
@Configuration
public class RedisTemplateConfiguration {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// use Jackson2JsonRedisSerializer for values
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}Follow/Unfollow Implementation
Business logic in FollowService handles follow, unfollow, and re‑follow operations, updates MySQL via FollowMapper, and synchronizes Redis sets.
package com.zjq.seckill.service;
import cn.hutool.core.bean.BeanUtil;
import com.zjq.commons.constant.ApiConstant;
import com.zjq.commons.constant.RedisKeyConstant;
import com.zjq.commons.exception.ParameterException;
import com.zjq.commons.model.domain.ResultInfo;
import com.zjq.commons.model.pojo.Follow;
import com.zjq.commons.model.vo.SignInUserInfo;
import com.zjq.commons.utils.AssertUtil;
import com.zjq.commons.utils.ResultInfoUtil;
import com.zjq.seckill.mapper.FollowMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
/**
* Follow/Unfollow service layer
*/
@Service
public class FollowService {
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Value("${service.name.ms-diners-server}")
private String dinersServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private FollowMapper followMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* Follow/Unfollow
*
* @param followUserId ID of the user to follow
* @param isFollowed 1 = follow, 0 = unfollow
* @param accessToken login token
* @param path request path
*/
public ResultInfo follow(Integer followUserId, int isFollowed,
String accessToken, String path) {
AssertUtil.isTrue(followUserId == null || followUserId < 1,
"Please select a user to follow");
SignInUserInfo dinerInfo = loadSignInDinerInfo(accessToken);
Follow follow = followMapper.selectFollow(dinerInfo.getId(), followUserId);
// add follow
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,
"Follow successful", path, "Follow successful");
}
// unfollow
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,
"Unfollow successful", path, "Unfollow successful");
}
// re‑follow
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,
"Follow successful", path, "Follow successful");
}
return ResultInfoUtil.buildSuccess(path, "Operation successful");
}
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);
}
private SignInUserInfo loadSignInDinerInfo(String accessToken) {
AssertUtil.mustLogin(accessToken);
String url = oauthServerName + "user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
throw new ParameterException(resultInfo.getMessage());
}
return BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
new SignInUserInfo(), false);
}
}Controller
package com.zjq.seckill.controller;
import com.zjq.commons.model.domain.ResultInfo;
import com.zjq.seckill.service.FollowService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* Follow/Unfollow controller
*/
@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) {
ResultInfo resultInfo = followService.follow(followUserId,
isFollowed, access_token, request.getServletPath());
return resultInfo;
}
}Gateway Route Configuration
spring:
application:
name: ms-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: ms-follow
uri: lb://ms-follow
predicates:
- Path=/follow/**
filters:
- StripPrefix=1Testing and Verification
Start the registration center, gateway, authentication service, and follow microservice. Example: user 5 follows user 1; Redis shows a following set and a followers set; MySQL records the relationship. Additional tests with users 5, 6, 7 following users 1, 2, 3 demonstrate correct storage and retrieval of common follows.
Common Follow List
To obtain users that both the logged‑in user and another user follow, intersect their Redis following sets and then fetch user details from the user service.
Controller Method
@GetMapping("commons/{userId}")
public ResultInfo findCommonsFriends(@PathVariable Integer userId,
String access_token) {
return followService.findCommonsFriends(userId, access_token, request.getServletPath());
}Service Method
@Transactional(rollbackFor = Exception.class)
public ResultInfo findCommonsFriends(Integer userId, String accessToken, String path) {
AssertUtil.isTrue(userId == null || userId < 1, "Please select a user to view");
SignInUserInfo userInfo = loadSignInuserInfo(accessToken);
String loginKey = RedisKeyConstant.following.getKey() + userInfo.getId();
String targetKey = RedisKeyConstant.following.getKey() + userId;
Set<Integer> commonIds = redisTemplate.opsForSet().intersect(loginKey, targetKey);
if (commonIds == null || commonIds.isEmpty()) {
return ResultInfoUtil.buildSuccess(path, new ArrayList<>());
}
ResultInfo resultInfo = restTemplate.getForObject(
usersServerName + "findByIds?access_token={accessToken}&ids={ids}",
ResultInfo.class, accessToken, String.join(",", commonIds));
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
List<LinkedHashMap> maps = (ArrayList) resultInfo.getData();
List<ShortUserInfo> users = maps.stream()
.map(m -> BeanUtil.fillBeanWithMap(m, new ShortUserInfo(), true))
.collect(Collectors.toList());
return ResultInfoUtil.buildSuccess(path, users);
}User Service Extension
Added endpoint to retrieve multiple users by IDs.
@GetMapping("findByIds")
public ResultInfo<List<ShortUserInfo>> findByIds(String ids) {
List<ShortUserInfo> dinerInfos = userService.findByIds(ids);
return ResultInfoUtil.buildSuccess(request.getServletPath(), dinerInfos);
} @Select("<script> " +
"select id, nickname, avatar_url from t_diners " +
"where is_valid = 1 and id in " +
"<foreach item=\"id\" collection=\"ids\" open=\"(\" separator=\",\" close=\")\"> " +
" #{id} " +
"</foreach> " +
"</script>")
List<ShortUserInfo> findByIds(@Param("ids") String[] ids);This article ends here. If you found it useful, please like it; your encouragement is the biggest motivation.
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 High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
