Redis高效實現點贊、取消點贊只需這四步
來自: 掘金,作者: solocoder
連結:
本文基於 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>
2.在啟動類上新增註釋 @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);
}
}
3.編寫 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種資料結構型別作簡單的介紹:
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
將具體操作方法封裝到了 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.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
public
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>
2.編寫配置檔案
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;
@Configuration
public
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();
}
}
3.編寫執行任務的類繼承自 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;
/**
* 點讚的定時任務
*/
@Slf4j
public
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 封裝的方法完成資料同步。
以上就是點贊功能的設計與實現,不足之處還請各位大佬多多指教。
另外,點贊/取消點贊 跟 點贊數 +1/ -1 應該保證是原子操作 , 不然出現併發問題就會有兩條重複的點贊記錄 , 所以要給整個原子操作加鎖 . 同時需要在Spring Boot 的系統關閉鉤子函式中補充同步redis中點贊資料到mysql中的過程 . 不然有可能出現距離上一次同步1小時59分的時候伺服器更新 , 把整整兩小時的點贊資料都給清空了 . 如果點贊設計到比較重要活動業務的話這就很尷尬了 .
想了解更多關於資料庫、雲技術的內容嗎?
快來關注“資料和雲"、"雲和恩墨,"公眾號及"雲和恩墨"官方網站,我們期待大家一同學習與進步!
小程式”DBASK“線上問答,隨時解惑,歡迎瞭解和關注!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31556440/viewspace-2673068/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 微信小程式實現點贊、取消點贊,和多項點選功能微信小程式
- Redis實現點贊功能模組Redis
- android短視訊開發,實現動態點贊出現的點贊動畫Android動畫
- vue實現對文章列表的點贊Vue
- 基於SpringBoot如何實現一個點贊功能?Spring Boot
- 全網最全的Java SpringBoot點贊功能實現JavaSpring Boot
- 給上海民警點贊
- 基於 Laravel 和 Redis 的點贊功能設計LaravelRedis
- js閉包例子—對不同物件實現點贊累加JS物件
- 用Flutter實現一個仿Twitter的點贊效果Flutter
- Flutter仿掘金點贊效果Flutter
- 通用點贊設計思路
- L1-034 點贊 pythonPython
- 仿抖音點贊按鈕
- Android 自定義View 點贊效果AndroidView
- redis系列:通過文章點贊排名案例學習sortedset命令Redis
- 微擎 微贊 微盟 有贊 點點客微信介面對比哪個好
- 點贊功能模組-獲取使用者詳情與點贊過的歷史文章
- 爬蟲實戰 -- QQ空間自動點贊爬蟲
- React Native 仿抖音點贊特效React Native特效
- 關於點贊業務對MySQL和Redis和MongoDB的思考MySqlRedisMongoDB
- 優點點贊系統開發的分析介紹
- L1-034 點贊 分數 20
- 視覺有難,八方點贊。視覺
- 點贊處理的一些小技巧
- iOS 點贊功能高併發的思考iOS
- 自定義計數器小技巧!CSS 實現長按點贊累加動畫CSS動畫
- Android自定義View教你一步一步實現即刻點贊效果AndroidView
- 這份深度學習課程筆記獲吳恩達點贊深度學習筆記吳恩達
- L2-021 點贊狂魔【模擬】
- 零售資料分析報表這樣做,老闆狂點贊!
- 直播軟體原始碼,好看的點贊按鈕原始碼
- 點贊收藏:PyTorch常用程式碼段整理合集PyTorch
- 「小程式JAVA實戰」小程式的影片點贊功能開發(62)Java
- 百萬點贊怎麼來?Python批量製作抖音的卡點視訊原來這麼簡單!Python
- 別人都在認真聽課,我埋頭寫Python為主播瘋狂點點點點點贊!Python
- WPF 使用動畫繪製一個點贊大拇指動畫
- 給上海的民警們點贊。做的太好了。