动态线程池+kafka自定义拒绝策略,做到任务不丢失

动态线程池+kafka自定义拒绝策略,做到任务不丢失

前言

开发环境为 jdk8 本文用到了 nacos、kafka 这两个第三方中间件,文章分为两个部分

第一部分:动态线程池 (nacos 必须要,kafka 不强求)(拒绝策略为默认报错)

第二部分:动态线程池 + kafka 自定义拒绝策略(nacos和kafka都需要,kafka是保证任务不丢失的关键)

项目地址:dynamic-thread-pool: 动态线程池+kafka自定义拒绝策略,做到任务不丢失

线程池执行过程(也是我的文章,这对后面测试用例有指导意义):线程池详解与线程池调优

技术实现选型

第三方中间件

工具 版本 jdk版本 备注
nacos nacos-server-2.0.4 8 不能比这个低
kafka kafka_2.13-4.1.1 17 主要是4以上的版本不需要zookeeper,其他也行

maven 主要版本(开发环境jdk8)

jar包 版本 备注
spring-cloud-alibaba 2021.0.6.2 不能比这个低
spring-cloud 2021.0.8 根据 spring-cloud-alibaba 版本选配
spring-boot 2.6.13 根据 spring-cloud-alibaba 版本选配

maven

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zhao</groupId>
    <artifactId>dynamicPool</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>dynamicPool</name>
    <description>dynamicPool</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.13</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <!-- 对应 Spring Boot 2.6.x -->
        <spring-cloud.version>2021.0.8</spring-cloud.version>
        <!-- 这里注意 2021.0.6.2 才能使用 @NacosConfigKeysListener 或 @NacosConfigListener 注解 参考链接如下 -->
        <!-- https://nacos.io/en/blog/nacos-gvr7dx_awbbpb_mmufdmayp5dfozci/?spm=5238cd80.cff869d.0.0.237f7e849GXM35&source=blog#nacosconfiglistener%E6%B3%A8%E8%A7%A3%E7%94%A8%E6%B3%95%E4%BB%8B%E7%BB%8D -->
        <!-- 请配合 nacos-server-2.0.4 使用(2以上) -->
        <spring-cloud-alibaba.version>2021.0.6.2</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- Spring Cloud Alibaba -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.42</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.26</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.18.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.0.1-android</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- Actuator (必须) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>dynamicPool</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.zhao.dynamicPool.DynamicPoolApplication</mainClass>
                    <!-- 移除 <skip>true</skip> 这一行 -->
                    <!-- 添加 excludes 排除不需要打包的依赖 -->
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

一、nacos配置(默认测试配置,测试完后归位)

bootstrap.yaml

yaml 复制代码
spring:
  application:
    name: dynamic-pool-service  # 应用名称,也是Nacos中的服务名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848  # Nacos Config Server地址
      # 配置中心配置
      config:
        server-addr: 127.0.0.1:8848  # Nacos Config Server地址
        file-extension: yaml         # 配置文件扩展名(yaml, yml, properties等)
        refresh-enabled: true  # 必须为true

配置如下

核心线程数:1,最大线程数:2,队列最大容量:2

二、动态线程池

spring 提供了 ThreadPoolTaskExecutor 线程池

他可以通过编码实现动态的改变线程池的核心线程数、最大线程数、超时时间,但是不能动态的调整阻塞队列的队列长度

因为 ThreadPoolTaskExecutor 使用的是 LinkedBlockingQueue 的阻塞队列

LinkedBlockingQueue 的 capacity 参数是 final 字段的,不可以修改

所以本章的核心是实现拥有可变的 capacity 参数的阻塞队列 VariableLinkedBlockingQueue + @NacosConfigKeysListener 注解实现 nacos 参数监听,通过配置文件修改线程池

可变阻塞队列:VariableLinkedBlockingQueue.java(不需要修改)

该 VariableLinkedBlockingQueue 队列是 dromara 团队的代码,dromara 团队是参考 rabbitmq 团队的代码,并实现了 LinkedBlockingQueue 的方法

dromara 团队代码如下:VariableLinkedBlockingQueue.java

rabbitmq 团队的代码如下:VariableLinkedBlockingQueue.java

