七(8)访客-FastDFS-小视频-SpringCache

课程总结

1.业务功能

我的访客

小视频功能

2.FastDFS

分布式文件存储服务

两个角色Tracker Server和Storage Server

工作流程

核心代码

3.通用缓存SpringCache

内部原理:AOP + 自定义注解

使用的三个步骤

一. 我的访客

查询别人来访了我的主页的信息,其他用户在浏览我的主页时,需要记录访客数据。访客在一天内每个用户只记录一次。

查询数据时,如果用户查询过列表,就需要记录这次查询数据的时间,下次查询时查询大于等于该时间的数据。

如果,用户没有记录查询时间,就查询最近的5个来访用户。

1-1 保存访客记录

修改接口, 在查看佳人详情时保存访客数据

1. 数据库表

visirots(访客记录表)

image-20220928090441254

同一个访客,当天只能有一条访问记录

date字段:来访时间毫秒数,visitDate:来访日期

2. 实体对象
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "visitors")
public class Visitors implements java.io.Serializable{

    private static final long serialVersionUID = 2811682148052386573L;

    private ObjectId id;
    private Long userId; //我的id
    private Long visitorUserId; //来访用户id
    private String from; //来源,如首页、圈子等
    private Long date; //来访时间
    private String visitDate;//来访日期
    private Double score; //得分

}
2. TanhuaService
    //查看佳人详情
    public TodayBest personalInfo(Long userId) {
        //1、根据用户id查询,用户详情
        UserInfo userInfo = userInfoApi.findById(userId);
        //2、根据操作人id和查看的用户id,查询两者的推荐数据
        RecommendUser user = recommendUserApi.queryByUserId(userId,UserHolder.getUserId());

        //构造访客数据,调用API保存
        Visitors visitors = new Visitors();
        visitors.setUserId(userId);
        visitors.setVisitorUserId(UserHolder.getUserId());
        visitors.setFrom("首页");
        visitors.setDate(System.currentTimeMillis());
        visitors.setVisitDate(new SimpleDateFormat("yyyyMMdd").format(new Date()));
        visitors.setScore(user.getScore());
        visitorsApi.save(visitors);

        //3、构造返回值
        return TodayBest.init(userInfo,user);
    }
3. VisitorsApiImpl
    //对于同一个用户,一天之内只能保存一次访客数据
	public void save(Visitors visitors) {
        //1、查询访客数据
        Query query = Query.query(Criteria.where("userId").is(visitors.getUserId())
                .and("visitorUserId").is(visitors.getVisitorUserId())
                .and("visitDate").is(visitors.getVisitDate()));
        //2、不存在,保存
        if(!mongoTemplate.exists(query,Visitors.class)) {
            mongoTemplate.save(visitors);
        }
    }

1-2 首页谁看过我

1. 接口文档

image-20220928091916829

2. 业务流程

image-20220928092803199

  1. 查询Redis获取上次访问时间(毫秒数)

  2. 调用API查询

  3. 判断是否存在上次访问时间,拼接条件查询

  4. Redis采用Hash结构,HashKey是当前用户ID

image-20220928093012840

3. VO对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitorsVo {

    private Long id; //用户id
    private String avatar;
    private String nickname;
    private String gender; //性别 man woman
    private Integer age;
    private String[] tags;
    private Long fateValue; //缘分值

    /**
     * 在vo对象中,补充一个工具方法,封装转化过程
     */
    public static VisitorsVo init(UserInfo userInfo, Visitors visitors) {
        VisitorsVo vo = new VisitorsVo();
        BeanUtils.copyProperties(userInfo,vo);
        if(userInfo.getTags() != null) {
            vo.setTags(userInfo.getTags().split(","));
        }
        vo.setFateValue(visitors.getScore().longValue());
        return vo;
    }
}
4. MovementController
    /**
     * 谁看过我
     */
    @GetMapping("visitors")
    public ResponseEntity queryVisitorsList(){
        List<VisitorsVo> list = movementService.queryVisitorsList();
        return ResponseEntity.ok(list);
    }
