首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【掌印日记-点赞功能实现】使用Redis实现分布式锁

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

作者头像
Karos
发布2023-01-16 08:03:39
9610
发布2023-01-16 08:03:39
举报
文章被收录于专栏:MyBlog-KarosMyBlog-Karos

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

这里实现分布式锁的代码,我们采用自实现枚举单例模式,防止反射攻击

/**
 * 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;
        }
}

分布式锁接口测试

    //权限校验
    @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中查看)

在后面的业务中,我改成了收藏功能,其实实现的原理也是一样的

    @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;
        }

定时任务持久化代码

我设置的五个小时的定时任务

    /**
     * 点赞信息持久化
     */
    @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||j%1000!=0)) {
                List<Notethumbrecords> e = thumblist.get(j);
                if (e==null) break;
                CollectionUtil.addAll(historyList,e);
                j++;
            }
            while(iterator.hasNext()&&(i==0||i%1000!=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());
    }

获取用户点赞列表

    @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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-1-16 2,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 分布式锁的实现方式
  • 为什么选择Redis实现分布式锁
    • 1、选用Redis实现分布式锁原因:
      • 2、使用命令介绍:
        • 3、实现思想:
          • 3、使用技术栈
          • LockUtil工具类
            • 分布式锁接口测试
            • 点赞Redis存储数据模型
            • 点赞代码实现(只放具体代码,更多代码在最下放github中查看)
            • 定时任务持久化代码
            • 获取用户点赞列表
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档