java 复制代码
public class VariableLinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;

    static class Node<E> {
        E item;

        Node<E> next;

        Node(E x) {
            item = x;
        }
    }

    private int capacity;

    private final AtomicInteger count = new AtomicInteger();

    transient Node<E> head;

    private transient Node<E> last;

    private final ReentrantLock takeLock = new ReentrantLock();

    private final Condition notEmpty = takeLock.newCondition();

    private final ReentrantLock putLock = new ReentrantLock();

    private final Condition notFull = putLock.newCondition();

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }

    public VariableLinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    public VariableLinkedBlockingQueue(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException();
        }
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    public VariableLinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null) {
                    throw new NullPointerException();
                }
                if (n == capacity) {
                    throw new IllegalStateException("Queue full");
                }
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

    @Override
    public int size() {
        return count.get();
    }

    public void setCapacity(int capacity) {
        final int oldCapacity = this.capacity;
        this.capacity = capacity;
        final int size = count.get();
        if (capacity > size && size >= oldCapacity) {
            signalNotFull();
        }
    }

    @Override
    public int remainingCapacity() {
        return capacity - count.get();
    }

    @Override
    public void put(E e) throws InterruptedException {
        if (e == null) {
            throw new NullPointerException();
        }
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() >= capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity) {
                notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0) {
            signalNotEmpty();
        }
    }

    @Override
    public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {

        if (e == null) {
            throw new NullPointerException();
        }
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() >= capacity) {
                if (nanos <= 0) {
                    return false;
                }
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity) {
                notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0) {
            signalNotEmpty();
        }
        return true;
    }

    @Override
    public boolean offer(E e) {
        if (e == null) {
            throw new NullPointerException();
        }
        final AtomicInteger count = this.count;
        if (count.get() >= capacity) {
            return false;
        }
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity) {
                    notFull.signal();
                }
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0) {
            signalNotEmpty();
        }
        return c >= 0;
    }

    @Override
    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1) {
                notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c >= capacity) {
            signalNotFull();
        }
        return x;
    }

    @Override
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                if (nanos <= 0) {
                    return null;
                }
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1) {
                notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c >= capacity) {
            signalNotFull();
        }
        return x;
    }

    @Override
    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0) {
            return null;
        }
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1) {
                    notEmpty.signal();
                }
            }
        } finally {
            takeLock.unlock();
        }
        if (c >= capacity) {
            signalNotFull();
        }
        return x;
    }

    @Override
    public E peek() {
        if (count.get() == 0) {
            return null;
        }
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null) {
                return null;
            } else {
                return first.item;
            }
        } finally {
            takeLock.unlock();
        }
    }

    void unlink(Node<E> p, Node<E> trail) {
        // assert isFullyLocked();
        // p.next is not changed, to allow iterators that are
        // traversing p to maintain their weak-consistency guarantee.
        p.item = null;
        trail.next = p.next;
        if (last == p) {
            last = trail;
        }
        if (count.getAndDecrement() >= capacity) {
            notFull.signal();
        }
    }

    @Override
    public boolean remove(Object o) {
        if (o == null) {
            return false;
        }
        fullyLock();
        try {
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }

    @Override
    public boolean contains(Object o) {
        if (o == null) {
            return false;
        }
        fullyLock();
        try {
            for (Node<E> p = head.next; p != null; p = p.next) {
                if (o.equals(p.item)) {
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }

    @Override
    public Object[] toArray() {
        fullyLock();
        try {
            int size = count.get();
            Object[] a = new Object[size];
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next) {
                a[k++] = p.item;
            }
            return a;
        } finally {
            fullyUnlock();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        fullyLock();
        try {
            int size = count.get();
            if (a.length < size) {
                a = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
            }

            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next) {
                a[k++] = (T) p.item;
            }
            if (a.length > k) {
                a[k] = null;
            }
            return a;
        } finally {
            fullyUnlock();
        }
    }

    @Override
    public String toString() {
        fullyLock();
        try {
            Node<E> p = head.next;
            if (p == null) {
                return "[]";
            }

            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (;;) {
                E e = p.item;
                sb.append(e == this ? "(this Collection)" : e);
                p = p.next;
                if (p == null) {
                    return sb.append(']').toString();
                }
                sb.append(',').append(' ');
            }
        } finally {
            fullyUnlock();
        }
    }

    @Override
    public void clear() {
        fullyLock();
        try {
            for (Node<E> p, h = head; (p = h.next) != null; h = p) {
                h.next = h;
                p.item = null;
            }
            head = last;
            // assert head.item == null && head.next == null;
            if (count.getAndSet(0) >= capacity) {
                notFull.signal();
            }
        } finally {
            fullyUnlock();
        }
    }

    @Override
    public int drainTo(Collection<? super E> c) {
        return drainTo(c, Integer.MAX_VALUE);
    }

    @Override
    public int drainTo(Collection<? super E> c, int maxElements) {
        if (c == null) {
            throw new NullPointerException();
        }
        if (c == this) {
            throw new IllegalArgumentException();
        }
        if (maxElements <= 0) {
            return 0;
        }
        boolean signalNotFull = false;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            int n = Math.min(maxElements, count.get());
            // count.get provides visibility to first n Nodes
            Node<E> h = head;
            int i = 0;
            try {
                while (i < n) {
                    Node<E> p = h.next;
                    c.add(p.item);
                    p.item = null;
                    h.next = h;
                    h = p;
                    ++i;
                }
                return n;
            } finally {
                // Restore invariants even if c.add() threw
                if (i > 0) {
                    // assert h.item == null;
                    head = h;
                    signalNotFull = (count.getAndAdd(-i) >= capacity);
                }
            }
        } finally {
            takeLock.unlock();
            if (signalNotFull) {
                signalNotFull();
            }
        }
    }

    @Override
    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        private Node<E> current;
        private Node<E> lastRet;
        private E currentElement;

        Itr() {
            fullyLock();
            try {
                current = head.next;
                if (current != null) {
                    currentElement = current.item;
                }
            } finally {
                fullyUnlock();
            }
        }

        @Override
        public boolean hasNext() {
            return current != null;
        }

        private Node<E> nextNode(Node<E> p) {
            for (;;) {
                Node<E> s = p.next;
                if (s == p) {
                    return head.next;
                }
                if (s == null || s.item != null) {
                    return s;
                }
                p = s;
            }
        }

        @Override
        public E next() {
            fullyLock();
            try {
                if (current == null) {
                    throw new NoSuchElementException();
                }
                E x = currentElement;
                lastRet = current;
                current = nextNode(current);
                currentElement = (current == null) ? null : current.item;
                return x;
            } finally {
                fullyUnlock();
            }
        }

        @Override
        public void remove() {
            if (lastRet == null) {
                throw new IllegalStateException();
            }
            fullyLock();
            try {
                Node<E> node = lastRet;
                lastRet = null;
                for (Node<E> trail = head, p = trail.next;
                     p != null;
                     trail = p, p = p.next) {
                    if (p == node) {
                        unlink(p, trail);
                        break;
                    }
                }
            } finally {
                fullyUnlock();
            }
        }
    }

    static final class LBQSpliterator<E> implements Spliterator<E> {
        static final int MAX_BATCH = 1 << 25;  // max batch array size;
        final VariableLinkedBlockingQueue<E> queue;
        Node<E> current;    // current node; null until initialized
        int batch;          // batch size for splits
        boolean exhausted;  // true when no more nodes
        long est;           // size estimate
        LBQSpliterator(VariableLinkedBlockingQueue<E> queue) {
            this.queue = queue;
            this.est = queue.size();
        }

        @Override
        public long estimateSize() {
            return est;
        }

        @Override
        public Spliterator<E> trySplit() {
            Node<E> h;
            final VariableLinkedBlockingQueue<E> q = this.queue;
            int b = batch;
            int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1;
            if (!exhausted &&
                    ((h = current) != null || (h = q.head.next) != null) &&
                    h.next != null) {
                Object[] a = new Object[n];
                int i = 0;
                Node<E> p = current;
                q.fullyLock();
                try {
                    if (p != null || (p = q.head.next) != null) {
                        do {
                            if ((a[i] = p.item) != null) {
                                ++i;
                            }
                        } while ((p = p.next) != null && i < n);
                    }
                } finally {
                    q.fullyUnlock();
                }
                if ((current = p) == null) {
                    est = 0L;
                    exhausted = true;
                } else if ((est -= i) < 0L) {
                    est = 0L;
                }
                if (i > 0) {
                    batch = i;
                    return Spliterators.spliterator(a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL |
                            Spliterator.CONCURRENT);
                }
            }
            return null;
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            if (action == null) {
                throw new NullPointerException();
            }
            final VariableLinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                exhausted = true;
                Node<E> p = current;
                do {
                    E e = null;
                    q.fullyLock();
                    try {
                        if (p == null) {
                            p = q.head.next;
                        }
                        while (p != null) {
                            e = p.item;
                            p = p.next;
                            if (e != null) {
                                break;
                            }
                        }
                    } finally {
                        q.fullyUnlock();
                    }
                    if (e != null) {
                        action.accept(e);
                    }
                } while (p != null);
            }
        }

        @Override
        public boolean tryAdvance(Consumer<? super E> action) {
            if (action == null) {
                throw new NullPointerException();
            }
            final VariableLinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                E e = null;
                q.fullyLock();
                try {
                    if (current == null) {
                        current = q.head.next;
                    }
                    while (current != null) {
                        e = current.item;
                        current = current.next;
                        if (e != null) {
                            break;
                        }
                    }
                } finally {
                    q.fullyUnlock();
                }
                if (current == null) {
                    exhausted = true;
                }
                if (e != null) {
                    action.accept(e);
                    return true;
                }
            }
            return false;
        }

        @Override
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.NONNULL |
                    Spliterator.CONCURRENT;
        }
    }

    @Override
    public Spliterator<E> spliterator() {
        return new LBQSpliterator<E>(this);
    }

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {

        fullyLock();
        try {
            // Write out any hidden stuff, plus capacity
            s.defaultWriteObject();

            // Write out all elements in the proper order.
            for (Node<E> p = head.next; p != null; p = p.next) {
                s.writeObject(p.item);
            }
            // Use trailing null as sentinel
            s.writeObject(null);
        } finally {
            fullyUnlock();
        }
    }

    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        // Read in capacity, and any hidden stuff
        s.defaultReadObject();

        count.set(0);
        last = head = new Node<E>(null);

        // Read in all elements and place in queue
        for (;;) {
            @SuppressWarnings("unchecked")
            E item = (E) s.readObject();
            if (item == null) {
                break;
            }
            add(item);
        }
    }
}