5. MovementService
    //首页-访客列表
    public List<VisitorsVo> queryVisitorsList() {
        //1、查询上次访问时间
        String key = Constants.VISITORS_USER;
        String hashKey = String.valueOf(UserHolder.getUserId());
        String value = (String) redisTemplate.opsForHash().get(key, hashKey);
        Long date = StringUtils.isEmpty(value) ? null:Long.valueOf(value);
        //2、调用API查询数据列表 List<Visitors>
        List<Visitors> list =  visitorsApi.queryMyVisitors(date,UserHolder.getUserId());
        if(CollUtil.isEmpty(list)) {
            return new ArrayList<>();
        }
        //3、提取用户的id
        List<Long> userIds = CollUtil.getFieldValues(list, "visitorUserId", Long.class);
        //4、查看用户详情
        Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);
        //5、构造返回
        List<VisitorsVo> vos = new ArrayList<>();
        for (Visitors visitors : list) {
            UserInfo userInfo = map.get(visitors.getVisitorUserId());
            if(userInfo != null) {
                VisitorsVo vo = VisitorsVo.init(userInfo, visitors);
                vos.add(vo);
            }
        }
        return vos;
    }
6. API实现

VisitorsApiImpl

    //查询首页访客列表
    public List<Visitors> queryMyVisitors(Long date, Long userId) {
        Criteria criteria = Criteria.where("userId").is(userId);
        if(date != null) {
            criteria.and("date").gt(date);
        }
        Query query = Query.query(criteria).limit(5).with(Sort.by(Sort.Order.desc("date")));
        return mongoTemplate.find(query,Visitors.class);
    }

二. FastDFS

2-1 FastDFS介绍

视频存储

  • 阿里云OSS(视频简单,贵!!!)
  • 自建存储系统

对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。

  • 对于存储而言,小视频的存储量以及容量都是非常巨大的
    • 所以我们选择自己搭建分布式存储系统 FastDFS进行存储
  • 对于推荐算法,我们将采用多种权重的计算方式进行计算
  • 对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速,当然了这需要额外购买CDN服务

FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

FastDFS 架构包括 Tracker server 和 Storage server。

Storage Server:文件存储服务器

Tracker Server:追踪调度服务器

image-20220928101832152

单一Storage Server具有容量限制,如何解决?

答案:扩容分组

image-20220928101911960

每组存放一部分文件,支持海量文件。组内是单一节点服务器,存在单点故障问题,如何解决?

答案:组内构建存储服务器集群

image-20220928101949371

客户端如何访问?

答案:通过Tracker Server进行追踪调度

image-20220928102017768

文件存储到Storage server服务器上。为了方便http访问调用,每个Storage server还要绑定一个nginx

  • Tracker server

    • 配置集群
    • Tracker server监控各个Storage server,调度存储服务
  • Storage server

    • Storage server(存储服务器),文件最终存放的位置
    • 通过Group(组),拓展文件存储容量
    • 各个Group(组)中,通过集群解决单点故障

2-2 工作原理

  • 文件上传

    • Storage Server 向Tracker Server, 汇报当前存储节点的状态信息(包括磁盘剩余空间、文件同步状况等统计信息)
    • 客户端程序连接Tracker Server发给上传请求
    • Tracker Server计算可用的Storage Server 节点,返回
    • 客户端将文件上传到Storage Server,并获取返回的file_id(包括路径信息和文件名称)
    • 客户端保存请求地址
  • 文件下载

    • 和文件上传类似
    • 文件下载使用频率并不高,由于客户端记录的访问地址,直接拼接地址访问即可
1. 文件的上传

image-20220928102423943

2. 文件的下载

image-20220928102445597

2-3 入门案例

使用FastDFS完成文件上传

1. 搭建服务

我们使用docker进行搭建。目前所有的组件全部以docker的形式配置

#进入目录
cd /root/docker-file/fastdfs/
#启动
docker-compose up -d
#查看容器
docker ps -a

tracker服务器地址:192.168.136.160:22122

storage中nginx地址:http://192.168.136.160:8888/

2. java client
  • 导入依赖(已经存在,被注释)
  • 在application.yml中配置Fastdfs
  • 注入FastFileStorageClient对象,完成文件上传

导入依赖:

找到tanhua-server的pom文件,打开fastdfs的依赖如下

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <version>1.26.7</version>
    <exclusions>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>
application.yml
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs:
  so-timeout: 1500
  connect-timeout: 600
  #缩略图生成参数
  thumb-image:
    width: 150
    height: 150
  #TrackerList参数,支持多个
  tracker-list: 192.168.136.160:22122
  #nginx访问路径
  web-server-url: http://192.168.136.160:8888/
测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TanhuaServerApplication.class)
public class TestFastDFS {

    //测试将文件上传到FastDFS文件系统中

