欢迎光临
我们一直在努力

数据查询慢怎么办?如何优化?

分布式缓存

  • Redis
  • memcached
  • Etcd

单机缓存

  • ehcache
  • java 内存集合
  • caffeine(Java 缓存性能之王,高性能)

Redis 缓存实现

Redis 数据结构

基本

  • String 字符串
  • List 列表
  • Set 集合
  • Hash 哈希
  • Zset 集合

高级

  • bollmfilter(布隆过滤器,只要从大量的数据中快速过滤值,比如邮件黑名单拦截)
  • geo(计算地理位置)
  • hyperloglog(pv/uv)
  • pub/sub(发布订阅,类似于消息队列)
  • BitMap

自定义序列化

为了防止写入 Redis 的数据乱码、浪费空间等,可以自定义序列化器,示例代码如下

package com.wfh.xunai.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisTemplateConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
return redisTemplate;
}
}

Java 操作 Redis

Spring Data Redis

1、使用方式:引入相关依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>

2、配置 redis 地址

spring:
# redis 配置
redis:
port: 6379
host: localhost
database: 0

设计缓存 key

关键点:不同用户看到的数据不同

比如:xunai:user:recommend:userid

ps:注意:redis 内存不能无限增加,一定要设计过期时间

缓存预热

问题:即使使用了缓存,第一个用户访问的还是数据库

缓存预热的优点

    • 解决了上面的问题。可以让用户始终访问速度很快
    • 也能一定程度上保护了数据库

缺点

  • 增加了开发成本
  • 预热的时间和时间如果错了,有可能让缓存的数据不对或者太老
  • 需要额外的占用空间

怎么缓存预热

1、定时任务

2、手动触发

实现

用定时任务,每天刷新所有用户的推荐列表

定时任务实现

1、使用 Spring Scheduler(推荐,默认整合了)

控制定时任务的执行

要控制定时任务在同一个时间只能由一台服务器执行

方案:

1、分裂定时任务程序和主程序,只在一个服务器运行定时任务,成本太大

2、写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器踩真是执行业务逻辑,其他的直接返回

3、动态配置

4、分布式锁

在有限资源的情况下,控制同一时间只有某些线程能访问到资源

Java 实现锁:synchronized 关键字,juc 的类

问题:只对单个 JVM 有效

分布式锁

强锁机制

怎么保证同一个时间只能有一台服务器能强到锁

核心思想: 先来的人先把数据改成自己的唯一标识,后来的人发现标识已经存在,就抢锁失败,继续等待

等先来的人把方法执行结束,把标识清空,其他的人继续抢锁

Redis 实现:内存数据库,读写速度快

setnx: 

注意

1、用完要释放

2、锁要加上过期时间

3、如果方法执行时间过长,锁就提前过期了?

导致问题:

1、连锁效应:释放掉别人的锁

解决方案:续期

Redis 实现分布式锁

示例代码

// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));

list.remove(0);

// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);

分布式锁保证定时任务不重复执行

