使用 Redis 如何實現點贊,取消點贊呢?

  • 2019 年 11 月 15 日
  • 筆記

本文基於 SpringCloud, 用戶發起點贊、取消點贊後先存入 Redis 中,再每隔兩小時從 Redis 讀取點贊數據寫入數據庫中做持久化存儲。

點贊功能在很多系統中都有,但別看功能小,想要做好需要考慮的東西還挺多的。

點贊、取消點贊是高頻次的操作,若每次都讀寫數據庫,大量的操作會影響數據庫性能,所以需要做緩存。

至於多久從 Redis 取一次數據存到數據庫中,根據項目的實際情況定吧,我是暫時設了兩個小時。

項目需求需要查看都誰點贊了,所以要存儲每個點贊的點贊人、被點贊人,不能簡單的做計數。

文章分四部分介紹:

  • Redis 緩存設計及實現
  • 數據庫設計
  • 數據庫操作
  • 開啟定時任務持久化存儲到數據庫

一、Redis 緩存設計及實現

1.1 Redis 安裝及運行

Redis 安裝請自行查閱相關教程。

說下Docker 安裝運行 Redis

docker run -d -p 6379:6379 redis:4.0.8

如果已經安裝了 Redis,打開命令行,輸入啟動 Redis 的命令

redis-server

1.2 Redis 與 SpringBoot 項目的整合

1、 在 pom.xml 中引入依賴

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency>

1、 在啟動類上添加註釋 @EnableCaching

@SpringBootApplication@EnableDiscoveryClient@EnableSwagger2@EnableFeignClients(basePackages = "com.solo.coderiver.project.client")@EnableCachingpublic class UserApplication {      public static void main(String[] args) {        SpringApplication.run(UserApplication.class, args);    }}

1、 編寫 Redis 配置類 RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;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.core.StringRedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;  import java.net.UnknownHostException;    @Configurationpublic class RedisConfig {      @Bean    @ConditionalOnMissingBean(name = "redisTemplate")    public RedisTemplate<String, Object> redisTemplate(            RedisConnectionFactory redisConnectionFactory)            throws UnknownHostException {          Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);        ObjectMapper om = new ObjectMapper();        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        jackson2JsonRedisSerializer.setObjectMapper(om);          RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();        template.setConnectionFactory(redisConnectionFactory);        template.setKeySerializer(jackson2JsonRedisSerializer);        template.setValueSerializer(jackson2JsonRedisSerializer);        template.setHashKeySerializer(jackson2JsonRedisSerializer);        template.setHashValueSerializer(jackson2JsonRedisSerializer);        template.afterPropertiesSet();        return template;    }        @Bean    @ConditionalOnMissingBean(StringRedisTemplate.class)    public StringRedisTemplate stringRedisTemplate(            RedisConnectionFactory redisConnectionFactory)            throws UnknownHostException {        StringRedisTemplate template = new StringRedisTemplate();        template.setConnectionFactory(redisConnectionFactory);        return template;    }}

至此 Redis 在 SpringBoot 項目中的配置已經完成,可以愉快的使用了。

1.3 Redis 的數據結構類型

Redis 可以存儲鍵與5種不同數據結構類型之間的映射,這5種數據結構類型分別為String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。

下面來對這5種數據結構類型作簡單的介紹:

1.4 點贊數據在 Redis 中的存儲格式

用 Redis 存儲兩種數據,一種是記錄點贊人、被點贊人、點贊狀態的數據,另一種是每個用戶被點贊了多少次,做個簡單的計數。

由於需要記錄點贊人和被點贊人,還有點贊狀態(點贊、取消點贊),還要固定時間間隔取出 Redis 中所有點贊數據,分析了下 Redis 數據格式中 Hash 最合適。

因為 Hash 里的數據都是存在一個鍵里,可以通過這個鍵很方便的把所有的點贊數據都取出。這個鍵裏面的數據還可以存成鍵值對的形式,方便存入點贊人、被點贊人和點贊狀態。

設點贊人的 id 為 likedPostId,被點贊人的 id 為 likedUserId ,點贊時狀態為 1,取消點贊狀態為 0。將點贊人 id 和被點贊人 id 作為鍵,兩個 id 中間用 :: 隔開,點贊狀態作為值。

