Java 项目实战: 交友项目 - 通用缓存SpringCache:配置视频列表缓存功能

飞塔防火墙和paloalto防火墙构建ipsec 一、配置fortigate防火墙1.防火墙配置上网功能1.1配置fortigate的wan1接口IP为100.100.100.2/24VLAN交换internal,IP地址为192.168.156.99/24。开启DHCP分配IP地址范围为192.168.156.110-192.168.156.2101.2配置访问互联网的缺省路由1.3配置访问互联网的安全策略2.配置与Paloalto防火墙的IPSECVPN。 阅读详情

概述

SpringCache 的注解语法讲完后,这里把它们真正落到交友项目的视频列表上:给 queryVideoList 加 @Cacheable,给发布视频加 @CacheEvict,再配上 TTL,把「读缓存 + 写失效」这条缓存一致性的最简闭环完整走一遍。

纲要

  • 现状与问题
  • 视频列表接口的查询链路(Dubbo + MongoDB + 用户信息聚合)
  • 不加缓存时每次分页查询的代价
  • 文件清单
  • 涉及的工程文件与改动位置
  • @Cacheable 落地:视频列表读缓存
  • 启动类开启缓存(工程实读)
  • queryVideoList 上的 @Cacheable(value="videos", key=userId_page_pagesize)
  • value 命名、key 三段拼接、命中与未命中两条路径
  • RedisCacheConfig:给 videos 命名空间设 TTL
  • 发布视频时的缓存失效:@CacheEvict(allEntries=true)
  • 为什么必须清全部
  • 「只清当前用户缓存」的错误推演
  • 闭环验证:四步用 redis-cli 实测
  • 时序图:发布视频 → 清缓存 → 回源 → 新缓存
  • 坑表与常见误解
  • 通用价值:读缓存 + 写失效的适用面

现状与问题

视频列表接口 queryVideoList 的查询链路不短:先从 Redis 取推荐视频 id 列表(VIDEOS_RECOMMEND_ + userId),取不到再走 Dubbo 调 VideoApi 分页查 MongoDB,最后还要按 userId 批量查 UserInfo 拼 VideoVo。一次请求串了两次远程调用,而列表页恰恰是用户反复下拉刷新的高频接口。

缓存注解的语法已在《通用缓存SpringCache:常用注解》中拆解过,这里只做落地。目标形态是一读一写两处:

位置注解作用
SmallVideosService.queryVideoList@Cacheable(value="videos", key=userId_page_pagesize)列表结果按「用户+页码」缓存
SmallVideosService.saveVideos@CacheEvict(value="videos", allEntries=true)发布视频后清空整个 videos 命名空间
———
SmallVideosService.queryVideoList@Cacheable(value="videos", key=userId_page_pagesize)列表结果按「用户+页码」缓存
SmallVideosService.saveVideos@CacheEvict(value="videos", allEntries=true)发布视频后清空整个 videos 命名空间

文件清单

改动集中在 mf-app-server 一个模块:

文件位置改动
AppServerApplication.javacom.mf.server加 @EnableCaching 开启缓存
SmallVideosService.javacom.mf.server.servicequeryVideoList 加 @Cacheable;saveVideos 加 @CacheEvict
RedisCacheConfig.javacom.mf.server.config按 cacheName 配置 TTL(videos 30 秒)
mf-app-server/src/main/java/com/mf/server/
├── AppServerApplication.java        # @EnableCaching 开启缓存支持
├── config/
│   └── RedisCacheConfig.java        # RedisCacheManagerBuilderCustomizer,按命名空间设 TTL
└── service/
    └── SmallVideosService.java      # queryVideoList 加 @Cacheable / saveVideos 加 @CacheEvict

依赖方面,Spring Data Redis 工程里本来就有,spring-boot-starter-cache 也不是必须,RedisCacheManager 由 Spring Boot 自动装配,前提只有一个:启动类上有 @EnableCaching。

@Cacheable 落地:视频列表读缓存

启动类开启缓存

工程终态的 AppServerApplication 已经带上了(还顺手排除了 Mongo 自动配置,这个与缓存无关):

package com.mf.server;

