面试场景题:基于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 查询状态为 PENDINGFAILED 的用户,推送消息到 RocketMQ。
  4. SmsConsumerService 消费消息,调用短信发送逻辑。
  5. 发送成功更新状态为 SUCCESS,失败更新为 FAILED,下次任务从失败用户继续。

满足需求分析

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

总结

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

相关推荐
Asthenia041210 分钟前
从零开始:Dockerfile 编写与 Spring Cloud 项目部署到 Docker Compose
后端
Asthenia04121 小时前
准备面试:Jenkins部署SpringCloudAlibaba微服务商城全攻略
后端
woniu_maggie1 小时前
SAP EXCEL DOI 详解
开发语言·后端·excel
uhakadotcom1 小时前
云计算与开源工具:基础知识与实践
后端·面试·github
Asthenia04122 小时前
零基础指南:在Linux上用Docker和Jenkins实现Spring Cloud微服务的CI/CD
后端
嘵奇2 小时前
深入解析 Spring Boot 测试核心注解
java·spring boot·后端
uhakadotcom2 小时前
BPF编程入门:使用Rust监控CPU占用
后端·面试·github
uhakadotcom2 小时前
GHSL-2024-252: Cloudflare Workers SDK 环境变量注入漏洞解析
后端·面试·github
uhakadotcom2 小时前
GHSL-2024-264_GHSL-2024-265: 了解 AWS CLI 中的正则表达式拒绝服务漏洞 (ReDoS)
后端·面试·github
Asthenia04122 小时前
Feign的协议和序列化是用的什么?
后端