Redis 缓存进阶篇,缓存真实数据和缓存文件指针最佳实现?如何选择?

目录

一. 场景再现、具体分析

二. 常见实现方案及方案分析

2.1 数据库字段最大存储理论分析

2.2 最佳实践方式分析

三. 接口选择、接口分析

四. 数据库设计

4.1 接口缓存表设计

4.1.1 建表SQL

4.1.2 查询接口设计

4.2 调用日志记录表设计

4.2.1 建表SQL

4.2.2 查询SQL

五. 项目代码落地实现

5.1 通用代码编写

5.1.1 创建接口请求参数实体类

5.1.2 创建接口响应通用返回结果类

5.1.3 创建接口缓存表实体类

5.1.4 创建接口缓存表Mapper接口

5.1.5 创建接口活动日志表实体类

5.1.6 创建接口活动记录表Mapper接口

5.1.7 创建 Controller 层控制器类

5.1.8 Service 层方法公共变量和方法创建

5.2 Redis 缓存真实数据业务代码实现

5.3 Redis 缓存文件指针,通过指针再读取文件内容业务实现

5.4 二者混合使用

六. 简要总结


一. 场景再现、具体分析

这里我们来设想以下场景。

假设你的个人项目或公司分给你的项目需求需要查询公司信息,所以对接了启信宝、企查查等第三方公司信息查询接口;此外,为了节省成本,当我们首次查询一个新公司的时候,如果调用企查查成功,则将企查查返回的数据保存到我们本地的数据库中,同时将数据库数据读到缓存中提高查询效率,后续我们再次查询此公司,则不会发起实际调用,而是从缓存获取数据返回。

对于缓存公司数据的方式,通常我们有以下两种方案。

方案一:将企查查接口返回的数据报文直接存储到数据库,可以使用 VARCHAR(MAX),TEXT,MEDIUMTEXT等字段存储,根据接口返回数据的大小选择;然后将接口原文直接缓存到 Redis 中;

方案二:将企查查接口返回的数据报文存储到文件中(文件可以存储在服务器上,也可以存储到 minio,阿里云OSS等云服务器上),然后将文件的指针(文件的地址)存储到数据库中,使用 VARCHAR 即可,同时为了提高查询效率,也将文件的指针缓存到 Redis 中;

再比如,以我们当前的 CSDN 网站为例,用户编写的文章,要进行暂存或发布,文章应该如何存储?是直接以文本格式存储到数据库中?还是存储到文本文件,然后将文件指针存储到数据中?

这种场景其实并不罕见,那么接下来,我们就来探讨一下,这两种文件存储方案的优缺点吧!

二. 常见实现方案及方案分析

2.1 数据库字段最大存储理论分析

如下表格所示,是MySQL中比较常见的几个存储文本的字段类型,目前比较常用的字符集有 utf8mb4 和 utf8mb3,更推荐使用 utf8mb4。

字段类型最大字节数计算基础

理论最大汉字数

utf8mb3,3字节)

理论最大汉字数

utf8mb4,4字节)

适用场景使用频率
VARCHAR65,53365,535 - 221,844 字16,383 字短文本极高
TEXT65,535固定限制21,845 字16,383 字普通长文本一般
JSON1,073,741,823LONGTEXT 级357,913,941 字268,435,455 字JSON 数据较低
MEDIUMTEXT16,777,21516MB5,592,405 字4,194,303 字大文本较低

重点来啦!!!小编这里尽可能简单的说一下,在数据库的底层,所有的数据都是存储在数据页中的,一张数据页的大小就是16KB,即16384字节,并且数据库的 InnoDB 存储引擎大数据存储机制,当一条数据的某个字段,以VARCHAR为例,大于数据页的一半8K(8192字节)时,则不会将字段值真实存储在当前数据页,而是存储到"溢出页",然后数据库底层会把"溢出页"的指针值存储到字段值中,当我们读取数据的时候,数据库底层读取到大字段的值之后,会根据指针值进行IO操作,将"溢出页"的值读出来然后进行返回;而TEXT、JSON、MEDIUMTEXT其它三者更不必多说,都是将数据存储在单独的数据页,然后记录中的字段值则是存储数据页的指针地址,具体见下表格。

行格式溢出阈值VARCHAR(10KB)存储TEXT 存储JSON 存储MEDIUMTEXT 存储版本建议
Redundant完整存行内完整存行内完整存行内完整存行内已淘汰
Compact768 字节768B 前缀 + 溢出链768B 前缀 + 溢出链768B 前缀 + 溢出链768B 前缀 + 溢出链兼容旧系统
Dynamic8 KB20B 指针 + 完整溢出链20B 指针 + 完整溢出链20B 指针 + 完整溢出链20B 指针 + 完整溢出链MySQL 5.7+默认

不难发现,在 MySQL5.7 版本之后,行格式已经做了进一步优化,采用了 Dynamic 动态行格式,可以简单理解为,当我们存储的一条数据某个字段大小小于 8K 时,MySQL选择直接存储完整数据到行内;但是当字段大小大于 8K时,则会将真实数据存储到其他数据页,字段则存储指针值。当然了,数据库底层的设计极为精妙,也有可能某个数据页第一条要存储的数据就是大数据,此时有16KB的空间,空间足够,一般是全量存储;但如果一个数据页已经存储了100条数据,剩下6KB的大小,但要存储一行8KB的大小的记录,此时就有可能触发溢出存储机制,将大数据存储在"溢出页"。

所以我们本篇文章就以 MySQL5.7+ 之后的版本为例,可以得出以下结论,一定牢记!!!

当使用 VARCHAR、TEXT、JSON、MEDIUMTEXT 字段存储数据时,若字段大小大于8K,则数据库行内字段值只存储文件指针,真实数据存储在数据页;若字段大小小于8K,都会把真实数据存储在行内;唯一的区别就是它们的最大字节数据不同会导致数据页的数量不同仅此而已。数据页数量越多,也就意味着数据库要进行更多次的随机 IO 去读取数据页,可能会影响数据库的性能。

2.2 最佳实践方式分析

通过上面的一顿分析,我们对于这四种存储字段已经比较清楚了,那么现在,我们就从实用的角度来考虑到底选择哪种字段?

小编更推荐使用 VARCHAR 类型!