import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(exclude = {
        MongoAutoConfiguration.class,
        MongoDataAutoConfiguration.class
}) //排除mongo的自动配置
@EnableCaching
public class AppServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AppServerApplication.class, args);
    }
}

没有这一行,后面所有缓存注解都只是摆设,注解本身不报错,方法照常执行,缓存完全不生效,这是排查「加了注解没效果」的第一检查点。

queryVideoList 加 @Cacheable

工程里的真实代码(SmallVideosService.java):

package com.mf.server.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.PageUtil;
import com.mf.dubbo.api.UserInfoApi;
import com.mf.dubbo.api.VideoApi;
import com.mf.model.domain.UserInfo;
import com.mf.model.mongo.Video;
import com.mf.model.vo.PageResult;
import com.mf.model.vo.VideoVo;
import com.mf.server.interceptor.UserHolder;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class SmallVideosService {

    @DubboReference
    private VideoApi videoApi;

    @DubboReference
    private UserInfoApi userInfoApi;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    //查询视频列表
    @Cacheable(
            value = "videos",
            key = "T(com.mf.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize")  //userid _ 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());
                //根据VID数组查询视频数据
                list = videoApi.findMovementsByVids(vids);
            }
            redisPages = PageUtil.totalPage(values.length, pagesize);
        }
        //4、如果redis数据不存在,分页查询视频数据
        if (list.isEmpty()) {
            //page的计算规则:传入的页码 -- redis查询的总页数
            list = videoApi.queryVideoList(page - redisPages, pagesize);
        }
        //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);
    }
}

value 与 key 的设计考量

value=「videos」 的命名。value 就是 cacheName,对应 Redis 里的命名空间前缀。叫 videos 而不是 videoCache 或 videoList,有两个实际考虑:一是它和 RedisCacheConfig 里按名称空间配 TTL 的 map 键对应,两边必须一字不差;二是它会在 Redis key 里露脸(videos::106_1_10),排查问题时一眼能认出归属。

key 的三段拼接:userId_page_pagesize。这份缓存要唯一标识「某个人看到的第某页、每页某条」的列表结果:

  • userId:推荐列表按人不同,两个人查同一页拿到的数据不一样,缺了 userId 就会串数据;
  • page:同一用户的不同页是不同内容,1 页 1 次缓存;
  • pagesize:客户端分页大小可能变化(10 条/20 条),页码相同、条数不同也不是同一份结果。

三段组合下来,106_1_10 就是「用户 106 的第一页、每页 10 条」,唯一对应一次方法调用的返回值。

T(com.mf.server.interceptor.UserHolder).getUserId() 这段 SpEL 是在调静态方法拿当前登录用户,T() 语法的细节在《通用缓存SpringCache:常用注解》里讲过,不展开。key 里的字符串常量要带引号('_'),方法参数前要带 #,这两处漏了都会启动报 SpEL 解析异常。

命中与未命中的两条路径

一次请求进来,SpringCache 的代理对象先拼 key 查 Redis:

路径行为
命中(Redis 里有 videos::106_1_10)方法体一行都不执行,直接反序列化返回缓存里的 PageResult。两次 Dubbo 调用全省
未命中执行方法体(查推荐列表 / 走 Dubbo 查 MongoDB),拿到 PageResult 后自动序列化写回 Redis,key 就是拼出来的那串

注意一个容易忽略的层次问题:@Cacheable 套在方法外层,而方法体内部自己还有一段「查 VIDEOS_RECOMMEND_ 推荐视频 id」的 Redis 逻辑。也就是说这个接口实际上是两层缓存:外层 SpringCache 缓存的是最终 PageResult,内层手写的 RedisTemplate 逻辑缓存的是推荐 id 列表。外层命中时,内层那套逻辑根本不会跑到,这也解释了为什么发布视频必须清 videos 命名空间,光动内层的 id 列表没用。

RedisCacheConfig:给 videos 命名空间设 TTL

不加配置时,SpringCache 写进 Redis 的缓存 TTL 是 -1(永不过期)。实测现象很典型:第一页数据一旦缓存,之后不管数据库里视频列表怎么变,用户看到的永远是那几条,数据不一致的问题直接摆在了脸上。

