背景
随着系统用户越来越多,业务流程越来越完善,但同时也暴露出了一些问题,比如,关键的流程节点用户比较关注,可能会涉及到时效性,用户只有登录到系统才能看到流程阶段信息。为了解决流程关键节信息差,时效性差等问题,项目决定对接短信。
业务模块:目前项目业务模块分为三个部分,以后有可能会增加其他模块。
- AA 模块
- BB 模块
- CC 模块
简单流程图
表设计
- sms_config:短信配置表,用于存储短信提供方模板标识和占位符参数获取方法
- sms:短信表,用于存储短信内容相关信息
- sms_scan:扫描表,用户定时任务拉取短信表中的数据,然后放到队列中,用完删除
- sms_receiver:短信接收者
- sms_errorarray:不同短信发送情况
- 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();
}
}