Redis 如何高效實現(xiàn)點贊、取消點贊功能
點擊“藍字”,關注,置頂公眾號
每日技術干貨,第一時間送達!
1
基于 SpringCloud, 用戶發(fā)起點贊、取消點贊后先存入 Redis 中,再每隔兩小時從 Redis 讀取點贊數(shù)據(jù)寫入數(shù)據(jù)庫中做持久化存儲。
點贊功能在很多系統(tǒng)中都有,但別看功能小,想要做好需要考慮的東西還挺多的。
點贊、取消點贊是高頻次的操作,若每次都讀寫數(shù)據(jù)庫,大量的操作會影響數(shù)據(jù)庫性能,所以需要做緩存。
至于多久從 Redis 取一次數(shù)據(jù)存到數(shù)據(jù)庫中,根據(jù)項目的實際情況定吧,我是暫時設了兩個小時。
項目需求需要查看都誰點贊了,所以要存儲每個點贊的點贊人、被點贊人,不能簡單的做計數(shù)。
文章分四部分介紹:
Redis 緩存設計及實現(xiàn)
數(shù)據(jù)庫設計
數(shù)據(jù)庫操作
開啟定時任務持久化存儲到數(shù)據(jù)庫
2
1.1 Redis 安裝及運行
Redis 安裝請自行查閱相關教程。
說下Docker 安裝運行 Redis
docker run -d -p?6379:6379?redis:4.0.8如果已經安裝了 Redis,打開命令行,輸入啟動 Redis 的命令
redis-server1.2 Redis 與 SpringBoot 項目的整合
1.在 pom.xml 中引入依賴
<dependency>??
????<groupId>org.springframework.bootgroupId>??
????<artifactId>spring-boot-starter-data-redisartifactId>??
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
@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 的數(shù)據(jù)結構類型
Redis 可以存儲鍵與5種不同數(shù)據(jù)結構類型之間的映射,這5種數(shù)據(jù)結構類型分別為String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。
下面來對這5種數(shù)據(jù)結構類型作簡單的介紹:

1.4 點贊數(shù)據(jù)在 Redis 中的存儲格式
用 Redis 存儲兩種數(shù)據(jù),一種是記錄點贊人、被點贊人、點贊狀態(tài)的數(shù)據(jù),另一種是每個用戶被點贊了多少次,做個簡單的計數(shù)。
由于需要記錄點贊人和被點贊人,還有點贊狀態(tài)(點贊、取消點贊),還要固定時間間隔取出 Redis 中所有點贊數(shù)據(jù),分析了下 Redis 數(shù)據(jù)格式中 Hash 最合適。
因為 Hash 里的數(shù)據(jù)都是存在一個鍵里,可以通過這個鍵很方便的把所有的點贊數(shù)據(jù)都取出。這個鍵里面的數(shù)據(jù)還可以存成鍵值對的形式,方便存入點贊人、被點贊人和點贊狀態(tài)。
設點贊人的 id 為 likedPostId,被點贊人的 id 為 likedUserId ,點贊時狀態(tài)為 1,取消點贊狀態(tài)為 0。將點贊人 id 和被點贊人 id 作為鍵,兩個 id 中間用 :: 隔開,點贊狀態(tài)作為值。
所以如果用戶點贊,存儲的鍵為:likedUserId::likedPostId,對應的值為 1 。取消點贊,存儲的鍵為:likedUserId::likedPostId,對應的值為 0 。取數(shù)據(jù)時把鍵用 :: 切開就得到了兩個id,也很方便。
在可視化工具 RDM 中看到的是這樣子


