博客记录-day134-力扣+项目MQ消息+Maven,场景题面试题

114 阅读19分钟

一、力扣

1、MySQL每月交易

1193. 每月交易 I

image.png

image.png

select DATE_FORMAT(trans_date, '%Y-%m') month,
        country,
        count(1) trans_count, 
        sum(if(state='approved',1,0)) approved_count, 
        sum(amount) trans_total_amount, 
        sum(if(state='approved',amount,0)) approved_total_amount
from Transactions
group by month,country

1. 常用MySQL函数

以下是 MySQL 中常用函数的简要介绍:


1. COUNT()
用于统计行数,可计算所有行(COUNT(*))或某列非 NULL 的行数(COUNT(column)),返回整数。
示例SELECT COUNT(*) FROM orders; 统计订单总数。

count(if(state='approved',1,0))

上面函数是错误的,count会统计0,应该改为

count(if(state='approved',1,null))

2. SUM()
对数值列求和,自动忽略 NULL,返回数值结果。
示例SELECT SUM(price) FROM products; 计算商品总价。


3. AVG()
计算数值列的平均值,忽略 NULL,返回浮点数或定点数。
示例SELECT AVG(score) FROM exams; 计算平均分。


4. MAX() / MIN()
返回某列的最大或最小值,支持数值、日期和字符串(按字典序)。
示例SELECT MAX(age) FROM users; 查找最年长用户的年龄。


5. GROUP_CONCAT()
将分组内的多行数据合并为字符串,默认以逗号分隔。
示例SELECT GROUP_CONCAT(name) FROM students; 拼接所有学生姓名。


6. DATEDIFF()
计算两个日期之间的天数差(结束日期 - 开始日期)。
示例SELECT DATEDIFF('2023-10-10', '2023-10-01'); 返回 9 天。


7. IFNULL(expr1, expr2)
expr1 不为 NULL,则返回 expr1,否则返回 expr2
示例SELECT IFNULL(address, '未知') FROM users; 空地址替换为“未知”。


8. CONCAT(str1, str2, ...)
拼接多个字符串,若任一参数为 NULL,则结果为 NULL。
示例SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users; 合并姓名。


9. SUBSTRING(str, start, length)
截取字符串的子串,从指定位置开始(start 可正负),取指定长度。
示例SELECT SUBSTRING('Hello', 2, 3); 返回 'ell'。


10. COALESCE(expr1, expr2, ...)
返回参数列表中第一个非 NULL 的值,全为 NULL 则返回 NULL。
示例SELECT COALESCE(phone, email, '无联系方式') FROM contacts; 优先显示有效联系方式。

2、最长递增子序列

300. 最长递增子序列

image.png

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        // dp数组用于维护递增子序列的最小末尾值,长度为n+1方便索引操作
        int[] dp = new int[n + 1];
        int len = 1;          // 当前已找到的最长递增子序列长度
        dp[len] = nums[0];    // 初始化第一个元素的序列
        
        // 遍历数组中的每个数字
        for (int i = 1; i < n; i++) {
            // 情况1:当前数字比最长序列末尾大,直接扩展序列
            if (nums[i] > dp[len]) {
                dp[++len] = nums[i];
            } else {
                // 情况2:通过二分查找找到合适的位置替换
                // 目的是维护dp数组的"递增性质",使得后续更容易构成更长序列
                int left = 1, right = len;
                while (left < right) {
                    int mid = (left + right) / 2;
                    // 寻找第一个 >= nums[i] 的位置
                    if (dp[mid] < nums[i]) {
                        left = mid + 1;
                    } else {
                        right = mid;
                    }
                }
                // 将找到的位置的值更新为更小的nums[i]
                dp[right] = nums[i];
            }
        }
        return len; // 返回最长递增子序列的长度
    }
}

3、最长连续序列

128. 最长连续序列

image.png