    //从调度服务器获取,一个目标存储服务器,上传
    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;// 获取存储服务器的请求URL

    @Test
    public void testFileUpdate() throws FileNotFoundException {
 		//1、指定文件
        File file = new File("D:\\1.jpg");
		//2、文件上传
        StorePath path = client.uploadFile(new FileInputStream(file),
                file.length(), "jpg", null);
		//3、拼接访问路径
        String url = webServer.getWebServerUrl() + path.getFullPath();
    }
}

2-4 总结

1.FastDFS的内部结构

Tracker Server:追踪服务器,监控storage Server,计算调度

Storage Server:文件存储服务器

2.工作原理

Storage Server同步状态到Tracker Server

客户端从Tracker Server查询可用Storage Server

客户端从Storage Server完成文件上传下载

3.核心代码

image-20220928105729455

三. 发布小视频

3-1 小视频功能说明

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。

1、视频发布(视频:容量大,视频存储到什么位置?)

2、查询视频列表(问题:数据库表)

3、关注视频作者

4、视频播放(客户端获取视频的URL地址,自动的播放)

效果:

1568878011614 1568878327076 1568878353854 1568878397710

3-2 表结构

video(视频记录表)

{
    "_id" : ObjectId("5fa60707ed0ad13fa89925cc"),
    "vid" : NumberLong(1),
    "userId" : NumberLong(1),
    "text" : "我就是我不一样的烟火~",
    "picUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/video_1.png",
    "videoUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/1576134125940400.mp4",
    "created" : NumberLong(1604716296066),
    "likeCount" : 0,
    "commentCount" : 0,
    "loveCount" : 0,
    "_class" : "com.tanhua.domain.mongo.Video"
}

image-20201230095859304

vid:用于推荐系统,唯一数字

likeCount/commentCount:喜欢数,评论数(同动态类似

3-3 搭建环境

1. 实体类

tanhua-domain中配置实体类Video

package com.tanhua.domain.mongo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "video")
public class Video implements java.io.Serializable {

    private static final long serialVersionUID = -3136732836884933873L;

    private ObjectId id; //主键id
    private Long vid; //自动增长
    private Long created; //创建时间


    private Long userId;
    private String text; //文字
    private String picUrl; //视频封面文件,URL
    private String videoUrl; //视频文件,URL


    private Integer likeCount=0; //点赞数
    private Integer commentCount=0; //评论数
    private Integer loveCount=0; //喜欢数
}

2. API接口与实现

定义接口

tanhua-dubbo-interface工程定义API接口VideoApi

public interface VideoApi {

}

接口实现类

tanhua-dubbo-service工程定义API接口实现类VideoApiImpl

@DUbboService
public class VideoApiImpl implements VideoApi {
    
}
3. controller控制器

tanhua-server定义SmallVideoController

@RestController
@RequestMapping("/smallVideos")
public class SmallVideoController {

    @Autowired
    private SmallVideosService videosService;
    
}
4. service业务层

tanhua-server定义SmallVideosService

@Service
public class SmallVideosService {

    @Autowired
    private OssTemplate ossTemplate;

    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;

    @Reference
    private VideoApi videoApi;

    @Reference
    private UserInfoApi userInfoApi;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
}

3-4 发布视频

1. 接口文档

http://192.168.136.160:3000/project/19/interface/api/214

image-20220928110237631
2. 发布流程

业务要求:

客户端上传视频时,自动生成封面图片一并发送请求。

封面图片:上传到阿里云OSS

视频:上传到FastDFS

操作流程:

1、用户发通过客户端APP上传视频到server服务

2、server服务上传视频到FastDFS文件系统,上传成功后返回视频的url地址

3、server服务上传封面图片到阿里云OSS

4、server通过rpc的调用dubbo服务进行保存小视频数据

image-20220928110623534

3. SmallVideosController
@RestController
@RequestMapping("/smallVideos")
public class SmallVideosController {

    @Autowired
    private SmallVideosService videosService;

    /**
     * 发布视频
     *  接口路径:POST
     *  请求参数:
     *      videoThumbnail:封面图
     *      videoFile:视频文件
     */
    @PostMapping
    public ResponseEntity saveVideos(MultipartFile videoThumbnail,MultipartFile videoFile) throws IOException {
        videosService.saveVideos(videoThumbnail,videoFile);
        return ResponseEntity.ok(null);
    }
}
4. SmallVideosService
@Service
public class SmallVideosService {

    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;

