redis+lua实现分布式限流

redis+lua实现分布式限流

文章目录

  • redis+lua实现分布式限流
    • 为什么使用redis+lua实现分布式限流
    • 使用ZSET也可以实现限流,为什么选择lua的方式
    • 实现
      • 依赖
      • lua脚本
      • yaml
      • 代码实现
    • Jmeter压测

为什么使用redis+lua实现分布式限流

  1. 原子性:通过Lua脚本执行限流逻辑,所有操作在一个原子上下文中完成,避免了多步操作导致的并发问题。
  2. 灵活性:Lua脚本可以编写复杂的逻辑,比如滑动窗口限流,易于扩展和定制化。
  3. 性能:由于所有逻辑在Redis服务器端执行,减少了网络往返,提高了执行效率。

使用ZSET也可以实现限流,为什么选择lua的方式

使用zset需要额度解决这些问题

  1. 并发控制:需要额外的逻辑来保证操作的原子性和准确性,可能需要配合Lua脚本或Lua脚本+WATCH/MULTI/EXEC模式来实现。
  2. 资源消耗:长期存储请求记录可能导致Redis占用更多的内存资源。

为什么redis+zset不能保证原子性和准确性

  1. 多步骤操作:滑动窗口限流通常需要执行多个步骤,比如检查当前窗口的请求次数、添加新的请求记录、可能还需要删除过期的请求记录等。这些操作如果分开执行,就有可能在多线程或多进程环境下出现不一致的情况。
  2. 非原子性复合操作:虽然单个Redis命令是原子的,但当你需要执行一系列操作来维持限流状态时(例如,先检查计数、再增加计数、最后可能还要删除旧记录),没有一个单一的Redis命令能完成这些复合操作。如果在这系列操作之间有其他客户端修改了数据,就会导致限流不准确。
  3. 竞争条件:在高并发环境下,多个客户端可能几乎同时执行限流检查和增加请求的操作,如果没有适当的同步机制,可能会导致请求计数错误。

实现

依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.kang</groupId><artifactId>rate-limiter-project</artifactId><version>0.0.1-SNAPSHOT</version><name>rate-limiter-project</name><description>rate-limiter-project</description><properties><java.version>8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.6.2</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>31.0.1-jre</version> <!-- 请检查最新版本 --></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

lua脚本

-- KEYS[1] 是Redis中存储计数的key,,,
local key = KEYS[1]-- ARGV[1]是当前时间戳-[当前时间戳]
local now = tonumber(ARGV[1])-- ARGV[2]是最大请求次数-[最大请求次数]
local maxRequests = tonumber(ARGV[2])-- ARGV[3]是时间窗口长度-[时间窗口长度]
local windowSize = tonumber(ARGV[3])-- 获取当前时间窗口的起始时间
local windowStart = math.floor(now / windowSize) * windowSize-- 构建时间窗口内的key,用于区分不同窗口的计数
local windowKey = key .. ':' .. tostring(windowStart)-- 获取当前窗口的计数
local currentCount = tonumber(redis.call('get', windowKey) or '0')-- 如果当前时间不在窗口内,重置计数
if now > windowStart + windowSize thenredis.call('del', windowKey)currentCount = 0
end-- 检查是否超过限制
if currentCount + 1 <= maxRequests then-- 未超过,增加计数并返回成功,并设置键的过期时间为窗口剩余时间,以自动清理过期数据。如果超过最大请求次数,则拒绝请求redis.call('set', windowKey, currentCount + 1, 'EX', windowSize - (now - windowStart))return 1 -- 成功
elsereturn 0 -- 失败
end

yaml

server:port: 10086spring:redis:host: 127.0.0.1port: 6379database: 0lettuce:pool:max-active: 20max-idle: 10min-idle: 5

代码实现

在这里插入图片描述

启动类

package com.kang.limter;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@Slf4j
@SpringBootApplication
public class RateLimiterProjectApplication {public static void main(String[] args) {SpringApplication.run(RateLimiterProjectApplication.class, args);log.info("RateLimiterProjectApplication start success");}}

CacheConfig