1.5 操作 Redis
將具體操作方法封裝到了 RedisService 接口里
RedisService.java
public?interface?RedisService?{?
????/**
?????* 點贊。狀態(tài)為1
?????*?@param?likedUserId
?????*?@param?likedPostId
?????*/??
????void?saveLiked2Redis(String likedUserId, String likedPostId);
????/**
?????* 取消點贊。將狀態(tài)改變?yōu)?
?????*?@param?likedUserId
?????*?@param?likedPostId
?????*/??
????void?unlikeFromRedis(String likedUserId, String likedPostId);
????/**
?????* 從Redis中刪除一條點贊數(shù)據(jù)
?????*?@param?likedUserId
?????*?@param?likedPostId
?????*/??
????void?deleteLikedFromRedis(String likedUserId, String likedPostId);
????/**
?????* 該用戶的點贊數(shù)加1
?????*?@param?likedUserId
?????*/??
????void?incrementLikedCount(String likedUserId);
????/**
?????* 該用戶的點贊數(shù)減1
?????*?@param?likedUserId
?????*/??
????void?decrementLikedCount(String likedUserId);
??
????/**
?????* 獲取Redis中存儲的所有點贊數(shù)據(jù)
?????*?@return??
?????*/??
????List?getLikedDataFromRedis() ;
????/**
?????* 獲取Redis中存儲的所有點贊數(shù)量
?????*?@return??
?????*/??
????List?getLikedCountFromRedis() ;
}實現(xiàn)類 RedisServiceImpl.java
@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 getLikedDataFromRedis() {
????????CursorObject ,?Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
????????List 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 getLikedCountFromRedis() {
????????CursorObject,?Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
????????List list =?new?ArrayList<>();
????????while?(cursor.hasNext()){
????????????Map.Entry<Object,?Object> map = cursor.next();
????????????//將點贊數(shù)量存儲在 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, 用于根據(jù)一定規(guī)則生成 key
public?class?RedisKeyUtils?{
????//保存用戶點贊數(shù)據(jù)的key
????public?static?final?String MAP_KEY_USER_LIKED =?"MAP_USER_LIKED";
????//保存用戶被點贊數(shù)量的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 用戶點贊狀態(tài)的枚舉類
/**
?* 用戶點贊的狀態(tài)
?*/
@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;
????}
}3
數(shù)據(jù)庫表中至少要包含三個字段:被點贊用戶id,點贊用戶id,點贊狀態(tài)。再加上主鍵id,創(chuàng)建時間,修改時間就行了。
建表語句
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?'點贊狀態(tài),0取消,1點贊',
????`create_time`?timestamp?not?null?default?current_timestamp?comment?'創(chuàng)建時間',
??`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
/**
?* 用戶點贊表
?*/
@Entity
@Data
public?class?UserLike {
????//主鍵id
????@Id
????@GeneratedValue(strategy = GenerationType.IDENTITY)
????private?Integer id;
????//被點贊的用戶的id
????private?String?likedUserId;
????//點贊的用戶的id
????private?String?likedPostId;
????//點贊的狀態(tài).默認未點贊
????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;
????}
}4
操作數(shù)據(jù)庫同樣封裝在接口中
LikedService
public?interface?LikedService?{
?
????/**
????* 保存點贊記錄
????*?@param?userLike
????*?@return
????*/
???UserLike save(UserLike userLike);
?
????/**
????* 批量保存或修改
????*?@param?list
????*/
???List saveAll(List?list);
?
???/**
????* 根據(jù)被點贊人的id查詢點贊列表(即查詢都誰給這個人點贊過)
????*?@param?likedUserId 被點贊人的id
????*?@param?pageable
????*?@return
????*/
???Page getLikedListByLikedUserId(String likedUserId, Pageable pageable);
????/**
????* 根據(jù)點贊人的id查詢點贊列表(即查詢這個人都給誰點贊過)
????*?@param?likedPostId
????*?@param?pageable
????*?@return
????*/
???Page getLikedListByLikedPostId(String likedPostId, Pageable pageable);
?
????/**
????* 通過被點贊人和點贊人id查詢是否存在點贊記錄
????*?@param?likedUserId
????*?@param?likedPostId
????*?@return
????*/
???UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId);
?
????/**
????* 將Redis里的點贊數(shù)據(jù)存入數(shù)據(jù)庫中
????*/
???void transLikedFromRedis2DB();
?
????/**
????* 將Redis中的點贊數(shù)量數(shù)據(jù)存入數(shù)據(jù)庫
????*/
???void transLikedCountFromRedis2DB();
?
} LikedServiceImpl 實現(xiàn)類
@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?saveAll(List list) ? {
????????return?likeRepository.saveAll(list);
????}
????@Override
????public?Page?getLikedListByLikedUserId(String likedUserId, Pageable pageable)? {
????????return?likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable);
????}
????@Override
????public?Page?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 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 list = redisService.getLikedCountFromRedis();
????????for?(LikedCountDTO dto : list) {
????????????UserInfo user = userService.findById(dto.getId());
????????????//點贊數(shù)量屬于無關緊要的操作,出錯無需拋異常
????????????if?(user !=?null){
????????????????Integer likeNum = user.getLikeNum() + dto.getCount();
????????????????user.setLikeNum(likeNum);
????????????????//更新點贊數(shù)量
????????????????userService.updateInfo(user);
????????????}
????????}
????}
} 數(shù)據(jù)庫的操作就這些,主要還是增刪改查。
?
5
定時任務 Quartz 很強大,就用它了。
Quartz 使用步驟:
1.添加依賴
<dependency>??
????<groupId>org.springframework.bootgroupId>??
????<artifactId>spring-boot-starter-quartzartifactId>??
dependency>2.編寫配置文件
@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)?//兩個小時執(zhí)行一次
????????????????.repeatForever();
????????return?TriggerBuilder.newTrigger().forJob(quartzDetail())
????????????????.withIdentity(LIKE_TASK_IDENTITY)
????????????????.withSchedule(scheduleBuilder)
????????????????.build();
????}
}3.編寫執(zhí)行任務的類繼承自 QuartzJobBean
/**
?* 點贊的定時任務
?*/
@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 里的點贊信息同步到數(shù)據(jù)庫里
????????likedService.transLikedFromRedis2DB();
????????likedService.transLikedCountFromRedis2DB();
????}
}?在定時任務中直接調用 LikedService 封裝的方法完成數(shù)據(jù)同步。
以上就是點贊功能的設計與實現(xiàn),不足之處還請各位大佬多多指教。
另外,點贊/取消點贊 跟 點贊數(shù) +1/ -1 應該保證是原子操作,不然出現(xiàn)并發(fā)問題就會有兩條重復的點贊記錄,所以要給整個原子操作加鎖。
同時需要在Spring Boot 的系統(tǒng)關閉鉤子函數(shù)中補充同步redis中點贊數(shù)據(jù)到mysql中的過程 . 不然有可能出現(xiàn)距離上一次同步1小時59分的時候服務器更新,把整整兩小時的點贊數(shù)據(jù)都給清空了。如果點贊設計到比較重要活動業(yè)務的話這就很尷尬了 。
來源:blog.csdn.net/lsy0903/article/details/103949459
往期推薦
