记录短信模块系统设计,采用redis队列(SMS)

11 篇文章 0 订阅

背景

随着系统用户越来越多,业务流程越来越完善,但同时也暴露出了一些问题,比如,关键的流程节点用户比较关注,可能会涉及到时效性,用户只有登录到系统才能看到流程阶段信息。为了解决流程关键节信息差,时效性差等问题,项目决定对接短信。

业务模块:目前项目业务模块分为三个部分,以后有可能会增加其他模块。

  1. AA 模块
  2. BB 模块
  3. CC 模块

简单流程图
在这里插入图片描述

表设计

  1. sms_config:短信配置表,用于存储短信提供方模板标识和占位符参数获取方法
  2. sms:短信表,用于存储短信内容相关信息
  3. sms_scan:扫描表,用户定时任务拉取短信表中的数据,然后放到队列中,用完删除
  4. sms_receiver:短信接收者
  5. sms_errorarray:不同短信发送情况
  6. sms_fail:失败记录表

在这里插入图片描述

类设计

流程节点枚举类 StageProcessorEnums

流程节点枚举是个非常重要的扩展类,可以横向扩展,也可以纵向扩展

public enum StageProcessorEnums {

    A_SH_PASS("1010","通过",1010),
    A_SH_REJECT("1020","补充材料",1020),
    A_SH_NOPASS("1030","不通过",1030),
    A_PAYMENT("1040","交费",1040),
    A_CD("1050","裁定",1050),
    B_SH_PASS("2010","审核通过",2010),
    B_SH_NOPASS("2020","审核不通过",2020),
    B_PAYMENT("2030","交费",2030),
    B_CH("2040","出函",2040),
    C_SH_REJECT("3010","退回",3010),
    C_CD("3020","裁定",3020);

    private String code;
    private String name;
    private int intValue;
    StageProcessorEnums(String code,String name,int intValue){
        this.code = code;
        this.name = name;
        this.intValue = intValue;
    }
    public String getCode() {
        return code;
    }
    public String getValue() {
        return name;
    }
    public int getIntCode() {
        return intValue;
    }
}
流程节点后置处理器StagePostProcessor

流程节点处理器接口,定义规范,目前短信实现了这个接口,如果有记录日志等其他业务也可以实现这个接口

public interface StagePostProcessor {
    void applyPostProcessor(Object obj, StageProcessorEnums stageProcessorEnums);
}
短信流程处理器 SmsPostProcessor

用于封装短信内容入库

public class SmsPostProcessor implements StagePostProcessor {

    @Autowired
    private RedisUtils redisUtils;
    @Override
    public void applyPostProcessor(Object obj, StageProcessorEnums stageProcessorEnums) {
        if(!smsConfig.isEnable()){
            return ;
        }
        // 配置表主键是StageProcessorEnums的code值
        SmsConfig smsConfig = (SmsConfig)redisUtils.get(String.format(CacheCons.CACHE_KEY_ALL_SMS_SEND,stageProcessorEnums.getCode()));
        if(Objects.isNull(template)){
            return ;
        }
        Date time = DateUtils.getDate();
        SmsWrap smsWrap = new SmsWrap();
        Sms sms = new Sms();
        SmsReceiver smsReceiver = new SmsReceiver();
        smsWrap.setSms(sms).setSmsReceiver(smsReceiver).setSmsConfig(smsConfig);
        // 处理信心公共部分
        // ...
        //对不同类型的短信内容填充
        SmsContentStrategy.populate(obj,smsWrap);
        smsMapper.insert(sms);
        smsReceiverMapper.insert(smsReceiver);
        smsScanMapper.insert(smsScan);
    }
}
分发委托类 StagePostProcessorDelegate

分发委托类用于通知不同类型的后置处理器,这里目前只有短信。这样可以在同一个阶段中处理不同类型业务,扩展性比较好,不需要修改原来的代码。

@Component
public class StagePostProcessorDelegate {
    public void invokeStagePostProcessors(Object obj, StageProcessorEnums stageProcessorEnums) {
    	// 获取StagePostProcessor类型的bean实例
        List<StagePostProcessor> list = ApplicationContextHelper.getBeanListOfType(StagePostProcessor.class);
        //通知
        list.forEach(stagePostProcessor->stagePostProcessor.applyPostProcessor(obj,stageProcessorEnums));
    }
}
短信内容策略类SmsContentStrategy

用于对不同类型的信息请求分发,时间复杂度O1,拒绝大量的if else 判断

