A simple database connection pool implementation

Posted by mmoranuk on Fri, 28 Jan 2022 23:55:54 +0100

Wait timeout mode

When calling a method, wait for a period of time. If the method can get the result within a given period of time, the result will be returned immediately. On the contrary, the timeout will return the default result.

Assuming that the timeout period is T, it can be inferred that the timeout will occur after the current time now+T.

Define the following variables:

  • Waiting duration: REMAINING=T.
  • Timeout: FUTURE=now+T.

At this time, you only need to wait(REMAINING). After the wait(REMAINING) returns, it will execute: REMAINING = future now. If REMAINING is less than or equal to 0, it means that it has timed out and quit directly. Otherwise, it will continue to execute wait(REMAINING).

public synchronized Object get(long mills) throws InterruptedException {
	long future = System.currentTimeMillis() + mills;
	long remaining = mills;
	while ((result == null) && remaining > 0) {
		wait(remaining);
		remaining = future - System.currentTimeMillis();
	}
	return result;
}

A simple database connection pool implementation

Use the wait timeout mode to construct a simple database connection pool.
First, let's look at the definition of connection pool. It initializes the maximum limit of the connection through the constructor and maintains the connection through a two-way queue. The caller needs to call the fetchConnection(long) method to specify how many milliseconds to timeout to obtain the connection. When the connection is used, it needs to call the releaseConnection(Connection) method to put the connection back into the thread pool.

package simpleconnpool;

import java.sql.Connection;
import java.util.LinkedList;

public class ConnectionPool {

    private LinkedList<Connection> pool = new LinkedList<>();

    public ConnectionPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0;i < initialSize;i++) {
                pool.addLast(ConnectionDriver.createConnection());
            }
        }
    }

    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool) {
                //When a connection is released, it needs to be notified so that other consumers can perceive that a connection has been returned in the connection pool
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    public Connection fetchConnection(long mills) throws InterruptedException {
        synchronized (pool) {
            //Full timeout
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            } else {
                long future = System.currentTimeMillis() + mills;
                long remaining = mills;
                while (pool.isEmpty() && remaining > 0) {
                    pool.wait(remaining);
                    remaining = future - System.currentTimeMillis();
                }
                Connection res = null;
                if (!pool.isEmpty()) {
                    res = pool.removeFirst();
                }
                return res;
            }
        }
    }
}

Due to Java sql. Connection is an interface, and the final implementation is implemented by the database driver provider. Considering that it is only an example, we construct a connection through a dynamic proxy. The proxy implementation of the connection only sleeps for 100ms when the commit method is called.

package simpleconnpool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.concurrent.TimeUnit;

public class ConnectionDriver {

    static class ConnectionHandler implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("commit")) {
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return null;
        }
    }

    //Create a Connection proxy and sleep for 100ms during commit
    public static final Connection createConnection() {
        return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(),
                new Class<?>[]{Connection.class},new ConnectionHandler());
    }

}

Let's test the working condition of the database connection pool and simulate the process of obtaining, using and finally releasing connections by the client ConnectionRunner. When it is used, the number of connections obtained will increase, otherwise, the number of connections not obtained will increase.

package simpleconnpool;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class ConnectionPoolTest {

    static ConnectionPool pool = new ConnectionPool(10);
    //Ensure that all connectionrunners can start at the same time
    static CountDownLatch start = new CountDownLatch(1);
    //The main thread will wait for all connectionrunners to finish before continuing
    static CountDownLatch end;

    public static void main(String[] args) throws InterruptedException {
        //The number of threads can be modified for observation
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();
        for (int i = 0;i < threadCount;i++) {
            Thread thread = new Thread(new ConnectionRunner(count, got, notGot), "ConnectionRunnerThread");
            thread.start();
        }
        start.countDown();
        end.await();
        System.out.println("total invoke: "+(threadCount*count));
        System.out.println("got connection: "+got);
        System.out.println("not got connection: "+notGot);
    }

    static class ConnectionRunner implements Runnable {

        int count;
        AtomicInteger got;
        AtomicInteger notGot;

        public ConnectionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        @Override
        public void run() {
            try {
                start.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (count > 0) {
                try {
                    //Get the connection from the thread pool. If it cannot be obtained within 1000ms, null will be returned
                    //Count the quantity got by connection and the quantity notGot not obtained by connection respectively
                    Connection connection = pool.fetchConnection(1000);
                    if (connection != null) {
                        try {
                            connection.createStatement();
                            connection.commit();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }

                    } else {
                        notGot.incrementAndGet();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    count--;
                }
            }
            end.countDown();
        }
    }

}

Use CountDownLatch to ensure that the ConnectionRunnerThread can start executing at the same time, and make the main thread return from the waiting state after all are finished.

Topics: Java Multithreading Concurrent Programming