多级缓存实现消息投递(短信发送)

4 篇文章 0 订阅

多级缓存实现消息投递(短信发送)

文章背景

接产品需求,要在项目中实现代扣失败后短信的发送,但从这一点来讲比较容易实现,代扣失败直接发短信就是了,但是需求难实现主要是在于场景的复杂性,目前代扣的场景包括,实时代扣、查证、回调三个部分;每天代扣次数不定,目前是两次。

要求

支持当天只能发一条失败短信通知,对于一个客户多个绑定卡的情况,只发送给最优卡绑定的手机;

支持后续开放对多次批扣,多次短信发送的扩展需求,可以通过开关实现。

设计

方案一:数据入库 代扣失败入库待发送的消息,成功修改数据状态,当最终为失败时,批量短信发送该消息。
方案二:缓存+数据库,缓存中存放待发送的标志,数据库作为相应数据信息发送的来源,采用分页实现。
方案三:缓存,数据存储、数据过滤,全部采用缓存实现。

最终方案

由于项目每天多次批扣,还有宽限期等,所以,本次设计完全采用缓存实现,即方案三。

代码设计

首先根据现有的业务逻辑,分析代扣出现的几种情况,实时批扣、代扣查询、代扣回调等。

如果想要在原来代码的基础之上做分情况的处理,一是代码耦合性比较高,二是可读性比较差,不方便扩展和问题定位,所有在处理方面采用公共处理的方式。

代码实现

缓存工具
public class Redis {

    private volatile static Redis instance;

    private static JedisCluster jedisCluster;

    private Redis() {
        jedisCluster = SpringContext.getBean("jedisCluster");
    }

    public static Redis getInstance() {
        if (instance == null) {
            synchronized (Redis.class) {
                if (instance == null) {
                    instance = new Redis();
                }
            }
        }
        return instance;
    }


    public JedisCluster getJedis() {
        return jedisCluster;
    }

    public void close(JedisCluster jedis) {

    }

    public String get(String key) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.get(key);
        } finally {
            close(jedis);
        }
    }

    public String set(String key, String value) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.set(key, value);
        } finally {
            close(jedis);
        }
    }

    public String setex(String key, int seconds, String value) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.setex(key, seconds, value);
        } finally {
            close(jedis);
        }
    }

    public Long expire(String key, int seconds) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.expire(key, seconds);
        } finally {
            close(jedis);
        }
    }

    public Long del(String key) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.del(key);
        } finally {
            close(jedis);
        }
    }

    public Long sadd(String key, String... members) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.sadd(key, members);
        } finally {
            close(jedis);
        }
    }

    public Long scard(String key) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.scard(key);
        } finally {
            close(jedis);
        }
    }

    public Set<String> smembers(String key) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.smembers(key);
        } finally {
            close(jedis);
        }
    }

    public Long hset(String key, String field, String value) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.hset(key, field, value);
        } finally {
            close(jedis);
        }
    }

    public String hget(String key, String field) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.hget(key, field);
        } finally {
            close(jedis);
        }
    }

    public Long hdel(String key, String... fields) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.hdel(key, fields);
        } finally {
            close(jedis);
        }
    }

    public Map<String, String> hgetAll(String key) {
        JedisCluster jedis = getJedis();
        try {
            return jedis.hgetAll(key);
        } finally {
            close(jedis);
        }
    }

    public String spop(String key) {

        JedisCluster jedis = getJedis();
        try {
            return jedis.spop(key);
        } finally {
            close(jedis);
        }
    }

    public <T> void setList(String key, List<T> list) {
        this.setexList(key, 0, list);
    }


    public <T> void setexList(String key, int second, List<T> list) {
        JedisCluster jedis = getJedis();
        try {
            second = 0 == second ? 当天剩余时间 : second;
            jedis.setex(key, second, JSON.toJSONString(list));
        } finally {
            close(jedis);
        }
    }

    public <T> List<T> getList(String key) {
        JedisCluster jedis = getJedis();
        try {
            String stringLists = jedis.get(key);
            return  (List<T>) JSON.parse(stringLists);
        } finally {
            close(jedis);
        }
    }

    public void setMap(String key, Map<String,Object> map) {
        this.setexMap(key, 0, map);
    }

    public void setexMap(String key, int second, Map<String,Object> map) {
        JedisCluster jedis = getJedis();
        try {
            second = 0 == second ? Helper.getCurrentDate() + Helper.ONE_DAY - Helper.getCurrentTime() : second;
            jedis.setex(key, second, JSON.toJSONString(map));
        } finally {
            close(jedis);
        }
    }

    public Map<String,Object> getMap(String key) {
        JedisCluster jedis = getJedis();
        try {
            return  (Map<String,Object>) JSON.parse(jedis.get(key));
        } finally {
            close(jedis);
        }
    }
}
设计缓存一

