Implementing Follow/Unfollow Functionality with Redis and MySQL in PHP

This article explains how to design and implement a follow/unfollow feature using a MySQL table for persistence and Redis ordered‑set commands (zAdd, zRem, zRange, zCard, zScore) in PHP, covering database schema, code examples, pagination, and status detection.

Laravel Tech Community
Laravel Tech Community
Laravel Tech Community
Implementing Follow/Unfollow Functionality with Redis and MySQL in PHP

When building an API that requires a follow/unfollow feature, using MySQL alone can be inefficient, so the author proposes a hybrid solution that stores relationship data in MySQL and caches it in Redis for fast access.

Database table

The MySQL table shc_sns stores the follower ID, the followed ID, the timestamp, and a follow state (1 for one‑way, 2 for mutual). The table definition is:

DROP TABLE IF EXISTS `shc_sns`;
CREATE TABLE `shc_sns` (
  `sns_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id值',
  `sns_frommid` int(11) NOT NULL COMMENT '会员id',
  `sns_tomid` int(11) NOT NULL COMMENT '朋友id',
  `sns_addtime` int(11) NOT NULL COMMENT '添加时间',
  `sns_followstate` tinyint(1) NOT NULL DEFAULT '1' COMMENT '关注状态 1为单方关注 2为双方关注',
  PRIMARY KEY (`sns_id`),
  KEY `FROMMID` (`sns_frommid`) USING BTREE,
  KEY `TOMID` (`sns_tomid`,`sns_frommid`) USING BTREE,
  KEY `FRIEND_IDS` (`sns_tomid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=196 DEFAULT CHARSET=utf8 COMMENT='好友数据表';

Using Redis to implement follow

The author uses Redis sorted sets (zAdd) to store follow and fan lists, using the current timestamp as the score to enable pagination.

public function addFollowsFansById($data) {
    if (empty($data)) return false;
    $data['sns_addtime'] = time();
    if ($data['sns_followstate'] == 1) { // single‑side follow
        $result = $this->addFollowFan($data);
        if ($result) {
            $this->cache->redis->zAdd('shc_follow:' . $data['sns_frommid'], time(), $data['sns_tomid']);
            $this->cache->redis->zAdd('shc_fans:' . $data['sns_tomid'], time(), $data['sns_frommid']);
        }
        return true;
    } elseif ($data['sns_followstate'] == 2) { // mutual follow
        $this->db->trans_begin();
        $this->addFollowFan($data);
        $this->editFollowFans([
            'sns_frommid' => $data['sns_tomid'],
            'sns_tomid'   => $data['sns_frommid']
        ], ['sns_followstate' => 2]);
        if ($this->db->trans_status() === false) {
            $this->db->trans_rollback();
            return false;
        } else {
            $this->db->trans_commit();
            $this->cache->redis->zAdd('shc_follow:' . $data['sns_frommid'], time(), $data['sns_tomid']);
            $this->cache->redis->zAdd('shc_fans:' . $data['sns_tomid'], time(), $data['sns_frommid']);
            return true;
        }
    } else {
        return false;
    }
}

Unfollow implementation

Unfollow removes entries from both sorted sets using zRem, handling both single‑side and mutual cases with database transactions.

public function deleteFollowFansById($user_id, $follow_id, $state) {
    if ($state == 1) {
        $result = $this->deleteFollowFans(['sns_frommid' => $user_id, 'sns_tomid' => $follow_id]);
        if ($result) {
            $this->cache->redis->zRem('shc_fans:' . $follow_id, $user_id);
        }
        return true;
    } elseif ($state == 2) {
        $this->db->trans_begin();
        $this->deleteFollowFans(['sns_frommid' => $user_id, 'sns_tomid' => $follow_id]);
        $this->editFollowFans(['sns_frommid' => $follow_id, 'sns_tomid' => $user_id], ['sns_followstate' => 1]);
        if ($this->db->trans_status() === false) {
            $this->db->trans_rollback();
            return false;
        } else {
            $this->cache->redis->zRem('shc_follow:' . $user_id, $follow_id);
            $this->cache->redis->zRem('shc_fans:' . $follow_id, $user_id);
            $this->db->trans_commit();
            return true;
        }
    } else {
        return false;
    }
}

Retrieving follow/fan lists

Lists are fetched with zRevRange (or zRange) using pagination parameters calculated from the requested page and page size.

public function getFollowsListById($user_id, $limit = array()) {
    $follow_list = $this->cache->redis->zRevRange('shc_follow:' . $user_id,
        $limit['current_page'],
        $limit['current_page'] + $limit['page_size'] - 1);
    if ($follow_list) {
        $member_list = $this->Member_Model->getMemberListByIdS($follow_list, 'user_id,user_nickname,user_portrait');
        return $member_list ? $member_list : $this->getFollowsListByDb(['a.sns_frommid' => $user_id], 'b.user_id,b.user_nickname,b.user_portrait', $limit);
    }
}

Counting follows/fans

The Redis zCard command returns the size of the ordered set, giving the number of follows or fans.

Follow status detection

By checking the existence of a member in the sorted sets with zScore, the system can determine whether the relationship is one‑way, the opposite way, or mutual.

Conclusion

Using Redis for follow data offloads read/write pressure from MySQL, enables fast pagination, and simplifies status checks, while the MySQL table remains the source of truth for persistence.

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.

BackendRedisMySQLPHPFollow Systemzadd
Laravel Tech Community
Written by

Laravel Tech Community

Specializing in Laravel development, we continuously publish fresh content and grow alongside the elegant, stable Laravel framework.

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.