最近公司有个需求,用户进行进行企业认证时,发送钉钉消息通知客服进行审批,系统发送钉钉消息时需要@到具体某个人。

查了下钉钉公布的api,当需要@人时,自定义群机器人发送消息可以做到这点。

java通过钉钉发送通知 发送钉钉消息接口_java通过钉钉发送通知

但是机器人发群消息有个限制,每分钟最多发20条,超过将无法发送出去。因此,需要对系统消息队列进行延时处理,控制在每4s发送一条消息。

具体整理后的技术点是:客户认证消息或许没有,或许源源不断的发来,而且是分布式,多台机,如果当前只有一条消息,直接发送出去,如果当前很多条消息,需要让消息进入队列,消息每4s发送出去。

but,怎么延时呢?!有人说,这还不简单,定时器呗!对不起,试过定时调度在分布式多线程调度时不太可行,很容易出问题,难以控制。或许本人水平有限,暂时没有想到用定时器很好的解决方案。

也有人说用mq,消息队列,试过后发现达不到分布式串行消息队列延时发送的效果。或许了解不深,没有找到用mq实现方案。

说下我的实现方案:

由于只有一台redis,因此可以设置redis锁来实现此业务场景:在redis里设置一把锁,设置锁的失效时间为4s,每个消息发送线程发送前都需要来先获取这把锁,如果锁有效,延时处理,时长与锁失效时间一致,延时时间到,再次获取这把锁,若还是有效,继续延时,若锁失效进行消息发送。分布式多线程情况下,谁先抢到这把锁,谁先发送。以此实现了多线程延时4s发送消息的功能。最后,上代码:

package com.lianj.dingtalk.center.impl;

import com.lianj.common.jms.QueueMessageProducer;
import com.lianj.commons.exception.BizException;
import com.lianj.commons.util.AssertUtil;
import com.lianj.commons.utils.JSONUtils;
import com.lianj.dingtalk.center.commons.config.Global;
import com.lianj.dingtalk.center.commons.utils.DingtalkUtil;
import com.lianj.dingtalk.center.commons.utils.StringUtils;
import com.lianj.dingtalk.center.dao.DingRobotDao;
import com.lianj.dingtalk.center.dao.po.DingRobotPo;
import com.lianj.dingtalk.center.helper.MessageHelper;
import com.lianj.dingtalk.center.helper.ThreadHelper;
import com.lianj.dingtalk.center.service.IDingRobotService;
import com.lianj.dingtalk.center.service.IDingtalkAssignAdviseConfigService;
import com.lianj.dingtalk.center.service.bo.*;
import com.lianj.framework.cache.impl.RedisCacheService;
import com.lianj.framework.db.base.commons.service.BaseService;
import com.lianj.framework.db.base.commons.utils.BeanMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.List;
import java.util.MissingFormatArgumentException;
import java.util.concurrent.TimeUnit;

/**
 * 功能:DingRobot -实现业务接口
 * 作者:laihuawei(laihuawei@lianj.com)
 */
@Service("dingRobotService")
public class DingRobotServiceImpl extends BaseService<DingRobotPo, DingRobotBo> implements IDingRobotService<DingRobotPo, DingRobotBo> {

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

    @Autowired
    private RedisCacheService redisCacheService;

    @Autowired
    private IDingtalkAssignAdviseConfigService dingtalkAssignAdviseConfigService;

    @Autowired
    private QueueMessageProducer dingtalkMessageProducer;


    @Autowired
    private DingRobotDao dao;

    @Override
    protected DingRobotDao getDao() {
        return dao;
    }

    public static final Integer expireTime = 4;// 消息发送间隔时间(单位:秒)


    @Override
    public DingRobotBo getOneBy(DingRobotBo dingRobotBo) {
        DingRobotQueryParam dingRobotQueryParam = new DingRobotQueryParam();
        dingRobotQueryParam.setDingRobot(dingRobotBo);
        DingRobotPo dingRobotPo = dao.findUniqueBy(dingRobotQueryParam);
        if (dingRobotPo != null) {
            return BeanMapper.map(dingRobotPo, DingRobotBo.class);
        }
        return null;
    }