    @Autowired
    private OssTemplate ossTemplate;
    
    @DubboReference
    private VideoApi videoApi;
    /**
     * 上传视频
     * @param videoThumbnail 封面图片文件
     * @param videoFile  视频文件
     */
    public void saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {

        if(videoFile.isEmpty() || videoThumbnail.isEmpty()) {
            throw new BusinessException(ErrorResult.error());
        }

        //1、将视频上传到FastDFS,获取访问URL
        String filename = videoFile.getOriginalFilename();  // abc.mp4
        filename = filename.substring(filename.lastIndexOf(".")+1);
        StorePath storePath = client.uploadFile(videoFile.getInputStream(), videoFile.getSize(), filename, null);
        String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();
        //2、将封面图片上传到阿里云OSS,获取访问的URL
        String imageUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());
        //3、构建Videos对象
        Video video = new Video();
        video.setUserId(UserHolder.getUserId());
        video.setPicUrl(imageUrl);
        video.setVideoUrl(videoUrl);
        video.setText("我就是我,不一样的烟火");
        //4、调用API保存数据
        String videoId = videoApi.save(video);
        if(StringUtils.isEmpty(videoId)) {
            throw new BusinessException(ErrorResult.error());
        }
    }
5. API接口

VideoApiVideoApiImpl中编写保存video的方法

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private IdWorker idWorker;

    @Override
    public String save(Video video) {
        //1、设置属性
        video.setVid(idWorker.getNextId("video"));
        video.setCreated(System.currentTimeMillis());
        //2、调用方法保存对象
        mongoTemplate.save(video);
        //3、返回对象id
        return video.getId().toHexString();
    }
6. 测试问题

对于SpringBoot工程进行文件上传,默认支持最大的文件是1M。为了解决这个问题,需要在application.yml中配置文件限制

如果上传视频,会导致异常,是因为请求太大的缘故:

1569075180126

7. 配置文件解析

tanhua-server工程的application.yml中添加解析器,配置请求文件和请求体

Spring:
  servlet:
    multipart:
      max-file-size: 30MB
      max-request-size: 30MB

四. 视频列表查询

4-1 流程分析

小视频的列表查询的实现需要注意的是,如果有推荐视频,优先返回推荐视频,如果没有,按照时间倒序查询视频表。

image-20220928141731612

数据库表:video

  • 创建VO对象

  • 创建controller对象,并配置分页查询接口方法

  • 创建service对象,其中调用API,构造vo对象返回值

    • 调用API:PageResult
    • 将Video转化成VO对象
  • 在API服务层,创建方法,分页查询小视频列表,返回PageResult<Video>

4-2 接口文档

image-20220928141846890

4-3 定义Vo对象

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo implements Serializable {


    private Long userId;
    private String avatar; //头像
    private String nickname; //昵称

    private String id;
    private String cover; //封面
    private String videoUrl; //视频URL
    private String signature; //发布视频时,传入的文字内容


    private Integer likeCount; //点赞数量
    private Integer hasLiked; //是否已赞(1是,0否)
    private Integer hasFocus; //是否关注 (1是,0否)
    private Integer commentCount; //评论数量

    public static VideoVo init(UserInfo userInfo, Video item) {
        VideoVo vo = new VideoVo();
        //copy用户属性
        BeanUtils.copyProperties(userInfo,vo);  //source,target
        //copy视频属性
        BeanUtils.copyProperties(item,vo);
        vo.setCover(item.getPicUrl());
        vo.setId(item.getId().toHexString());
        vo.setSignature(item.getText());
        vo.setHasFocus(0);
        vo.setHasLiked(0);
        return vo;
    }
}

4-4 Controller

    /**
     * 视频列表
     */
    @GetMapping
    public ResponseEntity queryVideoList(@RequestParam(defaultValue = "1")  Integer page,
                                         @RequestParam(defaultValue = "10") Integer pagesize) {
        PageResult result = videosService.queryVideoList(page, pagesize);
        return ResponseEntity.ok(result);
    }