采用Map+List实现,在Map中过滤数据信息,在List中存取需代扣的数据信息,最后遍历List取出所有数据,进行短信发送处理。

数据保存
private List<Object> judgeCustStatusAndSave(CustDetail detail, Map<String, Object> redisMap, List<Object> objectList) {
    // 如果当前客户缓存为空,RedisKey为一个枚举类
    Map<String, Object> cacheMap = Redis.getInstance().getMap(RedisKey.CUST_SEND_MSG.getKey() + detail.getCustomerId());
    if (null == cacheMap){
        installParam(detail, cacheMap);
    }
    // 缓存中是失败,当前数据是成功时更新状态信息
    if (CustDetailStatus.FAILED.getCode() == (Integer) cacheMap.get("status") &&
        CustDetailStatus.SUCCESS.getCode() == detail.getStatus() ){
        // 更新状态为成功
        installParam(detail, cacheMap);
        if (null == objectList){
            objectList = new ArrayList<>();
            objectList.add(cacheMap);
        }else {
            AtomicBoolean exits = new AtomicBoolean();
            // 当代扣失败后又存在代扣成功的数据时,删除list缓存中代扣信息,并修改
            objectList.forEach(o -> {
                Map<String, Object> objectMap = (Map<String, Object>) o;
                // 判断缓存中是否存在该客户信息
                if (detail.getCustomerId().equals(objectMap.get("customerId"))){
                    // 缓存中是失败,当前数据是成功时更新状态信息
                    installParam(detail, objectMap);
                    exits.set(true);
                }
            });
            // 当缓存中不存在时
            if (!exits.get()){
                objectList.add(cacheMap);
            }
        }
        // 将该客户信息放入缓存
        Redis.getInstance().setMap(RedisKey.CUST_SEND_MSG.getKey() + detail.getCustomerId(), cacheMap);

    }
    if (null == objectList){
        objectList = new ArrayList<>();
        objectList.add(redisMap);
    }else {
        AtomicBoolean exits = new AtomicBoolean();
        objectList.forEach(o -> {
            Map<String, Object> objectMap = (Map<String, Object>) o;
            // 判断缓存中是否存在该客户信息
            if (detail.getCustomerId().equals(objectMap.get("customerId"))){
                // 缓存中是失败,当前数据是成功时更新状态信息
                if (CustDetailStatus.FAILED.getCode() == (Integer) objectMap.get("status") &&
                    CustDetailStatus.SUCCESS.getCode() == detail.getStatus() ){
                    installParam(detail, objectMap);
                }
                exits.set(true);
            }
        });
        // 缓存中不存在客户信息时添加
        if (!exits.get()){
            Map<String, Object> redisMaps = new HashMap<>(4);
            installParam(detail, redisMaps);
            objectList.add(redisMap);
        }
    }
    return objectList;
}

private void installParam(CustDetail detail, Map<String, Object> redisMap) {
    redisMap.put("customerId",detail.getCustomerId());
    redisMap.put("phone", detail.getPhone());
    redisMap.put("msgContent", detail.getResultMsg());
    redisMap.put("status", detail.getStatus());
}
消息推送
@Service
public class CustMsgSendJob implements SimpleJobProcessor {

    private static final Logger logger = LoggerFactory.getLogger(CustMsgSendJob.class);

    @Autowired
    private CustDetailService custDetailService;