class Solution {
    public int longestConsecutive(int[] nums) {
        int ans = 0;
        Set<Integer> st = new HashSet<>();
        for (int num : nums) {
            st.add(num); // 把 nums 转成哈希集合
        }
        for (int x : st) { // 遍历哈希集合
            //如果有x-1,那么已经计算过
            //必须优化,否则超时
            if (st.contains(x - 1)) {
                continue;
            }
            // x 是序列的起点
            int y = x + 1;
            while (st.contains(y)) { // 不断查找下一个数是否在哈希集合中
                y++;
            }
            // 循环结束后,y-1 是最后一个在哈希集合中的数
            ans = Math.max(ans, y - x); // 从 x 到 y-1 一共 y-x 个数
        }
        return ans;
    }
}

4、汇总区间

228. 汇总区间

image.png

class Solution {
    public List<String> summaryRanges(int[] nums) {
        List<String> ret = new ArrayList<String>();
        int i = 0; 
        int n = nums.length;        
        // 遍历整个数组
        while (i < n) {
            int low = i; // 记录当前区间的起始位置
            i++; // 移动到下一个位置开始探测连续区间          
            // 探测连续区间的结束位置
            while (i < n && nums[i] == nums[i - 1] + 1) {
                i++;
            }           
            int high = i - 1; // 确定当前区间的结束位置
            StringBuilder temp = new StringBuilder(Integer.toString(nums[low])); // 创建字符串缓冲区            
            // 判断是否为连续区间(长度大于1)
            if (low < high) {
                temp.append("->"); // 添加区间连接符
                temp.append(Integer.toString(nums[high])); // 添加区间终点
            }          
            ret.add(temp.toString()); // 将当前区间字符串加入结果列表
        }
        return ret; // 返回最终结果
    }
}

5、合并区间

56. 合并区间

image.png

class Solution {
    // 合并重叠区间并返回不重叠的区间数组
    public int[][] merge(int[][] intervals) {
        ArrayDeque<int[]> stack = new ArrayDeque<>();         
        // 按区间起始点升序排序,起始点相同时按结束点升序排序
        Arrays.sort(intervals, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            return a[0] - b[0];
        });        
        // 将第一个区间加入栈作为初始值
        stack.push(new int[]{intervals[0][0], intervals[0][1]});        
        // 遍历剩余区间进行合并处理
        for (int i = 1; i < intervals.length; i++) {
            int[] top = stack.peek(); // 获取栈顶区间            
            // 当前区间起始点大于栈顶区间结束点,说明无重叠
            if (intervals[i][0] > top[1]) {
                stack.push(new int[]{intervals[i][0], intervals[i][1]});
            } else {
                // 有重叠,合并区间(更新栈顶区间的结束点)
                top[1] = Math.max(top[1], intervals[i][1]);
            }
        }        
        // 将栈中的结果转为二维数组(
        int[][] res = new int[stack.size()][2];
        int idx = 0;
        while (!stack.isEmpty()) {
            res[idx++] = stack.pollLast(); 
        }        
        return res;
    }
}

6、三数之和

15. 三数之和

image.png

class Solution {
    // 主方法:寻找数组中所有和为0的不重复三元组
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>(); /
        Arrays.sort(nums); // 对数组进行排序(便于双指针操作和去重)
        int n = nums.length; 
        
        // 遍历数组,固定第一个数(i从0开始)
        for (int i = 0; i < n; i++) {
            // 跳过重复的元素,避免结果重复(
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            int left = i + 1; // 左指针初始化为当前元素的下一个位置
            int right = n - 1; // 右指针初始化为数组末尾
            
            // 当左指针小于右指针时执行循环
            while (left < right) {
                int target = nums[i] + nums[left] + nums[right]; // 计算当前三数之和
                
                if (target > 0) {
                    right--; // 和过大,减小右指针的值
                } else if (target < 0) {
                    left++; // 和过小,增大左指针的值
                } else {
                    // 找到符合条件的三元组,加入结果集
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums[i]);
                    temp.add(nums[left]);
                    temp.add(nums[right]);
                    res.add(temp);
                    
                    // 跳过所有与当前left值相同的元素(去重)
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    // 跳过所有与当前right值相同的元素(去重)
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    
                    // 移动指针以寻找新的组合
                    left++;
                    right--;
                }
            }
        }
        return res; // 返回最终结果
    }
}