线程池配置:DynamicThreadPoolConfig.java(需要根据自己业务把使用参数名进行修改即可(我用todo标记了))

该类重点有三个

第一个是,实现 DynamicThreadPoolTaskExecutor 类用于继承 ThreadPoolTaskExecutor,重写 createQueue 方法,让该线程池使用容量可变的阻塞队列

第二个是,使用 @NacosConfigKeysListener 注解监听配置中心的线程池参数,实现线程池的动态修改,这里注意要使用 spring-cloud-alibaba 2021.0.6.2 及以上的版本,说明如下版本说明


第三个是,使用 getThreadPoolStatus 方法,获取到线程池运行参数作为监控

java 复制代码
/**
 * 动态线程池
 *
 * 加 todo 的 就是可以选择性修改的
 */
@Configuration
@RefreshScope // 支持配置热更新
@EnableAsync // 启用异步支持
@Slf4j
public class DynamicThreadPoolConfig {

    /**
     * 核心参数 默认使用io密集型
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.core-size:0}")
    private int coreSize;

    /**
     * 最大参数 默认使用io密集型
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.max-size:0}")
    private int maxSize;

    /**
     * 队列容量 默认 1w 的队列
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.queue-capacity:10000}")
    private int queueCapacity;

    /**
     * 超时时间 默认 60s 超时
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.keep-alive-seconds:60}")
    private int keepAliveSeconds;

    /**
     * 动态线程池
     * todo 根据业务情况自行修改 Bean 的值
     */
    @Bean("dynamicThreadPool")
    DynamicThreadPoolTaskExecutor dynamicThreadPool() {
        int cpuCores = Runtime.getRuntime().availableProcessors();
        if (coreSize <= 0) {
            coreSize = cpuCores * 2;
        }
        if (maxSize <= 0) {
            maxSize = cpuCores * 4;
        }
        DynamicThreadPoolTaskExecutor executor = new DynamicThreadPoolTaskExecutor();
        // 核心线程数(默认线程数)
        executor.setCorePoolSize(coreSize);
        // 最大线程数(队列满后扩容的最大值)
        executor.setMaxPoolSize(maxSize);
        // 线程空闲时间(秒)
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 队列容量(超过核心线程数时,任务进入队列)
        executor.setQueueCapacity(queueCapacity);
        // 线程名前缀
        executor.setThreadNamePrefix("dynamic-thread-");
        // 拒绝策略(此处使用报错执行)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }

    /**
     * 动态线程池
     * todo 如果你改了 Bean 的值 记得改 Qualifier 中的值
     */
    @Autowired
    @Qualifier("dynamicThreadPool")
    @Lazy
    private ThreadPoolTaskExecutor dynamicThreadPool;

    /**
     * 公平读写锁
     */
    private final static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(true);

    /**
     * 动态调整线程池参数
     *
     * @param corePoolSize     核心线程数
     * @param maxPoolSize      最大线程数
     * @param keepAliveSeconds 超时时间(秒)
     * @param queueCapacity    队列容量
     */
    public void adjustThreadPool(Integer corePoolSize, Integer maxPoolSize, Integer keepAliveSeconds, Integer queueCapacity) {
        ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
        try {
            writeLock.lock();
            // 调整核心线程数
            if (corePoolSize != null && corePoolSize != dynamicThreadPool.getCorePoolSize()) {
                dynamicThreadPool.setCorePoolSize(corePoolSize);
            }
            // 调整最大线程数
            if (maxPoolSize != null && maxPoolSize != dynamicThreadPool.getMaxPoolSize()) {
                dynamicThreadPool.setMaxPoolSize(maxPoolSize);
            }
            // 调整超时时间
            if (keepAliveSeconds != null && keepAliveSeconds != dynamicThreadPool.getKeepAliveSeconds()) {
                dynamicThreadPool.setKeepAliveSeconds(keepAliveSeconds);
            }
            // 调整队列容量(需重置队列)
            if (queueCapacity != null && queueCapacity != dynamicThreadPool.getQueueCapacity()) {
                VariableLinkedBlockingQueue<Runnable> queue =
                        (VariableLinkedBlockingQueue<Runnable>) dynamicThreadPool.getThreadPoolExecutor().getQueue();
                if (queueCapacity < queue.remainingCapacity() + queue.size()) {
                    throw new UnsupportedOperationException("线程池 不支持缩容");
                }
                // 需要自定义可调整队列容量
                queue.setCapacity(queueCapacity);
                // 重置线程池容量参数(这里只是改参数,用于查询时与队列保持同步,并不能调整队列大小)
                dynamicThreadPool.setQueueCapacity(queueCapacity);
            }
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 获取线程池状态
     */
    public ThreadPoolStatus getThreadPoolStatus() {
        ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
        try {
            readLock.lock();
            ThreadPoolExecutor threadPoolExecutor = dynamicThreadPool.getThreadPoolExecutor();
            BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
            int useQueueCapacity = queue.size();
            int remainingCapacity = queue.remainingCapacity();
            return new ThreadPoolStatus()
                    .setCorePoolSize(dynamicThreadPool.getCorePoolSize())
                    .setMaxPoolSize(dynamicThreadPool.getMaxPoolSize())
                    .setActiveThreads(dynamicThreadPool.getActiveCount())
                    .setKeepAliveSeconds(dynamicThreadPool.getKeepAliveSeconds())
                    .setQueueMaxCapacity(useQueueCapacity + remainingCapacity)
                    .setQueueUseCapacity(useQueueCapacity)
                    .setQueueRemainingCapacity(remainingCapacity);
        } finally {
            readLock.unlock();
        }
    }



    /**
     * 监听配置文件变更
     * todo 监听配置文件变更
     */
    @NacosConfigKeysListener(dataId = "dynamic-pool-service.yaml",
            group = "DEFAULT_GROUP", interestedKeyPrefixes = "dynamic.thread-pool.")
    private void onConfigChanged(ConfigChangeEvent changeEvent) {
        log.info("onConfigChanged 监听到配置变化:{}", JSONObject.toJSONString(changeEvent));
        DynamicPoolConfig config = new DynamicPoolConfig();
        try {
            Class<? extends DynamicPoolConfig> aClass = config.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                declaredField.setAccessible(true);
                if (declaredField.isAnnotationPresent(ConfigChangeEventToBean.class)) {
                    ConfigChangeEventToBean annotation = declaredField.getAnnotation(ConfigChangeEventToBean.class);
                    String value = annotation.value();
                    ConfigChangeItem changeItem = changeEvent.getChangeItem(value);
                    // 处理配置变更
                    if (changeItem != null && PropertyChangeType.MODIFIED.equals(changeItem.getType())){
                        declaredField.set(config, changeItem.getNewValue());
                    }
                }
            }
            log.info("onConfigChanged 配置转换结果:{}", config);
            this.adjustThreadPool(
                    Optional.ofNullable(config.getCoreSize()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getMaxSize()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getKeepAliveSeconds()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getQueueCapacity()).map(Integer::parseInt).orElse(null)
            );
        } catch (Exception e) {
            log.error("onConfigChanged 配置转换异常", e);
        }
    }

    /**
     * 线程池状态
     */
    @Data
    @Accessors(chain = true)
    public static class ThreadPoolStatus {
        /**
         * 核心线程数
         */
        private int corePoolSize;
        /**
         * 最大线程数
         */
        private int maxPoolSize;
        /**
         * 线程使用数
         */
        private int activeThreads;
        /**
         * 线程空闲时间(秒)
         */
        private int keepAliveSeconds;
        /**
         * 队列最大容量
         */
        private int queueMaxCapacity;
        /**
         * 队列使用容量
         */
        private int queueUseCapacity;
        /**
         * 队列剩余
         */
        private int queueRemainingCapacity;
    }

    /**
     * 用于解决配置转换问题
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    private @interface ConfigChangeEventToBean{
        String value();
    }

    /**
     * 动态线程池配置
     * todo 需要自己改一下 注解的 value 属性
     */
    @Data
    @Accessors(chain = true)
    static class DynamicPoolConfig {
        /**
         * 核心线程数
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.core-size")
        private String coreSize;
        /**
         * 最大线程数
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.max-size")
        private String maxSize;
        /**
         * 队列容量
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.queue-capacity")
        private String queueCapacity;
        /**
         * 超时时间(秒)
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.keep-alive-seconds")
        private String keepAliveSeconds;
    }

    /**
     * 自定义线程池 实现自定义可变队列
     */
    private static class DynamicThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

        @Override
        @NonNull
        protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
            return queueCapacity > 0 ? new VariableLinkedBlockingQueue<>(queueCapacity) : new VariableLinkedBlockingQueue<>();
        }
    }
}

测试案例:PoolTestController.java

java 复制代码
@RestController
@Slf4j
@RequestMapping("/poolTest")
public class PoolTestController {
    @Autowired
    @Qualifier("dynamicThreadPool")
    private ThreadPoolTaskExecutor dynamicThreadPool;

