Redis Combat – Dark Horse Review Project – Distributed Lock

1. Introduction

(1) Basic principles and characteristics

In cluster mode, multiple jvms will have multiple lock monitors, so the lock added to a certain thread will not be seen by threads under other jvms.

The distributed lock will enable multiple jvms to share a lock monitor, making a lock visible to multiple processes.

Distributed lock: A lock that is visible and mutually exclusive to multiple processes in a distributed system or cluster mode.

(2) Implementation method

There are three common

2. Distributed lock based on redis

Use the setnx operation of redis to set a lock that is automatically released after a few seconds, the primary version of the distributed lock

However, such logic may also cause the problem of accidental deletion of locks, as shown in the figure

When thread 1 is blocked for too long for some reason, causing the lock to be released early, thread 2 gets the lock and starts to execute the task. If thread 1 ends the blocking and starts to execute the task and releases the lock, thread 2 will Lost lock. At this time, thread 3 gets the lock, and threads 2 and 3 operate at the same time.

Therefore, it is necessary to perform thread judgment before releasing the lock. Only when the lock is the lock of the current thread, the current thread can release the lock.

When storing the thread ID, in order to solve the problem of thread ID conflict, uuid is used as the thread ID.

In addition, thread blocking may also occur between [judging whether it is your own lock] and [releasing the lock], which will also cause a similar problem to the previous one. as shown in the picture

When I thought everything was going well, after judging that it was my own lock, just about to release it, thread 1 was blocked. In this way, the blocking time is too long, beyond the automatic release time of the lock expiration, thread 2 gets the lock and starts to execute the task. If at this time, thread 1 ends blocking, it will immediately release the lock indiscriminately, and thread 3 will take advantage of it and run in parallel with thread 2.

In order to make the operations of [judging whether it is your own lock] and [between releasing the lock] atomic, I first thought of redis transactions, but redis transactions are actually a kind of batch processing. If you put these two in one transaction, it may will cause the release to fail.

A better solution is: use Lua script (Lua is a lightweight and compact scripting language).

So, how to call redis in Lua language? In fact, Lua has strong scalability and provides a redis-specific interface to call redis. like

-- Execute set name jack
redis.call('set','name','jack')

For more information, please check the rookie tutorial: Lua tutorial | rookie tutorial

The process of analyzing and releasing the lock is mainly: get the thread id in the current lock –> get the current thread id –> compare the same –> release the lock. write lua script

-- KEYS[1] here is the key of the lock, and ARGV[1] here is the current thread flag
-- Obtain the mark in the lock, and judge whether it is consistent with the current thread mark
if (redis. call('GET',KEYS[1]) == ARGV[1]) then
-- Consistent, delete the lock
return redis. call('DEL', KEYS[1])
end
-- If inconsistent, return directly
return 0

The Redis script uses the Lua interpreter to execute the script, and the common command to execute the script is EVAL.

Then in the java code, the api corresponding to eval is execute in stringRedisTemplate.

【Define interfaces and tool classes】

public interface ILock {
    boolean tryLock(long timeoutSec);
    void unlock();
}
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate){
        this.name=name;
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }//Initialization script

    @Override
    public boolean tryLock(long timeoutSec) {

        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
        //If you directly return the success of the Boolean type, it will be unboxed, which will have a security risk
    }

//Use lua script to make the release lock atomic
    @Override
    public void unlock() {
        //load lua script
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),//The name of the current lock, passed into the script, get the thread id in the lock
                ID_PREFIX + Thread.currentThread().getId()//current thread id
                );
    }

//The original does not have atomic release lock operation
    /* @Override
    public void unlock() {
        // Get the current thread id
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //Get the thread id in the current lock
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //Judge whether the current thread id is consistent with the thread id stored in the current lock
        if(threadId. equals(id)){
            //freed
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }*/
}

Modify the original order code of flash coupons, call the distributed lock, and lock the order creation business.

 //Coupons are enough to create an order
        Long userId = UserHolder. getUser(). getId();
        //Create redis distributed lock instead of pessimistic lock
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        boolean isLock = lock. tryLock(1200);
        if(!isLock){
            //failed to acquire lock
            return Result.fail("Cannot place repeated orders");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext. currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock. unlock();
        }