解法有两个:写操作时清缓存(后文的 @CacheEvict),以及给缓存设置失效时间兜底。SpringCache 配 TTL 的入口是自定义 RedisCacheManager,工程里放在 config/RedisCacheConfig.java:

package com.mf.server.config;

import com.google.common.collect.ImmutableMap;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;

import java.time.Duration;
import java.util.Map;

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

结构很简单:一个静态 map 维护「cacheName → TTL」,RedisCacheManagerBuilderCustomizer 遍历它给每个命名空间单独设值。以后新增要缓存的业务,往 map 里加一行就行,30 秒对推荐列表这种「允许短暂滞后」的数据刚好,太短了缓存形同虚设,太长了脏数据窗口拉大。

这个配置类属于「查资料拿过来用」的固定写法,不需要背,但要能看懂 map 键和 @Cacheable 的 value 的对应关系,配错了名字(比如写成 video),TTL 不会报错,只是默默不生效,TTL 依旧 -1。

发布视频时的缓存失效:@CacheEvict(allEntries=true)

讲义 6.4 给出的写法,给 saveVideos 补上清缓存动作:

/**
 * 发布小视频
 */
@CacheEvict(value = "videos", allEntries = true)  //清空缓存
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());
    }
}

这里要说明一处工程实读与讲义的差异:08 章代码快照里的 saveVideos 并没有加 @CacheEvict,只有 queryVideoList 上的 @Cacheable 落了地。讲义 6.4 明确要求补上,否则发布新视频后,所有还在 TTL 内的用户看到的都是旧列表。这不是理论风险,这里坑表里的第一个坑就是它,如果发现「发布成功但列表不更新」,先查这里。

为什么必须 allEntries 清全部

@CacheEvict 有两种清法:按 key 精确清(key = "..."))和按命名空间全清(allEntries = true)。这里只能选后者,原因在 key 的构成上:

  • 新发布的视频会进入全局视频池,影响的是所有用户的推荐列表,不只是发布者本人;
  • 缓存 key 是 userId_page_pagesize 三段拼接,发布者根本不知道「谁、在第几页、每页多少条」会看到这条新视频,这些 key 有多少个、长什么样,写代码时无法枚举。

既然精确 key 列不全,就按命名空间整体清空:allEntries = true 会把 videos::* 的所有 key 一次删掉,下一次任何人查任何页都回源到数据库,重新拿到包含新视频的结果。

错误推演:如果只清当前用户缓存会怎样

假设图省事写成按 key 清:

// 错误示范:只清发布者自己的缓存
@CacheEvict(value = "videos",
        key = "T(com.mf.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize")
public void saveVideos(...) { ... }

问题不止一个:

  1. 发布接口的参数里没有 page 和 pagesize,SpEL 取不到值,求值直接失败;
  2. 就算改成只拼 userId 清掉该用户的所有页(key 前缀匹配 SpringCache 不支持,key 要求全等),清掉的也只是发布者一个人的缓存;
  3. 用户 A、B、C 的 videos::A_1_10、videos::B_1_10 原封不动,他们各自的 TTL 窗口内(最长 30 秒)刷列表,新视频就是不出现,而且服务端日志毫无异常,属于最难受的那种「静默脏读」。

清缓存的粒度跟着「数据影响面」走:影响一个人的,按 key 清;影响所有人的,allEntries 清。这条判断在后面几乎所有写缓存场景里都成立。

闭环验证:四步用 redis-cli 实测

配置写完不代表闭环通了,完整的验证链路是「查列表 → 看缓存 → 再查命中 → 发布清缓存 → 再查回源」。全程用 redis-cli 观察(连接的是 application.yml 里配的 192.168.136.160:6379):

第一步:清场后首次调列表接口(未命中,查库)

# 清掉可能存在的旧缓存,保证从零开始
redis-cli -h 192.168.136.160 keys "videos::*"
# (空)
# 登录用户 106,在模拟器里进入视频列表页,触发 GET /smallVideos/videoList?page=1&pagesize=10
redis-cli -h 192.168.136.160 keys "videos::*"

预期现象:接口第一次返回走的是数据库(日志可见 Dubbo 调用),查询完成后 Redis 里多出一个 key:

1) "videos::106_1_10"

