一种基于TCP协议无边界系统延时队列自研系统的实现

概要

一种基于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
    在这里插入图片描述

在这里插入图片描述

  • 管理页面
    在这里插入图片描述

小结

敢想敢做、技术服务于业务,才能实现价值!!

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值