一、面试
1、数据库相关
1. B+树的结构特点及为何适合磁盘IO?
2. B树与B+树的区别是什么?
3. MySQL如何保证事务的原子性?
4. 客户端异常断开时,未提交的事务如何回滚?
自动回滚机制
- 事务的原子性保障:InnoDB存储引擎通过ACID特性保证事务的原子性。若客户端断开时事务未提交,MySQL会自动执行回滚,撤销所有未持久化的修改。
- 连接状态监测:服务器会定期检查连接活跃状态。若连接异常终止(如网络中断、客户端崩溃),未提交的事务会被识别为“悬空事务”并立即回滚。
2、分布式系统相关
1. Redis的ZSET(有序集合)如何实现?底层结构是跳表还是其他组合?
2. 缓存击穿的定义及解决方案是什么?
3、项目
1. 责任链模式的核心思想是什么?在抽奖系统中如何划分过滤环节?
责任链模式的核心思想是将请求的发送者和接收者解耦,通过一系列处理对象组成一条链,请求沿着这条链传递,直到有对象处理它或者到达链尾。
在代码中,我们可以看到抽奖系统中的责任链模式实现:
@Slf4j
public abstract class AbstractLogicChain implements ILogicChain{
private ILogicChain next;
@Override
public ILogicChain next() {
return next;
}
@Override
public ILogicChain appendNext(ILogicChain next) {
this.next = next;
return next;
}
protected abstract String ruleModel();
}
在抽奖系统中的过滤环节划分:
- 黑名单过滤:检查用户是否在黑名单中
- 规则权重过滤:根据用户积分等因素调整中奖概率
- 规则锁定过滤:检查用户是否满足抽奖次数等条件
- 库存过滤:检查奖品是否有库存
这些过滤环节通过责任链依次执行,每个环节可以决定是否继续传递请求或者直接返回结果。
2. 黑名单过滤失败后的兜底逻辑是什么?
从代码中可以看出,当用户命中黑名单时,系统会有兜底逻辑:
@Test
public void test_performRaffle_blacklist() {
RaffleFactorEntity raffleFactorEntity = RaffleFactorEntity.builder()
.userId("user003") // 黑名单用户 user001,user002,user003
.strategyId(100001L)
.build();
RaffleAwardEntity raffleAwardEntity = raffleStrategy.performRaffle(raffleFactorEntity);
log.info("请求参数:{}", JSON.toJSONString(raffleFactorEntity));
log.info("测试结果:{}", JSON.toJSONString(raffleAwardEntity));
}
黑名单过滤失败后的兜底逻辑是:
- 系统会返回一个默认的奖品或者未中奖结果
- 记录用户的抽奖行为
- 不继续执行后续的抽奖逻辑
这样可以确保即使用户在黑名单中,系统也能正常响应,不会出现异常情况。
3. 如何实现即使命中黑名单仍可调整中奖概率的功能?
从代码中可以看出,系统通过规则权重链来实现对黑名单用户的中奖概率调整:
@Before
public void setUp() {
// 策略装配 100001、100002、100003
log.info("测试结果:{}", strategyArmory.assembleLotteryStrategy(100001L));
log.info("测试结果:{}", strategyArmory.assembleLotteryStrategy(100006L));
// 通过反射 mock 规则中的值
ReflectionTestUtils.setField(ruleWeightLogicChain, "userScore", 4900L);
ReflectionTestUtils.setField(ruleLockLogicTreeNode, "userRaffleCount", 10L);
}
实现方式:
- 使用规则权重链(
RuleWeightLogicChain)来调整中奖概率 - 即使用户在黑名单中,系统仍然可以根据用户的积分等因素调整中奖概率
- 通过配置不同的权重值,可以实现对黑名单用户的差异化处理
- 系统可以根据业务需求,为黑名单用户设置极低的中奖概率,而不是完全禁止
4. 奖品库存为何使用Redis预扣键?如何保证最终一致性?
奖品库存使用Redis预扣键的原因:
- 高性能:Redis的内存操作速度快,适合高并发场景
- 原子性:Redis的操作具有原子性,避免并发问题
- 预扣机制:先在Redis中预扣库存,再异步更新数据库,减轻数据库压力
从代码中可以看出系统如何保证最终一致性:
@Scheduled(cron = "0/5 * * * * ?")
public void exec() {
try {
ActivitySkuStockKeyVO activitySkuStockKeyVO = skuStock.takeQueueValue();
if (null == activitySkuStockKeyVO) return;
log.info("定时任务,更新活动sku库存 sku:{} activityId:{}", activitySkuStockKeyVO.getSku(), activitySkuStockKeyVO.getActivityId());
skuStock.updateActivitySkuStock(activitySkuStockKeyVO.getSku());
} catch (Exception e) {
log.error("定时任务,更新活动sku库存失败", e);
}
}
保证最终一致性的机制:
- 延迟队列:使用延迟队列存储需要更新的库存信息
- 定时任务:定期从队列中获取数据并更新数据库
- 事务处理:使用事务确保数据库操作的原子性
- 异常处理:对异常情况进行处理,确保数据不会丢失
- 消息机制:当库存为零时,发送消息通知相关服务
5. 延迟队列的设计原理是什么?底层是否依赖Redis?为何选择延迟队列?
延迟队列的设计原理:
- 消息延迟投递:消息不会立即被消费,而是在指定的延迟时间后才可被消费
- 有序性:按照延迟时间排序,确保按时间顺序处理
- 可靠性:确保消息不会丢失,即使系统崩溃也能恢复
系统使用了Redisson的RDelayedQueue实现延迟队列功能,底层依赖Redis。
选择延迟队列的原因:
- 异步处理:减轻主流程压力,提高响应速度
- 削峰填谷:在高并发场景下,可以平滑处理请求
- 数据一致性:通过延迟队列和定时任务,保证Redis和数据库的数据最终一致性
- 故障恢复:即使系统崩溃,队列中的消息也不会丢失
- 减少数据库压力:避免频繁更新数据库,提高系统整体性能
通过EventPublisher类,系统实现了消息的发布功能:
public void publish(String topic, BaseEvent.EventMessage<?> eventMessage) {
try {
String messageJson = JSON.toJSONString(eventMessage);
rabbitTemplate.convertAndSend(topic, messageJson);
log.info("发送MQ消息 topic:{} message:{}", topic, messageJson);
} catch (Exception e) {
log.error("发送MQ消息失败 topic:{} message:{}", topic, JSON.toJSONString(eventMessage), e);
throw e;
}
}
这种设计使系统能够高效处理大量的抽奖请求,同时保证数据的一致性和可靠性。
4、算法
题目:二维坐标系中,小动物从原点出发,每次可向四个方向(上下左右)移动一步,走N步后可能到达的位置数量是多少?(N≤50)
public class CoordinatePositions {
public static void main(String[] args) {
// 测试不同步数的结果
for (int n = 0; n <= 5; n++) {
System.out.println("走 " + n + " 步后可能的位置数量: " + countPositions(n));
}
}
/**
* 计算走N步后可能的位置数量
* @param n 步数
* @return 可能的位置数量
*/
public static int countPositions(int n) {
// 通用公式:(n+1)²
return (n + 1) * (n + 1);
}
}