项目地址:http://notebook.wzl1.top/
在项目开发中,点赞事件频率较高,我们不可能直接将对点赞功能的操作放到MySQL里面,所以我们引入Redis中间件。
大概的思路是这样
但是很明显,在持久化的时候如果我们同时有点赞数据如何处理,因为在持久化的时候后,我打算对redis进行清空记录用户点赞信息列表,因为我认为这对点赞来说是一种无效资源,而只有点赞次数才是有效的,所以在这里我想了下,可以用锁来解决。
虽然这里可以用synchronized和Lock等单体锁来实现,但在未来我如果打算做成集群的话,单体锁明显不是一种好的选择(多个JVM),在这里引入分布式锁。
分布式锁的实现方式
- 基于数据库实现分布式锁;
- 基于缓存(Redis等)实现分布式锁;
- 基于Zookeeper实现分布式锁;
这里我们选择使用Redis解决分布式锁
为什么选择Redis实现分布式锁
1、选用Redis实现分布式锁原因:
(1)Redis有很高的性能; (2)Redis命令对此支持较好,实现起来比较方便
2、使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
3、实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)释放锁的时候,通过定时任务名判断是不是该锁,若是该锁,则执行delete进行锁释放。
(3)可以在对获取锁的过程加个exptime,但是这里我不做实现
3、使用技术栈
SpringBoot、MyBatisPlus、SpringDataRedis
LockUtil工具类
这里实现分布式锁的代码,我们采用自实现枚举单例模式,防止反射攻击
代码语言:javascript复制/**
* Title
*
* @ClassName: LockUtil
* @Description:锁工具类,通过内部枚举类实现单例,防止反射攻击
* @author: Karos
* @date: 2023/1/4 0:17
* @Blog: https://www.wzl1.top/
*/
package com.karos.KaTool.lock;
import cn.hutool.core.util.BooleanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
@Scope("prototype")
@Slf4j
public class LockUtil {
@Resource
RedisTemplate redisTemplate;
private LockUtil(){
}
//加锁
public boolean DistributedLock(Object obj,Long exptime,TimeUnit timeUnit){
//线程被锁住了,就一直等待
DistributedAssert(obj);
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("Lock:" obj.toString(), "1", exptime, timeUnit);
log.info("KaTool=> LockUntil => DistributedLock:{} value:{} extime:{} timeUnit:{}",obj.toString(), "1", exptime, timeUnit);
return BooleanUtil.isTrue(aBoolean);
}
//检锁
public void DistributedAssert(Object obj){
while(true){
Object o = redisTemplate.opsForValue().get("Lock:" obj.toString());
if (ObjectUtils.isEmpty(o))return;
}
}
//延期
public boolean delayDistributedLock(Object obj,Long exptime,TimeUnit timeUnit){
Boolean aBoolean = redisTemplate.opsForValue().setIfPresent("Lock:" obj.toString(), "1", exptime, timeUnit);
log.info("KaTool=> LockUntil => delayDistributedLock:{} value:{} extime:{} timeUnit:{}",obj.toString(), "1", exptime, timeUnit);
return BooleanUtil.isTrue(aBoolean);
}
//释放锁
public boolean DistributedUnLock(Object obj){
Boolean aBoolean = redisTemplate.delete("Lock:" obj.toString());
log.info("KaTool=> LockUntil => unDistributedLock:{} isdelete:{} ",obj.toString(),true);
return BooleanUtil.isTrue(aBoolean);
}
//利用枚举类实现单例模式,枚举类属性为静态的
private enum SingletonFactory{
Singleton;
LockUtil lockUtil;
private SingletonFactory(){
lockUtil=new LockUtil();
}
public LockUtil getInstance(){
return lockUtil;
}
}
@Bean
public static LockUtil getInstance(){
return SingletonFactory.Singleton.lockUtil;
}
}
分布式锁接口测试
代码语言:javascript复制 //权限校验
@AuthCheck(mustRole = "admin")
@GetMapping("/LockTest")
public BaseResponse<String> test(@RequestParam("expTime") Long expTime){
lockUtil.DistributedLock(RedisKeysConstant.ThumbsHistoryHash.intern(),expTime, TimeUnit.SECONDS);
return ResultUtils.success("上锁成功,请在20s内进行测试操作");
}
点赞Redis存储数据模型
点赞代码实现(只放具体代码,更多代码在最下放github中查看)
在后面的业务中,我改成了收藏功能,其实实现的原理也是一样的
代码语言:javascript复制 @AuthCheck
@PostMapping("/thumb")
public BaseResponse<Boolean> thumbNote(@RequestBody NoteDoThumbRequest noteDoThumbRequest, HttpServletRequest request){
Notethumbrecords notethumbrecords = new Notethumbrecords();
notethumbrecords.setNoteId(noteDoThumbRequest.getNoteId());
notethumbrecords.setThumbTime(new Date());
Boolean result = notethumbrecordsService.thumb(notethumbrecords, request);
if (result==null){
throw new BusinessException(ErrorCode.OPERATION_ERROR);
}
return ResultUtils.success(result,()->{
if (BooleanUtil.isTrue(result)) {
return "已放入收藏夹";
}
if (BooleanUtil.isFalse(result)){
return "已取消收藏";
}
return "收藏夹服务错误";
});
}
@Override
public Boolean thumb(Notethumbrecords entity, HttpServletRequest request) {
if (StringUtils.isAnyBlank(entity.getNoteId())){
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Long userId = entity.getUserId();
if (ObjectUtils.isEmpty(userId)){
if (ObjectUtils.isEmpty(request))
throw new BusinessException(ErrorCode.PARAMS_ERROR,"无法获取登录用户");
//获取当前登录用户
User loginUser = userService.getLoginUser(request);
entity.setUserId(loginUser.getId());
userId = entity.getUserId();
}
HashOperations hashOperations = redisTemplate.opsForHash();
SetOperations setOperations = redisTemplate.opsForSet();
//分布式锁校验,如果在这个时候在进行点赞数量持久化,那就等待
lockUtil.DistributedAssert(RedisKeysConstant.ThumbsHistoryHash.intern());
String userAccount=userService.getLoginUser(request).getUserAccount();
synchronized (userAccount.intern()) {
List list = (List) hashOperations.get(RedisKeysConstant.ThumbsHistoryHash, String.valueOf(userId));
if (ObjectUtils.isEmpty(list)) list = new ArrayList<Notethumbrecords>();
//如果点过赞那么取消,并且返回true
Integer o = (Integer) hashOperations.get(RedisKeysConstant.ThumbsNum, entity.getNoteId());
boolean contains = list.contains(entity);
if (BooleanUtil.isTrue(contains)) {
list.remove(entity);
Long delete = hashOperations.delete(RedisKeysConstant.ThumbsHistoryHash, String.valueOf(userId));
hashOperations.put(RedisKeysConstant.ThumbsHistoryHash, String.valueOf(userId), list);
hashOperations.increment(RedisKeysConstant.ThumbsNum, entity.getNoteId(), -1);
return !(delete == 1L);
}
//把实体存入Redis缓存中
list.add(entity);
setOperations.add(RedisKeysConstant.ThumbsUserSet, entity.getUserId());
hashOperations.put(RedisKeysConstant.ThumbsHistoryHash, String.valueOf(userId), list);
hashOperations.increment(RedisKeysConstant.ThumbsNum, entity.getNoteId(), 1);
return true;
}
定时任务持久化代码
我设置的五个小时的定时任务
代码语言:javascript复制 /**
* 点赞信息持久化
*/
@Scheduled(cron = "0 0 0/5 * * ? ")
public void PersistenceThumbs(){
//加锁
lockUtil.DistributedLock(LockConstant.ThumbsLock_Pers,10L, TimeUnit.SECONDS);
long beginTime = DateUtil.currentSeconds();
//持久化
//list 用于获取点赞的用户
SetOperations setOperations = redisTemplate.opsForSet();
//hash 用于获取用户点赞数据
HashOperations hashOperations = redisTemplate.opsForHash();
//从缓存中取出点赞过的用户ID
Long usersetsize = setOperations.size(ThumbsUserSet);
//如果没有人点赞,那就释放锁,并且退出
if (usersetsize<=0){
lockUtil.DistributedUnLock(LockConstant.ThumbsLock_Pers.intern());
return;
}
Set members = setOperations.members(ThumbsUserSet);
Set<String> userlist =new HashSet<>();
for(Object it:members){
userlist.add(it.toString());
if (DateUtil.currentSeconds()-beginTime<5) {
lockUtil.delayDistributedLock(LockConstant.ThumbsLock_Pers,10L, TimeUnit.SECONDS);
}
}
//清楚点过赞的用户
redisTemplate.delete(ThumbsUserSet);
ArrayList<CompletableFuture<Void> > futrueList=new ArrayList<>();
//获取所有用户点赞过的列表
List<List<Notethumbrecords>> thumblist = hashOperations.multiGet(ThumbsHistoryHash, userlist);
Map entries = hashOperations.entries(RedisKeysConstant.ThumbsNum);
int i=0;
int j=0;
Set set = entries.keySet();
Iterator iterator = set.iterator();
int size=set.size();
while(true){
if (j>=thumblist.size())break;
ArrayList<Notethumbrecords> historyList=new ArrayList<>();
ArrayList<Note> countList=new ArrayList<>();
while(j<thumblist.size()&&(j==0||j00!=0)) {
List<Notethumbrecords> e = thumblist.get(j);
if (e==null) break;
CollectionUtil.addAll(historyList,e);
j ;
}
while(iterator.hasNext()&&(i==0||i00!=0)){
String noteID = (String) iterator.next();
Long thumbNum=((Integer) entries.get(noteID)).longValue();
Note temp=new Note();
temp.setId(noteID);
temp.setThumbNum(thumbNum);
countList.add(temp);
i ;
}
//开启多线程
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
//将点赞数据持久化到mysql
notethumbrecordsService.saveOrUpdateBatch(historyList, (historyList.size()/3) 1);
noteService.updateBatchById(countList,(countList.size()/3) 1);
});
futrueList.add(future);
if (DateUtil.currentSeconds()-beginTime<5) {
lockUtil.delayDistributedLock(LockConstant.ThumbsLock_Pers,10L, TimeUnit.SECONDS);
}
}
CompletableFuture.allOf(futrueList.toArray(new CompletableFuture[]{})).join();
// notethumbrecordsService.saveOrUpdateBatch(thumblist,10000);
ArrayList<Note> list = (ArrayList<Note>) noteService.list();
for (Note k:list){
hashOperations.put(RedisKeysConstant.ThumbsNum,k.getId(),k.getThumbNum());
}
//释放锁
lockUtil.DistributedUnLock(LockConstant.ThumbsLock_Pers.intern());
}
获取用户点赞列表
代码语言:javascript复制 @AuthCheck
@GetMapping("/list/myfavorite")
public BaseResponse<Page<NoteVo>> listNoteByFavorite(HttpServletRequest request){
User loginUser = userService.getLoginUser(request);
Long id = loginUser.getId();
String userName = loginUser.getUserName();
String userAccount = loginUser.getUserAccount();
String userAvatar = loginUser.getUserAvatar();
Integer gender = loginUser.getGender();
String userRole = loginUser.getUserRole();
String userPassword = loginUser.getUserPassword();
Date createTime = loginUser.getCreateTime();
Date updateTime = loginUser.getUpdateTime();
String userMail = loginUser.getUserMail();
Integer isDelete = loginUser.getIsDelete();
HashOperations hashOperations = redisTemplate.opsForHash();
List<Notethumbrecords> list = (List) hashOperations.get(RedisKeysConstant.ThumbsHistoryHash, id.toString());
//如果缓存中有,那么从缓存里面取
if (list==null||list.size()<=0){
QueryWrapper<Notethumbrecords> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("userId",id);
list=notethumbrecordsService.list(queryWrapper);
//把list存到redis
hashOperations.put(RedisKeysConstant.ThumbsHistoryHash,id.toString(),list);
}
Page<Notethumbrecords> notethumbrecordsPage=new Page<>(0,list.size());
notethumbrecordsPage.setRecords(list);
List<Notethumbrecords> finalList = list;
Page<NoteVo> voList=(Page<NoteVo>) notethumbrecordsPage.convert(u->{
NoteVo v=new NoteVo();
Note a=noteService.getById(u.getNoteId());
BeanUtils.copyProperties(a,v);
Boolean thumb=false;
if (ObjectUtil.isNotEmpty(finalList)){
Iterator<Notethumbrecords> iterator = finalList.iterator();
while(iterator.hasNext()){
Notethumbrecords next = iterator.next();
if (next.getNoteId().equals(v.getId())){
thumb=true;
break;
}
}
}
v.setHasThumb(thumb);
if (hashOperations.hasKey(RedisKeysConstant.ThumbsNum,v.getId()))
v.setThumbNum(Long.valueOf((Integer)hashOperations.get(RedisKeysConstant.ThumbsNum,v.getId())));
return v;
});
return ResultUtils.success(voList);
}
后端源码:
GitHub
karosown/notebook-backyand
前端源码:
GitHub
karosown/notebook-frontyand
工具类Starter源码
GitHub
Karosown/KaTool