VARCHAR:可控可变长度,并且当数据小不需要额外存储数据页时,它直接存储完整数据,当数据量大需要数据页时,则存储文件地址,并且支持数据检查和内存临时表,并且是常见的类型,代码实现难度低,符合大众思路;

TEXT:相对来说比较好的一个字段,但还是不如 VARCHAR,不支持数据检查和内存临时表,另外一点就是,无论它是否使用到了额外的数据页,都会有一个20字节的文件指针,比较浪费空间,且此类型很少有人用,所以实际开发我们也不追求新鲜感,以通用型普遍性 VARCAHR 类型为主;

JSON:比较好的一个数据类型,但不建议使用。原因是需要使用 JSONObject 类型来接受,且部分开发人员未必对此字段类型熟悉,甚至可能SQL语法也不太清楚,需要额外进行学习,徒增开发压力;并且更重要的一点是,如果我们要在程序中获取JSON中的某个 Key 并进行操作,此时就显得较为麻烦,不如存储为字符串或文本类型,在经 JSONObject.parse() 转化;

MEDIUMTEXT:占用数据库存储较大,不易于管理,特别是当用户大量访问调用企查查接口时,会增加数据库存储压力,导致实际查询时,数据库底层会进行多次IO操作,如此一来还会如直接存储到数据库外的单独文件,在将文件指针存储到数据库。

总的来说,我们本篇文章着重考虑的就是是否要将真实数据存表,还是存文件指针,通过 MySQL 的动态优化策略,我们不难发现,当数据量大于 8K 时,即使我们不额外存到文件中,数据库底层还是会将数据存储到"数据页"中,在查询时需要进行额外的IO操作,既然如此,还不如当数据量小的数后,直接使用 VARCAHR 存储,当数据量大的时候,使用外部文件存储,然后使用 VARCHAR 存储外部文件指针。

所以,我们可以简单地把8K作为一个临界点,总结为以下表格,各位小伙伴可根据表格自行选择最优实现方案 (方案永远没有最好的,只有最合适的!)

决策因素数据库字段缓存完整数据数据库字段缓存文件指针
适用数据大小建议< 8KB≥ 8KB
用户查询频率建议高、中、低频率均可低频率(< 10次/分钟)
Redis 内存环境专属 Redis/内存充足,内存充足,随意使用多个项目或大型项目共享 Redis/内存紧张
实现复杂度对比简单(直接缓存完整JSON数据)

中等(需文件存储+指针管理)

如果是分布式系统多台服务器,可能还需考虑文件共享

数据库存储建议对数据存储无所谓,可接受大量数据直接存库对数据库要求较高,不希望大量公司数据占用数据库存储
响应时间对比1-5ms50-300ms
程序效率优先级⭐⭐⭐⭐⭐ (要求越快越好)⭐⭐ (可接受百毫秒级延迟)

三. 接口选择、接口分析

这里我们以对接启信宝第三方接口为例,如下图所示,可以发现官网对于接口有明确的标注接口ID——1.41 工商照面,查询公司的基本信息。

从官网可以得出接口的基本信息

接口地址:https://api.qixin.com/APIService/enterprise/getBasicInfo

数据格式:JSON

请求方式:HTTP/HTTPS的GET请求

请求示例:https://api.qixin.com/APIService/enterprise/getBasicInfo?keyword=开平达丰纺织印染服装有限公司

请求参数:Query,Query 参数内部有一个 keyword 属性

响应参数:标准的 status 状态码参数、message 响应描述、data 数据参数以及一个独有的数据签名参数 sign。

综合上述信息可以得知,要对接这个接口,至少需要创建一个请求参数Query,接口地址静态变量 private static final url = "",获取账号和密钥。

如果我们要对响应的数据进行操作,最好定义对应的 Java 实体类接收,然后通过 JSONObject.parse 转化为对应的实体类。

四. 数据库设计

经过上面一,二的分析,我们已经得出了结论,就是最为关键的"接口返回数据" 使用 VARCHAR 字段来存储,那么我们就开始设计数据库。

4.1 接口缓存表设计
4.1.1 建表SQL

每个字段的具体用处,小编都在注释中进行了说明,很好理解。

如果直接存储真实数据,则需要使用字段 "call_response_context";

如果存储文件指针,则需要使用字段 "bucket_name" 和 "file_name";

为了方便我都提前定义出来拉!

