Custom DDNS service based on Spring Boot

Recently, I found that the DDNS service provided by Asus is not working properly. When My public IP has changed, the DNS is not updated in time. It caused my VPN clients to disconnect from the server. Therefore, I wrote a DDNS service based on Spring Boot for my personal use. It works fine when run by jar file or docker. In my scenario, there is a Truenas Scale server locally. So, I just put a docker on it. It has been working fine for about several weeks.

For the users who may encounter the same issue with the built-in DDNS server, you can just try my open-source DDNS service. There is the GitHub link Please refer to the readme file. You are welcome to report issues or bugs. BTW, for now, it only supports Cloudflare DNS, because I’m using Cloudflare only right now. Please let me know if you want to use another DNS provider. It will be better if you provide me test account for my development.

Connect multiple Asus routers via OpenVPN


This guide is to discuss how to connect multiple Asus routers via OpenVPN. In general, other brand routers should also work fine with OpenVPN if they support it in their official firmware. If not, it should be a little difficult to install third-party firmware like OpenWrt or Padavan for someone not familiar with them. After all, I’ll use Asus’s official firmware in this guide. Please let me know if you need other firmware guides.

First, let’s say we have 3 routers named router0, router1 and router2. And each of them has a different ISP connected. Networking settings show in the image below. Remember those network settings are made up for convenience. Put your setting into the router’s dashboard.

  • Router0’s public IP is, the gateway is and the subnet is
  • Router1’s public IP is, the gateway is and the subnet is
  • Router2’s public IP is, the gateway is and the subnet is

In this guide, not all routers need to have public IPs. But do need one for the server. For example, if router0 has a public IP, and others don’t. Then, router0 will be set as an OpenVPN server, others are OpenVPN clients.


Unfortunately, some ISP don’t provide static public IP freely or even don’t provide dynamic public IP. There are 3 cases.

  1. If you have a static public IP, you don’t need this part.
  2. If you have a dynamic public IP, you need to keep reading this part.
  3. If you have no public IP, please talk to your ISP or use frp/ngrok to get your service exposed to the public. And this guide may not fit your situation.

DDNS is Dynamic DNS, which can dynamically update DNS records without the need for human interaction. If your public IP changes, the DNS of your domain keeps updating automatically.

As I said before, we’re using Asus’s official firmware. We’re going to use DDNS provided by Asus which is free.

Find WAN under advanced settings, and choose DDNS tab. By default, the Server choice is WWW.ASUS.COM. If not, select it. Enter the domain you want into the Host Name box then click Register. In this screenshot, I have registered, so it shows Deregister button. If the domain name you want is already registered, then it will indicate a failure. You can only choose an alternative domain name that is not registered. is a quick way to test if your domain is taken.

If everything goes well, a domain has been created and pointed to your router’s public IP. Let’s say the domain is

Config on server

Find VPN under advanced settings. And choose VPN Server and OpenVPN.

The only thing that needs to do here is to set the server port to whatever you like. Let’s say set 10000 as the server port for example.

Then scroll down and add the client’s username and password. In this guide, we have other 2 routers as clients. So add 2 accounts here.

  • username: router1, password: router1password
  • username: router2, password: router2password

Don’t forget to click the Apply button, if not the config won’t be activated.

Then go do VPN Details and change General to Advanced Setings, you will find more settings.

There are 4 things to do:

  1. Check if the Server Port is the one you set before.
  2. Change Username / Password Auth. Only from No to Yes
  3. Change the **VPN Subnet / Netmask, keeping the default is acceptable.
  4. Add client routers into Allowed Clients.
    • username: router1, subnet:, mask:
    • username: router2, subnet:, mask:

Then you need to export client’s config. Change VPN Details from Advanced Settings to General. And click export of Export OpenVPN configuration file. You will get a config file named client.ovpn by default. Then open this file and look at the first line. By default it will look like this:

remote public ip)

Change to your DDNS domain/url like

Config on client

It’s easy on client’s side. Just go to VPN page choose VPN Client -> Add profile -> Input Username, Password and upload your modified client.ovpn. Click OK and if this file is not activated click Activate

In the end

Everything is done, enjoy your custom sd-wan.

If you encounter any problems following this tutorial, feel free to leave a comment.

Redisson distributed lock watch dog mechanism

Found this exception when using Redisson to implement the distributed lock:

java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: d8fa7ae5-3152-468a-8793-6102b512df68 thread-id: 1