    @Autowired
    private DynamicThreadPoolConfig dynamicThreadPoolConfig;

    /**
     * 添加任务 测试 就让他等着 看拒绝情况
     */
    @GetMapping("/addTask")
    public Results<String> addTask() {
        dynamicThreadPool.submit(() -> {
            log.info("addTask 线程开始执行 {}", Thread.currentThread().getName());
            while (true) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    log.error("addTask 线程执行异常", e);
                }
            }
        });
        return Results.success();
    }

    /**
     * 获取参数
     */
    @GetMapping("/getParameter")
    public Results<DynamicThreadPoolConfig.ThreadPoolStatus> getParameter() {
        DynamicThreadPoolConfig.ThreadPoolStatus threadPoolStatus = dynamicThreadPoolConfig.getThreadPoolStatus();
        return Results.success(threadPoolStatus);
    }
}

测试结果:

1、默认配置

根据默认配置的 核心线程数:1,最大线程数:2,队列长度:2

那么一共可以执行 4 个任务

按照理论,第五次调用 /poolTest/addTask 接口就会报错

控制台会显示两个任务执行,一个任务被拒绝,我们试一下

下面可以看见我们调用了五次接口,有一个报错

我们查一下线程池状态

有两个任务交由两个线程执行,有两个任务在队列里面,所以第五次请求失败

2、修改配置

修改配置为 核心线程数:2,最大线程数:3,队列长度:3

那么一共可以执行的任务数量为 6,修改为后看下线程池配置

我们之前使用了 2 个线程 2 个队列

按照理论,我们再调用 /poolTest/addTask 接口三次会报错

我们会在控制台看见,有一个新线程开始任务,有一个新的报错,我们试一下

我们查线程状态时,队列里面也会多一个任务

三、动态线程池+kafka自定义拒绝策略

这里就不讲动态线程池的部分了,主要将 kafka 自定义拒绝策略的实现

因为这里也用到了动态线程池(有部分差异),所以得和上面二章节理论一起看

但测试时我就不展示动态线程部分了,就使用默认的 核心线程数:1,最大线程数:2,最大队列容量:2

kafka 配置:application.yaml(也可以放到nacos里面)

yaml 复制代码
spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: ${spring.application.name}
      enable-auto-commit: false  # 关闭自动提交
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      acks: -1
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    template:
      default-topic: test-topic
    listener:
      ack-mode: manual  # 监听,手动提交

工具类:SpringBootBeanUtil.java

主要用于下面 kafka 拒绝策略获取到配置的应用名称

java 复制代码
@Component
@SuppressWarnings("unused")
public class SpringBootBeanUtil implements ApplicationContextAware, EnvironmentAware {
    private static final Logger log = LoggerFactory.getLogger(SpringBootBeanUtil.class);

    private static ApplicationContext applicationContext;


    private static Environment environment;

    @Override
    public void setEnvironment(final Environment environment) {
        if (SpringBootBeanUtil.applicationContext == null) {
            SpringBootBeanUtil.environment = environment;
        }
    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
        if (SpringBootBeanUtil.applicationContext == null) {
            SpringBootBeanUtil.applicationContext = applicationContext;
        }
    }

    /**
     * 获取applicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 获取applicationContext
     */
    public static Environment getEnvironment() {
        return environment;
    }

