todo:
投票- 微服务改造
later:
- 用户行为跟踪,日志分析
- 语义搜索
- 推送
- AI 喜好分析
- 性能 optimize
- search after 改造
- hgetall pipeline优化
定时回调
在Spring Boot中实现定时执行回调函数并传递参数,可以通过以下几种方式实现,结合不同的场景和需求选择合适的方法:
使用 @Scheduled
注解 + 方法参数传递
适用于简单的静态定时任务,通过 @Scheduled
注解定义延迟时间,并在方法中直接接收参数:
@Component
public class ScheduledTask {
// 延迟5秒后执行,并传递参数
@Scheduled(initialDelay = 5000, fixedDelay = Long.MAX_VALUE)
public void delayedTask(String param) {
System.out.println("执行任务,参数:" + param);
}
// 通过其他方法触发带参数的定时任务
public void triggerTaskWithParam(String param) {
delayedTask(param); // 直接调用(需注意线程安全问题)
}
}
注意:此方式需确保参数在调用时已注入,且需手动触发任务(如通过其他方法调用)。
动态任务注册(SchedulingConfigurer
接口)
适用于需要运行时动态调整参数或延迟时间的场景,通过编程方式注册任务:
@Configuration
@EnableScheduling
public class DynamicSchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
// 动态添加任务:延迟10秒执行,并传递参数
registrar.addTriggerTask(
() -> System.out.println("执行任务,参数:" + customParam),
triggerContext -> {
Date nextExecutionTime = new Date(System.currentTimeMillis() + 10000);
return nextExecutionTime;
}
);
}
}
特点:
- 可结合数据库或配置中心动态加载参数。
- 支持灵活的时间控制(如根据条件计算延迟时间)。
使用 ThreadPoolTaskScheduler
异步调度
适用于需要异步执行或管理线程池的场景:
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
return scheduler;
}
// 在业务代码中调度任务
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
public void scheduleTaskWithParam(String param) {
taskScheduler.schedule(
() -> System.out.println("异步执行,参数:" + param),
new Date(System.currentTimeMillis() + 5000) // 延迟5秒
);
}
优势:
- 支持异步执行,避免阻塞主线程。
- 可手动取消任务(通过返回的
ScheduledFuture
对象)。
结合 @Async
注解实现异步延迟
若需在异步环境下执行任务并传参:
@Component
public class AsyncTask {
@Async
public void executeWithDelay(String param, long delay) {
try {
Thread.sleep(delay);
System.out.println("异步延迟执行,参数:" + param);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 调用示例
@Autowired
private AsyncTask asyncTask;
public void triggerAsyncTask() {
asyncTask.executeWithDelay("test", 5000); // 延迟5秒
}
适用场景:需要与其他异步任务结合,或对线程控制有更高要求时。
参数传递的注意事项
- 作用域问题:若通过方法参数传递,需确保参数在任务执行时仍有效(避免使用局部变量)。
- 线程安全:动态任务中若涉及共享参数,需使用线程安全的数据结构(如
ConcurrentHashMap
)。 - 外部化配置:可通过
@Value
注解从配置文件读取参数,或结合数据库动态加载。
最佳实践选择
- 简单场景:优先使用
@Scheduled(initialDelay)
+ 方法参数。 - 动态需求:选择
SchedulingConfigurer
或ThreadPoolTaskScheduler
。 - 异步控制:结合
@Async
和线程池管理。
具体实现需根据业务复杂度、参数动态性及执行环境灵活调整。
Vote索引设计
解决方案分析
方案一:将 relatedId
改为 String
并添加前缀
优点:
- 单字段存储所有关联类型:通过前缀(如
post_123
、comment_456
)直接标识关联类型和ID,简化表结构,无需额外字段。 - 查询灵活性:前缀可以快速识别关联类型,避免联合字段查询时的复杂条件判断。
缺点:
- 索引性能问题:字符串类型(如
post_123
)的索引长度和比较效率低于数值类型(Long
),可能影响查询速度。 - 数据冗余和维护成本:前缀逻辑需要应用层维护,例如解析字符串、生成前缀等,增加代码复杂度。
- 扩展性问题:新增类型时需要修改前缀逻辑,可能影响已有数据。
方案二:新增字段区分类型(如 relatedType
)
优点:
- 类型与ID分离:通过
relatedType
(枚举或字符串)和relatedId
(保持Long
)分离关注点,逻辑更清晰。 - 索引优化:可针对
relatedType
和relatedId
创建复合索引,提升查询效率。 - 扩展性更强:新增类型无需修改字段结构,只需扩展枚举值。
缺点:
- 表结构略微复杂:需要维护两个字段,但对数据库设计来说是常规操作。
PostgreSQL 单一索引与复合索引的性能差异
单一索引:
- 适用场景:仅对单个字段频繁查询(如仅按
relatedId
或relatedType
查询)。 - 缺点:
- 多条件查询需合并索引:例如同时按
relatedType
和relatedId
查询时,需要扫描多个索引并通过位图合并结果,增加 CPU 和内存开销。 - 无法覆盖排序:若查询需要对多列排序(如
ORDER BY relatedType, relatedId
),单一索引无法优化。
- 多条件查询需合并索引:例如同时按
- 适用场景:仅对单个字段频繁查询(如仅按
复合索引:
- 适用场景:高频多条件查询(如
WHERE relatedType = 'post' AND relatedId = 123
)。 - 优点:
- 高效过滤和排序:复合索引直接覆盖多列查询和排序,减少随机 I/O 和回表次数。
- 索引覆盖查询:若索引包含所有查询字段(如
(relatedType, relatedId)
),可避免访问主表数据(Index-Only Scan)。
- 设计要点:列顺序需优先选择高区分度的字段(如
relatedType
在前,relatedId
在后)。
- 适用场景:高频多条件查询(如
推荐方案
数据库设计:
- 建议新增
relatedType
字段,保持relatedId
为Long
类型,逻辑清晰且便于扩展。 - 创建复合索引:
CREATE INDEX idx_vote_related ON votes (relatedType, relatedId)
,以优化高频查询性能。
- 建议新增
查询优化:
- 若需要快速查询特定类型的所有关联记录(如所有
post
类型的投票),复合索引的过滤效率远高于字符串前缀方案。 - 若查询仅涉及
relatedId
(无类型过滤),可额外为relatedId
创建单一索引,但需权衡存储和维护成本。
- 若需要快速查询特定类型的所有关联记录(如所有
总结
- 字符串前缀方案适合简单场景,但存在性能和维护隐患。
- 字段分离+复合索引在 PostgreSQL 中更优,兼顾查询效率、扩展性和规范化设计。
Instant 计算
在Java中使用 Instant
类计算从现在起经过一段时间,可以通过以下两种核心方法实现,结合 Duration
类或 ChronoUnit
单位进行灵活操作:
方法一:通过 plus
方法直接添加时间单位
利用 Instant
的 plus(long, TemporalUnit)
方法,指定时间单位和数值,直接计算未来时间点。
示例代码:
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class InstantDemo {
public static void main(String[] args) {
Instant now = Instant.now();
// 添加10分钟
Instant future1 = now.plus(10, ChronoUnit.MINUTES);
// 添加3天
Instant future2 = now.plus(3, ChronoUnit.DAYS);
System.out.println("当前时间:" + now);
System.out.println("10分钟后:" + future1);
System.out.println("3天后:" + future2);
}
}
特点:
- 简洁直接,适合简单时间单位(如秒、分、小时、天)的加减。
ChronoUnit
支持多种时间单位,如SECONDS
、HOURS
、DAYS
等。
方法二:通过 Duration
类定义时间间隔
创建 Duration
对象表示具体的时间长度,再通过 plus()
方法添加到当前时间点。
示例代码:
import java.time.Duration;
import java.time.Instant;
public class DurationExample {
public static void main(String[] args) {
Instant now = Instant.now();
// 定义2小时的时间间隔
Duration duration = Duration.ofHours(2);
// 添加间隔
Instant future = now.plus(duration);
System.out.println("当前时间:" + now);
System.out.println("2小时后:" + future);
}
}
特点:
- 更灵活,支持以毫秒、秒、分钟等单位定义复杂的时间间隔。
- 适用于需要动态计算时间段的场景(如从配置或输入中获取时间长度)。
两种方法的对比
场景 | 推荐方法 | 优势 |
---|---|---|
简单单位(秒、分、天等) | plus(long, TemporalUnit) | 代码简洁,无需额外对象创建 |
复杂或动态时间间隔 | Duration + plus(Duration) | 支持更灵活的时间计算(如分钟+秒的组合) |
注意事项
- 不可变性:
Instant
对象不可变,所有加减操作均返回新实例,原对象不受影响。 - 精度问题:
Duration
和ChronoUnit
均支持纳秒级精度,但实际应用中需注意系统时钟的精度限制。 - 时区问题:
Instant
始终基于 UTC 时间,若需本地时间转换,需结合ZonedDateTime
使用。
综合应用示例
import java.time.Instant;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
public class CombinedExample {
public static void main(String[] args) {
Instant now = Instant.now();
// 方法1:直接添加2小时
Instant future1 = now.plus(2, ChronoUnit.HOURS);
// 方法2:通过Duration添加30分钟
Duration duration = Duration.ofMinutes(30);
Instant future2 = now.plus(duration);
System.out.println("当前时间:" + now);
System.out.println("2小时后:" + future1);
System.out.println("30分钟后:" + future2);
}
}
通过以上方法,可以根据具体需求灵活处理时间计算问题。如需更复杂的日期操作(如月、年),建议结合 LocalDateTime
或 ZonedDateTime
使用。
变长参数
在 Java 中,构造函数可以完全支持可变长参数(...args
)的语法,但需要遵循以下规则和注意事项:
使用规则
语法形式
可变长参数的声明方式与普通方法一致:在参数类型后加三个点...
,例如:public class Example { // 构造函数使用可变长参数 public Example(String... args) { // 参数 args 会被编译器转换为 String[] } }
位置限制
可变长参数必须是构造函数的最后一个参数,且一个构造函数中只能有一个可变长参数。以下写法会报错:public Example(int... nums, String name) {} // 错误:可变参数不在末尾 public Example(String... s1, int... s2) {} // 错误:多个可变参数
底层实现
可变长参数本质上是语法糖,编译器会将其转换为数组。例如:public Example(String... args) { ... } // 编译后等同于: public Example(String[] args) { ... }
使用示例
基本用法
public class Person {
private String[] hobbies;
// 构造函数接收可变长参数
public Person(String... hobbies) {
this.hobbies = hobbies;
}
}
// 调用方式
Person p1 = new Person("阅读", "编程"); // 传入多个参数
Person p2 = new Person(); // 不传参数(等效于空数组)
与固定参数重载时的优先级
当存在固定参数和可变长参数的重载构造函数时,固定参数的构造函数会优先被调用:
public class MyClass {
public MyClass(String s) {
System.out.println("固定参数构造函数");
}
public MyClass(String... s) {
System.out.println("可变长参数构造函数");
}
}
// 调用示例
MyClass obj = new MyClass("hello"); // 输出:"固定参数构造函数"
注意事项
避免重载歧义
如果存在多个可能匹配的可变长参数构造函数,编译器会报错。例如:public class Test { public Test(String... args) {} public Test(String s, String... args) {} // 错误:调用时可能产生歧义 }
与数组参数的冲突
构造函数不能同时定义可变长参数和数组参数,因为两者编译后等价:public class Conflict { public Conflict(String... args) {} // 编译报错:重复定义 public Conflict(String[] args) {} }
空值处理
如果传递null
给可变长参数,需要显式转型以避免歧义:new Example((String[]) null); // 显式声明参数类型为数组
适用场景
- 灵活初始化:适用于需要支持不同数量初始化参数的场景(如日志记录器、配置类)。
- 简化代码:替代手动创建数组的操作,例如
new Config("A", "B", "C")
比new Config(new String[]{"A", "B", "C"})
更简洁。
总结
Java 构造函数完全支持可变长参数,但需注意语法规则和重载时的潜在问题。其本质是通过数组实现,能够显著提升代码的灵活性和简洁性。
Spring boot 响应 String[] vs. List
在 Spring Boot 的响应中,String[]
和 List<String>
的主要区别体现在 数据结构特性 和 框架处理机制 上,具体如下:
数据结构特性差异
可变性
String[]
:数组是定长结构,创建后长度不可变。若需修改元素,需重新创建数组。List<String>
:基于接口的集合类型,支持动态扩容(如ArrayList
实现类),可灵活增删元素。
类型兼容性
String[]
是具体实现类,耦合度高。List<String>
是接口,遵循面向接口编程原则,可替换为LinkedList
等实现类,提升代码通用性。
Spring Boot 的序列化行为
JSON 转换规则
- 两者均会被 Spring Boot 默认序列化为 JSON 数组,例如:
// String[] 返回结果 ["A", "B", "C"] // List<String> 返回结果 ["A", "B", "C"]
- 在序列化结果上无差异,但
List<String>
更符合集合操作的语义。
- 两者均会被 Spring Boot 默认序列化为 JSON 数组,例如:
统一响应格式兼容性
- 若使用统一响应封装类(如
ResultData<T>
),两者均可作为data
字段的值。但List<String>
更易于扩展其他集合操作(如分页)。 - 示例:
// 统一响应类 public ResultData<List<String>> getList() { ... } public ResultData<String[]> getArray() { ... }
- 若使用统一响应封装类(如
框架支持与开发实践
响应类型控制
- 使用
ResponseEntity<List<String>>
可显式声明响应类型,帮助 Spring 自动设置Content-Type: application/json
,避免客户端解析歧义。 String[]
虽能隐式转换,但需确保前端能正确处理数组类型。
- 使用
异常处理
- 若接口可能返回空集合,
List<String>
更自然地表示空状态([]
),而String[]
的空数组可能需要额外判断null
。
- 若接口可能返回空集合,
代码可维护性
List<String>
支持 Stream API 和集合工具类(如Collectors
),便于数据转换和过滤。String[]
需借助Arrays
工具类操作,灵活性较低。
性能与内存影响
- 内存占用
String[]
作为连续内存块,随机访问效率高;List<String>
(如ArrayList
)底层基于数组,性能接近,但存在动态扩容时的额外开销。 - 适用场景
- 数据量固定且无需修改时,优先选
String[]
。 - 高频增删或不确定数据量时,选
List<String>
。
- 数据量固定且无需修改时,优先选
最佳实践建议
优先选择
List<String>
- 符合面向接口编程原则,降低代码耦合。
- 便于未来替换集合实现(如从
ArrayList
改为LinkedList
)。
需显式返回数组时
- 在需要兼容旧系统或第三方 API 时,可使用
String[]
,但建议通过注释说明原因。
- 在需要兼容旧系统或第三方 API 时,可使用
总结对比表
特性 | String[] | List<String> |
---|---|---|
数据结构 | 定长数组 | 动态集合(如 ArrayList ) |
序列化结果 | JSON 数组 | JSON 数组 |
代码扩展性 | 低(依赖具体实现) | 高(依赖接口) |
统一响应格式兼容性 | 支持 | 更灵活 |
推荐场景 | 固定长度、无需修改 | 动态数据、高频操作 |
若需进一步优化响应格式,可参考 Spring Boot 统一封装方案。
@RequestPart和@RequestBody
在Spring框架中,@RequestPart和@RequestBody不能同时使用。以下是具体原因及替代方案:
两者不能同时使用的原因
HTTP请求体的处理机制冲突
• @RequestBody 会尝试将整个请求体(Body)作为一个整体解析,适用于如application/json
的单体数据格式。• @RequestPart 用于处理
multipart/form-data
类型的请求,允许将请求体拆分为多个独立部分(如文件、JSON块等),每个部分可独立解析。• 当两者同时使用时,Spring无法同时处理单体请求体和多部分请求体,导致参数绑定失败。
Content-Type的限制
@RequestBody
要求请求头的Content-Type
为单体格式(如application/json
),而@RequestPart
需要multipart/form-data
,二者在同一个请求中无法共存。
替代解决方案 若需要同时传递文件和复杂数据(如JSON对象),可通过以下方式实现:
方案1:统一使用 @RequestPart
将JSON数据作为 multipart/form-data
中的一个独立部分,通过 @RequestPart
接收。
示例:
@PostMapping("/upload")
public String upload(
@RequestPart("file") MultipartFile file,
@RequestPart("data") YourDataObject data // Spring会根据Content-Type自动反序列化JSON
) {
// 处理文件和对象
}
前端需通过FormData包装数据:
const formData = new FormData();
formData.append("file", file);
formData.append("data", new Blob([JSON.stringify(data)], { type: "application/json" }));
此方法通过 multipart/form-data
同时传递文件和JSON块,后端通过 @RequestPart
分别接收。
方案2:用Map接收参数(灵活性更高)
若参数类型不固定,可通过 @RequestParam Map<String, Object>
接收普通参数,结合 @RequestPart
接收文件:
@PostMapping("/upload")
public String upload(
@RequestParam Map<String, String> params,
@RequestPart("file") MultipartFile file
) {
String jsonData = params.get("data");
// 手动反序列化JSON
}
总结
场景 | 推荐注解 | 适用场景 |
---|---|---|
单体JSON请求体 | @RequestBody | 接收整个JSON对象 |
多部分数据(含文件+JSON) | @RequestPart | 每个部分独立解析,支持自动反序列化 |
混合参数(非标准格式) | @RequestParam | 手动处理参数 |
提示:在
multipart/form-data
请求中,优先使用@RequestPart
,因其支持更灵活的数据类型(如JSON、XML)和文件混合上传。
Postgres 前缀搜索
在 PostgreSQL 中实现字段前缀搜索,可以通过多种方法优化查询性能和准确性。以下是具体实现方式及优化策略:
使用 LIKE
操作符结合 B-tree 索引
基础语法
直接使用LIKE 'prefix%'
进行前缀匹配,例如:SELECT * FROM table_name WHERE column_name LIKE 'abc%';
此语法会匹配所有以
abc
开头的字符串。索引优化
- 若字段类型为
text
或varchar
,可创建 B-tree 索引并指定varchar_pattern_ops
运算符类:CREATE INDEX idx_column_prefix ON table_name (column_name varchar_pattern_ops);
- 该索引会忽略默认的排序规则,直接按字节顺序比较字符串,提升前缀匹配的效率。
- 若字段类型为
表达式索引(函数索引)
若需要动态截取字段前缀或处理复杂条件,可通过表达式索引实现:
固定长度前缀索引
使用LEFT
或SUBSTRING
函数创建索引,例如截取前 3 个字符:CREATE INDEX idx_left_prefix ON table_name (LEFT(column_name, 3));
查询时需使用相同表达式:
SELECT * FROM table_name WHERE LEFT(column_name, 3) = 'abc';
动态前缀优化
结合SUBSTRING
和LIKE
,通过两步筛选减少扫描范围:SELECT * FROM table_name WHERE SUBSTRING(column_name, 1, 10) = 'prefix_part' AND column_name LIKE 'prefix%';
使用 pg_trgm
扩展加速模糊查询
对于更复杂的前缀匹配(如大小写不敏感或部分模糊场景):
安装扩展
CREATE EXTENSION pg_trgm;
创建 GIN/GiST 索引
CREATE INDEX idx_gin_prefix ON table_name USING GIN (column_name gin_trgm_ops);
此索引支持
LIKE 'abc%'
、ILIKE
(不区分大小写)等操作。
性能优化注意事项
避免全表扫描
- 若
LIKE
以通配符开头(如%abc
),索引将失效,需调整查询模式。 - 优先使用固定前缀(如
abc%
)以利用索引。
- 若
大小写敏感问题
- 默认
LIKE
区分大小写,若需不敏感,改用ILIKE
或转换为小写:CREATE INDEX idx_lower_prefix ON table_name (LOWER(column_name)); SELECT * FROM table_name WHERE LOWER(column_name) LIKE 'abc%';
- 默认
数据分布分析
选择合适的前缀长度(如截取前 5 或 10 字符),确保索引的选择性(即唯一性足够高)。
示例场景
场景:搜索 url
表中以 https://example.com/blog
开头的记录。
优化步骤:
- 创建表达式索引:
CREATE INDEX idx_url_prefix ON url (SUBSTRING(url FROM 1 FOR 20));
- 执行查询:
SELECT * FROM url WHERE SUBSTRING(url, 1, 20) = 'https://example.com/b' AND url LIKE 'https://example.com/blog%';
总结
PostgreSQL 实现前缀搜索的核心在于合理利用索引和表达式优化。对于简单前缀匹配,LIKE 'prefix%'
结合 B-tree 索引是首选;复杂场景可借助表达式索引或 pg_trgm
扩展。注意避免全表扫描并分析数据分布,以平衡存储开销与查询效率。
@Entity Long 字段
在JPA中实现@Entity
的Long类型字段自动设置为0,可以通过以下方法实现,结合数据库默认值和Java实体类初始化两种策略:
Java实体类初始化
直接初始化字段
在字段定义时直接赋初始值0L
,适用于所有未显式赋值的场景:
@Entity
public class Product {
@Id
private Long id;
@Column(name = "stock")
private Long stock = 0L; // 直接初始化为0
}
- 说明:此方式在对象实例化时自动设置
stock
为0。若未显式赋值,持久化时字段值始终为0。
通过构造函数初始化
在实体类的构造函数中显式设置默认值:
@Entity
public class Product {
@Id
private Long id;
@Column(name = "stock")
private Long stock;
public Product() {
this.stock = 0L; // 无参构造函数初始化
}
}
- 说明:确保所有新建对象均包含默认值。需注意:若存在其他构造函数,需同步添加初始化逻辑。
数据库默认值配置
使用@Column
的columnDefinition
属性
通过数据库DDL定义默认值,并强制JPA使用该配置:
@Entity
public class Product {
@Id
private Long id;
@Column(name = "stock", columnDefinition = "bigint default 0")
private Long stock;
}
- 效果:插入记录时若未提供
stock
值,数据库自动填充0。此方法独立于Java对象状态,适用于数据库层面约束。
结合@DynamicInsert
避免NULL覆盖
当字段允许Java对象为null
但需数据库填充默认值时,需添加@DynamicInsert
:
@Entity
@DynamicInsert // 仅插入非空字段
public class Product {
@Id
private Long id;
@Column(name = "stock", columnDefinition = "bigint default 0")
private Long stock; // 可为null,但数据库会填充默认值
}
- 适用场景:当业务允许字段在Java层为
null
,但需数据库自动补0时使用。注意:若Java对象未初始化且字段为null
,需确保数据库默认值生效。
混合策略(推荐)
Java初始化 + 数据库默认值
同时初始化字段和配置数据库默认值,避免层间差异:
@Entity
public class Product {
@Id
private Long id;
@Column(name = "stock", columnDefinition = "bigint default 0")
private Long stock = 0L; // 双重保障
}
- 优势:Java对象始终有合理默认值,数据库层进一步约束数据完整性。
注意事项
- 包装类型与基本类型:若使用基本类型
long
,默认值为0,但JPA中通常建议用包装类型Long
以支持null
。需根据业务需求选择。 - Hibernate配置:确保
hibernate.ddl-auto
设置为update
或validate
,使columnDefinition
生效。 - 事务一致性:若通过Java初始化,需确保所有新建对象均通过正确途径(如工厂方法)创建,避免遗漏初始化。
总结
推荐优先使用Java直接初始化或混合策略,既能保证代码可读性,又能利用数据库约束增强数据健壮性。若需动态控制默认值(如不同环境不同配置),可结合@PrePersist
注解在持久化前赋值。