public final class SmsContentStrategy{
    private final static Map<Class, SmsContentProcessor> singletonFactories = Maps.newHashMap();
    static {
        List<SmsContentProcessor> list = ApplicationContextHelper.getBeanListOfType(SmsContentProcessor.class);
        // SmsContentProcessor接口有两个关键的方法生命
        // 1、getTargetTypeClass() 处理器类型
        // 2、process 对应类型的处理器
        list.forEach(process->singletonFactories.put(process.getTargetTypeClass(),process));
    }
    private SmsStrategy(){}
    public static void populate (Object obj, SmsWrap smsWrap){
    	//根据对象传递类型,获取对应的短信内容处理器进行填充属性
        singletonFactories.get(obj.getClass()).populateSms(obj,smsWrap);
    }
}
短信内容处理器定义 SmsContentProcessor

用于约束类型和行为

public interface SmsContentProcessor {
    String SPLIT_COMMA = ",";
   	//获取当前处理器的类型
    Class getTargetTypeClass();
	//填充消息内容
    void populateSms(Object target, SmsWrap smsWrap);
    //转换代码
    default String codeConvert(String code){
        if(StringUtils.isNotEmpty(code)){
            int length = code.length();
            switch (length){
                case 1:
                    code = "000" + code;
                    break;
                case 2:
                    code = "00" + code;
                    break;
                case 3:
                    code = "0" + code;
                    break;
            }
            return code;
        }
        return "0000";
    }
	//占位符参数替换
	//methods:是在smsConfig中配置,占位符对应解析的方法
	//比如:我是{0},在{1},工作{2}时间    get0,get1,get2方法相对应,不同的占位符个数对应上就可以
    @SneakyThrows
    default String parameterListConvert(Object obj,String methods){
        String[] split = methods.split(SPLIT_COMMA);
        int length;
        if((length = split.length) < Code.YES_CODE){
            return null;
        }
        StringBuilder sb = new StringBuilder();
        Class clazz = obj.getClass();
        for(int i = 0;i < length;i++){
            Method method = clazz.getMethod(split[i]);
            method.setAccessible(true);
            Object res = method.invoke(obj);
            if(i != length - Code.YES_CODE){
                sb.append(res.toString());
                sb.append(SPLIT_COMMA);
            }else{
                sb.append(res.toString());
            }
        }
        return sb.toString();
    }
}
抽象类型定义 AbstractTypeDefinition

定义抽象类型,用于策略模式的获取SmsContentProcessor对应的处理bean

public abstract class AbstractTypeDefinition {
	//定义处理器类型
    public abstract Class getTargetTypeClass();
}
A类型定义 ATypeDefinition
public class ATypeDefinition extends AbstractTypeDefinition{
    private static final Class CLASS_TYPE = A.class;
    @Override
    public Class getTargetTypeClass() {
        return CLASS_TYPE;
    }
}
处理A类型的消息私有信息 ASmsContentProcessor
@Component
public class ASmsContentProcessor extends BTypeDefinition implements SmsContentProcessor {
    @Override
    public void populateSms(Object target, SmsWrap smsWrap) {
        A a = (A)target;
        Sms sms = smsWrap.getSms();
        sms.setCode(codeConvert(a.getCode()));
        //填充私有类型数据
        //...
        //处理占位符
      	sms.setParameters(this.parameterListConvert(a,smsWrap.getSmsConfig().getcPlaceholderMethods()));
    }
}
B类型定义 BTypeDefinition
public class BTypeDefinition extends AbstractTypeDefinition{
    private static final Class CLASS_TYPE = B.class;
    @Override
    public Class getTargetTypeClass() {
        return CLASS_TYPE;
    }
}
处理B类型的消息私有信息 ASmsContentProcessor
@Component
public class BSmsContentProcessor extends BTypeDefinition implements SmsContentProcessor{

    @Override
    public void populateSms(Object target, SmsWrap smsWrap) {
        B b = (B)target;
        Sms sms = smsWrap.getSms();
        sms.setCode(codeConvert(b.getCode()));
        //填充私有类型数据
        //...
        //处理占位符
      	sms.setParameters(this.parameterListConvert(b,smsWrap.getSmsConfig().getcPlaceholderMethods()));
    }
}
C类型定义 CTypeDefinition
public class CTypeDefinition extends AbstractTypeDefinition{
    private static final Class CLASS_TYPE = C.class;
    @Override
    public Class getTargetTypeClass() {
        return CLASS_TYPE;
    }
}
处理C类型的消息私有信息 CSmsContentProcessor
@Component
public class CSmsContentProcessor extends CTypeDefinition implements SmsContentProcessor{