4-5 Service

    //查询视频列表 userid _ page_pagesize
    @Cacheable(
            value="videos",
            key = "T(com.tanhua.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize")
	public PageResult queryVideoList(Integer page, Integer pagesize) {

        //1、查询redis数据
        String redisKey = Constants.VIDEOS_RECOMMEND +UserHolder.getUserId();
        String redisValue = redisTemplate.opsForValue().get(redisKey);
        //2、判断redis数据是否存在,判断redis中数据是否满足本次分页条数
        List<Video> list = new ArrayList<>();
        int redisPages = 0;
        if(!StringUtils.isEmpty(redisValue)) {
            //3、如果redis数据存在,根据VID查询数据
            String[] values = redisValue.split(",");
            //判断当前页的起始条数是否小于数组总数
            if( (page -1) * pagesize < values.length) {
                List<Long> vids = Arrays.stream(values).skip((page - 1) * pagesize).limit(pagesize)
                        .map(e->Long.valueOf(e))
                        .collect(Collectors.toList());
                //5、调用API根据PID数组查询动态数据
                list = videoApi.findMovementsByVids(vids);
            }
            redisPages = PageUtil.totalPage(values.length,pagesize);
        }
        //4、如果redis数据不存在,分页查询视频数据
        if(list.isEmpty()) {
            //page的计算规则,  传入的页码  -- redis查询的总页数
            list = videoApi.queryVideoList(page - redisPages, pagesize);  //page=1 ?
        }
        //5、提取视频列表中所有的用户id
        List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class);
        //6、查询用户信息
        Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);
        //7、构建返回值
        List<VideoVo> vos = new ArrayList<>();
        for (Video video : list) {
            UserInfo info = map.get(video.getUserId());
            if(info != null) {
                VideoVo vo = VideoVo.init(info, video);
                vos.add(vo);
            }
        }

        return new PageResult(page,pagesize,0l,vos);
    }

4-6 API接口和实现

VideoApiVideoApiImpl中编写分页查询方法

    @Override
    public List<Video> findMovementsByVids(List<Long> vids) {
        Query query = Query.query(Criteria.where("vid").in(vids));
        return mongoTemplate.find(query,Video.class);
    }

    @Override
    public List<Video> queryVideoList(int page, Integer pagesize) {
        Query query = new Query().limit(pagesize).skip((page -1) * pagesize)
                .with(Sort.by(Sort.Order.desc("created")));
        return mongoTemplate.find(query,Video.class);
    }

五. 关注用户

关注用户是关注小视频发布的作者,这样我们后面计算推荐时,关注的用户将权重更重一些。

关注用户

  • controller:接受参数
  • service:调用API,操作redis
  • api接口(VideoAPI):关注的保存和删除
  • 修改之前的查询service,从redis获取数据(如果存在返回1:,不存在返回0)

5-1 表和实体类对象

image-20220928144503156

//用户关注表(关注小视频的发布作者)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "focus_user")
public class FocusUser implements java.io.Serializable{

    private static final long serialVersionUID = 3148619072405056052L;

    private ObjectId id; //主键id
    private Long userId; //用户id    106
    private Long followUserId; //关注的用户id   1
    private Long created; //关注时间
}

5-2 接口文档

说明
接口路径/smallVideos/:uid/userFocus
请求方式POST
路径参数uid
响应结果ResponseEntity
说明
接口路径/smallVideos/:uid/userUnFocus
请求方式POST
路径参数uid
响应结果ResponseEntity
  1. 关注/取消关注和点赞/喜欢功能类似

  2. 调用API保存/删除MongoDB数据

  3. 为了提供效率,使用Redis缓存

    Reids使用Hash结构存储

  4. 修改视频列表的接口,添加是否关注视频作者状态

5-3 Controller

/**
 * 关注视频作者
 */
@PostMapping("/{id}/userFocus")
public ResponseEntity userFocus(@PathVariable("id") Long followUserId) {
    videosService.userFocus(followUserId);
	return ResponseEntity.ok(null);
}

/**
 * 取消关注视频作者
 */
@PostMapping("/{id}/userUnFocus")
public ResponseEntity userUnFocus(@PathVariable("id") Long followUserId) {
    videosService.userUnFocus(followUserId);
	return ResponseEntity.ok(null);    
}

5-4 Service

    //关注视频作者
    public void userFocus(Long followUserId) {
        //1、创建FollowUser对象,并设置属性
        FollowUser followUser = new FollowUser();
        followUser.setUserId(UserHolder.getUserId());
        followUser.setFollowUserId(followUserId);
        //2、调用API保存
        videoApi.saveFollowUser(followUser);
        //3、将关注记录存入redis中
        String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId();
        String hashKey = String.valueOf(followUserId);
        redisTemplate.opsForHash().put(key,hashKey,"1");
    }

    //取消关注视频作者
    public void userUnFocus(Long followUserId) {
        //1、调用API删除关注数据
        videoApi.deleteFollowUser(UserHolder.getUserId(),followUserId);
        //2、删除redis中关注记录
        String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId();
        String hashKey = String.valueOf(followUserId);
        redisTemplate.opsForHash().delete(key,hashKey);
    }