    /**
     * 通过name获取 Bean.
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean.
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

启动器:DynamicPoolApplication.java

优先把 SpringBootBeanUtil 注入到容器里面,不然后面初始化 kafka 的拒绝策略会报错(因为我们需要从配置里面获取应用名(spring.application.name))

java 复制代码
@SpringBootApplication
public class DynamicPoolApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DynamicPoolApplication.class);
        app.addInitializers(new PriorityRegistrationClass());
        app.run(args);
    }
    /**
     * 优先注册
     */
    static class PriorityRegistrationClass implements ApplicationContextInitializer<GenericApplicationContext> {
        @Override
        public void initialize(GenericApplicationContext applicationContext) {
            // 你可以注册多个Bean
            applicationContext.registerBean(SpringBootBeanUtil.class);
        }
    }

}

kafka 生产者:KafkaProducerService.java

java 复制代码
/**
 * kafka 生产者
 */
@Service
@Slf4j
public class KafkaProducerService {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    /**
     * 同步发送消息
     * @param topic 主体
     * @param msg 消息
     */
    public void sendMessageSync(String topic, Object msg) throws ExecutionException, InterruptedException {
        try {
            kafkaTemplate.send(topic, msg).get();
            log.info("sendMessageSync 同步消息发送 成功 topic={} msg={}", topic, msg);
        } catch (Exception e) {
            log.error("sendMessageSync 同步消息发送 失败", e);
            throw e;
        }
    }

    /**
     * 异步发送消息
     * @param topic 主体
     * @param msg 消息
     */
    public void sendMessageAsync(String topic, Object msg) {
        kafkaTemplate.send(topic, msg).addCallback(success -> {
            log.info("sendMessageAsync 异步消息发送 成功 topic={} msg={}", topic, msg);
        }, failure -> {
            log.error("sendMessageAsync 异步消息发送 失败", failure);
        });
    }
}

kafka 消费者:KafkaConsumerService.java

java 复制代码
/**
 * kafka 消费者
 */
@Service
@Slf4j
public class KafkaConsumerService {


    /**
     * 监听Kafka消息 手动提交
     * todo 测试topic 需修改
     *
     * @param msg 消息内容
     */
    @KafkaListener(topics = "test-reject", groupId = "dynamic-pool-service")
    public void consume(String msg, Acknowledgment ack) {
        log.info("kafka 开始消费 msg: {}", msg);
        try {
            // 这里是通过反射 进行任务执行
            KafkaRejectPolicy.RejectedTaskMessage.taskStart(msg);
            ack.acknowledge();
        } catch (Exception e) {
            log.info("kafka 消费异常", e);
        }
    }
}

kafka 拒绝策略

这里面重要的就是 KafkaTaskInfoRunnable 这个抽象类

我们的任务都需要继承这个类来实现 kafka 异步任务来做到任务不丢失

逻辑是这样的(后面测试案例会讲到,这里只是给大家理一下逻辑)

我们先实现一个 A 类继承 KafkaTaskInfoRunnable 这个抽象类(A.java extends KafkaTaskInfoRunnable.java)

KafkaTaskInfoRunnable 实现了 Runnable 接口,所以 A 类需要实现 run 方法,这样就可以通过线程池进行异步任务

我们线程池满了之后,任务被拒绝了,会给 kafka 发送一个消息(json字符串)

json 里面包括了实现任务的类(A.java)(Class)、入参(具体值)、入参类型(Class)等(这三个比较关键)

kafka 监听后会从消息(json)里面获取到任务的类(Class)、入参(具体值)、入参类型(Class)

然后通过反射同步的调用 A.java 的 run 方法,达到消峰(因为kafka会一个任务执行完再接着执行)和任务不丢失

由于要使用反射获取 A.java 的具体数据,所以 A.java 是不能使用匿名内部类的

ps:发送警告我没有处理,有兴趣大家可以实现以下,这个也简单,如果警告频繁,说明 QPS 过大,我们就需要自己调整线程池或者就是加机器了

java 复制代码
/**
 * Kafka拒绝策略 - 将被拒绝的任务发送到Kafka队列
 */
@Slf4j
public class KafkaRejectPolicy implements RejectedExecutionHandler {

    private static final AtomicLong rejectCount = new AtomicLong(0);
    private final String topic;

    /**
     * 应用名称(用于消息标识)
     */
    private final String applicationName;

    /**
     * 构造函数
     *
     * @param topic 目标主题
     */
    public KafkaRejectPolicy(String topic) {
        this.topic = topic;
        // 这里得先在启动器配置优先加载 SpringBootBeanUtil 的 bean 对象
        String applicationName = SpringBootBeanUtil.getEnvironment().getProperty("spring.application.name");
        if (applicationName == null || applicationName.isEmpty()) {
            throw new IllegalArgumentException("配置项 spring.application.name 不能为空");
        }
        this.applicationName = applicationName;
    }

