博客记录-day167-力扣+面试,面试场景题

90 阅读23分钟

一、力扣

1、圆环回原点

圆环回原点

image.png

import java.util.*;


public class Solution {
    
    public int circle (int n) {
        int[][] dp = new int[n + 1][10];
        int MOD = 1000000007;
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < 10; j++) {
                dp[i][j] = (dp[i - 1][(j - 1 + 10) % 10] + dp[i - 1][(j + 1) % 10]) % MOD;
            }
        }
        return dp[n][0];
    }
}

2、前 K 个高频元素

347. 前 K 个高频元素

image.png

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer,Integer> map=new HashMap<>();
        for(var x:nums){
            map.merge(x,1,Integer::sum);
        }
        int n=Collections.max(map.values());
        List<Integer>[] list=new List[n+1];
        Arrays.setAll(list,e->new ArrayList<>());
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            list[entry.getValue()].add(entry.getKey());
        }
        int[] res=new int[k];
        int point=0;
        for(int i=n;i>0;i--){
            for(var x:list[i]){
                if(point<k) res[point++]=x;
            }
        }
        return res;
    }
}
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int x : nums) {
            map.merge(x, 1, Integer::sum);
        }
        PriorityQueue<int[]> queue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (queue.size() == k) {
                if (entry.getValue() > queue.peek()[0]) {
                    queue.poll();
                    queue.offer(new int[] { entry.getValue(), entry.getKey() });
                }
            } else {
                queue.offer(new int[] { entry.getValue(), entry.getKey() });
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k; ++i) {
            ret[i] = queue.poll()[1];
        }
        return ret;
    }
}

二、面试

1、 加分布式滑块锁的意义,能否加库存,加库存后如何处理?

1. 加分布式滑块锁的意义

再细化一下,多个用户过来请求库存值,是不做任何加锁操作直接读取出来对吗?对。 追问:decr已经是原子操作了「多个请求一定是串行的」,你为什么还要加setnx锁?「是否有必要加这个锁」 加锁的意义是什么,不加这个锁又会出现什么问题?

回答:其实本身原本的库存,扣减最早的方式是数据库行级锁,数据库扛不住。优化到redis缓存,使用了redis独占锁,但这样会出现排队问题,导致有库存,但吞吐量不佳。后来设计为颗粒度更细的无锁化(乐观锁)设计。incr/decr 是原子操作,基本也不会出问题,这个操作就有点像数据库计数,1、2、3、4... 而加锁类似写了流水记录。相对来说这样的方式更可靠。避免在;集群配置、网络、库存恢复、人工调整等场景时,导致 incr/decr 的值不对。因为如果不加锁,这个时候不对,是不知道的。不知道就很可怕,哪怕有万分之一的概率,只要有足够多的用户参与,也会出问题。(尤其运营配置,运营是很多人,也有很多新人,实际公司里很多事故都是运营配置导致的,而且出问题的时候是一堆人围着一个运营,运营很慌,配置错的概率更大)

2. 能否加库存,加库存后如何处理

假设我当前活动奖品A还要十个库存,运营想再加十个库存,你这个时候是怎么处理的,支持吗? 追问:假设我再增加一次库存,或者n次库存,你是怎么处理的呢?「运营经常对库存做一些操作」 我的回答:活动降级补充库存?或者配置多个库存池,或者用incr+消息队列去补充库存

回答:可以先调整数据库库存,之后使用 incrby 给缓存加库存。这个时候要注意使用 incr 和总量对比。如果有对库存的失败记录,可以用 incr 和 总量 + 恢复了对比。 加锁,用于兜底;集群配置、网络、库存恢复、人工调整等。尽量避免出问题,加库存就是其中的一个。

2、 项目的亮点或较难的地方在哪里?

你的项目有哪些难点&亮点?

image.png

✅基于本地消息表实现分布式事务保证最终一致性

2.1 领域驱动设计(DDD)架构

项目采用了 DDD 架构,将系统分为多个模块:

  • big-market-api: 对外接口层
  • big-market-app: 应用层
  • big-market-domain: 领域层
  • big-market-infrastructure: 基础设施层
  • big-market-trigger: 触发器层
  • big-market-types: 类型定义层

