概要
一种基于http/https协议无边界系统延时队列自研系统的实现,主要为了满足业务系统需要延时任务的场景,使用http/https请求从而简单的可以使用系统延时队列功能,接口性能单机1000并发qps5.7W,支持集群部署、故障自动转移,定时任务采用时间轮算法,做到无边界定时任务。
应用场景:
1、工单1.0华为订单延时完单,通过http/https接入本系统,减轻了工作量;避免了定时器扫描数据库 的实现,减少了系统压力并且提高了延时订单的准确性。
2、工单1.0 app工单未完单实时统计。
整体架构流程
采用netty、hazelcast的实现,异步事件驱动的网络应用框架、多路IO复用、基于内存存储、数据同步的系统
技术名词解释
- 无边界
内存只有够大,支持的延时任务是没有上限的
技术细节
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.asd.queue</groupId>
<artifactId>asd-queue</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<jdk.version>1.8</jdk.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.12</version>
</dependency>
<!-- json模块 -->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-gson</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>5.1.7</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
</dependencies>
<build>
<finalName>asd-queue</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<!-- java8 保留参数名编译参数 -->
<compilerArgument>-parameters</compilerArgument>
<compilerArguments>
<verbose />
</compilerArguments>
</configuration>
</plugin>
<!-- jar 包中的配置文件优先级高于 config 目录下的 "同名文件" 因此,打包时需要排除掉 jar 包中来自 src/main/resources
目录的 配置文件,否则部署时 config 目录中的同名配置文件不会生效 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<excludes>
<exclude>static/**</exclude>
<exclude>templates/**</exclude>
<exclude>*.yml</exclude>
<exclude>*.properties</exclude>
<!-- <exclude>*.xml</exclude>-->
<exclude>*.txt</exclude>
</excludes>
</configuration>
</plugin>
<!-- 使用 mvn clean package 打包 更多配置可参考官司方文档:http://maven.apache.org/plugins/maven-assembly-plugin/single-mojo.html -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<!-- 打包生成的文件名 -->
<finalName>${project.artifactId}</finalName>
<!-- jar 等压缩文件在被打包进入 zip、tar.gz 时是否压缩,设置为 false 可加快打包速度 -->
<recompressZippedFiles>false</recompressZippedFiles>
<!-- 打包生成的文件是否要追加 release.xml 中定义的 id 值 -->
<appendAssemblyId>true</appendAssemblyId>
<!-- 指向打包描述文件 package.xml -->
<descriptors>
<descriptor>package.xml</descriptor>
</descriptors>
<!-- 打包结果输出的基础目录 -->
<outputDirectory>${project.build.directory}/</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>maven-releases</id>
<url>http://172.24.222.16:8081/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>maven-snapshots</id>
<url>http://172.24.222.16:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>
``
* package.xml打包配置
```xml
<assembly>
<id>release</id>
<formats>
<format>dir</format>
<format>zip</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${basedir}/src/main/resources</directory>
<outputDirectory>conf</outputDirectory>
</fileSet>
<fileSet>
<directory>${basedir}</directory>
<outputDirectory>bin/</outputDirectory>
<fileMode>755</fileMode>
<includes>
<include>*.sh</include>
<include>*.bat</include>
</includes>
</fileSet>
</fileSets>
<!-- 依赖的 jar 包 copy 到 lib 目录下 -->
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
</dependencySet>
</dependencySets>
</assembly>
- windows运行文件
@echo off & setlocal enabledelayedexpansion
title asd-project
cd %~dp0
set LIB_JARS=""
cd ..\lib
for %%i in (*) do set LIB_JARS=!LIB_JARS!;..\lib\%%i
cd ..\bin
::%1 mshta vbscript:CreateObject("WScript.Shell").Run("%~s0 ::",0,FALSE)(window.close)&&exit
java -Dapp.home=../ -Xms256m -Xmx2048m -classpath ..\conf;%LIB_JARS% com.asd.queue.Application
goto end
- mac/linux运行文件
#!/bin/bash
cd `dirname $0`
cd ..
DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/conf
LOGS_DIR=$DEPLOY_DIR/logs
APP_MAINCLASS=com.asd.queue.Application
PIDS=`ps -ef | grep -v grep | grep "$CONF_DIR" |awk '{print $2}'`
if [ -n "$PIDS" ]; then
echo "ERROR: already started!"
echo "PID: $PIDS"
exit 1
fi
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
CLOG_FILE=$LOGS_DIR/gc.log
LIB_DIR=$DEPLOY_DIR/lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'| xargs | sed "s/ /:/g"`
JAVA_OPTS=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true "
JAVA_DEBUG_OPTS=""
if [ "$1" = "debug" ]; then
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "
fi
JAVA_JMX_OPTS=""
if [ "$1" = "jmx" ]; then
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
fi
#JAVA_MEM_OPTS=""
JAVA_MEM_OPTS="-server -Xms256M -Xmx2048M -Xmn64M -Xnoclassgc -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:$CLOG_FILE"
echo -e "Starting the project...\c"
nohup java -Dapp.home=$DEPLOY_DIR $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS -classpath $CONF_DIR:$LIB_JARS $APP_MAINCLASS >/dev/null 2>&1 &
sleep 1
echo "started"
PIDS=`ps -ef | grep java | grep "$DEPLOY_DIR" | awk '{print $2}'`
echo "PID: $PIDS"
- mac/linux结束文件
#!/bin/bash
cd `dirname $0`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
LOGS_DIR=$DEPLOY_DIR/logs
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
PID=`ps -ef | grep -v grep | grep "$DEPLOY_DIR/conf" | awk '{print $2}'`
echo "PID: $PID"
if [ -z "$PID" ]; then
echo "ERROR: The project does not started!"
exit 1
fi
echo -e "Stopping project...\c"
kill $PID > $STDOUT_FILE 2>&1
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=1
PID_EXIST=`ps -f -p $PID | grep java`
if [ -n "$PID_EXIST" ]; then
COUNT=0
fi
done
echo "stopped"
echo "PID: $PID"
- application.conf配置文件
server.port = 8080
username="admin"
password="xxx"
exclude.path="/api/login,/api/test"
#用于api接口鉴权延迟值+当前年-月-日,字符串再反转,再MD5
extension.value="xxxxxx"
app.name = "asd-queue"
- logback.xml日志配置文件,实现自动按天切割日志
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="30 seconds" debug="false">
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)
</pattern>
</layout>
</appender>
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>
%contextName- %d [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>logs/info.%d.log</fileNamePattern>
</rollingPolicy>
</appender>
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>
%contextName- %d [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>logs/error.%d.log</fileNamePattern>
</rollingPolicy>
</appender>
<root level="info">
<appender-ref ref="consoleLog" />
<appender-ref ref="fileInfoLog" />
<appender-ref ref="fileErrorLog" />
</root>
</configuration>
- login.html后台管理登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
text-align: center;
}
.login-container h2 {
color: #333;
}
.login-form {
display: flex;
flex-direction: column;
margin-top: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
font-size: 14px;
margin-bottom: 5px;
display: block;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.form-group button {
background-color: #4caf50;
color: #fff;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.form-group button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="login-container">
<h2>安时达延时队列</h2>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<button onclick="login()" type="submit">登录</button>
</div>
</div>
<script src="https://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script>
function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
$.ajax({
url: '/api/login?username='+username+"&password="+password,
type: 'POST',
success: function(rep) {
console.log('rep:', rep);
if(rep.code==200){
localStorage.setItem("auth", rep.message);
window.location.href="/index";
}else {
alert(rep.message)
}
},
error: function(error) {
console.error('Error:', error);
}
});
}
</script>
</body>
</html>
- index.html后台管理登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Queue List</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
}
.list-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 90%;
text-align: center;
margin: 20px;
}
@media only screen and (max-width: 600px) {
.list-container {
width: 100%;
margin: 10px;
}
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-bar {
margin-bottom: 20px;
}
.delete-button {
cursor: pointer;
color: #e74c3c;
border: none;
background: none;
font-size: 14px;
}
</style>
</head>
<body>
<div class="list-container">
<div class="search-bar">
<label for="search">查询:</label>
<input type="text" id="search" name="search">
<button onclick="searchItems()">Search</button>
</div>
<table id="data-table">
<thead>
<tr>
<th>uuid</th>
<th>回调地址</th>
<th>数据</th>
<th>类型</th>
<th>回调时间</th>
<th>重试次数</th>
<th>重试间隔时间(毫秒)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="pagination">
<span>总页数: <span id="total-pages">0</span></span>
<span>当前页数: <span id="now-total-pages">0</span></span>
<div>
<button id="prev-page" disabled>上一页</button>
<button id="next-page">下一页</button>
</div>
</div>
</div>
<script src="https://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script>
const itemsPerPage = 15;
let currentPage = 1;
function renderTable(search) {
const tableBody = document.querySelector('#data-table tbody');
tableBody.innerHTML = '';
$.ajax({
url: 'api/list?currentPage='+currentPage+'&search='+search,
type: 'GET',
headers:{"auth": localStorage.getItem("auth")},
success: function(rep) {
console.log('rep:', rep);
let data= rep.data
for (let i = 0; i < data.length; i++) {
const row = document.createElement('tr');
let delayTime= timestampToDateTime(data[i].delayTime)
row.innerHTML = `
<td>${data[i].uuid}</td>
<td>${data[i].url}</td>
<td>${data[i].data}</td>
<td>${data[i].type}</td>
<td>${delayTime}</td>
<td>${data[i].retryNum}</td>
<td>${data[i].retryInterval}</td>
<td><button class="delete-button" οnclick="deleteItem('${data[i].uuid}')">Delete</button></td>`;
tableBody.appendChild(row);
}
updatePagination(rep.totalPages,data.length);
},
error: function(error) {
console.error('Error:', error);
window.location.href="/";
}
});
}
function updatePagination(totalPages,nowTotalPages) {
document.getElementById('total-pages').textContent = totalPages;
document.getElementById('now-total-pages').textContent = nowTotalPages;
const prevButton = document.getElementById('prev-page');
const nextButton = document.getElementById('next-page');
prevButton.disabled = currentPage === 1;
nextButton.disabled = currentPage === Math.ceil(totalPages / itemsPerPage);
}
function deleteItem(uuid) {
$.ajax({
url: 'api/remove?uuid='+uuid,
type: 'GET',
headers:{"auth": localStorage.getItem("auth")},
success: function(rep) {
console.log('rep:', rep);
if(rep.code==200){
renderTable();
}
alert(rep.message)
},
error: function(error) {
console.error('Error:', error);
}
});
}
document.getElementById('prev-page').addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderTable();
}
});
document.getElementById('next-page').addEventListener('click', () => {
let totalPages = $('#total-pages').html();
totalPages = Math.ceil(totalPages / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTable();
}
});
function searchItems() {
const searchValue = document.getElementById('search').value;
renderTable(searchValue);
}
function timestampToDateTime(timestamp) {
var date = new Date(timestamp);
var year = date.getFullYear();
var month = ('0' + (date.getMonth() + 1)).slice(-2);
var day = ('0' + date.getDate()).slice(-2);
var hours = ('0' + date.getHours()).slice(-2);
var minutes = ('0' + date.getMinutes()).slice(-2);
var seconds = ('0' + date.getSeconds()).slice(-2);
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
}
renderTable();
</script>
</body>
</html>
- Application.java启动类
package com.asd.queue;
import com.asd.queue.config.BeforeConfig;
import com.asd.queue.config.StartRunnable;
import com.asd.queue.controller.ApiController;
import io.jooby.json.GsonModule;
import static io.jooby.Jooby.runApp;
/**
* @author zhanqi
* @since 2023/12/16 15:04
*/
public class Application {
public static void main(String[] args) {
runApp(args, app -> {
app.assets("/", "public/login.html");
app.assets("/index", "public/index.html");
app.install(new GsonModule());
app.before(new BeforeConfig());
app.onStarting(new StartRunnable());
app.mvc(new ApiController());
});
}
}
- 封装三个工具类HttpResult、HttpStatus、MD5AuthUtils
package com.asd.queue.utils;
import lombok.Data;
import java.util.ArrayList;
/**
* HTTP结果封装
*
* @author zhanqi
* @since 2020-07-15
*/
@Data
public class HttpResult<T> {
private int code = 200;
private String message;
private T data;
public static <T> HttpResult<T> error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static <T> HttpResult<T> error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static <T> HttpResult<T> error(int code, String msg) {
HttpResult r = new HttpResult();
r.setCode(code);
r.setMessage(msg);
return r;
}
public static <T> HttpResult<T> ok(String msg) {
HttpResult r = new HttpResult();
r.setMessage(msg);
r.setData(new ArrayList<>());
return r;
}
public static <T> HttpResult<T> ok(T data) {
HttpResult r = new HttpResult();
r.setMessage("");
r.setData(data);
return r;
}
public static <T> HttpResult<T> ok(String msg,T data) {
HttpResult r = new HttpResult();
r.setMessage(msg);
r.setData(data);
return r;
}
/**
* 返回值
* @param msg
* @param code
* @param data
* @param <T>
* @return
*/
public static <T> HttpResult<T> back(String msg,int code,T data) {
HttpResult r = new HttpResult();
r.setMessage(msg);
r.setCode(code);
r.setData(data);
return r;
}
/**
* 返回值
* @param msg
* @param code
* @param data
* @param <T>
* @return
*/
public static <T> HttpResult<T> back(String msg,boolean code,T data) {
HttpResult r = new HttpResult();
r.setMessage(msg);
r.setCode(code?0:1);
r.setData(data);
return r;
}
public static <T> HttpResult<T> ok() {
return new HttpResult();
}
}
package com.asd.queue.utils;
public interface HttpStatus {
// --- 1xx Informational ---
/**
* {@code 100 Continue} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_CONTINUE = 100;
/**
* {@code 101 Switching Protocols} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_SWITCHING_PROTOCOLS = 101;
/**
* {@code 102 Processing} (WebDAV - RFC 2518)
*/
public static final int SC_PROCESSING = 102;
// --- 2xx Success ---
/**
* {@code 200 OK} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_OK = 200;
/**
* {@code 201 Created} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_CREATED = 201;
/**
* {@code 202 Accepted} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_ACCEPTED = 202;
/**
* {@code 203 Non Authoritative Information} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
/**
* {@code 204 No Content} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_NO_CONTENT = 204;
/**
* {@code 205 Reset Content} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_RESET_CONTENT = 205;
/**
* {@code 206 Partial Content} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_PARTIAL_CONTENT = 206;
/**
* {@code 207 Multi-Status} (WebDAV - RFC 2518)
* or
* {@code 207 Partial Update OK} (HTTP/1.1 - draft-ietf-http-v11-spec-rev-01?)
*/
public static final int SC_MULTI_STATUS = 207;
// --- 3xx Redirection ---
/**
* {@code 300 Mutliple Choices} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_MULTIPLE_CHOICES = 300;
/**
* {@code 301 Moved Permanently} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_MOVED_PERMANENTLY = 301;
/**
* {@code 302 Moved Temporarily} (Sometimes {@code Found}) (HTTP/1.0 - RFC 1945)
*/
public static final int SC_MOVED_TEMPORARILY = 302;
/**
* {@code 303 See Other} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_SEE_OTHER = 303;
/**
* {@code 304 Not Modified} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_NOT_MODIFIED = 304;
/**
* {@code 305 Use Proxy} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_USE_PROXY = 305;
/**
* {@code 307 Temporary Redirect} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_TEMPORARY_REDIRECT = 307;
// --- 4xx Client Error ---
/**
* {@code 400 Bad Request} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_BAD_REQUEST = 400;
/**
* {@code 401 Unauthorized} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_UNAUTHORIZED = 401;
/**
* {@code 402 Payment Required} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_PAYMENT_REQUIRED = 402;
/**
* {@code 403 Forbidden} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_FORBIDDEN = 403;
/**
* {@code 404 Not Found} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_NOT_FOUND = 404;
/**
* {@code 405 Method Not Allowed} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_METHOD_NOT_ALLOWED = 405;
/**
* {@code 406 Not Acceptable} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_NOT_ACCEPTABLE = 406;
/**
* {@code 407 Proxy Authentication Required} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
/**
* {@code 408 Request Timeout} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_REQUEST_TIMEOUT = 408;
/**
* {@code 409 Conflict} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_CONFLICT = 409;
/**
* {@code 410 Gone} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_GONE = 410;
/**
* {@code 411 Length Required} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_LENGTH_REQUIRED = 411;
/**
* {@code 412 Precondition Failed} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_PRECONDITION_FAILED = 412;
/**
* {@code 413 Request Entity Too Large} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_REQUEST_TOO_LONG = 413;
/**
* {@code 414 Request-URI Too Long} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_REQUEST_URI_TOO_LONG = 414;
/**
* {@code 415 Unsupported Media Type} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
/**
* {@code 416 Requested Range Not Satisfiable} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
/**
* {@code 417 Expectation Failed} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_EXPECTATION_FAILED = 417;
/**
* Static constant for a 418 error.
* {@code 418 Unprocessable Entity} (WebDAV drafts?)
* or {@code 418 Reauthentication Required} (HTTP/1.1 drafts?)
*/
// not used
// public static final int SC_UNPROCESSABLE_ENTITY = 418;
/**
* Static constant for a 419 error.
* {@code 419 Insufficient Space on Resource}
* (WebDAV - draft-ietf-webdav-protocol-05?)
* or {@code 419 Proxy Reauthentication Required}
* (HTTP/1.1 drafts?)
*/
public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
/**
* Static constant for a 420 error.
* {@code 420 Method Failure}
* (WebDAV - draft-ietf-webdav-protocol-05?)
*/
public static final int SC_METHOD_FAILURE = 420;
/**
* {@code 422 Unprocessable Entity} (WebDAV - RFC 2518)
*/
public static final int SC_UNPROCESSABLE_ENTITY = 422;
/**
* {@code 423 Locked} (WebDAV - RFC 2518)
*/
public static final int SC_LOCKED = 423;
/**
* {@code 424 Failed Dependency} (WebDAV - RFC 2518)
*/
public static final int SC_FAILED_DEPENDENCY = 424;
// --- 5xx Server Error ---
/**
* {@code 500 Server Error} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_INTERNAL_SERVER_ERROR = 500;
/**
* {@code 501 Not Implemented} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_NOT_IMPLEMENTED = 501;
/**
* {@code 502 Bad Gateway} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_BAD_GATEWAY = 502;
/**
* {@code 503 Service Unavailable} (HTTP/1.0 - RFC 1945)
*/
public static final int SC_SERVICE_UNAVAILABLE = 503;
/**
* {@code 504 Gateway Timeout} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_GATEWAY_TIMEOUT = 504;
/**
* {@code 505 HTTP Version Not Supported} (HTTP/1.1 - RFC 2616)
*/
public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
/**
* {@code 507 Insufficient Storage} (WebDAV - RFC 2518)
*/
public static final int SC_INSUFFICIENT_STORAGE = 507;
}
package com.asd.queue.utils;
import cn.hutool.crypto.digest.DigestUtil;
import com.asd.queue.service.DelayQueueService;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* @author zhanqi
* @since 2023/12/18 9:43
*/
public class MD5AuthUtils {
private static Config config = ConfigFactory.load();
public static List<String> getMd5Hash() {
String extension_value = config.getString("extension.value");
List<String> md5Hashes = new ArrayList<>();
LocalDate currentDate = LocalDate.now();
LocalDate nextDay = currentDate.plusDays(1);
LocalDate previousDay = currentDate.minusDays(1);
md5Hashes.add(calculateMd5Hash(extension_value, currentDate));
md5Hashes.add(calculateMd5Hash(extension_value, nextDay));
md5Hashes.add(calculateMd5Hash(extension_value, previousDay));
String token = DelayQueueService.configMap.get("token");
if(token!=null){
md5Hashes.add(token);
}
return md5Hashes;
}
private static String calculateMd5Hash(String inputString, LocalDate day) {
String combinedString = inputString + day;
String reversedString = reverseString(combinedString);
return DigestUtil.md5Hex(reversedString);
}
private static String reverseString(String str) {
return new StringBuilder(str).reverse().toString();
}
public static void main(String[] args) {
System.out.println(getMd5Hash());
}
}
- 两个实体类Message、QueueAddBean
package com.asd.queue.bean;
import java.io.Serializable;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author zhanqi
* @since 2023/12/15 16:11
*/
public class Message implements Delayed, Serializable {
private static final long serialVersionUID = 1580175879713296211l;
//uuid
private String uuid;
//回调接口地址
private String url;
private String data;
private String type;
private Integer retryNum;
private Long retryInterval;
private Long delayTime;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Integer getRetryNum() {
return retryNum;
}
public void setRetryNum(Integer retryNum) {
this.retryNum = retryNum;
}
public Long getRetryInterval() {
return retryInterval;
}
public void setRetryInterval(Long retryInterval) {
this.retryInterval = retryInterval;
}
public Long getDelayTime() {
return delayTime;
}
public void setDelayTime(Long delayTime) {
this.delayTime = delayTime;
}
public Message(String uuid, String url, Long delayTime, String data, String type, Integer retryNum,Long retryInterval) {
this.uuid = uuid;
this.url = url;
this.delayTime = System.currentTimeMillis() + delayTime;
this.data = data;
this.type = type;
this.retryNum = retryNum;
this.retryInterval = retryInterval;
}
@Override
public long getDelay(TimeUnit unit) {
long diff = delayTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if (this.delayTime < ((Message) o).delayTime) {
return -1;
} else if (this.delayTime > ((Message) o).delayTime) {
return 1;
}
return 0;
}
@Override
public String toString() {
return "Message{" +
"uuid='" + uuid + '\'' +
", url='" + url + '\'' +
", data='" + data + '\'' +
", type='" + type + '\'' +
", retryNum=" + retryNum +
", retryInterval=" + retryInterval +
", delayTime=" + delayTime +
'}';
}
}
package com.asd.queue.bean;
import lombok.Data;
/**
* @author zhanqi
* @since 2023/12/16 13:47
*/
@Data
public class QueueAddBean {
private String url;
private String data;
private String type;
private Long delayTime;
private Integer retryNum=0;
private Long retryInterval=5000l;
}
- 创建接口鉴权配置BeforeConfig
package com.asd.queue.config;
import com.asd.queue.utils.MD5AuthUtils;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.jooby.Context;
import io.jooby.Route;
import io.jooby.StatusCode;
import io.jooby.Value;
import javax.annotation.Nonnull;
import java.util.Arrays;
/**
* @author zhanqi
* @since 2023/12/18 9:10
*/
public class BeforeConfig implements Route.Before{
private Config config = ConfigFactory.load();
@Override
public void apply(@Nonnull Context ctx) {
String auth;
try {
Value val = ctx.header("auth");
auth= val.value();
}catch (Exception e){
auth="";
}
if(!Arrays.asList(config.getString("exclude.path").split(",")).contains(ctx.getRequestPath())){
if(!MD5AuthUtils.getMd5Hash().contains(auth)){
ctx.sendError(new RuntimeException("鉴权失败"), StatusCode.UNAUTHORIZED);
return;
}
}
}
@Nonnull
@Override
public Route.Before then(@Nonnull Route.Before next) {
return Route.Before.super.then(next);
}
@Nonnull
@Override
public Route.Handler then(@Nonnull Route.Handler next) {
return Route.Before.super.then(next);
}
}
- DelayQueueService队列方法
package com.asd.queue.service;
import com.asd.queue.bean.Message;
import com.asd.queue.bean.QueueAddBean;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import java.util.UUID;
import java.util.concurrent.DelayQueue;
public class DelayQueueService {
public static final String MASTER_NAME = "master";
public static Config config = new Config();
public static HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
public static IMap<String, String> configMap = hazelcastInstance.getMap("config");
public static IMap<String, Integer> deadLetterMap = hazelcastInstance.getMap("dead_letter");
public static IMap<String, Message> messageMap = hazelcastInstance.getMap("delayedMessages");
public static DelayQueue<Message> delayQueue = new DelayQueue<>();
/**
* 新增任务
* @param queueAddBean
* @return
*/
public static Message add(QueueAddBean queueAddBean) {
String uuid = UUID.randomUUID().toString();
Message message = new Message(uuid, queueAddBean.getUrl(), queueAddBean.getDelayTime(), queueAddBean.getData(), queueAddBean.getType(),queueAddBean.getRetryNum(),queueAddBean.getRetryInterval());
delayQueue.put(message);
messageMap.put(uuid, message);
return message;
}
/**
* 删除任务
* @param uuid
*/
public static void remove(String uuid) {
Message message= messageMap.get(uuid);
delayQueue.remove(message);
messageMap.remove(message.getUuid());
}
}
- 实现DelayQueue队列
package com.asd.queue.config;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.asd.queue.bean.Message;
import com.asd.queue.service.DelayQueueService;
import com.asd.queue.utils.HttpResult;
import com.asd.queue.utils.HttpStatus;
import com.hazelcast.cluster.Cluster;
import com.hazelcast.cluster.Member;
import com.hazelcast.cluster.MembershipEvent;
import com.hazelcast.cluster.MembershipListener;
import com.hazelcast.map.IMap;
import io.jooby.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zhanqi
* @since 2023/12/16 15:22
*/
@Slf4j
public class StartRunnable implements SneakyThrows.Runnable {
@Override
public void tryRun() {
Cluster cluster = DelayQueueService.hazelcastInstance.getCluster();
processDelayedMessages(cluster, DelayQueueService.configMap, DelayQueueService.messageMap, DelayQueueService.delayQueue);
cluster.addMembershipListener(new MembershipListener() {
@Override
public void memberAdded(MembershipEvent membershipEvent) {
System.out.println(membershipEvent);
}
@Override
public void memberRemoved(MembershipEvent membershipEvent) {
if (!cluster.getMembers().contains(DelayQueueService.configMap.get(DelayQueueService.MASTER_NAME))) {
processDelayedMessages(cluster, DelayQueueService.configMap, DelayQueueService.messageMap, DelayQueueService.delayQueue);
}
}
});
}
@Override
public void run() {
SneakyThrows.Runnable.super.run();
}
/**
* 选举
*
* @param clusterMembers
* @return
*/
public String clusterMembersVote(Set<Member> clusterMembers) {
List<Member> hosts = new ArrayList<>(clusterMembers);
Collections.sort(hosts, (p1, p2) -> {
Integer p1Host = Integer.valueOf(p1.getAddress().getHost().replaceAll("\\.", ""));
Integer p2Host = Integer.valueOf(p2.getAddress().getHost().replaceAll("\\.", ""));
return p2Host - p1Host;
});
System.out.println(hosts);
return hosts.get(0).getUuid().toString();
}
/**
* 执行队列
*
* @param cluster
* @param masterMap
* @param messageMap
* @param delayQueue
*/
public void processDelayedMessages(Cluster cluster, IMap<String, String> masterMap, IMap<String, Message> messageMap, DelayQueue<Message> delayQueue) {
Set<Member> clusterMembers = cluster.getMembers();
Member localMember = cluster.getLocalMember();
String voteClusterUuid = clusterMembersVote(clusterMembers);
String localMemberUuid = localMember.getUuid().toString();
if (localMemberUuid.equals(voteClusterUuid)) {
masterMap.put(DelayQueueService.MASTER_NAME, localMember.getUuid().toString());
for (String uuid : messageMap.keySet()) {
Message message = messageMap.get(uuid);
delayQueue.put(message);
}
//ExecutorService cachedThreadPool = Executors.newFixedThreadPool(100);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
new Thread(() -> {
while (!Thread.interrupted()) {
try {
Message message = delayQueue.take();
cachedThreadPool.execute(() -> processMessage(message, messageMap, delayQueue));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}).start();
}
}
private void processMessage(Message message, IMap<String, Message> messageMap, DelayQueue<Message> delayQueue) {
try {
HttpResponse httpResponse = HttpRequest.post(message.getUrl())
.body(message.getData())
.setReadTimeout(1000 * 5)
.execute();
if (httpResponse.getStatus() == 200) {
messageMap.remove(message.getUuid());
DelayQueueService.deadLetterMap.remove(message.getUuid());
log.error("执行成功!! 对象:{}", message);
} else {
throw new RuntimeException("请求异常!! status:" + httpResponse.getStatus() + " body:" + httpResponse.body());
}
} catch (Exception e) {
log.error("执行异常:{} 对象:{}", e.getMessage(), message);
Integer retryNum = DelayQueueService.deadLetterMap.get(message.getUuid());
retryNum = retryNum == null ? 1 : retryNum + 1;
if (message.getRetryNum() - retryNum > 0) {
message.setRetryNum(retryNum-1);
message.setDelayTime(System.currentTimeMillis() +message.getRetryInterval());
delayQueue.add(message);
messageMap.put(message.getUuid(),message);
DelayQueueService.deadLetterMap.put(message.getUuid(), retryNum);
} else {
messageMap.remove(message.getUuid());
}
}
}
}
- 实现API接口ApiController
package com.asd.queue.controller;
import com.asd.queue.bean.Message;
import com.asd.queue.bean.QueueAddBean;
import com.asd.queue.service.DelayQueueService;
import com.asd.queue.utils.HttpResult;
import com.hazelcast.map.IMap;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.jooby.annotations.GET;
import io.jooby.annotations.POST;
import io.jooby.annotations.Path;
import io.jooby.annotations.QueryParam;
import java.util.*;
/**
* @author zhanqi
* @since 2023/12/16 15:05
*/
@Path("/api")
public class ApiController {
private Config config = ConfigFactory.load();
@POST("/login")
public HttpResult login(@QueryParam String username, @QueryParam String password) {
String usernameConfig = config.getString("username");
String passwordConfig = config.getString("password");
if((username+password).equals(usernameConfig+passwordConfig)){
String token = UUID.randomUUID().toString();
DelayQueueService.configMap.put("token", token);
return HttpResult.ok(token);
}
return HttpResult.error("用户名或者密码错误");
}
@GET("/list")
public Map<String, Object> list(@QueryParam Integer currentPage, @QueryParam String search) {
Map<String, Object> map = new HashMap<>();
List<Message> list = new ArrayList<>();
IMap<String, Message> messageMap = DelayQueueService.messageMap;
if(!"undefined".equals(search)&&!"".equals(search)){
list.add(messageMap.get(search));
map.put("totalPages",list.size());
map.put("data",list);
return map;
}
list = getPageData(messageMap, currentPage, 15);
map.put("totalPages",messageMap.size());
map.put("data",list);
return map;
}
@POST("/add")
public HttpResult add(QueueAddBean queueAddBean) {
Message message=DelayQueueService.add(queueAddBean);
return HttpResult.ok("添加成功",message);
}
@GET("/remove")
public HttpResult remove(@QueryParam String uuid) {
DelayQueueService.remove(uuid);
return HttpResult.ok("移除成功");
}
@POST("/test")
public HttpResult remove() {
return HttpResult.ok("");
}
/**
* 分页
* @param data
* @param currentPage
* @param pageSize
* @return
*/
private static List<Message> getPageData(IMap<String, Message> data, int currentPage, int pageSize) {
int startIndex = (currentPage - 1) * pageSize;
int endIndex = startIndex + pageSize;
List<Message> list = new ArrayList<>();
int i = 0;
for (Map.Entry<String, Message> entry : data.entrySet()) {
if (i >= startIndex && i < endIndex) {
list.add(entry.getValue());
}
i++;
}
return list;
}
}
- API
- 管理页面
小结
敢想敢做、技术服务于业务,才能实现价值!!