Design and Implementation of a Friend Follow Microservice Using MySQL and Redis
This article explains how to design and implement a friend follow microservice by combining MySQL for persistent storage with Redis sets for efficient follow/unfollow operations and mutual follow queries, covering database schema, Maven dependencies, Spring Boot configuration, RedisTemplate setup, service and controller logic, and testing procedures.
Requirement Analysis
Friend (follow) functionality is a core feature in social applications, including follow/unfollow, followers, following, mutual follows, etc.
While a relational database can store simple follow lists, querying mutual follows among multiple users is inefficient; Redis sets provide fast intersection, union, and difference operations for this purpose.
Design Idea
The solution combines MySQL for persistent storage and Redis Sets for set operations. Each user maintains two Redis sets: one for the users they follow and another for their followers. Basic Redis commands such as SADD, SREM, SCARD, SISMEMBER, SMEMBERS and SINTER are used.
Database Table Design
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 ROW_FORMAT=COMPACT COMMENT='用户和用户关注表';Microservice Setup
Dependencies (pom.xml)
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-redis
org.mybatis.spring.boot
mybatis-spring-boot-starter
com.zjq
commons
1.0-SNAPSHOT
com.battcn
swagger-spring-boot-starterSpring Boot Configuration (application.yml)
server:
port: 7004
spring:
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:
client:
service-url:
defaultZone: http://localhost:7000/eureka/RedisTemplate Configuration
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
redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate
redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
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
Service Layer
package com.zjq.seckill.service;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@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;
public ResultInfo follow(Integer followUserId, int isFollowed, String accessToken, String path) {
// validation, load login user, check existing follow record
// add, remove or re‑add follow in MySQL and Redis sets
}
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) {
// call oauth server to get user info
}
}Controller Layer
package com.zjq.seckill.controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@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());
}
}Gateway Routing
spring:
cloud:
gateway:
routes:
- id: ms-follow
uri: lb://ms-follow
predicates:
- Path=/follow/**
filters:
- StripPrefix=1Common Follow List
The service reads the logged‑in user’s following set and another user’s following set from Redis, computes the intersection with SINTER, and then calls the user service to retrieve basic profile information.
public ResultInfo findCommonsFriends(Integer userId, String accessToken, String path) {
Set
userIds = redisTemplate.opsForSet().intersect(loginUserKey, userKey);
// remote call to user service, transform result
}Testing
After starting the registry, gateway, authentication service, and follow microservice, the author performed follow operations for several users, verified the Redis sets and MySQL records, and confirmed that mutual follow queries returned the expected results.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.