这种分层架构使得系统职责清晰,便于维护和扩展。

2.2 责任链模式实现抽奖规则

从代码中可以看到使用了责任链模式(如 BackListLogicChain 类)来处理不同的抽奖规则,这使得规则可以灵活组合和扩展:

  • 黑名单规则链
  • 权重规则链
  • 锁定规则链(通过抽奖次数解锁)

2.3 库存管理的异步处理

项目中实现了奖品库存的异步更新机制:

  • 使用 Redis 缓存库存信息
  • 通过延迟队列异步更新数据库库存
  • 定时任务 UpdateAwardStockJob 处理库存更新

这种设计避免了直接对数据库的频繁操作,提高了系统性能。

2.4 事件发布订阅模式

项目使用了事件发布订阅模式处理业务流程:

  • EventPublisher 负责消息发送
  • 使用 RabbitMQ 作为消息队列
  • 通过 TaskEntity 记录消息发送状态

2.5 消息补偿机制

实现了消息发送失败的补偿机制:

  • 消息发送状态记录
  • 失败任务的重试机制
  • 事务外执行消息发送,保证数据一致性

2.6 分库分表策略

项目使用了自定义的数据库路由策略:

  • @DBRouter 注解标记需要路由的方法
  • IDBRouterStrategy 接口定义路由策略
  • 基于用户ID进行分库分表

2.7 完善的单元测试

项目包含了丰富的单元测试,如 RaffleStrategyTestAwardDaoTest 等,确保各模块功能正常。

2.8 容器化部署

项目支持 Docker 容器化部署:

  • 包含 Dockerfile 和构建脚本

  • docker-compose 配置文件支持环境依赖(MySQL、Redis、RabbitMQ)的快速部署

3、 锁机制是如何实现的?(回答:使用Redis做次数锁)

decr预扣减+setnx分段锁

4、 布隆过滤器的底层实现逻辑是什么?如何减小误判误差?

✅布隆过滤器有什么缺点,如何解决?

image.png

对数据进行二次查询。

布隆过滤器基于位数组和多个独立哈希函数。元素插入时,通过多个哈希函数映射到位数组的多个位置并置1;查询时检查所有位置是否全为1,若否则元素一定不存在,若是则可能存在(误判)。​​减小误判误差的方法​​:

  1. ​增大位数组大小​​:减少哈希碰撞概率。
  2. ​增加哈希函数数量​​:提高分布均匀性,但会增加计算开销。
  3. ​选择低冲突的哈希函数​​:如MurmurHash等非加密型哈希。
  4. ​结合其他数据结构​​(如布谷鸟过滤器)或定期重建过滤器,但需权衡空间与时间效率。

5、 JVM对象创建的完整生命周期是怎样的?(涉及类加载、内存分配、垃圾回收等过程)

✅JVM是如何创建对象的?

image.png

6、 双亲委派机制是什么?有什么好处?

✅什么是双亲委派?如何破坏?

image.png

7、 常见的垃圾回收算法有哪些?各自适用场景和原理是什么?

✅JVM有哪些垃圾回收算法?

  1. ​标记-清除​​:标记无用对象后清除,产生内存碎片,适用于老年代(如CMS的并发标记清除)。
  2. ​复制算法​​:将内存分为两块,存活对象复制到另一块后清理旧块,适用于新生代(如Serial/ParNew)。
  3. ​标记-整理​​:标记后存活对象向一端移动,清理边界外内存,避免碎片,适用于老年代(如Parallel Old)。
  4. ​分代收集​​:结合新生代(复制)和老年代(标记-整理/CMS),适应不同对象存活率。
  5. ​增量收集(如G1)​​:分区管理,可预测停顿,适用于大堆内存和低延迟场景。

8、 JVM运行时数据区的各个部分及其作用是什么?(包括本地方法栈等)