key 形态正是 cacheName::key:videos 是 value 指定的命名空间,106_1_10 是 SpEL 拼出的三段 key,用户 106、第 1 页、每页 10 条。

第二步:确认 TTL 与再次调用(命中)

# 不配 RedisCacheConfig 时这里是 -1(永不过期);配上后是 30 秒起跳递减
redis-cli -h 192.168.136.160 ttl "videos::106_1_10"
# 27

# 再刷一次列表页,观察服务端:方法体不执行、没有 Dubbo 调用日志,响应明显变快

预期现象:TTL 是正数且递减(说明 RedisCacheConfig 生效);第二次请求接口直接返回,queryVideoList 的方法体没有执行,这是命中缓存的特征,可以在方法第一行打日志验证。

第三步:发布新视频,观察 @CacheEvict 生效

# 在模拟器发布一条新视频(POST /smallVideos,含视频文件与封面)
redis-cli -h 192.168.136.160 keys "videos::*"

预期现象:发布接口返回成功后,videos::* 下的 key 全部消失,keys 返回空。如果 key 还在,说明 saveVideos 上的 @CacheEvict 没生效,回头检查是不是工程里漏加了(前面说过,08 章代码快照里正好就漏了这一处)。

第四步:再次调列表接口(回源,重建缓存)

# 再刷一次视频列表
redis-cli -h 192.168.136.160 keys "videos::*"
# 1) "videos::106_1_10"
redis-cli -h 192.168.136.160 ttl "videos::106_1_10"
# 30

预期现象:这次请求走了数据库(新视频出现在列表里),查询完成后缓存被重新写入,TTL 重新从 30 秒起跳。至此「读缓存 → 写失效 → 回源 → 新缓存」四步闭环全部验证通过。

顺带一句:生产环境别用 keys 命令做日常观察(全库扫描会阻塞 Redis),验证时图方便可以,线上排查用 scan 0 match "videos::*" count 100 代替。

时序图:发布视频后的缓存流转

MongoDB(经Dubbo) Redis SmallVideosService 代理对象(SpringCache) 客户端 MongoDB(经Dubbo) Redis SmallVideosService 代理对象(SpringCache) 客户端 阶段一:发布视频,写失效 阶段二:下次查询,回源重建 POST /smallVideos(发布) 执行 saveVideos videoApi.save(video) videoId 返回 @CacheEvict(videos, allEntries) 删除 videos::* 全部 key 清空完成 发布成功 GET /smallVideos/videoList?page=1&pagesize=10 get videos::106_1_10 未命中(已被清空) 执行 queryVideoList 分页查询(含新视频) 视频列表 PageResult set videos::106_1_10 (TTL 30s) 返回包含新视频的列表

坑表

坑现象原因与解法
@CacheEvict 漏加发布视频成功,但列表页刷新看不到新视频,TTL 窗口内一直如此08 章工程快照里 saveVideos 就漏了这个注解(实读确认)。清缓存的动作要和写数据绑在同一个方法上,发布后列表必须更新的场景不能只靠 TTL 兜底
allEntries 清全部的性能顾虑高频发布场景下缓存命中率被反复清零这是正确性与命中率的取舍:视频发布频率低、影响面是全体用户,全清是对的;如果是单用户私有数据的更新,必须改成按 key 精确清,否则等于互相踢缓存
key 序列化形态看不懂redis-cli 里看到 videos::106_1_10,不知道哪段是哪段默认 RedisCacheConfiguration 的 key 是 String 序列化,格式固定为 cacheName::key;:: 前是 value 指定的命名空间,后是 SpEL 拼出的 key。想要更短的 : 分隔符得自定义 computePrefixWith
缓存对象序列化失败接口首次调用直接抛 SerializationException 或 NotSerializableException默认 value 用 JDK 序列化,方法返回值必须实现 Serializable。PageResult 已经实现了所以没事;如果哪天返回了没实现序列化接口的 VO,第一次未命中就会炸
TTL 配置了但不生效明明写了 RedisCacheConfig,redis-cli 里 TTL 还是 -1map 里的 cacheName 与 @Cacheable 的 value 对不上(比如写成了 video),不报错但不生效;或者启动类漏了 @EnableCaching