所以如果用戶點贊,存儲的鍵為:likedUserId::likedPostId,對應的值為 1 。

取消點贊,存儲的鍵為:likedUserId::likedPostId,對應的值為 0 。

取數據時把鍵用 :: 切開就得到了兩個id,也很方便。

在可視化工具 RDM 中看到的是這樣子

1.5 操作 Redis

Redis 各種數據格式的操作方法可以看看 這篇文章 ,寫的非常好。

將具體操作方法封裝到了 RedisService 接口裡

RedisService.java

import com.solo.coderiver.user.dataobject.UserLike;import com.solo.coderiver.user.dto.LikedCountDTO;  import java.util.List;  public interface RedisService {      /**     * 點贊。狀態為1     * @param likedUserId     * @param likedPostId     */    void saveLiked2Redis(String likedUserId, String likedPostId);      /**     * 取消點贊。將狀態改變為0     * @param likedUserId     * @param likedPostId     */    void unlikeFromRedis(String likedUserId, String likedPostId);      /**     * 從Redis中刪除一條點贊數據     * @param likedUserId     * @param likedPostId     */    void deleteLikedFromRedis(String likedUserId, String likedPostId);      /**     * 該用戶的點贊數加1     * @param likedUserId     */    void incrementLikedCount(String likedUserId);      /**     * 該用戶的點贊數減1     * @param likedUserId     */    void decrementLikedCount(String likedUserId);      /**     * 獲取Redis中存儲的所有點贊數據     * @return     */    List<UserLike> getLikedDataFromRedis();      /**     * 獲取Redis中存儲的所有點贊數量     * @return     */    List<LikedCountDTO> getLikedCountFromRedis();  }

實現類 RedisServiceImpl.java