✅JVM的运行时内存区域是怎样的?

  1. ​程序计数器​​:线程私有,记录当前线程执行的字节码行号(用于分支、循环等控制)。
  2. ​Java虚拟机栈​​:线程私有,存放方法栈帧(局部变量、操作数栈、动态链接等)。
  3. ​本地方法栈​​:为Native方法服务,与虚拟机栈类似。
  4. ​堆​​:线程共享,存放对象实例和数组,GC主要管理区域(分新生代、老年代)。
  5. ​方法区​​(元空间):存储类元信息、常量、静态变量,JDK8后使用本地内存。
  6. ​直接内存​​(堆外内存):通过NIO的ByteBuffer分配,减少堆内存拷贝,但受操作系统限制。

9、 JNI相关知识。如何用C语言实现比Java更高效的算法?(涉及JNI相关知识)

JNI允许Java调用本地(C/C++)代码,提升性能的场景:

  1. ​密集计算​​:如数学运算(FFT)、图像处理,C可直接操作内存,避免JVM解释开销。

  2. ​系统级调用​​:如文件IO、网络协议栈,绕过Java安全检查。

  3. ​优化技巧​​:

    • 减少JNI方法调用次数(批量处理数据)。
    • 使用GetPrimitiveArrayCritical直接访问数组内存,避免复制。
    • 避免频繁创建/销毁本地引用,使用全局引用缓存对象。
      示例:用C实现矩阵乘法,通过指针操作连续内存,比Java双重循环快10~100倍。

三、面试场景题

1、在海量用户场景下,如何判断一个用户是否抽过奖?

在海量用户场景下判断用户是否抽过奖,可采用布隆过滤器实现空间高效的概率判断,Redis位图存储用户状态实现高性能查询,分库分表+索引方案保障数据持久化,或结合多种技术的混合方案,通过多层缓存设计兼顾性能与可靠性,满足海量用户高并发抽奖场景需求。

1. 布隆过滤器方案

布隆过滤器是一种空间效率极高的概率型数据结构,特别适合判断"是否存在"的场景。

工作原理

  • 使用多个哈希函数将用户ID映射到位数组的不同位置
  • 查询时,如果对应位置都为1,则可能存在;如果有一个为0,则一定不存在
  • 内存占用极小,10亿用户只需约1.8GB内存(假设1%误判率)

优势

  • 查询和写入都是O(1)时间复杂度,性能极高
  • 内存占用极小,适合海量数据
  • 可以分布式部署

局限性

  • 有一定误判率(只会误判为"存在",不会误判为"不存在")
  • 不支持删除操作

2. Redis位图方案

利用Redis的Bitmap数据结构,每个用户占用1位。

实现方式

  • 使用SETBIT key offset 1标记用户已抽奖
  • 使用GETBIT key offset查询用户是否抽过奖
  • 可按活动ID或日期分开存储

优势

  • 极高的空间效率,1亿用户仅需约12MB内存
  • 查询性能高,O(1)复杂度
  • 支持持久化和分布式部署

适用场景

  • 用户ID为连续数字或可映射为连续数字
  • 单个活动参与用户量在亿级以下

3. 分库分表+数据库索引方案

对于需要持久化且有复杂查询需求的场景。

实现方式

  • 按用户ID哈希分库分表
  • 在用户ID和活动ID上建立联合索引
  • 使用读写分离提高查询性能

优势

  • 数据持久化,不会丢失
  • 支持复杂查询和统计
  • 可靠性高

局限性

  • 查询性能比内存方案低
  • 系统复杂度高
  • 存储成本高

2、如何统计每天用户的抽奖按钮点击次数?

统计每天用户抽奖按钮点击次数可通过Redis实时计数器实现高性能实时统计,日志收集+离线分析方案支持深度多维分析,流式计算框架实现准实时统计处理,或采用混合架构方案结合实时计数与批处理分析,通过双写架构保障数据一致性,同时支持多维度分析与异常监控。

1. 实时计数器方案

使用Redis实现高性能实时计数。

实现方式

  • 使用INCR命令增加计数
  • 按日期和用户ID组织键名:clicks:YYYY-MM-DD:userId
  • 设置TTL自动过期(如保留30天)

