最近公司有个需求,用户进行进行企业认证时,发送钉钉消息通知客服进行审批,系统发送钉钉消息时需要@到具体某个人。
查了下钉钉公布的api,当需要@人时,自定义群机器人发送消息可以做到这点。
但是机器人发群消息有个限制,每分钟最多发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);
}
}
}
}
}
最后,有错误或者不足之处请各位看官指正,或者有更好的解决方案也欢迎评论留言。