常见误解澄清

误解澄清
加了 @Cacheable 缓存就「永远一致」了不是。缓存数据在 TTL 窗口内是滞后的,一致性靠「写时清缓存 + TTL 兜底」两条腿,这条链路里任何一环漏了都有脏读窗口
清缓存只需要清发布者自己的视频列表是全局池,新视频影响所有用户的缓存。按影响面定粒度,这里必须 allEntries = true
@Cacheable 命中后方法体还会执行部分逻辑不会。代理在方法体之前就返回了,方法体内的推荐 id 查询、Dubbo 调用全都不会跑
SpringCache 和 RedisTemplate 混用会冲突不会,各管各的 key。但要想清楚层次:本例里 SpringCache 缓存最终结果,方法体内的 RedisTemplate 逻辑缓存的另一份数据,清理时要分别考虑
TTL 设得越长越好越长命中率越高,脏数据窗口也越大。列表类数据 30 秒是经验值,最终以业务对「新鲜度」的容忍度为准

API 速览

注解 / 配置这里用法说明
@EnableCaching启动类上,开启注解缓存不加则所有缓存注解无效
@Cacheable(value, key)queryVideoList,value=「videos」,key=userId_page_pagesize语法细节见前一篇常用注解
@CacheEvict(value, allEntries)saveVideos,allEntries=true 清空 videos 命名空间影响面是全体用户时用全清
RedisCacheManagerBuilderCustomizer按 cacheName 配 TTL(videos → 30s)cacheName 与 value 必须一致

官方文档

总结

  • 落地就三步:启动类 @EnableCaching,读方法 @Cacheable(value="videos", key=userId_page_pagesize),写方法 @CacheEvict(value="videos", allEntries=true),再由 RedisCacheConfig 给 videos 命名空间配 30 秒 TTL。
  • key 的三段拼接对应「谁、第几页、每页几条」,缺一段就串数据;allEntries 全清对应「新视频影响全体用户、key 无法枚举」。
  • 工程实读有一处与讲义的差异:08 章代码快照的 saveVideos 漏了 @CacheEvict,只有 TTL 在兜底,发布后最长 30 秒内列表是旧的。这正是「漏加清缓存」的活案例,写代码时要当成检查项。
  • 用 redis-cli 四步(首查建缓存 → 再查命中 → 发布清缓存 → 再查回源)验证闭环,每一步都有可观察的现象,TTL 从 -1 变 30 递减、key 的出现与消失都是判断依据。
  • 「读缓存 + 写失效 + TTL 兜底」是缓存一致性的最简闭环,所有读多写少的场景,推荐列表、用户资料、配置项,都可以套这个骨架,区别只在 key 怎么拼、清缓存的粒度多大。做新场景时先过一遍这个自检清单,比事后排查「为什么列表不更新」便宜得多。
LabVIEW内存泄漏诊断与优化:从报错到高效运行 本文深入探讨LabVIEW内存泄漏的诊断与优化方法,从理解内存泄漏本质到使用专业工具链进行问题定位,再到高频场景的解决方案和高级优化策略。通过实战案例展示如何解决工业数据采集系统中的内存不足问题,帮助开发者提升LabVIEW应用的稳定性和效率。 阅读详情

相关推荐

从ARCore到AICore:揭秘Google如何通过系统级服务推动Android生态创新

本文深入探讨了Google如何通过AICore等系统级服务推动Android生态创新。从ARCore到AICore,Google通过分层解耦策略将AI能力下沉至系统层,实现端侧大模型推理,如Gemini Nano在Pixel 8 Pro中的应用。文章剖析了AICore的技术架构、开发者赋能路径及场景化实践,展示了其在通信、内容创作和生产力工具中的卓越表现。

rust6ferris的博客 545

SpringBoot交友APP项目实战(详细介绍+案例源码) - 9.小视频(SpringCache缓存)

搭建一个Java框架的移动端交友APP,涵盖登录、注册、动态、即时通讯、视频(算法推荐)、个人中心等功能; 技术选型: Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo,即时通讯选用环信即时通讯

yuan 2799

华为交换机密码策略配置避坑指南:明文/密文、长度、复杂度,一个命令就出错