数据结构设计

  • 用户维度:clicks:date:userId → 计数值
  • 全局维度:clicks:date:total → 总计数值
  • 可使用Hash结构优化内存:HINCRBY clicks:date userId 1

优势

  • 实时性高,写入和查询都是O(1)
  • 支持多维度统计
  • 自动过期机制节省存储空间

2. 日志收集+离线分析方案

适合需要深度分析的场景。

实现流程

  1. 记录点击事件日志(包含用户ID、时间戳、事件类型等)
  2. 使用日志收集系统(如ELK、Flume)收集日志
  3. 通过Hadoop/Spark等进行离线分析
  4. 生成报表并存储结果

优势

  • 支持复杂的多维分析
  • 可追溯历史数据
  • 可与其他业务数据关联分析

局限性

  • 实时性较差
  • 系统复杂度高
  • 存储和计算成本高

3. 流式计算方案

使用流计算框架实现准实时统计。

技术选型

  • Flink/Spark Streaming等流计算框架
  • Kafka作为消息队列
  • Redis/HBase存储统计结果

实现流程

  1. 点击事件发送到Kafka
  2. 流计算作业消费消息并进行窗口计算
  3. 结果写入存储系统
  4. 提供API查询统计结果

优势

  • 准实时性(秒级延迟)
  • 支持复杂的时间窗口计算
  • 可扩展性好,适合大规模数据

4. 混合方案(推荐)

结合实时计数和批处理分析:

架构设计

  1. 前端埋点收集点击事件
  2. 双写架构:
    • 写入Redis进行实时计数
    • 写入Kafka用于后续分析
  3. 定时任务将Redis数据持久化到数据库
  4. 流计算作业处理Kafka数据,生成多维度统计结果

数据一致性保障

  • 使用分布式事务或最终一致性方案
  • 定期对账和修正数据偏差

扩展能力

  • 支持按时间、地域、用户群体等多维度分析
  • 支持异常检测和实时告警

3、如何统计API接口的调用耗时?

API接口调用耗时统计可通过代码埋点(AOP切面、过滤器、注解)实现细粒度监控,专业APM工具提供全面监控与可视化分析,分布式追踪系统实现微服务架构下的全链路分析,或构建多层次监控体系,从基础设施层到业务层全面覆盖,通过采集-存储-分析-可视化-告警的完整流程,及时发现并解决性能问题。

1. 代码埋点方案

在代码层面实现耗时统计。

实现方式

  1. AOP切面

    • 定义切面拦截所有API方法
    • 记录方法执行前后的时间差
    • 支持细粒度控制和自定义标签
  2. 过滤器/拦截器

    • 在请求处理前后记录时间
    • 适合Web应用,可获取HTTP相关信息
    • 实现简单,对代码侵入性低
  3. 注解方式

    • 自定义注解标记需要监控的方法
    • 结合AOP实现,灵活性高
    • 可按业务场景分类统计

数据处理

  • 本地聚合后定期上报
  • 使用异步方式避免影响主流程
  • 设置采样率减少数据量

2. 监控系统方案

使用专业的APM(应用性能监控)工具。

主流工具

  • Prometheus + Grafana:开源监控系统,支持自定义指标和告警
  • SkyWalking:分布式追踪系统,支持全链路分析
  • Pinpoint:细粒度性能分析工具
  • Datadog/New Relic:商业APM解决方案

实现方式

  • 集成监控SDK到应用中
  • 配置监控指标和采样规则
  • 设置可视化面板和告警规则

优势

  • 开箱即用,功能全面
  • 支持多维度分析和可视化
  • 有完善的告警机制

3. 分布式追踪方案

适合微服务架构下的全链路监控。

技术选型

  • Jaeger/Zipkin:开源分布式追踪系统
  • OpenTelemetry:可观测性标准
  • SkyWalking:全栈监控平台

核心概念

  • Trace:一次完整的请求调用链
  • Span:调用链中的一个操作单元
  • Context Propagation:上下文传播机制