    @Override
    public void process(JobContext context) throws Exception {
        logger.info("Start Task:" + this.getClass().getSimpleName() + " -> " + context.getParameter());
        String sendMsgKey = RedisKey.CUST_SEND_MSG.getKey() + Helper.getCurrentDate();
        AtomicInteger count = new AtomicInteger();
        RedisLock lock = new RedisLock((RedisKey.CUST_SEND_MSG.getKey());
                                       lock.wrap(() -> {
                                           List<Object> msgList = Redis.getInstance().getList(sendMsgKey);
                                           msgList.forEach(msgObj -> {
                                               try {
                                                   Map<String, Object> msgInitMap = (Map<String, Object>) msgObj;
                                                   if (CustDetailStatus.FAILED.getCode() == (Integer) msgInitMap.get("status")) {
                                                       // 短信发送成功增加条数统计,具体的发送信息的逻辑
                                                       if (msgService.installMsgMapAndSendMsg(msgInitMap)) {
                                                           count.getAndIncrement();
                                                       }
                                                   }
                                               } catch (Exception e) {
                                                   logger.error("代扣失败发送短信处理异常,redisKey->{}, 异常信息->{}", sendMsgKey, e);
                                               }
                                           });
                                           logger.info("Finish Task:" + this.getClass().getSimpleName() + " -> " + count);
                                       });
                                       logger.info("Finish Task:" + this.getClass().getSimpleName() + " -> " + count);

                                       }
                                       }
设计缓存二

由于采用List会在删除元素和最后发送处理的时候遍历List,当数据量大的时候会出现性能问题,所以需要优化缓存处理。

数据保存
public void cacheOfDeal(CustDetail detail) {
    try {
        // 拼接customer key
        String stringKey = getCustomerKey(detail,"string");
        String mapKey = getCustomerKey(detail, "map");
        String setKey = getCustomerKey(detail,"set");
        // 判断当前客户代扣状态,进行相应处理
        judgeAndSaveCustInfo(detail, stringKey, mapKey, setKey);
    } catch (Exception e) {
        logger.error("客户号->{}, 执行代扣缓存待处理短信异常->{}",detail.getCustomerId(), e);
        DingDingUtil.send(true, this.getClass().getSimpleName() + detail.getCustomerId(), "缓存代扣失败待处理短信异常");
    }
}
public void judgeStatusAndSaveCustInfo(CustDetail detail, String stringKey, String mapKey, String setKey) {
    Redis redis = Redis.getInstance();
    String custStatus = redis.get(stringKey);
    if (StringUtils.isEmpty(custStatus)) {
        // 初次遍历当前客户代扣数据时,添加代扣失败的数据到Map和Set中
        if (CustDetailStatus.FAILED.getCode() == detail.getStatus()) { // 如果首次失败
            redis.setex(stringKey, getRemainSeconds(),
                        String.valueOf(CustDetailStatus.FAILED.getCode()));
            String cacheParam = installParam(detail);
            redis.hset(mapKey, stringKey, cacheParam);
            redis.sadd(setKey, stringKey);
        }else if (CustDetailStatus.SUCCESS.getCode() == detail.getStatus()){ // 如果首次成功
            redis.setex(stringKey, getRemainSeconds(),
                        String.valueOf(CustDetailStatus.SUCCESS.getCode()));
        }
    }else {  // 缓存不为空
        // 当且仅当缓存中是失败,当前代扣为成功时执行Map删除操作,更新string value
        if (String.valueOf(CustDetailStatus.FAILED.getCode()).equals(custStatus) &&
            CustDetailStatus.SUCCESS.getCode() == detail.getStatus()){
            redis.hdel(mapKey, stringKey);
            redis.setex(stringKey, getRemainSeconds(),
                        String.valueOf(CustDetailStatus.SUCCESS.getCode()));
        }
    }
}

/**
     * 组装消息发送参数
     * @param detail
     */
public String installParam (CustDetail detail){
    Map<String, Object> map = new HashMap<>();
    map.put("customerId",detail.getCustomerId());
    map.put("phone", detail.getPhone());
    map.put("msgContent", detail.getResultMsg());
    map.put("status", String.valueOf(detail.getStatus()));
    return JSON.toJSONString(map);
}

/**
    * 获取当天剩余时间,需手动计算
    * @return
    */
private int getRemainSeconds() {
    // TODO
    return 1;
}

/**
     * 获取Redis缓存中的key
     * @param detail
     * @return
     */
private String getCustomerKey(CustDetail detail, String key) {

    String customerKey = RedisKey.CUST_MSG.getKey() + detail.getCustomerId();
    switch (key) {
        case "string":
            return customerKey;
        case "map":
            return RedisKey.CUST_SEND_MSG.getKey() + "_map";
        case "set":
            return RedisKey.CUST_SEND_MSG.getKey() + "_set";
        default:
            return RedisKey.CUST_SEND_MSG.getKey();
    }
}
消息推送
// Job处理
public Result<Integer> querysendMsg() {
    AtomicInteger integer = new AtomicInteger();
    Redis redis = Redis.getInstance();
    // 获取缓存中的数据信息
    String setKey = RedisKey.CUST_SEND_MSG.getKey() + "_set";
    String mapKey = RedisKey.CUST_SEND_MSG.getKey() + "_map";
    try {
        RedisLock lock = new RedisLock(RedisKey.CUST_SEND_MSG.getKey() + this.getClass());
        lock.wrap(() -> {
            String stringKey = redis.spop(setKey);
            // 当 stringKey不为 null 时处理
            while (null != stringKey){
                if (StringUtils.isNotEmpty(stringKey)) {
                    String StringMap = redis.hget(mapKey, stringKey);
                    // map不为空代表有代扣数据
                    if (StringUtils.isNotEmpty(StringMap)) {
                        try {
                            Map<String, Object> retMap = JSON.parse(StringMap, Map.class);
                            // 具体发短信、保存信息的处理逻辑
                            if (installMsgMapAndSendMsg(retMap)) {
                                // 发送完成删除map中的缓存
                                redis.hdel(mapKey, stringKey);
                                integer.getAndIncrement();
                            }
                        } catch (ParseException e) {
                            logger.error("客户key->{}, 数据转换异常->{}", stringKey, e);
                        }
                    }
                }
                // 进行下次循环
                stringKey = redis.spop(setKey);
            }
        });
    } catch (Exception e) {
        logger.error("代扣短信发送异常 mapKey->{}, 数据转换异常->{}", mapKey, e);
    }
    return new Result<>(integer.intValue());
}

比较两种缓存设计

第一种设计比较简单,很容易想象到和理解,但是由于轮训等原因不利于大数据量处理,影响性能;

第二种设计通过增加一种缓存和改变轮训为spop方式,提高处理的效率,设计更为复杂,但是可靠性更强。

更多优秀文章

代码已经在GitHub中更新,更多详情可关注dwyanewede

JDK动态代理实现原理
https://blog.csdn.net/shang_xs/article/details/92772437
java界的小学生
https://blog.csdn.net/shang_xs

公众号推荐

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
错误信息是出现在Java代码中,表示尝试调用一个虚拟方法时出错。该方法是Object类的getClass()方法,用于返回当前对象的运行时类型。当尝试调用一个对象的方法时,首先会去检查该对象是否为null,然后再执行对应的方法。然而在这个错误中,所调用的对象为null,导致无法执行getClass()方法。 出现这个错误的原因可能有多种,常见的有: 1. 对象为null:在调用一个对象的方法之前,应该先确保该对象不为null,否则会出现NullPointerException错误。 2. 方法名错误:检查调用方法的名称是否正确,确保没有拼写错误。 3. 对象类型错误:检查该对象是否确实拥有被调用方法。例如,如果定义一个父类对象,而尝试调用子类中新增的方法,就会出现该错误。 4. 引入错误的包:如果错误的导入了错误的包或类,也可能导致找不到相应的方法而出错。 为了解决这个错误,我们可以采取以下措施: 1. 检查对象是否为null,并确保对象的有效性。 2. 检查方法名是否正确,确保没有拼写错误。 3. 检查对象的类型,确保被调用的方法与对象的类型匹配。 4. 检查导入的包和类的正确性,确保没有导入错误的包或类。 总之,当出现"attempt to invoke virtual method 'java.lang.class java.lang.object.getclass()"错误时,需要仔细检查代码中调用方法的对象是否为null或者存在其他问题,以确保方法的正确调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值