CREATE TABLE `interface_cache` (`id`                    BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键自增ID,也可以用UUID,只要保证唯一即可,项目中无实际用处',`consumer_service_name` VARCHAR(50)     DEFAULT NULL COMMENT '调用服务名称,如果用多个服务,方便以后对各个服务的真实调用次数做统计',`interface_id`          VARCHAR(20)     DEFAULT NULL COMMENT '接口唯一标识(ID),一般情况下官网都会有,这里指上面要对接的工商照面接口ID是1.41',`interface_name`        VARCHAR(100)    DEFAULT NULL COMMENT '第三方接口名称,比如查询公司详情这里就是上面的"工商照面",可加可不加的字段,加上更易于理解接口名称',`interface_param`       VARCHAR(255)    DEFAULT NULL COMMENT '接口入参,默认为公司名称/公司社会唯一信用码等,对应上方1.41接口的参数值keyword值',`bucket_name`           VARCHAR(50)     DEFAULT NULL COMMENT '若文件存储在本地,则指代文件所在的全路径名称;若存储于minio,则指代桶的名称(方案二要使用的字段)',`file_name`             VARCHAR(50)     DEFAULT NULL COMMENT '存储实际公司数据的文件名称,建议由UUID工具生成(方案二要使用的字段)',`call_response_context` VARCHAR(8192)   DEFAULT NULL COMMENT '接口调用响应报文,因为有些接口不使用data存储数据而是直接返回,所以我们直接存储整个响应,对应1.41接口响应的四个参数(方案一使用的字段)',`call_input_time`       DATETIME        COMMENT              '接口调用时间,作为记录',`create_time`           DATETIME        DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间,其实和接口调用时间一样,个人感觉可加可不加',`expired_time`          DATETIME        COMMENT              '数据过期时间(可定期清理过期数据),默认3个月有效期,直接在接口调用时间字段值上+3个月有效期',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方接口调用返回数据缓存表';
4.1.2 查询接口设计

后续我们查询数据库时,基本是通过 api_id + api_param + expired_time 来确认唯一一条已存在的公司数据。所以我们可以对这三个主要字段添加索引,这里就不详细展示了。

Mapper 层接口如下所示,这里也可以直接使用实体传参,我这里分成了三个,怎么写都行

InterfaceCache findCacheByCondition(@Param("interfaceId") String interfaceId,@Param("interfaceParam") String interfaceParam,@Param("expiredTime") Date expiredTime
);
  <select id="findCacheByCondition" resultType="com.test.InterfaceCache">select *from interface_cachewhere interface_id = #{interfaceId}and interface_param = #{interfaceParam}and expired_time > #{expiredTime}</select>

插入SQL就直接继承 mybatisplus 的单行记录插入即可,就不多赘述了。

4.2 调用日志记录表设计
4.2.1 建表SQL
CREATE TABLE `interface_active_record` (`id`                    BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键自增ID,也可以用UUID,只要保证唯一即可,项目中无实际作用',`consumer_service_name` VARCHAR(50)     DEFAULT NULL COMMENT '调用服务名称,方便以后对各个服务的所有调用次数做统计(含缓存、数据库调用)',`interface_id`          VARCHAR(20)     DEFAULT NULL COMMENT '接口唯一标识(ID),一般情况下官网都会有,这里指上面要对接的工商照面接口ID是1.41',`interface_name`        VARCHAR(100)    DEFAULT NULL COMMENT '第三方接口名称,比如查询公司详情这里就是上面的"工商照面",可加可不加的字段,加上更易于理解接口名称',`interface_param`       VARCHAR(255)    DEFAULT NULL COMMENT '接口入参,默认为公司名称/公司社会唯一信用码等,对应上方1.41接口的参数值keyword值',`is_actually_call`      VARCHAR(10)     DEFAULT NULL COMMENT '是否实际调用启信查询(N:否,Y:是),也可以用tinyint类型码值0否,1是表示,看个人习惯',`call_description`      VARCHAR(255)    DEFAULT NULL COMMENT '接口调用响应描述(查询缓存返回数据?或查询数据库返回数据?或实际调用返回数据?',`call_response_status`  VARCHAR(20)     DEFAULT NULL COMMENT '接口调用响应状态码,例如200,201,404等,对应上方1.41接口的相应参数status的值',`call_response_message` VARCHAR(50)     DEFAULT NULL COMMENT '接口调用响应信息,对应上方1.41接口的响应参数message的值',`create_time`           DATETIME        DEFAULT CURRENT_TIMESTAMP COMMENT '当前日志信息创建(插入)时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方接口调用日志记录表';
4.2.2 查询SQL

方便我们后续查询日志表进行验证,这里先把日志表查询SQL写出来;

# 1. 查询最近的接口调用记录
select * from interface_active_record order by id desc;
# 2. 查询某个公司所有企查查接口的调用记录
select * from interface_active_record where interface_param = '?' order by id desc;
# 3. 查询某个公司特定的企查查接口调用记录
select * from interface_active_record where interface_id = '?'and interface_param = '?' order by id desc;

五. 项目代码落地实现

对于到底缓存完整公司数据,还是缓存文件指针,这都是我们请求成功后要做的操作,这两种方案,其实最本质的区别就在于业务层Service方法的处理逻辑略有区别,其它比如控制器层 Controller 类,控制器接口请求参数 Entity 类,控制器返回通用参数类型 CommonResponseDTO 类都是一样的。所以我们下面先把通用代码创建出来,在再分别去写两种方案的Service业务层方法即可。我们开始吧!

5.1 通用代码编写
5.1.1 创建接口请求参数实体类
/*** 1.41 工商照面接口请求参数* */
@Data
public class BusinessDetailDTO {/*** 企业全名/注册号/统一社会信用代码* */private String keyword;
}
5.1.2 创建接口响应通用返回结果类
/*** 通用返回结果* */
@Data
public class CommonResponseDTO {// 响应状态码,直接获取接口的响应状态码 status 的值private String status;// 响应消息描述,直接获取接口的响应消息 message 的值private String message;// 响应数据,直接获取接口的整个响应报文,即 status,message,data,sign 组成的JSON字符串private String context;
}
5.1.3 创建接口缓存表实体类
/*** 第三方接口调用返回数据缓存表*/
@Data
public class InterfaceCache {/*** 主键自增ID,也可以用UUID,只要保证唯一即可,项目中无实际用处*/private Long id;/*** 调用服务名称,如果用多个服务,方便以后对各个服务的真实调用次数做统计*/private String consumerServiceName;/*** 接口唯一标识(ID),一般情况下官网都会有,这里指上面要对接的工商照面接口ID是1.41*/private String interfaceId;/*** 第三方接口名称,比如查询公司详情这里就是上面的"工商照面",可加可不加的字段,加上更易于理解接口名称*/private String interfaceName;/*** 接口入参,默认为公司名称/公司社会唯一信用码等,对应上方1.41接口的参数值keyword值*/private String interfaceParam;/*** 若文件存储在本地,则指代文件所在的全路径名称;若存储于minio,则指代桶的名称(方案二要使用的字段)*/private String bucketName;/*** 存储实际公司数据的文件名称,建议由UUID工具生成(方案二要使用的字段)*/private String fileName;/*** 接口调用响应报文,因为有些接口不使用data存储数据而是直接返回,所以我们直接存储整个响应,对应1.41接口响应的四个参数(方案一使用的字段)*/private String callResponseContext;/*** 接口调用时间,作为记录*/private Date callInputTime;/*** 记录创建时间,其实和接口调用时间一样,个人感觉可加可不加*/private Date createTime;/*** 数据过期时间(可定期清理过期数据),默认3个月有效期,直接在接口调用时间字段值上+3个月有效期*/private Date expiredTime;
}
5.1.4 创建接口缓存表Mapper接口

Mapper 接口不需要自定义

@Mapper
public interface InterfaceCacheMapper2 extends BaseMapper<InterfaceCache> {InterfaceCache findCacheByCondition(@Param("interfaceId") String interfaceId,@Param("interfaceParam") String interfaceParam,@Param("expiredTime") Date expiredTime);
}
5.1.5 创建接口活动日志表实体类
/*** 第三方接口调用日志记录表*/
@Data
public class InterfaceActiveRecord {/*** 主键自增ID,也可以用UUID,只要保证唯一即可,项目中无实际作用*/private Long id;/*** 调用服务名称,方便以后对各个服务的所有调用次数做统计(含缓存、数据库调用)*/private String consumerServiceName;/*** 接口唯一标识(ID),一般情况下官网都会有,这里指上面要对接的工商照面接口ID是1.41*/private String interfaceId;/*** 第三方接口名称,比如查询公司详情这里就是上面的"工商照面",可加可不加的字段,加上更易于理解接口名称*/private String interfaceName;/*** 接口入参,默认为公司名称/公司社会唯一信用码等,对应上方1.41接口的参数值keyword值*/private String interfaceParam;/*** 是否实际调用启信查询(N:否,Y:是),也可以用tinyint类型码值0否,1是表示,看个人习惯*/private String isActuallyCall;/*** 接口调用响应描述(查询缓存返回数据?或查询数据库返回数据?或实际调用返回数据?*/private String callDescription;/*** 接口调用响应状态码,例如200,201,404等,对应上方1.41接口的相应参数status的值*/private String callResponseStatus;/*** 接口调用响应信息,对应上方1.41接口的响应参数message的值*/private String callResponseMessage;/*** 当前日志信息创建(插入)时间*/private Date createTime;
}
5.1.6 创建接口活动记录表Mapper接口
@Mapper
public interface InterfaceActiveRecordMapper extends BaseMapper<InterfaceActiveRecord> {
}
5.1.7 创建 Controller 层控制器类
@RestController
@RequestMapping("/qx")
public class QxController {@Autowiredprivate QxService qxService;/*** @param businessDetailDTO 主要负责企业全名/注册号/统一社会信用代码* @return CommonResponseDTO 公共响应结果类对象*/@PostMapping("/getCompanyInfo")public CommonResponseDTO getCompanyInfo(@RequestBody BusinessDetailDTO businessDetailDTO,HttpServletRequest request) {return qxService.getCompanyInfo(businessDetailDTO,request);}
}
5.1.8 Service 层方法公共变量和方法创建
@Slf4j
@Service
public class QxService {// 接口活动跟踪记录Mapper@Autowiredprivate InterfaceActiveRecordMapper interfaceActiveRecordMapper;// 接口数据缓存Mapper@Autowiredprivate InterfaceCacheMapper interfaceCacheMapper;// redis缓存@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 发送网络请求的restTemplate@Autowiredprivate RestTemplate restTemplate;// appkey,secret_key正常来讲应该定义来yml文件中,这里懒省事,直接定义在业务类中private static final String appkey = "appkey";private static final String secret_key = "secret_key";// 缓存失效时间,默认7天,正常来讲也应该定义在 yml 文件中,这里懒省事,直接定义在业务类中private static final int cacheDays = 7;// 下面这几个静态常量类正常来讲应该定义在Constants常量类中,这里懒省事,直接定义在业务类中private static final String QX_GET_BUSINESS_DETAIL_URL = "https://api.qixin.com/APIService/enterprise/getBasicInfo";private static final String QX_GET_BUSINESS_DETAIL_CACHE_PREFIX = "QX:getBusinessDetail:";private static final String QX_GET_BUSINESS_DETAIL_INTERFACE_ID = "1.41:";private static final String QX_GET_BUSINESS_DETAIL_INTERFACE_NAME = "企业工商照面";/*** MD5加密方法,待会业务方法中会用到,提前定义出来。* 加密规则 :* appkey + timestamp + secret_key 组成的 32 位md5加密的小写字符串(实际加密不带入 ‘+’)* */private String getMD5(String appkey, String timestamp, String secret_key) {byte[] digest = null;try {MessageDigest md = MessageDigest.getInstance("MD5");String str = appkey + timestamp + secret_key;md.update(str.getBytes(StandardCharsets.UTF_8));digest = md.digest();StringBuilder hexString = new StringBuilder();for (byte b : digest) {// 保证两位十六进制hexString.append(String.format("%02x", b));}return hexString.toString();}catch (Exception e){throw new RuntimeException("MD5加密失败", e);}}/*** RestTemplate 发送请求方法,返回请求结果,待会业务方法中会用到,提前定义出来。* */private String httpsWithRestTemplate(String appkey, String timestamp, String secret_key,String url){// 创建请求头HttpHeaders headers = new HttpHeaders();headers.set("Auth-Version", "2.0"); // 官网固定传入2.0headers.set("appkey", appkey);headers.set("timestamp", timestamp);headers.set("sign", getMD5(appkey, timestamp, secret_key));// 封装请求头和空请求体HttpEntity<String> requestEntity = new HttpEntity<>(headers);// 发送 GET 请求ResponseEntity<String> responseEntity = restTemplate.exchange(url,HttpMethod.GET,requestEntity,String.class);// 获取响应体return responseEntity.getBody();}/*** 生成完整的查询工商照面接口地址方法* 启信接口 -"查询工商照面"* 官网接口ID:1.41* 接口地址:https://api.qixin.com/APIService/enterprise/getBasicInfo* 接口参数:keyword - 待查询的企业名称* 请求示例:https://api.qixin.com/APIService/enterprise/getBasicInfo?keyword=开平达丰纺织印染服装有限公司* */private String getBusinessBasicDetailUrl(String keyword) {StringBuffer url = new StringBuffer();if (StringUtils.isNotBlank(keyword)){url.append(QX_GET_BUSINESS_DETAIL_URL).append("?keyword=").append(keyword);}return url.toString();}
}

5.2 Redis 缓存真实数据业务代码实现

如下所示,就是小编个人编写的一段缓存接口真实数据业务层代码,仅供各位参考。

核心逻辑就三点:

第一:先查询缓存,缓存命中则直接返回;

第二:缓存未命中,查询数据库,数据库命中,回写缓存并返回;

第三:缓存、数据库均未命中,则发送网络请求查询数据,判断响应结果,保存至数据库并回写缓存;

/*** 启信接口查询 - 1.41 工商照面* */
public CommonResponseDTO getCompanyInfo(BusinessDetailDTO businessDetailDTO,HttpServletRequest request) {// 创建方法返回对象CommonResponseDTO commonResponseDTO = new CommonResponseDTO();// 1. 创建接口活动跟踪记录对象并设置值InterfaceActiveRecord interfaceActiveRecord = new InterfaceActiveRecord();interfaceActiveRecord.setConsumerServiceName("XXXService"); // 服务名称,正常情况下应该从request中获取,这里写的比较随意interfaceActiveRecord.setInterfaceId(QX_GET_BUSINESS_DETAIL_INTERFACE_ID);interfaceActiveRecord.setInterfaceName(QX_GET_BUSINESS_DETAIL_INTERFACE_NAME);interfaceActiveRecord.setCreateTime(new Date());// 2. 获取企业名称参数,同时赋值给缓存key和接口活动跟踪对象String cacheKey = businessDetailDTO.getKeyword();interfaceActiveRecord.setInterfaceParam(cacheKey);// 3. 查询缓存,判断缓存值是否为空,不为空直接返回结果String cacheCompanyJson = stringRedisTemplate.opsForValue().get(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey);if (StringUtils.isNotBlank(cacheCompanyJson)) {// 返回结果JSONObject companyJSON = JSON.parseObject(cacheCompanyJson);commonResponseDTO.setStatus(companyJSON.getString("status"));commonResponseDTO.setMessage(companyJSON.getString("message"));commonResponseDTO.setContext(cacheCompanyJson);// 接口活动跟踪记录对象设置值interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("查询Redis缓存返回公司数据");interfaceActiveRecord.setCallResponseStatus(companyJSON.getString("status"));interfaceActiveRecord.setCallResponseMessage(companyJSON.getString("message"));interfaceActiveRecordMapper.insertWithParam(interfaceActiveRecord);return commonResponseDTO;}// 4. 缓存为空,则查询数据库,判断数据库数据是否为空InterfaceCache interfaceCache = null;interfaceCache = interfaceCacheMapper.findCacheByCondition(QX_GET_BUSINESS_DETAIL_INTERFACE_ID,cacheKey,new Date());if(interfaceCache != null) {// 写入缓存stringRedisTemplate.opsForValue().set(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey,JSON.toJSONString(interfaceCache.getCallResponseContext()), cacheDays, TimeUnit.DAYS);// 返回结果JSONObject companyJSON = JSON.parseObject(interfaceCache.getCallResponseContext());commonResponseDTO.setStatus(companyJSON.getString("status"));commonResponseDTO.setMessage(companyJSON.getString("message"));commonResponseDTO.setContext(JSON.toJSONString(interfaceCache.getCallResponseContext()));// 接口活动跟踪记录对象设置值interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("查询数据库缓存返回公司数据");interfaceActiveRecord.setCallResponseStatus(companyJSON.getString("status"));interfaceActiveRecord.setCallResponseMessage(companyJSON.getString("message"));interfaceActiveRecordMapper.insertWithParam(interfaceActiveRecord);return commonResponseDTO;}// 5. 数据库数据也为空,说明数据已过期或从未查询过,则调用启信官方接口查询数据String response = null;InterfaceCache interfaceCacheInsert = new InterfaceCache();try {log.info("调用启信查询1.41 工商照面接口入参\n{}", cacheKey);// 发送网络请求获取响应数据response = httpsWithRestTemplate(appkey,String.valueOf(System.currentTimeMillis()),secret_key,getBusinessBasicDetailUrl(cacheKey));log.info("调用启信查询1.41 工商照面接口返回数据\n{}", response);// 不管是否成功,都返回接口响应commonResponseDTO.setStatus(JSON.parseObject(response).getString("status"));commonResponseDTO.setStatus(JSON.parseObject(response).getString("message"));commonResponseDTO.setContext(String.valueOf(JSON.parseObject(response)));// 不管是否成功,接口活动跟踪记录对象设置值interfaceActiveRecord.setCallResponseStatus(JSON.parseObject(response).getString("status"));interfaceActiveRecord.setCallResponseMessage(JSON.parseObject(response).getString("message"));interfaceActiveRecord.setIsActuallyCall("Y");interfaceActiveRecord.setCallDescription("调用启信宝接口获取数据");// 判断状态是否为200,只有200写入数据库和缓存。因为可能出现201-余额不足;202-查询无结果等情况......if (response != null && "200".equals(JSON.parseObject(response).getString("status"))) {// 请求成功,将数据写入缓存stringRedisTemplate.opsForValue().set(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey, response, cacheDays, TimeUnit.DAYS);// 接口缓存对象赋值interfaceCacheInsert.setConsumerServiceName("XXXService");interfaceCacheInsert.setInterfaceId(QX_GET_BUSINESS_DETAIL_INTERFACE_ID);interfaceCacheInsert.setInterfaceName(QX_GET_BUSINESS_DETAIL_INTERFACE_NAME);interfaceCacheInsert.setInterfaceParam(cacheKey);interfaceCacheInsert.setBucketName(null);interfaceCacheInsert.setCallResponseContext(response);interfaceCacheInsert.setCallInputTime(new Date());interfaceCacheInsert.setCreateTime(new Date());interfaceCacheInsert.setExpiredTime(new Date(new Date().getTime() + 30L * 24 * 60 * 60 * 1000));// 将接口返回数据插入数据库interfaceCacheMapper.insert(interfaceCacheInsert);}} catch (Exception e) {interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("调用启信宝接口获取数据失败");interfaceActiveRecord.setCallResponseMessage(e.getMessage());throw new RuntimeException("企业基本信息数据写入本地缓存发生错误",e);} finally {// 6. 插入接口活动跟踪记录,不管调用是否成功,都要进行记录,放到 finally 块中interfaceActiveRecordMapper.insert(interfaceActiveRecord);}return commonResponseDTO;
}

5.3 Redis 缓存文件指针,通过指针再读取文件内容业务实现

因为要使用文件缓存接口响应数据,所以我们先写一个保存数据的方法。注释很详细,不过多解释啦。

这里我定义了一个文件前缀,就把文件保存到我的本地电脑磁盘上了,正常来讲公司的生产项目,通常会存储在运行项目的 Linux 服务器上,或者 minio 存储中间件上,或者阿里云OSS云存储等地方,这里就不整那么复杂啦,主要分享一个思路。同学们了解即可

// 缓存文件前缀,公司数据缓存文件存放在/data/gateway-server/cache目录下。
private static final String CACHE_FILE_PREFIX = "/data/gateway-server/cache";/***  保存公司数据到文件并返回文件名*  @param response: 第三方接口返回的公司数据,返回 json 字符串*  return: 文件名和文件桶的Map集合···*/
private Map<String,String> saveDataToFile(String response) throws Exception{Map<String,String> map = new HashMap<>();// 缓存文件日期前缀,精确到月份SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");String date = sdf.format(new Date());// 随机生成缓存文件名String fileName = UUID.randomUUID().toString().concat(".json");// 缓存文件保存的目录String bucketName = CACHE_FILE_PREFIX.concat("/").concat(date).concat("/");// 若不存在则生成文件File dir = new File(bucketName);if(!dir.exists() && !dir.isDirectory()){dir.mkdirs();}// 缓存数据保存到本地磁盘try(// 创建一个`FileWriter`对象,用于向指定文件写入字符数据FileWriter fileWriter = new FileWriter(bucketName.concat(fileName));// 创建一个`BufferedWriter`对象,包装`FileWriter`以提高写入效率,默认缓存大小为8192字节(8K),与我们上面分析的8K大小节点刚好相同BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)){// 执行实际写入操作bufferedWriter.write(response);} catch (IOException e){log.error("写入缓存数据失败:\n{}", e.getMessage());}map.put("fileName",fileName);map.put("bucketName",bucketName);return map;
}

将数据缓存到本地磁盘文件之后,我们再写一个从磁盘文件读取数据的的方法。

/*** 通过文件指针获取公司数据* @param bucketName 存储桶名称,或文件存储路径* @param fileName 文件名称* return: 文件内容,公司数据,返回 json 字符串*/
public String getDataByFile(String bucketName, String fileName)  {File file = new File(bucketName + fileName);StringBuffer sbf = new StringBuffer();BufferedReader reader = null;String response = "";log.info("缓存json读取的downloadPath:{}", bucketName + fileName);try {// 读取文件数据reader = new BufferedReader(new FileReader(file));String tempStr;while ((tempStr = reader.readLine()) != null) {sbf.append(tempStr);}reader.close();response = sbf.toString();return response;} catch (Exception e) {log.error("获取缓存数据失败:\n{}", e.getMessage());} finally {if (reader != null) {try {reader.close();} catch (IOException e) {log.error("关闭缓存文件失败:\n{}", e.getMessage());throw new RuntimeException(e);}}}return response;
}

编写完毕上面的两个方法,就可以正式来编写业务逻辑代码了,如下所示,大致逻辑其实和上面Redis 的思路一样的,只是在获取数据操作上加入了文件操作这一层,要求开发者对Java的IO代码编写有一定的基础。

/*** 启信接口查询 - 1.41 工商照面* */
public CommonResponseDTO getCompanyInfo(BusinessDetailDTO businessDetailDTO,HttpServletRequest request) {// 1. 创建方法返回对象CommonResponseDTO commonResponseDTO = new CommonResponseDTO();// 1. 创建接口活动跟踪记录对象并设置值InterfaceActiveRecord interfaceActiveRecord = new InterfaceActiveRecord();interfaceActiveRecord.setConsumerServiceName("XXXService"); // 服务名称,正常情况下应该从request中获取,这里写的比较随意interfaceActiveRecord.setInterfaceId(QX_GET_BUSINESS_DETAIL_INTERFACE_ID);interfaceActiveRecord.setInterfaceName(QX_GET_BUSINESS_DETAIL_INTERFACE_NAME);interfaceActiveRecord.setCreateTime(new Date());// 2. 获取企业名称参数,同时赋值给缓存key和接口活动跟踪对象String cacheKey = businessDetailDTO.getKeyword();interfaceActiveRecord.setInterfaceParam(cacheKey);// 3. 查询缓存,判断缓存值是否为空,不为空直接返回结果String interfaceCacheJson = stringRedisTemplate.opsForValue().get(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey);InterfaceCache interfaceCache = null;String response = null;if (StringUtils.isNotBlank(interfaceCacheJson)) {// 缓存不为空,则返回缓存数据,转化为 InterfaceCache 对象interfaceCache = JSON.parseObject(interfaceCacheJson, InterfaceCache.class);// 获取桶名和文件名称,从磁盘读取文件数据response = getDataByFile(interfaceCache.getBucketName(), interfaceCache.getFileName());// 解析 response 为 json 格式数据JSONObject jsonObject = JSON.parseObject(response);commonResponseDTO.setStatus(jsonObject.getString("status"));commonResponseDTO.setMessage(jsonObject.getString("message"));commonResponseDTO.setContext(response);// 接口活动跟踪记录对象设置值interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("查询Redis缓存返回公司数据");interfaceActiveRecord.setCallResponseStatus(jsonObject.getString("status"));interfaceActiveRecord.setCallResponseMessage(jsonObject.getString("message"));interfaceActiveRecordMapper.insertWithParam(interfaceActiveRecord);return commonResponseDTO;}// 4. 缓存为空,则查询数据库,判断数据库数据是否为空interfaceCache = interfaceCacheMapper.findCacheByCondition(QX_GET_BUSINESS_DETAIL_INTERFACE_ID,cacheKey,new Date());if(interfaceCache != null) {// 数据库中有数据,根据桶名和文件名获取数据response = getDataByFile(interfaceCache.getBucketName(), interfaceCache.getFileName());// 返回结果JSONObject companyJSON = JSON.parseObject(interfaceCache.getCallResponseContext());commonResponseDTO.setStatus(companyJSON.getString("status"));commonResponseDTO.setMessage(companyJSON.getString("message"));commonResponseDTO.setContext(response);// 写入缓存stringRedisTemplate.opsForValue().set(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey,JSON.toJSONString(interfaceCache), cacheDays, TimeUnit.DAYS);// 接口活动跟踪记录对象设置值interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("查询数据库缓存返回公司数据");interfaceActiveRecord.setCallResponseStatus(companyJSON.getString("status"));interfaceActiveRecord.setCallResponseMessage(companyJSON.getString("message"));interfaceActiveRecordMapper.insertWithParam(interfaceActiveRecord);return commonResponseDTO;}// 5. 数据库数据也为空,说明数据已过期或从未查询过,则调用启信官方接口查询数据InterfaceCache interfaceCacheInsert = new InterfaceCache();try {log.info("调用启信查询1.41 工商照面接口入参\n{}", cacheKey);// 发送网络请求获取响应数据response = httpsWithRestTemplate(appkey,String.valueOf(System.currentTimeMillis()),secret_key,getBusinessBasicDetailUrl(cacheKey));log.info("调用启信查询1.41 工商照面接口返回数据\n{}", response);// 不管是否成功,都返回接口响应commonResponseDTO.setStatus(JSON.parseObject(response).getString("status"));commonResponseDTO.setStatus(JSON.parseObject(response).getString("message"));commonResponseDTO.setContext(String.valueOf(JSON.parseObject(response)));// 不管是否成功,接口活动跟踪记录对象设置值interfaceActiveRecord.setCallResponseStatus(JSON.parseObject(response).getString("status"));interfaceActiveRecord.setCallResponseMessage(JSON.parseObject(response).getString("message"));interfaceActiveRecord.setIsActuallyCall("Y");interfaceActiveRecord.setCallDescription("调用启信宝接口获取数据");// 但判断状态是否为200,只有200写入数据库和缓存。因为可能出现201-余额不足;202-查询无结果等情况......if (response != null && "200".equals(JSON.parseObject(response).getString("status"))) {// 保存接口返回数据到本地文件Map<String, String> map = saveDataToFile(response);// 接口缓存对象赋值interfaceCacheInsert.setConsumerServiceName("XXXService");interfaceCacheInsert.setInterfaceId(QX_GET_BUSINESS_DETAIL_INTERFACE_ID);interfaceCacheInsert.setInterfaceName(QX_GET_BUSINESS_DETAIL_INTERFACE_NAME);interfaceCacheInsert.setInterfaceParam(cacheKey);interfaceCacheInsert.setBucketName(map.get("bucketName"));interfaceCacheInsert.setFileName(map.get("fileName"));interfaceCacheInsert.setCallInputTime(new Date());interfaceCacheInsert.setCreateTime(new Date());interfaceCacheInsert.setExpiredTime(new Date(new Date().getTime() + 30L * 24 * 60 * 60 * 1000));// 将接口返回数据插入数据库interfaceCacheMapper.insert(interfaceCacheInsert);// 插入数据库之后,interfaceCacheInsert 对象是一条带有主键ID值的完整数据,存入缓存stringRedisTemplate.opsForValue().set(QX_GET_BUSINESS_DETAIL_CACHE_PREFIX + cacheKey, JSON.toJSONString(interfaceCacheInsert), cacheDays, TimeUnit.DAYS);}} catch (Exception e) {interfaceActiveRecord.setIsActuallyCall("N");interfaceActiveRecord.setCallDescription("调用启信宝接口获取数据失败");interfaceActiveRecord.setCallResponseMessage(e.getMessage());throw new RuntimeException("企业基本信息数据写入本地缓存发生错误",e);} finally {// 6. 插入接口活动跟踪记录,不管调用是否成功,都要进行记录,放到 finally 块中interfaceActiveRecordMapper.insert(interfaceActiveRecord);}return commonResponseDTO;
}

5.4 二者混合使用

这一种方法,也不失为一种解决思路。

比如我们公司一共要对接10个企查查相关接口,有大接口返回大量数据(10~50K),有小接口返回少量数据(1~3K),此时我们就可以混合上面的两种方法,大接口采用文件指针的解决思路,小接口采用缓存直接存储的解决思路,可以达到部分接口提高响应效率,同时大接口又不会过度占用 Redis 内存。

代码就不详细举例了,只是将上面两种方案的代码都复制使用即可。不过这种方法,做起来复杂度较高就是了,各位开发者同学可以根据司机项目需求的需要,选择相对应的解决方案!

六. 简要总结

综上所述,可以简单总结为以下三句话。

(1) 对接第三方SDK接口时,如果响应体较小,且希望提高服务器响应效率,则可以将接口响应整个存储数据库和 Redis 缓存,实现复杂度低,响应效率高,缺点是大量请求时,可能导致 Redis 内存占用较高;

(2) 如果响应体较大,建议将响应体数据缓存到磁盘文件或指定存储服务器,将文件指针作为字段值存入数据库,读取文件时,先获取文件指针,然后通过文件指针读取文件缓存数据,响应给前端,缺点是因为需要进行文件IO操作,所以响应效率不如直接存储 Redis;

(3) 也可以结合二者,大接口使用文件指针,小接口直接存储 Redis,但编码复杂度较高,后续维护可能会略显复杂;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis常用数据结构以及多并发场景下的使用分析:Hash类型

文章目录前言hash 对比 String简单存储对象【秒杀系统】- 商品库存管理【用户会话管理】- 分布式Session存储【信息预热】- 首页信息预热降级策略总结前言 上文我们分析了String类型 在多并发下的应用 本文该轮到 Hash了&#xff0c;期不期待 兄弟们 hhh Redis常用数据结构以…

双因子认证(2FA)是什么?从零设计一个安全的双因子登录接口

前言在信息系统逐渐走向数字化、云端化的今天&#xff0c;账号密码登录已不再是足够安全的手段。数据泄露、撞库攻击、社工手段频发&#xff0c;仅靠「你知道的密码」已不足以保证账户安全。因此&#xff0c;双因子认证&#xff08;2FA, Two-Factor Authentication&#xff09;…

stack栈练习

为了你&#xff0c;我变成狼人模样&#xff1b; 为了你&#xff0c;染上了疯狂~ 目录stack栈练习栈括号的分数单调栈模板框架小结下一个更大元素 I&#xff08;单调栈哈希&#xff09;接雨水stack栈练习 栈 一种先进后出的线性数据结构 具体用法可参考往期文章或者维基介绍i…

详细页智能解析算法:洞悉海量页面数据的核心技术

详细页智能解析算法&#xff1a;突破网页数据提取瓶颈的核心技术剖析引言&#xff1a;数字时代的数据采集革命在当今数据驱动的商业环境中&#xff0c;详细页数据已成为企业决策的黄金资源。无论是电商商品详情、金融公告还是新闻资讯&#xff0c;​​有效提取结构化信息​​直…

ubuntu环境如何安装matlab2016

一、下载安装文件&#xff08;里面包含激活包CRACK&#xff09;可从度盘下载&#xff1a;链接:https://pan.baidu.com/s/1wxmVMzXiSY4RIT0dyKkjZg?pwd26h6 复制这段内容打开「百度网盘APP 即可获取」注&#xff1a;这里面包含三个文件&#xff0c;其中ISO包含安装文件&#x…

Mybits-plus 表关联查询,嵌套查询,子查询示例演示

在 MyBatis-Plus 中实现表关联查询、嵌套查询和子查询&#xff0c;通常需要结合 XML 映射文件或 Select 注解编写自定义 SQL。以下是具体示例演示&#xff1a;示例场景 假设有两张表&#xff1a; 用户表 userCREATE TABLE user (id BIGINT PRIMARY KEY,name VARCHAR(50),age IN…

Stable Diffusion Web 环境搭建

默认你的系统Ubuntu、CUDA、Conda等都存在&#xff0c;即具备运行深度学习模型的基础环境 本人&#xff1a;Ubuntu22.04、CUDA11.8环境搭建 克隆项目并且创建环境 https://github.com/AUTOMATIC1111/stable-diffusion-webui conda create -n sd python3.10运行过程自动安装依赖…

嵌入式系统中实现串口重定向

在嵌入式系统中实现串口重定向&#xff08;将标准输出如 printf 函数输出重定向到串口&#xff09;通常有以下几种常用方法&#xff0c;下面结合具体代码示例和适用场景进行说明&#xff1a; 1. 重写 fputc 函数&#xff08;最常见、最基础的方法&#xff09; 通过重写标准库中…

static补充知识点-代码

public class Student {private static int age;//静态的变量private double score;//非静态的方法public void run(){}public static void go(){}public static void main(String[] args) {new Student().run();Student.go();} } public class Person {//2 &#xff1a; 赋初始…

使用泛型<T>,模块化,反射思想进行多表数据推送

需求&#xff1a;有13个表&#xff0c;其中一个主表和12细表&#xff0c;主表用来记录推送状态&#xff0c;细表记录12种病例的详细信息&#xff0c;现在需要把这12张病例表数据进行数据推送&#xff1b;普通方法需要写12个方法分别去推送数据然后修改状态&#xff1b;现在可以…

光流 | RAFT光流算法如何改进提升

RAFT(Recurrent All-Pairs Field Transforms)作为ECCV 2020最佳论文,已成为光流估计领域的标杆模型。其通过构建4D相关体金字塔和GRU迭代优化机制,在精度与泛化性上实现了突破。但针对其计算效率、大位移处理、跨场景泛化等问题,研究者提出了多维度改进方案,核心方向可系…

linux/ubuntu日志管理--/dev/log 的本质与作用

文章目录 **一、基本概念****二、技术细节:UNIX域套接字****三、在不同日志系统中的角色****四、应用程序如何使用 `dev/log`****五、查看和验证 `/dev/log`****六、总结 `/dev/log` 的核心作用**一、基本概念 /dev/log 是一个 UNIX域套接字(Unix Domain Socket),是Linux系…

EMC整改案例之(1):汽车NFC进入模块BCI整改

EMC整改案例(1):汽车NFC进入模块BCI整改 在汽车电子系统中,NFC(Near Field Communication)进入模块用于实现无钥匙进入功能,但它在电磁兼容(EMC)测试中常面临挑战。本案例聚焦于BCI(Bulk Current Injection)测试整改,该测试模拟大电流注入对设备的影响。以下是基于…

2025年INS SCI2区,灵活交叉变异灰狼算法GWO_C/M+集群任务调度,深度解析+性能实测

目录1.摘要2.灰狼算法GWO原理3.灵活交叉变异灰狼算法GWO_C/M4.结果展示5.参考文献6.代码获取7.算法辅导应用定制读者交流1.摘要 随着云计算的快速发展&#xff0c;受自然现象启发的任务调度算法逐渐成为研究的热点。灰狼算法&#xff08;GWO&#xff09;因其强大的收敛性和易于…

Java常用加密算法详解与实战代码 - 附可直接运行的测试示例

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

2025开发者工具链革命:AI赋能的效率跃迁

目录引言&#xff1a;效率焦虑下的开发者生存现状一、智能代码编辑器&#xff1a;从辅助到主导的进化1.1 GitHub Copilot&#xff1a;全能型AI助手1.2 Cursor Pro&#xff1a;极致编码体验1.3 飞算JavaAI&#xff1a;垂直领域颠覆者二、版本控制革命&#xff1a;Git的AI进化论2…

“虚空”的物理、哲学悖论

一、虚空并非“完全真空”&#xff1a;量子场论揭示的“真空不空” 物理真空的本质 现代物理学中的“真空”并非绝对的空无一物&#xff0c;而是量子场的基态&#xff08;能量最低状态&#xff09;。根据量子场论&#xff1a; 虚粒子涨落&#xff1a;真空中持续发生量子涨落&am…

CSP-S模拟赛二总结(实际难度大于CSP-S)

T1 很简短&#xff0c;也很好做&#xff0c;第一题直接场切。 我的方法 首先要明确一件事&#xff1a;就是如果选了 ax,ya_{x,y}ax,y​&#xff0c;那么就必然要选 ay,xa_{y,x}ay,x​&#xff0c;所以第一步就在 ax,ya_{x,y}ax,y​ 的基础上加上 ay,xa_{y,x}ay,x​。 然后我…

旋转屏幕优化

1.问题背景 从google原生算法&#xff0c;可以知道其有2个比较大的缺陷&#xff1a; 1) 通过重力传感器传来的x&#xff0c;y&#xff0c;z轴的加速度合成之后只有一个垂直往下的加速度&#xff0c;如果此时用户在别的方向上有加速度&#xff0c;那么通过反余弦、反正切等计算…

Java---day2

七、IDEA开发工具 &#x1f4e6; 一、下载 IntelliJ IDEA 官网地址&#xff1a; &#x1f517; IntelliJ IDEA – the IDE for Pro Java and Kotlin Development 版本选择&#xff1a; 版本说明Community Edition (CE)免费开源版本&#xff0c;适合 Java、Kotlin、Android…