void testWatchDog() {
RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
try {
// 只有一个线程能获取到锁
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
// todo 实际要执行的方法
doSomeThings();
System.out.println("getLock: " + Thread.currentThread().getId());
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}

注意:

1、等待时间(waittime)设置为 0,表示只抢一次锁,抢不到就放弃

2、一定要释放锁,卸载 finnally 中

Redisson 看门狗机制

开启一个监听线程,如果方法还没有执行完,就自动重置 redis 锁的过期时间

原理:

1、监听当前的线程,默认过期时间是 30s,没 10s 续期一次

2、如果线程挂掉,就不会续期

Redis  + Caffeine多级缓存实现

 首先引入maven依赖

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version> <!– 请根据需要选择最新版 –>
</dependency>

什么是Caffeine:

Caffeine 是一个高性能、基于 Java 8 的本地缓存库,由 Ben Manes 开发,是 Google Guava Cache 的继任者。它在设计上借鉴了 Guava Cache 的优点,并引入了更先进的缓存淘汰算法(如 Window TinyLFU),显著提升了性能和内存效率。

一、Caffeine 核心特性

  • 高性能:采用 Window TinyLFU 淘汰策略,在高并发场景下表现优异。
  • 自动加载:支持通过 LoadingCache 自动加载缺失的缓存项。
  • 异步支持:提供 AsyncLoadingCache,支持异步加载和返回 CompletableFuture。
  • 丰富的过期策略:
    • 基于写入时间(expireAfterWrite)
    • 基于访问时间(expireAfterAccess)
    • 自定义过期(expireAfter)
  • 大小限制:可设置最大缓存条目数(maximumSize)或权重(maximumWeight)。
  • 监听器:支持缓存项被移除时的回调(RemovalListener)。
  • 统计信息:可开启命中率、加载次数等指标统计(recordStats)。

二、基本使用示例

1. 手动缓存(Manual Cache)

Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();

// 手动放入
cache.put("key1", "value1");

// 获取(可能为 null)
String value = cache.getIfPresent("key1");

// 获取或计算(若不存在)
String value2 = cache.get("key2", k -> createValue(k));

2. 自动加载缓存(Loading Cache)

LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build(key -> getValueFromDB(key)); // 自动加载函数

String value = loadingCache.get("someKey"); // 自动调用加载函数

3. 异步加载缓存(Async Loading Cache)

AsyncLoadingCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.buildAsync(key -> CompletableFuture.supplyAsync(() -> getValueFromDB(key)));

CompletableFuture<String> future = asyncCache.get("someKey");


三、常用配置详解

配置方法说明
maximumSize(long) 设置缓存最大条目数(基于条目数量)
maximumWeight(long) + weigher(Weigher) 按权重限制总大小(如按对象字节数)
expireAfterWrite(Duration) 写入后多久过期
expireAfterAccess(Duration) 最后一次访问后多久过期
refreshAfterWrite(Duration) 写入后多久可刷新(异步刷新,不影响当前读)
removalListener(RemovalListener) 缓存项被移除时触发回调
recordStats() 启用统计信息(可通过 cache.stats() 查看)

⚠️ 注意:expireAfterWrite 和 expireAfterAccess 不会主动清理过期数据,而是通过缓存操作(如 get/put)触发“惰性清理”。


四、淘汰策略:Window TinyLFU

Caffeine 使用 Window TinyLFU 算法,结合了:

  • TinyLFU:近似记录访问频率,节省内存;
  • Window(窗口)机制:保留新进入的条目一段时间,避免“突发访问”被误淘汰。

相比 LRU 或 FIFO,该策略在各种访问模式下(如热点数据、突发流量)都表现出色。

五、Redis+Caffeine实战

/**
* 分页获取所有公共空间的图表(带多级缓存)
* 缓存策略: Caffeine(L1) -> Redis(L2) -> DB
*/
@Override
public Page<DiagramVO> getPublicDiagramsByPage(DiagramQueryRequest pageRequest) {
int current = pageRequest.getCurrent();
int pageSize = pageRequest.getPageSize();
// 构造key
String redisKey = String.format(RedisPrefixConstant.ALL_DIAGRAM + "%s:%s:", current, pageSize);
String cacheKey = String.format(CachePrefixConstant.ALL_DIAGRAM + "%s:%s", current, pageSize);
// 先查询Caffeine中是否存在
Page<DiagramVO> cachePage = diagramsPageCache.getIfPresent(cacheKey);
if (cachePage == null) {
// 本地缓存为空,去查询redis
// 先查询redis是否存在
String pageStr = stringRedisTemplate.opsForValue().get(redisKey);
if (StringUtils.isEmpty(pageStr)) {
// 如果Redis中是空的话,就查询数据库并构造缓存
Page<Diagram> page = new Page<>(pageRequest.getCurrent(), pageRequest.getPageSize());
LambdaQueryWrapper<Diagram> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNull(Diagram::getSpaceId);
Page<Diagram> resultPage = this.page(page, queryWrapper);
List<DiagramVO> diagramVOList = resultPage.getRecords().stream()
.map(DiagramVO::objToVo)
.toList();
Page<DiagramVO> resPage = new Page<>(pageRequest.getCurrent(), pageRequest.getPageSize());
resPage.setRecords(diagramVOList);
resPage.setTotal(resultPage.getTotal());
String jsonStr = JSONUtil.toJsonStr(resPage);
// 设置Redis缓存
stringRedisTemplate.opsForValue().set(redisKey, jsonStr, RandomUtil.randomInt(1, 5), TimeUnit.MINUTES);
// 设置Caffeine缓存
diagramsPageCache.put(cacheKey, resPage);
return resPage;
} else {
// redis中不为空的话,直接反序列化之后返回给前端
Page<DiagramVO> page = JSONUtil.toBean(pageStr, Page.class);
// 同时设置本地缓存
diagramsPageCache.put(cacheKey, page);
return page;
}
}
// 本地缓存不为空,直接返回
return cachePage;
}

这样,当我们在查询图表分页数据的时候就会先查询本地缓存,如果本地缓存不存在就去查询Redis缓存并写入的本地缓存,如果Redis中也不存在,那么直接去查询数据库,并先写入到Redis然后写入到本地缓存中。

赞(0)
未经允许不得转载:171主机测评 » 数据查询慢怎么办?如何优化?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址