Implementing distributed locks based on redis

Article directory

  • Implementing distributed locks based on redis
    • Basic implementation
    • Anti-deadlock
    • Prevent accidental deletion
    • Atomicity cannot be guaranteed in high concurrency scenarios.
    • Use Lua to ensure deletion atomicity
  • Encapsulate the redis lock into a method

Implementing distributed locks based on redis

Basic implementation

With the help of the command setnx(key, value) in redis, if the key does not exist, it will be added, and if it exists, nothing will be done. If multiple clients send the setnx command at the same time, only one client can succeed and return 1 (true); other clients return 0 (false).

  1. Multiple clients acquire locks at the same time (setnx)
  2. Acquisition is successful, business logic is executed, and the lock is released after execution is completed (del)
  3. Other clients are waiting to retry

Transform the StockService method:

@Service
public class StockService {<!-- -->

    @Autowired
    private StockMapper stockMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void deduct() {<!-- -->
        // Lock setnx
        Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
        //Retry: recursive call
        if (!lock){<!-- -->
            try {<!-- -->
                Thread.sleep(50);
                this.deduct();
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
            }
        } else {<!-- -->
            try {<!-- -->
                // 1. Query inventory information
                String stock = redisTemplate.opsForValue().get("stock").toString();

                // 2. Determine whether the inventory is sufficient
                if (stock != null & amp; & amp; stock.length() != 0) {<!-- -->
                    Integer st = Integer.valueOf(stock);
                    if (st > 0) {<!-- -->
                        // 3. Deduct inventory
                        redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                    }
                }
            } finally {<!-- -->
                // Unlock
                this.redisTemplate.delete("lock");
            }
        }
    }
}

Among them, locking can also use loops:

// Lock, if failed to acquire the lock, try again
while (!this.redisTemplate.opsForValue().setIfAbsent("lock", "111")){<!-- -->
    try {<!-- -->
        Thread.sleep(40);
    } catch (InterruptedException e) {<!-- -->
        e.printStackTrace();
    }
}

Unlock:

// Release lock
this.redisTemplate.delete("lock");

The stress test using Jmeter is as follows:

Anti-deadlock


Problem: setnx has just acquired the lock, and the current server is down, causing del to release the lock and unable to execute it, which in turn causes the lock to be unable to be released (deadlock)

Solution: Set the expiration time for the lock and automatically release the lock.

There are two ways to set the expiration time:

  1. Set the expiration time through expire (lack of atomicity: if an exception occurs between setnx and expire, the lock cannot be released)
  2. Use the set command to set the expiration time: set key value ex 3 nx (not only achieves the effect of setnx, but also sets the expiration time)

Prevent accidental deletion

Problem: Locks on other servers may be released.

Scenario: If the execution time of business logic is 7 seconds. The execution process is as follows

  1. The index1 business logic has not been executed, and the lock is automatically released after 3 seconds.

  2. index2 acquires the lock, executes business logic, and the lock is automatically released after 3 seconds.

  3. index3 acquires the lock and executes business logic

  4. The execution of the index1 business logic is completed, and del is called to release the lock. At this time, the lock of index3 is released, causing the index3 business to be released by others after only executing for 1 second.

    In the end, it equals no lock.

Solution: When setnx acquires the lock, set a specified unique value (for example: uuid); obtain this value before releasing to determine whether it is your own lock

The implementation is as follows:

Problem: Delete operations lack atomicity.

Scenes:

  1. When index1 performs deletion, the queried lock value is indeed equal to the uuid.
  2. Before index1 is deleted, the lock expiration time has expired and is automatically released by redis.
  3. index2 acquired the lock
  4. Index1 executes deletion, and the lock of index2 will be deleted.

Solution: There is no single command that can judge + delete at the same time, so it can only be achieved through other methods (LUA script)

Atomicity cannot be guaranteed in high concurrency scenarios

Redis uses a single-threaded architecture, which can guarantee the atomicity of a single command, but cannot guarantee the atomicity of a group of commands in a high-concurrency scenario. For example:

In the serial scenario: the values of A and B must both be 3

In a concurrent scenario: the values of A and B may be between 0-6.