7、K 个一组翻转链表

25.K 个一组翻转链表

image.png

image.png

class Solution {
    // 反转链表中每k个连续节点组成的子链表,不足k个的保持原样
    public ListNode reverseKGroup(ListNode head, int k) {
        // 统计链表总节点数,用于判断后续是否还有足够k个节点可反转
        int n = 0;
        for (ListNode cur = head; cur != null; cur = cur.next) {
            n++;
        }

        // 创建哑结点简化头节点处理,p0用于定位每组反转后的连接位置
        ListNode dummy = new ListNode(0, head);
        ListNode p0 = dummy;  // 前一组反转后的尾节点(初始为哑结点)
        ListNode pre = null;  // 当前组反转过程中的前驱节点
        ListNode cur = head;  // 当前处理的节点
        
        // 外层循环:按k个一组处理整个链表
        for (; n >= k; n -= k) {
            pre = null;
            // 内层循环:反转当前组的k个节点(类似反转单链表操作)
            for (int i = 0; i < k; i++) {
                ListNode nxt = cur.next; // 保存下一个待处理节点
                cur.next = pre;          // 当前节点指向前驱,实现反转
                pre = cur;               // 前驱移动到当前节点
                cur = nxt;               // 当前节点移动到下一个节点
            }
            
            // 连接反转后的组与前后部分
            ListNode nxt = p0.next;      // 保存原p0的下一个节点(即未反转的起始节点)
            p0.next.next = cur;          // 原前一组尾节点的next指向当前组的尾部(cur)
            p0.next = pre;               // 原前一组尾节点的next指向当前组的首节点(pre)
            p0 = nxt;                    // 更新p0为当前组的尾部(作为下一组的前驱)
        }
        return dummy.next; // 返回处理后的链表头
    }
}

8、数组中的第K个最大元素

215. 数组中的第K个最大元素

image.png

1. 快排

class Solution {
    // 使用随机数生成器优化pivot选择,避免最坏时间复杂度
    Random random = new Random();

    public int findKthLargest(int[] nums, int k) {
        int n = nums.length;
        // 目标位置:第k大对应升序数组中的索引为n-k
        int target = n - k;
        
        int left = 0, right = n - 1;
        while (true) {
            // 分区操作,返回基准值的最终位置
            int pivotIdx = partition(nums, left, right);
            if (pivotIdx == target) {
                return nums[pivotIdx];
            }
            // 根据基准值位置调整搜索区间
            if (pivotIdx < target) {
                left = pivotIdx + 1;
            } else {
                right = pivotIdx - 1;
            }
        }
    }

    public int partition(int[] nums, int left, int right) {
        int randIdx = left + random.nextInt(right - left + 1);
        swap(nums, left, randIdx);
        int pivot = nums[left];
        
        // 双指针扫描
        int i = left + 1;   // 从左向右找第一个大于pivot的元素
        int j = right;      // 从右向左找第一个小于pivot的元素
        
        while (true) {
            // 跳过所有小于pivot的元素
            while (i <= j && nums[i] < pivot) i++;
            // 跳过所有大于pivot的元素
            while (i <= j && nums[j] > pivot) j--;
            
            if (i >= j) break; // 指针相遇时终止
            swap(nums, i, j);  // 交换元素
            i++;
            j--;
        }
        // 将基准值放到正确位置
        swap(nums, left, j);
        return j;
    }
    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

2. 构建堆

class Solution {
    // 主函数:寻找数组中第k大的元素
    public int findKthLargest(int[] nums, int k) {
        int heapSize = nums.length; // 初始化堆大小为数组长度
        
        // 构建最大堆
        buildMaxHeap(nums, heapSize);
        
        // 进行k次操作,每次将当前最大值移动到数组尾部
        for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
            swap(nums, 0, i);          // 将堆顶最大值与当前未处理部分的最后一个元素交换
            --heapSize;                // 缩小堆的有效范围(排除已处理的元素)
            maxHeapify(nums, 0, heapSize); // 重新调整堆结构保持最大堆性质
        }
        return nums[0]; // 此时堆顶即为第k大的元素
    }

