点赞这种需求还算是很常见的,其大体流程也是很容易想明白的。因为类似于点赞这种操作,如果用户比较闲,就是一顿点...点一下我就操作一下数据库,取消一下我再操作一下数据库......所以具体实现思路是:
用户点“点赞”按钮
redis存储这个“赞”
用户取消“赞”
redis随之取消“赞”
一定时间后,系统将这些“赞”做持久化
思路是这样的,具体实现也是比较容易的:
redis缓存相关
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
在maven引入依赖后,对redis进行相关配置
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; } }
配置文件也要写一下:
spring.redis.host=127.0.0.1 spring.redis.port: 6379
-
定时任务相关
一样的,引入定时的依赖:<!--定时任务--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
并配置:
import com.hanor.blog.quartz.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(60) //设置时间周期单位秒,这样效果更明显 //.withIntervalInHours(2) //两个小时执行一次 .repeatForever(); return TriggerBuilder.newTrigger().forJob(quartzDetail()) .withIdentity(LIKE_TASK_IDENTITY) .withSchedule(scheduleBuilder) .build(); } }
制定任务:
```java import com.hanor.blog.service.LikedService; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; /** * 点赞的定时任务 */ public class LikeTask extends QuartzJobBean { @Autowired private LikedService likedService; private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("-----------quartz------------"); //将 Redis 里的点赞信息同步到数据库里 likedService.transLikedFromRedis2DB(); likedService.transLikedCountFromRedis2DB(); } }
-
数据库表结构的设计
因为博客项目算是个小项目了,这里为了演示方便,点赞这个模块就先以简易为主。
liked_user_id为被赞者,liked_post_id为发出者。
import com.hanor.blog.entity.enums.LikedStatusEnum;
/**
* 用户点赞表
*/
public class UserLike {
//主键id
private String likeId;
//被点赞的用户的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;
}
//getter setter
}
其中,用了枚举。
/**
* 用户点赞的状态
*/
public enum LikedStatusEnum {
/**
* 点赞
*/
LIKE(1, "点赞"),
/**
* 取消赞
*/
UNLIKE(0, "取消点赞/未点赞");
private Integer code;
private String msg;
LikedStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode(){
return this.code;
}
public String getMsg(){
return this.msg;
}
}
最终的表,是这个样子的:
-
具体实现业务逻辑
这里有两点:第一是,先把用户的“赞”存在缓存层;第二,适当的时间,将缓存的数据拿出,进行持久化操作。
考虑到redis存储的特点,选用hash的形式对“用户点赞操作”及“用户被点赞数量”两项进行存储。采用hash的具体原因:把点赞造成的不同影响,储存为不同分区,方便管理。
@程序猿DD
因为 Hash 里的数据都是存在一个键里,可以通过这个键很方便的把所有的点赞数据都取出。
这个键里面的数据还可以存成键值对的形式,方便存入点赞人、被点赞人和点赞状态。
第一,先把用户的“赞”存在缓存层。
import com.hanor.blog.entity.DTO.LikedCountDTO;
import com.hanor.blog.entity.enums.LikedStatusEnum;
import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.RedisService;
import com.hanor.blog.util.RedisKeyUtils;
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
public class RedisServiceImpl implements RedisService {
@Autowired
RedisTemplate redisTemplate;
@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;
}
}
第二,持久化操作。
import com.alibaba.fastjson.JSONObject;
import com.hanor.blog.dao.BlogArticleMapper;
import com.hanor.blog.dao.UserLikeMapper;
import com.hanor.blog.entity.DTO.LikedCountDTO;
import com.hanor.blog.entity.pojo.BlogArticle;
import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.LikedService;
import com.hanor.blog.service.RedisService;
import com.hanor.blog.util.IdUtil;
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 java.util.Date;
import java.util.List;
@Service
public class LikedServiceImpl implements LikedService {
@Autowired
private RedisService redisService;
@Autowired
private UserLikeMapper userLikeMapper;
@Autowired
private BlogArticleMapper blogArticleMapper;
@Override
public int save(UserLike userLike) {
return userLikeMapper.saveLike(userLike);
}
@Override
public void saveAll(List<UserLike> list) {
for (UserLike userLike : list) {
userLikeMapper.saveLike(userLike);
}
}
@Override
public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
return null;
}
@Override
public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
return null;
}
@Override
public int getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
UserLike userLike = new UserLike();
userLike.setLikedPostId(likedPostId);
userLike.setLikedUserId(likedUserId);
return userLikeMapper.searchLike(userLike);
}
@Override
public void transLikedFromRedis2DB() {
List<UserLike> userLikeList = redisService.getLikedDataFromRedis();
for (UserLike like : userLikeList) {
Integer userLikeExist = userLikeMapper.searchLike(like);
if (userLikeExist > 0){
userLikeMapper.updateLike(like);
}else {
like.setLikeId(IdUtil.nextId() + "");
userLikeMapper.saveLike(like);
}
}
}
@Override
public void transLikedCountFromRedis2DB() {
List<LikedCountDTO> likedCountDTOs = redisService.getLikedCountFromRedis();
for (LikedCountDTO dto : likedCountDTOs) {
JSONObject blogArticle = blogArticleMapper.getArticleById(dto.getUserId());
if (null != blogArticle){
BlogArticle article = new BlogArticle();
article.setUpdateTime(new Date());
article.setArticleId(blogArticle.getString("articleId"));
article.setArticleLike(blogArticle.getInteger("articleLike") + dto.getLikedNum());
blogArticleMapper.updateArticle(article);
}else {
return;
}
}
}
}
- 用到的工具类
对点赞信息进行redis储存的id生成:
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();
}
}
因为想做一个分布式项目,所以项目用到的id生成策略采用了雪花算法,代码过长,就不贴了。
测试,给测试来个接口,用postman测吧。
import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/like")
public class LikeController {
@Autowired
private RedisService redisService;
@PostMapping
public void doLike(@RequestBody UserLike userLike){
redisService.saveLiked2Redis(userLike.getLikedUserId(),userLike.getLikedPostId());
redisService.incrementLikedCount(userLike.getLikedPostId());
}
发送值为:
{
"likedUserId":"123",
"likedPostId":"456"
}
此时缓存中可见:
![image](https://upload-images.jianshu.io/upload_images/19860184-50ad4050f1d8cc3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
过了一会,缓存数据将被存进数据库中:缓存中没有数据,且值被写入数据库。
![image](https://upload-images.jianshu.io/upload_images/19860184-c0ad37bfc244af80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
至此,点赞完成!!!撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。