Redis快取 + 定時寫入資料庫實現高效能點贊功能
本文基於 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 專案的整合
- 在
pom.xml
中引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 在啟動類上添加註釋
@EnableCaching
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
@EnableFeignClients(basePackages = "com.solo.coderiver.project.client")
@EnableCaching
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication. class, args);
}
}
- 編寫 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;
@Configuration
public 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種資料結構型別作簡單的介紹:
結構型別 | 結構儲存的值 | 結構的讀寫能力 |
---|---|---|
String | 可以是字串、整數或者浮點數 | 對整個字串或者字串的其中一部分執行操作;物件和浮點數執行自增(increment)或者自減(decrement) |
List | 一個連結串列,連結串列上的每個節點都包含了一個字串 | 從連結串列的兩端推入或者彈出元素;根據偏移量對連結串列進行修剪(trim);讀取單個或者多個元素;根據值來查詢或者移除元素 |
Set | 包含字串的無序收集器(unorderedcollection),並且被包含的每個字串都是獨一無二的、各不相同 | 新增、獲取、移除單個元素;檢查一個元素是否存在於某個集合中;計算交集、並集、差集;從集合裡賣弄隨機獲取元素 |
Hash | 包含鍵值對的無序散列表 | 新增、獲取、移除單個鍵值對;獲取所有鍵值對 |
Zset | 字串成員(member)與浮點數分值(score)之間的有序對映,元素的排列順序由分值的大小決定 | 新增、獲取、刪除單個元素;根據分值範圍(range)或者成員來獲取元素 |
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
@Slf4j
public 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;
/**
* 使用者點讚的狀態
*/
@Getter
public 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
@Data
public 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.springframewor