    /**
     * 获取ip 地址
     */
    private static String getIpAddress() throws UnknownHostException {
        InetAddress localHost = InetAddress.getLocalHost();
        return localHost.getHostAddress();
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // 检查任务类型
            if (!(r instanceof KafkaTaskInfoRunnable)) {
                // 使用 submit 方法时会走到 AbstractExecutorService 的 submit 方法,会封装一层 FutureTask
                // 如果是 FutureTask,尝试提取内部的 Runnable
                if (r instanceof FutureTask) {
                    try {
                        // 通过反射获取 FutureTask 内部的 callable
                        Field callableField = FutureTask.class.getDeclaredField("callable");
                        callableField.setAccessible(true);
                        Object callable = callableField.get(r);

                        // 如果是 RunnableAdapter(Executor.callable 返回的内部类)
                        if (callable != null && callable.getClass().getName().contains("RunnableAdapter")) {
                            Field taskField = callable.getClass().getDeclaredField("task");
                            taskField.setAccessible(true);
                            Object originalTask = taskField.get(callable);

                            if (originalTask instanceof KafkaTaskInfoRunnable) {
                                r = (Runnable) originalTask;
                            }
                        }
                    } catch (Exception e) {
                        log.error("提取 FutureTask 内部任务失败", e);
                    }
                }
            }

            // 最终检查
            if (!(r instanceof KafkaTaskInfoRunnable)) {
                throw new IllegalArgumentException("该任务无法由 Kafka 执行,必须使用 KafkaTaskInfoRunnable 类型");
            }
            KafkaTaskInfoRunnable<?> KafkaTaskInfoRunnable = (KafkaTaskInfoRunnable<?>) r;
            long count = rejectCount.incrementAndGet();

            // 获取线程池状态信息
            String poolInfo = getThreadPoolInfo(executor);

            // 构建拒绝任务的消息
            RejectedTaskMessage taskMessage = new RejectedTaskMessage()
                    .setTaskId(generateTaskId())
                    .setApplicationName(applicationName)
                    .setIpAddress(getIpAddress())
                    .setThreadPoolInfo(poolInfo)
                    .setRejectTime(System.currentTimeMillis())
                    .setRejectCount(count)
                    .setTaskDataJson(KafkaTaskInfoRunnable.getTaskDataJson())
                    .setTaskClass(KafkaTaskInfoRunnable.getTaskClass())
                    .setTaskInputParameterClass(KafkaTaskInfoRunnable.getTaskInputParameterClass());

            // 发送到Kafka
            String msg = JSONObject.toJSONString(taskMessage);
            // 使用 kafka 生产者发送消息
            KafkaProducerService kafkaProducer = SpringBootBeanUtil.getBean(KafkaProducerService.class);
            // 同步发送并处理回调
            kafkaProducer.sendMessageSync(topic, msg);

            log.info("拒绝任务已发送到Kafka队列。拒绝次数: {}, 线程池状态: {}", count, poolInfo);
        } catch (Exception e) {
            log.error(" kafka 拒绝策略 发送消息异常", e);
            // 降级处理(让当前线程自己处理)
            if (!executor.isShutdown()) {
                r.run();
            }
        }
    }

    /**
     * 获取线程池状态信息
     * <p>
     * 包含以下关键指标:
     * - 活跃线程数:当前正在执行任务的线程数量
     * - 线程池大小:线程池中当前的线程数量(包括空闲线程)
     * - 核心线程数:线程池保持的最小线程数量
     * - 最大线程数:线程池允许创建的最大线程数量
     * - 队列大小:任务队列中等待执行的任务数量
     * - 队列容量:任务队列的最大容量
     */
    private String getThreadPoolInfo(ThreadPoolExecutor executor) {
        BlockingQueue<Runnable> queue = executor.getQueue();
        return String.format(
                "活跃线程: %d, 池中线程: %d, 核心线程: %d, 最大线程: %d, 队列大小/队列容量: %d/%d",
                // 正在执行任务的线程数
                executor.getActiveCount(),
                // 线程池中当前的线程总数
                executor.getPoolSize(),
                // 核心线程数
                executor.getCorePoolSize(),
                // 最大线程数
                executor.getMaximumPoolSize(),
                // 队列中等待的任务数
                queue.size(),
                // 队列总容量
                queue.size() + queue.remainingCapacity()
        );
    }

    /**
     * 生成任务ID
     */
    private String generateTaskId() {
        return UUID.randomUUID().toString();
    }

    /**
     * 发送告警
     */
    private void sendAlert(RejectedTaskMessage message) {
        // 实现告警逻辑(邮件、短信、钉钉等)
        log.error("任务处理失败!任务ID: {}", message.getTaskId());
    }

    /**
     * 任务信息接口(不能使用匿名内部类)
     * 实现此接口的任务可以传递更多信息到拒绝策略
     */
    public static abstract class KafkaTaskInfoRunnable<T> implements Runnable {
        private final T taskData;

        public KafkaTaskInfoRunnable(T taskData) {
            // 运行时检查
            if (this.getClass().isAnonymousClass()) {
                throw new IllegalStateException("TaskInfoRunnable 类 不能使用匿名内部类");
            }
            this.taskData = taskData;
        }

        protected String getTaskDataJson() {
            return JSONObject.toJSONString(taskData);
        }

        protected T getTaskData() {
            return this.taskData;
        }

        /**
         * 任务执行类名(全类名)
         */
        protected Class<KafkaTaskInfoRunnable<T>> getTaskClass() {
            return (Class<KafkaTaskInfoRunnable<T>>) this.getClass();
        }

        /**
         * 任务入参类名(全类名)
         */
        protected Class<T> getTaskInputParameterClass() {
            return (Class<T>) taskData.getClass();
        }
    }

    /**
     * 拒绝任务消息实体
     */
    @Data
    @Accessors(chain = true)
    public static class RejectedTaskMessage implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 任务ID(唯一ID)
         */
        private String taskId;
        /**
         * 应用名称
         */
        private String applicationName;
        /**
         * ip地址
         */
        private String ipAddress;
        /**
         * 线程池状态信息
         */
        private String threadPoolInfo;
        /**
         * 拒绝时间戳
         */
        private long rejectTime;
        /**
         * 拒绝计数
         */
        private long rejectCount;
        /**
         * 任务执行类名(全类名)
         */
        private Class<? extends KafkaTaskInfoRunnable> taskClass;
        /**
         * 任务入参类名(全类名)
         */
        private Class<?> taskInputParameterClass;
        /**
         * 任务入参数据(json)
         */
        private String taskDataJson;

        public static void taskStart(String msgJson) throws Exception {
            JSON.config(JSONReader.Feature.SupportClassForName);
            KafkaRejectPolicy.RejectedTaskMessage message = JSONObject.parseObject(
                    msgJson,
                    KafkaRejectPolicy.RejectedTaskMessage.class
            );
            String dataJson = message.getTaskDataJson();
            Class<?> inputParameterClass = message.getTaskInputParameterClass();
            Class<KafkaTaskInfoRunnable<?>> taskClass = (Class<KafkaTaskInfoRunnable<?>>) message.getTaskClass();
            Object o = JSONObject.parseObject(dataJson, inputParameterClass);
            Constructor<KafkaTaskInfoRunnable<?>> constructor = taskClass.getDeclaredConstructor(inputParameterClass);
            constructor.setAccessible(true);
            KafkaTaskInfoRunnable<?> infoRunnable = constructor.newInstance(o);
            infoRunnable.run();
        }
    }
}

动态线程池:DynamicKafkaRejectThreadPoolConfig.java

跟上面的动态线程池类似,但有细微差距

最主要的是 ThreadPoolKafkaTaskExecutor 类

该类也继承了 ThreadPoolTaskExecutor 类,用于可以动态改变线程池的参数(核心、最大线程、队列容量(也是通过VariableLinkedBlockingQueue 实现的))

这里比较重要的就是重写线程池的 execute 和 submit 方法,只允许继承了 KafkaTaskInfoRunnable 的任务入队

java 复制代码
/**
 * 动态线程池 (kafka 拒绝策略版本)
 * <p>
 * 加 todo 的 就是可以选择性修改的
 */
@Configuration
@RefreshScope // 支持配置热更新
@EnableAsync // 启用异步支持
@Slf4j
@Lazy
public class DynamicKafkaRejectThreadPoolConfig {

    /**
     * 核心参数 默认使用io密集型
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.core-size:0}")
    private int coreSize;

    /**
     * 最大参数 默认使用io密集型
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.max-size:0}")
    private int maxSize;

    /**
     * 队列容量 默认 1w 的队列
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.queue-capacity:10000}")
    private int queueCapacity;

    /**
     * 超时时间 默认 60s 超时
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.keep-alive-seconds:60}")
    private int keepAliveSeconds;

    /**
     * 超时时间 默认 60s 超时
     * todo 根据业务情况自行修改
     */
    @Value("${dynamic.thread-pool.kafka-reject-policy-topic:test-reject}")
    private String kafkaRejectPolicyTopic;