    @Override
    public void populateSms(Object target, SmsWrap smsWrap) {
        C c = (C)target;
        Sms sms = smsWrap.getSms();
        sms.setCode(codeConvert(c.getCode()));
        //填充私有类型数据
        //...
        //处理占位符
      	sms.setParameters(this.parameterListConvert(c,smsWrap.getSmsConfig().getcPlaceholderMethods()));
    }
}

定时任务

定时任务的作用是生产和消费消息

定时任务1 消息入队

前面说消息有个扫描表,就是入队的时候使用的,通过关联sms_scan表,这里需要注意下,扫描的时候不建议使用表锁,这样数据库压力太大了,咱们使用redis分布式锁,Redission给咱们已经提供了工具,这里上锁的时间比较短,按照redis每秒1W的并发量计算,锁定时间不足1S,毕竟数据量没有那么大,如果数据量特别大,建议使用MQ

@Override
public void pushQueue() {
    RLock lock = redisson.getLock(PUSH_QUEUE_LOCK);
    if(Objects.isNull(lock)){
        return ;
    }
    try {
        lock.lock();
        List<SmsContent> list = smsMapper.getXxzxBoListBySmsScan(Code.YES_CODE,SmsEnum.SMS_SEND_ZT_DFS.getIntCode());
        if(CollectionUtils.isNotEmpty(list)){
            //这里必须要标识成功的,不然失败了的情况不好处理
            List<String> success = new ArrayList<>();
            try {
                for (SmsContent sms : list){
                    redisUtils.rpush(SMS_WORK_QUEUE, sms);
                    success.add(sms.getId());
                }
            }finally {
                // 这里有可优化点:如果这时数据库宕机,并且已经被消费的数据未能及时更新信息状态,会造成重复消费的问题
                // 既然redis没有宕机,可以使用redis缓存把入队的数据存储起来,然后过滤掉已经入队数据,更新状态失败的数据
                // 有失败的情况,失败了的等着下次轮询
                smsService.batchUpdataStageByList(success,SmsEnum.SMS_SEND_ZT_YZB.getIntCode());
                smsService.deleteSmsScanByList(success);
            }
        }
    }finally {
        lock.unlock();
    }
}
定时任务2 发送短信

就是从workQueue中拿到数据进行发送,发送失败就不进行了处理了,直接把数据扔到retryQueue中,retryQueue负责重试任务

public void sendHandler() {
    long listSize = redisUtils.listSize(SMS_WORK_QUEUE);
    if(listSize == Code.NO_CODE){
        return ;
    }
    List<String> list = new ArrayList<>();
    List<SmsErrorArray> errorArrays = new ArrayList<>();
    int size = smsConfig.getQuantity();
    SmsContent smsContent;
    try{
        while(size > 0){
            smsContent = (SmsContent)redisUtils.lpop(SMS_WORK_QUEUE);
            //队列中没有数据了
            if(Objects.isNull(smsContent)){
                break;
            }
            smsContent.setSendTime(DateUtils.getDate());
            Res result = this.send(smsConfig.getSendurl(), JsonUtil.toJson(smsContent));
            if(Objects.isNull(result) || !StringUtils.equals(result.getCode(),SmsEnum.SMS_RESULT_SUCCESS.getCode())){
                //失败了重新放到队列(重试队列)
                smsContent.setSmsFail(createSmsFail(smsContent.getId(),result));
                redisUtils.rpush(SMS_RETRY_QUEUE,smsContent);
            }else{
                // 发送成功
                list.add(smsContent.getId());
                //记录成功errorArray
                errorArrays.addAll(analysisRes(result,smsContent.getId()));
            }
            size--;
        }
    }finally {
        //发送结束,设置短信状态为已发送(这里没必要完全保证事务一致性,多维度控制,防止短信重复发送,也避免了长事务)
        smsService.batchUpdataStageByList(list,SmsEnum.SMS_SEND_ZT_YFS.getIntCode());
        smsService.batchInsertSmsErrorArray(errorArrays);
    }
}
定时任务3 重试

把定时任务2发送失败的短信进行重试,可能有人问,为什么不在任务2中重试呢,刚开始的想法是先分层,主任务和失败的任务分开,失败的有可能失败的概率也不算小,这样避免了因为一个蜗牛车大家都在后面排队等着的情况。

