一、力扣
1、MySQL每月交易
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、最长递增子序列
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、最长连续序列
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、汇总区间
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、合并区间
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、三数之和
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 个一组翻转链表
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个最大元素
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库存一致性
- 第一步;完成责任链的活动校验,时间、状态、库存。
- 第二步;对库存的扣减,使用 decr + lock 锁的方式(兜底)进行处理。
- 第三步;做完库存扣减后,发送延迟队列,由任务调度更新趋势库存,满足最终一致。
- 第四步;库存消耗为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
- 抽奖、抽奖策略,在前面已经实现完成,本节主要实现的是蓝色部分,写入中奖结果和任务,以及发送MQ消息。
- 这里我们会先简单的接收消费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. 任务补偿
- 定时任务扫描数据库这里需要注意,因为我们是分库分表的,所以需要通过 dbRouter.setDBKey(finalDbIdx); 设定扫描哪个库表。
- 扫描库表和消息发送都使用异步线程的方式进行处理。
- MQ 发送可能会存在更新数据库超时的情况后,多次发送MQ消息【实际中情况很低】,但虽然有MQ多发送,不过也没关系,因为所有的操作都是有唯一ID来保证幂等的。
这段代码是基于Spring框架实现的定时任务,每5秒执行一次,主要功能是通过异步批量处理机制将消息高效可靠地发送至MQ消息队列。其核心流程包含四个环节:
- 分库扫描:基于分库路由策略(dbRouter)并行扫描多个数据库,通过独立线程分别查询各库中待发送的消息任务;
- 并发发送:利用线程池对每个库的待发任务列表进行并发处理,批量提交消息至MQ以提升吞吐量;
- 状态闭环:根据消息发送结果(成功/失败),同步更新数据库任务状态,形成业务状态闭环;
- 容错设计:通过
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);
}
}
}