In the extreme case 1:

Then the result of A is 0 and the result of B is 3

Limit case 2:

Then the results of A and B are both 6

If the redis client sends three commands to the redis server at one time through the Lua script, then these three commands will not be interrupted by other client commands. Redis also guarantees that scripts will be executed in an atomic manner: while a script is running, no other scripts or Redis commands will be executed. This is very similar to using MULTI/EXEC wrapped transactions.

However, the MULTI/EXEC method uses the transaction function to package and execute a set of commands, and cannot perform business logic operations. During this period, if an error is reported when executing a certain command (such as incrementing a string), other commands will still be executed and will not be rolled back.

Use lua to ensure deletion atomicity

Remove LUA script:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

Code:

public void deduct() {<!-- -->
    String uuid = UUID.randomUUID().toString();
    // Lock setnx
    while (!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)) {<!-- -->
        //Retry: loop
        try {<!-- -->
            Thread.sleep(50);
        } catch (InterruptedException e) {<!-- -->
            e.printStackTrace();
        }
    }
    try {<!-- -->
        // this.redisTemplate.expire("lock", 3, TimeUnit.SECONDS);
        // 1. Query inventory information
        String stock = redisTemplate.opsForValue().get("stock").toString();

        // 2. Determine whether the inventory is sufficient
        if (stock != null & amp; & amp; stock.length() != 0) {<!-- -->
            Integer st = Integer.valueOf(stock);
            if (st > 0) {<!-- -->
                // 3. Deduct inventory
                redisTemplate.opsForValue().set("stock", String.valueOf(--st));
            }
        }
    } finally {<!-- -->
        // First determine whether it is your own lock, and then unlock it
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
            "then " +
            " return redis.call('del', KEYS[1]) " +
            "else " +
            " return 0 " +
            "end";
        this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock"), uuid);
    }
}

Encapsulate the redis lock into a method

/**
 * @Author: hrd
 * @CreateTime: 2023/10/20 14:57
 * @Description:
 */

public interface Lock {<!-- -->
    /**
     * Get the lock automatically released after 30 seconds by default
     * @param key business key is named according to your own business
     * @param code unique identifier
     */
    default void get(String key, String code) {<!-- -->
        get(key,code,30);
    }

    /**
     * Get the
     * @param key business key is named according to your own business
     * @param code unique identifier
     * @param timeout expiration time unit: seconds
     */
    void get(String key, String code, long timeout);



    /** Release lock
     * @param key business key is named according to your own business
     * @param code unique identifier
     */
    void release(String key, String code);


}
import com.common.star.base.abs.lock.Lock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @Author: hrd
 * @CreateTime: 2023/10/20 14:58
 * @Description:
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class RedisLock implements Lock {<!-- -->

    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * Lua script locking
     */
    public static final String LUA_SCRIPT_LOCK = "if redis.call('set',KEYS[1],ARGV[1],'EX',ARGV[2],'NX') then return 1 else return 0 end";
    /**
     * lua script unlock
     */
    public static final String LUA_SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Override
    public void get(String key,String code,long timeout) {<!-- -->
        if (key == null) {<!-- -->
            throw new NullPointerException("redis key cannot be empty");
        }
        while (!Boolean.TRUE.equals(redisTemplate.execute(new DefaultRedisScript<>(LUA_SCRIPT_LOCK,Boolean.class),List.of(key), code, timeout, TimeUnit.SECONDS))) {<!-- -->
            log.info("Trying to acquire lock----------------------{}", key);
            //Retry: loop
            try {<!-- -->
                Thread.sleep(50);
            } catch (InterruptedException e) {<!-- -->
                Thread.currentThread().interrupt();
                log.info("Interrupted!", e);
            }
        }
        log.info("Successfully acquired lock-----------------{}", key);
    }

    @Override
    public void release(String key,String code) {<!-- -->
        if (key == null) {<!-- -->
            return;
        }
        log.info("Release lock-----------------{}", key);
        this.redisTemplate.execute(new DefaultRedisScript<>(LUA_SCRIPT_UNLOCK, Boolean.class), List.of(key), code);
    }
}