public void retryQueue() {
	long size = redisUtils.listSize(SMS_RETRY_QUEUE);
	if(size <= Code.NO_CODE){
	    return ;
	}
	List<String> success = new ArrayList<>();
	List<SmsErrorArray> errorArrays = new ArrayList<>();
	SmsContent smsContent;
	try {
	    while(size > 0){
	    	size--;
	        smsContent = (SmsContent)redisUtils.lpop(SMS_RETRY_QUEUE);
	        if(Objects.isNull(smsContent)){
	            //队列里没有数据了
	            break;
	        }
	        //未开启重试,直接放到死信队列
	        if(smsContent.getRetry() == smsConfig.getRetrynum()){
	            redisUtils.rpush(SMS_DEADLETTER_QUEUE,smsContent);
	        }
	        smsContent.setRetry(smsContent.getRetry()+Code.YES_CODE);
	        Res result = this.send(smsConfig.getSendurl(), JsonUtil.toJson(smsContent));
	        if(Objects.isNull(result)){
	            //失败了重新放到队列(重试队列)
	            redisUtils.rpush(SMS_RETRY_QUEUE,smsContent);
	            continue;
	        }
	        //有响应结果
	        if(StringUtils.equals(result.getCode(),SmsEnum.SMS_RESULT_SUCCESS.getCode())){
	            success.add(smsContent.getId());
	            errorArrays.addAll(analysisRes(result,smsContent.getId()));
	        }else {
	            smsContent.getSmsFailJlb().setcSbyy(resultJson);
	            smsContent.getSmsFail().setUpdatetime(DateUtils.getDate());
	            if(可能是对方原因重试1){
	                // 可能是对方原因重试
	                redisUtils.rpush(SMS_MF_RETRY_QUEUE,smsContent);
	            }else if(可能是对方原因重试2){
	                // 可能是对方原因重试
	                redisUtils.rpush(SMS_MF_RETRY_QUEUE,smsContent);
	            }else{
	                // 自己原因比较大(自己的原因直接放弃,放到死信队列,但是这种可能性很小)
	                redisUtils.rpush(SMS_DEADLETTER_QUEUE,smsContent);
	            }
	        }
	    }
	}finally {
	    smsService.batchUpdataZtByList(success,SmsEnum.SMS_SEND_ZT_YFS.getIntCode());
	    smsService.batchInsertSmsErrorArray(errorArrays);
	}
}
定时任务4 更新发送失败的信息

这个任务就是把死信队列的数据更新到数据库

public void giveUp() {
    long size = redisUtils.listSize(SMS_DEADLETTER_QUEUE);
    if(size == Code.NO_CODE){
        return ;
    }
    RLock lock = redisson.getLock(SMS_DEADLETTER_LOCK);
    if(Objects.isNull(lock)){
        return ;
    }
    try {
        lock.lock();
	    List<Object> list = redisUtils.lGet(SMS_DEADLETTER_QUEUE, Code.NO_CODE, -1);
	    List<SmsFail> failList = new ArrayList<>();
	    List<String> failIds = new ArrayList<>();
	    SmsContent smsContent;
	    for(Object o : list){
	        smsContent = (SmsContent)o;
	        failList.add(smsContent.getSmsFail());
	        failIds.add(smsContent.getId());
	    }
	    smsService.batchUpdataZtByList(failIds,SmsEnum.SMS_SEND_ZT_FAIL.getIntCode());
	    smsService.batchInsertSmsFail(failList);
	    redisUtils.del(SMS_DEADLETTER_QUEUE);
    }finally{
		lock.unlock();
	}
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java抽奖系统后台使用Spring Boot、MyBatis和Redis队列可以有效地处理高并发情况。 首先,Spring Boot提供了一个轻量级的开发框架,可以简化Java后台开发的流程。它内置了Tomcat服务器,提供了自动配置和快速构建的功能,可快速搭建开发环境。此外,Spring Boot还具有良好的扩展性和灵活性,可以方便地集成其他框架和技术。 MyBatis是一款优秀的持久层框架,可以大幅简化数据库操作的代码。它提供了灵活的SQL映射配置,可以通过注解或XML编写SQL语句,同时也支持动态SQL。MyBatis还支持多种数据库连接池,能够提高数据库连接的效率和并发处理能力。 Redis是一款高性能的内存数据库,可作为缓存或消息队列使用。在抽奖系统中,可以将中奖结果存储在Redis中,以提高中奖查询的性能。此外,Redis还提供了发布-订阅(Publish-Subscribe)机制,可用于实现消息队列。当用户进行抽奖时,可以将请求放入Redis队列中,后台程序可以通过订阅该队列来处理请求,实现并发处理。 使用Redis队列处理高并发可以有效地降低系统的负载和响应时间。通过将请求放入队列中,可以使请求在后台异步处理,减少前端请求等待的时间。同时,通过控制队列的长度和处理速度,还可以防止系统负载过高。 综上所述,Java抽奖系统后台使用Spring Boot、MyBatis和Redis队列可以实现高并发的处理能力,提高抽奖系统的性能和可扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值