    // 构建最大堆的方法
    public void buildMaxHeap(int[] a, int heapSize) {
        // 从最后一个非叶子节点开始,向前遍历所有父节点
        for (int i = heapSize / 2 - 1; i >= 0; --i) {
            maxHeapify(a, i, heapSize); // 调整以i为根的子树为最大堆
        } 
    }

    // 维护最大堆性质的核心方法(递归实现)
    public void maxHeapify(int[] a, int i, int heapSize) {
        int l = i * 2 + 1; // 左子节点索引
        int r = i * 2 + 2; // 右子节点索引
        int largest = i;   // 默认当前节点为最大值
        
        // 比较左子节点,若更大则更新最大值索引
        if (l < heapSize && a[l] > a[largest]) {
            largest = l;
        } 
        // 比较右子节点,若更大则更新最大值索引
        if (r < heapSize && a[r] > a[largest]) {
            largest = r;
        }
        // 如果最大值不是当前节点,则交换并继续向下调整
        if (largest != i) {
            swap(a, i, largest);
            maxHeapify(a, largest, heapSize);
        }
    }

    // 数组元素交换方法
    public void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

二、项目MQ消息

1、引入MQ处理活动SKU库存一致性

《大营销平台系统设计实现》 - 营销服务 第16节:引入MQ处理活动SKU库存一致性

image.png