本文详细解析华为交换机密码策略配置的关键要点,包括密码长度、复杂度及等保合规要求。通过实战案例揭示常见配置误区,如明文/密文密码格式差异、复杂度检查联动机制等,帮助网络工程师避免安全漏洞。特别针对等保二级和三级要求,提供可直接落地的安全加固方案。

weixin_30879169的博客 368

Java 项目实战:交友项目 - 查询视频列表

小视频列表接口采用“推荐优先、时间兜底”策略:优先从Redis获取用户推荐视频ID串,按页码截取后批量查询MongoDB;若无推荐或已翻完,则按created倒序分页查询全量视频。通过page - redisPages实现页码无缝衔接,确保推荐与兜底区连续。接口返回带作者信息的VideoVo,关注/点赞状态暂置0,后续填充。整体设计兼顾性能与体验,实现高效信息流加载。

Wang的专栏 520

基于FastLIO2与TEB算法的机器狗自主导航系统实战

本文详细介绍了如何为机器狗构建一套基于FastLIO2与TEB算法的自主导航系统。通过整合FastLIO2激光惯性里程计实现精准定位,并采用TEB局部规划器进行灵活路径规划与动态避障,解决了传统方案在复杂地形下反应慢、易碰撞的问题。文章提供了从环境搭建、算法部署、地图转换到系统调试的完整实战指南,帮助开发者实现机器狗的稳健自主行走。

beer8的博客 320

IPD流程中CBB管理的5个致命误区,第3个坑了80%的团队

本文深入剖析了IPD流程中CBB管理的五个致命误区,尤其指出缺乏有效的度量与激励机制是影响80%团队的核心陷阱。文章强调CBB的核心在于前瞻性设计,而非简单归档,并需平衡通用性与产品竞争力,同时建立持续运营机制,确保CBB与市场需求对齐,从而真正实现缩短开发周期、降低成本的目标。

weixin_29234423的博客 569

Java 项目实战: 交友项目 - 小视频模块实战:访客记录、FastDFS 存储与 SpringCache 缓存

本文实现小视频模块核心功能:基于MongoDB记录“谁看过我”访客数据,使用FastDFS分布式文件系统存储视频与缩略图,通过Dubbo服务完成发布与列表接口。结合Spring Cache注解(@Cacheable/@CacheEvict)对视频列表缓存30秒,发布后自动清空缓存,提升性能并保证数据实时性。工程新增视频、访客实体及缓存配置,三条技术线(MongoDB、FastDFS、Redis)协同支撑完整链路。

Wang的专栏 326

【PCIe】CDNS PCIe VIP 杂记 -- Callback

最近在研究Cadence PCIe VIP,怎一个“惨淡”来描述。用的真是一言难尽啊,都是泪。记录一些最近学习的觉得想记录的内容在此,仅供参考讨论学习哈。有不对之处请指出大家一起讨论。

小邦是名小ICer的博客 2716

探花交友_第9章_小视频方案(新版)

探花交友_第9章_小视频方案(新版) 1. 我的访客 1.1 需求分析 1.1.1 功能说明 1.1.2 数据库表 1.2 记录访客数据 tanhua-model tanhua-app-server tanhua-dubbo-interface tanhua-dubbo-mongo 1.3 首页谁看过我 需求分析 tanhua-model tanhua-app-server MovementController MovementService tanhua-dubbo-interface tanhua-du

无 1510

6、Cocos2D游戏开发:精灵添加与角色实现

本文详细介绍了在Cocos2D游戏开发中如何添加背景精灵和玩家角色精灵。通过创建GameScene场景、组织项目资源、使用多分辨率图像适配不同设备,并深入讲解了将角色拆分为上下半身以实现更真实瞄准效果的原理。文章还解析了Cocos2D的图像搜索机制、锚点概念及其应用,以及如何通过父子节点关系管理复合角色。最终实现了猎人角色在场景中的正确显示与定位,为后续动画和交互功能打下基础。

e3f4g5的博客 90

【嵌入式硬件】DRV8874电机驱动