package com.kang.limter.cache;import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.kang.limter.utils.LuaScriptUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;import static com.kang.limter.constant.SystemConstant.REDIS_RATE_LIMITER_LUA_SCRIPT_PATH;/*** @Author Emperor Kang* @ClassName CacheConfig* @Description 缓存配置* @Date 2024/6/13 10:07* @Version 1.0* @Motto 让营地比你来时更干净*/
@Slf4j
@Configuration
public class CacheConfig {/*** 缓存配置,加载lua脚本* @return*/@Bean(name = "rateLimiterLuaCache")public LoadingCache<String, String> rateLimiterLuaCache() {LoadingCache<String, String> cache = CacheBuilder.newBuilder()// 设置缓存的最大容量,最多100个键值对.maximumSize(100)// 设置缓存项过期策略:写入后2小时过期.expireAfterWrite(2, TimeUnit.HOURS)// 缓存统计信息记录.recordStats()// 构建缓存加载器,用于加载缓存项的值.build(new CacheLoader<String, String>() {@Overridepublic String load(String scriptPath) throws Exception {try {return LuaScriptUtils.loadLuaScript(scriptPath);} catch (Exception e) {log.error("加载lua脚本失败:{}", e.getMessage());return null;}}});// 预热缓存warmUpCache(cache);return cache;}/*** 预热缓存*/private void warmUpCache(LoadingCache<String, String> cache) {try {// 假设我们有一个已知的脚本列表需要预热List<String> knownScripts = Collections.singletonList(REDIS_RATE_LIMITER_LUA_SCRIPT_PATH);for (String script : knownScripts) {String luaScript = LuaScriptUtils.loadLuaScript(script);// 手动初始化缓存cache.put(script, luaScript);log.info("预加载Lua脚本成功: {}, length: {}", script, luaScript.length());}} catch (Exception e) {log.error("预加载Lua脚本失败: {}", e.getMessage(), e);}}
}
  • 这里使用缓存预热加快lua脚本的加载速度,基于JVM内存操作,所以很快

SystemConstant

package com.kang.limter.constant;/*** @Author Emperor Kang* @ClassName SystemConstant* @Description 系统常量* @Date 2024/6/12 19:25* @Version 1.0* @Motto 让营地比你来时更干净*/
public class SystemConstant {/*** 限流配置缓存key前缀*/public static final String REDIS_RATE_LIMITER_KEY_PREFIX = "outreach:config:limiter:%s";/*** 限流lua脚本路径*/public static final String REDIS_RATE_LIMITER_LUA_SCRIPT_PATH = "classpath:lua/rate_limiter.lua";
}

RateLimiterController

package com.kang.limter.controller;import com.kang.limter.dto.RateLimiterRequestDto;
import com.kang.limter.utils.RateLimiterUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import static java.lang.Thread.sleep;/*** @Author Emperor Kang* @ClassName RateLimiterController* @Description TODO* @Date 2024/6/12 19:33* @Version 1.0* @Motto 让营地比你来时更干净*/
@Slf4j
@RestController
@RequestMapping("/rate/limiter")
public class RateLimiterController {@Autowiredprivate RateLimiterUtil rateLimiterUtil;@PostMapping("/test")public String test(@RequestBody RateLimiterRequestDto rateLimiterRequestDto) {// 是否限流if (!rateLimiterUtil.tryAcquire(rateLimiterRequestDto.getInterfaceCode(), 5, 1000)) {log.info("触发限流策略,InterfaceCode:{}", rateLimiterRequestDto.getInterfaceCode());return "我被限流了InterfaceCode:" + rateLimiterRequestDto.getInterfaceCode();}log.info("请求参数:{}", rateLimiterRequestDto);try {log.info("开始加工逻辑");sleep(1000);} catch (InterruptedException e) {log.error("休眠异常");Thread.currentThread().interrupt();return "加工异常";}return "加工成功,成功返回";}
}

RateLimiterRequestDto

package com.kang.limter.dto;import lombok.Data;/*** @Author Emperor Kang* @ClassName RateLimiterRequestDto* @Description TODO* @Date 2024/6/12 19:39* @Version 1.0* @Motto 让营地比你来时更干净*/
@Data
public class RateLimiterRequestDto {/*** 接口编码*/private String interfaceCode;
}

ResourceLoaderException

package com.kang.limter.exception;/*** @Author Emperor Kang* @ClassName ResourceLoaderException* @Description 自定义资源加载异常* @Date 2024/6/12 18:10* @Version 1.0* @Motto 让营地比你来时更干净*/
public class ResourceLoaderException extends Exception{public ResourceLoaderException() {super();}public ResourceLoaderException(String message) {super(message);}public ResourceLoaderException(String message, Throwable cause) {super(message, cause);}public ResourceLoaderException(Throwable cause) {super(cause);}protected ResourceLoaderException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}

LuaScriptUtils

package com.kang.limter.utils;import com.kang.limter.exception.ResourceLoaderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;@Slf4j
public class LuaScriptUtils {/*** 从类路径下读取Lua脚本内容。* @param scriptPath 类路径下的Lua脚本文件路径* @return Lua脚本的文本内容*/public static String loadLuaScript(String scriptPath) throws ResourceLoaderException {Assert.notNull(scriptPath, "script path must not be null");try {// 读取lua脚本ResourceLoader resourceLoader = new DefaultResourceLoader();Resource resource = resourceLoader.getResource(scriptPath);try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {StringBuilder scriptBuilder = new StringBuilder();String line;while ((line = reader.readLine()) != null) {scriptBuilder.append(line).append("\n");}String lua = scriptBuilder.toString();log.debug("读取的lua脚本为: {}", lua);return lua;}} catch (Exception e) {log.error("Failed to load Lua script from path: {}", scriptPath, e);throw new ResourceLoaderException("Failed to load Lua script from path: " + scriptPath, e);}}
}

RateLimiterUtil

package com.kang.limter.utils;import com.google.common.cache.LoadingCache;
import com.kang.limter.exception.ResourceLoaderException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;import java.nio.charset.StandardCharsets;import static com.kang.limter.constant.SystemConstant.REDIS_RATE_LIMITER_KEY_PREFIX;
import static com.kang.limter.constant.SystemConstant.REDIS_RATE_LIMITER_LUA_SCRIPT_PATH;/*** @Author Emperor Kang* @ClassName RateLimiterUtil* @Description 限流工具类* @Date 2024/6/12 17:56* @Version 1.0* @Motto 让营地比你来时更干净*/
@Slf4j
@Component
public class RateLimiterUtil {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowired@Qualifier("rateLimiterLuaCache")private LoadingCache<String, String> rateLimiterLuaCache;/*** @param interfaceCode 接口标识* @param maxRequests   最大请求数* @param windowSizeMs  窗口大小* @return boolean* @Description 尝试获取令牌* @Author Emperor Kang* @Date 2024/6/12 17:57* @Version 1.0*/public boolean tryAcquire(String interfaceCode, int maxRequests, long windowSizeMs) {try {long currentTimeMillis = System.currentTimeMillis();String luaScript = rateLimiterLuaCache.get(REDIS_RATE_LIMITER_LUA_SCRIPT_PATH);log.info("缓存查询lua,length={}", luaScript.length());if(StringUtils.isBlank(luaScript)){log.info("从缓存中未获取到lua脚本,尝试手动读取");luaScript = LuaScriptUtils.loadLuaScript(REDIS_RATE_LIMITER_LUA_SCRIPT_PATH);}// 二次确认if(StringUtils.isBlank(luaScript)){log.info("lua脚本加载失败,暂时放弃获取许可,不再限流");return true;}// 限流核心逻辑String finalLuaScript = luaScript;Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {// 用于存储的keybyte[] key = String.format(REDIS_RATE_LIMITER_KEY_PREFIX, interfaceCode).getBytes(StandardCharsets.UTF_8);// 当前时间(毫秒)byte[] now = String.valueOf(currentTimeMillis).getBytes(StandardCharsets.UTF_8);// 最大请求数byte[] maxRequestsBytes = String.valueOf(maxRequests).getBytes(StandardCharsets.UTF_8);// 窗口大小byte[] windowSizeBytes = String.valueOf(windowSizeMs).getBytes(StandardCharsets.UTF_8);// 执行lua脚本return connection.eval(finalLuaScript.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, 1, key, now, maxRequestsBytes, windowSizeBytes);});Assert.notNull(result, "执行lua脚本响应结果为null");// 获取结果return result == 1L;} catch (ResourceLoaderException e) {log.error("加载lua脚本失败", e);} catch (Exception e){log.error("执行限流逻辑异常", e);}return true;}
}

lua脚本

-- KEYS[1] 是Redis中存储计数的key,,,
local key = KEYS[1]-- ARGV[1]是当前时间戳-[当前时间戳]
local now = tonumber(ARGV[1])-- ARGV[2]是最大请求次数-[最大请求次数]
local maxRequests = tonumber(ARGV[2])-- ARGV[3]是时间窗口长度-[时间窗口长度]
local windowSize = tonumber(ARGV[3])-- 获取当前时间窗口的起始时间
local windowStart = math.floor(now / windowSize) * windowSize-- 构建时间窗口内的key,用于区分不同窗口的计数
local windowKey = key .. ':' .. tostring(windowStart)-- 获取当前窗口的计数
local currentCount = tonumber(redis.call('get', windowKey) or '0')-- 如果当前时间不在窗口内,重置计数
if now > windowStart + windowSize thenredis.call('del', windowKey)currentCount = 0
end-- 检查是否超过限制
if currentCount + 1 <= maxRequests then-- 未超过,增加计数并返回成功,并设置键的过期时间为窗口剩余时间,以自动清理过期数据。如果超过最大请求次数,则拒绝请求redis.call('set', windowKey, currentCount + 1, 'EX', windowSize - (now - windowStart))return 1 -- 成功
elsereturn 0 -- 失败
end

Jmeter压测

在这里插入图片描述

在这里插入图片描述

