【掌印日记-点赞功能实现】使用Redis实现分布式锁

2023-01-16 08:03:39 浏览数 (1)

项目地址: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

0 人点赞