实现流程

  1. 在服务入口创建Trace
  2. 在每个调用点创建Span
  3. 通过上下文传播机制关联Span
  4. 收集Trace数据并分析

优势

  • 支持全链路分析
  • 可视化调用关系
  • 精准定位性能瓶颈

4. 综合解决方案(推荐)

多层次监控体系:

监控层次

  1. 基础设施层

    • 服务器CPU、内存、网络监控
    • 数据库性能监控
    • 中间件监控
  2. 应用层

    • API响应时间监控
    • 错误率监控
    • JVM/容器资源监控
  3. 业务层

    • 核心业务指标监控
    • 用户体验监控
    • 业务异常监控

数据处理流程

  1. 采集:多种方式收集性能数据
  2. 存储:时序数据库存储监控数据
  3. 分析:统计分析和异常检测
  4. 可视化:直观展示性能指标
  5. 告警:及时发现并通知异常

最佳实践

  • 设置合理的采样率和聚合策略
  • 建立基线和阈值,实现智能告警
  • 关联业务指标,评估性能影响
  • 定期优化和回顾

4、如何将大文件从一个数据库安全完整地转移到另一个数据库?(需考虑顺序性保障)

大文件从一个数据库安全完整转移到另一个数据库,可采用批量处理方案分批迁移减少资源占用,专业ETL工具提供可视化配置与监控,数据库原生工具实现高效同类型迁移,CDC方案支持最小停机时间的实时同步,或根据数据规模选择合适的综合方案,通过唯一标识、排序字段、检查点机制、事务完整性、数据验证、回滚机制和监控告警等措施保障数据迁移的顺序性与完整性。

1. 批量处理方案

通过分批处理减少资源占用和风险。

实现步骤

  1. 数据分析

    • 分析数据量和结构
    • 确定主键或唯一标识
    • 评估依赖关系
  2. 分批策略

    • 按主键范围分批
    • 每批大小根据系统性能调整(通常500-5000条)
    • 保持每批数据的完整性
  3. 顺序保障

    • 使用有序字段(如自增ID、时间戳)
    • 严格按顺序处理批次
    • 记录处理进度和检查点
  4. 事务控制

    • 每批使用独立事务
    • 设置合理的超时时间
    • 实现幂等性处理

优势

  • 资源占用可控
  • 支持断点续传
  • 出错影响范围小

2. ETL工具方案

使用专业数据集成工具。

主流工具

  • Apache NiFi:可视化数据流管理
  • Talend:企业级数据集成平台
  • Informatica:商业ETL解决方案
  • Kettle(Pentaho):开源ETL工具

实现流程

  1. 配置源数据库和目标数据库连接
  2. 设计数据转换流程
  3. 配置错误处理和重试机制
  4. 设置监控和日志记录
  5. 执行并监控迁移过程

优势

  • 可视化配置,降低开发成本
  • 内置各种数据源连接器
  • 提供完善的监控和日志

3. 数据库原生工具方案

利用数据库自带的导入导出功能。

MySQL实现

# 导出(保持顺序)
mysqldump --single-transaction --order-by-primary --where="1 order by id" -u user -p db_name table_name > data.sql

# 导入
mysql -u user -p target_db < data.sql

PostgreSQL实现

# 导出
pg_dump -t table_name --data-only --column-inserts source_db > data.sql

# 导入
psql target_db < data.sql

优势

  • 工具成熟稳定
  • 支持事务和一致性
  • 适合同类型数据库迁移

4. CDC(变更数据捕获)方案

适合需要实时同步或最小停机时间的场景。

技术选型

  • Debezium:开源CDC平台
  • Oracle GoldenGate:商业CDC解决方案
  • AWS DMS:云平台数据迁移服务
  • Canal:阿里开源MySQL binlog解析工具

实现原理

  1. 捕获源数据库的变更日志(如binlog)
  2. 解析变更事件并转换为标准格式
  3. 按顺序应用到目标数据库
  4. 保证事务一致性和顺序性

优势

  • 最小停机时间
  • 保证数据一致性
  • 支持异构数据库迁移

综合解决方案(推荐)