5-5 API和实现类

VideoApiVideoApiImpl中编写关注方法

image-20201230132317915

解决重复关注的问题:

在保存关注数据时,可以根据userId和followUserId查询数据库,如果存在则不再保存数据

5-6 修改查询视频列表

查询视频列表是,从redis中获取关注数据

image-20201230143647160

六. 通用缓存SpringCache

6-1 概述

在项目中,我们通常会把高频的查询进行缓存。如资讯网站首页的文章列表、电商网站首页的商品列表、微博等社交媒体热搜的文章等等,当大量的用户发起查询时,借助缓存提高查询效率,同时减轻数据库压力。

目前的缓存框架有很多:比如Redis、Memcached、Guava、Caffeine等等

image-20220928152537549

1. 问题分析

在使用缓存时(以Redis为例),操作代码往往遵循类似的写法:

image-20220928152641533

思考 :

问题:大量方法,雷同的操作方式,缓存的灵活切换

需求:不改变源代码的前提,对方法增强

解决方法:Spring AOP
2. 解决方法

使用SpringAOP动态增强

自定义注解,进行缓存配置

适配多种缓存

image-20220928152739555

Spring Cache是Spring提供的通用缓存框架。它利用了AOP,实现了基于注解的缓存功能,使开发者不用关心底层使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。用户使用Spring Cache,可以快速开发一个很不错的缓存功能。

6-2 步骤

1、引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2、开启缓存

@SpringBootApplication
@EnableCaching
public class CachingApplication {
    public static void main(String[] args) {
        SpringApplication.run(CachingApplication.class, args);
    }
}

3、配置注解

@Cacheable("user")
public User findById(Long id) {
    return userDao.findById(id);
}

默认情况下, SpringCache使用ConcurrentHashMap作为本地缓存存储数据。如果要使用其它的缓存框架,我们只需要做简单的配置即可

以Redis为例

  • 引入Spring-data-redis依赖
<!--SpringDataRedis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 加入Redis配置
spring:
  redis:
    port: 6379
    host: 192.168.136.160

6-3 常用注解

Spring Cache有几个常用注解,分别为@Cacheable、@CachePut、@CacheEvict、@Caching。

1. @Cacheable

注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。

image-20220928160315922

key: 支持springel

redis-key的命名规则:value + “::” + key

    /**
     * value:名称空间(分组)
     * key: 支持springel
     * redis-key的命名规则:
     *      value + "::" + key
     */
    //@Cacheable(value="user" , key = "'test' + #id")
    @CachePut(value="user" , key = "'test' + #id")
    public User findById(Long id) {
        return userDao.findById(id);
    }

springel中调用静态方法调用规则T(全限定类名).method

image-20220928165216014

2. @CachePut

加了@CachePut注解的方法,不会从缓存查, 永远调用数据库查询, 会把方法的返回值put到缓存里面缓存起来,供其它地方使用。

image-20220928160404478

3. @CacheEvit

使用了CacheEvict注解的方法,会清空指定缓存。

image-20220928160807961

4. @Caching

Java代码中,同个方法,一个相同的注解只能配置一次。如若操作多个缓存,可以使用@Caching

image-20220928160844590

    //@CacheEvict(value="user" , key = "'test' + #id")
    @Caching(
            evict = {
                    @CacheEvict(value="user" , key = "'test' + #id"),
                    @CacheEvict(value="user" , key = "#id")
            }
    )
    public void update(Long id) {
        userDao.update(id);
    }

6-4 设置缓存失效时间

@Configuration
public class RedisCacheConfig {

    //设置失效时间
    private static final Map<String, Duration> cacheMap;

    static {
        cacheMap = ImmutableMap.<String, Duration>builder().put("videos", Duration.ofSeconds(30L)).build();
    }

    //配置RedisCacheManagerBuilderCustomizer对象
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return (builder) -> {
            //根据不同的cachename设置不同的失效时间
            for (Map.Entry<String, Duration> entry : cacheMap.entrySet()) {
                builder.withCacheConfiguration(entry.getKey(),
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(entry.getValue()));
            }
        };
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值