Database implementation of distributed lock

Database implementation of distributed lock

What is a distributed lock

In a single-instance single-process system, when multiple threads modify a shared variable at the same time, in order to ensure thread safety, it is necessary to synchronize the variable or code. This kind of synchronization operation can use synchronized and JUC packages in java. Under the explicit lock, cas+volatile to achieve.

At present, most systems are deployed in a distributed manner. Manual use such as synchronized can only ensure thread safety within a single process. Thread safety under multiple processes and multiple instances requires distributed locks.

There are currently four mainstream distributed lock solutions:

  • Based on database implementation (pessimistic + optimistic)
  • Based on Redis
  • Based on ZooKeeper
  • Based on Etcd

Pessimistic lock of database realizes distributed lock

Create table sql:

CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `amount` int(11) NOT NULL,
  `status` int(11) NOT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

INSERT INTO `t_order` (`amount`, `status`, `version`) VALUES ('100', '1', '1');

First use sql to simulate the realization of distributed locks:

step SessionA SessionB
1 begin; begin;
2 select * from t_order where id=1 for update;
3 select * from t_order where id=1 for update; – 阻塞
4 update t_order set status=2 where id=1 and status=1;
5 commit; Return query result
6 update t_order set status=2 where id=1 and status=1; – the status has changed but the update is not successful
7 commit;

Description:

  1. Client A and client B execute the first two rows of sql at the same time, client A returns data, and client B blocks waiting for the row lock to be acquired.
  2. Client A executes the next two rows of SQL, commits the transaction, and client B obtains the row lock and immediately returns the data.
  3. Client B executes the next two lines of SQL, commits the transaction, and releases the row lock.

Note that the for updatestatement must take the primary key index, otherwise the entire table will be locked if the index is not taken, and gap locks will be generated if other indexes are taken, which may lock multiple records.

Java code implementation:

package com.morris.distribute.lock.database.exclusive;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {
    
    

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 数据库分布式锁之悲观锁
     *
     * @param id
     */
    @Transactional
    public void updateStatus(int id) {
    
    
        log.info("updateStatus begin, {}", id);

        Integer status = jdbcTemplate.queryForObject("select status from t_order where id=? for update", new Object[]{
    
    id}, Integer.class);

        if (Order.ORDER_STATUS_NOT_PAY == status) {
    
    

            try {
    
    
                // 模拟耗时操作
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            int update = jdbcTemplate.update("update t_order set status=? where id=? and status=1", new Object[]{
    
    2, id, Order.ORDER_STATUS_NOT_PAY});

            if (update > 0) {
    
    
                log.info("updateStatus success, {}", id);
            } else {
    
    
                log.info("updateStatus failed, {}", id);
            }
        } else {
    
    
            log.info("updateStatus status already updated, ignore this request, {}", id);
        }
        log.info("updateStatus end, {}", id);
    }
}

Pay attention to opening the transaction @Transactional.

Use multiple threads to simulate competing locks:

package com.morris.distribute.lock.database.exclusive;

import com.morris.distribute.config.JdbcConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.IntStream;

public class Demo {
    
    

    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(JdbcConfig.class);
        applicationContext.register(OrderService.class);
        applicationContext.refresh();

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        IntStream.rangeClosed(1, 3).forEach((i) -> new Thread(() -> {
    
    
            OrderService orderService = applicationContext.getBean(OrderService.class);
            try {
    
    
                cyclicBarrier.await();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
    
    
                e.printStackTrace();
            }
            orderService.updateStatus(1);
        }, "t" + i).start());
    }
}

The results are as follows:

2020-09-16 14:16:53,248  INFO [t2] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248  INFO [t1] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248  INFO [t3] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:56,289  INFO [t2] (OrderService.java:42) - updateStatus success, 1
2020-09-16 14:16:56,289  INFO [t2] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,290  INFO [t3] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,290  INFO [t3] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,291  INFO [t1] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,291  INFO [t1] (OrderService.java:49) - updateStatus end, 1

It can be seen from the running results that only one thread holds the lock at the same time.

Optimistic lock of database realizes distributed lock

Optimistic locking uses the version number to determine whether the record has been updated every time.

package com.morris.distribute.lock.database.share;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {
    
    

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 数据库分布式锁之乐观锁
     *
     * @param id
     */
    public void updateStatus(int id) {
    
    

        log.info("updateStatus begin, {}", id);
        for (;;) {
    
     // 自旋,有可能对订单做其他操作,导致version变了,所以需要自旋
            Order order = jdbcTemplate.queryForObject("select status, version from t_order where id=?",
                    new Object[]{
    
    id}, (rs, row) -> {
    
    
                        Order o = new Order();
                        o.setStatus(rs.getInt(1));
                        o.setVersion(rs.getInt(2));
                        return o;
                    });

            if (Order.ORDER_STATUS_NOT_PAY == order.getStatus()) {
    
    

                try {
    
    
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }

                int update = jdbcTemplate.update("update t_order set status=?,version=? where id=? and version=? and status=?",
                        new Object[]{
    
    Order.ORDER_STATUS_PAY_SUCCESS, order.getVersion() + 1, id, order.getVersion(), Order.ORDER_STATUS_NOT_PAY});

                if (update > 0) {
    
    
                    log.info("updateStatus success, {}", id);
                    break;
                } else {
    
    
                    log.info("updateStatus failed, {}", id);
                }
            } else {
    
    
                log.info("updateStatus status already updated, ignore this request, {}", id);
                break;
            }
        }
        log.info("updateStatus end, {}", id);
    }

}

The results are as follows:

2020-09-16 14:21:08,934  INFO [t3] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934  INFO [t2] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934  INFO [t1] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:12,110  INFO [t1] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,110  INFO [t2] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,111  INFO [t3] (OrderService.java:47) - updateStatus success, 1
2020-09-16 14:21:12,111  INFO [t3] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117  INFO [t2] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117  INFO [t1] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117  INFO [t1] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117  INFO [t2] (OrderService.java:57) - updateStatus end, 1

to sum up

Pessimistic lock: It will lock the entire row of records, resulting in the inability to perform other business operations on the data, and it is inefficient. If the sql is not written well, a gap lock may occur, locking multiple records, or even the entire table.

Optimistic locking: Every table needs to add a version field that has nothing to do with business.

Advantages: Directly based on the database implementation, easy to implement.

Disadvantages: IO overhead is large, the number of connections is limited, and it cannot meet the needs of high concurrency.

Guess you like

Origin blog.csdn.net/u022812849/article/details/108621556