根据数据规模和业务要求选择合适方案:

小规模数据(<10GB):

  • 使用数据库原生工具一次性迁移
  • 设置合理的事务大小和超时时间
  • 迁移后进行数据校验

中等规模数据(10GB-100GB):

  • 使用批处理方案分批迁移
  • 实现检查点机制支持断点续传
  • 并行处理无依赖的表

大规模数据(>100GB):

  • 使用专业ETL工具或CDC方案
  • 先迁移历史数据,再同步增量变更
  • 实施分阶段迁移策略

顺序性保障关键措施

  1. 唯一标识:确保每条记录有唯一标识
  2. 排序字段:使用自增ID或时间戳等有序字段
  3. 检查点机制:记录已处理位置
  4. 事务完整性:保证每批数据的事务完整性
  5. 数据验证:迁移后进行数据一致性校验
  6. 回滚机制:出错时能够安全回滚
  7. 监控告警:实时监控迁移进度和异常

迁移前准备工作

  • 评估数据量和结构
  • 制定详细迁移计划
  • 准备充足的存储空间
  • 设置监控和告警
  • 进行小规模测试验证
  • 制定应急回滚方案

迁移后验证工作

  • 数据完整性校验

  • 数据一致性校验

  • 业务功能测试

  • 性能测试

  • 监控系统运行状态

5、项目中接口和抽象类的使用分析

一、接口(Interface)的使用

接口在项目中主要用于定义行为契约,实现依赖倒置原则,使高层模块不依赖于低层模块的实现细节。

1. 仓储接口
public interface IActivityRepository {
    ActivitySkuEntity queryActivitySku(Long sku);
    ActivityEntity queryRaffleActivityByActivityId(Long activityId);
    // ... existing code ...
}

使用原因

  • 遵循领域驱动设计(DDD)思想,在领域层定义仓储接口,由基础设施层实现
  • 实现依赖倒置,领域层不依赖于具体的数据访问技术
  • 便于单元测试,可以通过Mock仓储接口进行测试
2. 服务接口
public interface IRaffleActivityService {
    /**
     * 活动装配,数据预热缓存
     * @param activityId 活动ID
     * @return 装配结果
     */
    Response<Boolean> armory(Long activityId);
    
    /**
     * 活动抽奖接口
     * @param request 请求对象
     * @return 返回结果
     */
    Response<ActivityDrawResponseDTO> draw(ActivityDrawRequestDTO request);
}

使用原因

  • 定义服务契约,明确服务边界
  • 便于不同模块之间的集成
  • 支持面向接口编程,降低系统耦合度
3. DAO接口
@Mapper
public interface IRaffleActivityAccountDao {
    void insert(RaffleActivityAccount raffleActivityAccount);
    int updateAccountQuota(RaffleActivityAccount raffleActivityAccount);
    // ... existing code ...
}

使用原因

  • 与ORM框架(如MyBatis)集成,通过接口定义SQL操作
  • 分离数据访问逻辑与业务逻辑
  • 便于数据库操作的统一管理
4. 责任链接口
public interface ILogicChainArmory {
    ILogicChain next();
    ILogicChain appendNext(ILogicChain next);
}

使用原因

  • 定义责任链模式的标准行为
  • 支持灵活的链式处理流程
  • 便于扩展新的处理节点

二、抽象类(Abstract Class)的使用

抽象类在项目中主要用于提供基础实现,封装共同行为,同时允许子类定制特定行为。

1. 抽象策略类
@Slf4j
public abstract class AbstractRaffleStrategy implements IRaffleStrategy {
    // 策略仓储服务
    protected IStrategyRepository repository;
    // 策略调度服务
    protected IStrategyDispatch strategyDispatch;
    // ... existing code ...
    
    @Override
    public RaffleAwardEntity performRaffle(RaffleFactorEntity raffleFactorEntity) {
        // 1. 参数校验
        // ... existing code ...
        
        // 2. 责任链抽奖计算
        // ... existing code ...
        
        // 3. 规则树抽奖过滤
        // ... existing code ...
        
        // 4. 返回抽奖结果
        return buildRaffleAwardEntity(strategyId, treeStrategyAwardVO.getAwardId(), treeStrategyAwardVO.getAwardRuleValue());
    }
    