1)N沟道H桥电机驱动;2)驱动1个双向有刷直流电机;3)2个单向有刷直流电机;4)4.5V-37V工作电压范围;5)集成电流感测和调节;6)成比例电流输出(IPROPI);7)可选电流调节(IMODE):逐周期或固定关断时间;8)支持1.8、3V、5V逻辑输入;9)集成保护:欠压、电荷泵欠压、过流保护、输出关闭、热关断、自动故障恢复。

qq_39200110的博客 4422

Java 项目实战: 交友项目 - 通用缓存SpringCache:介绍以及入门案例

本文介绍如何仅通过注解实现无代码缓存:SpringCache作为缓存抽象层,通过@EnableCaching开启,配合@Cacheable等注解,实现方法级缓存。三步即可上手:引入依赖、开启缓存、标注方法。默认使用内存缓存,可无缝切换为Redis。核心原理基于AOP代理,注意同类内部调用会导致注解失效。掌握注解区别(如@Cacheable与@CachePut)及SpEL表达式使用,避免常见坑点,实现高效缓存。

Wang的专栏 572

Diffusion models代码实战:从零搭建自己的扩散模型

这个系列曾经写过三篇文章专门讲代码,分别从数据集、超参数、loss设计、参数计算、Unet结构、正向过程、逆向过程等部分详细介绍了如何搭建DDPM。Diffusion models领域发展神速,最近半年代表作品有OpenAI的GLIDE、DALL-E 2,Google Brain的ImageGen,海森堡大学的Latent Diffusion。这篇博客针对入门新手讲解一下如何利用已有的资源快速搭建自己的Diffusion models。.........

沉迷单车的追风少年 1万+

Java 项目实战: 交友项目 - 通用缓存SpringCache:问题引入

小视频列表接口高频请求导致频繁穿透Redis、MongoDB与MySQL,造成数据库压力过大。该数据读多写少且可容忍短暂旧数据,适合缓存。通过手写缓存代码发现存在重复逻辑、维护困难、失效机制缺失等问题。引入SpringCache的声明式注解方案,以AOP实现缓存管理,解决“缓存什么、缓存多久、如何失效”三大问题,显著提升性能与可维护性。

Wang的专栏 380

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Java 项目实战:交友项目 - SpringCache 入门案例深化:key 写法、RedisCacheManager 与写时失效

本文以交友项目视频列表缓存为例,解答 SpringCache 落地三大核心问题:key 生成防串号(通过 SpEL 组合 userId+page+pagesize)、Redis 过期时间与序列化配置(RedisCacheConfig 按 cacheName 设置 TTL,使用 GenericJackson2JsonRedisSerializer)、数据变更时缓存失效策略(发布视频时用 @CacheEvict(allEntries=true) 清除对应分组)。结合工程实读与踩坑验证,实现精准、可维护的缓存方案

Wang的专栏 733

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer

本文系统梳理InnoDB内存结构中的ChangeBuffer、Adaptive Hash Index(AHI)与LogBuffer。ChangeBuffer缓存二级索引页不在内存时的变更,减少随机IO,仅在页加载时合并;聚簇索引修改立即执行,因数据本身不可延后。AHI基于高频等值查询自动构建哈希索引,加速定位,仅支持等值操作。LogBuffer为redo log内存缓冲,配合磁盘日志实现崩溃恢复。三者均服务于性能优化,其中ChangeBuffer与AHI共享BufferPool空间,而LogBuffer独立

bksczm的博客 282

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Spring AI + GPT-5.5 + Ollama 配置教程:Base URL、超时与流式调用

由于不同 Spring AI、Spring Boot 小版本之间仍可能存在差异,创建项目时应检查 Spring Initializr、Spring AI BOM 和官方兼容矩阵,不要直接复制旧教程中的 Starter 名称和版本号。多人项目最好隔离不同客户端,并在脱敏前提下记录最终 URL、状态码、请求耗时、请求 ID 和模型 ID。Spring AI 接入模型 API,至少要同时确认 Starter、Base URL、请求路径、API Key、模型 ID、连接超时、读取超时和流式客户端这 8 项。

2601_95893075的博客 232

V8 机制、手写核心函数与 TypeScript 类型体操