import com.solo.coderiver.user.dataobject.UserLike;import com.solo.coderiver.user.dto.LikedCountDTO;import com.solo.coderiver.user.enums.LikedStatusEnum;import com.solo.coderiver.user.service.LikedService;import com.solo.coderiver.user.service.RedisService;import com.solo.coderiver.user.utils.RedisKeyUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.Cursor;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.ScanOptions;import org.springframework.stereotype.Service;  import java.util.ArrayList;import java.util.List;import java.util.Map;  @Service@Slf4jpublic class RedisServiceImpl implements RedisService {      @Autowired    RedisTemplate redisTemplate;      @Autowired    LikedService likedService;      @Override    public void saveLiked2Redis(String likedUserId, String likedPostId) {        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);        redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());    }      @Override    public void unlikeFromRedis(String likedUserId, String likedPostId) {        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);        redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());    }      @Override    public void deleteLikedFromRedis(String likedUserId, String likedPostId) {        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);        redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);    }      @Override    public void incrementLikedCount(String likedUserId) {        redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);    }      @Override    public void decrementLikedCount(String likedUserId) {        redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);    }      @Override    public List<UserLike> getLikedDataFromRedis() {        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);        List<UserLike> list = new ArrayList<>();        while (cursor.hasNext()){            Map.Entry<Object, Object> entry = cursor.next();            String key = (String) entry.getKey();            //分離出 likedUserId,likedPostId            String[] split = key.split("::");            String likedUserId = split[0];            String likedPostId = split[1];            Integer value = (Integer) entry.getValue();              //組裝成 UserLike 對象            UserLike userLike = new UserLike(likedUserId, likedPostId, value);            list.add(userLike);              //存到 list 後從 Redis 中刪除            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);        }          return list;    }      @Override    public List<LikedCountDTO> getLikedCountFromRedis() {        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);        List<LikedCountDTO> list = new ArrayList<>();        while (cursor.hasNext()){            Map.Entry<Object, Object> map = cursor.next();            //將點贊數量存儲在 LikedCountDT            String key = (String)map.getKey();            LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());            list.add(dto);            //從Redis中刪除這條記錄            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);        }        return list;    }}

用到的工具類和枚舉類

RedisKeyUtils, 用於根據一定規則生成 key

public class RedisKeyUtils {      //保存用戶點贊數據的key    public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";    //保存用戶被點贊數量的key    public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";      /**     * 拼接被點贊的用戶id和點贊的人的id作為key。格式 222222::333333     * @param likedUserId 被點贊的人id     * @param likedPostId 點贊的人的id     * @return     */    public static String getLikedKey(String likedUserId, String likedPostId){        StringBuilder builder = new StringBuilder();        builder.append(likedUserId);        builder.append("::");        builder.append(likedPostId);        return builder.toString();    }}

LikedStatusEnum 用戶點贊狀態的枚舉類

package com.solo.coderiver.user.enums;  import lombok.Getter;  /** * 用戶點贊的狀態 */@Getterpublic enum LikedStatusEnum {    LIKE(1, "點贊"),    UNLIKE(0, "取消點贊/未點贊"),    ;      private Integer code;      private String msg;      LikedStatusEnum(Integer code, String msg) {        this.code = code;        this.msg = msg;    }}

二、數據庫設計

數據庫表中至少要包含三個字段:被點贊用戶id,點贊用戶id,點贊狀態。再加上主鍵id,創建時間,修改時間就行了。

建表語句

create table `user_like`(    `id` int not null auto_increment,    `liked_user_id` varchar(32) not null comment '被點贊的用戶id',    `liked_post_id` varchar(32) not null comment '點贊的用戶id',    `status` tinyint(1) default '1' comment '點贊狀態,0取消,1點贊',    `create_time` timestamp not null default current_timestamp comment '創建時間',  `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改時間',    primary key(`id`),    INDEX `liked_user_id`(`liked_user_id`),    INDEX `liked_post_id`(`liked_post_id`)) comment '用戶點贊表';

對應的對象 UserLike

import com.solo.coderiver.user.enums.LikedStatusEnum;import lombok.Data;  import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;  /** * 用戶點贊表 */@Entity@Datapublic class UserLike {      //主鍵id    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private Integer id;      //被點贊的用戶的id    private String likedUserId;      //點贊的用戶的id    private String likedPostId;      //點贊的狀態.默認未點贊    private Integer status = LikedStatusEnum.UNLIKE.getCode();      public UserLike() {    }      public UserLike(String likedUserId, String likedPostId, Integer status) {        this.likedUserId = likedUserId;        this.likedPostId = likedPostId;        this.status = status;    }}

三、數據庫操作

操作數據庫同樣封裝在接口中

LikedService

import com.solo.coderiver.user.dataobject.UserLike;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;  import java.util.List;  public interface LikedService {      /**     * 保存點贊記錄     * @param userLike     * @return     */    UserLike save(UserLike userLike);      /**     * 批量保存或修改     * @param list     */    List<UserLike> saveAll(List<UserLike> list);        /**     * 根據被點贊人的id查詢點贊列表(即查詢都誰給這個人點贊過)     * @param likedUserId 被點贊人的id     * @param pageable     * @return     */    Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable);      /**     * 根據點贊人的id查詢點贊列表(即查詢這個人都給誰點贊過)     * @param likedPostId     * @param pageable     * @return     */    Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable);      /**     * 通過被點贊人和點贊人id查詢是否存在點贊記錄     * @param likedUserId     * @param likedPostId     * @return     */    UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId);      /**     * 將Redis里的點贊數據存入數據庫中     */    void transLikedFromRedis2DB();      /**     * 將Redis中的點贊數量數據存入數據庫     */    void transLikedCountFromRedis2DB();  }

LikedServiceImpl 實現類

import com.solo.coderiver.user.dataobject.UserInfo;import com.solo.coderiver.user.dataobject.UserLike;import com.solo.coderiver.user.dto.LikedCountDTO;import com.solo.coderiver.user.enums.LikedStatusEnum;import com.solo.coderiver.user.repository.UserLikeRepository;import com.solo.coderiver.user.service.LikedService;import com.solo.coderiver.user.service.RedisService;import com.solo.coderiver.user.service.UserService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;  import java.util.List;  @Service@Slf4jpublic class LikedServiceImpl implements LikedService {      @Autowired    UserLikeRepository likeRepository;      @Autowired    RedisService redisService;      @Autowired    UserService userService;      @Override    @Transactional    public UserLike save(UserLike userLike) {        return likeRepository.save(userLike);    }      @Override    @Transactional    public List<UserLike> saveAll(List<UserLike> list) {        return likeRepository.saveAll(list);    }      @Override    public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) {        return likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable);    }      @Override    public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) {        return likeRepository.findByLikedPostIdAndStatus(likedPostId, LikedStatusEnum.LIKE.getCode(), pageable);    }      @Override    public UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {        return likeRepository.findByLikedUserIdAndLikedPostId(likedUserId, likedPostId);    }      @Override    @Transactional    public void transLikedFromRedis2DB() {        List<UserLike> list = redisService.getLikedDataFromRedis();        for (UserLike like : list) {            UserLike ul = getByLikedUserIdAndLikedPostId(like.getLikedUserId(), like.getLikedPostId());            if (ul == null){                //沒有記錄,直接存入                save(like);            }else{                //有記錄,需要更新                ul.setStatus(like.getStatus());                save(ul);            }        }    }      @Override    @Transactional    public void transLikedCountFromRedis2DB() {        List<LikedCountDTO> list = redisService.getLikedCountFromRedis();        for (LikedCountDTO dto : list) {            UserInfo user = userService.findById(dto.getId());            //點贊數量屬於無關緊要的操作,出錯無需拋異常            if (user != null){                Integer likeNum = user.getLikeNum() + dto.getCount();                user.setLikeNum(likeNum);                //更新點贊數量                userService.updateInfo(user);            }        }    }}

數據庫的操作就這些,主要還是增刪改查。

四、開啟定時任務持久化存儲到數據庫

定時任務 Quartz 很強大,就用它了。

Quartz 使用步驟:

1、 添加依賴

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-quartz</artifactId></dependency>

1、 編寫配置文件

package com.solo.coderiver.user.config;  import com.solo.coderiver.user.task.LikeTask;import org.quartz.*;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;  @Configurationpublic class QuartzConfig {      private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";      @Bean    public JobDetail quartzDetail(){        return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();    }      @Bean    public Trigger quartzTrigger(){        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()//                .withIntervalInSeconds(10)  //設置時間周期單位秒                .withIntervalInHours(2)  //兩個小時執行一次                .repeatForever();        return TriggerBuilder.newTrigger().forJob(quartzDetail())                .withIdentity(LIKE_TASK_IDENTITY)                .withSchedule(scheduleBuilder)                .build();    }}

1、 編寫執行任務的類繼承自 QuartzJobBean

package com.solo.coderiver.user.task;  import com.solo.coderiver.user.service.LikedService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.time.DateUtils;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.quartz.QuartzJobBean;  import java.text.SimpleDateFormat;import java.util.Date;  /** * 點贊的定時任務 */@Slf4jpublic class LikeTask extends QuartzJobBean {      @Autowired    LikedService likedService;      private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");      @Override    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {          log.info("LikeTask-------- {}", sdf.format(new Date()));          //將 Redis 里的點贊信息同步到數據庫里        likedService.transLikedFromRedis2DB();        likedService.transLikedCountFromRedis2DB();    }}

在定時任務中直接調用 LikedService 封裝的方法完成數據同步。

以上就是點贊功能的設計與實現,不足之處還請各位大佬多多指教。

如有更好的實現方案歡迎在評論區交流…

代碼出自開源項目 CodeRiver,致力於打造全平台型全棧精品開源項目。

coderiver 中文名 河碼,是一個為程序員和設計師提供項目協作的平台。無論你是前端、後端、移動端開發人員,或是設計師、產品經理,都可以在平台上發佈項目,與志同道合的小夥伴一起協作完成項目。

coderiver河碼 類似程序員客棧,但主要目的是方便各細分領域人才之間技術交流,共同成長,多人協作完成項目。暫不涉及金錢交易。

計劃做成包含 pc端(Vue、React)、移動H5(Vue、React)、ReactNative混合開發、Android原生、微信小程序、java後端的全平台型全棧項目,歡迎關注。

https://github.com/cachecats/coderiver

來源:juejin.im/post/5bdc257e6fb9a049ba410098