    @Override
    public List<DingRobotBo> getListBy(DingRobotBo dingRobotBo) {
        DingRobotQueryParam dingRobotQueryParam = new DingRobotQueryParam();
        dingRobotQueryParam.setDingRobot(dingRobotBo);
        List<DingRobotPo> dingRobotPoList = dao.findBy(dingRobotQueryParam);
        if (dingRobotPoList != null && dingRobotPoList.size() > 0) {
            return BeanMapper.mapList(dingRobotPoList, DingRobotBo.class);
        }
        return null;
    }

    /**
     * 根据groupKey查询配置信息
     *
     * @param robotKey
     * @return
     */
    @Override
    public DingRobotBo findByRobotKey(String robotKey) {
        String cacheKey = DingtalkUtil.getDingRobotCacheKey(robotKey);
        String boStr = redisCacheService.get(cacheKey);
        DingRobotBo bo = null;
        if (StringUtils.isBlank(boStr)) {
            DingRobotPo po = dao.findByRobotKey(robotKey, getEnv(), "Y");
            if (po != null) {
                bo = BeanMapper.map(po, DingRobotBo.class);
                redisCacheService.put(cacheKey, bo);
            } else {
                redisCacheService.put(cacheKey, new DingRobotBo(), 10, TimeUnit.MINUTES);
            }
        } else {
            bo = JSONUtils.jsonToObj(boStr, DingRobotBo.class);
        }
        return bo;
    }

    @Override
    public void refreshCache(String robotKey) {
        String cacheKey = DingtalkUtil.getDingRobotCacheKey(robotKey);
        redisCacheService.delete(cacheKey);
        findByRobotKey(robotKey);
    }

    private String getEnv() {
        return Global.getConfig("dubbo.registry.group", "dev");
    }

    /**
     * 推送自定义机器人消息到群组(由于官方限制,每4s发送一条)
     *
     * @param message
     * @throws BizException
     */
    @Override
    public void sendRobotMsgToGroup(RobotMessage message) throws BizException {
        try {
            Assert.notNull(message, "参数缺失!");
            if (StringUtils.isBlank(message.getRobotKey())) {
                throw new BizException("robotKey参数缺失");
            }
            if (StringUtils.isBlank(message.getMsgtype())) {
                throw new BizException("msgtype参数缺失");
            }
            if (StringUtils.isBlank(message.getContent())) {
                throw new BizException("content消息内容为空");
            }

            // dingtalkMessageProducer.sendMessage(JSONUtils.toJson(message), 4000);// MQ 延时4s发送

            String robotKey = message.getRobotKey();
            DingRobotBo bo = this.findByRobotKey(robotKey);
            if (bo == null || StringUtils.isBlank(bo.getWebhook())) {
                throw new BizException("机器人webhook地址为空,请检查数据库是否正确配置");
            }

            sendAsyncMessage(robotKey, message);
        } catch (Exception e) {
            logger.error("发送机器人消息失败!", e);
            throw e;
        }
    }

    @Override
    public void sendRobotMsgToGroup(RobotMessageEx message) throws BizException {
        try {
            AssertUtil.isNotNull(message, "消息参数不能为空");
            AssertUtil.isTrue(StringUtils.isNotBlank(message.getContent())||StringUtils.isNotBlank(message.getAtText()), "消息内容不能为空!");
            RobotMessage messages = BeanMapper.map(message, RobotMessage.class);
            messages.setRobotKey(message.getRobotKey());
            messages.setAtAll(message.getAtAll() == null ? false : message.getAtAll());
            messages.setMsgtype(StringUtils.isNotBlank(message.getMsgtype()) ? message.getMsgtype() : "text");
            String messageContent = message.getContent();
            String phone = null;
            if (!messages.getAtAll()) {
                phone = dingtalkAssignAdviseConfigService.getAtPhoneNumber(message.getRobotMsgType().getBizType());
                if (StringUtils.isNoneBlank(phone)) {
                    List<String> phones = new ArrayList<>(1);
                    phones.add(phone);
                    messages.setAtMobiles(phones);
                }
            }
            if (StringUtils.isNotBlank(message.getAtText()))
                messageContent = String.format(message.getAtText(), StringUtils.isNotBlank(phone) ? "@" + phone : "");
            messages.setContent(messageContent);
            sendRobotMsgToGroup(messages);
        } catch (MissingFormatArgumentException e) {
            logger.error("发送机器人消息格式化失败!", e);
            throw new BizException("发送机器人消息信息格式化失败!");
        } catch (Exception e) {
            logger.error("发送机器人消息失败!", e);
            throw e;
        }
    }