    // 抽象方法,由子类实现
    public abstract DefaultChainFactory.StrategyAwardVO raffleLogicChain(String userId, Long strategyId);
    
    // ... existing code ...
}

使用原因

  • 实现模板方法模式,定义算法骨架,让子类实现特定步骤
  • 封装共同的业务逻辑,减少代码重复
  • 提供必要的抽象方法,强制子类实现特定行为
2. 抽象责任链
public abstract class AbstractActionChain implements IActionChain {
    private IActionChain next;

    @Override
    public IActionChain next() {
        return next;
    }

    @Override
    public IActionChain appendNext(IActionChain next) {
        this.next = next;
        return next;
    }
}

使用原因

  • 实现责任链模式的基础功能
  • 封装链式调用的通用逻辑
  • 让子类专注于实现具体的处理逻辑
3. 抽象活动配额服务
@Slf4j
public abstract class AbstractRaffleActivityAccountQuota extends RaffleActivityAccountQuotaSupport implements IRaffleActivityAccountQuotaService {

    public AbstractRaffleActivityAccountQuota(IActivityRepository activityRepository, DefaultActivityChainFactory defaultActivityChainFactory) {
        super(activityRepository, defaultActivityChainFactory);
    }

    @Override
    public String createOrder(SkuRechargeEntity skuRechargeEntity) {
        // 1. 参数校验
        // ... existing code ...
        
        // 2. 查询基础信息
        // ... existing code ...
        
        // 3. 活动动作规则校验
        // ... existing code ...
        
        // 4. 构建订单聚合对象
        // ... existing code ...
        
        // 5. 保存订单
        // ... existing code ...
        
        // 6. 返回单号
        return createOrderAggregate.getActivityOrderEntity().getOrderId();
    }

    protected abstract CreateQuotaOrderAggregate buildOrderAggregate(SkuRechargeEntity skuRechargeEntity, ActivitySkuEntity activitySkuEntity, ActivityEntity activityEntity, ActivityCountEntity activityCountEntity);

    protected abstract void doSaveOrder(CreateQuotaOrderAggregate createOrderAggregate);
}

使用原因

  • 实现模板方法模式,定义订单创建的标准流程
  • 封装通用的业务逻辑和校验规则
  • 通过抽象方法让子类实现特定的订单构建和保存逻辑
4. 抽象事件基类
@Data
public abstract class BaseEvent<T> {
    public abstract EventMessage<T> buildEventMessage(T data);
    public abstract String topic();

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class EventMessage<T> {
        private String id;
        private Date timestamp;
        private T data;
    }
}

使用原因

  • 定义事件的基本结构和行为
  • 使用泛型支持不同类型的事件数据
  • 强制子类实现特定的事件构建和主题定义方法
三、接口与抽象类的选择原则

在项目中,接口和抽象类的选择遵循以下原则:

  1. 使用接口的场景

    • 需要定义行为契约但不提供实现时
    • 需要支持多重继承时
    • 实现依赖倒置原则,解耦系统组件时
    • 定义服务边界和API时
  2. 使用抽象类的场景

    • 需要提供部分实现,减少子类代码重复时
    • 实现模板方法模式,定义算法骨架时
    • 需要在相关类之间共享状态和行为时
    • 需要定义受保护的方法和字段时

四、总结

在big-market项目中,接口和抽象类的使用体现了良好的面向对象设计原则:

  1. 接口主要用于定义契约、实现依赖倒置、支持多态,如仓储接口、服务接口等。

  2. 抽象类主要用于实现模板方法模式、封装共同行为、提供基础实现,如抽象策略类、抽象责任链等。

  3. 两者结合使用,构建了灵活、可扩展的系统架构,体现了"面向接口编程"和"组合优于继承"的设计思想。

这种设计方式使得系统具有良好的可维护性、可扩展性和可测试性,是Java企业级应用开发的最佳实践之一。