  1. 第一步;完成责任链的活动校验,时间、状态、库存。
  2. 第二步;对库存的扣减,使用 decr + lock 锁的方式(兜底)进行处理。
  3. 第三步;做完库存扣减后,发送延迟队列,由任务调度更新趋势库存,满足最终一致。
  4. 第四步;库存消耗为0后,发送MQ消息,驱动变更数据库库存为0

1. MQ消息发送代码

@Override
public boolean subtractionActivitySkuStock(Long sku, String cacheKey, Date endDateTime) {
    long surplus = redisService.decr(cacheKey);
    if (surplus == 0) {
        // 库存消耗没了以后,发送MQ消息,更新数据库库存
        eventPublisher.publish(activitySkuStockZeroMessageEvent.topic(), activitySkuStockZeroMessageEvent.buildEventMessage(sku));
    } else if (surplus < 0) {
        // 库存小于0,恢复为0个
        redisService.setAtomicLong(cacheKey, 0);
        return false;
    }

2. MQ消息监听代码

/​**​
 * 活动SKU库存耗尽事件消费者
 *
 * 监听RabbitMQ消息队列,当活动商品SKU库存降为0时触发以下操作:
 * 1. 记录库存清零事件日志
 * 2. 调用库存服务清空指定SKU库存
 * 3. 清除队列中的缓存数据(存在全量清除问题,待优化)
 * 
 */
@Slf4j
@Component
public class ActivitySkuStockZeroCustomer {

    // RabbitMQ消息主题配置项
    @Value("${spring.rabbitmq.topic.activity_sku_stock_zero}")
    private String topic;

    // 库存服务依赖注入
    @Resource
    private IRaffleActivitySkuStockService skuStock;

    /​**​
     * 处理库存耗尽消息的监听器
     * 
     * @param message 包含SKU ID的JSON消息体(格式:{"data":12345})
     * @throws Exception 消息处理异常(会触发RabbitMQ重试机制)
     */
    @RabbitListener(queuesToDeclare = @Queue(value = "${spring.rabbitmq.topic.activity_sku_stock_zero}"))
    public void listener(String message) {
        try {
            log.info("开始处理库存耗尽消息 | 主题={} | 消息内容={}", topic, message);
            
            // 将JSON字符串转换为EventMessage对象
            BaseEvent.EventMessage<Long> eventMessage = JSON.parseObject(
                message,
                new TypeReference<BaseEvent.EventMessage<Long>>() {}
            );
            
            // 获取被清零的SKU ID
            Long sku = eventMessage.getData();
            
            // 调用库存服务执行清零操作
            skuStock.clearActivitySkuStock(sku);
            
            // 清除队列中的相关缓存值
            skuStock.clearQueueValue();
            
            log.info("完成SKU库存清零处理 | SKU_ID={}", sku);
            
        } catch (Exception e) {
            log.error("库存清零处理失败 | 主题={} | 消息={}", topic, message, e);
            throw e; // 保持异常抛出以触发消息重试
        }
    }
}

2、写入中奖记录和任务补偿发送MQ

《大营销平台系统设计实现》 - 营销服务 第19节:写入中奖记录和任务补偿发送MQ

image.png

  1. 抽奖、抽奖策略,在前面已经实现完成,本节主要实现的是蓝色部分,写入中奖结果和任务,以及发送MQ消息。
  2. 这里我们会先简单的接收消费MQ,后续做消费奖品发放的其他处理。

从用户中奖到发奖,通常来说我们会做异步解耦,因为一些奖品的方法并不是都是在抽奖系统,而是各种 RPC/HTTP 接口来发放,这些接口有些时候会有超时的问题,需要重试处理。所以需要数据库写入一条记录,之后记录一个状态。之后奖品真正发放完以后,在更新这个这个状态。

这样可以让用户快速知道自己已中奖即可,之后点击详情或者奖品列表进入中查看自己的中奖结果。

那么这里的写入记录和发送 MQ 消息,不能用事务解决,事务主要是数据库事务,但 MQ 消息不是数据库事务。所以需要写入一个 task 表,通过任务补偿的方式进行处理。

1. MQ消息发送

MQ(消息队列)在这段代码中的作用是异步解耦核心业务与下游操作:在事务成功提交后,通过发送MQ消息触发后续业务流程(如通知、状态更新等),避免主事务因依赖外部系统而阻塞,同时通过补偿机制(更新任务状态)保障消息最终被处理,提升系统吞吐量和容错性。

@Override
public void saveUserAwardRecord(UserAwardRecordAggregate userAwardRecordAggregate) {

    // 根据用户ID进行数据库分片路由
    dbRouter.doRouter(userId);
    
    // 执行数据库事务操作
    transactionTemplate.execute(status -> {
        try {
            // 插入用户中奖记录到数据库
            userAwardRecordDao.insert(userAwardRecord);
            
            // 创建并保存抽奖任务记录
            taskDao.insert(task);
            
            // 更新用户抽奖单状态为已使用(防止重复抽奖)
            int count = userRaffleOrderDao.updateUserRaffleOrderStateUsed(userRaffleOrderReq);
            if (1 != count) {
                // 非预期更新数量时触发事务回滚
                status.setRollbackOnly();
                
                // 记录重复抽奖错误日志
                log.error("写入中奖记录,用户抽奖单已使用过,不可重复抽奖 userId: {} activityId: {} awardId: {}", userId, activityId, awardId);
                
                // 抛出自定义业务异常
                throw new AppException(ResponseCode.ACTIVITY_ORDER_ERROR.getCode(), ResponseCode.ACTIVITY_ORDER_ERROR.getInfo());
            }
            return 1; // 返回1表示事务提交
        } catch (DuplicateKeyException e) {
            // 处理唯一索引冲突异常
            status.setRollbackOnly();
            
            // 记录唯一键冲突错误日志
            log.error("写入中奖记录,唯一索引冲突 userId: {} activityId: {} awardId: {}", userId, activityId, awardId, e);
            
            // 抛出索引重复业务异常
            throw new AppException(ResponseCode.INDEX_DUP.getCode(), e);
        }
    });
    
    // 清除数据库路由配置
    dbRouter.clear();

    try {
        // 发送消息到消息队列(事务外异步执行)
        eventPublisher.publish(task.getTopic(), task.getMessage());
        
        // 更新任务表中的消息发送状态
        taskDao.updateTaskSendMessageCompleted(task);
    } catch (Exception e) {
        // 记录消息发送失败日志
        log.error("写入中奖记录,发送MQ消息失败 userId: {} topic: {}", userId, task.getTopic());
        
        // 更新任务表中的消息发送失败状态(补偿机制)
        taskDao.updateTaskSendMessageFail(task);
    }
}

2. 任务补偿

  1. 定时任务扫描数据库这里需要注意,因为我们是分库分表的,所以需要通过 dbRouter.setDBKey(finalDbIdx); 设定扫描哪个库表。
  2. 扫描库表和消息发送都使用异步线程的方式进行处理。
  3. MQ 发送可能会存在更新数据库超时的情况后,多次发送MQ消息【实际中情况很低】,但虽然有MQ多发送,不过也没关系,因为所有的操作都是有唯一ID来保证幂等的。

这段代码是基于Spring框架实现的定时任务,每5秒执行一次,主要功能是通过异步批量处理机制将消息高效可靠地发送至MQ消息队列。其核心流程包含四个环节:

  1. 分库扫描:基于分库路由策略(dbRouter)并行扫描多个数据库,通过独立线程分别查询各库中待发送的消息任务;
  2. 并发发送:利用线程池对每个库的待发任务列表进行并发处理,批量提交消息至MQ以提升吞吐量
  3. 状态闭环:根据消息发送结果(成功/失败),同步更新数据库任务状态,形成业务状态闭环
  4. 容错设计:通过try-finally确保数据库分片上下文被及时清理,防止分库状态污染

在此过程中,MQ作为异步通信中枢承担关键职能:
异步写入保障:通过非阻塞写入MQ避免同步调用下游服务的耗时问题,提升主线程效率;
流量削峰与容灾:依赖MQ的消息持久化特性,即使下游服务异常也能保证消息可靠存储;
模块解耦:生产端(任务服务)仅负责写入MQ,消费逻辑由MQ协调,实现生产消费解耦
状态一致性:结合MQ的重试机制与数据库状态更新,最终达成端到端可靠投递与数据闭环。

/**
 * MQ消息发送定时任务
 * 功能描述:通过定时任务扫描待发送消息的任务表,使用多线程并发向消息队列发送消息
 */
@Slf4j
@Component
public class SendMessageTaskJob {

    @Resource
    private ITaskService taskService;      // 任务服务接口,处理业务逻辑
    @Resource
    private ThreadPoolExecutor executor;   // 线程池,控制并发执行
    @Resource
    private IDBRouterStrategy dbRouter;    // 数据库路由策略,实现分库分表

    /**
     * 每5秒执行一次的定时任务
     * 1. 根据分库数量循环处理每个数据库
     * 2. 每个数据库独立线程查询待发送消息
     * 3. 使用线程池并发发送MQ消息
     * 4. 处理发送结果并更新状态
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void exec() {
        try {
            // 获取当前分库总数(水平分表的分库数)
            int dbCount = dbRouter.dbCount();

            // 遍历每个分库执行扫描(分库分表设计)
            for (int dbIdx = 1; dbIdx <= dbCount; dbIdx++) {
                int finalDbIdx = dbIdx;
                // 为每个分库创建独立线程处理(避免数据库间阻塞)
                executor.execute(() -> {
                    try {
                        // 设置当前数据库路由
                        dbRouter.setDBKey(finalDbIdx);
                        dbRouter.setTBKey(0);    // 表路由置0表示全表扫描
                        // 查询未发送的消息任务列表
                        List<TaskEntity> taskEntities = taskService.queryNoSendMessageTaskList();
                        
                        if (taskEntities.isEmpty()) return;  // 无任务则跳过
                        
                        // 并发发送MQ消息(线程池隔离发送过程)
                        for (TaskEntity taskEntity : taskEntities) {
                            executor.execute(() -> {
                                try {
                                    // 发送消息到MQ
                                    taskService.sendMessage(taskEntity);
                                    // 更新消息发送成功状态
                                    taskService.updateTaskSendMessageCompleted(
                                        taskEntity.getUserId(), 
                                        taskEntity.getMessageId()
                                    );
                                } catch (Exception e) {
                                    log.error("定时任务,发送MQ消息失败 userId: {} topic: {}", 
                                        taskEntity.getUserId(), 
                                        taskEntity.getTopic());
                                    // 更新消息发送失败状态(支持重试机制)
                                    taskService.updateTaskSendMessageFail(
                                        taskEntity.getUserId(), 
                                        taskEntity.getMessageId()
                                    );
                                }
                            });
                        }
                    } finally {
                        dbRouter.clear();  // 清理数据库路由上下文
                    }
                });
            }
        } catch (Exception e) {
            log.error("定时任务,扫描MQ任务表发送消息失败。", e);
        } finally {
            dbRouter.clear();  // 双重保险清理路由
        }
    }
}

3. MQ消息监听

这段代码是基于RabbitMQ消息队列实现的奖品发放通知服务组件,核心功能是通过监听指定主题的消息队列,接收并处理系统触发的用户奖品发放请求。通过@RabbitListener注解声明对spring.rabbitmq.topic.send_award队列的监听,当有消息到达时自动触发listener方法;利用@Value动态注入配置的队列名称,确保消息消费的灵活性。在方法内部,通过日志记录消息处理过程,捕获异常后重新抛出以触发RabbitMQ的消息重试机制,保障奖品发放流程的可靠性。

/​**​
 * 消息消费者服务类 - 处理用户奖品发放通知
 *
 * 该组件基于RabbitMQ实现消息监听,接收系统发送的用户奖品发放请求,
 * 并将处理结果记录到日志系统。当消息消费失败时会抛出异常触发消息重试机制。
 */
@Slf4j
@Component
public class SendAwardCustomer {

    /​**​
     * RabbitMQ交换主题名称
     */
    @Value("${spring.rabbitmq.topic.send_award}")
    private String topic;

    /​**​
     * 监听用户奖品发放消息队列
     * 
     * @param message 接收到的JSON格式消息体,包含用户ID、奖品信息等要素
     * @throws Exception 当消息处理失败时抛出异常(包含消息重试机制)
     * 
     * <p>消息处理流程:</p>
     * <ol>
     *   <li>解析JSON消息体</li>
     *   <li>校验奖品发放条件</li>
     *   <li>调用发奖服务</li>
     *   <li>记录操作流水</li>
     * </ol>
     */
    @RabbitListener(queuesToDeclare = @Queue(value = "${spring.rabbitmq.topic.send_award}"))
    public void listener(String message) {
        try {
            log.info("【消息监听】开始处理奖品发放请求 | 主题={} | 内容={}", topic, message);
            // 业务处理逻辑
        } catch (Exception e) {
            log.error("【消息消费异常】奖品发放失败,即将进行消息重试 | 主题={} | 内容={}", topic, message, e);
            throw e; // 抛出异常触发RabbitMQ消息重试机制
        }
    }
}

3、基于spring编程式事务和本地消息表实现奖品发放

核心代码:

/**
 * @author Fuzhengwei bugstack.cn 
 * @description 奖品仓储服务
 * @create 2024-04-06 10:09
 */
@Slf4j
@Component
public class AwardRepository implements IAwardRepository {

    /* 导入依赖包 */

    @Override
    public void saveUserAwardRecord(UserAwardRecordAggregate userAwardRecordAggregate) {

         /* 数据类型转换 */

        try {
            dbRouter.doRouter(userId);
            transactionTemplate.execute(status -> {
                try {
                    // 写入用户中奖记录表
                    userAwardRecordDao.insert(userAwardRecord);
                    // 写入任务发放记录表
                    taskDao.insert(task);
                    // 更新用户抽奖单表表单状态
                    int count = userRaffleOrderDao.updateUserRaffleOrderStateUsed(userRaffleOrderReq);
                    if (1 != count) {
                        status.setRollbackOnly();
                        log.error("写入中奖记录,用户抽奖单已使用过,不可重复抽奖 userId: {} activityId: {} awardId: {}", userId, activityId, awardId);
                        throw new AppException(ResponseCode.ACTIVITY_ORDER_ERROR.getCode(), ResponseCode.ACTIVITY_ORDER_ERROR.getInfo());
                    }
                    return 1;
                } catch (DuplicateKeyException e) {
                    status.setRollbackOnly();
                    log.error("写入中奖记录,唯一索引冲突 userId: {} activityId: {} awardId: {}", userId, activityId, awardId, e);
                    throw new AppException(ResponseCode.INDEX_DUP.getCode(), e);
                }
            });
        } finally {
            dbRouter.clear();
        }

        try {
            // 发送消息【在事务外执行,如果失败还有任务补偿】
            eventPublisher.publish(task.getTopic(), task.getMessage());
            // 更新数据库记录,task 任务表,表明发奖成功
            taskDao.updateTaskSendMessageCompleted(task);
        } catch (Exception e) {
            log.error("写入中奖记录,发送MQ消息失败 userId: {} topic: {}", userId, task.getTopic());
            taskDao.updateTaskSendMessageFail(task);
        }

    }

}

1. 语雀

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

image.png

image.png

image.png

三、语雀-Maven,Git面试题

1、Maven能解决什么问题?为什么要用?

✅Maven能解决什么问题?为什么要用?

image.png

2、Maven如何解决jar包冲突的问题?

✅Maven如何解决jar包冲突的问题?

image.png

3、Git如何回滚代码?reset和revert什么区别?

✅Git如何回滚代码?reset和revert什么区别?

image.png

image.png

四、语雀-场景题

1、订单到期关闭如何实现

✅订单到期关闭如何实现

image.png

1. Redisson + Redis

image.png

2、不用redis分布式锁, 如何防止用户重复点击?

✅不用redis分布式锁, 如何防止用户重复点击?

image.png

image.png

3、让你设计一个订单号生成服务,该怎么做?

✅让你设计一个订单号生成服务,该怎么做?

image.png

image.png

4、如何设计一个购物车功能?

✅如何设计一个购物车功能?

image.png

image.png

5、让你设计一个秒杀系统,你会考虑哪些问题?

✅让你设计一个秒杀系统,你会考虑哪些问题?

image.png

1. 高并发瞬时流量

image.png

image.png

2. 热点数据

image.png

3. 数据量大

image.png

4. 库存的正确扣减

image.png

5. 黄牛抢购

image.png

6. 重复下单

image.png

6、如果让你实现消息队列,会考虑哪些问题?

✅如果让你实现消息队列,会考虑哪些问题?

1. 基本架构和功能

image.png

2. 基本功能

image.png

3. 消息的可靠性保证

image.png

7、库存扣减如何避免超卖和少卖?

✅库存扣减如何避免超卖和少卖?

image.png

image.png

1. 数据库扣减

image.png

2. Redis扣减

image.png

3. 一致性保证

image.png

4. 少卖

image.png