The problem

(1) What is distributed lock?

(2) Why are distributed locks needed?

(3) how does mysql implement distributed lock?

(4) Advantages and disadvantages of mysql distributed lock?

Introduction to the

With the increasing amount of concurrency, the single-machine service will evolve to multi-node or micro-service sooner or later. At this time, synchronized or ReentrantLock used in the original single-machine mode will no longer be applicable. We urgently need a solution to ensure thread safety in distributed environment. Mysql distributed lock distributed lock distributed thread safety

Basic knowledge of

Mysql provides two functions — get_lock(‘key’, timeout) and release_lock(‘key’) — to implement a distributed lock. The lock can be assigned by key, which is a string. Seconds) to release the lock when release_lock(‘key’) is called or the client is disconnected.

They can be used as follows:

mysql> select get_lock('user_1', 10);
    -> 1
mysql> select release_lock('user_1'); - > 1Copy the code

Get_lock (‘user_1’, 10) returns 1 if the lock has been acquired within 10 seconds, 0 otherwise;

Release_lock (‘user_1’) Returns 1 if the lock is held by the current client, 0 if the lock is held by another client, and null if the lock is not held by any client.

Multiple client cases

In order to facilitate the example [this article by “Tong Elder brother read source code” original, please support the original, thank you!] , where the timeout is set to 0, that is, return immediately.

moment The client A Client B
1 get_lock(‘user_1’, 0) -> 1
2 get_lock(‘user_1’, 0) -> 0
3 release_lock(‘user_1’, 0) -> 0
4 release_lock(‘user_1’, 0) -> 1
5 release_lock(‘user_2’, 0) -> null
6 get_lock(‘user_1’, 0) -> 1
7 release_lock(‘user_1’, 0) -> 1

Java implementation

For quick implementation, spring Boot2.1 + Mybatis is used here, and the Spring configuration is omitted, listing only the main classes.

Defining the Locker interface

There is only one method in the interface. Input parameter 1 is the key to lock, and input parameter 2 is the command to execute.

public interface Locker {
    void lock(String key, Runnable command);
}
Copy the code

Mysql distributed lock implementation

Note the following points when implementing mysql:

(1) Lock adding and lock releasing must be in the same session (the same client), so it cannot be called in the way of Mapper interface, because Mapper interface may cause different sessions.

(2) Reentrancy is guaranteed by ThreadLocal;

@Slf4j
@Component
public class MysqlLocker implements Locker {

    private static final ThreadLocal<SqlSessionWrapper> localSession = new ThreadLocal<>();

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void lock(String key, Runnable command) {
        // Lock and release must use the same session
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        if (sqlSessionWrapper == null) {
            // Get the lock for the first time
            localSession.set(new SqlSessionWrapper(sqlSessionFactory.openSession()));
        }
        try {
            // this article is by "Tong Elder brother read source code" original, please support the original, thank you!
            // -1 indicates that the user waits until the lock is obtained
            if (getLock(key, -1)) { command.run(); }}catch (Exception e) {
            log.error("lock error", e);
        } finally{ releaseLock(key); }}private boolean getLock(String key, long timeout) {
        Map<String, Object> param = new HashMap<>();
        param.put("key", key);
        param.put("timeout", timeout);
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.getLock", param);
        if(result ! =null && result.intValue() == 1) {
            // If the lock is acquired, state is increased by 1
            sqlSessionWrapper.state++;
            return true;
        }
        return false;
    }

    private boolean releaseLock(String key) {
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.releaseLock", key);
        if(result ! =null && result.intValue() == 1) {
            // The lock is released successfully, state is reduced by 1
            sqlSessionWrapper.state--;
            // When state decreases to 0, all locks acquired by the current thread are released, closing the session and removing the session from the ThreadLocal
            if (sqlSessionWrapper.state == 0) {
                sqlSessionWrapper.sqlSession.close();
                localSession.remove();
            }
            return true;
        }
        return false;
    }

    private static class SqlSessionWrapper {
        int state;
        SqlSession sqlSession;

        public SqlSessionWrapper(SqlSession sqlSession) {
            this.state = 0;
            this.sqlSession = sqlSession; }}}Copy the code

LockerMapper.xml

Define statements for get_lock() and release_lock().

<?xml version="1.0" encoding="UTF-8"? >

      

<mapper namespace="LockerMapper">
    <select id="getLock" resultType="integer">
        select get_lock(#{key}, #{timeout});
    </select>

    <select id="releaseLock" resultType="integer">
        select release_lock(#{key})
    </select>
</mapper>
Copy the code

The test class

Here we start 1000 threads, each of which prints a sentence and sleeps for 2 seconds.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MysqlLockerTest {

    @Autowired
    private Locker locker;

    @Test
    public void testMysqlLocker(a) throws IOException {
        for (int i = 0; i < 1000; i++) {
            // Multi-node test
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()->{
                locker.lock("lock", () - > {// Reentrant test
                    locker.lock("lock", ()-> {
                        System.out.println(String.format("time: %d, threadName: %s", System.currentTimeMillis(), Thread.currentThread().getName()));
                        try {
                            Thread.sleep(2000);
                        } catch(InterruptedException e) { e.printStackTrace(); }}); }); },"Thread-"+i).start(); } System.in.read(); }}Copy the code

The results

The lock is valid if a thread is printed every 2 seconds. In a distributed environment, it is easy to use multiple instances of MysqlLockerTest.

time: 1568715905952, threadName: Thread-3
time: 1568715907955, threadName: Thread-4
time: 1568715909966, threadName: Thread-8
time: 1568715911967, threadName: Thread-0
time: 1568715913969, threadName: Thread-1
time: 1568715915972, threadName: Thread-9
time: 1568715917975, threadName: Thread-6
time: 1568715919997, threadName: Thread-5
time: 1568715921999, threadName: Thread-7
time: 1568715924001, threadName: Thread-2
Copy the code

conclusion

(1) Distributed lock is needed in distributed environment, and a single lock cannot ensure thread safety;

Mysql distributed lock is based on get_lock(‘key’, timeout) and release_lock(‘key’).

Mysql distributed lock is reentrant lock;

eggs

What do I need to pay attention to when using mysql distributed locks?

A: You must ensure that multiple service nodes use the same mysql library. .

What are the advantages of mysql distributed locks?

A: 1) Convenient and fast, because almost every service will connect to the database, but not every service will use Redis or ZooKeeper;

2) If the client is disconnected, the lock will be released automatically, so that the lock will not be occupied all the time;

3) Mysql distributed lock is reentrant lock, low cost for old code transformation;

What are the disadvantages of mysql distributed locks?

Answer: 1) Lock directly to the database, increasing the pressure of the database;

2) The locked thread will occupy one session, that is, one connection number. If the concurrency is large, normal SQL statements may fail to obtain connections;

3) If each service uses its own database after the service split, it is not appropriate;

4) Compared with Redis or ZooKeeper distributed lock, the efficiency is relatively lower;


Welcome to pay attention to my public number “Tong Elder brother read source code”, view more source code series articles, with Tong elder brother tour the ocean of source code.