  • 200次请求/s,限流了195,而我们设置的最大令牌数就是5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1451862.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机视觉】人脸算法之图像处理基础知识(三)

图像处理基础知识&#xff08;三&#xff09; 1.图像二值化 顾名思义&#xff0c;图像二值化是指一张图像上只有两种大小的像素值&#xff0c;常用的是0和255&#xff0c;0表示背景&#xff0c;255表示前景。这种处理方式是非常重要的&#xff0c;大部分的图像处理都会经历该…

简单Mesh多线程合并,使用什么库性能更高

1&#xff09;简单Mesh多线程合并&#xff0c;使用什么库性能更高 2&#xff09;Unity Semaphore.WaitForSignal耗时高 3&#xff09;VS编辑的C#代码注释的中文部分乱码 4&#xff09;变量IntPtr m_cachePtr切换线程后变空 这是第389篇UWA技术知识分享的推送&#xff0c;精选了…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】前馈神经网络

前馈神经网络又叫全连接神经网络、多层感知器&#xff0c;在网络中信息由输入到输出单向传递&#xff0c;具体特点有&#xff1a; 个神经元分别属于不同的层&#xff0c;层内无连接相邻两层之间的神经元全部两两连接整个网络中无反馈&#xff0c;信号从输入层像输出层单向传播…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】梯度的反向传播算法

矩阵微积分&#xff08;Matrix Calculus&#xff09; 在开始之前&#xff0c;需要先了解矩阵微积分的一些计算规则。 首先&#xff0c;对于矩阵微积分的表示&#xff0c;通常由两种符号约定&#xff1a; 分母布局 标量关于向量的导数为列向量 向量关于标量的导数为行向量 N维…

【LeetCode:2786. 访问数组中的位置使分数最大 + 递归 + 记忆化缓存 + dp】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Real3D:利用真实世界图像扩展3D重建模型

原理&#xff1a; 在3D重建领域&#xff0c;单视图重建任务由于其固有的不确定性而充满挑战。为了克服这一难题&#xff0c;研究者们一直在探索如何利用大型数据集训练模型以学习形状和纹理的通用先验知识。然而&#xff0c;现有训练方法依赖于合成数据或多视图捕获&#xff0c…

U-Mail国产信创邮件系统,让企业通信更加安全可控

信息技术应用创新产业&#xff0c;即信创产业&#xff0c;是信息化建设的核心&#xff0c;它涵盖了从硬件到软件的一系列关键技术。信创产业的目标是通过自主研发&#xff0c;减少对外部技术的依赖&#xff0c;增强信息安全&#xff0c;并提升国内产业的全球竞争力。该产业主要…

java打印99乘法表

public class NineNineMulTable{public static void main(String[] args){for(int i 1; i < 9; i ){for(int j 1; j < i; j ){System.out.print(j " * " i " " i * j "\t");//再次先输出j在输出i是打印出来是1*2&#xff0c;2*2}S…

Allegro X PCB设计小诀窍--如何在Allegro X中快速设置快捷键

背景介绍&#xff1a;我们在进行PCB设计时&#xff0c;经常会用到一些高频次操作&#xff0c;例如移动、复制、删除、旋转、绘制走线、铺铜等&#xff0c;这些操作在软件中通常需要点击对应命令菜单来实现。为了点击这些菜单&#xff0c;设计人员需要通过鼠标频繁的在设计界面进…

积木搭建游戏-第13届蓝桥杯省赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第83讲。 积木搭建游戏&…

三款有3D效果的js图表库

1、G2简洁的渐进式可视化语法。https://g2.antv.antgroup.com/manual/extra-topics/3d-charts 2、 https://www.highcharts.com/https://www.highcharts.com/ 3、https://www.fusioncharts.com/charts/pie-doughnut-charts/donut-chart-in-3d?frameworkjavascripthttps://www…

30V转5V3.5A大电流芯片 30降压12V3.5A DCDC低功耗恒压IC-H4012-车充芯片

H4012芯片是一款同步降压型DC-DC转换器&#xff0c;为高效率和大电流应用设计。它内置了30V耐压的MOS&#xff0c;并支持3.5A的持续输出电流&#xff0c;使得它在需要高功率输出的应用中表现出色。此外&#xff0c;H4012的输出电压可调&#xff0c;可支持100%占空比&#xff0c…

el-cascader 支持多层级,多选(可自定义限制数量),保留最后一级

多功能的 el-cascader 序言&#xff1a;最近遇到一个需求关于级联的&#xff0c;有点东西&#xff0c;这里是要获取某个产品类型下的产品&#xff0c;会存在产品类型和产品在同一级的情况&#xff0c;但是产品类型不能勾选&#xff1b; 情况1&#xff08;二级菜单是产品&…

深度学习笔记: 最详尽估算送达时间系统设计

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; 估算送达时间 1. 问题陈述 构建一个模型来估算在给定订单详情、市场条件和交通状况下的总送达时间。 为…

python-jupyter notebook安装教程

&#x1f308;所属专栏&#xff1a;【python】✨作者主页&#xff1a; Mr.Zwq✔️个人简介&#xff1a;一个正在努力学技术的Python领域创作者&#xff0c;擅长爬虫&#xff0c;逆向&#xff0c;全栈方向&#xff0c;专注基础和实战分享&#xff0c;欢迎咨询&#xff01; 您的…

Java内存模型,堆、栈和方法区的区别

Java内存管理是Java虚拟机&#xff08;JVM&#xff09;技术的核心之一。了解Java内存管理对于提高程序性能、解决内存泄漏和优化资源利用至关重要。 一、Java内存模型&#xff08;Java Memory Model, JMM&#xff09; Java内存模型描述了Java程序中变量&#xff08;包括实例字…

5.1 Python 函数的参数类型

1. 实参与形参 形参: 函数定义阶段, 括号内定义的参数名(变量名), 形参的表现形式只有一种就是参数命. 实参: 函数调用阶段, 括号内传入的参数值(变量值), 实参的表现形式有很多种(核心: 可以引用到值).两者之间的关系: 函数调用阶段 --> 实参的值绑定给形参名. 函数调用完…

GraphQL(9):Spring Boot集成Graphql简单实例

1 安装插件 我这边使用的是IDEA&#xff0c;需要先按照Graphql插件&#xff0c;步骤如下&#xff1a; &#xff08;1&#xff09;打开插件管理 在IDEA中&#xff0c;打开主菜单&#xff0c;选择 "File" -> "Settings" (或者使用快捷键 Ctrl Alt S …

什么是快乐?

什么是快乐&#xff1f; What is Happiness? 1. 快乐不是追求外在的物质&#xff0c;而是内心的平静与满足。当我们学会感恩&#xff0c;懂得珍惜眼前的一切&#xff0c;心中自然会充满喜悦。快乐并非来自拥有更多&#xff0c;而是感受到已经拥有的足够。每一天都怀抱感激之情…

最新情侣飞行棋高阶羞羞版,解锁私密版情侣小游戏,文末有福利!

今天要跟大家聊聊一种特别有意思的游戏——情侣飞行棋羞羞版。别急着脸红&#xff0c;这可是专为情侣设计的游戏&#xff0c;让你们在轻松愉快的氛围中&#xff0c;增进了解&#xff0c;加深感情。 谈恋爱&#xff0c;不就是两个人在一起&#xff0c;做些有趣的事情吗&#xf…