點(diǎn)贊這種需求還算是很常見(jiàn)的季惩,其大體流程也是很容易想明白的邀摆。因?yàn)轭愃朴邳c(diǎn)贊這種操作签财,如果用戶比較閑,就是一頓點(diǎn)...點(diǎn)一下我就操作一下數(shù)據(jù)庫(kù)淹朋,取消一下我再操作一下數(shù)據(jù)庫(kù)......所以具體實(shí)現(xiàn)思路是:
用戶點(diǎn)“點(diǎn)贊”按鈕
redis存儲(chǔ)這個(gè)“贊”
用戶取消“贊”
redis隨之取消“贊”
一定時(shí)間后笙各,系統(tǒng)將這些“贊”做持久化
思路是這樣的,具體實(shí)現(xiàn)也是比較容易的:
redis緩存相關(guān)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
在maven引入依賴后础芍,對(duì)redis進(jìn)行相關(guān)配置
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
-
定時(shí)任務(wù)相關(guān)
一樣的杈抢,引入定時(shí)的依賴:<!--定時(shí)任務(wù)--> <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) //設(shè)置時(shí)間周期單位秒,這樣效果更明顯 //.withIntervalInHours(2) //兩個(gè)小時(shí)執(zhí)行一次 .repeatForever(); return TriggerBuilder.newTrigger().forJob(quartzDetail()) .withIdentity(LIKE_TASK_IDENTITY) .withSchedule(scheduleBuilder) .build(); } }
制定任務(wù):
```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; /** * 點(diǎn)贊的定時(shí)任務(wù) */ 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 里的點(diǎn)贊信息同步到數(shù)據(jù)庫(kù)里 likedService.transLikedFromRedis2DB(); likedService.transLikedCountFromRedis2DB(); } }
-
數(shù)據(jù)庫(kù)表結(jié)構(gòu)的設(shè)計(jì)
因?yàn)椴┛晚?xiàng)目算是個(gè)小項(xiàng)目了,這里為了演示方便仑性,點(diǎn)贊這個(gè)模塊就先以簡(jiǎn)易為主惶楼。
liked_user_id為被贊者,liked_post_id為發(fā)出者诊杆。
import com.hanor.blog.entity.enums.LikedStatusEnum;
/**
* 用戶點(diǎn)贊表
*/
public class UserLike {
//主鍵id
private String likeId;
//被點(diǎn)贊的用戶的id
private String likedUserId;
//點(diǎn)贊的用戶的id
private String likedPostId;
//點(diǎn)贊的狀態(tài).默認(rèn)未點(diǎn)贊
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
}
其中歼捐,用了枚舉。
/**
* 用戶點(diǎn)贊的狀態(tài)
*/
public enum LikedStatusEnum {
/**
* 點(diǎn)贊
*/
LIKE(1, "點(diǎn)贊"),
/**
* 取消贊
*/
UNLIKE(0, "取消點(diǎn)贊/未點(diǎn)贊");
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;
}
}
最終的表晨汹,是這個(gè)樣子的:
-
具體實(shí)現(xiàn)業(yè)務(wù)邏輯
這里有兩點(diǎn):第一是豹储,先把用戶的“贊”存在緩存層;第二宰缤,適當(dāng)?shù)臅r(shí)間颂翼,將緩存的數(shù)據(jù)拿出晃洒,進(jìn)行持久化操作。
考慮到redis存儲(chǔ)的特點(diǎn)朦乏,選用hash的形式對(duì)“用戶點(diǎn)贊操作”及“用戶被點(diǎn)贊數(shù)量”兩項(xiàng)進(jìn)行存儲(chǔ)球及。采用hash的具體原因:把點(diǎn)贊造成的不同影響,儲(chǔ)存為不同分區(qū)呻疹,方便管理吃引。
@程序猿DD
因?yàn)?Hash 里的數(shù)據(jù)都是存在一個(gè)鍵里,可以通過(guò)這個(gè)鍵很方便的把所有的點(diǎn)贊數(shù)據(jù)都取出刽锤。
這個(gè)鍵里面的數(shù)據(jù)還可以存成鍵值對(duì)的形式镊尺,方便存入點(diǎn)贊人、被點(diǎn)贊人和點(diǎn)贊狀態(tài)并思。
第一庐氮,先把用戶的“贊”存在緩存層。
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 對(duì)象
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();
//將點(diǎn)贊數(shù)量存儲(chǔ)在 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;
}
}
}
}
- 用到的工具類
對(duì)點(diǎn)贊信息進(jìn)行redis儲(chǔ)存的id生成:
public class RedisKeyUtils {
//保存用戶點(diǎn)贊數(shù)據(jù)的key
public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
//保存用戶被點(diǎn)贊數(shù)量的key
public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";
/**
* 拼接被點(diǎn)贊的用戶id和點(diǎn)贊的人的id作為key输涕。格式 222222::333333
* @param likedUserId 被點(diǎn)贊的人id
* @param likedPostId 點(diǎn)贊的人的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();
}
}
因?yàn)橄胱鲆粋€(gè)分布式項(xiàng)目音婶,所以項(xiàng)目用到的id生成策略采用了雪花算法,代碼過(guò)長(zhǎng)莱坎,就不貼了衣式。
測(cè)試,給測(cè)試來(lái)個(gè)接口檐什,用postman測(cè)吧碴卧。
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());
}
發(fā)送值為:
{
"likedUserId":"123",
"likedPostId":"456"
}
此時(shí)緩存中可見(jiàn):

過(guò)了一會(huì),緩存數(shù)據(jù)將被存進(jìn)數(shù)據(jù)庫(kù)中:緩存中沒(méi)有數(shù)據(jù)乃正,且值被寫入數(shù)據(jù)庫(kù)螟深。

至此,點(diǎn)贊完成L淘帷!凡蜻!撒花★,°:.☆( ̄▽ ̄)/$:.°★ 搭综。