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.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
How to Build a High‑Performance Follow Service with MySQL, Redis, and Spring Boot

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=1

Testing 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.
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.

JavaredisSpring BootmysqlMicroserviceFollow Service
Java High-Performance Architecture
Written by

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.

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.