面试场景题:基于Redisson、RocketMQ和MyBatis的定时短信发送实现

面试场景题:基于Redisson、RocketMQ和MyBatis的定时短信发送实现

问题描述

场景题要求:有7台服务器,需要在每天早上10点定时向数据库中的用户表中的用户发送短信,满足以下条件:

  1. 消息不重复:确保每个用户只收到一条短信。
  2. 失败追踪:如果发送失败,需记录是哪个用户失败。
  3. 断点续传:下次发送时,从上一次失败的用户开始继续发送。

技术栈要求:使用 Redisson (分布式锁)、RocketMQ (消息队列)和 MyBatis(ORM)。以下是我的设计和代码实现。

解决方案

技术选型与思路

  • 定时任务 :使用 Spring 的 @Scheduled 实现每天10点的定时触发。
  • 分布式锁 :使用 Redisson 的 RLock 确保7台服务器中只有一台执行发送任务。
  • 消息队列:使用 RocketMQ 存储短信任务,解耦生产和消费,并通过消息状态追踪失败。
  • ORM:使用 MyBatis 管理数据库操作,维护用户发送状态表。

数据库表设计

sql 复制代码
CREATE TABLE message_status (
    user_id VARCHAR(50) PRIMARY KEY,
    status ENUM('PENDING', 'SENDING', 'SUCCESS', 'FAILED') DEFAULT 'PENDING',
    last_attempt_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    message_content VARCHAR(255)
);

代码实现

1. MyBatis 配置

首先配置 MyBatis,包括实体类、Mapper 接口和 XML 映射文件。

实体类:MessageStatus

java 复制代码
public class MessageStatus {
    private String userId;
    private String status;
    private Timestamp lastAttemptTime;
    private String messageContent;

    // Getters and Setters
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public Timestamp getLastAttemptTime() { return lastAttemptTime; }
    public void setLastAttemptTime(Timestamp lastAttemptTime) { this.lastAttemptTime = lastAttemptTime; }
    public String getMessageContent() { return messageContent; }
    public void setMessageContent(String messageContent) { this.messageContent = messageContent; }
}

Mapper 接口:MessageStatusMapper

java 复制代码
@Mapper
public interface MessageStatusMapper {
    @Select("SELECT * FROM message_status WHERE status IN ('PENDING', 'FAILED')")
    List<MessageStatus> findPendingOrFailed();

    @Update("UPDATE message_status SET status = #{status}, last_attempt_time = NOW() WHERE user_id = #{userId}")
    void updateStatus(@Param("userId") String userId, @Param("status") String status);
}

MyBatis 配置类

java 复制代码
@Configuration
@MapperScan("com.example.mapper") // 替换为实际包名
public class MyBatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }
}
2. 配置类(Redisson 和 RocketMQ)
java 复制代码
@Configuration
public class AppConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }

    @Bean
    public DefaultMQProducer rocketMQProducer() throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer("smsProducerGroup");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        return producer;
    }

    @Bean
    public DefaultMQPushConsumer rocketMQConsumer() throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("smsConsumerGroup");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("smsTopic", "*");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        return consumer;
    }
}
3. 定时任务与消息生产

使用 Redisson 分布式锁控制任务执行,通过 MyBatis 查询用户并推送消息到 RocketMQ。

java 复制代码
@Service
public class SmsSenderService {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private DefaultMQProducer producer;
    @Autowired
    private MessageStatusMapper messageStatusMapper;

    @Scheduled(cron = "0 0 10 * * ?") // 每天10点触发
    public void sendSmsTask() {
        RLock lock = redissonClient.getLock("smsSendLock");
        try {
            if (lock.tryLock(0, 30, TimeUnit.MINUTES)) { // 尝试获取锁,30分钟超时
                List<MessageStatus> users = messageStatusMapper.findPendingOrFailed();
                for (MessageStatus user : users) {
                    String userId = user.getUserId();
                    String content = user.getMessageContent();

                    // 更新状态为发送中
                    messageStatusMapper.updateStatus(userId, "SENDING");

                    // 发送到RocketMQ
                    Message msg = new Message("smsTopic", "smsTag", userId.getBytes(), content.getBytes());
                    producer.send(msg);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // 释放锁
            }
        }
    }
}
4. 消息消费与失败处理

使用 RocketMQ 消费者处理消息,发送短信并通过 MyBatis 更新状态。

java 复制代码
@Service
public class SmsConsumerService {

    @Autowired
    private DefaultMQPushConsumer consumer;
    @Autowired
    private MessageStatusMapper messageStatusMapper;

    @PostConstruct
    public void initConsumer() throws MQClientException {
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    String userId = new String(msg.getKeys());
                    String content = new String(msg.getBody());
                    try {
                        // 模拟发送短信
                        sendSms(userId, content);
                        // 发送成功,更新状态
                        messageStatusMapper.updateStatus(userId, "SUCCESS");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    } catch (Exception e) {
                        // 发送失败,更新状态
                        messageStatusMapper.updateStatus(userId, "FAILED");
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER; // 重试
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }

    private void sendSms(String userId, String content) {
        // 模拟短信发送,实际应调用短信服务API
        System.out.println("Sending SMS to " + userId + ": " + content);
        if (new Random().nextInt(10) < 2) { // 模拟20%失败率
            throw new RuntimeException("SMS send failed");
        }
    }
}
5. 主应用类
java 复制代码
@SpringBootApplication
@EnableScheduling
public class SmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(SmsApplication.class, args);
    }
}

运行流程

  1. 每天10点,SmsSenderService 的定时任务触发。
  2. 通过 Redisson 分布式锁确保只有一台服务器执行。
  3. 使用 MyBatis 查询状态为 PENDING 或 FAILED 的用户,推送消息到 RocketMQ。
  4. SmsConsumerService 消费消息,调用短信发送逻辑。
  5. 发送成功更新状态为 SUCCESS,失败更新为 FAILED,下次任务从失败用户继续。

满足需求分析

  • 消息不重复:Redisson 分布式锁保证单台服务器执行,MyBatis 通过主键和状态管理确保幂等性。
  • 失败追踪 :失败记录在 message_status 表的 status 和 last_attempt_time 中。
  • 断点续传 :通过 MyBatis 查询 FAILED 和 PENDING 状态实现从失败点继续。

总结

通过 Redisson、RocketMQ 和 MyBatis,我实现了一个可靠的分布式短信发送系统。Redisson 提供高效分布式锁,RocketMQ 保证消息可靠传递,MyBatis 简化数据库操作并支持状态管理,完美满足题目需求。如果用户量增加,可以通过 RocketMQ 分区扩展吞吐量。这是一个兼顾并发、可靠性和可扩展性的设计。

相关推荐
金銀銅鐵2 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路3 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x3 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn5 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈6 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780517 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123457 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒7 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记7 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598787 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端