或固定初始化(如数组先定长、对象先给全字段)保持类型与形状稳定;这些都比"换框架"更基础。只能处理纯数据(丢函数、Date、RegExp、循环引用报错)。生产级深拷贝要处理:原始值短路、循环引用、V8(Chrome / Node 的 JS 引擎)不是"解释一行跑一行"那么简单,它用。图:V8 执行流程——解释执行 + 热点优化,优化失败会 Deopt 回退。JS 内存自动管理,但"自动"不等于"不会漏"。是"固定间隔最多执行一次"。手写 Promise 是"懂异步"的试金石。是"停下后才执行最后一次",

qq_38468358的博客 346

Java EE 进阶:8.MyBatis 操作数据库(入门)(第一弹)

Java EE 进阶:8.MyBatis 操作数据库(入门)(第一弹)

要努力去发光,而不是被照亮~ 126

Spring Boot 数据源与 MyBatis(-Plus) 自定义配置详解

本文详解 Spring Boot 中单数据源环境下 HikariCP 与 MyBatis-Plus 的自定义配置。通过手动声明 SqlSessionFactory,实现下划线转驼峰、日志输出、一级缓存策略等精细化控制。结合 HikariCP 核心参数调优(如最大连接数、连接生命周期),确保高性能与稳定性。适用于仅需访问单一数据库的场景,避免多数据源复杂性,兼顾性能与可维护性。

BADAO_LIUMANG_QIZHI的博客 196

LeetCode Hot 100 二叉树专题(二)3 题(Java)

本篇文章共 3 题:543. 二叉树的直径、102. 二叉树的层序遍历、108. 将有序数组转换为二叉搜索树。 每道题都统一按「① 题目分析 & 解题思路 → ② 常见解法(附代码)及讲解 → ③ 知识点总结」的顺序组织;代码使用 Java 编写,节点定义沿用 LeetCode 默认的 TreeNode。

2401_88612756的博客 365

Decorator

TaskDecorator 是异步场景下"跨线程传递上下文"的标准方案,主要用于传递 MDC、用户信息、数据源路由等上下文数据,核心价值是让异步任务的日志链路不中断、业务上下文不丢失。TaskDecorator 是 Spring 提供的一个钩子接口,用于在异步任务执行前对 Runnable 进行包装,常用于跨线程传递上下文。如果你在项目中遇到具体的异步上下文传递问题,可以把代码贴出来,我帮你给出最合适的 Decorator 方案 👍。✅ 核心能力:在线程切换时,把"父线程的上下文"搬到"子线程"

wlwork66的博客 106

JDK 下载安装:装完 Java 提示「不是内部或外部命令」?环境变量没配

JDK 是写 Java 程序必须装的东西。它包含运行 Java 的虚拟机(JRE)和编译 Java 的编译器(javac)——装了它,能跑、.java文件能编译成可执行程序。免费,学习和小规模生产都不花钱。

科小怪的博客 630

C++ 多态详解:作用、原理与实战用法

多态是C++面向对象的核心特性,通过虚函数实现运行期动态绑定,使同一接口在不同对象上表现出不同行为。其核心作用包括统一接口、提升可扩展性、降低耦合、支持运行时分派与批量处理。实现依赖虚函数表(vtable)和虚指针(vptr),需注意虚析构函数、对象切片等陷阱。相比编译期多态(模板),运行期多态灵活性高但有调用开销。合理结合两者,可兼顾代码的灵活性与性能,是构建可扩展、低耦合系统的关键。

2402_85743155的博客 382

java框架大全

java框架大全

qq_39326137的博客 183

Java 多态 学习笔记

多态是面向对象编程的重要特性,指同一操作作用于不同对象时产生不同行为。其核心在于“父类引用指向子类对象”,结合方法重写实现运行时动态绑定。多态使代码更灵活、可扩展,如用Person p = new Student();统一处理各类人员注册,无需修改调用逻辑。但多态无法直接调用子类特有方法,需通过instanceof判断并强制转换。编译时看左边(类型),运行时看右边(实际对象),实现解耦与维护便利。

AA_AA114514的博客 359
上一篇: Java 项目实战: 交友项目 - SpringCache 常用注解的深度用法与易错点
下一篇: Java 项目实战:交友项目 - 小视频方案总结与自检
Wang's Blog
博客等级 码龄12年 7418粉丝 2407原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值