    /**
     * 动态线程池
     * todo 根据业务情况自行修改 Bean 的值
     */
    @Bean("dynamicKafkaRejectThreadPool")
    ThreadPoolKafkaTaskExecutor dynamicKafkaRejectThreadPool() {
        int cpuCores = Runtime.getRuntime().availableProcessors();
        if (coreSize <= 0) {
            coreSize = cpuCores * 2;
        }
        if (maxSize <= 0) {
            maxSize = cpuCores * 4;
        }
        ThreadPoolKafkaTaskExecutor executor = new ThreadPoolKafkaTaskExecutor();
        // 核心线程数(默认线程数)
        executor.setCorePoolSize(coreSize);
        // 最大线程数(队列满后扩容的最大值)
        executor.setMaxPoolSize(maxSize);
        // 线程空闲时间(秒)
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 队列容量(超过核心线程数时,任务进入队列)
        executor.setQueueCapacity(queueCapacity);
        // 线程名前缀
        executor.setThreadNamePrefix("dynamic-thread-");
        // 拒绝策略(此处使用报错执行)
        executor.setRejectedExecutionHandler(new KafkaRejectPolicy(kafkaRejectPolicyTopic));
        // 初始化
        executor.initialize();
        return executor;
    }

    /**
     * 动态线程池
     * todo 如果你改了 Bean 的值 记得改 Qualifier 中的值
     */
    @Autowired
    @Qualifier("dynamicKafkaRejectThreadPool")
    @Lazy
    private ThreadPoolTaskExecutor dynamicKafkaRejectThreadPool;

    /**
     * 公平读写锁
     */
    private final static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(true);

    /**
     * 动态调整线程池参数
     *
     * @param corePoolSize     核心线程数
     * @param maxPoolSize      最大线程数
     * @param keepAliveSeconds 超时时间(秒)
     * @param queueCapacity    队列容量
     */
    public void adjustThreadPool(Integer corePoolSize, Integer maxPoolSize, Integer keepAliveSeconds, Integer queueCapacity) {
        ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
        try {
            writeLock.lock();
            // 调整核心线程数
            if (corePoolSize != null && corePoolSize != dynamicKafkaRejectThreadPool.getCorePoolSize()) {
                dynamicKafkaRejectThreadPool.setCorePoolSize(corePoolSize);
            }
            // 调整最大线程数
            if (maxPoolSize != null && maxPoolSize != dynamicKafkaRejectThreadPool.getMaxPoolSize()) {
                dynamicKafkaRejectThreadPool.setMaxPoolSize(maxPoolSize);
            }
            // 调整超时时间
            if (keepAliveSeconds != null && keepAliveSeconds != dynamicKafkaRejectThreadPool.getKeepAliveSeconds()) {
                dynamicKafkaRejectThreadPool.setKeepAliveSeconds(keepAliveSeconds);
            }
            // 调整队列容量(需重置队列)
            if (queueCapacity != null && queueCapacity != dynamicKafkaRejectThreadPool.getQueueCapacity()) {
                VariableLinkedBlockingQueue<Runnable> queue =
                        (VariableLinkedBlockingQueue<Runnable>) dynamicKafkaRejectThreadPool.getThreadPoolExecutor().getQueue();
                if (queueCapacity < queue.remainingCapacity() + queue.size()) {
                    throw new UnsupportedOperationException("线程池 不支持缩容");
                }
                // 需要自定义可调整队列容量
                queue.setCapacity(queueCapacity);
                // 重置线程池容量参数(这里只是改参数,用于查询时与队列保持同步,并不能调整队列大小)
                dynamicKafkaRejectThreadPool.setQueueCapacity(queueCapacity);
            }
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 获取线程池状态
     */
    public ThreadPoolStatus getThreadPoolStatus() {
        ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
        try {
            readLock.lock();
            ThreadPoolExecutor threadPoolExecutor = dynamicKafkaRejectThreadPool.getThreadPoolExecutor();
            BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
            int useQueueCapacity = queue.size();
            int remainingCapacity = queue.remainingCapacity();
            return new ThreadPoolStatus()
                    .setCorePoolSize(dynamicKafkaRejectThreadPool.getCorePoolSize())
                    .setMaxPoolSize(dynamicKafkaRejectThreadPool.getMaxPoolSize())
                    .setActiveThreads(dynamicKafkaRejectThreadPool.getActiveCount())
                    .setKeepAliveSeconds(dynamicKafkaRejectThreadPool.getKeepAliveSeconds())
                    .setQueueMaxCapacity(useQueueCapacity + remainingCapacity)
                    .setQueueUseCapacity(useQueueCapacity)
                    .setQueueRemainingCapacity(remainingCapacity);
        } finally {
            readLock.unlock();
        }
    }


    /**
     * 监听配置文件变更
     * todo 监听配置文件变更
     */
    @NacosConfigKeysListener(dataId = "dynamic-pool-service.yaml",
            group = "DEFAULT_GROUP", interestedKeyPrefixes = "dynamic.thread-pool.")
    private void onConfigChanged(ConfigChangeEvent changeEvent) {
        log.info("onConfigChanged 监听到配置变化:{}", JSONObject.toJSONString(changeEvent));
        DynamicPoolConfig config = new DynamicPoolConfig();
        try {
            Class<? extends DynamicPoolConfig> aClass = config.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                declaredField.setAccessible(true);
                if (declaredField.isAnnotationPresent(ConfigChangeEventToBean.class)) {
                    ConfigChangeEventToBean annotation = declaredField.getAnnotation(ConfigChangeEventToBean.class);
                    String value = annotation.value();
                    ConfigChangeItem changeItem = changeEvent.getChangeItem(value);
                    // 处理配置变更
                    if (changeItem != null && PropertyChangeType.MODIFIED.equals(changeItem.getType())) {
                        declaredField.set(config, changeItem.getNewValue());
                    }
                }
            }
            log.info("onConfigChanged 配置转换结果:{}", config);
            this.adjustThreadPool(
                    Optional.ofNullable(config.getCoreSize()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getMaxSize()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getKeepAliveSeconds()).map(Integer::parseInt).orElse(null),
                    Optional.ofNullable(config.getQueueCapacity()).map(Integer::parseInt).orElse(null)
            );
        } catch (Exception e) {
            log.error("onConfigChanged 配置转换异常", e);
        }
    }

    /**
     * 线程池状态
     */
    @Data
    @Accessors(chain = true)
    public static class ThreadPoolStatus {
        /**
         * 核心线程数
         */
        private int corePoolSize;
        /**
         * 最大线程数
         */
        private int maxPoolSize;
        /**
         * 线程使用数
         */
        private int activeThreads;
        /**
         * 线程空闲时间(秒)
         */
        private int keepAliveSeconds;
        /**
         * 队列最大容量
         */
        private int queueMaxCapacity;
        /**
         * 队列使用容量
         */
        private int queueUseCapacity;
        /**
         * 队列剩余
         */
        private int queueRemainingCapacity;
    }

    /**
     * 用于解决配置转换问题
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    private @interface ConfigChangeEventToBean {
        String value();
    }

    /**
     * 动态线程池配置
     * todo 需要自己改一下 注解的 value 属性
     */
    @Data
    @Accessors(chain = true)
    static class DynamicPoolConfig {
        /**
         * 核心线程数
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.core-size")
        private String coreSize;
        /**
         * 最大线程数
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.max-size")
        private String maxSize;
        /**
         * 队列容量
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.queue-capacity")
        private String queueCapacity;
        /**
         * 超时时间(秒)
         */
        @ConfigChangeEventToBean("dynamic.thread-pool.keep-alive-seconds")
        private String keepAliveSeconds;
    }

    /**
     * 自定义线程池 实现自定义可变队列
     */
    public static class ThreadPoolKafkaTaskExecutor extends ThreadPoolTaskExecutor {

        public <T extends KafkaRejectPolicy.KafkaTaskInfoRunnable<?>> void execute(T task) {
            super.execute(task);
        }

        public <T extends KafkaRejectPolicy.KafkaTaskInfoRunnable<?>> Future<?> submit(T task) {
            return super.submit(task);
        }

        @Override
        @NonNull
        protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
            return queueCapacity > 0 ? new VariableLinkedBlockingQueue<>(queueCapacity) : new VariableLinkedBlockingQueue<>();
        }
    }
}

