CSDN

微信朋友圈

像CSDN的点赞功能只记录了数量,微信朋友圈的点赞功能有显示点赞人头像(获取userId查询用户信息封装返回即可)

点赞、取消点赞是高频次的操作,若每次都读写数据库,大量的操作会影响数据库性能,甚至宕机,所以用缓存处理再合适不过。本文以文章点赞为例来展开叙述

数据格式选择

Redis有5种数据结构分别为:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)

由于需要记录文章和点赞人,还有点赞状态(点赞、取消),分析下 Redis 数据格式中Hash最合适。

因为Hash里的数据都是存在一个Key中,通过Key很方便的把所有的点赞数据都取出。Key里面的数据还可以存成键值对的形式,方便存入点赞人、被点赞人和点赞状态

文章 idarticleId点赞人的 id userId点赞状态1(点赞)和0(取消点赞)文章 id 点赞人 id 作为HashKey,两个 id 中间用::隔开,点赞状态作为HashValue

2.png

整合SpringBoot

依赖

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

基础配置

spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
jedis:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0

配置类

开启Redis事务支持序列化

package cn.goitman.config;

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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;

/**
* @author Nicky
* @version 1.0
* @className RedisConfig
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Redis配置类
* @date 2022/5/16 14:40
*/
@Configuration
public class RedisConfig {

/**
* 凡事使用到template的redis操作都必须走@Transanctional注解式事务,要不然会导致连接一直占用,不关闭
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate template = new RedisTemplate();
// 改变redisTemplate的序列化方式,key为字符串格式,value为json格式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// HashKey 和 HashValue 为json格式
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
// 开启事务支持
template.setEnableTransactionSupport(true);
return template;
}

/**
* 配置事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
}

如果redisTemplate没有序列化,在可视化工具中看到的数据为乱码,获取数据时也可能为空,模糊查询(下文有叙述)功能也使用不了

1.png

Redis接口

package cn.goitman.service.impl;

import cn.goitman.pojo.Article;
import cn.goitman.pojo.Likes;
import cn.goitman.service.RedisService;
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 org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* @author Nicky
* @version 1.0
* @className RedisServiceImpl
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Redis接口实现类
* @date 2022/5/13 16:43
*/
@Service
@Transactional
public class RedisServiceImpl implements RedisService {

// 文章点赞 KEY
public static final String KEY_ARTICLE_LIKE = "ARTICLE_LIKE";

// 文章点赞数量 KEY
public static final String KEY_ARTICLE_LIKE_COUNT = "ARTICLE_LIKE_COUNT";

@Autowired
RedisTemplate redisTemplate;

/**
* 保存点赞和文章点赞量
*/
@Override
public void saveLike(String articleId, String userId) {
String field = getLikeKey(articleId, userId);
redisTemplate.opsForHash().put(KEY_ARTICLE_LIKE, field, 1);
redisTemplate.opsForHash().increment(KEY_ARTICLE_LIKE_COUNT, articleId, 1);
}

/**
* 取消点赞和文章点赞量
*/
@Override
public void unLike(String articleId, String userId) {
String field = getLikeKey(articleId, userId);
redisTemplate.opsForHash().put(KEY_ARTICLE_LIKE, field, 0);
redisTemplate.opsForHash().increment(KEY_ARTICLE_LIKE_COUNT, articleId, -1);
}

/**
* 删除点赞数据
*/
@Override
public void deleteLike(List<Likes> list) {
for (Likes like : list) {
String field = getLikeKey(like.getArticleId(), like.getUserId());
redisTemplate.opsForHash().delete(KEY_ARTICLE_LIKE, field);
}
}

/**
* 删除文章点赞量数据
*/
@Override
public void deleteLikeCount(String articleId) {
redisTemplate.opsForHash().delete(KEY_ARTICLE_LIKE_COUNT, articleId);
}

/**
* 获取全部点赞数据
*/
@Override
public List<Likes> getAllLikeData() {
List<Likes> list = new ArrayList<>();
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(KEY_ARTICLE_LIKE, ScanOptions.NONE);
while (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String keys = entry.getKey().toString();

String[] keyArr = keys.split("::");
Likes like = new Likes(keyArr[0], keyArr[1], (Integer) entry.getValue());

list.add(like);
}
return list;
}

/**
* 获取文章点赞量数据
*/
@Override
public List<Article> getArticleLikeCount() {
List<Article> list = new ArrayList<>();
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(KEY_ARTICLE_LIKE_COUNT, ScanOptions.NONE);
while (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String articleId = entry.getKey().toString();
Article article = new Article(articleId, (Integer) entry.getValue());
list.add(article);
}
return list;
}

/**
* 拼接文章ID和点赞人ID作为key
*/
private String getLikeKey(String articleId, String userId) {
return new StringBuilder().append(articleId).append("::").append(userId).toString();
}
}

搞掂,就是这么简单高效,在Redis内,存在相同数据只会修改value,并且Redis默认RDB持久化数据

当然也可加上限时内限制每个用户点赞次数的逻辑,防止恶意刷接口,逻辑简单,在此就不累述啦

有人问:”点赞功能完全用Redis替代业务数据存储,该怎么查询指定数据呢?” 模糊查询参上

模糊查询

Redis是支持通配符模糊查询的(不用通配符就是精确查找啦

*:通配任意多个字符
?:通配单个字符
[]:通配括号内的某一个字

查询Key

  • redisTemplate.keys(pattern)
/**
* 模糊查询 key
* 参数:key = ARTICLE
*/
@Override
public List<String> fuzzyQueryKey(String key) {
List<String> userIdList = (List<String>) redisTemplate.keys("*" + key + "*")
.stream()
.collect(Collectors.toList());

return userIdList; // [ARTICLE_LIKE_COUNT, ARTICLE_LIKE]
}

查询Hash数据中的HK

  • redisTemplate.opsForHash().scan(KEY,ScanOptions.scanOptions().match(pattern).build())
    /**
* 根据文章ID,查询点赞此文章的用户
* 参数:articleId = 1
*/
@Override
public List<String> fuzzyQueryHashKey(String articleId) {
List<String> userIdList = new ArrayList<>();

Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash()
.scan(KEY_ARTICLE_LIKE, ScanOptions.scanOptions()
.match("*" + articleId + "*") // 模糊匹配
// .count(100) // 可选,查询条数(默认 10)
.build());

while (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String[] keyArr = entry.getKey().toString().split("::");
userIdList.add(keyArr[1]);
}
return userIdList; // [1, 2, 3]
}

还是那句话,需要配置RedisTemplate的序列化,否则获取数据为空;

又有人说啦:”还是要固定间隔时间从Redis中捞取数据,保存在数据库中可靠点。” !@#$%^&* 业务说的都对,没办法,来吧

表设计

CREATE TABLE `article` (
`article_id` varchar(11) NOT NULL,
`like_count` int(11) NOT NULL COMMENT '点赞数量',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`article_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章表';
CREATE TABLE `likes` (
`article_id` varchar(32) NOT NULL COMMENT '被点赞的文章id',
`user_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 '修改时间',
UNIQUE KEY `likeId` (`article_id`,`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章点赞表';

POJO

package cn.goitman.pojo;

/**
* @author Nicky
* @version 1.0
* @className Article
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 文章点赞数量表
* @date 2022/5/13 16:32
*/
public class Article {

// 文章id
private String articleId;

// 文章点赞数
private Integer LikeCount;

public Article() {
}

public Article(String articleId, Integer likeCount) {
this.articleId = articleId;
LikeCount = likeCount;
}

public String getArticleId() {
return articleId;
}

public void setArticleId(String articleId) {
this.articleId = articleId;
}

public Integer getLikeCount() {
return LikeCount;
}

public void setLikeCount(Integer likeCount) {
LikeCount = likeCount;
}
}
package cn.goitman.pojo;

/**
* @author Nicky
* @version 1.0
* @className Like
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 文章点赞表
* @date 2022/5/12 20:12
*/
public class Likes {

// 文章id
private String articleId;

// 点赞用户id
private String userId;

// 点赞状态
private Integer status;

public Likes() {
}

public Likes(String articleId, String userId, Integer status) {
this.articleId = articleId;
this.userId = userId;
this.status = status;
}

public String getArticleId() {
return articleId;
}

public void setArticleId(String articleId) {
this.articleId = articleId;
}

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public Integer getStatus() {
return status;
}

public void setStatus(Integer status) {
this.status = status;
}
}

数据库操作

从Redis中获取数据保存到数据库后,删除Redis中相应数据

  • service接口实现
package cn.goitman.service.impl;

import cn.goitman.mapper.LikeDao;
import cn.goitman.pojo.Article;
import cn.goitman.pojo.Likes;
import cn.goitman.service.LikeService;
import cn.goitman.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
* @author Nicky
* @version 1.0
* @className LikeServiceImpl
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Like 接口实现类
* @date 2022/5/13 17:11
*/
@Service
@Transactional
public class LikeServiceImpl implements LikeService {

@Autowired
private RedisService redisService;

@Autowired
private LikeDao likeDao;

/**
* 保存点赞数据到数据库
*/
@Override
public void savaLikeData2DB() {
List<Likes> likeList = redisService.getAllLikeData();

if (likeList.size() > 0) {
for (Likes like : likeList) {
Likes likes = likeDao.getLikesList(like);
if (likes != null) {
likes.setStatus(like.getStatus());
likeDao.updataLike(likes);
} else {
likeDao.saveLike(like);
}
}
redisService.deleteLike(likeList);
}
}

/**
* 保存文章点赞量到数据库,Redis不持久化文章点赞量
*/
@Override
public void saveArticleLikeCount2DB() {
List<Article> articleList = redisService.getArticleLikeCount();

if (articleList.size() > 0) {
for (Article article : articleList) {
Article articleData = likeDao.getArticleData(article.getArticleId());

if (articleData != null) {
articleData.setLikeCount(articleData.getLikeCount() + article.getLikeCount());
likeDao.updataArticle(articleData);
} else {
likeDao.saveArticle(article);
}
redisService.deleteLikeCount(article.getArticleId());
}
}
}
}
  • mapper接口
package cn.goitman.mapper;

import cn.goitman.pojo.Article;
import cn.goitman.pojo.Likes;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
* @author Nicky
* @version 1.0
* @className LikeDao
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description
* @date 2022/5/19 17:13
*/
@Mapper
public interface LikeDao {

Likes getLikesList(Likes likes);

void saveLike(Likes likes);

void updataLike(Likes likes);

Article getArticleData(String articleId);

void saveArticle(Article article);

void updataArticle(Article article);
}
  • mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.goitman.mapper.LikeDao">

<select id="getLikesList" resultType="cn.goitman.pojo.Likes">
select article_id,user_id,status from likes where article_id = #{articleId} and user_id = #{userId}
</select>

<insert id="saveLike" flushCache="true">
insert into likes (article_id,user_id,status)
values (#{articleId},#{userId},#{status})
</insert>

<update id="updataLike" flushCache="true">
update likes set status = #{status} where article_id = #{articleId} and user_id = #{userId}
</update>

<select id="getArticleData" resultType="cn.goitman.pojo.Article">
select article_id,Like_count from article where article_id = #{articleId}
</select>

<insert id="saveArticle" flushCache="true">
insert into article (article_id,Like_count) values (#{articleId},#{LikeCount})
</insert>

<update id="updataArticle" flushCache="true">
update article set Like_count = #{LikeCount} where article_id = #{articleId}
</update>

</mapper>

定时任务

Scheduled方式

引导类开启Scheduling注解

package cn.goitman;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* @author Nicky
* @version 1.0
* @className RedisLikeDesignApplication
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 引导类
* @date 2022/5/12 15:55
*/
@SpringBootApplication
@EnableScheduling // 开启Scheduling注解
public class RedisLikeDesignApplication {
public static void main(String[] args) {
SpringApplication.run(RedisLikeDesignApplication.class, args);
}
}
package cn.goitman.scheduling;

import cn.goitman.service.LikeService;
import cn.goitman.task.LikeTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* @author Nicky
* @version 1.0
* @className LikeScheduling
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Scheduled 执行类
* @date 2022/5/13 15:55
*/
@Component
public class LikeScheduling {

private static Logger log = LoggerFactory.getLogger(LikeScheduling.class);

@Autowired
private LikeService likeService;

@Scheduled(cron = "0 0/1 * * * ? ")
public void likeCron() {
log.info("Scheduled 定时任务.........开始.........");
likeService.savaLikeData2DB();
likeService.saveArticleLikeCount2DB();
log.info("Scheduled 定时任务.........结束.........");
}
}

Quartz方式

  • Quartz 配置
package cn.goitman.config;

import cn.goitman.task.LikeTask;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author Nicky
* @version 1.0
* @className QuartzConfig
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Quartz 配置类
* @date 2022/5/23 11:00
*/
@Configuration
public class QuartzConfig {

private static final String LIKE_TASK_IDENTITY = "LikeTask";

@Bean
public JobDetail jobDatail() {
return JobBuilder.newJob(LikeTask.class) // 对应Job
.withIdentity(LIKE_TASK_IDENTITY) // 给JobDetail起个id
.storeDurably() // 即使没有Trigger关联时,也不需要删除该JobDetail
.build();
}

@Bean
public Trigger trigger() {
// 定时任务配置(SimpleScheduleBuilder 简单构建器、CronScheduleBuilder Cron构建器)
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
// .withIntervalInSeconds() // 以"秒"为单位执行
// .withIntervalInHours() // 以"时"为单位执行
// .withRepeatCount(1) // 执行次数(如果没配置任务开始时间,会在创建触发器时就触发一次(n+1))
.withIntervalInMinutes(1) // 以"分"为单位执行
.repeatForever(); // 指定触发器无限期重复

// CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0 */1 * * * ?");
return TriggerBuilder.newTrigger()
.forJob(jobDatail()) // 关联JobDetail
.withIdentity(LIKE_TASK_IDENTITY) // 给Trigger起个id
.withSchedule(simpleScheduleBuilder) // 关联任务配置
// .withSchedule(cronScheduleBuilder)
.build();
}
}
  • Quartz执行类
package cn.goitman.task;

import cn.goitman.listener.ApplicationListens;
import cn.goitman.service.LikeService;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
* @author Nicky
* @version 1.0
* @className LikeTask
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description Quartz 执行类
* @date 2022/5/23 11:26
*/
public class LikeTask extends QuartzJobBean {

private static Logger log = LoggerFactory.getLogger(LikeTask.class);

@Autowired
LikeService likeService;

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Quartz定时任务.........开始.........");
likeService.savaLikeData2DB();
likeService.saveArticleLikeCount2DB();
log.info("Quartz定时任务.........结束.........");
}
}

两种方法任选其一即可,完全看眼缘啦……

钩子函数

在项目开发或运行中,可能会遇到如随应用启动后或关闭前处理某些逻辑服务器突然断电(指有备用电缓冲下)防止数据丢失等的场景,这时钩子函数(回调函数)起到了决定性作用

package cn.goitman.listener;

import cn.goitman.service.LikeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
* @author Nicky
* @version 1.0
* @className ApplicationListens
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 监听程序启动与关闭,回调钩子
* CommandLineRunner接口:当应用启动成功后的回调
* DisposableBean接口:当应用正要被销毁前的回调
* @date 2022/5/12 15:57
*/
//@Component
public class ApplicationListens implements CommandLineRunner, DisposableBean {

private static Logger log = LoggerFactory.getLogger(ApplicationListens.class);

@Autowired
private LikeService likeService;

/**
* 启动后回调
*/
@Override
public void run(String... args) throws Exception {

}

/**
* 关闭前回调
*/
@Override
public void destroy() throws Exception {
log.info("程序关闭,钩子回调.........开始.........");
likeService.savaLikeData2DB();
likeService.saveArticleLikeCount2DB();
log.info("程序关闭,钩子回调.........结束.........");
}
}

源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/redisLikeDesign-demo