【Java】缓存击穿解决方案
文章目录
- 什么是SingleFlight?
- 优化
- 缺点
- 优化策略
什么是SingleFlight?
SingleFlight是go语言中sync包中的一个东西。它用于确保在并发环境下某个操作(例如,函数调用)即使被多个goroutine同时请求,也只会被执行一次。这对于防止重复的、昂贵的操作(如数据库查询、HTTP请求等)被不必要地多次执行是非常有用的。
使用 sync.SingleFlight,可以确保对于同一个键的并发请求,在缓存失效的情况下,只有一个请求会去加载数据(例如从数据库中),而其他并发的请求会等待这个加载操作完成,并共享相同的结果。这样,即便缓存失效,也不会因为大量的并发请求而对数据库或后端服务产生压力。
具体来说,当缓存失效时,第一个到达的请求会触发数据加载的操作(如数据库查询),而其他同时到达的请求会等待这个操作的完成。一旦数据被加载,它会被返回给所有等待的请求,并重新被放入缓存中。这个过程中,sync.SingleFlight 保证了数据加载函数只被调用一次,避免了不必要的重复处理。
SingleFlight主要提供以下功能:
● Do(key string, fn func() (interface{}, error)): 这是SingleFlight最核心的方法。当多个goroutine同时调用Do方法时,只有一个会真正执行传入的fn函数,其它等待这个函数执行完成。执行完成后,返回的结果和错误将会被返回给所有调用Do方法的goroutine。这里的key是用来区分不同操作的唯一标识。
● DoChan(key string, fn func() (interface{}, error)): 与Do类似,但它返回一个channel,你可以从这个channel中读取执行结果。
● Forget(key string): 这个方法用于清除SingleFlight中缓存的结果,以便于同一个key对应的函数在未来可以再次被执行。
● DupSuppressed() int64: 返回被SingleFlight机制抑制的重复调用次数。
SingleFlight的一个常见用途是缓存层,避免在缓存失效时由于缓存击穿而导致大量请求直接落到数据库。
如下是在写go语言的时候的使用SingleFight解决缓存击穿的代码。
var g singleflight.Groupfunc getCachedData(key string) (data interface{}, err error) {// 使用Do方法确保对于同一个key的请求,函数只会被执行一次v, err, _ := g.Do(key, func() (interface{}, error) {// 这里是实际的获取数据的操作,比如从数据库读取return getDataFromDatabase(key)})return v, err
}func getDataFromDatabase(key string) (interface{}, error) {// 模拟数据库操作// ...return data, nil
}
优化
SingleFlight 是一种用于减少重复工作的工具,特别是在并发编程中处理类似缓存击穿这样的问题时。尽管它非常有用,但也有一些潜在的缺点和限制:
缺点
- 资源锁定:如果用于一个长时间运行的操作,SingleFlight 会阻止其他所有相关的请求,直到这个操作完成。这可能导致长时间的等待,特别是在操作非常耗时的情况下。
- 错误传播:如果共享的操作因为某些原因失败了,这个错误会被传播给所有等待的请求。在某些情况下,单独重试可能更合适。
- 内存压力:在高并发情况下,如果许多不同的键被请求,SingleFlight 结构可能占用大量内存。
- 不适合高变动数据:对于频繁变化的数据,使用 SingleFlight 可能不太有效,因为一旦数据被缓存,就需要等待旧数据失效才能获取新数据。
- 分布式锁:缺乏对分布式场景下的解决方案
优化策略
- 设置超时:为 SingleFlight 中的操作设置合理的超时时间,可以防止一个慢操作阻塞其他请求过长时间。
- 错误重试机制:对于某些操作,特别是网络请求等,实现自动重试逻辑可能会有帮助,而不是直接将一个失败共享给所有请求。
- 限制并发数量:可以对 SingleFlight 正在进行的操作数量设置上限,以减少内存压力。
- 数据版本控制:对于频繁变化的数据,可以结合数据版本控制,确保即使在数据更新的时候也能获取到最新的数据。
对上面的缺点进行优化,得到如下Java代码。
package blossom.project.towelove.common.utils;import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;/*** @author: ZhangBlossom* @date: 2024/1/9 22:06* @contact: QQ:4602197553* @contact: WX:qczjhczs0114* @blog: https://blog.csdn.net/Zhangsama1* @github: https://github.com/ZhangBlossom* @description: 对于go语言中解决缓存击穿工具SingleFlight的实现*/
public abstract class SingleFlight<K, V> {private final ConcurrentHashMap<K, CompletableFuture<V>> ongoingOperations = new ConcurrentHashMap<>();/*** 版本号控制,使得每次进行更新的时候,一定都是对最新数据进行更新*/private final ConcurrentHashMap<K, AtomicLong> versions = new ConcurrentHashMap<>();// 设置默认超时时间,例如3秒private final long defaultTimeout = 3 * 1000;protected abstract boolean acquireDistributedLock(K key);protected abstract void releaseDistributedLock(K key);/*** 更新指定键的版本号** @param key 键*/public void updateVersion(K key) {versions.compute(key, (k, version) -> {if (version == null) {return new AtomicLong(1);} else {version.incrementAndGet();return version;}});}/*** 获取指定键的当前版本号** @param key 键* @return 版本号*/private long getVersion(K key) {return versions.getOrDefault(key, new AtomicLong(0)).get();}/*** 确保对于同一个键,相关的操作只会被执行一次,并且其结果将被所有调用者共享.* 如果超时了没有complete,将会返回TimeoutException** @param key 唯一key* @param function 要执行的方法函数* @param timeout 超时时间,单位为ms,默认3000ms=3s* @return*/public CompletableFuture<V> doOrTimeout(K key, Function<K, V> function, long timeout, boolean useDistributedLock) {if (useDistributedLock && !acquireDistributedLock(key)) {CompletableFuture<V> future = new CompletableFuture<>();future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));return future;}try {long versionAtCallTime = getVersion(key);return ongoingOperations.compute(key, (k, existingFuture) -> {if (existingFuture == null || getVersion(k) != versionAtCallTime) {CompletableFuture<V> future = new CompletableFuture<>();CompletableFuture.runAsync(() -> {try {V result = function.apply(k);future.complete(result);} catch (Exception e) {future.completeExceptionally(e);} finally {ongoingOperations.remove(k);}});// 应用超时设置return future.orTimeout(timeout, TimeUnit.MILLISECONDS);}return existingFuture;});} finally {if (useDistributedLock) {releaseDistributedLock(key);}}}/*** 当前方法会异步执行任务,并保证只有一个key能执行function任务,其他任务进行等待* 同时,如果执行失败,那么允许设定重试次数。并且再次执行function方法。** @param key 执行方法唯一key* @param function 要执行的任务* @param retries 重试次数* @param timeout 超时时间* @param delayBetweenRetries 重试前延迟时间* @return*/public CompletableFuture<V> doOrRetry(K key, Function<K, V> function, int retries, long timeout,long delayBetweenRetries, boolean useDistributedLock) {if (useDistributedLock && !acquireDistributedLock(key)) {CompletableFuture<V> future = new CompletableFuture<>();future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));return future;}try {long versionAtCallTime = getVersion(key);return ongoingOperations.compute(key, (k, existingFuture) -> {if (existingFuture == null || getVersion(k) != versionAtCallTime) {CompletableFuture<V> future = new CompletableFuture<>();executeWithRetriesOrCompensate(future, key, function, null, retries, timeout, delayBetweenRetries, versionAtCallTime);return future;}return existingFuture;});} finally {if (useDistributedLock) {releaseDistributedLock(key);}}}/*** 当前方法会异步执行任务,并保证只有一个key能执行function任务,其他任务进行等待* 同时,如果执行失败,那么允许设定重试次数。并且执行compensation补偿方法。** @param key 执行方法唯一key* @param function 原有方法* @param compensation 补偿方法 在执行失败的时候执行* @param retries 重试次数* @param timeout 超时时间* @param delayBetweenRetries 重试前延迟时间* @return*/public CompletableFuture<V> doOrCompensate(K key, Function<K, V> function, Function<K, V> compensation,int retries, long timeout, long delayBetweenRetries,boolean useDistributedLock) {if (useDistributedLock && !acquireDistributedLock(key)) {CompletableFuture<V> future = new CompletableFuture<>();future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));return future;}try {long versionAtCallTime = getVersion(key);return ongoingOperations.compute(key, (k, existingFuture) -> {if (existingFuture == null || getVersion(k) != versionAtCallTime) {CompletableFuture<V> future = new CompletableFuture<>();executeWithRetriesOrCompensate(future, key, function, compensation, retries, timeout,delayBetweenRetries, versionAtCallTime);return future;}return existingFuture;});} finally {if (useDistributedLock) {releaseDistributedLock(key);}}}/*** @param future* @param key* @param function* @param compensation* @param retries* @param timeout* @param delayBetweenRetries* @param versionAtCallTime*/private void executeWithRetriesOrCompensate(CompletableFuture<V> future,K key, Function<K, V> function, Function<K, V> compensation,int retries, long timeout, long delayBetweenRetries,long versionAtCallTime) {CompletableFuture.runAsync(() -> {try {if (getVersion(key) != versionAtCallTime) {throw new IllegalStateException("Data version changed");}V result = function.apply(key);future.complete(result);} catch (Exception e) {if (retries > 0 && getVersion(key) == versionAtCallTime) {try {TimeUnit.MILLISECONDS.sleep(delayBetweenRetries);} catch (InterruptedException ignored) {}Function<K, V> nextFunction = (compensation != null) ? compensation : function;executeWithRetriesOrCompensate(future, key, nextFunction, compensation, retries - 1,timeout, delayBetweenRetries, versionAtCallTime);} else {future.completeExceptionally(e);}}}).orTimeout(timeout, TimeUnit.MILLISECONDS).exceptionally(ex -> {if (retries > 0 && ex instanceof TimeoutException && getVersion(key) == versionAtCallTime) {Function<K, V> nextFunction = (compensation != null) ? compensation : function;executeWithRetriesOrCompensate(future, key, nextFunction, compensation, retries - 1, timeout,delayBetweenRetries, versionAtCallTime);} else {future.completeExceptionally(ex);}return null;});}/*** 提供一个方式来异步地执行操作,并返回一个 CompletableFuture,* 该 CompletableFuture 可以让调用者在未来某个时刻获取操作的结果** @param key* @param function* @return*/public CompletableFuture<CompletableFuture<V>> doChan(K key, Function<K, V> function) {return CompletableFuture.completedFuture(ongoingOperations.computeIfAbsent(key, k -> {CompletableFuture<V> future = new CompletableFuture<>();CompletableFuture.runAsync(() -> {try {V result = function.apply(k);future.complete(result);} catch (Exception e) {future.completeExceptionally(e);} finally {ongoingOperations.remove(k);}});return future;}));}/*** 从 ongoingOperations 映射中移除了给定的键** @param key*/public void forget(K key) {ongoingOperations.remove(key);}}/*** 假设一个基于Redis的SingleFlight分布式锁实现* 从而使得SingleFlight支持分布式锁* @param <K>* @param <V>*/
class RedisSingleFlight<K, V> extends SingleFlight<K, V> {// Redis 或其他分布式锁机制的实现@Overrideprotected boolean acquireDistributedLock(K key) {return false;}@Overrideprotected void releaseDistributedLock(K key) {}// 如果需要,可以添加特定于 Redis 的其他方法或逻辑
}
相关文章:
【Java】缓存击穿解决方案
文章目录 什么是SingleFlight?优化缺点优化策略 什么是SingleFlight? SingleFlight是go语言中sync包中的一个东西。它用于确保在并发环境下某个操作(例如,函数调用)即使被多个goroutine同时请求,也只会被执…...
【HarmonyOS】掌握 Stage 模型的核心概念与应用
从今天开始,博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”,对于刚接触这项技术的小伙伴在学习鸿蒙开发之前,有必要先了解一下鸿蒙,从你的角度来讲,你认为什么是鸿蒙呢?它出现的意义又是…...
2024年甘肃省职业院校技能大赛 “信息安全管理与评估”赛项样题卷①
2024年甘肃省职业院校技能大赛 高职学生组电子与信息大类信息安全管理与评估赛项样题 第一阶段:第二阶段:模块二 网络安全事件响应、数字取证调查、应用程序安全第二阶段 网络安全事件响应第一部分 网络安全事件响应第二部分 数字取证调查第三部分 应用程…...
我的AI之旅开始了
知道重要,但是就是不动。 今天告诉自己,必须开始学习了。 用这篇博文作为1月份AI学习之旅的起跑点吧。 从此,无惧AI,无惧编程。 AI之路就在脚下。 AI,在我理解,就是让机器变得更加智能&#…...
Day25 235二叉搜索树的公共祖先 701二叉搜索树插入 450二叉搜索树删除
235 二叉搜索树的最近公共祖先 如果利用普通二叉树的方法,就是利用后序遍历回溯从低向上搜索,遇到左子树有p,右子树有q,那么当前结点就是最近公共祖先。本题是二叉搜索树,所以说是有序的,一定能够简化上面…...
android系列-init 挂载文件系统
1.init 挂载文件系统 //android10\system\core\init\main.cppint main(int argc, char** argv) {return FirstStageMain(argc, argv); } //android10\system\core\init\first_stage_init.cppint FirstStageMain(int argc, char** argv) {CHECKCALL(mount("tmpfs",…...
Spring 七种事务传播性介绍
作者:vivo 互联网服务器团队 - Zhou Shaobin 本文主要介绍了Spring事务传播性的相关知识。 Spring中定义了7种事务传播性: PROPAGATION_REQUIRED PROPAGATION_SUPPORTS PROPAGATION_MANDATORY PROPAGATION_REQUIRES_NEW PROPAGATION_NOT_SUPPORTED…...
Count the Colors ZOJ - 1610
题目链接 题意: 给定n个区间[ l, r ]和颜色c, 每次给[l, r]涂上c这个颜色. 后面的涂色会覆盖之前的涂色. 最后要求输出区间[0, 8000]中每种颜色及其出现的次数, 如果该颜色没有出现过则不输出. 思路:典型的线段树区间染色问题,一般这种题…...
MATLAB点云处理总目录
一、点云滤波 原始点云包含过多噪点和冗余点,滤波和采样往往是点云预处理的必要步骤 1.滤波 重复点去除 NAN或INF无效点去除 自定义半径滤波 2.采样 基于空间格网的点云抽稀 随机下采样 均匀体素下采样 非均匀体素下采样 二、邻近搜索 如何组织点云快速获取当前…...
C语言逗号表达式如何计算
在 C 语言中,逗号表达式是一种特殊的表达式形式,它由逗号分隔的多个表达式组成。 逗号表达式的计算过程如下:1、从左到右依次计算每个表达式的值。2、最终返回的值是最右边表达式的值。3、逗号表达式的求值过程是顺序执行的,不会…...
Ubuntu 本地部署 ChatGPT-Next-Web
Ubuntu 本地部署 ChatGPT-Next-Web 文章目录 Ubuntu 本地部署 ChatGPT-Next-Web ChatGPT-Next-Web 项目地址:https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 本文主要演示如何在 Ubuntu 本地(默认是端口 3000)部署 ChatGPT-Next-Web&am…...
小程序商城搭建:快速入门指南
随着移动互联网的普及,小程序商城逐渐成为了商家们进行线上销售的重要渠道。如果你也想搭建一个小程序商城,那么本文将为你介绍如何使用乔拓云这一第三方小程序搭建平台来轻松搭建自己的小程序商城。 一、选择合适的第三方小程序搭建平台 在选择第三方小…...
c# windows10大小端试
测试代码: unsafe public void ceshi() {byte[] by BitConverter.GetBytes(0x12345678);Debug.WriteLine(" byte[0] 0x" by[0].ToString("x2"));Debug.WriteLine(" byte[1] 0x" by[1].ToString("x2"));Debug.WriteLi…...
【算法专题】动态规划之斐波那契数列模型
动态规划1.0 动态规划 - - - 斐波那契数列模型1. 第 N 个泰波那契数2. 三步问题3. 使用最小花费爬楼梯4. 解码方法 动态规划 - - - 斐波那契数列模型 1. 第 N 个泰波那契数 题目链接 -> Leetcode -1137. 第 N 个泰波那契数 Leetcode -1137. 第 N 个泰波那契数 题目&…...
K2P路由器刷OpenWrt官方最新版本固件OpenWrt 23.05.2方法 其他型号的智能路由器OpenWrt固件刷入方法也基本上适用
最近路由器在开机时总出问题,于是就那他来开刀,直接刷一个OpenWrt官方最新版本的固件, 刷其他第三方的固件总是觉得不安全, 而且很多第三方固件都带了些小工具,始终会有安全隐患, 而且占用内存空间太多,本来这个东西就没有多少内存,于是就干脆刷一个官方的原始固件(才6.3M, 相…...
AI大语言模型会带来了新一波人工智能浪潮?
以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮,可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…...
How to view the high-tech zone atmospheric project
How to view the high-tech zone atmospheric project 问题与建议登录界面没有验证码部分页面加载时间过长联动型下拉列表框点击反应迟钝页面缺乏导航没有采用https协议没有完成域名实名认证左侧菜单区不能收缩大屏区域功能图层不能完全隐藏部分页面表单控件没有文案提示其功能…...
sqlalchemy 中的缓存机制解释
SQLAlchemy 的缓存机制主要涉及两个层面:会话(Session)缓存和查询缓存。这两种缓存机制对于提升应用性能和数据一致性都非常重要。下面详细解释这两种缓存机制: 1. 会话(Session)缓存 会话缓存是 SQLAlch…...
网络安全B模块(笔记详解)- 漏洞扫描与利用
漏洞扫描与利用 1.通过Kali对服务器场景server2003以半开放式不进行ping的扫描方式并配合a,要求扫描信息输出格式为xml文件格式,从生成扫描结果获取局域网(例如172.16.101.0/24)中存活靶机,以xml格式向指定文件输出信息(使用工具Nmap,使用必须要使用的参数),并将该操…...
【C语言】指针——从底层原理到应用
C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻 目录 一、前言二、变量与指针的本质 1. 内存地址2. 32位与64位系统3. 变量4. 指针变量5. 操作指针变量 5.1 指针变量自身的值5.2 获取指针变量所指向的数据5.3 以什么样的数据类型来使用/解释指针变量所指…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
WEB3全栈开发——面试专业技能点P4数据库
一、mysql2 原生驱动及其连接机制 概念介绍 mysql2 是 Node.js 环境中广泛使用的 MySQL 客户端库,基于 mysql 库改进而来,具有更好的性能、Promise 支持、流式查询、二进制数据处理能力等。 主要特点: 支持 Promise / async-await…...