It happens when trying to unlock a lock that is already unlocked.
It can be reproduced by the codes below.

public void reproduce() throws InterruptedException {
    RLock lock = redissonClient.getLock("reproduce");
    }catch (IllegalMonitorStateException e){

For the sake of code robustness, we need to catch this exception when unlocking a lock.

public void fix() throws InterruptedException {
    RLock lock = redissonClient.getLock("fix");
    try {
    }catch (IllegalMonitorStateException e){
        System.out.println("Lock has expired already.");

The code above shows us the problem when setting a lock with a specific timeout value. But if we don’t set a timeout value, then this lock won’t be released by default from redisson. The Redis locks with no expiration time may cause deadlock issues when the thread has some exceptions. Redisson provides us with a watchdog to avoid the deadlock issue. It makes codes a lot more simple for us. No need to worry about the deadlock issue. The code below shows us the source code of the mechanism of the watchdog.

public void watchDog() throws InterruptedException {
    RLock lock = redissonClient.getLock("watchDog");
    for (int i = 0; i < 30; i++) {

The output is:


We can see that although the code does not explicitly specify the lock expiration time, Redisson sets it to 30 seconds by default, and then every 10 seconds, the lock expiration time will be renewed for 10 seconds, that is, as long as the lock-holding thread does not release the lock voluntarily or an exception occurs, then the lock will not be released. This is guaranteed by Redisson’s watchdog mechanism.

We can get this code from Redisson config file:

    * This parameter is only used if lock has been acquired without leaseTimeout parameter definition. 
    * Lock expires after <code>lockWatchdogTimeout</code> if watchdog 
    * didn't extend it to next <code>lockWatchdogTimeout</code> time interval.
    * <p>  
    * This prevents against infinity locked locks due to Redisson client crush or 
    * any other reason when lock can't be released in proper way.
    * <p>
    * Default is 30000 milliseconds
    * @param lockWatchdogTimeout timeout in milliseconds
    * @return config
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
    this.lockWatchdogTimeout = lockWatchdogTimeout;
    return this;

As you can see, the watchdog mechanism only intervenes when leaseTimeout is not set, as we can see from the source code below:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                if (res) {
                    // reschedule itself
                } else {
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);


By default, the lock will be renewed after internalLockLeaseTime / 3, TimeUnit.MILLISECONDS, which is 30/3 seconds after the default setting.

PostgreSQL Performance Test Using UUID as Primary Key

Environmental preparation

In order to compare the difference between HDD and SSD, the HDD and SSD are tested separately, and the machine is an ESXi virtual machine with the same configuration:

– CPU:2 cores
– RAM:4G
– Disk:32G(HDD/SSD)from H710 card with 1G cache
– PostgreSQL:psql (10.20 (Ubuntu 10.20-1.pgdg20.04+1))

Create table

--Unordered uuid
pgbenchdb=# create table test_uuid_v4(id char(32) primary key);
--Ordered uuid
pgbenchdb=# create table test_time_nextval(id char(32) primary key);
--Increasing sequence
pgbenchdb=# create table test_seq_bigint(id int8 primary key);
--Creating sequence
create sequence test_seq start with 1 ;

Test file

--Testing the unordered uuid
vi pgbench_uuid_v4.sql
insert into test_uuid_v4 (id) values (replace(uuid_generate_v4()::text,'-',''));
--Testing ordered uuid
vi pgbench_time_nextval.sql
insert into test_time_nextval (id) values (replace(uuid_time_nextval()::text,'-',''));
--Test sequence
vi pgbench_seq_bigint.sql
insert into test_seq_bigint (id) values (nextval('test_seq'::regclass));

Run test

pgbench -M prepared -r -n -j 8 -c 8 -T 60 -f ./pgbench_uuid_v4.sql -U sa pgbenchdb

pgbenchdb=# insert into test_uuid_v4 (id) select replace(uuid_generate_v4()::text,'-','') from generate_series(1,1000000);
INSERT 0 1000000
Time: 43389.817 ms (00:43.390)
pgbenchdb=# insert into test_time_nextval (id) select replace(uuid_time_nextval()::text,'-','') from generate_series(1,1000000);
INSERT 0 1000000
Time: 30585.134 ms (00:30.585)
pgbenchdb=# insert into test_seq_bigint select generate_series (1,1000000);
INSERT 0 1000000
Time: 9818.639 ms (00:09.819)
Unordered uuid insertion for 100w takes 43s, ordered takes 30s, and sequential takes 10s.