    /**
     * 发送 异步消息
     *
     * @param robotKey
     * @param message
     */
    private void sendAsyncMessage(final String robotKey, final RobotMessage message) {
        ThreadHelper.THREAD_POOL.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    if (waitLock(robotKey)) {
                        DingRobotBo bo = findByRobotKey(robotKey);
                        MessageHelper.sendRobotMessage(bo.getWebhook(), message);// 发送消息
                        logger.info("延时发送机器人消息:", message);
                    }
                } catch (Exception e) {
                    logger.error("发送异步消息失败!", e);
                }
            }
        });
    }

    /**
     * 获取锁发送消息
     *
     * @param robotKey
     */
    private synchronized Boolean waitLock(String robotKey) {
        Long lock = getLock(robotKey);// 获取redis锁
        if (lock == null) {
            throw new BizException("系统异常");
        } else if (lock == 0) {// 锁可用
            setLock(robotKey);
            return true;
        } else if (lock > 0) {// 锁不可用
            try {
                Thread.sleep(lock);// 线程睡眠
                waitLock(robotKey);
            } catch (InterruptedException e) {
                throw new BizException("系统异常");
            }
        } else {
            throw new BizException("系统异常");
        }

        return true;
    }

    /**
     * 根据robotKey获取redis锁(线程同步)
     *
     * @param robotKey
     * @return null-锁不可用,抛出数据异常;0-锁可用; long-大于0,锁不可用,返回等待时间(毫秒)
     */
    private synchronized Long getLock(String robotKey) {
        if (StringUtils.isNotEmpty(robotKey)) {
            String lockKey = DingtalkUtil.getDingRobotLockCacheKey(robotKey);
            if (StringUtils.isNotBlank(lockKey)) {
                String lockBoStr = redisCacheService.get(lockKey);
                RobotRedisLockBo lockBo = null;
                if (StringUtils.isBlank(lockBoStr)) {
                    return 0L;
                } else {
                    lockBo = JSONUtils.jsonToObj(lockBoStr, RobotRedisLockBo.class);
                    Integer lock = lockBo.getLock();// 锁标志位(0-关锁;1-开锁)
                    if (lock == 1) {
                        return 0L;
                    } else if (lock == 0) {// 关锁状态,取出等待时间
                        Long time = lockBo.getTime();
                        if (time != null && (System.currentTimeMillis() - time) > 0) {
                            return System.currentTimeMillis() - time;
                        }
                    } else {
                        return null;
                    }
                }
            }
        }

        return null;
    }

    /**
     * 上锁
     *
     * @param robotKey
     */
    private synchronized void setLock(String robotKey) {
        if (StringUtils.isNotEmpty(robotKey)) {
            String lockKey = DingtalkUtil.getDingRobotLockCacheKey(robotKey);
            if (StringUtils.isNotBlank(lockKey)) {
                String lockBoStr = redisCacheService.get(lockKey);
                RobotRedisLockBo lockBo = null;
                if (StringUtils.isBlank(lockBoStr)) {
                    lockBo = new RobotRedisLockBo();
                    lockBo.setLock(0);// 锁标志位(0-关锁;1-开锁)
                    lockBo.setRobotKey(robotKey);
                    lockBo.setTime(System.currentTimeMillis());
                    lockBo.setExpire(expireTime);
                    redisCacheService.put(lockKey, lockBo, expireTime, TimeUnit.SECONDS);// 设置超时时间
                } else {
                    redisCacheService.delete(lockKey);
                    setLock(robotKey);
                }
            }
        }
    }


}

 

最后,有错误或者不足之处请各位看官指正,或者有更好的解决方案也欢迎评论留言。