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.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Building a Scalable Follow Service with MySQL, Redis Sets, and Spring Boot

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendjavaMicroservicesredisSpring BootmysqlFollow Service
Java Architect Essentials
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.