任务执行:UserModifyNameHandler.java

这个方法就是用于执行异步任务(不能是匿名内部类,不然反射获取不到对应的类就执行 kafka 的任务了)

User.java 就是有两个成员变量 name 和 age 用于测试,这里就不展示了,可以看我 gitee 中的代码(链接再最上面的前言部分)

java 复制代码
@Slf4j
public class UserModifyNameHandler extends KafkaRejectPolicy.KafkaTaskInfoRunnable<User>{
    public UserModifyNameHandler(User taskData) {
        super(taskData);
    }

    @Override
    public void run() {
        User taskData = super.getTaskData();
        taskData.setName(taskData.getName() + "--修改了名字");
        // 睡两秒钟模仿耗时
        try {
            Thread.sleep(2_000);
        } catch (InterruptedException ignored) {
        }
        log.info("用户修改名字 任务开始执行 {}", taskData);
    }
}

测试案例:KafkaPoolTestController.java

这个测试案例里面,我们只需要调用异常 /kafkaPoolTest/addTask 方法

会执行 10 条任务,我们再任务 UserModifyNameHandler 里面睡了两秒钟,

所以我们先设置的线程池参数(核心线程数:1,最大线程数:2,最大队列容量:2),只会执行 4 个任务

其他的就会被拒绝掉,然后放进 kafka 的队列里面,由 kafka 的消费者消费(KafkaConsumerService)

得到的效果就是线程池执行了 4 条任务(异步), kafka 执行了 6 条任务(同步)

java 复制代码
@RestController
@Slf4j
@RequestMapping("/kafkaPoolTest")
public class KafkaPoolTestController {
    @Autowired
    @Qualifier("dynamicKafkaRejectThreadPool")
    private DynamicKafkaRejectThreadPoolConfig.ThreadPoolKafkaTaskExecutor dynamicThreadPool;

    @Autowired
    private DynamicThreadPoolConfig dynamicThreadPoolConfig;

    /**
     * 记录生成了多少用户
     */
    private static final AtomicLong userCount = new AtomicLong(0);

    /**
     * 添加任务 测试 就让他等着 看拒绝情况
     */
    @GetMapping("/addTask")
    public Results<String> addTask() {
        // 一次性生成十个数据
        List<User> users = mockUsers();
        // 线程池参数 核心线程:1 最大线程:2 队列容量:2
        // 那么一次调用只能有 4 个任务执行,其他的都会被拒绝
        // 那么拒绝的任务都进入了 kafka 的队列,由 KafkaConsumerService 处理
        users.forEach(user -> {
            Future<?> submit = dynamicThreadPool.submit(new UserModifyNameHandler(user));
        });

        return Results.success();
    }

    /**
     * 获取参数
     */
    @GetMapping("/getParameter")
    public Results<DynamicThreadPoolConfig.ThreadPoolStatus> getParameter() {
        DynamicThreadPoolConfig.ThreadPoolStatus threadPoolStatus = dynamicThreadPoolConfig.getThreadPoolStatus();
        return Results.success(threadPoolStatus);
    }

    /**
     * 模拟生成用户
     */
    public synchronized static List<User> mockUsers() {
        List<@Nullable User> list = Lists.newArrayList();
        for (int i = 0; i < 10; i++) {
            long count = userCount.incrementAndGet();
            User user = new User().setName("用户:" + count).setAge(18);
            list.add(user);
        }
        return list;
    }

}

测试结果:

由于打印的消息有点多,所以我就截取一部分进行展示

分别是

线程池异步进行了 4 条任务,用户 4、1、3、2

kafka 同步执行了 6 条任务,用户 5、6、7、8、9、10(这里有可能也不是这个顺序执行的,这要看 kafka 入队顺序了,因为本来线程池就是异步的执行任务,kafka 为什么我要写成同步呢?因为本来进入 kafka 队列就是因为服务器资源紧张,线程池不够用了,再异步执行的话,跟线程池设置超多的最大线程数或者最大队列数有什么区别呢,这样会导致 OOM 的,所以这里使用同步,只让一个线程执行任务,就会减少服务器的压力,从而做到消峰+任务不丢失)

相关推荐
yumgpkpm2 小时前
Hadoop如何用Flink支持实时数据分析需求
大数据·hadoop·分布式·hdfs·flink·kafka·cloudera
WizLC2 小时前
【后端】关于Elasticsearch的入门,下载安装+使用
java·大数据·后端·elasticsearch·搜索引擎·全文检索
青云交2 小时前
Java 大视界 -- 438 台物联网设备时序数据难题破解:Java+Redis+HBase+Kafka 实战全解析(查询延迟 18ms)(438)
java·智能制造·redis 缓存·hbase 存储·时序数据处理·kafka 消息队列·ai 异常检测
Slow菜鸟2 小时前
Java基础 | JSON 处理手册
java·开发语言·json
北城以北88882 小时前
SpringBoot--Spring Boot原生缓存基于Redis的Cacheable注解使用
java·spring boot·redis·缓存·intellij-idea
武子康2 小时前
Java-208 RabbitMQ Topic 主题交换器详解:routingKey/bindingKey 通配符与 Java 示例
java·分布式·性能优化·消息队列·系统架构·rabbitmq·java-rabbitmq
后端小张3 小时前
【JAVA 进阶】SpringMVC全面解析:从入门到实战的核心知识点梳理
java·开发语言·spring boot·spring·spring cloud·java-ee·springmvc
Lucky小小吴4 小时前
ClamAV扫描速度提升6.5倍:服务器杀毒配置优化实战指南
java·服务器·网络·clamav